深度学习回顾:前馈神经网络、反向传播算法、训练与验证流程
说实话,聊分子生成之前,我必须先带你过一遍最基础的东西。
前馈神经网络、反向传播、训练验证——这些听起来有点枯燥对吧?但我做项目时吃过不少亏,发现90%的模型翻车都出在这些「老生常谈」上。
你想想看,如果连前向传播的数学流都理不清,后面探讨Dropout、分子性质预测,那不就是空中楼阁吗?
前馈神经网络:信息是怎么一路「馈」下去的?
前馈神经网络,说白了就是数据从输入层一路传到输出层,中间不回头。每一层对上一层的输出做一次线性变换,再加个非线性激活函数。
我用一个最简单的三层网络来举例:
核心公式链
z[1] = W[1] · x + b[1] # 隐藏层线性变换
a[1] = σ(z[1]) # 隐藏层激活
z[2] = W[2] · a[1] + b[2] # 输出层线性变换
y_hat = softmax(z[2]) # 输出层激活(分类任务)
每个节点其实就干了两件事:先加权求和,再「决定」要不要激活。
有件事我觉得特别重要——激活函数的选择。我个人习惯在分子生成任务中优先用ReLU族,原因很直接:
- ReLU:计算快,能缓解梯度消失。我第一次用ReLU时也纳闷,负值怎么办?后来发现,正是这种「不激活」让网络变得稀疏高效。
- Sigmoid/Tanh:容易饱和。深层网络里用它们,反向传播时梯度基本就传不动了。
嗯,这里要注意:偏置项b经常被人忽略。我在一个分子活性预测项目里,去掉偏置后模型收敛慢了三倍。别小看这个偏移量,它给了网络「不经过原点」的自由度。
| 参数 | 维度 | 物理含义 |
|---|---|---|
| W[l] | n[l] × n[l-1] | 第l层到第l+1层的连接权重 |
| b[l] | n[l] × 1 | 第l层节点的偏置向量 |
| z[l] | n[l] × 1 | 第l层未经激活的线性输出 |
| a[l] | n[l] × 1 | 第l层经过激活后的输出 |
小技巧:做分子生成时,输入层维度通常对应分子指纹或者原子特征数。我习惯先用一个宽一点的隐藏层(比如512维),再慢慢压缩。太窄的隐藏层容易丢失分子结构信息。
反向传播算法:误差是怎么「倒着传」回去的?
前馈搞定了,接下来是训练的核心——反向传播。
反向传播的本质就是链式法则。我刚开始学的时候被各种公式绕晕了,后来自己手推了一遍才算真正理解。
先从损失函数开始。对于分子性质预测这种回归任务,常用的均方误差:
L(y, y_hat) = (1/2) * (y - y_hat)²
目标是让这个L尽可能小。怎么做?梯度下降,更新每一层的参数:
W[l] := W[l] - η * ∂L/∂W[l]
b[l] := b[l] - η * ∂L/∂b[l]
关键在于怎么求∂L/∂W[l]。我手写一下这个过程:
- 先算输出层的误差δ[2] = (a[2] - y) ⊙ σ'(z[2])
- 然后误差往回传:δ[1] = (W[2]ᵀ · δ[2]) ⊙ σ'(z[1])
- 最后梯度:∂L/∂W[l] = δ[l] · (a[l-1])ᵀ
为什么这么设计?
你想想看,反向传播的每一层都在做同一件事——把上一层的误差「分摊」到当前层的每个参数上。分摊的比例就是链式法则里的偏导数。
我曾经犯过一个经典错误:在深层网络里直接用Sigmoid激活,结果训练几个epoch后损失就不动了。检查梯度发现,靠近输入层的参数几乎没更新——梯度在反向传播过程中已经「消失」了。
用代码实现一个简化的反向传播:
import numpy as np
def forward(x, W1, b1, W2, b2):
z1 = np.dot(W1, x) + b1
a1 = np.maximum(0, z1) # ReLU
z2 = np.dot(W2, a1) + b2
a2 = 1 / (1 + np.exp(-z2)) # Sigmoid输出
return a1, a2
def backward(x, y, a1, a2, W2):
m = x.shape[1]
dz2 = a2 - y # 输出层误差
dW2 = (1/m) * np.dot(dz2, a1.T) # 输出层权重梯度
db2 = (1/m) * np.sum(dz2, axis=1, keepdims=True)
dz1 = np.dot(W2.T, dz2) * (a1 > 0) # 隐藏层误差(ReLU导数)
dW1 = (1/m) * np.dot(dz1, x.T)
db1 = (1/m) * np.sum(dz1, axis=1, keepdims=True)
return dW1, db1, dW2, db2
提示:上面的代码里,(a1 > 0)就是ReLU的「导数」——正值梯度为1,非正值梯度为0。这个设计让误差只能通过激活的神经元回传,自然会迫使网络学习到更关键的特征。
训练与验证流程:别让你的模型「死记硬背」
模型能跑了,但怎么知道它好不好?这就来到了训练与验证的环节。
我在验证这块吃过不少亏,所以想重点说说。
标准流程一般是这样的:
- 数据分割:训练集、验证集、测试集。分子生成任务里,我习惯按分子骨架来分,而不是随机分。否则会导致训练集和验证集出现非常相似的分子,验证指标会虚高。
- 一个epoch内:遍历训练集 -> 前向传播 -> 计算损失 -> 反向传播 -> 更新参数
- 每个epoch结束:在验证集上跑一次前向传播,不更新梯度,只计算损失和指标
- 早停:如果验证损失连续几个epoch不降,就停止训练。我在一个分子构象生成项目里,不加早停直接训了200个epoch,结果模型过拟合得一塌糊涂。
避坑指南:我曾经犯过一个低级错误——在训练过程中「偷看」测试集。当时为了调参方便,直接用测试集上的指标来选模型。直到最后提交时发现结果差了一截才反应过来。记住:测试集只能用来做最终评估,一次也不能提前用。
这里有个常见的问题:验证集和测试集的大小怎么定?
我个人习惯:数据量大(百万级)时,98-1-1分割;数据量小(千级)时,80-10-10。做分子生成,数据量通常不大,六四开或七三开比较常见。
为什么需要验证集?
直接点说:防过拟合。你想想看,一个好的模型能完美记住训练集的所有噪声和异常点,但对于新的分子,它是什么表现?验证集就是一面镜子,让你看到模型在未见数据上的真实水平。
训练过程中的典型「健康指标」:
| 现象 | 可能原因 | 怎么办 |
|---|---|---|
| 训练损失不断下降,验证损失上升 | 过拟合 | 加Dropout、加正则化、减小模型容量 |
| 训练损失和验证损失都高 | 欠拟合 | 增大模型、加层、加训练轮数 |
| 训练损失下降慢 | 梯度消失/学习率太小 | 换激活函数、增大学习率 |
| 损失震荡剧烈 | 学习率太大/batch太小 | 减小学习率、增大batch size |
做分子生成的时候,我还会额外监控一个指标——生成分子的可合成性分数。光看损失还不够,有时候损失很低,但生成出来的分子结构全是化学上不合理的。这时候就需要用验证集上的领域知识来把关。
经验补遗:我曾经为了一批分子活性预测的数据,花了三天时间清洗和归一化。结果模型性能提升明显。所以别把训练与验证流程只当作调模型的事——数据处理的质量决定了模型性能的天花板。
整个深度学习训练的流程,用一张图来总结:
你看这个流程,本质上就是一个闭环。前向传播算输出,反向传播调参数,验证集做监控。这里面任何一个环节出问题,整个训练都会跑偏。
总结一下重点:
- 前馈网络就是多层「线性变换 + 非线性激活」的堆叠
- 反向传播本质是链式法则,关键在于误差的逐层回传
- 训练和验证必须严格分离,验证集是你的「体检报告」
- 过拟合、欠拟合、梯度消失——这些老问题依然值得你警惕
嗯,这些基础打牢了,后面谈Dropout、谈分子生成里的各种技巧,你才能理解它们到底在解决什么问题。