李哥深度学习班 回归实战下
模型部分补充
def forward(self, x):
x = self.fc1(x) # fully connected 1 全连接
x = self.relu1(x)
x = self.fc2(x)
if len(x.size()) > 1:
x = x.squeeze(1)
return x
由于x是二维的张量,其在经过神经网络的计算后,所得到的预测值也会是一个二维的张量。
已知batchsize=16,dataloader每次会取出16个样本,每个样本具有93个特征,因而可以认为输入的x是一个16×93的二维张量,此时x.size()返回 [ 16, 93 ]
第一层全连接层是93×128的,即对应于样本的93个特征,将它们分别送入128个神经元,每个神经元有对应的93个参数w和一个偏置b,可以认为是将16×93矩阵与93×128的矩阵相乘,得到了16×128的新矩阵,此时x.size()返回 [ 16, 128 ]
第二层全连接层同上,该层是输出层,最后会得到一个16×1的二维张量,x.size()返回 [ 16, 1 ]
可见,此时得到的预测值仍然是一个二维张量,形如[ [ 1 ], [ 2 ], [ 3 ], ......]
但是,dataloader中取出的y却是一个一维张量(向量),形如[ 1, 2, 3, 4, ........],二者有本质上的区别
从前面的简单回归中已经知道,在计算loss时必须传入y_pred和y进行计算,那么这二者的维度就必须相同,二维张量和一维张量当然是不能一同运算的。
于是在前向传播的最后,要加上一句把输出的维度压缩成一维的代码。
pytorch描述张量形状的方式是通过一个一维数组来描述的,数组的长度就是张量维度的数量,数组中第 i 个元素代表这个维度的大小,也就是在第 i 维上 有几项数据
.size()方法就可以返回这个数组。如[ 16 ]代表这个张量是一个一维张量,含有16项数据,即一个 16维向量 (线代中对向量的定义);[ 16, 93 ]就代表这是一个二维张量,即一个16×93的矩阵,在本项目中我认为它反映了一组数据中含有16个样本,每个样本具有93个特征
于是可以知道,len(x.size())说的就是张量的维度,当维度超过1,就将其压缩
.squeeze()方法可以移除所有大小为1的维度,若传入了参数,则可以只移除指定维度(如果大小为1)
所以在这一步之后,形如[ [ 1 ], [ 2 ], [ 3 ], ......]的二维张量y就可以变成形如[ 1, 2, 3, 4, ........]的一维张量,就可以参与loss的计算了
优化器:
使用随机梯度下降进行参数优化,动量系数设置为通常值0.9
optimizer = optim.SGD(params=model.parameters(), lr=lr, momentum=0.9)
# 超参数:momentum,动量(实际上是动量系数,控制历史梯度信息的衰减速度)
momentum动量,可以用于优化梯度下降算法,原理是模拟真实世界中的下滑动作,使小球具有动量,使其具备冲过局部最低点的可能性,同时也使得小球不会在最低点附近抖动,可以加速收敛。
Momentum 的更新过程
以下是一个具体的更新过程示例:
- 初始化参数 和动量项 。
- 对于每次迭代t:
- 计算当前梯度 。
- 更新动量项:

更新参数:

重复上述步骤,直到收敛。
到这里为止,一个神经网络项目所应包含的四个部分中的前三个已经解决(Data、Model、HyperPara),最后还要进行训练流程的实现。
训练流程
函数定义和初始化
def train_val(model, train_loader, val_loader, lr, optimizer, device, epochs, save_path):
model = model.to(device) # 将模型移到指定设备(GPU/CPU)
start_time = time.time()
plt_train_loss = [] # 存储每个epoch的平均训练loss
plt_val_loss = [] # 存储每个epoch的平均验证loss
min_val_loss = 9999999.9 # 初始化最小验证loss
.to(device)方法
可以把数据或模型移动到指定的设备,对于模型来说也就是将模型的参数、缓冲区等移动到设备
CPU内存 <-------> GPU内存
↑ ↑
CPU计算 GPU计算(并行)
# 数据必须先在GPU内存中,GPU才能计算
# 模型参数必须在GPU内存中,GPU才能使用它们
外层Epoch循环+内层Batch训练+计算平均训练损失
for epoch in range(epochs): # 遍历所有epoch
model.train() # 将模型设置为训练模式
train_loss = 0.0 # 重置当前epoch的训练loss
for x, y in train_loader: # 遍历所有batch,每次取一个batchsize(16)个样本
x, y = x.to(device), y.to(device) # 数据移到设备
y_pred = model(x) # 前向传播
bat_loss = loss(y_pred, y) # 计算损失
bat_loss.backward() # 反向传播,计算梯度
optimizer.step() # 更新模型参数
optimizer.zero_grad() # 清空梯度
train_loss += bat_loss.cpu().item() # 累加batch loss
plt_train_loss.append(train_loss / train_loader.__len__())
# train_loader.__len__() 返回的是批次数(iterations),不是batch_size
# 也就是训练时一个epoch的迭代次数 = 总样本数 / batch_size
optimizer.step()
由于是基于梯度下降算法,因此:
# optimizer.step() 内部实际上执行类似这样的操作:
with torch.no_grad(): # 禁用梯度计算
for param in model.parameters():
if param.grad is not None:
# 更新参数:param = param - lr * param.grad
param -= learning_rate * param.grad
基本上跟前面那个简单回归代码做的是一样的事情
plt_train_loss.append(train_loss / train_loader.__len__())
计算当前这一个epoch的平均训练损失,并将其添加到列表中以作记录
验证阶段
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_y_pred = model(val_x)
val_bat_loss = loss(val_y_pred, val_y)
val_loss += val_bat_loss.cpu().item()
plt_val_loss.append(val_loss / val_loader.__len__())
torch.no_grad() 核心功能:禁用梯度计算
# no_grad模式:不记录计算图
x2 = torch.tensor([1.0], requires_grad=True)
with torch.no_grad():
y2 = x2 * 2 # 这个计算不会被跟踪
z2 = y2.mean()
z2.backward() # 这里会报错!
no_grad模式下,计算图不会被记录,因而无法通过.backward()反向传播计算梯度,会报错
使用with语句可以方便的划定哪一片的代码运行在no_grad模式下,就像打开文件那样自动调用__enter__和自动退出
验证阶段:只需要前向传播计算损失/准确率,不需要反向传播计算梯度,不需要更新模型参数
若不调用,则可能导致:1、内存泄漏;2、计算减慢;3、可能意外修改梯度
模型保存
if val_loss < min_val_loss:
min_val_loss = val_loss
torch.save(model, save_path) # 保存最佳模型
取val_loss最小的一次保存模型,此时的模型就是训练中得到的最佳模型,用于后续的提交测试
进度输出+可视化损失曲线
print("[%03d/%03d] %2.2f sec(s) train_loss: %.6f val_loss: %.6f " % \
(epoch+1, 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模型,对测试数据进行预测,并将结果保存为CSV文件。
模型加载与初始化
def evaluate(model_path, test_loader, rel_path, device):
model = torch.load(model_path, weights_only=False).to(device)
rel = [] # rel = result
model.eval()
torch.load()
作用:从指定路径加载已保存的PyTorch模型
model_path:模型文件的存储路径
weights_only=False:加载整个模型(包括架构和参数),而不是仅加载权重
.to(device):将模型移动到指定设备(CPU或GPU)
model.eval()
作用:将模型切换到评估模式,这会关闭Dropout、BatchNorm等训练时的特殊层,确保推理结果稳定
具体来说:
1、禁用训练特有的随机行为:
Dropout层:不丢弃神经元,所有神经元都参与计算
BatchNorm层:使用训练时累积的运行统计量(均值和方差),而不是当前批次的统计量
其他层:可能有的层也会改变行为
2、通常与torch.no_grad()一起使用:
model.eval():改变模型内部层的行为
torch.no_grad():不计算梯度,节省内存
与之相对的,model.train()就是将模型切换到训练模式,并开启一些在训练时会用到的功能或者随机行为
对于没有Dropout/BatchNorm的模型,即使模型没有这些层,也应该使用model.eval(),这是因为考虑到:
1、保持代码一致性;2、某些自定义层可能也有不同行为;3、未来添加这些层时无需修改代码
批量推理循环
with torch.no_grad():
for x in test_loader:
x = x.to(device) # 将数据移动到设备
pred = model(x) # 前向传播得到预测
rel.append(pred.cpu().item()) # 将结果移回CPU并添加到列表
结果保存到CSV文件
with open(rel_path, "w", newline="") as f:
csv_writer = csv.writer(f) # 创建CSV写入器
csv_writer.writerow(["id", "tested_positive"]) # 写入表头
for i, pred in enumerate(rel): # 遍历所有结果
csv_writer.writerow([str(i), str(pred)]) # 写入每一行
print("结果保存到了"+rel_path) # 输出保存成功信息
-
with open(...):打开文件,确保文件正确关闭
-
csv_writer = csv.writer(f):创建CSV写入对象
-
writerow(["id", "tested_positive"]):写入列标题
-
enumerate(rel):同时获取索引 i 和预测值 pred
-
writerow([str(i), str(pred)]):将索引和预测值写入一行
-
print(...):提示用户结果保存位置
更多推荐
所有评论(0)