単語の出現回数を数えてベクトル表記する. ScikitのCountVectorizerを使う.
In [1]:
from sklearn.feature_extraction.text import CountVectorizer
vectorizer = CountVectorizer(min_df=1)
上記min_dfパラメタの値より出現回数の小さい単語は無視される.
他にどんなパラメタがあるかは下記の通り.
In [2]:
print(vectorizer)
試しにベクトル化してみる.
In [3]:
content = ["How to format my hard disk", " Hard disk format problems "]
X = vectorizer.fit_transform(content)
vectorizer.get_feature_names()
Out[3]:
In [4]:
print(X.toarray().transpose())
In [5]:
import os
TOY_DIR = "/Users/Atsushi/Desktop/BuildingMachineLearningSystemsWithPython/ch03/data/toy"
posts = [open(os.path.join(TOY_DIR, f)).read() for f in os.listdir(TOY_DIR)]
posts
Out[5]:
In [6]:
from sklearn.feature_extraction.text import CountVectorizer
vectorizer = CountVectorizer(min_df=1)
X_train = vectorizer.fit_transform(posts)
num_samples, num_features = X_train.shape
print("#samples: %d, #features: %d" % (num_samples, num_features))
In [7]:
print(vectorizer.get_feature_names())
新しい文書のベクトル化は以下のようにする.
大抵が疎なベクトルなので,出現した単語のみの情報だけをデータに格納する.
In [32]:
new_post = "imaging databases"
new_post_vec = vectorizer.transform([new_post])
print(new_post_vec)
toarray()メソッドを用いることで,特徴ベクトルの全ての要素を表示することができる.
In [9]:
print(new_post_vec.toarray())
新しい文書と他の既存の文書の類似度(ユークリッド距離)を計算する.
In [10]:
import scipy as sp
def dist_raw(v1, v2):
delta = v1 - v2
return sp.linalg.norm(delta.toarray())
In [11]:
dist = dist_raw
In [33]:
import sys
best_dist = sys.maxsize
best_i = None
for i in range(0, num_samples):
post = posts[i]
if post == new_post:
continue
post_vec = X_train.getrow(i)
d = dist(post_vec, new_post_vec)
print("=== Post %i with dist=%.2f: %s" % (i, d, post))
if d < best_dist:
best_dist = d
best_i = i
print("Best post is %i with dist=%.2f" % (best_i, best_dist))
文書3と文書4を見比べる.
文書4は文書3を3回繰り返しただけなので,新しい文書に対しての類似度は,その二つの文書で同じであるべきでしょう.(ほんと?)
In [13]:
print(X_train.getrow(3).toarray())
print(X_train.getrow(4).toarray())
単語の出現頻度だけを特徴量として用いるのは単純すぎる.
特徴ベクトルを単位長さにするために,正規化する必要がある.
dist_raw関数を拡張して,単語の出現頻度からなるベクトルではなく,正規化したベクトルを返すようにする.
In [14]:
def dist_norm(v1, v2):
v1_normalized = v1 / sp.linalg.norm(v1.toarray())
v2_normalized = v2 / sp.linalg.norm(v2.toarray())
delta = v1_normalized - v2_normalized
return sp.linalg.norm(delta.toarray())
In [15]:
dist = dist_norm
上記スクリプト(※)でもう一度類似度を計算する.
今度は文書3と文書4は同じ類似度になっている.
文書2に含まれるmostのような単語は分野に関係なく様々な文章で登場する.
そのような単語は,imagesのような特定の分野で登場しやすい単語と比べて情報を持たず,文章の分類に貢献しない.
そのため,ストップワード(stop word)として処理の対象外とすべき.
In [16]:
vectorizer = CountVectorizer(min_df=1, stop_words='english')
stop_words='english'とすることで,318個の単語をストップワードとして登録出来る.
どのような単語がストップワードとして登録されるかを見るには以下のようにする.
In [18]:
print(sorted(vectorizer.get_stop_words())[0:20])
今は,意味的には同じ単語が語形変化により異なる単語としてカウントされている.(imaging, imagesなど)
これらは同じ単語としてカウントすべき.
Scikitには標準ではステミングを行う機能は含まれていない.
Natural Language Tooklit(NLTK)を用いることで出来る.
NLTKには異なる種類のステマーがあるが,例えば英語の場合,SnowballStemmerを用いる.
In [23]:
import nltk.stem
s= nltk.stem.SnowballStemmer('english')
print(s.stem("graphics"))
print(s.stem("imaging"))
print(s.stem("image"))
print(s.stem("imagination"))
print(s.stem("imagine"))
上記の例でわかるように,ステミングの結果は必ずしも正しい英単語になるとは限らない.
動詞については,次のような結果になる.
In [24]:
print(s.stem("buys"))
print(s.stem("buying"))
print(s.stem("bought"))
In [25]:
import nltk.stem
english_stemmer = nltk.stem.SnowballStemmer('english')
class StemmedCountVectorizer(CountVectorizer):
def build_analyzer(self):
analyzer = super(StemmedCountVectorizer, self).build_analyzer()
return lambda doc: (english_stemmer.stem(w) for w in analyzer(doc))
結果を見ると,imagesとimagingが同じ単語としてカウントされている.
In [30]:
vectorizer = StemmedCountVectorizer(min_df=1, stop_words='english')
X_train = vectorizer.fit_transform(posts)
print(vectorizer.get_feature_names())
再び上記スクリプト(※)で類似度を計算してみる.
これまで考えてきた特徴量は,単語の出現頻度を数えるだけの単純なものだった.
しかし,例えばsubjectのような,どの文書にも存在するような単語の影響を考えると今までのやり方は良くない.
そこで,「ある単語に対して,対象の文書中で出現した回数をカウントするのに加えて,その単語が他の文書でどれだけ出現するかをカウントし,その回数で割る」方法で解決する.
それによって,特定の文書だけで現れやすい単語,つまり,他の文書ではあまり現れない単語の特徴量の値は大きくなる.
これを,TF-IDF(term frequency - inverse document frequency)という.
In [34]:
import scipy as sp
def tfidf(t, d, D):
tf = float(d.count(t)) / sum(d.count(w) for w in set(d))
idf = sp.log(float(len(D)) / (len([doc for doc in D if t in doc])))
return tf * idf
簡単な例を以下に示す.
In [35]:
a, abb, abc = ["a"], ["a", "b", "b"], ["a", "b", "c"]
D = [a, abb, abc]
print(tfidf("a", a, D))
print(tfidf("b", abb, D))
print(tfidf("a", abc, D))
print(tfidf("b", abc, D))
print(tfidf("c", abc, D))
TF-IDFはTfidfVectorizer(CountVectorizerを継承したクラス)の中に含まれているため,これまで使用してきたステミングの機能を一から実装し直す必要はない.
In [38]:
from sklearn.feature_extraction.text import TfidfVectorizer
class StemmedTfidfVectorizer(TfidfVectorizer):
def build_analyzer(self):
analyzer = super(TfidfVectorizer, self).build_analyzer()
return lambda doc: (english_stemmer.stem(w) for w in analyzer(doc))
vectorizer = StemmedTfidfVectorizer(min_df=10, max_df=0.5, stop_words='english', decode_error='ignore')
charset_errorと書くとエラーになるので注意 → decode_error
1.テキストデータをトークン化する.
2.頻出しすぎる単語は,関連する文書を見つけるために役立たないため,取り除く.
3.滅多に使われない単語は,新しい文書でも使われる可能性が低いため,取り除く.
4.残った単語について,その出現回数をカウントする.
5.文書全体の状況を考慮するため,単語の出現回数からTF-IDFを計算する.
ただし,以下のような欠点がある.
●単語の関連性について考慮していない.「Car hits wall」と「Wall hits car」が同じ特徴ベクトルになる.
●否定的な意味を捉えることが出来ない.「I will eat ice cream」と「I will not eat ice cream」は意味的に逆だが,似た特徴ベクトルになる.しかし,2つの単語のペア(バイグラム)や3つ(トリグラム)をカウントすることで解決可能.
●タイプミスに対応出来ない.databaseとdatabasのように明らかに同じ意味だとわかる単語も別々に扱われる.