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月23日

C#でパズルを解こう!の記事をすべてQiitaに移行しました。

姉妹サイトである「Gushwell's C# Programming Page」の「C#でパズルを解こう!」で公開していた記事全27本をすべて、加筆修正し、Qiitaに移行しました。

Gushwell's C# Programming Page」では、多くのプログラムをC#+Silverlightで書いていましたが、C#+.NET Core(コンソールアプリ)に書き換えています。


記事の一覧を以下に示します。


協力最短詰めオセロ二人で協力しどちらかが完全勝利する最短手を見つける。 
ハノイの塔を再帰で解くハノイの塔を再帰で解くための考え方も解説してます。 
迷路を波状探索で解く迷路を波状探索(勝手に命名)で解いています。
迷路を幅優先探索で解く迷路を幅優先探索で解いています。
迷路を深さ優先探索で解く迷路を深さ優先探索で解いています。
MagicStarを解くMagicStar(星陣)を深さ優先探索で解く
8クィーンパズルN-Queenに対応しています。
ハイパークィーン問題8クィーンと似たパズル 
碁石拾い日本古来の碁石を使ったパズルを再帰で解く 
ナイト(騎士)巡回問題バックトラックでナイト巡回問題を解く
ナイト(騎士)の最適配置チェス盤に騎士を配置し効き筋で埋めます 
ステインハウスの三角形 整数のビットパターンで組合せを求める
Triangle15パズル順列が問題を解くカギ
コイン1510円玉と5円玉をつかったパズルを深さ優先探索で解く
覆銭問題Eventを発行し途中経過を知らせる 
割り切れる4桁の逆転数逆転した数で割り切れる4桁の数を得る
巡回数n倍すると巡回する自然数を求める
小町算 1-9の数字と+−で100にする:LINQで組合せを求める 
2乗した値が1-9で構成2乗した値と求めた値が1-9で構成される3桁の数を得る
小町数となる単位分数約分して1/nとなる数の分母と分子が小町数
3つの式が等しくなる小町数○○-○=○○/○=○+○*○が小町数
素因数分解小町素因数分解した結果の値が1-9で構成される数を求める
小町リング円の中の数の合計が等しくる小町数
センチュリーパズルK + N / D = 100 のK,N,Dが小町数
リングナンバーの最大公約数16個の数字の最大公約数を求める
二乗して回文となる非回文数例えば264は、264*264=69696 で結果が回文となる
Less Than Tree規則に沿って2色の石を置いてゆく。 IObserver / IObservableを利用。
   
Posted by gushwell at 17:45Comments(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)