春江暮客

春江暮客的个人学习分享网站

蛋白质语言模型入门:用 Python 提取 ESM-2 向量

2026-09-20 技术
蛋白质语言模型入门:用 Python 提取 ESM-2 向量

蛋白质序列是一串氨基酸字母,而多数机器学习工具需要数值输入。蛋白质语言模型可以把序列转成向量,用于检索、聚类,或作为监督学习任务的输入特征。

本文用小型 ESM-2 模型在 CPU 上为每条序列提取一个 320 维向量,保存为 JSON,并检查输出形状。你也会看到,为什么向量相似度不能直接证明蛋白质功能。掌握基础 Python 即可,不需要训练模型或开展实验。

1. 蛋白质语言模型学到了什么?

掩码蛋白质语言模型在训练时隐藏部分氨基酸 token,再根据周围序列预测它们。模型的隐藏状态由此表示每个残基及其序列上下文。ESM 使用这类掩码语言建模方法,详见 Transformers 官方 ESM 文档

Embedding(嵌入向量)是一组学习得到的数值特征。某个维度通常不能直接解释成温度、电荷或酶活性。后续任务可以利用这些特征,但向量本身不是测量结果。

先区分三种输出:

任务 输出 还需要检查什么
序列表征 每个残基或每条蛋白质的向量 这些特征是否有助于具体任务
结构预测 预测的三维坐标 置信度及其与独立证据的一致性
蛋白质生成 候选序列,也可能包含其他属性 候选蛋白质是否具有预期行为

本文使用 ESM-2 提取表征。ESMFold 在表征基础上增加结构预测架构;ESM3 是另一个涉及序列、结构和功能的多模态模型。ESM 项目仓库ESM3 论文 分别介绍了这些模型。运行下面的 ESM-2 示例,不会同时运行 ESMFold 或 ESM3。

2. 安装小型 ESM-2 示例

使用 PyTorch 支持的较新 Python 版本,先创建独立环境:

mkdir protein-lm-demo
cd protein-lm-demo
python3 -m venv .venv
. .venv/bin/activate
python -m pip install torch 'transformers==4.57.6'

这里选择 facebook/esm2_t6_8M_UR50D:6 层、约 800 万参数、320 维隐藏状态。模型配置文件 也记录了这些维度。它适合用短序列跑通流程,但本文不把它当作所有任务的最佳选择。

首次运行需要联网下载公开模型文件,之后可复用 Hugging Face 缓存。代码固定了模型版本,默认使用 CPU,这个示例不需要 GPU 或 API key。依赖包和模型文件仍会占用磁盘空间。

3. 把序列转成向量

保存为 protein_embeddings.py

import argparse
import json
from pathlib import Path

import torch
import torch.nn.functional as F
from transformers import AutoTokenizer, EsmModel


MODEL_ID = "facebook/esm2_t6_8M_UR50D"
REVISION = "c731040fcd8d73dceaa04b0a8e6329b345b0f5df"
AMINO_ACIDS = set("ACDEFGHIKLMNPQRSTVWY")
# Artificial strings for a software exercise; no biological function is claimed.
SEQUENCES = ["ACDEFGHIKLMNPQRSTVWY", "MKTAYIAKQRQISFVKSHFSRQ"]


def embed(sequences, tokenizer, model):
    for sequence in sequences:
        if not 1 <= len(sequence) <= 1022:
            raise ValueError("Use sequences of 1-1022 residues; no silent truncation")
        if set(sequence) - AMINO_ACIDS:
            raise ValueError("This demo accepts only the 20 standard uppercase amino acids")
    if not sequences:
        raise ValueError("Provide at least one sequence")

    batch = tokenizer(
        sequences, padding=True, truncation=False,
        return_special_tokens_mask=True, return_tensors="pt",
    )
    special = batch.pop("special_tokens_mask").bool()
    residues = batch["attention_mask"].bool() & ~special
    lengths = residues.sum(dim=1)
    expected = torch.tensor([len(s) for s in sequences])
    if not torch.equal(lengths, expected):
        raise ValueError("Token counts do not match residue counts")

    with torch.inference_mode():
        hidden = model(**batch).last_hidden_state
        weights = residues.unsqueeze(-1).to(hidden.dtype)
        pooled = (hidden * weights).sum(dim=1) / lengths.unsqueeze(-1)
    if not torch.isfinite(pooled).all():
        raise ValueError("Non-finite embedding values")
    return pooled, hidden.shape, lengths.tolist()


def main():
    parser = argparse.ArgumentParser(description="Extract ESM-2 protein embeddings on CPU")
    parser.add_argument("--output", type=Path, default=Path("embeddings.json"))
    args = parser.parse_args()
    if args.output.exists():
        parser.error("Output exists; choose another --output path")

    tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, revision=REVISION)
    model = EsmModel.from_pretrained(
        MODEL_ID, revision=REVISION, add_pooling_layer=False,
    )
    model.eval()
    vectors, token_shape, lengths = embed(SEQUENCES, tokenizer, model)
    similarity = F.cosine_similarity(vectors[0:1], vectors[1:2]).item()
    result = {
        "model": MODEL_ID,
        "revision": REVISION,
        "pooling": "last-layer mean over residue tokens only",
        "sequences": SEQUENCES,
        "embeddings": vectors.tolist(),
    }
    with args.output.open("x", encoding="utf-8") as output:
        json.dump(result, output, indent=2, allow_nan=False)
        output.write("\n")
    print("Residue counts:", lengths)
    print("Token tensor:", tuple(token_shape))
    print("Protein tensor:", tuple(vectors.shape))
    print(f"Cosine similarity (not a probability): {similarity:.6f}")
    print(f"Saved {args.output}")


if __name__ == "__main__":
    main()

输入中的两条字符串仅用于软件演示,不是评测数据,也不表示它们具有真实的蛋白质活性。确认示例可运行后,可把 SEQUENCES 换成自己的氨基酸序列。

脚本只接受 20 种标准氨基酸的大写字母。真实数据可能包含不确定或非标准残基,扩展输入规则之前,应先确定处理方式。不要把 FASTA 标题行、核酸序列、比对缺口或终止符直接放入列表。只含 A、C、G、T 的 DNA 字符串也会通过字符检查,因此还必须核实原始数据类型。

1,022 个残基是本示例采用的保守上限,不是所有蛋白质模型的通用限制。超长输入会被拒绝,避免静默截断。处理大数据集时,把长度接近的序列分成小批次;每批最长的序列会决定需要填充多少位置。

为什么平均之前要先过滤 token?

分词器会添加特殊 token,并给较短序列补齐长度。attention_mask 标记有效输入位置,但其中仍包含特殊 token。代码再结合 special_tokens_mask,让均值只覆盖氨基酸位置。

最后一层隐藏状态的形状是 (批次, token 位置, 隐藏维度)。对残基位置取均值后,输出变成 (批次, 隐藏维度)add_pooling_layer=False 关闭未使用的 pooler;脚本自行完成明确的均值池化。

均值池化适合作为基线,但会把整条序列压缩成一个向量,可能掩盖局部差异。如果任务要求逐位置预测,应保留残基级表征。这里使用的隐藏状态输出见 ESM API 文档

4. 运行并检查结果

python protein_embeddings.py --help
python protein_embeddings.py --output embeddings.json
python -m json.tool embeddings.json > /dev/null
python -m pip freeze > requirements-tested.txt

与形状相关的输出应为:

Residue counts: [20, 22]
Token tensor: (2, 24, 320)
Protein tensor: (2, 320)

较长字符串有 22 个残基,加上两个特殊 token,共 24 个位置。较短字符串会被填充到相同长度。脚本还会打印余弦相似度,并保存向量、输入序列、模型版本和池化方式。

独立检查保存的文件:

python - <<'PYTHON'
import json
import math

with open("embeddings.json", encoding="utf-8") as source:
    result = json.load(source)
vectors = result["embeddings"]
assert len(vectors) == len(result["sequences"]) == 2
assert all(len(vector) == 320 for vector in vectors)
assert all(math.isfinite(x) for vector in vectors for x in vector)
print("Validated: 2 protein embeddings, 320 finite values each")
PYTHON

本文在 CPU 上使用 Python 3.14.7、PyTorch 2.14.0、Transformers 4.57.6 和上述固定模型版本运行了示例。输出形状与上面一致,两条人工输入的余弦相似度为 0.937591。这个较高的数值恰好提醒我们:输入没有功能标签,不能凭这个分数推断功能相同。不同环境可能出现小幅数值差异。

脚本不会覆盖已有输出文件。再次运行时,用 --output 指定新路径。如果需要记录依赖版本,把 requirements-tested.txt 和 JSON 一起保存。

5. 如何使用向量,又不误读结果?

余弦相似度比较的是向量方向。它不是概率,不是序列比对的一致性百分比,也不能证明两个蛋白质功能相同。对于本示例的均值向量,没有一个通用阈值可以直接确认生物学关系。

2026 年的一项 Nature Methods 蛋白质表征不确定性研究 考察了不同模型和序列类型的嵌入表现。对本教程而言,实际启示是:应在你真正打算使用的数据上评价表征。

开始做分类实验时,我会先冻结 ESM-2,为带标签的数据集提取向量,再训练一个简单分类器。先与氨基酸组成等基础特征比较,再决定是否值得微调模型。

如果目标是预测不熟悉的蛋白质家族,随机划分训练集和测试集可能产生误导:两边可能存在近缘序列。应按预期用途采用考虑序列相似性的划分,并且只在训练部分拟合标准化或特征选择步骤。DataSAIL 论文 解释了跨数据划分的相似性为何可能抬高评测成绩。本文的两条字符串只能检查程序流程,不能评价模型质量。

常见问题

现象 处理方式
模型下载失败 检查网络或代理能否访问 Hugging Face,恢复连接后重试。
找不到匹配的 PyTorch 安装包 根据 PyTorch 官方安装页 选择支持的 Python 和平台组合,再重建环境。
输入校验失败 去掉标题行和空白,检查氨基酸记法及长度;不要静默删除有生物学意义的符号。
内存占用过高 减少每批序列数和序列长度,单独处理长序列。
每条蛋白质不是 320 个数值 检查模型版本、池化维度,以及是否误保存了残基向量。
输出文件已存在 使用 --output embeddings-second-run.json

下一步

现在你已有一条从蛋白质序列到向量文件的可重复流程,并明确处理了特殊 token、填充和输出检查。下一步可以选择一个小型带标签数据集,在符合预期用途的数据划分上测试冻结表征的基线。成功提取向量与得到有用的生物学预测,需要分别验证。

友情链接

其它