機械学習・自然言語処理の勉強メモ

学んだことのメモやまとめ

Pytorch:CNNでテキスト分類

はじめに



かなり前にTheanoでの実装例を解説した。

TheanoでSentiment analysis (CNN)① - 機械学習・自然言語処理の勉強メモ

今回は、Pytorchでの実装例を解説する。

下記のコードを実装例としてみていく。
github.com

前処理や学習のところは今回の本質ではないので割愛する。
(今回、対象とするのはmodel.pyのみ)

Embedding



入力を埋め込みベクトルで表現する。

nn.Embedding(V, D)  # vocab_size, emb_dim

以下は公式チュートリアルの例。

import torch
import torch.autograd as autograd
import torch.nn as nn

torch.manual_seed(1)
word_to_ix = {"hello": 0, "world": 1}
vocab_size = len(word_to_ix)
emb_dim = 5
embeds = nn.Embedding(vocab_size, emb_dim)
print(embeds)
# Embedding(2, 5)
lookup_tensor = torch.LongTensor([word_to_ix["hello"]])
hello_embed = embeds(lookup_tensor)
print(hello_embed)
# tensor([[ 0.6614,  0.2669,  0.0617,  0.6213, -0.4519]])

Conv2d



畳み込み層。
以下、ドキュメントでの使い方を引用する。

nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True)

簡単な実験例は以下の通り。

import torch
import torch.nn as nn

in_channels = 1
out_channels = 2
kernel_size = 3

conv = nn.Conv2d(in_channels, out_channels, kernel_size)
print(conv.weight)
tensor([[[[ 0.0764, -0.1259, -0.0754],
          [-0.2797,  0.1951,  0.0169],
          [ 0.0865,  0.0816,  0.1379]]],

        [[[-0.1002,  0.2892,  0.0825],
          [-0.2065, -0.0684,  0.3239],
          [ 0.1186,  0.0592,  0.1452]]]])

3×3のフィルターが2枚用意される。

これに簡単な入力を用意して畳み込みを行う。

x = torch.rand(1, 8, 8)
x = x.unsqueeze(1)

print(conv(x))
tensor([[[[-0.2262, -0.2997, -0.2248, -0.5556, -0.4375, -0.0610],
          [ 0.0063, -0.3972, -0.5817, -0.1896, -0.1904, -0.3476],
          [-0.3111, -0.4411, -0.2071, -0.0960, -0.1626, -0.2131],
          [-0.0050, -0.3743, -0.3604, -0.1137, -0.5571, -0.6160],
          [-0.3465, -0.0116, -0.6898, -0.4880, -0.3624, -0.2736],
          [-0.2974, -0.5222, -0.4942, -0.3071, -0.1523, -0.4169]],

         [[-0.0848,  0.4376,  0.3651,  0.0187,  0.2298,  0.1498],
          [ 0.1729,  0.3434,  0.0663,  0.1013,  0.3041,  0.2947],
          [ 0.0351,  0.3380,  0.0028,  0.1966,  0.4254,  0.2505],
          [ 0.0633,  0.0562,  0.2415,  0.1636, -0.1736,  0.1962],
          [ 0.3215,  0.3927,  0.1248, -0.0799, -0.0342,  0.3068],
          [ 0.2358,  0.5135,  0.1664, -0.0694,  0.1384,  0.4279]]]])
conv(x).size()
torch.Size([1, 2, 6, 6])

これは8×8の画像を1枚だけ用意して、畳み込みを行った結果。畳み込んだ後、6×6の特徴マップが2つ(フィルター数)生成される。

イメージとしてはこんな感じ。
f:id:kento1109:20180527125549p:plain
※畳み込んだ後の特徴マップはただのイメージ

複数チャネルでの畳み込みも同じである。

import torch
import torch.nn as nn

in_channels = 2
out_channels = 2
kernel_size = 3

conv = nn.Conv2d(in_channels, out_channels, kernel_size)
print(conv.weight)
tensor([[[[-0.1868, -0.0729, -0.1114],
          [-0.1679, -0.0790, -0.0492],
          [ 0.1048,  0.0593,  0.1306]],

         [[-0.1491, -0.0007, -0.1044],
          [-0.1816,  0.1241, -0.0810],
          [-0.1419,  0.0259, -0.1619]],

         [[-0.1226, -0.0513,  0.1501],
          [-0.0697, -0.0989, -0.0419],
          [-0.0267, -0.0147, -0.1794]]],


        [[[-0.1227, -0.1169,  0.1135],
          [-0.1095, -0.0382,  0.0035],
          [ 0.1144,  0.0448, -0.0450]],

         [[ 0.1692,  0.1316, -0.1729],
          [-0.0667, -0.1245,  0.1320],
          [ 0.1550, -0.1849,  0.0248]],

         [[ 0.1105, -0.0140,  0.1593],
          [-0.0641, -0.1699, -0.1260],
          [ 0.0953, -0.0339, -0.1776]]]])

3×3のフィルター2枚が3つ(チャネル数)用意される。

x = torch.rand(1, 3, 8, 8)

print(conv(x))
tensor([[[[ 0.2663,  0.0400,  0.1126, -0.0881, -0.1289,  0.2598],
          [ 0.3221, -0.1000,  0.2507, -0.0707,  0.1994, -0.3522],
          [-0.0926, -0.0729, -0.0659,  0.1827, -0.0877, -0.2633],
          [-0.0701,  0.0373,  0.1196, -0.0296, -0.1721,  0.0874],
          [-0.0849, -0.0340,  0.1357,  0.1637, -0.2666, -0.0477],
          [-0.1877,  0.0480,  0.0536,  0.1316, -0.1048,  0.1732]],

         [[-0.0516, -0.0644, -0.2848, -0.1315, -0.1382,  0.0227],
          [-0.2444, -0.4360, -0.4112, -0.0151, -0.1281, -0.3945],
          [-0.4198, -0.1515, -0.4334, -0.2968, -0.2997, -0.4605],
          [-0.2708, -0.2118, -0.5194, -0.0957,  0.0198, -0.1555],
          [-0.3458, -0.4015, -0.4023, -0.6113, -0.2948, -0.2127],
          [-0.3556, -0.0482, -0.0632, -0.1151,  0.0919, -0.2329]]]])
conv(x).size()
torch.Size([1, 2, 6, 6])

畳み込み後の出力サイズは1チャネルと等しくなる。

複数チャネル(RGBの3チャネル)でのイメージは下記の通り。
f:id:kento1109:20180527131757p:plain

公式ドキュメントでは出力は以下の式で計算されるとのことなので、各フィルターの畳み込み結果の総和が特徴マップとなるイメージ。
※複数チャネルあっても特徴マップではそれらが1つに足し算されるというとことがポイント。


\begin{equation*}
\text{out}(N_i, C_{out_j}) = \text{bias}(C_{out_j}) +
                        \sum_{k = 0}^{C_{in} - 1} \text{weight}(C_{out_j}, k) \star \text{input}(N_i, k)
\end{equation*}

※1Dと2Dの違いはフィルターの適用方法。
以下の絵が分かりやすかった。
f:id:kento1109:20180527112232p:plain
Computer Vision - Image Filters(p.21)

f:id:kento1109:20180527112133p:plain
Recurrent Neural Networks II (D2L3 Deep Learning for Speech and Langu…(p.10)

2枚目の絵を見る限り、NLPの場合は1Dでも2Dでも変わらない気がする・・

コードでは以下のようにして異なるカーネルサイズの畳み込み層を用意している。

nn.ModuleList([nn.Conv2d(Ci, Co, (K, D)) for K in Ks])

今回はチャネル数は1で固定しているが、分散表現でチャネルを分けたい場合などは、第一引数でチャネル数を指定すればよい。

これを訓練時は以下のようにして適用する。

x = [F.relu(conv(x)).squeeze(3) for conv in self.convs1]

max_pool1d



畳み込んだ後の結果をmax_poolingを取る。

x = [F.max_pool1d(i, i.size(2)).squeeze(2) for i in x] 

この辺りは割とシンプルな気がする。

最後にバラバラの計算結果を1つの行列にまとめる。

x = torch.cat(x, 1)

catテンソルの結合する関数。

x = torch.from_numpy(np.array([[1, 2, 3],[7, 8, 9]]))
y = torch.from_numpy(np.array([[4, 5, 6],[10,11,12]]))
z = [x,y]
torch.cat(z,1)
# tensor([[  1,   2,   3,   4,   5,   6],
#         [  7,   8,   9,  10,  11,  12]])

線形結合



max_poolingを取った結果を1つの行列にまとめた後、それを入力として線形結合して指定したクラス数の行列に変換する。

nn.Linear(len(Ks)*Co, C)