SocialGAN源代码阅读报告
⽬录
数据处理部分:trajectories.py及数据加载部分:loader.py
Social GAN源码是基于pytorch框架来写的,trajectories.py是⽂件的数据处理部分。⾸先,我们需要知道pytorch的数据加载到模型的操作顺序。
1. 创建⼀个Dataset对象,Dataset是⼀个代表着数据集的抽象类,所有关于数据集的类都可以定义成其⼦类,只需要重写__gititem__
函数和__len__函数即可。
2. 创建⼀个DataLoader对象,由于定义好的数据集不能都存放在内存中,否则内存会爆表,所以需要定义⼀个迭代器,每⼀步⽣成⼀个
batch,这就是DataLoader的作⽤,其能够为我们⾃动⽣成⼀个多线程的迭代器,只要传⼊⼏个参数即可,例如batchsize的⼤⼩,数据是否打乱等。
3. 循环调⽤DataLoader对象,将数据⼀批⼀批的加载到模型进⾏训练。
知道了pytorch数据加载的操作顺序,理解trajectories.py就很简单了。该⽂件主要对数据集进⾏了⼀定的处理,其重点在于TrajectoryDataset类的实现,该类继承⾄torch.utils.data中的Dataset类,其主要完成的⼯作就是上述操作顺序的第1步,准备数据
集。其主要对原始的数据集进⾏预处理,原始的数据集共有4列,分为为frame id,ped id,x,y,我们要对这些数据进⾏处理,⽣成我们想要的数据。TrajectoryDataset类的__init__函数的⼤致思路如下:
其主要对每个序列sequence进⾏处理,每个sequence的长度为seq_len=obs_len+pred_len,其主要是取出完整出现在这个序列
seq_len个帧中的⼈的数据,并且每个序列中的完整出现的⼈的数量必须要⼤于其参数min_ped,程序默认是1。举个例⼦,假设⼀个序列⼀共20帧,obs_len=8,pred_len=12,对于这个序列⽽⾔,完整出现在这个序列的⼈数为1,那么我们不需要这个⼈的数据,舍弃。因为完整出现在这个序列的⼈才⼀个,没有办法到⼈与⼈之间的交互,对⾏⼈轨迹预测意义不⼤。⽽如果完整出现在个序列中的⼈数为3,那么这3个⼈的数据我们将都会保存,因为这⾥⾯可以考虑到⼈与⼈之间的交互关系。__init__函数最终得到下列数据:
self.obs_traj      #shape[num_ped,2,obs_len]
self.pred_traj      #shape[num_ped,2,pred_len]
self.obs_traj_rel  #shape[num_ped,2,obs_len]
self.pred_traj_rel  #shape[num_ped,2,pred_len]
self.loss_mask      #shape[num_ped,seq_len]
<_linear_ped #shape[num_ped]
self.seq_start_end
其中,nun_ped为在数据集当中⼀共有多少满⾜的⼈,self.obs_traj即这nun_ped个⼈在obs_len个坐标数据。self.obs_traj_rel是每⼀帧相对于上⼀帧的位置变化。self.loss_mask源代码中似乎没有什么太⼤作⽤,_linear_ped表⽰这个⼈的轨迹是否线性,其是通过调⽤trajectories.py⽂件中的poly_fit函数返回是否线性的标志位,该函数的⼤致意思通过对预测轨迹进⾏最⼩⼆乘拟合,当拟合的残差⼤于⼀定阈值,认为轨迹不线性。主要注意的是,TrajectoryDataset类中的所有pred_traj都不是预测轨迹,⽽是预测轨迹的真值,因为这是从数据集中读到的数据。self.seq_start_end其是⼀个元组列表,其长度表⽰⼀共有多少满⾜条件的序列。其⽐较不好理解,所以举个例⼦,假设在所给数据集中⼀共有5个序列满⾜完整出现的⼈数⼤于min_ped,且这5个序列分别有2,3,2,4,3个⼈完整出现,那么
self.seq_start_end的长度为5,self.seq_start_end等于[(0,2),(2,5),(5,7),(7,11),(11,14)],也就是说
num_ped=14,self.seq_start_end的主要作⽤是为了以后⼀个⼀个序列的分析的⽅便,即由要分析的序列,即可根据它的值得到对应在这个序列中有哪⼏个⼈以及这⼏个⼈的所有相关数据。需要注意的是,因为这是在pytorch中调⽤接⼝,所以相关数据需要转换成tensor。
另外,正如上⽂所说,由于TrajectoryDataset继承⾄Dataset类,所以其需要重写__getitem__和__len__函数,__getitem__函数的作⽤就是有索引得到数据集在__init__函数处后的⼀个数据,其返回⼀个列表,在本例中,就是⼀个序列的数据。__len__函数的作⽤就是得到处理后的数据的长度,在本例中,就是所有满⾜条件的序列的长度。
总结来说,TrajectoryDataset为我们准备好了所有的数据集,但是我们要怎么⼀批⼀批的加载数据呢?这就到了pytorch加载模型的操作顺序的第2步了,即创建DataLoader对象,DataLoader是pytorch中数据读取的重要接⼝类。DataLoader有很多参数:
class DataLoader(object):
def __init__(self, dataset, batch_size=1, shuffle=False, sampler=None,
batch_sampler=None, num_workers=0, collate_fn=None,
pin_memory=False, drop_last=False, timeout=0,
worker_init_fn=None, multiprocessing_context=None)
这是DataLoader类的⼀些参数,这⾥只讲解⼏个,主要是在源代码中⽤到的⼏个:
dataset:传⼊的数据集,在本例中,就是TrajectoryDataset准备好的数据集
batch_size:每个batch中有多少样本
shuffle:是否将数据打乱
num_workers:处理数据加载的进程数
collate_fn:将⼀个列表中的样本组成⼀个mini-batch的函数
那么我们就来看看数据加载部分loader.py源代码。
loader.py⾥⾯只定义了data_loader⼀个函数,该函数内部⾸先创建了⼀个TrajectoryDataset类对象dset,它就是要传⼊DataLoader的参数,对应于dataset。注意,我们可以修改TrajectoryDataset中的参数min_ped来控制⼀个序列中完整出现的⼈数。当你想考虑较多⼈之间的交互,可以改⼤min_ped值,
该值默认为1。
data_loader函数紧接着创建了⼀个DataLoader类对象loader,该函对象的dataset参数即为刚刚创建的dset,的atch_size默认为64,我们可以控制shuffle为true或者false来选择让数据是否打乱。其中,对于⾃定义数据加载来说,最重要的是要重写collate_fn,在本例中,其在trajectories.py⽂件中新建了seq_collate函数,并将其赋值给collate_fn,seq_collate将batch_size的数据重新打包,将这些数据打包成我们要需要的数据格式,以便送⼊⽹络进⾏训练。也就是说,传⼊seq_collate的是batch_size个数据,每个数据对应⼀个序列。注意这⾥的数据并不是TrajectoryDataset中准备的全部数据,⽽是仅仅batch_size个数据,seq_collate将这batch_size数据合并打包组成⼀个mini-batch。这个函数内部只是对TrajectoryDataset类中准备的数据再次进⾏了⼀定的加⼯处理,例如将维度进⾏了交换,
[N,2,seq_len]→[seq_len,N,2],其主要是为了和LSTM⽹络的输⼊格式保持⼀致。
所以说,如果我们调⽤⼀次data_loader函数,就将得到很多批batch_size⼤⼩的数据。当我们对DataLoader对象实例loader进⾏for循环时,每次循环将得到⼀批batch_size⼤⼩的数据,然后加载到模型进⾏训练。这也就是pytorch数据加载到模型的操作顺序中的第3步。
总结来说,通常给定的数据集与我们模型⽹络要求的输⼊格式是不⼀样的,这个时候我们需要⾃定义数据格式。我们可以创建⼀个类继承⾄Dataset,在⾥⾯将数据集准备好,同时要重写__getitem__函
数与__len__函数。接下来,我们创建该类的⼀个对象,并将它传给DataLoader类的参数dataset。同时,我们要在重写⼀个函数负责将batch_size个数据打包成我们想要的⽹络模型的输⼊格式,形成⼀个mini-batch,以供⽹络进⾏训练。
好了,数据处理部分与数据加载部分已经讲解完毕,下⾯讲解⼀下⽹络模型部分,此部分需要结合论⽂来看。
⽹络模型部分:models.py
想要了解这部分内容,需要⾸先了解⼀下Module,在pytorch中,nn.Module是所有神经⽹络单元的基类。pytorch在nn.Module 中实现了__call__⽅法,⽽在__call__⽅法中调⽤了forward函数。__call__⽅法的主要作⽤是是类对象具有类似函数的功能,可以在类对象中进⾏传参,⽽__call__⽅法中⼜调⽤了forward函数。pytorch中的nn.Module类都包含了__init__⽅法与__call__⽅法。
__init__:类的初始化函数,类似C++中的构造函数
__call__:使得类对象具有类似函数的功能
举个例⼦,假设A是⼀个class,a是A的⼀个类对象,当我们执⾏a=A(),这会调⽤__init__⽅法构造类的对象;⽽当我们执⾏a(),其会调⽤
__call__⽅法,⽽在__call__⽅法内部⼜会调⽤forward函数,注意,这是通过类对象调⽤的,所以说使得类对象具有类似函数的功能。
关于nn.Module及__init__、__call__、forward可以参见下⾯两个⽹址:
好了,接下来让我们来详解⽹络模型部分吧!
models.py共包含2个函数以及6个类,其中SocialPooling类为作者早期研究Social LSTM中⽤到的池化层,在Social GAN没有⽤到,所以我们不需关注这个类。需要说明的是,在这些类当中,__init__主要是初始化⽹络结构,⽽forward函数则是真正有数据在⾥⾯流动进⾏训练或者评估。
先来看两个函数,make_mlp主要是构造多层的全连接⽹络,并且根据需求决定激活函数的类型,其参数dim_list是全连接⽹络各层维度的列表.get_noise函数主要是⽣成特定的噪声。
接下来我们来看encoder类部分,由其__init__⽅法可以看出,其⽹络结构主要包括⼀个全连接层和⼀个LSTM⽹络。研究其forward函数,⾸先,最原始的输⼊是这⼀批输数据所有⼈的观测数据中的相对位置变化坐标,即当前帧相对于上⼀帧每个⼈的坐标变化,其经过⼀个
2*16的全连接层,全连接层的输⼊的shape:[obs_len*batch,2],输出:[obs_len*batch,16],然后需要经过维度变换变成3维的以符合LSTM⽹络中输⼊input的格式要求,LSTM的输⼊input的shape为[seq_l
en,batch,input_size],然后再把h_0和c_0输⼊LSTM,输出隐藏状态h_t记为final_h。其中,LSTM⽹络需要设置⼀些参数,如input_size:输⼊数据的特征数量;hidden_size:隐藏状态的特征
数;num_layers:循环⽹络有⼏层LSTM以及其他⼀些参数。以下为LSTM的参数设置以及输⼊输出的格式,其中num_directions在程序中是1,其表⽰LSTM是双向的还是单向的,1表⽰单向,2表⽰双向。
LSTM的参数共有7个,这⾥只讲前⾯3个,也是必须要设置的3个参数,后⾯四个参数是可选的。
Parameters:
input_size:输⼊数据的特征数量,即输⼊数据的维度
hidden_size:隐藏状态h的特征数量
num_layers:循环⽹络中LSTM的层数
Inputs:input,(h_0,c_0)
input:shape[seq_len,batch,input_size] 包含输⼊序列特征的张量
h_0:shape[num_layers*num_directions,batch,hidden_size] 包含batch中每个元素初始隐藏状态的张量
c_0:shape[num_layers*num_directions,batch,hidden_size] 包含batch中每个元素初始细胞状态的张量
Outputs:output,(h_n,c_n)
output:shape[seq_len,batch,num_directions*hidden_size] 输出最后⼀层seq_len个时刻,每个时刻隐藏状态的集合,即h_1,h_2,...,h_n
h_n:shape[num_layers*num_directions,batch,hidden_size] 包含t=seq_len这个时候batch中每个元素隐藏状态的张量
c_n:shape[num_layers*num_directions,batch,hidden_size] 包含t=seq_len这个时候batch中每个元素细胞状态的张量
需要特别注意的是,也是我⼀开始对LSTM⼀直⽐较困惑的地⽅。单层LSTM并不是说⾥⾯只有⼀个cell,⽽是这⼀单层LSTM中包括了
源代码电影讲解seq_len个cell,每个cell按顺序分别输出隐藏状态h和细胞状态c。就是说如下图,该图并不是说LSTM有5层,该图LSTM只有⼀层。但是它的seq_len有5个,所以有5个cell,每个cell对应输出隐藏状态h和细胞状态c。
LSTM
encoder部分的结构抽象图如下图所⽰。图中的batch表⽰这⼀批数据中所有的⼈的数量。也就是说其主要⼲了两件事,⾸先将原始数据2维的坐标变化数据提升⾄16维,再送⼊LSTM⽹络产⽣final_h,以供后续使⽤。另外,源代码中有⼀个地⽅与论⽂中描述的不太⼀样,论⽂中在2*16的全连接层之后接了Relu激活函数,但是在源代码中并没有体现。
encoder⽹络结构抽象图
接下来来看池化层,即PoolHiddenNet类,其__init__⽅法中分别定义了2*16以及48*512*8两个全连接层,注意48*512*8这个全连接层带relu激活函数。我们主要看forward函数,forward函数的第⼀个参数h_states就是上⾯encoder的输出final_h,其维度为
[num_layers,batch,hidden_size],在程序中即[1,batch,32],batch即batch_size个sequence序列中的总⼈数,每⼀批数据其个数⼀般是不相等的。forward函数内部对DataLoader加载的batch_size个sequence序列逐次处理。在对每⼀个序列进⾏处理时,对每⼀个序列处理的⽰意图如下图所⽰。
Pooling Module⽹络结构抽象图
上图是Pooling Module每个序列的处理⽰意图.在forward函数中,其计算⼈的相对位置⽐较巧妙,其主要通过两次repeat操作将⼀个序列中的N个⼈的位置信息重复N次,这两次repeat是不同的repeat,假设⼀个序列中⼀共3个⼈,则第⼀次repeat得到
[P1,P2,P3,P1,P2,P3,P1,P2,P3],第⼆次repeat得到[P1,P1,P1,P2,P2,P2,P3,P3,P3],两个矩阵相减即可得到N*N⾏的矩阵,每⼀⾏代表相对坐标。以3⼈为例,得到的是P1->P1,P2-P1,P3->P1,P1->P2,P2->P2,P3->P2,P1->P3,P2->P3,P3->P3(Pm->Pn表⽰第m个⼈相对于第n个⼈的相对位置坐标)。通过将这[N*N,2]数据输⼊⾄2*16的全连接层得到shape为[N*N,16]的curr_rel_embedding。另⼀
⽅⾯,针对输⼊的final_h,其包括了⼀批数据所有⼈的隐藏状态,⽽不仅仅是⼀个序列的⼈的隐藏状态,所以需要对其提取每个序列的⼈的隐藏状态,同时进⾏维度变换以及repeat操作,仍以3⼈在⼀个序列为例,得到[H1,H2,H3,H1,H2,H3,H1,H2,H3],其为curr_hidden_1,维度为[N*N,32],通过将curr_rel_embedding和curr_hidden_1合并,形成mlp_h_input输⼊⾄48*512*8的全连接层,然后在对其进⾏Maxpooling,得到⼀个序列的pool_h,shape为[N,8],通过对这批数据所有序列都进⾏相应的处理,将所有的pool_h合并成新的
pool_h,shape为[batch,8],每⼀⾏对应⼀个⼈,即论⽂中所说的a pooled tensor Pi for  each person。如果仔细研究论⽂中的
figure2和论⽂中的figure3,你会发现作者以不同的填充图案以表征不同的tensor。在figure3中,实⼼的绿⾊表⽰经2*16全连接层MLP输出的curr_rel_embedding,其与encoder部分LSTM的输出斜条纹表⽰的curr_hidden_1进⾏合并(斜条纹正好与figure2的LSTM的输出是对应的),输⼊⾄48*512*8的全连接层,然后进⾏Maxpooling,然后得到了为每个⼈⽣成了⼀个特征张量Pi,其由砖块填充图案表⽰。是不是发现⼀切都对应上了,哈哈哈。
好了,接下来我先不急着看decoder部分,我们先来看TrajectoryGenerator类。如下图,其为该类的抽象图。该类的__init__⽅法分别定义了Encoder类、Decoder类、以及池化层类的实例。同时根据在输
⼊decoder部分的LSTM之前是否需要经过⼀个全连接层定义了⼀个带relu激活函数的全连接层。如果元组类型的参数noise_dim的第⼀个元素不为0或者有池化层或者encoder部分LSTM的hidden_size与decoder部分的LSTM的hidden_size不⼀致时,说明需要全连接层,这样可以保证最后的decoder_h符合LSTM的格式。
TrajectoryGnerator抽象图
来看其forward函数,⾸先得到encoder部分LSTM的隐藏状态输出final_encoder_h,即encoder⽹络结构抽象图中的final_h。然后根据是否有池化层进⾏以下操作:
有池化层,将上⼀步的final_encoder_h等数据送⼊池化层,得到pool_h,并将final_encoder_h与pool
_h进⾏拼接,得到
mlp_decoder_context_input
没有池化层,直接将上⼀步得到的final_encoder_h经过维度变换作为mlp_decoder_context_input
如果需要经过全连接层,则还需要将得到的mlp_decoder_context_input经过全连接层,如果有池化层,其必然是要经过全连接层的,且全连接层的输⼊层维度为40,⽽如果不需要池化层,则全连接层的输⼊层维度为32。经过全连接层之后,将得到的noise_input与⼀个噪声z进⾏合并,得到decoder_h,这个噪声分为全局噪声还是⾮全局噪声,如果是全局噪声,则每个⼈的噪声都⼀样,否则每个⼈的噪声都不⼀样。如果不经过全连接层,则decoder_h=noise_input=mlp_decoder_context_input。然后将最后⼀个观测帧中⼈的位置以及位置变化以及有decoder_h与decoder_c组成的元组等参数送⼊Decoder类的实例对象中作为其参数,经过decoder之后,得到预测的每⼀帧相对于上⼀帧的位置变化数据。
好了,接下来来看decoder部分吧。其__init__⽅法中定义了⼀个LSTM⽹络结构,⼀个2*16的全连接层,⼀个32*2的全连接层,并且根据是否每⽣成⼀次预测数据都池化⼀次⼜定义了池化层与全连接层。因为池化过后都是要经过TrajectoryGnerator抽象图中的全连接层的。decoder⽹络结构抽象图抽象图如下图所⽰。
decoder⽹络结构抽象图
如上图所看到的的,⾸先,对要预测的帧数进⾏循环,每次循环预测出⼀帧。其先将最近⼀帧的位置变化输⼊到2*16的全连接层,得到[batch,16]的decoder_input,再维度变换为LSTM的input的格式,然后decoder_h即为TrajectoryGnerator抽象图中的
decoder_h,decoder_h和decoder_c都是在TrajectoryGnerator的forward函数⾥⾯⽣成的。得到的output经过维度变换再输⼊⾄32*2的全连接层,得到[batch,2]的相对于当前帧的位置变化。这样for循环后,pre_len个坐标变化就得到了,根据每⼀帧的坐标变化,⾃然就可以得到预测轨迹了。另外decoder⾥有⼀个pool_every_timestep选项,也就是每预测出⼀帧的坐标变化后是否要重新池化。就
是说把Pooling Module⽹络结构抽象图中由encoder部分LSTM产⽣的输⼊final_h更新为decoder部分LSTM最新输出的隐藏状态作为Pooling Module的新输⼊去替换final_h,然后池化后再经过TrajectoryGnerator抽象图中的全连接层,不过注意的是,除了第⼀次添加噪声z之外,后续每⼀步的池化都不需要再追加噪声z了。所以,如果pre_len=12,其会池化12次,只有第⼀次需要z,后续不在追加噪声z。
接下来,就是最后⼀个类TrajectoryDiscriminator了,这个类⽐较简单。其主要就是对轨迹进⾏打分,以判断轨迹是真实的轨迹还是预测的轨迹。其主要是copy了⼀份上⽂encoder部分的⽹络,然后得到final_h,另外⼜搭建了⼀个全连接层,该全连接层输出⽹络⽣成的分数。另外这个类⾥⾯有⼀个d_type选项,其默认是local,如果是local的话,其会直接把final_h做维度变化作为全连接层的输⼊。如果是global的话,final_h需要先作为⼀个池化层的输⼊,经过池化以后再输⼊⾄全连接层⽣成得分。程序默认d_type为local,也就是独⽴的处理每⼀条轨迹,为每⼀条轨迹打分。global的话我猜⼤致是直接为这⼀批轨迹打分,这⼀批轨迹的分数都是⼀样的,即他们要么全被当做真实轨迹要么全被当做预测轨迹。
OK,全⽂结束

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。