第4章 深度学习基础:神经网络核心要素

深度学习在QSAR里到底怎么用?说白了,就是搭一个多层神经网络,让它在分子结构和活性之间找到映射关系。这一章我就把几个最核心的组件掰开揉碎讲清楚——网络结构长什么样、激活函数怎么选、损失函数干什么用、反向传播怎么算梯度、优化器怎么更新参数。这些东西你搞明白了,后面搭多任务模型才不会懵。

4.1 神经网络结构——搭积木的基本单元

神经网络本质上就是个多层函数嵌套。每一层把输入做一次线性变换,再套个非线性激活,然后传给下一层。最基本的单元就是神经元,也叫节点。

一个典型的全连接层长这样:

输出 = 激活函数( 权重 × 输入 + 偏置 )

我刚开始做QSAR项目时,以为层数越多效果一定越好,结果在hERG毒性预测任务上训了一个8层网络,过拟合得一塌糊涂。后来才明白,对于几千个分子样本,3~4层往往就够了。

实用建议:对于典型的QSAR数据集(几百到几千个分子),建议从2~3个隐藏层开始试,每层64~256个神经元。优先调数据质量,别盲目堆层数。

4.2 激活函数——给网络注入非线性

如果没有激活函数,多层线性变换等价于一层,你再怎么堆层都白搭。激活函数就是用来打破线性限制的。

激活函数公式适用场景我踩过的坑
Sigmoid1 / (1 + e-x)二分类输出层隐藏层用Sigmoid,梯度在两端几乎为0,深层根本训不动
Tanh(ex - e-x) / (ex + e-x)隐藏层(零中心)比Sigmoid好一点,但仍有梯度饱和问题
ReLUmax(0, x)隐藏层(默认首选)学习率太大时神经元会死掉,输出始终是0
Leaky ReLUmax(αx, x), α=0.01隐藏层(防神经元死亡)α太小跟ReLU没区别,太大又丢失非线性
Softmaxex_i / Σex_j多分类输出层数值不稳定时加个logits稳定版实现

我个人习惯,隐藏层无脑先用ReLU,如果出现大量死亡神经元,换成Leaky ReLU或ELU。输出层则根据任务选——回归用线性,二分类用Sigmoid,多分类用Softmax。

千万注意:Sigmoid和Tanh在深层网络中容易引发梯度消失。你如果搭超过5层的网络,隐藏层就别用它们了。我曾经因为这个在ADME毒性多任务模型上白费了三天时间调参,最后换成ReLU一步到位。

4.3 损失函数——告诉模型错在哪

损失函数度量模型预测和真实标签之间的差距。你想想看,模型靠这个信号来调整自己,所以选错损失函数,方向就错了。

QSAR里最常见的场景:

  • 回归任务(预测pIC50、logP等连续值):均方误差(MSE)或平均绝对误差(MAE)。
  • 二分类任务(活性/非活性、毒性/无毒):二分类交叉熵(Binary Cross-Entropy)。
  • 多分类任务(激酶抑制类型等):多分类交叉熵(Categorical Cross-Entropy)。
  • 多任务场景:各任务损失加权求和,权重可以自适应学习。
# PyTorch中常见的损失函数
import torch.nn as nn

# 回归
mse_loss = nn.MSELoss()          # 均方误差
mae_loss = nn.L1Loss()           # 平均绝对误差

# 分类
bce_loss = nn.BCEWithLogitsLoss()      # 二分类(自带sigmoid)
ce_loss = nn.CrossEntropyLoss()        # 多分类(自带softmax)

我在做多任务学习时,经常遇到不同任务损失尺度差几个数量级。比如一个任务是回归pIC50(MSE大约在0.5~2.0),另一个是二分类毒性(交叉熵大约在0.1~0.5)。如果不做加权平衡,大尺度任务会主导梯度。

多任务损失平衡小技巧:可以用不确定性加权法(Kendall et al., 2018)让网络自己学每个任务的噪声参数,自动调节损失权重。我个人试下来效果很稳。

4.4 反向传播与梯度计算——神经网络的学习引擎

反向传播就是用链式法则从输出层往输入层逐层求梯度。你可以理解为:误差从输出端一路往回传,每经过一层就把梯度分配给该层的参数。

为什么会这样?因为前向传播是复合函数,求导就得用链式法则一层层拆。有了梯度,参数才能朝减小损失的方向更新。

我曾经在训一个7层的分子性质预测网络时,发现前几层的梯度几乎是0,验证集loss死活不降。折腾了两天才意识到是梯度消失了——隐藏层用了Sigmoid,叠加后梯度在0附近,根本传不回去。

调试技巧:训练时定期打印每层梯度的均值和标准差。如果发现前面几层梯度远小于后面几层,就是梯度消失了;如果梯度爆炸了(数值超过1e5),就要考虑梯度裁剪或调整学习率。
# PyTorch中查看每层梯度的示例
for name, param in model.named_parameters():
    if param.grad is not None:
        print(f"{name}: grad_mean={param.grad.mean().item():.6f}, "
              f"grad_std={param.grad.std().item():.6f}")

4.5 优化器——控制参数更新的方向盘

有了梯度,怎么用它来更新参数?这就是优化器的事。市面上常见的优化器各有脾气:

优化器核心思路优点缺点
SGD梯度直接乘学习率简单,泛化性好收敛慢,需要精细调学习率
SGD + Momentum累积历史梯度方向加速收敛,越过局部极小多了一个动量超参需要调
Adam自适应学习率 + 动量收敛快,超参鲁棒有时泛化不如SGD
AdamWAdam + 解耦权重衰减比Adam正则化更好相对较新,部分场景未普及

我个人习惯:先用Adam跑50~100个epoch,快速摸到一个不错的区域,然后换SGD + Momentum做微调,提升泛化能力。这个组合在好几个QSAR竞赛里帮我拿到了前排名次。

我的学习率策略:初始学习率设为1e-3(Adam)或1e-2(SGD),配合余弦退火或阶梯式衰减。每10个epoch验证loss不降就降一半学习率,降到1e-6还没收敛就停。

4.6 核心组件协作流程

下面这张图把五个核心组件串起来了。你顺着箭头看一遍,就是一次完整的训练迭代:

深度学习训练迭代核心流程 输入数据(分子描述符) 神经网络结构(前向传播) 激活函数(引入非线性) 损失函数(计算误差) 反向传播(计算梯度) 优化器(更新参数) 收敛?验证集性能达标? 未收敛 → 下一轮迭代 收敛 → 保存模型 循环迭代直到收敛或达到最大epoch

看明白了吧?数据从输入层进入网络,经过每一层的线性变换和非线性激活,到达输出层得到预测值;然后损失函数计算预测值和真实值的差距;反向传播把这个差距的梯度逐层往回传;优化器拿着梯度更新所有参数。一次完整的迭代就这样完成了。

4.7 选型组合与实用建议

讲了这么多,你在QSAR项目里到底怎么配?我直接给一套经过实战检验的默认配置:

  • 网络结构:3层全连接,每层128个神经元,层间加BatchNorm和Dropout(p=0.2~0.3)。
  • 激活函数:隐藏层用ReLU,输出层根据任务定(回归→线性,二分类→Sigmoid)。
  • 损失函数:单任务按任务类型选;多任务用加权和 + 不确定性加权。
  • 优化器:先用Adam(lr=1e-3)跑热身,再切SGD+Momentum(lr=1e-2, momentum=0.9)精调。
  • 学习率调度:CosineAnnealing或ReduceLROnPlateau。
  • 早停:验证集loss连续15个epoch不降就停。
避坑提醒:我曾经在一个溶解度预测任务上直接套用Adam默认参数(lr=1e-3),结果loss震荡了一百多轮都不收敛。后来把学习率降到3e-4,配合学习率预热,20个epoch就稳了。记着——默认参数只是起点,不是终点。

这一章的内容是后面所有多任务模型的基石。你把这些组件吃透了,后面理解多任务学习中的梯度冲突、任务权重平衡、共享底层结构这些东西,就会轻松很多。