通过线性回归,对 COVID-19 的相关数据进行分析,预测病例
题目来源: ML2021Spring-hw1 | Kaggle
一、数据处理
Dataset模块
主要由__init__、__getitem__、__len__三部分构成
1、数据的读取与预处理
- 使用Python中的csv模块读取csv文件,存储为ori_data列表
- 使用numpy库去掉第一行和第一列(列名和id),转换为浮点数
- 将数据划分为特征和标签,其中特征存储在
feature
中,标签存储在label_data
中
2、特征选择
- 如果我们不使用所有特征(
all_feature
为False),
则使用get_feature_importance
函数,该函数使用SelectKBest
和chi2
统计量从所有特征中选择feature_dim
个最有用的特征(与预测结果最相关的特征) get_feature_importance
函数对特征和标签进行分析,根据卡方检验的结果找出最重要的k
个特征,并返回它们的索引。
3、数据集的划分
- 我们将数据集划分为训练集、验证集和测试集。
- 每5条数据中选择一条作为验证集,其余为训练集。测试集使用全部数据
- 对于训练集和验证集,取最后一列作为标签(对于每个数据的正确结果y),前面的列作为特征
- 对于特征数据,进行归一化处理,使用
(data - data.mean(dim=0, keepdim=True)) / data.std(dim=0, keepdim=True)
来确保数据的均值为 0 且标准差为 1,这有助于加速模型的收敛和提高模型的稳定性。
Z-score 标准化/零均值归一化:
使用梯度下降等优化算法训练模型时,不同特征的取值范围可能差异很大。例如,一个特征的范围是 0 到 1,而另一个特征的范围是 1000 到 10000,这种差异会导致损失函数的等高线变得非常扁平或非常陡峭,使优化算法在寻找最优解时需要更多的迭代次数,甚至可能陷入局部最优解。归一化将所有特征缩放到相似的范围,方便比较不同特征对结果的贡献,有助于特征选择和特征重要性分析。也使得损失函数的等高线更加接近圆形,梯度下降可以更快地找到最优解,从而加速模型的收敛速度。
class CovidDataset(Dataset):
def __init__(self, file_path, mode="train", all_feature=True, feature_dim=6): # 默认为train
with open(file_path, "r") as f:
ori_data = list(csv.reader(f))
column = ori_data[0]
# 去掉第一行变矩阵,再取第二列开始的行(去第一列),然后字符串转浮点
# [:, 1:]对生成的 numpy 数组进行切片操作,冒号 : 表示选取所有行,1: 表示从第二列开始选取所有列
csv_data = np.array(ori_data[1:])[:, 1:].astype(float)
feature = np.array(ori_data[1:])[:, 1:-1] # 从第二列1到最后一列
label_data = np.array(ori_data[1:])[:, -1]
if all_feature:
col = np.array([i for i in range(0, 93)])
else:
_, col = get_feature_importance(feature, label_data, feature_dim, column)
col = col.tolist()
# 训练集
if mode == "train":
# 跳过了所有0 5
indices = [i for i in range(len(csv_data)) if i % 5 != 0]
data = torch.tensor(csv_data[indices, :-1]) # :-1 用于选择列,它会选取除了最后一列的所有列
self.y = torch.tensor(csv_data[indices, -1]) # 最后一列
# 验证集 逢5取1
elif mode == "val":
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:
indices = [i for i in range(len(csv_data))]
data = torch.tensor(csv_data[indices]) # 不需要取最后一列
data = data[:, col]
# 数据归化,减去平均值,除以标准差,
# dim=0 表示沿着维度 0(通常是行维度)
# keepdim=True 确保结果张量的维度与原张量 data 的维度一致,即结果是一个形状为 (1, n) 的张量,其中 n 是 data 的列数。
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)
二、数据加载器
train_file = "covid.train.csv"
test_file = "covid.test.csv"
train_dataset = CovidDataset(train_file, "train", all_feature=all_feature, feature_dim=feature_dim)
val_dataset = CovidDataset(train_file, "val", all_feature=all_feature, feature_dim=feature_dim)
test_dataset = CovidDataset(test_file, "test", all_feature=all_feature, feature_dim=feature_dim)
batch_size = 16
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True) # shuffle打乱
val_loader = DataLoader(val_dataset, batch_size=batch_size, shuffle=True) # shuffle打乱
test_loader = DataLoader(test_dataset, batch_size=1, shuffle=False)
数据集创建
- 使用
CovidDataset
类创建了三个不同的数据集对象:train_dataset
、val_dataset
和test_dataset
。
数据加载器创建
- 使用 PyTorch 的
DataLoader
类,将CovidDataset
包装成train_loader
、val_loader
和test_loader
batch_size
:表示每个批次的大小。- 对于
train_loader
和val_loader
,batch_size
被设置为 16,这意味着每次从数据集中提取 16 个样本作为一个批次。 - 对于
test_loader
,batch_size
被设置为 1,因为在测试阶段,通常逐个样本进行测试,不需要进行批处理。
- 对于
shuffle
:决定是否打乱数据。- 对于
train_loader
和val_loader
,shuffle=True
,表示在每个epoch
开始时,对数据进行随机打乱。这样做可以防止模型在训练过程中对数据的顺序产生依赖,有助于提高模型的泛化能力。 - 对于
test_loader
,shuffle=False
,因为测试集不需要打乱,需要按顺序测试样本,以确保结果的可重复性和一致性。
- 对于
使用
DataLoader
类的好处在于:
- 方便地实现了数据的批处理:在深度学习中,通常不会一次性将所有数据都输入到模型中,而是将数据分成多个批次进行处理。这样可以节省内存,并允许使用随机梯度下降及其变种(如 SGD、Adam 等)进行更有效的训练。
- 自动处理数据的迭代:通过
DataLoader
,可以方便地使用for
循环遍历数据集,它会自动处理数据的迭代,每次返回一个批次的数据。例如:for batch_x, batch_y in train_loader: print(batch_x, batch_y) # batch_x 是一个包含 batch_size 个样本的特征张量 # batch_y 是相应的标签张量。这样可以方便地将数据输入到模型中进行训练或验证。
三、模型构建
# 模型部分
class MyModel(nn.Module):
def __init__(self, inDim):
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
我们定义了一个简单的神经网络模型 MyModel
,它继承自 nn.Module,
这是 PyTorch 中构建神经网络的标准做法。
1、__init__
方法:
- 该方法用于初始化模型的层和参数。
super(MyModel, self).__init__()
:调用父类nn.Module
的__init__
方法,确保正确初始化父类的部分。self.fc1 = nn.Linear(inDim, 64)
:- 这是一个线性层,将输入维度
inDim
映射到 64 维。这里的inDim
是输入特征的维度,根据输入数据的特征数量确定。 - 线性层的计算公式为:
,其中
是输入,
是权重矩阵,
是偏置向量,
是输出。
- 这是一个线性层,将输入维度
self.relu1 = nn.ReLU()
:- 定义一个
ReLU
(Rectified Linear Unit)激活函数。 ReLU
函数的定义为:,它为模型引入非线性,使模型能够学习非线性关系。如果没有激活函数,多个线性层的组合仍然是线性的,无法处理复杂的非线性问题。
- 定义一个
self.fc2 = nn.Linear(64, 1)
:- 另一个线性层,将 64 维映射到 1 维。这通常用于最终的输出层,例如在回归任务中预测一个数值,或者在二分类任务中预测一个概率。
2、forward
方法:
- 该方法定义了数据在模型中的前向传播过程,也就是数据如何通过模型的各个层。
x = self.fc1(x)
:将输入x
通过第一个线性层fc1
,得到一个 64 维的中间结果。x = self.relu1(x)
:将fc1
的输出通过ReLU
激活函数,引入非线性。x = self.fc2(x)
:将激活后的结果通过第二个线性层fc2
,得到最终的输出。if len(x.size()) > 1: return x.squeeze(1)
:x.size()
用于获取张量x
的形状。如果x
的维度大于 1(例如,x
是一个二维张量),使用squeeze(1)
方法去掉维度为 1 的维度。squeeze(1)
方法会将形状为(batch_size, 1)
的张量压缩为(batch_size,)
,这是为了确保输出的维度符合预期,通常在最终输出为一维时使用,例如回归任务中预测一个标量值。
为什么这样设计模型:
- 线性层和非线性层的组合:
- 神经网络需要非线性激活函数来处理复杂的非线性关系。仅使用线性层,无论多少层组合在一起,最终结果仍然是输入的线性函数,无法学习复杂的数据模式。通过
ReLU
激活函数,可以使模型具有更强的表达能力,能够学习到输入和输出之间的非线性映射。- 输入和输出维度:
- 第一个线性层
fc1
的输入维度inDim
是根据输入数据的特征数量确定的,这确保了模型能够接收输入数据。将其映射到 64 维,增加了模型的表示能力,同时不会导致过多的参数,避免过拟合和增加计算成本。- 第二个线性层
fc2
输出为 1 维,适用于许多任务,例如回归任务(预测一个连续值)或二分类任务(通过设置合适的阈值,可以将输出转换为概率)。squeeze
操作:
- 对于一些深度学习框架和损失函数,要求输入和输出的维度符合特定的格式。
squeeze(1)
确保输出的张量维度符合后续处理的要求,例如在计算损失函数时,如果使用nn.MSELoss
或nn.BCELoss
等,可能需要输出为(batch_size,)
而不是(batch_size, 1)
的形式,以避免维度不匹配的错误。
四、训练和验证--train_val
函数
def train_val(model, train_loader, val_loader, device, epochs, optimizer, loss, save_path):
model = model.to(device)
# 记录训练的loss值
plt_train_loss = []
plt_val_loss = []
# 记录最小loss
min_val_loss = 9999999999
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 / train_loader.__len__())
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 / val_loader.__len__())
if val_loss < min_val_loss:
torch.save(model, save_path)
min_val_loss = val_loss
print("[%03d/%03d] %2.2f sec(s) Trainloss: %.6f |Valloss: %.6f" % \
(epoch, epochs, time.time() - start_time, plt_train_loss[-1], plt_val_loss[-1]))
plt.plot(plt_train_loss)
plt.plot(plt_val_loss)
plt.title("loss")
plt.legend(["train", "val"])
plt.show()
函数功能
训练和验证 PyTorch 模型,同时记录训练和验证过程中的损失,并保存性能最佳的模型。
函数参数
model
:需要训练和验证的 PyTorch 模型对象,通常是nn.Module
的子类实例。train_loader
:训练集的数据加载器,使用DataLoader
类创建,提供训练数据的批次。val_loader
:验证集的数据加载器,使用DataLoader
类创建,提供验证数据的批次。device
:计算设备,如cuda
或cpu
,用于将数据和模型移至 GPU 或 CPU 进行计算。epochs
:训练的轮数,即模型将遍历整个训练集的次数。optimizer
:优化器,例如optim.SGD
或optim.Adam
,用于更新模型的参数。loss
:损失函数,用于计算预测值和真实值之间的差异,例如nn.MSELoss
或自定义的损失函数。save_path
:保存性能最佳模型的文件路径。
内部实现
1、设备迁移:
model = model.to(device)
- 将模型移动到指定的计算设备上,以便利用 GPU 的加速能力(如果
device
是cuda
)
2、初始化和准备工作:
plt_train_loss = [] # 用于存储每轮训练的平均损失
plt_val_loss = [] # 用于存储每轮训验证的平均损失
min_val_loss = 9999999999 # 存储最小的验证损失,初始化为一个很大的值,以便后续保存性能最佳的模型。
3、训练和验证的 epochs
循环:
for epoch in range(epochs):
epochs:深度学习模型通常需要对整个训练数据集进行多次遍历才能学习到数据中的复杂模式。每一次遍历整个训练集被称为一个 epoch。通过设置多个 epochs,可以让模型逐渐调整参数,以更好地拟合训练数据,从而提高模型的泛化能力。但过多的 epochs 可能导致过拟合,因此需要通过验证集来监控模型性能,选择合适的 epochs 数量。
①训练阶段:
train_loss = 0.0
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 / train_loader.__len__())
设置训练损失和训练模式
train_loss = 0.0
:初始化每轮的训练损失为 0,用于累加每个批次的损失,以便在一个 epoch 结束后计算平均损失。model.train()
:将模型设置为训练模式,启用 Dropout 和 BatchNorm 的训练行为(如果模型中有这些层)。例如,Dropout
层在训练时会随机丢弃一些神经元,以防止过拟合;BatchNorm
层会根据当前批次的数据计算均值和方差进行归一化。
遍历训练批次并更新模型
for batch_x, batch_y in train_loader:
:遍历训练集的每个批次。
批次遍历:使用
DataLoader
加载数据,以批次为单位处理数据。这不仅可以减少内存占用,还能利用随机梯度下降(SGD)及其变种优化算法。SGD 在每个批次上计算梯度并更新参数,相比在整个数据集上计算梯度,能够更快地收敛,并且有助于跳出局部最优解。
x,target = batch_x.to(device), batch_y.to(device)
:将批次数据和标签移动到指定设备。
数据迁移:将每个批次的输入数据和目标标签移动到指定的计算设备上,确保模型计算在同一设备上进行。
pred = model(x)
:使用模型进行预测。
前向传播:
pred = model(x)
通过模型对输入数据进行前向传播,得到预测结果。
train_bat_loss = loss(pred, target, model)
:计算预测值和真实值之间的损失,这里的loss
函数可以是自定义的
计算损失:使用损失函数计算预测值与真实值之间的差异。损失函数不仅衡量了模型当前的预测准确性,也是优化算法的目标,通过最小化损失来调整模型参数。这里使用自定义的损失函数
mseLoss_with_reg
,包含正则化项,用于防止过拟合。
train_bat_loss.backward()
:通过反向传播算法计算损失相对于模型参数的梯度。optimizer.step()
:根据计算得到的梯度更新模型的参数。optimizer.zero_grad()
:清空梯度,为下一个批次做准备。
反向传播与参数更新:
- 反向传播是深度学习中计算梯度的核心方法,它利用链式法则从输出层向输入层反向传播梯度,使得我们能够计算每个参数对损失的贡献。
- 不同的优化器(如
SGD
、Adam
等)有不同的参数更新策略,目的是朝着损失函数减小的方向调整参数。- 如果不清空梯度,梯度会在每个批次间累加,导致参数更新错误。
train_loss += train_bat_loss.cpu().item()
:累加每个批次的损失。plt_train_loss.append(train_loss / train_loader.__len__())
:计算并存储本epoch
的平均训练损失。
记录平均损失:
plt_train_loss.append(train_loss / train_loader.__len__())
在一个 epoch 结束后,计算该 epoch 的平均训练损失并记录下来,用于后续绘制训练损失曲线。
②验证阶段:
val_loss = 0.0
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 / val_loader.__len__())
if val_loss < min_val_loss:
torch.save(model, save_path)
min_val_loss = val_loss
设置验证损失和评估模式
val_loss = 0.0
:初始化每轮的验证损失为 0。用于累加验证集中每个批次的损失,以便计算平均验证损失。model.eval()
:将模型设置为评估模式,关闭 Dropout 和 BatchNorm 的训练行为。Dropout
层会停止随机丢弃神经元,BatchNorm
层会使用训练过程中计算得到的均值和方差进行归一化,这样可以确保验证结果的一致性和稳定性,避免在验证时引入额外的随机性。
遍历验证批次并计算损失
with torch.no_grad():
:确保在验证过程中不计算梯度,节省计算资源。
在验证过程中,不需要计算梯度,因为我们不打算在验证阶段更新模型参数。使用
torch.no_grad()
上下文管理器可以禁用梯度计算,从而节省计算资源和内存。
for batch_x, batch_y in val_loader:
:遍历验证集的每个批次。- 计算验证集上的损失,并累加到
val_loss
中。 plt_val_loss.append(val_loss / val_loader.__len__())
:计算并存储本epoch
的平均验证损失。
与训练阶段类似,遍历验证集的每个批次,将数据迁移到设备上,进行前向传播并计算损失。但在验证阶段,不进行反向传播和参数更新,只计算损失以评估模型在验证集上的性能。最后,计算并记录平均验证损失。
4、打印和可视化:
print("[%03d/%03d] %2.2f sec(s) Trainloss: %.6f |Valloss: %.6f" % \
(epoch, epochs, time.time() - start_time, plt_train_loss[-1], plt_val_loss[-1]))
plt.plot(plt_train_loss)
plt.plot(plt_val_loss)
plt.title("loss")
plt.legend(["train", "val"])
plt.show()
- 在每个 epoch 结束时,打印当前 epoch 的进度、训练和验证损失以及该 epoch 所花费的时间。这些信息有助于监控训练过程,了解模型的训练速度和性能变化情况。
- 通过绘制训练和验证损失曲线,可以直观地观察模型的训练动态。如果训练损失持续下降,而验证损失在某个点开始上升,这可能是过拟合的迹象;如果训练损失和验证损失都很高且没有明显下降趋势,可能存在欠拟合问题。通过观察损失曲线,我们可以调整模型结构、超参数(如学习率、正则化强度等)来优化模型性能。
五、评估和结果保存--evaluate
函数
# 得出测试结果文件
def evaluate(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)
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("文件已保存到" + rel_path)
该函数主要用于使用训练好的模型对测试集进行评估,并将评估结果存储到一个 CSV 文件中。
函数参数:
save_path
:存储训练好的最佳模型的文件路径。test_loader
:测试集的数据加载器,使用DataLoader
类创建,提供测试数据的批次。device
:计算设备,如cuda
或cpu
,用于将数据和模型移至 GPU 或 CPU 进行计算。rel_path
:存储测试结果的 CSV 文件路径。
内部实现
1、加载模型并迁移到设备
model = torch.load(save_path).to(device)
torch.load(save_path)
:从文件系统中加载之前保存的最佳模型。该模型是在train_val
函数中通过torch.save
保存的,它已经在训练和验证过程中被认为是性能最佳的模型。.to(device)
:将加载的模型移动到指定的计算设备上,以利用 GPU 或 CPU 进行计算。
2、预测并存储结果:
rel = []
with torch.no_grad():
for x in test_loader:
pred = model(x.to(device))
rel.append(pred.cpu().item())
rel = []
:初始化一个空列表rel
,用于存储预测结果。with torch.no_grad():
:使用torch.no_grad()
上下文管理器,确保在预测过程中不计算梯度,因为在测试阶段不需要更新模型参数,这样可以节省计算资源。for x in test_loader:
:遍历测试集的每个批次。pred = model(x.to(device))
:将测试数据x
移动到指定设备,通过模型进行预测,得到预测结果pred
。rel.append(pred.cpu().item())
:将预测结果从 GPU 迁移到 CPU(使用.cpu()
),并使用.item()
方法将张量转换为普通的 Python 数值,添加到rel
列表中。
3、保存结果到 CSV 文件:
print(rel)
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("文件已保存到" + rel_path)
print(rel)
:打印预测结果列表,方便查看。with open(rel_path, "w", newline='') as f:
:以写入模式打开文件rel_path
,并创建csvWriter
对象,使用csv.writer
进行 CSV 文件的写入操作。csvWriter.writerow(["id", "tested_positive"])
:首先写入 CSV 文件的表头,包含两列:id
和tested_positive
。for i, value in enumerate(rel):
:遍历rel
列表,将每个预测结果及其索引(作为id
)逐行写入 CSV 文件。
enumerate: Python 内置的一个函数,它用于将一个可迭代对象(如列表、元组、字符串等)组合为一个索引序列,同时列出数据和数据下标
enumerate(rel)
为rel
列表中的每个预测结果添加了一个索引。i
表示索引,value
表示预测结果。csvWriter.writerow([str(i), str(value)])
将每个预测结果及其索引作为一行写入 CSV 文件,其中i
作为id
,value
作为tested_positive
的值。
六、其他部分
1、get_feature_importance 函数
def get_feature_importance(feature_data, label_data, k=4, column=None):
model = SelectKBest(chi2, k=k)
feature_data = np.array(feature_data, dtype=np.float64)
X_new = model.fit_transform(feature_data, label_data)
print('x_new', X_new)
scores = model.scores_
indices = np.argsort(scores)[::-1]
if column:
k_best_features = [column[i] for i in indices[0:k].tolist()]
print('k best features are: ', k_best_features)
return X_new, indices[0:k]
- 此函数旨在从一组特征数据中选择最重要的
k
个特征。它使用sklearn
的SelectKBest
方法,并采用卡方检验(chi2
)作为特征选择的标准。 - 首先,将输入的
feature_data
(要求为字符串形式,内部会转换为numpy
数组)和label_data
(字符串形式)作为输入。 - 创建
SelectKBest
模型,指定选择k
个最佳特征。 - 使用
fit_transform
方法将特征数据和标签数据应用到模型中,得到X_new
,这是经过特征选择后的新特征矩阵。 - 通过
model.scores_
获取每个特征的得分,得分表示该特征与结果的相关性。 - 使用
np.argsort(scores)[::-1]
对得分进行排序并反转,得到indices
,使得最重要的特征排在前面。 - 如果提供了
column
(列名),它会打印出最重要的k
个特征的名称。
2、mseLoss_with_reg 函数
def mseLoss_with_reg(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
- 这是一个自定义的损失函数,在传统的均方误差损失(
MSELoss
)基础上添加了正则化项。 - 首先,创建
nn.MSELoss
实例,设置reduction='mean'
表示计算平均损失。 - 初始化
regularization_loss
为 0。 - 遍历模型的所有参数,将每个参数的平方累加到
regularization_loss
中,这里使用的是L2
正则化(将参数的平方求和)。 - 最终的损失是均方误差损失加上正则化项乘以 0.00075,添加正则化项可以防止过拟合,使模型在训练时不会过于依赖训练数据,提高模型的泛化能力。
3、超参部分及后续操作
device = "cuda" if torch.cuda.is_available() else "cpu" # 根据系统是否支持 CUDA,决定使用 GPU 还是 CPU 进行计算。
print(device)
config = {
"lr": 0.001,
"epochs": 20,
"momentum": 0.9,
"save_path": "model_save/best_model.pth",
"rel_path": "pred.csv",
}
lr
:学习率,控制模型参数更新的步长,这里设置为 0.001。epochs
:训练的轮数,决定了模型对整个训练数据集的遍历次数,设置为 20 次。momentum
:优化器的动量,有助于加速 SGD 的收敛,设置为 0.9。save_path
:存储训练过程中最佳模型的文件路径,这里是"model_save/best_model.pth"
。rel_path
:存储测试结果的文件路径,这里是"pred.csv"
。
4、模型创建和优化器配置
model = MyModel(inDim=feature_dim).to(device)
loss = mseLoss_with_reg
optimizer = optim.SGD(model.parameters(), lr=config["lr"], momentum=config["momentum"])
- 创建
MyModel
模型实例,并将其移动到指定的设备上。 - 使用自定义的带正则化的均方误差损失函数。
- 使用 SGD 优化器,传入模型的参数、学习率和动量。
5、训练和评估
train_val(model, train_loader, val_loader, device, config["epochs"], optimizer, loss, config["save_path"])
evaluate(config["save_path"], test_loader, device, config["rel_path"])
- 调用
train_val
函数进行模型的训练和验证。将创建的模型、数据加载器、设备、训练轮数、优化器、损失函数和保存路径作为参数传入,完成模型的训练和验证,并在训练过程中保存性能最佳的模型。 - 调用
evaluate
函数对测试集进行评估。它加载存储在save_path
的最佳模型,使用测试集test_loader
进行预测,并将预测结果存储在rel_path
的 CSV 文件中。
完整代码
import csv
import time
import matplotlib.pyplot as plt
import torch
import numpy as np
from torch.utils.data import DataLoader, Dataset
import torch.nn as nn
from torch import optim
from sklearn.feature_selection import SelectKBest, chi2
def get_feature_importance(feature_data, label_data, k=4, column=None):
"""
feature_data, label_data 要求字符串形式
k为选择的特征数量
如果需要打印column,需要传入行名
此处省略 feature_data, label_data 的生成代码。
如果是 CSV 文件,可通过 read_csv() 函数获得特征和标签。
这个函数的目的是, 找到所有的特征种, 比较有用的k个特征, 并打印这些列的名字。
"""
model = SelectKBest(chi2, k=k) # 定义一个选择k个最佳特征的函数
feature_data = np.array(feature_data, dtype=np.float64)
X_new = model.fit_transform(feature_data, label_data) # 用这个函数选择k个最佳特征
# feature_data是特征数据,label_data是标签数据,该函数可以选择出k个特征
print('x_new', X_new)
scores = model.scores_ # scores即每一列与结果的相关性
# 按重要性排序,选出最重要的 k 个
indices = np.argsort(scores)[::-1] # [::-1]表示反转一个列表或者矩阵。
# argsort这个函数, 可以矩阵排序后的下标。 比如 indices[0]表示的是,scores中最小值的下标。
if column: # 如果需要打印选中的列名字
k_best_features = [column[i] for i in indices[0:k].tolist()] # 选中这些列 打印
print('k best features are: ', k_best_features)
return X_new, indices[0:k] # 返回选中列的特征和他们的下标。
class CovidDataset(Dataset):
def __init__(self, file_path, mode="train", all_feature=True, feature_dim=6): # 默认为train
with open(file_path, "r") as f:
ori_data = list(csv.reader(f))
column = ori_data[0]
# 去掉第一行变矩阵,再取第二列开始的行(去第一列),然后字符串转浮点
# [:, 1:]对生成的 numpy 数组进行切片操作,冒号 : 表示选取所有行,1: 表示从第二列开始选取所有列
csv_data = np.array(ori_data[1:])[:, 1:].astype(float)
feature = np.array(ori_data[1:])[:, 1:-1] # 从第二列1到最后一列
label_data = np.array(ori_data[1:])[:, -1]
if all_feature:
col = np.array([i for i in range(0, 93)])
else:
_, col = get_feature_importance(feature, label_data, feature_dim, column)
col = col.tolist()
# 训练集
if mode == "train":
# 跳过了所有0 5
indices = [i for i in range(len(csv_data)) if i % 5 != 0]
data = torch.tensor(csv_data[indices, :-1]) # :-1 用于选择列,它会选取除了最后一列的所有列
self.y = torch.tensor(csv_data[indices, -1]) # 最后一列
# 验证集 逢5取1
elif mode == "val":
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:
indices = [i for i in range(len(csv_data))]
data = torch.tensor(csv_data[indices]) # 不需要取最后一列
data = data[:, col]
# 数据归化,减去平均值,除以标准差,
# dim=0 表示沿着维度 0(通常是行维度)
# keepdim=True 确保结果张量的维度与原张量 data 的维度一致,即结果是一个形状为 (1, n) 的张量,其中 n 是 data 的列数。
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)
all_feature = False
if all_feature:
feature_dim = 93
else:
feature_dim = 6
train_file = "covid.train.csv"
test_file = "covid.test.csv"
train_dataset = CovidDataset(train_file, "train", all_feature=all_feature, feature_dim=feature_dim)
val_dataset = CovidDataset(train_file, "val", all_feature=all_feature, feature_dim=feature_dim)
test_dataset = CovidDataset(test_file, "test", all_feature=all_feature, feature_dim=feature_dim)
batch_size = 16
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True) # shuffle打乱
val_loader = DataLoader(val_dataset, batch_size=batch_size, shuffle=True) # shuffle打乱
test_loader = DataLoader(test_dataset, batch_size=1, shuffle=False) # shuffle打乱
# for batch_x, batch_y in train_loader:
# print(batch_x, batch_y)
# 模型部分
class MyModel(nn.Module):
def __init__(self, inDim):
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
def train_val(model, train_loader, val_loader, device, epochs, optimizer, loss, save_path):
model = model.to(device)
# 记录训练的loss值
plt_train_loss = []
plt_val_loss = []
# 记录最小loss
min_val_loss = 9999999999
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 / train_loader.__len__())
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 / val_loader.__len__())
if val_loss < min_val_loss:
torch.save(model, save_path)
min_val_loss = val_loss
print("[%03d/%03d] %2.2f sec(s) Trainloss: %.6f |Valloss: %.6f" % \
(epoch, epochs, time.time() - start_time, plt_train_loss[-1], plt_val_loss[-1]))
plt.plot(plt_train_loss)
plt.plot(plt_val_loss)
plt.title("loss")
plt.legend(["train", "val"])
plt.show()
# 得出测试结果文件
def evaluate(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)
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("文件已保存到" + rel_path)
def mseLoss_with_reg(pred, target, model):
loss = nn.MSELoss(reduction='mean')
''' Calculate loss '''
regularization_loss = 0 # 正则项
for param in model.parameters():
# TODO: you may implement L1/L2 regularization here
# 使用L2正则项
# regularization_loss += torch.sum(abs(param))
regularization_loss += torch.sum(param ** 2) # 计算所有参数平方
return loss(pred, target) + 0.00075 * regularization_loss # 返回损失。
# 超参部分
device = "cuda" if torch.cuda.is_available() else "cpu" # 根据系统是否支持 CUDA,决定使用 GPU 还是 CPU 进行计算。
print(device)
config = {
"lr": 0.001,
"epochs": 20,
"momentum": 0.9,
"save_path": "model_save/best_model.pth",
"rel_path": "pred.csv",
}
model = MyModel(inDim=feature_dim).to(device)
# loss = nn.MSELoss()
loss = mseLoss_with_reg
optimizer = optim.SGD(model.parameters(), lr=config["lr"], momentum=config["momentum"])
train_val(model, train_loader, val_loader, device, config["epochs"], optimizer, loss, config["save_path"])
evaluate(config["save_path"], test_loader, device, config["rel_path"])