![](https://habrastorage.org/webt/uh/zp/cc/uhzpccyp0zgavcpqjkzgtorlhq8.png)
Это история о том, как я с нуля осваивал создание генеративных моделей МО, попутно обучая компьютер создавать шрифты. Да, настоящие типографские шрифты, состоящие из набора заглавных глифов. Созданная мной модель получает на входе описание шрифта и создаёт на выходе файл с их готовым набором. Назвал я свой проект FontoGen.
Выше вы видите несколько примеров шрифтов, сгенерированных моделью FontoGen.
Ну а дальше я подробно опишу всю историю.
![](https://habrastorage.org/webt/be/0s/hh/be0shhyhse8ckadwl2ik2fl1clu.png)
Я построю свой луна-парк — с блэкджеком и шлюхами! — Bender Bending Rodríguez
Вступление
В начале 2023 года, когда ИИ начал активно заполонять интернет, я, как и многие другие, очень заинтересовался этой темой. Меня затянуло в этот мир, где я создавал мемы с помощью Stable Diffusion, обучал LoRa на лицах моих друзей и подстраивал модели трансляции текста в речь, имитирующие известные голоса.
В один момент моё внимание привлекла технология генерации SVG на основе текста, которая оказалась куда более сложной задачей в сравнении с генерацией из того же текста растровых изображений. И сложность заключается не только в самом формате. Эта технология также позволяет представлять точную форму множеством различных способов. А поскольку я хотел научиться создавать генеративные модели МО с нуля, это направление стало моим проектом выходного дня.
Идея
Когда я начал изучать различные способы генерации SVG, то наткнулся на работу IconShop, в которой учёным удалось достичь весьма впечатлительных результатов. Мне потребовалось какое-то время, чтобы воспроизвести их путём создания модели на основе взятого из этой работы описания. Когда я всё же смог достичь достаточно близких результатов, то понял, что процесс генерации шрифтов может быть аналогичен процессу генерации SVG, и приступил к работе над этим проектом.
![](https://habrastorage.org/webt/xn/wf/cq/xnwfcqn10sttxfgvdcokljvzuaa.png)
Итоговый результат
В сравнении с генерацией SVG-картинок, шрифты генерировать и проще, и сложнее одновременно. Проще в том смысле, что в шрифтах нет цветного компонента, присущего изображениям SVG. Сложность же в том, что один шрифт состоит из множества глифов, и все эти глифы должны быть стилистически согласованы. Сохранение общей стилистики оказалось серьёзной проблемой, которую я опишу ниже.
Архитектура модели
Итак, в основу моей модели лёг описанный в работе IconShop метод генерации SVG. В итоге получилась «sequence-to-sequence» модель, обученная на последовательности, состоящей из текстовых эмбеддингов, сопровождаемых эмбеддингами шрифтов.
![](https://habrastorage.org/webt/6r/an/ws/6ranws-wdhczrbwmyf4bgz3qeqk.png)
Последовательность ввода
▍ Текстовые эмбеддинги
Для создания текстовых эмбеддингов я использовал предварительно обученную модель BERT, которая помогает передать «смысл» промпта. Последовательность текста ограничена 16 токенами, которые в случае BERT примерно соответствуют такому же количеству слов. И хотя теоретически промпт мог бы быть длиннее, для моей конфигурации с одним GPU ограничения памяти стали значительной проблемой. Итак, все текстовые описания шрифтов, присутствующих в датасете, при помощи GPT-3 были обобщены во множество ключевых слов.
▍ Эмбеддинги шрифтов
Для того, чтобы создать эмбеддинги шрифтов, шрифты сначала нужно преобразовать в последовательность токенов по аналогии с тем, как текст токенизируется при помощи токенизатора BERT. В этом проекте я рассматривал только формы глифов, игнорируя ширину, высоту, смещение и прочие полезные метаданные, присутствующие в файлах шрифтов. Каждый глиф претерпевал даунсэмплинг до размера 150х150 пикселей и нормализовался. Путём экспериментов я выяснил, что уменьшение до размера 150х150 сохраняет все особенности шрифта с минимальной деформацией глифов, которая при меньших размерах становилась уже более выраженной.
![](https://habrastorage.org/webt/xb/6h/g8/xb6hg8_qprwkczmxxlbez2dp37q.png)
Для парсинга файлов шрифтов я использовал библиотеку Python fonttools, которая ловко преобразует каждый глиф в последовательность кривых, линий и команд перемещения, где каждую команду можно сопровождать или не сопровождать очередной точкой. Для получения минимального пригодного к использованию шрифта я решил ограничить набор глифов следующими:
ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.,!?
В итоге словарь модели включил 22 547 токенов:
- 40 глифов,
- 5 операций перемещения по линиям:
moveTo
,lineTo
,qCurveTo
,curveTo
,closePath
, - 2 токена для представления EOS (end of sequence, конец последовательности) и PAD (padding, заполнение),
- 150^2 = 22 500 точек.
![](https://habrastorage.org/webt/xs/x1/6y/xsx16yhnpkvzffyt8uawfyzewdm.png)
Пример последовательности токенов шрифта
Затем последовательность токенов с помощью матриц эмбеддингов преобразуется в вектор эмбеддинга. Кроме того, как предлагает работа SkexGen, для координат
x
и y
я использовал отдельные матрицы. Заключительным шагом стало применение позиционных эмбеддингов.▍ Трансформер
Моя модель — это авторегрессионный декодер-трансформер, состоящий из 16 слоёв и 8 блоков. Размер модели 512, в результате чего мы получаем 73,7 миллиона параметров.
| Name | Type | Params
-----------------------------------
0 | model | Fontogen | 73.7 M
-----------------------------------
73.7 M Обучаемые параметры
0 Необучаемые параметры
73.7 M Всего параметров
294.728 Общий остаточный размер параметров (МБ)
Потери я вычислил с помощью простого метода перекрёстной энтропии, проигнорировав токен заполнения.
▍ Механизм внимания
При каждой генерации очередной части глифа есть ряд факторов, которые определяют, какой токен будет следующим. Первым делом форму глифа определяет заданный модели промпт. Затем модели нужно учесть все ранее сгенерированные для этого глифа токены. И, наконец, чтобы обеспечить согласованность стиля, ей нужно также учесть все глифы, которые были созданы перед этим.
В ходе первых экспериментов, где использовалось лишь небольшое число глифов, я начал с глобального внимания (global attention). Тем не менее по мере увеличения последовательности этот подход становился всё менее практичным, указывая на необходимость использовать разрежённое внимание (sparse attention). Изучив различные варианты, я остановил свой выбор на механизме BigBird. Этот метод поддерживает глобальное внимание, позволяя сфокусироваться на изначальном промпте, и скользящее внимание (window attention), когда модель просматривает N предыдущих токенов, определяя стиль нескольких предшествовавших глифов.
![](https://habrastorage.org/webt/9h/dm/af/9hdmafqy00y_jsq8r3yutlqeyo0.png)
Механизм внимания BigBird
Учитывая, что один глиф может иметь различное число токенов, я настроил механизм внимания на учитывание не менее 3 последних глифов. И хотя в основном этот подход обеспечивал успешное сохранение общего стиля шрифта, в некоторых сложных случаях стиль плавно скатывался в неисправимые каракули.
![](https://habrastorage.org/webt/9b/rx/3y/9brx3y5pj6j9_lbarhsj9andj2c.png)
Сложности каллиграфии
Обучение
Для обучения модели я собрал датасет из 71 000 шрифтов. Среди них 60% были отнесены к неопределённой категории, а 20% сопровождались более длинными описаниями, которые GPT-3.5 сжал в несколько ключевых слов. Кроме того, я добавил 15% шрифтов, в которых промпт содержал только имя шрифта, и 5% вообще без какого-либо описания, чтобы модель научилась генерировать шрифты без промпта.
Ввиду высоких требований к памяти, моя Nvidia 4090 с 24G VRAM смогла уместить в один пакет лишь две последовательности шрифтов, причём я часто наблюдал взрыв градиента. Решить проблему помогло использование аккумуляции градиента и градиентного клиппинга. Модель обучалась в течение 50 эпох, на что ушло 127 часов. Я перезапускал обучение после 36 эпох и прогонял её ещё в течение 14, снизив аккумуляцию градиента. Останавливалось обучение, когда потери при валидации практически переставали снижаться.
![](https://habrastorage.org/webt/ju/ev/5n/juev5n5s-_ilallzkiexg6rhw0a.png)
Потери при валидации в течение первых 36 эпох
![](https://habrastorage.org/webt/jy/gw/2k/jygw2kj56vf1vv1sh_rp_3w6ajs.png)
Потери при валидации в течение оставшихся 14 эпох
Повышение производительности
Для меня было очень важно добиться высокой скорости обучения, так как я проводил его на одном GPU, что требовало довольно много времени.
- При первой итерации я обрабатывал файлы шрифтов и текстовые описания на каждом шагу непосредственно внутри модели. И хотя структура этой базы данных упрощала прототипирование, это также означало, что одни и те же задачи нужно повторять снова и снова, растягивая процесс обучения. Кроме того, загрузка в память языковой модели BERT означала, что она займёт часть драгоценной VRAM. Максимально сдвинув весь процесс в сторону этапа предварительной обработки датасета, я смог втрое увеличить производительность.
- Изначально модель опиралась на трансформеры с платформы Hugging Face. Перенос кода на xFormers привёл к очевидному повышению скорости и эффективности потребления памяти.
Вместо заключения
Поставленной цели я достиг. Я научился создавать генеративные модели трансформеров и реализовал такую, которая в качестве побочного эффекта умеет генерировать шрифты. Но есть ещё много разных идей, которые я не попробовал. Например, что, если мою модель интегрировать в существующие редакторы шрифтов, чтобы дизайнеры могли просто создавать один глиф
А
, а модель бы генерировала все остальные. Или, возможно, редактор шрифтов мог бы просто предлагать контрольные точки для кривых Безье по ходу их отрисовки. Горизонт идей очень широк, и здесь ещё многое предстоит исследовать.Telegram-канал со скидками, розыгрышами призов и новостями IT 💻
![](https://habrastorage.org/webt/7u/o0/_9/7uo0_9itb0bkl_y8_0_n1a57vuc.jpeg)