第一章 卷积神经网络基础 v1.0
第一部分 概述
一、什么是CNN
卷积神经网络是一种前馈神经网络,其核心思想是通过卷积运算自动提取数据的局部特征,然后逐层抽象,最终实现对复杂模式的识别
CNN的特点包括:
- 局部连接:每个神经元只与输入数据的一个局部区域相连
- 权值共享:同一个卷积核在整个输入上共享参数
- 平移不变性:对目标的位置变化具有一定的鲁棒性
二、为什么需要CNN
在CNN出现之前,神经网络都是全连接的。每个神经元都要和上一层的所有神经元相连,参数量大得惊人
举个例子,一张224x224x3的图片,展平后有150528个像素。如果第一个隐藏层有1000个神经元,光这一层就需要1.5亿个参数
这显然不现实,于是CNN应运而生
第二部分 核心组件
一、卷积层
卷积层是CNN的核心组件,其作用是提取输入数据的局部特征
1.卷积运算
卷积运算的本质是滑动窗口加权求和。想象你拿着一个小窗口(卷积核)在图片上滑动,每到一个位置就把对应区域的像素值加权求和
import numpy as np
# 一张5x5的灰度图
image = np.array([
[0, 1, 1, 0, 0],
[0, 1, 1, 1, 0],
[0, 1, 1, 1, 0],
[0, 0, 1, 1, 0],
[0, 0, 0, 1, 0]
])
# 3x3的卷积核,用于检测边缘
kernel = np.array([
[-1, -1, -1],
[-1, 8, -1],
[-1, -1, -1]
])
# 手动计算卷积
def conv2d(image, kernel):
h, w = image.shape
kh, kw = kernel.shape
oh, ow = h - kh + 1, w - kw + 1
result = np.zeros((oh, ow))
for i in range(oh):
for j in range(ow):
window = image[i:i+kh, j:j+kw]
result[i, j] = np.sum(window * kernel)
return result
print(conv2d(image, kernel))这个卷积核的作用是检测边缘。你会发现,输出中数值大的地方正好对应原图中"1"聚集的边缘
2.卷积核
卷积核(Filter)是一个小的权重矩阵,用于在输入数据上滑动并提取特征
不同的卷积核可以提取不同的特征,例如:
- 边缘检测
- 纹理识别
- 形状检测
3.步长与填充
- 步长(Stride):卷积核每次滑动的距离
- 填充(Padding):在输入数据边缘填充0,保持输出尺寸
二、池化层
池化层的作用是降低特征图的尺寸,减少计算量
最常用的是最大池化(Max Pooling):在一个小区域内取最大值
import numpy as np
# 4x4的特征图
feature_map = np.array([
[1, 3, 2, 4],
[5, 6, 7, 8],
[9, 10, 11, 12],
[13, 14, 15, 16]
])
# 2x2最大池化
def max_pooling(feature_map, size=2):
h, w = feature_map.shape
oh, ow = h // size, w // size
result = np.zeros((oh, ow))
for i in range(oh):
for j in range(ow):
window = feature_map[i*size:(i+1)*size, j*size:(j+1)*size]
result[i, j] = np.max(window)
return result
print(max_pooling(feature_map))
# 输出:
# [[ 6. 8.]
# [14. 16.]]池化后,特征图尺寸减半,但保留了最重要的特征
三、全连接层
全连接层位于CNN的最后几层,用于将提取的特征进行分类
其结构与传统的全连接神经网络相同,每个神经元都与前一层的所有神经元相连
第三部分 经典架构
一、LeNet-5 (1998)
最早的CNN之一,由Yann LeCun提出,用于手写数字识别
结构:卷积层 -> 池化层 -> 卷积层 -> 池化层 -> 全连接层 -> 输出
二、AlexNet (2012)
在ImageNet竞赛中一战成名,把错误率从26%降到了15%
关键创新:
- 使用ReLU激活函数
- Dropout正则化
- GPU训练
三、VGGNet (2014)
证明了"深度很重要"这个道理。只用3x3的小卷积核,但堆了很多层
四、GoogLeNet (2014)
GoogLeNet也叫Inception网络,核心创新是Inception模块:用不同大小的卷积核并行提取特征,再拼接起来
这样既能捕捉多尺度特征,又控制了计算量。在当年ImageNet比赛中以较大优势夺冠
import torch
import torch.nn as nn
class InceptionModule(nn.Module):
def __init__(self, in_channels, ch1x1, ch3x3_reduce, ch3x3, ch5x5_reduce, ch5x5, pool_proj):
super().__init__()
# 1x1卷积分支
self.branch1 = nn.Sequential(
nn.Conv2d(in_channels, ch1x1, 1),
nn.BatchNorm2d(ch1x1),
nn.ReLU()
)
# 1x1卷积降维 + 3x3卷积
self.branch2 = nn.Sequential(
nn.Conv2d(in_channels, ch3x3_reduce, 1),
nn.BatchNorm2d(ch3x3_reduce),
nn.ReLU(),
nn.Conv2d(ch3x3_reduce, ch3x3, 3, padding=1),
nn.BatchNorm2d(ch3x3),
nn.ReLU()
)
# 1x1卷积降维 + 5x5卷积
self.branch3 = nn.Sequential(
nn.Conv2d(in_channels, ch5x5_reduce, 1),
nn.BatchNorm2d(ch5x5_reduce),
nn.ReLU(),
nn.Conv2d(ch5x5_reduce, ch5x5, 5, padding=2),
nn.BatchNorm2d(ch5x5),
nn.ReLU()
)
# 3x3池化 + 1x1卷积
self.branch4 = nn.Sequential(
nn.MaxPool2d(3, stride=1, padding=1),
nn.Conv2d(in_channels, pool_proj, 1),
nn.BatchNorm2d(pool_proj),
nn.ReLU()
)
def forward(self, x):
branch1 = self.branch1(x)
branch2 = self.branch2(x)
branch3 = self.branch3(x)
branch4 = self.branch4(x)
return torch.cat([branch1, branch2, branch3, branch4], dim=1)
# 示例:Inception模块
inception = InceptionModule(192, 64, 96, 128, 16, 32, 32)
x = torch.randn(1, 192, 28, 28)
print(inception(x).shape) # 输出: torch.Size([1, 256, 28, 28])Inception模块的设计哲学是"更宽而不是更深",让网络自己选择最合适的特征尺度
五、ResNet (2015)
引入了残差连接,解决了深层网络的梯度消失问题。让训练几百层的网络成为可能
import torch
import torch.nn as nn
class ResidualBlock(nn.Module):
def __init__(self, channels):
super().__init__()
self.conv1 = nn.Conv2d(channels, channels, 3, padding=1)
self.bn1 = nn.BatchNorm2d(channels)
self.relu = nn.ReLU()
self.conv2 = nn.Conv2d(channels, channels, 3, padding=1)
self.bn2 = nn.BatchNorm2d(channels)
def forward(self, x):
residual = x # 保存输入
out = self.relu(self.bn1(self.conv1(x)))
out = self.bn2(self.conv2(out))
out += residual # 跳跃连接
return self.relu(out)残差连接的精髓在于:让网络学习"残差"而不是完整的映射。如果某一层没什么用,至少不会让信息变差
六、DenseNet (2017)
DenseNet的核心思想是密集连接:每一层都与前面所有层直接相连,实现特征重用
与ResNet不同,DenseNet不是相加,而是拼接。这样既能缓解梯度消失,又能减少参数量
import torch
import torch.nn as nn
class DenseLayer(nn.Module):
def __init__(self, in_channels, growth_rate):
super().__init__()
self.bn1 = nn.BatchNorm2d(in_channels)
self.relu1 = nn.ReLU()
self.conv1 = nn.Conv2d(in_channels, 4 * growth_rate, 1)
self.bn2 = nn.BatchNorm2d(4 * growth_rate)
self.relu2 = nn.ReLU()
self.conv2 = nn.Conv2d(4 * growth_rate, growth_rate, 3, padding=1)
def forward(self, x):
out = self.conv1(self.relu1(self.bn1(x)))
out = self.conv2(self.relu2(self.bn2(out)))
return torch.cat([x, out], dim=1) # 密集连接:拼接
class DenseBlock(nn.Module):
def __init__(self, num_layers, in_channels, growth_rate):
super().__init__()
self.layers = nn.ModuleList()
for i in range(num_layers):
self.layers.append(DenseLayer(in_channels + i * growth_rate, growth_rate))
def forward(self, x):
for layer in self.layers:
x = layer(x)
return x
# 示例:4层Dense Block,增长率为12
block = DenseBlock(num_layers=4, in_channels=64, growth_rate=12)
x = torch.randn(1, 64, 32, 32)
print(block(x).shape) # 输出: torch.Size([1, 112, 32, 32])DenseNet通过密集连接让特征在网络中充分流动,特别适合数据量不大的场景
第四部分 应用场景
CNN在很多领域仍然是主力
- 图像分类:识别图片中有什么
- 目标检测:找出物体在哪里(YOLO、Faster R-CNN)
- 语义分割:给每个像素分类
- 人脸识别:解锁手机用的就是它
- 风格迁移:把照片变成名画风格
第五部分 局限性
CNN也不是万能的,它有几个明显的缺点
- 对旋转敏感:一只横着的猫和竖着的猫,CNN可能认不出是同一只
- 感受野有限:虽然可以通过堆叠层数来扩大感受野,但还是有上限
- 需要大量数据:CNN的参数量大,需要足够多的训练数据