Python の Gensim ライブラリを使ったチュートリアル https://github.com/RaRe-Technologies/movie-plots-by-genre の練習ノートです。
Word2Vec とは、Tomas Mikolov によて 2013 年に発表された文章の解析手法です。文章や単語を比較するのに使います。ここでは Gensim ライブラリのを使って Word2Vec を使ってみます。Gensim のレポジトリにあるチュートリアルそのままですが、自分で分かりづらいところや、現在のバージョンで動かなかった箇所を補足するためのメモです。
元論文:
- Efficient Estimation of Word Representations in Vector Space
- Distributed representations of words and phrases and their compositionality
他の役に立ちそうな参考資料:
- The Illustrated Word2vec
- Mike Tamir, Classifying Text without (many) Labels
- word2vec - Google Code
- models.word2vec – Word2vec embeddings
%load_ext autoreload
%autoreload 2
from gensim.matutils import unitvec
from nltk.corpus import stopwords
from pprint import pprint
from sklearn import linear_model
from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer
from sklearn.metrics import accuracy_score, confusion_matrix
from sklearn.model_selection import train_test_split
from smart_open import smart_open
import gensim.downloader as api
import matplotlib.pyplot as plt
import nltk
import numpy as np
import pandas as pd
import random
まず素材となるデータを読み込みます。これから映画の粗筋からジャンルを判別するというのをやります。
tagged_plots_movielens = "https://raw.githubusercontent.com/RaRe-Technologies/movie-plots-by-genre/master/data/tagged_plots_movielens.csv"
df = pd.read_csv(smart_open(tagged_plots_movielens)).dropna()
display(df)
print(f'plot: {df.iloc[12]["plot"]}')
print(f'tag: {df.iloc[12]["tag"]}')
Unnamed: 0 | movieId | plot | tag | |
---|---|---|---|---|
0 | 0 | 1 | A little boy named Andy loves to be in his roo... | animation |
1 | 1 | 2 | When two kids find and play a magical board ga... | fantasy |
2 | 2 | 3 | Things don't seem to change much in Wabasha Co... | comedy |
3 | 3 | 6 | Hunters and their prey--Neil and his professio... | action |
4 | 4 | 7 | An ugly duckling having undergone a remarkable... | romance |
... | ... | ... | ... | ... |
2443 | 2443 | 148618 | Three kids who travel back in time to 65 milli... | animation |
2444 | 2444 | 148624 | Top Cat and the gang face a new police chief, ... | animation |
2445 | 2445 | 149088 | Raggedy Ann and the rest of the toys in Marcel... | animation |
2446 | 2446 | 149406 | Continuing his "legendary adventures of awesom... | comedy |
2447 | 2447 | 151451 | A romance fantasy humorous situations cleverly... | romance |
2427 rows × 4 columns
plot: In a future world devastated by disease, a convict is sent back in time to gather information about the man-made virus that wiped out most of the human population on the planet.
tag: sci-fi
12 行目の内容を抜き出してみました。plot が粗筋、tag がジャンル名です。これから plot を読んで tag を推測します。参考までに tag の分布を見てみます。
df.tag.value_counts().plot.bar()
print("Comedy / All:", len(df[df.tag=="comedy"]) / len(df))
Comedy / All: 0.32138442521631644
comedy の tag が突出して多いので、何でもかんでも comedy と予測しただけで 32% の正確性になってしまいます。
sklearn の train_test_split 関数を使って元のデータの9割を教師データとして取り分け、残りの1割をテストデータに取っておきます。ジャンルが偏ってないかも一応確認します。
train_data, test_data = train_test_split(df, test_size=0.1, random_state=42)
print("Count train:", len(train_data))
print("Count test_data:", len(test_data))
test_data.tag.value_counts().plot.bar()
Count train: 2184
Count test_data: 243
<AxesSubplot:>
Model evaluation approach
これから色々な手法を比較評価していくためのヘルパー関数を作ります。
def plot_confusion_matrix(cm, title='Confusion matrix', cmap=plt.cm.Blues):
plt.imshow(cm, interpolation='nearest', cmap=cmap)
plt.title(title)
plt.colorbar()
target_names = df.tag.unique()
tick_marks = np.arange(len(target_names))
plt.xticks(tick_marks, target_names, rotation=45)
plt.yticks(tick_marks, target_names)
plt.tight_layout()
plt.ylabel('True label')
plt.xlabel('Predicted label')
def evaluate_prediction(predictions, target, title="Confusion matrix"):
print('accuracy %s' % accuracy_score(target, predictions))
cm = confusion_matrix(target, predictions)
print('confusion matrix\n %s' % cm)
print('(row=expected, col=predicted)')
cm_normalized = cm.astype('float') / cm.sum(axis=1)[:, np.newaxis]
plot_confusion_matrix(cm_normalized, title + ' Normalized')
def predict(vectorizer, classifier, data):
data_features = vectorizer.transform(data['plot'])
predictions = classifier.predict(data_features)
target = data['tag']
evaluate_prediction(predictions, target)
ここでは、predict という関数を作って予測器を評価します。predict に3つの引数を与えます。それぞれ
- vectorizer: 文章をベクトルに変換する。
- classifier: ベクトルを使って tag を予測する。
- data: 上で取り分けておいた test_data。
となります。では上のヘルパー関数の動作確認のために、どんな入力にも "comedy" と返していまう最もデタラメな予測器を与えてみましょう。
# どんな文章にも [0] を返すベクトル変換器
class Bogus_vectorizer:
def transform(self, texts):
return np.zeros((len(texts), 1), int)
# どんな入力にも "comedy" をデタラメに返す予測器
class Bogus_classifier:
def fit(self, features, tags):
self.tags = tags.values
def predict(self, features):
return np.full(len(features), "comedy")
#return np.array([random.choice(self.tags) for _ in features]) # こっちの行を有効にするとランダムに返します。
bogus_vectorizer = Bogus_vectorizer()
bogus_vectorizer.transform(test_data["plot"])
bogus_classifier = Bogus_classifier()
bogus_classifier.fit(bogus_vectorizer.transform(train_data["plot"]), train_data["tag"])
bogus_classifier.predict(bogus_vectorizer.transform(test_data["plot"]))
predict(bogus_vectorizer, bogus_classifier, test_data)
accuracy 0.35390946502057613
confusion matrix
[[ 0 0 42 0 0 0]
[ 0 0 31 0 0 0]
[ 0 0 86 0 0 0]
[ 0 0 16 0 0 0]
[ 0 0 35 0 0 0]
[ 0 0 33 0 0 0]]
(row=expected, col=predicted)
Confusion matrix の横軸が予測されたジャンル、縦軸が本当のジャンルです。結果の通り、本当ジャンルに関わらずこのデタラメ予測器は "comedy" と判定しますがそれでも accuracy は 35% 出ます。という事は少なくとも 35% 以上の精度が出ないと頑張る甲斐がないという事になります。
Bag of words
まず一番簡単な Bag of words と LogisticRegression を試します。Bag of worlds とは、単語の出現頻度を数えて文の特徴ベクトルとする物です。例えば "bye bye now" なら bye: 2, now: 1 のようなベクトルになります。出来た特徴ベクトルを教師データとして使って LogisticRegression を訓練して予測器を作ります。
Bag of words を作るために sklearn の CountVectorizer を使います。文章を単語ごとに区切る tokenizer を内蔵している優れものです。数える単語の数を上位頻出 3000 語に限ります。
%%time
# training
count_vectorizer = CountVectorizer(
analyzer="word",
stop_words='english',
max_features=3000)
train_data_features = count_vectorizer.fit_transform(train_data['plot'])
train_data_features
CPU times: user 103 ms, sys: 2.53 ms, total: 105 ms
Wall time: 105 ms
<2184x3000 sparse matrix of type '<class 'numpy.int64'>'
with 49189 stored elements in Compressed Sparse Row format>
train_data_features は粗筋の数 x 単語の数 (最大 3000) の表で、単語が何件出現するかを記録したものです。
試しに教師データの粗筋がどのように変換されたか見てみます。CountVectorizer はレアすぎる単語や頻繁すぎる単語は除外してくれるので、それなりに文章の特徴を表す単語がカウントされています。
def show_count(index):
print("plot:", train_data.iloc[index]["plot"])
print("count:")
print({
count_vectorizer.get_feature_names()[key]: train_data_features[index, key]
for key
in train_data_features[index].indices})
show_count(12)
plot: A bumbling professor accidently invents flying rubber, or "Flubber", an incredible material that gains energy every time it strikes a hard surface. It allows for the invention of shoes that can allow jumps of amazing heights and enables a modified Model-T to fly. Unfortunately, no one is interested in the material except for Alonzo Hawk, a corrupt businessman who wants to steal the material for himself.
count:
{'wants': 1, 'hard': 1, 'corrupt': 1, 'unfortunately': 1, 'bumbling': 1, 'professor': 1, 'flying': 1, 'incredible': 1, 'material': 3, 'gains': 1, 'energy': 1, 'time': 1, 'strikes': 1, 'surface': 1, 'allows': 1, 'invention': 1, 'shoes': 1, 'allow': 1, 'amazing': 1, 'heights': 1, 'model': 1, 'fly': 1, 'interested': 1, 'hawk': 1, 'businessman': 1, 'steal': 1}
得られた Bag of words を特徴ベクトルとして、LogisticRegression でジャンルの予測器を作ってみます。
logreg = linear_model.LogisticRegression(n_jobs=-1, C=1e5)
logreg.fit(train_data_features, train_data['tag'])
predict(count_vectorizer, logreg, test_data)
accuracy 0.4444444444444444
confusion matrix
[[23 2 10 0 4 3]
[ 2 11 8 2 3 5]
[12 8 45 2 17 2]
[ 3 2 2 3 5 1]
[ 3 4 12 1 14 1]
[ 9 4 6 2 0 12]]
(row=expected, col=predicted)
テストデータを使って評価すると、44% とまあまあ良い正答率です。特に amination や comedy の成績が良いです。
LogisticRegression の coef_ 属性を使うと、どのベクトルの次元の影響力が強いのか観察出来ます。coef_ は tag の数 x 特徴ベクトルの次元(単語の数) で、数字が大きいほど影響力が大きいです。まず、単純にジャンルごとの coef_ の内容を見てみます。
for tag_index, tag in enumerate(logreg.classes_):
print(f"{tag}: {logreg.coef_[tag_index]})")
action: [ 0.75573721 -0.47581172 0.54842262 ... -1.99314501 -0.2582481
-0.09286177])
animation: [-0.50553718 1.19642655 -0.12130582 ... -0.68777642 -0.11919812
0.69879877])
comedy: [-0.03884728 1.97907157 0.74785816 ... 5.12966293 -1.77318799
-0.08446958])
fantasy: [-0.62133302 -0.01795021 -0.63881266 ... -0.42830753 -1.0628616
-0.05787423])
romance: [-0.98587051 -2.00795811 -0.30017212 ... -0.37590957 1.07365758
-0.35639341])
sci-fi: [ 1.39585078 -0.67377808 -0.23599017 ... -1.64452441 2.13983822
-0.10719979])
これだけでは意味がわからないので、coef_ をソートしてから上位のインデックスを単語に変換して、ジャンルを特徴づける単語を確認します。割と納得の結果なのではと思います。
def most_influential_words(vectorizer, classifier, num_words=10):
words = vectorizer.get_feature_names()
for tag_index, tag in enumerate(classifier.classes_):
coef = classifier.coef_[tag_index]
sorted_coef = sorted(enumerate(coef), key=lambda x:x[1], reverse=True)
sorted_word = [words[index] for index, value in sorted_coef]
print(f"{tag}: {sorted_word[:num_words]})")
most_influential_words(count_vectorizer, logreg, 5)
action: ['america', 'assassin', 'conspiracy', 'pursue', 'terrorists'])
animation: ['forest', 'fight', 'snow', 'ending', 'adventurous'])
comedy: ['mistaken', 'comedy', 'dealer', 'actress', 'comedian'])
fantasy: ['national', 'princess', 'dragon', 'kingdom', 'beast'])
romance: ['decide', 'relationships', 'troubled', 'beth', 'nazi'])
sci-fi: ['future', 'futuristic', 'cube', 'enterprise', 'space'])
Character N-grams
次は N-grams です。N-grams というのは、単語で区切る事すらせずに、前後 n 個の文字列の頻度を数えて特徴量とする物です。乱暴なようですが、意外とそれなりの結果が出ます。ここでは 2 から 5 までの文字を区切って特徴量を作ります。
%%time
n_gram_vectorizer = CountVectorizer(
analyzer="char",
ngram_range=([2,5]),
tokenizer=None,
preprocessor=None,
max_features=3000)
logreg = linear_model.LogisticRegression(n_jobs=1, C=1e5, max_iter=1000)
train_data_features = n_gram_vectorizer.fit_transform(train_data['plot'])
logreg = logreg.fit(train_data_features, train_data['tag'])
CPU times: user 2min, sys: 43.1 s, total: 2min 44s
Wall time: 13.4 s
評価してみます。
predict(n_gram_vectorizer, logreg, test_data)
accuracy 0.3950617283950617
confusion matrix
[[17 3 10 1 9 2]
[ 5 9 9 3 1 4]
[15 8 41 4 16 2]
[ 2 2 3 3 2 4]
[ 5 1 14 1 13 1]
[ 9 1 5 4 1 13]]
(row=expected, col=predicted)
約 40% とまあまあです。
most_influential_words(n_gram_vectorizer, logreg, 5)
action: ['my', ' war', 'ank', 'es ', 'ist '])
animation: [' an', 'oy', 'ot', 'y d', 'ima'])
comedy: ['ks', 'ud', 'per', 'ate', 'man '])
fantasy: ['au', 'king', 'rag', 'd of ', 't d'])
romance: ['so', 'vel', ' love', 'par', 'ili'])
sci-fi: ['fu', 'rg', 'ar ', ' a f', ' fu'])
影響力の大きい特徴を見ても、よくこんなもので 40% も当てるなと思います。
TF-IDF
次に、Bag of words をもう少し進化させた term frequency–inverse document frequency 略して TF-IDF を試します。TF-IDF は Bag of words で単語の出現頻度を数えた後、文の特徴となるような珍しい単語の重みを上げ、平凡な単語の重みを下げます。
%%time
tf_vect = TfidfVectorizer(
#tokenizer=nltk.word_tokenize,
#ngram_range=(1, 2),
)
train_data_features = tf_vect.fit_transform(train_data['plot'])
logreg = linear_model.LogisticRegression(n_jobs=1, C=1e5, max_iter=1000)
logreg = logreg.fit(train_data_features, train_data['tag'])
CPU times: user 25.1 s, sys: 8.81 s, total: 33.9 s
Wall time: 2.63 s
predict(tf_vect, logreg, test_data)
accuracy 0.4609053497942387
confusion matrix
[[21 3 11 0 3 4]
[ 2 10 10 2 2 5]
[ 6 3 51 2 22 2]
[ 4 4 2 3 1 2]
[ 4 1 16 1 13 0]
[ 7 3 6 3 0 14]]
(row=expected, col=predicted)
正答率 46% です。Bag of words が 44% だったのでちょっと向上しました。tokenizer を変えたり N-gram を指定して複数単語で特徴量を見たりすると多少向上します。
most_influential_words(tf_vect, logreg, 5)
action: ['army', 'assassin', 'terrorists', 'terrorist', 'drug'])
animation: ['animated', 'forest', 'animals', 'named', 'halloween'])
comedy: ['comedy', 'up', 'jewish', 'around', 'jail'])
fantasy: ['king', 'louie', 'magical', 'harry', 'magic'])
romance: ['love', 'apartment', 'troubled', 'she', 'dance'])
sci-fi: ['future', 'alien', 'space', 'human', 'planet'])
影響の大きい特徴もちょっと良いような気がします。
Word2Vec
いよいよ Word2Vec を試します。Word2Vec というのは、大量の文章を学習して単語の類似度などの関係を反映させた特徴ベクトルを上手に抽出します。Bag of words や TF-IDF が「文章」に対応する特徴ベクトルを抽出するのに対して、Word2Vec では「単語」に対応する特徴ベクトルを抽出します。
参考:
- Word2Vec Model Tutorial
- models.word2vec – Word2vec embeddings
- models.keyedvectors – Store and query word vectors
学習には時間がかかるので、予め Google News dataset から作成された model を使います。
%%time
wv = api.load('word2vec-google-news-300')
CPU times: user 40.5 s, sys: 2.38 s, total: 42.8 s
Wall time: 43.8 s
単語の特徴ベクトルは配列アクセスの文法を使って取得出来ます。
wv["Tokyo"]
array([-0.109375 , 0.27148438, -0.00787354, 0.14648438, -0.20117188,
0.06396484, -0.22851562, -0.07421875, 0.02185059, 0.22167969,
-0.40820312, 0.55859375, 0.46289062, -0.34960938, -0.21582031,
-0.20117188, -0.10058594, 0.140625 , 0.234375 , -0.06201172,
0.05615234, -0.31835938, -0.05834961, 0.03662109, 0.01171875,
-0.02416992, -0.078125 , 0.01513672, -0.18652344, 0.08789062,
0.19042969, -0.09033203, -0.421875 , -0.265625 , -0.19628906,
-0.07080078, -0.09375 , 0.15917969, 0.13574219, -0.06103516,
-0.00558472, -0.06396484, 0.02185059, 0.296875 , -0.12402344,
0.46875 , -0.02392578, 0.06689453, 0.05834961, 0.0559082 ,
0.16699219, -0.00126648, 0.41796875, -0.05664062, -0.45898438,
0.41015625, -0.18847656, -0.26953125, 0.11132812, -0.41992188,
-0.09033203, 0.08935547, 0.04882812, -0.04394531, 0.20996094,
-0.21582031, 0.00473022, 0.15136719, 0.2109375 , 0.00393677,
-0.24902344, 0.09277344, 0.05981445, 0.12353516, -0.21386719,
-0.13085938, 0.06152344, 0.28125 , 0.3515625 , -0.18554688,
0.265625 , -0.32617188, -0.16503906, 0.10253906, 0.03417969,
-0.21679688, 0.00506592, -0.171875 , 0.08349609, 0.22070312,
-0.25195312, 0.1640625 , -0.11328125, 0.05908203, -0.11279297,
0.08105469, 0.0625 , 0.04663086, -0.2265625 , -0.00579834,
-0.20019531, 0.18066406, -0.00601196, 0.25585938, -0.00159454,
-0.00424194, -0.08496094, -0.13183594, 0.02075195, -0.07763672,
-0.01434326, 0.02844238, 0.30664062, -0.0189209 , -0.11376953,
-0.12695312, -0.09912109, -0.296875 , 0.17578125, 0.10986328,
-0.27929688, -0.0045166 , -0.17675781, 0.21875 , 0.32421875,
-0.18359375, -0.07275391, 0.01416016, 0.11279297, 0.00479126,
-0.05053711, -0.10107422, -0.08447266, -0.1328125 , -0.25195312,
-0.34570312, -0.10791016, -0.20019531, -0.27929688, 0.12402344,
0.10742188, 0.01928711, 0.171875 , -0.0189209 , 0.24414062,
0.02246094, -0.01495361, 0.10351562, -0.26367188, 0.10058594,
0.24804688, -0.01757812, 0.2890625 , 0.02282715, -0.05126953,
-0.2734375 , -0.09716797, -0.31640625, 0.02197266, -0.09033203,
0.10644531, -0.00201416, 0.23828125, 0.33007812, 0.10693359,
-0.1953125 , 0.2265625 , 0.21484375, 0.11035156, 0.27929688,
-0.04174805, -0.20117188, 0.13085938, 0.25585938, -0.0177002 ,
-0.3203125 , 0.12597656, -0.18066406, 0.14160156, 0.06054688,
0.05639648, -0.12792969, -0.07226562, -0.37109375, 0.4609375 ,
0.06982422, 0.02172852, -0.11181641, -0.28125 , -0.06933594,
-0.05493164, -0.0234375 , 0.02478027, -0.10742188, -0.14746094,
0.09521484, 0.29882812, -0.39257812, -0.01196289, -0.515625 ,
-0.17675781, -0.27734375, -0.0703125 , 0.03173828, -0.11572266,
-0.5 , 0.02087402, -0.2578125 , 0.01464844, -0.1640625 ,
-0.47460938, -0.03088379, 0.14550781, 0.2109375 , -0.20019531,
-0.01086426, -0.06689453, 0.37304688, 0.02844238, 0.06738281,
0.01623535, -0.09960938, -0.13671875, -0.04394531, -0.17675781,
0.37304688, -0.16015625, 0.04711914, -0.05786133, -0.234375 ,
-0.02062988, -0.13378906, -0.09472656, 0.01538086, 0.32617188,
0.26757812, -0.25 , -0.02502441, 0.15527344, -0.12890625,
0.14746094, 0.0065918 , -0.296875 , -0.11816406, -0.3671875 ,
0.08496094, -0.13769531, -0.07275391, 0.15136719, -0.0625 ,
-0.328125 , -0.04541016, 0.12792969, -0.10888672, -0.12011719,
-0.22363281, 0.16113281, -0.14160156, 0.00842285, 0.3125 ,
0.13769531, 0.07861328, 0.20117188, -0.03540039, -0.08837891,
0.12988281, -0.00915527, -0.3046875 , 0.29101562, -0.13085938,
-0.24023438, -0.11572266, 0.01086426, 0.04394531, 0.27539062,
0.02172852, -0.33984375, 0.453125 , 0.23828125, 0.2109375 ,
-0.11865234, -0.03881836, 0.08007812, -0.07275391, 0.30664062,
0.22851562, -0.33007812, -0.05249023, -0.3671875 , -0.04125977,
-0.0019989 , 0.12890625, 0.18066406, 0.10498047, -0.0625 ,
-0.14941406, -0.35546875, 0.15722656, -0.1640625 , 0.08789062],
dtype=float32)
うまく学習すると、まるで単語の特徴ベクトルに演算が成り立つかのようになります。例えば以下は「日本にとっての東京はフランスにとって何だ?」というクイズを表します。
wv.similar_by_vector(wv["Tokyo"] - wv["Japan"] + wv["France"], 1)
[('Paris', 0.8110150694847107)]
次に「父が男なら母は何だ?」です。
wv.similar_by_vector(wv["man"] - wv["father"] + wv["mother"], 1)
[('woman', 0.8460322618484497)]
映画のレビューは単語ではなく文章なので、このままでは予測に使えません。そこで、文章の特徴ベクトルを得るためどうするかというと、単純に平均化すると良いそうです。
まず単語を切り出します。その際に、nltk.corpus.stopwords を使って一般的すぎる単語を除外するとより良く特徴を捉える事が出来ます。
nltk.download('stopwords')
def w2v_tokenize_text(text):
tokens = []
for sent in nltk.sent_tokenize(text, language='english'):
for word in nltk.word_tokenize(sent, language='english'):
if len(word) < 2:
continue
if word.lower() in stopwords.words('english'):
continue
tokens.append(word)
return tokens
# 例文
tng_plot = "The series begins with the crew of the Enterprise-D put on trial by an omnipotent being known as Q, who became a recurring character. The god-like entity threatens the extinction of humanity for being a race of savages, forcing them to solve a mystery at nearby Farpoint Station to prove their worthiness to be spared. After successfully solving the mystery and avoiding disaster, the crew departs on its mission to explore strange new worlds.The series begins with the crew of the Enterprise-D put on trial by an omnipotent being known as Q, who became a recurring character. The god-like entity threatens the extinction of humanity for being a race of savages, forcing them to solve a mystery at nearby Farpoint Station to prove their worthiness to be spared. After successfully solving the mystery and avoiding disaster, the crew departs on its mission to explore strange new worlds."
print(w2v_tokenize_text(tng_plot))
['series', 'begins', 'crew', 'Enterprise-D', 'put', 'trial', 'omnipotent', 'known', 'became', 'recurring', 'character', 'god-like', 'entity', 'threatens', 'extinction', 'humanity', 'race', 'savages', 'forcing', 'solve', 'mystery', 'nearby', 'Farpoint', 'Station', 'prove', 'worthiness', 'spared', 'successfully', 'solving', 'mystery', 'avoiding', 'disaster', 'crew', 'departs', 'mission', 'explore', 'strange', 'new', 'worlds.The', 'series', 'begins', 'crew', 'Enterprise-D', 'put', 'trial', 'omnipotent', 'known', 'became', 'recurring', 'character', 'god-like', 'entity', 'threatens', 'extinction', 'humanity', 'race', 'savages', 'forcing', 'solve', 'mystery', 'nearby', 'Farpoint', 'Station', 'prove', 'worthiness', 'spared', 'successfully', 'solving', 'mystery', 'avoiding', 'disaster', 'crew', 'departs', 'mission', 'explore', 'strange', 'new', 'worlds']
[nltk_data] Downloading package stopwords to
[nltk_data] /Users/tyamamiya/nltk_data...
[nltk_data] Package stopwords is already up-to-date!
次に、単語ごとの word2vec の特徴ベクトルを get_vector で求めて平均化します。wv[単語]
ではなく wv.get_vector(単語, norm=True)
を使うのは、正規化したベクトルを使うからです。平均したあとまた最後に gensim.matutils.unitvec で正規化しておきます。
def averaging(wv, words):
mean = np.mean([wv.get_vector(word, norm=True)
for word
in words
if word in wv], axis=0)
return unitvec(mean)
def averaging_text(wv, text):
return averaging(wv, w2v_tokenize_text(text))
確認のため、得られた文章の特徴ベクトルがどんな単語に近いか見てみます。ピンと来ますでしょうか? ここで、restrict_vocab オプションを使って検索対象を絞るのがコツです。例えば restrict_vocab=100000 とすると、出現頻度の高い上位 100000 から検索するそうです。これ無しではモデルによってはゴミのような結果も検出されてしまいます。
wv.similar_by_vector(averaging_text(wv, tng_plot), restrict_vocab=100000, topn=20)
[('mystery', 0.4961079955101013),
('mysterious', 0.4670897126197815),
('fictional', 0.4605964720249176),
('supernatural', 0.448750764131546),
('confront', 0.4461035430431366),
('sort', 0.44454988837242126),
('solve', 0.4442543089389801),
('character', 0.44423702359199524),
('mysteries', 0.4432906210422516),
('solving', 0.43936479091644287),
('confronts', 0.43834561109542847),
('villain', 0.43776965141296387),
('humanity', 0.4373060464859009),
('mythical', 0.43303632736206055),
('peculiar', 0.432568222284317),
('revolves_around', 0.42918798327445984),
('alien', 0.42697280645370483),
('confronting', 0.4260649085044861),
('vanishes', 0.4246973693370819),
('villains', 0.4238564074039459)]
それではこの方法で教師データとテストデータから特徴ベクトルを作ります。
%%time
def averaging_list(wv, text_list):
return [averaging_text(wv, text) for text in text_list]
X_train_word_average = averaging_list(wv, train_data["plot"])
X_test_word_average = averaging_list(wv, test_data["plot"])
CPU times: user 18.2 s, sys: 4.89 s, total: 23.1 s
Wall time: 22.9 s
%%time
logreg = linear_model.LogisticRegression(n_jobs=1, C=1e5, max_iter=2000)
logreg.fit(X_train_word_average, train_data['tag'])
predicted = logreg.predict(X_test_word_average)
evaluate_prediction(predicted, test_data.tag)
accuracy 0.5349794238683128
confusion matrix
[[24 1 11 1 1 4]
[ 2 11 6 6 4 2]
[ 6 6 57 3 12 2]
[ 2 3 1 6 1 3]
[ 4 1 13 0 14 3]
[ 7 2 3 2 1 18]]
(row=expected, col=predicted)
CPU times: user 9.79 s, sys: 1.36 s, total: 11.2 s
Wall time: 1.44 s
単語ごとの特徴ベクトルを単に平均するという乱暴な方法でも今までで一番高い 53% が出ました。
確認のため、テスト用の粗筋と特徴ベクトル近辺の単語を見てみます。
def examine_word2vec(text):
print(text)
vector = averaging_text(wv, text)
pprint(wv.similar_by_vector(vector, restrict_vocab=100000, topn=20))
examine_word2vec(test_data.iloc[56]['plot'])
Scruffy but irresistibly attractive Yau Muk-yan, without a job or a place to live, moves in with sensitive, shy piano tuner Chan Kar-fu. Both are disturbed, then obsessed, by the amateurish piano playing of upstairs neighbour Mok Man-yee. Obsession turns to romance, and romance to fantasy. The film is structured in four "movements": two themes (Yau Muk-yan, Mok Man-yee), a duet (Yau Muk-yan & Mok Man-yee), and a set of variations (a wild fantasy of Chan Kar-fu in his new novel).
[('dreamy', 0.5190651416778564),
('Chan', 0.5157312750816345),
('Cheung', 0.5124940872192383),
('romance', 0.5102624893188477),
('romantic', 0.4882933795452118),
('Wong', 0.47440242767333984),
('sexy', 0.4723252058029175),
('neurotic', 0.47144070267677307),
('Leung', 0.468058705329895),
('moody', 0.46737581491470337),
('weird', 0.4657338857650757),
('Cheng', 0.4650360941886902),
('Yuen', 0.45778849720954895),
('vamp', 0.45643094182014465),
('Chow', 0.45602890849113464),
('Yu', 0.4541877210140228),
('By_TBT_staff', 0.45398378372192383),
('Kwok', 0.45246854424476624),
('Chang', 0.45167356729507446),
('funny', 0.4502175748348236)]
Doc2Vec
次に、Word2Vec を進化させた Doc2Vec というのを使ってみます。Word2Vec が単語の特徴ベクトルを作るのに対して、Doc2Vec は文章の特徴ベクトルを作る事が出来るそうです。
from gensim.models import Doc2Vec
from gensim.models.doc2vec import TaggedDocument
Doc2Vec の入力として、文章に tag という特徴を表す単語を与えます。ここでは映画のジャンルをそのまま与えます。すると TaggedDocument が出来ます。
%%time
train_tagged = [TaggedDocument(words=w2v_tokenize_text(line.plot), tags=[line.tag])
for line
in train_data.itertuples()]
test_tagged = [TaggedDocument(words=w2v_tokenize_text(line.plot), tags=[line.tag])
for line
in test_data.itertuples()]
CPU times: user 17.5 s, sys: 4.72 s, total: 22.2 s
Wall time: 22.1 s
TaggedDocument というのは単語のリストに tag をつけただけです。
test_tagged[56]
TaggedDocument(words=['Scruffy', 'irresistibly', 'attractive', 'Yau', 'Muk-yan', 'without', 'job', 'place', 'live', 'moves', 'sensitive', 'shy', 'piano', 'tuner', 'Chan', 'Kar-fu', 'disturbed', 'obsessed', 'amateurish', 'piano', 'playing', 'upstairs', 'neighbour', 'Mok', 'Man-yee', 'Obsession', 'turns', 'romance', 'romance', 'fantasy', 'film', 'structured', 'four', '``', 'movements', "''", 'two', 'themes', 'Yau', 'Muk-yan', 'Mok', 'Man-yee', 'duet', 'Yau', 'Muk-yan', 'Mok', 'Man-yee', 'set', 'variations', 'wild', 'fantasy', 'Chan', 'Kar-fu', 'new', 'novel'], tags=['romance'])
Doc2Vec() でモデルを作成します。
doc2vec_model = Doc2Vec(train_tagged)
各レビュー文章ごとの特徴ベクトルを取得します。
train_regressors = [doc2vec_model.infer_vector(doc.words) for doc in train_tagged]
テストデータの特徴ベクトルも同様に取得します。
test_regressors = [doc2vec_model.infer_vector(doc.words) for doc in test_tagged]
前と同じように教師データの特徴ベクトルを LogicticRegression に与えて予測器を作り、テストデータで精度を見てみましょう。
logreg = linear_model.LogisticRegression(n_jobs=-1, C=1e5, max_iter=4000)
logreg.fit(train_regressors, train_data["tag"])
evaluate_prediction(logreg.predict(test_regressors), test_data["tag"], title=str(doc2vec_model))
accuracy 0.4567901234567901
confusion matrix
[[26 3 8 1 1 3]
[ 5 9 8 4 4 1]
[ 8 7 47 1 18 5]
[ 4 2 3 2 4 1]
[ 4 2 16 1 9 3]
[ 7 2 1 5 0 18]]
(row=expected, col=predicted)
実行ごとに変わりますが、52% 程度の精度になる事があります。
それぞれのジャンルに割り当てられた特徴ベクトルの周囲の単語を取り出してみます。結構納得の単語が周囲にある事が分かります。
{tag: doc2vec_model.wv.most_similar([doc2vec_model.dv[tag]], topn=5) for tag in train_data.tag.unique()}
{'comedy': [('Hills', 0.9796565771102905),
('dumped', 0.9765863418579102),
('high', 0.9746807217597961),
('manager', 0.9728865623474121),
('clerk', 0.9718594551086426)],
'romance': [('love', 0.9938552975654602),
('falls', 0.9935888648033142),
('fall', 0.9930077791213989),
('falling', 0.9918095469474792),
('firm', 0.9858505725860596)],
'animation': [('princess', 0.9916613101959229),
('fairy', 0.9882818460464478),
('kingdom', 0.9866940975189209),
('dragon', 0.9830360412597656),
('castle', 0.9815301299095154)],
'action': [('CIA', 0.9886765480041504),
('lord', 0.9840854406356812),
('federal', 0.9802117347717285),
('United', 0.9752975702285767),
('Special', 0.9736286997795105)],
'sci-fi': [('planet', 0.9937312006950378),
('alien', 0.9913865923881531),
('Earth', 0.9910171627998352),
('distant', 0.9907531142234802),
('ship', 0.9902650713920593)],
'fantasy': [('dragon', 0.9933109879493713),
('baby', 0.9925127625465393),
('Willow', 0.9916751980781555),
('princess', 0.9912750720977783),
('Princess', 0.9907354116439819)]}
特に Word2Vec に比べて滅茶苦茶良いというわけでもなく、私のやり方が悪いのかもしれませんがここまでとします。