1.一文读懂CNN(卷积神经网络)

深度解析卷积神经网络原理

一、图像在计算机中的本质

图像在计算机中以像素(Pixel)矩阵的形式存储,每个像素代表图像中一个最小的颜色单元,其核心是“数值化的颜色信息”。具体表现为:

  • 灰度图:单通道矩阵,每个像素值用0-255的整数表示(0为纯黑,255为纯白),矩阵尺寸即图像分辨率(如28×28表示高28像素、宽28像素)。
    在这里插入图片描述

  • 彩色图:多通道矩阵,常见为RGB三通道(红、绿、蓝),每个通道均为独立的灰度矩阵,单个像素的颜色由三个通道的像素值组合决定(如RGB(255,0,0)为纯红),存储维度为“高度×宽度×通道数”(如224×224×3)。
    在这里插入图片描述

  • 本质:图像的本质是“离散化的数值矩阵”,计算机处理图像本质是对这些数值矩阵进行运算。

二、全连接神经网络存在的问题——为什么解决不了图像问题?

全连接神经网络(FCN)的核心是“每个输入节点与所有输出节点完全连接”,这种结构在处理图像时存在三大致命问题:

  1. 参数爆炸问题:图像分辨率稍高时,输入维度会急剧增加,导致连接权重参数数量呈指数级增长。例如:输入为224×224×3的彩色图,展开后输入节点数为224×224×3=150528,若第一个全连接层设1000个输出节点,仅该层权重参数就达150528×1000≈1.5×10⁸个,远超模型承载能力,且易过拟合。

  2. 丢失空间信息:图像的核心特征(如边缘、纹理、形状)依赖像素的空间位置关系(如“上方像素与下方像素的灰度差构成边缘”),但全连接网络需将二维像素矩阵展开为一维向量输入,彻底破坏了像素间的空间关联性,无法捕捉图像的空间特征。

  3. 缺乏平移不变性:若图像中目标的位置发生微小偏移(如猫从图像左侧移到右侧),展开后的一维向量中对应像素的位置会大幅变化,全连接网络会将其识别为完全不同的输入,导致模型对目标位置敏感,泛化能力极差。

三、卷积运算过程

卷积运算(Convolution)是CNN的核心操作,其核心是“用卷积核(Filter/Kernel)对图像的局部区域进行滑动计算,提取局部特征”,具体过程如下:

在这里插入图片描述

  1. 准备输入与卷积核:输入为二维特征图(如灰度图)或多通道特征图;卷积核为小型二维矩阵(如3×3、5×5),其尺寸远小于输入,每个元素为可学习的权重参数。

  2. 滑动卷积核:将卷积核的左上角与输入特征图的左上角对齐,以固定步长(见“步幅”)在输入特征图上沿水平和垂直方向滑动,每次滑动后停留的位置称为“感受野”(卷积核覆盖的局部区域)。

  3. 元素相乘求和:在每个感受野内,将卷积核的每个元素与输入对应位置的像素值相乘,再将所有乘积结果求和,得到输出特征图上对应位置的一个像素值。

  4. 激活函数处理:将求和结果输入激活函数(如ReLU),引入非线性,增强模型对复杂特征的表达能力,最终得到该位置的输出像素。

关键特性:局部连接(仅连接局部区域像素)、权值共享(同一卷积核在整个输入上重复使用,减少参数)。

单通道卷积运算代码实现


import numpy as np

def single_channel_convolution(input_feature, kernel, stride=1, padding=0):
    """
    实现单通道卷积运算
    参数:
        input_feature: 输入单通道特征图,形状为(H, W),H为高度,W为宽度
        kernel: 卷积核,形状为(K, K),K为卷积核尺寸
        stride: 步幅,默认1
        padding: 填充量,默认0(Valid Padding)
    返回:
        output_feature: 输出单通道特征图
    """
    # 1. 对输入特征图进行零填充
    # pad_width为(上下填充, 左右填充),(padding, padding)表示上下左右各填充padding层
    padded_input = np.pad(input_feature, ((padding, padding), (padding, padding)), mode='constant', constant_values=0)
    # 2. 获取输入和卷积核的尺寸
    H_in, W_in = padded_input.shape  # 填充后的输入尺寸
    K, _ = kernel.shape              # 卷积核尺寸(假设为正方形)
    # 3. 计算输出特征图的尺寸(使用公式)
    H_out = (H_in - K) // stride + 1
    W_out = (W_in - K) // stride + 1
    # 4. 初始化输出特征图
    output_feature = np.zeros((H_out, W_out))
    
    # 5. 滑动卷积核进行卷积运算
    for i in range(H_out):  # 沿高度方向滑动
        for j in range(W_out):  # 沿宽度方向滑动
            # 确定当前感受野的区域(卷积核覆盖的输入部分)
            receptive_field = padded_input[i*stride : i*stride+K, j*stride : j*stride+K]
            # 元素相乘并求和(卷积核心计算)
            conv_sum = np.sum(receptive_field * kernel)
            # 激活函数(ReLU)处理,引入非线性
            output_feature[i, j] = max(0, conv_sum)
    
    return output_feature

# 测试代码
if __name__ == "__main__":
    # 模拟输入:5×5的单通道灰度图(像素值0-255)
    input_image = np.array([[10, 20, 30, 20, 10],
                           [20, 40, 60, 40, 20],
                           [30, 60, 90, 60, 30],
                           [20, 40, 60, 40, 20],
                           [10, 20, 30, 20, 10]])
    # 3×3卷积核(边缘检测核,可提取水平边缘)
    edge_kernel = np.array([[-1, -2, -1],
                           [0, 0, 0],
                           [1, 2, 1]])
    # 执行卷积:步幅1,填充1(Same Padding)
    output = single_channel_convolution(input_image, edge_kernel, stride=1, padding=1)
    print("输入特征图形状:", input_image.shape)
    print("输出特征图形状:", output.shape)
    print("输出特征图(边缘检测结果):\n", output.round(2))

四、步幅

步幅(Stride)是卷积运算中卷积核沿输入特征图滑动的“每次移动的像素数”,分为水平步幅(Sₕ)和垂直步幅(Sᵥ),通常设为相同值(如S=1、S=2)。

  • 作用:控制输出特征图的尺寸和计算效率。步幅越大,卷积核滑动次数越少,输出特征图尺寸越小,计算速度越快;步幅越小,输出尺寸越大,特征保留越完整,但计算量越大。

  • 示例:输入尺寸为5×5,卷积核尺寸为3×3,步幅S=1时,水平/垂直滑动次数为5-3+1=3次,输出尺寸为3×3;步幅S=2时,滑动次数为(5-3)/2 +1=2次,输出尺寸为2×2。

五、填充

填充(Padding)是在输入特征图的边缘(上下左右)添加额外像素(通常为0,称为“零填充”),目的是“保持输出特征图的尺寸”或“保留边缘特征”。

  • 核心问题:无填充时,卷积核每次滑动会“舍弃”边缘像素,导致输出尺寸小于输入(如5×5输入用3×3卷积核,输出3×3),且边缘特征(如物体轮廓)因仅被卷积一次而提取不充分。

  • 常见填充方式:
    有效填充(Valid Padding):不添加任何像素,输出尺寸最小,无边缘信息补充。

  • 相同填充(Same Padding):添加像素使输出尺寸与输入尺寸相同,计算公式为填充量P=(K-1)/2(K为卷积核尺寸,需为奇数,如K=3时P=1,在边缘各加1层像素)。

六、输出特征图计算公式

输出特征图的尺寸(仅考虑二维单通道情况,设输出高度为Hₒᵤₜ、宽度为Wₒᵤₜ)由输入尺寸(Hᵢₙ、Wᵢₙ)、卷积核尺寸(Kₕ、Kᵥ,通常Kₕ=Kᵥ=K)、步幅(Sₕ、Sᵥ,通常Sₕ=Sᵥ=S)、填充量(Pₕ、Pᵥ,通常Pₕ=Pᵥ=P)共同决定,公式如下:

Hₒᵤₜ = ⌊(Hᵢₙ + 2Pₕ - Kₕ) / Sₕ⌋ + 1

Wₒᵤₜ = ⌊(Wᵢₙ + 2Pᵥ - Kᵥ) / Sᵥ⌋ + 1

  • 符号说明:⌊·⌋表示向下取整(若计算结果为小数,舍弃小数部分)。

  • 示例:输入Hᵢₙ=224、Wᵢₙ=224,K=3,S=1,P=1(Same Padding),则Hₒᵤₜ=(224+2×1-3)/1 +1=224,输出尺寸与输入一致。

输出特征图尺寸计算代码实现(含注释)


import math

def calculate_output_size(H_in, W_in, K, S=1, P=0):
    """
    计算卷积后输出特征图的尺寸
    参数:
        H_in: 输入特征图高度
        W_in: 输入特征图宽度
        K: 卷积核尺寸(正方形,K×K)
        S: 步幅(默认1,水平和垂直步幅相同)
        P: 填充量(默认0,上下左右填充量相同)
    返回:
        H_out: 输出特征图高度
        W_out: 输出特征图宽度
    """
    # 直接套用公式,使用math.floor实现向下取整
    H_out = math.floor((H_in + 2 * P - K) / S) + 1
    W_out = math.floor((W_in + 2 * P - K) / S) + 1
    return H_out, W_out

# 测试代码(覆盖不同场景)
if __name__ == "__main__":
    # 场景1:Valid Padding(无填充),5×5输入,3×3卷积,步幅1
    H1, W1 = calculate_output_size(H_in=5, W_in=5, K=3, S=1, P=0)
    print("场景1(Valid Padding)输出尺寸:", (H1, W1))  # 预期(3,3)
    
    # 场景2:Same Padding(填充1),5×5输入,3×3卷积,步幅1
    H2, W2 = calculate_output_size(H_in=5, W_in=5, K=3, S=1, P=1)
    print("场景2(Same Padding)输出尺寸:", (H2, W2))  # 预期(5,5)
    
    # 场景3:大步幅(步幅2),5×5输入,3×3卷积,无填充
    H3, W3 = calculate_output_size(H_in=5, W_in=5, K=3, S=2, P=0)
    print("场景3(步幅2)输出尺寸:", (H3, W3))  # 预期(2,2)
    
    # 场景4:经典CNN输入(224×224),3×3卷积,步幅1,Same Padding
    H4, W4 = calculate_output_size(H_in=224, W_in=224, K=3, S=1, P=1)
    print("场景4(224×224输入)输出尺寸:", (H4, W4))  # 预期(224,224)

七、多通道卷积运算

多通道场景分为“输入多通道”(如RGB彩色图3通道)和“输出多通道”(提取多种特征),运算核心是“通道对应卷积,结果累加,多卷积核实现多输出”。

  1. 输入多通道,输出单通道
    卷积核通道数=输入通道数(如输入3通道,卷积核为3个3×3矩阵,即3×3×3)。

  2. 运算过程:每个通道的输入特征图与对应通道的卷积核单独做卷积运算,得到3个单通道中间结果,再将这3个结果对应位置像素值相加,得到1个单通道输出特征图。

  3. 输入多通道,输出多通道
    输出通道数=卷积核的个数(如要输出16通道特征,需设置16个独立的3×3×3卷积核)。

  4. 运算过程:每个卷积核分别与输入多通道特征图做“单输出通道”运算,16个卷积核对应得到16个单通道输出特征图,组合后形成16通道的输出特征图。

意义:不同通道的卷积核可提取不同类型的特征(如边缘、纹理、颜色块),多输出通道实现多种特征的并行提取。

八、池化操作

池化(Pooling)是CNN中用于“降维、减少参数、增强鲁棒性”的操作,通常紧跟卷积层之后,核心是“对局部区域像素进行聚合计算”,不改变通道数。

  • 核心目的:
    降维:减少特征图尺寸和参数数量,降低计算量,防止过拟合。

import numpy as np

def multi_channel_convolution(input_feature, kernels, stride=1, padding=0):
    """
    实现多通道卷积运算(输入多通道,输出多通道)
    参数:
        input_feature: 输入多通道特征图,形状为(C_in, H, W),C_in为输入通道数
        kernels: 卷积核组,形状为(C_out, C_in, K, K),C_out为输出通道数,K为卷积核尺寸
        stride: 步幅,默认1
        padding: 填充量,默认0
    返回:
        output_feature: 输出多通道特征图,形状为(C_out, H_out, W_out)
    """
    C_in, H_in, W_in = input_feature.shape
    C_out, _, K, _ = kernels.shape
    
    # 对输入特征图的空间维度进行填充(通道维度不填充)
    padded_input = np.pad(input_feature, ((0, 0), (padding, padding), (padding, padding)), 
                         mode='constant', constant_values=0)
    # 计算输出特征图的空间尺寸
    H_out = (padded_input.shape[1] - K) // stride + 1
    W_out = (padded_input.shape[2] - K) // stride + 1
    # 初始化输出特征图
    output_feature = np.zeros((C_out, H_out, W_out))
    
    # 遍历每个输出通道(对应一个卷积核组)
    for c_out in range(C_out):
        # 当前输出通道对应的卷积核(形状:C_in×K×K)
        current_kernel = kernels[c_out]
        # 遍历空间维度进行卷积
        for i in range(H_out):
            for j in range(W_out):
                # 提取当前感受野(形状:C_in×K×K)
                receptive_field = padded_input[:, i*stride:i*stride+K, j*stride:j*stride+K]
                # 通道对应相乘后求和(C_in个通道的卷积结果累加)
                conv_sum = np.sum(receptive_field * current_kernel)
                # ReLU激活
                output_feature[c_out, i, j] = max(0, conv_sum)
    
    return output_feature

# 测试代码
if __name__ == "__main__":
    # 模拟输入:3通道(RGB)、5×5的彩色图(通道维度在前)
    input_rgb = np.array([
        # 红色通道
        [[1, 2, 3, 2, 1], [2, 3, 4, 3, 2], [3, 4, 5, 4, 3], [2, 3, 4, 3, 2], [1, 2, 3, 2, 1]],
        # 绿色通道
        [[0, 1, 2, 1, 0], [1, 2, 3, 2, 1], [2, 3, 4, 3, 2], [1, 2, 3, 2, 1], [0, 1, 2, 1, 0]],
        # 蓝色通道
        [[2, 1, 0, 1, 2], [1, 0, 1, 0, 1], [0, 1, 2, 1, 0], [1, 0, 1, 0, 1], [2, 1, 0, 1, 2]]
    ])
    # 卷积核组:2个输出通道,每个通道对应3个输入通道的3×3卷积核
    kernels = np.array([
        # 输出通道1的卷积核(模拟提取边缘)
        [
            [[-1, -1, -1], [0, 0, 0], [1, 1, 1]],  # 红色通道核
            [[-1, -1, -1], [0, 0, 0], [1, 1, 1]],  # 绿色通道核
            [[-1, -1, -1], [0, 0, 0], [1, 1, 1]]   # 蓝色通道核
        ],
        # 输出通道2的卷积核(模拟提取纹理)
        [
            [[1, 0, -1], [2, 0, -2], [1, 0, -1]],  # 红色通道核
            [[1, 0, -1], [2, 0, -2], [1, 0, -1]],  # 绿色通道核
            [[1, 0, -1], [2, 0, -2], [1, 0, -1]]   # 蓝色通道核
        ]
    ])
    # 执行多通道卷积:步幅1,填充1
    output = multi_channel_convolution(input_rgb, kernels, stride=1, padding=1)
    print("输入特征图形状(C_in, H, W):", input_rgb.shape)  # 预期(3,5,5)
    print("输出特征图形状(C_out, H, W):", output.shape)    # 预期(2,5,5)
    print("输出通道1特征图:\n", output[0].round(2))
    print("输出通道2特征图:\n", output[1].round(2))

多通道卷积运算代码实现

  • 增强鲁棒性:对微小的位置偏移不敏感(如目标轻微移动后,聚合结果不变),提升模型泛化能力。

常见类型:
最大池化(Max Pooling):用局部区域内的最大值作为输出像素(如2×2池化核,取4个像素中的最大值),能有效保留边缘、纹理等强特征,应用最广泛。

平均池化(Average Pooling):用局部区域内的平均值作为输出像素,能保留整体灰度信息,但特征提取能力弱于最大池化,常用于网络后期或分类层前。

运算逻辑:与卷积类似,池化核(如2×2)以固定步幅(通常与池化核尺寸相同,如2×2池化步幅=2)滑动,对每个局部区域做聚合计算。

九、卷积神经网络中的池化操作

在CNN架构中,池化操作是“卷积层的重要补充”,其位置和作用具有明确的设计逻辑,核心是“配合卷积层实现特征的逐步抽象”。

  1. 典型位置:通常位于卷积层之后(Conv→Pool→Conv→Pool…),形成“卷积提取特征-池化降维”的交替结构,部分网络(如ResNet)会在 bottleneck 结构中嵌入池化。

池化操作代码实现


import numpy as np

def pooling(input_feature, pool_size=2, stride=2, mode='max'):
    """
    实现池化操作(支持最大池化和平均池化,多通道兼容)
    参数:
        input_feature: 输入特征图,形状为(C, H, W),C为通道数
        pool_size: 池化核尺寸(正方形,默认2×2)
        stride: 步幅(默认2,与池化核尺寸相同,避免重叠)
        mode: 池化模式,'max'为最大池化,'avg'为平均池化(默认max)
    返回:
        output_feature: 输出池化后的特征图,形状为(C, H_out, W_out)
    """
    C, H_in, W_in = input_feature.shape
    # 计算输出特征图的空间尺寸
    H_out = (H_in - pool_size) // stride + 1
    W_out = (W_in - pool_size) // stride + 1
    # 初始化输出特征图
    output_feature = np.zeros((C, H_out, W_out))
    
    # 遍历每个通道(池化不改变通道数,通道独立运算)
    for c in range(C):
        # 遍历空间维度进行池化
        for i in range(H_out):
            for j in range(W_out):
                # 提取当前池化区域
                pool_region = input_feature[c, i*stride:i*stride+pool_size, j*stride:j*stride+pool_size]
                # 根据模式计算池化结果
                if mode == 'max':
                    output_feature[c, i, j] = np.max(pool_region)
                elif mode == 'avg':
                    output_feature[c, i, j] = np.mean(pool_region)
                else:
                    raise ValueError("仅支持'max'和'avg'两种池化模式")
    
    return output_feature

# 测试代码
if __name__ == "__main__":
    # 模拟输入:2通道、4×4的特征图(来自多通道卷积的输出)
    input_feature = np.array([
        # 通道1
        [[1.2, 0.8, 2.1, 1.5],
         [0.5, 1.9, 0.7, 2.3],
         [1.1, 0.3, 1.7, 0.9],
         [0.6, 1.4, 0.4, 1.8]],
        # 通道2
        [[0.9, 1.3, 0.5, 1.7],
         [1.6, 0.4, 1.2, 0.8],
         [0.7, 1.5, 0.6, 1.1],
         [1.0, 0.3, 1.4, 0.2]]
    ])
    # 执行最大池化(2×2池化核,步幅2)
    max_pool_output = pooling(input_feature, pool_size=2, stride=2, mode='max')
    # 执行平均池化(2×2池化核,步幅2)
    avg_pool_output = pooling(input_feature, pool_size=2, stride=2, mode='avg')
    
    print("输入特征图形状(C, H, W):", input_feature.shape)  # 预期(2,4,4)
    print("最大池化输出形状:", max_pool_output.shape)        # 预期(2,2,2)
    print("最大池化结果:\n", max_pool_output.round(2))
    print("平均池化输出形状:", avg_pool_output.shape)        # 预期(2,2,2)
    print("平均池化结果:\n", avg_pool_output.round(2))
  1. 关键作用
    特征层级提升:卷积层提取低层次特征(如边缘、线条),池化后特征图尺寸缩小,后续卷积层可捕捉更大范围的高层特征(如纹理、形状、目标部件)。

  2. 控制过拟合:通过减少参数数量和对位置的敏感性,降低模型对训练数据细节的依赖,提升泛化能力。

  3. 保持计算效率:若仅用卷积层降维(增大步幅),会丢失过多特征;池化在降维的同时能更好保留关键特征,平衡性能与效率。

  4. 注意事项:池化层无可学习参数,仅通过固定规则聚合像素;现代网络中,有时用“步幅大于1的卷积层”替代池化层,以在降维时保留更多特征信息。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Peter_Monster

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值