목차 만들기!
생물학적 뉴런에서 착안한 단순한 신경망 모델: 입력이 2개 이상이면 활성화(1), 그렇지 않으면 비활성화(0)
퍼셉트론(perceptron): TLU/LTU(threshold logic unit)이라고도 불리며, 입력의 가중치 합을 계산한 후 계단 함수(heaviside function == step function)을 적용하는 인공 뉴런
$$heaviside\left(z\right)=\begin{cases}0,\ \ \ z<0\\1,\ \ \ z\ge 0\end{cases}$$
한 층에 있는 모든 뉴런이 이전 층의 모든 뉴런과 연결되어 있을 때 이를 완전 연결 층(fully connected layer) 또는 밀집 층(Dense layer)이라고 부릅니다. 퍼셉트론은 층이 하나 뿐인 TLU로 구성되는데, 각 TLU는 모든 입력에 연결되어 있습니다.
$$h_{W,\ b}\left(X\right)=\Phi \left(XW+b\right)$$
X는 입력 특성 행렬입니다. 행은 샘플, 열은 특성입니다. 가중치 행렬 W은 (입력 뉴런 수, 출력층 뉴런 수)의 형태를 가집니다. 편향 벡터 b는 인공 뉴런마다 하나의 편향값이 있습니다. Φ는 활성화 함수입니다. TLU에서는 step function입니다.
퍼셉트론은 오차가 감소되도록 연결을 강화시킵니다. 오차가 큰 모든 출력 뉴런에 대해 올바른 예측을 하도록 입력에 연결된 가중치를 강화시킵니다.
$$w_{i,j}^{\left(next\ step\right)}=w_{i,\ j}+\eta \left(y_j-\hat{y_i}\right)x_i$$
- w_i,j는 i번째 입력 뉴런과 j번째 출력 뉴런 사이를 연결하는 가중치
- x_i는 현재 훈련 샘플의 i번째 뉴런의 입력값
- y_j_hat은 현재 훈련 샘플의 j번째 출력 뉴런의 출력값
- y_j는 현재 훈련 샘플의 j번째 출력 뉴런의 타깃값
- η는 학습률
sklearn.Perceptron
사이킷런은 하나의 TLU 네트워크를 구현한 Perceptron 클래스를 제공합니다. 퍼셉트론 학습 알고리즘은 확률적 경사 하강법과 매우 닮았습니다. Perceptron 클래스는 클래스 확률을 제공하지 않고, 고정된 임곗값을 기준으로 예측을 만듭니다.
import numpy as np
from sklearn.datasets import load_iris
from sklearn.linear_model import Perceptron
iris = load_iris()
X = iris.data[:, (2, 3)] # 꽃잎의 길이와 너비
y = (iris.target==0).astype(np.int) # Setosa인가?
per_clf = Perceptron()
per_clf.fit(X, y)
y_pred = per_clf.predict([[2, 0.5]])
print(y_pred)
'''
[0]
'''
퍼셉트론을 포함한 선형 분류기(ex. 로지스틱 회귀)는 XOR 분류 문제를 풀 수 없는데, 퍼셉트론을 여러 개 쌓아올리면 이러한 제약을 줄일 수 있다는 사실이 밝혀졌습니다. 이런 인공 신경망을 다층 퍼셉트론(MLP)이라 합니다. 실제로 아래 그림의 네트워크는 XOR 문제를 풀 수 있습니다. (0, 0), (1, 1)일 때 0을 출력하고, (0, 1), (1, 0)일 때 1을 출력합니다.
다층 퍼셉트론과 역전파
다층 퍼셉트론은 입력층(input layer) 한 개, 은닉층(hidden layer) 한 개 이상, 출력층(output layer) 한 개로 구성됩니다. 출력층을 제외하고 모든 층은 편향 뉴런을 포함하며, 다음 층과 완전히 연결되어 있습니다. 신호가 입력에서 출력으로 한 방향으로만 흐릅니다. 이러한 구조는 피드포워드 신경망(Feedforward Neural Network)에 속합니다.
자동으로 그레디언트를 계산하는 것을 자동 미분(autodiff)이라고 부르는데, 역전파에서 사용하는 기법은 후진 모드 자동 미분(reverse-mode autodiff)입니다. 한 번에 (예를 들어, 32개의 샘플이 포함된) 하나의 미니배치씩 진행하여 전체 훈련 세트를 처리합니다. 모든 훈련 세트 처리를 여러 번 반복하는데, 이 반복을 epoch이라고 합니다. 각 미니배치는 네트워크 입력층으로 전달되어 첫 번째 은닉층으로 보내집니다. 은닉층에서 출력층으로 출력을 계산합니다. 이것이 정방향 계산(forward pass)입니다.
그 다음 손실 함수를 사용해 오차 측정 값을 측정합니다. 이제 각 출력 연결이 이 오차에 기여하는 정도를 계산하는데, 미적분에서 연쇄 법칙을 적용하면 이 단계를 빠르고 정확하게 수행할 수 있습니다. 연쇄 법칙을 사용해 이전 층의 연결 가중치가 오차의 기여 정도에 얼마나 기여했는지 측정하는데, 이렇게 입력층에 도달할 때까지 역방향으로 계속됩니다.
마지막으로 경사 하강법을 수행해 방금 계산한 오차 그레디언트를 사용해 네트워크에 있는 모든 연결 가중치를 수정합니다. 은닉층의 연결 가중치를 랜덤하게 초기화하는 것이 중요한데, 예를 들어 모든 가중치와 편향을 0으로 초기화하면 층의 모든 뉴런이 같아져 하나의 뉴런처럼 동작할 것입니다. 대신 가중치를 랜덤하게 초기화하면 대칭성이 깨져서 역전파가 전체 뉴런을 다양하게 훈련할 수 있습니다.
이 역전파 알고리즘을 잘 작동시키고자 논문 저자들은 계단 함수를 로지스틱(시그모이드) 함수로 바꾸었습니다. 계단 함수는 수평선 밖에 없어 계산할 그레디언트가 없기 때문입니다. 이 알고리즘은 다른 활성화 함수와도 사용할 수 있습니다.
- 쌍곡 탄젠트 함수: S자 모양, 출력 범위(-1, 1)인데, 이 범위는 훈련 초기에 각 층의 출력을 원점 근처로 모으는 경향이 있어 빠르게 수렴되도록 도와줍니다.
- ReLU 함수: z=0에서 미분 가능하지 않지만, z<0일 때 그레디언트로 0을 반환하고, z>1일 때 입력값을 그대로 반환합니다. 잘 작동하고 계산 속도가 빠르다는 장점이 있어 기본 활성화 함수가 되었습니다. 출력에 최댓값이 없어 경사 하강법의 일부 문제를 완화해줍니다.
활성화 함수가 필요한 이유는 선형 변환을 여러 개 연결해도 얻는 것은 선형 변환 뿐입니다. f(x)=2x+3, g(x)=5x-1을 연결하면, f(g(x))=10x+1로 선형 함수가 됩니다. 따라서 층 사이에 비선형성을 추가하지 않으면 아무리 층을 많이 쌓아도 하나의 층과 동일해집니다. 이론상 비선형 활성화 함수가 충분히 있는 심층 신경망은 어떤 연속 함수도 근사할 수 있습니다.
회귀를 위한 다층 퍼셉트론
값 하나를 예측하는데 출력 뉴런이 하나만 필요합니다. 다변량 회귀(multivariate regression)에서는 (즉, 동시에 여러 값을 예측하는 경우) 출력 차원마다 출력 뉴런이 하나씩 필요합니다. 일반적으로 회귀용 다층 퍼셉트론의 출력 뉴런에는 활성화 함수를 사용하지 않고 어떤 범위의 값도 출력되도록 합니다.
상황에 따라 출력이 항상 양수여야 한다면 출력층에 ReLU 활성화 함수, softplus 활성화 함수를 사용할 수 있습니다. 어떤 범위 안의 값을 예측하고 싶다면, 로지스틱 함수나 하이퍼볼릭 탄젠트 함수를 사용할 수 있습니다.
$$softplus\left(z\right)=\log \left(1+e^z\right)$$
훈련시 사용하는 손실 함수는 전형적으로 MSE입니다. 하지만, 훈련 세트에 이상치가 많다면 MAE를 사용할 수 있습니다. 또는 이 둘을 조합한 후버(Huber) 손실을 사용할 수 있습니다. 후버 손실은 오차가 임곗값(보통 1)보다 작을 때는 이차함수, 클 때는 선형 함수입니다. 선형 함수 부분은 MSE보다 이상치에 덜 민감하고, 이차 함수 부분은 MAE보다 빠르고 정확하게 수렴하도록 도와줍니다.
회귀 MLP의 전형적인 구조
하이퍼파라미터 | 일반적인 값 |
입력 뉴런 수 | 특성마다 하나(ex. MNIST의 경우 28x28=784) |
은닉층 수 | 문제에 따라 다름. 일반적으로 1~5 |
은닉층의 뉴런 수 | 문제에 따라 다름. 일반적으로 10~100 |
출력 뉴런 수 | 예측 차원마다 하나 |
은닉층의 활성화 함수 | ReLU(or SELU) |
출력층의 활성화 함수 | 없음, or (출력이 양수일 때 ReLU/softplus) or (출력이 특점 범위일 때 logstic/tanh) |
손실 함수 | MSE or (이상치가 있다면 MAE/Huber) |
분류를 위한 다층 퍼셉트론
이진 분류 문제에서는 로지스틱 활성화 함수를 가진 하나의 출력 뉴런만 필요합니다. 출력은 0~1 사이의 실수인데, 이를 양성 클래스에 대한 예측 확률로 해석할 수 있습니다.
다중 레이블 이진 분류: 스팸 메일 여부와 긴급 메일인지 여부를 예측하는 이메일 분류 시스템이 있다고 해봅시다. 이 경우 로지스틱 활성화 함수를 가진 두 출력 뉴런이 필요합니다. 각 뉴런은 스팸 메일 양성 확률, 긴급 메일 양성 확률을 출력합니다.
다중 분류(multiclass classification)는 각 샘플이 3개 이상의 클래스 중 한 클래스에만 속해야할 때 (클래스가 서로 배타적일 때)필요합니다. 각 클래스마다 하나의 출력 뉴런이 필요합니다. 출력 층에는 소프트맥스 활성화 함수를 사용해야 하빈다. 소프트맥스 함수는 모든 예측 확률을 0과 1사이로 만들고 더했을 때 1이 되도록 만듭니다.
확률 분포를 예측해야하므로 손실 함수에는 일반적으로 크로스 엔트로피 손실(cross-entropy loss) 또는 로그 손실(log loss)를 선택하는 것이 좋습니다.
분류 MLP의 전형적인 구조
하이퍼파라미터 | 이진 분류 | 다중 레이블 분류 | 다중 분류 |
입력층과 은닉층 | 회귀와 동일 | 회귀와 동일 | 회귀와 동일 |
출력 뉴런 수 | 1개 | 레이블 마다 1개 | 클래스마다 1개 |
출력층의 활성화 함수 | 로지스틱 함수 | 로지스틱 함수 | 소프트맥스 함수 |
손실 함수 | 크로스 엔트로피 | 크로스 엔트로피 | 크로스 엔트로피 |
케라스로 다층 퍼셉트론 구현하기
케라스는 모든 종류의 신경망을 만들 수 있는 고수준 딥러닝 API입니다. 케라스의 참조 구현운 계산 백엔드에 의존하여 신경망에 필요한 많은 연산을 수행합니다. 인기 있는 세 가지 딥러닝 라이브러리 중에서 벡엔드를 선택할 수 있습니다. (텐서플로, CNTK, theano)
MXNet, Core ML, 자바스크립트나 타입스크립트, PlaidML에서 케라스를 실행할 수 있습니다. 텐서플로에서 자체적인 케라스 구현인 tf.keras를 번들로 포함시켰습니다. 이 구현은 벡엔드로 텐서플로만 지원합니다.
텐서플로, 케라스와 같이 인기 있는 딥러닝 라이브러리에는 파이토치가 있습니다. 이 API도 keras와 매우 비슷합니다.
버전확인
import tensorflow as tf
from tensorflow import keras
print(tf.__version__)
print(keras.__version__)
'''
2.7.0
2.7.0
'''
Sequential API를 사용하여 이미지 분류기 만들기
먼저 데이터 셋을 적재해야합니다. 패션 MNIST(10개 클래스, 28x28 픽셀의 흑백 이미지 70000개)를 사용하겠습니다.
케라스를 사용하여 데이터셋 적재하기
import tensorflow as tf
from tensorflow import keras
fashion_mnist = keras.datasets.fashion_mnist
(X_train_full, y_train_full), (X_test, y_test) = fashion_mnist.load_data()
keras는 MNIST, 패션 MNIST, 캘리포니아 주택 데이터셋 등 널리 사용되는 데이터셋을 다운하고 적재할 수 있는 유틸리티 함수를 제공합니다.
패션 MNIST는 28x28 크기의 배열에 픽셀 강도가 실수 0.0~255.0이 아닌 정수 0~255로 표현되어 있습니다.
X_train_full.shape
'''
(60000, 28, 28)
'''
X_train_full.dtype
'''
dtype('uint8')
'''
경사 하강법으로 신경망을 훈련하기 때문에 입력 특성의 스케일을 조정해야합니다.(?) 픽셀 강도를 255.0으로 나누어 0~1 사이 범위로 조정하겠습니다.
X_valid, X_train = X_train_full[:5000]/255.0, X_train_full[5000:]/255.0
y_valid, y_train = y_train_full[:5000], y_train_full[5000:]
X_test = X_test/255.0
패션 MNIST는 레이블에 해당하는 아이탬의 클래스 이름 리스트를 만들어야합니다.
class_names = ['T-shirt/top', 'Trouser', 'Pullover', 'Dress', 'Coat',
'Sandal', 'Shirt', 'Sneaker', 'Bag', 'Ankle boot']
y_train[0] #4
class_names[y_train[0]] # Coat
MNIST의 데이터셋의 일부 샘플을 보여줍니다.
시퀀셜 API를 사용하여 모델 만들기
model = keras.models.Sequential()
model.add(keras.layers.Flatten(input_shape=[28, 28]))
model.add(keras.layers.Dense(300, activation='relu'))
model.add(keras.layers.Dense(100, activation='relu'))
model.add(keras.layers.Dense(10, activation='softmax'))
두 개의 은닉층으로 이루어진 분류용 다층 퍼셉트론입니다.
- Flatten층은 첫 번째 층이므로 input_shape를 지정해주어야 합니다. 대신 InputLayer를 추가할 수도 있습니다. 사실, input_shape를 지정하면 InputLayer가 자동으로 추가 됩니다.
- Dense층마다 각자 가중치 행렬을 관리합니다. 층의 뉴런과 입력 사이 모든 연결 가중치, 뉴런마다 하나씩 있는 편향도 벡터 형태로 관리합니다.
- 마지막은 베타적인 multiclass이므로 활성화 함수로 softmax함수를 사용합니다.
model = keras.models.Sequential([
keras.layers.Flatten(input_shape=[28, 28]),
keras.layers.Dense(300, activation='relu'),
keras.layers.Dense(100, activation='relu'),
keras.layers.Dense(10, activation='softmax')
])
위 코드처럼 리스트를 Sequential 모델의 층을 전달할 수 있습니다.
model.summary()
model.summary() 메서드는 모델에 있는 모든 층을 출력합니다. Output Shape에 크기 None은 배치 크기에 어떤 값도 가능하다는 의미입니다. 전체 파라미터 개수와 훈련 가능한 파라미터 개수도 출력해줍니다.
model.summary()
'''
Model: "sequential"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
flatten (Flatten) (None, 784) 0
dense (Dense) (None, 300) 235500
dense_1 (Dense) (None, 100) 30100
dense_2 (Dense) (None, 10) 1010
=================================================================
Total params: 266,610
Trainable params: 266,610
Non-trainable params: 0
_________________________________________________________________
'''
Dense 층은 보통 많은 파라미터를 가집니다. 가령 첫 번째 은닉층은 784*300개의 연결 가중치와 300개의 편향을 가집니다. 이는 훈련 데이터를 학습하기에 충분한 유연성을 가지지만, 과대적합의 위험을 갖는다는 의미이기도 합니다. 특히, 훈련 데이터가 많지 않을 경우에 그렇습니다.
Note
from tensorflow.keras.utils import plot_model
plot_model(model)
'''
결과1
'''

model.layers
'''
[<keras.layers.core.flatten.Flatten at 0x7f9fe9eb5f10>,
<keras.layers.core.dense.Dense at 0x7f9fe9eb5350>,
<keras.layers.core.dense.Dense at 0x7f9fe9eb2210>,
<keras.layers.core.dense.Dense at 0x7f9fe9eb2650>]
'''
hidden1 = model.layers[1]
hidden1.name
'''
dense_3
'''
model.get_layer('dense_3') is hidden1
'''
True
'''
모델에 있는 층의 리스트는 model.layers로 선택할 수 있습니다. 층의 모든 파라미터는 get_weights() 메서드와 set_weights() 메서드를 통해 접근할 수 있습니다. Dense 층의 경우 가중치, 편향 모두 포함되어 있습니다.
weights, biases = hidden1.get_weights()
weights
'''
array([[-0.01616156, 0.06369385, 0.04228146, ..., -0.04931265,
0.06989832, -0.0097373 ],
...,
[ 0.00666216, 0.01424557, 0.01450137, ..., 0.04048412,
-0.05995655, -0.0589042 ]], dtype=float32)
'''
weights.shape
'''
(784, 300)
'''
biases
'''
array([0., 0., 0., 0. ... , 0., 0.], dtype=float32)
'''
biases.shape
'''
(300,)
'''
Dense 층은 앞서 언급한 대칭성을 깨뜨리기 위해 연결 가중치를 무작위로 초기화합니다. 편향은 0으로 초기화합니다. 초기화 방법을 변경하고 싶다면 가중치 행렬(kernel)은 kernel_initializer와 편향은 bias_initializer 매개변수를 설정할 수 있습니다. 11장에서 초기화 방법에 대해 자세히 알아보겠습니다.
Note
가중치 행렬의 크기는 Sequential 층의 입력층 크기에 달려있는데, 입력 크기를 지정하지 않아도 괜찮습니다. 케라스는 모델을 빌드하기 전까지 입력 크기를 기다릴 것입니다. 모델 빌드는 실제 데이터를 주입할 때(예를 들어, 훈련 과정 중) build() 메서드를 호출할 때 일어납니다. 모델이 실제 빌드되기 전에 층이 가중치를 갖지 않으면 summary(), 모델 저장 등 특정 작업을 수행할 수 없으니 입력 크기를 알고 있으면 지정하는 것이 좋습니다.
모델 컴파일
model.compile()을 통해 optimizer와 loss function을 지정해야합니다. 부가적으로 평가 지표도 지정할 수 있습니다.
model.compile(loss='sparse_categorical_crossentropy',
optimizer='sgd',
metrics=['accuracy'])
Note
loss='sparse_categorical_crossentropy'는 los=keras.losses.spars_categorical_crossentropy와 같습니다.
optimizer='sgd'는 optimizer=keras.optimizer.SGD()와 동일하고,
metrics=['accuracy']는 metrics=[keras.metrics.sparse_categorical_accuracy]와 같습니다.
이 코드는 클래스가 베타적이므로 'sparse_categorical_crossentropy' 손실을 사용합니다.
샘플 마다 클래스 별 타깃 확률을 가지고 있다면(예를 들어 클래스 3의 경우 [0., 0., 0., 1., 0., 0., 0., 0., 0., 0.]), 'categorical_crossentropy' 손실을 사용해야합니다. 희소 레이블, sparse label(즉, 클래스 인덱스)을 원-핫 벡터 레이블로 변환하려면 keras.utils.to_categorical() 함수를 사용하면 됩니다. 원-핫 벡터 레이블에서 sparse label로 변환하려면 np.argmax()함수를 사용합니다.
이진 분류를 수행한다면 softmax 함수 대신 'sigmoid' 함수를 사용하고, 'binary_crossentropy' 손실을 사용합니다.
optimizer에 'sgd'를 지정하면 기본 확률적 경사 하강법(stochastic gradient descent)을 사용하여 모델을 훈련한다는 의미입니다. 즉, 앞서 소개한 역전파 알고리즘을 수행한다는 의미입니다. 분류기이므로 훈련, 검증 시 정확도를 측정합니다.
Note
SGD optimizer를 사용할 때 학습률을 튜닝하는 것이 중요합니다. ex) optimizer=keras.optimizer.SGD(lr=0.01)
모델 훈련과 평가
history = model.fit(X_train, y_train, epochs=30,\
validation_data=(X_valid, y_valid))
'''
Epoch 1/30
1719/1719 [==============================] - 6s 3ms/step - loss: 0.7124 - accuracy: 0.7599
- val_loss: 0.4992 - val_accuracy: 0.8266
Epoch 2/30
1719/1719 [==============================] - 6s 3ms/step - loss: 0.4888 - accuracy: 0.8279
- val_loss: 0.4538 - val_accuracy: 0.8438
...
Epoch 30/30
1719/1719 [==============================] - 6s 3ms/step - loss: 0.2247 - accuracy: 0.9195
- val_loss: 0.2959 - val_accuracy: 0.8954
'''
epoch가 끝날 때마다 validation_data를 사용해 손실과 추가적인 측정 지표를 계산합니다. 훈련 세트 정확도가 검증 세트보다 월등히 높다면 과대적합되었을 것입니다.(훈련/검증 데이터 세트가 균일하게 분리됐을 경우) validation_data 대신에 validation_split=0.1을 쓰면 케라스는 검증에 (섞기 전의) 데이터의 마지막 10%를 사용합니다.
Note
훈련/검증 데이터의 크기가 서로 맞지 않으면 예외가 발생됩니다. 예를 들어 일렬로 펼친 이미지(X_train.reshape(-1, 784))를 담은 배열로 이 모델을 훈련한다면 아래와 같은 예외가 발생합니다.
"ValueError: Error when cheking input: expected flatten_input to have 3 dimensions, but got array with
shape (60000, 784)."
multiclass classification에서 특정 클래스가 많이 등장하고 다른 클래스는 조금 등장하는 등, 훈련 세트가 편중되어 있다면 fit() 메서드를 호출할 때 class_weight 매개변수를 지정하는 것이 좋습니다. 케라스는 손실을 계산할 때 이 가중치를 사용합니다.
샘플별로 가중치를 부여하고 싶다면 sample_weight 매개변수를 지정합니다. (class_weight과 sample_weight 모두 지정되면 두 값을 곱해 사용합니다.) 예를 들어 어떤 샘플은 전문가가 레이블을 할당하고, 다른 크라우드소싱 플랫폼을 사용해 레이블이 할당되었다면 샘플별 가중치를 둘 수 있습니다. validation_data=(X_test, y_test, 샘플 가중치)로 검증 데이터에 가중치를 지정할 수 있습니다.
History 객체
fit() 메서드가 반환하는 History 객체는 history.params(훈련 파라미터), history.epoch(수행된 epoch 리스트)를 포함하고 있습니다. 훈련 세트 및 검증 세트(있다면)의 손실, 측정 지표는 history.history(딕셔너리)에 담겨있습니다.
import pandas as pd
import matplotlib.pyplot as plt
pd.DataFrame(history.history).plot(figsize=(8, 5)) # DataFrame.plot() !
plt.grid(True)
plt.gca().set_ylim(0, 1) # plt.gca()는 현재 Axes객체를 반환
plt.show()
'''
결과2
'''
loss와 val_loss(검증 손실) 중 훈련 초기에 loss가 val_loss 보다 더 커서 훈련 세트보다 검증 세트가 초기에 더 좋은 성능을 낸 것처럼 보이지만, loss는 에포크가 진행되는 동안 계산되고 val_loss는 에포크가 끝난 후 계산되므로, loss 그래프는 배치 사이즈의 평균 만큼(에포크의 1/2) 왼쪽으로 이동되어야 합니다.
일반적으로 충분히 오래 훈련하면 훈련 세트의 성능이 검증 세트의 성능을 앞지릅니다. val_loss 손실이 여전히 감소한다면 모델이 아직 완전히 수렴하지 않았다고 볼 수 있습니다. 케라스에서 fit() 메서드를 다시 호출하면 중지되었던 곳에서부터 훈련을 이어갈 수 있습니다.
모델 성능이 만족스럽지 못하다면, 처음으로 돌아가 하이퍼파라미터를 튜닝해야 합니다. 맨 처음 확인할 것은 학습률입니다. 학습률이 도움이 되지 않으면 optimizer를 바꿔서 테스트를 해봅니다.(다른 하이퍼 파라미터를 바꾸면 항상 학습률을 다시 튜닝해야 합니다.) 여전히 성능이 낮으면 층 개수, 층의 뉴런 개수, 은닉층의 활성화함수, 배치 크기 등 다른 하이퍼 파라미터를 튜닝합니다.
model.evaluate()
모델을 상용 환경에 배포하기 전, 테스트 세트로 모델을 평가하여 일반화 오차를 수정해야합니다. 테스트 세트의 정확도를 계속 확인하며 하이퍼 파리미터를 수정하면 테스트 데이터의 일반성을 반영할 수 없습니다.
model.evaluate(X_test, y_test)
'''
313/313 [==============================] - 1s 2ms/step - loss: 0.3294 - accuracy: 0.8833
[0.32937338948249817, 0.8833000063896179]
'''
모델을 사용해 예측하기
model.predict()
X_new = X_test[:3]
y_proba = model.predict(X_new)
y_proba.round(2)
'''
array([[0. , 0. , 0. , 0. , 0. , 0. , 0. , 0.02, 0. , 0.97],
[0. , 0. , 1. , 0. , 0. , 0. , 0. , 0. , 0. , 0. ],
[0. , 1. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. ]],
dtype=float32)
'''
3개의 샘플에 대한 예측값을 보면, 각 클래스마다 추정 확률이 담겨 있음을 알 수 있습니다. 가장 높은 확률을 가진 클래스에만 관심이 있다면 np.argmax(model.predict(), axis=1)를 사용할 수 있습니다.
np.argmax(model.predict(), axis=1)
y_pred = np.argmax(y_proba, axis=1)
y_pred
'''
array([9, 2, 1])
'''
np.array(class_names)[y_pred]
'''
array(['Ankle boot', 'Pullover', 'Trouser'], dtype='<U11')
'''
# 이미지 plot
fig, ax = plt.subplots(1, 3, figsize=(9, 3))
for index in range(3):
col = index%3
ax[col].imshow(X_test[index], cmap='gray')
ax[col].set_title(str(class_names[y_pred[index]]))
plt.show()
'''
결과3
'''
세 개의 이미지 모두 올바르게 분류했음을 알 수 있습니다.
Sequential API를 사용하여 회귀용 다층 퍼셉트론 만들기
캘리포니아 주택 가격 데이터셋을 sklearn.datasets.fetch_california_housing() 함수를 사용해 적재하겠습니다. null_data 등 전처리 과정이 필요 없는 데이터셋입니다.
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
housing = fetch_california_housing()
X_train_full, X_test, y_train_full, y_train = train_test_split(housing.data, housing.target)
X_train, X_valid, y_train, y_valid = train_test_split(X_train_full, y_train_full)
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_valid = scaler.transform(X_valid)
X_test = scaler.transform(X_test)
Sequential API를 사용해 회귀용 MLP를 만드는 방법에서 분류와 주된 차이점은 출력층이 활성화 함수가 없는(하나의 값을 예측하기 때문에) 하나의 뉴런을 가진다는 것과 손실 함수로 MSE를 사용한다는 것입니다. 이 데이터셋에는 잡음이 많기 때문에 과대적합을 막는 용도로 뉴런 수가 적은 은닉층 하나만 사용합니다.
model = keras.models.Sequential([
keras.layers.Dense(30, activation='relu', input_shape=X_train.shape[1:]),
keras.layers.Dense(1)
])
model.compile(loss='mean_squared_error', optimizer='sgd')
history = model.fit(X_train, y_train, epochs=20,\
validation_data=(X_valid, y_valid))
mse_test = model.evaluate(X_test, y_test)
'''
Epoch 1/20
363/363 [==============================] - 2s 4ms/step - loss: 1.1027 - val_loss: 0.5439
Epoch 2/20
363/363 [==============================] - 1s 4ms/step - loss: 0.4750 - val_loss: 0.4623
...
Epoch 20/20
363/363 [==============================] - 1s 2ms/step - loss: 0.3469 - val_loss: 0.4022
162/162 [==============================] - 0s 1ms/step - loss: 0.3737
'''
X_new = X_test[:3]
y_pred = model.predict(X_new) #[[1.5233846] [2.423312 ] [0.9126055]]
함수형 API를 사용해 복잡한 모델 만들기
순차적이지 않은 신경망의 한 예는 wide & deep 신경망입니다. 입력의 일부 또는 전체가 출력층에 바로 연결됩니다. 일반적인 MLP는 층 전체에 모든 데이터를 통과시켜 데이터에 있는 간단한 패턴이 연속된 변환으로 인해 왜곡될 수 있었습니다. 이 구조를 사용하면 신경망이 복잡한 패턴(깊게 쌓은 층)과 간단한 규칙(짧은 경로)을 모두 학습할 수 있습니다.
input_ = keras.layers.Input(shape=X_train.shape[1:])
hidden1 = keras.layers.Dense(30, activation='relu')(input_)
hidden2 = keras.layers.Dense(30, activation='relu')(hidden1)
concat = keras.layers.Concatenate()([input_, hidden2])
output = keras.layers.Dense(1)(concat)
model = keras.Model(inputs=[input_], outputs=[output])
Input 객체를 만들어야 합니다. 이 객체는 shape와 dtype을 포함하여 모델의 입력을 정의합니다. 한 모델은 여러 개의 입력을 가질 수 있습니다. 파이썬의 내장 함수 input()과 이름이 충돌되지 않도록 이 객체의 이름을 input_으로 만들었습니다.
hidden1은 30개의 뉴런과 ReLU 활성화 함수를 가진 Dense 층을 만듭니다. 이 층은 만들어지자마자 입력과 함께 함수처럼 호출됩니다. 파이썬에는 객체를 함수처럼 호출하면 실행되는 특수한 __call__() 메서드가 있습니다. 이 메서드에서 build() 메서드를 호출하여 층의 가중치를 생성합니다.
Concatenate 클래스는 axis 매개변수에 연결할 기준 차원을 지정할 수 있습니다 기본값은 -1로 마지막 차원입니다.
마지막으로 출력층에 활성화 함수가 없는 하나의 뉴런을 만들고 Concatenate 층이 만든 결과를 사용해 호출합니다.
입력을 여러 개로 만들 수도 있습니다. 예를 들어 5개 특성(특성 인덱스 0~4번)을 짧은 경로로 보내고 6개 특성(특성 인덱스 2~7번)을 깊은 경로로 보낸다고 가정해봅시다.
input_A = keras.layers.Input(shape=[5], name='wide_input')
input_B = keras.layers.Input(shape=[6], name='deep_input')
hidden1 = keras.layers.Dense(30, activation='relu')(input_B)
hidden2 = keras.layers.Dense(30, activation='relu')(hidden1)
concat = keras.layers.concatenate([input_A, hidden2])
output = keras.layers.Dense(1, name='output')(concat)
model = keras.Model(inputs=[input_A, input_B], outputs=[output])
모델을 만들 때 inputs=[input_A, input_B]로 지정했는데, fit() 메서드를 호출할 때 하나의 입력 행렬 X_train이 아닌 하나의 행렬 튜플 (X_train_A, X_train_B)을 전달해야 합니다. X_valid에도 동일하게 적용됩니다. evaluate(), predict()를 호출할 때 X_test, X_new도 동일합니다.
또는 {'wide_input': X_train_A, 'deep_input': X_train_B}와 같이 입력 이름과 입력값을 매핑한 딕셔너리를 전달할 수도 있습니다.
optimizer = tf.keras.optimizers.SGD(learning_rate=1e-3)
model.compile(loss='mse', optimizer=optimizer)
X_train_A, X_train_B = X_train[:, :5], X_train[:, 2:]
X_valid_A, X_valid_B = X_valid[:, :5], X_valid[:, 2:]
X_test_A, X_test_B = X_test[:, :5], X_test[:, 2:]
X_new_A, X_new_B = X_test_A[:3], X_test_B[:3]
history = model.fit((X_train_A, X_train_B), y_train, epochs=20,
validation_data=((X_valid_A, X_valid_B), y_valid))
mse_test = model.evaluate((X_test_A, X_test_B), y_test)
y_pred = model.predict((X_new_A, X_new_B))
# array([[1.7123387], [2.4261556], [1.0448873]], dtype=float32)
여러개의 출력이 필요한 경우는 많습니다. 그림에 있는 주요 물체를 분류하고 위치를 알아야 하는 경우 회귀 작업(물체 중심의 좌표와 너비, 높이를 찾습니다.)과 분류 작업을 함께 하는 경우입니다.
다른 경우는 동일한 데이터에서 독립적인 여러 작업을 수행할 때 입니다. 작업마다 새로운 신경망을 훈련하는 것보다 작업마다 하나의 출력을 가진 단일 신경망을 훈련하는 것이 보통 더 나은 결과를 냅니다. 예를 들어 얼굴 사진으로 다중 작업 분류(multitask classification)을 수행할 수 있습니다. 한 출력은 사람의 얼굴 표정(기쁨, 놀람)을 분류하고 다른 출력은 안경을 썼는지 구별합니다.
또 다른 예는 규제 기법으로 사용하는 경우입니다. 과대적합을 감소하고 모델의 일반화 성능을 높이도록 훈련에 제약을 가합니다. 예를 들어 신경망 구조 안에 보조 출력을 추가할 수 있습니다. 보조 출력을 통해 하위 네트워크가 나머지 네트워크에 의존하지 않고 그 자체로 유용한 것을 학습하는지 확인할 수 있습니다.
보조 출력을 추가하는 것은 매우 쉽습니다. 적절한 층에 연결하고 모델의 출력 리스트에 추가하면 됩니다.
input_A = keras.layers.Input(shape=[5], name='wide_input')
input_B = keras.layers.Input(shape=[6], name='deep_input')
hidden1 = keras.layers.Dense(30, activation='relu')(input_B)
hidden2 = keras.layers.Dense(30, activation='relu')(hidden1)
concat = keras.layers.concatenate([input_A, hidden2])
output = keras.layers.Dense(1, name='output')(concat)
# 출력층 이전은 앞의 입력 2개 모델과 동일
aux_output = keras.layers.Dense(1, name='aux_output')(hidden2)
model = keras.Model(inputs=[input_A, input_B], outputs=[output, aux_output])
각 출력은 자신만의 손실함수가 필요합니다.(리스트 및 딕셔너리 형으로 전달) 하나의 손실을 전달하면 케라스는 모든 출력의 손실 함수가 동일하다고 가정합니다. 기본적으로 케라스는 나열된 손실을 모두 더하여 최종 손실을 구해 훈련에 사용합니다. 출력에 각각 가중치를 부여할 수도 있습니다. 주 출력에 더 많은 가중치를 부여해야합니다.
model.compile(loss=['mse', 'mse'], loss_weights=[0.9, 0.1], optimizer='sgd')
model.fit() 할 때 각 출력에 대한 레이블을 제공해야 합니다. 여기서는 주출력과 보조 출력이 같은 것을 예측해야 하므로 동일한 레이블을 사용합니다. 튜플 형으로 전달합니다.
history = model.fit(
[X_train_A, X_train_B], [y_train, y_train], epochs=20,
validation_data=([X_valid_A, X_valid_B], [y_valid, y_valid]))
'''
Epoch 1/20
363/363 [==============================] - 2s 4ms/step - loss: 1.1994 - output_loss: 1.0583 - aux_output_loss: 2.4693 - val_loss: 0.7183 - val_output_loss: 0.6428 - val_aux_output_loss: 1.3978
Epoch 2/20
363/363 [==============================] - 1s 3ms/step - loss: 0.5912 - output_loss: 0.5185 - aux_output_loss: 1.2451 - val_loss: 0.5818 - val_output_loss: 0.5218 - val_aux_output_loss: 1.1219
..
Epoch 20/20
363/363 [==============================] - 1s 3ms/step - loss: 0.3686 - output_loss: 0.3527 - aux_output_loss: 0.5121 - val_loss: 0.3956 - val_output_loss: 0.3792 - val_aux_output_loss: 0.5428
'''
모델을 평가하면 케라스는 개별 손실과 함께 총 손실을 반환합니다.
total_loss, main_loss, aux_loss = model.evaluate(
[X_test_A, X_test_B], [y_test, y_test]
)
'''
162/162 [==============================]
- 0s 2ms/step - loss: 0.3718 - output_loss: 0.3572 - aux_output_loss: 0.5030
'''
predict() 메서드는 각 출력에 대한 예측을 반환합니다.
y_pred_main, y_pred_aux = model.predict([X_new_A, X_new_B])
# y_pred_main: [[1.6474993] [2.2001808] [0.9556121]]
# y_pred_aux: [[1.4464535] [2.5180244] [1.151218 ]]
이처럼 함수형 API를 사용하면 원하는 어떤 종류의 구조도 손쉽게 만들 수 있습니다. 그럼 케라스 모델을 만들 수 있는 마지막 방법을 알아보겠습니다.
서브클래싱 API로 동적 모델 만들기
Sequential, 함수형 API는 모두 선언적(declarative)입니다. 사용할 층과 연결 방식을 먼저 정의해야합니다. 이는 모델 구조를 출력, 분석, 저장, 공유하기 쉽습니다. 프레임 워크가 크기를 짐작하고 타입을 확인하여 에러를 일찍 발견할 수 있습니다. 전체 모델이 층으로 구성된 정적 그래프이므로 디버깅하기도 쉽습니다.
하지만, 어떤 모델은 반복문, 조건문을 가지는 여러 가지 동적인 구조를 필요로 합니다. 이런 경우 명령형(imperative) 프로그래밍이 필요하며 서브클래싱(subclassing) API가 필요합니다.
간단히 Model 클래스를 상속한 다음 생성자 안에 필요한 층을 만듭니다. 그 다음 call() 메서드 안에 수행하려는 연산을 기술합니다.
WideAndDeepModel 클래스를 서브클래싱 API로 만들면 함수형 API로 만든 모델과 동일한 기능을 수행하며 이 인스턴스를 사용해 모델 컴파일, 훈련, 평가, 예측을 수행할 수 있습니다.
class WideAndDeepModel(keras.Model):
def __init__(self, units=30, activation='relu', **kwargs):
super().__init__(**kwargs) # 표준 매개 변수를 처리합니다. (예를 들면, name)
self.hidden1 = keras.layers.Dense(units, activation=activation)
self.hidden2 = keras.layers.Dense(units, activation=activation)
self.main_output = keras.layers.Dense(1)
self.aux_output = keras.layers.Dense(1)
def call(self, inputs):
input_A, input_B = inputs
hidden1 = self.hidden1(input_B)
hidden2 = self.hidden2(hidden1)
concat = keras.layers.concatenate([input_A, hidden2])
main_output = self.main_output(concat)
aux_output = self.aux_output(hidden2)
return main_output, aux_output
model = WideAndDeepModel()
이 예제는 함수형 API와 매우 비슷하지만, Input 클래스의 객체를 만들 필요 없이 call() 메서드의 input 매개변수를 사용합니다. 생성자에 있는 층 구성과 call() 메서드에 있는 정방향 계산을 분리했습니다.
함수형 API와 주된 차이점은 call() 메서드 안에 원하는 어떤 계산(for, if, 텐서플로 저수준 연산)도 가능하다는 것입니다.
유연성이 높아지면 그에 따른 비용도 발생하는데, 모델 구조가 call() 메서드 안에 숨겨져 있어 케라스가 쉽게 이를 분석할 수 없습니다. 즉, 모델을 저장하고 복사할 수 없습니다. summary() 메서드를 호출하면 층의 목록만 나열되고 층 간의 연결 정보를 얻을 수 없습니다. 또한 층의 타입과 크기도 알 수 없어 실수가 발생하기 쉽습니다. 따라서 필요할 때만 사용하는 것이 좋습니다.
모델 저장과 복원
Sequential API, 함수형 API를 사용하면 훈련된 케라스 모델을 저장하는 것은 매우 쉽습니다.
model = keras.models.Sequential([..]) # 또는 keras.Model([..])
model.compile([..])
model.fit([..])
model.save("my_keras_model.h5")
케라스는 HDF5 포맷을 사용하여 모델 구조, 층의 모든 모델 파라미터(연결 가중치, 편향)를 저장합니다. 또한, 옵티마이저도 저장합니다. model.save()의 save_format 매개변수를 tf로 지정하면 텐서플로의 SavedModel 포맷을 사용 가능합니다.
일반적으로 하나의 파이썬 스크립트에서 모델을 훈련하고 하나 이상의 스크립트(또는 웹 서비스)에서 모델을 로드하고 예측을 만드는데 활용합니다. 모델 로드는 아래와 같이 간단합니다.
model = keras.models.load_model('my_keras_model.h5')
CAUTION_
모델 서브 클레싱에서는 이 방식을 사용할 수 없고, save_weights()와 load_weights() 메서드를 사용하여 모델 파라미터를 저장하고 복원할 수 있습니다. 그 외에는 모두 수동으로 저장해야합니다. 파이썬의 피클(pickle) 모듈을 사용하여 모델 객체를 직렬화할 수 있습니다.
훈련이 몇 시간 동안 지속되는 경우 훈련 마지막에 모델을 저장하는 것뿐만 아니라 훈련 도중 일정 간격으로 체크 포인트를 저장해야합니다. callback을 사용하면 체크포인트를 저장할 수 있습니다.
콜백 사용하기
fit() 메서드의 callbacks 매개변수를 사용하여 케라스가 훈련의 시작, 끝에 호출할 객체 리스트를 지정할 수 있습니다. 에포크의 시작, 끝에 혹은 각 배치 처리 전후에 호출할 수도 있습니다. ModelCheckpoint는 매 에포크의 끝에서 호출됩니다.
[..] # 모델을 만들고 컴파일 하기
checkpoint_cb = keras.callbacks.ModelCheckpoint("my_keras_model.h5")
history = model.fit(X_train, y_train, epochs=10, callbacks=[checkpoint_cb])
ModelCheckpoint를 만들 때 save_best_only=True로 지정하면, 최상의 검증 세트 점수에서만 모델을 저장합니다. 오랜 훈련 시간으로 훈련 세트에 과적합될 걱정을 하지 않아도 됩니다. 훈련이 끝난 후 마지막 저장된 모델을 복원하면 됩니다. 다음 코드는 조기 종료를 구현하는 방법입니다.
checkpoint_cb = keras.callbacks.ModelCheckpoint("my_keras_model.h5",
save_best_only=True)
history = model.fit(X_train, y_train, epochs=10,
validation_data=(X_valid, y_valid),
callbacks=[checkpoint_cb])
model = keras.models.load_model('my_keras_model.h5') # 최상의 모델로 복원
조기 종료를 구현하는 다른 방법은 EarlyStopping 콜백을 사용하는 것입니다. 일정 에포크(patience 매개변수) 동안 점수가 향상되지 않으면 훈련을 멈춥니다. 따라서 에포크의 숫자를 크게 지정해도 됩니다. EarlyStopping 콜백은 훈련이 끝난 후 최상의 가중치를 복원하기 때문에 저장된 모델을 따로 복원할 필요가 없습니다. ModelCheckpoint 콜백과 EarlyStopping 콜백을 동시에 사용할 수도 있습니다. 이외에도 keras.callbacks 패키지에는 다른 종류의 콜백이 많이 있습니다.
사용자 정의 콜백을 만들 수도 있습니다. 다음 사용자 정의 콜백은 훈련하는 동안 검증 손실, 훈련 손실의 비율을 출력합니다.(즉 과대적합을 감지합니다.)
class PrintValTrainRatioCallback(keras.callbacks.Callback):
def on_epoch_end(self, epoch, logs):
print('\nval/train: {:.2f}'.format(logs['val_loss']/logs['loss']))
예상할 수 있겠지만 on_train_begin(), on_train_end(), on_epoch_begin(), on_epoch_end(), on_batch_begin(), on_batch_end()를 구현할 수 있습니다.
평가에서는 on_test_begin(), on_test_end(), on_test_batch_begin(), on_test_batch_end()를 구현해야합니다.(evaluate()에서 호출됩니다)
에측에 사용하려면 on_predict_begin(), on_predict_end(), on_predict_batch_begin(), on_predict_batch_end()를 구현해야 합니다.(predict()에서 호출됩니다.)
이제 tf.keras를 사용할 때 반드시 알아야 할 도구인 텐서보드(TensorBoard)를 알아봅시다.
텐서보드를 사용해 시각화하기
텐서보드는 매우 좋은 인터렉티브 시각화 도구입니다. 훈련하는 동안 학습 곡선을 그리거나 여러 실행 간의 학습 곡선을 비교하고 계산 그래프 시각화와 훈련 통계 분석을 수행할 수 있습니다. 텐서보드는 텐서플로를 설치할 때 자동으로 설치됩니다.
텐서보드를 사용하려면 프로그램을 수정하여 이벤트 파일(event file)이라는 특별한 이진 로그 파일에 시각화하려는 데이터를 출력해야 합니다. 각각의 이진 데이터 레코드를 summary라고 부릅니다. 텐서보드 서버는 로그 디렉터리를 모니터링하고 자동으로 변경사항을 읽어 그래프를 업데이트합니다. 일반적으로 텐서보드 서버는 root 로그 디렉터리를 가르키고, 프로그램은 실행할 때마다 다른 서브 디렉터리에 이벤트를 기록합니다. 이렇게 하면 복잡하지 않게 하나의 텐서보드 서버가 여러 프로그램의 결과를 시각화하고 비교할 수 있습니다.
텐서보드 로그를 위해 사용할 루트 로그 디렉터리를 정의하겠습니다. 현재 날짜, 시간을 이름으로 서브디렉터리 경로를 생성하는 간단한 함수도 만들겠습니다.
import os
root_logdir = os.path.join(os.curdir, 'my_logs')
def get_run_logdir():
import time
run_id = time.strftime("run_%Y_%m_%d-%H_%M_%S")
return os.path.join(root_logdir, run_id)
run_logdir = get_run_logdir() # 예를 들면, './my_logs/run_2019_06_07-15_15_22'
# 앞절의 모델 구성과 컴파일
tensorboard_cb = keras.callbacks.TensorBoard(run_logdir)
history = model.fit(X_train, y_train, epochs=30,
validation_data=(X_valid, y_valid),
callbacks=[tensorboard_cb])
TensorBoard() 콜백이 로그 디렉터리를 생성합니다. 실행마다 아래와 같은 서브 디렉터리가 계속 생성됩니다. train, validation 폴더에 있는 파일은 이벤트 파일입니다. 훈련 로그는 profiling trace 파일도 포함입니다. 텐서보드가 이 파일을 사용해 모델의 각 부분에서 시간이 얼마나 소요되었는지 보여주므로 성능 병목 지점을 찾는 데 큰 도움이 됩니다.
텐서보드 서버를 시작할 차례입니다. 터미널 명령을 실행하는 방법도 있고, 주피터 안에서 바로 텐서보드를 사용할 수도 있습니다.
$ tensorboard --logdir=./my_logs --port=6006
%load_ext tensorboard # 텐서보드 확장
%tensorboard --logdir=/content/my_logs --port=6006 # 포트 6006에서 텐서보드 서버 실행
첫 번째 훈련 손실, 두 번째 훈련 손실은 각각 sgd의 lr이 0.001, 0.05를 사용한 것인데, 확실히 두 번째가 더 빠르게 감소합니다.
또한, 전체 그래프, 3D로 투영된 학습된 가중치, 프로파일링 트레이스를 볼 수 있습니다. TensorBoard() 콜백은 embedding 같은 추가 데이터를 로깅할 수 있는 옵션을 제공합니다.(13장 참조)
tensorflow는 tf.summary 패키지로 저수준 API를 제공합니다. create_file_writer() 함수를 사용해 SummaryWritter를 만들고 with 블럭 안에서 텐서보드를 사용해 시각화할 수 있는 스칼라, 히스토그램, 이미지, 오디오, 텍스트를 시험삼아 기록해보겠습니다.
import numpy as np
test_logdir = get_run_logdir()
writer = tf.summary.create_file_writer(test_logdir)
with writer.as_default():
for step in range(1, 1000 + 1):
tf.summary.scalar('my_scalar', np.sin(step/10), step=step)
data = (np.random.randn(100)+2)*step/100 # 몇몇 랜덤 데이터
tf.summary.histogram('my_hist', data, buckets=50, step=step)
images = np.random.rand(2, 32, 32, 3) # 32x32 RGB 이미지
tf.summary.image('my_images', images*step/1000, step=step)
texts = ['The step is' + str(step), 'Its square is '+ str(step**2)]
tf.summary.text('my_text', texts, step=step)
sine_wave = tf.math.sin(tf.range(12000)/48000 * 2 * np.pi * step)
audio = tf.reshape(tf.cast(sine_wave, tf.float32), [1, -1, 1])
tf.summary.audio('my_audio', audio, sample_rate=48000, step=step)
신경망 하이퍼파라미터 튜닝하기
신경망은 유연해서 각 층에서 사용하는 활성화 함수, 가중치 초기화 전략 등 많은 것을 바꿀 수 있습니다. 파라미터를 튜닝하는 한 가지 방법은 많은 하이퍼파라미터 조합을 시도해보고 어떤 것이 검증 세트에서 가장 좋은 점수를 내는지 확인하는 것입니다. GridSearchCV, RandomizedSearchCV를 사용해 하이퍼파라미터 공간을 탐색할 수 있습니다. 이렇게 하려면 케라스 모델을 사이킷런 추정기처럼 보이도록 바꾸어야합니다.
먼저, 케라스 모델을 만들고 컴파일하는 함수를 만듭니다. 사이킷런과 마찬가지로 가능한 하이퍼파라미터에 적절한 기본값을 설정하는 것이 좋습니다.
def build_model(n_hidden=1, n_neurons=30, learning_rate=3e-3, input_shape=[8]):
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(1))
optimizer = tf.keras.optimizers.SGD(learning_rate=learning_rate)
model.compile(loss='mse', optimizer=optimizer)
return model
build_model() 함수를 사용해 KerasRegressor 클래스의 객체를 만듭니다.
keras_reg = keras.wrappers.scikit_learn.KerasRegressor(build_model)
KerasRegressor 객체는 build_model()로 만들어진 케라스 모델을 감싸는 간단한 wrapper입니다. 이 객체를 만들 때 어떤 파이퍼 파라미터도 지정하지 않았으므로 build_model()에 정의된 기본 하이퍼파라미터를 사용할 것입니다. 이제 일반적인 사이킷런 estimator처럼 이 객체를 사용할 수 있습니다. fit(), score(), predict() 메서드로 훈련, 평가, 예측을 만들 수 있습니다.
keras_reg.fit(X_train, y_train, epochs=100,
validation_data=(X_valid, y_valid),
callbacks=[keras.callbacks.EarlyStopping(patience=10)])
mse_test = keras_reg.score(X_test, y_test)
y_pred = keras_reg.predict(X_new)
fit() 메서드에 지정한 모든 매개변수는 케라스 모델로 전달됩니다. 사이킷런은 손실이 아니라 점수로 계산하기 때문에(즉, 높을 수록 좋습니다.) 출력 점수는 음수의 MSE입니다.
수백 개의 모델을 훈련하고 검증 세트에서 최상의 모델을 선택해야 합니다. 튜닝할 하이퍼파라미터가 많으므로 GridSearchCV보다 RandomizedSearchCV를 사용하는 것이 좋습니다. 은닉층 개수, 뉴런 개수, 학습률을 사용해 하이퍼파라미터 탐색을 수행합니다.
from scipy.stats import reciprocal
from sklearn.model_selection import RandomizedSearchCV
param_distribs ={
'n_hidden': [0, 1, 2, 3],
'n_neurons': np.arange(1, 100),
'learning_rate': reciprocal(3e-4, 3e-2)
}
rnd_search_cv = RandomizedSearchCV(keras_reg, param_distribs, n_iter=10, cv=3)
rnd_search_cv.fit(X_train, y_train, epochs=100,
validation_data=(X_valid, y_valid),
callbacks=[keras.callbacks.EarlyStopping(patience=10)])
RandomizedSearchCV는 K-Fold 교차 검증을 사용하기 때문에 X_valid, y_valid를 사용하지 않습니다. 이 데이터는 조기 종료시에만 사용됩니다.?
GridSearchCV, RandomizedSearchCV의 fit() 메서드는 훈련 데이터, 타깃 레이블, GroupKFold 분할기에 사용할 그룹을 지정하는 groups 매개변수를 제외한 모든 다른 매개변수를 estimator의 fit() 메서드로 전달합니다.
rnd_search_cv.best_params_
# {learning_rate': 0.0036, 'n_hidden': 2, 'n_neurons' :42}
rnd_search_cv.best_score_
# -0.31895
model = rnd_search_cv.best_estimator_.model
이제 이 모델을 저장하고 테스트 세트에서 평가합니다. 성능이 만족스럽다면 상용환경에 배포할 수 있습니다.
RandomizedSearchCV를 사용하는 것은 훈련에 시간이 많이 걸려서 탐색할 수 있는 하이퍼파라미터 공간에 제약이 생깁니다. 효율적으로 하이퍼파라미터 공간을 탐색하는 여러 기법이 있습니다. 다음은 하이퍼파라미터 최적화에 사용할 수 있는 몇 개의 파이썬 라이브러리입니다.
- Hyperopt: 학습률과 같은 float, 층의 개수와 같은 int를 포함해 모든 종류의 복잡한 탐색 공간에 대해 최적화를 수행
- Hyperas, kopt, Talos: 케라스 모델을 위한 하이퍼 파라미터 최적화 라이브러리(처음 두개는 Hyperopt 기반)
- Keras Tuner: 사용하기 쉬운 케라스 하이퍼파라미터 최적화 라이브러리
- Scikit-Optimize(skopt): 범용 최적화 라이브러리, BayesSearchCV 클래스는 GridSearchCV와 비슷한 인터페이스를 사용하여 Bayesian 최적화를 수행합니다.
- Spearmint: 베이즈 최적화 라이브러리
- Hyperband: 『Hyperband』의 논문을 기반으로 구축된 빠른 하이퍼파라미터 튜닝 라이브러리
- Sklearn-Deap: GridSearchCV와 비슷한 인터페이스를 가진 진화 알고리즘 기반 하이퍼파라미터 최적화 라이브러리
많은 회사들이 하이퍼파라미터 최적화 서비스를 제공합니다.
- 구글 클라우드 AI 플랫폼의 하이퍼 파라미터 튜닝 서비스 (19장에서 알아봄)
- Arimo (https:arimo.com)
- SigOpt (https://sigopt.com)
- Oscar (https://oscar.calldesk.ai)
하이퍼파라미터 튜닝 분야는 활발히 연구되고 있는데, 이 분야에 진화 알고리즘이 다시 등장하고 있습니다. 딥마인드의 논문(Population Based Training of Neural Networks)에서는 모델의 개체군(population)과 하이퍼파라미터를 함께 최적화했습니다. 하이퍼파라미터 탐색뿐만 아니라 문제에 최적인 신경망 구조를 찾기 위해 진화 전략을 사용합니다. 구글의 AutoML 서비스(https://cloud.google.com/automl)는 이미 클라우드로 제공됩니다.
신경망을 수동으로 만드는 시대는 곧 막을 내리지 않을까요?라는 주제에 대한 블로그 포스트(https://homl.info/automlpost)가 있습니다. 사실 진화 알고리즘은 경사 하강법을 대체하여 개별 신경망을 성공적으로 훈련해왔습니다. 예를 들어 심층 신경 진화(deep neuroevolution) 기법을 소개한 Uber의 포스트(https://homl.info/neuroevol)를 확인해보세요.
도구와 서비스만 쓰는 것이 아닌 신속한 프로토 타입 생성과 탐색 영역을 제안하기 위해 하이퍼파라미터에 어떤 값이 적절한지 생각해보는 것은 도움이 됩니다. 다음 절에서는 퍼셉트론의 은닉층 개수와 뉴런 개수를 고르고 주요 하이퍼파라미터에서 좋은 값을 선택하는 가이드라인을 소개하겠습니다.
은닉층 개수
은닉층 하나로 시작해도 뉴런 개수가 충분하면 아주 복잡한 함수도 모델링할 수 있습니다. 하지만, 복잡한 함수를 모델링하는 데 심층 신경망이 훨씬 적은 수의 뉴런을 사용하므로 동일한 양의 훈련 데이터에서 더 높은 성능을 낼 수 있습니다. 즉, 파라미터 효율성이 훨씬 좋습니다.
실제 데이터는 계층 구조를 가진 경우가 많아 심층 신경망이 유리한데, 아래쪽 은닉층은 저수준의 구조를 모델링하고(여러 방향이나 선), 중간 은닉층은 저수준의 구조를 연결해 중간 수준의 구조를 모델링합니다.(사각형, 원), 그리고 가장 위쪽 은닉층과 출력층은 중간 수준의 구조를 연결해 고수준의 구조를 모델링합니다.(얼굴) 은닉층 하나로 이 구조를 표현하려면 얼굴의 선, 방향을 모두 파악해야하므로 굉장히 많은 뉴런이 필요합니다.
계층 구조는 새로운 데이터에 일반화되는 능력도 향상시켜줍니다. 예를 들면, 얼굴을 인식하는 모델을 훈련한 후, 헤어스타일을 인식하는 신경망을 새로 훈련하려면 첫 번째 네트워크의 하위 층을 재사용하여 훈련을 시작할 수 있습니다. 새로운 신경망에서 처음 몇 개 층의 가중치와 편향을 난수로 초기화하는 대신 첫 번째 신경망에 있는 가중치와 편향값으로 초기화할 수 있습니다. 이런 방식을 사용하면 대부분의 사진에서 저수준 구조를 학습할 필요 없이 고수준 구조(헤어스타일)만 학습하면 됩니다. 이를 전이 학습(transfer learning)이라고 합니다.
대규모 이미지 분류나 음성 인식 같이 매우 복잡한 작업에서는 일반적으로 수십 개 층으로 이루어진 네트워크가 필요합니다. 그리고 훈련 데이터가 아주 많이 필요한데, 이런 네트워크를 처음부터 훈련하는 경우는 드물고, 비슷한 작업에서 가장 뛰어난 성능을 낸 미리 훈련된 네트워크 일부를 재사용하는 것이 일반적입니다. 훈련 속도는 훨씬 빠르고 데이터도 훨씬 적게 필요합니다.
은닉층의 뉴런 개수
입력층과 출력층의 뉴런 개수는 해당 작업에 필요한 입력과 출력의 형태에 따라 결정됩니다. 예를 들어 MNIST는 28x28=784개의 입력 뉴런과 10개의 출력 뉴런이 필요합니다.(층의 뉴런 수를 정할 때 편향 뉴런은 특별히 언급하지 않는 것이 보통입니다.)
은닉층의 구성 방식은 일반적으로 각 층의 뉴런을 점점 줄여서 깔때기처럼 구성합니다. 저수준의 많은 특성이 고수준의 적은 특성으로 합쳐질 수 있기 때문입니다. 하지만, 이 구성은 요즘엔 일반적이지 않고 대부분의 경우 모든 은닉층에 같은 크기를 사용해도 동일하거나 더 나은 성능을 냅니다. 이렇게 할 경우 하이퍼파라미터가 층마다 한 개씩이 아니라 전체를 통틀어 하나가 됩니다. 첫 번째 은닉층을 크게 하는 것은 도움이 됩니다.
스트레치 팬츠(stretch pants) 방식은 가능한 더 많은 층과 뉴런을 가진 모델을 선택하고, 그 다음 과대적합되지 않도록 조기 종료나 규제 기법을 사용하는 방식입니다. 이는 간단하고 효과적인 방식입니다. 즉, 나에게 맞는 사이즈의 바지를 찾느라 시간을 낭비하는 대신에 그냥 큰 스트레치 팬츠를 사고 나중에 알맞게 줄이는 것입니다. 이 방식을 사용하면 모델에서 문제를 일으키는 병목층을 피할 수 있습니다. 한 층의 뉴런 수가 너무 적으면 입력에 있는 유용한 정보를 모두 유지하지 못합니다. 예를 들어, 뉴런 두 개를 가진 층은 2D 데이터만 출력할 수 있습니다. 만약 3D 데이터를 처리한다면 일부 정보를 잃게 됩니다. 이 정보는 다시 복원되지 않습니다.
일반적으로 층의 뉴런 수보다 층 수를 늘리는 쪽이 이득이 많습니다.
학습률, 배치 크기 그리고 다른 하이퍼파라미터
은닉층, 뉴런 개수 외 중요한 하이퍼파라미터 일부와 조정 방법을 설명하겠습니다.
학습률
일반적으로 훈련 알고리즘이 발산하는 학습률(최대 학습률)의 절반 정도입니다. 좋은 학습률을 찾는 방법은 매우 낮은 학습률에서 점진적으로 매우 큰 학습률까지 수백 번 반복하여 모델을 훈련하는 것입니다. 반복마다 일정한 값을 학습률에 곱합니다. 예를 들면, 10^(-5)에서 10까지 exp(log[(10^(6))/500])를 500번 반복하여 곱하는 것입니다. 학습률에 대한 손실 그래프를 그리면 처음엔 손실이 줄어들다가 학습률이 커짐에 따라 다시 커지는 상승점이 있는데, 최적의 학습률은 상승점보다 조금 아래에 있을 것입니다.(일반적으로 상승점보다 약 10배 낮은 지점입니다.) 11장에서 더 자세히 알아보겠습니다.
최적의 학습률은 다른 하이퍼파라미터에 의존적이고, 특히 배치 크기에 영향을 많이 받습니다. 따라서 다른 하이퍼파라미터를 수정하면 학습률도 반드시 튜닝해야 합니다.
옵티마이저
11장에서 알아보겠습니다.
배치크기
학습률 예열(작은 학습률로 훈련을 시작해 점점 커지는 것)을 사용해 큰 배치 크기를 시도해본 후 훈련이 불안정하거나 최종 성능이 만족스럽지 못하면 작은 배치 크기를 사용하면 됩니다.
tensorflow 2.4 버전 이상의 케라스 모델의 compile() 메서드에 있는 step_per_execution 매개변수를 1 이상으로 설정하면 계산 그래프(?)를 한 번 실행할 때 여러 배치를 처리할 수 있기 때문에, GPU를 최대로 활용하고 배치 크기를 바꾸지 않고 훈련 속도를 높일 수 있습니다.
큰 배치 크기는 GPU와 같은 하드웨어 가속기를 효율적으로 활용할 수 있고, 초당 더 많은 샘플을 처리할 수 있습니다. 많은 연구자, 기술자들은 GPU 램에 맞는 가장 큰 배치 크기를 사용하라고 권장합니다. 하지만 한 가지 주의할 점은 큰 배치를 사용하면 훈련 초기 종종 불안정하게 훈련됩니다. 즉, 작은 배치 크기로 훈련된 모델만큼 일반화 성능을 내지 못할 수 있습니다.
활성화 함수
일반적으로 ReLU 활성화 함수가 모든 은닉층에 좋은 기본값입니다. 출력층의 활성화 함수는 수행하는 작업에 따라 달라집니다.
반복 횟수
훈련 반복 횟수는 튜닝할 필요 없습니다. 대신 조기 종료를 사용합니다.
연습문제
https://playground.tensorflow.org/
출처: Hands-On Machine Learning with Scikit-Learn, Keras & TensorFlow 2판
사진 출처:
https://formal.hknu.ac.kr/handson-ml2/slides/handson-ml2-10.slides.html#/
https://excelsior-cjh.tistory.com/172
'핸즈온 머신러닝 2판' 카테고리의 다른 글
15장 RNN과 CNN을 사용해 시퀀스 처리하기 (0) | 2022.01.21 |
---|---|
14장 합성곱 신경망을 사용한 컴퓨터 비전 (0) | 2022.01.15 |
13장 텐서플로에서 데이터 적재와 전처리하기 (0) | 2022.01.10 |
12장 텐서플로를 사용한 사용자 정의 모델과 훈련 (0) | 2022.01.04 |
11장 심층 신경망 훈련하기 (0) | 2021.12.30 |