【简介】:基于flower_data使用PyTorch搭建AlexNet模型进行图片分类
【参考】:3.2 使用pytorch搭建AlexNet并训练花分类数据集_哔哩哔哩_bilibili
【代码完整版】:02 AlexNet (github.com)
注:本人还在学习初期,此文是为了梳理自己所学整理的,有些说法是自己的理解,不一定对,如有差错,请批评指正!
文章目录
1.搭建AlexNet模型
新建一个文件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=(w−kernel+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)#训练集中图片的数量
注意路径:
为了让预测时能获取到数据集的类别名称,我们这里还将数据集的类别名称写入到了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
拓展:
-
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])
-
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)