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,会引入数据泄露,模型性能会虚高。我曾经吃过这个亏,后来改成了按发布日期切分,或者干脆在提取特征时只用“已知”结构域和通路,避免前瞻性信息。
特征融合示意图
画一张图帮你理解这四类特征如何流入最终模型。
这张图的核心观点很简单:不要只依赖单一特征。把序列的结构化的、物理化学的、以及前人累积的功能知识全部拉到同一空间,再去做联合学习,效果会稳健很多。