
简介基于深度学习的药物相互作用预测.zip 是一份面向药物研发、生物信息学研究者以及具备Python基础的深度学习学习者的项目资料核心任务是用神经网络模型从药物结构与药理学数据中发现潜在相互作用解决传统方法精度有限、可扩展性差的问题。压缩包共含19个文件其中13个Python脚本构成主体覆盖数据预处理、模型构建、训练评估等环节3个Jupyter Notebook提供探索性分析和可视化示例2张图片展示模型架构与图数据关系1个txt说明环境配置整体仅621KB轻量适合离线快速上手。已有192人学习特别适合想掌握Decagon框架或复现药物相互作用预测流程的读者。通过该项目能完整看到从SMILES编码、特征工程到GNN建模、指标评估的全链路并结合架构图理解图神经网络如何刻画原子与化学键拓扑为后续在临床决策支持、药物安全性分析等方向扩展应用提供扎实基础。1. 药物相互作用预测.zip多药联用越来越常见深度学习怎么算出「这两粒药能不能一起吃」一个慢病老人一天吞七八种药一个肿瘤病人化疗方案里同时出现五六种药物——这两种场景最怕的就是药物相互作用DDI两种药一起吃药效翻倍还是毒性翻倍没人敢拿病人试。基于深度学习的药物相互作用预测正是把它建模成二分类问题给定两个药物结构预测是否存在相互作用。一个名为基于深度学习的药物相互作用预测.zip的项目包常见内容是数据预处理脚本、模型代码、训练好的权重和 README解压跑通就能得到一个药物对风险打分器。它适合医药信息学研究生、药企信息化团队以及想找真实场景练手的深度学习开发者。先说个反直觉结论这类项目翻不翻车通常不取决于模型多深而取决于负样本构造和数据切分——随机切分 AUC 0.97 的模型换到按药物切分可能立刻掉到 0.7。2. 把 DDI 变成深度学习问题药物表示、数据集与样本构造拿到 zip 后我一般先做两件事把环境按 README 装好常见组合是 Python PyTorch RDKit scikit-learnGPU 机器再装对应 CUDA 版 PyTorch然后直接看数据清洗脚本。这一步别跳DDI 任务的成败大半在数据侧模型反而是相对标准的部分。2.1 从 SMILES 到特征为什么 Morgan 指纹是第一选择药物在数据库里最常见的形态是 SMILES 字符串比如阿司匹林写作CC(O)Oc1ccccc1C(O)O。深度学习模型吃不了字符串必须转成数值特征。常见方案有三种分子指纹、分子描述符、分子图。对 DDI 任务我建议先用 Morgan 指纹半径 2 的 ECFP4把基线跑通再决定要不要上更重的分子图方案。指纹的本质是「有损哈希」把每个原子周围两跳以内的子结构哈希成固定长度的 bit 向量。优点很直接——维度固定、生成快、对官能团敏感缺点则是丢掉了立体化学信息和整体拓扑关系。分子描述符logP、分子量等几百维信息量太薄通常只当补充特征。分子图信息最完整但要上 GNN调参成本高。先用指纹的原因很实际DDI 的标签本身噪声不小见 2.2特征做得再精细也补不齐标签噪声而指纹方案从构造到训练通常一小时能出第一版结果能快速判断这个数据集值不值得继续投入。import numpy as np from rdkit import Chem from rdkit.Chem import AllChem def smiles_to_fingerprint(smiles: str, radius: int 2, n_bits: int 1024): 把 SMILES 转成固定长度的 Morgan 位指纹 (ECFP4)。 mol Chem.MolFromSmiles(smiles) if mol is None: raise ValueError(fRDKit 解析失败: {smiles}) fp AllChem.GetMorganFingerprintAsBitVect(mol, radiusradius, nBitsn_bits) arr np.zeros((n_bits,), dtypenp.float32) for idx in fp.GetOnBits(): arr[idx] 1.0 return arr参数说明radius2对应 ECFP4每个特征来自原子周围两跳以内的子结构是做分子相似度、性质预测最常见的默认值改成 3 覆盖更大片段但哈希碰撞和稀疏度都会上升。n_bits1024是特征位数位数越多越稀疏药物分子普遍只有几十个重原子1024 位在区分度和计算量之间比较平衡小数据集可以降到 512 试试。GetOnBits()取出所有置 1 的位置再写入 float32 数组是为了直接喂 PyTorch 的 tensor如果分子大小差异显著可以改用计数版本指纹保留每个特征出现的次数配合 L2 归一化模型对分子尺寸会更敏感。注意解析失败的处理。跑几万条 SMILES 时Chem.MolFromSmiles一定会碰到解析不了的脏数据我的做法是单独筛出来记日志而不是让整个流程中断——这类问题在从公开数据库导出的数据里非常常见具体排查见第 5 章。2.2 公开数据集与正负样本构造负采样决定了模型上限DDI 任务的公开数据主要两个来源。DrugBank 人工整理药物间相互作用质量高、带作用类型协同、拮抗、代谢影响等但规模有限导出的交互记录通常几万到十几万条。TWOSIDES 由 FAERS 不良反应报告统计而来覆盖面广但「报告过一起事件」不等于「确认相互作用」噪声大建议按报告数量和效应强度先过滤再用。这类 zip 里通常已经整理好一份 CSV字段一般是 drug_a、drug_b、label有的带各自 SMILES。我拿到手先统计三件事label 分布、药物去重后的个数、有没有 SMILES 缺失。这三分钟能省掉后面大半排错时间。负样本构造是整个方案里最需要小心的一步。已知交互是正样本但「不在记录里的药物对」不等于「不相互作用」只是没人验证过。完全随机采样会让负样本和正样本在理化性质分布上差太多模型学到的是「分子整体长什么样」而不是「什么组合会作用」。我常用的做法是分布对齐采样按分子量、logP 分层从每层里采负样本缩小分布偏移。更严格的做法需要外部先验靶标、通路相似性工程量大适合基线跑通之后的进阶版本。import numpy as np import pandas as pd def build_ddi_samples(drug_smiles: dict, known_pairs: list, n_negatives: int 1, seed: int 42): 构造 DDI 二分类样本正样本来自标注负样本从未标注对中采样去重。 rng np.random.default_rng(seed) # frozenset 让 (a,b) 和 (b,a) 视为同一个药物对 known_set set(frozenset(p) for p in known_pairs) all_drugs np.array(list(drug_smiles.keys())) rows [[a, b, 1] for a, b in known_pairs] neg_needed len(known_pairs) * n_negatives neg_count, attempts 0, 0 while neg_count neg_needed and attempts 1_000_000: a, b rng.choice(all_drugs, size2, replaceFalse) attempts 1 pair frozenset((a, b)) if pair in known_set: continue rows.append([a, b, 0]) known_set.add(pair) # 同一个负样本不重复 neg_count 1 if neg_count neg_needed: print(f警告: 只采到 {neg_count}/{neg_needed} 个负样本药物数可能太少) return pd.DataFrame(rows, columns[drug_a, drug_b, label])逻辑说明用 frozenset 做去重键天然处理药物顺序known_set.add(pair)把采过的负样本也记进去避免同一组合反复出现导致模型过拟合。n_negatives1表示正负 1:1是 DDI 任务常见默认想模拟真实世界中「大多数药物对不合用」的先验可以设成 3 到 5但必须配合类别加权损失否则预测值会被整体压向 0。attempts 上限 100 万是防止药物数太少时 while 卡死真的采不满先查药物去重和标注规模而不是盲目调大上限。2.3 拼装 DataLoader先建指纹缓存别每轮重复算样本 DataFrame 到手后下一步是把它和指纹缓存拼成 PyTorch Dataset。指纹生成不算慢但几万对样本每轮训练都重复计算没意义先一次性算好存成 dict。import torch from torch.utils.data import Dataset class DDIDataset(Dataset): def __init__(self, df, fp_cache): self.drug_a df[drug_a].to_numpy() self.drug_b df[drug_b].to_numpy() self.label df[label].to_numpy(dtypenp.float32) self.fp_cache fp_cache def __len__(self): return len(self.label) def __getitem__(self, idx): a, b, y self.drug_a[idx], self.drug_b[idx], self.label[idx] fp_a torch.tensor(self.fp_cache[a], dtypetorch.float32) fp_b torch.tensor(self.fp_cache[b], dtypetorch.float32) return fp_a, fp_b, torch.tensor(y, dtypetorch.float32)fp_cache是{药物ID: 指纹数组}的 dict在 Dataset 外部构建后传入。这里有个容易看漏的清洗点如果药物 ID 存在别名比如数据库里同时出现 Aspirin 和 acetylsalicylic acid必须在构建缓存前统一到同一套 ID否则同一种药会变成两条不相关的记录直接拉低所有指标。别名统一我一般放在读 CSV 之后、生成样本之前用官方规范名或 DrugBank ID 做映射。3. 模型选型对称编码器、指纹 MLP 与分子图 GNN 的取舍数据准备好了模型反而不是最难的部分。DDI 预测在深度学习方法上有一个绕不开的结构约束药物对天然是对称的模型必须尊重这一点否则就是在浪费参数、强行学一个本来就该满足的性质。3.1 药物对是对称的别让网络把同一件事学两遍(阿司匹林, 华法林)和(华法林, 阿司匹林)是同一个事实。如果把两个药物的指纹拼接成 2048 维输入喂给 MLP这两个顺序会得到完全不同的输入向量但标签必须相同。网络只能靠大量样本强行记住这种对称性代价是收敛变慢而且在药物对稀疏的区域容易翻车——某个组合只出现过一次模型分不清是「顺序」导致的还是「组合」导致的。常见做法两种共享编码器加对称交互函数这是结构上更干净的选择或者在数据增强层面随机交换两份特征的顺序让网络被迫学到对称。前者更常用我一般用逐元素乘法做交互两个药物走同一个编码器得到 h_a 和 h_b然后 h_a * h_b 作为交互向量进分类头。乘法的含义是「两边同时具备某个子结构时该维度才被激活」对交互建模比较直接减法|h_a - h_b|则强调「一方有而另一方没有」进阶调优时可以拼接h_a*h_b和|h_a-h_b|一起用基线阶段先只留乘法。3.2 用 PyTorch 搭一个可复现的指纹 MLP 基准import torch import torch.nn as nn class SymmetricDDIPredictor(nn.Module): 共享编码器 逐元素乘法交互的 DDI 二分类模型。 def __init__(self, feat_dim1024, hidden_dim256, dropout0.3): super().__init__() self.encoder nn.Sequential( nn.Linear(feat_dim, hidden_dim), nn.BatchNorm1d(hidden_dim), nn.ReLU(), nn.Dropout(dropout), nn.Linear(hidden_dim, hidden_dim), nn.ReLU(), ) self.head nn.Sequential( nn.Linear(hidden_dim, 64), nn.ReLU(), nn.Linear(64, 1), ) def forward(self, fp_a, fp_b): h_a self.encoder(fp_a) h_b self.encoder(fp_b) h_pair h_a * h_b return self.head(h_pair).squeeze(-1)逻辑说明fp_a、fp_b 形状都是 (batch, 1024)。同一个 encoder 分别编码两个药物交换输入后 h_a 和 h_b 互换、h_pair 完全不变对称性从结构上被保证不需要靠数据增强补。BatchNorm1d 放在第一层 Linear 后面对稀疏特征的梯度稳定很有帮助但代价是推理时必须model.eval()否则 batch size 为 1 时的统计量会失真这点排查见第 5 章。Dropout 放在第二个隐藏层前DDI 数据集通常只有几万到十几万样本1024 维输入很容易记住训练对Dropout 是必要的过拟合刹车。参数建议hidden_dim256是起步值数据量大可以升到 512数据量少于两万对时降到 128 更稳。dropout0.3 是默认训练 loss 和验证 AUPR 差距大就提到 0.5两个 loss 都下不去就降到 0.1。head 部分我不加 BN因为 BN 对逐元素乘积这类分布的适配不稳定靠 dropout 控就够了。3.3 什么时候才值得上分子图 GNN指纹是有损哈希保留局部官能团模式但丢掉整体拓扑比如环与环之间怎么连接。GNN 从原子-键图学分子表示在性质预测上的上限通常高于指纹加 MLP。但 DDI 不是纯结构任务许多相互作用涉及代谢路径、蛋白靶点这些信息从两个分子的结构图里并不容易推断。所以我的判断标准很明确目标只是「在已知药物集合里补全标注」指纹 MLP 通常够用先别折腾 GNN目标是「新骨架药物也能预测」即冷启动泛化GNN 值得上因为图模型学的是原子连接模式而不是具体药物如果项目包自带分子图特征直接用图编码器替换指纹编码器交互头不变。用 PyTorch Geometric 做单分子编码的骨架import torch.nn.functional as F from torch_geometric.nn import GCNConv, global_mean_pool class MoleculeGCN(nn.Module): def __init__(self, in_dim, hidden_dim128, n_layers3): super().__init__() self.convs nn.ModuleList() self.convs.append(GCNConv(in_dim, hidden_dim)) for _ in range(n_layers - 1): self.convs.append(GCNConv(hidden_dim, hidden_dim)) def forward(self, x, edge_index, batch): for conv in self.convs: x torch.relu(conv(x, edge_index)) return global_mean_pool(x, batch) # 每个分子一个图级表示x 是原子特征常见组合是原子类型 one-hot、氢原子数、形式电荷、是否在环上、手性标记edge_index 是键连接表从 RDKit 建图后转成 PyG 格式。关键参数是层数分子图普遍很小几十个原子而已层数超过 4 后每个原子的感受野覆盖全图表示开始趋同oversmoothing在 DDI 数据量下基本是负收益2 到 3 层比较稳。池化先用global_mean_pool不要一上来就上注意力池化基线阶段 mean pool 的方差更小。交互头和 3.2 节完全一样两个分子的图表示做h_a * h_b进同一个分类头。整个模型从「取指纹」换成「从 SMILES 建图」外训练流程完全复用。4. 训练与评估AUPR 优先、两种切分和早停参数怎么定模型跑起来之后最需要较真的不是 loss 曲线而是「你凭什么说这个模型有用」。DDI 任务里评估设置比网络结构更容易自欺欺人尤其是类别不平衡和数据切分这两个点。4.1 为什么别拿准确率和 AUC 当唯一标准DDI 正样本是少数准确率在这种分布下没有参考价值——全预测「无相互作用」就能拿到 90% 以上的准确率。AUC 只看排序只要正样本分数普遍高于负样本就行正样本全排在中段也能拿高分。AUPR 同时惩罚「把负样本排太高」和「把正样本排太低」对不平衡更敏感也更接近筛选场景的真实诉求我需要在一堆候选组合里把少数高危组合捞出来。所以主指标用 AUPR附 AUC 便于和论文里的历史结果对比。训练损失用 BCEWithLogitsLoss 加pos_weight。这个权重做的事是「正样本判错的代价是负样本的几倍」粗略取值是负样本数除以正样本数更稳的做法是在验证集上扫几个档位再挑。zip 里如果默认用的是普通 BCE你自己加上pos_weight通常会看到 AUPR 明显提升。4.2 随机切分 vs 按药物切分两个分数回答两个问题这是 DDI 项目里最重要、也最容易被忽略的一个设置。随机切分按行切同一个药物既出现在训练又出现在测试评估的是「已知药物之间补全未知组合」分数天然偏高。按药物切分测试集里每个样本至少含一个训练时完全没见过的药物评估的是「模型能不能泛化到新药」。后者的分数通常会掉一大截但这才是「这个模型上不上得了线」的真实答案。zip 里默认的切分脚本大概率是按行随机切。复现出的 0.95 AUPR 别太当真那只说明「常见药物组合里模型能补全」如果业务场景是评估新药、新化合物必须换成按药物切分再测一遍。我对自己项目的要求是两种切分的分数都报告至少让使用者知道上限和下限在哪。import random def drug_wise_split(df, test_frac0.2, seed42): 按药物切分任一药物被划入测试集它参与的所有样本都进测试。 rng random.Random(seed) drugs sorted(set(df[drug_a]) | set(df[drug_b])) n_test_drugs max(1, int(len(drugs) * test_frac)) test_drugs set(rng.sample(drugs, n_test_drugs)) test_mask df[drug_a].isin(test_drugs) | df[drug_b].isin(test_drugs) return df[~test_mask].reset_index(dropTrue), df[test_mask].reset_index(dropTrue)逻辑说明先取全部药物做集合按比例抽样出测试药物然后把「任一端是测试药物」的样本全部划入测试集。这是一种严格的冷启动切分测试样本里的药物在训练阶段完全没见过。如果只想做温和版测试样本的两个药物都见过但组合没出现过改成两个药物都不在测试药物集合里才划入训练集就行。更稳的做法是做 5 折交叉验证每折用不同随机种子切一次最后报告均值±标准差。4.3 训练循环、早停和评估代码import torch import torch.nn as nn from sklearn.metrics import average_precision_score def evaluate_aupr(model, loader, devicecpu): model.eval() preds, labels [], [] with torch.no_grad(): for fp_a, fp_b, y in loader: logits model(fp_a, fp_b) preds.extend(torch.sigmoid(logits).tolist()) labels.extend(y.tolist()) return average_precision_score(labels, preds) def train_model(model, train_loader, val_loader, epochs60, lr1e-3, pos_weight3.0, patience10): opt torch.optim.Adam(model.parameters(), lrlr, weight_decay1e-5) criterion nn.BCEWithLogitsLoss(pos_weighttorch.tensor([pos_weight])) best_aupr, best_state, bad_epochs 0.0, None, 0 for epoch in range(epochs): model.train() total_loss 0.0 for fp_a, fp_b, y in train_loader: opt.zero_grad() loss criterion(model(fp_a, fp_b), y) loss.backward() opt.step() total_loss loss.item() val_aupr evaluate_aupr(model, val_loader) if val_aupr best_aupr: best_aupr val_aupr best_state {k: v.clone() for k, v in model.state_dict().items()} bad_epochs 0 else: bad_epochs 1 if bad_epochs patience: break if best_state is not None: model.load_state_dict(best_state) return model, best_aupr参数说明Adam 配lr1e-3、weight_decay1e-5是我的默认起点loss 震荡不降就先降学习率到 3e-4而不是加 BatchNorm 或换优化器。pos_weight不是随手填的从训练标签分布里按负样本数/正样本数算出来再微调。patience10 表示验证 AUPR 连续 10 个 epoch 不涨就停训练曲线震荡大时可以放宽到 15防止早停在半山腰。注意best_state存的是state_dict的深拷贝直接赋引用会在后续训练中跟着变这个细节坑过不少人。另外要提一句复现性训练前固定torch.manual_seed(seed)数据加载不设 shuffle 随机性时也要固定 DataLoader 的 seed否则同一个脚本两次跑出来的 AUPR 能差 0.05 以上。最后用 5 个种子各跑一遍报告均值±标准差——这是给审稿人看也是给自己看。5. 避坑指南DDI 预测里我踩过的 5 个坑现象、原因、解法模型和数据流程都通了剩下的全是细节。下面 5 个坑我都在实际项目中踩过按「现象 → 原因 → 解决」写清楚每条都能直接对照排查。5.1 随机切分 AUPR 0.93换成按药物切分直接掉到 0.68现象用默认脚本训练验证集 AUPR 漂亮得不行一到真实场景新药评估就崩。原因随机切分让同一个药物大量出现在训练和测试两侧。模型只需要记住「这个药物的指纹和某些标签相关」不需要真正理解两个药之间怎么交互就能拿高分。这不是模型不行是评估设置给了它作弊的机会也是最典型的「黑匣子」假象。解决统一用第 4 章的drug_wise_split做冷启动评估至少把按药物切分的分数当作验收线。如果两种切分差距过大说明模型主要靠记忆单药特征而不是组合交互优先考虑给模型加对称交互结构第 3 章或检查负采样是否引入了分布偏移。5.2 新药预测时指纹全零或 SMILES 解析直接失败现象训练指标正常但拿新化合物去推理返回的概率全是 0.5 附近一查指纹全是零向量。原因两个不同的问题。SMILES 解析失败多半是数据里有 rdkit 不认的写法——金属有机配合物、带括号的立体化学表达、盐类没拆分指纹全零则可能是分子本身很小1024 位里几乎没有命中或者解析退化成了几个原子。解决解析失败先用 RDKit 的SanitizeMol前后处理或正则把 counter-ion 部分拆掉再解析指纹全零就在smiles_to_fingerprint里加一个非零位数量检查比如非零位少于 8 个就告警这类分子要么换计数版本指纹要么在数据清洗阶段直接排除。别等到训练完才发现输入是垃圾。5.3 换个随机种子AUPR 飘了 0.1 以上现象固定了 PyTorch 种子但负采样脚本每次随机抽两次跑出来的 AUPR 差距很大。原因负样本是随机采的「未标注」的样本里混着大量实际有交互但没被发现的情况。换种子等于换了一批负样本模型学到的决策边界自然跟着变。这不是玄学是负采样方差。解决固定负采样种子第 2 章的seed参数并跑 5 个不同种子报告均值±标准差更严格的做法是分层负采样缩小分布偏移或在多个负采样版本上分别训练、集成都试过。单纯看单次结果的 AUPR容易被运气骗。5.4 GNN 越加层数指标越差训练 loss 还下不去现象GCN 加到 5 层、6 层验证 AUPR 不升反降训练 loss 也卡住不动。原因分子图太小层数一深每个原子的感受野覆盖全图所有原子表示趋同oversmoothing梯度也容易消失。加上 DDI 数据量本身不大深层模型纯属给优化找麻烦。解决GCN 控制在 2 到 3 层用残差连接加一层保险必要时配 PairNorm 或 DropEdge。判断标准很简单每加一层先看验证 loss 是否真的跟着降不降就回退。分子性质类任务里「深就是好」这个直觉基本不成立。5.5 高置信度预测违背药理常识模型说「没问题」的组合实际有明确禁忌现象某个组合在 DrugBank 里标注明确禁忌模型却给出 0.8 以上的「无风险」概率。原因训练数据里这类组合正样本太少指纹特征也表达不了代谢路径、靶点竞争这类机制。DDI 本质是药理过程不是纯结构相似度问题结构信息只能覆盖一部分交互模式剩下的靠数据量而数据量恰恰不够。解决把模型定位成「风险排序辅助工具」而不是「结论生成器」。对高危科室的用药清单做规则前置过滤比如已知禁忌组合直接拦截模型输出只用于对未标注组合排序、给人工复核排优先级。报告里也要写清楚这个模型能捕捉的是结构相关的交互代谢和药代动力学类交互需要换数据集或加知识图谱特征。6. 把模型接进真实筛药流程推理脚本、阈值选择与每周验证习惯模型训完、坑也排完了最后一步是把模型从 Notebook 里请出来变成一个能持续使用的工具。这一章我提供一个推理脚本、一个阈值选择思路以及我自己的验证习惯——最后这条最不值钱但最救命。6.1 一条命令跑完新药物对推理def predict_ddi(model, smiles_a, smiles_b, devicecpu): 对新药物对给出相互作用概率注意 model 必须 eval 模式。 fp_a torch.tensor([smiles_to_fingerprint(smiles_a)], dtypetorch.float32) fp_b torch.tensor([smiles_to_fingerprint(smiles_b)], dtypetorch.float32) model.eval() with torch.no_grad(): logit model(fp_a, fp_b) return torch.sigmoid(logit).item()推理时最容易犯的错是忘记model.eval()。只要模型里有 BatchNorm训练模式和 eval 模式对同一批输入的输出就不一样BatchNorm 在 batch size 为 1 时统计量会严重失真。把model.eval()写进推理函数而不是调用处是我吃过亏之后的固定习惯。6.2 阈值别用 0.5按筛查场景倒推很多项目默认 0.5 为界这在正负样本 1:1 的训练分布下勉强能用但推理时的真实场景里「有风险」的组合远少于「无风险」0.5 会让负样本大量误报成阳性。我的做法是在验证集上扫一遍阈值画 precision-recall 曲线按业务容忍度选点业务场景阈值取向理由药师人工复核辅助低阈值0.3~0.4宁可多看几对不漏掉高危组合自动拦截高风险处方高阈值0.7误拦会影响用药流程必须高置信科研候选排序不设阈值取 Top-K只关心排序不关心绝对概率如果 zip 里没附阈值参数就自己在验证集上选别继承训练脚本里那个 0.5。6.3 我自己的验证习惯和教训最后分享一个不成文的习惯每两周把模型对一批「新标注但模型没训练过」的药物对跑一遍和 DrugBank 的更新记录对比。看模型有没有在冷启动数据上保持住比盯训练曲线有用得多。我第一次做这个项目时只看了随机切分的 AUPR自我感觉良好后来在真实新药对上一测才发现模型对未见药物几乎无泛化能力——那时候才明白「按药物切分」的分数才是这东西能不能用的真相。现在无论给谁交付模型我都会附上冷启动切分的指标和三条以上的失效案例。这份小成本验证习惯希望帮到你。本文还有配套的精品资源点击获取