DeepSeek大模型高性能核心技术与多模态融合开发 - 商品搜索 - 京东
使用卷积进行图像识别已经成为计算机视觉领域的一种重要技术。卷积神经网络(CNN)通过其独特的卷积层结构,能够有效地提取图像中的特征信息,进而实现高精度的图像识别。
在图像识别过程中,卷积层扮演着关键角色。它通过滑动卷积核(也称为滤波器)来遍历整个图像,捕捉图像中的局部特征,如边缘、纹理等。这种局部特征提取方式不仅有助于减少模型的参数数量,还能提高模型对图像平移、缩放等变换的鲁棒性。
随着卷积层的逐层深入,网络能够逐渐捕捉到更加抽象和高级的特征表示。这些特征在后续的分类或识别任务中发挥着至关重要的作用。通过堆叠多个卷积层,CNN能够构建起一个深层次的特征提取网络,从而实现对图像内容的深入理解和准确识别。
此外,卷积神经网络还通过引入池化层、全连接层等组件,进一步完善了图像识别的流程。池化层主要用于降低特征图的维度,减少计算量,同时增强特征的鲁棒性。全连接层则负责将提取到的特征映射到最终的分类结果上。
本节我们将介绍图像分类数据集MNIST,并讲解单纯使用卷积完成MNIST图像分类任务,之后还会介绍使用通道注意力的图像分类模型。
5.4.1 数据集的准备
为了让读者聚焦于我们的模型设计,这里使用一个较简单的灰度图像数据集MNIST。实际上MNIST是一个手写数字的数据库,它有60 000个训练样本集和10 000个测试样本集。实际上MNIST数据集中的图像就是如图5-26所示的样子。
读者可以通过本书自带的MNIST数据集获取图像数据,代码如下:
from torch.utils.data import Dataset
from torchvision.transforms.v2 import PILToTensor,Compose
import torchvision
# 手写数字
class MNIST(Dataset):
def __init__(self,is_train=True):
super().__init__()
self.ds=torchvision.datasets.MNIST('../../dataset/mnist/',train=is_train,download=True)
self.img_convert=Compose([
PILToTensor(),
])
def __len__(self):
return len(self.ds)
def __getitem__(self,index):
img,label=self.ds[index]
return self.img_convert(img)/255.0,label
if __name__=='__main__':
import matplotlib.pyplot as plt
ds=MNIST()
img,label=ds[0]
print(label)
plt.imshow(img.permute(1,2,0))
plt.show()
在上面代码中,我们读取了MNIST数据集,并将第一个图像进行可视化展示,结果如图5-27所示。
图5-27 MNIST数据集中一幅图
读者可以自行运行代码查看结果。
5.4.2 图像识别模型的设计
在数字图像处理中有一种基本的处理方法叫线性滤波。它将待处理的二维数字看做一个大型矩阵,图像中的每个像素可以看做矩阵中的每个元素,像素的大小就是矩阵中的元素值。
而使用的滤波工具是另一个小型矩阵,这个矩阵被称为卷积核。卷积核的大小远远小于图像矩阵,而具体的计算方式就是:对于图像大矩阵中的每个像素,计算其周围的像素和卷积核对应位置的乘积,之后将结果相加最终得到的终值就是该像素的值,这样就完成了一次卷积。最简单的图像卷积运算如图5-28所示。
图5-28 最简单的图像卷积运算
关于卷积的基本知识本书并不做过多阐述,有兴趣的读着可以参考另一本作者讲解图像识别的书《PyTorch深度学习与计算机视觉实践》。
我们设计的基本MNIST图像分类模型如下所示。
import torch
import torch.nn as nn
import numpy as np
import einops.layers.torch as elt
class MnistNetword(nn.Module):
def __init__(self):
super(MnistNetword, self).__init__()
#前置的特征提取模块
self.convs_stack = nn.Sequential(
nn.Conv2d(1,12,kernel_size=7), #第一个卷积层
nn.ReLU(),
nn.Conv2d(12,24,kernel_size=5), #第二个卷积层
nn.ReLU(),
nn.Conv2d(24,6,kernel_size=3) #第三个卷积层
)
#最终分类器层
self.logits_layer = nn.Linear(in_features=1536,out_features=10)
def forward(self,inputs):
image = inputs
x = self.convs_stack(image)
#elt.Rearrange的作用是对输入数据维度进行调整,读者可以使用torch.nn.Flatten函数来替代
x = elt.Rearrange("b c h w -> b (c h w)")(x)
logits = self.logits_layer(x)
return logits
model = MnistNetword()
torch.save(model,"model.pth")
5.4.3 结合通道注意力图像分类模型
注意力机制在深度学习中多种多样,它们侧重于模型在处理信息时对不同部分的关注度。而SE块(Squeeze-and-Excitation Block)所引入的,正是一种独特的注意力机制—通道注意力机制,这与我们通常理解的注意力机制有所不同。
通道注意力机制的核心思想在于,它旨在深入探索每个卷积核通道中的特征信息,并通过综合这些特征来揭示不同通道之间的内在联系。具体来说,SE块首先通过挤压(squeeze)操作,将每个通道的全局空间信息压缩为一个标量值,这个值在某种程度上代表了该通道特征的重要性。随后,通过激励(excitation)操作,这些标量值被用于重新调整(或加权)原始通道特征,以增强模型对重要特征的关注度并抑制不相关的特征。
1. 什么是通道注意力机制?
在卷积神经网络中,每个卷积层都会输出多个特征图(feature maps),每个特征图对应一个通道(channel)。传统上,这些通道是被同等对待的,即每个通道对最终决策的贡献被认为是相同的。通道注意力的结构如图5-29所示。
图5-29 通道注意力SEblock
SE块是SENet(Squeeze-and-Excitation Networks)中的核心组件,它通过两个精心设计的操作—Squeeze和Excitation,实现了对通道关系的显式建模和特征重新校准,从而显著提升了卷积神经网络的性能。
2. Squeeze操作
Squeeze操作的主要目的是将全局空间信息有效地压缩到一个通道描述符中。对于给定的特征图U,其维度通常为(H, W, C),其中H和W分别代表特征图的高度和宽度,C代表通道数。Squeeze操作通过全局平均池化(Global Average Pooling,GAP)来实现,具体过程如下:
- 全局平均池化:对于特征图U中的每个通道,计算其所有空间位置上的平均值。这样,每个通道就被压缩为一个单一的标量值,整个特征图U被压缩成一个维度为(1, 1, C)的特征向量Z。
- 通道描述符:这个特征向量Z可以看作是通道级别的全局特征描述符,每个元素代表了对应通道的全局特征响应。这些响应值反映了不同通道在全局范围内的特征重要性。
3. Excitation操作
Excitation操作基于Squeeze操作得到的通道描述符Z,进一步学习通道间的相互依赖关系,并为每个通道生成一个权重。这一操作通过两个全连接层(FC)和一个激活函数来实现,具体过程如下:
- 降维与升维:首先,通过第一个全连接层(FC)对通道描述符Z进行降维处理,减少通道数至C/r(r为降维比例,手动设置),以减少计算量和防止过拟合。然后,通过ReLU激活函数增加非线性。接下来,通过第二个全连接层将通道数恢复回C,以生成与原始通道数相同的权重向量S。
- Sigmoid激活:最后,通过Sigmoid激活函数将权重向量S中的每个元素压缩到0和1之间,得到一个归一化的权重向量。这些权重代表了不同通道的重要性程度,值越大表示该通道的特征越重要。
SE块通过Squeeze和Excitation这两个关键操作,实现了对通道关系的显式建模和特征重新校准,使网络能够自适应地调整不同通道的特征响应,从而显著提升卷积神经网络的性能。这种通道注意力机制在多个视觉任务中表现出了优异的性能,证明了其有效性和通用性。
这种通道注意力机制不仅提高了模型对特征选择的敏感性,还使得网络能够更加高效地利用有限的计算资源。通过自适应地调整通道特征的权重,SE块帮助深度学习模型在处理复杂任务时实现更加精准和高效的特征提取与表示,从而提升了模型的整体性能。
实现的SE模块的代码如下所示。
class SEBlock(torch.nn.Module):
def __init__(self, in_chnls, ratio):
super(SEBlock, self).__init__()
self.squeeze = torch.nn.AdaptiveAvgPool2d(1)
self.compress = torch.nn.Conv2d(in_chnls, in_chnls // ratio, kernel_size=1, stride=1, padding=0)
self.excitation = torch.nn.Conv2d(in_chnls // ratio, in_chnls, kernel_size=1, stride=1, padding=0)
def forward(self, x):
out = self.squeeze(x)
out = self.compress(out)
out = torch.nn.functional.silu(out) # 使用正确的激活函数名称
out = self.excitation(out)
out = torch.sigmoid(out) # 使用 torch.sigmoid 而不是 torch.nn.functional.sigmoid
# 将 out 缩放到与输入 x 相同的形状
return x * out
此时完整的加载SE模块的图像分类模型如下:
class MnistModel(torch.nn.Module):
def __init__(self):
super(MnistModel, self).__init__()
self.conv = torch.nn.Conv2d(1, 3, kernel_size=3, stride=1, padding=1)
self.senet = SEBlock(in_chnls=3, ratio=1)
self.logits_layer = torch.nn.Linear(75,10)
def forward(self, x):
image = self.conv(x)
image = self.senet(image)
image = torch.nn.Flatten()(image)
logits = self.logits_layer(image)
return logits
5.4.4 图像识别模型SEnet的训练与验证
接下来,我们将完成图像识别模型的训练与验证部分。在下面代码中,我们首先载入数据集,在运行36轮的训练后,我们可以完成对应的图像验证。
from torch.utils.data import DataLoader
from get_dataset import MNIST
import se_model
from tqdm import tqdm
import torch
DEVICE='cuda' if torch.cuda.is_available() else 'cpu' # 设备
BATCH_SIZE = 128
dataset=MNIST() # 数据集
model = se_model.MnistModel().to(DEVICE)
optimizer =torch.optim.AdamW(model.parameters(),lr=1e-5) # 优化器
loss_fn = torch.nn.CrossEntropyLoss()
dataloader=DataLoader(dataset,batch_size=BATCH_SIZE,shuffle=True) # 数据加载器
for epoch in range(36):
pbar = tqdm(dataloader, total=len(dataloader))
for imgs, labels in pbar:
imgs, labels = imgs.to(DEVICE), labels.to(DEVICE) #将数据和标签移动到指定的设备上
# 前向传播
outputs = model(imgs)
loss = loss_fn(outputs, labels)
# 反向传播和优化
optimizer.zero_grad() # 清空梯度
loss.backward() # 反向传播计算梯度
optimizer.step() # 更新模型参数
# 更新进度条信息
pbar.set_description(f"Epoch {epoch + 1}/{36}, Loss: {loss.item():.4f}")
model.eval() # 设置模型为评估模式
val_dataset = MNIST(is_train=False) # 验证数据集
val_dataloader = DataLoader(val_dataset, batch_size=BATCH_SIZE, shuffle=False) # 验证数据加载器,通常不需要打乱
correct = 0 # 正确预测的计数
total = 0 # 总样本数
with torch.no_grad(): # 不需要计算梯度,节省内存和计算资源
for imgs, labels in tqdm(val_dataloader, total=len(val_dataloader)):
imgs, labels = imgs.to(DEVICE), labels.to(DEVICE) #将数据和标签移动到指定的设备上
# 前向传播
outputs = model(imgs)
_, predicted = torch.max(outputs.data, 1) # 得到预测结果
# 计算准确率
total += labels.size(0) # 更新总样本数
correct += (predicted == labels).sum().item() # 更新正确预测的计数
# 计算并打印准确率
accuracy = 100 * correct / total
print(f'Validation Accuracy of the model on the test images: {accuracy:.2f}%')
训练结果请读者自行测试。