NNの中間層の出力ベクトルを特徴量として利用してみたい

学習済のニューラルネットワークの中間層の出力はそれ自体が特徴量となります。
頭では理解しているものの、今まで実装したことはありませんでした。

今回はtomaCup第5回のときの
しみしげ (@nyanchdayooon) | Twitter さんの 1D-CNN & Extract Feature Vectors
katsu1110 (@kk1110tt) | Twitter さんの GBDTと昆布の物語(Private 2位、Public 5位の解法) を参考に、
probspaceのスプラトゥーンコンペを題材に実装してみたいと思います。

(ただこちらのコンペ、開始から2週間以上経ってもTOPのaccuracyが0.569090なので、可視化した時に対して大きな差が目に見えない可能性もあり、すでに題材ミスの可能性も・・・)

下準備

データの用意

TRAIN_DATA_PATH = "./data/train_data.csv"
TEST_DATA_PATH = "./data/test_data.csv"
# e-toppoさんの申請してくださった武器詳細データ
# https://prob.space/competitions/game_winner/discussions/e-toppo-Post0082a60376ef134af3a4
WEAPON_DATA_PATH = "./data/weapon.csv"

データはコンペで提出されているテストデータ、訓練データ、トピック(ディスカッション)で申請し許可が降りていた外部データを利用します。

欠損値の補完、不要な列データの削除

データの前処理を行います。日付などは勝敗と無関係だと判断したためいくつかの特徴量を削りました。
また欠損値は−1や、最も頻度の高い値で埋めました。本題と逸れるため、詳細なコードはprobspaceに記載した物をご確認ください。
対戦ゲームデータ分析甲子園 | ProbSpace

学習

学習の前に数値データとカテゴリデータに分類します。

nums = np.stack([X[col].astype(np.float16).values for col in num_features], 1)
nums = torch.tensor(nums, dtype=torch.float)
cats = np.stack([X[col].values for col in cat_features], 1)
cats = torch.tensor(cats, dtype=torch.int64)

学習させるクラスは以下のように定義しています。

class TabularModel(nn.Module):

    def __init__(self, emb_szs, n_cont, out_sz, layers, p=0.5):
        super().__init__()
        self.embeds = nn.ModuleList([nn.Embedding(ni, nf) for ni,nf in emb_szs])
        self.emb_drop = nn.Dropout(p)
        self.bn_cont = nn.BatchNorm1d(n_cont)
        
        layerlist = []
        n_emb = sum((nf for ni,nf in emb_szs))
        n_in = n_emb + n_cont
        
        for i in layers:
            layerlist.append(nn.Linear(n_in,i)) 
            layerlist.append(nn.ReLU(inplace=True))
            layerlist.append(nn.BatchNorm1d(i))
            layerlist.append(nn.Dropout(p))
            n_in = i
        layerlist.append(nn.Linear(layers[-1],out_sz))
            
        self.layers = nn.Sequential(*layerlist)
    
    def forward(self, x_cat, x_cont):
        embeddings = []
        for i,e in enumerate(self.embeds):
            embeddings.append(e(x_cat[:,i]))
        x = torch.cat(embeddings, 1)
        x = self.emb_drop(x)
        
        x_cont = self.bn_cont(x_cont)
        x = torch.cat([x, x_cont], 1)
        x = self.layers(x)
        return x

コードは PyTorch for Deep Learning with Python Bootcamp | Udemy をベースとしています。

学習した結果は以下のようになりました。
f:id:hirasakanai:20200830172617p:plain

学習は無事進んでいるようです。

モデルの確認

デルレイヤーは現在このようになっています。

print(model)
TabularModel(
  (embeds): ModuleList(
    (0): Embedding(2, 1)
    (1): Embedding(5, 3)
    (2): Embedding(23, 12)
    (3): Embedding(13, 7)
    (4): Embedding(13, 7)
    (5): Embedding(13, 7)
    (6): Embedding(13, 7)
    (7): Embedding(13, 7)
    (8): Embedding(13, 7)
    (9): Embedding(13, 7)
    (10): Embedding(13, 7)
    (11): Embedding(13, 7)
    (12): Embedding(15, 8)
    (13): Embedding(48, 24)
    (14): Embedding(10, 5)
    (15): Embedding(13, 7)
    (16): Embedding(15, 8)
    (17): Embedding(48, 24)
    (18): Embedding(10, 5)
    (19): Embedding(13, 7)
    (20): Embedding(15, 8)
    (21): Embedding(48, 24)
    (22): Embedding(10, 5)
    (23): Embedding(13, 7)
    (24): Embedding(15, 8)
    (25): Embedding(48, 24)
    (26): Embedding(10, 5)
    (27): Embedding(13, 7)
    (28): Embedding(15, 8)
    (29): Embedding(48, 24)
    (30): Embedding(10, 5)
    (31): Embedding(13, 7)
    (32): Embedding(15, 8)
    (33): Embedding(48, 24)
    (34): Embedding(10, 5)
    (35): Embedding(13, 7)
    (36): Embedding(15, 8)
    (37): Embedding(48, 24)
    (38): Embedding(10, 5)
    (39): Embedding(13, 7)
    (40): Embedding(15, 8)
    (41): Embedding(48, 24)
    (42): Embedding(10, 5)
  )
  (emb_drop): Dropout(p=0.5, inplace=False)
  (bn_cont): BatchNorm1d(8, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
  (layers): Sequential(
    (0): Linear(in_features=432, out_features=200, bias=True)
    (1): ReLU(inplace=True)
    (2): BatchNorm1d(200, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (3): Dropout(p=0.5, inplace=False)
    (4): Linear(in_features=200, out_features=100, bias=True)
    (5): ReLU(inplace=True)
    (6): BatchNorm1d(100, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (7): Dropout(p=0.5, inplace=False)
    (8): Linear(in_features=100, out_features=2, bias=True)
  )
)

大量のカテゴリ変数をベクトルに置き換えるためのemdded層と数値データを取り込むBatchNorm1d、
そのあと

(0): Linear(in_features=432, out_features=200, bias=True)
(1): ReLU(inplace=True)
(2): BatchNorm1d(200, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(3): Dropout(p=0.5, inplace=False)

のReLUを活性化関数にして全結合層を二層繰り返しています。

中間層のの出力を取得する

    def forward(self, x_cat, x_cont):
        embeddings = []
        for i,e in enumerate(self.embeds):
            embeddings.append(e(x_cat[:,i]))
        x = torch.cat(embeddings, 1)
        x = self.emb_drop(x)
        
        x_cont = self.bn_cont(x_cont)
        x = torch.cat([x, x_cont], 1)
        x = self.layers(x)
        return x

これを実行するためには、クラスの中のforwardと同様のことを実施すると良いです。
例えば上記のコード3行目の部分、 self.embededの部分をmodel.embedsのようにして属性にアクセスできます。

# catsはカテゴリ変数のみを入れたテンソルです。
# cats = np.stack([X[col].values for col in cat_features], 1)
# cats = torch.tensor(cats, dtype=torch.int64)

print(cats.shape)
embeddings = []
for i, emded in enumerate(model.embeds):
    embeddings.append(emded(cats[:,i]))
embeddings = torch.cat(embeddings, 1)

print(embeddings.shape)

出力
無事43列のカテゴリデータが424次元のベクトルに変換されました。

torch.Size([66125, 43])
torch.Size([66125, 424])

次元削減してみる

最後上記で取得したembeddingsを2次元空間にプロットしてみようと思います。

from sklearn.decomposition import PCA 
pca = PCA()
embeddings = embeddings.detach().numpy().copy()
pca.fit(embeddings)
feature = pca.transform(embeddings)

df = pd.DataFrame(feature).head()
plt.figure(figsize=(6, 6))
plt.scatter(feature[:, 0], feature[:, 1], alpha=0.8, c=y)
plt.grid()
plt.xlabel("PC1")
plt.ylabel("PC2")
plt.show()

f:id:hirasakanai:20200830180137p:plain


・・・・・・・題材ミスですね!こういう僅差のデータを可視化するのは一体どうしたらいいのやら。。。