如何通过图卷积来进行群组分析

[源码解读]Learning Actor Relation Graphs for Group Activity Recognition

论文链接:https://arxiv.org/pdf/1904.10117.pdf

代码链接:https://github.com/wjchaoGit/Group-Activity-Recognition

前言:这篇文章是为数不多的研究群组分析的文章,群组分析可用的且比较主流的数据集目前就两个:1. volleyball 和 2. collective;所以大部分工作对数据集的处理方法以及提取特征的方式大同小异,不同的仅是进行识别的部分,这篇文章采用的是图卷积的方式,至于为何有效暂且不讨论。在这里我仅仅记录源码的学习过程,主要分为以下几部分:1.数据集的处理2.网络的搭建3.训练与测试

一、数据集处理

这里仅以volleyball数据集来举例,因为它的数据量更大,更具有代表性。相关源码仅记录主要部分:

1.1 volleyball.py 采样帧

先定义了两个标签的数组,分布代表个人行为和群组活动:

``

'''八个群组活动'''
ACTIVITIES = ['r_set', 'r_spike', 'r-pass', 'r_winpoint',
              'l_set', 'l-spike', 'l-pass', 'l_winpoint']

NUM_ACTIVITIES = 8

'''九个个人行为'''
ACTIONS = ['blocking', 'digging', 'falling', 'jumping',
           'moving', 'setting', 'spiking', 'standing',
           'waiting']
NUM_ACTIONS = 9
1.1.1 对帧图像标号

得到每一帧的标签,并且把标签都转换成序号表示:

def volley_read_annotations(path):
    """
    reading annotations for the given sequence
    """
    annotations = {}

    gact_to_id = {name: i for i, name in enumerate(ACTIVITIES)}
    act_to_id = {name: i for i, name in enumerate(ACTIONS)}

    with open(path) as f:
        for l in f.readlines():
            values = l[:-1].split(' ')
            file_name = values[0]
            activity = gact_to_id[values[1]]

            values = values[2:]
            num_people = len(values) // 5

            action_names = values[4::5]
            actions = [act_to_id[name]
                       for name in action_names]

            def _read_bbox(xywh):
                x, y, w, h = map(int, xywh)
                return y, x, y+h, x+w
            bboxes = np.array([_read_bbox(values[i:i+4])
                               for i in range(0, 5*num_people, 5)])

            fid = int(file_name.split('.')[0])
            annotations[fid] = {
                'file_name': file_name,
                'group_activity': activity,
                'actions': actions,
                'bboxes': bboxes,
            }
    return annotations

这一部分输入的是数据集已有的标签文本文件,返回的是每一帧(fid)的标签,包括**(文件名,群组活动,个人行为,人物级边界框),其中边界框按照(x,y,w,h)**给出

接着通过volley_read_datasetvolley_all_frames两个函数,完成对数据集所有帧的标号,形式是**(sid,fid),其中sid**代表视频的第几段,fid代表一段视频的第几帧。

``

def volley_read_dataset(path, seqs):
    data = {}
    for sid in seqs:
        data[sid] = volley_read_annotations(path + '/%d/annotations.txt' % sid)
        #字典嵌套字典的格式
    return data


def volley_all_frames(data):
    frames = []
    for sid, anns in data.items():
        for fid, ann in anns.items():
            frames.append((sid, fid))

    return frames
1.1.2 采样帧

标号完成之后,需要进行采样用于训练的帧,并且转换Pytorch可以理解的形式(tensor):

``

#获取采样帧方法
def __getitem__(self,index):
    """
    Generate one sample of the dataset
    """

    select_frames=self.volley_frames_sample(self.frames[index])

    sample=self.load_samples_sequence(select_frames)
    
    return sample

很明显上述方法有两个部分:volley_frames_sample 和load_samples_sequence两个方法,分别实现了采样和将标签转换成pytorch张量的功能:

1. volley_frames_sample

``

sid, src_fid = frame

if self.is_finetune:
    #stage1
    if self.is_training:
        fid=random.randint(src_fid-self.num_before, src_fid+self.num_after)#在中心帧附近随机抽一帧
        return [(sid, src_fid, fid)]
    else:
        return [(sid, src_fid, fid)
                for fid in range(src_fid-self.num_before, src_fid+self.num_after+1)]#在中心帧周围采样10帧
else:
    # stage2
    #这里全部采样中心采样10帧的策略
    if self.is_training:
        # 训练的时候源代码稀疏采样3帧
        # sample_frames=random.sample(range(src_fid-self.num_before, src_fid+self.num_after+1), 3)
        # return [(sid, src_fid, fid)
        #         for fid in sample_frames]
        return [(sid, src_fid, fid)
                # for fid in[src_fid+5,src_fid - 3, src_fid, src_fid + 3, src_fid - 4, src_fid - 1, src_fid + 2, src_fid - 2,src_fid + 1, src_fid + 4]]
                for fid in[src_fid -4, src_fid - 3, src_fid-2, src_fid-1, src_fid , src_fid + 1, src_fid + 2,src_fid +3, src_fid + 4, src_fid + 5]]
    else:
        return [(sid, src_fid, fid) 
                for fid in  [src_fid -4, src_fid - 3, src_fid-2, src_fid-1, src_fid , src_fid + 1, src_fid + 2,src_fid +3, src_fid + 4, src_fid + 5]]

需要说明,src_fid是原数据集中自带box和个人行为标注的帧,这里的采样策略是以该帧为中心,取附近的帧用于训练和测试,由于这里采用两阶段训练,在第一阶段微调预训练模型第二阶段应用图卷积进行训练的采样策略稍有不同,主要体现在训练的时候采样的帧数,阶段一只在中心帧附件采样一帧而阶段二都是采样十帧(源码是采样三帧,这里作了修改,以实现更好的训练效果)

该方法输入是frame的标号(sid,src_fid),返回的是采样的帧集(sid,src_fid,fid)

2. load_samples_sequence

``

def load_samples_sequence(self,select_frames):
    """
    load samples sequence

    Returns:
        pytorch tensors
    """
    
    OH, OW=self.feature_size
    
    images, boxes = [], []
    activities, actions = [], []
    for i, (sid, src_fid, fid) in enumerate(select_frames):

        img = Image.open(self.images_path + '/%d/%d/%d.jpg' % (sid, src_fid, fid))#读取帧图像

        img=transforms.functional.resize(img,self.image_size)
        img=np.array(img)#变成np数组

        # H,W,3 -> 3,H,W
        img=img.transpose(2,0,1)#通道数放最前面
        images.append(img)#保存图片

        temp_boxes=np.ones_like(self.tracks[(sid, src_fid)][fid])#保存由tracklet提供的box
        for i,track in enumerate(self.tracks[(sid, src_fid)][fid]):
            
            y1,x1,y2,x2 = track
            w1,h1,w2,h2 = x1*OW, y1*OH, x2*OW, y2*OH  
            temp_boxes[i]=np.array([w1,h1,w2,h2])
        
        boxes.append(temp_boxes)
        
        
        actions.append(self.anns[sid][src_fid]['actions'])
        
        if len(boxes[-1]) != self.num_boxes:
            boxes[-1] = np.vstack([boxes[-1], boxes[-1][:self.num_boxes-len(boxes[-1])]])
            actions[-1] = actions[-1] + actions[-1][:self.num_boxes-len(actions[-1])]
        activities.append(self.anns[sid][src_fid]['group_activity'])

    images = np.stack(images)
    activities = np.array(activities, dtype=np.int32)
    bboxes = np.vstack(boxes).reshape([-1, self.num_boxes, 4])
    actions = np.hstack(actions).reshape([-1, self.num_boxes])
    

    #convert to pytorch tensor
    images=torch.from_numpy(images).float()
    bboxes=torch.from_numpy(bboxes).float()
    actions=torch.from_numpy(actions).long()
    activities=torch.from_numpy(activities).long()

    return images, bboxes,  actions, activities

注意:由于跟踪器提供的box不一定是完整的(即一张图中boxes数目和设定的num_boxes不符),此时通过if len(boxes[-1]) != self.num_boxes:判断,如果不够的话需要补齐,将boxes特征前几行填在最末,即代码中的boxes[-1] = np.vstack([boxes[-1], boxes[-1][:self.num_boxes-len(boxes[-1])]])。同理,action缺失的也需要填充。该方法最终返回的是一个batchsize里的所有的训练帧图像,边界框坐标,动作标签和活动标签,也就是我们说的样本序列并且都转换成了pytorch tensor.

1.2 dataset.py

为了返回一个pytorch可以使用的数据集,调用return_dataset方法:

``

def return_dataset(cfg):
    if cfg.dataset_name=='volleyball':
        train_anns = volley_read_dataset(cfg.data_path, cfg.train_seqs)
        train_frames = volley_all_frames(train_anns)

        test_anns = volley_read_dataset(cfg.data_path, cfg.test_seqs)
        test_frames = volley_all_frames(test_anns)

        all_anns = {**train_anns, **test_anns}#**
        all_tracks = pickle.load(open(cfg.data_path + '/tracks_normalized.pkl', 'rb'))


        training_set=VolleyballDataset(all_anns,all_tracks,train_frames,
                                      cfg.data_path,cfg.image_size,cfg.out_size,num_before=cfg.num_before,
                                       num_after=cfg.num_after,is_training=True,is_finetune=(cfg.training_stage==1))

        validation_set=VolleyballDataset(all_anns,all_tracks,test_frames,
                                      cfg.data_path,cfg.image_size,cfg.out_size,num_before=cfg.num_before,
                                         num_after=cfg.num_after,is_training=False,is_finetune=(cfg.training_stage==1))

该方法通过调用VolleyballDataset方法制作训练集测试集作为返回结果。

二、模型的搭建

2.1 base_model.py 微调模型

base_model是未采用图卷积的模型,即stage1微调模型时使用的模型。

首先引入一些模块,其中比较重要的是roi_align它是一个用于特征裁剪的库,最初在Fast-RCNN中提出,值得注意的是它无法通过pip下载,需要自己下载文件并安装,感兴趣的可以去这个项目的github网页上查看:https://github.com/longcw/RoIAlign.pytorch

``

from roi_align.roi_align import RoIAlign      # RoIAlign module
from roi_align.roi_align import CropAndResize # crop_and_resize module

接下来我们主要关注,Basenet_volleyball这个类:

2.1.1 初始化方法
    def __init__(self, cfg):
        super(Basenet_volleyball, self).__init__()
        self.cfg=cfg
        
        NFB=self.cfg.num_features_boxes#box的特征通道数
        D=self.cfg.emb_features#的特征通道数
        K=self.cfg.crop_size[0]#裁剪框的大小
        

        if cfg.backbone=='inv3':#选型
            self.backbone=MyInception_v3(transform_input=False,pretrained=True)
        elif cfg.backbone=='vgg16':
            self.backbone=MyVGG16(pretrained=True)
        elif cfg.backbone=='vgg19':
            self.backbone=MyVGG19(pretrained=True)
        else:
            assert False
        
        self.roi_align=RoIAlign(*self.cfg.crop_size)
        
        
        self.fc_emb = nn.Linear(K*K*D,NFB)
        self.dropout_emb = nn.Dropout(p=self.cfg.train_dropout_prob)
        self.fc_actions=nn.Linear(NFB,self.cfg.num_actions)
        self.fc_activities=nn.Linear(NFB,self.cfg.num_activities)
        
        
        for m in self.modules():
            if isinstance(m,nn.Linear):#初始化全连接层的权重和偏置
                nn.init.kaiming_normal_(m.weight)
                nn.init.zeros_(m.bias)

这里面cfg.backbone确定模型的骨干网络,默认是inception_v3,roi_align=RoIAlign(*self.cfg.crop_size)确定裁剪框的大小,其他的参数诸如NFB,D,K都可在参数文件config.py里看到。同时还定义了嵌入操作fc_emb(全连接层),dropout层,还有分类输出层fc_actionsfc_activities

2.1.2 前向传播过程

先对数据进行维度的转换,为了方便计算,把B和T相乘都放在第一维表示,有关张量的含义:images_in_flat表示输入帧图像,boxes_in_flat是每张图上的box标注,以及boxes_idx表示每张帧图片的box标号。

``

# Reshape the input data
images_in_flat=torch.reshape(images_in,(B*T,3,H,W))  #B*T, 3, H, W
boxes_in_flat=torch.reshape(boxes_in,(B*T*N,4))  #B*T*N, 4

boxes_idx=[i * torch.ones(N, dtype=torch.int)   for i in range(B*T) ]
boxes_idx=torch.stack(boxes_idx).to(device=boxes_in.device)  # B*T, N
boxes_idx_flat=torch.reshape(boxes_idx,(B*T*N,))  #B*T*N,

接着将帧图像输入到backbone里面,提取特征:

``

# Use backbone to extract features of images_in
# Pre-precess first
images_in_flat=prep_images(images_in_flat)
outputs=self.backbone(images_in_flat)
    

# Build multiscale features
features_multiscale=[]
for features in outputs:#由inception输出的特征图大小与输出大小不符
    if features.shape[2:4]!=torch.Size([OH,OW]):
        features=F.interpolate(features,size=(OH,OW),mode='bilinear',align_corners=True)
    features_multiscale.append(features)

features_multiscale=torch.cat(features_multiscale,dim=1)  #B*T, D, OH, OW

features_multiscale用于保存多尺度特征,因为根据inception网络架构的设置,在最终特征图输出前几层会有一个辅助输出,这两个特征图的大小是不同的,所以这里通过 features=F.interpolate(features,size(OH,OW),mode='bilinear',align_corners=True)来对辅助输出进行上采样使其与最终输出的大小相同,接着通过torch.cat(features_multiscale,dim=1)将两个特征图按照特征通道进行连接。得到最终提取的特征。

接着使用roi_align在提取出来的特征上做裁剪,得到每帧图像每个box的特征;然后还需要通过嵌入的方式把特征维度大小变换成预设 的大小,在config.py里面有定义。最终的Box特征存入到boxes_states里面:

# RoI Align
boxes_features=self.roi_align(features_multiscale,
                                    boxes_in_flat,
                                    boxes_idx_flat)  #B*T*N, D, K, K,

boxes_features=boxes_features.reshape(B*T*N,-1) # B*T*N, D*K*K有可能不稳定
    
# Embedding to hidden state
boxes_features=self.fc_emb(boxes_features)  # B*T*N, NFB

boxes_features=F.relu(boxes_features)

boxes_features=self.dropout_emb(boxes_features)

boxes_states=boxes_features.reshape(B,T,N,NFB)

得到每个框的特征后就可以将它们输入分类层,预测个人动作分类和群组活动分类:

# Predict actions
boxes_states_flat=boxes_states.reshape(-1,NFB)  #B*T*N, NFB

actions_scores=self.fc_actions(boxes_states_flat)  #B*T*N, actn_num

在群组活动分类之前需要将所有Box的特征进行最大池化,然后再输入到分类层预测群组活动:

# Predict activities
boxes_states_pooled,_=torch.max(boxes_states,dim=2)  #B, T, NFB 最大池化
boxes_states_pooled_flat=boxes_states_pooled.reshape(-1,NFB)  #B*T, NFB

activities_scores=self.fc_activities(boxes_states_pooled_flat)  #B*T, acty_num

接着如果输入的是多帧图片的特征张量,需要在时间维度上取平均作为整个时间序列的预测结果:

if T!=1:
    actions_scores=actions_scores.reshape(B,T,N,-1).mean(dim=1).reshape(B*N,-1)
    activities_scores=activities_scores.reshape(B,T,-1).mean(dim=1)
    
return actions_scores, activities_scores

2.2 gcn_model.py 图卷积模型(核心方法)

和base_model相比,这一部分主要多加入了图卷积方法class GCN_Module(nn.Module),论文的核心方法就在于此:

2.2.1 初始化方法
def __init__(self, cfg):
    super(GCN_Module, self).__init__()
    
    self.cfg=cfg
    
    NFR =cfg.num_features_relation
    
    NG=cfg.num_graph
    N=cfg.num_boxes
    T=cfg.num_frames
    
    NFG=cfg.num_features_gcn
    NFG_ONE=NFG
    
    self.fc_rn_theta_list=torch.nn.ModuleList([ nn.Linear(NFG,NFR) for i in range(NG) ])
    self.fc_rn_phi_list=torch.nn.ModuleList([ nn.Linear(NFG,NFR) for i in range(NG) ])
    
    
    self.fc_gcn_list=torch.nn.ModuleList([ nn.Linear(NFG,NFG_ONE,bias=False) for i in range(NG) ])
    
    if cfg.dataset_name=='volleyball':
        self.nl_gcn_list=torch.nn.ModuleList([ nn.LayerNorm([T*N,NFG_ONE]) for i in range(NG) ])
    else:
        self.nl_gcn_list=torch.nn.ModuleList([ nn.LayerNorm([NFG_ONE]) for i in range(NG) ])

值得说明的是,其中的fc_rn_theta_listfc_rn_phi_list是论文3.3节外观关系Appearance relation)中定义的两个可学习的线性变换:

在这里插入图片描述

在这里插入图片描述

可以看出它们都是全连接层,目的都是将每个box的图特征通道数NFG转换成设定的关系特征通道数NFR,输入Box位置或者特征关系,得到这些关系的线性变换已学习到更好的特征。包括接下来定义的fc_gcn_list图卷积层,实质上也是一个全连接层,并且这个层的输入是构建的图(graph),输出的也是图。

还有nl_gcn_list,采用了按层归一化**(LayerNomalization)**,即对每一层的输入进行归一化操作。

2.2.2 前向传播过程
(1)构建活动关系图( Actor Relation Graphs)

首先要计算各个Box坐标位置之间的距离关系(Position relation),找到每个box的中心坐标,graph_boxes_positions,接着计算这些中心坐标之间的欧式距离,最终形成一个距离矩阵(这个矩阵是对称矩阵),且根据论文描述,当两物体之间的距离大于一定阈值时就将他们之间的关系设为0,这一部分是通过position_mask=( graph_boxes_distances > (pos_threshold*OW) )来判断,之后会用到这个position_mask

# Prepare position mask
graph_boxes_positions=boxes_in_flat  #B*T*N, 4
graph_boxes_positions[:,0]=(graph_boxes_positions[:,0] + graph_boxes_positions[:,2]) / 2 
graph_boxes_positions[:,1]=(graph_boxes_positions[:,1] + graph_boxes_positions[:,3]) / 2 
graph_boxes_positions=graph_boxes_positions[:,:2].reshape(B,N,2)  #B*T, N, 2 每个bounding_box的中心

graph_boxes_distances=calc_pairwise_distance_3d(graph_boxes_positions,graph_boxes_positions)  #B, N, N 

position_mask=( graph_boxes_distances > (pos_threshold*OW) )#

构建图之前还需要计算每个Box特征之间的关系(Appearance relation),采用的是原文中提到的嵌入点积的方式,最终存放在similarity_relation_graph里面
在这里插入图片描述

        relation_graph=None
        graph_boxes_features_list=[]
        for i in range(NG):
            graph_boxes_features_theta=self.fc_rn_theta_list[i](graph_boxes_features)  #B,N,NFR 通过FC改变特征通道数
            graph_boxes_features_phi=self.fc_rn_phi_list[i](graph_boxes_features)  #B,N,NFR

#             graph_boxes_features_theta=self.nl_rn_theta_list[i](graph_boxes_features_theta)
#             graph_boxes_features_phi=self.nl_rn_phi_list[i](graph_boxes_features_phi)

             similarity_relation_graph=
             torch.matmul(graph_boxes_features_theta,graph_boxes_features_phi.transpose(1,2))  #B,N,N

            similarity_relation_graph=similarity_relation_graph/np.sqrt(NFR)#采用原文中提到的嵌入点积方式

            similarity_relation_graph=similarity_relation_graph.reshape(-1,1)  #B*N*N, 1

接着构建ARG(Actor Relation Graphs),并使其变成一个NxN的矩阵,然后利用softmax将内部的距离归一化(0到1之间):

# Build relation graph
relation_graph=similarity_relation_graph

relation_graph = relation_graph.reshape(B,N,N)

relation_graph[position_mask]=-float('inf')#如果欧式距离大于阈值,则关系图中两个box的关系为0

relation_graph = torch.softmax(relation_graph,dim=2)    
(2)对ARG进行图卷积

图卷积的代码相对简单,按照原文中的公式(8)进行图卷积:

在这里插入图片描述

其中G∈RN×N为图的矩阵表示。Z(l)∈RN×d为第l层中节点的特征表示,Z(0)= X, W(l)∈Rd×d为该层特有的可学习权值矩阵。σ(·)为激活函数,本文采用ReLU。根据原文描述,代码表示如下:

# Graph convolution
one_graph_boxes_features=self.fc_gcn_list[i]( torch.matmul(relation_graph,graph_boxes_features) )#B, N, NFG_ONE
one_graph_boxes_features=self.nl_gcn_list[i](one_graph_boxes_features)#层归一化
one_graph_boxes_features=F.relu(one_graph_boxes_features)

graph_boxes_features_list.append(one_graph_boxes_features)#将图中每个box的图卷积特征存入一个列表

值得注意的是,为了捕捉不同类型的关系信号,原文中构造的ARG不止一个,所以需要把多个图卷积的结果聚合得到最终的图卷积特征:

graph_boxes_features=torch.sum(torch.stack(graph_boxes_features_list),dim=0) #B, N, NFG

最终返回的图卷积结果graph_boxes_features和构建关系图relation_graph

2.2.3 修改base_model

在图卷积模型构造好后,需要考虑将其加入到base_model从而形成第二阶段的GCNnet;基于上面的叙述,gcn_list是调用图卷积的接口,那么在原始模型中,我们只需要在提取出box特征之后加入图卷积操作就实现了对模型的修改,代码如下所示:

        # GCN       
        graph_boxes_features=boxes_features.reshape(B,T*N,NFG)
  
        for i in range(len(self.gcn_list)):
            graph_boxes_features,relation_graph=self.gcn_list[i](graph_boxes_features,boxes_in_flat)

gcn_list理论上是一系列的图卷积层,但是原文中为了提高效率只使用了一层,因此这里的for循环应该只是一个形式;

接着将提取的原始特征和图卷积特征直接相加作为最终输入分类器的特征:

#         boxes_states= torch.cat( [graph_boxes_features,boxes_features],dim=3)  #B, T, N, NFG+NFB
        boxes_states=graph_boxes_features+boxes_features

接下来将boxes_states输入到分类器,得到分类结果

三、训练和测试

3.1 train_net.py 封装训练程序

构建好训练所需要的模型之后,可以开始着手准备训练部分。首先也需要把训练过程封装成一个方法,关于一些超参数的设置包括变量的位置和学习率调整策略不详细解释了,这里只看最主要的部分:

# Build model and optimizer
basenet_list={'volleyball':Basenet_volleyball, 'collective':Basenet_collective}
gcnnet_list={'volleyball':GCNnet_volleyball, 'collective':GCNnet_collective}

if cfg.training_stage==1:
    Basenet=basenet_list[cfg.dataset_name]
    model=Basenet(cfg)
elif cfg.training_stage==2:
    GCNnet=gcnnet_list[cfg.dataset_name]
    model=GCNnet(cfg)
    # Load backbone
    model.loadmodel(cfg.stage1_model_path)
else:
    assert(False)

这里就是根据训练阶段training_stage的取值来确定采用哪种模型(前面说的那两个Model)。

然后根据具体数据集选用不同的训练网络:

train_list={'volleyball':train_volleyball, 'collective':train_collective}
test_list={'volleyball':test_volleyball, 'collective':test_collective}
train=train_list[cfg.dataset_name]
test=test_list[cfg.dataset_name]

接下来进入训练周期:

# Training iteration
best_resultsolo={'epoch':0,'actions_acc':0}#单人行为
best_result={'epoch':0, 'activities_acc':0}#群体活动
start_epoch=1
for epoch in range(start_epoch, start_epoch+cfg.max_epoch):
    
    if epoch in cfg.lr_plan:
        adjust_lr(optimizer, cfg.lr_plan[epoch])
        
    # One epoch of forward and backward
    train_info=train(training_loader, model, device, optimizer, epoch, cfg)
    show_epoch_info('Train', cfg.log_path, train_info)

    # Test
    if epoch % cfg.test_interval_epoch == 0:#一定间隔周期进行一次测试
        test_info=test(validation_loader, model, device, epoch, cfg)
        show_epoch_info('Test', cfg.log_path, test_info)
        if test_info['actions_acc']>best_resultsolo['actions_acc']:
            best_resultsolo = test_info
        print_log(cfg.log_path,
                  'Best action accuracy: %.2f%% at epoch #%d,group activity accuracy: %.2f%%' % (
                  best_resultsolo['actions_acc'], best_resultsolo['epoch'],best_resultsolo['activities_acc']))
        if test_info['activities_acc']>best_result['activities_acc']:
            best_result=test_info
        print_log(cfg.log_path, 
                  'Best group activity accuracy: %.2f%% at epoch #%d,action accuracy: %.2f%%'%(best_result['activities_acc'], best_result['epoch'],best_result['actions_acc']))

可以看到每个周期都需要调用一次train然后每隔几个周期调用一次test输出准确率,它们是根据具体数据集确定的训练和测试函数,下面仅以volleyball为例说明一下

3.2 train_volleyball.py

简单看一下这个训练函数的前向传播过程:

# forward
actions_scores,activities_scores=model((batch_data[0],batch_data[1]))

# Predict actions
actions_weights=torch.tensor(cfg.actions_weights).to(device=device)
actions_loss=F.cross_entropy(actions_scores,actions_in,weight=actions_weights)  
actions_labels=torch.argmax(actions_scores,dim=1)  
actions_correct=torch.sum(torch.eq(actions_labels.int(),actions_in.int()).float())

# Predict activities
activities_loss=F.cross_entropy(activities_scores,activities_in)
activities_labels=torch.argmax(activities_scores,dim=1)  
activities_correct=torch.sum(torch.eq(activities_labels.int(),activities_in.int()).float())

由于文章中做了两个分类预测,分别是个人动作和群组活动,所以也会得到两个预测损失,这里采用的是交叉熵损失函数,最终在计算总损失的时候还需要将两者加权求和:

total_loss=activities_loss+cfg.actions_loss_weight*actions_loss

接着训练的时候使用这个总的Loss,最终返回的结果如下:

train_info={
    'time':epoch_timer.timeit(),
    'epoch':epoch,
    'loss':loss_meter.avg,
    'activities_acc':activities_meter.avg*100,
    'actions_acc':actions_meter.avg*100
}

3.3 test_volleyball

测试和训练的代码很接近,只是不使用用优化器也不记录张量的梯度,最终返回:

test_info={
    'time':epoch_timer.timeit(),
    'epoch':epoch,
    'loss':loss_meter.avg,
    'activities_acc':activities_meter.avg*100,
    'actions_acc':actions_meter.avg*100
}

3.4 Scripts 训练模型的入口

项目的Scripts文件夹下面保存了,训练的主函数,主要分为stage1和stage2的训练阶段,分别设置了一些超参数再调用训练模型:

3.4.1 train_volleyball_stage1.py
import sys
sys.path.append(".")
from train_net import *

cfg=Config('volleyball')

cfg.device_list="1,2"
cfg.training_stage=1
cfg.stage1_model_path='/media/a5/image3/lj/result/stage1/STAGE1_MODEL.pth'
cfg.train_backbone=True

cfg.batch_size=4
cfg.test_batch_size=4
cfg.num_frames=1
cfg.train_learning_rate=1e-5
cfg.lr_plan={}
cfg.max_epoch=200
cfg.actions_weights=[[1., 1., 2., 3., 1., 2., 2., 0.2, 1.]]  

cfg.exp_note='Volleyball_stage1'
train_net(cfg)
3.4.2 train_volleyball_stage2.py
import sys
sys.path.append(".")
from train_net import *

cfg=Config('volleyball')

cfg.device_list="1,2"
cfg.training_stage=2
# /media/a5/image3/lj/result/stage1/[Volleyball_stage1_stage1]<2020-10-21_09-58-30>
cfg.stage1_model_path='/media/a5/image3/lj/result/stage1/[Volleyball_stage1_stage1]<2020-10-21_09-58-30>/stage1_epoch128_90.50%.pth'  #PATH OF THE BASE MODEL
cfg.train_backbone=False

cfg.batch_size=4 #32
cfg.test_batch_size=4
cfg.num_frames=10
cfg.train_learning_rate=2e-4 
cfg.lr_plan={41:1e-4, 81:5e-5, 121:1e-5}
cfg.max_epoch=150
cfg.actions_weights=[[1., 1., 2., 3., 1., 2., 2., 0.2, 1.]]  

cfg.exp_note='Volleyball_stage2'
train_net(cfg)

#### 3.4.2 train_volleyball_stage2.py

```python
import sys
sys.path.append(".")
from train_net import *

cfg=Config('volleyball')

cfg.device_list="1,2"
cfg.training_stage=2
# /media/a5/image3/lj/result/stage1/[Volleyball_stage1_stage1]<2020-10-21_09-58-30>
cfg.stage1_model_path='/media/a5/image3/lj/result/stage1/[Volleyball_stage1_stage1]<2020-10-21_09-58-30>/stage1_epoch128_90.50%.pth'  #PATH OF THE BASE MODEL
cfg.train_backbone=False

cfg.batch_size=4 #32
cfg.test_batch_size=4
cfg.num_frames=10
cfg.train_learning_rate=2e-4 
cfg.lr_plan={41:1e-4, 81:5e-5, 121:1e-5}
cfg.max_epoch=150
cfg.actions_weights=[[1., 1., 2., 3., 1., 2., 2., 0.2, 1.]]  

cfg.exp_note='Volleyball_stage2'
train_net(cfg)

以上就是整个代码的流程。

  • 10
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Amos是一种常用的结构方程模型分析软件,多群组分析常用于比较不同群体之间的差异。通过Amos进行群组分析可以帮助我们了解不同群体在某一变量或多个变量上的差异程度。 在Amos中进行群组分析时,首先需要对不同群组进行分组,如男性和女性、不同年龄段等。然后,我们需要构建一个结构方程模型,模型中包括我们想要比较的变量以及它们之间的关系。接下来,我们需要设置多个群组,并对不同群组进行对比。 Amos会自动计算不同群组之间的不同参数估计值,例如路径系数、拟合优度指标等。通过分析这些估计值,我们可以得出以下结论: 1. 不同群组之间的路径系数差异:路径系数代表了变量之间的关系强度,它们的大小可以反映群组之间的差异。通过比较路径系数的估计值,我们可以判断不同群组之间是否存在显著差异。 2. 拟合优度差异:拟合优度指标反映了模型与实际数据的拟合程度,其数值越接近1越好。通过比较不同群组的拟合优度指标,我们可以判断不同群组之间在模型拟合上是否存在显著差异。 3. 其他参数差异:除了路径系数和拟合优度,Amos还可以提供其他参数的估计值,如标准误、置信区间等。我们可以通过比较这些参数的估计值,进一步了解不同群组之间的差异特点。 通过以上解读,我们可以得出结论,即不同群组之间在某一变量或多个变量上存在显著差异,并且可以具体了解到差异的程度和特点。这对于我们理解和比较不同群体之间的行为、动态和特征具有重要意义。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值