什么是卷积:翻转→滑动→相乘→求和,一文讲透卷积的本质副标题: 从数学公式到CNN核心,为什么"翻转"才是卷积的灵魂痛点:为什么你学不会卷积?学CNN的时候,你是不是遇到过这种情况?看论文——满屏公式,看不懂。看教程——“卷积就是滑动窗口”,好像懂了。写代码——torch.nn.Conv2d一跑,发现输出尺寸不对。更奇怪的是:为什么卷积核要"翻转"?如果只是"滑动窗口",为什么要把卷积核旋转180度?直接相关不行吗?这个问题困扰了我很久。直到我看到了信号处理的祖师爷——才发现翻转是卷积的本质,不是可选项,而是必须步骤。今天这篇文章,就是要把卷积讲透。一、卷积的数学表达:你看到的公式可能是错的1.1 标准卷积公式教科书上的公式:(f∗g)(t)=∫−∞+∞f(τ)g(t−τ)dτ(f * g)(t) = \int_{-\infty}^{+\infty} f(\tau) g(t - \tau) d\tau(f∗g)(t)=∫−∞+∞​f(τ)g(t−τ)dτ或者离散版本:(f∗g)[n]=∑m=−∞+∞f[m]g[n−m](f * g)[n] = \sum_{m=-\infty}^{+\infty} f[m] g[n - m](f∗g)[n]=m=−∞∑+∞​f[m]g[n−m]这个公式的问题是:它没有告诉你为什么要翻转。1.2 信号处理的视角:翻转是为了"对齐"让我用一个具体的例子解释:场景:你对着山谷喊"喂——“,山谷会回声"喂——喂——”。这个回声,就是声音信号和山谷冲激响应的卷积。数学解释:假设:你的声音:f(t)f(t)f(t)山谷响应:g(t)g(t)g(t)(是一个延迟和衰减)回声y(t)=f(t)∗g(t)y(t) = f(t) * g(t)y(t)=f(t)∗g(t)关键问题:g(t−τ)g(t - \tau)g(t−τ)是什么意思?ttt是当前时刻τ\tauτ是声音发出的时刻t−τt - \taut−τ是从发出到听到经过了多长时间这意味着:卷积在做的事情是"回溯"——从当前的听到的声音,反推历史上发生了什么。翻转的本质:g(t−τ)g(t - \tau)g(t−τ)意味着把g(τ)g(\tau)g(τ)翻过来,让τ=0\tau=0τ=0的地方对准当前时刻ttt。1.3 一个具体的数值例子信号:f=[1,2,3,4]f = [1, 2, 3, 4]f=[1,2,3,4]卷积核:g=[1,1,1]g = [1, 1, 1]g=[1,1,1](简单移动平均)手算卷积(输出y[n]=f[n]∗g[n]y[n] = f[n] * g[n]y[n]=f[n]∗g[n]):# 卷积核翻转180度g_flip=[1,1,1][::-1]=[1,1,1]# 对称核,不需要翻转# 计算y[0]y[0]=f[0]*g[0]+f[-1]*g[1]+f[-2]*g[2]=1*1=1# 计算y[1]y[1]=f[1]*g[0]+f[0]*g[1]+f[-1]*g[2]=2+1=3# 计算y[2]y[2]=f[2]*g[0]+f[1]*g[1]+f[0]*g[2]=3+2+1=6# ...以此类推结果:y=[1,3,6,9,7,4]y = [1, 3, 6, 9, 7, 4]y=[1,3,6,9,7,4](长度为 4+3-1=6)1.4 为什么是"翻转"而不是"直接相关"?关键洞察:卷积满足交换律f∗g=g∗ff * g = g * ff∗g=g∗f,但互相关不满足。# 卷积(满足交换律)conv=scipy.signal.convolve(f,g)# 互相关(不满足交换律)corr=scipy.signal.correlate(f,g)为什么这很重要?物理上,f∗gf * gf∗g和g∗fg * fg∗f代表同样的物理过程(输入输出互换位置,系统特性不变)。但在CNN中,我们通常把卷积核固定,输入图像变化。如果用互相关,就丢失了这个对称性。二、卷积核到底在"卷"什么?2.1 图像卷积的操作流程四步法:翻转 → 滑动 → 相乘 → 求和让我用GIF的思路解释:Step 1:翻转(Flip)把卷积核旋转180度[a, b, c] → [c, b, a](水平翻转)[d, e, f] → [f, e, d](垂直翻转)组合起来:[[a,b,c],[d,e,f],[g,h,i]] → [[i,h,g],[f,e,d],[c,b,a]]Step 2:滑动(Slide)从图像左上角开始每次移动一格(步长可调)Step 3:相乘(Multiply)重叠的元素逐个相乘Step 4:求和(Sum)把所有乘积加起来得到输出矩阵的一个元素2.2 PyTorch代码实现importtorchimporttorch.nn.functionalasFdefmy_conv2d(image,kernel,stride=1,padding=0):""" 手写卷积实现(简化版) image: [B, C, H, W] kernel: [K, C, KH, KW] """B,C,H,W=image.shape K,_,KH,KW=kernel.shape# 计算输出尺寸OH=(H+2*padding-KH)//stride+1OW=(W+2*padding-KW)//stride+1# 填充ifpadding0:image=F.pad(image,(padding,)*4)# 翻转核(关键步骤!)kernel=torch.flip(kernel,dims=[2,3])# 180度翻转# 滑窗相乘求和output=F.conv2d