
简介这份资源是面向计算机、人工智能、通信工程等专业学生与教师的机器学习课程设计完整包聚焦基于序列的miRNA与gene关系预测这一生物信息学课题可作为课程作业、毕业设计或项目初期立项的参考方案。压缩包共11个文件约15.75MB包含5个csv数据集文件、3个py源码、1个m脚本、1个md说明及1个zip数据包覆盖数据、建模与训练全流程。其中既有随机森林算法底层实现代码也有调用库中决策树的版本并附有训练脚本与序列数据便于读者对比不同实现思路、理解特征构建与模型评估环节。资源已有220人学习下载作者代码均经运行测试答辩评审平均分达94.5分。读者可借此快速掌握序列特征处理、决策树与随机森林建模的完整链路并在此基础上改动扩展功能。1. 序列建模做 miRNA-gene 关系预测一份课程设计里真正能跑通的路miRNA 和 gene 的关系预测是机器学习课程设计里少见的「数据不大、模型不深、但每一步都能讲出道理」的题目。它要解决的问题很具体给定一条 miRNA 序列和一条 gene 序列判断它们之间是否存在调控关系。传统做法靠序列比对和热力学计算慢且依赖先验换成序列建模的思路就是把两条生物序列当成文本用编码器各自抽特征再做交互判断。这套方案适合谁适合正在做机器学习课程设计、手头有 miRNA/gene 配对数据、想找一个既能体现特征工程又能体现模型设计的题目的同学。它不需要 GPU 集群一台普通笔记本就能跑完训练但里面的坑一点不少——数据泄漏、负样本构造、序列编码方式任何一个处理不好准确率都能虚高到 0.99 然后一测就崩。下面按「数据怎么来、特征怎么抽、模型怎么搭、坑在哪」的顺序讲清楚。2. 数据准备与负样本构造决定上限的一步2.1 miRNA 与 gene 序列数据的来源和格式miRNA 序列通常从 miRBase 拿长度在 18 到 25 nt 之间成熟体序列短且保守。gene 序列一般取 3UTR 区域因为 miRNA 主要通过结合 3UTR 发挥作用长度从几百到几千 nt 不等。课程设计里常见的数据组织方式是三列miRNA 名称、gene 名称、标签1 表示有调控关系0 表示无。也有把序列直接拼进去的变成 miRNA 序列、gene 序列、标签三列。我一般会把数据整理成两个独立的 FASTA 或 CSV 文件一个存正样本对一个存候选负样本池。正样本来自已验证的数据库记录负样本不能随便拿两条序列凑一对这是后面会重点讲的坑。数据量方面课程设计级别通常几千到几万对就够太多反而训练慢、调参周期长。整理完的数据建议做一次去重和长度过滤miRNA 长度不在 18-25 的直接丢掉gene 序列短于 50 nt 的也丢掉因为太短的序列抽不出有意义的特征。这一步用 pandas 几行就能完成。import pandas as pd # 读取原始配对数据 df pd.read_csv(mirna_gene_pairs.csv) # 过滤 miRNA 长度异常的记录 df df[(df[mirna_len] 18) (df[mirna_len] 25)] # 过滤 gene 序列过短的记录 df df[df[gene_len] 50] # 按 (mirna, gene) 去重保留第一条 df df.drop_duplicates(subset[mirna_id, gene_id], keepfirst) print(f过滤后样本数: {len(df)}, 正样本: {df[label].sum()}) df.to_csv(pairs_clean.csv, indexFalse)这段代码做了三件事长度过滤、去重、输出清洗后的文件。mirna_len和gene_len是预处理时算好的列如果没有就先用len()算出来。去重那一步很关键同一对 miRNA-gene 在数据库里可能因为不同实验证据重复出现不去重会导致训练集和验证集出现同一条记录指标虚高。2.2 负样本怎么造才不算作弊负样本构造是这类任务里最容易翻车的地方。常见错误做法是随机拿一条 miRNA 和一条 gene 配对标成 0。问题在于随机配对里绝大多数确实没有关系但模型学到的可能只是「这两条序列长度差异大」或者「碱基组成不匹配」这种表面特征而不是真正的调控模式。更糟的是如果负样本里混进了实际存在但未被数据库收录的关系模型会把对的判成错的训练信号被污染。我一般用两种策略结合。第一种是「同基因不同 miRNA」选一个已知和某 gene 有关系的 miRNA再从其他 gene 的 miRNA 池里换一条进来保持 gene 不变。第二种是「同 miRNA 不同基因」保持 miRNA 不变换一个不相关的 gene。这样构造的负样本在序列组成上和正样本更接近模型必须学到更细的交互特征才能区分。import random import pandas as pd pos pd.read_csv(positive_pairs.csv) all_mirna pos[mirna_id].unique().tolist() all_gene pos[gene_id].unique().tolist() neg_records [] for _, row in pos.iterrows(): # 策略一换 miRNA保持 gene 不变 other_mirna random.choice([m for m in all_mirna if m ! row[mirna_id]]) neg_records.append({ mirna_id: other_mirna, gene_id: row[gene_id], label: 0 }) # 策略二换 gene保持 miRNA 不变 other_gene random.choice([g for g in all_gene if g ! row[gene_id]]) neg_records.append({ mirna_id: row[mirna_id], gene_id: other_gene, label: 0 }) neg_df pd.DataFrame(neg_records).drop_duplicates() # 正负样本按 1:1 或 1:2 混合 final_df pd.concat([pos, neg_df]).sample(frac1, random_state42) final_df.to_csv(dataset_balanced.csv, indexFalse)这里正负比例建议控制在 1:1 到 1:2 之间。负样本太多会让模型偏向预测 0太少又学不到区分边界。random_state固定住保证每次跑出来的数据集一致方便复现。构造完负样本后一定要检查有没有和正样本重复的对有的话删掉否则就是自己给自己埋雷。3. 序列编码与特征抽取把碱基变成模型能吃的数字3.1 从 one-hot 到 k-mer三种编码方式的取舍序列模型的第一步是把 ACGT 变成数字。最直接的是 one-hot 编码每个碱基映射成一个 4 维向量A[1,0,0,0]C[0,1,0,0]以此类推。一条 22 nt 的 miRNA 就变成 22×4 的矩阵。这种方式保留位置信息但维度随序列长度线性增长gene 序列动辄上千 nt直接 one-hot 会让输入维度爆炸。第二种是 k-mer 频率编码。把序列切成所有长度为 k 的子串统计每个子串出现的频率得到一个固定长度的向量。k3 时共有 4³64 种组合不管序列多长输出都是 64 维。这种方式丢掉了位置信息但维度固定、计算快适合 gene 这种长序列。k 取 3 到 5 比较常见k 太大向量稀疏k 太小区分度不够。第三种是学习式编码用 Embedding 层让模型自己学每个碱基或 k-mer 的向量表示。这种方式灵活但需要足够的数据量才能学好课程设计级别几千条数据用 Embedding 容易过拟合。我一般 miRNA 用 one-hot短位置信息重要gene 用 k-mer 频率长需要降维两者拼接后送入模型。这样既保留了 miRNA 的序列顺序又控制了 gene 的输入维度。import numpy as np from itertools import product # one-hot 编码用于 miRNA def one_hot_encode(seq): mapping {A: 0, C: 1, G: 2, U: 3, T: 3} max_len 25 mat np.zeros((max_len, 4), dtypenp.float32) for i, base in enumerate(seq[:max_len]): if base in mapping: mat[i, mapping[base]] 1.0 return mat # k-mer 频率编码用于 gene def kmer_freq(seq, k3): kmers [.join(p) for p in product(ACGU, repeatk)] index {kmer: i for i, kmer in enumerate(kmers)} vec np.zeros(len(kmers), dtypenp.float32) seq seq.replace(T, U) for i in range(len(seq) - k 1): sub seq[i:ik] if sub in index: vec[index[sub]] 1 total vec.sum() return vec / total if total 0 else vecone_hot_encode里把 T 映射到 U 的位置因为 miRNA 是 RNA 序列但数据库里有时用 T 表示。max_len25是 miRNA 的常见上限短的补零长的截断。kmer_freq里先做 T→U 替换再统计频率并归一化保证不同长度的 gene 序列输出在同一量纲上。k3 时输出 64 维如果数据里 gene 序列特别长可以试 k4输出 256 维但要注意样本量是否撑得住。3.2 特征拼接与标准化别让量纲差异毁掉训练miRNA 的 one-hot 矩阵展平后是 25×4100 维gene 的 k-mer 是 64 维拼起来 164 维。如果直接用one-hot 里大量 0 和 1k-mer 是 0 到 1 之间的小数两者尺度不一致梯度下降会震荡。常见做法是对拼接后的特征做一次标准化或者分别对两路特征做 BatchNorm。我一般会在拼接前对 k-mer 特征做一次 L2 归一化one-hot 保持原样因为 one-hot 本身就是 0/1不需要再标准化。拼接后再过一层全连接把维度压到 128 或 64让模型自己学怎么融合两路信息。import torch import torch.nn as nn class FeatureEncoder(nn.Module): def __init__(self, mirna_dim100, gene_dim64, hidden128): super().__init__() self.mirna_fc nn.Linear(mirna_dim, 64) self.gene_fc nn.Linear(gene_dim, 64) self.fuse nn.Sequential( nn.Linear(128, hidden), nn.ReLU(), nn.Dropout(0.3), nn.Linear(hidden, 64) ) def forward(self, mirna_feat, gene_feat): m torch.relu(self.mirna_fc(mirna_feat)) g torch.relu(self.gene_fc(gene_feat)) combined torch.cat([m, g], dim-1) return self.fuse(combined)mirna_fc和gene_fc各自把输入压到 64 维保证两路特征在拼接时尺度接近。Dropout(0.3)是防过拟合的常规操作课程设计数据量不大dropout 可以设到 0.3 到 0.5。融合层最后输出 64 维后面接分类头做二分类。如果显存够可以把 hidden 调到 256但要注意验证集 loss 是否开始上升。4. 模型搭建与训练从 CNN 到注意力选哪个不玄学4.1 用 CNN 做序列特征提取的最小实现序列数据天然适合卷积因为局部模式比如 miRNA 的种子区对调控关系影响很大。一维卷积在序列上滑动能捕捉到连续的碱基模式。我一般用两层 Conv1dkernel size 分别取 3 和 5覆盖不同长度的模式然后接全局最大池化把变长输出压成固定维度。class CNNEncoder(nn.Module): def __init__(self, in_channels4, embed_dim64): super().__init__() self.conv1 nn.Conv1d(in_channels, 32, kernel_size3, padding1) self.conv2 nn.Conv1d(32, embed_dim, kernel_size5, padding2) self.pool nn.AdaptiveMaxPool1d(1) def forward(self, x): # x: (batch, 4, seq_len) x torch.relu(self.conv1(x)) x torch.relu(self.conv2(x)) x self.pool(x).squeeze(-1) # (batch, embed_dim) return xin_channels4对应 one-hot 的四个碱基通道输入需要转成(batch, 4, seq_len)的格式。padding1和padding2保证卷积后序列长度不变方便堆叠。AdaptiveMaxPool1d(1)把整个序列压成一个向量不管输入多长输出都是embed_dim维。这个编码器可以同时用于 miRNA 和 gene只是 gene 序列更长卷积核可以适当加大到 7 或 9。4.2 双路模型拼接与分类头的参数设置miRNA 和 gene 各走一个编码器输出两个向量后拼接再过分类头。分类头一般是两层全连接加 dropout最后输出一个 logit用 BCEWithLogitsLoss 算损失。学习率我一般从 1e-3 开始用 Adam 优化器如果验证集 loss 不降就降到 1e-4。batch size 设 32 或 64课程设计数据量下 32 比较稳。class MiRNAGeneModel(nn.Module): def __init__(self): super().__init__() self.mirna_enc CNNEncoder(in_channels4, embed_dim64) self.gene_enc CNNEncoder(in_channels4, embed_dim64) self.classifier nn.Sequential( nn.Linear(128, 64), nn.ReLU(), nn.Dropout(0.4), nn.Linear(64, 1) ) def forward(self, mirna, gene): m self.mirna_enc(mirna) g self.gene_enc(gene) x torch.cat([m, g], dim-1) return self.classifier(x).squeeze(-1)训练循环里注意三点一是正负样本要打乱用 DataLoader 的 shuffle二是每轮记录验证集的 AUC 和 F1不要只看准确率因为正负比例可能不均衡三是早停验证集 AUC 连续 5 轮不升就停防止过拟合。课程设计里常见的一个问题是训练集准确率冲到 0.98验证集只有 0.7这就是过拟合的典型信号加 dropout、减层数、加正则都能缓解。4.3 训练脚本与关键超参from torch.utils.data import DataLoader, TensorDataset import torch.optim as optim # 假设 X_mirna, X_gene, y 已经准备好 dataset TensorDataset( torch.tensor(X_mirna, dtypetorch.float32), torch.tensor(X_gene, dtypetorch.float32), torch.tensor(y, dtypetorch.float32) ) loader DataLoader(dataset, batch_size32, shuffleTrue) model MiRNAGeneModel() optimizer optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) criterion nn.BCEWithLogitsLoss() for epoch in range(50): model.train() total_loss 0 for mirna, gene, label in loader: optimizer.zero_grad() logits model(mirna, gene) loss criterion(logits, label) loss.backward() optimizer.step() total_loss loss.item() print(fEpoch {epoch}, Loss: {total_loss / len(loader):.4f})weight_decay1e-4是 L2 正则配合 dropout 一起防过拟合。BCEWithLogitsLoss内部做了 sigmoid比先 sigmoid 再 BCELoss 数值更稳定。训练轮数设 50 是上限实际靠早停控制。如果 loss 降到 0.1 以下还在降但验证集指标不动说明模型在背训练集该停了。5. 避坑与排查那些让指标虚高又让你答辩翻车的细节5.1 数据泄漏同源序列混进验证集现象训练集 AUC 0.99验证集 AUC 0.99换一批新数据掉到 0.6。原因同一家族的 miRNA 序列高度相似随机划分数据集时相似的序列同时出现在训练集和验证集模型记住了模式而不是学到规律。解决按 miRNA 家族或序列相似度聚类后再划分保证验证集里的 miRNA 和训练集不重叠。简单做法是用 CD-HIT 对 miRNA 序列去冗余阈值设 0.8然后按簇划分。5.2 负样本构造引入的假阴性现象模型在测试集上把一些已知有关系的对预测成 0。原因负样本里混进了实际存在但数据库未收录的关系模型学到「这类序列对是负的」遇到真阳性反而判错。解决负样本构造时避开已知关系的 miRNA-gene 对可以用数据库的全量关系表做一次过滤确保负样本不在正样本集合里。另外负样本比例不要超过 1:2太多负样本会让模型偏向预测 0。5.3 序列编码的 T/U 不一致现象训练时 loss 正常下降但预测新序列时结果全错。原因训练数据里 miRNA 用 U 表示预测时输入用了 Tone-hot 映射表里 T 和 U 映射到同一个位置还好但如果 k-mer 编码时没做替换T 开头的 k-mer 全部匹配不上特征全零。解决在编码函数入口统一做seq.replace(T, U)不管输入是 DNA 还是 RNA 格式先转成统一表示再编码。5.4 过拟合训练集和验证集指标差距过大现象训练 loss 降到 0.05验证 loss 停在 0.6 不降。原因模型参数太多、数据太少、dropout 不够。解决先减模型容量把 CNN 通道数从 64 降到 32全连接层从 128 降到 64再把 dropout 从 0.3 提到 0.5最后加 L2 正则weight_decay 从 1e-4 提到 1e-3。三招下去还过拟合说明数据量确实不够考虑用数据增强比如对 miRNA 序列做同义替换种子区不动其他位置换碱基。5.5 类别不均衡导致的阈值偏移现象模型输出的概率集中在 0.3 到 0.5 之间用 0.5 做阈值时召回率很低。原因正负样本比例不均衡模型偏向预测多数类。解决训练时用pos_weight参数给正样本加权或者训练后调整分类阈值用验证集画 PR 曲线选 F1 最大的点作为阈值。不要死守 0.5。6. 进阶技巧用交叉验证和注意力可视化把课程设计做出深度课程设计如果只做到训练一个 CNN 看准确率答辩时很难讲出深度。我一般会加两个东西一是 k 折交叉验证二是注意力权重可视化。k 折交叉验证能把数据用到极致同时给出指标的置信区间比单次划分更有说服力。具体做法是把数据分成 5 折每次用 4 折训练、1 折验证跑 5 次取平均 AUC 和标准差。如果 5 折的 AUC 标准差超过 0.05说明数据划分不稳定需要检查是否有泄漏或者样本量太小。from sklearn.model_selection import KFold import numpy as np kf KFold(n_splits5, shuffleTrue, random_state42) auc_scores [] for fold, (train_idx, val_idx) in enumerate(kf.split(X_mirna)): X_m_train, X_g_train X_mirna[train_idx], X_gene[train_idx] X_m_val, X_g_val X_mirna[val_idx], X_gene[val_idx] y_train, y_val y[train_idx], y[val_idx] model MiRNAGeneModel() # ... 训练代码同上省略 ... # 训练完后在验证集上算 AUC # auc roc_auc_score(y_val, preds) # auc_scores.append(auc) print(fFold {fold}, AUC: {auc:.4f}) print(fMean AUC: {np.mean(auc_scores):.4f} ± {np.std(auc_scores):.4f})注意力可视化是另一个加分项。把 CNN 换成带注意力池化的结构训练完后把注意力权重画在序列上看模型关注哪些位置。如果模型在 miRNA 的 5 端种子区第 2 到第 8 位权重高说明它学到了生物学上已知的规律答辩时这就是一个有力的解释。具体实现是在 CNN 输出后加一个注意力层对每个位置的卷积输出算一个权重加权求和代替最大池化。class AttentionPool(nn.Module): def __init__(self, dim): super().__init__() self.attn nn.Linear(dim, 1) def forward(self, x): # x: (batch, dim, seq_len) x x.transpose(1, 2) # (batch, seq_len, dim) weights torch.softmax(self.attn(x), dim1) # (batch, seq_len, 1) out (x * weights).sum(dim1) # (batch, dim) return out, weights.squeeze(-1)把CNNEncoder里的AdaptiveMaxPool1d换成AttentionPool训练完后取出weights对验证集里的正样本画热图横轴是序列位置纵轴是样本颜色深浅表示注意力大小。如果高注意力区域集中在种子区说明模型学到的模式和生物学先验一致这个图放在答辩 PPT 里比单纯报准确率有说服力得多。最后说一个我自己的习惯每次跑完实验不管指标好坏都把超参数、数据划分方式、随机种子记在一个表格里。课程设计周期短很容易改了参数忘了之前跑的是什么回头想复现最佳结果时找不到配置。这个习惯帮我省过很多后悔药。希望帮到你。本文还有配套的精品资源点击获取