[혼만딥] 2주차 LeNet 사용해보기
본 게시글은 한빛미디어의 혼자 만들면서 공부하는 딥러닝을 바탕으로 작성되었습니다.
시작하기에 앞서…
너무 시작부터 글을 너무 많이 썼나 다음 글을 쓰기 너무 귀찮았었다. 그리고 갑자기 조금 일이 생겨서 부랴부랴 하느라 조금 자체 휴강?을 한번 해보았다. 이번에 불법 지각을 미리 해버려서 나머지 주차들은 열심히 시간을 맞춰서 올려야겠다. 사실 혼공 족장님께서 다른 글들 피드백 하시는 속도를 보고 아 이 정도 속도면 나중에 올려도 되겠다 싶었던 불충한 마음이 든 것도 사실이지만 일단 넘어가기로 하자.
이번에 우수 혼공족으로 선정되어 메가커피의 미숫가루 커피를 받았다. 그런데 사실 난 커피를 마시지 않기에 오랜만에 초코칩 프라페에 펄을 추가해서 먹었다. 초코 프라페에 펄 추가는 실패하지 않는데 이번엔 실패했다. 뭔가 이렇게 먹을 때마다 실패하는 것 같다. 지난번에는 실수로 프라페가 아니라 라떼에 펄을 추가해버렸고, 그 전에는 펄 추가까진 잘 했는데 빨대를 그냥 빨대를 가져와서 집에서 펄을 숟가락으로 떠먹고, 이번에는 제조 과정에서 문제가 있었는지 얼음은 하나도 없고 초코칩 국물에 펄 추가를 해서 먹게 되었다.
뭔가 이상한을 느낀다면 정상이다. ChatGPT한테 내 손의 사진을 지워달라고 해봤는데 생각보다 잘 지워주는 것 같다. 그리고 혼공 족장님이 너무 부담스러운 후기를 남겨주셔서 블로그를 쓰기가 무서워졌다. 원래 목표를 이루기 위해서는 주변 사람에게 떠벌리고 다니는 게 효과적이라는 건 들었다. 그런데 난 사람 별로 없는 거리에서 피켓 들고 서있는 정도를 생각했는데 혼공 족장님께서 시장 바닥에 확성기 틀어놓고 홍보해주시는 느낌이라 부담스러워졌다.
하지만 어쩔 수 없다. 이것도 스불재인 것이겠지… 아 그리고 생각해보니 ebook에 대해서 말씀하셨던 것이 생각났다. 사실 ebook을 그동안 한 번도 안 사다가 이번에 처음으로 약간 돈이 아까워서 ebook을 처음 사본 것인데 매우 불만족하고 있다. 그리고 생각보다 책 내용이 나쁘지 않은 것 같아서 더 후회 중이다. 비록 책꽃이가 다 차서 책을 책상에도 쌓아두고 있는 상황이지만 책은 종이책으로 사서 보는게 제일 좋은 것 같다. 그리고 책이 가장 좋은 인테리어 소품이라는 말도 있으니 일석이조인 셈이다.
이제 조금 쓰기 귀찮아서 대충 시작한 LeNet-5 코드 부분을 살펴보기로 하자.
활성화 함수
이번에는 지난번에 LeNet-5에 대한 이론적인 내용을 살펴보았으므로, 이번에는 LeNet-5를 실제 코드로 구현하고 얼마나 classification을 잘 하는지 확인해 볼 것이다.
그런데, 그 전에 지난번에 까먹고 안 다뤘던 것이 하나 있다. 바로 activation function이다. 먼저 여기서 자주 보이는 sigmoid에 대해서 살펴보기로 하자. sigmoid의 식은 다음과 같다.
\[\sigma(X)=\frac{1}{1+e^{-X}}\]그리고 위 식에 대한 plot을 그려보면 아래와 같다. 이건 LaTeX로 그린 것이기에 이렇게 이쁘게 나오는 것이다. 이걸 책에 나온 것처럼 한번 python으로도 그려보기로 하자.
Plot은 항상 우리가 사용하는 matplotlib를 활용해서 그릴 수 있다. 아까 살펴본 Sigmoid의 식을 그대로 python에 옮겨서 코드를 실행해보면 아래와 같이 위에서 그린 plot과 거의 동일한 모양인 것을 확인할 수 있다. 하지만 아까의 plot과 같이 정말 교재에 나오는 것 같은 깔끔한 그림은 나오지 않는다. 그렇기에 번거롭지만 LaTeX로 plot을 그리게 되어버렸다. LaTeX 최고!
1
2
3
4
5
6
7
8
9
10
import numpy as np
import matplotlib.pyplot as plt
x = np.arange(-10, 10, 0.2)
plt.plot(x, 1 / (1 + np.exp(-x)))
plt.title("Sigmoid Function")
plt.xlabel("x")
plt.ylabel("f(x)")
plt.show()
그런데, 여기서 한 가지 궁금증이 생길 수 있다. 분명히 책에서는 함수 구현을 조금 다르게 한 거 같은데 그건 다른 것일까? 사실 동일한 것이다. 다른 점은 sigmoid를 라이브러리에서 미리 정의해서 편하게 사용할 수 있게 설정해놓은 것이다. 그런데, 약간 다른 점은 SciPy에서 구현을 할 때, 최적화를 진행하여 sigmoid를 활용할 때 조금이나마 더 빠른 결과물을 확인할 수 있게 된다는 것이다. 그런데 지금과 같은 경우에는 뭘 쓰던 크게 상관 없기는 하다.
1
2
3
4
5
6
7
8
9
10
11
import numpy as np
import matplotlib.pyplot as plt
from scipy.special import expit
x = np.arange(-10, 10, 0.2)
plt.plot(x, expit(x))
plt.title("Sigmoid Function")
plt.xlabel("x")
plt.ylabel("f(x)")
plt.show()
그리고 이번에는 최근 많이 사용하는 ReLU를 살펴보기로 하자. ReLU는 생각보다 간단한 함수로, 0 이하인 경우는 그냥 0으로 설정해버리고 1 이상이면 그냥 그 값을 출력하는 함수이다.
\[\rm{ReLU}\it(X)=\begin{cases}\rm{max}\it(0, X), X\geq0\\0, X< 0\end{cases}\]이것도 python을 통해서 매우 간단하게 구현할 수 있다. 하나는 위 수식을 그대로 구현하는 방식으로 numpy의 maximum()을 쓰는 것이다.
1
2
3
4
5
6
7
8
9
10
import numpy as np
import matplotlib.pyplot as plt
x = np.arange(-10, 10, 0.2)
plt.plot(x, np.maximum(0, x))
plt.title("ReLU Function")
plt.xlabel("x")
plt.ylabel("f(x)")
plt.show()
또 다른 방법은 numpy의 clip을 사용하는 방식이다. 둘 다 동일하게 numpy 라이브러리의 기능을 사용하는 것이고, 동일한 출력을 내므로 어떤 것을 사용해도 크게 상관은 없다. 그런데 수식을 조금 더 명확하게 보여주는 maximum()을 쓰는 것이 여기서는 조금 더 낫지 않을까 생각이 된다.
1
2
3
4
5
6
7
8
9
10
import numpy as np
import matplotlib.pyplot as plt
x = np.arange(-10, 10, 0.2)
plt.plot(x, np.clip(x, 0, None))
plt.title("ReLU Function")
plt.xlabel("x")
plt.ylabel("f(x)")
plt.show()
먼저, 교재에서 제시하고 있는 LeNet-5 model 구현 코드는 다음과 같다. 지금은 엄청 간단하게 sequential model을 구현하는 것이기에 layer를 하나하나 추가하는 방식으로 model을 구현하고 있다. 이러한 방식 말고도 여러 구현 방식이 있으나 지금은 약간 귀찮기에 그냥 여기서 하는 그대로 해보기로 했다.
1
2
3
4
5
6
7
8
9
10
11
12
13
import keras
from keras import layers, models
lenet5 = keras.Sequential()
lenet5.add(layers.Input(shape=(28, 28, 1)))
lenet5.add(layers.Conv2D(filters=6, kernel_size=5, activation='sigmoid', padding='same'))
lenet5.add(layers.AveragePooling2D(pool_size=2))
lenet5.add(layers.Conv2D(filters=16, kernel_size=5, activation='sigmoid'))
lenet5.add(layers.AveragePooling2D(pool_size=2))
lenet5.add(layers.Flatten())
lenet5.add(layers.Dense(120, activation='sigmoid'))
lenet5.add(layers.Dense(84, activation='sigmoid'))
lenet5.add(layers.Dense(10, activation='softmax'))
우선 논문에서는 activation function으로 sigmoid가 아니라 hyperbolic tangent를 사용했다고 했다. 이전의 sigmoid와 유사하지만 $\rm tanh(\it{X})$의 경우 출력의 평균이 0에 가깝다는 특징이 있다. 그리고 출력의 범위가 sigmoid보다 더 넓게 분포하고 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import tensorflow as tf
from tensorflow.keras import layers, models
def build_lenet_tf():
model = models.Sequential([
layers.Input(shape=(32, 32, 1)),
layers.Conv2D(6, kernel_size=5, activation='tanh', padding='same'),
layers.AveragePooling2D(pool_size=2, strides=2),
layers.Conv2D(16, kernel_size=5, activation='tanh'),
layers.AveragePooling2D(pool_size=2, strides=2),
layers.Flatten(),
layers.Dense(120, activation='tanh'),
layers.Dense(84, activation='tanh'),
layers.Dense(10, activation='softmax')
])
return model
model_tf = build_lenet_tf()
model_tf.summary()
사실 여기서 더 추가로 들어가보자면, 단순한 hyperbolic tangent가 아니라 scaled hyperbolic tangent를 사용했다고 언급하고 있다. 식은 다음과 같고 동일하게 파이썬으로도 구현할 수 있다. 그런데 조금 귀찮으니 나중에 대충 hyperbolic tangent만 사용해서 실험해보기로 하자.
\[f(a)=1.7159\cdot\tanh(Sa)\]
1
2
def scaled_tanh(x):
return 1.7159*tf.math.tanh(2*x/3)
분류 실험
교재 기본 코드
제일 먼저 교재에 나온 그대로 실험을 한 번 해보기로 하자. 아까 살펴본 것과 같이 $28\times28$ 크기의 이미지를 입력받고, activation function을 sigmoid로 한 것이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import keras
from keras import layers, models
lenet5 = keras.Sequential()
lenet5.add(layers.Input(shape=(28, 28, 1)))
lenet5.add(layers.Conv2D(filters=6, kernel_size=5, activation='sigmoid', padding='same'))
lenet5.add(layers.AveragePooling2D(pool_size=2))
lenet5.add(layers.Conv2D(filters=16, kernel_size=5, activation='sigmoid'))
lenet5.add(layers.AveragePooling2D(pool_size=2))
lenet5.add(layers.Flatten())
lenet5.add(layers.Dense(120, activation='sigmoid'))
lenet5.add(layers.Dense(84, activation='sigmoid'))
lenet5.add(layers.Dense(10, activation='softmax'))
lenet5.summary()
Model: "sequential" ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ ┃ Layer (type) ┃ Output Shape ┃ Param # ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ │ conv2d (Conv2D) │ (None, 28, 28, 6) │ 156 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ average_pooling2d │ (None, 14, 14, 6) │ 0 │ │ (AveragePooling2D) │ │ │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ conv2d_1 (Conv2D) │ (None, 10, 10, 16) │ 2,416 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ average_pooling2d_1 │ (None, 5, 5, 16) │ 0 │ │ (AveragePooling2D) │ │ │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ flatten (Flatten) │ (None, 400) │ 0 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ dense (Dense) │ (None, 120) │ 48,120 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ dense_1 (Dense) │ (None, 84) │ 10,164 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ dense_2 (Dense) │ (None, 10) │ 850 │ └─────────────────────────────────┴────────────────────────┴───────────────┘
다음으로 교재에 나온 것과 같이 fashion MNIST 데이터를 불러오고, 정규화를 진행해준 후 train data, validation data를 나눠준다. 이때, validation data는 20% 비율로 설정해준다.
1
2
3
4
5
6
from tensorflow.keras.datasets import fashion_mnist
from sklearn.model_selection import train_test_split
(X_train, y_train), (X_test, y_test) = fashion_mnist.load_data()
X_train = X_train.reshape(-1, 28, 28, 1)/255.0
X_train, X_val, y_train, y_val = train_test_split(X_train, y_train, test_size=0.2, random_state=42)
그리고 학습 과정에서의 hyper-parameter들을 설정해준다. 여기서는 교재와 조금 다르게 patience를 5로 설정해서 조금 더 오래 성능 하락이 있더라도 학습을 진행하도록 하였다.
1
2
3
checkpoint_cb = keras.callbacks.ModelCheckpoint('lenet5-model.keras', save_best_only=True)
early_stopping_cb = keras.callbacks.EarlyStopping(patience=5, restore_best_weights=True)
lenet5.compile(loss='sparse_categorical_crossentropy', metrics=['accuracy'])
그리고 학습을 진행해준다. 교재에서는 적은 epoch를 설정했으나 여기서는 조금 더 큰 50으로 설정해줬다.
1
hist = lenet5.fit(X_train, y_train, epochs=50, validation_data=(X_val, y_val), callbacks=[checkpoint_cb, early_stopping_cb])
Epoch 1/50
1500/1500 ━━━━━━━━━━━━━━━━━━━━ 4s 2ms/step - accuracy: 0.4151 - loss: 1.5564 - val_accuracy: 0.7107 - val_loss: 0.7286
Epoch 2/50
1500/1500 ━━━━━━━━━━━━━━━━━━━━ 3s 2ms/step - accuracy: 0.7351 - loss: 0.6802 - val_accuracy: 0.7424 - val_loss: 0.6459
Epoch 3/50
1500/1500 ━━━━━━━━━━━━━━━━━━━━ 3s 2ms/step - accuracy: 0.7676 - loss: 0.5988 - val_accuracy: 0.7872 - val_loss: 0.5440
Epoch 4/50
1500/1500 ━━━━━━━━━━━━━━━━━━━━ 3s 2ms/step - accuracy: 0.8007 - loss: 0.5252 - val_accuracy: 0.8128 - val_loss: 0.5106
Epoch 5/50
1500/1500 ━━━━━━━━━━━━━━━━━━━━ 3s 2ms/step - accuracy: 0.8223 - loss: 0.4787 - val_accuracy: 0.8207 - val_loss: 0.4758
Epoch 6/50
1500/1500 ━━━━━━━━━━━━━━━━━━━━ 3s 2ms/step - accuracy: 0.8340 - loss: 0.4497 - val_accuracy: 0.8295 - val_loss: 0.4479
Epoch 7/50
1500/1500 ━━━━━━━━━━━━━━━━━━━━ 3s 2ms/step - accuracy: 0.8417 - loss: 0.4263 - val_accuracy: 0.8413 - val_loss: 0.4281
Epoch 8/50
1500/1500 ━━━━━━━━━━━━━━━━━━━━ 3s 2ms/step - accuracy: 0.8504 - loss: 0.4057 - val_accuracy: 0.8562 - val_loss: 0.3962
Epoch 9/50
1500/1500 ━━━━━━━━━━━━━━━━━━━━ 3s 2ms/step - accuracy: 0.8543 - loss: 0.3907 - val_accuracy: 0.8547 - val_loss: 0.3933
Epoch 10/50
1500/1500 ━━━━━━━━━━━━━━━━━━━━ 3s 2ms/step - accuracy: 0.8606 - loss: 0.3720 - val_accuracy: 0.8617 - val_loss: 0.3743
Epoch 11/50
1500/1500 ━━━━━━━━━━━━━━━━━━━━ 3s 2ms/step - accuracy: 0.8666 - loss: 0.3584 - val_accuracy: 0.8549 - val_loss: 0.3877
Epoch 12/50
1500/1500 ━━━━━━━━━━━━━━━━━━━━ 3s 2ms/step - accuracy: 0.8698 - loss: 0.3468 - val_accuracy: 0.8578 - val_loss: 0.3716
Epoch 13/50
…
Epoch 43/50
1500/1500 ━━━━━━━━━━━━━━━━━━━━ 3s 2ms/step - accuracy: 0.9146 - loss: 0.2245 - val_accuracy: 0.8967 - val_loss: 0.3103
Epoch 44/50
1500/1500 ━━━━━━━━━━━━━━━━━━━━ 3s 2ms/step - accuracy: 0.9180 - loss: 0.2190 - val_accuracy: 0.9001 - val_loss: 0.3052
이렇게 해서 최종적으로 최적의 모델에 대한 train accuracy는 약 91%, validation accuracy는 90%로 overfitting의 우려는 크게 없어 보인다.
1
2
3
4
5
train_loss, train_acc = hist.history["loss"][-1], hist.history["accuracy"][-1]
val_loss, val_acc = hist.history["val_loss"][-1], hist.history["val_accuracy"][-1]
print(f"Train loss: {train_loss:.4f} | accuracy: {train_acc:.4%}")
print(f"Validation loss: {val_loss:.4f} | accuracy: {val_acc:.4%}")
Train loss: 0.2235 | accuracy: 91.6458% |
Validation loss: 0.3052 | accuracy: 90.0083% |
다음으로 우리가 가장 중요하게 생각하는 test data에 대한 값도 확인해보자.
1
2
test_loss, test_acc = lenet5.evaluate(X_test, y_test, verbose=0)
print(f"Test loss: {test_loss:.4f} | accuracy: {test_acc:.4%}")
Test loss: 0.5707 | accuracy: 80.4900% |
이전에 봤던 train data나 validation data에 대한 accuracy보다 좀 낮긴 하지만 80% 정도면 나쁘지 않다고 생각된다. 그런데 10%p 정도 차이는 조금 크다고 생각되는데 validation data의 비율을 조금 높이면 차이가 줄어들지 않을까 생각이 된다.
이제 이걸 plot을 그려서 어떻게 loss와 accuracy가 epoch에 따라서 변화했는지 살펴보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import matplotlib.pyplot as plt
epochs = range(1, len(hist.history["loss"])+1)
plt.figure(figsize=(10,4))
plt.subplot(1,2,1)
plt.plot(epochs, hist.history["loss"], label="Train")
plt.plot(epochs, hist.history["val_loss"], label="Val")
plt.title("Loss per Epoch")
plt.xlabel("Epoch")
plt.ylabel("Loss")
plt.legend()
plt.subplot(1,2,2)
plt.plot(epochs, hist.history["accuracy"], label="Train")
plt.plot(epochs, hist.history["val_accuracy"], label="Val")
plt.title("Accuracy per Epoch")
plt.xlabel("Epoch")
plt.ylabel("Accuracy")
plt.legend()
plt.tight_layout()
plt.show()
Plot을 보니, 우선 train loss는 계속해서 내려가고 train accuracy는 지속해서 올라가는 것을 확인할 수 있다. 이건 사실 당연한 것이다. 한정된 연습문제를 계속 반복해서 풀었기에 이미 풀어봤던 문제를 맞추게 되는 것은 당연하다. 하지만 validation loss나 accuracy는 조금 왔다갔다 하는 것을 확인할 수 있다. 하지만 전체적으로 봤을 때 train data에 대한 것과 유사하게 움직이는 것을 확인할 수 있다.
손실함수
categorical_crossentropy
이때, 앞에서 ‘sparse_categorical_crossentropy’라는 것을 확인할 수 있었다. 그런데 categorical_crossentropy라는 것도 확인해봤던 기억이 있을 것이다. 둘은 어떤 차이가 있을까? 그리고 언제 사용해야 할까?
우선, 그냥 단순한 categorical cross entropy는 다음과 같이 구할 수 있다. 이때, $C$는 클래수의 수, $y_i$는 one-hot 인코딩(0 or 1)된 정답, ${\hat{y}}_i$는 Softmax 활성화 함수로 예측된 확률이다.
\[Loss=-\sum_{i=1}^{C}{y_ilog{\left({\hat{y}}_i\right)}}\]이때, one-hot 인코딩된 정답 벡터는 정답에 해당하는 위치만 1로 설정하고 오답은 0으로 설정하기에 결과적으로 정답 클래스에 해당하는 예측의 확률만을 사용한다. 만약 정답 벡터 중 $t$번째 값이 정답이라고 할 때, 정답 벡터와 예측한 확률의 벡터가 다음과 같다면
\[\mathbf{y}=\left(0,0,\cdots,1,\cdots,0\right)\] \[\hat{\mathbf{y}}=\left({\hat{y}}_1,{\hat{y}}_2,\cdots,{\hat{y}}_t,\cdots,{\hat{y}}_C\right)\]손실함수는 아래와 같이 구할 수 있다.
\[Loss=-\sum_{i=1}^{C}{y_ilog{\left({\hat{y}}_i\right)}}=-\left[0log\left({\hat{y}}_1\right)+0log\left({\hat{y}}_2\right)+\cdots+1log\left({\hat{y}}_t\right)+\cdots+0log\left({\hat{y}}_C\right)\right]=-log\left({\hat{y}}_t\right)\]따라서 결과적으로는 하나의 $-\log{\left({\hat{y}}_i\right)}$값만을 사용하게 되며, $\log{\left({\hat{y}}_i\right)}$값은 아래의 그래프와 같이 확률이 정답에 가까워질수록, 즉 ${\hat{y}}_i$값이 커질수록 커진다.
이때, 손실이 감소하는 쪽으로 학습이 진행될 수 있도록 (-1)을 곱하여 아래와 같이 확률이 정답에 가까워질수록 작아지게 설정하게 된다.
sparse_categorical_crossentropy
이제 여기서 사용한 sparse_categorical_crossentropy를 살펴보도록 하자. Sparse categorical cross-entropy는 이전 식보다 간단한다. $y$는 정답 클래스의 인덱스, ${\hat{y}}_y$는 Softmax 활성화 함수로 예측된 정답 클래스의 확률라고 할 때, 식은 다음과 같다.
\[Loss=-log{\left({\hat{y}}_y\right)}\]이때, sparse categorical cross-entropy는 정답 클래스의 인덱스로 categorical cross-entropy로 동일하게 정답 클래스에 해당하는 예측의 확률만을 사용한다. 정답 벡터와 예측한 확률의 벡터가 다음과 같다면,
\[y=t\] \[\hat{\mathbf{y}}=\left({\hat{y}}_1,{\hat{y}}_2,\cdots,{\hat{y}}_t,\cdots,{\hat{y}}_C\right)\]활성화 함수로 예측된 정답 클래스의 확률은 ${\hat{y}}_y={\hat{y}}_t$이고, 손실은 categorical cross-entropy와 동일하게 아래와 같이 구할 수 있다.
\[Loss=-log{\left({\hat{y}}_y\right)}=-log{\left({\hat{y}}_t\right)}\]결과적으로 categorical cross-entropy와 sparse categorical cross-entropy가 구하게 되는 것은 동일하다. 다만, 입력을 할 때, categorical cross-entropy는 정답 클래스가 one-hot 인코딩된 vector 형태이고 정답 클래스가 정수인 정답 인덱스로 제공된다면 sparse categorical cross-entropy를 활용하여 계산을 하게 되는 것이다.
조금 크고 아름다운 Epoch 모델
그런데 이렇게 했을 때 epoch를 크게 하면 더 좋은 것이 아닐까?라는 생각이 들 수도 있다. 만약 이 생각이 든다면 혼자 공부하는 머신러닝+딥러닝부터 보고 와야 한다. 그래도 여기서 약간 친절하게 설명을 해주자면 validation data에 대한 loss는 U-shape의 plot을 그리게 된다. 지나치게 train data에 적응을 하다보면 너무 train data에 맞게 모델이 적합이 되어 실제 test data나 validation data에서 문제를 오히려 못 맞추게 되는 것이다.
실제로 Epoch를 300으로 두고, patience를 없앤 상태에서 동일한 모델을 학습할 경우 아래와 같은 결과가 나온다.
Train loss: 0.0546 | accuracy: 98.0500% |
Validation loss: 0.8181 | accuracy: 88.3000% |
Test loss: 0.6285 | accuracy: 79.9200% |
Train data에 대해서는 거의 일부를 제외하고는 정답을 맞추는 것을 확인할 수 있으나 validation data에 대해서는 아까보다 accuracy가 낮아졌다. 그리고 test data에 대해서도 어느정도 낮아지는 것을 확인할 수 있다.
따라서 단순히 Epoch를 늘리는 것보다는 validation score를 확인해 U-shape의 loss plot에서 가장 낮은, global minimum을 찾아내는 것이 좋은 기준일 것이다.
$32\times32$ Zero padding 모델
그런데 여기까지만 하면 조금 아쉬울 것이다. 이전에 LeNet-5 모델을 다룰 때, feature를 더 잘 뽑아내기 위해서 테두리에 padding을 했다는 것을 언급한 적이 있을 것이다. 그런데 그게 실제로 성능에 유의미한 향상을 야기하는지는 확인해보지 않았다. 따라서 여기서 동일한 조건 하에 padding을 한 후 학습을 하면 어떤 결과가 나올지 확인해보자. 물론 LeNet-5 모델을 제안한 논문에서는 그냥 MNIST를 사용했기에 지금과는 상황이 매우 다르지만, 그냥 한 번 시도해봤다.
우선, 모델은 아래와 같이 설정해준다. 기존 모델에 패딩을 상하좌우에 2씩 추가하는 레이어를 추가해줬다.
1
2
3
4
5
6
7
8
9
10
11
12
lenet5 = keras.Sequential([
layers.Input(shape=(28, 28, 1)),
layers.ZeroPadding2D(padding=2),
layers.Conv2D(6, 5, activation='sigmoid', padding='valid'),
layers.AveragePooling2D(2),
layers.Conv2D(16, 5, activation='sigmoid'),
layers.AveragePooling2D(2),
layers.Flatten(),
layers.Dense(120, activation='sigmoid'),
layers.Dense(84, activation='sigmoid'),
layers.Dense(10, activation='softmax')
])
결과는 예상 외로 test data에서 성능이 올라간 것을 확인할 수 있었다. 솔직히 차이가 거의 없을 것이라 생각했는데 성능이 조금이나마 올라간 것을 확인할 수 있었다.
Train loss: 0.2461 | accuracy: 90.8354% |
Validation loss: 0.3246 | accuracy: 88.6250% |
Test loss: 0.4598 | accuracy: 83.5400% |
tanh 모델
다음으로는, 논문에서 사용했던 hyperbolic tangent를 사용해서 결과를 살펴볼 것이다.
1
2
3
4
5
6
7
8
9
10
11
12
lenet5 = keras.Sequential([
layers.Input(shape=(28, 28, 1)),
layers.ZeroPadding2D(padding=2),
layers.Conv2D(6, 5, activation='tanh', padding='valid'),
layers.AveragePooling2D(2),
layers.Conv2D(16, 5, activation='tanh'),
layers.AveragePooling2D(2),
layers.Flatten(),
layers.Dense(120, activation='tanh'),
layers.Dense(84, activation='tanh'),
layers.Dense(10, activation='softmax')
])
결과를 봤더니 hyperbolic tanget를 사용했을 때 더 성능이 높은 것을 확인할 수 있다. hyperbolic tanget는 sigmoid보다 가능한 범위가 넓어 역전파시 gradient가 덜 사라지기 때문에 더 높은 최적화 수준을 달성할 수 있다. 따라서 은닉층에서는 sigmoid보다는 더 범위가 넓은 hyperbolic tanget와 같은 activation function을 활용하는 것이 더 적절하다.
Train loss: 0.2003 | accuracy: 92.6229% |
Validation loss: 0.3270 | accuracy: 88.9250% |
Test loss: 0.4542 | accuracy: 83.9500% |