oryou-sanのブログ

備忘録です

Word2Vecの出力をMDSで解釈してみる

はじめに

前回紹介したWord2Vecを用いた文章の分散表現をMDSで次元削減し視覚的に解釈可能にしてみます。
oryou-san.hatenablog.com


前処理

scikit-learnのdataset"fetch_20newsgroups"を学習用データとして用います。
Word2Vecの入力に適するように文章を単語毎に区切られたリストに変換し、記号は空白に置換します。

from sklearn.datasets import fetch_20newsgroups

data = fetch_20newsgroups(remove=('headers', 'footers', 'quotes'))

texts = [txt.replace('.', '').replace(',', '').replace('/', '').split() for txt in data.data]
texts = [w for w in texts]

print(texts[0])

# ['I', 'was', 'wondering', 'if', 'anyone', 'out', 'there', 'could', 'enlighten', ... , 'e-mail']


学習(Word2Vec)

前処理を施したデータをモデルに投入します。

単語の類似度をどの程度学習できたかを確かめるのに'windows'という単語ベクトルとコサイン類似度の高い単語を降順で出力してみます。
PC関係の単語がずらずらと出力されてきたのでモデルの学習が正常に進んだことを確認できました。

from gensim.models import Word2Vec

model = Word2Vec(texts, min_count=1, seed=1)
key = 'windows'

for w in model.wv.most_similar(key, topn = 10):
    print(w)

# ('Windows', 0.936229407787323)
# ('drivers', 0.9341704845428467)
# ('memory', 0.9085516929626465)
# ('disks', 0.9075465798377991)
# ...
# ('bus', 0.8947420716285706)


単語の分散表現から文章トピックの分散表現を獲得

データセットに存在する単語の分散表現を獲得できたので、これを用いて文章トピックの分散表現を計算してみます。

以下の手順で計算しています。

1.文章に登場する単語のベクトルの算術平均を取り、これを文章の分散表現とする
2.トピックに登場する単語のベクトルの算術平均を取り、これをトピックの分散表現とする

import numpy as np

vec_news = [np.average([model.wv[w] for w in txt], axis = 0) if txt != [] else np.zeros(100) for txt in texts]
vec_topics = []

for i in range(20):
    tpc_i = [idx for idx,d in enumerate(data.target) if d == i]
    vec_topic = np.average(np.array(vec_news)[tpc_i], axis=0)
    vec_topics.append(vec_topic)

vec_topics = np.array(vec_topics)


学習(MDS)

いよいよMDSに学習データを投入します。
先ほど獲得したトピック毎の分散表現(トピック数(20) * ベクトル次元数(100))から各トピック間のL2ノルムを計算します(トピック数(20) * トピック数(20))。

L2ノルム計算にあたってはブロードキャスト処理を用いて効率化しております。
(記事末尾に記載のサイト様を参考にしました。)

diffs = np.expand_dims(vec_topics, axis=1) - np.expand_dims(vec_topics, axis=0)
dist = np.sqrt(np.sum(diffs ** 2, axis=-1))

from sklearn import manifold

mds = manifold.MDS(n_components=2, dissimilarity="precomputed", random_state=1)
pos = mds.fit_transform(dist)


結果を散布図にして出力

MDSにて2次元に圧縮されたトピックの分散表現を散布図に出力してみました。
トピックの大分類(7つ)を用いてドットを色分けしていますが、散布図上でも綺麗に色毎にまとまっているように見えます。

labels = data.target_names
label_big = [label[:(label.find('.'))] for label in labels]

%matplotlib inline
import matplotlib.pyplot as plt

x = pos[:,0]
y = pos[:,1]

for label in list(set(label_big)):
    label_idx = [i for i,l in enumerate(label_big) if l == label]
    plt.scatter(x[label_idx], y[label_idx], s=20)

plt.legend(list(set(label_big)))
plt.show()

f:id:oryou-san:20210627190730p:plain


参考

Pythonでxy座標上の2点間の距離をforループを使わずに計算する方法 | Shikoan's ML Blog

Word2Vecを使ってみる

はじめに

Python自然言語処理系のライブラリであるgensimでWord2Vecを使ってみます。


データ準備

Word2Vecにかけるデータを準備します。分かち書きされている文章をリストにまとめたものならOKです。
前回の記事で紹介したMeCabを使っています。

oryou-san.hatenablog.com

import MeCab
mecab = MeCab.Tagger("-Ochasen")

s1 = 'おいしいパスタつくったお前'
s2 = 'あなたを詐欺罪と器物損壊罪で訴えます'
s3 = 'その点トッポってすげぇよな'

data =[s1,s2,s3]

def wakachigaki_bow(s):
    wakachi = mecab.parse(s).split('\n')
    wakachi = [w.split('\t') for w in wakachi if len(w.split('\t')) > 1]
    wakachi = list(filter(lambda x: x[3][:2] == '名詞'
                      or x[3][:2] == '動詞' 
                      or x[3][:3] == '形容詞', wakachi))
    wakachi_base = [w[2] for w in wakachi]
    return wakachi_base

sentences = [wakachigaki_bow(s) for s in data]

print(sentences)

# [['おいしい', 'パスタ', 'つくる', 'お前'], ['あなた', '詐欺', '罪', '器物', '損壊', '罪', '訴える'], ['点', 'トッポ', 'すげる', 'ぇよな']]


早速まわしてみる

準備したデータをモデルにつっこみます。

from gensim.models import Word2Vec
model = Word2Vec(sentences, min_count=1)

print(model.wv['パスタ'])
print(model.wv.most_similar('パスタ', topn = 3))

# [ 9.7702928e-03  8.1651136e-03  1.2809705e-03  5.0975773e-03
#   1.4081288e-03 -6.4551616e-03 -1.4280510e-03  6.4491653e-03
#  -4.6173073e-03 -3.9930656e-03  4.9244044e-03  ...]

# [('ぇよな', 0.17272794246673584), ('点', 0.16694681346416473), ('器物', 0.11117953807115555)]

学習済みのモデルから試しに2つ出力してみました。

・単語の分散表現
.wv['key']で単語の分散表現を出力できます(デフォルトだと100次元)。

・類語
.wv.most_similar('key', topn = n)でkeyとなった単語の類語(上位n個)を出力できます。
コサイン類似度の高いものから順に単語を出力しているようです。


参考

models.word2vec – Word2vec embeddings — gensim

MeCabで分かち書き

はじめに

MeCabというライブラリを使って日本語の分かち書きをしてみます。


まずは使ってみる

出力はこんな感じになります。

s = 'おいしいパスタつくったお前'

import MeCab
mecab = MeCab.Tagger("-Ochasen")

print(mecab.parse(s))

# おいしい	オイシイ	おいしい	形容詞-自立	形容詞・イ段	基本形
# パスタ	パスタ	パスタ	名詞-一般		
# つくっ	ツクッ	つくる	動詞-自立	五段・ラ行	連用タ接続
# た	タ	た	助動詞	特殊・タ	基本形
# お前	オマエ	お前	名詞-代名詞-一般		
# EOS

出力は単語毎に'\n'(改行)で区切られ、文末には'EOS'が付されます。
各単語は原型・読み・品詞 などの情報が付加され、'\t'(タブ)で区切られています。


分かち書きの結果を出力

次に、品詞の情報などを取り除いて単純に分かち書きの結果を出力してみます。

wakachi = mecab.parse(s).split('\n')
wakachi = [w.split('\t') for w in wakachi if len(w.split('\t')) > 1]

wakachi_surface = [w[0] for w in wakachi]
wakachi_surface = ' '.join(wakachi_surface)
    
print(wakachi_surface)

# おいしい パスタ つくっ た お前

これを関数にして、複数文をforでとりこんでみます。

def wakachigaki(s):
    wakachi = mecab.parse(s).split('\n')
    wakachi = [w.split('\t') for w in wakachi if len(w.split('\t')) > 1]
    wakachi_surface = [w[0] for w in wakachi]
    wakachi_surface = ' '.join(wakachi_surface)
    return wakachi_surface

s1 = 'おいしいパスタつくったお前'
s2 = 'あなたを詐欺罪と器物損壊罪で訴えます'
s3 = 'その点トッポってすげぇよな'

data =[s1,s2,s3]

for s in data:
    print(wakachigaki(s))

# おいしい パスタ つくっ た お前
# あなた を 詐欺 罪 と 器物 損壊 罪 で 訴え ます
# その 点 トッポ って すげ ぇよな


分かち書きの結果を出力(応用編)

このままでも機械学習にかける前処理としては充分かもしれませんが、一工夫して以下の処理を加えてみます。

・活用が存在する品詞(形容詞、動詞)はその原型を出力とする
・出力は名詞・動詞・形容詞に絞る

def wakachigaki_bow(s):
    wakachi = mecab.parse(s).split('\n')
    wakachi = [w.split('\t') for w in wakachi if len(w.split('\t')) > 1]
    wakachi = list(filter(lambda x: x[3][:2] == '名詞'
                      or x[3][:2] == '動詞' 
                      or x[3][:3] == '形容詞', wakachi))

    wakachi_base = [w[2] for w in wakachi]
    wakachi_base = ' '.join(wakachi_base)
    return wakachi_base

for s in data:
    print(wakachigaki_bow(s))

# おいしい パスタ つくる お前
# あなた 詐欺 罪 器物 損壊 罪 訴える
# 点 トッポ すげる ぇよな

LDA(トピックモデル)を使ってみる

はじめに

LDAを試しに使ってみたので備忘として残します。
加藤公一氏著の機械学習図鑑からコードを拝借しました。


学習&分類

以下書籍からの引用です。(コードはgithubから落とせます。)
やってることは以下の通りです。

・scikit-learnのdataset"fetch_20newsgroups"よりサンプルデータを取得
・CountVectorizerにてサンプルデータの文章をベクトル化
・ベクトル化された文章をLDAで次元削減(サンプルデータのラベルが20種類なので、これに合わせて20次元に削減)

from sklearn.datasets import fetch_20newsgroups
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.decomposition import LatentDirichletAllocation
 
# removeで本文以外の情報を取り除く
data = fetch_20newsgroups(remove=('headers', 'footers', 'quotes'))

max_features = 1000
# 文書 データをベクトルに変換(n_data, max_features=1,000)
tf_vectorizer = CountVectorizer(max_features=max_features,
stop_words='english')
tf = tf_vectorizer.fit_transform(data.data)

n_topics = 20
model = LatentDirichletAllocation(n_components=n_topics)
model.fit(tf)

CountVectorizerクラスに設定されている引数2つは以下の通りです。

・max_features
文章をベクトル化する際に最頻n個の単語のみを考慮するように設定できます。
ここでは最頻1,000個の単語をベクトル化するように設定しています。

・stop_words
文章をベクトル化するにあたり除外すべき単語をここで指定します。
ここでは'english'となっていますがこれでbuilt-inの英語のstop-wordsが指定できます。
.get_stop_words()のメソッドで登録されている単語群が確認できます。

stop_words = tf_vectorizer.get_stop_words()
stop_words = [w for w in stop_words]

print(f'len:{len(stop_words)} \nword_list: {stop_words}')
# len:318 
# word_list: ['anyone', 'you', 'to', 'thereby', 'via', 'someone', 'seeming', 'find', 'least', 'besides', 'now', 'thick', 'itself', 'own', 'back', 'latter', 'became', 'take', 'name', 'moreover', 'due', 'could', .....


結果

書籍だとトピック毎に頻出する単語をリストにして結果を解釈しておりましたので同じように結果を出力してみます。
やってることは以下の通りです。

・文章毎に削減した20次元のベクトル(トピック)のうち、一番値の大きいものを選ぶ(一番もっともらしいトピックを選ぶ)
・同じトピックに分類された文章毎に単語を頻出頻度順(降順)に出力

import numpy as np

# 文書データのベクトルを20次元に削減(n_data, n_topics=20)
tpc_cls = np.argmax(model.transform(tf), axis=1)

for a in range(20):
    tpc_id = [i for i,c in enumerate(tpc_cls) if c == a]
    tpc_stc = [s for i,s in enumerate(data.data) if i in tpc_id]
    tf_tpc = tf_vectorizer.fit_transform(tpc_stc)
    term_frequency = np.array(tf_tpc.sum(axis=0))[0]
    print(f'topic_{a+1}')
    print([tf_vectorizer.get_feature_names()[b] for b in term_frequency.argsort()[:-10:-1]])

出力はこんな感じになるはずです。

topic_1
['god', 'jesus', 'people', 'bible', 'christ', 'does', 'christian', 'believe', 'church']
topic_2
['don', 'know', 'think', 'people', 'does', 'just', 'like', 'say', 'god']
topic_3
['00', 'new', '50', 'sale', '10', 'dos', 'good', 'offer', '20']
topic_4
['president', 'mr', 'stephanopoulos', 'people', 'think', 'going', 'know', 'don', 'jobs']
topic_5
['entry', 'file', 'cx', 'output', 'w7', 'program', 'c_', 'chz', 't7']
topic_6
['key', 'encryption', 'chip', 'use', 'clipper', 'keys', 'privacy', 'government', 'security']
topic_7
['pts', 'period', 'la', 'play', 'power', 'pp', '10', 'pt', 'scorer']
topic_8
['space', 'nasa', '1993', 'research', 'university', 'launch', 'program', 'information', 'use']
topic_9
['people', 'israel', 'government', 'right', 'israeli', 'state', 'war', 'don', 'jews']
topic_10
['said', 'people', 'know', 'didn', 'don', 'just', 'went', 'like', 'time']
topic_11
['edu', 'com', 'available', 'use', 'file', 'image', 'window', 'ftp', 'program']
topic_12
['10', '25', '55', '11', '12', '14', '15', '20', '16']
topic_13
['gun', 'file', 'guns', 'firearms', 'crime', 'control', 'law', '000', 'use']
topic_14
['scsi', 'use', 'bit', 'data', 'bus', 'ide', 'chip', 'like', 'does']
topic_15
['team', 'game', 'year', 'season', 'games', 'hockey', 'players', 'league', 'nhl']
topic_16
['drive', 'windows', 'card', 'use', 'thanks', 'disk', 'problem', 'know', 'does']
topic_17
['like', 'just', 'good', 'don', 'time', 'car', 'think', 'know', 've']
topic_18
['ax', 'max', 'g9v', 'b8f', 'a86', 'pl', '145', '1d9', '0t']
topic_19
['db', 'armenian', 'turkish', 'armenians', 'people', 'turkey', 'jews', 'turks', 'genocide']
topic_20
['people', 'just', 'like', 'think', 'don', 'time', 'post', 'know', 'does']

topic_1はキリスト教、topic_8は宇宙、といったところでしょうか。
反対にtopic_12,18, 20あたりは解釈が難しく、stop_wordsを見直すなどしてより精度の高い分類ができるような工夫が必要になりそうです。