1. 项目概述
新冠疫情的预测一直是公共卫生领域的重要课题。作为一名长期从事机器学习应用开发的技术人员,我最近尝试使用PyTorch框架构建了一个简单的回归模型,用于预测新冠病例数。这个项目虽然基础,但完整涵盖了从数据预处理到模型训练、评估的整个流程,特别适合想要入门PyTorch实战的开发者参考。
在本文中,我将详细解析这个项目的实现过程,包括数据集的构建、神经网络模型的设计、训练过程的优化技巧,以及如何评估模型性能。不同于简单的代码展示,我会重点分享在实际开发中遇到的坑和解决方案,这些经验对于初学者来说尤为宝贵。
2. 数据准备与预处理
2.1 数据集结构分析
我们使用的数据集包含两个CSV文件:covid.train.csv(训练集)和covid.test.csv(测试集)。从代码中可以看出,每个样本包含93个特征和1个目标值(测试阳性数)。训练集和测试集都来自同一个数据源,我们手动将其划分为训练集(80%)和验证集(20%)。
注意:在实际项目中,确保训练集和测试集来自同一分布非常重要。如果数据收集时间或来源不同,可能需要特殊处理。
2.2 CovidDataset类的实现
CovidDataset类继承自PyTorch的Dataset类,负责数据的加载和预处理。它的核心功能包括:
- 读取CSV文件并转换为NumPy数组
- 按5:1的比例划分训练集和验证集
- 对特征进行标准化处理(减去均值,除以标准差)
python复制class CovidDataset(Dataset):
def __init__(self, file_path, mode):
with open(file_path, "r") as f:
ori_data = list(csv.reader(f))
csv_data = np.array(ori_data)[1:, 1:].astype(float)
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]
elif mode == "test":
indices = [i for i in range(len(csv_data))]
X = torch.tensor(csv_data[indices, :93])
if mode != "test":
self.Y = torch.tensor(csv_data[indices, -1])
self.X = (X - X.mean(dim=0, keepdim=True)) / X.std(dim=0, keepdim=True)
self.mode = mode
2.3 数据标准化的必要性
代码中对特征进行了标准化处理(减去均值,除以标准差),这是深度学习中常见的预处理步骤。标准化有以下几个好处:
- 加速模型收敛:所有特征处于相近的数值范围,梯度下降更稳定
- 防止某些特征主导训练过程
- 使学习率的选择更容易
在实际应用中,我们保存训练集的均值和标准差,然后用相同的参数来标准化测试集,避免数据泄露。
3. 模型设计与实现
3.1 神经网络架构
我们构建了一个简单的两层全连接神经网络:
- 输入层:93个神经元,对应93个特征
- 隐藏层:128个神经元,使用ReLU激活函数
- 输出层:1个神经元,输出预测值
python复制class myModel(nn.Module):
def __init__(self, inDim):
super(myModel, self).__init__()
self.fc1 = nn.Linear(inDim, 128)
self.relu1 = nn.ReLU()
self.fc2 = nn.Linear(128, 1)
def forward(self, x):
x = self.fc1(x)
x = self.relu1(x)
x = self.fc2(x)
if len(x.size()) > 1:
x = x.squeeze(1)
return x
3.2 激活函数的选择
隐藏层使用了ReLU激活函数,相比传统的Sigmoid或Tanh函数,ReLU有以下优势:
- 计算简单,加速训练
- 缓解梯度消失问题
- 能产生稀疏激活,有助于模型学习更有意义的特征
不过ReLU也有"神经元死亡"的问题,如果遇到这种情况,可以尝试LeakyReLU或ELU等变体。
3.3 模型初始化技巧
虽然代码中没有显式设置初始化方法,但PyTorch的Linear层默认使用Kaiming初始化(针对ReLU优化)。在实际项目中,合理的初始化对模型性能有很大影响。对于ReLU激活函数,推荐使用:
python复制nn.init.kaiming_normal_(self.fc1.weight, mode='fan_in', nonlinearity='relu')
nn.init.zeros_(self.fc1.bias)
4. 训练过程与优化
4.1 训练流程详解
train_val函数实现了完整的训练和验证流程:
- 将模型移动到指定设备(CPU或GPU)
- 每个epoch分为训练和验证两个阶段
- 训练阶段:前向传播→计算损失→反向传播→参数更新
- 验证阶段:仅前向传播,计算验证损失
- 保存验证损失最小的模型
python复制def train_val(model, train_loader, val_loader, lr, optimizer, device, epochs, save_path):
model = model.to(device)
plt_train_loss = []
plt_val_loss = []
min_val_loss = float('inf')
for epoch in range(epochs):
model.train()
start_time = time.time()
train_loss = 0.0
for x, y in train_loader:
x, y = x.to(device), y.to(device)
y_pred = model(x)
bat_loss = loss(y_pred, y, model)
bat_loss.backward()
optimizer.step()
optimizer.zero_grad()
train_loss += bat_loss.cpu().item()
plt_train_loss.append(train_loss/len(train_loader))
model.eval()
val_loss = 0.0
with torch.no_grad():
for val_x, val_y in val_loader:
val_x, val_y = val_x.to(device), val_y.to(device)
val_pred_y = model(val_x)
val_bat_loss = loss(val_pred_y, val_y, model)
val_loss += val_bat_loss.cpu().item()
plt_val_loss.append(val_loss/len(val_loader))
if val_loss < min_val_loss:
min_val_loss = val_loss
torch.save(model, save_path)
print(f"[{epoch:03d}/{epochs:03d}] {(time.time()-start_time):.2f} sec(s) train_loss: {plt_train_loss[-1]:.6f} val_loss:{plt_val_loss[-1]:.6f}")
plt.plot(plt_train_loss)
plt.plot(plt_val_loss)
plt.title("loss")
plt.legend(["train", "val"])
plt.show()
4.2 损失函数设计
我们使用了带L2正则化的均方误差损失函数,这有助于防止模型过拟合:
python复制def mseLoss(pred, target, model):
loss = nn.MSELoss(reduction='mean')
regularization_loss = 0
for param in model.parameters():
regularization_loss += torch.sum(param ** 2)
return loss(pred, target) + 0.00075 * regularization_loss
L2正则化通过对大权重施加惩罚,鼓励模型学习更简单的模式。正则化系数0.00075需要根据具体问题调整,太大会导致欠拟合,太小则可能无法有效防止过拟合。
4.3 优化器选择与配置
代码中使用了带动量的SGD优化器:
python复制optimizer = optim.SGD(params=model.parameters(), lr=lr, momentum=0.9)
动量项(momentum=0.9)可以帮助优化器冲出局部极小值,加速收敛。对于这种简单的全连接网络,SGD通常表现不错。但对于更复杂的模型,Adam优化器可能是更好的选择,因为它能自动调整学习率。
5. 模型评估与结果分析
5.1 评估流程实现
evaluate函数加载训练好的最佳模型,对测试集进行预测,并将结果保存为CSV文件:
python复制def evaluate(model_path, test_loader, rel_path, device):
model = torch.load(model_path).to(device)
rel = []
model.eval()
with torch.no_grad():
for x in test_loader:
x = x.to(device)
pred = model(x)
rel.append(pred.cpu().item())
with open(rel_path, "w", newline="") as f:
csv_writer = csv.writer(f)
csv_writer.writerow(["id", "tested_positive"])
for i, pred in enumerate(rel):
csv_writer.writerow([str(i), str(pred)])
print(f"结果保存到了{rel_path}")
5.2 训练曲线分析
训练过程中记录了训练集和验证集的损失变化,并绘制了损失曲线。理想的训练曲线应该显示:
- 训练损失和验证损失都持续下降
- 最终两者都收敛到一个较低的值
- 两者之间的差距不大
如果出现以下情况,可能需要调整模型或训练策略:
- 训练损失下降但验证损失不降:可能过拟合,需要增加正则化或获取更多数据
- 两者都下降很慢:可能需要增大模型容量或调整学习率
- 验证损失波动大:可能需要减小批量大小或使用更稳定的优化器
5.3 模型性能改进方向
虽然这个基础模型已经能工作,但还有很大的改进空间:
- 尝试更复杂的网络结构(如增加层数、使用批归一化)
- 使用交叉验证更可靠地评估模型性能
- 实现早停(Early Stopping)防止过拟合
- 添加更详细的特征工程
- 尝试不同的优化器和学习率调度策略
6. 实战经验与常见问题
6.1 设备选择与CUDA使用
代码中自动检测并使用可用的CUDA设备:
python复制device = "cuda" if torch.cuda.is_available() else "cpu"
在实际使用中,有几点需要注意:
- 确保数据和模型都在同一设备上,否则会报错
- 对于小模型和小数据集,使用GPU可能不会带来明显加速
- 可以使用torch.cuda.empty_cache()定期清理GPU缓存
6.2 数据加载的最佳实践
DataLoader的配置对训练效率有很大影响:
python复制train_loader = DataLoader(train_set, batch_size=batch_size, shuffle=True)
val_loader = DataLoader(val_set, batch_size=batch_size, shuffle=True)
test_loader = DataLoader(test_set, batch_size=1, shuffle=False)
关键参数说明:
- batch_size:根据GPU内存选择,通常16-256之间
- shuffle:训练集必须shuffle,验证集和测试集不需要
- num_workers:可以设置大于0的值加速数据加载(在__main__中使用)
6.3 常见错误与解决方案
-
维度不匹配错误:
- 检查模型输入输出维度是否与数据匹配
- 使用print(x.shape)调试张量形状
-
梯度爆炸/消失:
- 使用梯度裁剪(torch.nn.utils.clip_grad_norm_)
- 尝试不同的权重初始化方法
-
过拟合:
- 增加L2正则化系数
- 添加Dropout层
- 获取更多训练数据
-
训练不收敛:
- 检查学习率是否合适
- 尝试不同的优化器
- 确保数据预处理正确
7. 项目扩展与进阶方向
这个基础项目可以进一步扩展为更实用的疫情预测系统:
- 时间序列建模:将普通全连接网络改为LSTM或Transformer,更好地处理时间序列数据
- 多任务学习:同时预测多个相关指标(如确诊病例、重症病例、死亡病例)
- 部署为Web服务:使用Flask或FastAPI将模型部署为REST API
- 自动化模型训练:使用MLflow或Weights & Biases管理实验
- 模型解释性:使用SHAP或LIME解释模型预测
对于想要深入学习的开发者,我建议从PyTorch Lightning开始,它能大幅减少样板代码,让你更专注于模型设计和实验。