ALexNet 图像识别

1.AlexNet基本结构

  • 输入为RGB三通道的224 × 224 × 3大小的图像(也可填充为227 × 227 × 3 )
  • 8层,包含5 个卷积层和 3 个全连接层,每个卷积层都包含卷积核、偏置项、ReLU激活函数和局部响应归一化(LRN)模块
  • 1、2、5个卷积层后面跟着一个最大池化层
  • 最终输出层为softmax,将网络输出转化为概率值,用于预测图像的类别

 由于AlexNet结构复杂、参数很庞大,难以在单个GPU上进行训练。因此AlexNet采用两路GTX 580 3GB GPU并行训练。也就是说把原先的卷积层平分成两部分FeatureMap分别在两块GPU上进行训练(例如卷积层55x55x96分成两个FeatureMap:55x55x48)。上图中上部分和下部分是对称的

2.AlexNet创新

  • 2种数据增强(data augmentation)方法
  • 镜像反射和随机剪裁:先对图像做镜像反射,在原图和镜像反射的图(256×256)中随机裁剪227×227的区域,测试的时候,对左上、右上、左下、右下、中间分别做了5次裁剪,然后翻转,共10个裁剪,之后对结果求平均
  • 改变训练样本RGB通道的强度值:对RGB空间做PCA(主成分分析),然后对主成分做一个(0, 0.1)的高斯扰动,也就是对颜色、光照作变换,结果使错误率又下降了1%
  • 激活函数ReLU
    • 标准的神经元激活函数是tanh()函数,这种饱和的非线性函数在梯度下降的时候要比非饱和的非线性函数慢得多
  • 局部响应归一化
  • Dropout 抑制过拟合
  • 重叠池化
  • 双GPU训练
  • 端到端训练
    • CNN的输入直接是一张图片

 3.AlexNet各层解析

卷积+池化层(前五层)

输入层:AlexNet的输入图像尺寸是224x224x3。但是实际图像尺寸为227x227

卷积层C1:该层的处理流程是:卷积-->ReLU-->局部响应归一化(LRN)-->池化。
  • 卷积:输入是227x227x3,使用96个11x11x3的卷积核进行卷积,padding=0,stride=4根据公式:(input_size + 2 * padding - kernel_size) / stride + 1=(227+2*0-11)/4+1=55,得到输出是55x55x96
  • ReLU:将一对55×55×48的特征图分别放入ReLU激活函数,生成激活图。
  • 局部响应归一化:局部响应归一化层简称LRN,是在深度学习中提高准确度的技术方法。一般是在激活、池化后进行。LRN对局部神经元的活动创建竞争机制,使得其中响应比较大的值变得相对更大,并抑制其他反馈较小的神经元,增强了模型的泛化能力

  • 池化:使用3x3,stride=2的池化单元进行最大池化操作(max pooling)。这里使用的是重叠池化,即stride小于池化单元的边长。根据公式:(55+2*0-3)/2+1=27,每组得到的输出为27x27x48。
 卷积层C2:该层的处理流程是:卷积-->ReLU-->局部响应归一化(LRN)-->池化
  • 卷积:两组输入均是27x27x48,各组分别使用128个5x5x48的卷积核进行卷积,padding=2,stride=1,根据公式:(input_size + 2 * padding - kernel_size) / stride + 1=(27+2*2-5)/1+1=27,得到每组输出是27x27x128。
  • ReLU:将一对55×55×48的特征图分别放入ReLU激活函数,生成激活图
  • 局部响应归一化:使用参数k=2,n=5,α=0.0001,β=0.75进行归一化。每组输出仍然是27x27x128。
  • 池化:使用3x3,stride=2的池化单元进行最大池化操作(max pooling)。这里使用的是重叠池化,即stride小于池化单元的边长。根据公式:(27+2*0-3)/2+1=13,每组得到的输出为13x13x128。
卷积层C3:该层的处理流程是: 卷积-->ReLU
  • 卷积:输入是13x13x256,使用384个3x3x256的卷积核进行卷积,padding=1,stride=1,根据公式:(input_size + 2 * padding - kernel_size) / stride + 1=(13+2*1-3)/1+1=13,得到输出是13x13x384。
  • ReLU:将卷积层输出的FeatureMap输入到ReLU函数中。将输出其分成两组,每组FeatureMap大小是13x13x192,分别位于单个GPU上。
卷积层C4:该层的处理流程是:卷积-->ReLU
  • 卷积:两组输入均是13x13x192,各组分别使用192个3x3x192的卷积核进行卷积,padding=1,stride=1,根据公式:(input_size + 2 * padding - kernel_size) / stride + 1=(13+2*1-3)/1+1=13,每组FeatureMap输出是13x13x192
  • ReLU:将卷积层输出的FeatureMap输入到ReLU函数中。
卷积层C5:该层的处理流程是:卷积-->ReLU-->池化
  • 卷积:两组输入均是13x13x192,各组分别使用128个3x3x192的卷积核进行卷积,padding=1,stride=1,根据公式:(input_size + 2 * padding - kernel_size) / stride + 1=(13+2*1-3)/1+1=13,每组FeatureMap输出是13x13x128。
  • ReLU:将卷积层输出的FeatureMap输入到ReLU函数中。
  • 池化:使用3x3,stride=2的池化单元进行最大池化操作(max pooling)。这里使用的是重叠池化,即stride小于池化单元的边长。根据公式:(13+2*0-3)/2+1=6,每组得到的输出为6x6x128。

全连接层(后三层)

全连接层(FC6): 该层的流程为:(卷积)全连接 -->ReLU -->Dropout
  • 全连接:输入为6×6×256,使用4096个6×6×256的卷积核进行卷积,由于卷积核尺寸与输入的尺寸完全相同,即卷积核中的每个系数只与输入尺寸的一个像素值相乘一一对应,根据公式:(input_size + 2 * padding - kernel_size) / stride + 1=(6+2*0-6)/1+1=1,得到输出是1x1x4096。既有4096个神经元,该层被称为全连接层。
  • ReLU:这4096个神经元的运算结果通过ReLU激活函数中。
  • Dropout:随机的断开全连接层某些神经元的连接,通过不激活某些神经元的方式防止过拟合。4096个神经元也被均分到两块GPU上进行运算。
全连接层(FC7): (卷积)全连接 -->ReLU -->Dropout
  • 全连接:输入为4096个神经元,输出也是4096个神经元(作者设定的)。
  • ReLU:这4096个神经元的运算结果通过ReLU激活函数中。
  • Dropout:随机的断开全连接层某些神经元的连接,通过不激活某些神经元的方式防止过拟合。4096个神经元也被均分到两块GPU上进行运算。
输出层(output): (卷积)全连接 -->Softmax
  • 全连接:输入为4096个神经元,输出是1000个神经元。这1000个神经元即对应1000个检测类别。
  • Softmax:这1000个神经元的运算结果通过Softmax函数中,输出1000个类别对应的预测概率值。

 

4.用AlexNet实现对花朵的分类

数据集下载链接: http://download.tensorflow.org/example_images/flower_photos.tgz

模型搭建
import torch
import torch.nn as nn
import os 
import json 
import time
from tqdm import tqdm
from torchvision import transforms, datasets, utils 
import torch.optim as optim  
import matplotlib.pyplot as plt 
import numpy as np

'''模型搭建'''
class AlexNet(nn.Module): 
    def __init__(self, num_classes=1000, init_weights=False):  #num_classes决定的是数据集所需要分类的数量,init_weights决定是否初始化参数
        super(AlexNet, self).__init__()
        '''卷积层'''
        self.features = nn.Sequential(  #打包  
            nn.Conv2d(3, 48, kernel_size=11, stride=4, padding=2),  # input[3, 224, 224]  output[48, 55, 55] 自动舍去小数点后
            nn.ReLU(inplace=True), #inplace 可以载入更大模型
            nn.MaxPool2d(kernel_size=3, stride=2),                  # output[48, 27, 27] kernel_num为原论文一半
            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) #先进行特征提取,输出是128 × 6 × 6 
        x = torch.flatten(x, start_dim=1) #扁平化处理 或者view()
        x = self.classifier(x) #分类
        return x
    
    '''权重参数初始化,不同的层采用不同的方法'''
    def _initialize_weights(self):
        for m in self.modules():
            #卷积层:权重采用采用凯明初始化方法,偏差设为0
            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)
            #全连接层:权重采用正态分布均值为0,偏差为0.01的初始化,偏差设为0
            elif isinstance(m, nn.Linear):
                nn.init.normal_(m.weight, 0, 0.01)  #正态分布赋值
                nn.init.constant_(m.bias, 0)
net=AlexNet()
print(net)

数据集处理
'''将花朵数据集划分为训练集和验证集'''
import os
from shutil import copy
import random

def mkfile(file):
    if not os.path.exists(file):
        os.makedirs(file)

#遍历data/flower_photos目录下的子目录,排除以.txt结尾的文件,得到花朵的类别列表
file = 'data/flower_data/flower_photos'
flower_class = [cla for cla in os.listdir(file) if ".txt" not in cla] 

# 创建了一个名为data/train的文件夹,以及每个花朵类别对应的子文件夹,
# 在每个类别下创建名为data/val的文件夹和对应的子文件夹
mkfile('data/flower_data/train')
for cla in flower_class:
    mkfile('data/flower_data/train/'+cla)
mkfile('data/flower_data/val')
for cla in flower_class:
    mkfile('data/flower_data/val/'+cla)

#定义了一个变量split_rate,表示验证集占总数据集的比例
split_rate = 0.1

'''对每个花朵类别进行处理'''
for cla in flower_class:
    cla_path = file + '/' + cla + '/' #获取当前类别的路径cla_path
    images = os.listdir(cla_path) #获取该类别下的所有图像文件列表images
    num = len(images)
    eval_index = random.sample(images, k=int(num*split_rate))  # 计算验证集中需要抽样的图像数量int(num*split_rate)
    
    '''随机抽样从images中选取eval_index张图像作为验证集'''
    #遍历所有图像文件,如果当前图像在eval_index中,则将其复制到验证集文件夹中;否则,将其复制到训练集文件夹中。
    for index, image in enumerate(images):
        if image in eval_index:
            image_path = cla_path + image
            new_path = 'data/flower_data/val/' + cla
            copy(image_path, new_path)
        else:
            image_path = cla_path + image
            new_path = 'data/flower_data/train/' + cla
            copy(image_path, new_path)
        #在复制过程中,使用print语句显示处理进度
        print("\r[{}] processing [{}/{}]".format(cla, index+1, num), end="")  # processing bar
    print()
print("processing done!")

 

 定义训练所需函数
def main():
    '''使用GPU进行训练'''
    device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") 
    print("using {} device.".format(device))  
    
    '''设置数据集路径'''
    data_root = os.path.abspath(os.path.join(os.getcwd()))  #获取当前绝对路径
    image_path = os.path.join(data_root,  "data", "flower_data")  
    #使用os.path.join方法将数据集根目录的路径拼接起来,得到完整的花朵数据集路径,保存在image_path变量中
    assert os.path.exists(image_path), "{} path does not exist.".format(image_path)
    # 使用assert语句检查image_path是否存在,如果不存在,则会抛出异常信息
   
    batch_size = 32
    # nw = min([os.cpu_count(), batch_size if batch_size > 1 else 0, 8])  # number of workers
    nw = 0 #数据加载器的工作进程数
    print('Using {} dataloader workers every process'.format(nw))
    
    '''定义图像预处理操作'''
    data_transform = {  #transforms.Compose方法将多个预处理操作组合在一起
        # '''对于训练集先进行大小为224的随机裁剪,然后再水平方向上进行随机翻转,再转化为Tensor,最后再进行数据标准化'''
        "train": transforms.Compose([transforms.RandomResizedCrop(224),  # 随机裁剪224,将输入的图片随机裁剪为指定大小的图像
                                     transforms.RandomHorizontalFlip(),  # 水平方向上 随机翻转
                                     transforms.ToTensor(), #将图像转换为张量(Tensor)格式
                                     transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]), 
                                     #对图像进行归一化操作,将每个通道的像素值减去0.5,然后除以0.5,使得图像的像素值范围在[-1, 1]之间。
                                     #对于RGB图像,由于每个通道的像素值范围都是[0, 1],所以使用(0.5, 0.5, 0.5)作为mean和std参数,将每个通道的平均值设置为0.5,标准差也设置为0.5
        # '''对于验证集,我们只是验证其网络训练的准确性,因此没有必要再进行随即裁剪,翻转的操作,只需要将格式大小调整为224 × 224 224\times224224×224,再转化为Tensor,最后再进行数据标准化'''
        "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))])}
        
    '''通过datasets.ImageFolder函数加载训练数据集'''
    train_dataset = datasets.ImageFolder(root=os.path.join(image_path, "train"), #使用os.path.join方法将其与训练数据集文件夹名称拼接起来
                                         transform=data_transform["train"]) #transform参数则指定了对训练数据集进行预处理的函数,
                                                                            #使用了data_transform["train"]来获取之前定义的数据预处理函数
  
    train_num = len(train_dataset) #获取了训练数据集的样本数量
    train_loader = torch.utils.data.DataLoader(train_dataset,
                                               batch_size=batch_size, 
                                               shuffle=True, #打乱数据顺序
                                               num_workers=nw)
    
    '''保存训练数据集中的花朵分类索引和分类名称的映射关系,以便后续根据索引查找分类名称'''
    # {'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()) # 将键和值反过来,得到了花朵分类索引对应分类名称的字典
    json_str = json.dumps(cla_dict, indent=4)  # 将cla_dict字典编码成 JSON 格式的字符串,indent=4参数指定输出时的缩进量
    with open('7.2.2_class_indices.json', 'w') as json_file: # 保存到json文件中
        json_file.write(json_str)
    
    '''通过datasets.ImageFolder函数加载验证数据集'''
    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=batch_size, 
                                                  shuffle =False,
                                                  num_workers=nw)
    print("using {} images for training, {} images for validation.".format(train_num,
                                                                           val_num))
训练模型 
'''训练模型'''
    net = AlexNet(num_classes=5, init_weights=True)
    net.to(device) #将模型放到GPU上跑
    loss_function = nn.CrossEntropyLoss()
    # pata = list(net.parameters())
    optimizer = optim.Adam(net.parameters(), lr=0.0002) # 优化器

    epochs = 10
    save_path = './7.2.2_AlexNet.pth'
    best_acc = 0.0
    train_steps = len(train_loader)
    for epoch in range(epochs):
        net.train() # 在每个训练周期开始前,将模型设置为训练状态,在训练过程中, dropout起作用
        running_loss = 0.0 # 当前训练周期的累计损失
        train_bar = tqdm(train_loader) #创建了一个进度条,用于可视化显示训练进度
        t1 = time.perf_counter() 
        for step, data in enumerate(train_bar): #使用enumerate(train_bar)遍历训练数据加载器中的数据
            images, labels = data
            optimizer.zero_grad() #将优化器的梯度置零(optimizer.zero_grad()),以防止梯度累积
            outputs = net(images.to(device))
            loss = loss_function(outputs, labels.to(device))
            loss.backward()
            optimizer.step() #更新参数

            running_loss += loss.item()

            train_bar.desc = "train epoch[{}/{}] loss:{:.3f}".format(epoch + 1, epochs,loss)
            #更新进度条的描述信息,包括当前训练周期的索引、总训练周期数和当前批次的损失值
        print()
        print(time.perf_counter()-t1)
        
        '''在每个训练周期结束后,在验证集上进行模型的评估和保存'''
        net.eval()  # 将模型设置为评估状态,禁用 dropout
        acc = 0.0  # 累计正确分类的样本数量
        with torch.no_grad():
            val_bar = tqdm(validate_loader, colour='green') #创建一个进度条,对验证数据加载器中的数据进行遍历
            
            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() #统计预测正确的样本数量,并累加到acc中

        val_accurate = acc / val_num

        print('[epoch %d] train_loss: %.3f  val_accuracy: %.3f' %
              (epoch + 1, running_loss / train_steps, val_accurate))
        
        #当前的验证准确率大于之前的最佳准确率(best_acc),则更新最佳准确率并保存模型参数到指定路径(torch.save(net.state_dict(), save_path))
        if val_accurate > best_acc:
            best_acc = val_accurate
            torch.save(net.state_dict(), save_path)

    print('Finished Training')

if __name__ == '__main__':
    main()
预测 
'''预测'''
def test():
    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))])

    # 加载图片
    img_path = "./data/flower_data/val/daisy/1150395827_6f94a5c6e4_n.jpg"
    assert os.path.exists(img_path), "file: '{}' dose not exist.".format(img_path) #使用assert语句确保图像文件存在。
    img = Image.open(img_path).convert('RGB') # 使用PIL库打开图像,并转换为RGB格式
    plt.imshow(img) 
    
    # [N, C, H, W]
    img = data_transform(img) #应用之前定义的data_transform对图像进行数据转换
    
    # 对读入的图片扩充一个维度,因为 原训练格式为[N, C, H, W]
    img = torch.unsqueeze(img, dim=0) #扩展图像的维度,将其变成一个batch,以便与模型进行处理

    
    json_path = './7.2.2_class_indices.json'
    assert os.path.exists(json_path), "file: '{}' dose not exist.".format(json_path)
    json_file = open(json_path, "r")
    class_indict = json.load(json_file)

    # 创建模型
    model = AlexNet(num_classes=5).to(device)

    # 加载预训练的模型权重到模型中
    weights_path = "./7.2.2_AlexNet.pth"
    assert os.path.exists(weights_path), "file: '{}' dose not exist.".format(weights_path)
    model.load_state_dict(torch.load(weights_path))
    
    # 将模型设置为评估模式,并使用torch.no_grad()关闭梯度计算
    model.eval()
    with torch.no_grad():
        # 预测类别
        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)
    print(print_res)
    plt.show()
    
if __name__ == '__main__': 
    test()
结果展示 

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值