1.7. 演習:ニューラルネットワーク#

1.7.1. ニューラルネットワーク種類#

ニューラルネットワークは、広い意味で「人工神経回路網」を指す用語であり、複数のニューロンが層状に配置されたモデル全般を指します。パーセプトロンを複数繋げて複雑な問題を解けるようにした多重パーセプトロンmultilayer perceptron; MLP)を含め、畳み込みニューラルネットワークconvolutional neural network; CNN)、RNNrecurrent neural network; RNN)、生成敵対的ネットワークgenerative adversarial networks; GANs)などがあります。

多層パーセプトロン

多層パーセプトロンは、入力層から出力層の間に少なくとも 1 つの隠れ層を含み、各層のノードが完全に接続されているニューラルネットワークです。分類や回帰などに使われます。

畳み込みニューラルネットワーク

畳み込みニューラルネットワークは、画像の特徴(例えばエッジや形)を抽出する畳み込み層やプーリング層を組み込んだニューラルネットワークです。画像中にある物体を分類したり、検出したりするために広く使われています。

再帰型ニューラルネットワーク

再帰型ニューラルネットワークは、時間的に連続した情報が入力されたときに、過去の情報を記録したりするためのパラメータを持つように設計されたニューラルネットワークです。音声やテキストなどの系列データを扱うために利用されます。再帰型ニューラルネットワークを改良した長・短期記憶long short-term memory; LSTM)やゲート付き再帰ユニットgated recurrent unit; GRU)などもよく利用されています。

敵対的生成ネットワーク

敵対的生成ネットワークは、生成器と識別器という 2 つのニューラルネットワークから構成され、生成器はリアルなデータを生成しようとし、識別器はそれが本物か偽物かを判定するように、互いに競合しながら訓練するニューラルネットワークです。競合的な訓練結果、生成器は本物に近いデータが生成されるようになり、画像生成やデータ拡張などに使われています。

1.7.2. 演習準備#

本節では、次の Python ライブラリを利用します。numpy、pandas、matplotlib ライブラリはデータの読み込みや整形、可視化などに利用します。sklearn はサンプルデータセットの生成およびデータセットを訓練サブセットと検証サブセットに分けるために利用します。

import numpy as np
import pandas as pd
import matplotlib.animation
import matplotlib.pyplot as plt
import sklearn.datasets
import sklearn.model_selection
import sklearn.neural_network
import sklearn.preprocessing
import sklearn.decomposition
import sklearn.pipeline
import sklearn.metrics

1.7.3. 演習(多層パーセプトロン)#

線形分離不可能なサンプルデータを生成し、多重パーセプトロンを利用して分類を行う例を示します。sklearn.datasets モジュールを利用してサンプルデータを生成し、訓練サブセットと検証サブセットに分けます。

X, Y = sklearn.datasets.make_gaussian_quantiles(n_samples=200, n_features=2, n_classes=2, random_state=0)
X = sklearn.preprocessing.StandardScaler().fit_transform(X)
data = pd.concat([
    pd.Series(Y, name='Y'),
    pd.DataFrame(X, columns=['X1', 'X2'])
], axis='columns')
train_data, valid_data = sklearn.model_selection.train_test_split(data, test_size=0.2, random_state=0)
data.head()
Y X1 X2
0 0 -0.277064 0.144542
1 1 -0.980677 -1.466450
2 0 -0.328820 0.362942
3 0 1.008124 0.332191
4 0 -0.802167 -0.606805
Hide code cell source
fig = plt.figure()
ax = fig.add_subplot()
ax.scatter(data['X1'][data['Y'] == 0], data['X2'][data['Y'] == 0], label='0', alpha=0.5)
ax.scatter(data['X1'][data['Y'] == 1], data['X2'][data['Y'] == 1], label='1', alpha=0.5)
ax.set_xlabel('X1')
ax.set_ylabel('X2')
ax.legend()
plt.show()
../_images/66af15b7f977e75f6c1020a3deb26ccc4f41aa44d3a3fbefa3488c2ed33a75ca.png

1 つのパーセプトロンは 1 本の直線を生成するため、単独では線形分離不可能なデータを分類することはできません。しかし、データの分布から、5 本の直線があればおおよそ分類できると予測されるため、パーセプトロンを 5 つ繋げたニューラルネットワークで学習効果を確認していきましょう。

Hide code cell source
from graphviz import Digraph
from IPython.display import display, SVG

dot = Digraph()
dot.attr(rankdir='LR')

# input layer
dot.node('x1', 'X1', shape='circle')
dot.node('x2', 'X2', shape='circle')

# hidden layer
for i in range(1, 6):
    dot.node(f'h{i}', '', shape='circle')

# output layer
dot.node('y', 'Y', shape='circle')

# connections
for input_node in ['x1', 'x2']:
    for i in range(1, 6):
        dot.edge(input_node, f'h{i}')
for i in range(1, 6):
    dot.edge(f'h{i}', 'y')

display(SVG(dot.pipe(format='svg')))
../_images/ac34add501c2edbd6810e7c75a2572d10a47951ae452be18ecd17a308620ebb5.svg
model = sklearn.neural_network.MLPClassifier(hidden_layer_sizes=(5,), max_iter=1, learning_rate_init=0.05, warm_start=True, shuffle=True)
Hide code cell source
model.fit(train_data.drop(columns=['Y']), train_data['Y'])

fig = plt.figure()
ax = fig.add_subplot()
frames = ([i for i in range(101) if (i + 1) % 5 == 0])

ax0 = ax.scatter(train_data['X1'][train_data['Y'] == 0], train_data['X2'][train_data['Y'] == 0], label='0', alpha=0.5, color='#333333')
ax1 = ax.scatter(train_data['X1'][train_data['Y'] == 1], train_data['X2'][train_data['Y'] == 1], label='1', alpha=0.5, color='#E69F00')
x_vals = np.linspace(train_data['X1'].min(), train_data['X1'].max(), 1000)
y_vals = np.linspace(train_data['X2'].min(), train_data['X2'].max(), 1000)
xx, yy = np.meshgrid(x_vals, y_vals)
z = model.predict(np.c_[xx.ravel(), yy.ravel()])
z = z.reshape(xx.shape)
contour = ax.contourf(xx, yy, z, alpha=0.3)

ax.set_xlabel('X1')
ax.set_ylabel('X2')
ax.legend()

def _update_step(i):
    model.fit(train_data.drop(columns=['Y']), train_data['Y'])
    z = model.predict(np.c_[xx.ravel(), yy.ravel()])
    z = z.reshape(xx.shape)
    
    for coll in ax.collections:
        coll.remove()

    ax0 = ax.scatter(train_data['X1'][train_data['Y'] == 0], train_data['X2'][train_data['Y'] == 0], label='0', alpha=0.5, color='#333333')
    ax1 = ax.scatter(train_data['X1'][train_data['Y'] == 1], train_data['X2'][train_data['Y'] == 1], label='1', alpha=0.5, color='#E69F00')
    contour = ax.contourf(xx, yy, z, alpha=0.3)

    return ax0, ax1, contour.collections


ani = matplotlib.animation.FuncAnimation(fig, _update_step, frames=frames, interval=100, repeat=False, blit=False)

html = ani.to_jshtml()
plt.close(fig)
HTML(html)

このように、パーセプトロンが 5 つになると、分類境界を探るような学習過程は見られます。しかし、すべてのデータをきれいに分類できたわけではないため、最適解となっていません。そこで、パーセプトロンの数を 10 にして、もう一度モデルを構築してみよう。

model = sklearn.neural_network.MLPClassifier(hidden_layer_sizes=(10,), max_iter=1, learning_rate_init=0.05, warm_start=True, shuffle=True)
Hide code cell source
model.fit(train_data.drop(columns=['Y']), train_data['Y'])

fig = plt.figure()
ax = fig.add_subplot()
frames = ([i for i in range(101) if (i + 1) % 10 == 0])

ax0 = ax.scatter(train_data['X1'][train_data['Y'] == 0], train_data['X2'][train_data['Y'] == 0], label='0', alpha=0.5, color='#333333')
ax1 = ax.scatter(train_data['X1'][train_data['Y'] == 1], train_data['X2'][train_data['Y'] == 1], label='1', alpha=0.5, color='#E69F00')
x_vals = np.linspace(train_data['X1'].min(), train_data['X1'].max(), 1000)
y_vals = np.linspace(train_data['X2'].min(), train_data['X2'].max(), 1000)
xx, yy = np.meshgrid(x_vals, y_vals)
z = model.predict(np.c_[xx.ravel(), yy.ravel()])
z = z.reshape(xx.shape)
contour = ax.contourf(xx, yy, z, alpha=0.3)

ax.set_xlabel('X1')
ax.set_ylabel('X2')
ax.legend()

def _update_step(i):
    model.fit(train_data.drop(columns=['Y']), train_data['Y'])
    z = model.predict(np.c_[xx.ravel(), yy.ravel()])
    z = z.reshape(xx.shape)
    
    for coll in ax.collections:
        coll.remove()

    ax0 = ax.scatter(train_data['X1'][train_data['Y'] == 0], train_data['X2'][train_data['Y'] == 0], label='0', alpha=0.5, color='#333333')
    ax1 = ax.scatter(train_data['X1'][train_data['Y'] == 1], train_data['X2'][train_data['Y'] == 1], label='1', alpha=0.5, color='#E69F00')
    contour = ax.contourf(xx, yy, z, alpha=0.3)

    return ax0, ax1, contour.collections


ani = matplotlib.animation.FuncAnimation(fig, _update_step, frames=frames, interval=100, repeat=False, blit=False)

html = ani.to_jshtml()
plt.close(fig)
HTML(html)

このようにパーセプトロンの数を増やせば、利用できる直線の数も増えて、画面をいろいろな形で分割できるようになります。パーセプトロンが 1 つだけで解けなかった問題も、複数個のパーセプトロンを並列に繋げることで、ある程度値を分類できるようになります。

次に、パーセプトロンの学習において、その学習率 learning_rate_init を小さくしたり、大きくしたりしてみてください。学習率を小さくすると(例えば、0.001)、学習の進捗が遅くなり、学習を重ねても境界線がほとんど変化していないことが確認できるでしょう。また、学習率を大きくすると(例えば、0.1)、学習の進捗が速くなるものの、最適だと思われる境界で行ったり来たりして、なかなか最適な場所に収束しません。これは学習率が大きくて、少しも間違えると、ヒステリックになって間違えた箇所だけを覚えて、ついついそこを学習しすぎて全体を見落としてしまうようなイメージです。このように、学習率によって最終的に得られる結果が異なります。モデルを構築するにあたって、人間が何度も試行錯誤して、最適な学習率を決める必要があります。

1.7.4. 演習(ニューラルネットワーク)#

次に、並列に繋げた複数のパーセプトロンを一つの層とみなして、二つの層からなる多重パーセプトロンを構築します。1 層目と 2 層目のパーセプトロンの数をそれぞれ 5 つに設定します。

Hide code cell source
from graphviz import Digraph
from IPython.display import display, SVG
dot = Digraph()
dot.attr(rankdir='LR')

dot.node('x1', 'X1', shape='circle')
dot.node('x2', 'X2', shape='circle')
for i in range(1, 6):
    dot.node(f'h1_{i}', '', shape='circle')
for i in range(1, 6):
    dot.node(f'h2_{i}', '', shape='circle')
dot.node('y', 'Y', shape='circle')

for input_node in ['x1', 'x2']:
    for i in range(1, 6):
        dot.edge(input_node, f'h1_{i}')
for i in range(1, 6):
    for j in range(1, 6):
        dot.edge(f'h1_{i}', f'h2_{j}')
for i in range(1, 6):
    dot.edge(f'h2_{i}', 'y')

display(SVG(dot.pipe(format='svg')))
../_images/ad51d5035e33266d7a16816468f9764e7cca7024211d7fb265ef16674d6135da.svg
model_2 = sklearn.neural_network.MLPClassifier(hidden_layer_sizes=(5, 5), max_iter=1, learning_rate_init=0.01, warm_start=True, shuffle=True)
Hide code cell source
model_2.fit(train_data.drop(columns=['Y']), train_data['Y'])

fig = plt.figure()
ax = fig.add_subplot()
frames = ([i for i in range(101) if (i + 1) % 10 == 0])

ax0 = ax.scatter(train_data['X1'][train_data['Y'] == 0], train_data['X2'][train_data['Y'] == 0], label='0', alpha=0.5, color='#333333')
ax1 = ax.scatter(train_data['X1'][train_data['Y'] == 1], train_data['X2'][train_data['Y'] == 1], label='1', alpha=0.5, color='#E69F00')
x_vals = np.linspace(train_data['X1'].min(), train_data['X1'].max(), 1000)
y_vals = np.linspace(train_data['X2'].min(), train_data['X2'].max(), 1000)
xx, yy = np.meshgrid(x_vals, y_vals)
z = model_2.predict(np.c_[xx.ravel(), yy.ravel()])
z = z.reshape(xx.shape)
contour = ax.contourf(xx, yy, z, alpha=0.3)

ax.set_xlabel('X1')
ax.set_ylabel('X2')
ax.legend()

def _update_step(i):
    model_2.fit(train_data.drop(columns=['Y']), train_data['Y'])
    z = model_2.predict(np.c_[xx.ravel(), yy.ravel()])
    z = z.reshape(xx.shape)
    
    for coll in ax.collections:
        coll.remove()

    ax0 = ax.scatter(train_data['X1'][train_data['Y'] == 0], train_data['X2'][train_data['Y'] == 0], label='0', alpha=0.5, color='#333333')
    ax1 = ax.scatter(train_data['X1'][train_data['Y'] == 1], train_data['X2'][train_data['Y'] == 1], label='1', alpha=0.5, color='#E69F00')
    contour = ax.contourf(xx, yy, z, alpha=0.3)

    return ax0, ax1, contour.collections


ani = matplotlib.animation.FuncAnimation(fig, _update_step, frames=frames, interval=100, repeat=False, blit=False)

html = ani.to_jshtml()
plt.close(fig)
HTML(html)

パーセプトロンの層をさらに深くして、分離境界がどのように変化するのかをみていきます。入力と出力の間に 3 つの層を加え、それぞれの層には 5 つのパーセプトロンを配置します。

Hide code cell source
from graphviz import Digraph
from IPython.display import display, SVG

dot = Digraph()
dot.attr(rankdir='LR')

dot.node('x1', 'X1', shape='circle')
dot.node('x2', 'X2', shape='circle')

for i in range(1, 6):
    dot.node(f'h1_{i}', f'', shape='circle')
for i in range(1, 6):
    dot.node(f'h2_{i}', f'', shape='circle')
for i in range(1, 6):
    dot.node(f'h3_{i}', f'', shape='circle')
dot.node('y', 'Y', shape='circle')

for input_node in ['x1', 'x2']:
    for i in range(1, 6):
        dot.edge(input_node, f'h1_{i}')
for i in range(1, 6):
    for j in range(1, 6):
        dot.edge(f'h1_{i}', f'h2_{j}')
for i in range(1, 6):
    for j in range(1, 6):
        dot.edge(f'h2_{i}', f'h3_{j}')
for i in range(1, 6):
    dot.edge(f'h3_{i}', 'y')

display(SVG(dot.pipe(format='svg')))
../_images/3cd27b3aef474517ae03de6f13063b8159280dcae5ba80cd3953400d64d351cc.svg
model_3 = sklearn.neural_network.MLPClassifier(hidden_layer_sizes=(5, 5, 5), max_iter=1, learning_rate_init=0.01, warm_start=True, shuffle=True)
Hide code cell source
model_3.fit(train_data.drop(columns=['Y']), train_data['Y'])

fig = plt.figure()
ax = fig.add_subplot()
frames = ([i for i in range(101) if (i + 1) % 10 == 0])

ax0 = ax.scatter(train_data['X1'][train_data['Y'] == 0], train_data['X2'][train_data['Y'] == 0], label='0', alpha=0.5, color='#333333')
ax1 = ax.scatter(train_data['X1'][train_data['Y'] == 1], train_data['X2'][train_data['Y'] == 1], label='1', alpha=0.5, color='#E69F00')
x_vals = np.linspace(train_data['X1'].min(), train_data['X1'].max(), 1000)
y_vals = np.linspace(train_data['X2'].min(), train_data['X2'].max(), 1000)
xx, yy = np.meshgrid(x_vals, y_vals)
z = model_3.predict(np.c_[xx.ravel(), yy.ravel()])
z = z.reshape(xx.shape)
contour = ax.contourf(xx, yy, z, alpha=0.3)

ax.set_xlabel('X1')
ax.set_ylabel('X2')
ax.legend()

def _update_step(i):
    model_3.fit(train_data.drop(columns=['Y']), train_data['Y'])
    z = model_3.predict(np.c_[xx.ravel(), yy.ravel()])
    z = z.reshape(xx.shape)
    
    for coll in ax.collections:
        coll.remove()

    ax0 = ax.scatter(train_data['X1'][train_data['Y'] == 0], train_data['X2'][train_data['Y'] == 0], label='0', alpha=0.5, color='#333333')
    ax1 = ax.scatter(train_data['X1'][train_data['Y'] == 1], train_data['X2'][train_data['Y'] == 1], label='1', alpha=0.5, color='#E69F00')
    contour = ax.contourf(xx, yy, z, alpha=0.3)

    return ax0, ax1, contour.collections


ani = matplotlib.animation.FuncAnimation(fig, _update_step, frames=frames, interval=100, repeat=False, blit=False)

html = ani.to_jshtml()
plt.close(fig)
HTML(html)

次に、2 層のときと 3 層の時の予測性能を計算します。

pred_2 = model_2.predict(valid_data.drop(columns=['Y']))
acc_2 = sklearn.metrics.accuracy_score(valid_data['Y'], pred_2)

pred_3 = model_3.predict(valid_data.drop(columns=['Y']))
acc_3 = sklearn.metrics.accuracy_score(valid_data['Y'], pred_3)

print([acc_2, acc_3])
[0.45, 0.6]

このように、パーセプトロンからなる層を重ねると予測性能が改善されます。予測性能を改善するために、1 つの層においてパーセプトロンの増やのか、それとも層を深くするのかについて正解はありません。また、問題の複雑さによって、最適となるパーセプトロンの構造が異なります。そのため、多重パーセプトロンを利用するにあたり、各層のパーセプトロンの数や層の数を調整しなければなりません。非常に大変な作業です。また、入力と各パーセプトロンのつながりそれぞれにパラメータがあり、さらにパーセプトロンの層が増えると、そのパラメータも爆発に増加します。そのため、データが少ない場合は、十分に性能の良い構造を設計できない場合も多いです。

1.7.5. 演習(ネットワーク設計)#

sklearn.datasets の breask cancer データセットを利用して腫瘍細胞の種類を予測するニューラルネットワークを構築します。この節では、いくつかの異なる構造をしたニューラルネットワークを設計し、その中でもっともよいものを決めていくことにします。まず、breast cancer データを読み込みます。

data = sklearn.datasets.load_breast_cancer()
Y = data.target
X = data.data
data = pd.concat([
    pd.Series(Y^(Y&1==Y), name='Y'),
    pd.DataFrame(X, columns=data.feature_names),
], axis='columns')
data.head()
Y mean radius mean texture mean perimeter mean area mean smoothness mean compactness mean concavity mean concave points mean symmetry mean fractal dimension radius error texture error perimeter error area error smoothness error compactness error concavity error concave points error symmetry error fractal dimension error worst radius worst texture worst perimeter worst area worst smoothness worst compactness worst concavity worst concave points worst symmetry worst fractal dimension
0 1 17.99 10.38 122.80 1001.0 0.11840 0.27760 0.3001 0.14710 0.2419 0.07871 1.0950 0.9053 8.589 153.40 0.006399 0.04904 0.05373 0.01587 0.03003 0.006193 25.38 17.33 184.60 2019.0 0.1622 0.6656 0.7119 0.2654 0.4601 0.11890
1 1 20.57 17.77 132.90 1326.0 0.08474 0.07864 0.0869 0.07017 0.1812 0.05667 0.5435 0.7339 3.398 74.08 0.005225 0.01308 0.01860 0.01340 0.01389 0.003532 24.99 23.41 158.80 1956.0 0.1238 0.1866 0.2416 0.1860 0.2750 0.08902
2 1 19.69 21.25 130.00 1203.0 0.10960 0.15990 0.1974 0.12790 0.2069 0.05999 0.7456 0.7869 4.585 94.03 0.006150 0.04006 0.03832 0.02058 0.02250 0.004571 23.57 25.53 152.50 1709.0 0.1444 0.4245 0.4504 0.2430 0.3613 0.08758
3 1 11.42 20.38 77.58 386.1 0.14250 0.28390 0.2414 0.10520 0.2597 0.09744 0.4956 1.1560 3.445 27.23 0.009110 0.07458 0.05661 0.01867 0.05963 0.009208 14.91 26.50 98.87 567.7 0.2098 0.8663 0.6869 0.2575 0.6638 0.17300
4 1 20.29 14.34 135.10 1297.0 0.10030 0.13280 0.1980 0.10430 0.1809 0.05883 0.7572 0.7813 5.438 94.44 0.011490 0.02461 0.05688 0.01885 0.01756 0.005115 22.54 16.67 152.20 1575.0 0.1374 0.2050 0.4000 0.1625 0.2364 0.07678

次に、訓練サブセットと検証サブセットを用意します。この時点で作成した検証サブセットをテストサブセットと呼ぶことにします。なお、本来ならば、テストサブセットを、再実験などを通して取得するのが望ましいが、生物学や医学データの場合は難しいです。そのため、既存のデータセットからテストサブセットを作ります。訓練サブセットはモデルを訓練用に使います。テストデータセットは、訓練済みのモデルの性能を検証(テスト)するために利用します。

train_data, test_data = sklearn.model_selection.train_test_split(data, test_size=0.2, random_state=0)

既存のニューラルネットワークを利用するだけであれば、訓練サブセットでニューラルネットワークを訓練するだけで十分です。しかし、自分でいくつかのニューラルネットワーク構造を考え、その中から最適な構造を決定したい場合は、その判断に利用する検証データも必要です。そこで、訓練サブセットをさらに小分けして、訓練サブセットと検証サブセットにします。

train_data_, valid_data_ = sklearn.model_selection.train_test_split(train_data, test_size=0.2, random_state=0)

ここで、5 つのニューラルネットワークを設計して、訓練サブセット train_data_ で訓練します。

model_1 = sklearn.neural_network.MLPClassifier(hidden_layer_sizes=(2, ), learning_rate_init=0.001, random_state=0)
model_1.fit(train_data_.drop(columns=['Y']), train_data_['Y'])

model_2 = sklearn.neural_network.MLPClassifier(hidden_layer_sizes=(5, ), learning_rate_init=0.001, random_state=0)
model_2.fit(train_data_.drop(columns=['Y']), train_data_['Y'])

model_3 = sklearn.neural_network.MLPClassifier(hidden_layer_sizes=(10), learning_rate_init=0.001, random_state=0)
model_3.fit(train_data_.drop(columns=['Y']), train_data_['Y'])

model_4 = sklearn.neural_network.MLPClassifier(hidden_layer_sizes=(5, 5), learning_rate_init=0.001, random_state=0)
model_4.fit(train_data_.drop(columns=['Y']), train_data_['Y'])

model_5 = sklearn.neural_network.MLPClassifier(hidden_layer_sizes=(5, 5, 5, 5), learning_rate_init=0.001, random_state=0)
model_5.fit(train_data_.drop(columns=['Y']), train_data_['Y'])

次に訓練された 5 つのニューラルネットワークに検証データセットを代入し、それらの性能を見ます。ここでは性能を測る指標として正確度(accuracy)を利用します。なお、実際の問題では、解決したい問題にあった指標を用いる必要があります。例えば、良性を悪性と間違って予測してもよいので、悪性をしっかり悪性と予測できるようなモデルを作る場合は再現率を用います。

acc_1 = sklearn.metrics.accuracy_score(valid_data_['Y'], model_1.predict(valid_data_.drop(columns=['Y'])))
acc_2 = sklearn.metrics.accuracy_score(valid_data_['Y'], model_2.predict(valid_data_.drop(columns=['Y'])))
acc_3 = sklearn.metrics.accuracy_score(valid_data_['Y'], model_3.predict(valid_data_.drop(columns=['Y'])))
acc_4 = sklearn.metrics.accuracy_score(valid_data_['Y'], model_4.predict(valid_data_.drop(columns=['Y'])))
acc_5 = sklearn.metrics.accuracy_score(valid_data_['Y'], model_5.predict(valid_data_.drop(columns=['Y'])))

print([acc_1, acc_2, acc_3, acc_4, acc_5])
[0.6043956043956044, 0.6043956043956044, 0.8901098901098901, 0.945054945054945, 0.9340659340659341]

検証の結果を確認すると 2 層構造の Model 4 の検証性能が最も高いことがわかった。次に、Model 4 の性能を正しく評価するために、訓練サブセット(train_data_)と検証サブセット(valid_data_)を合わせてモデルを訓練し直して、テストセブセットでテストします。

model_best = sklearn.neural_network.MLPClassifier(hidden_layer_sizes=(5, 5), learning_rate_init=0.001, random_state=0)
model_best.fit(train_data.drop(columns=['Y']), train_data['Y'])

test_acc = sklearn.metrics.accuracy_score(test_data['Y'], model_best.predict(test_data.drop(columns=['Y'])))
print(test_acc)
0.9210526315789473

ここで出力されたテスト性能は一般的にいうモデルの性能となります。論文発表や製品実用化テストなどにおいて、このテスト性能の数値を報告します。一方で、途中に検証サブセット(valid_data_)で計算した性能はモデル選択に使われているものであり、最終的なモデルの性能の指標として使うべきではないです。しかし、残念なことに、検証サブセットで計算された性能を報告する場合が非常に多いです。おそらく、データセットの少なさが原因となっていることが考えられます。

1.7.6. 演習(交差検証)#

前節のように、複雑なプロセスを踏んで最終的に Model 4 が最適なモデルと決定することができ、その最終的な性能も評価できました。しかし、ここでは注意しなければならないことがあります。試しに、モデルの構造と訓練過程をそのままにして、乱数を変えた時に、推測されるモデルの性能はどう変わるのかを見ていきましょう。

model_1 = sklearn.neural_network.MLPClassifier(hidden_layer_sizes=(2, ), learning_rate_init=0.001, random_state=10)
model_1.fit(train_data_.drop(columns=['Y']), train_data_['Y'])

model_2 = sklearn.neural_network.MLPClassifier(hidden_layer_sizes=(5, ), learning_rate_init=0.001, random_state=10)
model_2.fit(train_data_.drop(columns=['Y']), train_data_['Y'])

model_3 = sklearn.neural_network.MLPClassifier(hidden_layer_sizes=(10), learning_rate_init=0.001, random_state=10)
model_3.fit(train_data_.drop(columns=['Y']), train_data_['Y'])

model_4 = sklearn.neural_network.MLPClassifier(hidden_layer_sizes=(5, 5), learning_rate_init=0.001, random_state=10)
model_4.fit(train_data_.drop(columns=['Y']), train_data_['Y'])

model_5 = sklearn.neural_network.MLPClassifier(hidden_layer_sizes=(5, 5, 5, 5), learning_rate_init=0.001, random_state=10)
model_5.fit(train_data_.drop(columns=['Y']), train_data_['Y'])

acc_1 = sklearn.metrics.accuracy_score(valid_data_['Y'], model_1.predict(valid_data_.drop(columns=['Y'])))
acc_2 = sklearn.metrics.accuracy_score(valid_data_['Y'], model_2.predict(valid_data_.drop(columns=['Y'])))
acc_3 = sklearn.metrics.accuracy_score(valid_data_['Y'], model_3.predict(valid_data_.drop(columns=['Y'])))
acc_4 = sklearn.metrics.accuracy_score(valid_data_['Y'], model_4.predict(valid_data_.drop(columns=['Y'])))
acc_5 = sklearn.metrics.accuracy_score(valid_data_['Y'], model_5.predict(valid_data_.drop(columns=['Y'])))

print([acc_1, acc_2, acc_3, acc_4, acc_5])
[0.9010989010989011, 0.6043956043956044, 0.9230769230769231, 0.6043956043956044, 0.9010989010989011]

このように、モデルの構造や訓練サブセット、訓練オプションなどが変化していないにも関わらず、乱数が変わるだけで、最適なモデルが変わってしまいました。乱数を変えることで、パラメータの初期値が変化し、また訓練サブセットのデータもシャッフルされるため、こられの影響で結果が変化したと考えられます。このことから、訓練サブセットが変化しても、結果がかわることが容易に想像されます。訓練サブセットが変わると結果も変わるのでは、最適なモデル選択できません。そこで、この訓練サブセットによる影響を取り除くために、k-分割交差検証とよばれる検証方法を用いて、最適なモデルを決定します。

k-分割交差検証では、データを k 分割します。次に、分割されたサブセットのうち k - 1 サブセットを利用してモデルを訓練し、残りの 1 サブセットでモデルの検証を行います。この操作を k 回繰り返して、すべてのサブセットが訓練と検証に使われるようにします。k 回繰り返すと、k 個の検証結果が得られるため、それらの平均を計算します。この平均指標を用いて最適なモデルを決定します。

では、まずデータセットを訓練サブセットとテストサブセットに分けます。

train_data, test_data = sklearn.model_selection.train_test_split(data, test_size=0.2, random_state=0)

次に訓練サブセットを 10 分割(k = 10)して、合計 10 回の訓練と検証を繰り返します。

model_designs = [(2,), (5,), (10,), (5, 5), (5, 5, 5)]
acc = []

for train_idx, valid_idx in sklearn.model_selection.KFold(n_splits=10, shuffle=True, random_state=0).split(train_data):
    for model_design in model_designs:
        train_data_, valid_data_ = train_data.iloc[train_idx, :], train_data.iloc[valid_idx, :]
        model = sklearn.neural_network.MLPClassifier(hidden_layer_sizes=model_design, learning_rate_init=0.001, random_state=0)
        model.fit(train_data_.drop(columns=['Y']), train_data_['Y'])
        acc.append(sklearn.metrics.accuracy_score(valid_data_['Y'], model.predict(valid_data_.drop(columns=['Y']))))

acc = np.array(acc).reshape(-1, 5)
print(acc)
[[0.60869565 0.60869565 0.93478261 0.97826087 0.97826087]
 [0.58695652 0.58695652 0.91304348 0.84782609 0.91304348]
 [0.67391304 0.67391304 0.86956522 0.91304348 0.93478261]
 [0.69565217 0.69565217 0.95652174 0.95652174 0.93478261]
 [0.69565217 0.69565217 0.91304348 0.86956522 0.86956522]
 [0.62222222 0.62222222 0.91111111 0.95555556 0.91111111]
 [0.64444444 0.64444444 0.88888889 0.93333333 0.93333333]
 [0.64444444 0.64444444 0.91111111 0.95555556 0.95555556]
 [0.55555556 0.55555556 0.91111111 0.95555556 0.93333333]
 [0.64444444 0.64444444 0.88888889 0.86666667 0.84444444]]
print(acc.mean(axis=0))
[0.63719807 0.63719807 0.90980676 0.92318841 0.92082126]
print(acc.std(axis=0))
[0.04308789 0.04308789 0.0230795  0.04386622 0.03718078]

5 回の検証を見ると、モデル 3、モデル 4、モデル 5 が他の二つに比べて明らかに性能が良いことがわかります。さらに 5 回のモデルの検証を通して、その accuracy の平均値で判断すると、モデル 4 またはモデル 5 のどちらかが最適モデルであると考えられます。しかし、標準偏差も考慮に入れて判断すると、結局モデル 3、モデル 4、モデル 5 どれも最適なモデルと考えられ、現段階の結果では判断が付きません。このような場合において、一般的に、パラメータの少ないモデル、つまりモデル 3 を選ぶことが多いです。