
用 PyTorch 做 COVID-19 阳性率预测从特征选择到模型调参的完整实践本文记录一次深度学习回归任务的完整实践使用 PyTorch 构建神经网络根据美国各州 COVID-19 相关特征预测未来阳性检测比例。相比单纯贴代码我更想把这次实验中的建模思路、调参过程和踩坑经验整理出来。1. 任务背景这次任务来自COVID-19 Cases Prediction回归问题。数据集中每条样本包含美国某州在不同时间窗口下的 COVID-19 相关指标例如人群流动情况类流感症状比例口罩佩戴情况心理和经济担忧指标过去时间窗口的阳性检测比例等。模型目标是预测tested_positive.2也就是后一个时间窗口中的阳性检测比例。本质上这是一个典型的表格数据回归任务。最终评价指标主要关注MAEMean Absolute Error平均绝对误差。2. 实验环境实验使用 PyTorch 完成主要环境如下PyTorch version: 2.11.0cu128 Torchvision version: 0.26.0cu128 CUDA available: True GPU: Tesla T4为了保证实验可复现我固定了随机种子并关闭了 cuDNN 的 benchmarkmyseed42069torch.backends.cudnn.deterministicTruetorch.backends.cudnn.benchmarkFalsenp.random.seed(myseed)torch.manual_seed(myseed)iftorch.cuda.is_available():torch.cuda.manual_seed_all(myseed)这里有一个容易忽略的点小数据集上的实验结果对随机划分和初始化非常敏感。如果不固定随机种子可能每次训练出来的验证集结果都有明显波动不利于判断调参是否真的有效。3. 数据概览与质量检查训练集规模如下训练集 shape: (2700, 95)其中1 列id93 个输入特征1 个目标值tested_positive.2。我首先做了基础的数据质量检查检查项结果缺失值0重复行0数据类型主要为 float64内存占用约 2.0 MB整体来看这份数据比较干净不需要复杂的数据清洗。4. 相关性分析为什么需要特征选择虽然数据质量不错但一个明显问题是很多特征高度共线。例如同一指标在不同时间窗口下的值非常接近特征 1特征 2相关系数hh_cmnty_clihh_cmnty_cli.10.9984hh_cmnty_cli.1hh_cmnty_cli.20.9983wearing_mask.1wearing_mask.20.9983wearing_maskwearing_mask.10.9983nohh_cmnty_clinohh_cmnty_cli.10.9983这说明不同时间窗口之间的信息有大量重复。如果直接把全部 93 个特征都喂给模型会带来两个问题输入维度变大模型更容易过拟合高度相关的特征会让模型学到重复信息影响泛化。因此我最终使用f_regression从 93 个特征中筛选出最相关的 20 个特征。核心代码如下fromsklearn.feature_selectionimportf_regression Xdata_for_fs.iloc[:,1:-1]# 93 个特征列ydata_for_fs.iloc[:,-1]# tested_positive.2f_scores,p_valuesf_regression(X,y)top20_indicesnp.argsort(f_scores)[-20:]selected_featssorted(top20_indices.tolist())最终选择的特征索引为selected_feats[40,41,42,43,51,56,57,58,59,60,61,69,74,75,76,77,78,79,87,92]F 值最高的特征主要集中在历史tested_positivecli/ili类流感症状指标hh_cmnty_cli、nohh_cmnty_cli等社区症状指标部分经济担忧和公共交通相关指标。这也比较符合直觉过去的阳性率和症状相关指标对未来阳性率最有预测价值。5. 数据集划分与归一化我将训练数据划分为三部分数据集比例样本数Train80%2160Dev10%270Dev Test10%270这里额外保留一个dev_test主要是为了在调参之后模拟一个相对独立的测试集避免只盯着 dev 集反复调参。5.1 自定义 Dataset核心数据集类如下classCOVID19Dataset(Dataset):_split_cache{}def__init__(self,path,modetrain,target_onlyFalse,fit_normFalse,norm_statsNone):self.modemode self.pathpathwithopen(path,r)asfp:datalist(csv.reader(fp))datanp.array(data[1:])self.idsdata[:,0].astype(int)datadata[:,1:].astype(float)featsselected_featsiftarget_onlyelselist(range(93))ifmodetest:self.datatorch.FloatTensor(data[:,feats])self.targetNoneelse:ifpathnotinCOVID19Dataset._split_cache:nlen(data)indicesnp.random.permutation(n)train_endint(n*0.8)dev_endint(n*0.9)COVID19Dataset._split_cache[path]{train:indices[:train_end],dev:indices[train_end:dev_end],dev_test:indices[dev_end:],}indicesCOVID19Dataset._split_cache[path][mode]self.datatorch.FloatTensor(data[indices][:,feats])self.targettorch.FloatTensor(data[indices,-1])iffit_norm:self.meanself.data.mean(dim0,keepdimTrue)self.stdself.data.std(dim0,keepdimTrue)elifnorm_statsisnotNone:self.mean,self.stdnorm_statselse:raiseValueError(非训练集模式下必须提供 norm_stats)self.data(self.data-self.mean)/(self.std1e-8)self.dimself.data.shape[1]5.2 一个关键细节归一化不能泄漏信息归一化时我只在训练集上计算mean/stdtr_datasetCOVID19Dataset(tr_path,train,target_onlyTrue,fit_normTrue)train_stats(tr_dataset.mean,tr_dataset.std)然后 dev、dev_test、test 都复用训练集统计量dv_datasetCOVID19Dataset(tr_path,dev,target_onlyTrue,norm_statstrain_stats)dvt_datasetCOVID19Dataset(tr_path,dev_test,target_onlyTrue,norm_statstrain_stats)tt_datasetCOVID19Dataset(tt_path,test,target_onlyTrue,norm_statstrain_stats)这是非常重要的一步。如果在 dev 或 test 上重新计算均值和标准差就等于提前看到了验证集/测试集的整体分布会造成data leakage最终评估结果不可信。6. 模型设计简单网络反而更有效一开始我尝试过更深的全连接网络但效果并不好。最终保留下来的是两个非常轻量的单隐藏层网络。6.1 DNN_basicnn.Sequential(OrderedDict([(linear1,nn.Linear(20,64)),(bn1,nn.BatchNorm1d(64)),(relu1,nn.ReLU()),(dropout1,nn.Dropout(p0.2)),(out,nn.Linear(64,1))]))参数量1,537。6.2 DNN_threenn.Sequential(OrderedDict([(linear1,nn.Linear(20,32)),(bn1,nn.BatchNorm1d(32)),(relu1,nn.ReLU()),(dropout1,nn.Dropout(p0.2)),(out,nn.Linear(32,1))]))参数量769。两种网络都使用BatchNorm稳定训练过程ReLU引入非线性Dropout缓解过拟合单输出节点完成回归预测。7. 训练策略主要超参数如下参数取值learning rate0.01batch size270epochs3000optimizerAdamWweight decay0.03patience1000min_delta0.001损失函数使用 MSElossF.mse_loss(preds.squeeze(),labels)同时记录 MAE 作为更直观的误差指标maeF.l1_loss(preds.squeeze(),labels)7.1 Early Stopping训练过程中我保存验证集 loss 最低的模型并使用 early stopping 防止无意义训练。判断逻辑是如果 current_val_loss best_val_loss - min_delta 保存当前模型 early_stop_counter 清零 否则 early_stop_counter 1 如果 early_stop_counter patience 停止训练这能避免模型在验证集已经不再提升时继续训练。8. 实验结果最终两个模型的表现如下Run模型Best EpochBest Val LossTest LossTest MAE1DNN_basic5140.86461.07670.74322DNN_three2410.86551.06770.7383最终表现最好的是DNN_three: Test MAE 0.7383虽然DNN_three的隐藏层只有 32 个神经元参数量也更少但它反而取得了更好的测试表现。这说明在这个任务中模型不是越大越好。对于只有 2700 条样本的表格数据任务过大的神经网络很容易过拟合适当缩小模型反而更容易泛化。9. 训练曲线观察从训练过程来看整体趋势比较清晰前 50 个 epoch 内train loss 和 val loss 快速下降中后期 train loss 继续缓慢下降但 val loss 逐渐进入平台期最佳 epoch 附近val loss 和 train loss 的差距不大只有轻微过拟合每个 epoch 训练耗时约 0.020.03 秒。如果放图的话可以重点展示两张图Train Loss vs Val LossTrain MAE vs Val MAE。在技术文章里曲线图比大段日志更有说服力。10. 预测结果分析在 dev_test 上观察预测值和真实值的关系可以得到几个结论散点整体沿着理想预测线分布说明模型没有明显系统偏差残差分布接近正态均值接近 0低值区域略微预测偏高高值区域略微预测偏低这是一种典型的“回归到均值”现象。也就是说模型对大多数样本预测比较稳定但对极端值的拟合能力仍然有限。11. 踩坑与反思11.1 网络不是越深越好我最开始设计的是更深的三层全连接网络直觉上觉得层数越多表达能力越强结果应该越好。但实际情况是模型训练不稳定loss 降不下来泛化效果也不好。后来我逐步简化网络结构从三层砍到两层再到最终只保留一个隐藏层。结果发现32 个隐藏单元的简单网络反而最好。这让我意识到对小规模表格数据来说模型容量过大不一定是优势反而可能成为负担。11.2 Epoch 太少会误判模型能力一开始我只训练 80 个 epochloss 居高不下于是怀疑模型结构或者代码有问题。后来把 epoch 增加到 3000并配合 early stopping才发现模型其实可以继续收敛。原因也很简单训练样本数 2160 batch_size 270 每个 epoch 只有 8 次参数更新 80 个 epoch 只有 640 次更新对于神经网络来说640 次参数更新确实太少了。所以训练轮数不能只看 epoch 数字还要结合 batch size 和每轮更新步数一起判断。11.3 Z-score 归一化非常关键如果不做归一化不同特征尺度差异很大会导致优化过程非常困难。更重要的是dev/test 必须使用训练集的 mean/std不能单独重新计算。这个细节如果处理错验证结果会被污染。11.4 经验参数不一定适用于当前任务最终比较好的组合是lr 0.01 weight_decay 0.03 batch_size 270其中weight_decay0.03比很多教程中常见的 0.001 或 0.01 更大batch_size270也偏大。这说明调参不能完全依赖经验值。经验只能给一个起点最终还是要通过实验结果判断。12. 后续改进方向这次实验还有不少可以继续优化的地方尝试更多特征数量当前只使用 top-20 特征可以尝试 top-40 或全部 93 个特征并配合更强正则化。增加特征交叉例如cli × wearing_mask、tested_positive × public_transit等捕捉非线性交互。使用学习率调度器可以尝试CosineAnnealingLR或ReduceLROnPlateau在 loss 平台期自动降低学习率。模型集成对多个模型预测结果取平均有可能进一步降低方差。更系统的超参数搜索可以使用 Optuna、Ray Tune 等工具进行自动化调参。13. 总结这次实验完整走了一遍表格回归任务的流程数据检查 → 相关性分析 → 特征选择 → 数据归一化 → 模型设计 → 训练调参 → 结果分析最终参数更少的DNN_three取得了最好的表现Test MAE 0.7383这次最大的收获不是这个 MAE 数字本身而是过程中踩过的坑三层网络不一定比一层网络好epoch 太少会误判模型效果归一化必须避免数据泄漏小数据集上过大的模型很容易过拟合调参要相信实验结果而不是只相信经验。对我来说这次作业不仅是一次 PyTorch 练习更像是一次完整的小型机器学习项目实践。附完整代码说明githubhttps://github.com/fiveyearoldlookingup/DLHW-Assignments/tree/main/HW1完整代码保存在 Jupyter Notebook 中包含数据下载EDA特征选择Dataset 定义NetworkFactoryRunManager模型训练TensorBoard 记录Kaggle submission 生成。