赞
踩
- 本帖经过一次大的修改,现在只包含数据增广、微调和图片分类(赛事)
- 这些帖子是我做的李沐-动手深度学习课程的笔记。之前这篇帖子写的是数据增广、微调、目标检测(锚框)等等。后来我把目标检测(锚框)的部分内容移到了另一篇帖子(动手深度学习13:计算机视觉——目标检测:锚框算法原理与实现、SSD、R-CNN)
参考李沐《动手深度学习》、哔哩哔哩视频
图片分类常见卷积神经网络(VGG、ResNet)等可以参考李沐《现代卷积神经网络》、VGG哔哩哔哩视频
也可以参考我之前做的笔记《学习笔记五:卷积神经网络一》
解决:开场前一天现场测试有问题,马上现场采集数据发回国内,训练一个新的模型,并且买了一块桌布就没有反光了。
例如语音识别、CV等场景,训练模型时可以模拟部署场景的各种情况,是提高模型泛化性的一种手段。
一般的做法是图片进行随机在线数据增广之后再进行训练,相当于一个正则项。
pip install torch==1.10.2
pip install torchvision==0.11.3
pip install d2l==0.17.4
%matplotlib inline
import torch
import torchvision
from torch import nn
from d2l import torch as d2l
d2l.set_figsize()
img = d2l.Image.open('../img/cat1.jpg')
d2l.plt.imshow(img);
定义辅助函数apply。此函数在输⼊图像img上多次运⾏图像增⼴⽅法aug并显⽰所有结果:
def apply(img, aug, num_rows=2, num_cols=4, scale=1.5):
Y = [aug(img) for _ in range(num_rows * num_cols)]#对num_rows * num_cols的图片进行增广存储到Y
d2l.show_images(Y, num_rows, num_cols, scale=scale)#打印出增广后的图片
img:被增广图片
aug:增广办法
num_rows、num_cols:图片被增广成几行几列
scale:放大比例
apply(img,torchvision.transforms.RandomHorizontalFlip())#随机左右翻转
apply(img, torchvision.transforms.RandomVerticalFlip())#随机上下翻转
从原图切割一块,再变形到固定形状(卷积网络固定输入)。切割方式可以是随机高宽比、随机大小、随机位置。
shape_aug = torchvision.transforms.RandomResizedCrop(
(200, 200), scale=(0.1, 1), ratio=(0.5, 2))
apply(img, shape_aug)
主要是改变色调(偏黄偏蓝等等)、饱和度(浓度)、亮度等等。创建一个RandomColorJitter实例,并设置如何同时[随机更改图像的亮度(brightness)、对比度(contrast)、饱和度(saturation)和色调(hue)]:
color_aug = torchvision.transforms.ColorJitter(
brightness=0.5, contrast=0.5, saturation=0.5, hue=0.5)
apply(img, color_aug)
#brightness=0.5表示明度在50%-150%之间随机变化,其它参数类同
#四个参数随机取值组合决定图片变化
shape_aug = torchvision.transforms.RandomResizedCrop(
(200, 200), scale=(0.1, 1), ratio=(0.5, 2))
color_aug = torchvision.transforms.ColorJitter(
brightness=0.2, contrast=0, saturation=0.2, hue=0.05)
augs = torchvision.transforms.Compose([
torchvision.transforms.RandomHorizontalFlip(), color_aug, shape_aug])
apply(img, augs)
我们使⽤CIFAR-10数据集进行图像增广之后再训练模型。CIFAR-10数据集中对象的颜⾊和⼤⼩差异更明显。CIFAR-10数据集中的前32个训练图像如下所⽰:
all_images = torchvision.datasets.CIFAR10(train=True, root="../data",
download=True)
d2l.show_images([all_images[i][0] for i in range(32)], 4, 8, scale=0.8);
ToTensor
实例将一批图像转换为4d的Tensor矩阵方便训练,即形状为(批量大小,通道数,高度,宽度)的32位浮点数,取值范围为0到1。train_augs = torchvision.transforms.Compose([
torchvision.transforms.RandomHorizontalFlip(),
torchvision.transforms.ToTensor()])
test_augs = torchvision.transforms.Compose([
torchvision.transforms.ToTensor()])
def load_cifar10(is_train, augs, batch_size):
dataset = torchvision.datasets.CIFAR10(root="../data", train=is_train,
transform=augs, download=True)
dataloader = torch.utils.data.DataLoader(dataset, batch_size=batch_size,
shuffle=is_train, num_workers=d2l.get_dataloader_workers())
return dataloader
这里的d2l.get_dataloader_workers()=4,多开进程是因为图片随机增广的时候计算量很大,开多进程可以快一点。
#@save def train_batch_ch13(net, X, y, loss, trainer, devices): """用多GPU进行小批量训练""" if isinstance(X, list): # 微调BERT中所需(稍后讨论) X = [x.to(devices[0]) for x in X]#如果X是list就一个个copy到devices else: X = X.to(devices[0]) y = y.to(devices[0]) net.train() trainer.zero_grad()#梯度归零 pred = net(X) l = loss(pred, y) l.sum().backward()#梯度回传 trainer.step() train_loss_sum = l.sum() train_acc_sum = d2l.accuracy(pred, y) return train_loss_sum, train_acc_sum
#@save def train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, devices=d2l.try_all_gpus()): """用多GPU进行模型训练""" timer, num_batches = d2l.Timer(), len(train_iter) animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0, 1], legend=['train loss', 'train acc', 'test acc']) net = nn.DataParallel(net, device_ids=devices).to(devices[0]) for epoch in range(num_epochs): # 4个维度:储存训练损失,训练准确度,实例数,特点数 metric = d2l.Accumulator(4) for i, (features, labels) in enumerate(train_iter): timer.start() l, acc = train_batch_ch13( net, features, labels, loss, trainer, devices) metric.add(l, acc, labels.shape[0], labels.numel()) timer.stop() if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1: animator.add(epoch + (i + 1) / num_batches, (metric[0] / metric[2], metric[1] / metric[3], None)) test_acc = d2l.evaluate_accuracy_gpu(net, test_iter) animator.add(epoch + 1, (None, None, test_acc)) print(f'loss {metric[0] / metric[2]:.3f}, train acc ' f'{metric[1] / metric[3]:.3f}, test acc {test_acc:.3f}') print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec on ' f'{str(devices)}')
batch_size, devices, net = 256, d2l.try_all_gpus(), d2l.resnet18(10, 3)
def init_weights(m):
if type(m) in [nn.Linear, nn.Conv2d]:
nn.init.xavier_uniform_(m.weight)
net.apply(init_weights)
def train_with_data_aug(train_augs, test_augs, net, lr=0.001):#训练集是增广数据集train_augs
train_iter = load_cifar10(True, train_augs, batch_size)
test_iter = load_cifar10(False, test_augs, batch_size)
loss = nn.CrossEntropyLoss(reduction="none")
trainer = torch.optim.Adam(net.parameters(), lr=lr)
train_ch13(net, train_iter, test_iter, loss, trainer, 10, devices)
使用基于随机左右翻转的图像增广来训练模型:
train_with_data_aug(train_augs, test_augs, net)
loss 0.167, train acc 0.943, test acc 0.843
5486.0 examples/sec on [device(type='cuda', index=0), device(type='cuda', index=1)]
不用图像增广,效果:
loss 0.072, train acc 0.975, test acc 0.824
5560.0 examples/sec on [device(type='cuda', index=0), device(type='cuda', index=1)]
对比可以看到,简单翻转图片可以有效降低过拟合程度(训练测试集精度差异更小,overfiting更小)。有些情况下测试集精度高于训练集精度,是由于训练集图片增广太狠,出现很多奇怪的图片,精度下降。而测试集中图片不会那么奇怪,效果反而更好。
收集更多的数据。 但是,收集和标记数据可能需要大量的时间和金钱。 例如,为了收集ImageNet数据集,研究人员花费了数百万美元的研究资金。 尽管目前的数据收集成本已大幅降低,但这一成本仍不能忽视。
应用迁移学习(transfer learning)将从源数据集学到的知识迁移到目标数据集。 例如,尽管ImageNet数据集中的大多数图像与椅子无关,但在此数据集上训练的模型可能会提取更通用的图像特征,这有助于识别边缘、纹理、形状和对象组合。 这些类似的特征也可能有效地识别椅子。
当目标数据集比源数据集小得多时,微调有助于提高模型的泛化能力。(其实相当于用源模型的参数作为目标模型的参数初始化,这样比目标模型随机初始化效果好。而输出层是随机初始化来训练)
简介:热狗数据集有正负两类,预下载在ImageNet上训练好的resnet-18模型,然后在热狗数据集上微调。微调时,最后一个fc层学习率为10*lr,其它层学习率为lr,最后效果很好。(基本2个epoch效果就很好了,lr=5e-5比较小)
hotdogs = [train_imgs[i][0] for i in range(8)]
not_hotdogs = [train_imgs[-i - 1][0] for i in range(8)]
d2l.show_images(hotdogs + not_hotdogs, 2, 8, scale=1.4);
注意:
!pip install d2l==0.17.5
import collections
import math
import os
import shutil
import pandas as pd
import torch
import torchvision
from torch import nn
from d2l import torch as d2l
为了便于⼊⻔,我们提供包含前1000个训练图像和5个随机测试图像的数据集的⼩规模样本。要使⽤Kaggle竞赛的完整数据集,你需要将以下demo变量设置为False:
#@save
d2l.DATA_HUB['cifar10_tiny'] = (d2l.DATA_URL + 'kaggle_cifar10_tiny.zip',
'2068874e4b9a9f0fb07ebe0ad2b29754449ccacd')
# 如果你使用完整的Kaggle竞赛的数据集,设置demo为False
demo = False
if demo:
data_dir = d2l.download_extract('cifar10_tiny')
else:
data_dir = '../data/cifar-10/'
read_csv_labels :读取’trainLabels.csv‘,返回一个图片标签的字典,最后labels格式如下:
#@save
def read_csv_labels(fname):
"""读取fname来给标签字典返回一个文件名"""
with open(fname, 'r') as f:
# 跳过文件头行(列名)
lines = f.readlines()[1:]
tokens = [l.rstrip().split(',') for l in lines]#去除换行符
return dict(((name, label) for name, label in tokens))#把csv一行行数据(列表)换成图片id和类别的字典
labels = read_csv_labels(os.path.join(data_dir, 'trainLabels.csv'))
print('# 训练样本 :', len(labels))
print('# 类别 :', len(set(labels.values())))
"""
lines:['1,frog\n', '2,truck\n']...
tokens:[['1', 'frog'], ['2', 'truck']...]
labels:
{'1': 'frog',
'2': 'truck',...}...
"""
# 训练样本 : 1000
# 类别 : 10
labels:包含本来的训练集’train‘的所有图片标签的字典,其格式如下:
{‘1’: ‘frog’,
‘2’: ‘truck’,…}
os.path.join(data_dir, ‘train_valid_test’,‘train_valid’, label):创建新文件夹‘data_dir/train_valid_test/train_valid’,并创建类别(label)子文件夹,如下图所示:
#@save def copyfile(filename, target_dir): """将文件复制到目标目录""" os.makedirs(target_dir, exist_ok=True) shutil.copy(filename, target_dir) #@save def reorg_train_valid(data_dir, labels, valid_ratio): """将验证集从原始的训练集中拆分出来""" # 训练数据集中样本最少的类别中的样本数 n = collections.Counter(labels.values()).most_common()[-1][1] # 验证集中每个类别的样本数 n_valid_per_label = max(1, math.floor(n * valid_ratio)) label_count = {} #遍历训练集'train'的文件 for train_file in os.listdir(os.path.join(data_dir, 'train')): """labels是包含训练集标签的字典,格式如下: {'1': 'frog', '2': 'truck',...} print(train_file)格式为: 1.png 2.png ... 所以train_file.split('.')[0]就是每张图片的编号1.2.3.... labels[train_file.split('.')[0]]就是训练集每张图片的label frog truck truck """ label = labels[train_file.split('.')[0]]#图片标签,和train_file对应 fname = os.path.join(data_dir, 'train', train_file)#原训练集图片名,和train_file对应 #创建新文件夹‘data_dir/train_valid_test/train_valid',并创建类别文件夹 #这里厉害的是,fname和train_file对应,label也和train_file对应。所以训练集图片一一copy到类别文件夹而不会出错。 copyfile(fname, os.path.join(data_dir, 'train_valid_test', 'train_valid', label)) #每个类新来的图片和不到n_valid_per_label数量的图片直接copy到valid文件夹 #label_count是记录copy到valid的图片的类别数字典。每copy一次就+1 if label not in label_count or label_count[label] < n_valid_per_label: copyfile(fname, os.path.join(data_dir, 'train_valid_test', 'valid', label)) label_count[label] = label_count.get(label, 0) + 1 else: copyfile(fname, os.path.join(data_dir, 'train_valid_test', 'train', label)) return n_valid_per_label
下面的reorg_test函数用来在预测期间整理测试集,以方便读取。(原test文件复制到’train_valid_test/test/unknown’文件夹)
#@save
def reorg_test(data_dir):
"""在预测期间整理测试集,以方便读取"""
for test_file in os.listdir(os.path.join(data_dir, 'test')):
copyfile(os.path.join(data_dir, 'test', test_file),
os.path.join(data_dir, 'train_valid_test', 'test',
'unknown'))
为了便于⼊⻔,我们使用tiny数据集(包含前1000个训练图像和5个随机测试图像)。要使⽤Kaggle竞赛的完整数据集,你需要将以下demo变量设置为False
def reorg_cifar10_data(data_dir, valid_ratio):
labels = read_csv_labels(os.path.join(data_dir, 'trainLabels.csv'))
reorg_train_valid(data_dir, labels, valid_ratio)
reorg_test(data_dir)
batch_size = 32 if demo else 128
valid_ratio = 0.1
reorg_cifar10_data(data_dir, valid_ratio)
transform_train = torchvision.transforms.Compose([ # 在高度和宽度上将图像放大到40像素的正方形 torchvision.transforms.Resize(40), # 随机裁剪出一个高度和宽度均为40像素的正方形图像, # 生成一个面积为原始图像面积0.64到1倍的小正方形, # 然后将其缩放为高度和宽度均为32像素的正方形 torchvision.transforms.RandomResizedCrop(32, scale=(0.64, 1.0), ratio=(1.0, 1.0)), torchvision.transforms.RandomHorizontalFlip(), torchvision.transforms.ToTensor(), # 标准化图像的每个通道 torchvision.transforms.Normalize([0.4914, 0.4822, 0.4465], [0.2023, 0.1994, 0.2010])]) #在测试期间,我们只对图像执行标准化,以消除评估结果中的随机性。 transform_test = torchvision.transforms.Compose([ torchvision.transforms.ToTensor(), torchvision.transforms.Normalize([0.4914, 0.4822, 0.4465], [0.2023, 0.1994, 0.2010])])
train_ds, train_valid_ds = [torchvision.datasets.ImageFolder(
os.path.join(data_dir, 'train_valid_test', folder),
transform=transform_train) for folder in ['train', 'train_valid']]
valid_ds, test_ds = [torchvision.datasets.ImageFolder(
os.path.join(data_dir, 'train_valid_test', folder),
transform=transform_test) for folder in ['valid', 'test']]
train_iter, train_valid_iter = [torch.utils.data.DataLoader(
dataset, batch_size, shuffle=True, drop_last=True)
for dataset in (train_ds, train_valid_ds)]
valid_iter = torch.utils.data.DataLoader(valid_ds, batch_size, shuffle=False,
drop_last=True)
test_iter = torch.utils.data.DataLoader(test_ds, batch_size, shuffle=False,
drop_last=False)
def get_net():
num_classes = 10
net = d2l.resnet18(num_classes, 3)
return net
loss = nn.CrossEntropyLoss(reduction="none")
def train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period, lr_decay): trainer = torch.optim.SGD(net.parameters(), lr=lr, momentum=0.9, weight_decay=wd) scheduler = torch.optim.lr_scheduler.StepLR(trainer, lr_period, lr_decay) num_batches, timer = len(train_iter), d2l.Timer() legend = ['train loss', 'train acc'] if valid_iter is not None: legend.append('valid acc') animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs], legend=legend) net = nn.DataParallel(net, device_ids=devices).to(devices[0])#应用多GPU训练 for epoch in range(num_epochs): net.train() metric = d2l.Accumulator(3) for i, (features, labels) in enumerate(train_iter): timer.start() l, acc = d2l.train_batch_ch13(net, features, labels, loss, trainer, devices) metric.add(l, acc, labels.shape[0]) timer.stop() if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1: animator.add(epoch + (i + 1) / num_batches, (metric[0] / metric[2], metric[1] / metric[2], None)) if valid_iter is not None: valid_acc = d2l.evaluate_accuracy_gpu(net, valid_iter) animator.add(epoch + 1, (None, None, valid_acc)) scheduler.step() measures = (f'train loss {metric[0] / metric[2]:.3f}, ' f'train acc {metric[1] / metric[2]:.3f}') if valid_iter is not None: measures += f', valid acc {valid_acc:.3f}' print(measures + f'\n{metric[2] * num_epochs / timer.sum():.1f}' f' examples/sec on {str(devices)}')
devices, num_epochs, lr, wd = d2l.try_all_gpus(), 20, 0.1, 0.001
lr_period, lr_decay, net = 10, 0.1, get_net()
train(net, train_iter, valid_iter, num_epochs, lr, wd, devices, lr_period,
lr_decay)
在获得具有超参数的满意的模型后,我们使用所有标记的数据(包括验证集)来重新训练模型并对测试集进行分类。
net, preds = get_net(), []
train(net, train_valid_iter, None, num_epochs, lr, wd, devices, lr_period,
lr_decay)
for X, _ in test_iter:
y_hat = net(X.to(devices[0]))
preds.extend(y_hat.argmax(dim=1).type(torch.int32).cpu().numpy())
sorted_ids = list(range(1, len(test_ds) + 1))
sorted_ids.sort(key=lambda x: str(x))
df = pd.DataFrame({'id': sorted_ids, 'label': preds})
df['label'] = df['label'].apply(lambda x: train_valid_ds.classes[x])
df.to_csv('submission.csv', index=False)
在这场Kaggle竞赛中使用完整的CIFAR-10数据集。将超参数设为batch_size = 128
,num_epochs = 100
,lr = 0.1
,lr_period = 50
,lr_decay = 0.1
。看看你在这场比赛中能达到什么准确度和排名。或者你能进一步改进吗?
这段代码是此次kaggle比赛的一位选手的代码《[96%] Fine-tuning ResNet34 with PyTorch》
在整个笔记本中,我们使用了来自 Kaggle 的 CIFAR-10 数据集,这是一个流行的计算机视觉数据集,包含 60,000 个 32x32 彩色图像,被分类为十个类别之一,每个类别有 6000 个图像。 该数据集足够复杂,可以很好地了解微调的好处,以及为什么可以使用此过程来获得高精度分数,而无需花费数小时和金钱从头开始重新训练复杂模型。 使用默认超参数和中型模型 (ResNet34),我们能够在 Kaggle 上实现大约 96% 的准确率,比该数据集的最新技术低 3%。
Kaggle 提供的原始 CIFAR-10 数据集由两个 .7z 文件 (train.7z, test.7z)、标签 (trainLabels.csv) 和示例提交 (sampleSubmission.csv) 组成)。我们在 data 文件夹中提供这些文件。
首先,我们将两个压缩文件提取到两个名为 original_train 和 original_test 的文件夹中。然后,我们将这些文件移动到 PyTorch 的 ImageDataset 类所需的结构中,其中每个图像都存储在一个以其标签命名的文件夹中(例如,第 n 个飞机图像将存储在 *airplane/n.png *)。我们将在 data 中创建四个子文件夹:
# This Python 3 environment comes with many helpful analytics libraries installed # It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python # For example, here's several helpful packages to load import numpy as np # linear algebra import pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv) # Input data files are available in the read-only "../input/" directory # For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory import os for dirname, _, filenames in os.walk('/kaggle/input'): for filename in filenames: print(os.path.join(dirname, filename)) # You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using "Save & Run All" # You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session
# py7zr is required to extract the .7tz files
!pip install py7zr
import py7zr import shutil from pathlib import Path root = Path('./data') input_path = Path('../input/cifar-10') with py7zr.SevenZipFile(input_path/'train.7z', mode='r') as z: z.extractall(root) with py7zr.SevenZipFile(input_path/'test.7z', mode='r') as z: z.extractall(root) shutil.copy(input_path/'trainLabels.csv', root/'trainLabels.csv') #train.7z, test.7z解压到original_train 和 original_test 文件夹 (root/'train').rename(root/'original_train') (root/'test').rename(root/'original_test')
打印出来就是:
img_index | entry.name |
---|---|
126979 | 126979.png |
180981 | 180981.png |
288998 | 288998.png |
另外关于mkdir函数:
mkdir(parents=True, exist_ok=True)
from random import random import os def copy_file(source_directory, destination_directory, filename): """ copy_file函数用于将source_directory下的文件复制到destination_directory下 """ destination_directory.mkdir(parents=True,exist_ok=True) shutil.copy(source_directory/filename,destination_directory/filename) def organize_train_valid_dataset(root,labels,valid_probability=0.1): """ 根据 PyTorch 的 ImageDataset 结构创建 train、train_valid 和 valid 文件夹 根据valid_probability拆分训练集和验证集。 """ source_directory = root/'original_train' with os.scandir(source_directory) as it: for entry in it: if entry.is_file(): img_index = entry.name.split('.')[0] # index是图像的名称,除了扩展名 #通过真假矩阵过滤,查找labels中索引entry的类别class img_class = labels[labels.id==int(img_index)].label.values[0] # 以概率“valid_probability”将图像随机拆分到valid文件夹 channel = Path('train') if random()>valid_probability else Path('valid') destination_directory = root/channel/img_class #将图像复制到 train 或 valid 文件夹,以及 train_valid 文件夹 #原始train训练集拆分为train和valid数据集,用来训练 #原始train训练集复制到'train_valid'文件夹,用于最终训练 copy_file(source_directory, destination_directory, entry.name) copy_file(source_directory, root/'train_valid'/img_class, entry.name) def organize_test_dataset(root): """ 将test文件夹下的图片标记为‘undefined’,创建符合PyTorch ImageDataset结构的文件夹 """ source_directory = root/'original_test' with os.scandir(source_directory) as it: for entry in it: if entry.is_file(): img_index = entry.name.split('.')[0] #index是图像的名称,除了扩展名 channel = Path('test') destination_directory = root/channel/'undefined' copy_file(source_directory,destination_directory, entry.name)
import pandas as pd
# 用DataFrame格式读取'trainLabels.csv'
labels = pd.read_csv(root/'trainLabels.csv')
# 创建train/train_valid/valid 文件夹结构
valid_probability = 0.1
organize_train_valid_dataset(root, labels, valid_probability)
# 创建test 文件夹结构
organize_test_dataset(root)
如上所述,我们依靠 PyTorch 的 ImageDataset 类来创建训练、验证、训练+验证和测试所需的数据集。 然后,我们从每个数据集中创建一个 DataLoader,用于训练/评估循环,以有效地从磁盘批量获取图像。
我们执行初始步骤以加载训练数据并计算所有图像和所有像素的每个通道(R、G、B)的数据集的均值和标准差。 我们逐批计算平均值和标准差值以避免将整个数据集加载到内存中,然后计算平均值和标准差的平均值。
注意:如果您有足够的 RAM(或 GPU 上的内存),您可以使用等于整个 train_dataset 长度的 batch_size,它将提供更准确的通道估计均值和标准差。
读取整个train数据集,计算所有图片剪裁、转成tensor之后的均值和方差,用于下一步的标准化Normalize
import torchvision import torch """这一步是为了计算数据集的均值和方差,用来在后面Normalize(mean, stdev)时 使用,而不是直接用ImageNet的数值Normalize([0.4914, 0.4822, 0.4465], [0.2023, 0.1994, 0.2010])])""" train_dataset = torchvision.datasets.ImageFolder( root/'train', transform=torchvision.transforms.Compose([ # Resize step is required as we will use a ResNet model, which accepts at leats 224x224 images torchvision.transforms.Resize((224,224)), torchvision.transforms.ToTensor(), ]) ) train_dataloader = torch.utils.data.DataLoader(train_dataset, batch_size=512, shuffle=False, num_workers=2, pin_memory=True) means = [] stdevs = [] for X, _ in train_dataloader: # Dimensions 0,2,3 are respectively the batch, height and width dimensions means.append(X.mean(dim=(0,2,3))) stdevs.append(X.std(dim=(0,2,3))) mean = torch.stack(means, dim=0).mean(dim=0) stdev = torch.stack(stdevs, dim=0).mean(dim=0)
用于训练和训练+验证数据集的转换包括通过我们的 ResNet 模型 (224x224) 将图像大小调整到所需的分辨率,使用在 CIFAR10 数据集上学习的“AutoAugment”策略,最后将图像从 PIL 转换为 Tensor。 对于验证集和测试集,我们只需调整图像大小并将其转换为张量格式。
torchvision.transforms.AutoAugment
函数,用于数据增强。参数有:
"""这部分代码基本和李沐老师的差不多""" train_transforms = torchvision.transforms.Compose([ torchvision.transforms.Resize((224,224)), torchvision.transforms.AutoAugment(policy=torchvision.transforms.AutoAugmentPolicy.CIFAR10), torchvision.transforms.ToTensor(), torchvision.transforms.Normalize(mean, stdev)]) train_dataset, train_valid_dataset = [torchvision.datasets.ImageFolder(folder, transform=train_transforms) for folder in [root/'train', root/'train_valid']] valid_transforms = torchvision.transforms.Compose([ torchvision.transforms.Resize((224,224)), torchvision.transforms.ToTensor(), torchvision.transforms.Normalize(mean, stdev)]) valid_dataset, test_dataset = [torchvision.datasets.ImageFolder(folder, transform=valid_transforms) for folder in [root/'valid', root/'test']]
train 和 train+validation DataLoader 使用较小的“batch_size”,因为我们还需要跟踪内存中的梯度。 此外,我们在每个 epoch 打乱数据集以避免以相同的顺序加载批次。
valid的和test的 DataLoader 使用更大的 batch_size
并且不需要shuffle,因为我们想要确定性的结果。
作为 Kaggle 的经验法则,worker 的数量通常设置为 2 * num_gpus
,使用 pin_memory = True
来加快数据传输到 GPU 的速度。
num_gpus = torch.cuda.device_count()
train_dataloader = torch.utils.data.DataLoader(train_dataset, batch_size=128, shuffle=True,
num_workers=2*num_gpus, pin_memory=True)
train_valid_dataloader = torch.utils.data.DataLoader(train_valid_dataset, batch_size=128,
shuffle=True, num_workers=2*num_gpus, pin_memory=True)
valid_dataloader = torch.utils.data.DataLoader(valid_dataset, batch_size=256, shuffle=False,
num_workers=2*num_gpus, pin_memory=True)
test_dataloader = torch.utils.data.DataLoader(test_dataset, batch_size=256, shuffle=False,
num_workers=2*num_gpus, pin_memory=True)
该过程的第一步是在我们自己的验证集上评估模型性能,验证集包含我们从 Kaggle 获得的 10% 的标记数据。 理想情况下,将在找到最佳模型和超参数以提高最终准确性的同时执行此步骤。 在这里,我们只是在将结果提交给 Kaggle 之前执行此步骤以显示预期的模型精度。
注意:在进行适当的超参数调整时,根据整体标签数据的大小,k-fold 方法可能更适合估计模型的泛化能力。
我们微调了一个在 ImageNet 上训练的 ResNet34 模型。 可能会使用其他模型,但就本笔记本而言,ResNet34 是训练时间和模型准确性之间的良好折衷。
该模型最初有一个 1000 维的输出层,但我们的数据集只有 10 个类,因此我们移除了输出层并定义了一个新的全连接层,其中只有 10 个神经元,CIFAR-10 中的每个类一个。 这些新神经元的参数通过 Xavier 初始化进行初始化。
def get_net():
resnet = torchvision.models.resnet34(pretrained=True)
# 替换FC输出层
resnet.fc = torch.nn.Linear(resnet.fc.in_features, 10)
torch.nn.init.xavier_uniform_(resnet.fc.weight)
return resnet
训练循环是一个标准的 PyTorch 循环,对于每个 epoch,我们执行以下宏步骤:
checkpoint_epochs
之后存储模型检查点(可选)import time def train(net,train_dataloader,valid_dataloader,criterion,optimizer,scheduler=None,epochs=10,device='cpu',checkpoint_epochs=10): start = time.time() print(f'Training for {epochs} epochs on {device}') for epoch in range(1,epochs+1): print(f"Epoch {epoch}/{epochs}") net.train() # 将网络置于训练模式以进行 Dropout 和 Batch Normalization train_loss = torch.tensor(0., device=device) # loss和accuracy在GPU上,以避免数据传输 train_accuracy = torch.tensor(0., device=device) for X, y in train_dataloader: X = X.to(device) y = y.to(device) preds = net(X) loss = criterion(preds, y) optimizer.zero_grad() loss.backward() optimizer.step() with torch.no_grad(): train_loss += loss * train_dataloader.batch_size train_accuracy += (torch.argmax(preds, dim=1) == y).sum() if valid_dataloader is not None: net.eval() # 将网络置于训练模式以进行 Dropout 和 Batch Normalization valid_loss = torch.tensor(0., device=device) valid_accuracy = torch.tensor(0., device=device) with torch.no_grad(): for X, y in valid_dataloader: X = X.to(device) y = y.to(device) preds = net(X) loss = criterion(preds, y) valid_loss += loss * valid_dataloader.batch_size valid_accuracy += (torch.argmax(preds, dim=1) == y).sum() if scheduler is not None: scheduler.step() print(f'Training loss: {train_loss/len(train_dataloader.dataset):.2f}') print(f'Training accuracy: {100*train_accuracy/len(train_dataloader.dataset):.2f}') if valid_dataloader is not None: print(f'Valid loss: {valid_loss/len(valid_dataloader.dataset):.2f}') print(f'Valid accuracy: {100*valid_accuracy/len(valid_dataloader.dataset):.2f}') #设定多少个epoch之后保存模型 if epoch%checkpoint_epochs==0: torch.save({ 'epoch': epoch, 'state_dict': net.state_dict(), 'optimizer': optimizer.state_dict(), }, './checkpoint.pth.tar') print() end = time.time() print(f'Total training time: {end-start:.1f} seconds') return net
在这个笔记本中,我们最多只使用一个 GPU,如果你有更多的 GPU 和/或设备,你可以自由地重构代码以使用 DistributedDataParallel。
微调时,网络主体的模型参数使用比头部更低的学习率进行训练,因为对于后者,我们必须从头开始训练它们。 我们依靠 PyTorch 的 Parameter Groups 为这两个组定义两个学习率,并使用带有 weight_decay = 5e-4
的 Adam 优化器(通过超参数搜索找到)。
device = 'cuda' if torch.cuda.is_available() else 'cpu'
lr, weight_decay, epochs = 1e-5, 5e-4, 20
net = get_net().to(device)
criterion = torch.nn.CrossEntropyLoss()
# params_1x 是网络主体的参数,即除FC层之外的所有层的参数。设定fc层学习率是其它层的10倍
params_1x = [param for name, param in net.named_parameters() if 'fc' not in str(name)]
optimizer = torch.optim.Adam([{'params':params_1x}, {'params': net.fc.parameters(), 'lr': lr*10}], lr=lr, weight_decay=weight_decay)
net = train(net, train_dataloader, valid_dataloader, criterion, optimizer, None, epochs, device)
在评估了验证集上的模型性能之后,我们希望在完整的训练 + 验证数据上重新训练模型,以在将结果提交给 Kaggle 之前压缩所有剩余的性能。 作为一般规则,我们训练的数据越多,结果就越好。
lr, weight_decay, epochs = 1e-5, 5e-4, 20
net = get_net().to(device)
criterion = torch.nn.CrossEntropyLoss()
params_1x = [param for name, param in net.named_parameters() if 'fc' not in str(name)]
optimizer = torch.optim.Adam([{'params':params_1x}, {'params': net.fc.parameters(), 'lr': lr*10}], lr=lr, weight_decay=weight_decay)
net = train(net, train_valid_dataloader, None, criterion, optimizer, None, epochs, device)
在完整标记的数据集上重新训练网络后,我们准备对测试集进行评分并将结果提交给 Kaggle。 我们遍历 test_dataloader 以获得每个测试图像的预测标签,然后创建一个最终的 DataFrame,就像 sampleSubmission.csv 中提供的那样,以在 Kaggle 上使用。
preds = []
net.eval()
with torch.no_grad():
for X, _ in test_dataloader:
X = X.to(device)
preds.extend(net(X).argmax(dim=1).type(torch.int32).cpu().numpy())
ids = list(range(1, len(test_dataset)+1))
ids.sort(key=lambda x: str(x))
df = pd.DataFrame({'id': ids, 'label': preds})
df['label'] = df['label'].apply(lambda x: train_dataset.classes[x])
df.to_csv('submission.csv', index=False)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。