目录

1.数据集简介
2.模型相关知识
3.split_data.py——训练集与测试集划分
4.model.py——定义AlexNet网络模型
5.train.py——加载数据集并训练,训练集计算损失值loss,测试集计算accuracy,保存训练好的网络参数
6.predict.py——利用训练好的网络参数后,用自己找的图像进行分类测试

一、数据集简介

有 5 种类型(雏菊,蒲公英,玫瑰,向日葵,郁金香)的花,每种类型有600~900张图像
数据集下载https://storage.googleapis.com/download.tensorflow.org/example_images/flower_photos.tgz

二、模型相关知识

之前有文章介绍模型,如果不清楚可以点下链接转过去学习
卷积神经网络CNN里经典网络模型之 AlexNet全网最详解(理论篇)

三、split_data.py——训练集与测试集划分

由于此数据集不像 CIFAR10 那样下载时就划分好了训练集和测试集,因此需要自己划分。

split_data.py
第一种情况是同一目录

import os
from shutil import copy
import random
 
def mkfile(file):
    if not os.path.exists(file):
        os.makedirs(file)
        
# 获取 flower_photos 文件夹下除 .txt 文件以外所有文件夹名(即5种花的类名)
file_path = 'flower_data/flower_photos'
flower_class = [cla for cla in os.listdir(file_path) if ".txt" not in cla] 
 
# 创建 训练集train 文件夹,并由5种类名在其目录下创建5个子目录
mkfile('flower_data/train')
for cla in flower_class:
    mkfile('flower_data/train/'+cla)
    
# 创建 验证集val 文件夹,并由5种类名在其目录下创建5个子目录
mkfile('flower_data/val')
for cla in flower_class:
    mkfile('flower_data/val/'+cla)
 
# 划分比例,训练集 : 验证集 = 9 : 1
split_rate = 0.1
 
# 遍历5种花的全部图像并按比例分成训练集和验证集
for cla in flower_class:
    cla_path = file_path + '/' + cla + '/'  # 某一类别花的子目录
    images = os.listdir(cla_path)		    # iamges 列表存储了该目录下所有图像的名称
    num = len(images)
    eval_index = random.sample(images, k=int(num*split_rate)) # 从images列表中随机抽取 k 个图像名称
    for index, image in enumerate(images):
    	# eval_index 中保存验证集val的图像名称
        if image in eval_index:					
            image_path = cla_path + image
            new_path = 'flower_data/val/' + cla
            copy(image_path, new_path)  # 将选中的图像复制到新路径
           
        # 其余的图像保存在训练集train中
        else:
            image_path = cla_path + image
            new_path = 'flower_data/train/' + cla
            copy(image_path, new_path)
        print("\r[{}] processing [{}/{}]".format(cla, index+1, num), end="")  # processing bar
    print()
 
print("processing done!")

第二种情况此数据集放在前两级目录

import os
from shutil import copy, rmtree
import random

def mk_file(file_path: str):
    if os.path.exists(file_path):
        # 如果文件夹存在,则先删除原文件夹在重新创建
        rmtree(file_path)
    os.makedirs(file_path)


def main():
    # 保证随机可复现
    random.seed(0)

    # 将数据集中10%的数据划分到验证集中
    split_rate = 0.1

    # 指向你解压后的flower_photos文件夹
    cwd = os.getcwd()
    data_root = os.path.join(cwd, "flower_data")
    origin_flower_path = os.path.join(data_root, "flower_photos")
    assert os.path.exists(origin_flower_path), "path '{}' does not exist.".format(origin_flower_path)

    flower_class = [cla for cla in os.listdir(origin_flower_path)
                    if os.path.isdir(os.path.join(origin_flower_path, cla))]

    # 建立保存训练集的文件夹
    train_root = os.path.join(data_root, "train")
    mk_file(train_root)
    for cla in flower_class:
        # 建立每个类别对应的文件夹
        mk_file(os.path.join(train_root, cla))

    # 建立保存验证集的文件夹
    val_root = os.path.join(data_root, "val")
    mk_file(val_root)
    for cla in flower_class:
        # 建立每个类别对应的文件夹
        mk_file(os.path.join(val_root, cla))

    for cla in flower_class:
        cla_path = os.path.join(origin_flower_path, cla)
        images = os.listdir(cla_path)
        num = len(images)
        # 随机采样验证集的索引
        eval_index = random.sample(images, k=int(num*split_rate))
        for index, image in enumerate(images):
            if image in eval_index:
                # 将分配至验证集中的文件复制到相应目录
                image_path = os.path.join(cla_path, image)
                new_path = os.path.join(val_root, cla)
                copy(image_path, new_path)
            else:
                # 将分配至训练集中的文件复制到相应目录
                image_path = os.path.join(cla_path, image)
                new_path = os.path.join(train_root, cla)
                copy(image_path, new_path)
            print("\r[{}] processing [{}/{}]".format(cla, index+1, num), end="")  # processing bar
        print()

    print("processing done!")


if __name__ == '__main__':
    main()

(1)先将数据集压缩包解压到data_set文件夹中的flower_data中
(2)在data_set目录下执行 )shift + 右键 打开 PowerShell 或者ctrl+B cmd窗口,
(3)执行 “split_data.py” 分类脚本自动将数据集划分成 训练集train 和 验证集val

四、model.py——定义AlexNet网络模型

(1)pytorch 中 Tensor 参数的顺序为 (batch, channel, height, width)
(2)卷积的参数为Conv2d(in_channels, out_channels, kernel_size, stride, padding, …),一般这5个参数用的多。
(3)卷积池化层提取图像特征,全连接层进行图像分类,代码中写成两个模块,方便调用
(4)为了加快训练,代码只使用了一半的网络参数,相当于只用了原论文中网络结构的下半部分。

import torch.nn as nn
import torch

class AlexNet(nn.Module):
    #类ALEXNET继承nn.module这个父类
    def __init__(self, num_classes=1000, init_weights=False):
        #通过初始化函数,定义网络在正向传播过程中需要使用的层结构
        #num_classes是指输出的图片种类个数,init_weights=False意味着不定义模型中的初始权重
        super(AlexNet, self).__init__()
        #nn.Sequential模块,可以将一系列的层结构进行打包,组合成一个新的结构,
        # 将专门用于提取图像特征的结构的名称取为features
        self.features = nn.Sequential(
            nn.Conv2d(3, 48, kernel_size=11, stride=4, padding=2),
            # input[3, 224, 224]  output[48, 55, 55]
            #第一个卷积层,彩色图片深度为3,卷积核个数位48,卷积核大小11,步长4,padding2
            nn.ReLU(inplace=True),
            #使用Relu激活函数时要将设置inplace=True
            #使用relu激活函数,f(x)=max(0,x),
            #相比sigmod函数与tanh函数有以下几个优点
            # 1)克服梯度消失的问题
            # 2)加快训练速度
            # 注:正因为克服了梯度消失问题,训练才会快
            # 缺点:
            # 1)输入负数,则完全不激活,ReLU函数死掉。
            # 2)ReLU函数输出要么是0,要么是正数,也就是ReLU函数不是以0为中心的函数

            nn.MaxPool2d(kernel_size=3, stride=2),                  # output[48, 27, 27]
            nn.Conv2d(48, 128, kernel_size=5, padding=2),#步长默认为1,当步长为1时不用设置# 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, padding=1),          # output[192, 13, 13]
            nn.ReLU(inplace=True),
            nn.Conv2d(192, 192, kernel_size=3, padding=1),          # output[192, 13, 13]
            nn.ReLU(inplace=True),
            nn.Conv2d(192, 128, kernel_size=3, 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),#随机将一半的节点失活,默认为0.5
            nn.Linear(128 * 6 * 6, 2048),#将特征矩阵展平,128*6*6最后输出的长**高,2048为全连接层节点个数
            nn.ReLU(inplace=True),#Relu激活函数
            nn.Dropout(p=0.5),
            nn.Linear(2048, 2048),#全连接层2的输入为全连接层1的输出2048,全连接层2的节点个数2048
            nn.ReLU(inplace=True),
            nn.Linear(2048, num_classes),#全连接层3的输入为全连接层2的输出2048
            #全连接层最后的输出就是图片类别的个数
        )
        if init_weights:
            self._initialize_weights()
        #是否初始化权重,如果初始化函数中的init_weights=Ture,就会进入到初始化权重的函数


    def forward(self, x):
        #forward正向传播过程,x为输入的变量
        x = self.features(x)
        #将训练样本输入features
        x = torch.flatten(x, start_dim=1)
        #将输入的变量进行展平从深度高度宽度三个维度进行展开,索引从1开始,展成一个一维向量
        x = self.classifier(x)
        #将展平后的数据输入到分类器中(三个全连接层组成的)
        return x#最后的输出为图片类别

    #初始化权重的函数
    def _initialize_weights(self):
        for m in self.modules():#遍历self.modules这样一个模块,该模块继承自它的父类nn.module,该模块会迭代每一个层次
            if isinstance(m, nn.Conv2d):#如果该层次是一个卷积层,就会使用kaiming_normal_这样一个方法初始化权重
                nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
                if m.bias is not None:
                    nn.init.constant_(m.bias, 0)#如果偏值不为空的话,就用0对权重进行初始化
            elif isinstance(m, nn.Linear):#如果该层次是一个全连接层,就用normal进行初始化
                nn.init.normal_(m.weight, 0, 0.01)#正态分布对权重进行赋值,均值为0,方差为0.01
                nn.init.constant_(m.bias, 0)#设置全连接层的偏值为0

五、train.py——模型训练,加载数据集并训练,训练集计算损失值loss,测试集计算accuracy,保存训练好的网络参数

import os
import json
import torch
import torch.nn as nn
from torchvision import transforms, datasets, utils
import matplotlib.pyplot as plt
import numpy as np
import torch.optim as optim
from tqdm import tqdm
from model import AlexNet


def main():
    device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
    #r如果当前有可使用的gpu,默认使用第一块gpu设备。如果没有gpu就使用cpu设备
    print("using {} device.".format(device))

    data_transform = {
    	#该方法为数据预处理方法
        #当关键字为train时,返回训练集的数据与处理方法
        "train": transforms.Compose([transforms.RandomResizedCrop(224),#将图片用随机裁剪方法裁剪成224*224
                                     transforms.RandomHorizontalFlip(),#在水平方向随机翻转
                                     transforms.ToTensor(),#将它转化成tnesor
                                     transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]),
                                     #将数据进行标准化处理

        #当关键字为val时,返回训练集的数据与处理方法
        "val": transforms.Compose([transforms.Resize((224, 224)),#将图片转化成224*224大小
                                   transforms.ToTensor(),#将数据转化成tensor
                                   transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
                                   #将数据进行标准化处理
                                   }

    data_root = os.path.abspath(os.path.join(os.getcwd(), "../.."))  
    #返回到上一级目录的上一级目录,获取数据的根目录
    image_path = os.path.join(data_root, "data_set", "flower_data")  
    #再进入到data_set下的flower_data文件夹下
    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,使用训练集的数据处理方法处理数据
    train_num = len(train_dataset)#将训练集中的图片个数赋值给train_num

    # {'daisy':0, 'dandelion':1, 'roses':2, 'sunflower':3, 'tulips':4}
    flower_list = train_dataset.class_to_idx#获取分类名称所对应的索引
    cla_dict= dict((val, key) for key, val in flower_list.items()
    #遍历所获取的分类以及索引的字典,并且将key,values交换位置
    
    # write dict into json file
    json_str = json.dumps(cla_dict, indent=4) #将字典编码成json格式
    with open('class_indices.json', 'w') as json_file:
        json_file.write(json_str)

    batch_size = 32#定义batch_size=32
    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 = torch.utils.data.DataLoader(train_dataset,
                                               batch_size=batch_size, shuffle=True,
                                               num_workers=0)
    #train_loader函数是为了随机在数据集中获取一批批数据,num_workers=0加载数据的线程个数,在windows系统下该数为                 0,意思为在windows系统下使用一个主线程加载数据

    validate_dataset = datasets.ImageFolder(root=os.path.join(image_path, "val"),
                                            transform=data_transform["val"])
    val_num = len(validate_dataset)
    validate_loader = torch.utils.data.DataLoader(validate_dataset,
                                                  batch_size=4, shuffle=True,
                                                  num_workers=0)

    print("using {} images for training, {} images for validation.".format(train_num,
                                                                           val_num))
	 net = AlexNet(num_classes=5, init_weights=True)#num_classes=5花有5种类别,初始化权重

    net.to(device)#将该网络分配到制定的设备上(gpu或者cpu)
    loss_function = nn.CrossEntropyLoss()#定义损失函数,针对多类别的损失交叉熵函数
    # pata = list(net.parameters())
    optimizer = optim.Adam(net.parameters(), lr=0.0002)
    #定义一个Adam优化器,优化对象是所有可训练的参数,定义学习率为0.0002,通过调试获得的最佳学习率

    epochs = 10
    save_path = './AlexNet.pth'#保存准确率最高的那次模型的路径
    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 = tqdm(train_loader)

        for step, data in enumerate(train_bar):#遍历数据集
            images, labels = data#将数据分为图像标签
            optimizer.zero_grad()#清空之前的梯度信息
            outputs = net(images.to(device))#通过正向传播的到输出
            loss = loss_function(outputs, labels.to(device))#指定设备gpu或者cpu,通过Loss_function函数计算预测值与真实值之间的差距
            loss.backward()#将损失反向传播到每一个节点
            optimizer.step()#通过optimizer更新每一个参数

            # print statistics
            running_loss += loss.item()#累加损失
            #print train process
            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),end="")
        print()


        # validate
        net.eval()#预测过程中使用net.eval()函数,该函数会关闭掉dropout
        acc = 0.0  # accumulate accurate number / epoch
        with torch.no_grad():#使用该函数,禁止pytorch对参数进行跟踪,即训练过程中不会计算损失梯度
            val_bar = tqdm(validate_loader)
            for val_data in val_bar:#遍历验证集
                val_images, val_labels = val_data#将数据划分为图片和标签
                outputs = net(val_images.to(device))
                predict_y = torch.max(outputs, dim=1)[1]#求的预测过程中最有可能的标签
                acc += torch.eq(predict_y, val_labels.to(device)).sum().item()#准确的个数累加

        val_accurate = acc / val_num#测试集准确率
        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('Finished Training')


if __name__ == '__main__':
    main() 

训练流程
1.选择设备(GPU或者CPU)
2.定义处理数据的方法,训练集和测试集的数据处理方式不同,对训练集的预处理,多了3.随机裁剪和水平翻转这两个步骤。可以起到扩充数据集的作用,增强模型泛化能力。
4.读取图片数据
5.处理数据
6.将图片的索引字典存储成json文件,并且将索引和种类调换位置,以便输出时清晰明了
7.实例化类Alexnet
8.分配设备
9.数据处理
10.模型训练
11.将准确度最高的模型参数保存下来

训练结果如下

using cpu device.
Using 8 dataloader workers every process
using 3306 images for training, 364 images for validation.
train epoch[1/10] loss:1.469: 100%|██████████| 104/104 [01:08<00:00,  1.52it/s]
100%|██████████| 91/91 [00:16<00:00,  5.58it/s]
[epoch 1] train_loss: 1.367  val_accuracy: 0.426
train epoch[2/10] loss:1.269: 100%|██████████| 104/104 [01:01<00:00,  1.70it/s]
100%|██████████| 91/91 [00:15<00:00,  5.75it/s]
[epoch 2] train_loss: 1.210  val_accuracy: 0.519
train epoch[3/10] loss:1.265: 100%|██████████| 104/104 [01:01<00:00,  1.70it/s]
100%|██████████| 91/91 [00:15<00:00,  5.70it/s]
[epoch 3] train_loss: 1.129  val_accuracy: 0.582
train epoch[4/10] loss:1.000: 100%|██████████| 104/104 [01:01<00:00,  1.69it/s]
100%|██████████| 91/91 [00:15<00:00,  5.83it/s]
[epoch 4] train_loss: 1.025  val_accuracy: 0.635
train epoch[5/10] loss:0.906: 100%|██████████| 104/104 [01:01<00:00,  1.70it/s]
100%|██████████| 91/91 [00:15<00:00,  5.86it/s]
[epoch 5] train_loss: 0.991  val_accuracy: 0.635
train epoch[6/10] loss:0.901: 100%|██████████| 104/104 [01:01<00:00,  1.69it/s]
100%|██████████| 91/91 [00:15<00:00,  5.70it/s]
[epoch 6] train_loss: 0.944  val_accuracy: 0.654
train epoch[7/10] loss:0.841: 100%|██████████| 104/104 [01:01<00:00,  1.69it/s]
100%|██████████| 91/91 [00:15<00:00,  5.71it/s]
[epoch 7] train_loss: 0.914  val_accuracy: 0.651
train epoch[8/10] loss:0.771: 100%|██████████| 104/104 [01:00<00:00,  1.71it/s]
100%|██████████| 91/91 [00:15<00:00,  5.75it/s]
[epoch 8] train_loss: 0.869  val_accuracy: 0.670
train epoch[9/10] loss:0.835: 100%|██████████| 104/104 [01:00<00:00,  1.71it/s]
100%|██████████| 91/91 [00:15<00:00,  5.85it/s]
[epoch 9] train_loss: 0.861  val_accuracy: 0.690
train epoch[10/10] loss:0.672: 100%|██████████| 104/104 [01:00<00:00,  1.71it/s]
100%|██████████| 91/91 [00:15<00:00,  5.89it/s]
[epoch 10] train_loss: 0.808  val_accuracy: 0.690
Finished Training

Process finished with exit code 0

六、predict.py——利用训练好的网络参数后,用自己找的图像进行分类测试

在网上下载了一郁金香的图片,使用Alexnet网络查看是否可以将图片种类正确识别。

import os
import json

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

from model import AlexNet


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
    img_path = "1.jpg"#在python库中载入图片
    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 = AlexNet(num_classes=5).to(device)#初始化网络,类别为5

    # load model weights
    weights_path = "./AlexNet.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()

预测结果截图

其他参考链接和视频学习
https://blog.csdn.net/qq_45649076/article/details/120467508
https://blog.csdn.net/m0_37867091/article/details/107150142
https://blog.csdn.net/qq_62932195/article/details/122094111