.. _sec_sentiment_cnn: 畳み込みニューラルネットワークを用いた感情分析 ============================================== :numref:`chap_cnn` では、 2次元CNNを用いて 2次元画像データを処理する 仕組みを調べた。 そこでは、 隣接ピクセルのような 局所特徴に対して CNNが適用された。 CNNはもともと コンピュータビジョン向けに 設計されたが、 自然言語処理でも広く使われている。 簡単に言えば、 任意のテキスト系列を 1次元画像だと考えればよいのである。 このようにすると、 1次元CNNは テキスト中の :math:`n`-gram のような 局所特徴を処理できる。 この節では、 単一のテキストを表現するためのCNNアーキテクチャを どのように設計するかを示すために、 *textCNN* モデルを用いる :cite:`Kim.2014`\ 。 感情分析のために GloVeの事前学習を用いたRNNアーキテクチャを使う :numref:`fig_nlp-map-sa-rnn` と比べると、 :numref:`fig_nlp-map-sa-cnn` における 唯一の違いは アーキテクチャの選択にある。 .. _fig_nlp-map-sa-cnn: .. figure:: ../img/nlp-map-sa-cnn.svg この節では、事前学習済みのGloVeをCNNベースの感情分析アーキテクチャに入力する。 .. raw:: html
pytorchmxnetjaxtensorflow
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python 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) .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output Downloading ../data/aclImdb_v1.tar.gz from http://d2l-data.s3-accelerate.amazonaws.com/aclImdb_v1.tar.gz... .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python 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) .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output [07:17:55] ../src/storage/storage.cc:196: Using Pooled (Naive) StorageManager for CPU .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python 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 .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python 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 .. raw:: html
.. raw:: html
1次元畳み込み ------------- モデルを紹介する前に、 1次元畳み込みがどのように動作するかを見てみよう。 これは、 相互相関演算に基づく 2次元畳み込みの特殊な場合にすぎないことを 念頭に置いよ。 .. _fig_conv1d: .. figure:: ../img/conv1d.svg 1次元の相互相関演算。網掛け部分は最初の出力要素と、その出力計算に使われる入力テンソルおよびカーネルテンソルの要素を示している: :math:`0\times1+1\times2=2`. :numref:`fig_conv1d` に示すように、 1次元の場合、 畳み込みウィンドウは 入力テンソル上を 左から右へと スライドする。 スライド中、 畳み込みウィンドウの ある位置に含まれる 入力部分テンソル(たとえば :numref:`fig_conv1d` の :math:`0` と :math:`1`\ )と カーネルテンソル(たとえば :numref:`fig_conv1d` の :math:`1` と :math:`2`\ )を 要素ごとに掛け合わせる。 これらの積の和が、 出力テンソルの対応する位置における 1つのスカラー値(たとえば :numref:`fig_conv1d` の :math:`0\times1+1\times2=2`\ )を 与える。 以下の ``corr1d`` 関数で 1次元の相互相関を実装する。 入力テンソル ``X`` と カーネルテンソル ``K`` が与えられると、 出力テンソル ``Y`` を返す。 .. raw:: html
pytorchmxnetjaxtensorflow
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python 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 .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python 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 .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python X, K = d2l.tensor([0, 1, 2, 3, 4, 5, 6]), d2l.tensor([1, 2]) corr1d(X, K) .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python X, K = d2l.tensor([0, 1, 2, 3, 4, 5, 6]), d2l.tensor([1, 2]) corr1d(X, K) .. raw:: html
.. raw:: html
:numref:`fig_conv1d` の入力テンソル ``X`` とカーネルテンソル ``K`` を構成して、 上の1次元相互相関実装の出力を検証できる。 .. raw:: html
pytorchmxnetjaxtensorflow
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python X, K = d2l.tensor([0, 1, 2, 3, 4, 5, 6]), d2l.tensor([1, 2]) corr1d(X, K) .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output tensor([ 2., 5., 8., 11., 14., 17.]) .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python X, K = d2l.tensor([0, 1, 2, 3, 4, 5, 6]), d2l.tensor([1, 2]) corr1d(X, K) .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output array([ 2., 5., 8., 11., 14., 17.]) .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python 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) .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python 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) .. raw:: html
.. raw:: html
複数チャネルを持つ任意の 1次元入力に対しては、 畳み込みカーネルも 同じ数の入力チャネルを持つ必要がある。 その後、各チャネルごとに、 入力の1次元テンソルと畳み込みカーネルの1次元テンソルに対して 相互相関演算を行い、 すべてのチャネルにわたる結果を 足し合わせて 1次元の出力テンソルを生成する。 :numref:`fig_conv1d_channel` は 3つの入力チャネルを持つ 1次元相互相関演算を示している。 .. _fig_conv1d_channel: .. figure:: ../img/conv1d-channel.svg 3つの入力チャネルを持つ1次元の相互相関演算。網掛け部分は最初の出力要素と、その出力計算に使われる入力テンソルおよびカーネルテンソルの要素を示している: :math:`0\times1+1\times2+1\times3+2\times4+2\times(-1)+3\times(-3)=2`. 複数入力チャネルを持つ 1次元相互相関演算を実装し、 :numref:`fig_conv1d_channel` の結果を検証できる。 .. raw:: html
pytorchmxnetjaxtensorflow
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python 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) .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output tensor([ 2., 8., 14., 20., 26., 32.]) .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python 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) .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output array([ 2., 8., 14., 20., 26., 32.]) .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python d2l.predict_sentiment(net, vocab, 'this movie is so great') .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python d2l.predict_sentiment(net, vocab, 'this movie is so great') .. raw:: html
.. raw:: html
複数入力チャネルを持つ 1次元相互相関は、 単一入力チャネルの 2次元相互相関と等価であることに 注意しよ。 例として、 :numref:`fig_conv1d_channel` における 複数入力チャネルの1次元相互相関に対応する形は、 :numref:`fig_conv1d_2d` における 単一入力チャネルの 2次元相互相関である。 ここでは、畳み込みカーネルの高さは 入力テンソルの高さと 同じでなければならない。 .. _fig_conv1d_2d: .. figure:: ../img/conv1d-2d.svg 単一入力チャネルを持つ2次元の相互相関演算。網掛け部分は最初の出力要素と、その出力計算に使われる入力テンソルおよびカーネルテンソルの要素を示している: :math:`2\times(-1)+3\times(-3)+1\times3+2\times4+0\times1+1\times2=2`. :numref:`fig_conv1d` と :numref:`fig_conv1d_channel` の両方の出力は、 どちらも1チャネルしか持たない。 :numref:`subsec_multi-output-channels` で説明した 複数出力チャネルを持つ2次元畳み込みと同様に、 1次元畳み込みでも 複数の出力チャネルを指定できる。 時間方向最大プーリング ---------------------- 同様に、プーリングを用いて 系列表現から 各時刻にわたる最も重要な特徴として 最大値を抽出できる。 textCNNで使われる *max-over-time pooling* は、 1次元のグローバル最大プーリング :cite:`Collobert.Weston.Bottou.ea.2011` のように動作する。 各チャネルが 異なる時刻の値を保持する 多チャネル入力に対しては、 各チャネルの出力は そのチャネルの最大値になる。 なお、 max-over-time pooling では チャネルごとに 時刻数が異なっていても構わない。 textCNNモデル ------------- 1次元畳み込みと max-over-time pooling を用いて、 textCNNモデルは 個々の事前学習済みトークン表現を入力とし、 その後、 下流タスク向けの系列表現を 得て変換する。 :math:`n` 個のトークンを :math:`d` 次元ベクトルで表した 単一のテキスト系列に対して、 入力テンソルの幅、高さ、チャネル数は それぞれ :math:`n`\ 、\ :math:`1`\ 、\ :math:`d` である。 textCNNモデルは 次のように入力を出力へ変換する。 1. 複数の1次元畳み込みカーネルを定義し、入力に対してそれぞれ個別に畳み込み演算を行う。幅の異なる畳み込みカーネルは、異なる数の隣接トークン間の局所特徴を捉えられる。 2. すべての出力チャネルに対して max-over-time pooling を行い、その後、すべてのスカラープーリング出力を連結してベクトルにする。 3. 連結したベクトルを全結合層で出力カテゴリへ変換する。過学習を抑えるためにドロップアウトを使える。 .. _fig_conv1d_textcnn: .. figure:: ../img/textcnn.svg textCNNのモデルアーキテクチャ。 :numref:`fig_conv1d_textcnn` は、 具体例を用いて textCNNのモデルアーキテクチャを示している。 入力は11個のトークンからなる文で、 各トークンは6次元ベクトルで表されている。 したがって、幅11の6チャネル入力がある。 幅が2と4の 2つの1次元畳み込みカーネルを定義し、 それぞれ4チャネルと5チャネルの出力を持たせる。 それらは、 幅 :math:`11-2+1=10` の4つの出力チャネルと、 幅 :math:`11-4+1=8` の5つの出力チャネルを生成する。 これら9チャネルは幅が異なるが、 max-over-time pooling により 連結された9次元ベクトルが得られ、 最終的に 2次元出力ベクトルへ変換されて 二値感情予測を行う。 モデルの定義 ~~~~~~~~~~~~ 以下のクラスでtextCNNモデルを実装する。 :numref:`sec_sentiment_rnn` の双方向RNNモデルと比べると、 再帰層を畳み込み層に置き換えるだけでなく、 2つの埋め込み層も使う。 1つは学習可能な重みを持ち、 もう1つは固定重みを持つ。 .. raw:: html
pytorchmxnetjaxtensorflow
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python 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 .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python 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 .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python d2l.predict_sentiment(net, vocab, 'this movie is so bad') .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python d2l.predict_sentiment(net, vocab, 'this movie is so bad') .. raw:: html
.. raw:: html
textCNNインスタンスを作成しよう。 これは、カーネル幅が3、4、5で、 すべて100個の出力チャネルを持つ 3つの畳み込み層を備えている。 .. raw:: html
pytorchmxnet
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python 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); .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python 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) .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output [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 .. raw:: html
.. raw:: html
事前学習済み単語ベクトルの読み込み ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :numref:`sec_sentiment_rnn` と同様に、 事前学習済みの100次元GloVe埋め込みを 初期化済みのトークン表現として読み込みる。 これらのトークン表現(埋め込み重み)は、 ``embedding`` では学習され、 ``constant_embedding`` では固定される。 .. raw:: html
pytorchmxnet
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python 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 .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output Downloading ../data/glove.6B.100d.zip from http://d2l-data.s3-accelerate.amazonaws.com/glove.6B.100d.zip... .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python 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') .. raw:: html
.. raw:: html
モデルの学習と評価 ~~~~~~~~~~~~~~~~~~ これで、感情分析のためにtextCNNモデルを学習できる。 .. raw:: html
pytorchmxnet
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python 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) .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output 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)] .. figure:: output_sentiment-analysis-cnn_a6d733_96_1.svg .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python 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) .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output loss 0.088, train acc 0.969, test acc 0.867 1430.3 examples/sec on [gpu(0), gpu(1)] .. figure:: output_sentiment-analysis-cnn_a6d733_99_1.svg .. raw:: html
.. raw:: html
以下では、学習済みモデルを使って 2つの簡単な文の感情を予測する。 .. raw:: latex \diilbookstyleinputcell .. code:: python d2l.predict_sentiment(net, vocab, 'this movie is so great') .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output 'positive' .. raw:: latex \diilbookstyleinputcell .. code:: python d2l.predict_sentiment(net, vocab, 'this movie is so bad') .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output 'negative' まとめ ------ - 1次元CNNは、テキスト中の :math:`n`-gram のような局所特徴を処理できる。 - 複数入力チャネルを持つ1次元相互相関は、単一入力チャネルの2次元相互相関と等価である。 - max-over-time pooling では、チャネルごとに時刻数が異なっていてもよい。 - textCNNモデルは、1次元畳み込み層と max-over-time pooling 層を用いて、個々のトークン表現を下流アプリケーションの出力へ変換する。 演習 ---- 1. ハイパーパラメータを調整し、 :numref:`sec_sentiment_rnn` とこの節の2つのアーキテクチャを、分類精度や計算効率などで比較せよ。 2. :numref:`sec_sentiment_rnn` の演習で紹介した手法を用いて、モデルの分類精度をさらに改善できるか。 3. 入力表現に位置エンコーディングを追加せよ。分類精度は向上するか。