Pytorch基础知识(14)基于PyTorch的视频分类

  • Post author:
  • Post category:其他


到目前为止,我们只处理了图像。我们建立了各种图像分类、检测和分割模型。我们甚至可以凭空生成新的图像(噪音)。但是图像是静止的。静态图像中没有运动。真正的快乐来自运动。这就是视频发挥作用的方式。

事实上,视频并不比图像复杂多少。视频实际上是一组连续播放的帧或图像的集合。

为了获得流畅的视频,我们需要每秒播放一定数量的帧数;否则,视频看起来脱节。我们日常生活中处理的大多数视频每秒超过30帧。按照这个比例,一个10秒长的短视频相当于300张图片。如此多的图像让事情变得复杂。

尽管如此复杂,我们仍有许多应用程序可以用于视频处理。其中一些应用程序与我们已经学习过的图像处理应用程序密切相关。例如,在多类图像分类中,我们开发了一个多分类模型来分类图像。现在我们在处理视频,我们可能也对视频分类感兴趣。如果你想知道视频中发生了什么活动,而不是图像中出现了什么对象,这样的应用程序很有用。在本章中,我们将使用PyTorch构建一个视频分类模型。

在本章中,我们将介绍以下教程:

  • 自定义数据集
  • 定义模型
  • 训练模型
  • 部署视频分类模型



自定义数据集

和往常一样,第一步是创建数据集。我们需要一个训练数据集来训练我们的模型,以及一个测试或验证数据集来评估模型。为此,我们将使用

HMDB

:一个大型人体运动数据库。

HMDB数据集包括电影、YouTube和谷歌视频等。这是一个相当大的数据集(2 GB),总共有7000个视频剪辑。有51个动作类,每个包含至少101个片段。

51种行为的介绍如下:

在这里插入图片描述
在这里插入图片描述

为了创建用于视频分类的数据集,我们将把视频转换为图像。每个视频都有数百帧图像。处理一段视频的所有帧在计算上是不可行的。为了简化,我们将在每个视频中选择16帧,这些帧在整个视频中间隔相等。然后,我们将定义一个PyTorch数据集类。接下来,我们将为两类深度学习模型定义PyTorch数据加载器:递归神经网络(RNN)模型和三维卷积神经网络(3D-CNN)模型。

在本教程中,您将下载HMDB数据集,将视频剪辑转换为图像,并定义用于视频分类的PyTorch数据集和数据加载器类。



数据集下载

在本节中,我们将下载HMDB数据集。下载数据库的步骤如下:

  1. 访问

    链接
  2. 点击Download菜单
  3. 单击HMDB51下载数据集。

在这里插入图片描述

下载的文件(hmdb51_org.rar)是.rar格式的压缩文件。在Linux机器上,您可能需要在计算机上安装unrar程序才能提取视频。Windows用户可以使用7-Zip解压RAR文件。

接下来,将hmdb51_org.rar文件解压到hmdb51_org文件夹中。该文件夹应包含对应于51个类别的51个子文件夹。此外,每个子文件夹应该包含至少101个.avi类型的视频文件。试着从一个文件夹中随机播放一个视频来熟悉这些视频。

然后,在与脚本相同的目录下创建一个名为data的文件夹,并将hmdb51_org文件夹复制到data文件夹中。

整个工程文件目录:

工程文件目录

在下一节中,我们将把视频转换成图像,然后定义数据集和数据加载器类。



数据准备

让我们读取视频,把它们转换成图像。每个视频可能包含数百个图像。为了简化,我们将在每个视频中选择16帧,这些帧在整个视频中间隔相等,然后将它们以.jpg形式存储。

在数据准备之前,我们写一个辅助文件myutils.py

import os 
import torch
import copy
from tqdm import tqdm_notebook
from torchvision.transforms.functional import to_pil_image
import matplotlib.pylab as plt
device=torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
def get_vids(path2ajpgs):
	listOfCats=os.listdir(path2ajpgs)
	ids=[]
	labels=[]
	for catg in listOfCats:
		path2catg=os.path.join(path2ajpgs,catg)
		listOfSubCats=os.listdir(path2catg)
		path2subCats=[os.path.join(path2catg,los) for los in  listOfSubCats]
		ids.extend(path2subCats)
		labels.extend([catg]*len(listOfSubCats))
	return ids,labels,listOfCats

def denormalize(x_, mean, std):
	x = x_.clone()
	for i in range(3):
		x[i] = x[i]*std[i] + mean[i]
	x = to_pil_image(x)
	return x

def train_val(model, params):
	num_epochs=params["num_epochs"]
	loss_func=params["loss_func"]
	opt=params["optimizer"]
	train_dl=params["train_dl"]
	val_dl=params["val_dl"]
	sanity_check=params["sanity_check"]
	lr_scheduler=params["lr_scheduler"]
	path2weights=params["path2weights"]

	loss_history={"train":[],"val":[]}
	metric_history={"train":[],"val":[]}
	best_model_wts=copy.deepcopy(model.state_dict())
	best_loss=float("inf")
	for epoch in range(num_epochs):
		current_lr=get_lr(opt)
		print("Epoch {}/{}, current lr={}".format(epoch, num_epochs-1,current_lr))
		model.train()
		train_loss,train_metric=loss_epoch(model,loss_func,train_dl,sanity_check,opt)
		loss_history["train"].append(train_loss)
		metric_history["train"].append(train_metric)
		model.eval()
		with torch.no_grad():
			val_loss,val_metric=loss_epoch(model,loss_func,val_dl,sanity_check)
		if val_loss<best_loss:
			best_loss=val_loss
			best_model_wts=copy.deepcopy(model.state_dict())
			torch.save(model.state_dict(),path2weights)
			print("Copied best model weights")
		loss_history["val"].append(val_loss)
		metric_history["val"].append(val_metric)
		lr_scheduler.step(val_loss)
		if current_lr!=get_lr(opt):
			print("Loading best model weights")
			model.load_state_dict(best_model_wts)
		print("Train loss:%.6f, dev loss:%.6f, accuracy:%.2f" % (train_loss, val_loss, 100*val_metric))
		print("-"*10)
	model.load_state_dict(best_model_wts)
	return model, loss_history, metric_history

def get_lr(opt):
	for param_group in opt.param_groups:
		return param_group["lr"]

def metrics_batch(output, target):
	pred=output.argmax(dim=1,keepdim=True)
	corrects=pred.eq(target.view_as(pred)).sum().item()
	return corrects
def loss_batch(loss_func, output, target, opt=None):
	loss=loss_func(output, target)
	with torch.no_grad():
		metric_b=metrics_batch(output,target)
	if opt is not None:
		opt.zero_grad()
		loss.backward()
		opt.step()
	return loss.item(), metric_b
def loss_epoch(model, loss_func, dataset_dl, sanity_check=False,opt=None):
	running_loss=0.0
	running_metric=0.0
	len_data=len(dataset_dl.dataset)
	for xb,yb in dataset_dl:
		xb=xb.to(device)
		yb=yb.to(device)
		output=model(xb)
		loss_b,metric_b=loss_batch(loss_func,output,yb,opt)
		running_loss+=loss_b
		if metric_b is not None:
			running_metric+=metric_b
		if sanity_check is True:
			break
	loss=running_loss/float(len_data)
	metric=running_metric/float(len_data)
	return loss, metric
def plot_loss(loss_hist, metric_hist):
	num_epochs=len(loss_hist["train"])
	plt.title("Train-Val Loss")
	plt.plot(range(1,num_epochs+1),loss_hist["train"],label="train")
	plt.plot(range(1,num_epochs+1),loss_hist["val"],label="val")
	plt.ylabel("Loss")
	plt.xlabel("Training Epochs")
	plt.legend()
	plt.show()
	plt.title("Train-Val Accuracy")
	plt.plot(range(1,num_epochs+1),metric_hist["train"],label="train")
	plt.plot(range(1,num_epochs+1),metric_hist["val"],label="val")
	plt.ylabel("Accuracy")
	plt.xlabel("Training Epochs")
	plt.legend()
	plt.show()

from torch import nn
class Resnet18Rnn(nn.Module):
	def __init__(self,params_model):
		super(Resnet18Rnn,self).__init__()
		num_classes=params_model["num_classes"]
		dr_rate=params_model["dr_rate"]
		pretrained=params_model["pretrained"]
		rnn_hidden_size=params_model["rnn_hidden_size"]
		rnn_num_layers=params_model["rnn_num_layers"]
		baseModel=models.resnet18(pretrained=pretrained)
		num_features=baseModel.fc.in_features
		baseModel.fc=Identity()
		self.baseModel=baseModel
		self.dropout=nn.Dropout(dr_rate)
		self.rnn=nn.LSTM(num_features,rnn_hidden_size,rnn_num_layers)
		self.fc1=nn.Linear(rnn_hidden_size, num_classes)
	def forward(self,x):
		b_z,ts,c,h,w=x.shape
		ii=0
		y=self.baseModel((x[:,ii]))
		output,(hn,cn)=self.rnn(y.unsqueeze(1))
		for ii in range(1,ts):
			y=self.baseModel((x[:,ii]))
			out,(hn,cn)=self.rnn(y.unsqueeze(1),(hn,cn))
		out=self.dropout(out[:-1])
		out=self.fc1(out)
		return out
class Identity(nn.Module):
	def __init__(self):
		super(Identity,self).__init__()
	def forward(self,x):
		return x
from torchvision import models
from torch import nn
def get_model(num_classes, model_type="rnn"):
	if model_type=="rnn":
		params_model={
			"num_classes":num_classes,
			"dr_rate":0.1,
			"pretrained":True,
			"rnn_num_layers":1,
			"rnn_hidden_size":100,
			}
		model=Resnet18Rnn(params_model)
	else:
		model=models.video.r3d_18(pretrained=True,progress=False)
		num_features=model.fc.in_features
		model.fc=nn.Linear(num_features,num_classes)
	return model

import cv2
import numpy as np
def get_frames(filename,n_frames=1):
	frames=[]
	v_cap=cv2.VideoCapture(filename)
	v_len=int(v_cap.get(cv2.CAP_PROP_FRAME_COUNT))
	frame_list=np.linspace(0,v_len-1,n_frames+1,dtype=np.int16)
	for fn in range(v_len):
		success, frame=v_cap.read()
		if success is False:
			continue
		if (fn in frame_list):
			frame=cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
			frames.append(frame)
	v_cap.release()
	return frames,v_len

import torchvision.transforms as transforms
from PIL import Image
def transform_frames(frames, model_type="rnn"):
    if model_type == "rnn":
        h, w = 224, 224
        mean = [0.485, 0.456, 0.406]
        std = [0.229, 0.224, 0.225]
    else:
        h, w = 112, 112
        mean = [0.43216, 0.394666, 0.37645]
        std = [0.22803, 0.22145, 0.216989]
    test_transformer = transforms.Compose([
        transforms.Resize((h, w)),
        transforms.ToTensor(),
        transforms.Normalize(mean, std)])
    frames_tr = []
    for frame in frames:
        frame = Image.fromarray(frame)
        frame_tr = test_transformer(frame)
        frames_tr.append(frame_tr)
    imgs_tensor = torch.stack(frames_tr)
    if model_type == "3dcnn":
        imgs_tensor = torch.transpose(imgs_tensor, 1, 0)
    imgs_tensor = imgs_tensor.unsqueeze(0)
    return imgs_tensor

def store_frames(frames,path2store):
	for ii,frame in enumerate(frames):
		frame=cv2.cvtColor(frame,cv2.COLOR_RGB2BGR)
		path2img=os.path.join(path2store,"frame"+str(ii)+".jpg")
		cv2.imwrite(path2img, frame)

开始数据准备:

#1. 获取数据集的目录列表
import os
path2data="./data"
sub_folder="hmdb51_org"
sub_folder_jpg="hmdb51_jpg"
path2aCatgs=os.path.join(path2data,sub_folder)
listOfCategories=os.listdir(path2aCatgs)
print(listOfCategories,len(listOfCategories))
# ['brush_hair', 'cartwheel', ..., 'wave'] 51
#2. 获取每个类别的子文件数
for cat in listOfCategories:
	print("category:",cat)
	path2aCat=os.path.join(path2aCatgs,cat)
	listOfSubs=os.listdir(path2aCat)
	print("number of sub-folders:",len(listOfSubs))
	print("-"*50)
# category: brush_hair
# number of sub-folders: 107
# --------------------------------------------------
# category: cartwheel
# number of sub-folders: 107
# --------------------------------------------------
# ...
#3. 导入myutils.py
import myutils
#4. 循环读取视频,获取帧,并将它们存储为jpg文件:
extension=".avi"
n_frames=16
for root,dirs,files in os.walk(path2aCatgs,topdown=False):
	for name in files:
		if extension not in name:
			continue
		path2vid=os.path.join(root,name)
		frames,vlen=myutils.get_frames(path2vid,n_frames=n_frames)
		path2store=path2vid.replace(sub_folder,sub_folder_jpg)
		path2store=path2store.replace(extension,"")
		print(path2store)
		os.makedirs(path2store, exist_ok=True)
		myutils.store_frames(frames,path2store)		



分割数据

在前一节准备数据中,我们将视频转换为图像。对于每个视频,我们提取了16帧。在这里,我们将把数据集分成训练集和测试集。myutils.py脚本包含了所需的一些实用函数:

#1. 定义数据路径并导入myutils
import os
import imutils
path2data="./data"
sub_folder_jpg="hmdb51_jpg"
path2ajpgs=os.path.join(path2data, sub_folder_jpg)

#2. 每个视频的帧都存储在与该视频同名的文件夹中。调用myutils中的get_vids辅助函数来获得一个视频文件名和标签列表:
all_vids, all_labels, catgs=myutils.get_vids(path2ajpgs)
print(len(all_vids), len(all_labels), len(catgs))
# (6766, 6766, 51)
print(all_vids[:3], all_labels[:3],catgs[:3])
# ['./data\\hmdb51_jpg\\brush_hair\\April_09_brush_hair_u_nm_np1_ba_goo_0', 
# './data\\hmdb51_jpg\\brush_hair\\April_09_brush_hair_u_nm_np1_ba_goo_1', 
# './data\\hmdb51_jpg\\brush_hair\\April_09_brush_hair_u_nm_np1_ba_goo_2'] 
# ['brush_hair', 'brush_hair', 'brush_hair'] ['brush_hair', 'cartwheel', 'catch']

#3. 定义一个Python字典来保存标签的数值
labels_dict={}
ind=0
for uc in catgs:
	labels_dict[uc]=ind
	ind+=1
print(labels_dict)
# {'brush_hair': 0, 'cartwheel': 1, 'catch': 2,..., 'walk': 49, 'wave': 50}
#4. 我们可以看到,总共有51个类别。为了简化这个问题,我们将选择5个动作类并过滤视频:
num_classes=5
unique_ids=[id_ for id_, label in zip(all_vids,all_labels) if labels_dict[label]<num_classes]
unique_labels=[label for id_,label in zip(all_vids,all_labels) if labels_dict[label]<num_classes]
print(len(unique_ids),len(unique_labels))
# 555 555
#5. 将数据集分为训练数据集和测试数据集
from sklearn.model_selection import StratifiedShuffleSplit
sss=StratifiedShuffleSplit(n_splits=2,test_size=0.1,random_state=0)
train_indx,test_indx=next(sss.split(unique_ids,unique_labels))
train_ids=[unique_ids[ind] for ind in train_indx]
train_labels=[unique_labels[ind] for ind in train_indx]
print(len(train_ids),len(train_labels))

test_ids=[unique_ids[ind] for ind in test_indx]
test_labels=[unique_labels[ind] for ind in test_indx]
print(len(test_ids), len(test_labels))
# 499 499
# 56 56

在下一节中,我们将定义一个PyTorch数据集类,并为训练和测试数据集实例化两个对象。



定义PyTorch数据集

在前面的数据分割部分中,我们将数据分割为训练集和测试集。这里,我们将定义一个PyTorch数据集类。然后,我们将为训练和测试数据集实例化类的两个对象:

#1. 导入需要的包
from torch.utils.data import Dataset, DataLoader, Subset
import glob
from PIL import Image
import torch
import numpy as np
import random
np.random.seed(2021)
random.seed(2021)
torch.manual_seed(2021)

#2. 定义数据集类
class VideoDataset(Dataset):
	def __init__(self,ids,labels,transform):
		self.transform=transform
		self.ids=ids
		self.labels=labels
	def __len__(self):
		return len(self.ids)
	def __getitem__(self,idx):
		path2imgs=glob.glob(self.ids[idx]+"/*.jpg")
		path2imgs=path2imgs[:timesteps]
		label=labels_dict[self.labels[idx]]
		frames=[]
		for p2i in path2imgs:
			frame=Image.open(p2i)
			frames.append(frame)
		seed=np.random.randint(1e9)
		frames_tr=[]
		for frame in frames:
			random.seed(seed)
			np.random.seed(seed)
			frame=self.transform(frame)
			frames_tr.append(frame)
		if len(frames_tr)>0:
			frames_tr=torch.stack(frames_tr)
		return frames_tr, label
#3. 定义变换参数
model_type="3dcnn"
# model_type="rnn"  # 二选一
timesteps=16
if model_type=="rnn":
	h,w=224,224
	mean=[0.485,0.456,0.406]
	std=[0.229,0.224,0.225]
else:
	h,w=112,112
	mean=[0.43216,0.394666,0.37645]
	std=[0.22803,0.22145,0.216989]
		
#4. 为训练集定义变换函数
import torchvision.transforms as transforms
train_transformer=transforms.Compose([
					transforms.Resize((h,w)),
					transforms.RandomHorizontalFlip(p=0.5),
					transforms.RandomAffine(degrees=0, translate=(0.1,0.1)),
					transforms.ToTensor(),
					transforms.Normalize(mean,std),
					])		
#5. 实例化数据集类
train_ds=VideoDataset(ids=train_ids,labels=train_labels,transform=train_transformer)
print(len(train_ds))
# 499
#6. 获取train_ds中的一个数据
imgs,label=train_ds[1]
if len(imgs)>0:
	print(imgs.shape,label,torch.min(imgs),torch.max(imgs))
# torch.Size([16, 3, 112, 112]) 3 tensor(-1.8952) tensor(2.8194)

#7. 显示一些帧数据
import matplotlib.pylab as plt
plt.figure(figsize=(10,10))
for ii,img in enumerate(imgs[::4]):
	plt.subplot(2,2,ii+1)
	plt.imshow(myutils.denormalize(img,mean,std))
	plt.title(label)
plt.show()
#8. 为测试集定义变换函数
test_transformer=transforms.Compose([
				transforms.Resize((h,w)),
				transforms.ToTensor(),
				transforms.Normalize(mean,std),
				])
#9. 实例化数据集类test_ds
test_ds=VideoDataset(ids=test_ids,labels=test_labels,transform=test_transformer)
print(len(test_ds))
# 56
#10.  获取test_ds中的一个数据
imgs,label=test_ds[1]
print(imgs.shape,label,torch.min(imgs),torch.max(imgs))
# torch.Size([16, 3, 112, 112]) 3 tensor(-1.8952) tensor(2.8736)
#11. 显示test_ds一些帧数据
import matplotlib.pylab as plt
plt.figure(figsize=(10,10))
for ii,img in enumerate(imgs[::4]):
	plt.subplot(2,2,ii+1)
	plt.imshow(myutils.denormalize(img,mean,std))
	plt.title(label)
plt.show()

训练集数据
验证集数据



定义数据集加载器

如你所知,我们从数据加载器提取小批量数据训练模型。根据模型类型,我们将定义两个数据加载器实例对象:

# 定于collate_fn_3dcnn辅助函数
def collate_fn_3dcnn(batch):
	imgs_batch,label_batch=list(zip(*batch))
	imgs_batch=[imgs for imgs in imgs_batch if len(imgs)>0]
	label_batch=[torch.tensor(l) for l,imgs in zip(label_batch,imgs_batch) if len(imgs)>0]
	imgs_tensor=torch.stack(imgs_batch)
	imgs_tensor=torch.transpose(imgs_tensor,2,1)
	labels_tensor=torch.stack(label_batch)
	return imgs_tensor,labels_tensor

# 定义collate_fn_rnn辅助函数
def collate_fn_rnn(batch):
	imgs_batch,label_batch=list(zip(*batch))
	imgs_batch=[imgs for imgs in imgs_batch if len(imgs)>0]
	label_batch=[torch.tensor(l) for l, imgs in zip(label_batch,imgs_batch) if len(imgs)>0]
	imgs_tensor=torch.stack(imgs_batch)
	labels_tensor=torch.stack(label_batch)
	return imgs_tensor,labels_tensor
	
	
#1. 定义数据加载器
batch_size=16
if model_type=="rnn":
	train_dl=DataLoader(train_ds, batch_size=batch_size,shuffle=True,collate_fn=collate_fn_rnn)
	test_dl=DataLoader(test_ds,batch_size=2*batch_size,shuffle=False,collate_fn=collate_fn_rnn)
else:
	train_dl=DataLoader(train_ds, batch_size=batch_size,shuffle=True,collate_fn=collate_fn_3dcnn)
	test_dl=DataLoader(test_ds,batch_size=2*batch_size,shuffle=False,collate_fn=collate_fn_3dcnn)

#2. 现在,设置模型类型为“3dcnn”,并从train_dl获取一个小批数据:
for xb,yb in train_dl:
	print(xb.shape, yb.shape)
	break
# torch.Size([16, 3, 16, 112, 112]) torch.Size([16])
# 重复前面的步骤,但这次将model_type设置为“rnn”。您将看到以下输出:
# torch.Size([16, 16, 3, 224, 224]) torch.Size([16])

代码解析:



准备数据

小节,我们将视频转换为图像。因为加载视频是一个耗时的过程,所以我们提前完成了这一步。加载图像比加载视频快得多。在步骤1中,我们得到了动作类别的列表(标签列表)。如预期的那样,有51个动作类别(标签类别)。在步骤2中,我们得到了每个动作类别中视频的数量。不出所料,每个动作类别有超过100个视频。

在步骤3中,我们导入了myutils。这个程序文件包含了所需要的一些辅助函数。为了节省空间,我们将一些辅助函数放在myutils.py文件中。导入之后,我们使用定义的辅助函数。get_frames 辅助函数从文件名加载视频并返回指定的帧数。store_frames辅助函数获取帧并将它们存储在给定的路径中。这里需要注意。OpenCV包以BGR格式加载图像,所以我们在get_frames辅助函数中使用cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)将图像转换为RGB。OpenCV在保存图像时采用BGR格式,所以我们在store_frames辅助函数中使用cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)将图像转为BGR格式。

在步骤4中,我们对视频进行遍历,每个视频获得16帧,并将它们存储为jpg图像。



数据分段

小节中,我们得到了视频文件名的列表,并将其分成训练集和测试集。在步骤1中,我们导入了myutils。我们假设您完成了前面的步骤,所需的图像现在存储在hmdb51_jpg文件夹中。在步骤2中,我们调用了get_vids辅助函数。使用这个辅助函数,我们得到了视频文件名列表。你可能还记得,我们为每个视频提取了16帧。对于每个视频,这16帧被存储在一个与视频文件名同名的文件夹中。如我们所见,有6766个视频和标签。每个视频都有一个文本标签,与视频的动作(标签)相对应。此外,还打印了all_vid和all_labels列表的摘录。

在步骤3中,我们定义了一个Python字典来保存标签。由于标签是文本格式的,并且为了后续的操作,我们需要一个数字来代替文本格式的标签,所以我们为每个类任意分配了一个数字。在步骤4中,为了简化问题,我们从总共51个类中选择了5个类。可以增加或减少类的数量。就像我们看到的,当我们把视频过滤到五个类别时,还有555个视频。这减少了数据大小和问题的复杂性。在步骤5中,我们使用sklearn中的StratifiedShuffleSplit函数将数据分解为训练集和测试集。我们分出10%来做测试。如我们所见,分裂后,499个视频在训练数据集中,56个视频在测试数据集中。



定义数据集

小节中,我们创建了数据集类。在步骤1中,我们导入了所有必要的包,并为重现性设置了随机的种子点。在步骤2中,我们定义了数据集类,即VideoDataset。这个类有三个方法或函数。

_init__函数的输入如下:

  • ids:视频文件名列表
  • labels:对应于ids的类别标签列表
  • transform:图像变换函数

__getitem__函数的输入如下:

  • idx: 包含16张jpg图像的视频文件夹的路径

在这个函数中,我们得到了.jpg图像的列表,然后将它们作为PIL图像加载。然后,我们对每个图像进行图像变换。请注意,我们希望对视频的所有16帧执行相同类型的转换。因此,我们在每次调用转换时设置随机种子点。这将确保所有16个图像将进行相同的转换。

在步骤3中,我们定义了一些图像转换所需的参数。这些包括调整图像大小的h, w和标准化图像的mean, std。注意,根据模型的不同,参数的设置是不同的。您可以选择“3dcnn”或“rnn”作为模型类型。稍后,我们将在

定义模型

的教程中详细讲解。对于模型类型为“rnn”模型,我们调整图像大小为224乘224,而对于“3dcnn”模型,我们将图像大小调整为112 * 112。我们这样做的原因是,不同模型有不同的训练配置。

在步骤4中,我们定义了图像变换。注意,除了调整图像大小和规范化,我们还使用了两个数据增强转换函数:RandomHorizontalFlip和RandomAffine。在第5步中,我们实例化了VideoDataset类的一个对象,即train_ds。如预期,在训练数据集中有555个视频。在第6步中,我们从train_ds中获得了一个样本。这是确保返回张量格式正确的探索性步骤。返回的张量形状为[timesteps, 3, h, w],其中timesteps=16, h和w依赖于model_type。

在步骤7中,我们从返回的张量中显示了一些样本帧。在步骤8中,我们为测试数据集定义了变换函数。在这里,我们不需要执行数据扩充。在第9步中,我们将VideoDataset类的对象实例化为test_ds。如预期的那样,测试数据集包含56个视频。在第10步中,我们从test_ds中获得一个样本。在第11步中,我们展示了一些样本张量的帧。

在下一节中,我们将定义模型。



定义模型

与图像分类相比,视频分类更加复杂,因为我们需要同时处理多幅图像。你可以从“二分类图像”与”多类图像分类“回忆起,我们使用了一个基于二维卷积神经网络(2D-CNN)的模型。一种简单的方法是使用2D-CNN模型一次处理一幅视频图像,然后平均输出。然而,这种方法没有考虑帧之间的时间相关性。相反,我们倾向于使用一个模型来处理一个视频的多帧图像,以提取时间相关性。为此,我们将使用两种不同的模型进行视频分类任务。

第一个模型是基于RNN架构的。RNN模型的目标是通过保留过去图像的记忆来提取图像之间的时间相关性。模型框图如下:

在这里插入图片描述

正如我们所看到的,视频图像被输入到一个基本模型来提取高级特征。然后将特征输入到RNN层,RNN层的输出连接到全连通层,得到分类输出。这个模型的输入应该是[batch_size, timesteps, 3, height, width]的形状,其中timesteps=16是每个视频的帧数。我们将使用在ImageNet数据集上预先训练过的称为ResNet18的模型作为基础模型。

第二个模型是一个18层的

Resnet3D模型

。让我们称这个模型为3dcnn。这个模型的输入应该是[batch_size, 3, timesteps, height, width]。此模型内置在torchvision.models.video包中。在本教程中,您将学习如何为视频分类定义两个模型。

# 为视频分类定义两个模型
#1. 定义Resnet8Rnn:
from torch import nn
class Resnet18Rnn(nn.Module):
	def __init__(self,params_model):
		super(Resnet18Rnn,self).__init__()
		num_classes=params_model["num_classes"]
		dr_rate=params_model["dr_rate"]
		pretrained=params_model["pretrained"]
		rnn_hidden_size=params_model["rnn_hidden_size"]
		rnn_num_layers=params_model["rnn_num_layers"]
		baseModel=models.resnet18(pretrained=pretrained)
		num_features=baseModel.fc.in_features
		baseModel.fc=Identity()
		self.baseModel=baseModel
		self.dropout=nn.Dropout(dr_rate)
		self.rnn=nn.LSTM(num_features,rnn_hidden_size,rnn_num_layers)
		self.fc1=nn.Linear(rnn_hidden_size, num_classes)
	def forward(self,x):
		b_z,ts,c,h,w=x.shape
		ii=0
		y=self.baseModel((x[:,ii]))
		output,(hn,cn)=self.rnn(y.unsqueeze(1))
		for ii in range(1,ts):
			y=self.baseModel((x[:,ii]))
			out,(hn,cn)=self.rnn(y.unsqueeze(1),(hn,cn))
		out=self.dropout(out[:-1])
		out=self.fc1(out)
		return out
class Identity(nn.Module):
	def __init__(self):
		super(Identity,self).__init__()
	def forward(self,x):
		return x
#2. 使用条件语句来定义任意一个模型
from torchvision import models
from torch import nn

if model_type=="rnn":
	params_model={
		"num_classes":num_classes,
		"dr_rate":0.1,
		"pretrained":True,
		"rnn_num_layers":1,
		"rnn_hidden_size":100,
		}
	model=Resnet18Rnn(params_model)
else:
	model=models.video.r3d_18(pretrained=True,progress=False)
	num_features=model.fc.in_features
	model.fc=nn.Linear(num_features,num_classes)
#3. 使用一些虚拟输入测试模型
with torch.no_grad():
	if model_type=="rnn":
		x=torch.zeros(1,16,3,h,w)
	else:
		x=torch.zeros(1,3,16,h,w)
	y=model(x)
	print(y.shape)
# torch.Size([1,5])
#4. 将模型移到GPU设备
device=torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
model=model.to(device)
#5.打印模型
print(model)
# 根据model_type的不同,将打印相应的模型。下面是打印3dcnn模型结果:
# VideoResNet(
# (stem): BasicStem(
# (0): Conv3d(3, 64, kernel_size=(3, 7, 7), stride=(1, 2, 2), padding=(1, 3, 3), bias=False)
# (1): BatchNorm3d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
# (2): ReLU(inplace=True)
# )
# ...
# rnn模型的打印结果如下所示:
# Resnt18Rnn(
# (baseModel): ResNet(
# (conv1): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
# (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
# (relu): ReLU(inplace=True)
# (maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)
# ...

代码解析:

在步骤1中,我们定义了RNN模型类,即Resnet18Rnn。我们使用在ImageNet数据集上预先训练过的Resnet18模型作为特征提取器。然后将提取的特征输入RNN层,提取时间相关性。将RNN层的输出输入到全连通层,得到分类输出。在步骤2中,我们使用if条件选择其中一个模型。如果model_type设置为“rnn”,则使用Resnet18RNN类实例化rnn模型;如果model_type设置为“3dcnn”,则使用PyTorch的内置模型。

在步骤3中,我们测试了定义的模型,以确保一切都是正确的。我们向模型传递了一些虚拟输入,得到了期望的输出。在继续下一步之前,使用此步骤调试您的模型。在步骤4中,我们定义了一个CUDA设备,并将模型移动到CUDA设备。在步骤5中,我们打印了模型。根据model_type的不同,会打印出相应的模型。

下一步,将训练模型



训练模型

到目前为止,我们已经定义了数据集、数据加载器和模型。您可能会注意到,这个过程与图像分类类似,只是数据格式和模型有一些变化。我们也可以使用

多类图像分类

中定义的相同的损失函数和优化器,这并不奇怪。此外,对于训练过程,我们将使用相同的随机梯度下降算法。为了避免重复,我们将大部分训练脚本放在myutils.py文件中。

在这里,您将学习如何训练视频分类模型。

# 1. 定义损失函数、优化器和学习率计划:
from torch import optim
from torch.optim.lr_scheduler import CosineAnnelingLR, ReduceLROnPlateau
loss_func=nn.CrossEntropyLoss(reduction="sum")
opt=optim.Adam(model.parameters(),lr=3e-5)
# 余弦退火学习率中LR的变化是周期性的,T_max是周期的1/2;eta_min(float)表示学习率的最小值,默认为0;
# last_epoch(int)代表上一个epoch数,该变量用来指示学习率是否需要调整。当last_epoch符合设定的间隔时,
# 就会对学习率进行调整。当为-1时,学习率设为初始值。
# lr_scheduler = CosineAnnealingLR(opt, T_max=20, verbose=True)
lr_scheduler=ReduceLROnPlateau(opt,mode="min",factor=0.5,patience=5,verbose=1)
os.makedirs("./models",exist_ok=True)
#2. 调用myutils中的train_val辅助函数训练模型
params_train={
	"num_epochs":20,
	"optimizer":opt,
	"loss_func":loss_func,
	"train_dl":train_dl,
	"val_dl":test_dl,
	"sanity_check":True,
	"lr_scheduler":lr_scheduler,
	"path2weights":"./models/weights_"+model_type+".pt",
}
model,loss_hist,metric_hist=myutils.train_val(model,params_train)
# 运行完前面的代码片段后,训练将开始,您应该会在屏幕上看到它的进度。
#3. 训练结束后,绘制训练进度
myutils.plot_loss(loss_hist, metric_hist)
# 前面的片段将显示一个损失和准确性的图。

一旦您完成了对模型的训练,您就可以重做这些步骤来训练其他模型。你可以在

创建数据集



定义数据加载器

小节中将model_type更改为“rnn”或“3dcnn”,然后执行所有步骤。

代码解析:

在步骤1中,我们定义了损失函数、优化器和学习率计划。我们使用与

多类图像分类

相同的定义。详情请参阅那一章。

在步骤2中,我们调用了myutils中的train_val辅助函数。该函数在

多类图像分类

中进行了详细的说明。

在步骤3中,我们调用myutils中定义的辅助函数来绘制训练进度。



部署视频分类模型

我们已经训练了两种不同的模型。现在,是时候在视频中部署模型了。为了避免重复,我们将所需的函数放在myutils.py文件中。要在训练脚本的单独脚本中部署模型,我们需要实例化模型类的一个对象。可以通过调用myutils.py文件中定义的get_model函数来实现这一点。然后,我们将训练后的权重加载到模型中。

在本教程中,您将学习如何部署我们的视频分类模型。

# 让我们实例化模型的一个对象,将预先训练好的权重加载到模型中,并将模型部署到一个视频中:
#1. 加载模型
import myutils
model_type="rnn"
model=myutils.get_model(model_type=model_type, num_classes=5)
model.eval()
# 在前面的代码片段中,您可以将model_type设置为“3dcnn”来加载第二个模型。
#2. 导入权重
import torch
device=torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
path2weights="./models/weights_"+model_type+".pt"
model.load_state_dict(torch.load(path2weights))
model.to(device)
#3.从视频中获取帧数据
path2video="./data/hmdb51_org/brush_hair/April_09_brush_hair_u_nm_np1_ba_goo_0.avi"
frames,v_len=myutils.get_frames(path2video, n_frames=16)
print(len(frames), vlen)
# (16, 409)
#4. 使用myutils中定义的辅助函数将帧转换为张量
imgs_tensor=myutils.transform_frames(frames, model_type)
print(imgs_tensor.shape, torch.min(imgs_tensor),torch.max(imgs_tensor))
# torch.Size([1, 16, 3, 224, 224]) tensor(-2.1179) tensor(2.6400)
#5. 获取模型预测结果
with torch.no_grad():
	out=model(imgs_tensor.to(device)).cpu()
	print(out.shape)
	pred=torch.argmax(out).item()
	print(pred)
# torch.Size([1,5])
# 3

代码解析:

在第1步中,我们调用myutils.py文件中定义的get_model辅助函数来实例化一个模型对象。模型的输入如下:

  • model_type:“rnn”,”3dcnn”二选一
  • num_classes:类别数目,此处是5

在步骤2中,我们将预先训练好的权重加载到模型中。在第3步中,我们调用myutils.py文件中定义的get_frames 辅助函数来获得视频的16帧。在步骤4中,我们显示了一些示例图像。

在第5步中,我们调用myutils.py文件中定义的transform_frames函数,将帧转换为PyTorch张量。这些转换与我们在定义PyTorch数据集小节中创建数据集教程中定义的转换相同。在步骤6中,我们将张量传递给模型并得到它的输出。



版权声明:本文为weixin_43229348原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。