言語処理100本ノック 2015年版 (78,79)
78. 5分割交差検定
76-77の実験では,学習に用いた事例を評価にも用いたため,正当な評価とは言えない.すなわち,分類器が訓練事例を丸暗記する際の性能を評価しており,モデルの汎化性能を測定していない.そこで,5分割交差検定により,極性分類の正解率,適合率,再現率,F1スコアを求めよ.
5分割交差検定とは、学習データを5つに分割し、1つをテスト例、残り4つを学習データとして5回分類器を作成してはテストする方法です。最終的に平均を取ります。
見出し語化したデータは10695行存在するので、これを5で割ると2139行となります。
#!/usr/bin/env python import codecs import re import collections import math eta0 = 0.66 etan = 0.9999 guard = 0.0002 def sigmoid(x): return 1.0 / (1.0 + math.exp(-x)) def update(W, features, label, eta): a = sum([W[x] for x in features]) init_feature = 1 predict = sigmoid(a) label = ( label + 1) / 2 for x in features: dif = eta * ( predict -label ) * init_feature if (W[x] - dif) > guard or ( W[x] - dif) < (guard * -1): W[x] = W[x] - dif def cross_validation(datalist,testlist): t = 0 W = collections.defaultdict(float) fsen = codecs.open('72.txt', 'r', 'latin_1') n = 1 for line in fsen: if n in datalist: features = line[:-1].split(" ") update(W, features[1:], float(features[0]), eta0 * ( etan **t)) t += 1 n += 1 n = 1 countp = 0 countn = 0 good = 0 bad = 0 exapos = 0 trupos = 0 fsen = codecs.open('72.txt', 'r', 'latin_1') for line in fsen: if n in testlist: features = line[:-1].split(" ") a = sum([W[x] for x in features[1:]]) predict = sigmoid(a) predict = (predict * 2) - 1 predictlabel = "+1" if predict > 0 else "-1" # print(features[0],"\t",predictlabel,"\t",predict) # ポジネガ生成数 if predictlabel == "+1": countp += 1 else: countn += 1 # 正答率 if features[0] == predictlabel : good += 1 else : bad += 1 # 正例に関する適合率 if features[0] == "+1" and predictlabel == "+1" : exapos += 1 # 正例の数 if features[0] == "+1": trupos += 1 n += 1 accuracy_rate = good / ( good + bad) precision = exapos / countp recall = exapos / trupos f1 = ( 2 * precision * recall ) / ( precision + recall ) return [ accuracy_rate , precision , recall , f1 ] if __name__ == "__main__": linetotal = 10695 totallist = range(1,linetotal) resultlist = [] for x in range(5): xstart = x * ( linetotal / 5) + 1 xend = (x + 1) * ( linetotal / 5 ) datalist = range( int(xstart) , int(xend) ) testlist = list(set(totallist)-set(datalist)) resultlist.append(cross_validation(datalist,testlist)) accuracy_rate = 0 precision = 0 recall = 0 f1 = 0 for result in resultlist: accuracy_rate += result[0] precision += result[1] recall += result[2] f1 += result[3] print("accuracy_rate:", accuracy_rate / 5 ) print("precision:", precision / 5 ) print("recall:", recall / 5 ) print("f1:", f1 / 5 )
結果
accuracy_rate: 0.6725806451612903 precision: 0.672900104233187 recall: 0.6719448077506203 f1: 0.6721992196647257
79. 適合率-再現率グラフの描画
ロジスティック回帰モデルの分類の閾値を変化させることで,適合率-再現率グラフを描画せよ.
普通の「適合率-再現率グラフ」といえば検索結果の順位付けによる適合率と再現率をそれぞれ軸としたプロットですが、「閾値の変化」だとちょっと違います。少しずつ閾値を変化させていくことで適合率と再現率の変動を見ていくグラフのことで、「適合率-再現率グラフ」よりも「適合率&再現率グラフ」のほうがわかりやすいでしょう。
今回の場合であれば予想値が0付近のものは正例か負例か怪しいということで、こちらを削除すれば当たる率は上昇していきます。
ベースとして77を使うか78を使うか。78のほうが正当性は高いが5倍の時間がかかります。とりあえず今回は77をベースに作成してみます。
#!/usr/bin/env python import codecs import re import collections import math import numpy as np import matplotlib.pyplot as plt import matplotlib.font_manager as fm eta0 = 0.66 etan = 0.9999 guard = 0.0002 def sigmoid(x): return 1.0 / (1.0 + math.exp(-x)) def update(W, features, label, eta): a = sum([W[x] for x in features]) init_feature = 1 predict = sigmoid(a) label = ( label + 1) / 2 for x in features: dif = eta * ( predict -label ) * init_feature if (W[x] - dif) > guard or ( W[x] - dif) < (guard * -1): W[x] = W[x] - dif def logistic_regression(threshold): t = 0 W = collections.defaultdict(float) fsen = codecs.open('72.txt', 'r', 'latin_1') n = 0 for line in fsen: features = line[:-1].split(" ") update(W, features[1:], float(features[0]), eta0 * ( etan **t)) t += 1 n += 1 n = 0 countp = 0 countn = 0 zero = 0 good = 0 bad = 0 exapos = 0 trupos = 0 ts = threshold fsen = codecs.open('72.txt', 'r', 'latin_1') for line in fsen: features = line[:-1].split(" ") a = sum([W[x] for x in features[1:]]) predict = sigmoid(a) predict = (predict * 2) - 1 predictlabel = "+1" if predict > 0 else "-1" if predict > ts: predictlabel = "+1" elif predict < ts * -1: predictlabel = "-1" else: predictlabel = "0" print(features[0],"\t",predictlabel,"\t",predict) # ポジネガ生成数 if predictlabel == "+1": countp += 1 elif predictlabel == "-1": countn += 1 else: zero += 1 # 正答率 if features[0] == predictlabel : good += 1 elif predictlabel == "0": pass else : bad += 1 # 正例に関する適合率 if features[0] == "+1" and predictlabel == "+1" : exapos += 1 # 正例の数 if features[0] == "+1": trupos += 1 n += 1 accuracy_rate = good / ( good + bad) precision = exapos / countp recall = exapos / trupos f1 = ( 2 * precision * recall ) / ( precision + recall ) print("p:",countp," n:",countn) print("good:",good," bad:",bad) print("accuracy_rate:", accuracy_rate ) print("precision:", precision ) print("recall:", recall ) print("f1:", f1) return(ts,precision,recall,exapos,trupos) if __name__ == "__main__": resultlist = [logistic_regression(float(threshold)) for threshold in np.arange(0,1,0.1)] print(resultlist) threshold = [round(x[0],1) for x in resultlist ] precision = [x[1] for x in resultlist ] recall = [x[2]for x in resultlist ] plt.plot(threshold, precision, label = "precision", color = "red") plt.plot(threshold, recall, label = "recall", color = "blue") plt.xlabel("threshold") plt.ylabel("rate") plt.xlim(-0.05 , 0.95) plt.ylim(0,1) plt.title("logistic_regresssion") plt.legend(loc = 3) plt.show()
結果のグラフ
正例に関する適合率: 正例を正例と予測できた数 / 正例と予測した数
再現率: 正例を正例と予測できた数 / 実際の正例の数
閾値をあげると、正例を正例と予測できた数は減っていきます。ただし、正例と予測した数も減っていきます。そして、予測を外す可能性は減っていくので、適合率は上がっていきます。
しかし、再現率はどんどん下がってしまいます。