今回はPythonのパッケージである「sumy」を用いて文章要約を行います。文章要約の技術には要約元の文章から新しい文章を自動生成する「抽象型」と文章の内容を表す上で重要な文を何らかのアルゴリズムを用いて抽出する「抽出型」があり、sumyは抽出型の要約を行うことができるパッケージです。また、sumyは様々な抽出型アルゴリズムが備わっているため、複数のアルゴリズムを試したり比較したりすることができます。

実行環境

  • Python==3.7.3

  • sumy==0.8.1

  • tinysegmenter==0.4(sumyの内部で使用されるシンプルな形態素解析器)

  • 文ごとに分割~形態素解析

    • spacy==2.2.4
    • ja-ginza==3.1.0
    • ja-ginza-dict==3.1.0
    • Janome==0.3.10
    • en_core_web_sm==2.2.5(spacyの英語辞書)
  • 前処理

    • mojimoji==0.0.11
    • emoji==0.6.0
    • neologdn==0.4

spacyの英語の辞書を以下のコマンドでダウンロードしておきます

python -m spacy download en_core_web_sm

コーパスの作成

要約したい文章を、sumyに渡すためにコーパスを作成します。流れとしては1文ずつ形態素解析を行なって、単語を空白区切にします。日本語と英語の場合で若干処理が異なるため、クラスを2つ作成指定ます。

import spacy
import neologdn
import re
import emoji
import mojimoji

class JapaneseCorpus:
    # ①
    def __init__(self):
        self.nlp = spacy.load('ja_ginza')
        self.analyzer = Analyzer(
            [UnicodeNormalizeCharFilter(), RegexReplaceCharFilter(r'[(\)「」、。]', ' ')],  # ()「」、。は全てスペースに置き換える
            JanomeTokenizer(),
            [POSKeepFilter(['名詞', '形容詞', '副詞', '動詞']), ExtractAttributeFilter('base_form')]  # 名詞・形容詞・副詞・動詞の原型のみ
        )

    # ②
    def preprocessing(self, text):
        text = re.sub(r'\n', '', text)
        text = re.sub(r'\r', '', text)
        text = re.sub(r'\s', '', text)
        text = text.lower()
        text = mojimoji.zen_to_han(text, kana=True)
        text = mojimoji.han_to_zen(text, digit=False, ascii=False)
        text = ''.join(c for c in text if c not in emoji.UNICODE_EMOJI)
        text = neologdn.normalize(text)

        return text

    # ③
    def make_sentence_list(self, sentences):
        doc = self.nlp(sentences)
        self.ginza_sents_object = doc.sents
        sentence_list = [s for s in doc.sents]

        return sentence_list

    # ④
    def make_corpus(self):
        corpus = [' '.join(self.analyzer.analyze(str(s))) + '。' for s in self.ginza_sents_object]

        return corpus

class EnglishCorpus(JapaneseCorpus):
    # ①
    def __init__(self):
        self.nlp = spacy.load('en_core_web_sm')

    # ②
    def preprocessing(self, text):
        text = re.sub(r'\n', '', text)
        text = re.sub(r'\r', '', text)
        text = mojimoji.han_to_zen(text, digit=False, ascii=False)
        text = mojimoji.zen_to_han(text, kana=True)
        text = ''.join(c for c in text if c not in emoji.UNICODE_EMOJI)
        text = neologdn.normalize(text)        

        return text

    # ④
    def make_corpus(self):
        corpus = []
        for s in self.ginza_sents_object:
            tokens = [str(t) for t in s]
            corpus.append(' '.join(tokens))

        return corpus

①ではspacyで用いる辞書を読み込みと、形態素解析機の準備を行なっています。
②ではテキストを前処理にかけています。ここは想定するテキストによって適宜変更してください。
③ではspacyで文章を1文ずつに分けています。返り値はDocオブジェクトで、1文ずつに分割した結果であるだけでなく、形態素解析の結果も保持しています。詳しくはこちらを読んでいただけるとよろしいかと思います。
④では1文ずつ単語を空白区切にしています。日本語の場合は形態素解析が必要ですが、③で出力されたDocオブジェクトは前述したように形態素解析の結果を保持しているため、イテレーションすることで1文ずつ形態素解析の結果を取得できるのですが、今回のタスクではjanomeを用いた場合の方が精度がよかったため、janomeで形態素解析を行なっています。形態素解析の部分では今回は名詞、副詞、形容詞、動詞の単語のみを残して空白区切りにします。

要約

いよいよsumyを使った要約を行います。といってもコーパスを渡すだけで要約結果を出力してくれるのでとても便利です。今回はあらかじめ機能として備わっているLexRank、TextRank、LSA、KL、 Luhn、 Reduction、SumBasicのアルゴリズムを使えるような実装にしています。公式リファレンスに各アルゴリズムの説明や論文のリンクがあります。sentences_countを指定することで要約後の文章の数を指定することができます。

from sumy.parsers.plaintext import PlaintextParser
from sumy.nlp.tokenizers import Tokenizer
from sumy.utils import get_stop_words

# algorithms
from sumy.summarizers.lex_rank import LexRankSummarizer
from sumy.summarizers.text_rank import TextRankSummarizer
from sumy.summarizers.lsa import LsaSummarizer
from sumy.summarizers.kl import KLSummarizer
from sumy.summarizers.luhn import LuhnSummarizer
from sumy.summarizers.reduction import ReductionSummarizer
from sumy.summarizers.sum_basic import SumBasicSummarizer

algorithm_dic = {"lex": LexRankSummarizer(), "tex": TextRankSummarizer(), "lsa": LsaSummarizer(),\
                 "kl": KLSummarizer(), "luhn": LuhnSummarizer(), "redu": ReductionSummarizer(),\
                 "sum": SumBasicSummarizer()}

def summarize_sentences(sentences, sentences_count=3, algorithm="lex", language="japanese"):
    # ①
    if language == "japanese":
        corpus_maker = JapaneseCorpus()
    else:
        corpus_maker = EnglishCorpus()
    preprocessed_sentences = corpus_maker.preprocessing(sentences)
    preprocessed_sentence_list = corpus_maker.make_sentence_list(preprocessed_sentences)
    corpus = corpus_maker.make_corpus()
    parser = PlaintextParser.from_string(" ".join(corpus), Tokenizer(language))

    # ②
    try:
        summarizer = algorithm_dic[algorithm]
    except KeyError:
        print("algorithm name:'{}'is not found.".format(algorithm))

    summarizer.stop_words = get_stop_words(language)
    summary = summarizer(document=parser.document, sentences_count=sentences_count)

    # ③
    if language == "japanese":
        return "".join([str(preprocessed_sentence_list[corpus.index(sentence.__str__())]) for sentence in summary])
    else:
        return " ".join([sentence.__str__() for sentence in summary])

①では言語ごとに先ほど作成したクラスを切り替えて前処理からコーパスの作成を行なっています。また、PlaintextParserの部分で連結したcorpusを再度tinysegmenterでトークナイズさせて、sumyが受け取れる形にしています。
②以降ではアルゴリズムを選択して、実際に要約をしています。stopwordは言語ごとに用意されているものを使っていますが、個別に指定することも可能です。
③では日本語の場合は要約の結果を元の文章(単語区切になっていない文章)にして返しています。英語の場合は結果をそのまま連結して返します。

実行例

text = """東京都の小池百合子知事は7月5日の東京都知事選挙を難なく圧勝し、2期目を確保して、予測不可能な時期に行なわれた予測可能な選挙活動に終止符を打った。
7月15日に68歳になるこの現職の知事(小池氏のこと)は、不安なパンデミックの存在によって影が薄れた選挙活動で、自分よりも知名度の低い多数の候補者と戦った。 
小池氏が獲得した3,661,371票は、次点だった候補者の4倍以上で、東京の有権者の多数が新型コロナウイルスとの戦いを続けることに小池氏を信頼していることを示している。
73歳の弁護士で元日本弁護士連合会会長の宇都宮健児氏はわずか844,151票の得票だった。俳優から既成政党に対抗する政党のれいわ新選組の代表へと転身した山本太郎氏(45)は3位で657,277票の得票で終わった。
選挙の投票率は55%で、2016年の選挙の59%から減少した。
小池氏は、検査のキャパシティを増し、病院のベッド数を増やし、都の医療システムを強化することで、新型コロナウイルスの予防と、第2波への備えに集中するつもりだと語った。
小池氏は、新型コロナウイルスへの都の対応を強固にし、来年「簡素化された」オリンピックを実施するため、アメリカの疾病対策予防センター(CDC)に似た、東京独自の疾病コントロールセンターを創設することを目指しているとも語った。"""
sentences_count = 3
algorithm = "lex"
language="japanese"
sum_sentences = summarize_sentences(text, sentences_count=sentences_count, algorithm=algorithm, language=language)
print(sum_sentences)
東京都の小池百合子知事は7月5日の東京都知事選挙を難なく圧勝し、2期目を確保して、予測不可能な時期に行なわれた予測可能な選挙活動に終止符を打った。
7月15日に68歳になるこの現職の知事(小池氏のこと)は、不安なパンデミックの存在によって影が薄れた選挙活動で、自分よりも知名度の低い多数の候補者と戦った。
小池氏が獲得した3,661,371票は、次点だった候補者の4倍以上で、東京の有権者の多数が新型コロナウイルスとの戦いを続けることに小池氏を信頼していることを示している。
en_text = """Tokyo Gov. Yuriko Koike cruised to a resounding victory in the gubernatorial election July 5,securing a second term and marking the end of a predictable campaign held during unpredictable times.
The incumbent, who turns 68 on July 15, 
was pitted against a slew of lesser-known candidates in a campaign overshadowed by the unnerving presence of a pandemic. 
Koike’s vote total of 3,661,371 was more than four times higher than that of her closest challenger, 
a sign that a majority of voters in Tokyo trust her to continue the battle against the novel coronavirus. 
Kenji Utsunomiya, a 73-year-old lawyer and former head of the Japan Federation of Bar Associations, 
won only 844,151 votes. Taro Yamamoto, 45, an actor-turned-leader of anti-establishment party Reiwa Shinsengumi, 
finished third with 657,277 votes. Election turnout was 55%, down from 59% in the 2016 poll. 
Koike said she aims to focus her efforts on preventing and preparing for a possible second wave of the novel coronavirus by enhancing testing capacity, 
increasing the number of hospital beds and bolstering the city’s health care system. She also said she aims to establish Tokyo’s own center for disease control, 
akin to the Centers for Disease Control and Prevention (CDC) in the U.S., 
to consolidate the city’s response to the virus and stage a “simplified” Olympics next year."""
sentences_count = 3
algorithm = "lex"
language="english"
sum_sentences = summarize_sentences(en_text, sentences_count=sentences_count, algorithm=algorithm, language=language)
sum_sentences
'Tokyo Gov. 
Yuriko Koike cruised to a resounding victory in the gubernatorial election July 5,securing a second term and marking the end of a predictable campaign held during unpredictable times . 
The incumbent , who turns 68 on July 15 , was pitted against a slew of lesser - known candidates in a campaign overshadowed by the unnerving presence of a pandemic .'

このように、sumyを使うとシンプルなコードで抽出型の文章要約を実装することができます。興味がある方はぜひ試してみてください。

(著:木村 幸

関連記事