利用pytorch实现AlexNet网络

目  录

1 AlexNet网络

1.1 网络结构

1.2 AlexNet网络各层详解

2 利用Pytorch实现AlexNet网络

2.1 模型定义

2.2 训练过程

2.3 预测部分


1 AlexNet网络

1.1 网络结构

该网络是2012年ISLVRC 2012竞赛的冠军网络,该网络的亮点在于:

①首次利用GPU进行网络加速训练;

②使用了ReLU激活函数;

③使用了LRN局部响应归一化;

④在全连接层的前两层中使用了Dropout随即失活神经元操作,以减少过拟合。


对于过拟合

左图是网络的初始状态;经过训练会得到中间的图,得到一个效果比较好的分类边界;最右面的图能够完美的预测训练集,但是对于其他数据的预测效果差,过度拟合了训练数据但是没有考虑到泛化能力。过拟合的原因有很多,比如特征维度过多,模型假设过于复杂,参数过多,训练数据过少,噪声过多等等。

为了解决该问题,AlexNet网络的Dropout操作在正向传播过程中随机失活了一部分神经元,减少了网络的训练参数,从而可以达到减少过拟合的效果。


1.2 AlexNet网络各层详解

kernels kernel_size padding stride output_size
Conv1 96 11 [1,2] 4 [55,55,96]
Maxpool1 \ 3 0 2 [27,27,96]
Conv2 256 5 [2,2] 1 [27,27,256]
Maxpool2 \ 3 0 2 [13,13,256]
Conv3 384 3 [1,1] 1 [13,13,384]
Conv4 384 3 [1,1] 1 [13,13,384]
Conv5 256 3 [1,1] 1 [13,13,256]
Maxpool3 \ 3 0 2 [6,6,256]
FC1 2048 \ \ \ 2048
FC2 2048 \ \ \ 2048
FC3 1000 \ \ \ 1000

(注:输入为[224,224,3],输出层节点数要根据我们自己分类类别个数调整)


2 利用Pytorch实现AlexNet网络

2.1 模型定义

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(
            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):
                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)

在初始化函数中定义网络正向传播过程中使用的层结构,使用了nn.Sequential模块,可以将一系列的层结构打包,组合成一个新的结构命名为features,专门用来提取图像的特征。若不使用该模块,每创建一个层结构都需要使用self.模块名称=该模块,会大大增加工作量。将全连接层利用nn.Sequential模块打包命名为classifier。

对于第一个卷积层,padding的值若为一个int(1),则表示在四周各补一列0;若为一个tuple(1, 2),则1代表在高度方向上(上方和下方)各补一行0,2代表在左右两侧各补两列0;若想精确的实现在左侧补一列,右侧补两列,上方补一列,下方补两列,需要用到nn.ZeroPad2d((1, 2, 1, 2))方法来实现更精细的操作。在此处为了方便输入一个整型2,但计算出来的H会是一个小数,此时Pytorch会自动将最右侧的一列0和最下侧的一行0舍弃掉,计算的输出是符合要求的。

然后经过一个激活函数nn.ReLU,inplace参数可以理解为Pytorch通过一种方法增加计算量,但是能够降低内存使用,可以通过该方法载入一个更大的模型。

再之后的层的设置和参数的设置同理,然后进入全连接层。AlexNet网络中使用了Dropout方法,一般放在全连接层与全连接层之间,在经过最后一个池化层之后,要将特征矩阵展平成一个一维向量与全连接层进行全连接,在这之间可以加入一个nn.Dropout函数,其中参数p代表随机失活神经元的比例,默认为0.5,在全连接层之后也要加上激活函数,最后一层的输出为数据集类别个数,在初始化函数中有定义默认值为1000,根据数据集不同可以传入不同的参数。

在模型定义时还定义了初始化权重的方法,在Pytorch中卷积层和全连接层是默认使用Kaiming初始化方法的,如果需要使用其他初始化方法也可以在此处定义。

在定义完网络组件之后,进行正向传播,首先将训练样本送入到features组件中,将得到的输出展平,注意是从第一维度(channel)开始展平,展成一个一维向量,再将其送入到classifier中,得到输出。


2.2 训练过程

通过一个函数来实现GPU训练

device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print("using {} device.".format(device))

若当前有可使用的GPU设备,就是用第一块GPU来进行训练,若没有则使用CPU训练。

然后对数据进行预处理:

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))])}

当Key为train时,RandomResizedCrop(224)将图片随机裁剪成224×224像素大小,RandomHorizontalFlip将图片在水平方向上随机翻转,然后转化成tensor类型并进行标准化处理;当Key为val时,将图片缩放成224×224的,也将图片转化成tensor类型并进行标准化处理。

读取训练集和测试集并且采用对应的图片处理方式:

train_dataset = datasets.ImageFolder(root="flower_data/train",
                                     transform=data_transform["train"])
validate_dataset = datasets.ImageFolder(root="flower_data/val",
                                        transform=data_transform["val"])

利用class_to_idx获取分类名称所对应的索引,并将key和val反过来,这样做预测完之后我们通过返回的索引,根据该字典能够直接直接获取到对应的类别,将该字典转换为json格式并写到json文件中(indent参数表示缩进几格),方便之后预测时读取类别:

# {'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
json_str = json.dumps(cla_dict, indent=4)
with open('class_indices.json', 'w') as json_file:
    json_file.write(json_str)

定义batch_size并载入训练集和测试集:

batch_size = 32
train_loader = torch.utils.data.DataLoader(train_dataset,
                                           batch_size=batch_size, shuffle=True,
                                           num_workers=0)
validate_loader = torch.utils.data.DataLoader(validate_dataset,
                                              batch_size=batch_size, shuffle=False,
                                              num_workers=0)

实例化AlexNet,注意将传入参数改为5;将网络放在指定的设备上训练,定义损失函数和优化器(使用Adam优化器)并指定学习率:

net = AlexNet(num_classes=5, init_weights=True)
net.to(device)
loss_function = nn.CrossEntropyLoss()
optimizer = optim.Adam(net.parameters(), lr=0.0002)

然后开始训练(代码已注释):

# 设定训练次数
epochs = 10
# 设置模型存储路径
save_path = './AlexNet.pth'
# 用来记录最优的正确率
best_acc = 0.0
# 一共有多少个batch(训练集数目 / batch_size),用于后面求平均损失
train_steps = len(train_loader)
# 开始迭代
for epoch in range(epochs):
    # train
    # 启用Dropout方法
    net.train()
    # 记录每一次的损失,所以每次迭代都清0
    running_loss = 0.0
    # 生成一个迭代器
    train_bar = tqdm(train_loader, file=sys.stdout)
    # 通过enumerate获得迭代器的索引和值
    for step, data in enumerate(train_bar):
        # 将值赋给图像和标签
        images, labels = data
        # 每次迭代都清空梯度
        optimizer.zero_grad()
        # 将图像送入到网络训练(注意添加到设备)
        outputs = net(images.to(device))
        # 将输出值和实际值做对比,计算损失(注意labels也要添加到设备)
        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
    # 禁用Dropout方法
    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))
            # 在outputs的第1维找最大值;返回的第0维是最大值,第1维是最大值对应的标签;[1]将标签赋给predict_y
            predict_y = torch.max(outputs, dim=1)[1]
            # 将预测值与实际值对比,相同返回1,否则返回0,求和可得正确的个数,通过item()获得其值
            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)

几个注意点:

net.train()net.eval()是为了保证Dropout方法只在训练过程中生效,在验证过程中不使用该方法;net.train()代表启用Dropout方法,net.eval()代表关闭Dropout方法。

tqdm()用于在python长循环中添加一个进度提示信息,只需要传入任意的迭代器即可tqdm(iterator),对于参数file=sys.stdout是打印输出到控制台(在pycharm中加了这个参数,进度条是白色的,不加该参数进度条是红色的,还没整明白,之后再查查)。

③通过item()获取值显示精度更高,item()返回的是一个浮点型数据,正常返回的是一个tensor类型,所以我们在求loss或者accuracy时,一般使用item()。


2.3 预测部分

训练完成后可以对图片进行预测:

# 用GPU训练
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

# 图片预处理:缩放,转换成tensor,归一化
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 = "sunflower1.png"
img = Image.open(img_path)
# 显示图片
plt.imshow(img)
# 对图片进行预处理,载入的图片是[H, W, C],经过预处理后会自动把深度提前[C, H, W]
img = data_transform(img)
# 要求的输入有四个维度,所以给图片加一个维度变为[N, C, H, W]
img = torch.unsqueeze(img, dim=0)

# 获取记录类别名称的json文件
json_path = 'class_indices.json'
# 解码成我们需要的字典
with open(json_path, "r") as f:
    class_indict = json.load(f)

# 初始化网络
model = AlexNet(num_classes=5).to(device)

# 载入权重
weights_path = "AlexNet.pth"
# 利用torch.load加载权重并利用model.load_state_dict()函数把加载的权重复制到模型的权重中去
model.load_state_dict(torch.load(weights_path))

# 禁用Dropout方法
model.eval()
# 不需要计算梯度也不进行反向传播
with torch.no_grad():
    # predict class
    # 将图片送入网络,利用squeeze注意要将tensor转换到CPU,因为后面的numpy是CPU-only
    output = torch.squeeze(model(img.to(device))).cpu()
    # 利用softmax函数使输出满足概率分布
    predict = torch.softmax(output, dim=0)
    # 获取概率最大处所对应的索引值
    predict_cla = torch.argmax(predict).numpy()

# 在记录类别名称的json文件中利用索引获取类别,并且利用索引获得类别对应的概率
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()

这里的torch.unsqueeze()函数torch.squeeze()函数需要注意:torch.unsqueeze是在dim对应的位置加入一个大小为1的维度(本来图片的shape是[3, 224, 224],在dim=0处加一维,shape变为[1, 3, 224, 224]),将图片送入到网络,加入的batch维不变,因此output的shape由[1, 3, 224, 224]变为[1, 5],此时的torch.squeeze()函数会移除张量维度里面有大小为1的部分,因此output的shape为[5]。

 上图为预测结果,我找了一张向日葵的图片,该模型预测图片有0.999的概率归属于向日葵。

猜你喜欢

转载自blog.csdn.net/AdjsWsgz/article/details/127338763