Como ficariam as fotos do seu feriado de verão se Edvard Munch as tivesse tocado? (Talvez seja melhor não saber). Vamos dar um exemplo mais reconfortante: como seria uma bela paisagem de rio sumartamente se pintada por Katsushika Hokusai?
A transferência de estilo em imagens não é nova, mas recebeu um impulso quando Gatys, Ecker e Bethge(Gatys, Ecker e Bethge 2015) mostrou como fazê -lo com sucesso com o aprendizado profundo. A idéia principal é direta: crie um híbrido que seja uma troca entre o imagem de conteúdo Queremos manipular e um imagem de estilo Queremos imitar, otimizando para obter a máxima semelhança com ambos ao mesmo tempo.
Se você leu o capítulo sobre transferência de estilo neural de Aprendizado profundo com rvocê pode reconhecer alguns dos trechos de código que se seguem. No entanto, há uma diferença importante: esta postagem usa tensorflow Execução ansiosapermitindo uma maneira imperativa de codificação que facilita o mapeamento dos conceitos para codificar. Assim como posts anteriores sobre execução ansiosa neste weblog, este é um porto de um Pocket book colaboratório do Google que executa a mesma tarefa em Python.
Como sempre, verifique se você possui as versões de pacote necessárias instaladas. E não há necessidade de copiar os trechos – você encontrará o código completo entre os Exemplos de Keras.
Pré -requisitos
O código nesta postagem depende das versões mais recentes de vários pacotes Tensorflow R. Você pode instalar estes pacotes da seguinte maneira:
set up.packages(c("tensorflow", "keras", "tfdatasets"))
Você também deve ter certeza de que está executando a versão mais recente do TensorFlow (v1.10), que você pode instalar como assim:
library(tensorflow)
install_tensorflow()
Existem requisitos adicionais para usar a execução ansiosa do TensorFlow. Primeiro, precisamos ligar tfe_enable_eager_execution()
Emblem no início do programa. Segundo, precisamos usar a implementação de Keras incluída no Tensorflow, em vez da implementação da base do Keras.
Pré -requisitos para trás, vamos começar!
Imagens de entrada
Aqui está a nossa imagem de conteúdo – substitua por uma imagem própria:
# If in case you have sufficient reminiscence in your GPU, no have to load the pictures
# at such small dimension.
# That is the dimensions I discovered working for a 4G GPU.
img_shape <- c(128, 128, 3)
content_path <- "isar.jpg"
content_image <- image_load(content_path, target_size = img_shape(1:2))
content_image %>%
image_to_array() %>%
`/`(., 255) %>%
as.raster() %>%
plot()
E aqui está o modelo de estilo, Hokusai’s A grande onda de Kanagawada qual você pode baixar Wikimedia Commons:
Criamos um invólucro que carrega e pré -processos as imagens de entrada para nós. Como trabalharemos com o VGG19, uma rede que foi treinada no ImageNet, precisamos transformar nossas imagens de entrada da mesma maneira que foi usada para treiná -la. Mais tarde, aplicaremos a transformação inversa à nossa imagem combinada antes de exibi -la.
load_and_preprocess_image <- perform(path) {
img <- image_load(path, target_size = img_shape(1:2)) %>%
image_to_array() %>%
k_expand_dims(axis = 1) %>%
imagenet_preprocess_input()
}
deprocess_image <- perform(x) {
x <- x(1, , ,)
# Take away zero-center by imply pixel
x(, , 1) <- x(, , 1) + 103.939
x(, , 2) <- x(, , 2) + 116.779
x(, , 3) <- x(, , 3) + 123.68
# 'BGR'->'RGB'
x <- x(, , c(3, 2, 1))
x(x > 255) <- 255
x(x < 0) <- 0
x() <- as.integer(x) / 255
x
}
Definindo a cena
Vamos usar uma rede neural, mas não a treinaremos. A transferência de estilo neural é um pouco incomum, pois não otimizamos os pesos da rede, mas propagará a perda para a camada de entrada (a imagem), a fim de movê -la na direção desejada.
Estaremos interessados em dois tipos de saídas da rede, correspondendo aos nossos dois objetivos. Em primeiro lugar, queremos manter a imagem combinada semelhante à imagem de conteúdo, em um nível alto. Em um convnet, as camadas superiores são mapeadas para conceitos mais holísticos, por isso estamos colhendo uma camada alta no gráfico para comparar saídas da fonte e da combinação.
Em segundo lugar, a imagem gerada deve “parecer” a imagem de estilo. O estilo corresponde a recursos de nível inferior, como textura, formas, traços … para comparar a combinação com o exemplo de estilo, escolhemos um conjunto de blocos de convivência de nível inferior para comparação e agregar os resultados.
content_layers <- c("block5_conv2")
style_layers <- c("block1_conv1",
"block2_conv1",
"block3_conv1",
"block4_conv1",
"block5_conv1")
num_content_layers <- size(content_layers)
num_style_layers <- size(style_layers)
get_model <- perform() {
vgg <- application_vgg19(include_top = FALSE, weights = "imagenet")
vgg$trainable <- FALSE
style_outputs <- map(style_layers, perform(layer) vgg$get_layer(layer)$output)
content_outputs <- map(content_layers, perform(layer) vgg$get_layer(layer)$output)
model_outputs <- c(style_outputs, content_outputs)
keras_model(vgg$enter, model_outputs)
}
Perdas
Ao otimizar a imagem de entrada, consideraremos três tipos de perdas. Em primeiro lugar, o perda de conteúdo: Quão diferente é a imagem combinada da fonte? Aqui, estamos usando a soma dos erros quadrados para comparação.
content_loss <- perform(content_image, goal) {
k_sum(k_square(goal - content_image))
}
Nossa segunda preocupação é fazer com que os estilos correspondam o mais próximo possível. O estilo é comumente operacionalizado como o Matriz Gram de mapas de recursos achatados em uma camada. Assim, assumimos que o estilo está relacionado a como os mapas em uma camada se correlacionam com outros.
Portanto, calculamos as matrizes Gram das camadas em que estamos interessados (definidos acima), para a imagem de origem, bem como o candidato a otimização, e comparamos novamente usando a soma dos erros quadrados.
gram_matrix <- perform(x) {
options <- k_batch_flatten(k_permute_dimensions(x, c(3, 1, 2)))
gram <- k_dot(options, k_transpose(options))
gram
}
style_loss <- perform(gram_target, mixture) {
gram_comb <- gram_matrix(mixture)
k_sum(k_square(gram_target - gram_comb)) /
(4 * (img_shape(3) ^ 2) * (img_shape(1) * img_shape(2)) ^ 2)
}
Em terceiro lugar, não queremos que a imagem combinada pareça excessivamente pixelizada, assim estamos adicionando um componente de regularização, a variação whole na imagem:
total_variation_loss <- perform(picture) {
y_ij <- picture(1:(img_shape(1) - 1L), 1:(img_shape(2) - 1L),)
y_i1j <- picture(2:(img_shape(1)), 1:(img_shape(2) - 1L),)
y_ij1 <- picture(1:(img_shape(1) - 1L), 2:(img_shape(2)),)
a <- k_square(y_ij - y_i1j)
b <- k_square(y_ij - y_ij1)
k_sum(k_pow(a + b, 1.25))
}
O mais complicado é como combinar essas perdas. Atingimos resultados aceitáveis com os seguintes ponderações, mas fique à vontade para brincar como achar melhor:
content_weight <- 100
style_weight <- 0.8
total_variation_weight <- 0.01
Obtenha saídas de modelo para as imagens de conteúdo e estilo
Precisamos da saída do modelo para as imagens de conteúdo e estilo, mas aqui basta fazer isso apenas uma vez. Concatenamos as duas imagens ao longo da dimensão do lote, passamos essa entrada para o modelo e recebemos uma lista de saídas, onde todos os elementos da lista são um tensor 4-D. Para a imagem de estilo, estamos interessados nas saídas de estilo na posição 1 do lote, enquanto que para a imagem de conteúdo, precisamos da saída de conteúdo na posição 2 do lote 2.
Nos comentários abaixo, observe que os tamanhos das dimensões 2 e 3 serão diferentes se você estiver carregando imagens de um tamanho diferente.
get_feature_representations <-
perform(mannequin, content_path, style_path) {
# dim == (1, 128, 128, 3)
style_image <-
load_and_process_image(style_path) %>% k_cast("float32")
# dim == (1, 128, 128, 3)
content_image <-
load_and_process_image(content_path) %>% k_cast("float32")
# dim == (2, 128, 128, 3)
stack_images <- k_concatenate(listing(style_image, content_image), axis = 1)
# size(model_outputs) == 6
# dim(model_outputs((1))) = (2, 128, 128, 64)
# dim(model_outputs((6))) = (2, 8, 8, 512)
model_outputs <- mannequin(stack_images)
style_features <-
model_outputs(1:num_style_layers) %>%
map(perform(batch) batch(1, , , ))
content_features <-
model_outputs((num_style_layers + 1):(num_style_layers + num_content_layers)) %>%
map(perform(batch) batch(2, , , ))
listing(style_features, content_features)
}
Calculando as perdas
Em todas as iterações, precisamos passar a imagem combinada através do modelo, obter o estilo e as saídas de conteúdo e calcular as perdas. Novamente, o código é comentado extensivamente com tamanhos de tensor para facilitar a verificação, mas lembre -se de que os números exatos pressupõem que você está trabalhando com imagens de 128×128.
compute_loss <-
perform(mannequin, loss_weights, init_image, gram_style_features, content_features) {
c(style_weight, content_weight) %<-% loss_weights
model_outputs <- mannequin(init_image)
style_output_features <- model_outputs(1:num_style_layers)
content_output_features <-
model_outputs((num_style_layers + 1):(num_style_layers + num_content_layers))
# type loss
weight_per_style_layer <- 1 / num_style_layers
style_score <- 0
# dim(style_zip((5))((1))) == (512, 512)
style_zip <- transpose(listing(gram_style_features, style_output_features))
for (l in 1:size(style_zip)) {
# for l == 1:
# dim(target_style) == (64, 64)
# dim(comb_style) == (1, 128, 128, 64)
c(target_style, comb_style) %<-% style_zip((l))
style_score <- style_score + weight_per_style_layer *
style_loss(target_style, comb_style(1, , , ))
}
# content material loss
weight_per_content_layer <- 1 / num_content_layers
content_score <- 0
content_zip <- transpose(listing(content_features, content_output_features))
for (l in 1:size(content_zip)) {
# dim(comb_content) == (1, 8, 8, 512)
# dim(target_content) == (8, 8, 512)
c(target_content, comb_content) %<-% content_zip((l))
content_score <- content_score + weight_per_content_layer *
content_loss(comb_content(1, , , ), target_content)
}
# whole variation loss
variation_loss <- total_variation_loss(init_image(1, , ,))
style_score <- style_score * style_weight
content_score <- content_score * content_weight
variation_score <- variation_loss * total_variation_weight
loss <- style_score + content_score + variation_score
listing(loss, style_score, content_score, variation_score)
}
Calculando os gradientes
Assim que tivermos as perdas, obter os gradientes da perda geral em relação à imagem de entrada é apenas uma questão de ligar tape$gradient
no GradientTape
. Observe que a chamada aninhada para compute_loss
e, portanto, o chamado do modelo em nossa imagem de combinação acontece dentro do GradientTape
contexto.
compute_grads <-
perform(mannequin, loss_weights, init_image, gram_style_features, content_features) {
with(tf$GradientTape() %as% tape, {
scores <-
compute_loss(mannequin,
loss_weights,
init_image,
gram_style_features,
content_features)
})
total_loss <- scores((1))
listing(tape$gradient(total_loss, init_image), scores)
}
Fase de treinamento
Agora é hora de treinar! Embora a continuação pure desta frase tenha sido “… o modelo”, o modelo que estamos treinando aqui não é VGG19 (que estamos apenas usando como ferramenta), mas uma configuração mínima de apenas:
- um
Variable
que mantém nossa imagem otimizada a ser otimizada - as funções de perda que definimos acima
- um otimizador que aplicará os gradientes calculados à variável de imagem (
tf$practice$AdamOptimizer
)
Abaixo, obtemos os recursos de estilo (da imagem do estilo) e o recurso de conteúdo (da imagem de conteúdo) apenas uma vez, depois iterará o processo de otimização, salvando a saída a cada 100 iterações.
Em contraste com o artigo authentic e o Aprendizado profundo com r Livro, mas, seguindo o pocket book do Google, não estamos usando o L-BFGS para otimização, mas Adam, pois nosso objetivo aqui é fornecer uma introdução concisa à execução ansiosa. No entanto, você pode conectar outro método de otimização, se quiser, substituindo
optimizer$apply_gradients(listing(tuple(grads, init_image)))
por um algoritmo de sua escolha (e, claro, atribuindo o resultado da otimização ao Variable
segurando a imagem).
run_style_transfer <- perform(content_path, style_path) {
mannequin <- get_model()
stroll(mannequin$layers, perform(layer) layer$trainable = FALSE)
c(style_features, content_features) %<-%
get_feature_representations(mannequin, content_path, style_path)
# dim(gram_style_features((1))) == (64, 64)
gram_style_features <- map(style_features, perform(function) gram_matrix(function))
init_image <- load_and_process_image(content_path)
init_image <- tf$contrib$keen$Variable(init_image, dtype = "float32")
optimizer <- tf$practice$AdamOptimizer(learning_rate = 1,
beta1 = 0.99,
epsilon = 1e-1)
c(best_loss, best_image) %<-% listing(Inf, NULL)
loss_weights <- listing(style_weight, content_weight)
start_time <- Sys.time()
global_start <- Sys.time()
norm_means <- c(103.939, 116.779, 123.68)
min_vals <- -norm_means
max_vals <- 255 - norm_means
for (i in seq_len(num_iterations)) {
# dim(grads) == (1, 128, 128, 3)
c(grads, all_losses) %<-% compute_grads(mannequin,
loss_weights,
init_image,
gram_style_features,
content_features)
c(loss, style_score, content_score, variation_score) %<-% all_losses
optimizer$apply_gradients(listing(tuple(grads, init_image)))
clipped <- tf$clip_by_value(init_image, min_vals, max_vals)
init_image$assign(clipped)
end_time <- Sys.time()
if (k_cast_to_floatx(loss) < best_loss) {
best_loss <- k_cast_to_floatx(loss)
best_image <- init_image
}
if (i %% 50 == 0) {
glue("Iteration: {i}") %>% print()
glue(
"Whole loss: {k_cast_to_floatx(loss)},
type loss: {k_cast_to_floatx(style_score)},
content material loss: {k_cast_to_floatx(content_score)},
whole variation loss: {k_cast_to_floatx(variation_score)},
time for 1 iteration: {(Sys.time() - start_time) %>% spherical(2)}"
) %>% print()
if (i %% 100 == 0) {
png(paste0("style_epoch_", i, ".png"))
plot_image <- best_image$numpy()
plot_image <- deprocess_image(plot_image)
plot(as.raster(plot_image), foremost = glue("Iteration {i}"))
dev.off()
}
}
}
glue("Whole time: {Sys.time() - global_start} seconds") %>% print()
listing(best_image, best_loss)
}
Pronto para correr
Agora, estamos prontos para iniciar o processo:
c(best_image, best_loss) %<-% run_style_transfer(content_path, style_path)
No nosso caso, os resultados não mudaram muito após ~ ~ iteração 1000, e é assim que nossa paisagem do rio estava olhando:
… Definitivamente mais convidativo do que foi pintado por Edvard Munch!
Conclusão
Com a transferência de estilo neural, algumas brincadeiras podem ser necessárias até que você obtenha o resultado desejado. Mas, como mostra nosso exemplo, isso não significa que o código deve ser complicado. Além disso, para ser fácil de entender, a execução ansiosa também permite adicionar saída de depuração e passar pela linha de código para verificar se as formas tensoras. Até a próxima vez em nossa série de execução ansiosa!
Gatys, Leon A., Alexander S. Ecker e Matthias Bethge. 2015. “Um algoritmo neural do estilo artístico”. Corr ABS/1508.06576. http://arxiv.org/abs/1508.06576.