pytorch版本的R-C3D工作以及扩展

时序行为检测新工作开展

最近开始的一项新工作,首先是基于R-C3D.pytorch这一部分进行工作(看了下是电子科大的大佬迁移写出来的,确实是在这里救急了,十分感谢)的baseline具体工程见链接。这个方法是结合了C3D的框架还有faster-rcnn的做法来做的一项工作,也就是两个工作的结合。不得不说其实这一块只要是有一个比较好的做法提出,实际上就是把别人的两个方法一结合就呈现了自己的方法....搞得十分真实。我们的想法也是打算将这里之前的ma-i3d的network移植过来替代一下这里的C3D的network看看能不能对于时序检测工作上有一些改进帮助。

时序行为检测的难点

根据这两天对于时序行为检测的理解,当前打算和R-C3D的工作相结合做一个新框架出来,对于时序行为检测的两个难点了解到一个是时序检测片段要有精确的边界,就是说什么时候算是一个行为的开始,什么时候又是一个行为的结束,这个一点对比用bounding boxes来框一个静态object来讲难度相对大一些。(就是说就算拆帧来算的话也要精确到起始帧)。还有就是要结合时序信息来进行识别,看了下THUMOS2014数据集无论training还是validation都是一定会包含一个是feature和一个是video的文件夹。feature里面包含的一些信息是,video里面就是原始视频了。如果单纯我们切帧的话确实是会存在一部分的问题无论是R-C3D还是SCNN都是要先找到proposal,这里的精确度越高,对于后面分类的帮助越大。
当前想做的几个数据集诸如THUMOS2014ActivityNet_v1.3、AVA(根据具体情况决定),Charades。其中THUMOS2014的训练集其实就是UCF101数据集,这个下载下来看一看就知道了。实际上现有数据集多多少少会有一些问题,里面视频带水印或者是时间长度十分短对于我们人来讲理解都有一定的困难,何况机器了...不过好在机器不像人一样会感觉疲劳。

R-C3D的baseline工作开展

实际上这一块只要在理解C3D基础上以及有了解过faster-rcnn之类的做法,就会发现这就是两块工作(proposal+classification的结合)。他的网络结构比较容易理解,可以看看这篇文章讲的比我详细,就是主要两个子网络,一:时序上的行为检测,检测起始帧和结束帧看这一段是否是一个完整的动作,这里训练集中倒是给出了指定的start-time和end-time。

R-C3D.pytorch遇到的一些问题以及最终解决方案

我们来详细解析一下这个工程的一些调试和遇到具体问题
首先是大体看了一下这个工作的实现情况,具体的指标暂且还没有研究,只是凭感觉把结果调试出来了。工程内包含的文件见图1

13453016-40db641c239aaa93.png
图1: 工程内包含的所有文件

因为数据集在远程服务器上然后比较大,我就下了两三个视频拿来进行测试,这里有一个问题一开始没有解决因为这个作者写的十分不明确,对于第一次使用该数据集的人了解会有一些误解,我就是吃了这个亏卡在读取数据处理数据这里。
这里我们首先要讲一下 THUMOS2014数据集,训练集就是上面的ucf101数据集我们已经了解到了,但是,最重要的一点是这个数据集是包含两个任务的,识别和检测两大任务,它本身是从一个竞赛中分离过来的,也就是说本身他不是说要用ucf101来进行做检测这一块的。下面这个图2是我们服务器上的THUMOS2014数据集,其中1-training里面就是ucf101数据集,包括101类动作,共计13320段分割好的视频片段。THUMOS2014的验证集和测试集则分别包括1010和1574个未分割过的视频。就是后面的3-validation和6-testing这两个文件夹里的东西。在时序行为检测任务中,只有20类动作的未分割视频是有时序行为片段标注的,包括200个验证集视频(包含3007个行为片段)和213个测试集视频(包含3358个行为片段)。这些经过标注的未分割视频可以被用于训练和测试时序行为检测模型。这里说明就是说这里是用来做检测的数据,也就是我们真正做检测要用的训练的和测试的数据。一开始没有理解透,老是在想应该怎么把ucf101当做训练数据来匹配进去,耽误了不少时间,实在是不应该。话说论文中就有写啊,就应该一开始把数据集论文先读一遍,这样直接看R-C3D的方法实际上进展也并未有很多(嘤)。下面图3是3-validation里面的一些文件,图4是6-testing里的文件。
13453016-0cde69972b4673af.png
图2:THUMOS2014数据集里全部的几个文件,名称有过改变
13453016-ce659d63a75d4742.png
图3:3-validation里的文件,也就是我们做时序检测要用的训练集

13453016-0e36f37c80d3ab7a.png
图4:6-testing里的文件,也就是我们做时序检测的测试集

理解了上面的任务要点后,那么根据完成这个任务的完整流程走的话,首先就是看看官方README里怎么讲的,图5告诉我们比较明确的前置准备工作了
13453016-9527bd320a5e327b.png
图5:官方README给出的一个做法
数据我直接选取了3-validation里面的validation-videos里的三个视频,它里面的视频命名是和图6展示一样的。
13453016-13995100f379b3d9.png
图6: 训练视频数据集的文件命名形式

然后这里我们是要对输入的视频进行分割的,必须要做的一部分。这个地方一开始很难理解,因为我们看到训练的时候不是有1010个未分割的视频在validation里,为什么不能用这么多?因为这里我们要做的是检测啊,上面也有提到只有20类动作的未分割视频是有时序行为片段标注的,也就说我们想得到时序上标注的动作信息,也只有这点数据供给我们的机器学习了。就相当于我们复习的时候只有两本书复习,别的什么复习资料都没有,就问你看还是不看吧,不看反正一点不会,看了多少还能有点进展。这里我们从图7-9中看一下具体的一些文件吧。
13453016-42492a15a91be592.png
图7:标注信息
13453016-340083d68b66e411.png
图8:Ambigious里面的内容

图7这里的文件包含的都是一些时序标注信息,也就是我们所讲的能检测出动作的时间,然后图8里面就是这个具体动作到底在那几个视频中出现过,然后后面两个数字是他具体出现的起始时间,这里不光是猜测,经过验证过的,图9是我对应截到的一个能看的出来是动作的一帧(截图鬼才),我们可以看到下方的时间轴也正好和图8中的开始结束时间相对应。这里的起始时间都是按秒数算的,从多少秒到多少秒为止结束,小数点精确到微秒(相对来说比较宽泛了,不过也说得过去,再细一点其实影响也不是很大,毕竟这还是一个时序上的动作,多一个静止的图也就是截出的帧不一定有很大的帮助)。
13453016-6300fde969f19cec.png
图9:截出的一个图,视频名上方也可以看到

然后我们就这样跟着来读一下generate_frames.py这个文件吧,这个文件在R-C3D.pytorch/process/thumos2014/文件夹下面,这里我们把我这里修改的程序也给出来:

#coding=utf-8
# --------------------------------------------------------
# R-C3D
# Copyright (c) 2017 Boston University
# Licensed under The MIT License [see LICENSE for details]
# Written by Huijuan Xu
# --------------------------------------------------------

import os
from util import *
import json
import glob

fps = 25
ext = '.mp4'
VIDEO_DIR = '/home/simon/ApplyEyeMakeup'
FRAME_DIR = '/home/simon/THUMOS14'

META_DIR = os.path.join(FRAME_DIR, 'annotation_')

def generate_frame(split):
  SUB_FRAME_DIR = os.path.join(FRAME_DIR, split)
  mkdir(SUB_FRAME_DIR)
  segment = dataset_label_parser(META_DIR+split, split, use_ambiguous=True)
  video_list = segment.keys()
  for vid in video_list:
    filename = os.path.join(VIDEO_DIR, vid+ext)
    outpath = os.path.join(FRAME_DIR, split, vid)
    outfile = os.path.join(outpath, "image_%5d.jpg")
    mkdir(outpath)
    ffmpeg(filename, outfile, fps)
    for framename in os.listdir(outpath):
      resize(os.path.join(outpath, framename))
    frame_size = len(os.listdir(outpath))
    print (filename, fps, frame_size)

generate_frame('val')
#generate_frame('test')
#generate_frame('testing')

这里我们video-dir中存放的就是我们有的原始视频了,frames-dir是我们存放一些生成的frame的地方。这个文件单看是不够的,因为他从同一文件夹下的util.py中定义了一个方法dataset_label_parser(),我们把util.py也贴在下面:

# --------------------------------------------------------
# R-C3D
# Copyright (c) 2017 Boston University
# Licensed under The MIT License [see LICENSE for details]
# Written by Huijuan Xu
# --------------------------------------------------------

import subprocess
#import shutil
import os, errno
import cv2
from collections import defaultdict
import shutil
import matplotlib
import numpy as np

def dataset_label_parser(meta_dir, split, use_ambiguous=False):
  class_id = defaultdict(int)
  with open(os.path.join(meta_dir, 'class-index-detection.txt'), 'r') as f:
    lines = f.readlines()
    for l in lines:
      cname = l.strip().split()[-1]# leibie name
      #print(cname)
      cid = int(l.strip().split()[0])## leibie id
      class_id[cname] = cid
      if use_ambiguous:
        class_id['Ambiguous'] = 21
    segment = {}
    #video_instance = set()
  for cname in class_id.keys():
    tmp = '{}_{}.txt'.format(cname, split)
    with open(os.path.join(meta_dir, tmp)) as f:
      lines = f.readlines()
      for l in lines:
        vid_name = l.strip().split()[0]
        start_t = float(l.strip().split()[1])
        end_t = float(l.strip().split()[2])
        #video_instance.add(vid_name)
        # initionalize at the first time
        if not vid_name in segment.keys():
          segment[vid_name] = [[start_t, end_t, class_id[cname]]]
        else:
          segment[vid_name].append([start_t, end_t, class_id[cname]])

  # sort segments by start_time
  for vid in segment:
    segment[vid].sort(key=lambda x: x[0])

  if True:
    keys = list(segment.keys())
    keys.sort()
    with open('segment.txt', 'w') as f:
      for k in keys:
        f.write("{}\n{}\n\n".format(k,segment[k]))

  return segment

def get_segment_len(segment):
  segment_len = []
  for vid_seg in segment.values():
    for seg in vid_seg:
      l = seg[1] - seg[0]
      assert l > 0
      segment_len.append(l)
  return segment_len

def mkdir(path):
  try:
    os.makedirs(path)
  except OSError as e:
    if e.errno != errno.EEXIST:
      raise

def rm(path):
  try:
    shutil.rmtree(path)
  except OSError as e:
    if e.errno != errno.ENOENT:
      raise

def ffmpeg(filename, outfile, fps):
  command = ["ffmpeg", "-i", filename, "-q:v", "1", "-r", str(fps), outfile]
  pipe = subprocess.Popen(command, stdout = subprocess.PIPE, stderr = subprocess.STDOUT)
  pipe.communicate()


def resize(filename, size = (171, 128)):
  img = cv2.imread(filename, 100)
  img2 = cv2.resize(img, size, interpolation=cv2.INTER_LINEAR)
  cv2.imwrite(filename, img2, [100])

# get segs_len from segments by: segs_len = [ s[1]-s[0] for v in segments.values() for s in v ]
def kmeans(segs_len, K=5, vis=False):
  X = np.array(segs_len).reshape(-1, 1)
  cls = KMeans(K).fit(X)
  print( "the cluster centers are: ")
  print( cls.cluster_centers_)
  if vis:
    markers = ['^','x','o','*','+']
    for i in range(K):
      members = cls.labels_ == i
      matplotlib.scatter(X[members,0],X[members,0],s=60,marker=markers[min(i,K-1)],c='b',alpha=0.5)
      matplotlib.title(' ')
      matplotlib.show()

这里我们直接先看util.py中的'class-index-detection.txt',这个文件里存放的是我们有的这20类有标注的类别的名称:

7 BaseballPitch
9 BasketballDunk
12 Billiards
21 CleanAndJerk
22 CliffDiving
23 CricketBowling
24 CricketShot
26 Diving
31 FrisbeeCatch
33 GolfSwing
36 HammerThrow
40 HighJump
45 JavelinThrow
51 LongJump
68 PoleVault
79 Shotput
85 SoccerPenalty
92 TennisSwing
93 ThrowDiscus
97 VolleyballSpiking

然而这里面并没有Ambigious,这里应该是说只是单纯识别是一个夸张的动作吧,反正分类里面没有,我们是学习不到具体是哪一类的。
然后具体的我们再回去看generate_frames.py中,这里对于THUMOS2014文件夹是我们自己建的,里面包含annotaiton_val文件,这个稍微懂点程序的都能看出来,不多赘述。annotaiton文件夹里面就包含我们上述图7的标注信息,找寻这些动作的所在起始时间。
好了,然后再往下读util.py里的dataset_label_parser()方法吧,这里还有一个他自己给的segment.txt,里面不光有我们所存的动作标注,后面还有一个数应该是划分的k段吧。上面采样率已经设置好了是25了。然后就根据segment划分出来每一帧存到对应的文件夹中,不是什么问题。
然后下一步来看这个generate_roidb_training.py文件。

#coding=utf-8
# --------------------------------------------------------
# R-C3D
# Copyright (c) 2017 Boston University
# Licensed under The MIT License [see LICENSE for details]
# Written by Huijuan Xu
# --------------------------------------------------------

import os
import copy
import json
import pickle
import subprocess
import numpy as np
import cv2
from util import *
import glob

FPS = 25
ext = '.mp4'
LENGTH = 768
min_length = 3
overlap_thresh = 0.7
STEP = LENGTH / 4
WINS = [LENGTH * 1]
WINS = [LENGTH * 1]
FRAME_DIR = '/home/simon/THUMOS14'## it can be changed
META_DIR = os.path.join(FRAME_DIR, 'annotation_')

print ('Generate Training Segments')
train_segment = dataset_label_parser(META_DIR+'val', 'val', use_ambiguous=False)

def generate_roi(rois, video, start, end, stride, split):
  tmp = {}
  tmp['wins'] = ( rois[:,:2] - start ) / stride
  tmp['durations'] = tmp['wins'][:,1] - tmp['wins'][:,0]
  tmp['gt_classes'] = rois[:,2]
  tmp['max_classes'] = rois[:,2]
  tmp['max_overlaps'] = np.ones(len(rois))
  tmp['flipped'] = False
  tmp['frames'] = np.array([[0, start, end, stride]])
  tmp['bg_name'] = os.path.join(FRAME_DIR, split, video)
  tmp['fg_name'] = os.path.join(FRAME_DIR, split, video)
  if not os.path.isfile(os.path.join(FRAME_DIR, split, video, 'image_' + str(end-1).zfill(5) + '.jpg')):
    print (os.path.join(FRAME_DIR, split, video, 'image_' + str(end-1).zfill(5) + '.jpg'))
    raise
  return tmp

def generate_roidb(split, segment):
  VIDEO_PATH = os.path.join(FRAME_DIR, split)
  video_list = set(os.listdir(VIDEO_PATH))
  duration = []
  roidb = []
  for vid in segment:
    if vid in video_list:
      length = len(os.listdir(os.path.join(VIDEO_PATH, vid)))
      db = np.array(segment[vid])
      if len(db) == 0:
        continue
      db[:,:2] = db[:,:2] * FPS

      for win in WINS:
        # inner of windows
        stride = int(win / LENGTH)
        # Outer of windows
        step = int(stride * STEP)
        # Forward Direction
        for start in range(0, max(1, length - win + 1), step):
          end = min(start + win, length)
          assert end <= length
          rois = db[np.logical_not(np.logical_or(db[:,0] >= end, db[:,1] <= start))]

          # Remove duration less than min_length
          if len(rois) > 0:
            duration = rois[:,1] - rois[:,0]
            rois = rois[duration >= min_length]

          # Remove overlap less than overlap_thresh
          if len(rois) > 0:
            time_in_wins = (np.minimum(end, rois[:,1]) - np.maximum(start, rois[:,0]))*1.0
            overlap = time_in_wins / (rois[:,1] - rois[:,0])
            assert min(overlap) >= 0
            assert max(overlap) <= 1
            rois = rois[overlap >= overlap_thresh]

          # Append data
          if len(rois) > 0:
            rois[:,0] = np.maximum(start, rois[:,0])
            rois[:,1] = np.minimum(end, rois[:,1])
            tmp = generate_roi(rois, vid, start, end, stride, split)
            roidb.append(tmp)
            if USE_FLIPPED:
               flipped_tmp = copy.deepcopy(tmp)
               flipped_tmp['flipped'] = True
               roidb.append(flipped_tmp)

        # Backward Direction
        for end in range(length, win-1, - step):
          start = end - win
          assert start >= 0
          rois = db[np.logical_not(np.logical_or(db[:,0] >= end, db[:,1] <= start))]

          # Remove duration less than min_length
          if len(rois) > 0:
            duration = rois[:,1] - rois[:,0]
            rois = rois[duration > min_length]

          # Remove overlap less than overlap_thresh
          if len(rois) > 0:
            time_in_wins = (np.minimum(end, rois[:,1]) - np.maximum(start, rois[:,0]))*1.0
            overlap = time_in_wins / (rois[:,1] - rois[:,0])
            assert min(overlap) >= 0
            assert max(overlap) <= 1
            rois = rois[overlap > overlap_thresh]

          # Append data
          if len(rois) > 0:
            rois[:,0] = np.maximum(start, rois[:,0])
            rois[:,1] = np.minimum(end, rois[:,1])
            tmp = generate_roi(rois, vid, start, end, stride, split)
            roidb.append(tmp)
            if USE_FLIPPED:
               flipped_tmp = copy.deepcopy(tmp)
               flipped_tmp['flipped'] = True
               roidb.append(flipped_tmp)

  return roidb

if __name__ == '__main__':

    USE_FLIPPED = True      
    train_roidb = generate_roidb('val', train_segment)
    print (len(train_roidb))
    print ("Save dictionary")
    pickle.dump(train_roidb, open('train_data_25fps_flipped.pkl','wb'), pickle.HIGHEST_PROTOCOL)

这个就是生成我们需要的输入数据roidb,这个我查了一下是faster-rcnn读取数据的格式,这篇文章里面讲的比较详细,我也是从这里大概了解了一下,啊,反正总的来说他就是一个数据读取格式。我们就算不知道他是什么原理,反正能读取这一步是已经做到了。

下面说一下中间出现的一些问题,首先就是报出这个torch里没有安cuda,这个我们把cuda9.0文件中的文件直接全复制做个硬链接暴力解决。
然后本机测试遇到这个问题:ImportError: No module named 'numpy.core._multiarray_umath'
终端使用pip install -U numpy更新到最新版本后可以解决上述问题。
然后运行

python train_net.py

我们直接看结果,上述问题也都是从运行train这步出现的一些问题。
最后终于,能够运行训练并且保留模型了。


13453016-11ffa94d283a2b90.png
训练结果

这里我们可以直接把自己的网络替换,因为这个老哥也就给了4个网络,c3d,i3d,res34,res50这四个,然后剩下的网络我们可以自己添加来看看结果如何。
还存在的问题就是训练占得显存也很大,batch-size默认为1我的本机单卡1070显存就占了6000m左右这个后续调优我们再说。
不管怎么样,第一步做的还是不错的,这项工作继续开展,还会持续更新。
因为数据集比较大,所以跟师兄商量后拿出5类进行测试训练,每一类挑选了5个视频,然后还有一类分类为ambigious,一共定下是6类。
从图11我了解到,train_net.py里最终的loss由四个loss想加得到一个总的loss函数,因为我们有两个子网络,proposal和classification两个subnet。一个sunet里包含两个loss来约束最终的结果。直接翻译下面的英文的话,就是


13453016-f48bb13e7f11a252.png
图11:论文中对于loss的解释

出现cudaerro(59),查了一下是标签出现问题,更正后从1开始打到最后一个类别。
目前evaluation没有完成,看了一下需要从log里找到所需要的东西,看一下test.log应该怎么保存,保存要什么格式。(可以确定的是.txt格式)
然后我们就直接用脚本输出看看最后这个出来的日志生成json看看,这个pytorch版的R3D还是一个残次品,不完全。粗略看了一下log_analysis文件,他是截取log中的关键词来生成对应我们想要的json文件,问题应该不是很大,等待半个小时log打印出来后去评估一下,评估json我们直接用python版本的这个evaluation文件就好了,原版使用的是matlab版的,这个今下午应该可以做出来。
上述问题解决,现在已经正式开始训练thumos2014数据集了,先看看结果能不能复现出来,可以的话就没有太大问题。预计两天半训练结果出来。

Processed过程

又重新看了一下processed数据的过程,因为训练的时读取的是roidb的数据格式,刚刚研究并且从faster-rcnn对应得到启发,找一下里面各个键对应的含义,一会整理一下到下方表格中。
这里的anchor中的K设置的是4,其实可以自由变换的,论文中说道给了2,4,5,6,8,9,10,12,14,16这么10种选择,实际上并没有用到这么多。

目前难点以及可以优化部分

从头捋了一遍,难点主要是对于不同数据集是否可以进行相同的处理方式?看了一下这个版本中generate_frames对于三个数据集的处理除了采样率不一样没有其他大的区别。基本是以activitynet的处理方式进行的。按照可以从大范围来约束小范围的理论来讲,应该是没有什么问题。可以先等等结果看看评估如何,如果处理不得当再进行优化修改。
对于resnet50来讲,这里我用了之前3D-ResNet50-Pytorch的预训练kinetics模型,没有想到竟然嵌套进去了,这里应该是只要了resnet50每一层的权重,不会导致出来loss是nan(没有权重加载进来算loss)

猜你喜欢

转载自blog.csdn.net/weixin_34242331/article/details/87245974