単語分散表現 chiVe の活用方法 Part 2

**chiVeの活用方法シリーズ**
* [Part 1 「分散表現とは？chiVeとは？」](https://qiita.com/nishihara_d/items/99bc1a1185c2efd36d64)
* Part 2 「chiVeでできること（単語ベクトルの性質）」（本記事）
* [Part 3 「chiVeでできること（単語ベクトルで文書分類）」](https://qiita.com/nishihara_d/items/9e3967ad37780d93c7ac)
* [Part 4 「chiVeの追加学習」](https://qiita.com/nishihara_d/items/c2d43ec98242afe0d7b4)


## はじめに

2022年4月末に、[WAP NLP Tech Talk#5](https://worksapplications.connpass.com/event/244032/) にて、「単語分散表現 chiVeの活用方法」というタイトルでお話しをしました。
そのQiita版として、[スライド](https://speakerdeck.com/waptech/dan-yu-fen-san-biao-xian-chivefalsehuo-yong-fang-fa-at-wap-nlp-tech-talk-number-5) の内容を補足しながら4回に分けて紹介します。

[chiVe](https://github.com/WorksApplications/chiVe) とは、日本語単語分散表現です。分散表現の基本事項や、chiVe の特長については、前回の [Part 1](https://qiita.com/nishihara_d/items/99bc1a1185c2efd36d64) で触れています。


:::note warn
本記事では、実際に chiVe を使ったソースコードが登場します。
chiVe のインストール方法は、[ドキュメント](https://github.com/WorksApplications/chiVe) に書かれています。本記事の一番最後でも紹介しておきますので、手元で動かす場合は、先にどちらかを参照してインストールしておいてください。
:::


## chiVeでできること
<!--
1. [単語の足し引き](#1-単語の足し引き)
1. [単語のキョリ](#1-単語の足し引き)
1. [単語の分類](#1-単語の足し引き)
1. [文ベクトル](#1-単語の足し引き)
1. [文書分類](#1-単語の足し引き)
-->

### 0. 単語ベクトルの確認

単語分散表現は、単語という文字列を、単語ベクトルという数値表現に変えたものでした。
まずは、chiVe (v1.2-mc90) の中身を見てみましょう。

```py
import gensim

# chiVe (v1.2-mc90) の読み込み
chive = gensim.models.KeyedVectors.load('chive-1.2-mc90_gensim/chive-1.2-mc90.kv')

# 登録されている単語数
len(chive.vocab)
# >>> 482238

# ベクトルの次元数
chive.vector_size
# >>> 300

# 単語「父」のベクトルを確認
print(chive['父'])
# >>> [-0.08687506  0.03098334  0.12407982  0.08719584  0.17613985  0.37390327
#      -0.0206242   0.03894367 -0.42564225  0.15581265  0.15123358 -0.27710935
#      -0.3717159  -0.10732947  0.37858045 -0.19058919 -0.02951239  0.09886529
#        (長いので中略)
#      -0.04864032 -0.00600725  0.13924384 -0.07966962 -0.06504062  0.1642649 ]
```

:::note warn
chiVe に登録されている単語は、[正規化表記](https://github.com/WorksApplications/Sudachi#%E8%A1%A8%E8%A8%98%E6%AD%A3%E8%A6%8F%E5%8C%96)です。表層形では KeyError になりますので注意してください。

<details>
<summary>例：「スダチ」の正規化表記は「酢橘」</summary>

```py
print(chive['スダチ'])
# >>> KeyError: "word 'スダチ' not in vocabulary"

print(chive['すだち'])
# >>> KeyError: "word 'すだち' not in vocabulary"

print(chive['酢橘'])
# >>> [ 1.76016882e-01  6.98166564e-02 -1.31085247e-01 -5.59600532e-01
#       1.22947872e-01  1.60852432e-01  1.21640936e-01  2.50322729e-01
#       5.36368489e-02 -4.19754624e-01 -2.62630999e-01  3.48565847e-01
#        (長いので中略)
#      -3.47189873e-01 -1.05501451e-01 -2.38098979e-01 -4.38676141e-02]
```

</details>
<details>
<summary>正規化表記の確認方法</summary>

```bash
$ pip install sudachipy sudachidict_core
```
```py
from sudachipy import dictionary
tokenizer = dictionary.Dictionary().create()
[m.normalized_form() for m in tokenizer.tokenize('スダチ')]
# >>> ['酢橘']
```

</details>

:::

### 1. 単語の足し引き

単語ベクトルという数値表現に変換したおかげで、コンピュータ上で「足し引き」を考えることができます。

#### 問1　「父」－「男性」＋「女性」＝「母」

「父」ベクトルから、「男性」ベクトルを引いて、「女性」ベクトルを足すと、「母」ベクトルが得られます。

```py
chive.most_similar(positive=['父', '女性'], negative=['男性'], topn=1)
# >>> [('母', 0.8106178045272827)]
```
これは、「父」の中の「男性」という部分を「女性」に変えると「母」になるという、直感的な意味の足し引きに近い結果ですね。[^1]

[^1]: 性の多様性の観点からは、この限りではないかもしれませんが、ここでは単語分散表現の分かりやすい例として挙げています。

:::note
$A-B+C=D$ というのは、「$B$ に対する $A$ の関係が、$C$ に対する $D$ である」の $D$ を推測しようとしています。このようなタスクを **アナロジータスク** と呼びます。
アナロジータスクは、単語分散表現の性能を評価する方法のひとつとして使われています。

なお、等号「$=$」を用いていますが、実際に数値が等しいわけではありません。
$A-B+C$ の結果に最も近いものが $D$ であるとき、ここでは $A-B+C=D$ と表記しています。[^2]
[^2]: 実際、上記の結果を見ると、問1で得られるベクトルと「母」ベクトルの類似度は、0.8106178045272827 となっており、1ではありません。
:::

#### 問2　「横浜市」－「神奈川県」＋「北海道」＝「札幌市」

```py
chive.most_similar(positive=['横浜市', '北海道'], negative=['神奈川県'], topn=1)
# >>> [('札幌市', 0.740805447101593)]
```

chiVe は、「神奈川県」の県庁所在地が「横浜市」、「北海道」の道庁所在地が「札幌市」という対応を上手く捉えられているようです。


#### 他にも

他にも、直感とよく合っている例が沢山見つかります。

* 「大統領」　　－「アメリカ」　＋「日本」　＝「首相」
* 「冬」　　　　－「寒い」　　　＋「暑い」　＝「夏」
* 「韓国」　　　－「ウォン」　　＋「円」　　＝「日本」
* 「エビフライ」－「エビ（蝦）」＋「豚」　　＝「豚カツ」
* 「トランプ」　－「洋風」　　　＋「和風」　＝「花札」
* 「スペイン」　－「マドリード」＋「アテネ」＝「ギリシャ」

単語分散表現のアナロジータスクの面白さを、感じられたでしょうか？

<details>
<summary>（補足）「足し引き」は、何に役立つのか？</summary>

:::note
実生活では「横浜市」－「神奈川県」＋「北海道」のようなアナロジータスクを考える機会は、あまりありませんね。ビジネスの実用面でもこの結果を直接使う場面は、あまり無いでしょう。ひとつの遊びです。

ここでは、人間にとって直感的な "意味の" 足し引きと、コンピュータ上での "数値の" 足し引きの間に、（なんとなく）関連がありそうだという雰囲気を感じてください。
ベクトルとして演算できること自体が、この後のタスクで必要になります。
:::

</details>


### 2. 単語間の類似度の推定

:::note warn
[スライド](https://speakerdeck.com/waptech/dan-yu-fen-san-biao-xian-chivefalsehuo-yong-fang-fa-at-wap-nlp-tech-talk-number-5?slide=12)中では、(キョリ)$=1-$(類似度) と定義して、「キョリ」という単語を使っていました。表現が異なるのみで、説明の本質的な内容は同じです。
:::

次に、2単語間の類似度というものを考えます。
例えば、「犬、猫、柚子、カボス、スダチ」の5単語のベクトルの位置を図示すると、次のようになります。数値は、2単語間の類似度を表しています。

<img width="192" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2513839/8a92025d-d2d1-f79a-ec2c-6cedbad0af05.png">

「柚子」と「猫」の類似度 0.27 に比べて、「犬」と「猫」の類似度は 0.74 と高くなっています。意味が近い単語や関連する単語ほど、類似度が高くなりそうですね。

単語分散表現はベクトルですから、数学的に類似度を計算できます。よく使うのは、[コサイン類似度](https://www.google.com/search?q=%E3%82%B3%E3%82%B5%E3%82%A4%E3%83%B3%E9%A1%9E%E4%BC%BC%E5%BA%A6)です。

:::note
コサイン類似度とは？
2つのベクトルの類似度を求める方法の一つであり、最小値は $-1$ で、最大値は $1$ です。2つのベクトル（の向き）が同じだった場合、コサイン類似度は $1$ になります。

<details>
<summary>定義</summary>
次元数が同じ2つのベクトル $\boldsymbol{x}, \boldsymbol{y}$ のコサイン類似度は、それらのなす角を $\theta$ とすると、 
$\cos\theta=\displaystyle\frac{\boldsymbol{x}\cdot\boldsymbol{y}}{\|\boldsymbol{x}\|\cdot\|\boldsymbol{y}\|}$
で求められます。
</details>

<details>
<summary>python でコサイン類似度を計算する方法</summary>

* [sklearn.metrics.pairwise.cosine_similarity](https://scikit-learn.org/stable/modules/generated/sklearn.metrics.pairwise.cosine_similarity.html) を使う方法
`$ pip install scikit-learn` で scikit-learn をインストールして使う（本記事はこちらを使っています）

* numpy を使う方法
`$ pip install numpy` でインストールし、次のようなメソッドを作る
```py
import numpy as np
def cosine_similarity(x, y):
    return np.dot(x, y) / (np.linalg.norm(x) * np.linalg.norm(y))
```
* [scipy.spatial.distance.cosine](https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.distance.cosine.html) を使う方法
```py
from scipy.spatial.distance import cosine as cosine_distance
def cosine_similarity(x, y):
    return 1. - cosine_distance(x, y)
```
* 自作する方法（エラー処理省略）
```py
def my_cosine_similarity(x, y):
    dot_of_x_and_y = sum(a * b for a, b in zip(x, y))
    if dot_of_x_and_y == 0:
        return 0
    norm_of_x = sum(e**2 for e in x)**0.5
    norm_of_y = sum(e**2 for e in y)**0.5
    return dot_of_x_and_y / (norm_of_x * norm_of_y)
```

</details>

:::

```py
from sklearn.metrics.pairwise import cosine_similarity

# 「犬」と「猫」の類似度
print(cosine_similarity([chive['犬']], [chive['猫']]))
# >>> [[0.73851484]]

# 「猫」と「柚子」の類似度
print(cosine_similarity([chive['猫']], [chive['柚子']]))
# >>> [[0.26728642]]

#「柚子」と「スダチ（酢橘）」の類似度
print(cosine_similarity([chive['柚子']], [chive['酢橘']]))
# >>> [[0.6755279]]
```

また逆に、ある単語に対して類似度が高い単語を探すこともできます。例えば、「スダチ（酢橘）」に近い単語トップ10は、次の通りです。

```py
# スダチ（酢橘）に近い上位10単語を取り出す
chive.most_similar('酢橘', topn=10)
```

```
[('臭橙', 0.7334902286529541),
 ('レモン', 0.7124131321907043),
 ('青紫蘇', 0.6865516901016235),
 ('シークワーサー', 0.6774625182151794),
 ('柚子', 0.6755279302597046),
 ('ポン酢', 0.6577829122543335),
 ('柑橘', 0.6447854042053223),
 ('大根下ろし', 0.6406464576721191),
 ('蜜柑', 0.6371013522148132),
 ('日向夏', 0.6344248056411743)]
```

直感通り、カボス（臭橙）、レモン、シークワーサー、柚子などの柑橘類が上位に来ています。
一方、青紫蘇、ポン酢、大根下ろしなどのように、柑橘類ではない単語も入っていますね。スダチは魚の付け合わせの薬味として食べることが多い食材ですので、実はこれも直感に反さない結果です。
（柑橘類の中でも、ミカンやグレープフルーツなどの果物よりも、カボス（臭橙）などの薬味として使われるような果実の方が、スダチ（酢橘）に近い結果となっていますね。）

### 3. 単語のクラスタリング

単語の類似度が計算できると、似ている単語どうしでグループ化（クラスタリング）できます。

<img width="256" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2513839/52315c1c-2841-3b4b-1602-23f56873a427.png">

例えば
**Group1**: 「柚子」「スダチ」「カボス」
**Group2**: 「犬」「猫」
のように分けられそうですね。

[yamamomo](https://github.com/WorksApplications/yamaMomo) というデータセットを使うと、単語のグループ化の実験ができます。データセットの使い方は過去の記事にあります。

https://qiita.com/katsuta/items/302cfd67b20d5524e7c4#%E5%88%86%E9%87%8E%E6%83%85%E5%A0%B1%E3%81%AE%E8%A9%95%E4%BE%A1

今回はデータセット中の Concept Categorization を使います。これは、4つの単語（2カテゴリx2単語）を、同じカテゴリどうしで分ける実験です。

<details>
<summary>データセットの例</summary>

例1　アップデート（IT）、ウェブサイト（IT）、配置（建築）、レイアウト（建築）
例2　コピペ（IT）、コピーペースト（IT）、特会（ビジネス）、インターンシップ（ビジネス）

例えば、例1では、ITカテゴリの「アップデート」「ウェブサイト」、建築カテゴリの「配置」「レイアウト」の合計4単語がシャッフルされており、正しく2単語ずつに分けられるかどうかを試します。

:::note
2グループに分けるのみでカテゴリの予測はしませんので、アップデートとウェブサイトが同じグループであるとだけ分かれば良く、それらがITカテゴリであることは予測しなくてよいタスクとなります。
:::

</details>

先ほどの[過去記事](https://qiita.com/katsuta/items/302cfd67b20d5524e7c4#%E5%88%86%E9%87%8E%E6%83%85%E5%A0%B1%E3%81%AE%E8%A9%95%E4%BE%A1)では、朝日新聞ベクトルのみでの実験ですが、[野口ら (2022)](https://www.anlp.jp/proceedings/annual_meeting/2022/pdf_dir/PT2-3.pdf) の論文で chiVe も含めた実験をしていますので、その評価結果を引用します。
正しくクラスタリングできた割合として正解率を計算しています。chiVe が最も正しくクラスタリングできていますね。

| 単語分散表現                 | 正解率 |
|:----------------------------|:----:|
| chiVe (v1.2-mc90)           | 61.7 |
| chiVe (v1.2-mc15)           | 60.8 |
| chiVe (v1.2-mc5)            | 61.6 |
| 朝日新聞 (CBOW)              | 49.8 |
| 朝日新聞 (CBOW-retrofitting) | 52.6 |


## 今回のまとめ

chiVe でできることとして、今回は「単語」という単位に着目して紹介しました。

1. 単語の足し引き
1. 単語間の類似度の推定
1. 単語のクラスタリング

次回は、単語を組み合わせた「文」のベクトルについて考えます。

---

## 付録：chiVe のインストール

まずは、Python が使える環境を事前に整えてください。
chiVe は、[gensim](https://radimrehurek.com/gensim/) または [Magnitude](https://github.com/plasticityai/magnitude) を利用します。
本記事では、より広く使われている gensim を使って書いています。
後発の Magnitude は、gensim に比べて機能が多く、早く処理ができるケースがあるなどの利点がありますが、最初のインストールに時間がかかる、ライブラリの容量が大きいなどの欠点があります。

:::note warn
但し、chiVeを追加学習したい場合は、gensim を利用してください。
:::



### gensimをインストール

```sh
$ pip install gensim
```


### chiVe のダウンロードと動作確認

[一覧](https://github.com/WorksApplications/chiVe#%E3%83%87%E3%83%BC%E3%82%BF)から必要なモデルを選んでリンクをコピーし、ダウンロードします。

```sh
# ダウンロード　例：chiVe (v1.2-mc90)
$ wget https://sudachi.s3-ap-northeast-1.amazonaws.com/chive/chive-1.2-mc90_gensim.tar.gz

# 解凍
$ tar xzf chive-1.2-mc90_gensim.tar.gz
```

pythonで動作確認できれば完了です。
```py
import gensim
chive = gensim.models.KeyedVectors.load('chive-1.2-mc90_gensim/chive-1.2-mc90.kv')
chive.most_similar(['酢橘'], topn=5)
```

```
[('臭橙', 0.7334902286529541),
 ('レモン', 0.7124130725860596),
 ('青紫蘇', 0.6865517497062683),
 ('シークワーサー', 0.6774625778198242),
 ('柚子', 0.6755279898643494)]
```


### フルモデルを使う場合

追加学習を行うには、[フルモデル](https://github.com/WorksApplications/chiVe#%E8%BF%BD%E5%8A%A0%E5%AD%A6%E7%BF%92%E7%94%A8%E3%81%AE%E3%83%95%E3%83%AB%E3%83%A2%E3%83%87%E3%83%AB) を使います。詳しくは、[GitHub](https://github.com/WorksApplications/chiVe/blob/master/docs/continue-training.md) に書かれています。