青空文庫の形態素解析データを作りたい
例えば『お好み焼きの戦前史 第二版』においても過去文献類をスキャンしたり、ネット上の文献をダウンロードすることで作成した電子テキストデータ群を解析することで料理方法の初出や普及の過程を明らかにしており、電子テキストデータ群の準備こそが重要な研究材料となっている。
上記においてEPUB電子書籍データの日本語形態素解析を行ったが、青空文庫にある電子書籍の解析も行いたい。
青空文庫はテキストファイルに独自の注釈記法を追加して書かれているが、XHTML版のファイルも用意されているため、こちらの解析を行っていく。
青空文庫からXHTMLファイルを取得して解析
- src/aozora.py
(前略) def request(self, url): res = requests.get(url) res.raise_for_status() res.encoding = 'shift_jis' return res.text.encode('shift_jis') def read(self, path): with open(path, 'r', encoding='shift_jis') as f: return f.read().encode('shift_jis') def extract(self, text): cleaner = lxml.html.clean.Cleaner( page_structure=False, remove_tags=['ruby', 'br'], kill_tags=['rt', 'rp'] ) html = cleaner.clean_html(text).decode('utf-8') html = lxml.html.fromstring(html) return html.find_class('main_text')[0].text_content() (後略)
インターネットまたはローカルファイルからShift JIS 形式で取得した青空文庫の XHMTL ファイルに lxml.html を利用して不要なタグを除去。CSS の main_text class 以下を utf-8 に変換して取得。
テキスト抽出後は上記の環境の mecab で品詞を取り出して単語出現数を計測。
tf-idf で単語の重要度を計測
計測した情報を元に文書内における重要単語を抽出したい。重要単語のスコアリングを行うために一般的に使われる手法として tf-idf が挙げられる。
tf-idfは、文書中に含まれる単語の重要度を評価する手法の1つであり、主に情報検索やトピック分析などの分野で用いられている。
tf-idfは、tf(英: Term Frequency、単語の出現頻度)とidf(英: Inverse Document Frequency、逆文書頻度)の二つの指標に基づいて計算される。
tf は 特定単語の出現数 / 全体の単語数
を計算したもので idf は log( 解析対象文書数 / 特定単語を含む文書数 )
で計算できる。logを使うのは文書数が大きくなった場合の影響を下げるため。tf-idf をこれらを掛け合わせたもの。要するに言えば、「特定の文書内だけに頻出する単語はその文章を特徴づける単語である」と推定できるわけだ。
tf-idf 計量データから文芸作品連想クイズを生成する
tf は単純に特定の文書の全単語出現数で割れば出せるが、 idf を効率的に出すためにはどのようにすれば良いのか。自分としては以下のプログラムを作成した。
- src/aozora_tf_idf.py
(前略) def docs_freq(self, docs_path): freq_list = [] for path in docs_path: freq = self.freq(path) freq['doc_id'] = path freq_list.append(freq) # 単語・文書ID(path)でまとめる docs_freq = pandas.concat(freq_list) docs_freq = docs_freq.groupby(['term', 'doc_id']).sum() docs_freq = docs_freq.reset_index() return docs_freq def main(self, args): self.logger.info(f'{__file__} {__version__} {args}') # TF作成 target = self.freq(args.path) target = self.aozora.term_freq(target) # 全文書データ作成 docs_path = glob.glob('../work/*.html') docs_freq = self.docs_freq(docs_path) # 単語出現文書数 target['doc_freq'] = target['term'].map( lambda t: (docs_freq['term'] == t).sum() ) # IDF doc_num = len(docs_path) target['idf'] = target['doc_freq'].map( lambda df: math.log(doc_num / df) ) # tf-idf target['tf-idf'] = target['term_freq'] * target['idf'] # 出力用整形 out = target.sort_values('tf-idf', ascending=True).tail(20) for c in ['tf-idf', 'term_freq', 'idf']: out[c] = out[c].round(3) (後略)
まずは glob を使って特定のパスの配下にある html リストを作成。今回は江戸川乱歩の短編作品20篇ほどを解析対象文書群とした。それらの文書群を 単語, 文書PATH
でグルーピング。特定文書内に出現する単語で検索できた行数を Document Frequency として逆数計算の分母とする。
なお、解析対象文書を読み込む度に pandas.Dataframe の append() を利用して行を結合していくと処理がかなり遅くなってしまうので、 list に格納してから pandas.concat() で一気に UNION ALL 相当の動作をさせてからグルーピングしている。
最終的に計算した tf と idf の積をとってスコアを出す。今回はスコアの上位20位を取得して、カウントダウン形式で出力する。
文芸作品連想クイズの結果
term info1 info2 freq term_freq doc_freq idf tf-idf 兄さん 名詞 一般 6 0.002 4 1.749 0.003 娘 名詞 固有名詞 19 0.006 13 0.571 0.003 額 名詞 一般 12 0.004 9 0.938 0.004 汽車 名詞 一般 12 0.004 9 0.938 0.004 私達 名詞 固有名詞 9 0.003 6 1.344 0.004 古風 名詞 形容動詞語幹 8 0.003 5 1.526 0.004 双眼鏡 名詞 一般 6 0.002 3 2.037 0.004 馬車鉄道 名詞 固有名詞 4 0.001 1 3.135 0.004 覗き 名詞 一般 14 0.005 7 1.19 0.005 蜃気楼 名詞 一般 7 0.002 2 2.442 0.006 魚津 名詞 固有名詞 6 0.002 1 3.135 0.006 白髪 名詞 一般 7 0.002 1 3.135 0.007 さかさ 名詞 一般 9 0.003 2 2.442 0.007 老人 名詞 一般 37 0.012 11 0.738 0.009 眼鏡 名詞 一般 15 0.005 3 2.037 0.01 様 名詞 非自立 91 0.029 15 0.427 0.013 私 名詞 代名詞 118 0.038 16 0.363 0.014 遠眼鏡 名詞 一般 17 0.005 1 3.135 0.017 押絵 名詞 一般 29 0.009 1 3.135 0.029 兄 名詞 一般 78 0.025 5 1.526 0.038
今回の連想クイズを読んでいくと、自分の場合は「額」「汽車」「双眼鏡」あたりでピンときて「蜃気楼」で確定したところ。一回でも当該作品を読んだ事があれば『押絵と旅する男』であることが分かるだろう。最大ヒントの「押絵」まであるし。
tf-idf の弱点として「私」や「兄」などの一度出てくると頻出するが、どの作品に出るわけでもない単語が強くなりすぎる事が挙げられる。「僕」や「姉」しか出てこない作品もあるので idf がそこまで悪くならないのだ。この辺りはヒューステリックな一般語フィルタも必要になってくるかもしれない。
遠眼鏡 押絵覗き見 蜃気楼
これを利用することで上記のように作品をミニマルに表現する俳句を詠んでみたり、自身が好む作品にのみ頻出しがちな単語を定量的に知ったりができるので、なかなか楽しい。ちなみに『D坂の殺人事件』であっても、「アイスクリーム」「生傷」「蕎麦屋」などの印象的な単語がちゃんと選ばれた。