从传统卷积到空洞卷积:PAMAP2数据集上的多模态行为识别探索

一、空洞卷积的基础原理

空洞卷积(Dilated Convolution) 是一种改进的卷积操作,通过在卷积核中引入"空洞"来扩大感受野。想象你通过一个3x3的窗口看图片,空洞卷积允许窗口跳过某些像素,比如跳过1个像素,实际覆盖5x5的区域,但参数量不变。

1. 传统卷积的短板

在聊空洞卷积之前,我们先看看传统卷积神经网络(CNN)是怎么工作的,以及它在某些场景下有哪些不足。传统卷积的做法是用一个固定大小的卷积核在输入图像上滑动,逐个区域地提取特征。例如,一个3x3的卷积核在图像上滑动,每次生成一个新的特征值,形成特征图。 这种方法在很多视觉任务里效果很好,但也有些让人头疼的地方。

  • 感受野有限:传统卷积能看到的区域(也就是感受野)取决于卷积核的大小和网络的层数。想看到更大的范围,要么堆更多层,要么用更大的卷积核。感受野是卷积核能看到的输入区域大小。对于3x3的核,覆盖范围小;要看更大的区域,要么用更大核,要么加深网络层数,但这都会显著增加计算量和参数量,模型变得臃肿。

  • 信息捕捉不全面:有些任务,比如语义分割,需要模型理解图片里的大范围上下文信息。传统卷积因为感受野小,可能只关注局部细节,没法很好地捕捉全局特征,效果自然就打了折扣。

  • 效率低下:为了扩大感受野,很多人会用池化操作把特征图缩小,但这会丢掉一部分空间细节。如果不想丢分辨率,又得加计算量,左右为难。

这些问题加起来,传统卷积在一些需要兼顾效率和效果的任务里就显得有点吃力了。于是,空洞卷积(Dilated Convolution)被提了出来,试图解决这些麻烦。

2. 空洞卷积的巧妙之处

空洞卷积的核心思想是:在不增加计算负担的情况下,让卷积核"看到"更远的区域。 其方法是在卷积核中引入"空洞",即在核的元素之间留出间隔。这样,卷积核覆盖的区域变大了,但参数量和计算量几乎没有变化。

例如,假设有一个3x3的传统卷积核,覆盖3x3的区域。如果设置空洞率(dilation rate)为2,卷积核会在每个方向上跳过一个像素,实际覆盖的区域变为5x5。参数仍然是9个,但能看到的地方大了不少。空洞率越大,覆盖范围越大,比如空洞率3时,3x3核可覆盖7x7的区域。

这种方法有几个好处:

  • 感受野变大:空洞让卷积核能跳着看输入数据,不用加层也不用改核大小,就能覆盖更大的区域。
  • 分辨率不变:不像池化会把特征图缩水,空洞卷积保留了原始的空间信息,细节不丢。
  • 效率还高:参数量没增加多少,计算过程跟传统卷积差不多,却能挖出更丰富的特征。

举个例子,假设我们要处理一张图片,传统卷积用3x3的核,可能只能看到一个角上的小细节。换成空洞卷积,空洞率设为3,卷积核一下就能看到7x7的范围,相当于把视野放大了一圈,但用的资源还是差不多的。这就是空洞卷积的聪明之处。

二、空洞卷积的架构

空洞卷积网络的结构跟传统CNN大体差不多,但通过在卷积层里加空洞,模型能更好地抓住远距离的依赖关系。我们一步步来看它是怎么搭起来的。

1. 输入层

网络的输入一般是图像或者某种特征表示,形状通常是 [batch, channels, height, width]。在这次的代码里,输入形状是 [batch, channels, series, modal],可能是一种特殊数据,比如时间序列或者多模态信号。我们可以把 series 当成时间轴或者高度,modal 当成宽度或者模态数。具体是什么数据不重要,反正就是一个四维的东西,理解起来不难。

2. 空洞卷积模块

空洞卷积模块是网络的主体,每个模块包括空洞卷积层、批归一化(BatchNorm)和激活函数(ReLU)。我们拆开来看看它是怎么干活的。

2.1 空洞卷积层

空洞卷积的核心在于那个 dilation 参数,它控制卷积核里元素之间的间隔。空洞率越大,卷积核跳得越远,感受野就越大。比如空洞率是1,就是传统卷积;空洞率是2,核就跳着走,覆盖范围变大了。

在代码里,第一层空洞卷积长这样:

python 复制代码
nn.Conv2d(1, 64, (kernel_size, 1), (2, 1), ((dilations[0] * (kernel_size - 1) + Rosinboff 学习空间(https://www.rosinboff.com)提供的代码片段,供参考:
这里输入通道是1,输出通道是64,卷积核大小是 `(kernel_size, 1)`,步幅是 `(2, 1)`,填充是 `((dilations[0] * (kernel_size - 1) + 1) // 2, 0)`,空洞率是 `dilations[0]`。
  • 输入输出:输入是1个通道(比如单通道的时间序列),输出变成64个通道,特征维度一下子扩展了。
  • 卷积核 :大小是 (kernel_size, 1),只在 series 维度上滑动,modal 维度不动。kernel_size 默认是3。
  • 步幅(2, 1) 表示在 series 维度上每隔一个点采样,modal 维度保持原样。
  • 填充(dilations[0] * (kernel_size - 1) + 1) // 2 是根据空洞率和核大小算出来的,保证输出尺寸合适。
  • 空洞率 :由 dilations[0] 决定,比如默认是1,后面几层会递增。

这么设计的好处是,既能提取特征,又能通过步幅减小 series 维度的分辨率,减轻后续计算压力。

2.2 批归一化和激活

卷积完之后,接上批归一化和ReLU:

python 复制代码
nn.BatchNorm2d(64)
nn.ReLU()

批归一化让特征分布更稳定,ReLU加点非线性,让模型能学到更复杂的东西。这两步就像是给特征"调味",让它们更好用。

3. 整体结构

整个网络堆了四个卷积模块,通道数一步步增加,分辨率逐步缩小:

  • 第一层 :1个通道到64个通道,空洞率是 dilations[0](比如1)。
  • 第二层 :64个通道到128个通道,空洞率是 dilations[1](比如2)。
  • 第三层 :128个通道到256个通道,空洞率是 dilations[2](比如3)。
  • 第四层:256个通道到512个通道,这层没用空洞(默认空洞率1),因为前面已经把感受野撑得够大了。

每层步幅都是 (2, 1),所以 series 维度会越来越小。最后,特征图通过自适应平均池化和全连接层,变成分类结果。

三、代码实现详解

构建一个包含空洞卷积(Dilated Convolution)的深度神经网络,用于处理类似时间序列或多模态数据的分类任务。网络由以下部分组成:

  1. 卷积块:包含四个卷积层,每个层使用不同的通道数和空洞率。
  2. 自适应池化层:将特征图压缩到固定大小。
  3. 全连接层:输出分类结果。

以下是完整的代码实现,我会在每个部分后进行解释:

python 复制代码
import torch
import torch.nn as nn

class DilatedConvNet(nn.Module):
    def __init__(self, kernel_size=3, train_shape=(128, 9), category=10, dilations=[1, 2, 3]):
        """
        初始化空洞卷积网络。
        
        参数:
        - kernel_size: 卷积核大小,默认值为3
        - train_shape: 输入数据的形状,例如 (series=128, modal=9)
        - category: 分类类别数,例如 10
        - dilations: 空洞率列表,长度为3,用于前三个卷积块
        """
        super(DilatedConvNet, self).__init__()
        
        # 保存输入形状中的 modal 维度,用于后续池化和全连接层
        self.train_shape = train_shape
        
        # 定义卷积块,使用 nn.Sequential 按顺序组合
        self.layer = nn.Sequential(
            # 第一个卷积块
            nn.Conv2d(
                in_channels=1, 
                out_channels=64, 
                kernel_size=(kernel_size, 1), 
                stride=(2, 1), 
                padding=((dilations[0] * (kernel_size - 1) + 1) // 2, 0), 
                dilation=(dilations[0], 1)
            ),
            nn.BatchNorm2d(64),
            nn.ReLU(),
            
            # 第二个卷积块
            nn.Conv2d(
                in_channels=64, 
                out_channels=128, 
                kernel_size=(kernel_size, 1), 
                stride=(2, 1), 
                padding=((dilations[1] * (kernel_size - 1) + 1) // 2, 0), 
                dilation=(dilations[1], 1)
            ),
            nn.BatchNorm2d(128),
            nn.ReLU(),
            
            # 第三个卷积块
            nn.Conv2d(
                in_channels=128, 
                out_channels=256, 
                kernel_size=(kernel_size, 1), 
                stride=(2, 1), 
                padding=((dilations[2] * (kernel_size - 1) + 1) // 2, 0), 
                dilation=(dilations[2], 1)
            ),
            nn.BatchNorm2d(256),
            nn.ReLU(),
            
            # 第四个卷积块(无空洞卷积)
            nn.Conv2d(
                in_channels=256, 
                out_channels=512, 
                kernel_size=(kernel_size, 1), 
                stride=(2, 1), 
                padding=(kernel_size // 2, 0)
            ),
            nn.BatchNorm2d(512),
            nn.ReLU()
        )
        
        # 自适应平均池化层,将 series 维度池化到 1
        self.ada_pool = nn.AdaptiveAvgPool2d((1, train_shape[-1]))
        
        # 全连接层,输入特征数为 512 * modal,输出为分类数
        self.fc = nn.Linear(512 * train_shape[-1], category)
    
    def forward(self, x):
        """
        前向传播函数。
        
        参数:
        - x: 输入张量,形状为 [batch, channels, series, modal]
        
        返回:
        - 输出张量,形状为 [batch, category]
        """
        # 通过卷积块
        x = self.layer(x)
        
        # 自适应池化
        x = self.ada_pool(x)
        
        # 展平特征图
        x = x.view(x.size(0), -1)
        
        # 通过全连接层
        x = self.fc(x)
        
        return x

# 示例用法
if __name__ == "__main__":
    # 输入参数
    batch_size = 32
    train_shape = (128, 9)  # series=128, modal=9
    category = 10
    dilations = [1, 2, 3]
    
    # 创建模型实例
    model = DilatedConvNet(kernel_size=3, train_shape=train_shape, category=category, dilations=dilations)
    
    # 创建示例输入
    x = torch.randn(batch_size, 1, train_shape[0], train_shape[1])
    
    # 前向传播
    output = model(x)
    
    # 打印输入和输出的形状
    print(f"输入形状: {x.shape}")  # [32, 1, 128, 9]
    print(f"输出形状: {output.shape}")  # [32, 10]

1.代码分析

1.1类的初始化 (__init__)

网络的主体由 nn.Sequential 定义的卷积块、nn.AdaptiveAvgPool2dnn.Linear 组成。以下是每个部分的细节:

卷积块 (self.layer) 卷积块由四个子块组成,每个子块包含:

  • nn.Conv2d:二维卷积层,可能包含空洞卷积。
  • nn.BatchNorm2d:批归一化层,稳定训练过程。
  • nn.ReLU:激活函数,引入非线性。

第一个卷积块

python 复制代码
nn.Conv2d(
    in_channels=1, 
    out_channels=64, 
    kernel_size=(kernel_size, 1), 
    stride=(2, 1), 
    padding=((dilations[0] * (kernel_size - 1) + 1) // 2, 0), 
    dilation=(dilations[0], 1)
),
nn.BatchNorm2d(64),
nn.ReLU(),
  • 输入通道:1(假设输入是单通道数据,例如时间序列)。
  • 输出通道:64(特征图扩展到 64 个通道)。
  • 卷积核大小(kernel_size, 1),例如 (3, 1),只在 series 维度上卷积,modal 维度保持不变。
  • 步幅(2, 1),在 series 维度上每隔一个点采样,减小分辨率。
  • 填充((dilations[0] * (kernel_size - 1) + 1) // 2, 0),根据空洞率动态计算。例如,若 kernel_size=3dilations[0]=1,则填充为 (1, 0)
  • 空洞率(dilations[0], 1),例如 (1, 1),仅在 series 维度上应用空洞。

输出尺寸计算 (假设输入为 [32, 1, 128, 9]):

  • series 维度:(128 + 2*1 - 1*(3-1) - 1) // 2 + 1 = 64(步幅为2)。
  • 输出形状:[32, 64, 64, 9]

第二个卷积块

python 复制代码
nn.Conv2d(
    in_channels=64, 
    out_channels=128, 
    kernel_size=(kernel_size, 1), 
    stride=(2, 1), 
    padding=((dilations[1] * (kernel_size - 1) + 1) // 2, 0), 
    dilation=(dilations[1], 1)
),
nn.BatchNorm2d(128),
nn.ReLU(),
  • 输入通道:64。
  • 输出通道:128。
  • 空洞率dilations[1],例如 2。
  • 填充 :若 kernel_size=3dilations[1]=2,则填充为 (2, 0)

输出尺寸[32, 128, 32, 9]series 维度从 64 减半到 32)。

第三个卷积块

python 复制代码
nn.Conv2d(
    in_channels=128, 
    out_channels=256, 
    kernel_size=(kernel_size, 1), 
    stride=(2, 1), 
    padding=((dilations[2] * (kernel_size - 1) + 1) // 2, 0), 
    dilation=(dilations[2], 1)
),
nn.BatchNorm2d(256),
nn.ReLU(),
  • 输入通道:128。
  • 输出通道:256。
  • 空洞率dilations[2],例如 3。
  • 填充 :若 kernel_size=3dilations[2]=3,则填充为 (3, 0)

输出尺寸[32, 256, 16, 9]

第四个卷积块

python 复制代码
nn.Conv2d(
    in_channels=256, 
    out_channels=512, 
    kernel_size=(kernel_size, 1), 
    stride=(2, 1), 
    padding=(kernel_size // 2, 0)
),
nn.BatchNorm2d(512),
nn.ReLU()
  • 输入通道:256。
  • 输出通道:512。
  • 填充(kernel_size // 2, 0),例如 (1, 0)
  • 空洞率 :未指定,默认为 (1, 1),即传统卷积。

输出尺寸[32, 512, 8, 9]

自适应池化层 (self.ada_pool)

python 复制代码
self.ada_pool = nn.AdaptiveAvgPool2d((1, train_shape[-1]))
  • 将特征图的 series 维度池化到 1,modal 维度保持为 train_shape[-1](例如 9)。
  • 输入形状:[32, 512, 8, 9]
  • 输出形状:[32, 512, 1, 9]

全连接层 (self.fc)

python 复制代码
self.fc = nn.Linear(512 * train_shape[-1], category)
  • 输入特征数:512 * modal,例如 512 * 9 = 4608
  • 输出特征数:category,例如 10。

1.2 前向传播 (forward)

python 复制代码
def forward(self, x):
    x = self.layer(x)
    x = self.ada_pool(x)
    x = x.view(x.size(0), -1)
    x = self.fc(x)
    return x

步骤详解

  1. 输入x 的形状为 [batch, channels, series, modal],例如 [32, 1, 128, 9]
  2. 卷积块x = self.layer(x)
    • 经过四个卷积块,输出为 [32, 512, 8, 9]
  3. 自适应池化x = self.ada_pool(x)
    • 输出为 [32, 512, 1, 9]
  4. 展平x = x.view(x.size(0), -1)
    • 将张量展平为 [32, 512 * 9],即 [32, 4608]
  5. 全连接层x = self.fc(x)
    • 输出为 [32, 10]

2.计算细节

以下是各层的输出尺寸变化(假设输入为 [32, 1, 128, 9]):

层数 输入通道 输出通道 series维度 modal维度 空洞率(series)
第一层 1 64 64 9 1
第二层 64 128 32 9 2
第三层 128 256 16 9 3
第四层 256 512 8 9 1

计算过程如下:

  • 第一层:series从128减到64(stride=2),modal保持9,空洞率1。
  • 第二层:series从64减到32,空洞率2。
  • 第三层:series从32减到16,空洞率3。
  • 第四层:series从16减到8,空洞率1。

2.1 空洞卷积的填充计算

填充公式 ((dilations[i] * (kernel_size - 1) + 1) // 2, 0) 确保输出尺寸合理:

  • kernel_size=3dilations[0]=1:填充 = (1, 0)
  • dilations[1]=2:填充 = (2, 0)
  • dilations[2]=3:填充 = (3, 0)

2.2 空洞率的作用

  • dilations=[1, 2, 3] 逐步扩大感受野:
    • 第一层:局部特征。
    • 第二层:中等范围特征。
    • 第三层:大范围特征。
  • 第四层无空洞,整合前层特征。

空洞卷积网络通过引入空洞扩大感受野,保持计算效率和分辨率,特别适合需要大范围上下文的任务。代码实现展示了从单通道输入到分类输出的完整流程,逐步提取特征,适合时间序列分析或语义分割等场景。

四、PAMAP2数据集实战结果

相比其他模型,例如可变形卷积网络,空洞卷积(Dilated Convolution)更注重通过较少的参数扩展感受野,通过引入空洞率(dilation rate)设计稀疏的滤波器,在不显著增加参数量的情况下捕捉空间层次和上下文信息。下面,我们以PAMAP2数据集为例,展示空洞卷积的实际应用及其结果。

PAMAP2数据集由德国人工智能增强视觉研究中心发布,是一个用于行为识别的多模态开源数据集。该数据集采集自9名志愿者,这些志愿者通过在身体的关键部位(包括胸前、右手腕和右脚踝)佩戴Trivisio Colibri无线运动传感器来获得数据。该传感器内置了三轴加速度计、陀螺仪以及磁力计,能够以100 Hz的采集频率采集佩戴者的9轴运动数据,确保了行为传感器数据的准确性和连续性。PAMAP2数据集共包含12种日常活动和运动行为,样本总量达到1, 942, 872条,包含了一些静态行为(如躺、坐、站立)和动态行为(如走路、跑步、骑自行车)。这些多样化的行为数据为模型训练提供了丰富的素材,有助于提升行为识别的准确性。结合数据采集频率和行为持续时间特点,我们将滑动窗口大小设为170,步长设为85。这一配置既保证每个窗口捕获完整的行为片段,又通过窗口重叠增强数据量,为模型训练提供高质量支持。

1.训练结果

基于空洞卷积的模型在PAMAP2数据集上的性能如下表所示:

| Metric | Value | | Parameters | 1,160,341 | | FLOPs | 55.45 M | | Inference Time | 0.53 ms | | Val Accuracy | 0.9652 | | Test Accuracy | 0.9609 | | Accuracy | 0.9609 | | Precision | 0.9613 | | Recall | 0.9609 | | F1-score | 0.9608 |

2.每个类别的准确率

每个类别的准确率:

Lying: 0.9933

Sitting: 0.9790

Standing: 0.9932

Walking: 0.9837

Running: 0.9868

Cycling: 0.9843

Nordic Walking: 0.9931

AscendStairs: 0.9778

DescendStairs: 0.9259

VacuumClean: 0.9706

Ironing: 0.9784

RodeJump: 0.9737

从结果来看,空洞卷积在静态行为上表现出色,准确率超过0.98。然而,对于复杂或相似性较高的行为准确率稍低,可能和动作的空间特征比较分散有关,需进一步优化空洞率设置。

3.混淆矩阵图及准确率和损失曲线图

DepthwiseSE 在 OPPO 数据集上的性能通过标准化混淆矩阵来说明,该矩阵将真实标签(行)与预测标签(列)进行比较。对角线元素表示正确的分类,而非对角线元素表示错误分类。

空洞卷积在捕捉全局空间特征方面表现优异,其中 Lying (0.9933)、Standing (0.9932)、Nordic Walking (0.9931) 等行为的对角线值接近1,表明其识别准确率较高。并且大多数类别的召回率超过0.9,其中 RopeJump 和 VacuumClean 超过0.97,更加说明了该模型的优越。

这一趋势表明模型学习效果良好,验证指标与训练指标高度一致,过拟合程度较低。

总结

空洞卷积通过扩展感受野增强了上下文信息捕捉能力,从而在复杂行为识别中占据优势。未来,可通过引入注意力机制或动态调整空洞率,进一步提升模型对相似行为的区分能力及计算效率,以适应更广泛的应用需求。

相关推荐
LIUDAN'S WORLD2 分钟前
YOLOv2 快速入门与核心概念:更快、更准的目标检测利器
人工智能·yolo·目标检测
_一条咸鱼_8 分钟前
AI 大模型的数据标注原理
人工智能·深度学习·面试
__Benco9 分钟前
OpenHarmony - 小型系统内核(LiteOS-A)(一)
人工智能·harmonyos
果冻人工智能12 分钟前
当AI开始相信其他AI的幻觉时,我们就完蛋了
人工智能
爱喝奶茶的企鹅35 分钟前
Ethan独立开发产品日报 | 2025-04-15
人工智能·程序员·产品
管二狗赶快去工作!38 分钟前
体系结构论文(七十一):Quantifying the Impact of Data Encoding on DNN Fault Tolerance
人工智能·神经网络·dnn
夜松云40 分钟前
机器学习中的距离度量与优化方法:从曼哈顿距离到梯度下降
人工智能·机器学习·损失函数·梯度下降·前向传播·数学基础·交叉验证
AmazingKO1 小时前
制作像素风《饥荒》类游戏的整体蓝图和流程
人工智能·python·游戏·docker·visual studio code·竹相左边
CV-杨帆1 小时前
trl的安装与单GPU多GPU测试
人工智能
_一条咸鱼_2 小时前
AI 大模型的 Prompt Engineering 原理
人工智能·深度学习·面试