pixivの中の人が、単語の意味ベクトルを機械学習した記事を公開されています。
アニメやゲームの二次創作で賑わっているpixiv小説を学習しただけあって、きちんとオタクらしい単語の意味を獲得できているところが凄いですね。
「刀剣男子」 - 「男」 + 「女」= 「艦娘」
なんと素敵なことに学習済みモデルも配布されているので、ちょっと遊んでみようと思います。
まほうつかいはじめました!
試しに、『ラブライブ!』のスクールアイドル「矢澤にこ」
の意味ベクトルを覗いてみましょう。
これがFacebookの人工知能が学習した、宇宙No.1アイドルにこにーの意味ベクトルだ!
$ grep 矢澤にこ fasttext-model.vec
矢澤にこ 0.019783 0.19108 -0.022026 0.084974 -0.22116 0.40731 0.050229 0.08452 -0.32226 0.21247 -0.18102 -0.16565 -0.51189 -0.57782 0.25588 -0.68081 -0.78397 0.082184 -0.13755 0.27661 -0.18925 0.69547 -0.39642 0.42797 -0.35558 -0.12432 -0.25115 0.2353 -0.64518 0.07407 0.059794 0.031658 0.55932 0.29246 0.28443 0.21839 -0.63347 0.29398 -0.22737 -0.29317 0.25269 -0.31109 0.10771 -0.56458 -0.038022 0.013576 0.47242 -0.036706 0.62488 -0.65502 -0.45005 -0.20644 0.20615 0.62102 -0.28411 0.4585 -0.032914 -0.41461 0.10216 0.128 -0.27425 0.14086 0.46892 0.026439 -0.22837 -0.23224 0.11419 0.31121 0.10832 -0.96888 -0.30923 0.028069 0.072835 -0.14563 -0.40337 0.55399 -0.21664 0.31468 -0.098204 0.072447 0.29686 0.047919 -0.0831 -0.38392 0.094459 -0.22222 -0.43531 -0.55599 -0.04801 -0.18968 0.67759 0.69869 0.3708 0.80129 -0.79446 -0.16282 -0.26733 -0.41572 0.057653 0.25807
なるほどなー。この数字の羅列が、\にっこにっこにー/というわけですね。……わけが分からない!
分かりませんが、単語が100次元のベクトルに落としこめたということは、あんな方法(足し算)やこんな方法(内積)で「矢澤にこ」について計算したい放題ということです。うーん、ちょっと魔法みたいですね?
μ'sからAqoursが受け継いだもの
アニメ最終回の記憶も新しい『ラブライブ!サンシャイン!!』。
そのスクールアイドルグループAqoursは、色々な意味で前作のμ'sを意図的に踏襲しているようです。
実際アニメのストーリーは、μ'sへの憧れと昇華をテーマとしていましたね。ずっと終わりを探していたμ'sの影を追いながら、しかしAqoursは彼女たちだけの始まりを成しとげたのだ、というのは劇場版への見事なアンサーになっていて心を揺さぶるものがありました。
それでは実際、キャラクター性はどのくらい受け継がれているのでしょうか?
ファンによって意見の分かれるところかもしれませんが、スクールアイドルの意味ベクトルを手に入れた我々は、それを計算することができます。してみましょう!
というわけで、意味ベクトルのコサイン類似度を計算して、Aqoursの各メンバーに近いμ'sのメンバーをリストアップしてみました。
残念ながら、「松浦果南」は学習されていないようです。まだ人工知能にハグは早かった^^
Aqours | 一番目に近いμ's | 二番目に近いμ's |
---|---|---|
黒澤ルビィ | 高坂穂乃果(0.7011) | 小泉花陽(0.6929) |
国木田花丸 | 小泉花陽 (0.7594) | 星空凛 (0.7396) |
津島善子 | 小泉花陽 (0.7050) | 南ことり(0.6679) |
高海千歌 | 高坂穂乃果(0.7458) | 南ことり(0.7229) |
渡辺曜 | 高坂穂乃果(0.6156) | 南ことり(0.6106) |
桜内梨子 | 小泉花陽 (0.8064) | 南ことり(0.7481) |
黒澤ダイヤ | 南ことり (0.7377) | 園田海未(0.7357) |
小原鞠莉 | 星空凛 (0.7306) | 小泉花陽(0.7210) |
さて計算結果ですが、うーん「高坂穂乃果」「小泉花陽」が一番近い結果が多いですね。。。
これは、どちらかというと、μ'sの中でAqoursっぽいのが「高坂穂乃果」「小泉花陽」ということかもしれません。
そもそもμ'sのスクールアイドルは皆μ'sらしさを持っていて、Aqoursのスクールアイドルは皆Aqoursらしさを持っているため、それを差し引いてみないことには、グループ内の役割は浮き上がってこないのかも?
ということで、まずAqoursの意味ベクトルの重心を計算して、それを各スクールアイドルから引いておくことにします。
μ'sについても同じことをして、コサイン類似度を計算をしてみましょう!
Aqours | 一番目に近いμ's | 二番目に近いμ's |
---|---|---|
黒澤ルビィ | 高坂穂乃果(0.2069) | 矢澤にこ (0.2029) |
国木田花丸 | 星空凛 (0.2939) | 小泉花陽 (0.2564) |
津島善子 | 東條希 (0.1874) | 矢澤にこ (0.0465) |
高海千歌 | 高坂穂乃果(0.2555) | 園田海未 (0.2186) |
渡辺曜 | 矢澤にこ (0.1685) | 高坂穂乃果(0.1341) |
桜内梨子 | 小泉花陽 (0.2313) | 西木野真姫(0.2077) |
黒澤ダイヤ | 絢瀬絵里 (0.3079) | 園田海未 (0.2324) |
小原鞠莉 | 星空凛 (0.3541) | 南ことり (0.2077) |
今度はバラけましたっ。
納得性が高いのは、1年生コンビの引っ張り役「国木田花丸」=>「星空凛」、アニメ主人公の「高海千歌」=>「高坂穂乃果」、生徒会長の「黒澤ダイヤ」=>「絢瀬絵里」です。
二番目まで見ると、可愛さアピールの「黒澤ルビィ」=>「矢澤にこ」、日常的にキャラ作りしている「津島善子」=>「矢澤にこ」、作曲担当の「桜内梨子」=>「西木野真姫」も、わかるわという感じです。
どうでしょうか。
個人的には、良い感じの学習結果と頷きつつも、まだ感覚ジャストフィットというわけでは無さそうです。このあたり原因としては、次のようなものが考えられます。
- 学習データが2016年6月中旬から7月中旬のため、アニメについては最大2話までの情報しか反映されていない。
- そもそも電撃G'sの読者企画のため、途中でキャクター設定の変更も行われている影響もあるかもしれない。アニメが放送されると、おそらく二次創作ではそのイメージが主流になる。
- 二次創作ではフルネーム表記よりも、お互いの呼び名「ホノカチャン」といった単語の方が特徴が出やすいかもしれない。
- 筆者の感覚が、二次創作界隈とズレている。
- 二次創作ならではの、特殊なキャラ付けが流行っているかもしれない。
- fastTextは部分語の情報を使うそうなので、それが邪魔しているかもしれない。
- 個人的にピンと来ない「国木田花丸」=>「小泉花陽」は、「花」が一致してる。
- 「松浦果南」が計算されていないため、Aqoursの重心からズレている。
- ベクトル計算方法(ユークリッド距離による重心からの相対ベクトルを用いたコサイン類似)が微妙。
765 + 346 = 1111
さて、せっかくなので別のアイドル作品、『アイドルマスター』の765プロダクションと、『アイドルマスター シンデレラガールズ』の346プロダクションでもやってみましょう。
346プロダクションは人数が多いので、アニメで描かれていたシンデレラプロジェクトのアイドルに絞ります。ラブライブと違って、アイドルマスターではキャラクター性の踏襲は感じられませんが、どういう結果になるでしょうか。
残念ながら、「双海真美」「アナスタシア」「三村かな子」「緒方智絵里」は学習されていないため、計算外です。イズヴィニーチェ……。
346 | 一番目に近い765 | 二番目に近い765 |
---|---|---|
島村卯月 | 天海春香 (0.3686) | 如月千早 (0.2087) |
渋谷凛 | 天海春香 (0.3544) | 如月千早 (0.2885) |
本田未央 | 三浦あずさ(0.1982) | 如月千早 (0.1644) |
新田美波 | 星井美希 (0.2687) | 高槻やよい(0.1091) |
神崎蘭子 | 菊地真 (0.3722) | 水瀬伊織 (0.2985) |
双葉杏 | 我那覇響 (0.1454) | 水瀬伊織 (0.1116) |
城ヶ崎莉嘉 | 双海亜美 (0.3565) | 萩原雪歩 (0.1988) |
諸星きらり | 水瀬伊織 (0.2205) | 我那覇響 (0.1880) |
赤城みりあ | 水瀬伊織 (0.1721) | 双海亜美 (0.1538) |
前川みく | 四条貴音 (0.1939) | 双海亜美 (0.0797) |
多田李衣菜 | 我那覇響 (0.1833) | 星井美希 (0.1544) |
千川ちひろ | 音無小鳥 (0.4666) | 秋月律子 (0.2980) |
おおお、一番類似度の高いのが、「千川ちひろ」=>「音無小鳥」です。
これはプロデューサーの皆様方におかれましては、満場一致の結果でしょう。え、その二人はアイドルじゃない?
ともあれ、面白いのが「島村卯月」=>「天海春香」と「渋谷凛」=>「天海春香」です。
アニメでは「島村卯月」と「天海春香」が、それぞれのメインヒロインでしたが、原作のゲームでは「島村卯月」「渋谷凛」「本田未央」が各属性のチュートリアルキャラを務めているため、三人まとめてメインヒロイン扱いされることも多く、それを反映しているのでしょう。あれ、ちゃんみお?
他に類似度が0.3超えているところで見ると、元気いっぱい年少組の「城ヶ崎莉嘉」=>「双海亜美」も良い感じですね。
うんうんそれもまたアイカツだね
さて、ここまでやっていることは単なる計算なので、やろうと思えばAqoursと346プロダクションを比べるといったこともできるわけです。
とはいえ、そんなことに意味があるのでしょうか。どちらもアイドル作品とはいえ、ラブライブのスクールアイドルは観客もまたステージに引きあげてしまう輝かしい部活動であり、アイマスのアイドルはファンの兄(C)姉(C)の生きていく糧となるお仕事であるわけです。それを混同して一括りにしてしまうのは、あまりにも雑な、、、
Aqours | 一番目に近い346 | 二番目に近い346 |
---|---|---|
黒澤ルビィ | 多田李衣菜(0.1528) | 千川ちひろ(0.1035) |
国木田花丸 | 神崎蘭子 (0.1583) | 前川みく (0.1463) |
津島善子 | 神崎蘭子 (0.2852) | 多田李衣菜(0.1576) |
高海千歌 | 島村卯月 (0.2339) | 渋谷凛 (0.1696) |
渡辺曜 | 島村卯月 (0.2241) | 城ヶ崎莉嘉(0.0715) |
桜内梨子 | 新田美波 (0.2009) | 双葉杏 (0.1296) |
黒澤ダイヤ | 千川ちひろ(0.2430) | 新田美波 (0.1046) |
小原鞠莉 | 神崎蘭子 (0.2489) | 城ヶ崎莉嘉(0.2424) |
うおおおおお、一番類似度の高いのが、「津島善子」=>「神崎蘭子」です。中二病ポジションを学習できている! やみのま堕天!
がんばらねば〜ねばねばぎぶあっぷ♩
さて、今回は小説の学習結果を使って遊んでみたわけですが、学習の先にはプログラムに小説を自動生成させたいという話があります。
これは昔からある夢の一つで、たとえばSF『小説 進物史観』(円城塔の元ネタ)が1997年に書かれているわけですが、最近はディープラーニング(深層学習)のRNN+LSTMで自動生成するのが流行っているみたいです。
ただ個人的には、今の確率ベースの機械学習だけでは、きちんとした物語……少なくとも面白いものは生まれないんじゃないかなという気がしています。構成の立て方、伏線の通し方、整合性の取り方、そういうったところは論理的なものだと思うので、もうしばらくはテンプレート生成以上のものにはならないんじゃないかなぁ、と。
ただ「りんな」のようなチャットボットの隆盛により、だんだん自然な会話はできるようになってきそうな雰囲気はあるため、初めに自動生成の成功例が出るのは、物語の本筋には影響のない日常会話パートな気がします。キャラの立ったチャットボット二体を会話させればいいので。
という妄想の元、物語を書くというのはどういうことなのか自ら実感するため、また会話パートの学習データを作るため、『深層の令妹 ζ(*゚w゚)ζ』というほぼセリフだけのネット小説を書いてたりしています。
「いやだって、お前。面会といっても、こうして病院の地下室で曇りガラス越しに会話するだけだしな。ぶっちゃけ架空の存在と思われても仕方ないのでは?」
「たはー照れますなー。よくできた妹は、人工知能と区別が付かない的なー」
SFとかミステリーの香りを添えつつも、愉快なキャラクターたちが出てくるラノベなので、ぜひ読んでみてください〜。
そして誰か早く、続きを書く人工知能を作ってください(ぉぃ
コード
やっつけ感ありますが、こんな感じのScalaコードで計算しました。
テキストファイルを読んでベクトル計算しているだけなので、どんな言語でもさらっと書けると重います。
import scala.io.Source
case class Word(name: String, vector: Seq[Double])
object VecCal {
val dict = Using(Source.fromFile("fasttext-model.vec", "UTF-8")) { source =>
for (line <- source.getLines.toVector.tail) yield {
val data = line.split(" ")
(data.head, Word(data.head, data.tail.map(_.toDouble).toVector))
}
}.toMap
val mus = toWords("小泉花陽", "星空凛", "西木野真姫", "高坂穂乃果", "園田海未", "南ことり", "絢瀬絵里", "東條希", "矢澤にこ")
val aqours = toWords("黒澤ルビィ", "国木田花丸", "津島善子", "高海千歌", "渡辺曜", "桜内梨子", "黒澤ダイヤ", "松浦果南", "小原鞠莉")
val prod765 = toWords("天海春香", "如月千早", "萩原雪歩", "高槻やよい", "秋月律子", "水瀬伊織", "三浦あずさ", "双海亜美", "双海真美", "菊地真", "星井美希", "我那覇響", "四条貴音", "音無小鳥")
val prod346 = toWords("島村卯月", "渋谷凛", "本田未央", "新田美波", "アナスタシア", "神崎蘭子", "三村かな子", "双葉杏", "緒方智絵里", "城ヶ崎莉嘉", "諸星きらり", "赤城みりあ", "前川みく", "多田李衣菜", "千川ちひろ")
def toWords(names: String*): Seq[Word] = {
names.flatMap { name =>
val word = dict.get(name)
if (word.isEmpty) println("unkown: " + name)
word
}
}
def role(words: Seq[Word]): Seq[Word] = {
val average = words.map(_.vector).reduce(_ + _).map(_ / words.size)
words.map(word => Word(word.name, word.vector - average))
}
def printSimilar(sources: Seq[Word], targets: Seq[Word], n: Int = 3) = {
val results = for (source <- sources) yield {
(source.name, targets.map(target => (target.name, source.vector.cos(target.vector))).sortBy(-_._2).take(n))
}
val sizes = results.map(result => result._1.length +: result._2.map(_._1.length)).transpose.map(_.max)
for ((source, targetsDistance) <- results) {
println(
"| " + source + (" " * (sizes.head - source.length)) + " | " +
targetsDistance.zip(sizes.tail).map { case ((target, distance), size) =>
target + (" " * (size - target.length)) + f"($distance%.4f)"
}.mkString("", " | ", " |"))
}
}
implicit class RichSeqDouble(val a: Seq[Double]) extends AnyVal {
def elementWise(op: (Double, Double) => Double)(b: Seq[Double]): Seq[Double] = {
assert(a.size == b.size)
a.zip(b).map(pair => op(pair._1, pair._2))
}
def + = elementWise(_ + _) _
def - = elementWise(_ - _) _
def * = elementWise(_ * _) _
def cos(b: Seq[Double]): Double = {
(a * b).sum / math.sqrt((a * a).sum * (b * b).sum)
}
}
}
import java.io.Writer
import scala.io.Source
object Using {
def apply[A, B](resource: A)(process: A => B)(implicit closer: Closer[A]): B =
try {
process(resource)
} finally {
closer.close(resource)
}
}
case class Closer[-A](close: A => Unit)
object Closer {
implicit val sourceCloser = Closer[Source](_.close())
implicit val writerCloser = Closer[Writer](_.close())
}
$ scala
Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_101).