先說(shuō)說(shuō)數(shù)據(jù)增強(qiáng)
大規(guī)模數(shù)據(jù)集是成功應(yīng)用深度神經(jīng)網(wǎng)絡(luò)的前提。圖像增廣(image augmentation)技術(shù)通過(guò)對(duì)訓(xùn)練圖像做一系列隨機(jī)改變,來(lái)產(chǎn)生相似但又不同的訓(xùn)練樣本,從而擴(kuò)大訓(xùn)練數(shù)據(jù)集的規(guī)模。圖像增廣的另一種解釋是,隨機(jī)改變訓(xùn)練樣本可以降低模型對(duì)某些屬性的依賴(lài),從而提高模型的泛化能力。例如,我們可以對(duì)圖像進(jìn)行不同方式的裁剪,使感興趣的物體出現(xiàn)在不同位置,從而減輕模型對(duì)物體出現(xiàn)位置的依賴(lài)性。我們也可以調(diào)整亮度、色彩等因素來(lái)降低模型對(duì)色彩的敏感度??梢哉f(shuō),在當(dāng)年AlexNet的成功中,圖像增廣技術(shù)功不可沒(méi)。
顯示圖像:
def show_images(imgs, num_rows, num_cols, scale=2):
figsize = (num_cols * scale, num_rows * scale)
_, axes = d2l.plt.subplots(num_rows, num_cols, figsize=figsize)
for i in range(num_rows):
for j in range(num_cols):
axes[i][j].imshow(imgs[i * num_cols + j])
axes[i][j].axes.get_xaxis().set_visible(False)
axes[i][j].axes.get_yaxis().set_visible(False)
return axes
構(gòu)造輔助函數(shù):
def apply(img, aug, num_rows=2, num_cols=4, scale=1.5):
Y = [aug(img) for _ in range(num_rows * num_cols)]
show_images(Y, num_rows, num_cols, scale)
翻轉(zhuǎn)和裁剪
左右翻轉(zhuǎn)圖像通常不改變物體的類(lèi)別。它是最早也是最廣泛使用的一種圖像增廣方法。下面我們通過(guò)torchvision.transforms模塊創(chuàng)建RandomHorizontalFlip實(shí)例來(lái)實(shí)現(xiàn)一半概率的圖像水平(左右)翻轉(zhuǎn)。
apply(img, torchvision.transforms.RandomHorizontalFlip())

上下翻轉(zhuǎn)不如左右翻轉(zhuǎn)通用。但是至少對(duì)于樣例圖像,上下翻轉(zhuǎn)不會(huì)造成識(shí)別障礙。下面我們創(chuàng)建RandomVerticalFlip實(shí)例來(lái)實(shí)現(xiàn)一半概率的圖像垂直(上下)翻轉(zhuǎn)。
apply(img, torchvision.transforms.RandomVerticalFlip())

變化顏色。我們可以從4個(gè)方面改變圖像的顏色:
亮度(brightness)、對(duì)比度(contrast)、飽和度(saturation)和色調(diào)(hue)。在下面的例子里,我們將圖像的亮度隨機(jī)變化為原圖亮度的()()。apply(img, torchvision.transforms.ColorJitter(brightness=0.5, contrast=0, saturation=0, hue=0))
img, torchvision.transforms.ColorJitter(brightness=0, contrast=0, saturation=0, hue=0.5)另外,我們還可以疊加多個(gè)圖像增廣方法
augs = torchvision.transforms.Compose([
torchvision.transforms.RandomHorizontalFlip(), color_aug, shape_aug])
apply(img, augs)
- 為了在預(yù)測(cè)時(shí)得到確定的結(jié)果,我們通常只將圖像增廣應(yīng)用在訓(xùn)練樣本上,而不在預(yù)測(cè)時(shí)使用含隨機(jī)操作的圖像增廣。在這里我們只使用最簡(jiǎn)單的隨機(jī)左右翻轉(zhuǎn)。此外,我們使用ToTensor將小批量圖像轉(zhuǎn)成PyTorch需要的格式,即形狀為(批量大小, 通道數(shù), 高, 寬)、值域在0到1之間且類(lèi)型為32位浮點(diǎn)數(shù)。
flip_aug = torchvision.transforms.Compose([
torchvision.transforms.RandomHorizontalFlip(),
torchvision.transforms.ToTensor()])
no_aug = torchvision.transforms.Compose([
torchvision.transforms.ToTensor()])
num_workers = 0 if sys.platform.startswith('win32') else 4
def load_cifar10(is_train, augs, batch_size, root=CIFAR_ROOT_PATH):
dataset = torchvision.datasets.CIFAR10(root=root, train=is_train, transform=augs, download=False)
return DataLoader(dataset, batch_size=batch_size, shuffle=is_train, num_workers=num_workers)
def train_with_data_aug(train_augs, test_augs, lr=0.001):
# 設(shè)計(jì)方法使得訓(xùn)練圖像進(jìn)行aug圖像處理
batch_size, net = 256, d2l.resnet18(10)
optimizer = torch.optim.Adam(net.parameters(), lr=lr)
loss = torch.nn.CrossEntropyLoss()
train_iter = load_cifar10(True, train_augs, batch_size)
test_iter = load_cifar10(False, test_augs, batch_size)
train(train_iter, test_iter, net, loss, optimizer, device, num_epochs=10)
模型微調(diào)
在前面的一些章節(jié)中,我們介紹了如何在只有6萬(wàn)張圖像的Fashion-MNIST訓(xùn)練數(shù)據(jù)集上訓(xùn)練模型。我們還描述了學(xué)術(shù)界當(dāng)下使用最廣泛的大規(guī)模圖像數(shù)據(jù)集ImageNet,它有超過(guò)1,000萬(wàn)的圖像和1,000類(lèi)的物體。然而,我們平常接觸到數(shù)據(jù)集的規(guī)模通常在這兩者之間。
假設(shè)我們想從圖像中識(shí)別出不同種類(lèi)的椅子,然后將購(gòu)買(mǎi)鏈接推薦給用戶(hù)。一種可能的方法是先找出100種常見(jiàn)的椅子,為每種椅子拍攝1,000張不同角度的圖像,然后在收集到的圖像數(shù)據(jù)集上訓(xùn)練一個(gè)分類(lèi)模型。這個(gè)椅子數(shù)據(jù)集雖然可能比Fashion-MNIST數(shù)據(jù)集要龐大,但樣本數(shù)仍然不及ImageNet數(shù)據(jù)集中樣本數(shù)的十分之一。這可能會(huì)導(dǎo)致適用于ImageNet數(shù)據(jù)集的復(fù)雜模型在這個(gè)椅子數(shù)據(jù)集上過(guò)擬合。同時(shí),因?yàn)閿?shù)據(jù)量有限,最終訓(xùn)練得到的模型的精度也可能達(dá)不到實(shí)用的要求。
為了應(yīng)對(duì)上述問(wèn)題,一個(gè)顯而易見(jiàn)的解決辦法是收集更多的數(shù)據(jù)。然而,收集和標(biāo)注數(shù)據(jù)會(huì)花費(fèi)大量的時(shí)間和資金。例如,為了收集ImageNet數(shù)據(jù)集,研究人員花費(fèi)了數(shù)百萬(wàn)美元的研究經(jīng)費(fèi)。雖然目前的數(shù)據(jù)采集成本已降低了不少,但其成本仍然不可忽略。
另外一種解決辦法是應(yīng)用遷移學(xué)習(xí)(transfer learning),將從源數(shù)據(jù)集學(xué)到的知識(shí)遷移到目標(biāo)數(shù)據(jù)集上。例如,雖然ImageNet數(shù)據(jù)集的圖像大多跟椅子無(wú)關(guān),但在該數(shù)據(jù)集上訓(xùn)練的模型可以抽取較通用的圖像特征,從而能夠幫助識(shí)別邊緣、紋理、形狀和物體組成等。這些類(lèi)似的特征對(duì)于識(shí)別椅子也可能同樣有效。
本節(jié)我們介紹遷移學(xué)習(xí)中的一種常用技術(shù):微調(diào)(fine tuning)。如圖9.1所示,微調(diào)由以下4步構(gòu)成。
- 在源數(shù)據(jù)集(如ImageNet數(shù)據(jù)集)上預(yù)訓(xùn)練一個(gè)神經(jīng)網(wǎng)絡(luò)模型,即源模型。
- 創(chuàng)建一個(gè)新的神經(jīng)網(wǎng)絡(luò)模型,即目標(biāo)模型。它復(fù)制了源模型上除了輸出層外的所有模型設(shè)計(jì)及其參數(shù)。我們假設(shè)這些模型參數(shù)包含了源數(shù)據(jù)集上學(xué)習(xí)到的知識(shí),且這些知識(shí)同樣適用于目標(biāo)數(shù)據(jù)集。我們還假設(shè)源模型的輸出層跟源數(shù)據(jù)集的標(biāo)簽緊密相關(guān),因此在目標(biāo)模型中不予采用。
- 為目標(biāo)模型添加一個(gè)輸出大小為目標(biāo)數(shù)據(jù)集類(lèi)別個(gè)數(shù)的輸出層,并隨機(jī)初始化該層的模型參數(shù)。
- 在目標(biāo)數(shù)據(jù)集(如椅子數(shù)據(jù)集)上訓(xùn)練目標(biāo)模型。我們將從頭訓(xùn)練輸出層,而其余層的參數(shù)都是基于源模型的參數(shù)微調(diào)得到的。
舉個(gè)例子
我們將基于一個(gè)小數(shù)據(jù)集對(duì)在ImageNet數(shù)據(jù)集上訓(xùn)練好的ResNet模型進(jìn)行微調(diào)。該小數(shù)據(jù)集含有數(shù)千張包含熱狗和不包含熱狗的圖像。我們將使用微調(diào)得到的模型來(lái)識(shí)別一張圖像中是否包含熱狗。
首先,導(dǎo)入實(shí)驗(yàn)所需的包或模塊。torchvision的models包提供了常用的預(yù)訓(xùn)練模型。如果希望獲取更多的預(yù)訓(xùn)練模型,可以使用使用pretrained-models.pytorch倉(cāng)庫(kù)。
完整代碼:
%matplotlib inline
import torch
from torch import nn, optim
from torch.utils.data import Dataset, DataLoader
import torchvision
from torchvision.datasets import ImageFolder
from torchvision import transforms
from torchvision import models
import os
import sys
sys.path.append("/home/kesci/input/")
import d2lzh1981 as d2l
os.environ["CUDA_VISIBLE_DEVICES"] = "0"
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
使用的熱狗數(shù)據(jù)集(點(diǎn)擊下載)是從網(wǎng)上抓取的,它含有1400張包含熱狗的正類(lèi)圖像,和同樣多包含其他食品的負(fù)類(lèi)圖像。各類(lèi)的1000張圖像被用于訓(xùn)練,其余則用于測(cè)試。
我們首先將壓縮后的數(shù)據(jù)集下載到路徑data_dir之下,然后在該路徑將下載好的數(shù)據(jù)集解壓,得到兩個(gè)文件夾hotdog/train和hotdog/test。這兩個(gè)文件夾下面均有hotdog和not-hotdog兩個(gè)類(lèi)別文件夾,每個(gè)類(lèi)別文件夾里面是圖像文件。
我們創(chuàng)建兩個(gè)ImageFolder實(shí)例來(lái)分別讀取訓(xùn)練數(shù)據(jù)集和測(cè)試數(shù)據(jù)集中的所有圖像文件。
import os
os.listdir('/home/kesci/input/resnet185352')
data_dir = '/home/kesci/input/hotdog4014'
os.listdir(os.path.join(data_dir, "hotdog"))
train_imgs = ImageFolder(os.path.join(data_dir, 'hotdog/train'))
test_imgs = ImageFolder(os.path.join(data_dir, 'hotdog/test'))
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);

在訓(xùn)練時(shí),我們先從圖像中裁剪出隨機(jī)大小和隨機(jī)高寬比的一塊隨機(jī)區(qū)域,然后將該區(qū)域縮放為高和寬均為224像素的輸入。測(cè)試時(shí),我們將圖像的高和寬均縮放為256像素,然后從中裁剪出高和寬均為224像素的中心區(qū)域作為輸入。此外,我們對(duì)RGB(紅、綠、藍(lán))三個(gè)顏色通道的數(shù)值做標(biāo)準(zhǔn)化:每個(gè)數(shù)值減去該通道所有數(shù)值的平均值,再除以該通道所有數(shù)值的標(biāo)準(zhǔn)差作為輸出。
注: 在使用預(yù)訓(xùn)練模型時(shí),一定要和預(yù)訓(xùn)練時(shí)作同樣的預(yù)處理。
normalize = transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
train_augs = transforms.Compose([
transforms.RandomResizedCrop(size=224),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
normalize
])
test_augs = transforms.Compose([
transforms.Resize(size=256),
transforms.CenterCrop(size=224),
transforms.ToTensor(),
normalize
])
定義和初始化模型
我們使用在ImageNet數(shù)據(jù)集上預(yù)訓(xùn)練的ResNet-18作為源模型。這里指定pretrained=True來(lái)自動(dòng)下載并加載預(yù)訓(xùn)練的模型參數(shù)。在第一次使用時(shí)需要聯(lián)網(wǎng)下載模型參數(shù)。
pretrained_net = models.resnet18(pretrained=False)
pretrained_net.load_state_dict(torch.load('/home/kesci/input/resnet185352/resnet18-5c106cde.pth'))
下面打印源模型的成員變量fc。作為一個(gè)全連接層,它將ResNet最終的全局平均池化層輸出變換成ImageNet數(shù)據(jù)集上1000類(lèi)的輸出。
print(pretrained_net.fc)
注: 如果你使用的是其他模型,那可能沒(méi)有成員變量
fc(比如models中的VGG預(yù)訓(xùn)練模型),所以正確做法是查看對(duì)應(yīng)模型源碼中其定義部分,這樣既不會(huì)出錯(cuò)也能加深我們對(duì)模型的理解。pretrained-models.pytorch倉(cāng)庫(kù)貌似統(tǒng)一了接口,但是我還是建議使用時(shí)查看一下對(duì)應(yīng)模型的源碼。
可見(jiàn)此時(shí)pretrained_net最后的輸出個(gè)數(shù)等于目標(biāo)數(shù)據(jù)集的類(lèi)別數(shù)1000。所以我們應(yīng)該將最后的fc成修改我們需要的輸出類(lèi)別數(shù):
pretrained_net.fc = nn.Linear(512, 2)
print(pretrained_net.fc)
Linear(in_features=512, out_features=2, bias=True)
此時(shí),pretrained_net的fc層就被隨機(jī)初始化了,但是其他層依然保存著預(yù)訓(xùn)練得到的參數(shù)。由于是在很大的ImageNet數(shù)據(jù)集上預(yù)訓(xùn)練的,所以參數(shù)已經(jīng)足夠好,因此一般只需使用較小的學(xué)習(xí)率來(lái)微調(diào)這些參數(shù),而fc中的隨機(jī)初始化參數(shù)一般需要更大的學(xué)習(xí)率從頭訓(xùn)練。PyTorch可以方便的對(duì)模型的不同部分設(shè)置不同的學(xué)習(xí)參數(shù),我們?cè)谙旅娲a中將fc的學(xué)習(xí)率設(shè)為已經(jīng)預(yù)訓(xùn)練過(guò)的部分的10倍。
output_params = list(map(id, pretrained_net.fc.parameters()))
feature_params = filter(lambda p: id(p) not in output_params, pretrained_net.parameters())
lr = 0.01
optimizer = optim.SGD([{'params': feature_params},
{'params': pretrained_net.fc.parameters(), 'lr': lr * 10}],
lr=lr, weight_decay=0.001)
# 模型微調(diào)
def train_fine_tuning(net, optimizer, batch_size=128, num_epochs=5):
train_iter = DataLoader(ImageFolder(os.path.join(data_dir, 'hotdog/train'), transform=train_augs),
batch_size, shuffle=True)
test_iter = DataLoader(ImageFolder(os.path.join(data_dir, 'hotdog/test'), transform=test_augs),
batch_size)
loss = torch.nn.CrossEntropyLoss()
d2l.train(train_iter, test_iter, net, loss, optimizer, device, num_epochs)