Pytorch图像分类:02使用PyTorch搭建AlexNet模型

简介】:基于flower_data使用PyTorch搭建AlexNet模型进行图片分类
【参考】:3.2 使用pytorch搭建AlexNet并训练花分类数据集_哔哩哔哩_bilibili
【代码完整版】:02 AlexNet (github.com)

注:本人还在学习初期,此文是为了梳理自己所学整理的,有些说法是自己的理解,不一定对,如有差错,请批评指正!


1.搭建AlexNet模型

LeNet
在这里插入图片描述
新建一个文件model.py,定义AlexNet类,它是继承于torch.nn.Module类的,首先定义__init__(),该函数定义了网络在正向传播过程中需要用到的一些层结构,与上一个构建LeNet模型不同的是,这里使用了 nn.Sequential()模块,它能将一系列的层结构进行打包,组合成一个新结构,当层结构比较多时,使用它能精简我们的代码。

import torch
from torch import nn

class AlexNet(nn.Module):
    def __init__(self,num_classes=1000,init_weight=False):
        super(AlexNet,self).__init__()
        self.features=nn.Sequential(
            nn.Conv2d(3,48,kernel_size=11,stride=4,padding=2),  # input[3, 224, 224]
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3,stride=2),               # output[48, 27, 27]

            nn.Conv2d(48,128,kernel_size=5,stride=1,padding=2), # output[128, 27, 27]
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3,stride=2),               # output[128, 13, 13]

            nn.Conv2d(128,192,kernel_size=3,stride=1,padding=1),# output[192, 13, 13]
            nn.ReLU(inplace=True),
            nn.Conv2d(192,192,kernel_size=3,stride=1,padding=1),# output[192, 13, 13]
            nn.ReLU(inplace=True),
            nn.Conv2d(192,128,kernel_size=3,stride=1,padding=1),# output[128, 13, 13]
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3,stride=2)                # output[128, 6, 6]
        )
        self.classifier = nn.Sequential(
            nn.Dropout(p=0.5),
            nn.Linear(128 * 6 * 6, 2048),
            nn.ReLU(inplace=True),
            nn.Dropout(p=0.5),
            nn.Linear(2048, 2048),
            nn.ReLU(inplace=True),
            nn.Linear(2048, num_classes),
        )
        if init_weights:
            self._initialize_weights()

解释:

  • 可以看到,init()里面主要有features和classifier 两个成员,这是因为将网络模型拆分了一下,或者说将网络层结构组合了一下,features主要包括了卷积层和池化层,它们的主要功能是进行特征提取,classifier主要包括了全连接层,功能是分类器。通过这样,我们可以清楚地看出AlexNet的网络结构为2x(卷积,池化)+3卷积+3全连接,这比LeNet的 2x(卷积,池化)+3全连接更复杂了。
  • 在代码中第一层卷积被设置为:
nn.Conv2d(3, 48, kernel_size=11, stride=4, padding=2),  # input[3, 224, 224]  output[48, 55, 55]

原本的卷积核个数为96,这里为了加快速度,设为48,这和原网络的准确率一样。
原本的padding应该为[1,2],也就是在左边补一列0,右边补两列0,上边补一行0,下边补两行0。
但在nn.Conv2d()里,padding只能有两种类型的变量,一是整形int,另一种是元组tuple类型,当传入为int类型时,会在图像的上下左右都补一行或一列0,当传入类型为tuple类型时,它里面只能有两个整形值,如(1,2),这表示会在上下补一列0,左右补两列0。
如果想要精确地在左侧补一列,右侧补两列,需要使用到官方的nn.ZeroPad2d()方法conv1=nn.ZeroPad2d((1,2,1,2))。
但是为了简便,这里设置为2,即四周都补上2行或两列0,按照公式 w 1 = ( w − k e r n e l + 2 p a d d i n g ) / s t r i d e + 1 w1=(w-kernel+2padding)/stride+1 w1=(wkernel+2padding)/stride+1算出w1=55.25,在Pytorch中,如果卷积或池化过程中算出的数据不是整数,它会自动将多余的数据所在的行和列(最后一行和最后一列)给舍弃掉,也就是它会将右侧和下侧的一列0给舍弃掉,这样就相当于在左侧补两列,右侧补一列,和之前说的左侧补一列,右侧补两列没什么太大的区别。

  • nn.ReLU(inplace=True)中的inplace参数,可以理解为PyTorch通过一种方法增加计算量,但是能够降低我们内存使用容量的一种方法,也就是说你能通过这个方法,在你的内存中载入更大的一个模型。
  • 在classifier 里面,全连接层之前会使用 nn.Dropout(p=0.5),这是因为 AlexNet使用了Dropout正则化,它能使全连接层的神经元以一定比例进行失活,以防止过拟合,p=0.5表示一个元素有0.5的概率为0(即失活)。

    在init()的最后,有一个初始化权重变量,我们默认为False。
    def _initialize_weights(self):
        for m in self.modules():#返回一个迭代器遍历我们定义的所有层结构
            if isinstance(m,nn.Conv2d):#如果是卷积层
                nn.init.kaiming_normal_(m.weight,mode='fan_out',nonlinearity='relu')#使用此函数进行初始化
                if m.bias is not None:
                    nn.init.constant_(m.bias,0)
            elif isinstance(m,nn.Linear):#如果是全连接层,则进行如下初始化
                nn.init.normal_(m.weight,0,0.01)
                nn.init.constant_(m.bias,0)

接着,就要定义forward()函数了,这就比较简单了,首先我们将输入进来的训练样本,输入到我们的features这么一个部件当中,得到它的输出之后,再将它进行一个展平处理,这里使用的是torch.Flatten(),start_dim=1,这里的1表示下标,从0开始,[batch,channel,height,width],batch不去动它,所以它展平的纬度是从第一维度channel开始的,即将channel高度宽度这三个维度展成一个一维向量,这里也可以用之前使用过的view。

    def forward(self,x):
        x=self.features(x)
        x=torch.flatten(x,start_dim=1)
        x=self.classifier(x)
        return x

至此,model.py文件就完成了,模型就搭建好了。

2.训练

2.1数据预处理

数据集介绍:
这里使用花卉数据集,包括5个类别。
数据集链接:flower-dataset(github.com)在这里插入图片描述
下载后解压数据集,再运行split_data.py进行数据集划分。
注:要将数据解压至"flower_data/"路径下,然后运行split_data.py才不会报错,此时,"flower_data/"路径下会多出两个文件夹,“train”和“val”,里面分别是5类花卉类名。

1)定义数据预处理函数

这里将训练集和测试集的预处理函数写成了一个。

import torch
import torchvision
from torchvision import transforms,datasets

device="cuda" if torch.cuda.is_available() else"cpu"
print("using {} device".format(device))

data_transform={
    "train":transforms.Compose([transforms.RandomResizedCrop(224),#随机裁剪
                                transforms.RandomHorizontalFlip(),#随机翻转
                                transforms.ToTensor(),
                                transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
                                ]),
    "val":transforms.Compose([transforms.Resize((224,224)),# cannot 224, must (224, 224)
                              transforms.ToTensor(),
                              transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
                              ])
}

在开始之前,获取了设备,查看是使用了cpu还是gpu,如果是gpu,则会打印"cuda"。

2)读取数据,进行数据预处理

import os
import

data_root=os.path.abspath(os.path.join(os.getcwd(),'../'))# get data root path
image_path=os.path.join(data_root,"data","flower_data") # get flower dataset
assert os.path.exists(image_path),"{} path does not exist".format(image_path)

train_dataset=datasets.ImageFolder(root=os.path.join(image_path,"train"),
                                   transform=data_transform["train"])

train_num=len(train_dataset)#训练集中图片的数量

注意路径:
root
为了让预测时能获取到数据集的类别名称,我们这里还将数据集的类别名称写入到了json文件中,具体代码如下:

import json

#{'daisy': 0, 'dandelion': 1, 'roses': 2, 'sunflowers': 3, 'tulips': 4}
flower_list=train_dataset.class_to_idx
#转变为-> {0: 'daisy', 1: 'dandelion', 2: 'roses', 3: 'sunflowers', 4: 'tulips'}
cla_dict=dict((val,key)for key,val in flower_list.items())
# write dict into json file
json_str=json.dumps(cla_dict,indent=4)
with open('class_indices.json','w') as json_file:
    json_file.write(json_str)

class_indices.json文件内容:

{
    "0": "daisy",
    "1": "dandelion",
    "2": "roses",
    "3": "sunflowers",
    "4": "tulips"
}

3)加载数据

batch_size=32
train_loader=torch.utils.data.DataLoader(train_dataset,batch_size=batch_size,shuffle=True,num_workers=0)

验证集图片直接按上面的思路写:

validate_dataset=datasets.ImageFolder(root=image_path+"/val",
                                  transform=data_transform["val"])
val_num=len(validate_dataset)
validate_loader=torch.utils.data.DataLoader(validate_dataset,batch_size=batch_size,shuffle=False,num_workers=0)

看一下数据集,将batch_size设为4,表示一个批次取4张图片,shuffle设为True,不然每次都是一样的花。看完之后将这段代码注释掉。

import torchvision
import matplotlib.pyplot as plt
import numpy as np

validate_loader=torch.utils.data.DataLoader(validate_dataset,batch_size=4,shuffle=True,num_workers=0)

test_data_iter = iter(validate_loader)
test_image, test_label = next(test_data_iter)

def imshow(img):
    img = img / 2 + 0.5  # unnormalize
    npimg = img.numpy()
    plt.imshow(np.transpose(npimg, (1, 2, 0)))
    plt.show()

print(' '.join('%5s' % cla_dict[test_label[j].item()] for j in range(4)))
imshow(torchvision.utils.make_grid(test_image))

在这里插入图片描述

2.2定义模型、损失函数、优化器

from model import AlexNet
from torch import nn

net=AlexNet(num_classes=5,init_weight=True)
net.to(device)
loss_function=nn.CrossEntropyLoss()
optimizer=torch.optim.Adam(net.parameters(),lr=0.002)

2.3模型训练

📌📌📌模型训练关键步骤为:开启训练模式-清空梯度-正向传播计算损失-反向传播-更新梯度

...
net.train()
...
optimizer.zero_grad()
loss=loss_function(outputs,labels)
optimizer.step()
...

🎯最简单的训练部分代码如下:

epoches=3
for epoch in range(epoches):
	# train
    net.train()
    running_loss=0.0
    for step,data in enumerate(train_loader):
        images,labels=data
        optimizer.zero_grad()
        outputs=net(images.to(device))
        loss=loss_function(outputs,labels.to(device))
        loss.backward()
        optimizer.step()

        # print statistic
        running_loss += loss.item()
        print("train epoch[{}/{}] loss:{:.3f}").format(epoch+1,epoches,loss)

但如果想要直观地查看训练的进度,可以加一个进度条,代码修改

from tqdm import tqdm

epoches=3
train_steps = len(train_loader)
for epoch in range(epoches):
	# train
    net.train()
    running_loss=0.0
	train_bar=tqdm(train_loader,file=sys.stdout)
	for step,data in enumerate(train_bar):
	...
	train_bar.desc="train epoch[{}/{}] loss:{:.3f}".format(epoch+1,epoches,loss)
	...

📌📌📌模型训练验证部分关键步骤为:开启评估模式-得到预测类别-计算准确率
计算准确率的核心都是通过将最大值的索引与label进行比较

...
net.eval()
...
pred=torch.max(outputs,dim=1)[1]#[batch,num_classes]
acc+=torch.eq(pred,val_labels.to(device)).sum().item()
#等价于
#acc+=(pred.argmax(1)==y).type(torch.float).sum().item()#正确的个数

...

验证部分的完整代码为:

    #train
    ...
    #validate
    net.eval()
    acc=0.0
    with torch.no_grad():
        val_bar=tqdm(validate_loader,file=sys.stdout)
        for val_data in val_bar:
            val_images,val_labels=val_data
            outputs=net(val_images.to(device))#[batch,num_classes]
            pred=torch.max(outputs,dim=1)[1]
            acc+=torch.eq(pred,val_labels.to(device)).sum().item()
    val_accurate=acc/val_num
    print('[epoch %d] training loss:%.3f val_accuracy:%.3f' %
          (epoch+1,running_loss/ train_steps,val_accurate))

    if val_accurate>best_acc:
        best_acc=val_accurate
        torch.save(net.state_dict(),save_path)
print("Finished Training!")

这里在最后还加上了一个保存准确率最大的模型的功能,要在训练开始前,加上如下代码:

best_acc=0.0
save_path='./AlexNet.pth'

至此,模型训练完成
在这里插入图片描述
拓展:如果要使用自己的数据集,如何做?
1.保持与本项目相同的数据集结构,即"data-train/val-images";
2.修改代码的num_classes为自己的类别数量

3.预测

3.1数据预处理

①定义数据预处理工具包

import torch
from torch import nn
from torchvision import transforms

device="cuda" if torch.cuda.is_available() else "cpu"

data_transform=transforms.Compose([
    transforms.Resize((224,224)),
    transforms.ToTensor(),
    transforms.Normalize((0.5,0.5,0.5),(0.5,0.5,0.5))
])

②准备数据

#load data
img_path="./tulip.jpg"
assert os.path.exists(img_path), "file: '{}' does not exist".format(img_path)
img=Image.open(img_path)#打开
plt.imshow(img)#在绘图区添加一个对象

img=data_transform(img)#[C,H,W]
img=torch.unsqueeze(img,dim=0)#增加一个维度->[N,C,H,W]

# read class_indice
json_path='./class_indices.json'
assert os.path.exists(json_path), "file: '{}' does not exist".format(json_path)

with open(json_path,"r") as file:
    class_indice=json.load(file)

这里多了一步读取json文件,是为了获得类别名称。

3.2模型

定义并加载模型:

#create model
model=AlexNet(num_classes=5).to(device)

#load model weights
weights_path="./AlexNet.pth"
assert os.path.exists(weights_path), "file: '{}' does not exist".format(weights_path)
model.load_state_dict(torch.load(weights_path))

3.3预测

这里最后使用了softmax函数,它能将预测的值映射到[0,1],这正好满足预测的概率值的范围为[0,1]。

model.eval()
with torch.no_grad():
    output=torch.squeeze(model(img.to(device)))#二维数组[1,num_classes]->一维[num_classes] :tensor([-2.1923, -4.5021,  1.2517, -2.1776,  1.8861])
    predict=torch.softmax(output,dim=0)#tensor([0.0108, 0.0011, 0.3386, 0.0110, 0.6385])
    predict_cla=torch.argmax(predict).numpy()#找到概率最大值的下标
print(class_indice[str(predict_cla)],predict[predict_cla].item())
plt.show()#显示创建的对象

输出>:tulip,0.6385
如果想要显示出对每个类别的预测概率,则修改成以下代码

# print(class_indice[str(predict_cla)],predict[predict_cla].item())
# plt.show()#显示创建的对象
print_res = "class: {}   prob: {:.3}".format(class_indice[str(predict_cla)],
                                             predict[predict_cla].numpy())
plt.title(print_res)
for i in range(len(predict)):
    print("class: {:10}   prob: {:.3}".format(class_indice[str(i)],
                                              predict[i].numpy()))
plt.show()

输出>:

class: daisy        prob: 0.0108
class: dandelion    prob: 0.00107
class: roses        prob: 0.339
class: sunflowers   prob: 0.011
class: tulips       prob: 0.639

在这里插入图片描述
拓展:

  1. torch.squeeze()torch.unsqueeze()
    它们用于在张量(tensor)中增加或减少维度。
    torch.squeeze()的用法非常简单,它只接受一个参数,进行张量的维度减少,具体而言,就是去除原始张量中那些为1的维度,如果原始张量 x 的维度是 (1, 3, 1, 5),而经过 squeeze 函数处理后,张量 y 的维度变为了 (3, 5)。下面是一个示例:

    import torch
    
    # 创建一个维度为 (1, 3, 1, 5) 的张量
    x = torch.randn(1, 3, 1, 5)
    
    # 使用 squeeze 函数减少维度
    y = torch.squeeze(x)
    
    print("原始张量 x 的维度:", x.size())
    print("减少维度后的张量 y 的维度:", y.size())
    
    
    原始张量 x 的维度: torch.Size([1, 3, 1, 5])
    减少维度后的张量 y 的维度: torch.Size([3, 5])
    

    torch.unsqueeze()的用法稍微复杂一些,它需要接受两个参数,第一个是要增加维度的张量,第二个是指定要增加的位置。下面是一个示例:

    import torch
    
    # 创建一个维度为 (3, 5) 的张量
    x = torch.randn(3, 5)
    
    # 使用 unsqueeze 函数增加维度
    y = torch.unsqueeze(x, dim=0)
    
    print("原始张量 x 的维度:", x.size())
    print("增加维度后的张量 y 的维度:", y.size())
    
    
    原始张量 x 的维度: torch.Size([3, 5])
    增加维度后的张量 y 的维度: torch.Size([1, 3, 5])
    
  2. plt.imshow()plt.show()

    最本质的区别/它们的使用方式在于:先使用 plt.imshow 来创建并配置图像,然后使用 plt.show 来实际显示图像。

    plt.imshow 和 plt.show 是 Matplotlib 库中用于显示图像的两个不同函数。虽然它们常常一起使用,但它们在功能和用途上有明显的区别。

    plt.imshow():

    功能: plt.imshow 用于在绘图区域显示一幅图像。它会创建一个图像对象,并将其添加到当前的绘图区域(即当前的 Axes 对象)中。
    使用场景: 当你有一个图像数据(如一个 NumPy 数组)并希望在绘图区域显示它时,可以使用 plt.imshow。例如,显示一个二维数组作为图像。

    import matplotlib.pyplot as plt
    import numpy as np
    
    # 创建一个随机的二维数组
    data = np.random.rand(10, 10)
    
    # 使用 plt.imshow 显示数组
    plt.imshow(data)
    

    plt.show():

    功能: plt.show 用于显示所有已创建的图形。它会打开一个图形窗口,并渲染当前所有的绘图对象。这是 Matplotlib 用于将绘图对象实际显示在屏幕上的方法。
    使用场景: 当你完成了所有绘图命令,并希望将图形显示在屏幕上时,可以使用 plt.show。在没有 plt.show 的情况下,绘图命令只是创建了图形对象,并不会真正显示。

    # 显示图像
    plt.show()
    

    为什么要同时用?

    结合使用的典型过程:
    先使用 plt.imshow 来创建并配置图像。
    然后使用 plt.show 来实际显示图像。

    原因:

    plt.imshow 创建图像对象并配置其属性,但不会显示图像。
    plt.show 实际上显示所有已经配置好的图形,包括由 plt.imshow 创建的图像。
    举个完整的例子:

    import matplotlib.pyplot as plt
    import numpy as np
    
    # 创建一个随机的二维数组
    data = np.random.rand(10, 10)
    
    # 使用 plt.imshow 显示数组
    plt.imshow(data)
    
    # 显示图像
    plt.show()
    

    在这个例子中,plt.imshow 创建了一个图像对象,plt.show 则将这个图像对象显示在屏幕上。这样,你就可以看到实际的图像输出。
    参考自:Matplotlib imshow() 方法 | 菜鸟教程 (runoob.com)

以下是使用PyTorch搭建AlexNet实现图像分类的示例代码,其中使用了CIFAR-10数据集。 ``` import torch import torch.nn as nn import torch.optim as optim import torch.nn.functional as F import torchvision import torchvision.transforms as transforms # 定义AlexNet模型 class AlexNet(nn.Module): def __init__(self): super(AlexNet, self).__init__() self.conv1 = nn.Conv2d(3, 64, kernel_size=11, stride=4, padding=2) self.pool1 = nn.MaxPool2d(kernel_size=3, stride=2) self.conv2 = nn.Conv2d(64, 192, kernel_size=5, padding=2) self.pool2 = nn.MaxPool2d(kernel_size=3, stride=2) self.conv3 = nn.Conv2d(192, 384, kernel_size=3, padding=1) self.conv4 = nn.Conv2d(384, 256, kernel_size=3, padding=1) self.conv5 = nn.Conv2d(256, 256, kernel_size=3, padding=1) self.pool5 = nn.MaxPool2d(kernel_size=3, stride=2) self.fc1 = nn.Linear(256 * 6 * 6, 4096) self.dropout1 = nn.Dropout() self.fc2 = nn.Linear(4096, 4096) self.dropout2 = nn.Dropout() self.fc3 = nn.Linear(4096, 10) def forward(self, x): x = F.relu(self.conv1(x)) x = self.pool1(x) x = F.relu(self.conv2(x)) x = self.pool2(x) x = F.relu(self.conv3(x)) x = F.relu(self.conv4(x)) x = F.relu(self.conv5(x)) x = self.pool5(x) x = x.view(-1, 256 * 6 * 6) x = F.relu(self.fc1(x)) x = self.dropout1(x) x = F.relu(self.fc2(x)) x = self.dropout2(x) x = self.fc3(x) return x # 加载CIFAR-10数据集 transform_train = transforms.Compose([ transforms.RandomCrop(32, padding=4), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) transform_test = transforms.Compose([ transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) trainset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform_train) trainloader = torch.utils.data.DataLoader(trainset, batch_size=128, shuffle=True, num_workers=2) testset = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform_test) testloader = torch.utils.data.DataLoader(testset, batch_size=128, shuffle=False, num_workers=2) # 定义损失函数和优化器 criterion = nn.CrossEntropyLoss() optimizer = optim.SGD(net.parameters(), lr=0.01, momentum=0.9, weight_decay=5e-4) # 训练模型 net = AlexNet() device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") net.to(device) for epoch in range(10): running_loss = 0.0 for i, data in enumerate(trainloader, 0): inputs, labels = data inputs, labels = inputs.to(device), labels.to(device) optimizer.zero_grad() outputs = net(inputs) loss = criterion(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() if i % 100 == 99: print('[%d, %5d] loss: %.3f' % (epoch + 1, i + 1, running_loss / 100)) running_loss = 0.0 print('Finished Training') # 测试模型 correct = 0 total = 0 with torch.no_grad(): for data in testloader: images, labels = data images, labels = images.to(device), labels.to(device) outputs = net(images) _, predicted = torch.max(outputs.data, 1) total += labels.size(0) correct += (predicted == labels).sum().item() print('Accuracy of the network on the 10000 test images: %d %%' % (100 * correct / total)) ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值