# Visualizing MNIST Training with Deep Neural Networks

> Source: <https://gist.github.com/tado/381b77fe7aa1606d01c78cc812c7a03c>
> Published: 2026-07-10 01:10:32+00:00

| 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() |
