名言ガチャ


こんにちは．神奈川県の大学に通っているmayukorinと申します．今日は，サポーターズの技育展に向けて開発をしていた「名言ガチャ」というアプリを紹介したいと思います．是非[こちら](https://rails-production-020e.up.railway.app/)から使ってみてください！


# 名言ガチャとは？
名言をガチャガチャで取り出せるアプリです．主に以下3つの使い方があります！

1. カテゴリを選んで名言をゲット
![normal-gacha.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1840262/b6f78250-db7e-bf5b-d940-cbb7a3424d23.gif)
1. オリジナルのこの世にはない名言をゲット
「オリジナルの名言」ガチャをクリックすると，この世にはないオリジナルの名？（迷）言をゲットできます．
![スクリーンショット 2022-12-04 22.08.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1840262/446d2972-5365-1234-0733-4a2eb11c58ee.png)

1. 予定に合う名言をゲット

予定を入れると，その予定に合った名言がゲットできます．
例えば，「学校」と入れると

![スクリーンショット 2022-12-04 22.12.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1840262/e609b934-c9bc-d0e5-c108-d4235584cdef.png)

このような名言がゲットできます．学校頑張って行こうかな〜という気になりますよね？？笑

![スクリーンショット 2022-12-04 22.32.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1840262/c7620dc9-4f40-962f-5211-bc047de67742.png)


# 名言ガチャを作ろうと思った理由
名言が好きだからです笑．私自身，大事な予定の前に偉人の名言で勇気づけられて，頑張ろうと思えた経験が何度もあります．私のように，名言の力で日々の生活を少しでも頑張ろうと思ってもらえる人が増えたらいいなと思い，開発しました．

# 「予定に合う名言」の取得

先ほど紹介した3つの機能のうち，特に工夫したのは「予定に合う名言」の取得の部分です．技術的な説明をしたいと思います．

## どのように「予定に合う名言」を取得する？

初めは，機械学習を用いて予定をカテゴリーに分類して，そのカテゴリーの名言を出力することを考えていました．例えば，「アルバイト」という予定が入力されたらそれを「仕事」というカテゴリーに分類して，「仕事」に関する名言を出力するという流れです．しかし，一つ大きな課題がありました．それは，__機械学習モデルの学習に使う「予定」のデータを大量に集めるのが難しい__ という点です．Twitter の文章から「予定」っぽい言葉を集めてそれを学習データに使うということも検討しましたが，「予定」っぽい言葉かどうか判定するのは手動で行わなければならないため，コストが高いなと感じました...

そこで，考えました．本来したいことは，予定をカテゴリーに分類することではなく，__予定に合った名言を取得すること__ です．なので，カテゴリ分類できなくても，__予定に似た名言を出力できれば良い__ のではないかと感じました．そこで，様々な知見をお借りして，以下のような「予定に合う名言」の出力フローを考えました．



1. 予定の類義語を求める．
1. 予定と類義語をベクトル表現に変換する．変換には，「Word2Vec」という単語をベクトル表現に変換できる機械学習モデルを用いる．
1. 同様に名言をベクトル表現に変換する．
1. 2, 3 で求めた二つのベクトル表現の類似度を求める．
1. 3-4 を名言分繰り返して，一番類似度が高い名言を「予定に合う名言」として出力する．

### 工夫ポイント
工夫した点は主に2点です．

1. 予定と名言のベクトル表現の類似度を見て，「予定に合う名言」を出力する．
先ほどお話ししたように，本来したいことは予定をカテゴリーに分類することではなく __予定に合った名言を取得すること__ です．そこで，予定に似た名言を出力できれば良いと考えました．ベクトル表現に変換することで，「似ている」ということを定量的に測定できるようにしました．
1. 予定だけではなく，予定の類義語も加えてベクトル表現に変換する．
フロー1, 2 の部分で，予定名単体のベクトル表現を使うのではなく，予定名とその類義語のベクトル表現を使うようにしました．その理由としては，__予定名の意味により合った名言を出力するため__ です．一般に予定は短い単語であることが多く，予定名だけのベクトル表現は，その予定名に大きく依存しています．予定名だけ見てそれに似た名言を判断することになるため，予定名の意味とは少しずれた名言が取得されてしまう可能性もあります．それを防ぐために，予定名の意味的に近い類義語も加えてベクトル表現として出力することで，より予定名の意味に合った名言を取得できるのではないかと考えました．実際この方法を使うことで，より予定の意味に合った名言を出せるようになりました！

## 「予定に合う名言」を取得する部分のソースコード

Python で書きました．ソースコード（一部抜粋）を以下に示します．

```python
import MeCab
import numpy as np

POSTPOSITONAL_PARTICLE = "助詞"

# model: word2Vec, meigen_vecs: 名言ごとのベクトル表現が保存されたnumpy配列，meigen_list : 名言が書かれたExcelファイル
model, meigen_vecs, meigen_list = load_tools()
mecab=MeCab.Tagger('-Ochasen')
most_fit_meigen = ""

schedule_with_similar_words = generate_schedule_with_similar_words(model, mecab, schedule)
schedule_vec = word2vector(model, mecab, schedule_with_similar_words)

if schedule_vec is not None:
  max_similarity_score = 0
  max_index = 0
  for i, vec_key in enumerate(meigen_vecs.files):
    meigen_vec = meigen_vecs[vec_key]
    if max_similarity_score < cos_similarity(schedule_vec, meigen_vec.T):
      max_similarity_score = cos_similarity(schedule_vec, meigen_vec.T)
      max_index = i
    
  most_fit_meigen = ((meigen_list["Sheet1"]).cell(int(max_index)+1, 2)).value

return most_fit_meigen

def generate_schedule_with_similar_words(model, mecab, schedule):
    schedule_with_similar_words = schedule
    node = mecab.parseToNode(schedule)
    
    while node:
    if node.feature.split(",")[0] != POSTPOSITONAL_PARTICLE:
        w = node.surface
        try:
          for sm in model.most_similar(w, topn=4):
            schedule_with_similar_words  += sm[0]
        except KeyError as e:
          print(e)
        finally:
          node = node.next
    else:
      node = node.next
    
    return schedule_with_similar_words


def word2vector(model, mecab, sentence, unknowns=[]):
    node = mecab.parseToNode(sentence)
    _sv = np.empty((0,300), np.float32)

    while node:
      if node.feature.split(",")[0] != POSTPOSITONAL_PARTICLE:
          w = node.surface
          try:
            wv = model[w]
            _sv = np.append(_sv, np.array([wv]), axis=0) 
          except KeyError:
            if w not in unknowns:
              unknowns.append(w)
          finally:
            node = node.next
      else:
        node = node.next

    if _sv.shape[0]>0:
      return np.array([np.average(_sv, axis = 0)])
    else:
      print('Ignore sentence', sentence)
      return None

def cos_similarity(v1, v2):
    return np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))
```

上で説明したフローに沿ってソースコードを説明していきます．

1. 予定の類義語を求める．
`generate_schedule_with_similar_words` で，予定とその類義語を連結した文字列を返しています．
まず，以下の部分で予定名をさらに細かい単語（サブワード）に分解します．分解には mecab と呼ばれるパッケージを利用します．分解する理由は後述します．
```python
node = mecab.parseToNode(schedule)
```
そして，word2Vec で，top4まで類義語を求めます．
```python
 for sm in model.most_similar(w, topn=4):
     schedule_with_similar_words  += sm[0]
```
word2Vec は，自身のボキャブラリーに含まれる単語だったらその類義語を求めることができます．逆にボキャブラリーに含まれない単語だったら類義語を求めることはできません．ボキャブラリーに含まれる確率を上げるために，予定名をさらに細かい単語（サブワード）に分解しています．

2. 予定と類義語をベクトル表現に変換する
`word2vector` で，予定と類義語をベクトル表現に変換します．
まず，予定と類義語の文章を先ほどと同様にサブワードに分解します．この理由も先ほどと同様です．
```python
node = mecab.parseToNode(sentence)
```
そして，word2Vec を使って，サブワードごとにベクトル表現を求めます．
```python
wv = model[w]
```
最後に，ベクトル表現の平均を取って，「予定・類義語のベクトル表現」とします．
```python
return np.array([np.average(_sv, axis = 0)])
```

3.同様に名言をベクトル表現に変換する．
1, 2 と同様に名言についてもベクトル表現を求めます．今回は，事前に名言についてはベクトル表現を求めてファイルに保存しておいて，それをロードする形で使っています．

```python 
# ここで事前に求めた名言ベクトル表現をロード
model, meigen_vecs, meigen_list = load_tools()
```

4. 2, 3 で求めた二つのベクトル表現の類似度を求める．
ベクトルの類似度計算には，Cosine similarity を用いています．
```python
def cos_similarity(v1, v2):
    return np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))
```

5. 3-4 を名言分繰り返して，一番類似度が高い名言を「予定に合う名言」として出力する．
以下のコードでそれを行っています．今回は，名言一覧が書かれたエクセルシートから名言を取得しています．
```python 
 for i, vec_key in enumerate(meigen_vecs.files):
    meigen_vec = meigen_vecs[vec_key]
    if max_similarity_score < cos_similarity(schedule_vec, meigen_vec.T):
      max_similarity_score = cos_similarity(schedule_vec, meigen_vec.T)
      max_index = i
  # 名言一覧が書かれたエクセルシートから名言を取得
  most_fit_meigen = ((meigen_list["Sheet1"]).cell(int(max_index)+1, 2)).value
```

## 実際どんな感じの名言が取得できるのか
例えば，こんな感じの名言が取得できます．割と良い感じの名言が取得できていることが分かります．
```
勉強会 : 学ぶことで才能は開花する。志がなければ、学問の完成はない。

試験 : 長い人生には必ず浮沈がある。しかし、努力勉強は必ず報われる

輪講 : 人生は一冊の書物に似ている。愚かな者はパラパラとそれをめくっていくが、賢い者は丹念にそれを読む。なぜなら彼は、ただ一度しかそれを読めないことを知っているからだ。

学会発表 : 人間にとっていちばん大切なことは、各自の仕事に進歩を求めて励むことだ．

インターン : 誰よりも三倍、四倍、五倍勉強する者、それが天才だ。

アルバイト : 仕事をする時は上機嫌でやれ．そうすれば仕事もはかどるし、身体も疲れない．

仕事 : 大変な仕事だと思っても、まずとりかかってみなさい．仕事に手をつけた、それで半分の仕事は終わってしまったのです． 

研究 : 学ぶことで才能は開花する。志がなければ、学問の完成はない。

面談 : 自分の仕事を愛し、その日の仕事を完全に成し遂げて満足した．こんな軽い気持ちで晩餐の宅に帰れる人が世にもっとも幸福な人である

授業 : 学ぶことで才能は開花する。志がなければ、学問の完成はない。 
```

## 課題

現時点で，課題が主に2点あるかなと思っています．

#### 名言の中の一部の単語に影響されて，予定にあまり関係ない名言が出力されることがある

例えば，「実験」という予定を入れると「人間が恋に落ちるのは万有引力のせいではない」という名言が取得されてしまいます笑．これは，「万有引力」と「実験」が強い類似度を持ってしまっているからであると考えられます．これを解決する方法の一つとしては，単語一つ一つではなく文章全体の意味合いからベクトル表現を生成するということがあるかなと考えています．そのようなことができる機械学習モデルに BERT というものがあります．BERTを使えばこの課題が解決するかもしれません．ただ，BERT の良さを生かすために「予定+類義語」を入力として使うのではなく，「予定から自動生成した文章」を入力として使った方が良いのかもと考えています．

#### word2Vec のボキャブラリーにない単語だと，予定に合う名言を求めることができない

先ほど話したように，word2Vec のボキャブラリーに含まれる確率を上げるために予定をサブワードに分解して，それをword2Vecに入力しています．しかし，そのサブワードもword2Vecのボキャブラリーに含まれなかった場合，ベクトル表現を生成することができません．この解決策としては，word2Vec よりも広いボキャブラリーを保持している辞書（例：mecab-ipadic-neologd ）を使って，上位概念の単語に変換することを考えています．具体的には以下の流れです．
1. word2Vec よりも広いボキャブラリーを保持している辞書を使って，単語の上位概念を取得する．
1. その上位概念を word2Vec に入力してベクトル表現を求める．

このようにより一般的な単語に変換することで，元々word2Vec のボキャブラリーに含まれていない単語でも含まれる確率が上がり，ベクトル表現を生成できるのではないかと考えています．

# 最後に
ここまで読んでくださり，ありがとうございました！名言ガチャ，是非使ってみてください！名言の力で日々の生活を少しでも頑張ろうと思ってもらえたら幸いです．