纯NumPy实现Softmax+CrossEntropy神经网络故障排查求助
我帮你排查了代码里的几个核心问题,这些应该是导致模型始终预测单一类别的关键原因:
1. 最致命错误:交叉熵+Softmax的梯度计算逻辑错误
当交叉熵损失和Softmax激活函数搭配使用时,不需要单独计算两个函数的导数再相乘——两者结合后的梯度有非常简洁的简化形式,这是多分类任务中最常用的优化技巧:
- 交叉熵损失公式:$L = -\sum(y \cdot \log(pred))$,其中$pred$是Softmax的输出
- 对Softmax输入$z$(即
self.z2)求导,最终结果为:$\frac{dL}{dz} = pred - y$
你之前分开计算cross_entropy_derivative和softmax_derivative的方式完全错误,尤其是softmax_derivative的实现(Softmax的导数是雅可比矩阵,并非逐元素运算),直接导致梯度更新完全偏离正确方向。
2. 损失函数计算未取均值
你的cross_entropy函数返回的是每个样本的损失数组,没有做均值/求和处理,这会导致损失值量级过大,梯度更新不稳定。
3. 学习率过小
0.001的学习率对于简单的IRIS数据集来说太小,10000次迭代可能不足以让模型收敛到有效参数。
4. 权重初始化不合理
使用np.random.rand生成[0,1]区间的初始权重,容易导致Sigmoid激活函数直接进入饱和区,引发梯度消失,模型无法更新参数。
修改后的关键代码片段
1. 修正神经网络类的初始化、前馈和反向传播
class NeuralNetwork: def __init__(self, x, y): self.x = x input_dim = self.x.shape[1] hidden_dim = 16 output_dim = y.shape[1] # 使用Xavier初始化避免Sigmoid饱和 self.weights1 = np.random.randn(input_dim, hidden_dim) * np.sqrt(1/(input_dim + hidden_dim)) self.bias1 = np.zeros(hidden_dim) # 偏置初始化为0更稳妥 self.weights2 = np.random.randn(hidden_dim, output_dim) * np.sqrt(1/(hidden_dim + output_dim)) self.bias2 = np.zeros(output_dim) self.y = y self.pred = np.zeros(y.shape) self.lr = 0.01 # 调大学习率加快收敛 def feedforward(self): self.layer1 = sigmoid(np.dot(self.x, self.weights1) + self.bias1) self.z2 = np.dot(self.layer1, self.weights2) + self.bias2 # 保存Softmax的输入,用于梯度计算 self.layer2 = softmax(self.z2) return self.layer2 def backprop(self): batch_size = self.x.shape[0] # 交叉熵+Softmax的简化梯度:pred - y dz2 = self.pred - self.y # 计算输出层权重和偏置的梯度(除以batch_size做均值,稳定训练) d_weights2 = np.dot(self.layer1.T, dz2) / batch_size d_bias2 = np.sum(dz2, axis=0) / batch_size # 计算隐藏层梯度 dz1 = np.dot(dz2, self.weights2.T) * sigmoid_derivative(self.layer1) d_weights1 = np.dot(self.x.T, dz1) / batch_size d_bias1 = np.sum(dz1, axis=0) / batch_size # 更新参数(注意是减号:梯度下降) self.weights1 -= self.lr * d_weights1 self.weights2 -= self.lr * d_weights2 self.bias1 -= self.lr * d_bias1 self.bias2 -= self.lr * d_bias2 # 其余train/predict/evaluate方法保留,仅修改evaluate里的损失计算 def evaluate(self, y, pred): self.y = y self.pred = pred self.loss = cross_entropy(self.pred, self.y) return self.loss
2. 修正交叉熵损失函数
def cross_entropy(X,y): X = X.clip(min=1e-8, max=None) # 对所有样本的损失取均值 return np.mean(np.where(y==1, -np.log(X), 0).sum(axis=1))
3. 删除无用的导数函数
你可以直接删除cross_entropy_derivative和softmax_derivative,因为现在已经不需要它们了。
验证修改效果
修改后重新运行训练代码,你会看到损失值持续下降,最终模型能准确区分IRIS的三个类别。核心逻辑是:
- 用简化后的梯度公式保证参数更新方向正确
- 合理的初始化和学习率让模型快速收敛
- 标准化的损失计算让训练过程更稳定
内容的提问来源于stack exchange,提问作者DrSpill
相关产品推荐
相关产品推荐

