SGD感知器(Python)无法训练MNIST数字识别问题排查
基于SGD的感知器MNIST数字识别问题分析与解答
代码中的核心错误(导致无法学习的原因)
- 样本维度处理错误:原代码中
sample = np.append(X_train[s], [])未将28×28的图像展平为784维向量,导致输入向量维度与权重矩阵不匹配,加权和计算逻辑完全错误。正确做法是使用sample = X_train[s].flatten()。 - 梯度计算逻辑错误:δ_j是每个输出神经元的独立误差项,公式应为
δ_j = -(d_j - y_j) * f'(net_j)(元素级乘法,而非内积)。原代码将误差向量与导数向量做内积得到单一标量,再用该标量统一更新所有神经元的权重,完全丢失了每个输出神经元的独立梯度信息,导致权重更新方向错误。 - 训练终止条件不合理:仅以单个样本的误差小于阈值就终止训练,会导致模型在极少量迭代后停止,根本未完成有效学习。正确做法是设置固定训练轮数,或监控训练集/验证集的平均误差下降情况。
- 测试集使用错误:测试函数中误用
X_train[j]作为测试样本,应改为X_test[j].flatten()。
修正后的核心训练代码示例:
def train(X_train, y_train, epochs=10): # Weight initialization weights = np.random.rand(X_vector_len, y_vector_len) * offset * 2 - offset bias = np.random.rand(y_vector_len) * offset * 2 - offset for epoch in range(epochs): total_error = 0.0 # 随机打乱训练集(SGD标准操作) indices = np.random.permutation(len(X_train)) for s in indices: sample = X_train[s].flatten() # 展平为784维向量 desired = np.zeros(y_vector_len, dtype=np.float64) desired[y_train[s]] = 1 net = np.dot(sample, weights) + bias y_pred = activation_function(net) e_vec = desired - y_pred e = np.sum(e_vec ** 2) / 2 total_error += e # 计算每个输出神经元的delta向量(元素级乘法) delta = -e_vec * acivation_derivative(net) # Numpy向量化更新权重:outer生成(784,10)梯度矩阵 weights += learning_rate * np.outer(sample, delta) # 更新偏置 bias += learning_rate * delta avg_error = total_error / len(X_train) print(f"Epoch {epoch+1}, Average Error: {avg_error:.4f}") # 可选:平均误差低于阈值时提前终止 if avg_error < e_threshold: break return weights, bias, epoch+1
针对疑问的解答
1. 为何误差向量正确但神经网络无法学习?
核心原因是梯度计算和权重更新逻辑错误:你错误地将误差向量与导数向量做内积得到单一标量,用该标量统一更新所有输出神经元的权重,完全丢失了每个神经元的独立误差梯度信息。此外,样本未展平导致输入维度不匹配、训练终止条件不合理,也让模型根本没机会完成有效学习。
2. δ是内积得到的标量,δ_j具体是什么?若用δ替代δ_j是否会混淆权重更新?
δ_j是第j个输出神经元的误差项,公式为δ_j = -(d_j - y_j) * f'(net_j),是一个与输出神经元数量相同的向量(而非标量)。每个输出神经元j对应独立的δ_j,用于更新该神经元对应的所有输入权重(w_ij)。
用内积得到的标量替代δ_j是完全错误的:内积会把所有神经元的误差和导数混合成一个值,导致所有神经元的权重更新方向和幅度完全一致,无法针对每个类别的误差进行独立调整,这也是你的模型无法学习的关键原因。
3. Sigmoid值域为(0,1),误差向量范围为(-1,1),其他值域不同的激活函数(如f(x)=x、arctg(x))如何工作?
不同激活函数的适配逻辑核心在于激活值域与目标值范围的匹配:
- 线性激活(f(x)=x):值域为(-∞,+∞),适合回归任务,若用于分类,需将目标值调整到对应范围(比如MNIST可设为[-5,5]区间的对应值),损失函数仍可用均方误差,但分类效果通常不如非线性激活。
- arctg(x):值域为(-π/2, π/2),属于饱和非线性激活,导数始终为正且有界,目标值需映射到该值域内(比如将MNIST的目标1映射为π/2,0映射为-π/2),梯度更新逻辑与sigmoid一致,仅需替换激活函数和导数公式。
4. 单位阶跃函数是否适用于SGD?
单位阶跃函数不适用于SGD:它在x=0处不可导,其他位置导数为0,无法计算梯度,而SGD的核心依赖梯度下降来更新权重。如果硬要用,只能改用感知器的原始更新规则(基于分类错误更新,而非梯度),但这种规则仅适用于线性可分问题,MNIST是非线性可分的,所以效果极差。
5. 能否用Numpy函数实现权重更新(将w_ij行与x_i相乘)?
完全可以,这是推荐的高效做法(避免嵌套循环)。使用np.outer(sample, delta)即可实现:
sample是(784,)的输入向量,delta是(10,)的误差项向量np.outer(sample, delta)会生成(784,10)的矩阵,每个元素为sample[i] * delta[j],正好对应权重更新的η * δ_j * x_i项- 直接执行
weights += learning_rate * np.outer(sample, delta)即可完成所有权重的向量化更新,效率远高于嵌套循环。
内容的提问来源于stack exchange,提问作者cherv11
相关产品推荐
相关产品推荐

