C3D网络介绍及代码撰写详解(总结3)

news2024/11/25 15:36:52

可以从本人以前的文章中可以看出作者以前从事的是嵌入式控制方面相关的工作,是一个机器视觉小白,之所以开始入门机器视觉的学习主要是一个idea,想把机器视觉与控制相融合未来做一点小东西。废话不多说开始正题。(如有侵权立即删稿)

摘要

本文是介绍C3D网络,个人对其的知识总结,以及结合论文进行讲解,网络设计的知识点,以及代码如何撰写,基于pytorch编写代码。作为一个刚入门的小白怎么去学习别人的代码,一步一步的去理解每一行代码,怎么将网络设计变成代码,模仿大佬的代码去撰写。作为小白如有不足之处请批评指正哈。

C3D

在网络设计之前需要明白什么是C3D。
以下是我借鉴的文章的参考链接:
【1】「深度学习一遍过」必修28:基于C3D预训练模型训练自己的视频分类数据集的设计与实现
【2】C3D代码总结(Pytorch)
【3】深度学习文章阅读2–3D Convolutional Neural Networks for Human Action Recognition
【4】Learning Spatiotemporal Features with 3D Convolutional Networks(这篇论文我也不知道我在哪下的了)
【5】深度学习笔记----三维卷积及其应用(3DCNN,PointNet,3D U-Net)
在这里插入图片描述
其实可以从这张图片中就大致猜想出,三维卷积的工作原理,二维卷积是经过卷积核运算得出一个点的值,而三维卷积就是三维卷积核将连续的多(三)张图片卷积处理得出的依旧是一个点的值。

• 使用标准的3D卷积层,直接在时间、宽度和高度三个维度上进行卷积。
• 通常由多个3D卷积层组成,简单而有效,但没有利用到预训练模型的优势。
• 在一些基本的视频分类和动作识别任务中表现良好,但相对于更复杂的网络,其性能有限。
• 由于其结构相对简单,训练和推理的计算成本较低,但也因此限制了其表达能力。

预训练权重文件在深度学习中具有重要的作用,尤其是在处理图像和视频等任务时。其主要用途和优点包括:

1. 加速训练过程
•	使用预训练权重可以显著减少模型的训练时间,因为网络已经学习到了一些通用特征。
2. 提高模型性能
•	预训练模型通常在大规模数据集上进行训练,能够捕捉到丰富的特征表示,从而在特定任务上表现更好。
3. 减少过拟合风险
•	在小数据集上训练时,使用预训练权重可以帮助模型更好地泛化,降低过拟合的风险。
4. 迁移学习
•	预训练权重使得迁移学习成为可能,用户可以在一个领域(如图像分类)上训练得到的模型权重,再将其应用于另一个相关领域(如物体检测或视频分析)。

是否可以不使用预训练权重直接训练?
当然可以!直接从头开始训练一个模型是可行的,尤其是在以下情况下:
• 大数据集: 如果你有足够大的数据集进行训练,模型可以自主学习到有效的特征。
• 特定任务: 对于某些特定任务,预训练的特征可能并不适用,此时从头开始训练可能会更有效。
• 实验需求: 在一些研究或实验中,你可能希望观察从零开始训练的效果,以了解模型的学习能力。
但是一般来说,使用预训练权重会更加高效,特别是在数据量有限的情况下。

简而言之,C3D是一个相对基础的3D卷积网络,主要依靠从头开始训练。强调一下,本人见过网上的C3D代码是有使用预训练模型的。但是本人设计的网络简单,设计的是一个可以自己训练任意长度的模型,但本博客仅仅识别3个动作,没有对预训练代码进行撰写(后悔了,训练时间还是很长)。我写这篇文章的初衷就是总结C3D的知识点说实在的C3D的识别效果是真的差,还训练的久。

理论基础

论文摘要

我们提出了一种简单而有效的时空特征学习方法,使用在大规模监督视频数据集上训练的深度3维卷积网络(3D ConvNets)。我们的发现有三个方面:
1)与2D ConvNets相比,3D ConvNets更适合时空特征学习;
2)在所有层中具有小的3 × 3 × 3卷积核的同构架构是3D ConvNets的最佳性能架构之一;
3)我们学习的功能,即C3 D(卷积3D),具有简单的线性分类器,在4个不同的基准测试中优于最先进的方法,并且在其他2个基准测试中与当前最好的方法相当。此外,这些特征非常紧凑:在UCF 101数据集上实现了52.8%的准确率,只有10个维度,并且由于ConvNets的快速推理,计算效率也非常高。最后,它们在概念上非常简单,易于训练和使用。

3D卷积和池化

在这里插入图片描述
a)在图像上应用2D卷积产生图像。b)在视频体积上应用2D卷积(多个帧作为多个通道)也产生图像。c)在视频体积上应用3D卷积产生另一体积,保留输入信号的时间信息。

我们认为3D ConvNet非常适合时空特征学习。与2D ConvNet相比,由于3D卷积和3D池化操作,3D ConvNet能够更好地建模时间信息。在3D ConvNets中,卷积和池化操作是在时空上执行的,而在2D ConvNets中,它们只在空间上执行。上图说明了差异,应用于图像的2D卷积将输出图像,应用于多个图像的2D卷积也会产生图像。因此,2D ConvNets在每次卷积运算之后都会丢失输入信号的时间信息。只有3D卷积保留了输入信号的时间信息,从而产生输出体积。

1.输入特征图的深度 D
2.卷积核的深度 d
3.输入特征图的高度和宽度为 H 和 W
4.P 是填充(padding)的数量
5.S 是步幅(stride)
6.输出的深度 Od

在这里插入图片描述
小结:主要是需要理解b,c。三维卷积就是你的d小于L,假设d=L-1,那么你的输出output为2。

在这里插入图片描述

在这里插入图片描述论文中架构搜索研究,将空间感受野固定为3 × 3,仅改变3D卷积核的时间深度。
为了简单起见,从现在开始,我们引用大小为c × l × h × w的视频剪辑,其中c是通道数(一般为RGB3通道),l是帧数的长度,h和w分别是帧的高度和宽度。我们还将3D卷积和池化内核大小称为d×k×k,其中d是内核时间深度,k是内核空间大小。


在这里插入图片描述
这些网络被设置为将视频片段作为输入。所有视频帧都被调整为128 × 171(这大约是UCF101帧分辨率的一半,博客后续开发没有用这个数据库),视频被分成不重叠的16帧剪辑,然后用作网络的输入(也就是这段视频随机选取一段连续的16帧作为输入)。输入尺寸为3 × 16 × 128 × 171。我们还通过在训练期间使用大小为3 × 16 × 112 × 112的输入剪辑的随机裁剪来消除抖动(这一块代码本人没有写)。该网络有5个卷积层和5个池化层(每个卷积层后面紧跟着一个池化层),2个全连接层和一个softmax loss层来预测动作标签。从1到5的5个卷积层的滤波器的数量分别为64、128、256、256、256。
适当的padding(空间和时间)和步幅1,因此从这些卷积层的输入到输出的大小没有变化。所有池化层都是最大池化,内核大小为2 × 2 × 2(第一层除外),步长为1,这意味着输出信号的大小与输入信号相比减少了8倍。第一个池化层的内核大小为1 × 2 × 2,目的是不过早合并时间信号,并且还满足16帧的剪辑长度(例如,在完全折叠时间信号之前,我们最多可以使用因子2进行4次时间池化)。两个完全连接的层有2048个输出。我们使用30个片段的小批量从头开始训练网络,初始学习率为0.003。学习率在每4个epoch之后除以10。训练在16个epoch之后停止。


在这里插入图片描述
所有的3D卷积滤波器都是3 × 3 × 3,步长为1 × 1 × 1。所有3D池化层都是2×2×2,步长为2×2×2,除了pool1,其内核大小为1 × 2 × 2,步长为1 × 2 × 2,目的是保留早期阶段的时间信息。每个完全连接的层具有4096个输出单元。

训练要求

由于有许多长视频,我们从每个训练视频中随机提取五个2秒长的片段。剪辑的大小调整为128 × 171的帧大小。在训练中,我们将输入剪辑随机裁剪为16×112×112个裁剪,以进行空间和时间抖动。我们也以50%的概率水平翻转它们。训练由SGD完成,小批量大小为30个示例(本人用的8个)。初始学习率为0.003,每150K次迭代除以2。优化在190万次迭代(大约13个epoch)时停止。
小结:根据C3D论文的训练步骤,对于一个12秒的视频,它可以被随机分成5个2秒的片段。每个2秒的片段包含32帧(2秒 × 16帧/秒 = 32帧),再将32帧的视频均匀采样成16帧,每个16帧片段作为输入送入网络进行训练。

软件代码构思

在这里插入图片描述
有了以上理论基础后,开始构建代码思路,整体构建思路如下图所示,写代码之前一定要构思好大致思路,代码永远是为你思路框架服务的。

Model代码撰写

1.搭建C3D网络,这一部分网络搭建是基于以上理论分析搭建的。

#-----------------------1.搭建C3D网络-------------------------
class C3D(nn.Module):
    def __init__(self, num_classes, pretrained=False):
        super(C3D, self).__init__()

        self.conv1 = nn.Conv3d(3, 64, kernel_size=(3, 3, 3), padding=(1, 1, 1))
        self.pool1 = nn.MaxPool3d(kernel_size=(1, 2, 2), stride=(1, 2, 2))

        self.conv2 = nn.Conv3d(64, 128, kernel_size=(3, 3, 3), padding=(1, 1, 1))
        self.pool2 = nn.MaxPool3d(kernel_size=(2, 2, 2), stride=(2, 2, 2))

        self.conv3a = nn.Conv3d(128, 256, kernel_size=(3, 3, 3), padding=(1, 1, 1))
        self.conv3b = nn.Conv3d(256, 256, kernel_size=(3, 3, 3), padding=(1, 1, 1))
        self.pool3 = nn.MaxPool3d(kernel_size=(2, 2, 2), stride=(2, 2, 2))

        self.conv4a = nn.Conv3d(256, 512, kernel_size=(3, 3, 3), padding=(1, 1, 1))
        self.conv4b = nn.Conv3d(512, 512, kernel_size=(3, 3, 3), padding=(1, 1, 1))
        self.pool4 = nn.MaxPool3d(kernel_size=(2, 2, 2), stride=(2, 2, 2))

        self.conv5a = nn.Conv3d(512, 512, kernel_size=(3, 3, 3), padding=(1, 1, 1))
        self.conv5b = nn.Conv3d(512, 512, kernel_size=(3, 3, 3), padding=(1, 1, 1))
        self.pool5 = nn.MaxPool3d(kernel_size=(2, 2, 4), stride=(2, 2, 2), padding=(0, 0, 0))

        self.fc6 = nn.Linear(8192, 4096)
        self.fc7 = nn.Linear(4096, 4096)
        self.fc8 = nn.Linear(4096, num_classes)

        self.dropout = nn.Dropout(p=0.5)

        self.relu = nn.ReLU()

        self.__init_weight()


    def forward(self, x):

        x = self.relu(self.conv1(x))
        x = self.pool1(x)

        x = self.relu(self.conv2(x))
        x = self.pool2(x)

        x = self.relu(self.conv3a(x))
        x = self.relu(self.conv3b(x))
        x = self.pool3(x)

        x = self.relu(self.conv4a(x))
        x = self.relu(self.conv4b(x))
        x = self.pool4(x)

        x = self.relu(self.conv5a(x))
        x = self.relu(self.conv5b(x))
        x = self.pool5(x)                #这里出问题
        x = x.reshape(x.size(0), -1)  # 拉平操作,(batch_size, num_features) 也就是(8,)
        x = self.relu(self.fc6(x))
        x = self.dropout(x)
        x = self.relu(self.fc7(x))
        x = self.dropout(x)

        logits = self.fc8(x)

        return logits

    def __init_weight(self):
        for m in self.modules():
            if isinstance(m, nn.Conv3d):
                torch.nn.init.kaiming_normal_(m.weight)
            elif isinstance(m, nn.BatchNorm3d):
                m.weight.data.fill_(1)
                m.bias.data.zero_()
输入计算
•	输入形状: (batch_size, channels, depth, height, width) = (8, 3, 16, 128, 171)
Conv1
•	卷积层: self.conv1 = nn.Conv3d(3, 64, kernel_size=(3, 3, 3), padding=(1, 1, 1))
•	输出形状计算:
•	输出通道数: 64
•	深度(D): (16+213)/1+1=16
•	高度(H): (128+213)/1+1=128
•	宽度(W): (171+213)/1+1=171
•	输出形状: (8, 64, 16, 128, 171)
Pool1
•	池化层: self.pool1 = nn.MaxPool3d(kernel_size=(1, 2, 2), stride=(1, 2, 2))
•	输出形状计算:
•	深度(D): 16/1=16
•	高度(H): 128/2=64
•	宽度(W): 171/2=85.585 (取整)
•	输出形状: (8, 64, 16, 64, 85)
Conv2
•	卷积层: self.conv2 = nn.Conv3d(64, 128, kernel_size=(3, 3, 3), padding=(1, 1, 1))
•	输出形状计算:
•	输出通道数: 128
•	深度(D): 16+213=16
•	高度(H): 64+213=64
•	宽度(W): 85+213=85
•	输出形状: (8, 128, 16, 64, 85)
Pool2
•	池化层: self.pool2 = nn.MaxPool3d(kernel_size=(2, 2, 2), stride=(2, 2, 2))
•	输出形状计算:
•	深度(D): 16/2=8
•	高度(H): 64/2=32
•	宽度(W): 85/2=42.5
•	输出形状: (8, 128, 8, 32, 42)
Conv3a
•	卷积层: self.conv3a = nn.Conv3d(128, 256, kernel_size=(3, 3, 3), padding=(1, 1, 1))
•	输出形状计算:
•	输出通道数: 256
•	深度(D): 8+213=8
•	高度(H): 32+213=32
•	宽度(W): 42+213=42
•	输出形状: (8, 256, 8, 32, 42)
Conv3b
•	卷积层: self.conv3b = nn.Conv3d(256, 256, kernel_size=(3, 3, 3), padding=(1, 1, 1))
•	输出形状计算:
•	输出通道数: 256
•	深度(D): 8+213=8
•	高度(H): 32+213=32
•	宽度(W): 42+213=42
•	输出形状: (8, 256, 8, 32, 42)
Pool3
•	池化层: self.pool3 = nn.MaxPool3d(kernel_size=(2, 2, 2), stride=(2, 2, 2))
•	输出形状计算:
•	深度(D): 8/2=4
•	高度(H): 32/2=16
•	宽度(W): 42/2=21
•	输出形状: (8, 256, 4, 16, 21)
Conv4a
•	卷积层: self.conv4a = nn.Conv3d(256, 512, kernel_size=(3, 3, 3), padding=(1, 1, 1))
•	输出形状计算:
•	输出通道数: 512
•	深度(D): 4+213=4
•	高度(H): 16+213=16
•	宽度(W): 21+213=21
•	输出形状: (8, 512, 4, 16, 21)
Conv4b
•	卷积层: self.conv4b = nn.Conv3d(512, 512, kernel_size=(3, 3, 3), padding=(1, 1, 1))
•	输出形状计算:
•	输出通道数: 512
•	深度(D): 4+213=4
•	高度(H): 16+213=16
•	宽度(W): 21+213=21
•	输出形状: (8, 512, 4, 16, 21)
Pool4
•	池化层: self.pool4 = nn.MaxPool3d(kernel_size=(2, 2, 2), stride=(2, 2, 2))
•	输出形状计算:
•	深度(D): 4/2=24/2=2
•	高度(H): 16/2=816/2=8
•	宽度(W): 21/2=10.510 (取整)
•	输出形状: (8, 512, 2, 8, 10)
Conv5a
•	卷积层: self.conv5a = nn.Conv3d(512, 512, kernel_size=(3, 3, 3), padding=(1, 1, 1))
•	输出形状计算:
•	输出通道数: 512
•	深度(D): 2+213=2
•	高度(H): 8+213=8
•	宽度(W): 10+213=10
•	输出形状: (8, 512, 2, 8, 10)
Conv5b
•	卷积层: self.conv5b = nn.Conv3d(512, 512, kernel_size=(3, 3, 3), padding=(1, 1, 1))
•	输出形状计算:
•	输出通道数: 512
•	深度(D): 2+213=2
•	高度(H): 8+213=8
•	宽度(W): 10+213=10
•	输出形状: (8, 512, 2, 8, 10)
Pool5
•	池化层: self.pool5 = nn.MaxPool3d(kernel_size=(2, 2, 4), stride=(2, 2, 2), padding=(0, 0, 0))
•	输出形状计算:
•	深度(D): 2/2=1
•	高度(H): (8+202)/2+1=4
•	宽度(W): (10+204)/2+1=4 
•	输出形状: (8, 512, 1, 4, 4)
Flatten and Fully Connected Layers
•	在进入全连接层之前,我们需要将输出展平。展平的输出大小为:512×1×4×4=8192
全连接层
•	self.fc6 = nn.Linear(8192, 4096)
•	self.fc7 = nn.Linear(4096, 4096)
•	self.fc8 = nn.Linear(4096, num_classes)
最终的输出形状为 (8, num_classes),其中 num_classes 为你在模型初始化时指定的类别数量。
最终输出
•	输出形状: (8, num_classes)

在这里插入图片描述
也可以带这个公式计算。

dataset代码撰写

1.视频标签处理

初始化视频像素,标签编号为1.txt读取。

import os
import cv2
import numpy as np
import torch
from torch.utils.data import Dataset
from torchvision import transforms
import random

class C3DDataset(Dataset):
    def __init__(self, video_dir, label_dir, transform=None):
        self.video_dir = video_dir
        self.label_dir = label_dir
        self.transform = transform
        self.video_labels = self.load_labels()
        self.num_frames = 16
        self.target_height = 128
        self.target_width = 171
        self.segment_duration = 2  # 每个片段的持续时间(秒)
#-------------------------1.视频标签处理-----------------------------
    def load_labels(self):
        label_map = {'running': 0, 'walking': 1, 'fall_down': 2}
        labels = []
        for label_file in os.listdir(self.label_dir):
            with open(os.path.join(self.label_dir, label_file), 'r') as f:
                line = f.readline().strip()
                labels.append(label_map.get(line, -1))
        return labels

    def __len__(self):
        return len(self.video_labels)

2.视频处理

这部分实现的功能主要是,输入任意的视频,视频编号为1.avi,若视频长度大于10s也就是,可以截取5个样本,则随机抽取5个。若视频长度不足的化,任意复制其中的一份使其长度为5。也就是假设你的视频只有4s,两个样本,则会任意取其一,复制3份,扩充成5个。最终的返回值为output_segments, label。output_segments为tensor(batch_size,5,128,171)。

#-----------------------------2.视频处理-----------------------------
    def __getitem__(self, idx):
        video_path = os.path.join(self.video_dir, f'{idx + 1}.avi')
        cap = cv2.VideoCapture(video_path)

        total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
        fps = cap.get(cv2.CAP_PROP_FPS)
        video_duration = total_frames / fps  # 视频时长(秒)

        # 计算可以提取的片段数量
        num_segments = int(video_duration // self.segment_duration)

        if num_segments == 0:
            print(f"视频 {video_path} 不足 {self.segment_duration} 秒,舍去")
            return None  # 不足2秒的视频舍去

        output_segments = []

        # 随机选择5个片段,如果可用片段少于5,则随机重复片段
        selected_segments = random.sample(range(num_segments), min(num_segments, 5))

        # 如果选择的片段少于5个,填充到5个
        while len(selected_segments) < 5:
            selected_segments.append(random.choice(selected_segments))

        for segment_index in selected_segments:
            start_frame = int(segment_index * self.segment_duration * fps)
            output = np.zeros((3, self.num_frames, self.target_height, self.target_width), dtype=np.float32)

            for i in range(self.num_frames):
                frame_index = start_frame + int(i * (self.segment_duration * fps / self.num_frames))
                cap.set(cv2.CAP_PROP_POS_FRAMES, frame_index)
                ret, frame = cap.read()

                if not ret:
                    print(f"无法读取帧 {frame_index} from {video_path}")
                    break
                frame = cv2.resize(frame, (self.target_width, self.target_height))
                frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
                frame = frame.astype(np.float32) / 255.0
                output[:, i, :, :] = frame.transpose(2, 0, 1)

            output_segments.append(output)

        cap.release()

        label = self.video_labels[idx]

        # 转换为张量,并返回
        if output_segments:  # 确保不为空
            return torch.stack([torch.tensor(segment) for segment in output_segments]), label
        else:
            return None  # 如果没有片段,返回 None

        return output_segments, label


# 定义转换,可以根据需要进行调整
transform = transforms.Compose([
    transforms.Lambda(lambda x: torch.tensor(x)),  # 转换为 Tensor
    # transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])  # 归一化
])

在这里插入图片描述
如图所示,我的视频长度为21.8,output_segment = 10。

训练代码撰写

1.初始化,标准参数初始化

import torch
import torchvision.transforms as transforms
from torch.utils.data import random_split, DataLoader

import argparse
from dataset.dataset import C3DDataset
# ------------------------------1.初始化,标准参数初始化-------------------------------

def parse_opt():
    parser = argparse.ArgumentParser()  # 创建 ArgumentParser 对象
    parser.add_argument('--epochs', type=int, default=16, help='total training epochs')  # 添加参数
    parser.add_argument('--batch_size', type=int, default=8, help='size of each batch')  # 添加批次大小参数
    parser.add_argument('--learning_rate', type=int, default=0.003, help='size of learning_rate')  # 添加批次大小参数
    #--device "cuda:0,cuda:1" 启用多个设备
    parser.add_argument('--device', default='cuda:0', help='cuda device, i.e. 0 or 0,1,2,3 or cpu')  #--device cuda:1
    # 解析参数
    opt = parser.parse_args()
    return opt

opt = parse_opt()  # 调用解析函数
epochs = opt.epochs              # 训练的轮数
batch_size = opt.batch_size      # 每个批次的样本数量
learning_rate = opt.learning_rate
device = torch.device(opt.device)


# 定义转换,可以根据需要进行调整
transform = transforms.Compose([
    transforms.Lambda(lambda x: torch.tensor(x)),  # 转换为 Tensor
    #transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])  # 归一化
])


# 初始化数据集
walking_data = C3DDataset(
    video_dir = r'D:\Pycharm\C3D\dataset\walking',
    label_dir = r'D:\Pycharm\C3D\dataset\walking_label',
    transform=transform
)
runing_data = C3DDataset(
    video_dir = r'D:\Pycharm\C3D\dataset\running',
    label_dir = r'D:\Pycharm\C3D\dataset\running_label',
    transform=transform
)
fall_down_data = C3DDataset(
    video_dir = r'D:\Pycharm\C3D\dataset\fall_down',
    label_dir = r'D:\Pycharm\C3D\dataset\fall_down_label',
    transform=transform
)

2.将数据集总和并随机打乱并分成训练集测试集

#-----------------------2.将数据集总和并随机打乱并分成训练集测试集-------------------
# 合并数据集
total_data = walking_data+runing_data+fall_down_data
# 计算训练集和验证集的大小
total_size = len(total_data)
train_size = int(0.7 * total_size)  # 70%
val_size = total_size - train_size    # 30%

train_data_size = train_size
val_data_size = val_size

print("训练集长度:{}".format(train_data_size))
print("测试集长度:{}".format(val_data_size))
# 随机分割数据集
train_data, val_data = random_split(total_data, [train_size, val_size])
# 创建 DataLoader,分成小批量(batches),以便于进行训练和验证
train_dataloader = DataLoader(train_data, batch_size, shuffle=True)  # shuffle=True可以随机打乱数据
val_dataloader = DataLoader(val_data,batch_size, shuffle=False)
# 打印数据集大小
print("训练集大小:{}".format(len(train_data)))
print("验证集大小:{}".format(len(val_data)))

3.创建网络模型

#----------------------------3.创建网络模型--------------------------------
from C3D_model import C3D

MY_C3D = C3D(num_classes=3, pretrained=True)
MY_C3D = MY_C3D.to(device)

以上部分本人都在这篇文章中有所讲解,详细了解请见下文。
VGG16网络介绍及代码撰写详解(总结1)


4.训练加测试

这一部分内容有所不同,本人是按照作者的想法撰写的这一部分内容,但是发现训练过程太慢了,所以在5次样品之中只抽取了第3个样品作为输入(主要是由于一个epoch快30分钟了还跑不完)。这是我撰写代码的不同之处。并且本人加了一个进度条显示训练速度。

#------------------------------------4.训练加测试-----------------------
import matplotlib
matplotlib.use('TkAgg')  # 或者尝试 'Qt5Agg',有这行代码会多一个弹窗显示
import matplotlib.pyplot as plt
import torch.nn as nn
import pandas as pd
def train():
    best_accuracy = 0.0
    # (1).损失函数构建
    loss_fn = nn.CrossEntropyLoss()  # 计算预测值与真实标签之间的差异
    loss_fn = loss_fn.to(device)  # 将模型和数据都放在同一个设备上,GPU

    # (2).优化器
    # #随机梯度下降(Stochastic Gradient Descent)优化器的一种实现。SGD 是一种常见的优化算法
    optimizer = torch.optim.SGD(MY_C3D.parameters(), lr=learning_rate)

    # 用于存储损失和准确率
    train_loss = []
    accuracies = []
    test_loss = []

    for i in range(epochs):
        loss_temp = 0  # 临时变量
        print("--------第{}轮训练开始--------".format(i + 1))
        if epochs > 0 and epochs % 4 == 0:
            learning_rate * 0.1
        # 训练阶段
        MY_C3D.train()  # 设置为训练模式,用来管理Dropout方法:训练时使用Dropout方法,验证时不使用Dropout方法

        for data in tqdm(train_dataloader, desc=f'Epoch {i+1}/{epochs}', leave=True):
            imgs, targets = data
            imgs = imgs.to(device)
            targets = targets.to(device)

            # 将历史损失梯度清零
            optimizer.zero_grad()

            # 存储模型输出
            all_outputs = []

            # 使用for循环逐个片段送入模型
            for i in range(imgs.size(1)):  # imgs.size(1) 是5
                outputs = MY_C3D(imgs[:, i, :, :, :, :])  # 取出第i个片段,形状为(8, 3, 16, 128, 171)
                all_outputs.append(outputs)

            # 将所有输出合并,假设我们关心的是主输出
            all_outputs = torch.stack(all_outputs)  # 形状变为(5, 8, num_classes) 如果outputs的形状是(8, num_classes)
            # 通过 permute 转换形状
            all_outputs = all_outputs.permute(1, 0, 2)  # 变为 (8, 5, num_classes)
            # 可以选择其他时间步,或使用 max 操作等
            selected_outputs = all_outputs[:,-2, :]  # 选择最后一个时间步的输出 -> (8, num_classes)
            # 初始化损失函数
            loss_fn = nn.CrossEntropyLoss()
            # 计算损失
            loss = loss_fn(selected_outputs, targets)
            # 优化器优化模型
            loss.backward()  # 反向传播
            optimizer.step()  # 梯度更新

            # 将当前损失添加到 train_loss 列表
            train_loss.append(loss.item())

        # -----------------------测试阶段---------------------------------
        # 测试阶段
        MY_C3D.eval()  # 设置为评估模式,关闭Dropout
        total_accuracy = 0
        test_loss = []
        correct = 0
        total = 0
        # 验证过程中不计算损失梯度
        with torch.no_grad():
            # 初始化 test_loss 列表
            for data in tqdm(val_dataloader, desc='Validation', leave=True):
                imgs, targets = data
                imgs = imgs.to(device)
                targets = targets.to(device)

                # 存储模型输出
                all_outputs = []

                # 使用for循环逐个片段送入模型
                for i in range(imgs.size(1)):  # imgs.size(1) 是5
                    outputs = MY_C3D(imgs[:, i, :, :, :, :])  # 取出第i个片段
                    all_outputs.append(outputs)

                # 将所有输出合并
                all_outputs = torch.stack(all_outputs)  # 形状变为(5, 8, num_classes)
                all_outputs = all_outputs.permute(1, 0, 2)  # 变为(8, 5, num_classes)
                # 选择最后一个时间步的输出
                selected_outputs = all_outputs[:, -1, :]  # (8, num_classes)

                # 初始化损失函数
                loss_fn = nn.CrossEntropyLoss()
                # 计算损失
                loss = loss_fn(selected_outputs, targets)

                # 将当前损失添加到 test_loss 列表
                test_loss.append(loss.item())

                # 计算准确率
                _, predicted = torch.max(selected_outputs.data, 1)  # 取得预测结果
                total += targets.size(0)  # 累加总样本数
                correct += (predicted == targets).sum().item()  # 统计正确分类的数量

        # 计算平均损失和准确率
        average_test_loss = sum(test_loss) / len(test_loss)
        accuracy = 100 * correct / total

        print(f'测试集平均损失: {average_test_loss:.4f}, 准确率: {accuracy:.2f}%')


5.保存数据至.pth文件

#-----------------------5.保存数据至.pth文件---------------------------
        #   如果当前测试集准确率大于历史最优准确率
        if (total_accuracy / val_data_size) > best_accuracy:
            #   更新历史最优准确率
            best_accuracy = (total_accuracy / val_data_size)
            #   保存当前权重
            torch.save(MY_C3D, "C3DNet_{}.pth".format(1))
            print("模型已保存")

        # 记录验证损失和准确率
        accuracies.append(total_accuracy / val_data_size)

        print("整体测试集上的正确率:{}".format(total_accuracy / val_data_size))
        print("测试集上的Loss:{}".format(test_loss[-1]))

if __name__ == "__main__":
    train()


训练效果图
在这里插入图片描述
在这里插入图片描述

dete代码撰写

1.视频图像处理

这一部分主要是框定识别的区域,选定指定区域进行识别,可以一定程度上提高正确率。再然后就是读取训练权重,读取识别视频。

#检测
import torch
import numpy as np
import C3D_model
import cv2
torch.backends.cudnn.benchmark = True
#--------------------------------1.视频图像处理---------------------------
#对输入的图像帧进行中心裁剪
def CenterCrop(frame, size):       #输入图像,裁剪后的尺寸(高,宽)
    h, w = np.shape(frame)[0:2]    #获取图像的高度 h 和宽度 w
    th, tw = size
    x1 = int(round((w - tw) / 2.)) #计算裁剪区域的起始坐标 (x1, y1)
    y1 = int(round((h - th) / 2.))

    frame = frame[y1:y1 + th, x1:x1 + tw, :]  #计算出的坐标裁剪图像,并返回裁剪后的图像
    return np.array(frame).astype(np.uint8)


def center_crop(frame): #对输入帧进行固定区域的裁剪
    frame = frame[0:128, 20:192, :]   #从帧中裁剪出指定区域 [8:120, 30:142],并返回处理后的图像
    return np.array(frame).astype(np.uint8)


def main():
    device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
    print("Device being used:", device)

    with open('D:/Pycharm/C3D/dataset/ucf_labels.txt', 'r') as f: #读取 UCF101 数据集的类标签文件,存储类名
        class_names = f.readlines()
        f.close()
    # init model
    model = C3D_model.C3D(num_classes=3)
    model = torch.load('D:/Pycharm/C3D/C3DNet_1.pth')
    model.to(device)
    model.eval()    #设置模型为评估模式(不进行梯度计算)

    # read video
    video = 'D:/Pycharm/C3D/dataset/peple_fall.mp4'
    cap = cv2.VideoCapture(video)
    retaining = True

    clip = []       #逐帧读取视频并进行预处理
    while retaining:
        retaining, frame = cap.read()   #读取每一帧,直到没有帧可读
        if not retaining and frame is None:
            continue
        tmp_ = center_crop(cv2.resize(frame, (180, 320)))   # 将帧缩放至 (128, 171) 并进行裁剪
        tmp = tmp_ - np.array([[[90.0, 98.0, 102.0]]])      # 执行均值减法以进行图像归一化
        clip.append(tmp)                                    # 将处理后的帧添加到 clip 列表中

2.当积累到16帧时进行动作识别预测
 #-------------------2.当积累到16帧时进行动作识别预测---------------------------------
        if len(clip) == 16:                                 # 当积累到16帧时进行动作识别预测
            #print("Clip shape:", np.array(clip).shape)      # 输出 clip 的形状,(16, 128, 171, 3)
            inputs = np.array(clip).astype(np.float32)      # 将 clip 列表转换为 NumPy 数组并进行维度转换,使其满足模型输入要求
            inputs = np.expand_dims(inputs, axis=0)         # 数组的最前面添加一个新维度,输入的形状变为 (1, 16, 128, 171, 3)
            inputs = np.transpose(inputs, (0, 4, 1, 2, 3))  # 调整数组的维度顺序,使其符合C3D模型的输入格式。转置后的形状为 (1, 3, 16, 128, 171)
            inputs = torch.from_numpy(inputs)               # 将NumPy数组转换为PyTorch张量
            #print("Transposed input shape:", inputs.shape)  # 输出转置后的形状([1, 3, 16, 128, 171])
            inputs = torch.autograd.Variable(inputs, requires_grad=False).to(device)  #将张量包装成Variable对象,允许在计算梯度时使用(但此处不需要计算梯度,设置requires_grad=False)
            with torch.no_grad():
                outputs = model.forward(inputs)             # 前向传播,得到输出,即各类别的原始得分

3.视频实时显示预测结果

这一部分主要是打开你的视频,在屏幕上输出你的识别结果。

#---------------------------3.视频实时显示预测结果--------------------------------------
            probs = torch.nn.Softmax(dim=1)(outputs)        # 对每个样本的类别得分进行归一化
            print(probs)
            label = torch.max(probs, 1)[1].detach().cpu().numpy()[0] # 取[0]得到单个预测标签

            cv2.putText(frame, class_names[label].split(' ')[-1].strip(), (40, 40),  #将类别名称显示在帧的坐标位置(40, 40)
                        cv2.FONT_HERSHEY_SIMPLEX, 2,   # 字体大小为2,颜色为红色,线宽为4
                        (0, 0, 255), 4)
            cv2.putText(frame, "prob: %.4f" % probs[0][label], (40, 100),
                        cv2.FONT_HERSHEY_SIMPLEX, 2,
                        (0, 0, 255), 3)
            clip.pop(0)               # 从clip列表中移除最早的一帧,以保持clip中始终有16帧数据,准备接收新的视频帧

        cv2.imshow('result', frame)   # 显示处理后的帧,并释放资源
        cv2.waitKey(30)               # waitKey(30) 使窗口保持打开状态,每隔30毫秒刷新一次

    cap.release()                     # 退出循环后,释放视频捕获对象并关闭所有 OpenCV 窗口
    cv2.destroyAllWindows()


if __name__ == '__main__':
    main()

代码训练只要把我的路径改一下就可以训练了,简单易上手。

实验结果

在这里插入图片描述
在这里插入图片描述
它在跑步和走路之间跳来跳去,就是不可能是摔跤(还挺欣慰的)。

以上就是本人的心得与总结,如有不足之处请多多包涵。
百度网盘链接代码权值及训练视频、论文:https://pan.baidu.com/s/11fyRwwvX9PlzBj8K5Quhvw?pwd=47u5
提取码: 47u5

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2212101.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于微信小程序的在线点歌系统(论文+源码)-kaic

摘 要 随着社会与互联网的发展&#xff0c;人们已经不在停留在温饱的层面上&#xff0c;而是开始了享受生活&#xff0c;而最能突出网络飞速发展的当属娱乐&#xff0c;从最初的网吧上网到现在的我们可以随时随地上网&#xff0c;观看自己喜欢的各式各样的电影电视剧和短视频&a…

数据结构与算法 - 顺序表与链表的区别

文章目录 前言 一、顺序表与链表的定义 1、顺序表 2、链表 二、区别 1、顺序表(动态顺序表)&#xff1a; 2、链表(带头双向循环链表)&#xff1a; 3、将上述文字用图表形式展示&#xff1a; 4、CPU高速缓存命中率 总结 前言 路漫漫其修远兮&#xff0c;吾将上下而求索…

Nginx实战指南:基础知识、配置详解及最佳实践全攻略

背景 在Java系统实现过程中&#xff0c;我们不可避免地会借助大量开源功能组件。然而&#xff0c;这些组件往往功能丰富且体系庞大&#xff0c;官方文档常常详尽至数百页。而在实际项目中&#xff0c;我们可能仅需使用其中的一小部分功能&#xff0c;这就造成了一个挑战&#…

数组中两个字符串的最小距离(图+文字详解)

链接&#xff1a;数组中两个字符串的最小距离__牛客网 题目:给定一个字符串数组strs&#xff0c;再给定两个字符串str1和str2&#xff0c;返回在strs中str1和str2的最小距离&#xff0c;如果str1或str2为null&#xff0c;或不在strs中&#xff0c;返回-1。 思路: 给定两个下标…

Pycharm 随时调整字体大小(放大或缩小)

实现按住 ctrl 滑动鼠标滚轮实现代码窗口字体大小调整&#xff1a; File 一>Settings 一>Editor一>General里 的Mouse Control把Change font size with CtrlMouse Wheel打上对勾&#xff0c;点击OK即可 使用快捷键 放大字体&#xff1a; Windows/Linux: Ctrl macOS…

IP报文格式、IPv6概述

IPv4报文格式 IPv4报文首部长度至少为20字节(没有可选字段和填充的情况下)&#xff0c;下面来逐一介绍首部各个字段的含义 Version版本&#xff1a;表示采用哪一种具体的IP协议&#xff0c;对于IPv4来说该字段就填充4以表示&#xff0c;如果是IPv6就填充6IHL首部长度&#xff…

Android内容观察者(案例:监听数据库+代码+效果图)

目录 1.内容观察者概念 1. 什么是 ContentObserver&#xff1f; 2. 主要方法 3. 使用场景 4. 工作原理 5. 注册和注销 6. 实现步骤 7. 注意事项 2.创建内容观察者 3.注册内容观察者 4.取消注册内容观察者 5.完整的activity代码 6.案例:检测数据库 1&#xff09;创建一个Android…

HTML(五)列表详解

在HTML中&#xff0c;列表可以分为两种&#xff0c;一种为有序列表。另一种为无序列表 今天就来详细讲解一下这两种列表如何实现&#xff0c;效果如何 1.有序列表 有序列表的标准格式如下&#xff1a; <ol><li>列表项一</li><li>列表项二</li>…

Linux下CMake入门

CMake的基础知识 什么是 CMake CMake 是一个跨平台的构建工具&#xff0c;主要用于管理构建过程。CMake 不直接构建项目&#xff0c;而是生成特定平台上的构建系统&#xff08;如 Unix 下的 Makefile&#xff0c;Windows 下的 Visual Studio 工程&#xff09;&#xff0c;然后…

《OpenCV计算机视觉》—— 人脸检测

文章目录 一、人脸检测流程介绍二、用于人脸检测的关键方法1.加载分类器&#xff08;cv2.CascadeClassifier()&#xff09;2.检测图像中的人脸&#xff08;cv2.CascadeClassifier.detectMultiscale()&#xff09; 三、代码实现 一、人脸检测流程介绍 下面是一张含有多个人脸的…

电子电气架构---软件定义汽车,产业变革

我是穿拖鞋的汉子&#xff0c;魔都中坚持长期主义的汽车电子工程师。 老规矩&#xff0c;分享一段喜欢的文字&#xff0c;避免自己成为高知识低文化的工程师&#xff1a; 屏蔽力是信息过载时代一个人的特殊竞争力&#xff0c;任何消耗你的人和事&#xff0c;多看一眼都是你的不…

影刀RPA实战:操作Mysql数据库

1.摘要 影刀RPA&#xff08;Robotic Process Automation&#xff09;是一种软件自动化工具&#xff0c;它可以模拟人类用户执行各种重复性任务&#xff0c;其中包括对数据库的操作。 我们可以使用软件自动化指令&#xff0c;通过获取数据库窗口对象来操作数据库&#xff0c;也…

JSP 的 response 和 session 内置对象

文章目录 前言一、response 内置对象 1.重定向网页2.处理 HTTP 文件头3.设置输出缓存二、session 内置对象 1.创建及获取客户会话2.会话中移动指定的绑定对象3.销毁 session 内置对象4.会话超时的管理5. session 对象的应用总结 前言 JSP 的 response 和 session 内置对像&…

linux tar 打包文件去掉文件所在路径

一、准备目录 /root/tmp/images /root/tmp/images2 执行命令打包目录/root/tmp/images 到 /root/tmp/images.tar.gz 再解压到/root/tmp/images2 cd /root/tmp/images && tar -cvzf images.tar.gz * && mv images.tar.gz /root/tmp/ tar -C /root/tmp/image…

ctf.bugku-baby lfi 2

题目来源&#xff1a;baby lfi 2 - Bugku CTF平台 访问页面 翻译解析&#xff1a;百度翻译-您的超级翻译伙伴&#xff08;文本、文档翻译&#xff09; (baidu.com) LFI Warmups- level 2 -本地文件包含&#xff08;Local File Inclusion&#xff0c;简称LFI&#xff09; Hello…

力扣面试150 交错字符串 二维DP

Problem: 97. 交错字符串 &#x1f468;‍&#x1f3eb; 参考题解 class Solution {public boolean isInterleave(String s1, String s2, String s3) {int m s1.length();int n s2.length();if(s3.length() ! m n) return false;boolean[][] dp new boolean[m1][n1];dp[0]…

ZYNQ使用XGPIO驱动外设模块(后半部分)

目录 注意重点&#xff1a; 一、SDK代码开发部分&#xff1a; 显示所需的字符编码&#xff1a; 1.用于显示8x16的字符函数&#xff1a; 2.绘制图片: 3.清楚给定两个坐标之间的显示&#xff1a; 4.显示16*32的阿拉伯数字字符&#xff1a; 5.显示16*32的整型数字&#xff…

Excel:vlookup函数实现查找

1.要查找宋江的英语&#xff0c;把鼠标放在对应单元格然后开始编辑 2.选中所选区域&#xff0c;点击F4锁定区域&#xff0c;不然下拉填充的时候会变VLOOKUP 在查找时有严格要求&#xff0c;查找值必须在所选区域的第一列&#xff0c;因此如果你的查找值不在第一列&#xff0c;可…

TGRS 2024 面向雾天遥感图像的定向目标检测算法

TGRS 2024 | 面向雾天遥感图像的定向目标检测算法 论文信息 摘要 目前&#xff0c;大量工作集中在航空目标检测上&#xff0c;并取得了良好的结果。尽管这些方法在传统数据集上取得了有希望的结果&#xff0c;但在恶劣天气条件下捕获的低质量图像中定位对象仍然具有挑战性。目…

RabbitMQ 入门(四)SpringAMQP五种消息类型

一、WorkQueue(工作消息队列) Work queues&#xff0c;也被称为&#xff08;Task queues&#xff09;&#xff0c;任务模型。简单来说就是让多个消费者绑定到一个队列&#xff0c;共同消费队列中的消息。 当消息处理比较耗时的时候&#xff0c;可能生产消息的速度会远远大于…