言語処理100本ノック 2015の挑戦記録です。環境はUbuntu 16.04 LTS + Python 3.5.2 :: Anaconda 4.1.1 (64-bit)です。過去のノックの一覧はこちらからどうぞ。
第9章: ベクトル空間法 (I)
enwiki-20150112-400-r10-105752.txt.bz2は,2015年1月12日時点の英語のWikipedia記事のうち,約400語以上で構成される記事の中から,ランダムに1/10サンプリングした105,752記事のテキストをbzip2形式で圧縮したものである.このテキストをコーパスとして,単語の意味を表すベクトル(分散表現)を学習したい.第9章の前半では,コーパスから作成した単語文脈共起行列に主成分分析を適用し,単語ベクトルを学習する過程を,いくつかの処理に分けて実装する.第9章の後半では,学習で得られた単語ベクトル(300次元)を用い,単語の類似度計算やアナロジー(類推)を行う.
なお,問題83を素直に実装すると,大量(約7GB)の主記憶が必要になる. メモリが不足する場合は,処理を工夫するか,1/100サンプリングのコーパスenwiki-20150112-400-r100-10576.txt.bz2を用いよ.
###83. 単語/文脈の頻度の計測
82の出力を利用し,以下の出現分布,および定数を求めよ.
- $ f(t,c) $: 単語$ t $と文脈語$ c $の共起回数
- $ f(t,∗) $: 単語$ t $の出現回数
- $ f(∗,c) $: 文脈語$ c $の出現回数
- $ N $:単語と文脈語のペアの総出現回数
####出来上がったコード:
# coding: utf-8
from collections import Counter
import pickle
fname_input = 'context.txt'
fname_counter_tc = 'counter_tc'
fname_counter_t = 'counter_t'
fname_counter_c = 'counter_c'
# Counter作成
counter_tc = Counter()
counter_t = Counter()
counter_c = Counter()
# 1行ずつ処理
work_tc = []
work_t = []
work_c = []
with open(fname_input, 'rt') as data_file:
for i, line in enumerate(data_file, start=1):
line = line.strip()
tokens = line.split('\t')
work_tc.append(line)
work_t.append(tokens[0])
work_c.append(tokens[1])
# 1,000,000行単位でCounterに追加
if i % 1000000 == 0:
counter_tc.update(work_tc)
counter_t.update(work_t)
counter_c.update(work_c)
work_tc = []
work_t = []
work_c = []
print('{} done.'.format(i))
# 最後の半端分を追加
counter_tc.update(work_tc)
counter_t.update(work_t)
counter_c.update(work_c)
# Counter書き出し
with open(fname_counter_tc, 'wb') as data_file:
pickle.dump(counter_tc, data_file)
with open(fname_counter_t, 'wb') as data_file:
pickle.dump(counter_t, data_file)
with open(fname_counter_c, 'wb') as data_file:
pickle.dump(counter_c, data_file)
print('N={}'.format(i))
####実行結果:
N=68031841
出現分布については、ファイル「dict_tc」、「dict_t」、「dict_c」に出力します。
処理時間は手元のマシンで7分ほどでした。
###頻度の計測
今回の問題は、次の問題で単語をベクトル化するために必要な出現頻度を集計します。単語$ t $をベクトル化するための準備として、問題82でその単語の周辺の単語を文脈語$ c $として抽出しましたが、そのカウント作業です。
式で書かれると難しそうになりますが、$ f(t,c) $は単語$ t $と文脈語$ c $のペアが何回出てくるか集計するだけなので、問題82で作ったファイルの行(=単語$ t $と文脈語$ c $がタブ区切りになっている)をそのまま集計しています。$ f(t,∗) $は単純に単語$ t $(=各行の1カラム目)の出現頻度、$ f(∗,c) $は単語$ c $(=各行の2カラム目)の出現頻度の集計です。
出現頻度の集計は問題36で使ったcollections.Counter
オブジェクトを使いました。
ただ、1単語ずつcollections.Counter.update()
しているとかなり遅かったので、100万単語単位でリストを作って追加するようにしています。
###オブジェクトの直列化・シリアライズ
この問題で算出した出現頻度は、次の問題で使うためファイルに保存しておく必要があります。今回は作ったCounterオブジェクトそのものをファイルに保存しました。このような作業を直列化とかシリアライズとか呼びます。
pythonではpickle
というモジュールがあり、pickle.dump()
を使うと直列化でき、pickle.load()
で戻せます。ピクルスのpickleですね。オブジェクトを漬物にする感じでしょうか。この辺のネーミングセンスは好きです^^
###メモリの使用量
第9章の問題文で「素直に実装すると,大量(約7GB)の主記憶が必要になる」とあり、1/10サンプリングではなく1/100サンプリングのコーパスを使っているのですが、それでも7GB前後のメモリを使ってしまっているようです。
メモリ使用量を減らすうまいアルゴリズムが思い浮かばないのですが、実装上の工夫というレベルであれば、メモリ上でCounterオブジェクトや辞書を作るのではなく、問題60で使ったLevelDBなどを使うのも1つの手だと思います。ただし、処理時間とはトレードオフになりそうですね。
84本目のノックは以上です。誤りなどありましたら、ご指摘いただけますと幸いです。
実行結果には、100本ノックで用いるコーパス・データで配布されているデータの一部が含まれます。この第9章で用いているデータのライセンスはクリエイティブ・コモンズ 表示-継承 3.0 非移植(日本語訳)です。