Python: LexRankで日本語の記事を要約する

仕事で行っているPoCの中で、文章の要約が使えるのではと思い、調査をし始めています。

今回はsumyのLexRankの実装を使い、過去の投稿を要約してみます。

LexRank

LexRankは、抽出型に分類される要約アルゴリズムで、文書からグラフ構造を作り出して重要な文のランキングを作ることで要約と言える文を発見します。2004年に提案されています (提案論文はこちら) 。

  • 要約アルゴリズムは抽出型と生成型に大きく分けられます
    • 抽出型は、対象の文章内から要約と言える代表的な文を抜き出す方法 (大事なところに線を引くのと近い方法)
    • 生成型は、文章内の文をそのまま使わずに、要約文を作る方法 (読書メモを作るのと近い方法)

LexRankのキーポイントは2つで、PageRankから着想を得たTextRank (提案論文PDF) の派生となります。

  • 文をノード、文間の類似度をエッジとした無方向グラフを作る
    • 提案論文では、TF-IDFからコサイン類似度で計算 (現代的にはword2vecなども使えるはず)
  • 上のグラフから得られた推移確率行列 (M) と確率ベクトル (P) が安定する状態 (MP=P) まで計算して、最終的な確率ベクトルの値が大きい文を要約文として選択する
    • PageRankの計算と同じ

上の理論を視覚化したの下図 (提案論文Figure 2から抜粋) で、エッジ数が多く (=たくさんの文と類似している) かつエッジが太い (=類似度が高い) d5s1やd4s1などが要約文の候補となります。

実装

それではLexRankで要約文を抽出します。過去の投稿を2行で要約します (タグやマークダウンの記述は除外しました) 。

ohke.hateblo.jp

sumy

LexRankの実装として、sumyを使います。

  • LexRank以外にも様々な要約アルゴリズムが実装されています
  • 日本語もサポートしており、内部はtinysegmenterが形態素解析器に採用されています
    • が、tinysegmenterは本当に必要最低限のため、今回はJanomeで解析します

github.com

pip install sumy tinysegmenter Janome

形態素解析

最初にJanomeでトークンに分離します。Janomeについては Python janomeのanalyzerが便利 - け日記 も参考にしてみてください。

from janome.analyzer import Analyzer
from janome.charfilter import UnicodeNormalizeCharFilter, RegexReplaceCharFilter
from janome.tokenizer import Tokenizer as JanomeTokenizer  # sumyのTokenizerと名前が被るため
from janome.tokenfilter import POSKeepFilter, ExtractAttributeFilter

text = """転職 Advent Calendar 2016 - Qiitaの14日目となります。 少しポエムも含みます。
今年11月にSIerからWebサービスの会社へ転職しました。
早くから退職することを報告していたこともあって、幸いにも有給消化として1ヶ月のお休みをいただくことができました(これでも10日ほど余らせてしまいました)。
# ・・・ (省略) ・・・
だからこそ、有給消化期間はなんとしてでももぎ取るようにしましょう。"""

# 1行1文となっているため、改行コードで分離
sentences = [t for t in text.split('\n')]
for i in range(2):
    print(sentences[i])
# 転職 Advent Calendar 2016 - Qiitaの14日目となります。 少しポエムも含みます。
# 今年11月にSIerからWebサービスの会社へ転職しました。

# 形態素解析器を作る
analyzer = Analyzer(
    [UnicodeNormalizeCharFilter(), RegexReplaceCharFilter(r'[(\)「」、。]', ' ')],  # ()「」、。は全てスペースに置き換える
    JanomeTokenizer(),
    [POSKeepFilter(['名詞', '形容詞', '副詞', '動詞']), ExtractAttributeFilter('base_form')]  # 名詞・形容詞・副詞・動詞の原型のみ
)

# 抽出された単語をスペースで連結
# 末尾の'。'は、この後使うtinysegmenterで文として分離させるため。
corpus = [' '.join(analyzer.analyze(s)) + '。' for s in sentences]
for i in range(2):
    print(corpus[i])
# 転職 Advent Calendar 2016 - Qiita 14 日 目 なる 少し ポエム 含む。
# 今年 11 月 SIer Web サービス 会社 転職 する。

要約文の抽出

上で生成したコーパスから、sumyで要約文を抽出します。

  • PlaintextParserは、文字列やファイルから文書を読み込みます (他にもHtmlParserが用意されている)
    • Tokenizerには言語を指定する ('english', 'german', 'chinese', 'japanese', 'slovak'または'czech'が選択できる)
  • LexRankSummarizerで、LexRankの計算を行い、要約文を抽出します
    • stop_wordsプロパティでストップワードを指定する
    • callメソッドにparserで解析された文書オブジェクトと、抽出したい文の数を、それぞれ引数に渡す
from sumy.parsers.plaintext import PlaintextParser
from sumy.nlp.tokenizers import Tokenizer
from sumy.summarizers.lex_rank import LexRankSummarizer

# 連結したcorpusを再度tinysegmenterでトークナイズさせる
parser = PlaintextParser.from_string(''.join(corpus), Tokenizer('japanese'))

# LexRankで要約を2文抽出
summarizer = LexRankSummarizer()
summarizer.stop_words = [' ']  # スペースも1単語として認識されるため、ストップワードにすることで除外する

summary = summarizer(document=parser.document, sentences_count=2)

# 元の文を表示
for sentence in summary:
    print(sentences[corpus.index(sentence.__str__())])

要約文として抽出された文は、以下の2つです。

  • 1文目は何について記述されているのか説明しており、そういう意味では要約になってます
  • 2文目は外している感じがします (勉強が1つのテーマになっているのですが...)
今まで長期休みを取らずに働いていた人は、意外と同じような疑問を持つ人が多いのかなと思いましたので、参考までに自分が有給中に何をしていたのかを書いてみたいと思います。
もし不安なら内定後の面談で何を勉強しておいたほうが良いか直接確認しても良いと思います(私はそうしました)。

まとめ

小さな実験でしたが、今回の結果からLexRankの特徴について以下のことが言えそうです。

  • LexRankではTF-IDFから文書のベクトルを計算しており、"有給"や"勉強"などの主題となる単語を含む文をキャッチできているようです
  • そういった単語を含む文の中から相応しいのが選択できているかというとそうではなく、上手くいかないパターンがありそうです

また論文の方も読んで理解を深めていきたいと思います。