Cómo programar Neural Style Transfer en Python

Share on linkedin
Share on twitter
Share on email

En este blog hemos hablado mucho de redes neuronales: las hemos programado desde 0, las hemos utilizado para clasificar imágenes e, incluso, hemos creado imágenes ficticias con ellas. Hoy aprenderemos otra de las utilidades fascinantes de las redes neuronales: utilizar los estilos de una imagen en otra. Hoy, vamos a programar una red neuronal de Neural Style Transfer en Python. ¿Suena interesante? ¡Pues vamos a ello!

Preparación del entorno

Preparación de Google Colab

En mi caso estoy programando este post en Google Colab, de tal forma que pueda entrenar a la red neuronal sobre GPU de forma gratuita.

Sin embargo, como Google Colab te desconecta cada cierto tiempo, voy a sincronizar mi cuenta de Colab con Google Drive. De esta forma, iré guardando los resultados que vaya obteniendo sin tener que volver a empezar. En este caso puede que no sea tan extremo como en el de la GAN… pero bueno, nunca está de más hacerlo. Además, esto es algo que ya expliqué en este post, por lo que no me voy a detener mucho en ello.

En cualquier caso, primero activamos el uso de GPU.

import tensorflow as tf
device_name = tf.test.gpu_device_name()
if device_name != '/device:GPU:0':
  raise SystemError('GPU device not found')
print('Found GPU at: {}'.format(device_name))
Found GPU at: /device:GPU:0

Ahora que ya estamos trabajando sobre GPU, vamos a conectar Google Colab con nuestro Google Drive.

from google.colab import drive
import os
drive.mount('/content/gdrive/')
Drive already mounted at /content/gdrive/; to attempt to forcibly remount, call drive.mount("/content/gdrive/", force_remount=True).

Por último, accedemos a la carpeta de Drive donde guardo la información referida a este post.

%cd /content/gdrive/My\ Drive/Posts/Neural \Style \Transfer
/content/gdrive/My Drive/Posts/Neural Style Transfer

Ahora que ya tenemos Google Colab y Drive sincronizados, vamos a cargar los paquetes y los datos.

Cargando paquetes y datos

Como siempre, lo primero que tenemos que hacer es cargar los paquetes y los datos que vamos a utilizar. Respecto a los paquetes, utilizaremos Keras y Tensorflow para las redes neuronales y Numpy para manipulación de datos.

Asimismo, cabe destacar que esta implementación no me la he inventado, sino que me baso en la implementación ofrecida por Keras (con una explicación mucho más profunda y algunos cambios de código, eso sí).

import numpy as np
from keras.utils import get_file
import matplotlib.pyplot as plt

style_transfer_url = "https://i.imgur.com/9ooB60I.jpg"
base_url = "https://tourism.euskadi.eus/contenidos/d_destinos_turisticos/0000004981_d2_rec_turismo/en_4981/images/CT_cabecerabilbaoguggen.jpg"

style_image_path = get_file(fname = "skyscraper.jpg", origin = style_transfer_url)
base_image_path = get_file(fname = "bilbao.jpg", origin = base_url)
Downloading data from https://i.imgur.com/9ooB60I.jpg
942080/935806 [==============================] - 0s 0us/step
Downloading data from https://tourism.euskadi.eus/contenidos/d_destinos_turisticos/0000004981_d2_rec_turismo/en_4981/images/CT_cabecerabilbaoguggen.jpg
204800/201498 [==============================] - 0s 2us/step

Por último, vamos a visualizar las imágenes que nos hemos descargado y que vamos a utilizar para el Neural Style Transfer.

import matplotlib.image as mpimg
import matplotlib.pyplot as plt

# read the image file in a numpy array
a = plt.imread(base_image_path)
b = plt.imread(style_image_path)
f, axarr = plt.subplots(1,2, figsize=(15,15))
axarr[0].imshow(a)
axarr[1].imshow(b)
plt.show()
Imágen de contenido y de estilo a utilizar para programar el algoritmo de Neural Style Transfer en Python

Ahora que ya tenemos los datos cargados, ¡vamos a programar nuestro Neural Style Transfer en Python!

Entendiendo cómo funciona una Neural Style Transfer

Si queremos programar un Neural Style Transfer, lo primero que debemos hacer es entender a la perfección cómo funcionan las redes neuronales convolucionales. En mi caso, ya hablé de ellas en este post, aunque hoy va a ser más específico.

De cara a que una red de Neural Style Transfer funcione, debemos conseguir, mínimo, dos cosas:

  1. Transmitir el estilo lo máximo posible.
  2. Que la imagen resultante se parezca lo máximo posible a la imagen original.

Podríamos considerar un tercer objetivo, y es que la imagen resultante sea coherente a nivel interno. Esto es algo que incluye la explicación de Keras pero que, en mi caso, no voy a profundizar.

Para ello, una red de neural style transfer cuenta con lo siguiente:

  1. Una red neuronal convolucional ya entrenada (como puede ser VGG19 o VGG16). A esta red se le pasarán tres imágenes, la imagen base, la imagen de estilos y la combinación. Esta última podría ser tanto una imagen de ruido como la imagen base, aunque generalmente se suele pasar la imagen base para asegurarnos de que la imagen resultante se parece y agilizar el proceso.
  2. Una imagen base que va cambiando de tal forma que coja los estilos de la imagen de estilos mientras mantiene el contenido de la imagen base. Para ello, optimizaremos la imagen mediante dos costes diferentes (estilo y contenido).
Funcionamiento del algoritmo de Neural Style Transfer

En la medida en que consigamos estos dosobjetivos, tendremos buenos resultados. Ahora bien, ¿cómo conseguimos que nuestra red aprenda esto? ¡Veámoslo!

Cómo transferir el estilo de una imagen

En las redes neuronales convolucionales, cuanto más profundo vamos en la red, la red es capaz de distinguir formas más complejas. Esto es algo que se puede ver claramente en la aplicación ConvNet Playground, la cual permite ver los canales de capas en distinta «profundidad» de la red.

Por tanto, si queremos transferir el estilo de una imagen, tendremos que conseguir que los valores de los features de las capas profundas de nuestra red se parezcan a los de la red de la imagen de estilo.

Pero, ¿cómo podemos calcular la función de coste de este proceso para así ir ajustándolo? Para ello, utilizamos las conocidas como Gram Matrix.

Qué es y cómo obtener la matrix Gram Matrix

Supongamos que queremos calcular el coste de estilo en una capa. Para ello, lo primero que debemos hacer es aplanar (flatten) nuestra capa. Esto es algo bueno, ya que el cálculo del Gram Matrix no cambiará en función del tamaño de la capa.

Así pues, supongamos que hacemos un flatten sobre una capa de 3 filtros. Pues bien, el Gram Matrix muestra la similitud entre los filtros y se obtiene calculando producto escalar entre los vectores:

Fórmula Gram Matrix

Al estar calculando productos escalares, estamos teniendo en cuenta la distancia: a menor producto escalar, más cerca están los dos vectores y viceversa. Al final es algo similar a lo que ya hicimos cuando programamos el sistema de recomendación.

Si queréis profundizar en cómo se calculara os recomiendo que veáis este vídeo. De todos modos, en nuestro caso, lo vamos a programar:

def gram_matrix(x):
    x = tf.transpose(x, (2, 0, 1))
    features = tf.reshape(x, (tf.shape(x)[0], -1))
    gram = tf.matmul(features, tf.transpose(features))
    return gram

Ahora que ya tenemos la Gram matrix podemos calcular la función de coste del estilo, que básicamente es el grado de correlación entre los estilos dentro de una capa.

Por tanto, para calcular la función de coste vamos a calcular la Gram matrix tanto de la imgen a transferir como de la imagen resultante y calcular el error cuadrático medio. Según el paper original, esto es dividido por 4 (no he llegado ha encontrar la explicación de eso).

En cualquier caso, lo programamos:

def coste_estilo(style, combination):
    S = gram_matrix(style)
    C = gram_matrix(combination)
    channels = 3
    size = img_nrows * img_ncols
    return tf.reduce_sum(tf.square(S - C)) / (4.0 * (channels ** 2) * (size ** 2))

Y con esto ya, ¡ya hemos programado la función de coste del estilo programado! Sí, es complejo, pero no te preocupes que el resto de cuestiones son más sencillas.

Ahora, veámos cómo conseguir el segundo punto vital que hemos comentado: conseguir que la imagen resultante se parezca lo máximo posible a la imagen de entrada.

Cómo conseguir que la imagen resultante se parezca a la imagen original

Para conseguir que la imagen original y la resultante se parezcan, debemos, de alguna forma, medir la similitud entre ambos. Cómo no, esta será una función de coste a utilizar, que en este caso llamaremos función de coste de contenido.

En este sentido, la función de coste de contenido es bastante más sencilla que la de estilo. Y es que, según este estudio, imágenes parecidas suelen tener capas profundas similares.

Por tanto, si dos imágenes tienen un contenido similar, entonces tendrán capas profundas similares. Teniendo esto en cuenta es como programaremos la función de coste de contenido.

def coste_contenido(base, combination):
    return tf.reduce_sum(tf.square(combination - base))

Con esto ya nos aseguraríamos de que cumplimos el segundo requisito. Por tanto, ya nos podemos asegurar que vamos a conseguir que se transifera el estilo manteniendo el contenido.

Ahora bien, ¿qué estructura necesitamos para generar todo esto? ¡Vamos a verlo!

Estructura de una red de Neural Style Transfer

Para programar una Neural Style Transfer (en este caso en Python), al igual que en una GAN, partiremos de una imagen base. Como he comentado, esta imagen puede ser o bien ‘ruido’ o la propia imagen de base (generalmente se suele usar la imagen base ya que suele ser más rápido).

Esta imagen la pasaremos por una red neuronal convolucional de clasificación. Generalmente se suelen aprovechar redes neuronales ya creadas. En nuestro caso, usaremos la red VGG19 entrenada con el dataset ImageNet, la cual es una red neuronal que ofrece Keras.

¿Influye el tipo de red neuronal que usemos? Pues según este artículo, sí: las redes como VGG-16 o VGG-19 generan imágenes con estilo al oleo, mientras que el uso de inception networks genera imágenes más estilo lápiz.

En cualquier caso, independientemente de la red neuronal que elijamos, pasaremos por esa red tanto a la imagen generada, como a la imagen base y la imagen de estilo.

Así, comparando los datos en la distintas capas entre la imagen base y la imagen generada obtendremos el coste de contenido, mientras que, comparando las capas de las capas de la imagen de estilo con la imagen generada obtendremos el coste de estilo.

Con estos dos costes obtendremos el coste total que, al optimizar, irá mejorando los resultados de nuestra imagen.

Ahora que ya tenemos una buena base de cómo funciona una red de Neural Style Transfer, vamos a aprender a programarla en Python!

Cómo programar una Neural Style Transfer en Python

Carga del Modelo VGG19

Lo primero de todo vamos a cargar el modelo VGG19. Como comentaba, es un modelo que ya lo ofrece Keras, por lo que no hay mayor complicación:

from tensorflow.keras.applications import vgg19
from keras.utils import plot_model

model = vgg19.VGG19(weights="imagenet", include_top=False)

model.summary()
Model: "vgg19"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_1 (InputLayer)         [(None, None, None, 3)]   0         
_________________________________________________________________
block1_conv1 (Conv2D)        (None, None, None, 64)    1792      
_________________________________________________________________
block1_conv2 (Conv2D)        (None, None, None, 64)    36928     
_________________________________________________________________
block1_pool (MaxPooling2D)   (None, None, None, 64)    0         
_________________________________________________________________
block2_conv1 (Conv2D)        (None, None, None, 128)   73856     
_________________________________________________________________
block2_conv2 (Conv2D)        (None, None, None, 128)   147584    
_________________________________________________________________
block2_pool (MaxPooling2D)   (None, None, None, 128)   0         
_________________________________________________________________
block3_conv1 (Conv2D)        (None, None, None, 256)   295168    
_________________________________________________________________
block3_conv2 (Conv2D)        (None, None, None, 256)   590080    
_________________________________________________________________
block3_conv3 (Conv2D)        (None, None, None, 256)   590080    
_________________________________________________________________
block3_conv4 (Conv2D)        (None, None, None, 256)   590080    
_________________________________________________________________
block3_pool (MaxPooling2D)   (None, None, None, 256)   0         
_________________________________________________________________
block4_conv1 (Conv2D)        (None, None, None, 512)   1180160   
_________________________________________________________________
block4_conv2 (Conv2D)        (None, None, None, 512)   2359808   
_________________________________________________________________
block4_conv3 (Conv2D)        (None, None, None, 512)   2359808   
_________________________________________________________________
block4_conv4 (Conv2D)        (None, None, None, 512)   2359808   
_________________________________________________________________
block4_pool (MaxPooling2D)   (None, None, None, 512)   0         
_________________________________________________________________
block5_conv1 (Conv2D)        (None, None, None, 512)   2359808   
_________________________________________________________________
block5_conv2 (Conv2D)        (None, None, None, 512)   2359808   
_________________________________________________________________
block5_conv3 (Conv2D)        (None, None, None, 512)   2359808   
_________________________________________________________________
block5_conv4 (Conv2D)        (None, None, None, 512)   2359808   
_________________________________________________________________
block5_pool (MaxPooling2D)   (None, None, None, 512)   0         
=================================================================
Total params: 20,024,384
Trainable params: 20,024,384
Non-trainable params: 0
_________________________________________________________________

Cálculo de las funciones de coste

Ahora que tenemos el modelo, debemos crear una función que nos extraiga los valores de ese modelo para unas capas dadas (de esta forma podremos usarlo tanto para el error de contenido como el error de estilo).

from keras import Model

outputs_dict= dict([(layer.name, layer.output) for layer in model.layers])

feature_extractor = Model(inputs=model.inputs, outputs=outputs_dict)

Así pues, vamos a definir qué capas vamos a usar para calcular la función de coste del estilo y qué capa vamos a usar para calcular la función de coste del contenido.

Siguiendo el ejemplo ofrecido por Keras, para calcular la función de coste de estilo usaremos la primera convolución de cada bloque, mientras que para el contenido vamos a usar la segunda convolución del último bloque.

Así pues, para calcular el coste seguiremos los siguientes pasos:

  1. Combinar todas las imágenes en un mismo tensor.
  2. Obtener los valores en todas las capas para las tres imágenes. Sí, es verdad que no necesitaremos todos los valores de todas las imágenes, pero así será más fácil, ya que ya tendremos todo extraído. De hecho, si quisiéramos cambiar la extracción de estilos, también sería muy sencillo.
  3. Inicializar el vector de coste para poder ir sumando los resultados.
  4. Extraer las capas de contenido para la imagen base y la combinación y calcular la función de coste de contenido.
  5. Extraer las capas de estilo para la imagen de estilo y la combinación y calcular la función de coste de estilo.

Como véis, es bastante fácil, así que vamos a hacerlo!

capas_estilo = [
    "block1_conv1",
    "block2_conv1",
    "block3_conv1",
    "block4_conv1",
    "block5_conv1",
]

capas_contenido = "block5_conv2"

content_weight = 2.5e-8
style_weight = 1e-6

def loss_function(combination_image, base_image, style_reference_image):

    # 1. Combinar todas las imágenes en un mismo tensor.
    input_tensor = tf.concat(
        [base_image, style_reference_image, combination_image], axis=0
    )

    # 2. Obtener los valores en todas las capas para las tres imágenes
    features = feature_extractor(input_tensor)

    #3. Inicializar el coste
    loss = tf.zeros(shape=())

    # 4. Extraer las capas de contenido + coste de contenido
    layer_features = features[capas_contenido]
    base_image_features = layer_features[0, :, :, :]
    combination_features = layer_features[2, :, :, :]

    loss = loss + content_weight * coste_contenido(
        base_image_features, combination_features
    )
    # 5. Extraer las capas de estilo + coste de estilo
    for layer_name in capas_estilo:
        layer_features = features[layer_name]
        style_reference_features = layer_features[1, :, :, :]
        combination_features = layer_features[2, :, :, :]
        sl = coste_estilo(style_reference_features, combination_features)
        loss += (style_weight / len(capas_estilo)) * sl

    return loss

¡Funciones de coste terminadas! Ahora vamos a ver cómo hacemos que la red aprenda. ¡Vayamos a por la optimziación y los gradientes!

Aprendizaje del Neural Style Transfer

Ahora que ya tenemos la función de coste, tenemos que calcular las deltas, que son las que utiliza gradient descent (o cualquier otro optimizador) para encontrar nuestros valores óptimos. Para calcular estas deltas hacen falta calcular las derivadas.

Esto, en Tensorflow lo conseguimos con la función GradientTape. Así pues, crearemos una función que, dado un coste, devuelva los gradientes. Estos gradientes se calcularán únicamente con la imagen base.

import tensorflow as tf

@tf.function
def compute_loss_and_grads(combination_image, base_image, style_reference_image):
    with tf.GradientTape() as tape:
        loss = loss_function(combination_image, base_image, style_reference_image)
    grads = tape.gradient(loss, combination_image)
    return loss, grads

¡Con esto ya tenemos la fase de aprendizaje hecha! Por último solo quedan dos cosas para terminar de programar nuestro Neural Style Transfer en Python: preparar las imágenes y crear el loop de entrenamiento.

¡Vamos a por ello!

Preprocesar y desprocesar las imágenes

Cómo preprocesar las imágenes

El preprocesamiento de las imágenes consiste en dar a las imágenes el formato que requiere nuestra red. En el caso de Keras, al tratarse del modelo VGG19, el propio modelo cuenta con una función de preprocesamiento de imágene: preprocess_input.

Hay que tener en cuenta que Keras trabaja con batches de imágenes. Por tanto, la información que le pasemos deberá estar en este formato. Para ello, realizareos los siguientes procesos:

  1. load_image: cargamos una imagen y le damos un tamaño concreto.
  2. img_to_array: convertimos la imagen cargada en un array que considere el número de canales. En nuestro caso, al ser imágenes a color, tendremos tres canales, mientras que una imagen en blanco y negro tendría un único canal.
  3. expand_dims: agrupamos todas las imágenes en un único array, ya que, como hemos dicho, Keras trabaja con batches de imágenes. Así, el resultado de este paso será un array con dimensiones (3, ancho, alto, 3).
  4. preprocess_input: sustrae la media de los valores RGB del dataset Imagenet (con el cual está entrenado VGG19), de tal forma que consigamos que las imágenes tengan promedio cero. Esto es un preprocesamiento típico en las imágenes, ya que esto evita que los gradiente sean muy «extremos», consiguiendo por tanto mejores resultados del modelo (enlace).
  5. convert_to_tensor: por último vamos a convertir nuestro array ya centrado en un tipo de datos que entienda Tensorflow. Para eso, simplemente lo convertiremos a tensor con esta función.

Así pues, vamos a crear una función que realice, precisamente, el preprocesamiento que acabamos de explicar.

import keras
from tensorflow.keras.applications import vgg19
import numpy as np


def preprocess_image(image_path):
    # Util function to open, resize and format pictures into appropriate tensors
    img = keras.preprocessing.image.load_img(
        image_path, target_size=(img_nrows, img_ncols)
    )
    img = keras.preprocessing.image.img_to_array(img)
    img = np.expand_dims(img, axis=0)
    img = vgg19.preprocess_input(img)
    return tf.convert_to_tensor(img)

¡Preprocesamiento de las imágenes terminado! Acabamos de ver cómo convertir imágenes (arrays) en un tipo de datos que entienda nuestro modelo (tensores).

Ahora, vamos a ver exactamente lo contrario, cómo convertir el resultado del modelo (tensor) en una imagen que podamos visualizar. ¡Vamos a programar nuestro deprocesador de imágenes!

 Deprocesar imágenes

Como he comentado, para deprocesar las imágenes vamos a tener que seguir un proceso casi inverso al que hemos utilizado para procesar las imágenes. Para ello realizaremos los siguientes pasos:

  1. Convertir el tensor en un array que podamos utilizar.
  2. Hacer que los datos no tengan promedio cero. Para ello debemos sumar el promedio para cada uno de los canales del dataset Imagenet. Por suerte esto no es algo que haya que calcular, ya que lo podemos encontrar aquí. Además, nos aseguramos de que no haya valores por encima de 255 o por debajo de 0.
  3. Convertir las imágenes de BGR a RGB. Esto es debido a que históricamente se popularizó el uso del formato de BGR y por eso paquetes como OpenCV lee imágenes como BGR en vez de RGB.

Dicho esto, vamos a programar la decompresión de la Neural Style Transfer que estamos aprendiendo a programar en Python!

def deprocess_image(x):

    # Convertimos el tensor en Array
    x = x.reshape((img_nrows, img_ncols, 3))

    # Hacemos que no tengan promedio 0
    x[:, :, 0] += 103.939
    x[:, :, 1] += 116.779
    x[:, :, 2] += 123.68

    # Convertimos de BGR a RGB.
    x = x[:, :, ::-1]

    # Nos aseguramos que están entre 0 y 255
    x = np.clip(x, 0, 255).astype("uint8")

    return x

¡Deprocessing de imágenes terminado!

Ahora solo nos queda lo último: crear el loop de entrenamiento y entrenar a nuestra red. ¡Vamos a por ello!

Entrenamiento de nuestra red de Neural Style Transfer

Ahora que tenemos todos los ingredientes ya creados, crear el loop de entrenamiento es bastante sencillo. Simplemente hay que:

  1. Preprocesar las imágenes y crear la imágen de resultado.
  2. Iterativamente, calcular el coste y aplicar los gradientes.
  3. Cada ciertas iteraciones, mostrar el error y guardar la imágen generada y el modelo. En mi caso, como estoy entrenando a la red en Google Colab guardar el modelo es fundamental, ya que, sino nos arriesgamos a que nos desconecten del servidor durante el proceso de entrenamiento y tengamos que volver a empezar de cero.

Así pues, para que el entrenamiento quede más limpio, voy a crear una función que haga el tercer punto.

Guardar el modelo y las imágenes generadas

La función que vamos a crear es una función sencilla, que dado un modelo, genere una imagen y guarde tanto el modelo como la imagen. Vamos a hacerlo:

from datetime import datetime

def result_saver(iteration):
  # Create name
  now = datetime.now()
  now = now.strftime("%Y%m%d_%H%M%S")
  #model_name = str(i) + '_' + str(now)+"_model_" + '.h5'
  image_name = str(i) + '_' + str(now)+"_image" + '.png'

  # Save model and image
  img = deprocess_image(combination_image.numpy())
  keras.preprocessing.image.save_img(image_name, img)

Ahora que ya tenemos todo preparado, ¡vamos a programar el entrenamiento de nuestra Neural Style transfer hecha en Python!

from keras.optimizers import SGD

width, height = keras.preprocessing.image.load_img(base_image_path).size
img_nrows = 400
img_ncols = int(width * img_nrows / height)

optimizer = SGD(
    keras.optimizers.schedules.ExponentialDecay(
        initial_learning_rate=100.0, decay_steps=100, decay_rate=0.96
    )
)

base_image = preprocess_image(base_image_path)
style_reference_image = preprocess_image(style_image_path)
combination_image = tf.Variable(preprocess_image(base_image_path))

iterations = 4000

for i in range(1, iterations + 1):
    loss, grads = compute_loss_and_grads(
        combination_image, base_image, style_reference_image
    )
    optimizer.apply_gradients([(grads, combination_image)])
    if i % 10 == 0:
        print("Iteration %d: loss=%.2f" % (i, loss))
        result_saver(i)
Iteration 10: loss=21816.62
Iteration 20: loss=9596.32
Iteration 30: loss=6480.99
Iteration 40: loss=4951.99
Iteration 50: loss=4035.06
...
Iteration 3950: loss=283.40
Iteration 3960: loss=283.22
Iteration 3970: loss=283.05
Iteration 3980: loss=282.88
Iteration 3990: loss=282.71
Iteration 4000: loss=282.54

¡Ya tenemos nuestra imagen de Neural Style Transfer generada! Vamos a visualizar cómo ha sido la evolución y el resultado final:

Resultado de programar un algoritmo de Neural Style Transfer

Conclusión de programar una red de Neural Style Transfer

Como véis, programar una red neuronal de Neural Style Transfer en Pytho no es muy complicado (más allá de calcular los errores). En cualquier caso, esto no es todo, las redes de Neural Style Transfer no terminan aquí, ofrecen muchas más posibilidades, desde aplicación únicamente a una sección de una imagen usando máscaras a trasparasar también el color (mira este repositorio para encontrar inspiración).

En mi opinión, el único problema de los algoritmos de Neural Style Transfer es su puesta en producción. Y es que, el modelo se usa ad hoc para cada imágen. Por tanto, la implementación no suele ser tan sencilla como en el caso de un algoritmo tradicional.

Además, creo que a nivel de negocio no es un algoritmo que sea muy útil en la mayoría de negocios. En

En cualquier caso, espero que este haya resultado interesante, que hayas aprendido a programar tu propia red de Neural Style Transfer en Python y que te sea útil aunque sea para generar imágenes de regalo.

¡Nos vemos en el siguiente!

¡No te pierdas ningún post!

Si te gusta lo que lees... suscríbete para estar al día de los contenidos que subo.
¡Serás el primero en enterarte!