Khóa học
Mạng nơ-ron tích chập (Convolutional Neural Networks - CNN) là dòng mạng nơ-ron mạnh mẽ nhất cho bài toán phân loại và phân tích ảnh. Ứng dụng của chúng đã vượt qua nhiều giới hạn và được chứng minh là yếu tố then chốt của nhiều ứng dụng học sâu mà bạn thấy ngày nay. Ở mức khái quát, CNN có thể học các biểu diễn đặc trưng nội tại của ảnh được đưa vào, nhờ đó chúng trở nên cực kỳ mạnh mẽ. Hóa ra đặc tính nổi bật này của CNN không chỉ hữu ích cho các tác vụ như phân loại ảnh mà còn cho cả việc kiến tạo ảnh. Những ứng dụng như Deep Dream và Neural Style Transfer tổng hợp hình ảnh dựa trên các kích hoạt lớp trong CNN và các đặc trưng trích xuất được.
Trong hướng dẫn này, bạn sẽ tìm hiểu Neural Style Transfer hoạt động ra sao và cách triển khai nó bằng TensorFlow 2.0.
Lưu ý để theo kịp hướng dẫn này, bạn cần hiểu CNN hoạt động như thế nào. Nếu bạn muốn ôn nhanh về CNN, hãy thử các tài nguyên sau:
- Khóa học Convolutional Neural Networks for Image Processing của DataCamp
- CS231n: Convolutional Neural Networks for Visual Recognition
Neural Style Transfer là gì?
Với những người mới bước vào lĩnh vực thị giác máy tính và học sâu, thuật ngữ neural style transfer có thể hơi choáng ngợp. Để hiểu từng thành phần của thuật ngữ này, hãy xem xét hai hình dưới đây:

Trong bối cảnh neural style transfer, ảnh bên trái được gọi là ảnh nội dung (content), còn ảnh bên phải được gọi là ảnh phong cách (style). Bạn muốn “tô phong cách” cho một ảnh (trong trường hợp này là ảnh bên trái) bằng cách dùng một ảnh khác (bên phải). Điều này cấu thành hai từ cuối trong thuật ngữ - style transfer (truyền phong cách). Để thực hiện, một mạng nơ-ron (CNN) được huấn luyện để tối ưu một hàm mất mát tùy biến, do đó có từ đầu tiên - neural. Khi hai ảnh trên được hòa trộn bằng neural style transfer, đầu ra cuối cùng sẽ như sau (bên phải) -

Bạn có thể hỏi neural style transfer dùng để làm gì? Hãy tưởng tượng bạn có một bức vẽ gốc của Vincent van Gogh. Bạn muốn xem bức vẽ đó sẽ trông như thế nào nếu chuyển hóa sang một bức vẽ của chính bạn. Đây là một trong những ứng dụng của neural style transfer. Một ví dụ khác là các ứng dụng bộ lọc ảnh như Prisma, cho phép bạn thực hiện neural style transfer qua một giao diện mượt mà.
Phân tách giữa nội dung và phong cách
Cho đến giờ, ý tưởng bạn có được về neural style transfer là quá trình này xoay quanh việc kết hợp nội dung của một ảnh với phong cách của một ảnh khác. Thực tế, điều này đúng 100%. Trong phần này, hãy hiểu nội dung và phong cách có nghĩa gì trong bối cảnh CNN.
Một CNN thường là tập hợp của nhiều lớp tích chập và lớp pooling. Các lớp tích chập chịu trách nhiệm trích xuất các đặc trưng phức tạp từ ảnh, trong khi các lớp pooling loại bỏ thông tin không gian chi tiết không liên quan đến bài toán phân loại ảnh. Hệ quả là nó giúp CNN học “nội dung” của ảnh hơn là những yếu tố cụ thể như màu sắc, kết cấu, v.v. Càng đi sâu vào CNN, độ phức tạp của đặc trưng càng tăng và các lớp tích chập sâu thường được gọi là biểu diễn nội dung.
Một ví dụ về “phong cách” sẽ là bất cứ thứ gì đặc thù cho thuộc tính hình ảnh như kết cấu, màu sắc, v.v. — chẳng hạn việc sử dụng nổi bật một màu cụ thể trong một bức vẽ. Vậy làm sao trích xuất phong cách của một ảnh? Điều này được thực hiện bằng cách tính tương quan giữa các lớp tích chập. Tương quan cho ta mức độ hai hay nhiều biến giống/ liên hệ nhau thế nào. Để hiểu điều này, hãy xét một lớp tích chập đã học gồm nhiều bản đồ đặc trưng (feature map). Với mỗi bản đồ đặc trưng, bạn có thể đo mức độ đặc trưng nó phát hiện tương quan mạnh đến các bản đồ khác trong cùng lớp. Điều này cho bạn ước lượng những thứ như: liệu một màu được phát hiện ở bản đồ thứ nhất có tương tự màu ở bản đồ khác không? Có hình dạng nào phổ biến trên các bản đồ không? Những đặc tính/điểm tương đồng này định hình phong cách của ảnh. Quá trình đo mức độ tương đồng giữa nội dung của nhiều bản đồ đặc trưng trong một lớp tích chập giúp mạng học biểu diễn đa tỉ lệ của ảnh, tập trung vào các đặc trưng không gian như kết cấu và màu sắc.
Một điểm quan trọng cần ghi nhớ là khi áp dụng neural style transfer, bạn cũng cần đảm bảo nội dung của một ảnh được bảo toàn cùng với phong cách mong muốn của ảnh khác. Bạn sẽ thấy cách thực hiện điều này ở phần sau. Ở phần kế tiếp, bạn sẽ học cách trích xuất đặc trưng từ một ảnh (ảnh nội dung) và tính toán content loss.
Định lượng ảnh nội dung và tính toán Content Loss
Thuật toán neural style transfer lần đầu được giới thiệu bởi Gatys và cộng sự trong bài báo năm 2015, A Neural Algorithm of Artistic Style. Tuy nhiên, hướng dẫn này tham chiếu từ bài Image Style Transfer Using Convolutional Neural Networks, có thể xem như phần tiếp nối bài trước.
Theo bài báo Image Style Transfer Using Convolutional Neural Networks, họ sử dụng kiến trúc CNN VGG-19 để trích xuất cả đặc trưng nội dung và phong cách từ ảnh nội dung và ảnh phong cách tương ứng. Để lấy đặc trưng nội dung, họ dùng lớp tích chập thứ hai của khối thứ tư (các lớp tích chập). Vì tiện lợi, tác giả gọi nó là conv4_2. Khi đã có đặc trưng nội dung, bạn sẽ so sánh nó với một ảnh đích để đo content loss. Ảnh đích là gì? Vì sao cần tính content loss ở đây? Hãy lùi lại một bước và tập trung vào hai câu hỏi này.
Để kết hợp đặc trưng nội dung và phong cách vào một ảnh duy nhất, bạn sẽ bắt đầu với một ảnh đích, hoặc là một ảnh trống, hoặc là bản sao của ảnh nội dung. Giờ, để học hiệu quả cả đặc trưng nội dung và phong cách bằng CNN, bạn cần một hàm mất mát tùy biến để tối ưu, nhờ đó tạo ra một ảnh có phong cách mượt mà được cấu thành từ ảnh nội dung và ảnh phong cách. Hàm mất mát tùy biến này về bản chất là sự kết hợp của hai loại mất mát:
- Content loss, đảm bảo tổng mức độ nội dung được bảo toàn.
- Style loss, đảm nhiệm lượng phong cách được truyền sang ảnh đích.
Bây giờ bạn đã hình dung về hai câu hỏi trên, hãy quay lại content loss và định nghĩa nó. Content loss được định nghĩa như sau -
trong đó, $T_c$ là ảnh đích và $C_c$ là ảnh nội dung. Hàm mất mát này đo mức độ khác biệt giữa các đặc trưng của ảnh nội dung và ảnh đích. Mạng sẽ cố gắng tối thiểu hóa mất mát này. Ở phần tiếp theo, bạn sẽ thấy cách xác định style loss.
Style Loss và Ma trận Gram
Để xác định style loss, bài báo hướng dẫn bạn lấy các biểu diễn (về cơ bản là các con số) từ các lớp sau và thu được ma trận gram của các bản đồ đặc trưng trong những lớp đó.
'conv1_1'
'conv2_1'
'conv3_1'
'conv4_1'
'conv5_1'
Lợi thế của việc dùng nhiều lớp để định nghĩa style loss là nó cho phép học một biểu diễn đa tỉ lệ của phong cách chứa trong một ảnh. Các ma trận gram giúp bạn xác định mức độ tương đồng giữa các đặc trưng trên các bản đồ đặc trưng trong cùng một lớp tích chập. Điều này tiếp tục khắc họa thông tin phi địa phương về ảnh, vốn định hình phong cách của nó. Một ma trận gram trong bối cảnh này được tính như sau.
Giả sử kích thước của một ảnh sau tích chập là 8x8x16, nghĩa là có 16 bản đồ đặc trưng. Bạn muốn tìm độ tương đồng giữa các đặc trưng trên các bản đồ này bằng một ma trận gram. Để làm vậy, bạn làm phẳng hai chiều đầu của ảnh tích chập và biến chúng thành một vector 1D. Kích thước sau khi làm phẳng trong trường hợp này sẽ có 64 phần tử trong vector 1D. Điều này được lặp lại trên các bản đồ đặc trưng. Vậy bạn thu được một ma trận cuối có kích thước 16x64 (gọi là ma trận A). Ma trận này sau đó được chuyển vị (thành 64x16) và nhân với ma trận A. Ma trận gram trong trường hợp này sẽ có kích thước 16x16. Một giá trị cụ thể trong ma trận gram biểu thị mức độ tương đồng giữa các bản đồ đặc trưng. Về trực quan, quá trình này giống như sau:

Để tính style loss giữa ảnh đích và ảnh phong cách, bạn lấy khoảng cách bình phương trung bình giữa các ma trận gram của chúng tại mỗi khối lớp tích chập. Về hình thức, có thể định nghĩa như sau:
trong đó, T(s,i) là ma trận gram của ảnh đích tính tại khối i và S(s,i) là ma trận gram của ảnh phong cách tính tại khối i. Với wi, bạn có thể gán các trọng số tùy biến cho các khối tích chập khác nhau để đạt biểu diễn phong cách chi tiết hơn. Cuối cùng, $a$ là một hằng số phản ánh các giá trị trong mỗi lớp của các khối. Hãy ghép hai loại mất mát lại để định nghĩa tổng mất mát mà mạng sẽ tối ưu trong quá trình neural style transfer.
Định nghĩa Tổng mất mát
Tổng mất mát giờ chỉ là phép cộng có trọng số tùy chỉnh -
trong đó, $\alpha$ là trọng số nội dung, và $\beta$ là trọng số phong cách. Điều này giúp bạn duy trì cân bằng hợp lý giữa nội dung và lượng phong cách được truyền sang ảnh đích. Tác động của các kết hợp $\alpha$ và $\beta$ khác nhau có thể xem trong bài báo, và bạn nên tham khảo. Giờ bạn đã có đầy đủ mảnh ghép để triển khai neural style transfer. Hãy bắt tay vào phần tiếp theo.
Neural Style Transfer thực chiến
Ở phần triển khai, bạn sẽ dùng TensorFlow 2.0. Bản này có rất nhiều bổ sung xuất sắc và là một trong những cập nhật toàn diện nhất của thư viện tính đến nay. Nếu bạn muốn tìm hiểu một vài điểm nổi bật, có thể xem bài viết này.
Phiên bản mới nhất của TensorFlow (tại thời điểm viết hướng dẫn) là 2.0.0-beta0. Nếu bạn chưa cài đặt, vui lòng cài theo hướng dẫn tại đây. Bạn sẽ bắt đầu bằng cách nhập các gói cần thiết và nạp ảnh nội dung cũng như ảnh phong cách.
# Packages
import tensorflow as tf
from tensorflow.keras.applications.vgg19 import preprocess_input
from tensorflow.keras.models import Model
import matplotlib.pyplot as plt
import numpy as np
np.random.seed(7)
%matplotlib inline
print(tf.__version__)
2.0.0-beta0
# Load the content and style images
content = plt.imread('Content.jpeg')
style = plt.imread('Style.jpg')
# Display the images
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(20, 10))
# Content and style images side-by-side
ax1.imshow(content)
ax1.set_title('Content Image')
ax2.imshow(style)
ax2.set_title('Style Image')
plt.show()

Bước tiếp theo, bạn sẽ viết một hàm trợ giúp để nạp hai ảnh vào mảng số (vì máy tính chỉ hiểu số) và đổi kích thước để tương thích với mô hình.
def load_image(image):
image = plt.imread(image)
img = tf.image.convert_image_dtype(image, tf.float32)
img = tf.image.resize(img, [400, 400])
# Shape -> (batch_size, h, w, d)
img = img[tf.newaxis, :]
return img
# Use load_image of content and style images
content = load_image('Content.jpeg')
style = load_image('Style.jpg')
# Verify the shapes
content.shape, style.shape
(TensorShape([1, 400, 400, 3]), TensorShape([1, 400, 400, 3]))
Các ảnh đã được đổi kích thước. Bây giờ, bạn sẽ nạp mô hình VGG19 đã huấn luyện sẵn để trích xuất đặc trưng. Vì dùng mô hình để trích xuất đặc trưng, bạn không cần phần phân loại của mô hình.
vgg = tf.keras.applications.VGG19(include_top=False, weights='imagenet')
vgg.trainable = False
Downloading data from https://github.com/fchollet/deep-learning-models/releases/download/v0.1/vgg19_weights_tf_dim_ordering_tf_kernels_notop.h5
80142336/80134624 [==============================] - 5s 0us/step
Các tác giả đã nêu tên khối họ dùng để trích xuất đặc trưng nội dung và phong cách. Mô hình VGG19 bạn vừa nạp có quy ước đặt tên tương tự. Tận dụng điều đó, trước tiên bạn sẽ in tên tất cả các lớp trong mạng.
# Print the layer names for convenience
for layer in vgg.layers:
print(layer.name)
input_1
block1_conv1
block1_conv2
block1_pool
block2_conv1
block2_conv2
block2_pool
block3_conv1
block3_conv2
block3_conv3
block3_conv4
block3_pool
block4_conv1
block4_conv2
block4_conv3
block4_conv4
block4_pool
block5_conv1
block5_conv2
block5_conv3
block5_conv4
block5_pool
Tuy nhiên, bạn quan tâm đến các lớp sau để lấy đặc trưng phong cách:
'conv1_1'
'conv2_1'
'conv3_1'
'conv4_1'
'conv5_1'
Với đặc trưng nội dung, bạn sẽ cần conv4_2. Hãy lưu chúng vào biến tương ứng.
# Content layer
content_layers = ['block4_conv2']
# Style layer
style_layers = ['block1_conv1',
'block2_conv1',
'block3_conv1',
'block4_conv1',
'block5_conv1']
num_content_layers = len(content_layers)
num_style_layers = len(style_layers)
Giờ hãy tạo một mô hình VGG tùy chỉnh gồm các lớp đã chỉ định. Điều này sẽ giúp bạn chạy forward pass trên ảnh và trích xuất các đặc trưng cần thiết trong quá trình.
def mini_model(layer_names, model):
outputs = [model.get_layer(name).output for name in layer_names]
model = Model([vgg.input], outputs)
return model
Định nghĩa ma trận gram trong TensorFlow rất dễ, bạn có thể làm như sau:
# Gram matrix
def gram_matrix(tensor):
temp = tensor
temp = tf.squeeze(temp)
fun = tf.reshape(temp, [temp.shape[2], temp.shape[0]*temp.shape[1]])
result = tf.matmul(temp, temp, transpose_b=True)
gram = tf.expand_dims(result, axis=0)
return gram
Bây giờ bạn sẽ định nghĩa một mô hình tùy chỉnh dùng hàm mini_model(). Mô hình này sẽ trả về các đặc trưng nội dung và phong cách từ các ảnh tương ứng.
class Custom_Style_Model(tf.keras.models.Model):
def __init__(self, style_layers, content_layers):
super(Custom_Style_Model, self).__init__()
self.vgg = mini_model(style_layers + content_layers, vgg)
self.style_layers = style_layers
self.content_layers = content_layers
self.num_style_layers = len(style_layers)
self.vgg.trainable = False
def call(self, inputs):
# Scale back the pixel values
inputs = inputs*255.0
# Preprocess them with respect to VGG19 stats
preprocessed_input = preprocess_input(inputs)
# Pass through the mini network
outputs = self.vgg(preprocessed_input)
# Segregate the style and content representations
style_outputs, content_outputs = (outputs[:self.num_style_layers],
outputs[self.num_style_layers:])
# Calculate the gram matrix for each layer
style_outputs = [gram_matrix(style_output)
for style_output in style_outputs]
# Assign the content representation and gram matrix in
# a layer by layer fashion in dicts
content_dict = {content_name:value
for content_name, value
in zip(self.content_layers, content_outputs)}
style_dict = {style_name:value
for style_name, value
in zip(self.style_layers, style_outputs)}
return {'content':content_dict, 'style':style_dict}
Giờ bạn đã định nghĩa mô hình tùy chỉnh, hãy dùng nó trên các ảnh để lấy đặc trưng nội dung và phong cách tương ứng -
# Note that the content and style images are loaded in
# content and style variables respectively
extractor = Custom_Style_Model(style_layers, content_layers)
style_targets = extractor(style)['style']
content_targets = extractor(content)['content']
In the paper, optimization was done using the L-BFGS algorithm, but you can use Adam also.
opt = tf.optimizers.Adam(learning_rate=0.02)
Giờ hãy định nghĩa các trọng số tổng thể cho nội dung và phong cách, cũng như trọng số cho từng biểu diễn phong cách như đã thảo luận trước đó. Lưu ý rằng đây là các siêu tham số và bạn nên thử nghiệm.
# Custom weights for style and content updates
style_weight=100
content_weight=10
# Custom weights for different style layers
style_weights = {'block1_conv1': 1.,
'block2_conv1': 0.8,
'block3_conv1': 0.5,
'block4_conv1': 0.3,
'block5_conv1': 0.1}
Bây giờ đến phần quan trọng nhất khiến quá trình neural style transfer thú vị hơn nhiều - hàm mất mát.
# The loss function to optimize
def total_loss(outputs):
style_outputs = outputs['style']
content_outputs = outputs['content']
style_loss = tf.add_n([style_weights[name]*tf.reduce_mean((style_outputs[name]-style_targets[name])**2)
for name in style_outputs.keys()])
# Normalize
style_loss *= style_weight / num_style_layers
content_loss = tf.add_n([tf.reduce_mean((content_outputs[name]-content_targets[name])**2)
for name in content_outputs.keys()])
# Normalize
content_loss *= content_weight / num_content_layers
loss = style_loss + content_loss
return loss
Giờ bạn sẽ viết một hàm khác để:
- Tính gradient của hàm mất mát bạn vừa định nghĩa.
- Dùng các gradient này để cập nhật ảnh đích.
Với GradientTape, bạn có thể tận dụng vi phân tự động để tính gradient của một hàm dựa trên thành phần của nó. Bạn cũng sẽ dùng decorator tf.function để tăng tốc thao tác. Đọc thêm tại đây.
@tf.function()
def train_step(image):
with tf.GradientTape() as tape:
# Extract the features
outputs = extractor(image)
# Calculate the loss
loss = total_loss(outputs)
# Determine the gradients of the loss function w.r.t the image pixels
grad = tape.gradient(loss, image)
# Update the pixels
opt.apply_gradients([(grad, image)])
# Clip the pixel values that fall outside the range of [0,1]
image.assign(tf.clip_by_value(image, clip_value_min=0.0, clip_value_max=1.0))
Bước duy nhất còn lại trước khi huấn luyện mạng là định nghĩa ảnh đích. Với ảnh đích, bạn sẽ dùng luôn ảnh nội dung.
target_image = tf.Variable(content)
Bây giờ bạn đã sẵn sàng để huấn luyện mạng.
epochs = 10
steps_per_epoch = 100
step = 0
for n in range(epochs):
for m in range(steps_per_epoch):
step += 1
train_step(target_image)
plt.imshow(np.squeeze(target_image.read_value(), 0))
plt.title("Train step: {}".format(step))
plt.show()
WARNING: Logging before flag parsing goes to stderr.
W0617 16:21:34.491543 140709216896896 deprecation.py:323] From /usr/local/lib/python3.6/dist-packages/tensorflow/python/ops/math_grad.py:1205: add_dispatch_support.<locals>.wrapper (from tensorflow.python.ops.array_ops) is deprecated and will be removed in a future version.
Instructions for updating:
Use tf.where in 2.0, which has the same broadcast rule as np.where
Bạn nên thử thay đổi các trọng số nội dung và phong cách để quan sát sự thay đổi trên ảnh đích.
Kết luận
Bài báo được tham chiếu trong hướng dẫn này không đề cập đến việc tối ưu thêm chất lượng ảnh tạo ra. Như bạn có thể thấy ở các kết quả trên, vẫn cần một mức độ mượt mà hơn trong mối quan hệ không gian giữa tổng thể nội dung và phong cách của ảnh đầu ra. Để khắc phục, total_variantional_loss đã được giới thiệu, tương tự như việc dùng regularization. Ở mức cao, total_variational_loss phạt các nhiễu tần số cao do thuật toán neural style transfer gốc gây ra. Xem hướng dẫn này nếu bạn muốn triển khai. Sau đây là một số tài nguyên tham khảo khi viết hướng dẫn này:
- Hands-On Transfer Learning with Python
- Neural style transfer
- Bài học về Style Transfer từ Udacity
- Deep Learning for Computer Vision của Adrian Rosebrock (Phần II)
Nếu bạn muốn rèn luyện kỹ năng học sâu, bạn có thể xem các khóa học và hướng dẫn sau của DataCamp: