可以从本人以前的文章中可以看出作者以前从事的是嵌入式控制方面相关的工作,是一个机器视觉小白,之所以开始入门机器视觉的学习主要是一个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+2⋅1−3)/1+1=16
• 高度(H): (128+2⋅1−3)/1+1=128
• 宽度(W): (171+2⋅1−3)/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.5→85 (取整)
• 输出形状: (8, 64, 16, 64, 85)
Conv2
• 卷积层: self.conv2 = nn.Conv3d(64, 128, kernel_size=(3, 3, 3), padding=(1, 1, 1))
• 输出形状计算:
• 输出通道数: 128
• 深度(D): 16+2⋅1−3=16
• 高度(H): 64+2⋅1−3=64
• 宽度(W): 85+2⋅1−3=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+2⋅1−3=8
• 高度(H): 32+2⋅1−3=32
• 宽度(W): 42+2⋅1−3=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+2⋅1−3=8
• 高度(H): 32+2⋅1−3=32
• 宽度(W): 42+2⋅1−3=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+2⋅1−3=4
• 高度(H): 16+2⋅1−3=16
• 宽度(W): 21+2⋅1−3=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+2⋅1−3=4
• 高度(H): 16+2⋅1−3=16
• 宽度(W): 21+2⋅1−3=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.5→10 (取整)
• 输出形状: (8, 512, 2, 8, 10)
Conv5a
• 卷积层: self.conv5a = nn.Conv3d(512, 512, kernel_size=(3, 3, 3), padding=(1, 1, 1))
• 输出形状计算:
• 输出通道数: 512
• 深度(D): 2+2⋅1−3=2
• 高度(H): 8+2⋅1−3=8
• 宽度(W): 10+2⋅1−3=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+2⋅1−3=2
• 高度(H): 8+2⋅1−3=8
• 宽度(W): 10+2⋅1−3=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+2⋅0−2)/2+1=4
• 宽度(W): (10+2⋅0−4)/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