이글은 다음 문서를 참조합니다.

www.tensorflow.org/guide/keras/functional

(번역은 자력 + 파파고 + 구글 번역기를 사용하였으니, 부자연스럽더라도 양해바랍니다.)

2019/03/29 - [ML/tensorflow2.0(keras)] - tensorflow 2.0 keras Functional API (1)


Extending the API by writing custom layers

tf.keras에는 수많은 layer들이 담겨있습니다.

  • Convolutional layers : Conv1D, Conv2D, Conv3D, Conv2DTranspose, etc.
  • Pooling layers : MaxPooling1D, MaxPooling2D, MaxPooling3D, AveragePooling1D, etc.
  • RNN layers : GRU, LSTM, ConvLSTM2D, etc.
  • BatchNormalization, Dropout, Embedding, etc.

만약 원하는 layer가 없을 경우 직접 정의하는 것도 가능합니다.

  • 모든 layer는 Layer 클래스의 하위호환입니다.
  • call method는 layer의 연산부분을 담당합니다. 
  • build method는 weights를 생성하는 부분입니다. (이는 관습적인 부분이며, __init__에서도 weight를 생성할 수 있습니다.)

Dense layer의 예를 보겠습니다.

class CustomDense(layers.Layer):

  def __init__(self, units=32):
    super(CustomDense, self).__init__()
    self.units = units

  def build(self, input_shape):
    self.w = self.add_weight(shape=(input_shape[-1], self.units),
                             initializer='random_normal',
                             trainable=True)
    self.b = self.add_weight(shape=(self.units,),
                             initializer='random_normal',
                             trainable=True)

  def call(self, inputs):
    return tf.matmul(inputs, self.w) + self.b
      
inputs = keras.Input((4,))
outputs = CustomDense(10)(inputs)

model = keras.Model(inputs, outputs)

또한, 직렬화기능을 사용하려면 get_config method를 정의해야한다고 합니다.

class CustomDense(layers.Layer):

  def __init__(self, units=32):
    super(CustomDense, self).__init__()
    self.units = units

  def build(self, input_shape):
    self.w = self.add_weight(shape=(input_shape[-1], self.units),
                             initializer='random_normal',
                             trainable=True)
    self.b = self.add_weight(shape=(self.units,),
                             initializer='random_normal',
                             trainable=True)

  def call(self, inputs):
    return tf.matmul(inputs, self.w) + self.b

  def get_config(self):
    return {'units': self.units}
    
    
inputs = keras.Input((4,))
outputs = CustomDense(10)(inputs)

model = keras.Model(inputs, outputs)
config = model.get_config()

new_model = keras.Model.from_config(
    config, custom_objects={'CustomDense': CustomDense})
    

코드와 본문내용으로 짐작해보면, CustomDense를 config를 얻어 from_config를 사용하면 재사용할 수 있다~ 뭐 이런 것 같습니다.

(또, 위 2개의 예제가 name parameter에서 에러가 뜹니다. 댓글좀 달아주세요. API에 보면 name of layers인데 이름 집어넣어도 에러가 뜹니다) -> 그래프 초기화가 안되서 그런것 같습니다, 그냥 input_shape[-1]을 4로 바꿔주시면 됩니다. 그리고 name = 'string' 아무거나 집어넣어주세요


When to use the Functional API

함수형 API를 써서 새 모델을 만들 것인지, Model의 하위클래스로서 만들것인지 어떻게 결정할까요?

일반적으로 함수형 API는 사용이 쉽고 안전하며, Model 클래스가 가지고 있지 않은 기능을 가지고 있습니다.

그러나 Model의 하위클래스로 작업을 할 경우 Tree-RNN과 같은 함수형 API로는 표현이 쉽지 않은 모델을 구축할 수 있습니다.

위의 말은 그냥 high-level과 low-level의 차이를 설명해놓은거네요. 당연하죠.


Here are the strengths of the Functional API:

함수형 API는 super(Myclass, self).__init__(...), def call(self, ...)과 같은 함수를 정의하지 않아도 됩니다.

단 몇줄이면 됩니다.

inputs = keras.Input(shape=(32,))
x = layers.Dense(64, activation='relu')(inputs)
outputs = layers.Dense(10)(x)
mlp = keras.Model(inputs, outputs)

subclass 버전과 비교해보세요

class MLP(keras.Model):
  
  def __init__(self, **kwargs):
    super(MLP, self).__init__(**kwargs)
    self.dense_1 = layers.Dense(64, activation='relu')
    self.dense_2 = layers.Dense(10)
    
  def call(self, inputs):
    x = self.dense_1(inputs)
    return self.dense_2(x)
 
# Instantiate the model.
mlp = MLP()
# Necessary to create the model's state.
# The model doesn't have a state until it's called at least once.
_ = mlp(tf.zeros((1, 32)))

여러분이 위 함수를 정의하는 동안 함수형 API는 여러분의 모델을 검증할 수 있습니다.

함수형 API는 plotting 및 검증이 가능합니다. - 모델을 그려볼 수도 있고, 아래와 같이 중간층도 출력하기 쉽습니다.

features_list = [layer.output for layer in vgg19.layers]
feat_extraction_model = keras.Model(inputs=vgg19.input, outputs=features_list)

Here are the weaknesses of the Functional API:

dynamic한 구조를 구축하기 어렵습니다(high-API의 단점), 전적으로 high-API의 단점을 가지고 있는거죠.


Mix-and-matching different API styles

중요한 것은, 기능 API 또는 모델 하위 분류 중 하나를 선택하는 것은 여러분을 한 범주의 모델로 제한하는 이항 결정이 아니라는 점이다. tf.keras API의 모든 모델은 시퀀셜 모델, 기능 모델 또는 처음부터 작성된 하위 분류 모델/레이어 등 각 모델과 상호 작용할 수 있다.

units = 32
timesteps = 10
input_dim = 5

# Define a Functional model
inputs = keras.Input((None, units))
x = layers.GlobalAveragePooling1D()(inputs)
outputs = layers.Dense(1, activation='sigmoid')(x)
model = keras.Model(inputs, outputs)


class CustomRNN(layers.Layer):

  def __init__(self):
    super(CustomRNN, self).__init__()
    self.units = units
    self.projection_1 = layers.Dense(units=units, activation='tanh')
    self.projection_2 = layers.Dense(units=units, activation='tanh')
    # Our previously-defined Functional model
    self.classifier = model

  def call(self, inputs):
    outputs = []
    state = tf.zeros(shape=(inputs.shape[0], self.units))
    for t in range(inputs.shape[1]):
      x = inputs[:, t, :]
      h = self.projection_1(x)
      y = h + self.projection_2(state)
      state = y
      outputs.append(y)
    features = tf.stack(outputs, axis=1)
    print(features.shape)
    return self.classifier(features)

rnn_model = CustomRNN()
_ = rnn_model(tf.zeros((1, timesteps, input_dim)))

위와 같이 함수형 API와 subclass version을 섞어서도 구성할 수 있습니다.

반대로 subclass version을 구현하여 함수형 API의 input, layer or output으로 사용할 수 있습니다. 다만, 이 방식을 사용할려면 다음과 같이 call method를 구현해야 합니다.

  • call(self, inputs, **kwargs) where inputs is a tensor or a nested structure of tensors (e.g. a list of tensors), and where **kwargs are non-tensor arguments (non-inputs).
  • call(self, inputs, training=None, **kwargs) where training is a boolean indicating whether the layer should behave in training mode and inference mode.
  • call(self, inputs, mask=None, **kwargs) where mask is a boolean mask tensor (useful for RNNs, for instance).
  • call(self, inputs, training=None, mask=None, **kwargs) -- of course you can have both masking and training-specific behavior at the same time.

다음은 예시입니다.

units = 32
timesteps = 10
input_dim = 5
batch_size = 16


class CustomRNN(layers.Layer):

  def __init__(self):
    super(CustomRNN, self).__init__()
    self.units = units
    self.projection_1 = layers.Dense(units=units, activation='tanh')
    self.projection_2 = layers.Dense(units=units, activation='tanh')
    self.classifier = layers.Dense(1, activation='sigmoid')

  def call(self, inputs):
    outputs = []
    state = tf.zeros(shape=(inputs.shape[0], self.units))
    for t in range(inputs.shape[1]):
      x = inputs[:, t, :]
      h = self.projection_1(x)
      y = h + self.projection_2(state)
      state = y
      outputs.append(y)
    features = tf.stack(outputs, axis=1)
    return self.classifier(features)

# Note that we specify a static batch size for the inputs with the `batch_shape`
# arg, because the inner computation of `CustomRNN` requires a static batch size
# (when we create the `state` zeros tensor).
inputs = keras.Input(batch_shape=(batch_size, timesteps, input_dim))
x = layers.Conv1D(32, 3)(inputs)
outputs = CustomRNN()(x)

model = keras.Model(inputs, outputs)

rnn_model = CustomRNN()
_ = rnn_model(tf.zeros((1, 10, 5)))