北野坂備忘録

主にインストールやプログラミングのメモを載せています。

言語処理100本ノック 2015年版 (80~82)

第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を用いよ.

申し訳ございませんが、メモリが不安なので最初っからr100を使用して進めていきます。
というか、壮絶に失敗しています。

80. コーパスの整形

文を単語列に変換する最も単純な方法は,空白文字で単語に区切ることである. ただ,この方法では文末のピリオドや括弧などの記号が単語に含まれてしまう. そこで,コーパスの各行のテキストを空白文字でトークンのリストに分割した後,各トークンに以下の処理を施し,単語から記号を除去せよ.

トークンの先頭と末尾に出現する次の文字を削除: .,!?;:()[]'"
空文字列となったトークンは削除

以上の処理を適用した後,トークンをスペースで連結してファイルに保存せよ.

これだけ単純な処理でも量が多いので結構な時間がかかります。

#!/usr/bin/env python

import codecs
import re

fsen = codecs.open('enwiki-20150112-400-r100-10576.txt', 'r', 'utf_8')
fout = codecs.open('80.txt', 'w', 'utf_8')

words = []
result = []

if __name__ == "__main__":

  n = 0
  for line in fsen:
    words = re.compile(r'[\s]').split(line)
    for word in words:
      word = re.sub(r'^[\.,!\?;:()\[\]\'"]*',r'',word)
      word = re.sub(r'[\.,!\?;:()\[\]\'"]*$',r'',word)
      if word:
        result.append(word)
    if result:
      fout.writelines(" ".join(result) + "\n") 
      result = [] 
    print(n)
    n += 1
結果
Anarchism
Anarchism is a political philosophy that advocates stateless societies often defined as self-governed voluntary institutions but that several authors have defined as more specific institutions based on non-hierarchical free associations Anarchism holds the state to be undesirable unnecessary or harmful While anti-statism is central anarchism entails opposing authority or hierarchical organisation in the conduct of human relations including but not limited to the state system
(略)

81. 複合語からなる国名への対処

英語では,複数の語の連接が意味を成すことがある.例えば,アメリカ合衆国は"United States",イギリスは"United Kingdom"と表現されるが,"United"や"States","Kingdom"という単語だけでは,指し示している概念・実体が曖昧である.そこで,コーパス中に含まれる複合語を認識し,複合語を1語として扱うことで,複合語の意味を推定したい.しかしながら,複合語を正確に認定するのは大変むずかしいので,ここでは複合語からなる国名を認定したい.

インターネット上から国名リストを各自で入手し,80のコーパス中に出現する複合語の国名に関して,スペースをアンダーバーに置換せよ.例えば,"United States"は"United_States","Isle of Man"は"Isle_of_Man"になるはずである.

"Isle of Man"を例として出している以上、"Isle of Man"は入っているリストを使え、ということでしょう。
"List of countries of the world in alphabetical order"
http://www.listofcountriesoftheworld.com/
というそのままの名前のページがあったので、ここのデータを使います。
で、嫌な形になっているのが

Cocos (Keeling) Islands
Congo, Democratic Republic of the
Congo, Republic of the
Falkland Islands (Islas Malvinas)
Gambia, The
Holy See (Vatican City)
Korea, North
Korea, South
Micronesia, Federated States of

などです。
Cocos (Keeling) Islandsについては、

Cocos Islands
Keeling Islands
Cocos Keeling Islands

という表現が本文中にありました。
本文中にある複合語に整形して、

Cocos Islands
Keeling Islands
Cocos Keeling Islands
Democratic Republic of the Congo
Republic of the Congo
Falkland Islands
Holy See
Vatican City
North Korea 
South Korea
Federated States of Micronesia

としました。
先ほどのリストからその他の空白で区切られた国名を収集すると、82の国名が複合国名として準備できました。
置換ですが、sedを使った方が早そうです。
sed -i -e 's/Cocos Islands/Cocos_Islands/g' 80.txt
pythonで一行ずつ処理するのと比べものにならないぐらい速かったので、pythonからsedを呼び出します。

#!/usr/bin/env python

import codecs
import re
import subprocess

f80 = codecs.open('80.txt', 'r', 'utf_8')
f81c = codecs.open('81c.txt', 'r', 'utf_8')

lcountry = []

def changecountryname(beforec,afterc):
  cmd = "sed -i -e 's/%s/%s/g' 80.txt" % (beforec,afterc)
  string = "s/" + beforec + "/" + afterc + "/g"
  cmd = ['sed','-i','-e', string ,'80.txt']
  print(cmd)

  subprocess.call( cmd )

if __name__ == "__main__":

  for line in f81c:
    country = re.split(r' ',line[:-1])
    if len(country) > 1:
      lcountry.append(len(country))
      print(" ".join(country),"/","_".join(country))
      changecountryname(" ".join(country),"_".join(country))

82. 文脈の抽出

81で作成したコーパス中に出現するすべての単語t関して,単語tと文脈語cのペアをタブ区切り形式ですべて書き出せ.ただし,文脈語の定義は次の通りとする.

ある単語tの前後d単語を文脈語cとして抽出する(ただし,文脈語に単語tそのものは含まない)
単語tを選ぶ度に,文脈幅dは{1,2,3,4,5}の範囲でランダムに決める.

文脈幅と言っていますが、英語では context window size(文脈窓幅)と言います。
ペアなので、例えば文脈幅が3だとすると、
A -3A
A -2A
A -1A
A +1A
A +2A
A +3A
という風に表現しろ、だと思います。
最初に一文をリストにしてから処理した方がいいだろうな……。

#!/usr/bin/env python

import codecs
import re
import random

fsen = codecs.open('80.txt', 'r', 'utf_8')
fout = codecs.open('82.txt', 'w', 'utf_8')

def context_word(words):
  listn = 0
  for word in words:
    cws = random.randint(1,5) #context window size
    cwslist = list(range( -1 * cws, cws + 1))
    cwslist.remove(0)
    if word:
      for x in cwslist:
        if listn+x >= 0 and listn+x < (len(words) - 1):
          fout.writelines(word+"\t"+words[listn+x]+"\n") 
    listn += 1

if __name__ == "__main__":

  for line in fsen:
    words = re.compile(r'[\s]').split(line)
    context_word(words)
結果
Anarchism       is
Anarchism       a
Anarchism       political
Anarchism       philosophy
Anarchism       that
is      Anarchism
is      a
a       Anarchism
a       is
(略)

777MBのテキストデータができました。
とりあえずはここまで。