東方キャラの関連性の強さをニコニコ動画の動画数で測ってみた
昨日の記事でニコニコ動画の動画検索の結果を取得できるようになりました
ニコニコ動画 『スナップショット検索API』 に触ってみた - 唯物是真 @Scaled_Wurm
このデータを使って東方キャラ同士の関連性の強さを測ってみます
PMI
PMI(Pointwise Mutual Information)という指標で関連性の強さを測ります
前に別の記事でもちょっと話題にしましたが、これは共起性(ある2つのことが一緒に発生しやすいか)を調べるための指標の一つです
共起性の指標には他にもいろいろあって下記の記事が参考になります
なぜ単純に2つのことが同時に発生した頻度の大小を比較しないかというと、例えばそれぞれ起こりやすい2つの事象が偶然同時に起こる頻度が大きくなることがあるからです
式としては次のようになっています(Wikipediaの式を参考)
$$\operatorname{pmi}(x;y) \equiv \log\frac{p(x,y)}{p(x)p(y)} = \log\frac{p(x|y)}{p(x)} = \log\frac{p(y|x)}{p(y)}$$ 2つの事象\(x\)と\(y\)についてのPMIは\(x\)と\(y\)がそれぞれ起こる確率\(p(x), p(y)\)と、\(x\)と\(y\)が同時に発生する確率\(p(x, y)\)で表されます
事象\(x\)と\(y\)が独立(同時に起こりやすい、起こりにくいなどの関係を持たない)場合\(p(x,y)=p(x)p(y)\)となるので、\(\log\frac{p(x,y)}{p(x)p(y)}\)は独立な場合と比較した時の起こりやすさの比の対数をとったものになっています
PMIは\(-\infty\)のような扱いづらい値になりうるので、-1から1に正規化したバージョンのNormalized PMI(NPMI)を今回は使います
$$\operatorname{npmi}(x;y) = \frac{\operatorname{pmi}(x;y)}{-\log \left[ p(x, y) \right] }$$
東方キャラの関連性の強さをNPMIで測る
昨日の記事でも使いましたが以下の記事から手作業で抽出したキャラのリストを使います
東方Projectの登場キャラクターとは (トウホウプロジェクトノトウジョウキャラクターとは) [単語記事] - ニコニコ大百科
東方を含む動画の中からある一人のキャラを含む確率と、ある二人のキャラを含む確率を使ってNPMIを計算します
PMIは頻度が少ないものに対しては大きくなる傾向があるので、100件の動画以上に含まれるキャラに限定して実行します
結果
上位30件を示します
そこそこ関係性の強いペアが取れていて、よい結果が得られていると思います
| NPMI | キャラ名1 | キャラ名2 | 両方のキャラを含む動画数 | キャラ1を含む動画数 | キャラ2を含む動画数 |
|---|---|---|---|---|---|
| 0.931126459724 | マエリベリー・ハーン | 宇佐見蓮子 | 87 | 103 | 112 |
| 0.650891211299 | 八坂神奈子 | 洩矢諏訪子 | 43 | 130 | 154 |
| 0.631734271825 | 火焔猫燐 | 霊烏路空 | 40 | 137 | 148 |
| 0.602341571226 | 姫海棠はたて | 射命丸文 | 49 | 102 | 337 |
| 0.59609987972 | 姫海棠はたて | 犬走椛 | 32 | 102 | 178 |
| 0.590804045548 | ナズーリン | 寅丸星 | 44 | 210 | 149 |
| 0.588793784001 | 古明地こいし | 古明地さとり | 120 | 411 | 380 |
| 0.57607555259 | 茨木華扇 | 霍青娥 | 22 | 116 | 100 |
| 0.566549459188 | 射命丸文 | 犬走椛 | 60 | 337 | 178 |
| 0.546457926202 | チルノ | 大妖精 | 159 | 1090 | 283 |
| 0.541293449936 | 十六夜咲夜 | 紅美鈴 | 187 | 830 | 491 |
| 0.528101230505 | 八雲藍 | 橙 | 71 | 292 | 341 |
| 0.522984039421 | フランドール・スカーレット | レミリア・スカーレット | 163 | 561 | 650 |
| 0.502878552921 | 八意永琳 | 鈴仙・優曇華院・イナバ | 21 | 117 | 160 |
| 0.495110667871 | 上白沢慧音 | 藤原妹紅 | 42 | 208 | 269 |
| 0.487466019296 | 聖白蓮 | 雲居一輪 | 24 | 249 | 103 |
| 0.484447101257 | 八意永琳 | 蓬莱山輝夜 | 22 | 117 | 197 |
| 0.482048739037 | 星熊勇儀 | 水橋パルスィ | 29 | 228 | 155 |
| 0.477043462432 | 西行寺幽々子 | 魂魄妖夢 | 51 | 241 | 350 |
| 0.475368618885 | 博麗霊夢 | 霧雨魔理沙 | 249 | 1123 | 788 |
| 0.474838904611 | 星熊勇儀 | 茨木華扇 | 23 | 228 | 116 |
| 0.471282529366 | 物部布都 | 豊聡耳神子 | 35 | 230 | 219 |
| 0.470335114779 | 多々良小傘 | 東風谷早苗 | 55 | 191 | 516 |
| 0.463467329564 | 八坂神奈子 | 東風谷早苗 | 41 | 130 | 516 |
| 0.458571493885 | 東風谷早苗 | 洩矢諏訪子 | 45 | 516 | 154 |
| 0.454412940559 | 古明地さとり | 火焔猫燐 | 33 | 380 | 137 |
| 0.451740069099 | パチュリー・ノーレッジ | 小悪魔 | 80 | 383 | 501 |
| 0.451734260935 | 八雲紫 | 八雲藍 | 67 | 508 | 292 |
| 0.446079627968 | パチュリー・ノーレッジ | 紅美鈴 | 77 | 383 | 491 |
| 0.439212242507 | 蓬莱山輝夜 | 藤原妹紅 | 31 | 197 | 269 |
ソースコード
実験に使ったキャラ数や期間が違うので、上の結果とはちょっと違う値が出るかもしれません
何件以上の動画に出てきたかのしきい値が低いとキャラ数の組み合わせが多くなってしまい、検索APIを叩くのに1秒ずつ間を開けているので、データを取るのに数時間かかる場合があるので注意
# -*- coding: utf-8 -*- import math from NiconicoSnapshotAPIWrapper import * def npmi(px, py, pxy): if pxy == 0: return -1 if px == py == pxy: return 1 return -math.log(pxy / px / py) / math.log(pxy) if __name__ == '__main__': api = NiconicoSnapshotAPIWrapper('NiconicoSnapshotAPIWrapper')#アプリ名を入れてください freq = {} freq[u'東方'] = api.query(u'東方', size = 0, filters=[api.makeFilterRange('start_time', '2014-01-01 00:00:00', '2014-11-08 00:00:00')]).total data = u"""博麗霊夢 霧雨魔理沙 ルーミア 大妖精 チルノ 紅美鈴 小悪魔 パチュリー・ノーレッジ 十六夜咲夜 レミリア・スカーレット フランドール・スカーレット レティ・ホワイトロック 橙 アリス・マーガトロイド リリーホワイト ルナサ・プリズムリバー メルラン・プリズムリバー リリカ・プリズムリバー 魂魄妖夢 西行寺幽々子 八雲藍 八雲紫 伊吹萃香 リグル・ナイトバグ ミスティア・ローレライ 上白沢慧音 因幡てゐ 鈴仙・優曇華院・イナバ 八意永琳 蓬莱山輝夜 藤原妹紅 メディスン・メランコリー 風見幽香 小野塚小町 四季映姫・ヤマザナドゥ 射命丸文 秋静葉 秋穣子 鍵山雛 河城にとり 犬走椛 東風谷早苗 八坂神奈子 洩矢諏訪子 永江衣玖 比那名居天子 キスメ 黒谷ヤマメ 水橋パルスィ 星熊勇儀 古明地さとり 火焔猫燐 霊烏路空 古明地こいし ナズーリン 多々良小傘 雲居一輪 雲山 村紗水蜜 寅丸星 聖白蓮 封獣ぬえ 姫海棠はたて 幽谷響子 宮古芳香 霍青娥 蘇我屠自古 物部布都 豊聡耳神子 二ッ岩マミゾウ 秦こころ わかさぎ姫 赤蛮奇 今泉影狼 九十九弁々 九十九八橋 鬼人正邪 少名針妙丸 堀川雷鼓 綿月豊姫 綿月依姫 レイセン サニーミルク ルナチャイルド スターサファイア 茨木華扇 本居小鈴 森近霖之助 稗田阿求 宇佐見蓮子 マエリベリー・ハーン""".split() for word in data: time.sleep(1) freq[word] = api.query(u'東方 ' + word, retry = 3, size = 0, filters=[api.makeFilterRange('start_time', '2014-01-01 00:00:00', '2014-11-08 00:00:00')]).total cofreq = {} for word1 in data: for word2 in data: if word2 <= word1: continue time.sleep(1) cofreq[word1 + u' ' + word2] = api.query(u'東方 ' + word1 + ' ' + word2, retry = 5, size = 0, filters=[api.makeFilterRange('start_time', '2014-01-01 00:00:00', '2014-11-08 00:00:00')]).total TH = 100 #しきい値 import numpy as np result = [] for word1 in data: if freq[word1] < TH: continue px = float(freq[word1]) / freq[u'東方'] for word2 in data: if word2 <= word1 or freq[word2] < TH: continue py = float(freq[word2]) / freq[u'東方'] pxy = float(cofreq[word1 + u' ' + word2]) / freq[u'東方'] result.append((npmi(px, py, pxy), word1, word2, cofreq[word1 + u' ' + word2], freq[word1], freq[word2])) result.sort(reverse=True) for r in result[:10]: print r[0], r[1], r[2], r[3], r[4], r[5]
Isomapで東方キャラの関連性のデータを二次元に可視化する
昨日の記事で得られた東方キャラ同士の関連性の強さ(NPMI)を使って、Isomapという手法でキャラを二次元上に配置して可視化します
東方キャラの関連性の強さをニコニコ動画の動画数で測ってみた - 唯物是真 @Scaled_Wurm
Isomapの概要
Isomapは非線形次元削減、あるいは多様体学習の手法の一つです
非線形次元削減では、与えられたデータの元の次元数での情報をできるだけ失わないように、より低次元に埋め込みます
多くの手法では元のデータでの近傍(近いデータ点)や距離を保存するような埋め込みを行います
最近流行りのニューラルネットワークも内部では非線形次元削減的なことをしていて、単語を密なベクトルに変換するword2vecなどは、単語同士の意味の足し算引き算がうまくいくことがあっておもしろいです
ちなみにscikit-learnの多様体学習の説明は、実行結果の図や計算量などいろいろのっていてわかりやすいです
Isomapのアルゴリズム
Isomapでは以下のような処理を行います
- データ点のk近傍(k番目までの近いデータ点)を辺でつないだグラフを作る(グラフ理論とかのグラフです)
- 辺はデータ点の距離で重み付けします
- グラフ上でのデータ点同士の最短距離を測る
- 上で計算した距離との誤差が小さいような低次元の埋め込みを求める
簡単な例を示します
以下の様なデータが与えられたとします

これの近傍をつなぐと次のようになります

これは曲がりくねった曲線上の点であると解釈することができ、点の近傍の関係を保ったまま1次元に埋め込むことができます
このようにデータをより低次元の構造で表せる的なのが多様体の考え方です(?)
非線形次元削減では距離の決め方や近傍の定義がとても重要になります
例えばベクトルの次元ごとに値の範囲の大小が違うと悪影響があったりします(絶対値の大きな次元のほうがユークリッド距離では大きな影響を与える)
東方キャラを2次元に埋め込む
基本的な流れは、まず1次元1キャラで対応付けてキャラごとに相手キャラとのNPMIの値のベクトルを作ります
次にIsomapを適用して2次元に埋め込んで可視化します
scikit-learnのIsomapの実装はユークリッド距離しか使えないので、ベクトルの方を工夫してうまく似たキャラが近くになるようにします
具体的には各キャラのベクトルごとに、NPMIが上位の相手キャラだけの値を入れて、他のキャラ(次元)の値は0とします
NPMIが高いキャラ同士が近くなって欲しいのでこのようなベクトルにします
また同様の理由でIsomapで近傍グラフを作るときのkの数は3としました
結果
クリックすると拡大した画像が表示されます(100件以上の動画に出現したキャラしか使っていないので、見つからないキャラについては、お察しください)

各キャラの近くのキャラ同士はだいたい関係のあるキャラ同士が集まっています
紅魔館などが離れたところに島を形成しているのは、グループ内だけで近傍の関係がほぼまとまっていたからだと考えられます
可視化としては全体的にはあまりよい見た目ではありませんが、データ点が重ならないようにばらけさせるような処理があるわけではないので、こんな感じになります
参考
Isomapの説明は日本語だと以下のスライドがわかりやすいと思います
Rでisomap(多様体学習のはなし)
Isomapと似た方法で多次元尺度構成法(Multidimensional scaling, MDS)という手法があります
これはIsomapとは違い、元の次元数でのすべてのデータ点の間の距離を保存するような埋め込みを行う手法です
ソースコード
後で足します