Natural Language Processing from scratch #1
What is natural language ?
自然言語と言われてすぐにピンとくる人は多くないと思う。自然言語は、エスペラントなどの人工言語に対する自然発生した言語というわけではなく、アセンブリ言語やプログラミング言語のような各文が一義的な意味を表すコンピュータが理解するための人工言語と対比した、人が意思疎通のために利用する言語のことである。
自然言語は、同じ単語、文章でも文脈によって異なる意味を示すことが多々あり、さらに、時代の変遷により単語に新しい意味が生まれたり、新しい単語が生まれるため、自然言語の意味を正しくコンピュータに理解させるというタスクは非常に困難である。
Importance of NLP.
人類の知識は、文章によって保存され伝えられることが多いため(現にwikipediaには自然言語で書かれた無数の情報があるし、論文や特許は自然言語によって記述されている)、コンピュータが自然言語を理解することができれば、文明の発展に大きな影響を与えるような仕事をコンピュータにさせることもできるかもしれない。
コンピュータによる自然言語の理解=自然言語処理は、既にGoogleなどの検索機能や翻訳機能で使われている。あのGoogleの検索や翻訳に使われている自然言語処理ですらまだまだ完璧ではないことを考えると、自然言語処理について深い理解がある人材の需要は今後も増えていくと思われる。
今回は、単語の意味をコンピュータに理解させるための古典的な自然言語処理の手法について書いていきたいと思う。
ソースコードの全体はgithubに載せているため、要所部分の解説のみを行いたいと思う。
Thesaurus
コンピュータに単語の意味を理解させるための原始的な手法として、人手によって辞書を作成することが考えられる。これが、シソーラスと呼ばれるもので、グラフデータ構造によって単語間の関係性を定義している。
Pythonのnltkというライブラリを使用して、WordNetというシソーラスの構造を確認しようと思う。
まず、nltkをインストールする。
$ pip install nltk
次に、pythonインタプリンタを実行し、wordnetをダウンロードする。
>>> import nltk >>> nltk.download('wordnet')
wordnetでmouseという単語にどのような関係性のネットワークが構築されているかを確認する。
>>> from nltk.corpus import wordnet >>> wordnet.synsets('mouse') [Synset('mouse.n.01'), Synset('shiner.n.01'), Synset('mouse.n.03'), Synset('mouse.n.04'), Synset('sneak.v.01'), Synset('mouse.v.02')]
この結果は、mouseという単語には、名詞に4つの意味グループが存在し、動詞に2つの意味グループが存在していることを表している。
例えば、'mouse.n.04'と同じ意味グループの単語は、以下のコマンドで見ることができる。
>>> wordnet.synset('mouse.n.04').lemma_names() ['mouse', 'computer_mouse']
mouseという名詞の4番目の意味グループには、'mouse', 'computer_mouse'という2つの単語があることが分かる。この2つの単語がどちらもコンピュータのHIDとしてのポインティングデバイスであるマウスを指していることは想像できるだろう。このようにシソーラスでは、単語が意味ごとの関係性によって分類・整理されている。
シソーラスは、コンピュータに単語の意味を理解させるという意味では、非常に優れた辞書だが、辞書は人手によって作成されているため、新語や新しい意味が誕生するたびに人の手によって辞書を整理し直す必要があり、英語だけでも100万語以上存在すると言われていることを鑑みると、コンピュータ用の辞書としては、シソーラスはあまり現実的な手法とは言えないだろう。
Vector based on word count
現在の自然言語処理の基礎となっているのが、機械的な処理によって単語を分散表現(ベクトル)で表すという手法である。単語を分散表現で表す手法にはさまざまな手法が考えられるが、最も基本的な、文章中の目標単語周辺に出現する単語をカウントすることで分散表現を獲得する手法について紹介する。これは、単語の意味は、その単語の周辺に存在する単語によって形成されるという考え方に基づいている。例えば、次のスティーブ・ジョブズの名言を見てみると、
You can’t connect the dots looking forward; you can only connect them looking backwards. So you have to trust that the dots will somehow connect in your future.
"connect"という単語は、"you"という単語の近くに出現している。これは"connect"が人に関係が深い単語であることを示唆していると考えられるだろう。また、"connect"は、"dots"という単語の近くにも出現している。これ、"connect"が複数のdotに関係する単語であることを表していると考えられるだろう。つまり、カウントの結果を集めることで、"connect"という単語が、人が主体となって行い、複数のモノに対して(結びつけるという)動作を行う単語であることを推察できる分散表現を獲得することができる。さらに、カウントの対象となる文章を増やすことにより、周辺に存在する単語の傾向がより明確になり、単語の意味がより正確に推測できる。
pythonで、カウントによる分散表現を計算する。
''' 単語出現頻度カウントによるベクトルの作成 ''' def count(all_words_num, id_text): #記録用のテーブルを作成 table = [[0] * all_words_num for i in range(all_words_num)]
上記コードでは、単語の出現回数をカウントするためのリストを作成している。リストは、全語彙数文の要素数を持つリストを全語彙数分だけ用意している。
for k in id_text: num_k = len(k) for l in range(num_k): element = table[k[l]] if l == 0:#文章の開始単語の場合、右側単語のみを調べる right = k[l+1] element[right] += 1 elif l == num_k-1:#文章の終了単語の場合、左側単語のみを調べる left = k[l-1] element[left] += 1 else:#文章の途中の単語の場合、両側を調べる left = k[l-1] right = k[l+1] element[left] += 1 element[right] += 1 return table
上記コードでは、注目単語周辺に存在した単語をリストに記録していき、forループによって、全文章文繰り返している。文章の開始単語は、当然左側に単語が存在しないため、右側のみをカウントし、文章の終了単語は、右側に単語が存在しないため、左側のみをカウントするために、if文によって処理を分岐させている。
最終的な結果は下のようになった。分散表現の各リストの登場
---入力テキスト--- ---入力テキスト--- ['you', 'can’t', 'connect', 'the', 'dots', 'looking', 'forward'] ['you', 'can', 'only', 'connect', 'them', 'looking', 'backwards'] ['so', 'you', 'have', 'to', 'trust', 'that', 'the', 'dots', 'will', 'somehow', 'connect', 'in', 'your', 'future'] ---単語一覧--- ('forward', 'future', 'your', 'looking', 'connect', 'can', 'will', 'have', 'the', 'can’t', 'backwards', 'in', 'so', 'trust', 'that', 'only', 'dots', 'them', 'you', 'somehow', 'to') ---分散表現--- forward : [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] future : [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] your : [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0] looking : [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0] connect : [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0] can : [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0] will : [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0] have : [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1] the : [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 2, 0, 0, 0, 0] can’t : [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0] backwards : [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] in : [0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] so : [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0] trust : [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1] that : [0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0] only : [0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] dots : [0, 0, 0, 1, 0, 0, 1, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] them : [0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] you : [0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0] somehow : [0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] to : [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0]
入力文章を増やすことにより、単語ごとの傾向がより顕著となり、得られる分散表現の精度が高くなると思われる。
欠点としては、語彙数が増加するほど、ベクトルの次元数が大きくなるため計算量が増加してしまうという問題がある。この欠点を解消するためには、特異値分解SCDを用いた次元削減が用いられている。
また、他の欠点として、特定の単語との関係が薄い”the”などの単語に対して、"dots"という単語との結びつきが強いという誤った解釈がされていることが挙げられる。そこで、この欠点を解消する手法として、TF-IDFと呼ばれる手法がある。
次回は、SVDによる次元削減と、TF-IDFを用いたベクトルの重み付けについて書きたいと思う。