言語処理100本ノック 2015の82本目「文脈の抽出」の記録です。
今回も後続のための前処理系で特に難しい処理をしておらず、技術的には解説すること少ないです。ただ、問題文が素人にはわかりずらく、理解に少し時間を要しました。
参考リンク
リンク | 備考 |
---|---|
082.文脈の抽出.ipynb | 回答プログラムのGitHubリンク |
素人の言語処理100本ノック:82 | 言語処理100本ノックで常にお世話になっています |
言語処理100本ノック 2015年版 (80~82) | 第9章で参考にしました |
環境
種類 | バージョン | 内容 |
---|---|---|
OS | Ubuntu18.04.01 LTS | 仮想で動かしています |
pyenv | 1.2.15 | 複数Python環境を使うことがあるのでpyenv使っています |
Python | 3.6.9 | pyenv上でpython3.6.9を使っています 3.7や3.8系を使っていないことに深い理由はありません パッケージはvenvを使って管理しています |
課題
第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を用いよ.
今回は*「1/100サンプリングのコーパスenwiki-20150112-400-r100-10576.txt.bz2」*を使っています。
82. 文脈の抽出
81で作成したコーパス中に出現するすべての単語tに関して,単語$ t $と文脈語$ c $のペアをタブ区切り形式ですべて書き出せ.ただし,文脈語の定義は次の通りとする.
- ある単語$ t $の前後$ d $単語を文脈語$ c $として抽出する(ただし,文脈語に単語tそのものは含まない)
- 単語$ t $を選ぶ度に,文脈幅$ d $は{1,2,3,4,5}の範囲でランダムに決める.
課題補足
「文脈語」とは
対象とする単語を**「対象語」(Target word)、対象語の前後を「文脈語」(Context word)と呼びます。対象語から何単語までを文脈語とするかを「文脈幅」(Context Window SizeまたはWindow Size)**と呼びます。
課題の元ファイルにある下記の例文で説明します。
No surface details of Adrastea are known due to the low resolution of available images
例えば上記でAdrasteaを対象語とすると、前後にある"details", "of", "are", "known"が文脈幅2の文脈語です。
では、仮に文脈幅を2として上記文に対して今回の課題を実行する場合、今回は下記のようなファイルを作成します。
※ *「単語$ t $を選ぶ度に,文脈幅$ d $は{1,2,3,4,5}の範囲でランダムに決める」*部分は無視して解説
1列名 | 2列目 |
---|---|
No | surface |
No | details |
surface | No |
surface | details |
surface | of |
details | No |
details | surface |
details | of |
details | Adrastea |
回答
回答プログラム 082.文脈の抽出.ipynb
20行程度の短いプログラムですが、データ量が多いので処理に10分程度かかります。
また、作成されるファイルは800MB程度のサイズとなり大きいので注意が必要です。
ちなみに9割以上が記事「素人の言語処理100本ノック:82」のコピペです。
import random
with open('./081.corpus.txt') as file_in, \
open('./082.context.txt', mode='w') as file_out:
for i, line in enumerate(file_in):
tokens = line.strip.split(' ')
for j in range(len(tokens)):
d = random.randint(1, 5) # 文脈幅d
# 前後d語以内の語の列挙
for k in range(max(j - d, 0), min(j + d + 1, len(tokens))):
# 自分自身の場合は出力しない
if j != k:
file_out.writelines(tokens[j]+'\t'+tokens[k]+'\n')
if i < 4:
print(len(tokens), tokens)
else:
print('\r Processding line: {0}'.format(i), end='')
回答解説
下記コードがメインの部分です。
対象語の場所j
から文脈幅d
を増減させた数のループです。ただ単純に増減させると最初の方の単語は負の数になったり、最後の方の単語は合計単語数を超過してしまったりとするのでmax
とmin
関数を使って幅の調整をしています。
# 前後d語以内の語の列挙
for k in range(max(j - d, 0), min(j + d + 1, len(tokens))):
# 自分自身の場合は出力しない
if j != k:
file_out.writelines(tokens[j]+'\t'+tokens[k]+'\n')
4行目まではコンソールに対象語の数と処理対象文を、それ以降は何行目を処理しているかを出力しています。
if i < 4:
print(len(tokens), tokens)
else:
print('\r Processding line: {0}'.format(i), end='')
こぼれ話(トークン化失敗)
文のトークン化に関する失敗談です。
最初はあまり考えずに下記のようにsplit
関数を使っていました。
tokens = line.split()
しかし、結果がこんなになってしまったものもあり、後続でPandas使ったときにエラーで気づきました。
"b")("s" "c
− "b")("s"
− "c
本当はこうなるべきでした。これは一見、スペース区切りになっているように見えてスペースっぽい部分には\xa0
が使われています。前々回記事の「\xa0について」で少し触れています。
known k" = √("s"("s" − "a")("s" − "b")("s" − "c
で、正しくすべくstrip
関数を使ってスペースだけで区切っています。
tokens = line.strip.split(' ')