ニューラルネットワークでカテゴリ変数を扱う方法としては One-Hot エンコーディングがある。 しかし、One-Hot エンコーディングでは特徴量のカーディナリティが高いと扱う次元数が大きくなる。 そこで、今回紹介する Entity Embedding を使うと、ラベルエンコードしたカテゴリ変数をニューラルネットワークで扱うことができるようになる。 Entity Embedding では、Embedding 層を使うことでカテゴリ変数ごとにパラメータの重み (分散表現) を学習する。 今回は TensorFlow/Keras で Entity Embedding を試してみる。
使った環境は次のとおり。
$ sw_vers ProductName: Mac OS X ProductVersion: 10.15.7 BuildVersion: 19H114 $ python -V Python 3.8.7
下準備
あらかじめ、使用するパッケージをインストールしておく。
$ pip install tensorflow scikit-learn sklearn-pandas seaborn
Embedding 層について
Entity Embedding を理解する上では、まず Embedding 層について理解する必要がある。
Keras では Embedding 層の実装として tensorflow.keras.layers.Embedding
が使える。
上記を実装するには tensorflow.nn.embedding_lookup()
という関数が使われている。
そこで、まずはこの関数の振る舞いについて見ていくことにしよう。 はじめに Python のインタプリタを起動する。
$ python
TensorFlow をインポートしたら、例として次のような 5 行 2 列の Tensor オブジェクトを用意する。
>>> import tensorflow as tf >>> tensor = tf.constant([ ... [1, 2], ... [3, 4], ... [5, 6], ... [7, 8], ... [9, 10], ... ]) >>> tensor <tf.Tensor: shape=(5, 2), dtype=int32, numpy= array([[ 1, 2], [ 3, 4], [ 5, 6], [ 7, 8], [ 9, 10]], dtype=int32)>
上記の Tensor オブジェクトに対してtf.nn.embedding_lookup()
関数を使ってみよう。
この関数を使ってできることは単純で、行のインデックス番号を指定すると、その行を取り出すことができる。
例えば、次のように 0, 2, 3
をリストで渡すと、その行に対応した要素を取り出すことができる。
>>> tf.nn.embedding_lookup(tensor, [0, 2, 3]) <tf.Tensor: shape=(3, 2), dtype=int32, numpy= array([[1, 2], [5, 6], [7, 8]], dtype=int32)>
先ほど作った Tensor オブジェクトから 0, 2, 3
行目が取り出されていることがわかる。
できることがわかったところで、これの何が嬉しいのか考えてみよう。 先ほど作った Tensor オブジェクトで、行がカテゴリ変数の各カテゴリ、列がそのカテゴリの重み (分散表現) に対応すると捉えたらどうだろうか。 カテゴリ変数を扱う上で、One-Hot にしなくても、ラベルエンコードしたカテゴリ変数についてインデックス番号ごとに分散表現を管理できることがわかる。
念の為、tensorflow.keras.layers.Embedding
の挙動についても確認しておこう。
先ほど作った Tensor オブジェクトと同じ重みを持った Embedding
オブジェクトを次のように作る。
>>> initializer = tf.keras.initializers.Constant( value=[ [1, 2], [3, 4], [5, 6], [7, 8], [9, 10], ] ) >>> embedding_layer = tf.keras.layers.Embedding(5, 2, embeddings_initializer=initializer)
あとは入力として、先ほど tf.nn.embedding_lookup()
関数の引数に使ったのと同じインデックス番号を渡してみよう。
>>> embedding_layer(inputs=tf.constant([0, 2, 3])) <tf.Tensor: shape=(3, 2), dtype=float32, numpy= array([[1., 2.], [5., 6.], [7., 8.]], dtype=float32)>
すると、ちゃんとインデックス番号に対応した重みが出力として得られることがわかる。
カテゴリ変数を Entity Embedding で扱う MLP を書いてみる
続いては、実際にカテゴリ変数を Entity Embedding で扱うサンプルコードを見てみる。
以下では、Diamonds データセットを使って、3 層の MLP (Multi Layer Perceptron) で価格 (price
) を推定する回帰問題を学習している。
Diamonds データセットには、カテゴリ変数として cut
、color
、clarity
という特徴量がある。
今回は、それらのカテゴリ変数をラベルエンコードした数値として受け取って、2 次元の分散表現として学習させた。
モデルの評価は 3-Fold CV で、Out-of-Fold で推定した値を RMSLE で最後に出力している。
#!/usr/bin/env python3 # -*- coding: utf-8 -*- import numpy as np import tensorflow as tf from sklearn.metrics import mean_squared_log_error from sklearn_pandas import DataFrameMapper from sklearn.model_selection import KFold from sklearn.preprocessing import LabelEncoder from sklearn.preprocessing import MinMaxScaler import seaborn as sns from matplotlib import pyplot as plt class MultiLayerPerceptronWithEntityEmbedding(tf.keras.Model): """カテゴリ変数を Entity Embedding で扱う MLP""" def __init__(self): super().__init__() # cut を Entity Embedding する層 self.cut_embedding = tf.keras.Sequential([ tf.keras.layers.Input(shape=(1,)), tf.keras.layers.Embedding(5, 2), tf.keras.layers.Flatten(), ], name='cut_embedding') # color を Entity Embedding する層 self.color_embedding = tf.keras.Sequential([ tf.keras.layers.Input(shape=(1,)), tf.keras.layers.Embedding(7, 2), tf.keras.layers.Flatten(), ], name='color_embedding') # clarity を Entity Embedding する層 self.clarity_embedding = tf.keras.Sequential([ tf.keras.layers.Input(shape=(1,)), tf.keras.layers.Embedding(8, 2), tf.keras.layers.Flatten(), ], name='clarity_embedding') # MLP 層 self.mlp = tf.keras.Sequential([ tf.keras.layers.Dense(64, activation=tf.keras.activations.relu), tf.keras.layers.Dropout(0.25), tf.keras.layers.Dense(32, activation=tf.keras.activations.relu), tf.keras.layers.Dropout(0.25), tf.keras.layers.Dense(1), ], name='mlp') def call(self, inputs): # cut の Embedding cut = inputs[:, 0] cut_embed = self.cut_embedding(cut) # color の Embedding color = inputs[:, 1] color_embed = self.color_embedding(color) # clarity の Embedding clarity = inputs[:, 2] clarity_embed = self.clarity_embedding(clarity) # 残りの特徴量と連結する concat_layers = [ cut_embed, color_embed, clarity_embed, inputs[:, 3:], ] x = tf.keras.layers.Concatenate()(concat_layers) # MLP 層に入れる x = self.mlp(x) return x def main(): # Diamonds データセットを読み込む df = sns.load_dataset('diamonds') x, y = df.drop(['price'], axis=1), df['price'] mapper = DataFrameMapper([ # カテゴリ変数はラベルエンコードする ('cut', LabelEncoder()), ('color', LabelEncoder()), ('clarity', LabelEncoder()), # 連続変数は Min-Max スケーリングで標準化する (['carat', 'depth', 'table', 'x', 'y', 'z'], MinMaxScaler()), ], df_out=True) # エンコードする x_transformed = mapper.fit_transform(x) print(x_transformed.head()) models = [] folds = KFold(n_splits=3, shuffle=True, random_state=42) for train_index, val_index in folds.split(x_transformed, y): train_x = x_transformed.iloc[train_index] train_y = y.iloc[train_index] valid_x = x_transformed.iloc[val_index] valid_y = y.iloc[val_index] # タスクを定義する model = MultiLayerPerceptronWithEntityEmbedding() optimizer = tf.keras.optimizers.Adam(learning_rate=1e-2) loss_function = tf.keras.losses.MeanSquaredLogarithmicError() model.compile(optimizer=optimizer, loss=loss_function) model.build((None,) + train_x.shape[1:]) # 学習に使うコールバック checkpoint_filepath = '/tmp/keras-checkpoint' callbacks = [ # 検証データに対する損失が一定のエポック数の間に改善しないときは学習を打ち切る tf.keras.callbacks.EarlyStopping(monitor='val_loss', patience=50, verbose=1, mode='min'), # モデルを記録する tf.keras.callbacks.ModelCheckpoint(monitor='val_loss', filepath=checkpoint_filepath, save_best_only=True, verbose=1, mode='min'), ] # 学習する model.fit(train_x, train_y, batch_size=1024, epochs=1_000, verbose=1, validation_data=(valid_x, valid_y), callbacks=callbacks, ) # 最良のモデルを読み込む model.load_weights(checkpoint_filepath) models.append(model) y_preds = np.zeros_like(y, dtype=np.float64) model_fold_mappings = zip(models, folds.split(x_transformed, y)) for model, (_, val_index) in model_fold_mappings: # Out-of-Fold で予測を作る valid_x = x_transformed.iloc[val_index] y_pred = model.predict(valid_x) y_preds[val_index] = y_pred.ravel() rmsle = np.sqrt(mean_squared_log_error(y, y_preds)) print(f'Root Mean Squared Log Error: {rmsle}') # predicted vs actual plot plt.scatter(y_preds, y) y_max = max(max(y_preds), max(y)) plt.plot([0, y_max], [0, y_max], color='r') plt.xlabel('Predicted') plt.ylabel('Actual') plt.show() if __name__ == '__main__': main()
上記を実行してみよう。 なお、データもネットワークも小さいので CPU で十分処理できる。
$ python entityembed.py ... Root Mean Squared Log Error: 0.11266712415049261
回帰問題は評価指標だけだとイマイチイメージがつきにくいので真の価格と推定した価格の散布図も示す。
とりあえず、カテゴリ変数を One-Hot しなくても学習できていることがわかった。 これができると、特徴量を扱う処理でもニューラルネットワーク用とそれ以外の差分が小さくなる余地ができてありがたい。
補足
今回使った Diamonds データセットについては、以下の理由からあえて Entity Embedding する必要はないかなという話もある。
- カテゴリ変数のカーディナリティが小さいので One-Hot エンコーディングでも十分に扱える
- カテゴリ変数が名義尺度ではなく順序尺度なので、順序にもとづいて Ordinal エンコーディングすることもできる
なお、軽く検証した限りでは、今回のデータとネットワークと評価指標だと性能は One-Hot Encoding > Entity Embedding > Ordinal Encoding という感じだった。 Ordinal Encoding がイマイチ効かないのは、順序尺度毎の寄与がすべて一定という仮定を置くことになって、それが誤りだからなのかな。
コード的な差分は、One-Hot エンコードに関しては前処理を次のようにする。
mapper = DataFrameMapper([ # カテゴリ変数は One-Hot エンコードする ('cut', LabelBinarizer()), ('color', LabelBinarizer()), ('clarity', LabelBinarizer()), (['carat', 'depth', 'table', 'x', 'y', 'z'], MinMaxScaler()), ], df_out=True)
Ordinal エンコードであれば、こう。
mapper = DataFrameMapper([ # カテゴリ変数を Ordinal エンコードした後に標準化する (['cut', 'color', 'clarity'], [ OrdinalEncoder(categories=[ ['Ideal', 'Premium', 'Very Good', 'Good', 'Fair'], ['D', 'E', 'F', 'G', 'H', 'I', 'J'], ['IF', 'VVS1', 'VVS2', 'VS1', 'VS2', 'SI1', 'SI2', 'I1'], ]), MinMaxScaler(), ]), (['carat', 'depth', 'table', 'x', 'y', 'z'], MinMaxScaler()), ], df_out=True)
そして、モデルは MLP 層だけにして上記の出力をそのまま突っ込んでやれば良い。
参考文献
元ネタの論文では、Entity Embedding のメリットとして扱う次元数の減少や学習の高速化以外についても触れられている。
いじょう。
- 作者:もみじあめ
- 発売日: 2020/02/29
- メディア: Kindle版