XOR 문제 구현
# XOR_NN_TF2.ipynb
import tensorflow as tf
import numpy as np
x = np.array([[0,0],[0,1],[1,0],[1,1]]).astype('float32')
y = np.array([[0],[1],[1],[0]]).astype('float32')
from tensorflow.keras import layers
model = tf.keras.Sequential()
model.add(layers.Dense(3, activation='sigmoid', input_dim=2))
model.add(layers.Dense(2, activation='sigmoid')) #추가된 부분!
model.add(layers.Dense(1, activation='sigmoid'))
sgd = tf.keras.optimizers.SGD(learning_rate=0.1)
# 결과적으로 노드의 갯수가 레이어를 지남에 따라 2개 -> 3개 -> 2개 -> 1개가 되는 형태
# model.compile(optimizer='sgd', loss='binary_crossentropy',metrics=['accuracy'])
model.compile(optimizer=sgd, loss='binary_crossentropy',metrics=['accuracy'])
model.fit(x, y, epochs=10000, batch_size=4, verbose=0)
model.evaluate(x, y)
predicted = model.predict(x)
print(predicted)
>>>
[[0.05775115]
[0.9573945 ]
[0.9088514 ]
[0.04816197]]
위 코드는 XOR 문제를 구현한 코드이다. 주목할 점은 model.add()를 이용해 히든 레이어를 추가했다는 점과, 학습률 지정을 위해 sgd를 직접 작성하여 사용했다는 점이다.
Gradient Vanishing Problem
import tensorflow as tf
import numpy as np
x = np.array([[0,0],[0,1],[1,0],[1,1]]).astype('float32')
y = np.array([[0],[1],[1],[0]]).astype('float32')
from tensorflow.keras import layers
actFunc = 'sigmoid'
model = tf.keras.Sequential()
model.add(layers.Dense(10, activation=actFunc, input_dim=2))
model.add(layers.Dense(10, activation=actFunc))
model.add(layers.Dense(10, activation=actFunc))
model.add(layers.Dense(10, activation=actFunc))
model.add(layers.Dense(10, activation=actFunc))
model.add(layers.Dense(10, activation=actFunc))
model.add(layers.Dense(10, activation=actFunc))
model.add(layers.Dense(1, activation='sigmoid'))
sgd = tf.keras.optimizers.SGD(learning_rate=0.1)
model.compile(optimizer=sgd, loss='binary_crossentropy',metrics=['accuracy'])
model.fit(x, y, epochs=10000, batch_size=4, verbose=0)
model.evaluate(x, y)
predicted = model.predict(x)
print(predicted)
>>>
[[0.499997 ]
[0.50000376]
[0.49999616]
[0.50000316]]
위 코드는 XOR 문제를 딥러닝으로 확장한 코드이다. 결과를 살펴보면 전혀 학습되지 않았음을 알 수 있다. 이러한 문제를 Gradient Vanishing Problem라고 한다.
이러한 문제가 발생한 이유는 출력값이 0과 1 사이의 범위에 위치하도록 사용한 활성화 함수인 Sigmoid Function 때문이다. Sigmoid Function을 사용하면 Backpropagation 계산 중간에 Sigmoid Function의 미분값을 계산하는 과정에서 그 값이 0이 되어버리게 된다. 이로 인해 가중값이 업데이트 되지 않는 문제가 발생한다.
이 문제를 해결하기 위해서는 활성화 함수를 Sigmoid가 아닌 ReLU로 변경하면 된다. 참고로, ReLU 함수의 음수 부분이 꺾여 있는 이유는 선형 함수가 되는 것을 방지하기 위함이라고 한다.
중요한 점은, 중간 레이어의 활성화 함수는 ReLU로 변경해도 되지만 출력 레이어에서는 활성화 함수로 Sigmoid Function을 사용해야 한다. 출력값을 0과 1사이의 확률값으로 나타내기 위함이라고 한다.
Xavier Initialization
신경망 모형에서는 초기값에 따라 결과 및 수렴 속도가 달라진다. Xavier 초기값은 보다 빠른 수렴을 위해 고안된 초기값이다. 가중치의 분산을 일정 수준 이하로 만들면 더 빠른 수렴을 보인다는 연구를 바탕으로 고안되었다고 한다. 이를 코드로 나타내면 다음과 같다.
W=np.random.randn(fan_in, fan_out)/np.sqrt(fan_in) # Xavier initialization(2010)
W=np.random.randn(fan_in, fan_out)/np.sqrt(fan_in/2) #He initialization(2015)
위 코드에서 np.random.randn()은 표준정규분포의 난수를 의미하고, fan_in은 인풋 노드의 수, fan_out은 아웃풋 노드의 수를 의미한다.
Drop Out
Drop Out은 레이어 마다 어떤 노드를 무시할 것인지를 랜덤으로 결정하는 것을 말한다. 이를 코드로 구현하면 다음과 같다.
layers.Dropout(0.3)
위와 같이 코드를 작성하면, 계산 시 다음 레이어의 30%는 무시한다는 의미가 된다. 이러한 과정은 필요 없는 노드를 무시하고, 핵심적인 노드만 살리고자 할 때 사용된다. 모든 노드를 살리게 되면 오히려 Overfitting될 위험이 있기 때문에 Drop Out 과정이 필요하다.
Batch Normalization
앞서 언급했듯이 딥러닝에서는 가중값을 구하기 위해 초기 가중값을 난수로 지정한다. 이때, 초기 난수에 따라 수렴 속도가 달라진다. 만약에 초기 난수가 적절하기 않게 지정되더라도, 안정적으로 최적값을 찾을 수 있도록 돕기 위해서는 배치 정규화가 필요하다.
$$ BN(x_i)=\gamma\cdot\frac{x_i-\mu_B}{\sqrt{\sigma^2_B+\epsilon}}+\beta $$
배치 정규화는 딥러닝 중간 레이어의 출력을 정규화하는 아이디어로, 위 식을 통해 이전 레이어의 출력을 정규화하여 학습한다. 이로써 오차가 적어지는 방향으로 값이 갱신되어 최적값을 찾아가게 된다.
model.add(tf.keras.layers.BatchNormalization())
배치 정규화를 코드로 구현하면 위와 같다.
Optimizer
관점 | 종류 |
모멘텀의 관점 | - Momentum - Nesterov Momentum |
학습률 개선의 관점 | - AdaGrad - RMSPropt - AdamOptimizer |
Optimizer는 학습 과정에서 사용되는 알고리즘으로, 모델 파라미터를 업데이트하는 방법을 결정한다. 즉, 학습 중에 모델이 얼마나 빠르고 효과적으로 수렴할지를 결정하는 요소 중에 하나이다. 이러한 Optimizer를 모멘텀과 학습률 개선의 관점에서 살펴보면 위와 같이 구분할 수 있다.
먼저 모멘텀의 관점에서 살펴보겠다.
$$ w := w - \lambda\frac{\partial Cost(w)}{\partial w} $$
Gradient Descent Optimizer에 대한 연구가 진행된 결과 여러 Optimizer가 개발되었다. 일반적으로 쓰였던 Gradient Descent를 수식으로 나타내면 위와 같다. 뒤에 작성할 Optimizer와 비교하기 위해 써놓겠다.
Momentum
$$ m := \alpha \cdot m + \varepsilon \frac{\partial Cost(w)}{\partial w} $$
모멘텀 방식은 마치 볼링공이 굴러가듯이 이전 속도가 현재 속도에 영향을 미치는 구조이다. 위 식을 살펴보면 $\alpha$가 곱해져 있다는 특징을 확인할 수 있다.
$m$은 모멘텀을 의미하고, $\alpha$는 0과 1 사이의 값으로, 그 값이 클 수록 이전 모멘트를 많이 반영함을 의미한다. 이러한 모멘텀 방식은 일반적으로 기본적인 최대경사법보다 성능이 좋다고 알려져있다.
tf.keras.optimizers.SGD(lr=0.01, decay=1e-6, momentum=0.9)
모멘텀을 코드로 구현하면 위와 같다.
참고로 decay는 lr(학습률)의 감소율을 의미한다. 최적값으로 수렴하기 위한 장치이다.
Nesterov Momentum
네스테로프 모멘텀은 기존 모멘텀 방식을 변형한 것이다. 수렴 속도가 더 빠르다는 특징이 있다. 이를 수식으로 나타내면 다음과 같다.
$$ m := \beta m + \lambda \frac{\partial Cost(W-\beta m)}{\partial W}, \; W := W - m $$
이를 코드로 나타내면 다음과 같다.
tf.keras.optimizers.SGD(lr=0.01, decay=1e-6, momentum=0.9, nesterov=True)
다음으로 Optimizer를 학습률 개선의 관점에서 살펴보겠다.
$$ w_1 = w_0 - \lambda \frac{\partial Cost(w)}{\partial w} $$
if L(w1) < L(w0):
w0 = w1
else:
lr = lr / 2
기본적인 학습률 개선의 과정은 위와 같다. 이전 파라미터의 손실값과 갱신된 파라미터의 손실값을 비교하여, 손실값이 작아지면 정상적으로 파라미터를 갱신하고, 그렇지 않다면 파라미터를 갱신하지 않고 학습률을 반으로 줄인다.
AdaGrad
Gradient의 제곱의 누적값 h를 파라미터마다 계산하여 벡터 형태로 저장하고, 학습률을 h의 제곱근으로 나누어 학습률을 조금씩 작게 만든다. 결국 Gradient의 제곱의 크기가 클 수록 학습률이 작게 된다.
이 방식의 장점은 각 파라미터가 최적해로 가는 속도를 조절하기 좋은 아이디어라는 점이다. 단점은 조기 종료하는 경향이 있어서 실질적으로는 사용하지 않는 방식이라는 점이다.
RSMPropt
tf.keras.optimizers.RMSprop(learning_rate=1e-3)
이 방식은 AdaGrad의 단점을 보완하기 위해 고안된 방법이다. 변화량의 제곱합을 누적하지 않고 지수 평균으로 구한다고 한다.
이로써 최근 변화향이 중요하게 반영되어 시계열에서 더 정확한 의미를 가진다고 한다.
AdamOptimizer
tf.keras.optimizers.Adam(learning_rate=0.001, beta_1=0.9, beta_2=0.999)
이 방식은 Momentum과 RMSPropt를 합친 개념으로, 가장 많이 사용되는 방식이다. 이 방식에서 Moment의 효과는 두 가지가 있다. 첫 째로는 최적경로로 가는 경로의 진동을 방지한다는 점, 둘 째로는 파라미터 벡터가 해로 가는 속도를 비슷하게 맞춤으로 빠르게 해에 수렴하게 한다는 점이다.
DNN을 이용한 Mnist 분류
코드 보기
# Mnist_DNN_Model_Save_TF2.ipynb
#Hidden Layer 추가
import tensorflow as tf
import numpy as np
from tensorflow.keras import datasets
from tensorflow.keras.utils import to_categorical
mnist = datasets.mnist
(train_x, train_y), (test_x, test_y) = mnist.load_data()
train_x = train_x.reshape(-1,784)
test_x = test_x.reshape(-1,784)
train_x = train_x / 255
test_x = test_x / 255
train_y_onehot = to_categorical(train_y)
test_y_onehot = to_categorical(test_y)
from tensorflow.keras import layers
model = tf.keras.Sequential()
# # 원래 쓰던 코드
# model.add(layers.Dense(256, activation='relu', input_dim=784))
# model.add(layers.Dense(256, activation='relu'))
# model.add(layers.Dense(10, activation='softmax'))
# # 초기값을 He가 제안해준대로 사용한 코드
# model.add(layers.Dense(256, activation='relu', input_dim=784, kernel_initializer='he_normal'))
# model.add(layers.Dense(256, activation='relu', kernel_initializer='he_normal'))
# model.add(layers.Dense(10, activation='softmax', kernel_initializer='he_normal'))
# 드롭 아웃을 추가한 코드
model.add(layers.Dense(256, activation='relu', kernel_initializer='he_normal', input_dim=784))
model.add(layers.Dropout(0.3))
model.add(layers.Dense(256, activation='relu', kernel_initializer='he_normal'))
model.add(layers.Dropout(0.3))
model.add(layers.Dense(10, activation='softmax'))
# 'sgd' 사용하는 코드
# model.compile(optimizer='sgd', loss='categorical_crossentropy',metrics=['accuracy'])
# 'adam' 사용하는 코드
model.compile(optimizer='adam', loss='categorical_crossentropy',metrics=['accuracy'])
model.fit(train_x, train_y_onehot, batch_size = 100, epochs=5)
>>>
#sgd사용, 드롭아웃 사용 X
Epoch 1/5
600/600 [==============================] - 5s 8ms/step - loss: 1.0885 - accuracy: 0.7371
Epoch 2/5
600/600 [==============================] - 4s 7ms/step - loss: 0.4280 - accuracy: 0.8848
Epoch 3/5
600/600 [==============================] - 4s 6ms/step - loss: 0.3449 - accuracy: 0.9031
Epoch 4/5
600/600 [==============================] - 4s 7ms/step - loss: 0.3062 - accuracy: 0.9132
Epoch 5/5
600/600 [==============================] - 5s 8ms/step - loss: 0.2804 - accuracy: 0.9203
#adam아용, 드롭아웃 사용 X
Epoch 1/5
600/600 [==============================] - 6s 8ms/step - loss: 0.2448 - accuracy: 0.9289
Epoch 2/5
600/600 [==============================] - 6s 10ms/step - loss: 0.0917 - accuracy: 0.9718
Epoch 3/5
600/600 [==============================] - 5s 8ms/step - loss: 0.0607 - accuracy: 0.9814
Epoch 4/5
600/600 [==============================] - 6s 10ms/step - loss: 0.0420 - accuracy: 0.9868
Epoch 5/5
600/600 [==============================] - 5s 8ms/step - loss: 0.0332 - accuracy: 0.9897
#adam사용, 드롭아웃 사용
Epoch 1/5
600/600 [==============================] - 7s 10ms/step - loss: 0.3433 - accuracy: 0.8963
Epoch 2/5
600/600 [==============================] - 7s 11ms/step - loss: 0.1481 - accuracy: 0.9551
Epoch 3/5
600/600 [==============================] - 5s 9ms/step - loss: 0.1098 - accuracy: 0.9667
Epoch 4/5
600/600 [==============================] - 7s 11ms/step - loss: 0.0920 - accuracy: 0.9713
Epoch 5/5
600/600 [==============================] - 5s 9ms/step - loss: 0.0795 - accuracy: 0.9748
만약 Loss 값이 커지면, optimizer를 adam로 설정하거나, 학습률을 줄이거나, Batch Size를 높인다.
원해 Logistic Regression을 통해서는 88%의 정확도를 가졌는데, 딥러닝을 사용하니 92%의 정확도를 갖게 되었다.
기타 학습할 사항
✅ 기계학습 과정은 Forward, Backward로 나눌 수 있으며, Forward는 입력 레이어부터 출력 레이어로 향하는 방향으로 학습하는 것을 의미하고, Backward는 반대로 출력 레이어부터 입력 레이어로 향하는 방향으로 학습하는 것을 의미한다.
✅ 학습이란, Forward와 Backward를 수행하여 weights와 bias를 계산하는 것이다.
✅ 예측이란, Cost Function을 최소화하는 W와 b를 이용해 Forward 계산을 수행하는 것이다.
✅ 학습 데이터 저장 및 불러오는 방법은 다음과 같다.
#save to disk
model.save('file_name.h5')
#load file
model = tf.keras.models.load_model('mnistDNN_weights.h5')