Visualizing MNIST Training with Deep Neural Networks A developer visualized the training of a deep neural network on the MNIST dataset using PyTorch. The project displays real-time updates of the input image, hidden layer activations, output probability distribution, and loss curves. The implementation uses a single hidden layer with 300 nodes and sigmoid activation, trained with stochastic gradient descent. | import numpy as np | | | import matplotlib.pyplot as plt | | | import pandas as pd | | | import matplotlib fontja | | | import torch | | | import torch.nn as nn | | | グラフの配色(見分けやすさを検証済みのパレット) | | | C BLUE = ' 2a78d6' 損失・確率バーの色 | | | C AQUA = ' 1baf7a' 正解率の色 | | | C INK = ' 0b0b0b' 文字色 | | | C MUTED = ' 898781' 軸・補助的な文字の色 | | | C GRID = ' e1e0d9' グリッド線の色 | | | ニューラルネットワークの構造を定義 | | | i nodes = 784 入力層のノード数(28×28ピクセル = 784) | | | h nodes = 300 中間層(隠れ層)のノード数 | | | o nodes = 10 出力層のノード数(数字0〜9の10種類) | | | MNIST(手書き数字)の学習用データを読み込む(1行 = 正解ラベル + 784ピクセルの値) | | | f = pd.read csv 'mnist train 10000.csv', encoding='ms932', sep=',', header=None | | | data = f.values | | | データの並び順をランダムにシャッフルする | | | index = np.arange len f | | | np.random.shuffle index | | | 学習に使うデータ数をユーザーに入力してもらう | | | no of trains = int input '何回学習しますか?' | | | train index = index 0:no of trains | | | 学習に使わなかったデータから200件を「検証用」に取っておく | | | (学習中に正解率を測るためのデータ。学習には使わない) | | | val index = index no of trains:no of trains + 200 | | | if len val index == 0: | | | val index = index 0:200 | | | 入力データ:ピクセル値(0〜255)を255で割って0〜1の範囲に正規化する | | | inpt = np.asarray data train index, 1: / 255.0 | | | ===== PyTorchでニューラルネットワークを定義する ===== | | | model = nn.Sequential | | | nn.Linear i nodes, h nodes, bias=False , 入力層→中間層の重み | | | nn.Sigmoid , シグモイド関数で中間層の出力を計算 | | | nn.Linear h nodes, o nodes, bias=False , 中間層→出力層の重み | | | | | | 損失関数:交差エントロピー(内部でソフトマックス関数を適用して誤差を計算する) | | | loss fn = nn.CrossEntropyLoss | | | 最適化手法:勾配降下法(SGD) | | | learningRate = 0.1 | | | optimizer = torch.optim.SGD model.parameters , lr=learningRate | | | 学習データをPyTorchのテンソル形式に変換する | | | x train = torch.tensor inpt, dtype=torch.float32 | | | t train = torch.tensor data train index, 0 .astype np.int64 | | | x val = torch.tensor np.asarray data val index, 1: / 255.0, dtype=torch.float32 | | | t val = data val index, 0 .astype np.int64 | | | ===== 学習の様子を映す画面(7つのパネル)を準備する ===== | | | plt.ion | | | fig = plt.figure figsize= 19, 9 , dpi=100, constrained layout=True | | | gs = fig.add gridspec 2, 4 | | | 折れ線グラフのパネルの見た目を整える共通処理 | | | def style axis ax : | | | ax.grid True, color=C GRID, linewidth=0.8 | | | ax.tick params colors=C MUTED, labelsize=10 | | | for spine in ax.spines.values : | | | spine.set color C GRID | | | --- パネル1:いま学習している画像 --- | | | ax img = fig.add subplot gs 0, 0 | | | ax img.set title 'いま学習している画像', size=14 | | | ax img.axis 'off' | | | im img = ax img.imshow np.zeros 28, 28 , cmap='Greys', vmin=0, vmax=1, interpolation='None' | | | --- パネル2:中間層の反応(いまの画像に対する300ノードの出力) --- | | | シグモイド関数の出力(0〜1)を15×20のマス目にして表示する。 | | | 画像ごとに反応するノードの組み合わせが変わる様子が見える | | | ax hid = fig.add subplot gs 0, 1 | | | ax hid.set title '中間層の反応(300ノード)\n濃い = 強く反応', size=14 | | | ax hid.axis 'off' | | | im hid = ax hid.imshow np.zeros 15, 20 , cmap='Blues', vmin=0, vmax=1, interpolation='None' | | | --- パネル3:出力層の確率分布(10個の数字それぞれの「自信」) --- | | | ax prob = fig.add subplot gs 0, 2 | | | ax prob.set title '出力の確率分布', size=14 | | | bars = ax prob.bar np.arange o nodes , np.zeros o nodes , color=C BLUE | | | ax prob.set ylim 0, 1 | | | ax prob.set xticks np.arange o nodes | | | ax prob.set xlabel '数字(▲ = 正解)', color=C MUTED | | | style axis ax prob | | | 正解の位置を示す ▲ マーカー | | | marker, = ax prob.plot 0 , -0.06 , marker='^', markersize=12, | | | color=C INK, clip on=False, linestyle='None' | | | --- パネル4:損失(誤差)の推移 --- | | | ax loss = fig.add subplot gs 0, 3 | | | ax loss.set title '損失(誤差)の推移', size=14 | | | ax loss.set xlabel '学習した枚数', color=C MUTED | | | line raw, = ax loss.plot , , color=C MUTED, linewidth=0.8, alpha=0.4, label='1枚ごとの損失' | | | line avg, = ax loss.plot , , color=C BLUE, linewidth=2, label='移動平均(50枚)' | | | ax loss.legend loc='upper right', frameon=False, fontsize=10 | | | style axis ax loss | | | --- パネル5:入力層→中間層の重みの変化 --- | | | 中間層の16個のノードについて、784個の重みを28×28の画像にして並べる。 | | | 学習が進むと、ランダムなノイズから数字の「部品」のようなパターンが浮かび上がってくる | | | ax w = fig.add subplot gs 1, 0 | | | ax w.set title '入力層→中間層の重み(16ノード分)\n青 = マイナス / 赤 = プラス', size=14 | | | ax w.axis 'off' | | | n show = 4 4×4 = 16ノード分を表示 | | | gap = 2 タイル同士のすき間(ピクセル) | | | def weight mosaic : | | | w = model 0 .weight.detach .numpy 形状は 中間層ノード数, 784 | | | size = n show 28 + n show - 1 gap | | | mosaic = np.full size, size , np.nan すき間はNaN(背景色)にする | | | for k in range n show n show : | | | r, c = divmod k, n show | | | top, left = r 28 + gap , c 28 + gap | | | mosaic top:top+28, left:left+28 = w k .reshape 28, 28 | | | return mosaic | | | cmap w = plt.get cmap 'RdBu r' .copy | | | cmap w.set bad ' fcfcfb' NaN部分(すき間)の色 | | | m = weight mosaic | | | vmax = np.nanmax np.abs m | | | im w = ax w.imshow m, cmap=cmap w, vmin=-vmax, vmax=vmax, interpolation='None' | | | --- パネル6:中間層がとらえた画像の特徴(2次元に圧縮して表示) --- | | | 検証用画像それぞれに対する中間層の出力(300個の数値)を、 | | | 主成分分析(PCA)で2次元に圧縮して散布図にする。 | | | 最初はバラバラだが、学習が進むと同じ数字どうしが集まってくる | | | ax pca = fig.add subplot gs 1, 1 | | | ax pca.set title '中間層がとらえた特徴の地図\n(同じ数字が集まれば学習成功)', size=14 | | | ax pca.set xticks | | | ax pca.set yticks | | | for spine in ax pca.spines.values : | | | spine.set color C GRID | | | 各画像を「数字の文字」そのもので描く(数字ごとに色も変える) | | | digit colors = plt.get cmap 'tab10' .colors | | | pca texts = ax pca.text 0, 0, str lbl , color=digit colors lbl , | | | fontsize=10, ha='center', va='center' | | | for lbl in t val | | | 主成分分析(PCA):300次元の中間層出力を、散らばりが最も大きい2方向に射影する | | | prev pc = None | | | def pca 2d hidden : | | | global prev pc | | | centered = hidden - hidden.mean axis=0 | | | , , vt = np.linalg.svd centered, full matrices=False | | | pc = vt :2 .copy | | | 前回の軸と向きを揃える(フレームごとに図が反転しないようにする) | | | if prev pc is not None: | | | for k in range 2 : | | | if np.dot pc k , prev pc k < 0: | | | pc k = -pc k | | | prev pc = pc | | | return centered @ pc.T | | | --- パネル7:検証データでの正解率の推移 --- | | | ax acc = fig.add subplot gs 1, 2: | | | ax acc.set title '正解率の推移(学習に使っていない画像' + str len val index + '枚で判定)', size=14 | | | ax acc.set xlabel '学習した枚数', color=C MUTED | | | ax acc.set ylim 0, 1 | | | ax acc.yaxis.set major formatter lambda v, pos: str int v 100 + '%' | | | line acc, = ax acc.plot , , color=C AQUA, linewidth=2 | | | style axis ax acc | | | 検証データを順伝播して、中間層の出力と正解率を求める(学習はしない) | | | def val forward : | | | with torch.no grad : | | | hidden = model :2 x val 入力層→中間層(シグモイドまで)の出力 | | | pred = model 2 hidden .argmax dim=1 .numpy | | | return hidden.numpy , pred == t val .mean | | | 画面を更新する頻度(全体でおよそ200回更新するように調整) | | | update every = max 1, no of trains // 200 | | | losses = | | | acc steps = | | | acc values = | | | ===== 学習のメインループ(1枚ずつ画像を学習する)===== | | | for n, x, t in enumerate zip x train, t train : | | | x = x.reshape 1, len x | | | --- 順伝播:入力から出力を計算する --- | | | z out = model x | | | --- 誤差の計算 --- | | | loss = loss fn z out, t.reshape 1 | | | losses.append loss.item | | | --- 誤差逆伝播と重みの更新 --- | | | optimizer.zero grad | | | loss.backward | | | optimizer.step | | | --- 画面の更新 --- | | | if n + 1 % update every == 0 or n == no of trains - 1: | | | パネル1:いまの学習画像 | | | im img.set data x.numpy .reshape 28, 28 | | | パネル2:いまの画像に対する中間層300ノードの反応 | | | with torch.no grad : | | | h cur = model :2 x .numpy .reshape 15, 20 | | | im hid.set data h cur | | | パネル3:確率分布と正解の位置 | | | probs = torch.softmax z out.detach , dim=1 .numpy 0 | | | for bar, p in zip bars, probs : | | | bar.set height p | | | marker.set xdata t.item | | | ax prob.set title '出力の確率分布 予測: ' + str int probs.argmax | | | + ' / 正解: ' + str t.item , size=14 | | | パネル4:損失の推移(薄い線 = 生の値、濃い線 = 移動平均) | | | steps = np.arange 1, len losses + 1 | | | line raw.set data steps, losses | | | window = min 50, len losses | | | avg = np.convolve losses, np.ones window / window, mode='valid' | | | line avg.set data steps window - 1: , avg | | | ax loss.set xlim 0, max 10, len losses | | | ax loss.set ylim 0, max losses 1.05 | | | パネル5:重みの画像 | | | m = weight mosaic | | | vmax = np.nanmax np.abs m | | | im w.set data m | | | im w.set clim -vmax, vmax | | | 検証データを順伝播(パネル6と7で使う) | | | hidden, acc = val forward | | | パネル6:中間層がとらえた特徴の地図(PCAで2次元に圧縮) | | | xy = pca 2d hidden | | | for txt, px, py in zip pca texts, xy : | | | txt.set position px, py | | | pad x = xy :, 0 .max - xy :, 0 .min 0.05 + 1e-6 | | | pad y = xy :, 1 .max - xy :, 1 .min 0.05 + 1e-6 | | | ax pca.set xlim xy :, 0 .min - pad x, xy :, 0 .max + pad x | | | ax pca.set ylim xy :, 1 .min - pad y, xy :, 1 .max + pad y | | | パネル7:正解率の推移 | | | acc steps.append n + 1 | | | acc values.append acc | | | line acc.set data acc steps, acc values | | | ax acc.set xlim 0, max 10, n + 1 | | | fig.suptitle '学習中… ' + str n + 1 + ' / ' + str no of trains + ' 枚 ' | | | + '正解率: ' + str int acc values -1 100 + '%', size=18 | | | plt.pause 0.001 | | | fig.suptitle '学習が終わりました(' + str no of trains + '枚) ' | | | + '正解率: ' + str int acc values -1 100 + '%', size=18 | | | plt.ioff | | | ===== テスト:学習済みネットワークで未知の画像10枚を判定する ===== | | | f = pd.read csv 'mnist test 10.csv', encoding='ms932', sep=',', header=None | | | f = f.values | | | x = np.asarray f :, 1: / 255.0 | | | correct label = f :, 0 | | | model.eval | | | with torch.no grad : | | | z in = model torch.tensor x, dtype=torch.float32 | | | z out = torch.softmax z in, dim=1 .numpy | | | テスト画像と判定結果(正解ラベル・予測値)を並べて表示する | | | fig2 = plt.figure figsize= 18, 8 , dpi=100, constrained layout=True | | | for i, xx, zz, c in enumerate zip x, z out, correct label : | | | image array = np.asarray xx .reshape 28, 28 | | | ax = fig2.add subplot 2, 5, i + 1 | | | ax.imshow image array, cmap='Greys', interpolation='None' | | | ax.axis "off" | | | label = np.argmax zz | | | ax.text | | | 0.4, 0.4, | | | 'Correct Label:' + str c + '\nPredict Value:' + str label , | | | size=20, | | | linespacing=2 | | | | | | plt.show |