連載
» 2020年03月23日 05時00分 公開

TensorFlow 2+Keras(tf.keras)入門:第6回 カスタマイズするための、TensorFlow 2.0最新の書き方入門

TensorFlow 2.0以降では「サブクラス化」という基本パターンに従って簡単にカスタマイズができる。その全カスタマイズ方法を紹介し、最後にtf.estimator高水準APIについても言及する。

[一色政彦,デジタルアドバンテージ]

この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。

「TensorFlow 2+Keras(tf.keras)入門」のインデックス

連載目次

 前々回前回は、TensorFlow 2.x(2.0以降)の書き方を説明した。既に全3種類4通りの書き方を説明済みだが、基本はそれらで修了である。

 今回は、応用編として残り2つの書き方を紹介する。脚注や図、コードリストの番号は前回からの続き番号としている(前々回・前回・今回は、切り離さず、ひとまとまりの記事として読んでほしいため連続性を持たせている)。


Google Colabで実行する
GitHubでソースコードを見る

今回の内容と方針について

 TensorFlowのメリットは、初心者向けからエキスパート向けまでさまざまな書き方が用意されており、誰でも簡単に学んで使いこなせるだけでなく、必要に応じて応用発展的な実装も可能なことである。

 逆に、そのメリットがデメリットでもあり、人によって書き方が大きく違うこととなり、初心者から見ると理解の妨げになりやすい。例えばTensorFlowの公式チュートリアルやドキュメント、ネット上/GitHub上のサンプルコードを見ると、実にさまざまな書き方がなされている。

 それに拍車をかけるのが、TensorFlow 1.x時代のコードである(例えば、こちらの記事が1.x時代のコード)。1.x時代は、TensorFlowライブラリ内にさまざまな高水準APIが乱立した。2.0になった段階で、高水準APIのほとんどが廃止され、tf.kerasとtf.estimatorという2つのみが高水準APIとして残った。このように1.xから2.xへと移行した段階で大変革が起きている。ネット上には、今後は2.xベースのコードが増えていくだろうが、依然として1.x時代のコードが大量に残っているため、TensorFlowのコードに関する誤解や混乱状態はしばらく続くだろう(と筆者は予想している)。

 そういった点を踏まえ、あえて本連載の読者ターゲットである「ディープラーニング初心者向け」を想定して、TensorFlow 2.x時代の書き方を今回までの全3回でまとめている。全ての書き方を「マスターしてほしい」という意図ではなく、知識として書き方のバリエーションを知ってもらうことで、初心者でも「世界中にあるドキュメントやサンプルコードを戸惑わずに読めるようになってほしい」というのが意図である。

 今回は、ニューラルネットワークのモデル設計〜学習〜評価までをフルカスタマイズする方法を紹介する。

図 ニューラルネットワーク実装のフルカスタマイズ 図 ニューラルネットワーク実装のフルカスタマイズ

 しかしこれは(繰り返しになるが)「マスターしてほしい」というわけではなく、「知識として知っておいてほしい」というのが目的である。よって今回は書き方を、ざっと紹介するにとどめ、具体的な解説は割愛する。たとえコードの中身が分からなくても気にせず、本当に必要になってから本稿の内容を再読したりして、本当のフルカスタマイズを実践すればよい。

 また、初心者が扱う機能は基本的にライブラリ内に存在するので、まずはそれを探せるように、既存機能の情報を箇条書きで提示する。“車輪の再発明”をする必要はないので、実践ではまず既存機能を確実にチェックするようにしてほしい。

コードの書き方を実行するための準備

前提条件

 今回も前々回/前回に引き続き、Python(バージョン3.6)と、ディープラーニングのライブラリ「TensorFlow」の最新版2.1を利用する。また、開発環境にGoogle Colaboratory(以下、Colab)を用いる。

 前提条件の準備は前々回/前回と同じなので、説明を割愛する。詳しくはColabのノートブックを確認してほしい。

 それでは準備が整ったとして、(5)の書き方を説明していこう。

(5)TensorFlow低水準APIでカスタム実装[TensorFlow Low-level API]

 TensorFlow 2.xでフルカスタマイズを行う場合、カスタマイズのベースとなるのが、前回説明した(4)Subclassing(サブクラス化)モデルである。その書き方では、tf.keras.Modelクラスをサブクラス化して独自のモデルを作成した。これは、「モデルをカスタマイズした」と見ることができる。これがTensorFlow 2.x時代のカスタマイズの基本パターンである。

基本パターン

 TensorFlow 2.xでは、

  • 活性化関数tf.keras.layers.Layerクラスをサブクラス化
  • レイヤーtf.keras.layers.Layerクラスをサブクラス化
  • オプティマイザ最適化アルゴリズム): tf.keras.optimizers.Optimizerクラスをサブクラス化
  • 損失関数tf.keras.losses.Lossクラスをサブクラス化
  • 評価関数tf.keras.metrics.Metricクラスをサブクラス化

などの機能がカスタマイズ可能である(図5-1)。

図5-1 カスタマイズの基本パターン 図5-1 カスタマイズの基本パターン

 上記の箇条書きに示したように、いずれも何らかのクラスをサブクラス化することで実装できる。これが本稿で伝えたい「フルカスタマイズの本質」である。サブクラス化の中で実装すべきメンバーメソッドは少しずつ異なるので、以下ではそれらを見ていく。

活性化関数のカスタム実装

 まずは、活性化関数の既存機能を確認しよう。

 TensorFlow 2.xの活性化関数は、Keras由来のAPIと、TensorFlow由来のAPIの2種類がある。非常に充実しているので、カスタム実装を行う必要性はほとんど発生しないはずである。具体的には以下のものがある。

【Keras由来】

  • 'sigmoid'tf.keras.activations.sigmoid()関数
  • 'hard_sigmoid'tf.keras.activations.hard_sigmoid()関数
  • 'tanh'tf.keras.activations.tanh()関数
  • 'relu',tf.keras.activations.relu()関数
  • 'elu'tf.keras.activations.elu()関数
  • 'selu'tf.keras.activations.selu()関数
  • 'swish'tf.keras.activations.swish()関数
  • 'softsign'tf.keras.activations.softsign()関数
  • 'softplus'tf.keras.activations.softplus()関数
  • 'softmax'tf.keras.activations.softmax()関数
  • 'linear'tf.keras.activations.linear()関数
  • 'exponential'tf.keras.activations.exponential()関数

【TensorFlow由来】

  • tf.nn.sigmoid()関数
  • tf.nn.tanh()関数
  • tf.nn.relu()関数
  • tf.nn.relu6()関数
  • tf.nn.leaky_relu()関数
  • tf.nn.elu()関数
  • tf.nn.selu()関数
  • tf.nn.swish()関数
  • tf.nn.softsign()関数
  • tf.nn.softplus()関数
  • tf.nn.softmax()関数

 前々回説明した、

  • Sequentialモデル(リスト1-1/2-1)では'tanh'という文字列を
  • Functional API(リスト3-1)やSubclassingモデル(リスト4-1)ではtf.keras.layers.Activationクラスを

使用した。それらの代わりに、tf.keras.activations.tanh()関数やtf.nn.tanh()関数を使用することも可能である。

 既存の活性化関数やそのサブクラス化について詳しくは下記のリンク先を参照してほしい。

 以上を探しても目的の活性化関数が見つからない場合は、次のようにしてカスタム(独自)のものを作成することが可能だ(リスト5-1)。

# カスタムの活性化関数のPython関数を実装
def custom_activation(x):
    return (tf.exp(x)-tf.exp(-x))/(tf.exp(x)+tf.exp(-x))

# カスタムの活性化関数クラスを実装(レイヤーのサブクラス化)
# (tf.keras.layers.Activation()の代わり)
class CustomActivation(tf.keras.layers.Layer):
  def __init__(self, **kwargs):
    super(CustomActivation, self).__init__(**kwargs)

  def call(self, inputs):
    return custom_activation(inputs)

リスト5-1 活性化関数のカスタム実装

 tf.keras.layers.Layerクラスをサブクラス化して、独自のCustomActivationクラスを実装している(前回説明したモデルのサブクラス化と非常に似ている方法であることに気付くだろう)。クラス内部のメンバーメソッドは以下のようになっている。

  • __init__()メソッド: 初期化処理
  • call()メソッド: 活性化関数の処理

 tf.keras.layers.Layerクラスのサブクラス化についてより詳しくは、

を参照してほしい(ページ内を「subclass」で検索するとよい)。

 リスト5-1では、活性化関数をPython関数(custom_activation()関数)として切り出すことで、tf.keras.activations.tanh()関数やtf.nn.tanh()関数と同じような感覚でも使えるようにも実装したが、もちろんcall()メソッド内に直接実装してもよい。

 また、custom_activation()関数の中では、tf.exp()関数や+-/といった四則演算が記載されている(なお、関数の引数に渡されるxは、TensorFlowテンソルである)。このように数学計算は、基本的にはNumPyと同じように行える。こういったtf.xxxという関数や演算子はTensorFlow低水準APIと呼ばれる。それぞれ詳しくは下記のドキュメントを参照してほしい。

 リスト5-1では、TensorFlow低水準APIで記述したが、Keras backend APIを使うこともできる。tf.kerasではなく、スタンドアロンKerasを使う場合は、Keras backend APIを使うことになる(リスト5-2)。

import tensorflow.keras.backend as K

# カスタムの活性化関数のPython関数を実装
def custom_activation(x):
    return (K.exp(x)-K.exp(-x))/(K.exp(x)+K.exp(-x))

# ……独自クラスの実装は同じなので割愛……

リスト5-2 Keras backend APIを使った書き方

 と言っても、リスト5-1と5-2を比較すると分かるように、ほぼ同じコードとなる。以下では、TensorFlow低水準APIのみで記述する。Keras backend APIの参考実装は、冒頭で示したColabノートブックの方にコメントで含めておいたので、必要に応じて参考にしてほしい。

レイヤーのカスタム実装

 次に、レイヤーの既存機能を確認しよう。

 TensorFlow 2.xのレイヤーは、全てKeras由来のものに統一されている。これも非常に充実しているので、カスタム実装を行う必要性はほとんど発生しないだろう。代表的なものには以下のものがある。

  • Denseクラス
  • Activationクラス
  • Dropoutクラス
     ・SpatialDropout1Dクラス
     ・SpatialDropout2Dクラス
     ・SpatialDropout3Dクラス
  • Flattenクラス
  • InputLayerクラス
  • Reshapeクラス
  • Permuteクラス
  • RepeatVectorクラス
  • Lambdaクラス
  • ActivityRegularizationクラス
  • Maskingクラス

 レイヤーは多種多様なものが用意されており、数が多すぎてここで列挙するととても長くなってしまうので、一部だけを抜粋した。既存のレイヤーについて詳しくは下記のリンク先を参照してほしい。

 以上を探しても目的のレイヤーが見つからない場合は、次のようにしてカスタム(独自)のものを作成することが可能だ(リスト5-3)。

# カスタムの全結合層(線形変換)のPython関数を実装
def fully_connected(inputs, weights, bias):
  return tf.matmul(inputs, weights) + bias

# カスタムのレイヤークラスを実装(レイヤーのサブクラス化)
# (tf.keras.layers.Dense()の代わり)
class CustomLayer(tf.keras.layers.Layer):
  def __init__(self, units, input_dim=None, **kwargs):
    self.input_dim = input_dim  # 入力の次元数(=レイヤーの入力数)
    self.units = units          # ニューロン数(=レイヤーの出力数)
    super(CustomLayer, self).__init__(**kwargs)

  def get_config(self):
    # レイヤー構成をシリアライズ可能にするメソッド(独自の設定項目がメンバー変数としてある場合など、必要に応じて実装する)
    config = super(CustomLayer, self).get_config()
    config.update({
        'input_dim': self.input_dim,
        'units': self.units
    })
    return config

  def build(self, input_shape):
    #print(input_shape) # 入力形状。例えば「(2, 2)」=2行2列なら入力の次元数は2列
    input_data_dim = input_shape[-1] # 入力の次元数(=レイヤーの入力数)

    # 入力の次元数をチェック(デバッグ)
    if self.input_dim != None:
      assert input_data_dim == self.input_dim  # 指定された入力次元数と実際の入力次元数が異なります

    # 重みを追加する
    self.kernel = self.add_weight(
        shape=(input_data_dim, self.units),
        name='kernel',
        initializer='glorot_uniform'# 前々回のリスト1-3のような独自の関数も指定できる
        trainable=True)

    # バイアスを追加する
    self.bias = self.add_weight(
        shape=(self.units,),
        name='bias',
        initializer='zeros',
        trainable=True)
    
    #self.built = True # Layerクラスでビルド済みかどうかを管理するのに使われている(なくても大きな問題はない)
    super(CustomLayer, self).build(input_shape) # 上と同じ意味。APIドキュメントで推奨されている書き方

  def call(self, inputs):
    return fully_connected(inputs, self.kernel, self.bias)

リスト5-3 レイヤーのカスタム実装

 tf.keras.layers.Layerクラスをサブクラス化して、独自のCustomLayerクラスを実装している。クラス内部のメンバーメソッドは以下のようになっている。

  • __init__()メソッド: 初期化処理。渡された引数をメンバー変数に保存したりする
  • get_config()メソッド: オプション(実装しなくてもよい)。メンバー変数を含む構成情報をPython辞書形式にシリアライズする(保存→ロード用)
  • build()メソッド: オプション。基本的に重みやバイアスなどのパラメーターを作成する
  • call()メソッド: レイヤーの処理。この例では全結合の線形変換を、fully_connected関数に実装して、それを呼び出している

 tf.keras.layers.Layerクラスのサブクラス化についてより詳しくは、

を参照してほしい(ページ内を「subclass」で検索するとよい)。

オプティマイザ(最適化アルゴリズム)のカスタム実装

 続いて、オプティマイザの既存機能を確認しよう。

 TensorFlow 2.xのオプティマイザも、全てKeras由来のものに統一されている。tf.optimizers名前空間は、Keras由来のtf.keras.optimizers名前空間のエイリアスになっている。

 これも充実しているので、カスタム実装を行う必要性はほとんどない。具体的には以下のものがある。

  • SGDクラス(確率的勾配降下法)
  • RMSpropクラス
  • Adagradクラス
  • Adadeltaクラス
  • Adamクラス
  • Adamaxクラス
  • Nadamクラス
  • Ftrlクラス

 既存のオプティマイザについて詳しくは下記のリンク先を参照してほしい。

 以上を探しても目的のオプティマイザが見つからない場合は、次のようにしてカスタム(独自)のものを作成することが可能だ(リスト5-4)。

# カスタムの最適化アルゴリズムクラスを実装(オプティマイザのサブクラス化)
# (tf.keras.optimizers.SGDの代わり)
class CustomOptimizer(tf.keras.optimizers.Optimizer):
  def __init__(self, learning_rate=0.01, name='CustomOptimizer', **kwargs):
    super(CustomOptimizer, self).__init__(name, **kwargs)
    self.learning_rate = kwargs.get('lr', learning_rate)

  def get_config(self):
    config = super(CustomOptimizer, self).get_config()
    config.update({
        'learning_rate': self.learning_rate
    })
    return config

  def _create_slots(self, var_list):
    for v in var_list: # `Variable`オブジェクトのリスト
      self.add_slot(v, 'accumulator', tf.zeros_like(v))
    # 参考実装例。ここで作成したスロットは未使用になっている

  def _resource_apply_dense(self, grad, var):
    # 引数「grad」: 勾配(テンソル)
    # 引数「var」: 更新対象の「変数」を示すリソース(“resource”データ型のテンソル)
    # var.device の内容例: /job:localhost/replica:0/task:0/device:CPU:0
    # var.dtype.base_dtype の内容例: <dtype: 'float32'>
    acc = self.get_slot(var, 'accumulator') # 参考実装例(スロットは未使用)
    return var.assign_sub(self.learning_rate * grad)  # 変数の値(パラメーター)を更新
    
  def _resource_apply_sparse(self, grad, var, indices):
    # 引数「grad」: 勾配(インデックス付きの、スパースなテンソル)
    # 引数「var」: 更新対象の「変数」を示すリソース(“resource”データ型のテンソル)
    # 引数「indices」: 勾配がゼロではない要素の 「インデックス」(整数型のテンソル)
    raise NotImplementedError("今回は使わないので未実装")
    # return ……変数の値(パラメーター)を更新する「操作」を返却する……

リスト5-4 オプティマイザ(最適化アルゴリズム)のカスタム実装

 tf.keras.optimizers.Optimizerクラスをサブクラス化して、独自のCustomOptimizerクラスを実装している。クラス内部のメンバーメソッドは以下のようになっている。

  • __init__()メソッド: 初期化処理。渡された引数をメンバー変数に保存したりする
  • get_config()メソッド: オプション(実装しなくてもよい)。メンバー変数を含む構成情報をPython辞書形式にシリアライズする(保存→ロード用)
  • _create_slots()メソッド: オプション。スロット(=トレーニングする変数に関連付けられた追加の変数)を作成する。スロットは「Adam」や「Adagrad」などで必要となる
  • _resource_apply_dense()メソッド: 最適化処理。勾配が「(通常の)密なテンソル」である場合の、パラメーター(重みやバイアス)の更新処理
  • _resource_apply_sparse()メソッド: 最適化処理。勾配が「スパースなテンソル(=要素の多くが0である疎な状態)」である場合の、パラメーターの更新処理

 tf.keras.optimizers.Optimizerクラスのサブクラス化についてより詳しくは、

を参照してほしい(ページ内を「subclass」で検索するとよい)。

学習のカスタム実装について

 学習のカスタム実装については、前回説明済みである。これはフルカスタマイズの手法であるが、tf.kerasモデル内部の一部の関数を拡張して、tf.kerasモデルのfit()メソッド/evaluateメソッド/predictメソッド実行時の挙動を変えることも可能である。

 具体的には、tf.keras.Modelクラスをサブクラス化したモデルの基底クラス内にある、

  • train_step()メソッド
  • test_step()メソッド
  • predict_step()メソッド

をオーバーライドすることになる。

 前回説明したフルカスタマイズの場合は、全て自分の世界観で全てを制御することになる。自分で考えたロジック内容だから、影響範囲も管理しやすい。

 一方、tf.kerasモデル内部の部分的な拡張の場合は、あくまでKerasの世界観を壊さないように作る必要がある。よって考え方によっては、「こちらの実装の方が難しい」とも言えるだろう。

図5-2 フルカスタマイズ vs. 部分的な拡張 図5-2 フルカスタマイズ vs. 部分的な拡張

 部分的な拡張の実装方法については、(筆者が確認した限り)ほとんど説明資料がない。唯一、「TensorFlow Dev Summit 2020」のセッション「Learning to read with TensorFlow and Keras (TF Dev Summit '20) - YouTube」でPaige Bailey氏が簡単に言及したのを確認したのみである。よって、tf.keras.Modelのソースコードを読んで理解したうえで実装する必要があり、あまりお勧めできない。そのため本稿では割愛する。

損失関数のカスタム実装

 さて次に、損失関数の既存機能を確認しよう。

 TensorFlow 2.xの損失関数も、全てKeras由来のものに統一されている。tf.losses名前空間は、Keras由来のtf.keras.losses名前空間のエイリアスになっている。

 同じく充実しているので、カスタム実装を行う必要性はほとんどない。具体的には以下のものがある。

  • MeanSquaredErrorクラス(略記:MSE): mean_squared_error()関数
  • MeanAbsoluteErrorクラス(略記:MAE): mean_absolute_error()関数
  • MeanAbsolutePercentageErrorクラス(略記:MAPE): mean_absolute_percentage_error()関数
  • MeanSquaredLogarithmicErrorクラス(略記:MSLE): mean_squared_logarithmic_error()関数
  • SquaredHingeクラス: squared_hinge()関数
  • Hingeクラス: hinge()関数
  • CategoricalHingeクラス: categorical_hinge()関数
  • LogCoshクラス: logcosh()関数
  • Huberクラス: huber_loss()関数
  • CategoricalCrossentropyクラス: categorical_crossentropy()関数
  • SparseCategoricalCrossentropyクラス: sparse_categorical_crossentropy()関数
  • BinaryCrossentropyクラス: binary_crossentropy()関数
  • KLDivergenceクラス(略記:KLD): kullback_leibler_divergence()関数
  • Poissonクラス: poisson()関数
  • CosineSimilarityクラス: cosine_proximity()関数

 既存の損失関数について詳しくは下記のリンク先を参照してほしい。

 以上を探しても目的の損失関数が見つからない場合は、次のようにしてカスタム(独自)のものを作成することが可能だ(リスト5-5)。

# カスタムの損失関数のPython関数を実装
def custom_loss(y_true, y_pred):
    return tf.reduce_mean(tf.square(y_true - y_pred))

# カスタムの損失関数クラスを実装(レイヤーのサブクラス化)
# (tf.keras.losses.MeanSquaredError()の代わり)
class CustomLoss(tf.keras.losses.Loss):
  def __init__(self, name="custom_loss", **kwargs):
    super(CustomLoss, self).__init__(name=name, **kwargs)

  def call(self, y_true, y_pred):
    y_pred = tf.convert_to_tensor(y_pred)  # 念のためTensor化
    y_true = tf.cast(y_true, y_pred.dtype) # 念のため同じデータ型化
    return custom_loss(y_true, y_pred)

リスト5-5 損失関数のカスタム実装

 tf.keras.losses.Lossクラスをサブクラス化して、独自のCustomLossクラスを実装している。クラス内部のメンバーメソッドは以下のようになっている。

  • __init__()メソッド: 初期化処理
  • call()メソッド: 損失関数の処理

 tf.keras.losses.Lossクラスのサブクラス化についてより詳しくは、

を参照してほしい(ページ内を「subclass」で検索するとよい)。

評価関数(正解率や損失)のカスタム実装

 さて最後に、評価関数の既存機能を確認しよう。

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。