代码和语料库下载:
通过网盘分享的文件:SG10.rar
链接: https://pan.baidu.com/s/1MxCrpwoBYQiv1TDfWFdJKg 提取码: tyv9
整体架构与作用目的
该代码是一个用于事故检测的深度学习模型训练与评估框架。通过处理视频帧序列(将视频拆分为图片帧),训练模型识别视频中是否发生事故,同时可能预测事故发生的时间(TOA,Time of Accident),属于视频时序分类与事件检测任务。
整体架构
-
数据处理层:
- 从配置文件读取参数(网络结构、路径、超参数等)。
- 加载训练 / 测试数据(视频帧序列),通过自定义采样器(
MySampler
)和数据集(MyDataset
)处理时序数据。 - 对图像进行预处理( resize、归一化等)。
-
模型层:
- 核心模型为
Accident_Model
,支持不同的特征提取器(如 ResNet50、VGG16)和网络结构(可能包含 GRU 等时序模型)。 - 支持多种损失函数(通过
loss_type
配置),用于训练事故检测任务。
- 核心模型为
-
训练与评估层:
- 训练过程:使用 Adam 优化器,结合学习率调度器(
ReduceLROnPlateau
),通过 TensorBoard 记录训练日志。 - 评估过程:计算准确率(AP)、平均事故时间(mTTA)等指标,通过
evaluation_P_R80
实现。 - 模型保存:保存每轮训练的模型和最优模型(基于验证损失)。
- 训练过程:使用 Adam 优化器,结合学习率调度器(
一、导入pytorch核心库和自定义模块
# 导入必要的库
import torch # PyTorch核心库
import torch.nn as nn # 神经网络模块
from torch.utils.data import DataLoader # 数据加载工具
import torchvision.transforms as transforms # 图像预处理工具
#自定义模块
from src.model import Accident_Model # 自定义事故检测模型
from src.vid_dataloader import MyDataset, MySampler # 自定义数据集和采样器
from tqdm import tqdm # 进度条工具
import os # 文件系统操作
from tensorboardX import SummaryWriter # 训练日志可视化
import glob # 文件路径匹配
import numpy as np # 数值计算库
from src.eval_tools import evaluation_P_R80, print_results, vis_results # 评估工具函数
# import argparse # 命令行参数解析(未使用,改用配置文件)
import yaml # YAML配置文件解析
from natsort import natsorted # 自然排序(用于按顺序读取视频帧)
# 从配置文件加载参数
# ===========================================
with open('config.yml', 'r') as yamlfile: # 打开YAML配置文件
data = yaml.load(yamlfile, Loader=yaml.FullLoader) # 解析配置文件(使用安全加载器)
# 提取配置参数
cfg = data['NETWORK'] # 网络相关配置(如层数、学习率等)
directory = data['DIRECTORY'] # 路径配置(数据、模型、日志路径)
# 网络超参数(从配置中读取)
num_classes = cfg['num_cls'] # 类别数量(如0=无事故,1=有事故)
learning_rate = cfg['lr'] # 学习率
batch_size = cfg['batch_size'] # 批次大小
h_dim = cfg['h_dim'] # 隐藏层维度
z_dim = cfg['z_dim'] # 潜在空间维度(用于特征压缩)
n_layers = cfg['n_layers'] # 网络层数(如GRU的层数)
num_epochs = cfg['epoch'] # 训练轮数
input_dim = cfg['input_dim'] # 输入图像尺寸(如[224,224])
n_mean = cfg['n_mean'] # 图像归一化均值(RGB三通道)
n_std = cfg['n_std'] # 图像归一化标准差(RGB三通道)
loss_type = cfg['loss_type'] # 损失函数类型
network_type = cfg['network_type'] # 网络结构类型(如GRU、CNN+GRU)
extractor = cfg['extractor'] # 特征提取器(如resnet50、vgg16)
fps = cfg['fps'] # 视频帧率(用于时间相关指标计算)
gpu_id = cfg['gpu_id'] # GPU设备ID
dropout = cfg['dropout'] # Dropout概率(防止过拟合)
# 路径参数(从配置中读取)
train_data_path = directory['train_dir'] # 训练数据路径
test_data_path = directory['test_dir'] # 测试数据路径
model_dir = directory['model_dir'] # 模型保存路径
logs_dir = directory['logs_dir'] # 日志保存路径
# 设备设置(GPU优先)
device = ("cuda" if torch.cuda.is_available() else "cpu") # 自动选择设备(GPU/CPU)
# os.environ['CUDA_VISIBLE_DEVICES'] = gpu_id # 手动指定GPU(多GPU时启用)
二、数据处理模块(数据加载与预处理)
# 图像预处理 pipeline
transform = transforms.Compose(
[
transforms.Resize((input_dim[0], input_dim[1])), # 调整图像尺寸到input_dim (高度, 宽度)
transforms.ToTensor(), # 转换为Tensor(HWC→CHW,像素值归一化到0-1)
transforms.Normalize((n_mean[0], n_mean[1], n_mean[2]), # 各通道均值
(n_std[0], n_std[1], n_std[2])), # 各通道标准差
]
)
# --------------训练数据加载----------------------------------------
# 获取训练数据的类别文件夹路径
train_class_paths = [d.path for d in os.scandir(train_data_path) if d.is_dir] # 遍历目录,收集子文件夹路径
# 收集训练数据的图像路径和序列信息
train_class_image_paths = [] # 存储(图像路径,类别ID)的列表
train_end_idx = [] # 存储每个视频序列的长度(用于分割不同视频)
for c, class_path in enumerate(train_class_paths): # 遍历每个类别文件夹
for d in os.scandir(class_path): # 遍历类别下的视频文件夹(每个视频对应一个子文件夹)
if d.is_dir: # 确认是文件夹(视频帧文件夹)
paths = natsorted(glob.glob(os.path.join(d.path, '*.jpg'))) # 按自然顺序读取该视频的所有jpg帧
paths = [(p, c) for p in paths] # 为每个帧路径添加类别ID(c)
train_class_image_paths.extend(paths) # 合并到总列表
train_end_idx.extend([len(paths)]) # 记录当前视频的帧数量
# 计算训练数据的序列索引(用于采样器分割视频)
train_end_idx = [0, *train_end_idx] # 在开头添加0作为累加起点
# 在列表 train_end_idx 的开头插入 0,作为累加的起始点。
# 假设原始 train_end_idx 是每个视频的帧数列表(例如 [100, 150] 表示第一个视频 100 帧,第二个视频 150 帧),插入 0 后变为 [0, 100, 150]。
train_end_idx = torch.cumsum(torch.tensor(train_end_idx), 0) # 累加得到每个视频的结束索引(如[0,100,250])
# 计算 累加和(cumulative sum),将帧数转换为每个视频的 结束索引。
# 例如 [0, 100, 150] 经过累加后变为 [0, 100, 250],表示:
seq_length = 99 # 每个样本的序列长度(每次取99帧)
print('train_end_idx', len(train_end_idx)) # 打印训练视频数量+1(因开头加了0)
print('seq_length', seq_length) # 打印序列长度
train_sampler = MySampler(train_end_idx, seq_length) # 自定义采样器,用于抽取固定长度的序列
##-------------测试数据加载-------------------------------
# 与训练数据加载逻辑相同
test_class_paths = [d.path for d in os.scandir(test_data_path) if d.is_dir] # 测试类别文件夹路径
test_class_image_paths = [] # 测试图像路径列表
test_end_idx = [] # 测试视频序列长度列表
for c, class_path in enumerate(test_class_paths): # 遍历测试类别文件夹
for d in os.scandir(class_path): # 遍历视频文件夹
if d.is_dir: # 确认是视频帧文件夹
paths = natsorted(glob.glob(os.path.join(d.path, '*.jpg'))) # 按顺序读取帧路径
paths = [(p, c) for p in paths] # 添加类别ID
test_class_image_paths.extend(paths) # 合并路径
test_end_idx.extend([len(paths)]) # 记录帧数量
# 计算测试数据的序列索引
test_end_idx = [0, *test_end_idx] # 开头加0
test_end_idx = torch.cumsum(torch.tensor(test_end_idx), 0) # 累加得到结束索引
seq_length = 99 # 测试序列长度
test_sampler = MySampler(test_end_idx, seq_length) # 测试数据采样器
# 实例化训练数据集
train_data = MyDataset(
image_paths=train_class_image_paths, # 图像路径与类别
seq_length=seq_length, # 序列长度
transform=transform, # 预处理函数
length=len(train_sampler) # 数据集大小(由采样器决定)
)
# 实例化测试数据集
test_data = MyDataset(
image_paths=test_class_image_paths, # 测试图像路径与类别
seq_length=seq_length, # 序列长度
transform=transform, # 预处理函数
length=len(test_sampler) # 数据集大小
)
# 创建数据加载器(结合数据集和采样器)
train_dataloader = DataLoader(dataset=train_data, batch_size=batch_size, sampler=train_sampler)
test_dataloader = DataLoader(dataset=test_data, batch_size=batch_size, sampler=test_sampler)
解答:
1.train_end_idx = torch.cumsum(torch.tensor(train_end_idx), 0)
作用:
计算 累加和(cumulative sum),将帧数转换为每个视频的 结束索引。
例如 [0, 100, 150]
经过累加后变为 [0, 100, 250]
,表示:
-
第 1 个视频的帧索引范围:
0
(起始)到100
(结束,不包含) -
第 2 个视频的帧索引范围:
100
到250
(结束,不包含)
2.glob.glob()
作用:
-
glob
模块用于查找匹配特定模式的文件路径 -
*.jpg
是通配符,匹配所有.jpg
后缀的文件 -
返回一个列表,包含所有匹配文件的完整路径
(例如:['/home/user/images/cat.jpg', '/home/user/images/dog.jpg']
)
三、日志与评估工具模块(日志记录与测试函数)
# 记录训练日志(TensorBoard)
def write_scalars(logger, epoch, loss):
#logger:SummaryWriter 实例,负责日志写入。
#loss:训练 / 测试损失值。
logger.add_scalars('train/loss', {'loss': loss}, epoch) # 向TensorBoard添加训练损失
#【TensorBoard系列】调用add_scalars()函数绘制多变量曲线
# 记录测试日志(TensorBoard)
def write_test_scalars(logger, epoch, losses, metrics):
logger.add_scalars('test/losses/total_loss', {'Loss': losses}, epoch) # 测试损失
logger.add_scalars('test/accuracy/AP', {'AP': metrics['AP'], 'PR80': metrics['PR80']}, epoch) # AP和PR80指标
logger.add_scalars('test/accuracy/time-to-accident', # 时间相关指标
{'mTTA': metrics['mTTA'], 'TTA_R80': metrics['TTA_R80']}, epoch)
# 测试函数(评估模型性能)
def test(test_dataloader, model):
all_pred = [] # 存储所有预测结果
all_labels = [] # 存储所有真实标签
losses_all = [] # 存储所有测试损失
all_toas = [] # 存储所有事故发生时间(TOA)
with torch.no_grad(): # 关闭梯度计算(测试时不更新参数)
loop = tqdm(test_dataloader, total=len(test_dataloader), leave=True) # 测试进度条
for imgs, labels, toa in loop: # 遍历测试数据
imgs = imgs.to(device) # 图像数据移至设备(GPU/CPU)
labels = torch.squeeze(labels) # 压缩标签维度(去除冗余维度)
labels = labels.to(device) # 标签移至设备
# 前向传播,获取损失和输出(模型返回每帧的预测)
loss, outputs = model(imgs, labels, toa)
loss = loss['total_loss'].item() # 提取总损失的数值
losses_all.append(loss) # 记录损失
num_frames = imgs.size()[1] # 当前批次的序列长度(帧数)
batch_size = imgs.size()[0] # 批次大小
pred_frames = np.zeros((batch_size, num_frames), dtype=np.float32) # 初始化预测结果数组
for t in range(num_frames): # 遍历每帧的预测结果
pred = outputs[t] # 第t帧的输出
# 转换为numpy数组(若在GPU上则先移至CPU)
pred = pred.cpu().numpy() if pred.is_cuda else pred.detach().numpy()
# 计算事故类别的概率(softmax):exp(事故类概率) / 所有类概率之和
pred_frames[:, t] = np.exp(pred[:, 1]) / np.sum(np.exp(pred), axis=1)
# 收集预测结果和标签
all_pred.append(pred_frames) # 预测结果
label_onehot = labels.cpu().numpy() # 标签移至CPU并转numpy
label = np.reshape(label_onehot[:, 1], [batch_size, ]) # 提取事故类别标签(假设为one-hot编码)
all_labels.append(label)
toas = np.squeeze(toa.cpu().numpy()).astype(np.int64) # TOA移至CPU并转numpy
all_toas.append(toas)
loop.set_postfix(val_loss=np.mean(losses_all)) # 更新进度条显示
# 合并所有批次的结果(处理最后一个批次可能的维度问题)
all_pred = np.vstack((np.vstack(all_pred[0][:-1]), all_pred[0][-1]))
all_labels = np.hstack((np.hstack(all_labels[0][:-1]), all_labels[0][-1]))
all_toas = np.hstack((np.hstack(all_toas[0][:-1]), all_toas[0][-1]))
return all_pred, all_labels, all_toas, losses_all
四、模型训练与保存模块(核心训练逻辑)
# 训练函数
def train():
# 创建模型和日志保存目录(若不存在)
if not os.path.exists(model_dir):
os.makedirs(model_dir)
if not os.path.exists(logs_dir):
os.makedirs(logs_dir)
logger = SummaryWriter(logs_dir) # 初始化TensorBoard日志记录器
# 实例化事故检测模型
model = Accident_Model(
num_classes, h_dim, z_dim, n_layers, dropout,
extractor, loss_type, network_type
).to(device) # 模型移至设备(GPU/CPU)
# 优化器与学习率调度器
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate) # Adam优化器
# 学习率调度器:当损失不再下降时,学习率乘以0.5(耐心5轮)
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.5, patience=5)
# 迁移学习:冻结部分预训练参数
# ==============================================================================
if extractor == 'resnet50': # 若使用ResNet50作为特征提取器
for name, param in model.features.named_parameters(): # 遍历特征提取器参数
# 只解冻自定义全连接层参数,冻结预训练的特征提取层
if "fc.0.weight" in name or "fc.0.bias" in name:
param.requires_grad = True # 可训练
else:
param.requires_grad = False # 冻结
elif extractor == 'vgg16': # 若使用VGG16作为特征提取器
# 冻结VGG16的特征提取层,解冻分类器
for name, param in model.features.named_parameters():
if "classifier" in name:
param.requires_grad = True # 分类器可训练
else:
param.requires_grad = False # 特征层冻结
# 解冻GRU网络的关键参数
for name, param in model.gru_net.named_parameters():
if 'gru.weight' in name or 'gru.bias' in name: # GRU的权重和偏置
param.requires_grad = True
elif 'dense1' in name or 'dense2' in name: # 输出层全连接层
param.requires_grad = True
else:
param.requires_grad = False # 其他层冻结
else:
raise NotImplementedError # 未实现的特征提取器
# ==============================================================================
model.train() # 模型设为训练模式
loss_best = 100 # 初始化最优损失(用于保存最优模型)
for epoch in range(num_epochs): # 遍历训练轮数
loop = tqdm(train_dataloader, total=len(train_dataloader), leave=True) # 训练进度条
for imgs, labels, toa in loop: # 遍历训练数据
loop.set_description(f"Epoch [{epoch + 1}/{num_epochs}]") # 显示当前轮数
imgs = imgs.to(device) # 图像移至设备
labels = torch.squeeze(labels) # 压缩标签维度
labels = labels.to(device) # 标签移至设备
# 前向传播:获取损失和输出
loss, outputs = model(imgs, labels, toa)
optimizer.zero_grad() # 清空梯度
loss['total_loss'].mean().backward() # 总损失求平均后反向传播
torch.nn.utils.clip_grad_norm_(model.parameters(), 10) # 梯度裁剪(防止梯度爆炸)
optimizer.step() # 更新参数
# 更新进度条显示
loop.set_description(f"Epoch [{epoch + 1}/{num_epochs}]")
loop.set_postfix(loss=loss['total_loss'].item())
lr = optimizer.param_groups[0]['lr'] # 当前学习率
# 记录训练日志
write_scalars(logger, epoch, loss['total_loss'])
# 模型测试与评估
print('-------------------------------')
print('------开始评估------')
model.eval() # 模型设为评估模式
all_pred, all_labels, all_toas, losses_all = test(test_dataloader, model) # 测试
total_loss = np.mean(losses_all) # 平均测试损失
# 计算评估指标
metrics = {}
metrics['AP'], metrics['mTTA'], metrics['TTA_R80'], metrics['PR80'] = evaluation_P_R80(
all_pred, all_labels, all_toas, fps
)
# 记录测试日志
write_test_scalars(logger, epoch, total_loss, metrics)
model.train() # 恢复训练