語彙力ゲーム「弓箭」であそぶ

この記事はTSG Advent Calendar 2018の12日目として書かれたものです。昨日はmoratorium08さんによる「 TSG live AI コンテストの内容と感想、盤面生成に関する妄想など」でした。

さてお久しぶりです、pizzacat83です。今年からTSGというサークルに入りまして,Advent Calendarになんか書こうということで本当はクラスで使ってるSlackのお話をするつもりだったんですが,その記事を半分くらい書いたところでこのゲームを思いついたのでこっちのお話をします。TSGはコミュニケーションツールとしてSlackを使っているのですが,そこで語彙力で戦うゲーム「弓箭」を遊べるbotを作りました。

弓箭とは

お題の単語に対して“近い”単語を回答すると,その単語とお題の類似度と,その単語のレア度(コーパス中の出現頻度の低さ)に応じた点数をもらえるゲームです。つまり,お題に似ていて,かつあまり使われない単語を答えた人の勝ちです。

ゲームの名前「弓箭(きゅうせん)」は弓矢,弓矢を扱う武士,弓矢を用いた戦いなどを意味する単語*1です。「戦い」を意味する単語で語彙力が高そうなやつ,といういかにも語彙力のなさそうな理由で名付けました。英語表記はとりあえずvocabwarにしていますが,Blitzkrieg*2とかにしようかな。

ちなみにこれは私がオリジナルで考えたゲームではなくて,英単語についてほとんど同じルール(ただし類似度とレア度ではなく類似度と解答スピードを評価)のゲームがあるんですが,ググり力がなくて見つかりませんでした。黒い背景に水色の文字みたいな見た目だったと思うんですが,有識者教えてください。

ゲームの様子

Slackに「弓箭」と書き込むと,お題の単語を10個提案してくれます。単語を一つ選ぶと回答を書き込むフェーズに入ります。

f:id:pizzacat83:20181211124939p:plain:w400
弓箭スタート

お題選ぶ必要なくない?という指摘があったので,「即弓箭」と書き込むと提案なしでランダムにお題が決まるようになっています。運が悪いと知らない単語が出てきてたほいやになります。

f:id:pizzacat83:20181212170025p:plain:w400
わからない

ちなみに形容詞・形容動詞・副詞のみが出題されるようになっています。というのも,名詞は類義語が作りづらいことがありそうなのと,たほいやが名詞ばっかりなのでそちらに一任したいという気持ちがあります。あと日本語は細かいニュアンスを動詞の違いではなく修飾語によって表現することが多い(ex. glimpse「ちらりと見る」 stare「じっと見る」) (接頭語もあるけど)というのもあります。

回答を書き込みます。未知語は弾かれます。(私が👍しているように見えるのはbotトークンを使っていないからです)

f:id:pizzacat83:20181212170132p:plain:w400
弓箭で遊ぶ人々

30秒経過すると結果発表です。参加人数の2倍のダミーを混ぜて,類似度が高い順に上位1/3が正の得点を得ます。

f:id:pizzacat83:20181212171450p:plain:w400
結果発表

元がスピード勝負のゲームだったのでこちらも時間を厳しく設定しているのですが,得点が期待できる単語を考えている間に結果発表されてしまうこともしばしば。難しい単語を考えるとなるともう少し長めにした方がいいかもしれませんね。

ちなみにダミーはレア度の高い単語が多めに出るようにしていて,結果発表にコトバンクから取ってきた単語の意味も記載しているので,結果発表の後はダミーを眺めて次の戦いに備え語彙力を高めることができます。たほいやのダミー選択肢を覚えてるこわい人多いし,弓箭のダミー単語もどんどん覚えられていくんじゃないですかね。

実装

たほいやは博多市さんが一晩で作ったらしいですが,私は強くないので弓箭をn晩かけて作りました。全体的にたほいやに似ているので,大枠のコードはほとんど一緒です。弓箭の本質は類似度とレア度なので,その辺の話を軽く。

類似度はword2vecを使っているんですが,分かち書きされたコーパスが必要なのでWikipedia全文データを取ってきて分かち書きをします。で,このときついでに単語の出現回数を数えます。Wikipediaの全文データはXMLになっているので、wp2txtでプレーンテキストにしてあげます。

$ sudo gem install wp2txt
$ wp2txt --input-file ./jawiki-latest-pages-articles.xml

まあこれ以外にも同様のツール色々あるので探してください。

んでこれを分かち書きしつつ単語の出現頻度を見ます。TSGのslackbotはNode.jsで動いていて,tashibotという別のbotがkuromoji.jsを使っていたので私も使ってみたんですが,なんせ巨大ファイルを入出力するので(おそらく出力の仕方が下手で)メモリをバカ食いして死にました。よく考えたらNode.jsやkuromoji.jsでやる必要がないことに気づいたのでPythonで適当に書きます。こっちは常識的なメモリを食べていました。

これで分かち書きされたコーパスとレア度(出現頻度の逆数の常用対数)を得られました。さてword2vecしようということですが,Node.jsからword2vecできるパッケージが存在します。

www.npmjs.com

ということで早速学習させてみたんですが今度はCPUとメモリをバカ食いして死にました。小さなコーパスでやってみたら学習結果の出力形式が単語 0.000000 0.000000 ...という感じの分かりやすいテキストファイルになっていて,よく考えたらNode.jsでやる必要がないことに気づいたのでPythonで適当に書きます。gensimは良い子なのでメモリは500MBくらい,2時間ほどで学習終わったかな。

from gensim.models import word2vec
data = word2vec.Text8Corpus("corpus/wiki_wakachi.txt")
model = word2vec.Word2Vec(data, size=300)
model.save("corpus/wiki_wakati.model") # モデルの保存
model.wv.save_word2vec_format("corpus/wiki.wv.txt") # node.jsでも読める単語ベクトル一覧のテキストファイル

Node.jsで読めるように出力関数を自分で書こうとしたらgensimが用意してくれてました。本当に良い子。ただテキストファイルで単語ベクトル一覧を保存する場合,拡張子は.txtにすべきです。それ以外だとNode.jsのword2vecパッケージがバイナリとして解釈してよくわからん感じになります(これにn時間溶かした)。ただ中身を見る必要がないならファイルサイズも小さくなるしバイナリにした方が良いと思います。この場合は最下行が次のようになります。

model.wv.save_word2vec_format("corpus/wiki.wv.txt", binary=True)

これでレア度と類似度を求めることができます。

ちなみに「ゲームの様子」で書いた通りダミーはレア度の高い単語が多めに出るようになっています。要するに重み付き乱択です。重み付き乱択って色々アルゴリズムがあって見るたびに賢いなーっていつも思います。私は走査1回で済ませるやつと前処理して二分探索するやつが好きです。が,今回は重み付き非復元抽出なのと前処理をする気にならない(ただでさえメモリ食べてるので…)のとで,愚直に「重みなしで1つ抽出してレア度に応じた確率で棄却する」という方針でやっています。数学的に確かめてないんですけどこれで重みに応じて選択されるんですかね。

ちなみに高得点を得るコツが分かってくると,人間の尺度ではなくword2vecの尺度で「近い」単語を考えるようになります。弓箭を極めて歩くword2vecになろう。

今後

現状では得点が保存されないんですが実装した方がいいですかね。それからさらなる語彙力の向上のために,結果発表に模範解答(類似度そこそこ高く,レア度が高い)を含めようかなとか思ってます。まあこれは賛否両論かもしれませんが。

あとWikipediaの語彙力だけでは少し物足りないので青空文庫も学習させようと思ってるんですが,gensimで追加学習しようとすると怒られます。

model.train(corpus_file="corpus/aozora/wakachi.txt", epochs=model.epochs, total_examples=12345, total_words=1234567)
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
(中略)
*/venv/lib/python3.7/site-packages/gensim/models/base_any2vec.py in _train_epoch_corpusfile(self, corpus_file, cur_epoch, total_examples, total_words, **kwargs)
    403             raise ValueError("total_words must be provided alongside corpus_file argument.")
    404 
--> 405         from gensim.models.word2vec_corpusfile import CythonVocab
    406         from gensim.models.fasttext import FastText
    407         cython_vocab = CythonVocab(self.wv, hs=self.hs, fasttext=isinstance(self, FastText))

ModuleNotFoundError: No module named 'gensim.models.word2vec_corpusfile'

古いバージョン使ったら行けたりするんですかね。有識者教えてください。最初から学習し直すのも一手か。

あとはそもそも日本語ってあんまり弓箭向いてないんですよね。外来語の取り込み方が上手なので,類義語が簡単に思いついてしまってあんまり面白くないです(編集距離で加点しようかな)。英語とかだといろんな言語からの借用語が統合されないので類義語の綴りが全然違っていて,もう少し楽しくなるんですけどね。ただ「英語版弓箭」は最初の方に書いた通り弓箭の元ネタとして既存(名前を忘れたけど)なので,どうせやるなら情報科学の論文のみをコーパスとする弓箭とか面白いんじゃないですかね。実用的だし。

ということで,TSG Advent Calendar 2018 12日目でした。明日はfiordさんによる「TSG LIVE2!の感想・反省等を適当に書いたポエム - アルゴリズマーの備忘録」です。

*1:ちなみに「弓箭」と「戦争」「戦い」の類似度はそんなに高くなくて、「弓矢」との類似度はとても高いです。弓矢の意味で使われてることがほとんどということですね

*2:電撃戦のこと。ドイツ語より。