深度学习基础:神经网络与反向传播

说实话,很多做药物预测的同事一听到“神经网络”就头大。我当年也一样,觉得这玩意儿玄乎得很。后来在 ADMET 毒性预测项目里硬啃了几个月才真正搞懂。说白了,神经网络就是一堆数学函数拼起来的模型。我们来把这层窗户纸捅破。

神经网络的“元件”到底长什么样?

一个神经元的结构,其实特别简单。你想想看,细胞有树突、轴突,人工神经元也有相应的“输入”、“计算”和“输出”。

  • 输入:接收上一层的信号,比如分子指纹或描述符。
  • 加权求和:每个输入都配一个权重,再累加。
  • 激活函数:决定这个神经元是否“兴奋”,引入非线性。
  • 输出:传向下一个层级。

我记得早期做肝毒性预测时,用的就是最朴素的 ReLU 激活函数。很多新手喜欢用 Sigmoid,结果梯度一深就消失,模型训不动。后来我总结了不同激活函数的特性:

激活函数 表达式 适用场景 我在项目中踩过的坑
Sigmoid σ(x)=1/(1+e⁻ˣ) 二分类输出层 隐藏层用这个,梯度一深就死,建议别碰。
ReLU ReLU(x)=max(0,x) 隐藏层首选 我曾经在分子特征稀疏时没调学习率,结果一堆神经元“坏死”。
Tanh tanh(x) 部分强相关场景 范围在[-1,1],但计算量大,小数据不推荐。

前向传播:数据如何流经网络

前向传播没什么神秘感。就是输入从左到右,一层层算下去。对 ADMET 预测来说,输入可能是 2048 维的摩根指纹,经过两三层的全连接,最后输出一个毒性概率。

我个人习惯把前向传播分为三个步骤理解:

  1. 线性变换:计算 z = W·x + b,权重矩阵乘输入加偏置。
  2. 非线性激活:对 z 应用激活函数,比如 ReLU。
  3. 逐层传递:把输出作为下一层的输入,直到输出层。

关键点:前向传播只负责“猜”,不负责“改”。猜得准不准,全靠损失函数来衡量。

反向传播:模型进步的引擎

反向传播是所有深度学习的核心。我刚开始接触时总觉得这东西像黑魔法,后来拆解了 链式法则 才恍然大悟。说白了,就是利用链式法则,从输出层往输入层逐层计算每个权重对损失的贡献。

为什么要从后往前?因为输出层的误差可直接计算,隐藏层的误差需要靠它“反传”回去。举个简单例子,假设损失是 L,权重是 W,我们要算 ∂L/∂W。直接算不现实,但通过链式法则可以一步步拆解。

我的避坑指南:我曾经在肝毒性项目里因为学习率设太大,反向传播一步跨过了最优值,损失直接爆炸。后来我经验就是——从 0.001 开始调,优先用 Adam 优化器。

反向传播的具体流程,我用这张图说明:

神经网络前向传播与反向传播流程图 输入层(分子指纹) 隐藏层 1(ReLU) 隐藏层 2(ReLU) 输出层(毒素概率) 损失函数(交叉熵) 前向传播(实线箭头): 数据从左向右流动,逐层计算输出。 最终得到损失值。 反向传播(虚线箭头): 从输出层往回计算梯度。 利用链式法则逐层更新权重。

梯度下降与权重更新

有了反向传播算出的梯度,接下来就是权重更新。这一步很直白:

# 简单梯度下降更新公式
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,把隐藏层节点适当放大,同样的骨架就能用。

总结一下核心心法:神经网络其实不神秘。前向传播做“猜测”,反向传播做“纠错”,梯度下降做“调整”。三句话记住就够了。我之前做肝毒性预测时,就是靠这个最基础的理解,一步步搭出来的模型。