torchvision.transforms常用的几个图像预处理方法
-
transforms.Resize(size) :会把图片的短边尺寸转换成size,整体长宽比不变
-
transforms.CenterCrop(size) : 在图片的中心抠出一张(size, size)的小图像,往往配合Resize使用
-
transforms.RandomRotation((n,m)) : 把图片旋转,旋转角度为n~m的随机角度,可为负数
-
transforms.RandomHorizontalFlip§ : 图片有p的概率会水平翻转
-
transforms.RandomVerticalFlip§ : 图片有p的概率会垂直翻转
-
上面的几个对象(别看像方法,实则是创建的对象)都是对PILImage格式进行操作的,所以有两点注意事项
-
在读取数据时,最好用PIL来读,用opencv也可以,就是容易出bug。
from PIL import Image pic = Image.open(img_path)
-
transforms.ToTensor()要放在前面几个对象的后面
-
模型保存和读取
主要是三个方法,torch.save、torch.load、model.load_state_dict
最简单的存取:
torch.save(net.state_dict(),PATH)
model.load_state_dict(torch.load(PATH))
专业点的存取:
torch.save({'epoch':epoch, 'state_dict':model.state_dict(),'optimizer': optimizer.state_dict()}, PATH) #把想要的数据以map的形式记录下来
def load_checkpoint(model, checkpoint_PATH):
model_CKPT = torch.load(checkpoint_PATH) #读取所有信息
model.load_state_dict(model_CKPT['state_dict'])
optimizer.load_state_dict(model_CKPT['optimizer'])
# model_CKPT['其他信息']
只导入需要的参数:
def load_checkpoint(model, checkpoint_PATH):
model_CKPT = torch.load(checkpoint_PATH) #读取所有信息
pretrained_dict = model_CKPT['state_dict'] #读取预训练模型
model_dict = model.state_dict() #读取目标模型的所有参数列表
new_dict = {k: v for k, v in pretrained_dict.items() if k in model_dict.keys()} #搜集预训练模型中哪些参数有用
model_dict.update(new_dict) #更新目标模型的参数列表(可能只更新其中一部分)
model.load_state_dict(model_dict) #重新load回去
预训练模型的使用
在torchvision.models中提供了很多出名的模型代码和其预训练模型,同样,网上也有很多高人写了一些牛皮的网络和预训练模型,我们在借鉴时,往往需要对模型进行改造,最常见的就是把最后一层的全链接输出改成自己想要的种类数目。
以torchvision.models的resnet34为例,我们可以读取到网络及其参数后,直接修改最后一层(前面层的数据还在),在这基础上进行几轮训练,就能达到不错的效果
net = models.resnet34(pretrained=True)
net.fc = nn.Linear(512, cfg.num_class)
学习了上面的模型导入,我们也可以自己大幅度调整网络,然后利用参数筛选更新来使用预训练模型。
训练过程中的部分参数冻结
利用name_parameters()来获取参数列表,把符合条件的参数的requires_grad设置为False,并从优化器的参数中剔除
for name, value in model.named_parameters():
if name 满足某些条件:
value.requires_grad = False
# setup optimizer
params = filter(lambda p: p.requires_grad, model.parameters()) #过滤器,requires_grad为False的过滤掉
optimizer = torch.optim.Adam(params, lr=1e-4)
学习率的调整
在训练过程中,学习率逐步衰减的效果会比较好,我们可以利用优化器的param_groups来获取当前的学习率并调整
for p in opt.param_groups:
p['lr'] *= decay_rate
net.train()和net.eval()
在模型的训练过程中,往往会穿插校验的过程,这时就需要切换模型为训练模式或者校验模式
直接net.train()、net.eval()即可切换
在train模式下,dropout和BN层会起作用,在eval模式下,dropout不起作用,BN会直接使用之前训练好的参数。
实战:Dog Breed Identification
这是kaggle上的小比赛,我下载了它的数据集做了下实战
数据集规模:10000张带标签的120类狗狗图像
-
第一步:整理数据
我从10000张图像中随机抽取500+张图像作为校验集,并把所有图片按标签重命名(方便后面获取相应的标签),按这种比例分数据集其实不是很合理,不过训练集实在是不大。def getDev(): dev_path = './dev' train_path = './train' if not os.path.exists(dev_path): os.mkdir(dev_path) img_list = os.listdir(train_path) for i in range(len(img_list)): if i % 20 == 0: os.rename(os.path.join(train_path, img_list[i]), os.path.join(dev_path, img_list[i])) def renamePic(): Map = {} with open('labels.csv', 'r') as f: for line in f: info = line.split(',') if info[0] == 'id': continue Map[info[0]] = info[1].strip() dev_path = './dev' train_path = './train' train_list = os.listdir(train_path) num = 0 for item in train_list: name = item.split('.') os.rename(os.path.join(train_path, item), os.path.join(train_path, Map[name[0]] + '+' + str(num) + '.' + name[1])) num += 1 dev_list = os.listdir(dev_path) for item in dev_list: name = item.split('.') os.rename(os.path.join(dev_path, item), os.path.join(dev_path, Map[name[0]] + '+' + str(num) + '.' + name[1])) num += 1
-
第二步:设计模型
在本次实战中,采用的是torchvision.models中自带的resnet101模型进行微调(调用后预训练模型会自动下载),torchvision中不只有resnet,还有很多其他模型,详见:https://pytorch.org/docs/stable/torchvision/models.html
参照上面讲的预训练模型使用部分,先是确认了resnet的最后一层fc层的结构,然后直接修改fc层。net = models.resnet101(pretrained=True) # net.fc = nn.Linear(2048, cfg.num_class) net.fc = nn.Sequential( nn.Linear(2048, 512), nn.Linear(512, cfg.num_class) )
-
第三步:设计数据
直接上代码:class MyDataset(data.Dataset): def __init__(self, root): self.root = './' + root self.img_list = os.listdir(self.root) self.Map = utils.getLabel() #获取label所对应的id self.transforms = transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), #resnet101预训练模型用的size(可改) # transforms.RandomRotation((-30, 30)), #在该分类中加数据增强反而效果变差了 # transforms.RandomHorizontalFlip(0.5), transforms.ToTensor(), transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225)) #imagenet的均值方差 # transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) #直接归一化到[-1, 1] ]) def __getitem__(self, index): path = os.path.join(self.root, self.img_list[index]) pic = Image.open(path) # pic = pic.resize((300, 300)) pic = self.transforms(pic) label = self.Map[self.img_list[index].split('+')[0]] return pic, label def __len__(self): return len(self.img_list)
-
第四步:模型训练
本次实战中,模型训练并不把预训练好的那部分参数进行训练,换言之,只训练我们修改的fc层trainSet = MyDataset('train') devSet = MyDataset('dev') trainLoader = DataLoader(trainSet, batch_size=cfg.batch_size, shuffle=True, drop_last=True) devLoader = DataLoader(devSet, batch_size=1, shuffle=True, drop_last=True) #筛除不需要训练的参数 for name, value in net.named_parameters(): if 'fc' not in name: value.requires_grad = False params = filter(lambda p: p.requires_grad, net.parameters()) opt = optim.Adam(params, lr=cfg.lr)
-
总结
对于这次实战,我进行了很多实验,具体如下:- size=224,标准化至(-1, 1),batch_size=32,没有数据增强,没有训练预训练部分,acc=0.86
- size=224,imagenet标准化,batch_size=32,没有数据增强,没有训练预训练部分,acc=0.87
- size=224,imagenet标准化,batch_size=32,没有数据增强,训练预训练部分,acc=0.81, 训练集loss很低
- size=384,imagenet标准化,batch_size=32,没有数据增强,没有训练预训练部分,acc=0.85
- size=224,imagenet标准化,batch_size=32,有数据增强,没有训练预训练部分,acc=0.82
- size=224,imagenet标准化,batch_size=64,没有数据增强,没有训练预训练部分,acc=0.88
总结了一下,对模型效果影响最大的是训不训练预训练好的参数,为什么训练预训练部分loss更低但是效果差很多呢?我想了想,预训练部分主要作用是提取图片中的特征,原本训练集是超级大的imagenet,而我们这次用的训练集不过万张,如果动了原本的参数,提取的效果肯定会变差,至于训练集的loss很低,这也是正常的,毕竟整个模型都在为了降低loss而不停修改参数,校验集的准确率却变低了很多可以理解为过拟合的现象了。第二个影响因素是数据增强,其实对于这种追求准确率,且数据都是干干净净端端正正的,确实不需要数据增强,感觉数据增强应该用在增强模型召回率上。其他部分影响不太大,但最好还是用预训练的size和均值方差。
github已上传相关代码:https://github.com/Zou-CM/pytorch_practice/tree/master/DogsClassification