自然言語処理100本ノック：73で苦労した話

# 背景
自分が書いた前の記事、「[自然言語系サービス色々試してみた](https://qiita.com/silverbox/items/048804eba4a4c429bfe8)」の中でも引用させて頂いた「[言語処理100本ノック 2015](http://www.cl.ecei.tohoku.ac.jp/nlp100/)」。
各種自然言語サービスをもっと試す前に、基礎を固めておこうと思ってこの100本ノックを少しずつ進めている状況。
今までの課題は粛々と進めて、特筆すべき事は無く、Qiita書く程のネタは無かった。しかし、この課題73に関しては結構苦労したので、きっと参考になる人もいると思い、自分の復習も兼ねて記事にする事にした。

# 対象の課題

> **第8章: 機械学習**
> 本章では，Bo Pang氏とLillian Lee氏が公開しているMovie Review Dataのsentence polarity dataset v1.0を用い，文を肯定的（ポジティブ）もしくは否定的（ネガティブ）に分類するタスク（極性分析）に取り組む．

感情分析をする課題ブロックですね。

> **72. 素性抽出**
> 
> 極性分析に有用そうな素性を各自で設計し，学習データから素性を抽出せよ．素性としては，レビューからストップワードを除去し，各単語をステミング処理したものが最低限のベースラインとなるであろう．

本題の課題73で使用する、特徴としての単語集め。機械学習界で前処理とか言われる部分かな？
各自で設計し、とはいうものの、「学習データである所の文章中に学習対象単語があるかどうかフラグ」以外の設計はあまりない気がする。不要と思われるデータを除外するぐらいかな？
※ストップワード：a とか the とか I とか頻繁に出すぎて学習に向かない単語
※ステミング処理：say, said など活用による変化の原形に変換して１種類の単語とする処理

> **73. 学習**
>
> 72で抽出した素性を用いて，ロジスティック回帰モデルを学習せよ．

そして本題。説明文は１行で簡単に言ってくれています。一気にハードル上がった気がします。詳しくは次の節で説明。

# 完了までの道のり

以下のステップ一つ一つが課題になっててもおかしくない感じです。出来た今だから順調に書き出せてますが、とりかかった当初は右往左往。

### 学習データの行列化
機械学習するにはデータを行列化する必要がある（無くても頑張ればできるかもしれないが、まぁコード書くのも大変だし、実行速度も遅くなるだろう）。
課題72で、全学習データから抽出した単語が1000件、全学習データ10662件（課題71で作成）としたら、学習データレコードにその単語が含まれているかどうかの0/1フラグによる、10662×1000(+1 特徴と関係ない係数)の行列を作成。

```python:行列作成部
import numpy as np
def getMetrics(words_list, feature_wordlist):
    ret = []
    for words in words_list:
        xi = []
        xi.append(float(1)) # for theta0
        for f_word in feature_wordlist: # f_word は言葉と出現件数のタプル
            if f_word[0] in words: 
                xi.append(float(1))
            else:
                xi.append(float(0))
        ret.append(xi)
    return np.array(ret, dtype=np.float64)
```
このステップでの自分的イベント
1. numpy（python用行列演算ライブラリ） で行列作成体験

### ロジスティック回帰モデル作成。
ここはAndrew先生に敬意を表し、[Couseraの機械学習コース](https://www.coursera.org/learn/machine-learning?)で勉強した、３週目のプログラミング課題２のステップをまねる。
#### シグモイド関数を作成

```python:sigmoid関数
def sigmoid(z): # zはベクトル
    return 1.0 / (1.0 + np.exp(-1 * z))

```
このステップでの自分的イベント
1. numpy.exp体験

#### 予測関数を作成
```python:sigmoid関数
def hypothesis(theta, X):
    return sigmoid(np.dot(X, theta))
```
このステップでの自分的イベント
1. numpy.dot体験

#### コスト関数を作成
```python:コスト関数
def costFunction(theta, X, y): # theta, yはベクトル, X は行列
    m = len(y)
    hyp = hypothesis(theta, X)

    wkz = np.log(hyp)
    wka = (-np.multiply(y, wkz))
    wkb = (1 - y)
    wkc = np.log(1 - (hyp))
    return sum(wka - np.multiply(wkb, wkc)) / m
```
このステップでの自分的イベント
1. numpy.multiply体験

#### 傾き関数を作成
```python:gradient関数
def gradient(theta, X, y):
    m = len(y)
    hyp = hypothesis(theta, X)
    wkdiff = (hyp - y)    # m * 1    matrix
    gradwk = np.dot(X.T, wkdiff) / m
    return gradwk.T
```
このステップでの自分的イベント
1. 転置行列を体験

※Coursera機械学習ではコスト関数の戻り値がコスト値と傾き値。

#### fminuncでモデル算出
・・・ってあれ？pythonではこの関数無い？ ネットで「fminunc python」と調べると、scipy.optimize というのがそれに該当するらしい。が、使用可能な関数に、minimize、fmin_cg、fmin_bfgsとかロジックなのか使用目的かが違うものがあり、さらにその引数が多いらしく、どれを使ったらよいのかこの関数を初めて使う（機械学習を触り始めた）人間にとっては迷宮以外の何物でもない。
[SciPyのminimize公式リファレンス](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html)
[SciPyのfmin_cg公式リファレンス](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.fmin_cg.html)
とりあえず見よう見まねでやってみた。

が、関数の実行結果を見るもθの重みづけ係数が全て０になってしまう。
考えられる原因は以下の通りだが、切り分け判断が出来ない。

  - 関数の使い方が悪い
  - 関数の戻り値の使い方が悪い
  - 引数として渡している関数に問題がある
  - 引数として渡しているデータが悪い

思い返すと、Courseraの学習では一つ一つのステップで提出し、都度それが正解かどうか分かったので、安心して先に進めた。しかし今回は正解が用意されていない。
そこで、[素人の言語処理100本ノック:73](https://qiita.com/segavvy/items/5ad0d5742a674bdf56cc) を参考にさせて頂く事に。まずは、引数部分を少し変えたりして、正解の処理を完コピさせて頂く。その後、関数を自分のと入れ替えて投入データが間違ってないか、からの確認。そして順次チェックしていく。結果、以下の問題点が発見できた。

  - 整数値での配列を作っていた
  - Octaveのソースからの変更にミスがあった

それを修正した上で、関数のパラメーターを色々変えてみて、結果として以下の関数にまとまった。

```python:学習
import scipy.optimize as op
def learnData(X, y): # theta, yはベクトル, X は行列
    m,n= X.shape
    initial_theta = np.zeros(n, dtype=np.float64)
    return op.minimize(fun=costFunction, x0=initial_theta, args=(X, y), method='TNC', jac=gradient, options={'maxiter':1000})
```
このステップでの自分的イベント
1. np.zerosを体験
2. scipy.optimize.minimizeを体験

```console:前述learnData関数の戻り値をprintした結果。θはresult['x']で取得。
     fun: 0.2186551631728408
     jac: array([-1.24229859e-03, -4.02902790e-04, -3.13793105e-04, ...,
        4.57530540e-06, -1.85517634e-06, -1.74574424e-06])
 message: 'Converged (|f_n-f_(n-1)| ~= 0)'
    nfev: 188
     nit: 13
  status: 1
 success: True
       x: array([-0.6217017 ,  0.67637518, -0.43255428, ..., -1.12773972,
        3.85226178,  7.33103357])
```


### 検証
データとしては、以下の処理を施したもの。

  - ストップワードを除去
  - 各単語をステミング処理
  - 出現頻度が５回以下の単語は除去

##### 課題77で使用する関数で正解率チェック
```python:コスト関数
def calcScore(theta, X, y):
    hyp = hypothesis(theta, X)
    ttlcnt = len(y)
    abs = np.abs(hyp - y)
    ok = np.count_nonzero(abs < 0.5)
    print('ttl={0}, ok={1}, rate={2}'.format(ttlcnt, ok, ok * 100 / ttlcnt))
```
にて、以下の結果を得る。

```console:結果
[0.99999379 0.34166364 0.00430491 ... 0.99475144 0.05514151 0.99999954]
ttl=10662, ok=9714, rate=91.10861001688238
```
ちゃんと学習自体は出来ている様子。

##### 課題75で重要な単語チェック
```python:課題75

def topAndWorst10(theta, feature_wordlist):
    sortwklist = []
    for idx, coefficient in enumerate(theta[1:]):
        sortwk = {}
        sortwk[idx] = coefficient
        sortwklist.append(sortwk)

    sortedlist = sorted(sortwklist, key=lambda elem: list(elem.values())[0], reverse=True)

    for idx, item in enumerate(sortedlist[0:9]):
        key = list(item.keys())[0]
        val = list(item.values())[0]
        print('rank {0} = {1}, value = {2}'.format(idx, feature_wordlist[key], val))

    for idx, item in enumerate(sortedlist[-9:]):
        key = list(item.keys())[0]
        val = list(item.values())[0]
        print('rank {0} = {1}, value = {2}'.format(len(sortedlist) - 10 + idx, feature_wordlist[key], val))
```

その結果。

```console:重み出力
rank 0 = ('rival', 7), value = 15.487425109257911
rank 1 = ('dogtown', 7), value = 15.116509791167863
rank 2 = ('engross', 34), value = 14.714563379452631
rank 3 = ('creatur', 10), value = 14.258338401361565
rank 4 = ('grandeur', 10), value = 14.131937516077285
rank 5 = ('notch', 6), value = 13.126639961427905
rank 6 = ('leigh', 7), value = 13.013409404232442
rank 7 = ('gentli', 8), value = 12.77251552801514
rank 8 = ('uma', 6), value = 12.440912803170404
rank 3149 = ('uninspir', 13), value = -12.190452724510878
rank 3150 = ('51', 10), value = -12.530073693634368
rank 3151 = ('wilder', 8), value = -12.739129470469681
rank 3152 = ('ballist', 10), value = -12.952886580008153
rank 3153 = ('q', 7), value = -13.107108773450731
rank 3154 = ('drain', 7), value = -13.713487382294955
rank 3155 = ('dud', 9), value = -14.343427923426695
rank 3156 = ('badli', 23), value = -14.96784549758193
rank 3157 = ('mckay', 6), value = -17.096731503417494
```
学習データに対して、上記単語を検索してみると、確かに上位ランクの単語はポジティブ感想に多く、下位ランクはネガティブ感想に多い様子。
ただ、[素人の言語処理100本ノック:75](https://qiita.com/segavvy/items/5ded025346c9621aedb2) の結果とは結構違ってる・・・こちらの方は１文字の単語や記号抜いたりなどのデータクリーニングをより適切にやってるみたいですね。
おそらく自分の今の状態は素性抽出不足か過学習かという所でしょう。まぁこの状態の方が後続の「課題78：5分割交差検定」の有効性を確かめるには良さそうなのでこのまま進める事にします。

# 教訓

１ステップ毎の検証は、unittest作って、ちゃんとやりましょう。

# 参考にさせてもらったページ
[素人の言語処理100本ノック:73](https://qiita.com/segavvy/items/5ad0d5742a674bdf56cc)

[素人の言語処理100本ノック:75](https://qiita.com/segavvy/items/5ded025346c9621aedb2)

[Python fminunc 的替代方法](https://blog.csdn.net/csdn_inside/article/details/81558079)

[Coursera / Machine Learningの教材を２度楽しむ](https://qiita.com/TomokIshii/items/b22a3681cb17836c8f6e)

[共役勾配法によるコスト関数最適化](http://aidiary.hatenablog.com/entry/20140414/1397481258)