Style Transferیکی از کاربردهای Deep Learning در هنر است که مشابه Deep Dream میباشد.
در این کاربرد یک عکس content و یک عکس style داریم. هدف این شبکه ساختن عکسی است که image آن از عکس content و style آن از عکس style گرفته شده باشد. یعنی content را حفظ کرده و آنرا به سبک style ببرد.
به طور کلی به object ها content و به .pattern,texture,color,. ها style گفته میشود.
نمونه ای از خروجیهای این شبکه :
آنچه ConvNet ها یاد میگیرند
همانطور که میدانید لایههای اول به featureهای generalتر مانند خط و لبه و لایههای آخر به featureهای abstractتر حساساند.
از visualization برای درک اینکه هر لایه چه تفسیری از image دارد، استفاده میشود. و این به عنوان hyper parameter برای style trafnsfer استفاده میشود. (برای مثال: استفاده از کدام لایه style را بهتر حفظ میکند.)
در مقاله Zeiler & Fergus سه متد درباره visualization معرفی شده است. که در اینجا به یکی از آنها میپردازیم.
متد اول:
یک unit در لایه اول برداشته، تمام عکسهای training set را به شبکه میدهیم، و 9 تا از عکسهایی که unit’s activation را ماکزیمم میکنند، پیدا میکنیم.
این کار را 9بار برای 9 تا unit مختلف انجام میدهیم. و این کار را برای لایههای مختلف تکرار میکنیم.
Cost Function
از یک شبکه pre-trained استفاده میکنیم، برای مثال یک AlexNet یا Vgg را برداشته و آن را freeze میکنیم. و مسئله را optimization عکس ورودی فرض میکنیم. یک عکس رندوم به شبکه داده، و cost آن را طوری تعریف میکنیم که content آن شبیه عکس content و style آن مانند عکس style باشد.
در هر iteration عکس رندوم تغییر میکند. اما در این training وزنهای شبکه train نمیشوند، بلکه هدف بهینه کردن pixelهای عکس ورودی است.
پس تابع cost function به این صورت تعریف میشود:
طبق فرمول، این تابع به عنوان ورودی content و generated image (که در ابتدا همان عکس رندوم است) را گرفته، و فاصلهی content آنها را حساب میکند. همچنین فاصلهی style عکس style و generated image را محاسبه میکند و این دو مقدار را با یک ضریب باهم جمع میکند.
یک مثال:
در مرحله اول عکس رندوم به صورت نویز است :
که میخواهیم content آن شبیه عکس سمت چپ و style آن شبیه عکس سمت راست شود.
طی چند generated image،epoch به این صورت تغییر میکند:
پس دیدیم که تابع cost function به دو تابع content cost function و style cost function تقسیم میشود. حال جداگانه به بررسی هرکدام میپردازیم:
Content Cost Function
یک شبکهpre-trained مانند VGG برمیداریم، یک hidden layer(L) را انتخاب کرده، که باید content ها را خوب حدس بزند. این لایه hyper parameter است و بسته به نوع مسئله تغییر میکند و از روی visualization مسئله قابل تشخیص است.
Content با activation map مقایسه میشود، یعنی یک لایه L را در نظر میگیریم، عکس content و generated image را به عنوان ورودی به آن میدهیم، و اگر activation map لایه روی این دو عکس را مقایسه کنیم، میزان تفاوت content آنها مشخص میشود. اگر بخواهیم خیلی سختگیرانه، بگوییم content باید مثل عکس ورودی باشد، باید لایه اول را انتخاب کنیم، زیرا به هر تغییر کوچکی در خط و رنگ و لبه حساس است. پس انتخاب لایه اول انعطاف کمی به ما میدهد. و لایههای آخر هم به دلیل میزان زیاد abstract بودن مناسب نیستند. به همین دلیل این لایه، معمولا از لایههای وسطی انتخاب میشود.
سپس باید activation map های عکس اصلی و عکس content مثل هم شوند.
در نهایت اگر a[L](C) و a[L](G) مثل هم بودند، میتوان گفت هر دو عکسcontent یکسان دارند.
برای مقایسه میزان شباهت این دو، از MSE استفاده میکنیم.
Style
در این مقاله، correlation، بین activation ها در کانالهای مختلف عکس style تعریف میشود.
Activation map هر کانال، نمایانگر تعدادی feature است. سپس کانالهای عکس در هم ضرب میشوند و correlation آنها محاسبه میشود. اگر این مقدار بالا باشد، یعنی آن فیلترها با هم زیاد دیده شدهاند. و اگر منفی باشد یعنی اصلا باهم دیده نشدهاند.
برای مثال، اگر شکل زیر، نمایانگر 9تا از کانالها باشد، و 9تا از object هایی که در هر کانال بیشتر fire میکند، اگر correlation یک کانال، با یک کانال دیگر زیاد باشد، یعنی دو pattern همیشه باهم رخ دادهاند. پس در style هنگام استفاده از یکی از patternها، از دیگری هم استفاده میشود. و اگر این correlation پایین باشد، هنگام استفاده از یکی، به هیچ وجه از دیگری استفاده نمیشود.
به ازای هر کانال، به تعداد کل کانالها مقایسه انجام میشود.
Gram Matrix
برای مقایسه correlation بین کانالها ازGram Matrix استفاده میکنند. اگر برای مثال، یک عکس با ابعاد 10*10*20 داشته باشیم، ابعاد این ماتریس 20*20 خواهد بود. یعنی 20*20 حالت کانالهای این عکس در هم ضرب میشوند.
Gram Matrix برای عکس style :
Gram Matrix برای عکس generated :
Style Cost Function
در نتیجه، style cost function به این صورت تعریف میشود:
گفته شد که برای content از لایههای وسط استفاده میشود، اما برای style از لایههای مختلف استفاده میشود. یعنی loss آن را ترکیبی از loss لایههای مختلف در نظر میگیرند.
پیادهسازی
مسیر دو عکس content و style را مشخص میکنیم. (در این کد، target image همان content image است.) وسپس سایز عکس را برای راحتی کار یکسان میکنیم.
1 2 3 4 5 6 7 8 9 10 11 12 |
Import keras from keras.preprocessing.image import load_img, img_to_array # This is the path to the image you want to transform. target_image_path = './images/rob_cont.jpg' # This is the path to the style image. style_reference_image_path = './images/style.jpg' # Dimensions of the generated picture. width, height = load_img(target_image_path).size img_height = 400 img_width = int(width * img_height / height) |
image preprocessing
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import numpy as np from keras.applications import vgg19 def preprocess_image(image_path): img = load_img(image_path, target_size=(img_height, img_width)) img = img_to_array(img) img = np.expand_dims(img, axis=0) img = vgg19.preprocess_input(img) return img def deprocess_image(x): # Remove zero-center by mean pixel x[:, :, 0] += 103.939 x[:, :, 1] += 116.779 x[:, :, 2] += 123.68 # 'BGR'->'RGB' x = x[:, :, ::-1] x = np.clip(x, 0, 255).astype('uint8') return x |
دو عکس content و style را به صورت constant تعریف میکنیم. چون نمیخواهیم در طول برنامه تغییری کنند.
سپس یک placeholder تعریف میکنیم تا چیزی که قرار است به شبکه feed کنیم را تعیین کنیم.
سپس این سه مورد را باهم concat میکنیم. تا شبکه VGG ، activation map را برای هر سه آنها حساب کند. و سرعت کار افزایش یابد.
include top را هم false قرار میدهیم. چون فقط به لایههای کانولوشنی به عنوان activation map نیاز داریم. نه لایههای fully connected.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
from keras import backend as K target_image = K.constant(preprocess_image(target_image_path)) style_reference_image = K.constant(preprocess_image(style_reference_image_path)) # This placeholder will contain our generated image combination_image = K.placeholder((1, img_height, img_width, 3)) # We combine the 3 images into a single batch input_tensor = K.concatenate([target_image, style_reference_image, combination_image], axis=0) # We build the VGG19 network with our batch of 3 images as input. # The model will be loaded with pre-trained ImageNet weights. model = vgg19.VGG19(input_tensor=input_tensor, weights='imagenet', include_top=False) print('Model loaded.') |
سپس content loss را تعریف میکنیم. که با MSE محاسبه میشود.
1 2 |
def content_loss(base, combination): return K.sum(K.square(combination - base)) |
سپس gram matrix و style loss را همراه با نرمالسازی تعریف میکنیم.
1 2 3 4 5 6 7 8 9 10 11 12 |
def gram_matrix(x): features = K.batch_flatten(K.permute_dimensions(x, (2, 0, 1))) gram = K.dot(features, K.transpose(features)) return gram def style_loss(style, combination): S = gram_matrix(style) C = gram_matrix(combination) channels = 3 size = img_height * img_width return K.sum(K.square(S - C)) / (4. * (channels ** 2) * (size ** 2)) |
همچنین یک نوع loss دیگر به عنوان regularization loss تعریف میکنیم که در مقاله گفته شده بود. و بیانگر این است که هر پیکسل نباید با پیکسل مجاور خیلی فاصله داشته باشد. چون در صورد زیاد بودن فاصلهشان تصویر نهایی smooth نخواهد شد. همچنین باید ضریب آن کوچک باشد تا روی loss اصلی خیلی تاثیر نگذارد.
1 2 3 4 5 6 |
def total_variation_loss(x): a = K.square( x[:, :img_height - 1, :img_width - 1, :] - x[:, 1:, :img_width - 1, :]) b = K.square( x[:, :img_height - 1, :img_width - 1, :] - x[:, :img_height - 1, 1:, :]) return K.sum(K.pow(a + b, 1.25)) |
سپس یک dictionary میسازیم و activation map output هر لایه را میگیریم.
بعد یک لایه راانتخاب میکنیم تا content را به دست آوریم. و 5لایه نیز برای style انتخاب میکنیم.
و ضریب هر loss را نیز مشخص میکنیم.
سپس loss را یک متغیر با مقدار صفر در نظر میگیریم و در هر مرحله با محاسبه انواع loss آن را تغییر میدهیم. outputs_dict[content_layer] نشاندهنده activation map هر لایه است.
content loss و style loss و total_variation loss را محاسبه میکنیم و به loss اضافه میکنیم.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
Model.summary() # Dict mapping layer names to activation tensors outputs_dict = dict([(layer.name, layer.output) for layer in model.layers]) # Name of layer used for content loss content_layer = 'block5_conv2' # Name of layers used for style loss style_layers = ['block1_conv1', 'block2_conv1', 'block3_conv1', 'block4_conv1', 'block5_conv1'] # Weights in the weighted average of the loss components total_variation_weight = 1e-4 style_weight = 1. content_weight = 0.025 # Define the loss by adding all components to a `loss` variable loss = K.variable(0.) layer_features = outputs_dict[content_layer] target_image_features = layer_features[0, :, :, :] combination_features = layer_features[2, :, :, :] loss += content_weight * content_loss(target_image_features, combination_features) for layer_name in style_layers: layer_features = outputs_dict[layer_name] style_reference_features = layer_features[1, :, :, :] combination_features = layer_features[2, :, :, :] sl = style_loss(style_reference_features, combination_features) loss += (style_weight / len(style_layers)) * sl loss += total_variation_weight * total_variation_loss(combination_image) |
در مقاله از LBFGS به عنوان optimizer استفاده شده است. که در کتابخانه scipy موجود است. که با دادن loss، gradian را حساب میکند.
هدف از نوشتن این تابع این است که یکبار loss و gradian حساب شود و به عنوان cache ذخیره شود. تا دفعات بعدی نیازی به محاسبه مجدد نباشد.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
# Get the gradients of the generated image wrt the loss grads = K.gradients(loss, combination_image)[0] # Function to fetch the values of the current loss and the current gradients fetch_loss_and_grads = K.function([combination_image], [loss, grads]) class Evaluator(object): def __init__(self): self.loss_value = None self.grads_values = None def loss(self, x): assert self.loss_value is None x = x.reshape((1, img_height, img_width, 3)) outs = fetch_loss_and_grads([x]) loss_value = outs[0] grad_values = outs[1].flatten().astype('float64') self.loss_value = loss_value self.grad_values = grad_values return self.loss_value def grads(self, x): assert self.loss_value is not None grad_values = np.copy(self.grad_values) self.loss_value = None self.grad_values = None return grad_values evaluator = Evaluator() |
در مرحله بعد از LBFGS را لود میکنیم. سپس روی عکس pre processing انجام میدهیم. و آن را flat میکنیم. و روی عکس iteration انجام میدهیم.
سپس از تابع lbfgs استفاده میکنیم. و x را به روز میکنیم.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
from scipy.optimize import fmin_l_bfgs_b from scipy.misc import imsave import time result_prefix = 'style_transfer_result' iterations = 3 # Run scipy-based optimization (L-BFGS) over the pixels of the generated image # so as to minimize the neural style loss. # This is our initial state: the target image. # Note that `scipy.optimize.fmin_l_bfgs_b` can only process flat vectors. x = preprocess_image(target_image_path) x = x.flatten() for i in range(iterations): print('Start of iteration', i) start_time = time.time() x, min_val, info = fmin_l_bfgs_b(evaluator.loss, x, fprime=evaluator.grads, maxfun=20) print('Current loss value:', min_val) # Save current generated image img = x.copy().reshape((img_height, img_width, 3)) img = deprocess_image(img) fname = result_prefix + '_at_iteration_%d.png' % i imsave(fname, img) end_time = time.time() print('Image saved as', fname) print('Iteration %d completed in %ds' % (i, end_time - start_time)) |
و در آخر نتیجه را مشاهده میکنیم:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
from matplotlib import pyplot as plt %matplotlib inline # Content image plt.imshow(load_img(target_image_path, target_size=(img_height, img_width))) plt.figure() # Style image plt.imshow(load_img(style_reference_image_path, target_size=(img_height, img_width))) plt.figure() # Generate image plt.imshow(img) plt.show() |
شبکه های اجتماعی