Entity embeddingレイヤの応用

Embbedingレイヤの応用方法とその具体例

このチュートリアルでは、RenomでEntity embeddingレイヤを応用する方法を紹介します。 Entity embedding法は、ニューラルネットワークでカテゴリ変数を扱いやすくする方法の1つです。Embeddingレイヤには、PCAのような入力のカテゴリ変数の次元を削減する役割があると言える。 実際には、Word2vecとしてよく知られているように、自然言語処理にEmbedding法が応用されています。 このチュートリアルではEntity embeddingの他の応用[1]についても説明します。 ニューラルネットワークでカテゴリ変数を使用する場合、生のデータをRenomで扱いやすいデータに変換する必要があります。 我々はそのためのチュートリアル(" http://www.renom.jp/ja/notebooks/preprocessing/onehot/notebook.html ", " http://www.renom.jp/ja/notebooks/preprocessing/embedding/notebook.html " )も用意していますので、最初はそれらを参照することをお勧めします。

このチュートリアルでは、モデリングのチュートリアルに従って、まずデータを生成して、Embeddingレイヤを持つニューラルネットワークをトレーニングします。 さらに、その結果を用いて、応用の例をいくつか紹介します。

必要なライブラリ

  • Python 3.5.2
  • Numpy 1.13.3
  • ReNom 2.3.1
  • Matplotlib 2.1.0
  • Scikit-learn 0.19.1
In [1]:
%matplotlib inline
from __future__ import division, print_function
import numpy as np
import time
import matplotlib.pyplot as plt
from sklearn.preprocessing import OneHotEncoder
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsRegressor
from sklearn.ensemble import RandomForestRegressor
from sklearn.decomposition import PCA

import renom as rm
from renom.optimizer import Adam
from renom.utility.initializer import Uniform

Data generation

ここでは、人工的に生成されたデータを用いてEntity embeddingレイヤを使用する方法を示します。目的変数( \mathbf{Y} )はいくつかのカテゴリ変数から変換されると仮定します。言い換えれば、目的変数は、各カテゴリ変数をワンホットエンコードした変数全ての線形結合である。 X_i は、各 i = 0,...,K-1 のカテゴリ変数を示します。但し、 K はカテゴリ変数の総数です。 X_i の範囲は任意の i に対して \{0,...,N_i-1 \} とします。但し、 N_i X_i のカテゴリの数です。 X_i は任意の i に対して \{0,...,N_i-1 \} に一様かつ独立して分布していると仮定します。 X_i の各値は等しい確率を持ち、独立して観測されます。 X_i の値がどれくらい大きいかは、その値の指示変数が目的変数にどの程度効果的かを意味しないということに注意してください。簡略化のために、 K = 3 N_1 = 10 N_2 = 10 N_3 = 10 の場合を考えます。 \mathbf{H}_{ij} i 番目のカテゴリ変数の値 j の指示変数とし、 \beta_{ij} \mathbf{H}_{ij} の係数とします。したがって、生成式は次のようになります。

\begin{equation*} \mathbf{Y} = \beta_{-1} + \sum_{j=0}^9\beta_{0j}\mathbf{H}_{0j} + \sum_{j=0}^9\beta_{1j}\mathbf{H}_{1j} + \sum_{j=0}^9\beta_{2j}\mathbf{H}_{2j} + \varepsilon, \end{equation*}

ここで、 \beta_{-1} は切片であり、 \varepsilon は確率的ノイズです。 まず、データを生成する関数を定義します。

In [2]:
def generate_data(sample_size, categorical_dim, coef=None, intercept=0, random=0):
    np.random.seed(random)
    for i in range(len(categorical_dim)):
        dim = categorical_dim[i]
        x = np.random.randint(size=(sample_size, 1), low=0, high=dim)
        b = np.random.randint(size=(dim, 1), low=-30, high=30)
        if i == 0:
            X = x
            B = b
        else:
            X = np.concatenate((X, x), axis=1)
            B = np.concatenate((B, b), axis=0)

    if coef is not None:
        B = np.asarray(coef).reshape(-1,1)

    Y = intercept + np.dot(OneHotEncoder().fit_transform(X).toarray(), B) + np.random.randn(sample_size, 1)

    max_Y = np.max(Y)
    min_Y = np.min(Y)

    Y = ((Y - min_Y) / (max_Y - min_Y)).astype(np.float32)
    return (X, Y), (max_Y, min_Y), B
In [3]:
sample_size = 10000
categorical_dim = [10,10,10]
reduced_dim = [2,2,2]
coef = []
for d in categorical_dim:
    if d == 15:
        coef += [0 for i in range(d)]
    else:
        coef += list(range(d))

(data_X, data_y), (max_y, min_y), coef = generate_data(sample_size, categorical_dim)

print("Mean of |coefficients| of X1 = {}"
      .format(np.mean(np.abs(coef[0:np.sum(categorical_dim[:1])]))))
print("Mean of |coefficients| of X2 = {}"
      .format(np.mean(np.abs(coef[np.sum(categorical_dim[:1]):np.sum(categorical_dim[:2])]))))
print("Mean of |coefficients| of X3 = {}"
      .format(np.mean(np.abs(coef[np.sum(categorical_dim[:2]):np.sum(categorical_dim[:3])]))))

Mean of |coefficients| of X1 = 12.0
Mean of |coefficients| of X2 = 17.2
Mean of |coefficients| of X3 = 10.2

Entity embeddingレイヤのあるニューラルネットワーク

入力データをトレーニングデータとテストデータに分割します。

In [4]:
X_train, X_test, y_train, y_test = train_test_split(data_X, data_y, test_size=0.1, random_state=1)

モデルの定義

In [5]:
class NN_with_EE(rm.Model):

    def __init__(self):
        super(NN_with_EE, self).__init__()

        # Define entity embedding layers
        self._embedding_0 = rm.Embedding(reduced_dim[0], categorical_dim[0], initializer=Uniform(min=-0.05, max=0.05))
        self._embedding_1 = rm.Embedding(reduced_dim[1], categorical_dim[1], initializer=Uniform(min=-0.05, max=0.05))
        self._embedding_2 = rm.Embedding(reduced_dim[2], categorical_dim[2], initializer=Uniform(min=-0.05, max=0.05))

        # Define fully connected layers
        self._layer1 = rm.Dense(50, initializer=Uniform(min=-0.05, max=0.05))
        self._layer2 = rm.Dense(20, initializer=Uniform(min=-0.05, max=0.05))
        self._layer3 = rm.Dense(1)


    # Define forward calculation.
    def forward(self, x):
        _x = rm.Node(x)
        v1 = self._embedding_0(_x[:, 0].reshape(-1,1))
        v2 = self._embedding_1(_x[:, 1].reshape(-1,1))
        v3 = self._embedding_2(_x[:, 2].reshape(-1,1))

        z = v1

        for v in [v2, v3]:
            z = rm.concat(z, v)

        return rm.sigmoid(self._layer3(rm.relu(self._layer2(rm.relu(self._layer1(z))))))

トレーニングループ

In [6]:
optimiser = rm.Adam()

model = NN_with_EE()

epoch = 100
N = len(X_train)
batch_size = 128
train_curve = []
test_curve = []

start = time.time()
for i in range(1, epoch+1):
    perm = np.random.permutation(N)
    total_loss = 0
    for j in range(N//batch_size):
        index = perm[j*batch_size:(j+1)*batch_size]
        train_batch_X = X_train[index]
        train_batch_y = y_train[index]
        with model.train():
            z = model(train_batch_X)
            loss = rm.mean_squared_error(z, train_batch_y.reshape(-1,1))
        grad = loss.grad()
        grad.update(optimiser)
        total_loss += rm.mean_squared_error(z, train_batch_y).as_ndarray()
    train_curve.append(total_loss/(N//batch_size))

    y_test_pred = model(X_test)
    test_loss = rm.mean_squared_error(y_test_pred, y_test).as_ndarray()
    test_curve.append(test_loss)
    elapsed_time = time.time() - start

    if i%10 == 0:
        print("Epoch %02d - Train loss:%f - Test loss:%f - Elapsed time:%f"
              %(i, train_curve[-1], test_curve[-1], elapsed_time))

Epoch 10 - Train loss:0.000104 - Test loss:0.000104 - Elapsed time:2.511724
Epoch 20 - Train loss:0.000062 - Test loss:0.000062 - Elapsed time:5.062957
Epoch 30 - Train loss:0.000055 - Test loss:0.000056 - Elapsed time:7.584701
Epoch 40 - Train loss:0.000044 - Test loss:0.000041 - Elapsed time:10.109916
Epoch 50 - Train loss:0.000036 - Test loss:0.000037 - Elapsed time:12.658092
Epoch 60 - Train loss:0.000035 - Test loss:0.000034 - Elapsed time:15.182339
Epoch 70 - Train loss:0.000036 - Test loss:0.000036 - Elapsed time:17.732062
Epoch 80 - Train loss:0.000034 - Test loss:0.000037 - Elapsed time:20.301953
Epoch 90 - Train loss:0.000035 - Test loss:0.000035 - Elapsed time:22.780132
Epoch 100 - Train loss:0.000035 - Test loss:0.000035 - Elapsed time:25.319993

Entity embeddingレイヤの利用

Entity embeddingレイヤの利点の1つは、Embeddingレイヤを使用して、PCAのように各カテゴリ変数の次元を削減することです。 その利点に関する2つの例を以下に示します。

例:その他の機械学習の手法における利用

まず、K近傍法、ランダムフォレストなどの他の機械学習法とEmbeddingレイヤとの組み合わせ方法を紹介します。 ニューラルネットワークは、予測するのに非常に有用な方法ですが、物理的または社会的構造が背景に隠れている構造的データに対処することは困難です。 その結果を目的変数と入力変数との関係として解釈することも困難です。 しかし、他の機械学習法の中には、ニューラルネットワークにとって苦手なものが得意なものもあります。 したがって、他の機械学習法の入力となるようにEmbeddingレイヤの出力を使用することで、性能を改善し、データの関係性を見つけることができるかもしれません。

そこで、トレーニングデータとテストデータのEmbeddingレイヤの出力を計算します。

In [7]:
ls_layer = [model._embedding_0, model._embedding_1, model._embedding_2]

train_embed = []
test_embed = []


for i in range(len(ls_layer)):
    train_embed.append(ls_layer[i](rm.Node(X_train[:, i]).reshape(-1,1)))
    test_embed.append(ls_layer[i](rm.Node(X_test[:, i]).reshape(-1,1)))

X_train_embed = np.concatenate(train_embed, axis=1)
X_test_embed = np.concatenate(test_embed, axis=1)

K近傍回帰とランダムフォレスト回帰を例として示します。 通常、ランダムフォレストを使用する分析の場合と同様に、sklearnの関数によって特徴量の重要度を計算することができます。

In [8]:
knn_embed = KNeighborsRegressor(n_neighbors=100, weights='distance', p=1)
knn_embed.fit(X_train_embed, y_train)
y_knn_embed_predicted = knn_embed.predict(X_test_embed).reshape(-1,1)
print("MSE of K Nearest Neighbor:{:.6f}".format(np.float(rm.mean_squared_error(y_knn_embed_predicted, y_test))))


rf_embed = RandomForestRegressor(n_estimators=100, max_depth=50, min_samples_split=2, min_samples_leaf=1)
rf_embed.fit(X_train_embed, y_train.ravel())
y_rf_embed_predicted = rf_embed.predict(X_test_embed).reshape(-1,1)
print("MSE of Random Forest:{:.6f}".format(np.float(rm.mean_squared_error(y_rf_embed_predicted, y_test))))

print("Sum of feature importance of X1 = {:.3f}"
      .format(np.mean(np.sum(rf_embed.feature_importances_[0:sum(reduced_dim[:1])]))))
print("Sum of feature importance of X2 = {:.3f}"
      .format(np.mean(np.sum(rf_embed.feature_importances_[sum(reduced_dim[:1]):sum(reduced_dim[:2])]))))
print("Sum of feature importance of X3 = {:.3f}"
      .format(np.mean(np.sum(rf_embed.feature_importances_[sum(reduced_dim[:2]):sum(reduced_dim[:3])]))))
MSE of K Nearest Neighbor:0.000031
MSE of Random Forest:0.000031
Sum of feature importance of X1 = 0.263
Sum of feature importance of X2 = 0.495
Sum of feature importance of X3 = 0.242

一方、各カテゴリ変数の係数の絶対平均は、

In [9]:
print("Mean of |coefficients| of X1 = {}"
      .format(np.mean(np.abs(coef[0:np.sum(categorical_dim[:1])]))))
print("Mean of |coefficients| of X2 = {}"
      .format(np.mean(np.abs(coef[np.sum(categorical_dim[:1]):np.sum(categorical_dim[:2])]))))
print("Mean of |coefficients| of X3 = {}"
      .format(np.mean(np.abs(coef[np.sum(categorical_dim[:2]):np.sum(categorical_dim[:3])]))))
Mean of |coefficients| of X1 = 12.0
Mean of |coefficients| of X2 = 17.2
Mean of |coefficients| of X3 = 10.2

この絶対平均と比較すると、絶対平均の大きさが特徴の重要度に部分的に反映されていることがわかります。

例:次元削減法と組み合わせた可視化における利用

次に、ニューラルネットワークを用いてデータを可視化する手法の1つとして、Embedding空間の可視化を紹介します。 この例では、PCAのような次元削減法としてEmbeddingレイヤを捉えることができます。 次元削減の利点の1つは、データの可視化を可能にすることです。 しかし、Embeddingレイヤの次元は必ずしも4未満ではありません。通常、4次元空間を視覚化することはできません。 そこで、Embeddingレイヤの出力に次元削減法を適用することができます。 ここではEmbeddingレイヤの出力とワンホットエンコードされたデータを用いてPCAを行い、各第1主成分と係数との関係を示します。

In [10]:
X_train_one_hot = OneHotEncoder().fit_transform(X_train).toarray()
X_test_one_hot = OneHotEncoder().fit_transform(X_test).toarray()
In [11]:
ind = np.random.permutation(len(X_train))[:3000]

ls_name = [range(d) for d in categorical_dim]

ls_title = ["X0", "X1", "X2"]

fig, _figs = plt.subplots(ncols=3, figsize=(15,8))
d = 0
d_ = 0
for i in range(len(categorical_dim)):
    feature_embed = X_train_embed[:, d:(d+reduced_dim[i])]
    coef_ = coef[d_:(d_+categorical_dim[i])]
    dic = {k: v[0] for (k, v) in enumerate(coef_)}
    beta = np.array([dic[c] for c in X_train[ind, i]])
    pca = PCA(n_components=2)
    pca_fit= pca.fit_transform(feature_embed[ind])
    _figs[i].scatter(pca_fit[:, 0], beta, c = X_train[ind, i])
    _figs[i].set_title(ls_title[i])
    _figs[i].set_xlabel('First Principle Component')
    if i == 0:
        _figs[i].set_ylabel('Magnitude of Coefficient')
    for k in range(len(ls_name[i])):
        _figs[i].annotate(ls_name[i][k] ,xy=(pca_fit[X_train[ind, i] == k][0, 0]-0.01,
                                             beta[X_train[ind, i] == k][0]+1), size=15)
    _figs[i].grid(True)
    d += reduced_dim[i]
    d_ += categorical_dim[i]
../../../_images/notebooks_embedding_entity_embedding_notebook_32_0.png

ポイントは各プロットの対角線上にあることがわかります。 このプロットは、Embeddingレイヤの出力の第1主成分が、各指示変数に対応する係数の大きさを相当程度示すように見えます。 この単純な状況では、各指示変数の係数が、Embeddingレイヤの出力から目標変数を予測する上でどの程度影響力を持つかを復元できることがわかります。

In [12]:
fig, _figs = plt.subplots(ncols=3, figsize=(15,8))
d_ = 0
for i in range(len(categorical_dim)):
    feature_one_hot = X_train_one_hot[:, d_:(d_+categorical_dim[i])]
    coef_ = coef[d_:(d_+categorical_dim[i])]
    dic = {k: v[0] for (k, v) in enumerate(coef_)}
    beta = np.array([dic[c] for c in X_train[ind, i]])
    pca = PCA(n_components=2)
    pca_fit = pca.fit_transform(feature_one_hot[ind])
    _figs[i].scatter(pca_fit[:, 0], beta, c = X_train[ind, i])
    _figs[i].set_title(ls_title[i])
    _figs[i].set_xlabel('First Principle Component')
    if i == 0:
        _figs[i].set_ylabel('Magnitude of Coefficient')
    for k in range(len(ls_name[i])):
        _figs[i].annotate(ls_name[i][k] ,xy=(pca_fit[X_train[ind, i] == k][0, 0],
                                             beta[X_train[ind, i] == k][0]), size=15)
    _figs[i].grid(True)
    d_ += categorical_dim[i]
../../../_images/notebooks_embedding_entity_embedding_notebook_34_0.png

一方、第2のプロットは、ワンホットエンコードされたデータの主成分が係数の大きさと関連しないということを示しています。 この結果は、目標変数と指示変数との関係が線形であるという仮定に依存することに注意してください。 しかしながら、ニューラルネットワークを用いてカテゴリ変数の影響を検出する可能性があるという事実は、Embeddingレイヤの非常に有用な機能となるでしょう。

参考文献

[1] Cheng Guo and Felix Berkhahn. Entity Embeddings of Categorical Variables. CoRR, 2016. https://arxiv.org/abs/1604.06737