基于MNIST数据集的手写数字识别:从数值梯度到反向传播
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
真实标签 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)
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)