Michael's Study Blog

Backend Engineering & Deep learning!

0%

본 포스트는 Hands-on Machine learning 2nd Edition, CS231n, Tensorflow 공식 document를 참조하여 작성되었음을 알립니다.

Index

  1. Introduction of Convolution Operation
  2. Definition of Convolutional Neural Network(CNN)
  3. Back Propagation of CNN
  4. Partice

여기서는 1~3는 Part. A이고 4은 Part. B에서 다루도록 하겠다.

Introduction of Convolution Operation


Convolutional Neural Network는 Convolution 연산을 Neural Network에 적용한 것이다. 따라서 이를 알기 위해서는 Convolution 연산을 먼저 알아야할 필요가 있다. 관련 학과 대학생이라면 아마도 신호와 시스템을 배우면서 이를 처음 접했을 것이다. Continuous domain, Discrete domian까지 이 연산을 정의될 수 있고 각각에 따라 계산 방법 또한 배웠을 것이다. 예를 들어서 2차원의 Image와 2차원의 Filter의 Convolution 연산을 수식으로 표현해 보도록 하겠다.

  • Image 행렬 정의

Image의 $i$열, $j$행의 성분

  • Filter 행렬 정의

Filter의 $i$열, $j$행의 성분. 높이를 $k_1$, 너비를 $k_2$라고 가정.

  • $I$와 $K$의 Convolution 연산

위 정의를 조금 틀면 다음과 같이도 표현이 가능하다.

위와 같은 연산의 형태를 Correlation이라고 한다. 즉, Convolution 연산의 Filter를 $\pi$만큼 회전시킨다면 그것이 Correlation 연산인 것이다. 이는 아주 중요한 관계이므로 꼭 기억해 두도록 하자.

Definition of Convolutional Neural Network(CNN)


CNN의 정의는 위의 Convolution 연산을 사용하여 Weight와 Input을 계산하는 것이다. 매우 간단한 예시로 LeNet이라는 것을 보자. 너무 자주 나오는 예시라서 하품이 나올 것 같지만, 본래 기본이라는 것은 “쉬운”것이 아니라 “중요한”것이다.

그림에서의 각 파트를 분해해서 살펴보면 다음과 같다.

Image Input -> (Convolution) -> Feature Map -> (Pooling) -> Feature Map -> (Convolution) -> Feature Map -> (Pooling) -> Feature Map -> (Flatten) -> Feature Vector -> (FCNN) -> Feature Vector -> (FCNN) -> Feature Vector -> (Gaussian Connection) -> Output Vector

여기서 괄호 안에 들어 있는 것이 연산의 이름이다. FCNN은 다른 포스트에서 봤다고 가정하고, 여기서 주목해야 할 것은 Pooling Layer이다.

Pooling은 다양한 종류가 있는데 간단하게 한가지만 소개하자면 Max Pooling이 있다. 자세한 것은 Tensorflow 2 공식 문서를 참조하는 것이 더 좋을 것 같다.
여기서는 Pooling까지 자세히 다룰 이유는 없는 것 같다.

이제 본격적으로 Convolution 연산에 대해서 알아보도록 하겠다. 그 전에 FCNN 포스트에서도 그랬듯이, 수식 표현을 하기 위한 정의부터 하고 시작하자.

Definition 1

  • $u_{ijm}^l$: $l$번째 층의 Feature Map의 $m$번째 Channel $i$행, $j$열의 성분
  • $x_{ijm}$: Input의 $m$번째 Channel의 $i$행, $j$열의 성분
  • $w_{ijmk}^l$: $l$번째 층의 Weight의 $k$번째 Kernel Set에 $m$번째 Channel, $i$행, $j$열의 성분
  • $b_m^l$: $l$번째 층의 $m$번째 Channel의 Bias

이를 통해서 Convolution Layer를 수학적으로 표현해 보자면 다음과 같다.

여기서 $z$행렬은 $u$행렬에 activation function을 씌워 놓은 것이라고 생각하면 편하다.

이를 그림으로 표현해보면 다음과 같다.

위 수식에서는 아직 정의되지 않은 부분, 설명되지 않은 부분이 많다. 첫번째로 $H$, $W$, $K$의 의미, 그리고 index 부분의 $s$의 의미이다. 또한, 위의 연산은 Correlation인데 왜 Convolution 연산이라고 하는 것일까?

일단 첫번째는 $H$, $W$, $K$인데, 이는 각각 Weight의 높이, 너비, 채널수이다. 그리고 index 부분의 $s$는 Stride이다. Convolution 연산의 Weight를 옮겨가면서 곱셉을 할때 얼마나 옮길지를 결정한다. 이 값을 키울수록 결과 이미지의 크기가 작아진다. 자세한 것을 하나하나 까볼려면 오래 걸리니, 이 부분은 혼자서 잘 생각해 보는게 좋을 것 같다. 어디까지나 이 문서는 입문서가 아니라는 점을 알아주었으면 좋겠다. 기존에 Tensorflow/Pytorch만을 사용하던 사람들에게 이론을 제공하고자 함이다.

Back Propagation of CNN


자. 본격적으로 어려운 부분이다. 앞으로 Deep learning 강의를 써내려가면서 이보다 어려운 부분은 없다. 그리고 필자가 생각하기에도 쓸모가 없다. 단지 지적 유희를 위해서 읽어주기를 바라며 틀린 부분이 있다면 지적해 주기를 바란다.

그 전에, 왜 필자는 굳이 이 파트를 써내려 가는가를 적어보도록 하겠다. (잡담이니 굳이 안 읽어도 상관 없다.) 최근의 Deep learning 개발은 Auto Grad 계열의 알고리즘들을 활용하여 앞먹임 연산만을 정의하면 알아서 역전파 수식이 계산되어 BackPropagation을 편리하게 할 수 있다. 하지만 라이브러리에 모든 것을 맡기고 개발만 하는 것이 과연 좋은 개발자/연구원 이라고 할 수 있을까? 필요하다면 더 깊은 인사이트를 얻어서 문제를 해결해야할 필요가 있다. 이 글은 그런 사람들을 위함이기도 하고 나처럼 학문 변태들을 위한 것이기도 하다. 그러니 이 파트가 필요 없다고 판단되면 읽지 않는 것을 추천하고, 만약 틀린 것이 있다면 부디 연락해서 알려주었으면 좋겠다. 환영하는 마음으로 받아들이고 수정하도록 하겠다.

사족이 길었는데, 그래서 Back Propagation이 어떻게 정의되는 것일까? 큰 틀은 FCNN과 다를 바가 없다. Weight를 업데이트함에 있어서 Chain Rule을 활용하는 것이다. 그렇다면 어떻게 그것을 진행할 것인가?
우선 첫번째로 미분부터 써내려 가보자.

이것을 구해서 Weight를 업데이트하는 것이 Back Propagation의 핵심이다. 그렇다면 FCNN과 똑같이 일반화된 Delta Rule을 활용해 보는 것으로 시작하자. 이를 위해서 chain rule을 적용해 보면 다음과 같이 표현할 수 있다.

이때, $H’$, $W’$는 Convolution output의 결과 Feature map의 높이와 너비이다.
FCNN때와 똑같이 한다면 다음과 같이 Delta를 정의하고 식을 수정할 수 있다.

그리고 다음과 같이 식을 유도하는 것이 가능하다.

결국 다음과 같이 표현 가능하다.

이제 delta를 정의했으니, 앞층의 delta와 뒷층의 delta간의 관계를 밝혀내서 연산을 효율화 하면 된다. 이 과정을 손으로 유도하는 것은 필자가 생각해도 실용적 측면에서는 정말 쓸데가 없다. 왜냐하면 현대의 신경망 구조는 너무 복잡해져서 이걸 유도했다 쳐도 다른 구조들이 정말 많이 때문에 써먹을 수가 없기 때문이다. 하지만 아주 제한적인 경우에 대해서 이걸 유도해 보도록 하겠다.

  1. Convolution - Convolution layer 에서의 Delta 점화식

우선 다시 한번 delta에서 chain rule을 적용해 보도록 하겠다 . 이 과정은 FCNN에서도 했을 것이다. 따라서 최대한 간결하게 진행해 보도록 하겠다.

이때 $H’’$, $W’’$, $C’’$는 다음 층에서의 Feature Map의 크기이다.

이를 전개해 보면 다음과 같다.

이는 다음과 같이 Convolution 연산으로 표현될 수 있다.

위의 $\odot$은 성분 끼리의 곱(element wise multiplication)을 의미한다.

  1. Convolution - Pooling - Convolution 에서의 Delta 점화식

위에서 delta를 유도함에 있어서 한 층이 더 추가될 뿐이다. 다음과 같이 말이다. 여기서는 Max Pooling, Average Pooling을 예로 들어보겠다.

위 식에서 중간에 있는 $l+1$층이 Pooling 층이다. 이 미분은 다음과 같이 정의된다.

  • Max Pooling의 경우
  • Average Pooling의 경우

여기서 $H’’’$,$W’’’$는 Pooling Layer의 크기이다.

결국 Pooling layer까지 포함하면 다음과 같이 convolution 연산으로 정의할 수 있다.

어디까지나 이렇게 연산을 할 수 있는 이유는 Pooling layer는 업데이트를 할 필요가 없기 때문이다. 만약 업데이트를 할 피라미터가 있다면 “제대로” 다시 delta rule의 방정식을 수정해 줘야 한다.

  1. 이게 정말 쓸데 없는 이유

현대의 신경망은 에시당초 Convolution layer에서 탈각하는 분위기 인데다가 Convolution - Feed Forward 관계나 ResNet같은 현대의 신경망 구조에서는 이런 복잡한 수식으로 구현하는 것은 사실상 불가능에 가깝다. 그러니 우리는 Autograd를 믿고 이런건 그냥 지적 유희로만 알아 두도록 하자.

그리고 마지막으로 Bias의 Update 방법을 알아보면 다음과 같다.

이때, 곱셈 term의 뒷 항은 전부 1이므로 다음과 같은 식이 성립한다.

이걸로 CNN의 이론 부분은 끝났다. 다음에는 실습 부분으로 찾아오도록 하겠다.

본 포스트는 Hands-on Machine learning 2nd Edition, CS231n, Tensorflow 공식 document를 참조하여 작성되었음을 알립니다.

Index
  1. Basic of Neural Network
  2. Definition of Fully Connected Neural Network(FCNN)
  3. Feed Forward of FCNN
  4. Gradient Descent
  5. Back Propagation of FCNN
  6. Partice(+ 부록 Hyper parameter tuning)


6. Partice(+ 부록 Hyper parameter tuning)


자 실습 시간이다. 왜 실습을 Part. B로 뺐느냐? FCNN이 뭐 할게 있다고?
뭐 할게 있겠다. Tensorflow 2가 대충 어떻게 이루어 졌는지 설명하기 위해 분량 조절을 위해서 뺀것이다.
무엇보다 Part. A 쓰는데 수식을 너무 많이 써서 힘들어서 분리했다. Tlqkf
자, 우선 실습에 들어가기에 앞서, TF 2를 애정하는 나로써는 앞으로 이 스터디 포스트에 작성될 대부분의 소스코드를 꿰뚫는 구현 체계를 먼저 설명하고 넘어가겠다.
다음 사진을 보자.

출처: pyimagesearch blog: 링크

위 그림에서 필자는 대부분의 코드를 Model Subclassing 방식으로 구현할 것이다. 구현 하면서 설명할 터이니 잘 따라와 주기를 바란다.
여기서부터는 대학교 강의 수준의 객체지향프로그래밍 지식을 갖추지 않으면 읽기 힘들 수 있다. “상속”, “오버라이딩”의 개념이라도 살펴보고 오자.

6-1. Model Subclassing

우선 준비한 소스부터 보고 시작하자.
file: model/FCNN1.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from tensorflow import keras
import numpy as np

class FCNN(keras.Model):
def __init__(self, _units = [56, 56], _activation = ['relu', 'relu'], **kwargs):
super().__init__(**kwargs)
self.Hidden1 = keras.layers.Dense(units=_units[0], activation=_activation[0], kernel_initializer="normal")
self.Hidden2 = keras.layers.Dense(units=_units[1], activation=_activation[1], kernel_initializer="normal")
self._output = keras.layers.Dense(units=10, activation="softmax")

def call(self, Input):
hidden1 = self.Hidden1(Input)
hidden2 = self.Hidden2(hidden1)
Output = self._output(hidden2)
return Output

자, 별거 없다. keras를 써봤다면 뭔지 바로 감이 올 것이다.
이 부분에 대해서는 함수에 대한 설명보다는 class에 대한 설명을 해야할 것 같다. 바로 FCNN class가 상속을 받은 부모 클래스인 keras.Model 클래스에 관해서이다.

keras.Model class는 케라스에서 Deep learning을 진행하는 모델을 정의해주는 class이다. 우리가 이미 존재하는 layer를 가져다가 특정 순서로 연산을 진행하는 graph를 만들어 내기 위한 class이다. 하지만 그렇게 어렵게 생각하지 말자. 사용하는 것을 보면 바로 답이 나온다.

여기서는 생성자와 call이라는 함수를 오버라이딩을 통해서 사용자가 재 정의를 해서 사용한다. call은 우리가 구현하고자 하는 model이 feed forward를 진행할때 호출되는 함수이다. 생성자는 사용할 폭이 넓다. 여기서는 model을 구성하는 layer를 정의하는데 사용하였는데, 꼭 그 역할만 할 필요는 없는 것이다.

가타부타 말이 많았는데, 실제 어떻게 동작을 시키는가?

file: train_MNIST.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from model.FCNN1 import FCNN
from tensorflow import keras
import tensorflow as tf
import numpy as np

mnist = keras.datasets.mnist

(train_img, train_labels), (test_img, test_labels) = mnist.load_data()

train_img, test_img = train_img.reshape([-1, 784]), test_img.reshape([-1, 784])

train_img = train_img.astype(np.float32) / 255.
test_img = test_img.astype(np.float32) / 255.

model = FCNN()
model.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=["accuracy"])
model.fit(train_img, train_labels, batch_size = 32, epochs = 15, verbose = 1, validation_split = 0.3)

model.evaluate(test_img, test_labels, verbose=2)

간단하다. keras.Model class를 상속 받았으니, 그곳에 있는 기본 함수들을 모두 사용할 수있다. fit method로 학습을 진행하고 evalute로 test데이터로 모델을 평가한다.
이는 기존에 keras의 사용법과 별반 다른게 없다.

여기까지는 쉽다. 하지만 이러고 끝낼거면 시작도 하지 않았다.

다음을 진짜 자세하게 설명할 것이다.

file: model/layer.py

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
53
54
55
56
57
58
59
import tensorflow as tf
from tensorflow import keras

# Weight Sum을 진행하는 Layer를 정의하는 예제 1
class WeightSum(keras.layers.Layer):
def __init__(self, units=32, input_dim=32, trainable=True, name=None, dtype=None, dynamic=False, **kwargs):
super().__init__(trainable=trainable, name=name, dtype=dtype, dynamic=dynamic, **kwargs)
# tf.Variable을 활용하는 예시
# 가중치 행렬을 초기화 하기 위한 객체
w_init = tf.random_normal_initializer()
# 가중치 행렬을 tf.Variable로 정의함. 당연히 학습해야하니 training parameter를 true로 둠.
self.Weight = tf.Variable(
initial_value = w_init(shape=(input_dim, units), dtype="float32"),
trainable = True
)
b_init = tf.zeros_initializer()
# 위랑 같음. 별반 다를거 X
self.Bias = tf.Variable(
initial_value = b_init(shape=(units, ), dtype="float32"),
trainable = True
)
# add_weight를 활용하는 예시 - Short Cut
'''
# 이는 keras.layers.Layer의 method 중에서 add_weight를 사용하는 방법임.
# 주로 training을 시키기 위한 행렬을 이렇게 선언해서 나중에 편하게 불러오기 위한 목적이 큼.
self.Weight = self.add_weight(
shape=(input_dim, units), initializer="random_normal", trainable=True
)
self.Bias = self.add_weight(shape=(units,), initializer="zeros", trainable=True)
'''

def call(self, Input):
# 행렬 곱을 위한 tf 함수임. 별거 없음
# 그냥 U = WZ + B 구현한거.
return tf.matmul(Input, self.Weight) + self.Bias

# Weight Sum을 진행하는 Layer를 정의하는 예제 2
class WeightSumBuild(keras.layers.Layer):
def __init__(self, _units = 32, trainable=True, name=None, dtype=None, dynamic=False, **kwargs):
super().__init__(trainable=trainable, name=name, dtype=dtype, dynamic=dynamic, **kwargs)
self.units = _units

def build(self, input_dim):
# 이 함수는 밑에서 자세히 설명함.
self.Weight = self.add_weight(
shape=(input_dim[-1], self.units),
initializer = "random_normal",
trainable= True
)

self.Bias = self.add_weight(
shape = (self.units,),
initializer = "random_normal",
trainable = True
)

def call(self, Input):
# 상기 동일.
return tf.matmul(Input, self.Weight) + self.Bias

위의 2개의 class는 하는 짓거리가 완벽하게 똑같다. 하지만 하는 짓거리는 같은데 아주 치명적인 부분이 조금 다르다. 바로 build 함수의 overwritting이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#                입력 벡터/텐서의 크기를 받음
def build(self, input_dim):
# 그에 따라서 Weight와 Bias의 차원을 결정함.
# 물론, Bias는 차이가 없을 지언정, Weight는 크게 차이가 나게 된다.
self.Weight = self.add_weight(
shape=(input_dim[-1], self.units),
initializer = "random_normal",
trainable= True
)

self.Bias = self.add_weight(
shape = (self.units,),
initializer = "random_normal",
trainable = True
)

이 함수는 call이 호출되기 전에 무조건 실행되는 함수라고 생각하면 된다. 즉, 호출하기 전에 Weight를 정의하는 것이다. 즉, 입력 벡터의 크기에 따라 모델의 형태가 알아서 바꾸게 해줄 수 있는 것이다. 이는 Image를 처리할때 이점이 될 수 있다.
Image를 학습시킬때, 이러한 처리가 없으면 이미지를 전부 동일한 크기로 만들어 주어야 한다. 하지만 build 함수를 정의해서 그때 그때 입력 벡터/텐서에 따라 커널을 수정해 주면 굳이 그럴 필요가 없다. 전처리 비용이 줄어드는 것이다.

그리고, 이제 이를 학습시키기 위한 코드를 보도록 하자.

file: train_MNIST_2.py

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import tensorflow as tf
import numpy as np
from tensorflow import keras
from model.FCNN2 import FCNN

EPOCHS = 15
LR = 0.01
BatchSize = 32

# 이미지 불러오기
mnist = keras.datasets.mnist
(train_img, train_labels), (test_img, test_labels) = mnist.load_data()

# 필요한 전처리
train_img, test_img = train_img.reshape([-1, 784]), test_img.reshape([-1, 784])
train_img = train_img.astype(np.float32) / 255.
test_img = test_img.astype(np.float32) / 255.

# Train-Validation Split
validation_img = train_img[-18000:]
validation_label = train_labels[-18000:]
train_img = train_img[:-18000]
train_labels = train_labels[:-18000]

# Train Data의 규합. & Batch 별로 쪼갬
train_dataset = tf.data.Dataset.from_tensor_slices((train_img, train_labels))
train_dataset = train_dataset.shuffle(buffer_size=1024).batch(BatchSize)

# Validation Data의 규합 & Batch 별로 쪼갬
validation_dataset = tf.data.Dataset.from_tensor_slices((validation_img, validation_label))
validation_dataset = validation_dataset.batch(BatchSize)

# Optimizer & Loss Function 정의
optimizer = keras.optimizers.Adam(learning_rate=LR)
loss_function = keras.losses.SparseCategoricalCrossentropy(from_logits=True)

# 학습이 잘 되고 있나 확인하기 위한 지표를 확인하기 위함.
train_accuracy = keras.metrics.SparseCategoricalAccuracy()
val_acc_metric = keras.metrics.SparseCategoricalAccuracy()

model = FCNN()

# Custom Training을 위한 반복문
for epoch in range(EPOCHS):
print("Epoch %d start"%epoch)
for step, (x_batch, y_batch) in enumerate(train_dataset):
with tf.GradientTape() as tape:
# Model의 Feed Forward
logits = model(x_batch, training=True)
# Feed Forward 결과를 바탕으로 Loss를 구함
loss_val = loss_function(y_batch, logits)
# 위의 과정을 바탕으로 gradient를 구함
grad = tape.gradient(loss_val, model.trainable_weights)
# Optimizer를 통해서 Training Variables를 업데이트
optimizer.apply_gradients(zip(grad, model.trainable_weights))
# Batch 별로 Training dataset에 대한 정확도를 구함.
train_accuracy.update_state(y_batch, logits)
if step % 500 == 0 :
print("Training loss at step %d: %.4f"%(step, loss_val))
# 정확도를 규합해서 출력
train_acc = train_accuracy.result()
print("Training acc over epoch: %.4f" % (float(train_acc),))
train_accuracy.reset_states()

# Validation을 진행함.
for x_batch_val, y_batch_val in validation_dataset:
# Validation을 위한 Feed Forward
val_logits = model(x_batch_val, training = False)
# Batch 별로 Validation dataset에 대한 정확도를 구함
val_acc_metric.update_state(y_batch_val, val_logits)
# 구한 정확도를 규합하여 출력함.
val_acc = val_acc_metric.result()
print("Validation acc: %.4f" % (float(val_acc),))
val_acc_metric.reset_states()

이는 TF2에서 추가된 tf.GradientTape를 통해서 사용자 정의 학습 루프를 만든 것이다. 각 줄마다 주석을 달아 놓았으니 Part. A의 내용을 숙지했다면 그렇게 어렵지 않게 알아들을 수 있을 것이다.

이제, 한가지 의문이 든다. 그렇다면 위의 Hyper parameter들을 변화시켜가면서 model을 최적화 하려면 노가다 밖에 답이없는건가? 답은 아니다. 이번에는 맛보기만 보여줄 것이다. 이는 scikit learn의 RandomizedSearchCV를 통해서 확인할 수 있다.

file: RandomSearch.py

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
from tensorflow import keras
from scipy.stats import reciprocal
from sklearn.model_selection import RandomizedSearchCV
import numpy as np

mnist = keras.datasets.mnist
(train_img, train_labels), (test_img, test_labels) = mnist.load_data()
train_img, test_img = train_img.reshape([-1, 784]), test_img.reshape([-1, 784])
train_img = train_img.astype(np.float32) / 255.
test_img = test_img.astype(np.float32) / 255.

# 여기 parameter들의 Key는 무적권 입력 함수의 parameter와 같아야 한다. 아마도 **kwargs로 한번에 보내버리는 것일거다.
param_distribution = {
"n_hidden": [0,1,2,3],
"n_neurons": np.arange(1,100),
"lr": reciprocal(3e-4, 3e-2)
}

# 이것을 사용하기 위해서는 모델을 만들어줄 함수가 하나 필요하다.
# 여기서는 그냥 Sequential API를 사용하였다. 생각하기 귀찮았다. ㅋ;;
def Build_model(n_hidden = 1, n_neurons=30, lr = 3e-3, input_shape=[784]):
model = keras.models.Sequential()
model.add(keras.layers.InputLayer(input_shape=input_shape))
for layer in range(n_hidden):
model.add(keras.layers.Dense(n_neurons, activation='relu'))
model.add(keras.layers.Dense(units=10, activation="softmax"))
optimizer = keras.optimizers.SGD(learning_rate=lr)
model.compile(optimizer=optimizer, loss='sparse_categorical_crossentropy', metrics=["accuracy"])
return model

# 이는 keras 모델을 scikit learn에서 관리하기 위해 호출하는 함수이다.
keras_classify = keras.wrappers.scikit_learn.KerasClassifier(Build_model)
# 여기서부터 본론이다. parameter_distribution으로 주어진 집합 한에서 가장 좋은 성능의 모델을 탐색한다.
# 여기서는 Cross-Validation을 사용한다. cv 항은 몇개로 Validation-training set을 분리할지 정하는 것이다.
rnd_search_model = RandomizedSearchCV(keras_classify, param_distributions=param_distribution, n_iter = 10, cv = 3)
# 이제 주어진 데이터를 가지고 학습을 돌면서 최적의 모델을 탐색한다.
rnd_search_model.fit(train_img, train_labels, epochs=10, validation_data=(test_img,test_labels), callbacks=[keras.callbacks.EarlyStopping(patience=10)])

print(rnd_search_model.best_params_)

오늘은 이것으로 끝내도록 하자.
다음 포스트는 Convolutional Neural Network를 오늘처럼 다뤄볼 예정이다.

본 포스트는 Hands-on Machine learning 2nd Edition, CS231n, Tensorflow 공식 document를 참조하여 작성되었음을 알립니다.

Index

  1. Basic of Neural Network
  2. Definition of Fully Connected Neural Network(FCNN)
  3. Feed Forward of FCNN
  4. Gradient Descent
  5. Back Propagation of FCNN
  6. Hyper parameter tuning
  7. Partice

여기서는 1~5는 Part. A이고 6,7은 Part. B에서 다루도록 하겠다.

Basic of Neural Network


Neural Network의 기본을 설명할때 항상 나오는 것이 바로 뉴런 구조 사진이다. 여기서는 그들을 생략하도록 하겠다. 굳이 여기서 설명할 필요를 못 느끼겠다.
궁금하면 SLP(Single Layer Perceptron)와 MLP(Multi Layer Perceptron)에 대해서 구글링을 해보도록 하자.

우선 FCNN을 본격적으로 들어가기 전에, 지도 학습에 대한 Deep learning의 학습 프로세스에 대한 개략적인 모식도를 보고 가자.

이러한 모식도에 개략적인 생략이 들어 갔고, 반례도 충분히 있겠지만, 지도 학습의 형태로 학습하는 대부분의 Neural Network는 이런 Process로 학습을 하게 된다.
앞으로 나오는 신경망 구조들 중에서 위의 “출력”, “평가”, “학습”의 3가지의 과정을 집중적으로 기술할 생각이다.

그렇다면 Neural Network 구조를 활용해서 풀 수 있는 문제는 무엇이 있을까?
여기서는 크게 2가지만 다룰 예정이다.

  1. Classification: 어떤 입력을 특정 범주로 분류해내는 문제.
  2. Regression: 어떤 변수들 사이의 관계를 도출해내는 문제. (Modeling에 가깝다고 생각하면 편함.)

이러한 2개의 문제의 범주를 설명하고 넘어가는 이유는 Neural Network로 어떤 문제를 풀 것이냐에 따라서 학습 과정에서 다양한 부분이 상이하게 변하기 때문이다. 이는 나중에 따로 자세하게 다룰 예정이다. 지금은 일단 이런 것이 있다 라는 것만 알아두고 가자.


Definition of FCNN


여기서는 Fully Connected Nueral Network의 정의를 먼저 알아보도록 하겠다. 간단하게 이를 표현하자면 다음과 같은 구조를 가지는 Neural Network를 일컫는다.

Figure 1: FCNN의 모식도 1
이렇게 앞층의 뉴런과 뒷층의 뉴런이 빠짐없이 전부 연결된 구조를 FCNN이라고 한다. 여기서 만약 독자가 MLP를 모르는 상태라면 위의 구조가 상당히 추상적으로 다가올 것이다. 간단하게 생각해서, 각 뉴런들은 숫자를 가지고 있다고 생각하면 된다. 다음 그림을 보면 훨씬 쉽게 이해할 수 있다.

Figure 2: FCNN의 모식도 2

이렇게 층의 뉴런마다 간선으로 연결되어 있고 각 뉴런들은 뒤의 뉴런들에게 어떠한 가중치를 반영해서 자신의 값을 가지고 있다고 생각하면 된다. 이제부터 우리는 이러한 구조를 가진 Neural Network가 어떻게 주어진 문제에 대해서 결과를 도출하고 학습을 진행하는지 알아보도록 하겠다.


Feed Forward of FCNN


이 과정은 학습이 되었건 되지 않았건 FCNN의 구조에서 입력을 통해 출력을 얻어내는 과정이다. 층층이 쌓인 Neural Network 구조에서 앞으로 계속 나아가면서 결과를 도출해 내는 모습에서 Feed Forward(앞 먹임)이라는 이름이 붙은 것이다.
앞으로 대부분의 예시는 Figure 2와 비슷한 형식으로 나올 것이다. 그리고 앞으로는 뉴런이라는 명칭보다는 Unit이라는 명칭을 사용할 것이다.
이제, 이전 슬라이드에 있는 것들을 기호를 명확하게 정의해 보도록 하겠다.

Definition 1

  • $x_i$: 입력 벡터의 $i$번째 원소
  • $u_i^k$: $k$번째 Layer의 $i$번째 Hidden Units의 값.
  • $w^k_{ij}$: $k$번째 층의 $i$번째 Unit과 $k-1$번째 층의 $j$번째 Unit을 이어주는 가중치
  • $y_i$: 출력층의 $i$번째 Unit
  • $n_k$: $k$번째 층의 Unit의 갯수

그렇다면 이제 Feed Forward를 위해서 가장 중요한 부분을 설명하도록 하겠다. Weighted Sum(가중합)에 대한 부분이다.
앞선 Figure 1Figure 2에서 앞층과 뒷층의 관계를 우리는 다음과 같이 정의한 것이다.

정말 단순하게 생각해서 이전 유닛에 해당하는 가중치만큼 앞의 유닛에 반영해 주면 되는 것이다. 이렇게 뒤의 층에서 앞의 층으로 순차적으로 계산해 나가면 된다.

하지만 여기에는 치명적인 문제점이 있다. 바로, 이렇게 된다면 Neural Network 구조로 Linear Function 밖에 표현할 수 없다는 것이다.
위의 $equation (1)$의 형식으로 Feed Foward를 진행하게 된다면 층을 쌓는 것이 의미가 없어진다. 그 이유는 단순히 일변수 선형 함수를 생각해보면 알 수 있다.
우리가 $y = a_ix$라는 임의의 $N$개의 함수들을 층층이 쌓는다 할저언정 그것은 새로운 선형 함수 $y = a’x, \quad a’ = a_1a_2a_3\cdot\cdot\cdot a_N$ 를 만들어 내는 것과 별반 다를 것이 없다. 이렇게 되면 가장 대표적인 문제점은 바로 XOR문제를 해결할 수 없다는 것이다. 이는 너무 보편적인 문제라서 그냥 구글링 하면 나보다 설명 잘해놓은 글이 널려있다. 따라서 여기서는 생략한다.
똑같은 논리이다. 이에 대한 자세한 논의는 Appendix. A에 자세히 기술하도록 하겠다.

이러한 문제를 해결하고 더 다양한 함수를 신경망이 표현할 수 있게 하기 위해서 Activation function과 Bias를 적용하게 된다.

Activation Function(활성 함수)

신경망이 Linear한 함수만을 근사시키는 것을 방지하기 위해서 중간에 Non Linear Function을 Unit에 적용하게 된다. 이때 이러한 함수를 Non linear function이라고 한다. 그렇다고, 전부 Non linear인 것은 아니다. 하지만 Non linear function이여야 앞선 문제가 발생하지 않을 것이다. 그 종류는 대략 다음과 같다.

  1. Sigmoid: $\sigma(x) = \frac{1}{1+\exp(-x)}$
  2. ReLU(Rectified Linear Unit): $max(0, x)$
  3. Leaky ReLU: $max(0.1x, x)$
  4. Hyperbolic tangent: $\tanh(x) = \frac{\exp(x) - \exp(-x)}{\exp(x) + \exp(-x)}$
  5. ELU(Exponential Linear Unit): $\sigma(x) = xu(x) + a(\exp(x)-1)u(-x)$

이것 뿐만 아니라 진짜 개 많다. 나머지는 Tensorflow 공식 Document나 구글링을 통해서 알아보도록 하라.

이렇게 Activation Function을 씌운 값을 앞으로는 다음과 같이 표현하도록 하겠다.

Defintion 2: 활성함수를 적용한 Unit의 값.

Bias (편향)

Bias로써는 Non Linear성을 신경망에 추가할 수는 없다. 하지만 Activation Function을 평행 이동 시켜주는 역할을 하게 해서 입력이 0인 지점에서의 신경망의 자율도를 높여주게 된다. 예를 들어보자. 위에 예시를 든 Activation function들에서는 예를 들어서 $(0, 32)$같은 점을 표현할 수 없다. 그렇기 때문에 Bias를 주어서 평행 이동을 시켜주는 것이다. 이것의 유무가 신경망의 학습 양상과 성능이 크게 차이를 주는 경우가 많다.

즉, 최대한 많은 함수를 근사할 수 있도록 위와 같은 설정을 추가해 주는 것이다.
이러면 어떤 잼민이는 이런 질문을 할 수 있을 것이다.

???: 아 ㅋㅋㅋㅋ 그런 근사 못 하는 함수는 어쩔건데요 ㅋㅋ루삥뽕

걱정 마라. 이미 만능 근사 정리(Universal Approximation Theorem, 시벤코 정리)에 의해서 증명되었다. 그냥 안심하고 쓰면 된다.

그렇다면 Bias를 적용해서 다시금 Unit의 값을 작성해 보자.

Definition 3: Bias를 적용한 Unit의 값


Figure 3: Bias를 반영한 Neural Network의 표현

이제 이를 행렬로 표현해보자. 행렬로써 표현하여 식을 짧고 간편하게 정리할 수 있으며, 컴퓨터에서의 구현을 다소 직관적이고 편하게 할 수 있다.






Gradient Descent


이번 장에서는 Gradient Descent에 대해서 다룰 예정이다. 하지만 너무 깊게는 안 다룰 생각이다. 그렇다고 걱정할 필요는 없다. 나중에 존나 자세하게 다룰거니깐 걱정은 붙들어 매도록 하자.

Figure 4. Gradient Descent을 묘사하는 Figure
Gradient Descent는 간단하게 설명해서 Gradient를 활용해서 Objective Function(목적 함수)의 최저점을 찾는 것이 목표이다. 위의 사진처럼 말이다. 이의 명확한 표현을 직관적으로 납득시키기 위해서 다음의 사진을 보자.

Figure 5. Gradient Descent를 직관적으로 설명하기 위한 그림

이처럼 목적 함수의 미분값을 활용하여 목적 함수의 최저점을 찾는 것이 가능하다. 이에 대한 자세한 설명은 진짜 나중에 질리도록 해주도록 하겠다. 걱정하지 말아달라.

그렇다면 이제 Neural Network에서 목적 함수는 어떤 것을 사용하는지 알아봐야한다. 이것도 지금은 그냥 “그런 것이 있다”라고만 생각하자. 이 파트는 신경망을 이해하는데 있어서 중요하고 심도있는 내용이기 때문에 나중에 Post 하나를 통으로 열어서 집중적으로 설명할 것이다.

이번 포스트에서는 목적함수로써 주로 2가지를 다룰 것이다.

  1. Mean Squared Error
  1. Cross-Entropy

$Equation 3$은 주로 regression 문제에서 목적 함수로 쓰이고, $Equation 4$는 주로 classification 문제에서 쓰인다.
이렇게 Error function이 있는데 Update를 어떻게 한다고 하는 것일까? 다음과 같은 공식으로 Update 하면 된다

직관적으로 생각해보면, 그래프의 미분값이 음수이면 아래로 볼록한 이차 함수에서 극소점이 오른쪽에 있다는 것이다. 그렇다면 변수에 양수를 더해줘야 극소점에 다가갈 것이다. 반대도 마찬가지이다. 그리고 한번 Update를 할때 너무 변화폭을 급하게 하지 않기 위해서 learning rate를 곱해줘서 update를 해준다. 그렇게 적절한 learning rate를 지정하여 몇번 반복해주면 최저점에 수렴해 있을 것이다.

집요하지만, 이번 주제에 관한 내용은 나중에 “Optimization, Error function, and Problem”에 관한 포스트에서 자세하게 다룰 것이다. 그러니 지금은 직관적으로만 학습의 Process를 이해하는데 사력을 다해주기를 바란다.

Back-propagation of FCNN


이번에는 어떻게 신경망에 Gradient Descent를 적용할 것인지에 대한 내용이다. 위의 Gradient Descent를 배운 사람이라면 를 활용하여 가중치의 Update를 다음과 같이 해줘야 할 것이라는데 이견을 가지지는 않을 것이다.

여기서 $K$는 층의 갯수이다.

이때 수치해석을 공부한 사람이라면, 다음과 같은 말을 할 수 있다.

적당한 $\epsilon$을 선택하고, 수치 미분을 해주면 되지 않을까?

뭐, 틀린 말은 아니다. 실제로도 좋은 방법이 될 수 있다.(신경망이 아니라면 말이지) 하지만 이건 Neural Network이다. 미친 연산량을 자랑하는 이쪽 바닥에서 안이하게 접근했다가는 피를 볼 수 있다.
요즘은 많은 알고리즘들이 발달해서 어떨지는 모르지만, 기본적인 수치 미분은 여기서 좋은 방법이 아니다. 우선 수치 미분을 진행하는 방법은 다음과 같다.

우선 이걸 신경망에 적용시키기 위해 가장 먼저 떠오르는 방법은 다음과 같을 것이다.

  1. $f(x+\epsilon)$를 계산한다.
  2. $f(x)$를 계산한다.
  3. 미분을 계산한다.

자, 1,2번의 과정을 신경망에 적용해 주려면 당신은 2번의 함수 값을 계산해 줘야한다. 만약, 계산량이 크지 않은 함수라면 빨리 되겠지만, 이건 신경망이다. Figure 3을 보자. 저기 그려져 있는 모든 간선에 대해서 Update를 해줘야하는데 진짜 어림도 없는 방법이 아닐 수 없다. 이걸 parameter마다 해준다? 진짜 어림도 없는 소리가 된다.

그렇다면 다음으로 패기 넘치는 잼민이가 다음과 같이 말한다.

그냥 간단한 함수만 쓰고 손으로 계산하면 안됨? 그것도 못하누 ㅋㅋ루삥뽕

^^ 열심히 해보십셔 잼민님^^(말도 안되는 소리는 집어 치우자)

여기서 tensorflow의 자동 미분(Autograd)가 나오는데, 우리는 이 전에 수학적인 부분으로 이를 접근해 보도록하자.

가장 먼저, 이는 엄청나게 곂곂이 쌓인 함수이다. 일변수 Scalar 함수로 예시를 들어보면 다음과 비슷한 것이라고 생각할 수 있다.

이를 미분해야한다고 생각해보자. Calculus를 배웠다면 가장 먼저 떠오르는 것은 Chain Rule 일 것이다.

하지만, 어디서부터 어떻게 Chain Rule을 적용해야하는지 막막하다. 그 전에 우선 상대적으로 구히기 쉬울 것 같은 출력층 바로 이전의 가중치의 미분을 Chaine Rule을 통해서 구해보자.

이는 출력층 바로 이전의 층의 가중치이기에, Chain Rule을 통해서 비교적 쉽게 구할 수 있다. 다음과 같이 말이다.
(Error function(목적 함수)는 MSE를 사용하였다고 생각하자.)

이렇게 성공적으로 $W^K$의 미분은 구했다고 치고, 어떻게 그 뒤의 층을 구할 것인가?
우선, 다음 그림을 보자.

Figure 7. Neural Network 구조에서 일부를 떼어서 표현한 그림
이는 Neural Network 구조에서 일부를 떼어낸 것을 표현한 그림이다. 위의 $\vdots$가 그려진 Unit은 그냥 거기에 많은 수의 Unit들이 있다고 생각하면 될 것이다.
우리는 여기서 $W^k$의 $E$에 대한 미분을 구하는 것이 목표이다.
즉, $Equation (9)$를 구하는 것이 목표이다.

그것을 위해서 우선, Chaine Rule을 진행해 보자.

여기까지만 봐서는 잘 모르겠다. 하지만 다음의 그림을 보고, $\frac{\partial E}{\partial u^k_i}$ 에 한번 더 Chain Rule을 적용해 보자.

Figure 6. $u^k_i$가 영향을 주는 앞 층의 Unit들
FCNN의 정의에 의해, $u^k_i$는 앞층의 모든 뉴런에 연결되어 있다. 그리고 그 앞 층의 뉴런들은 또 그 다음층으로 값을 전달하며, 점점 출력층에 가까워 지고, Error에 영향을 미치게 된다. 한마디로, 다음과 같이 Chain Rule을 펼칠 수 있다.

이때 우리는 위의 $Equation (10)$에서 유사한 부분을 찾을 수 있다.

그렇다면 우리는 다음과 같이 정의를 해보자.

그렇다면 $Equation (10)$을 다음과 같이 작성할 수 있다.

이 점화식이 이번 포스트의 핵심이다.
그렇다면 식을 다시 정리해서, 최종적인 미분 값을 구해보자.

그리고, $Equation (12)$를 통해서 $Equation (10)$을 다시 써보면 다음과 같다.

자! 다 왔다. 이제 최종 미분식을 다시 써보자.

이 포스트의 핵심은 이것이다.
결국 우리는 상대적으로 구하기 쉬운 출력층의 미분을 구하는 과정에서 미분을 구하기 어려운 층의 미분을 구할 수 있는 실마리를 얻은 것이다.
무슨 말이냐? 우리는 $\delta^k_i$를 정의했다면, 출력층의 $\delta^K$도 구할 수 있을 것이다. 그렇다면 $K-1$층의 $\delta^{K-1}$는 $\delta^K$를 통해서 구할 수 있으니, 결국 $W^{K-1}$층의 미분도 구할 수 있다는 뜻이 된다.

본격적으로 $\delta^K$를 구해보자.

그렇다. 바로 구할 수 있다. 그렇다면 이를 통해서 뒤에 있는 층의 미분을 차례로 구할 수 있는 것이다.
Feed Forward와는 달리, 이 과정은 방향이 반대로 진행된다. 따라서 이는 Back Propagation이라고 하며, 이렇게 $\delta$를 정의하여 미분을 구하는 방식을 일반화된 델타 규칙이라고 한다.

자, Weight의 미분은 구했으니, Bias의 미분도 구해야한다. 이는 매우 간단하다.

이렇게 우리는 $\delta$에 관한 값만 구하면 신경망을 Update 하기 위한 미분을 전부 구할 수 있다. 그렇기에, 핵심이라고 할 수 있는 방정식 $Def. 8$, $Equation (13)$, $Equation (15)$, $Equation (14)$에 대해서 행렬로써 표현해 보자.

Definition of Delta

Delta Matrix Relation

Bias Update Equation

Weight Update Equation

이걸로 FCNN의 Feed Forward, Back Propagation의 이론적인 부분이 끝났다. 이제 실습은 Part. B에서 마저 다룰 예정이다.