Bag of Wordsについて書いてみます。
ほとんどの機械学習は入力として数値データを与えなければなりません。そのため、自然言語処理において自然言語で書かれたデータを何らかの形で数値に変換する必要があります。Bag of Wordsはそのための一つの方法になります。
Bag of Wordsって何?
Bag of Wordsというのは自然言語処理において自然言語(人間が日常で使用している言語)で記述されたデータ、つまり文をベクトルで表現する方法のことです。
Bag of Wordsは次の3ステップで作ることができます。
- 数値変換
- one hot vector
- 足し合わせる
1. 数値変換
はじめに自然言語で書かれたデータを数値変換します。
例えば、以下のような3つの文A, B, Cがあったとします。
A: 私はラーメンが好きです。
B: 私は餃子が好きです。
C: 私はラーメンが嫌いです。
まず、この3つの文章を単語に分割します。
単語に分割する方法はいくつかありますが、今回は形態素解析を使います。
形態素解析した結果が下になります。
A: 私 は ラーメン が 好き です 。
B: 私 は 餃子 が 好き です 。
C: 私 は ラーメン が 嫌い です 。
形態素解析をすると、全部で9コの単語が得られます。
'私', 'は', 'ラーメン', 'が', '好き', 'です', '。', '餃子', '嫌い'
この9コの単語に数値を割り当てます。例えば、こんな感じです。
{'私': 0, 'は': 1, 'ラーメン': 2, 'が': 3, '好き': 4, 'です': 5, '。': 6, '餃子': 7, '嫌い': 8}
割り当てる数値は他の単語と重複しないようにします。
単語に数値を割り当てたので、すべての単語をone hot vectorに変換します。one hot vectorとはある要素が1で、それ以外が0になっているベクトルのことです。
one hot vectorではある単語を表現するためにN次元のベクトルを用います。ここで、Nとは単語の総数のことです。今回の例では、N=9となります。(※機械学習で自然言語をone hot vectorにすると少なくとも数千次元になると思います。)
ある単語を表現するには、その単語に対応する要素を1とし、のこり全てを0とします。例えば、以下のような感じです。※要素は0からN-1で表現することにします。
私は0が割り当てられたので、0番目の要素が1となり他はすべて0になっています。
好きも同様に4番目の要素が1になっています。
""" one hot vectorによる単語表現
私 -> [1, 0, 0, 0, 0, 0, 0, 0, 0]
好き -> [0, 0, 0, 0, 1, 0, 0, 0, 0]
"""
3. 足し合わせる
最後にone hot vectorにしたベクトルを足してBag of Wordsに変換します。
""" 私は餃子が好きです。
one hot vector
私 -> [1, 0, 0, 0, 0, 0, 0, 0, 0]
は -> [0, 1, 0, 0, 0, 0, 0, 0, 0]
餃子 -> [0, 0, 0, 0, 0, 0, 0, 1, 0]
が -> [0, 0, 0, 1, 0, 0, 0, 0, 0]
好き -> [0, 0, 0, 0, 1, 0, 0, 0, 0]
です -> [0, 0, 0, 0, 0, 1, 0, 0, 0]
。 -> [0, 0, 0, 0, 0, 0, 1, 0, 0]
Bag of Words
[1, 1, 0, 1, 1, 1, 1, 1, 0]
"""
ライブラリを使わずに実装してみる。
morphemes = ['私 は ラーメン が 好き です 。',
'私 は 餃子 が 好き です 。',
'私 は ラーメン が 嫌い です 。']
word2id = {}
for line in morphemes:
for word in line.split():
if word in word2id:
continue
word2id[word] = len(word2id)
bow_set = []
for line in morphemes:
bow = [0] * len(word2id)
for word in line.split():
try:
bow[word2id[word]] += 1
except:
pass
bow_set.append(bow)
print(*bow_set, sep="\n")
""" 結果
[1, 1, 1, 1, 1, 1, 1, 0, 0]
[1, 1, 0, 1, 1, 1, 1, 1, 0]
[1, 1, 1, 1, 0, 1, 1, 0, 1]
"""
gensimで実装
gensimを使って文書をBoWに変換する。
はじめに単語辞書を作る。
morphemesは上の例で使ったやつと同じ。分かち書きされたものがリストに入っている。
doc2bowでBoWのフォーマットに変換する。
from gensim.corpora import Dictionary
from gensim import matutils as mtu
dct = Dictionary()
for line in morphemes:
dct.add_documents([line.split()])
word2id = dct.token2id
print(word2id)
bow_set = []
for line in morphemes:
bow_format = dct.doc2bow(line.split())
bow_set.append(bow_format)
print(line)
print("BoW format: (word ID, word frequency)")
print(bow_format)
bow = mtu.corpus2dense([bow_format], num_terms=len(dct)).T[0]
print("BoW")
print(bow)
print(bow.tolist())
print(list(map(int, bow.tolist())))
"""
{'私': 0, 'は': 1, 'ラーメン': 2, 'が': 3, '好き': 4, 'です': 5, '。': 6,
'餃子': 7, '嫌い': 8}
私 は ラーメン が 好き です 。
BoW format: (word ID, word frequency)
[(0, 1), (1, 1), (2, 1), (3, 1), (4, 1), (5, 1), (6, 1)]
BoW
[ 1. 1. 1. 1. 1. 1. 1. 0. 0.]
[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0]
[1, 1, 1, 1, 1, 1, 1, 0, 0]
私 は 餃子 が 好き です 。
BoW format: (word ID, word frequency)
[(0, 1), (1, 1), (3, 1), (4, 1), (5, 1), (6, 1), (7, 1)]
BoW
[ 1. 1. 0. 1. 1. 1. 1. 1. 0.]
[1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0]
[1, 1, 0, 1, 1, 1, 1, 1, 0]
私 は ラーメン が 嫌い です 。
BoW format: (word ID, word frequency)
[(0, 1), (1, 1), (2, 1), (3, 1), (5, 1), (6, 1), (8, 1)]
BoW
[ 1. 1. 1. 1. 0. 1. 1. 0. 1.]
[1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0]
[1, 1, 1, 1, 0, 1, 1, 0, 1]
"""
scikit-learnを使った実装
今度はscikit-learnを使った実装方法を紹介します。今回はMeCabを使ってみます。corpus
にあるテキストを形態素解析して分かち書きしたものを再度corpus
に入れています。
import MeCab
corpus = [
"私はラーメンが好きです。",
"私は餃子が好きです。",
"私はラーメンが大嫌いです。"
]
tagger = MeCab.Tagger('-Owakati')
corpus = [tagger.parse(sentence).strip() for sentence in corpus]
中身は下のようになります。
print(*corpus, sep="\n")
"""出力結果
私 は ラーメン が 好き です 。
私 は 餃子 が 好き です 。
私 は ラーメン が 大嫌い です 。
"""
これをscikit-learnのCountVectorizerを使うことによりBoWに変換することができます。
from sklearn.feature_extraction.text import CountVectorizer
vectorizer = CountVectorizer(token_pattern=u'(?u)\\b\\w+\\b')
bag = vectorizer.fit_transform(corpus)
CountVectorizerのfit_transform()
を使うことにより与えられたテキストの集まり、今回はcorpus
ですが、これをもとにして単語辞書を作成し、corpus
にあるそれぞれのテキストに含まれる単語の出現頻度を計算してくれます。
中身を見るにはtoarray()
を使います。
print(bag.toarray())
"""出力結果
[[1 1 1 1 0 1 1 0]
[1 1 1 0 0 1 1 1]
[1 1 1 1 1 0 1 0]]
"""
単語辞書を見るにはvocabulary_
print(vectorizer.vocabulary_)
単語のみを取り出すにはget_feature_names()
print(vectorizer.get_feature_names())
ここで別のテキストをBoWに変換してみます。
sample = "私は味噌ラーメンと醤油ラーメンが好きです。"
これを先ほどcorpus
から作成したBoWをもとに変換してみます。今回は新しくBoWを作るわけではないのでfit_transform()
ではなくtransform()
を使います。
bag = vectorizer.transform([tagger.parse(sample).strip()])
print(bag.toarray())
"""出力結果
[[1 1 1 2 0 1 1 0]]
"""
transform()
には分かち書きしたものをリストに入れて渡します。
sample
には単語ラーメンが2回出てきています。ラーメンの単語辞書のインデックスは3になっているので出力結果のインデックス3が2になっています。他の単語も単語辞書をもとに出現回数が計算されており、正しくBoWに変換されていることがわかります。
自分が自然言語処理について勉強したことを記事にしてまとめたものです。
www.pytry3g.com
ゼロから作るDeepLearning
自然言語処理を勉強したい方へのおすすめの本です。
tensorflow, chainerやPyTorchといったフレームワークを使わずにゼロからnumpyを使ってディープラーニングの実装をしています。
扱っている内容はword2vec, RNN, GRU, seq2seqやAttentionなど、、、
おわり
最後にBag of Wordsについてまとめます。
Bag of Wordsはone hot vectorを足し合わせて表現しています。このことから、Bag of Wordsを用いるとある文にどのような単語がいくつ含まれているのかを知ることができます。
しかし、one hot vectorはあくまで単語をベクトルで表現しただけで、ある単語が文のどこに出てきたのか、ある単語は文を特徴づけるものになるのかは知ることができません。さらに、単語の数が増えればそれに伴って次元も増えるのであまり実用的ではないのかなと思いました。