はじめに
前回の記事↓
単語ベクトルchiVeを使った類似度計算―RNNで文章生成〈6〉
第7回目の今回は前回、前々回と使用した日本語単語ベクトルchiVeを使用して、分かち書きしたコーパスを単語IDの配列へ変換していきます。
第4回ではコーパスに行う前処理として次の3つを挙げました。
1.小説のテキスト全体を一行の文にする
2.単語ごとに分割して分かち書きする
3.単語を分散表現に変換する
そのあとに、「今回の記事で1,2を行い、次回の記事で3を行います。」と書いているのですが、chiVeについていろいろやっていたので第7回の今回になってようやく前処理の続きを行うことになりました。項目の中では「3.単語を分散表現に変換する」となっていますが、これを「3.単語を単語IDに変換する」に変更して行っていきます。単語IDによって行列(Embeddingレイヤー)からその単語に対応する分散表現を抽出するので結局は同じことです。
単語から単語IDへ変換する辞書は第5回で作成した辞書型オブジェクトword2idのpickleをロードして使用します。上位5万単語しか使わないので、その分だけを読み込んだpickleファイルword2id_50k.pkl(とnpyファイルemb_layer_50k.npy) をすでに用意しています。
単語から単語IDへ
コードは第4回のpreprocessor.pyに少し変更を加えたものになっています。
import os, re, zipfile, pickle
import numpy as np
from pathlib import Path
from sudachipy import tokenizer, dictionary
zip_dir_path = '../path/to/zipfiles_dir/books_zip'
# 単語IDの辞書をロード
word2id = {}
pickle_path = 'save_data/word2id_50k.pkl'
if os.path.exists(pickle_path):
with open(pickle_path, 'rb') as f:
word2id = pickle.load(f)
# トークナイザの作成
tokenizer_obj = dictionary.Dictionary().create()
mode = tokenizer.Tokenizer.SplitMode.C
# 青空文庫のテキストの整形
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)
# かっこ等の記号を削除
normalized_text = re.sub(r'《.*?》|||[.*?]|〔|〕|#|※', '', text)
return normalized_text
# 単語分割処理
def sudachi_tokenizer(normalized_text):
morphemes = tokenizer_obj.tokenize(normalized_text, mode)
tokens = [m.surface() for m in morphemes]
for i, token in enumerate(tokens):
if token not in word2id:
tokens[i] = '<unk>'
return tokens
# 単語ID配列へ変換
def word_to_id(tokens):
corpus = []
for word in tokens:
if word == '<unk>':
corpus.append(len(word2id))
continue
corpus.append(word2id[word])
return corpus
# 単語IDをnpyで保存
def save_corpus(corpus, name):
npy_path = 'save_data/corpus/' + name + '.npy'
np.save(npy_path, np.array(corpus))
# 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)
tokens = sudachi_tokenizer(normalized_text)
corpus = word_to_id(tokens)
save_corpus(corpus, infos[0].filename.split('.')[0])
単語分割処理を行う関数sudachi_tokenizer()での変更点は以下の通りです。
1.分割後の単語の形を正規化したものm.normalized_form()からそのままの形m.surface()にする
2.word2idに登録されていない単語を<unk>に置き換える
3.tokensを空白で結合せずにそのまま返す
さらに、単語の配列であるtokensを単語IDの配列に変換する関数word_to_id()と、それをnpy形式で保存する関数save_corpus()を追加しています。
これを実行すると、小説のテキスト全体を単語IDの配列に変換するという前処理が済んだコーパスが完成します。この単語IDの配列が言語モデルの入力データになります。
未知語置き換え後のテキスト
未知語を<unk>に置き換えた後のテキストがどうなっているのかを、「自信の無さ」というテキストを例にとって見てみます。
tokens = sudachi_tokenizer(normalized_text)
print(' '.join(tokens))
>>
本紙 <unk> 朝日新聞 <unk> の <unk> で 、 <unk> 先生 が 、 私 の 下手 な 作品 を 例 に 挙げ て 、 現代 新人 の <unk> を
指摘 し て 居 られ まし た 。 他 の 新人 諸君 に 対し て 、 責任 を 感じ まし た の で 、 一言 <unk> を 致し ます 。 古来 一流 の 作家 の もの は
<unk> が 判然 し て い て 、 その 実感 が 強く 、 従っ て そこ に 或る 動かし 難い 自信 を 持っ て いる 。 その 反対 に 今 の 新人 は その 基本
<unk> に 自信 が なく 、 <unk> て いる 、 と いう お 言葉 は 、 まさに <unk> の 一 針 にて 、 的確 な もの と 思い まし た 。 自信
を 、 持ち たい と 思い ます 。 けれど も 私たち は 、 自信 を 持つ こと が 出来 ませ ん 。 どう し た の でしょう 。 私たち は 、 決して 怠け て など
居り ませ ん 。 <unk> の 生活 も し て 居り ませ ん 。 ひそか に 読書 も し て いる 筈 で あり ます 。 けれど も 、 努力 と 共 に 、 いよいよ
自信 が なくなり ます 。 私たち は 、 その 原因 を あれこれ と 指摘 し 、 罪 を 社会 に 転嫁 する よう な 事 も 致し ませ ん 。 私たち は 、 この 世紀
の 姿 を 、 この 世紀 の まま で 素直 に 肯定 し たい の で あり ます 。 みんな 卑屈 で あり ます 。 みんな <unk> で あり ます 。 みんな 「 臆病
な 苦労 」 を し て い ます 。 けれど も 、 私たち は 、 それ を 決定的 な 汚点 だ と は 、 ちっとも 思い ませ ん 。 いま は 、 大 <unk> だ と
思い ます 。 私たち は 、 当分 、 自信 の 無 さ から 、 <unk> 事 は 出来 ませ ん 。 誰 の 顔 を 見 て も 、 みんな 卑屈 です 。 私たち は 、 この
「 自信 の 無 さ 」 を 大事 に し たい と 思い ます 。 卑屈 の 克服 から で は 無し に 、 卑屈 の 素直 な 肯定 の 中 から 、 前例 の 無い 見事 な 花
の 咲く こと を 、 私 は 祈念 し て い ます 。
固有名詞や難しい単語は<unk>に置き換わっています。また、なぜか丸括弧も辞書には登録されていないようです。しかし、文の大半が<unk>に置き換わるというようなことはないようなので、語彙が極端に不足しているということはなさそうです。
ちなみに、単語IDに変換した結果がこちらです。
corpus = word_to_id(tokens)
print(corpus)
>>
[38193, 50000, 17737, 50000, 0, 50000, 7, 1, 50000, 346, 4, 1, 74, 0, 3739, 14, 300, 8, 936, 2, 2551, 3, 1, 2534,
4601, 0, 50000, 8, 1856, 10, 3, 1074, 111, 22, 6, 1130, 190, 0, 4601, 15719, 2, 404, 3, 1, 1334, 8, 106, 22, 6,
0, 7, 1, 1901, 50000, 8, 1025, 16, 1130, 19298, 8163, 0, 2737, 0, 52, 5, 50000, 4, 48641, 10, 3, 23, 3, 1, 56,
1961, 4, 1062, 1, 4990, 3, 340, 2, 24067, 4197, 5817, 1758, 8, 211, 3, 30, 1130, 56, 1770, 2, 137, 0, 4601, 5,
56, 934, 50000, 2, 1758, 4, 61, 1, 50000, 3, 30, 1, 9, 34, 33, 287, 5, 1, 1974, 50000, 0, 112, 4959, 507, 1, 7603,
14, 52, 9, 70, 22, 6, 1130, 1758, 8, 1, 804, 65, 9, 70, 16, 1130, 379, 11, 1924, 5, 1, 1758, 8, 921, 24, 4, 191,
48, 19, 1130, 110, 10, 6, 0, 84, 1130, 1924, 5, 1, 2191, 27060, 3, 71, 26471, 48, 19, 1130, 50000, 0, 362, 11, 10,
3, 26471, 48, 19, 1130, 15908, 2, 4287, 11, 10, 3, 30, 5638, 7, 47, 16, 1130, 379, 11, 1, 1540, 9, 985, 2, 1, 2279,
1758, 4, 4076, 16, 1130, 1924, 5, 1, 56, 856, 8, 4648, 9, 1856, 10, 1, 3625, 8, 701, 2, 23198, 28, 38, 14, 91, 11,
1025, 48, 19, 1130, 1924, 5, 1, 59, 2283, 0, 518, 8, 1, 59, 2283, 0, 252, 7, 2696, 2, 9629, 10, 65, 0, 7, 47, 16,
1130, 359, 30909, 7, 47, 16, 1130, 359, 50000, 7, 47, 16, 1130, 359, 25, 17705, 14, 2137, 18, 8, 10, 3, 23, 16,
1130, 379, 11, 1, 1924, 5, 1, 95, 8, 11776, 14, 49192, 20, 9, 5, 1, 9485, 70, 48, 19, 1130, 1379, 5, 1, 223, 50000,
20, 9, 70, 16, 1130, 1924, 5, 1, 7629, 1, 1758, 0, 1330, 32, 21, 1, 50000, 91, 5, 191, 48, 19, 1130, 315, 0, 292,
8, 64, 3, 11, 1, 359, 30909, 13, 1130, 1924, 5, 1, 59, 25, 1758, 0, 1330, 32, 18, 8, 861, 2, 10, 65, 9, 70, 16,
1130, 30909, 0, 5948, 21, 7, 5, 1807, 2, 1, 30909, 0, 2696, 14, 9629, 0, 76, 21, 1, 23047, 0, 258, 1847, 14, 439,
0, 4435, 24, 8, 1, 74, 5, 26337, 10, 3, 23, 16, 1130]
もはや解読不能です。。。
ところどころに入っている50000が<unk>に対応しています。
おわりに
やっと入力データが完成しました。次回からモデルの構築に入っていこうと思います。TensorFlowかPyTorchかで悩み中。。。
次回の記事↓
