どん底から這い上がるまでの記録

どん底から這い上がりたいけど這い上がれない人がいろいろ書くブログ(主にプログラミング)

Bag of Wordsについて書いてみる

 

Bag of Wordsについて書いてみます。

ほとんどの機械学習は入力として数値データを与えなければなりません。そのため、自然言語処理において自然言語で書かれたデータを何らかの形で数値に変換する必要があります。Bag of Wordsはそのための一つの方法になります。

 

Bag of Wordsって何?

Bag of Wordsというのは自然言語処理において自然言語(人間が日常で使用している言語)で記述されたデータ、つまり文をベクトルで表現する方法のことです。

Bag of Wordsは次の3ステップで作ることができます。

  1. 数値変換
  2. one hot vector
  3. 足し合わせる

1. 数値変換

はじめに自然言語で書かれたデータを数値変換します。

例えば、以下のような3つの文A, B, Cがあったとします。

 A: 私はラーメンが好きです。

 B: 私は餃子が好きです。

 C: 私はラーメンが嫌いです。

まず、この3つの文章を単語に分割します。

単語に分割する方法はいくつかありますが、今回は形態素解析を使います。

形態素解析した結果が下になります。

 A: 私 は ラーメン が 好き です 。

 B: 私 は 餃子 が 好き です 。

 C: 私 は ラーメン が 嫌い です 。

形態素解析をすると、全部で9コの単語が得られます。

'私', 'は', 'ラーメン', 'が', '好き', 'です', '。', '餃子', '嫌い'

この9コの単語に数値を割り当てます。例えば、こんな感じです。

{'私': 0, 'は': 1, 'ラーメン': 2, 'が': 3, '好き': 4, 'です': 5, '。': 6, '餃子': 7, '嫌い': 8}

割り当てる数値は他の単語と重複しないようにします。

2. one hot vector

単語に数値を割り当てたので、すべての単語を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]
"""

pythonでの実装

ライブラリを使わずに実装してみる。

# すでに3つの文章が形態素解析済みであるとします。
# morphemesには分かち書きされたものが入っています。
morphemes = ['私 は ラーメン が 好き です 。',
             '私 は 餃子 が 好き です 。',
             '私 は ラーメン が 嫌い です 。']

# 単語に数値を割り当てます。
word2id = {} # {単語: ID}
for line in morphemes:
    for word in line.split():
        if word in word2id:
            continue
        word2id[word] = len(word2id)

# Bag of Wordsを作る
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:
    # 辞書の更新
    # All tokens should be already tokenized and normalized.
    dct.add_documents([line.split()])
word2id = dct.token2id # 単語 -> ID
print(word2id)
bow_set = []
# 文をBoWに変換
for line in morphemes:
    # [(word ID, word frequency)]
    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)
    # numpyからlistに変える
    print(bow.tolist())
    # intにする
    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_)
# {'私': 6, 'は': 2, 'ラーメン': 3, 'が': 0, '好き': 5, 'です': 1, '餃子': 7, '大嫌い': 4}

 

単語のみを取り出すには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はあくまで単語をベクトルで表現しただけで、ある単語が文のどこに出てきたのか、ある単語は文を特徴づけるものになるのかは知ることができません。さらに、単語の数が増えればそれに伴って次元も増えるのであまり実用的ではないのかなと思いました。