Pointer-Generator Networksで要約をしてみる

抽象型要約の手法である[Pointer-Generator Networks](https://arxiv.org/abs/1704.04368)をChainerで動くようにしてみました。

# Pointer-Generator Networksについて

Pointer-Generator Networksは抽象型要約としてsequence to sequenceを使うアイディアを発展させたものです。
先行研究として、CopyNetがあります。CopyNetに関してはqiitaに解説記事があります。

* [今更ながらchainerでSeq2Seq（3）〜CopyNet編〜 - Qiita](https://qiita.com/kenchin110100/items/6dd952b73f2a2411a04f)

既存のseq2seqは未知語に対して一意な単語IDを付与するという挙動が一般的ですが、特に要約という領域では本文中の単語がそのまま要約にも使われるということがよくあります。
そこで、本文中に出てくる未知語(Out of Vocabulary)に対しそれぞれに一時的なIDを付与し、要約側にも同じ単語があればそれを使って表現できるようにしよう、という考え方がCopyNetやPointer-Generator Networksです。

<!-- (後で図を交えて解説を書く予定) -->

[渡邉亮彦氏による解説PDF](http://www.lr.pi.titech.ac.jp/~haseshun/acl2017suzukake/slides/03.pdf)があります。

## データセット

オリジナルの論文では訓練用データに[CNN/DailyMail](https://github.com/abisee/cnn-dailymail)を使っています。

このデータセットではhighlitとマーキングされた文が1記事に複数あり、Pointer-Generator Networksではこの複数の文章を要約文として生成させます。

### Livedoorニュースコーパス

日本語の要約に利用できる公開されたデータセットというのはなかなかないのですが、ライブドアニュースコーパスの記事見出しを記事本文に対する要約とみなすことで利用できそうです。

残念ながら大元のデータは公開を終了されてしまっているのですが、ライセンスがCC-SA 2.1なので再配布に制限はありません。[株式会社ロンウィットさんが配布](https://www.rondhuit.com/download.html)してくださっています。

これを処理して要約モデルに使える形にするスクリプトを用意しました。ライセンスにより改変したものの再配布が禁止されているため、このスクリプトが生成したものは配布できないことに留意してください。

* https://github.com/knok/ldcc-summarize

記事数は約8000程度とCNN/DailyMailには及びませんが、ある程度うまく動いているかどうかの判断ぐらいはできると思います。また、基本的に記事のタイトルは1つしかないので、単文のみが生成要約対象であるというところも異なります。

このコーパス固有の処理として、要約文の先頭、末尾にある隅付き括弧【】とその中身を削除しています。本文も末尾は削除したほうが良い文面が多く見られるのですが、良いルールが思い浮かばなかったのでそのままにしています。

もちろん、自分で用意したデータセットがあればそちらで試すこともできます。

## コード

当初、Chainerでやってみようと思い既に[公開されている実装](https://github.com/kiyomaro927/chainer-pointer-generator-nets)を確認したのですが、微妙に正しくありませんでした。具体的には、未知語(Out of Vocab)の処理が動的に行われていないという問題がありました。

forkして問題を修正したバージョンを公開しています。ちょっと元とのコードの乖離が激しくて、Pull Requestを出すことは躊躇しています…

* https://github.com/knok/chainer-pointer-generator-nets

加えて、ライブドアニュースコーパスをダウンロード、処理して訓練の開始までを一気に行うスクリプトも用意しました。これにより、実装を誰でも追試できるようになりました。

* https://github.com/knok/chainer-pointer-generator-nets/blob/master/README-ldcc.ja.md

### 構造の違い

[fork元の実装と](https://github.com/kiyomaro927/chainer-pointer-generator-nets)[論文著者によるオリジナル実装(Tensorflow)](https://github.com/abisee/pointer-generator)を比較し、細かい構造の違いなどについて調べ、不適切な部分を修正するなどの変更を行いました。
それでも一部の構造は違ったままですが、基本的にはうまく動いています。

#### 語彙の扱い

公式実装では、Encoder, Decoderで共通のembeddingを使っていますが、chainer実装ではそれぞれ独立していました。
実用上はどちらでも構わないと思われるのですが、OOVを語彙IDベースで取り扱う上で、embeddingを共通化したほうが容易になるので、この部分は公式実装に合わせて変更しました。

もとの実装の間違っている部分として、入力データすべての未知語を含めた単語に対し固有のIDをふる、ということをしています。これを改めました。もとの実装のままだと、訓練時に存在した未知語しか扱えないので論文の意図を正しく反映していないと言えます。

#### Encoder特徴量の計算方法

公式実装ではCNNを使っていますが、chainer実装は全層結合です。

#### LSTMのパラメータ

公式実装では単語分散表現の次元数が128, Encoder-Decoderの隠れ層が256次元となっています。chainer実装では、単語分散表現の次元数と隠れ層の次元数が同一(128)になっています。

<!-- この点を変えたときにどのような変化があるのかは試していません。 -->

#### 最適化

公式実装ではAdagradを使っていますが、chainer実装ではAdamを使っています。

<!-- これについての比較検討はしていません。 -->

<!-- #### 損失 -->

#### 推論

ここはもっとも異なる部分です。公式実装はCNN/DailyMailの構造にうまく適応できるよう、できるだけ長いシーケンスを生成できるようにビームサーチを行うようになっています。さらに、指定した長さより早い段階でEOSに至った場合は、続けて次の文を探索するようになっています。
chainer実装では単純に各ステップで最も確率が高い単語のみを選択し、最初にEOSを出した時点で打ち切るようになっています。要約が1文しかないライブドアニュースコーパスでの訓練では、あえてビームサーチをする必要性は薄いのではないかと思ってそのままにしています。

## 訓練

ライブドアニュースコーパスを使って訓練するためのスクリプトを用意したので、それを使う場合はすぐに動かすことができます。

```sh
$ git clone https://github.com/knok/chainer-pointer-generator-nets
$ cd chainer-pointer-generator-nets
$ ./bin/train-ldcc.sh -s -p -t -g 0
```
* ref: https://github.com/knok/chainer-pointer-generator-nets/blob/master/README-ldcc.ja.md

## コーパスに関して

ライブドアニュースコーパスは以下のような特徴があります。

* わりとくだけた日本語
* 記事数に対し語彙が多い
 * 特に出現頻度が低い語彙が多い
* 本文と見出しとの対応関係が感覚的に離れている

評価用データを与えてみても、思ったほど良い要約を生成してくれません。多くの語彙を使っているけれども記事数が少ないため学習時にはあまり出現しないパターンの文章が多いせいではないかと推測します。

正直なところ、要約一般として考えるにはあまり適切なデータとはいいがたい印象があります。大手新聞社の記事のデータが利用できれば良さそうですが、コストもそれなりにしそうです。

このコーパス以外でOOVをうまく扱えるという特性を活かせそうなデータとしては、天気予報や為替などに関する記事といったものが考えられます。数値に関しては語彙に持つよりも本文のOOVとして扱えば適切な要約になりそうです。

## 今後について

本当は極力公式実装に構造を合わせた場合との違いを比較検討すべきなのだろうと思いますが、そこまで手が回っていません。
AWS p2.xlargeインスタンスで処理をさせてみているのですが、TensorFlow実装に比べると、chainer実装では少ない語彙しか扱えない印象があります。語彙を同じレベルに増やすとGPUのメモリが足りなくなってしまうので…

### [Bottom-up Abstract Summarization](https://arxiv.org/abs/1808.10792)

Pointer-Generator networksをさらに発展させた手法があるのですが、実装はAllenNLP(PyTorch)で行われています。できればこれをChainerで動くようにしてみたいところです。

ただ、現状オリジナル実装を動作させるとp2.xlarge (61GBメモリ)でもメモリが枯渇し途中でOOM killerに殺されてしまうので、まずはその原因を探るところからすすめてゆきたいです。

### より適切なデータセット

もう少しOOVを扱えるという特色を活かせるデータセットがほしいところです。できれば再配布可能な形のものが良いのですが、なかなか良いソースが思いつきません。
もし良いアイディアがあればコメントなどをいただけると幸いです。