理论问题回答一、学习与模型(第 1 章)Q1. 神经网络训练过程中哪些量是已知的哪些量是未知的?已知量1.训练数据和测试数据或监督数据和测试数据2.网络结构神经元的层数、各层神经元数量人为设定的结构参数。未知量权重参数W和偏置参数b。学习的目标到底是什么?通过调整未知的权重参数和偏置参数最小化损失函数的值让模型具备对测试数据的泛化识别能力实现模型对未知数据的稳定预测。二、线性模型与非线性(第 2 章)Q2. 为什么单层感知机只能解决线性可分问题?因为单层感知机只能表示由直线分割的线性空间对于异或门这类非线性可分问题无法用一条直线划分两类样本。**Q3. 为什么必须引入非线性激活函数? **因为线性激活函数有局限性只能等价于单层线性模型无法学习复杂的非线性关系。为了发挥叠加层所带来的优势激活函数必须使用非线性函数。如果把神经网络中所有激活函数都去掉会发生什么?无法学习非线性模式仅能处理线性可分问题。三、神经网络的前向计算(第 3 章)Q4. 神经网络的前向传播本质上在做什么数学运算?本质上是矩阵乘法和激活函数运算的交替执行。先通过矩阵乘法计算输入信号与权重的加权和并叠加偏置再通过激活函数对结果进行非线性转换逐层传递至输出层。Q5. 为什么分类问题中输出层通常使用 Softmax? Softmax 在概率意义上做了什么?核心原因将神经网络输出的“得分”未归一化值转换为概率分布使输出值总和为1便于直观解读类别概率且能与交叉熵误差配合使反向传播时梯度计算更简洁直接得到输出与标签的差分。在概率上它对于每个类别i都计算其相对概率既保留了各得分的相对大小关系又将输出归一化到[0,1]区间满足概率的基本性质非负性、总和为1。四、损失函数与梯度(第 4 章 · 核心)**Q6. 为什么“准确率”不能作为训练时的优化目标? **因为准确率是离散指标多数情况下梯度为0无法引导参数更新。例如微小调整权重可能不会改变分类结果导致准确率不变参数更新停滞且准确率的变化不连续无法反映参数变化对模型性能的连续影响。为什么必须引入损失函数?因为它是连续可微的函数能量化模型预测与真实标签的差异其梯度可指导参数沿“减小误差”的方向更新同时损失函数的连续变化能反映参数调整的效果确保学习过程持续推进。五、梯度下降的本质(第 4 章 · 灵魂)Q7. 梯度在几何意义上代表什么?在几何意义上梯度是损失函数在当前参数点处的方向导数最大值方向即函数值增长最快的方向其模长表示增长的速率。为什么沿着负梯度方向更新参数?因为神经网络学习的目标是最小化损失函数负梯度方向是损失函数值减小最快的方向沿该方向更新参数能高效逼近损失函数的最小值或局部最小值。**Q8. 学习率在梯度下降中起什么作用? **作用控制参数更新的步长决定每次迭代中参数沿负梯度方向调整的幅度是平衡学习速度与收敛效果的关键超参数。学习率过大会怎样?过小又会怎样?学习率过大参数更新步长过大可能导致损失函数值震荡不收敛甚至发散如参数值超出最优范围损失函数值持续增大。学习率过小参数更新步长过小学习速度极慢需要大量迭代才能逼近最优解且可能陷入局部最小值或鞍点无法抵达全局最优。2. 完整可运行代码1.激活函数 损失函数使用 numpy 实现SigmoidReLUSoftmax实现交叉熵损失支持 batch 输入import numpy as np def sigmoid(x): Sigmoid 激活函数 公式: h(x) 1 / (1 exp(-x)) 参数: x: 输入数据 返回: Sigmoid 输出 return 1 / (1 np.exp(-x)) def relu(x): ReLU 激活函数 公式: h(x) max(0, x) 参数: x: 输入数据 (numpy array) 返回: ReLU 输出 return np.maximum(0, x) def softmax(x): Softmax 激活函数 公式: y_k exp(a_k) / sum(exp(a_i)) 参数: x: 输入数据 (numpy array) 如果是 1D 数组视为单个样本。 如果是 2D 数组视为 batch 样本。 返回: Softmax 输出 if x.ndim 2: # Batch 处理 x x.T x x - np.max(x, axis0) # 稳定性优化减去最大值防止 exp 溢出 y np.exp(x) / np.sum(np.exp(x), axis0) return y.T # 单个样本处理 x x - np.max(x) return np.exp(x) / np.sum(np.exp(x)) def cross_entropy_error(y, t): 交叉熵损失函数 公式: E -sum(t_k * log(y_k)) 参数: y: 神经网络的输出 (概率分布)经过 Softmax t: 监督数据 (标签) 可以是 one-hot 向量 (例如 [0, 1, 0, 0, ...]) 也可以是标签索引 (例如 1) 返回: 损失值 (标量) if y.ndim 1: t t.reshape(1, t.size) y y.reshape(1, y.size) # 如果 t 是 one-hot 向量转换为标签索引 if t.size y.size: t t.argmax(axis1) batch_size y.shape[0] # 添加一个微小值 delta 防止 log(0) delta 1e-7 return -np.sum(np.log(y[np.arange(batch_size), t] delta)) / batch_size2.数值梯度import numpy as np def numerical_gradient(f, x): 数值梯度计算函数 使用中心差分法近似计算梯度: (f(xh) - f(x-h)) / 2h 参数: f: 目标函数 x: 输入变量 (numpy array) 返回: 梯度 (与 x 形状相同) h 1e-4 #设置一个很小的数 grad np.zeros_like(x) # 生成和 x 形状相同的数组用于存放梯度 #遍历x的每一个元素 it np.nditer(x, flags[multi_index], op_flags[readwrite]) while not it.finished: idx it.multi_index tmp_val x[idx] # 计算 f(xh) x[idx] float(tmp_val) h fxh1 f(x) # 计算 f(x-h) x[idx] float(tmp_val) - h fxh2 f(x) # 计算梯度 grad[idx] (fxh1 - fxh2) / (2*h) # 还原值 x[idx] tmp_val it.iternext() return grad3.搭建网络层与反向传播import numpy as np from src.functions import softmax, cross_entropy_error class Relu: ReLU 层 前向传播: out x (x 0), 0 (x 0) 反向传播: dx dout (x 0), 0 (x 0) def __init__(self): self.mask None # 用于记录 x 中小于等于 0 的位置 def forward(self, x): 前向传播 self.mask (x 0) out x.copy() out[self.mask] 0 return out def backward(self, dout): 反向传播 dout: 上一层传下来的梯度 dout[self.mask] 0 dx dout return dx class Affine: Affine 层 (全连接层) 前向传播: out np.dot(x, W) b def __init__(self, W, b): self.W W # 权重 self.b b # 偏置 self.x None # 保存输入用于反向传播 self.dW None # 权重的梯度 self.db None # 偏置的梯度 def forward(self, x): # 如果输入是张量 (N, C, H, W)需要展平为 (N, D) self.original_x_shape x.shape x x.reshape(x.shape[0], -1) self.x x out np.dot(self.x, self.W) self.b return out def backward(self, dout): dx np.dot(dout, self.W.T) self.dW np.dot(self.x.T, dout) self.db np.sum(dout, axis0) dx dx.reshape(*self.original_x_shape) # 还原输入形状 return dx class SoftmaxWithLoss: SoftmaxWithLoss 层 结合了 Softmax 激活函数和 Cross Entropy Loss def __init__(self): self.loss None self.y None # Softmax 的输出 self.t None # 监督数据 (One-hot 或 标签索引) def forward(self, x, t): self.t t self.y softmax(x) self.loss cross_entropy_error(self.y, self.t) return self.loss def backward(self, dout1): batch_size self.t.shape[0] # 处理 one-hot 编码和标签索引两种情况 if self.t.size self.y.size: # one-hot dx (self.y - self.t) / batch_size else: dx self.y.copy() dx[np.arange(batch_size), self.t] - 1 dx dx / batch_size return dx组装网络import numpy as np from collections import OrderedDict from src.layers import * from src.gradient import numerical_gradient class TwoLayerNet: 两层神经网络 结构: Input - Affine - ReLU - Affine - SoftmaxWithLoss def __init__(self, input_size, hidden_size, output_size, weight_init_std0.01): 初始化网络权重 参数: input_size: 输入层神经元数量 hidden_size: 隐藏层神经元数量 output_size: 输出层神经元数量 weight_init_std: 权重初始化标准差 # 初始化权重 self.params {} self.params[W1] weight_init_std * np.random.randn(input_size, hidden_size) self.params[b1] np.zeros(hidden_size) self.params[W2] weight_init_std * np.random.randn(hidden_size, output_size) 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 predict(self, x): 前向传播 (预测) for layer in self.layers.values(): x layer.forward(x) return x def loss(self, x, t): 计算损失函数值 参数: x: 输入数据 t: 监督数据 (标签) y self.predict(x) return self.lastLayer.forward(y, t) def accuracy(self, x, t): 计算精度 y self.predict(x) y np.argmax(y, axis1) if t.ndim ! 1 : t np.argmax(t, axis1) accuracy np.sum(y t) / float(x.shape[0]) return accuracy 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 gradient(self, x, t): 使用误差反向传播法计算梯度 (速度快) # 1. 前向传播 self.loss(x, t) # 2. 反向传播 dout 1 dout self.lastLayer.backward(dout) layers list(self.layers.values()) layers.reverse() for layer in layers: dout layer.backward(dout) # 3. 收集梯度 grads {} grads[W1] self.layers[Affine1].dW grads[b1] self.layers[Affine1].db grads[W2] self.layers[Affine2].dW grads[b2] self.layers[Affine2].db return grads4.数据加载及主训练循环数据加载import os import gzip import numpy as np import urllib.request # MNIST 数据集下载链接 url_base https://ossci- datasets.s3.amazonaws.com/mnist/ 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 ... ) # 使用 header 模拟浏览器防止某些服务器拒绝 headers {User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3} req urllib.request.Request(url_base file_name, headersheaders) try: with urllib.request.urlopen(req) as response, open(file_path, wb) as out_file: data response.read() out_file.write(data) print(Done) except Exception as e: print(fFailed to download {file_name}: {e}) # 如果下载失败尝试备用镜像或提示用户 print(Please try to download manually and place in: , dataset_dir) 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, offset8) 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, offset16) 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 ...) import pickle with open(save_file, wb) as f: pickle.dump(dataset, f, -1) print(Done!) def load_mnist(normalizeTrue, flattenTrue, one_hot_labelFalse): 读入 MNIST 数据集 Parameters normalize : 将图像的像素值正规化为 0.0~1.0 one_hot_label : False - 7, 2, ... True - [0,0,0,0,0,0,0,1,0,0], [0,0,1,0,0,0,0,0,0,0], ... flatten : 是否将图像展开为一维数组 Returns (训练图像, 训练标签), (测试图像, 测试标签) if not os.path.exists(save_file): init_mnist() import pickle 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]) def _change_one_hot_label(X): T np.zeros((X.size, 10)) for idx, row in enumerate(T): row[X[idx]] 1 return T主训练循环import numpy as np import matplotlib.pyplot as plt from src.dataset import load_mnist from src.network import TwoLayerNet # 1. 读入数据 print(正在加载 MNIST 数据集...) (x_train, t_train), (x_test, t_test) load_mnist(normalizeTrue, one_hot_labelTrue) print(f训练数据形状: {x_train.shape}) print(f测试数据形状: {x_test.shape}) # 2. 超参数设置 iters_num 10000 # 适当设定循环的次数 train_size x_train.shape[0] batch_size 100 learning_rate 0.1 train_loss_list [] train_acc_list [] test_acc_list [] # 平均每个 epoch 的重复次数 iter_per_epoch max(train_size / batch_size, 1) # 3. 初始化网络 # 输入层 784 (28x28), 隐藏层 50, 输出层 10 (0-9) network TwoLayerNet(input_size784, hidden_size50, output_size10) print(开始训练...) for i in range(iters_num): # 获取 mini-batch batch_mask np.random.choice(train_size, batch_size) x_batch x_train[batch_mask] t_batch t_train[batch_mask] # 计算梯度 # 推荐: 使用反向传播 (Task 4 实现) grad network.gradient(x_batch, t_batch) # 也可以使用数值梯度 (Task 3 实现)但速度非常慢不建议在实际训练中使用 # grad network.numerical_gradient(x_batch, t_batch) # 更新参数 (SGD - Task 5) for key in (W1, b1, W2, b2): network.params[key] - learning_rate * grad[key] # 记录学习过程 loss network.loss(x_batch, t_batch) train_loss_list.append(loss) # 计算每个 epoch 的识别精度 if i % iter_per_epoch 0: train_acc network.accuracy(x_train, t_train) test_acc network.accuracy(x_test, t_test) train_acc_list.append(train_acc) test_acc_list.append(test_acc) print(fepoch: {int(i/iter_per_epoch)}, loss: {loss:.4f}, train acc: {train_acc:.4f}, test acc: {test_acc:.4f}) print(训练结束!) # 4. 绘图 (Task 6) # 绘制损失函数变化 plt.figure(figsize(12, 5)) plt.subplot(1, 2, 1) plt.plot(train_loss_list) plt.title(Loss Function History) plt.xlabel(Iterations) plt.ylabel(Loss) # 绘制精度变化 plt.subplot(1, 2, 2) markers {train: o, test: s} x np.arange(len(train_acc_list)) plt.plot(x, train_acc_list, labeltrain acc) plt.plot(x, test_acc_list, labeltest acc, linestyle--) plt.xlabel(Epochs) plt.ylabel(Accuracy) plt.ylim(0, 1.0) plt.legend(loclower right) plt.title(Accuracy History) plt.tight_layout() plt.savefig(training_result.png) print(结果已保存至 training_result.png) # plt.show()注关于MNIST数据集在代码中使用from src.dataset import load_mnist # 第一次运行时会自动下载并生成 mnist.pkl 缓存文件 (x_train, t_train), (x_test, t_test) load_mnist(normalizeTrue, flattenTrue, one_hot_labelTrue) print(x_train.shape) # (60000, 784) print(t_train.shape) # (60000, 10)运行1.安装依赖pip install -r requirements.txt2.运行训练python main.py3.运行过程正在加载 MNIST 数据集... 训练数据形状: (60000, 784) 测试数据形状: (10000, 784) 开始训练... epoch: 0, loss: 2.3002, train acc: 0.1208, test acc: 0.1133 epoch: 1, loss: 0.2386, train acc: 0.9035, test acc: 0.9079 epoch: 2, loss: 0.2094, train acc: 0.9235, test acc: 0.9258 epoch: 3, loss: 0.0903, train acc: 0.9368, test acc: 0.9350 epoch: 4, loss: 0.1779, train acc: 0.9452, test acc: 0.9442 epoch: 5, loss: 0.0762, train acc: 0.9505, test acc: 0.9488 epoch: 6, loss: 0.2360, train acc: 0.9567, test acc: 0.9532 epoch: 7, loss: 0.0739, train acc: 0.9613, test acc: 0.9540 epoch: 8, loss: 0.1321, train acc: 0.9636, test acc: 0.9577 epoch: 9, loss: 0.0399, train acc: 0.9663, test acc: 0.9599 epoch: 10, loss: 0.0545, train acc: 0.9688, test acc: 0.9629 epoch: 11, loss: 0.0721, train acc: 0.9704, test acc: 0.9635 epoch: 12, loss: 0.1163, train acc: 0.9738, test acc: 0.9648 epoch: 13, loss: 0.0445, train acc: 0.9757, test acc: 0.9678 epoch: 14, loss: 0.0429, train acc: 0.9769, test acc: 0.9673 epoch: 15, loss: 0.0374, train acc: 0.9780, test acc: 0.9704 epoch: 16, loss: 0.0675, train acc: 0.9792, test acc: 0.9698 训练结束! 结果已保存至 training_result.png3. 简单实验记录loss 曲线或日志
从梯度下降到神经网络学习
发布时间:2026/6/4 0:09:14
理论问题回答一、学习与模型(第 1 章)Q1. 神经网络训练过程中哪些量是已知的哪些量是未知的?已知量1.训练数据和测试数据或监督数据和测试数据2.网络结构神经元的层数、各层神经元数量人为设定的结构参数。未知量权重参数W和偏置参数b。学习的目标到底是什么?通过调整未知的权重参数和偏置参数最小化损失函数的值让模型具备对测试数据的泛化识别能力实现模型对未知数据的稳定预测。二、线性模型与非线性(第 2 章)Q2. 为什么单层感知机只能解决线性可分问题?因为单层感知机只能表示由直线分割的线性空间对于异或门这类非线性可分问题无法用一条直线划分两类样本。**Q3. 为什么必须引入非线性激活函数? **因为线性激活函数有局限性只能等价于单层线性模型无法学习复杂的非线性关系。为了发挥叠加层所带来的优势激活函数必须使用非线性函数。如果把神经网络中所有激活函数都去掉会发生什么?无法学习非线性模式仅能处理线性可分问题。三、神经网络的前向计算(第 3 章)Q4. 神经网络的前向传播本质上在做什么数学运算?本质上是矩阵乘法和激活函数运算的交替执行。先通过矩阵乘法计算输入信号与权重的加权和并叠加偏置再通过激活函数对结果进行非线性转换逐层传递至输出层。Q5. 为什么分类问题中输出层通常使用 Softmax? Softmax 在概率意义上做了什么?核心原因将神经网络输出的“得分”未归一化值转换为概率分布使输出值总和为1便于直观解读类别概率且能与交叉熵误差配合使反向传播时梯度计算更简洁直接得到输出与标签的差分。在概率上它对于每个类别i都计算其相对概率既保留了各得分的相对大小关系又将输出归一化到[0,1]区间满足概率的基本性质非负性、总和为1。四、损失函数与梯度(第 4 章 · 核心)**Q6. 为什么“准确率”不能作为训练时的优化目标? **因为准确率是离散指标多数情况下梯度为0无法引导参数更新。例如微小调整权重可能不会改变分类结果导致准确率不变参数更新停滞且准确率的变化不连续无法反映参数变化对模型性能的连续影响。为什么必须引入损失函数?因为它是连续可微的函数能量化模型预测与真实标签的差异其梯度可指导参数沿“减小误差”的方向更新同时损失函数的连续变化能反映参数调整的效果确保学习过程持续推进。五、梯度下降的本质(第 4 章 · 灵魂)Q7. 梯度在几何意义上代表什么?在几何意义上梯度是损失函数在当前参数点处的方向导数最大值方向即函数值增长最快的方向其模长表示增长的速率。为什么沿着负梯度方向更新参数?因为神经网络学习的目标是最小化损失函数负梯度方向是损失函数值减小最快的方向沿该方向更新参数能高效逼近损失函数的最小值或局部最小值。**Q8. 学习率在梯度下降中起什么作用? **作用控制参数更新的步长决定每次迭代中参数沿负梯度方向调整的幅度是平衡学习速度与收敛效果的关键超参数。学习率过大会怎样?过小又会怎样?学习率过大参数更新步长过大可能导致损失函数值震荡不收敛甚至发散如参数值超出最优范围损失函数值持续增大。学习率过小参数更新步长过小学习速度极慢需要大量迭代才能逼近最优解且可能陷入局部最小值或鞍点无法抵达全局最优。2. 完整可运行代码1.激活函数 损失函数使用 numpy 实现SigmoidReLUSoftmax实现交叉熵损失支持 batch 输入import numpy as np def sigmoid(x): Sigmoid 激活函数 公式: h(x) 1 / (1 exp(-x)) 参数: x: 输入数据 返回: Sigmoid 输出 return 1 / (1 np.exp(-x)) def relu(x): ReLU 激活函数 公式: h(x) max(0, x) 参数: x: 输入数据 (numpy array) 返回: ReLU 输出 return np.maximum(0, x) def softmax(x): Softmax 激活函数 公式: y_k exp(a_k) / sum(exp(a_i)) 参数: x: 输入数据 (numpy array) 如果是 1D 数组视为单个样本。 如果是 2D 数组视为 batch 样本。 返回: Softmax 输出 if x.ndim 2: # Batch 处理 x x.T x x - np.max(x, axis0) # 稳定性优化减去最大值防止 exp 溢出 y np.exp(x) / np.sum(np.exp(x), axis0) return y.T # 单个样本处理 x x - np.max(x) return np.exp(x) / np.sum(np.exp(x)) def cross_entropy_error(y, t): 交叉熵损失函数 公式: E -sum(t_k * log(y_k)) 参数: y: 神经网络的输出 (概率分布)经过 Softmax t: 监督数据 (标签) 可以是 one-hot 向量 (例如 [0, 1, 0, 0, ...]) 也可以是标签索引 (例如 1) 返回: 损失值 (标量) if y.ndim 1: t t.reshape(1, t.size) y y.reshape(1, y.size) # 如果 t 是 one-hot 向量转换为标签索引 if t.size y.size: t t.argmax(axis1) batch_size y.shape[0] # 添加一个微小值 delta 防止 log(0) delta 1e-7 return -np.sum(np.log(y[np.arange(batch_size), t] delta)) / batch_size2.数值梯度import numpy as np def numerical_gradient(f, x): 数值梯度计算函数 使用中心差分法近似计算梯度: (f(xh) - f(x-h)) / 2h 参数: f: 目标函数 x: 输入变量 (numpy array) 返回: 梯度 (与 x 形状相同) h 1e-4 #设置一个很小的数 grad np.zeros_like(x) # 生成和 x 形状相同的数组用于存放梯度 #遍历x的每一个元素 it np.nditer(x, flags[multi_index], op_flags[readwrite]) while not it.finished: idx it.multi_index tmp_val x[idx] # 计算 f(xh) x[idx] float(tmp_val) h fxh1 f(x) # 计算 f(x-h) x[idx] float(tmp_val) - h fxh2 f(x) # 计算梯度 grad[idx] (fxh1 - fxh2) / (2*h) # 还原值 x[idx] tmp_val it.iternext() return grad3.搭建网络层与反向传播import numpy as np from src.functions import softmax, cross_entropy_error class Relu: ReLU 层 前向传播: out x (x 0), 0 (x 0) 反向传播: dx dout (x 0), 0 (x 0) def __init__(self): self.mask None # 用于记录 x 中小于等于 0 的位置 def forward(self, x): 前向传播 self.mask (x 0) out x.copy() out[self.mask] 0 return out def backward(self, dout): 反向传播 dout: 上一层传下来的梯度 dout[self.mask] 0 dx dout return dx class Affine: Affine 层 (全连接层) 前向传播: out np.dot(x, W) b def __init__(self, W, b): self.W W # 权重 self.b b # 偏置 self.x None # 保存输入用于反向传播 self.dW None # 权重的梯度 self.db None # 偏置的梯度 def forward(self, x): # 如果输入是张量 (N, C, H, W)需要展平为 (N, D) self.original_x_shape x.shape x x.reshape(x.shape[0], -1) self.x x out np.dot(self.x, self.W) self.b return out def backward(self, dout): dx np.dot(dout, self.W.T) self.dW np.dot(self.x.T, dout) self.db np.sum(dout, axis0) dx dx.reshape(*self.original_x_shape) # 还原输入形状 return dx class SoftmaxWithLoss: SoftmaxWithLoss 层 结合了 Softmax 激活函数和 Cross Entropy Loss def __init__(self): self.loss None self.y None # Softmax 的输出 self.t None # 监督数据 (One-hot 或 标签索引) def forward(self, x, t): self.t t self.y softmax(x) self.loss cross_entropy_error(self.y, self.t) return self.loss def backward(self, dout1): batch_size self.t.shape[0] # 处理 one-hot 编码和标签索引两种情况 if self.t.size self.y.size: # one-hot dx (self.y - self.t) / batch_size else: dx self.y.copy() dx[np.arange(batch_size), self.t] - 1 dx dx / batch_size return dx组装网络import numpy as np from collections import OrderedDict from src.layers import * from src.gradient import numerical_gradient class TwoLayerNet: 两层神经网络 结构: Input - Affine - ReLU - Affine - SoftmaxWithLoss def __init__(self, input_size, hidden_size, output_size, weight_init_std0.01): 初始化网络权重 参数: input_size: 输入层神经元数量 hidden_size: 隐藏层神经元数量 output_size: 输出层神经元数量 weight_init_std: 权重初始化标准差 # 初始化权重 self.params {} self.params[W1] weight_init_std * np.random.randn(input_size, hidden_size) self.params[b1] np.zeros(hidden_size) self.params[W2] weight_init_std * np.random.randn(hidden_size, output_size) 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 predict(self, x): 前向传播 (预测) for layer in self.layers.values(): x layer.forward(x) return x def loss(self, x, t): 计算损失函数值 参数: x: 输入数据 t: 监督数据 (标签) y self.predict(x) return self.lastLayer.forward(y, t) def accuracy(self, x, t): 计算精度 y self.predict(x) y np.argmax(y, axis1) if t.ndim ! 1 : t np.argmax(t, axis1) accuracy np.sum(y t) / float(x.shape[0]) return accuracy 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 gradient(self, x, t): 使用误差反向传播法计算梯度 (速度快) # 1. 前向传播 self.loss(x, t) # 2. 反向传播 dout 1 dout self.lastLayer.backward(dout) layers list(self.layers.values()) layers.reverse() for layer in layers: dout layer.backward(dout) # 3. 收集梯度 grads {} grads[W1] self.layers[Affine1].dW grads[b1] self.layers[Affine1].db grads[W2] self.layers[Affine2].dW grads[b2] self.layers[Affine2].db return grads4.数据加载及主训练循环数据加载import os import gzip import numpy as np import urllib.request # MNIST 数据集下载链接 url_base https://ossci- datasets.s3.amazonaws.com/mnist/ 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 ... ) # 使用 header 模拟浏览器防止某些服务器拒绝 headers {User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3} req urllib.request.Request(url_base file_name, headersheaders) try: with urllib.request.urlopen(req) as response, open(file_path, wb) as out_file: data response.read() out_file.write(data) print(Done) except Exception as e: print(fFailed to download {file_name}: {e}) # 如果下载失败尝试备用镜像或提示用户 print(Please try to download manually and place in: , dataset_dir) 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, offset8) 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, offset16) 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 ...) import pickle with open(save_file, wb) as f: pickle.dump(dataset, f, -1) print(Done!) def load_mnist(normalizeTrue, flattenTrue, one_hot_labelFalse): 读入 MNIST 数据集 Parameters normalize : 将图像的像素值正规化为 0.0~1.0 one_hot_label : False - 7, 2, ... True - [0,0,0,0,0,0,0,1,0,0], [0,0,1,0,0,0,0,0,0,0], ... flatten : 是否将图像展开为一维数组 Returns (训练图像, 训练标签), (测试图像, 测试标签) if not os.path.exists(save_file): init_mnist() import pickle 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]) def _change_one_hot_label(X): T np.zeros((X.size, 10)) for idx, row in enumerate(T): row[X[idx]] 1 return T主训练循环import numpy as np import matplotlib.pyplot as plt from src.dataset import load_mnist from src.network import TwoLayerNet # 1. 读入数据 print(正在加载 MNIST 数据集...) (x_train, t_train), (x_test, t_test) load_mnist(normalizeTrue, one_hot_labelTrue) print(f训练数据形状: {x_train.shape}) print(f测试数据形状: {x_test.shape}) # 2. 超参数设置 iters_num 10000 # 适当设定循环的次数 train_size x_train.shape[0] batch_size 100 learning_rate 0.1 train_loss_list [] train_acc_list [] test_acc_list [] # 平均每个 epoch 的重复次数 iter_per_epoch max(train_size / batch_size, 1) # 3. 初始化网络 # 输入层 784 (28x28), 隐藏层 50, 输出层 10 (0-9) network TwoLayerNet(input_size784, hidden_size50, output_size10) print(开始训练...) for i in range(iters_num): # 获取 mini-batch batch_mask np.random.choice(train_size, batch_size) x_batch x_train[batch_mask] t_batch t_train[batch_mask] # 计算梯度 # 推荐: 使用反向传播 (Task 4 实现) grad network.gradient(x_batch, t_batch) # 也可以使用数值梯度 (Task 3 实现)但速度非常慢不建议在实际训练中使用 # grad network.numerical_gradient(x_batch, t_batch) # 更新参数 (SGD - Task 5) for key in (W1, b1, W2, b2): network.params[key] - learning_rate * grad[key] # 记录学习过程 loss network.loss(x_batch, t_batch) train_loss_list.append(loss) # 计算每个 epoch 的识别精度 if i % iter_per_epoch 0: train_acc network.accuracy(x_train, t_train) test_acc network.accuracy(x_test, t_test) train_acc_list.append(train_acc) test_acc_list.append(test_acc) print(fepoch: {int(i/iter_per_epoch)}, loss: {loss:.4f}, train acc: {train_acc:.4f}, test acc: {test_acc:.4f}) print(训练结束!) # 4. 绘图 (Task 6) # 绘制损失函数变化 plt.figure(figsize(12, 5)) plt.subplot(1, 2, 1) plt.plot(train_loss_list) plt.title(Loss Function History) plt.xlabel(Iterations) plt.ylabel(Loss) # 绘制精度变化 plt.subplot(1, 2, 2) markers {train: o, test: s} x np.arange(len(train_acc_list)) plt.plot(x, train_acc_list, labeltrain acc) plt.plot(x, test_acc_list, labeltest acc, linestyle--) plt.xlabel(Epochs) plt.ylabel(Accuracy) plt.ylim(0, 1.0) plt.legend(loclower right) plt.title(Accuracy History) plt.tight_layout() plt.savefig(training_result.png) print(结果已保存至 training_result.png) # plt.show()注关于MNIST数据集在代码中使用from src.dataset import load_mnist # 第一次运行时会自动下载并生成 mnist.pkl 缓存文件 (x_train, t_train), (x_test, t_test) load_mnist(normalizeTrue, flattenTrue, one_hot_labelTrue) print(x_train.shape) # (60000, 784) print(t_train.shape) # (60000, 10)运行1.安装依赖pip install -r requirements.txt2.运行训练python main.py3.运行过程正在加载 MNIST 数据集... 训练数据形状: (60000, 784) 测试数据形状: (10000, 784) 开始训练... epoch: 0, loss: 2.3002, train acc: 0.1208, test acc: 0.1133 epoch: 1, loss: 0.2386, train acc: 0.9035, test acc: 0.9079 epoch: 2, loss: 0.2094, train acc: 0.9235, test acc: 0.9258 epoch: 3, loss: 0.0903, train acc: 0.9368, test acc: 0.9350 epoch: 4, loss: 0.1779, train acc: 0.9452, test acc: 0.9442 epoch: 5, loss: 0.0762, train acc: 0.9505, test acc: 0.9488 epoch: 6, loss: 0.2360, train acc: 0.9567, test acc: 0.9532 epoch: 7, loss: 0.0739, train acc: 0.9613, test acc: 0.9540 epoch: 8, loss: 0.1321, train acc: 0.9636, test acc: 0.9577 epoch: 9, loss: 0.0399, train acc: 0.9663, test acc: 0.9599 epoch: 10, loss: 0.0545, train acc: 0.9688, test acc: 0.9629 epoch: 11, loss: 0.0721, train acc: 0.9704, test acc: 0.9635 epoch: 12, loss: 0.1163, train acc: 0.9738, test acc: 0.9648 epoch: 13, loss: 0.0445, train acc: 0.9757, test acc: 0.9678 epoch: 14, loss: 0.0429, train acc: 0.9769, test acc: 0.9673 epoch: 15, loss: 0.0374, train acc: 0.9780, test acc: 0.9704 epoch: 16, loss: 0.0675, train acc: 0.9792, test acc: 0.9698 训练结束! 结果已保存至 training_result.png3. 简单实验记录loss 曲线或日志