手码图像分类网络AlexNet 学习记录和教程

本文介绍了如何使用Python和PyTorch实现AlexNet模型进行图像分类,包括模型结构解析、数据预处理、训练过程和模型预测。作者以实际代码示例展示了AlexNet在手写字识别网络中的应用,适合初学者学习。
摘要由CSDN通过智能技术生成

1、前言

        这篇文章是我在学习python、C++、数据结构与算法分析、《智能建造基础算法教程》(中国建筑工业出版社,刘界鹏、周绪红等著)后,同时在完成“手写字识别”网络的搭建和训练之后,以本平台另一位博主的文章(深度学习pytorch实战二:AlexNet图像分类篇且官网提供花数据集分五类_alexnet 分类_Studying 开龙wu的博客-CSDN博客)为学习蓝本完成。本文的目的是自己学习的一个记录,同时也想给和我一样跨专业的人后者非科班的人一个学习的指导,或许站在一个初学者的角度写的文章更适合初学者看。

2、AlexNet简介

他的由来和具体的优势劣势这里就不赘述,这篇文章默认大家具备基本的深度学习知识,明白python的基本语法,会简单的pytorch的基本操作。

        以上图片是Alexnet网络的经典分析图,这里希望初学者都能自己推到一边,推到的核心在于理解卷积、池化的意义,同时这里明确卷积和池化过程中,原图的H,W和结果图的H,W的关系式:

W=(W^{\textup{*}}-K+2*P)/S+1 

W: 卷积(池化)后图像的宽(一般情况下H=W)

W\textup{*}: 原图的宽(一般情况下原图H=W)

K: 卷积核的大小

P: padding值

S: stride值

(注意:看来很多资料,上述公示当W算出是一个小数时,采取向上取整,但是在使用torch.nn实践时发现采用的是向下取整)

模型分析

这里以参考作者的代码举例

从图中可以看到输入的图像


Input layer: 3x224x224--(kernel: size=(11,11),stride=(4,4))—>C1: (224-11+2*0)/4+1=(48x54x54);


C1: (3x54x54)——(kernel: size=(3,3), stride=(4,4))——>C2: (54-3+2*0)/2+1=(48x26x26);


C2: (48x26x26)——(kernel:size=(5,5),padding=2)——>C3: (26-5+2*2)/1+1 = (128x26x26)


C3: (128x26x26)——(kernel: size=(3,3),stride=(2,2)——C4: (26-3+2*0)/2+1=(128x12x12)


………

后面的以此类推

3、Model.py

import torch
import torch.nn  as nn

class myAlexNet(nn.Module):
    def __init__(self,num_classes):
        super(myAlexNet,self).__init__()

        # nn.Sequential()可以将各层集合成一个代码块

        # 将专门用于提取图像特征的结构的名称取为features
        self.feature = nn.Sequential(
            #the first Conveluton  input:(3,224,224)->out:(48,54,54)
            nn.Conv2d(in_channels=3,out_channels=48,kernel_size=(11,11),stride=(4,4)),
            nn.ReLU(inplace=True),
            # the first MaxPool input:(48,54,54)->out:(48,26,26)
            nn.MaxPool2d(kernel_size=(3,3),stride=(2,2)),
            #the second Convelution input:(48,26,26)->out:(128,26,26)
            nn.Conv2d(in_channels=48,out_channels=128,kernel_size=(5,5),padding=2),
            nn.ReLU(inplace=True),
            #the second MaxPool input:(128,26,26)->out:(128,12,12)
            nn.MaxPool2d(kernel_size=(3,3),stride=(2,2)),
            #the third Convelution input:(128,12,12)->(192,12,12)
            nn.Conv2d(in_channels=128,out_channels=192,kernel_size=(3,3),padding=1),
            nn.ReLU(inplace=True),
            #the forth Convelution input:(192,12,12)->out:(192,12,12)
            nn.Conv2d(in_channels=192,out_channels=192,kernel_size=(3,3),padding=1),
            nn.ReLU(inplace=True),
            #the fifth Convelution input:(192,12,13)->out:(128,12,12)
            nn.Conv2d(in_channels=192,out_channels=128,kernel_size=(3,3),padding=1),
            nn.ReLU(inplace=True),
            # the third MaxPool input:(128,12,12)->out:(128,5,5)
            nn.MaxPool2d(kernel_size=(3,3),stride=2)
        )

        # 将三个全连接层打包成一个新的模块,分类器
        self.classifier = nn.Sequential(
            #训练过程中随机将张量元素置为0,以避免过拟合
            # nn.Dropout(p=0.5),
            #将矩阵展平
            nn.Linear(128*5*5,2048),
            nn.ReLU(inplace=True),
            # nn.Dropout(p=0.5),
            #第二个全连接层
            nn.Linear(2048, 2048),
            nn.ReLU(inplace=True),
            #输出结果
            nn.Linear(2048,num_classes)
         )

    def forward(self,x):
        #将样本放入feature
        x = self.feature(x)
        # print(x.shape)
        #将x展开成一维向量,从axis=0开始,即全部展开
        x = torch.flatten(x,start_dim=1)
        #将展开后的数据输入到分类器中
        x = self.classifier(x)
        return x

4、train.py

import json
import os.path
import matplotlib.pyplot  as plt
import torch
from torchvision import transforms,datasets
from torch.utils import data
from net import myAlexNet
from PIL import Image
import numpy as np
from tqdm import tqdm
import cv2
# 图像显示
def showImage(path):

    image = cv2.imread(path)
    cv2.imshow(image)


# 确定训练的机器
device = 'cuda' if torch.cuda.is_available() else 'cpu'
print(f"这次调用的是{device}用于训练")

# 图像预处理的方式
imgTransforms = {
    "train":transforms.Compose([
        transforms.RandomResizedCrop((224,224)),#将输入的图片随机裁剪成224,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)),  # 将输入的图片大小改为成224,224的大小
        transforms.ToTensor(),
        transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])
    ]
    )
}

img_path =os.path.join('./dataset','train')

train_datasets = datasets.ImageFolder(root=img_path,transform=imgTransforms['train'])
train_number = len(train_datasets)
# ImageFolder返回的类型的三个属性,classes,class_to_idx,imgs
# print(train_datasets.classes)  #根据分的文件夹的名字来确定的类别
#train_datasets.classes: ['daisy', 'dandelion', 'roses', 'sunflowers', 'tulips']
# print(train_datasets.class_to_idx) #按顺序为这些类别定义索引为0,1...
# class_to_idx {'daisy': 0, 'dandelion': 1, 'roses': 2, 'sunflowers': 3, 'tulips': 4}
# print(train_datasets.imgs) #返回从所有文件夹中得到的图片以及其类别
# imgs:[('./dataset\\train\\daisy\\100080576_f52e8ee070_n.jpg', 0), ……]注意返回地元组中0直接是PIL.Image类型
# print(type(train_datasets[0][0]))
# plt.figure()
# plt.imshow(train_datasets[0][0])
# plt.show()


# 遍历所获取的分类以及索引的字典,并且将key,values交换位置
odl_dict = train_datasets.class_to_idx
new_dict = dict((value,key) for key,value in odl_dict.items())
# print(new_dict) {0: 'daisy', 1: 'dandelion', 2: 'roses', 3: 'sunflowers', 4: 'tulips'}

# write dict into jsonfile
josncontent =  json.dumps(new_dict)#将字典变成字符串,json.load()将字符串变成字典
with open('class_indices.json','w') as f:
    f.write(josncontent)



batch_size = 16#定义batch_size=16[B,N,W,C]
nw = min([os.cpu_count(), batch_size if batch_size > 1 else 0, 8])  # number of workers
print('Using {} dataloader workers every process'.format(nw))

# train_loader函数是为了随机在数据集中获取一批批数据,num_workers=0加载数据的线程个数,在windows系统下该数为
train_loader = data.DataLoader(train_datasets,
                               batch_size=batch_size,
                               shuffle=True,
                               num_workers=0)

val_path = './dataset/val'
val_dataset = datasets.ImageFolder(val_path,transform=imgTransforms['val'])
val_number = len(val_dataset)
val_loader = torch.utils.data.DataLoader(val_dataset,
                                         batch_size=4,
                                         shuffle=True,num_workers=0)

print("using {} images for training, {} images for validation.".format(train_number,val_number))


net = myAlexNet(num_classes=5,init_weights=False)
net.to(device)
# 定义一个交叉熵损失函数
loss_fn = torch.nn.CrossEntropyLoss()
# 定义一个优化器
optimizer = torch.optim.SGD(net.parameters(),lr=0.01,momentum=0.9)


epochs = 35
save_path = './model/AlaxNet.pth'
if not os.path.exists('./model'):
    os.mkdir('model')
best_acc = 0.0
train_steps = len(train_loader)
for epoch in range(epochs):
    #train:
    net.train()#使用net.train()方法,该方法中有dropout
    running_loss = 0.0  # 使用running_loss方法统计训练过程中的平均损失
    train_bar = train_loader

    for step,data in enumerate(train_bar):
        #step:(0,1,2,3……),data=tensor
        image,label = data
        optimizer.zero_grad()
        output = net(image.to(device))
        #计算损失值
        loss = loss_fn(output,label.to(device))
        #将损失反向传播(反向计算每一个节点的导数)
        loss.backward()
        optimizer.step()#更新参数

        running_loss += loss.item()
        # 打印输出
        rate = (step + 1) / len(train_loader)
        a = "*" * int(rate * 50)
        b = "." * int((1 - rate) * 50)
        print("\rtrain loss:{:^3.0f}%[{}->{}]{:.3f}".format(int(rate * 100), a, b, loss.item()), end="")
    print()

    # validate
    net.eval()#预测过程中使用net.eval()函数,该函数会关闭掉dropout
    acc = 0.0
    with torch.no_grad():#使用该函数,禁止pytorch对参数进行跟踪,即训练过程中不会计算损失梯度

        for val_data in val_loader:
            val_img,val_label = val_data
            val_output = net(val_img.to(device))
          #此函数输出两个tensor,1.第一个tensor是每行的最大概率,2.第二个tensor是每行最大概率的索引
            predict_y = torch.max(val_output,dim=1)[1]#拿到每行最大可能性的索引
            # print("val_output:",val_output)
            # print("val_label:",val_label)
            # print("predict_y:",predict_y)

            #val_output: tensor([
            #[ 0.0119,  0.0119,  0.0224, -0.0134, -0.0169],每一行代表一张图片的5个类别的预测可能性,val的batchsize为4所以是四行
            #[ 0.0101,  0.0111,  0.0230, -0.0140, -0.0166],
            #[ 0.0113,  0.0118,  0.0236, -0.0133, -0.0158],
            #[ 0.0106,  0.0123,  0.0236, -0.0145, -0.0165]], device='cuda:0')
            #
            #predict_y: tensor([2, 2, 2, 2], device='cuda:0')通过torch.max拿到了每一行的最大值的索引
            #val_label: tensor([0, 4, 1, 3])标签对应的四张图片的对应类别


            acc += torch.eq(predict_y,val_label.to(device)).sum().item()#比较两个tensor对应元素是否一样,一样的返回1,然后累加

    val_accurate = acc / val_number  # 测试集准确率
    print('[epoch %d] train_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("All DONE")

数据组织

这里最大的难点在于数据的组织,建议仔细看torch.torchvision.datasets.ImageFolder()函数,

import os
import random
from shutil import copy

def mkDir(path):
    if not os.path.exists(path):
        os.mkdir(path)




split_rate = 0.1
root_path = './flower_photos'
img_dir = [cls for cls in os.listdir(root_path) if '.txt'  not in cls]

# 创建训练集文件夹并在其子目录下创建五个子目录
mkDir('train')
for i in img_dir:
    mkDir(os.path.join('train',i))

# 创建训练集文件夹并在其子目录下创建五个子目录
mkDir('val')
for i in img_dir:
    mkDir(os.path.join('val',i))

for i in img_dir:
    img_path = os.path.join('./flower_photos/',i)
    image = os.listdir(img_path)
    num_img = len(image)

    # 随机在image中抽取特定数字的照片作为验证集
    val_idex = random.sample(image,k=int(split_rate*num_img))
    for index,img in enumerate(image):
        # val_index 中保存验证集val的图像名称
        if img in val_idex:
            old_path = os.path.join(img_path,img)
            # print("old_path:",old_path)
            new_path = os.path.join(f'./val/{i}',img)
            # print("new_path:",new_path)
            copy(old_path,new_path)
        else:
            old_path = os.path.join(img_path,img)
            new_path = os.path.join(f'./train/{i}',img)
            copy(old_path,new_path)

文件组织:

5、predict.py

import os
import json

import torch
from PIL import Image
from torchvision import transforms
import matplotlib.pyplot as plt

from net import myAlexNet


def main():
    device = torch.device("cuda:0" 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 image
    f = open('./dataset/flower_photos/val.txt')
    content = f.readlines()
    for i in content:

        img_path = i[:-3]
        assert os.path.exists(img_path), "file: '{}' dose not exist.".format(img_path)#判断图片是否存在
        img = Image.open(img_path)

        plt.imshow(img)#展示图片
        # [N, C, H, W]

        img = data_transform(img)#对图片进行预处理操作
        # expand batch dimension  #batch是指一次处理图片的数量,批
        img = torch.unsqueeze(img, dim=0)#处理后变成[batch, C, H, W]

        # read class_indict
        json_path = './class_indices.json'#读取索引对应的类别名称
        assert os.path.exists(json_path), "file: '{}' dose not exist.".format(json_path)#将json文件解码成字典模式

        json_file = open(json_path, "r")
        class_indict = json.load(json_file)

        # create model
        model = myAlexNet(num_classes=5).to(device)#初始化网络,类别为5

        # load model weights
        weights_path = "./model/AlaxNet.pth"
        assert os.path.exists(weights_path), "file: '{}' dose not exist.".format(weights_path)
        model.load_state_dict(torch.load(weights_path))#载入网络模型

        model.eval()#进入eval模式,即关闭dropout方法
        with torch.no_grad():#让变量不去跟踪模型的损失梯度
            # predict class
            output = torch.squeeze(model(img.to(device))).cpu()#通过正向传播得到输出,并将输出进行压缩,将batch维度压缩
            predict = torch.softmax(output, dim=0)#通过softmax处理之后变成概率分布
            predict_cla = torch.argmax(predict).numpy()#获取概率最大处对应的索引值

        print_res = "class: {}   prob: {:.3}".format(class_indict[str(predict_cla)],
                                                     predict[predict_cla].numpy())#打印类别名称以及预测正确的概率
        plt.title(print_res)
        print(print_res)
        plt.show()


if __name__ == '__main__':
    main()

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值