You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

多输出神经网络反向传播问题:Loss不下降的排查与求解

问题描述

参考教程搭建了一个多输出神经网络,设计如下:

  • 输入为128*128的数字图像数组
  • 1个包含10个神经元的隐藏层
  • 输出层含10个神经元,第i个对应检测数字i+1(第一个对应1,第二个对应2,以此类推)

训练时Loss始终不下降,存在两个核心疑问:

  1. 处理数字1的图像时,通过dL/dy * dy/dh * dh/dw计算第一层权重的dL/dw,处理数字2的图像时,是否会破坏之前的计算?
  2. 多输出场景下应如何正确计算Loss?

附Loss曲线:
Loss曲线

以下是实现代码:

from PIL import Image
import numpy as np
import os
from glob import glob
import random


path = 'D:\Digits'

L=128
H=10
s=0.001
k=44

imgs=[] # all my dataset images
for root, dirs, files in os.walk(path):
    for file in files:
        if file.endswith('.png'):
            imgs.append(os.path.join(root, file))


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

def deriv(x):
  fx = sig(x)
  return fx * (1 - fx)

def loss(y_true, y_pred):
  return ((y_true - y_pred) ** 2).mean()

class NeuralNetwork:
  def __init__(self):
      
    self.wsh=[]
    for i in range (H):
        self.wsh.append(np.random.rand(L**2))
    self.wsh=np.asarray(self.wsh)

    self.wso=[]
    for i in range (H):
        self.wso.append(np.random.rand(H))
    self.wso=np.asarray(self.wso)

    self.hs=np.random.rand(H) 
    self.bsh=np.random.rand(H) 
    self.bso=np.random.rand(H) 
    self.os=np.random.rand(H)
    self.dhdw=[]
    for i in range (H):
           self.dhdw.append(np.random.rand(L**2))
    self.dhdw=np.asarray(self.dhdw)
      
    self.dwh=[]
    for i in range (H):
           self.dwh.append(np.random.rand(L**2))
    self.dwh=np.asarray(self.dwh)

  def forward(self, x, fold):
    for i in range (H):  
        self.hs[i]=sig(np.dot(self.wsh[i],x))
    for i in range (H):
        self.os[i]=sig(np.dot(self.wso[i],self.hs))
    y_true=np.zeros(H)
    y_true[fold]=1
    print (loss(y_true,self.os))
    f = open('D:\Питон\loss.txt','a')  
    f.write(str(loss(y_true,self.os))+'\n')  
    f.close()  


  def train(self,x,j)  :
    self.dydh=np.random.rand(H)
    self.dydw=np.random.rand(H)
    self.dwo=np.random.rand(H)
      
    self.dhdb=[]
    self.dhdb=np.random.rand(H)
    
    self.dydb=[]
    self.dydb=np.random.rand(H)

    self.dbh=[]
    self.dbh=np.random.rand(H)


    dL = -2*(1-self.os[j])
    for k in range (H):
        self.dydh [k]= self.wso[j][k] * deriv(np.dot(self.wso[k],self.hs))
    #print ('dydh done')    
    for t in range (H):
        t1=deriv(np.dot(self.wsh[t],x))
        for g in range (L**2):
           self.dhdw [t][g]= x[g]* t1 #deriv(np.dot(self.wsh[t],x))
        #print ('dhdw - ',t)              
    #print ('dhdw done')           
    for t in range (H):
        t2=s * dL * self.dydh [t]
        for g in range (L**2):
            self.dwh[t][g] = t2 * self.dhdw [t][g]
            self.wsh[t][g] -= self.dwh[t][g]
    #print ('wsh done')            
    for t in range (H):
        self.dydw[t] = self.hs[t] * deriv (np.dot(self.hs,self.wso[t]))
    #print ('dydw done')        

    for t in range (H):
        self.dwo[t] = s * dL * self.dydw [t]
        self.wso[j][t] -= self.dwo[t]
    #print ('wso done')

    
    for t in range (H):
        self.dhdb[t] = deriv (np.dot(self.wsh[t],x))
        self.bsh[t] = self.bsh[t] - s * dL * self.dydh[t] * self.dhdb[t]
    
    for t in range (H):
        self.dydb[t] = deriv (np.dot(self.wso[t],self.hs))
        self.bso[t] = self.bso[t] - s * dL * self.dydb[t] 
            
    
N=NeuralNetwork()

#read the weights from the past
N.wsh = np.loadtxt("D:\Питон\\wsh"+str(k-1)+".npy")
N.wso = np.loadtxt("D:\Питон\\wso"+str(k-1)+".npy")
N.bsh = np.loadtxt("D:\Питон\\bsh"+str(k-1)+".npy")
N.bso = np.loadtxt("D:\Питон\\bso"+str(k-1)+".npy")


for i in range (100):
   
    file=imgs[random.randint(0,len(imgs))]
    fold=int(file[10])

    img =Image.open(file) 
    img=np.asarray(img)
    img = [element for row in img for element in row]


    #print(str(k),"-ая эпоха")
    N.forward(img,fold)
    
    N.train(img,fold)
    np.savetxt("D:\Питон\\wsh"+str(k)+".npy",N.wsh)
    np.savetxt("D:\Питон\\wso"+str(k)+".npy",N.wso)
    np.savetxt("D:\Питон\\bsh"+str(k)+".npy",N.bsh)
    np.savetxt("D:\Питон\\bso"+str(k)+".npy",N.bso)
    
    k+=1    

问题分析与解决方案

疑问解答

1. 不同样本的梯度计算是否会互相破坏?

不会。随机梯度下降(SGD)的核心就是每个样本计算梯度后更新权重,处理下一个样本时,新梯度基于当前权重计算,属于正常迭代流程,不存在“破坏之前计算”的问题。你的问题出在梯度计算逻辑错误,而非样本间干扰。

2. 多输出场景下Loss的正确计算方式

你当前用的MSE(均方误差)是可行的,多输出场景下需计算所有输出节点与对应真实值的误差平方的平均值,你当前的loss函数实现正确,但反向传播时需基于整个Loss的梯度,而非单个输出节点的局部梯度。

Loss不下降的核心原因排查

1. 前向传播遗漏偏置项

forward方法中完全没用到偏置bsh和bso,导致网络表达能力严重受限:

# 错误写法:无偏置
self.hs[i]=sig(np.dot(self.wsh[i],x))
self.os[i]=sig(np.dot(self.wso[i],self.hs))

# 正确写法:加入对应偏置
self.hs[i]=sig(np.dot(self.wsh[i],x) + self.bsh[i])
self.os[i]=sig(np.dot(self.wso[i],self.hs) + self.bso[i])

2. 反向传播梯度计算错误

(1)输出层梯度推导错误

你仅计算了目标输出节点j的梯度,且dL = -2*(1-self.os[j])仅适用于y_true_j=1的场景,逻辑不通用;同时链式求导的索引对应关系完全错误,导致梯度更新方向偏离正确方向。

(2)隐藏层梯度逻辑混乱

dydh、dhdw等变量的索引匹配错误,梯度计算完全不符合链式求导规则。

3. 初始化与数据预处理问题

  • 权重初始化不当:用np.random.rand生成0-1权重,对于16384维度的输入,会导致sigmoid进入饱和区(输出接近0或1),导数趋近于0,引发梯度消失。建议用正态分布权重并缩放:np.random.randn(H, L**2) / np.sqrt(L**2)。
  • 图像未归一化:像素值0-255直接输入会导致加权和过大,同样引发sigmoid饱和,需归一化到0-1区间:img = np.asarray(img) / 255.0。

4. 训练流程问题

  • 学习率s=0.001对于高维度输入可能过小,导致权重更新缓慢;可尝试调整为0.01或0.0001测试。
  • 训练轮次仅100次,远不足以收敛;建议增加到数千轮,且每个epoch打乱数据集。

修正后的核心代码示例

修正前向传播

def forward(self, x, fold):
    # 计算隐藏层:加入偏置
    for i in range(H):
        self.hs[i] = sig(np.dot(self.wsh[i], x) + self.bsh[i])
    # 计算输出层:加入偏置
    for i in range(H):
        self.os[i] = sig(np.dot(self.wso[i], self.hs) + self.bso[i])
    # 生成真实标签
    y_true = np.zeros(H)
    y_true[fold] = 1
    current_loss = loss(y_true, self.os)
    print(current_loss)
    with open('D:\Питон\loss.txt','a') as f:
        f.write(f"{current_loss}\n")
    return y_true  # 返回真实标签给train方法使用

修正反向传播

def train(self, x, y_true):
    # 计算输出层误差
    output_errors = self.os - y_true
    # 输出层激活导数
    os_deriv = deriv(np.dot(self.wso, self.hs) + self.bso)
    # 输出层权重与偏置梯度
    d_wso = np.outer(output_errors * os_deriv, self.hs)
    d_bso = output_errors * os_deriv

    # 计算隐藏层误差
    hidden_errors = np.dot(self.wso.T, output_errors * os_deriv)
    # 隐藏层激活导数
    hs_deriv = deriv(np.dot(self.wsh, x) + self.bsh)
    # 隐藏层权重与偏置梯度
    d_wsh = np.outer(hidden_errors * hs_deriv, x)
    d_bsh = hidden_errors * hs_deriv

    # 更新权重与偏置
    self.wsh -= s * d_wsh
    self.bsh -= s * d_bsh
    self.wso -= s * d_wso
    self.bso -= s * d_bso

修正初始化与数据处理

def __init__(self):
    # 权重初始化:正态分布缩放,避免饱和
    self.wsh = np.random.randn(H, L**2) / np.sqrt(L**2)
    self.wso = np.random.randn(H, H) / np.sqrt(H)
    self.bsh = np.zeros(H)  # 偏置初始化为0更稳定
    self.bso = np.zeros(H)
    self.hs = np.zeros(H)
    self.os = np.zeros(H)

# 图像数据处理
img = Image.open(file)
img = np.asarray(img) / 255.0  # 归一化
img = img.flatten()  # 扁平化替代手动列表推导

总结

  1. 优先修复前向传播的偏置遗漏问题,这是最基础的错误。
  2. 修正反向传播的梯度计算逻辑,确保链式求导每一步对应正确。
  3. 做好权重初始化和数据归一化,避免梯度消失。
  4. 调整学习率和训练轮次,优化训练流程。

内容的提问来源于stack exchange,提问作者Алексей Васильев

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.05 00:44:57