コーパスの意味について調べてみた【ら、ベクトル空間だということが分かった】

コーパスが何なのか分からなかったので、コーパスの意味を調べてみた。そのメモ。


コーパスとは?の疑問に答えられるようになることがこの記事の最終目標です。



gensimに実装されたコーパスの中身を見てみました


コーパスの意味を知るには、コーパスをprintしてみて実際のデータ構造を見るのが手っ取り早そうです。


そのために、gensimに実装されたコーパスの中身を見てみることにしました。


gensimのチュートリアル通りに進みます。今回見るのは、「Corpora and Vector Spaces」です。


実際のコードを追っていきます。


>>> import logging
>>> logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)

# フォーマットしたprintのために必要
>>> import pprint

>>> from gensim import corpora, models, similarities

>>> documents = ["Human machine interface for lab abc computer applications",
>>>              "A survey of user opinion of computer system response time",
>>>              "The EPS user interface management system",
>>>              "System and human system engineering testing of EPS",
>>>              "Relation of user perceived response time to error measurement",
>>>              "The generation of random binary unordered trees",
>>>              "The intersection graph of paths in trees",
>>>              "Graph minors IV Widths of trees and well quasi ordering",
>>>              "Graph minors A survey"]

# 頻繁にでてくる単語の除去と、トークンへの分割
>>> stoplist = set('for a of the and to in'.split())
>>> texts = [[word for word in document.lower().split() if word not in stoplist] for document in documents]

# 1回しかでてこない単語の除去
>>> all_tokens = sum(texts, [])
>>> tokens_once = set(word for word in set(all_tokens) if all_tokens.count(word) == 1)
>>> texts = [[word for word in text if word not in tokens_once] for text in texts]

>>> pprint.pprint(texts)
[['human', 'interface', 'computer'],
 ['survey', 'user', 'computer', 'system', 'response', 'time'],
 ['eps', 'user', 'interface', 'system'],
 ['system', 'human', 'system', 'eps'],
 ['user', 'response', 'time'],
 ['trees'],
 ['graph', 'trees'],
 ['graph', 'minors', 'trees'],
 ['graph', 'minors', 'survey']]

ここまでは、自然言語処理によくある事前準備です。コーパスの意味とはあんまり関係ないです。


最後にtextsをprintした結果が、最終的に利用されるテキストデータになります。余分な単語は省かれていることが分かります。


>>> dictionary = corpora.Dictionary(texts)

# dictionaryをファイルに保存する方法
>>> dictionary.save('./deerwester.dict')
>>> dictionary.save_as_text('./deerwester_text.dict')

# dictionaryをファイルから読み込む方法
# >>> dictionary = corpora.Dictionary.load('./deerwester.dict')
# >>> dictionary = corpora.Dictionary.load_from_text('./deerwester_text.dict')

>>> print dictionary
Dictionary(12 unique tokens)

>>> pprint.pprint(dictionary.token2id)
{'minors': 11, 'graph': 10, 'system': 5, 'trees': 9, 'eps': 8, 'computer': 0,
'survey': 4, 'user': 7, 'human': 1, 'time': 6, 'interface': 2, 'response': 3}

# 一旦コマンドラインに戻ってから
cat ./deerwester_text.dict

# 実際にはtsv(タブ区切り)です
# format: id[TAB]word_utf8[TAB]document frequency[NEWLINE]
0  computer  2
8  eps       2
10 graph     3
1  human     2
2  interface 2
11 minors    2
3  response  2
4  survey    2
5  system    3
6  time      2
9  trees     3
7  user      3

ここまでで、dictionaryというデータを作りました。上記の通り、各単語のID、各単語、各単語の出現回数が書かれています。


gensimではこういうデータをdictionaryと呼ぶようです。一般的な呼び方というわけではありません。次はいよいよコーパスの作成です。


>>> corpus = [dictionary.doc2bow(text) for text in texts]

# corpusをファイルに保存する方法
>>> corpora.MmCorpus.serialize('./deerwester.mm', corpus)

# corpusをファイルから読み込む方法
# >>> corpus = corpora.MmCorpus('./deerwester.mm')

>>> pprint.pprint(corpus)
[(0, 1), (1, 1), (2, 1)]
[(0, 1), (3, 1), (4, 1), (5, 1), (6, 1), (7, 1)]
[(2, 1), (5, 1), (7, 1), (8, 1)]
[(1, 1), (5, 2), (8, 1)]
[(3, 1), (6, 1), (7, 1)]
[(9, 1)]
[(9, 1), (10, 1)]
[(9, 1), (10, 1), (11, 1)]
[(4, 1), (10, 1), (11, 1)]

# corpusとの比較のために再掲
>>> pprint.pprint(texts)
[['human', 'interface', 'computer'],
 ['survey', 'user', 'computer', 'system', 'response', 'time'],
 ['eps', 'user', 'interface', 'system'],
 ['system', 'human', 'system', 'eps'],
 ['user', 'response', 'time'],
 ['trees'],
 ['graph', 'trees'],
 ['graph', 'minors', 'trees'],
 ['graph', 'minors', 'survey']]

ここでやっとコーパス(corpus)がでてきました。コーパスの中身は二次元配列(python的にはリストとタプル)になっていることがcorpusのprint結果から分かります。


一行目[(0, 1), (1, 1), (2, 1)]の意味は、ID=0の単語(computer)が1回、ID=1の単語(human)が1回、ID=2の単語(interface)が1回出現している、という意味です。


textsをprintした結果の一行目は同じものを違う形式で表現しています。


元の文章「Human machine interface for lab abc computer applications」を見ると、確かに各単語とその出現回数はcorpusのprint結果と合っていることが分かります。他の文章についても同様です。


corpusとtextsのprint結果で順番が変わっているのは、corpusの中ではID順のソート、textsの中では出現順のソート、になっているというだけです。


ここまではcorpusを単なる二次元配列として捉えました。次に、corpusの各行をベクトルとして捉えて見ていきます。



コーパスを行ベクトルの集まりとして捉える


各単語のIDを次元、各行をベクトルだと捉えると、「corpusは、9本のベクトルが張る12次元のベクトル空間である」という言い方ができます。


corpusを通常の行列として表現すると下記のようになります。


# corpusの行列表現
| 1 1 1 0 0 0 0 0 0 0 0 0 |
| 1 0 0 1 1 1 1 1 0 0 0 0 |
| 0 0 1 0 0 1 0 1 1 0 0 0 |
| 0 1 0 0 0 2 0 0 1 0 0 0 |
| 0 0 0 1 0 0 1 1 0 0 0 0 |
| 0 0 0 0 0 0 0 0 0 1 0 0 |
| 0 0 0 0 0 0 0 0 0 1 1 0 |
| 0 0 0 0 0 0 0 0 0 1 1 1 |
| 0 0 0 0 1 0 0 0 0 0 1 1 |

# corpusの行列表現との比較のために再掲
>>> pprint.pprint(corpus)
[(0, 1), (1, 1), (2, 1)]
[(0, 1), (3, 1), (4, 1), (5, 1), (6, 1), (7, 1)]
[(2, 1), (5, 1), (7, 1), (8, 1)]
[(1, 1), (5, 2), (8, 1)]
[(3, 1), (6, 1), (7, 1)]
[(9, 1)]
[(9, 1), (10, 1)]
[(9, 1), (10, 1), (11, 1)]
[(4, 1), (10, 1), (11, 1)]

corpusをprintした結果は、各文章の単語とその出現回数を二次元配列で表現したものでした。さらに、それをベクトル空間で表現することもできました。


つまり、「コーパスとは、文章集合の単語とその出現回数をベクトル空間で表現したもの」ということが言えます。


コーパスの各行は最初に与えた各文章を表しています。12次元なのに行ベクトルが9個しかないのは、各ベクトルが直行していないためです。


ここまでで、コーパスとは各文章の集合をベクトル空間で表現したものであることが分かりました。次に、ベクトル空間で表現することで何ができるようになるのかを説明します。



ベクトル空間で表現できると類似度を計算することができる


ベクトル空間には、コサイン類似度という尺度があります。コサイン類似度は-1以上1以下の値をとり、数字が1に近いほど似ているという意味になります。


2つのベクトルが直行していると、cosの値と内積の値が0、つまりコサイン類似度が0=全く似ていない、という関係があることになります。逆に2つのベクトルが直行していないと、似ている(もしくは似ていない)という関係があることになります。


コーパスの各行ベクトルは直行していないと先ほど書きました。ということは、いくつかの行ベクトルの組み合わせに対して、似ている・似ていない、という尺度が計算できることになります。


つまり、文章集合をベクトル空間で表現することで、各文章に対して類似度が計算できるようになった、ということが分かります。



実際に類似度を計算してみる


「He is a human.」という文章と、最初に与えた各文章の類似度を計算してみます。


# 元の文章
He is a human.

# 元の文章をpythonのデータで表現したもの
[(1, 1)]

# 上記のデータを行ベクトルで表現したもの
| 0 1 0 0 0 0 0 0 0 0 0 0 |

「He is a human.」を行ベクトルで表現しました。


次に、最初の9個の文章と「He is a human.」のコサイン類似度を計算していきます。


| 0 1 0 0 0 0 0 0 0 0 0 0 | -> vec_x

| 1 1 1 0 0 0 0 0 0 0 0 0 | -> vec_0
| 1 0 0 1 1 1 1 1 0 0 0 0 | -> vec_1
| 0 0 1 0 0 1 0 1 1 0 0 0 | -> vec_2
| 0 1 0 0 0 2 0 0 1 0 0 0 | -> vec_3
| 0 0 0 1 0 0 1 1 0 0 0 0 | -> vec_4
| 0 0 0 0 0 0 0 0 0 1 0 0 | -> vec_5
| 0 0 0 0 0 0 0 0 0 1 1 0 | -> vec_6
| 0 0 0 0 0 0 0 0 0 1 1 1 | -> vec_7
| 0 0 0 0 1 0 0 0 0 0 1 1 | -> vec_8

# 内積の計算式(・は内積という意味です)
vec_a・vec_b = vec_a(x1) * vec_b(x1) + ... + vec_a(xn) * vec_b(xn)

# コサイン類似度の計算式
cos_sim(vec_a, vec_b) = vec_a・vec_b / (|vec_a| * |vec_b|)

# 上記の計算式から、以下のようにコサイン類似度を計算できます。

cos_sim(vec_x, vec_0) = 1 / (1 * √3) = 0.57
cos_sim(vec_x, vec_1) = 0 / (1 * √5) = 0
cos_sim(vec_x, vec_2) = 0 / (1 * √4) = 0
cos_sim(vec_x, vec_3) = 1 / (1 * √5) = 0.44
cos_sim(vec_x, vec_4) = 0 / (1 * √3) = 0
cos_sim(vec_x, vec_5) = 0 / (1 * √1) = 0
cos_sim(vec_x, vec_6) = 0 / (1 * √2) = 0
cos_sim(vec_x, vec_7) = 0 / (1 * √3) = 0
cos_sim(vec_x, vec_8) = 0 / (1 * √3) = 0

vec_0 = [(0, 1), (1, 1), (2, 1)]
      = ['human', 'interface', 'computer']
      = Human machine interface for lab abc computer applications

vec_3 = [(1, 1), (5, 2), (8, 1)]
      = ['system', 'human', 'system', 'eps']
      = System and human system engineering testing of EPS

上記の結果から、「He is a human.」と一番似ているのは文章ID=0の文章である、という結果になりました。


文章ID=0も文章ID=3も、humanという単語を含んでいるものの、文章ID=3は文章ID=0よりも単語数が多い(4つ)ため、文章ID=0の方がより似ている、という結果になりました。



つまりコーパスとは、各文章の特徴をベクトル空間で表現したものである


ここまでの結果から、コーパスとは文章集合をベクトル空間で表現したものである、ということが分かりました。


そして、文章集合をベクトル空間で表現することで、各文章同士の類似度が計算できることが分かりました。


類似度を計算できるという性質も踏まえてコーパスをもっと汎用的に表現すると、「文章集合の特徴を計算可能な表現に置き換えたもの」とも言えそうです。



まとめ


gensimに実装されたコーパス前提ですが、ここまでで、コーパスとは?という質問の一つの答えは見付けることができました。


gensimには他にも複数の形式のコーパスがあります。余力があれば、これらのコーパスの違いについても調べてみようと思います。


著者プロフィール
Webサイトをいくつか作っています。
著者プロフィール