[AIAS] Lect4. Neural Networks (part2)

17 minutes to read

AIAS-Lect4_Neural Networks (part2)

Data Preprocessing

일반적인 경우 우리가 가진 original data를 바로 학습에 사용하면 성능이 기대에 다소 못 미칠 수 있다. original data가 weight matrix에 너무 민감하여 optimize하기 어려운 경우가 그에 해당한다. 따라서 이런 경우 적절한 data preprocessing(전처리)를 통해 학습에 사용하기 좋은 형태로 변환해주어야 한다.

Normalization

아래 그림은 데이터가 편향되거나 너무 분산된 경우 zero-centering과 normalization을 통해 preprocessing하는 것을 보여준다.

image-20210418133156030

zero-centering이란 data의 평균값을 0으로 맞춰주는 과정이다. 앞서 sigmoid의 단점으로 bias shift가 있다고 언급한 바 있다. 데이터에서도 마찬가지로 전체적으로 어느 방향으로 치우친 데이터보단 평균이 0인 데이터가 좋다는 입장에서 zero-centering을 한다.

normalizationdata feature의 스케일을 동일한 정도로 맞춰주는 과정이다. unnormalized data의 경우, 최적화하는 과정에서 큰 폭으로 단계를 거쳐 최적값에 도달한다. 반면 normalized data의 경우는 시작점과 무관하게 일정한 폭으로 최적값이 도달할 수 있다. 이러한 특징은 학습을 보다 빠르게 만들어준다.

normalization은 feature의 scale이 동일할 때 학습이 더 잘 될 것이라는 가정 하에 사용해야 한다. data 분석 없이 마구잡이로 사용하는 것이 아니다.

PCA and Whitening

아래 그림은 데이터가 편향되거나 너무 분산된 경우 decorrelation과 whitening을 통해 preprocessing하는 것을 보여준다.

image-20210418134232817

decorrelation

Whitening은 eigenbasis data(기저벡터)를 eigenvalue(고유값)로 나누어 정규화 하는 방법이다.

일반적으로 이미지 데이터에 대해서는 PCA나 whitening을 잘 사용하지 않는다.

Weight Initialization

이전에 배웠듯 머신러닝 학습 과정의 큰 틀은 다음과 같다.

feed-forward → loss function → backpropagation(optimization) → update model parameters → feed-forward → …

그렇다면 학습 가장 초반에 model parameters는 어떻게 초기화하는 것이 좋을까?

Weights and Activation function

학습 초기 initialization 과정은 매우 중요하다. model parameters가 어떻게 initialization되었는가에 따라 결과가 달라지기 때문이다. 또한 weigths와 activation function은 연관성이 깊은데, network가 deep해질 수록 activation functions에 의해 model parameters(weights)가 전혀 update가 안될 수도 있기 때문이다. 아래에서 사례를 통해 연관성을 살펴보자.

Small random numbers with tanh

\[W-N(0,(1e-2)^2)\]

위와 같이 평균이 0, 표준편차가 1e-2인 가우시안 분포를 따르는 random변수로 초기화 할 수 있다.

W=0.01 * np.random.randn(Din, Dout)

이런 경우 small networks에는 나쁘지 않게 작동하지만 network가 깊어질 수록 문제가 발생한다. 가령 activationo functions으로 tanh()함수를 사용한 아래의 경우를 살펴보자.

# Forward pass for a 6-layer net with hidden size 4096
dims = [4096] * 7
hs = []
x = np.random.randn(16, dims[0])
cnt = 1

for Din, Dout in zip(dims[:-1], dims[1:]):
    W = 0.01 * np.random.randn(Din, Dout)
    x = np.tanh(x.dot(W))
    hs.append(x)
    print('[',cnt,'layers]  mean:%.2f'%np.mean(x), 'std:%.2f'%np.std(x))
    cnt=cnt+1
"""
[ 1 layers]  mean:-0.00 std:0.49
[ 2 layers]  mean:0.00 std:0.29
[ 3 layers]  mean:-0.00 std:0.18
[ 4 layers]  mean:0.00 std:0.11
[ 5 layers]  mean:-0.00 std:0.07
[ 6 layers]  mean:-0.00 std:0.05
"""

image-20210418170041457

위 코드는에서 W값은 mean=0, std=1e-2인 가우시안 분포를 따르는 랜덤변수이다. 각 tanh activation function을 통과한 결과에 대하여 평균과 분산을 출력한 결과는 위와 같다. 이를 통해 layer를 통과할 수록 분산이 매우 작아져 결국 출력값의 대부분이 0으로 수렴한다는 것을 알 수 있다. 출력값이 0에 수렴하는 이유는 tanh activation function의 꼴을 보면 이해할 수 있다.

image-20210418170359983

tanh()함수는 입력의 절댓값이 작은 경우 출력값이 0에 가까워지는 특징을 가졌다. 이러한 함수 특징에 의해 layer를 통과할 수록 출력값이 0에 수렴하는 것이다. layer의 출력값이 0인 경우 학습이 전혀 되지 않는다. 그 이유는 chain rule에 의해 쉽게 설명 가능하다. \(\frac{∂L}{∂w_i^{l+1}}=\frac{∂L}{∂f_{l+1}}*\frac{∂f_{l+1}}{∂w_i^{l+1}}=\frac{∂L}{∂f_{l+1}}*x_i^{l+1}≈0\\ (∵\ f_{l+1}(x_i^{l+1},w_i^{l+1})=W^{l+1}*X^{l+1}\ )\) downstream gradient는 upstream gradient와 local gradient의 곱으로 계산할 수 있다. 이때 layers를 통과할 수록 x값이 0에 수렴하므로 결국 downstream gradient도 0으로 수렴하여 backpropagation이 전혀 진행되지 않음을 상상할 수 있다.

### Increase std for initial weights from 0.01 to 0.05

weights의 초기값이 너무 작은 경우 위와 같이 deep network에서 layer를 통과할 수록 출력값이 0에 수렴해 backpropagation이 전혀 진행되지 않았음을 확인했다. 그렇다면 weights 표준편차의 초기값을 0.01에서 0.05로 늘려보자.

# Forward pass for a 6-layer net with hidden size 4096
dims = [4096] * 7
hs = []
x = np.random.randn(16, dims[0])
cnt = 1

for Din, Dout in zip(dims[:-1], dims[1:]):
    W = 0.05 * np.random.randn(Din, Dout) #★
    x = np.tanh(x.dot(W))
    hs.append(x)
    print('[',cnt,'layers]  mean:%.2f'%np.mean(x), 'std:%.2f'%np.std(x))
    cnt=cnt+1
"""
[ 1 layers]  mean:0.00 std:0.87
[ 2 layers]  mean:0.00 std:0.85
[ 3 layers]  mean:-0.00 std:0.85
[ 4 layers]  mean:-0.00 std:0.85
[ 5 layers]  mean:-0.01 std:0.85
[ 6 layers]  mean:-0.00 std:0.85
"""

image-20210418170515596

위 코드는에서 W값은 mean=0, std=0.05인 가우시안 분포를 따르는 랜덤변수이다. 각 tanh activation function을 통과한 결과에 대하여 평균과 분산을 출력한 결과는 위와 같다. 한편, 이번에는 출력값의 대부분이 -1또는 1인것을 histogram을 통해 확인할 수 있다. 이 또한 tanh activation function의 특징을 통해 설명 가능하다.

image-20210418170634722

tanh()함수는 입력의 절댓값이 큰 경우 출력값이 -1 또는 1에 가까워지는 특징을 가졌다. 이러한 함수 특징에 의해 layer를 통과할 수록 출력값이 -1 또는 1에 수렴하는 것이다. 이러한 경우를 saturated activation function이라고 하는데, 이 경우 또한 학습이 전혀 되지 않는다. 그 이유는 chain rule에 의해 쉽게 설명 가능하다. \(\frac{∂L}{∂w_i}=\frac{∂L}{∂f}*\frac{∂f}{∂w_i}=\frac{∂L}{∂f}*\frac{∂f}{∂s}*\frac{∂s}{∂w_i}≈0\\ (∵\ s=\sum_i{w_ix_i}+b,\ f:activation\ function일\ 때,\ \frac{∂f}{∂s}≈0\ )\) s를 loss function, f를 activation function이라고 하자. 출력이 -1또는 1인 경우의 activation function (tanh)의 미분값은 위 tanh그래프를 통해 0에 수렴함을 알 수 있다. 따라서 local gradient이 0으로 수렴하므로 downstream gradient도 0으로 수렴하여 backpropagation이 전혀 진행되지 않음을 상상할 수 있다.

Xavier (with tanh) Initialization

위의 사례를 통해 activation function이 tanh인 경우, weights가 너무 작거나 크면 backpropagation이 안되는 문제를 확인했다. 출력값의 분포를 적당하게 맞춰주는 작업이 필요해졌고, 그에 따라 Xavier initialization 방법이 제안되었다.

# Forward pass for a 6-layer net with hidden size 4096
dims = [4096] * 7
hs = []
x = np.random.randn(16, dims[0])
cnt = 1

for Din, Dout in zip(dims[:-1], dims[1:]):
    W = np.random.randn(Din, Dout) / np.sqrt(Din) #★
    x = np.tanh(x.dot(W))
    hs.append(x)
    print('[',cnt,'layers]  mean:%.2f'%np.mean(x), 'std:%.2f'%np.std(x))
    cnt=cnt+1
"""
[ 1 layers]  mean:0.00 std:0.63
[ 2 layers]  mean:0.00 std:0.49
[ 3 layers]  mean:-0.00 std:0.41
[ 4 layers]  mean:-0.00 std:0.36
[ 5 layers]  mean:0.00 std:0.32
[ 6 layers]  mean:0.00 std:0.29
"""

image-20210418171907614

위 코드는에서 W값은 mean=0, std=1/sqrt(Din)인 가우시안 분포를 따르는 랜덤변수이다. 이때 입력의 루트값으로 나눠준 값을 표준편차로 정의하는 경우, 위와 같이 출력의 분산이 -1과 1 사이에 골고루 퍼지는 것을 확인할 수 있다. 이 원리에 대해서는 학부생의 수준을 넘어서므로 여기서는 논의하지 않는다. 다만 어떠한 작업(1/sqrt(Din))을 통해 출력의 분산을 적절히 scaling해주었다는 정도만 알면 된다. 이 경우 모든 layers에 대해서 적절한 입력이 주어지므로 학습이 잘 될 것임을 기대할 수 있다.

Xavier (with ReLU) Initialization

이번에는 Xavier initialization에 activation function으로 ReLU함수를 사용해보자.

# Forward pass for a 6-layer net with hidden size 4096
dims = [4096] * 7
hs = []
x = np.random.randn(16, dims[0])
cnt = 1

for Din, Dout in zip(dims[:-1], dims[1:]):
    W = np.random.randn(Din, Dout) / np.sqrt(Din)
    x = np.maximum(0, x.dot(W)) #★
    hs.append(x)
    print('[',cnt,'layers]  mean:%.2f'%np.mean(x), 'std:%.2f'%np.std(x))
    cnt=cnt+1
"""
[ 1 layers]  mean:0.40 std:0.58
[ 2 layers]  mean:0.28 std:0.41
[ 3 layers]  mean:0.19 std:0.29
[ 4 layers]  mean:0.14 std:0.20
[ 5 layers]  mean:0.10 std:0.14
[ 6 layers]  mean:0.07 std:0.10
"""

image-20210418172843174

activation function을 ReLU함수로 바꾸니 위와 같이 출력이 다시 0으로 수렴하는 현상이 발생했다. 이러한 결과는 음수 입력값에 대해 항상 0을 출력하는 ReLU함수의 특징 때문이다.

Kaiming/MSRA (with ReLU) Initialization

위의 예시를 통해 Xavier initialization에 ReLU함수를 사용하면 출력이 0으로 수렴하는 문제가 발생함을 확인했다. 다음과 같이 weights 부분을 고침으로써 이러한 문제를 해결할 수 있다.

# Forward pass for a 6-layer net with hidden size 4096
dims = [4096] * 7
hs = []
x = np.random.randn(16, dims[0])
cnt = 1

for Din, Dout in zip(dims[:-1], dims[1:]):
    W = np.random.randn(Din, Dout) / np.sqrt(Din/2) #★
    x = np.maximum(0, x.dot(W))
    hs.append(x)
    print('[',cnt,'layers]  mean:%.2f'%np.mean(x), 'std:%.2f'%np.std(x))
    cnt=cnt+1
"""
[ 1 layers]  mean:0.56 std:0.83
[ 2 layers]  mean:0.58 std:0.83
[ 3 layers]  mean:0.58 std:0.83
[ 4 layers]  mean:0.56 std:0.83
[ 5 layers]  mean:0.55 std:0.80
[ 6 layers]  mean:0.55 std:0.80
"""

image-20210418191214260

ReLU함수는 음수입력값이 0으로 출력된다는 문제로 인해, 입력의 절반 정도가 사라진다는 이유로 학습이 제대로 이루어지지 않았다. 따라서 Xavier initialization에서의 아이디어에 2로 나누어 입력의 절반이 없어진다는 점을 반영한다. 그 결과 위와 같이 좋은 분포를 출력함을 확인할 수 있다.

Stochastic Gradient Descent (SGD)

Gradient Steps

모델은 gradient를 통해 loss가 줄어드는 방향으로 model parameters가 업데이트 되며 학습이 이루어진다. 이때 model parameters는 다음과 같이 업데이트된다. \(x'=x-α▽_xf(x)\\ α: learning\ rate\\ ▽_x: gradient\) 이때 learning rate(α)가 작으면 아래 그림에서 화살표 방향(gradient)으로 조금 이동하고, learning rate가 큰 경우, 화살표 방향으로 크게 이동한다.

image-20210418191925234

가장 최적의 point를 global optimum이라 하고, 일부 영역에서만 놓고 봤을 때 최적의 point는 local mimum이라고 한다. 이때 learning rate가 너무 작은 경우, global minimum에 도달하는데 까지 걸리는 오랜 시간이 소요될 수 있다. 반면 learning rate가 너무 작은 경우, 섬세한 이동을 하지 못해 global minimum를 정확히 찾지 못할 수 있다. 이러한 learning rate는 아래 그림과 같이 gradient descent의 convergence(수렴) 여부에 영향을 미친다.

image-20210418192259743

위 그림은 big learning rate인 경우 발산할 위험성이 있음을 보여준다. 따라서 적절한 learning rate를 결정하는 것이 학습에 있어서 매우 중요하다.

SGD 탄생 배경

Single Training Sample의 경우

다음과 같이 주어진 조건에서 최적의 model parameters θ={W,b}을 찾아보자.

image-20210418200053743

single training sample에 대해서 학습하는 방법은 아래와 같다. \(θ^{k+1}=θ^k-α▽_θL_i(θ^k,x_i,y_i)\) 즉, 현재 parameters θ에서 gradient(▽)와 Loss function(L)의 곱을 learning rate(α)만큼 빼주면 새로운 parameters가 된다.

Multiple Training Samples의 경우

이번에는 single training sample이 아닌 n개의 multiple training samples {xi, yi}에 대해 최적의 model parameters를 구한다고 생각해보자. 이때의 cost는 아래와 같이 계산할 수 있다. \(Cost\ L=\frac{1}{n}\sum_{i=1}^nL_i(θ, x_i, y_i)\) n개의 single training sample 각각에 대한 loss의 평균을 구하면 multiple training samples에 대한 cost를 구할 수 있다.

Too Many Training Samples의 경우

그렇다면 아주 training samples가 너무 많은 경우는 어떻게 될까? 모든 training samples 각각에 대한 loss를 계산한 뒤 평균을 내는 방법은 연산량이 너무 많아 비효율적이다. 이러한 배경에서 stochastic gradient descent(SGD)가 탄생했다.

SGD의 개념

n개의 training samples에 대해 gradient를 계산하려면 O(n)만큼의 연산이 필요하다. 확률에 근거하면 전체 training samples에 대한 loss는 전체 training samples의 loss 기댓값으로 나타낼 수 있다. \(\frac{1}{n}(\sum_{i=1}^n{L_i(θ, x_i, y_i)})= \mathbb{E}_{i~[1,...,n]}[L_i(θ,x_i,y_i)]\) 이때 loss의 기댓값은 일부 training samples만으로 아래와 같이 추정할 수 있다. \(\mathbb{E}_{i~[1,...,n]}[L_i(θ,x_i,y_i)] ≈ \frac{1}{|S|}\sum_{j∈S}(L_j(θ,x_j,y_j))\ with\ S⊆{1,...n}\) 이러한 전체 training samples의 subset을 minibatch라고 하며 아래와 같이 수학적 기호로 표현할 수 있다. \(B_i=\{\{x_1,y_1\},\{x_2,y_2\}, ... ,\{x_m,y_m\}\}=\{B_1, B_2,...,B_{n/m}\}\)

용어 정리

SGD에서 새롭게 알게 될 용어들에 대해 간단히 살펴보자.

Epoch

Epoch은 한국말로 “시대”를 뜻한다. AI에서 epoch은 전체 dataset에 대하여 한 번 학습을 완료한 상태(forward & backward pass과정을 거친 상태)를 의미한다.

Batch

batch나누어진 dataset을 의미한다. gradient descent에서는 전체 dataset을 한꺼번에 학습시키는데, 이로인해 발생하는 비효율 문제를 해소하고자 나온 알고리즘이 바로 stochastic gradient descent이었다. 여기서 SGD는 전체 dataset을 잘게 쪼개어 batch 단위로 학습시킨다고 볼 수 있다. batch를 mini-batch라고도 부르며, 일반적으로 사이즈는 32, 64, 128개를 주로 사용한다.

Iteration

iteration은 한국말로 “되풀이”라는 뜻이다. 전체 dataset을 쪼개어 batch 단위로 학습을 수행할 때, 1 epoch을 달성하기 위해서는 여러 번의 실행이 필요하다. 이러한 실행 반복을 iteration이라고 한다. 전체 data의 양이 N개라고 할 때 batch size가 n이면, iteration은 N/n이 되는 관계이다.

학습 과정

Stochastic Gradient Descent(SGD)는 다음과 같은 학습 과정을 거친다.

Loop:
	1. Sample a batch of data
	2. Forward prop it through the graph(network), get loss
	3. Backprop to calculate the gradients
	4. Update the parameters using the gradient

samples의 개수가 많은 경우 일반적인 gradient descent보다 stochastic gradient descent 알고리즘을 더 많이 사용하는데, 그 이유는 아래의 그림으로 설명할 수 있다.

image-20210418202625500

gradient descent의 경우 한 step을 이동할 때 아주 정확한 방향으로 최적화를 수행한다. 반면 stochastic gradient descent는 한 step을 이동할때 방향적 정확도는 gradient descent보다 떨어짐을 확인할 수 있다. 하지만 그럼에도 불구하고 대량의 데이터에 대해 stochastic gradient descent가 더 좋은 성능을 보이는 이유는, 빠르기 때문이다. gradient descent가 최적의 한 step을 계산하는 동안 stochastic gradient descent는 이미 몇 step 이동해있기 때문이다.

비유하자면, gradient descent는 완벽주의자, stochastic gradient descent는 행동주의자이다. 여기서 완벽한 것도 좋지만 조금 틀리더라도 일단 해보는 게 중요하다는 교훈을 얻을 수 있다.

(Fancier) Optimizers

Gradient descent의 학습속도 문제를 해결하고자 stochastic gradient descent가 제안되었다. 하지만 SGD는 gradient 기반 optimizer이므로 local minimum에 갇힐 수 있다는 문제점을 가지고 있다.

image-20210418203310926

위 이미지는 local minimum(혹은 saddle point)에 갇히는 경우를 나타낸다. gradient값이 일시적으로 0인 경우, gradient descent가 최적화를 중지한다는 문제를 해결하고자 여러가지 optimizers가 제안되었다.

SGD + Momentum

먼저 SGD의 학습은 아래와 같이 이루어졌다. \(x_{t+1}=x_t-α▽f(x_t)\)

while True:
	dx = compute_gradient(x)
	x -= learning_rate * dx

여기에 momentum의 개념을 더한 SGD+Momentum optimizer는 아래와 같다. \(v_{t+1}=ρv_t+▽f(x_t)\\ x_{t+1}=x_t-αv_{t+1}\)

vx = 0
while True:
	dx = compute_gradient(x)
	vx = rho * vx + dx
	x -= learning_rate * vx

momentum이란 “현재 가고있는 방향을 유지하려는 성질”이다. 자연계의 대부분의 현상은 갑작스럽게 변하지 않고 서서히 변한다는 가정 아래 momentum의 개념을 추가할 수 있다.

image-20210418204552073

여기서 rho(ρ)값은 일반적으로 0.9 혹은 0.99를 사용하며, 관성을 유지하는 정도를 나타낸다.

AdaGrad

한편, SGD+momentum optimizer는 learning rate가 너무 작으면 학습시간이 너무 길고, learning rate가 너무 크면 발산한다는 문제가 있었다. 이러한 문제를 해결하고자 AdaGrad optimizer가 등장했다. AdaGrad는 “시간이 지남에 따라 optimum에 가까워진다”라는 가정 하에 learning rate를 계속 감소시키며 학습을 진행한다. \(h←h+▽f(x)^2\\ x←x-α\frac{1}{\sqrt{h}}▽f(x)\) 이처럼 h에 이전 기울기의 제곱값을 누적해서 더한 뒤, model parameter(x)를 업데이트 할 때 gradient와 learning rate의 곱에 √h를 나누어 준다. h값은 학습이 진행됨에 따라 값이 커지므로, x값의 learning rate(α)가 학습이 진행됨에 따라 감소하게 된다. 이를 코드로 나타내면 아래와 같다.

grad_squared = 0
while True:
	dx = compute_gradient(x)
	grad_squared += dx * dx
	x -= learning_rate * dx / (np.sqrt(grad_squared) + 1e-7)

그 외의 optimizers

출처: 자습해도 모르겠던 딥러닝, 머리속에 인스톨 시켜드립니다.

잘 모르겠다면 일단 Adam을 써라!!

Regularization

이전에 잠시 loss가 최소가 되는 model parameters는 unique하지 않다는 것을 언급한 바 있다. 따라서 최적의 model parameters를 찾기 위해 regularization의 개념이 필요하다.

혹시 까먹었을까봐

Regularizationmodel이 training data에 대하여 너무 잘 맞는 **overfitting**을 방지하기 위해 추가되는 항이다.

Model Ensembles

Model ensembles란 독립적인 모델 여러 개를 함께 사용하여 overfitting을 방지하고 성능을 향상시키는 방법이다. 각각의 모델을 따로 학습시킨 뒤 test time때 각각의 결과를 평균내는 방식으로 진행된다.

하지만 model ensembles을 하기 위해서는 좋은 컴퓨팅 성능이 필수적이다. 따라서 개인 차원에서는 잘 안하고 회사 차원에서 시도해볼 수 있는 기법이다.

model ensembles에서 변형을 가하는 방법은 다음과 같이 매우 다양하다.

  1. Same model but different initializations
  2. Same model but different optimization/objective function
  3. Same model but different datasets
  4. Top models discovered during cross-validation
  5. Different checkpoints (i.e. iteration) of a single model
  6. Running average of parameters during training

이러한 model ensembles의 종류에 대해 살펴보자.

Dropout

각 node에서 forward pass를 할 때, 랜덤한 확률로 일부 neurons을 zero로 만드는 기법dropout이라고 한다. dropping 확률은 hyperparameter로 일반적인 경우 0.5를 많이 사용한다.

image-20210418212357585

아래와 같이 코드로 구현할 수 있다.

p = 0.5 # probability of keeping a unit active. higher = less dropout

def train_step(X):
    """ X contains the data """
    
    # forward pass for example 3-layer neural network
    H1 = np.maximum(0, np.dot(W1,X) + b1)
    U1 = np.random.randn(*H1.shape) < p # first dropout mask
    H1 *= U1 # drop!
    H2 = np.maximum(0, np.dot(W2,H1) + b2)
    U2 = np.random.randn(*H2.shape) < p # second dropout mask
    H2 *= U2 # drop!
    out = np.dot(W3, H2) + b3
    
    # backward pass: compute gradients ... (not shown)
    # perform parameter update ... (not shown)

dropout은 다음과 같은 가정 하에 탄생했다. 예를 들어, 고양이 이미지를 판별하는 모델을 만들어보자. 모델은 뾰족한 귀가 있고, 길쭉한 꼬리가 있고, 털이 있으면 고양이라고 판단한다고 하자. 이때 스코티쉬폴드(귀가 접힌 고양이 종류)는 귀가 뾰족하지 않아서 고양이가 아니고, 스핑크스 고양이(털이 없는 고양이)는 털이 없어서 고양이가 아니라고 판별하는 사태가 벌어질 수 있다. 이러한 overfitting을 방지하기 위해 일부 node를 의도적으로 차단시켜 “귀가 납작하지만 길쭉한 꼬리와 털을 가졌으니 고양이”라고 판단하도록 하는 게 바로 dropout 방식이다.

dropout을 model ensemble관점에서 보면 다음과 같이 해석할 수 있다. 각각의 binary mask를 하나의 model이라 보면, 같은 model parameters를 공유하는 여러 개의 model의 조합으로 볼 수 있다.

한편, train time때는 확률(p)을 통해 일부 nodes를 zero로 만들어 학습하지만, test time때는 모든 nodes를 사용한다. 이때, 각 node가 켜질 확률을 반영하여 각 layer를 통과할 때마다 확률 p를 곱해주어야 한다. 예를 들어 이러한 동작을 수행하는 코드는 아래와 같다.

def predict(X):
    # ensembled forward pass
    H1 = np.maximum(0, np.dot(W1, X) + b1) * p # NOTE: scale the activations
    H2 = np.maximum(0, np.dot(W2, X) + b2) * p # NOTE: scale the activations
    out = np.dot(W3, H2) + b3

dropout을 수행하는 전체 코드(train+test)는 아래와 같다.

""" Vanilla Dropout: Not recommended implementation (see notes below)"""

p = 0.5 # probability of keeping a unit active. higher = less dropout

def train_step(X):
    """ X contains the data """
    
    # forward pass for example 3-layer neural network
    H1 = np.maximum(0, np.dot(W1,X) + b1)
    U1 = np.random.randn(*H1.shape) < p # first dropout mask
    H1 *= U1 # drop!
    H2 = np.maximum(0, np.dot(W2,H1) + b2)
    U2 = np.random.randn(*H2.shape) < p # second dropout mask
    H2 *= U2 # drop!
    out = np.dot(W3, H2) + b3
    
    # backward pass: compute gradients ... (not shown)
    # perform parameter update ... (not shown)

def predict(X):
    # ensembled forward pass
    H1 = np.maximum(0, np.dot(W1, X) + b1) * p # NOTE: scale the activations
    H2 = np.maximum(0, np.dot(W2, X) + b2) * p # NOTE: scale the activations
    out = np.dot(W3, H2) + b3

train time에는 확률 p를 이용하여 일부 node를 drop시키고, test time에는 모든 nodes를 키되, 이러한 확률을 반영하여 activation function을 지날 때 마다 확률 p를 곱해준다. 이때 model parameters를 학습시킬 때 애초에 확률 p가 곱해진 형태로 나오도록 하는 트릭을 통해 아래와 같이 test time의 코드는 기존과 같이(확률 p를 반영하지 않도록) 만들 수 있다.

""" Vanilla Dropout: Not recommended implementation (see notes below)"""

p = 0.5 # probability of keeping a unit active. higher = less dropout

def train_step(X):
    """ X contains the data """
    
    # forward pass for example 3-layer neural network
    H1 = np.maximum(0, np.dot(W1,X) + b1)
    U1 = (np.random.randn(*H1.shape) < p) / p # ★ first dropout mask
    H1 *= U1 # drop!
    H2 = np.maximum(0, np.dot(W2,H1) + b2)
    U2 = (np.random.randn(*H2.shape) < p) / p # ★ second dropout mask
    H2 *= U2 # drop!
    out = np.dot(W3, H2) + b3
    
    # backward pass: compute gradients ... (not shown)
    # perform parameter update ... (not shown)

def predict(X):
    # ensembled forward pass
    H1 = np.maximum(0, np.dot(W1, X) + b1) # ★ no scaling necessary
    H2 = np.maximum(0, np.dot(W2, X) + b2) # ★ no scaling necessary
    out = np.dot(W3, H2) + b3

Data Augmentation

Data augmentation은 입력 데이터에 약간의 변형을 가하여 데이터의 양을 늘리는 일종의 트릭이다. 데이터 변형은 “실제 상황에서 발생할 수 있을 정도”를 지키는 한에서 다음과 같이 다양하게 변형을 가할 수 있다.

Horizontal Flips

image-20210418214849448

Random crops and scales

image-20210418215158878

일반적인 경우, 5개(양쪽 모서리와 정 가운데)로 쪼갠다.

Color Jitter

image-20210418215255521

밝기나 대조값을 조정할 수 있다.

그 외

image-20210418215602212

그 외에 translation, rotation, stretching, shearing, lens distortions 등 다양한 변형 방법을 조합하여 나만의 data augmentation을 수행할 수 있다.

DropConnect

Training: Drop connections between neurons (set weights to 0)
Testing: Use all the connections

DropConnect뉴런간의 connections을 dropping하는 방식이다. nodes를 random하게 dropping하는 dropout과는 달리 nodes는 모두 살리고 nodes간의 connections을 random하게 dropping한다는 점에서 차이가 있다.

image-20210418215812864

dropconnect의 동작을 간단하게 표현하면 다음과 같다.

Cutout

Training: Set random image regions to zero
Testing: Use full image

image-20210418220059365

train time에 cutout은 이미지의 일부 영역의 data값을 zero로 만듦으로써 수행된다.

Mixup

Training: Train on random blends of images
Testing: Use original images

image-20210418220210576

Mixup은 train time에 서로 다른 두 개의 이미지를 섞고, 그 비율에 따라 target label을 정의함으로써 수행된다.

Cutmix

Training: Train on random blends of images
Testing: Use original images

image-20210418222009055

Cutmix는 train time에 서로 다른 두 개의 이미지를 잘라 붙이고, 그 비율에 따라 target label을 정의함으로써 수행된다.

Hyperparameter Tuning

이전에 parameters는 모델이 학습하는 model parameters와 사람이 정의하는 hyperparameters로 나눠진다고 설명한 바 있다. 이러한 hyperparameters의 종류에는 다음의 것들이 있다.

  • Network architecture (e.g., layers 개수, weights 개수)
  • Number of itarations
  • Learning rate(s) (i.e., solver parameters, decay, etc.)
  • Regularization (more later next lecture)
  • Batch size

아래는 hyperparameter를 선택하는 방법 중 grid search와 random search를 비교한 그림이다.

image-20210418222618991

세로축 방향의 hyperparameter는 어떤 값이든 크게 중요치 않은 값이고, 가로축 방향의 hyperparameter는 특정 값(볼록 튀어나온 부분)에서 성능이 크게 향상되는 중요한 값이라고 하자. grid search의 경우 왼쪽 그림에서 확인할 수 있듯이 생각보다 성능이 나쁘다. 반면 오른쪽 그림과 같이 random search를 하는 경우, 성능이 좋은 hyperparameters를 발견할 가능성이 더 높다.

이러한 random search를 통해 rough한 공간 내에서의 좋은 hyperparameters 영역을 찾은 뒤, 이후 세부적인 영역 내에서 더 좋은 hyperparameters를 찾는 방식을 적용하면 더 좋은 성능을 끌어올릴 수 있다.

Example: run coarse search for 5 epochs

max_count = 100
for count in xrange(max_count):
    reg = 10**uniform(-5,5)
    lr = 10**uniform(-3,-6)
    
    trainer = ClassifierTrainer()
    model = init_two_layer_model(32*32*3, 50, 10) # input size, hidden size, number of classes
    trainer = ClassifierTrainer()
    best_model_local, stats = trainer.train (X_train, y_train, X_val, y_val, model, two_layer_net, num_epochs=5, reg=reg, update='momentum', learning_rate_decay=0.9, sample_batchs=True, batch_size=100, learning_rate=lr, verbose=False)

실행결과:

image-20210418223443065

일부 hyperparameters에 대하여 val_acc값이 대략 0.4 이상으로 좋게 나오는 것을 확인할 수 있다. 이러한 영역 내에서 더 좋은 hyperparameters를 찾기 위해 아래와 같이 수행할 수 있다.

max_count = 100
for count in xrange(max_count):
    reg = 10**uniform(-4, 0) # ★
    lr = 10**uniform(-3, -4) # ★

실행결과:

image-20210418223647345

이러한 영역 좁히기 과정을 통해 더 나은 hyperparameters를 결정할 수 있다. 이때 가장 성능이 좋은 val_acc:0.53100, lr: 9471549e-04, reg: 1.433895e-03, (14/100) 을 살펴보면, 위에서 조정해준 영역 reg(-4,0), lr(-3,-4)의 경계면에 맞닿아있는 것을 확인할 수 있다. 이러한 경우 설정해준 경계면 바깥에 더 좋은 hyperparameters가 있을 수 있다는 합리적 의심을 해볼 수 있다.

Guideline

Step1: Check initial loss

image-20210418224058971

먼저 학습 초기에는 overfitting을 걱정할 필요가 없다 (오히려 overfitting으로 향해 나아가야 하는 단계이므로). 따라서 regularization loss(a.k.a. weight decay) 없이 학습 과정을 수행한다.

Step2: Overfit a small sample

전체 training data는 너무 크므로 small training data로 sampling을 한 뒤, 해당 samples에 대해서만 일단 최대한 100% training accuracy를 가지도록 architecture도 조금 수정하고, learning rate도 조금 수정하고, weigth initialization도 조금 손 봐 가며 학습을 수행한다 (일단은 overfitting 걱정하지 말고, 경향성을 살펴보자는 의미).

예를 들어 loss가 줄어들지 않는다면 learning rate를 조금 키울 수 있고, loss가 발산하면 learning rate를 조금 줄이는 식으로 hyperparameters를 rough하게 결정할 수 있다.

Step3: Find LR that makes loss go down

이후 전체 training data에 대하여 이전의 단계 (overfitting)를 수행해본다.

일반적으로 초기 learning rate의 값으로 1e-1, 1e-2, 1e-3, 1e-4를 주로 사용한다.

Step4: Coarse grid, train for ~1-5 epochs

step3으로 부터 확인한 영역 내에서 learning rate나 weight decay(regularization loss)등의 값을 선택한 뒤, 몇 개의 models에 대해 1~5 epochs만큼 train을 수행해 본다.

일반적으로 초기 weight decay값으로 1e-4, 1e-5, 0를 선택할 수 있다.

Step5: Refine grid, train longer

step4에서 성능이 좋았던 models를 선택하여 learning rate decay 없이 좀 더 길게(10~20 epochs) training을 수행해본다.

Step6: Look at loss curves

이때 loss curves를 확인해본다. loss curves는 일반적으로 다음의 경우로 나타날 수 있다.

  1. training이 더 필요한 경우

    image-20210418225233564

    training accuracy와 validation accuracy가 모두 계속해서 증가하고 있다면 아직 학습이 더 필요한 상태라는 뜻이다.

  2. overfitting이 발생한 경우

    image-20210418225347502

    training accuracy는 증가하지만 validation accuracy는 오히려 감소한다면, overfitting이 발생한 것이다. 이런 경우 regularization을 증가시키거나 더 많은 data를 입력하는 방안을 택할 수 있다.

  3. underfitting이 발생한 경우

    image-20210418225510471

    training accuracy와 validation accuracy간에 gap이 매우 작은 경우는 underfitting되었음을 의미한다. 이런 경우 더 길게 training을 시키거나, 더 큰 model을 사용하는 방안을 택할 수 있다.

Step7: GOTO step5

loss curve를 확인한 후 다시 step5로 돌아갈 수 있다.


내가 풀려는 문제와 유사한 모델을 가져와서 사용된 hyperparameters에서부터 시작해본다면 여러가지 시행착오를 줄일 수 있다.