書きかけの記事とか書いたけど公開してない記事が溜まっているので2014年になったのを機に公開して供養していく
以下の記事はニコニコデータセットを利用しています
ネットスラングの草「w」は以下のように笑いとか強調を表す意味で使われます
吹いたw ちょまwww クソワロタwwwwwwwwwwwwwww
最近国語辞典に載ったことでも話題になりました
すごい!国語辞典に、この意味を載せたのは初めてですよ!
ダブリュー[W](名)①②③省略④〔←warai=笑い〕〔俗〕〔インターネットで〕(あざ)笑うことをあらわす文字。「まさかwww」(以下略)
(三省堂国語辞典第七版) pic.twitter.com/bFbb1HI3XE
— 三国ことは@三国ちゃん/紅白閲覧 (@sankokuziten) 2013, 12月 15
- 作者: 見坊豪紀,市川孝,飛田良文,山崎誠,飯間浩明,塩田雄大
- 出版社/メーカー: 三省堂
- 発売日: 2013/12/11
- メディア: 単行本
- この商品を含むブログ (2件) を見る
ちなみにニコニコ動画のコメントの3文の1には草が生えています
草「w」が生えているコメントと生えていないコメントでは、含まれている単語にどのような違いがあるか調べてみました。
ちなみにこういったネットに特徴的な表現を用いて知識を獲得するという話はいくつか研究されています。
↓以前紹介した論文では同じ文字を連続して書く強調表現がよく使われる単語について調べています
方法
ある単語が含まれるコメントに草「w」が含まれている条件付き確率を使ってランキングします
単純に草「w」を含むコメントに含まれる回数の多い単語を調べると、草「w」と関係なくたくさん出現する単語もでてきてしまうので、このような条件付き確率を使います
不確かな結果を取り除くために、ある閾値以上のコメントに含まれていた単語について調べました
2つの事象が独立に起きたか関連して起きたかを測る尺度はいろいろありますが、その内の一つにPMI(Pointwise mutual information)というものがあります
$$\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)}$$
PMIは2つの事象の同時確率を、それぞれの事象の確率で割った式になっています
コメントに草「w」が含まれる事象\(www\)と、ある単語が含まれる事象\(word\)とするとPMIは以下のようになり
$$\operatorname{pmi}(www;word) = \log\frac{p(www,word)}{p(www)p(word)}$$
今回は\(p(www)\)は固定なので取り除き、更に\(\log\)も外すと、以下の条件付き確率と同じになります
$$p(www|word) = \frac{p(www, word)}{p(word)}$$
最初に書いたようにこの条件付き確率(ある単語が含まれるコメントに草「w」が含まれている確率)を使ってランキングします
結局条件付き確率を使うんだからPMIの説明いらないんじゃ……とも思うかもしれませんが、共起の強さを調べる話ではよく出てくるので覚えておくといつか役立つかもしれません(?)
結果
100000個以上出現した単語を対象として、条件付き確率でランキングしました
形態素解析器のMeCab(辞書はUniDic)を使って単語分割をしています
単純にすべての単語の頻度を数えるとメモリが足りなくなるので、メモリの節約のため以下の記事の方法で頻度には最大2万強程度の誤差を許してカウントしています
単語ユニグラム
以下が各単語についての結果です
だいたい「w」と関係ありそうな単語がとれています
特に「市場」や「タグ」「腹筋」「次長」などの笑いをあらわしているけど、直接的にはそういう意味でない単語も得られています
しかし「ぶね」や「ロタ」など単語分割の誤りによると思われる単語がたくさん含まれています
単語 | 条件付き確率 | 単語を含むコメント数 |
---|---|---|
ちょ | 0.84656427565 | 20716010 |
ばかす | 0.831706118834 | 144521 |
ぶね | 0.831159795415 | 241465 |
やめれ | 0.794905592405 | 235945 |
クソワロタ | 0.78850685975 | 363643 |
イミフ | 0.779140005262 | 178629 |
ふい | 0.775312211945 | 1392964 |
吹い | 0.767270617747 | 4042270 |
クソワロタ | 0.763314788542 | 125462 |
ひでぇ | 0.753735539681 | 1705443 |
ちげえ | 0.749309781006 | 373070 |
ついな | 0.747701853838 | 104432 |
吹く | 0.724741537291 | 578420 |
シュール | 0.721803628421 | 706864 |
はらい | 0.71619302008 | 233613 |
おちつけ | 0.713128182815 | 225233 |
ひでえ | 0.707687424747 | 2640273 |
噴い | 0.696182575471 | 145255 |
わろ | 0.695727399151 | 1873215 |
ワロス | 0.689259619461 | 160404 |
おい | 0.689069389403 | 11658077 |
市場 | 0.67884457184 | 1244768 |
カオス | 0.677287903151 | 1758772 |
腹痛 | 0.677168722642 | 201040 |
ロタ | 0.673225452595 | 1961796 |
タグ | 0.670385485261 | 4679712 |
んな | 0.66920936007 | 982685 |
おもしれえ | 0.667895822751 | 159459 |
はええ | 0.665946171718 | 383070 |
うける | 0.665838218668 | 611183 |
腹筋 | 0.66388515156 | 919403 |
こら | 0.660185701575 | 1174465 |
自重 | 0.652157771211 | 3224137 |
テラ | 0.649580230529 | 1199587 |
次長 | 0.645315731035 | 98169 |
盛大 | 0.644173657955 | 124083 |
フイ | 0.636872454108 | 94515 |
どす | 0.636105786809 | 180514 |
うるせえ | 0.633580450735 | 634786 |
ひどい | 0.63328878178 | 4332060 |
おせえ | 0.629643049402 | 193192 |
いてぇ | 0.628084315998 | 352104 |
なげえ | 0.627076589109 | 421966 |
わらっ | 0.625973874844 | 171865 |
うっせえ | 0.625940442038 | 123745 |
エコー | 0.623771637783 | 198842 |
いてえ | 0.623406473737 | 215795 |
なげ | 0.619582462414 | 277963 |
なんぞ | 0.619292533658 | 1570421 |
テラ | 0.619231280755 | 150380 |
単語バイグラム
連続した2つの単語を一つのまとまりとみなして頻度を数えた場合の結果がこちらです
単語ユニグラムの方では「ぶね」となっていたものがこちらでは「あ」「ぶね」のバイグラムとして取ることができています
以下の表を見ると本来一つの単語になるべき多くの表現が不適切に分けられていそうだということがわかると思います
単語1 | 単語2 | 条件付き確率 | 単語を含むコメント数 |
---|---|---|---|
あ | ぶね | 0.934244083747 | 151743 |
やめ | い | 0.870483874927 | 122201 |
お | ま | 0.86705000784 | 2595773 |
ひ | どす | 0.860346102902 | 116266 |
う | は | 0.831733276174 | 1940764 |
て | んな | 0.831139013643 | 188593 |
バカ | ス | 0.829278681687 | 225379 |
しん | だ | 0.828024446513 | 98828 |
ふい | た | 0.825762355332 | 1266437 |
あ | ほす | 0.823392583205 | 119944 |
ね | え | 0.818281289313 | 1196327 |
ちょ | ま | 0.817135200175 | 109704 |
ひっ | でぇ | 0.813445464335 | 97505 |
バ | ロス | 0.799840395739 | 201749 |
くそ | わろ | 0.797980223411 | 213687 |
なつ | い | 0.791029269367 | 240627 |
あ | ほ | 0.79043219382 | 97410 |
うっ | は | 0.786982214004 | 171427 |
っ | ちょ | 0.785347615992 | 152726 |
吹い | た | 0.780489913069 | 3875708 |
で | 吹く | 0.779675111474 | 139494 |
だれ | う | 0.777064729896 | 124193 |
ぶ | は | 0.760263061304 | 264273 |
出 | 落ち | 0.760138582841 | 153843 |
ー | せん | 0.755296579213 | 171101 |
おもしろ | すぎる | 0.755201581691 | 107227 |
腹痛 | い | 0.753969038673 | 141596 |
な | ついな | 0.752863547071 | 99265 |
歌う | な | 0.751260982753 | 122920 |
また | か | 0.750090730458 | 1543032 |
やる | やる | 0.749787256675 | 150416 |
ひっ | で | 0.749130963847 | 252291 |
腹筋 | が | 0.747277277203 | 270134 |
っ | を | 0.743200499318 | 94529 |
出 | オチ | 0.743044375645 | 193800 |
ち | げ | 0.741762273917 | 331129 |
だ | それ | 0.741309867787 | 205578 |
そこ | か | 0.732165582399 | 196446 |
そっち | か | 0.731009815407 | 241559 |
面白 | すぎる | 0.729674964169 | 121402 |
だ | いま | 0.728263153156 | 99957 |
おい | こら | 0.725972473881 | 155634 |
じゃ | ん | 0.72518171528 | 568059 |
に | 吹い | 0.721443379445 | 312572 |
わろ | す | 0.714949875914 | 244992 |
ぱ | ね | 0.714541009155 | 214318 |
笑い | すぎ | 0.712867054454 | 142690 |
噴い | た | 0.710249882959 | 136704 |
きめ | ぇ | 0.7102111714 | 1013821 |
なげ | ぇ | 0.707068064539 | 152899 |
まとめ
ニコニコ動画のコメントから、草「w」が生えている時に特徴的な単語を抽出した
2つの出来事の関係性をはかるときには単純に頻度を数えるんじゃなくてPMIとかを使ったほうがうまくいく
ニコ動のコメントとかの形態素解析はネットスラングとかが多くてうまくいかないので注意
コメント全部の単語バイグラムの頻度を数えようと思うと数十GBのメモリを使用したりするので、誤差を許すカウント法やあるいはランダムサンプリングなどで使用するメモリを減らさないといけないかも
ソースコード
コメント中の単語を数えて条件付き確率を計算するコード
ただし頻度にある程度の誤差を許す
入力は1行1文で、単語が空白で分かち書きされたテキスト(mecab -Owakatiを想定
# -*- coding: utf-8 -*- import sys import collections class LossyCounting(object): def __init__(self, epsilon): self.N = 0 self.count = {} self.bucketID = {} self.epsilon = epsilon self.b_current = 1 def getCount(self, item): return self.count[item] if item in self.count else 0 def __getitem__(self, item): return self.getCount(item) def getBucketID(self, item): return self.bucketID[item] def trim(self): for item in self.count.keys(): if self.count[item] <= self.b_current - self.bucketID[item]: del self.count[item] del self.bucketID[item] def addCount(self, item): self.N += 1 if item in self.count: self.count[item] += 1 else: self.count[item] = 1 self.bucketID[item] = self.b_current - 1 if self.N % int(1 / self.epsilon) == 0: self.trim() self.b_current += 1 def iterateOverThresholdCount(self, threshold_count): assert threshold_count > self.epsilon * self.N, "too small threshold" self.trim() for item in self.count: if self.count[item] >= threshold_count - self.epsilon * self.N: yield (item, self.count[item]) def iterateOverThresholdRate(self, threshold_rate): return self.iterateOverThresholdCount(threshold_rate * self.N) count = LossyCounting(1e-6) count_w = LossyCounting(1e-6) count_bi = LossyCounting(1e-6) count_w_bi = LossyCounting(1e-6) for line in sys.stdin: line = line.strip() if line == '': continue isW = line.decode('utf-8')[-1] == u'w' split = line.split() L = len(split) for w in set([split[i] for i in xrange(L)]): if w.decode('utf-8')[-1] != u'w': count.addCount(w) if isW: count_w.addCount(w) for before, w in set([(split[i - 1], split[i]) for i in xrange(1, L)]): if before.decode('utf-8')[-1] != u'w' and w.decode('utf-8')[-1] != u'w': count_bi.addCount((before, w)) if isW: count_w_bi.addCount((before, w)) pmi_like = [] for c, w in count.iterateOverThresholdCount(100000): pmi_like.append((float(count_w.getCount(c)) / w, c)) pmi_like.sort(reverse=True) for v, c in pmi_like[:100]: print c, v, count[c] print pmi_like = [] for c, w in count_bi.iterateOverThresholdCount(100000): pmi_like.append((float(count_w_bi.getCount(c)) / w, c)) pmi_like.sort(reverse=True) for v, c in pmi_like[:100]: print c[0], c[1], v, count_bi[c]