Cómo puedes entrenar a una IA para convertir tus maquetas de diseño en HTML y CSS

Dentro de tres años, el aprendizaje profundo cambiará el desarrollo front-end. Aumentará la velocidad de creación de prototipos y disminuirá la barrera para la creación de software.

El campo despegó el año pasado cuando Tony Beltramelli presentó el documento pix2code y Airbnb lanzó sketch2code.

Foto de Wesson Wang en Unsplash

Actualmente, la barrera más grande para automatizar el desarrollo front-end es la potencia informática. Sin embargo, podemos usar los algoritmos actuales de aprendizaje profundo, junto con datos de entrenamiento sintetizados, para comenzar a explorar la automatización frontal artificial ahora mismo.

En esta publicación, enseñaremos a una red neuronal cómo codificar un sitio web HTML y CSS básico en función de una imagen de una maqueta de diseño. Aquí hay una descripción rápida del proceso:

1) Dar una imagen de diseño a la red neuronal entrenada

2) La red neuronal convierte la imagen en marcado HTML

3) Salida renderizada

Construiremos la red neuronal en tres iteraciones.

En primer lugar, crearemos una versión mínima para controlar las partes móviles. La segunda versión, HTML, se centrará en automatizar todos los pasos y explicar las capas de la red neuronal. En la versión final, Bootstrap, crearemos un modelo que pueda generalizar y explorar la capa LSTM.

Todo el código está preparado en GitHub y FloydHub en cuadernos Jupyter. Todos los cuadernos FloydHub están dentro del directorio floydhub y los equivalentes locales están debajo de local.

Los modelos se basan en el papel pix2code de Beltramelli y los tutoriales de subtítulos de imágenes de Jason Brownlee. El código está escrito en Python y Keras, un marco encima de TensorFlow.

Si eres nuevo en el aprendizaje profundo, te recomiendo que tengas una idea de Python, la propagación hacia atrás y las redes neuronales convolucionales. Mis tres publicaciones anteriores en el blog de FloydHub lo ayudarán a comenzar:

  • Mi primer fin de semana de aprendizaje profundo
  • Codificación de la historia del aprendizaje profundo
  • Colorear fotos en blanco y negro con redes neuronales

Core Logic

Recapitulemos nuestro objetivo. Queremos construir una red neuronal que genere un marcado HTML / CSS que corresponda a una captura de pantalla.

Cuando entrenas la red neuronal, le das varias capturas de pantalla con HTML coincidente.

Aprende prediciendo todas las etiquetas de marcado HTML coincidentes una por una. Cuando predice la siguiente etiqueta de marcado, recibe la captura de pantalla y todas las etiquetas de marcado correctas hasta ese punto.

Aquí hay un ejemplo simple de datos de entrenamiento en una hoja de Google.

Crear un modelo que prediga palabra por palabra es el enfoque más común en la actualidad. Hay otros enfoques, pero ese es el método que usaremos a lo largo de este tutorial.

Tenga en cuenta que para cada predicción obtiene la misma captura de pantalla. Entonces, si tiene que predecir 20 palabras, obtendrá la misma maqueta de diseño veinte veces. Por ahora, no se preocupe por cómo funciona la red neuronal. Concéntrese en captar la entrada y salida de la red neuronal.

Centrémonos en el marcado anterior. Digamos que entrenamos a la red para predecir la oración "Puedo codificar". Cuando recibe "I", entonces predice "puedo". La próxima vez recibirá "Yo puedo" y predecirá "código". Recibe todas las palabras anteriores y solo tiene que predecir la siguiente palabra.

La red neuronal crea características a partir de los datos. La red crea funciones para vincular los datos de entrada con los datos de salida. Tiene que crear representaciones para comprender qué hay en cada captura de pantalla, la sintaxis HTML, que ha predicho. Esto construye el conocimiento para predecir la próxima etiqueta.

Cuando desee utilizar el modelo entrenado para el uso en el mundo real, es similar a cuando entrena el modelo. El texto se genera uno por uno con la misma captura de pantalla cada vez. En lugar de alimentarlo con las etiquetas HTML correctas, recibe el marcado que ha generado hasta ahora. Luego, predice la siguiente etiqueta de marcado. La predicción se inicia con una "etiqueta de inicio" y se detiene cuando predice una "etiqueta de finalización" o alcanza un límite máximo. Aquí hay otro ejemplo en una hoja de Google.

Versión "Hola Mundo"

Construyamos una versión de "hola mundo". Le daremos a una red neuronal una captura de pantalla con un sitio web que muestra "¡Hola mundo!" Y le enseñaremos a generar el marcado.

Primero, la red neuronal mapea la maqueta de diseño en una lista de valores de píxeles. De 0 a 255 en tres canales: rojo, azul y verde.

Para representar el marcado de una manera que la red neuronal comprende, utilizo una codificación activa. Por lo tanto, la oración "Puedo codificar" podría mapearse como se muestra a continuación.

En el gráfico anterior, incluimos la etiqueta de inicio y fin. Estas etiquetas son señales de cuándo la red comienza sus predicciones y cuándo detenerse.

Para los datos de entrada, usaremos oraciones, comenzando con la primera palabra y luego agregando cada palabra una por una. Los datos de salida son siempre una palabra.

Las oraciones siguen la misma lógica que las palabras. También necesitan la misma longitud de entrada. En lugar de estar limitados por el vocabulario, están obligados por la longitud máxima de la oración. Si es más corto que la longitud máxima, lo llena con palabras vacías, una palabra con solo ceros.

Como puede ver, las palabras se imprimen de derecha a izquierda. Esto obliga a cada palabra a cambiar de posición para cada ronda de entrenamiento. Esto permite que el modelo aprenda la secuencia en lugar de memorizar la posición de cada palabra.

En el gráfico a continuación hay cuatro predicciones. Cada fila es una predicción. A la izquierda están las imágenes representadas en sus tres canales de color: rojo, verde y azul y las palabras anteriores. Fuera de los corchetes están las predicciones una por una, que terminan con un cuadrado rojo para marcar el final.

bloques verdes = fichas de inicio | bloque rojo = token final
# Longitud de la oración más larga
max_caption_len = 3
# Tamaño del vocabulario
vocab_size = 3
# Cargue una captura de pantalla para cada palabra y conviértalas en dígitos
imágenes = []
para i en rango (2):
    images.append (img_to_array (load_img ('screenshot.jpg', target_size = (224, 224))))
images = np.array (images, dtype = float)
# Entrada de preproceso para el modelo VGG16
images = preprocess_input (imágenes)
# Convierta tokens de inicio en codificación de un solo hot
html_input = np.array (
            [[[0., 0., 0.], #start
             [0., 0., 0.],
             [1., 0., 0.]],
             [[0., 0., 0.], #start  Hello World! 
             [1., 0., 0.],
             [0., 1., 0.]]])
# Convierta la siguiente palabra en codificación de una sola vez
next_words = np.array (
            [[0., 1., 0.], #  ¡Hola, mundo! 
             [0., 0., 1.]]) # fin
# Cargue el modelo VGG16 entrenado en imagenet y envíe la función de clasificación
VGG = VGG16 (pesos = 'imagenet', include_top = True)
# Extrae las características de la imagen
características = VGG.predict (imágenes)
# Cargue la función en la red, aplique una capa densa y repita el vector
vgg_feature = Entrada (forma = (1000,))
vgg_feature_dense = Denso (5) (vgg_feature)
vgg_feature_repeat = RepeatVector (max_caption_len) (vgg_feature_dense)
# Extraer información de la secuencia de entrada
language_input = Input (shape = (vocab_size, vocab_size))
language_model = LSTM (5, return_sequences = True) (language_input)
# Concatenar la información de la imagen y la entrada
decoder = concatenate ([vgg_feature_repeat, language_model])
# Extraer información de la salida concatenada
decodificador = LSTM (5, return_sequences = False) (decodificador)
# Predecir qué palabra viene después
decoder_output = Dense (vocab_size, activación = 'softmax') (decodificador)
# Compila y ejecuta la red neuronal
modelo = Modelo (entradas = [vgg_feature, language_input], salidas = decoder_output)
model.compile (pérdida = 'categorical_crossentropy', optimizador = 'rmsprop')
# Entrena la red neuronal
model.fit ([features, html_input], next_words, batch_size = 2, shuffle = False, epochs = 1000)

En la versión hello world, usamos tres tokens: inicio,

Hello World!

y finalice. Una ficha puede ser cualquier cosa. Puede ser un personaje, palabra u oración. Las versiones de personajes requieren un vocabulario más pequeño pero limitan la red neuronal. Los tokens de nivel de palabra tienden a funcionar mejor.

Aquí hacemos la predicción:

# Cree una oración vacía e inserte el token de inicio
oración = np.zeros ((1, 3, 3)) # [[0,0,0], [0,0,0], [0,0,0]]
start_token = [1., 0., 0.] # start
oración [0] [2] = start_token # colocar inicio en oración vacía
    
# Hacer la primera predicción con el token de inicio
second_word = model.predict ([np.array ([características [1]]), oración])
    
# Ponga la segunda palabra en la oración y haga la predicción final
oración [0] [1] = inicio_token
oración [0] [2] = np.round (segunda palabra)
third_word = model.predict ([np.array ([características [1]]), oración])
    
# Coloque el token de inicio y nuestras dos predicciones en la oración
oración [0] [0] = inicio_token
oración [0] [1] = np.round (segunda palabra)
oración [0] [2] = np.round (tercera palabra)
    
# Transforma nuestras predicciones únicas en los tokens finales
vocabulary = ["start", " 

Hello World!

", "end"] para i en la oración [0]: print (vocabulario [np.argmax (i)], end = '')

Salida

  • 10 épocas: inicio inicio inicio
  • 100 épocas: inicie

    Hello World!

    Hello World!

  • 300 épocas: inicio

    Hello World!

    end

Errores que cometí:

  • Cree la primera versión de trabajo antes de recopilar los datos. Al principio de este proyecto, logré obtener una copia de un archivo antiguo del sitio web de alojamiento de Geocities. Tenía 38 millones de sitios web. Cegado por el potencial, ignoré la enorme carga de trabajo que se requeriría para reducir el vocabulario del tamaño de 100K.
  • Tratar con un valor de terabyte de datos requiere un buen hardware o mucha paciencia. Después de que mi Mac tuviera varios problemas, terminé usando un poderoso servidor remoto. Espere alquilar una plataforma con 8 núcleos de CPU modernos y una conexión a Internet de 1 GPS para tener un flujo de trabajo decente.
  • Nada tenía sentido hasta que entendí los datos de entrada y salida. La entrada, X, es una captura de pantalla y las etiquetas de marcado anteriores. La salida, Y, es la siguiente etiqueta de marcado. Cuando obtuve esto, se hizo más fácil entender todo entre ellos. También se hizo más fácil experimentar con diferentes arquitecturas.
  • Esté atento a las madrigueras del conejo. Debido a que este proyecto se cruza con muchos campos en el aprendizaje profundo, me quedé atrapado en muchos agujeros de conejo en el camino. Pasé una semana programando RNN desde cero, me fascinó demasiado incrustar espacios vectoriales y me sedujeron las implementaciones exóticas.
  • Las redes de imagen a código son modelos de subtítulos de imágenes disfrazados. Incluso cuando aprendí esto, aún ignoré muchos de los documentos de subtítulos de imágenes, simplemente porque eran menos geniales. Una vez que obtuve un poco de perspectiva, aceleré mi aprendizaje del espacio del problema.

Ejecutando el código en FloydHub

FloydHub es una plataforma de capacitación para el aprendizaje profundo. Me los encontré cuando comencé a aprender aprendizaje profundo y los he usado desde entonces para entrenar y administrar mis experimentos de aprendizaje profundo. Puede ejecutar su primer modelo en 30 segundos haciendo clic aquí.

Abre un espacio de trabajo en FloydHub donde encontrará el mismo entorno y conjunto de datos utilizados para la versión Bootstrap. También puede encontrar los modelos entrenados para las pruebas.

O puede hacer una instalación manual siguiendo estos pasos: instalación de 2 minutos o mi recorrido de 5 minutos.

Clonar el repositorio

git clone https://github.com/emilwallner/Screenshot-to-code-in-Keras.git

Inicie sesión e inicie la herramienta de línea de comandos FloydHub

cd Captura de pantalla para codificar en Keras
inicio de sesión floyd
floyd init s2c

Ejecute una computadora portátil Jupyter en una máquina de GPU en la nube FloydHub:

floyd run --gpu --env tensorflow-1.4 --data emilwallner / datasets / imagetocode / 2: data --mode jupyter

Todos los cuadernos se preparan dentro del directorio FloydHub. Los equivalentes locales están bajo locales. Una vez que se está ejecutando, puede encontrar el primer cuaderno aquí: floydhub / Helloworld / helloworld.ipynb.

Si desea instrucciones más detalladas y una explicación de las banderas, consulte mi publicación anterior.

Versión HTML

En esta versión, automatizaremos muchos de los pasos del modelo Hello World. Esta sección se enfocará en crear una implementación escalable y las piezas móviles en la red neuronal.

Esta versión no podrá predecir HTML a partir de sitios web aleatorios, pero sigue siendo una excelente configuración para explorar la dinámica del problema.

Visión general

Si expandimos los componentes del gráfico anterior, se ve así.

Hay dos secciones principales. Primero, el codificador. Aquí es donde creamos características de imagen y características de marcado anteriores. Las características son los componentes básicos que crea la red para conectar las maquetas de diseño con el marcado. Al final del codificador, pegamos las características de la imagen a cada palabra en el marcado anterior.

El decodificador toma la característica combinada de diseño y marcado y crea una próxima característica de etiqueta. Esta característica se ejecuta a través de una red neuronal completamente conectada para predecir la siguiente etiqueta.

Características de maquetas de diseño

Como necesitamos insertar una captura de pantalla para cada palabra, esto se convierte en un cuello de botella cuando se entrena la red (ejemplo). En lugar de usar las imágenes, extraemos la información que necesitamos para generar el marcado.

La información está codificada en características de imagen. Esto se realiza mediante el uso de una red neuronal convolucional (CNN) ya preformada. El modelo está pre-entrenado en Imagenet.

Extraemos las características de la capa antes de la clasificación final.

Terminamos con 1536 imágenes de ocho por ocho píxeles conocidas como características. Aunque son difíciles de entender para nosotros, una red neuronal puede extraer los objetos y la posición de los elementos de estas características.

Características de marcado

En la versión de hello world, utilizamos una codificación única para representar el marcado. En esta versión, usaremos una incrustación de palabras para la entrada y mantendremos la codificación única para la salida.

La forma en que estructuramos cada oración permanece igual, pero la forma en que mapeamos cada token cambia. La codificación única trata cada palabra como una unidad aislada. En cambio, convertimos cada palabra en los datos de entrada a listas de dígitos. Estos representan la relación entre las etiquetas de marcado.

La dimensión de esta palabra incrustada es de ocho, pero a menudo varía entre 50 y 500 dependiendo del tamaño del vocabulario.

Los ocho dígitos para cada palabra son pesos similares a una red neuronal vainilla. Están sintonizados para mapear cómo se relacionan las palabras entre sí (Mikolov et al., 2013).

Así es como comenzamos a desarrollar características de marcado. Las características son las que desarrolla la red neuronal para vincular los datos de entrada con los datos de salida. Por ahora, no se preocupe por lo que son, profundizaremos en esto en la siguiente sección.

El codificador

Tomaremos las incrustaciones de palabras y las ejecutaremos a través de un LSTM y devolveremos una secuencia de características de marcado. Estos se ejecutan a través de una capa densa distribuida en el tiempo: piense en ella como una capa densa con múltiples entradas y salidas.

En paralelo, las características de la imagen se aplanan primero. Independientemente de cómo se estructuraron los dígitos, se transforman en una gran lista de números. Luego aplicamos una capa densa en esta capa para formar una entidad de alto nivel. Estas características de imagen se concatenan a las características de marcado.

Esto puede ser difícil de entender, así que vamos a analizarlo.

Características de marcado

Aquí ejecutamos las incrustaciones de palabras a través de la capa LSTM. En este gráfico, todas las oraciones se rellenan para alcanzar el tamaño máximo de tres tokens.

Para mezclar señales y encontrar patrones de nivel superior, aplicamos una capa densa distribuida en el tiempo a las características de marcado. TimeDistributed denso es lo mismo que una capa densa, pero con múltiples entradas y salidas.

Características de la imagen

En paralelo, preparamos las imágenes. Tomamos todas las funciones de mini imágenes y las transformamos en una larga lista. La información no cambia, solo se reorganiza.

Nuevamente, para mezclar señales y extraer nociones de nivel superior, aplicamos una capa densa. Como solo estamos tratando con un valor de entrada, podemos usar una capa densa normal. Para conectar las funciones de imagen a las funciones de marcado, copiamos las funciones de imagen.

En este caso, tenemos tres características de marcado. Por lo tanto, terminamos con una cantidad igual de características de imagen y características de marcado.

Concatenando la imagen y las características de marcado

Todas las oraciones se rellenan para crear tres características de marcado. Como hemos preparado las funciones de imagen, ahora podemos agregar una función de imagen para cada función de marcado.

Después de pegar una función de imagen en cada función de marcado, terminamos con tres funciones de marcado de imagen. Esta es la entrada que alimentamos al decodificador.

El decodificador

Aquí usamos las características combinadas de marcado de imágenes para predecir la próxima etiqueta.

En el siguiente ejemplo, utilizamos tres pares de características de marcado de imagen y generamos una próxima característica de etiqueta.

Tenga en cuenta que la capa LSTM tiene la secuencia establecida en falso. En lugar de devolver la longitud de la secuencia de entrada, solo predice una característica. En nuestro caso, es una función para la próxima etiqueta. Contiene la información para la predicción final.

La predicción final

La capa densa funciona como una red neuronal tradicional de avance. Conecta los 512 dígitos en la siguiente función de etiqueta con las 4 predicciones finales. Digamos que tenemos 4 palabras en nuestro vocabulario: inicio, hola, mundo y fin.

La predicción del vocabulario podría ser [0.1, 0.1, 0.1, 0.7]. La activación de softmax en la capa densa distribuye una probabilidad de 0 a 1, con la suma de todas las predicciones igual a 1. En este caso, predice que la cuarta palabra es la siguiente etiqueta. Luego traduce la codificación de hot-one [0, 0, 0, 1] en el valor mapeado, diga "fin".

# Cargue las imágenes y preproceselas para el inicio-resnet
imágenes = []
all_filenames = listdir ('imágenes /')
all_filenames.sort ()
para el nombre de archivo en all_filenames:
    images.append (img_to_array (load_img ('images /' + filename, target_size = (299, 299))))
images = np.array (images, dtype = float)
images = preprocess_input (imágenes)
# Ejecute las imágenes a través de inception-resnet y extraiga las características sin la capa de clasificación
IR2 = InceptionResNetV2 (pesos = 'imagenet', include_top = False)
características = IR2.predict (imágenes)
# Limitaremos cada secuencia de entrada a 100 tokens
max_caption_len = 100
# Inicializar la función que creará nuestro vocabulario.
tokenizer = Tokenizer (filtros = '', split = "", lower = False)
# Leer un documento y devolver una cadena
def load_doc (nombre de archivo):
    archivo = abierto (nombre de archivo, 'r')
    text = file.read ()
    file.close ()
    texto de retorno
# Cargue todos los archivos HTML
X = []
all_filenames = listdir ('html /')
all_filenames.sort ()
para el nombre de archivo en all_filenames:
    X.append (load_doc ('html /' + filename))
# Crear el vocabulario de los archivos html
tokenizer.fit_on_texts (X)
# Agregue +1 para dejar espacio para palabras vacías
vocab_size = len (tokenizer.word_index) + 1
# Traducir cada palabra del archivo de texto al índice de vocabulario correspondiente
secuencias = tokenizer.texts_to_sequences (X)
# El archivo HTML más largo
max_length = max (len (s) para s en secuencias)
# Inicializar nuestra entrada final al modelo
X, y, image_data = list (), list (), list ()
para img_no, seq en enumerate (secuencias):
    para i en rango (1, len (seq)):
        # Agregue la secuencia completa a la entrada y solo mantenga la siguiente palabra para la salida
        in_seq, out_seq = seq [: i], seq [i]
        # Si la oración es más corta que max_length, complétela con palabras vacías
        in_seq = pad_sequences ([in_seq], maxlen = max_length) [0]
        # Asigna la salida a la codificación de uno en caliente
        out_seq = to_categorical ([out_seq], num_classes = vocab_size) [0]
        # Agregar e imagen correspondiente al archivo HTML
        image_data.append (características [img_no])
        # Corta la oración de entrada a 100 tokens y agrégala a los datos de entrada
        X.append (en_seq [-100:])
        y.append (out_seq)
X, y, image_data = np.array (X), np.array (y), np.array (image_data)
# Crear el codificador
image_features = Input (forma = (8, 8, 1536,))
image_flat = Flatten () (image_features)
image_flat = Denso (128, activación = 'relu') (image_flat)
ir2_out = RepeatVector (max_caption_len) (image_flat)
language_input = Input (shape = (max_caption_len,))
language_model = Incrustar (vocab_size, 200, input_length = max_caption_len) (language_input)
language_model = LSTM (256, return_sequences = True) (language_model)
language_model = LSTM (256, return_sequences = True) (language_model)
language_model = TimeDistributed (Denso (128, activación = 'relu')) (language_model)
# Crear el decodificador
decodificador = concatenar ([ir2_out, language_model])
decodificador = LSTM (512, return_sequences = False) (decodificador)
decoder_output = Dense (vocab_size, activación = 'softmax') (decodificador)
# Compila el modelo
modelo = Modelo (entradas = [características de imagen, entrada de idioma], salidas = salida de decodificador)
model.compile (pérdida = 'categorical_crossentropy', optimizador = 'rmsprop')
# Entrena la red neuronal
model.fit ([image_data, X], y, batch_size = 64, shuffle = False, epochs = 2)
# mapear un entero a una palabra
def word_for_id (entero, tokenizer):
    para Word, indice en tokenizer.word_index.items ():
        si index == entero:
            palabra de retorno
    regresar Ninguno
# generar una descripción para una imagen
def generate_desc (modelo, tokenizer, foto, max_length):
    # sembrar el proceso de generación
    in_text = 'START'
    # iterar a lo largo de toda la secuencia
    para i en rango (900):
        # secuencia de entrada de codificación de enteros
        secuencia = tokenizer.texts_to_sequences ([en_texto]) [0] [- 100:]
        # entrada de pad
        secuencia = pad_sequences ([secuencia], maxlen = max_length)
        # predecir la siguiente palabra
        yhat = model.predict ([foto, secuencia], detallado = 0)
        # convertir probabilidad a entero
        yhat = np.argmax (yhat)
        # mapa entero a palabra
        word = word_for_id (yhat, tokenizer)
        # detener si no podemos mapear la palabra
        si la palabra es Ninguna:
            rotura
        # agregar como entrada para generar la siguiente palabra
        in_text + = '' + palabra
        # Imprime la predicción
        print ('' + palabra, fin = '')
        # detener si predecimos el final de la secuencia
        si la palabra == 'FIN':
            rotura
    regreso
# Carga e imagen, preprocese para IR2, extraiga funciones y genere el HTML
test_image = img_to_array (load_img ('images / 87.jpg', target_size = (299, 299)))
test_image = np.array (test_image, dtype = float)
test_image = preprocess_input (test_image)
test_features = IR2.predict (np.array ([test_image]))
generate_desc (model, tokenizer, np.array (test_features), 100)

Salida

Enlaces a sitios web generados

  • 250 épocas
  • 350 épocas
  • 450 épocas
  • 550 épocas

Si no puede ver nada cuando hace clic en estos enlaces, puede hacer clic derecho y hacer clic en "Ver código fuente de la página". Aquí está el sitio web original para referencia.

Errores que cometí:

  • Los LSTM son mucho más pesados ​​para mi cognición en comparación con los CNN. Cuando desenrollé todos los LSTM, se volvieron más fáciles de entender. El video de Fast.ai en RNN fue muy útil. Además, concéntrese en las características de entrada y salida antes de intentar comprender cómo funcionan.
  • Construir un vocabulario desde cero es mucho más fácil que reducir un vocabulario enorme. Esto incluye todo, desde fuentes, tamaños div y colores hexadecimales hasta nombres de variables y palabras normales.
  • La mayoría de las bibliotecas se crean para analizar documentos de texto y no para codificar. En los documentos, todo está separado por un espacio, pero en el código, necesita un análisis personalizado.
  • Puede extraer funciones con un modelo capacitado en Imagenet. Esto puede parecer contradictorio ya que Imagenet tiene pocas imágenes web. Sin embargo, la pérdida es un 30% mayor en comparación con un modelo pix2code, que está entrenado desde cero. Sería interesante utilizar un modelo de modelo de inicio-entrenamiento previo al entrenamiento basado en capturas de pantalla web.

Versión Bootstrap

En nuestra versión final, utilizaremos un conjunto de datos de sitios web de arranque generados a partir del documento pix2code. Mediante el uso de bootstrap de Twitter, podemos combinar HTML y CSS y disminuir el tamaño del vocabulario.

Le permitiremos generar el marcado para una captura de pantalla que no ha visto antes. También profundizaremos en cómo genera conocimiento sobre la captura de pantalla y el marcado.

En lugar de entrenarlo en el marcado de arranque, usaremos 17 tokens simplificados que luego traduciremos a HTML y CSS. El conjunto de datos incluye 1500 capturas de pantalla de prueba y 250 imágenes de validación. Para cada captura de pantalla hay un promedio de 65 tokens, lo que resulta en 96925 ejemplos de entrenamiento.

Al ajustar el modelo en el documento pix2code, el modelo puede predecir los componentes web con un 97% de precisión (BLEU 4-ngram búsqueda codiciosa, más sobre esto más adelante).

Un enfoque de extremo a extremo

La extracción de características de modelos previamente entrenados funciona bien en modelos de subtítulos de imágenes. Pero después de algunos experimentos, me di cuenta de que el enfoque de extremo a extremo de pix2code funciona mejor para este problema. Los modelos pre-entrenados no han sido entrenados en datos web y están personalizados para su clasificación.

En este modelo, reemplazamos las características de imagen pre-entrenadas con una red neuronal convolucional ligera. En lugar de utilizar la agrupación máxima para aumentar la densidad de información, aumentamos los avances. Esto mantiene la posición y el color de los elementos frontales.

Hay dos modelos principales que permiten esto: redes neuronales convolucionales (CNN) y redes neuronales recurrentes (RNN). La red neuronal recurrente más común es la memoria a corto y largo plazo (LSTM), así que a eso me referiré.

Hay muchos tutoriales excelentes de CNN, y los cubrí en mi artículo anterior. Aquí, me enfocaré en los LSTM.

Comprender los pasos de tiempo en LSTM

Una de las cosas más difíciles de comprender acerca de los LSTM es el paso del tiempo. Una red neuronal vainilla puede considerarse como dos pasos de tiempo. Si le dices "Hola", predice "Mundo". Pero sería difícil predecir más pasos de tiempo. En el siguiente ejemplo, la entrada tiene cuatro pasos de tiempo, uno para cada palabra.

Los LSTM están hechos para entrada con pasos de tiempo. Es una red neuronal personalizada para obtener información en orden. Si desenrolla nuestro modelo, se ve así. Para cada paso descendente, mantienes los mismos pesos. Aplica un conjunto de pesos a la salida anterior y otro conjunto a la nueva entrada.

La entrada y salida ponderada se concatenan y se agregan junto con una activación. Esta es la salida para ese paso de tiempo. Como reutilizamos los pesos, obtienen información de varias entradas y generan conocimiento de la secuencia.

Aquí hay una versión simplificada del proceso para cada paso de tiempo en un LSTM.

Para tener una idea de esta lógica, recomendaría construir un RNN desde cero con el brillante tutorial de Andrew Trask.

Comprensión de las unidades en capas LSTM

El número de unidades en cada capa LSTM determina su capacidad de memorizar. Esto también corresponde al tamaño de cada función de salida. Una vez más, una característica es una larga lista de números utilizados para transferir información entre capas.

Cada unidad en la capa LSTM aprende a realizar un seguimiento de diferentes aspectos de la sintaxis. A continuación se muestra una visualización de una unidad que realiza un seguimiento de la información en la fila div. Este es el marcado simplificado que estamos usando para entrenar el modelo bootstrap.

Cada unidad LSTM mantiene un estado celular. Piense en el estado celular como la memoria. Los pesos y activaciones se utilizan para modificar el estado de diferentes maneras. Esto permite que las capas LSTM afinen qué información guardar y descartar para cada entrada.

Además de pasar por una función de salida para cada entrada, también reenvía los estados de celda, un valor para cada unidad en el LSTM. Para tener una idea de cómo interactúan los componentes dentro del LSTM, recomiendo el tutorial de Colah, la implementación de Jayasiri Numpy y la lectura y redacción de Karphay.

dir_name = 'recursos / eval_light /'
# Leer un archivo y devolver una cadena
def load_doc (nombre de archivo):
    archivo = abierto (nombre de archivo, 'r')
    text = file.read ()
    file.close ()
    texto de retorno
def load_data (data_dir):
    texto = []
    imágenes = []
    # Cargue todos los archivos y ordénelos
    all_filenames = listdir (data_dir)
    all_filenames.sort ()
    para el nombre de archivo en (all_filenames):
        if nombre de archivo [-3:] == "npz":
            # Cargue las imágenes ya preparadas en matrices
            image = np.load (data_dir + filename)
            images.append (image ['características'])
        más:
            # Cargue las fichas boostrap y rapee en una etiqueta de inicio y fin
            sintaxis = '' + load_doc (data_dir + filename) + ''
            # Separa todas las palabras con un solo espacio
            sintaxis = '' .join (syntax.split ())
            # Agregar un espacio después de cada coma
            sintaxis = sintaxis.replace (',', ',')
            text.append (sintaxis)
    images = np.array (images, dtype = float)
    devolver imágenes, texto
características del tren, textos = load_data (dir_name)
# Inicializa la función para crear el vocabulario
tokenizer = Tokenizer (filtros = '', split = "", lower = False)
# Crear el vocabulario
tokenizer.fit_on_texts ([load_doc ('bootstrap.vocab')])
# Agregue un lugar para la palabra vacía en el vocabulario
vocab_size = len (tokenizer.word_index) + 1
# Mapear las oraciones de entrada en los índices de vocabulario
train_sequences = tokenizer.texts_to_sequences (textos)
# El conjunto más largo de tokens boostrap
max_sequence = max (len (s) para s en train_sequences)
# Especifique cuántas fichas tener en cada oración de entrada
max_length = 48
def preprocess_data (secuencias, características):
    X, y, image_data = list (), list (), list ()
    para img_no, seq en enumerate (secuencias):
        para i en rango (1, len (seq)):
            # Agregue la oración hasta el conteo actual (i) y agregue el conteo actual a la salida
            in_seq, out_seq = seq [: i], seq [i]
            # Rellene todas las oraciones de token de entrada a max_sequence
            in_seq = pad_sequences ([in_seq], maxlen = max_sequence) [0]
            # Convierta la salida en codificación one-hot
            out_seq = to_categorical ([out_seq], num_classes = vocab_size) [0]
            # Agregue la imagen correspondiente al archivo de token boostrap
            image_data.append (características [img_no])
            # Limita la frase de entrada a 48 tokens y agrégala
            X.append (en_seq [-48:])
            y.append (out_seq)
    return np.array (X), np.array (y), np.array (image_data)
X, y, image_data = preprocess_data (train_sequences, train_features)
# Crear el codificador
image_model = Sequential ()
image_model.add (Conv2D (16, (3, 3), padding = 'válido', activación = 'relu', input_shape = (256, 256, 3,)))
image_model.add (Conv2D (16, (3,3), activación = 'relu', relleno = 'igual', zancadas = 2))
image_model.add (Conv2D (32, (3,3), activación = 'relu', relleno = 'mismo'))
image_model.add (Conv2D (32, (3,3), activación = 'relu', relleno = 'igual', zancadas = 2))
image_model.add (Conv2D (64, (3,3), activación = 'relu', relleno = 'igual'))
image_model.add (Conv2D (64, (3,3), activación = 'relu', relleno = 'igual', zancadas = 2))
image_model.add (Conv2D (128, (3,3), activación = 'relu', relleno = 'igual'))
image_model.add (Flatten ())
image_model.add (Denso (1024, activación = 'relu'))
image_model.add (Dropout (0.3))
image_model.add (Denso (1024, activación = 'relu'))
image_model.add (Dropout (0.3))
image_model.add (RepeatVector (max_length))
visual_input = Input (shape = (256, 256, 3,))
encoded_image = image_model (visual_input)
language_input = Input (shape = (max_length,))
language_model = Incrustar (vocab_size, 50, input_length = max_length, mask_zero = True) (language_input)
language_model = LSTM (128, return_sequences = True) (language_model)
language_model = LSTM (128, return_sequences = True) (language_model)
# Crear el decodificador
decoder = concatenate ([encoded_image, language_model])
decodificador = LSTM (512, return_sequences = True) (decodificador)
decodificador = LSTM (512, return_sequences = False) (decodificador)
decodificador = Denso (vocab_size, activación = 'softmax') (decodificador)
# Compila el modelo
modelo = Modelo (entradas = [visual_input, language_input], salidas = decodificador)
optimizador = RMSprop (lr = 0.0001, clipvalue = 1.0)
model.compile (pérdida = 'categorical_crossentropy', optimizador = optimizador)
#Guarde el modelo para cada 2a época
filepath = "org-weights-epoch- {epoch: 04d} - val_loss- {val_loss: .4f} - loss- {loss: .4f} .hdf5"
checkpoint = ModelCheckpoint (filepath, monitor = 'val_loss', verbose = 1, save_weights_only = True, punto = 2)
callbacks_list = [punto de control]
# Entrena al modelo
model.fit ([image_data, X], y, batch_size = 64, shuffle = False, validation_split = 0.1, callbacks = callbacks_list, verbose = 1, epochs = 50)

Exactitud de prueba

Es difícil encontrar una manera justa de medir la precisión. Digamos que comparas palabra por palabra. Si su predicción no está sincronizada con una palabra, es posible que tenga una precisión del 0%. Si elimina una palabra que sincroniza la predicción, podría terminar con 99/100.

Utilicé el puntaje BLEU, la mejor práctica en traducción automática y modelos de subtítulos de imágenes. Divide la oración en cuatro n-gramos, de secuencias de 1 a 4 palabras. En la siguiente predicción, se supone que "gato" es "código".

Para obtener el puntaje final, multiplique cada puntaje con 25%, (4/5) * 0.25 + (2/4) * 0.25 + (1/3) * 0.25 + (0/2) * 0.25 = 0.2 + 0.125 + 0.083 + 0 = 0.408. La suma se multiplica con una pena de longitud de la oración. Como la longitud es correcta en nuestro ejemplo, se convierte en nuestro puntaje final.

Puede aumentar la cantidad de n-gramos para que sea más difícil. Un modelo de cuatro n-gramas es el modelo que mejor corresponde a las traducciones humanas. Recomiendo ejecutar algunos ejemplos con el siguiente código y leer la página wiki.

# Crear una función para leer un archivo y devolver su contenido
def load_doc (nombre de archivo):
    archivo = abierto (nombre de archivo, 'r')
    text = file.read ()
    file.close ()
    texto de retorno
def load_data (data_dir):
    texto = []
    imágenes = []
    files_in_folder = os.listdir (data_dir)
    files_in_folder.sort ()
    para el nombre de archivo en tqdm (files_in_folder):
        #Agregar una imagen
        if nombre de archivo [-3:] == "npz":
            image = np.load (data_dir + filename)
            images.append (image ['características'])
        más:
        # Agregue texto y envuélvalo en una etiqueta de inicio y fin
            sintaxis = '' + load_doc (data_dir + filename) + ''
            # Separe cada palabra con un espacio
            sintaxis = '' .join (syntax.split ())
            # Agregue un espacio entre cada coma
            sintaxis = sintaxis.replace (',', ',')
            text.append (sintaxis)
    images = np.array (images, dtype = float)
    devolver imágenes, texto
#Intialize la función para crear el vocabulario
tokenizer = Tokenizer (filtros = '', split = "", lower = False)
#Cree el vocabulario en un orden específico
tokenizer.fit_on_texts ([load_doc ('bootstrap.vocab')])
dir_name = '../../../../eval/'
características del tren, textos = load_data (dir_name)
# cargar modelo y pesos
json_file = open ('../../../../ model.json', 'r')
loaded_model_json = json_file.read ()
json_file.close ()
loaded_model = model_from_json (loaded_model_json)
# cargar pesos en el nuevo modelo
loaded_model.load_weights ("../../../../ weights.hdf5")
print ("Modelo cargado desde el disco")
# mapear un entero a una palabra
def word_for_id (entero, tokenizer):
    para Word, indice en tokenizer.word_index.items ():
        si index == entero:
            palabra de retorno
    regresar Ninguno
print (word_for_id (17, tokenizer))
# generar una descripción para una imagen
def generate_desc (modelo, tokenizer, foto, max_length):
    photo = np.array ([foto])
    # sembrar el proceso de generación
    in_text = ''
    # iterar a lo largo de toda la secuencia
    print ('\ nPrediction ----> \ n \ n ', end = '')
    para i en rango (150):
        # secuencia de entrada de codificación de enteros
        secuencia = tokenizer.texts_to_sequences ([en_texto]) [0]
        # entrada de pad
        secuencia = pad_sequences ([secuencia], maxlen = max_length)
        # predecir la siguiente palabra
        yhat = loaded_model.predict ([foto, secuencia], detallado = 0)
        # convertir probabilidad a entero
        yhat = argmax (yhat)
        # mapa entero a palabra
        word = word_for_id (yhat, tokenizer)
        # detener si no podemos mapear la palabra
        si la palabra es Ninguna:
            rotura
        # agregar como entrada para generar la siguiente palabra
        in_text + = word + ''
        # detener si predecimos el final de la secuencia
        print (palabra + '', fin = '')
        si la palabra == '':
            rotura
    volver en_texto
max_length = 48
# evaluar la habilidad del modelo
def eval_model (modelo, descripciones, fotos, tokenizer, max_length):
    real, predicho = lista (), lista ()
    # paso por todo el conjunto
    para i en rango (len (textos)):
        yhat = generate_desc (modelo, tokenizer, fotos [i], max_length)
        # tienda real y prevista
        print ('\ n \ nReal ----> \ n \ n' + textos [i])
        actual.append ([textos [i] .split ()])
        predicted.append (yhat.split ())
    # calcular puntaje BLEU
    bleu = corpus_bleu (real, predicho)
    return bleu, real, predicho
bleu, real, predicho = evaluar_modelo (loaded_model, textos, características del tren, tokenizer, max_length)
# Compila los tokens en HTML y CSS
dsl_path = "compilador / assets / web-dsl-mapping.json"
compilador = Compilador (dsl_path)
compiled_website = compiler.compile (predicho [0], 'index.html')
print (sitio web compilado)
imprimir (bleu)

Salida

Enlaces a resultados de muestra

  • Sitio web generado 1 - Original 1
  • Sitio web generado 2 - Original 2
  • Sitio web generado 3 - Original 3
  • Sitio web generado 4 - Original 4
  • Sitio web generado 5 - Original 5

Errores que cometí:

  • Comprenda la debilidad de los modelos en lugar de probar modelos aleatorios. Primero, apliqué cosas aleatorias como la normalización por lotes y las redes bidireccionales e intenté implementar la atención. Después de mirar los datos de prueba y ver que no podía predecir el color y la posición con alta precisión, me di cuenta de que había una debilidad en la CNN. Esto me llevó a reemplazar maxpooling con zancadas aumentadas. La pérdida de validación pasó de 0,12 a 0,02 y aumentó la puntuación BLEU del 85% al ​​97%.
  • Solo use modelos pre-entrenados si son relevantes. Dado el pequeño conjunto de datos, pensé que un modelo de imagen previamente entrenado mejoraría el rendimiento. De mis experimentos, y el modelo de extremo a extremo es más lento para entrenar y requiere más memoria, pero es un 30% más preciso.
  • Planifique una ligera variación cuando ejecute su modelo en un servidor remoto. En mi Mac, lee los archivos en orden alfabético. Sin embargo, en el servidor, se ubicó al azar. Esto creó una falta de coincidencia entre las capturas de pantalla y el código. Todavía convergía, pero los datos de validación eran un 50% peores que cuando lo arreglé.
  • Asegúrese de comprender las funciones de la biblioteca. Incluya espacio para la ficha vacía en su vocabulario. Cuando no lo agregué, no incluía uno de los tokens. Solo lo noté después de mirar el resultado final varias veces y notar que nunca predijo un token "único". Después de una revisión rápida, me di cuenta de que ni siquiera estaba en el vocabulario. Además, use el mismo orden en el vocabulario para el entrenamiento y las pruebas.
  • Use modelos más ligeros cuando experimente. El uso de GRU en lugar de LSTM redujo cada ciclo de época en un 30% y no tuvo un gran efecto en el rendimiento.

Próximos pasos

El desarrollo front-end es un espacio ideal para aplicar el aprendizaje profundo. Es fácil generar datos, y los algoritmos actuales de aprendizaje profundo pueden mapear la mayor parte de la lógica.

Una de las áreas más interesantes es prestar atención a los LSTM. Esto no solo mejorará la precisión, sino que nos permitirá visualizar dónde se centra CNN mientras genera el marcado.

La atención también es clave para la comunicación entre el marcado, las hojas de estilo, los scripts y, finalmente, el backend. Las capas de atención pueden realizar un seguimiento de las variables, permitiendo que la red se comunique entre lenguajes de programación.

Pero en la característica cercana, el mayor impacto vendrá de construir una forma escalable para sintetizar datos. Luego, puede agregar fuentes, colores, palabras y animaciones paso a paso.

Hasta ahora, la mayoría del progreso está sucediendo al tomar bocetos y convertirlos en aplicaciones de plantilla. En menos de dos años, podremos dibujar una aplicación en papel y tener el front-end correspondiente en menos de un segundo. Ya hay dos prototipos funcionales construidos por el equipo de diseño de Airbnb y Uizard.

Aquí hay algunos experimentos para comenzar.

Los experimentos

Empezando

  • Ejecuta todos los modelos
  • Pruebe diferentes hiperparámetros
  • Probar una arquitectura CNN diferente
  • Agregar modelos bidireccionales LSTM
  • Implemente el modelo con un conjunto de datos diferente. (Puede montar fácilmente este conjunto de datos en sus trabajos de FloydHub con este indicador --data emilwallner / datasets / 100k-html: data)

Más experimentos

  • Creación de una aplicación aleatoria sólida / generador web con la sintaxis correspondiente.
  • Datos para un boceto al modelo de aplicación. Convierta automáticamente las capturas de pantalla de la aplicación / web en bocetos y use una GAN para crear variedad.
  • Aplique una capa de atención para visualizar el enfoque en la imagen para cada predicción, similar a este modelo.
  • Cree un marco para un enfoque modular. Digamos, tener modelos de codificador para fuentes, uno para color, otro para diseño y combinarlos con un decodificador. Un buen comienzo podría ser una imagen sólida.
  • Alimente a la red con componentes HTML simples y enséñele a generar animaciones usando CSS. Sería fascinante tener un enfoque de atención y visualizar el enfoque en ambas fuentes de entrada.

Muchas gracias a Tony Beltramelli y Jon Gold por sus investigaciones e ideas, y por responder preguntas. Gracias a Jason Brownlee por sus tutoriales estelares de Keras (incluí algunos fragmentos de su tutorial en la implementación central de Keras) y a Beltramelli por proporcionar los datos. También gracias a Qingping Hou, Charlie Harrington, Sai Soundararaj, Jannes Klaas, Claudio Cabral, Alain Demenet y Dylan Djian por leer los borradores de esto.

Sobre Emil Wallner

Esta es la cuarta parte de una serie de blogs de varias partes de Emil mientras aprende el aprendizaje profundo. Emil ha pasado una década explorando el aprendizaje humano. Trabajó para la escuela de negocios de Oxford, invirtió en nuevas empresas educativas y creó un negocio de tecnología educativa. El año pasado, se inscribió en Ecole 42 para aplicar su conocimiento del aprendizaje humano al aprendizaje automático.

Si construyes algo o te quedas atascado, envíame un ping a continuación o en twitter: emilwallner. Me encantaría ver lo que estás construyendo.

Esto se publicó por primera vez como una publicación de la comunidad en el blog de Floydhub.