アラカン"BOKU"のITな日常

文系システムエンジニアの”BOKU”が勉強したこと、経験したこと、日々思うこと。

試行錯誤その1:自然言語のネガポジ判定をpythonでやってみよう。

自然言語処理の話題でよく目にするネガポジ判定。

 

単語感情極性なんて言い方もあるみたいですが・・。

 

ようするに、文章を解析してネガティブな文章かネガティブな文章かの2択で判断するというだけのことなのですけど。

 

自然言語処理の基本の勉強を兼ねて、いろいろ試してみるには、とっつきやすいテーマですし、面白そうです。

 

早速、初めてみようかと・・。

 

まずはネガポジ辞書を作らないとはじまらない

 

ネガポジ判断には、ネガポジ辞書が必要です。

 

作成して公開いただいているものもあるので、それを使う手もあるかなとは思いました。

 

例えば「日本語評価極性辞書」とか「単語感情極性対応表」です。

 

でも、今回は使わないことにしました。

 

理由は単純で、既存の辞書に勝手に新しい語を追加できないからです。

 

やっぱり、辞書はどんどん更新しないといけないものだという気があるので。

 

なので、ネガポジ辞書を自前で作る方法を考えてみます。

 

難しい理論はわからないのでシンプルに考える

 

辞書の仕様はとりあえず、シンプルにしてみます。

 

単語ごとに、ネガティブな文章に出現した割合と、ポジティブな文章に出現した割合の両方を保持するものにします。

 

同じ単語が、ポジティブな文章にも、ネガティブな文章にも現れるケースがあるので、単純に「ポジ」か「ネガ」かの二択にはできないと考えたからです。

 

例えば、ある単語は60%ポジティブで、40%ネガティブだ。

 

この方が現実に近い気がするんですよね。

 

だから、両方持っておいて、文章を解析しながらポジティブレートとネガティブレードをそれぞれ合計して、どちらの比率が高いかでネガかポジを判断するみたいな使い方をしたら面白いものになるんじゃないかと思ってます。

 

まあ、本当に思惑通り、良好な結果がでるかどうか?はわかりません。

 

が、まあ、とりあえず辞書を作って学習させてやってみたら、今気付いてないことが色々でてきて、最終的に面白いものができるかもしれませんから。

 

思いついたら、やってみる・・ですね。

 

最初の思いつき

非常にシンプルに考えれば。

 

単語単位でネガティブかポジティブかのスコアをつけた辞書があれば、それを適用してスコアを計算すれば、いけるんじゃないか?

 

そう思うわけです。

 

ただ、ある単語がネガティブかポジティブかの判断は人によって違うかもしれないので、自分の判断で辞書を作って判定して、自分が納得できるかでやってみます。

 

まず、ネガポジ辞書管理クラスを作る

アイディアはこうです。

 

単語ではなく、文章をもとデータにする。

 

その文章単位でネガティブかポジティブかを指定します。

 

で・・、ネガティブな文章に登場する単語はネガティブなスコアにカウントする。

 

逆にポジティブな文章に登場する単語はポジティブにカウントする。

 

それを繰り返すことを学習と考えれば、同じ単語でも、ポジティブ・ネガティブの各文章に登場する頻度によって、例えばポジティブ 60%、ネガティブ40%みたいなスコアができあがります。

 

それを適用して、評価する文章に登場する単語のスコアを加算していけば、合計でポジティブかネガティブを判定できるんじゃないか?

 

ということです。

 

悪くない気はしますが、試してみます。

 

pythonのクラスでネガポジ辞書管理を実装してみます

 

名前は「NgpgDict」にします。

 

辞書には、単語ごとに、ポジティブ・ネガティブそれぞれの出現回数を保持するカウンターと、都度再計算してもとめるレート(ポジティブレート、ネガティブレート)を持ちます。

 

学習時には、ポジティブとして加算(add_p)とネガティブとして加算(add_n)して、都度、レートを計算しておきます。

 

学習結果はJSON形式の辞書ファイルに保存(save())します。

 

その辞書はコンストラクタで読み込むようにします。

 

それで文章のネガポジ判断をする時は、ポジティブレート(get_prate)と、ネガティブレート(get_nrate)を取り出して、加算していって、文字数カウントした結果か、TF-IDF等で処理した結果に掛け合わせて、ネガポジ判定する・・みたいな使い方を想定した仕様にします。

 

まず、クラス定義とコンストラクトと保存(save())のソースです。

 

当面、自分しか使わないので、辞書ファイルを直書きしたりしてるのは、ご愛嬌とさせてください。

 

from janome.tokenizer import Tokenizer # 形態素解析用
import os
import json

class NgpgDict:
    def __init__(self):
        self.__save_file_name = "ngpgdict.json"
        self.__total = 'Total'
        self.__pcount = 'pcount'
        self.__ncount = 'ncount'
        self.__prate = 'prate'
        self.__nrate = 'nrate'
        if(os.path.isfile(self.__save_file_name)):
            fr = open(self.__save_file_name,"r")
            self.__word_dict = json.load(fr)
            fr.close()
        else:   
            self.__word_dict = {}
            self.__word_dict.setdefault(self.__total,{})
            self.__word_dict[self.__total].setdefault(self.__pcount,0)
            self.__word_dict[self.__total].setdefault(self.__ncount,0)
            self.__word_dict[self.__total].setdefault(self.__prate,0)
            self.__word_dict[self.__total].setdefault(self.__nrate,0)
   
    def save(self):
           fw = open(self.__save_file_name,"w")
           json.dump(self.__word_dict,fw,indent=4)
           fw.close()

 

あと、加算して都度レートを計算する部分ですね。

 

初めて登場する単語のための辞書の初期化とかしてるので、ごちゃごちゃ見えますが、やってることはカウンタを加算して、比率を求めなおしているだけです。

 

    def add_p(self,word,value):
        self.__word_dict.setdefault(word,{})
        self.__word_dict[word].setdefault(self.__pcount,0)
        self.__word_dict[word].setdefault(self.__ncount,0)
        self.__word_dict[word].setdefault(self.__prate,0)
        self.__word_dict[word].setdefault(self.__nrate,0)
        pc = self.__word_dict[word][self.__pcount]
        self.__word_dict[word][self.__pcount] = pc + value
        tc = self.__word_dict[self.__total][self.__pcount] 
        self.__word_dict[self.__total][self.__pcount] = tc+ value
        pcu = self.__word_dict[word][self.__pcount]
        ncu = self.__word_dict[word][self.__ncount]
        tcu = self.__word_dict[word][self.__pcount] + self.__word_dict[word][self.__ncount]
        self.__word_dict[word][self.__prate] = pcu / tcu
        self.__word_dict[word][self.__nrate] = ncu / tcu
    
    def add_n(self,word,value):
        self.__word_dict.setdefault(word,{})
        self.__word_dict[word].setdefault(self.__pcount,0)
        self.__word_dict[word].setdefault(self.__ncount,0)
        self.__word_dict[word].setdefault(self.__prate,0)
        self.__word_dict[word].setdefault(self.__nrate,0)
        pc = self.__word_dict[word][self.__ncount]
        self.__word_dict[word][self.__ncount] = pc + value
        tc = self.__word_dict[self.__total][self.__ncount]
        self.__word_dict[self.__total][self.__ncount] = tc+ value
        pcu = self.__word_dict[word][self.__pcount]
        ncu = self.__word_dict[word][self.__ncount]
        tcu = self.__word_dict[word][self.__ncount] + self.__word_dict[word][self.__pcount]
        self.__word_dict[word][self.__prate] = pcu / tcu
        self.__word_dict[word][self.__nrate] = ncu / tcu

 

getterにあたるレートを取り出す部分です。

文字が辞書になかったら、0.5(要するに中立)を返すようにしてます。

    def get_prate(self,word):
        if(word not in self.__word_dict):
            return 0.5
        else:
            return self.__word_dict[word][self.__prate]

    def get_nrate(self,word):
        if(word not in self.__word_dict):
            return 0.5
        else:
            return self.__word_dict[word][self.__nrate] 

 

あと、内容を全部書きだす処理(list_all)と辞書を初期化する(clear())です。

    def list_all(self):
        self.__recursive_dict__(self.__word_dict)
    
    def __recursive_dict__(self,argd):
        for k in sorted(argd.keys(),reverse=False):
            print("{0}:{1}:{2}".format(k,self.__pcount,argd[k][self.__pcount]))
            print("{0}:{1}:{2}".format(k,self.__ncount,argd[k][self.__ncount]))
            print("{0}:{1}:{2}".format(k,self.__prate,argd[k][self.__prate]))
            print("{0}:{1}:{2}".format(k,self.__nrate,argd[k][self.__nrate]))

    def clear(self):
        self.__recursive_clear__(self.__word_dict)

    def __recursive_clear__(self,argd):
        for k in sorted(argd.keys(),reverse=False):
            if(isinstance(argd[k],dict)):
                self.__recursive_clear__(argd[k])
            else:
                if(k != self.__total):
                    argd[k] = 0      

 

とりあえず、このくらいあれば、基本的なことはできるでしょう。

 

動作確認をしてみます。

n = NgpgDict()
n.clear()
n.add_p('てすと',1)
n.add_n('てすと',2)
n.add_p('てすと',3)
n.add_n('てすと',1)
n.add_p('てらす',3)
n.add_n('てらす',2)
n.add_n('困った',2)
n.add_n('困った',5)
print("positive-rate:{0} / negative-rate:{1}".format(str(n.get_prate('てすと')),str(n.get_nrate('てすと'))))
print("positive-rate:{0} / negative-rate:{1}".format(str(n.get_prate('てらす')),str(n.get_nrate('てらす'))))
print("positive-rate:{0} / negative-rate:{1}".format(str(n.get_prate('困った')),str(n.get_nrate('困った'))))

 

結果はこんな感じ。

positive-rate:0.5714285714285714 / negative-rate:0.42857142857142855
positive-rate:0.6 / negative-rate:0.4
positive-rate:0.0 / negative-rate:1.0

 

 検算してみると、あってる。

 

とりあえず、初期化して<n = NgpgDict()>、学習して<add_p,add_n>、保存する<save()>の3つだけ使ってグルグルまわしておけば良いので、学習させるプログラムもシンプルに書けそうな感じにはできましたし。

 

一旦、第一弾としては悪くないかな。