浅野直樹の学習日記

この画面は、簡易表示です

未分類

scikit-learnでMNIST手書き数字の分類機械学習(3)

scikit-learnでMNIST手書き数字の分類機械学習 – 浅野直樹の学習日記scikit-learnでMNIST手書き数字の分類機械学習(2) – 浅野直樹の学習日記の続きです。

今回は、しきい値の調節、ニューラルネットワーク、アンサンブルに取り組みます。

 

1.しきい値の調節

機械学習の目的によっては、単純に正解率のスコアを上げるのではなく、正解率は下がってもよいからできるだけ漏れなく検出してほしいという場合があります。そのような場合にはしきい値を調節します。

0〜9までの10種類の数字を分類するのはややこしいので、8か8以外の数字かを分類するという二項分類の例で考えます。

from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn import svm
from sklearn.metrics import accuracy_score
from sklearn.metrics import plot_confusion_matrix
import matplotlib.pyplot as plt

#データの読み込み
digits = datasets.load_digits()

#訓練データとテストデータに分割
X_train, X_test, y_train, y_test = train_test_split(digits.data, digits.target, shuffle=False)

#8か8以外の数字かでnumpy配列のマスク処理
y_train_8 = (y_train == 8)
y_test_8 = (y_test == 8)

#機械学習モデルの作成
model = svm.SVC()

#作成したモデルで学習
model.fit(X_train, y_train_8)

#学習結果のテスト
y_test_pred = model.predict(X_test)
print('accuracy_score:', accuracy_score(y_test_8, y_test_pred))

#混同行列
plot_confusion_matrix(model, X_test, y_test_8)
plt.show()

いつもの要領で学習をして、結果を表示させます。

0.9755555555555555

正解率のスコアが0.975…とはなかなかいいですね。

次に、訓練データの混同行列と適合率(precision)・再現率(recall)・F1値を確認してみましょう。

import matplotlib.pyplot as plt
from sklearn.metrics import plot_confusion_matrix
from sklearn.metrics import precision_score, recall_score
from sklearn.metrics import classification_report

#混同行列
plot_confusion_matrix(model, X_train, y_train_8)
plt.show()

#適合率(precision)・再現率(recall)・F1値の手計算
y_train_pred = model.predict(X_train)
precision_score = precision_score(y_train_8, y_train_pred)
recall_score = recall_score(y_train_8, y_train_pred)
f1_score = (2 * precision_score * recall_score) / (precision_score + recall_score)
print('precision_score:', precision_score)
print('recall_score:', recall_score)
print('f1_score:', f1_score, '\n\n')

#適合率(precision)・再現率(recall)・F1値の一括計算
print(classification_report(y_train_8, y_train_pred))

以下の図と文字列が表示されます。

precision_score: 1.0
recall_score: 0.8872180451127819
f1_score: 0.9402390438247012


precision recall f1-score support

False 0.99 1.00 0.99 1214
True 1.00 0.89 0.94 133

accuracy 0.99 1347
macro avg 0.99 0.94 0.97 1347
weighted avg 0.99 0.99 0.99 1347

適合率は1.0と完璧ですが、再現率は0.88程度とやや低く、本当は8なのに見逃しているものが15個とけっこうたくさんあります。

適合率が下がることは覚悟で再現率を上げてみましょう。

その前に、適合率と再現率の関係をグラフ化します。

from sklearn.metrics import plot_precision_recall_curve

#適合率―再現率曲線
plot_precision_recall_curve(model, X_train, y_train_8)
plt.show()

これだけで簡単きれいに表示されます。

ROC曲線も見てみましょう。

from sklearn.metrics import plot_roc_curve

#ROC曲線
plot_roc_curve(model, X_train, y_train_8)
plt.show()

再現率(recall)0.99を狙ってみます。訓練データの中にある133個の8のうち、先ほどは15個を見逃していましたが、見逃してもよいのは1個だけということです。

from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay
import numpy as np

#訓練データの中で実際には8なのに8ではないと予測したデータ(false_negative)だけをリスト内包表記で抽出
false_negative = [X_train[i] for i in range(len(X_train)) if (y_train_pred[i] == False and y_train_8[i] == True)]

#false_negativeに決定関数を適用した値を取得後、ソートして表示
false_negative_value = model.decision_function(false_negative)
sorted_false_negative_value = np.sort(false_negative_value)
print(sorted_false_negative_value, '\n\n')

#決定関数のしきい値をsorted_false_negative_valueの2番目に小さい数に設定
y_train_pred_lower_threshold = (model.decision_function(X_train) >= sorted_false_negative_value[1])

#新しく設定したしきい値での適合率(precision)・再現率(recall)・F1値の一括計算
print(classification_report(y_train_8, y_train_pred_lower_threshold))

#新しく設定したしきい値での混同行列
disp = ConfusionMatrixDisplay(confusion_matrix(y_train_8, y_train_pred_lower_threshold))
disp.plot()
plt.show()

#新しく設定したしきい値での学習結果のテスト
y_test_pred_lower_threshold = (model.decision_function(X_test) >= sorted_false_negative_value[1])
accuracy_score(y_test_8, y_test_pred_lower_threshold)

#新しく設定したしきい値で学習したテストデータの混同行列
disp = ConfusionMatrixDisplay(confusion_matrix(y_test_8, y_test_pred_lower_threshold))
disp.plot()
plt.show()

結果は以下です。

[-0.56471249 -0.5230137 -0.4710125 -0.47010167 -0.46740437 -0.35965516
-0.34701922 -0.21615345 -0.192735 -0.11733131 -0.10970646 -0.06225593
-0.04509709 -0.03724317 -0.02381575]


precision recall f1-score support

False 1.00 1.00 1.00 1214
True 0.99 0.99 0.99 133

accuracy 1.00 1347
macro avg 0.99 1.00 0.99 1347
weighted avg 1.00 1.00 1.00 1347

プログラム内のコメントに書いたとおり、false_negative(偽陰性)だけを取り出し、その決定関数の値を参考にして、しきい値を-0.523…に設定しています。

当然ながら、目論見通りに再現率(recall)0.99を達成しています。

テストデータでの結果はそこまでよくありませんが、それでも当初の結果と比べると、見逃しは11個から4個に減少しています。

plot_confusion_matrixはXとyを引数に取るため、しきい値を変更した場合の混同行列はconfusion_matrixとConfusionMatrixDisplayを併用する形で表現しています。

やりたいことはできました。

Aurélien Géron 著,下田倫大 監訳,長尾高弘 訳『scikit-learnとTensorFlowによる実践機械学習』(オライリー・ジャパン, 2018)の3章を主に参照しました。

 

2.ニューラルネットワーク

scikit-learnでお手軽にニューラルネットワークを試してみます。

from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.neural_network import MLPClassifier
from sklearn.metrics import accuracy_score
from sklearn.metrics import plot_confusion_matrix
import matplotlib.pyplot as plt

#データの読み込み
digits = datasets.load_digits()

#訓練データとテストデータに分割
X_train, X_test, y_train, y_test = train_test_split(digits.data, digits.target, shuffle=False)

#機械学習モデルの作成
model = MLPClassifier(verbose=True, random_state=0)

#学習
model.fit(X_train, y_train)

#学習結果のテスト
y_pred = model.predict(X_test)
print('accuracy_score:', accuracy_score(y_test, y_pred))

#混同行列
plot_confusion_matrix(model, X_test, y_test)
plt.show()

いつもの手順でモデルにMLPClassifierを指定しただけです。ニューラルネットワークの雰囲気を醸し出すためにverbose=Trueにしています。よって以下のように出力されます。

Iteration 1, loss = 11.97505127
Iteration 2, loss = 5.72047413
Iteration 3, loss = 3.28455794
(中略)
Iteration 133, loss = 0.00351769
Iteration 134, loss = 0.00345282
Iteration 135, loss = 0.00341083
Training loss did not improve more than tol=0.000100 for 10 consecutive epochs. Stopping.
accuracy_score: 0.9244444444444444

サンプル数が少ないためか、それほどよくない結果ですね。

 

3.アンサンブル

アンサンブルも試してみます。

#データ関係
from sklearn import datasets
from sklearn.model_selection import train_test_split
#学習モデル関係
from sklearn.ensemble import VotingClassifier
from sklearn.svm import SVC
from sklearn.neighbors import KNeighborsClassifier
from sklearn.linear_model import LogisticRegressionCV
from sklearn.ensemble import RandomForestClassifier
from sklearn.neural_network import MLPClassifier
#結果の表示
from sklearn.metrics import accuracy_score
from sklearn.metrics import plot_confusion_matrix
import matplotlib.pyplot as plt

#データの読み込み
digits = datasets.load_digits()

#訓練データとテストデータに分割
X_train, X_test, y_train, y_test = train_test_split(digits.data, digits.target, shuffle=False)

#スケーリング
X_train = X_train / 16
X_test = X_test / 16

#機械学習モデルの作成
svc = SVC(probability=True)
kn = KNeighborsClassifier()
lr = LogisticRegressionCV(max_iter=500)
rf = RandomForestClassifier()
mlp = MLPClassifier(max_iter=500)

#アンサンブルモデルの作成
hard_voting = VotingClassifier(
estimators=[('svc', svc), ('kn', kn), ('lr', lr), ('rf', rf), ('mlp', mlp)],
voting='hard')

soft_voting = VotingClassifier(
estimators=[('svc', svc), ('kn', kn), ('lr', lr), ('rf', rf), ('mlp', mlp)],
voting='soft')

#学習
hard_voting.fit(X_train, y_train)
soft_voting.fit(X_train, y_train)

#学習結果のテスト
y_hard_pred = hard_voting.predict(X_test)
y_soft_pred = soft_voting.predict(X_test)
print('hard_voting accuracy_score:', accuracy_score(y_test, y_hard_pred))
print('soft_voting accuracy_score:', accuracy_score(y_test, y_soft_pred))

#混同行列
plot_confusion_matrix(hard_voting, X_test, y_test)
plt.show()
plot_confusion_matrix(soft_voting, X_test, y_test)
plt.show()

#個別のモデル
estimators = [svc, kn, lr, rf, mlp]
for estimator in estimators:
estimator.fit(X_train, y_train)
y_individual_pred = estimator.predict(X_test)
print(estimator.__class__.__name__, 'accuracy_score:', accuracy_score(y_test, y_individual_pred))

soft_votingのためにSVC(probability=True)としています。また、警告表示を見て、LogisticRegressionCVとMLPClassifierにはmax_iter=500と設定しました。

結果は以下です。

hard_voting accuracy_score: 0.9488888888888889
soft_voting accuracy_score: 0.9444444444444444

SVC accuracy_score: 0.9488888888888889
KNeighborsClassifier accuracy_score: 0.9644444444444444
LogisticRegressionCV accuracy_score: 0.9288888888888889
RandomForestClassifier accuracy_score: 0.9333333333333333
MLPClassifier accuracy_score: 0.9244444444444444

アンサンブルにしたからといって劇的にスコアがよくなるというわけではなさそうです。

 

 



scikit-learnでMNIST手書き数字の分類機械学習(2)

scikit-learnでMNIST手書き数字の分類機械学習 – 浅野直樹の学習日記の続きです。

今回はモデルやパラメータの選択に挑戦してみます。

その前に、次元削減、パイプライン、交差検証の練習をしておきます。

 

1.次元削減

今回は学習を何度も実行するので、その学習にかかる時間を短くできるように特徴量の次元削減をします。

import matplotlib.pyplot as plt
import numpy as np
from sklearn import datasets
from sklearn.decomposition import PCA

#データの読み込み
digits = datasets.load_digits()

#PCAによって削減される特徴量数と失われるデータの分散との関係を表示
pca = PCA().fit(digits.data)
plt.plot(np.cumsum(pca.explained_variance_ratio_))
plt.xlabel('number of components')
plt.ylabel('cumulative explained variance')

以下のような図が表示されます。

コードはIn Depth: Principal Component Analysis | Python Data Science Handbookから流用させてもらいました。

この図を見ると、元のデータの分散の0.95を説明できるようにしても特徴量の数を半分以下にできそうです。

実際に試してみましょう。

#PCAで次元削減
pca = PCA(0.95).fit(digits.data)
reduced_data = pca.transform(digits.data)
restored_data = pca.inverse_transform(reduced_data)
print('pca.n_components_:', pca.n_components_)
print('digits.data.shape:', digits.data.shape)
print('reduced_data.shape:', reduced_data.shape)
print('restored_data.shape:', restored_data.shape)

####次元削減をしてから元に戻した手書き数字データの画像表示####
#描画領域の確保
fig, axes = plt.subplots(10, 10, figsize=(15, 15), subplot_kw={'xticks':[], 'yticks':[]}, gridspec_kw=dict(hspace=0.5, wspace=0.5))

#最初の100枚の確保した描画領域に表示
for i, ax in enumerate(axes.flat):
    ax.imshow(restored_data[i].reshape(8, 8), cmap=plt.cm.gray_r, interpolation='nearest')
    ax.set_title(digits.target[i])

次のような文字列と画像が出力されます。

pca.n_components_: 29
digits.data.shape: (1797, 64)
reduced_data.shape: (1797, 29)
restored_data.shape: (1797, 64)

特徴量の次元数を64から29に減少させたたのに、画像を目視で確認すると次元削減をしなかった前回と比べても変化に気づかないくらいです。

なお、ここで言っている次元数は特徴量の次元数のことであって、配列の次元数ではないことにご注意ください。機械学習をするための配列の次元数は常に2です。

 

2.パイプライン

これから特徴量の次元数を削減してからいろいろなモデルで学習させることになるので、特徴量の次元削減とモデルとをパイプラインでくっつけます。

最初に一度だけデータ全体の特徴量の次元削減をしてからいろいろなモデルで学習させればよいのではないかと思う人もいるかもしれませんが、次元削減は訓練データにだけするようにしないと情報がリークして学習に悪影響を及ぼしてしまうので、そうしてはいけません。

from sklearn.model_selection import train_test_split
from sklearn import svm
from sklearn.pipeline import Pipeline
from sklearn.metrics import accuracy_score

#訓練データとテストデータに分割
X_train, X_test, y_train, y_test = train_test_split(digits.data, digits.target, shuffle=False)

#パイプラインの構築
steps = [('pca', PCA()), ('model', svm.SVC())]
pipe = Pipeline(steps)

#構築したパイプラインで学習
pipe.fit(X_train, y_train)

#学習結果のテスト
y_model = pipe.predict(X_test)
accuracy_score(y_test, y_model)

結果は以下です。

0.9622222222222222

前回と似たような値になりました。

 

3.交差検証

これまでは1回のテストでスコア(正解率)を計算してきましたが、訓練データとテストデータの分け方によってその値が変わります。

そこで、交差検証を導入して複数回のテストを行い、より信頼できる値を出してみます。

from sklearn.model_selection import cross_val_score

#交差検証
scores = cross_val_score(pipe, digits.data, digits.target, cv=3)
print('scores:', scores)
print('average_score:', scores.mean())

出力は以下です。

scores: [0.96994992 0.98330551 0.96994992]
average_score: 0.9744017807456872

scikit-learnのおかげで簡単にできました。

 

4.モデル選択

いよいよ本題のモデル選択です。scikit-learnには全てのモデルを返してくれるall_estimators()関数があるのでこれを使いましょう。

もう一度データの読み込みからやり直して一気に行きます。

import warnings
import pandas as pd
from sklearn import datasets
from sklearn.utils import all_estimators
from sklearn.decomposition import PCA
from sklearn.pipeline import Pipeline
from sklearn.model_selection import cross_val_score

#警告の非表示
warnings.filterwarnings('ignore')

#結果を格納するデータフレームの作成
df = pd.DataFrame(columns=['model_name', 'score1', 'score2', 'score3', 'average_score'])

#データの読み込み
digits = datasets.load_digits()

#全てのモデルで学習
for (name, algorithm) in all_estimators(type_filter="classifier"):
    try:
        steps = [('pca', PCA()), ('model', algorithm())]
        pipe = Pipeline(steps)
        scores = cross_val_score(pipe, digits.data, digits.target, cv=3)
        df.loc[len(df)+1] = [name, *scores, scores.mean()]
    except:
        pass

#スコアのいい順に並べ替えて表示
sorted_df = df.sort_values('average_score', ascending=False)
sorted_df.set_axis(range(1, (len(df)+1) ), axis='index')

気になる結果を表示します。

model_name score1 score2 score3 average_score
1 SVC 0.969950 0.983306 0.969950 0.974402
2 KNeighborsClassifier 0.958264 0.963272 0.966611 0.962716
3 ExtraTreesClassifier 0.949917 0.956594 0.943239 0.949917
4 NuSVC 0.943239 0.956594 0.936561 0.945465
5 MLPClassifier 0.921536 0.951586 0.933222 0.935448
6 LogisticRegressionCV 0.923205 0.951586 0.928214 0.934335
7 RandomForestClassifier 0.924875 0.929883 0.933222 0.929327
8 LogisticRegression 0.924875 0.934891 0.923205 0.927657
9 SGDClassifier 0.918197 0.934891 0.901503 0.918197
10 CalibratedClassifierCV 0.914858 0.934891 0.903172 0.917641
11 HistGradientBoostingClassifier 0.911519 0.928214 0.908180 0.915971
12 LinearDiscriminantAnalysis 0.926544 0.911519 0.906511 0.914858
13 PassiveAggressiveClassifier 0.899833 0.934891 0.903172 0.912632
14 LinearSVC 0.903172 0.929883 0.888147 0.907067
15 RidgeClassifierCV 0.921536 0.911519 0.879800 0.904285
16 RidgeClassifier 0.923205 0.906511 0.879800 0.903172
17 GradientBoostingClassifier 0.898164 0.881469 0.906511 0.895381
18 NearestCentroid 0.891486 0.881469 0.881469 0.884808
19 BaggingClassifier 0.894825 0.871452 0.846411 0.870896
20 GaussianNB 0.881469 0.851419 0.863105 0.865331
21 Perceptron 0.869783 0.874791 0.789649 0.844741
22 QuadraticDiscriminantAnalysis 0.881469 0.816361 0.813022 0.836950
23 DecisionTreeClassifier 0.833055 0.766277 0.756260 0.785198
24 BernoulliNB 0.757930 0.777963 0.786311 0.774068
25 ExtraTreeClassifier 0.621035 0.535893 0.580968 0.579299
26 AdaBoostClassifier 0.292154 0.345576 0.230384 0.289371
27 GaussianProcessClassifier 0.100167 0.101836 0.101836 0.101280
28 LabelPropagation 0.100167 0.098497 0.098497 0.099054
29 LabelSpreading 0.100167 0.098497 0.098497 0.099054
30 DummyClassifier 0.091820 0.088481 0.091820 0.090707
31 CategoricalNB NaN NaN NaN NaN
32 ComplementNB NaN NaN NaN NaN
33 MultinomialNB NaN NaN NaN NaN

うまく表示されました。

このようにランキング形式で表示させるのに苦労しました。pandasを使ういい練習になりました。

jupyter notebook上には先ほどのコードを実行するだけで結果がきれいに表示されますが、このブログ記事に載せるためにはto_html()メソッドを使ってhtmlにしました。

all_estimators()関数で取得できるモデルの中にはエラーが発生するものもあるのでtry節の中に入れています。

エラーまでは発生しなくても警告が表示されるものもたくさんあるので警告の表示をしないようにもしました。

本来はこのようにしてテストデータを見てしまうのではなく、Choosing the right estimator — scikit-learn 0.24.2 documentationなどを参考にしながらモデルを選ぶべきなのでしょう。

 

5.グリッドサーチで最適なパラメータの選択

次は一番成績のよいモデルだったSVCの最適なパラメータを探りましょう。

グリッドサーチで交差検証をしてくれるGridSearchCVに頼ります。

from sklearn.model_selection import train_test_split
from sklearn import svm
from sklearn.pipeline import Pipeline
from sklearn.model_selection import GridSearchCV
from sklearn.metrics import accuracy_score
import seaborn as sns

#訓練データとテストデータに分割
X_train, X_test, y_train, y_test = train_test_split(digits.data, digits.target, shuffle=False)

#パイプラインの構築
steps = [('pca', PCA()), ('model', svm.SVC())]
pipe = Pipeline(steps)

#グリッドサーチのパラメータの設定
param_grid = {
    'model__C': [0.1, 1, 10, 100, 1000],
    'model__gamma': [0.00001, 0.0001, 0.001, 0.01, 0.1]
}

#グリッドサーチの実行
grid_search = GridSearchCV(pipe, param_grid, cv=3)
grid_search.fit(X_train, y_train)

#結果の表示
print('cv_score:', grid_search.best_score_)
print('best parameters:', grid_search.best_params_)

#テストデータへの適用と結果の表示
y_best_model = grid_search.predict(X_test)
print('test_score:', accuracy_score(y_test, y_best_model))

#結果の可視化
cv_result = pd.DataFrame(grid_search.cv_results_)
cv_result = cv_result[['param_model__gamma', 'param_model__C', 'mean_test_score']]
cv_result_pivot = cv_result.pivot_table('mean_test_score', 'param_model__gamma', 'param_model__C')
heat_map = sns.heatmap(cv_result_pivot, cmap='viridis', annot=True)

結果は以下の通りです。

cv_score: 0.96362286562732
best parameters: {'model__C': 10, 'model__gamma': 0.001}
test_score: 0.9688888888888889

このあたりはAndreas C.Müller, Sarah Guido 著,中田秀基 訳『Pythonではじめる機械学習 : scikit-learnで学ぶ特徴量エンジニアリングと機械学習の基礎』(オライリー・ジャパン, 2017)を大いに参照しました。

 

6.自分で書いたオリジナルの手書き数字で実験

前回同様自分で書いたオリジナルの手書き数字で実験してみます。

import os
from skimage import io, color
from skimage.transform import resize
import numpy as np
import matplotlib.pyplot as plt

#オリジナルデータを格納するリストの作成
X_original = []
y_original = []

#ディレクトリから画像の読み込み
image_files = os.listdir('./original_images/')
for filename in image_files:
    image = io.imread('./original_images/' + filename)
    inverted_image = np.invert(image)
    gray_image = color.rgb2gray(inverted_image)
    resized_image = resize(gray_image, (8, 8))
    scaled_image = resized_image * 16
    X_original.append(scaled_image)
    y_original.append(int(filename[0]))

#画像データを機械学習に適した形にする
X_original_data = np.array(X_original).reshape(10, -1)
y_original_target = np.array(y_original)

#以前に作成したモデルでオリジナルデータの予測
y_original_model = grid_search.predict(X_original_data)

#描画領域の確保
fig, axes = plt.subplots(1, 10, figsize=(15, 15), subplot_kw={'xticks':[], 'yticks':[]}, gridspec_kw=dict(hspace=0.5, wspace=0.5))

#オリジナルデータを「予測値→真の値」というラベルとともに表示
for i, ax in enumerate(axes.flat):
    ax.imshow(X_original_data[i].reshape(8, 8), cmap=plt.cm.gray_r, interpolation='nearest')
    label = str(y_original_model[i]) + '→' + str(y_original_target[i])
    title_color = "black" if y_original_model[i] == y_original_target[i] else "red"
    ax.set_title(label, color=title_color)

前回「model.predict(X_original_data)」と書いたところを「grid_search.predict(X_original_data)」に変更しただけです。

前回よりも結果が悪くなってしまいました。

MNISTデータにより適合しすぎた(オリジナルの手書き数字をターゲットだと考えるなら訓練データに過学習した)せいかもしれません。

 



scikit-learnでMNIST手書き数字の分類機械学習

前から気になっていた機械学習に手を出し始めました。 何冊かの本を読み、インターネット上の資料も見て、ようやく少し理解できました。 自分の理解を整理するために、scikit-learnを使ったMNIST手書き数字の分類という典型的な例を示してみます。

 

Recognizing hand-written digits — scikit-learn 0.24.2 documentationの焼き直しです。In-Depth: Decision Trees and Random Forests | Python Data Science Handbookも参考にしています。

 

jupyter notebook上で順次実行するという形で試していますが、他の環境でも本質的な部分には変わりはないと思います。

 

1.データの読み込みと確認

まずはデータを読み込んで確認します。

import matplotlib.pyplot as plt
from sklearn import datasets

####MNIST手書き数字のデータ読み込みと確認####
digits = datasets.load_digits()
print(digits.keys())

####読み込んだ手書き数字データの画像表示####
#描画領域の確保
fig, axes = plt.subplots(10, 10, figsize=(15, 15), subplot_kw={'xticks':[], 'yticks':[]}, gridspec_kw=dict(hspace=0.5, wspace=0.5))

#確保した描画領域に読み込んだ画像の最初の100枚を表示
for i, ax in enumerate(axes.flat):
    ax.imshow(digits.images[i], cmap=plt.cm.gray_r, interpolation='nearest')
    ax.set_title(digits.target[i])

これを実行すると次のような文字と画像が表示されます。

dict_keys(['data', 'target', 'frame', 'feature_names', 'target_names', 'images', 'DESCR'])

digitsという変数名で読み込んだデータのimagesというプロパティに画像のデータが、targetというプロパティに正解ラベル(その人が何の数字を書いたか)が入っています。

 

2.データの前処理

機械学習に適した形になるようデータを前処理します。具体的に言うと、サンプル数×特徴量数の2次元配列を作るということです。

 

まず、先ほど画像として表示したdigits.imagesの形式を確認してみましょう。

#imagesプロパティの形状を取得して表示
image_shape = digits.images.shape
print(image_shape)

#サンプル数の取得
n_samples = len(digits.images)
print(n_samples)

#念のために上記二種類のやり方で取得したサンプル数が一致することを確認
print(image_shape[0] == n_samples)

次のように表示されます。

(1797, 8, 8)
1797
True

サンプル数が1797というのはいいとして、特徴量の部分が8×8の2次元の配列になっており、全体で3次元の配列になってしまっています。

画像として表示するためには2次元の配列であるほうが都合がよいのですが、機械学習のためには1次元のほうがよいです。

そこで次のように変形します。

my_data = digits.images.reshape((n_samples, -1))
print(my_data.shape)

reshapeメソッドにマイナス1を渡すと適当にうまく値を定めてくれます。ここでは「my_data = digits.images.reshape((n_samples, 64))」と同じことになります。

(1797, 64)

これでデータの前処理は完了です。

今回は画像に適した2次元配列の特徴量から機械学習に適した1次元配列の特徴量へと手作業で変換しましたが、実はdigitsデータには最初から1次元配列に変換されたデータが含まれています。digits.dataです。

import numpy as np
flag = np.allclose(my_data, digits.data)
print(flag)

同じデータになっていることが確認されました。

True

 

 

3.訓練データとテストデータに分け、機械学習のモデルを作成し、学習させる

いよいよ機械学習の本体的な部分です。

from sklearn.model_selection import train_test_split
from sklearn import svm
from sklearn.metrics import accuracy_score

#訓練データとテストデータに分割
X_train, X_test, y_train, y_test = train_test_split(my_data, digits.target, shuffle=False)

#機械学習モデルの作成
model = svm.SVC()

#作成したモデルで学習
model.fit(X_train, y_train)

#学習結果のテスト
y_model = model.predict(X_test)
accuracy_score(y_test, y_model)

たったのこれだけです。難しいことは背後でscikit-learnがやってくれます。

0.9488888888888889

1に近いほどいい値であり、0.95くらいならまずまずではないでしょうか。

せっかくなので、もう一つ別のモデルでも試してみましょう。

from sklearn.ensemble import RandomForestClassifier

#別のモデルの作成
another_model = RandomForestClassifier()

#別のモデルで学習
another_model.fit(X_train, y_train)

#別のモデルの学習結果のテスト
y_another_model = another_model.predict(X_test)
accuracy_score(y_test, y_another_model)

結果の数字は先ほどより少し悪くなりました。

0.9333333333333333

モデルやパラメータの選択が機械学習の腕の見せ所になるのですが、ここではこれ以上深入りしません。

 

4.結果の可視化

どの数字をどの数字に間違えたのかを混同行列で示してみましょう。

from sklearn.metrics import plot_confusion_matrix
plot_confusion_matrix(model, X_test, y_test)
plt.show()

きれいなヒートマップが表示されます。

本当は3なのに8だと予測してしまったものが6つあるということがわかります。

今度は予測値と真の値を画像で表示してみましょう。

先ほどとは逆に1次元配列の特徴量を2次元配列の特徴量へと復元します。「X_test[i].reshape(8, 8)」の部分です。

#描画領域の確保
fig, axes = plt.subplots(10, 10, figsize=(15, 15), subplot_kw={'xticks':[], 'yticks':[]}, gridspec_kw=dict(hspace=0.5, wspace=0.5))

#テストデータの最初の100枚を「予測値→真の値」というラベルとともに表示
for i, ax in enumerate(axes.flat):
    ax.imshow(X_test[i].reshape(8, 8), cmap=plt.cm.gray_r, interpolation='nearest')
    label = str(y_model[i]) + '→' + str(y_test[i])
    title_color = "black" if y_model[i] == y_test[i] else "red"
    ax.set_title(label, color=title_color)

このような画像が表示されます。

上から2行目、左から5列目のデータで、正しくは5であるところを6と予測してしまっています。画像を目で見るとこれは仕方ないかなという気もします。

せっかくなので予測を間違えたものだけを取り出して見てみましょう。

データの画像を表示する部分は基本的に先ほどと同じですが、描画領域の100に対してデータが23しかなくて list index out of rangeエラーが表示されるのを防ぐため、「if i+1 >= len(failed_test): break」を最後に加えています。

#テストデータのサンプル数を取得
n_test = len(X_test)

#予測を間違えたテストデータだけを予測値と真の値とともにリスト内包表記で抽出
failed_test = [{'data':X_test[i], 'y_model':y_model[i], 'y_test':y_test[i]} for i in range(n_test) if y_model[i] != y_test[i]]

#描画領域の確保
fig, axes = plt.subplots(10, 10, figsize=(15, 15), subplot_kw={'xticks':[], 'yticks':[]}, gridspec_kw=dict(hspace=0.5, wspace=0.5))

#予測を間違えたテストデータを「予測値→真の値」というラベルとともに表示
for i, ax in enumerate(axes.flat):
    ax.imshow(failed_test[i]['data'].reshape(8, 8), cmap=plt.cm.gray_r, interpolation='nearest')
    label = str(failed_test[i]['y_model']) + '→' + str(failed_test[i]['y_test'])
    ax.set_title(label, color='red')
    if i+1 >= len(failed_test): break

次のような画像が表示されます。

予測を間違えるのも頷ける判別しづらい画像が並んでいます。

 

5.自分で書いたオリジナルの手書き数字で実験

ここからはオマケです。しかし一番苦労した部分でもあります。

自分で書いたオリジナルの手書き数字を判別できるか実験してみました。

MNISTデータと同じような画像にして同じようなデータにするのが大変でした。

試行錯誤の過程は省いて結論だけ言います。

GIMPを使い、32px×32pxの背景を1600%に拡大して表示して、5px×5pxの鉛筆を選んでマウスで数字を書きました。

プログラムを実行しているファイルが存在している階層にoriginal_imagesディレクトリを作成し、そこに0.png, 1.png, …, 9.pngという名前で画像を保存しました。

画像を読み込んで変形し、先ほど学習したモデルで予測をするコードは以下です。

import os
from skimage import io, color
from skimage.transform import resize

#オリジナルデータを格納するリストの作成
X_original = []
y_original = []

#ディレクトリから画像の読み込み
image_files = os.listdir('./original_images/')
for filename in image_files:
    image = io.imread('./original_images/' + filename)
    inverted_image = np.invert(image)
    gray_image = color.rgb2gray(inverted_image)
    resized_image = resize(gray_image, (8, 8))
    scaled_image = resized_image * 16
    X_original.append(scaled_image)
    y_original.append(int(filename[0]))

#画像データを機械学習に適した形にする
X_original_data = np.array(X_original).reshape(10, -1)
y_original_target = np.array(y_original)

#以前に作成したモデルでオリジナルデータの予測
y_original_model = model.predict(X_original_data)

#描画領域の確保
fig, axes = plt.subplots(1, 10, figsize=(15, 15), subplot_kw={'xticks':[], 'yticks':[]}, gridspec_kw=dict(hspace=0.5, wspace=0.5))

#オリジナルデータを「予測値→真の値」というラベルとともに表示
for i, ax in enumerate(axes.flat):
    ax.imshow(X_original_data[i].reshape(8, 8), cmap=plt.cm.gray_r, interpolation='nearest')
    label = str(y_original_model[i]) + '→' + str(y_original_target[i])
    title_color = "black" if y_original_model[i] == y_original_target[i] else "red"
    ax.set_title(label, color=title_color)

なぜか色が反転していたのでnp.invert関数でその補正をして、rgb画像からgray画像に変換し、32×32の画像を8×8にリサイズしています。

MNISTデータは0〜16で色の濃さを表現していることに気づかずハマリました。scikit-imageで何も指定せずにファイルを読み込むと0〜1で色の濃さが表現されているので、それを単純に16倍しました。

結果の表示は以下の通りです。

まずまずではないでしょうか。

 



令和3(2021)年司法試験予備試験論文再現答案民事訴訟法

再現答案

 以下民事訴訟法についてはその条数のみを示す。

 

〔設問1〕

(1) 本件訴訟は、債権者代位権(民法423条1項)に基づいて提起されている。この場合、債権者のXは、債務者Yの法定訴訟担当であると解される。そして、民法423条の6により、XはYに対して訴訟告知をしているはずである。

 訴訟告知を受けて訴訟に参加する場合は、補助参加(42条)によることが多いが、だからといって共同訴訟参加(52条)が許されないということではない。そこで、52条1項の要件に沿って検討する。

 本件訴訟の目的である、本件不動産のZの持分2分の1について、ZからYに対して遺産分割を原因とする所有権移転登記手続請求権は、当事者の一方であるX及び第三者Yについて合一にのみ確定すべき場合に当たる。よって、Yは、本件訴訟に共同訴訟参加をすることができる。

 Yは、XY間に債権債務関係はないと考えており、もしそうであるなら本件訴訟の前提となっている被代位債権が存在しないことになるので、共同訴訟参加をすることができないようにも思われる。しかし、Yが主観的にそのように考えているということは、共同訴訟参加の際の訴訟資料には顕出されないため、共同訴訟参加をすることができると解する。

 もっとも、本件訴訟が確定すると、信義則により、Yは後訴で本件貸付債権の不存在を主張することができなくなると解されるので、共同訴訟参加ではなく、独立当事者参加(47条1項)をするほうが望ましい。

(2) 独立当事者参加の要件は47条1項に規定されている。以下では前段と後段に分けて検討する。

 ① 47条1項前段について

 既判力は、主文に包含するものに限り発生するので(114条1項)、被代位債権の判断について既判力は生じない。そうすると、後訴で本件貸付債権の不存在を主張することができるため、その点につき、訴訟の結果によって権利が害されることはない。

 Xが敗訴した場合、ZからYに対して遺産分割を原因とする所有権移転登記手続を求めることができなくなるが、Yとしては、Zに対して登記名義の移転を求めるつもりはないので、訴訟の結果によって権利が害されることはない。

 Xが勝訴して、本件不動産に執行をかけられると、YがZから責任を追及されるおそれがある。しかし、それは後訴で本件貸付債権の不存在を主張することによって防ぐべきことであり、本件訴訟の結果によって権利が害されるとは言えない。

 ② 47条1項後段について

 本件訴訟の目的である、本件不動産のZの持分2分の1について、ZからYに対しての遺産分割を原因とする所有権移転登記手続請求権について、Yは自己の権利であることを主張していないため、要件を満たさない。

 以上より、Yは、本件訴訟に独立当事者参加をすることはできない。

 

〔設問2〕

第1 本件判決の既判力がYに及ぶか否か

 先述したように、債権者代位訴訟は、法定訴訟担当なので、当事者であるXが他人であるYのために原告となった場合に当たり、その他人であるYに既判力が及ぶ。

 仮にYに既判力が及ばないとしても、Yは訴訟告知を受けているので、53条4項により、46条の参加的効力が及ぶ。この参加的効力は、敗訴責任の分担という補助参加の制度趣旨から、被参加人敗訴の場合は、主文に包含するものだけでなく、それを導くための理由についても効力が生じると解されている。既判力以上の効力である。本件では、被参加人Xが敗訴しており、Yに既判力以上の参加的効力が及ぶので、既判力が及ぶと言ってよい。

第2 本件判決の効力がAに及ぶか否か

 繰り返し述べているように、債権者代位訴訟は、被代位債権の債務者のために債権者が訴訟を遂行するものである。第1で述べたように、本件判決の既判力がYに及ぶのだから、Yを介してその効力はAに及ぶ。

以上

 

感想

 法律実務基礎科目に続いて債権者代位なのかと思い、準備してきた人なら正解筋をすらすらと書くことができるのだろうなと想像しました。私は準備をしておらず、よくわからないまま、全体の整合性もあまり考えず、目の前の記述に集中しました。



令和3(2021)年司法試験予備試験論文再現答案商法

再現答案

 以下会社法についてはその条数のみを示す。

 

〔設問1〕

 乙社はまず、本件代金が発生する基礎となった本件取引基本契約が有効に成立していると主張する。

 これに対し、甲社は、株式会社は代表者を通じて契約を締結するのであって、代表取締役のBが存在するからCは代表権のない取締役であり(349条1項)、そのCが締結した本件取引基本契約の効力は甲社に帰属しないと反論する。

 乙社は、それに対し、354条の表見代表取締役の主張をする。代表取締役以外の取締役であるCは、副社長という株式会社を代表する権限を有するものと認められる名称を使用して本件取引基本契約を締結している。甲社がその名称を付したと言えるかどうかが問題となるが、甲社の発行済株式の5分の4を有しているAがこれを容認しており(少なくともBと同等の権限をCにも与えるべきだということはCに代表権を与えるべきだということである)、甲社がその名称を付したと評価してよい。

 この契約書には代表印が押されており、その他乙社がCに代表権がないことを知ることのできた事情は見当たらない。

 以上より、甲社は、本件取引基本契約を締結した責任を負い、乙社は甲社に対して本件代金を請求することができる。

 

〔設問2〕

第1 甲社のBに対する本件慰労金の返還請求の根拠及び内容

 退職慰労金は、取締役を退任後に支給されるものであるが、職務執行の対価として株式会社から受ける財産上の利益である。よって、361条1項により、同項各号について、定款に当該事項を定めていないときは、株主総会の決議によって定めなければならない。

 本件では、役員の報酬については定款に定められていないということなので、退職慰労金の定めもないものと思われる。そして、退職慰労金についての株主総会の決議も存在しない。

 以上より、本件慰労金の支給は無効である。

 よって、民法703条の不当利得返還請求権を行使して、甲社は、Bに対し、1800万円の返還を請求することになる。

第2 これを拒むためにBの立場において考えられる主張及びその当否

1.361条1項の要件は満たしているという主張

 361条1項の趣旨は、取締役が自ら高額の報酬を決定するというお手盛りを防止することであり、だからこそ定款又は株主総会の決議が要求されているのである。

 本件では、Aが他社から甲社の取締役として引き抜いてきたBが代表取締役に選定された時点で、Aは甲社の株式の全部を保有していた。よって、Aが書面又は電磁的記録により同意の意思表示をしたときは、株主総会の決議があったものとみなすことができた(319条1項)。確かに甲社ではその手続きをしておらず、株主総会の決議は存在していないのであるが、前述の361条1項の趣旨からすると、要件を満たしていると言える。

 そうだとしても、その内容は、1800万円という具体的な金額ではなく、本件内規に従うというものである。このような定めも、361条1項2号により、有効である。

 以上より、本件慰労金は有効であり、Bは返還請求を拒むことができる。

2.取締役解職によって生じた損害賠償請求権との相殺

 念のために、Bとしては、取締役解職によって生じた損害賠償請求権と相殺するとの予備的主張を行う。

 役員である取締役は、いつでも、株主総会の決議によって解任することができるが(339条1項)、その解任について正当な理由がある場合を除き、株式会社に対し、解任によって生じた損害の賠償を請求することができる(339条2項)。代表取締役から代表権のない取締役にさせる解職と、取締役の地位を奪う解任とは異なるが、中小企業では代表取締役が独断的に決定して代表権のない取締役には事実上何の権限もないことも珍しくなく、339条を類推適用してよい。

 本件では、Bが株主総会で代表取締役から解職され、その結果自ら辞任している。この解職に正当な理由はない。よって、Bは、解任によって生じた損害の賠償を請求することができる。代表権のある取締役の報酬がいくらで代表権のない取締役の報酬がいくらかなどが定かではないが、仮に本件慰労金の返還請求権が発生するとしたら、相当額の損害賠償請求権と相殺すると主張する。民法505条1項の相殺の要件は満たしている。

 

感想

 〔設問1〕では908条の登記の効力との関係が頭をよぎりましたが、時間も厳しく盛り込むことができませんでした。〔設問2〕は、役員の任期がわざわざ問題文に記載されていることと、他に書くことがあまり思いつかなかったことから、339条の主張をすべきなのだと判断しました。




top