雑記: Keras で自作レイヤー(Dense)

Keras でオリジナルの自作レイヤーを追加したいときとかあると思います。
自作レイヤー自体は以下の記事でつかったことがありますが、これはウェイトをもつレイヤーではなく、最後にかぶせて損失関数のみをカスタマイズするためのレイヤーでした。
Keras で変分自己符号化器(VAE)を学習したい - クッキーの日記
ウェイトをもつレイヤーはどう追加するのか知りたいのですが、以下のドキュメントの通りにすればできるのか確認します。
Writing your own Keras layers - Keras Documentation
上のページの MyLayer は単純な Dense のようなので、実際に Dense に置き換えられるか確認します。

keras.layers.Dense をつかった場合

まず、 普通に Keras の Dense をつかって MNIST の分類器をつくると例えば以下になると思います。モデルは適当です。
→ 下記スクリプトの設定で 95% 程度のテスト精度になりました。

# -*- coding: utf-8 -*-
import numpy
from keras.layers import Input, Dense, Dropout
from keras.models import Model
from keras.datasets import mnist
from keras.utils import np_utils

if __name__ == "__main__":
  (x_train, y_train), (x_test, y_test) = mnist.load_data()
  x_train = x_train.astype('float32') / 255.
  x_test = x_test.astype('float32') / 255.
  x_train = x_train.reshape((len(x_train), numpy.prod(x_train.shape[1:])))
  x_test = x_test.reshape((len(x_test), numpy.prod(x_test.shape[1:])))
  y_train = np_utils.to_categorical(y_train, num_classes=10)
  y_test = np_utils.to_categorical(y_test, num_classes=10)

  input = Input(shape=(x_train.shape[1],))
  converted = Dense(20, activation='relu')(input)
  converted = Dropout(0.2)(converted)
  converted = Dense(20, activation='relu')(converted)
  converted = Dropout(0.2)(converted)
  converted = Dense(10, activation='softmax')(converted)
  classifier = Model(input, converted)
  classifier.compile(optimizer='adam', loss='categorical_crossentropy',
                     metrics=['accuracy'])
  print(classifier.summary())

  classifier.fit(x_train, y_train, epochs=20, batch_size=100, shuffle=True)

  scores = classifier.evaluate(x_train, y_train, verbose=0)
  print('Accuracy (train): %.2f%%' % (scores[1]*100))
  scores = classifier.evaluate(x_test, y_test, verbose=0)
  print('Accuracy (test) : %.2f%%' % (scores[1]*100))
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_1 (InputLayer)         (None, 784)               0         
_________________________________________________________________
dense_1 (Dense)              (None, 20)                15700     
_________________________________________________________________
dropout_1 (Dropout)          (None, 20)                0         
_________________________________________________________________
dense_2 (Dense)              (None, 20)                420       
_________________________________________________________________
dropout_2 (Dropout)          (None, 20)                0         
_________________________________________________________________
dense_3 (Dense)              (None, 10)                210       
=================================================================
Total params: 16,330
Trainable params: 16,330
Non-trainable params: 0
_________________________________________________________________
Accuracy (train): 95.69%
Accuracy (test) : 94.92%
自作 Dense をつかった場合

次に、import Dense をつかわずに自作 Dense ( class MyDense ) に置き換えたのが以下です。
→ これも 95% 程度のテスト精度になったので、ちゃんと Dense になっていると思います。
  パラメータ数も上と同じ 16,330 です。

Keras ドキュメント(Writing your own Keras layers - Keras Documentation)にあるとおり、自作レイヤーのクラスでは build メソッドでウェイトを定義し、call メソッドにウェイトを用いたこのレイヤーの計算処理を実装すればよいはずです。compute_output_shape メソッドは次のレイヤーのために出力 shape を返すようにしておきます。

# -*- coding: utf-8 -*-
import numpy
from keras import backend as K
from keras.engine.topology import Layer
from keras.layers import Input, Dropout
from keras.models import Model
from keras.datasets import mnist
from keras.utils import np_utils

class MyDense(Layer):
  def __init__(self, output_dim, activation, **kwargs):
    self.output_dim = output_dim
    self.activation = activation
    super(MyDense, self).__init__(**kwargs)

  def build(self, input_shape):
    self.kernel = self.add_weight(name='kernel',
                                  shape=(input_shape[1], self.output_dim),
                                  initializer='glorot_uniform')
    self.bias   = self.add_weight(name='bias',
                                  shape=(1, self.output_dim),
                                  initializer='zeros')
    super(MyDense, self).build(input_shape)

  def call(self, x):
    if self.activation == 'relu':
      return(K.relu(K.dot(x, self.kernel) + self.bias))
    elif self.activation == 'softmax':
      return(K.softmax(K.dot(x, self.kernel) + self.bias))
    else:
      return(K.dot(x, self.kernel) + self.bias)

  def compute_output_shape(self, input_shape):
    return(input_shape[0], self.output_dim)

if __name__ == "__main__":
  (x_train, y_train), (x_test, y_test) = mnist.load_data()
  x_train = x_train.astype('float32') / 255.
  x_test = x_test.astype('float32') / 255.
  x_train = x_train.reshape((len(x_train), numpy.prod(x_train.shape[1:])))
  x_test = x_test.reshape((len(x_test), numpy.prod(x_test.shape[1:])))
  y_train = np_utils.to_categorical(y_train, num_classes=10)
  y_test = np_utils.to_categorical(y_test, num_classes=10)

  input = Input(shape=(x_train.shape[1],))
  converted = MyDense(20, activation='relu')(input)
  converted = Dropout(0.2)(converted)
  converted = MyDense(20, activation='relu')(converted)
  converted = Dropout(0.2)(converted)
  converted = MyDense(10, activation='softmax')(converted)
  classifier = Model(input, converted)
  classifier.compile(optimizer='adam', loss='categorical_crossentropy',
                     metrics=['accuracy'])
  print(classifier.summary())

  classifier.fit(x_train, y_train, epochs=20, batch_size=100, shuffle=True)

  scores = classifier.evaluate(x_train, y_train, verbose=0)
  print('Accuracy (train): %.2f%%' % (scores[1]*100))
  scores = classifier.evaluate(x_test, y_test, verbose=0)
  print('Accuracy (test) : %.2f%%' % (scores[1]*100))
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_1 (InputLayer)         (None, 784)               0         
_________________________________________________________________
my_dense_1 (MyDense)         (None, 20)                15700     
_________________________________________________________________
dropout_1 (Dropout)          (None, 20)                0         
_________________________________________________________________
my_dense_2 (MyDense)         (None, 20)                420       
_________________________________________________________________
dropout_2 (Dropout)          (None, 20)                0         
_________________________________________________________________
my_dense_3 (MyDense)         (None, 10)                210       
=================================================================
Total params: 16,330
Trainable params: 16,330
Non-trainable params: 0
_________________________________________________________________
Accuracy (train): 95.83%
Accuracy (test) : 95.14%

class MyDense の実装は Keras ドキュメント(Writing your own Keras layers - Keras Documentation)の MyLayer を参考にしましたが、必要に応じて keras.engine.topology.Layer のソースを参照して修正しました。
keras/topology.py at master · fchollet/keras · GitHub

  • 当初 Keras ドキュメントの MyLayer のとおりに実装したところ、パラメータ数からバイアス項がないことに気付いたので、バイアス項を追加しました。
  • add_weight の引数に name が必要だったので追加しました。name をつけて何につかうのかわかりません。→ 後から自作 MyDense にはバイアス項がないことに気付いたとき、ユニークな名前でいくつでも weight 行列を追加できることに気付きました。
  • kernel と bias の初期化方法は本家 Dense に合わせました(初期化が uniform だといまいち到達精度が 94% 程度にしかなりませんでした)(誤差もあるかもしれませんが)。
  • 今回のモデルでは Dense を ReLU または softmax で活性化するので、Dense よろしく MyDense インスタンス生成時に活性化関数も指定するようにしました(ReLU と softmax 以外未対応)(活性化関数自体を引数に取ればいいんだろうとは思います)。



これで Dense が自作できることは確認できたのですが、自分がつくりたいのは Dense ではないのでこれからもっと調べていきます。