最近在复盘机器学习项目时,我重新实现了一个基于PyTorch的新冠病例预测模型。这个项目最初是台大李宏毅老师机器学习课程的作业,核心目标是通过93个地区特征预测第三天的新冠阳性人数。作为典型的回归问题,这个案例涵盖了数据预处理、特征选择、模型构建、训练优化等完整流程,非常适合用来巩固机器学习基础知识。
在本文中,我将详细拆解整个项目的实现过程,特别会重点说明几个关键设计决策背后的思考逻辑。不同于简单的代码罗列,我会结合自己实际训练过程中踩过的坑,分享一些在官方文档和教程中很少提及的实战经验。无论你是刚开始接触PyTorch的新手,还是想了解工业级模型开发流程的进阶学习者,相信都能从中获得启发。
原始数据包含两个CSV文件:
covid.train.csv:训练集,包含2700条样本,每行有93个特征列和1个标签列(第三天阳性人数)covid.test.csv:测试集,893条样本,仅包含特征列使用Python标准库的csv模块读取数据时,需要特别注意两点:
python复制with open(path,'r') as f:
csv_data = list(csv.reader(f))
# 跳过首行(特征名)和首列(ID)
x = np.array(csv_data)[1:,1:-1] # 特征
y = np.array(csv_data)[1:,-1] # 标签
我采用了简单的5折分割策略:
这种确定性的分割方式相比随机分割有两个优势:
python复制if mode == 'train':
indices = [i for i in range(len(csv_data)) if i % 5 != 0]
elif mode == 'val':
indices = [i for i in range(len(csv_data)) if i % 5 == 0]
93个特征并非全部有用,我使用SelectKBest结合卡方检验(chi2)选择与目标最相关的k个特征。这种方法计算简单且解释性强,适合作为基线方案。
实际项目中还可以尝试:
python复制from sklearn.feature_selection import SelectKBest, chi2
def get_feature_importance(feature_data, label_data, k=4):
selector = SelectKBest(chi2, k=k)
X_new = selector.fit_transform(feature_data, label_data)
return X_new, selector.scores_
不同特征的量纲差异会导致模型训练困难,我采用了列级别的Z-score标准化:
python复制self.data = (self.data - self.data.mean(dim=0, keepdim=True)) / self.data.std(dim=0, keepdim=True)
注意:标准化参数(均值、标准差)应该只从训练集计算,然后应用到验证集和测试集,避免数据泄漏。
我构建了一个简单的双层全连接网络:
python复制class myNet(nn.Module):
def __init__(self, inDim):
super(myNet, self).__init__()
self.fc1 = nn.Linear(inDim, 64)
self.relu = nn.ReLU()
self.fc2 = nn.Linear(64, 1)
在forward过程中,我特别处理了输出维度问题:
python复制def forward(self, x):
x = self.fc2(self.relu(self.fc1(x)))
return x.squeeze(1) if len(x.size()) > 1 else x
这是因为:
模型参数初始化对训练效果影响很大。PyTorch的Linear层默认使用Kaiming均匀初始化,适合ReLU激活函数。如果需要自定义初始化,可以在__init__中添加:
python复制# He初始化
nn.init.kaiming_normal_(self.fc1.weight, mode='fan_out', nonlinearity='relu')
nn.init.zeros_(self.fc1.bias)
设备转移是PyTorch的常见操作,我通常这样处理:
python复制device = 'cuda' if torch.cuda.is_available() else 'cpu'
model = model.to(device)
使用MSE损失(均方误差)作为基础损失,并加入L2正则化防止过拟合:
python复制def mseLoss(pred, target, model):
base_loss = nn.MSELoss(reduction='mean')(pred, target)
l2_reg = torch.sum(torch.stack([(param**2).sum() for param in model.parameters()]))
return base_loss + 0.00075 * l2_reg
正则化系数0.00075需要根据具体任务调整:
选择SGD优化器并启用动量(momentum):
python复制optimizer = optim.SGD(model.parameters(),
lr=0.001,
momentum=0.9)
动量项能加速收敛并帮助跳出局部最优。对于这种小规模网络,SGD通常比Adam表现更好。
学习率设置经验:
完整的训练循环包含几个关键部分:
python复制for epoch in range(n_epochs):
# 训练阶段
model.train()
for x, y in trainloader:
optimizer.zero_grad()
pred = model(x.to(device))
loss = loss_fn(pred, y.to(device), model)
loss.backward()
optimizer.step()
# 验证阶段
model.eval()
with torch.no_grad():
for x, y in valloader:
val_pred = model(x.to(device))
val_loss = loss_fn(val_pred, y.to(device), model)
实现简单的早停机制,当验证损失连续不下降时停止训练:
python复制if val_loss < best_loss:
best_loss = val_loss
torch.save(model.state_dict(), 'best_model.pth')
patience = 0
else:
patience += 1
if patience >= early_stop:
break
典型的训练过程损失曲线应该呈现:

除了MSE,回归任务还应关注:
python复制from sklearn.metrics import mean_absolute_error, r2_score
mae = mean_absolute_error(y_true, y_pred)
r2 = r2_score(y_true, y_pred)
常见问题及解决方案:
损失不下降:
过拟合严重:
预测值偏离:
建议的工程结构:
code复制covid_prediction/
├── data/
│ ├── train.csv
│ └── test.csv
├── models/
│ ├── base_model.py
│ └── utils.py
├── configs.py
├── train.py
└── evaluate.py
使用配置文件管理超参数:
python复制# configs.py
config = {
'lr': 1e-3,
'batch_size': 256,
'feature_dim': 6,
'hidden_dim': 64,
'n_epochs': 50,
'early_stop': 20
}
推荐使用:
python复制with open('log.csv', 'a') as f:
f.write(f"{epoch},{train_loss},{val_loss}\n")
这个基础项目可以进一步扩展:
在实现这个项目的过程中,我发现数据质量往往比模型复杂度更重要。花时间做好数据探索和预处理,通常能获得比换模型更大的提升。另外,在资源允许的情况下,建议始终进行交叉验证而不是简单的单次划分,这样得到的性能评估会更可靠。