春江暮客

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

蛋白质数据集如何避免泄漏:用 Python 按组划分训练集与测试集

2026-09-24 技术
蛋白质数据集如何避免泄漏:用 Python 按组划分训练集与测试集

如果测试集中存在训练序列的近亲,蛋白质向量模型可能显得很有效。在比较 ESM-2 向量和氨基酸组成基线之前,先明确希望模型推广到什么数据:熟悉蛋白质的另一次测量、同一亲本的新变体,还是另一个家族。

本文用一个可以复现的 Python 示例划分训练集、验证集和测试集,让指定分组内的样本始终留在一起。它衔接上一篇 FASTA 文件检查,为氨基酸组成基线ESM-2 向量流程准备共同的评估划分。

先定义哪些样本必须放在一起

随机划分数据行,与留出整个家族,回答的是不同问题。TAPE 论文按任务设计数据划分,把具有生物学意义的泛化能力作为评估目标。处理自己的数据时,也应先写清使用场景,再决定分组规则。

希望评估的场景 可以考虑的分组依据 仍需检查的问题
没有完全重复输入的新序列 规范化后的序列 相似序列仍可能分布在不同子集。
未见过的亲本产生的变体 亲本蛋白或构建谱系 亲本注释可能不完整。
来自留出序列组的蛋白质 序列聚类结果 分组不重叠,并不证明任意跨组序列都低于某个一致性阈值。
来自新来源的样本 供体、研究项目或实验批次 不同来源仍可能共享序列。

这些是不同的实验设计,不能互相替代。对于仅以序列为输入的预测器,同一序列的多次测量共享相同输入;除非评估明确建模了另一个变化的输入,否则应放在同一子集。标签冲突时,不要未经说明就取平均。

同时约束两个因素时,直接使用 (sequence_cluster, donor) 这样的元组可能无效:同一供体可能出现在多个序列簇中,形成多个元组,随后被分到不同子集。一种做法是把满足任一关联条件的记录连接起来,以连通分量作为分组。这可能产生很大的组。应报告这个限制,不要为了凑比例而悄悄拆组。

生成明确的划分表

示例包含十个人工分组,各组依次有 1 到 10 行,共 55 行。没有真实序列、实验标签或模型成绩。代码中的序列键只是演示重叠检查的占位标识。

下载 split_demo.py,在虚拟环境中安装经过测试的库版本,然后运行:

python3 -m venv .venv
source .venv/bin/activate
python -m pip install "scikit-learn==1.9.1"
python split_demo.py > split.csv

本例使用 Python 3.14.7 和 scikit-learn 1.9.1 验证。Shell 会将划分表重定向到 split.csv;需要保留旧文件时,请使用新的文件名。下面的统计信息写入标准错误流:

train: rows=32 groups=6
valid: rows=12 groups=2
test: rows=11 groups=2
PASS: rows, groups, and exact-sequence keys checked

第一次划分留出两组作为测试集,第二次从剩余八组中留出两组作为验证集。组数比例是 60/20/20,行数比例却不同。GroupShuffleSplit 文档说明,小数形式的比例针对分组,测试组数向上取整。因此,分组较少时,实际比例可能偏离原先设想。

下面是完整的下载脚本:

"""Synthetic grouped split demo; no biological measurements or model scores."""
import csv
import sys
from itertools import combinations

import numpy as np
from sklearn.model_selection import GroupShuffleSplit


def grouped_split(groups):
    groups = np.asarray(groups)
    if groups.ndim != 1 or len(set(groups.tolist())) < 5:
        raise ValueError("Supply a one-dimensional array with at least five groups")
    rows = np.arange(len(groups))
    first = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=17)
    dev, test = next(first.split(rows, groups=groups))
    second = GroupShuffleSplit(n_splits=1, test_size=0.25, random_state=29)
    train_local, valid_local = next(second.split(dev, groups=groups[dev]))
    # The second split returns positions within dev, not original row numbers.
    return {"train": dev[train_local], "valid": dev[valid_local], "test": test}


def verify(parts, groups, sequence_keys):
    rows = [int(i) for indices in parts.values() for i in indices]
    if sorted(rows) != list(range(len(groups))):
        raise ValueError("Every row must occur exactly once")
    if len(sequence_keys) != len(groups):
        raise ValueError("Sequence keys and groups must have equal length")
    for left, right in combinations(parts, 2):
        for values, label in [(groups, "group"), (sequence_keys, "exact sequence")]:
            overlap = {values[i] for i in parts[left]} & {values[i] for i in parts[right]}
            if overlap:
                raise ValueError(f"{left}/{right}: overlapping {label}")


def main():
    # Invented groups of unequal sizes; keys stand in for canonical sequences.
    groups = np.repeat(np.arange(10), np.arange(1, 11))
    keys = [f"synthetic-sequence-{i}" for i in range(len(groups))]
    parts = grouped_split(groups)
    verify(parts, groups, keys)
    writer = csv.writer(sys.stdout, lineterminator="\n")
    writer.writerow(["sample_id", "group_id", "split"])
    for name, indices in parts.items():
        group_count = len(set(groups[indices]))
        print(f"{name}: rows={len(indices)} groups={group_count}", file=sys.stderr)
        for i in indices:
            writer.writerow([f"sample_{i:03d}", int(groups[i]), name])
    print("PASS: rows, groups, and exact-sequence keys checked", file=sys.stderr)


if __name__ == "__main__":
    main()

第二次划分返回的是 dev 内部的位置,必须通过 dev 映射回原始行号。直接把这些位置当成原始行号,可能分配到错误的样本。

换成真实元数据

每行保留稳定的样本编号。提供没有缺失值、类型一致的一维分组编号数组,然后调用 grouped_split(groups)。示例要求至少五组,便于执行两次划分;拥有五组并不意味着评估在统计上已经可靠。

keys 替换成真实的规范化蛋白质序列,或已记录的密码学摘要,顺序必须与数据行一致。规范化方法沿用 FASTA 检查时写明的输入规则。拟合模型之前,运行 verify(parts, groups, keys)。它会拒绝遗漏或重复分配的数据行、跨子集共享的分组,以及跨子集重复的完全相同序列键。检查通过,不代表那些键不同的近似序列不存在重叠风险。

如果使用序列聚类,记录软件、版本、序列一致性的定义、比对覆盖度要求和聚类模式。簇编号只是这套流程的输出。如果研究结论要求每条测试序列与训练序列的相似程度都低于给定阈值,就应使用明确的一致性与覆盖度规则,另外检查跨子集的序列关系。不能仅凭簇编号不同就推断满足这个条件。

在查看模型成绩之前确定这些规则。将 sample_id,group_id,split 划分表与数据版本、文件校验值、随机种子和分组参数一起保存。数据顺序或分组注释改变后,仅保留种子无法还原旧划分。

需要拟合的预处理只使用训练数据

预测器以及需要学习参数的缩放、缺失值填补、PCA 和特征筛选,都只在训练数据上拟合。验证集用于选择模型,选择固定之后再评估测试集。scikit-learn 数据泄漏说明解释了为什么在全量数据上拟合变换会污染评估;使用流水线有助于让这些操作留在训练分区内部。

固定的氨基酸组成计算不学习整个数据集的统计量。使用未更新的预训练模型,在评估模式下逐条独立编码序列,也不涉及这类拟合。但如果对全部向量拟合 PCA,或者使用留出集标签微调编码器,情况就变了。此外,公开预训练数据可能已经包含评估序列;下游划分干净,并不能证明预训练阶段也完全独立。

如果采用交叉验证,开发集内部的每次划分也应保留分组。交叉验证指南介绍了相应方法。GroupShuffleSplit 不负责平衡类别标签,因此需要检查各子集的类别数量和目标值范围。如果目标是预测未来实验,随机分组也可能不适合,应另行制定按时间留出的规则。

检查失败后先修复,再拟合

错误或现象 处理方法
Supply a one-dimensional array with at least five groups 检查分组列的维度及不同分组的数量。如果独立组太少,应重新设计评估,不要编造组编号绕过检查。
Every row must occur exactly once 恢复通过 dev 映射原始行号的步骤,检查是否遗漏或重复使用行号。
overlapping groupoverlapping exact sequence 检查报错指出的两个子集,修正分组注释或合并有关联的组,重新生成划分并再次检查。
某个子集只有一个类别 检查现有独立分组能否支持这个任务,在评估模型之前制定并记录兼顾类别的分组规则。

报告这次划分到底测了什么

下一次比较模型时,将划分表、各子集的行数与组数、标签分布,以及实际执行的重叠检查一起保存并报告。氨基酸组成基线与蛋白质向量使用同一份划分,并说明各自如何选择模型。

按组划分后成绩下降,不一定意味着模型变差,也可能是泛化问题变难了。在把分数提升视为进步之前,先说清这个问题,保留独立测试集,并让样本分配可以复现。

封面是概念插画,不是实验测定的蛋白质结构。文档核对日期:2026 年 9 月 24 日。

友情链接

其它