.. _sec_sentiment_rnn: 感情分析: 再帰型ニューラルネットワークの使用 ============================================ 単語類似度や類推のタスクと同様に、 感情分析にも事前学習済みの単語ベクトルを適用できる。 :numref:`sec_sentiment` の IMDb レビューデータセットは それほど大きくないため、 大規模コーパスで事前学習された テキスト表現を用いることで、 モデルの過学習を抑えられる可能性がある。 :numref:`fig_nlp-map-sa-rnn` に示す具体例では、 各トークンを 事前学習済みの GloVe モデルで表現し、 それらのトークン表現を 多層双方向 RNN に入力して テキスト系列表現を得る。 その表現は 感情分析の出力へと変換される :cite:`Maas.Daly.Pham.ea.2011`\ 。 同じ下流アプリケーションに対して、 後ほど別のアーキテクチャ上の 選択肢も検討する。 .. _fig_nlp-map-sa-rnn: .. figure:: ../img/nlp-map-sa-rnn.svg この節では、事前学習済み GloVe を RNN ベースの感情分析アーキテクチャに入力する。 .. 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:: 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, rnn npx.set_np() 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... [07:03:42] ../src/storage/storage.cc:196: Using Pooled (Naive) StorageManager for CPU .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python embed_size, num_hiddens, num_layers, devices = 100, 100, 2, d2l.try_all_gpus() net = BiRNN(len(vocab), embed_size, num_hiddens, num_layers) .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python embed_size, num_hiddens, num_layers, devices = 100, 100, 2, d2l.try_all_gpus() net = BiRNN(len(vocab), embed_size, num_hiddens, num_layers) .. raw:: html
.. raw:: html
RNN による単一テキストの表現 ---------------------------- 感情分析のようなテキスト分類タスクでは、 可変長のテキスト系列を 固定長のカテゴリへ変換する。 以下の ``BiRNN`` クラスでは、 テキスト系列の各トークンが 埋め込み層 (``self.embedding``) を通じて 個別の事前学習済み GloVe 表現を得るが、 系列全体は双方向 RNN (``self.encoder``) によって符号化される。 より具体的には、 双方向 LSTM の (最終層における) 最初と最後の時間ステップでの隠れ状態を連結し、 テキスト系列の表現とする。 この単一のテキスト表現は、 全結合層 (``self.decoder``) によって 2 つの出力(“positive” と “negative”)を持つ 出力カテゴリへ変換される。 .. raw:: html
pytorchmxnetjaxtensorflow
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python class BiRNN(nn.Module): def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, **kwargs): super(BiRNN, self).__init__(**kwargs) self.embedding = nn.Embedding(vocab_size, embed_size) # Set `bidirectional` to True to get a bidirectional RNN self.encoder = nn.LSTM(embed_size, num_hiddens, num_layers=num_layers, bidirectional=True) self.decoder = nn.Linear(4 * num_hiddens, 2) def forward(self, inputs): # The shape of `inputs` is (batch size, no. of time steps). Because # LSTM requires its input's first dimension to be the temporal # dimension, the input is transposed before obtaining token # representations. The output shape is (no. of time steps, batch size, # word vector dimension) embeddings = self.embedding(inputs.T) self.encoder.flatten_parameters() # Returns hidden states of the last hidden layer at different time # steps. The shape of `outputs` is (no. of time steps, batch size, # 2 * no. of hidden units) outputs, _ = self.encoder(embeddings) # Concatenate the hidden states at the initial and final time steps as # the input of the fully connected layer. Its shape is (batch size, # 4 * no. of hidden units) encoding = torch.cat((outputs[0], outputs[-1]), dim=1) outs = self.decoder(encoding) return outs .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python class BiRNN(nn.Block): def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, **kwargs): super(BiRNN, self).__init__(**kwargs) self.embedding = nn.Embedding(vocab_size, embed_size) # Set `bidirectional` to True to get a bidirectional RNN self.encoder = rnn.LSTM(num_hiddens, num_layers=num_layers, bidirectional=True, input_size=embed_size) self.decoder = nn.Dense(2) def forward(self, inputs): # The shape of `inputs` is (batch size, no. of time steps). Because # LSTM requires its input's first dimension to be the temporal # dimension, the input is transposed before obtaining token # representations. The output shape is (no. of time steps, batch size, # word vector dimension) embeddings = self.embedding(inputs.T) # Returns hidden states of the last hidden layer at different time # steps. The shape of `outputs` is (no. of time steps, batch size, # 2 * no. of hidden units) outputs = self.encoder(embeddings) # Concatenate the hidden states at the initial and final time steps as # the input of the fully connected layer. Its shape is (batch size, # 4 * no. of hidden units) encoding = np.concatenate((outputs[0], outputs[-1]), axis=1) outs = self.decoder(encoding) return outs .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python glove_embedding = d2l.TokenEmbedding('glove.6b.100d') .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python glove_embedding = d2l.TokenEmbedding('glove.6b.100d') .. raw:: html
.. raw:: html
感情分析のために単一テキストを表現する双方向 RNN を、2 層の隠れ層で構成してみよう。 .. raw:: html
pytorchmxnetjaxtensorflow
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python embed_size, num_hiddens, num_layers, devices = 100, 100, 2, d2l.try_all_gpus() net = BiRNN(len(vocab), embed_size, num_hiddens, num_layers) .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python embed_size, num_hiddens, num_layers, devices = 100, 100, 2, d2l.try_all_gpus() net = BiRNN(len(vocab), embed_size, num_hiddens, num_layers) .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python embeds = glove_embedding[vocab.idx_to_token] embeds.shape .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python embeds = glove_embedding[vocab.idx_to_token] embeds.shape .. raw:: html
.. raw:: html
.. raw:: html
pytorchmxnetjaxtensorflow
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python def init_weights(module): if type(module) == nn.Linear: nn.init.xavier_uniform_(module.weight) if type(module) == nn.LSTM: for param in module._flat_weights_names: if "weight" in param: nn.init.xavier_uniform_(module._parameters[param]) net.apply(init_weights); .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python net.initialize(init.Xavier(), ctx=devices) .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output [07:03:48] ../src/storage/storage.cc:196: Using Pooled (Naive) StorageManager for GPU [07:03:48] ../src/storage/storage.cc:196: Using Pooled (Naive) StorageManager for GPU .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python predict_sentiment(net, vocab, 'this movie is so great') .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python predict_sentiment(net, vocab, 'this movie is so great') .. raw:: html
.. raw:: html
事前学習済み単語ベクトルの読み込み ---------------------------------- 以下では、語彙内のトークンに対応する 事前学習済みの 100 次元(\ ``embed_size`` と一致している必要がある)の GloVe 埋め込みを読み込みる。 .. raw:: html
pytorchmxnetjaxtensorflow
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python glove_embedding = d2l.TokenEmbedding('glove.6b.100d') .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python glove_embedding = d2l.TokenEmbedding('glove.6b.100d') .. 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 predict_sentiment(net, vocab, 'this movie is so bad') .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python predict_sentiment(net, vocab, 'this movie is so bad') .. raw:: html
.. raw:: html
語彙内のすべてのトークンに対する ベクトルの形状を表示す。 .. raw:: latex \diilbookstyleinputcell .. code:: python embeds = glove_embedding[vocab.idx_to_token] embeds.shape .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output torch.Size([49346, 100]) これらの事前学習済み 単語ベクトルを用いて レビュー中のトークンを表現し、 学習中にこれらのベクトルは更新しない。 .. raw:: html
pytorchmxnet
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python net.embedding.weight.data.copy_(embeds) net.embedding.weight.requires_grad = False .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python net.embedding.weight.set_data(embeds) net.embedding.collect_params().setattr('grad_req', 'null') .. raw:: html
.. raw:: html
モデルの学習と評価 ------------------ これで、感情分析のために双方向 RNN を学習できる。 .. raw:: html
pytorchmxnet
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python lr, num_epochs = 0.01, 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.295, train acc 0.877, test acc 0.848 2845.4 examples/sec on [device(type='cuda', index=0), device(type='cuda', index=1)] .. figure:: output_sentiment-analysis-rnn_e3f486_88_1.svg .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python lr, num_epochs = 0.01, 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.291, train acc 0.877, test acc 0.854 747.8 examples/sec on [gpu(0), gpu(1)] .. figure:: output_sentiment-analysis-rnn_e3f486_91_1.svg .. raw:: html
.. raw:: html
学習済みモデル ``net`` を用いて テキスト系列の感情を予測するために、 以下の関数を定義する。 .. raw:: html
pytorchmxnet
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python #@save def predict_sentiment(net, vocab, sequence): """Predict the sentiment of a text sequence.""" sequence = torch.tensor(vocab[sequence.split()], device=d2l.try_gpu()) label = torch.argmax(net(sequence.reshape(1, -1)), dim=1) return 'positive' if label == 1 else 'negative' .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python #@save def predict_sentiment(net, vocab, sequence): """Predict the sentiment of a text sequence.""" sequence = np.array(vocab[sequence.split()], ctx=d2l.try_gpu()) label = np.argmax(net(sequence.reshape(1, -1)), axis=1) return 'positive' if label == 1 else 'negative' .. raw:: html
.. raw:: html
最後に、学習済みモデルを使って 2 つの簡単な文の感情を予測してみよう。 .. raw:: latex \diilbookstyleinputcell .. code:: python predict_sentiment(net, vocab, 'this movie is so great') .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output 'positive' .. raw:: latex \diilbookstyleinputcell .. code:: python predict_sentiment(net, vocab, 'this movie is so bad') .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output 'negative' まとめ ------ - 事前学習済み単語ベクトルは、テキスト系列内の個々のトークンを表現できる。 - 双方向 RNN は、最初と最後の時間ステップでの隠れ状態の連結などによって、テキスト系列を表現できる。この単一のテキスト表現は、全結合層を用いてカテゴリへ変換できる。 演習 ---- 1. エポック数を増やしてみよう。学習精度とテスト精度を改善できるか? 他のハイパーパラメータを調整した場合はどうだろうか? 2. 300 次元 GloVe 埋め込みのような、より大きな事前学習済み単語ベクトルを使ってみよう。分類精度は向上するか? 3. spaCy のトークン化を使うことで分類精度を改善できるか? spaCy をインストールし(\ ``pip install spacy``\ )、英語パッケージをインストールする必要がある(\ ``python -m spacy download en``\ )。コードでは、まず spaCy をインポートし(\ ``import spacy``\ )、次に spaCy の英語パッケージを読み込みます(\ ``spacy_en = spacy.load('en')``\ )。最後に、\ ``def tokenizer(text): return [tok.text for tok in spacy_en.tokenizer(text)]`` を定義して、元の ``tokenizer`` 関数を置き換えよ。GloVe と spaCy ではフレーズトークンの形式が異なることに注意しよ。たとえば、フレーズトークン “new york” は、GloVe では “new-york” の形式であり、spaCy のトークン化後は “new york” の形式になる。