В Понимании нейронных сетей мы рассмотрели обзор Искусственных Нейронных Сетей и попытались дать примеры из реального мира для их понимания на более высоком уровне. В этой статье мы собираемся рассмотреть, как обучать нейронные сети и затем развертывать их на программируемой пользователем вентильной матрице (FPGA) с использованием открытой библиотеки под названием hls4ml.
В отличие от Понимания нейронных сетей, мы собираемся рассмотреть гораздо более простую модель по двум основным причинам. Для некоторых переход от 28 x 28 пикселей к полной модели был огромным скачком. Хотя я объяснял разбиение как серию кусочно-непрерывных функций в большой многомерной матрице, это всё равно было сложно уловить. Мы часто слышим о "параметрах", но в моделях обработки изображений они могут показаться абстрактными и сложными. Мы также хотим использовать более простую модель, потому что собираемся реализовать её на аппаратном обеспечении. Графический процессор (GPU), даже стандартный домашний настольный GPU, может быстро вычислять и обучать модели на лету. Он специально предназначен для этой задачи (т.е. для вычислений). Подумайте, сколько вычислений происходит в 3D-играх с их сложными физическими движками и рендерингом изображений. GPU также популярны при майнинге различных типов криптовалют из-за их способности к вычислениям. И, наконец, обучение и запуск нейронной сети также являются лишь серией матричных умножений и параллельных вычислений. Хотя в FPGA содержатся математические блоки в их структуре, программные инструменты для компиляции этих вычислительных последовательностей не оптимизированы так, как GPU (по крайней мере, пока что).
По этим двум причинам я выбрал гораздо более простую модель, модель классификации Ирисов. Хотя это и не так весело, как модель на основе зрения (например, база данных MNIST), она гораздо более проста в использовании. Набор данных, использованный для обучения модели, представляет собой 150 наблюдений трех различных видов цветков ириса: Сетоса, Версиколор и Виргиника. Каждое наблюдение (т.е. набор данных) содержит несколько измерений, уникальных для этих цветов. Они следующие:
Используя эти измерения, мы можем создать модель, которая может сказать нам (с довольно высокой степенью уверенности) к какому виду цветка она относится. Мы предоставляем эти четыре параметра, и она говорит нам, является ли ирис видом Сетоса, Версиколор или Виргиника. Хорошо то, что в этих значениях почти нет перекрытий. Например, у Сетосы, Версиколора и Виргиники довольно отчетливые длины чашелистиков. То же самое касается и других трех характеристик. Это показано на точечной диаграмме набора данных:
Рисунок 1: Попарные точечные диаграммы набора данных Ирисов
Когда мы говорили о том, почему использовать нейронную сеть в Понимании нейронных сетей, мы обсуждали концепцию программирования на основе правил: набор правил, по которым может следовать компьютер (т.е. алгоритм). В данном случае мы вполне могли бы закодировать эту проблему как алгоритм. Приведенные выше диаграммы рассеяния, вероятно, могут быть представлены серией математических уравнений. Мы используем этот набор данных скорее как пример для понимания сложности, которая нас ждет (подумайте о базе данных MNIST). С этим мы переходим к следующей теме: Реализация.
По мере того, как мы готовимся к реализации этого на FPGA, вы, вероятно, спрашиваете: "Зачем вообще строить нейронную сеть на FPGA?" Мы уже установили, что для этой задачи гораздо больше подходят GPU. Как вы, возможно, знаете или не знаете, GPU дороги и требуют много энергии. Также они генерируют много тепла при работе на полную мощность. При проектировании специализированного аппаратного обеспечения (или чипа) вы оптимизируете пространство, энергопотребление, тепловыделение и стоимость. Первый шаг к проектированию специализированной интегральной схемы (ASIC) - это сначала спроектировать её на FPGA. Если мы когда-либо планируем строить нейронные сети на ASIC, это будет первый шаг.
Итак, теперь мы наконец убедились в том, чтобы попробовать это. Мы видели примеры того, как обучать модель, и предполагаем, что если упростить модель, то реализация ее на FPGA должна быть довольно простой, верно? Неправильно. Помните, что мы говорили о множестве матриц и серии уравнений? Попытка уместить все эти вычисления на маленьком FPGA может быть чрезвычайно сложной. К счастью, существует действительно хороший обходной путь, над которым добрые люди из Лаборатории Быстрого Машинного Обучения (и сообщество) работали в рамках открытого проекта под названием hls4ml. Этот проект берет существующие модели и преобразует их в язык высокоуровневой синтеза (например, SystemC), который затем может быть преобразован в общепринятые языки FPGA, такие как Verilog или VHDL (с использованием инструментов синтеза FPGA, таких как Vivado от AMD Xilinx). Этот перевод сам по себе исключает огромное количество шагов. Попытка построить полноценную нейронную сеть только на Verilog или VHDL (особенно сложную) была бы чрезвычайно сложной. Этот инструмент действительно помогает нам перейти непосредственно к сути - но не без нескольких промежуточных шагов.
Мы на том этапе, когда обучили нашу модель и хотели бы запустить hls4ml и протестировать это на FPGA. Не так быстро. Если вы возьмете набор данных Iris, обучите его базовыми методами, а затем синтезируете код с помощью hls4ml, вы, вероятно, пройдете этап синтеза. Теперь направляйтесь к этапу размещения и трассировки, и вы никогда не сможете уместить эту модель в таком виде на маленьком FPGA. Помните, нам также нужна вся логика обработки данных и коммуникации на нашем FPGA. В их учебных материалах они используют плату Pynq-Z2 для демонстрации работы модели на FPGA. Эта плата была выбрана, потому что содержит не только FPGA, но и микропроцессор на чипе (Zynq 7000), который запускает полноценный веб-сервер Jupyter Notebook. Это означает, что вы можете запускать функции с аппаратным ускорением на FPGA, но также получить простой и удобный интерфейс для загрузки и тестирования ваших данных. Этот интерфейс, который действует как транспортный слой между операционной системой и FPGA, все еще занимает место на самом FPGA (так же, как если бы мы планировали использовать чистый FPGA, например, Spartan или Virtex FPGA).
Как упоминалось выше, проблема заключается в том, что FPGA ограничены по размеру и не обладают такой же емкостью, как GPU. В результате мы не сможем загрузить полную модель на FPGA - она просто не поместится. Даже простая модель Iris изначально не могла быть размещена на Pynq-Z2. К Уроку 4 вы начинаете более тесно знакомиться с техниками оптимизации (некоторые из которых настолько эзотерические, что я даже не начинал понимать, как они работают изнутри). Как только вы примените эти методы оптимизации, вы должны сможете разместить модели (по крайней мере, более простые) на FPGA. В этом репозитории мы можем посмотреть на BuildModel.py (в частности, на функцию обучения модели) и заметить, что мы не просто обучаем базовую модель, но и оптимизируем её:
def train_model(self):
"""
Построение, обучение и сохранение усеченной и квантованной нейронной сетевой модели с использованием QKeras.
Эта функция создает последовательную модель с слоями QKeras. Модель подвергается прунингу для уменьшения количества параметров
и квантования для использования более низкой точности для весов и активаций. Процесс обучения включает настройку
колбэков для обновления шагов прунинга. После обучения оболочки прунинга удаляются, и итоговая модель сохраняется.
Параметры:
Нет
Возвращает:
Нет
"""
# Инициализация последовательной модели
self.model = Sequential()
# Добавьте первый слой QDense с квантованием и прореживанием
self.model.add(
QDense(
64, # Количество нейронов в слое
input_shape=(4,), # Форма входных данных для набора данных Iris (4 признака)
name='fc1',
kernel_quantizer=quantized_bits(6, 0, alpha=1), # Квантование весов до 6 бит
bias_quantizer=quantized_bits(6, 0, alpha=1), # Квантование смещений до 6 бит
kernel_initializer='lecun_uniform',
kernel_regularizer=l1(0.0001),
)
)
# Добавьте квантованный слой активации ReLU
self.model.add(QActivation(activation=quantized_relu(6), name='relu1'))
# Добавьте второй слой QDense с квантованием и прореживанием
self.model.add(
QDense(
32, # Количество нейронов в слое
name='fc2',
kernel_quantizer=quantized_bits(6, 0, alpha=1), # Квантование весов до 6 бит
bias_quantizer=quantized_bits(6, 0, alpha=1), # Квантование смещений до 6 бит
kernel_initializer='lecun_uniform',
kernel_regularizer=l1(0.0001),
)
)
# Добавьте слой активации ReLU с квантованием
self.model.add(QActivation(activation=quantized_relu(6), name='relu2'))
# Добавить третий слой QDense с квантованием и прореживанием
self.model.add(
QDense(
32, # Количество нейронов в слое
name='fc3',
kernel_quantizer=quantized_bits(6, 0, alpha=1), # Квантование весов до 6 бит
bias_quantizer=quantized_bits(6, 0, alpha=1), # Квантование смещений до 6 бит
kernel_initializer='lecun_uniform',
kernel_regularizer=l1(0.0001),
)
)
# Добавить квантованный слой активации ReLU
self.model.add(QActivation(activation=quantized_relu(6), name='relu3'))
# Добавить выходной слой QDense с квантованием и прореживанием
self.model.add(
QDense(
3, # Количество нейронов в выходном слое (соответствует количеству классов в наборе данных Iris)
name='output',
kernel_quantizer=quantized_bits(6, 0, alpha=1), # Квантование весов до 6 бит
bias_quantizer=quantized_bits(6, 0, alpha=1), # Квантование смещений до 6 бит
kernel_initializer='lecun_uniform',
kernel_regularizer=l1(0.0001),
)
)
# Добавить слой активации softmax для классификации
self.model.add(Activation(activation='softmax', name='softmax'))
# Настройка параметров обрезки для удаления 75% весов, начиная после 2000 шагов и обновляя каждые 100 шагов
pruning_params = {"pruning_schedule": pruning_schedule.ConstantSparsity(0.75, begin_step=2000, frequency=100)}
self.model = prune.prune_low_magnitude(self.model, **pruning_params)
# Компиляция модели с оптимизатором Adam и потерями категориальной кросс-энтропии
adam = Adam(lr=0.0001)
self.model.compile(optimizer=adam, loss=['categorical_crossentropy'], metrics=['accuracy'])
# Настройка колбэка обрезки для обновления шагов обрезки во время обучения
callbacks = [
pruning_callbacks.UpdatePruningStep(),
]
# Обучение модели
self.model.fit(
self.X_train_val,
self.y_train_val,
batch_size=32, # Размер пакета для обучения
epochs=30, # Количество эпох обучения
validation_split=0.25, # Доля обучающих данных, используемых как валидационные данные
shuffle=True, # Перемешивание обучающих данных перед каждой эпохой
callbacks=callbacks, # Включение обратного вызова для обрезки
)
# Удаление обёрток обрезки из модели
self.model = strip_pruning(self.model)
Здесь предстоит усвоить огромное количество информации, но суть происходящего (помимо обучения) заключается в том, что благодаря квантованию, регуляризации, обрезке, инициализации весов и оптимизации (Adam) мы уменьшаем размер и сложность весов, чтобы сделать процесс более эффективным и быстрым. Некоторые из этих методов используются для повышения точности и обеспечения эффективности процесса обучения. После применения к нашей модели этих техник она должна быть достаточно маленькой, чтобы её можно было преобразовать в код для FPGA. Помните, существует множество способов это сделать. Это был подход, выбранный в учебнике, поэтому я придерживался того, что работало.
Итак, теперь мы готовы к компиляции, синтезу и размещению с маршрутизацией нашего проекта. В конце процесса мы должны получить файл битового потока, который, фактически, служит файлом флэш-памяти, инструктирующим FPGA, как работать. Поскольку Zynq имеет как процессор, так и FPGA, программирование FPGA через Python довольно просто (что мы рассмотрим позже). Используя указанный репозиторий, мы можем определить и скомпилировать нашу модель с использованием HLS, а затем построить файл битового потока всего в нескольких строках кода Python. В следующем коде мы выполняем дополнительную проверку, чтобы убедиться, что наша модель, сгенерированная HLS, имеет такую же (или достаточно близкую) точность, как и оригинальная модель.
def build_bitstream(self):
"""
Создает битовый поток HLS для обученной модели.
Эта функция преобразует обученную модель Keras в модель HLS с использованием hls4ml, компилирует ее и генерирует битовый поток для развертывания на FPGA.
Она также проверяет модель HLS по сравнению с оригинальной моделью и выводит точность обеих моделей.
"""
# Создайте конфигурацию HLS из модели Keras, с детализацией по именам слоев
config = hls4ml.utils.config_from_keras_model(self.model, granularity='name')
# Установите точность для слоя softmax
config['LayerName']['softmax']['exp_table_t'] = 'ap_fixed<18,8>'
config['LayerName']['softmax']['inv_table_t'] = 'ap_fixed<18,4>'
# Установите ReuseFactor для полносвязных слоев равным 512
for layer in ['fc1', 'fc2', 'fc3', 'output']:
config['LayerName'][layer]['ReuseFactor'] = 512
# Преобразуйте модель Keras в модель HLS
hls_model = hls4ml.converters.convert_from_keras_model(
self.model, hls_config=config, output_dir='hls4ml_prj_pynq', backend='VivadoAccelerator', board='pynq-z2'
)
# Скомпилируйте модель HLS
hls_model.compile()
# Прогнозирование с использованием модели HLS
y_hls = hls_model.predict(np.ascontiguousarray(self.X_test))
np.save('package/y_hls.npy', y_hls)
# Проверка модели HLS по сравнению с оригинальной моделью
y_pred = self.model.predict(self.X_test)
accuracy_original = accuracy_score(np.argmax(self.y_test, axis=1), np.argmax(y_pred, axis=1))
accuracy_hls = accuracy_score(np.argmax(self.y_test, axis=1), np.argmax(y_hls, axis=1))
print(f"Точность оригинальной обрезанной и квантованной модели: {accuracy_original * 100:.2f}%")
print(f"Точность модели HLS: {accuracy_hls * 100:.2f}%")
# Сборка модели HLS
hls_model.build(csim=False, export=True, bitfile=True)
Сложно оценить проделанную здесь работу, но объем труда, необходимый для преобразования модели Keras в файл битового потока, огромен. Это значительно упрощает задачу для среднестатистического пользователя (хотя квантование, уменьшение и оптимизация были далеко не простыми).
Конфигурации, опять же, были заимствованы из учебника, и коэффициент повторного использования - это число, которое может изменяться в зависимости от того, сколько логики мы хотим повторно использовать. После этого несколько вызовов hls4ml и, вуаля, у вас есть битовый файл!
Итак, теперь, когда у нас есть битовый файл, мы хотели бы протестировать его на реальном FPGA. Использование среды Pynq упрощает эту задачу. Для начала нам нужно скопировать битовый файл, наш тестовый скрипт и тестовые векторы на плату Pynq. Это можно сделать с помощью простой команды SCP или через веб-интерфейс. Чтобы упростить процесс (как это было сделано в учебнике hls4ml), мы помещаем все в одну папку под названием «package» и затем копируем её на целевое устройство через SCP (см. скрипт BuildModel.py в репозитории для получения дополнительной информации). Важно отметить здесь наличие дополнительной автоматически сгенерированной библиотеки под названием axi_stream_driver.py. Этот файл содержит вспомогательные функции не только для прошивки FPGA-части Zynq, но и для выполнения передачи данных для тестирования модели нейронной сети на базе FPGA.
После того как файлы были переданы на плату Pynq, мы можем либо открыть SSH-терминал на целевом устройстве, либо создать новую записную книжку для запуска кода, который находится в on_target.py. Я предпочитаю запускать скрипт через командную строку, но для этого потребуются права администратора, поэтому сначала вам нужно выполнить команду sudo -s после того, как вы подключились к устройству через SSH. Как только у вас будет запущен терминал с правами администратора на плате Pynq, вы можете перейти в jupyter_notebooks/iris_model_on_fpgas и запустить тест с помощью команды python3 on_target.py. Если все выполнено правильно, вы должны увидеть следующий вывод:
Рисунок 2: Ожидаемый результат выполнения скрипта on_targer.py
Итак, как нам узнать, работает ли все как надо? Теперь нам нужно проверить результаты (т.е. прогнозы), которые сгенерировал FPGA, и сравнить их с нашей локальной моделью, которую мы создали с использованием нашего GPU (или CPU). Проще говоря, для каждой длины/ширины чашелистика/лепестка, которую мы подаем на вход, мы ожидаем на выходе Ирис определенного типа: Сетоса, Версиколор или Виргиника (конечно, все в числах). Мы надеемся, что FPGA будет так же точен, как модель, работающая на нашем локальном компьютере.
Нам нужно будет скопировать вывод из скрипта обратно на нашу машину. Вы можете снова использовать команду SCP или просто скачать через интерфейс Jupyter notebook. Выходной файл будет называться y_hw.npy. После копирования файла нам нужно будет запустить utilities/validate_model.py. Вывод должен выглядеть примерно так:
Рисунок 3: Результаты из скрипта валидации
Как вы можете видеть, наша оптимизированная модель (на ПК) и синтезированная модель (на FPGA) обе делят один и тот же уровень точности: 96,67%. Из всех 30 тестовых точек мы не смогли предсказать только одну - отлично!
В этой статье мы взяли набор данных для классификации Iris и создали из него модель нейронной сети. Используя hls4ml, мы построили битовый файл и необходимые библиотеки для запуска той же модели на плате Pynq-Z2. Мы также запустили скрипт валидации, который сравнил модель, основанную на компьютере, с моделью, основанной на FPGA, и как они обе соотносились с оригинальными данными. Хотя модель и набор данных были довольно тривиальными, этот учебник заложил основу для того, что такое проектирование сложных нейронных сетей на FPGA.
Примечание: Весь код для этого проекта можно найти в этом репозитории.