Windows下,Pytorch使用Imagenet-1K训练ResNet的经验(有代码)

感谢中科院,感谢东南大学,感谢南京医科大,感谢江苏省人民医院以的赞助

题记

只有被ImageNet真正殴打过一次才算是真的到了深度学习的坑边,下一步才是入坑。

引用装备所兰海大佬的一句话:
能借鉴别人经验的一定要借鉴别人的经验,不要人家已经过河了,你还假装在河里摸石头,别装了。

1. 下载Imagent-1k数据集

首先,ImageNet数据集大家都可以通过百度云下载。
可以通过我这便的链接下载。

链接:https://pan.baidu.com/s/1NlenXev0cN1l55ZSVQ-_nw
提取码:d6tn
我这个链接下载下来的是迅雷的磁力链接,下载下来后用迅雷下载会很快。没办法,百度云会员太贵了。

一般来讲,直接下载下来的train数据集是每个类都在一个文件夹中的,但是val数据集是所图像在一个数据集中的,你需要自己与预处理一下。
关于预处理数据集的方法可以查看我这个博客,我已经处理好了。

2. 大家ResNet模型

在这里,我把ResNet模型的ResNet18,ResNet34,ResNet50,ResNet101等都集成到一个代码中了。很方便调用。

直接上代码。

# -*- coding: utf-8 -*-
"""
Created on Wed Dec 22 10:31:58 2021

@author: DELL
"""


from __future__ import print_function, division
import torch.nn.functional as F
import matplotlib.pyplot as plt
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
from torchvision import datasets, transforms
from torchvision import models
from torch.autograd import Variable
import torchsummary as summary
import os
import csv
import codecs
import numpy as np
import time
from thop import profile

os.environ['KMP_DUPLICATE_LIB_OK'] = 'TRUE'
'''定义超参数'''
EPOCH = 150
batch_size=512
classes_num=1000
learning_rate=1e-3

'''定义Transform'''
 #对训练集做一个变换
train_transforms = transforms.Compose([
    transforms.Resize(224),
    transforms.RandomResizedCrop(224),		#对图片尺寸做一个缩放切割
    transforms.RandomHorizontalFlip(),		#水平翻转
    transforms.ToTensor(),					#转化为张量
    transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225))	#进行归一化
])
#对测试集做变换
val_transforms = transforms.Compose([
    transforms.Resize(224),
    transforms.RandomResizedCrop(224),
    transforms.ToTensor(),
    transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225))
])

train_dir = "D:/Dateset/Alldataset/Imagenet/ILSVRC2012_img_train"           #训练集路径
#train_dir = "D:/Dateset/Alldataset/mini-imagenet/train"
#train_dir = "D:/2021year/CVPR/PermuteNet-main/CNNonMNIST/data/trainNum_T/test"
#定义数据集
train_datasets = datasets.ImageFolder(train_dir, transform=train_transforms)
#加载数据集
train_dataloader = torch.utils.data.DataLoader(train_datasets, batch_size=batch_size, shuffle=True,num_workers=8,pin_memory=True)#,num_workers=16,pin_memory=False

#val_dir = "D:/Dateset/Alldataset/mini-imagenet/val"
#val_dir = "D:/2021year/CVPR/PermuteNet-main/CNNonMNIST/data/trainNum_T/val"
val_dir = "D:/Dateset/Alldataset/Imagenet/ILSVRC2012_img_val1"
val_datasets = datasets.ImageFolder(val_dir, transform=val_transforms)
val_dataloader = torch.utils.data.DataLoader(val_datasets, batch_size=batch_size, shuffle=True,num_workers=8,pin_memory=True)#,num_workers=16,pin_memory=True


class BasicBlock(nn.Module):
    '''这个函数适用于构成ResNet18和ResNet34的block'''
    expansion = 1
    def __init__(self, in_planes, planes, stride=1):
        super(BasicBlock, self).__init__()
        self.conv1 = nn.Conv2d(in_planes, planes, kernel_size=3, stride=stride, padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(planes)
        self.conv2 = nn.Conv2d(planes, planes, kernel_size=3,stride=1, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(planes)
        self.shortcut = nn.Sequential()
        if stride != 1 or in_planes != self.expansion*planes:
            self.shortcut = nn.Sequential(
                nn.Conv2d(in_planes, self.expansion*planes,
                          kernel_size=1, stride=stride, bias=False),
                nn.BatchNorm2d(self.expansion*planes)
            )

    def forward(self, x):
        out = torch.relu(self.bn1(self.conv1(x)))
        out = self.bn2(self.conv2(out))
        out += self.shortcut(x)
        out = torch.relu(out)
        return out


class Bottleneck(nn.Module):
    '''这个函数适用于构成ResNet50及更深层模型的的block'''
    expansion = 4

    def __init__(self, in_planes, planes, stride=1):
        super(Bottleneck, self).__init__()
        self.conv1 = nn.Conv2d(in_planes, planes, kernel_size=1, bias=False)
        self.bn1 = nn.BatchNorm2d(planes)
        self.conv2 = nn.Conv2d(planes, planes, kernel_size=3,stride=stride, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(planes)
        self.conv3 = nn.Conv2d(planes, self.expansion *planes, kernel_size=1, bias=False)
        self.bn3 = nn.BatchNorm2d(self.expansion*planes)

        self.shortcut = nn.Sequential()
        if stride != 1 or in_planes != self.expansion*planes:
            self.shortcut = nn.Sequential(nn.Conv2d(in_planes, self.expansion*planes,kernel_size=1, stride=stride, bias=False),
                                          nn.BatchNorm2d(self.expansion*planes))
    def forward(self, x):
        out = torch.relu(self.bn1(self.conv1(x)))
        out = torch.relu(self.bn2(self.conv2(out)))
        out = self.bn3(self.conv3(out))
        out += self.shortcut(x)
        out = torch.relu(out)
        return out


class ResNet(nn.Module):
    def __init__(self, block, num_blocks, num_classes=classes_num):
        super(ResNet, self).__init__()
        self.in_planes = 64
        self.conv1 = nn.Conv2d(3, 64, kernel_size=3,stride=2, padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(64)
        self.relu = nn.ReLU(inplace=False)
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
        self.layer1 = self._make_layer(block, 64, num_blocks[0], stride=1)
        self.layer2 = self._make_layer(block, 128, num_blocks[1], stride=2)
        self.layer3 = self._make_layer(block, 256, num_blocks[2], stride=2)
        self.layer4 = self._make_layer(block, 512, num_blocks[3], stride=2)
        self.avgpool = nn.AvgPool2d(7, stride=1)
        self.classifier = nn.Linear(512*block.expansion, num_classes)#

    def _make_layer(self, block, planes, num_blocks, stride):
        strides = [stride] + [1]*(num_blocks-1)
        layers = []
        for stride in strides:
            layers.append(block(self.in_planes, planes, stride))
            self.in_planes = planes * block.expansion
        return nn.Sequential(*layers)

    def forward(self, x):
        out = torch.relu(self.bn1(self.conv1(x)))
        out_x1 = self.relu(out)
        out_x2 = self.maxpool(out_x1)
        out1 = self.layer1(out_x2)    #56*56      4
        out2 = self.layer2(out1)    #28*28        4
        out3 = self.layer3(out2)    #14*14        4
        out4 = self.layer4(out3)   #(512*7*7)     4
        #out5 = F.avg_pool2d(out4, 4)#平均池化
        out5 = self.avgpool(out4 )
        out6 = out5.view(out5.size(0),-1)#view()函数相当于numpy中的reshape
        out7 = self.classifier(out6)#平均池化全连接分类
        return out7


def ResNet18():
    return ResNet(BasicBlock, [2, 2, 2, 2])
def ResNet34():
    return ResNet(BasicBlock, [3, 4, 6, 3])
def ResNet50():
    return ResNet(Bottleneck, [3, 4, 6, 3])
def ResNet101():
    return ResNet(Bottleneck, [3, 4, 23, 3])
def ResNet152():
    return ResNet(Bottleneck, [3, 8, 36, 3])
#--------------------训练过程---------------------------------
model = ResNet18()#在这里更换你需要训练的模型
summary.summary(model, input_size=(3,224,224),device="cpu")#我们选择图形的出入尺寸为(3,224,224)

params = [{'params': md.parameters()} for md in model.children()
          if md in [model.classifier]]
#optimizer = optim.Adam(model.parameters(), lr=learning_rate, betas=(0.9, 0.999), eps=1e-08, weight_decay=0)
optimizer = optim.SGD(model.parameters(), lr=0.01,momentum=0.9, weight_decay=1e-4)
StepLR    = optim.lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.1)#按训练批次调整学习率,每30个epoch调整一次
loss_func = nn.CrossEntropyLoss()
#存储测试loss和acc
Loss_list = []
Accuracy_list = []
#存储训练loss和acc
train_Loss_list = []
train_Accuracy_list = []
#这俩作用是为了提前开辟一个
loss = []
loss1 = []
def train_res(model,train_dataloader,epoch):
    model.train()
    #print('epoch {}'.format(epoch + 1))
    # training-----------------------------
    train_loss = 0.
    train_acc = 0.
    for batch_x, batch_y in train_dataloader:
        batch_x  = Variable(batch_x).cuda()
        batch_y  = Variable(batch_y).cuda()
        optimizer.zero_grad()
        out = model(batch_x)
        loss1 = loss_func(out, batch_y)
        train_loss += loss1.item()
        pred = torch.max(out, 1)[1]
        train_correct = (pred == batch_y).sum()
        train_acc += train_correct.item()
        loss1.backward()
        optimizer.step()
    print('Train Loss: {:.6f}, Acc: {:.6f}'.format(train_loss / (len(train_datasets)), train_acc / (len(train_datasets))))#输出训练时的loss和acc
    train_Loss_list.append(train_loss / (len(val_datasets)))
    train_Accuracy_list.append(100 * train_acc / (len(val_datasets)))

# evaluation--------------------------------
def val(model,val_dataloader):
    model.eval()
    eval_loss= 0.
    eval_acc = 0.
    for batch_x, batch_y in val_dataloader:
        batch_x = Variable(batch_x, volatile=True).cuda()
        batch_y = Variable(batch_y, volatile=True).cuda()
        out = model(batch_x)
        loss = loss_func(out, batch_y)
        eval_loss += loss.item()
        pred = torch.max(out, 1)[1]
        num_correct = (pred == batch_y).sum()
        eval_acc += num_correct.item()
    print('Test Loss: {:.6f}, Acc: {:.6f}'.format(eval_loss / (len(val_datasets)), eval_acc / (len(val_datasets))))#输出测试时的loss和acc
    Loss_list.append(eval_loss / (len(val_datasets)))
    Accuracy_list.append(100 * eval_acc / (len(val_datasets)))
        
# 保存模型的参数
#torch.save(model.state_dict(), 'ResNet18.pth')
#state = {'model': model.state_dict(), 'optimizer': optimizer.state_dict(), 'epoch': epoch}
#torch.save(state, 'ResNet18.pth')


log_dir = 'D:/2021year/CVPR/PermuteNet-main/Keras_position/Pytorch_Code/resnet18.pth'
def main():
    if torch.cuda.is_available():
        model.cuda()
    test_flag = False
    # 如果test_flag=True,则加载已保存的模型
    if test_flag:
        # 加载保存的模型直接进行测试机验证,不进行此模块以后的步骤
        checkpoint = torch.load(log_dir)
        model.load_state_dict(checkpoint['model'])
        optimizer.load_state_dict(checkpoint['optimizer'])
        start_epoch = checkpoint['epoch']
        val(model, val_dataloader)
        #如果只评估模型,则保留这个return,如果是从某个阶段开始继续训练模型,则去掉这个模型
        #同时把上面的False改成True
        return

    # 如果有保存的模型,则加载模型,并在其基础上继续训练
    if os.path.exists(log_dir) and test_flag:
        checkpoint = torch.load(log_dir)
        model.load_state_dict(checkpoint['model'])
        optimizer.load_state_dict(checkpoint['optimizer'])
        start_epoch = checkpoint['epoch']
        print('加载 epoch {} 成功!'.format(start_epoch))
    else:
        start_epoch = 1
        print('无保存模型,将从头开始训练!')

    for epoch in range(start_epoch, EPOCH):
        since = time.time()
        print('epoch {}'.format(epoch))#显示每次训练次数
        train_res(model, train_dataloader, epoch)
        time_elapsed = time.time() - since
        print('Training complete in {:.0f}m {:.0f}s'.format(time_elapsed // 60, time_elapsed % 60))  # 输出训练和测试的时间
        #通过一个if语句判断,让模型每十次评估一次模型并且保存一次模型参数
        epoch_num = epoch/10
        epoch_numcl = [1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0,12.0,13.0,15.0,16.0,17.0,18.0,19.0,20.0,21.0]
        print('epoch_num',epoch_num)
        if epoch_num in epoch_numcl:
            print('评估模型')
            val(model, val_dataloader)
            print('保存模型')
            state = {'model':model.state_dict(), 'optimizer':optimizer.state_dict(), 'epoch':epoch}
            torch.save(state, log_dir)
        
    y1 = Accuracy_list
    y2 = Loss_list
    y3 = train_Accuracy_list
    y4 = train_Loss_list

    x1 = range(len(Accuracy_list))
    x2 = range(len(Loss_list))
    x3 = range(len(train_Accuracy_list))
    x4 = range(len(train_Loss_list))

    plt.subplot(2, 1, 1)
    plt.plot(x1, y1,'-')
    plt.title('Test accuracy vs. epoches')
    plt.ylabel('Test accuracy')
    plt.subplot(2, 1, 2)
    plt.plot(x2, y2,'-')
    plt.xlabel('Test loss vs. epoches')
    plt.ylabel('Test loss')
    plt.show()

    plt.subplot(2, 1, 1)
    plt.plot(x3, y3,'-')
    plt.title('Train accuracy vs. epoches')
    plt.ylabel('Train accuracy')
    plt.subplot(2, 1, 2)
    plt.plot(x4, y4,'-')
    plt.xlabel('Train loss vs. epoches')
    plt.ylabel('Train loss')
    plt.show()



if __name__ == '__main__':
    main()

3. 关于训练一开始的问题

由于一开始跑的都是很小的数据集,很多兄弟习惯了上来就看GPU的利用率。
这个时候,由于数据集数量比较少,很快啊,数据集就全被加载器加载完成了,然后开始训练,正常情况下如果有GPU的同学,都会看到一个让你满意的结果,至少利用率大于30%,或者一直在跳动,大家都可以可以接受,毕竟小数据集也不在乎这点时间了。

但是如果是大型数据集,大家就要争分夺秒了,甚至利用率为99%都觉得这块显卡是在偷懒。于是乎各种多线程操作就开始了。

在win10下面看GPU利用率的方法有两种,第一种是Ctrl+Alt+. 但是任务管理器这种方法显示的GPU利用率不准。

因此推荐在cmd终端输入
最后一个数字控制刷新时间1表示1秒刷新一次。

nvidia-smi -l 1


从上面两个图看出,这个时候的磁盘利用率直接拉满,cpu和GPU的利用率基本为0.。GPU的显存一开始也是很低,后来才慢慢拉满。

面对这种显存拉满,GPU和CPU利用率为0的情况,很多人肯定会着急,为什么会这样。然后上网开始像模像样的根据各类大神的调参经验开始调参,然后就这样一会启动,一会运行的,就是一点效果都没有。于是乎开始怀疑是不是自己设备不行。其实不是。。。。。

其实,这时候是在读取那128G的数据集,大概需要多久,我们也不清楚。但是,荡第一个批次之后,pytorch的下载器把数据都加载好之后,从第二个epoch开始,利用率基本不开始一直大于97%。
具体可以参考这个大佬的实验证据

关于GPU利用率低的一些问题,可以参考这个大佬的无私奉献

4. 关于学习率,初始学习率,epoch,bachsize之间的关系。

4.1 如果你使用每个30批次学习率下降十倍的方法这种操作

'''
例:lr = 0.05, step_size=30, gamma=0.1
这个gamma=0.1表示学习率每次下降10倍,step_size=30表示每30个epoch变化一次。
在我的例子中就是使用这种操作的,基本上大家都是这样的。每30个批次学习率下降十倍
lr = 0.05     if epoch < 30
lr = 0.005    if 30 <= epoch < 60
lr = 0.0005   if 60 <= epoch < 90
'''

4.2 bachsize的设置与Datalodar

在合理范围内,增大 Batch_Size 有何好处?
内存利用率提高了,大矩阵乘法的并行化效率提高
跑完一次epoch(全数据集)所需要的迭代次数减小,对于相同数据量的处理速度进一步加快
在一定范围内,一般来说batch size越大,其确定的下降方向越准,引起的训练震荡越小

盲目增大batch size 有什么坏处
内存利用率提高了,但是内存容量可能撑不住了
跑完一次epoch(全数据集)所需要的迭代次数减少,但是想要达到相同的精度,其所花费的时间大大增加了,从而对参数的修正也就显得更加缓慢
batch size 大到一定的程度,其确定的下降方向已经基本不再变化

首先,batch-size和num_workers。一般情况下,为了榨干GPU的每一滴精血,让GPU利用率达到100.我们都会在DataLoader中设置num_workers,一般来讲设置为8和16

train_dataloader = torch.utils.data.DataLoader(train_datasets, batch_size=batch_size
                         ,shuffle=True,num_workers=8,pin_memory=True)

设置这两个参数可以有效提高训练速度,但是很容易出现OOM(out of memory)的现象。因此这便还需要控制。
这里,继续要考虑到显存,也需要考虑到内存条的内存。

num_worker大,下一轮迭代的batch可能在上一轮/上上一轮…迭代时已经加载好了。坏处是内存(显卡内存?)开销大(开了pin memory?) ,也加重了CPU负担。显存=显卡内存(内存单词是memory),作用是用来存储显卡芯片处理过或者即将提取的渲染数据。

如同计算机的内存一样,显存是用来存储要处理的图形信息的部件。(显存和GPU的关系有点类似于内存和CPU的关系)

CPU不能直接调用存储在硬盘上的系统、程序和数据,必须首先将硬盘的有关内容存储在内存中,这样才能被CPU读取运行。因而,内存(即物理 内存,是相对于硬盘这个“外存”而言)作为硬盘和CPU的“中转站”,对电脑运行速度有较大影响。

https://zhuanlan.zhihu.com/p/31558973

pin memory: 开了就在GPU上面预留一片固定的内存区域,以加速传输。

如果开了pin memory: 增大num_workers的数量,内存占用也会增加。因为每个worker都需要缓存一个batch的数据。此时,batch size和num_workers的增大不可兼得,否则cuda out of memory。
开pin memory不觉得能快多少,反倒是牺牲了batch size。我是16G显存24G运存,训练Imagenet时batch size= 512和num_workers = 8.这样没啥问题

后续更新…

来源:难受啊!马飞…

物联沃分享整理
物联沃-IOTWORD物联网 » Windows下,Pytorch使用Imagenet-1K训练ResNet的经验(有代码)

发表评论