はじめに
ふと思い立って勉強を始めた「ゼロから作るDeep Learning❷ーー自然言語処理編」の2章で私がつまずいたことのメモです。
実行環境はmacOS Catalina + Anaconda 2019.10、Pythonのバージョンは3.7.4です。詳細はこのメモの1章をご参照ください。
(このメモの他の章へ:1章 / 2章 / 3章 / 4章 / 5章 / 6章 / 7章 / 8章 / まとめ)
この記事は個人で作成したものであり、内容や意見は所属企業・部門見解を代表するものではありません。
2章 自然言語処理と単語の分散表現
この章から自然言語処理の話が始まります。
2.1 自然言語処理
自然言語処理とは「私たちの言葉をコンピュータに理解させるための技術(分野)」とありますが、「コンピュータに理解させる」と聞くとイメージが膨らんでしまってドラえもんみたいなものを想像してしまうので、私は「コンピュータで処理可能にする」くらいの表現が良いのではないかと思います。
数値データであれば、合計したり、平均取って比べたり、グラフで可視化したり、時系列データで先を予測したり、etc.と簡単に処理できます。前巻で学んだディープラーニングも活用できます。でも、自然言語のデータではそうはいきません。それを可能にするための技術ということです。
あと、NLPという略語は神経言語プログラミング(Neuro-Linguistic Programming)と同じで、「NLP」でググると神経言語プログラミングの話が最初にでてきます。分野が違うので混乱することはないかとは思いますが、ディープラーニングの勉強中に出てくると一瞬関係するかも?と思ってしまうかも知れませんのでご注意ください。
2.2 シソーラス
本では英語の話しか出てこないので、日本語のシソーラスについてメモしておきます。
- WordNetには日本語版の日本語WordNetがあります。ただし、本の「付録B WordNet を動かす」のようにNLTKで使えるのかは未確認です。
- プログラムから利用可能なデータが公開されている訳ではないのですが、国立研究開発法人 科学技術振興機構(JST)が構築しているシソーラスも有名のようです。こちらはJSTシソーラスmapという用語検索サイトが用意されていて、用語を検索すると本の図2-2のようなグラフが表示されます。シソーラスだけでなく文献における共起頻度の統計情報なども使われていて、例えば「自動車」などで検索すると、スクロールしないと見渡せない規模の壮大なグラフが表示されます。ダブルクリックで用語をたどっていけます。
2.3 カウントベースの手法
カウントベースの手法は、3年ほど前に言語処理100本ノック2015で勉強していたので、そのおさらいをする形になりました。この100本ノックの第9章: ベクトル空間法 (I)が、この本の「2.3 カウントベースの手法」と「2.4 カウントベースの手法の改善」に相当するので、この後の特異値分解以外では特につまずく部分はなかったです。
2.4 カウントベースの手法の改善
「2.4.2 次元削減」の図2-8が、やや分かりにくいかもしれません。この図でイメージが湧かなかった方は、@aya_takaさんの30分でわかる機械学習用語「次元削減(Dimensionality Reduction)」の冒頭にある身長と体重の例が分かりやすくて良いかと思います。
特異値分解(SVD)についてはつまずきました。実はもう3回目の勉強になるのですが(4年くらい前にやったCourseraのオンライン講座のMachine Learningと前述の言語処理100本ノック2015)、イメージは理解できるものの計算の中身までは相変わらず良くわかりません。ちょっとググったくらいでは説明の意味がわからず、行列の勉強をきっちりやり直さないとダメそうです。NumPy(および次に出てくるscikit-learn)が計算してくれるということで、今回もそれに感謝しつつ先に進むことにしました
「2.4.4 PTBデータセット」では大きなコーパスとして英語のPTBコーパスを使うのですが、日本語大好きな私は日本語で試してみることにしました。日本語は英語と違って単語の境界に空白がないので、空白を入れる分かち書きの処理が必要ですが、今回はそれが済んでいる青空文庫の分かち書き済みテキストを使います。
本ではPTBコーパスを dataset/ptb.py
で利用する形になっていますが、これを改造してdataset/aozorabunko.py
を作りました。以下、そのソースコードですが、その前にいくつか注意点です。
- 対象データは3名の著者の作品を適当に13作品選んで連結しただけなので、かなりの偏りがあります。手法のベンチマークに使えるような代物ではなく、あくまでも「やってみた」なのでご了承ください。
-
ptb.load_data()
では引数で'train'
、'test'
、'valid'
が選べるのですが、まだ今回使う'train'
しか対応していません。同じ著者で今回使っていない作品を、そのうち持ってこようかと考えています。 - PTBコーパスの
len(corpus)
は929,589でしたが、今回の青空文庫のデータでは873,028でした。やや少ないです。 -
前巻を勉強した時のメモではソースコードをほとんど一から書いていたのですが、本のコードをベースにされている方にとっては参考にしにくいため、今回は本のコードを最大限に流用する形に変えました。主な改造部分に
★
を入れています。(Visual Studio Codeに入れてるLinterのflake8が赤波線だらけにしてしまうのがやや辛いですが)
# coding: utf-8
import sys
import os
sys.path.append('..')
try:
import urllib.request
except ImportError:
raise ImportError('Use Python3!')
import pickle
import numpy as np
# ★このURLはGitHubにアップしてある分かち書き済みの青空文庫作品詰め合わせのダウンロードURL
# 詳細は https://github.com/segavvy/wakatigaki-aozorabunko を参照してください。
url_base = 'https://github.com/segavvy/wakatigaki-aozorabunko/raw/master/'
key_file = {
'train': '20200516merge.txt',
'test': '', # ★まだ使わないので準備していない
'valid': '' # ★まだ使わないので準備していない
}
save_file = {
'train': 'aozorabunko.train.npy',
'test': 'aozorabunko.test.npy',
'valid': 'aozorabunko.valid.npy'
}
vocab_file = 'aozorabunko.vocab.pkl'
dataset_dir = os.path.dirname(os.path.abspath(__file__))
def _download(file_name):
file_path = dataset_dir + '/' + file_name
if os.path.exists(file_path):
return
print('Downloading ' + file_name + ' ... ')
try:
urllib.request.urlretrieve(url_base + file_name, file_path)
except urllib.error.URLError:
import ssl
ssl._create_default_https_context = ssl._create_unverified_context
urllib.request.urlretrieve(url_base + file_name, file_path)
print('Done')
# ★テキストの分割が2箇所で使われるため関数化。実装は超その場しのぎ……
def _split_data(text):
return text.replace('\n', '<eos> ').replace('。', '<eos> ').strip().split()
def load_vocab():
vocab_path = dataset_dir + '/' + vocab_file
if os.path.exists(vocab_path):
with open(vocab_path, 'rb') as f:
word_to_id, id_to_word = pickle.load(f)
return word_to_id, id_to_word
word_to_id = {}
id_to_word = {}
data_type = 'train'
file_name = key_file[data_type]
file_path = dataset_dir + '/' + file_name
_download(file_name)
words = _split_data(open(file_path).read())
for i, word in enumerate(words):
if word not in word_to_id:
tmp_id = len(word_to_id)
word_to_id[word] = tmp_id
id_to_word[tmp_id] = word
with open(vocab_path, 'wb') as f:
pickle.dump((word_to_id, id_to_word), f)
return word_to_id, id_to_word
def load_data(data_type='train'):
'''
:param data_type: データの種類:'train' or 'test' or 'valid (val)'
:return:
'''
if data_type == 'val': data_type = 'valid'
save_path = dataset_dir + '/' + save_file[data_type]
word_to_id, id_to_word = load_vocab()
if os.path.exists(save_path):
corpus = np.load(save_path)
return corpus, word_to_id, id_to_word
file_name = key_file[data_type]
file_path = dataset_dir + '/' + file_name
_download(file_name)
words = _split_data(open(file_path).read())
corpus = np.array([word_to_id[w] for w in words])
np.save(save_path, corpus)
return corpus, word_to_id, id_to_word
if __name__ == '__main__':
for data_type in ('train', 'val', 'test'):
load_data(data_type)
このファイルをdataset
ディレクトリの中に置いて、ptb.py
の代わりにaozorabunko.py
をimportして、ptb.load_data()
の代わりにaozorabunko.load_data()
すれば、PTBコーパスの代わりに青空文庫のデータが使えます。
なお、「2.4.5 PTBデータセットでの評価」の説明で「sklearnモジュールをインストールする必要があります」とありますが、このsklearnはscikit-learnというPythonの機械学習ライブラリのことで、Anacondaに同梱されています。そのため、前巻の1章の手順でAnacondaをインストールされている場合は、特に何もせずに利用できます。
あと、PPMIの計算にもかなり時間がかかります。私の環境では数時間かかってしまうので、一度計算したらファイルにキャッシュするように変えました。また、クエリをいろいろ試してみたくなったため、標準入力できるようにしています。
以下、改造したch02/count_method_big.py
です。主な改造部分に★
を入れています。
# coding: utf-8
import sys
sys.path.append('..')
import numpy as np
from common.util import most_similar, create_co_matrix, ppmi
from dataset import aozorabunko # ★青空文庫のコーパスを利用するように変更
import os # ★PPMIの計算結果をキャッシュするため追加
import pickle # ★PPMIの計算結果をキャッシュするため追加
window_size = 2
wordvec_size = 100
corpus, word_to_id, id_to_word = aozorabunko.load_data('train') # ★コーパス変更
vocab_size = len(word_to_id)
print('counting co-occurrence ...')
C = create_co_matrix(corpus, vocab_size, window_size)
# ★PPMIの計算は時間がかかるので前回結果をキャッシュし、Cが同じなら再利用するように変更
print('calculating PPMI ...')
W = None
ppmi_path = os.path.dirname(os.path.abspath(__file__)) + '/' + 'ppmi.pkl'
if os.path.exists(ppmi_path):
# ★キャッシュの読み込み
with open(ppmi_path, 'rb') as f:
cache_C, cache_W = pickle.load(f)
if np.array_equal(cache_C, C):
W = cache_W # Cの中身が同じなので再利用
if W is None:
W = ppmi(C, verbose=True)
with open(ppmi_path, 'wb') as f:
pickle.dump((C, W), f) # キャッシュとして保存
print('calculating SVD ...')
try:
# truncated SVD (fast!)
from sklearn.utils.extmath import randomized_svd
U, S, V = randomized_svd(W, n_components=wordvec_size, n_iter=5,
random_state=None)
except ImportError:
# SVD (slow)
U, S, V = np.linalg.svd(W)
word_vecs = U[:, :wordvec_size]
# ★クエリを標準入力する形に変更
while True:
query = input('\nquery? ')
if not query:
break
most_similar(query, word_to_id, id_to_word, word_vecs, top=5)
以下、クエリーをいくつか試してみた結果です。まずは、本のクエリーの日本語訳から。
[query] あなた
奥さん: 0.6728986501693726
妻: 0.6299399137496948
K: 0.6205178499221802
父: 0.5986840128898621
私: 0.5941839814186096
[query] 年
反: 0.8162745237350464
百: 0.8051895499229431
分: 0.7906433939933777
八: 0.7857747077941895
円: 0.7682645320892334
[query] 車
ドア: 0.6294019222259521
ドアー: 0.6016885638237
自動車: 0.5859153270721436
門: 0.5726617574691772
カーテン: 0.5608214139938354
トヨタ is not found
「あなた」はいい感じですね。「年」は単位としての類義語が得られている感じです。「車」はコーパスに使っている作品にほとんど出てこないからかイマイチです。「トヨタ」はそもそも存在しない時代なので仕方がありません。
他にもいくつか試してみたものを載せておきます。前半は比較的上手くいっているもので、後半は良くわらかないものです。
[query] 朝
晩: 0.7267987132072449
ごろ: 0.660172164440155
昼: 0.6085118055343628
夕方: 0.6021789908409119
翌: 0.6002975106239319
[query] 学校
東京: 0.6504884958267212
高等: 0.6290650367736816
中学校: 0.5801640748977661
大学: 0.5742003917694092
下宿: 0.5358142852783203
[query] 座敷
書斎: 0.6603355407714844
椽側: 0.6362787485122681
室: 0.6142982244491577
部屋: 0.6024710536003113
台所: 0.6014574766159058
[query] 着物
髯: 0.5216895937919617
黒: 0.5200990438461304
服: 0.5096032619476318
洋服: 0.48781922459602356
帽子: 0.4869200587272644
[query] 吾輩
主人: 0.6372452974319458
余: 0.5826579332351685
金田: 0.4684762954711914
彼等: 0.4676626920700073
迷亭: 0.4615904688835144
[query] 犯人
怪人: 0.6609077453613281
賊: 0.6374931931495667
団員: 0.6308270692825317
あいつ: 0.6046633720397949
潜航: 0.5931873917579651
[query] 注文
話: 0.6200630068778992
相談: 0.5290789604187012
多忙: 0.5178924202919006
親切: 0.5033778548240662
講釈: 0.4894390106201172
[query] 無鉄砲
陳腐: 0.7266454696655273
古風: 0.6771457195281982
鋸: 0.6735808849334717
鼻息: 0.6516652703285217
無知: 0.650424063205719
[query] 猫
南無: 0.6659030318260193
信女: 0.5759447813034058
墨: 0.5374482870101929
身分: 0.5352671146392822
普通: 0.5205280780792236
[query] 酒
書物: 0.5834404230117798
茶: 0.469807893037796
休ん: 0.4605821967124939
食う: 0.44864168763160706
棒: 0.4349029064178467
[query] 料理
かせぎ: 0.5380040407180786
落款: 0.5214874744415283
原: 0.5175281763076782
法: 0.5082278847694397
屋: 0.5001937747001648
ちなみに対象データの著者は、夏目漱石、宮沢賢治、江戸川乱歩です。コーパスがちょっと偏りすぎですが、それはそれで面白いので、よろしければいろいろ試してみてください。
2.5 まとめ
おさらいが多かったので比較的スムーズに読めました。次章からが本番になりそうです。
この章は以上です。誤りなどありましたら、ご指摘いただけますとうれしいです。