{"slug": "visualizing-mnist-training-with-deep-neural-networks", "title": "Visualizing MNIST Training with Deep Neural Networks", "summary": "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.", "body_md": "| import numpy as np | |\n| import matplotlib.pyplot as plt | |\n| import pandas as pd | |\n| import matplotlib_fontja | |\n| import torch | |\n| import torch.nn as nn | |\n| # グラフの配色（見分けやすさを検証済みのパレット） | |\n| C_BLUE = '#2a78d6' # 損失・確率バーの色 | |\n| C_AQUA = '#1baf7a' # 正解率の色 | |\n| C_INK = '#0b0b0b' # 文字色 | |\n| C_MUTED = '#898781' # 軸・補助的な文字の色 | |\n| C_GRID = '#e1e0d9' # グリッド線の色 | |\n| # ニューラルネットワークの構造を定義 | |\n| i_nodes = 784 # 入力層のノード数（28×28ピクセル = 784） | |\n| h_nodes = 300 # 中間層（隠れ層）のノード数 | |\n| o_nodes = 10 # 出力層のノード数（数字0〜9の10種類） | |\n| # MNIST（手書き数字）の学習用データを読み込む（1行 = 正解ラベル + 784ピクセルの値） | |\n| f = pd.read_csv('mnist_train_10000.csv', encoding='ms932', sep=',', header=None) | |\n| data = f.values | |\n| # データの並び順をランダムにシャッフルする | |\n| index = np.arange(len(f)) | |\n| np.random.shuffle(index) | |\n| # 学習に使うデータ数をユーザーに入力してもらう | |\n| no_of_trains = int(input('何回学習しますか？')) | |\n| train_index = index[0:no_of_trains] | |\n| # 学習に使わなかったデータから200件を「検証用」に取っておく | |\n| # （学習中に正解率を測るためのデータ。学習には使わない） | |\n| val_index = index[no_of_trains:no_of_trains + 200] | |\n| if len(val_index) == 0: | |\n| val_index = index[0:200] | |\n| # 入力データ：ピクセル値（0〜255）を255で割って0〜1の範囲に正規化する | |\n| inpt = np.asarray(data[train_index, 1:]) / 255.0 | |\n| # ===== PyTorchでニューラルネットワークを定義する ===== | |\n| model = nn.Sequential( | |\n| nn.Linear(i_nodes, h_nodes, bias=False), # 入力層→中間層の重み | |\n| nn.Sigmoid(), # シグモイド関数で中間層の出力を計算 | |\n| nn.Linear(h_nodes, o_nodes, bias=False), # 中間層→出力層の重み | |\n| ) | |\n| # 損失関数：交差エントロピー（内部でソフトマックス関数を適用して誤差を計算する） | |\n| loss_fn = nn.CrossEntropyLoss() | |\n| # 最適化手法：勾配降下法（SGD） | |\n| learningRate = 0.1 | |\n| optimizer = torch.optim.SGD(model.parameters(), lr=learningRate) | |\n| # 学習データをPyTorchのテンソル形式に変換する | |\n| x_train = torch.tensor(inpt, dtype=torch.float32) | |\n| t_train = torch.tensor(data[train_index, 0].astype(np.int64)) | |\n| x_val = torch.tensor(np.asarray(data[val_index, 1:]) / 255.0, dtype=torch.float32) | |\n| t_val = data[val_index, 0].astype(np.int64) | |\n| # ===== 学習の様子を映す画面（7つのパネル）を準備する ===== | |\n| plt.ion() | |\n| fig = plt.figure(figsize=(19, 9), dpi=100, constrained_layout=True) | |\n| gs = fig.add_gridspec(2, 4) | |\n| # 折れ線グラフのパネルの見た目を整える共通処理 | |\n| def style_axis(ax): | |\n| ax.grid(True, color=C_GRID, linewidth=0.8) | |\n| ax.tick_params(colors=C_MUTED, labelsize=10) | |\n| for spine in ax.spines.values(): | |\n| spine.set_color(C_GRID) | |\n| # --- パネル1：いま学習している画像 --- | |\n| ax_img = fig.add_subplot(gs[0, 0]) | |\n| ax_img.set_title('いま学習している画像', size=14) | |\n| ax_img.axis('off') | |\n| im_img = ax_img.imshow(np.zeros((28, 28)), cmap='Greys', vmin=0, vmax=1, interpolation='None') | |\n| # --- パネル2：中間層の反応（いまの画像に対する300ノードの出力） --- | |\n| # シグモイド関数の出力（0〜1）を15×20のマス目にして表示する。 | |\n| # 画像ごとに反応するノードの組み合わせが変わる様子が見える | |\n| ax_hid = fig.add_subplot(gs[0, 1]) | |\n| ax_hid.set_title('中間層の反応（300ノード）\\n濃い = 強く反応', size=14) | |\n| ax_hid.axis('off') | |\n| im_hid = ax_hid.imshow(np.zeros((15, 20)), cmap='Blues', vmin=0, vmax=1, interpolation='None') | |\n| # --- パネル3：出力層の確率分布（10個の数字それぞれの「自信」） --- | |\n| ax_prob = fig.add_subplot(gs[0, 2]) | |\n| ax_prob.set_title('出力の確率分布', size=14) | |\n| bars = ax_prob.bar(np.arange(o_nodes), np.zeros(o_nodes), color=C_BLUE) | |\n| ax_prob.set_ylim(0, 1) | |\n| ax_prob.set_xticks(np.arange(o_nodes)) | |\n| ax_prob.set_xlabel('数字（▲ = 正解）', color=C_MUTED) | |\n| style_axis(ax_prob) | |\n| # 正解の位置を示す ▲ マーカー | |\n| marker, = ax_prob.plot([0], [-0.06], marker='^', markersize=12, | |\n| color=C_INK, clip_on=False, linestyle='None') | |\n| # --- パネル4：損失（誤差）の推移 --- | |\n| ax_loss = fig.add_subplot(gs[0, 3]) | |\n| ax_loss.set_title('損失（誤差）の推移', size=14) | |\n| ax_loss.set_xlabel('学習した枚数', color=C_MUTED) | |\n| line_raw, = ax_loss.plot([], [], color=C_MUTED, linewidth=0.8, alpha=0.4, label='1枚ごとの損失') | |\n| line_avg, = ax_loss.plot([], [], color=C_BLUE, linewidth=2, label='移動平均（50枚）') | |\n| ax_loss.legend(loc='upper right', frameon=False, fontsize=10) | |\n| style_axis(ax_loss) | |\n| # --- パネル5：入力層→中間層の重みの変化 --- | |\n| # 中間層の16個のノードについて、784個の重みを28×28の画像にして並べる。 | |\n| # 学習が進むと、ランダムなノイズから数字の「部品」のようなパターンが浮かび上がってくる | |\n| ax_w = fig.add_subplot(gs[1, 0]) | |\n| ax_w.set_title('入力層→中間層の重み（16ノード分）\\n青 = マイナス / 赤 = プラス', size=14) | |\n| ax_w.axis('off') | |\n| n_show = 4 # 4×4 = 16ノード分を表示 | |\n| gap = 2 # タイル同士のすき間（ピクセル） | |\n| def weight_mosaic(): | |\n| w = model[0].weight.detach().numpy() # 形状は (中間層ノード数, 784) | |\n| size = n_show * 28 + (n_show - 1) * gap | |\n| mosaic = np.full((size, size), np.nan) # すき間はNaN（背景色）にする | |\n| for k in range(n_show * n_show): | |\n| r, c = divmod(k, n_show) | |\n| top, left = r * (28 + gap), c * (28 + gap) | |\n| mosaic[top:top+28, left:left+28] = w[k].reshape(28, 28) | |\n| return mosaic | |\n| cmap_w = plt.get_cmap('RdBu_r').copy() | |\n| cmap_w.set_bad('#fcfcfb') # NaN部分（すき間）の色 | |\n| m = weight_mosaic() | |\n| vmax = np.nanmax(np.abs(m)) | |\n| im_w = ax_w.imshow(m, cmap=cmap_w, vmin=-vmax, vmax=vmax, interpolation='None') | |\n| # --- パネル6：中間層がとらえた画像の特徴（2次元に圧縮して表示） --- | |\n| # 検証用画像それぞれに対する中間層の出力（300個の数値）を、 | |\n| # 主成分分析（PCA）で2次元に圧縮して散布図にする。 | |\n| # 最初はバラバラだが、学習が進むと同じ数字どうしが集まってくる | |\n| ax_pca = fig.add_subplot(gs[1, 1]) | |\n| ax_pca.set_title('中間層がとらえた特徴の地図\\n（同じ数字が集まれば学習成功）', size=14) | |\n| ax_pca.set_xticks([]) | |\n| ax_pca.set_yticks([]) | |\n| for spine in ax_pca.spines.values(): | |\n| spine.set_color(C_GRID) | |\n| # 各画像を「数字の文字」そのもので描く（数字ごとに色も変える） | |\n| digit_colors = plt.get_cmap('tab10').colors | |\n| pca_texts = [ax_pca.text(0, 0, str(lbl), color=digit_colors[lbl], | |\n| fontsize=10, ha='center', va='center') | |\n| for lbl in t_val] | |\n| # 主成分分析（PCA）：300次元の中間層出力を、散らばりが最も大きい2方向に射影する | |\n| prev_pc = None | |\n| def pca_2d(hidden): | |\n| global prev_pc | |\n| centered = hidden - hidden.mean(axis=0) | |\n| _, _, vt = np.linalg.svd(centered, full_matrices=False) | |\n| pc = vt[:2].copy() | |\n| # 前回の軸と向きを揃える（フレームごとに図が反転しないようにする） | |\n| if prev_pc is not None: | |\n| for k in range(2): | |\n| if np.dot(pc[k], prev_pc[k]) < 0: | |\n| pc[k] = -pc[k] | |\n| prev_pc = pc | |\n| return centered @ pc.T | |\n| # --- パネル7：検証データでの正解率の推移 --- | |\n| ax_acc = fig.add_subplot(gs[1, 2:]) | |\n| ax_acc.set_title('正解率の推移（学習に使っていない画像' + str(len(val_index)) + '枚で判定）', size=14) | |\n| ax_acc.set_xlabel('学習した枚数', color=C_MUTED) | |\n| ax_acc.set_ylim(0, 1) | |\n| ax_acc.yaxis.set_major_formatter(lambda v, pos: str(int(v * 100)) + '%') | |\n| line_acc, = ax_acc.plot([], [], color=C_AQUA, linewidth=2) | |\n| style_axis(ax_acc) | |\n| # 検証データを順伝播して、中間層の出力と正解率を求める（学習はしない） | |\n| def val_forward(): | |\n| with torch.no_grad(): | |\n| hidden = model[:2](x_val) # 入力層→中間層（シグモイドまで）の出力 | |\n| pred = model[2](hidden).argmax(dim=1).numpy() | |\n| return hidden.numpy(), (pred == t_val).mean() | |\n| # 画面を更新する頻度（全体でおよそ200回更新するように調整） | |\n| update_every = max(1, no_of_trains // 200) | |\n| losses = [] | |\n| acc_steps = [] | |\n| acc_values = [] | |\n| # ===== 学習のメインループ（1枚ずつ画像を学習する）===== | |\n| for n, (x, t) in enumerate(zip(x_train, t_train)): | |\n| x = x.reshape(1, len(x)) | |\n| # --- 順伝播：入力から出力を計算する --- | |\n| z_out = model(x) | |\n| # --- 誤差の計算 --- | |\n| loss = loss_fn(z_out, t.reshape(1)) | |\n| losses.append(loss.item()) | |\n| # --- 誤差逆伝播と重みの更新 --- | |\n| optimizer.zero_grad() | |\n| loss.backward() | |\n| optimizer.step() | |\n| # --- 画面の更新 --- | |\n| if (n + 1) % update_every == 0 or n == no_of_trains - 1: | |\n| # パネル1：いまの学習画像 | |\n| im_img.set_data(x.numpy().reshape(28, 28)) | |\n| # パネル2：いまの画像に対する中間層300ノードの反応 | |\n| with torch.no_grad(): | |\n| h_cur = model[:2](x).numpy().reshape(15, 20) | |\n| im_hid.set_data(h_cur) | |\n| # パネル3：確率分布と正解の位置 | |\n| probs = torch.softmax(z_out.detach(), dim=1).numpy()[0] | |\n| for bar, p in zip(bars, probs): | |\n| bar.set_height(p) | |\n| marker.set_xdata([t.item()]) | |\n| ax_prob.set_title('出力の確率分布 予測: ' + str(int(probs.argmax())) | |\n| + ' / 正解: ' + str(t.item()), size=14) | |\n| # パネル4：損失の推移（薄い線 = 生の値、濃い線 = 移動平均） | |\n| steps = np.arange(1, len(losses) + 1) | |\n| line_raw.set_data(steps, losses) | |\n| window = min(50, len(losses)) | |\n| avg = np.convolve(losses, np.ones(window) / window, mode='valid') | |\n| line_avg.set_data(steps[window - 1:], avg) | |\n| ax_loss.set_xlim(0, max(10, len(losses))) | |\n| ax_loss.set_ylim(0, max(losses) * 1.05) | |\n| # パネル5：重みの画像 | |\n| m = weight_mosaic() | |\n| vmax = np.nanmax(np.abs(m)) | |\n| im_w.set_data(m) | |\n| im_w.set_clim(-vmax, vmax) | |\n| # 検証データを順伝播（パネル6と7で使う） | |\n| hidden, acc = val_forward() | |\n| # パネル6：中間層がとらえた特徴の地図（PCAで2次元に圧縮） | |\n| xy = pca_2d(hidden) | |\n| for txt, (px, py) in zip(pca_texts, xy): | |\n| txt.set_position((px, py)) | |\n| pad_x = (xy[:, 0].max() - xy[:, 0].min()) * 0.05 + 1e-6 | |\n| pad_y = (xy[:, 1].max() - xy[:, 1].min()) * 0.05 + 1e-6 | |\n| ax_pca.set_xlim(xy[:, 0].min() - pad_x, xy[:, 0].max() + pad_x) | |\n| ax_pca.set_ylim(xy[:, 1].min() - pad_y, xy[:, 1].max() + pad_y) | |\n| # パネル7：正解率の推移 | |\n| acc_steps.append(n + 1) | |\n| acc_values.append(acc) | |\n| line_acc.set_data(acc_steps, acc_values) | |\n| ax_acc.set_xlim(0, max(10, n + 1)) | |\n| fig.suptitle('学習中… ' + str(n + 1) + ' / ' + str(no_of_trains) + ' 枚 ' | |\n| + '正解率: ' + str(int(acc_values[-1] * 100)) + '%', size=18) | |\n| plt.pause(0.001) | |\n| fig.suptitle('学習が終わりました（' + str(no_of_trains) + '枚） ' | |\n| + '正解率: ' + str(int(acc_values[-1] * 100)) + '%', size=18) | |\n| plt.ioff() | |\n| # ===== テスト：学習済みネットワークで未知の画像10枚を判定する ===== | |\n| f = pd.read_csv('mnist_test_10.csv', encoding='ms932', sep=',', header=None) | |\n| f = f.values | |\n| x = np.asarray(f[:, 1:]) / 255.0 | |\n| correct_label = f[:, 0] | |\n| model.eval() | |\n| with torch.no_grad(): | |\n| z_in = model(torch.tensor(x, dtype=torch.float32)) | |\n| z_out = torch.softmax(z_in, dim=1).numpy() | |\n| # テスト画像と判定結果（正解ラベル・予測値）を並べて表示する | |\n| fig2 = plt.figure(figsize=(18, 8), dpi=100, constrained_layout=True) | |\n| for i, (xx, zz, c) in enumerate(zip(x, z_out, correct_label)): | |\n| image_array = np.asarray(xx).reshape((28, 28)) | |\n| ax = fig2.add_subplot(2, 5, i + 1) | |\n| ax.imshow(image_array, cmap='Greys', interpolation='None') | |\n| ax.axis(\"off\") | |\n| label = np.argmax(zz) | |\n| ax.text( | |\n| 0.4, 0.4, | |\n| 'Correct Label:' + str(c) + '\\nPredict Value:' + str(label), | |\n| size=20, | |\n| linespacing=2 | |\n| ) | |\n| plt.show() |", "url": "https://wpnews.pro/news/visualizing-mnist-training-with-deep-neural-networks", "canonical_source": "https://gist.github.com/tado/381b77fe7aa1606d01c78cc812c7a03c", "published_at": "2026-07-10 01:10:32+00:00", "updated_at": "2026-07-10 03:05:11.402823+00:00", "lang": "en", "topics": ["machine-learning", "neural-networks", "developer-tools"], "entities": ["PyTorch", "MNIST"], "alternates": {"html": "https://wpnews.pro/news/visualizing-mnist-training-with-deep-neural-networks", "markdown": "https://wpnews.pro/news/visualizing-mnist-training-with-deep-neural-networks.md", "text": "https://wpnews.pro/news/visualizing-mnist-training-with-deep-neural-networks.txt", "jsonld": "https://wpnews.pro/news/visualizing-mnist-training-with-deep-neural-networks.jsonld"}}