忍者スリスリくんの影武者を作っている

この記事は GMOペパボ Advent Calendar 2019 - Qiita の15日目の記事です。

kagemusha0

オリジナルグッズ作成・販売サービスのSUZURI(スズリ) には、公式忍者の「忍者スリスリくん(@suzurijp)」がいる。

@suzurijp

私はスリスリくんのファンだ。

そんなスリスリくんについて、1つだけ気になっていることがある。

それは

follower

忍者のくせにフォロワー数が多すぎることだ。

控えめに言って隠密行動に向いていない。露出しすぎ。

だがここまで露出してしまったことを悔いても仕方がない。この状況を打破するためには、何か対策を打つ必要がある。

こんな時に有効なのが「影武者」だ。

敵の目を欺くために、大将などと同じ服装をさせた身代わりの武者。

出典: 影武者(かげむしゃ)の意味 - goo国語辞書

忍者スリスリくんにそっくりの影武者がいれば、この問題の解決策になる可能性がある。 私は忍者スリスリくんのファンなので、彼の身の安全を確保し、安心して隠密行動をしてもらうために「忍者カゲムシャくん」を作ることにした。

忍者カゲムシャくんとは

作ったものがこれだ。https://kagemushakun.herokuapp.com/

kagemusha1

カゲムシャくんの見た目は特殊メイク*1でスリスリくんそっくりに作った。98%の人はカゲムシャくんだと気づかないだろう。

そして何かメッセージをいれて「しゃべらせる」を押すと↓

kagemusha2

まるでスリスリくんのようにしゃべらせることができる。スリスリくんは「マス」などの表現が独特なのだ。

これで99%の人を欺くことができる。

さらに「あいさつさせる」にチェックを入れてしゃべらせると↓

kagemusha3

スリスリくんの独特なあいさつを再現することもできる。小粋だ。

これでスリスリくんと完全一致する。作った私ですらカゲムシャなのかホンモノなのか区別がつかなくなってきている。

ぜひ忍者カゲムシャくんで遊んでみてほしい。リンクを再掲しておく。 https://kagemushakun.herokuapp.com/

ここから先は忍者カゲムシャくんのカラクリを書いていく。(ここでページを閉じてもらっても構わない)

忍者カゲムシャくんのカラク

忍者カゲムシャくんのカラクリを紹介する。

ただし、この情報は極秘情報だ。なぜなら、この情報が拡散されるとカゲムシャのカラクリが露呈し、スリスリくんの身に危険が及ぶかもしれないからだ。

むやみやたらに拡散してはいけない。この約束を守ると誓ってくれる人は、このページの一番下にある☆を押してほしい。*2

ラクリの概要

忍者カゲムシャくんは Ruby on Rails 6.0 で動いている。DBはいまのところ必要ないので使っていない。Rails 6 から Webpacker がデフォルトになっているが、その辺も不要なので --skip-javascript をつけて skip した。画面の見た目はCSSフレームワークBulma (正確には bulma-rails) を使って簡単なデザインをしている。特に変わったことはしていない。

カゲムシャくんの機能的な特徴はスリスリくんに似せた「しゃべり」と「あいさつ」だ。 これは suri_lang という rubygem を開発することで実現した。 この自作 gem について紹介していくことで、カゲムシャくんのカラクリを明らかにする。

ラクリ①「しゃべり」

スリスリくんのtwitterおよび筆者が独自ルートで手に入れた情報*3を元に、しゃべりの特徴を分析すると

  • 語尾の表記にカタカナを使う傾向がある
  • 次のような一部の単語はひらがなを使う傾向がある
    • 「等」「事」などの一部の名詞
    • 「是非」「沢山」などの一部の副詞
    • 「及び」「但し」などの一部の接続詞

ということが分かる。特定の単語を上記の法則に則って変換するだけで、まあまあスリスリくんっぽくなると予想できる。

これを実現するための手順は次の通りだ。

  1. 変換したい単語のリストを用意する
  2. しゃべらせたい文章を単語に分割(= 形態素解析)する
  3. 分割した単語をひとつずつ、変換したい単語のリストと比較する
  4. 変換したい単語と一致したら変換する

1. はコツコツ*4やる。
私はスリスリくんのファンなのでまったく苦にならずにリストを用意することができた。

2.MeCab という形態素解析エンジンを用いる。
厳密には MeCabRuby で扱いやすくした natto という gem を使って形態素解析をする。

natto を使うとこのような実装で形態素解析をすることができる。詳しい解説は natto/README.md at master · buruzaemon/nattoHome · buruzaemon/natto Wiki を参照してほしい。

require 'natto'

nm = Natto::MeCab.new
nm.enum_parse('私の名前は田中です').each do |elm| 
  print "#{elm.surface}\t#{elm.feature}\n"
end
# 私    名詞,代名詞,一般,*,*,*,私,ワタシ,ワタシ
# の    助詞,連体化,*,*,*,*,の,ノ,ノ
# 名前  名詞,一般,*,*,*,*,名前,ナマエ,ナマエ
# は    助詞,係助詞,*,*,*,*,は,ハ,ワ
# 田中  名詞,固有名詞,人名,姓,*,*,田中,タナカ,タナカ
# です  助動詞,*,*,*,特殊・デス,基本形,です,デス,デス
#       BOS/EOS,*,*,*,*,*,*,*,*

3.4. はやや荒い実装だ。
まず 1. のリストから key が 変換前の単語,その品詞 , value変換後の単語 となる Hash オブジェクトを作成しておく。 そして 2. で分割した単語とその品詞を使って、その Hash にアクセスし、該当のキーがあれば 変換後の単語 を、なければ 変換前の単語 を返す。

require "natto"

module SuriLang
  class Translator
    DICTIONARY = {
      'です,助動詞': 'デス',
      'ます,助動詞': 'マス',
      # ... 略 ...
    }

    def self.translate(text)
      nm = Natto::MeCab.new('-F%m,%f[0]')
      features = nm.enum_parse(text)
                   .select{|n| !n.is_eos?}
                   .map{|n| n.feature}

      features.map{|feature| to_suri_word(feature)}.join
    end

    private

    def self.to_suri_word(feature)
      DICTIONARY[feature.to_sym] || feature.split(',')[0]
    end
  end
end

実際に suri_lang をこのように使ってスリスリくんのようにしゃべらせることができる。

require 'suri_lang'

SuriLang::Translator.translate('SUZURIの忍者スリスリくんです')
# => "SUZURIの忍者スリスリくんデス" 

ラクリ②「あいさつ」

私はスリスリくんのファンなので 忍者スリスリくん ( surisurikun )のジャーナルズ ∞ SUZURI(スズリ) をよく読んでいる。

すると、ある特徴に気づく。記事冒頭の小粋なあいさつだ。

例えばこれだ。

石器ブンブン! SUZURIの公式忍者、忍者スリスリくんデス。

引用: 忍者スリスリくん ( surisurikun ) の「 【終了】ビキビキ!800円引きセール 」というジャーナル ∞ SUZURI(スズリ)

意味はわからないが、なんとなく可愛い。

また、例えばこのように

そうめんツルツル!忍者スリスリくんデス。

引用: 忍者スリスリくん ( surisurikun ) の「 人気のTシャツに学ぶ!デザインのヒント 」というジャーナル ∞ SUZURI(スズリ)

夏の風物詩である「そうめん」を使った、季節感のあるあいさつもある。どうやら季節に合わせてあいさつを変えているようだ。

これを実現するための手順は次の通りだ。

  1. 忍者スリスリくんのジャーナルから冒頭のあいさつをリストアップする
  2. あいさつをyamlファイルにしておく
  3. ランダムにあいさつを表示する

1. はコツコツ*5やる。
私はスリスリくんのファンなのでまったく苦にならずにリストアップすることができた。

2. は以下のようなyamlファイルを作った。

- message: 石器ブンブン! SUZURIの公式忍者、忍者スリスリくんデス。
  source: https://suzuri.jp/surisurikun/journals/2019-05-07
  season: none
- message: そうめんツルツル!忍者スリスリくんデス。
  source: https://suzuri.jp/surisurikun/journals/2019-07-02
  season: summer
- ...

3.message, source, seasonインスタンス変数とする Greeter クラスを実装し、yaml を読み込んで作られる Array からランダムに1つ取り出して Greeter クラスのインスタンスを作るようにした。

実際に suri_lang をこのように使うとあいさつさせることができる。

require 'suri_lang'

greeter = SuriLang::Greeter.random_build
greeter.message
# => "石器ブンブン! SUZURIの公式忍者、忍者スリスリくんデス。" 

忍者カゲムシャくんの今後の展望

以上が忍者カゲムシャくんのカラクリだ。長々と書いてきたが、大したことはしていない。

今後の展望として、改良点を2つ考えている。

改良点①「入力された文章の季節に合わせてあいさつをさせる」

例えばこのように、冬の内容の文章を入力しても、真夏のようなあいさつをしてくる場合がある。

kagemusha4

忍者スリスリくんは賢いので、このような季節が食い違う発言はしない。

これについては、「入力された文章がどの季節っぽいか」を判定するカラクを実装しようと考えている。

具体的には、次のような手順を想定している

  1. なんらかの文章データを用いて、なるべく多くの単語の分散表現(ベクトル)を用意する
  2. 各季節を代表する単語(例えば、春を代表する単語として「桜」など)をリストアップする
  3. 入力された文章を構成する単語のベクトルと各季節を代表する単語のベクトルとを比較し、ベクトルの類似度を数値化する
  4. 各季節ごとに類似度の合計値を算出し、類似度が低い季節を特定する
  5. 類似度が低い季節のあいさつは選択されないようにする

この手順でいい感じになるかはやってみないと分からない。

1. の「文章データ」を決めかねている。「スリスリくんがしゃべる単語」という意味ではスリスリくんのジャーナルやtwitterでの発言を対象にすると良さそうだが、データ量が限られているため語彙数が少ない懸念がある。そうなると入力された文章の単語の多くが「未知語」と判定されてしまう可能性があり、各季節との類似度判定の精度が低くなりそうだ。

単語の分散表現の獲得については、(Deep Learning の勉強も兼ねて)推論ベースのアプローチを取るつもりだ。具体的には CBOW(continuous bag-of-words) モデルの word2vec を実装してみる。

2. の「各季節を代表する単語」の選び方も決めかねている。主観で良いのか?和歌の季語を活用してみるか?などを考えている。

3. 4. 5. は、そもそも単語ごとの類似度の数値化をすることで文章の季節感を表現できるのか?という懸念がある。例えば文章中に否定語が入っている場合に、想定外に季節の類似度が評価されてしまう可能性がある。(「今は桜が咲いていない」= 「桜」という単語が評価されて「春」だと判断されてしまう、など)

とはいえ、やってみないと分からないので、やってみてから考えようと思う。

改良点②「リスト取得の自動化」

今回は「しゃべり」と「あいさつ」に必要なデータを手動で獲得したが、これではメンテナンスコストが掛かりそうだ。

「しゃべり」に関しては、スリスリくんの発言から特徴的なカタカナ表記やひらがな表記を、 「あいさつ」に関しては、スリスリくんのジャーナルから冒頭文の数行を、 それぞれ自動で抽出できるようにしていこうと考えている。

具体的な手順は検討中だ。

まとめ

この記事では、忍者スリスリくんの影武者である「忍者カゲムシャくん」を紹介し、そのカラクリについて説明した。 今後も「忍者カゲムシャくん」の改良を続けていくので暖かい目で見守ってほしい。

*1:画像加工とも呼ばれる。怒られないか心配だ。

*2:押してくれると筆者がよろこぶ。このもう少し下に☆があるはず。SNSシェアでも可。

*3:実は社内にいい感じのドキュメントがある。入社したら読める。we are hiring!

*4:小さなことからコツコツと。大切にしていることだ。

*5:小さなことからコツコツと。大事なことだから2回言った。