參考:https://pytorch.org/tutorials/advanced/neural_style_tutorial.html
具體的理論就不解釋了,這里主要是解釋代碼:
⚠️使用的是python2.7
1.導入包和選擇設備
下面是需要用來實現神經遷移的包列表:
- torch, torch.nn, numpy (使用pytorch實現神經網絡必不可少的包)
- torch.optim (有效梯度下降)
- PIL, PIL.Image, matplotlib.pyplot (下載和顯示圖像)
- torchvision.transforms (轉移PIL圖像為張量)
- torchvision.models (訓練或下載預訓練模型)
- copy (深度復制模型;系統包)
代碼為:
from __future__ import print_function import torch import torch.nn as nn import torch.nn.functional as F import torch.optim as optim from PIL import Image import matplotlib.pyplot as plt import torchvision.transforms as transforms import torchvision.models as models import copy
接下來我們就需要去選擇使用什么設備去運行網絡,以及導入內容和風格圖片。在大圖像上運行神經遷移算法要更長時間,但是如果運行在GPU中它能夠運行地更快一些。我們可以使用torch.cuda.is_available()去檢測本地是否有GPU能夠使用。接下來,我們將通過torch.device設置該GPU,讓他能在整個教程中使用。.to(device)方法也用來將張量和模塊移動到想要使用的設備上。
代碼為:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
即如果有CUDA就使用它,沒有就使用CPU
2.下載圖片
現在我們將導入風格和內容圖片。原始的PIL圖片有在0-255之間的值,但是當轉換成torch張量后,他們的值就轉換成了0-1.為了有相同的維度,這些圖片也需要被調整大小。一個重要的細節需要被注意——來自torch庫的神經網絡是使用值在0-1的張量來進行訓練的。如果你想要傳給網絡0-255張量的圖片,那么激活特征映射將不能夠感知想要的內容和風格。可是來自Caffe庫的預訓練網絡就是使用0-255的張量進行訓練的。
⚠️
在這個課程中需要下載兩張圖片:picasso.jpg 和 dancing.jpg.
下載這兩個圖片,並將它們添加在你本地路徑的data/images目錄當中
代碼為:
#輸出圖片的期望大小 imsize = 512 if torch.cuda.is_available() else 128 # 如果使用的設備不是GPU,那么就使用小點的大小 loader = transforms.Compose([ transforms.Resize(imsize), # 輸入圖像的規模,調整其大小 transforms.ToTensor()]) # 將其轉換成torch張量 def image_loader(image_name): image = Image.open(image_name) # 用來滿足網絡的輸入維度的假batch維度,即不足之處補0 image = loader(image).unsqueeze(0) return image.to(device, torch.float) #下載的風格圖像可很容圖像 style_img = image_loader("./data/imagese/picasso.jpg") content_img = image_loader("./data/images/dancing.jpg") #斷言風格圖像和內容圖像是否大小一致,如果大小不一致則報錯,說明需要大小一致的內容和風格圖像 assert style_img.size() == content_img.size(), \ "we need to import style and content images of the same size"
PIL:Python Imaging Library,Python平台的圖像處理標准庫
現在,讓我們創建一個通過再次轉換圖片的復制體成PIL格式和使用plt.imshow命令顯示復制體的方法來顯示一張圖片的函數。我們將試着顯示風格和內容圖像去保證他們已經成功導入
代碼為:
unloader = transforms.ToPILImage() # 將圖像再次轉換成PIL圖像 plt.ion() def imshow(tensor, title=None): #該函數用來顯示你上傳的內容和風格兩張圖像 image = tensor.cpu().clone() # 克隆張量並不進行更改 image = image.squeeze(0) # 移除假batch維度,即刪掉上面添加的0remove the fake batch dimension image = unloader(image) #將圖像轉換成PIL圖像 plt.imshow(image) #顯示圖像 if title is not None: plt.title(title) #title用於為圖片標注標題 plt.pause(0.001) # 稍作停頓,以便更新繪圖 plt.figure() imshow(style_img, title='Style Image') plt.figure() imshow(content_img, title='Content Image')
圖為,之后運行了以后再截自己的圖:
3.損失函數
1)內容損失
我們將直接在卷積層后添加內容損失模塊,該模塊將用於計算內容距離。這樣子每當網絡傳進輸入圖像后,該內容損失將會在期望的層次進行計算,同時計算梯度。為了使內容損失層透明,我們必須要定義一個forward函數去計算內容損失,並返回該層的輸出。該計算損失降作為模型的參數進行存儲
代碼為:
class ContentLoss(nn.Module): def __init__(self, target,): super(ContentLoss, self).__init__() # 我們從用於動態計算梯度的樹中“分離”目標內容:這是一個狀態值,而不是一個變量 # 否則該標准的forward方法將拋出一個error self.target = target.detach() def forward(self, input): self.loss = F.mse_loss(input, self.target) return input
⚠️雖然這個模塊命名為ContentLoss,但是它並不是一個真的PyTorch損失函數。如果你想要定義你自己的內容損失作為PyTorch損失函數,你必須創建一個autograd函數去在backward方法中重計算或手動實現梯度
2)風格損失
風格損失模塊與內容損失模塊的實現是相似的。它將作為一個網絡中的透明層去計算該層的風格損失。為了去計算風格損失,我們需要去計算gram矩陣GXL。該gram矩陣是他的轉置矩陣和一個給定矩陣相乘的結果。在該應用中,這個給定矩陣是層次L的特征映射FXL的改造版本。FXL被改造去形成F̂ XL,這是一個K*N矩陣,K是L層中特征映射的數量,N是所有垂直特征映射的長度FkXL。比如F̂ XL的第一行與第一個垂直特征映射F1XL相關
最后,必須通過將每個元素除以矩陣中元素的總數來規范化gram矩陣。這個規范化用以抵消帶有N維F̂ XL矩陣將在gram矩陣中生成更大的值這一事實。這些較大的值將導致第一個層(池化層之前)在梯度下降期間產生較大的影響。風格特性往往位於網絡的更深層,因此這一標准化步驟至關重要。
代碼為:
def gram_matrix(input): a, b, c, d = input.size() # a=batch size(=1) # b=number of feature maps # (c,d)=dimensions of a f. map (N=c*d) features = input.view(a * b, c * d) # resise F_XL into \hat F_XL G = torch.mm(features, features.t()) # 計算gram產出 # 我們通過在每一個特征映射中數億元素數量的方法來規范化gram矩陣的值 return G.div(a * b * c * d)
現在風格損失模塊看起來幾乎就像風格損失模塊了。這個風格距離也是使用 GXL 和GSL均方差的方法來計算的。代碼為:
class StyleLoss(nn.Module): def __init__(self, target_feature): super(StyleLoss, self).__init__() self.target = gram_matrix(target_feature).detach() def forward(self, input): G = gram_matrix(input) self.loss = F.mse_loss(G, self.target) return input
4.導入模塊
現在我們需要導入一個預訓練神經網絡。我們將使用在論文中的19層VGG網絡。
VGG的PyTorch的實現是一個分成兩個子序列模塊的模塊:features(包含卷積和池化層)和classifier(包含全連接層)。我們將使用features模塊,應為我們需要獨立卷積層的輸出去測量內容和風格損失。在訓練過程中,一些層有着與評估時不同的行為,所以我們必須使用.eval()將網絡設置為評估模式,即:
cnn = models.vgg19(pretrained=True).features.to(device).eval()
除此之外,VGG網絡將在每個通道都被mean=[0.485, 0.456, 0.406] 和std=[0.229, 0.224, 0.225]規范化的圖片上進行訓練。我們將使用他們在將圖片發送到網絡之前去將其規范化,即:
cnn_normalization_mean = torch.tensor([0.485, 0.456, 0.406]).to(device) cnn_normalization_std = torch.tensor([0.229, 0.224, 0.225]).to(device) # 創建一個模塊去規范化輸入圖像,所以我們能夠輕易地將其輸入到nn.Sequential class Normalization(nn.Module): def __init__(self, mean, std): super(Normalization, self).__init__() # 使用.view方法去將mean和std值變為[C x 1 x 1]維,所以他們能夠直接與形狀為[B x C x H x W]的圖像張量工作 # B是batch大小,C是通道數,H是高,W是寬度 self.mean = torch.tensor(mean).view(-1, 1, 1) self.std = torch.tensor(std).view(-1, 1, 1) def forward(self, img): #規范化img return (img - self.mean) / self.std
Sequential模塊包含了一個子模塊的順序列表。比如vgg19.features包含按深度的正確順序對齊的序列(Conv2d, ReLU, MaxPool2d, Conv2d, ReLU…)。我們需要在他們檢測的卷積層后面馬上添加我們的內容損失和風格損失層。為了這么做,我們一定要創建一個新的Sequential模塊去正確地插入內容損失和風格損失
# 想要用來計算風格/內容損失的層次深度 content_layers_default = ['conv_4'] style_layers_default = ['conv_1', 'conv_2', 'conv_3', 'conv_4', 'conv_5'] def get_style_model_and_losses(cnn, normalization_mean, normalization_std, style_img, content_img, content_layers=content_layers_default, style_layers=style_layers_default): cnn = copy.deepcopy(cnn) # 規范化模塊 normalization = Normalization(normalization_mean, normalization_std).to(device) # 只是為了獲得迭代入口或內容/風格損失 content_losses = [] style_losses = [] # 假設cnn是一個nn.Sequential,所以我們創建了一個新的nn.Sequential去放入假設被順序激活的模塊 model = nn.Sequential(normalization) i = 0 # 每次看見卷積時自增 for layer in cnn.children(): //去判斷cnn中的子序列是什么類型的層 if isinstance(layer, nn.Conv2d): i += 1 name = 'conv_{}'.format(i) elif isinstance(layer, nn.ReLU): name = 'relu_{}'.format(i) # 本地版本沒有和我們下面插入的ContentLoss和StyleLoss相處地很好 # 所以我們使用另一個版本來替代它 layer = nn.ReLU(inplace=False) elif isinstance(layer, nn.MaxPool2d): name = 'pool_{}'.format(i) elif isinstance(layer, nn.BatchNorm2d): name = 'bn_{}'.format(i) else: raise RuntimeError('Unrecognized layer: {}'.format(layer.__class__.__name__)) //只有是上面幾種類型的層才會添加到規范化的model中 model.add_module(name, layer) if name in content_layers: //如果該name存在於content_layers,就會為其添加內容損失的計算 # 添加內容損失 target = model(content_img).detach() content_loss = ContentLoss(target) model.add_module("content_loss_{}".format(i), content_loss) content_losses.append(content_loss) if name in style_layers: //如果該name存在於style_layers,就會為其添加風格損失的計算 # 添加風格損失 target_feature = model(style_img).detach() style_loss = StyleLoss(target_feature) model.add_module("style_loss_{}".format(i), style_loss) style_losses.append(style_loss) # 現在我們在最后一個內容和風格損失后修建層次 for i in range(len(model) - 1, -1, -1): //由后向前獲得model的值 if isinstance(model[i], ContentLoss) or isinstance(model[i], StyleLoss): break model = model[:(i + 1)] return model, style_losses, content_losses
接下來,我們選擇輸入圖像。你可以使用一個內容圖像的副本或白噪聲
input_img = content_img.clone() # 如果你想要使用白噪聲,請取消下面一行的注釋 # input_img = torch.randn(content_img.data.size(), device=device) # 添加原始輸入圖像到figure: plt.figure() imshow(input_img, title='Input Image')
5.梯度下降
我們使用L-BFGS算法去運行梯度下降。不像訓練一個網絡,我們想要訓練一個輸入的圖像去最小化內容/風格損失。我們將創建一個PyTorch L-BFGS優化器 optim.LBFGS,然后將我們的圖像作為一個張量傳給他去優化
def get_input_optimizer(input_img): # 該行用於顯示輸入是一個需要梯度的參數 optimizer = optim.LBFGS([input_img.requires_grad_()]) return optimizer
最后我們必須要定義一個可以形成神經遷移的函數。對於網絡的每一次迭代,它都將會傳入更新后的輸入並計算新的損失。我們將會運行每一個損失模塊中的backward方法去動態計算他們的梯度。該優化器需要一個“封閉”函數,用於重新計算模塊並返回損失
我們還有最后一個約束需要解決。網絡可能嘗試去為了圖像優化帶着超過圖像的0到1張量范圍值的輸入。我們可以在網絡每次運行時通過修正輸入值為0-1之間來解決這個問題
def run_style_transfer(cnn, normalization_mean, normalization_std, content_img, style_img, input_img, num_steps=300, style_weight=1000000, content_weight=1): """運行風格轉移""" print('Building the style transfer model..') model, style_losses, content_losses = get_style_model_and_losses(cnn, normalization_mean, normalization_std, style_img, content_img) optimizer = get_input_optimizer(input_img) print('Optimizing..') run = [0] while run[0] <= num_steps: def closure(): # 修正更新過的輸入圖像的值 input_img.data.clamp_(0, 1) optimizer.zero_grad() model(input_img) style_score = 0 content_score = 0 for sl in style_losses: style_score += sl.loss for cl in content_losses: content_score += cl.loss style_score *= style_weight content_score *= content_weight loss = style_score + content_score loss.backward() run[0] += 1 if run[0] % 50 == 0: print("run {}:".format(run)) print('Style Loss : {:4f} Content Loss: {:4f}'.format( style_score.item(), content_score.item())) print() return style_score + content_score optimizer.step(closure) # 最后一次修正 input_img.data.clamp_(0, 1) return input_img
最后我們可以運行這個算法:
output = run_style_transfer(cnn, cnn_normalization_mean, cnn_normalization_std, content_img, style_img, input_img) plt.figure() imshow(output, title='Output Image') # sphinx_gallery_thumbnail_number = 4 plt.ioff() plt.show()
運行起來為:
(deeplearning2) userdeMBP:neural transfer user$ python neural_style_tutorial.py Downloading: "https://download.pytorch.org/models/vgg19-dcbb9e9d.pth" to /Users/user/.torch/models/vgg19-dcbb9e9d.pth 100.0% Building the style transfer model.. neural_style_tutorial.py:121: UserWarning: To copy construct from a tensor, it is recommended to use sourceTensor.clone().detach() or sourceTensor.clone().detach().requires_grad_(True), rather than torch.tensor(sourceTensor). self.mean = torch.tensor(mean).view(-1, 1, 1) neural_style_tutorial.py:122: UserWarning: To copy construct from a tensor, it is recommended to use sourceTensor.clone().detach() or sourceTensor.clone().detach().requires_grad_(True), rather than torch.tensor(sourceTensor). self.std = torch.tensor(std).view(-1, 1, 1) Optimizing.. run [50]: Style Loss : 93.626976 Content Loss: 17.819944 run [100]: Style Loss : 22.882374 Content Loss: 15.977382 run [150]: Style Loss : 9.978903 Content Loss: 14.216763 run [200]: Style Loss : 5.153259 Content Loss: 12.498219 run [250]: Style Loss : 3.383650 Content Loss: 10.965824 run [300]: Style Loss : 2.633158 Content Loss: 9.868271
圖像為: