卷积运算是一种在信号处理、图像处理和神经网络等领域中广泛应用的数学运算。在图像处理和神经网络中,卷积运算可以用来提取特征、模糊图像、边缘检测等。在信号处理中,卷积运算可以用来实现滤波器等操作。 本文将介绍一维卷积运算,包括步长、零填充;宽卷积、窄卷积、等宽卷积;卷积运算与互相关运算等及其PyTorch实现。
本系列实验使用如下环境
conda create -n DL python==3.11
conda activate DL
conda install pytorch torchvision torchaudio pytorch-cuda=12.1 -c pytorch -c nvidia
Tensor(张量)是PyTorch中用于表示多维数据的主要数据结构,类似于多维数组,可以存储和操作数字数据。
Tensor(张量)的维度(Dimensions)是指张量的轴数或阶数。在PyTorch中,可以使用size()方法获取张量的维度信息,使用dim()方法获取张量的轴数。
PyTorch中的张量可以具有不同的数据类型:
【深度学习】Pytorch 系列教程(一):PyTorch数据结构:1、Tensor(张量)及其维度(Dimensions)、数据类型(Data Types)
【深度学习】Pytorch 系列教程(二):PyTorch数据结构:1、Tensor(张量): GPU加速(GPU Acceleration)
PyTorch提供了丰富的操作函数,用于对Tensor进行各种操作,如数学运算、统计计算、张量变形、索引和切片等。这些操作函数能够高效地利用GPU进行并行计算,加速模型训练过程。
【深度学习】Pytorch 系列教程(三):PyTorch数据结构:2、张量的数学运算(1):向量运算(加减乘除、数乘、内积、外积、范数、广播机制)
【深度学习】Pytorch 系列教程(四):PyTorch数据结构:2、张量的数学运算(2):矩阵运算及其数学原理(基础运算、转置、行列式、迹、伴随矩阵、逆、特征值和特征向量)
【深度学习】Pytorch 系列教程(五):PyTorch数据结构:2、张量的数学运算(3):向量范数(0、1、2、p、无穷)、矩阵范数(弗罗贝尼乌斯、列和、行和、谱范数、核范数)与谱半径详解
卷积运算是一种在信号处理、图像处理和神经网络等领域中广泛应用的数学运算。在图像处理和神经网络中,卷积运算可以用来提取特征、模糊图像、边缘检测等。在信号处理中,卷积运算可以用来实现滤波器等操作。
在离散的情况下,给定两个函数
和
,它们的卷积运算定义为:
这里的
代表卷积运算,
代表离散的变量。具体地,
和
的卷积运算
表示在
位置上的加权求和,其中每个加权项是
和
的乘积,
是自由变量。
在连续的情况下,给定两个函数
和
,它们的卷积运算定义为:
这里的
代表连续的变量。具体地,
和
的卷积运算
表示在
位置上的加权积分,其中每个加权项是
和
的乘积,
是自由变量。
在图像处理和神经网络中,卷积运算通常是在离散的情况下进行的。卷积运算的主要特点是具有移动不变性和线性性质,这使得它在信号处理和图像处理中具有广泛的应用。
基于卷积的定义,给定长度为
的输入信号
和长度为
的卷积核(或滤波器)
,一维离散卷积操作可以表示为:
其中,
表示输入信号的第
个元素,
表示卷积核的第
个元素。
计算步骤:
在深度学习中,一维卷积常被用于处理时序数据,例如语音识别、文本分类等任务。同时,一维卷积操作也是构建一维卷积神经网络(1D-CNN) 的基础,通过多层一维卷积层和池化层的堆叠,可以提取输入时序数据中的特征。
步长(stride
)是指卷积核在进行卷积操作时在输入信号上滑动的步长大小。在一维卷积中,如果步长为1,则卷积核每次只移动一个元素进行卷积操作;如果步长为2,则卷积核每次移动两个元素进行卷积操作,以此类推。调整步长可以影响输出信号的长度,以及最终卷积后的特征提取效果。较大的步长可以减少输出信号的长度,同时减少特征提取时的重叠部分,而较小的步长则可以保留更多输入信号的信息。
零填充(zero-padding
)是一种在进行卷积操作时在输入信号的两侧(或者一个侧)填充零值的技术。填充的目的是为了调整卷积操作后输出信号的长度,以及保持输入输出信号的对齐性。
卷积的结果按输出长度不同可以分为三类(输入向量的长度M, 卷积核的长度K):
在PyTorch中,使用nn.Conv1d
计算一维卷积,其期望的输入是一个三维张量,具有以下维度:[batch_size, in_channels, input_length]
。同样,卷积核也需要转换成合适的维度。一般来说,卷积操作期望输入信号和卷积核的维度满足这种规范。
batch_size
表示一次输入的样本数量,通常情况下使用1。in_channels
表示输入信号的通道数。input_length
表示输入信号的长度。因此需要使用.view(1, 1, -1)
将输入信号和卷积核转换成了这种期望的维度格式。
import torch
import torch.nn as nn
input_signal = torch.tensor([1, 1, 2, -1, 1, -3, 1], dtype=torch.float)
# conv_kernel = torch.tensor([1/3, 1/3, 1/3])
conv_kernel = torch.tensor([-1, 0, 1], dtype=torch.float)
conv_kernel = torch.flip(conv_kernel, [0])
print(f"原始维度:{conv_kernel.size()}")
print(f"原始轴数:{conv_kernel.dim()}")
# 将输入信号和卷积核转换成合适的维度
input_signal = input_signal.view(1, 1, -1)
conv_kernel = conv_kernel.view(1, 1, -1)
print(f"转换后维度:{conv_kernel.size()}")
print(f"转换后轴数:{conv_kernel.dim()}")
# conv_kernel = torch.flip(conv_kernel, [0])
# print(f"conv_kernel:{conv_kernel}")
M = input_signal.size(-1)
K = conv_kernel.size(-1)
# 创建宽、窄、等宽卷积层
conv1d_wide = nn.Conv1d(in_channels=1, out_channels=1, kernel_size=K, padding=K - 1, bias=False)
conv1d_narrow = nn.Conv1d(in_channels=1, out_channels=1, kernel_size=K, bias=False)
conv1d_same = nn.Conv1d(in_channels=1, out_channels=1, kernel_size=K, padding=int((K - 1) / 2), bias=False)
conv1d_stride2 = nn.Conv1d(in_channels=1, out_channels=1, kernel_size=K, stride=2, bias=False)
# 将卷积核加载到卷积层中
conv1d_wide.weight.data = conv_kernel
conv1d_narrow.weight.data = conv_kernel
conv1d_same.weight.data = conv_kernel
conv1d_stride2.weight.data = conv_kernel
output_wide = conv1d_wide(input_signal)
print(f"宽卷积:\n\t{output_wide}")
output_same = conv1d_same(input_signal)
print(f"等宽卷积:\n\t{output_same}")
output_narrow = conv1d_narrow(input_signal)
print(f"窄卷积:\n\t{output_narrow}")
output_stride2 = conv1d_stride2(input_signal)
print(f"步长=2窄卷积:\n\t{output_stride2}")
注意:在神经网络中使用卷积是为了进行特征抽取,卷积核是否进行翻转和其特征抽取的能力无关(互相关和卷积的区别也可以理解为图像是否进行翻转)。特别是当卷积核是可学习的参数时,卷积和互相关在能力上是等价的。因此,为了实现上(或描述上)的方便起见,我们用互相关来代替卷积。事实上,很多深度学习工具中卷积操作其实都是互相关操作,如上图PyTorch计算结果即为互相关。
翻转卷积核,计算结果与前面手动计算结果相同:
conv_kernel = torch.flip(conv_kernel, [0])
import torch
import torch.nn.functional as F
input_signal = torch.tensor([1, 1, 2, -1, 1, -3, 1], dtype=torch.float)
# conv_kernel = torch.tensor([1/3, 1/3, 1/3])
conv_kernel = torch.tensor([-1, 0, 1], dtype=torch.float)
input_signal = input_signal.view(1, 1, -1)
conv_kernel = conv_kernel.view(1, 1, -1)
K = conv_kernel.size(-1)
print("………………………………………………………………………………互相关………………………………………………………………………………")
output_wide = F.conv1d(input_signal, conv_kernel, padding=K - 1)
output_narrow = F.conv1d(input_signal, conv_kernel, )
output_same = F.conv1d(input_signal, conv_kernel, padding=int((K - 1) / 2))
print(f"宽卷积:\n\t{output_wide}")
print(f"等宽卷积:\n\t{output_same}")
print(f"窄卷积:\n\t{output_narrow}")
print("………………………………………………………………………………卷积………………………………………………………………………………")
conv_kernel = torch.flip(conv_kernel, [2])
output_wide = F.conv1d(input_signal, conv_kernel, padding=K - 1)
output_narrow = F.conv1d(input_signal, conv_kernel, )
output_same = F.conv1d(input_signal, conv_kernel, padding=int((K - 1) / 2))
print(f"宽卷积:\n\t{output_wide}")
print(f"等宽卷积:\n\t{output_same}")
print(f"窄卷积:\n\t{output_narrow}")
注意:卷积与互相关不是相反数关系,上述卷积核-1, 0, 1
特殊~
import torch
input_signal = torch.tensor([1, 1, 2, -1, 1, -3, 1], dtype=torch.float)
conv_kernel = torch.tensor([-1, 0, 1], dtype=torch.float)
# 反转卷积核~定义
conv_kernel_flipped = torch.flip(conv_kernel, [0])
K = conv_kernel.numel()
print(f"K:{K}")
# 零填充输入信号
padded_input = torch.nn.functional.pad(input_signal, (K - 1, K - 1), 'constant', 0)
print(f"输入信号:{padded_input}")
# 执行卷积运算
output1 = torch.nn.functional.conv1d(padded_input.view(1, 1, -1), conv_kernel_flipped.view(1, 1, -1))
# 输出结果
print(f"卷积运算:{output1}")
# 互相关运算
output2 = torch.nn.functional.conv1d(padded_input.view(1, 1, -1), conv_kernel.view(1, 1, -1))
# 输出结果
print(f"互相关运算:{output2}")