Giới thiệu về Chuyển Đổi Phong Cách Nghệ Thuật
Chuyển đổi phong cách nghệ thuật là một ứng dụng thú vị của học sâu, cho phép bạn kết hợp nội dung của một hình ảnh với phong cách của một hình ảnh khác. Ví dụ: bạn có thể lấy một bức ảnh về thành phố của mình và áp dụng phong cách của một bức tranh nổi tiếng, tạo ra một tác phẩm nghệ thuật độc đáo. Thuật toán này, như đã đề cập trong video
, lấy ba hình ảnh: hình ảnh đầu vào, hình ảnh nội dung, và hình ảnh phong cách.
Hình ảnh đầu vào là hình ảnh mà bạn muốn chuyển đổi. Hình ảnh nội dung cung cấp nội dung mà bạn muốn giữ lại trong hình ảnh đầu ra. Hình ảnh phong cách cung cấp phong cách nghệ thuật mà bạn muốn áp dụng.
Ví dụ: Nếu bạn muốn tạo ra một bức ảnh chân dung theo phong cách Van Gogh, bạn sẽ sử dụng ảnh chân dung của mình làm hình ảnh đầu vào, một bức ảnh phong cảnh quen thuộc làm hình ảnh nội dung (để giữ lại bố cục) và một bức tranh của Van Gogh làm hình ảnh phong cách. Kết quả sẽ là một bức chân dung mang phong cách đặc trưng của Van Gogh.
Nguyên tắc Hoạt động Của Thuật Toán Chuyển Đổi Phong Cách
Nguyên tắc cơ bản của thuật toán tương đối đơn giản. Chúng ta định nghĩa hai loại khoảng cách: khoảng cách nội dung và khoảng cách phong cách. Khoảng cách nội dung đo lường sự khác biệt về nội dung giữa hai hình ảnh. Khoảng cách phong cách đo lường sự khác biệt về phong cách giữa Hai hình ảnh
.
Mục tiêu của thuật toán là giảm thiểu cả hai loại khoảng cách này. Điều này được thực hiện bằng cách lấy hình ảnh đầu vào và điều chỉnh nó để nó vừa giống với nội dung của hình ảnh nội dung, vừa giống với phong cách của hình ảnh phong cách. Quá trình này liên quan đến việc tính toán các gradient và sử dụng một thuật toán tối ưu hóa để điều chỉnh các tensor hình ảnh.
Tối ưu hóa là một quá trình lặp đi lặp lại. Trong mỗi lần lặp, thuật toán sẽ tính toán các gradient của hàm mất mát (loss function) liên quan đến hình ảnh đầu vào. Gradient chỉ ra hướng mà hình ảnh đầu vào cần phải thay đổi để giảm thiểu hàm mất mát. Thuật toán sau đó sẽ điều chỉnh hình ảnh đầu vào theo hướng này. Quá trình này được lặp lại nhiều lần cho đến khi hình ảnh đầu ra đạt được sự cân bằng mong muốn giữa nội dung và phong cách. Điều này có thể mất nhiều thời gian, tùy thuộc vào độ phức tạp của hình ảnh và sức mạnh tính toán của hệ thống của bạn.
Nhập Các Gói và Chọn Thiết Bị
Để bắt đầu, chúng ta cần nhập các gói cần thiết từ PyTorch. Các gói này bao gồm:
torch
: Gói cốt lõi của PyTorch, cung cấp các tensor và các hàm toán học cơ bản
.
torch.nn
: Cung cấp các lớp và các hàm cho việc xây dựng mạng nơ-ron.
torch.optim
: Cung cấp các thuật toán tối ưu hóa.
PIL
: Thư viện hình ảnh Python, dùng để tải và hiển thị hình ảnh.
matplotlib
: Thư viện vẽ đồ thị Python, dùng để hiển thị hình ảnh.
torchvision.transforms
: Cung cấp các phép biến đổi hình ảnh.
torchvision.models
: Cung cấp các mô hình được huấn luyện trước.
copy
: Để sao chép các đối tượng.
Sau khi nhập các gói, chúng ta cần chọn thiết bị mà chúng ta sẽ sử dụng để chạy mạng nơ-ron. Nếu bạn có một GPU, bạn nên sử dụng nó để tăng tốc quá trình tính toán . Nếu không, bạn có thể sử dụng CPU.
Bạn có thể kiểm tra xem GPU có sẵn hay không bằng cách sử dụng hàm torch.cuda.is_available()
. Nếu GPU có sẵn, bạn có thể đặt thiết bị thành 'cuda'
. Nếu không, bạn có thể đặt nó thành 'cpu'
.
Tải Hình Ảnh
Tiếp theo, chúng ta cần tải hình ảnh nội dung và phong cách. Bạn có thể sử dụng thư viện PIL để tải hình ảnh từ các tệp. Sau khi tải hình ảnh, chúng ta cần phải biến đổi chúng thành các tensor PyTorch. Chúng ta có thể sử dụng các phép biến đổi từ torchvision.transforms
để thực hiện việc này.
Các phép biến đổi phổ biến bao gồm:
Resize
: Thay đổi kích thước hình ảnh.
ToTensor
: Chuyển đổi hình ảnh thành một tensor PyTorch.
Normalize
: Chuẩn hóa các giá trị pixel của hình ảnh.
Chúng ta cần đảm bảo rằng hình ảnh nội dung và phong cách có cùng kích thước. Điều này là do thuật toán chuyển đổi phong cách hoạt động tốt nhất khi hình ảnh đầu vào có cùng kích thước.
import torch
from PIL import Image
from torchvision import transforms
# Định nghĩa kích thước mong muốn của hình ảnh đầu ra
imsize = 512 if torch.cuda.is_available() else 128 # if GPU is available, use larger size
loader = transforms.Compose([
transforms.Resize(imsize),
transforms.ToTensor()])
def image_loader(image_name):
image = Image.open(image_name)
image = loader(image).unsqueeze(0)
return image.to(device, torch.float)
style_img = image_loader("images/picasso.jpg")
content_img = image_loader("images/dancing.jpg")
assert style_img.size() == content_img.size(), \
"we need to import style and content images of the same size"
Trong đoạn code trên:
- Chúng ta định nghĩa kích thước mong muốn của hình ảnh đầu ra (
imsize
). Nếu GPU có sẵn, chúng ta sử dụng kích thước lớn hơn (512). Nếu không, chúng ta sử dụng kích thước nhỏ hơn (128).
- Chúng ta tạo một
transform
bằng cách sử dụng transforms.Compose
. Transform này sẽ thay đổi kích thước hình ảnh và chuyển đổi nó thành một tensor PyTorch.
- Chúng ta định nghĩa một hàm
image_loader
để tải hình ảnh và áp dụng các transform.
- Chúng ta tải hình ảnh phong cách và nội dung.
- Chúng ta xác nhận rằng hình ảnh phong cách và nội dung có cùng kích thước.
Định Nghĩa Hàm Mất Mát (Loss Functions)
Hàm mất mát là một phần quan trọng của thuật toán chuyển đổi phong cách. Nó đo lường sự khác biệt giữa hình ảnh đầu ra và hình ảnh nội dung và hình ảnh phong cách. Mục tiêu của thuật toán là giảm thiểu hàm mất mát này. Chúng ta sử dụng hàm mất mát cho nội dung và một hàm mất mát khác cho phong cách
.
Mất mát nội dung là một hàm đo lường sự khác biệt giữa các đặc trưng nội dung của hình ảnh đầu ra và hình ảnh nội dung. Các đặc trưng nội dung là các đặc trưng được trích xuất từ một mạng nơ-ron được huấn luyện trước, chẳng hạn như VGG19.
Mất mát phong cách là một hàm đo lường sự khác biệt giữa các đặc trưng phong cách của hình ảnh đầu ra và hình ảnh phong cách. Các đặc trưng phong cách là các đặc trưng được trích xuất từ một mạng nơ-ron được huấn luyện trước, chẳng hạn như VGG19, nhưng chúng được tính toán khác với các đặc trưng nội dung.
class ContentLoss(nn.Module):
def __init__(self, target,):
super(ContentLoss, self).__init__()
# we 'detach' the target content from the tree used
# to dynamically compute the gradient: this is a stated value.
# We're going to use it as a constant target.
# This helps better training towards the later layers.
self.target = target.detach()
def forward(self, input):
self.loss = F.mse_loss(input, self.target)
return input
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) # resize F_XL into \hat F_XL
G = torch.mm(features, features.t()) # compute the gram product
# we 'normalize' the values of the gram matrix
# by dividing by the number of element in each feature maps.
return G.div(a * b * c * d)
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
Xây Dựng Mô Hình
Ở đây, chúng ta muốn tạo một mô hình, một mạng nơ-ron được thiết kế để phân tích và kết hợp các đặc trưng của hình ảnh. Kiến trúc của mô hình này sẽ dựa trên mô hình VGG19 được huấn luyện trước, một lựa chọn phổ biến để trích xuất các đặc trưng thị giác. Mô hình này sẽ bao gồm các lớp tích chập và các lớp gộp, theo sau là các lớp hoàn toàn kết nối để phân loại hình ảnh.
Chúng ta xác định các lớp nội dung và phong cách, quyết định lớp nào sẽ được sử dụng để tính toán các hàm mất mát nội dung và phong cách. Các lớp này sẽ xác định các đặc trưng của hình ảnh đầu vào được sử dụng để chuyển đổi phong cách. Thông thường, các lớp sâu hơn trong mạng nơ-ron nắm bắt thông tin ngữ nghĩa cao hơn, trong khi các lớp nông hơn nắm bắt thông tin phong cách.
Chèn các hàm mất mát vào mô hình là rất quan trọng
. Chúng ta sẽ chèn các mô-đun mất mát nội dung và phong cách trực tiếp sau các lớp tích chập mà chúng ta đã chọn trước đó. Các mô-đun mất mát này sẽ tính toán mất mát nội dung và phong cách ở mỗi lớp được chỉ định, cho phép mô hình học cách bảo tồn nội dung và phong cách của hình ảnh.
import torch.nn as nn
import torch.nn.functional as F
import torchvision.models as models
vgg = models.vgg19(pretrained=True).features.to(device).eval()
norm_mean = torch.tensor([0.485, 0.456, 0.406]).to(device)
norm_std = torch.tensor([0.229, 0.224, 0.225]).to(device)
# create a module to normalize input image so we can easily put it in a
# nn.Sequential
class Normalization(nn.Module):
def __init__(self, mean, std):
super(Normalization, self).__init__()
# .view the mean and std to make them [C x 1 x 1] so that they can
# directly work with image Tensor of shape [B x C x H x W].
# B is batch size. C is number of channels. H is height and W is width.
self.mean = torch.tensor(mean).view(-1, 1, 1)
self.std = torch.tensor(std).view(-1, 1, 1)
def forward(self, img):
# normalize img
return (img - self.mean) / self.std
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):
# normalization module
normalization = Normalization(normalization_mean, normalization_std).to(device)
# just in order to have an iterable access or list of content/style
# losses
content_losses = []
style_losses = []
# assuming that cnn is a nn.Sequential, so we make a new nn.Sequential
# to put in modules that are supposed to be activated sequentially
model = nn.Sequential(normalization)
i = 0 # increment every time we see a conv
for layer in cnn.children():
if isinstance(layer, nn.Conv2d):
i += 1
name = 'conv_{}'.format(i)
elif isinstance(layer, nn.ReLU):
name = 'relu_{}'.format(i)
# The in-place version doesn't play very nicely with the ContentLoss
# and StyleLoss we insert below. So we replace with out-of-place
# ones here.
layer = nn.ReLU(inplace=False)
elif isinstance(layer, nn.MaxPool2d):
name = 'pool_{}'.format(i)
model.add_module(name, layer)
if name in content_layers:
# add content loss:
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:
# add style loss:
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)
# now we trim off the layers after the last content and style losses
for i in range(len(model) - 1, -1, -1):
if isinstance(model[i], ContentLoss) or isinstance(model[i], StyleLoss):
break
model = model[:(i + 1)]
return model, style_losses, content_losses
Chọn Hình Ảnh Đầu Vào
Ở bước này, chúng ta sẽ chọn hình ảnh đầu vào mà chúng ta muốn chuyển đổi phong cách
. Bạn có thể sử dụng một bản sao của hình ảnh nội dung hoặc tạo một hình ảnh hoàn toàn mới bằng nhiễu trắng. Hình ảnh đầu vào sẽ được chuyển đổi sao cho nó vừa giống với nội dung của hình ảnh nội dung, vừa giống với phong cách của hình ảnh phong cách.
input_img = content_img.clone()
# if you want to use white noise instead uncommment the below line:
# input_img = torch.randn(content_img.data.size(), device=device)
# add the original input image to the figure:
plt.figure()
imshow(input_img, title='Input Image')
Gradient Descent (Hạ Gradient)
Cuối cùng, chúng ta sẽ định nghĩa một hàm để chạy gradient descent. Gradient descent là một thuật toán tối ưu hóa được sử dụng để tìm giá trị tối thiểu của một hàm số. Trong trường hợp này, chúng ta muốn tìm hình ảnh đầu vào làm giảm thiểu hàm mất mát. Hàm get_input_optimizer
lấy hình ảnh đầu vào và trả về một trình tối ưu hóa .
def get_input_optimizer(input_img):
# this line to show that input is a parameter that requires a gradient
optimizer = optim.LBFGS([input_img.requires_grad_(True)])
return optimizer
# Finally, run the training.
def run_style_transfer(cnn, normalization_mean, normalization_std,
content_img, style_img, input_img, num_steps=300,
style_weight=1000000, content_weight=1):
"""Run the style transfer."""
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)
# Run gradient descent.
print('Optimizing..')
run = [0]
while run[0] <= num_steps:
def closure():
# correct the values of updated input image
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 {}:\
Chọn Lớp Để Tính Toán Mất Mát
Một khía cạnh quan trọng khác của chuyển đổi phong cách là lựa chọn lớp nào trong mạng nơ-ron tích chập để tính toán mất mát nội dung và phong cách
. Các lớp khác nhau trong mạng nơ-ron nắm bắt các đặc trưng khác nhau của hình ảnh, vì vậy việc lựa chọn lớp phù hợp có thể có ảnh hưởng lớn đến kết quả.