5.6.1 板鞋、凉鞋、靴子图像分类
在实际的深度学习项目中,我们可能想要尝试许多不同的神经网络模型,每一个又或许需要不同的训练代码,在这种情况下,我们最好给每一个模型独立写一个train函数和test函数。
如果你100%确定你的所有模型全都共享同样的训练和测试代码,那么你可以只完成一个train函数,一个eval函数和一个test函数,然后用函数的参数控制一些细节变量。
在这个图像分类任务中,由于所有的神经网络模型都是简单的分类器,它们一致输入图像,返回类别,因此我可以只写一套。
我们在TrainEvalTest目录下新建eval.py,test.py和train.py,也可以新建Utils.py用于存放一些很通用的函数,于是我们的项目目录已经进化到了最终形态:
最终的目录结构
我们先来完成train.py里的代码,但是要注意,此时虽然我们导入了eval.py里的eval函数,但是我们还没写这个函数。实际上,在完成train函数的时候,我假设eval已经完成并直接使用,后面再去真的完成eval。当然我一开始假设的用法可能不对,但是我完成eval之后可以回来再改。
# train.py
import torch
# 导入我们定义好的数据集
from Dataset.ShoeSandalBootDataset import ShoeSandalBootDataset
# 导入数据加载器
from torch.utils.data import DataLoader
# 进度条
from tqdm import tqdm
# tensorboard是用于实时监控训练过程的工具
from torch.utils.tensorboard import SummaryWriter
# datetime是Python的一个时间处理模块
from datetime import datetime
# os是Python的一个系统模块
import os
# 导入Eval.py中的eval函数
from TrainEvalTest.eval import eval
# train函数的参数还是要尽可能全面的,我这个例子还不算非常全面
# 总之,尽可能不要在函数内部出现硬编码的数值
def train(
model: torch.nn.Module,
batch_size: int,
learning_rate: float,
num_epochs: int,
lr_reduce_factor: float,
lr_reduce_patience: int,
title: str = ""
) -> torch.nn.Module:
# 初始化训练集
train_dataset = ShoeSandalBootDataset("./Dataset/dataset_cache.pth", "train")
# 初始化验证集
eval_dataset = ShoeSandalBootDataset("./Dataset/dataset_cache.pth", "eval")
# 使用DataLoader加载数据
# DataLoader是一个可迭代的对象,它的主要作用是将整个数据集分成若干个小的batch
# 每次加载一个batch的数据,并返回
# dataset参数是我们加载的数据集
# batch_size参数是每次加载的批次大小
# shuffle参数是是否打乱数据集中的数据顺序
# drop_last参数是是否丢弃最后一个batch,如果数据总数不能被batch_size整除,那么最后一个batch的数据量会小于batch_size
# collate_fn参数是一个函数,用来将一组数据打包成一个batch
train_loader = DataLoader(dataset=train_dataset,
batch_size=batch_size,
shuffle=True,
drop_last=False,
collate_fn=train_dataset.collate_fn)
eval_loader = DataLoader(dataset=eval_dataset,
batch_size=batch_size,
shuffle=False, # 为了保证验证的准确性,我们不打乱验证集
drop_last=False,
collate_fn=eval_dataset.collate_fn)
# 定义一个优化器,我们使用Adam优化器
# 第一个参数,model.parameters()返回模型中所有需要被优化的参数
# 第二个参数是学习率,即每次参数更新的步长
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
# 定义一个学习率调整器
# 想象你打高尔夫球,当你离球洞很远的时候,你需要用大力一点的击球力
# 当你离球洞很近的时候,你只需要用小力一点的击球力
# 又或者想象一个小球要滚动到一个坑的最低点,当它离最低点很远的时候,它的速度可以很快,这使它更快逼近坑底
# 但当它离坑底很近的时候,它的速度就要慢下来,这样才不会滚过头
# 学习率调整器的作用就是根据模型的训练情况,动态调整学习率
# 在合适的时候,将学习率变化到合适的大小
# 第一个参数是要调整的优化器
# ReduceLROnPlateau会根据某种指标调整学习率,而mode='min'表示当指标不再减小的时候,学习率要调整
# factor是调整因子,即每次学习率变为原来的多少
# patience是我们可以容忍多少次的指标不再减小,而不是指标不再减小时立即调整学习率
# verbose=True表示当学习率调整的时候,要打印一些信息
lr_scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=lr_reduce_factor,
patience=lr_reduce_patience, verbose=True)
# 对于分类问题,我们使用交叉熵损失函数
loss_func = torch.nn.CrossEntropyLoss()
# 获取模型的类的名字
model_name: str = model.__class__.__name__
# 构造日志目录,日志目录的构成是:Runs/模型名/时间_标题
now: str = datetime.now().strftime('%y%m%d_%H%M%S')
log_dir: str = os.path.join('Runs', model_name, now + ('_' + title if title else ''))
# 创建该目录
os.makedirs(log_dir, exist_ok=True)
# 创建一个tensorboard的SummaryWriter对象
# 这个对象可以将训练过程中的信息写入到硬盘中,我们可以通过tensorboard指令来查看这些信息
# tensorboard的用法请询问LLM或直接搜索查询
writer = SummaryWriter(log_dir=log_dir)
# 用于记录全局一共训练了多少次
global_step = 0
# 用于记录最好的损失
best_loss = float('inf')
# 开始训练
# 神经网络的训练要重复使用数据集多次,每遍历一次数据集称为一个epoch
# 就好像,你上学的时候也不是每道题只做一次
for epoch in range(1, num_epochs+1):
# 新建进度条,desc参数是进度条的描述
pbar = tqdm(train_loader, desc=f'Training Epoch {epoch}/{num_epochs}')
# 初始化训练损失
total_train_loss = 0.0
# 记录识别正确的数量,以便计算准确率
total_num_correct = 0.0
# 计算到目前为止的样本数量
total_num_samples = 0
# 开始训练
# 将模型设为训练模式(模型一开始默认就是训练模式)
model.train()
# 注意我们在ShoeSandalBootDataset中定义了collate_fn函数
# 返回一个字典,此时我们循环pbar这个进度条
# 而进度条又是基于train_loader的,因此这里的batch格式是我们collate_fn返回的格式
for batch in pbar:
# 我们需要将模型的梯度清零
# 不然PyTorch会累积梯度
optimizer.zero_grad()
# 前向推理
prediction = model(batch['input_data'])
# 计算损失
loss = loss_func(prediction, batch['labels'])
# 反向传播
loss.backward()
# 更新参数
optimizer.step()
# 更新损失和正确识别的数量
# 注意!!!这里使用.item()方法将tensor转换为Python的float,不然的话这个tensor会一直占用显存
total_train_loss += loss.item()
# 使用argmax方法找到预测的类别,argmax函数可以返回最大数值对应的索引
# 回想one-hot编码以及模型输出的得分,最高得分对应的索引就是预测的类别
pred_classes = prediction.argmax(dim=1)
total_num_correct += (pred_classes == batch['labels']).sum().item()
total_num_samples += len(batch['labels'])
# 更新进度条,让进度条显示当前的损失、准确率以及当前的学习率
pbar.set_postfix_str(f'Loss: {total_train_loss / total_num_samples:.4f}, '
f'Accuracy: {total_num_correct / total_num_samples:.4f}, '
f'LR: {optimizer.param_groups[0]["lr"]:.4e}')
global_step += 1
pbar.close()
# 将训练损失写入tensorboard
writer.add_scalar('Train Loss', total_train_loss / total_num_samples, global_step)
# 将训练准确率写入tensorboard
writer.add_scalar('Train Accuracy', total_num_correct / total_num_samples, global_step)
# 将学习率写入tensorboard
writer.add_scalar('Learning Rate', optimizer.param_groups[0]['lr'], global_step)
# 更新学习率
lr_scheduler.step(total_train_loss)
# 每个epoch结束后,我们都可以验证一下模型的效果
# 如果你的模型太大,也可以隔几个epoch跑一次验证
eval_loss, _ = eval(model, eval_loader, writer, loss_func, global_step)
if eval_loss < best_loss:
best_loss = eval_loss
# 保存最好的模型,model.state_dict()可以获取模型的参数
torch.save(model.state_dict(), os.path.join(log_dir, 'best.pth'))
# 保存最新的模型
torch.save(model.state_dict(), os.path.join(log_dir, 'last.pth'))
writer.close()
return model
随后,我们完成eval.py的代码。注意,这里我们又用了plotConfusionMatrix,这个用于画图的函数原来是写在eval.py里的,但是由于后面发现test.py里也要用,于是把它挪到了Utils.py中,这样它就可以公用了。
import torch
# 导入我们定义好的数据集
from Dataset.ShoeSandalBootDataset import ShoeSandalBootDataset
# 导入混淆矩阵
from sklearn.metrics import confusion_matrix
from TrainEvalTest.Utils import plotConfusionMatrix
def eval(model, eval_loader, writer, loss_func, global_step):
# 设置模型为验证模式
# 模型在验证模式时,不会保存用于计算梯度和参数更新的数值,因此速度更快
# 一些模块在验证模式和训练模式的行为也不同,例如Dropout层
model.eval()
pred_classes = []
true_classes = []
# 开始验证
for batch in eval_loader:
# 前向,这里我们不需要更新参数,因此使用torch.no_grad()禁止梯度计算
with torch.no_grad():
prediction = model(batch['input_data'])
pred_classes.append(prediction.argmax(dim=1).detach())
true_classes.append(batch['labels'])
# 合并所有的预测类别和真实类别
pred_classes = torch.cat(pred_classes)
true_classes = torch.cat(true_classes)
# 对于分类问题,可能loss,accuracy和混淆矩阵比较直观,所以我们输出这三个
# 计算总损失
loss = loss_func(prediction, batch['labels']).item() / len(eval_loader)
# 计算准确率
accuracy = (pred_classes == true_classes).sum().item() / len(true_classes)
# 计算混淆矩阵
confusion = confusion_matrix(true_classes.cpu().numpy(), pred_classes.cpu().numpy())
# 将图像写入tensorboard
writer.add_figure('Confusion Matrix', plotConfusionMatrix(confusion), global_step)
# 将损失写入tensorboard
writer.add_scalar('Eval Loss', loss, global_step)
# 将准确率写入tensorboard
writer.add_scalar('Eval Accuracy', accuracy, global_step)
# 打印验证结果
print(f'Eval Loss: {loss:.4f}, Eval Accuracy: {accuracy:.4f}')
# 把模型设置回训练模式
model.train()
return loss, accuracy
然后,test.py的逻辑和eval.py更像,只不过test.py可能会输出一些更具总结性的指标,以及绘图:
import torch
import numpy as np
import pandas as pd
from Dataset.ShoeSandalBootDataset import ShoeSandalBootDataset
from TrainEvalTest.Utils import plotConfusionMatrix
from torch.utils.data import DataLoader
from sklearn.metrics import confusion_matrix
import matplotlib.pyplot as plt
import os
def test(model, batch_size):
test_dataset = ShoeSandalBootDataset("./Dataset/dataset_cache.pth", "test")
test_loader = DataLoader(dataset=test_dataset,
batch_size=batch_size,
shuffle=False,
drop_last=False,
collate_fn=test_dataset.collate_fn)
# 设置模型为验证模式
model.eval()
pred_classes = []
true_classes = []
# 开始验证
for batch in test_loader:
# 前向
with torch.no_grad():
prediction = model(batch['input_data'])
pred_classes.append(prediction.argmax(dim=1).detach().cpu())
true_classes.append(batch['labels'])
# 合并所有的预测类别和真实类别
pred_classes = torch.cat(pred_classes)
true_classes = torch.cat(true_classes)
# 计算总损失
loss = torch.nn.functional.cross_entropy(prediction, batch['labels']).item() / len(true_classes)
# 计算准确率
accuracy = (pred_classes == true_classes).sum().item() / len(true_classes)
# 计算混淆矩阵
confusion = confusion_matrix(true_classes.cpu().numpy(), pred_classes.cpu().numpy())
print(f'Test Loss: {loss:.4f}, Test Accuracy: {accuracy:.4f}')
plotConfusionMatrix(confusion)
# 记得最好要保存一下测试结果的细节
model_name = model.__class__.__name__
test_result_path = os.path.join("Reports", f"{model_name}_test_result.csv")
if not os.path.exists("Reports"):
os.makedirs("Reports")
plt.savefig(f"Reports/confusion_matrix_{model_name}.png")
# 保存测试结果
# 当你以后需要分析不同模型的效果,或者进行其他对比和可视化时
# 直接拿到测试结果的csv文件会非常方便
test_result = pd.DataFrame({"Predicted": pred_classes.cpu().numpy(), "True": true_classes.cpu().numpy()})
test_result.to_csv(test_result_path, index=False)
最后就是我们上面提到的Utils.py,我这里只完成了绘制混淆矩阵的可视化代码,实际上这里有更多可写的东西:
from Dataset.ShoeSandalBootDataset import ShoeSandalBootDataset
# 我们可能需要绘制混淆矩阵的图像
import matplotlib.pyplot as plt
def plotConfusionMatrix(confusion):
# 渲染混淆矩阵的图像
fig = plt.figure()
plt.imshow(confusion, cmap='cool', interpolation='nearest')
plt.colorbar()
# 把x标签写在上面
plt.xticks([0, 1, 2], [ShoeSandalBootDataset.classIndexToLabel(i) for i in range(3)]) # 0, 1, 2分别对应Shoe, Sandal, Boot
# 把y标签写在右边
plt.yticks([0, 1, 2], [ShoeSandalBootDataset.classIndexToLabel(i) for i in range(3)]) # 0, 1, 2分别对应Shoe, Sandal, Boot
plt.xlabel('Predicted')
plt.ylabel('True')
for i in range(3):
for j in range(3):
plt.text(j, i, confusion[i, j], ha='center', va='center', color='black')
# 画格子,格线要偏移0.5
for i in range(3):
plt.axhline(i - 0.5, color='black')
plt.axvline(i - 0.5, color='black')
plt.title('Confusion Matrix')
plt.tight_layout()
return fig
最后,在main.py中,我们可以进行各项运行参数的设置,设置训练与测试程序的计划执行,等等:
import torch
from Dataset.ShoeSandalBootDataset import DEVICE
from Model.MyResnet import ResNet as MyResnet
from Model.OfficialNetworks import resnet18, resnet34, mobilenet_v2, alexnet
from TrainEvalTest.train import train
from TrainEvalTest.test import test
# 这里我初始化了一个默认的参数字典,你可以根据需要修改这个字典
# 很多项目中,我们会把参数写在一个配置文件中,然后在这里读取配置文件
# 又或者使用argparse库,从命令行中读取参数
# 但我想,既然你用PyTorch,就都会Python,那为何不省去这些麻烦,直接在Python代码中写参数呢?
# 因此,这个main.py实际上扮演一个配置文件 + 运行规划的角色
# 你可以在这里配置参数,后面你也可以写一些循环逻辑,来实现自动化的训练、验证、测试
default_params = {
"batch_size": 32,
"learning_rate": 1e-3,
"num_epochs": 40,
"lr_reduce_factor": 0.5,
"lr_reduce_patience": 10,
"title": ""
}
if __name__ == "__main__":
# 我们可以在这里初始化模型,并规划一写运行逻辑
model = MyResnet().to(DEVICE)
# 复制字典,并修改title参数
my_resnet_params = default_params.copy()
my_resnet_params["title"] = "MyResnet"
print("Training MyResnet")
model = train(model, **my_resnet_params)
test(model, batch_size=default_params["batch_size"])
# 循环测试其他模型
for other_model in [resnet18, resnet34, mobilenet_v2, alexnet]:
print(f"Training {other_model.__class__.__name__}")
model = other_model(num_classes=3).to(DEVICE)
# 复制字典,并修改title参数
other_model_params = default_params.copy()
other_model_params["title"] = other_model.__class__.__name__
train(model, **other_model_params)
test(model, batch_size=default_params["batch_size"])
# 我们也可以直接加载模型,然后进行测试
model = MyResnet().to(DEVICE)
model.load_state_dict(torch.load("Runs/ResNet/250318_003539_MyResnet/last.pth"))
test(model, batch_size=default_params["batch_size"])
# 我们当然也可以写一些超参数搜索的逻辑
for batch_size in [16, 32, 64]:
for learning_rate in [1e-3, 5e-4, 1e-4]:
model = MyResnet().to(DEVICE)
# 复制字典,并修改batch_size和learning_rate参数
search_params = default_params.copy()
search_params["batch_size"] = batch_size
search_params["learning_rate"] = learning_rate
search_params["title"] = f"MyRessnet_bs{batch_size}_lr{learning_rate}"
model = train(model, **search_params)
test(model, batch_size=default_params["batch_size"])
5.6.2 GPS轨迹时长预测
本项目拥有和上面的项目类似的项目结构,但要注意,下图展示的是训练和测试代码已经运行过之后的目录结构。有Runs文件夹负责保存运行日志以及存储训练好的模型文件,以及Reports文件夹复杂保存测试结果。
我们首先完成TrainEvalTest/MyTrainer这个文件夹里的内容,这里我们可以基本上效仿上一个项目完成train.py以及eval.py:
# train.py
import torch
from Dataset.TrajectoryDataset import TrajectoryDataset
from torch.utils.data import DataLoader
from tqdm import tqdm
from torch.utils.tensorboard import SummaryWriter
from datetime import datetime
import os
from TrainEvalTest.MyTrainer.eval import eval
def train(
model: torch.nn.Module,
batch_size: int,
learning_rate: float,
num_epochs: int,
lr_reduce_factor: float,
lr_reduce_patience: int,
title: str = ""
) -> torch.nn.Module:
# --- 初始化数据集和数据加载器 ---
train_dataset = TrajectoryDataset("./Dataset/TrajectoryDataset.pth", "train")
eval_dataset = TrajectoryDataset("./Dataset/TrajectoryDataset.pth", "eval")
train_loader = DataLoader(dataset=train_dataset,
batch_size=batch_size,
shuffle=True,
drop_last=False,
collate_fn=train_dataset.collate_fn)
eval_loader = DataLoader(dataset=eval_dataset,
batch_size=batch_size,
shuffle=False, # 为了保证验证的准确性,我们不打乱验证集
drop_last=False,
collate_fn=eval_dataset.collate_fn)
# --- 初始化优化器、学习率调度器和损失函数 ---
optimizer = torch.optim.AdamW(model.parameters(), lr=learning_rate, weight_decay=1e-2)
lr_scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=lr_reduce_factor,
patience=lr_reduce_patience, verbose=True)
# 对于回归问题,我们可以使用MSE或MAE(L1Loss)作为损失函数
# 经过实验,发现L1Loss的效果更好
loss_func = torch.nn.L1Loss()
# --- 初始化日志系统 ---
model_name: str = model.__class__.__name__
now: str = datetime.now().strftime('%y%m%d_%H%M%S')
log_dir: str = os.path.join('Runs', model_name, now + ('_' + title if title else ''))
os.makedirs(log_dir, exist_ok=True)
writer = SummaryWriter(log_dir=log_dir)
# --- 全局记录 ---
global_step = 0
best_loss = float('inf')
# 开始训练
# 神经网络的训练要重复使用数据集多次,每遍历一次数据集称为一个epoch
for epoch in range(1, num_epochs+1):
# 新建进度条,desc参数是进度条的描述
pbar = tqdm(train_loader, desc=f'Training Epoch {epoch}/{num_epochs}')
# 初始化训练损失
total_train_loss = 0.0
# 开始训练
model.train()
# 注意我们在ShoeSandalBootDataset中定义了collate_fn函数
# 返回一个字典,此时我们循环pbar这个进度条
# 而进度条又是基于train_loader的,因此这里的batch格式是我们collate_fn返回的格式
for bi, batch in enumerate(pbar, 1):
# 我们需要将模型的梯度清零(这一步是必须的,无脑写上就对了)
optimizer.zero_grad()
# 前向
prediction = model(batch['traj'])
# 计算损失
loss = loss_func(prediction, batch['duration'])
# 反向传播
loss.backward()
# 梯度裁剪,防止梯度爆炸
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
# 更新参数
optimizer.step()
# 更新损失和的数量
total_train_loss += loss.item()
# 更新进度条,让进度条显示当前的损失以及当前的学习率
pbar.set_postfix_str(f'Loss: {total_train_loss / bi:.4f}, '
f'LR: {optimizer.param_groups[0]["lr"]:.4e}')
global_step += 1
pbar.close()
# 将训练损失写入tensorboard
writer.add_scalar('Train Loss', total_train_loss / len(train_loader), global_step)
# 将学习率写入tensorboard
writer.add_scalar('Learning Rate', optimizer.param_groups[0]['lr'], global_step)
# 更新学习率
lr_scheduler.step(total_train_loss)
# 每个epoch结束后,我们都要验证一下模型的效果
eval_loss = eval(model, eval_loader, writer, global_step)
if eval_loss < best_loss:
best_loss = eval_loss
# 保存最好的模型,model.state_dict()可以获取模型的参数
torch.save(model.state_dict(), os.path.join(log_dir, 'best.pth'))
# 保存最新的模型
torch.save(model.state_dict(), os.path.join(log_dir, 'last.pth'))
writer.close()
return model
然后是eval.py,需要注意的是,由于这次我们执行的是回归任务,所以就使用MSE,MAE以及RMSE这三个指标,而不适用准确率和混淆矩阵:
import torch
def eval(model, eval_loader, writer, global_step):
# 设置模型为验证模式
model.eval()
pred_durations = []
true_durations = []
# 开始验证
for batch in eval_loader:
# 前向,这里我们不需要更新参数,因此使用torch.no_grad()禁止梯度计算
with torch.no_grad():
prediction = model(batch['traj'])
pred_durations.append(prediction.detach())
true_durations.append(batch['duration'])
# 合并所有的预测值和真实值
pred_durations = torch.cat(pred_durations)
true_durations = torch.cat(true_durations)
# 计算总损失
mse = torch.nn.functional.mse_loss(prediction, batch['duration']).item()
mae = torch.nn.functional.l1_loss(prediction, batch['duration']).item()
rmse = mse ** 0.5
# 将损失写入tensorboard
writer.add_scalar('Eval/MSE', mse, global_step)
writer.add_scalar('Eval/MAE', mae, global_step)
writer.add_scalar('Eval/RMSE', rmse, global_step)
# 打印验证结果
print(f'Eval MSE: {mse:.4f}, MAE: {mae:.4f}, RMSE: {rmse:.4f}')
# 把模型设置回训练模式
model.train()
return mse
同时,我们也新建HuggingFaceTrainer/train.py。一些深度学习库,如pytorch lightening以及HuggingFace的trasnformer库,提供了整套的训练流程,它们大多数提供许许多多的接口,你只需要配置好所有参数和变量,即可套用它们的训练流程。我这里展示了HuggingFace的trasnformer库提供的训练器。
注意,此处是非必要知识,这些现成的训练流程大多数不是很灵活,而且过于庞大冗余,不够轻量化和定制化。你要改点什么自定义的东西也很难改,到头来可能还得钻进它里面的代码去重写部分内容。因此,下面的代码注释很少,尤其是调用transformers库的Trainer时指定了一大堆的参数,你可以访问transformers库的官方文档去找每个参数的意义。但实际上,此处我建议,你只是知道以下代码提供了另一种训练流程即可,可以跳过这部分细节。
import torch
from Dataset.TrajectoryDataset import TrajectoryDataset
from transformers import Trainer, TrainingArguments
from datetime import datetime
import os
import numpy as np
class MyTrainer(Trainer):
def compute_loss(self, model, inputs, return_outputs=False):
outputs = model(inputs["traj"])
loss = torch.nn.functional.mse_loss(outputs, inputs["duration"])
return (loss, {"duration": outputs}) if return_outputs else loss
def training_step(self, model, inputs, num_items_in_batch=None):
model.train()
inputs = self._prepare_inputs(inputs)
with self.autocast_smart_context_manager():
loss = self.compute_loss(model, inputs)
loss.backward()
return loss.detach()
def compute_metrics(eval_pred):
predictions, labels = eval_pred
mse = ((predictions - labels)** 2).mean()
mae = np.abs((predictions - labels)).mean()
rmse = np.sqrt(mse)
return {"MSE": mse, "MAE": mae, "RMSE": rmse}
def train(
model: torch.nn.Module,
batch_size: int,
learning_rate: float,
num_epochs: int,
lr_reduce_factor: float,
lr_reduce_patience: int,
title: str = ""
) -> torch.nn.Module:
# --- 初始化数据集和数据加载器 ---
train_dataset = TrajectoryDataset("./Dataset/TrajectoryDataset.pth", "train")
eval_dataset = TrajectoryDataset("./Dataset/TrajectoryDataset.pth", "eval")
# --- 初始化日志系统 ---
model_name: str = model.__class__.__name__
now: str = datetime.now().strftime('%y%m%d_%H%M%S')
log_dir: str = os.path.join('Runs', model_name, now + ('_' + title if title else ''))
os.makedirs(log_dir, exist_ok=True)
trainer = MyTrainer(
model=model,
args=TrainingArguments(
output_dir=log_dir,
do_train=True,
do_eval=True,
eval_strategy='epoch',
per_device_train_batch_size=batch_size,
per_device_eval_batch_size=batch_size,
learning_rate=learning_rate,
num_train_epochs=num_epochs,
lr_scheduler_type='reduce_lr_on_plateau',
lr_scheduler_kwargs={
'factor': lr_reduce_factor,
'patience': lr_reduce_patience
},
logging_dir=log_dir,
logging_strategy='epoch',
save_strategy='epoch',
save_only_model=True,
dataloader_drop_last=False,
run_name=title,
label_names=['duration'],
optim="adamw_torch",
report_to=['tensorboard'],
dataloader_pin_memory=False
),
data_collator=TrajectoryDataset.collate_fn,
train_dataset=train_dataset,
eval_dataset=eval_dataset,
compute_metrics=compute_metrics
)
trainer.train()
return model
测试用的代码test.py则是通用的,不论是我自己写的训练流程,还是其他深度学习库提供的训练流程,最终都会把训练好的深度学习模型(参数)保存下来。我们只需要加载保存的模型,然后运行test函数:
import torch
import numpy as np
import pandas as pd
from Dataset.TrajectoryDataset import TrajectoryDataset
from torch.utils.data import DataLoader
import os
def test(model, batch_size):
test_dataset = TrajectoryDataset("./Dataset/TrajectoryDataset.pth", "test")
test_loader = DataLoader(dataset=test_dataset,
batch_size=batch_size,
shuffle=False,
drop_last=False,
collate_fn=test_dataset.collate_fn)
# 设置模型为验证模式
model.eval()
pred_durations = []
true_durations = []
# 开始验证
for batch in test_loader:
# 前向
with torch.no_grad():
prediction = model(batch['traj'])
pred_durations.append(prediction.detach().cpu())
true_durations.append(batch['duration'])
# 合并所有的预测类别和真实类别
pred_durations = torch.cat(pred_durations)
true_durations = torch.cat(true_durations)
# 计算总损失
mse = torch.nn.functional.mse_loss(prediction, batch['duration']).item()
mae = torch.nn.functional.l1_loss(prediction, batch['duration']).item()
rmse = mse ** 0.5
print(f'Test MSE: {mse:.4f}, MAE: {mae:.4f}, RMSE: {rmse:.4f}')
# 记得最好要保存一下测试结果的细节
model_name = model.__class__.__name__
test_result_path = os.path.join("Reports", f"{model_name}_test_result.csv")
if not os.path.exists("Reports"):
os.makedirs("Reports")
# 保存测试结果
# 当你以后需要分析不同模型的效果,或者进行其他对比和可视化时
# 直接拿到测试结果的csv文件会非常方便
test_result = pd.DataFrame({"Predicted": pred_durations.cpu().numpy(), "True": true_durations.cpu().numpy()})
test_result.to_csv(test_result_path, index=False)
随后,在main.py里,我们可以对比我们自己实现的训练流程与HuggingFace的transformers库提供的训练流程:
import torch
from Dataset.TrajectoryDataset import Device
from Model.ResNet1D import ResNet1D as MyResnet
from Model.Transformer import MyTransformer
from Model.MLP import MLP
from TrainEvalTest.MyTrainer.train import train as my_train
from TrainEvalTest.test import test
from TrainEvalTest.HuggingFaceTrainer.train import train as hf_train
default_params = {
"batch_size": 64,
"learning_rate": 4e-4,
"num_epochs": 150,
"lr_reduce_factor": 0.5,
"lr_reduce_patience": 10,
"title": ""
}
if __name__ == "__main__":
# 我们可以在这里初始化模型,并规划一写运行逻辑
model = MyResnet(depth_multiplier=2, width_multiplier=2).to(Device)
# model = MyTransformer(d_model=128, nhead=4, n_layers=4, dim_feedforward=512).to(Device)
# 复制字典,并修改title参数
train_params = default_params.copy()
train_params["title"] = "MyTrainer"
print("Training With My Trainer")
model = my_train(model, **train_params)
test(model, batch_size=default_params["batch_size"])
# 复制字典,并修改title参数
train_params = default_params.copy()
train_params["title"] = "HuggingfaceTrainer"
print("Training With Huggingface Trainer")
model = hf_train(model, **train_params)
test(model, batch_size=default_params["batch_size"])
至此,两个深度学习项目的代码就已经完成了。注:经过我初步的训练,第二个项目稍微难一些,损失曲线很是波折,我认为最好的情况下能将测试集的预测误差缩小到10分钟左右。
第六章 总结
关于总结,其实你可以重新返回第一章,因为第一章就是最好的,对深度学习项目整体流程的总结。
在本教程之前,或许很多人对深度学习一无所知,希望本教程可以让大家稍微了解深度学习的整体思路,我认为足以让你迈出第一步(比如新建文件夹)。
你应当拿着你的项目,结合你的数据和实际情况,照着这个教程一步一步来,从数据处理,到神经网络搭建,最后到训练代码和训练。期间你必然会遇到很多问题,因为本教程说得太不细致了,但你可以随时上网搜索你的问题,或是询问LLM。相比于无法开始你的项目,光是遇到问题这点就已经是很大的进步。如果你想对深度学习有更加深入的了解 (也就是说你想深度学习深度学习(∩_∩)),那么本篇教程是完全不够的。
深度学习中有很多的细节需要了解,但除了阅读,很重要的还是自己动手去写,一定不能只看不动手。人脑就像神经网络一样,每次写出BUG对我们来说都是一个Loss,根据这个Loss我们才能更新大脑的参数,使得下次不再写出一样的BUG。如果不动手的话,连犯错的机会都不会有,更不会有Loss,也不会有优化,那么何谈学习呢?而当我们像神经网络一样犯了太多的错,收到了太多的Loss之后,可能正是学会知识的最终时刻。
所以,学习的本质可能就是实践和犯错吧。
What I cannot create, I do not understand.