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つ(フィルター数)生成される。
イメージとしてはこんな感じ。
※畳み込んだ後の特徴マップはただのイメージ
複数チャネルでの畳み込みも同じである。
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チャネル)でのイメージは下記の通り。
公式ドキュメントでは出力は以下の式で計算されるとのことなので、各フィルターの畳み込み結果の総和が特徴マップとなるイメージ。
※複数チャネルあっても特徴マップではそれらが1つに足し算されるというとことがポイント。
※1Dと2Dの違いはフィルターの適用方法。
以下の絵が分かりやすかった。
Computer Vision - Image Filters(p.21)
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)