基于ResNet的图像分类--算法实现
- 数据准备
- ResNet
- 概述
- 核心思想
- 1. ResNet-18/34
- 2. ResNet-50/101/152
- 网络架构
- ResNet-34 示例结构:
- 各版本详细配置:
- ResNet34网络代码实现
- 数据增强
- 数据集封装Dataset类
- 数据集封装DataLoader类
数据准备
导包
fromPILimportImageimportcv2importnumpyasnpimportosfromsklearn.model_selectionimporttrain_test_splitfromsklearn.kernel_approximationimportAdditiveChi2Samplerimportrandomimportmathfromimutilsimportpaths读取数据
# 图像数据data=[]# 图像对应的标签labels=[]# 储存标签信息的临时变量labels_tep=[]# 数据集的地址# 请读者自行下载数据集,并将数据集放在代码的同级目录下image_paths=list(paths.list_images('E:\\jupyterNotebook\\Hands-on-CV-main\\10classify\\caltech-101'))forimage_pathinimage_paths:# 获取图像类别label=image_path.split(os.path.sep)[-2]# 读取每个类别的图像image=cv2.imread(image_path)# 将图像通道从BGR转换为RGBimage=cv2.cvtColor(image,cv2.COLOR_BGR2RGB)# 统一输入图像的尺寸image=cv2.resize(image,(200,200),interpolation=cv2.INTER_AREA)data.append(image)labels_tep.append(label)name2label={}tep={}foridx,nameinenumerate(labels_tep):tep[name]=idxforidx,nameinenumerate(tep):name2label[name]=idxforidx,image_pathinenumerate(image_paths):labels.append(name2label[image_path.split(os.path.sep)[-2]])data=np.array(data)labels=np.array(labels)数据集划分
(x_train,X,y_train,Y)=train_test_split(data,labels,test_size=0.4,stratify=labels,random_state=42)(x_val,x_test,y_val,y_test)=train_test_split(X,Y,test_size=0.5,random_state=42)print(f"x_train examples:{x_train.shape}\n\ x_test examples:{x_test.shape}\n\ x_val examples:{x_val.shape}")上面数据获取部分和前面一节是一致的,不再赘述
ResNet
概述
ResNet(Residual Network,残差网络)是2015年由微软研究院的何恺明等人提出的一种深度卷积神经网络架构。它通过引入残差学习(Residual Learning)解决了深度神经网络中的梯度消失/爆炸和退化问题,使得可以训练非常深的网络(超过100层)。
核心思想
- 残差块(Residual Block)
ResNet的核心创新是残差块,它通过"跳跃连接"(skip connection)将输入直接传递到输出:
输出 = F(x) + x其中:
x是输入F(x)是需要学习的残差映射F(x) + x通过跳跃连接实现
- 数学表达
传统网络:H(x) = F(x)
残差网络:H(x) = F(x) + x
目标学习:F(x) = H(x) - x(残差)
1. ResNet-18/34
使用基本的残差块:
Conv 3×3 ReLU Conv 3×3跳跃连接:如果输入输出维度相同,直接相加;如果维度不同,使用1×1卷积调整维度。
2. ResNet-50/101/152
使用瓶颈结构(Bottleneck):
Conv 1×1, 减少通道数 Conv 3×3, 主要计算 Conv 1×1, 恢复通道数网络架构
ResNet-34 示例结构:
输入 → Conv 7×7 → MaxPool ↓ [Residual Block ×3] 通道数:64 ↓ [Residual Block ×4] 通道数:128 ↓ [Residual Block ×6] 通道数:256 ↓ [Residual Block ×3] 通道数:512 ↓ 全局平均池化 → 全连接层 → 输出各版本详细配置:
| 网络层 | ResNet-18 | ResNet-34 | ResNet-50 | ResNet-101 | ResNet-152 |
|---|---|---|---|---|---|
| 卷积层1 | 7×7,64,stride2 | 7×7,64,stride2 | 7×7,64,stride2 | 7×7,64,stride2 | 7×7,64,stride2 |
| 池化层 | 3×3 max pool, stride2 | 3×3 max pool, stride2 | 3×3 max pool, stride2 | 3×3 max pool, stride2 | 3×3 max pool, stride2 |
| 卷积层2 | [3×3,64]×2 | [3×3,64]×3 | [1×1,64 3×3,64 1×1,256]×3 | [1×1,64 3×3,64 1×1,256]×3 | [1×1,64 3×3,64 1×1,256]×3 |
| 卷积层3 | [3×3,128]×2 | [3×3,128]×4 | [1×1,128 3×3,128 1×1,512]×4 | [1×1,128 3×3,128 1×1,512]×4 | [1×1,128 3×3,128 1×1,512]×8 |
| 卷积层4 | [3×3,256]×2 | [3×3,256]×6 | [1×1,256 3×3,256 1×1,1024]×6 | [1×1,256 3×3,256 1×1,1024]×23 | [1×1,256 3×3,256 1×1,1024]×36 |
| 卷积层5 | [3×3,512]×2 | [3×3,512]×3 | [1×1,512 3×3,512 1×1,2048]×3 | [1×1,512 3×3,512 1×1,2048]×3 | [1×1,512 3×3,512 1×1,2048]×3 |
| 参数数量 | 11.7M | 21.8M | 25.6M | 44.5M | 60.2M |
ResNet34网络代码实现
左图为传统网络结构,右图为残差网络结构
importtorchastfromtorchimportnnfromtorch.nnimportfunctionalasF# 残差结构classResidualBlock(nn.Module):# 深度学习中的图像和利用模型提取的特征图往往有很多通道(channel)# 比如RGB图像的通道为3,即R通道、B通道与G通道def__init__(self,inchannel,outchannel,stride=1,shortcut=None):super(ResidualBlock,self).__init__()# 观察图 ,不难发现残差结构可大致分为左右两部分# 左边是一系列的网络层级,右边是一个跳跃连接# 定义左边self.left=nn.Sequential(# 对应图 中第一个权重层nn.Conv2d(inchannel,outchannel,3,stride,1,bias=False),nn.BatchNorm2d(outchannel),# 对应图中的 RELUnn.ReLU(inplace=True),# 对应图中第二个权重层nn.Conv2d(outchannel,outchannel,3,1,1,bias=False),nn.BatchNorm2d(outchannel))# 定义右边self.right=shortcut# forward()函数在网络结构中起到举足轻重的作用,它决定着网络如何对数据进行传播defforward(self,x):out=self.left(x)# 构建残差结构residual=xifself.rightisNoneelseself.right(x)out+=residualreturnF.relu(out)# 在这一模块中,将实现 ResNet34classResNet(nn.Module):def__init__(self,num_classes=102):super(ResNet,self).__init__()# 前几层图像转换self.pre=nn.Sequential(nn.Conv2d(3,64,7,2,3,bias=False),nn.BatchNorm2d(64),nn.ReLU(inplace=True),nn.MaxPool2d(3,2,1),)# 重复的网络层分别有3、4、6、3个残差块self.layer1=self._make_layer(64,128,3)self.layer2=self._make_layer(128,256,4,stride=2)self.layer3=self._make_layer(256,512,6,stride=2)self.layer4=self._make_layer(512,512,3,stride=2)# 分类用的全连接层,将一个多通道的特征图映射到一个维度为类别数目的向量self.fc=nn.Linear(512,num_classes)def_make_layer(self,inchannel,outchannel,block_num,stride=1):# 定义shortcut连接shortcut=nn.Sequential(nn.Conv2d(inchannel,outchannel,1,stride,bias=False),nn.BatchNorm2d(outchannel))layers=[]# 给当前网络层级添加残差块layers.append(ResidualBlock(inchannel,outchannel,stride,shortcut))foriinrange(1,block_num):layers.append(ResidualBlock(outchannel,outchannel))returnnn.Sequential(*layers)defforward(self,x):x=self.pre(x)x=self.layer1(x)x=self.layer2(x)x=self.layer3(x)x=self.layer4(x)x=F.avg_pool2d(x,7)x=x.view(x.size(0),-1)returnself.fc(x)数据增强
# 定义训练图像增强(变换)的方法train_transform=transforms.Compose([transforms.ToPILImage(),# transforms.Resize((224, 224)),transforms.ToTensor(),transforms.Normalize(mean=[0.485,0.456,0.406],std=[0.229,0.224,0.225])])# 定义训练图像增强(变换)的方法val_transform=transforms.Compose([transforms.ToPILImage(),# transforms.Resize((224, 224)),transforms.ToTensor(),transforms.Normalize(mean=[0.485,0.456,0.406],std=[0.229,0.224,0.225])])数据集封装Dataset类
# 数据集classImageDataset(Dataset):def__init__(self,images,labels=None,transforms=None):self.X=images self.y=labels self.transforms=transformsdef__len__(self):return(len(self.X))# 用于深度学习训练构建的数据类中,__getitem__()函数非常重要# __getitem__()决定着数据如何传入模型# 在下面的代码中,可以发现,当transforms非空时:# 数据将先经过transforms进行数据增强,再返回进行后续操作def__getitem__(self,i):data=self.X[i][:]ifself.transforms:data=self.transforms(data)ifself.yisnotNone:return(data,self.y[i])else:returndata生成dataset类
# 生成不同的类用于训练、验证以及测试train_data=ImageDataset(x_train,y_train,train_transform)val_data=ImageDataset(x_val,y_val,val_transform)test_data=ImageDataset(x_test,y_test,val_transform)数据集封装DataLoader类
BATCH_SIZE=128trainloader=DataLoader(train_data,batch_size=BATCH_SIZE,shuffle=True)valloader=DataLoader(val_data,batch_size=BATCH_SIZE,shuffle=True)testloader=DataLoader(test_data,batch_size=BATCH_SIZE,shuffle=False)