PyTorch 進行神經(jīng)傳遞

2020-09-07 16:20 更新
原文: https://pytorch.org/tutorials/advanced/neural_style_tutorial.html

作者: Alexis Jacq

編輯:溫斯頓·鯡魚

介紹

本教程說明了如何實現(xiàn)由 Leon A. Gatys,Alexander S. Ecker 和 Matthias Bethge 開發(fā)的神經(jīng)樣式算法。 神經(jīng)風(fēng)格(Neural-Style)或神經(jīng)傳遞(Neural-Transfer)使您可以拍攝圖像并以新的藝術(shù)風(fēng)格對其進行再現(xiàn)。 該算法獲取三個圖像,即輸入圖像,內(nèi)容圖像和樣式圖像,然后更改輸入以使其類似于內(nèi)容圖像的內(nèi)容和樣式圖像的藝術(shù)風(fēng)格。

content1

基本原理

原理很簡單:我們定義了兩個距離,一個為內(nèi)容( ),一個為樣式( )。  測量兩個圖像之間的內(nèi)容有多大不同,而 測量兩個圖像之間的樣式有多大不同。 然后,我們獲取第三個圖像(輸入),并將其轉(zhuǎn)換為最小化與內(nèi)容圖像的內(nèi)容距離和與樣式圖像的樣式距離。 現(xiàn)在我們可以導(dǎo)入必要的程序包并開始神經(jīng)傳遞。

導(dǎo)入軟件包并選擇設(shè)備

以下是實現(xiàn)神經(jīng)傳遞所需的軟件包列表。

  • torch,torch.nn,numpy(使用 PyTorch 的神經(jīng)網(wǎng)絡(luò)必不可少的軟件包)
  • torch.optim(有效梯度下降)
  • PIL,PIL.Image,matplotlib.pyplot(加載并顯示圖像)
  • torchvision.transforms(將 PIL 圖像轉(zhuǎn)換為張量)
  • torchvision.models(訓(xùn)練或負載預(yù)訓(xùn)練模型)
  • copy(用于深復(fù)制模型;系統(tǒng)軟件包)
  1. from __future__ import print_function
  2. import torch
  3. import torch.nn as nn
  4. import torch.nn.functional as F
  5. import torch.optim as optim
  6. from PIL import Image
  7. import matplotlib.pyplot as plt
  8. import torchvision.transforms as transforms
  9. import torchvision.models as models
  10. import copy

接下來,我們需要選擇要在哪個設(shè)備上運行網(wǎng)絡(luò)并導(dǎo)入內(nèi)容和樣式圖像。 在大圖像上運行神經(jīng)傳遞算法需要更長的時間,并且在 GPU 上運行時會更快。 我們可以使用torch.cuda.is_available()來檢測是否有 GPU。 接下來,我們設(shè)置torch.device以在整個教程中使用。 .to(device)方法也用于將張量或模塊移動到所需的設(shè)備。

  1. device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

加載圖像

現(xiàn)在,我們將導(dǎo)入樣式和內(nèi)容圖像。 原始的 PIL 圖像的值在 0 到 255 之間,但是當(dāng)轉(zhuǎn)換為torch張量時,其值將轉(zhuǎn)換為 0 到 1 之間。圖像也需要調(diào)整大小以具有相同的尺寸。 需要注意的一個重要細節(jié)是,使用從 0 到 1 的張量值對torch庫中的神經(jīng)網(wǎng)絡(luò)進行訓(xùn)練。如果嘗試為網(wǎng)絡(luò)提供 0 到 255 張量圖像,則激活的特征圖將無法感知預(yù)期的內(nèi)容 和風(fēng)格。 但是,使用 0 到 255 張量圖像對 Caffe 庫中的預(yù)訓(xùn)練網(wǎng)絡(luò)進行訓(xùn)練。

Note

以下是下載運行本教程所需的圖像的鏈接: picasso.jpg 和 dance.jpg 。 下載這兩個圖像并將它們添加到當(dāng)前工作目錄中名稱為images的目錄中。

  1. # desired size of the output image
  2. imsize = 512 if torch.cuda.is_available() else 128 # use small size if no gpu
  3. loader = transforms.Compose([
  4. transforms.Resize(imsize), # scale imported image
  5. transforms.ToTensor()]) # transform it into a torch tensor
  6. def image_loader(image_name):
  7. image = Image.open(image_name)
  8. # fake batch dimension required to fit network's input dimensions
  9. image = loader(image).unsqueeze(0)
  10. return image.to(device, torch.float)
  11. style_img = image_loader("./daimg/neural-style/picasso.jpg")
  12. content_img = image_loader("./daimg/neural-style/dancing.jpg")
  13. assert style_img.size() == content_img.size(), \
  14. "we need to import style and content images of the same size"

現(xiàn)在,讓我們創(chuàng)建一個顯示圖像的功能,方法是將圖像的副本轉(zhuǎn)換為 PIL 格式,然后使用plt.imshow顯示該副本。 我們將嘗試顯示內(nèi)容和樣式圖像,以確保正確導(dǎo)入它們。

  1. unloader = transforms.ToPILImage() # reconvert into PIL image
  2. plt.ion()
  3. def imshow(tensor, title=None):
  4. image = tensor.cpu().clone() # we clone the tensor to not do changes on it
  5. image = image.squeeze(0) # remove the fake batch dimension
  6. image = unloader(image)
  7. plt.imshow(image)
  8. if title is not None:
  9. plt.title(title)
  10. plt.pause(0.001) # pause a bit so that plots are updated
  11. plt.figure()
  12. imshow(style_img, title='Style Image')
  13. plt.figure()
  14. imshow(content_img, title='Content Image')
  • ../_images/sphx_glr_neural_style_tutorial_001.png
  • ../_images/sphx_glr_neural_style_tutorial_002.png

損失函數(shù)

內(nèi)容損失

內(nèi)容損失是代表單個圖層內(nèi)容距離的加權(quán)版本的函數(shù)。 該功能獲取網(wǎng)絡(luò)處理輸入 中層 的特征圖 ,并返回圖像 和內(nèi)容圖像 之間的加權(quán)內(nèi)容距離 。 為了計算內(nèi)容距離,該功能必須知道內(nèi)容圖像的特征圖( )。 我們將此功能實現(xiàn)為炬管模塊,并使用以 作為輸入的構(gòu)造函數(shù)。 距離 是兩組特征圖之間的均方誤差,可以使用nn.MSELoss進行計算。

我們將直接在用于計算內(nèi)容距離的卷積層之后添加此內(nèi)容丟失模塊。 這樣,每次向網(wǎng)絡(luò)饋入輸入圖像時,都會在所需層上計算內(nèi)容損失,并且由于自動漸變,將計算所有梯度。 現(xiàn)在,為了使內(nèi)容丟失層透明,我們必須定義一種forward方法,該方法計算內(nèi)容丟失,然后返回該層的輸入。 計算出的損耗將保存為模塊的參數(shù)。

  1. class ContentLoss(nn.Module):
  2. def __init__(self, target,):
  3. super(ContentLoss, self).__init__()
  4. # we 'detach' the target content from the tree used
  5. # to dynamically compute the gradient: this is a stated value,
  6. # not a variable. Otherwise the forward method of the criterion
  7. # will throw an error.
  8. self.target = target.detach()
  9. def forward(self, input):
  10. self.loss = F.mse_loss(input, self.target)
  11. return input

注意:

重要細節(jié):盡管此模塊名為ContentLoss,但它不是真正的 PyTorch Loss 函數(shù)。 如果要將內(nèi)容損失定義為 PyTorch 損失函數(shù),則必須創(chuàng)建一個 PyTorch autograd 函數(shù)以使用backward方法手動重新計算/實現(xiàn)漸變。

風(fēng)格損失

樣式丟失模塊的實現(xiàn)類似于內(nèi)容丟失模塊。 在網(wǎng)絡(luò)中它將充當(dāng)透明層,計算該層的樣式損失。 為了計算樣式損失,我們需要計算語法矩陣 。 gram 矩陣是給定矩陣與其轉(zhuǎn)置矩陣相乘的結(jié)果。 在此應(yīng)用程序中,給定的矩陣是圖層 的特征圖 的重塑版本。  被重塑以形成 ,  x  矩陣,其中 是第 層特征圖的數(shù)量, 是任何矢量化特征圖 的長度 ]。 例如, 的第一行對應(yīng)于第一矢量化特征圖 。

最后,必須通過將每個元素除以矩陣中元素的總數(shù)來對 gram 矩陣進行歸一化。 此歸一化是為了抵消 尺寸較大的 矩陣在 Gram 矩陣中產(chǎn)生較大值的事實。 這些較大的值將導(dǎo)致第一層(在合并池之前)在梯度下降期間具有較大的影響。 樣式特征往往位于網(wǎng)絡(luò)的更深層,因此此標準化步驟至關(guān)重要。

  1. def gram_matrix(input):
  2. a, b, c, d = input.size() # a=batch size(=1)
  3. # b=number of feature maps
  4. # (c,d)=dimensions of a f. map (N=c*d)
  5. features = input.view(a * b, c * d) # resise F_XL into \hat F_XL
  6. G = torch.mm(features, features.t()) # compute the gram product
  7. # we 'normalize' the values of the gram matrix
  8. # by dividing by the number of element in each feature maps.
  9. return G.div(a * b * c * d)

現(xiàn)在,樣式丟失模塊看起來幾乎與內(nèi)容丟失模塊完全一樣。 還使用 之間的均方誤差來計算樣式距離。

  1. class StyleLoss(nn.Module):
  2. def __init__(self, target_feature):
  3. super(StyleLoss, self).__init__()
  4. self.target = gram_matrix(target_feature).detach()
  5. def forward(self, input):
  6. G = gram_matrix(input)
  7. self.loss = F.mse_loss(G, self.target)
  8. return input

導(dǎo)入模型

現(xiàn)在我們需要導(dǎo)入一個預(yù)訓(xùn)練的神經(jīng)網(wǎng)絡(luò)。 我們將使用 19 層 VGG 網(wǎng)絡(luò),就像本文中使用的那樣。

PyTorch 的 VGG 實現(xiàn)是一個模塊,分為兩個子Sequential模塊:features(包含卷積和池化層)和classifier(包含完全連接的層)。 我們將使用features模塊,因為我們需要各個卷積層的輸出來測量內(nèi)容和樣式損失。 某些層在訓(xùn)練期間的行為與評估不同,因此我們必須使用.eval()將網(wǎng)絡(luò)設(shè)置為評估模式。

  1. cnn = models.vgg19(pretrained=True).features.to(device).eval()

另外,在圖像上訓(xùn)練 VGG 網(wǎng)絡(luò),每個通道的均值通過均值= [0.485,0.456,0.406]和 std = [0.229,0.224,0.225]歸一化。 在將其發(fā)送到網(wǎng)絡(luò)之前,我們將使用它們對圖像進行規(guī)范化。

  1. cnn_normalization_mean = torch.tensor([0.485, 0.456, 0.406]).to(device)
  2. cnn_normalization_std = torch.tensor([0.229, 0.224, 0.225]).to(device)
  3. ## create a module to normalize input image so we can easily put it in a
  4. ## nn.Sequential
  5. class Normalization(nn.Module):
  6. def __init__(self, mean, std):
  7. super(Normalization, self).__init__()
  8. # .view the mean and std to make them [C x 1 x 1] so that they can
  9. # directly work with image Tensor of shape [B x C x H x W].
  10. # B is batch size. C is number of channels. H is height and W is width.
  11. self.mean = torch.tensor(mean).view(-1, 1, 1)
  12. self.std = torch.tensor(std).view(-1, 1, 1)
  13. def forward(self, img):
  14. # normalize img
  15. return (img - self.mean) / self.std

Sequential模塊包含子模塊的有序列表。 例如,vgg19.features包含以正確的深度順序排列的序列(Conv2d,ReLU,MaxPool2d,Conv2d,ReLU…)。 我們需要在檢測到的卷積層之后立即添加內(nèi)容丟失層和樣式丟失層。 為此,我們必須創(chuàng)建一個新的Sequential模塊,該模塊具有正確插入的內(nèi)容丟失和樣式丟失模塊。

  1. # desired depth layers to compute style/content losses :
  2. content_layers_default = ['conv_4']
  3. style_layers_default = ['conv_1', 'conv_2', 'conv_3', 'conv_4', 'conv_5']
  4. def get_style_model_and_losses(cnn, normalization_mean, normalization_std,
  5. style_img, content_img,
  6. content_layers=content_layers_default,
  7. style_layers=style_layers_default):
  8. cnn = copy.deepcopy(cnn)
  9. # normalization module
  10. normalization = Normalization(normalization_mean, normalization_std).to(device)
  11. # just in order to have an iterable access to or list of content/syle
  12. # losses
  13. content_losses = []
  14. style_losses = []
  15. # assuming that cnn is a nn.Sequential, so we make a new nn.Sequential
  16. # to put in modules that are supposed to be activated sequentially
  17. model = nn.Sequential(normalization)
  18. i = 0 # increment every time we see a conv
  19. for layer in cnn.children():
  20. if isinstance(layer, nn.Conv2d):
  21. i += 1
  22. name = 'conv_{}'.format(i)
  23. elif isinstance(layer, nn.ReLU):
  24. name = 'relu_{}'.format(i)
  25. # The in-place version doesn't play very nicely with the ContentLoss
  26. # and StyleLoss we insert below. So we replace with out-of-place
  27. # ones here.
  28. layer = nn.ReLU(inplace=False)
  29. elif isinstance(layer, nn.MaxPool2d):
  30. name = 'pool_{}'.format(i)
  31. elif isinstance(layer, nn.BatchNorm2d):
  32. name = 'bn_{}'.format(i)
  33. else:
  34. raise RuntimeError('Unrecognized layer: {}'.format(layer.__class__.__name__))
  35. model.add_module(name, layer)
  36. if name in content_layers:
  37. # add content loss:
  38. target = model(content_img).detach()
  39. content_loss = ContentLoss(target)
  40. model.add_module("content_loss_{}".format(i), content_loss)
  41. content_losses.append(content_loss)
  42. if name in style_layers:
  43. # add style loss:
  44. target_feature = model(style_img).detach()
  45. style_loss = StyleLoss(target_feature)
  46. model.add_module("style_loss_{}".format(i), style_loss)
  47. style_losses.append(style_loss)
  48. # now we trim off the layers after the last content and style losses
  49. for i in range(len(model) - 1, -1, -1):
  50. if isinstance(model[i], ContentLoss) or isinstance(model[i], StyleLoss):
  51. break
  52. model = model[:(i + 1)]
  53. return model, style_losses, content_losses

接下來,我們選擇輸入圖像。 您可以使用內(nèi)容圖像或白噪聲的副本。

  1. input_img = content_img.clone()
  2. ## if you want to use white noise instead uncomment the below line:
  3. ## input_img = torch.randn(content_img.data.size(), device=device)
  4. ## add the original input image to the figure:
  5. plt.figure()
  6. imshow(input_img, title='Input Image')

../_images/sphx_glr_neural_style_tutorial_003.png

梯度下降

正如算法作者 Leon Gatys 在此處建議一樣,我們將使用 L-BFGS 算法來運行梯度下降。 與訓(xùn)練網(wǎng)絡(luò)不同,我們希望訓(xùn)練輸入圖像,以最大程度地減少內(nèi)容/樣式損失。 我們將創(chuàng)建一個 PyTorch L-BFGS 優(yōu)化器optim.LBFGS,并將圖像作為張量傳遞給它進行優(yōu)化。

  1. def get_input_optimizer(input_img):
  2. # this line to show that input is a parameter that requires a gradient
  3. optimizer = optim.LBFGS([input_img.requires_grad_()])
  4. return optimizer

最后,我們必須定義一個執(zhí)行神經(jīng)傳遞的函數(shù)。 對于網(wǎng)絡(luò)的每次迭代,它都會被提供更新的輸入并計算新的損耗。 我們將運行每個損失模塊的backward方法來動態(tài)計算其梯度。 優(yōu)化器需要“關(guān)閉”功能,該功能可以重新評估模數(shù)并返回損耗。

我們還有最后一個約束要解決。 網(wǎng)絡(luò)可能會嘗試使用超出圖像的 0 到 1 張量范圍的值來優(yōu)化輸入。 我們可以通過在每次網(wǎng)絡(luò)運行時將輸入值校正為 0 到 1 之間來解決此問題。

  1. def run_style_transfer(cnn, normalization_mean, normalization_std,
  2. content_img, style_img, input_img, num_steps=300,
  3. style_weight=1000000, content_weight=1):
  4. """Run the style transfer."""
  5. print('Building the style transfer model..')
  6. model, style_losses, content_losses = get_style_model_and_losses(cnn,
  7. normalization_mean, normalization_std, style_img, content_img)
  8. optimizer = get_input_optimizer(input_img)
  9. print('Optimizing..')
  10. run = [0]
  11. while run[0] <= num_steps:
  12. def closure():
  13. # correct the values of updated input image
  14. input_img.data.clamp_(0, 1)
  15. optimizer.zero_grad()
  16. model(input_img)
  17. style_score = 0
  18. content_score = 0
  19. for sl in style_losses:
  20. style_score += sl.loss
  21. for cl in content_losses:
  22. content_score += cl.loss
  23. style_score *= style_weight
  24. content_score *= content_weight
  25. loss = style_score + content_score
  26. loss.backward()
  27. run[0] += 1
  28. if run[0] % 50 == 0:
  29. print("run {}:".format(run))
  30. print('Style Loss : {:4f} Content Loss: {:4f}'.format(
  31. style_score.item(), content_score.item()))
  32. print()
  33. return style_score + content_score
  34. optimizer.step(closure)
  35. # a last correction...
  36. input_img.data.clamp_(0, 1)
  37. return input_img

最后,我們可以運行算法。

  1. output = run_style_transfer(cnn, cnn_normalization_mean, cnn_normalization_std,
  2. content_img, style_img, input_img)
  3. plt.figure()
  4. imshow(output, title='Output Image')
  5. ## sphinx_gallery_thumbnail_number = 4
  6. plt.ioff()
  7. plt.show()

../_images/sphx_glr_neural_style_tutorial_004.png

得出:

  1. Building the style transfer model..
  2. Optimizing..
  3. run [50]:
  4. Style Loss : 4.169305 Content Loss: 4.235329
  5. run [100]:
  6. Style Loss : 1.145476 Content Loss: 3.039176
  7. run [150]:
  8. Style Loss : 0.716769 Content Loss: 2.663749
  9. run [200]:
  10. Style Loss : 0.476047 Content Loss: 2.500893
  11. run [250]:
  12. Style Loss : 0.347092 Content Loss: 2.410895
  13. run [300]:
  14. Style Loss : 0.263698 Content Loss: 2.358449

腳本的總運行時間:(1 分鐘 20.670 秒)

Download Python source code: neural_style_tutorial.py Download Jupyter notebook: neural_style_tutorial.ipynb

由獅身人面像畫廊生成的畫廊


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號