训练前用 Python 检查蛋白质嵌入文件
蛋白质嵌入矩阵的行数正确,也可能配错标签。FASTA 排序、提取批次合并、失败序列过滤,都会改变记录顺序。分类器随后读到的数值看似正常,却可能属于另一条蛋白质序列。
这篇教程在训练前检查保存的矩阵:每个向量对应一个唯一的样本 ID,特征维数与提取配置一致,浮点数全部有限,并且 ID 与独立的标签顺序清单逐项相同。它承接 ESM-2 嵌入教程和蛋白质标签连接教程。需要基础 Python 和 NumPy 知识,演示不需要 GPU 或下载模型。
1. 定义嵌入文件要求
使用只包含两个命名数组的 NPZ 文件。这是本教程约定的简易交换格式,不是 ESM 的原生导出格式。
| 字段 | 格式要求 | 含义 |
|---|---|---|
sample_id |
形状为 (N,) 的 Unicode 字符串数组 |
每一行代表的样本。 |
embedding |
形状为 (N, D) 的浮点数组 |
每个样本一个池化后的特征向量。 |
ids.txt |
UTF-8 文本,每行一个 ID | 按下游标签实际顺序排列的预期 ID。 |
--dim |
正整数 | 从提取配置中取得的特征维数。 |
ID 必须非空、唯一,并且不含空白字符。脚本按原始文本精确比较:001 与 1 不同,大小写也不同。本教程要求每个样本只有一行;如果保留重复测量,需要给样本分配不同 ID,并另存它们与母序列的对应关系。
从最终标签表独立生成 ids.txt。如果直接把嵌入文件的 ID 复制成清单,顺序检查就失去了意义。嵌入文件里的 ID 应在提取时取自该向量对应的序列记录。
逐残基表示通常需要先池化,才能满足这里的矩阵要求。官方 ESM 提取脚本同时提供逐 token 表示和均值表示,计算均值时先选择残基位置。不要为了通过形状检查,把含残基轴的张量直接展平。随数据记录模型检查点、revision、表示层、残基掩码、池化方法和截断规则。
2. 下载示例并安装 NumPy
将 audit_embeddings.py和 make_demo.py下载到新的工作目录。下面固定的 NumPy 版本需要 Python 3.12 或更新版本。示例在 Python 3.14.7、NumPy 2.5.3 下验证。
python3 -m venv .venv
source .venv/bin/activate
python -m pip install "numpy==2.5.3"
python audit_embeddings.py --help
下面的生成器新建 demo-embeddings 目录;目录已经存在时会停止,避免覆盖之前的演示文件。向量全部由示例构造:三个样本,每个样本四个特征。它们不是 ESM 输出,也不是生物测量结果。
"""Create small synthetic fixtures without extracting real protein embeddings."""
from pathlib import Path
import numpy as np
def main():
root = Path("demo-embeddings")
root.mkdir() # Refuse to overwrite a previous demo directory.
ids = np.array(["protein_A", "protein_B", "protein_C"], dtype=np.str_)
vectors = np.array([[0.1, 0.2, 0.3, 0.4],
[0.4, 0.3, 0.2, 0.1],
[-0.1, 0.0, 0.2, 0.5]], dtype=np.float32)
(root / "ids.txt").write_text("\n".join(ids.tolist()) + "\n", encoding="utf-8")
cases = {"good": (ids, vectors),
"reordered": (ids[::-1], vectors[::-1]),
"duplicate": (np.array(["protein_A", "protein_A", "protein_C"]), vectors)}
bad = vectors.copy()
bad[1, 2] = np.nan
cases["nan"] = (ids, bad)
for name, (sample_ids, embedding) in cases.items():
np.savez(root / f"{name}.npz", sample_id=sample_ids, embedding=embedding,
allow_pickle=False)
print("Created demo-embeddings with four synthetic archives and ids.txt")
if __name__ == "__main__":
main()
numpy.savez按关键字参数名保存数组。明确使用 Unicode ID 和浮点向量,可以避免在这些文件中存入 object 数组。
3. 检查正常文件
python make_demo.py
python audit_embeddings.py demo-embeddings/good.npz demo-embeddings/ids.txt --dim 4
检查脚本的预期输出:
{"status": "ok", "samples": 3, "dimensions": 4, "dtype": "float32", "zero_rows": 0}
处理真实数据时,把 4 换成实际提取配置里的特征维数。不要从待检查文件本身推导预期维数。不同检查点或池化流程可能输出相同维数,因此维数相同不能证明表示方法一致。
下面是完整检查脚本,与下载文件相同:
"""Check a pooled protein embedding archive against an ordered ID manifest."""
import argparse
import json
from pathlib import Path
import sys
from zipfile import BadZipFile
import numpy as np
def check_ids(ids, source):
if not ids:
raise ValueError(f"{source}: no sample IDs")
seen = set()
for row, value in enumerate(ids):
if not value or any(char.isspace() for char in value):
raise ValueError(f"{source}: empty ID or whitespace at row {row}")
if value in seen:
raise ValueError(f"{source}: duplicate ID {value!r}")
seen.add(value)
def audit(path, ids_path, expected_dim):
if expected_dim < 1:
raise ValueError("expected dimension must be positive")
expected = Path(ids_path).read_text(encoding="utf-8-sig").splitlines()
check_ids(expected, "manifest")
archive = np.load(path, allow_pickle=False)
if not isinstance(archive, np.lib.npyio.NpzFile):
raise ValueError("expected an NPZ archive")
with archive:
if len(archive.files) != 2 or set(archive.files) != {"sample_id", "embedding"}:
raise ValueError("archive must contain only sample_id and embedding")
ids = archive["sample_id"]
vectors = archive["embedding"]
if ids.ndim != 1 or ids.dtype.kind != "U":
raise ValueError("sample_id must be a one-dimensional Unicode array")
names = ids.tolist()
check_ids(names, "archive")
if vectors.ndim != 2 or vectors.shape != (len(names), expected_dim):
raise ValueError(f"expected embedding shape ({len(names)}, {expected_dim})")
if vectors.dtype.kind != "f":
raise ValueError("embedding must have a floating-point dtype")
if names != expected:
if set(names) == set(expected):
raise ValueError("ID order differs; join by sample_id before export")
raise ValueError("archive IDs differ from manifest IDs")
zero_rows = 0
# Arrays are loaded into RAM; chunking limits temporary boolean arrays.
for start in range(0, len(names), 1024):
block = vectors[start:start + 1024]
finite = np.isfinite(block)
if not finite.all():
row, column = np.argwhere(~finite)[0]
raise ValueError(f"non-finite value at row {start + int(row)}, column {int(column)}")
zero_rows += int(np.all(block == 0, axis=1).sum())
return {"status": "ok", "samples": len(names), "dimensions": expected_dim,
"dtype": str(vectors.dtype), "zero_rows": zero_rows}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("archive", type=Path)
parser.add_argument("ids", type=Path, help="one ID per line, in downstream label order")
parser.add_argument("--dim", type=int, required=True, help="expected pooled feature count")
args = parser.parse_args()
try:
result = audit(args.archive, args.ids, args.dim)
except (OSError, ValueError, EOFError, BadZipFile) as error:
print(f"ERROR: {error}", file=sys.stderr)
return 1
print(json.dumps(result, ensure_ascii=False))
return 0
if __name__ == "__main__":
raise SystemExit(main())
脚本调用 numpy.load时设置 allow_pickle=False,读取后关闭 NPZ 文件,拒绝 object 数组。如果旧导出文件需要 pickle 才能读取 ID,应从可信源重新导出为 Unicode ID 数组。
numpy.isfinite检查 NaN、正无穷和负无穷。全零行只计入报告,不自动拒绝。做余弦归一化前需要核查这些行,因为归一化要求向量非零。零向量是否属于提取错误,要结合记录的表示方法判断。
4. 复现三种失败
分别执行下面的命令,每条都应在标准错误输出中报告问题,并以状态码 1 退出:
python audit_embeddings.py demo-embeddings/reordered.npz demo-embeddings/ids.txt --dim 4
python audit_embeddings.py demo-embeddings/duplicate.npz demo-embeddings/ids.txt --dim 4
python audit_embeddings.py demo-embeddings/nan.npz demo-embeddings/ids.txt --dim 4
ERROR: ID order differs; join by sample_id before export
ERROR: archive: duplicate ID 'protein_A'
ERROR: non-finite value at row 1, column 2
顺序错误示例同时反转 ID 和向量,保留了蛋白质与向量之间的对应关系,却与下游标签顺序不同。仅检查 ID 集合相同会漏掉这个问题。报告中的数组位置均从零开始计数。
脚本只读取文件,不修改输入;所有检查通过后才输出成功报告。在自动流程中,先要求检查命令成功,再启动训练任务:
python audit_embeddings.py demo-embeddings/good.npz demo-embeddings/ids.txt --dim 4 && echo "Embedding checks passed"
5. 根据错误修复数据
| 问题 | 具体处理 |
|---|---|
| ID 成员相同但顺序不同 | 按 sample_id 将嵌入行连接到标签表,用连接结果中的同一行排列导出 ID 和向量,再运行检查。 |
| 文件和清单的 ID 不同 | 核查提取失败记录、过滤规则和数据版本,先解决缺失或多出的样本,再连接。 |
| ID 重复 | 找出重复提取记录;如果属于有意保留的重复样本,定义不同的样本 ID,评估时仍按母序列分组。 |
| 形状不符 | 检查模型预期维数和池化输出。逐残基张量或转置矩阵应在上游修正。 |
| object 数组或非浮点向量 | 按预定的 Unicode ID 和浮点 dtype 重新导出。强制转换错误数据不能恢复正确嵌入。 |
| 数值非有限 | 追踪报告中的样本,检查分词、池化和数值精度,按事先记录的规则重新计算或排除。把 NaN 换成零可能掩盖原因。 |
不要只对 ID、标签或向量中的一项排序。应执行明确的连接,或对对应记录应用同一排列。标签连接教程介绍了如何检查键的成员关系并保留样本对应关系。
6. 保存来源信息并估算内存
这个检查器把两个数组读入内存,按每批 1,024 行检查数值,以限制临时布尔数组的大小,但没有流式读取 NPZ 矩阵。100,000 个样本、1,280 个特征的 float32 矩阵本身约占 488 MiB,还不包括 ID、Python 对象和临时数组。压缩文件大小不能代表加载后的内存需求。
更大的数据集可以考虑单独的 .npy 矩阵、内存映射,以及带版本的 ID 清单。那种布局需要对应的检查器;本示例只接受 NPZ。NumPy 在 numpy.load 文档中说明了内存映射的用法。
检查通过仍不能发现上游把向量挂到错误 ID、残基池化错误、混用模型 revision 或训练测试泄漏。保留序列校验和及提取元数据,保存数据集清单,并在比较嵌入与氨基酸组成基线时使用同一份按组划分结果。需要拟合的标准化、PCA 和预测器选择,仍应在相应的训练与验证分区内完成。
小结
拟合蛋白质预测器前,先检查样本对应关系、行顺序、维数、dtype 和有限数值。保留提取元数据及序列映射,让保存的数值可以追溯到来源。检查通过后,再使用符合生物学泛化问题的数据划分评估表示效果。
封面为概念插画,不是实验测得的蛋白质结构。
- 原文作者:春江暮客
- 原文链接:https://www.bobobk.com/protein-embedding-audit-python.html
- 版权声明:本作品采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。