0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

DeepLearning学習日誌9 (数値微分)2層ニューラルネットワークの実装2 MNISTデータの使用

0
Last updated at Posted at 2026-01-12

2層ニューラルネットワークの実装(数値微分)その2

MNISTデータを読み込みます。

Geminiとの対話をベースに、記事を構成しています。

参考書:
ゼロから作るDeepLearning Pythonで学ぶディープラーニングの理論と実践 斎藤康毅 著

開発環境:
VScode + 拡張機能Python(microsoft) + anaconda(統計処理、参考書の推薦ライブラリ)

この記事は、ゼロから作るDeep Learning 第4章の学習記録と、補足知識の記録になります。

MNISTとは

・0から9までの手書き数字の画像。
・画像解像度: $28 \times 28$ ピクセル(合計784画素)。
・色階調: グレースケール(0〜255の1バイトデータ)。0が背景(黒)、255が文字(白)を表します。
・データ構成:訓練データ: 60,000枚(学習用)テストデータ: 10,000枚(評価用)
・ファイル形式: IDXフォーマット(バイナリ形式)。先頭数バイトにマジックナンバーやデータ数などのヘッダー情報が含まれています。

1. MNISTのファイル構造

(IDXフォーマット)MNISTは、IDXフォーマットと呼ばれる独自のバイナリ形式を採用しています。ファイルの中身は大きく「ヘッダー」と「データ本体」に分かれています。
画像ファイル (train-images-idx3-ubyte.gz) の中身

オフセット 項目 内容
0000 マジックナンバー 2051
0004 画像枚数60,0000008 行数 (Rows)
0012 列数(Cols) 28
0016 〜ピクセルデータ ここから先、0〜255の数値が延々と並ぶ

2. 0と1ではなく「0〜255の濃淡」

「0と1」で構成されているのかという疑問に対する答えは、**「いいえ、0〜255の256段階のグレースケールです」**となります。
0: 真っ黒(背景)
255: 真っ白(文字の最も濃い部分)
1〜254: 中間色(文字の縁などの滑らかな部分)
もしこれが「0と1(白か黒か)」の2値データだったら、文字の縁がガタガタになり、ニューラルネットワークが特徴を捉えるのが難しくなります。この「絶妙な中間色(グラデーション)」があるからこそ、高い精度で識別が可能になります。

3. ラベルファイル (train-labels-idx1-ubyte.gz) の中身

ラベル(正解)ファイルはもっとシンプルです。
ヘッダー(8バイト): マジックナンバー(2049)とデータ数(60,000)
データ本体: 0x05, 0x00, 0x04, 0x01... のように、0から9の数値が1バイトずつ並んでいます。

4. プログラムから見た「データの姿」

私たちが load_mnist(normalize=True) を呼び出したとき、プログラム内部では以下のような変換が起きています。

バイナリを読み込む: 00 00 00 FF ... というバイト列。

NumPy配列化: [0, 0, 0, 255, ...] という 0〜255 の整数配列。

正規化 (normalize): 255で割り、[0.0, 0.0, 0.0, 1.0, ...] という 0.0〜1.0 の浮動小数点数へ変換。

質問MNIST12_12.png
gemini解答:
なぜ書籍では $28 \times 28$ を選んでいるのか
汎用性の確保: $12 \times 12$ だと、少し斜めになったり太さが変わったりするだけで別の数字に見えてしまい、「遊び(余裕)」が少なくなります。業界標準への挑戦: MNIST($28 \times 28$)は深層学習の世界の「Hello World」です。あえて計算の重い標準データセットに挑むことで、「アルゴリズムによる高速化(第5章)」の圧倒的な恩恵を読者に伝えるという教育的な意図があります。

プロジェクト構成

myProject/
├── dataset/ # データセット読み込み用のフォルダ
│ ├── mnist.py # MNISTデータのダウンロード・整形処理
│ └── ... # (取得したデータファイルなど)
├── common/ # プロジェクト全体で再利用する共通モジュール
│ ├── myFunctions.py # 活性化関数(sigmoid, softmax)や損失関数など
│ ├── myGradient.py # 数値微分(numerical_gradient)の関数
│ ├── layers.py # 第5章以降で使用する各レイヤのクラス定義
│ └── util.py # その他便利なユーティリティ関数
├── main/ # 【実行用フォルダ】
│ ├── myTwo_layer_net.py # 2層ニューラルネットワークのクラス定義
│ ├── myTrain_neuralnet.py # 学習を実行するメインスクリプト
│ └── predict_test.py # 推論をテストするためのスクリプト
└── README.md # プロジェクトの概要や実行方法のメモ

dataset/mnist.py
python dataset/mnist.py
# coding: utf-8
try:
    import urllib.request
except ImportError:
    raise ImportError('You should use Python 3.x')
import os.path
import gzip
import pickle
import os
import numpy as np


#url_base = 'http://yann.lecun.com/exdb/mnist/'
url_base = 'https://ossci-datasets.s3.amazonaws.com/mnist/'  # mirror site

key_file = {
    'train_img':'train-images-idx3-ubyte.gz',
    'train_label':'train-labels-idx1-ubyte.gz',
    'test_img':'t10k-images-idx3-ubyte.gz',
    'test_label':'t10k-labels-idx1-ubyte.gz'
}

dataset_dir = os.path.dirname(os.path.abspath(__file__))
save_file = dataset_dir + "/mnist.pkl"

train_num = 60000
test_num = 10000
img_dim = (1, 28, 28)
img_size = 784


def _download(file_name):
    file_path = dataset_dir + "/" + file_name

    if os.path.exists(file_path):
        return

    print("Downloading " + file_name + " ... ")
    headers = {"User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:47.0) Gecko/20100101 Firefox/47.0"}
    request = urllib.request.Request(url_base+file_name, headers=headers)
    response = urllib.request.urlopen(request).read()
    with open(file_path, mode='wb') as f:
        f.write(response)
    print("Done")

def download_mnist():
    for v in key_file.values():
       _download(v)

def _load_label(file_name):
    file_path = dataset_dir + "/" + file_name

    print("Converting " + file_name + " to NumPy Array ...")
    with gzip.open(file_path, 'rb') as f:
            labels = np.frombuffer(f.read(), np.uint8, offset=8)
    print("Done")

    return labels

def _load_img(file_name):
    file_path = dataset_dir + "/" + file_name

    print("Converting " + file_name + " to NumPy Array ...")
    with gzip.open(file_path, 'rb') as f:
            data = np.frombuffer(f.read(), np.uint8, offset=16)
    data = data.reshape(-1, img_size)
    print("Done")

    return data

def _convert_numpy():
    dataset = {}
    dataset['train_img'] =  _load_img(key_file['train_img'])
    dataset['train_label'] = _load_label(key_file['train_label'])
    dataset['test_img'] = _load_img(key_file['test_img'])
    dataset['test_label'] = _load_label(key_file['test_label'])

    return dataset

def init_mnist():
    download_mnist()
    dataset = _convert_numpy()
    print("Creating pickle file ...")
    with open(save_file, 'wb') as f:
        pickle.dump(dataset, f, -1)
    print("Done!")

def _change_one_hot_label(X):
    T = np.zeros((X.size, 10))
    for idx, row in enumerate(T):
        row[X[idx]] = 1

    return T


def load_mnist(normalize=True, flatten=True, one_hot_label=False):
    """MNISTデータセットの読み込み

    Parameters
    ----------
    normalize : 画像のピクセル値を0.0~1.0に正規化する
    one_hot_label :
        one_hot_labelがTrueの場合、ラベルはone-hot配列として返す
        one-hot配列とは、たとえば[0,0,1,0,0,0,0,0,0,0]のような配列
    flatten : 画像を一次元配列に平にするかどうか

    Returns
    -------
    (訓練画像, 訓練ラベル), (テスト画像, テストラベル)
    """
    if not os.path.exists(save_file):
        init_mnist()

    with open(save_file, 'rb') as f:
        dataset = pickle.load(f)

    if normalize:
        for key in ('train_img', 'test_img'):
            dataset[key] = dataset[key].astype(np.float32)
            dataset[key] /= 255.0

    if one_hot_label:
        dataset['train_label'] = _change_one_hot_label(dataset['train_label'])
        dataset['test_label'] = _change_one_hot_label(dataset['test_label'])

    if not flatten:
         for key in ('train_img', 'test_img'):
            dataset[key] = dataset[key].reshape(-1, 1, 28, 28)

    return (dataset['train_img'], dataset['train_label']), (dataset['test_img'], dataset['test_label'])


if __name__ == '__main__':
    init_mnist()
common/myFunctions.py
python common/myFunctions.py
import numpy as np

def sigmoid(x):
    return 1/(1 + np.exp(-x))

#rectified linear unit正規化線形関数
def relu(x):
    return np.maximum(0,x)
    #2つの配列(または配列とスカラ値)を比較し、
    # 大きい方の数値を取って新しい配列を作ります。

#ソフトマックス
def softmax(x):
    if x.ndim == 2: #2次元ならば、
        x = x.T #行列を転置
        x = x - np.max(x, axis = 0) #すべての値<=0
        y = np.exp(x)/np.sum(np.exp(x),axis = 0)
        return y.T
    x = x - np.max(x)
    return np.exp(x)/np.sum(np.exp(x))

#交差エントロピー誤差 
def cross_entropy_error(y,t):
    if y.ndim == 1:
        t = t.reshape(1,t.size)
        y = y.reshape(1,y.size)
    if t.size == y.size:
        t = t.argmax(axis=1)
    batch_size = y.shape[0]
    print("cce:",batch_size)
    return -np.sum(np.log(y[np.arange(batch_size),t]+1e-7))/batch_size


common/myGradient.py
python common/myGradient.py
import numpy as np

def numerical_gradient(f,x):
    h = 1e-4
    grad = np.zeros_like(x)

    it = np.nditer(x,flags=['multi_index'], op_flags=['readwrite'])

    while not it.finished:
        idx = it.multi_index
        tmp_val = x[idx]
        x[idx] = tmp_val + h
        fxh1 = f(x)
        x[idx] = tmp_val - h
        fxh2 = f(x)
        grad[idx] = (fxh1-fxh2)/(2*h)
        x[idx] = tmp_val
        it.iternext()
    return grad
main/two_layer_net.py
python main/two_layer_net.py
import sys,os
sys.path.append(os.pardir)
from common.myFunctions import *
from common.myGradient import numerical_gradient
import numpy as np

#self は、一言で言うと 「生成されたインスタンス(自分自身)を指すラベル」 です。
class TwoLayerNet:
    def __init__(self, input_size, hidden_size, output_size, weight_init_sdt=0.01):
        self.params = {}
        self.params['W1']=weight_init_sdt*np.random.rand(input_size,hidden_size)
        self.params['b1']=np.zeros(hidden_size)
        self.params['W2']=weight_init_sdt*np.random.rand(hidden_size,output_size)
        self.params['b2']=np.zeros(output_size)

#タプル・アンパック
#この書き方の背景には、Pythonの「タプル」という概念があります。
#パッキング: 右辺の self.params['W1'], self.params['W2'] は、暗黙的に一つの**タプル(データのセット)**としてまとめられます。
#アンパック: そのセットが、左辺の W1, W2 という2つの変数に「荷ほどき(アンパック)」されて代入されます。
    def predict(self, x):
        W1,W2 = self.params['W1'], self.params['W2']
        b1,b2 = self.params['b1'], self.params['b2']
        
        a1 = np.dot(x,W1) + b1
        z1 = sigmoid(a1)
        a2 = np.dot(z1,W2) + b2
        y = softmax(a2)
        return y
    
    def loss(self, x, t):
        y = self.predict(x)
        return cross_entropy_error(y,t)
    
    def numerical_gradient(self, x, t):
        loss_W = lambda W: self.loss(x,t)

        grads = {}
        grads['W1'] = numerical_gradient(loss_W, self.params['W1'])
        grads['b1'] = numerical_gradient(loss_W, self.params['b1'])
        grads['W2'] = numerical_gradient(loss_W, self.params['W2'])
        grads['b2'] = numerical_gradient(loss_W, self.params['b2'])
        return grads
    
    def accuracy(self, x, t):
        #1.まず予測を行う
        y = self.predict(x)
        #2.各行(データごと)で、最大値のインデックス(予測した数字)を取得
        y = np.argmax(y, axis = 1)
        #3.正解ラベルもインデックス形式に変換(One-Hotラベルの場合)
        if t.ndim !=1:
            t = np.argmax(t, axis=1)
        #4.予測と正解が一致している割合を計算
        accuracy = np.sum(y==t)/ float(x.shape[0])
        return accuracy
    
    
main/myTrain_neuralnet.py
python main/myTrain_neuralnet.py
import sys, os
sys.path.append(os.pardir)
import numpy as np
import matplotlib.pyplot as plt

from myTwo_layer_net import TwoLayerNet
try:
    from dataset.mnist import load_mnist
    print("成功")
except ImportError as e:
    print(f"失敗:インポートエラーが発生しました。\n現在の検索パス: {sys.path}")
    print(f"現在の作業ディレクトリ: {os.getcwd()}")


np.set_printoptions(linewidth = 200)

testA = False
if testA:
    train_size = 100 #データ数
    input_size = 100 #784入力ノード数
    hidden_size = 10 #隠れ層のニューロン数
    output_size = 10

np.random.seed(42)#再現性のため固定

if testA:
    #入力データ:正規分布で生成
    x_train = np.random.randn(train_size, input_size)
    #正解ラベル:one-hot表現をランダム生成
    t_train = np.zeros((train_size, output_size))
    for i in range(train_size):
        rand_label = np.random.randint(0,output_size)#最小値、最大値
        t_train[i,rand_label] = 1

testMNIST = True
if testMNIST:
    (x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label= True)

    #読み込んだデータの確認
    print(f"訓練データの形状:{x_train.shape}")
    print(f"訓練データの形状:{t_train.shape}")


if testMNIST:
    train_size = x_train.shape[0] #データ数 60000
    input_size = 28*28 #784入力ノード数
    hidden_size = 50 #隠れ層のニューロン数
    output_size = 10

#sys.exit() #実行終了

# --- ネットワークの初期化 ---
net = TwoLayerNet(input_size=input_size, hidden_size = hidden_size, output_size = output_size)

# --- ハイパーパラメータ ---
iters_num = 100 #繰り返し回数(処理が重いので、少なめに設定)
batch_size = 100  #ミニバッチサイズ
learning_rate = 0.1 #学習率

train_loss_list = []
print("学習開始(数値微分のため時間がかかります...)")

for i in range(iters_num):
    #1.ミニバッチの取得
    #第一引数に整数 n を渡すと、range(n) (0からn-1まで)の中から選んでくれます。
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_mask]
    t_batch = t_train[batch_mask]

    print("i:",i)

    #2.勾配の計算
    grad = net.numerical_gradient(x_batch,t_batch)
    
    #3.パラメータの更新
    for key in ('W1', 'b1', 'W2', 'b2'):
        net.params[key] -= learning_rate * grad[key]

    #4.学習経過の記録
    """if(i+1) % 10==0:
        current_softmax = net.predict(x_batch)
        print("current_softmax:\n",current_softmax)"""
    
    loss = net.loss(x_batch, t_batch)
    train_loss_list.append(loss)
    print(f"Iteration {i+1}: Loss = {loss:.4f}")

print("-"*30)
print("学習終了")

#print(t_batch)
#newA = current_softmax*t_batch
#print(newA)

# 1. 訓練データでの精度(「練習問題」がどれくらい解けるようになったか)
train_acc = net.accuracy(x_train, t_train)

# 2. テストデータでの精度(「初めて見る問題」がどれくらい解けるか)
test_acc = net.accuracy(x_test, t_test)

print(f"訓練データの正解率: {train_acc * 100:.2f}%")
print(f"テストデータの正解率: {test_acc * 100:.2f}%")

# グラフの描画
x = np.arange(len(train_loss_list))
plt.plot(x,train_loss_list)
plt.xlabel("iterations")
plt.ylabel("loss")
plt.title("Training Loss Curve")
plt.show()

実行してみます。

MNISデータ実行における、イテレーションごとの損失値を記録しました。
MNISTtest1.png
出力は、10ノードあるので、学習前の確率平均は、0.1。交差エントロピー-ln(0.1) = 2.30なので、初期の値から、組み込んだ計算が合っている可能性が高いとわかります。
そして、イテレーションの回数が進むと、グラフは徐々に、2.3から、下降しています。すこしずつ学習が進んでいる様子がうかがえます。

精度(Accuracy)を計測

python 精度の計算.py

# 1. 訓練データでの精度(「練習問題」がどれくらい解けるようになったか)
train_acc = net.accuracy(x_train, t_train)

# 2. テストデータでの精度(「初めて見る問題」がどれくらい解けるか)
test_acc = net.accuracy(x_test, t_test)

print(f"訓練データの正解率: {train_acc * 100:.2f}%")
print(f"テストデータの正解率: {test_acc * 100:.2f}%")

イテレーション回数が完了したら、変化した重みと、バイアスをもとに、正解確率を求めます。
訓練、テストともに、おなじような値なら、学習が進んでいることになります。

イテレーション100回の結果で計算してみると、
訓練データの正解率: 11.24%
テストデータの正解率: 11.35% 
学習前の理論値、確率平均は、10%なので、1%ぐらい確率が上がっています。
同じぐらいだから問題集の答えを暗記しただけ(過学習 / Overfitting)ではない可能性が高いです。

第5章 誤差逆伝播法へ

これで、はっきりわかります。数値微分では、とてもじゃないが、計算量が多すぎると。
イテレーション100回で、20分かかりました。

Pythonは遅いと言われるが、実はロジックの司令塔が1コア制限(GIL) を受けているだけで、NumPyを通じた行列計算ではGILを超えて、すべてのコアを使って計算されています。

それでも、数値微分で、勾配を求めるのは、実用的でないと、はっきりわかったので、第5章 誤差逆伝播法を学ぶことになります。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?