简介这份资源是面向计算机、人工智能、通信工程等专业学生与教师的机器学习课程设计完整包聚焦基于序列的miRNA与gene关系预测这一生物信息学课题可作为课程作业、毕业设计或项目初期立项的参考模板。压缩包共11个文件约15.75MB包含5个csv数据集文件、3个py源码脚本、1个m脚本、1个md说明文档及1个zip数据包覆盖数据、建模与训练全流程。资源中提供了两种底层实现思路一份侧重原理但理解尚浅另一份调用库中决策树完成基本功能便于读者对比学习算法落地差异。目前已有220人学习下载作者代码均经运行测试答辩评审平均分达94.5分。读者可借此掌握序列特征处理、随机森林与决策树建模、模型训练与预测评估的完整链路并在此基础上改动扩展功能。1. 序列建模做 miRNA-gene 关系预测从一份课程设计压缩包说起翻课程设计题目的时候看到「基于序列的 miRNA 和 gene 关系预测」第一反应是这题不新鲜但真要做扎实比想象中难。miRNA 是一段约 22nt 的非编码小 RNAgene 是它的靶基因两者之间的调控关系预测本质是一个二分类问题——给定一对 miRNA 序列和 gene 序列判断它们是否存在调控关系。传统做法靠序列互补配对打分比如 seed 区匹配但假阳性高得离谱。机器学习尤其是序列建模的思路是把这对序列编码成特征向量让模型自己学出「什么样的序列组合更可能成对」。这份课程设计压缩包里通常包含三样东西一份整理好的正负样本数据集、一套序列编码加模型训练的源码、以及训练好的模型权重或评估脚本。适合谁适合正在做机器学习课程设计、想找一个「数据规模可控、模型结构清晰、能讲出完整故事」的本科生或研一学生。也适合想练手序列分类任务、但不想碰图像或 NLP 大模型的工程师。它不追求 SOTA追求的是把「序列特征工程 → 模型选型 → 训练评估」这条链路走通并且每一步都能解释清楚为什么这么做。2. 数据从哪来、怎么编码miRNA-gene 配对样本的构造逻辑2.1 正负样本的来源与配对策略公开的 miRNA-gene 关系数据常见来源是 miRTarBase 和 TarBase 这类人工审编数据库。正样本就是数据库里记录为「已验证」的 miRNA-target 对。负样本的构造才是真正决定模型上限的地方。我一般会这么做从正样本里随机打乱 miRNA 和 gene 的配对关系生成「不匹配对」作为负样本同时控制正负比例在 1:1 到 1:3 之间。比例失衡太严重模型会直接摆烂全预测多数类。这里有个容易翻车的点如果负样本的 gene 序列和正样本的 gene 序列在 GC 含量或长度分布上差异太大模型可能学到的是「序列组成偏好」而不是「配对关系」。所以负采样之后最好做一次分布对齐检查看正负样本的序列长度和 GC 含量分布是否接近。import random import numpy as np def build_pairs(mirna_list, gene_list, positive_pairs, neg_ratio2): mirna_list: miRNA 序列列表 gene_list: gene 序列列表 positive_pairs: [(mirna_idx, gene_idx), ...] 正样本索引对 neg_ratio: 负样本相对正样本的倍数 pos_set set(positive_pairs) neg_pairs [] all_combos [(i, j) for i in range(len(mirna_list)) for j in range(len(gene_list))] random.shuffle(all_combos) for combo in all_combos: if combo not in pos_set: neg_pairs.append(combo) if len(neg_pairs) len(positive_pairs) * neg_ratio: break # 合并并打标签 pairs positive_pairs neg_pairs labels [1] * len(positive_pairs) [0] * len(neg_pairs) combined list(zip(pairs, labels)) random.shuffle(combined) pairs, labels zip(*combined) return list(pairs), list(labels)这段代码的核心逻辑是先构建所有可能的 miRNA-gene 组合排除掉已知正样本对剩下的作为候选负样本池再按比例采样。neg_ratio这个参数我一般设 2也就是负样本是正样本的两倍。设太小模型学不到决策边界设太大训练时间线性增长且容易过拟合负样本的噪声。采样完记得 shuffle否则正负样本在训练集里是分块排列的batch 内分布会严重偏斜。2.2 序列编码从 one-hot 到 k-mer 频率miRNA 只有 22nt 左右gene 序列可能几千 nt。直接把 gene 全序列丢进模型不现实常见做法是截取 gene 的 3‘UTR 区域因为 miRNA 主要结合在 3’UTR。截取长度一般取 500-2000nt太短会丢失结合位点上下文太长则引入大量无关序列。编码方式上one-hot 是最直观的A[1,0,0,0]C[0,1,0,0]G[0,0,1,0]U[0,0,0,1]。但 gene 序列太长one-hot 之后维度爆炸。所以更常用的是 k-mer 频率编码比如取 k3统计序列中所有 3-mer 的出现频率得到一个 64 维的向量。miRNA 和 gene 各自编码后拼接形成最终特征向量。from itertools import product from collections import Counter def kmer_freq(seq, k3): 统计序列的 k-mer 频率返回归一化向量 bases ACGU kmers [.join(p) for p in product(bases, repeatk)] kmer2idx {km: i for i, km in enumerate(kmers)} vec np.zeros(len(kmers)) seq seq.upper().replace(T, U) for i in range(len(seq) - k 1): km seq[i:ik] if km in kmer2idx: vec[kmer2idx[km]] 1 # L2 归一化避免序列长度影响 norm np.linalg.norm(vec) if norm 0: vec vec / norm return vec def encode_pair(mirna_seq, gene_seq, k_mirna2, k_gene3): miRNA 用 2-mergene 用 3-mer拼接 mirna_vec kmer_freq(mirna_seq, kk_mirna) # 16 维 gene_vec kmer_freq(gene_seq, kk_gene) # 64 维 return np.concatenate([mirna_vec, gene_vec]) # 80 维这里 miRNA 用 2-mer 是因为它本身只有 22nt用 3-mer 的话很多 3-mer 只出现一次甚至不出现频率向量太稀疏。gene 用 3-mer 是折中4-mer 会到 256 维对课程设计的计算量来说偏大。归一化那一步别省不然长序列的 k-mer 计数天然比短序列大模型会偏向长序列。k_mirna和k_gene这两个参数可以调但一般不建议超过 4维度增长太快而样本量往往撑不住。3. 模型怎么搭、怎么训从 CNN 到 Transformer 的选型与实现3.1 为什么序列任务首选 CNN 而不是 RNN序列分类任务里RNN 和 LSTM 曾经是标配但 miRNA-gene 这个场景有个特点关键信号是局部模式比如 miRNA 的 seed 区第 2-8 位和 gene 序列的互补匹配。这种局部模式用 CNN 的卷积核去捕捉比 RNN 的逐步传递更直接而且训练快得多。我一般会搭一个双通道 CNN一个通道处理 miRNA 编码一个通道处理 gene 编码各自卷积池化之后拼接再过全连接分类。import torch import torch.nn as nn class MiRNAGeneCNN(nn.Module): def __init__(self, mirna_dim16, gene_dim64, num_filters64): super().__init__() # miRNA 分支输入 1 x 16视为长度 16 的单通道序列 self.mirna_conv nn.Sequential( nn.Conv1d(1, num_filters, kernel_size3, padding1), nn.ReLU(), nn.MaxPool1d(2), nn.Conv1d(num_filters, num_filters, kernel_size3, padding1), nn.ReLU(), nn.AdaptiveAvgPool1d(1) # 输出 num_filters x 1 ) # gene 分支输入 1 x 64 self.gene_conv nn.Sequential( nn.Conv1d(1, num_filters, kernel_size3, padding1), nn.ReLU(), nn.MaxPool1d(2), nn.Conv1d(num_filters, num_filters, kernel_size3, padding1), nn.ReLU(), nn.AdaptiveAvgPool1d(1) ) self.classifier nn.Sequential( nn.Linear(num_filters * 2, 128), nn.ReLU(), nn.Dropout(0.3), nn.Linear(128, 2) ) def forward(self, mirna_x, gene_x): # mirna_x: batch x 1 x 16, gene_x: batch x 1 x 64 m self.mirna_conv(mirna_x).squeeze(-1) # batch x num_filters g self.gene_conv(gene_x).squeeze(-1) combined torch.cat([m, g], dim1) return self.classifier(combined)这个结构里num_filters设 64 是课程设计级别的合理值设 128 或 256 在小数据集上容易过拟合。Dropout(0.3)是正则化手段如果训练集小于 5000 对可以提到 0.5。两个分支各自用AdaptiveAvgPool1d(1)把序列维度压成 1这样不管输入编码维度是多少输出都是固定大小的向量方便拼接。注意 miRNA 分支的输入维度是 162-mergene 是 643-mer别搞反了。3.2 训练循环里的三个关键参数训练代码本身不复杂但有几个参数直接决定模型能不能收敛。第一个是学习率我一般从 1e-3 开始配合ReduceLROnPlateau在验证集 loss 不降时减半。第二个是 batch size序列任务里 32 或 64 比较稳太小梯度噪声大太大泛化差。第三个是早停 patience设 5-10 个 epoch别让模型在训练集上死磕。from torch.utils.data import DataLoader, TensorDataset from torch.optim import Adam from torch.optim.lr_scheduler import ReduceLROnPlateau def train_model(model, train_pairs, train_labels, val_pairs, val_labels, epochs50, batch_size32, lr1e-3, patience7): # 假设 pairs 已经编码成 mirna_vec 和 gene_vec mirna_train torch.tensor([p[0] for p in train_pairs], dtypetorch.float32).unsqueeze(1) gene_train torch.tensor([p[1] for p in train_pairs], dtypetorch.float32).unsqueeze(1) y_train torch.tensor(train_labels, dtypetorch.long) mirna_val torch.tensor([p[0] for p in val_pairs], dtypetorch.float32).unsqueeze(1) gene_val torch.tensor([p[1] for p in val_pairs], dtypetorch.float32).unsqueeze(1) y_val torch.tensor(val_labels, dtypetorch.long) train_loader DataLoader(TensorDataset(mirna_train, gene_train, y_train), batch_sizebatch_size, shuffleTrue) optimizer Adam(model.parameters(), lrlr, weight_decay1e-4) scheduler ReduceLROnPlateau(optimizer, modemin, factor0.5, patience3) criterion nn.CrossEntropyLoss() best_val_loss float(inf) wait 0 for epoch in range(epochs): model.train() for m_x, g_x, y in train_loader: optimizer.zero_grad() logits model(m_x, g_x) loss criterion(logits, y) loss.backward() optimizer.step() # 验证 model.eval() with torch.no_grad(): val_logits model(mirna_val, gene_val) val_loss criterion(val_logits, y_val).item() scheduler.step(val_loss) if val_loss best_val_loss: best_val_loss val_loss wait 0 torch.save(model.state_dict(), best_model.pt) else: wait 1 if wait patience: print(fEarly stop at epoch {epoch}) break return modelweight_decay1e-4是 L2 正则配合 Dropout 一起用防止模型记住训练集里的噪声配对。ReduceLROnPlateau的patience3和早停的patience7是两回事前者是学习率不降就减半后者是验证 loss 不降就停。这两个 patience 别设成一样否则学习率刚减半还没来得及见效就被早停掐了。保存best_model.pt而不是最后一个 epoch 的模型是因为过拟合之后验证集性能会掉最后一个 epoch 未必最好。4. 避坑与排查序列关系预测里最容易翻车的五件事4.1 正负样本泄漏训练集和测试集里出现了同一对 miRNA-gene现象模型在测试集上准确率 95% 以上换一批新数据直接掉到 60%。原因构造数据集时按样本随机划分同一对 miRNA-gene 可能同时出现在训练集和测试集里模型只是记住了这对组合的标签。解决按 miRNA 或 gene 维度做划分确保训练集里的 miRNA 和测试集里的 miRNA 不重叠。如果数据量不够至少按 gene 划分因为 gene 序列更长记忆效应更明显。4.2 序列编码时 T 和 U 没统一现象模型训练 loss 正常下降但预测新序列时结果完全随机。原因miRNA 序列在数据库里可能用 T 表示而 RNA 实际是 U编码时如果没做replace(T, U)同一个碱基会被编码成两个不同的维度。解决在kmer_freq函数里统一转成 U并且检查输入序列里有没有 N 或其它非 ACGU 字符有的话要么剔除要么用统一占位符替代。4.3 gene 序列截取位置不对现象模型在验证集上 AUC 只有 0.6 左右怎么调参都上不去。原因gene 序列截取时没有定位到 3‘UTR而是从 CDS 或 5’UTR 开始截导致 miRNA 结合位点根本不在输入序列里。解决如果数据集里没有标注 UTR 区域至少确认截取的是 gene 的 3‘端。常见做法是取 gene 序列最后 1000-2000nt因为 3’UTR 通常在基因末端。4.4 batch 内正负样本比例失衡现象训练 loss 震荡剧烈准确率在 50% 附近来回跳。原因DataLoader 的 shuffle 虽然打乱了样本顺序但如果正负样本在数据集里是分块排列的一个 batch 里可能全是正样本或全是负样本。解决在构造 TensorDataset 之前先 shuffle 一次或者用WeightedRandomSampler按类别权重采样。更简单的办法是确保build_pairs返回时已经打乱并且 DataLoader 的shuffleTrue别关。4.5 用准确率当唯一指标现象模型准确率 85%看起来不错但实际预测时几乎把所有样本都判为负类。原因正负样本比例 1:3 时全预测负类就有 75% 准确率。解决看 AUC、F1 和召回率。特别是召回率如果模型漏掉太多正样本在实际应用里等于没预测。我一般要求正类召回率不低于 0.7否则回去检查负采样比例和分类阈值。5. 把模型用起来从单对预测到批量筛选的工程化技巧训练完模型只是第一步真正要用起来得解决「给定一条 miRNA怎么从几千条 gene 里筛出最可能成对的候选」这个问题。最直接的做法是遍历所有 gene逐对预测概率然后按概率排序取 top-k。但这样计算量是 O(N)如果 gene 有 20000 条每条都要编码加推理耗时不可接受。我一般会做两件事一是把 gene 编码提前算好缓存成矩阵推理时只做模型前向二是用 batch 推理一次喂 256 或 512 对GPU 利用率拉满。def rank_genes_for_mirna(model, mirna_seq, gene_seqs, top_k50, batch_size256): 给定一条 miRNA对候选 gene 列表按预测概率排序 model.eval() mirna_vec kmer_freq(mirna_seq, k2) mirna_tensor torch.tensor(mirna_vec, dtypetorch.float32).unsqueeze(0).unsqueeze(0) # 提前编码所有 gene gene_vecs np.array([kmer_freq(g, k3) for g in gene_seqs]) gene_tensor torch.tensor(gene_vecs, dtypetorch.float32).unsqueeze(1) all_probs [] with torch.no_grad(): for i in range(0, len(gene_seqs), batch_size): batch_gene gene_tensor[i:ibatch_size] batch_mirna mirna_tensor.expand(batch_gene.size(0), -1, -1) logits model(batch_mirna, batch_gene) probs torch.softmax(logits, dim1)[:, 1] # 正类概率 all_probs.extend(probs.cpu().numpy()) # 排序取 top-k ranked sorted(zip(gene_seqs, all_probs), keylambda x: x[1], reverseTrue) return ranked[:top_k]这里mirna_tensor.expand是把单条 miRNA 的编码复制到和 batch 内 gene 数量一致避免重复编码。torch.softmax取正类概率作为打分比直接取 logits 更直观。top_k设 50 是经验值实际用的时候可以结合一个概率阈值比如只保留概率大于 0.8 的候选避免 top-k 里混进低置信度的预测。还有一个进阶技巧模型集成。单独一个 CNN 的方差可能比较大训练 3-5 个不同随机种子或不同 k-mer 参数的模型把预测概率平均AUC 通常能涨 2-3 个点。代价是推理时间翻倍但在课程设计里完全可接受。我自己的习惯是至少训三个种子取验证集 AUC 最高的那个保存但最终预测时用三个模型的平均。这个习惯是从一次翻车经历来的当时只训了一个模型测试集 AUC 0.82换了个随机种子重训变成 0.76方差大到没法接受。后来固定用集成结果稳定多了。希望帮到你。本文还有配套的精品资源点击获取