はじめに
前回の記事↓
Webスクレイピングによるコーパス収集―RNNで文章生成〈3〉
第4回目の今回は収集したコーパスに前処理を行っていきます。
前処理
小説のテキストはそのまま学習モデルに入力することはできないので、事前に何らかの処理を施しておく必要があります。今回の文章生成モデル作成においては以下のような前処理を行うことにしました。
1.小説のテキスト全体を一行の文にする
2.単語ごとに分割して分かち書きする
3.単語を分散表現に変換する
今回の記事で1,2を行い、次回の記事で3を行う予定です。
zipファイルからテキストを読み込む
小説のテキストデータはzipファイルでダウンロードしているので、わざわざ解凍せずに中身のテキストデータを読み込んでいきます。zipfile
というライブラリとpathlib
ライブラリの中のPath
クラスを使用します。
import zipfile
from pathlib import Path
zip_dir_path = '../path/to/zipfiles_dir/books_zip'
# zipファイルからテキストを読み込み
for zip_path in Path(zip_dir_path).glob('*.zip'):
with zipfile.ZipFile(zip_path, 'r') as zip_file:
infos = zip_file.infolist()
# zipファイルに中身が存在し、テキストファイルだった場合のみ処理を続ける
if not infos or not infos[0] or not infos[0].filename.endswith('.txt'):
continue
raw_text = zip_file.read(infos[0]).decode('shift_jis')
zip_dir_path
はzipファイルが保存されているディレクトリのパスです。まず、Path(zip_dir_path).glob('*.zip')
ですべてのzipファイルのパスを取得し、それらに対して繰り返し処理を行います。with zipfile.ZipFile(zip_path, 'r') as zip_file
でzipファイルのZipfileオブジェクトをzip_file
として生成します。Zipfileオブジェクトを使うと、infolist()
メソッドによってzipファイルの中身のファイルのリストを取得できます。青空文庫から取得したzipファイルは基本的に小説のテキストファイルのみを中身に含んでいるので、infos[0]
が読み込み対象のテキストファイルを指します。ファイルの読み込みはzip_file.read(infos[0])
で行いますが、この場合バイナリ形式で読み込むのでテキスト形式にデコードする必要があります。青空文庫のテキストデータはエンコード方式にShift_JISを使用しているので、decode('shift_jis')
メソッドを追加します。
こうして、小説テキストの生のデータであるraw_text
が完成しました。例として「自信の無さ」というテキストのraw_text
を示します。
自信の無さ
太宰治
-------------------------------------------------------
【テキスト中に現れる記号について】
《》:ルビ
(例)下手《へた》
[#]:入力者注 主に外字の説明や、傍点の位置の指定
(例)前例の無い[#「前例の無い」に傍点]
-------------------------------------------------------
本紙(朝日新聞)の文芸時評で、長与先生が、私の下手《へた》な作品を例に挙げて、現代新人の通性を指摘して居られました。
...(中略)...
卑屈の克服からでは無しに、卑屈の素直な肯定の中から、前例の無い[#「前例の無い」に傍点]見事な花の咲くことを、私は祈念しています。
底本:「太宰治全集10」ちくま文庫、筑摩書房
1989(平成元)年6月27日第1刷発行
1998(平成10)年6月15日第4刷発行
...(中略)...
青空文庫作成ファイル:
このファイルは、インターネットの図書館、青空文庫(http://www.aozora.gr.jp/)で作られました。入力、校正、制作にあたったのは、ボランティアの皆さんです。
テキストを整形する
raw_text
はルビや注釈、説明文などの入力データには必要のない部分が多く含まれているので、それらを削除することが必要です。加えて、段落の空白や改行をなくして一つの長い文章にまとめる処理を行います。これらの整形処理をまとめて一つのメソッドにします。
import re
# 青空文庫のテキストの整形
def aozora_normalizer(raw_text):
# タイトル・著者名を削除
text = re.sub(r'\A.*?\n.*?\n', '', raw_text)
# 空白・改行を削除
text = re.sub(r' | |\n|\r', '', text)
# ルビ説明文を削除
text = re.sub(r'-{55}.*?-{55}', '', text)
# 末尾の文を削除
text = re.sub(r'底本:.*\Z', '', text)
# かっこ等の記号を削除
text = re.sub(r'《.*?》|||[.*?]|〔|〕|#|※', '', text)
return text
まず正規表現を使用するためにreライブラリをインポートしておきます。re.sub()
は正規表現によって文字を置換するためのメソッドで、第一引数に置換対象の文字、第二引数に置換先の文字、第三引数に置換を適用するテキストをとります。文字を削除する場合は第二引数を空の文字列とします。第一引数には正規表現を使うのでPythonのraw文字列という、エスケープシーケンスを無視する文字列を使用しています。そのため、文字列の前にrがついています。正規表現の記述には以下のサイトを参考にしました。
とても詳しく解説されているのでわからなかったらこっちのサイトを見てください。ただ、僕はあまり正規表現には慣れていないので上のコードの記述はあまりスマートではないかも。。。特に気を付けたいのが.*
と.*?
の違いです。前者が最長一致、後者が最短一致という違いがあるのですが、それを知らずにr'《.*》'
と記述していたら文章の大半が削除されてしまうという事態が起こりました。
先ほどの「自信の無さ」のraw_text
をこのメソッドに通した場合の結果がこちらです。ちゃんと余計な部分が削除されて本文のみが一行になっています。
本紙(朝日新聞)の文芸時評で、長与先生が、私の下手な作品を例に挙げて、現代新人の通性を指摘して居られました。...(中略)...卑屈の克服からでは無しに、卑屈の素直な肯定の中から、前例の無い見事な花の咲くことを、私は祈念しています。
形態素解析器Sudachi
第1回で説明した通り、分かち書きに使う形態素解析器はMeCabを使うつもりだったのですが、色々調べたところSudachiという比較的新しく、機能が豊富なものが見つかったのでそちらを使おうと思います。ごめんな、MeCab。
Sudachiの使用にあたってこれらのサイトを参考にしました。
WorksApplications/SudachiPy(GitHub)
Pythonで形態素解析器Sudachiを使う (SudachiPy)
Sudachiの機能の中で魅力的だと思ったのが、分割単位の設定と単語の正規化です。
分割単位にはA,B,Cの3つのモードがあり、それぞれ分割の長さが違います。たとえば「国家公務員」という単語を分割すると、それぞれのモードで結果がこのようになります。
A: 国家 公務 員
B: 国家 公務員
C: 国家公務員
細かく分割すると単語の意味が通らなくなるのでこれからはCモードを主に使って行きます。
単語の正規化とは単語の表記の揺れを一つにまとめる事です。例えば「焼き肉」には「焼肉」という表記もありますが、正規化をするとどちらも「焼き肉」に一本化されます。この機能も積極的に使っていきます。
単語ごとに分割し分かち書き
SudachiをPythonで使うためにPython用のライブラリであるsudachipy
をインストールしました。分かち書きを行うコードはこちらになります。
from sudachipy import tokenizer
from sudachipy import dictionary
# トークナイザの作成
tokenizer_obj = dictionary.Dictionary().create()
mode = tokenizer.Tokenizer.SplitMode.C
# 単語分割処理
morphemes = tokenizer_obj.tokenize(normalized_text, mode)
tokens = [m.normalized_form() for m in morphemes]
tokenized_text = ' '.join(tokens)
初めにトークナイザ(文字分割器?)を生成して、tokenize()
メソッドでnormalized_text
を形態素解析しています(分割モードはC)。このメソッドの返り値はMorpheme
オブジェクトのリストなので、さらにリストのすべての要素にnormalized_form()
を適用して、正規化された単語のリストを生成します。Morpheme
オブジェクトには他にも、そのままの単語を返すsurface()
や活用をなくして(正規化せずに)返すdictionary_form()
などのメソッドがあります。最後に' '.join()
を使って、リストの中身を空白で結合すれば分かち書きされたtokenized_text
が完成します。
「自信の無さ」の分かち書きはこうなります。正規化するか、活用をなくすかはこの後に使用する学習済みword2vecの仕様によって変更があるかもしれません。
本紙 ( 朝日新聞 ) の 文芸 時評 で 、 長与 先生 が 、 私 の 下手 だ 作品 を 例 に 上げる て 、 現代 新人 の 通性 を 指摘 為る て 居る られる ます た 。...(中略)... 卑屈 の 克服 から だ は 無し に 、 卑屈 の 素直 だ 肯定 の 中 から 、 前例 の 無い 見事 だ 花 の 咲く こと を 、 私 は 祈念 為る て 居る ます 。
ソースコードのまとめ
ここまでに書いたコードをまとめるとこうなります。
import re
import zipfile
from pathlib import Path
from sudachipy import tokenizer
from sudachipy import dictionary
zip_dir_path = '../path/to/zipfiles_dir/books_zip'
# トークナイザの作成
tokenizer_obj = dictionary.Dictionary().create()
mode = tokenizer.Tokenizer.SplitMode.C
# 青空文庫のテキストの整形
def aozora_normalizer(raw_text):
... # 省略
# zipファイルからテキストを読み込み
for zip_path in Path(zip_dir_path).glob('*.zip'):
with zipfile.ZipFile(zip_path, 'r') as zip_file:
infos = zip_file.infolist()
if not infos or not infos[0] or not infos[0].filename.endswith('.txt'):
continue
raw_text = zip_file.read(infos[0]).decode('shift_jis')
# テキストを整形
normalized_text = aozora_normalizer(raw_text)
# 単語分割処理
morphemes = tokenizer_obj.tokenize(normalized_text, mode)
tokens = [m.normalized_form() for m in morphemes]
tokenized_text = ' '.join(tokens)
おわりに
次回はword2vecによる単語の分散表現への変換をやっていきます。都合のいい学習済みword2vecが見つかるかどうかが現在の懸念です。。。
次回の記事↓