케라스 API를 사용한 사용자 정의 모델 만들기 with 텐서플로 2.3+2.4

DLD(Daejeon Learning Day) 2020을 위해 작성된 노트북입니다.

In [1]:
import tensorflow as tf

tf.__version__
Out[1]:
'2.3.0'

MNIST 손글씨 숫자 데이터 적재

In [2]:
(X_train, y_train), (X_test, y_test) = tf.keras.datasets.mnist.load_data()

X_train = X_train.reshape(-1, 784) / 255.
In [3]:
X_train.shape
Out[3]:
(60000, 784)

Sequential() 클래스와 함수형 API의 관계

Sequential():

시퀀셜 모델에 10개의 유닛을 가진 완전 연결 층을 추가합니다.

In [4]:
seq_model = tf.keras.Sequential()

seq_model.add(tf.keras.layers.Dense(units=10, 
                                    activation='softmax',
                                    input_shape=(784,)))

seq_model.summary()
Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense (Dense)                (None, 10)                7850      
=================================================================
Total params: 7,850
Trainable params: 7,850
Non-trainable params: 0
_________________________________________________________________
In [5]:
seq_model.compile(loss='sparse_categorical_crossentropy',
                  metrics=['accuracy'])
seq_model.fit(X_train, y_train, batch_size=32, epochs=2)
Epoch 1/2
1875/1875 [==============================] - 4s 2ms/step - loss: 0.4386 - accuracy: 0.8830
Epoch 2/2
1875/1875 [==============================] - 4s 2ms/step - loss: 0.3024 - accuracy: 0.9159
Out[5]:
<tensorflow.python.keras.callbacks.History at 0x7f2781d38ef0>

함수형 API:

함수형 API를 사용할 때는 Input()을 사용해 입력의 크기를 정의해야 합니다. 하지만 InputLayer 층이 추가되어 있습니다.

In [6]:
inputs = tf.keras.layers.Input(784)

outputs = tf.keras.layers.Dense(units=10,
                                activation='softmax')(inputs)  # __call()__ 메서드 호출
# dense = tf.keras.layers.Dense(units=10, activation='softmax')
# outputs = dense(inputs)

func_model = tf.keras.Model(inputs, outputs)

func_model.summary()
Model: "functional_1"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_1 (InputLayer)         [(None, 784)]             0         
_________________________________________________________________
dense_1 (Dense)              (None, 10)                7850      
=================================================================
Total params: 7,850
Trainable params: 7,850
Non-trainable params: 0
_________________________________________________________________
In [7]:
func_model.compile(loss='sparse_categorical_crossentropy',
                  metrics=['accuracy'])
func_model.fit(X_train, y_train, batch_size=32, epochs=2)
Epoch 1/2
1875/1875 [==============================] - 4s 2ms/step - loss: 0.4412 - accuracy: 0.8821
Epoch 2/2
1875/1875 [==============================] - 4s 2ms/step - loss: 0.3027 - accuracy: 0.9160
Out[7]:
<tensorflow.python.keras.callbacks.History at 0x7f277f245710>

Input의 정체는 무엇일까요? 이 함수는 InputLayer 클래스의 객체를 만들어 그 결과를 반환합니다.

In [8]:
type(tf.keras.layers.Input)
Out[8]:
function

사실 신경망의 입력층은 입력 그 자체입니다. InputLayer 객체의 입력 노드 출력을 그대로 Dense 층에 주입할 수 있습니다. 모든 층은 입력과 출력 노드를 정의합니다.

In [9]:
# inputs = tf.keras.layers.Input(784)

input_layer = tf.keras.layers.InputLayer(784)
inputs = input_layer._inbound_nodes[0].outputs

outputs = tf.keras.layers.Dense(units=10,
                                activation='softmax')(inputs)

input_layer_model = tf.keras.Model(inputs, outputs)

input_layer_model.summary()
Model: "functional_3"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_2 (InputLayer)         [(None, 784)]             0         
_________________________________________________________________
dense_2 (Dense)              (None, 10)                7850      
=================================================================
Total params: 7,850
Trainable params: 7,850
Non-trainable params: 0
_________________________________________________________________
In [10]:
input_layer_model.compile(loss='sparse_categorical_crossentropy', 
                          metrics=['accuracy'])
input_layer_model.fit(X_train, y_train, batch_size=32, epochs=2)
Epoch 1/2
1875/1875 [==============================] - 4s 2ms/step - loss: 0.4338 - accuracy: 0.8850
Epoch 2/2
1875/1875 [==============================] - 4s 2ms/step - loss: 0.3025 - accuracy: 0.9161
Out[10]:
<tensorflow.python.keras.callbacks.History at 0x7f277f193be0>

함수형 API를 사용한 모델은 layers 속성에 InputLayer 클래스를 포함합니다.

In [11]:
func_model.layers
Out[11]:
[<tensorflow.python.keras.engine.input_layer.InputLayer at 0x7f277f3be5c0>,
 <tensorflow.python.keras.layers.core.Dense at 0x7f27dc1d3a20>]

하지만 시퀀셜 모델은 layers 속성에 InputLayer 클래스가 보이지 않습니다.

In [12]:
seq_model.layers
Out[12]:
[<tensorflow.python.keras.layers.core.Dense at 0x7f2781d04978>]

모델은 감춰진 _layers 속성이 또 있습니다. 여기에서 InputLayer 클래스를 확인할 수 있습니다.

In [13]:
seq_model._layers
Out[13]:
[<tensorflow.python.keras.engine.input_layer.InputLayer at 0x7f2781d04f98>,
 <tensorflow.python.keras.layers.core.Dense at 0x7f2781d04978>]

또는 _input_layers 속성에서도 확인할 수 있습니다.

In [14]:
seq_model._input_layers, func_model._input_layers
Out[14]:
([<tensorflow.python.keras.engine.input_layer.InputLayer at 0x7f2781d04f98>],
 [<tensorflow.python.keras.engine.input_layer.InputLayer at 0x7f277f3be5c0>])
In [15]:
seq_model._output_layers, func_model._output_layers
Out[15]:
([<tensorflow.python.keras.layers.core.Dense at 0x7f2781d04978>],
 [<tensorflow.python.keras.layers.core.Dense at 0x7f27dc1d3a20>])

Model 클래스로 만든 func_model은 사실 Functional 클래스의 객체입니다. Model 클래스는 서브클래싱에 사용합니다.

In [16]:
func_model.__class__
Out[16]:
tensorflow.python.keras.engine.functional.Functional

시퀀셜 모델은 함수형 모델의 특별한 경우입니다. (Model --> Functional --> Sequential)

사용자 정의 층 만들기

tf.layers.Layer 클래스를 상속하고 build() 메서드에서 가중치를 만든다음 call() 메서드에서 연산을 구현합니다.

In [17]:
class MyDense(tf.keras.layers.Layer):
    
    def __init__(self, units, activation=None, **kwargs):
        # units와 activation 매개변수 외에 나머지 변수를 부모 클래스의 생성자로 전달합니다.
        super(MyDense, self).__init__(**kwargs)
        self.units = units
        # 문자열로 미리 정의된 활성화 함수를 선택합니다. e.g., 'softmax', 'relu'
        self.activation = tf.keras.activations.get(activation)
        
    def build(self, input_shape):
        # __call__() 메서드를 호출할 때 호출됩니다. 가중치 생성을 지연합니다.
        # 가중치와 절편을 생성합니다.
        self.kernel = self.add_weight(name='kernel', 
                                      shape=[input_shape[-1], self.units],
                                      initializer='glorot_uniform'   # 케라스의 기본 초기화
                                     )
        self.bias = self.add_weight(name='bias',
                                    shape=[self.units],
                                    initializer='zeros')
    
    def call(self, inputs):  # training=None은 training은 배치 정규화나 드롭아웃 같은 경우 사용
        # __call__() 메서드를 호출할 때 호출됩니다.
        # 실제 연산을 수행합니다. [batch_size, units]
        z = tf.matmul(inputs, self.kernel) + self.bias
        if self.activation:
            return self.activation(z)
        return z
In [18]:
inputs = tf.keras.layers.Input(784)
# Layer.__call__() --> MyDense().build() --> Layer.build() --> MyDense().call()
outputs = MyDense(units=10, activation='softmax')(inputs)

my_dense_model = tf.keras.Model(inputs, outputs)

my_dense_model.summary()
Model: "functional_5"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_3 (InputLayer)         [(None, 784)]             0         
_________________________________________________________________
my_dense (MyDense)           (None, 10)                7850      
=================================================================
Total params: 7,850
Trainable params: 7,850
Non-trainable params: 0
_________________________________________________________________
In [19]:
my_dense_model.compile(loss='sparse_categorical_crossentropy', 
                       metrics=['accuracy'])
my_dense_model.fit(X_train, y_train, batch_size=32, epochs=2)
Epoch 1/2
1875/1875 [==============================] - 4s 2ms/step - loss: 0.4339 - accuracy: 0.8830
Epoch 2/2
1875/1875 [==============================] - 4s 2ms/step - loss: 0.3017 - accuracy: 0.9154
Out[19]:
<tensorflow.python.keras.callbacks.History at 0x7f277f0d0e10>

사용자 정의 모델 만들기

In [20]:
# fit(), compile(), predict(), evaluate() 등의 메서드 제공
class MyModel(tf.keras.Model):
    
    def __init__(self):
        super(MyModel, self).__init__()
        self.output_layer = MyDense(units=10, activation='softmax')
    
    def call(self, inputs):
        return self.output_layer(inputs)
In [21]:
my_model = MyModel()

my_model.compile(loss='sparse_categorical_crossentropy', 
                       metrics=['accuracy'])
my_model.fit(X_train, y_train, batch_size=32, epochs=2)
Epoch 1/2
1875/1875 [==============================] - 4s 2ms/step - loss: 0.4412 - accuracy: 0.8818
Epoch 2/2
1875/1875 [==============================] - 4s 2ms/step - loss: 0.3026 - accuracy: 0.9162
Out[21]:
<tensorflow.python.keras.callbacks.History at 0x7f27681b61d0>

사용자 정의 훈련

In [22]:
class MyCustomStep(MyModel):
    
    def train_step(self, data):
        # fit()에서 전달된 데이터
        x, y = data

        # 그레이디언트 기록 시작
        with tf.GradientTape() as tape:
            # 정방향 계산
            y_pred = self(x)
            # compile() 메서드에서 지정한 손실 계산
            loss = self.compiled_loss(y, y_pred)

        # 훈련가능한 파라미터에 대한 그레이디언트 계산
        gradients = tape.gradient(loss, self.trainable_variables)
        # 파라미터 업데이트
        self.optimizer.apply_gradients(zip(gradients, self.trainable_variables))
        
        # TF 2.4에서는
        # self.optimizer.minimize(loss, self.trainable_variables, tape=tape)
        
        # compile() 메서드에서 지정한 지표 계산
        self.compiled_metrics.update_state(y, y_pred)
        
        # 현재까지 지표와 결괏값을 딕셔너리로 반환
        return {m.name: m.result() for m in self.metrics}
In [23]:
my_custom_step = MyCustomStep()

my_custom_step.compile(loss='sparse_categorical_crossentropy', 
                       metrics=['accuracy'])
my_custom_step.fit(X_train, y_train, batch_size=32, epochs=2)
Epoch 1/2
1875/1875 [==============================] - 4s 2ms/step - loss: 0.4383 - accuracy: 0.8842
Epoch 2/2
1875/1875 [==============================] - 4s 2ms/step - loss: 0.3022 - accuracy: 0.9160
Out[23]:
<tensorflow.python.keras.callbacks.History at 0x7f27680877b8>
In [ ]: