14 분 소요

본 글은 [핸즈온 머신러닝 2판 (Hands-On Machine Learning with Scikit-Learn, keras & TensorFlow)] - (박해선 옮김) 책을 개인공부하기 위해 요약, 정리한 내용입니다. 전체 코드는 https://github.com/ingu627/handson-ml2에 기재했습니다.(원본 코드 fork) 뿐만 아니라 책에서 설명이 부족하거나 이해가 안 되는 것들은 외부 자료들을 토대로 메꿔 놓았습니다. 오타나 오류는 알려주시길 바라며, 도움이 되길 바랍니다.

10장. 케라스를 사용한 인공 신경망 소개

  • 지능적인 기계를 만드는 법에 대한 영감을 얻으려면 뇌 구조를 살펴보는 것이 합리적이다. 이는 인공 신경망(ANN; Artificial Neural Network)을 촉발시킨 근원이다.
  • 인공 신경망은 뇌에 있는 생물학적 뉴런의 네트워크에서 영감을 받은 머신러닝 모델이다.
    • 요즈음에는 뉴런(neuron) 대신 유닛(unit)으로 부른다.
  • 케라스(Keras)는 신경망 구축, 훈련, 평가, 실행을 목적으로 설계된 고수준 딥러닝 API이다.

  • 이 글에서 다루는 것
    • 생물학적 뉴런에서 인공 뉴런(TLU, 퍼셉트론)으로의 발전 과정과 논리 연산
    • 다층 퍼셉트론(MLP) 구조와 역전파(Backpropagation) 알고리즘 및 활성화 함수
    • 회귀와 분류를 위한 MLP 아키텍처 설계
    • 텐서플로 2와 케라스 시퀀셜 API를 활용한 패션 MNIST 이미지 분류기 및 회귀 모델 구축
    • 함수형 API(Wide & Deep 모델) 및 서브클래싱 API를 활용한 복잡·동적 모델 구현
    • 모델 저장·복원과 조기 종료 콜백(EarlyStopping, ModelCheckpoint) 활용법
    • 신경망 하이퍼파라미터(은닉층 개수, 뉴런 수) 튜닝 가이드

10.1 생물학적 뉴런에서 인공 뉴런까지

image

이미지 출처1

  • 과거와 달리 현대 인공 신경망이 실생활과 다양한 산업 영역에서 실질적인 성과를 내게 된 주요 요인은 다음과 같다.
  1. 신경망 훈련을 위한 대규모 데이터셋 구축이 가능해졌다. 인공 신경망은 복잡도가 높고 대용량인 패턴 인식 문제에서 기존 머신러닝 기법 대비 뛰어난 표현력을 나타낸다.
  2. 1990년대 이후 컴퓨팅 하드웨어 성능이 급격히 향상되었다. 특히 GPU의 고성능 대규모 병렬 연산 지원과 분산 클라우드 환경 확대로 대규모 파라미터를 갖춘 심층 신경망을 실용적인 시간 내에 훈련할 수 있게 되었다.

10.1.1 생물학적 뉴런

생물학적 뉴런

이미지 출처2

  • 이 세포는 핵을 포함하는 세포체와 복잡한 구성 요소로 이루어져 있다.
  • 수상돌기라는 나뭇가지 모양의 돌기와 축삭돌기라는 긴 단일 돌기가 있다. 축삭돌기의 끝은 축삭끝가지라는 여러 갈래로 나뉘고, 이 가지 끝은 시냅스 말단이라는 미세한 구조이며 다른 뉴런의 수상돌기나 세포체에 연결된다.
  • 다시 정리하면 각각의 뉴런은 수상돌기를 통해 다른 뉴런에서 입력 신호를 받아서 축색돌기를 통해 다른 뉴런으로 신호를 내보낸다. 시냅스는 뉴런과 뉴런을 연결하는 역할을 한다. 출력신호는 입력된 신호가 모여서 일정한 용량을 넘어설 때 일어난다.3

  • Dendrite : 수상돌기, 다른 뉴런으로부터 신호를 수용하는 부분
  • Axon : 축삭돌기, 신호를 내보내는 부분
  • Synaptic terminals : 시냅스(synapse) 뉴런의 접합부, 다른 뉴런으로 부터 짧은 전기 자극 신호(signal)를 받음

10.1.2 뉴런을 사용한 논리 연산

image

  • 인공 뉴런 모델은 하나 이상의 이진(on/off) 입력과 이진 출력 하나를 가진다.
  • 인공 뉴런은 단순히 입력이 일정 개수만큼 활성화되었을 때 출력을 내보낸다.
  • ①은 항등함수를 의미하며, 뉴런 A가 활성화 되면 뉴런 C 또한 활성화된다.
  • ②는 논리곱 연산을 의미하며, 뉴런 A와 B가 모두 활성화될 때만 뉴런 C가 활성화된다.
  • ③은 논리합 연산을 의미하며, 뉴런 A와 B 둘 중 하나라도 활성화되면 C가 활성화된다.
  • ④는 어떤 입력이 뉴런의 활성화를 억제할 수 있다고 가정하면, 위의 그림에서 처럼 뉴런 A가 활성화 되고 B가 비활성화될 때 뉴런 C가 활성화된다.4

10.1.3 퍼셉트론

  • 퍼셉트론은 가장 간단한 인공 신경망 구조 중 하나이다.
  • 퍼셉트론은 TLU(Threshold Logic Unit)이라는 형태의 뉴런을 기반으로 하며, 아래의 그림과 같이 입력과 출력이 어떤 숫자고 각각의 입력에 각각 고유한 가중치($W$, weight)가 곱해진다.
  • TLU는 입력의 가중치 합을 계산 ($z = w_1x_1 + w_2x_2 + … + w_nx_n = x^tw$)한 뒤 계산된 합에 계단 함수(step function)를 적용하여 결과를 출력한다. 즉, $h_w(x) = step(z)$, 여기에서 $z=x^tw$이다.

image

  • 가장 널리 쓰이는 계단 함수는 헤비사이드 계단 함수(Heaviside step function)이다. 때로는 부호 함수(sign function)를 대신 사용하기도 한다.
\[heaviside(z) = \begin{cases} 0 \quad (z<0) \\ 1 \quad (z\ge0) \end{cases}\] \[sgn(z) = \begin{cases} -1 \quad (z<0) \\ 0 \quad (z=0) \\ +1 \quad (z>0) \end{cases}\]
  • 퍼셉트론은 단순히 하나의 층에 여러 개의 TLU가 모여있는 형태이며, 각 뉴런은 모든 입력에 연결되어 있다.

image

  • 입력층은 입력을 단순히 출력으로 내보낸다.
  • 모든 입력 뉴런은 편향 뉴런에 연결되어 있으며 편향 뉴런은 항상 1을 출력한다.

  • 완전 연결 층 (fully connected layer) : 한 층의 모든 뉴런이 이전 층의 모든 뉴런과 연결되어 있을 때 이를 완전 연결 층 또는 밀집 층 (dense layer)이라고 부른다.

  • TLU로 구성된 퍼셉트론 하나를 거쳐 나오는 식은 다음과 같이 나타낼 수 있다.
    • $X$ : 입력 특성의 행렬. 행은 샘플, 열은 특성
    • $W$ : 편향 뉴런을 제외한 모든 연결 가중치를 담은 행렬
    • $b$ : 편향 벡터
    • $\phi$ : 활성화 함수. 인공 뉴런이 TLU일 때 이 함수는 계단 함수
\[h_{W, b}(X) = \phi(XW+b)\]
  • 퍼셉트론의 학습 규칙은 오차가 감소되도록 뉴런의 연결을 강화시킨다.
  • 즉, 퍼셉트론에 한 번에 하나씩 샘플이 주입되면 각 샘플에 대해 예측을 출력하고 잘못된 예측을 하는 모든 출력 뉴런에 대해 올바른 예측을 만들 수 있도록 아래 식처럼 입력의 연결 가중치를 강화한다.
    • $w_{i,j}$ : i번째 입력 뉴런과 j번째 출력 뉴런 사이의 연결 가중치
    • $x_i$ : 현재 훈련 샘플의 i번째 뉴런의 입력값
    • $\hat y_j$ : 현재 훈련 샘플의 j번째 출력 뉴런의 출력값
    • $y_j$ : 현재 훈련 샘플의 j번째 출력 뉴런의 타깃값
    • $\eta$ : 학습률
\[w_{i,j}^{(next)} = w_{i,j} + \eta(y_j - \hat y_j)x_i\]
  • 각 출력 뉴런의 결정 경계는 선형이므로 퍼셉트론은 복잡한 패턴을 학습하지 못한다.
  • 하지만 훈련 샘플이 선형적으로 구분될 수 있다면 이 알고리즘이 정답에 수렴한다는 것을 증명했는데, 이를 퍼셉트론 수렴 이론 (perceptron convergence theorem)이라고 한다.

  • 사이킷런의 Perceptron 클래스 사용5
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_) # 부채붓꽃(Iris-Setosa)인가?

per_clf = Perceptron()
per_clf.fit(X, y)

y_pred = per_clf.predict([[2, 0.5]])
y_pred
# array([1])
  • 퍼셉트론의 결정 경계
a = -per_clf.coef_[0][0] / per_clf.coef_[0][1]
b = -per_clf.intercept_[0] / per_clf.coef_[0][1]

axes = [0, 5, 0, 2]

x0, x1 = np.meshgrid(
        np.linspace(axes[0], axes[1], 500).reshape(-1, 1),
        np.linspace(axes[2], axes[3], 200).reshape(-1, 1),
    )
X_new = np.c_[x0.ravel(), x1.ravel()]
y_predict = per_clf.predict(X_new)
zz = y_predict.reshape(x0.shape)

plt.figure(figsize=(10, 4))
plt.plot(X[y==0, 0], X[y==0, 1], "bs", label="Iris-Setosa 아님")
plt.plot(X[y==1, 0], X[y==1, 1], "yo", label="Iris-Setosa")

plt.plot([axes[0], axes[1]], [a * axes[0] + b, a * axes[1] + b], "k-", linewidth=3)
from matplotlib.colors import ListedColormap
custom_cmap = ListedColormap(['#9898ff', '#fafcaa'])

plt.contourf(x0, x1, zz, cmap=custom_cmap)
plt.xlabel("꽃잎 길이", fontsize=14)
plt.ylabel("꽃잎 너비", fontsize=14)
plt.legend(loc="lower right", fontsize=14)
plt.axis(axes)

plt.show()

image

  • 퍼셉트론은 로지스틱 회귀 분류기와 다르게 클래스 확률을 제공하지 않으며 고정된 기본값을 기준으로 예측을 만든다.
  • 퍼셉트론을 여러 개 쌓아올려 다층 퍼셉트론(MLP; Multilayer Perceptron) 구조를 만들면 XOR 문제를 풀 수 있다.

10.1.4 다층 퍼셉트론과 역전파

  • 다층 퍼셉트론은 입력층 하나와 은닉층(hidden layer)이라 부르는 하나 이상의 TLU 층과 출력층(output layer)이라 부르는 마지막 하나의 TLU 층으로 구성된다.
  • 입력층과 가까운 층을 보통 하위 층(lower layer)이라 부르고 출력에 가까운 층을 상위 층(upper layer)이라 부른다.
  • 은닉층을 여러 개 쌓아 올린 인공 신경망을 심층 신경망(DNN; Deep Neural Network)이라고 한다.

image

  • 역전파(Backpropagation) 훈련 알고리즘은 후진 모드 자동 미분(reverse-mode autodiff) 기법을 활용하여 네트워크를 정방향, 역방향으로 두 번 통과하는 것만으로 모든 모델 파라미터에 대한 오차 그레이디언트를 정확히 계산한다.
  • 즉, 역전파 알고리즘은 오차를 감소시키기 위해 각 연결 가중치와 편향값이 어떻게 바뀌어야 하는지 찾아낸다.

image

알고리즘

  • 한 번에 하나의 미니배치씩 진행하여 전체 훈련 세트를 처리하는 과정을 반복하며, 각 반복을 에포크 (epoch)라고 한다.
  • 각 미니배치는 네트워크의 입력층으로 전달되어 첫 번째 은닉층으로 보내진다.
  • 그 다음 해당 층에 있는 모든 뉴런의 출력을 계산한다. 이 결과는 다음 층으로 전달된다.
  • 다시 출력을 계산하여 다음 층으로 전달되며 마지막 출력층의 출력을 계산할 때까지 계속된다. 이것이 정방향 계산(forward pass)이다.
  • 그 다음 알고리즘이 네트워크의 출력 오차를 측정한다.
  • 그 다음 각 출력 연결이 오차에 기여하는 정도를 계산한다. 이는 미분의 연쇄 법칙(chain rule)을 적용하여 해석적으로 정확하게 수행한다.
  • 그 다음 또 연쇄 법칙을 사용하여 이전 층의 연결 가중치가 이 오차의 기여하는 정도를 파악한다.
  • 이런 식으로 입력층에 도달할 때까지 오차 그레이디언트를 뒤쪽으로 전달한다. 이것이 역방향 계산(reverse pass)이다.
  • 마지막으로 알고리즘은 방금 계산한 오차 그레이디언트를 사용하여 네트워크에 있는 모든 연결 가중치에 경사 하강법 단계를 수행한다.

다시 정리 6

  1. 각 훈련 샘플에 대해 역전파 알고리즘이 먼저 예측을 만들고(정방향 계산) 오차를 측정한다.
  2. 그다음 역방향으로 각 층을 거치면서 각 연결이 오차에 기여한 정도를 측정한다. (역방향 계산)
  3. 이 오차가 감소하도록 가중치를 조정한다. (경사 하강법 단계)

왜 활성화 함수가 필요할까?

  • 선형 변환을 여러 개 연결해도 얻을 수 있는 것은 선형 변환뿐이다.
  • 층 사이에 비선형성을 추가하지 않으면 아무리 층을 많이 쌓아도 하나의 층과 동일해진다. 이런 층으로는 복잡한 문제를 풀 수 없다.
  • 반대로 비선형 활성화 함수가 있는 충분히 큰 심층 신경망은 이론적으로 어떤 연속 함수도 근사할 수 있다.

image

  • 로지스틱(시그모이드) 함수 : $\sigma(z) = \frac {1}{1+exp(-z)}$
  • 하이퍼볼릭 탄젠트 함수 (쌍곡 탄젠트 함수) : $\tanh(z) = 2\sigma(2z) - 1$
  • ReLU 함수 : $ReLU(z)=max(0, z)$
    • ReLU 함수의 값이 0보다 클 때 기울기는 항상 1이므로 오차 그레이디언트를 그대로 역전파시킨다.
    • ReLU 함수 값이 0보다 작거나 같을 때는 오차 그레이디언트를 역전파하지 않는다.

로지스틱 함수나 하이퍼볼릭 탄젠트 함수는 출력 범위가 0~1 또는 -1~1 사이로 한정되어 양극단에서 기울기가 급격히 감소하므로 오차 그레이디언트를 잘 역전파하지 못한다.

10.1.5 회귀를 위한 다층 퍼셉트론

  • 회귀 MLP의 전형적인 구조
하이퍼파라미터 일반적인 값
입력 뉴런수 특성마다 하나 (예를 들어 MNIST의 경우 28x28=784)
은닉층 수 문제에 따라 다름. 일반적으로 1에서 5 사이
은닉층의 뉴런 수 문제에 따라 다름. 일반적으로 10에서 100사이
출력 뉴런 수 예측 차원마다 하나
은닉층의 활성화 함수 ReLU
출력층의 활성화 함수 1. 없음 / 2. 출력이 양수일 때: ReLU 또는 softplus / 3. 출력을 특정 범위로 제한할 때: logistic(0~1) 또는 tanh(-1~1) 사용
손실 함수 MSE나 (이상치가 있다면)MAE
  • 출력 뉴런 수 : 예측해야 하는 값의 수에 따라 출력 뉴런 설정

10.1.6 분류를 위한 다층 퍼셉트론

  • 다층 퍼셉트론은 분류 작업에도 사용할 수 있다.
  • 이진 분류 문제에서는 로지스틱 활성화 함수를 가진 하나의 출력 뉴런만 필요하다.
    • 출력은 0과 1사이의 실수이다.
  • 다층 퍼셉트론은 다중 레이블 이진 분류 (multilabel binary classification) 문제를 쉽게 처리할 수 있다.
  • 각 샘플이 3개 이상의 클래스 중 한 클래스에만 속할 수 있다면 (예를 들어 숫자 이미지 분류에서 클래스 0에서 9까지) 클래스마다 하나의 출력 뉴런이 필요하다. 출력층에는 소프트맥스 활성화 함수를 사용해야 한다.
    • 소프트맥스 함수는 모든 예측 확률을 0과 1 사이로 만들고 더했을 때 1이 되도록 만든다. 이를 다중 분류 (multiclass classifcation)라고 부른다.
  • 확률 분포를 예측해야 하므로 손실 함수에는 일반적으로 크로스 엔트로피 손실(cross-entropy loss)을 선택하는 것이 좋다.

image

  • 분류 MLP의 전형적인 구조
하이퍼파라미터 이진 분류 다중 레이블 분류 다중 분류
입력층과 은닉층 회귀와 동일 회귀와 동일 회귀와 동일
출력 뉴런 수 1개 레이블마다 1개 클래스마다 1개
출력층의 활성화 함수 로지스틱 함수 로지스틱 함수 소프트맥스 함수
손실 함수 크로스 엔트로피 크로스 엔트로피 크로스 엔트로피

10.1 절을 마치며

지금까지 생물학적 뉴런의 개념에서 출발하여 TLU, 퍼셉트론, 그리고 역전파 알고리즘과 활성화 함수를 갖춘 심층 신경망(MLP)의 수학적·이론적 배경을 살펴보았다. 이제 이러한 신경망 이론을 바탕으로 고수준 딥러닝 API인 케라스(Keras)를 활용하여 실제 데이터셋에 대한 분류 및 회귀 모델을 구축하고 훈련하는 방법을 알아본다.


10.2 케라스로 다층 퍼셉트론 구현하기

  • 케라스는 모든 종류의 신경망을 손쉽게 만들고 훈련, 평가, 실행할 수 있는 고수준 딥러닝 API이다.
  • 텐서플로와 케라스 다음으로 가장 인기 있는 딥러닝 라이브러리는 페이스북 파이토치 (PyTorch)이다.

10.2.1 텐서플로 2 설치

10.2.2 시퀀셜 API를 사용하여 이미지 분류기 만들기

# 케라스를 사용하여 데이터셋 적재하기
import keras
import tensorflow as tf

fashion_mnist = tf.keras.datasets.fashion_mnist
(X_train_full, y_train_full), (X_test, y_test) = fashion_mnist.load_data()

print(X_train_full.shape) # (60000, 28, 28)
print(X_train_full.dtype) # uint8

# 경사 하강법으로 신경망을 훈련하기 때문에 입력 특성의 스케일을 조정해야 한다. 255(최대 범위)로 나누어 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

# 클래스 이름의 리스트 만들기
class_names = ["T-shirt/top", "Trouser", "Pullover", "Dress", "Coat", "Sandal", "Shirt", "Sneaker", "Bag", "Ankle boot"]

시퀀셜 API를 사용하여 모델 만들기

from tensorflow.keras.layers import Flatten, Dense
from tensorflow.keras.models import Sequential

model = Sequential() # 순서대로 연결된 층을 일렬로 쌓아서 구성한다.
model.add(Flatten(input_shape=[28,28])) # 모델의 첫 번째 층이므로 input_shape를 지정해야 한다. 이 층은 어떤 모델 파라미터도 가지지 않고 간단한 전처리를 수행할 뿐이다. 여기에는 배치 크기를 제외하고 샘플의 크기만 써야 한다. 
model.add(Dense(300, activation='relu')) # 뉴런 300개를 가진 Dense 은닉층을 추가한다. 이 층은 ReLU 활성화 함수를 사용한다. Dense 층마다 각자 가중치 행렬을 관리한다. 이 행렬에는 층의 뉴런과 입력 사이의 모든 연결 가중치가 포함된다. 또한 (뉴런마다 하나씩 있는) 편향도 벡터로 관리한다. 
model.add(Dense(300, activation='relu'))
model.add(Dense(10, activation='softmax')) # (클래스마다 하나씩) 뉴런 10개를 가진 Dense 출력층을 추가한다. 배타적인 클래스이므로 소프트맥스 활성화 함수를 사용한다.
model.summary()

Model: "sequential"
# _________________________________________________________________
# Layer (type)                 Output Shape              Param #   
# =================================================================
# flatten (Flatten)            (None, 784)               0         
# _________________________________________________________________
# dense (Dense)                (None, 300)               235500    
# _________________________________________________________________
# dense_1 (Dense)              (None, 300)               90300     
# _________________________________________________________________
# dense_2 (Dense)              (None, 10)                3010      
# =================================================================
# Total params: 328,810
# Trainable params: 328,810
# Non-trainable params: 0
# _________________________________________________________________
  • 첫 번째 은닉층은 784 x 300개의 연결 가중치와 300개의 편향이 있다.
  • 파라미터 수가 많으면 훈련 데이터를 학습하기 충분한 유연성을 가지지만, 과대적합되기 쉽다.

모델 컴파일

  • 모델을 만든 후 compile() 메서드를 호출하여 사용할 손실 함수와 옵티마이저를 지정해야 한다.
model.compile(loss="sparse_categorical_crossentropy", # 클래스가 배타적이므로(0~9)
              optimizer="sgd", # 확률적 경사 하강법
              metrics=["accuracy"])
  • 레이블이 샘플마다 클래스의 타깃 확률을 가지고 있다면, categorical_crossentropy 손실을 사용해야 한다.
  • 이진 분류나 다중 레이블 이진 분류를 수행한다면, 출력층에 sigmoid 함수를 사용하고 binary_crossentropy 손실을 사용한다.

모델 훈련과 평가

  • 모델을 훈련하려면 fit() 메서드를 호출한다.
history = model.fit(X_train, y_train, epochs=30,
                    validation_data=(X_valid, y_valid))

# Epoch 1/30
# 1719/1719 [==============================] - 12s 6ms/step - loss: 1.0080 - accuracy: 0.6764 - val_loss: 0.5268 - val_accuracy: 0.8168
# Epoch 2/30
# 1719/1719 [==============================] - 10s 6ms/step - loss: 0.5108 - accuracy: 0.8239 - val_loss: 0.4558 - val_accuracy: 0.8476
# Epoch 3/30
# 1719/1719 [==============================] - 11s 6ms/step - loss: 0.4533 - accuracy: 0.8430 - val_loss: 0.4098 - val_accuracy: 0.8634
# Epoch 4/30
# 1719/1719 [==============================] - 10s 6ms/step - loss: 0.4222 - accuracy: 0.8512 - val_loss: 0.4230 - val_accuracy: 0.8574
# Epoch 5/30
# 1719/1719 [==============================] - 10s 6ms/step - loss: 0.4025 - accuracy: 0.8583 - val_loss: 0.4019 - val_accuracy: 0.8598
# Epoch 6/30
# 1719/1719 [==============================] - 10s 6ms/step - loss: 0.3807 - accuracy: 0.8658 - val_loss: 0.3893 - val_accuracy: 0.8618
# Epoch 7/30
# 1719/1719 [==============================] - 11s 6ms/step - loss: 0.3699 - accuracy: 0.8711 - val_loss: 0.3613 - val_accuracy: 0.8748
# ...
# Epoch 29/30
# 1719/1719 [==============================] - 10s 6ms/step - loss: 0.2322 - accuracy: 0.9151 - val_loss: 0.3025 - val_accuracy: 0.8916
# Epoch 30/30
# 1719/1719 [==============================] - 10s 6ms/step - loss: 0.2277 - accuracy: 0.9185 - val_loss: 0.3012 - val_accuracy: 0.8912
  • 케라스는 에포크가 끝날 때마다 검증 세트를 사용해 손실과 추가적인 측정 지표를 계산한다.
  • 훈련 세트 성능이 검증 세트 성능에 비해 현저히 높거나 검증 손실이 증가하기 시작한다면 모델이 훈련 세트에 과대적합(overfitting)되었을 가능성이 높다.
model.evaluate(X_test, y_test)
# 학습 곡선
import pandas as pd
import matplotlib.pyplot as plt

pd.DataFrame(history.history).plot(figsize=(8, 5))
plt.grid(True)
plt.gca().set_ylim(0, 1) # 수직축 범위를 0~1 사이로 설정
plt.show()

output

  • 모델 성능이 만족스럽지 않다면 처음으로 되돌아가서 하이퍼파라미터를 튜닝해야 한다.
  • 제일 먼저 확인할 것은 학습률이다. 학습률을 낮추거나 높여보고 옵티마이저를 바꾸거나 층 개수, 층이 뉴런 개수, 은닉층이 사용하는 활성화 함수 등을 튜닝해본다. 배치 크기를 바꾸어 볼 수도 있다.
  • 만족스러운 검증 정확도를 얻었다면 일반화 오차를 추정하기 위해 테스트 세트로 모델을 평가해야 한다. evaluate() 메서드를 사용한다.

모델을 사용해 예측 만들기

  • 모델의 predict() 메서드를 사용해 새로운 샘플에 대해 예측을 만들 수 있다.

10.2.3 시퀀셜 API를 사용하여 회귀용 다층 퍼셉트론 만들기

  • 캘리포니아 주택 가격 데이터셋을 사용해 회귀 문제를 해결해보자.
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_test = 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)
  • 시퀀셜 API를 사용하여 회귀용 MLP를 구축, 훈련, 평가, 예측을 만들어본다.
from tensorflow.keras.layers import Dense
from tensorflow.keras.models import Sequential

model = Sequential([
    Dense(30, activation="relu", input_shape=X_train.shape[1:]),
    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)
X_new = X_test[:3] # 새로운 샘플이라고 가정
y_pred = model.predict(X_new)

케라스 Sequential 클래스 활용의 장단점

  • 장점 : Sequential 모델은 계층 구조가 직관적이며 순차적인 신경망을 빠르게 구성할 수 있다.
  • 단점 : 토폴로지가 복잡하거나 다중 입력 또는 다중 출력을 가진 신경망을 구현하기 어렵다.

10.2.4 함수형 API를 사용해 복잡한 모델 만들기

  • 순차적이지 않은 신경망의 한 예는 와이드 & 딥 (Wide & Deep) 신경망이다. 7
  • 이 신경망은 입력의 일부 또는 전체가 출력층에 바로 연결된다.

  • 이 구조를 사용하면 신경망이 (깊은 경로를 통해서) 복잡한 패턴과 (짧은 경로를 통해서) 간단한 규칙을 모두 학습할 수 있다.
from tensorflow.keras.layers import Input, Dense, concatenate
from tensorflow.keras.models import Model

# Input 객체를 만든다. shape와 dtype을 포함하여 모델의 입력을 정의한다.
input_ = Input(shape=X_train.shape[1:])
# 30개의 뉴런과 ReLU 활성화 함수를 가진 Dense 은닉층을 만든다. 이 층은 만들어지자마자 입력과 함께 함수처럼 호출된다. (케라스가 층을 연결하는 방식)
hidden1 = Dense(30, activation="relu")(input_)
hidden2 = Dense(30, activation="relu")(hidden1)
# concatenate 층을 만들고 함수처럼 호출하여 두 번째 은닉층의 출력과 입력을 연결한다.
concat = concatenate([input_, hidden2])
# 하나의 뉴런과 활성화 함수가 없는 출력층을 만들고 concatenate 층의 결과로 호출한다.
output = Dense(1)(concat)
# 사용할 입력과 출력을 지정하여 케라스 Model 객체를 만든다.
model = Model(inputs=[input_], outputs=[output])
  • 만약 일부 특성은 짧은 경로로 전달하고 다른 특성은 깊은 경로로 전달하고 싶다면 다중 입력을 사용하면 된다. 8

from tensorflow.keras.layers import Input, Dense, concatenate
from tensorflow.keras.models import Model

input_A = Input(shape=[5], name="wide_input")
input_B = Input(shape=[6], name="deep_input")
hidden1 = Dense(30, activation="relu")(input_B)
hidden2 = Dense(30, activation="relu")(hidden1)
concat = concatenate([input_A, hidden2])
output = Dense(1, name="output")(concat)
model = Model(inputs=[input_A, input_B], outputs=[output])
  • 모델을 컴파일할 때 이전과 동일하지만 fit() 메서드를 호출할 때 하나의 입력 행렬 X_train을 전달하는 대신 입력마다 하나씩 행렬의 튜플((X_train_A, X_train_B))을 전달해야 한다.
from tensorflow.keras.optimizers import SGD

model.compile(loss="mse", optimizer=SGD(lr=1e-3))

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))
  • 여러 출력이 필요한 경우도 존재한다.
    • 회귀 작업과 분류 작업을 함께 수행하는 경우
    • 동일한 데이터에서 독립적인 여러 작업을 수행할 때
    • 규제 기법으로 사용하는 경우 (보조 출력을 활용해 과대적합을 줄이고 일반화 성능을 높임)

# 은닉층2까지는 위 코드와 동일
output = Dense(1, name="main_output")(concat)
aux_output = Dense(1, name="aux_output")(hidden2)
model = Model(inputs=[input_A, input_B], outputs=[output, aux_output])
  • 각 출력은 자신만의 손실 함수가 필요하다.
  • 따라서 모델을 컴파일할 때 손실의 리스트를 전달해야 한다.
# 케라스는 나열된 손실을 모두 더하여 최종 손실을 구해 훈련에 사용한다.
# 보조 출력보다 주 출력에 더 관심이 많다면 주 출력의 손실에 더 많은 가중치를 부여한다.
model.compile(loss=['mse', 'mse'], loss_weights=[0.9, 0.1], optimizer='sgd')
  • 모델을 훈련할 때 각 출력에 대해 레이블을 제공해야 한다.
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]))
total_loss, main_loss, aux_loss = model.evaluate([X_test_A, X_test_B], [y_test, y_test])
y_pred_main, y_pred_aux = model.predict([X_new_A, X_new_B])

10.2.5 서브클래싱 API로 동적 모델 만들기

  • 시퀀셜 API와 함수형 API는 모두 선언적(declarative)이다.
    • 사용할 층과 연결 방식을 먼저 정의해야 한다. 그 다음 모델에 데이터를 주입하여 훈련이나 추론을 시작할 수 있다.
    • 이 방식의 장점은 모델을 저장하거나 복사, 공유하기 쉽고 모델의 구조를 출력하거나 분석하기 쉽다. 또한 프레임워크가 크기를 변환하고 타입을 검사할 수 있어 에러를 일찍 발견할 수 있다.
    • 단점은 정적이기 때문에 반복문이나 조건문, 기타 동적인 구조를 다루기 어렵다.
  • 이를 해결하기 위해 동적인 구조를 지원하는 서브클래싱 API를 사용한다.
  • Model 클래스를 상속받은 다음 생성자에서 필요한 층을 만들고 call() 메서드 안에 수행하려는 연산을 작성한다.
import tensorflow as tf

class WideAndDeepModel(tf.keras.Model):
    def __init__(self, units=30, activation="relu", **kwargs):
        super().__init__(**kwargs) # 표준 매개변수를 처리 (예를 들면 name)
        self.hidden1 = Dense(units, activation=activation)
        self.hidden2 = Dense(units, activation=activation)
        self.main_output = Dense(1)
        self.aux_output = Dense(1)

    def call(self, inputs):
        input_A, input_B = inputs
        hidden1 = self.hidden1(input_B)
        hidden2 = self.hidden2(hidden1)
        concat = concatenate([input_A, hidden2])
        main_output = self.main_output(concat)
        aux_output = self.aux_output(hidden2)
        return main_output, aux_output

model = WideAndDeepModel()

10.2.6 모델 저장과 복원

  • 시퀀셜 API나 함수형 API를 사용하면 훈련된 케라스 모델을 HDF5 포맷(h5)으로 손쉽게 저장하고 복원할 수 있다.
model = Sequential([...])
model.compile([...])
model.fit([...])
model.save("my_keras_model.h5")

# 복원
from tensorflow.keras.models import load_model

model = load_model("my_keras_model.h5")
  • 서브클래싱 모델은 save_weights()와 load_weights()를 사용하여 모델 파라미터를 저장하고 복원해야 한다.

10.2.7. 콜백 사용하기

  • fit() 메서드의 callbacks 매개변수를 사용하여 훈련 시작이나 끝, 각 에포크의 시작이나 끝 등에 호출할 객체 리스트를 지정할 수 있다.9
import tensorflow as tf
from tensorflow.keras.callbacks import ModelCheckpoint

checkpoint_cb = ModelCheckpoint('my_keras_model.h5')
history = model.fit(X_train, y_train, epochs=10, callbacks=[checkpoint_cb])
  • ModelCheckpoint에서 save_best_only=True로 지정하면 최상의 검증 세트 점수에서만 모델을 저장한다.
import tensorflow as tf
from tensorflow.keras.callbacks import ModelCheckpoint
from tensorflow.keras.models import load_model

checkpoint_cb = 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 = load_model("my_keras_model.h5")
  • EarlyStopping 콜백은 일정 에포크 동안 검증 세트에 대한 점수가 향상되지 않으면 훈련을 멈춘다.
  • 모델이 향상되지 않으면 훈련이 자동으로 중지되기 때문에 에포크의 숫자를 크게 지정해도 된다.
  • restore_best_weights=True : 최상의 모델 복원 기능 설정
    • 훈련이 끝난 후 최적 가중치 바로 복원
from tensorflow.keras.callbacks import EarlyStopping

early_stopping_cb = EarlyStopping(patience=10,
                                  restore_best_weights=True)
history = model.fit(X_train, y_train, epochs=100,
                    validation_data=(X_valid, y_valid),
                    callbacks=[checkpoint_cb, early_stopping_cb])

10.3.1 은닉층 개수

  • 심층 신경망은 계층적 특징 구조를 학습함으로써 복잡한 함수를 모델링할 때 얕은 신경망 대비 적은 수의 뉴런으로도 높은 파라미터 효율성을 발휘한다.
  • 하나 또는 두 개의 은닉층만으로도 많은 문제를 꽤 잘 해결할 수 있다.

10.3.2 은닉층의 뉴런 개수

  • 네트워크가 과대적합이 시작되기 전까지 점진적으로 뉴런 수를 늘릴 수 있다.
  • 모델이 과대적합되지 않도록 조기 종료(Early Stopping)나 가중치 규제(L1/L2) 기법을 함께 적용한다.

한계 및 트레이드오프

  • 단일 퍼셉트론의 선형 분리 한계: 단일 층 퍼셉트론(TLU)은 결정 경계가 선형 평면으로 제한되므로, XOR 문제와 같은 비선형 분류 문제를 단독으로 해결할 수 없다.
  • 다층 퍼셉트론의 과대적합 및 규제 트레이드오프: 은닉층과 뉴런 수를 늘릴수록 네트워크의 표현 용량은 기하급수적으로 증가하지만, 그에 비례해 과대적합 위험이 커진다. 이를 억제하기 위해 가중치 규제(L1/L2)나 드롭아웃을 과도하게 적용할 경우 모델의 표현력이 저하되어 과소적합이 발생할 수 있다.
  • 역전파 시 그레이디언트 소실/폭주: 로지스틱이나 하이퍼볼릭 탄젠트와 같은 포화 활성화 함수는 심층 신경망에서 역전파 시 그레이디언트 소실을 유발하며, ReLU를 사용하더라도 비정상적으로 큰 학습률에서는 뉴런이 영구히 비활성화되는 죽은 ReLU(dying ReLU) 현상이 나타날 수 있다.
  • 콜백 및 조기 종료 트레이드오프: EarlyStopping의 patience 값을 지나치게 작게 설정하면 일시적인 검증 손실 진동에 의해 최적점에 도달하기 전 훈련이 조기 종료될 수 있고, 반대로 너무 크게 설정하면 불필요한 연산 자원이 낭비되거나 과대적합된 가중치 영역으로 진입할 수 있다.

핵심 정리

  • 인공 신경망과 퍼셉트론: 인공 신경망은 생물학적 뉴런 네트워크에서 영감을 받았으며, TLU 기반의 퍼셉트론에서 다층 퍼셉트론(MLP)으로 발전하며 비선형 문제를 해결할 수 있게 되었다.
  • 역전파 알고리즘과 활성화 함수: 정방향 계산 후 오차를 역방향으로 전달하며 연쇄 법칙으로 그레이디언트를 계산하는 역전파 알고리즘과, 네트워크에 비선형 표현력을 부여하는 활성화 함수(ReLU 등)가 심층 신경망의 핵심이다.
  • 케라스 시퀀셜 API (Sequential API): 순차적인 층 구조를 빠르게 구축할 수 있는 API로, Flatten, Dense 층을 쌓고 compile(loss, optimizer, metrics) 후 fit()으로 손쉽게 이미지 분류기 및 회귀 모델을 훈련한다.
  • 함수형 API (Functional API)와 복잡한 토폴로지: 다중 입력, 다중 출력, 스킵 연결(Wide & Deep)과 같이 비순차적이고 유연한 네트워크 구조를 구축할 때 함수형 API를 활용한다.
  • 서브클래싱 API (Subclassing API): tf.keras.Model을 상속받아 동적 제어 흐름(반복문, 조건문)을 포함하는 유연한 모델을 구현할 수 있으며 디버깅이 용이하다.
  • 콜백(Callback)을 통한 훈련 제어: ModelCheckpoint로 최적 검증 점수 모델을 자동 저장하고, EarlyStopping을 통해 과대적합 발생 전 최적 가중치를 복원하며 조기 종료한다.
  • 하이퍼파라미터 튜닝: 은닉층 개수와 층당 뉴런 수는 문제의 복잡도에 따라 점진적으로 확장하며, 과대적합 방지를 위해 규제 및 조기 종료 기법과 함께 최적 조합을 탐색한다.

References

  1. Aurélien Géron, Hands-On Machine Learning with Scikit-Learn, Keras, and TensorFlow, 2nd Edition, Chapter 10 Notebook: https://github.com/ageron/handson-ml2/blob/master/10_neural_nets_with_keras.ipynb ↩

  2. 강민형 교수, [인공신경망과 서포트벡터머신] 신경망과 서포트벡터머신의 이해, https://www.youtube.com/watch?v=cSdbs3x4YBw 및 Eric R. Kandel et al., Principles of Neural Science, 5th ed. ↩

  3. 생물학적 뉴런의 활동전위 및 시냅스 신호 전달 메커니즘, https://brunch.co.kr/@gdhan/6 ↩

  4. Warren S. McCulloch, Walter Pitts (1943), “A logical calculus of the ideas immanent in nervous activity”, The Bulletin of Mathematical Biophysics, 5(4), pp. 115–133. ↩

  5. Scikit-Learn 공식 문서: sklearn.linear_model.Perceptron, https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.Perceptron.html ↩

  6. David E. Rumelhart, Geoffrey E. Hinton, Ronald J. Williams (1986), “Learning representations by back-propagating errors”, Nature, 323, pp. 533–536. ↩

  7. Heng-Tze Cheng et al. (2016), “Wide & Deep Learning for Recommender Systems”, Proceedings of the 1st Workshop on DLRS, arXiv:1606.07792 및 handson-ml2 Chapter 10 슬라이드: https://formal.hknu.ac.kr/handson-ml2/slides/handson-ml2-10.slides.html#/79 ↩

  8. TensorFlow 공식 가이드: Keras Functional API, https://www.tensorflow.org/guide/keras/functional 및 handson-ml2 Chapter 10 슬라이드: https://formal.hknu.ac.kr/handson-ml2/slides/handson-ml2-10.slides.html#/82 ↩

  9. TensorFlow 공식 API 문서: Keras Callbacks (EarlyStopping, ModelCheckpoint), https://www.tensorflow.org/api_docs/python/tf/keras/callbacks/EarlyStopping 및 handson-ml2 Chapter 10 슬라이드: https://formal.hknu.ac.kr/handson-ml2/slides/handson-ml2-10.slides.html#/86 ↩

댓글남기기