前言

考研结束后,我决定系统学习深度学习的实践应用。这篇博客记录了我第一个完整的深度学习项目——基于PyTorch的COVID-19检测结果预测系统。从数据处理到模型训练,从特征选择到正则化优化。

一、项目背景与数据集

1.1 问题定义

这是一个回归预测问题:根据患者的医疗指标数据,预测COVID-19检测结果的数值。

数据集信息:

  • 训练集:covid.train.csv(包含标签)
  • 测试集:covid.test.csv(不包含标签)
  • 特征维度:93个医疗指标
  • 目标变量:COVID-19检测结果(连续值)

1.2 Excel数据样式

id, feature_1, feature_2,..., feature_93, tested_positive
 0,       1.2,       3.4,...,        5.6,             0.8
 1,       2.3,       4.5,...,        6.7,             1.2
...

二、环境配置

2.1 导入必要的库

#导入matplotlib.pyplot模块,用于绘制训练和验证损失曲线
import matplotlib.pyplot as plt
#导入PyTorch深度学习框架核心库,用来计算张量和实现自动微分
import torch
#NumPy库,用于数值运算和数组处理
import numpy as np
#用于读写CSV格式文件
import csv
#pandas数据分析库用于数据预处理
import pandas as pd
#从torch.utils.data导入DataLoader(DataLoader提供批量加载数据的功能)和Dataset
from torch.utils.data import DataLoader, Dataset
#torch.nn模块,包含神经网络构建所需层和函数
import torch.nn as nn
#optim模块,实现优化功能
from torch import optim
#time模块记录训练耗时
import time
#从sklearn.feature_selection导入SelectKBest(特征选择器)和chi2(卡方检验函数)
from sklearn.feature_selection import SelectKBest, chi2
#导入os模块,用于操作系统相关功能(如环境变量设置)
import os
# 解决matplotlib在某些环境下的兼容性问题
os.environ["KMP_DUPLICATE_LIB_OK"]="TRUE"

2.2 检查GPU可用性

device = "cuda" if torch.cuda.is_available() else "cpu"   
print(f"使用设备: {device}")

输出

使用设备: cuda  # 如果有GPU
使用设备: cpu   # 如果没有GPU

三、特征工程:卡方检验特征选择

3.1 为什么需要特征选择?

问题: 93个特征过多,当特征越多,模型参数越多,越容易过拟合

解决方案: 使用卡方检验选出最重要的K个特征

3.2 卡方检验

卡方检验: 利用统计学特性来高效筛选高价值特征:

3.3 代码实现

def get_feature_importance(feature_data, label_data, k=4, column=None):   
    """
    使用卡方检验选择最重要的k个特征
    
    参数:
        feature_data: 特征数据 (n_samples, n_features)
        label_data: 标签数据 (n_samples,)
        k: 要选择的特征数量
        column: 列名列表(用于打印特征名称)
    
    返回:
        X_new: 选择后的特征数据
        indices: 选中特征的索引
    """
    # 定义卡方检验选择器,选择k个最佳特征
    model = SelectKBest(chi2, k=k)
    
    # 确保数据类型正确
    feature_data = np.array(feature_data, dtype=np.float64)
    
    # 拟合并转换数据
    X_new = model.fit_transform(feature_data, label_data)
    
    # 获取每个特征的卡方得分
    scores = model.scores_
    
    # 按得分从高到低排序,获取索引
    indices = np.argsort(scores)[::-1]  # [::-1]表示反转,从大到小
    
    # 如果提供了列名,打印选中的特征
    if column:
        k_best_features = [column[i+1] for i in indices[0:k].tolist()]
        print('选中的最佳特征:', k_best_features)
    
    return X_new, indices[0:k]

3.4 特征选择效果

# 原始:93个特征
# 选择后:6个特征

# 参数量对比:
# 93特征 → 模型参数:93×64 + 64×1 = 6016个
# 6特征  → 模型参数:6×64 + 64×1 = 448个

# 参数减少:93%!
# 训练速度提升:约10倍!

四、自定义数据集类

4.1 自定义Dataset

PyTorch的DataLoader需要一个Dataset对象来:

  • 读取和预处理数据
  • 支持索引访问
  • 支持批量加载

4.2 完整代码实现

class CovidDataset(Dataset):   
    def __init__(self, file_path, mode="train", all_feature=False, feature_dim=6):   
        """
        初始化数据集
        
        参数:
            file_path: CSV文件路径
            mode: "train"(训练集) / "val"(验证集) / "test"(测试集)
            all_feature: 是否使用所有特征
            feature_dim: 使用的特征数量
        """
        # 第1步:读取CSV文件
        with open(file_path, "r") as f:   
            ori_data = list(csv.reader(f))
            column = ori_data[0]  # 第一行是列名
            csv_data = np.array(ori_data[1:])[:, 1:].astype(float)  # 去掉id列
        
        # 第2步:分离特征和标签 
        feature = np.array(ori_data[1:])[:, 1:-1]  # 所有列除了id和最后一列
        label_data = np.array(ori_data[1:])[:, -1]  # 最后一列是标签
        
        # 第3步:特征选择 
        if all_feature:
            # 使用所有93个特征
            col = np.array([i for i in range(0, 93)])
        else:
            # 使用卡方检验选择k个最佳特征
            X_new, col = get_feature_importance(feature, label_data, feature_dim, column)
        
        col = col.tolist()
        
        # 第4步:划分训练集/验证集 
        if mode == "train":
            # 训练集:每5个样本取4个(索引不能被5整除的)
            indices = [i for i in range(len(csv_data)) if i % 5 != 0]
            data = torch.tensor(csv_data[indices, :-1])
            self.y = torch.tensor(csv_data[indices, -1])
            
        elif mode == "val":
            # 验证集:每5个样本取1个(索引能被5整除的)
            indices = [i for i in range(len(csv_data)) if i % 5 == 0]
            data = torch.tensor(csv_data[indices, :-1])
            self.y = torch.tensor(csv_data[indices, -1])
            
        else:  # test
            # 测试集:使用所有数据
            indices = [i for i in range(len(csv_data))]
            data = torch.tensor(csv_data[indices])
        
        # 第5步:选择特征列
        data = data[:, col]
        
        # 第6步:数据标准化
        # 标准化公式:(x - mean) / std
        self.data = (data - data.mean(dim=0, keepdim=True)) / data.std(dim=0, keepdim=True)
        
        self.mode = mode
    
    def __getitem__(self, idx):
        """
        根据索引返回一个样本
        """
        if self.mode != "test":
            return self.data[idx].float(), self.y[idx].float()
        else:
            return self.data[idx].float()
    
    def __len__(self):
        """
        返回数据集大小
        """
        return len(self.data)

4.3 技术点详解

4.3.1 训练集/验证集划分策略

        逢5取1策略:

# 索引: 0  1  2  3  4  5  6  7  8  9  10 ...
# 类型: V  T  T  T  T  V  T  T  T  T  V  ...
#       ↑              ↑              ↑
#     验证集         验证集         验证集

# 训练集:80%的数据(i % 5 != 0)
# 验证集:20%的数据(i % 5 == 0)
4.3.2 数据标准化  
# 标准化公式:normalized_data = (data - mean) / std

# 例:原始数据:[10, 20, 30, 40, 50] 均值:30 标准差:14.14 标准化后:[-1.41, -0.71, 0, 0.71, 1.41]

五、神经网络模型设计

5.1 模型架构

class MyModel(nn.Module):   
    def __init__(self, inDim):
        """
        初始化模型
        
        参数:
            inDim: 输入特征维度(6或93)
        """
        super(MyModel, self).__init__()
        
        # 第一层:输入层 → 隐藏层
        self.fc1 = nn.Linear(inDim, 64)
        
        # 激活函数
        self.relu1 = nn.ReLU()
        
        # 第二层:隐藏层 → 输出层
        self.fc2 = nn.Linear(64, 1)
    
    def forward(self, x):
        """
        前向传播
        """
        x = self.fc1(x)      # 线性变换
        x = self.relu1(x)    # 非线性激活
        x = self.fc2(x)      # 输出层
        
        # 处理输出维度
        if len(x.size()) > 1:
            return x.squeeze(1)  # 去掉多余的维度
        return x

5.2 模型结构可视化

输入层          隐藏层          输出层
(6维)          (64维)          (1维)

x₁ ─┐
x₂ ─┤
x₃ ─┼─→ [64个神经元] ─→ [1个神经元] ─→ 预测值
x₄ ─┤    (ReLU激活)
x₅ ─┤
x₆ ─┘

参数量计算:
- fc1: 6×64 + 64 = 448个参数
- fc2: 64×1 + 1 = 65个参数
- 总计: 513个参数

六、损失函数与正则化

6.1 自定义损失函数

def mseLoss_with_reg(pred, target, model):
    """
    带L2正则化的MSE损失函数
    
    参数:
        pred: 模型预测值
        target: 真实标签
        model: 模型对象(用于获取参数)
    
    返回:
        总损失 = MSE损失 + L2正则化损失
    """
    # 第1步:计算MSE损失
    loss = nn.MSELoss(reduction='mean')
    mse_loss = loss(pred, target)
    
    # 第2步:计算L2正则化损失
    regularization_loss = 0
    
    for param in model.parameters():
        # L2正则化:参数的平方和
        regularization_loss += torch.sum(param ** 2)
        
        # L1正则化(注释掉的):
        # regularization_loss += torch.sum(abs(param))
    
    # 第3步:组合损失
    # λ = 0.00075(正则化系数)
    total_loss = mse_loss + 0.00075 * regularization_loss
    
    return total_loss

6.2 MSE损失详解

# MSE (Mean Squared Error) 均方误差

MSE = (1/n) × Σ(预测值 - 真实值)²

# 例子:
真实值:[1.0, 2.0, 3.0]
预测值:[1.2, 1.8, 3.1]

误差:[0.2, -0.2, 0.1]
平方:[0.04, 0.04, 0.01]
MSE = (0.04 + 0.04 + 0.01) / 3 = 0.03

为什么用MSE?

  • 回归问题的标准损失函数
  • 惩罚大误差(平方项)
  • 可微分,便于梯度下降

6.3 L2正则化详解

# L2正则化公式
L2_loss = λ × Σ(参数²)

# 总损失
total_loss = MSE + λ × Σ(参数²)
            ↑           ↑
        拟合数据    保持简单

作用机制:

# 没有正则化:
模型只关心降低MSE
→ 参数可能变得很大
→ 模型过度复杂
→ 过拟合

# 有L2正则化:
模型需要平衡两个目标:
1. 降低MSE(拟合数据)
2. 降低参数平方和(保持简单)
→ 参数保持较小
→ 模型简单稳定
→ 泛化能力强

λ = 0.00075的含义:

# λ太小(如0.00001):
正则化作用弱,可能过拟合

# λ适中(如0.00075):✓
平衡拟合和简单性

# λ太大(如0.1):
正则化作用太强,可能欠拟合

七、模型训练

7.1 训练配置

config = {   
    "lr": 0.001,              # 学习率
    "epochs": 20,             # 训练轮数
    "momentum": 0.9,          # 动量
    "save_path": "model_save/best_model.pth",  # 模型保存路径
    "rel_path": "pred.csv"    # 预测结果保存路径
}

7.2 完整训练函数

def train_val(model, train_loader, val_loader, device, epochs, optimizer, loss, save_path):
    """
    训练和验证模型
    
    参数:
        model: 神经网络模型
        train_loader: 训练数据加载器
        val_loader: 验证数据加载器
        device: 设备(CPU或GPU)
        epochs: 训练轮数
        optimizer: 优化器
        loss: 损失函数
        save_path: 模型保存路径
    """
    model = model.to(device)
    
    # 记录每轮的损失
    plt_train_loss = []
    plt_val_loss = []
    
    # 记录最小验证损失
    min_val_loss = 9999999999999
    
    # 开始训练
    for epoch in range(epochs):
        train_loss = 0.0
        val_loss = 0.0
        start_time = time.time()
        
        # 训练阶段
        model.train()  # 设置为训练模式
        
        for batch_x, batch_y in train_loader:
            # 数据移到设备上
            x, target = batch_x.to(device), batch_y.to(device)
            
            # 前向传播
            pred = model(x)
            
            # 计算损失(包含正则化)
            train_bat_loss = loss(pred, target, model)
            
            # 反向传播
            train_bat_loss.backward()
            
            # 更新参数
            optimizer.step()
            
            # 清空梯度
            optimizer.zero_grad()
            
            # 累加损失
            train_loss += train_bat_loss.cpu().item()
        
        # 计算平均训练损失
        plt_train_loss.append(train_loss / len(train_loader))
        
        # 验证阶段
        model.eval()  # 设置为评估模式
        
        with torch.no_grad():  # 不计算梯度
            for batch_x, batch_y in val_loader:
                x, target = batch_x.to(device), batch_y.to(device)
                
                # 前向传播
                pred = model(x)
                
                # 计算损失
                val_bat_loss = loss(pred, target, model)
                
                # 累加损失
                val_loss += val_bat_loss.cpu().item()
        
        # 计算平均验证损失
        plt_val_loss.append(val_loss / len(val_loader))
        
        # 保存最佳模型
        if val_loss < min_val_loss:
            torch.save(model, save_path)
            min_val_loss = val_loss
            print(f"✓ 保存最佳模型(验证损失: {val_loss:.6f})")
        
        # 打印训练信息
        print("[%03d/%03d] %2.2f sec(s) Train loss: %.6f | Val loss: %.6f" % 
              (epoch, epochs, time.time()-start_time, 
               plt_train_loss[-1], plt_val_loss[-1]))
    
    # 绘制损失曲线
    plt.plot(plt_train_loss, label='Train Loss')
    plt.plot(plt_val_loss, label='Val Loss')
    plt.title("Training and Validation Loss")
    plt.xlabel("Epoch")
    plt.ylabel("Loss")
    plt.legend()
    plt.grid(True)
    plt.show()

7.3 训练过程详解

7.3.1 训练模式 vs 评估模式
# 训练模式
model.train()
- 启用Dropout(如果有)
- 启用BatchNorm的更新(如果有)
- 计算梯度

# 评估模式
model.eval()
- 禁用Dropout
- 使用固定的BatchNorm参数
- 不计算梯度(配合torch.no_grad())
7.3.2 梯度清零的重要性
# 为什么需要optimizer.zero_grad()?

# PyTorch的梯度是累加的:
第1次backward():grad = 梯度1
第2次backward():grad = 梯度1 + 梯度2  # 累加!
第3次backward():grad = 梯度1 + 梯度2 + 梯度3

# 如果不清零:
梯度会越来越大 → 参数更新错误 → 训练失败

# 正确做法:
for batch in data_loader:
    optimizer.zero_grad()  # 清零梯度
    loss.backward()        # 计算梯度
    optimizer.step()       # 更新参数
7.3.3 保存最佳模型策略
# 为什么保存验证损失最小的模型?

训练损失最小 ≠ 模型最好
验证损失最小 = 泛化能力最强 ✓

# 例子:
Epoch 10: train_loss=0.20, val_loss=0.35 → 保存
Epoch 11: train_loss=0.18, val_loss=0.40 → 不保存(验证损失变大)
Epoch 12: train_loss=0.15, val_loss=0.32 → 保存(验证损失更小)

7.4 训练输出示例

k best features are:  ['tested_positive', 'tested_positive', 'nohh_cmnty_cli', 'nohh_cmnty_cli', 'nohh_cmnty_cli', 'hh_cmnty_cli']
cuda
[000/020] 2.17 sec(s) Trainloss: 24.923024 |Valloss: 1.101962
[001/020] 0.29 sec(s) Trainloss: 1.307560 |Valloss: 1.073767
[002/020] 0.30 sec(s) Trainloss: 1.236160 |Valloss: 1.528566
[003/020] 0.30 sec(s) Trainloss: 1.169382 |Valloss: 1.005413
[004/020] 0.30 sec(s) Trainloss: 1.132450 |Valloss: 0.986223
[005/020] 0.30 sec(s) Trainloss: 1.129263 |Valloss: 1.044358
[006/020] 0.28 sec(s) Trainloss: 1.137480 |Valloss: 1.008713
[007/020] 0.29 sec(s) Trainloss: 1.125366 |Valloss: 0.958152
[008/020] 0.28 sec(s) Trainloss: 1.157921 |Valloss: 1.735986
[009/020] 0.29 sec(s) Trainloss: 1.144497 |Valloss: 0.945615
[010/020] 0.29 sec(s) Trainloss: 1.164328 |Valloss: 1.145512
[011/020] 0.30 sec(s) Trainloss: 1.137105 |Valloss: 0.990260
[012/020] 0.31 sec(s) Trainloss: 1.119704 |Valloss: 1.120381
[013/020] 0.31 sec(s) Trainloss: 1.123307 |Valloss: 0.974390
[014/020] 0.30 sec(s) Trainloss: 1.105090 |Valloss: 1.004918
[015/020] 0.31 sec(s) Trainloss: 1.111448 |Valloss: 0.941703
[016/020] 0.34 sec(s) Trainloss: 1.111513 |Valloss: 1.115820
[017/020] 0.33 sec(s) Trainloss: 1.097978 |Valloss: 0.975191
[018/020] 0.35 sec(s) Trainloss: 1.158795 |Valloss: 1.250422
[019/020] 0.37 sec(s) Trainloss: 1.157728 |Valloss: 0.979788

八、模型评估与预测

8.1 评估函数

def evaluate(save_path, test_loader, device, rel_path):
    """
    在测试集上评估模型并保存预测结果
    
    参数:
        save_path: 保存的模型路径
        test_loader: 测试数据加载器
        device: 设备
        rel_path: 预测结果保存路径
    """
    # 加载最佳模型
    model = torch.load(save_path).to(device)
    
    # 存储预测结果
    rel = []
    
    # 预测
    with torch.no_grad():
        for x in test_loader:
            pred = model(x.to(device))
            rel.append(pred.cpu().item())
    
    print("预测结果:", rel[:10], "...")  # 打印前10个
    
    # 保存到CSV文件
    with open(rel_path, "w", newline='') as f:
        csvWriter = csv.writer(f)
        csvWriter.writerow(["id", "tested_positive"])
        for i, value in enumerate(rel):
            csvWriter.writerow([str(i), str(value)])
    
    print(f"✓ 预测结果已保存到 {rel_path}")

九、损失曲线可视化

 本项目的损失曲线

 十、常见问题与解决方案

10.1 问题1:KMP_DUPLICATE_LIB_OK错误

错误信息:

OMP: Error #15: Initializing libiomp5md.dll, but found libiomp5md.dll already initialized.

解决方案:

import os
os.environ["KMP_DUPLICATE_LIB_OK"]="TRUE"

10.2 问题2:验证损失不下降

可能原因:

  • 学习率太大或太小
  • 正则化系数太大
  • 模型太简单

解决方案:

# 调整学习率
config["lr"] = 0.0001  # 降低学习率

# 调整正则化系数
regularization_loss * 0.0001  # 降低正则化强度

# 增加模型复杂度
self.fc1 = nn.Linear(inDim, 128)  # 增加隐藏层神经元

10.3 问题3:训练损失和验证损失差距大

原因: 过拟合

解决方案:

# 1. 增强正则化
regularization_loss * 0.001  # 增大λ

# 2. 减少模型复杂度
self.fc1 = nn.Linear(inDim, 32)  # 减少神经元

# 3. 增加训练数据
# 或使用数据增强技术

10.4 问题4:GPU内存不足

错误信息:

RuntimeError: CUDA out of memory

解决方案:

# 1. 减小batch_size
batch_size = 8  # 从16减到8

# 2. 使用CPU训练
device = "cpu"

# 3. 清理GPU缓存
torch.cuda.empty_cache()

10.5 问题5:损失变成NaN

可能原因:

  • 学习率太大
  • 梯度爆炸
  • 数据未标准化

解决方案:

# 1. 降低学习率
config["lr"] = 0.0001

# 2. 梯度裁剪
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

# 3. 检查数据标准化
print(data.mean(), data.std())  # 应该接近0和1
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐