Практические руководства

Создание ONNX с нуля

ONNX предоставляет чрезвычайно гибкий формат для хранения моделей и конвейеров AI / ML. Чтобы узнать, как это сделать, поучительно построить график ONNX вручную.

ONNX существует уже некоторое время, и он становится успешным промежуточным форматом для перемещения, часто тяжелых, обученных нейронных сетей от одного обучающего инструмента к другому (например, перемещение между pyTorch и Tensorflow) или для развертывания моделей в облако с использованием ONNX runtime. В этих случаях пользователи часто просто сохраняют модель в формате ONNX, не беспокоясь о результирующем графике ONNX.

Однако ONNX можно найти гораздо более универсально: ONNX можно легко использовать для ручного указания конвейеров обработки AI / ML, включая всю предварительную и постобработку, которая часто необходима для реальных развертываний . Кроме того, благодаря своей стандартизированной и открытой структуре конвейер, хранящийся в ONNX, можно легко развернуть даже на периферийных устройствах (например, с помощью автоматической компиляции в WebAssembly для эффективного развертывания на различных целевых объектах). В этом руководстве мы покажем, как использовать onnx.helper инструменты в Python для создания конвейера ONNX с нуля и его эффективного развертывания.

Учебное пособие состоит из следующих частей:

  1. Немного истории о ONNX. Прежде чем мы начнем, полезно концептуально понять, что делает ONNX.
  2. Сценарий «домашней охоты». В этом руководстве мы сосредоточимся на создании конвейера для прогнозирования цены рекламируемого дома, а затем оценим, соответствует ли дом нашим ограничениям поиска (то есть нашим желаемым).
  3. Модельное обучение. Хотя на самом деле это не часть конвейера развертывания, мы покажем, как мы использовали `sklearn` для обучения модели прогнозирования.
  4. Создание конвейера ONNX. Это основная часть этого руководства, и мы рассмотрим его поэтапно:
    - Предварительная обработка: мы стандартизируем входные данные, используя результаты нашего обучения.
    - Вывод: мы будем прогнозировать (логарифмическую) цену, используя модель, подобранную во время обучения.
    - Постобработка: мы проверим, соответствуют ли результаты нашим desiderata.
    - Собираем все вместе: мы объединим конвейеры предварительной обработки, вывода и постобработки в один граф ONNX.
  5. Развертывание модели: можно использовать среду выполнения ONNX для развертывания моделей ONNX или оптимизировать подобранный граф и развернуть с помощью WebAssembly. Кратко рассмотрим оба варианта.

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

1. Что такое ONNX?

Согласно официальному сайту ONNX:

ONNX - это открытый формат, созданный для представления моделей машинного обучения. ONNX определяет общий набор операторов - строительных блоков моделей машинного обучения и глубокого обучения - и общий формат файлов, позволяющий разработчикам ИИ использовать модели с различными фреймворками, инструментами, средами выполнения и компиляторами (см. onnx.ai ).

Таким образом, ONNX - это открытый формат файла для хранения (обученных) моделей / конвейеров машинного обучения, содержащих достаточно деталей (в отношении типов данных и т. Д.) Для перехода с одной платформы на другую. Специфика ONNX даже позволяет автоматически компилировать сохраненные операции на языки более низкого уровня для встраивания на различные устройства. Фактически файл onnx будет содержать все, что вам нужно знать для восстановления полного конвейера обработки данных при переходе с одной платформы на другую.

Концептуально формат ONNX достаточно прост: файл onnx определяет ориентированный граф, в котором каждое ребро представляет собой тензор определенного типа, который перемещается от одного узла к другому. Сами узлы называются операторами, и они оперируют своими входами (то есть результатами своих родителей в графе) и отправляют результат своей операции своим потомкам. ONNX определяет список операций, которые вместе позволяют указать практически любую операцию AI / ML, которую вы, возможно, захотите выполнить (а в противном случае набор операторов легко расширяется).

В качестве концептуального примера предположим, что мы хотели бы сделать выводы из модели логистической регрессии. Учитывая входной вектор x, мы бы:

1. Вычислите линейный предиктор ylin, используя, например, оператор Gemm, чтобы умножить наш входной вектор x на изученные коэффициенты beta.
2. Преобразуйте линейный предиктор в шкалу вероятности с помощью оператора Sigmoid.
3. Сгенерируйте вывод истина или ложь с использованием оператора Less.

Таким образом, эффективно объединяя ряд операторов, мы можем генерировать выводы с учетом некоторого вектора признаков. Однако не обманывайтесь простотой примера: благодаря поддержке тензора ONNX и обширному списку операторов даже сложные DNN для обработки видео могут быть представлены в ONNX.

Пусть вас не обманывает простота примера: благодаря поддержке тензора ONNX и обширному списку операторов даже сложные DNN для обработки видео могут быть представлены в ONNX.

2. Наш сценарий охоты за домом

Чтобы продемонстрировать, как создавать графики ONNX с нуля, мы представим простой сценарий:

«Предположим, вы ищете новый дом по соседству. Учитывая набор данных о предыдущих продажах домов, содержащий yard размер двора, жилая площадь area и количество комнат rooms проданных домов и их цены price, постройте модель для прогнозирования цены недавно рекламируемого дома (т. Е. Вам дается только двор, площадь и количество комнат). Затем вас интересует любой дом, который доступен менее чем за 400 000 евро и имеет двор (т. Е. Размер двора больше 0) ».

В этом руководстве мы построим конвейер обработки данных, включая модель линейной регрессии для вывода, которая с учетом примера вектора признаков (yard,area,rooms) предоставляет логическое значение, указывающее, представляет ли дом интерес.

3. Модельное обучение

Чтобы создать конвейер, нам сначала нужно создать модель, которую мы используем для прогнозирования цен. Приведенный ниже код открывает файл данных houses.csv (доступен здесь) и генерирует модель. Обратите внимание, что мы предварительно выполняем несколько шагов:

  • Мы стандартизируем входные данные (таким образом, для дестандартизации в нашем конвейере предварительной обработки нам понадобятся «mean» и «std» каждой из входных переменных).
  • Мы регистрируем преобразование цен (таким образом, в нашем конвейере постобработки нам нужно будет преобразовать наш вывод обратно в правильный масштаб).
  • Мы подбираем простую модель линейной регрессии (и сохраняем оценочные коэффициенты для использования в нашем конвейере обработки).

Вот закомментированный код (или щелкните здесь, чтобы увидеть полную записную книжку):

import numpy as np
from sklearn import linear_model as lm
# Open the training data:
data = np.loadtxt(open(“houses.csv”, “rb”), delimiter=”,”, skiprows=1)
# Retreive feature vectors and outcomes:
datX = data[:, [1,2,3]] # Input features (yard, area, rooms)
datY = data[:, [0]] # Price
# Standardize the inputs:
barX = np.mean(datX, 0) # Mean for each of the inputs
sdX = np.std(datX, 0) # Sd for each of the inputs
datZ = (datX — barX) / sdX
# Log transform the output
logY = np.log(datY)
# Fit a linear model
lin_mod = lm.LinearRegression()
lin_mod.fit(datZ, logY)
# retrieve intercept and fitted coefficients:
intercept = lin_mod.intercept_
beta = lin_mod.coef_
## Storing what we need for inference in our processing pipeline.
print(“ — — Values retrieved from training — — “)
print(“For input statdardization / pre-processing we need:”)
print(“ — The column means {}”.format(barX))
print(“ — The column sds {}”.format(sdX))
print(“For the prediction we need:”)
print(“ — The estimated coefficients: {}”.format(beta))
print(“ — The intercept: {}”.format(intercept))
# store the training results in an object to make the code more readable later on:
training_results = {
 “barX” : barX.astype(np.float32),
 “sdX” : sdX.astype(np.float32),
 “beta” : beta.astype(np.float32),
 “intercept” : intercept.astype(np.float32),
}
# And, also creating the constraints (for usage in block 3):
constraints = {
 “maxprice” : np.array([400000]),
 “minyard” : np.array([1]),
}

Хотя объекты training_results и constraints - это все, что нам нужно на этом этапе обучения для реализации нашего конвейера обработки в ONNX, поучительно продемонстрировать, как можно было бы генерировать выводы без преобразования в ONNX. Следующий код демонстрирует полный конвейер, который мы стремимся построить, используя простой код Python и используя первый экземпляр в нашем обучающем наборе в качестве примера:

# Get the data from a single house
first_row_example = data[1,:]
input_example = first_row_example[[1,2,3]]  # The features
output_example = first_row_example[0]  # The observed price
# 1. Standardize input for input to the model:
standardized_input_example = (input_example - training_results['barX'])/ training_results['sdX']
# 2. Predict the *log* price (using a dot product and the intercept)
predicted_log_price_example = training_results['intercept'] + np.dot(standardized_input_example, training_results['beta'].flatten())
# Compute the actual prediction on the original scale
predicted_price_example = np.exp(predicted_log_price_example)
print("Observed price: {}, predicted price: {}".format(output_example, predicted_price_example))
# See if it is interesting according to our simple decision rules:
interesting = input_example[1] > 0 and predicted_price_example < 400000
print("Interesting? {}".format(interesting))

4. Создание конвейера ONNX.

Наш конвейер ONNX должен, учитывая пример экземпляра, описанный входным вектором длины 3 (yard,area,rooms):

  1. [предварительная обработка] Стандартизируйте ввод, вычтя среднее значение (наблюдаемое в обучающем наборе) и разделив на стандартное отклонение.
  2. [вывод] Прогнозируйте стоимость дома для данного экземпляра в логарифмической шкале (используя коэффициенты из обученной модели sklearn выше)
  3. [постобработка] Верните цену к исходному масштабу и проверьте, а) доступен ли дом и б) есть ли у него двор.

Чтобы быть более точным и представить используемые операторы ONNX, мы будем генерировать следующий конвейер:

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

4а. Предварительная обработка

Мы будем использовать onnx.helper инструменты, предоставленные в Python, для построения нашего конвейера. Сначала мы создаем константы, затем рабочие узлы (хотя константы также являются операторами), а затем граф:

# The required constants:
c1 = h.make_node(‘Constant’, inputs=[], outputs=[‘c1’], name=”c1-node”, 
 value=h.make_tensor(name=”c1v”, data_type=tp.FLOAT, 
 dims=training_results[‘barX’].shape, 
 vals=training_results[‘barX’].flatten()))
c2 = h.make_node(‘Constant’, inputs=[], outputs=[‘c2’], name=”c2-node”, 
 value=h.make_tensor(name=”c2v”, data_type=tp.FLOAT, 
 dims=training_results[‘sdX’].shape, 
 vals=training_results[‘sdX’].flatten()))
# The functional nodes:
n1 = h.make_node(‘Sub’, inputs=[‘x’, ‘c1’], outputs=[‘xmin’], name=’n1')
n2 = h.make_node(‘Div’, inputs=[‘xmin’, ‘c2’], outputs=[‘zx’], name=”n2")
# Create the graph
g1 = h.make_graph([c1, n1, c2, n2], ‘preprocessing’,
 [h.make_tensor_value_info(‘x’, tp.FLOAT, [3])],
 [h.make_tensor_value_info(‘zx’, tp.FLOAT, [3])])
# Create the model and check
m1 = helper.make_model(g1, producer_name=’scailable-demo’)
checker.check_model(m1)
# Save the model
save(m1, ‘pre-processing.onnx’)

Приведенный выше код создает конвейер предварительной обработки и сохраняет его в формате onnx. Из Python мы можем напрямую протестировать сохраненную модель, используя onnxruntime:

# A few lines to evaluate the stored model, useful for debugging:
import onnxruntime as rt
# test
sess = rt.InferenceSession(“pre-processing.onnx”) # Start the inference session and open the model
xin = input_example.astype(np.float32) # Use the input_example from block 0 as input
zx = sess.run([“zx”], {“x”: xin}) # Compute the standardized output
print(“Check:”)
print(“The standardized input using onnx pipeline is: {}”.format(zx))
print(“ — Compare to standardized first row in block 0: {}”.format(datZ[1,:]))

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

4b. Вывод

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

# The constants:
c3 = h.make_node(‘Constant’, inputs=[], outputs=[‘c3’], name=”c3-node”, 
 value=h.make_tensor(name=”c3v”, data_type=tp.FLOAT, 
 dims=training_results[‘beta’].shape, 
 vals=training_results[‘beta’].flatten()))
c4 = h.make_node(‘Constant’, inputs=[], outputs=[‘c4’], name=”c4-node”, 
 value=h.make_tensor(name=”c4v”, data_type=tp.FLOAT, 
 dims=training_results[‘intercept’].shape, 
 vals=training_results[‘intercept’].flatten()))
# The operating nodes, Multiply, reduceSum, and Add
n3 = h.make_node(‘Mul’, inputs=[‘zx’, ‘c3’], outputs=[‘mulx’], name=”multiplyBeta”)
n4 = h.make_node(‘ReduceSum’, inputs=[‘mulx’], outputs=[‘sumx’], name=”reduceSum”, keepdims=0)
n5 = h.make_node(‘Add’, inputs=[‘sumx’, ‘c4’], outputs=[‘yhatlog’], name=’addIntercept’)
# The graph
g2 = h.make_graph([c3, c4, n3, n4, n5], ‘linear_regression’,
 [h.make_tensor_value_info(‘zx’, tp.FLOAT, [3])],
 [h.make_tensor_value_info(‘yhatlog’, tp.FLOAT, [1])])
# The model and check:
m2 = h.make_model(g2, producer_name=’scailable-demo’)
checker.check_model(m2)
# Save the model
save(m2, ‘linear-regression.onnx’)

Опять же, легко проверить правильность прогнозов:

# test
sess = rt.InferenceSession(“linear-regression.onnx”) # Start the inference session and open the model
xin = standardized_input_example.astype(np.float32) # Use the input_example from block 0 as input
yhatlog = sess.run([“yhatlog”], {“zx”: xin}) # Compute the standardized output
print(“Check:”)
print(“The log predicted price from ONNX is: {}”.format(yhatlog))
print(“ — Compare to analysis in block 0: {}”.format(predicted_log_price_example))

4c. Постобработка

Конвейер постобработки немного сложнее, поскольку он проверяет, соответствует ли дом нашим требованиям (т.е. predicted_price < 400000 и yard > 0 и, таким образом, использует различные источники ввода (и обратите внимание, что оператор `Slice` немного задействован):

# Constants (note using the constraints object created in block 0 above)
c5 = h.make_node(‘Constant’, inputs=[], outputs=[‘c5’], name=”c5-node”, 
 value=h.make_tensor(name=”c5v”, data_type=tp.FLOAT, 
 dims=constraints[‘maxprice’].shape, 
 vals=constraints[‘maxprice’].flatten()))
c6 = h.make_node(‘Constant’, inputs=[], outputs=[‘c6’], name=”c6-node”, 
 value=h.make_tensor(name=”c6v”, data_type=tp.FLOAT, 
 dims=constraints[‘minyard’].shape, 
 vals=constraints[‘minyard’].flatten()))
# Auxiliary constants for the slice operator:
caux1 = h.make_node(‘Constant’, inputs=[], outputs=[‘caux1’], name=”caux1-node”,
 value=h.make_tensor(name=’caux1v’, data_type=tp.INT32,
 dims=np.array([0]).shape, vals=np.array([0]).flatten()))
caux2 = h.make_node(‘Constant’, inputs=[], outputs=[‘caux2’], name=”caux2-node”,
 value=h.make_tensor(name=’caux2v’, data_type=tp.INT32,
 dims=np.array([1]).shape, vals=np.array([1]).flatten()))
caux3 = h.make_node(‘Constant’, inputs=[], outputs=[‘caux3’], name=”caux3-node”,
 value=h.make_tensor(name=’caux3v’, data_type=tp.INT32,
 dims=np.array([0]).shape, vals=np.array([0]).flatten()))
caux4 = h.make_node(‘Constant’, inputs=[], outputs=[‘caux4’], name=”caux4-node”,
 value=h.make_tensor(name=’caux4v’, data_type=tp.INT32,
 dims=np.array([1]).shape, vals=np.array([1]).flatten()))
 
# Nodes:
n6 = h.make_node(‘Exp’, inputs=[‘yhatlog’], outputs=[‘yhat’], name=’exponent’)
n7 = h.make_node(‘Less’, inputs=[‘yhat’, ‘c5’], outputs=[‘price_ok’], name=’priceLess’)
n8 = h.make_node(‘Slice’, inputs=[‘x’, ‘caux1’, ‘caux2’, ‘caux3’, ‘caux4’], outputs=[‘yard’],)
n9 = h.make_node(‘Less’, inputs=[‘c6’, ‘yard’], outputs=[‘yard_ok’], name=”yardMore”) # note reversal
n10 = h.make_node(‘And’, inputs=[‘price_ok’, ‘yard_ok’], outputs=[‘result’], name=’andBools’)
# The graph
g3 = h.make_graph([c5, c6, caux1, caux2, caux3, caux4, n6, n7, n8, n9, n10], ‘postprocessing’,
 [h.make_tensor_value_info(‘x’, tp.FLOAT, [3]), h.make_tensor_value_info(‘yhatlog’, tp.FLOAT, [1])],
 [h.make_tensor_value_info(‘result’, tp.BOOL, [1])])
# The model and check:
m3 = h.make_model(g3, producer_name=’scailable-demo’)
checker.check_model(m3)
# Save the model
save(m3, ‘post-processing.onnx’)

Опять же, результаты легко проверить:

# test
sess = rt.InferenceSession(“post-processing.onnx”) # Start the inference session and open the model
x = input_example.astype(np.float32) # Use the input_example from block 0 as input
yhatlog = np.array(yhatlog).flatten()
result = sess.run([“result”], {“x”: x, “yhatlog” : yhatlog}) # Compute the standardized output
print(“Check:”)
print(“Predicted price {} and yardsize {} are appealing {}.”.format(np.exp(yhatlog), input_example[0], result))

4г. Собираем все вместе

Хотя приятно, чтобы каждая часть конвейера обработки была преобразована в ONNX для упрощения развертывания, одна из возможностей ONNX - это возможность связывать один набор операций с другим. Следовательно, мы можем легко определить наш полный конвейер в один граф ONNX:

g_full = h.make_graph([c1, n1, c2, n2, c3, c4, n3, n4, n5, c5, c6, caux1, caux2, caux3, caux4, n6, n7, n8, n9, n10], 
 ‘fullpipeline’,
 [h.make_tensor_value_info(‘x’, tp.FLOAT, [3])],
 [h.make_tensor_value_info(‘result’, tp.BOOL, [1])])
m_full = h.make_model(g_full, producer_name=’scailable-demo’)
checker.check_model(m_full)
# Save the model
save(m_full, ‘full-pipeline.onnx’)

Опять же, наш результат легко проверить:

# test
sess = rt.InferenceSession(“full-pipeline.onnx”) # Start the inference session and open the model
xin = input_example.astype(np.float32) # Use the input_example from block 0 as input
yhatlog = np.array(yhatlog).flatten()
result = sess.run([“result”], {“x”: xin}) # Compute the standardized output
print(“Check:”)
print(“Example {} is appealing: {}.”.format(xin, result))

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

При просмотре с помощью Netron наш результирующий конвейер ONNX выглядит так:

Обратите внимание, что возможность комбинировать части графов в другие графы при условии совпадения входных и выходных данных позволяет легко сохранять общие операции предварительной и постобработки в ONNX и повторно использовать их в различных проектах.

5. Развертывание.

Выше мы надеемся показать, что ONNX - это не просто какой-то абстрактный формат файла, который используется внутри различных сложных инструментов обучения ИИ. ONNX также упрощает создание конвейеров предварительной и постобработки вручную путем объединения вручную созданных блоков ONNX. Таким образом, ONNX - это суперэффективный инструмент для создания конвейеров анализа данных, которые можно использовать (и повторно использовать) где угодно.

Если у вас есть конвейер ONNX, для его развертывания доступны различные варианты:

  1. Вы можете использовать onnxruntime (это то, что мы использовали в некоторых быстрых тестах выше). Если вы хотите выполнить развертывание в облаке, можно создать простую конечную точку REST (например, Flask), которая выполняет onnxruntime, и развернуть ее с помощью Docker. Хотя это относительно просто, это также часто довольно неэффективно (и потребляет много памяти).
  2. Вы можете использовать инструменты, подобные тем, которые предлагает Scailable, чтобы перенести вашу модель ONNX в WebAssembly для чрезвычайно эффективного развертывания. Фактически, благодаря своему уровню детализации ONNX позволяет автоматически генерировать автономные исполняемые файлы на языках нижнего уровня на основе графика ONNX. Это обеспечивает собственную скорость выполнения (при переносе, например, на c) и (при переносе на переносную цель, например .wasm) позволяет перемещать ту же модель из облака на периферию и даже на небольшие (I) устройства IoT.

Довольно круто.

В заключение, просто небольшое сравнение размера и скорости между двумя вариантами развертывания, упомянутыми выше:

  1. Полный конвейер ONNX, который мы создали выше, потребляет немного меньше 1 КБ памяти. Однако для работы самому onnxruntime требуется чуть более 200 M b. Используя этот объем памяти, я могу на своем локальном компьютере выполнить наш конвейер 1000 раз за 1,9 секунды (при каждом перезапуске сеанса заново).
  2. При преобразовании в WebAssembly (как это делается из коробки с помощью Scailable для любого графа ONNX) объем памяти составляет около 70 КБ для двоичного файла .wasm (что больше, чем в спецификации .onnx, поскольку он включает функциональная спецификация необходимых операторов) но только 60Кб для времени выполнения. Таким образом, всего менее ‹0,2 Мб. Используя этот объем памяти, я могу на том же локальном компьютере сгенерировать 1000 выводов за 0,7 секунды (аналогично перезагружая каждый раз заново; без перезагрузки разница во времени между временем выполнения ONNX и WebAssembly фактически равна 0, так как оба после инициализации запускаются почти родная скорость).

Так что да, объединение ONNX с WebAssembly обеспечивает выразительность (ONNX) и эффективность (WASM) для всех целей.

Так что да, объединение ONNX с WebAssembly обеспечивает выразительность (ONNX) и эффективность (WASM) для всех целей.

Надеюсь, вам понравился этот урок; не стесняйтесь обращаться с любыми вопросами, связанными с развертыванием ONNX / WebAssembly!

Отказ от ответственности

Приятно отметить мое личное участие в этом: я профессор Data Science в Jheronimus Academy of Data Science и один из соучредителей Scailable. Таким образом, без сомнения, я кровно заинтересован в Scailable; Я заинтересован в том, чтобы он разрастался, чтобы мы наконец смогли внедрить ИИ в производство и выполнить свои обещания. Высказанные здесь мнения являются моими собственными.