知识补充

CNN模型简介

深度学习中一种非常强大的神经网络架构,广泛应用于计算机视觉和图像处理任务。关键特点是利用卷积操作自动提取图像特征。

卷积层

核心操作是卷积,卷积操作通过滑动的卷积核(Filter)逐步扫描输入图像,并计算局部区域的加权和,生成特征图(Feature Map)每个卷积核都在图像上滑动并执行相同的计算,因此它的参数是共享的,大大减少了模型的参数量。

操作步骤

输入:假设输入图像是一个二维矩阵,大小为H*W,其中H是图像高度,W是图像宽度,图像可以是灰度图(单通道)或者彩色图(多通道)。

卷积核(Filter):卷积核是一个比输入图像小的矩阵,通常用来在图像上滑动并进行加权求和,卷积核的大小通常为3*3或5*5或更大,取决于任务的复杂性。

运算

卷积核在输入图像上滑动,进行局部区域的加权求和。每次卷积核在图像的某个区域上滑动,都会计算卷积核与该区域的点积,然后将结果存储到输出特征图的相应位置。

输出特征图

卷积操作的结果是一个特征图,其大小由输入图像大小、卷积核大小和步幅(stride)决定,特征图每个值代表卷积核与图像某个区域的局部相应。

关键参数

步幅

步幅是卷积核在输入图像上滑动的步长。步幅越大,特征图的尺寸越小,因为卷积核滑动的越快,减少了计算量。

零填充

避免卷积操作导致特征图尺寸过小,通常会在图像边缘添加一些零填充,保证了卷积核可以完全覆盖图像边缘区域

卷积核数量

在CNN中,通常使用多个卷积核来提取不同的特征。例如第一个卷积层可能有32个卷积核,第二个卷积层可能有64个卷积核,每个卷积核负责提取不同类型的特征。

作用

提取图像特征

卷积层核心作用是从原始图像中提取特征,通过多个卷积层的堆叠,网络可以提取从低级(边缘,纹理)到高级(物体形状,局部特征)等不同的层次特征。

低级特征:卷积核能够提取边缘、角点等局部特征,这些特征对手势的识别很重要

高级特征:通过多层卷积操作,CNN可以组合低级特征,识别更复杂的模式,例如手势的具体形态。

保持空间结构

卷积层具有局部感受野,能够保持图像的空间结构。在卷积操作中,卷积核值处理局部区域,保留了图像中的空间信息,这对于手势识别至关重要。

在手语识别中的作用

静态手势识别:对于每个手势,卷积层提取手势图像的低级特征(如手部形状,位置,边缘)并未后续分类任务提供输入

特征层次构建:通过多层卷积,网络可以从简单的边缘特征逐步构建复杂的手势模式

激活函数

激活函数为网络引入非线性特征,使得神经网络能够学习和表示复杂关系。没有激活函数,神经网络就相当于多个线性变换的组合,不具备任何复杂的表达能力,因此激活函数的作用至关重要。

池化层

用于对特征图进行下采样,减少计算量,降低参数量,同时保留重要特征。

减少特征图尺寸:通过池化操作,特征图的尺寸变小,减少后续层的计算量

减少过拟合:通过降低特征图的分辨率,池化层有效的压缩了数据,帮只模型提高泛化能力,避免过拟合

提取不变特征:池化操作能提高特征在不同输入图像中变化的稳定性,使得网络更加鲁棒。

常见类型

最大池化

从每个池化窗口中选择最大值作为该区域的代表

优点:保留图像中最重要的特征;提高网络的鲁棒性和稳定性

缺点:对于小物体或细节特征的损失较大

平均池化

对池化窗口所有值取平均值,作为该区域代表

优点:可以平滑特征,减少噪声影响;提取的是平均特征信息,适合一些需要平滑特征的场景

缺点:可能导致细节的丢失,因为它没有保留最显著特征

池化层在手语识别中的作用

特征缩放:通过池化操作,手势图像的特征被缩小,减少了计算量,有助于加速训练和推理

保持重要特征:最大池化能够保留手势图像中的显著特征,如手的形状,位置,边缘等,同时消除无关信息

减少过拟合:池化操作减少了网络参数,降低了模型复杂度,从而减少过拟合风险

多层卷积和池化

多层卷积和池化通过层层堆叠卷积层和池化层,能够逐步提取图像中的局部和全局特征,有效提高模型的识别能力

全连接层(Fully Connected Layer,简称FC层)

用于将神经网络中所有神经元之间建立连接,最终输出网络的结果。全连接层通常用于神经网络的最后几层,用于根据前面层提取的特征进行分类,回归等任务。特点是每一个神经元都与前一层所有神经元相连接。

结构

输入:每个神经元都接收前一层所有神经元的输出,形成一个向量

权重:每个连接都有一个权重,决定了该连接的强度。权重矩阵是通过训练学习得到的

偏置:每个神经元都有一个偏置项,帮助模型对数据进行平移,增加模型灵活性。偏置允许神经网络调整调整激活函数的输出,即使输入为零,神经元也可以有一个输出值,其作用是使得神经网络的激活更加灵活,无论输入值如何网络都能学习到合适的函数映射。

输出:每个神经元输出一个值,作为下一层的输入,通常会通过激活函数进行处理

作用

在手语转文字中,卷积层和池化层通常负责提取图像的低级特征,而全连接层负责将这些特征整合,并输出最终的分类结果。

比如以下举例:

手语字母识别:对于每个手语字母,卷积层和池化层提取图像中的手势特征,最后通过全连接层将这些特征映射到对应的字母标签。

手语句子识别:对于一系列连接的手势,卷积层和池化层提取每一帧的图像特征,如果是LSTM(Long Short-Term Memory,长短期记忆网络)处理时序信息,最后通过全连接层将这些特征综合,输出句子预测。

LSTM(Long Short-Term Memory,长短期记忆网络)

用于处理序列数据,用一条传送带+三个“门”控制信息的保留/丢弃,显著缓解梯度消失/爆炸,能学更长依赖。

结构与公式(时间步t)

应用

LSTM的时序模式适用于手势的动态特征。我们采用CNN提取帧特征得到向量喂给LSTM进行处理。

为了提取孤立词,我们时序池化后分类进行提取。

在使用过程中我们采用LSTM的变体,BILSTM,它具有前向后向时序的特征,前后文照应更强,我们利用此特征生成一句一句的翻译。

CTC模型介绍

适用场景:语音识别、手写/OCR、手语视频转文本等输入/输出长度不对齐且只需句级标注的任务。CTC通过对其路径求和让模型在无需帧级标签的前提下训练。

相关概念介绍

输入特征序列:x1:T,长度T(例如T帧视频或声谱帧)。

词表:V(字符/拼音/词片),再额外加blank。

模型在每个时间步t产生一个分布

p(k|xt)表示在实践步t模型对当前步输出为k的后验概率(即已知这一帧的内容估计对应标签为k的概率)

这里的xt常指编码器在第t步的特征/状态(ht),实际来自整段输入做过卷积的处理

若是双向/Transformer,ht含有全序列x1:t的信息,

若是流式/单向RNN,则主要依赖到当前的上下文

目标标签序列

长度为U(不包含blank)

折叠函数B(collapse)

1.合并相邻的重复(但跨过blank不合并)

2.删除所有blank

例如:a_aab_cc_ --->(合并相邻重复)a_ab_c_--->(去blank)aabc

CTC目标:让所有能折叠刀标签y的路径概率之和最大

即让图中的LCTC最小。这里的πt为一个路径中的第t个字符选用乘积形式就是说这个路径是由这T个字符组成的概率大小。再取所有可以通过折叠得到y句子的路径概率求和,让这个值尽可能大。

HMM(隐马尔可夫模型)

用“看不见的状态序列”(马尔可夫链)+可见的观测序列(如帧序列),来刻画时序数据,并用一套动态规划算法评估、解码。学习。

概念介绍

隐状态:系统真正运行在的模式/标签,记作zt

观测:我们能测到的信号,记作xt

马尔可夫性:下一步只以来当前隐状态P(zt|z(t-1))

发射:给定隐状态生成观测P(xt|zt)

举手语翻译的例子,隐状态是我们手语的原子词汇,而观测是展现出的视觉特征(RGB/光流/骨骼关键点/手部形状)

与CTC的对比

HMM显示有转移A与发射B,A能处理一个隐状态后面更可能跟什么隐状态,B能处理一个隐状态可能会有什么样的观测,能表达谁后面更可能跟谁。而CTC一句观测对每帧给出后验分布,并用固定规则做单调对齐,得到符号序列,若要形成自然语言句子,需要额外的语言/翻译模型。

batch size

batch size指的是模型在一次前向传播+反向传播中,同时处理的样本数量

batch size=1:一次只看一个样本,算一次梯度,更新一次参数

batch size=16:一次同时看16个样本,综合它们的误差,算一次梯度,再更新参数

从数学角度看:batch size 决定“梯度是怎么来的

训练时参数更新公式本质是:

batch size=1时

梯度噪声很大,更新很抖,但泛化能力有时更好

batch size=B时

梯度更稳定,GPU利用率更高,收敛更平滑

工程视角:batch size直接影响三件事

显存占用:在实际训练过程中显存与batch size基本呈线性相关

训练速度:batch太小,GPU吃不满,频繁同步,慢;batch过大,单步很慢,整体也不一定快。应该选取适中的batch size。

收敛域泛化

经验规律(不是铁律):

batch size特点
很小 (1~4)噪声大,泛化好,但训练不稳定
中等 (8~32)最常用,稳定+泛化平衡
很大 (64+)稳定但可能泛化下降,需要调 LR

模型介绍

model_ctc.py

import torch
import torch.nn as nn
import torch.nn.functional as F

def build_mediapipe_A():
    V = 75
    A = torch.eye(V)

    pose_edges = [
        (0, 1), (1, 2), (2, 3), (3, 7),
        (0, 4), (4, 5), (5, 6),
        (0, 8), (8, 9), (9, 10),
        (11, 12),
        (11, 13), (13, 15),
        (12, 14), (14, 16),
        (15, 17), (16, 18),
        (6, 20), (10, 19),
    ]

    hand_edges = [
        (0, 1), (1, 2), (2, 3), (3, 4),
        (0, 5), (5, 6), (6, 7), (7, 8),
        (0, 9), (9,10), (10,11), (11,12),
        (0,13), (13,14), (14,15), (15,16),
        (0,17), (17,18), (18,19), (19,20)
    ]

    left_hand_offset = 33
    right_hand_offset = 54
    left_hand_edges = [(i + left_hand_offset, j + left_hand_offset) for i, j in hand_edges]
    right_hand_edges = [(i + right_hand_offset, j + right_hand_offset) for i, j in hand_edges]

    A[15, 33] = A[33, 15] = 1
    A[16, 54] = A[54, 16] = 1

    for i, j in pose_edges + left_hand_edges + right_hand_edges:
        A[i, j] = 1
        A[j, i] = 1

    D = torch.diag(1.0 / torch.sqrt(A.sum(1) + 1e-6))
    return D @ A @ D

class STGCNBlock(nn.Module):
    def __init__(self, in_ch, out_ch, A, stride=1):
        super().__init__()
        self.A = A
        self.theta = nn.Conv2d(in_ch, out_ch, 1)
        self.tconv = nn.Conv2d(out_ch, out_ch, (7, 1), padding=(3, 0), stride=(stride, 1))
        self.bn = nn.BatchNorm2d(out_ch)
        self.relu = nn.ReLU(inplace=True)
        self.down = None
        if in_ch != out_ch or stride != 1:
            self.down = nn.Sequential(
                nn.Conv2d(in_ch, out_ch, 1, stride=(stride, 1)),
                nn.BatchNorm2d(out_ch),
            )

    def forward(self, x):
        A = self.A.to(x.device)
        y = self.theta(x)
        y = torch.einsum("nctv,vw->nctw", y, A)
        y = self.tconv(y)
        res = x if self.down is None else self.down(x)
        y = self.bn(y)
        return self.relu(y + res)

class BiLSTM_Head(nn.Module):
    def __init__(self, in_ch, num_classes, hidden_size=256, num_layers=2, dropout=0.3):
        super().__init__()
        self.hidden_size = hidden_size
        self.num_layers = num_layers
        
        # BiLSTM层
        self.lstm = nn.LSTM(
            input_size=in_ch,
            hidden_size=hidden_size,
            num_layers=num_layers,
            batch_first=False,  # 使用 (T, N, C) 格式以适应CTC
            bidirectional=True,
            dropout=dropout if num_layers > 1 else 0
        )
        
        # 输出层
        self.output = nn.Linear(hidden_size * 2, num_classes)  # *2 因为双向
        
        # Dropout层
        self.dropout = nn.Dropout(dropout)

    def forward(self, x):
        # 输入 x 形状: [N, C, T] -> 需要转换为 [T, N, C] 给LSTM
        x = x.permute(2, 0, 1)  # [T, N, C]
        
        # 通过BiLSTM
        lstm_out, _ = self.lstm(x)  # [T, N, hidden_size * 2]
        
        # Dropout
        lstm_out = self.dropout(lstm_out)
        
        # 输出层
        output = self.output(lstm_out)  # [T, N, num_classes]
        
        return output

class STGCN_BiLSTM_CTC(nn.Module):
    def __init__(self, num_classes, gcn_channels=(64, 128, 256), lstm_hidden=256, lstm_layers=2):
        super().__init__()
        self.V = 75
        self.A = build_mediapipe_A()

        c1, c2, c3 = gcn_channels
        self.g1 = STGCNBlock(3, c1, self.A)
        self.g2 = STGCNBlock(c1, c2, self.A)
        self.g3 = STGCNBlock(c2, c3, self.A)

        self.gap = nn.AdaptiveAvgPool2d((None, 1))
        
        # 用BiLSTM替换MSTCN
        self.bilstm = BiLSTM_Head(
            in_ch=c3, 
            num_classes=num_classes, 
            hidden_size=lstm_hidden, 
            num_layers=lstm_layers
        )

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        N, T, D = x.shape
        assert D == 225, f"Expected feature dim 225, got {D}"

        # 空间特征提取
        x = x.view(N, T, 75, 3).permute(0, 3, 1, 2).contiguous()

        y = self.g1(x)
        y = self.g2(y)
        y = self.g3(y)

        # 全局平均池化
        y = self.gap(y).squeeze(-1)  # [N, Cg, T]
        
        # 通过BiLSTM
        y = self.bilstm(y)  # [T, N, num_classes]
        
        return F.log_softmax(y, dim=-1)

问题背景与整体思路

手语翻译本质上是一个结构化时序到文本序列的映射问题。与普通视频分类或动作识别不同,它同时具备以下三个显著特征:

1.强空间结构性 手语由人体姿态与双手关节协同完成,关键点之间并非独立,而是受骨架拓扑强约束。

2.强时间序列依赖 单个手势的语义往往依赖于前后多个帧的连续变化,而非孤立帧。

3.弱对其监督 在大多数数据集中,只能获得“句级或词级标签”,而无法精确标注每一帧对应的语义单位。

针对以上特点,本文采用了一种ST-GCN+BiSTM+CTC的组合模型,整体思路如下:
使用ST-GCN(时空卷积网络)建模人体与双手关键节点的空间拓扑及短期时间动态;使用BiLSTM进一步捕捉长程时序依赖;使用CTC(Connectionist Temporal Classification)解决无显式对其条件下的序列预测问题。

数据表示:MediaPipe75关键点序列

本模型基于MediaPipe提取的关键点序列作为输入特征,具体结构为:

Pose:33个关键点

Left Hand:21个关键点

Right Hand:21个关键点

合计75个关键点,每个点包含(x,y,z)三维坐标,因此:

单帧特征维度:75*3=225

输入张量形状:[N,T,225]

其中N为batch size,T为视频帧数

在进入图卷积网络前,特征会被重塑为

[N, C, T, V] = [N, 3, T, 75]

以符合ST-GCN的输入规范。

人体-手部图结构建模(邻接矩阵设计)

节点与拓扑定义

为了显式建模骨架,本文将75个关键点视为图的节点,并构建一张固定拓扑图:

姿态关键点:采用MediaPipe Pose官方拓扑

单只手:以wrist为根节点,向五指分叉的梳状结构

左右手分辨偏移索引后假如整体图中

身体与手部的关键连接

仅有姿态图和手部图仍然是不够的,为了让模型理解手相对于身体的位置关系,本文显式的添加了如下连接:

左手wrist与左肩/肘部对应关键点,右手wrist与右肩/肘部对应关键点

这一步使得身体与双手构成一张连通图而非三个鼓励子图,对于建模空间语义至关重要。

邻接矩阵归一化

最后邻接矩阵采用对称归一化:

其中D为度矩阵,该处理可以避免高度节点在信息聚合时产生数值放大,有助于训练稳定性

ST-GCN Block:时空卷积模块解析

单个ST-GCN Block的输入输出形状均为

[N, C, T, V]

其内部结构由四个关键步骤组成:

通道映射(1*1卷积)

通过Conv2d(in_ch,out_ch,kernel=1)对节点特征进行线性投影,相当于对每个(t,v)的特征向量做通道变换。

空间图卷积(邻接传播)

利用矩阵乘法实现图卷积

该操作使每个节点能够聚合其邻居节点(及自身)的信息。

时间卷积

使用(7*1)的卷积核,仅沿时间维进行卷积,用于提取短期动态模式(局部动作变化)。

残差连接

当通道数或时间步长不一致时,通过1*1卷积对残差分支进行对其,保证深层网络训练稳定。

主干网络结构

本文采用三层ST-GCN堆叠结构

(3 → 64) → (64 → 128) → (128 → 256)

其设计在于低层关注局部关节运动,高层逐步组合成更复杂的全身运动模式。

在ST-GCN主干之后,使用:

AdaptiveAvgPool2d((None, 1))

对关节维V=75做全局平均池化,而保留时间维T,得到:

[N, 256, T]

即帧级全面特征序列。

BiLSTM时序建模头

虽然ST-GCN已包含时间卷积,但其感受野仍然有限,为了进一步捕捉长距离手势依赖和前后文语义信息,本文引入双向LSTM作为书序建模模块。

更多推荐