나의 작은 valley
[딥러닝] 성능 고도화 본문
딥러닝 모델 성늘 고도화 방법론들에 대해서 알아보자.
Drop Out
학습에 사용할 Node 중 랜덤으로 노이즈를 주어 학습이 진행되지 않게 한다. 학습이 완료시면 여러 모델을 앙상블시킨 것과 비슷한 효과가 나온다. 학습 때 뉴런을 무작위로 삭제하는 행위가 마치 매번 다른 모델을 학습시키는 것과 유사하기 때문이다.
구현
정규화
1) eature scalling
: 값의 범위를 맞추는 작업이다. 보통 [0,1] 사이로 맞춘다.
2) Batch Normalization
-motive) Vanishing Gradient Problem을 해결하기 위함. Vanishing Gradient Problem 이란 다음과 같다. 역전파 과정을 보면 미분을 하는 과정이 있다. activaton_function으로 sigmoid 함수를 사용할 경우 x값이 양 끝으로 갈수록 미분 값은 0에 수렴한다. 미분 값이 작아지면 파라미터 업데이트 정도가 낮아진다. 따라서 layer수가 많은 경우 뒤로 갈수록 업데이트가 급속도로 느려지는 문제를 말한다.
-functioning) 우선 batch 단위로 들어온 랜덤한 데이터들의 평균과 분산을 구한다. 각 데이터 마다 평균을 빼고 분산으로 나누면 전체 데이터들은 평균이 0이고 분산이 1인 분포를 따르게 된다. 이 분포를 x_i라고 하면 batch_normalization의 결과는
a*x_i + b가 된다. 이떄 a,b는 하이퍼 파라미터로 업데이트의 대상이다.
- meaning) 평균이 0이고 분산이 1인 분포로 만들어주는 작업은 랜덤한 데이터들을 모아서 중간 지점(x=0)에 뿌려주는 것을 의미한다. 이를 통해 미분 값이 0과 유사해지는 경우를 방지할 수 있다. 다만 무조건 0에만 뿌려주면 actvation_function을 사용하는 의미가 없어진다. linear한 상황이 되기 떄문이다. 그렇기에 h_parms를 부여해서 우선 0근처로 뿌려주되 이후 적당히 loss를 줄여주는 방향으로 분포를 이동시키는 작업을 수행한다.
-test dataset의 경우) test를 하는 경우에는 평균 값과 분산 값은 batch들의 평균들의 평균, 분산들의 평균을 사용한다.
-postion) 활성화 함수에 넣어주기 전에 적용한다.
-구현
class BatchNormalization:
def __init__(self, gamma, beta, momentum=0.9, running_mean = None, running_var = None) -> None:
self.gamma = gamma
self.beta = beta
self.momentum = momentum
#입력 데이터의 형태를 저장하기 위한 변수
self.input_shape = None
#테스트 상황에 사용할 평균과 분산
self.running_mean = running_mean
self.running_var = running_var
#역전파 과정에서 사용할 변수들
self.batch_size = None
self.xc = None
self.std = None
self.dgamma = None
self.dbeta = None
일단 배치 정규화 예시에서 사용한 a,b가 gamma와 beta를 의미한다. momentum은 모든 데이터의 전반적인 평균, 분산을 구할 때 사용되는 값이다. input_shape는 입력 데이터의 형태를 저장하는데 반환할 때 원본 형태로 reshape 해주기 위해 사용된다.
running_mean/var가 되게 재밌는 변수들인데 테스트 상황에서는 보통 입력 데이터가 1개이다. 이 경우 mean/var를 구할 수 없기에 훈련 기간동안 running_mean/var을 업데이트하고 테스트 상황에 사용한다.
def forward(self,x,train_flag = True):
# 4D 텐서의 경우
if x.dim == 4:
N, C, H, W = x.shape
x = x.reshape(N,-1)
if self.running_mean is None:
N, D = x.shape
self.running_mean = np.zeros(D)
self.running_var = np.zeros(D)
if train_flag:
mu = x.mean(axis=0)
xc = x - mu
var = np.mean(xc**2, axis=0)
std = np.sqrt(var + 10e-7)
xn = xc / std
#중간 값을 저장
self.batch_size = x.shape[0]
self.xc = xc
self.xn = xn
self.std = std
self.running_mean = self.momentum * self.running_mean + (1-self.momentum) * mu
self.running_var = self.momentum * self.running_var + (1-self.momentum) * var
else:
xc = x - self.running_mean
xn = xc / ((np.sqrt(self.running_var + 10e-7)))
output = self.gamma * xn + self.beta
return output.reshape(*self.input_shape)
순전파의 코드이다. 설명했던 대로 평균을 구해서 데이터에서 빼주고 분산(std)를 구해서 나눠 xn을 만든다. running_mean/var이 어떠한 방식으로 업데이트 되는지를 유의깊게 보자.
advantage)
배치 정규화를 사용하면 GVP이 해결이 된다. 이전에는 layer 위치에 따라서 업데이트 되는 정도가 lr가 크면 컸어서 lr를 작게 설정해주어야 했는데 이제 배치 정규화를 사용하면 그런 문제가 해결되기에 맘놓고 lr를 크게 잡아도 된다.
3) 레이어 정규화
: Serial 데이터의 경우 batch 단위로 끊어주면 의미를 잃는 경우가 생김. ex) 문장
-> batch 단위가 아닌 Layer 별로 끊어주자
-> 대부분의 경우: 배치 정규화 / Serial 데이터 : 레이어 정규화 / 이미지 변환 : 인스턴스 정규화 / 나머지 : 그룹 정규화
가중치 초기화
아무렇게나 하이퍼 파라미터들을 초기화하면 아무 값이나 나온다...어떻게하면 효과적으로 가중치를 초기화 할 수 있을까.
-> 역사적인 내용은 생략
1) Xavier 초기화 <- 표준편차가 1/ np.sqrt(n)인 정규 분포로 초기화. 이렇게하면 적당히 넓게 고루 분포된 값을 초기 가중치 값들로 사용할 수 있다.
2) He 초기화 <- Xavier 방식이 다 좋은데 특정 활성화 함수들의 경우 미분값이 음수일 때 0이여서 layer가 깊어질수록 기울기 소실이 발생할 수 있다는 단점이 있다.이러한 단점을 해소하기 위해 가중치의 분포를 더 넓게(np.sqrt(2/n))한 정규 분포로 초기화하는 방식이 He 초기화이다.
cf) He(허)라고 읽는 사람들이 많더라고요 ㅋㅋ
-구현
class Net:
def __init__(self):
"""생략"""
def __init_weight(self, weight_init_std):
all_size_list = [self.input_size] + self.hidden_size_list + [self.output_size]
for idx in range(1,all_size_list+1):
scale = weight_init_std
if str(weight_init_std).lower() in ('relu','he'):
scale = np.sqrt( 2.0 / all_size_list[idx -1])
elif str(weight_init_std).lower() in ('sigmoid','xavier'):
scale = np.sqrt( 1.0 / all_size_list[idx -1])
#h_parms 초기화
self.params[f'W{idx}'] = scale * np.random.randn(all_size_list[idx-1],all_size_list[idx])
self.params[f'b{idx}'] = np.zeros(all_size_list[idx])
conclusion) 보통 a_funciont으로 sigmoid를 쓰면 Xavier 초기화를 하고 LeLU를 쓰면 He 초기화를 한다고 함.
가중치 감쇠
가능하다면 동일한 Loss라는 가정 하에 가중치가 낮은게 더 좋다는 직관을 기반으로 탄생한 기법.
Loss <- Loss + ratio_value * sigma(W^2)
학습 조기 종료
Training Loss가 감소함에도 너무 과하면 overfiiting이 일어나는데 이를 valudation Loss가 증가함을 근거로 알아차릴 수 있다. overfitting이 일어나면 조기 종료를 수행하도록 설정 가능하다.
학습 스케줄러
학습 스케줄러는 lr을 동적으로 조절하는 역활을 수행한다. 적절한 lr는 학습 속도를 빠르게하고 local_min_problem에서 벗어나게 도와준다.
- constant
: 초기에 설정한 학습률을 전체 과정에서 변경하지 않는다.
- step decay
: 일정한 주기마다 감소
- exponential decay
: 지수 함수적으로 감소
최적화 알고리즘
예측값과 실제 값의 차이(loss)를 줄여주는 방향으로 파라미터를 업데이트하는 방식을 최적화 알고리즘이라고 한다. 마치 a에서 b로 가는 길을 찾는 과정이라고 생각하면 된다. 문제는 전체 지도를 모르고 있다는 점이다. 알고 있는 것은 현재 loss 값과 그 loss 값에서의 기울기 값이다.
두 정보만을 이용해 탄생한 최초의 최적화 알고리즘이 Gradient Desent 알고리즘이다. 이에 대한 설명은 기존 포스팅에서 했으니 생략한다. 여하튼 파라미터를 업데이트 할 때 고려해야 될 두가지 요소가 있는데 바로 방향과 step size이다.
cf) SGD 구현
- 방향
현재 기울기에 이전까지 이동한 정보(관성. momentum)을 추가하여극솟값에 빠지는 상황을 방지할 수 있다.
문제는 local_min을 탈출한 것처럼 global_min 값도 탈출가능하다.
- 구현
Nesterow Accelerated GD
: momentum step 이후 gradient 업데이트 및 이동
-> 현재의 momentum 정보를 사용하기에 momentum에 과하게 의존하는 단점을 완화
-구현
- step size
step size를 조절하겠다는 말은 결국 lr를 조절한다는 이야기이다.
AdaGrad
: lr을 상황에 따라서 조절하는 방법론이다.
엡실론은 G_t가 0으로 수렴하면 a가 발산하기 떄문에 그거 막아주려고 있는 상수여서 큰 의미는 없고 중요한 것은 G_{t} 값이다.
모든 gradient 값의 제곱의 합이다. 이는 g의 값이 커질수록 업데이트는 적게 해준다는 뜻이다
즉, 가파를 떄는 size를 줄여 천천히 업데이트를 한다.
prob) 계속 g들을 더하다보면 G_t가 무한대로 커질 수 있다는 문제가 있다.
- 구현
RMSProp
: AdaGrad_prob을 해결하기 위해 G_t를 새롭게 정의한다.
과거에 gradient 값일수록 베타가 거듭해서 곱해지기에 ~ 0 꼴이 되서 prob_sol
- 구현
Adam Optim
: RMSProp + NAGD 방식 합침. -> 제일 많이 사용함.
구현
데이터 증강
: 학습 데이터가 부족할 떄 인위적으로 데이터의 숫자를 늘리는 방식으로 성능을 높힐 수 있다.
1) 이미지 증강
-> resize, rotate, flip, crop
2) 텍스트 대체
` 유의어, ` 무작위 교체(의미 유지 필요), ` 무작위 삭제(의미 유지 필요), `역번역, `능동태 <-> 수동태, ` 무작위 삽입
마치며
딥러닝의 성능을 올리기 위해 할 수 있는 행동들에 대해서 알아보았다. ㅎㅎ
'Computer Science > [인공지능]' 카테고리의 다른 글
[딥러닝] DNN 구현 (0) | 2024.03.15 |
---|---|
[딥러닝] Tensor Manipulation (2) | 2024.03.15 |
[딥러닝] 모델 학습법 (0) | 2024.03.06 |
[Ai] 클러스터링 (0) | 2024.02.20 |
[AI] 분류 (0) | 2024.02.08 |