导读:本期聚焦于坚哥创作的《如何在自定义数据集上微调PyTorch I3D视频行为识别模型?》,敬请观看详情。视频行为识别任务中,I3D模型凭借 inflated 3D 卷积结构在 Kinetics 数据集上取得了出色的表现,但直接拿来用在自有数据上效果往往不理想。本文围绕如何在自己的视频数据集上微调 I3D 模型展开,先讲清预训练权重的加载与分类头的替换方法,再介绍视频帧的采样策略、数据增强与批处理拼接细节,最后给出完整的训练循环代码、学习率设置建议以及常见报错的处理方式,帮助你把预训练模型顺利迁移到自定义场景中,用少量标注数据获得不错的识别精度。

视频行为识别是计算机视觉中一个应用面很广的方向,比如安防场景的异常行为检测、体育动作分析、工厂流水线上的操作规范识别等。I3D(Inflated 3D ConvNet)是目前最经典的视频识别骨干网络之一,它把 2D 的 Inception-V1 网络逐层膨胀成 3D 结构,卷积核在时间和空间三个维度上同时滑动,天然适合捕捉动作的时序信息。官方提供了在 Kinetics-400 上预训练好的权重,我们要做的就是把这份知识迁移到自己的数据集上。这篇文章完整梳理一遍微调流程,包括权重加载、分类头替换、数据管道搭建和训练细节,照着做基本能一次跑通。

如何在自定义数据集上微调PyTorch I3D视频行为识别模型?

一、I3D 模型结构与预训练权重的处理

I3D 的核心思想是参数膨胀:把一个 2D 卷积核尺寸的权重直接复制堆叠 N 份得到 3D 卷积核,这样既继承了 ImageNet 预训练的视觉特征,又能处理视频输入。常用的开源实现有 dpc(piergiaq)和 videoclassification 两个仓库,前者代码简洁、适合二次开发,社区用得最多。模型输出层是一个 VideoClassifier 模块,最后一层全连接输出 400 维的 Kinetics 类别分数。

微调的第一步是替换分类头。假设我们的自定义数据集有 10 个类别,就需要把最后的全连接层换成输出 10 维的新层。由于新层是随机初始化的,学习率也应该和其他层区分开。权重加载时要注意 PyTorch 的 load_state_dict 默认 strict 模式会因为新旧结构不一致而报错,处理办法是先加载骨干部分的权重,再手动替换分类层:

import torch
import torch.nn as nn
from pytorch_i3d import InceptionI3d

model = InceptionI3d(num_classes=400, in_channels=3)
# 加载 Kinetics 预训练权重,忽略缺失和多余的键
state_dict = torch.load('rgb_imagenet.pt', map_location='cpu')
model.load_state_dict(state_dict, strict=False)

# 替换分类头为自定义类别数
NUM_CLASSES = 10
model.logits = nn.Linear(768, NUM_CLASSES)

这样骨干网络的 768 维特征被保留下来,新分类头负责映射到 10 个类别。有一点容易被忽略:如果使用光流双流版本,光流分支的输入通道数要设为 2,且预训练权重要选 flow 版本而不是 rgb 版本,两者不能混用,否则收敛会非常慢甚至不收敛。

二、视频数据管道:采样、增强与批处理

视频数据不能像图片那样整段读进显存,必须做帧采样。I3D 标准输入是 16 帧连续片段,分辨率 224x224。实践中常用的策略有两种:训练时在视频时间轴上随机取一个起点,连续截取 16 帧,这样每次看到的片段不同,相当于一种天然的数据增强;验证和测试时则均匀采样多个片段,分别推理后取分数平均,结果更稳定。

数据增强方面,空间维度的随机裁剪、水平翻转都适用,但要注意竖直翻转通常没有意义(人倒立做动作的场景很少见)。时序维度可以做轻微的抖动,比如起点加一个随机偏移。下面给出一个简化版的 Dataset 实现:

import cv2
import numpy as np
from torch.utils.data import Dataset

class VideoDataset(Dataset):
    def __init__(self, samples, num_frames=16, size=224, training=True):
        # samples 是 (视频路径, 标签) 的列表
        self.samples = samples
        self.num_frames = num_frames
        self.size = size
        self.training = training

    def __getitem__(self, idx):
        path, label = self.samples[idx]
        cap = cv2.VideoCapture(path)
        frames = []
        while True:
            ret, frame = cap.read()
            if not ret:
                break
            frames.append(frame)
        cap.release()

        total = len(frames)
        if self.training:
            start = np.random.randint(0, max(1, total - self.num_frames))
        else:
            start = (total - self.num_frames) // 2
        clip = frames[start:start + self.num_frames]

        # 统一到固定帧数,短视频循环填充
        while len(clip) < self.num_frames:
            clip.append(clip[-1])

        processed = []
        for f in clip:
            f = cv2.cvtColor(f, cv2.COLOR_BGR2RGB)
            if self.training:
                # 随机裁剪到 224
                h, w = f.shape[:2]
                top = np.random.randint(0, h - self.size)
                left = np.random.randint(0, w - self.size)
                f = f[top:top + self.size, left:left + self.size]
                if np.random.rand() > 0.5:
                    f = np.flip(f, axis=1)  # 水平翻转
            else:
                f = cv2.resize(f, (self.size, self.size))
            processed.append(f)

        # 转成 (C, T, H, W),归一化到 [-1, 1]
        data = np.stack(processed).astype(np.float32) / 255.0
        data = (data - 0.5) * 2.0
        data = data.transpose(3, 0, 1, 2)
        return torch.from_numpy(data.copy()), label

特别要注意 I3D 的输入维度顺序是 (N, C, T, H, W),时间维在通道后面,这一点和不少其他视频模型不同,transpose 错了维度模型不报错但精度会大幅下降。归一化建议用 [-1, 1] 区间,与预训练时保持一致。另外用 OpenCV 逐帧解码非常慢,数据量大时建议提前把视频抽帧存成 jpg 或者转成解码更快的格式,否则训练瓶颈会卡在数据加载上,GPU 利用率常常不到三成。

三、训练配置与常见问题处理

训练配置上有几个经验值值得参考。优化器用 SGD 配合动量 0.9,骨干网络学习率设为 0.001,新分类头可以放大 10 倍用 0.01,因为新头是随机初始化的,需要更快的学习速度。学习率衰减用余弦退火或者每 20 个 epoch 衰减一半都可以。batch size 受显存限制,16 帧的 I3D 比较吃显存,一张 8G 卡大概只能放 4 到 8 个样本,配梯度累积可以等效扩大 batch:

import torch.optim as optim
from torch.optim.lr_scheduler import CosineAnnealingLR

model = model.cuda()
backbone_params = [p for n, p in model.named_parameters() if 'logits' not in n]
head_params = model.logits.parameters()

optimizer = optim.SGD([
    {'params': backbone_params, 'lr': 1e-3},
    {'params': head_params, 'lr': 1e-2},
], momentum=0.9, weight_decay=1e-7)

scheduler = CosineAnnealingLR(optimizer, T_max=50)

ACCUM_STEPS = 4
for epoch in range(50):
    model.train()
    optimizer.zero_grad()
    for i, (inputs, labels) in enumerate(train_loader):
        inputs = inputs.cuda(non_blocking=True)
        labels = labels.cuda(non_blocking=True)
        out = model(inputs)          # 输出 (N, num_classes, 1, 1)
        out = out.mean(dim=[2, 3])   # 抹掉空间维
        loss = nn.functional.cross_entropy(out, labels)
        (loss / ACCUM_STEPS).backward()
        if (i + 1) % ACCUM_STEPS == 0:
            optimizer.step()
            optimizer.zero_grad()
    scheduler.step()
    print(f'epoch {epoch} loss {loss.item():.4f}')

常见报错里最典型的是显存溢出(CUDA out of memory),解决办法按优先级排:减小 batch size、把 num_frames 从 16 降到 8(会牺牲一点精度)、开启混合精度训练(amp)。其次是验证集精度剧烈波动,多半是学习率太大或者数据集类别不均衡,后者可以按类别加权计算交叉熵损失。还有一点,I3D 的输出形状是 (N, num_classes, 1, 1),直接喂给交叉熵会维度不匹配,记得先做上面的 mean 或者 squeeze 处理。

最后谈谈数据量的问题。经验上每个类别至少要有 50 段以上的视频,微调才能稳定收敛;数据更少时建议冻结前面的 Mixed 层,只训练后面几层加分类头,虽然上限低一些但不容易过拟合。评估时用 top-1 加混淆矩阵一起看,能快速发现哪几类动作容易混淆,再针对性补充训练样本,往往比盲目加 epoch 更有效。整个流程跑通后,把训练和推理的采样策略、归一化方式固化成同一套代码,部署阶段的精度就能和验证结果保持一致。

PyTorchI3D模型微调修改时间:2026-09-16 15:10:47

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0916/58015.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。