📑

Google Colabで日本語要約してみた

2021/03/18に公開1

pythonで日本語の要約をしてみたいと思い、調べてみました。
「sumy」というパッケージを使って要約ができました。

パターンを2つ掲載しています。1が簡易版、2が少しリッチなものになってます。

  1. LexRankアルゴリズムとsumyでの実装
  2. spaCy(自然言語処理ライブラリ)とsumyでの実装、アルゴリズムは選択可能(LexRank、TextRank、Lsa、KL、Luhn、Reduction、SumBasic)、日本語と英語に対応

手順、所感など

パターン1は、だいたい記事の通りで動きましたが、前処理が少なかったようで要約結果が空になってしまいました。そのためパターン2の実装を参考に前処理を加えています。(preprocessing関数)

パターン2は、Google Colab上で動かすために必要な設定が足りなかったようなので、環境構築の部分を加えています。

処理速度はパターン1の方が圧倒的に速いです。
出力結果は微妙に異なっていて、精度に差があるようです。パターン2はアルゴリズムが選べるので、一番要約できてそうな印象のものを選ぶと良さそうです。

要約結果比較

全体の3割程度でそれぞれ要約した結果です。

原文

こちらの記事を要約してみました。

引用元)
https://jp.techcrunch.com/2021/03/09/2021-03-08-zapier-buys-no-code-focused-makerpad-in-its-first-acquisition/

パターン1(LexRank)

2割設定です。

ノーコードの自動化ツールで知られるzapier(ザピアー)は、ノーコード教育サービスとコミュニティのmakerpad(メイカーパッド)を買収した。
取り引きの条件については公表されていない。
すると彼は、合併することで、単独では成し得ないノーコードの世界の拡大が可能になるからだと答えた。
トッセル氏がtechcrunchに話したところによれば、ノーコードはすでに「想像以上に大きく成長している」とのこと。
それが現在、コミュニティを引き寄せ、同社サービスにユーザーを直接招き入れることが可能になるだろう。
ローコードでは9桁のラウンドも現れている。
この数カ月間で、ノーコード界の重要人物やローコード界の企業創設者と投資家に話を聞いてきた結果として、さらに大きなビジネス市場が、ローコードサービスと、ノーコードツールをいち早く採り入れた小さな企業の周辺に近づいて来るのは確かなようだ。
ローコードツールが次第にコーディングから独立し、ノーコードツールの機能性が高まると、この2つの兄弟カテゴリーは融合することになるだろう。

パターン2(LexRank)

こちらも2割設定。

取り引きの条件については公表されていない。
techcrunchはトッセル氏に、会社の売却がなぜ今なのかを尋ねた。
airtableとzapierがmakerpadの中心的ユーティリティーになるとチェン氏は話している)。
彼はずっと前からトッセル氏の仕事に注目していて、以前にも夕食をともにしたことがあったそうだ。
トッセル氏がtechcrunchに話したところによれば、ノーコードはすでに「想像以上に大きく成長している」とのこと。
それが現在、コミュニティを引き寄せ、同社サービスにユーザーを直接招き入れることが可能になるだろう。
その兄弟であるニッチ市場のローコード分野も同じだ。
ローコードでは9桁のラウンドも現れている。
この数カ月間で、ノーコード界の重要人物やローコード界の企業創設者と投資家に話を聞いてきた結果として、さらに大きなビジネス市場が、ローコードサービスと、ノーコードツールをいち早く採り入れた小さな企業の周辺に近づいて来るのは確かなようだ。

パターン2(TextRank)

ノーコードの自動化ツールで知られるzapier(ザピアー)は、ノーコード教育サービスとコミュニティのmakerpad(メイカーパッド)を買収した。
それ以来、同社はサービスに高額なプランを追加し、チーム作業向けの機能を構築し、最近ではリモートのみで運用するチームの拡大についてextracrunchに語っている。
techcrunchはmakerpadの創設者であるbentossell(ベン・トッセル)氏に今回の取り引きの構成について尋ねたところ、新たな親会社の下で「スタンドアローン」部門として運営することになると電子メールで答えてくれた。
トッセル氏は「makerpadの究極のビジョンは、できるだけ多くの人に、コードを書かずにソフトウェアが構築できる力を教えること」だ話している。
彼はずっと前からトッセル氏の仕事に注目していて、以前にも夕食をともにしたことがあったそうだ。
フォスター氏は、新型コロナ後の世界でスモールビジネスの同社のサービスへの依存度が次第に高まっていることについて詳しく説明してくれた。
トッセル氏がtechcrunchに話したところによれば、ノーコードはすでに「想像以上に大きく成長している」とのこと。
この数カ月間で、ノーコード界の重要人物やローコード界の企業創設者と投資家に話を聞いてきた結果として、さらに大きなビジネス市場が、ローコードサービスと、ノーコードツールをいち早く採り入れた小さな企業の周辺に近づいて来るのは確かなようだ。
ローコードツールが次第にコーディングから独立し、ノーコードツールの機能性が高まると、この2つの兄弟カテゴリーは融合することになるだろう。

自動要約サービス

参考までに自動要約サービスを使って比較してみました。

ユーザーローカル自動要約ツール

ユーザーローカルさんの自動要約。10行ダイジェストです。

取り引きの条件については公表されていない。 
これを読んだZapier のCEO から接触があり、話が進んで契約に至ったという。 
両社とも、最近の四半期で急速な成長を遂げている。 
パンデミックに襲われたZapier には中小企業からの力強い引き合いがあったという。 
ノーコードはすでに「想像以上に大きく成長している」とのこと。 
また、おそらくノーコーダーの集団を時間とともに大きくしていくはずだ。 
兄弟であるニッチ市場のローコード分野も同じだ。 
ローコードでは9桁のラウンドも現れている。 
一部の企業では、ローコードで社内用ソフトウェアを迅速に開発できるようにもなった。 
SPAC の可能性を尋ねると、フォスター氏の答えはやや明確になった。

IMAKITA Document Squeezer

こちらも10行ダイジェストです。

ノーコードの自動化ツールで知られるZapier(ザピアー)は、ノーコード教育サービスとコミュニティのMakerpad(メイカーパッド)を買収した。
TechCrunchでは、その誕生以来ずっとZapierを追いかけてきた。
それ以来、同社はサービスに高額なプランを追加し、チーム作業向けの機能を構築し、最近ではリモートのみで運用するチームの拡大についてExtra Crunchに語っている。
今回の取り引きでは、この小さなスタートアップが契約前から取り組んできた事業をひっくり返すようなことは想定されていないようだ。
フォスター氏は、新型コロナ後の世界でスモールビジネスの同社のサービスへの依存度が次第に高まっていることについて詳しく説明してくれた。

実装

以下、コードです。今回はあるWebページからテキストを取得して来るという実装を行っています。

パターン1

参考記事)
https://software-data-mining.com/pythonによる自然言語処理技法をふんだんに使用した/

!pip install sumy tinysegmenter Janome
!pip install neologdn emoji mojimoji

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
import re

from sumy.parsers.plaintext import PlaintextParser
from sumy.nlp.tokenizers import Tokenizer
from sumy.summarizers.lex_rank import LexRankSummarizer

import requests
from bs4 import BeautifulSoup

import emoji
import mojimoji

def preprocessing(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

# スクレイピング対象の URL にリクエストを送り HTML を取得する
res = requests.get('対象のURL')

# レスポンスの HTML から BeautifulSoup オブジェクトを作る
soup = BeautifulSoup(res.text, 'html.parser')

# title タグの文字列を取得する
contents = soup.select_one('テキストがある要素のCSSセレクタ').get_text()
#print(contents)

text = re.findall("[^。]+。?", preprocessing(contents))
#print(text)

# 形態素解析(単語単位に分割する)
analyzer = Analyzer(char_filters=[UnicodeNormalizeCharFilter(), RegexReplaceCharFilter(r'[(\)「」、。]', ' ')], tokenizer=JanomeTokenizer(), token_filters=[POSKeepFilter(['名詞', '形容詞', '副詞', '動詞']), ExtractAttributeFilter('base_form')])

corpus = [' '.join(analyzer.analyze(sentence)) + u'。' for sentence in text]
print(corpus)
print(len(corpus))

# 文書要約処理実行
parser = PlaintextParser.from_string(''.join(corpus), Tokenizer('japanese'))

# LexRankで要約を原文書の3割程度抽出
summarizer = LexRankSummarizer()
summarizer.stop_words = [' ']

# 文書の重要なポイントは2割から3割といわれている?ので、それを参考にsentences_countを設定する。
summary = summarizer(document=parser.document, sentences_count=int(len(corpus)/10*3))

print(u'文書要約結果')
print(len(summary))
for sentence in summary:
  print(text[corpus.index(sentence.__str__())])

パターン2

参考記事)
https://deepblue-ts.co.jp/nlp/sumy-extractives-ummarization/

!pip install neologdn emoji mojimoji
!python -m spacy download en_core_web_sm
!pip install fugashi[unidic-lite]

!pip install -U ginza
!pip install sumy

!pip install tinysegmenter
!pip install janome

from janome.tokenizer import Tokenizer as JanomeTokenizer
from janome.analyzer import Analyzer
from janome.charfilter import *
from janome.tokenfilter import *

import pkg_resources, imp
imp.reload(pkg_resources)

import spacy
nlp = spacy.load('ja_ginza')

from ginza import *

import neologdn
import re
import emoji
import mojimoji

class JapaneseCorpus:
    # ①
    def __init__(self):
        self.nlp = spacy.load('ja_ginza')
        self.analyzer = Analyzer(
            char_filters=[UnicodeNormalizeCharFilter(), RegexReplaceCharFilter(r'[(\)「」、。]', ' ')],  # ()「」、。は全てスペースに置き換える
            tokenizer=JanomeTokenizer(),
            token_filters=[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

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)
    sentences_count = int(len(corpus)/10*3)
    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])

import requests
from bs4 import BeautifulSoup

# スクレイピング対象の URL にリクエストを送り HTML を取得する
res = requests.get('対象のURL')

# レスポンスの HTML から BeautifulSoup オブジェクトを作る
soup = BeautifulSoup(res.text, 'html.parser')

# title タグの文字列を取得する
contents = soup.select_one('テキストがある要素のCSSセレクタ').get_text()
text = contents

algorithm = "lex"
language="japanese"
sum_sentences = summarize_sentences(text, algorithm=algorithm, language=language)
print(sum_sentences.replace('。', '。\n'))

Discussion

Seiki TokunagaSeiki Tokunaga

とても参考になる記事ありがとうございます。
文書要約を実施したかったのでとても助かりました。
1点、掲載頂いているパターン2を実施しようとした際に以下のような実行時エラーが発生しました。
解決方法まで記載してみました、お手すきの際にご確認宜しくお願いいたします。

問題

OSError: [E050] Can't find model 'ja_ginza'. It doesn't seem to be a Python package or a valid path to a data directory.

解決方法

以下の記事を参考に↓のコマンドをCollab上で実行した後にパターン2を実行したところ、無事要約が出力できました。

!pip install -U ginza ja-ginza # jupyter-lab

【Python】spacyのload()でja_ginzaがPATHエラーを吐くときの対処法