Как научить ИИ превращать дизайн-макеты в HTML/CSS. Часть 2

NOP
NOP::Nuances of Programming
8 min readJul 14, 2018

Перевод статьи Emil Wallner: How you can train an AI to convert your design mockups into HTML and CSS

Предыдущие части: Часть 1

В первой части статьи мы рассмотрели общую схему обучения нейронной сети и создали первую итерацию с версией Hello World.

Во второй части статьи мы перейдем ко второму шагу и создадим версию HTML

Выполнение кода на FloydHub

FloydHub – это обучающая платформа для глубокого изучения. Я наткнулся на нее практически сразу и активно использовал для обучения сетей и управления экспериментами. Можете установить ее и запустить свою первую модель уже через 10 минут. Пожалуй, из всего имеющегося инструментария – это лучшая платформа для запуска моделей на облачной графике.

Если вы еще не знакомы с FloydHub, то посмотрите их ролики по 2-минутной установке или мой 5-минутный гид по работе.

Создание копии репозитория

Вот здесь: https://github.com/emilwallner/Screenshot-to-code-in-Keras.git

Авторизация и инициирование инструмента командной строки FloydHub

cd Screenshot-to-code-in-Keras
floyd login
floyd init s2c

Запуск блокнота Jupyter на облачном графическом процессоре FloydHub:

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

Все блокноты уже готовы к работе внутри директории FloydHub. Локальные копии добавляются в папку local. Свой первый блокнот вы сможете найти сразу после запуска вот здесь: floydhub/Helloworld/helloworld.ipynb .

Если вам нужны более подробные инструкции и пояснения, то почитайте вот это.

Версия HTML

В данной версии мы автоматизируем большую часть шагов из модели Hello World, а также создадим масштабируемую реализацию и подвижные части нейронной сети.

Мы не сможем предугадать HTML со случайных сайтов, но эта модель идеально подойдет для изучения нужной динамики.

Общее представление

Если мы развернем компоненты предыдущего рисунка, то получим следующее:

У нас имеются два основных раздела. Первый – энкодер. Там мы создаем признаки изображения и предыдущей разметки. Признаки – это главные элементы, которые создаются сетью для определения связей между макетом и разметкой. В конце кодирования мы привязываем признаки изображения к каждому слову предыдущей разметки.

Затем декодер берет объединенные признаки макета и разметки и создает признак следующего тега. Данная функция запускается через полностью подключенную нейронную сеть и выполняет предсказание следующего тега.

Признаки дизайн-макета

Самое трудное при обучении сети (пример) – это добавлять по одному скриншоту на каждое слово. Вместо использования самих изображений мы извлекаем информацию, которая потребуется для создания разметки.

Такая информация кодируется в признаки изображения. Делается это с использованием предобученной сверточной нейронной сети (СНС). Данная модель заранее тренируется на Imagenet.

Перед окончательной классификацией мы извлекаем признаки из слоя.

В итоге получаются 1536 изображений 8х8 пикселей – именно они и являются признаками. И хотя данная методика крайне тяжела для нашего понимания, нейронная сеть умеет находить через нее объекты и их расположение.

Признаки разметки

В версии с Hello world мы делали представление разметки с помощью прямого кодирования (метод one-hot). Здесь же мы будем добавлять слова во входные значения, а на выходе пользоваться прямым кодированием.

Таким образом мы задаем неизменность предложения, но выбираем различное представление токенов. Прямое кодирование воспринимает каждое слово как независимую единицу. Поэтому мы просто преобразуем каждое слово из входных значений к числовому списку. Этот список создает взаимосвязь между тегами разметки.

Размерность именно этого вектора представления слова – 8. Но очень часто она разнится от 50 до 500 в зависимости от объема словаря.

Восемь цифр для каждого слова имеют весовые значения, соизмеримые с нейронной сетью Vanilla. Эти данные отображают взаимосвязь слов между собой (Mikolov et al., 2013).

Именно с них мы и начнем изучение признаков разметки. Признаки – это то, что создает нейронная сеть для определения связи между входными и выходными значениями. На первых порах такого определения будет достаточно. Более подробно мы разберем эту тему в следующем разделе.

Энкодер

Возьмем вектор представления слова, прогоним его через долгую краткосрочную память (LSTM) и на выходе получим последовательность из признаков разметки. Все проходит через Dense-слой TimeDistributed – проще говоря, через Dense-слой с множеством входов и выходов.

Параллельно происходит сведение признаков изображения. Вне зависимости от количества структурированных чисел, все они преобразуются в один длинный числовой список. Затем к слою применяется Dense-слой и создаются признаки высокого уровня. Далее эти признаки объединяются в признаки разметки.

На словах все кажется очень сложным, поэтому давайте рассмотрим каждый этап по отдельности.

Признаки разметки

Здесь мы прогоняем векторы представления слов через LSTM-слой. В рисунке ниже все предложения заполняются до достижения максимального размера в три токена.

Для смешения сигналов и поиска высокоуровневых шаблонов в признаках разметки задействуем Dense-слой TimeDistributed. Это то же самое, что и простой Dense-слой, однако в TimeDistributed предусмотрено множество входных и выходных значений.

Признаки изображений

Займемся подготовкой изображения. Берем признаки с мини-картинок и соединяем их в один длинный список. Сама информация остается прежней, но структурная организация поменяется.

Вновь обратимся к Dense-слою, чтобы смешать сигналы и извлечь высокоуровневые структуры. Раз уж мы взаимодействуем только с одним входным значением, то можно перейти на обычным Dense-слой. Для объединения признаков разметки и изображения достаточно скопировать только признаки изображения.

У нас есть три признака разметки, поэтому в итоге получится одинаковое количество признаков изображения и разметки.

Объединение признаков разметки и изображения

Для создания трех признаков разметки производится дополнение всех предложений. Поскольку мы уже подготовили признаки изображений, то можем приступить к поочередному добавлению признаков изображений к признакам разметки.

После пошаговой привязки признаков изображения и разметки получатся три общих признака изображения-разметки. Это входное значение нужно будет скормить декодеру.

Декодер

Теперь для предсказания следующего тега будем пользоваться объединенными признаками изображения-разметки.

В примере ниже у нас есть три пары признаков изображения-разметки и один выходной признак следующего тега.

Обратите внимание, что LSTM-слой имеет последовательность в значении false. Вместо возвращения длины последовательности на входе он предугадывает признак. В нашем примере – это признак следующего тега, в котором содержится информация об итоговом предсказании.

Итоговое предсказание

Dense-слой выступает в роли традиционной нейронной сети прямого распространения. Она связывает 512 цифр из признака следующего тега с 4 итоговыми предсказаниями. Допустим, в словаре присутствует 4 слова: start, hello, world и end.

Предсказание словаря можно записать как [0.1, 0.1, 0.1, 0.7]. Активация softmax в Dense-слое распределяет вероятность от 0 до 1 с суммой всех предсказаний равной 1. В таком случае, алгоритм предсказывает, что четвертое слово – это следующий тег. Затем переводим прямое кодирование [0, 0, 0, 1] в преобразованные значения (например, “end”).

# Загрузка изображений и предварительная подготовка их для inception-resnet
images = []
all_filenames = listdir('images/')
all_filenames.sort()
for filename in all_filenames:
images.append(img_to_array(load_img('images/'+filename, target_size=(299, 299))))
images = np.array(images, dtype=float)
images = preprocess_input(images)
# Прогон изображений через inception-resnet и извлечение признаков без уровня классификации
IR2 = InceptionResNetV2(weights='imagenet', include_top=False)
features = IR2.predict(images)
# Ограничение в 100 токенов для каждой входной последовательности
max_caption_len = 100
# Инициирование функции для создания словаря
tokenizer = Tokenizer(filters='', split=" ", lower=False)
# Чтение документа и возвращение строки
def load_doc(filename):
file = open(filename, 'r')
text = file.read()
file.close()
return text
# Загрузка всего HTML-файла
X = []
all_filenames = listdir('html/')
all_filenames.sort()
for filename in all_filenames:
X.append(load_doc('html/'+filename))
# Создание словаря из HTML-файлов
tokenizer.fit_on_texts(X)
# Добавление +1 – это место для хранения пустых слов
vocab_size = len(tokenizer.word_index) + 1
# Перевод каждого слова в текстовый формат с соответствующим индексом словаря
sequences = tokenizer.texts_to_sequences(X)
# Самый длинный HTML-файл
max_length = max(len(s) for s in sequences)
# Инициирование входного значения итогового предсказания в модели
X, y, image_data = list(), list(), list()
for img_no, seq in enumerate(sequences):
for i in range(1, len(seq)):
# добавляем целиковую последовательность на вход; следующее слово остается на выход
in_seq, out_seq = seq[:i], seq[i]
# если предложение короче max_length, то заполняем его пустыми словами
in_seq = pad_sequences([in_seq], maxlen=max_length)[0]
# отмечаем выход на прямом кодировании
out_seq = to_categorical([out_seq], num_classes=vocab_size)[0]
# добавляем изображение, соответствующее HTML-файлу
image_data.append(features[img_no])
# обрезаем последовательность на входе до 100 токенов и добавляем ее во входное значение
X.append(in_seq[-100:])
y.append(out_seq)
X, y, image_data = np.array(X), np.array(y), np.array(image_data)
# Создание энкодера
image_features = Input(shape=(8, 8, 1536,))
image_flat = Flatten()(image_features)
image_flat = Dense(128, activation='relu')(image_flat)
ir2_out = RepeatVector(max_caption_len)(image_flat)
language_input = Input(shape=(max_caption_len,))
language_model = Embedding(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(Dense(128, activation='relu'))(language_model)
# Создание декодера
decoder = concatenate([ir2_out, language_model])
decoder = LSTM(512, return_sequences=False)(decoder)
decoder_output = Dense(vocab_size, activation='softmax')(decoder)
# Компиляция модели
model = Model(inputs=[image_features, language_input], outputs=decoder_output)
model.compile(loss='categorical_crossentropy', optimizer='rmsprop')
# Обучение нейронной сети
model.fit([image_data, X], y, batch_size=64, shuffle=False, epochs=2)
# Преобразование целого числа в слово
def word_for_id(integer, tokenizer):
for word, index in tokenizer.word_index.items():
if index == integer:
return word
return None
# Создание описания изображения
def generate_desc(model, tokenizer, photo, max_length):
# запуск процесса формирования
in_text = 'START'
# выполнение итерации по всей длине последовательности
for i in range(900):
# целочисленная преобразованная последовательность на входе
sequence = tokenizer.texts_to_sequences([in_text])[0][-100:]
# выравниватель на входе
sequence = pad_sequences([sequence], maxlen=max_length)
# предсказание следующего слова
yhat = model.predict([photo,sequence], verbose=0)
# преобразование вероятности в целое число
yhat = np.argmax(yhat)
# присвоение целого числа для слова
word = word_for_id(yhat, tokenizer)
# остановка, если не можем найти слово
if word is None:
break
# добавляем на вход при создании следующего слова
in_text += ' ' + word
# печать предсказания
print(' ' + word, end='')
# остановка при предсказании окончания последовательности
if word == 'END':
break
return
# Загрузка и подготовка изображения для IR2, извлечение признаков и генерация 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)

Выходное значение

Ссылки на сгенерированные сайты

· 250 epochs

· 350 epochs

· 450 epochs

· 550 epochs

Если страницы не открываются, то щелкните по ссылкам правой кнопкой и выберите «Просмотреть код страницы». Вот первоначальный сайт для сравнения.

Допущенные ошибки:

· Разобраться в долгой краткосрочной памяти было куда труднее, чем в сверточных нейронных сетях. После развертки всех слоев LSTM процесс познания значительно упростился. Очень помогло видео Fast.ai по рекуррентным нейронным сетям. Вначале нужно разобраться в признаках входных и выходных значений, а лишь после этого переходить к изучению принципов их работы.

· Создание словаря с нуля намного проще сокращения уже существующего. Касается всего, начиная со шрифтов, размеров div, HEX цветов и заканчивая именами переменных и обычными словами.

· Большинство библиотек создается для парсинга текстовых документов, а не кода. В документах элементы отделяются пробелами. Но в коде потребуется настраиваемый парсинг.

Можно извлекать признаки из модели, обученной на Imagenet. На первый взгляд, звучит весьма сомнительно, т.к. в Imagenet заложено крайне мало веб-изображений. Однако в таком случае потери на 30% выше по сравнению с моделью pix2code, обученной с нуля. Было бы интересно поработать с обученной на веб-скриншотах моделью типа inception-resnet.

В следующей, заключительной части мы обобщим полученные знания, создадим bootstrap-модель и рассмотрим аспекты дальнейшего развития нейронных сетей.

--

--