基于骨架的(Skeleton-Based Action Recognition)主要任务是从一系列时间连续的骨骼关键点(2D/3D)中识别出正在执行的动作。因为牵涉到骨骼框架这种图结构的输入,采用的方法逐渐成为了主流,并取得了不错的效果。ST-GCN是基于动态骨骼的动作识别方法ST-GCN(时空图卷积网络模型)
论文链接:https://arxiv.org/abs/1801.07455
Github 代码:https://github.com/yysijie/st-gcn?
建议先看完GCN的相关知识
对GCN的总结就是GCN算法的步骤简单可总结为:
1、初始化:为每个节点分配初始特征表示。
2、邻居聚合:对于每个节点,将其自身特征与邻居节点的特征进行加权平均或拼接,得到聚合后的特征。
3、特征转换:对聚合后的特征进行线性变换,以充分利用特征之间的关系。
4、非线性激活:应用非线性激活函数,如ReLU,将线性变换后的特征映射到非线性空间。
5、循环迭代:重复进行邻居聚合、特征转换和非线性激活的步骤,直到达到所需的网络层数或收敛条件。
对于每个节点,我们在聚类时从它的所有邻居节点处获取其特征信息,当然也包括它自身的特征。
香港中大-商汤科技联合实验室的最新 AAAI 会议论文「Spatial Temporal Graph Convolution Networks for Skeleton Based Action Recognition」提出了一种新的 ST-GCN,即时空图卷积网络模型,用于解决基于人体骨架关键点的人类动作识别问题。该方法除了思路新颖之外,在标准的动作识别数据集上也取得了较大的性能提升。
我们都知道一个动作都是连续的,例如我们进行喝水这个动作,那么从拿起水杯到喝完放下水杯着整个动作才能被称作为喝水;这时候就需要对视频进行每一帧的骨骼点输出,组装在一起形成一个完整的动作,因此基于骨架的动作识别方法的一般输入为时间连续的人体骨架关键点。
OpenPose 是一个标注人体的关节(颈部,肩膀,肘部等),连接成骨骼,进而估计人体姿态的算法。作为视频的预处理工具,我们只需要关注 OpenPose 的输出就可以了。
总的来说,视频的骨骼标注结果维数比较高。在一个视频中,可能有很多帧(Frame)。每个帧中,可能存在很多人(Man)。每个人又有很多关节(Joint)。每一个关节又有不同特征(位置、置信度)。其数据维度一般为**(N, C, T, V, M )**
考虑到动作识别的特点,作者并未使用单一的卷积核,而是使用『图划分』,将 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^表示的所有边如上图右侧所示:
在ST-GCN这篇文章中,作者的另一大创新点是通过对运动的分析引入了图划分策略,即建立多个反应不同运动状态(如静止,离心运动和向心运动)的邻接矩阵。作者在原文中提到其采用了三种不同的策略,分别为:
使用这样的分解方法,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`。
该代码主要通过对图的划分,对节点对进行分类并创建多个权重矩阵。这些权重矩阵根据节点之间的距离和它们到中心节点的距离进行分类,从而在神经网络中对不同跳数的节点进行不同权重的处理。'''
从结果上看,最简单的图卷积似乎已经能取得很好的效果了,具体实现如下:
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)=D−1AX
化简如下:
其实就是以边为权值对节点特征求加权平均。其中, A ^ = D − 1 A \hat{A}=D^{-1} A A^=D−1A可以理解为卷积核。
加上上面的图划分策略,我们可以可以写出带有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`。
这些步骤组合在一起,展示了通过卷积操作与邻接矩阵来对输入张量进行处理,并最终返回处理后的张量和邻接矩阵。'''
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 行像素的卷积
在时间卷积中,的大小为『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` 对象将这些层结合在一起,形成一个顺序执行的结构。'''
作者在进行图卷积之前,还设计了一个简易的注意力模型(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 层来处理输入数据,并通过权重参数调整边的重要性。这种关注边权重的方式可以让模型更好地学习空间时间图结构的特征,提高模型的预测性能。'''
时空图卷积分为空间图卷积
和时间图卷积
,其中空间图卷积是核心部分。一个空间图卷积加上一个时间卷积就是一层,一共10层,但是第一层没有残差结构,所以大部分文献都称它9层。原代码里的空间图卷积对应名称gcn
,时间对应tcn
。每一层的结构如下。
论文中的网络结构如下:
首先我们先来看第一部分:对输入矩阵进行归一化
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)进行归一化。
这个操作的作用:
然后,通过 ST-GCN 单元,交替的使用 GCN 和 TCN,对时间和空间维度进行变换:
# 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)对特征进行分类,具体实现如下:
# 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中的表现就是能够同时聚合时空维度的信息,如下图所示。
其具体网络层如图所示:
其具体可以分为以下步骤:
其具体结合openpose实现可参考
参考:
本文章仅当做记录学习使用,以便以后回顾