自然言語処理の領域の一部に、文章要約の世界があります。
そこではLexRankだとかTextRankだとか、LSAだとかいろいろな文章を短くする(というよりかは重要な文章を特定する)アルゴリズムが開発されていて、pythonのsumyというライブラリではとてもお手軽にそれらを利用することができます。
アルゴリズムの中身は全然理解できていなくて、面白そうだから触ってみた、くらいの感覚ですが、同じように文章要約どんなもんかなと気になっている人の参考になれば幸いです。
テキストデータを準備
今回使う「羅生門」のテキストデータは青空文庫のサイトからダウンロードできます。
(※著作権は切れており、ダウンロードしても起こられることはありません。)
https://www.aozora.gr.jp/cards/000879/card127.html#download
こちらから、「127_ruby_150.zip」のファイルをダウンロードして、解凍すると中にテキストファイルが入っています。
データの前処理~分かち書き
rashomon.txtには本文以外にもルビの説明や注記等、今回やりたいことに対して余計な情報が含まれているので、まずはそれを削除してしまいます。
そのあと、janomeを用いて文章を分かち書きしていきます。
データの前処理
データを読み込み
1 2 3 4 5 6 7 8 9 10 11 |
import numpy as np import pandas as pd with open("rashomon.txt","r",encoding="shift_jis") as f: data = f.read() print(data) ''' '羅生門\n芥川龍之介\n\n-------------------------------------------------------\n【テキスト中に現れる記号について】\n\n《》:ルビ\n(例)下人《げにん》\n\n|:ルビの付く文字列の始まりを特定する記号\n(例)所々|丹塗《にぬり》の剥《は》げた\n\n[#]:入力者注\u3000主に外字の・・・ ''' |
そのさいshift-jis形式にエンコードしておきましょう。
前処理
1 2 3 4 |
import re data = re.sub("《[^》]+》", "", data) data = re.sub("[[^]]+]", "", data) data = re.sub("[| 「」\n]", "", data) |
基本的な使い方としてはsampleという変数に文章が入っていたとしたら、
reg_sample = re.sub(“置換前の文字(列)”,”置換後の文字(列)”,sample)
みたいな風に文字列置換を行えます。
[] はその間に入る各文字を認識してくれて、^は除外、+は繰り返しを表現するので、例えばre.sub(“《[^》]+》”, “”, data)とした場合は、《~~》という固まりを空白に置換、ということになります。
※rashomon.txtでは 下人《げにん》 といった表現でルビが振られています。
本文だけを抽出する
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
lines = data.split("。") lines[0] ''' '羅生門芥川龍之介-------------------------------------------------------【テキスト中に現れる記号について】《》:ルビ(例)下人:ルビの付く文字列の始まりを特定する記号(例)所々丹塗の剥げた:入力者注主に外字の説明や、傍点の位置の指定(数字は、JISX0213の面区点番号、または底本のページと行数)(例)※-------------------------------------------------------ある日の暮方の事である' ''' #本文開始はlines[1]、本文末尾はlines[-4]であることを確認 lines[1] ''' '一人の下人が、羅生門の下で雨やみを待っていた' ''' lines[-4] ''' '下人の行方は、誰も知らない' ''' #本文のみ抽出 mainText = lines[1:-3] #各文章の末尾に。をつける mainText = [x+"。" for x in mainText] print(mainText[0:5]) ''' ['一人の下人が、羅生門の下で雨やみを待っていた。', '広い門の下には、この男のほかに誰もいない。', 'ただ、所々丹塗の剥げた、大きな円柱に、蟋蟀が一匹とまっている。', '羅生門が、朱雀大路にある以上は、この男のほかにも、雨やみをする市女笠や揉烏帽子が、もう二三人はありそうなものである。', 'それが、この男のほかには誰もいない。'] ''' |
分かち書き
janomeを用いて文章を分かち書きします。
sumyで処理するために、例えば「この男のほかには誰もいない」という文章を
「この 男 の ほか に は 誰 も い ない」
なんていう風に形態素解析して各単語間に空白をいれる感じにでデータを変形します。
※初めて使う人はpip install janomeをしておきましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
from janome.tokenizer import Tokenizer as Tokenizer t = tokenizer() wakati = [] tmp1 = [] tmp2 = [] for line in mainText: for token in t.tokenize(line): tmp1.append(token.surface) tmp2 = " ".join(tmp1) wakati.append(tmp2) tmp1 = [] #分かち書き結果を確認 print(wakati[0]) ''' '一 人 の 下人 が 、 羅生門 の 下 で 雨 やみ を 待っ て い た 。' ''' #文章をくっつける wakati = " ".join(wakati) |
こんな感じでテキストが分かち書きされていればOKです!
文章要約
sumyというモジュールを使用します。sumyとは↓
Module for automatic summarization of text documents and HTML pages.
この引用元のページにいくと、テキストサマライザーとしてどんなメソッドを使えるかが一覧で表示されています。

各アルゴリズムに関しては、リンク先で紹介されている論文を読むことで理解を深められます。
お試しで、今回はLSAとLexRank,TextRankでやってみようと思います。
はじめてつかう方は
pip install tinysegmenter
をしておきましょう。
実装についてはこちらのブログをまんま参考にさせてもらいました。ありがとうございます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
from sumy.parsers.plaintext import PlaintextParser from sumy.nlp.tokenizers import Tokenizer from sumy.summarizers.lex_rank import LexRankSummarizer from sumy.summarizers.text_rank import TextRankSummarizer from sumy.summarizers.lsa import LsaSummarizer parser = PlaintextParser.from_string("".join(wakati),Tokenizer("japanese")) #Lex-Rank LexRankSummarizer = LexRankSummarizer() LexRankSummarizer.stop_words = [" "] summary = summarizer(document=parser.document, sentences_count=5) #5行に要約 print(summary) ''' (<Sentence: それ が 、 この 男 の ほか に は 誰 も い ない 。>, <Sentence: 上 なら 、 人 が い た に し て も 、 どうせ 死人 ばかり で ある 。>, <Sentence: それ が 、 梯子 を 二 三 段 上っ て 見る と 、 上 で は 誰 か 火 を とぼし て 、 しかも その 火 を そこ ここ と 動かし て いる らしい 。>, <Sentence: この 時 、 誰か が この 下人 に 、 さっき 門 の 下 で この 男 が 考え て い た 、 饑死 を する か 盗人 に なる か と 云う 問題 を 、 改めて 持出し たら 、 恐らく 下人 は 、 何 の 未練 も なく 、 饑死 を 選ん だ 事 で あろ う 。>, <Sentence: その 時 の この 男 の 心もち から 云え ば 、 饑死 など と 云う 事 は 、 ほとんど 、 考える 事 さえ 出来 ない ほど 、 意識 の 外 に 追い出さ れ て い た 。>) ''' #LSA LsaSummarizer = LsaSummarizer() summary = LsaSummarizer(document=parser.document, sentences_count=5) print(symmary) ''' (<Sentence: むしろ 、 あらゆる 悪 に対する 反感 が 、 一 分 毎 に 強 さ を 増し て 来 た の で ある 。>, <Sentence: そこで 、 下人 は 、 両足 に 力 を 入れ て 、 いきなり 、 梯子 から 上 へ 飛び 上っ た 。>, <Sentence: だから お前 に 縄 を かけ て 、 どう しよ う と 云う よう な 事 は ない 。>, <Sentence: じゃが 、 ここ に いる 死人 ども は 、 皆 、 その くらい な 事 を 、 さ れ て も いい 人間 ばかり だ ぞ よ 。>, <Sentence: され ば 、 今 また 、 わし の し て いた事 も 悪い 事 と は 思わ ぬ ぞ よ 。>) ''' #Text-Rank TextRankSummarizer = TextRankSummarizer() summary = TextRankSummarizer(document=parser.document, sentences_count=5) print(summary) ''' (<Sentence: 下人 は 七 段 ある 石段 の 一番 上 の 段 に 、 洗いざらし た 紺 の 襖 の 尻 を 据え て 、 右 の 頬 に 出来 た 、 大きな 面皰 を 気 に し ながら 、 ぼんやり 、 雨 の ふる の を 眺め て い た 。>, <Sentence: そこで 、 下人 は 、 何 を おい て も 差 当り 明日 の 暮し を どうにか しよ う として ―― 云わ ば どう に も なら ない 事 を 、 どうにか しよ う として 、 とりとめ も ない 考え を たどり ながら 、 さっき から 朱雀 大路 に ふる 雨 の 音 を 、 聞く とも なく 聞い て い た の で ある 。>, <Sentence: 下人 は 、 手段 を 選ば ない という 事 を 肯定 し ながら も 、 この すれ ば の かた を つける ため に 、 当然 、 その後 に 来る 可 き 盗人 に なる より ほか に 仕方 が ない と 云う 事 を 、 積極 的 に 肯定 する だけ の 、 勇気 が 出 ず に い た の で ある 。>, <Sentence: 羅生門 の 楼 の 上 へ 出る 、 幅 の 広い 梯子 の 中段 に 、 一 人 の 男 が 、 猫 の よう に 身 を ちぢめ て 、 息 を 殺し ながら 、 上 の 容子 を 窺っ て い た 。>, <Sentence: すると 老婆 は 、 松 の 木片 を 、 床 板の間 に 挿し て 、 それから 、 今 まで 眺め て い た 死骸 の 首 に 両手 を かける と 、 丁度 、 猿 の 親 が 猿 の 子 の 虱 を とる よう に 、 その 長い 髪の毛 を 一 本 ずつ 抜き はじめ た 。>) ''' |
羅生門は、飢え死ぬか、盗人になるかの2択に悩んでいた下人が、死人の髪の毛を抜いている(≒盗んでいる)老婆の姿を目撃し、そこに憎悪を抱くものの、老婆の話を聞いて悪を肯定(盗人になる決意)をした、といった話の流れだったかと思います。
そういう風に小説をとらえるならば、どの要約文もその一部分しか抽出できていない感覚を受けますね。
というわけで、中身まったくわからない素人がとりあえず使ってみたまとめでした。
最後まで読んでいただきありがとうございました。