2. 深度学习基础:神经网络原理、激活函数、损失函数、优化器

说实话,刚接触深度学习那会儿,我也觉得这东西挺玄乎的。什么神经元、反向传播,听起来像生物课。但干我们ADMET这行的,说白了就是把模型调明白,让分子性质预测得准。所以我习惯把深度学习拆成四个块来理解:网络怎么搭、激活怎么选、损失怎么算、优化怎么跑。一个一个吃透,后面调批次校正才不会懵。

2.1 神经网络原理:从感知机到多层结构

先看个最简单的场景。你要判断一个化合物是不是有毒性,只看分子量肯定不够。但如果你把十几个分子描述符拼起来,扔进一个线性模型,它学不动非线性关系。这时候就需要神经网络的“层”来帮忙。

一个神经元干的事很简单:输入乘权重,加偏置,过激活函数。

z = w1*x1 + w2*x2 + ... + wn*xn + b
a = activation(z)

单个神经元就是逻辑回归。但一旦堆成多层,它的表达能力就上来了。我刚开始做毒性预测时,用了个三层网络,效果确实比XGBoost好一截。但也不是越深越好,我试过七层,反而过拟合严重,训练集跑得很好,验证集直接崩。这就是ADMET场景里常见的坑——数据量小,网络一深就容易出事。

下面这张图把神经网络的核心流程串起来了。你可以看到数据从输入一路流到输出,中间经过隐藏层,再通过损失函数和优化器完成闭环。

神经网络核心流程 输入层 分子描述符 指纹特征 理化参数 隐藏层1 128 神经元 ReLU 隐藏层2 64 神经元 ReLU 输出层 毒性概率 Sigmoid 损失函数 优化器 反向传播 全连接 → 激活 → Dropout 层间信息逐级抽象 最终映射到输出
实战心得:在ADMET数据集上,我建议从2到3个隐藏层开始调。数据量少于1000条时,别急着堆层数。先保证第一层有足够宽度(64~128),再考虑加深。

2.2 激活函数:非线性瓶颈

如果网络全是线性变换,那堆多少层都没用。激活函数就是用来打破线性的。常见的就那几种,各有各的脾气。

激活函数 公式 优点 缺点
Sigmoid σ(z) = 1 / (1 + e-z) 输出在0~1,适合二分类 梯度饱和,容易消失
Tanh tanh(z) = (ez - e-z) / (ez + e-z) 零中心,比Sigmoid好点 还是梯度饱和
ReLU f(z) = max(0, z) 计算快,梯度不饱和 神经元可能"死掉"
Leaky ReLU f(z) = max(αz, z) 解决ReLU死亡问题 α需要调

我自己的经验是:隐藏层无脑用ReLU。我在做批次校正时试过Tanh,训练慢了一倍,效果还没ReLU好。但注意,输出层要根据任务选——二分类用Sigmoid,多分类用Softmax,回归任务就别用激活了,直接线性输出。

避坑提示:我曾经在一个回归任务里给输出层加了ReLU,结果所有预测值都大于0,漏掉了负值的分布。你说合理吗?完全不合适。输出层一定想清楚你预测的是什么范围。

2.3 损失函数:告诉模型错在哪

损失函数就是模型犯错的代价。选对了,模型知道往哪优化;选错了,训练半天也是白费功夫。

ADMET场景里最常用的两个损失函数:

  • 均方误差(MSE):适合回归任务,比如预测logP、溶解度。公式很简单:L = (y - ŷ)²,误差大惩罚重。
  • 交叉熵损失(Cross-Entropy):适合分类任务,比如预测毒性、致癌性。二分类用BCE(Binary Cross-Entropy),多分类用Categorical Cross-Entropy。

你可能会问,什么时候用MAE(平均绝对误差)而不是MSE?我自己的习惯是:如果数据里有明显离群点,MAE更鲁棒,因为MSE会把离群点的误差平方放大。我拿过一个logP数据集,里面有几个合成出错导致的离群值,用MSE训练那叫一个惨,后来换了MAE才稳住。

# PyTorch 里常见的损失函数用法
import torch.nn as nn

# 回归任务
mse_loss = nn.MSELoss()
mae_loss = nn.L1Loss()

# 二分类
bce_loss = nn.BCEWithLogitsLoss()  # 自带Sigmoid,更稳定

# 多分类
ce_loss = nn.CrossEntropyLoss()    # 自带Softmax
关键点:BCEWithLogitsLoss 和 CrossEntropyLoss 内部都集成了激活函数,你输入logits就行,别在外层再加Sigmoid/Softmax。我以前踩过这个坑,数值反复爆炸,查了半天才发现梯度算了两遍。

2.4 优化器:梯度下降的进化史

损失函数告诉我们错在哪,优化器负责怎么修正权重。以前只有SGD(随机梯度下降),但它太慢了,而且容易卡在鞍点。现在主流的是Adam,自适应学习率,基本开箱即用。

优化器 特点 适用场景
SGD 简单,学习率固定 数据足够大、训练时间充裕
SGD + Momentum 加速收敛,缓解震荡 比SGD稳,调参经验多
Adam 自适应学习率,动量+RMSProp结合 大部分场景首选
AdamW Adam + 权重衰减解耦 需要正则化时

我个人的习惯是:新项目默认用Adam,学习率从0.001开始。跑几个epoch看看loss曲线,如果震荡厉害就降到0.0005,如果太慢就提到0.003。但注意,Adam不是万能的。有一次我做一个图像相关的回归任务,Adam在验证集上一直震荡,换成SGD+Momentum反而稳定了。所以,最终你还是要看数据说话。

# 常用优化器配置
import torch.optim as optim

optimizer = optim.Adam(
    model.parameters(),
    lr=0.001,
    betas=(0.9, 0.999),
    weight_decay=1e-5   # 加一点正则化
)

# 如果你需要学习率衰减
scheduler = optim.lr_scheduler.ReduceLROnPlateau(
    optimizer,
    mode='min',
    factor=0.5,
    patience=5
)
个人建议:学习率调度器先用ReduceLROnPlateau,简单有效。当验证集loss连续5个epoch不下降时自动降一半学习率。我几乎所有ADMET项目都是用这个,省心。

2.5 知识结构一览

把这一章的内容拉一个表格,方便你记笔记:

模块 核心要素 常用选择 常见陷阱
神经网络 层数、宽度、连接方式 2~3个隐藏层,64~128个神经元 层数太深=过拟合
激活函数 非线性变换 隐藏层用ReLU,输出层看任务 输出层用错激活
损失函数 衡量预测误差 MSE(回归)、BCE(二分类) 离群点用MSE被放大
优化器 更新权重的策略 Adam(默认),AdamW(正则) 学习率设太大loss震荡

这一块是深度学习最核心的基础。后面讲批次校正的时候,你会发现所有技巧都绕不开这些概念:网络结构决定容量,激活函数决定非线性表达能力,损失函数定义优化方向,优化器控制收敛效率。把这几样东西吃透,调起参来才能有方向。