
모멘텀과 AdaGrad 기법 융합한 최적화 방식
매개변수의 공간을 효율적으로 탐색
편향 보정 진행
아담 최적화 갱신 경로

class Adam:
"""Adam (http://arxiv.org/abs/1412.6980v8)"""
def __init__(self, lr=0.001, beta1=0.9, beta2=0.999):
# 학습률(lr), 1차 모멘텀 계수(beta1), 2차 모멘텀 계수(beta2) 초기화
self.lr = lr
self.beta1 = beta1
self.beta2 = beta2
self.iter = 0 # 업데이트 횟수(에포크 등) 카운트
self.m = None # 1차 모멘텀 변수(평균, 1st moment vector)
self.v = None # 2차 모멘텀 변수(분산, 2nd moment vector)
def update(self, params, grads):
# params: 파라미터(딕셔너리), grads: 파라미터에 대한 그래디언트(동일한 구조의 딕셔너리)
# 첫 업데이트 시 m, v 초기화 (zero tensor와 같은 크기로)
if self.m is None:
self.m, self.v = {}, {}
for key, val in params.items():
self.m[key] = np.zeros_like(val) # key별로 0 배열 할당
self.v[key] = np.zeros_like(val)
self.iter += 1 # 업데이트 횟수 증가
# bias correction을 곱한 현재 스텝별 학습률(lr_t) 계산
# (초기 t에서는 m, v가 0에 가까우므로 보정 필요)
lr_t = self.lr * np.sqrt(1.0 - self.beta2**self.iter) / (1.0 - self.beta1**self.iter)
# 각 파라미터별로 업데이트 진행
for key in params.keys():
# 모멘텀 갱신(이동평균)
#self.m[key] = self.beta1 * self.m[key] + (1 - self.beta1) * grads[key]
#self.v[key] = self.beta2 * self.v[key] + (1 - self.beta2) * (grads[key]**2)
# 위는 일반적인 이동평균 공식,
# 아래는 효율성 위해 바로 배열값 자체 갱신(동치임)
self.m[key] += (1 - self.beta1) * (grads[key] - self.m[key])
self.v[key] += (1 - self.beta2) * (grads[key]**2 - self.v[key])
# 편향 보정된 모멘텀과 분산 사용해서 파라미터 업데이트
params[key] -= lr_t * self.m[key] / (np.sqrt(self.v[key]) + 1e-7)
# 1e-7은 0으로 나누는 것을 방지하는 작은 값(Epsilon)
# 아래는 alternate 방식(주석 처리됨)
#unbias_m += (1 - self.beta1) * (grads[key] - self.m[key])
#unbisa_b += (1 - self.beta2) * (grads[key]*grads[key] - self.v[key])
#params[key] += self.lr * unbias_m / (np.sqrt(unbisa_b) + 1e-7)
# 실제론 위에서 이미 편향 보정된 lr_t를 사용하고 있으므로, 이 부분은 필요 없음
크게 네 가지 매개변수 갱신 방법 존재.
사용 기법에 따라 갱신 경로가 다름.
각자의 장단이 있어서 잘 푸는 문제와 서툰 문제가 있음.
그 외의 기울기 갱신 방식을 다시 복습해보자.
SGD (Stochastic Gradient Descent)
Momentum (모멘텀)
AdaGrad (Adaptive Gradient Algorithm)
이유: 역전파에서 모든 가중치 값이 똑같이 갱신되기 때문.
“예를 들어 2층 신경망에서 첫 번째와 두 번째 층의 가중치가 0이라고 가정하겠습니다. 그럼 순전파 때는 입력층의 가중치가 0이기 때문에 두 번째 층의 뉴런에 모두 같은 값이 전달됩니다. 두 번째 층의 모든 뉴런에 같은 값이 입력된다는 것은 역전파 때 두 번째 층의 가중치가 모두 똑같이 갱신된다는 말이 됩니다. 그래서 가중치들은 같은 초깃값에서 시작하고 갱신을 거쳐도 여전히 같은 값을 유지하는 것이죠. 이는 가중치를 여러 개 갖는 의미를 사라지게 합니다.”
import numpy as np
import matplotlib.pyplot as plt
def sigmoid(x):
return 1 / (1 + np.exp(-x))
x = np.random.randn(1000, 100) # 1000개의 데이터
node_num = 100 # 각 은닉층의 노드(뉴런) 수
hidden_layer_size = 5 # 은닉층이 5개
activations = {} # 이곳에 활성화 결과(활성화값)를 저장
for i in range(hidden_layer_size):
if i != 0:
x = activations[i-1]
w = np.random.randn(node_num, node_num) * 1
a = np.dot(x, w)
z = sigmoid(a)
activations[i] = z
# 히스토그램 그리기
for i, a in activations.items():
plt.subplot(1, len(activations), i+1)
plt.title(str(i+1) + "-layer")
plt.hist(a.flatten(), 30, range=(0,1))
plt.show()
활성화 함수별 활성화값 분포
w = random.randn(node_num, node_num) * 1활성화 함수의 일반적인 특성: 활성화값들이 0과 1에 치우쳐 분포되어 있음.
시그모이드 함수는 그 출력이 0이나 1에 가까워지면 그 미분은 0에 다가감(함수 그래프를 보면 알 수 있음).
데이터가 0과 1에 치우쳐 분포하게 되면 역전파 기울기 값이 점점 작아지다가 사라짐.
이것이 기울기 소실. 층이 깊은 경우 더 큰 문제가 될 수 있음.



이제 표준편차를 0.01로 한 경우 각 활성화 함수의 정규분포를 보기(표준편차를 줄였기 때문에 가운데로 몰려 있을 수 밖에 없음)
출력이 0.5에 치우쳐 있음. 이러면 기울기는 살아남지만, 출력층의 다양성이 매우 떨어짐.



시그모이드 활성화함수가 특히 기울기 소실 문제를 가장 직관적으로 보여준다.
Xavier 초깃값: 일반적인 딥러닝 프레임워크들이 표준적으로 이용하고 있음.
이 논문은 각 층의 활성화값들을 광범위하게 분포시킬 목적으로 가중치의 적절한 분포를 찾고자 함.
앞 계층의 노드가 n개라면 표준편차가 인 분포를 사용하면 됨.
앞 층의 노드가 많을수록 대상 노드의 초깃값으로 설정하는 가중치가 좁게 퍼짐.
그림: Xavier 초깃값(n은 앞 층의 노드 수)

Xavier분포를 활용한 경우의 활성화 분포
시그모이드




tanh와 시그모이드의 차이: 둘다 모양은 같은데 시그모이드는 (0, 0.5)에서 대칭, tanh는 (0, 0)에서 대칭. tanh로 xavier 분포를 사용한 그래프를 보면 좀 더 매끄러운 분포를 볼 수 있다.

ReLU 특성 상 활성화 분포가 0에 치우칠 수밖에 없음.
특화된 초깃값을 사용해야 함.
He 초깃값: 노드 n에 대해 표준편차가 인 정규분포를 사용함.
ReLU는 음의 영역이 0이라서 더 넓게 분포시키기 위해 Xavier 분포에 비해 2배의 계수가 필요하다고 직간접적 해석 가능.
활성화 함수로 ReLU를 사용한 경우의 가중치 초깃값에 따른 활성화값 분포 변화
ReLU + 0.01

ReLU + Xavier:

ReLU + He

실제 데이터로 가중치 초깃값 비교
표준편차 0.01, Xavier, He 초깃값 각각의 학습 효과 비교.

환경: python venv(3.10), macOS, m1, macbook air
로그:
Exception has occurred: ModuleNotFoundError
No module named 'dataset.mnist'
File "/Users/johyeonho/WegraLee-deep-learning-from-scratch/ch06/weight_init_compare.py", line 9, in <module>
from dataset.mnist import load_mnist
ModuleNotFoundError: No module named 'dataset.mnist'
최근 변경 사항
sys.path.append(os.pardir) # 부모 디렉터리의 파일을 가져올 수 있도록 설정
import numpy as np
import matplotlib.pyplot as plt
from dataset.mnist import load_mnist
확인
from dataset.mnist에서 dataset이 가질 수 있는 경로(디렉토리)는 sys.path 내의 여러 경로를 순서대로 탐색해서 찾습니다.dataset 폴더를 찾으면 그 경로를 기준으로 하위 mnist.py 파일을 찾고 임포트합니다.sys.path에 등록된 순서에 따라 처음 발견된 경로가 우선됩니다.print(os.getcwd())로 현재 디렉토리를 확인해보자.cwd: /Users/johyeonho/WegraLee-deep-learning-from-scratch
all paths
/Users/johyeonho/WegraLee-deep-learning-from-scratch/ch06
/Library/Frameworks/Python.framework/Versions/3.10/lib/python310.zip
/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10
/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/lib-dynload
/Users/johyeonho/WegraLee-deep-learning-from-scratch/.venv/lib/python3.10/site-packages
dataset 패키지는 분명 현재 디렉토리 하위에 존재하고 있음. 정작, paths에는 이 경로가 포함돼 있지 않음. 왜 cwd가 ch6이 아닌 상위로 설정된 걸까? 그건 지금 중요한 문제가 아니고, 일단 제대로 /Users/johyeonho/WegraLee-deep-learning-from-scratch이 경로가 sys.path에 포함이 됐음에도 /Users/johyeonho/WegraLee-deep-learning-from-scratch/dataset 패키지를 참조하지 못하고 있음.
시도
결과분석
배치 정규화의 장점
방법: 배치 정규화 계층을 신경망에 삽입
말 그대로 미니 배치를 단위로 정규화.
구체적으로는, 데이터 분포가 평균이 0, 분산이 1이 되도록 정규화.
미니배치라는 m개의 입력 데이터의 집합에 대해 평균 와 분산 을 구하고, 입력 데이터를 평균이 0, 분산이 1이 되도록 정규화
이 처리를 활성화 함수의 앞 혹은 뒤에 삽입하여 데이터 분포가 덜 치우치게 함.
배치 정규화 계층마다 이 정규화된 데이터에 고유한 확대와 이동 변환을 수행.
이 알고리즘이 신경망에서 순전파 때 적용.
그림: 배치 정규화를 사용한 신경망의 예


오버 피팅은 주로 다음의 두 경우에 발생
오버피팅 억제용: 가중치 감소. 학습 과정에서 큰 가중치에 대해서는 그에 상응하는 큰 패널티를 부과하여 오버피팅을 억제하는 방법. (원래 오버피팅은 가중치 매개변수의 값이 커서 발생하는 경우가 많음)
“가중치의 제곱 노름을 손실 함수에 더합니다. 그러면 가중치가 커지는 것을 억제할 수 있죠. 가중치를 W라 하면 L2 노름에 따른 가중치 감소는 이 되고, 이 을 손실 함수에 더합니다. 여기에서 람다는 정규화의 세기를 조절하는 하이퍼파라메터입니다. (아하! L2노름의 학습률과 같은 역할을 하는구나) 또 의 앞쪽 1/2는 의 미분결과인 를 조정하는 역할의 상수입니다.”
“가중치 감소는 모든 가중치 각각의 손실 함수에 을 더합니다. 따라서 가중치의 기울기를 구하는 그동안의 오차역전파법에 따른 결과에 정규화 항을 미분한 를 더합니다.”
중요! “L2 노름은 각 원소의 제곱들을 더한 것에 해당합니다. 가중치 이 있다면 L2 노름에서는 으로 계산할 수 있습니다. L2 노름 외에 L1 노름과 L inf 노름도 있습니다.” 즉, 애초에 각 원소들의 제곱 합을 루트 씌운 것이 L2 노름임.
오버피팅


가중치 감소(L2 정규화)는 간단하게 구현할 수 있고, 어느 정도 지나친 학습을 억제할 수 있음. 하지만 복잡한 네트워크에서 한계.(가중치 감소를 일괄로 적용해서 그런듯)
드롭아웃: 임의로 뉴런을 삭제하면서 학습.
왜 앙상블과 비슷한가?: 신경망의 맥락에서 이야기하면, 같은 구조의 네트워크 5개 준비하여 따로따로 학습시키고, 시험 때는 그 5개의 출력을 평균 내어 답하는 것임. 드랍아웃이 학습 때 뉴런을 무작위로 삭제하는 행위를 매번 다른 모델을 학습시키는 것으로 해석할 수 있음.
그림: 드롭아웃의 개념

class Dropout:
def __init__(self, dropout_ratio=0.5):
self.dropout_ratio = dropout_ratio
self.mask = None
def forward(self, x, train_flg=True):
if train_flg:
self.mask = np.random.rand(*x.shape) > self.dropout_ratio
return x * self.mask
else:
return x * (1.0 - self.dropout_ratio)
def backward(self, dout):
return dout * self.mask


하이퍼파라미터 최적화 핵심은 하이퍼파라미터의 ‘최적 값’이 존재하는 범위를 조금씩 줄여나가는 것.
하이퍼파라미터 최적화에서는 그리드 서지 같은 규칙적인 탐색보다는 무작위 샘플링 탐색이 좋은 결과를 낸다고 함
하이퍼파라미터는 대략적으로 지정하는 것이 효과적임. 로그 스케일로 지정하기
로그 스케일: 그냥 스케일이 큰 단위 개념. 0.001에서 ~ 1000사이로 10의 거듭 제곱으로 범위를 넓게 넓게 지정하는 것.
딥러닝 학습에는 오랜 시간이 걸리므로 나쁠 듯한 값은 일찍 포기하는게 좋음.
하이퍼파라미터 최적화 스텝
책에도 나와 있지만 그냥 과학이라기에는 직관이다. 더 세련된 최적화는 베이즈 최적화라는게 있음. (bayesian optimization) 하지만 안 쓰는데는 또 이유가 있긴 하다.(직관으로 해도 얼추 실무가 되니까)
일단 예제의 경우는 학습률과 가중치 감소율 범위를 랜덤으로 잡아서 테스트하는 것.
하이퍼파라미터 최적화 구현
# 탐색한 하이퍼파라미터의 범위 지정===============
weight_decay = 10 ** np.random.uniform(-8, -4) # 10의 제곱수로 logarithmic scale로 조절하는 모습을 볼 수 있음
lr = 10 ** np.random.uniform(-6, -2)

=========== Hyper-Parameter Optimization Result ===========
Best-1(val acc:0.82) | lr:0.0095672414080316, weight decay:6.635070742416745e-06
Best-2(val acc:0.82) | lr:0.009477221325778502, weight decay:1.1760685227016651e-07
Best-3(val acc:0.8) | lr:0.006739037463419945, weight decay:1.7010661695265425e-06
Best-4(val acc:0.77) | lr:0.00934915370701577, weight decay:1.7997196297327098e-06
Best-5(val acc:0.76) | lr:0.009868985075769459, weight decay:1.635486821915554e-06
Best-6(val acc:0.75) | lr:0.005475284194275679, weight decay:4.937344966443514e-05
Best-7(val acc:0.73) | lr:0.006905775299401849, weight decay:3.6399087334523756e-05
Best-8(val acc:0.69) | lr:0.00649821775816646, weight decay:5.47158945826805e-07
Best-9(val acc:0.67) | lr:0.005076157371628773, weight decay:1.734058463590518e-05
Best-10(val acc:0.66) | lr:0.004422836414123521, weight decay:7.264194832560532e-08
Best-11(val acc:0.5) | lr:0.0029130171987521453, weight decay:2.475549407588865e-05
Best-12(val acc:0.48) | lr:0.002579622537811143, weight decay:2.9655297816333528e-05
Best-13(val acc:0.47) | lr:0.0032637996247260035, weight decay:2.361395686396247e-08
Best-14(val acc:0.37) | lr:0.0021051083047830257, weight decay:6.928148559885636e-05
Best-15(val acc:0.33) | lr:0.0014275293878210434, weight decay:5.3801532386646794e-05
Best-16(val acc:0.33) | lr:0.002500298574255524, weight decay:6.352249911767835e-05
Best-17(val acc:0.31) | lr:0.003043759722856916, weight decay:4.893367583617906e-07
Best-18(val acc:0.31) | lr:0.0011888516788333278, weight decay:4.345552677170607e-06
Best-19(val acc:0.3) | lr:0.0025389223212827433, weight decay:1.5845294906776385e-06
Best-20(val acc:0.3) | lr:0.0018240949466978775, weight decay:3.5713163246505394e-08