Handwritten digit recognition (0-9) based on the MNIST dataset.: From Numerical Gradient to Backpropagation

一、项目简介 | Project Overview

本项目实现 MNIST 手写数字分类任务(0–9)。
This project implements MNIST handwritten digit classification (0–9).

我们构建了两个结构完全相同的神经网络模型。
We build two neural networks with identical architectures.

模型A:使用数值梯度(Numerical Gradient)
Model A: uses numerical gradient

模型B:使用反向传播(Backpropagation)
Model B: uses backpropagation

核心目标:对比两种梯度计算方式的效率与原理差异。
Core goal: compare efficiency and principles of two gradient computation methods.

二、模型结构 | Model Architecture

2.1 网络结构 | Network Structure

本模型是一个两层前馈神经网络。
This model is a two-layer feedforward neural network.

输入层:784维(28×28图像)
Input layer: 784 dimensions (28×28 image)

隐藏层:50个神经元
Hidden layer: 50 neurons

输出层:10个类别
Output layer: 10 classes

2.2 前向传播 | Forward Propagation

def forward(self, X):
    W1, W2, b1, b2 = self.params["W1"], self.params["W2"], self.params["b1"], self.params["b2"]

    a1 = X @ W1 + b1
    z1 = sigmoid(a1)

    a2 = z1 @ W2 + b2
    y = softmax(a2)

    return y

第一步进行线性变换。
Step 1 performs linear transformation.

第二步加入非线性激活函数。
Step 2 applies non-linear activation.

第三步映射到输出空间。
Step 3 maps to output space.

第四步转为概率分布。
Step 4 converts outputs into probability distribution.

三、交叉熵损失 | Cross Entropy Loss

L = - \sum_{i=1}^{C} t_i \log(y_i)

真实标签 t 表示正确类别。
True label t represents correct class.

预测 y 表示模型输出概率。
Prediction y represents model probability.

#交叉熵损失
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)
    #利用列表索引,计算损失
    n=y.shape[0]
    return -np.sum(np.log(y[np.arange(n),t]+1e-7))/n

四、方法一:数值梯度 | Method 1: Numerical Gradient

4.1 核心代码 | Core Code

def gradient_numerical(self, X, t):
    loss_f = lambda _: self.loss(X, t)

    grads = {}
    for key in self.params.keys():
        grads[key] = numerical_gradient(loss_f, self.params[key])

    return grads

4.2 原理解释 | Principle

数值梯度通过“扰动参数”计算导数。
Numerical gradient computes derivatives by perturbing parameters.

#利用数值微分计算梯度向量
def numerical_gradient(f,x):
    h=1e-4
   #定义要计算反馈的梯度向量,形状与x相同
    grad=np.zeros_like(x)
    # 使用 nditer 遍历多维数组的每个元素
    it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
    while not it.finished:
        idx = it.multi_index  # 获取当前元素的多维索引
        tmp = x[idx]  # 保存原值
        # f(x+h)
        x[idx] = tmp + h
        fxh1 = f(x)
        # f(x-h)
        x[idx] = tmp - h
        fxh2 = f(x)
        # 中心差分计算偏导数
        grad[idx] = (fxh1 - fxh2) / (2 * h)
        # 恢复自变量的值
        x[idx] = tmp
        it.iternext()  # 移动到下一个元素
    return grad

4.3 特点 | Characteristics

优点:实现简单,不需要求导。
Advantage: simple, no derivation needed.

缺点:计算极慢。
Disadvantage: extremely slow.

五、方法二:反向传播 | Method 2: Backpropagation

5.1 模型结构(计算图) | Computation Graph

self.layers = OrderedDict()
self.layers['Affine1'] = Affine(W1, b1)
self.layers['Relu1'] = Relu()
self.layers['Affine2'] = Affine(W2, b2)
self.lastLayer = SoftmaxWithLoss()

5.2 前向传播 | Forward Pass

def forward(self, X):
    for layer in self.layers.values():
        X = layer.forward(X)
    return X

5.3 反向传播 | Backward Pass


#利用反向传播计算梯度
    def backward(self,X,t):
        #前向传播,计算损失
        self.loss(X,t)
        #反向传播
        reversed_layers=list(self.layers.values())
        reversed_layers.reverse()
        dy=1
        dy=self.lastLayer.backward(dy)
        for layer in reversed_layers:
            dy=layer.backward(dy)
        grads={}
        grads['W1'],grads['b1']=self.layers['Affine1'].dW,self.layers['Affine1'].db
        grads['W2'], grads['b2'] = self.layers['Affine2'].dW, self.layers['Affine2'].db
        return grads

误差从输出层逐层传播回输入层。
Errors propagate backward layer by layer.

七、训练方式对比 | Training Comparison

数值梯度训练 | Numerical Gradient Training

grads = model.gradient_numerical(x_batch, t_batch)

for key in model.params:
    model.params[key] -= lr * grads[key]

反向传播训练 | Backpropagation Training

grads = model.backward(x_batch, t_batch)
optimizer.update(model.params, grads)
#common.load_data.py
#获取数据
import pandas as pd
from sklearn.preprocessing import MinMaxScaler
from sklearn.model_selection import train_test_split
def get_data():
    # 1加载数据集
    dataset=pd.read_csv('../data/train.csv')
    # 2划分数据集
    x=dataset.drop("label",axis=1)
    y=dataset["label"]
    x_train,x_test,y_train,y_test=train_test_split(x,y,test_size=0.2,random_state=42)
    #特征工程
    scaler=MinMaxScaler()
    x_train=scaler.fit_transform(x_train)
    x_test=scaler.transform(x_test)

    return x_train, x_test, y_train.values, y_test.values
#4_digit_recognizer_train
#“基于数值梯度的两层前馈神经网络 + Mini-Batch梯度下降 + 交叉熵损失的监督学习分类模型”
import numpy as np
import matplotlib.pyplot as plt
from common.load_data import get_data
from tow_layer_net import tow_layer_net#另外的py文件

#1加载数据
x_train, x_test, y_train, y_test = get_data()
print(x_train.shape)
print(y_train.shape)

#2创建模型
model=tow_layer_net(input_size=x_train.shape[1],hidden_size=50,output_size=10)

lr=0.1
batch_size=100
num_epochs=10#迭代轮次数
n=x_train.shape[0]#训练集样本总数
iter_per_epoch=np.ceil(n/batch_size)#每个轮次中的迭代次数
interactions=int(iter_per_epoch * num_epochs)#总迭代次数

#用列表保存训练过程中的损失值,以及训练和测试准确率
train_loss_list=[]
train_acc_list=[]
test_acc_list=[]

#4 梯度下降法:循环迭代,更新参数(训练模型)
for i in range(interactions):
    #4.1 随机选取一小批数据样本
    batch_index=np.random.choice(n,batch_size)
    x_batch=x_train[batch_index]
    t_batch=y_train[batch_index]
    #4.2 计算梯度
    grads=model.gradient_numerical(x_batch, t_batch)
    #4.3 更新参数
    for key in model.params.keys():
        model.params[key]-=lr*grads[key]

    #4.4 计算当前训练损失
    this_loss=model.loss(x_batch,t_batch)
    train_loss_list.append(this_loss)
    print(f"Loss: {this_loss}")

    #4.5 每个轮次训练完成,计算准确率
    if i%iter_per_epoch==0:
        train_acc=model.accuracy(x_train,y_train)
        test_acc=model.accuracy(x_test,y_test)
        train_acc_list.append(train_acc)
        test_acc_list.append(test_acc)
        print(f"Tran Acc:{train_acc} Test Acc:{test_acc}")



#tow_layer_net.py
import numpy as np
from common.function import softmax , cross_entropy_error,sigmoid
from common.gradient import numerical_gradient #利用数值微分计算梯度

#自定义两层神经网络
class tow_layer_net:
    #初始化
    def __init__(self,input_size,hidden_size,output_size,weights_init_std=0.01):
        self.params={}
        self.params["W1"] = np.random.randn(input_size, hidden_size) * weights_init_std
        self.params["b1"] = np.zeros(hidden_size)

        self.params["W2"] = np.random.randn(hidden_size, output_size) * weights_init_std
        self.params["b2"] = np.zeros(output_size)
    #前向传播
    def forward(self,X):
        W1,W2,b1,b2 = self.params["W1"],self.params["W2"],self.params["b1"],self.params["b2"]
        #第一层传递
        a1 = X @ W1 + b1
        z1 = sigmoid(a1)
        # 第二层
        a2 = z1 @ W2 + b2
        z2 = sigmoid(a2)
        y = softmax(a2)
        return y
    #计算损失
    def loss(self,X,t):
        y = self.forward(X)#预测输出
        loss_value=cross_entropy_error(y,t)#计算交叉熵损失
        return loss_value
    #评估指标:计算准确度
    def accuracy(self,X,t):
        y=self.forward(X)
        y_pred=np.argmax(y,axis=1)
        #叠加分类正确的个数
        acc_cnt=np.sum(y_pred==t)
        return acc_cnt/X.shape[0]
    #计算所有参数的梯度
    def gradient_numerical(self, X, t):
        #定义目标函数(损失函数)
        loss_f = lambda _:self.loss(X,t)
        #计算每个参数的梯度,保存到字典中
        grads={}
        for key in self.params.keys():
            grads[key]=numerical_gradient(loss_f,self.params[key])
        return grads
#commom.function.py
import numpy as np

#接跃函数
def step_function0(x):
    if x > 0:
        return 1
    else:
        return 0

def step_function(x):
    return np.array(x>0,dtype=int)

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

#Relu
def relu(x):
    return np.maximum(0,x)

# #softmax
# def softmax(x):
#     x=x.T
#     x=x-np.max(x,axis=0)#防止数据溢出,有时候其中一个会特别大
#     y=np.exp(x) / np.sum(np.exp(x))
#     return y.T
def softmax(x):
    if x.ndim == 2:
        x = x - np.max(x, axis=1, keepdims=True)  # ★ 减去每行最大值
        x = np.exp(x)
        x /= np.sum(x, axis=1, keepdims=True)
    else:
        x = x - np.max(x)  # ★ 减去最大值
        x = np.exp(x) / np.sum(np.exp(x))
    return x


def identity(x):
    return x

#MSE,L2-Loss
def mean_squared_error(y, t):#y预测值,t真实值
    return 0.5*np.sum((y-t)**2)/y.shape[0]

#交叉熵损失
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)
    #利用列表索引,计算损失
    n=y.shape[0]
    return -np.sum(np.log(y[np.arange(n),t]+1e-7))/n



if __name__ == '__main__':
    x1=np.array([1,2,3,4,5,-1,-2,-3,-4,-5])
    x2=np.array([[1,2,3,4,5],[-1,-2,-3,-4,-5]])
    print(step_function(x1))
    print(sigmoid(x1))
    print(np.tanh(x1))#双曲正切函数
    print(relu(x1))

    print(softmax(x2))
#common.gradient.py
import numpy as np

#中心差分,返回函数在某点函数的导数值
def numerical_diff(f,x):
    h=1e-4
    return (f(x+h)-f(x-h))/(2*h)

#利用数值微分计算梯度向量
def numerical_gradient(f,x):
    h=1e-4
   #定义要计算反馈的梯度向量,形状与x相同
    grad=np.zeros_like(x)
    # 使用 nditer 遍历多维数组的每个元素
    it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
    while not it.finished:
        idx = it.multi_index  # 获取当前元素的多维索引
        tmp = x[idx]  # 保存原值
        # f(x+h)
        x[idx] = tmp + h
        fxh1 = f(x)
        # f(x-h)
        x[idx] = tmp - h
        fxh2 = f(x)
        # 中心差分计算偏导数
        grad[idx] = (fxh1 - fxh2) / (2 * h)
        # 恢复自变量的值
        x[idx] = tmp
        it.iternext()  # 移动到下一个元素
    return grad


#利用数值微风计算梯度,X可以是N*m的矩阵,也可以是m维向量
def numerical_jacobian(f,X):
    #如果是一维,就直接调用内部函数
    if X.ndim==1:
        return numerical_gradient(f,X)
    else:
        grad=np.zeros_like(X)
        #遍历X的每一行向量
        for i,x in enumerate(X):
            grad[i]=numerical_gradient(f,x)
        return grad

#梯度下降
def gradient_descent(f,init_x,lr=0.01,iterations=100):
    x=init_x
    x_history=[]#记录参数x变化轨迹
    #循环迭代
    for i in range (iterations):
        x_history.append(x.copy())
        grad=numerical_gradient(f,x)
        #更新x
        x-=lr*grad

    return x,np.array(x_history)

#1_digit_recognizer_train_backward.py
#基于计算图的两层前馈神经网络,使用反向传播求梯度,
# 并结合Mini-Batch与优化算法SGD
# 进行参数优化的多分类监督学习模型
import numpy as np
import matplotlib.pyplot as plt
from common.load_data import get_data
from tow_layer_net import TowLayerNet #另外的py文件
from common.optimizers import SGD  #使用优化器

#1加载数据
x_train, x_test, y_train, y_test = get_data()
print(x_train.shape)
print(y_train.shape)

#2创建模型
model=TowLayerNet(input_size=x_train.shape[1],hidden_size=50,output_size=10)

lr=0.001
batch_size=100
num_epochs=10#迭代轮次数
n=x_train.shape[0]#训练集样本总数
iter_per_epoch=np.ceil(n/batch_size)#每个轮次中的迭代次数
interactions=int(iter_per_epoch * num_epochs)#总迭代次数

#用列表保存训练过程中的损失值,以及训练和测试准确率
train_loss_list=[]
train_acc_list=[]
test_acc_list=[]

optimizer =SGD(lr)

# 4 梯度下降法:循环迭代,更新参数(训练模型)
for i in range(interactions):
    #4.1 随机选取一小批数据样本
    batch_index=np.random.choice(n,batch_size)
    x_batch=x_train[batch_index]
    t_batch=y_train[batch_index]
    #4.2 计算梯度
    #grads=model.gradient_numerical(x_batch,t_batch)
    grads=model.backward(x_batch,t_batch)
    #4.3 更新参数
    # for key in model.params.keys():
    #     model.params[key]-=lr*grads[key]
    optimizer.update(model.params,grads)

    #4.4 计算当前训练损失
    this_loss=model.loss(x_batch,t_batch)
    train_loss_list.append(this_loss)
    #print(f"Loss: {this_loss}")

    #4.5 每个轮次训练完成,计算准确率
    if i%iter_per_epoch==0:
        train_acc=model.accuracy(x_train,y_train)
        test_acc=model.accuracy(x_test,y_test)
        train_acc_list.append(train_acc)
        test_acc_list.append(test_acc)
        print(f"Tran Acc:{train_acc} Test Acc:{test_acc}")
        if i % 100 == 0:
            print(f"Loss: {this_loss}")

#5.画图
x=np.arange(len(train_acc_list))
plt.plot(x,train_acc_list,label="train acc")
plt.plot(x,test_acc_list,label="test acc",linestyle="--")
plt.xlabel("epoch")
plt.ylabel("accuracy")
plt.legend()
plt.show()
#tow_layer_net.py
import numpy as np
from common.function import softmax , cross_entropy_error,sigmoid
from common.gradient import numerical_gradient #利用数值微分计算梯度
from common.layers import *
from collections import OrderedDict

#自定义两层神经网络
class TowLayerNet:
    #初始化
    def __init__(self,input_size,hidden_size,output_size,weights_init_std=0.01):
        self.params={}
        self.params["W1"] = np.random.randn(input_size, hidden_size) * weights_init_std
        self.params["b1"] = np.zeros(hidden_size)

        self.params["W2"] = np.random.randn(hidden_size, output_size) * weights_init_std
        self.params["b2"] = np.zeros(output_size)
        #定义神经网络的所有层
        self.layers=OrderedDict()
        self.layers['Affine1']=Affine(self.params['W1'],self.params['b1'])
        self.layers['Relu1']=Relu()
        self.layers['Affine2'] = Affine(self.params['W2'], self.params['b2'])
        self.lastLayer=SoftmaxWithLoss()

    #前向传播
    def forward(self,X):
        #遍历所有层,,调用forward
        for layer in self.layers.values():
            X=layer.forward(X)
        return X

    #计算损失
    def loss(self,X,t):
        y = self.forward(X)#预测输出
        #loss_value=cross_entropy_error(y,t)#计算交叉熵损失
        return self.lastLayer.forward(y,t)
    #评估指标:计算准确度
    def accuracy(self,X,t):
        y=self.forward(X)
        #将预测输出得分转换为预测分类号
        y_pred=np.argmax(y,axis=1)
        #叠加分类正确的个数
        acc_cnt=np.sum(y_pred==t)
        return acc_cnt/X.shape[0]
    #计算所有参数的梯度
    def gradient_numerical(self,X,t):
        #定义目标函数(损失函数)
        loss_f = lambda _:self.loss(X,t)
        #计算每个参数的梯度,保存到字典中
        grads={}
        for key in self.params.keys():
            grads[key]=numerical_gradient(loss_f,self.params[key])
        return grads

#新增
#利用反向传播计算梯度
    def backward(self,X,t):
        #前向传播,计算损失
        self.loss(X,t)
        #反向传播
        reversed_layers=list(self.layers.values())
        reversed_layers.reverse()
        dy=1
        dy=self.lastLayer.backward(dy)
        for layer in reversed_layers:
            dy=layer.backward(dy)
        grads={}
        grads['W1'],grads['b1']=self.layers['Affine1'].dW,self.layers['Affine1'].db
        grads['W2'], grads['b2'] = self.layers['Affine2'].dW, self.layers['Affine2'].db
        return grads

#优化器
#common/optimizers.py
#随机梯度下降
import numpy as np
class SGD:
    def __init__(self, lr=0.01):
        self.lr = lr

    def update(self, params, grads):
        for key in params.keys():
            params[key] -= self.lr * grads[key]

#Momentum(动量法)会保存历史梯度并给予一定的权重,使其也参与到参数更新中
class Momentum:
    def __init__(self, lr=0.01,momentum=0.9):
        self.lr = lr
        self.momentum=momentum
        self.v=None
    #更新参数
    def update(self, params, grads):
        #初始化v
        if self.v is None:
            self.v={}
            for key,val in params.items():
                self.v[key] = np.zeros_like(val)
        #按公式更新所有参数
        for key in params.keys():
            self.v[key] = self.momentum*self.v[key] - self.lr*grads[key]
            params[key] += self.v[key]

#AdaGrad(Adaptive Gradient,自适应梯度)会为每个参数适当地调整学习率,并且随着学习的进行,学习率会逐渐减小。
class AdaGrad:
    def __init__(self, lr=0.1):
        self.lr = lr
        self.h=None
    def update(self, params, grads):
        if self.h is None:
            self.h={}
            for key,value in params.items():
                self.h[key]=np.zeros_like(value)
        #更新参数
        for key in params.keys():
            self.h[key]+=grads[key]*grads[key]
            params[key]-=self.lr*grads[key]/np.sqrt(self.h[key]+1e-7)

#RMSProp(Root Mean Square Propagation,均方根传播)是在AdaGrad基础上的改进,
#它并非将过去所有梯度一视同仁的相加,而是逐渐遗忘过去的梯度,采用指数移动加权平均,呈指数地减小过去梯度的尺度。
class RMSProp:
    def __init__(self, lr=0.01,decay=0.9):
        self.lr = lr
        self.decay=decay
        self.h=None
    def update(self, params, grads):
        if self.h is None:
            self.h={}
            for key,value in params.items():
                self.h[key]=np.zeros_like(value)
        #更新参数
        for key in params.keys():
            self.h[key]*= self.decay
            self.h[key]+=(1-self.decay)*grads[key]*grads[key]
            params[key]-=self.lr*grads[key]/np.sqrt(self.h[key]+1e-7)

#Adam(Adaptive Moment Estimation,自适应矩估计)融合了Momentum和AdaGrad的方法。
class Adam:
    def __init__(self, lr=0.1,alpha1=0.9,alpha2=0.999):
        self.lr = lr
        self.alpha1=alpha1
        self.alpha2=alpha2
        self.h=None
        self.v=None
        self.iter=0
    def update (self, params, grads):
        # 初始化 v,h
        if self.v is None:
            self.v,self.h={}, {}
            for key,value in params.items():
                self.v[key]=np.zeros_like(value)
                self.h[key] = np.zeros_like(value)
        self.iter += 1#迭代次数+1
        #计算按照轮次调整之后的学习率
        lr_t=self.lr*np.sqrt(1-self.alpha2**self.iter)/(1-self.alpha1**self.iter)
        #更新参数
        for key in params.keys():
            self.v[key] = self.alpha1 * self.v[key] + (1 - self.alpha1) * grads[key]
            self.h[key] = self.alpha2 * self.h[key] + (1 - self.alpha2) * (grads[key] ** 2)

            # self.v[key]=(1-self.alpha1)*(grads[key]-self.v[key])
            # self.h[key] = (1 - self.alpha2) * (grads[key]**2 - self.h[key])

            params[key]-=lr_t*grads[key]/np.sqrt(self.h[key]+1e-7)

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐