目录
第 3 步:窗口继续右移,直到一行结束,再下移 1 行重复计算
什么是二维互相关?
二维互相关是卷积神经网络(CNN)中提取局部特征的核心操作:通过一个卷积核(Kernel,也称过滤器) 在输入矩阵上滑动,计算每个位置的局部加权和,生成输出特征图。
简单来说,它的作用是:用卷积核 “扫描” 输入矩阵,捕捉局部区域的特征(如边缘、纹理)。
一、直观理解:像 “盖章” 一样扫描输入
想象你有一张图片(输入矩阵)和一个小印章(卷积核),互相关运算就是:
- 用 “印章” 在图片上从左到右、从上到下滑动(每次移动 1 步,步长可调整)。
- 每滑动到一个位置,就计算 “印章” 与图片对应区域的加权和,结果作为输出矩阵中对应位置的值。
整个过程类似用放大镜观察图片的局部细节,不同的 “印章”(卷积核)能捕捉不同的特征。
二、数学定义
三、步骤拆解(带实例)
用一个具体例子演示计算过程,直观理解每一步:
1. 输入与卷积核
- 输入矩阵 X(5×5,模拟一张灰度图):
[[1, 2, 3, 4, 5], [6, 7, 8, 9, 10], [11,12,13,14,15], [16,17,18,19,20], [21,22,23,24,25]]
- 卷积核 K(3×3,模拟 “垂直边缘检测器”):
[[1, 0, -1], [1, 0, -1], [1, 0, -1]]
2. 滑动窗口计算(步长 = 1)
卷积核像 “窗口” 一样在输入上滑动,每移动一次计算一个输出值:
第 1 步:窗口左上角对齐 \(X[0,0]\)
- 截取输入局部区域(3×3):
[[1, 2, 3], [6, 7, 8], [11,12,13]]
- 与卷积核对应元素相乘后求和:
- 输出
第 2 步:窗口右移 1 格(对齐
)
- 截取输入局部区域:
[[2, 3, 4], [7, 8, 9], [12,13,14]]
- 计算得:\(Y[0,1] = -6\)
第 3 步:窗口继续右移,直到一行结束,再下移 1 行重复计算
最终得到 3×3 的输出矩阵 Y:
[[-6, -6, -6], [-18, -18, -18], [-30, -30, -30]]
四、核心作用:提取局部特征
卷积核的参数决定了它能提取的特征类型:
- 例子中的卷积核 K 是 “垂直边缘检测器”:它对左侧元素赋正权重,右侧赋负权重,中间为 0。当输入中存在垂直边缘(左右像素差异大)时,输出值的绝对值会很大;而在平坦区域(左右像素差异小),输出值接近 0。
- 训练过程中,卷积核的参数会自动学习(通过反向传播优化),以适应任务需求(如识别猫的 “耳朵特征”、车的 “轮子特征”)。
五、与卷积运算的区别
很多人会混淆 “互相关” 和 “卷积”,两者的唯一区别是:
- 互相关:直接用原始卷积核滑动计算。
- 卷积:先将卷积核旋转 180°,再滑动计算。
在深度学习中,两者效果几乎相同(因为卷积核参数会被训练优化),因此实际应用中常用互相关代替卷积(计算更简单)。
六、关键参数
- 卷积核大小(h, w):常见 3×3、5×5,越大能捕捉越复杂的特征,但计算量也越大。
- 步长(stride):每次滑动的距离(默认 1),步长越大,输出尺寸越小。
- 填充(padding):在输入边缘补 0,用于保持输出尺寸与输入一致(如 “same padding”)。
七、总结
二维互相关是 CNN 的 “眼睛”,通过滑动卷积核计算局部加权和,实现对输入数据的局部特征提取。它的优势在于:
- 参数共享:一个卷积核在输入的所有位置复用,大幅减少参数数量。
- 局部连接:每个输出只依赖输入的局部区域,符合 “局部特征决定整体” 的视觉规律(如识别物体先看边缘、纹理)。
互相关运算为啥放在前向传播中?
1. 前向传播的本质是计算数据流
- 前向传播是神经网络从输入到输出的计算过程,负责将输入数据通过层层变换转换为预测结果。
- 互相关运算是卷积层的核心操作,通过滑动卷积核对输入数据进行特征提取。将其放在前向传播中,正是为了实现这一特征提取的过程。
2. 互相关运算是卷积层的数学实现
- 在深度学习中,卷积层的本质是通过互相关运算来实现的。虽然数学上的卷积需要对卷积核旋转 180°,但在实际应用中:
- 由于卷积核是通过训练学习得到的参数,旋转与否不影响模型的表达能力。
- 因此,直接使用互相关运算代替卷积,简化了计算过程。
3. 前向传播与反向传播的分工
- 前向传播:负责计算预测值,使用当前参数(如卷积核权重、偏置)对输入进行变换。
- 反向传播:负责计算梯度,根据预测误差调整参数。
将互相关运算放在前向传播中,而将梯度计算(基于链式法则)放在反向传播中,实现了计算过程的清晰分工。
完整代码
"""
文件名: 6.1
作者: 墨尘
日期: 2025/7/13
项目名: dl_env
备注:
"""
import torch
from torch import nn
from d2l import torch as d2l
def corr2d(X, K): # @save
"""计算二维互相关运算"""
# 获取卷积核K的高度h和宽度w
h, w = K.shape
# 计算输出特征图的尺寸
Y_height = X.shape[0] - h + 1
Y_width = X.shape[1] - w + 1
# 初始化输出特征图Y为全零张量
Y = torch.zeros((Y_height, Y_width)) #2*2
# 遍历输出特征图的每个位置
for i in range(Y_height):
for j in range(Y_width):
# 提取当前窗口区域
window = X[i:i + h, j:j + w]
# 窗口区域与卷积核逐元素相乘后求和
Y[i, j] = (window * K).sum()
return Y
# 卷积层
class Conv2D(nn.Module):
def __init__(self, kernel_size):
super().__init__()
# 随机初始化卷积核权重
self.weight = nn.Parameter(torch.rand(kernel_size))
# 初始化偏置为0
self.bias = nn.Parameter(torch.zeros(1))
def forward(self, x):
# 调用corr2d函数进行卷积运算,然后加上偏置 前向传播
return corr2d(x, self.weight) + self.bias
if __name__ == '__main__':
X = torch.tensor([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]])
K = torch.tensor([[0.0, 1.0], [2.0, 3.0]])
print(corr2d(X, K))
# 图像中目标的边缘检测
"""构造一个6*8像素的黑白图像。中间四列为黑色(0),其余像素为白色(1)"""
X = torch.ones((6, 8))
X[:, 2:6] = 0
print(X)
"""构造一个高度为1、宽度为2的卷积核K"""
K = torch.tensor([[1.0, -1.0]]) #两个元素值
Y = corr2d(X, K)
print(Y)
# corr2d(X.t(), K)
# -------------------------------------------------------------------------
# 一个卷积核的训练过程
# -------------------------------------------------------------------------
# 创建中心化层实例
# 构造一个二维卷积层,它具有1个输出通道和形状为(1,2)的卷积核
conv2d = nn.Conv2d(1, 1, kernel_size=(1, 2), bias=False)
# 这个二维卷积层使用四维输入和输出格式(批量大小、通道、高度、宽度),
# 其中批量大小和通道数都为1
"""
输入数据 X 的形状:(1, 1, 6, 8),表示 1 个样本,1 个通道,高度 6,宽度 8
目标输出 Y 的形状:(1, 1, 6, 7),宽度 7 是因为 1x2 的卷积核在 8 宽度上滑动产生 7 个输出
学习率:控制参数更新的步长"""
X = X.reshape((1, 1, 6, 8))
Y = Y.reshape((1, 1, 6, 7))
lr = 3e-2 # 学习率
# 前向传播:Y_hat = conv2d (X),计算模型预测值
# 损失计算:l = (Y_hat - Y) ** 2,使用均方误差
# 梯度清零:conv2d.zero_grad (),清除之前的梯度积累
# 反向传播:l.sum ().backward (),计算损失对参数的梯度
# 参数更新:手动实现梯度下降,更新卷积核权重
# 打印损失:每 2 个迭代打印一次损失值
for i in range(10):
Y_hat = conv2d(X)
l = (Y_hat - Y) ** 2
conv2d.zero_grad()
l.sum().backward()
# 迭代卷积核
conv2d.weight.data[:] -= lr * conv2d.weight.grad
if (i + 1) % 2 == 0:
print(f'epoch {i + 1}, loss {l.sum():.3f}')