2018年12月03日

言語処理100本ノックでPython入門 #79 - 機械学習、scikit-learnで適合率-再現率&グラフの描画


いよいよ言語処理100本ノック 2015の第8章・機械学習の最後の問題です。

■ 問題

79. 適合率-再現率グラフの描画
ロジスティック回帰モデルの分類の閾値を変化させることで,適合率-再現率グラフを描画せよ.

■ どう解いたか

久しぶりにmatplotlibを利用します。

標準では、閾値が0.5より大きいか、小さいかで+1にする-1にするのかを判断しているので、この値を変化させることで、適合率-再現率がどう変化するのかを調べる問題です。

scikit-learnには、sklearn.metrics.precision_recall_curveという関数が用意されていて、これを使うと、閾値を少しずつ変化させ、その閾値毎の適合率(precision)と再現率(recall)を計算することができます。

precision, recall, threshold = precision_recall_curve(y_test, pred_positive)
thresholdはその閾値の値のリストになっています。

この値を使って、matplotlibでグラフを描画します。

これについては、

scikit-learnのロジスティック回帰で特定のクラスに分類されやすくする

を参考にさせていただきました。 ただ、僕はnumpyが全く分かってません。というかPythonがまだよくわかっていないことがわかりました。
np.argmin(np.abs(thresholds - (i * 0.05)))

この thresholds - (i * 0.05) って文法的に何を意味しているのだろう。 thresholdsというのがarrayだったら、型が違うからエラーになると思ったんだけど、エラーになりません。arrayに対して マイナス記号で引き算すると、各要素の値に対して減算されるって、規定されてるのかな?

それともscikit-learnのどこかで、- 記号が overloadされてるんだろうか?  あるいは、numpyのクラスで、 - 記号が overloadされてるんだろうか?  

まだまだ知らないことがたくさんありそうです。 numpyのことをもう少し勉強する必要がありそうです。

話を戻すと、ある値の絶対値をとって、その最小値のインデックスを求めているんだから、 たぶん、各リスト内容の各要素に対して、N - (i*0.05) を計算してるんだろうと思います。 そうすれば、閾値にもっとも近いものを取り出せるってことなんだと理解。

実行した結果のグラフ(最後に示します)を見てみると、再現率を上げれば、適合率が下がり、適合率を上げれば、再現率は下がるという関係になっています。 閾値をどこにするかは、とても難しい問題です。

■ Pythonのコード
import re
from nltk import stem
import numpy as np
from sklearn.metrics import precision_recall_curve
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.externals import joblib
from sklearn.model_selection import train_test_split
import matplotlib.pyplot as plt

class Stopwords:
    words = [ \
        'a', 'about', 'all', 'an', 'and', 'any', 'are', 'as', \
        'at', 'be', 'been', 'but', 'by', 'can', 'could', 'do', \
        'does', 'for', 'from', 'has', 'have', 'he', 'her', 'his', \
        'how', 'i', 'if', 'in', 'into', 'is', 'it', 'its', 'made', \
        'make', 'may', 'me', 'my', 'no', 'not', 'of', 'on', 'one', \
        'or', 'out', 'she', 'should', 'so', 'some', 'than', 'that', \
        'the', 'their', 'them', 'there', 'then', 'they', 'this', \
        'those', 'to', 'too', 'us', 'was', 'we', 'what', 'when',\
        'which', 'who', 'with', 'would', 'you', 'your', ''
        ]

    @staticmethod
    def exists(word):
        return word in  Stopwords.words

class SentimentFeatures:
    def __init__(self):
        self.stemmer = stem.PorterStemmer()
        self.validreg = re.compile(r'^[-=!@#$%^&*()_+|;";,.<>/?]+$')
        self.splitreg = re.compile(r'\s|,|\.|\(|\)|\'|/|\'|\[|\]|-')

    def isValid(self, word):
        if word == '' or len(word) <= 2:
            return False
        if self.validreg.match(word):
            return False
        return not Stopwords.exists(word)

    def getFromLine(self, line):
        array = self.splitreg.split(line)
        # こういう時はlambda キーワードいらないんですね。
        words = filter(self.isValid, array)
        xs = map(self.stemmer.stem, words)
        return xs

    def enumerate(self, filename, encoding):
        with open(filename, 'r', encoding=encoding) as fin:
            for line in fin:
                sentiment = line[:3]
                yield sentiment, self.getFromLine(line[3:])

class SentimentAnalyser:
    def __init__(self):
        self.cv = CountVectorizer(encoding='utf-8')
        self.lr = LogisticRegression(solver='sag', max_iter=10000)

    # LogisticRegression を使い学習する
    def fit(self, X_train, y_train):
        X_train_cv = self.cv.fit_transform(X_train)
        self.lr.fit(X_train_cv, y_train)

    # LogisticRegression を使い予測する
    def predict(self, X_test):
        x = self.cv.transform(X_test)
        return self.lr.predict(x)

    # 予測し、分類毎に確率を得る
    def predict_proba(self, X_test):
        x = self.cv.transform(X_test)
        return self.lr.predict_proba(x)

    # 学習済みデータをロードする
    def load(self):
        self.cv = joblib.load('chapter08/cv73.learn')
        self.lr = joblib.load('chapter08/lr73.learn')

    # 学習済みデータを保存する
    def save(self):
        # 学習したデータを保存する
        joblib.dump(self.cv, 'chapter08/cv73.learn')
        joblib.dump(self.lr, 'chapter08/lr73.learn')

    # 学習に利用するデータを取り出す
    # y[] は、センチメント
    # X[] は、素性データ
    @staticmethod
    def getFeatureData(filename):
        X = []
        y = []
        sf = SentimentFeatures()
        for sentiment, features in sf.enumerate(filename, 'utf-8'):
            y.append(1.0 if sentiment[0] == '+' else 0.0)
            X.append(' '.join(features))
        return X, y

# 参考URL http://ohke.hateblo.jp/entry/2017/08/25/230000

def main():
    sa = SentimentAnalyser()
    X, y = sa.getFeatureData('chapter08/sentiment.txt')
    # 5分割交差検定を行うためにデータを分割する
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)

    # ベクターに変換
    #X_train_cv = sa.trainToVector(X_train)
    #X_test_cv = sa.testToVector(X_test)

    # 学習
    sa.fit(X_train, y_train)

    # 予測
    pp = sa.predict_proba(X_test)
    print(pp[:, 1])
    print()
    print(pp[:5, 1])

    # +1の予測確率を取り出す  [:, 1] は、1列目のすべてのデータ
    pred_positive = sa.predict_proba(X_test)[:, 1]

    # ある閾値の時の適合率、再現率, 閾値の値を取得
    precisions, recalls, thresholds = precision_recall_curve(y_test, pred_positive)

    # 0から1まで0.05刻みで○をプロット
    for i in range(21):
        close_point = np.argmin(np.abs(thresholds - (i * 0.05)))
        plt.plot(precisions[close_point], recalls[close_point], 'o')

    # 適合率-再現率曲線
    plt.plot(precisions, recalls)
    plt.xlabel('Precision')
    plt.ylabel('Recall')

    plt.show()
    input()

if __name__ == '__main__':
    main()


■ 実行結果

スクリーンショット 2018-11-23 16.50.03



■ 最後に

言語処理100本ノック 2015
は、第9章、第10章が残っているんですが、すこし興味を持続するのが難しくなってきました。 ということで、この「言語処理100本ノック 2015」のシリーズはいったん、終了したいと思います。

まあ、最初はPython全くわからなかったけど、なんとかここまで来て、Pythonの知識も随分と増えたかなと思います。でも、使わないとすぐ忘れてしまうんだろうなとは思います。

以前やった Haskell とかは完全に忘却のかなたです(笑)

...

ところで、livedoorブログっていつになったら、https化するんでしょうね。ちょっと待ちくたびれた感があります。

markdown使えないのも不便だし...

14年間続けてきたけど、他に移ろうかな〜。Qiita一本でもいいかなとも感じてます。





  

Posted by gushwell at 22:05Comments(0)

2018年11月27日

言語処理100本ノックでPython入門 #78 - 機械学習、scikit-learnの5分割交差検定


今日は言語処理100本ノック 2015の第8章・機械学習の問題78に挑戦です。

■ 問題

78. 5分割交差検定
76-77の実験では,学習に用いた事例を評価にも用いたため,正当な評価とは言えない.すなわち,分類器が訓練事例を丸暗記する際の性能を評価しており,モデルの汎化性能を測定していない.そこで,5分割交差検定により,極性分類の正解率,適合率,再現率,F1スコアを求めよ.

■ Pythonのコード

import re
from nltk import stem
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.externals import joblib
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, f1_score, precision_score, recall_score

class Stopwords:
    words = [ \
        'a', 'about', 'all', 'an', 'and', 'any', 'are', 'as', \
        'at', 'be', 'been', 'but', 'by', 'can', 'could', 'do', \
        'does', 'for', 'from', 'has', 'have', 'he', 'her', 'his', \
        'how', 'i', 'if', 'in', 'into', 'is', 'it', 'its', 'made', \
        'make', 'may', 'me', 'my', 'no', 'not', 'of', 'on', 'one', \
        'or', 'out', 'she', 'should', 'so', 'some', 'than', 'that', \
        'the', 'their', 'them', 'there', 'then', 'they', 'this', \
        'those', 'to', 'too', 'us', 'was', 'we', 'what', 'when',\
        'which', 'who', 'with', 'would', 'you', 'your', ''
        ]

    @staticmethod
    def exists(word):
        return word in  Stopwords.words

class SentimentFeatures:
    def __init__(self):
        self.stemmer = stem.PorterStemmer()
        self.validreg = re.compile(r'^[-=!@#$%^&*()_+|;";,.<>/?]+$')
        self.splitreg = re.compile(r'\s|,|\.|\(|\)|\'|/|\'|\[|\]|-')

    def isValid(self, word):
        if word == '' or len(word) <= 2:
            return False
        if self.validreg.match(word):
            return False
        return not Stopwords.exists(word)

    def getFromLine(self, line):
        array = self.splitreg.split(line)
        # こういう時はlambda キーワードいらないんですね。
        words = filter(self.isValid, array)
        xs = map(self.stemmer.stem, words)
        return xs

    def enumerate(self, filename, encoding):
        with open(filename, 'r', encoding=encoding) as fin:
            for line in fin:
                sentiment = line[:3]
                yield sentiment, self.getFromLine(line[3:])

class SentimentAnalyser:
    def __init__(self):
        self.cv = CountVectorizer(encoding='utf-8')
        self.lr = LogisticRegression(solver='sag', max_iter=10000)

    # LogisticRegression を使い学習する
    def fit(self, X_train, y_train):
        X_train_cv = self.cv.fit_transform(X_train)
        self.lr.fit(X_train_cv, y_train)

    # LogisticRegression を使い予測する
    def predict(self, X_test):
        x = self.cv.transform(X_test)
        return self.lr.predict(x)

    # 予測し、分類毎に確率を得る
    def predict_proba(self, X_test):
        x = self.cv.transform(X_test)
        return self.lr.predict_proba(x)

    # 学習済みデータをロードする
    def load(self):
        self.cv = joblib.load('chapter08/cv73.learn')
        self.lr = joblib.load('chapter08/lr73.learn')

    # 学習済みデータを保存する
    def save(self):
        # 学習したデータを保存する
        joblib.dump(self.cv, 'chapter08/cv73.learn')
        joblib.dump(self.lr, 'chapter08/lr73.learn')

    # 学習に利用するデータを取り出す
    # y[] は、センチメント
    # X[] は、素性データ
    @staticmethod
    def getFeatureData(filename):
        X = []
        y = []
        sf = SentimentFeatures()
        for sentiment, features in sf.enumerate(filename, 'utf-8'):
            y.append(1.0 if sentiment[0] == '+' else 0.0)
            X.append(' '.join(features))
        return X, y

def main():
    sa = SentimentAnalyser()
    X, y = sa.getFeatureData('chapter08/sentiment.txt')
    # 5分割交差検定を行うためにデータを分割する
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)

    # 4/5のデータで学習する
    # X_train_cv = sa.trainToVector(X_train)
    sa.fit(X_train, y_train)

    # 1/5のデータで予測する
    y_test_pred = sa.predict(X_test)

    print('正解率 accuracy:', accuracy_score(y_test, y_test_pred))
    print('適合率 precision:', precision_score(y_test, y_test_pred))
    print('再現率 recall:', recall_score(y_test, y_test_pred))
    print('F1スコア f1_score:', f1_score(y_test, y_test_pred))

if __name__ == '__main__':
    main()


scikit-Learnには、n分割交差検定を行うためのテストデータの分割機能が用意されています。 以下のコードで、データの4/5を学習用データに、1/5をテストデータに分割することができます。

# 5分割交差検定を行うためにデータを分割する
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)

これができれば、あとは、fitとpredictを呼び出すだけです。

最後に、accuracy_score、precision_scoreなどのメソッドを使い、適合率などを求めています。

■ 結果

正解率 accuracy: 0.768870135959
適合率 precision: 0.770398481973
再現率 recall: 0.763875823142
F1スコア f1_score: 0.767123287671


学習データとテストデータが別れたので、当然ながらそれぞれに値が下がっていることを確認できます。  
Posted by gushwell at 21:50Comments(0)

2018年11月18日

言語処理100本ノックでPython入門 #77 - 機械学習、scikit-learnでの正解率の計測


今日は言語処理100本ノック 2015の第8章・機械学習の問題77に挑戦です。

■ 問題

77. 正解率の計測
76の出力を受け取り,予測の正解率,正例に関する適合率,再現率,F1スコアを求めるプログラムを作成せよ.

■ Pythonのコード

import re
from nltk import stem
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report, accuracy_score, \
     f1_score, precision_score, recall_score
from sklearn.externals import joblib

class Stopwords:
    words = [ \
        'a', 'about', 'all', 'an', 'and', 'any', 'are', 'as', \
        'at', 'be', 'been', 'but', 'by', 'can', 'could', 'do', \
        'does', 'for', 'from', 'has', 'have', 'he', 'her', 'his', \
        'how', 'i', 'if', 'in', 'into', 'is', 'it', 'its', 'made', \
        'make', 'may', 'me', 'my', 'no', 'not', 'of', 'on', 'one', \
        'or', 'out', 'she', 'should', 'so', 'some', 'than', 'that', \
        'the', 'their', 'them', 'there', 'then', 'they', 'this', \
        'those', 'to', 'too', 'us', 'was', 'we', 'what', 'when',\
        'which', 'who', 'with', 'would', 'you', 'your', ''
        ]

    @staticmethod
    def exists(word):
        return word in  Stopwords.words

class SentimentFeatures:
    def __init__(self):
        self.stemmer = stem.PorterStemmer()
        self.validreg = re.compile(r'^[-=!@#$%^&*()_+|;";,.<>/?]+$')
        self.splitreg = re.compile(r'\s|,|\.|\(|\)|\'|/|\'|\[|\]|-')

    def isValid(self, word):
        if word == '' or len(word) <= 2:
            return False
        if self.validreg.match(word):
            return False
        return not Stopwords.exists(word)

    def getFromLine(self, line):
        array = self.splitreg.split(line)
        # こういう時はlambda キーワードいらないんですね。
        words = filter(self.isValid, array)
        xs = map(self.stemmer.stem, words)
        return xs

    def enumerate(self, filename, encoding):
        with open(filename, 'r', encoding=encoding) as fin:
            for line in fin:
                sentiment = line[:3]
                yield sentiment, self.getFromLine(line[3:])

class SentimentAnalyser:
    def __init__(self):
        self.cv = CountVectorizer(encoding='utf-8')
        self.lr = LogisticRegression(solver='sag', max_iter=10000)

    # LogisticRegression を使い学習する
    def fit(self, X_train, y_train):
        X_train_cv = self.cv.fit_transform(X_train)
        self.lr.fit(X_train_cv, y_train)

    # LogisticRegression を使い予測する
    def predict(self, X_test):
        x = self.cv.transform(X_test)
        return self.lr.predict(x)

    # 予測し、分類毎に確率を得る
    def predict_proba(self, X_test):
        x = self.cv.transform(X_test)
        return self.lr.predict_proba(x)

    # 学習済みデータをロードする
    def load(self):
        self.cv = joblib.load('chapter08/cv73.learn')
        self.lr = joblib.load('chapter08/lr73.learn')

    # 学習済みデータを保存する
    def save(self):
        # 学習したデータを保存する
        joblib.dump(self.cv, 'chapter08/cv73.learn')
        joblib.dump(self.lr, 'chapter08/lr73.learn')

    # 学習に利用するデータを取り出す
    # y[] は、センチメント
    # X[] は、素性データ
    @staticmethod
    def getFeatureData(filename):
        X = []
        y = []
        sf = SentimentFeatures()
        for sentiment, features in sf.enumerate(filename,'utf-8'):
            y.append(1.0 if sentiment[0] == '+' else 0.0)
            X.append(' '.join(features))
        return X, y

def main():
    sa = SentimentAnalyser()
    sa.load()
    X_test, y_test = sa.getFeatureData('chapter08/sentiment.txt')
    y_test_pred = sa.predict(X_test)
    print('正解率 accuracy:', accuracy_score(y_test, y_test_pred))
    print('適合率 precision:', precision_score(y_test, y_test_pred))
    print('再現率 recall:', recall_score(y_test, y_test_pred))
    print('F1スコア f1_score:', f1_score(y_test, y_test_pred))

if __name__ == '__main__':
    main()



■ どう解いたか

今回も、3つのクラス(Stopwords,SentimentFeatures,SentimentAnalyser)は前回と同じです、mainメソッドだけを変更しています。

問題文は「76の出力を受け取り」とありますが、これをどう解釈したら良いか出題意図がわからなかったので、この部分は無視して、「予測の正解率,正例に関する適合率,再現率,F1スコアを求めるプログラムを作成」しています。
predictメソッドで予測した後に、accuracy_score、precision_scoreなどのメソッドを使い、求める値を得ています。

■ 結果

正解率 accuracy: 0.94363158882
適合率 precision: 0.946563444109
再現率 recall: 0.940348902645
F1スコア f1_score: 0.943445939588

ちなみに

    print(classification_report(y_test, y_test_pred))
とすると、以下の結果が得られます。

             precision    recall  f1-score   support

        0.0       0.94      0.95      0.94      5331
        1.0       0.95      0.94      0.94      5331

avg / total       0.94      0.94      0.94     10662
  
Posted by gushwell at 21:30Comments(0)

2018年11月11日

言語処理100本ノックでPython入門 #76 - 機械学習、scikit-learnでの予測確率


今日は言語処理100本ノック 2015の第8章・機械学習の問題76に挑戦です。

■ 問題

76. ラベル付け
学習データに対してロジスティック回帰モデルを適用し,正解のラベル,予測されたラベル,予測確率をタブ区切り形式で出力せよ.

■ どう解いたか 

今回も、3つのクラス(Stopwords,SentimentFeatures,SentimentAnalyser)は前回と同じです。

学習に利用したデータをすべて予測するために、getFeatureDataでデータを取得し、それを predictメソッドとLogisticRegressionオブジェクトのpredict_probaメソッドを使って予測と予測確率を得ています。 

このロジックは、すでに問題74で、予測確率を求めるメソッドとして定義していたので、今回は、mainメソッドだけの変更ですみました。



■ Pythonのコード
import re
from nltk import stem
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.externals import joblib

class Stopwords:
    words = [ \
        'a', 'about', 'all', 'an', 'and', 'any', 'are', 'as', \
        'at', 'be', 'been', 'but', 'by', 'can', 'could', 'do', \
        'does', 'for', 'from', 'has', 'have', 'he', 'her', 'his', \
        'how', 'i', 'if', 'in', 'into', 'is', 'it', 'its', 'made', \
        'make', 'may', 'me', 'my', 'no', 'not', 'of', 'on', 'one', \
        'or', 'out', 'she', 'should', 'so', 'some', 'than', 'that', \
        'the', 'their', 'them', 'there', 'then', 'they', 'this', \
        'those', 'to', 'too', 'us', 'was', 'we', 'what', 'when',\
        'which', 'who', 'with', 'would', 'you', 'your', ''
        ]

    @staticmethod
    def exists(word):
        return word in  Stopwords.words

class SentimentFeatures:
    def __init__(self):
        self.stemmer = stem.PorterStemmer()
        self.validreg = re.compile(r'^[-=!@#$%^&*()_+|;";,.<>/?]+$')
        self.splitreg = re.compile(r'\s|,|\.|\(|\)|\'|/|\'|\[|\]|-')

    def isValid(self, word):
        if word == '' or len(word) <= 2:
            return False
        if self.validreg.match(word):
            return False
        return not Stopwords.exists(word)

    def getFromLine(self, line):
        array = self.splitreg.split(line)
        # こういう時はlambda キーワードいらないんですね。
        words = filter(self.isValid, array)
        xs = map(self.stemmer.stem, words)
        return xs

    def enumerate(self, filename, encoding):
        with open(filename, 'r', encoding=encoding) as fin:
            for line in fin:
                sentiment = line[:3]
                yield sentiment, self.getFromLine(line[3:])

class SentimentAnalyser:
    def __init__(self):
        self.cv = CountVectorizer(encoding='utf-8')
        self.lr = LogisticRegression(solver='sag', max_iter=10000)

    # LogisticRegression を使い学習する
    def fit(self, X_train, y_train):
        X_train_cv = self.cv.fit_transform(X_train)
        self.lr.fit(X_train_cv, y_train)

    # LogisticRegression を使い予測する
    def predict(self, X_test):
        x = self.cv.transform(X_test)
        return self.lr.predict(x)

    # 予測し、分類毎に確率を得る
    def predict_proba(self, X_test):
        x = self.cv.transform(X_test)
        return self.lr.predict_proba(x)

    # 学習済みデータをロードする
    def load(self):
        self.cv = joblib.load('chapter08/cv73.learn')
        self.lr = joblib.load('chapter08/lr73.learn')

    # 学習済みデータを保存する
    def save(self):
        # 学習したデータを保存する
        joblib.dump(self.cv, 'chapter08/cv73.learn')
        joblib.dump(self.lr, 'chapter08/lr73.learn')

    # 学習に利用するデータを取り出す
    # y[] は、センチメント
    # X[] は、素性データ
    @staticmethod
    def getFeatureData(filename):
        X = []
        y = []
        sf = SentimentFeatures()
        for sentiment, features in sf.enumerate(filename, 'cp1252'):
            y.append(1.0 if sentiment[0] == '+' else 0.0)
            X.append(' '.join(features))
        return X, y

def getLabel(val):
    return '+1' if val > 0.5 else '-1'


def main():
    sa = SentimentAnalyser()
    sa.load()

    X_test, y_test = sa.getFeatureData('chapter08/sentiment.txt')
    y_test_pred = sa.predict(X_test)
    pr = sa.predict_proba(X_test)
    for right, pred, proba in zip(y_test, y_test_pred, pr):
        print('{}\t{}\t{}'.format(getLabel(right), getLabel(pred), \
            proba[0] if pred == 0 else proba[1]))

if __name__ == '__main__':
    main()


■ 結果

先頭の20個だけを掲載します。

-1	-1	0.6097303429740366
-1	-1	0.7237875917615046
+1	+1	0.8452377813768515
+1	+1	0.6449560336941579
+1	+1	0.543156360826781
+1	+1	0.9113562170418062
+1	+1	0.6398913869466663
-1	-1	0.6568220959620661
+1	+1	0.7594696176352425
-1	-1	0.7002498960488786
+1	+1	0.927910364837036
+1	+1	0.9032093714572444
+1	+1	0.714278110906943
+1	+1	0.8908909959384128
+1	+1	0.588722939785566
+1	+1	0.7755451604607824
+1	+1	0.9985328530583656
+1	+1	0.747913188658218
-1	-1	0.6764951558532446
+1	+1	0.9859825558428327
  
Posted by gushwell at 21:00Comments(0)

2018年10月28日

言語処理100本ノックでPython入門 #75 - 機械学習、scikit-learnのcoef_ プロパティ


今日は言語処理100本ノック 2015の第8章・機械学習の問題75に挑戦です。

■ 問題

75. 素性の重み
73で学習したロジスティック回帰モデルの中で,重みの高い素性トップ10と,重みの低い素性トップ10を確認せよ.


■ どんなふうに解くか

今回は、3つのクラス(Stopwords,SentimentFeatures,SentimentAnalyser)は前回と同じです、mainメソッドだけを変更しています。

やっていることは、まず学習済みデータをロードして、CountVectorizerのget_feature_names メソッドで重複が除かれた素性データ一覧(単語一覧)を得ます。

これだけだと、重み順になっていないので、LogisticRegressionオブジェクトの coef_ を参照します。 ここには get_feature_namesで得た素性リストの順に重みデータがリストで入っています。

numpyのargsortを使って、素性リストをソート使しています。 このargsortはちょっと変わっていて、リストの中身で(重み順で)ソートしてくれるのですが、 その結果は、ソート順に並び替えられた要素のインデックスが得られます。 まあ、処理速度を考慮するとそれが妥当なのかも。これを使ってTop10, Bottom 10を取り出しています。

■ Pythonのコード

import re
from nltk import stem
import numpy as np
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.externals import joblib

class Stopwords:
    words = ['a', 'about', 'all', 'an', 'and', 'any', 'are', 'as', \
            'at', 'be', 'been', 'but', 'by', 'can', 'could', 'do', \
            'does', 'for', 'from', 'has', 'have', 'he', 'her', 'his', \
            'how', 'i', 'if', 'in', 'into', 'is', 'it', 'its', 'made', \
            'make', 'may', 'me', 'my', 'no', 'not', 'of', 'on', 'one', \
            'or', 'out', 'she', 'should', 'so', 'some', 'than', 'that', \
            'the', 'their', 'them', 'there', 'then', 'they', 'this', \
            'those', 'to', 'too', 'us', 'was', 'we', 'what', 'when',\
            'which', 'who', 'with', 'would', 'you', 'your', ''
        ]

    @staticmethod
    def exists(word):
        return word in  Stopwords.words

class SentimentFeatures:
    def __init__(self):
        self.stemmer = stem.PorterStemmer()
        self.validreg = re.compile(r'^[-=!@#$%^&*()_+|;";,.<>/?]+$')
        self.splitreg = re.compile(r'\s|,|\.|\(|\)|\'|/|\'|\[|\]|-')

    def isValid(self, word):
        if word == '' or len(word) <= 2:
            return False
        if self.validreg.match(word):
            return False
        return not Stopwords.exists(word)

    def getFromLine(self, line):
        array = self.splitreg.split(line)
        # こういう時はlambda キーワードいらないんですね。
        words = filter(self.isValid, array)
        xs = map(self.stemmer.stem, words)
        return xs

    def enumerate(self, filename, encoding):
        with open(filename, 'r', encoding=encoding) as fin:
            for line in fin:
                sentiment = line[:3]
                yield sentiment, self.getFromLine(line[3:])

class SentimentAnalyser:
    def __init__(self):
        self.cv = CountVectorizer(encoding='utf-8')
        self.lr = LogisticRegression(solver='sag', max_iter=10000)

    # LogisticRegression を使い学習する
    def fit(self, X_train, y_train):
        X_train_cv = self.cv.fit_transform(X_train)
        self.lr.fit(X_train_cv, y_train)

    # LogisticRegression を使い予測する
    def predict(self, X_test):
        x = self.cv.transform(X_test)
        return self.lr.predict(x)

    # 予測し、分類毎に確率を得る
    def predict_proba(self, X_test):
        x = self.cv.transform(X_test)
        return self.lr.predict_proba(x)

    # 学習済みデータをロードする
    def load(self):
        self.cv = joblib.load('chapter08/cv73.learn')
        self.lr = joblib.load('chapter08/lr73.learn')

    # 学習済みデータを保存する
    def save(self):
        # 学習したデータを保存する
        joblib.dump(self.cv, 'chapter08/cv73.learn')
        joblib.dump(self.lr, 'chapter08/lr73.learn')

    # 学習に利用するデータを取り出す
    # y[] は、センチメント
    # X[] は、素性データ
    @staticmethod
    def getFeatureData(filename):
        X = []
        y = []
        sf = SentimentFeatures()
        for sentiment, features in sf.enumerate(filename, 'cp1252'):
            y.append(1.0 if sentiment[0] == '+' else 0.0)
            X.append(' '.join(features))
        return X, y

def main():
    sa = SentimentAnalyser()
    sa.load()

    # fit_transform/transformに渡した単語一覧(学習データ一覧)を得る (重複はなし)
    features = sa.cv.get_feature_names()
    # coef_ には学習した結果の重みが入る。
    # これをソートして、そのインデックスを得る argsortはソートした結果のインデックスが返る
    sorted_idx = np.argsort(sa.lr.coef_)[0]
    print('重みの高い素性トップ10')
    for i in sorted_idx[-1:-11:-1]:
        print(features[i])
    print()
    print('重みの低い素性トップ10')
    for i in sorted_idx[:10]:
        print(features[i])

if __name__ == '__main__':
    main()


■ 結果

重みの高い素性トップ10
engross
refresh
smarter
unexpect
remark
resist
grown
examin
refreshingli
confid

重みの低い素性トップ10
bore
dull
fail
mediocr
plod
routin
wasn
neither
badli
wast
  
Posted by gushwell at 22:00Comments(0)

2018年10月08日

言語処理100本ノックでPython入門 #74 - 機械学習、scikit-learnでロジスティック回帰の予測


今日は言語処理100本ノック 2015の第8章・機械学習の問題74を解きました。

■ 問題

74. 予測
73で学習したロジスティック回帰モデルを用い,与えられた文の極性ラベル(正例なら"+1",負例なら"-1")と,その予測確率を計算するプログラムを実装せよ.


■ Pythonのコード

import re
from nltk import stem
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.linear_model import LogisticRegression
#from sklearn.metrics import accuracy_score, classification_report
from sklearn.externals import joblib

class Stopwords:
    words = ['a', 'about', 'all', 'an', 'and', 'any', 'are', 'as', \
            'at', 'be', 'been', 'but', 'by', 'can', 'could', 'do', \
            'does', 'for', 'from', 'has', 'have', 'he', 'her', 'his', \
            'how', 'i', 'if', 'in', 'into', 'is', 'it', 'its', 'made', \
            'make', 'may', 'me', 'my', 'no', 'not', 'of', 'on', 'one', \
            'or', 'out', 'she', 'should', 'so', 'some', 'than', 'that', \
            'the', 'their', 'them', 'there', 'then', 'they', 'this', \
            'those', 'to', 'too', 'us', 'was', 'we', 'what', 'when',\
            'which', 'who', 'with', 'would', 'you', 'your', ''
        ]

    @staticmethod
    def exists(word):
        return word in  Stopwords.words

class SentimentFeatures:
    def __init__(self):
        self.stemmer = stem.PorterStemmer()
        self.validreg = re.compile(r'^[-=!@#$%^&*()_+|;";,.<>/?]+$')
        self.splitreg = re.compile(r'\s|,|\.|\(|\)|\'|/|\'|\[|\]|-')

    def isValid(self, word):
        if word == '' or len(word) <= 2:
            return False
        if self.validreg.match(word):
            return False
        return not Stopwords.exists(word)

    def getFromLine(self, line):
        array = self.splitreg.split(line)
        # こういう時はlambda キーワードいらないんですね。
        words = filter(self.isValid, array)
        xs = map(self.stemmer.stem, words)
        return xs

    def enumerate(self, filename, encoding):
        with open(filename, 'r', encoding=encoding) as fin:
            for line in fin:
                sentiment = line[:3]
                yield sentiment, self.getFromLine(line[3:])

class SentimentAnalyser:
    def __init__(self):
        self.cv = CountVectorizer(encoding='utf-8')
        self.lr = LogisticRegression(solver='sag', max_iter=10000)

    # LogisticRegression を使い学習する
    def fit(self, X_train, y_train):
        X_train_cv = self.cv.fit_transform(X_train)
        self.lr.fit(X_train_cv, y_train)

    # LogisticRegression を使い予測する
    def predict(self, X_test):
        x = self.cv.transform(X_test)
        return self.lr.predict(x)

    # 予測し、分類毎に確率を得る
    def predict_proba(self, X_test):
        x = self.cv.transform(X_test)
        return self.lr.predict_proba(x)

    # 学習済みデータをロードする。ファイル名が良くなかったなー
    def load(self):
        self.cv = joblib.load('chapter08/cv73.learn')
        self.lr = joblib.load('chapter08/lr73.learn')

    # 学習済みデータを保存する
    def save(self):
        # 学習したデータを保存する
        joblib.dump(self.cv, 'chapter08/cv73.learn')
        joblib.dump(self.lr, 'chapter08/lr73.learn')

    # 学習に利用するデータを取り出す
    # y[] は、センチメント
    # X[] は、素性データ
    @staticmethod
    def getFeatureData(filename):
        X = []
        y = []
        sf = SentimentFeatures()
        for sentiment, features in sf.enumerate(filename, 'cp1252'):
            y.append(1.0 if sentiment[0] == '+' else 0.0)
            X.append(' '.join(features))
        return X, y

# dataのセンチメントを予測する
def predictSentiment(sa, data):
    x = [data]
    y_test_pred = sa.predict(x)
    pr = sa.predict_proba(x)
    print(data.rstrip('\n'))
    print('予測:{} 確率:{}\n'.format('+1' if y_test_pred[0] == 1 else '-1', \
        pr[0][0] if y_test_pred[0] == 0 else pr[0][1]))

def main():
    sa = SentimentAnalyser()
    sa.load()

    # テストの文を考えるのが面倒なので、元のデータから6つほど借用して、テストデータにしている。
    # これだとあまり意味がないけど...
    texts = [\
        'perhaps the best sports movie i''ve ever seen.', \
        'i had more fun watching spy than i had with most of the big summer movies.', \
        'vividly conveys the shadow side of the 30-year friendship between two english women.', \
        'an excruciating demonstration of the unsalvageability of a movie saddled with an amateurish screenplay.', \
        'sadly , hewitt''s forte is leaning forward while wearing low-cut gowns , not making snappy comebacks.', \
        'since lee is a sentimentalist , the film is more worshipful than your random e ! true hollywood story.'
    ]
    sf = SentimentFeatures()
    for text in texts:
        features = sf.getFromLine(text)
        x_test = ' '.join(features)
        predictSentiment(sa, x_test)

if __name__ == '__main__':
    main()


■ 少し解説

今回は、前回定義したSentimentAnalyserクラスに新たに3つのメソッドを追加しました。

predictメソッド
LogisticRegression を使い予測します。予測した結果は0か1に分類されます。0がネガティブである"-1"を、 1がポジティブである"+1"を表します。

predict_proba
予測し、分類毎に確率を得ています。
本当は、このメソッドだけにしたいのですが、LogisticRegressionのpredict_probaが返すデータの中に、 LogisticRegressionのpredictメソッドが返すデータが入っていないような気がするので、predictメソッドとこのメソッドの2つを定義しています。

load
学習済みデータをロードします。

それと、単独の関数predictSentimentも定義。

predictSentiment
これは単独の関数。与えられた文章から結果を予測し、その予測した結果と、予測確率を表示しています。 予測確率は、predict_probaメソッドが返すデータから取得しています。 予測結果(0か1)により、予測確率の取り出す場所が異なっています。

■ 結果

perhap best sport movi ive ever seen
予測:+1 確率:0.9006017914136548
had more fun watch spi had most big summer movi
予測:+1 確率:0.7479161215615849
vividli convey shadow side year friendship between two english women
予測:+1 確率:0.9409305758954958
excruci demonstr unsalvag movi saddl amateurish screenplay
予測:-1 確率:0.8628974528161055
sadli hewitt fort lean forward while wear low cut gown make snappi comeback
予測:-1 確率:0.9293995549165373
sinc lee sentimentalist film more worship random true hollywood stori
予測:-1 確率:0.8095457186618346
  
Posted by gushwell at 21:00Comments(0)

2018年09月30日

言語処理100本ノックでPython入門 #73 - 機械学習、scikit-learnでロジスティック回帰


今日は言語処理100本ノック 2015の第8章・機械学習の問題73に挑戦です。

■ 問題

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

■ scikit-learnの準備


問題72までやって、やっと機械学習のための下準備ができたので、これから機械学習に入ります。第8章の学習項目には、scikit-learn とあるので、scikit-learnを使って機械学習をさせようと思います。 anaconda3には、scikit-learnモジュールが含まれているので、そのままimportするだけです。

以下の3つをimportします。
from sklearn.linear_model import LogisticRegression
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.externals import joblib

LogisticRegressionがロジスティック回帰分析を行うモジュールです。 CountVectorizerは、単語リストをベクトル化するために使います。 joblibは、学習した結果をファイルに保存するために使います。

今回作成したクラスは、SentimentAnalyserクラスです。SentimentFeatures、Stopwordsは前回と同じです。

まだ、scikit-learnについてあまり知識がない状況なので、いろんなサイトを見ながら作成しています。 以下にコードを載せます。

■ Pythonのコード

import re
from nltk import stem
from sklearn.linear_model import LogisticRegression
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.externals import joblib

class Stopwords:
    words = [
        'a', 'about', 'all', 'an', 'and', 'any', 'are', 'as', \
        'at', 'be', 'been', 'but', 'by', 'can', 'could', 'do', \
        'does', 'for', 'from', 'has', 'have', 'he', 'her', 'his', \
        'how', 'i', 'if', 'in', 'into', 'is', 'it', 'its', 'made', \
        'make', 'may', 'me', 'my', 'no', 'not', 'of', 'on', 'one', \
        'or', 'out', 'she', 'should', 'so', 'some', 'than', 'that', \
        'the', 'their', 'them', 'there', 'then', 'they', 'this', \
        'those', 'to', 'too', 'us', 'was', 'we', 'what', 'when',\
        'which', 'who', 'with', 'would', 'you', 'your', ''
    ]

    @staticmethod
    def exists(word):
        return word in  Stopwords.words

class SentimentFeatures:
    def __init__(self, filename):
        self.filename = filename
        self.stemmer = stem.PorterStemmer()

    @staticmethod
    def isValid(word):
        if word == '' or len(word) <= 2:
            return False
        if re.match(r'^[-=!@#$%^&*()_+|;";,.<>/?]+$', word):
            return False
        return not Stopwords.exists(word)

    def getFromLine(self, line):
        sentiment = line[:3]
        array = re.split(r'\s|,|\.|\(|\)|\'|/|\'|\[|\]|-', line[3:])
        # こういう時はlambda キーワードいらないんですね。
        words = filter(self.isValid, array)
        xs = map(self.stemmer.stem, words)
        return sentiment, xs

    def enumerate(self):
        with open(self.filename, 'r') as fin:
            for line in fin:
                yield self.getFromLine(line)


class SentimentAnalyser:
    def __init__(self):
        self.cv = CountVectorizer(encoding='utf-8')
        self.lr = LogisticRegression(solver='sag', max_iter=10000)

    # LogisticRegression を使い学習する
    def fit(self, X_train, y_train):
        X_train_cv = self.cv.fit_transform(X_train)
        self.lr.fit(X_train_cv, y_train)

    # 学習済みデータを保存する
    def save(self):
        # 学習したデータを保存する
        joblib.dump(self.cv, 'chapter08/cv73.learn')
        joblib.dump(self.lr, 'chapter08/lr73.learn')

    # 学習に利用するデータを取り出す
    # X[] は、素性データ
    # y[] は、センチメント (正解データ)
    @staticmethod
    def getFeatureData(filename):
        X = []
        y = []
        sf = SentimentFeatures(filename)
        for sentiment, features in sf.enumerate():
            y.append(1.0 if sentiment[0] == '+' else 0.0)
            X.append(' '.join(features))
        return X, y

def main():
    sa = SentimentAnalyser()
    X_train, y_train = sa.getFeatureData('chapter08/sentiment.txt')
    sa.fit(X_train, y_train)
    sa.save()

if __name__ == '__main__':
    main()


■ ちょっと解説 

getFeatureDataは、学習に利用する素性データをファイルから取り出すメソッドです。リストにいっぺんに入れています。

戻り値のXが学習用データ、yが正解データです。

fitが学習メソッド 与えられたデータをもとにfitメソッドで学習します。 fitメソッドを呼び出す前に、

X_train_cv = self.cv.fit_transform(X_train)

で、X_Trainデータをベクトル化しています。self.cv.は、CountVectorizerオブジェクトです。 って、本当にこれでいいのかはっきり言って自身がないです。

ベクトル化したものをLogisticRegressionのオブジェクトのfitメソッドに渡して学習させます。 もちろん、正解データも一緒に渡します。

LogisticRegressionのオブジェクト生成時のsolverには、
 'newton-cg', 'lbfgs', 'liblinear', 'sag', 'saga' 
のいずれかを指定しますが、この学習ではどれが良いのかよくわかっていません。ここでは、'sag'を選びました。

saveが学習済みのデータを保存するメソッドです。 CountVectorizerオブジェクトも保存しています。

main関数では、SentimentAnalyserクラスのインスタンスを生成し、getFeatureData, fit, save を順番に呼び出しています。

実行して、しばらく待つと、2つのファイルが作成されます。

  
Posted by gushwell at 21:30Comments(0)

2018年09月25日

言語処理100本ノックでPython入門 #72 - 機械学習、素性抽出


今日は言語処理100本ノック 2015の第8章・機械学習の問題72に挑戦です。

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

■ どうやって解くか

素性を各自で設計しと言われても、その知識が無いので困りました。

ただ、「素性としては,レビューからストップワードを除去し,各単語をステミング処理したものが最低限のベースラインとなるであろう.」 とあるので、極性分析(+評価か-評価か)に不要そうな単語を取り除けばよさそうです。 ステミングは、第6章でやったので、その時に使った nltkモジュールを使いました。

Stopwordsクラスは前問と同じものです。 新たに、SentimentFeaturesクラスを定義しました。
main関数では、このSentimentFeaturesクラスのenumerateで素性を列挙し、確認用に先頭50個だけを表示しています。

SentimentFeaturesクラスのメソッドは以下の3つです。

isValid
極性分析に有効そうな単語かどうかを判断、isValidならば、素性データとして残す。無効な単語は、「ストップワード」「文字数が2以下の単語」「記号からなるもの」の3つ。

getFromLine
与えられた1行(問題70で作成したテキストファイルの1行)を単語に分割しisValidの単語を抜き出し、それをステミングしたものを返す。この時、極性データ(+1, -1)とともにタプルで返す。

enumerate
問題70で作成したsentiment.txtから1行ずつ読み込み、getFromLineにその1行を渡して得られたデータ(極性データと、素性としての単語のタプル)を列挙。


ところで、getFromLineメソッドの中でfilterとmap関数を使っていますが、ラムダ式ではなくて、関数名だけを指定することもできるんですね。 これは、C#と同じです。ちゃんとpylintが指摘してくれました。

■ Pythonのコード

import re
from nltk import stem

class Stopwords:
    words = ['a', 'about', 'all', 'an', 'and', 'any', 'are', 'as', \
            'at', 'be', 'been', 'but', 'by', 'can', 'could', 'do', \
            'does', 'for', 'from', 'has', 'have', 'he', 'her', 'his', \
            'how', 'i', 'if', 'in', 'into', 'is', 'it', 'its', 'made', \
            'make', 'may', 'me', 'my', 'no', 'not', 'of', 'on', 'one', \
            'or', 'out', 'she', 'should', 'so', 'some', 'than', 'that', \
            'the', 'their', 'them', 'there', 'then', 'they', 'this', \
            'those', 'to', 'too', 'us', 'was', 'we', 'what', 'when',\
            'which', 'who', 'with', 'would', 'you', 'your', ''
        ]

    @staticmethod
    def exists(word):
        return word in  Stopwords.words

class SentimentFeatures:
    def __init__(self, filename):
        self.filename = filename
        self.stemmer = stem.PorterStemmer()

    @staticmethod
    def isValid(word):
        if word == '' or len(word) <= 2:
            return False
        if re.match(r'^[-=!@#$%^&*()_+|;";,.<>/?]+$', word):
            return False
        return not Stopwords.exists(word)

    def getFromLine(self, line):
        sentiment = line[:3]
        array = re.split(r'\s|,|\.|\(|\)|\'|/|\'|\[|\]|-', line[3:])
        # こういう時はlambda キーワードいらないんですね。
        words = filter(self.isValid, array)
        xs = map(self.stemmer.stem, words)
        return sentiment, xs

    def enumerate(self):
        with open(self.filename, 'r') as fin:
            for line in fin:
                yield self.getFromLine(line)

def main():
    sf = SentimentFeatures('chapter08/sentiment.txt')
    for sentiment, features in list(sf.enumerate())[:50]:
        print(sentiment, " ".join(features))

if __name__ == '__main__':
    main()


■ 結果

10個だけ載せます。

-1  film depress life itself
-1  ill fit tuxedo strictli off rack
-1  clich escap perfervid treatment gang warfar call ce wild
-1  circuit queen won learn thing busi curs film strateg place white sheet
-1  waterlog script plumb unchart depth stupid incoher sub sophomor sexual banter
+1  imax strap pair goggl shut real world take vicari voyag last frontier space
-1  analyz movi three word thumb friggin down
+1  stori haven seen big screen befor stori american human be know
-1  bray complet sea noth savag garden music video resum clue make movi
+1  teen review such recommend onli under year age onli veri mild rental



  
Posted by gushwell at 21:30Comments(0)