动作识别(二)复现C3D网络

动作识别(二)——复现C3D网络

  • 一、本代码运行环境
  • 二、代码目录解释
  • 三、可视化日志文件步骤
  • 四、注意事项
  • 五、C3D训练网络部分代码
  • 六、训练可视化结果
  • 七、第一步进行测试
  • 八、第二步进行训练
  • 九、致谢和说明
  • 十、数据和源码地址

本代码是本人在前人的基础上,复现了《Learning Spatiotemporal Features with 3D Convolutional Networks》此篇文献。本代码较原文最大的区别在于,
数据集本人只使用了UC101的前两类,其余基本是一致的。现在分享在这里,主要是供大家学习和交流,很感谢大家的喜欢,我是DMK!

动作识别(二)复现C3D网络_第1张图片 图1 C3D架构

一、本代码运行环境

(1)win10;
(2)cuda10.1+RTX2060显卡,自查;
(3)conda 创建环境: conda create -n pytorch python=3.6.2;
(4)安装pyrtorch: pip install torch1.7.1+cu101 torchvision0.8.2+cu101 torchaudio==0.7.2 -f https://download.pytorch.org/whl/torch_stable.html
(5)安装其他库: pip install tensorboard, tensorboardX, tqdm, sklearn, opencv -i https://pypi.tuna.tsinghua.edu.cn/simple

二、代码目录解释

|-assets:存放了两个测试效果视频,你可以观看
|-data:存放UCF101数据集(其他的也可以啊)
|-data_process:存放处理好的数据
|----ucf101:ucf101目录(这部分是运行dataset.py文件自动生成的,所以你不用担心)
|----test:测试集
|----train:训练集
|----val:验证集
|-dataloaders:生成数据文件目录
|-network:存放3种网络模型,本人只用到了C3D,其余大家可以研究啊
|----C3D_model.py
|----R3D_model.py
|----R2Plus1D_model.py
|-run:日志文件和存放模型的文件
|----run_0
|----models:存放日志和训练模型
|-mypath.py:调整路径文件
|-test.py
|-train.py
|-readme.md

三、可视化日志文件步骤

(1)打开终端,切换当前环境和代码目录;
(2)输入tensorboard --logdir model 命令在网页查看训练过程

四、注意事项

(1)该网络采用3D卷积来处理16帧固定图片,故网络输入大小为(batch_size, 3, 16, 112, 112),输出大小为(batch_size, 2)
(2)测试其实采用的是滑动窗口方式去识别动作视频;

五、C3D训练网络部分代码

import timeit
from datetime import datetime
import socket
import os
import glob
from tqdm import tqdm

import torch
from tensorboardX import SummaryWriter
from torch import nn, optim
from torch.utils.data import DataLoader
from torch.autograd import Variable

from dataloaders.dataset import VideoDataset
from network import C3D_model, R2Plus1D_model, R3D_model

# 选择使用的设备去训练网络
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
# 打印当前使用的设备
print("Device being used:", device)

nEpochs = 1  # 训练总轮数
resume_epoch = 50  # 从第几轮开始去训练,默认是0
useTest = True  # 在训练时是否进行测试
nTestInterval = 2  # 每nTestInterval去测试一次
snapshot = 5  # 每snapshot轮去保存一次模型
lr = 1e-4  # 初始学习率
dataset = 'ucf101'  # 当前使用的数据集名字: hmdb51 or ucf101

if dataset == 'hmdb51':
    num_classes = 51
elif dataset == 'ucf101':
    num_classes = 2
else:
    print('We only implemented hmdb and ucf101 and mydata datasets.')
    raise NotImplementedError

save_dir_root = os.path.join(os.path.dirname(os.path.abspath(__file__)))  # 获取当前文件的绝对路径,eg.
exp_name = os.path.dirname(os.path.abspath(__file__)).split('/')[-1]  # 根据train.py的绝对路径

# 这部分主要是为了搜寻run文件下是否有文件,依此判断runs和run_id的值,这样就可以决定后面的模型是在哪一轮的基础上训练,大家可以自行调试查看
# 本人在这里有修改,即无论你运行多少次,只能接着下面生成run_0文件夹中的模型进行训练,比如当前你run_0文件夹中有第二轮保存的模型,那么你下次训练的resum_epoch只能等于2
if resume_epoch != 0:
    runs = sorted(glob.glob(os.path.join(save_dir_root, 'run', 'run_*')))
    run_id = int(runs[-1].split('_')[-1]) if runs else 0
else:
    runs = sorted(glob.glob(os.path.join(save_dir_root, 'run', 'run_*')))
    run_id = int(runs[-1].split('_')[-1]) if runs else 0

save_dir = os.path.join(save_dir_root, 'run', 'run_' + str(run_id))  # 在run文件下,建立run_0文件夹,以保存训练的模型和日志文件
modelName = 'C3D'  # 选哪一个网络?: C3D or R2Plus1D or R3D
saveName = modelName + '-' + dataset  # 保存模型的命名格式:如C3D-ucf101

def train_model(dataset=dataset, save_dir=save_dir, num_classes=num_classes, lr=lr,
                num_epochs=nEpochs, save_epoch=snapshot, useTest=useTest, test_interval=nTestInterval):
    """
        Args:
            num_classes (int): 数据中的类别
            num_epochs (int, optional): 训练的总轮数
    """


    model = C3D_model.C3D(num_classes=num_classes, pretrained=False)  # C3D网络,第一个参数为类别数,第二个参数为是否使用预训练模型
    train_params = [{'params': C3D_model.get_1x_lr_params(model), 'lr': lr},
                    {'params': C3D_model.get_10x_lr_params(model), 'lr': lr * 10}]  # 这里生成train_params这个列表存放了两个字典
    # 其中第一个字典目的是设置网络的卷积层和紧跟其后的两个全连接层的学习率为lr,第二个字典的目的是为了将网络的最后一个全连接层的学习率设置为lr*10
    
    criterion = nn.CrossEntropyLoss()  # 采用标准的交叉熵损失函数
    optimizer = optim.SGD(train_params, lr=lr, momentum=0.9, weight_decay=5e-4)  # 使用SGD优化器,传入上述模型参数,学习率,动量因子以及权重衰减系数
    scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=20,
                                          gamma=0.1)  # scheduler的作用是:每训练step_size轮调整一次学习率
    # 判断是否从第0次开始训练,如果没有就加载预训练模型
    if resume_epoch == 0:
        print("Training {} from scratch...".format(modelName))
    else:
        checkpoint = torch.load(os.path.join(save_dir, 'models', saveName + '_epoch-' + str(resume_epoch) + '.pth.tar'),
                       map_location=lambda storage, loc: storage)   # Load all tensors onto the CPU
        print("Initializing weights from: {}...".format(
            os.path.join(save_dir, 'models', saveName + '_epoch-' + str(resume_epoch) + '.pth.tar')))
        model.load_state_dict(checkpoint['state_dict'])
        optimizer.load_state_dict(checkpoint['opt_dict'])
        # 重载optimizer的参数时将所有的tensor都放到cuda上, dmk
        for state in optimizer.state.values():
            for k, v in state.items():
                if torch.is_tensor(v):
                    state[k] = v.cuda()

    print('Total params: %.2fM' % (sum(p.numel() for p in model.parameters()) / 1000000.0))  # 显示当前模型的总参数
    model.to(device)
    criterion.to(device)
    # log_dir生成存放日志文件的目录
    log_dir = os.path.join(save_dir, 'models', datetime.now().strftime('%b%d_%H-%M-%S') + '_' + socket.gethostname())
    writer = SummaryWriter(log_dir=log_dir)

    print('Training model on {} dataset...'.format(dataset))
    # 加载数据
    train_dataloader = DataLoader(VideoDataset(dataset=dataset, split='train', clip_len=16), batch_size=10, shuffle=True, num_workers=0)
    val_dataloader = DataLoader(VideoDataset(dataset=dataset, split='val',  clip_len=16), batch_size=10, num_workers=0)
    test_dataloader = DataLoader(VideoDataset(dataset=dataset, split='test', clip_len=16), batch_size=10, num_workers=0)
    # 将训练集和验证集长度放在一个列表中,测试集长度单独存放
    trainval_loaders = {'train': train_dataloader, 'val': val_dataloader}
    trainval_sizes = {x: len(trainval_loaders[x].dataset) for x in ['train', 'val']}
    test_size = len(test_dataloader.dataset)
    # 正式训练阶段啦
    for epoch in range(resume_epoch, num_epochs):
        # 每一轮都要进行一次训练和验证
        for phase in ['train', 'val']:
            start_time = timeit.default_timer()  # 开始时间

            # 重置每一轮运行中的损失和正确个数为0
            running_loss = 0.0
            running_corrects = 0.0

            # 设置模型到训练/评估模式取决于是否它被训练/验证,二者(.train()和.eval())主要影响的层是BatchNorm or Dropout
            if phase == 'train':
                # scheduler.step() 只在训练时使用
                scheduler.step()
                model.train()
            else:
                model.eval()
            # 从rainval_loaders[phase]这个可迭代对象依次按照预设值的batch_size将其遍历完
            for inputs, labels in tqdm(trainval_loaders[phase]):
                # move inputs and labels to the device the training is taking place on
                # 把每次取出来的inputs和labels送入当前正在使用的设备中
                inputs = Variable(inputs, requires_grad=True).to(device)
                labels = Variable(labels).to(device)
                optimizer.zero_grad()  # 优化器从零梯度开始

                if phase == 'train':
                    outputs = model(inputs)
                else:
                    with torch.no_grad():
                        outputs = model(inputs)
                # 用Softmax函数将每一个输入的输出总和变为1
                probs = nn.Softmax(dim=1)(outputs).to(device)
                preds = torch.max(probs, 1)[1]  # 取出预测结果标签
                loss = criterion(outputs, labels.long()).to(device)  # 计算损失

                if phase == 'train':
                    loss.backward()  # 反向传播
                    optimizer.step()  # 使用优化器


                a, b = loss.item, inputs.size(0)  #
                running_loss += loss.item() * inputs.size(0)  # 当前损失等于上一次的加上本次的,由于后面的平均太小,故每次乘以batch_size大小
                running_corrects += torch.sum(preds == labels.data)  # 当前正确个数

            epoch_loss = running_loss / trainval_sizes[phase]  # 每一轮的平均损失
            epoch_acc = running_corrects.double() / trainval_sizes[phase]  # 每一轮的准确率
            # 写入日志文件
            if phase == 'train':
                writer.add_scalar('data/train_loss_epoch', epoch_loss, epoch)
                writer.add_scalar('data/train_acc_epoch', epoch_acc, epoch)
            else:
                writer.add_scalar('data/val_loss_epoch', epoch_loss, epoch)
                writer.add_scalar('data/val_acc_epoch', epoch_acc, epoch)

            print("[{}] Epoch: {}/{} Loss: {} Acc: {}".format(phase, epoch+1, nEpochs, epoch_loss, epoch_acc))
            stop_time = timeit.default_timer()  # 停止时间
            print("Execution time: " + str(stop_time - start_time) + "\n")

        if epoch % save_epoch == (save_epoch - 1):
            torch.save({
                'epoch': epoch,
                'state_dict': model.state_dict(),
                'opt_dict': optimizer.state_dict(),
            }, os.path.join(save_dir, 'models', saveName + '_epoch-' + str(epoch + 1) + '.pth.tar'))
            print("Save model at {}\n".format(os.path.join(save_dir, 'models', saveName + '_epoch-' + str(epoch+1) + '.pth.tar')))
        # 测试
        if useTest and epoch % test_interval == (test_interval - 1):
            model.eval()
            start_time = timeit.default_timer()

            running_loss = 0.0
            running_corrects = 0.0

            for inputs, labels in tqdm(test_dataloader):
                inputs = inputs.to(device)
                labels = labels.to(device)

                with torch.no_grad():
                    outputs = model(inputs)
                probs = nn.Softmax(dim=1)(outputs)
                preds = torch.max(probs, 1)[1]
                loss = criterion(outputs, labels.long())

                running_loss += loss.item() * inputs.size(0)
                running_corrects += torch.sum(preds == labels.data)

            epoch_loss = running_loss / test_size
            epoch_acc = running_corrects.double() / test_size

            writer.add_scalar('data/test_loss_epoch', epoch_loss, epoch)
            writer.add_scalar('data/test_acc_epoch', epoch_acc, epoch)

            print("[test] Epoch: {}/{} Loss: {} Acc: {}".format(epoch+1, nEpochs, epoch_loss, epoch_acc))
            stop_time = timeit.default_timer()
            print("Execution time: " + str(stop_time - start_time) + "\n")

    writer.close()


if __name__ == "__main__":
    train_model()

六、训练可视化结果

动作识别(二)复现C3D网络_第2张图片

图2 训练过程可视化

七、第一步进行测试

(1)安装好上述环境;
(2)在data文件夹下面放入ucf101前两类的原始数据,一个训练过的模型我会直接放在工程文件内,所以这里你不用担心
(3)修改mypath.py文件中的root_dir和output_dir路径,前者为UCF-101文件的路径,后者为处理好的数据输出路径,如:root_dir = “D:/pytorch_study/c3d_recogniztion/data/UCF101/UCF-101” ;
output_dir = “D:/pytorch_study/c3d_recogniztion/data_process/ucf101”;
(4)运行dataloaders文件夹下面的dataset.py文件,生成预处理数据;这一步也会生成ucf_labels.txt这个标签文件,在下一步会用到;
(4)再运行test.py文件,便可以看见结果,不过效果不是很好;

八、第二步进行训练

(1)必须经历完第一步的测试过程;
(2)直接运行train.py文件就可以运行啦;

九、致谢和说明

本代码参考了大量的作者代码,非常感谢它们!本人代码也要开源!也给自己写一点笔记,哈哈!

十、数据和源码地址

强调一下,由于UCF101原始数据太大了,本人百度云空间有限,所以将UCF101中的前两个数据拿出来了!
如果需要原始UCF101视频数据可联系本人邮箱[email protected]
数据: DATA.提取码:a35y
C3D代码: 代码.提取码:fmbt

你可能感兴趣的:(动作识别,笔记,python,深度学习)