020、深度学习入门:神经网络基础与反向传播
昨天调一个三层的全连接网络,loss死活不降。打印梯度发现第一层的权重全是零——反向传播根本没传过去。同事凑过来看了一眼:“你激活函数梯度写错了吧?”一查代码,果然在tanh求导的地方少了个平方。这种低级错误让我想起刚入门时,反向传播的链式法则绕得人头晕。今天咱们就掰开揉碎讲清楚,神经网络到底是怎么“学习”的。
从实际问题切入:梯度去哪了?
假设你在训练一个简单的分类网络,结构是输入层(784) -> 隐藏层(128) -> 输出层(10)。前向推理代码大概长这样:
def forward(x):
z1 = x @ w1 + b1 # 第一层线性变换
a1 = np.tanh(z1) # 激活函数
z2 = a1 @ w2 + b2
output = softmax(z2)
return output
前向传播容易理解,就是数据一层层流过去。问题出在反向传播:模型输出和真实标签的误差,怎么反过来告诉每一层的权重该往哪个方向调整?
反向传播:链式法则的工程实现
反向传播的核心是链式求导。但工程实现时,我们通常从后往前计算“局部梯度”。以两层网络为例,损失函数L对w1的梯度要经过三层传递:
∂L/∂w1 = ∂L/∂z2 · ∂z2/∂a1 · ∂a1/∂z1 · ∂z1/∂w1
实际编程时,我们不会真的展开这个式子,而是用计算图的思想逐层回传。下面这个反向传播的实现很典型:
def backward(x, y, output):
# 输出层梯度
dz2 = output - y # softmax交叉熵的梯度就是这个简洁形式,记住能省事
# 第二层权重梯度
dw2 = a1.T @ dz2 # 这里形状要对齐,转置经常忘
db2 = np.sum(dz2, axis=0)
# 往第一层回传
da1 = dz2 @ w2.T # 注意这里w2要转置,维度匹配容易出错
dz1 = da1 * (1 - a1**2) # tanh的导数!我当初就是这里少写了平方
# 第一层权重梯度
dw1 = x.T @ dz1
db1 = np.sum(dz1, axis=0)
return dw1, db1, dw2, db2
注意看第11行,tanh的导数是1 - tanh(x)^2。如果你写成1 - tanh(x),梯度值会小很多,前面层的权重几乎更新不动。这就是开头说的bug来源。
激活函数选型:别只看准确率
新手常纠结用什么激活函数。ReLU确实流行,但死亡ReLU问题在嵌入式设备上很头疼——某些神经元一旦输出全零就再也激活不了。我的经验是:
- 中间层用LeakyReLU,给负值留个0.01的斜率,避免死亡
- 二分类输出层用sigmoid,多分类用softmax,回归任务用线性层
- tanh在RNN里还行,但深度网络容易梯度消失
# 激活函数实现别写太复杂
def leaky_relu(x, alpha=0.01):
return np.where(x > 0, x, x * alpha) # 向量化操作,比for循环快几十倍
# 反向传播时梯度要对应
def leaky_relu_grad(x, alpha=0.01):
dx = np.ones_like(x)
dx[x < 0] = alpha # 小于0的位置梯度为alpha
return dx
初始化权重:不是小事
权重初始化不对,网络可能一开始就“死”了。比如全用零初始化,所有神经元输出相同,梯度也相同,相当于一个神经元。常用的Xavier初始化实现起来很简单:
def init_weights(input_size, output_size):
limit = np.sqrt(6 / (input_size + output_size))
return np.random.uniform(-limit, limit, (input_size, output_size))
但要注意,Xavier假设激活函数是线性的,对ReLU系激活函数效果打折扣。这时用He初始化(方差为2/n)更合适。
调试技巧:梯度检查
当你自己实现反向传播时,一定要做梯度检查。用数值梯度验证解析梯度:
def grad_check(layer_func, x, w, epsilon=1e-7):
grad_analytic = backward(x, w) # 你的反向传播结果
grad_numeric = np.zeros_like(w)
for i in range(w.size):
w_plus = w.copy()
w_minus = w.copy()
w_plus.flat[i] += epsilon
w_minus.flat[i] -= epsilon
loss_plus = layer_func(x, w_plus)
loss_minus = layer_func(x, w_minus)
grad_numeric.flat[i] = (loss_plus - loss_minus) / (2 * epsilon)
diff = np.linalg.norm(grad_analytic - grad_numeric) / np.linalg.norm(grad_analytic + grad_numeric)
return diff < 1e-7 # 小于这个阈值基本就对了
这个操作计算量大,训练时别用,但验证新网络结构时能救命。
个人经验:别重复造轮子,但要懂轮子
现在深度学习框架很成熟,TensorFlow/PyTorch一行代码就搞定反向传播。但我建议初学者至少手写一次全连接网络的反向传播,包括矩阵维度变换、激活函数求导、损失函数求导。这个过程能让你真正理解:
- 梯度消失/爆炸是怎么发生的(试试点乘操作连乘十次)
- 批量训练时梯度怎么平均(注意axis参数)
- dropout在反向传播时要做什么(mask要记住)
实际项目中,我通常用框架搭原型,但遇到性能瓶颈或部署到嵌入式设备时,经常要拆开网络手动优化。这时候对反向传播的理解就值钱了。
最后给个实用建议:把常用层的梯度公式写成小抄贴在显示器边上,比如卷积层的im2col梯度、LSTM的梯度流。调试时对照着看,能省下大量瞎猜的时间。神经网络训练不收敛时,先检查梯度,再调学习率,最后怀疑数据——这个顺序能帮你少走弯路。
更多推荐
所有评论(0)