深度学习:基于时空特征的交通事故预测(CNN-LSTM混合模型)(内附完整代码和语料库)

代码和语料库下载:

通过网盘分享的文件:SG10.rar
链接: https://pan.baidu.com/s/1MxCrpwoBYQiv1TDfWFdJKg 提取码: tyv9

整体架构与作用目的

该代码是一个用于事故检测的深度学习模型训练与评估框架。通过处理视频帧序列(将视频拆分为图片帧),训练模型识别视频中是否发生事故,同时可能预测事故发生的时间(TOA,Time of Accident),属于视频时序分类与事件检测任务。

整体架构

  1. 数据处理层

    • 从配置文件读取参数(网络结构、路径、超参数等)。
    • 加载训练 / 测试数据(视频帧序列),通过自定义采样器(MySampler)和数据集(MyDataset)处理时序数据。
    • 对图像进行预处理( resize、归一化等)。
  2. 模型层

    • 核心模型为Accident_Model,支持不同的特征提取器(如 ResNet50、VGG16)和网络结构(可能包含 GRU 等时序模型)。
    • 支持多种损失函数(通过loss_type配置),用于训练事故检测任务。
  3. 训练与评估层

    • 训练过程:使用 Adam 优化器,结合学习率调度器(ReduceLROnPlateau),通过 TensorBoard 记录训练日志。
    • 评估过程:计算准确率(AP)、平均事故时间(mTTA)等指标,通过evaluation_P_R80实现。
    • 模型保存:保存每轮训练的模型和最优模型(基于验证损失)。

一、导入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()  # 恢复训练

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值