はてだBlog(仮称)

私的なブログど真ん中のつもりでしたが、気づけばWebサイト系のアプリケーション開発周りで感じたこと寄りの自分メモなどをつれづれ述べています。2020年6月現在、Elasticsearch、pandas、CMSなどに関する話題が多めです。...ですが、だんだんとより私的なプログラムのスニペット置き場になりつつあります。ブログで述べている内容は所属組織で販売している製品などに関するものではなく、また所属する組織の見解を代表するものではありません。

sumy で要約(spaCy、GiNZA) を試してみた: Pythonで自然言語処理にフリーライド

要約・キーフレーズ抽出について

sumy は、Pythonで実装された、抽出型のドキュメント要約ライブラリです。

3行でまとめて! ってやつですね。

ドキュメント中の最重要と思われるセンテンスを抜き出すことで、元の内容のエッセンスを抽出することをめざします。

sumyについて

sumyの公式ページにも書いてありますが、著名な要約アルゴリズムをいくつか実装しているようです。

今回は、それぞれのアルゴリズムでサンプルデータに対してどのような要約が得られるか確認してみます。

... といいつつ、アカデミックな評価などは行っておりません。 このブログのいつもの、使ってみた・多分うまく動いている系のおためしサンプルコードをペタっと貼り付けですので、ご了承ください。

pypi.org

github.com

sumy/summarizators.md at master · miso-belica/sumy · GitHub

sumyは、「japanese」対応にはなっていますが、表記揺れや語形変化を考慮した、出現数カウントやそもそもの単語の分かち書きには対応していないので、spaCyとGiNZAでサポートすることにしています。

spaCy/ GiNZA

spaCyは、Pythonの今時の自然言語処理ライブラリのようです。実際にプロダクトに組み込んで使えるような機能群がそろっているという触れ込みのようです。

また、GiNZAはspaCy上で日本語ドキュメントを処理するにあたって、日本語関係のいろいろモダンな叡智がつまっているライブラリのようです。

megagonlabs.github.io

spacy.io

それぞれ、今回利用したような範囲以上のパワフルな使い方ができると思いますが、今回は、sumyを日本語対応させるにあたり、日本語での分かち書き・レンマ化のツールとして使いました。

sumyの(ほぼ)最小限の使い方*1

  1. 対象ドキュメントを読み込む(sumyには、WebサイトのhtmlをダウンロードするHTTPクライアント機能などもあるようですが、今回はテキストファイルを文字列として読み込みした例を使います)
  2. 当該ドキュメントに関して、その言語での、文の区切れ、単語の区切れを取得するルールを定義する(Token化)
  3. 上記2のToken化クラス*2を、今回使いたい要約アルゴリズムの関数に引数として与える 〜 解析
  4. 戻り値として、要約の文がイテレータで得られる

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が必要と怒られるので、この呼び出し方ではダメなので省略

出力例

上記の実行結果です。

今回は、評価は行わないので雰囲気のみ。

f:id:azotar:20200607205324p:plain

以上です。

◆参考リンク

「要約」について

qiita.com

spaCy と GiNZA

www.ogis-ri.co.jp

sumyに関して日本語参考にさせていただいたサイト

ohke.hateblo.jp

sumyを使って青空文庫を要約してみる - Qiita

Pythonの要約(抽出型 Extractiveのもので)いつか試してみたいもの

boudinfl.github.io

はじめての自然言語処理 pke によるキーフレーズ抽出 | オブジェクトの広場

nakagami.blog.ss-blog.jp

*1:筆者が見よう見まねや公式ホームページを見てこうかなと思うやり方です。

*2:便宜上クラスと言いましたが、ファクトリーメソッド的なもの。