もふもふ技術部

IT技術系mofmofメディア

word2vec対人間でコードネーム(ボドゲ)を対戦してみたら大接戦だった

codename

コードネームで遊んでいたある瞬間にあることに気付いてしまった。word2vecを使えば、人間よりも賢く正解を選べるのではないかということに。

コードネームというのは今mofmof inc.で少し流行っているボドゲなんですが、超簡単に説明すると、それぞれ単語が書かれた25枚のカードを並べて、マスターが単語一つのヒントを言って、他プレイヤーはそのヒントから連想されるカードを当てるルールで、2チームに分かれて戦うゲームです。

word2vecとは、超簡単に説明すると、単語をベクトル化する手法のことです。自然言語処理とか機械学習とかする際に、コンピューターは文字列を直接計算することは出来ないので、数値の行列で表現された値(ベクトル)に変換したい。そういうシーンに使う技術ですね。

word2vecを使えば、意味の近い単語は、ベクトルも近くなります。単語のベクトル同士を比較して検索すれば、マスターが言ったヒントに対して、連想される単語を解くことが出来るのではないか?と考えたわけです。

早速やってみましょう。

pythonのバージョンを最新にする

別に最新にしなくてもいいのですが、久々に触るのでpython最新バージョンにしておく。pyenv install --listに3.6系が表示されなかったのでpyenv自体をアップグレードした。

$ brew update && brew upgrade pyenv
$ pyenv install 3.6.3

単純に単語間の距離を図れる実装をする

まずは簡単に単語ベクトル間の類似度を計算する処理を実装。未知の単語にも対応しやすいのでword2vecといいつつfastTextの学習済みモデルを使うことにします。

こちらからNEologdの方をダウンロードします。

https://qiita.com/Hironsan/items/513b9f93752ecee9e670

import gensim

model = gensim.models.KeyedVectors.load_word2vec_format('model.vec', binary=False)
print(model.similarity('国王', '王妃'))

「国王」と「王妃」を比較した結果。まあそこそこ類似している。

0.7415558764104452

単語リストから類似度が近い順に出力する

候補の単語リストから意味の近いが単語リストを出力する実装。

import gensim

model = gensim.models.KeyedVectors.load_word2vec_format('model.vec', binary=False)
print('model loaded')

input = '音楽'

entries = ['水', 'ピアノ', '歌', 'ネズミ', '山田', 'のだめ', 'ラーメン', 'チャーハン']
similarities = [model.similarity(input, entry) for entry in entries]


results = [[similarity, entry] for (entry, similarity) in zip(entries, similarities)]
print(sorted(results, reverse=True))

結果。

「音楽」と類似性のある単語なので、「ピアノ」「歌」が上位に来ます。面白いのは「のだめ」ですね。「のだめカンタービレ」の「のだめ」だと思うのですが、ちゃんと3位に入っててすごい。

model loaded
[[0.6169899229268544, 'ピアノ'], [0.4713573407607582, '歌'], [0.2292222342027308, 'のだめ'], [0.20652593259197552, 'チャーハン'], [0.1938283436807833, 'ラーメン'], [0.17242243144392436, 'ネズミ'], [0.13525180029671519, '水'], [0.11767428906901088, '山田']]

対話式にヒントを入力出来るようにする

fastTextのモデルは容量が大きいため、毎回モデルをロードしていると時間がかかってツラい。対話式にヒント文字列をインプットさせるように改善。

import gensim

model = gensim.models.KeyedVectors.load_word2vec_format('model.vec', binary=False)
print('model loaded')

entries = ['水', 'ピアノ', '歌', 'ネズミ', '山田', 'のだめ', 'ラーメン', 'チャーハン']

while True:
    hint = input("Please Enter Hint Word: ")
    similarities = [model.similarity(hint, entry) for entry in entries]
    results = [[similarity, entry] for (entry, similarity) in zip(entries, similarities)]
    print(sorted(results, reverse=True))

いくつか思いついた単語をインプットさせてみた。大体直感通りの結果が来ているように見える。「女の子」からの「ネズミ」は謎。ミニーか?ミニーなのか?

Please Enter Hint Word: ピアノ
[[1.0, 'ピアノ'], [0.43565867161470445, '歌'], [0.3320467465769171, 'のだめ'], [0.26950472594431163, 'チャーハン'], [0.19272638659128938, 'ラーメン'], [0.18938207555065334, 'ネズミ'], [0.1550712436945725, '山田'], [0.10224726495755349, '水']]

Please Enter Hint Word: 音楽
[[0.6169899229268544, 'ピアノ'], [0.4713573407607582, '歌'], [0.2292222342027308, 'のだめ'], [0.20652593259197552, 'チャーハン'], [0.1938283436807833, 'ラーメン'], [0.17242243144392436, 'ネズミ'], [0.13525180029671519, '水'], [0.11767428906901088, '山田']]

Please Enter Hint Word: 犬
[[0.5613603123122953, 'ネズミ'], [0.3226369348454945, 'チャーハン'], [0.2243613060427448, 'ラーメン'], [0.21535947962259477, '歌'], [0.20445501347267214, 'のだめ'], [0.18993679794265358, 'ピアノ'], [0.18400771550299977, '水'], [0.1777356578838234, '山田']]

Please Enter Hint Word: 中華
[[0.39156823156332166, 'ラーメン'], [0.3760020975907288, 'チャーハン'], [0.17497745854109759, '山田'], [0.1470604288216415, 'ネズミ'], [0.1367321315218868, '水'], [0.133490035235623, '歌'], [0.12712744795792122, 'ピアノ'], [0.11558425960228152, 'のだめ']]

Please Enter Hint Word: ご飯
[[0.6251783265716403, 'チャーハン'], [0.5756860064634584, 'ラーメン'], [0.32121973780734264, 'ネズミ'], [0.30025417864681714, '水'], [0.24296017247392815, 'のだめ'], [0.23776385035795294, 'ピアノ'], [0.21658843752440543, '歌'], [0.12862305947556246, '山田']]

Please Enter Hint Word: 女の子
[[0.4351578615781734, 'のだめ'], [0.3653867143383554, 'ネズミ'], [0.35502207264730345, '歌'], [0.3229137653972869, 'ピアノ'], [0.3139616192660859, 'チャーハン'], [0.2808129630522954, 'ラーメン'], [0.1665110543566241, '山田'], [0.13388873838939902, '水']]

コードネームのカードに合わせて候補を修正

コードネームでは25枚のカードを並べるので、とりあえず適当にGoogle画像検索して出てきた25枚のカードの単語を入れておく。

http://hobbyjapan.co.jp/game?attachment_id=11580

  • アルプス
  • コミック
  • 億万長者
  • ルール
  • 飛行機
  • ルート
  • ライター
  • 南極
  • パイロット
  • 草原
  • 靴下
  • 手袋
  • モスクワ
  • レーザー
  • タップ
  • カボチャ
  • トリップ
  • パーツ
  • イス
  • 雪だるま
  • レース
  • ノート

entriesの部分だけを修正。

entries = ['アルプス', 'コミック', '億万長者', 'ルール', '飛行機', 'ルート', 'ライター', '南極', 'パイロット', '草原', '靴下', '手袋', 'モスクワ', 'レーザー', 'タップ', '氷', 'カボチャ', 'トリップ', '死', 'パーツ', 'イス', '雪だるま', '森', 'レース', 'ノート']

この状況で、「海外」というヒントで「飛行機」と「南極」と「モスクワ」を当てたいとする。

やってみる。

Please Enter Hint Word: 海外
[[0.2652039376190187, '億万長者'], [0.2423529363055031, 'モスクワ'], [0.2302802390443617, '南極'], [0.22584301443358396, 'レース'], [0.21508693164279488, 'ライター'], [0.2126630599750537, 'パーツ'], [0.20409594764793076, '飛行機'], [0.18853815636993165, 'コミック'], [0.1797889052083787, 'トリップ'], [0.17823215055840946, 'レーザー'], [0.16019663678811671, 'パイロット'], [0.15045440960752246, 'ノート'], [0.1491811526597114, 'ルール'], [0.14693725258557108, 'アルプス'], [0.14678200418773513, '靴下'], [0.13754995885115084, 'ルート'], [0.1121549481165682, 'タップ'], [0.10258050183270567, '手袋'], [0.09644947898167762, '草原'], [0.0889428734420587, '氷'], [0.08442313357454047, 'カボチャ'], [0.07666276056588606, 'イス'], [0.06462464366401303, '森'], [0.020523827709298982, '死'], [-0.006318696251227121, '雪だるま']]

億万長者、モスクワ、南極ときた。惜しい。確かに億万長者はしょっちゅう海外旅行とかしてそうだわ。

意外にも飛行機は低い。よくよく見たら「トリップ」もあるけど9位かー。

人 vs fastText

human vs word2vec

実際にコードネームを人 vs fastTextでやってみます。

ルールは、人間側は通常通り、fastText側は、スパイマスターを人間が担当し、ヒントに対して順位が高い順で、まだ埋まっていないカードの個数分回答するというルールで行きます。

25枚のカードはこんな感じ。

  • エース
  • オペラ
  • エンジン
  • バット
  • ショップ
  • ボンド
  • クジラ
  • コード
  • ボルト
  • スター
  • パイロット
  • ロック
  • クラブ
  • ジム
  • ライン
  • 美人
  • 億万長者
  • ウグイス
  • ウマ
  • レベル
  • キング
  • カンガルー
  • パイプ
  • ネズミ
entries = ['エース', 'オペラ', 'エンジン', 'バット', 'ショップ', 'ボンド', 'クジラ', 'コード', 'ボルト', 'スター', 'パイロット', 'ロック', 'クラブ', 'ジム', 'ライン', '角', '美人', '億万長者', 'ウグイス', 'ウマ', 'レベル', 'キング', 'カンガルー', 'パイプ', 'ネズミ']

codename1

勝負開始。fastTextチーム(青)が先攻です。

  • fastTextチーム: 空,3

カンガルーなんでやねん感。

[[0.2870392852951838, 'パイロット'], [0.21829171212829024, 'カンガルー'], [0.21727741279040874, 'ロック'], [0.21023259355948917, 'バット'], [0.20568046933214254, '角'], [0.19986737254043008, 'ウグイス'], [0.19029222152230607, 'エンジン'], [0.18888399806571204, 'ネズミ'], [0.16026705941054614, 'スター'], [0.15878767920156325, 'クジラ'], [0.15323450853258877, 'コード'], [0.15197290684500064, 'エース'], [0.1343044911978491, 'ジム'], [0.1259317094648073, 'ボルト'], [0.11379137982524966, 'ショップ'], [0.11214101696242451, 'パイプ'], [0.10339879401552102, '美人'], [0.10260221684933914, 'レベル'], [0.10000406048527827, 'キング'], [0.07217669627242934, '億万長者'], [0.06366703234200857, 'オペラ'], [0.05161884188791782, 'クラブ'], [0.05142958639759595, 'ライン'], [0.03911065356602782, 'ウマ'], [-0.0010060499133467161, 'ボンド']]

codename2

  • 人間側チーム(赤): ガンダム,4
    • ジム
    • 角 => ハズレ

続いてfastTextチーム。

  • fastTextチーム: 羽,2
    • ネズミ => ハズレ

ネズミが人間チームだったのでターンエンド。溢れ出るポンコツ感。

[[0.35734856837058027, 'ネズミ'], [0.3407354197681828, 'ウグイス'], [0.30231385330822524, 'クジラ'], [0.28724149778678476, '角'], [0.24109124045949643, 'ウマ'], [0.24068317440672196, 'カンガルー'], [0.20713968863634802, 'ボルト'], [0.19417215080172334, 'バット'], [0.17436678271832054, 'エンジン'], [0.17365521737037787, 'パイロット'], [0.17113591614157414, '美人'], [0.169808562367904, 'クラブ'], [0.12864236735953827, 'ライン'], [0.12624107089377473, 'レベル'], [0.11531520026721212, 'ジム'], [0.11233562800001257, 'オペラ'], [0.11046519252814493, 'パイプ'], [0.10422670025589856, 'エース'], [0.09568902666510001, 'スター'], [0.09444617948438301, 'キング'], [0.09271052656660722, '億万長者'], [0.06332397624638349, 'ショップ'], [0.056563711607298, 'コード'], [0.055648877169895264, 'ロック'], [-0.0023425476067552506, 'ボンド']]

codename3

  • 人間側チーム: ギアス,2

    • コード
    • 美人 => ハズレ
  • fastTextチーム: 動物,3

    • クジラ
    • カンガルー
    • ウマ

ここへ来て3つゲット。比較的シンプルなやつは取れるみたい。

[[0.5696311596743517, 'ネズミ'], [0.5463947374986629, 'クジラ'], [0.4855075893380812, 'カンガルー'], [0.45932066597278115, 'ウマ'], [0.298899659109754, 'ウグイス'], [0.24723084645481186, '角'], [0.2087318571586103, 'パイロット'], [0.18443540056977387, '億万長者'], [0.16804010770987501, 'ボルト'], [0.1617030601478015, 'キング'], [0.1571200401476309, 'ロック'], [0.15202821648892634, 'オペラ'], [0.15200179497228475, 'コード'], [0.14946434630113611, 'レベル'], [0.1487640951091421, 'ボンド'], [0.14730008641054324, 'クラブ'], [0.14339190244608485, 'ショップ'], [0.12885679823869345, 'バット'], [0.12811775931470615, '美人'], [0.12097259230570187, 'エンジン'], [0.11955913608979468, 'パイプ'], [0.1086937427762299, 'スター'], [0.08837524939310173, 'ジム'], [0.0460660135603881, 'ライン'], [-0.0014591560410979441, 'エース']]

codename4

  • 人間チーム: シャア,2

    • エース
    • (自信がないのでパス)
  • fastTextチーム: 夜,2

    • オペラ
    • ロック => ハズレ

「美人」は人間チームのミスにより取れていたので「ロック」ではずれ。なんで「夜」で「ロック」なのか。

[[0.25774374499610764, 'オペラ'], [0.2542392513254049, '美人'], [0.23534930814788474, 'ネズミ'], [0.18502404981435372, 'ロック'], [0.1844820741668377, '億万長者'], [0.18014364691477852, '角'], [0.17442130839433692, 'ウグイス'], [0.16690787006841948, 'カンガルー'], [0.1646107840885699, 'ショップ'], [0.16305189586888158, 'キング'], [0.1601975913275284, 'クラブ'], [0.1591940688367847, 'パイロット'], [0.15825170931264693, 'スター'], [0.14873614292055967, 'ボンド'], [0.12343069160244946, 'クジラ'], [0.12268757616174808, 'ジム'], [0.10103128298101888, 'エース'], [0.09733534362156634, 'バット'], [0.08775457165328907, 'ライン'], [0.08344272753165256, 'ボルト'], [0.0661518794366569, 'エンジン'], [0.06262750416613111, 'ウマ'], [0.052760784672292525, 'パイプ'], [0.04537312397143086, 'コード'], [0.01714649279629577, 'レベル']]

codename5

  • 人間チーム: ロボット,3
    • エンジン => ハズレ

人間もなかなかのポンコツである。

codename6

  • fastTextチーム: 野球選手,2
    • スター
    • 億万長者

惜しい!「億万長者」はハズレ。確かにプロ野球選手は億万長者感ある。たぶん「バット」が正解かな。

[[0.34802177778130905, 'エース'], [0.33340216729208483, 'スター'], [0.32214194212203506, '億万長者'], [0.31483632238309694, 'バット'], [0.2750351502269966, 'パイロット'], [0.2732072559781063, 'ジム'], [0.2609737851820647, 'クラブ'], [0.2506141156462058, '美人'], [0.21487114590407286, 'オペラ'], [0.214036341752585, 'ショップ'], [0.1987855456797399, 'ウマ'], [0.1854599426674845, 'ボンド'], [0.1802219592150528, 'キング'], [0.17922812999260357, 'ロック'], [0.17827116968421233, 'レベル'], [0.17661715705083686, 'クジラ'], [0.1689675758288308, 'カンガルー'], [0.15991714099596605, 'ネズミ'], [0.1243013783504487, 'ウグイス'], [0.11574148710187698, 'コード'], [0.11521406142864558, 'ライン'], [0.11219830870994738, 'エンジン'], [0.09774109613563253, 'ボルト'], [0.07148620058215267, '角'], [0.05636974546217209, 'パイプ']]

codename7

  • 人間チーム: 裸,3

    • (忘れちゃった。。)
  • fastTextチーム: 数字,1

    • レベル

正解。

[[0.3530641051450358, 'コード'], [0.2618974268855117, 'レベル'], [0.2175016304249158, 'ライン'], [0.21734292186608345, 'エンジン'], [0.19371016472275848, 'エース'], [0.17528782497162335, 'ウグイス'], [0.17451741260631007, 'バット'], [0.1695551802214793, 'ネズミ'], [0.16587366054071234, 'パイプ'], [0.1584419811179874, 'キング'], [0.15216019013694537, 'クラブ'], [0.1490712559697229, 'ボルト'], [0.14550270370695348, '億万長者'], [0.14256240552769223, 'スター'], [0.13850684865839583, 'ボンド'], [0.12828860284612895, 'カンガルー'], [0.11533758380355856, 'パイロット'], [0.11517715498450164, 'ウマ'], [0.11275299567629085, '角'], [0.10439623405073879, 'ショップ'], [0.09806607416381932, 'ロック'], [0.0976247548940662, 'オペラ'], [0.09441490579550431, '美人'], [0.06449104057276794, 'クジラ'], [0.0354603003950006, 'ジム']]

codename8

  • 人間チーム: お◯◯◯◯(放送禁止用語のため自粛),2
    • パイプ
    • ボルト

人間チームのギリギリ勝利!!

codename9

まとめ

word2vecは非常にシンプルなヒント「動物」などにはうまく対応出来るけど、やはり難しいケースは多いみたい。なんでそれやねんみたいのも結構多かった。

今回のfastTextのコーパスはwikipedia2017/01/01だったようなので、また別のコーパスを使えば違う成績を出しそう。どんなコーパス使うと強くなるのかなー。

むしろコードネームのプレーログを全部集めて普通に分類問題にして機械学習する方が簡単に強くなりそう。