神经网络-TSN代码复现

一、前言

  • 系统:win10
  • 环境:python3.73
  • 框架:torch

作为一个初学者,TSN的原始代码看的有点懵,代码注释太少了。这里以学习的角度,从熟悉torch框架开始复现TSN1算法。

二、相关原理介绍

TSN1是一种基于双流(Two Stream)模型2的姿态识别方法。在原有的网络模型上,通过修改输入数据的采样方式,在样本量少的情况下,高效的实现了长视频的姿态识别。

在介绍TSN之前,先介绍双流(Two Stream)模型2的大致原理。Two Stream在单网络(可以是resnet3、BNInception4)的基础上提出了双网络结构,采用两种不同的Input数据,分别输入到两个相同的网络之中,并且在处理过程中这两个网络互不相干,在最后将两个网络输出进行简单融合的处理。这两个不同的Input分别是:

  • RGB:视频里面随机提取的一帧图片
  • Optical Flow(光流)5或者RGB Diff(RGB 差分)6:用连续两张及以上的图片通过对应算法计算出的和原图片大小一样的“图片”

TSN的改进部分,双流(Two Stream)模型的输入是使用的连续帧进行姿态识别,而连续帧之间的冗余信息其实很高,而且识别长视频的时候,对计算资源和内存空间开销很大。TSN改进了这种采集方法,将一个动作视频等间隔分为3个部分,在每个部分中随机提取数据(RGB,Optical Flow),这样由于中间跳过了很多帧,降低了计算资源的开销,就可以实现对长视频的动作识别,并且速度更快。准确率从实验结果上看,并没有明显下降。除此之外,TSN还提出了一些方法,扩展了有限的样本数量,使得学习的精度更高。

TSN的网络结构如下图1所示,主要分为了3个部分:

  • Video(视频):将一个视频分为3个部分(segment)
  • Snippet(片段):每个部分里面随机提取出一张RGB图像和一个Optical Flow或者RGB Diff信息。
  • TSN网络:每个RGB图像单独使用一个空间网络(Spatial ConvNet),每个Optical Flow单独使用一个时间网络(Temporal ConvNet)。最后3个空间网络联合得到结果1(Segmental Consensus 绿色),时间网络联合得到结果2(Segmental Consensus 蓝色),结果1和结果2联合分析得到最终结果(Class Score Fusion)

在本文中,也按照这三个部分的顺序来进行复现TSN代码

图1 TSN网络结构

根据图片,在实现过程中的误区指示:

  • 3个空间网络内权值共享,3个时间网络内部权值共享
  • 在每一个Snppet中,可以输入多个光流。一个光流由2张图片构成(x方向光流,y方向光流)
  • Spatial ConvNet和Temporal的输出为图片的Lable,Segmental Consensus只是对3个网络做一个平均
  • Class Score Fusion是对RGB、RGBDiff、FLow的输出做个平均。

三、数据集获取

这类视频数据集资源比较少,这里采用UCF101作为实验例子,实现以下功能:

  • 分段随机提取RGB
  • 分段随机提取光流

简单介绍一下UCF101数据集。

  • 内含13320 个短视频
  • 视频来源:YouTube
  • 视频类别:101 种
  • 主要包括这5大类动作 :人和物体交互,只有肢体动作,人与人交互,玩音乐器材,各类运动
  • 视频格式:.avi

3.1 数据集下载

下载下载是一个zip压缩包,解压后里面是各种动作的视频,下载速度较慢,建议使用IDM下载,开32线程同步下载。联通4G网速,大概下载了40分钟。

下载地址

需要下载两个,一个是数据,一个用来区分数据集和训练集的TXT文件。

1558803947029

TXT压缩包和文件内容的截图如下:

1558804168140

1558804140704

3.2 数据制作

根据7.5节数据集的制作原理,这里使用7.5方案一。为了得到TSN需要的训练数据,这里两个数据集

  • 数据集一:又可以称原始数据集,仅仅返回视频地址,不对视频本身做任何处理。输入为数据集根目录地址,可提取到每个视频的地址和Lable。
  • 数据集二:根据所需的数据,对每个视频进行处理,返回训练所需要的具体数据,如RGB,RGBDiff,Flow(光流)等需要的不同类型的数据。(这里只实现最简单的RGB作为例子,光流可以使用opencv内部的库实现)

这么设计的理由是为了降低内存的开销,如果一下把全部视频分解成帧写入到内存,对计算机内存消耗太高。现在使用这种方法,只有在参与训练的数据会写入到内存中。

3.2.1 数据集一设计

为了方便理解(加强我的记忆),把需要读取的7个文件列内容列出来。一个训练集对应一个测试集,有三组,每组训练测试集只是选取的数据不同,切不可把三个训练集和三个测试集全部同时读入。我使用的testlist01、trainlist01用作TSN复现。

classInd.txttestlist01.txttestlist02.txttestlist03.txttrainlist01.txttrainlist02.txttrainlist03.txt

  • 输入:数据集地址、加载训练集还是测试集,默认训练集
  • 输出:视频地址,lable
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
from torch.utils import data

# data.Dataset 继承父类,就可以加入到torch家族,使用它内部的其他功能
class ListDataSet(data.Dataset):
def __init__(self, DataPath, Train = True):
"""
:param DataPath: 数据集根目录地址
:param Train: 是否加载训练集,默认加载
:return: Null
"""
self.Train = Train
if self.Train:
# trainlist01文件里面自带ID信息
self._GetAllTrainData(DataPath + "/ucfTrainTestlist/trainlist01.txt")
else:
# 因为testlist01文件没有ID信息,所以需要先获取ClassID信息
self._GetClassID(DataPath + "/ucfTrainTestlist/classInd.txt")
self._GetAllTestData(DataPath + "/ucfTrainTestlist/testlist01.txt")

def __getitem__(self, item): # 来自继承的接口,必须实现,供上层调用
"""
:param item: 上层传入想要的index,类型为int
:return: DataPath,Lable
"""
if self.Train:
return self.TrainData[item][0],self.TrainData[item][1]
else:
return self.TestData[item][0],self.TestData[item][1]

def __len__(self): # 来自继承的接口,必须实现,供上层调用
if self.Train:
return len(self.TrainData)
else:
return len(self.TestData)

def _GetAllTrainData(self, TrainDataPath):
self.TrainData = list()
TrainDataFile1 = open(TrainDataPath,"r")

for line in TrainDataFile1:
temp = line.strip("\n").split(" ")
temp[1] = int(temp[1]) - 1 # 让lable从0开始计数,默认是1
# temp: ['ApplyEyeMakeup/v_ApplyEyeMakeup_g08_c01.avi', 0]
self.TrainData.append(temp)

TrainDataFile1.close()
#print(self.TrainData)

def _GetAllTestData(self, TestDataPath):
self.TestData = list()
TestDataFile1 = open(TestDataPath,"r")

for line in TestDataFile1:
linedata = line.strip("\n") # 去掉最后的换行符
# linedata: ApplyEyeMakeup/v_ApplyEyeMakeup_g01_c01.avi
classid = self.ClassID[linedata.split("/")[0]] # 获得该视频的分类
temp = [linedata,classid] # 制作成一个组
# temp: ['ApplyEyeMakeup/v_ApplyEyeMakeup_g01_c01.avi', 0]
self.TestData.append(temp) # 添加到list

TestDataFile1.close()

# 由于Trainlist文件自带了ID,所以只有test数据才用的上查字典
def _GetClassID(self, ClassIDPath):
self.ClassID = dict()
classidfile = open(ClassIDPath,"r")
if classidfile == None:
print("ClassPath"+"文件打开失败")
raise IOError
for line in classidfile:
# 读取文件的每一行,去掉字符,以空格分开数据
temp = line.strip("\n").split(" ")
# temp: ['1', 'ApplyEyeMakeup']
temp[0] = int(temp[0]) # 读出来的是str类型,强转为int类型
# temp: [1, 'ApplyEyeMakeup']
# 做成字典
self.ClassID[temp[1]] = temp[0] - 1 # -1是为了让lable从0开始计数,默认是1开始
classidfile.close()
#print(self.ClassID) # 可打印查看列表数据

# 测试部分
if __name__ == "__main__":
a = ListDataSet("data")

# 分组,并且打乱数据
b = data.DataLoader(dataset=a,batch_size=3,shuffle=True)
for D,L in b:
print(D)
print(L)

最后的输出结果点击查看,现在已经可以很方便的获得视频的地址和lable

3.2.2 数据集二设计

这里用来输出对视频处理之后的数据

  • 输入:数据集地址、训练集/测试集、所需数据类型、视频分段数等
  • 输出:数据、Lable

代码说明:功能查看注释就好,以下对一些参数说明。

  • weight、height:resnet101网络默认输入的图片尺寸为224*224,所以这里输出的图片尺寸就得跟着
  • self.data:一张表,里面有视频的地址和Lable,依赖3.2.1数据集一
  • __getitem__:核心部分,看代码建议从这里作为入口
  • _Change_to_RGB:将视频随机抽帧,转为一组RGB图片
  • _generate_random:生成随机数,不需要仔细看
  • _Image_ReShape:修改图片的shape形状,不改变数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
class myDataSet(data.Dataset):
# 这里要把列表信息处理好
def __init__(self, DataPath, Train=True, ModuleType="RGB", Segments=3,
DataLen = 1,Weight = 224, Height = 224):
self.DataPath = DataPath # 数据集的位置
self.Train = Train # 是否选择训练集
self.moduleType = ModuleType # 输出模式,RGB,RGBDff,光流
self.segments = Segments # 视频分为几段
self.weight = Weight # 需要输出视频的宽
self.height = Height # 需要输出视频的高
self.DataLen = DataLen # 每一个Segment提取多少张图片

# 不同类型的通道数不一样。光流通道数为2(x,y方向的矢量各一个图片)
if self.moduleType == "RGB":
self.channel = 3

# 获取视频列表数据信息
self.data = ListDataSet(self.DataPath,self.Train)

# item其实就是index数组下标,这里是数据的输出出口,__getitem__是内联函数
def __getitem__(self, item):
VideoDataPath = self.data[item][0]
Label = self.data[item][1]

# 获取数据的时候,开始对视频进行处理,这样虽然浪费的时间,但是节约了内存
if self.moduleType == "RGB": # 为了后期拓展,加个类型判断
images = self._Change_to_RGB(self.DataPath + "/UCF101/" + VideoDataPath)

# 转换为torch参数类型输出,torch网络只能使用torch内部的类型,输出多少张图片,就需要有多少个Lable,而同一个视频的lable是一样的,所以填充对应维度即可
return torch.from_numpy(images), torch.full([self.segments*self.DataLen], Label)

def _Change_to_RGB(self, VideoDataPath):
"""
:param VideoDataPath: 视频地址 # F:/data/UCF101/PushUps/v_PushUps_g01_c01.avi
:return: RGB数据
"""
cap = cv2.VideoCapture() # 初始化opencv
cap.open(VideoDataPath) # 打开视频文件
n_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) # 计算帧的数量
segment_length = int(n_frames / self.segments) # 计算没一段帧的数量
print("n_frames ",n_frames)
print("segment_length ",segment_length)
if self.DataLen > segment_length:
print("所需要的数据长度大于每段所拥有的数据长度")
raise IOError

# dstimage 初始化为0,主要是开辟空间,初始化为1也没有问题
dstimage = np.zeros([self.DataLen*self.segments,self.channel,self.weight,self.height])
times = 0

for i in range(self.segments):
# 对每个段都重新计算抽取帧的位置
temp = self._generate_random(segment_length,self.DataLen) # 在每一segment里面随机获得self.DataLen张图片,生成index
for k in temp:
# 抽取指定位置的图片
res,image = cap.read(k + i * segment_length) # image.shape : (240, 320, 3)
if not res: # 判断是否抽取成功
break

# 由于网络输入的图片尺寸是固定的,不方便修改,这里修改训练库图片尺寸(总不能为每个分辨率设计一个网络吧)
image = cv2.resize(image,(self.weight,self.height),interpolation = cv2.INTER_AREA) # 修改图片尺寸
# shape: [224,224,3]
# 网络无法使用上面的shape,所以修改至下面的
image = self._Image_ReShape(image) # shape: [3,224,224]

# 添加到输出空间中
dstimage[times] = image
times += 1

# 释放opencv空间
cap.release()
# print(dstimage.shape)
return dstimage

def _generate_random(self, num_range, nsize):
"""
Args: 生成nsize个,0~num_range范围的不同随机数的list,且随机数不能重复
num_range: 需要的随机数范围
nsize: 需要的随机数数量

Returns: 随机数组成的list
"""
generate = True
temp = list() # type: list
while generate:
temp = np.random.randint(num_range,size=nsize).tolist() # type: list
temp.sort()
if nsize == 1:
break

# 判断是否有重复的帧序号
for i in range(nsize - 1):
if temp[i] == temp[i + 1]:
generate = True
break
generate = False
# print(temp)
return temp

def _Image_ReShape(self, image):
"""
Args:
image: 输入图片 [224,224,3]

Returns: reshape后的图片 [3,224,224]
"""
dstimage = np.zeros([self.channel,self.weight,self.height])
for a in range(self.channel):
dstimage[a] = image[:,:,a]

return dstimage

# 返回长度
def __len__(self):
return self.data.__len__()

3.3 数据使用

对数据集的功能做一个简单的测试,输出数据集的size,并显示图片(因为图片的默认shape被修改,显示出来的单通道颜色比较奇怪),首先把数据集和数据集list文件放在一个路径下,就像这样:

1558940877321

在和上述代码同一个文件最下面,添加以下代码,并运行

1
2
3
4
5
6
7
8
9
10
11
12
13
if __name__ == "__main__":
import matplotlib.pyplot as plt
a = myDataSet("F:/data",Train=False,DataLen=5)

# 分组,并且打乱数据
b = data.DataLoader(dataset=a,batch_size=1,shuffle=True)
for index,(D,L) in enumerate(b):
print(D.size())
print(" ")
plt.imshow(D[0][0][0])
plt.show()
if index == 5:
break

输出结果:

1
2
3
n_frames  56
segment_length 18
torch.Size([1, 15, 3, 224, 224])

img

画重点了,仔细看这里将15张图片全部放在了一起,并没有分段,这是因为TSN网络里面看似是3个网络,但是这三个网络实际上是共享权重的,意味着其实只是一个网络。所以让一个网络同时学习15张图片,最后对loss进行一个求均值操作,完美。

3.4 总结

TSN作者写的源代码数据集制作方法如下,他在数据集和网络里都对图片数据进行了操作,代码互耦程度教高

  • 提取全部帧保存(保存完后60多个G大小)
  • 在数据集里提取对应图片
  • 在网络里对图片进行差分

我的方法

  • 提取全部视频地址和Lable。(无额外储存空间开销,总共大小6G)
  • 在数据集里处理视频(转RGB,差分,光流),提取对应图片(差分和光流还没有做)
  • 网络没有对数据集进行处理,纯粹只是训练网络功能

我的方法优缺点:

  • 优点:没有额外储存空间,代码互耦低。不需要对视频进行预处理,方便实时监测视频动作。
  • 缺点:在训练网络的时候还需要进行额外的视频操作,延长了训练时间

四、网络搭建

在开始学习的时候,使用的是mnist数据集来做3层的卷积网络,很方便自己搭建模型,这里有github例子可以参考8。但是如果面对100多层,并且是残差设计的深度学习网络,手动搭建起来就有点麻烦。Torch内部自带有一些常用的网络模型如resnet3,vgg等模型,使用的时候只需要进行加载,并且进行一些简单的修改。这里可能会有问题,为什么可以修改?哪里需要修改?

  • 为什么可以修改?

答:输入数据的尺寸大小是对卷积层没有影响的,卷积层只对输入数据的维度感兴趣。只有全连接层对输入数据尺度有影响。

  • 哪里需要修改?

答:根据上一个问题的回答。只需要对网络模型修改第一层卷积conv1的输入维度和全连接层对应的数据大小即可。注:对于mnist实验来说,第一层卷积输入维度是1,而在本次实验中,其值可能为2,4,10。

4.1 加载模型

torchvision库还有dataset等好用的东西,可以研究一下。以下代码粘贴复制即可直接使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import torchvision.models

# True 表示有有预训练,本地没有的话,就会自动从网络中下载模型
obase_model = torchvision.models.resnet101(True)

# 会打印出网络的结构,太长了,结果不好贴
# print(obase_model)

# 获得模型网络的构成,如conv1卷积层结构等
base_model = list(obase_model.modules())

print(base_model[1])

result:
# 输入3维,输出64维,卷积核维7*7,往左往下每次都是移动2个位子,填充大小为3*3
Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)

为了方便理解,贴出obasemodel的打印,点击查看

4.2 修改模型

模型加载好了之后,有些并不能直接使用,可以对Conv2d的参数进行修改,一般来说,只需要修改维度值就好了,修改方法如下:(需要和上面代码联合使用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 得到所有网络类型为Conv2d的下标,取第一个。前面有说为什么需要第一个
first_conv_idx = list(filter(lambda x: isinstance(base_model[x], nn.Conv2d), list(range(len(base_model)))))[0]
conv = base_model[first_conv_idx]
container = base_model[first_conv_idx - 1] # type: torchvision.models.resnet.ResNet
# <class 'torchvision.models.resnet.ResNet'>

# 这里讲输入通道in_channels 改成2,跳转到章节7.1会发现惊喜
new_conv = nn.Conv2d(2, conv.out_channels,conv.kernel_size,conv.stride,conv.padding,
conv.dilation,conv.groups,conv.bias,conv.padding_mode)

# 0表示的是第一个网络(conv1.weight),-7指的是字符串倒数第7个位置
lay_name = list(container.state_dict().keys())[0][:-7]
# conv1

# 修改container中lay_name的网络为new_conv,按道理修改list内的值是不会改变
# 原obase_model的,因为list会重新开辟一个空间,查看章节7.2。但这里的container
# 等于obase_model,就可以进行修改,第5行代码有注释。这里可以将container替换
# 成obase_model
setattr(container,lay_name,new_conv)

print(obase_model) # 发现网络已经被修改

理论上这样就修改成功了,但是这里使用的是预训练的网络,更换了第一层网络后,其权重并没有跟着修改,接下来把权重也给复制上去,完整代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import torchvision.models

obase_model = torchvision.models.resnet101(True)
base_model = list(obase_model.modules())

first_conv_idx = list(filter(lambda x: isinstance(base_model[x], nn.Conv2d), list(range(len(base_model)))))[0]
conv = base_model[first_conv_idx]
container = base_model[first_conv_idx - 1] # type: torchvision.models.resnet.ResNet

new_conv = nn.Conv2d(2, conv.out_channels,conv.kernel_size,conv.stride,conv.padding,
conv.dilation,conv.groups,conv.bias,conv.padding_mode)

lay_name = list(container.state_dict().keys())[0][:-7]

# 注意:从这里开始获取预训练的值
# 这里面都是weights
params = [x.clone() for x in conv.parameters()]

kernel_size = params[0].size()
# torch.Size([64, 3, 7, 7])

# 这种语法仅仅支持torch.Size类型,torch.tensor类型都不支持
new_kernel_size = kernel_size[:1] + (2, ) + kernel_size[2:]
# torch.Size([64, 2, 7, 7])

# contiguous表示如果内存是连续的则返回原来的tensor,其他语法见7.3 7.4
new_kernels = params[0].data.mean(dim=1, keepdim=True).expand(new_kernel_size).contiguous()

# 对新网络的权重进行赋值
new_conv.weight.data = new_kernels

# 更新到网络容器container之中
setattr(container,lay_name,new_conv)

修改成功,最后一层的修改方式和第一层的修改方式一样。注:kernel_size[:1] + (2, ) + kernel_size[2:]这种神奇的用法只适用于torch.Size类型。

4.3 TSN完整模型设计

TSN作者写的代码把三个网络写到一个Class里面,通过参数来选择不同的网络,虽然降低了代码冗余度,但是导致整个class太长,不方便阅读。这里我建立3个网络,方便阅读的同时,便于维护。

仔细查看TSN网络的构成,我发现TSN三种不同的输入对于网络部分(Temporal Segment Networks)来说是独立的!!意味着我可以单独实现RGB、RGBDiff、Optical Flow。光流算法需要依赖OpenCv库,这里在最后进行实现或者不实现,先从最简单的RGB开始。由于完全介绍的篇幅太长,转至Github上查看,有丰富的代码注释。

图1 TSN网络结构

4.3 RGB网络

点击传送

4.4 RGBDiff网络

还未实现

4.5 Optical Flow网络

还未实现

五、网络训练

训练相对较慢,需要动态调整学习速率 10

六、网络测试

在损失降低到一定程度后,开始测试。将训练模式转为测试模式即可。

七、语法小技巧

7.1 代码自动补全

python作为一门动态语言,在使用的过程中一般不需要注意变量类型。不过从实际使用角度上面来讲有点背道而驰了,因为变量类型必须要用到,但是在语法上面却没有了提示,这严重影响了写代码的体验(因为不知道类型,无法自动补全),以下提出一种方法,解决这个问题。(代码需要看前面的才看的懂,如果仅仅想了解如何指定类型,看参考文献7

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import torchvision.models
import torch.nn as nn

base_model = torchvision.models.resnet101(True)

# 这里因为使用了list,编辑器无法知道base_model里面参数的类型,必须要运行起来才知道
base_model = list(base_model.modules())

# 这里获取了第一个卷积的 index 下标
first_conv_idx = list(filter(lambda x: isinstance(base_model[x], nn.Conv2d), list(range(len(base_model)))))[0]

# 这里通过type指定类型,这样编辑器就会知道了,就可以自动补全了
conv = base_model[first_conv_idx] # type: nn.Conv2d

print(conv.kernel_size) # 舒服

7.2 list使用细节

list的会开辟新的储存空间

1
2
3
4
5
6
7
8
9
10
11
a = [1,2,3,4]
print(a[1])

b = list(a)
b[1] = 10

print(a[1])

result:
2
2

7.3 Torch.mean()用法

官方说明如下:

torch.mean(input, dim, keepdim=False, out=None)

返回新的张量,其中包含输入张量input指定维度dim中每行的平均值。

若keepdim值为True,则在输出张量中,除了被操作的dim维度值降为1,其它维度与输入张量input相同。否则,dim维度相当于被执行torch.squeeze()维度压缩操作,导致此维度消失,最终输出张量会比输入张量少一个维度。

参数:

  • input (Tensor) - 输入张量
  • dim (int) - 指定进行均值计算的维度
  • keepdim (bool, optional) - 输出张量是否保持与输入张量有相同数量的维度,如果False,dim对应的那个维度消失,如果为True,dim对应的那个维度不会消失,大小变为1
  • out (Tensor) - 结果张量

他的具体算法结构是什么样子呢?先举个例子

1
2
3
4
5
6
7
8
9
10
11
12
import torch

a = torch.randn([1,2,3,4])

print(a.mean(dim=0).size())
print(a.mean(dim=1).size())
print(a.mean(dim=1,keepdim=True).size())

result:
torch.Size([2, 3, 4])
torch.Size([1, 3, 4])
torch.Size([1, 1, 3, 4])

从输出结果可以看见,dim等于多少,那一个维度就会消失(计算成为了均值),这样他的内部算法就比较好猜了,应该是如下这样:

1
b[][][] = (a[][0][][] + a[][1][][]) / 2 # 大致意思,应该运行不了,空括号内变量对应1,3,4维度不同值

其他max,min使用方法类似

7.4 torch.tensor.expand()用法

官方介绍

expand(*sizes)

返回tensor的一个新视图,单个维度扩大为更大的尺寸。 tensor也可以扩大为更高维,新增加的维度将附在前面。 扩大tensor不需要分配新内存,只是仅仅新建一个tensor的视图,其中通过将stride设为0,一维将会扩展位更高维。任何一个一维的在不分配新内存情况下可扩展为任意的数值。

参数: - sizes(torch.Size or int…)-需要扩展的大小

注意,只能对大小为1的那个维度进行扩展,扩展方法为复制原来的值,使用方法如下:

1
2
3
4
5
6
7
8
9
10
a = torch.randn([1,2,3]).float()

print(a.expand((2,2,3)))

result:
tensor([[[ 0.1812, -2.6267, -0.5700],
[ 0.1686, -1.1026, -0.0917]],

[[ 0.1812, -2.6267, -0.5700],
[ 0.1686, -1.1026, -0.0917]]])

错误用法如下:

1
2
3
4
5
6
7
8
a = torch.randn([1,2,3]).float()
print(a.expand((1,4,3)))

result:
Traceback (most recent call last):
File "F:/Users/Zever/Desktop/tsn-pytorch-master/tsn-pytorch/test.py", line 38, in <module>
print(a.expand((1,4,3)))
RuntimeError: The expanded size of the tensor (4) must match the existing size (2) at non-singleton dimension 1.

7.5 数据集制作

7.5.1 方法一

__getitem__是一个内建函数,它在什么时候被调用呢?下面用一个简单的例子来说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from torch.utils import data

class myDataSet(data.Dataset):
def __init__(self):
print("__init__")

# 该内建函数必须实现,返回值一般为data,Lable
def __getitem__(self, item):
print("__getitem__")
return 1,2

# 该内建函数必须实现,返回值为全部数据的长度,DataLoader函数需要调用次功能
def __len__(self):
return 10

dataset = myDataSet()
data_loader_train = data.DataLoader(dataset = dataset, batch_size = 2,shuffle=True)

print("start for")
for a,b in data_loader_train:
print(a)

结果是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
__init__
start for
__getitem__
__getitem__
tensor([1, 2])
__getitem__
__getitem__
tensor([3, 4])
__getitem__
__getitem__
tensor([5, 6])
__getitem__
__getitem__
tensor([7, 8])
__getitem__
__getitem__
tensor([ 9, 10])

说明只有在进行读取数据集的时候__getitem__才会运行,在使用data.DataLoader的时候,不会调用。探寻这个有什么意义?

  • 如果data.DataLoader会调用,那么只要使用了DataLoader函数,它就会将全部数据写入内存中,这种方法对计算机内存极为不友好。
  • 如果是使用的时候调用,那么可以自定义__getitem__的实现方式,实现每次只读batch_size大小的数据进入内存。

注意:在DataLoader中,我设置了shuffle=True,但是输出结果并没有被打乱。查了资料后,发现它打乱的是__getitem__函数的item参数,修改代码如下,可以看到数据已经被打乱。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from torch.utils import data

class myDataSet(data.Dataset):
def __init__(self):
self.a = 0
self.b = 0
print("__init__")

# item其实就是index数组下标
def __getitem__(self, item):
print("__getitem__")
self.a += 1
return item, self.a

def __len__(self):
return 9


dataset = myDataSet()
data_loader_train = data.DataLoader(dataset = dataset, batch_size = 3,shuffle=True)

print("start for")
for a,b in data_loader_train:
print(a)
print(b)

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
__init__
start for
__getitem__
__getitem__
__getitem__
tensor([4, 2, 5])
tensor([1, 2, 3])
__getitem__
__getitem__
__getitem__
tensor([7, 0, 1])
tensor([4, 5, 6])
__getitem__
__getitem__
__getitem__
tensor([8, 6, 3])
tensor([7, 8, 9])

可以看到,数据被打乱。

7.5.2 方法二

使用TensorDataset实现对数据的封装,这种方法适用于比较简单的数据。其中enumerate是对数据加上index下边。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from torch.utils import data
from torch.utils.data import TensorDataset
import torch

a = [1,2,3,4] # Data
b = [6,7,8,9] # Lable

c = TensorDataset(torch.tensor(a),torch.tensor(b))

d = data.DataLoader(dataset=c,batch_size=2,shuffle=True)

for index,(D,L) in enumerate(d):
print("index:%d"%index)
print(D)
print(L)

输出结果为:

1
2
3
4
5
6
index:0
tensor([3, 4])
tensor([8, 9])
index:1
tensor([1, 2])
tensor([6, 7])

八、调试问题

8.1 cuda runtime error (59)

在计算loss的时候出现的错误,原因是lable没有从0开始计数9。为什么非要从0开始计数,这要从误差函数的实现方式说起。为了方便理解,先抛出一个问题,看下面代码(运行正确的代码截断):

1
2
3
4
# # output : torch.Size([3, 101])
# # OL : torch.Size([3])

loss = criterion(output,OL) # type: torch.Tensor

可以误差函数criterion输出的output的size和OL的size不一样,不一样就不能直接计算,必须要经过处理。本人猜测,criterion内部先求出了output最大值对应的下标index(0~100),这样就变成了OL一样的size,这样就可以和OL进行计算(我测试过,不是求最大值),所以label的值的范围也是0~100。而数据集默认给的范围是1~101,故而由于数组越界而报错,严重甚至蓝屏(显卡出问题容易蓝屏)。解决方法,从数据集返回的lable下手,第三章数据集修改已更新。

接下来开始跟踪criterion内部的实现方式,我困了,下次弄

参考文献

1. Temporal Segment Networks: Towards Good Practices for Deep Action Recognition
2. Two-Stream Convolutional Networks for Action Recognition in Videos
3. ResNet结构分析
4. Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift
5. 计算机视觉—光流法(optical flow)简介
6. 图像差分的方法
7. pyCharm中python对象的自动提示
8. torch 框架奇葩语法
9. cuda runtime error (59)
10. pytorch学习笔记(十):learning rate decay(学习率衰减)
-------------本文结束感谢您的阅读-------------
0%