4. 特征工程入门:为什么需要特征,特征类型

特征工程,说白了就是把原始数据翻译成模型能听懂的“悄悄话”。

蛋白质互作预测这件事,模型拿到的原始数据无非是一堆氨基酸字母和三维坐标。你觉得模型自己能看懂这些字母和坐标里的生物学含义吗?

我个人习惯,先把这个问题搞清楚:特征的本质是信息的压缩与结构化。你想想看,你把一条序列丢进网络,它不知道什么是疏水作用,也不知道什么是氢键。你得帮它把这些知识“装”进去。

为什么需要特征?——别让模型“猜哑谜”

我曾经犯过一个很傻的错误。刚开始做互作预测时,直接把两个蛋白质的序列拼接成一个长字符串,扔给一个简单的卷积网络。结果训练了三天,准确率还不如随机猜测。

为什么?因为模型在猜哑谜。它不知道序列上哪些位置是结合位点,不知道哪些残基的物理化学性质更适合配对。它只能看到字符的排列,看不到底层的生物学逻辑。

这就是特征工程存在的理由:用领域知识把隐藏的信号显式化。

具体的价值,我列在下面这张表里:

问题没有特征工程有特征工程
维度灾难模型在高维稀疏空间里迷失降维编码,聚焦关键信号
噪声干扰无关变异淹没结合信号平滑过滤,突出结合模式
非线性关系直线切割永远分不开构造交互项,释放高阶信息
生物学解释黑箱输出,无从下手特征权重对应物理/化学含义

所以,特征工程不是“可选项”,是“必做题”。不做好这一步,后面的模型调参、误差分析都是在沙子上盖楼。

特征类型总览——四类弹药

蛋白质互作预测的特征,我习惯分成四类。每一种都有自己的脾气。我自己在项目里,经常需要混着用。

  • 序列特征:从一级序列直接提取的字符串信息。
  • 结构特征:从三维坐标中计算的空间属性。
  • 物理化学特征:残基层面的理化性质数值。
  • 功能注释特征:从数据库或实验注释中获取的知识。

核心原则:没有一种特征能包打天下。联合学习的魅力就在于把不同维度的信息“拧”在一起,互补强弱。

1. 序列特征——最直接的“字面信息”

序列特征是最容易获取的。你只要知道蛋白质的氨基酸排列,就能算出一堆描述符。

常见的包括:

  • One-hot编码:把20种氨基酸摊成20维向量。我记得第一次用One-hot时,发现序列长度不一致,补零后模型学到的是“补零位置”,而不是结合模式。后来我改用截断+padding,加上mask机制才解决。
  • PSSM矩阵:位置特异性得分矩阵。这个信息量很大,但本身很稀疏。我建议对PSSM做一次行标准化,否则不同蛋白质的得分范围差异会导致模型梯度爆炸。
  • k-mer频率:统计短肽片段出现次数。这种方法对局部模式敏感,但丢失了顺序信息。可以考虑与位置编码配合使用。

给你一个Python风格的伪代码,展示最基础的序列特征提取逻辑:

def extract_sequence_features(seq, k=3):
    # seq: str, 氨基酸序列
    # 1. One-hot
    one_hot = np.zeros((len(seq), 20))
    for i, aa in enumerate(seq):
        one_hot[i, aa_to_idx[aa]] = 1

    # 2. k-mer 频率
    kmer_counts = {}
    for i in range(len(seq)-k+1):
        kmer = seq[i:i+k]
        kmer_counts[kmer] = kmer_counts.get(kmer, 0) + 1

    return {
        "one_hot": one_hot,
        "kmer_freq": kmer_counts
    }

2. 结构特征——蛋白质的“三维身份证”

序列特征的局限很明显:它不知道每个残基在真实空间里“长什么样”。这时候就得靠结构特征了。

我最常用的结构特征包括:

  • 二级结构组分:螺旋/折叠/转角占比。我在一个酵母双杂交数据的实验中发现,两个蛋白质的二级结构组分相似性越高,互作概率反而越低——“同质相斥”的现象。
  • 溶剂可及性:每个残基暴露在溶剂中的程度。结合位点通常有较高的局部暴露度。
  • 接触基质距离:针对已知复合物结构,计算两个蛋白对应残基之间的空间距离。这个特征对定位结合界面非常关键,但数据覆盖面有限。

要注意,结构特征往往依赖同源建模或实验解析的结构,数据不全时容易引入偏差。

个人经验:当你手头既有序列又有结构时,优先用结构特征做主干,把序列特征作为补充。结构是“硬证据”,序列是“推测”。

3. 物理化学特征——藏在数字里的“生物化学密码”

这类特征把每个残基抽象成一组数字——等电点、疏水性指数、极性、电荷、分子量等等。模型能从这些数值中“嗅”到化学相互作用的味道。

我一直使用一个20×7的特征矩阵,7列对应7种理化属性,20行是20种氨基酸的基准值。但这里有个坑:

我曾经直接把这7维向量作为残基特征跟序列特征拼接,结果模型学到的是“氨基酸编号顺序”,而不是真实的理化趋势。后来我改成滑动窗口均值,把局部理化趋势融合进来,效果才明显提升。

常用理化属性表:

属性含义对互作的贡献
疏水性 (Kyte-Doolittle)残基亲水/疏水倾向疏水核心驱动折叠与结合
电荷 (pKa)残基在生理pH下的带电状态静电吸引/排斥影响配体识别
极性/非极性残基侧链极性分类氢键网络稳定性
分子量残基质量体积效应影响空间位阻
可旋转键数量侧链柔性构象适配能力

4. 功能注释特征——站在前人的肩膀上

最后一类特征很特别,它不需要从序列或结构里“算”出来,而是从已有的生物学知识库里直接拿。

主要来源:

  • Gene Ontology (GO):分子功能、生物学过程、细胞组分。两个蛋白质如果共享某些GO项,互作概率会显著增高。
  • KEGG通路:如果两个蛋白出现在同一条代谢通路里,它们直接互作的可能性很大。
  • InterPro结构域:某些结构域专门负责蛋白-蛋白互作,比如SH3、PDZ。

我自己的一个教训:功能注释的覆盖率很不均匀。你对模式生物(比如人、小鼠)可以拿到大量GO注释,但对一些冷门物种,注释几乎为零。此时如果用功能注释特征做主成分,模型会在“未知”上跑偏。我的做法是:对注释缺失的蛋白质,单独标记一个“未知”类别,而不是强行补零或者忽略。

下面是我经常使用的注释特征编码逻辑,你参考一下:

def go_term_embedding(go_list, vocab_size=5000):
    # go_list: ["GO:0003674", "GO:0008150", ...]
    # 使用DictVectorizer 或者 embedding lookup
    encoder = DictVectorizer(sparse=True)
    feature_dict = {f"go_{g}": 1 for g in go_list}
    return encoder.fit_transform([feature_dict])

嗯,这里要注意:GO层级很深,直接使用所有层级会产生高维噪声。我通常只保留证据代码为EXP(实验证据)的注释,并去掉IEA(电子推断)条目。

避坑指南:千万不要混用训练集和测试集的GO注释!因为GO本身也是一个不断更新的数据库,如果训练集和测试集的注释来自同一次数据库dump,会引入数据泄露,模型性能会虚高。我曾经吃过这个亏,后来改成了按发布日期切分,或者干脆在提取特征时只用“已知”结构域和通路,避免前瞻性信息。

特征融合示意图

画一张图帮你理解这四类特征如何流入最终模型。

特征类型与互作预测流程 特征工程:四类特征协同驱动互作预测 序列特征 结构特征 理化特征 功能注释特征 特征融合 / 联合编码 拼接 · 加权 · 注意力 · 跨模态对齐 互作分类器 MLP / GNN / Transformer 互作概率

这张图的核心观点很简单:不要只依赖单一特征。把序列的结构化的、物理化学的、以及前人累积的功能知识全部拉到同一空间,再去做联合学习,效果会稳健很多。