背景问题
有时候,我们会使用深度神经网络来学习数据。如果效果不理想,常常都会 加深和加宽全连接层 来让效果更加的完善。但是加深加宽网络会引来更加多的问题,比如 梯度消失/梯度爆炸 等。
特别是面对图像识别问题的时候,比如手写数字识别,一幅图片本来就有大量的信息,width
/height
/3 color channels
等,加深加宽网络下的模型,结果往往未如理想。
CNN
就是为了解决上述的问题的。CNN
即是 Convolution Neural Network
,卷积神经网络。
先讲解不同的子组件,再组合起来,形成手写的模型,这样就更加容易的去理解模型。
卷积和卷积核
卷积
卷积,是 来源矩阵 和 卷积核矩阵 一个个对应位置进行相乘后,再相加的运算。
# 对 【中心点】进行卷积运算
# 来源举证
input_array = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
# 卷积核
kernel_array = np.array([[1, 0, 0],
[0, 1, 0],
[0, 0, 1]])
# 卷积操作
# 1*1+2*0+3*0+4*0+5*1+6*0+7*0+8*0+9*1 = 15
# 对应卷积后
Output:
[[15]]
如果加上 padding
和 stride
的话,即卷积核就可以沿着 来源矩阵 滑动,完成整个卷积操作。
# padding = 1 ,stride = 1
# 来源矩阵
input_array = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
# 卷积核
kernel_array = np.array([[1, 0, 0],
[0, 1, 0],
[0, 0, 1]])
# 对应卷积后
Output:
[[ 6 8 3]
[12 15 8]
[ 7 12 14]]
卷积核 kernel's
size
,padding
, stride
与 输入矩阵 input
和输出矩阵 output
大小的对应关系:
如果是 3*3 的卷积核,padding=1 stride=1,输入和输出的矩阵大小一致。
卷积核
上例中,卷积核,就是 kernel_array
,[[1, 0, 0],[0, 1, 0],[0, 0, 1]]
,他是 3X3 的卷积核。
从 网站 知道,不同的卷积核应用到相同的图片上,可以得到不同的结果。比如有一些是可以提取边缘特征,有些是可以模糊的。
💊 卷积核,就是提取特征的矩阵。有多少个卷积核,就是代表提取多少个特征。不同的卷积核可以提取不同的特征。
下面的代码,就是使用预设的卷积核(提取边缘轮廓特征的卷积核)去获取特征,我们可以看到直接输出的结果。
import torch
import torch.nn as nn
import matplotlib.pyplot as plt
import numpy as np
# Create a sample input image (1, 1, 5, 5)
input_image = torch.tensor([[[[1, 2, 3, 0, 1],
[0, 1, 2, 3, 1],
[3, 0, 1, 2, 0],
[1, 2, 3, 0, 1],
[0, 1, 2, 3, 0]]]], dtype=torch.float32)
# Define a convolutional layer
conv_layer = nn.Conv2d(in_channels=1, out_channels=1, kernel_size=3, stride=1, padding=1)
# Initialize the kernel weights for demonstration purposes (e.g., edge detection kernel)
edge_detection_kernel = torch.tensor([[[[-1, -1, -1],
[-1, 8, -1],
[-1, -1, -1]]]], dtype=torch.float32)
conv_layer.weight = torch.nn.Parameter(edge_detection_kernel)
conv_layer.bias = torch.nn.Parameter(torch.zeros(1))
# Apply the convolutional layer to the input image
output_image = conv_layer(input_image)
# Convert the output to a NumPy array for visualization
output_image_np = output_image.detach().numpy().squeeze()
# Visualize the input and output images
def visualize_images(input_img, output_img):
fig, axes = plt.subplots(1, 2, figsize=(10, 5))
axes[0].imshow(input_img.squeeze(), cmap='gray')
axes[0].set_title('Input Image')
axes[0].axis('off')
axes[1].imshow(output_img, cmap='gray')
axes[1].set_title('Output Image')
axes[1].axis('off')
plt.show()
# Display the images
visualize_images(input_image, output_image_np)
# Print the input and output values for comparison
print("Input Image:\n", input_image.squeeze().numpy())
print("\nOutput Image:\n", output_image_np)
Input Image:
[[1. 2. 3. 0. 1.]
[0. 1. 2. 3. 1.]
[3. 0. 1. 2. 0.]
[1. 2. 3. 0. 1.]
[0. 1. 2. 3. 0.]]
Output Image:
[[ 5. 9. 16. -10. 4.]
[ -7. -4. 4. 14. 2.]
[ 20. -13. -5. 5. -7.]
[ 2. 5. 13. -12. 3.]
[ -4. 0. 7. 18. -4.]]
从结果知道(从灰度图看不出问题,但是从数字矩阵可以看出),这个卷积核令到 黑的更加的黑, 白的更加的白,突出了特征。
卷积的好处
- 共享参数
卷积核,就是滑过数据的共享参数,那么他能够提取对应的特征数据。所以一个卷积核对应着一个特征。需要提取的特征,不论是在图片的哪个位置,都可以提取出来。
- 参数减少
比如 100 * 100 的图像,要去识别苹果,但是这个苹果有可能出现在上下左右,随机的位置。
如果使用 NN
神经网络 就要使用 (100 * 100, n, n) 网络, 网络的输入是每个坐标像素。但是苹果的出现的位置是随机的,即坐标是随机的,所以对应的神经元也是需要学习,导致非常难收敛。同时,参数量就是巨大的,达到 10000 * n * n。
但是使用 CNN
,可以针对 苹果轮廓 训练 对应卷积核,达到识别的效果,就大大减少参数量。参数量就是 卷积核上的数值 和 后续的高维度的全链接网络的参数。
- 位置信息的共享
如果上例子中,苹果轮廓信息是共享的。
池化
有平均池化和最大值池化,他 没有参数学习,完全是按照默认的逻辑去运行的。
- 平均池化,取平均值,会模糊特征,他可以考虑到所有像素的关系;
- 最大值池化,取最大值,就突出了特征了,完全是忽视了最小值。
Input_array:
[[ 1. 2. 3. 4.]
[ 5. 6. 7. 8.]
[ 9. 10. 11. 12.]
[13. 14. 15. 16.]]
应用 kernel_size=2, stride=2 的 池化
Average Pooled Image:
[[ 3.5 5.5]
[11.5 13.5]]
Max Pooled Image:
[[ 6. 8.]
[14. 16.]]
从结果知道,如果池化后,计算量是加上了 3/4,同时可以保留原来图像的特征。
池化好处
就是为了减少运算量,而且没有参数,不用学习。换一句话,有很多的数据是没有意义的,计算他们投入产出比太低,不如直接放弃。
手写简单 CNN 网络
组装 卷积 和 _池化 _的一个网络,就是一个简单的 CNN
网络,比如 以下的代码就是一个简单的 卷积和池化操作相结合。
# 部分代码的演示
# conv 定义卷积
conv = .Conv2d(1, 32, kernel_size=3, padding=1
# pool 定义池化
pool = nn.MaxPool2d(kernel_size=2, stride=2)
# 结合激活函数
x = pool(relu(conv(x)))
所有模型(包括
NN
,CNN
,AlexNet
,ResNet
,RNN
等 ),都是一个个的子组件的组装,就好像积木一样。所以理解好不同的 子组件的原理和用法,就更加容易理解由他们组装而成的大模型。
完整代码检测手写数字的图片
导入手写数字的图片库,同时使用一个 简单的 CNN 来训练
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
from torch.utils.data import DataLoader
import torch.nn.functional as F
# Define the CNN model
class SimpleCNN(nn.Module):
def __init__(self):
super(SimpleCNN, self).__init__()
self.conv1 = nn.Conv2d(1, 32, kernel_size=3, padding=1)
self.pool = nn.MaxPool2d(kernel_size=2, stride=2)
self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1)
self.fc1 = nn.Linear(64 * 7 * 7, 64)
self.fc2 = nn.Linear(64, 10)
def forward(self, x):
x = self.pool(F.relu(self.conv1(x)))
x = self.pool(F.relu(self.conv2(x)))
x = x.view(-1, 64 * 7 * 7)
x = F.relu(self.fc1(x))
x = self.fc2(x)
return x
# Load and preprocess the MNIST dataset
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5,), (0.5,))
])
trainset = torchvision.datasets.MNIST(root='../data/mnist', train=True, download=True, transform=transform)
trainloader = DataLoader(trainset, batch_size=64, shuffle=True)
testset = torchvision.datasets.MNIST(root='../data/mnist', train=False, download=True, transform=transform)
testloader = DataLoader(testset, batch_size=64, shuffle=False)
# Instantiate the model, define the loss function and the optimizer
model = SimpleCNN()
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9)
# Training loop
num_epochs = 5
for epoch in range(num_epochs):
running_loss = 0.0
for i, data in enumerate(trainloader, 0):
inputs, labels = data
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
running_loss += loss.item()
if i % 100 == 99:
print(f'[Epoch {epoch + 1}, Batch {i + 1}] loss: {running_loss / 100:.3f}')
running_loss = 0.0
print('Finished Training')
# Test the model
correct = 0
total = 0
with torch.no_grad():
for data in testloader:
images, labels = data
outputs = model(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
print(f'Accuracy of the network on the 10000 test images: {100 * correct / total:.2f}%')
'''
# 部分打印
[Epoch 5, Batch 600] loss: 0.075
[Epoch 5, Batch 700] loss: 0.093
[Epoch 5, Batch 800] loss: 0.074
[Epoch 5, Batch 900] loss: 0.081
Finished Training
Accuracy of the network on the 10000 test images: 98.08%
'''
模型训练的结果是另人满意的。
说明
上面的例子,已经说明了 CNN 的整体的框架。
结构图
面对 分类任务,主体上分成两层: Feature Extraction 特征提取 和 Classification 分类
Feature Extraction
: input 就是输入的数据,Convolution 就是卷积,Pooling 就是池化,到这步为止,就是从低维度特征的提取。
Classification
:之后对提取后的特征进行 高维全网络学习 和 softmax 进行分类。
一句话原理
- 全数据先用卷积提取特征,再使用 池化层 进行数据量的简化,同时保留特征
- 特征再用全链接学习规律
总结成一句话就是,每个组件都在自己擅长东西。
❓卷积核的 out_channel
是否重要?
nn.Conv2d(32, 64, kernel_size=3, padding=1)
中的64
就是out_channel
重要,他是决定了多少个卷积核,即多少个 features 可以给提取。
题外话,卷积核的 size
,是决定了他可以识别的范围(接收野 receptive field),比如 小的卷积核,就可以识别精细的特征;大的,可以识别轮廓的特征。
❓卷积核的参数是学习而来,还是预设定的呢?
可以预设,也可以学习而来的。
但是在 CNN 中,尽量不用指定卷积核,让他去进行训练习得。
简要总结
卷积
- 共享参数
- 参数减少
- 可以有位置信息的共享
卷积核
- out_channel 决定了提取多少个特征
- kernel_size 决定你有多少的视野
池化,就是为了简化数量的同时,又可以保留特征;全链接层,对抽取的特征进行学习。
_CNN _就是组合上述组件,形成一个大的网络。
优缺点和场景
- 有空间位置信息,就可以使用卷积神经网络
- 特别图像识别