ArchLinux,MATLAB,制御理論など(の予定)

[rogy Advent Calendar 2016]にゃーんみが深い

2016-12-20

にゃーん(ニャーン):言葉にならない様。

rogy AdventCalendar 2016 #

この記事はrogy Advent Calendar 2016の20日目の記事です。 前記事はこちら

はじめに #

突然ですが、人生においてにゃーんみが深まることは多々ありますよね。 私はしょっちゅうにゃーんとか呟いています。

そこで、にゃーんみの深まりを肯定してくれるプログラムを書けば自己肯定感が高まると思ってつらつらプログラムを組んでいるところです。 なんとなく以下のような流れで出来ないかしらと思案中。

  1. 文章を形態素解析する
  2. 分解した各単語のネガポジ具合を示したベクトルを形成
  3. にゃーんみが深そうなツイートでのベクトルを学習して判定器を形成

今回は2の途中までを書きたいと思います。

文章を形態素解析する #

形態素解析とは #

形態素解析(けいたいそかいせき、Morphological Analysis)とは、文法的な情報の注記の無い自然言語のテキストデータ(文)から、対象言語の文法や、辞書と呼ばれる単語の品詞等の情報にもとづき、形態素(Morpheme, おおまかにいえば、言語で意味を持つ最小単位)の列に分割し、それぞれの形態素の品詞等を判別する作業である。 (Wikipediaより引用)

つまり、文章を構成する要素ごとに分けるというのが形態素解析です。英語だと単語ごとにスペースが置かれるので比較的単語に分けること自体は簡単そうですが、日本語は単語の区切りが機械的に判断しづらいことと活用が複雑なことがあり、なかなか面倒くさそうです。 まぁ、便利なライブラリがあるんですけどね(ばばーん

MeCabでお手軽形態素解析 #

形態素解析用のライブラリとしてMeCabというものがあります。なかなか高速で使いやすいです。 インストール方法は下記のURLに載っていますので省略しますが、ArchLinuxユーザーはAURからyaourtで簡単にインストールすることが出来ます。形態素解析に必要な辞書データもAURにあります。 mecab(AUR) mecab-ipadic(AUR) ただし、辞書に関しては現代語に上手く対応できないという難点があるので、以下の辞書を使っています。 mecab-ipadic-neologd 上記のURLの通りにneologdをインストールする際にipadicが必要になるのですが、yaourtでインストールしている場合neologdが想定しているディレクトリとは別のディレクトリにipadicがインストールされているので、libexec/make-mecab-ipadic-neologd.shの中で指定されているパスを適当な場所に変更する必要があります。 辞書を変更する時には、/etc/mecabrcのdicdirで辞書を指定することを忘れずに。

MeCabをインストールすると、こんな感じでコマンドライン上で形態素解析を試すことが出来ます。

1
2
3
4
5
6
7
8
9
10
11
12
13
% mecab
これはテストです
これ 名詞,代名詞,一般,*,*,*,これ,コレ,コレ
は 助詞,係助詞,*,*,*,*,は,ハ,ワ
テスト 名詞,サ変接続,*,*,*,*,テスト,テスト,テスト
です 助動詞,*,*,*,特殊・デス,基本形,です,デス,デス
EOS
にゃーんみが深い
にゃーん 副詞,一般,*,*,*,*,にゃーん,ニャーン,ニャーン
み 動詞,自立,*,*,一段,連用形,みる,ミ,ミ
が 助詞,格助詞,一般,*,*,*,が,ガ,ガ
深い 形容詞,自立,*,*,形容詞・アウオ段,基本形,深い,フカイ,フカイ
EOS

なんだかこれだけで一仕事終えた気がしてきます。(自己肯定感 ただし見て分かるようににゃーんみの「み」が動詞になっていたり多少正確でない部分はあります。そのような場合にはMeCabを自前でコスト学習データ込でビルドすると調整できるようですが、私は面倒くさいのでしていません。

色々な言語からMeCabを呼び出す #

私が行ったことがあるのはC++とrubyからの呼び出しです。rubyでの呼び出しに関してはnattoというMeCabバインディングのgemが存在します。 ただ、rubyで使ってみた感じ複雑なことをしようとすると(適当に実装したせいもありますが)遅いなという印象があるので、C++を使うのがおすすめです。 MeCabの呼び出しに関しては以下のサイトに詳しく載っています。 MeCabをC++から使ってみる さてさて、MeCabをC++から呼び出すことに成功したのでネガポジ判定に参りましょう。

分解した各単語のネガポジ具合を示したベクトルを形成 #

単語によって、ネガティブな印象やネガティブな印象が存在します。 例えば「悪い」という単語にはネガティブな印象を受けますが、 「良い」にはポジティブな印象を受けます。 このような各単語によるネガポジ度をまとめてくれた資料が以下に存在しました。 PN Table これは東工大の高村先生が作った判別表です。 ネガティブを-1、ポジティブを+1として間の実数でネガポジ判定をすることが出来ます。これを利用してネガポジベクトルを生成した。 実はMeCabでは辞書をビルドする際に任意のユーザ項目を作ることが出来ます。 なのでMeCabで形態素解析した瞬間にネガポジスコアを取得するということが可能になります。 ただし、PN Tableを細かく変更していく予定であり毎回ビルドしていられないので今回はcsvファイルを読み込んで自作したプログラム上で管理する方法にしました。 上記のPN Tableを加工してカンマ区切りのcsvにしたり読みをカタカナに変更してMeCabと合わせたりしました。

ネガポジベクトル生成の工夫 #

まぁ工夫というほど工夫でもないですが、mapを使って割と高速に単語とスコアの関係付を行いました。 mapの説明は以下のURL C++ 連想配列クラス std::map 入門 ハッシュマップを使えばもっと速くなりそうですが、ハッシュの知見がないので今回は使ってないです。そのうち書き換えます。 ただし、今回注意したいのがmapのkeyは単語をぶち込みたいという点です。単語には

  1. 表記
  2. 読み
  3. 品詞

という要素があります。同じ表記でも品詞が違ったりするわけです。 なので、mapのkeyに自作の構造体をぶち込むことになるのですが、mapのkeyは比較できる必要があるので比較規則を記述してやる必要があります。 なのでこんな構造体を宣言しておきました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#define unsigned long long ull
using namespace std;
struct kind{
string surface;//表記
string reading;//読み
string part;//品詞
//mapのkeyとして使うには大小関係を定義する必要がある
bool operator < (const kind& right ) const {
ull i=0;
//まずはsurfaceの一文字ずつで比較
while(i<this->surface.size() && i<right.surface.size()){
if(this->surface[i]!=right.surface[i])
return this->surface[i]<right.surface[i];
i++;
}
//小さい方まで見たが一緒だった場合は文字列の長さで見る
if(this->surface.size()!=right.surface.size())
return this->surface.size()<right.surface.size();
//長さも同じだった場合は以下reading,partを比較
i=0;
while(i<this->reading.size() && i<right.reading.size()){
if(this->reading[i]!=right.reading[i])
return this->reading[i]<right.reading[i];
i++;
}
if(this->reading.size()!=right.reading.size())
return this->reading.size()<right.reading.size();
i=0;
while(i<this->part.size() && i<right.part.size()){
if(this->part[i]!=right.part[i])
return this->part[i]<right.part[i];
i++;
}
if(this->part.size()!=right.part.size())
return this->part.size()<right.part.size();
//ここまで落ちてきた場合は全て同一
return false;
}
}

まぁ大小関係の記述の仕方はどのように記述しても良いと思いますがとりあえずこんな感じでいってみました。

ベクトルを作る #

そんなこんなでスコアを決定するmapを保持して形態素解析してみます。

  1. 文字をMeCabに投げる
  2. 帰ってきた分を’’でパース→単語ごとの情報になる
  3. 単語ごとの情報を’’でパース→表記とその他情報に別れる
  4. その他情報を’,’でパース→単語の情報になる
  5. 単語の方法から必要なものを抜き出してスコアを得る
  6. 各単語に繰り返してベクトルを形成

こんな感じです。 以下コード抜粋

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
bool generator::makeVectors(const string& str,
vector<string>& c,
vector<kind>& k,
vector<double>& s,
const ull size){
if(str.size()==0)return false;
cout << str << endl;
vector<string> wordData;
string data;
string surface;
vector<string> feature;
size_t pos;
kind tmp;
tmp.surface = "*";
tmp.reading = "*";
tmp.part = "*";
ull last;
const string c_def = "*";
const kind k_def = tmp;
const double s_def = 0.0;
//一行分の評価開始
//mecabで形態素解析を行い、各単語データにパース
wordData=split(tagger->parse(str.c_str()),'\n');
if(wordData.size()==0) return false;
//各単語データをもとに一行分のvectorを形成
c.assign(size,c_def);
k.assign(size,k_def);
s.assign(size,s_def);
wordData.size()<size?(last=wordData.size()):(last=size);
for(ull i=0;i<last;i++){
data=wordData.at(i);
pos = data.find('\t');
if(pos==string::npos||pos>=data.size()){
return data=="EOS";
}
else{
surface=data.substr(0,pos);
feature=split(data.substr(pos+1),',');
if(feature.size()>=8){
tmp.surface=surface;
tmp.reading=feature.at(7);
tmp.part=feature.at(0);
if(pnscore.find(tmp)!=pnscore.end()){
c.at(i)=surface;
s.at(i)=pnscore[tmp];
k.at(i)=tmp;
}
else{
c.at(i)=surface;
s.at(i)=s_def;
k.at(i)=k_def;
}
}
}
}
return true;
}

これを対話形式で出来るようになるとこんな感じになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
% ./irb (git)-[generator]
pn_table is loaded
irb mode
---------
これはテストです。
これはテストです。
これ,は,テスト,です,。,*,*,*,*,*,*,*,*,*,*,*,*,*,*,*
0.000000,0.000000,-0.138678,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000
average:-0.0069339
max:0
min:-0.138678
---------
明日は楽しいことが待っている
明日は楽しいことが待っている
明日,は,楽しい,こと,が,待っ,て,いる,*,*,*,*,*,*,*,*,*,*,*,*
-0.465200,0.000000,0.995837,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000
average:0.0265319
max:0.995837
min:-0.4652
---------
冬は眠い
冬は眠い
冬,は,眠い,*,*,*,*,*,*,*,*,*,*,*,*,*,*,*,*,*
-0.868095,0.000000,-0.765739,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000
average:-0.0816917
max:0
min:-0.868095
---------

なんとなくそれっぽい気がする…? 一応これでネガポジベクトルは生成できたわけですが、今度は辞書の精度が気になってきます。

1
2
3
4
5
6
7
8
9
---------
にゃんこ最高
にゃんこ最高
にゃんこ,最高,*,*,*,*,*,*,*,*,*,*,*,*,*,*,*,*,*,*
0.000000,-0.443009,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000
average:-0.0221504
max:0
min:-0.443009
---------

???????????
あれ、めっちゃポジティブな気持ちで打ったんだが? 実は他のPN Table利用者のブログ記事を読んでもネガティブなワードがめちゃくちゃ多いそうなのでちょっと怪しそう…?

辞書改変中 #

というわけで現在辞書を改変中です。 みんな大好きtwitterのTLから辞書のスコアを更新しています。 コンセプトとしては入力する文章集合Sに対して順番にインデックスが割り振られているとして、対応するネガポジベクトル集合Vを得るとき、

\[ S = \{S_1,S_2,...,S_n\}\\ V = \{V_1,V_2...,V_n\} \]

i+1番目のベクトルを生成するために使われるテーブルPNが以下のように計算されるように更新していくという方法です。ただし、kは任意の単語を示します。

\[ {PN}^{i+1}(k)=\alpha {PN}^{i}(k)+\beta f(V_i,k)+\gamma ave(V_i) \]

ただし各係数αβγの和は1になります。 fはViの隣接関係による影響を計算した後、kに関する影響を取り出すといった関数になります。(抽象的 今は隣接関係で重ね合わせる関数の形とパラメータをいじっているような段階です。 重ね合わせる関数はガウス分布のような形にすると助詞の値がめちゃくちゃになったりするのでsinc関数っぽい何かを与えてみたりと試行錯誤中です。

まとめ #

rogy AdC次回予告 #

次回はmaquinistaの記事です。APPARE!


Blog comments powered by Disqus