深度学习基础:神经网络与反向传播
说实话,很多做药物预测的同事一听到“神经网络”就头大。我当年也一样,觉得这玩意儿玄乎得很。后来在 ADMET 毒性预测项目里硬啃了几个月才真正搞懂。说白了,神经网络就是一堆数学函数拼起来的模型。我们来把这层窗户纸捅破。
神经网络的“元件”到底长什么样?
一个神经元的结构,其实特别简单。你想想看,细胞有树突、轴突,人工神经元也有相应的“输入”、“计算”和“输出”。
- 输入:接收上一层的信号,比如分子指纹或描述符。
- 加权求和:每个输入都配一个权重,再累加。
- 激活函数:决定这个神经元是否“兴奋”,引入非线性。
- 输出:传向下一个层级。
我记得早期做肝毒性预测时,用的就是最朴素的 ReLU 激活函数。很多新手喜欢用 Sigmoid,结果梯度一深就消失,模型训不动。后来我总结了不同激活函数的特性:
| 激活函数 | 表达式 | 适用场景 | 我在项目中踩过的坑 |
|---|---|---|---|
| Sigmoid | σ(x)=1/(1+e⁻ˣ) | 二分类输出层 | 隐藏层用这个,梯度一深就死,建议别碰。 |
| ReLU | ReLU(x)=max(0,x) | 隐藏层首选 | 我曾经在分子特征稀疏时没调学习率,结果一堆神经元“坏死”。 |
| Tanh | tanh(x) | 部分强相关场景 | 范围在[-1,1],但计算量大,小数据不推荐。 |
前向传播:数据如何流经网络
前向传播没什么神秘感。就是输入从左到右,一层层算下去。对 ADMET 预测来说,输入可能是 2048 维的摩根指纹,经过两三层的全连接,最后输出一个毒性概率。
我个人习惯把前向传播分为三个步骤理解:
- 线性变换:计算 z = W·x + b,权重矩阵乘输入加偏置。
- 非线性激活:对 z 应用激活函数,比如 ReLU。
- 逐层传递:把输出作为下一层的输入,直到输出层。
关键点:前向传播只负责“猜”,不负责“改”。猜得准不准,全靠损失函数来衡量。
反向传播:模型进步的引擎
反向传播是所有深度学习的核心。我刚开始接触时总觉得这东西像黑魔法,后来拆解了 链式法则 才恍然大悟。说白了,就是利用链式法则,从输出层往输入层逐层计算每个权重对损失的贡献。
为什么要从后往前?因为输出层的误差可直接计算,隐藏层的误差需要靠它“反传”回去。举个简单例子,假设损失是 L,权重是 W,我们要算 ∂L/∂W。直接算不现实,但通过链式法则可以一步步拆解。
我的避坑指南:我曾经在肝毒性项目里因为学习率设太大,反向传播一步跨过了最优值,损失直接爆炸。后来我经验就是——从 0.001 开始调,优先用 Adam 优化器。
反向传播的具体流程,我用这张图说明:
梯度下降与权重更新
有了反向传播算出的梯度,接下来就是权重更新。这一步很直白:
# 简单梯度下降更新公式
for param in model.parameters():
param -= learning_rate * param.grad
但在实际项目中,我建议别直接写裸循环。都用 PyTorch 了,optimizer 一步搞定:
import torch.optim as optim
model = MyToxicityModel()
optimizer = optim.Adam(model.parameters(), lr=0.001)
# 训练循环内
predictions = model(batch_features)
loss = criterion(predictions, batch_labels)
loss.backward() # 反向传播,自动算梯度
optimizer.step() # 更新权重
optimizer.zero_grad() # 清空梯度,避免累积
这里有个大坑:我刚开始用 PyTorch 时,忘了调 zero_grad(),结果梯度一直在累加,几个 epoch 后模型直接崩了。你想想看,每次反向传播都把新旧梯度加一起,权重更新方向肯定乱套。
损失函数的选择
ADMET 肝毒性预测本质上是 二分类问题(毒性/无毒)。我个人偏爱用 交叉熵损失。它的好处是梯度变化相对平滑,模型收敛比较稳定。
import torch.nn as nn
criterion = nn.BCEWithLogitsLoss() # 内部集成了 Sigmoid + 交叉熵
为什么不用均方误差(MSE)?我之前试过,效果很一般。MSE 在分类问题上梯度太小,收敛速度慢得让人抓狂。交叉熵在梯度计算上直接惩罚“置信度错误”,更适合预测概率的场景。
一个完整的小网络示例
最后,我手写个最简单的全连接网络,覆盖前向传播 + 反向传播。你可以直接跑,体会一下全流程。
import torch
import torch.nn as nn
import torch.optim as optim
class TinyToxModel(nn.Module):
def __init__(self, input_dim=64):
super().__init__()
self.fc1 = nn.Linear(input_dim, 32)
self.relu = nn.ReLU()
self.fc2 = nn.Linear(32, 16)
self.relu2 = nn.ReLU()
self.out = nn.Linear(16, 1) # 输出一个毒性概率
def forward(self, x):
x = self.relu(self.fc1(x))
x = self.relu2(self.fc2(x))
x = self.out(x) # 配合 BCEWithLogitsLoss,不用额外加 Sigmoid
return x
# 模拟一批输入
features = torch.randn(8, 64) # 8 个样本,64 维特征
labels = torch.randint(0, 2, (8, 1)).float()
model = TinyToxModel()
criterion = nn.BCEWithLogitsLoss()
optimizer = optim.SGD(model.parameters(), lr=0.01)
# 训练一步
outputs = model(features) # 前向传播
loss = criterion(outputs, labels)
loss.backward() # 反向传播
optimizer.step() # 更新权重
print("Loss 下降:", loss.item())
这个例子虽然小,但五脏俱全。你换到真实的 ADMET 分子数据集上,把输入维度改成 2048,把隐藏层节点适当放大,同样的骨架就能用。
总结一下核心心法:神经网络其实不神秘。前向传播做“猜测”,反向传播做“纠错”,梯度下降做“调整”。三句话记住就够了。我之前做肝毒性预测时,就是靠这个最基础的理解,一步步搭出来的模型。