说实话,我第一次看到这个标题的时候,脑子里冒出的第一个念头是:终于有人不只盯着“整分子级别”的表征了。药物-药物相互作用预测(DDI)这个方向,前几年大家几乎都在卷同一件事——把两个药物分子各自压成一个定长向量,然后拼起来做个二分类或关系分类。看起来干净利落,但真正跑过下游任务的人都知道,这种“一分子一向量”的抽象损失了太多信息。有些相互作用恰恰是由分子上某个小小的官能团、某段特定环系决定的,你把整分子平均池化成 128 维,这个局部信号基本就被稀释没了。
这篇《基于局部子结构特征及其补语的药物-药物相互作用预测》抓住了这个痛点。它的思路不复杂,甚至可以说是朴素的:把分子拆成“局部子结构”和“子结构之外的补语”两条路走,分别编码,再融合起来做 DDI 预测。但朴素不等于简单,从子结构怎么切、切多细、补语怎么定义、两路怎么融合,每一个环节都有一堆工程取舍。这篇博文就是我在读完这篇论文、照着复现了一版原型之后的完整整理,包括它为什么这么设计、具体怎么落地、我踩了哪些坑。适合已经对 GNN 和分子图有基本概念、想搞清楚“子结构建模”这条路子怎么走的朋友,也适合刚开始做 DDI 预测、想快速搭出一个能跑基线的新手。
1. 这篇论文到底想解决什么问题
1.1 DDI 预测的真实业务场景
先说清楚这件事为什么值得做。临床上多药联用是常态,尤其是慢病患者,同时吃五种以上药物并不罕见。药物之间可能发生协同、拮抗,也可能产生严重的不良反应,有些联合用药的风险在说明书里根本写不全。靠临床试验去穷举组合,成本高到不现实——已上市药物数以千计,两两组合是指数级膨胀。所以计算侧的 DDI 预测一直有人做,目标很明确:在实验验证之前,先把高风险组合筛出来,缩小需要重点关注的候选集。
这个场景决定了模型必须回答两类问题。一类是“这两种药会不会相互作用”,本质是二分类;另一类是“会产生哪一类相互作用”,比如药效增强、药效减弱、吸收受影响、代谢被抑制,本质是多分类或者多标签。论文里通常两种任务都会报,因为后者的信息量更大,但对标注质量要求也更高。我个人的看法是,二分类的结果更容易被下游接受,多分类的结果更适合做机制解释的辅助,实际落地时别把两者混为一谈。
这类任务的数据标注有个天然特点:正样本基本来自公开的药物相互作用数据库,负样本绝大多数是“没查到记录”而不是“确认无相互作用”。这个区别非常关键,后面讲评估和采样的时候会反复用到。
1.2 传统整分子表征的三个硬伤
为什么“整分子压成一个向量”不够用?我把踩过的坑归成三条。
第一条是信息稀释。一个分子可能有一百多个原子,池化之后所有原子的贡献被平均掉。而 DDI 的触发机制往往是局部的,比如某个含氮杂环容易和靶点形成配位,或者某段酯键容易被水解酶识别。这些细节在全局向量里只占很小的权重,模型很难把它单独拎出来。
第二条是相似分子不可区分。很多药物共享一个很大的骨架,只在侧链上有一点差异。全局表征下,两个分子的向量余弦相似度可能高达 0.95,模型就倾向于给它们相同的预测,可实际上毒副作用和相互作用谱可能完全不同。这种“大骨架遮蔽小差异”的现象在激酶抑制剂里特别常见。
第三条是缺乏可解释性。临床和药化的人拿到一个预测结果,第一反应是问“为什么”。你告诉他“因为这两个向量距离近”,没人会信服。但如果你能指出“是分子 A 上的这个子结构和分子 B 上的那个片段发生了作用”,沟通成本立刻降下来。子结构建模的一个附加价值就在这里。
注意:不要把“子结构建模”当成万能药。它解决的是局部信号被稀释的问题,但如果你手上的数据本身就稀疏、正负样本严重失衡,先把数据侧的问题处理干净,比换模型结构收益大得多。
1.3 “局部子结构 + 补语”的切入角度
论文的核心主张可以概括成一句话:一个药物分子可以分解成若干局部子结构,以及这些子结构之外剩下的部分——也就是补语。两路分别编码,子结构那一路负责捕捉具体的药效团和反应位点,补语那一路负责保留分子的整体骨架和长程上下文,最后融合。
我理解这个设计的动机,其实是受对比学习里“局部—全局”那套思路的启发。只建模局部,分子就变成一堆碎片的集合,丢失了“这些碎片在什么骨架上”的信息;只建模全局,又回到原来的老问题。补语分支的存在,相当于给局部子结构提供一个上下文锚点,告诉模型“这个片段是挂在一个苯环上还是挂在一个糖环上”,同样的片段在不同骨架环境下,相互作用倾向可能完全不同。
另一个我觉得很妙的地方,是补语定义成“扣掉子结构之后剩下的图”。这个操作在实现上其实就是节点掩码,代价极低,但它让两路编码形成了互补关系,而不是简单的多尺度堆叠。多尺度那种做法通常是不同感受野的卷积并联,信息是冗余的;子结构加补语是严格划分,信息是互斥且完备的。
2. 方案拆解:为什么是子结构和它的补语
2.1 药物分子的图视角
要理解后面的细节,得先把分子怎么变成图这件事说清楚。通常的做法是原子作节点,化学键作边。节点特征包括原子类型、电荷、杂化状态、是否在环上、芳香性、连接的氢原子数;边特征包括键类型(单、双、三、芳香)、是否共轭、是否在环内。这套特征设计在分子图学习里几乎是标配,我见过的实现大同小异,差别主要在于特征做不做 one-hot、连续特征怎么归一化。
这里有个容易忽略的点:不要丢掉氢原子信息。很多开源实现为了省事直接把氢隐式处理,但氢键供体/受体的数量对 DDI 预测是有实际影响的,你可以在节点特征里用“连接氢原子数”这个属性补回来。我试过完全去掉这个特征,在某个基准库上 AUROC 掉了将近两个点,不算致命但也不该白丢。
图构建完之后,一个分子就是一个带节点特征和边特征的图对象。接下来的所有操作,子结构切分也好、补语提取也好,都是在这个图上做节点集合的划分。
2.2 子结构切分的常见做法与取舍
子结构怎么切,是这套方法里最需要斟酌的地方。我看到过的做法大致有四类,各有各的适用场景。
第一类是基于化学规则的切分,比如按照可断裂键把分子拆成环系、链段、官能团。这类方法的优点是切出来的片段有化学意义,人看得懂;缺点是对规则依赖强,遇到复杂稠环或者非常规结构容易切出很碎的片段。
第二类是基于频繁子图挖掘,从数据集里统计高频出现的子结构,把它们作为词典,然后对每个分子做子图匹配。优点是数据驱动、覆盖面广;缺点是词典构建有额外开销,而且罕见图会被漏掉,对长尾药物不友好。
第三类是基于注意力或聚类的软划分,让模型自己学哪些节点该归到一组。优点是灵活;缺点是切出来的“子结构”没有化学解释,而且训练不稳定,我试过一版,同一个分子跑两次切分结果都不一样,调试起来很痛苦。
第四类是固定粒度的滑窗或随机游走采样,实现最简单,但语义最弱。
论文走的是偏第一类和第二类结合的路子——用化学上可解释的切分规则保证子结构的语义,同时对切分粒度做约束,避免碎成一地。我个人在实际复现时倾向先用规则切分拿到可解释的片段,再判断要不要引入数据驱动的词典。如果你的目标是发论文,第二类的故事更好讲;如果你只是想要一个能用的基线,第一类足够。
2.3 补语分支的存在意义
补语这个概念乍一听有点绕,其实很直白:给定分子图 G 和选出的子结构节点集合 S,补语就是 G 中除去 S 之后剩下的节点和它们之间的边构成的子图。注意,是节点级排除,不是把子结构整个删掉再连边,跨界的边直接丢弃。
为什么要有这一路?我总结了三个作用。
其一是提供上下文。单个子结构脱离骨架是没有意义的,同样是羧基,挂在芳香环上和挂在脂肪链上,解离常数和反应性都不一样。补语分支编码的正是这个“挂在哪”的信息。
其二是防止子结构选择失误。子结构切分总有可能切错,把关键片段切掉或者切得太碎。补语分支相当于一个兜底通道,即使子结构那一路漏了信息,补语里还能找回来一部分。这一点我在消融实验里验证过:把补语分支去掉,模型在几个子结构切分质量较差的药物类别上性能下降明显。
其三是支撑对比学习的目标。论文里用子结构表征和补语表征做对比约束,让两者既不冗余又不脱节。这个设计有没有用,取决于你的数据量,数据少的时候对比损失容易主导训练,反而伤害主任务,后面会细说。
2.4 双通道融合的几种思路对比
两路编码出来之后怎么合,也是一个需要实验的选择。我把常见的几种列出来做个对比。
| 融合方式 | 实现复杂度 | 表达能力 | 适用场景 | 我的实测感受 |
|---|---|---|---|---|
| 直接拼接 | 低 | 中 | 快速搭基线 | 稳,但两路权重固定,没法自适应 |
| 逐元素相加 | 低 | 低 | 两路维度相同且语义接近 | 容易互相抵消,不推荐 |
| 门控加权 | 中 | 高 | 子结构质量参差不齐 | 效果最好,但门控容易塌缩到一路 |
| 注意力融合 | 中高 | 高 | 数据量充足 | 提升有限,调参成本高 |
| 双线性池化 | 高 | 高 | 需要建模交互 | 参数量大,小数据集易过拟合 |
我自己的经验是,中小规模数据上用门控加权性价比最高。门控塌缩的问题可以通过给门控输出加一个小的熵正则来缓解,别让它太快退化成 0 或 1。
3. 核心细节解析与实操要点
3.1 分子图构建的原子与键特征设计
特征设计这块,我建议一开始就把表列全,别后面想起来再改,因为改特征意味着所有缓存图都要重建,很浪费时间。
节点特征我一般用这些:原子类型的 one-hot(覆盖 C、N、O、S、P、卤素和常见金属)、形式电荷、手性标记、是否芳香、是否在环内、杂化类型、连接重原子数、连接氢原子数、是否属于某个官能团。后两个特征是需要额外计算的,官能团归属可以用子结构匹配来做,成本不低,如果嫌重可以先用前九个。
边特征用:键类型的 one-hot、是否共轭、是否在环内、键的立体标记。这些在 RDKit 里都有现成接口,几行代码就能拿到。我不建议自己写 SMILES 解析器,化学信息学里的边界情况太多,踩过一次坑之后再也不想碰。
注意:特征维度对齐要统一。我见过有人把原子类型 one-hot 做成 44 维、有人做成 100 维,混用的时候没注意,拼接时直接报错,排查了半天。建议在配置里显式写死各特征的维度和偏移量。
3.2 子结构提取的粒度控制
粒度是这套方法成败的关键。切得太细,一个分子变成二三十个碎片,每个碎片只有两三个原子,编码出来几乎没信息,还会让后续的图神经网络在极小图上不稳定;切得太粗,一个分子就两三个大块,那就退化成了粗粒度的全局表征,失去了局部建模的意义。
我的经验值是平均每个分子切出 3 到 8 个子结构,单个子结构的原子数控制在 3 到 15 之间。超过上限的片段可以进一步拆,比如按环系断开;低于下限的片段可以合并到相邻片段或者直接归入补语。
具体怎么控制?可以设两个超参:最大片段数和最小片段原子数。切分算法先按规则产出一批候选,然后做后处理——把太小的片段并入补语,把太大的片段按桥键再拆一次。这个后处理逻辑看起来不起眼,但我实测下来对结果的稳定性影响不小。
3.3 编码器选型:GNN 还是 GAT 还是 MPNN
编码器这块,论文里用的具体结构需要看原文,但通用的选型逻辑我可以给出来。
图卷积(GCN)最简单,但它是各向同性的,所有邻居权重一样,对化学图这种邻居重要性差异很大的场景不够。图注意力(GAT)引入了邻居权重,表达能力强一些,代价是参数量增加、训练更容易过拟合。消息传递网络(MPNN)更通用,边特征可以参与消息计算,对键类型敏感的任务更合适。
我的建议是:子结构分支用 MPNN 类的结构,补语分支用轻量一点的 GCN。原因是子结构小、层数不用深,用强一点的算子在局部把信息提干净;补语图规模大,用 GCN 控制开销,避免整体训练时间爆炸。这个不对称设计在实现上要小心,两路输出的维度必须对齐,否则融合层没法接。
层数方面,子结构分支两到三层足够,因为子结构本身原子数就少,层数再多感受野溢出;补语分支三到四层,但要注意过平滑,节点特征趋同之后区分度会掉。
3.4 补语分支如何避免信息泄漏
这是个很隐蔽的坑。补语是“分子图减去子结构”,如果你在实现时不小心把子结构的节点特征通过某种方式带进了补语分支,两路就不是互补关系了,变成了冗余,模型学到的融合权重也就没有意义。
常见的泄漏路径有两个。一是全局池化时机不对——如果你先对整个分子图做一次全局池化,再从这个全局表示里拆出子结构和补语,那两边都含了对方的信息。正确的做法是先做节点级划分,再在各自子图上独立池化。
二是边特征的跨界残留。跨界的边应该直接删掉,而不是保留但把一端节点的特征置零,后者会让 GNN 在消息传递时传一个空信号过去,间接影响补语分支的表征。这个细节在代码里就是一行判断,但漏掉的话整个消融实验的结论都不可信。
3.5 损失函数与负采样策略
主任务损失用交叉熵或二元交叉熵,取决于你是多分类还是二分类。辅助的对比损失用 InfoNCE 那一套,让子结构表征和它对应的补语表征在表示空间里靠近,和别的分子的补语推远。
负采样这块要重点说。DDI 数据集的负样本大部分是“无记录”,不是“确认无相互作用”。直接随机采样会引入大量假阴性,模型学到的可能是“这两个药不常一起出现”,而不是“它们不相互作用”。我的做法是在采样时做流行度校正——高频出现的药物被采为负样本的概率适当降低,让模型不要走捷径。另外可以按药物类别分层采样,避免某一类药物在负样本里过度集中。
对比损失的温度系数建议从 0.07 起步,这个值大了对比信号太弱,小了训练容易崩。辅助损失的权重我一般给 0.1 到 0.3,数据量小的时候往下调,不然主任务还没学起来,对比损失先把表示空间拉乱了。
4. 实操过程:从零复现这套流程
4.1 数据准备与清洗
数据来源通常是公开的药物相互作用数据库,我下面用基准库 A 和基准库 B 代称。基准库 A 的标注质量高但规模小,基准库 B 规模大但噪声多,两个都跑一遍能看出方法的鲁棒性。
清洗步骤我按这个顺序做:先去掉没有结构信息的药物条目,再去掉只出现一次的极低频药物,然后处理重复对和方向性问题。DDI 是有方向的,A 影响 B 和 B 影响 A 在机制上可能不同,但很多数据集把方向抹掉了,建模时要明确你预测的是无向还是有向,别到最后评估指标和任务定义对不上。
分子结构统一用 SMILES 表示,用化学信息学工具包解析成图。这一步要有容错,总有一些 SMILES 解析失败,直接跳过并记录,别让整个流程崩掉。
4.2 分子图与子结构图构建代码
下面这段是我复现时用的骨架代码,简化过但逻辑完整。
from collections import defaultdict def build_mol_graph(mol): node_feats, edge_index, edge_feats = [], [], [] for atom in mol.GetAtoms(): node_feats.append([ atom.GetAtomicNum(), atom.GetFormalCharge(), int(atom.GetIsAromatic()), int(atom.IsInRing()), atom.GetTotalNumHs(), atom.GetDegree(), ]) for bond in mol.GetBonds(): i, j = bond.GetBeginAtomIdx(), bond.GetEndAtomIdx() edge_index += [[i, j], [j, i]] bf = [int(bond.GetBondTypeAsDouble()), int(bond.GetIsConjugated()), int(bond.IsInRing())] edge_feats += [bf, bf] return node_feats, edge_index, edge_feats def split_substructures(mol, max_parts=8, min_atoms=3): # 基于环系和可断裂键做候选切分 candidates = ring_and_linker_split(mol) subs, rest = [], set(range(mol.GetNumAtoms())) for frag in candidates: if len(frag) < min_atoms: continue if len(subs) >= max_parts: break subs.append(set(frag)) rest -= set(frag) # 剩下的节点构成补语 return subs, rest关键在split_substructures这个函数。候选切分的规则怎么写,直接决定后面模型能学到什么。我用的策略是先按环系切开,环与环之间的连接键作为可断点,然后再看链段长度,太短的并入相邻片段。这个规则不完美,但对大多数类药分子够用。
补语的构造就是rest集合诱导出的子图:保留两端都在rest里的边,一端在子结构里的边全删。这一步一定要写对,前面说的信息泄漏就是在这里发生的。
4.3 模型搭建与参数计算
模型分三块:子结构编码器、补语编码器、融合与分类头。子结构可能不止一个,所以子结构编码器的输出要先做一个聚合,常用的有平均池化、注意力池化和最大池化。
参数量估算我习惯先手算一遍再跑,避免显存爆了才发现。以子结构分支为例,假设节点特征 64 维,隐藏层 128 维,两层 MPNN,每层的主要参数来自消息网络和更新网络,大致是2 * (64*128 + 128*128) ≈ 49K,再乘上层数和两路分支,加上融合层,整体在几十万到百万级。这个规模在单卡上完全跑得动,瓶颈通常在数据加载和图构建,不在模型本身。
融合部分我用门控:
class GatedFusion(nn.Module): def __init__(self, dim): super().__init__() self.gate = nn.Linear(2 * dim, dim) self.proj = nn.Linear(2 * dim, dim) def forward(self, sub_repr, comp_repr): cat = torch.cat([sub_repr, comp_repr], dim=-1) g = torch.sigmoid(self.gate(cat)) fused = g * sub_repr + (1 - g) * comp_repr return fused + self.proj(cat)门控输出加一个残差投影,是为了避免门控塌缩之后信息直接流失。这个残差我在两个基准库上都试过,去掉之后在子结构质量差的类别上掉点明显。
4.4 训练循环与评估指标
训练用 Adam,学习率 1e-3 起步,带权重衰减 1e-5,批大小按显存调到 64 或 128。早停看验证集的 AUROC,耐心值给 10 到 15 个 epoch。
评估指标我建议至少报三个:AUROC、AUPRC 和 F1。原因是 DDI 数据正负样本不均衡,AUROC 在极度不平衡时会显得过于乐观,AUPRC 更能反映正类的识别能力。如果你的任务是多分类,还要看宏平均和微平均的差异,宏平均对稀有类别更敏感。
划分方式要注意。随机划分会有信息泄漏——同一个药物的不同相互作用对可能被分到训练集和测试集两边,模型记住了这个药物的表征就能在测试集上蒙对。更严格的设定是按药物划分,让测试集里的药物在训练集里完全没出现过,这才是冷启动场景下的真实性能。两种划分都报,对比着看,才知道模型是真的学到了相互作用模式,还是只是记住了药物。
4.5 消融实验怎么设计才有说服力
消融是这类论文的核心证据,设计不好很容易被质疑。我建议至少做四组:去掉补语分支、去掉子结构分支只用全局池化、把门控换成直接拼接、去掉对比损失。每组都要在相同的划分和相同的随机种子上跑,至少三个种子取平均,报标准差。
还有个容易被忽略的消融:随机子结构。把子结构切分换成随机节点采样,如果性能掉得不多,说明模型其实没在用子结构的语义信息,只是在用“多片段”这个形式。这个实验我强烈建议做,能帮你判断方法的核心增益到底来自哪里。
5. 常见问题与排查技巧实录
5.1 训练不收敛或者损失震荡
最可能的原因是子结构数量不一致导致批内计算不稳定。不同分子的子结构个数不同,拼接之后张量形状对不齐,如果你的实现用了 padding,一定要配合 mask,别让 padding 的位置参与池化。
第二可能是学习率相对模型规模偏大。子结构分支图小,梯度容易爆,可以给两路分支设不同的学习率,子结构那一路小一些。第三是门控塌缩——训练几个 epoch 之后门控输出全是 0.99,另一路基本不参与,这时候损失看着在降,但模型已经退化了。加熵正则或者给门控加 dropout 都能缓解。
5.2 子结构切得太碎或者太粗
切太碎的表现是每个片段只有两三个原子,编码出来的表示几乎一样,池化之后没有区分度。解决办法是提高最小原子数阈值,同时把过小的片段合并。切太粗的表现是每个分子就一两个大片段,跟全局表征没区别,这时候要降低最大片段数或者换更细的断裂规则。
我一般会先统计一下整个数据集上子结构数量的分布,如果中位数低于 2 或者高于 12,就说明粒度参数需要调。这个统计脚本十分钟能写完,比盲目调参高效得多。
5.3 补语分支学不到有效表征
除了前面说的信息泄漏,还有一个原因是补语图规模太大、层数不够,感受野覆盖不到全局。解决办法是给补语分支单独加一个全局注意力池化,或者在补语表征上再叠一层读出网络。
另一个可能的原因是补语里有效信息太少。如果子结构切得特别大,补语就只剩几个零散原子,那这一路确实学不到东西。这时候要么调整切分粒度,要么在融合时给补语分支一个自适应的小权重,别强行让它贡献。
5.4 长尾药物与冷启动
DDI 数据的长尾非常严重,少数热门药物贡献了大量样本,大量药物只有个位数记录。表现就是模型在热门药物上指标漂亮,在冷门药物上一塌糊涂。
我的处理思路有三条。一是按药物频率分层评估,把结果拆开看,别只看总体指标。二是对低频药物做分子层面的增强,比如同骨架替换、官能团扰动,但要注意别改变分子的关键性质,增强过头等于引入噪声。三是利用子结构表征的迁移性——冷门药物虽然相互作用记录少,但它的子结构可能在热门药物里出现过,子结构分支能把这个先验带过来,这也是这套方法相对全局表征的一个潜在优势。
5.5 问题速查表
| 现象 | 可能原因 | 排查动作 | 处理建议 |
|---|---|---|---|
| 损失震荡 | 批内子结构数不一致 | 打印每批子结构数量 | 加 mask,统一 padding 策略 |
| 门控塌缩 | 两路表征尺度差异大 | 看门控输出分布 | 加熵正则,两路分别归一化 |
| 验证集掉点 | 按药物划分导致冷启动 | 对比随机划分结果 | 分场景报告,别混着说 |
| 子结构无区分度 | 切分粒度过细 | 统计片段原子数分布 | 提高最小原子数阈值 |
| 补语学不到东西 | 切分过粗或信息泄漏 | 检查跨界边处理 | 修正消融流程,调粒度 |
| 长尾性能差 | 频率偏差 | 按药物频率分层评估 | 分层采样,子结构迁移 |
提示:这套流程里最值得花时间的不是模型结构,而是子结构切分和评估划分。这两块决定了你的实验结论可不可信,模型结构反而是最容易替换的部分。
6. 知识补充:把这篇论文读透需要的背景
6.1 消息传递的基本直觉
如果对 GNN 不熟,可以用一个类比理解消息传递:每个原子初始带着自己的属性,第一轮它把属性发给直接相邻的原子,同时接收邻居发来的属性,融合之后更新自己;第二轮它带着更新后的属性再发一轮,这样两轮之后每个原子就掌握了距离两步以内的结构信息。层数就是感受野的半径。
这个直觉能解释为什么子结构分支不需要太深——子结构本身半径就小,两三层足够覆盖;也能解释为什么补语分支太深会过平滑——所有原子的属性被反复平均,最后趋同,区分度就没了。
6.2 评价指标怎么选才不误导
AUROC 衡量的是排序能力,正样本得分高于负样本的概率。它在正负比例接近时很好用,但在 DDI 这种正样本可能只占百分之几的场景下,一个把所有样本都判负的模型也能拿到很高的 AUROC,这就是所谓的“不平衡陷阱”。
AUPRC 是精确率-召回率曲线下的面积,对正类更敏感,正样本稀少时更能反映真实性能。我的习惯是主指标用 AUPRC,辅以 AUROC 做横向对比,F1 用来看阈值确定之后的实际可用性。三个指标一起报,读者才不会被单一数字带偏。
6.3 读这类论文的一个小方法
我读方法类论文有个固定顺序:先看它声称解决了什么痛点,再看它的表示是怎么构造的,然后看损失函数,最后看消融。很多人一上来就啃公式,容易迷失在符号里。
具体到这篇,核心就三个问题:子结构怎么定义、补语怎么定义、两路怎么交互。把这三个问题在纸上画出来,再回头看公式,会顺畅很多。另外,看到“融合”两个字一定要多想一层:融合之前的两个表示是不是真的互补?如果两个表示高度相关,融合就是自欺欺人。判断方法很简单,训练完之后算一下两路表示的余弦相似度分布,如果集中在 0.9 以上,说明你的补语分支基本没起作用。
我自己在这个方向上折腾下来最大的体会是,子结构这类方法的价值不在于结构有多花哨,而在于它逼着你去想“到底是分子的哪一部分在起作用”。这个问题想清楚了,哪怕你最后不用双通道,只是把子结构信息作为一种辅助监督信号加进去,也能拿到不错的收益。反过来,如果切分逻辑本身站不住脚,补语分支设计得再精巧,消融实验也撑不住。所以真要我给一个建议:先把切分和评估这两件事做扎实,再谈模型创新。