16.3. 畳み込みニューラルネットワークを用いた感情分析¶
7 章 では、 2次元CNNを用いて 2次元画像データを処理する 仕組みを調べた。 そこでは、 隣接ピクセルのような 局所特徴に対して CNNが適用された。 CNNはもともと コンピュータビジョン向けに 設計されたが、 自然言語処理でも広く使われている。 簡単に言えば、 任意のテキスト系列を 1次元画像だと考えればよいのである。 このようにすると、 1次元CNNは テキスト中の \(n\)-gram のような 局所特徴を処理できる。
この節では、 単一のテキストを表現するためのCNNアーキテクチャを どのように設計するかを示すために、 textCNN モデルを用いる (Kim, 2014)。 感情分析のために GloVeの事前学習を用いたRNNアーキテクチャを使う 図 16.2.1 と比べると、 図 16.3.1 における 唯一の違いは アーキテクチャの選択にある。
図 16.3.1 この節では、事前学習済みのGloVeをCNNベースの感情分析アーキテクチャに入力する。¶
from d2l import torch as d2l
import torch
from torch import nn
batch_size = 64
train_iter, test_iter, vocab = d2l.load_data_imdb(batch_size)
Downloading ../data/aclImdb_v1.tar.gz from http://d2l-data.s3-accelerate.amazonaws.com/aclImdb_v1.tar.gz...
from d2l import mxnet as d2l
from mxnet import gluon, init, np, npx
from mxnet.gluon import nn
npx.set_np()
batch_size = 64
train_iter, test_iter, vocab = d2l.load_data_imdb(batch_size)
[07:17:55] ../src/storage/storage.cc:196: Using Pooled (Naive) StorageManager for CPU
def corr1d(X, K):
w = K.shape[0]
Y = d2l.zeros((X.shape[0] - w + 1))
for i in range(Y.shape[0]):
Y[i] = (X[i: i + w] * K).sum()
return Y
def corr1d(X, K):
w = K.shape[0]
Y = d2l.zeros((X.shape[0] - w + 1))
for i in range(Y.shape[0]):
Y[i] = (X[i: i + w] * K).sum()
return Y
16.3.1. 1次元畳み込み¶
モデルを紹介する前に、 1次元畳み込みがどのように動作するかを見てみよう。 これは、 相互相関演算に基づく 2次元畳み込みの特殊な場合にすぎないことを 念頭に置いよ。
図 16.3.2 1次元の相互相関演算。網掛け部分は最初の出力要素と、その出力計算に使われる入力テンソルおよびカーネルテンソルの要素を示している: \(0\times1+1\times2=2\).¶
図 16.3.2 に示すように、 1次元の場合、 畳み込みウィンドウは 入力テンソル上を 左から右へと スライドする。 スライド中、 畳み込みウィンドウの ある位置に含まれる 入力部分テンソル(たとえば 図 16.3.2 の \(0\) と \(1\))と カーネルテンソル(たとえば 図 16.3.2 の \(1\) と \(2\))を 要素ごとに掛け合わせる。 これらの積の和が、 出力テンソルの対応する位置における 1つのスカラー値(たとえば 図 16.3.2 の \(0\times1+1\times2=2\))を 与える。
以下の corr1d 関数で 1次元の相互相関を実装する。 入力テンソル X
と カーネルテンソル K が与えられると、 出力テンソル Y を返す。
def corr1d(X, K):
w = K.shape[0]
Y = d2l.zeros((X.shape[0] - w + 1))
for i in range(Y.shape[0]):
Y[i] = (X[i: i + w] * K).sum()
return Y
def corr1d(X, K):
w = K.shape[0]
Y = d2l.zeros((X.shape[0] - w + 1))
for i in range(Y.shape[0]):
Y[i] = (X[i: i + w] * K).sum()
return Y
X, K = d2l.tensor([0, 1, 2, 3, 4, 5, 6]), d2l.tensor([1, 2])
corr1d(X, K)
X, K = d2l.tensor([0, 1, 2, 3, 4, 5, 6]), d2l.tensor([1, 2])
corr1d(X, K)
図 16.3.2 の入力テンソル X とカーネルテンソル K
を構成して、 上の1次元相互相関実装の出力を検証できる。
X, K = d2l.tensor([0, 1, 2, 3, 4, 5, 6]), d2l.tensor([1, 2])
corr1d(X, K)
tensor([ 2., 5., 8., 11., 14., 17.])
X, K = d2l.tensor([0, 1, 2, 3, 4, 5, 6]), d2l.tensor([1, 2])
corr1d(X, K)
array([ 2., 5., 8., 11., 14., 17.])
def corr1d_multi_in(X, K):
# まず、`X` と `K` の0次元目(チャネル次元)を反復する。
# その後、それらを足し合わせる
return sum(corr1d(x, k) for x, k in zip(X, K))
X = d2l.tensor([[0, 1, 2, 3, 4, 5, 6],
[1, 2, 3, 4, 5, 6, 7],
[2, 3, 4, 5, 6, 7, 8]])
K = d2l.tensor([[1, 2], [3, 4], [-1, -3]])
corr1d_multi_in(X, K)
def corr1d_multi_in(X, K):
# まず、`X` と `K` の0次元目(チャネル次元)を反復する。
# その後、それらを足し合わせる
return sum(corr1d(x, k) for x, k in zip(X, K))
X = d2l.tensor([[0, 1, 2, 3, 4, 5, 6],
[1, 2, 3, 4, 5, 6, 7],
[2, 3, 4, 5, 6, 7, 8]])
K = d2l.tensor([[1, 2], [3, 4], [-1, -3]])
corr1d_multi_in(X, K)
複数チャネルを持つ任意の 1次元入力に対しては、 畳み込みカーネルも 同じ数の入力チャネルを持つ必要がある。 その後、各チャネルごとに、 入力の1次元テンソルと畳み込みカーネルの1次元テンソルに対して 相互相関演算を行い、 すべてのチャネルにわたる結果を 足し合わせて 1次元の出力テンソルを生成する。 図 16.3.3 は 3つの入力チャネルを持つ 1次元相互相関演算を示している。
図 16.3.3 3つの入力チャネルを持つ1次元の相互相関演算。網掛け部分は最初の出力要素と、その出力計算に使われる入力テンソルおよびカーネルテンソルの要素を示している: \(0\times1+1\times2+1\times3+2\times4+2\times(-1)+3\times(-3)=2\).¶
複数入力チャネルを持つ 1次元相互相関演算を実装し、 図 16.3.3 の結果を検証できる。
def corr1d_multi_in(X, K):
# まず、`X` と `K` の0次元目(チャネル次元)を反復する。
# その後、それらを足し合わせる
return sum(corr1d(x, k) for x, k in zip(X, K))
X = d2l.tensor([[0, 1, 2, 3, 4, 5, 6],
[1, 2, 3, 4, 5, 6, 7],
[2, 3, 4, 5, 6, 7, 8]])
K = d2l.tensor([[1, 2], [3, 4], [-1, -3]])
corr1d_multi_in(X, K)
tensor([ 2., 8., 14., 20., 26., 32.])
def corr1d_multi_in(X, K):
# まず、`X` と `K` の0次元目(チャネル次元)を反復する。
# その後、それらを足し合わせる
return sum(corr1d(x, k) for x, k in zip(X, K))
X = d2l.tensor([[0, 1, 2, 3, 4, 5, 6],
[1, 2, 3, 4, 5, 6, 7],
[2, 3, 4, 5, 6, 7, 8]])
K = d2l.tensor([[1, 2], [3, 4], [-1, -3]])
corr1d_multi_in(X, K)
array([ 2., 8., 14., 20., 26., 32.])
d2l.predict_sentiment(net, vocab, 'this movie is so great')
d2l.predict_sentiment(net, vocab, 'this movie is so great')
複数入力チャネルを持つ 1次元相互相関は、 単一入力チャネルの 2次元相互相関と等価であることに 注意しよ。 例として、 図 16.3.3 における 複数入力チャネルの1次元相互相関に対応する形は、 図 16.3.4 における 単一入力チャネルの 2次元相互相関である。 ここでは、畳み込みカーネルの高さは 入力テンソルの高さと 同じでなければならない。
図 16.3.4 単一入力チャネルを持つ2次元の相互相関演算。網掛け部分は最初の出力要素と、その出力計算に使われる入力テンソルおよびカーネルテンソルの要素を示している: \(2\times(-1)+3\times(-3)+1\times3+2\times4+0\times1+1\times2=2\).¶
図 16.3.2 と 図 16.3.3 の両方の出力は、 どちらも1チャネルしか持たない。 7.4.2 章 で説明した 複数出力チャネルを持つ2次元畳み込みと同様に、 1次元畳み込みでも 複数の出力チャネルを指定できる。
16.3.2. 時間方向最大プーリング¶
同様に、プーリングを用いて 系列表現から 各時刻にわたる最も重要な特徴として 最大値を抽出できる。 textCNNで使われる max-over-time pooling は、 1次元のグローバル最大プーリング
のように動作する。 各チャネルが 異なる時刻の値を保持する 多チャネル入力に対しては、 各チャネルの出力は そのチャネルの最大値になる。 なお、 max-over-time pooling では チャネルごとに 時刻数が異なっていても構わない。
16.3.3. textCNNモデル¶
1次元畳み込みと max-over-time pooling を用いて、 textCNNモデルは 個々の事前学習済みトークン表現を入力とし、 その後、 下流タスク向けの系列表現を 得て変換する。
\(n\) 個のトークンを \(d\) 次元ベクトルで表した 単一のテキスト系列に対して、 入力テンソルの幅、高さ、チャネル数は それぞれ \(n\)、\(1\)、\(d\) である。 textCNNモデルは 次のように入力を出力へ変換する。
複数の1次元畳み込みカーネルを定義し、入力に対してそれぞれ個別に畳み込み演算を行う。幅の異なる畳み込みカーネルは、異なる数の隣接トークン間の局所特徴を捉えられる。
すべての出力チャネルに対して max-over-time pooling を行い、その後、すべてのスカラープーリング出力を連結してベクトルにする。
連結したベクトルを全結合層で出力カテゴリへ変換する。過学習を抑えるためにドロップアウトを使える。
図 16.3.5 textCNNのモデルアーキテクチャ。¶
図 16.3.5 は、 具体例を用いて textCNNのモデルアーキテクチャを示している。 入力は11個のトークンからなる文で、 各トークンは6次元ベクトルで表されている。 したがって、幅11の6チャネル入力がある。 幅が2と4の 2つの1次元畳み込みカーネルを定義し、 それぞれ4チャネルと5チャネルの出力を持たせる。 それらは、 幅 \(11-2+1=10\) の4つの出力チャネルと、 幅 \(11-4+1=8\) の5つの出力チャネルを生成する。 これら9チャネルは幅が異なるが、 max-over-time pooling により 連結された9次元ベクトルが得られ、 最終的に 2次元出力ベクトルへ変換されて 二値感情予測を行う。
16.3.3.1. モデルの定義¶
以下のクラスでtextCNNモデルを実装する。 16.2 章 の双方向RNNモデルと比べると、 再帰層を畳み込み層に置き換えるだけでなく、 2つの埋め込み層も使う。 1つは学習可能な重みを持ち、 もう1つは固定重みを持つ。
class TextCNN(nn.Module):
def __init__(self, vocab_size, embed_size, kernel_sizes, num_channels,
**kwargs):
super(TextCNN, self).__init__(**kwargs)
self.embedding = nn.Embedding(vocab_size, embed_size)
# 学習しない埋め込み層
self.constant_embedding = nn.Embedding(vocab_size, embed_size)
self.dropout = nn.Dropout(0.5)
self.decoder = nn.Linear(sum(num_channels), 2)
# max-over-time pooling 層にはパラメータがないので、このインスタンスは
# 共有できる
self.pool = nn.AdaptiveAvgPool1d(1)
self.relu = nn.ReLU()
# 複数の1次元畳み込み層を作成する
self.convs = nn.ModuleList()
for c, k in zip(num_channels, kernel_sizes):
self.convs.append(nn.Conv1d(2 * embed_size, c, k))
def forward(self, inputs):
# 形状 (batch size, no.
# of tokens, token vector dimension) の2つの埋め込み層の出力をベクトル方向に連結する
embeddings = torch.cat((
self.embedding(inputs), self.constant_embedding(inputs)), dim=2)
# 1次元畳み込み層の入力形式に合わせて、
# テンソルを並べ替え、2次元目にチャネルを格納する
embeddings = embeddings.permute(0, 2, 1)
# 各1次元畳み込み層について、max-over-time
# pooling の後、形状 (batch size, no. of channels, 1) のテンソルが
# 得られる。最後の次元を削除し、チャネル方向に連結する
encoding = torch.cat([
torch.squeeze(self.relu(self.pool(conv(embeddings))), dim=-1)
for conv in self.convs], dim=1)
outputs = self.decoder(self.dropout(encoding))
return outputs
class TextCNN(nn.Block):
def __init__(self, vocab_size, embed_size, kernel_sizes, num_channels,
**kwargs):
super(TextCNN, self).__init__(**kwargs)
self.embedding = nn.Embedding(vocab_size, embed_size)
# 学習しない埋め込み層
self.constant_embedding = nn.Embedding(vocab_size, embed_size)
self.dropout = nn.Dropout(0.5)
self.decoder = nn.Dense(2)
# max-over-time pooling 層にはパラメータがないので、このインスタンスは
# 共有できる
self.pool = nn.GlobalMaxPool1D()
# 複数の1次元畳み込み層を作成する
self.convs = nn.Sequential()
for c, k in zip(num_channels, kernel_sizes):
self.convs.add(nn.Conv1D(c, k, activation='relu'))
def forward(self, inputs):
# 形状 (batch size, no.
# of tokens, token vector dimension) の2つの埋め込み層の出力をベクトル方向に連結する
embeddings = np.concatenate((
self.embedding(inputs), self.constant_embedding(inputs)), axis=2)
# 1次元畳み込み層の入力形式に合わせて、
# テンソルを並べ替え、2次元目にチャネルを格納する
embeddings = embeddings.transpose(0, 2, 1)
# 各1次元畳み込み層について、max-over-time
# pooling の後、形状 (batch size, no. of channels, 1) のテンソルが
# 得られる。最後の次元を削除し、チャネル方向に連結する
encoding = np.concatenate([
np.squeeze(self.pool(conv(embeddings)), axis=-1)
for conv in self.convs], axis=1)
outputs = self.decoder(self.dropout(encoding))
return outputs
d2l.predict_sentiment(net, vocab, 'this movie is so bad')
d2l.predict_sentiment(net, vocab, 'this movie is so bad')
textCNNインスタンスを作成しよう。 これは、カーネル幅が3、4、5で、 すべて100個の出力チャネルを持つ 3つの畳み込み層を備えている。
embed_size, kernel_sizes, nums_channels = 100, [3, 4, 5], [100, 100, 100]
devices = d2l.try_all_gpus()
net = TextCNN(len(vocab), embed_size, kernel_sizes, nums_channels)
def init_weights(module):
if type(module) in (nn.Linear, nn.Conv1d):
nn.init.xavier_uniform_(module.weight)
net.apply(init_weights);
embed_size, kernel_sizes, nums_channels = 100, [3, 4, 5], [100, 100, 100]
devices = d2l.try_all_gpus()
net = TextCNN(len(vocab), embed_size, kernel_sizes, nums_channels)
net.initialize(init.Xavier(), ctx=devices)
[07:18:01] ../src/storage/storage.cc:196: Using Pooled (Naive) StorageManager for GPU
[07:18:01] ../src/storage/storage.cc:196: Using Pooled (Naive) StorageManager for GPU
16.3.3.2. 事前学習済み単語ベクトルの読み込み¶
16.2 章 と同様に、
事前学習済みの100次元GloVe埋め込みを
初期化済みのトークン表現として読み込みる。
これらのトークン表現(埋め込み重み)は、 embedding では学習され、
constant_embedding では固定される。
glove_embedding = d2l.TokenEmbedding('glove.6b.100d')
embeds = glove_embedding[vocab.idx_to_token]
net.embedding.weight.data.copy_(embeds)
net.constant_embedding.weight.data.copy_(embeds)
net.constant_embedding.weight.requires_grad = False
Downloading ../data/glove.6B.100d.zip from http://d2l-data.s3-accelerate.amazonaws.com/glove.6B.100d.zip...
glove_embedding = d2l.TokenEmbedding('glove.6b.100d')
embeds = glove_embedding[vocab.idx_to_token]
net.embedding.weight.set_data(embeds)
net.constant_embedding.weight.set_data(embeds)
net.constant_embedding.collect_params().setattr('grad_req', 'null')
16.3.3.3. モデルの学習と評価¶
これで、感情分析のためにtextCNNモデルを学習できる。
lr, num_epochs = 0.001, 5
trainer = torch.optim.Adam(net.parameters(), lr=lr)
loss = nn.CrossEntropyLoss(reduction="none")
d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, devices)
loss 0.066, train acc 0.979, test acc 0.877
5609.3 examples/sec on [device(type='cuda', index=0), device(type='cuda', index=1)]
lr, num_epochs = 0.001, 5
trainer = gluon.Trainer(net.collect_params(), 'adam', {'learning_rate': lr})
loss = gluon.loss.SoftmaxCrossEntropyLoss()
d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, devices)
loss 0.088, train acc 0.969, test acc 0.867
1430.3 examples/sec on [gpu(0), gpu(1)]
以下では、学習済みモデルを使って 2つの簡単な文の感情を予測する。
d2l.predict_sentiment(net, vocab, 'this movie is so great')
'positive'
d2l.predict_sentiment(net, vocab, 'this movie is so bad')
'negative'
16.3.4. まとめ¶
1次元CNNは、テキスト中の \(n\)-gram のような局所特徴を処理できる。
複数入力チャネルを持つ1次元相互相関は、単一入力チャネルの2次元相互相関と等価である。
max-over-time pooling では、チャネルごとに時刻数が異なっていてもよい。
textCNNモデルは、1次元畳み込み層と max-over-time pooling 層を用いて、個々のトークン表現を下流アプリケーションの出力へ変換する。