资讯动态

STR混合图谱解析四问:分类建模、非负最小二乘与小波去噪

发布时间:2026/10/30 5:15:55 来源:尧图企业网站定制
简介面向2025深圳杯数学建模竞赛D题参赛者与数学建模学习者的完整分析论文PDF围绕赛题给出从问题重述、问题分析、模型假设与符号定义到多问建模求解的系统框架适合赛前梳理思路、赛后复盘及机器学习建模方法参考。资源包共1个PDF文件约2.71MB正文以论文形式整合模型推导、可运行代码与相关数据说明目录覆盖特征工程设计、分类模型结构与数学表达、模型总体性能对比、不同贡献者数量表现以及问题三的贡献者兼容性筛选、混合比例优化估计和组合匹配评分函数等模块。已有330人学习下载。读者可据此理清D题完整解题链路理解分类模型构建、评估指标与优化匹配的实现细节并借助论文中的代码和数据复现实验、对照结果提升把建模思路转化为可验证方案的能力。1. 半夜对着一个准确率下降的分类器时这份 D 题论文能帮你解决什么问题凌晨两点我把一份混合样本的峰高表丢进分类器2 人混合样本的 F1 冲到 0.99换成 4 人混合样本后直接掉到 0.34特征没改、模型没改、随机种子也没改。这是我第一次真正意识到STR 混合图谱解析的核心难点不在于是选 GBDT 还是随机森林而在于人数一多、等位基因一开始重叠特征空间就失去了区分度。这份某数学建模竞赛 D 题的完整分析论文讨论的正是这个场景从混合 STR 图谱里判断有几个人、各自占多少比例、还原每个人的基因型、再把噪声和拖尾峰清理掉。四个问题从判别到回归再到组合搜索最后一问落在信号去噪上形成一条完整链路。论文带模型推导、可运行代码和数据适合正在备赛建模的同学也适合想练一遍「多分类 非负最小二乘 小波阈值」这条技术路线的开发者。2. 四问一条流水线模型分层、技术栈与可执行环境2.1 四个问题不是四篇独立论文而是一条前后强耦合的流水线很多人拿到这类论文会按章节单独阅读读完四章仍不知道代码怎么串起来。这四个问题的实际依赖关系是单向的问题一输出贡献者人数 N问题二在 N 已知的前提下才能确定比例类别的维度问题三需要 N 才能确定枚举组合的规模问题四的去噪结果又反哺前三问的输入质量。任何一环判断错误误差都会向后传播。论文的结构设计恰好对应这条链路。问题一被建模成多类分类标签是 2/3/4/5 人特征层从等位基因结构、峰高统计、片段长度分布、基因座 one-hot 四个方向提取模型层对比了随机森林、GBDT、MLP、SVM 四套方案。问题二把连续的比例估计离散成类别分类按人数分别训练分类器——2 人用 GBDT、3 人用 MLP、4 到 5 人用随机森林。问题三转成组合搜索先按兼容性把候选个体池筛到能枚举的规模再用加权最小二乘解混合比例最后用综合评分排序。问题四引入小波阈值去噪、拖尾峰规则过滤、自适应峰值筛选三段处理。值得注意的是问题二的建模思路。严格来说比例是连续量直接做回归更自然但论文选择了离散类别理由是样本量小、类别设计本身就是竞赛给定的网格1:1、1:4、1:9 这类当成分类问题能直接复用问题一的特征管线和评估指标工程上更省事。这个取舍在数据量有限的场景下是合理的代价是泛化到网格外的比例时会失效——这一点在二次开发部分值得再处理。问题任务类型主模型关键输出问题一多类分类GBDT贡献者人数 N问题二多类分类按 N 分组建模GBDT / MLP / RF各贡献者占比问题三组合搜索 非负最小二乘兼容性评分 加权最小二乘基因型组合问题四信号去噪 匹配小波阈值 规则过滤清洗后的峰表2.2 技术栈选型为什么最终是 sklearn 配 PyWavelets 这一套整个方案没有引入深度学习框架模型层全部落在 scikit-learn 上。论文里 GBDT 的准确率是 0.9608交叉验证 0.9584±0.0136随机森林 0.7255MLP 0.7549 但交叉验证只有 0.3603SVM 0.5931。这组数字本身就说明了选型逻辑样本量在千级特征维度在二十上下树模型在这个区间几乎总是赢家而 MLP 训练不充分时交叉验证会大幅下滑这正是它 0.36 那个数字的来源。问题四的信号处理部分依赖 PyWavelets。小波分解、软阈值、重构这三个动作在 pywt 里各一个函数就能完成不需要自己写滤波器组。数据处理和可视化走 pandas、numpy、matplotlib交互调试用 Jupyter。这套组合的安装成本很低一台普通笔记本十几分钟能配完。提示论文正文里的公式在 OCR 后有一定丢失符号定义那一章尤其明显。拿代码对照公式读比硬啃 PDF 快得多。2.3 把 PDF 里的代码变成能跑的工程论文给的是分析文档加代码片段直接复制粘贴跑不通得先补一层工程结构。我一般会按照「数据、特征、模型、评估」四个目录切开让每一问对应一个 notebook公共逻辑抽到 src 下面。这样改一个问题不会牵连其他三个。# 建议的环境准备Python 3.10 上下都行 python -m venv .venv source .venv/bin/activate # Windows 用 .venv\Scripts\activate pip install numpy pandas scikit-learn scipy PyWavelets matplotlib jupyter目录按下面这个形状铺开能让后面每一章的代码都有明确去处str-mix/ ├── data/ │ ├── raw/ # 题目给的混合图谱峰表一个样本一行或一个基因座一行 │ └── reference/ # 候选个体基因型库问题三要用 ├── src/ │ ├── features.py # 特征构造函数问题一、二共用 │ ├── train_q1.py # 人数判别训练脚本 │ ├── train_q2.py # 比例分类训练脚本 │ ├── search_q3.py # 兼容性筛选 最小二乘配比 │ └── denoise_q4.py # 小波阈值去噪 拖尾峰过滤 ├── notebooks/ # 四个问题各一个负责画图和对比实验 └── outputs/ # 模型文件、评估报告、去噪前后对照图把公共特征函数放进features.py而不是复制到两个 notebook 里是我踩过坑之后养成的习惯。问题一和问题二的特征高度重叠一旦两个地方各改一版后面做消融实验时你根本说不清是哪份代码在跑。3. 特征工程拆解把峰高表变成特征向量的每一步3.1 图谱原始字段长什么样原始数据的最小单元是「一个样本 × 一个基因座」下的一组峰每个峰带四个字段等位基因编号allele、峰高height、片段长度size、是否 OL 标记is_ol。OL 指的是无法与标准梯度比对的片段这类峰在论文里被单独计数因为它们的数量本身就和混合复杂度相关。论文的观察是单个基因座下正常情况最多两个等位基因超过两个就基本可以判定为多人混合。这个判断构成了整个特征工程的地基——allele_count和expected_allele_ratio之所以在特征重要性排序里排到前两位就是因为它们直接刻画了这个结构信号。峰高则服从明显的幂律分布大多数峰集中在低位少数主峰高度突出主次峰比值top1/top2能反映混合是否均衡。数据分布上还有一个细节值得记住论文统计的样本里2 人和 3 人混合各占 240 和 256 个4 人 176 个5 人 144 个。类别本身就不均衡所以评估时必须看加权 F1 和混淆矩阵只看 accuracy 会被多数类带偏。3.2 特征构造函数的逐段拆解特征构造是整个方案里最值得抄的一段代码因为它同时服务问题一和问题二改动一处就能影响两个任务的输入。下面这个版本按论文的特征清单重写把「训练时可用的特征」和「预测时可用的特征」分开处理。import numpy as np import pandas as pd # 16 个核心基因座按论文顺序编号避免直接写死具体位点名 CORE_LOCI [fLocus{i:02d} for i in range(1, 17)] def locus_features(peaks: pd.DataFrame, n_contrib: int | None None) - dict: peaks: 单个基因座下的峰表必须含 allele / height / size / is_ol 四列 n_contrib: 真实贡献者人数。只在离线造训练集时传入 线上预测时必须传 None否则会造出标签泄漏特征 h peaks[height].to_numpy(dtypefloat) h h[h 0] # 零峰视为未检出先剔掉 h_sorted np.sort(h)[::-1] # 降序方便取 top-k feats {} # 1) 结构特征等位基因数量类 feats[allele_count] len(peaks) feats[non_ol_count] int((peaks[is_ol] 0).sum()) feats[ol_ratio] 1.0 - feats[non_ol_count] / max(len(peaks), 1) # 2) 峰高统计特征均值、离散度、极值 feats[height_mean] float(h.mean()) if len(h) else 0.0 feats[height_std] float(h.std(ddof0)) if len(h) else 0.0 feats[height_cv] feats[height_std] / max(feats[height_mean], 1e-6) feats[height_min] float(h.min()) if len(h) else 0.0 feats[height_max] float(h.max()) if len(h) else 0.0 feats[height_range] feats[height_max] - feats[height_min] # 3) 主次峰比值判断是否存在单方主导 top np.pad(h_sorted[:4], (0, max(0, 4 - len(h_sorted))), constant_values0.0) feats[height_top1] float(top[0]) feats[height_top4] float(top[3]) feats[top1_top2_ratio] float(top[0] / top[1]) if top[1] 0 else 10.0 feats[height_ratio_mean] float(top[:3].mean() / max(top[0], 1e-6)) # 4) 片段长度分布混合越复杂size 展布通常越宽 s peaks[size].to_numpy(dtypefloat) feats[size_mean] float(s.mean()) if len(s) else 0.0 feats[size_std] float(s.std(ddof0)) if len(s) else 0.0 feats[size_range] float(s.max() - s.min()) if len(s) else 0.0 # 5) 这个特征只在训练期存在线上拿不到 n_contrib if n_contrib is not None: feats[expected_allele_ratio] feats[allele_count] / (2.0 * n_contrib) return feats这段代码的关键点在最后那个if。expected_allele_ratio的计算式是「观测等位基因数 ÷ 理论等位基因数」而理论值2 × n_contrib里直接含了标签 N。训练时它能让模型准确率冲到 0.96上线时这个输入根本不存在模型性能会大幅下降。论文的特征重要性图里它权重超过 0.5排在第一恰恰说明模型学到的主要是这个泄漏信号而非真实的图谱规律。参数上还有两处要留意。top1_top2_ratio在次高峰为 0 时会给一个默认值 10这个哨兵值如果设得太小会和真实的高比值样本混在一起设成 10 在多数 STR 数据里够用但换数据集时最好重新看一下分布。height_cv的分母做了 1e-6 的保护防止全零峰导致 NaN这类 NaN 一旦漏进管线树模型会直接报错。3.3 问题二的比例标签把连续占比离散成类别问题二把比例估计做成分类标签得自己从数据里还原。论文给出的类别是按人数变化的网格2 人样本有 1:1、1:4、1:9 三类3 人有 6 类4 人和 5 人各 7 到 8 类。实际数据里比例可能带小数误差需要按最近邻映射回设计网格。def ratio_label(shares, grid): shares: 一串占比例如 [0.2, 0.8] grid: 该人数下所有合法比例模板形如 [[1,1],[1,4],[1,9]] 返回: 最接近的模板对应的字符串标签 x np.asarray(shares, dtypefloat) x x / x.sum() # 先归一化避免采集误差 best, best_err None, np.inf for g in grid: t np.asarray(g, dtypefloat) t t / t.sum() err np.abs(np.sort(x) - np.sort(t)).sum() # 排序后比较规避排列歧义 if err best_err: best, best_err g, err return :.join(str(v) for v in best)这里把两个向量都排序后再比是为了规避「谁排第一位」带来的歧义。混合样本里贡献者本身没有顺序1:4 和 4:1 是同一个类别如果不排序就生成标签同一类会被拆成两个类别数直接翻倍。标签生成完之后训练要按人数分组做。论文分别给 2 人、3 人、4/5 人挑了三套模型理由是不同人数下的特征分布差异太大混在一起训一个多类分类器会互相干扰。这个做法在小样本场景下确实有效但每个分组只有一百到两百条样本交叉验证的方差会很大报告指标时最好把折间标准差一起写出来。4. 问题三与问题四的实现兼容性搜索、最小二乘配比与小波去噪4.1 兼容性评分先把候选组合砍到能枚举的规模问题三的输入是候选个体基因型库输出是「哪几个人组成了这个混合样本」。如果库里有一百人、要找 3 人组合枚举量是十六万量级逐个算峰高拟合不现实。论文的做法是先算兼容性把明显不可能的个体剔掉再在小集合上枚举。兼容性评分的定义很直接某个候选人的基因型在该基因座上能否覆盖观测到的等位基因。完全覆盖记满分部分覆盖扣分完全对不上直接出局。把所有基因座的得分汇总取前若干名进入组合搜索。def compatibility(cand_geno: dict, observed: dict) - float: cand_geno: {基因座: (等位基因a, 等位基因b)}纯合则 a b observed: {基因座: set(观测到的等位基因)} 返回: 0~1 之间的兼容度按基因座取平均 scores [] for loc, obs in observed.items(): g cand_geno.get(loc) if g is None: scores.append(0.0) continue allow set(g) # 该候选人可能贡献的等位基因集合 hit len(allow obs) scores.append(hit / max(len(allow), 1)) return float(np.mean(scores)) if scores else 0.0这段代码的判分逻辑是「分母用候选人自己的基因型规模」不是用观测集合的规模。这一点容易被写反如果用观测集合做分母等位基因多的混合样本会把所有候选人的分数都压低排序结果被稀释用候选人自己的基因型做分母纯合子两个相同等位基因和杂合子就能在同一个尺度上比较。阈值怎么定是个经验值。我一般会按分数排序取前 15 到 20 个候选进入枚举3 人组合大约一千组算下来可接受。如果库里人数更多可以改成贪心先定兼容度最高的一个再在剩下的里面找与之互补度最高的牺牲一点最优性换速度。4.2 用非负最小二乘解混合比例候选组合确定之后剩下的问题是「这组人各自占多少」。论文的假设是峰高与 DNA 含量近似线性相关于是可以写成线性方程组观测峰高 ≈ 各贡献者在该位点的等位基因拷贝数 × 各自比例 × 一个缩放系数。用最小二乘解比例再归一化。from scipy.optimize import nnls import numpy as np def design_matrix(alleles, genotypes): alleles: 该基因座下观测到的等位基因列表 genotypes: 候选人的基因型列表每个形如 (a, b) 返回: 矩阵 G形状 (len(alleles), n_candidates) G[i][k] 第 k 个候选人在 alleles[i] 上的拷贝数0/1/2 G np.zeros((len(alleles), len(genotypes))) idx {a: i for i, a in enumerate(alleles)} for k, (a, b) in enumerate(genotypes): if a in idx: G[idx[a], k] 1 if b in idx: G[idx[b], k] 1 return G def fit_shares(heights, alleles, genotypes): heights 与 alleles 一一对应返回归一化后的比例向量 G design_matrix(alleles, genotypes) w, _ nnls(G, np.asarray(heights, dtypefloat)) # 非负约束杜绝负比例 return w / w.sum() if w.sum() 0 else w之所以用nnls而不用普通的lstsq是因为普通最小二乘在噪声存在时会解出负的比例值物理上无意义还得手工截断截断后又破坏了残差最小的性质。非负最小二乘直接把这个约束加进去代价是求解慢一点但在这个规模下可以忽略。解出比例后论文还设计了一个综合匹配评分把「等位基因解释率」和「峰高分布相关性」两件事合到一起解释率衡量这个组合能不能覆盖所有观测峰相关性衡量拟合出来的峰高曲线和实际曲线像不像。前者防止漏掉贡献者后者防止比例解偏。两者加权求和排序取最高分作为最终答案。注意解释率高但相关性低的组合很常见典型情况是把一个次要贡献者的等位基因也算进去了覆盖率上去了峰高却对不上。加权时相关性那一项别设太小我的经验是两项接近等权比较稳。4.3 小波阈值去噪与拖尾峰过滤问题四处理的是输入质量。混合图谱里的噪声有两类一类是高频随机噪声体现在峰高的小幅抖动上另一类是 PCR 扩增产生的拖尾峰stutter特征是紧挨着主峰、片段长度差 2 到 4 bp、高度明显偏低。小波阈值去噪负责第一类。做法是把信号做多层小波分解对高频细节系数做软阈值收缩再重构回去。阈值取 σ√(2lnN) 这个经典形式σ 用第一层细节系数的中位数绝对偏差估计。import pywt import numpy as np def wavelet_denoise(y, waveletdb4, level3): y: 一条峰高序列按片段长度排序 wavelet: 小波基db4 在保留尖峰形状上比 haar 好 level: 分解层数信号短于 2**level 时要往下调 coeffs pywt.wavedec(y, wavelet, levellevel) sigma np.median(np.abs(coeffs[-1])) / 0.6745 # 用最细层估计噪声水平 thr sigma * np.sqrt(2 * np.log(len(y))) # 第 0 个是近似系数不动只收缩细节层 coeffs[1:] [pywt.threshold(c, thr, modesoft) for c in coeffs[1:]] recon pywt.waverec(coeffs, wavelet) return recon[: len(y)] # 重构后长度可能多出 1~2 点三个参数里最容易出问题的是level。分解层数过深低频近似系数被过度压缩主峰高度会被削平后面的比例估计全跟着偏层数过浅则噪声去不干净。论文的经验是 3 层这个值跟信号长度有关序列点数是 2 的幂次以下时层数别超过 log2(长度)-1。拖尾峰的过滤走规则而不是滤波。论文给的判据是峰高比值加片段长度差与主峰的片段长度差在 2 到 4 bp 之内且峰高比低于某个阈值常见取 0.15 上下就判为 stutter 剔掉。这个规则简单但有效前提是同一基因座内的峰已经按片段长度排好序。最后一步是自适应峰值筛选对候选峰做质量评分——峰高归一化后加上与相邻峰之间的间距低于阈值的峰不进后续匹配。三段处理串起来之后论文报告在多数样本上贡献者识别准确率能到 0.8 至 1.0极端样本上的表现也优于只做预处理或传统去噪的方案。5. 避坑与排查这套代码跑起来最容易出问题的五个地方5.1 训练阶段的两个隐性问题现象一离线准确率 0.96换个数据集直接掉到 0.6 以下。原因是expected_allele_ratio这个特征在训练期用到了真实人数。它本质上是把标签做了个除法再喂回模型树模型只要在这个维度上切一刀就能分类。排查方法很简单把所有含n_contrib的特征列删掉重训一遍如果准确率大幅下降说明模型之前主要靠泄漏特征。解决方式是在locus_features里强制n_contribNone生成线上特征训练集也只保留这一版泄漏特征单独拿去做错误分析不进模型。现象二MLP 的训练准确率 0.75交叉验证只有 0.36。这是论文里真实出现的数字典型的小样本过拟合。MLP 参数量相对千级样本太多默认配置下几轮就把训练集背下来了。排查时看训练损失和验证损失的曲线是否分叉分叉就是过拟合。解决方式是砍隐藏层宽度、加 dropout、加 L2或者干脆换成树模型——在特征维度只有二十上下、样本量不大的情况下GBDT 的性价比确实更高。现象三混淆矩阵里 4 人样本大量被判成 3 人。这是等位基因重叠导致的不是代码 bug。人数越多重复等位基因越多观测到的allele_count增长会饱和模型分不清 3 人和 4 人。排查方式是画出不同人数下allele_count的分布箱线图如果 3 人和 4 人的箱子大面积重叠就说明这个特征本身已经到上限了。解决方向是引入峰高比例的细粒度特征各峰占该位点总峰高的比例或者引入相邻基因座的联合特征让模型有更多区分依据。5.2 数据与工程层面的三个坑现象四某几个基因座的特征全是 NaN训练直接报错。常见原因是该基因座在所有样本里都没检出峰height_mean除零后变成 NaNheight_cv再一除就变成 inf。排查时对特征矩阵跑一遍df.isna().sum()和np.isinf检查。解决方式是在locus_features里对空峰表直接返回全零字典并在管线最前面加一层SimpleImputer(strategyconstant, fill_value0)保底。现象五小波去噪后主峰高度普遍变矮比例估计整体偏移。原因是分解层数超过了信号实际能支撑的长度或者软阈值把主峰的细节系数一起收缩了。排查方式是画出去噪前后的对照曲线重点看最高的那几个峰有没有被削。解决方式是把level降到 2 或者换modegarrote同时限定只对最细的两层做阈值处理近似系数层完全不动。提示去噪前后一定要存图。这个环节的失败是静默的——程序不报错指标略微变差你不看曲线根本发现不了。除了上面五条还有一个容易忽视的点问题三的候选个体池里如果混进了亲缘关系较近的个体兼容性评分会集体偏高排序失效。这属于模型假设被打破的情况论文里明确假设了贡献者之间基因型独立、不存在亲缘关系超出这个假设的数据不该硬套。6. 二次开发把 0.96 的准确率拆开重装一遍拿到这套代码之后我做的第一件事是把特征表按「是否用到标签」分成两栏逐列过一遍。这一步不改模型只改输入但决定了后面所有指标是否可信。特征列是否保留理由allele_count保留纯观测值预测时可得non_ol_count保留纯观测值height_*系列保留全部来自峰表size_*系列保留全部来自峰表expected_allele_ratio删除分母含真实人数属标签泄漏基因座 one-hot保留无泄漏但要注意维度膨胀删掉泄漏特征后重训准确率肯定会下降这个过程不必追求高指标。指标下降说明模型之前确实靠了那条捷径现在学到的东西才对应真实的图谱规律。第二个改造方向是把问题二的离散分类换成回归加约束。原来按人数分组、每组挑一个分类器本质上是用分组绕开了「比例」这个连续量的建模难度。改成回归之后输出一个 N 维的占比向量约束非负、和为 1样本利用率比分组分类高得多也能处理网格外的比例。代价是需要重新设计评估指标MAE 比 accuracy 更能反映实际误差。第三个方向是验证流程。原有方案用的是普通交叉验证同一个样本的不同基因座可能同时出现在训练集和验证集里导致指标虚高。改成按样本分组、按基因座分层即用GroupKFold保证同一样本不跨折评估结果才贴近真实场景。from sklearn.model_selection import GroupKFold, cross_val_score from sklearn.ensemble import GradientBoostingClassifier # groups 是样本 ID同一样本的所有基因座行共享同一个 group cv GroupKFold(n_splits5) scores cross_val_score( GradientBoostingClassifier(n_estimators200, max_depth3, random_state0), X_clean, y, groupsgroups, cvcv, scoringf1_weighted ) print(scores.mean(), scores.std()) # 这个数字才是能写进报告的从那以后我每次拿到一份带「准确率 0.96」的分析文档都强制先做一遍泄漏排查把所有特征列按「预测时能不能拿到」过一遍删掉拿不到的再重跑指标。这一步通常只花二十分钟却能避免后面两周基于不真实指标做出来的错误设计。这套 D 题论文的价值不在于那个 0.96而在于它把四个问题的完整链路、特征设计、去噪思路和失败案例都留了下来改起来有据可依。希望帮到你。题外一句论文正文里还有几处 OCR 断字公式部分对不上的时候直接对着代码里的实现倒推比反复读文字快。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑