Tech Blog

Information Technology / Machine Learning / Data Analysis / Big Data / System Integration

Pythonで日本語WordNetと英語WordNetを利用して、単語間の類似度を測る

目的

「Mr.Childrenの歌詞分析(4): 単語の意味を考慮した、シングル曲のクラスタリング」では、文書の距離を計測するのに、同一語や同一概念の出現頻度を用いていた。この場合、"似ている"単語は考慮されておらず、クラスタリングの精度もいまひとつであった。

 

この問題を解決するために、単語間の距離をWordNetを用いて計測したい。今回は、2つのテキストファイルを元に、それぞれの単語の類似度を出力するプログラムを作成した。日本語WordNetと英語WordNetを用いて、多義語に対応した単語間の距離を計測している。

f:id:tkdmah:20130122223017p:plain

 

ちなみに、わざわざ英語版WordNetを用いているのは、類似度を測るAPIが日本語WordNet(のPython API)には用意されていないから。また、「Python による日本語自然言語処理 12.1.5 日本語WordNet」 には日本語 WordNet に対するリーダーが掲載されているが、複数のSynset(概念)を持つ多義語に対応できていない。

 

手法

環境設定

前準備として、日本語WordNetと英語WordNetの設定を行う。

  1. 日本語WordNet をダウンロードする。今回はタブ区切りテキスト(Just Japanese Words linked to Princeton WordNet Synsets)を用いる。 
  2. 英語WordNet をダウンロードする。Pythonインタープリタから以下のコマンドでダウンロードできる。
    import nltk
    nltk.download() # ブラウザが表示されるので、WordNetを選択してダウンロード
    # Chromeがデフォルトブラウザだと、ブラウザが表示されない
  3. 今回はEclipseで開発を行った。依存関係や入力補助など便利。 

Pythonスクリプト

単語のリストが書かれた2つのテキストファイルを入力として、その間の類似度を出力するスクリプトは、以下のとおり。

・jwn_driver.py 

'''
メインモジュール
"""
fin1 = 'TagExamples.txt' #入力ファイル1
fin2 = 'WordExamples.txt' #入力ファイル2
fout = 'Output.txt' #出力ファイル
 
import sim
wordLists = sim.makeWordLists(fin1,fin2) #単語リストを作成
synLists = sim.convWords2Synsets(wordLists[0], wordLists[1]) #概念リストを作成
simMatrix = sim.calcSim(synLists[0], synLists[1]) #類似度行列を作成
sim.writeSim(wordLists[0],wordLists[1],simMatrix,fout) #ファイルへの書き込み

・sim.py 

# -*- coding: utf-8 -*-
'''
英語WordNetから類似度を算出するモジュール
""" 
import codecs
 
def makeWordLists(fin1,fin2):
  """ ファイル名を2つ受け取って単語リストのリストを返す """
  fins = [fin1, fin2]
  wordLists = [[ ], [ ]]
  for i in [0,1]:
    f=codecs.open(fins[i], encoding="utf-8")
    for line in f:
      wordLists[i].append(line.strip("\r\n").strip("\n"))
    f.close()
  return wordLists
 
def convWords2Synsets(wordList1, wordList2):
  """ 単語リストを2つ受け取って概念リストのリストを返す """
  import jwn_corpusreader
  jwn = jwn_corpusreader.JapaneseWordNetCorpusReader('C:/LyricsWorkspace/nltk_data/corpora/wordnet', 'C:/LyricsWorkspace/WordNet/wnjpn-ok.tab') #英語WordNetと日本語WordNetを指定する
  synLists = [[ ],[ ]]
  wordLists = [wordList1, wordList2]
  for i in [0,1]:
    for j in range(len(wordLists[i])):
      synLists[i].append(jwn.synsets(wordLists[i][j]))
  return synLists
 
def calcSim(synList1,synList2):
  """ 概念リストを2つ受け取って類似度の行列を返す """
  import numpy as np
  simMatrix = np.zeros( (len(synList1), len(synList2)))
  for i in range(len(synList1)):
    for j in range(len(synList2)):
      sims = [ ]
      for syn1 in synList1[i]:
        for syn2 in synList2[j]:
          sims.append(syn1.path_similarity(syn2))
      simMatrix[i,j] = max(sims)
  return simMatrix
 
def writeSim(wordList1, wordList2, simMatrix,fout):
  """ 単語リストを2つと類似度行列とファイル名を受け取ってファイルに出力する """
  f = codecs.open(fout,'w', encoding="utf-8")
  for i in range(len(wordList1)):
    for j in range(len(wordList2)):
      f.write(wordList1[i] + "-" + wordList2[j] +": " + str(simMatrix[i][j])+"\r\n")
  f.close()

・jwn_corpusreader.py(WordNetCorpusReaderを継承)

# -*- coding: utf-8 -*-
"""
日本語リーダー
""" 
from nltk.corpus.reader.wordnet import WordNetCorpusReader
class JapaneseWordNetCorpusReader(WordNetCorpusReader):
    def __init__(self, root, filename):
        WordNetCorpusReader.__init__(self, root)
        import codecs
        f=codecs.open(filename, encoding="utf-8")
        self._jword2offset = {}
        for line in f:
            _cells = line.strip().split('\t')
            _offset_pos = _cells[0]
            _word = _cells[1]
            if len(_cells)>2: _tag = _cells[2]
            _offset, _pos = _offset_pos.split('-')
            try:
              self._jword2offset[_word].append({'offset': int(_offset), 'pos': _pos})
            except:
              self._jword2offset[_word]=[{'offset': int(_offset), 'pos': _pos}]
    def synsets(self, word):
        if word in self._jword2offset:
            results = [ ]
            for offset in (self._jword2offset[word]):
                results.append(WordNetCorpusReader._synset_from_pos_and_offset(
                self, offset['pos'], offset['offset']
                ))
            return results
        else:
            return None

 

結果

以下のような出力が得られた。多義語に対して最も類似度の高い類似度を採用した結果がsim_max.txt, 類似度の平均値を採用した結果がsim_average.txtである。

f:id:tkdmah:20130122231449p:plain

類似度の高い組み合わせとして、「楽しむ-好き」「楽しむ-失望」「楽しむ-空」が挙がっている。なぜか「楽しむ-失望」が類似している・・・これはいただけない・・・。また、「別れる」と類似しているのは「楽しむ」だと・・・。

 

そもそもWordNetの類似度(path_similarity)は、共通の上位語階層の概念への最短経路の長さを表す。つまり、名詞-動詞間の類似度や、系統的でない関係を持つ単語間の類似度は、人間が感じる心理的な類似度と乖離してしまっている。考えてみると当然と言えるかもしれない。これを解決するには、WebテキストやTwitterから共起を検出するしかないのかなぁ。