图像分类学习笔记(二)——AlexNet

AlexNet是一个经典的卷积神经网络模型,由Alex Krizhevsky等人在2012年提出。ImageNet ISVRC是计算机视觉领域里一个十分重要的比赛,AlexNet是2012年ISLVRC 2012竞赛的冠军,分类准确率由传统的70%+提升到80%+。也是在那年之后,深度学习开始迅速发展。

ISLVRC 2012

训练集:1,281,167张已标注图片

验证集:50,000张已标注图片

测试集:100,000张未标注图片

AlexNet共有8层网络结构,第1、2、5层使用较小的卷积核(11x11、5x5和3x3),并采用ReLU激活函数;第3、4层则使用池化层进行下采样;第6、7层是全连接层,最后一层是softmax分类层。此外,AlexNet还采用了一些增强训练效果的技巧,如局部响应归一化和随机失活等。

一、要点

  • 首次利用GPU进行网络加速训练
  • 为了防止过拟合,首次使用ReLUs作为激活函数(而不是传统的Sigmoid激活函数以及Tanh激活函数,缺点:求导麻烦、当网络比较深的时候会出现梯度消失的现象)。
  • 使用局部响应归一化(LRN),有助于快速收敛,增强了模型的泛化能力。(一般是在激活、池化后进行的一种处理方法)。
  • 在全连接层的前两层中使用了DropOut机制(丢弃法)随机失活神经元操作,以减少过拟合(可以理解为对模型的控制,因为模型更大了)
  • 过拟合:根本原因是特征维度过多,模型假设过于复杂,参数过多,训练数据过少,噪声过多,导致拟合的函数完美的预测训练集,但对新数据的测试集预测结果差。过度的拟合了训练数据,而没有考虑到泛化能力。
  • dropout可以理解为它变相地减少了网络训练的参数

二、模型架构

 上图包含了GPU通信的部分。这是由当时GPU内存的限制引起的,作者使用两块GPU进行计算,因此分为了上下两部分。但是,以目前GPU的处理能力,单GPU足够了,因此其结构图可以如下所示:

经卷积后的矩阵尺寸大小计算公式为:

N = (W - F + 2P)/ S + 1

  •  输入图片大小W*W
  • Filter大小F*F
  • 步长S
  • padding的像素数P

 padding [1,2] :特征矩阵左侧补1列零,右侧补2列零,上方补1行零,下方补2行零

三、使用pytorch搭建

(一)model.py

import torch.nn as nn
import torch


class AlexNet(nn.Module):
    def __init__(self, num_classes=1000, init_weights=False):
        super(AlexNet, self).__init__()
        self.features = nn.Sequential(
            # 数据集比较小且为了加快训练速度,所以把卷积核的个数96变为原论文的一半48,准确率基本一样
            nn.Conv2d(3, 48, kernel_size=11, stride=4, padding=2),  # input[3, 224, 224]  output[48, 55, 55]
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2),                  # output[48, 27, 27]
            nn.Conv2d(48, 128, kernel_size=5, 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, 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),
            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()

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

    # 初始化权重函数
    def _initialize_weights(self):
        for m in self.modules():
            # 判断该层结构是否是卷积层
            if isinstance(m, nn.Conv2d):
                # 应用 Kaiming 正态分布初始化权重
                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) # 均值为 0,标准差为 0.01
                nn.init.constant_(m.bias, 0) # 偏置项初始化为零
  • 关于参数padding:
    • 传入的类型只能是int整型或者tuple元组
    • 若传入的是int整型为1,则会在上下左右各补1行(列)零
    • 若传入的是tuple(1,2),则会在上下方各补1行零,左右两侧各补2列零

使用nn.ZeroPad2d((1,2,1,2)):左侧补1列,右侧补2列,上方补1行,下方补2行

  •  nn.Conv2d(3, 48, kernel_size=11, stride=4, padding=2)
    • 按照计算公式:尺寸N=(224-11+2*2)/4+1=55.25
    • 发现不是整数,pytorch会自动把特征矩阵最右侧的1列零,和最下方的1列零舍弃
    • 这样就和[二、模型架构]中的padding[1,2]类似
  •  nn.ReLU(inplace=True)
    • inplace可以理解为一种增加计算量但可以降低内存使用容量的方法,通过这种方法可以在内存里载入更大的模型
  • nn.Dropout(p=0.5)
    • 在正向传播过程中随机失活一部分神经元
    • p表示随机失活的比例
  • for m in self.modules():...

    •  迭代定义的每一个层结构

(二)train.py

import os
import sys
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")
    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))])}

    data_root = os.path.abspath(os.path.join(os.getcwd(), "../.."))  # get data root path
    image_path = os.path.join(data_root, "data_set", "flower_data")  # flower data set path
    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)

    # {'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())
    # write dict into json file
    # 将cla_dict编码成json的格式
    json_str = json.dumps(cla_dict, indent=4)
    with open('class_indices.json', 'w') as json_file:
        json_file.write(json_str)

    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=nw)

    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=False,
                                                  num_workers=nw)

    print("using {} images for training, {} images for validation.".format(train_num,
                                                                           val_num))
    # test_data_iter = iter(validate_loader)
    # test_image, test_label = test_data_iter.__next__()
    #
    # 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(utils.make_grid(test_image))


    net = AlexNet(num_classes=5, init_weights=True)
    net.to(device)
    loss_function = nn.CrossEntropyLoss()
    # pata = list(net.parameters()) # 查看模型的参数
    optimizer = optim.Adam(net.parameters(), lr=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()
        running_loss = 0.0
        train_bar = tqdm(train_loader, file=sys.stdout)
        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))
            loss.backward()
            optimizer.step()

            # print statistics
            running_loss += loss.item()

            train_bar.desc = "train epoch[{}/{}] loss:{:.3f}".format(epoch + 1,
                                                                     epochs,
                                                                     loss)

        # validate
        net.eval()
        acc = 0.0  # accumulate accurate number / epoch
        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))
                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()
  • device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
    • if torch.cuda.is_available():
          device = torch.device("cuda")
      else:
          device = torch.device("cpu")
  • transforms.RandomResizedCrop(224)
    • 将图像随机裁剪为不同的大小和宽高比
    • 然后缩放所裁剪得到的图像为指定的大小
  • transforms.RandomHorizontalFlip()
    • 以给定的概率随机水平旋转给定的PIL的图像,默认为0.5
  • os.path.abspath(os.path.join(os.getcwd(), "../.."))
    • os.path.abspath()返回绝对路径
    • os.getcwd()获取当前文件所在的目录
import os

# 获取当前工作目录的绝对路径
print(os.getcwd())
# 向上移动一级目录的绝对路径
print(os.path.abspath(os.path.join(os.getcwd(), "..")))
# 向上移动两级目录的绝对路径
print(os.path.abspath(os.path.join(os.getcwd(), "../..")))
# 向上移动三级目录的绝对路径
print(os.path.abspath(os.path.join(os.getcwd(), "../../..")))

  • assert os.path.exists(image_path)
    • 在一个程序还没完善或者测试好之前,我们不知道程序那里会出错,与其让他在运行时崩溃,不如在出现错误条件时就触发异常。assert可以在条件不满足程序运行的情况下直接返回错误,而不必等待程序运行后出现崩溃的情况
    • os.path.exists()判断括号里的文件是否存在
  • datasets.ImageFolder(root=os.path.join(image_path, "train"),
                                             transform=data_transform["train"])
    • 说明: 
      1)每个类别需要单独成立一个文件夹
      2)每个类别里面的图片需要按顺序排列(无论使用英语还是数字)
    • 参数:

    • 生成的对象有三个特性:

  •    # 获取分类名称对应的索引
        flower_list = train_dataset.class_to_idx
        # 调换键和值的顺序
        cla_dict = dict((val, key) for key, val in flower_list.items())

  • json_str = json.dumps(cla_dict, indent=4)

    • 使用 Python 内置的 json 模块中的 dumps 函数,将字典 cla_dict 转换为格式化的 JSON 字符串。indent=4 参数用于指定缩进的空格数,使得生成的 JSON 字符串更易读。

    • json库的一些方法
  • with open('class_indices.json', 'w') as json_file:
            json_file.write(json_str)
    • 打开一个名为 'class_indices.json' 的文件,模式为写入('w')
    • 将之前生成的 JSON 字符串写入打开的文件中,从而将字典数据以 JSON 格式保存到文件中
  • nw = min([os.cpu_count(), batch_size if batch_size > 1 else 0, 8])  
    • 作用是计算可用的并行线程数 nw,以便在某些多线程或多进程操作中使用
    • os.cpu_count():返回计算机可用的 CPU 核心数
    • batch_size if batch_size > 1 else 0:根据batch_size的值判断,如果 batch_size大于 1,则使用batch_size 的值,否则使用 0
    • min([os.cpu_count(), batch_size if batch_size > 1 else 0, 8]) :从给定的值列表中选取最小的值。这里给定了一个包含三个值的列表:可用 CPU 核心数、batch_size的值、以及最大允许的线程数8。最终,nw将设置为这三个值中的最小值。
  • net.train()
    • 启用 BatchNormalization 和 Dropout。 在模型测试阶段使用让net变成训练模式,此时 dropout和batch normalization的操作在训练q起到防止网络过拟合的问题。
  • net.eval()
    • 不启用 BatchNormalization 和 Dropout。此时pytorch会自动把BN和DropOut固定住,不会取平均,而是用训练好的值。不然的话,一旦test的batch_size过小,很容易就会因BN层导致模型performance损失较大
  • tqdm(train_loader, file=sys.stdout)

    • Tqdm是python进度条库,可以在Python长循环中添加一个进度提示信息。用户只需要封装任意的迭代器,是一个快速、扩展性强的进度条工具库。

    • tqdm用在dataloader上其实是对每个batch和batch总数做的进度条

​​​​​​​​​​​​​​

(三)predict.py

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 = "../tulip.jpg"
    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
    img = torch.unsqueeze(img, dim=0)

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

    with open(json_path, "r") as f:
        class_indict = json.load(f)

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

    # 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()
    with torch.no_grad():
        # predict class
        output = torch.squeeze(model(img.to(device))).cpu()
        predict = torch.softmax(output, dim=0)
        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)
    for i in range(len(predict)):
        print("class: {:10}   prob: {:.3}".format(class_indict[str(i)],
                                                  predict[i].numpy()))
    plt.show()


if __name__ == '__main__':
    main()

 

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值