5. Multilayer Perceptrons

Status
Done
Tags
Due5.1 ~ 6.3 ] 2024.01.17
TM
DS
SY
최종 작성 완료

1. Multilayer Perceptrons

@Tommy Kim

1) Hidden Layers

우리는 이전의 내용들을 통해 하나의 output을 만들기 위해 하나의 affine function(편향을 포함한 선형 조합)이 필요함을 배웠다. 하지만, 단순한 하나의 선형 조합으로 output을 만들어 내는 것은 완벽하지 않다.

a. Limitations of Linear Models

선형 모델은 단조성(monotonicity)을 포함한다. 이는 어떤 변수의 증감이 그대로 모델의 출력에 동일한 영향을 미친다는 의미이다. 하지만, 현실 세계의 데이터는 이러한 간단한 관계를 따르지 않는다. 소득의 증가에 따라 대출 상환 가능성이 증가한다고 가정해보자. 소득이 0원에서 500만원이 되면 상환 가능성이 매우 높아지겠지만, 10억에서 10억 500만원이 되는 것은 영향이 매우 적다고 볼 수 있다. 또한, 체온과 건강의 관계에서도 정상 온도에서 높아지거나 낮아질 때 모두 건강의 문제가 생긴 것이므로, 모델링에 어려움이 있다. 마지막으로 개와 고양이를 구별하는 이미지 분류 문제에서도, 이미지 데이터는 여러 픽셀들의 복잡한 상황을 고려해야하므로, 단순한 선형 모델로는 이러한 복잡성을 알아내기 어렵다.

b. Incorporating Hidden Layers

단순 선형 모델의 한계를 극복하기 위해 하나 이상의 층을 더 쌓는 방법을 취할 수 있다. 이 추가된 층을 은닉층(hidden layer)라고 한다. 이렇게 층이 추가된 네트워크 구조를 다층 퍼셉트론이라고 부르며, 종종 MLP로 약칭으로 부른다.

위의 예시를 보면, 네트워크는 4개의 입력, 3개의 출력, 은닉층에는 5개의 hidden node가 있다. 입력층에서는 따로 계산이 이루어지지 않기 때문에 두 개의 층에 대한 계산만 구현하면 된다. 이 때 해당 MLP의 층 수는 두 개라고 정의한다.

c. From Linear to Nonlinear

우선 위의 네트워크를 벡터 및 행렬을 활용해서 간단한 수식으로 표현해보자.

H=XW(1)+b(1),O=HW(2)+b(2)\bold {H = XW^{(1)} + b^{(1)}}, \\ \bold {O = HW^{(2)} + b^{(2)}}

이렇게 수식을 표현해보고 나서 관찰을 해보면 한 가지 문제가 있다. 식을 간단히 정리하면, 위의 연립 식은 하나의 선형 조합으로 표현된다.

O=(XW(1)+b(1))W(2)+b(2)=XW(1)W(2)+b(1)W(2)+b(2)=XW+b\bold {O = (XW^{(1)} + b^{(1)})W^{(2)} + b^{(2)} = XW^{(1)}W^{(2)} + b^{(1)}W^{(2)} + b^{(2)} = XW + b}

여기서 W=W(1)W(2),b=b(1)W(2)+b(2)\bold {W = W^{(1)}W^{(2)}, b = b^{(1)}W^{(2)} + b^{(2)}}이다.

위의 문제를 해결하기 위해, 우리는 hiddent layer의 hidden node를 계산할 때 activation function을 추가하기로 했다. 활성화 함수 σ(x)\sigma(x)를 활용하면 식을 다음과 같이 바꿀 수 있다.

H=σ(XW(1)+b(1)),O=HW(2)+b(2)\bold {H = \sigma (XW^{(1)} + b^{(1)})}, \\ \bold {O = HW^{(2)} + b^{(2)}}

여기서 활성화 함수는 rowwise(행 기준)가 아닌, elementwise(요소별 적용) 방식으로 적용된다. 또한 hidden layer의 수는 제한 없이 원하는 만큼 추가할 수 있다.

d. Universal Approximators

Cybenko(1989)에서 언급이 되었듯이, 하나의 은닉층을 포함하는 네트워크는 충분한 노드와 적절한 가중치가 주어진다면, 어떠한 함수라도 모델링할 수 있다고 제안되었다. 그렇다고 모든 문제를 단일 은닉층 네트워크로 학습하는 것은 매우 무모하고, 계산이 매우 어려워진다. 이에 따라 우리는 더 깊은(더 많은 hidden layer)를 추가하여 많은 함수를 훨씬 더 간결하게 근사할 수 있다.

2) Activation Functions

활성화 함수는 미분 가능한 함수로써, 선형 조합으로 만들어진 하나의 output에 해당하는 노드를 활성화 할 것인지 아닌지를 결정할 수 있다.

a. ReLU

ReLU(rectified linear unit) 함수는 미분이 쉽고, 구현이 매우 간단하여 인기가 많은 활성화 함수이다.

ReLU(x) = max(x,0)\mathrm {ReLU} (x) \ = \ \max(x,0)

ReLU 함수는 입력 값이 양수일 때 값을 그대로 유지하고, 음수이면 0으로 만든다. 함수를 시각화 하면 다음과 같다.

x = torch.arange(-8.0, 8.0, 0.1, requires_grad=True)
y = torch.relu(x)
d2l.plot(x.detach(), y.detach(), 'x', 'relu(x)', figsize=(5, 2.5))

ReLU 함수는 미분도 매우 간단하다. 음수일 때 0이고, 양수일 때 도함수는 1이다. 다만, 입력이 정확히 0일때 ReLU 함수는 미분 가능하지 않다는 점에 유의해야 한다. 우리는 입력이 0일 때 왼쪽의 미분 값을 이어서 미분값을 0으로 정의한다. 이는 실제로 데이터가 0인 경우가 드물기도 하고, 정확하게 수학적으로 해석하는 일이 중요한 문제가 아니기 때문이다. 아래는 ReLU의 도함수이다.

y.backward(torch.ones_like(x), retain_graph=True)
d2l.plot(x.detach(), x.grad, 'x', 'grad of relu', figsize=(5, 2.5))

ReLU 함수를 쓰는 가장 주된 이유는 작동이 잘 되기 때문이다. ReLU 함수의 도함수는 데이터를 사라지게 하거나, 단순하게 통과시켜주는 역할을 한다. 이는 최적화를 더 잘 하게 되고, vanishing gradient 문제를 완화 시켜준다. 현재는 ReLU의 변형 버전 pReLU(paramiterized ReLU)도 등장하였다.

pReLU(x)=max(0,x)+αmin(0,x)\mathrm {pReLU} (x) = \max (0, x) + \alpha \min (0, x)

b. Sigmoid

시그모이드 함수는 입려값을 (0, 1)인 boundary 안의 출력값으로 변환한다. 이 때문에 시그모이드를 종종 ‘압축 함수’라고도 불린다. 시그모이드가 등장하기 전, 임계값 활성화 함수가 가장 널리 쓰였는데, 이 함수는 임계값을 기준으로 미만일 때 0, 초과일 때 1의 값으로 출력값을 바꿔주는 함수였다. 하지만, gradient descent 기반의 학습 방법이 등장하면서, 암계값 활성화 함수보다 부드럽고 미분 가능한 시그모이드 함수를 더 많이 쓰게 되었다. 이 함수는 현재까지도 종종 쓰이지만, vanishing gradient 문제 때문에 대부분 ReLU 함수에 대체되었다. 다음은 sigmoid의 그래프이다.

y = torch.sigmoid(x)
d2l.plot(x.detach(), y.detach(), 'x', 'sigmoid(x)', figsize=(5, 2.5))

시그모이드의 도함수는 다음과 같이 구할 수 있다.

ddxsigmoid(x)=exp(x)(1+exp(x))2=sigmoid(x)(1 sigmoid(x))\frac d {dx} \mathrm {sigmoid}(x) \\ = \frac {\mathrm {exp} (-x)} {(1 + \exp(-x))^2 } = \mathrm {sigmoid}(x)(1 \ -\mathrm {sigmoid(x)})

시그모이드 함수의 도함수는 입력값이 0일 때 최대값을 갖고, 그 때의 값은 0.25이다.

# Clear out previous gradients
x.grad.data.zero_()
y.backward(torch.ones_like(x),retain_graph=True)
d2l.plot(x.detach(), x.grad, 'x', 'grad of sigmoid', figsize=(5, 2.5))

c. Tanh

tanh(hyperbolic tangent) 함수도 입력값들을 -1과 1 사이로 압축해준다.

tanh(x)=1exp(2x)1+exp(2x)\mathrm{tanh} (x) = \frac {1-\exp(-2x)} {1+\exp(-2x)}

그래프는 다음과 같다.

y = torch.tanh(x)
d2l.plot(x.detach(), y.detach(), 'x', 'tanh(x)', figsize=(5, 2.5))

도함수 및 그래프는 다음과 같다.

ddxtanh(x)=1tanh2(x)\frac d {dx}\mathrm {tanh} (x) = 1 - \mathrm {tanh}^2(x)
# Clear out previous gradients
x.grad.data.zero_()
y.backward(torch.ones_like(x),retain_graph=True)
d2l.plot(x.detach(), x.grad, 'x', 'grad of tanh', figsize=(5, 2.5))

2. Implementation of Multilayer Perceptrons

@리치

MLP는 단순한 선형 모델보다 구현하기가 훨씬 복잡하지 않다. 핵심적인 개념적 차이는 단순히 여러 개의 레이어를 연결한다는 점이다.

1) Implementation from Scratch

API나 프레임워크를 사용하는 대신, 알고리즘의 기본 원리부터 시작해서 코드를 작성해보자.

1.1) Initializing Model Parameters

이전 단원에서 Fashion-MNIST 데이터를 본 기억이 있을 것이다. 해당 데이터는 10개의 클래스가 포함되어 있고, 28X28 회색 스케일 픽셀 값의 격자로 구성되어 있다. 이전과 동일하게 이번 구현에서는 픽셀 간의 공간 구조를 무시하여 784개의 입력 특징과 10개의 클래스가 있는 분류 데이터 집합으로 생각한다.

먼저, 하나의 hidden layer와 256개의 hidden units을 가진 MLP를 구현한다. 해당 구현에서 레이어의 수와 너비는 모두 조정할 수 있다. 이는 일종의 하이퍼 파라미터로 간주할 수 있다. 일반적으로 레이어 너비를 2의 거듭 제곱으로 나눌 수 있도록 선택한다. (Batch에서도 비슷한 얘기가 나왔었다.) 이는 하드웨어에서 메모리가 할당되고 주소가 지정되는 방식 때문에 계산 상 효율적이다.

또한, 여러 개의 텐서를 사용해 파라미터를 표현한다. 모든 레이어에 대해 하나의 가중치 행렬과 하나의 편향(bias) 벡터를 추적해야 한다는 점을 기억해야 한다. 항상 그렇듯, 이러한 파라미터와 관련된 손실의 기울기를 위해 메모리를 할당한다.

class MLPScratch(d2l.Classifier):
    def __init__(self, num_inputs, num_outputs, num_hiddens, lr, sigma=0.01):
        super().__init__()
        self.save_hyperparameters()
        self.W1 = nn.Parameter(torch.randn(num_inputs, num_hiddens) * sigma) # 입력과 은닉층 사이의 가중치, sigma로 스케일 조정
        self.b1 = nn.Parameter(torch.zeros(num_hiddens)) # 은닉층의 편향, 모든 값이 0인 벡터 생성하여 초기 편향 설정
        self.W2 = nn.Parameter(torch.randn(num_hiddens, num_outputs) * sigma)
        self.b2 = nn.Parameter(torch.zeros(num_outputs))

nn.Parameter는 PyTorch의 텐서에 대해 자동 미분(autograd)를 가능하게 하는 특별 클래스다.

1.2) Model

우선, ReLU 활성화 함수를 직접 코드로 구현한다.

def relu(X):
    a = torch.zeros_like(X)
    return torch.max(X, a)

이미지: wolframalpha

다음으로 앞서 공간 구조를 무시하기로 했으므로, 각 2 차원 이미지를 길이 num_input의 평면 벡터로 재구성한다. 마찬가지로 torch 내부적으로 autograd가 적용된다.

@d2l.add_to_class(MLPScratch)
def forward(self, X):
    X = X.reshape((-1, self.num_inputs)) # 평면 벡터화 
    H = relu(torch.matmul(X, self.W1) + self.b1) # 활성화 함수 
    return torch.matmul(H, self.W2) + self.b2 # 최종 출력 

1.3) Training

Training loop은 softmax 회귀와 완벽하게 일치한다. 따라서, 아래 코드를 통해 데이터를 모델에 적합시킬 수 있다.

model = MLPScratch(num_inputs=784, num_outputs=10, num_hiddens=256, lr=0.1)
data = d2l.FashionMNIST(batch_size=256)
trainer = d2l.Trainer(max_epochs=10)
trainer.fit(model, data)

2) Concise Implementation

이제, 동일한 모델을 API를 통해 간단하게 구현해보자.

2.1) Model

Section 4.5에서 구현했던 softmax 회귀와 비교했을 때 다른 점은 단지 두 개의 fully connected 레이어를 추가했다는 점이다. (이전에는 한 개) 첫 레이어는 은닉층 이며, 두 번째는 출력층이다.

class MLP(d2l.Classifier):
    def __init__(self, num_outputs, num_hiddens, lr):
        super().__init__()
        self.save_hyperparameters()
        self.net = nn.Sequential(nn.Flatten(), nn.LazyLinear(num_hiddens),
                                 nn.ReLU(), nn.LazyLinear(num_outputs))

코드를 보면, forward 매서드를 정의하지 않은 것을 볼 수 있다. 본래 MLP는 3.2.2에서 구현했던 Module클래스를 상속받아 아래 코드처럼 호출한다.

# Section 3.2.2 Moudle Class
def forward(self, X):
        assert hasattr(self, 'net'), 'Neural network is defined'
        return self.net(X)

그러나, PyTorch에서 제공하는 nn.Sequential이라는 편리한 클래스를 제공한다. 해당 클래스를 통해, 모델의 계층을 순차적으로 나열하여 간단하게 모델 구현이 가능하다. 여기서 사용된 LazyLinear는 첫 번째 Forwad때 입력 특징의 수를 자동으로 감지하고, 이에 기반하여 해당 계층의 가중치를 초기화한다.

2.2) Training

Training loop는 softmax 회귀를 구현할 때와 완벽히 동일하다. 이러한 모듈화 덕분에 모델 아키텍처와 관련된 사항들을 다른 독립적인 요소나 고려 사항으로부터 분리할 수 있다. (orthogonal consideration)

model = MLP(num_outputs=10, num_hiddens=256, lr=0.1)
trainer.fit(model, data)

그러나, MLP를 처음부터 구현하는 것은 모델 매개변수의 이름을 지정하고 추적하는 것이 복잡하기 때문에 모델을 확장하기가 어렵다는 문제점이 있다. 예를 들어, 레이어 42와 43 사이에 다른 레이어를 삽입하고 싶다고 가정해 보자. 순차적으로 이름을 바꾸지 않는 한 이 레이어는 이제 42b 레이어가 될 수 있다. 또한, 네트워크를 처음부터 구현하면 프레임워크가 의미 있는 성능 최적화를 수행하기가 훨씬 더 어렵다.

3. Forward Propagation, Backward Propagation, and Computational Graphs

@Seoyoon.J

우리가 딥러닝 알고리즘을 구현할 때 forward propagation(순방향 전파)와 관련된 계산에 관심을 가지고, 자동으로 미분을 계산하는 역전파 함수를 호출하여 기울기 계산을 하곤 한다. 하지만 기울기가 내부적으로 어떻게 계산되는지 아는 것이 딥러닝을 잘 이해하는데 꼭 필요하다.

1) Forward Propagation

*단순화를 위해, Input은 x\bold{x}이고 bias(편향) 항이 포함되어 있지 않다고 가정

z=W(1)xh=ϕ(z)o=W(2)hL=l(o,y)s(l2reg)=λ2(W(1)F2+W(2)F2)J=L+s\bold{z} = \bold{W}^{(1)}\bold{x} \\ \bold{h} = \phi(\bold{z}) \\ \bold{o}=\bold{W}^{(2)}\bold{h} \\ L = l(\bold{o},y) \\ s(l_2 \medspace\text{reg}) = \frac{\lambda}{2}(\lVert \bold{W}^{(1)}\rVert_F^2+\lVert \bold{W}^{(2)}\rVert_F^2) \\ J = L+s

W(1),W(2)\bold{W}^{(1)},\bold{W}^{(2)} : weight parameter

z\bold{z} : weighted sum

ϕ\phi : activation function

h\bold{h} : hidden layer out

o\bold{o} : final output

ll : loss function

yy : label(ground truth)

λ\lambda : hyperparameter

JJ : objective function

> 행렬의 Frobenius Norm (xF2\lVert x \rVert_F^2)은 단순히 행렬을 벡터로 평탄화(flatten)한 후 적용되는 l2 norm

JJ는 모델의 정규화된 loss이며, 최종 objective function(목적 함수)로 값을 최소화하거나 최대화하는 것을 목적으로 하는 함수이다.

2) Computational Graph of Forward Propagation

Computational graph(계산 그래프)를 그리면 계산 내에서 연산자와 변수의 종속성을 시각화하는 데 도움이 된다. 아래 그림에서 사각형은 변수를 나타내고 원은 연산자를 나타낸다.

3) Backpropagation

Chain Rule
Y=f(X),Z=g(Y)ZX=ZYYX\bold{Y} = f(\bold{X}), \bold{Z}=g(\bold{Y}) \quad \frac{\partial\bold{Z}}{\partial\bold{X}} =\frac{\partial\bold{Z}}{\partial\bold{Y}}\cdot\frac{\partial\bold{Y}}{\partial\bold{X}}
(1)JL=1&Js=1(2)Jo=JLLo=1×Lo(3)sW(1)=λW(1)&sW(2)=λW(2)(4)JW(2)=JooW(2)+JssW(2)=Joh+λW(2)(5)Jh=Jooh=W(2)Jo(6)Jz=Jhhz=Jhϕ(z)(7)JW(1)=JzzW(1)+JssW(1)=Jzx+λW(1)\begin{align*} (1)\quad\frac{\partial J}{\partial L} = 1 \quad \& \quad \frac{\partial J}{\partial s} = 1 \\(2)\quad \frac{\partial J}{\partial \bold{o}} =\frac{\partial J}{\partial L} \cdot \frac{\partial L}{\partial \bold {o}}= 1\times\frac{\partial L}{\partial \bold{o}} \\(3)\quad\frac{\partial s}{\partial \bold{W}^{(1)}} = \lambda \bold{W}^{(1)} \quad \& \quad \frac{\partial s}{\partial \bold{W}^{(2)}} = \lambda \bold{W}^{(2)}\\(4)\quad \frac{\partial J}{\partial \bold{W}^{(2)}} = \frac{\partial J}{\partial \bold{o}} \cdot \frac{\partial \bold{o}}{\partial \bold {W}^{(2)}}+\frac{\partial J}{\partial \bold{s}} \cdot \frac{\partial \bold{s}}{\partial \bold {W}^{(2)}}=\frac{\partial \bold{J}}{\partial \bold {o}}\bold{h}^\top+\lambda\bold{W}^{(2)} \\(5)\quad \frac{\partial J}{\partial \bold{h}} = \frac{\partial J}{\partial \bold{o}} \cdot \frac{\partial \bold{o}}{\partial \bold{h}}=\bold{W}^{(2)^\top}\frac{\partial J}{\partial \bold{o}} \\(6)\quad \frac{\partial J}{\partial \bold{z}} = \frac{\partial J}{\partial \bold{h}} \cdot \frac{\partial \bold{h}}{\partial \bold{z}}=\frac{\partial \bold{J}}{\partial \bold{h}}\odot \phi'(\bold{z}) \\(7)\quad \frac{\partial J}{\partial \bold{W}^{(1)}} = \frac{\partial J}{\partial \bold{z}} \cdot \frac{\partial \bold{z}}{\partial \bold {W}^{(1)}}+\frac{\partial J}{\partial \bold{s}} \cdot \frac{\partial \bold{s}}{\partial \bold {W}^{(1)}}=\frac{\partial \bold{J}}{\partial \bold {z}}\bold{x}^\top+\lambda\bold{W}^{(1)} \end{align*}

(1) LLss에 대해 JJ 미분

(2) o\bold o에 대해 JJ 미분

(3) W(1),W(2)\bold {W}^{(1)}, \bold {W}^{(2)}에 대해 ss 미분

(4) 출력층에 가장 가까운 모델 매개변수(W(2)\bold {W}^{(2)})에 대해 JJ 미분

(5) 은닉층의 결과(h\bold h)에 대해 JJ 미분
* backpropagation; 출력층 → 은닉층

(6) z\bold z에 대해 JJ 미분
*
ϕ\phi가 요소별로 적용되므로, elementwise multiplication operator(\odot) 필요

(7) 입력층에 가장 가까운 모델 매개변수(W(1)\bold {W}^{(1)})에 대해 JJ 미분

4) Training Neural Networks

Forward propagation(순방향 전파)은 종속적인 방향으로 모든 변수를 계산하고, backpropagation(역전파)에서 그 계산 순서가 반대가 되기에 둘은 서로 의존적이다.

순방향 전파 동안 정규화 항(ss)을 계산하는 것은 모델 매개변수 W(1)\bold{W}^{(1)}W(2)\bold{W}^{(2)} 에 따라 달라진다. 반면, 역전파 동안 매개변수 W(1)\bold{W}^{(1)}W(2)\bold{W}^{(2)}에 대한 기울기 계산은 순방향 전파에 의해 제공되는 은닉층 출력 h\bold h에 의해 달라진다.

신경망을 훈련할 때, 처음에 모델 매개변수를 초기화한 후 역전파에 의해 계산된 기울기를 사용하여 모델 매개변수를 업데이트한다. 역전파는 중복 계산을 피하기 위해 순방향 전파로 저장된 중간 값을 재사용한다. 즉, 역전파가 끝날 때까지 중간 값을 유지해야 하기 때문에 일반 예측보다 훨씬 더 많은 메모리가 필요하다. 중간 값의 크기는 layer 수와 배치 크기에 비례하기 때문에, 더 큰 배치 크기를 사용하여 더 깊은 네트워크를 훈련하면 메모리 부족 오류가 더 쉽게 발생한다.

4. Numerical Stability and Initialization

@리치

지금까지는 미리 지정된 분포에 따라 매개변수를 초기화 했다. 하지만, 초기화 방식은 신경망 학습의 수치 안정성에 중요한 역할을 한다. 또한, 이러한 선택은 비선형 활성화 함수의 선택과 연관될 수도 있다. 함수의 선택과 매개변수의 초기화는 최적화 알고리즘이 얼마나 빨리 수렴 하는 지를 결정한다. 잘못 선택할 경우 기울기 소실(gradinets vanishing)이나 기울기 폭주(gradinets exploding)이 발생할 수 있다.

1) Vanshing and Exploding Gradients

LL개의 레이어가 있고, 입력 x\bold x와 출력o\bold o인 딥 네트워크를 생각해보자. 각 레이어 ll은 가중치 W(l)W^{(l)}에 의해 매개변수화된 변환 flf_l으로 정의되며, 여러 은닉층이 출력값 h(l)h^{(l)} (h(0)=x)(h^{(0)}=\bold x)생성한다.

h(l)=fl(h(l1)) and thus o=fLfl(x)h^{(l)}=f_l(h^{(l-1)})\text { and thus } \bold o=f_L \circ \cdots \circ f_l(\bold x)

만약, 모든 은닉층의 출력과 입력이 벡터라면, o\bold o의 기울기를 어떤 매개변수 W(l)W^{(l)}과 관련하여 아래 수식과 같이 쓸 수 있다. (역전파)

W(l)o=h(L1)h(L)M(L)=defh(l)h(l+1)M(l+1)=defW(l)h(l)v(l)=def\partial_{W^{(l)}}\bold o= \underbrace {\partial_{h^{(L-1)}}h^{(L)}}_{M^{(L)}\overset{\underset{\mathrm{def}}{}}{=}} \cdots \underbrace{\partial_{h^{(l)}}h^{(l+1)}}_{M^{(l+1)}\overset{\underset{\mathrm{def}}{}}{=}} \underbrace{\partial_{W^{(l)}} h^{(l)}}_{\text{v}^{(l)}\overset{\underset{\mathrm{def}}{}}{=}}

이 수식은 연쇄법칙(Chain Rule)을 적용해 기울기를 계산하고 있다. 이를 다르게 표현하면, M(l)=h(l1)h(l)M^{(l)}=\partial_{h^{(l-1)}}h^{(l)}로 정의하여 L1L-1개의 M(L)M(l+1)M^{(L)} \cdots M^{(l+1)}행렬들과 기울기 벡터 v(l)\bold v^{(l)}의 곱으로 이 기울기가 표현될 수 있다.

이런 확률의 곱을 연속해서 수행하는 경우, 언더플로우(underflow)문제가 발생할 수 있다. 일반적으로, 이러한 확률의 곱은 log\log를 통해 변환한다. 또한, 행렬의 고유값(Eigenvalues)의 값도 문제가 된다. 행렬들은 다양한 고유값을 가질 수 있으며, 그 값이 매우 크거나 작을 수 있다.

https://www.jefkine.com/general/2018/05/21/2018-05-21-vanishing-and-exploding-gradient-problems/

불안정한 기울기는 수치적 표현을 넘어서는 위험이다. 예측할 수 없는 크기의 기울기는 최적화 알고리즘의 안정성도 위협한다. 예를 들어, 지나치게 값이 큰 경우 역전파 과정에서 이러한 행렬들을 통과하는 기울기는 지수적으로 증가할 수 있다. 이를 앞서 언급한 Gradinet Exploding이라 부른다. 반대로, 지나치게 값이 작은 경우 신경망을 통과하면서 값이 점점 감소하게 된다. 이때, 매우 작은 기울기가 생성될 수 있으며 이를 Gradient Vanshing이라 부른다.

1.1) Vanshing Graidents

Vanishing Gradients를 일으키는 대표적인 활성화 함수로 Sigmoid 함수가 있다. Sigmoid 함수는 1/(1+exp(x))1/(1+\text {exp} (-x)) 이다.

x = torch.arange(-8.0, 8.0, 0.1, requires_grad=True)
y = torch.sigmoid(x)
y.backward(torch.ones_like(x))

d2l.plot(x.detach().numpy(), [y.detach().numpy(), x.grad.numpy()],
         legend=['sigmoid', 'gradient'], figsize=(4.5, 2.5))
이미지: wolframalpha

시그모이드 함수를 보면 알 수 있듯이 x의 크기에 상관없이 모두 Gradient Vanishing이 일어난다. 또한, 0에 가까운 Goldilocks zone에 있는 것이 아니라면, 결국 전체가 소실될 가능성이 있다. 그 결과, 더 안정적이지만 신경학적으로 덜 그럴듯한 ReLU가 더 자주 사용되게 되었다.

1.2) Exploding Gradients

Gradinet Exploding도 비슷한 문제이다. 이를 설명하기 위해 100개의 가우스 랜덤 행렬을 만들고 여기에 초기 행렬을 곱해보자. σ2=1\sigma^2=1일 때, 행렬 곱은 폭발적으로 증가한다. 이는, 딥 네트워크의 초기화로 발생하며 경사 하강 최적화기가 수렴하지 못한다.

M = torch.normal(0, 1, size=(4, 4))
print('a single matrix \n',M)
for i in range(100):
    M = M @ torch.normal(0, 1, size=(4, 4)) # 가우스 분포 
print('after multiplying 100 matrices\n', M)
a single matrix
 tensor([[-0.8755, -1.2171,  1.3316,  0.1357],
        [ 0.4399,  1.4073, -1.9131, -0.4608],
        [-2.1420,  0.3643, -0.5267,  1.0277],
        [-0.1734, -0.7549,  2.3024,  1.3085]])
after multiplying 100 matrices
 tensor([[-2.9185e+23,  1.3915e+25, -1.1865e+25,  1.4354e+24],
        [ 4.9142e+23, -2.3430e+25,  1.9979e+25, -2.4169e+24],
        [ 2.6578e+23, -1.2672e+25,  1.0805e+25, -1.3072e+24],
        [-5.2223e+23,  2.4899e+25, -2.1231e+25,  2.5684e+24]])

1.3) Breaking the Symmetry

Another problem in neural network design is the symmetry inherent in their parametrization(네트워크를 구성하는 가중치(weights)와 편향(biases) 값들을 설정하는 방법)? 하나의 은닉층과 2개의 유닛이 있는 간단한 MLP를 가정하자. 이 경우, 첫 레이어의 가중치 W(1)\bold W^{(1)}를 재배열하고 출력 계층의 가중치 역시 재배열하면 동일한 함수를 얻을 수 있다. 즉, 은닉층의 각 유닛은 permutation symmetric 하다.

앞서 언급한 두 개의 히든 유닛들을 생각해보자. 예를 들어, 출력층이 두 개의 히든 유닛을 하나의 출력 유닛으로 변환한다고 가정하자. 은닉층의 모든 파라미터를 어떤 상수 cc에 대해서 W(1)=c\bold W^{(1)} = c 같이 초기화하면 어떻게 될까?

이 경우 forward propagation에서 두 히든 유닛은 동일한 입력과 파라미터를 사용한다. 동일한 활성화를 생성하고 출력 유닛이 된다. Backpropagation에서 출력 유닛을 W(1)\bold W^{(1)}에 대해서 미분하게 되면 모든 요소가 똑같은 gradient값을 가지게 된다. 따라서, 미니 배치 SGD(Stochastic Gradient Descent)와 같은 경우에도 동일한 값을 가지게 된다. 결국, 대칭을 깨뜨리지 못하게 되고, 네트워크의 표현력을 잃게 된다. 은닉층은 마치 하나의 유닛만 있는 것처럼 작동하는데, 미니 배치 SGD는 이 대칭을 깨지 않지만 대신, Drop out regularization을 통해 가능하다.

2) Parameter Initialization

위에서 언급한 문제들을 완화하는 방법은 신중한 초기화이다. 또한, 후술할 최적화 과정에서의 적절한 정규화는 안정성을 향상 시킬 수 있다.

2.1) Defualt Initialization

이전 3.5섹션에서는 정규 분포를 사용해 가중치 값을 초기화 했다. 값을 지정하지 않으면 프레임워크는 무작위 값으로 초기화를 진행한다. 일반적으로, 적당한 문제 크기에 대해서 실제로 잘 동작한다.

2.2) Xavier Initailization

비선형성이 없는 레이어가 완전히 연결된 출력 oio_i의 스케일 분포를 살펴보자. 이 레이어에 대해 ninn_{\text{in}}개의 입력 xjx_j와 결합되어 있는 wijw_{ij}가 있을 때 출력은 아래와 같이 주어진다.

oi=j=1ninwijxjo_i=\sum_{j=1}^{n_{\text {in}}}w_{ij}x_j

가중치 wijw_{ij}는 모두 동일한 분포에서 독립적으로 나온다. 또한, 이 분포의 평균과 분산의 σ2\sigma^2이 0이라고 가정하자. (가우스 분포일 필요가 없으며, 평균과 분산이 존재해야함) 지금부터 레이어 xjx_j에 대해서 입력 값 역시 평균과 분산이 0이고, 독립이라고 가정하자. 이러한 경우 oio_i의 평균과 아래와 같다.

E[oi]=j=1ninE[wijxj]=j=1ninE[wij]E[xj]=0\begin{align*} E[o_i] &= \sum_{j=1}^{n_{\text {in}}} E[w_{ij}x_j] \\ &= \sum_{j=1}^{n_{\text {in}}}E[w_{ij}]E[x_j] \\ &= 0 \end{align*}

분산의 경우에는

Var[oi]=E[oi2](E[oi])2=j=1ninE[wij2xj2]0=j=1ninE[wij2]E[xj2]=ninσ2γ2\begin{align*} \text {Var}[o_i] &=E[o_i^2]-(E[o_i])^2 \\ &=\sum_{j=1}^{n_{\text {in}}}E[w_{ij}^2x_j^2] - 0 \\ &= \sum_{j=1}^{n_{\text {in}}}E[w_{ij}^2]E[x_j^2] \\ &= n_{\text{in}}\sigma^2\gamma^2 \end{align*}

분산을 고정 시키는 방법 중 하나는 ninσ2=1n_{\text{in}}\sigma^2=1로 놓는 것이다. 이제, 역전파를 생각해보자. 출력에 더 가까운 레이어에서 gradient가 전파되지만, 역시나 비슷한 문제가 발생한다. 순방향 전파의 경우 동일한 추론을 적용했을 때 noutσ2=1n_{\text{out}}\sigma^2=1 이 된다. (noutn_{\text{out}}은 레이어의 출력 수) 즉, 두 조건을 동시에 만족시킬 수 없는 딜레마에 빠지게 된다. 따라서, 이를 만족시키려 노력하게 된다.

12(nint+nout)σ2=1 or equvalently σ=2nin+nout\frac{1}{2}(n_{\text{int}}+n_{\text{out}})\sigma^2 =1 \text{ or equvalently } \sigma = \sqrt{\frac{2}{n_{\text{in}}+n_{\text{out}}}}

이것이 현재 표준으로 자리 잡은 실질적으로 유용한 Xavier 초기화의 기초가 되는 추론이다. 일반적으로 Xavier 초기화는 평균이 0이고, 분산이 σ2=2nin+nout\sigma^2 =\frac{2}{n_{\text{in}}+n_{\text{out}}}인 가우스 분포에서 가중치를 샘플링하게 된다. 또한, 균등 분포에서 가중치를 샘플링할 때도 분산을 선택하도록 할 수 있다. 균등 분포 U(a,a)U(-a,a)에서 분산은 a23\frac{a^2}{3}이다. 우리의 식에 σ2\sigma^2대신 넣으면 아래와 같은 수식이 된다.

U(6nin+nout,6nin+nout)U(-\sqrt{\frac{6}{n_{\text{in}}+n_{\text{out}}}}, \sqrt{\frac{6}{n_{\text{in}}+n_{\text{out}}}})

위의 수학적 추론에서 비선형성이 존재하지 않는다는 가정은 신경망에서 쉽게 위반될 수 있지만, Xavier 초기화 방법은 실제로 잘 작동하는 것으로 밝혀졌다.

그런데, 활성 함수가 ReLU인 경우 Xavier 초기화를 사용하면 데이터의 크기가 점점 작아지는 문제가 있다. 이는, 활성화 함수가 Sigmoid라는 가정을 했기 때문이다. (입력 데이터가 0근처의 작은 값이기 떄문에 Sigmoid의 중앙 부분을 지나게 되고, 이는 직선에 가깝기 때문이다.) 따라서, ReLU에서 음수 구간에서 가정과 맞지 않게 된다.

이를 해결하기 위해 He 초기화를 사용한다. He 초기화는 2n\sqrt{\frac{2}{n}}을 표준편차로 하는 정규 분포로 초기화한다.

Delving Deep into Rectifiers: Surpassing Human-Level Performance...
Rectified activation units (rectifiers) are essential for state-of-the-art neural networks. In this work, we study rectifier neural networks for image classification from two aspects. First, we...
https://arxiv.org/abs/1502.01852v1

5. Generalization in Deep Learning

@Tommy Kim

기계 학습 분야에서 우리는 항상 모든 모델을 훈련 데이터에 적합시켜서 회귀 및 분류 문제를 다루었다. 하지만, 가장 기계 학습에서 가장 중요한 것은 훈련 데이터 외에도 완전히 새롭게 추출된 새로운 예제들(알려진 것이든, 알려지지 않은 것이든)에 대해서도 정확한 예측을 하는 것이다. 다행히도, 여러 분야(컴퓨터 비전, 자연어 처리, 시계열 데이터, 추천 시스템, 전자 건강 기록, 비디오 게임에서의 가치 함수 근사)에서 확률적 경사 하강법으로 학습된 deep neural network는 꽤나 일반화 성능이 좋다는 것이 밝혀졌다. 하지만, 어떻게 신경망을 최적화할 수 있는지, 그 누구도 왜 학습된 심층 신경망이 일반화 성능이 좋은지는 설명하기 힘들다. 일단, ‘어떻게 최적화를 시킬 수 있는가?’에 대한 문제는 중요하지 않다. 많은 개발자 및 실무자들이 훈련데이터를 적합시킬 적당한 매개변수(hyper parameter)들을 그에 맞게 조절하기 때문이다. 그에 반해 왜 신경망이 일반화 성능이 좋은지에 대한 논의는 현재까지도 꾸준히 진행되어오는 중이다.

1) Revisiting Overfitting and Regularization

앞선 내용에서 알 수 있듯이 훈련 데이터에 대한 모델의 적합도와 테스트 데이터에 대한 모델의 적합도 사이의 차이를 일반화 갭(generalization gap)이라고 하며, 이 간극이 큰 경우에 과적합(overfitting)되었다고 판단한다. 아주 극단적인 경우에는, training data는 완벽하게 적합되었지만, test data에서는 엉망일 수 있는 것이다. 대부분의 경우에 우리는 모델이 너무 복잡하다고 판단하여, 특성의 수, 매개변수 등을 줄임으로써 과적합을 대응한다.

하지만 딥러닝의 경우에는 재밌는 현상이 발생한다. 이미지 및 텍스트 인식 및 분류 문제에서는 모델에서 노드와 epoch 수를 더욱 늘려 모델을 더 표현력 있게 만들면, 많은 경우에 일반화의 오류가 더 줄어드는 현상이 발생한다. 더 커진 복잡성이 처음에는 해를 끼치지만, 이후에는 ‘이중 하강’ 패턴이 등장하면서 도움이 될 수 있다.

이렇게 deep neural network가 일반화 성능이 좋아져도 고전적인 학습 이론(VC 차원, Rademacher 복잡도)들은 왜 이러한 현상이 발생하는 지 설명해줄 수 없다.

2) Inspiration from Nonparametrics

많은 매개변수들로 이루어진 딥러닝 모델은 parametric 모델로 생각하는 것이 자연스러울 수 있다. 하지만, 어떤 면에서는 신경망을 nonparametric 모델로 행동한다고 생각하는 것이 딥러닝에 대한 이해도를 높일 수 있다.

nonparametric model의 가장 간단한 예시는 k 최근접 이웃 알고리즘이다. 이 알고리즘에서 모델은 데이터 셋을 기억한다. 그 후에 예측을 진행할 때, 새로운 점
xx가 주어지면, 모델은 k개의 가장 가까운 이웃한 점을 찾는다. k가 1이면, 이 알고리즘 이름은 1 최근접 이웃이라고 불리며, 훈련 오류는 항상 0이 된다(가장 가까운 점은 자기 자신이 되므로). k가 1보다 클 때도, 다양한 거리 함수에 대해서 1-최근접 이웃 알고리즘과 비슷하게 훈련 오류를 0으로 만들 수 있지만, 다양한 거리 함수에 대해 다양한 예측 결과가 나올 수 있다.

위의 신경망 모델도 마찬가지로 training error는 거의 0으로 완벽하게 적합시킬 수 있지만, 실제 test error는 적합하지 않을 수 있다는 사실을 고려할 때, 신경망 모델도 비모수적 모델(non-parametric)과 유사하게 동작한다고 볼 수도 있다.

3) Early Stopping

최근 연구에 따르면 deep neural network는 학습이 반복됨에 따라, 무작위의 라벨을 모두 적합할 수 있는 사실을 발견했다. 신경망 모델은 먼저 깨끗하게 라벨링된 데이터를 먼저 적합시키고, 그 후에 라벨링이 잘못된 데이터까지 보간하여 적합하기 때문에, 오히려 깨끗하게 라벨링된 데이터만 적합시켰을 때 일반화 성능이 더 뛰어났다는 것이다.

이를 해결하기 위해 고전적인 학습 방법 중 하나 조기 종료(early stopping)이 도움이 된다. 훈련의 반복수(epoch)를 조절하는 방법이다. 데이터의 일부를 validation set으로 나눈 후에, 검증 오류를 모니터링하면서 지정된 epoch 동안 검증 오류가 감소하지 않는다면 training을 중단하는데, 이 지정된 epoch 수를 patience criterion이라고 부른다. early stopping은 시간이 절약된다는 또 다른 이점이 있다.

중요한 점은, 라벨에 노이즈가 없는 데이터셋만 존재할 때(고양이, 개를 구분하는 것으로 확실히 분리가 될 때)는 early stopping이 성능의 개선을 이끌어내지는 않지만, 라벨에 변동성이 큰 경우(ex : 환자의 사망률 예측)는 early stopping이 중요하다.

4) Classical Regularization Methods for Deep Networks

딥러닝 모델의 과적합을 막기 위해 고전적인 규제 방법이 도입되었다. 대표적으로는 릿지 정규화 (l2l_2 penalty)나 라쏘 정규화(l1l_1 penalty)등이 있다. 하지만, 이러한 정규화 방법 만으로는 충분하지는 않고, early stopping등과 같은 다른 방법과 결합될 때 더 효과적이라는 사실이 밝혀졌다. 또한, 정규화 기법들은 확장 및 발전을 거듭하며, 최근에는 dropout(활성화되는 노드 수 제한)과 같은 기법들도 등장하였다.

6. Dropout

@Seoyoon.J

고전적인 일반화 이론(Classical generalization theory)은 훈련과 테스트 성능 간의 격차를 줄이기 위해 간단한 모델을 목표로 해야 한다고 제안한다. ‘단순성’은 작은 차원의 형태로 나타낼 수 있다. 가중치 감소(weight decay)를 통한 매개변수의 norm도 단순성의 유용한 척도가 되며, 단순성은 함수가 입력의 작은 변화에 민감하지 않다는 것을 의미하기도 한다.

Bishop (1995)에 의해 입력에 노이즈를 추가하여 훈련하는 것이 Tikhonov 정규화와 동일하다는 것이 증명되었고, 함수가 단순해야한다는 것과 이상치가 포함된 입력에 탄력적이어야한다는 것 사이에 수학적인 연결을 이끌어 내면서 이 아이디어가 공식화 되었다.

Srivastava et al. (2014)는 ‘드롭아웃(dropout)’을 제시하며 Bishop의 아이디어를 네트워크의 내부 계층에 적용하는 방법으로 발전시켰다. 이는 순방향 전파 동안 각 내부 층 계산에 노이즈를 주입하는 것과 관련되며, 훈련 중에 일부 뉴런을 삭제 즉, 다음 층 계산 전에 노드의 일부를 0으로 만드는 것으로 구성된다.

노이즈를 주입하는 방법이 핵심 과제가 되는데, 한 가지 방법은 unbiased manner를 취해준다. - Bishop는 선형 모델의 입력에 가우스 노이즈를 추가했다. 각 훈련 반복에서 평균이 0인 분포에서 샘플링된 노이즈를 입력 x\bold x에 추가하여, x=x+ϵ\bold{x}' = \bold x + \epsilon인 혼란스러운 지점을 만들어낸다. 이 때, E[x]=xE[\bold x '] = \bold x.

표준 드롭아웃 정규화에서는 각 층에 있는 노드의 일부 부분을 0으로 만든 다음 유지된(드롭아웃되지 않은) 노드의 비율로 정규화하여 각 층의 편향성을 제거한다. 즉, 드롭아웃 확률(dropout rate) p를 사용하면 각 중간 activation function h는 다음과 같이 확률 변수 h'로 대체된다. 이때, 기댓값은 변하지 않는다(E[h]=hE[h']=h).

h={0with probabilityph1potherwiseh' = \begin{cases}0 \quad\text{with probability}\medspace p \\ \frac{h}{1-p}\quad\text{otherwise} \end{cases}

1) Dropout in Practice

은닉층에 드롭아웃을 적용하여 각 은닉 유닛을 확률 p로 0으로 만들면 결과는 원래 뉴런의 하위 집합만 포함하는 네트워크로 볼 수 있다. 위 그림에선 h2h_2%h5h_5가 제거됨에 따라 출력층의 계산은 더 이상 h2h_2%h5h_5에 의존하지 않으며 역전파를 수행할 때도 해당 기울기는 사라진다. 이를 통해, 출력층의 계산은 h1,,h5h_1,\ldots,h_5 중 하나에 지나치게 의존할 수 없어지게 된다.

일반적으로 테스트 단계에서는 드롭아웃을 비활성화하여 정규화가 필요없다. 그러나 일부 연구자는 신경망 예측의 불확실성을 추정하기 위한 경험적 방법으로 테스트 시 드롭아웃을 사용한다. 예측이 다양한 드롭아웃 출력에서 일치하면 네트워크를 더 신뢰할 수 있다고 말할 수 있다.

2) Imprementation from Scratch

단일 층에 대한 드롭아웃 기능을 구현하기 위해선 Bernulli(이진) 랜덤 변수에서 많은 샘플을 뽑아야 한다. 이때, 랜던 변수는 pp 확률로 0(drop), 1p1-p 확률로 1(keep)이다. 이를 간단히 구현하는 방법은 균일 분포 U[0,1]U[0,1]에서 샘플을 추출한 후 p보다 큰 샘플의 노드는 유지하고 나머지는 삭제하는 것이다.

def dropout_layer(X, dropout):
    assert 0 <= dropout <= 1 # dropout rate
    if dropout == 1: return torch.zeros_like(X)
    mask = (torch.rand(X.shape) > dropout).float()
    return mask * X / (1.0 - dropout) # Remain node(dropout x) - regularization

X = torch.arange(16, dtype = torch.float32).reshape((2, 8))
print('dropout_p = 0:', dropout_layer(X, 0))
print('dropout_p = 0.5:', dropout_layer(X, 0.5))
print('dropout_p = 1:', dropout_layer(X, 1))
# > dropout_p = 0: tensor([[ 0.,  1.,  2.,  3.,  4.,  5.,  6.,  7.],
#        [ 8.,  9., 10., 11., 12., 13., 14., 15.]])
# > dropout_p = 0.5: tensor([[ 0.,  2.,  0.,  6.,  8.,  0.,  0.,  0.],
#        [16., 18., 20., 22., 24., 26., 28., 30.]])
# > dropout_p = 1: tensor([[0., 0., 0., 0., 0., 0., 0., 0.],
#        [0., 0., 0., 0., 0., 0., 0., 0.]])

Defining the Model & Training

각 은닉층의 출력에 대해 드롭아웃을 적용한다. 일반적으로 입력층에 가까울수록 드롭아웃 확률을 낮게 설정한다. 또한, 훈련 중에만 드롭아웃이 활성화되도록 해야한다.

class DropoutMLPScratch(d2l.Classifier):
    def __init__(self, num_outputs, num_hiddens_1, num_hiddens_2,
                 dropout_1, dropout_2, lr):
        super().__init__()
        self.save_hyperparameters()
        self.lin1 = nn.LazyLinear(num_hiddens_1)
        self.lin2 = nn.LazyLinear(num_hiddens_2)
        self.lin3 = nn.LazyLinear(num_outputs)
        self.relu = nn.ReLU()

    def forward(self, X):
        H1 = self.relu(self.lin1(X.reshape((X.shape[0], -1))))
        if self.training:
            H1 = dropout_layer(H1, self.dropout_1)
        H2 = self.relu(self.lin2(H1))
        if self.training:
            H2 = dropout_layer(H2, self.dropout_2)
        return self.lin3(H2)
hparams = {'num_outputs':10, 'num_hiddens_1':256, 'num_hiddens_2':256,
           'dropout_1':0.5, 'dropout_2':0.5, 'lr':0.1} # dropout_1, dropout_2 = dropout rate
model = DropoutMLPScratch(**hparams)
data = d2l.FashionMNIST(batch_size=256)
trainer = d2l.Trainer(max_epochs=10)
trainer.fit(model, data)

3) Concise Implementation

FC(Fully Connected) layer 뒤에 드롭아웃 레이어를 추가하고 드롭아웃 확률을 생성자에 인수(argument)로 전달한다. 학습 중에 드롭아웃 레이어는 지정된 드롭아웃 확률에 따라 이전 층의 출력(or 후속 층의 입력)을 무작위로 제거한다. 학습 모드가 아닌 경우에는, 드롭아웃 레이어는 무시된다(데이터 단순 통과).

 class DropoutMLP(d2l.Classifier):
    def __init__(self, num_outputs, num_hiddens_1, num_hiddens_2,
                 dropout_1, dropout_2, lr): # dropout_1, dropout_2 = dropout rate
        super().__init__()
        self.save_hyperparameters()
        self.net = nn.Sequential(
            nn.Flatten(), nn.LazyLinear(num_hiddens_1), nn.ReLU(),
            nn.Dropout(dropout_1), nn.LazyLinear(num_hiddens_2), nn.ReLU(),
            nn.Dropout(dropout_2), nn.LazyLinear(num_outputs))

model = DropoutMLP(**hparams)
trainer.fit(model, data)