文章の要約

2020年12月26日

はじめに

sumy による文章の要約について。

環境

  • macOS 10.14.6
  • Miniconda3
  • Python 3.7
  • sumy 0.8.1
  • Janome 0.4.1
  • NumPy 1.17.0 (sumy に必要)
  • TinySegmenter 0.4 (Janome に必要)

セットアップ

conda を使う。仮想環境を作る。

(base) $ conda create -n text python=3.7

sumy、janome、numpy、tinysegmenter をインストール。

(base) $ conda activate text
(text) $ pip install sumy
(text) $ pip install janome
(text) $ conda install numpy
(text) $ pip install tinysegmenter

sumy による文章の要約

文章の要約の方法として、抽出 (extract) と抽象 (abstract) 型とあるらしい。抽出型は、もとの文章から文を取り出して要約を構成する。文を選ぶだけなので比較的簡単なようだが、もとの文章に依存する。たとえば、アンケートの自由記述をこれで整理すると、様々な文体が混ざることになる。一方の抽象型は、ちゃんと要約するやつで、やはり難しいらしい。sumy は抽出型の要約モジュールである。

モジュールの準備。

import re
from janome.tokenizer import Tokenizer as JanomeTokenizer
from janome.charfilter import UnicodeNormalizeCharFilter, RegexReplaceCharFilter
from janome.tokenfilter import POSKeepFilter, ExtractAttributeFilter
from janome.analyzer import Analyzer
from sumy.parsers.plaintext import PlaintextParser
from sumy.nlp.tokenizers import Tokenizer
from sumy.summarizers.lex_rank import LexRankSummarizer
from sumy.utils import get_stop_words

要約の処理は以下のようになる。

def summrize(filename, sentences_count, encoding="utf-8"):
    contents = []
    with open(filename, encoding=encoding) as f:
        for line in f:
            line = line.strip().strip("\r\n")
            if line != "" and line[-1] == "。":
                contents.append(line)
                
    contents = "".join(contents)
    
    #print("contents =")
    #print(contents)
    #print()
    
    contents = contents.replace("\n", "")
    contents = contents.replace("\"", "")
    text = re.findall("[^。]+。?",  contents)
    
    #print("text =")
    #print(text)
    #print()
    
    text_len = []
    for t in text:
        text_len.append(len(t))
    
    print("average length = ", sum(text_len)/len(text_len))
                                            
    tokenizer = JanomeTokenizer("japanese")
    char_filters = [UnicodeNormalizeCharFilter(), RegexReplaceCharFilter(r"[(\)「」、。]", " ")]
    token_filters = [POSKeepFilter(["名詞", "形容詞", "副詞", "動詞"]), ExtractAttributeFilter("base_form")]
    analyzer = Analyzer(
        char_filters=char_filters,
        tokenizer=tokenizer,
        token_filters=token_filters
    )
    
    corpus = [" ".join(analyzer.analyze(sentence)) + "。" for sentence in text]
    
    #print("corpus =")
    #print(corpus)
    #print(len(corpus))
    #print()
    
    parser = PlaintextParser.from_string("".join(corpus), Tokenizer("japanese"))
    
    summarizer = LexRankSummarizer()
    summarizer.stop_words = get_stop_words("japanese")
    
    summary = summarizer(document=parser.document, sentences_count=sentences_count)
    summary_text = "".join([text[corpus.index(sentence.__str__())] for sentence in summary])
    
    print("summary =")
    print(summary_text)
    print("length =", len(summary_text))

ファイル名を与えたら、それを読み込んで要約を表示する。やりかたは色々ありそうだが、ここでは余計なタイトルなどを取り除くために "。" で終わる文だけを取り出すようにしている。LexRank という手法を用いている。

サンプル 1

自分が書いた長めの記事 (OpenFOAM 情報) を要約してみる。

とりあえず 1 文だけ取り出してみる。

summrize("sample.txt", 1)

結果

average length =  47.03404255319149
summary =
OpenFOAM は Linux 用に開発されているので、Linux マシンを用意するのが素直である。
length = 51

十分に内容を含みそうで、読んで苦痛でない長さとして 300 文字を目安としてみる。1 文が 50 文字程度なので、6 文取り出してみる。

summrize("sample.txt", 6)

結果

average length =  47.03404255319149
summary =
Linux ユーザーで、CFD コードを自分で書くような人ならよいかも。適当な機能を備えたソルバーが標準ソルバーの中にない場合は、自分でソルバーを開発する必要がある。OpenFOAM は Linux 用に開発されているので、Linux マシンを用意するのが素直である。本来は Linux 用の OpenFOAM だが、Windows 版もあるにはある。ソルバーを使うだけなら必要ないが、ソースコードを理解したいのなら、それ専用のツールを使ったほうがよい。OpenFOAM Foundation 版 OpenFOAM と OpenCFD (ESI) 版 OpenFOAM (OpenFOAM+) の違いは、OpenFOAM v3.0+ ?? にあるように、RHEL と Fedora の違いのようなものとのことであるが、よくわからない。
length = 367

なんでこんな結果なのかはわからないが、いい線はいっている気がする。

サンプル 2

比較的短い作品だが、中島敦の 名人伝 を要約してみる。

summrize("sample2.txt", 1)

結果

average length =  42.82142857142857
summary =
その四十年の間、彼は絶えて射を口にすることが無かった。
length = 27

300 文字目安で 8 文選んでみる。

summrize("sample2.txt", 8)

結果

average length =  42.82142857142857
summary =
毎日毎日彼は窓にぶら下った虱を見詰める。ある日ふと気が付くと、窓の虱が馬のような大きさに見えていた。傍で見ていた師の飛衛も思わず「善し!」と言った。弓? と老人は笑う。弓矢の要いる中はまだ射之射じゃ。九年たって山を降りて来た時、人々は紀昌の顔付の変ったのに驚いた。これでこそ初めて天下の名人だ。その四十年の間、彼は絶えて射を口にすることが無かった。
length = 174

なんかこんな話だった気もするが、ちょっと説明不足な感じである。

意外と短かったので、300 文字になるように文を増やしてみる。

summrize("sample2.txt", 13)

結果

average length =  42.82142857142857
summary =
趙ちょうの邯鄲かんたんの都に住む紀昌きしょうという男が、天下第一の弓の名人になろうと志を立てた。それを聞いて飛衛がいう。毎日毎日彼は窓にぶら下った虱を見詰める。初め、もちろんそれは一匹の虱に過ぎない。ある日ふと気が付くと、窓の虱が馬のような大きさに見えていた。傍で見ていた師の飛衛も思わず「善し!」と言った。※(「にんべん+爾」、第3水準1-14-45)の師と頼むべきは、今は甘蠅師の外にあるまいと。弓? と老人は笑う。弓矢の要いる中はまだ射之射じゃ。不射之射には、烏漆うしつの弓も粛慎しゅくしんの矢もいらぬ。九年たって山を降りて来た時、人々は紀昌の顔付の変ったのに驚いた。これでこそ初めて天下の名人だ。その四十年の間、彼は絶えて射を口にすることが無かった。
length = 329

「にんべん+爾」となっているのは、「なんじ」である。よくまとまっているような気もしなくもない。

モジュールの準備。