唯物是真 @Scaled_Wurm

プログラミング(主にPython2.7)とか機械学習とか

草「w」はどんなコメントに生えるか?

書きかけの記事とか書いたけど公開してない記事が溜まっているので2014年になったのを機に公開して供養していく

以下の記事はニコニコデータセットを利用しています

ネットスラングの草「w」は以下のように笑いとか強調を表す意味で使われます

吹いたw
ちょまwww
クソワロタwwwwwwwwwwwwwww

最近国語辞典に載ったことでも話題になりました

三省堂国語辞典 第七版

三省堂国語辞典 第七版

ちなみにニコニコ動画のコメントの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]