ST-GCN模型详解(+openpose)

ST-GCN模型详解(+openpose)

一、什么是ST-GCN呢

基于骨架的动作识别(Skeleton-Based Action Recognition)主要任务是从一系列时间连续的骨骼关键点(2D/3D)中识别出正在执行的动作。因为牵涉到骨骼框架这种图结构的输入,采用GCN的方法逐渐成为了主流,并取得了不错的效果。ST-GCN是基于动态骨骼的动作识别方法ST-GCN(时空图卷积网络模型)

论文链接:https://arxiv.org/abs/1801.07455

Github 代码:https://github.com/yysijie/st-gcn?

建议先看完GCN的相关知识图卷积神经网络-GCN_图卷积网络-CSDN博客

对GCN的总结就是GCN算法的步骤简单可总结为:

1、初始化:为每个节点分配初始特征表示。

2、邻居聚合:对于每个节点,将其自身特征与邻居节点的特征进行加权平均或拼接,得到聚合后的特征。

3、特征转换:对聚合后的特征进行线性变换,以充分利用特征之间的关系。

4、非线性激活:应用非线性激活函数,如ReLU,将线性变换后的特征映射到非线性空间。

5、循环迭代:重复进行邻居聚合、特征转换和非线性激活的步骤,直到达到所需的网络层数或收敛条件。

对于每个节点,我们在聚类时从它的所有邻居节点处获取其特征信息,当然也包括它自身的特征。

香港中大-商汤科技联合实验室的最新 AAAI 会议论文「Spatial Temporal Graph Convolution Networks for Skeleton Based Action Recognition」提出了一种新的 ST-GCN,即时空图卷积网络模型,用于解决基于人体骨架关键点的人类动作识别问题。该方法除了思路新颖之外,在标准的动作识别数据集上也取得了较大的性能提升。
img

二、ST-GCN的各部分结构

1、数据输入结构

我们都知道一个动作都是连续的,例如我们进行喝水这个动作,那么从拿起水杯到喝完放下水杯着整个动作才能被称作为喝水;这时候就需要对视频进行每一帧的骨骼点输出,组装在一起形成一个完整的动作,因此基于骨架的动作识别方法的一般输入为时间连续的人体骨架关键点。

在这里插入图片描述
这些关键点可以通过openpose进行姿态估计获取,也可以手动标注。其数据维度一般为(N, C, T, V, M ),那么什么是openpose呢?

2、openpose预处理

OpenPose 是一个标注人体的关节(颈部,肩膀,肘部等),连接成骨骼,进而估计人体姿态的算法。作为视频的预处理工具,我们只需要关注 OpenPose 的输出就可以了。

img

总的来说,视频的骨骼标注结果维数比较高。在一个视频中,可能有很多帧(Frame)。每个帧中,可能存在很多人(Man)。每个人又有很多关节(Joint)。每一个关节又有不同特征(位置、置信度)。其数据维度一般为**(N, C, T, V, M )**img

  • N代表视频的数量,通常一个 batch 有 256 个视频(其实随便设置,最好是 2 的指数);
  • C代表关节的特征,通常一个关节包含x,y,acc 等 3 个特征(如果是三维骨骼就是 4 个),x,y为节点关节的位置坐标,acc为置信度。
  • T 代表关键帧的数量,一般一个视频有 150 帧。
  • V 代表关节的数量,通常一个人标注 18 个关节。
  • M代表一帧中的人数,一般选择平均置信度最高的 2 个人。

3、图划分策略

考虑到动作识别的特点,作者并未使用单一的卷积核,而是使用『图划分』,将 A ^ \hat{A} A^ 分解成了 A 1 ^ , A 2 ^ , A 3 ^ \hat{A_{1}}, \hat{A_{2}}, \hat{A_{3}} A1^,A2^,A3^

A ^ \hat{A} A^表示的所有边如上图右侧所示:

  • 两个节点之间有一条双向边
  • 节点自身有一个自环

img

在ST-GCN这篇文章中,作者的另一大创新点是通过对运动的分析引入了图划分策略,即建立多个反应不同运动状态(如静止,离心运动和向心运动)的邻接矩阵。作者在原文中提到其采用了三种不同的策略,分别为:

  • Uni-labeling,即与跟根节点相邻的所有结点具有相同的label,如下图b所示。
  • Distance partitioning,即根节点本身的label设为0,其邻接点设置为1,如下图c所示。
  • Spatial configuration partitioning,是本文提出的图划分策略。也就是以根节点与重心的距离为基准(label=0),在所有邻接节点到重心距离中,小于基准值的视为向节心点(label=1),大于基准值的视为离心节点(label=2)。什么意思呢?对于一个根节点,与它相连的边可以分为 3 部分(图d)。
    • 第 1 部分连接了空间位置上比本节点更远离整个骨架重心的邻居节点(黄色节点),包含了离心运动的特征。
    • 第 2 部分连接了更为靠近重心的邻居节点(蓝色节点),包含了向心运动的特征。
    • 第 3 部分连接了根节点本身(绿色节点),包含了静止的特征。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传)

使用这样的分解方法,1 个图分解成了 3 个子图。卷积核也从 1 个变为了 3 个,即 (1,18,18)变为 (3,18,18)。3 个卷积核的卷积结果分别表达了不同尺度的动作特征。要得到卷积的结果,只需要使用每个卷积核分别进行卷积,在进行加权平均(和图像卷积相同)。

代码如下:

A = []
for hop in valid_hop:
    a_root = np.zeros((self.num_node, self.num_node))
    a_close = np.zeros((self.num_node, self.num_node))
    a_further = np.zeros((self.num_node, self.num_node))
    for i in range(self.num_node):
        for j in range(self.num_node):
            if self.hop_dis[j, i] == hop:
                if self.hop_dis[j, self.center] == self.hop_dis[
                        i, self.center]:
                    a_root[j, i] = normalize_adjacency[j, i]
                elif self.hop_dis[j, self.
                                  center] > self.hop_dis[i, self.
                                                         center]:
                    a_close[j, i] = normalize_adjacency[j, i]
                else:
                    a_further[j, i] = normalize_adjacency[j, i]
    if hop == 0:
        A.append(a_root)
    else:
        A.append(a_root + a_close)
        A.append(a_further)
A = np.stack(A)
self.A = A

'''这段代码的主要目的是根据给定的邻接矩阵 `normalize_adjacency` 和节点间的跳跃距离 `self.hop_dis` 创建多个权重矩阵。这些权重矩阵根据距离的不同对图进行划分,最后将结果组合成一个三维张量 `A`。

下面是这段代码的详细解释:

1. **初始化空列表 `A`**:首先创建一个空列表 `A` 来存储权重矩阵。
   
2. **循环遍历 `valid_hop`**:对于给定的每个跳数(`valid_hop` 列表中的每个跳数),分别对图进行划分。

3. **初始化权重矩阵**:在每次循环中,初始化三个权重矩阵 `a_root`、`a_close` 和 `a_further`,它们都是与邻接矩阵尺寸相同的零矩阵。

4. **遍历所有节点对**:通过双层循环遍历所有节点对 `(i, j)`。

    - **根据跳数判断**:对于给定的跳数 `hop`,如果 `self.hop_dis[j, i]` 等于 `hop`,则进行进一步分类:
    
        - **`a_root`**:如果 `self.hop_dis[j, i]` 等于 `hop`,并且 `j` 和 `i` 到中心节点(`self.center`)的距离相同,则将 `normalize_adjacency[j, i]` 赋值给 `a_root[j, i]`。
        
        - **`a_close`**:如果 `j` 到中心节点的距离大于 `i` 到中心节点的距离,则将 `normalize_adjacency[j, i]` 赋值给 `a_close[j, i]`。
        
        - **`a_further`**:如果 `j` 到中心节点的距离小于 `i` 到中心节点的距离,则将 `normalize_adjacency[j, i]` 赋值给 `a_further[j, i]`。

5. **添加权重矩阵到列表**:在每次循环中,根据 `hop` 是否为 `0`,将权重矩阵 `a_root` 添加到列表 `A` 中。如果 `hop` 不是 `0`,则将 `a_root + a_close` 和 `a_further` 分别添加到列表 `A` 中。

6. **将列表转化为张量**:使用 `np.stack` 将列表 `A` 转化为一个三维张量,并将其赋值给 `self.A`。

该代码主要通过对图的划分,对节点对进行分类并创建多个权重矩阵。这些权重矩阵根据节点之间的距离和它们到中心节点的距离进行分类,从而在神经网络中对不同跳数的节点进行不同权重的处理。'''

4、GCN

从结果上看,最简单的图卷积似乎已经能取得很好的效果了,具体实现如下:

def normalize_digraph(A):
    Dl = np.sum(A, 0)
    num_node = A.shape[0]
    Dn = np.zeros((num_node, num_node))
    for i in range(num_node):
        if Dl[i] > 0:
            Dn[i, i] = Dl[i]**(-1)
    AD = np.dot(A, Dn)
    return AD


'''这个函数 `normalize_digraph` 用于对给定的有向图邻接矩阵 `A` 进行归一化处理。它使用一种基于节点的度的归一化方法,具体过程如下:

1. **计算节点的出度**:首先计算每个节点的出度(即每个节点有多少条边从它出发)。这通过计算矩阵 `A` 中每列的和(即 `np.sum(A, 0)`)得到,并将结果存储在 `Dl` 中。

2. **初始化度矩阵 `Dn`**:创建一个零矩阵 `Dn`,尺寸与邻接矩阵 `A` 相同。

3. **填充度矩阵**:对于每个节点 `i`,如果节点 `i` 的出度大于 `0`,则将 `Dn[i, i]` 设置为 `Dl[i]` 的倒数(`Dl[i]**(-1)`)。这意味着在度矩阵 `Dn` 中,只对对角线元素(节点的出度)进行非零赋值。

4. **对邻接矩阵进行归一化**:计算邻接矩阵 `A` 与度矩阵 `Dn` 的点积,得到归一化后的邻接矩阵 `AD`。这一步是关键的归一化操作。通过矩阵 `A` 与度矩阵 `Dn` 的点积操作,邻接矩阵中的每个元素被它的源节点的出度进行归一化。

5. **返回归一化后的邻接矩阵**:最终返回归一化后的邻接矩阵 `AD`。

通过这个归一化过程,每个节点的出度被归一化到总和为 `1`,这对于在神经网络中处理图数据时是一个重要的前处理步骤,因为它有助于确保特征值和特征向量的稳定性。'''

但是作者在实际项目中使用的图卷积公式是:

aggre ⁡ ( x ) = D − 1 A X \operatorname{aggre}(x)=D^{-1} A X aggre(x)=D1AX

化简如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

其实就是以边为权值对节点特征求加权平均。其中, A ^ = D − 1 A \hat{A}=D^{-1} A A^=D1A可以理解为卷积核。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

加上上面的图划分策略,我们可以可以写出带有k 个卷积核的图卷积表达式了外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

对 v求和代表了节点的加权平均,对 k 求和代表了不同卷积核 feature map 的加权平均,具体实现如下:

代码如下:

self.conv = nn.Conv2d(
        in_channels,
        out_channels * kernel_size,
        kernel_size=(t_kernel_size, 1),
        padding=(t_padding, 0),
        stride=(t_stride, 1),
        dilation=(t_dilation, 1),
        bias=bias)
 
def forward(self, x, A):
    assert A.size(0) == self.kernel_size
 
    x = self.conv(x)
 
    n, kc, t, v = x.size()
    x = x.view(n, self.kernel_size, kc//self.kernel_size, t, v)
    x = torch.einsum('nkctv,kvw->nctw', (x, A))
 
    return x.contiguous(), A


'''这段代码展示了一个卷积操作(通过 `nn.Conv2d` 类)和其前向传播函数 `forward` 方法的实现。在 `forward` 方法中,输入是一个四维张量 `x` 和一个邻接矩阵 `A`。在代码中,这个前向传播函数主要包括以下步骤:

1. **断言**:使用 `assert` 语句检查邻接矩阵 `A` 的尺寸。确保 `A` 的第一个维度等于卷积核大小(即 `self.kernel_size`)。

2. **卷积操作**:通过调用 `self.conv(x)` 对输入张量 `x` 进行卷积操作,得到卷积后的输出。

3. **调整维度**:卷积操作得到的输出 `x` 是一个四维张量 `(n, kc, t, v)`,这里 `n` 是批次大小,`kc` 是通道数乘以卷积核大小(`out_channels * kernel_size`),`t` 是时间步数,`v` 是节点数量。为了接下来的操作,将 `x` 重整为一个五维张量 `(n, self.kernel_size, kc // self.kernel_size, t, v)`,在其中 `kc // self.kernel_size` 代表的是原通道数。

4. **爱因斯坦求和约定**:使用 `torch.einsum` 函数根据爱因斯坦求和约定(`einsum`)对调整后的 `x` 和邻接矩阵 `A` 进行运算。运算的形式为 `'nkctv,kvw->nctw'`,意思是将 `x` 的前两个维度和 `A` 的后两个维度进行矩阵乘法。得到的结果是一个四维张量。

5. **返回结果**:`forward` 方法返回连续化后的 `x`(使用 `contiguous()` 方法确保 `x` 的连续性)和邻接矩阵 `A`。

这些步骤组合在一起,展示了通过卷积操作与邻接矩阵来对输入张量进行处理,并最终返回处理后的张量和邻接矩阵。'''

5、TCN

GCN 帮助我们学习了到空间中相邻关节的局部特征。在此基础上,我们需要学习时间中关节变化的局部特征。**如何为 Graph 叠加时序特征,是图网络面临的问题之一。**这方面的研究主要有两个思路:时间卷积(TCN)和序列模型(LSTM)。

ST-GCN 使用的是 TCN,由于形状固定,我们可以使用传统的卷积层完成时间卷积操作。为了便于理解,可以类比图像的卷积操作。st-gcn 的 feature map 最后三个维度的形状为 (C,V,T) ,与图像 feature map 的形状 (C,W,H) 相对应。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在图像卷积中,卷积核的大小为『w』× 『1』,则每次完成 w 行像素,1 列像素的卷积。『stride』为 s,则每次移动 s 像素,完成 1 行后进行下 1 行像素的卷积

img

在时间卷积中,卷积核的大小为『temporal_kernel_size』 ×『1』,则每次完成 1 个节点,temporal_kernel_size 个关键帧的卷积。『stride』为 1,则每次移动 1 帧,完成 1 个节点后进行下 1 个节点的卷积。

代码如下:

padding = ((kernel_size[0] - 1) // 2, 0)

self.tcn = nn.Sequential(
    nn.BatchNorm2d(out_channels),
    nn.ReLU(inplace=True),
    nn.Conv2d(
        out_channels,
        out_channels,
        (temporal_kernel_size, 1),
        (1, 1),
        padding,
    ),
    nn.BatchNorm2d(out_channels),
    nn.Dropout(dropout, inplace=True),
)


'''这段代码定义了一个名为 `self.tcn` 的 `nn.Sequential` 对象,用于构建一个时序卷积神经网络(TCN)的一个层。这层由几个 PyTorch 模块组成,包括批归一化、激活函数、卷积层、再批归一化和丢弃层。下面是对每个组件的解释:

1. **nn.BatchNorm2d**:这是一个二维批归一化层。它会对输入张量在特征维度上进行批量归一化(即在维度 1,即通道数的维度上进行归一化)。归一化有助于稳定训练过程。

2. **nn.ReLU**:这是一个激活函数层,使用的是整形版的 ReLU 函数。ReLU 是一种常用的非线性激活函数。`inplace=True` 参数表示激活操作将直接对输入张量进行修改,而不是创建一个新的张量。

3. **nn.Conv2d**:这是一个二维卷积层,卷积核尺寸为 `(temporal_kernel_size, 1)`,卷积步长为 `(1, 1)`,填充为 `padding`。`out_channels` 是输出通道数,与输入通道数一致,因此该层的输入输出维度相同。卷积操作有助于捕获输入张量中的局部时空特征。

4. **第二个 nn.BatchNorm2d**:再次使用批归一化层,以进一步稳定训练过程和提高模型性能。

5. **nn.Dropout**:这是一个丢弃层,目的是在训练过程中随机地将一部分神经元的输出设置为 0,以减少过拟合。`dropout` 参数控制丢弃的概率,`inplace=True` 表示丢弃操作直接在输入张量上进行。

总的来说,这段代码定义了一个用于时序卷积神经网络的模块,结合了批归一化、激活函数、卷积操作和丢弃以提高模型的稳定性和性能。通过 `nn.Sequential` 对象将这些层结合在一起,形成一个顺序执行的结构。'''

6、Attention

作者在进行图卷积之前,还设计了一个简易的注意力模型(ATT)

代码如下:

# 注意力参数
# 每个 st-gcn 单元都有自己的权重参数用于训练
self.edge_importance = nn.ParameterList([
    nn.Parameter(torch.ones(self.A.size()))
    for i in self.st_gcn_networks
])
# st-gcn 卷积
for gcn, importance in zip(self.st_gcn_networks, self.edge_importance):
    print(x.shape)
    # 关注重要的边信息
    x, _ = gcn(x, self.A * importance)

'''这段代码的主要目的是使用一系列堆叠的 ST-GCN(空间时间图卷积网络)层来处理输入数据 `x`,同时考虑每个 ST-GCN 层的边权重 `importance`。以下是对代码的解释:

1. **`self.edge_importance`**:这是一个 `nn.ParameterList` 对象,包含多个可学习的参数。每个参数对应一个 ST-GCN 层,并用于调整图的边权重。这些参数初始值为全 1 的张量,大小与邻接矩阵 `self.A` 相同。这些权重参数用于在训练过程中学习边的重要性。

2. **迭代堆叠的 ST-GCN 层**:代码使用 `zip` 函数迭代遍历 `self.st_gcn_networks` 中的 ST-GCN 层和 `self.edge_importance` 中的权重参数。对于每一层,代码首先打印当前输入数据 `x` 的形状。

3. **关注重要的边信息**:对于每个 `st_gcn` 层,代码将当前的输入数据 `x` 与对应的权重参数 `importance` 相乘(通过 `self.A * importance` 得到),然后将乘积结果与 `gcn` 层进行操作。`gcn` 层是一个 ST-GCN 层,它接受经过加权的邻接矩阵 `self.A * importance` 和输入数据 `x`,然后对 `x` 进行卷积操作。卷积操作的结果返回并更新 `x`。

4. **操作过程**:在每次迭代中,代码输出当前输入数据 `x` 的形状,这有助于跟踪数据在网络中的流动和变化。在 `st_gcn` 层中,`x` 会被调整为图结构的边重要性,并通过卷积处理。每一层的输出结果作为下一层的输入数据。

这段代码展示了如何在一个网络中使用多个 ST-GCN 层来处理输入数据,并通过权重参数调整边的重要性。这种关注边权重的方式可以让模型更好地学习空间时间图结构的特征,提高模型的预测性能。'''

7、整体结构

时空图卷积分为空间图卷积时间图卷积,其中空间图卷积是核心部分。一个空间图卷积加上一个时间卷积就是一层,一共10层,但是第一层没有残差结构,所以大部分文献都称它9层。原代码里的空间图卷积对应名称gcn,时间对应tcn。每一层的结构如下。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
论文中的网络结构如下:

img

首先我们先来看第一部分:对输入矩阵进行归一化

img

self.data_bn = nn.BatchNorm1d(in_channels * A.size(1))#函数data_bn的定义
N, C, T, V, M = x.size()
# 进行维度交换后记得调用 contiguous 再调用 view 保持显存连续
x = x.permute(0, 4, 3, 1, 2).contiguous()
x = x.view(N * M, V * C, T)
x = self.data_bn(x)
x = x.view(N, M, V, C, T)
x = x.permute(0, 1, 3, 4, 2).contiguous()
x = x.view(N * M, C, T, V)

'''
这段代码是一种数据预处理步骤,用于对输入数据 `x` 进行规范化和重排。数据的形状在不同的步骤中发生了变化。让我们分解这段代码来理解其功能和目的:

1. **输入数据**:代码开始时,输入数据 `x` 的形状为 `(N, C, T, V, M)`。这代表了一个五维张量,其中:
    - `N`:样本数量(batch size)。
    - `C`:通道数(例如 RGB 图像中的红、绿、蓝三通道)。
    - `T`:时间步数(时间维度,表示时序数据)。
    - `V`:节点数(例如,表示人体姿态中的关节数)。
    - `M`:图形实例数量(例如,在多个人体实例的场景下,M 表示实例的数量)。

2. **调整维度顺序**:通过 `x.permute(0, 4, 3, 1, 2)`,数据的维度顺序被调整为 `(N, M, V, C, T)`。这样做的原因是为了方便后续的重整和操作。

3. **重整形状**:通过 `x.view(N * M, V * C, T)`,将数据重整为 `(N * M, V * C, T)` 形状。这种重整方式将批量和实例维度合并在一起,方便后续的批量归一化(batch normalization)。

4. **批量归一化**:`self.data_bn(x)` 对数据应用了批量归一化。这是对数据进行归一化的一种常见方法,有助于稳定和加速训练过程。

5. **恢复形状**:通过 `x.view(N, M, V, C, T)` 将数据恢复为原始的 `(N, M, V, C, T)` 形状。

6. **调整维度顺序**:再次通过 `x.permute(0, 1, 3, 4, 2)` 调整维度顺序为 `(N, M, C, T, V)`,这样做可能是为了适应后续的网络层或操作。

7. **重整形状**:最后,通过 `x.view(N * M, C, T, V)`,将数据重新整形为 `(N * M, C, T, V)` 形状。
'''

归一化是在时间和空间维度下进行的( V×C )。也就是将一个关节在不同帧下的位置特征(x 和 y 和 acc)进行归一化。

这个操作的作用:

  • 关节在不同帧下的关节位置变化很大,如果不进行归一化不利于算法收敛
  • 在不同 batch 不同帧下的关节位置基本上服从随机分布,不会造成不同 batch 归一化结果相差太大,而导致准确率波动。

然后,通过 ST-GCN 单元,交替的使用 GCN 和 TCN,对时间和空间维度进行变换:

img

# N*M(256*2)/C(3)/T(150)/V(18)
Input:[512, 3, 150, 18]
ST-GCN-1[512, 64, 150, 18]
ST-GCN-2[512, 64, 150, 18]
ST-GCN-3[512, 64, 150, 18]
ST-GCN-4[512, 64, 150, 18]
ST-GCN-5[512, 128, 75, 18]
ST-GCN-6[512, 128, 75, 18]
ST-GCN-7[512, 128, 75, 18]
ST-GCN-8[512, 256, 38, 18]
ST-GCN-9[512, 256, 38, 18]

代码如下:

self.st_gcn_networks = nn.ModuleList((
    st_gcn(in_channels, 64, kernel_size, 1, residual=False, **kwargs0),
    st_gcn(64, 64, kernel_size, 1, **kwargs),
    st_gcn(64, 64, kernel_size, 1, **kwargs),
    st_gcn(64, 64, kernel_size, 1, **kwargs),
    st_gcn(64, 128, kernel_size, 2, **kwargs),
    st_gcn(128, 128, kernel_size, 1, **kwargs),
    st_gcn(128, 128, kernel_size, 1, **kwargs),
    st_gcn(128, 256, kernel_size, 2, **kwargs),
    st_gcn(256, 256, kernel_size, 1, **kwargs),
    st_gcn(256, 256, kernel_size, 1, **kwargs),
))
 
# initialize parameters for edge importance weighting
if edge_importance_weighting:
    self.edge_importance = nn.ParameterList([
        nn.Parameter(torch.ones(self.A.size()))
        for i in self.st_gcn_networks
    ])
else:
    self.edge_importance = [1] * len(self.st_gcn_networks)
 
# ST-GCN与可学习的权重矩阵不断重复与堆叠
for gcn, importance in zip(self.st_gcn_networks, self.edge_importance):
 x, _ = gcn(x, self.A * importance)



'''这段代码定义了一个基于空间时间图卷积网络(Spatial-Temporal Graph Convolutional Network, ST-GCN)的模型,并对输入数据 `x` 进行多层的 ST-GCN 操作。模型中使用了多个堆叠的 ST-GCN 层。以下是对代码的解释:

1. **`self.st_gcn_networks`**:这是一个 `nn.ModuleList` 对象,包含多个 `st_gcn` 模块。`st_gcn` 是空间时间图卷积网络(ST-GCN)的一个层,每一层接受不同的输入和输出通道数,以及不同的卷积内核和步长。模型通过将多个 `st_gcn` 层堆叠在一起形成完整的网络。

2. **`edge_importance_weighting`**:该变量控制是否对边缘进行权重调整。根据其值,代码初始化了 `self.edge_importance`。如果 `edge_importance_weighting` 为 `True`,则初始化一个可学习的权重参数列表 `nn.ParameterList`,每个 `st_gcn` 层对应一个权重参数;否则,将 `self.edge_importance` 设置为包含一个权重为 1 的列表。

3. **迭代堆叠的 ST-GCN 层**:代码使用 `zip` 函数迭代遍历 `self.st_gcn_networks` 中的 ST-GCN 层以及 `self.edge_importance` 中的权重参数。对于每一层,代码将当前的输入数据 `x` 与边权重 `importance` 相乘后作为输入,并通过当前的 `st_gcn` 层进行操作。每一层的输出结果作为下一层的输入。

4. **操作 `gcn` 层**:在每次迭代中,`x` 被传递到 `gcn` 层与边权重 `importance` 的乘积中,计算结果被存储在 `x` 中作为下一层的输入。这种连续的操作使得输入数据 `x` 在多个 ST-GCN 层之间传递和处理。

总体而言,这段代码实现了一个多层 ST-GCN 网络模型,使用可学习的权重矩阵对输入数据进行处理。这样能够在模型中自动学习边缘的重要性,从而提高模型的性能。'''

空间维度是关节的特征(开始为 3),时间的维度是关键帧数(开始为 150)。在经过所有 ST-GCN 单元的时空卷积后,关节的特征维度增加到 256,关键帧维度降低到 38。

个人感觉这样设计是因为,人的动作阶段并不多,但是每个阶段内的动作比较复杂。比如,一个挥高尔夫球杆的动作可能只需要分解为 5 步,但是每一步的手部、腰部和脚部动作要求却比较多。

最后,使用平均池化、全连接层(或者叫 FCN)对特征进行分类,具体实现如下:

img

# self.fcn = nn.Conv2d(256, num_class, kernel_size=1)

# global pooling
x = F.avg_pool2d(x, x.size()[2:])
x = x.view(N, M, -1, 1, 1).mean(dim=1)
# prediction
x = self.fcn(x)
x = x.view(x.size(0), -1)

'''这段代码展示了一个典型的卷积神经网络(CNN)中的最后部分,用于对特征进行全局池化(global pooling)、预测分类,以及将输出形状调整为合适的形式。让我们分解这段代码来理解其功能:

1. **定义卷积层**:
    - `self.fcn = nn.Conv2d(256, num_class, kernel_size=1)`: 定义了一个卷积层 `self.fcn`,输入通道数为 256,输出通道数为 `num_class`(类别数量),卷积核尺寸为 1x1。这是一种用于特征图变换的卷积层,将特征映射到分类的类别空间。

2. **全局池化**:
    - `x = F.avg_pool2d(x, x.size()[2:])`: 使用 PyTorch 的 `F.avg_pool2d` 函数对 `x` 进行全局平均池化。池化的核大小和步长等于特征图的空间维度,因此整个特征图将被池化成一个单一的值。
    
3. **调整形状**:
    - `x = x.view(N, M, -1, 1, 1).mean(dim=1)`: 这一步首先通过 `x.view()` 将张量 `x` 形状调整为 `(N, M, -1, 1, 1)`。这里的 `-1` 表示剩余的通道和特征图维度被折叠成一个通道。然后通过 `mean(dim=1)` 对 `M` 维度求均值,即将实例数目维度合并,这种操作常用于多个实例的特征融合。
    
4. **预测分类**:
    - `x = self.fcn(x)`: 将 `x` 输入到 `self.fcn` 卷积层中,执行分类操作。这一步的输出是 `(N, num_class, 1, 1)` 形状的张量。
    
5. **调整形状**:
    - `x = x.view(x.size(0), -1)`: 通过 `view` 操作将输出调整为 `(N, num_class)` 形状,这是为了适应后续的损失函数或预测任务。
'''

Graph 上的平均池化可以理解为对 Graph 进行 read out,即汇总节点特征表示整个 graph 特征的过程。这里的 read out 就是汇总关节特征表示动作特征的过程了。通常我们会使用基于统计的方法,例如对节点求 max,sum,mean 等等。mean 鲁棒性比较好,所以这里使用了 mean。

这些就是ST-GCN的整体的网络架构了

三、总结

ST-GCN应当具备能够从时空维度提取特征的能力,其在GCN中的表现就是能够同时聚合时空维度的信息,如下图所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

其具体网络层如图所示:

7b2e57439e2e663dcf898a87e708e333.png

其具体可以分为以下步骤:

  • 步骤1:引入一个可学习的权重矩阵(与邻接矩阵等大小)与邻接矩阵按位相乘。该权重矩阵叫做“Learnable edge importance weight”,用来赋予邻接矩阵中重要边(节点)较大的权重抑制非重要边(节点)的权重
  • 步骤2:将加权后的邻接矩阵与输入送至GCN中进行运算。同时,作者还引入了残差结构(一个CNN+BN)计算获得Res,与GCN的输出按位相加,实现空间维度信息的聚合。
  • 步骤3:利用TCN网络(实际上是一种普通的CNN,在时间维度的kernel size>1)实现时间维度信息的聚合。

其具体结合openpose实现可参考用自建kinetics-skeleton行为识别数据集训练st-gcn网络流程记录_kinetics_skeleton数据集制作-CSDN博客

参考:时空图卷积ST-GCN理论和代码详解-CSDN博客

深入理解时空图卷积(ST-GCN)-CSDN博客

(8 条消息) 如何评价ST-GCN动作识别算法? - 知乎 (zhihu.com)

本文章仅当做记录学习使用,以便以后回顾

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/557864.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

CentOS7升级openssl

文章目录 一 系统环境二 操作步骤三 版本检查 一 系统环境 公司服务器等保要求,修复openssl的高危漏洞。 本机使用centos7.9系统,openssl版本是1.0.2k,计划升级到1.1.1q 在执行下列操作前,务必要打快照做好备份,以防升…

030——从GUI->Client->Server->driver实现对红外遥控的控制

目录 1、 解决韦东山老师irda模块中断申请失败的bug 2、 client添加处理程序 3、 添加服务器处理程序和驱动处理句柄 4、 处理数据读出不准确问题 5、 修改后的展示 1、 解决韦东山老师irda模块中断申请失败的bug irda需要通过中断来触发读操作,申请中断需要引…

Octopus v2:斯坦福的嵌入设备专用大模型

斯坦福大学推出了 Octopus v2,这是一种突破性的设备上语言模型,旨在解决与现有模型相关的延迟、准确性和隐私问题。 NSDT工具推荐: Three.js AI纹理开发包 - YOLO合成数据生成器 - GLTF/GLB在线编辑 - 3D模型格式在线转换 - 可编程3D场景编辑…

源码篇--Nacos服务--中章(1):Nacos服务端的启动

文章目录 前言一、Nacos Console 启动入口:二、启动过程:2.1 容器启动监听器:2.1.1 调整启动标识为正在启动状态:2.1.2 环境准备阶段:2.1.3 容器环境准备:2.1.4 自定义的环境变量 设置:2.1.5 服…

Spectre-v2 以及 Linux Retpoline技术简介

文章目录 前言一、Executive Summary1.1 Spectre-v2: Branch Predictor Poisoning1.2 Mitigating Spectre-v2 with Retpolines1.3 Retpoline Concept 二、BackgroundExploit Composition 三、(Un-)Directing Speculative Execution四、Construction (x86)4.1 Speculation Barri…

测试人员通常遇到的“坑”

网上看到一个帖子,从事多年的测试从业者,吐槽测试过程中遇到的“坑”,感觉比较有意思,我在工作当中也遇到通常的问题,看得出这位网友比较喜欢总结,帖子地址奉上,有兴趣的可以浏览一下&#xff1…

bug(警告):[vue-router] Duplicate named routes definition: …

查看警告:[vue-router] Duplicate named routes definition——翻译[vue-router]重复命名路由定义 小编劝诫:当我们在开发过程中警告也一定不要忽略,虽然你在本地跑代码时这些警告影响项目的正常运行,但是会让你产生误区&#xff…

大模型日报|今日必读的8篇大模型论文

大家好,今日必读的大模型论文来啦! 1.EdgeFusion:端侧文本到图像生成,只需不到一秒 用于文本到图像生成的稳定扩散(SD)技术需要大量计算,这对其实际应用构成了重大障碍。为此,最近…

Oracle進階SQLDay03

一、函數進階復習 1、行轉列 select 用水儿量(噸) 统计项, sum(case when t_account.month01 then USENUM end) 一月, sum(case when t_account.month02 then USENUM end) 二月, sum(case when t_account.month03 then USENUM end) 三月, sum(case when …

STM32学习和实践笔记(15):STM32中断系统

中断概念 CPU执行程序时,由于发生了某种随机的事件(外部或内部),引起CPU暂 时中断正在运行的程序,转去执行一段特殊的服务程序(中断服务子程序 或中断处理程序),以处理该事件,该事件处理完后又返回被中断的程序 继…

飞桨Ai(二)paddle使用CPU版本可以正常识别,切换为GPU版本时无法识别结果

一、问题描述: 刚开始用paddle的CPU版本,对训练好的模型进行推理,正常识别出想要的结果后来尝试使用paddle的GPU版本,然后发现识别出来是空的 二、系统思路: 最终系统环境如下: 系统:win10 …

有哪些公认好用且免费的云渲染网渲平台?渲染100邀请码1a12

现在云渲染是越来越火了,无论是在建筑设计、影视动画还是效果图行业都有它的身影,云渲染能缩短制作周期,提高工作效率,那么市面上有哪些公认好用且免费的云渲染平台呢?这次我们来了解下。 首先,我们来看看有…

vulfocus靶场tomcat-cve_2017_12615 文件上传

7.0.0-7.0.81 影响版本 Windows上的Apache Tomcat如果开启PUT方法(默认关闭),则存在此漏洞,攻击者可以利用该漏洞上传JSP文件,从而导致远程代码执行。 Tomcat 是一个小型的轻量级应用服务器,在中小型系统和并发访问用户不是很多…

「GO基础」在Windows上配置VS Code GO语言开发环境

💝💝💝欢迎莅临我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」…

查看linux的主机配置脚本

废话不说 直接上指令 curl -Lso- bench.sh | bash 等待后,结果如图: 使用后没有问题,看情况使用 出事概不负责 介意勿用!!!

LD-Pruner、EdgeFusion(On-Device T2I)、FreeDiff、TextCenGen、MemLLM

本文首发于公众号:机器感知 https://mp.weixin.qq.com/s/KiyNfwYWU-wBiCO-hE9qkA 苏 The devil is in the object boundary: towards annotation-free instance segmentation using Foundation Models Foundation models, pre-trained on a large amount of data…

Windows系统下安装paddle

开始使用_飞桨-源于产业实践的开源深度学习平台 (paddlepaddle.org.cn) 命令行下: python -m pip install --upgrade pip --user python -m pip install paddlepaddle2.6.1 -i https://pypi.tuna.tsinghua.edu.cn/simple 报异常 ERROR: Could not install packa…

Jmeter 测试Dubbo接口-实例

1、Dubbo插件准备 ①把jmeter-plugins-dubbo-2.7.4.1-jar-with-dependencies.jar包放在D:\apache-jmeter-5.5\lib\ext目录 ②重新打开Jmeter客户端 在线程组-添加-取样器-dubbo simple,添加dubbo接口请求 2、Jmeter测试lottery接口 ①配置zookeeper参数 由于dub…

windows和虚拟机互传文件

在虚拟机中设置共享文件夹 操作方法:打开VMware–>虚拟机–>设置–>选项–>共享文件夹(见下图),大家在共享文件夹当中就可以把Windows当中的D盘或者其它盘共享到虚拟机中。比如我就是将D盘和E盘共享到了虚拟机中。 共…

【Vue】实现显示输入框字符长度

<div style"float: right; margin-right: 10px"><el-popover placement"top-start" width"200" trigger"hover" :content"当前输入的内容字节长度为&#xff1a; this.byteLength &#xff0c;剩余可输入的字节长度和最…