【自然言語処理】2.裁判所の判例で判例分類(NMF編)

# 流れ

前回、word2vecで判例内にある単語の類似度を調べました。
今回はword2vecではなく、Non-negative Matrix Factorization(NMF; 非負値行列因子分解)をという手法を用いて次元圧縮し、そのあと、ランダムフォレストを用いて判例の分類を行ってみたいと思います。
判例の解析の流れは以下の通りです。

1.判例文書の分かち書き(MeCab)
↓
2.TF-IDFによる文書単語行列の作成
↓
3.NMFによる次元圧縮
↓
4.ランダムフォレストによる判例文書の分類

# 1. 判例文書の分かち書き
前回、mecab-ipadic-NEologdを用いて、単純に文書を分かち書きしましたが、
今回は品詞を動詞、形容詞、名詞(名詞-数以外)に絞ります。

```
import MeCab
import codecs

#判例のファイルリストを入力
input_list = "hanrei.list"

words_by_lines_mecab = []
for file in open(input_list,"r"):
    words_by_lines_in = []
    file = file.strip('\n')
    outfile = file.replace('.txt', "_mecab.txt")
    f = codecs.open(file, 'rb', 'utf-8')
    lines = f.read()
    lines=lines.replace('\n','')
    m = MeCab.Tagger('-Ochasen')
    words = m.parse(lines).splitlines()

    for l in words:
        chunks = l.split('\t')

        #品詞の絞込み
        if len(chunks) > 3 and (chunks[3].startswith('動詞') or chunks[3].startswith('形容詞') or (chunks[3].startswith('名詞') and not chunks[3].startswith('名詞-数'))):
        
        
    words_by_lines_mecab.append(words_by_lines_in)
```

これで、2017年の各判例で品詞を絞り込んだリスト(words_by_lines_mecab)が完成です。

# 2. TF-IDFによる文書単語行列の作成
分かち書きしたものをNMFにかけるために、単語を数値化した文書単語行列を作成します。
まずは、単純に文書内にある単語の数で文書単語行列を作成してみます。

```
import numpy as np
from array import array
from scipy.sparse import csr_matrix
from collections import Counter, defaultdict

def countvectorizer(documents):
    vocabulary = defaultdict()
    vocabulary.default_factory = vocabulary.__len__
    indices = array('i')
    indptr = array('i')
    indptr.append(0)
    for document in documents:
        for word in document:
            indices.append(vocabulary[word])
        indptr.append(len(indices))

    indices = np.frombuffer(indices, dtype=np.intc)
    indptr = np.frombuffer(indptr, dtype=np.intc)
    values = np.ones(len(indices))

    X = csr_matrix((values, indices, indptr),
                   shape=(len(indptr) - 1, len(vocabulary)))

    X.sum_duplicates()
    return X

matrix =  countvectorizer(words_by_lines_mecab)
print(matrix.toarray())
```
これで以下のように、各判例文書の各単語数が行ごとに格納されます。
実は、Scikit-learnのCountVectorizerを使えば、上記のような関数はわざわざ作成する必要はありませんが、一応その中身を見てみました。

```
#matrixの内容

[[  6.  11.   2. ...,   0.   0.   0.]
 [  3.   3.   0. ...,   0.   0.   0.]
 [ 14.  14.   3. ...,   0.   0.   0.]
 ..., 
 [  8.  13.   0. ...,   0.   0.   0.]
 [  7.  16.   0. ...,   0.   0.   0.]
 [  4.   4.   0. ...,   1.   1.   1.]]
```

ただ、単純に単語数を数えたものを使っても面白くないので、TF-IDFにかけてみます。
TF-IDFのアルゴリズムについては以下のサイトをご覧ください。

[TF-IDFアルゴリズム](https://qiita.com/AwaJ/items/5937665d5a4152cc24cf)

簡単にいうと、文書内での出現回数が多い(TFの値が大きい)　かつ他の文書であまり出てこない単語で文書を特徴付けることが出来ます。



TF-IDFですが、Scikit-learnのTfidfVectorizer使えば、簡単に計算できます。

```
from sklearn.feature_extraction.text import TfidfVectorizer

tfidf_transformer = TfidfTransformer(norm='l2', sublinear_tf=False)
tfidf = tfidf_transformer.fit_transform(matrix)
print(tfidf.toarray())
```

これで以下のようにTF-IDFをかけた文書単語行列を作成できます。

```
#tfidf

[[ 0.04  0.07  0.02 ...,  0.    0.    0.  ]
 [ 0.03  0.03  0.   ...,  0.    0.    0.  ]
 [ 0.    0.    0.   ...,  0.    0.    0.  ]
 ..., 
 [ 0.01  0.02  0.   ...,  0.    0.    0.  ]
 [ 0.04  0.09  0.   ...,  0.    0.    0.  ]
 [ 0.04  0.04  0.   ...,  0.06  0.06  0.06]]
```

#3.NMFによる次元圧縮
上記で計算したままでは、スパースな行列(0が多い行列)なので、特徴を出すためにNMFによって次元圧縮をかけてみます。
その前にNMFのアルゴリズムについて述べます。
与えられたI×Jサイズの非負値行列をXとすると、NMFは以下のようにI×Kサイズの非負値行列WとK×Jサイズの非負値行列Vに分解できます。

```math
X　= WV
```
それぞれの行列の要素$x_{ij}$、$w_{ik}$、$v_{kj}$はすべて0以上である。以下のように、

```math
\boldsymbol{w}_i= [w_{1i},\cdot\cdot\cdot,w_{Ki}]^{T} \\

\boldsymbol{v}_i= [v_{1j},\cdot\cdot\cdot,v_{Kj}]^{T}
```
とすると、これらの内積は、

```math

\boldsymbol{w}_i^{T}\boldsymbol{v}_j =\sum_{k=1}^{K}w_{ik}v_{kj}=x_{ij}

```
となる。

この分解を数値的に行うために、行列XとWHの距離D(X,WH)を定義し、これを最小化することを考える。NMFでよく用いられる距離は以下の3種類である。

* Eu: ユークリッド距離の2乗
* KL: 一般化Kullback-Leibler divergence
* IS: Itakura-Saito divergence

距離D(X,WH)はそれぞれ、

```math

D_{*}(X,WH)=\sum_{i=1}^{I}\sum_{j=1}^{J}d_{*}(x_{ij},\boldsymbol{w}_i^{T}\boldsymbol{v}_j)
```
で定義され、$d_{*}$は以下のとおりである。

```math

d_{Eu}(x_{ij},\boldsymbol{w}_i^{T}\boldsymbol{v}_j)=(x_{ij}-\boldsymbol{w}_i^{T}\boldsymbol{v}_j)^{2} \\ \\
d_{KL}(x_{ij},\boldsymbol{w}_i^{T}\boldsymbol{v}_j)=x_{ij}\log \frac{x_{ij}}{\boldsymbol{w}_i^{T}\boldsymbol{v}_j} -x_{ij}+\boldsymbol{w}_i^{T}\boldsymbol{v}_j \\ \\

d_{IS}(x_{ij},\boldsymbol{w}_i^{T}\boldsymbol{v}_j)=\frac{x_{ij}}{\boldsymbol{w}_i^{T}\boldsymbol{v}_j}-\log \frac{x_{ij}}{\boldsymbol{w}_i^{T}\boldsymbol{v}_j}-1
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/212602/2390eaed-cd23-0799-9ded-da5f099958db.png)
(引用: 非負値行列因子分解 NMF の基礎とデータ／信号解析への応用 澤田宏 著)

それぞれの特徴は上記の図のとおりです。
NMFの更新アルゴリズムは非負値行列因子分解 NMF の基礎とデータ／信号解析への応用を参照してください。

では、実装してみます。Scikit-learnを使えば簡単です。さきほどの距離はデフォルトでユークリッド距離を指定しています。

```
from sklearn.decomposition import NMF

def NMF_ana(matrix,n_c):
    model = NMF(n_components=n_c, init='random', random_state=0) # n_componentsで特徴の次元を指定
    W = model.fit_transform(matrix) # 学習
    V = model.components_
    return W,V
```

まずは、基底の数(n_components;K)を変えて、$||X-WV||$の収束を見てみます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/212602/48c8ccfb-5fc2-73bc-0586-4b14dff61f14.png)

基底の数が5になるところまで、急激に下がっていることがわかります。とりあえず、基底の数を5として、NMFをやってみた結果を以下に示します。

```
#W(文書(縦)×特徴(横))

[[ 0.07  0.    0.    0.01  0.  ]
 [ 0.02  0.    0.    0.    0.  ]
 [ 0.11  0.05  0.1   0.    0.69]
 ..., 
 [ 0.3   0.03  0.    0.    0.01]
 [ 0.1   0.    0.    0.    0.  ]
 [ 0.03  0.01  0.    0.    0.  ]]


#V(特徴(縦)×単語(横))

[[  5.59e+01   9.18e+01   3.55e+00 ...,   2.13e-04   2.13e-04   2.13e-04]
 [  8.30e+00   1.72e+01   4.02e-01 ...,   1.10e-06   1.10e-06   1.10e-06]
 [  1.71e+01   3.08e+01   4.49e-01 ...,   0.00e+00   0.00e+00   0.00e+00]
 [  7.81e+01   7.80e+01   7.13e+00 ...,   0.00e+00   0.00e+00   0.00e+00]
 [  0.00e+00   0.00e+00   0.00e+00 ...,   0.00e+00   0.00e+00   0.00e+00]]
```


#4.ランダムフォレストによる判例文書の分類
ここまで、NMFで判例を圧縮してきました。今度はW(文書×特徴)を説明変数として、判例の分類を行ってみます。目的変数はどんな判例かのラベルを用いたいのですが、今回は簡単に特許の判例かその他かで分類してみたいと思います。判例を特許で検索して、一つでも引っかかれば、その判例は特許のものとします。

```
#list_yは特許を1にして、その他を0に割当て、Wの文書方向と同じ並び

#学習用データ(75%)とテストデータ(25%)に分ける
x_train, x_test, y_train, y_test = train_test_split(W, list_y, random_state=0)

#ランダムフォレストモデル作成
forest = RandomForestClassifier(min_samples_leaf=3, random_state=0)
forest.fit(x_train, y_train)
```

モデルおよび分類予測の評価は以下の通りでした。
そこそこ高い結果になっているように見えます。

* 学習データ正解率: 95%
* テストデータ正解率: 89%

また、各特徴の重要度は、以下のとおりです。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/212602/fdb5b4f0-1e17-18c3-4810-b7a6b68ce7d0.png)

これを見ると、0から数えて、4番目の特徴が強く出ています。
ついでに、特許という単語の特徴ベクトルを以下に示します。

```
#特許

[  0.00e+00   1.16e-03   0.00e+00   1.12e+01   4.61e+01]
```
これをみると、0から数えて4番目の特徴が高く出ています。
なので、この4番目の特徴は特許を色濃く反映してものだといえます。

NMFで精度が高いように見えますが、NMFをかけずにTF-IDFだけでランダムフォレストによる分類を行ってみると、以下のようにNMFよりも高い結果になりました。

* 学習データ正解率: 98%
* テストデータ正解率: 97%

これは、今回判例内の単語検索で目的変数を作成したので、下手に圧縮するよりも特許の単語数を直接見たほうが良い結果が出るはずです。
また、NMFでは文書内にまんべんなく文章の特徴が現れていれば良いのですが、文章中の一部だけになるとその特徴が周りのコンタミによって薄くなる可能性はあると思います。

今回は目的変数がいまいちいけてないですが、目的変数を文書とは一見関係ないものでつけてみたいですね。