ST-GCN模型详解(+openpose)

news2024/11/25 12:35:00

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)

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

工控CTF之协议分析类型

协议分析 主要以工控流量和恶意流量为主,难度较低的题目主要考察Wireshark使用和找规律,难度较高的题目主要考察协议定义和特征 简单只能简单得干篇一律,难可以难得五花八门 常见的工控协议有:Modbus、MMS、IEC60870、MQTT、CoA…

完整版软件建模复习题和答案

一、单选题 D )1.下面哪个不是信息系统利益相关者? A.客户 B.用户 C.开发人员 D.监理人员 B )2.下面哪项不是用户主要关注的软件质量属性? A.…

解线性方程组——上三角、下三角,回代算法 | 北太天元

解上三角(回代) a i i ≠ 0 a_{ii\neq0} aii0​ , i 1 , 2 , … , n i1,2,\ldots,n i1,2,…,n a 11 x 1 a 12 x 2 ⋯ a 1 n x n b 1 a 22 x 2 ⋯ a 2 n x n b 2 ⋯ a n n x n b n \begin{aligned} a_{11}x_1a_{12}x_2\cdotsa_{1n}x_n&b_1 \\ a_{22}x_2\cdotsa_…

基于Matlab机器人工具箱对Dobot机械臂的研究

文章目录 文章目录 前言 一、Dobot Mangician 分析 二、Matlab 机器人工具箱 1. 建立模型 2. DoBot 正向运动学 3. Dobot 逆运动学 4. Dobot workpace 5. Dobot轨迹规划 三、Dobot studio 1. DoBot teaching 2. DoBot Python 程序 总结 前言 在本实验中&#xf…

自如电费均摊问题

3月份搬了次家,嫌麻烦租了自如,第一个月的电费账单出来了,由于我是中途搬进去的,于是乎就好奇他会如何计算均摊,这个月电费账单出来了,算了下发现了点东西。 先说结论:按照我的这个均摊的方式&a…

TCP报文与三次握手四次断开、TCP最大连接数与文件打开数限制、keepalive、tcpdump、wireshark抓包分析工具

TCP报文 tcp详解、tcp与udp对比等 TCP:传输控制协议 UDP:用户数据报协议 源端口和目的端口字段:各占 2 字节(16位)。端口是运输层与应用层的服务接口。运输层的复用和分用功能都要通过端口才能实现。 序列号:在建立…

万兆以太网10G Ethernet简介

2002年6月IEEE标准协会批准了万兆(10G)以太网的正式标准。此标准的全名是“10Gbit/s工作的媒体接入控制参数、物理层和管理参数”。 另一个组织是10G以太网联盟(10GEA)。10GEA由网络界的著名企业创建,现已有一百多家企业参加,中国…

Pytorch DistributedDataParallel(DDP)教程一:快速入门理论篇

Pytorch DistributedDataParallel(DDP)教程一:快速入门理论篇 目录 一、 写在前面二、什么是分布式并行训练1. 并行训练2. 数据并行 三、DDP的基本原理1. DDP的训练过程2. Ring-All-Reduce算法 四、如何搭建一个Pytorch DDP代码框架1. 与DDP有…

javaScript常用知识点

1. this指向问题 在绝大多数情况下,函数的调用方式决定了this的值。this不能在执行期间被赋值,并且在每次函数被调用时this的值也可能会不同。 this指向的对象称为函数的上下文对象context;this的指向取决于函数被调用方式this的指向不是函数…

【机器学习】小波变换在特征提取中的实践与应用

小波变换在特征提取中的实践与应用 一、小波变换的基本原理与数学表达二、基于小波变换的特征提取方法与实例三、小波变换在特征提取中的优势与展望 在信号处理与数据分析领域,小波变换作为一种强大的数学工具,其多尺度分析特性使得它在特征提取中扮演着…

云服务器部署Springboot项目

前端项目打包 修改ip地址 在控制台输入npm run build:prod 会产生dist文件 将dist文件中的内容移动至/usr/local/nginx/html目录下 后端项目打包 修改ip地址 执行clean操作 执行install操作 将生成的target文件中的jar包移动至/usr/local/src目录下 启动 注意⚠️&#xff…

【linux】Ubuntu 修改用户名

第一次打开Ubuntu时不小心把初始用户名“siriusiot”写成“siriousiot”(多了一个o) 。作为技术人,我们要保持严谨,我们要纠正过来(其实就是单词拼错了怕被笑话)。 打开终端,输入: …

Redis key(BigKey、MoreKey)的存储策略

1. MoreKey 案例 1.1 大批量往 redis 里面 插入2000w 测试数据key (1) Linux Bash 下面执行&#xff0c;插入 100w rootspray:~# for((i1;i<100*10000;i)); do echo "set k$i v$i" >> /tmp/redisTest.txt; done; 查看 rootspray:~# more /tmp/redisTest.…

ABAP报表开发总结---采购排产表

1.动态创建内表 1.1首先维护好一个子例程 FORM frm_add_fcat USING value1 value2 value3 value4.wa_fcat-fieldname value1.wa_fcat-inttype value2.wa_fcat-reptext value3.wa_fcat-intlen value4.APPEND wa_fcat TO it_fcat.CLEAR: wa_fcat. ENDFORM. "frm_add_f…

win/mac达芬奇19下载:DaVinci Resolve Studio 19

DaVinci Resolve Studio 19 是一款功能强大的视频编辑和调色软件&#xff0c;广泛应用于电影、电视和网络节目的后期制作。这款软件不仅提供了专业的剪辑、调色和音频处理工具&#xff0c;还引入了全新的DaVinci Neural Engine AI工具&#xff0c;对100多项功能进行了大规模升级…

Mamba 学习

Vision Mamba U-Mamba 以后的趋势&#xff1a; 1.Mamba模型机机制上和transform一样&#xff0c;但是参数量上做了改进&#xff0c;可以直接替代 2.vision上可以实时处理

游戏登录界面制作

登录界面制作 1.导入模块和初始化窗口 import subprocessimport tkinter as tkimport picklefrom tkinter import messageboxwindow tk.Tk()window.title(Welcome)window.geometry(450x300) 导入必要的模块&#xff0c;并初始化了主窗口window&#xff0c;设置了窗口的标题和…

一 Mybatis简介

一 Mybatis简介 1.1 简介 MyBatis最初是Apache的一个开源项目iBatis, 2010年6月这个项目由Apache Software Foundation迁移到了Google Code。随着开发团队转投Google Code旗下&#xff0c; iBatis3.x正式更名为MyBatis。代码于2013年11月迁移到Github。 **MyBatis 是一款优秀…

JavaSE备忘录(未完)

文章目录 基本数据类型println 小知识除法( / ) 和 Infinity(无穷) 小知识除法InfinityInfinity 在除法中正负判断 求余(%) 小知识 基本数据类型 除 int、char 的包装类分别为 Integer、Character 外&#xff0c;其余基本数据类型的第一个字母大写就是它的包装类。 println 小…

微信小程序使用 Vant Weapp 中 Collapse 折叠面板 的问题!

需求&#xff1a;结合Tab 标签页 和 Collapse 折叠面板 组合成显示课本和章节内容&#xff0c;并且用户体验要好点&#xff01; 如下图展示&#xff1a; 问题&#xff1a;如何使用Collapse 折叠面板 将内容循环展示出来&#xff1f; js中的数据是这样的 代码实现&#xff1…