logo

텐서플로-케라스에서 시퀄션 모델, 함수형 API로 MLP 정의하고 훈련하는 법 📂머신러닝

텐서플로-케라스에서 시퀄션 모델, 함수형 API로 MLP 정의하고 훈련하는 법

개요

텐서플로에서는 케라스를 이용하여 쉽게 신경망을 정의할 수 있다. 아래에서는 Sequential()과 함수형 API로 간단한 MLP를 정의하고 훈련하는 법을 소개한다. 다만 Sequential()은 모델을 정의하는 자체만 쉬울 뿐이지, 이를 가지고 복잡한 구조를 설계하기엔 어렵다. 마찬가지로 함수형 API로 복잡한 구조를 설계할거면 keras.Model 클래스를 쓰는게 낫고, 더 복잡하고 자유로운 커스터마이징을 원한다면 아예 케라스 없이 저수준으로 구현하는 것이 낫다. 어떤 작업을 위해 딥러닝을 쓰느냐에 따라 다르겠지만, 본인이 이공계 연구자이고 전공에 딥러닝을 접목하고 싶다면 아래의 방법들을 주요하게 사용할 가능성은 낮다. 딥러닝을 처음 배우고 실습할 때 '이렇게 쓰는 거구나'하고 감을 잡는 수준이라고 보면 된다.

시퀀셜 모델

모델 정의

사인 함수 $\sin : \mathbb{R} \to \mathbb{R}$의 근사를 위해 입력과 출력의 차원이 1인 MLP를 다음과 같이 정의하자.

import tensorflow as tf
from tensorflow.keras import Sequential
from tensorflow.keras.layers import Dense

# 모델 정의
model = Sequential([Dense(10, input_dim = 1, activation = "relu"),
                    Dense(10, input_dim = 10, activation = "relu"),
                    Dense(1, input_dim = 10)])
model.summary() # output↓
# Model: "sequential_3"
# _________________________________________________________________
# Layer (type)                Output Shape              Param #   
# =================================================================
# dense_9 (Dense)             (None, 10)                20        
#                                                                 
# dense_10 (Dense)            (None, 10)                110       
#                                                                 
# dense_11 (Dense)            (None, 1)                 11        
#                                                                 
# =================================================================
# Total params: 141
# Trainable params: 141
# Non-trainable params: 0
# _________________________________________________________________

keras.layers.Dense()의 특징 중 하나는 입력의 차원을 적지 않아도 된다는 것이다. 왜 이런 허용을 뒀는지는 모르겠으나, 코드의 가독성을 위해서라면 (특히 다른 사람이 볼 수 있는 코드라면) 입력의 차원을 명시적으로 적는 것이 좋다. 이런 점 때문에 출력의 차원이 왼쪽, 입력의 차원이 오른쪽에 적힌다는 특징이 있다. 그래서 모델의 구조를 읽기 위해서는 아랍어도 아니고 오른쪽에서 왼쪽으로 읽어야한다. 만약에 선형층을 선형변환으로써의 행렬으로 간주했다면, $\mathbf{y} = A\mathbf{x}$이므로 입력이 오른쪽, 출력이 왼쪽에 오는 것이 자연스럽긴하다. 다만 텐서플로는 딱히 이런 수학적인 엄밀함을 고려해서 설계된 언어가 아니라서 이러한 이유 때문이라고 보기는 어렵다. 심지어 수학적 엄밀함을 엄청나게 따지는 줄리아에서도 선형층은 Dense(in, out)와 같이 구현되어있다. 이는 당연하게도 왼쪽에서 오른쪽으로 읽는 것이 편하고 알아보기 쉽기 때문이다. 애초에 $X$에서 $Y$로의 함수 $f$의 표기 자체가 $f : X \to Y$이고, (케라스를 제외한) 세상 어디에도 오른쪽에서 왼쪽으로의 매핑으로 묘사되는 함수는 없다.

데이터 생성

사인함수를 훈련할 것이므로 데이터를 사인함수의 함숫값으로 두고, 모델의 출력과 사인함수의 그래프를 비교해보면 다음과 같다.

# 데이터 생성
from math import pi

x = tf.linspace(0., 2*pi, num=1000)    # 입력 데이터
y = tf.sin(x)                          # 출력 데이터(label)

# 모델의 출력 확인
import matplotlib.pyplot as plt

plt.plot(x, model(x), label="model")
plt.plot(x, y, label="sin")
plt.legend()
plt.show()

훈련 및 결과

from tensorflow.keras.optimizers import Adam

model.compile(optimizer=Adam(learning_rate=0.001), loss='mse')
  • model.compile(optimizer, loss, metric)

.comile() 메서드로 옵티마이저와 손실함수를 지정한다. 다른 주요한 옵션으로는 metric이 있는데 이는 모델을 평가하는 함수를 말한다. 이는 loss와 같을 수도 있고, 다를 수도 있다. 가령 MLP로 MNIST 데이터 셋을 학습시킨다면 loss는 출력과 레이블의 mse이고, metric은 전체 데이터 중에서 예측에 성공한 비율이 될 것이다.

> model.fit(x, y, epochs=10000, batch_size=1000, verbose='auto')
.
.
.
Epoch 9998/10000
1/1 [==============================] - 0s 8ms/step - loss: 6.2260e-06
Epoch 9999/10000
1/1 [==============================] - 0s 4ms/step - loss: 6.2394e-06
Epoch 10000/10000
1/1 [==============================] - 0s 3ms/step - loss: 6.2385e-06
  • .fit() 메서드에 입력과 레이블, 에포크, 배치사이즈등을 입력하면 학습이 실행된다. verbose는 훈련 진행 경과를 어떻게 출력할지 정하는 옵션이다. 0, 1, 2중에서 선택할 수 있으며 0은 아무것도 출력하지 않는다. 나머지는 다음과 같은 양식으로 출력한다.
# verbose=1
Epoch (현재 에포크)/(전체 에포크)
(현재 배치)/(전체 배치) [==============================] - 0s 8ms/step - loss: 0.7884

# verbose=2
Epoch (현재 에포크)/(전체 에포크)
(현재 배치)/(전체 배치) - 0s - loss: 0.7335 - 16ms/epoch - 8ms/step

훈련이 끝나고 사인함수와 모델의 함숫값을 비교해보면 학습이 잘된 것을 알 수 있다.

함수형 API

Input() 함수와 Model() 함수로 레이어를 직접 연결시키는 방법이다. MLP와 같 같단한 모델이라면 그냥 위의 시퀀셜 모델로 정의하는 것이 훨씬 편하다. 위의 시퀀셜 모델에서 정의했던 신경망과 똑같은 구조의 모델을 정의하는 방법은 다음과 같다.

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

input = Input(shape=(10)) # 변수는 "출력의 차원 = 첫번째 층의 입력의 차원"
dense1 = Dense(10, activation = "relu")(input)
dense2 = Dense(10, activation = "relu")(dense1)
output = Dense(1)(dense2)

model = Model(inputs=input, outputs=output)
model.summary() # output↓
# Model: "model_10"
# _________________________________________________________________
#  Layer (type)                Output Shape              Param #
# =================================================================
#  input_13 (InputLayer)       [(None, 1)]               0
# 
#  dense_19 (Dense)            (None, 10)                20
# 
#  dense_20 (Dense)            (None, 10)                110
# 
#  dense_21 (Dense)            (None, 1)                 11
# 
# =================================================================
# Total params: 141
# Trainable params: 141
# Non-trainable params: 0
# _________________________________________________________________

Input은 인풋 레이어를 정의하는 함수이다. 정확하게는 레이어가 아니라 텐서이긴 하지만 중요한 것은 아니니 그냥 입력층이라고 받아들여도 좋다. 헷갈리는 점은 변수로 출력의 차원을 입력해야한다는 점이다. 그러니까 첫번째 층의 입력의 차원을 입력해야한다. 이를 정의하고나면 Dense 함수의 인풋으로 입력하여 명시적으로 직접 각 층을 연결한다. 마지막으로 Model 함수에서 인풋과 아웃풋을 인자로 넣으면 모델을 정의할 수 있다.

이후 모델을 .compile() 메서드로 컴파일하고, .fit() 메서드로 훈련시키는 과정은 위에서 소개한 바와 같다.

환경

  • OS: Windows11
  • Version: Python 3.9.13, tensorflow==2.12.0, keras==2.12.0