TSN 与 TSM:时间片段与通道移位的高效动作识别
动作识别入门:TSN 与 TSM 高效时序建模
为什么需要 TSN 与 TSM?
动作识别是视频理解的核心任务,目标是对一段视频中人的行为进行分类(如“游泳”、“弹吉他”、“拥抱”)。早期方法尝试将 2D 卷积网络(如 ResNet)直接应用到每一帧,然后对帧级预测取平均,但这种单帧方式完全忽略了运动信息。3D 卷积(如 C3D、I3D)虽然能够捕获时空特征,但计算量巨大、参数量庞大,难以在普通硬件上训练。
为解决效率与性能的矛盾,时序分割网络(Temporal Segment Networks, TSN) 和 时序移位模块(Temporal Shift Module, TSM) 被提出。它们都基于 2D CNN,却能用极少的额外成本实现强大的时序建模能力,成为视频动作识别领域轻量且高效的基线。
TSN:时间片段采样的稀疏时序融合
核心思想
TSN 的设计哲学是长程时序结构的稀疏采样。它并不处理整个视频,而是将视频均匀分割为 K 个片段(segments),从每个片段中随机抽取一帧(或一小段 snippet)。这些帧分别经过共享的 2D CNN 提取特征,最后通过一种共识函数(如平均池化)融合所有片段的预测。
网络结构
输入视频 -> 分割为3片段 -> [帧1, 帧2, 帧3] -> 共享CNN -> [特征1, 特征2, 特征3] -> 共识(平均)-> 最终预测
- 分段采样:无论视频多长,只抽取固定数量的帧,保证计算开销恒定。
- 共享权重:所有片段使用同一个 CNN,模型完全基于 2D 架构。
- 共识函数:最简单的选择是对所有片段的类别得分取平均,也可使用最大值或加权投票。
为什么有效?
TSN 的假设是:动作可以用少数关键帧来判别。例如“投篮”只需看到起跳和出手的瞬间,而不需要连续几百帧。通过对全视频分段采样,模型被迫关注整体时序结构,而不是局部细节。这种设计极大降低了计算量,同时保留了长时序范围的能力。
TSN 的局限性
虽然 TSN 引入了多帧,但帧与帧之间在时间上是相互独立的——每帧特征提取时看不到其他帧的运动。因此 TSN 对快速运动、动态变化大的动作仍不够鲁棒。为了解决这个问题,研究者开始在 2D CNN 内部加入时序交互,TSM 便是其中的代表。
TSM:零参数零计算的时序融合
核心思想
TSM 的关键操作为通道移位(channel shift)。它的灵感来源于:时序上的信息交换不一定需要 3D 卷积,只需将相邻帧的特征图在通道维度上移动一部分即可。
例如,对于当前帧的一部分通道,我们将其替换为上一帧相同位置的通道值;而另一部分通道则替换为下一帧的通道值。这样,当前帧的特征就自然融合了前后时刻的信息,且无需任何额外参数和 FLOPs。
移位操作图解
假设特征图形状为 [T, C, H, W],T 为帧数,C 为通道数。
- 将 C 个通道三等分:
C/3向前移位,C/3保持不变,C/3向后移位。 - 向前移位:把第 t 帧的前
C/3个通道赋值为第 t-1 帧的对应通道(第 0 帧用自身填充或补零)。 - 向后移位:把第 t 帧的后
C/3个通道赋值为第 t+1 帧的对应通道(最后一帧用自身填充)。
代码示意(简化版):
def shift(x, n_segment=3, fold_div=3):
# x: [N*T, C, H, W] 经 reshape 为 [N, T, C, H, W]
nt, c, h, w = x.size()
n_batch = nt // n_segment
x = x.view(n_batch, n_segment, c, h, w)
fold = c // fold_div
out = torch.zeros_like(x)
# 向前移位: 取前一帧的前 fold 个通道
out[:, :-1, :fold] = x[:, 1:, :fold]
# 中间不变: 中间 fold 个通道保持不变
out[:, :, fold:2*fold] = x[:, :, fold:2*fold]
# 向后移位: 取后一帧的后 fold 个通道
out[:, 1:, 2*fold:] = x[:, :-1, 2*fold:]
# 将边缘帧补全(复制自身)
out[:, 0, :fold] = x[:, 0, :fold] # 第一帧向前补自身
out[:, -1, 2*fold:] = x[:, -1, 2*fold:] # 最后一帧向后补自身
out = out.view(nt, c, h, w)
return out
这个移位模块可以插入到 ResNet 的每个残差块中,替换掉部分卷积或放在卷积之前。最终网络仍然是 2D CNN,但具备了一定的时序建模能力。
TSM 的两种形式
- 在线 TSM:只能使用过去的信息(前向移位),适用于实时视频分析。
- 离线 TSM:可以同时使用过去和未来的信息(双向移位),精度更高,适合离线处理。
为什么 TSM 如此高效?
- 零参数:移位只改变特征的排列,不引入任何可学习参数。
- 零 FLOPs:移位本质是内存移动,没有浮点运算增加。
- 硬件友好:移位操作可以实现为简单的 tensor 索引,在 GPU 上几乎无额外耗时。
实验表明,TSM 在 Something-Something、Kinetics 等数据集上能以 2D CNN 的成本达到接近 3D CNN 的精度。
TSN vs TSM 对比
| 特性 | TSN | TSM |
|---|---|---|
| 时序建模位置 | 网络末端的共识函数 | 网络内部的通道移位 |
| 帧间交互 | 无,各自独立提取特征 | 有,通过通道混合实现帧间通信 |
| 额外计算量 | 几乎为零(只有多头共识) | 零,无额外 FLOPs |
| 额外参数 | 无 | 无 |
| 适用场景 | 长视频粗粒度分类 | 短时序精细动作、实时推理 |
| 典型骨架 | BN-Inception, ResNet | ResNet-50 |
何时选择 TSN? 当处理较长视频,且动作可通过少数关键帧区分时,TSN 简单有效,且分段采样的思路天然适合处理长视频。
何时选择 TSM? 当动作需要精细运动信息(如“打开”和“关闭”这种时序相关动作),或需要实时推理且希望保持 2D 网络效率时,TSM 是更优选择。
实践:使用 PyTorch 构建 TSM-ResNet
以下搭建一个最基本的 TSM-ResNet,将原始 ResNet-50 的 Bottleneck 块中加入通道移位。
1. 定义移位函数
import torch
import torch.nn as nn
def temporal_shift(x, n_segment=8, fold_div=3):
n, c, h, w = x.size()
n_batch = n // n_segment
x = x.view(n_batch, n_segment, c, h, w)
fold = c // fold_div
out = torch.zeros_like(x)
out[:, :-1, :fold] = x[:, 1:, :fold] # 向前移位
out[:, :, fold:2*fold] = x[:, :, fold:2*fold] # 不变
out[:, 1:, 2*fold:] = x[:, :-1, 2*fold:] # 向后移位
# 边界处理
out[:, 0, :fold] = x[:, 0, :fold]
out[:, -1, 2*fold:] = x[:, -1, 2*fold:]
return out.view(n, c, h, w)
2. 自定义带移位的 Bottleneck
class BottleneckTSM(nn.Module):
def __init__(self, original_bottleneck, n_segment=8):
super().__init__()
self.conv1 = original_bottleneck.conv1
self.bn1 = original_bottleneck.bn1
self.conv2 = original_bottleneck.conv2
self.bn2 = original_bottleneck.bn2
self.conv3 = original_bottleneck.conv3
self.bn3 = original_bottleneck.bn3
self.relu = original_bottleneck.relu
self.downsample = original_bottleneck.downsample
self.n_segment = n_segment
def forward(self, x):
identity = x
# 在第一个卷积之前插入移位
out = temporal_shift(x, self.n_segment)
out = self.conv1(out)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
out = self.relu(out)
out = self.conv3(out)
out = self.bn3(out)
if self.downsample is not None:
identity = self.downsample(x)
out += identity
out = self.relu(out)
return out
3. 替换 ResNet 中的模块
import torchvision.models as models
def make_tsm_resnet50(n_segment=8, num_classes=101):
model = models.resnet50(pretrained=True)
# 替换每个 layer 中的 Bottleneck
for name, module in model.named_children():
if name in ['layer1', 'layer2', 'layer3', 'layer4']:
for block in module:
block.__class__ = BottleneckTSM
block.n_segment = n_segment
# 修改全连接层
model.fc = nn.Linear(model.fc.in_features, num_classes)
return model
4. 数据加载与训练注意事项
- 帧采样:对于每个视频,均匀采样
n_segment帧,然后将这若干帧堆叠成 batch,输入形状为[batch*n_segment, C, H, W]。 - 标签:同一个视频的
n_segment帧共享同一个标签,共识函数已经隐含在训练中(通过平均损失)。 - 学习率:通常从 0.01 开始,使用余弦退火,训练约 50~100 个 epoch。
总结与展望
TSN 和 TSM 为视频动作识别提供了高效、易于部署的 2D 基线。TSN 告诉我们如何用稀疏采样捕获长程结构,TSM 则展示了零成本的帧间通信方法。二者思想也可以融合:使用 TSM 作为特征提取器,TSN 的分段采样与共识仍然可以保留。
进一步发展方向包括:
- TSM 的变体:如 TEA(Temporal Excitation and Aggregation)引入注意力加权。
- Video Transformer:TimeSformer、Video Swin Transformer 虽性能更强,但 TSN/TSM 仍然在低资源场景和工业界占有重要地位。
掌握 TSN 和 TSM 的设计哲学,是进入视频理解领域的最佳起点。