LoginSignup
18
19

More than 1 year has passed since last update.

発想力を測定するWebアプリをWord2Vecを使って作ってみた

Last updated at Posted at 2021-10-31

はじめに

発想力測定器と題しまして、以下のWebアプリを作成しました。できるだけ意味の異なる単語を10個入力することで、あなたの発想力が数値化されます。
https://imagination-checker.izumi-satoshi.com/

github
フロントエンドエンド : https://github.com/IzumiSatoshi/imagination_checker_front
バックエンド : https://github.com/IzumiSatoshi/imagination_checker_back

なお、この発想力測定器は、Naming unrelated words predicts creativity という研究をもとにして作成したものになりますので、学術的な内容に興味がある方は、本家の記事を読むことをお勧めします。

動機

「できるだけ意味の異なる単語を10個挙げると、発想力が測定できるらしい」という話を聞き、調べてみると、Naming unrelated words predicts creativity という研究にたどり着いた。 実際にWeb上で発想力を測定できるサイト (英語)は既に公開されているのだが、日本語版はないようだ。英語版でも測定できることはできるが、やはり自分の母語でテストを受けたいという気持ちは強い。あれ、キリン、キリンって英語でなんだっけと、思考が乱れるのでは、明らかに正確な測定ができない。
ないのなら作ってしまおうと思ったのが、このWebアプリを作るに至った経緯である。

測定器開発偏

どうやって測定するか

発想力を測定するには、いくつかのできるだけ意味の異なる単語を挙げてもらい、それぞれが実際にどのくらい異なっているかを測定すればよいらしい。
もう少し深く説明するために、本家Webサイトからの引用を挟む。

The Divergent Association Task is a quick measure of verbal creativity and divergent thinking, the ability to generate diverse solutions to open-ended problems. The task involves thinking of 10 words that are as different from each other as possible. For example, the words cat and dog are similar, but the words cat and book are not. People who are more creative tend to generate words that have greater distances between them. These distances are inferred by examining how often the words are used together in similar contexts. Still, this task measures only a sliver of the complex process of creativity.
出典: About the task

以下は私の無断Google翻訳+α

発散関連タスク(The Divergent Association Task)は言葉の創造力と発散的思考(自由形式の問題に対する多様な解決策を生み出す能力)の迅速な尺度です。タスクは、互いに可能な限り異なる10単語を考えることを含みます。例えば、猫と犬という言葉は似ていますが、猫と本という言葉は似ていません。より高い創造力を持つ人は、より異なる単語を生成する傾向があります。ここで言う「異なる」とは、単語が同様の文脈で一緒に使用される頻度を調べることによって推測されます。 それでも、このタスクは、創造性の複雑なプロセスのほんの一部しか測定しません。

つまり、より創造力の高い人は、より異なる単語を生成する傾向があるので、生成された単語がどの程度異なっているかを測定すれば、その人の創造力を測定することができるというわけだ。

余談だが、いまさらながら、発想力ではなく創造力を測定するタスクだったということに気づいた。githubのリポジトリ名やドメイン名をcreativityではなくimaginationにしてしまったことを後悔している。だが、引用文にある「自由形式の問題に対する多様な解決策を生み出す能力」としては発想力のほうが近い気もするし、いまさら全てを書き換えるのは面倒なので、以降は発想力で統一しようと思う。(創造力と想像力と発想力の違いを考えたことはありますか?)

基本的な考え方は説明したが、プログラムに落とし込むためには、さらに具体的に考える必要がある。
「単語間の意味がどれくらい異なっているか」を測定するのが難しいわけだが、私は既に、Word2Vecという、この目的にピッタリなモデルを知っていた。つまり、Word2Vecを用いてそれぞれの単語をベクトル化し、そのベクトル同士の類似度からそれらしい測定値を作ることができそうだと考えた。Pythonのライブラリを使えば簡単に実装できそうだ。

Word2Vecって何?

ということで、Word2Vecについて軽くおさらいしておこう。(私が)

絵で理解するWord2vecの仕組み
word2vec(Skip-Gram Model)の仕組みを恐らく日本一簡潔にまとめてみたつもり
などの記事を斜め読みし、ざっくりと何をしているのか理解しようとした。
何となくわかったことは、単語の定義を調べるといった人間的(?)な作業をしているわけではなく、「単語Aの周辺に単語Bがあれば、単語Aと単語Bは似てるよね!」などの、一見大丈夫か?というような方法で単語をベクトル化しているということだ。膨大な文章を学習すればある程度法則が見えてくるものなのだろうか。

調べている途中、そもそも私は幼少期にどうやって単語を学んだのかと考え、不思議な気持ちになった。

Word2Vecを使ってみる

ライブラリインストールして、サンプルプログラムを動かすところまで進めていこう。
以下の記事が参考になった。
https://zenn.dev/sorami/articles/fb2eb78e250568b767fd

Word2Vecの実装では、Gensimというライブラリが有名なようだが、今回はその改良版であるらしいMagnitudeを使っていく。
ライブラリのほかに、日本語コーパスも必要なので、chiVeという日本語単語ベクトルを以下のページからダウンロードした。
https://github.com/WorksApplications/chiVe

from pymagnitude import Magnitude
vectors = Magnitude("./chive-1.2-mc5.magnitude")

print('りんご と みかん   :', vectors.similarity('りんご', 'みかん'))
print('りんご と 機械学習 :', vectors.similarity('りんご', '機械学習'))

"""
(実行結果)
りんご と みかん   : 0.22651043798939477
りんご と 機械学習 : -0.02963153130331572
"""

いい感じだ。

発想力を数値化してみる

以下のような流れで発想力を数値化する。

  1. 10個の単語を入力
  2. 単語同士の全組み合わせを列挙
  3. それぞれの組み合わせについて、単語ベクトル間のコサイン距離を求める
  4. コサイン距離をスコアに換算する
  5. 全組み合わせのスコアの平均を求める

コサイン距離とは、以下の数式で求まる値だ。

$$f(\vec{q},\vec{d}) = 1 - \frac{\vec{q}\cdot\vec{d}}{|\vec{q}||\vec{d}|}$$
0(同じ) から 2(全く違う)までの値をとる。今回は、100点満点で測定したいので、この値に50をかける。
コサイン距離は距離じゃないんだから、勘違いしないでよねっ!
という著者のテンションが高そうな記事を読み、コサイン距離は距離じゃないということを学んだが、それっぽい値が出るので良しとした。(本当に大丈夫?)

以下のプログラムが発想力測定器の中身である。名前だけ聞くと複雑な処理をしているように聞こえるが、50行程度の簡単なプログラムだ。諸ライブラリへの感謝を忘れないようにしたい。

import itertools
from pymagnitude import Magnitude
from scipy import spatial

vectors = Magnitude('./chive-1.2-mc90.magnitude')


def calc_score(word_list):
    """
    単語リストからスコアを算出
    マークダウン形式の表で出力(Qiitaに張り付けるため)
    """
    score_sum = 0
    # 単語の組み合わせ全通りをリストにする
    word_pair_list = list(itertools.combinations(word_list, 2))

    # 列名を表示
    print('|単語1|単語2|スコア|')
    print('|---|---|---|')
    for pair in word_pair_list:
        distance = spatial.distance.cosine(
            vectors.query(pair[0]), vectors.query(pair[1])
        )
        # コサイン距離は0 ~ 2のはず。それを100点満点に変換した値をスコアとする。
        score = (distance / 2) * 100
        # 小数第一位にまとめる
        score = round(score, 1)

        print(f'|{pair[0]}|{pair[1]}|{score}|')

        # socre_sumに加算
        score_sum += score

    # 合計スコアをペア数で割って平均スコアを求める
    score_mean = score_sum / len(word_pair_list)
    # 少数第一位にまとめる
    score_mean = round(score_mean, 1)

    print('total = ', score_mean)


word_list = ['葉巻', '女子高校生', 'トリアージ', '密林', '横隔膜',
             'ニューラルネットワーク', 'オルゴール', '枯山水庭園', 'シャリ', '社会保険']
calc_score(word_list)

実行結果を以下に示す。サンプルとして入力した単語リストは、私の発想力を最大限生かしたものである。

実行結果 被験者 : 私

入力単語 : 葉巻, 女子高校生, トリアージ, 密林, 横隔膜,ニューラルネットワーク, オルゴール, 枯山水庭園, シャリ, 社会保険
総合スコア : 45.5
内訳

単語1 単語2 スコア
葉巻 女子高校生 48.0
葉巻 トリアージ 49.6
葉巻 密林 45.1
葉巻 横隔膜 43.5
葉巻 ニューラルネットワーク 48.1
葉巻 オルゴール 42.5
葉巻 枯山水庭園 44.4
葉巻 シャリ 41.6
葉巻 社会保険 49.4
女子高校生 トリアージ 41.3
女子高校生 密林 44.5
女子高校生 横隔膜 46.5
女子高校生 ニューラルネットワーク 46.6
女子高校生 オルゴール 47.4
女子高校生 枯山水庭園 42.8
女子高校生 シャリ 48.9
女子高校生 社会保険 44.6
トリアージ 密林 45.3
トリアージ 横隔膜 43.1
トリアージ ニューラルネットワーク 39.0
トリアージ オルゴール 49.5
トリアージ 枯山水庭園 48.6
トリアージ シャリ 48.5
トリアージ 社会保険 38.9
密林 横隔膜 46.0
密林 ニューラルネットワーク 47.3
密林 オルゴール 45.2
密林 枯山水庭園 41.0
密林 シャリ 45.9
密林 社会保険 50.4
横隔膜 ニューラルネットワーク 41.6
横隔膜 オルゴール 41.3
横隔膜 枯山水庭園 47.8
横隔膜 シャリ 44.3
横隔膜 社会保険 47.4
ニューラルネットワーク オルゴール 47.0
ニューラルネットワーク 枯山水庭園 48.1
ニューラルネットワーク シャリ 46.2
ニューラルネットワーク 社会保険 42.7
オルゴール 枯山水庭園 43.3
オルゴール シャリ 48.0
オルゴール 社会保険 45.7
枯山水庭園 シャリ 44.9
枯山水庭園 社会保険 47.4
シャリ 社会保険 46.9

トリアージと社会保険を近い意味と判断しているのには感心した。

ただ、これだけでは何とも言えないので、「文房具しか思いつかなかった人」を想定してもう一度実行してみる。

実行結果 被験者 : 文房具しか思いつかなかった人

入力単語 : シャープペンシル, ホワイトボード, 消しゴム, 万年筆,定規, 三角定規, コンパス, 分度器, ボールペン, 修正液
総合スコア : 30.0
内訳

単語1 単語2 スコア
シャープペンシル ホワイトボード 30.9
シャープペンシル 消しゴム 23.4
シャープペンシル 万年筆 14.2
シャープペンシル 定規 25.9
シャープペンシル 三角定規 39.9
シャープペンシル コンパス 32.0
シャープペンシル 分度器 29.0
シャープペンシル ボールペン 9.0
シャープペンシル 修正液 23.9
ホワイトボード 消しゴム 30.3
ホワイトボード 万年筆 33.6
ホワイトボード 定規 29.4
ホワイトボード 三角定規 39.5
ホワイトボード コンパス 32.6
ホワイトボード 分度器 31.0
ホワイトボード ボールペン 26.3
ホワイトボード 修正液 32.1
消しゴム 万年筆 30.1
消しゴム 定規 30.0
消しゴム 三角定規 37.1
消しゴム コンパス 40.0
消しゴム 分度器 31.9
消しゴム ボールペン 20.2
消しゴム 修正液 29.0
万年筆 定規 33.1
万年筆 三角定規 46.4
万年筆 コンパス 36.4
万年筆 分度器 34.1
万年筆 ボールペン 12.6
万年筆 修正液 28.2
定規 三角定規 27.9
定規 コンパス 25.9
定規 分度器 18.8
定規 ボールペン 26.9
定規 修正液 29.1
三角定規 コンパス 34.2
三角定規 分度器 28.5
三角定規 ボールペン 40.5
三角定規 修正液 39.9
コンパス 分度器 25.7
コンパス ボールペン 31.8
コンパス 修正液 38.4
分度器 ボールペン 33.0
分度器 修正液 34.0
ボールペン 修正液 23.7

予想通り、低いスコアが測定された。一応、測定器としての役割は果たしていると思われる。

高いスコアを出すには

いろいろな単語を入力して測定を行い、何となくではあるが以下のような傾向を見出すことができた。
以下に挙げる傾向はすべて、Word2Vecの「周辺にある単語は近い意味だよね」という性質に起因していると思われる。

1. より具体的な単語は、より高いスコアを出しやすい

この傾向は強く感じられた。例えば、「ごはん→米→白米」のように、より具体的にな単語に置き換えていくことで、簡単にスコアを上げることができる。なので、高スコアを目指すためには、関連性の低い単語を引っ張ってくる能力に加え、思いついた単語をより具体的なものに置き換えていくといった方向の発想力も重要になってくるのだろう。

2. より使いにくそうな単語は、より高いスコアを出しやすい。

1の傾向と重なる部分もあるが、使いにくい単語は高スコアを出しやすい。例えば「白米→酢飯→シャリ」と置き換えていくことで、使用場面が寿司屋に限定され、より使いにくい単語となる。そのため、文章中で他の単語の周辺に位置する機会も少ないのだろう。

3. 抽象度の高い単語はスコアが低くなりやすい

「時間」や「上」などの概念は、スコアが低くなりやすかった。これは、様々な単語と組み合わせて使用することができるためだろう。例えば、「時間」と「みかん」という一見関連性がないように見える単語でも、「時間がない。みかんを食べよう。」というような文があると、Word2Vecの性質上近い単語だと判断されてしまうのかもしれない。

気になる点

以下に挙げるような気になる点もいくつかあるが、今は動くものを完成させることを優先しよう。

  1. コーパスに含まれていない単語はどのように処理されるのだろうか
  2. 50点を超えるのがかなり難しいように感じたが、それはなぜか
  3. 二つ以上の名詞がつながった複合名詞の測定はどうなっているのか

また、本来であれば、この測定器の信ぴょう性を検証するため、その他の発想力テストやIQテストなどとの相関関係を調べるのが良いと思われるが、今回はこれ以上は踏み込まないことにする。(それを行っているのが本家の研究)

何はともあれ、測定器が完成してひと段落ついた。

Webアプリ開発偏

全体の構成としては、Flaskで測定器のAPIを開発し、Reactで作るフロントエンドエンドからAPIを呼び出すという形をとった。Flaskは軽量なPythonのWebアプリケーションフレームワークで、ReactはSPA(Single Page Application)が簡単に作れるJavaScriptライブラリだ。
この程度の規模のWebアプリであれば、フロントエンドエンドもFlaskに任せるのが良いような気もするが、今回は学習中のReactを使ってみる。githubのリポジトリもバックエンドとフロントエンドエンドで分け、両者の疎結合を意識した開発を行っていく。

Flaskで測定器APIを実装

単語リストを投げたら、スコアとその内訳を返すAPIを実装する。Flaskのチュートリアル等を読みながら、前述した測定器のプログラムを順当にWebAPI化した。

import itertools
from flask import Flask, request
from flask.json import jsonify
from flask_cors import CORS
from pymagnitude import Magnitude
from scipy import spatial
from waitress import serve


vectors = Magnitude('./chive-1.2-mc90.magnitude')
app = Flask(__name__)
app.config['JSON_AS_ASCII'] = False
CORS(app)


@app.route('/', methods=['POST'])
def api():
    json_dict = request.json
    word_list = json_dict['word_list']

    response = calc_score(word_list)

    return response


def calc_score(word_list):
    """
    単語リストからスコアを算出し、スコアとその内訳をjsonで返す
    """

    score_sum = 0
    breakdown_dict = dict()
    word_pair_list = list(itertools.combinations(word_list, 2))
    for idx, pair in enumerate(word_pair_list):
        distance = spatial.distance.cosine(
            vectors.query(pair[0]), vectors.query(pair[1])
        )
        # コサイン距離は0 ~ 2のはず。それを100点満点に変換した値をスコアとする。
        score = (distance / 2) * 100

        # socre_sumに加算
        score_sum += score

        # 内訳dictに入れる
        breakdown_dict[idx] = {
            'word1': pair[0],
            'word2': pair[1],
            'score': score,
        }

    # 合計スコアをペア数で割って平均スコアを求める
    score_mean = score_sum / len(word_pair_list)

    json_obj = jsonify({
        'score': score_mean,
        'breakdown': breakdown_dict
    })

    return json_obj


if __name__ == '__main__':
    print('start server')
    serve(app, host='0.0.0.0', port=5000)

ここで一つ問題発生

githubにプッシュしようとしたら、magnitudeファイル(単語ベクトルデータ)が100MBを超えているとのことで拒否された。
ということで、git lfsなるものを導入したが、よくわからないエラーに悩まされる。
数時間格闘したのち、結局、magnitudeファイルはgitignoreに入れて、自力で管理することにした。

バックエンドをデプロイ

Docker化

まず、FlaskアプリケーションをDocker化し、docker compose upだけで立ち上がるようにしておく。Dockerイメージのサイズが5GBになっているが、Word2Vec関連のファイルサイズが大きいのだろう。いや、何か設定がおかしいのかもしれない。

サービス選定

次に、デプロイ先のサービスを選定する。
この測定器APIを動かすためには、700MB以上の単語ベクトルデータをメモリ上に展開する必要があり、ある程度のマシンスペックが必要だと考えていた。
デプロイ先としてまず思いついたのがHerokuの無料枠だったが、利用可能メモリが512MBだということで断念した。そこで、GCE(Google Compute Engine)にデプロイすることにした。永久無料枠のVM(Virtual Machine)インスタンスの利用可能メモリは1GBだ。前々から一度使ってみたいと思っていたので、いい機会になる。

GCPにデプロイ

永久無料枠を利用する手順については、こちらの記事が分かりやすかった。OSは使い慣れているUbuntuを選ぶ。
https://qiita.com/Brutus/items/22dfd31a681b67837a74

VMインスタンス上のFlaskアプリケーションに外部からアクセスするには、Nginxを導入する必要があるらしい。以下の記事が非常に参考になった。本来はNginxも含めてDocker化するものなのだろうが、まぁいいだろう。
https://triple-four.hatenablog.com/entry/20210810/1628584545

不安点

パフォーマンステスト等は全く行っていないので、1回の測定あたりどのくらいのリソースを食うか分かっていない。アクセスが集中するとサーバーがダウンすることは容易に想定できるが、そういった心配は、今回のような個人開発サービスにおいて、杞憂になることが多いと知っているので、ひとまずはスルーすることにする。

APIの動作確認が成功して、ひと段落付いた。初めてのGCPへのデプロイだったので、いろいろはまりどころがあるかと思っていたが、案外簡単に動くものができてうれしかった。

Reactでフロントエンドを実装

フロントエンドエンドの実装は、常にDone is better than perfect!!と叫びながらのものであった。避けなければならない最悪の事態は、途中でモチベーションを失ってしまい、未完成のまま放置されることだ。私にはよくある。なので、まずは、どれだけ見た目が貧相でもいいので、動くものを公開することにした。

測定結果の表示方法

今回作るアプリには大きく分けて2つのパートがある。1つ目が単語を入力する部分で、2つ目が測定結果を表示する部分だ。その他にも補足情報を掲示するための「これは何?」というページも作成したが、メインではない。
どのように単語を入力する部分と結果を表示する部分を接続するか考えた結果、以下のような手順で処理を行うことにした。単語入力側と結果表示側を別々のページとして実装する。

  1. 単語を入力される(単語入力側)
  2. 入力された単語のリストをURLのクエリ文字列に変換(単語入力側)
  3. 結果表示ページに、クエリ文字列を含んだURLからリダイレクト(単語入力側)
  4. クエリ文字列を解析し、単語のリスト抽出 (結果表示側)
  5. 測定器APIにリクエストを送る(結果表示側)
  6. 結果を表示する(結果表示側)

この手順で処理を行うことで、結果表示ページとそのURLとが1対1で対応するようになり、結果表示ページを共有することが可能になる。例えば私の測定結果をここに張り付けることができる。

将来的にSNSでの結果共有機能を実装することを考えると、悪くないアイデアだと思う。

測定結果に一言添える

測定結果として、数値だけが出されても、多くの人は自分に発想力があるのかないのか分からないだろう。そこで、「あなたの発想力は人並です」というようなメッセージを添えるようにする。処理自体は単純で、スコアをif文で区切り、あらかじめ設定しておいたメッセージを表示するだけだ。このメッセージに根拠はないので、無視してもらっても構わない。

測定結果の内訳を表示する

単語の組み合わせごとにスコアを算出し、最後に平均をとるという測定を行っていみるため、単語の組み合わせごとのスコアも表示することにした。これがあることで、前回の測定でスコアの低かった単語を入れ替え、再度測定するというような楽しみ方も可能となる。単語ペアをスコアで昇順ソートし、スコアを下げている原因となっている単語を見つけやすくした。

レスポンシブルデザイン

今の時代において、スマホからも使いやすいアプリであることは必須だろう。ヘッダーのデザインを切り替えたり、画面の幅を調整したりといった基本的な箇所のみの実装にとどまったが、スマホユーザにも配慮した発想力測定器になった。

この当たりで、フロントエンドの開発にはいったん終止符を打つことにする。自分の作っているものが、だんだんと形になっていくのを見るのはやはり楽しい。モノづくりの醍醐味が感じられる。

フロントエンドをデプロイ

フロントエンドエンドのデプロイ先は、静的ファイルを設置できればどこでもよい。これは、SPAであることのメリットの1つだと思う。今回はFirebaseにデプロイすることにした。

バックエンドのSSL化

Firebase CLIの

Firebase deploy

だけですんなりデプロイできると思っていたが、測定器APIとの通信がうまくいっていない模様。
調べてみると、Firebaseは外部との通信含めてhttpsしか許可していないらしい。
ということで、バックエンド側のAPIをhttpsに対応させる作業を開始した。だが、Let’s Encrypt(無料SSL証明書)の手続きをするためには独自ドメインをとる必要があるらしい。どうせフロントエンドは独自ドメインで運用するつもりだったので、お名前ドットコムで取得。誤ってWhoisメール転送オプションをつけてしまい300円損した。
GCEサーバー上でのSSL化の作業は以下の記事を参考に進めた。自動更新の設定もすることができ、満足した。
https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-20-04-ja

独自ドメインで公開

バックエンドに続き、フロントエンドエンドのほうも独自ドメイン化する。特に難はなく、サクッと完了した。

完成!

ついに、動くものを公開するという目標を完遂することができた。
1日当たり数時間程度の作業で、約一週間ほどの時間がかかった。これは、今後小規模なWebアプリを作成するときの、自分の中での目安の1つになるだろう。

完成後は、家族の発想力を測定して楽しんだ。信ぴょう性に欠けるためか、家族が測定器に強い興味を持っている様子は感じられなかったが、まぁいいだろう。

私としては、自分の母語で発想力を測定できるようになり、大満足である。

今後やりたいこと

  • Twitterで測定結果をつぶやけるようにする
  • 測定結果の表を総当たりの対戦表みたいにして、見やすくする
  • 測定結果を収集し、平均値とか偏差値とかを算出したりする

おわりに

間違いなどがありましたら、ご指摘いただけると幸いです。
最後までお読みくださり、ありがとうございました。

18
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
19