抽象型(Abstractive)要約の機械学習モデル詳細
- Fast Abstractive Summarization with Reinforce-Selected Sentence Rewriting の手法を詳細にまとめる
- まず全体の概要を記載し、その後実装レベルの詳細,数式について記載する
全体の概要
学習処理の流れ
- 全体の流れは以下のようになっている
- Extractorについては スライド Fast Abstractive Summarization with Reinforce-Selected Sentence Rewriting
- Abstractorについては スライド Get To The Point: Summarization with Pointer-Generator Networks_acl17_論文紹介
- INPUTの文章(d1,d2,d3...)
- Extractor(抽出された文章(d1,d2,d3...)を作成) class PtrExtractSumm(nn.Module)
- 畳み込みエンコーダーがそれぞれの文節rjとして処理する class ConvSentEncoder(nn.Module)
- RNNエンコーダーが隠し層hjを処理する class LSTMEncoder(nn.Module)
- RNNデコーダーが隠し層jtをタイムステップtで処理する class LSTMPointerNet(nn.Module)
- Abstractor(要約された文章(s1,s2,s3...)を作成) class CopySumm(Seq2SeqSumm)
- RNNエンコーダーが隠し層hjを処理する Seq2SeqSumm#encode
- RNNデコーダーが隠し層stを処理する class CopyLSTMDecoder(AttentionalLSTMDecoder)
- 学習はすべて一気通貫で実施するのではなく、Extractor、Abstractor、それらを統合する強化学習の順で実施する
- 抽出(Extractor)と書き換え(Abstractor)をそれぞれ別のseq2seqの機械学習モデルが実行している
- 抽出(Extractor)の学習コード train_extractor_ml.py
- 書き換え(Abstractor)の学習コード train_abstractor.py
- 異なるNNをつなぎ合わせるために強化学習を行っている
- 統合的な強化学習 train_full_rl.py
- 抽出(Extractor)と書き換え(Abstractor)をそれぞれ別のseq2seqの機械学習モデルが実行している
復号処理(学習済みモデルの実行)の流れ
- 学習済みモデルの実行 decode_full_model.py
beam=1 の場合
- INPUTの文章(d1,d2,d3...)
- Extractor(抽出された文章(d1,d2,d3...)を作成) class RLExtractor(object)
- pickleから読み出して, class PtrExtractSumm(nn.Module) 実行
- Abstractor(要約された文章(s1,s2,s3...)を作成) class Abstractor(object)
前提知識
実装レベルの詳細,数式
Extractor
学習の設定
- ニューラルネットワークの設定
- わかった部分にコメントを追加
+ | train_extractor_ml.py |
- Extractorの実装は2つあった(どちらもpytorchのtorch.nn.Module)を継承している
- ExtractSumm ... ff: 順伝播型ニューラルネットワーク
- PtrExtractSumm ... rnn: 回帰型ニューラルネットワーク
- PyTorchでMNISTする が参考になる
- 要は、torch.nn.Moduleを継承してforwardをオーバーライドしておけば、forwardに定義した通り学習データの学習ができる
- pytorch側の構造を見てみると、典型的なTemplate Methodパターンで実装されている
- Pythonでcallを実装すると、クラス(引数)のような呼び出しで処理が実行できる、それをTemplate Methodパターンの呼び出し実装で使っている
- については Pythonのクラスにおける__call__メソッドの使い方 がよくまとめられていた
+ | PyTorchによる学習部分の実装 |
- Extractor部分の流れ
- 以降、この部分をブレークダウンする
def forward(self, article_sents, sent_nums, target): # 畳み込みエンコーダーがそれぞれの文節rjとして処理 # RNNエンコーダーが隠し層hjを処理する enc_out = self._encode(article_sents, sent_nums) bs, nt = target.size() d = enc_out.size(2) ptr_in = torch.gather( enc_out, dim=1, index=target.unsqueeze(2).expand(bs, nt, d) ) # RNNデコーダーが隠し層jtをタイムステップtで処理する output = self._extractor(enc_out, sent_nums, ptr_in) return output
畳み込みエンコーダーがそれぞれの文節rjとして処理
- テキスト分類にCNNを使用
- Convolutional Neural Networks for Sentence Classification
- 単純な構文解析ではだめだったのだろうか?
- CNNに食わせる前に、インプットデータは3次元のテンソルになっている
PyTorchでLSTMをする際、食わせるインプットデータは3次元のテンソルある必要があります。具体的には、文章の長さ × バッチサイズ × ベクトル次元数 となっています。
- ソースコードの当該箇所
Convolutional word-level sentence encoder
w/ max-over-time pooling, [3, 4, 5] kernel sizes, ReLU activation
単語レベル、文の畳み込みエンコーダー pooling層の設定は"max-over-time"、カーネルサイズ [3, 4, 5]、活性関数はReLU
- 最後しかまともにわからねえ
- pooling層とはCNNのフィルターで特徴マップから一番大きい値を取り出すもの
- カーネル(=フィルター)サイズ
- 行列(長方形の値が入ったマス目と考える)を指定された値の正方形で領域探索し、カーネルに設定された重みでその出力を変えること
- 活性関数
- Coursera 機械学習 - Week4 でやった。今回はシグモイド関数ではなくReLU(ランプ関数)を使うというだけ
- Dropout
- 【ニューラルネットワーク】Dropout(ドロップアウト)についてまとめる
- 落ちこぼれることではなく、学習の際の過学習を防ぐために訓練データをランダムで0を混ぜる設定
// nn.Moduleを継承し、forwardを実装する // 初期化は以下のようにされている ConvSentEncoder( vocab_size, # 語彙のサイズ emb_dim, # word2vecを使って作られたベクトルの次元 conv_hidden, # 隠し層のサイズ dropout # dropoutさせる確率、デフォルト=0.0なので使われていない )
class ConvSentEncoder(nn.Module): def __init__(self, vocab_size, emb_dim, n_hidden, dropout): super().__init__() # 畳み込みニューラルネットワークを作る # https://pytorch.org/docs/master/generated/torch.nn.Conv1d.html # # 入力チャンネル:word2vecを使って作られたベクトルの次元 # 出力チャンネル数:隠し層のサイズ # カーネルサイズ:[3, 4, 5] self._convs = nn.ModuleList([nn.Conv1d(emb_dim, n_hidden, i) for i in range(3, 6)]) ... def forward(self, input_): # input_ は "文章の長さ × バッチサイズ × ベクトル次元数" のテンソルになっているはず emb_input = self._embedding(input_) # dropoutは学習の際の過学習を防ぐために訓練データをランダムで0を混ぜる設定 conv_in = F.dropout(emb_input.transpose(1, 2), self._dropout, training=self.training) # torch.cat: 指定された次元でデータを結合(dim=1なので配列になる) # 3層の畳み込みNNにデータを入力して活性関数ReLUで取り出してそのmax値を取り出して結合 # output = torch.cat([F.relu(conv(conv_in)).max(dim=2)[0] for conv in self._convs], dim=1) return output
RNNエンコーダーが隠し層hjを処理する
ドキュメントのj番目の文のhjとして示される、同じドキュメント内の過去および将来のすべての文のコンテキストを考慮した強力な表現を学習できます
とあるが、説明がほとんどない。よくよくコードを読んでみるとLSTMを使って分類しているだけのようだ。以下がポイント
- ソースコードの当該箇所
- extract.py#L57
- LSTMエンコーダーの実装は以下の場所
- model/extract.py#L40
- model/rnn.py#L9
- extract.py#L57
- PyTorchのnn.LSTMを宣言している
- nn.LSTMについては pytorchでLSTMに入門したい...したくない? がよかった。
- nn.Moduleを継承してLSTMのモデルを定義する部分に関しては PyTorchを使ってLSTMで文章分類を実装してみた
class LSTMEncoder(nn.Module): def __init__(self, input_dim, n_hidden, n_layer, dropout, bidirectional): super().__init__() ... # input_dim: 各時刻における入力ベクトルのサイズ # n_hidden: LSTMの隠れ層ベクトルのサイズ # dropoutは使われてない、bidirectionalは双方向LSTMかどうかを定義 # ここの定義はLSTMの隠れ層を定義しているようだ self._lstm = nn.LSTM(input_dim, n_hidden, n_layer, dropout=dropout, bidirectional=bidirectional) // 順伝播のコード def forward(self, input_, in_lens=None): """ [batch_size, max_num_sent, input_dim] Tensor""" size = (self._init_h.size(0), input_.size(0), self._init_h.size(1)) init_states = (self._init_h.unsqueeze(1).expand(*size), self._init_c.unsqueeze(1).expand(*size)) # many to manyのタスクなので、第1戻り値を使っている # torch.nn.LSTMのoutputはoutput,(h_n, c_n) = torch.nn.LSTMという形式 lstm_out, _ = lstm_encoder(input_, self._lstm, in_lens, init_states) return lstm_out.transpose(0, 1)
LSTMの結果を返す前に中のデータをいじっているように見える。それは以下で
functional LSTM encoder (sequence is [b, t]/[b, t, d], lstm should be rolled lstm)
関数的なLSTMエンコーダー(シーケンスは[b、t] / [b、t、d]、 LSTMは展開されたLSTMである必要があります)
- コードは適宜整形している
// model/rnn.py#L9 def lstm_encoder(sequence, lstm, seq_lens=None, init_states=None, embedding=None): """ functional LSTM encoder (sequence is [b, t]/[b, t, d], lstm should be rolled lstm)""" batch_size = sequence.size(0) // LSTMに`batch_first=True`が設定されていると、LSTMの引数となるテンソルは「バッチサイズ × 文章の長さ × ベクトル次元数」となる // なのでここで転置をかけている、まあ不要 if not lstm.batch_first: ... // こちらも使われていなそう if seq_lens: ... if init_states is None: // ここも、どうやらLSTMの初期状態が引数で渡されないことを想定している。不要。 device = sequence.device init_states = init_lstm_states(lstm, batch_size, device) else: init_states = (init_states[0].contiguous(), init_states[1].contiguous()) if seq_lens: ... else: // 実際にLSTMのモデルにデータを渡す // また nn.Moduleの __call__を使った呼び出し、forwardを実行 lstm_out, final_states = lstm(emb_sequence, init_states) // torch.nn.LSTMのoutputはoutput,(h_n, c_n) // 結局呼び出し元で final_statuesは捨てられている return lstm_out, final_states
RNNデコーダーが隠し層jtをタイムステップtで処理する
- RNNデコーダーが隠し層jtをタイムステップtで処理する: 実装(LSTMPointerNet)
- Pointer Networks を使う
- 元論文のサマリー 2015: Pointer Networks #19
- インプットが可変のデータを処理する際に、組合せ爆発を起こさずうまいこと固定長で出力させるための仕組みっぽい。要約文は原文に対して固定の文字列で返したいからか。
- このWiki内でのまとめは → Pointer network
Abstractor
- 読んでいる限りここが一番重要なステップになりそう
- Abstractor部分の流れ
- 以降、この部分をブレークダウンする
- 抽象型(Abstractive)要約の復号処理 に詳細を記載した
def forward(self, article, art_lens, abstract, extend_art, extend_vsize): attention, init_dec_states = self.encode(article, art_lens) mask = len_mask(art_lens, attention.device).unsqueeze(-2) logit = self._decoder( (attention, mask, extend_art, extend_vsize), init_dec_states, abstract ) return logit
RNNエンコーダーが隠し層hjを処理する
- 抽象型(Abstractive)要約の復号処理 に詳細を記載した
RNNデコーダーが隠し層stを処理する
- 抽象型(Abstractive)要約の復号処理 に詳細を記載した
強化学習
- 強化学習にはActor Criticという手法が使われている