要約・キーフレーズ抽出について
sumy は、Pythonで実装された、抽出型のドキュメント要約ライブラリです。
3行でまとめて! ってやつですね。
ドキュメント中の最重要と思われるセンテンスを抜き出すことで、元の内容のエッセンスを抽出することをめざします。
sumyについて
sumyの公式ページにも書いてありますが、著名な要約アルゴリズムをいくつか実装しているようです。
今回は、それぞれのアルゴリズムでサンプルデータに対してどのような要約が得られるか確認してみます。
... といいつつ、アカデミックな評価などは行っておりません。 このブログのいつもの、使ってみた・多分うまく動いている系のおためしサンプルコードをペタっと貼り付けですので、ご了承ください。
sumy/summarizators.md at master · miso-belica/sumy · GitHub
sumyは、「japanese」対応にはなっていますが、表記揺れや語形変化を考慮した、出現数カウントやそもそもの単語の分かち書きには対応していないので、spaCyとGiNZAでサポートすることにしています。
spaCy/ GiNZA
spaCyは、Pythonの今時の自然言語処理ライブラリのようです。実際にプロダクトに組み込んで使えるような機能群がそろっているという触れ込みのようです。
また、GiNZAはspaCy上で日本語ドキュメントを処理するにあたって、日本語関係のいろいろモダンな叡智がつまっているライブラリのようです。
それぞれ、今回利用したような範囲以上のパワフルな使い方ができると思いますが、今回は、sumyを日本語対応させるにあたり、日本語での分かち書き・レンマ化のツールとして使いました。
sumyの(ほぼ)最小限の使い方*1
- 対象ドキュメントを読み込む(sumyには、WebサイトのhtmlをダウンロードするHTTPクライアント機能などもあるようですが、今回はテキストファイルを文字列として読み込みした例を使います)
- 当該ドキュメントに関して、その言語での、文の区切れ、単語の区切れを取得するルールを定義する(Token化)
- 上記2のToken化クラス*2を、今回使いたい要約アルゴリズムの関数に引数として与える 〜 解析
- 戻り値として、要約の文がイテレータで得られる
1は、青空文庫の「海野十三 ある宇宙塵の秘密」のテキストファイルを使いました。
2では、spaCy/GiNZAで分かち書きなどを行いました。なお、sumy公式の次の How to add new natural language support into Sumy というページを参考にしました。
sumy/how-to-add-new-language.md at master · miso-belica/sumy · GitHub
上記に従い、約束事に従ったTokenizerクラスを作成し、to_sentences、to_wordsを規定のシグネチャに沿って実装すれば良さげでしたので、
spaCyの「sents https://spacy.io/api/doc#sents」「lemma_ https://spacy.io/api/token#attributes 」
というプロパティを使いました。
LexRankやLSAなどは、文や単語の関係から重みを計算するアルゴリズム(だとヤワな理解をしておりますが...)なので、spaCy/GiNZAで分かち書きするとともに、レンマ化して語形変化などを考慮して本体は同じ単語の出現回数をよろしく数えられるようにするのかなと思って進めてみています。
sumy 日本語利用のサンプルコード
実行時のカレントディレクトリに、インプットファイルの 'unno.txt'が配置されている前提です!
※以下、初出時gistを貼り付けしていたのですが、はてなのブログ貼り付けがNGになってしまうようになったので、ベタ張りに変更。
""" sumyでドキュメント要約を行うサンプルプログラム """ # spaCy import spacy # sumy from sumy.parsers.plaintext import PlaintextParser # 以下、要約アルゴリズム from sumy.summarizers.lex_rank import LexRankSummarizer from sumy.summarizers.lsa import LsaSummarizer from sumy.summarizers.reduction import ReductionSummarizer from sumy.summarizers.luhn import LuhnSummarizer from sumy.summarizers.sum_basic import SumBasicSummarizer from sumy.summarizers.kl import KLSummarizer from sumy.summarizers.edmundson import EdmundsonSummarizer # 前処理と言えば前処理 ----------------------------- # GiNZA/spaCyの初期化 nlp = spacy.load('ja_ginza') # sumy の sumy.nlp.tokenizers.Tokenizerに似せた、オリジナルのTokenizerを定義 # https://github.com/miso-belica/sumy/blob/master/docs/how-to-add-new-language class myTokenizer: @staticmethod def to_sentences(text) : return [str(s) for s in nlp(text).sents] # spaCyは、「sents」で文のジェネレータを戻す @staticmethod def to_words(sentence) : l = next(nlp(sentence).sents).lemma_ # spaCyは、「lemma_」で文のレンマ化した文字列を戻す return l.split(' ') # spacy/GiNZAの仕様により、半角スペース区切りでトークン化されるようなのでそれを前提にリストにする # ドキュメントの読み込み doc_str = open('unno.txt').read().replace(' ', '').replace(' ', '') # 今回は、スペースは最初の時点でストップワードとして除外しておく。 # 何行に要約するかの値を算出 # (※これはsumy利用のポイントではなく、筆者がお試しするのにこうしておくのが便利だと思った味付け。 # この味付けは不要、単に3行に要約したければ、sentences_count=3 とすれば良い) num = len(doc_str.split('。')) # 句点の数を文の数とみなす。 N = 3 sentences_count = N if num < 100 else int(num/10) # 長めの文章なら10分の1に、そうでなければN行に要約 # パーサーの設定(入力ドキュメントを読み込ませて、Tokenizerでコーパスを生成する...など) parser = PlaintextParser.from_string(doc_str, myTokenizer()) # 以下、アルゴリズムを指定して要約する ----------------------------- def summarize(summarizer): # 出力関数(手抜き) result = summarizer(document=parser.document, sentences_count=sentences_count) print('\n',summarizer) for s in result: print(s) ## summarize(LexRankSummarizer()) ## summarize(LsaSummarizer()) ## summarize(ReductionSummarizer()) ## summarize(LuhnSummarizer()) ## summarize(SumBasicSummarizer()) ## summarize(KLSummarizer()) ## # summarize(EdmundsonSummarizer()) ## bonus_wordsが必要と怒られるので、この呼び出し方ではダメなので省略
出力例
上記の実行結果です。
今回は、評価は行わないので雰囲気のみ。
以上です。