0
点赞
收藏
分享

微信扫一扫

ST-GCN论文简读以及复现

ST-GCN论文简读以及复现_pytorch
code: ​​​https://github.com/yysijie/st-gcn​​


文章目录

  • ​​普通卷积与图卷积​​
  • ​​普通卷积​​
  • ​​图卷积​​
  • ​​数据输入​​
  • ​​数据结构​​
  • ​​数据预处理​​
  • ​​图划分策略​​
  • ​​网络结构​​
  • ​​复现​​

普通卷积与图卷积

普通卷积

在理解图卷积之前,需要认识一下传统的卷积。对于一个3×3的卷积操作而言,相当于把3×3的卷积核在图像上滑动。每一次会有对应的9个数字相乘之后相加,然后得到一个最终的值。

在这里,换一个角度来看,图像上的每一个数字都是一个特征,而卷积的操作,是以一个像素点为中心,将其周围的8个点连同它自己的特征加权(卷积核的值)后加到一起,更通俗地理解为:将一个点的特征变为它周围所有邻居的特征的加权和。下图中卷积核参数全为1,相当于加权值为1。

ST-GCN论文简读以及复现_深度学习_02

图卷积

​​https://www.zhihu.com/question/54504471/answer/611222866​​

数据输入

数据结构

基于骨架的动作识别方法的一般输入为时间连续的人体骨架关键点,如下图1所示。
ST-GCN论文简读以及复现_pytorch_03
这些关键点可以通过openpose进行姿态估计获取,也可以手动标注。其数据维度一般为(N, C, T, V, M ),其中:

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

需要注意C(特征),T(时间),V(空间)。

数据预处理

事实上,上述输入数据(N, C, T, V, M )在输入至ST-GCN网络之前需要进行标准化操作。

该标准化是在时间维度上进行的,具体来说,就是标准化某节点在所有T个关键帧的特征值。其具体实现代码如下:

# data normalization
N, C, T, V, M = x.size()
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)

其中函数data_bn定义如下:

self.data_bn = nn.BatchNorm1d(in_channels * A.size(1))

图划分策略

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

  • Uni-labeling,即与跟根节点相邻的所有结点具有相同的label,如下图b所示。
  • Distance partitioning,即根节点本身的label设为0,其邻接点设置为1,如下图c所示。
  • Spatial configuration partitioning,是本文提出的图划分策略。也就是以根节点与重心的距离为基准(label=0),在所有邻接节点到重心距离中,小于基准值的视为向节心点(label=1),大于基准值的视为离心节点(label=2)。

ST-GCN论文简读以及复现_卷积_04
具体的代码实现如下:

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)

值得注意的是,hop类似于CNN中的kernel size。hop=0就是根节点自身,hop=1表示根节点与其距离等于1的邻接点们,也就是上图(a)的红色虚线框。

为了便于更好理解代码,我们默认上述两个循环中的为根节点。因为条件***if self.hop_dis[j, i] == hop***限制,可以视为根节点的本身(hop=0)或者其邻接节点(hop=1)。

网络结构

骨架输入数据具有时间与空间属性,这些属性对运动检测至关重要。因此提出ST-GCN应当具备能够从时空维度提取特征的能力,其在GCN中的表现就是能够同时聚合时空维度的信息,如下图所示。

ST-GCN论文简读以及复现_卷积_05
更具体地,我们给出了ST-GCN具体的结构图,如下图所示。

ST-GCN论文简读以及复现_2d_06
其具体可以分为以下步骤:

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

上述ST-GCN模块的代码实现如下:

def forward(self, x, A):

res = self.residual(x)
x, A = self.gcn(x, A)
x = self.tcn(x) + res

return self.relu(x), A

其中残差结构self.residual定义如下:

self.residual = nn.Sequential(
nn.Conv2d(
in_channels,
out_channels,
kernel_size=1,
stride=(stride, 1)),
nn.BatchNorm2d(out_channels),
)

GCN定义如下

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

TCN定义如下

self.tcn = nn.Sequential(
nn.BatchNorm2d(out_channels),
nn.ReLU(inplace=True),
nn.Conv2d(
out_channels,
out_channels,
(kernel_size[0], 1),
(stride, 1),
padding,
),
nn.BatchNorm2d(out_channels),
nn.Dropout(dropout, inplace=True),
)

实际上,本文提出模通过不断堆叠ST-GCN从图结构输入中持续提取高级的语义特征,具体如下:

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)

之后,和一般的分类任务类似,作者引入了全局平均池化以及全卷积层输出预测分支,如下:

# 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)

至此,通过代码我们就很容易理解ST-GCN的具体网络结构了。

复现

openpose python API安装, 可以参考​​这篇博文​​

修改下代码,如下图:
ST-GCN论文简读以及复现_卷积_07

sys.path.append('{}/python/openpose/Release'.format(self.arg.openpose))
os.environ['PATH'] = os.environ['PATH'] + ';' + f'{self.arg.openpose}/x64/Release;' + f'{self.arg.openpose}/bin;'

ST-GCN论文简读以及复现_神经网络_08


举报

相关推荐

0 条评论