Prophetの論文を読んで重要箇所を抜粋

重要な箇所の抜粋

auto.arima : The methods in the figure are: auto.arima, which fits a range of ARIMA models and automatically selects the best one;(p3)

ets : which fits a collection of exponential smoothing models and selects the best (Hyndmanet al. 2002);

ETSモデル

単変量時系列分析の続き:ARIMA vs. ETS vs. Robust ETS - 渋谷駅前で働くデータサイエンティストのブログ

こちらのブログからETSモデルへの理解を抜粋すると

より直近の過去値からの影響を重視し、過去になればなるほど指数関数的に影響が小さくなるモデル 普通の加法型誤差項だけでなく乗法型誤差項も可能なモデル 以上の制約のもとで状態空間モデルにまとめたもので、モデル推定は観測値ではなく状態値に基づく ということなのかなと。これから直感的に分かるのは

直近の変動に対して追従しやすい 故に推定される信頼区間も未来になればなるほど広がりやすい 状態空間モデルなので異常値や欠損値に対して強い

最も詳しく書かれているのはこの辺

7.5 Innovations state space models for exponential smoothing | Forecasting: Principles and Practice (2nd ed)

snaive, a random walk model that makes constant predictions with weekly seasonality (seasonal naive)

snaiveというモデルも利用されている. ランダムウォークが基礎になっているモデル? 詳しく書かれているのはここ

3.1 Some simple forecasting methods | Forecasting: Principles and Practice (2nd ed)

ざっと読んだ感じ、「今」の状態は「その一つ前」に依存して発生するというそれだけの話の様子。 そのため一つ前の状態からの動作になるためrandom walk forecastと呼ばれている様子?(ランダムウォークだとその地点から定数分移動するイメージだが、これは別に加減に限らない様子なので、僕のランダムウォークに対する理解が狭義すぎる?)

で、それに季節性を持たせたものがseasonal naiveのようで。「今」と「その1シーズン前」に対して依存するという考え方でモデルを組んでいる。

さらにtbatsモデルというものも利用されている様子。

tbats, a TBATS model with both weekly and yearly seasonalities

この記事が非常に詳しい

11.1 Complex seasonality | Forecasting: Principles and Practice (2nd ed)

ハーモニック回帰に比べて季節性が時間と共にゆっくりと変化していく性質を持っている。。。ここで述べられているハーモニック回帰が見つけられなかったので、後日調査。

prophetの式

 y(t) = g(t) + s(t) + h(t) + εt.

g : トレンド項 s : 季節性項 h : 休日効果 ε : 誤差項

時系列データを回帰として扱いながらも、線形および非線形な関数を内包している

ARIMAモデルと異なる点 - 複数の期間ごとの周期性を解析し、アナリストに傾向値という知見をもたらす - 一定のデータ間隔を必須とせず、欠損値の補完も不要 - fitが早い - パラメータが解釈可能(例えばフーリエ級数など三角関数の定義の話に持っていけるので直感的(まあ人によって感じ方は違ってARIMAモデル(AR MA)のパラメータの方がわかりやすいっていう人はいるかもだけど))

それぞれの各項の詳細についてはBASEのブログで割愛する

devblog.thebase.in

正直トレンド項だけ論文及び記事を読んでも半分も理解できなかった・復習する

メモ:分解可能なモデルの利点はトレンド、週の変動性、年の変動性をそれぞれ別でみることができること。

In the Prophet model specification there are several places where analysts can alter the model to apply their expertise and external knowledge without requiring any understanding of the underlying statistics.

論文としては、コードを書く人は以下の4点さえ気にしたら良いと書いてある

  1. キャパシティ
  2. 変化点
  3. 祝日効果と周期
  4. スムージングパラメータ 例えば季節性のスムージングパラメータを調整することで、モデルに対してどのくらいの期間のデータを尊重するか決められる

解釈可能性として以下の指標を利用している

mean absolute percentage error (MAPE) for its interpretability

書いて理解するPytorchのforwardとbackward

forwardは一言で言えば順伝搬の処理を定義しています。

元々はkerasを利用していましたが、時代はpytorchみたいな雰囲気に呑まれpytorchに移行中です。
ただkerasに比べて複雑に感じる時があります。
今回はforwardを書いていて、「なんだっけこれ」と初心者してしまっておりますので、
pytorch.org
今回はこちらの公式のexampleを実行してみて、理解に努めようと思います。

データセットの用意

必要なライブラリは読み込んでおきます

import numpy as np
import pandas as pd
import torch
from sklearn.model_selection import train_test_split
from sklearn.datasets import load_iris
import matplotlib.pyplot as plt

データセットは今回アヤメを利用しようと思います。

data = pd.DataFrame(iris.data, columns=iris.feature_names)
y = iris.target

バッチサイズは25、隠れ層の次元数も適当に3にしておきます。

iris = load_iris()
data = pd.DataFrame(iris.data, columns=iris.feature_names)
y = iris.target
y = np.identity(3, dtype=np.int64)[y]
X_train, X_test, y_train, y_test = train_test_split(data.values, y, train_size=0.67, shuffle=True)
print(f"X_train: {X_train.shape}, X_test: {X_test.shape}, y_train: {y_train.shape}, y_test: {y_test.shape}")
X_train = torch.from_numpy(X_train).float()
y_train = torch.from_numpy(y_train).float()
X_test = torch.from_numpy(X_test).float()
y_test = torch.from_numpy(y_test).float()


# N is batch size; D_in is input dimension;
# H is hidden dimension; D_out is output dimension.
N, D_in, H, D_out = 25, X_train.shape[1], 3, y_train.shape[1]

まずは適当に学習させてみる

class TwoLayerNet(torch.nn.Module):
    def __init__(self, D_in, H, D_out):
        """
        In the constructor we instantiate two nn.Linear modules and assign them as
        member variables.
        """
        super(TwoLayerNet, self).__init__()
        self.linear1 = torch.nn.Linear(D_in, H)
        self.linear2 = torch.nn.Linear(H, D_out)

    def forward(self, x):
        """
        In the forward function we accept a Tensor of input data and we must return
        a Tensor of output data. We can use Modules defined in the constructor as
        well as arbitrary operators on Tensors.
        """
        h_relu = self.linear1(x).clamp(min=0)
        y_pred = self.linear2(h_relu)
        return y_pred

# Construct our model by instantiating the class defined above
model = TwoLayerNet(D_in, H, D_out)

# Construct our loss function and an Optimizer. The call to model.parameters()
# in the SGD constructor will contain the learnable parameters of the two
# nn.Linear modules which are members of the model.
criterion = torch.nn.MSELoss(reduction='sum')
optimizer = torch.optim.SGD(model.parameters(), lr=1e-4)

losses = []
testlosses = []

for t in range(50):
    # Forward pass: Compute predicted y by passing x to the model

    y_pred = model(X_train)

    # Compute and print loss
    loss = criterion(y_pred, y_train)
    losses.append(loss.item())
    if t % 10 == 0:
        print(t, loss.item())

    # Zero gradients, perform a backward pass, and update the weights.
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    with torch.no_grad():
        y_test_pred = model(X_test)
        testloss = criterion(y_test_pred, y_test)
        testlosses.append(testloss)
plt.plot(losses)
plt.plot(testlosses)

f:id:hirasakanai:20200919151917p:plain
無事学習は進んでいるようです。
モデルもexampleから適当に見繕いました。
※分類問題なのでクロスエントロピーを利用するのがベストプラクティスかもしれませんが、なぜかうまくいかなかったことと本題から逸れるため、このままMSEで行きます。

forwardを理解したい

モデルの中のforwardを理解したい

上のコードの中から抜粋

class TwoLayerNet(torch.nn.Module):
    def __init__(self, D_in, H, D_out):
        """
        In the constructor we instantiate two nn.Linear modules and assign them as
        member variables.
        """
        super(TwoLayerNet, self).__init__()
        self.linear1 = torch.nn.Linear(D_in, H)
        self.linear2 = torch.nn.Linear(H, D_out)

    def forward(self, x):
        """
        In the forward function we accept a Tensor of input data and we must return
        a Tensor of output data. We can use Modules defined in the constructor as
        well as arbitrary operators on Tensors.
        """
        h_relu = self.linear1(x).clamp(min=0)
        y_pred = self.linear2(h_relu)
        return y_pred

`torch.nn.Linear` は重みとバイアスだけを持つ簡単な結合層です。(Linear — PyTorch 1.6.0 documentation)

隠れ層は一層だけで、今回適当に3を設定しています。入力層・隠れ層・出力層をこの2行で表現しています。

        self.linear1 = torch.nn.Linear(D_in, H)
        self.linear2 = torch.nn.Linear(H, D_out)

次に本題のforwardです。

    def forward(self, x):
        h_relu = self.linear1(x).clamp(min=0)
        y_pred = self.linear2(h_relu)
        return y_pred

forwardはデータxをtensor型で受け取って、3層のレイヤーを通って出力しているだけです。
clampはデータに最小値・最大値を与えてその範囲内にデータを納めます。(torch.clamp — PyTorch 1.6.0 documentation
以下がexampleです。

>>> a = torch.randn(4)
>>> a
tensor([-1.7120,  0.1734, -0.0478, -0.0922])
>>> torch.clamp(a, min=-0.5, max=0.5)
tensor([-0.5000,  0.1734, -0.0478, -0.0922])

上記の学習コードでは最低値を0として他をそのままの値で出力しています。つまりReLU関数と同等です。そのための出力の変数もh_reluとなっています。

なぜforwardに書く必要があるのか?

本題です。私はこれに悩みました。
特にこの後実験しようとしている損失関数や活性化関数の中でもforwardメソッドは定義されます。
この辺りがすごく不思議に思えてしまったのです。

ここでニューラルネットワークの基本的な構造を思い出します。
f:id:hirasakanai:20200919154921p:plain
ニューラルネットワークの基礎を学んだなら当たり前のことなのですが、それぞれの層・活性化関数・損失関数も全て入力から出力までを順伝搬する仕組みです。
全てデータを左から受け取って右に流していく役割を持っているので、定義するのは至極当たり前でした。
それぞれをレイヤーとして認識してnn.Moduleクラスを基底として全てに順伝搬を定義しているだけです。


さらに深く理解するため、backwardの役割を改めて見ておきます。

optimizer.zero_grad()
loss.backward()
optimizer.step()

ニューラルネットワークの重みを更新するためには誤差逆伝搬法を利用します。(誤差逆伝搬法が何かについては探せばいくらでも記事が出てきますのでこちらでは割愛)
端的にいうとそれぞれの重みの勾配を計算しそれが損失関数にどれくらい影響を与えているのか計算できます。
重みの更新には多くの手法があり上記ではSGDを利用しています。

それでは上のコードが何をしているか確認していきます。
optimizer.zero_grad()は一旦「計算した勾配の初期化」だと思っておいて下さい。

次にloss.backwordを見ていきます。
lossは損失関数でここではMSE(Mean Square Error)を利用しています。ですが、損失関数と名のついた関数には変わりありません。
別の簡素な関数を用意して、事例を見ていきます。

backwardは何をしているのか。PytochのAutogradという概念。
x = torch.tensor(3.0, requires_grad=True)

簡単な関数を用意しました。 x = 3です。これを入力だと意識します。
requires_gradは勾配を自動で計算することを定義する引数です。ここでTrueとしておくと、その先にある様々の層の計算に大して、どれくらい寄与するのかその勾配を計算します。

そして次に出力としてy = 2xを定義します。

y = 2*x
print(y)
# tensor(4., grad_fn=<MulBackward0>)

この時y = 2xのxの勾配を出力します。
その値はx.gradに入っています。ただし先に勾配を計算する必要があります。

y.backward()

です。こうすることでx.gradは計算されます。xを定義する時にrequires_grad=Trueとしていないと、このタイミングでエラーが発生します。
結果はy = 2xにおけるxの勾配は2となります。

print(x.grad)
# tensor(3.)

次に伝搬のイメージをするために新しくz=5yを用意します。

z = 5*y

これは合成関数の微分を使って解くと(代入してもさくっと解けてしまいますが)zをxで微分すると15となります。これがzに対するxの勾配です。
あたらためて全て書き直すと以下のようになります。

x = torch.tensor(2.0, requires_grad=True)
y = 3*x
z = 5*y
z.backward()
print(x.grad)
# tensor(15.)

なぜ全て書き直したかというと、この時4行目のz.backwardをy.backwardにすると3が出力されるからです。ぜひ丸っとコピペして試してみてください。
つまりbackwardはrequires_grad=Trueとした変数に対して(ここではx)、目的の関数(ここではy, z)に対して微分を行った時の勾配を計算する事ができます。

(個人的に、まだ自分自身が慣れてないだけですが、z.gradとかy.gradと表示された方がみやすい気がしてしまいます。ただ実際のニューラルネットワークの各層に対してこの勾配を持つので、各層の変数から取り出せた方がいいというのも理解できます。)

そしてこの更新に対して注意が必要なのは、計算された重みは追加されていくという事です。つまりz.backward()をn実行すると、x.gradはn * 15となります。
そのためoptimizer.zero_grad()で勾配を初期化する必要が出てくるわけです。

最後にoptimizer.step()

これは重みの更新を行っています。
この行をコメントアウトすると学習の結果は以下のようになります。
f:id:hirasakanai:20200919170453p:plain
何も更新されません。

optimizer.step()で学習率と最適化手法に基づいて重みを更新しているわけです。
さらにschedulerなどを設定している時は、学習率の動的な変更もこのタイミングで行われています。

損失関数を新しく定義したい時もforward

class RMSELoss(nn.Module):
    def __init__(self, eps=1e-6):
        super().__init__()
        self.mse = nn.MSELoss()
        self.eps = eps

    def forward(self, yhat, y):
        loss = torch.sqrt(self.mse(yhat, y) + self.eps)
        return loss

これはMSEをRMSEに変更したい時のコードです。
(ちなみにこれを見て理解はできるものの本当に説明できるか不安になり、本記事につながりました)
これを利用し、上記のコードで学習し直すと。
f:id:hirasakanai:20200919171134p:plain
無事学習できました!
RMSEはMSEの平方根を取る形です。MSEに比べて外れ値に鈍感になる上損失は数字上小さくなるので(単位も異なりますので)学習を回す回数は増えましたが、ちゃんと収束しました。

活性化関数を新しく定義したい時もforward

class ReLU(Module):
    def __init__(self, inplace: bool = False):
        super(ReLU, self).__init__()
        self.inplace = inplace

    def forward(self, input: Tensor) -> Tensor:
        return F.relu(input, inplace=self.inplace)

まとめ

  • PyTorchはnn.Moduleクラスを基底とし、順伝搬の処理をforwardの中に書いている。
  • さらにnn.Moduleを基底として、それらの入力層・隠れ層・出力層・活性化関数・損失関数などを組み合わせる(つなぎ合わせる)ことでモデルを作成し学習する。
  • 学習はAutogradという勾配を自動で計算する仕組みに支えられている。
  • そのAutogradを利用して、optimizer.zero_grad()で勾配を初期化し、loss.barkward()でそのエポックにおける損失に対する勾配を計算し、optimizer.step()で重みの更新を行う。この繰り返しで学習が進む。


ご覧いただきありがとうございました!

下記書籍とブログも参考にしています。
つくりながら学ぶ!PyTorchによる発展ディープラーニング | 小川 雄太郎 | 工学 | Kindleストア | Amazon
PyTorchは誤差逆伝播とパラメータ更新をどうやって行っているのか? - け日記

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


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

pytorch.nn.Embeddingが何をしているのか理解したい【入門】

embeddingを直訳すると「埋め込み・嵌め込み」みたいな意味です。
ここで行っているembeddingはWord embeddings(単語埋め込み)など、自然言語処理などで言われるembeddingの意味で、
何かの特徴を特定のベクトルに変換する意味です。

実際にコードを実行してみようと思います。

下準備

hours = np.random.randint(0, 24, 5)
dayofweek = np.random.randint(0, 7, 5)
catz = torch.tensor(np.concatenate([hours, dayofweek]).reshape(2,5).T)
catz
tensor([[ 4,  3],
        [16,  5],
        [18,  2],
        [18,  0],
        [ 7,  5]])

上記のような5*2のテンソルを作成しました。hoursとdayofweekである必要はないですけれども、なんとなくのイメージです。

Embeddingでベクトルに埋め込む

実際に一つのクラスに埋め込むことを想定してコードを書いてみます。
複数の列をembeddingしたいことをイメージして、nn.ModuleListに格納します。

# 24時間を12次元(tensor)に7つの曜日を4時限tensorに変化を試みる
emb_szs = [(24, 6), (7, 4)]
# torch.nn.ModuleListに格納する ni, nf = inputするカテゴリ数, 変換するベクトルの次元数
selfembeds = nn.ModuleList([nn.Embedding(ni, nf) for ni,nf in emb_szs])
selfembeds
ModuleList(
  (0): Embedding(24, 6)
  (1): Embedding(7, 4)
)

nn.ModuleListに格納することで、nn.Moduleのクラス内で定義した時にModuleの一つのレイヤーとして確認できるようになります。


これを実行に移すときはnn.Embedding(ni, nf)(<テンソル>)として実行するだけです。

embeddingz = []
for i,e in (selfembeds):
    embeddingz.append(e(catz[:,i]))
embeddingz
[tensor([[ 0.0934, -0.6163, -0.8566, -0.2958, -0.2675,  0.7492],
         [ 0.2193,  0.2933, -0.3107,  0.5948,  0.6845,  3.0315],
         [-0.5964,  0.0574,  0.0242, -0.0439, -0.5735, -0.5559],
         [-0.9389,  0.7380, -0.4325, -0.0264, -0.5290,  0.2135],
         [ 0.2193,  0.2933, -0.3107,  0.5948,  0.6845,  3.0315]],
        grad_fn=<EmbeddingBackward>),
 tensor([[-0.9179, -0.3926,  0.2028,  0.5319],
         [-0.9692, -0.5956,  3.2422, -0.9217],
         [ 0.5377,  0.1080, -1.4133,  0.2854],
         [-1.0105, -0.4767, -0.2607,  0.3381],
         [-0.9692, -0.5956,  3.2422, -0.9217]], grad_fn=<EmbeddingBackward>)]

上記のように6次元のベクトルと4次元のベクトルに変換されます。

embeddingの使い所

本コードの元はudemyの
PyTorch for Deep Learning with Python Bootcamp | Udemy
で登場したコードでした。その際も似たような変数に対して上記embeddingを実装していました。

実際に調べてみると自然言語処理の領域で利用されることの多いレイヤーのようです。

class NGramLanguageModeler(nn.Module):

    def __init__(self, vocab_size, embedding_dim, context_size):
        super(NGramLanguageModeler, self).__init__()
        self.embeddings = nn.Embedding(vocab_size, embedding_dim)
        self.linear1 = nn.Linear(context_size * embedding_dim, 128)
        self.linear2 = nn.Linear(128, vocab_size)

    def forward(self, inputs):
        embeds = self.embeddings(inputs).view((1, -1))
        out = F.relu(self.linear1(embeds))
        out = self.linear2(out)
        log_probs = F.log_softmax(out, dim=1)
        return log_probs

上記のコードではnn.Embeddingを利用してN-gramモデルを実装しています。
自然言語に置いて単語をone-hot-encodingすると次元数は大きくなり、
ある一定の文章量から計算が不可能となります。
そこで次元数を限定し言語をベクトルで表現することで、計算量を低く抑えることができます。
またEmbeddingは学習の対象となり、重みを持ちます。
学習済の重みを反映させることも可能で、その重みは単語の特徴量として捉えることもできます。