什么是卷积神经网络?
一、概念
卷积神经网络(Convolutional Neural Network, CNN)可以说是最为经典的深度学习模型之一,特别适用于处理图像等具有网格结构的数据。CNN通过卷积层、池化层和全连接层的组合,能够自动提取数据的特征,并进行分类或回归任务。其中,CNN最核心的部件便是卷积层和池化层:
-
卷积层(Convolutional Layer):卷积层是CNN的核心组件,通过卷积操作提取输入数据的局部特征。卷积操作使用多个卷积核(filter)在输入数据上滑动,生成特征图(feature map)。
-
池化层(Pooling Layer):池化层用于对特征图进行下采样,减少数据的维度和计算量,同时保留重要特征。常见的池化操作有最大池化(Max Pooling)和平均池化(Average Pooling)。
二、核心算法
1、卷积操作
卷积操作是通过卷积核(filter)在输入数据上滑动,进行点积运算,生成特征图(feature map)。卷积核是一组可训练的权重,用于提取输入数据的局部特征,每次滑动时计算卷积核与输入数据的局部区域的点积,并将结果存储在特征图中。卷积操作可以通过以下公式表示:
其中,x是输入数据,w是卷积核,b是偏置,y是输出特征图,i 和 j 是特征图的坐标,M和N是卷积核的高度和宽度。假设输入的形状是,卷积核大小为,在不进行padding填充和stride步幅调整的情况下(后面会介绍这两个参数),输出形状为。一般来说,卷积操作的计算步骤如下:
- 选择卷积核大小:选择卷积核的高度和宽度(例如,3x3或5x5)。
- 滑动卷积核:将卷积核从输入数据的左上角开始,逐步向右和向下滑动,直到覆盖整个输入数据。
- 计算点积:在每个位置,计算卷积核与输入数据的局部区域的点积,并加上偏置。
- 存储结果:将点积结果存储在特征图的相应位置。
上面这个图中的卷积操作,原始图像大小3*3,卷积核大小2*2,最后得到大小为2*2的特征图,计算过程如下:
- 左上角特征值:0*0+1*1+3*2+4*3 = 19
- 右上角特征值:1*0+2*1+4*2+5*3 = 25
- 左下角特征值:3*0+4*1+6*2+7*3 = 37
- 右下角特征值:4*0+5*1+7*2+8*3 = 43
2、池化操作
池化操作用于对特征图进行下采样,减少数据的维度和计算量,同时保留重要特征。常见的池化操作有最大池化(Max Pooling)和平均池化(Average Pooling)。池化操作的基本原理是将池化窗口在特征图上滑动,在每个位置计算池化窗口内的最大值或平均值,并将结果存储在下采样后的特征图中。池化操作之后,输入层和输出层的大小关系计算与卷积操作一致。一般的池化计算步骤如下:
- 选择池化窗口大小:选择池化窗口的高度和宽度(例如,2*2或3*3)。
- 滑动池化窗口:将池化窗口从特征图的左上角开始,逐步向右和向下滑动,直到覆盖整个特征图。
- 计算池化值:在每个位置,计算池化窗口内的最大值或平均值。
- 存储结果:将池化值存储在下采样后的特征图的相应位置。
上图中,设卷积之后的特征图大小为3*3,池化层大小为2*2且使用最大池化策略,则池化结果特征图的大小为2*2,计算过程如下:
- 左上角特征值:max(0, 1, 3, 4) = 4
- 右上角特征值:max(1, 2, 4, 5) = 5
- 左下角特征值:max(3, 4, 6, 7) = 7
- 右下角特征值:max(4, 5, 7, 8) = 8
3、参数选择
不论是卷积还是池化,我们都可以通过调整以下的两个参数(非必须)来让特征提取结果符合我们的需求。
- 填充(padding):卷积和池化过程默认不进行填充,但我们可以通过在输入高和宽的两侧填充元素(通常是0)来改变输入的高和宽,从而改变卷积或者池化输出的特征图大小,以便于我们在构造CNN网络的过程中按需求来维持每个层的输出形状,例如让输入和输出保持相同的维度。
- 步幅(stride):卷积和池化都是在输入数据上进行滑动来提取特征,那么每次滑动的行数和列数称为步幅。默认的情况下,在高和宽上的滑动步幅都是1,我们也可以分别将它们设置成更大的值,从而进一步压缩输出大小。需要注意的是,卷积和池化只会在滑动到的位置上进行计算,这也就意味着如果我们设置的步幅较大,卷积和池化操作会跳过一些输入特征,而这些特征将被抛弃掉,不参与任何计算过程。
三、python实现
得益于深度学习库的强大,我们无需手动构建卷积层和池化层。这里,我们构建一个简单的数据集并进行CNN分类示例。SimpleDataset类生成一个包含黑色方块和白色方块的简单数据集。每个图像是28x28像素的灰度图像,黑色方块的像素值为0,白色方块的像素值为1。SimpleCNN类则定义了一个简单的卷积神经网络,包括两个卷积层、两个池化层、一个全连接层和一个输出层。
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision.transforms as transforms
from torch.utils.data import Dataset, DataLoader
# 自定义数据集
class SimpleDataset(Dataset):
def __init__(self, transform=None):
self.transform = transform
self.data, self.labels = self._generate_data()
def _generate_data(self):
data = []
labels = []
for i in range(1000):
if i % 2 == 0:
# 黑色方块
img = np.zeros((28, 28), dtype=np.float32)
label = 0
else:
# 白色方块
img = np.ones((28, 28), dtype=np.float32)
label = 1
data.append(img)
labels.append(label)
return np.array(data), np.array(labels)
def __len__(self):
return len(self.data)
def __getitem__(self, idx):
img = self.data[idx]
label = self.labels[idx]
if self.transform:
img = self.transform(img)
return img, label
# 数据预处理
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5,), (0.5,))
])
# 创建数据集和数据加载器
trainset = SimpleDataset(transform=transform)
trainloader = DataLoader(trainset, batch_size=100, shuffle=True, num_workers=2)
# 定义卷积神经网络
class SimpleCNN(nn.Module):
def __init__(self):
super(SimpleCNN, self).__init__()
self.conv1 = nn.Conv2d(in_channels=1, out_channels=32, kernel_size=3, stride=1, padding=1)
self.conv2 = nn.Conv2d(in_channels=32, out_channels=64, kernel_size=3, stride=1, padding=1)
self.pool = nn.MaxPool2d(kernel_size=2, stride=2, padding=0)
self.fc1 = nn.Linear(64 * 7 * 7, 128)
self.fc2 = nn.Linear(128, 2)
self.relu = nn.ReLU()
def forward(self, x):
x = self.pool(self.relu(self.conv1(x)))
x = self.pool(self.relu(self.conv2(x)))
x = x.view(-1, 64 * 7 * 7)
x = self.relu(self.fc1(x))
x = self.fc2(x)
return x
# 定义损失函数和优化器
net = SimpleCNN()
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)
# 训练网络
for epoch in range(10): # 迭代10个epoch
running_loss = 0.0
for i, data in enumerate(trainloader, 0):
inputs, labels = data
optimizer.zero_grad()
outputs = net(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
running_loss += loss.item()
if i % 10 == 9: # 每10个batch打印一次损失
print(f'[Epoch {epoch + 1}, Batch {i + 1}] loss: {running_loss / 10:.3f}')
running_loss = 0.0
print('Finished Training')
# 特征提取
def extract_features(model, dataloader):
model.eval()
features = []
with torch.no_grad():
for data in dataloader:
inputs, _ = data
x = model.pool(model.relu(model.conv1(inputs)))
x = model.pool(model.relu(model.conv2(x)))
x = x.view(-1, 64 * 7 * 7)
features.append(x)
return torch.cat(features, dim=0)
# 提取特征
features = extract_features(net, trainloader)
print('Extracted features shape:', features.shape)