前回こちらでロジスティック回帰(およびソフトマックス回帰)でMNISTの0から9までの手書き数字の画像データセットを分類する記事を書きました。
[機械学習] ロジスティック回帰(およびソフトマックス回帰)でMNISTの分類
今回はMLP(多層パーセプトロン)を実装し、同様にMNISTの学習、分類を行います。
前提
- 基本の学習用に深層モデルやそのライブラリは用いず、NumPyで実装
- 実行環境はGoogle Colab。ランタイムはPython3(T4 GPU)を使用
※ 参照:機械学習・深層学習を勉強する際の検証用環境について - 本記事のコード全容はこちらからダウンロード可能。ipynbファイルであり、そのまま自身のGoogle Driveにアップロードして実行可能
- 数学的知識や用語の説明について、参考文献やリンクを最下部に掲載 (本記事内で詳細には解説しませんが、流れや実施内容がわかるようにしたいと思います)
全体の流れ
大きく分けると 7ステップ になります。
- データ準備・前処理
- 活性化関数・損失計算の定義
- 全結合層(Denseレイヤ)の実装
- モデル(MLP全体)の構築
- ミニバッチ学習の実装
- 検証・精度評価
- 推論結果の可視化(正解・不正解)
全体像を一言でいうとMNISTを対象に、NumPyだけで多層パーセプトロン(MLP)を実装し、
順伝播・誤差逆伝播・ミニバッチ学習によって分類精度を向上させ、
汎化性能と誤分類の傾向を可視化して確認する、という流れになります。
実装
1. データ準備・前処理
学習可能な数値表現に変換するフェーズで、ニューラルネットワークに入力可能な形式へ変換し、
汎化性能評価のためにデータセットを分割してます。
- MNIST(手書き数字画像)を読み込み
- 画素値を 0〜1に正規化
- 正解ラベルを one-hot ベクトルに変換
- 画像(28×28)を **1次元ベクトル(784次元)**へ変形
- 学習・検証・テストに分割
from sklearn.utils import shuffle
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split
from keras.datasets import mnist
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(34)
(x_mnist_1, t_mnist_1), (x_mnist_2, t_mnist_2) = mnist.load_data()
x_mnist = np.r_[x_mnist_1, x_mnist_2]
t_mnist = np.r_[t_mnist_1, t_mnist_2]
x_mnist = x_mnist.astype("float64") / 255.
t_mnist = np.eye(N=10)[t_mnist.astype("int32").flatten()]
x_mnist = x_mnist.reshape(x_mnist.shape[0], -1)
x_train_mnist, x_test_mnist, t_train_mnist, t_test_mnist =\
train_test_split(x_mnist, t_mnist, test_size=10000)
x_train_mnist, x_valid_mnist, t_train_mnist, t_valid_mnist =\
train_test_split(x_train_mnist, t_train_mnist, test_size=10000)
2. 活性化関数・損失計算の定義
数式の部品を用意するフェーズで、非線形性の導入と確率的出力を実現するため、
活性化関数および損失関数計算に必要な関数群を定義しています。
- ReLU(中間層)
- Softmax(出力層)
- ReLUの導関数
- 数値安定化したlog
def relu(x):
return np.maximum(x, 0)
def deriv_relu(x):
return (x > 0).astype(x.dtype)
def softmax(x):
x -= x.max(axis=1, keepdims=True)
x_exp = np.exp(x)
return x_exp / np.sum(x_exp, axis=1, keepdims=True)
def np_log(x):
return np.log(np.clip(x, 1e-10, 1e+10))
3. 全結合層(Denseレイヤ)の実装
ニューラルネットの1層を定義するフェーズで、各層における線形変換と活性化、および
誤差逆伝播に基づく勾配計算をクラスとして抽象化しています。
- 重み・バイアスの初期化(He初期化)
- 順伝播(xW + b → activation)
- 誤差逆伝播(delta計算)
- 勾配計算(L2正則化つき)
class Dense:
def __init__(self, in_dim, out_dim, function, deriv_function):
# He initialization (for ReLU)
self.W = (np.random.randn(in_dim, out_dim)
* np.sqrt(2.0 / in_dim)).astype("float64")
self.b = np.zeros(out_dim).astype("float64")
self.function = function
self.deriv_function = deriv_function
self.x = None
self.u = None
self.dW = None
self.db = None
def __call__(self, x):
"""
順伝播処理を行うメソッド.
x: (batch_size, in_dim_{j})
h: (batch_size, out_dim_{j})
"""
self.x = x
self.u = np.matmul(self.x, self.W) + self.b
h = self.function(self.u)
return h
def b_prop(self, delta, W):
"""
誤差逆伝播を行うメソッド.
"""
self.delta = self.deriv_function(self.u) * np.matmul(delta, W.T)
return self.delta
def compute_grad(self):
"""
勾配を計算するメソッド.
"""
batch_size = self.delta.shape[0]
lambda_ = 1e-4
self.dW = (np.matmul(self.x.T, self.delta) / batch_size
+ lambda_ * self.W)
self.db = np.matmul(np.ones(batch_size), self.delta) / batch_size
4. モデル(MLP全体)の構築
レイヤをつなげてネットワークにするフェーズで、複数の全結合層を直列に接続し、
MLP全体としての順伝播・逆伝播・学習更新を定義しています。
- Dense層を複数積み重ねてMLPを構成
- 順伝播:入力 → 出力
- 逆伝播:出力誤差 → 各層へ伝搬
- パラメータ更新(SGD)
class Model:
def __init__(self, hidden_dims, activation_functions, deriv_functions):
"""
:param hiden_dims: List[int],各層のノード数を格納したリスト.
:params activation_functions: List, 各層で用いる活性化関数を格納したリスト.
:params derive_functions: List,各層で用いる活性化関数の導関数を格納したリスト.
"""
# 各層をリストに格納していく
self.layers = []
for i in range(len(hidden_dims)-2): # 出力層以外は同じ構造
self.layers.append(Dense(hidden_dims[i], hidden_dims[i+1],
activation_functions[i], deriv_functions[i]))
self.layers.append(Dense(hidden_dims[-2], hidden_dims[-1],
activation_functions[-1], deriv_functions[-1])) # 出力層を追加
def __call__(self, x):
return self.forward(x)
def forward(self, x):
"""順伝播処理を行うメソッド"""
for layer in self.layers:
x = layer(x)
return x
def backward(self, delta):
"""誤差逆伝播,勾配計算を行うメソッド"""
for i, layer in enumerate(self.layers[::-1]):
if i == 0: # 出力層の場合
layer.delta = delta # y - t
layer.compute_grad()
else: # 出力層以外の場合
delta = layer.b_prop(delta, W) # 逆伝播
layer.compute_grad() # 勾配の計算
W = layer.W
def update(self, eps=0.01):
"""パラメータの更新を行うメソッド"""
for layer in self.layers:
layer.W -= eps * layer.dW
layer.b -= eps * layer.db
model = Model(hidden_dims=[784, 256, 128, 10],
activation_functions=[relu, relu, softmax],
deriv_functions=[deriv_relu, deriv_relu, deriv_softmax])
5. ミニバッチ学習の実装
実際に学習させるフェーズで、確率的勾配降下法(SGD)に基づく
ミニバッチ学習を実装し、効率的な最適化を行っています。
- データをシャッフル
- ミニバッチに分割
- 各バッチで
- 順伝播
- クロスエントロピー損失計算
- 逆伝播
- 重み更新
def create_batch(data, batch_size):
"""
:param data: np.ndarray,入力データ
:param batch_size: int,バッチサイズ
"""
num_batches, mod = divmod(data.shape[0], batch_size)
batched_data = np.split(data[: batch_size * num_batches], num_batches)
if mod:
batched_data.append(data[batch_size * num_batches:])
return batched_data
6. 検証・精度評価
ちゃんと学習できているか確認するフェーズで、学習データとは独立した検証データを用いて
損失と分類精度を評価し、汎化性能を確認している。
- 検証データで順伝播のみ実行
- 損失(COST)を計算
- Accuracy(正解率)を算出
- エポックごとにログ出力
def train_mst(model, x, t, eps=0.01):
# 順伝播
y = model(x)
# 誤差の計算
cost = (-t * np_log(y)).sum(axis=1).mean()
# 逆伝播
delta = y - t
model.backward(delta)
# パラメータの更新
model.update(eps)
return cost
def valid_mst(model, x, t):
# 順伝播
y = model(x)
# 誤差の計算
cost = (-t * np_log(y)).sum(axis=1).mean()
return cost, y
学習ループ
# バッチサイズを指定
batch_size = 128
for epoch in range(30):
x_train_mnist, t_train_mnist = shuffle(x_train_mnist, t_train_mnist)
x_train_batch, t_train_batch = \
create_batch(x_train_mnist, batch_size), create_batch(t_train_mnist, batch_size)
# ミニバッチ学習
for x, t in zip(x_train_batch, t_train_batch):
cost = train_mst(model, x, t, eps=0.01)
cost, y_pred = valid_mst(model, x_valid_mnist, t_valid_mnist)
accuracy = accuracy_score(t_valid_mnist.argmax(axis=1), y_pred.argmax(axis=1))
print(f"EPOCH: {epoch+1} Valid COST: {cost:.3f} Valid ACC: {accuracy:.3f}")
7. 推論結果の可視化(正解・不正解)
モデルの癖を理解するフェーズで、定量評価に加えて定性的評価を行い、
モデルの誤分類傾向や限界を可視的に分析します。
- 正解した画像と誤分類した画像を抽出
- 画像+真のラベル+予測ラベルを表示
- どんな数字で間違えるかを確認
# ===== 予測 =====
_, y_pred = valid_mst(model, x_valid_mnist, t_valid_mnist)
y_true = t_valid_mnist.argmax(axis=1)
y_pred_label = y_pred.argmax(axis=1)
def show_correct_incorrect(x, y_true, y_pred, n=10):
"""
正解・不正解を画像で表示
上段: 正解, 下段: 不正解
"""
correct_idx = np.where(y_true == y_pred)[0]
incorrect_idx = np.where(y_true != y_pred)[0]
fig = plt.figure(figsize=(12, 4))
# 正解例
for i, idx in enumerate(correct_idx[:n]):
ax = fig.add_subplot(2, n, i + 1, xticks=[], yticks=[])
ax.imshow(x[idx].reshape(28, 28), cmap="gray")
ax.set_title(f"✓ T:{y_true[idx]} P:{y_pred[idx]}", fontsize=9)
# 不正解例
for i, idx in enumerate(incorrect_idx[:n]):
ax = fig.add_subplot(2, n, n + i + 1, xticks=[], yticks=[])
ax.imshow(x[idx].reshape(28, 28), cmap="gray")
ax.set_title(f"✗ T:{y_true[idx]} P:{y_pred[idx]}", fontsize=9)
plt.suptitle("MNIST MLP Classification Results")
plt.tight_layout()
plt.show()
show_correct_incorrect(
x_valid_mnist,
y_true,
y_pred_label,
n=10
)
最後に
今回はかなり崩れている手書き数字は予測値と測定値で誤りがありました。この結果はむしろ正しくて、前回のソフトマックス回帰とあわせて以下となりました。
ソフトマックス回帰
→ 「わかりやすく書けている数字(典型例)しか当たらない」
今回のMLPは
→ 「典型例はかなり当たるが、崩れは弱い」
ようです。
MLPが崩れに弱い理由は、
👉 「位置関係を知らない」「全部を一気に見る」からで、コードでは
x_mnist = x_mnist.reshape(x_mnist.shape[0], -1) # 784次元
つまり、28×28の画像
→ 784個の数値の並び
であり、MLPにとっては「これは画像」ではなく「784次元のベクトル」であるからです。
例えば7の横棒がちょっと下がるなどすると、人間には「ちょっと崩れてるけど7だよね」となります。
でもMLPには、別の次元が変化した全く別の入力パターンとして見え、「同じ形が少しズレただけ」という概念が存在しないようです。
次は位置を活かしたCNN(畳み込みニューラルネットワーク)で、これらが解決できるかも検証する必要がありそうです。
参考文献、リンク
- ゼロからつくるPython機械学習プログラミング入門
-
詳解ディープラーニング第2版
※ 詳解とありますが、入門的な内容から丁寧に解説してあります。 -
YouTubeチャンネル - 予備校のノリで学ぶ「大学の数学・物理」
※ 数学的知識の学習としては、世界一わかりやすかったです。

