単語以外の分散表現をword2vecで取得しRNNのEmbedding層に組み込むまで

## 本記事でやること

- 購買履歴データから商品アイテムの分散表現をword2vecを使って取得する。
- 特定商品に似ているアイテムを表示する。
- 商品アイテムの分散表現をRNN(リカレントニューラルネットワーク)のEmbedding層に利用する。

本記事でやらないことは以下の通りなので、他の記事を参照してください。

- Pythonの各ライブラリー（gensimなど）のinstall方法

## 対象読者

- Python入門者
- RNNを学び始めた人

## 使用言語及びフレームワーク

- Python 3.6.3
- keras 2.2.2

## データセットの用意

今回使用するのはUCIで用意されている[購買データ](http://archive.ics.uci.edu/ml/datasets/online+retail)になります。
またUCIのデータをユーザー単位で買った順に商品IDで並べたデータを[こちら](https://github.com/tizuo/keras/blob/master/is_future_purchase.ipynb)から拝借しましたので`git clone`でローカル環境にダウンロードしてください。
データは以下のような形になっています。

<img width="1079" alt="スクリーンショット 2019-01-13 16.42.36.png" src="https://qiita-image-store.s3.amazonaws.com/0/270991/ca9e7262-5349-a0cf-2cbd-34c504dcb324.png">

## word2vecで商品の分散表現を取得する

```python:jupyernotebook
import numpy as np
import pandas as pd

# データの読み込み
data_path = ! echo file://$PWD/keras/purchacedata_base.csv
raw_data = pd.read_csv(data_path[0], header = 0)

max_length = 10 # 購入件数を指定
# 購入件数を限定し、Naがあるレコードを削除したデータフレームを取得
df = raw_data[raw_data.columns[0:(max_length + 2)]].dropna().astype(int)
df.head()
```

<img width="785" alt="スクリーンショット 2019-01-13 16.47.12.png" src="https://qiita-image-store.s3.amazonaws.com/0/270991/2d2ae77b-236c-eb30-bb4c-51bc9a7b9365.png">

わかち書きをしたように、1つのカラムに商品id同士を空白で区切った状態にしたいので以下のような処理を行います。

```python:jupyternotebook

df['items'] = df['P1'].astype(str)
for i in range(2, 11):
    df['items'] += ' '+ df['P'+str(i)].astype(str)
```

<img width="1024" alt="スクリーンショット 2019-01-13 16.51.46.png" src="https://qiita-image-store.s3.amazonaws.com/0/270991/f7b839d8-ad5a-3d5b-c68d-12310b2b8c72.png">

一旦、ユーザー単位でわかち書きのような処理を行ったカラムのみをtext形式でダウンロードします。

```python:jupyternotebook
df["items"].to_csv("data.txt", header = False, index=False)
```

そして、ダウンロードしたtextを読み込み以下のような方法でword2vecで分散表現を取得します。
`word2vec.Word2Vec`で分散表現を取得しているのですが、引数の`size`や`window`、`min_count`は[公式のドキュメント](https://radimrehurek.com/gensim/models/word2vec.html)を参照してください。

```python:jupyternotebook
from gensim.models import word2vec

text_path = ! echo file://$PWD/data.txt

sentences = word2vec.Text8Corpus(text_path[0])
model = word2vec.Word2Vec(sentences, size=100, window=1, min_count=1)
model.save("item.model")
```

## 似ている商品を表示

modelには`most_similar`という関数が用意しているので引数に商品のIDを入れるだけでokです。

```python:jupyternotebook
# 商品ID = 117と似ている商品を表示
model.most_similar("117")
```

```text:出力
[('2', 0.925855815410614),
 ('1', 0.9179891347885132),
 ('4', 0.9112440943717957),
 ('6', 0.9003599882125854),
 ('14', 0.8991273045539856),
 ('31', 0.8983150124549866),
 ('10', 0.8967700600624084),
 ('13', 0.895893931388855),
 ('41', 0.8949518799781799),
 ('7', 0.8913787603378296)]
```

商品ID=2や商品ID=1が全体の購買履歴の中で頻度が多いので、その影響が出てしまっていると思われます。
一応、ここまでで各商品の分散表現を取得出来ました。

## RNNのEmbedding層に組み込む

Embedding層の重みに上記で作成した単語の分散表現を利用します。
まずは、商品IDに対応するIndexを作成します。のちにこのIndexを利用してEmbedding層の空の重みに商品ID毎の分散表現を順に入力をさせていきます。

```python:jupyternotebook
# kerasのTokemizerを利用します
from keras.preprocessing.text import Tokenizer

tokenizer = Tokenizer()
tokenizer.fit_on_texts(df["items"])
# 商品idをindexに変換する
word_index = tokenizer.word_index
print(word_index)
```

以下のようなディクショナリー形式で{'商品ID', Index}が得られます。

```text:出力
{'552': 652, '910': 450, '2538': 2410, '2394': 2074, '2359': 1827, '962': 1173,...}
```

そして、以下の処理でEmbedding層の空の重みに分散表現を順に入力させていきます。

- `embedding_matrix`は商品数+1個の行と分散表現の取得時に設定した次元数の大きさになります。（要素はゼロで埋めておきます）
- `word_index.items()`で単語とIndexを順に取得し、`model.wv[word]`で取得した分散表現を`embedding_matrix`に入れていきます。

```python:jupyternotebook
embedding_matrix = np.zeros((len(word_index)+1, 100))
for word, i in word_index.items():
    embedding_matrix[i] = model.wv[word]
```

上記でEmbedding層の重みに分散表現を組み込むことが出来ました。

あとは、モデルを組み立てていくだけですが、`model.add(Embedding())`の引数`weights`で上記で定義した分散表現を設定し、Embedding層の重みは訓練しないので、`trainable`は`False`にするので注意してください。


```python:jupyternotebook
from keras.models import Sequential 
from keras.layers import Embedding, SimpleRNN, Dense

model = Sequential()
model.add(Embedding(len(word_index)+1,
                    100,
                    weights=[embedding_matrix],
                    trainable=False))
model.add(SimpleRNN(100))
model.add(Dense(1, activation='sigmoid'))
model.compile(loss='mean_squared_error', optimizer='sgd', metrics=["acc"])

```


## 終わりに
時間がある時にSageMakerのBlazingTextやSparkでの分散表現の取得に試みたいと思います。