CNTK API文件翻譯(19)——藝術風格轉變
本教程展示瞭如何將一張圖片的風格轉換成另外一種。這讓我們可以將一張原始照片渲染成世界名畫的風格。
與建立一個好看的圖片不同,在本教程中你講學習如何在CNTK中載入一個已經訓練好的VGG模型,如何基於輸入變數獲取對應的梯度,以及如何在不使用CNTK的時候使用梯度。
我們使用Leon A. Gatys等人提出並經過Novak和Nikulin改進的方法。當然有更快的技術,不過那些只侷限於改進圖片風格。
我們首先引入一些我們需要的模組。除了通用模組(numpy、scipy和cntk)之外,我們需要引入PIL模組處理影象,requests模組下載訓練好的模型以及h5py模組讀取已經訓練好的模型裡的權重。
from __future__ import print_function
import numpy as np
from scipy import optimize as opt
import cntk as C
from PIL import Image
import requests
import h5py
import os
import matplotlib.pyplot as plt
# Select the right target device when this notebook is being tested:
if 'TEST_DEVICE' in os.environ:
if os.environ['TEST_DEVICE'] == 'cpu':
C.device.try_set_default_device(C.device.cpu())
else:
C.device.try_set_default_device(C.device.gpu(0))
訓練好的模型是一個VGG神經網路,我們從https://gist.github.com/baraldilorenzo/07d7802847aaad0a35d3獲取。我們(微軟)把他放在了自己的伺服器上,以便能輕鬆的下載到他。下面的程式碼裡面我們就將下載他(如果本地不存在),然後將其載入成numpy陣列。
def download(url, filename):
response = requests.get(url, stream=True)
with open(filename, 'wb') as handle:
for data in response.iter_content(chunk_size=2**20):
if data: handle.write(data)
def load_vgg(path):
f = h5py.File(path)
layers = []
for k in range(f.attrs['nb_layers']):
g = f['layer_{}'.format(k)]
n = g.attrs['nb_params']
layers.append([g['param_{}'.format(p)][:] for p in range(n)])
f.close()
return layers
# Check for an environment variable defined in CNTK's test infrastructure
envvar = 'CNTK_EXTERNAL_TESTDATA_SOURCE_DIRECTORY'
def is_test(): return envvar in os.environ
path = 'vgg16_weights.bin'
url = 'https://cntk.ai/jup/models/vgg16_weights.bin'
# We check for the model locally
if not os.path.exists(path):
# If not there we might be running in CNTK's test infrastructure
if is_test():
path = os.path.join(os.environ[envvar],'PreTrainedModels','Vgg16','v0',path)
else:
#If neither is true we download the file from the web
print('downloading VGG model (~0.5GB)')
download(url, path)
layers = load_vgg(path)
print('loaded VGG model')
接下來我們使用CNTK來定義VGG神經網路
# A convolutional layer in the VGG network
def vggblock(x, arrays, layer_map, name):
f = arrays[0]
b = arrays[1]
k = C.constant(value=f)
t = C.constant(value=np.reshape(b, (-1, 1, 1)))
y = C.relu(C.convolution(k, x, auto_padding=[False, True, True]) + t)
layer_map[name] = y
return y
# A pooling layer in the VGG network
def vggpool(x):
return C.pooling(x, C.AVG_POOLING, (2, 2), (2, 2))
# Build the graph for the VGG network (excluding fully connected layers)
def model(x, layers):
model_layers = {}
def convolutional(z): return len(z) == 2 and len(z[0].shape) == 4
conv = [layer for layer in layers if convolutional(layer)]
cnt = 0
num_convs = {1: 2, 2: 2, 3: 3, 4: 3, 5: 3}
for outer in range(1,6):
for inner in range(num_convs[outer]):
x = vggblock(x, conv[cnt], model_layers, 'conv%d_%d' % (outer, 1+inner))
cnt += 1
x = vggpool(x)
return x, C.combine([model_layers[k] for k in sorted(model_layers.keys())])
定義成本函式
在本教程中比較有意思的部分是定義一個成本函式,當優化完成之後,導致的結果一張內容與一張圖片相似但是風格與另一張圖片相識的圖片。這個成本函式包含很多專案,其中有一些是VGG神經網路建立時定義的。具體來說,成本函式以待計算影象x為引數,計算內容損失,風格損失以及總畸變損失的加權和:
其中
- 總畸變損失
T(x) 是最容易理解的:他衡量相鄰畫素值平方差之和的平均值,他的變小會讓影象銳度降低。我們通過使用一個包含(-1,1)的卷積核對影象進行橫向和縱向的卷積運算,對結果進行平方運算,然後計算他們的平均值。 - 內容損失衡量內容圖片和待計算圖片之間的平方差。我們既可以直接計算原始畫素的差值,也可以計算VGG網路裡面各個層的差值。因為內容影象是固定的,所以雖然其需要依賴內容圖片實現,我們並沒有將其寫入公式。
- 風格損失
S(x) 與內容損失類似,也需要依靠另一張圖片實現。Leon A. Gatys等人提出的風格的定義是神經網路中節點啟用值的相互關係,衡量風格損失也就是這些關係的平方差。具體來說,對於特定的網路層,我們計算輸出通道中所有為位置的協方差矩陣平均值。風格損失就是風格影象的協方差矩陣和待計算影象的協方差矩陣之間的均方誤差。我們故意沒說特定層是哪一層,不同的實現方式有不同的計算方法,下面我們將使用所有層所有風格損失的加權和。
def flatten(x):
assert len(x.shape) >= 3
return C.reshape(x, (x.shape[-3], x.shape[-2] * x.shape[-1]))
def gram(x):
features = C.minus(flatten(x), C.reduce_mean(x))
return C.times_transpose(features, features)
def npgram(x):
features = np.reshape(x, (-1, x.shape[-2]*x.shape[-1])) - np.mean(x)
return features.dot(features.T)
def style_loss(a, b):
channels, x, y = a.shape
assert x == y
A = gram(a)
B = npgram(b)
return C.squared_error(A, B)/(channels**2 * x**4)
def content_loss(a,b):
channels, x, y = a.shape
return C.squared_error(a, b)/(channels*x*y)
def total_variation_loss(x):
xx = C.reshape(x, (1,)+x.shape)
delta = np.array([-1, 1], dtype=np.float32)
kh = C.constant(value=delta.reshape(1, 1, 1, 1, 2))
kv = C.constant(value=delta.reshape(1, 1, 1, 2, 1))
dh = C.convolution(kh, xx, auto_padding=[False])
dv = C.convolution(kv, xx, auto_padding=[False])
avg = 0.5 * (C.reduce_mean(C.square(dv)) + C.reduce_mean(C.square(dh)))
return avg
計算成本
現在我們準備使用兩張圖片計算成本。我們會使用一張波蘭的風景照片和一張梵高的《星空》。我們先定義幾個調節引數,他們的解釋如下:
- 依據程式碼在CPU或者GPU上執行,我們設定影象的大小分別是64×64或300×300,以及對應的調整優化迴圈的次數來加速處理的程序,方便我們實驗。當然如果你想要更好的結果,你可以使用更大的圖片。如果你只有CPU,這就需要花一會了。
- 內容權重和風格權重是影響結果圖片最主要的引數。
- 衰減係數是一個在(0,1)之間的數字,決定個網路層的貢獻值。根據Novak和Nikulin的研究,所有的網路層都影響內容損失和風格損失。VGG神經網路中的輸入層對內容損失的影響最大,後面的層的權重隨著層數的增加指數級減少。VGG神經網路中的輸出層對風格損失的影響最大,之前的層的權重隨著離輸出層的距離指數級減少。我們和Novak和Nikulin的文章中一樣,衰減係數設為0.5。
- inner和outer引數定義我們如何得到最終結果。我們將在成本值最小的時候獲取outer個截圖。每經過inner次優化我們都會得到一個截圖。
- 最後,非常重要的一點是我們用到的已經訓練好的模型是如何訓練的。具體來說,在訓練時,訓練集中每個樣本的紅綠藍三色通道都被減掉了一個常數向量,這讓輸入的資料以0為中心,方便訓練執行。如果我們自己的圖片不減掉這個常數向量,我們的資料和訓練時使用的資料就會不太一樣,也得不到好的結果。這個向量是下面的SHIFT。
style_path = 'style.jpg'
content_path = 'content.jpg'
start_from_random = False
content_weight = 5.0
style_weight = 1.0
decay = 0.5
if is_test():
outer = 2
inner = 2
SIZE = 64
else:
outer = 10
inner = 20
SIZE = 300
SHIFT = np.reshape([103.939, 116.779, 123.68], (3, 1, 1)).astype('f')
def load_image(path):
with Image.open(path) as pic:
hw = pic.size[0] / 2
hh = pic.size[1] / 2
mh = min(hw,hh)
cropped = pic.crop((hw - mh, hh - mh, hw + mh, hh + mh))
array = np.array(cropped.resize((SIZE,SIZE), Image.BICUBIC), dtype=np.float32)
return np.ascontiguousarray(np.transpose(array, (2,0,1)))-SHIFT
def save_image(img, path):
sanitized_img = np.maximum(0, np.minimum(255, img+SHIFT))
pic = Image.fromarray(np.uint8(np.transpose(sanitized_img, (1, 2, 0))))
pic.save(path)
def ordered_outputs(f, binding):
_, output_dict = f.forward(binding, f.outputs)
return [np.squeeze(output_dict[out]) for out in f.outputs]
# download the images if they are not available locally
for local_path in content_path, style_path:
if not os.path.exists(local_path):
download('https://cntk.ai/jup/%s' % local_path, local_path)
# Load the images
style = load_image(style_path)
content = load_image(content_path)
# Display the images
for img in content, style:
plt.figure()
plt.imshow(np.asarray(np.transpose(img+SHIFT, (1, 2, 0)), dtype=np.uint8))
# Push the images through the VGG network
# First define the input and the output
y = C.input_variable((3, SIZE, SIZE), needs_gradient=True)
z, intermediate_layers = model(y, layers)
# Now get the activations for the two images
content_activations = ordered_outputs(intermediate_layers, {y: [[content]]})
style_activations = ordered_outputs(intermediate_layers, {y: [[style]]})
style_output = np.squeeze(z.eval({y: [[style]]}))
# Finally define the loss
n = len(content_activations)
# makes sure that changing the decay does not affect the magnitude of content/style
total = (1-decay**(n+1))/(1-decay)
loss = (1.0/total * content_weight * content_loss(y, content)
+ 1.0/total * style_weight * style_loss(z, style_output)
+ total_variation_loss(y))
for i in range(n):
loss = (loss
+ decay**(i+1)/total * content_weight * content_loss(intermediate_layers.outputs[i], content_activations[i])
+ decay**(n-i)/total * style_weight * style_loss(intermediate_layers.outputs[i], style_activations[i]))
優化成本
現在我們做好了得到成本值最小時的影象的準備了。我們將使用scipy裡的優化包,具體來說是LBFGS方法。LBFGS方法是一個非常棒的優化程式,像我們的案例中在計算完整梯度可行時,他非常受歡迎。
注意我們根據輸入值計算梯度,這與我們之前根據網路中的引數計算梯度非常不同,預設情況下CNTK的輸入變數不需要梯度,不過我們定義輸入變數如下
y = C.input_variable((3, SIZE, SIZE), needs_gradient=True)
上述程式碼表示CNTK將根據輸入變數計算梯度。
剩下的程式碼就比較簡單了,最複雜的部分都是使用scipy優化包:
- 這個優化器只支援雙精度向量,所以img2vec函式輸入一個(3,SIZE,SIZE)大小的影象,將其轉換成雙精度的向量。
- CNTK需要輸入是影象但是scipy要求返回向量。
- CNTK計算出來的梯度也是影象,但是scipy要求梯度是向量。
除開上面那些比較複雜的地方,我們輸入內容圖片,執行優化,展示最後的圖片。
# utility to convert a vector to an image
def vec2img(x):
d = np.round(np.sqrt(x.size / 3)).astype('i')
return np.reshape(x.astype(np.float32), (3, d, d))
# utility to convert an image to a vector
def img2vec(img):
return img.flatten().astype(np.float64)
# utility to compute the value and the gradient of f at a particular place defined by binding
def value_and_grads(f, binding):
if len(f.outputs) != 1:
raise ValueError('function must return a single tensor')
df, valdict = f.forward(binding, [f.output], set([f.output]))
value = list(valdict.values())[0]
grads = f.backward(df, {f.output: np.ones_like(value)}, set(binding.keys()))
return value, grads
# an objective function that scipy will be happy with
def objfun(x, loss):
y = vec2img(x)
v, g = value_and_grads(loss, {loss.arguments[0]: [[y]]})
v = np.reshape(v, (1,))
g = img2vec(list(g.values())[0])
return v, g
# the actual optimization procedure
def optimize(loss, x0, inner, outer):
bounds = [(-np.min(SHIFT), 255-np.max(SHIFT))]*x0.size
for i in range(outer):
s = opt.minimize(objfun, img2vec(x0), args=(loss,), method='L-BFGS-B',
bounds=bounds, options={'maxiter': inner}, jac=True)
print('objective : %s' % s.fun[0])
x0 = vec2img(s.x)
path = 'output_%d.jpg' % i
save_image(x0, path)
return x0
np.random.seed(98052)
if start_from_random:
x0 = np.random.randn(3, SIZE, SIZE).astype(np.float32)
else:
x0 = content
xstar = optimize(loss, x0, inner, outer)
plt.imshow(np.asarray(np.transpose(xstar+SHIFT, (1, 2, 0)), dtype=np.uint8))