本節課内容目标檢測基礎、圖像風格遷移、圖像分類案例1
一、目标檢測基礎
錨框
目标檢測算法通常會在輸入圖像中采樣大量的區域,然後判斷這些區域中是否包含我們感興趣的目标,并調整區域邊緣進而更準确地預測目标的真實邊界框(ground-truth bounding box)。不同的模型使用的區域采樣方法可能不同。這裡我們介紹其中的一種方法:它以每個像素為中心生成多個大小和寬高比(aspect ratio)不同的邊界框。這些邊界框被稱為錨框(anchor box)。我們将在後面基于錨框實踐目标檢測。
import numpy as np
import math
import torch
import os
IMAGE_DIR = '/home/kesci/input/img2083/img/'
print(torch.__version__)
生成多個錨框

def MultiBoxPrior(feature_map, sizes=[0.75, 0.5, 0.25], ratios=[1, 2, 0.5]):
"""
# 按照「9.4.1. 生成多個錨框」所講的實作, anchor表示成(xmin, ymin, xmax, ymax).
https://zh.d2l.ai/chapter_computer-vision/anchor.html
Args:
feature_map: torch tensor, Shape: [N, C, H, W].
sizes: List of sizes (0~1) of generated MultiBoxPriores.
ratios: List of aspect ratios (non-negative) of generated MultiBoxPriores.
Returns:
anchors of shape (1, num_anchors, 4). 由于batch裡每個都一樣, 是以第一維為1
"""
pairs = [] # pair of (size, sqrt(ration))
# 生成n + m -1個框
for r in ratios:
pairs.append([sizes[0], math.sqrt(r)])
for s in sizes[1:]:
pairs.append([s, math.sqrt(ratios[0])])
pairs = np.array(pairs)
# 生成相對于坐标中心點的框(x,y,x,y)
ss1 = pairs[:, 0] * pairs[:, 1] # size * sqrt(ration)
ss2 = pairs[:, 0] / pairs[:, 1] # size / sqrt(ration)
base_anchors = np.stack([-ss1, -ss2, ss1, ss2], axis=1) / 2
#将坐标點和anchor組合起來生成hw(n+m-1)個框輸出
h, w = feature_map.shape[-2:]
shifts_x = np.arange(0, w) / w
shifts_y = np.arange(0, h) / h
shift_x, shift_y = np.meshgrid(shifts_x, shifts_y)
shift_x = shift_x.reshape(-1)
shift_y = shift_y.reshape(-1)
shifts = np.stack((shift_x, shift_y, shift_x, shift_y), axis=1)
anchors = shifts.reshape((-1, 1, 4)) + base_anchors.reshape((1, -1, 4))
return torch.tensor(anchors, dtype=torch.float32).view(1, -1, 4)
我們看到,傳回錨框變量y的形狀為(1,錨框個數,4)。将錨框變量y的形狀變為(圖像高,圖像寬,以相同像素為中心的錨框個數,4)後,我們就可以通過指定像素位置來擷取所有以該像素為中心的錨框了。下面的例子裡我們通路以(250,250)為中心的第一個錨框。它有4個元素,分别是錨框左上角的x和y軸坐标和右下角的x和y軸坐标,其中x和y軸的坐标值分别已除以圖像的寬和高,是以值域均為0和1之間。
def show_bboxes(axes, bboxes, labels=None, colors=None):
def _make_list(obj, default_values=None):
if obj is None:
obj = default_values
elif not isinstance(obj, (list, tuple)):
obj = [obj]
return obj
labels = _make_list(labels)
colors = _make_list(colors, ['b', 'g', 'r', 'm', 'c'])
for i, bbox in enumerate(bboxes):
color = colors[i % len(colors)]
rect = d2l.bbox_to_rect(bbox.detach().cpu().numpy(), color)
axes.add_patch(rect)
if labels and len(labels) > i:
text_color = 'k' if color == 'w' else 'w'
axes.text(rect.xy[0], rect.xy[1], labels[i],
va='center', ha='center', fontsize=6, color=text_color,
bbox=dict(facecolor=color, lw=0))
# 展示 250 250像素點的anchor
d2l.set_figsize()
fig = d2l.plt.imshow(img)
bbox_scale = torch.tensor([[w, h, w, h]], dtype=torch.float32)
show_bboxes(fig.axes, boxes[250, 250, :, :] * bbox_scale,
['s=0.75, r=1', 's=0.75, r=2', 's=0.75, r=0.5', 's=0.5, r=1', 's=0.25, r=1'])
交并比
我們剛剛提到某個錨框較好地覆寫了圖像中的狗。如果該目标的真實邊界框已知,這裡的“較好”該如何量化呢?一種直覺的方法是衡量錨框和真實邊界框之間的相似度。我們知道,Jaccard系數(Jaccard index)可以衡量兩個集合的相似度。給定集合A和B ,它們的Jaccard系數即二者交集大小除以二者并集大小:
實際上,我們可以把邊界框内的像素區域看成是像素的集合。如此一來,我們可以用兩個邊界框的像素集合的Jaccard系數衡量這兩個邊界框的相似度。當衡量兩個邊界框的相似度時,我們通常将Jaccard系數稱為交并比(Intersection over Union,IoU),即兩個邊界框相交面積與相并面積之比,如下圖所示。交并比的取值範圍在0和1之間:0表示兩個邊界框無重合像素,1表示兩個邊界框相等。
# 以下函數已儲存在d2lzh_pytorch包中友善以後使用
def compute_intersection(set_1, set_2):
"""
計算anchor之間的交集
Args:
set_1: a tensor of dimensions (n1, 4), anchor表示成(xmin, ymin, xmax, ymax)
set_2: a tensor of dimensions (n2, 4), anchor表示成(xmin, ymin, xmax, ymax)
Returns:
intersection of each of the boxes in set 1 with respect to each of the boxes in set 2, shape: (n1, n2)
"""
# PyTorch auto-broadcasts singleton dimensions
lower_bounds = torch.max(set_1[:, :2].unsqueeze(1), set_2[:, :2].unsqueeze(0)) # (n1, n2, 2)
upper_bounds = torch.min(set_1[:, 2:].unsqueeze(1), set_2[:, 2:].unsqueeze(0)) # (n1, n2, 2)
intersection_dims = torch.clamp(upper_bounds - lower_bounds, min=0) # (n1, n2, 2)
return intersection_dims[:, :, 0] * intersection_dims[:, :, 1] # (n1, n2)
def compute_jaccard(set_1, set_2):
"""
計算anchor之間的Jaccard系數(IoU)
Args:
set_1: a tensor of dimensions (n1, 4), anchor表示成(xmin, ymin, xmax, ymax)
set_2: a tensor of dimensions (n2, 4), anchor表示成(xmin, ymin, xmax, ymax)
Returns:
Jaccard Overlap of each of the boxes in set 1 with respect to each of the boxes in set 2, shape: (n1, n2)
"""
# Find intersections
intersection = compute_intersection(set_1, set_2) # (n1, n2)
# Find areas of each box in both sets
areas_set_1 = (set_1[:, 2] - set_1[:, 0]) * (set_1[:, 3] - set_1[:, 1]) # (n1)
areas_set_2 = (set_2[:, 2] - set_2[:, 0]) * (set_2[:, 3] - set_2[:, 1]) # (n2)
# Find the union
# PyTorch auto-broadcasts singleton dimensions
union = areas_set_1.unsqueeze(1) + areas_set_2.unsqueeze(0) - intersection # (n1, n2)
return intersection / union # (n1, n2)
标注訓練集的錨框
二、圖像風格遷移
樣式遷移
樣式遷移需要兩張輸入圖像,一張是内容圖像,另一張是樣式圖像,我們将使用神經網絡修改内容圖像使其在樣式上接近樣式圖像。下圖中的内容圖像為本書作者在西雅圖郊區的雷尼爾山國家公園(Mount Rainier National Park)拍攝的風景照,而樣式圖像則是一副主題為秋天橡樹的油畫。最終輸出的合成圖像在保留了内容圖像中物體主體形狀的情況下應用了樣式圖像的油畫筆觸,同時也讓整體顔色更加鮮豔。
方法
下圖用一個例子來闡述基于卷積神經網絡的樣式遷移方法。首先,我們初始化合成圖像,例如将其初始化成内容圖像。該合成圖像是樣式遷移過程中唯一需要更新的變量,即樣式遷移所需疊代的模型參數。然後,我們選擇一個預訓練的卷積神經網絡來抽取圖像的特征,其中的模型參數在訓練中無須更新。深度卷積神經網絡憑借多個層逐級抽取圖像的特征。我們可以選擇其中某些層的輸出作為内容特征或樣式特征。以圖9.13為例,這裡選取的預訓練的神經網絡含有3個卷積層,其中第二層輸出圖像的内容特征,而第一層和第三層的輸出被作為圖像的樣式特征。接下來,我們通過正向傳播(實線箭頭方向)計算樣式遷移的損失函數,并通過反向傳播(虛線箭頭方向)疊代模型參數,即不斷更新合成圖像。樣式遷移常用的損失函數由3部分組成:内容損失(content loss)使合成圖像與内容圖像在内容特征上接近,樣式損失(style loss)令合成圖像與樣式圖像在樣式特征上接近,而總變差損失(total variation loss)則有助于減少合成圖像中的噪點。最後,當模型訓練結束時,我們輸出樣式遷移的模型參數,即得到最終的合成圖像。
%matplotlib inline
import time
import torch
import torch.nn.functional as F
import torchvision
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image
import sys
sys.path.append("/home/kesci/input")
import d2len9900 as d2l
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # 均已測試
print(device, torch.__version__)
預處理和後處理
rgb_mean = np.array([0.485, 0.456, 0.406])
rgb_std = np.array([0.229, 0.224, 0.225])
def preprocess(PIL_img, image_shape):
process = torchvision.transforms.Compose([
torchvision.transforms.Resize(image_shape),
torchvision.transforms.ToTensor(),
torchvision.transforms.Normalize(mean=rgb_mean, std=rgb_std)])
return process(PIL_img).unsqueeze(dim = 0) # (batch_size, 3, H, W)
def postprocess(img_tensor):
inv_normalize = torchvision.transforms.Normalize(
mean= -rgb_mean / rgb_std,
std= 1/rgb_std)
to_PIL_image = torchvision.transforms.ToPILImage()
return to_PIL_image(inv_normalize(img_tensor[0].cpu()).clamp(0, 1))
抽取特征
!echo $TORCH_HOME # 将會把預訓練好的模型下載下傳到此處(沒有輸出的話預設是.cache/torch)
pretrained_net = torchvision.models.vgg19(pretrained=False)
pretrained_net.load_state_dict(torch.load('/home/kesci/input/vgg193427/vgg19-dcbb9e9d.pth'))
style_layers, content_layers = [0, 5, 10, 19, 28], [25]
net_list = []
for i in range(max(content_layers + style_layers) + 1):
net_list.append(pretrained_net.features[i])
net = torch.nn.Sequential(*net_list)
def extract_features(X, content_layers, style_layers):
contents = []
styles = []
for i in range(len(net)):
X = net[i](X)
if i in style_layers:
styles.append(X)
if i in content_layers:
contents.append(X)
return contents, styles
def get_contents(image_shape, device):
content_X = preprocess(content_img, image_shape).to(device)
contents_Y, _ = extract_features(content_X, content_layers, style_layers)
return content_X, contents_Y
def get_styles(image_shape, device):
style_X = preprocess(style_img, image_shape).to(device)
_, styles_Y = extract_features(style_X, content_layers, style_layers)
return style_X, styles_Y
定義損失函數
内容損失:與線性回歸中的損失函數類似,内容損失通過平方誤差函數衡量合成圖像與内容圖像在内容特征上的差異。平方誤差函數的兩個輸入均為extract_features函數計算所得到的内容層的輸出。
def content_loss(Y_hat, Y):
return F.mse_loss(Y_hat, Y)
樣式損失:樣式損失也一樣通過平方誤差函數衡量合成圖像與樣式圖像在樣式上的差異。為了表達樣式層輸出的樣式,我們先通過extract_features函數計算樣式層的輸出。
def gram(X):
num_channels, n = X.shape[1], X.shape[2] * X.shape[3]
X = X.view(num_channels, n)
return torch.matmul(X, X.t()) / (num_channels * n)
def style_loss(Y_hat, gram_Y):
return F.mse_loss(gram(Y_hat), gram_Y)
總損失:有時候,我們學到的合成圖像裡面有大量高頻噪點,即有特别亮或者特别暗的顆粒像素。一種常用的降噪方法是總變差降噪(total variation denoising)。
def tv_loss(Y_hat):
return 0.5 * (F.l1_loss(Y_hat[:, :, 1:, :], Y_hat[:, :, :-1, :]) +
F.l1_loss(Y_hat[:, :, :, 1:], Y_hat[:, :, :, :-1]))
損失函數:樣式遷移的損失函數即内容損失、樣式損失和總變差損失的權重和。通過調節這些權值超參數,我們可以權衡合成圖像在保留内容、遷移樣式以及降噪三方面的相對重要性。
content_weight, style_weight, tv_weight = 1, 1e3, 10
def compute_loss(X, contents_Y_hat, styles_Y_hat, contents_Y, styles_Y_gram):
# 分别計算内容損失、樣式損失和總變差損失
contents_l = [content_loss(Y_hat, Y) * content_weight for Y_hat, Y in zip(
contents_Y_hat, contents_Y)]
styles_l = [style_loss(Y_hat, Y) * style_weight for Y_hat, Y in zip(
styles_Y_hat, styles_Y_gram)]
tv_l = tv_loss(X) * tv_weight
# 對所有損失求和
l = sum(styles_l) + sum(contents_l) + tv_l
return contents_l, styles_l, tv_l, l
建立和初始化合成圖像
class GeneratedImage(torch.nn.Module):
def __init__(self, img_shape):
super(GeneratedImage, self).__init__()
self.weight = torch.nn.Parameter(torch.rand(*img_shape))
def forward(self):
return self.weight
def get_inits(X, device, lr, styles_Y):
'''
建立了合成圖像的模型執行個體,并将其初始化為圖像X。樣式圖像在各個樣式層的格拉姆矩陣styles_Y_gram将在訓練前預先計算好。
'''
gen_img = GeneratedImage(X.shape).to(device)
gen_img.weight.data = X.data
optimizer = torch.optim.Adam(gen_img.parameters(), lr=lr)
styles_Y_gram = [gram(Y) for Y in styles_Y]
return gen_img(), styles_Y_gram, optimizer
訓練
def train(X, contents_Y, styles_Y, device, lr, max_epochs, lr_decay_epoch):
print("training on ", device)
X, styles_Y_gram, optimizer = get_inits(X, device, lr, styles_Y)
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, lr_decay_epoch, gamma=0.1)
for i in range(max_epochs):
start = time.time()
contents_Y_hat, styles_Y_hat = extract_features(
X, content_layers, style_layers)
contents_l, styles_l, tv_l, l = compute_loss(
X, contents_Y_hat, styles_Y_hat, contents_Y, styles_Y_gram)
optimizer.zero_grad()
l.backward(retain_graph = True)
optimizer.step()
scheduler.step()
if i % 50 == 0 and i != 0:
print('epoch %3d, content loss %.2f, style loss %.2f, '
'TV loss %.2f, %.2f sec'
% (i, sum(contents_l).item(), sum(styles_l).item(), tv_l.item(),
time.time() - start))
return X.detach()
三、圖像分類案例1
比較簡單,在此略過
注:
以上所有内容均來自伯禹平台動手學深度學習課程