多层感知机判断氨基酸亲疏水性(PyTorch版)

最近我做了一个很小的机器学习项目:用 MLP 预测 20 种标准氨基酸的亲疏水性。

这个项目的数据量很小,模型也不复杂,但它刚好覆盖了一个机器学习实验最重要的几个环节:

  • 原始数据如何变成模型能理解的数值特征
  • 神经网络如何通过损失函数和优化器学习
  • 小数据集为什么容易过拟合
  • 为什么评估方式比单次准确率更重要
  • 为什么要做 baseline 和正则化

项目地址中的核心流程是:

rust 复制代码
SMILES 字符串 -> RDKit 解析 -> Morgan 指纹 -> MLP 分类 -> 留一交叉验证

项目目标

给定一个氨基酸的分子结构,用机器学习模型预测它是疏水还是亲水。

数据文件中每一行是一种氨基酸,例如:

scss 复制代码
name,abbreviation,three_letter,smiles,hydrophobic
丙氨酸,Ala,Ala,CC(N)C(=O)O,1
丝氨酸,Ser,Ser,NC(CO)C(=O)O,0

其中:

  • smiles 是氨基酸的分子结构表示
  • hydrophobic 是标签,1 表示疏水,0 表示亲水

这是一个二分类问题。

为什么不能直接把 SMILES 喂给神经网络

SMILES 是一种文本形式的分子表示,例如丙氨酸的 SMILES 是:

scss 复制代码
CC(N)C(=O)O

但是神经网络本质上处理的是数值张量。它并不直接理解 CNO 这些字符代表什么化学意义。

所以第一步要做特征工程:把 SMILES 转换成模型可以学习的数值向量。

在这个项目中,我使用 RDKit 生成 Morgan 指纹:

python 复制代码
def smiles_to_morgan_bits(
    smiles: str,
    radius: int = 2,
    fp_size: int = 2048,
) -> torch.Tensor:
    mol = Chem.MolFromSmiles(smiles)
    if mol is None:
        raise ValueError(f"Invalid SMILES: {smiles}")
​
    generator = AllChem.GetMorganGenerator(radius=radius, fpSize=fp_size)
    fingerprint = generator.GetFingerprint(mol)
    return torch.tensor(
        list(map(int, fingerprint.ToBitString())),
        dtype=torch.float32,
    )

Morgan 指纹可以理解为:把分子中的局部结构模式编码成一个固定长度的 0/1 向量。

比如原始输入是:

scss 复制代码
CC(N)C(=O)O

经过特征工程后变成类似这样的向量:

csharp 复制代码
[0, 1, 0, 0, 1, ..., 0]

在这个项目中,每个氨基酸最终都会变成一个 2048 维的向量。

模型结构

模型使用的是一个很小的 MLP:

rust 复制代码
Input (2048) -> Linear -> ReLU -> Dropout -> Linear -> Output (1)

对应代码:

ini 复制代码
model = HydroMLP(in_dim=2048, hidden_layer_sizes=(32,), dropout=0.1)

这里有一个重要选择:模型没有设计得很大。

原因是数据只有 20 条,而输入特征却有 2048 维。如果模型太大,它很容易把训练集记住,而不是真的学到亲疏水性的规律。这就是过拟合。

所以我做了两个约束:

  • 使用较小的隐藏层:2048 -> 32 -> 1
  • 加入正则化:Dropoutweight_decay

训练时使用:

ini 复制代码
criterion = nn.BCEWithLogitsLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.003, weight_decay=1e-3)

BCEWithLogitsLoss 适合二分类任务。它内部会把模型输出的 logit 转换成概率,再计算二分类交叉熵。

weight_decay 是 L2 正则化,可以限制模型参数不要变得过大,从而降低过拟合风险。

为什么使用留一交叉验证

一开始我用的是普通的训练集/测试集划分,例如 15 条训练、5 条测试。

但这个项目只有 20 条数据。测试集只有 5 条时,结果非常容易受随机划分影响。某一次准确率高,不一定说明模型真的好;某一次准确率低,也不一定说明模型完全没学到东西。

因此我改成了留一交叉验证。

留一交叉验证的做法是:

  1. 每次拿 1 个氨基酸作为测试样本
  2. 剩下 19 个氨基酸作为训练样本
  3. 重复 20 次,让每个氨基酸都当一次测试样本
  4. 汇总 20 次预测结果,计算总体准确率

代码中的核心逻辑是:

scss 复制代码
for test_idx in range(len(X)):
    train_idx = [i for i in range(len(X)) if i != test_idx]
​
    model = train_model(X[train_idx], y[train_idx])
    model.eval()
​
    with torch.no_grad():
        logit = model(X[test_idx].unsqueeze(0))
        prob = torch.sigmoid(logit).item()
        pred = int(prob > 0.5)

对于小数据集来说,留一交叉验证比单次随机划分更适合用来观察模型表现。

如何复现

项目使用 uv 管理依赖。安装依赖后,直接运行主脚本:

arduino 复制代码
uv sync
uv run python main.py

项目结构如下:

css 复制代码
├── data/
│   └── amino_acids.csv
├── src/
│   ├── features.py
│   └── model.py
├── main.py
└── pyproject.toml

其中:

  • data/amino_acids.csv 保存 20 种氨基酸的数据和标签
  • src/features.py 负责把 SMILES 转换成 Morgan 指纹
  • src/model.py 定义 MLP 模型
  • main.py 负责训练、留一交叉验证和结果输出

实验结果

当前运行结果是:

makefile 复制代码
留一交叉验证准确率: 65.0% (13/20)

部分预测结果如下:

复制代码
✓ 丙氨酸: 预测=疏水 (疏水概率 0.70), 真实=疏水
✓ 缬氨酸: 预测=疏水 (疏水概率 0.61), 真实=疏水
✗ 亮氨酸: 预测=亲水 (疏水概率 0.31), 真实=疏水
✓ 丝氨酸: 预测=亲水 (疏水概率 0.29), 真实=亲水
✗ 酪氨酸: 预测=疏水 (疏水概率 0.97), 真实=亲水
✓ 精氨酸: 预测=亲水 (疏水概率 0.02), 真实=亲水

这个结果说明模型确实学到了一部分规律,但还不稳定。

比如它能识别一些明显的亲水氨基酸,也能识别一部分疏水氨基酸。但对于边界比较模糊,或者结构上有特殊基团的氨基酸,仍然容易出错。

这也提醒我:不能只看训练集损失。如果训练损失很低,但留一交叉验证表现一般,那模型很可能只是记住了训练样本。

我从这个项目学到了什么

1. 特征工程是机器学习的入口

模型并不是直接学习 SMILES 字符串,而是学习 Morgan 指纹。

所以这个项目真正的输入不是:

scss 复制代码
CC(N)C(=O)O

而是:

yaml 复制代码
2048 维 Morgan 指纹向量

特征工程决定了模型能看到什么信息。

2. 小数据项目里,评估比训练更重要

只有 20 条数据时,模型很容易把训练集背下来。

如果只看训练损失,很容易得到错误信心。留一交叉验证虽然不能让数据变多,但能让评估更完整。

3. 正则化是在限制模型死记硬背

这个项目里使用了两种正则化方式:

  • Dropout
  • weight_decay

它们的目的不是让模型更复杂,而是让模型更克制。

4. Baseline 很重要

这个项目目前已经有了 MLP,但下一步应该做 baseline。

Baseline 就是一个简单参照模型,用来回答一个问题:

我的复杂模型真的比简单方法更好吗?

可以尝试的 baseline 包括:

  • 多数类 baseline:永远预测数据中数量更多的类别
  • Logistic Regression:使用同样的 Morgan 指纹,但只训练线性分类器
  • RDKit 描述符模型:使用 LogP、分子量、TPSA、氢键供体/受体数量等少量特征

如果一个简单 baseline 就能达到和 MLP 接近的准确率,那说明 MLP 可能并没有带来明显收益。

项目局限

这个项目是一个学习项目,不适合直接当作严肃的化学预测模型。

主要局限有:

  • 数据只有 20 条,远远不够训练稳定模型
  • 亲疏水性本身不是绝对二分类,不同教材或标度可能会有不同划分
  • Morgan 指纹只是一种特征表示,可能没有捕捉到所有与亲疏水性相关的信息
  • 没有和 baseline 模型做系统比较
  • 没有调参实验,也没有更多评价指标

这些局限并不代表项目没有价值。相反,它们正好说明了机器学习实验中最重要的一点:模型结果必须结合数据、特征、评估方式一起解释。

后续可以继续做什么

后续我想从三个方向继续优化:

  1. 增加 baseline 用 Logistic Regression、Random Forest 或多数类预测作为对照。
  2. 尝试 RDKit 分子描述符 不只使用 Morgan 指纹,还可以加入 LogP、TPSA、分子量、氢键供体和受体数量等更直观的化学特征。
  3. 输出更多评价指标 除了 accuracy,还可以看 confusion matrix、precision、recall,观察模型到底更容易把哪一类预测错。

总结

这个项目很小,但它让我完整走了一遍机器学习实验流程:

rust 复制代码
数据 -> 特征工程 -> 模型 -> 训练 -> 正则化 -> 交叉验证 -> 结果解释

对我来说,这个项目最重要的收获不是准确率有多高,而是开始理解:

  • 模型只能学习它看到的特征
  • 训练集表现好不代表泛化能力强
  • 小数据集更需要谨慎评估
  • baseline 是判断模型价值的参照物
  • 机器学习结果需要被解释,而不是只被展示

这也是我觉得这个项目适合作为机器学习入门练习的原因:它不大,但关键概念都在里面。

项目地址:gitee.com/o_insist/mp...

相关推荐
AICAT1 小时前
让主题模型“心领神会”:GCTM-OT如何用目标提示与最优传输终结跑偏话题
人工智能
数字时代全景窗1 小时前
数字的长征:从蒸汽机到智能体——可计算化革命的底层演进脉络
人工智能·架构·软件工程
LinDaiDai_霖呆呆1 小时前
大白话介绍大模型的一些底层原理,看完终于能跟人聊两句了
前端·人工智能·面试
workflower1 小时前
从拿订单到看方向
大数据·人工智能·设计模式·机器人·动态规划
蜘蛛小助理1 小时前
HR 效率神器:零代码搭建招聘 + 考勤 + 薪酬一体化管理系统
人工智能·ai·人事管理·hr·多维表格·蜘蛛表格
数智化管理手记2 小时前
设备总停机?找准根源+TPM核心逻辑,筑牢零故障基础
数据库·人工智能·低代码·制造
青山师2 小时前
【AI热点资讯】5月10日AI热点:Cloudflare裁员1100人、Musk庭审第二周回顾、OpenAI发布Codex Chrome插件
前端·人工智能·chrome·ai·ai热点
长亭外的少年2 小时前
从 Prompt 到工程体系:如何真正把 AI 用进软件开发
人工智能·prompt
zhangshuang-peta2 小时前
MCP + OpenClaw:执行框架如何被“约束成系统”
数据库·人工智能·ai·ai agent·mcp·peta