gepc.graph
1.graph.py:定义graph类,得到邻接矩阵A
graph.py 的思路为
1.初始化。得到布局、策略、有无头部关键点、节点之间的联系、按照策略获得邻接矩阵。
2.获取数据集的边界数据edge,输入为布局layout。
获取布局(openpose or ntu 数据集节点信息)
得到每个节点自身的link关系
得到数据集中给出的关节点相互之间的link关系
是否加入头部关键点
edge=关节点自身link+ 关节点之间的link
设置姿态中心关节点
3.获取关节点之间的距离
建立一个 (节点数*节点数) 大小的零矩阵,并给矩阵中所有元素赋值为1
计算关节点之间距离步数:
节点之间的距离=零矩阵+正无穷。
对a矩阵中的数值进行d次方运算,对d取遍从0到最大距离1.
将新生成transermat进行堆叠。
对d在1到-1之间以0个步长进行遍历,让d将arrivemat中的d进行赋值。
返回hopdis。
4.获得邻接矩阵A
关节之间的有效距离范围为(0,maxhop=1)。
邻接矩阵=(顶点数目 * 顶点数目) 大小的零矩阵。
在有效距离中进行遍历,得到能使hopdis与hop相等的邻接矩阵,并将这个矩阵赋值为1,下称“有效距离邻接矩阵”。
对这个矩阵进行归一化。
最终获得邻接矩阵A。
如果在uniform策略下。建立一个 (1*顶点数目*顶点数目) 的零矩阵,将归一化的矩阵放在a(0)项,作为graph类的self.A。
如果在distance策略下。建立一个 (有效距离*顶点数目*顶点数目)的零矩阵,把有效距离邻接矩阵放入。
**如果在spatial策略下。把一个数组A放入。(在空间策略下,由于d的不同,对距离中心点远近不同的矩阵按照由近到远的顺序进行依次放入)**
建立零矩阵root,建立零矩阵close,建立零矩阵further。
如果hop为0,就在A数组后面添加root矩阵。
否则,加入root+close矩阵。再加入further矩阵。
对按照以上过程得到的A矩阵进行堆叠,得到邻接矩阵A。
最终返回邻接矩阵A:静态邻接矩阵:使用身体部位的连通性作为节点上的先验关系。
静态邻接A被固定,并由所有层共享。
"""
Graph definitions, based on awesome previous work by https://github.com/yysijie/st-gcn
图形定义,基于stgcn
"""
import numpy as np
class Graph:
""" The Graph to models the skeletons extracted by the openpose Graph:用于对openpose提取的骨骼进行建模
Args:
strategy (string): must be one of the follow candidates 策略(字符串):必须为以下之一
- uniform: Uniform Labeling 统一:统一标签
- distance: Distance Partitioning 距离:距离分区
- spatial: Spatial Configuration 空间:空间配置
For more information, please refer to the section 'Partition Strategies'
in our paper (https://arxiv.org/abs/1801.07455).
layout (string): must be one of the follow candidates 布局(字符串):必须为以下候选者之一
- openpose: Is consists of 18 joints. For more information, please
refer to https://github.com/CMU-Perceptual-Computing-Lab/openpose#output
- ntu-rgb+d: Is consists of 25 joints. For more information, please
refer to https://github.com/shahroudy/NTURGB-D
max_hop (int): the maximal distance between two connected nodes 两个连接的节点之间的最大距离
"""
def __init__(self,
layout='openpose',
strategy='spatial',
headless=False,
max_hop=1):
self.headless = headless
self.max_hop = max_hop
self.get_edge(layout)
self.hop_dis = get_hop_distance(self.num_node, self.edge, max_hop=max_hop)
self.get_adjacency(strategy) # 按照策略获得邻接矩阵
def __str__(self):
return self.A # a为(38行)得到邻接矩阵时创建的零矩阵
def get_edge(self, layout): # 获取数据集的边界edge
if layout == 'openpose':
self.num_node = 14 # openpose顶点node为14个(无头)
self_link = [(i, i) for i in range(self.num_node)] # 循环14次,得到关节自身的link
neighbor_link = [(4, 3), (3, 2), (7, 6), (6, 5), (13, 12), (12, 11),
(10, 9), (9, 8), (11, 5), (8, 2), (5, 1), (2, 1), (0, 1)] # openpose中关节之间的联系
if not self.headless: # if not= none,即headless=none,在有头部的情况下
neighbor_link += [(15, 0), (14, 0), (17, 15), (16, 14)] # 需要加入openpose中头部的关节联系
self.num_node = 18 # openpose顶点node为18个(有头)
self.edge = self_link + neighbor_link # edge为关节自身edge加关节之间的联系
self.center = 1 # 姿态中心节点为1
elif layout == 'ntu-rgb+d':
self.num_node = 25
self_link = [(i, i) for i in range(self.num_node)]
neighbor_1base = [(1, 2), (2, 21), (3, 21), (4, 3), (5, 21),
(6, 5), (7, 6), (8, 7), (9, 21), (10, 9),
(11, 10), (12, 11), (13, 1), (14, 13), (15, 14),
(16, 15), (17, 1), (18, 17), (19, 18), (20, 19),
(22, 23), (23, 8), (24, 25), (25, 12)]
neighbor_link = [(i - 1, j - 1) for (i, j) in neighbor_1base] # 由于不包含0节点
self.edge = self_link + neighbor_link
self.center = 21 - 1
else:
raise ValueError("Do Not Exist This Layout.") # 如果不为这两个模型就输出 没有出现这个布局
def get_adjacency(self, strategy): # 邻接的获得
valid_hop = range(0, self.max_hop + 1) # 关节之间的有效距离为(0,两个节点的最大距离),从0开始计数到两个节点的最大距离
adjacency = np.zeros((self.num_node, self.num_node)) # 生成一个顶点数目*顶点数目大小的零矩阵
for hop in valid_hop: # 在validhop中进行遍历
adjacency[self.hop_dis == hop] = 1 # 得到maxhop数量的邻接矩阵=1
normalize_adjacency = normalize_digraph(adjacency) # 图像归一化
if strategy == 'uniform': # uniform策略下
A = np.zeros((1, self.num_node, self.num_node))
A[0] = normalize_adjacency
self.A = A
elif strategy == 'distance': # 距离策略下
A = np.zeros((len(valid_hop), self.num_node, self.num_node))
for i, hop in enumerate(valid_hop): # 获得索引和对应值的方法,索引为validhop
A[i][self.hop_dis == hop] = normalize_adjacency[self.hop_dis == hop] # 获得归一化邻接矩阵
self.A = A
elif strategy == 'spatial': # 在空间策略下
A = []
for hop in valid_hop: # 在validhop进行循环
a_root = np.zeros((self.num_node, self.num_node)) # 建立零矩阵root
a_close = np.zeros((self.num_node, self.num_node)) # 建立零矩阵close
a_further = np.zeros((self.num_node, self.num_node)) # 建立零矩阵further
for i in range(self.num_node): # 在顶点数目-1循环
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) # 在列表末尾添加新的对象,在a矩阵后面添加root矩阵
else:
A.append(a_root + a_close)
A.append(a_further)
A = np.stack(A)
self.A = A
else:
raise ValueError("Do Not Exist This Strategy")
def get_hop_distance(num_node, edge, max_hop=1): # 关节之间的距离
A = np.zeros((num_node, num_node)) # 建立一个 节点*节点 大小的零矩阵
for i, j in edge: # 给矩阵进行赋值为1
A[j, i] = 1
A[i, j] = 1
# compute hop steps # 计算 关节之间距离的步数
hop_dis = np.zeros((num_node, num_node)) + np.inf # 节点之间的距离= 零矩阵+ 正无穷
transfer_mat = [np.linalg.matrix_power(A, d) for d in range(max_hop + 1)] # 对A矩阵中的数值进行d次方运算,d取遍0到最大距离
arrive_mat = (np.stack(transfer_mat) > 0) # 沿着新轴连接数组的序列,axis=0,增加第一个维度
for d in range(max_hop, -1, -1): # 在最大距离到0之间进行循环
hop_dis[arrive_mat[d]] = d
return hop_dis
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
2.sagc.py
sagc.py的主要思路为:
1.初始化sagc模块
初始化及超类设置。按照stgcn的思想,将通道数划分为三个子集。
对A矩阵设置默认值。
使用nturgb或者open pose数据集布局。
将graph类中的布局策略,strategy,layout,headless导入。
把设定好的静态临界矩阵放入statadj变量中。
建立global矩阵(B矩阵),以statadj矩阵为基础。
将其转换为整型矩阵,将矩阵中0的元素填充为1e-6,属性为可进行梯度下降。
将statadj转换为整形矩阵,属性为不进行梯度估计,子集数量为3。
c矩阵的计算。
设计a、b两个卷积模块,用3个2d卷积函数(in、inter、1)对两个卷积模块进行扩充。
设计图卷积模块,用3个图卷积函数(out)进行扩充。
残差层设计。
如果输入输出通道数不相同,进行2d卷积操作和批量patch归一化。否则,x矩阵不变。
批量归一化,求softmax,激活。
取得statadj矩阵弟零项,并乘3作为卷积核大小。(每个单独通道的邻接矩阵不同)
扩大卷积。2d卷积(in、out*卷积核、卷积核、步长=1、没有偏移)
衰减卷积。2d卷积(3*out、out、步长=1、没有偏移)
2.计算c矩阵
x矩阵尺寸输入。包括(n,chan,t,v)
建立一个数组C。
对卷积模块a中的值进行维度0123到0312的转换,再改变tensor的维度(n,v,中心通道数*t),达到转置的目的。
对卷积模块a和b之间进行点积运算,输入进softmax函数,并进行归一化,最后得到 inferred 邻接矩阵(c矩阵)。
3.前向传播
返回C矩阵。得到C矩阵的残差。对x矩阵进行扩大卷积。
把邻接矩阵adj赋值给A矩阵。提取输入x的大小(n,kc,t,v),并转变x矩阵的维度。
定义abc三个矩阵的获得方法。每个gconv获取outchannels作为输入,区别在于他们哦那个有不同的卷积核。
将这三个矩阵获得方法进行连接,步长为1,在给定维度上对输入的张量序列进行连续操作。得出的结果赋值给输入x。
执行衰减卷积。
输出y=x+x的残差。
全局卷积归一化和1*1卷积层
1.全局卷积:主要思想为,对bn和relu过程应用单个邻接图卷积
初始化。定义bn为2d批量归一化函数,激活函数为relu,将计算得到的值直接覆盖之前得到的值。
前向传播。
全局邻接矩阵的情况下。如果邻接矩阵的尺寸长度为3,使用矩阵乘法,改变其维度为nctw。
每个样本矩阵情况下。如果尺寸长度为4,改变维度。
归一化之后进行激活,最终返回x。
2.1*1卷积层:能够将尺寸缩小3倍计算。
初始化。定义组为输入通道数//邻接矩阵的类型数,并赋值给输出通道数。
应用处理视频的3d卷积函数,提取时序信息。
前向传播。
3.统计减少:通过汇总将尺寸减少3倍。
组=输入通道数//邻接矩阵类型数。
前向传播,提取输入矩阵的大小,改变其维度(第一维缩小一倍),去掉第二维。
import torch
import torch.nn as nn
import numpy as np
from models.graph.graph import Graph
class SAGC(nn.Module):
"""
Spatial Attention Graph Convolution 空间注意力图卷积
Applied to K_n adjacency subsets, each with K_a matrices 应用于K_n个邻接子集,每个子集都有K_a个矩阵
Base class provides the data-based C matrix and returns 基类提供基于数据的C矩阵并返回
K_n * K_a results.
"""
def __init__(self, in_channels, out_channels,
stat_adj=None,
num_subset=3,
coff_embedding=4,
ntu=False,
headless=False):
super().__init__() # 初始化及超类设置
inter_channels = out_channels // coff_embedding # 划分子集的方式,按照stgcn方式。中心通道=输出通道//嵌入coff
self.adj_types = 3 # 邻接矩阵的类型=3
self.inter_c = inter_channels # 按照stgcn,一共划分为三个子集
if stat_adj is None: # Default value for A: 对a矩阵设置默认值 如果没有静态临界矩阵
layout = 'ntu-rgb+d' if ntu else 'openpose' # 使用ntu数据集,否则openpose
self.graph = Graph(strategy='spatial', layout=layout, headless=headless) # 布局策略,空间、布局、无头
stat_adj = self.graph.A # 把设定好的邻接矩阵a矩阵放入stat(静态邻接)中
self.g_adj = torch·.from_numpy(stat_adj.astype(np.float32))
# 建立global矩阵。 astype是布尔类型,非零为true,零为false,即转换为整型矩阵
nn.init.constant_(self.g_adj, 1e-6) # 用1e-6填充向量,将0的位置填充为1e-6
self.g_adj = nn.Parameter(self.g_adj, requires_grad=True) # 转换gsdj的格式,判断条件为梯度需要
self.stat_adj = torch.from_numpy(stat_adj.astype(np.float32)) # 转换为整型矩阵
self.stat_adj.requires_grad = False # 把stat邻接矩阵转换为不要梯度
self.num_subset = num_subset # 数据集的数量为3
# Convolutions for calculating C adj matrix 计算C矩阵的卷积
self.conv_a = nn.ModuleList() # modulelist内部没有封装forward函数,只是使用append或者extend的操作对list进行扩充。
self.conv_b = nn.ModuleList()
for i in range(self.num_subset): # 卷积三次
self.conv_a.append(nn.Conv2d(in_channels, inter_channels, 1))
# conv2d作为二位卷积的实现。先实例化再使用。卷积核大小为1*1
self.conv_b.append(nn.Conv2d(in_channels, inter_channels, 1))
self.gconv = nn.ModuleList() # global矩阵
for i in range(self.adj_types):
self.gconv.append(GraphConvBR(out_channels)) # gconv后面加全局卷积生成的x
# Residual Layer 残差层
if in_channels != out_channels: # 如果输入输出通道数不相同
self.down = nn.Sequential(nn.Conv2d(in_channels, out_channels, 1),
nn.BatchNorm2d(out_channels))
# 进行卷积和批量归一化
# sequential是一个有序的容器,神经网络模块将按照传入构造器的顺序一次被添加到计算图中执行,
# 同时以神经网络模块为元素的有序字典也可以传入参数。
# 根据自己的需求,把不同函数组合成一个小模块使用。
else:
self.down = lambda x: x # 否则down等于x矩阵不变
self.bn = nn.BatchNorm2d(out_channels) # bn为批量标准化
self.soft = nn.Softmax(-2) # Cols sum to 1.0, perhaps rows should? softmax函数
self.relu = nn.CELU(0.01) # nn.ReLU()
self.kernel_size = self.stat_adj.size(0) # Split to two different operators? 卷积核为stat邻接矩阵的第0项
self.kernel_size *= 3 # Separate channels for each adj matrix 每个调整矩阵的单独通道
self.expanding_conv = nn.Conv2d(in_channels,
out_channels * self.kernel_size,
kernel_size=(1, 1),
padding=(0, 0),
stride=(1, 1),
dilation=(1, 1),
bias=False)
# 扩大卷积
self.reduction_conv = nn.Conv2d(3 * out_channels, out_channels, 1, bias=False) # 衰减卷积
def forward(self, x, adj): # 前向传播
C = self.calc_C(x) # 返回c矩阵
x_residual = self.down(x) # 剩余层的x矩阵的得到
x = self.expanding_conv(x) # 对x矩阵进行扩大卷积
A = adj # 把邻接矩阵赋给a
N, KC, T, V = x.size() # 提取x的大小
x = x.view(N, self.kernel_size, KC//self.kernel_size, T, V) # 转变x矩阵的维度
# Each gconv gets out_channels as input, for sep_sum the channels
# are from separate convolution kernels, otherwise they are the same inputs
# 每个gconv获取out_channels作为输入,对于sep_sum,这些通道来自单独的卷积内核,否则它们是相同的输入
x_A = self.gconv[0](x[:, 0:3], A)
x_B = self.gconv[1](x[:, 3:6], self.g_adj)
x_C = self.gconv[2](x[:, 6:9], C)
# Summarize over A0,A1,A2, and conv-pool over x_A, x_B, x_C 汇总A0,A1,A2,并汇总x_A,x_B,x_C的转换池
x = torch.cat((x_A, x_B, x_C), dim=1) # 在给定维度上对输入的张量序列进行连接操作,输入为xabc,将这三个列表按照维度为一进行连接。
x = self.reduction_conv(x) # 执行衰减卷积
y = x + x_residual # 最终得到y
return y, adj # 返回y和连接
def calc_C(self, x): # c矩阵的计算
N, chan, T, V = x.size() # x矩阵的尺寸的输入
# Calc C, the data-dependent adjacency matrix 数据相关的邻接矩阵
C = [] # 建立一个数组
for i in range(self.num_subset): # (0,2)循环
C1 = self.conv_a[i](x).permute(0, 3, 1, 2).contiguous().view(N, V, self.inter_c * T)
# conva函数维度123交换位置。
# 在使用peimute函数之后,数据不再连续,需要使用contiguous函数,在使用view函数改变tensor维度。否则会报错。
C2 = self.conv_b[i](x).view(N, self.inter_c * T, V)
C_curr = self.soft(torch.matmul(C1, C2)) # / C1.size(-1)) # N V V 矩阵乘法后,输入进softmax函数
C.append(C_curr) # c矩阵后加入currt函数
del C1, C2
C = torch.stack(C).permute(1, 0, 2, 3).contiguous() # 最终得到c矩阵
return C
class GraphConvBR(torch.nn.Module): # 全局卷积
def __init__(self, out_channels):
"""
Applies a single adjacency graph convolution with BN and Relu 对BN和Relu应用单个邻接图卷积
bn是批量标准化,在cnn中,batch就是训练网络所设定的图片数量batchsize
对于输入的一个batch数据,先计算出每个维度的平均值和方差,
对于一个2*2的灰度图像来说,那么就要计算一个batch内每个像素通道的平均值和方差(共计算四次,得到四个平均值和方差)。
"""
super().__init__()
self.bn = nn.BatchNorm2d(out_channels)
#返回一个shape与num-feature相同的tensor,numfeature为输入batch中图像的Chanel数(按照每一个chanel来做归一化)
self.act = nn.ReLU(inplace=True) # 激活函数relu,inplace=true:是否将计算得到的值直接覆盖之前的值
def forward(self, x, adj): # 前向传播
if len(adj.size()) == 3: # Global adj matrix
x = torch.einsum('nkctv,kvw->nctw', (x, adj))
elif len(adj.size()) == 4: # Per sample matrix
x = torch.einsum('nkctv,nkvw->nctw', (x, adj))
x = self.act(self.bn(x)) # 归一化之后进行激活
return x
class Conv1x1(torch.nn.Module): # 1*1卷积层
def __init__(self, in_channels, adj_types=3):
"""
An operation for reducing dimensions by a factor of 3 using 1x1 convs. 使用1x1转换将尺寸缩小3倍的运算。
Reduces in groups of 3, i.e. (512, 3, 64, 12, 18) -> (512, 1, 64, 12, 18)
and (512, 9, 64, 12, 18) -> (512, 3, 64, 12, 18) where channel i of the output 3 is
a function of channels (3*i:3*i+2) of the input [Batch, adj, chan, time, vertices]
"""
super().__init__()
groups = in_channels // adj_types # 组=输入通道//3
out_channels = groups # Each group reduces to a single channle 每组减少到一个通道
self.conv = nn.Conv3d(in_channels, out_channels, kernel_size=1, groups=groups)
# 在处理视频时使用,目的是为了提取时序信息。输入输出通道数、卷积核大小、group(步长/补位)。
def forward(self, x): # 前向传播
x_out = self.conv(x)
return x_out
class SumReduce(torch.nn.Module):
def __init__(self, in_channels, adj_types=3):
"""
An operation for reducing dimensions by a factor of 3 by summarizing. 通过汇总将尺寸减小三倍的操作。
Reduces in groups of 3, i.e. (512, 3, 64, 12, 18) -> (512, 1, 64, 12, 18)
and (512, 9, 64, 12, 18) -> (512, 3, 64, 12, 18) where channel i of the output 3 is
a function of channels (3*i:3*i+2) of the input [Batch, adj, chan, time, vertices]
"""
super().__init__()
self.groups = in_channels // adj_types
def forward(self, x):
N, adj, Chan, T, V = x.size() # I.e. [512, 6, 32, 12, 18]
x = x.view(N, self.groups, adj // self.groups, Chan, T, V) # I.e. [512, 3, 2, 32, 12, 18]
x_out = x.sum(dim=2).contiguous() # I.e. [512, 3, 32, 12, 18]
return x_out
3.pygeoconv.py
convtemporalgraphical类
时域图像卷积:应用图卷积的基本模块。
输入卷积核。
通过2d卷积定义卷积内容。
定义前向传播。
确认邻接矩阵的第零项是否等于卷积核大小。
进行卷积,并提取x的尺寸信息。
改变输入矩阵的维度大小。
pygeoconv类
初始化。输入为self类、输入输出通道数、卷积核、卷积操作模式、有无头和dropout。
如果卷积操作为 sagc (2.空间注意力图像卷积),执行sagc.py中的sagc函数。
如果卷积操作为 gcn (图卷积),执行本部分中的convtempral函数。
进行前向传播,最终返回卷积结果。
import torch
import torch.nn as nn
from models.graph.sagc import SAGC
class PyGeoConv(nn.Module):
def __init__(self,
in_channels,
out_channels,
kernel_size=1,
conv_oper='sagc',
headless=False,
dropout=0.0,
):
super().__init__()
self.dropout = dropout
self.kernel_size = kernel_size
self.in_channels = in_channels
self.out_channels = out_channels
self.multi_adj = kernel_size > 1
if conv_oper == 'sagc': # 如果卷积操作为sagc
self.g_conv = SAGC(in_channels, out_channels, headless=headless)
elif conv_oper == 'gcn': # 卷积操作为gcn
self.g_conv = ConvTemporalGraphical(in_channels, out_channels, kernel_size) # 使用下面的gcn图卷积基本方法
else:
raise Exception('No such Conv oper')
def forward(self, inp, adj): # 定义前向传播
return self.g_conv(inp, adj)
class ConvTemporalGraphical(nn.Module):
"""
The basic module for applying a graph convolution. 应用图卷积的基本模块
Args:
in_channels (int): Number of channels in the input sequence data 输入序列数据通道数
out_channels (int): Number of channels produced by the convolution 卷积产生的通道数
kernel_size (int): Size of the graph convolving kernel 图卷积核的大小
t_kernel_size (int): Size of the temporal convolving kernel 时域卷积核的大小
t_stride (int, optional): Stride of the temporal convolution. Default: 1 时域卷积的步长,默认为1
t_padding (int, optional): Temporal zero-padding added to both sides of the input. Default: 0
在输入两侧都添加了时域的零padding
bias (bool, optional): If ``True``, adds a learnable bias to the output. Default: ``True``
如果为真,给输出增加可学习的偏见
"""
def __init__(self,
in_channels,
out_channels,
kernel_size,
t_kernel_size=1,
t_stride=1,
bias=True):
super().__init__()
self.kernel_size = kernel_size # 输入卷积核
self.conv = nn.Conv2d( # 定义卷积,in,out,1*1的卷积核,步长为1,有偏见
in_channels,
out_channels * kernel_size,
kernel_size=(t_kernel_size, 1),
stride=(t_stride, 1),
dilation=(1, 1),
bias=bias)
def forward(self, x, adj): # 定义前向传播
assert adj.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, adj))
return x.contiguous(), adj
4.st-graph-conv-block
时空图卷积块
初始化。输入为:in/out chanels,kernel size,步长,dropout,卷积操作,是否激活,输出归一化,输出激活,残差,有无头。
确定卷积核是否为2,确认卷积核的第零项%2==1.。设置padding的长度。
将无头、输出激活、卷积操作、激活函数进行输入。
然后执行上述pygeoconv模块,执行 sagc函数。进行空间注意力图卷积。
进行输出批量归一化,否则,将函数输入进身份层。
执行时域卷积网络函数(tcn)。
批量归一化,激活,卷积,进入归一化层,dropout(将所有元素中每个元素按照概率0.5更改为0,起到求均值的作用)。
残差处理。
如果没有残差,设置残差=0.
如果有残差,且输出通道数==输出通道数,步长为1,那么就把残差保留。
否则,再次进行卷积核归一化操作。
前向传播。
得到残差。得到输入矩阵和邻接矩阵。
将x输入tcn函数,得到结果+残差,返回输入x的值。
再进行输出激活操作。最终返回x和adj。
import torch.nn as nn
from models.graph.pygeoconv import PyGeoConv
class ConvBlock(nn.Module): # 卷积块
def __init__(self, in_channels, out_channels, kernel_size,
stride=1,
dropout=0,
conv_oper='sagc',
act=None,
out_bn=True,
out_act=True,
residual=True,
headless=False,):
super().__init__()
assert len(kernel_size) == 2 # 确认卷积核是否等于2
assert kernel_size[0] % 2 == 1 # 确人卷积核大小是否
padding = ((kernel_size[0] - 1) // 2, 0) # padding:移动盒子的位置
self.headless = headless # 无头
self.out_act = out_act # 输出激活
self.conv_oper = conv_oper # 卷积操作
self.act = nn.ReLU(inplace=True) if act is None else act # 激活函数,替代原来的值
self.gcn = PyGeoConv(in_channels, out_channels, kernel_size=kernel_size[1], dropout=dropout,
headless=self.headless, conv_oper=self.conv_oper) # 执行sagc模块
if out_bn: # 如果存在输出批量归一化
bn_layer = nn.BatchNorm2d(out_channels) # 执行归一化层
else:
bn_layer = nn.Identity()
# Identity layer shall no BN be used 身份层不能使用bn。
# identify函数建立一个输入模块,什么都不做,通常用在神经网络的输入层。
# 相当于一个容器,把输入保存下来
self.tcn = nn.Sequential(nn.BatchNorm2d(out_channels),
self.act,
nn.Conv2d(out_channels,
out_channels,
(kernel_size[0], 1),
(stride, 1),
padding),
bn_layer,
nn.Dropout(dropout, inplace=True))
# 时域卷积网络函数。
# 执行如下操作:批量归一化,激活,卷积,进入归一化层,将所有元素中每个元素按照概率0.5更改为0
if not residual: # 如果没有残差
self.residual = lambda x: 0 # 把残差设为0
elif (in_channels == out_channels) and (stride == 1): # 如果输入输出通道数相等且步长为1
self.residual = lambda x: x # 残差设为x
else:
self.residual = nn.Sequential(nn.Conv2d(in_channels, # 否则再次进行卷积
out_channels,
kernel_size=1,
stride=(stride, 1)),
nn.BatchNorm2d(out_channels))
def forward(self, x, adj): # 前向传播
res = self.residual(x)
x, adj = self.gcn(x, adj)
x = self.tcn(x) + res
if self.out_act:
x = self.act(x)
return x, adj