Embeddingとは、端的に言うとWord2Vecのようにベクトル化することです。
Word2Vecは単語のベクトル化ですが、この記事ではQiitaのタグのベクトル化を目指します。
Word2Vecを計算するニューラルネットワークでは単語の順序を考慮したタスクを設定しますが、タグに順序はありませんので、今回は同じ記事に付けられる別のタグの出現確率を予測するタスクを設定します。
例えばある記事に付けられたタグが「JavaScript jQuery」であれば、「JavaScript」→「jQuery」、「jQuery」→「JavaScript」を予測することになります。
ベクトル化できたら、次元圧縮して平面にプロットしてみたいと思います。
実装
Python+Jupyter Notebookで実装します。
Qiitaのタグを収集する
Qiita APIを使って、記事ごとのタグセットを取得します。Qiita APIの使い方については、他にわかりやすい記事がたくさんありますので割愛します。
今回使用したAPIは次の2つです。
- /api/v2/tags: タグ一覧を取得(上限10000件)
- /api/v2/tags/:tag_id/items: 指定されたタグが付けられた記事一覧を取得(タグごとに上限10000件)
取得したデータは次のような構造です。
id | tags |
---|---|
0000273579c09aef1fe5 | JavaScript jQuery |
00004684d3b597a2e7ea | vagrant docker etcd CoreOS fleet |
00006bb84ee687cccb1a | zabbix zabbix3.4 |
取得した記事は353011件、タグは39191種類でした。
タグをEmbeddingする
早速ですが、ニューラルネットワークはこんな感じになります。
今回はEmbeddingの次元数を100とします。画像左端のXが入力、画像右端のyがターゲット(同じ記事の別のタグ)です。
ちなみに画像はMicrosoft Office Lensで撮影しました。きれいに台形補正してくれるので気に入ってます。
タグをインデックスに変換する
タグをインデックスに変換する必要があります。次のようなイメージです。
# 変換前
['JavaScript jQuery', 'vagrant docker etcd CoreOS fleet', 'zabbix zabbix3.4', …]
# 変換後
[[2, 34], [33, 3780, 12, 1654, 382], [182, 4855], …]
keras.preprocessing.text.Tokenizer
を使います。便利ですね。
from keras.preprocessing.text import Tokenizer
tokenizer = Tokenizer(filters='')
tokenizer.fit_on_texts(tags)
sequences = tokenizer.texts_to_sequences(tags)
インデックスは1から始まります。0はパディングのための特別値として扱うことがある1ため、空けておきます。今回は使いません。
tokenizer.word_index
{'python': 1,
'javascript': 2,
'ruby': 3,
'php': 4,
'ios': 5,
…
Xとyを作る
画像両端のXとyを作ります。
タグが1つしかない記事はペアが作れないので除去します。
sequences = [s for s in sequences if len(s) > 1]
次にちょっとした小技を使います。Xとyを作るまでの流れは次のイメージです。
# 1. 記事ごとにタグのペア(順列)を作る
[[[2, 34], [34, 2]], [[33, 3780], [33, 12], [33, 1654], [33, 382], [3780, 33], …]
# 2. 1つのリストに繋げる
[[2, 34], [34, 2], [33, 3780], [33, 12], [33, 1654], [33, 382], [3780, 33], …]
# 3. 行と列を入れ替える
[[2, 34, 33, 33, 33, 33, 3780, …], [34, 2, 3780, 12, 1654, 382, 33, …]]
itertools
を使うと、1行で次のように書けます。
import itertools
X, y = zip(*itertools.chain.from_iterable(itertools.permutations(s, 2) for s in sequences))
最後にXはndarrayに、
import numpy as np
X = np.asarray(X)
yはOne-hot表現に変換します。タグの種類が多いので、疎行列で返してくれるsklearn.preprocessing.OneHotEncoder
を使います。
from sklearn.preprocessing import OneHotEncoder
y = OneHotEncoder().fit_transform([[y_] for y_ in y])
shapeを見ると、
X.shape, y.shape
((2000836,), (2000836, 39167))
データは2000836件でした。タグが1つしかない記事を除去したので、タグが39191種類から39167種類に減ってますね。
ニューラルネットワークを構築する
Kerasで記述すると次のようになります。
from keras.layers import *
from keras.models import Model
input_1 = Input(shape=(1,))
embedding_1 = Embedding(39191+1, 100, input_length=1)(input_1)
x = Flatten()(embedding_1)
x = Dense(y.shape[1], activation='softmax')(x)
model = Model(inputs=[input_1], outputs=[x])
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['categorical_accuracy'])
model.summary()
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_1 (InputLayer) (None, 1) 0
_________________________________________________________________
embedding_1 (Embedding) (None, 1, 100) 3919200
_________________________________________________________________
flatten_1 (Flatten) (None, 100) 0
_________________________________________________________________
dense_1 (Dense) (None, 39167) 3955867
=================================================================
Total params: 7,875,067
Trainable params: 7,875,067
Non-trainable params: 0
_________________________________________________________________
Embeddingレイヤーで39191+1
とするのは、インデックスが1から始まるからです。
学習する
適当に10epochsほど学習させます。未知データを予測するタスクではないのでvalidationは不要です。
from keras.callbacks import *
history = model.fit(X, y_encoded, batch_size=1024, epochs=10, shuffle=True)
Epoch 1/10
2000836/2000836 [==============================] - 781s 391us/step - loss: 7.2171 - categorical_accuracy: 0.1012
Epoch 2/10
2000836/2000836 [==============================] - 782s 391us/step - loss: 6.0863 - categorical_accuracy: 0.1335
Epoch 3/10
2000836/2000836 [==============================] - 794s 397us/step - loss: 5.7346 - categorical_accuracy: 0.1435
Epoch 4/10
2000836/2000836 [==============================] - 794s 397us/step - loss: 5.5185 - categorical_accuracy: 0.1487
Epoch 5/10
2000836/2000836 [==============================] - 787s 393us/step - loss: 5.3642 - categorical_accuracy: 0.1519
Epoch 6/10
2000836/2000836 [==============================] - 793s 396us/step - loss: 5.2446 - categorical_accuracy: 0.1540
Epoch 7/10
2000836/2000836 [==============================] - 791s 395us/step - loss: 5.1477 - categorical_accuracy: 0.1553
Epoch 8/10
2000836/2000836 [==============================] - 792s 396us/step - loss: 5.0672 - categorical_accuracy: 0.1560
Epoch 9/10
2000836/2000836 [==============================] - 792s 396us/step - loss: 4.9997 - categorical_accuracy: 0.1565
Epoch 10/10
2000836/2000836 [==============================] - 793s 397us/step - loss: 4.9421 - categorical_accuracy: 0.1564
import matplotlib.pyplot as plt
fig, (axL, axR) = plt.subplots(ncols=2, figsize=(16, 5))
axL.plot(history.history['loss'])
axL.set_title('model loss')
axL.set_xlabel('epoch')
axL.set_ylabel('loss')
axR.plot(history.history['categorical_accuracy'])
axR.set_title('model accuracy')
axR.set_xlabel('epoch')
axR.set_ylabel('accuracy')
それっぽく学習が進んでいるようにも見えますが、accuracyがかなり低いのでダメな気がします…。
タグのベクトルを取得する
Embeddingレイヤーの重みがタグのベクトルになっています。インデックス0は使いませんので1以降を取得します。
vectors = model.get_weights()[0][1:]
可視化する
全てのタグをプロットするとこんな感じで潰れて見えなくなってしまうので、
一部のタグだけプロットします。今回はTIOBE Index for January 2019の人気プログラミング言語トップ20を見てみます。
tags = ['Java', 'C', 'Python', 'C++', 'VB.Net', 'JavaScript', 'C#', 'PHP', 'SQL', 'Objective-C', 'matlab', 'R', 'Perl', 'アセンブリ言語', 'Swift', 'Go', 'Pascal', 'Ruby', 'plsql', 'VisualBasic']
PCAで
PCAで100次元から2次元に圧縮します。
from sklearn.decomposition import PCA
vectors = PCA(n_components=2).fit_transform(vectors)
プロットするベクトルを取得します。
vectors = vectors[[tokenizer.word_index[k.lower()] for k in tags]]
プロットします。
%matplotlib inline
import matplotlib.pyplot as plt
from matplotlib import rcParams
rcParams['font.family'] = 'TakaoPGothic'
plt.figure(figsize=(10, 10))
plt.scatter(vectors[:, 0], vectors[:, 1])
for label, x, y in zip(tags, vectors[:, 0], vectors[:, 1]):
plt.annotate(label, xy=(x, y), xytext=(8, -4), textcoords='offset points', size=16)
plt.show()
あまりピンとこない位置関係になりました。
t-SNEで
t-SNEで試してみます。
from sklearn.manifold import TSNE
vectors = TSNE(n_components=2).fit_transform(vectors)
C#とVB.Netが近かったり、Rとmatlabが近かったり、Objective-CとSwiftが近かったり、こっちは一般的なイメージに近い位置関係になったのではないでしょうか?
終わり
今回の結果が統計的に有意であるか検証していないので、都合よく解釈しただけかもしれません。
この記事は、Netadashi Meetup #8のLT用に作成したものです。