11-30 08:51
Notice
Recent Posts
Recent Comments
관리 메뉴

Scientific Computing & Data Science

[Artificial Intelligence / Machine Learning] R 딥러닝: 인공신경망 바닥부터 구현하기 Part 2. 본문

Artificial Intelligence/Machine Learning

[Artificial Intelligence / Machine Learning] R 딥러닝: 인공신경망 바닥부터 구현하기 Part 2.

cinema4dr12 2017. 8. 4. 16:31

Written by Geol Choi | 

부제목: 인공신경망 처절하게 제대로 이해하기


지난 포스팅에서 R에서 딥러닝을 바닥부터(from scratch) 구현하는 방법에 대해 개괄적으로 살펴본 적이 있는데, 이번 포스팅에서는 코드를 자세하게 분석하면서 수학적으로 과정을 풀어보고자 합니다.

1. 데이터 준비

딥러닝 코드를 작성하기 위해 테스트 용도의 데이터로 iris 데이터셋을 사용할 것입니다. iris는 일종의 꽃을 꽃받침 및 꽃잎의 폭과 길이 등으로 분류한 데이터입이며, R의 기본 패키지에 포함이 되어 있습니다.


대략적인 데이터의 형태는 다음과 같습니다:


> head(iris)
  Sepal.Length Sepal.Width Petal.Length Petal.Width Species
1          5.1         3.5          1.4         0.2  setosa
2          4.9         3.0          1.4         0.2  setosa
3          4.7         3.2          1.3         0.2  setosa
4          4.6         3.1          1.5         0.2  setosa
5          5.0         3.6          1.4         0.2  setosa
6          5.4         3.9          1.7         0.4  setosa


총 4개의 Features(Sepal.Length, Sepal.Width, Petal.Length, Petal.Width)를 가지며, Species는 꽃을 분류한 클래스(setosa, versicolor, virginica로 총 3개의 클래스)입니다. 데이터의 개수는 총 150개입니다.


주어진 데이터를 이용하여 학습 및 테스트를 위해 test datasettrain dataset으로 나눕니다:


1
2
3
4
# 1. split data into test/train
samp <- base::c(base::sample(1:50,25),
                base::sample(51:100,25),
                base::sample(101:150,25))
cs


samptest dataset으로 데이터의 인덱스를 1~50 범위에서 25개, 51~100 범위에서 25개, 101~150 범위에서 25개씩 총 75개를 가급적 고르게 선택한 것입니다.


선택된 인덱스에 해당하는 데이터셋(Dataset)을 traindata로, 이들을 제외한 나머지를 testdata로 지정합니다:


1
2
traindata = iris[samp,]
testdata = iris[-samp,]
cs

2. Neural Network 구성

구성한 Neural Network는 Multilayer Perceptron(MLP) 형태로, 4개의 Input Feature를 갖는 입력 레이어(Layer)와 6개의 노드(Node)를 포함하는 1개의 Hidden Layer, 3개의 출력 노드를 갖는 출력 레이어로 구성하였습니다. 다음 이미지를 참고하시기 바랍니다.



D(=4)는 Input Feature의 개수를, H(=6)는 Hidden Layer 내 노드 개수를, K(=3)는 의미합니다. 앞으로의 설명은 구체적인 숫자를 언급하기 보다는 가급적 일반화를 위해 이들 변수 이름을 이용하여 설명을 사용할 것이니 각 변수의 값을 기억해 두시면 설명을 좀 더 이해하는데 도움이 될 것입니다.

3. Feed-forward Operations

전체적인 네트워크 구성은 위의 이미지와 같지만, 사실 활성화 함수(Activation Function), Softmax Classifier 등 더 많은 복잡한 계산이 포함됩니다.


그래서 좀 더 구체적으로 표현한 것이 다음 이미지입니다.





조금 복잡해 보이지만 수학적 설명을 곁들여 하나씩 풀어 나가보도록 하겠습니다.


\(\mathbf{X}\)는 Input Feature 벡터입니다. 75개의 데이터로 구성되어 있지만 설명을 간단하게 하기 위해 1개의 데이터를 갖는다고 가정하도록 하겠습니다. 따라서, \(\mathbf{X}\)의 차원은 [75×4] (또는 [N×D])가 아닌 [1×4] (또는 [1×D])입니다.


첫번째 Weight \(\mathbf{W}^{(1)}\)는 차원 [D×H]를 갖는 행렬이며, 첫번째 Bias \(\mathbf{b}^{(1)}\)는 차원 [1×D]를 갖는 벡터입니다(사실 1차원 행렬이라고해도 무방합니다).


\(\mathbf{\Sigma}^{(1)}\)은 \(\mathbf{W}^{(1)}\)과 \(\mathbf{X}\)를 행렬곱하고 \(\mathbf{b}^{(1)}\)을 더한 것입니다. 이를 수학적으로 표현하면 다음과 같습니다(편의 상 Tensor 형식으로 수식을 표현하고자 하며 중복첨자는 Summation을 의미합니다):


Op.[1]  \( \displaystyle{ \sigma_{i}^{(1)} = x_k w_{ki}^{(1)} + b_{i}^{(1)} } \), \(i = 1, ..., H\), \(k=1,...,D\)


활성화 함수는 ReLU(Rectified Linear Unit)을 사용하였으며, 이는 0보다 작은 경우에는 0을, 0보다 큰 경우에는 자기 자신을 출력하는 함수입니다. 이에 대한 수식을 다음과 같습니다:


Op.[2]  \( h_i = \mathrm{max}(0, \sigma_{i}^{(1)}) \), \(i = 1,...,H\)


Hidden Layer \(\mathbf{H}\)는 두번째 Weight 행렬인 \(\mathbf{W}^{(2)}\)(차원 [H×K])와 행렬곱을 한 후 두번째 Bias 벡터인 \(\mathbf{b}^{(2)}\)(차원 [1×K])와 더해집니다:


Op.[3]

\( \sigma_{i}^{(2)} = h_k w_{ki}^{(2)} + b_{i}^{(2)} \), \(i = 1,...,K\), \(k = 1,...,H\)


방금 Op.[3]을 통해 계산된 \( \mathbf{\Sigma^{(2)}}\)(행렬 형태) 또는 \(\sigma_{i}^{(2)}\)(행렬의 엘리먼트 형태)를 특별히 score라고 칭하도록 하겠습니다(나중에 코드의 변수와 대응할 것입니다).


Information Theory에 근거하여 Data Loss를 계산하기 위해 \(\sigma_{i}^{(2)}\)에 Exponential 함수를 적용합니다.


Op.[4]  \( e_i = \mathrm{exp}(\sigma_{i}^{(2)}) \), \(i=1,...,K\)


Op.[4]에 의해 계산된 결과, 행렬 \(\mathbf{E}\)는 차원 [1×3]의 경우 다음과 형태를 갖습니다.


\( \mathbf{E} = \begin{bmatrix} \mathrm{exp}(\sigma_{1}^{(2)}) & \mathrm{exp}(\sigma_{2}^{(2)}) & \mathrm{exp}(\sigma_{3}^{(2)}) \end{bmatrix}  \)


이제 행렬 \(\mathbf{E}\)의 각 엘리먼트를 엘리먼트를 모두 더한 값으로 나눕니다.


Op.[5]  \( \displaystyle{ q_i = \frac{e_i}{\sum_{k=1}^{K}{e_k}} = \frac{e_i}{S} } \), \(i = 1,...,K\)

여기서, \(S = \displaystyle{\sum_{k=1}^{K}{e_k}} \)


학습하는 과정에서 실제 데이터 라벨(또는 클래스) - 이를 Ground Truth Label이라고 합니다 - 과 예측된 라벨을 비교하여 차이가 나는 경우 Information Theory에 입각하여 이를 데이터 손실(Data Loss)라고 하며, True Distribution \(p\)와 Estimated Distribution \(q\) 사이의 Cross-entropy \(H(p,q)\)로 표현합니다:


\(H(p,q) = \displaystyle{{p \mathrm{log}\begin{pmatrix}\displaystyle{\frac{1}{q}}\end{pmatrix}}} = -p \mathrm{log}(q) \)


\(K\)개의 Class에 대하여 표현하면,


\( H(p,q) = \displaystyle{- \sum_{k=1}^{K}{p_k \mathrm{log}(q_k)}} \)



과 같습니다. True Distribution \(p\)는,


\( \mathbf{p} = \begin{bmatrix} p_1 & p_2 & ... & p_K \end{bmatrix} \), \( \displaystyle{\sum_{k=1}^{K}{p_K} = 1} \)


인데, Ground Truth Label이 y번째 Index, 즉, \(k=y\)이면 결국, \( p_{k=y} = 1 \)이며, \( p_{k \ne y} =0 \)으로, y번째 Index만이 1이며, 그 외에는 모두 0가 됩니다. 따라서, Kronecker Delta Property를 이용하여 다음과 같이 간단하게 표현할 수 있습니다:


\( H(p,q) = -\mathrm{log}(q_k) \cdot \delta_{ky}\), \(k=1,...,K\), \(y \in [1,...K]\)


Cross-entropy는 Data Loss를 의미하므로, Ground Truth Label y에 대한 Data Loss \(L_D\)는,


Op.[6]  \( L_D = -\mathrm{log}(q_k) \cdot \delta_{ky}\), \(k=1,...,K\), \(y \in [1,...K]\): Ground Truth Label


위의 식에서 첨자(Subscript) \(k\)가 두 번 중복되어 있음을 볼 수 있는데 Tensor 표현에 의하면 일반적으로 중복 첨자는 이 첨자에 대하여 합 연산(Summation)을 수행합니다. 따라서, 다음과 같이 Data Loss 식을 더욱 간단히 표현할 수 있습니다:


Op.[7]  \( L_D = -\mathrm{log}(q_y) \)


Op.[7]에 의해 계산된 \(q\)를 correct.logprobs라 칭하도록 하겠습니다. 1개의 데이터에 대한 예를 들어 설명하고 있지만, 만약 여러 개의 데이터인 경우, 이들을 평균을 계산하여 Data Loss로 계산합니다. 가령, N개의 데이터에 대하여 Data Loss는,


Op.[8]  \( \displaystyle{ L_D = - \frac{1}{N}\sum_{i=1}^{N}{\mathrm{log}(q_i)} } \)


로 확장할 수 있습니다. 여기서, \(L_D\)는 전체 데이터셋에 대한 평균 Data Loss를 의미하며, \(q_k\)는 k번째 데이터의 Data Loss를 의미합니다.


Loss는 앞서 언급한 Data Loss 외에 Regularization Loss라는 것이 있습니다. Stanford CS231n 강의 사이트에 매우 잘 설명되어 있는데, 간략히 요약하자면, 특정 데이터에 대하여 과도하게 가중치가 부여되는 과도적합(Overfitting)이라고 하며 이를 방지하기 위해 Regularization Loss(L-2 Regularization)를 다음과 같이 정의합니다:


Op.[9]  \( L_R = \displaystyle{\frac{1}{2}} \lambda \displaystyle{\sum_{k}{\sum_{l}{w_{kl}^2}}} \)


여기서, \(\lambda\)는 Hyperparameter의 일종으로 Regularization Factor라고 하며 1/2을 곱한 이유는 Weights에 대한 L-2 Regularization(모든 Weights를 제곱을 하여 더한 것)에 대하여 미분을 할 경우 1/2을 제거하기 위함입니다 (상수 계수 어떤 값이 곱해지더라도 최적화의 결과는 동일합니다).


Data Loss \(L_D\)와 Regularization Loss \(L_R\)를 더한 전체 Loss \(L\)를 변수 \(\mathbf{W}\)와 \(\mathbf{b}\)에 대하여 최소화합니다:


Op.[10]  \( \displaystyle{ \min_{\mathbf{W, b}}{L = L_D + L_R} }\)

지금까지의 과정을 간단하게 수식으로 정리하면 다음과 같습니다(첨자 표현은 앞의 과정과 일치하지 않을 수 있습니다):


Op.[ForwardPass]


\(N\)개의 Dataset, \(D\)개의 Input Dimensionality, \(H\)개의 뉴런을 갖는 1개의 Hidden Layer, \(K\)개의 Output Class를 갖는 Multi-layer Perceptron에 대하여,


\( \sigma_{i,j}^{(1)} = x_{i,k} w_{kj}^{(1)} + b_{i,j}^{(1)} \), \(i=1,...,N\), \(j=1,...,H\), \(k=1,...,D\)


\( h_{i,j} = \mathrm{max}(0, \sigma_{i,j}^{(1)}) \), \(i=1,...,N\), \(j=1,...,H\)

hidden.layer[N×H] = h_{i,j}


\( \sigma_{i,j}^{(2)} = h_{i,k} w_{kj}^{(2)} + b_{i,j}^{(2)} \), \(i=1,...,K\), \(j=1,...,H\), \(k=1,...,H\)

score[N×K] = \(\sigma_{i,j}^{(2)}\)


\( e_{i,j} = \mathrm{exp}(\sigma_{i,j}^{(2)}) \), \(i=1,...,N\), \(j=1,...,K\)

score.exp[N×K] = \(e_{i,j}\)


\( \displaystyle{ q_{i,j} = \frac{e_{i,j}}{\sum_{k=1}^{K}{e_{i,k}}}} \), \(i=1,...,N\), \(j=1,...,K\)

probs[N×K] = \(q_{i,j}\)


\( L_{D_{i}} = - \mathrm{log}(q_{y_{i}}) \)

correct.logprobs[N×1] = \(L_{D_{i}}\)


\( L_D = \displaystyle{ L_D = \frac{1}{N}\sum_{i=1}^{N}{L_{D_{i}}} = - \frac{1}{N} \sum_{i=1}^{N}{q_{y_{i}}} } \)

data.loss = \(L_D\)


\( \displaystyle{ L_R = \frac{1}{2} \lambda \begin{bmatrix} \displaystyle{\sum_{k=1}^{D}{\sum_{l=1}^{H}{w_{kl}^{(1)}}}^2} + \displaystyle{\sum_{k=1}^{H}{\sum_{l=1}^{K}{w_{kl}^{(2)}}}^2} \end{bmatrix} } \)

reg.loss = \(L_R\)


\( L = L_D + L_R \)

loss = \(L\)

4. Backward Pass

Backward Pass 또는 Backpropagation은 학습을 하는 과정 동안 최적화 파라미터들(Optimization Parameters)을 업데이트 하기 위해 이 파라미터들에 대한 전체 Loss의 민감도(Sensitivity)를 계산하는 과정인데, Loss로부터 네트워크를 Feed-forward 방향의 역으로 거슬러 올라가면서 계산하기 때문에 Backward Pass라고 불리워집니다.


다시 데이터셋트의 1개의 데이터에 대하여 설명을 진행하겠으나, N개의 데이터에서도 쉽게 확장할 수 있으니, 1개의 데이터에 대해 확실히 이해를 하는 것이 중요합니다.


좀 더 명확한 설명을 위해 Hidden Layer ~ Classification 부분을 다음과 같이 따로 떼어 놓겠습니다.






간단하게 그림을 설명 드리면, 우선 Hidden Layer \(\mathbf{h}\)로부터,


\( \sigma_{i}^{(2)} = w_{ki}^{(2)}h_k + b_{i}^{(2)} \), \(i=1,...,K\) \(k=1,...,H\)


이며, \(K\)와 \(H\)는 각각 Class의 개수와 Hidden Layer 내 Neuron의 개수를 의미합니다. 별다른 설명이 없는 한, Tensor Notation에 의해 중복첨자에 대해서는 합 연산(Summation)을 하는 것으로 간주하도록 하겠습니다.


그리고, \(\sigma_{i}^{(2)}\)에 Exponential 함수를 취하여,


\( e_{i} = \mathrm{exp}(\sigma_{i}^{(2)}) \), \(i=1,...,K\)


와 같이 표현하였습니다. 또한 각 Class의 Score 확률 \(q_i\)는,


\( q_i = \displaystyle{\frac{e_i}{\displaystyle{\sum_{j=1}^{K}{e_j}}}} = \displaystyle{\frac{e_i}{S}} \)


이며, Ground Truth Label의 Index \(y\)에 대하여 Data Loss \(L_D\)는,


\( L_D = - \displaystyle{ \sum_{i=1}^{N}{\mathrm{log}(q_{y_i})} } \)


입니다.


일단 1차 목표는, score에 대한 Data Loss의 민감도 식 \( \displaystyle{ \frac{\partial L_D}{\partial \sigma_{i}^{(2)}} }\)를 구하는 것이 되겠습니다. 이것은 다음과 같은 Chain Rule에 의해 식을 유도할 수 있습니다.


\( \displaystyle{ \frac{\partial L_D}{\partial \sigma_{i}^{(2)}} = \frac{\partial L_D}{\partial q_y} \frac{\partial q_y}{\partial e_k} \frac{\partial e_k}{\partial \sigma_{i}^{(2)}}}  \)


이에 대한 첫번째 단계는 correct.logprobs에 대한 Data Loss의 민감도 식, 즉, 편미분(Partial Derivatives) 식을 구하는 것입니다. \(N = 1\)이므로 \(q_{y_i} \rightarrow q_y \)로 간단히 표현하여,


Op.[11]  \( \displaystyle{ \frac{\partial L_D}{\partial q_y} } = \displaystyle{ \frac{\partial}{\partial q_y} \begin{pmatrix} -\mathrm{log}(q_y) \end{pmatrix} } = \displaystyle{-\frac{1}{q_y}} \)


와 같이 식을 얻습니다. 그 다음, \(q_y\)에 대한 \(e_k\)의 편미분 식을 구합니다.


Op.[12]  \( \displaystyle{ \frac{\partial q_y}{\partial e_k} = \displaystyle{ \frac{\partial}{\partial e_k} \begin{pmatrix} \displaystyle{\frac{e_y}{S}} \end{pmatrix} } } = \displaystyle{ \frac{\delta_{ky} - e_y}{S^2} } \)


이런 식으로 Forward Pass를 차례차례로 역으로 편미분 식을 유도하면 됩니다. 다음은 \(\sigma_{i}^{(2)}\)에 대한 \(e_k\)의 편미분을 구하는 식입니다:


Op.[13]  \( \displaystyle{ \frac{\partial e_k}{\partial \sigma_{i}^{(2)}} } = \displaystyle{ \frac{\partial}{\partial \sigma_{i}^{(2)}}[\mathrm{exp}(\sigma_{k}^{(2)})] } = \mathrm{exp}(\sigma_{k}^{(2)}) = e_k \delta_{ik} \), no sum on \(k\)


Op.[13]의 경우, \(k\)가 중복첨자이지만 합 연산을 하지 않습니다. 이제 Op.[11]~[13]의 Chain Rule을 이용하여 \(\sigma_{i}^{(2)}\) 에 대한 Data Loss의 편미분 값을 구할 수 있습니다:


Op.[14]  \( \displaystyle{\frac{\partial L_D}{\partial \sigma_{i}^{(2)}}} = \displaystyle{ \frac{\partial L_D}{\partial q_y} \frac{\partial q_y}{\partial e_k} \frac{\partial e_k}{\partial \sigma_{i}^{(2)}} } = \displaystyle{ -\frac{S}{e_y} \frac{\delta_{ky}S - e_y}{S^2} e_k \delta_{ik} = \frac{e_y e_k \delta_{ik}}{e_y S} - \frac{\delta_{ky} S e_k \delta_{ik}}{e_y S} = \frac{e_i}{S} - \frac{\delta_{ky} e_k \delta_{ik}}{e_y} } \)

         =  \(q_i - \delta_{ky} \delta_{ky} \delta_{ik} = q_i - \delta_{iy}\)


지금까지 복잡한 수식을 잘 따라오셨습니다. 1차 목표는 \(\mathbf{W}^{(2)}\)와 \(\mathbf{b}^{(2)}\)의 Data Loss \(L_D\)에 대한 편미분을 각각 구하는 것입니다. 이 편미분은 \(\mathbf{W}^{(2)}\)와 \(\mathbf{b}^{(2)}\)의 변화가 Data Loss에 대한 변화의 영향을 의미합니다.


역시 마찬가지로 Chain Rule에 의해 다음 식을 얻을 수 있으며:


Op.[15]  \( \displaystyle{ \frac{\partial L_D}{\partial b_{j}^{(2)}} = \frac{\partial L_D}{\partial q_y} \frac{\partial q_y}{\partial e_k} \frac{\partial e_k}{\partial \sigma_{i}^{(2)}} \frac{\partial \sigma_{i}^{(2)}}{\partial b_{j}^{(2)}} } \)


다음 식이 성립하므로,


Op.[16]  \( \displaystyle{ \frac{\partial \sigma_{i}^{(2)}}{\partial b_{j}^{(2)}} = \frac{\partial}{\partial b_{j}^{(2)}} \begin{bmatrix} \displaystyle{ h_m w_{mj}^{(2)} + b_{i}^{(2)} } \end{bmatrix} = \delta_{ij} } \), \(i=1,...,K\), \(j=1,...,K\)


을 얻을 수 있습니다. 참고로, 위의 식에서 Index \(m\)은 Dummy Variable입니다.


Op.[14]~[16]을 통해 다음 식을 얻을 수 있습니다:


Op.[17]  \( \displaystyle{ \frac{\partial L_D}{\partial b_{j}^{(2)}} = (q_i - \delta_{yi}) \delta_{ij} = q_j - \delta_{ij} } \)


이와 유사하게,


Op.[18]  \( \displaystyle{ \frac{\partial \sigma_{i}^{(2)}}{\partial w_{jl}^{(2)}} = \frac{\partial}{\partial w_{jl}^{(2)}} \begin{bmatrix} h_m w_{mi}^{(2)} + b_{i}^{(2)} \end{bmatrix} = h_m \frac{\partial w_{mi}^{(2)}}{\partial w_{jl}^{(2)}} = h_m \delta_{mj} \delta_{il} = h_j \delta_{il} } \),

\(m=1,...,H\), \(i=1,...,K\), \(j=1,...,H\), \(k=1,...,K\)


를 얻을 수 있으므로,


Op.[19]  \( \displaystyle{ \frac{\partial L_D}{\partial w_{jl}^{(2)}} = \frac{\partial L_D}{\partial q_y} \frac{\partial q_y}{\partial e_k} \frac{\partial e_k}{\partial \sigma_{i}^{(2)}} \frac{\partial \sigma_{i}^{(2)}}{\partial w_{jl}^{(2)}} = h_j (q_l - \delta_{yl}) } \)


와 같이 \(\mathbf{W}^{(2)}\)에 대한 Data Loss \(L_D\)의 편미분 값을 구할 수 있습니다.


지금까지 순수하게 Tensor Notation을 이용하여 수식을 유도하였습니다. 그러나, 뭔가 와닿지 않고 머리만 복잡해지고 있는 것 같습니다. 그래서 1개의 데이터에 대한 예를 들어 행렬 형태로 쉽게 설명하고자 합니다.


Hidden Layer의 Neuron 개수를 4개, 즉 \(D=4\)이고, Class의 개수를 3개, 즉 \(K=3\)으로 가정하겠습니다. 앞서 그림에서 표현했듯 \( \mathbf{\sigma}^{(2)} \)와 \(\mathbf{h}\)는 다음과 같은 관계가 성립됩니다:


Op.[20]  \( \mathbf{\sigma}^{(2)} = \mathbf{W}^{(2)T} \mathbf{h} + \mathbf{b}^{(2)} \)


여기서, 가중치 행렬 \(\mathbf{W}^{(2)}\)는 다음과 같으며:


\( \mathbf{W}^{(2)} = \begin{bmatrix} w_{11} & w_{12} & w_{13} \\ w_{21} & w_{22} & w_{23} \\ w_{31} & w_{32} & w_{33} \\ w_{41} & w_{42} & w_{43} \\ w_{51} & w_{52}  & w_{53} \\ w_{61} & w_{62} & w_{63} \end{bmatrix}  \)


\(\mathbf{\sigma}^{(2)}\), \(\mathbf{h}\), \(\mathbf{b}^{(2)}\)는 각각,


\(\mathbf{\sigma}^{(2)} = \begin{bmatrix} \sigma_{1}^{(2)} \\ \sigma_{2}^{(2)} \\  \sigma_{3}^{(2)} \end{bmatrix}\),


\( \mathbf{h} = \begin{bmatrix} h_1 \\ h_2 \\ h_3 \\ h_4 \\ h_5 \\ h_6 \end{bmatrix} \),


\( \mathbf{b}^{(2)} = \begin{bmatrix} b_{1}^{(2)} \\ b_{2}^{(2)} \\ b_{3}^{(2)} \end{bmatrix} \)


입니다. 위의 식을 이용하여 Op.[20]을 풀어쓰면 다음과 같습니다:


Op.[21]

\( \sigma_{1}^{(2)} = w_{11}^{(1)} h_1 + w_{21}^{(1)} h_2 + w_{31}^{(1)} h_3 + w_{41}^{(1)} h_4 + w_{51}^{(1)} h_5 + w_{61}^{(1)} h_6 + b_{1}^{(2)} \)

\( \sigma_{2}^{(2)} = w_{12}^{(1)} h_1 + w_{22}^{(1)} h_2 + w_{32}^{(1)} h_3 + w_{42}^{(1)} h_4 + w_{52}^{(1)} h_5 + w_{62}^{(1)} h_6 + b_{2}^{(2)} \)

\( \sigma_{3}^{(2)} = w_{13}^{(1)} h_1 + w_{23}^{(1)} h_2 + w_{33}^{(1)} h_3 + w_{43}^{(1)} h_4 + w_{53}^{(1)} h_5 + w_{63}^{(1)} h_6 + b_{3}^{(2)}\)


표현을 간단히 하기위해 \( \displaystyle{ \frac{\partial L_D}{\partial \sigma_{i}^{(2)}} } = \Delta_i \)로 하면,


\( \displaystyle{ \frac{\partial L_D}{\partial w_{11}^{(2)}} = \frac{\partial L_D}{\partial \sigma_{i}^{(2)}} \frac{\partial \sigma_{i}^{(2)}}{\partial w_{11}^{(2)}} } = \frac{\partial L_D}{\partial \sigma_{1}^{(2)}} \frac{\partial \sigma_{1}^{(2)}}{\partial w_{11}^{(2)}} + \frac{\partial L_D}{\partial \sigma_{2}^{(2)}} \frac{\partial \sigma_{2}^{(2)}}{\partial w_{11}^{(2)}} + \frac{\partial L_D}{\partial \sigma_{3}^{(2)}} \frac{\partial \sigma_{3}^{(2)}}{\partial w_{11}^{(2)}} = \Delta_1 h_2 \)


과 같이 표현할 수 있습니다. 이런 식으로,


\( \displaystyle{ \frac{\partial L_D}{\partial w_{21}^{(2)}} = \Delta_1 h_2 } \)

 ·

 ·

 ·

\( \displaystyle{ \frac{\partial L_D}{\partial w_{61}^{(2)}} = \Delta_1 h_6 } \)


이며, 일반적으로는,


Op.[22]  \( \displaystyle{ \frac{\partial L_D}{\partial w_{jl}^{(2)}} } = \Delta_l h_j \)


와 같이 표현할 수 있습니다. 따라서,


Op.[23]

\( \mathbf{dW}^{(2)} = \displaystyle{ \frac{\partial L_D}{\partial \mathbf{W}^{(2)}} } = \displaystyle{\begin{bmatrix} \frac{\partial L_D}{\partial w_{11}^{(2)}} & \frac{\partial L_D}{\partial w_{12}^{(2)}} & \frac{\partial L_D}{\partial w_{13}^{(2)}} \\ \frac{\partial L_D}{\partial w_{21}^{(2)}} & \frac{\partial L_D}{\partial w_{22}^{(2)}} & \frac{\partial L_D}{\partial w_{23}^{(2)}} \\ \vdots \\ \frac{\partial L_D}{\partial w_{61}^{(2)}} & \frac{\partial L_D}{\partial w_{62}^{(2)}} & \frac{\partial L_D}{\partial w_{63}^{(2)}} \end{bmatrix} = \begin{bmatrix} \Delta_1 h_1 & \Delta_2 h_1 & \Delta_3 h_1 \\ \Delta_1 h_2 & \Delta_2 h_2 & \Delta_3 h_2 \\ \vdots \\ \Delta_1 h_6 & \Delta_2 h_6 & \Delta_3 h_6 \end{bmatrix} = \begin{bmatrix} h_1 \\ h_2 \\ \vdots \\ h_6 \end{bmatrix} \begin{bmatrix} \Delta_1 & \Delta_2 & \Delta_3 \end{bmatrix} } \)


으로 표현이 가능합니다. 위의 행렬 중,


\( \displaystyle{ \begin{bmatrix} \Delta_1 & \Delta_2 & \Delta_3 \end{bmatrix} } = \) dscores,


\( \displaystyle{ \begin{bmatrix} h_1 & h_2 & h_3 & h_4 & h_5 & h_6 \end{bmatrix} } = \) hidden.layer


라고 지칭하겠습니다. 따라서, Op.[22]는,


hidden.layer\(^{T}\) dscores


의 행렬 곱으로 표현이 가능합니다.


한편, \( \displaystyle{ \frac{d L_D}{d \mathbf{b}^{(2)}} } \) 역시 다음과 같이 표현됩니다:


\( \displaystyle{ \frac{d L_D}{d b_{1}^{(2)}} = \frac{\partial L_D}{\partial \sigma_{1}^{(2)}} \frac{\partial \sigma_{1}^{(2)}}{\partial b_{1}^{(2)}} + \frac{\partial L_D}{\partial \sigma_{2}^{(2)}} \frac{\partial \sigma_{2}^{(2)}}{\partial b_{1}^{(2)}} + \frac{\partial L_D}{\partial \sigma_{3}^{(2)}} \frac{\partial \sigma_{3}^{(2)}}{\partial b_{1}^{(2)}} = \Delta_1 } \)

\( \displaystyle{ \frac{d L_D}{d b_{2}^{(2)}} = \frac{\partial L_D}{\partial \sigma_{1}^{(2)}} \frac{\partial \sigma_{1}^{(2)}}{\partial b_{2}^{(2)}} + \frac{\partial L_D}{\partial \sigma_{2}^{(2)}} \frac{\partial \sigma_{2}^{(2)}}{\partial b_{2}^{(2)}} + \frac{\partial L_D}{\partial \sigma_{3}^{(2)}} \frac{\partial \sigma_{3}^{(2)}}{\partial b_{2}^{(2)}} = \Delta_2 } \)

\( \displaystyle{ \frac{d L_D}{d b_{3}^{(2)}} = \frac{\partial L_D}{\partial \sigma_{1}^{(2)}} \frac{\partial \sigma_{1}^{(2)}}{\partial b_{3}^{(2)}} + \frac{\partial L_D}{\partial \sigma_{2}^{(2)}} \frac{\partial \sigma_{2}^{(2)}}{\partial b_{3}^{(2)}} + \frac{\partial L_D}{\partial \sigma_{3}^{(2)}} \frac{\partial \sigma_{3}^{(2)}}{\partial b_{3}^{(2)}} = \Delta_3 } \)


이 역시 간단하게 표현하면,


\( \displaystyle{ \frac{\partial L_D}{\partial b_{j}^{(2)}} = \Delta_j } \)


입니다.


이제 좀 더 일반적인 식으로 이해하기 위해 2개의 데이터가 입력으로 공급되는 경우, 즉, \(N = 2\)인 경우를 가정해 보겠습니다.


우선, Notation을 다음과 같이 정의되며,


Op.[24]  \( \sigma_{i,j}^{(2)} \), for \(i\)-th Input and \(j\)-th Class, \(i=1,2\) & \(j=1,2,3\)


Weights \(\mathbf{W}^{(2)}\)와 Biases \(\mathbf{b}^{(2)}\)는 두 데이터에 대하여 동일합니다.


1번째 입력 데이터에 대하여:


Op.[25]

\( \sigma_{1,1}^{(2)} = w_{11}^{(2)} h_{1,1} + w_{21}^{(2)} h_{1,2} + w_{31}^{(2)} h_{1,3} + \cdots + w_{61}^{(2)} h_{1,6} + b_{1}^{(2)} \)

\( \sigma_{1,2}^{(2)} = w_{12}^{(2)} h_{1,1} + w_{22}^{(2)} h_{1,2} + w_{32}^{(2)} h_{1,3} + \cdots + w_{62}^{(2)} h_{1,6} + b_{2}^{(2)} \)

\( \sigma_{1,3}^{(2)} = w_{13}^{(2)} h_{1,1} + w_{23}^{(2)} h_{1,2} + w_{33}^{(2)} h_{1,3} + \cdots + w_{63}^{(2)} h_{1,6} + b_{3}^{(2)} \)


2번째 입력 데이터에 대하여:


Op.[26]

\( \sigma_{2,1}^{(2)} = w_{11}^{(2)} h_{2,1} + w_{21}^{(2)} h_{2,2} + w_{31}^{(2)} h_{2,3} + \cdots + w_{61}^{(2)} h_{2,6} + b_{1}^{(2)} \)

\( \sigma_{2,2}^{(2)} = w_{12}^{(2)} h_{2,1} + w_{22}^{(2)} h_{2,2} + w_{32}^{(2)} h_{2,3} + \cdots + w_{62}^{(2)} h_{2,6} + b_{2}^{(2)} \)

\( \sigma_{2,3}^{(2)} = w_{13}^{(2)} h_{2,1} + w_{23}^{(2)} h_{2,2} + w_{33}^{(2)} h_{2,3} + \cdots + w_{63}^{(2)} h_{2,6} + b_{3}^{(2)} \)


또는 간단한 행렬식으로,


\( \mathbf{\sigma}^{(2)} = \mathbf{h}^{T} \mathbf{W}^{(2)} + \mathbf{b}^{(2)} \)


과 같이 표현할 수 있습니다. 여기서,


\( \mathbf{h} = \displaystyle{ \begin{bmatrix} h_{1,1} & h_{2,1} \\ h_{1,2} & h_{2,2} \\ h_{1,3} & h_{2,3} \\ h_{1,4} & h_{2,4} \\ h_{1,5} & h_{2,5} \\ h_{1,6} & h_{2,6} \end{bmatrix} } \)


\( \mathbf{W}^{(2)} = \displaystyle{ \begin{bmatrix} w_{11}^{(2)} & w_{12}^{(2)} && w_{13}^{(2)} \\ w_{21}^{(2)} & w_{22}^{(2)} && w_{23}^{(2)} \\ w_{31}^{(2)} & w_{32}^{(2)} && w_{33}^{(2)} \\ w_{41}^{(2)} & w_{42}^{(2)} && w_{43}^{(2)} \\ w_{51}^{(2)} & w_{52}^{(2)} && w_{53}^{(2)} \\ w_{61}^{(2)} & w_{62}^{(2)} && w_{63}^{(2)} \end{bmatrix} } \)


\( \mathbf{\sigma}^{(2)} = \displaystyle{ \begin{bmatrix} \sigma_{11}^{(2)} & \sigma_{12}^{(2)} & \sigma_{13}^{(2)} \\ \sigma_{21}^{(2)} & \sigma_{22}^{(2)} & \sigma_{23}^{(2)} \end{bmatrix} } \)


입니다.


위의 식을 이용하여 2개의 입력 데이터가 있는 경우, Op.[23]은 다음과 같이 표현됩니다:


dW\(^{(2)}\) = hidden.layer \(^{T}\) \(\cdot\) dscores


여기서,


hidden.layer \( \displaystyle{ \begin{bmatrix} h_{11} & h_{12} & h_{13} & h_{14} & h_{15} & h_{16} \\ h_{21} & h_{22} & h_{23} & h_{24} & h_{25} & h_{26} \end{bmatrix} } \)


dscores\( \displaystyle{ \begin{bmatrix} \Delta_{1,1} & \Delta_{1,2} & \Delta_{1,3} \\ \Delta_{2,1} & \Delta_{2,2} & \Delta_{2,3} \end{bmatrix} } \)


입니다.


현편, Op.[25], [26]으로부터,


\( \displaystyle{ \frac{d L_D}{d b_{1}^{(2)}} = \frac{\partial L_D}{\partial \sigma_{1,1}^{(2)}} \frac{\partial \sigma_{1,1}^{(2)}}{\partial b_{1}^{(2)}} + \frac{\partial L_D}{\partial \sigma_{1,2}^{(2)}} \frac{\partial \sigma_{1,2}^{(2)}}{\partial b_{1}^{(2)}} + \frac{\partial L_D}{\partial \sigma_{1,3}^{(2)}} \frac{\partial \sigma_{1,3}^{(2)}}{\partial b_{1}^{(2)}} + \frac{\partial L_D}{\partial \sigma_{2,1}^{(2)}} \frac{\partial \sigma_{2,1}^{(2)}}{\partial b_{1}^{(2)}} + \frac{\partial L_D}{\partial \sigma_{2,2}^{(2)}} \frac{\partial \sigma_{2,2}^{(2)}}{\partial b_{1}^{(2)}} + \frac{\partial L_D}{\partial \sigma_{2,3}^{(2)}} \frac{\partial \sigma_{21,3}^{(2)}}{\partial b_{1}^{(2)}} } \)

        \( \displaystyle{ = \frac{\partial L_D}{\partial \sigma_{1,1}^{(2)}} (1) + \frac{\partial L_D}{\partial \sigma_{1,2}^{(2)}} (0) + \frac{\partial L_D}{\partial \sigma_{1,3}^{(2)}} (0) + \frac{\partial L_D}{\partial \sigma_{2,1}^{(2)}} (1) + \frac{\partial L_D}{\partial \sigma_{2,2}^{(2)}} (0) + \frac{\partial L_D}{\partial \sigma_{2,3}^{(2)}} (0) } \)

        \( \displaystyle{ = \frac{\partial L_D}{\partial \sigma_{1,1}^{(2)}} + \frac{\partial L_D}{\partial \sigma_{2,1}^{(2)}} } \)

          \( = \Delta_{1,1} + \Delta_{2,1} \)


를 구할 수 있으며, 동일한 방식으로,


\( \displaystyle{ \frac{d L_D}{d b_{2}^{(2)}} = \Delta_{1,2} + \Delta_{2,2}} \)



\( \displaystyle{ \frac{d L_D}{d b_{3}^{(2)}} = \Delta_{1,3} + \Delta_{2,3}} \)


를 얻을 수 있습니다. 즉, Data Loss \(L_D\)의 \(\mathbf{b}^{(2)}\)에 대한 미분은 행렬 dscores의 열(Column)을 각각 더한 것임을 알 수 있으며,


Op.[27]  \( \displaystyle{ \frac{d L_D}{d b_{i}^{(2)}} } = \sum_{j=1}^{N}{\Delta_{j,i}} \)


과 같이 일반적인 식으로 표현할 수 있습니다. 물론 \(N\)은 입력 데이터의 개수를 의미합니다.


이제 일반적인 식을 얻기 위해 입력 데이터의 개수가 \(N\)개로 확장하면,


Op.[28]

dW\(^{(2)}\) = hidden.layer\(^{T}\) \(\cdot\) dscores

db\(^{(2)}\) = ColumnSums(dscores)


where

hidden.layer = \(h_{i,k}\), \(i=1,...,N\), \(k=1,...,H\)

dscores = \(\Delta_{k,j}\), \(k=1,...,N\), \(j=1,...,K\)

\( \Delta_{k,j} = \displaystyle{ \frac{d L_D}{d \sigma_{k,j}^{(2)}} } \)


로 정리할 수 있습니다.


다음으로 구할 식은 Hidden Layer \(\mathbf{h}\)에 대한 Data Loss \(L_D\)의 미분식, 즉, 이며, 이 식 역시 Chain Rule에 의해,


\( \displaystyle{ \frac{\partial L_D}{\partial q_y} \frac{\partial q_y}{\partial e_k} \frac{\partial e_k}{\partial \sigma_{i}^{(2)}} \frac{\partial \sigma_{i}^{(2)}}{\partial h_j} } \)


인데,


\( \displaystyle{ \frac{\partial L_D}{\partial q_y} \frac{\partial q_y}{\partial e_k} \frac{\partial e_k}{\partial \sigma_{i}^{(2)}} } \) = dscores


이며, Op.[21]의 식을 참고하여 유도해 보면,


\( \displaystyle{ \frac{\partial \sigma_{i}^{(2)}}{\partial h_j} = w_{ji}^{(2)} = \mathbf{W}^{(2)T} } \)


임을 알 수 있습니다. 즉, \(N\)개의 데이터에 대한 일반적인 식으로 표현하면,


Op.[29]

dhidden = dscores \(\cdot\) \(\mathbf{W}^{(2)T}\) = \( \displaystyle{ \begin{bmatrix} d_{1,1} & d_{1,2} & \cdots & d_{1,K} \\ d_{2,1} & d_{2,2} & \cdots & d_{2,K} \\ \vdots & \vdots & \vdots & \vdots \\ d_{N,1} & d_{N,2} & \cdots & d_{N,K} \end{bmatrix} \begin{bmatrix} w_{1,1}^{(2)} & w_{2,1}^{(2)} & \cdots & w_{K,1}^{(2)} \\ w_{1,2}^{(2)} & w_{2,2}^{(2)} & \cdots & w_{K,2}^{(2)} \\ \vdots & \vdots & \vdots & \vdots \\ w_{1,H}^{(2)} & w_{2,H}^{(2)} & \cdots & w_{K,H}^{(2)} \end{bmatrix} } \)


과 같습니다.


이제 Input에서 Hidden Layer까지 이르는 전반부를 살펴보겠습니다.





아시는 바와 같이, \(\mathrm{ReLU}(x) = \mathrm{max}(0,x)\)는 \(x \le 0\)에 대해서는 0을, \(x > 0\)에 대해서는 입력값을 그대로 출력합니다. 따라서, ReLU의 미분값은,


\( \displaystyle{ \frac{d ReLU(x)}{dx} = \begin{cases} 1, & x > 0 \\ 0, & x \le 0  \end{cases} } \)


과 같습니다.


\( h_i = \mathrm{ReLU}(\sigma_{i}^{(1)}) \)


이므로, 이를 \(\sigma_{i}^{(1)}\)에 대하여 편미분하면,


Op.[30]  \( \displaystyle{\frac{\partial h_i}{\partial \sigma_{l}^{(1)}}} = \begin{cases} 1, & h_i > 0 \\ 0, & h_i \le 0 \end{cases} \)


과 같습니다. Op.[29]과 [30]으로부터 Chain Rule을 적용하여,


Op.[31]  \( \displaystyle{ \frac{dL_D}{d \sigma_{l}^{(1)}} = \underbrace{ \frac{\partial L_D}{\partial q_y} \frac{\partial q_y}{\partial e_k} \frac{\partial e_k}{\partial \sigma_{i}^{(2)}} \frac{\partial \sigma_{i}^{(2)}}{\partial h_j}  }_{\mathrm{= dhidden}} \underbrace{ \frac{\partial h_j}{\partial \sigma_{l}^{(1)}} }_{ = \begin{cases} 1 & \mathrm{if} \; h_j > 0 \\ 0 & \mathrm{otherwise} \end{cases} } } = \begin{cases} \mathrm{dhidden} & \mathrm{if} \; h_j > 0 \\ 0 & \mathrm{if} \; h_j \le 0 \end{cases} \)



를 얻을 수 있는데, 5. Implementation에서 다시 설명드리겠지만 Op.[29]~[31]은 다음과 같이 간략하게 코드로 표현이 가능합니다:


1
2
dhidden <- dscores %*% base::t(W2)
dhidden[hidden.layer <= 0] <- 0
cs



위의 코드의 Line 2는 hidden.layer의 Element가 0과 같거나 0 보다 작을 때 동일한 행과 열에 해당하는 dhidden의 Element를 0으로 변경한다는 의미입니다.


Op.[28]을 얻는 방식과 동일한 방식으로,


Op.[32]

dW\(^{(1)}\) = X\(^{T}\) \(\cdot\) dhidden

db\(^{(1)}\) = ColumnSums(dhidden)


을 얻을 수 있습니다.


이제 \(\mathbf{W}^{(1)}\), \(\mathbf{b}^{(1)}\), \(\mathbf{W}^{(2)}\), \(\mathbf{b}^{(2)}\)에 대한 \(L_D\)의 미분식을 모두 얻었으니, 이들에 대한 업데이트 식 \( \theta \leftarrow  \theta - \alpha \nabla_{\theta}{L} \) (여기서, \(\theta\)는 모델 파라미터이며 \(\alpha\)는 학습률(Learning Rate)를 의미함)을 이용하여 얻을 수 있습니다.


그런데, 한 가지를 빠뜨렸습니다. 그것은 Op.[9]에서 언급하였던 Regularization Loss(\(L_R\))에 대한 각 모델 파라미터의 미분값들입니다. \(L_D\)은 Weights 값들의 제곱의 합으로 구성되어 있으므로 두말할 것도 없이 Bias에 대한 미분값은 모두 0일 것입니다.


가령, \( \displaystyle{ \frac{\partial L_R}{\partial w_{i,j}} } \)을 구한다고 하면, \(L_R\)은 각 Weight의 제곱을 합한 것이므로(물론 \(\frac{1}{2}\lambda\)가 곱해졌습니다) 미분하고자 하는 대상 외에는 모두 0이 될 것입니다. 즉, \( \displaystyle{ \frac{\partial L_R}{\partial w_{i,j}} } = w_{i,j} \)입니다.


따라서, 앞서 구했던 \(\mathbf{dW}^{(1)}\)과 \(\mathbf{dW}^{(2)}\)에 Regularization Loss의 미분값을 반영하면,


Op.[33]

dW\(^{(1)}\) \(\leftarrow\) dW\(^{(1)}\) + \(\lambda\) * W\(^{(1)}\)

dW\(^{(2)}\) \(\leftarrow\) dW\(^{(2)}\) + \(\lambda\) * W\(^{(2)}\)


와 같이 얻을 수 있으며, 최종적으로 모델 파라미터들의 업데이트 식은,


Op.[34]

W\(^{(1)}\) \(\leftarrow\) W\(^{(1)}\) - \(\alpha\) * dW\(^{(1)}\)
b\(^{(1)}\) \(\leftarrow\) b\(^{(1)}\) - \(\alpha\) * db\(^{(1)}\)

W\(^{(2)}\) \(\leftarrow\) W\(^{(2)}\) - \(\alpha\) * dW\(^{(2)}\)
b\(^{(2)}\) \(\leftarrow\) b\(^{(2)}\) - \(\alpha\) * db\(^{(2)}\)


과 같습니다.


이로써 Backprogation에 대한 식을 모두 구하였고, 이들을 이용하여 일정 조건이 만족될 때까지 반복하여 모델 파라미터들을 업데이트 합니다.


지금까지의 과정을 간단하게 수식으로 정리하면 다음과 같습니다(첨자 표현은 앞의 과정과 일치하지 않을 수 있습니다):


Op.[BackwardPass]


\(N\)개의 Dataset, \(D\)개의 Input Dimensionality, \(H\)개의 뉴런을 갖는 1개의 Hidden Layer, \(K\)개의 Output Class를 갖는 Multi-layer Perceptron에 대하여,


\( \displaystyle{ \frac{\partial L_D}{\partial w_{kj}^{(i)}} = w_{kj}^{(i)} } \), \( i \in \begin{Bmatrix} 1,2 \end{Bmatrix} \), \( \begin{cases} \mathrm{for} \; i=1, \; k=1,...,D, \; j=1,...,H \\ \mathrm{for} \; i=2, \; k=1,...,H, \; j=1,...,K \end{cases} \)


\( \displaystyle{ \frac{\partial L_R}{\partial w_{kj}^{(i)}} = w_{kj}^{(i)} } \), \( \begin{cases} \mathrm{for} \; i=1, \; k=1,...,D, \; j=1,...,H \\ \mathrm{for} \; i=2, \; k=1,...,H, \; j=1,...,K \end{cases} \)


dscores[N×K] = \( q_{i,j} - \delta_{jy_{i}} \), \( i=1,...,N, \; j=1,...,K \)

 

hidden.layer[N×H] = \(h_{i,j}\), \(i=1,...,N, \; j=1,...,H\)


dW\(^{(2)}\) = \( \displaystyle{ h_{j,i} \frac{\partial L_D}{\partial \sigma_{i,k}^{(2)}} } \) + \(\lambda\) \(w_{jk}^{(2)}\)

      = hidden.layer\(^{T}\) \(\cdot\) dscores + \(\lambda\) \(\cdot\) W\(^{(2)}\), (\( i=1,...,N, \; j=1,...,H, \; k=1,...,K \))


db\(^{(2)}\) = \( \displaystyle{ \sum_{i=1}^{N}{(q_{i,j} - \delta_{jy_{i}} )} } \) = ColumnSums(dscores)


dhidden[N×H] = \( \displaystyle{ \frac{\partial L_D}{\partial \sigma_{i,j}^{(2)} } \frac{\partial \sigma_{i,j}^{(2)} }{\partial h_{i,k} } \frac{\partial h_{i,k} }{\partial \sigma_{i,l}^{(1)} } } \)

   = \( \displaystyle{ \begin{cases} \frac{\partial L_D}{\partial \sigma_{i,j}^{(2)} } \frac{\partial \sigma_{i,j}^{(2)} }{\partial h_{i,k} } & \mathrm{if} \; h_{i,k} > 0 \\ 0 & \mathrm{if} \; h_{i,k} \le 0 \end{cases} } \)


dW\(^{(1)}\) = X\(^{T}\) \(\cdot\) dhidden + \(\lambda\) \(\cdot\) W\(^{(1)}\)


db\(^{(1)}\) = ColumnSums(dhidden)


[UPDATE of MODEL PARAMETERS]

W\(^{(2)}\) \(\leftarrow\) W\(^{(2)}\) - \(\alpha\) \(\cdot\) dW\(^{(2)}\)

b\(^{(2)}\) \(\leftarrow\) b\(^{(2)}\) - \(\alpha\) \(\cdot\) db\(^{(2)}\)


W\(^{(1)}\) \(\leftarrow\) W\(^{(1)}\) - \(\alpha\) \(\cdot\) dW\(^{(1)}\)\(\)

b\(^{(1)}\) \(\leftarrow\) b\(^{(1)}\) - \(\alpha\) \(\cdot\) db\(^{(1)}\)\(\)

5. Implementation

이제 구현된 코드를 분석하면셔 앞서 유도한 식이 코드 상에서 어떻게 구현되어 있는지 살펴보도록 하겠습니다.


우선, 전체 코드는 다음과 같이 3개의 소스파일로 구성되어 있습니다 (소스파일의 Author는 Peng Zhao(patric.zhao@gmail.com)이며, 원래 1개의 소스였으나, 기능을 분리하여 제가 3개로 나눈 것입니다):


main.R

train.dnn.R

predict.dnn.R

5.1. train.dnn() 프로토타입

소스 파일들 중 가장 핵심이 되는 train.dnn.R을 살펴보도록 하겠습니다. train.dnn.R은 단 하나의 함수 train.dnn()을 갖는 소스파일입니다. 파일의 프로토타입을 보면,


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
train.dnn <- function(x,
                      y,
                      traindata = data,
                      testdata = NULL,
                      model = NULL,
                      # set hidden layers and neurons
                      # currently, only support 1 hidden layer
                      hidden = base::c(6), 
                      # max iteration steps
                      maxit = 2000,
                      # delta loss 
                      abstol = 1e-2,
                      # learning rate
                      lr = 1e-2,
                      # regularization rate
                      reg = 1e-3,
                      # show results every 'display' step
                      display = 100,
                      random.seed = 1)
cs


인데, 각각의 파라미터에 대한 설명은 다음과 같습니다:


  • x : 입력 데이터(Input Features)의 Column Index입니다. main.R을 보면 x = 1:4를 파라미터로 전달하는데, 이는 첫번째부터 네번째까지의 Column(Sepal.Length ~ Petal.Width)을 Input Features로 정한다는 뜻입니다.

  • y : 출력 데이터(Output Classes)의 Column Index입니다. main.R을 보면 y = 5를 파라미터로 전달하는데, 이는 다섯번째 Column(Species)을 Output Class로 정한다는 뜻입니다.

  • traindata : 학습을 시킬 데이터셋입니다.

  • testdata : 학습을 통해 생성한 예측 모델을 테스트하기 위한 데이터셋입니다.

  • model : 학습을 통해 생성한 예측 모델이며, 학습 시에는 사용되지 않으며, 예측 시에 사용됩니다. 즉, predict.dnn() 함수에서 사용하는 파라미터입니다.

  • hidden : Hidden Layer 내 노드의 개수이며, 6개로 설정하였습니다.

  • maxit : 학습을 시키리 최대 반복 계산 횟수이며, 기본값은 2000으로, main.R에서 전달하는 값은 3000입니다.

  • abstolmaxit 외에 학습을 종료시키는 조건으로, 계산된 Loss가 이 값보다 같거나 작으면 학습을 종료시킵니다. 기본값은 \(10^{-2}\)입니다.

  • lr : 학습률(Learning Rate)입니다. 기본값은 \(10^{-2}\)입니다.

  • reg : Regularization의 Hyperparameter(\(\lambda\))이며, 기본값은 \(10^{-3}\)입니다.

  • display : 학습하는 과정 중 중간 결과를 출력하는 Step입니다.

  • random.seed : Weights와 Biases 초기화 시 난수를 사용하기 위한 Random Seed입니다.

5.2. 부가 변수 설정

난수를 이용한 Weights 및 Biases 초기화를 위해 파라미터로 전달받은 random.seed로 Random Seed를 설정합니다.


1
2
# to make the case reproducible.
base::set.seed(random.seed)
cs


학습시킬 데이터(traindata)의 전체 개수를 변수 N에 저장합니다(데이터 개수는 75개입니다).


1
2
# total number of training set
<- base::nrow(traindata)
cs


Data Frame 형식으로 입력받은 파라미터 traindata에서 Input Features에 해당하는 Columns(1~4)를 취하되 Features의 이름은 필요없으므로 이를 제거합니다:


1
2
3
# extract the data and label
# don't need atribute 
<- base::unname(base::data.matrix(traindata[,x]))
cs


Output Class에 해당하는 Column(5)을 취하여 변수 Y에 저장하는데,


1
2
# correct categories represented by integer
<- traindata[,y]
cs


Y에 저장된 형태는 nominal value인 경우, 이를 정수 형태로 변환한다. 즉, Y에는 다음과 같이 세 개의 클래스 이름이 저장되어 있습니다.


> Y
 [1] setosa     setosa     setosa     setosa     setosa     setosa     setosa     setosa     setosa     setosa     setosa     setosa     setosa    
[14] setosa     setosa     setosa     setosa     setosa     setosa     setosa     setosa     setosa     setosa     setosa     setosa     versicolor
[27] versicolor versicolor versicolor versicolor versicolor versicolor versicolor versicolor versicolor versicolor versicolor versicolor versicolor
[40] versicolor versicolor versicolor versicolor versicolor versicolor versicolor versicolor versicolor versicolor versicolor virginica  virginica 
[53] virginica  virginica  virginica  virginica  virginica  virginica  virginica  virginica  virginica  virginica  virginica  virginica  virginica 
[66] virginica  virginica  virginica  virginica  virginica  virginica  virginica  virginica  virginica  virginica 
Levels: setosa versicolor virginica


1
if(base::is.factor(Y)) { Y <- base::as.integer(Y) }
cs


다음 코드를 살펴보시면,


1
2
3
4
# create index for both row and col
Y.len   <- base::length(base::unique(Y))
Y.set   <- base::sort(base::unique(Y))
Y.index <- base::cbind(1:N, base::match(Y, Y.set))
cs


Line 2에서 base::unique() 함수를 이용하여 중복되지 않은 엘리먼트들(Elements)을 뽑아내고 이들의 개수를 구하여 Y.len에 저장합니다.


Line 3에서는 이 엘리먼트들을 오름차순(Ascending Order)으로 정렬한 결과를 Y.set에 저장합니다.


Line 4에서 base::match() 함수를 이용하여 Y의 각 엘리먼트가 Y.set의 엘리먼트와 일치하는 위치를 찾습니다. 다음 예를 통해 base::match() 함수를 설명하도록 하겠습니다.


> base::match(base::c(4,3,4,3,2,3,2,3,1,1,3,4,4), base::c(1,3,2))
 [1] NA  2 NA  2  3  2  3  2  1  1  2 NA NA


위으 코드에서 base::match() 함수의 첫번째 파라미터의 첫번째 엘리먼트 4는 두번째 파라미터의 일치하는 엘리먼트가 없으므로 결과는 NA입니다. 첫번째 파라미터의 두번째 엘리먼트 3은 두번째 파라미터에 일치하는 엘리먼트가 존재하며 인덱스는 2입니다. 첫번째 파라미터의 아홉번째 엘리먼트 1은 두번째 파라미터에 일치하는 엘리먼트가 존재하면 인덱스는 1입니다. 이와 같이 첫번째 파라미터의 각 엘리먼트가 두번째 파라미터의 일치하는 엘리먼트의 인덱스를 넘겨주는 함수가 base::match()입니다.


1:N 벡터와 base::match(Y, Y.set) 벡터를 base::cbind() 함수를 이용하여 Column Binding한 결과를 Y.index에 저장합니다.

5.3. Training

이제 다음 코드를 살펴보도록 하겠습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
# number of input features
<- base::ncol(X)
 
# number of categories for classification
<- base::length(base::unique(Y))
<- hidden
 
# create and initialize weights and bias 
W1 <- 0.01 * base::matrix(stats::rnorm(D*H), nrow = D, ncol = H)
b1 <- base::matrix(0, nrow=1, ncol=H)
 
W2 <- 0.01 * base::matrix(stats::rnorm(H*K), nrow = H, ncol = K)
b2 <- base::matrix(0, nrow = 1, ncol = K)
cs



Line 1~6은 Input Dimensionality(D), Hidden Layer 내 Neuron개수(H), Class 개수(H)를 저장합니다.


Line 8~13은 Model Parameters를 주어진 Dimension 크기의 행렬로 초기화합니다.


1
2
3
4
# use all train data to update weights since it's a small dataset
batchsize <- N
# initialize loss to a very big value
loss <- 100000
cs


위의 코드에서는 Batch Size와 Loss를 초기화합니다. Batch Size를 전체 데이터의 개수 N으로 설정한 것은 Full Batch를 의미하며 N이 매우 클 경우 Full Batch가 비효율적일 수 있으므로 Mini Batch(\(\mathrm{batchsize} < N\))로 수행하며, Loss는 최소화의 대상이므로 충분히 큰 값으로 초기화합니다.


자, 이제 본격적으로 학습을 진행합니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# Training the network
<- 0
while(i < maxit && loss > abstol) {
    # iteration index
    i <- i +1
    
    # forward ....
    # 1 indicate row, 2 indicate col
    hidden.layer <- base::sweep(X %*% W1, 2, b1, '+')
    
    # neurons : ReLU
    hidden.layer <- base::pmax(hidden.layer, 0)
    score <- base::sweep(hidden.layer %*% W2, 2, b2, '+')
    
    # softmax
    score.exp <- base::exp(score)
    
    # debug
    probs <- score.exp / base::rowSums(score.exp)
    
    # compute the loss
    correct.logprobs <- -log(probs[Y.index])
    data.loss  <- base::sum(correct.logprobs) / batchsize
    reg.loss   <- 0.5 * reg * (base::sum(W1 * W1) + base::sum(W2 * W2))
    loss <- data.loss + reg.loss
    
    # backward ....
    dscores <- probs
    dscores[Y.index] <- dscores[Y.index] - 1
    dscores <- dscores / batchsize
    
    dW2 <- base::t(hidden.layer) %*% dscores
    db2 <- base::colSums(dscores)
    
    dhidden <- dscores %*% base::t(W2)
    dhidden[hidden.layer <= 0<- 0
    
    dW1 <- base::t(X) %*% dhidden
    db1 <- base::colSums(dhidden)
    
    # update ....
    dW2 <- dW2 + reg * W2
    dW1 <- dW1 + reg * W1
    
    W1 <- W1 - lr * dW1
    b1 <- b1 - lr * db1
    
    W2 <- W2 - lr * dW2
    b2 <- b2 - lr * db2
  }
cs


앞서 이론 부분에서 충분히 설명드렸다고 생각되는데, Op.[ForwardPass]Op.[BackwardPass]를 대응시켜 보시면 코드가 완벽하게 이해되리라 믿습니다.


학습이 완료되면 아래의 코드와 같이 model이라는 list 변수에 모델 파라미터들을 저장하고 결과로 리턴해 줍니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# final results
# creat list to store learned parameters
# you can add more parameters for debug and visualization
# such as residuals, fitted.values ...
model <- list( D = D,
                 H = H,
                 K = K,
                 # weights and bias
                 W1 = W1, 
                 b1 = b1, 
                 W2 = W2, 
                 b2 = b2)
  
# plotting with Plotly
learning_val <- learning_val[-1,]
<- plotly::plot_ly(data = learning_val,
                    x = ~Epoch,
                    y = ~Loss,
                    name = "Loss",
                    type = 'scatter',
                    mode = 'lines+markers',
                    line = list(color = 'rgb(205, 12, 24)', width = 3)) %>%
    add_trace(y = ~Accuracy,
            name = "Accuracy",
            line = list(color = 'rgb(22, 96, 167)',
                        width = 4)) %>%
    layout(title = "Loss & Accuracy as Steps Proceed",
        xaxis = list(title = "steps"),
        yaxis = list (title = "Loss & Acc."))
  
print(p)
  
return(model)
cs

5.4. predict.dnn( ) 프로토타입

predict.dnn() 함수는 predict.dnn.R 소스 파일에 있습니다. 함수 선언은 다음과 같은데,


1
predict.dnn <- function(model, data = X.test)
cs


입력 변수에 대한 설명은 다음과 같습니다:

  • model : train.dnn()에서 학습시킨 모델 파라미터들.

  • data : 테스트 데이터.


Predict 과정은 단순히 테스트 입력 데이터를 앞서 얻은 model을 이용하여 Forward Pass를 거쳐 Score가 가장 높은 Class를 얻는 과정이라고 보시면 되겠습니다.


다음 코드를 참고하시기 바랍니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Prediction
predict.dnn <- function(model,
                        data = X.test)
{
  # new data, transfer to matrix
  new.data <- base::data.matrix(data)
  
  # Feed Forwad
  hidden.layer <- base::sweep(new.data %*% model$W1, 2, model$b1, '+')
  # neurons : Rectified Linear
  hidden.layer <- base::pmax(hidden.layer, 0)
  score <- base::sweep(hidden.layer %*% model$W2, 2, model$b2, '+')
  
  # Loss Function: softmax
  score.exp <- base::exp(score)
  probs <- base::sweep(score.exp, 1, base::rowSums(score.exp), '/'
  
  # select max possiblity
  labels.predicted <- base::max.col(probs)
  return(labels.predicted)
}
cs

5.5. main.R

main.R에서 하는 작업은 다음과 같습니다:


  1. 주어진 데이터(iris)를 train datatest data로 분류한다.

  2. train datatrain.dnn() 함수에 공급하여 학습 모델을 얻는다.

  3. test datapredict.dnn() 함수에 공급하여 예측 결과를 얻는다.

  4. 얻어진 예측 결과와 실제 데이터(Ground Truth Labels)와 일치도를 평가하여 Accuracy를 얻는다.

다음은 main.R 코드입니다:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# Copyright 2016: www.ParallelR.com
# Parallel Blog : R For Deep Learning (I): Build Fully Connected Neural Network From Scratch
# Classification by 2-layers DNN and tested by iris dataset
# Author: Peng Zhao, patric.zhao@gmail.com
 
base::rm(list = ls())
base::gc()
 
base::source('./train.dnn.R'echo=FALSE)
base::source('./predict.dnn.R'echo=FALSE)
 
########################################################################
# testing
#######################################################################
set.seed(1)
 
# 0. EDA
base::summary(iris)
base::names(iris)
graphics::plot(iris)
 
# 1. split data into test/train
samp <- base::c(base::sample(1:50,25),
                base::sample(51:100,25),
                base::sample(101:150,25))
 
# 2. train model
ir.model <- train.dnn(x = 1:4,
                      y = 5,
                      traindata = iris[samp,],
                      testdata = iris[-samp,],
                      hidden = 6,
                      maxit = 3000,
                      display = 50)
# ir.model <- train.dnn(x=1:4, y=5, traindata=iris[samp,], hidden=6, maxit=2000, display=50)
 
# 3. prediction
# NOTE: if the predict is factor, we need to transfer the number into class manually.
#       To make the code clear, I don't write this change into predict.dnn function.
labels.dnn <- predict.dnn(ir.model, iris[-samp, -5])
 
# 4. verify the results
base::table(model = iris[-samp,5], data = labels.dnn)
#          labels.dnn
#            1  2  3
#setosa     25  0  0
#versicolor  0 24  1
#virginica   0  0 25
 
#accuracy
print( sprintf("Accuracy: %f", base::mean(as.integer(iris[-samp, 5]) == labels.dnn)) )
# 0.98
cs

5.6. 실행결과

main.R 코드를 실행한 결과는 다음과 같습니다.


50 1.09785 0.3333333 
100 1.096281 0.3333333 
150 1.091853 0.3333333 
200 1.080326 0.3333333 
250 1.056044 0.3333333 
300 1.015552 0.3466667 
350 0.952453 0.6666667 
400 0.8789816 0.6666667 
450 0.8070193 0.6666667 
500 0.7290098 0.6666667 
550 0.649967 0.6666667 
600 0.5840677 0.68 
650 0.5341543 0.7066667 
700 0.4962127 0.8 
750 0.4660278 0.88 
800 0.4406975 0.92 
850 0.4184247 0.9333333 
900 0.3981292 0.96 
950 0.379204 0.96 
1000 0.361337 0.96 
1050 0.344356 0.96 
1100 0.3281951 0.9866667 
1150 0.3128612 0.9866667 
1200 0.2983606 0.9866667 
1250 0.284711 0.9866667 
1300 0.2719326 0.9866667 
1350 0.2600172 0.9866667 
1400 0.2489488 0.9866667 
1450 0.2387016 0.9866667 
1500 0.2292342 0.9866667 
1550 0.2204989 0.9866667 
1600 0.2124452 0.9866667 
1650 0.205022 0.9866667 
1700 0.1981784 0.9866667 
1750 0.1918652 0.9866667 
1800 0.1860356 0.9866667 
1850 0.180646 0.9866667 
1900 0.1756561 0.9866667 
1950 0.171029 0.9866667 
2000 0.1667312 0.9866667 
2050 0.1627323 0.9866667 
2100 0.1590047 0.9866667 
2150 0.1555238 0.9866667 
2200 0.1522674 0.9866667 
2250 0.1492155 0.9866667 
2300 0.1463502 0.9866667 
2350 0.1436553 0.9866667 
2400 0.1411165 0.9866667 
2450 0.1387206 0.9866667 
2500 0.136456 0.9866667 
2550 0.1343122 0.9866667 
2600 0.1322797 0.9866667 
2650 0.1303498 0.9866667 
2700 0.1285149 0.9866667 
2750 0.1267679 0.9866667 
2800 0.1251025 0.9866667 
2850 0.1235127 0.9866667 
2900 0.1219935 0.9866667 
2950 0.1205399 0.9866667 
3000 0.1191476 0.9866667 
[1] "Accuracy: 0.986667"





지금까지 인공신경망(그 중, MLP)의 코드 구현을 이론과 병행하여 장황하고 길게 설명 드렸습니다. Hidden Layer가 1개인 경우를 예로 들어 설명했으나 모든 과정을 잘 이해하였다면 여러 개의 Hidden Layer를 갖는 MLP로 쉽게 확장 가능합니다.


또한, 단순 MLP 뿐만 아니라 CNN, RNN 등도 네트워크 구조가 다를 뿐 기본적인 Forward Pass와 Backward Pass는 동일하므로, 원리를 이해하는데에는 큰 문제가 없을 것입니다.

Comments