简介:本资源为2021年华为杯数学建模竞赛D题「抗乳腺癌候选药物的优化建模」的完整解答包,面向参加数学建模竞赛的高校学生及对生物信息学、医疗数据分析感兴趣的进阶学习者。内容围绕特征选择、回归预测、二分类建模、最优化求解、模型训练与验证、数据预处理、结果可视化等核心环节展开,可帮助读者理解如何将机器学习方法落地到药物研发场景。压缩包共83个文件,以32个Python脚本、22个CSV数据文件、14张PNG图表为主,另含XLSX、XML、Markdown与DOCX等辅助材料,整体约31.82MB,目录按代码、数据与文档分层组织,便于按模块查阅。目前已有3477人学习下载,适合需要复盘赛题思路、参考建模流程与代码实现、查漏补缺的参赛者与研究者。
1. 华为杯D题抗乳腺癌候选药物优化建模:这道题到底在考什么
2021年华为杯研究生数学建模竞赛D题,题目全称是「抗乳腺癌候选药物的优化建模」,属于典型的「数据驱动 + 多目标优化」复合型赛题。它给出的是一批化合物分子描述符数据,要求参赛队围绕生物活性、ADMET性质(吸收、分布、代谢、排泄、毒性)建立预测模型,再在此基础上做分子结构的优化筛选。很多队伍拿到题的第一反应是直接上机器学习调包,结果发现数据维度高、样本量小、指标之间还互相打架,最后论文写得像实验报告,模型精度也上不去。
这道题真正考的不是某个算法会不会用,而是你能不能把「预测」和「优化」串成一条闭环:先用回归模型把pIC50等活性指标预测准,再用分类模型判断化合物是否满足ADMET约束,最后在满足约束的前提下搜索活性更高的分子描述符组合。适合已经做过一两次数学建模、想冲击华为杯优秀论文的研究生,也适合想系统练一遍「特征工程 + 多目标优化」落地流程的工程师。下面我按自己复现这道题时的实际路径,把选型理由、参数设置和踩过的坑讲清楚。
2. 数据预处理与特征筛选:从729个描述符里挑出能用的
2.1 先搞清楚数据长什么样再动手
这道题官方给的数据通常包含训练集和测试集两部分,训练集里每个化合物有分子描述符(MD)、pIC50值、以及若干ADMET标签。分子描述符数量在729个左右,涵盖拓扑指数、分子指纹、物化性质等。样本量通常只有几百到一千出头,属于典型的「宽而浅」数据。直接把这729列全丢进模型,结果一定是过拟合,交叉验证分数好看但测试集崩掉。
我一般会先做三件事:检查缺失值分布、看每列方差、算一下描述符与pIC50的皮尔逊相关系数。方差接近0的列直接删,因为对区分样本没贡献;相关系数绝对值低于某个阈值的列也先放一边,但不要急着永久删除,后面做特征重要性时还要回头看。
import pandas as pd import numpy as np from sklearn.feature_selection import VarianceThreshold # 读取训练数据,假设描述符列从第3列开始 train = pd.read_csv('train.csv') desc_cols = train.columns[2:] # 根据实际列名调整 # 1. 删除方差极低的描述符 selector = VarianceThreshold(threshold=0.01) X_var = selector.fit_transform(train[desc_cols]) kept_cols = desc_cols[selector.get_support()] print(f'方差筛选后剩余特征数: {len(kept_cols)}') # 2. 计算与pIC50的相关系数 corr = train[kept_cols].corrwith(train['pIC50']).abs() corr_selected = corr[corr > 0.1].index.tolist() print(f'相关性筛选后剩余特征数: {len(corr_selected)}')这段代码的逻辑是先用方差阈值砍掉近似常数列,再用相关系数做粗筛。threshold=0.01这个值不是固定的,如果数据做过标准化,可以适当调高到0.05;如果没标准化,方差本身量纲差异大,建议先做StandardScaler再筛。相关系数阈值0.1也是经验值,目的是保留弱相关但可能非线性有用的特征,真正精细的筛选交给后面的模型。
2.2 用随机森林做嵌入法特征重要性排序
粗筛之后特征数可能还有两三百个,这时候用嵌入法让模型自己挑。随机森林的特征重要性排序稳定、对共线性不敏感,适合这种高维小样本场景。我一般会跑一次RandomForestRegressor,取importance累计贡献达到95%的前N个特征作为最终输入。
from sklearn.ensemble import RandomForestRegressor rf = RandomForestRegressor(n_estimators=500, max_depth=8, random_state=42, n_jobs=-1) rf.fit(train[corr_selected], train['pIC50']) imp = pd.Series(rf.feature_importances_, index=corr_selected).sort_values(ascending=False) cum_imp = imp.cumsum() / imp.sum() final_features = cum_imp[cum_imp <= 0.95].index.tolist() print(f'最终入模特征数: {len(final_features)}')n_estimators=500是为了让重要性估计更稳定,max_depth=8是防止单棵树过深导致重要性被噪声主导。累计贡献阈值0.95意味着保留能解释95%重要性的特征,剩下的5%大概率是噪声。这一步做完,特征数通常能压到50以内,后面建模会轻很多。
注意:特征筛选必须在交叉验证的每一折内部独立做,不能在全量数据上筛完再交叉验证,否则会信息泄漏,交叉验证分数虚高。我见过不少论文在这里翻车。
3. 活性预测模型:pIC50回归怎么选、怎么调
3.1 为什么我首选梯度提升树而不是深度学习
这道题的样本量决定了深度学习不是好选择。几百个样本、几十个特征,神经网络参数量随便就超过样本量,训练集loss降得再低也是背答案。梯度提升树(XGBoost、LightGBM、CatBoost)在这个量级上表现稳定,对特征缩放不敏感,还能直接输出特征重要性做解释。我一般先用LightGBM跑基线,再用XGBoost对比,最后用贝叶斯优化调参。
import lightgbm as lgb from sklearn.model_selection import KFold, cross_val_score model = lgb.LGBMRegressor( n_estimators=800, learning_rate=0.03, num_leaves=15, max_depth=6, min_child_samples=10, subsample=0.8, colsample_bytree=0.7, reg_alpha=0.1, reg_lambda=0.1, random_state=42 ) kf = KFold(n_splits=5, shuffle=True, random_state=42) scores = cross_val_score(model, train[final_features], train['pIC50'], cv=kf, scoring='neg_mean_squared_error') rmse = np.sqrt(-scores.mean()) print(f'5折交叉验证RMSE: {rmse:.4f}')num_leaves=15和max_depth=6是控制模型复杂度的关键,小样本下叶子数太多必然过拟合。learning_rate=0.03配合n_estimators=800是慢学习率多棵树的经典组合,比0.1配200棵更稳。min_child_samples=10保证每个叶子至少有10个样本,防止模型记住个别离群点。这些参数不是拍脑袋,是我在类似规模数据集上反复试出来的区间,你可以在这个基础上用Optuna做精细搜索。
3.2 评估指标不能只看RMSE
回归任务里RMSE是基础,但这道题最终要服务于「筛选高活性化合物」,所以排序能力比绝对误差更重要。我建议同时看Spearman相关系数和Top-K命中率:把预测pIC50最高的前10%化合物挑出来,看真实pIC50也排在前10%的比例有多少。这个指标直接对应后续优化环节的实用性。
from scipy.stats import spearmanr # 用交叉验证的预测结果计算排序指标 from sklearn.model_selection import cross_val_predict pred = cross_val_predict(model, train[final_features], train['pIC50'], cv=kf) spearman = spearmanr(pred, train['pIC50']).correlation print(f'Spearman相关系数: {spearman:.4f}') # Top-K命中率 k = int(len(pred) * 0.1) top_pred_idx = np.argsort(pred)[-k:] top_true_idx = np.argsort(train['pIC50'].values)[-k:] hit_rate = len(set(top_pred_idx) & set(top_true_idx)) / k print(f'Top-10%命中率: {hit_rate:.4f}')Spearman能到0.7以上、Top-10%命中率能到0.5以上,这个模型就算可用了。如果Spearman低于0.6,优先检查特征筛选是不是漏掉了关键描述符,而不是急着换模型。
4. ADMET分类与多目标约束:把「能用」和「好用」分开
4.1 ADMET标签不平衡怎么处理
ADMET性质通常是二分类标签,比如是否具有肝毒性、是否高血浆蛋白结合率。这类数据往往正负样本比例悬殊,直接训练分类器会偏向多数类。我的做法是用SMOTE做少数类过采样,但只在训练折内部做,验证折保持原始分布。分类器选LightGBM的class_weight='balanced'也能缓解,但SMOTE在小样本下更直接。
from imblearn.over_sampling import SMOTE from sklearn.model_selection import StratifiedKFold from sklearn.metrics import f1_score skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) f1_scores = [] for train_idx, val_idx in skf.split(train[final_features], train['ADMET_label']): X_tr, X_val = train[final_features].iloc[train_idx], train[final_features].iloc[val_idx] y_tr, y_val = train['ADMET_label'].iloc[train_idx], train['ADMET_label'].iloc[val_idx] smote = SMOTE(random_state=42, k_neighbors=3) X_res, y_res = smote.fit_resample(X_tr, y_tr) clf = lgb.LGBMClassifier(n_estimators=300, learning_rate=0.05, num_leaves=15, random_state=42) clf.fit(X_res, y_res) pred_val = clf.predict(X_val) f1_scores.append(f1_score(y_val, pred_val)) print(f'ADMET分类平均F1: {np.mean(f1_scores):.4f}')k_neighbors=3是因为小样本下邻居太多会生成不真实的合成样本。F1比准确率更适合评估不平衡分类,如果F1低于0.6,说明ADMET标签本身噪声大或者特征区分度不够,这时候不要硬调模型,回去检查标签定义和特征覆盖。
4.2 多目标优化:活性最大化和ADMET约束怎么同时满足
这道题的核心输出是一组推荐化合物,要求活性高且ADMET性质达标。我把它建模成带约束的单目标优化:目标函数是预测pIC50最大化,约束是各ADMET分类器预测概率超过阈值。搜索空间是分子描述符的可行域,但描述符之间不独立,不能简单随机采样。常见做法是在现有化合物库中做筛选排序,而不是从头生成分子。
# 对测试集化合物打分:活性预测 + ADMET约束过滤 test_pred_activity = model.predict(test[final_features]) admet_probs = clf.predict_proba(test[final_features])[:, 1] # 约束:ADMET达标概率 > 0.7 mask = admet_probs > 0.7 candidates = test[mask].copy() candidates['pred_pIC50'] = test_pred_activity[mask] candidates = candidates.sort_values('pred_pIC50', ascending=False) print(f'满足ADMET约束的候选化合物数: {len(candidates)}') print(candidates[['compound_id', 'pred_pIC50']].head(10))阈值0.7是平衡召回和精度的经验值,如果候选太少可以降到0.6,候选太多就提到0.8。这一步的关键是先把约束卡死,再在可行域内排序,而不是把两个目标加权求和——加权系数很难解释,评审也不买账。
5. 避坑与排查:这道题最容易翻车的五个地方
5.1 特征筛选在交叉验证外面做导致分数虚高
现象:交叉验证RMSE只有0.3,测试集RMSE飙到0.8以上。原因:在全量数据上做特征筛选,验证折的信息泄漏到了训练过程。解决:把方差筛选、相关性筛选、随机森林重要性全部封装进Pipeline,在每折训练集上fit,验证集上transform。
5.2 把ADMET标签当回归做
现象:用回归模型预测ADMET数值,结果全是0.5左右的无效输出。原因:ADMET标签是二分类,回归模型优化MSE会倾向于预测均值。解决:改用分类模型,评估指标换成F1或AUC,不要看RMSE。
5.3 优化环节直接对描述符做梯度上升
现象:生成的描述符组合在化学上不可能存在,评审一眼看出是编的。原因:描述符之间有物理约束,独立扰动会破坏分子合法性。解决:在现有化合物库内做筛选排序,或者用遗传算法在合法分子空间搜索,不要对描述符向量直接做梯度优化。
5.4 忽略描述符的量纲差异
现象:基于距离的模型(KNN、SVM)效果远差于树模型。原因:描述符量纲从0.001到10000都有,距离计算被大量纲特征主导。解决:用树模型可以跳过标准化,但如果要用SVM或KNN,必须先做StandardScaler或MinMaxScaler。
5.5 论文里只写最终模型不写选型过程
现象:评审质疑为什么用LightGBM不用随机森林,答不上来。原因:只跑了最终模型,没做基线对比。解决:至少跑三个基线(线性回归、随机森林、XGBoost),用表格列出交叉验证RMSE和Spearman,选型理由自然就有了。
6. 从复现到拿奖:一个被低估的提分技巧
这道题拿高分的关键,往往不在模型本身,而在「可解释性」和「闭环验证」这两块。我复盘过几篇华为杯优秀论文,发现它们的共同点是:不仅给出了预测模型,还把特征重要性映射回化学意义,比如哪些拓扑指数对应分子柔性、哪些指纹对应疏水性,然后解释为什么这些性质影响抗乳腺癌活性。评审看的是你有没有把数学建模和领域知识接上。
具体操作上,我习惯在最终论文里加一张「关键描述符-生物活性」对照表,用SHAP值量化每个特征对pIC50的贡献方向。SHAP比特征重要性多了一层方向信息,能直接说「这个描述符增大时活性上升还是下降」。
import shap explainer = shap.TreeExplainer(model) shap_values = explainer.shap_values(train[final_features]) # 输出每个特征的平均绝对SHAP值,按降序排列 shap_importance = pd.Series( np.abs(shap_values).mean(axis=0), index=final_features ).sort_values(ascending=False) print(shap_importance.head(15))这张表出来之后,挑前5个特征去查化学数据库,确认它们对应的分子性质,写进论文的「结果分析」部分。这一步花不了多少时间,但能让论文从「调包报告」变成「有洞察的建模工作」。
另一个提分点是做敏感性分析:把ADMET约束阈值从0.6到0.9逐档变化,看推荐化合物数量和平均预测活性的变化曲线。这条曲线能说明你的优化方案在不同严格程度下都稳定,而不是卡在一个特定阈值上才有效。我一般会跑5档,画一张双轴图,左边是候选数量、右边是平均pIC50,评审看到这种分析基本会给加分。
最后说个血泪经验:这道题的数据预处理和特征筛选至少占整个工作量的60%,建模和优化各占20%。很多队伍反过来,80%时间调模型,结果特征没选好,怎么调都上不去。我现在的习惯是先把特征筛选的Pipeline搭稳,交叉验证分数稳定了再动模型参数。希望帮到你。
本文还有配套的精品资源,点击获取