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)風格(Neural-Style)或神經(jīng)傳遞(Neural-Transfer)使您可以拍攝圖像并以新的藝術(shù)風格對其進行再現(xiàn)。 該算法獲取三個圖像,即輸入圖像,內(nèi)容圖像和樣式圖像,然后更改輸入以使其類似于內(nèi)容圖像的內(nèi)容和樣式圖像的藝術(shù)風格。

content1

基本原理

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

導入軟件包并選擇設備

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

  • torch,torch.nnnumpy(使用 PyTorch 的神經(jīng)網(wǎng)絡必不可少的軟件包)
  • torch.optim(有效梯度下降)
  • PIL,PIL.Image,matplotlib.pyplot(加載并顯示圖像)
  • torchvision.transforms(將 PIL 圖像轉(zhuǎn)換為張量)
  • torchvision.models(訓練或負載預訓練模型)
  • copy(用于深復制模型;系統(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

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

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

加載圖像

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

Note

以下是下載運行本教程所需的圖像的鏈接: picasso.jpg 和 dance.jpg 。 下載這兩個圖像并將它們添加到當前工作目錄中名稱為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)容和樣式圖像,以確保正確導入它們。

  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)絡處理輸入 中層 的特征圖 ,并返回圖像 和內(nèi)容圖像 之間的加權(quán)內(nèi)容距離 。 為了計算內(nèi)容距離,該功能必須知道內(nèi)容圖像的特征圖( )。 我們將此功能實現(xiàn)為炬管模塊,并使用以 作為輸入的構(gòu)造函數(shù)。 距離 是兩組特征圖之間的均方誤差,可以使用nn.MSELoss進行計算。

我們將直接在用于計算內(nèi)容距離的卷積層之后添加此內(nèi)容丟失模塊。 這樣,每次向網(wǎng)絡饋入輸入圖像時,都會在所需層上計算內(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)漸變。

風格損失

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

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

導入模型

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

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

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

另外,在圖像上訓練 VGG 網(wǎng)絡,每個通道的均值通過均值= [0.485,0.456,0.406]和 std = [0.229,0.224,0.225]歸一化。 在將其發(fā)送到網(wǎng)絡之前,我們將使用它們對圖像進行規(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 算法來運行梯度下降。 與訓練網(wǎng)絡不同,我們希望訓練輸入圖像,以最大程度地減少內(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)絡的每次迭代,它都會被提供更新的輸入并計算新的損耗。 我們將運行每個損失模塊的backward方法來動態(tài)計算其梯度。 優(yōu)化器需要“關(guān)閉”功能,該功能可以重新評估模數(shù)并返回損耗。

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

公眾號
微信公眾號

編程獅公眾號