Объединение графиков ONNX

Объединяйте, объединяйте, разделяйте и объединяйте графики ONNX с помощью sclblonnx.

ONNX становится все более популярным. Хотя изначально он задумывался преимущественно как формат файлов для простого хранения моделей AI/ML, в последние годы его использование изменилось. В настоящее время мы видим, что многие специалисты по данным используют ONNX в качестве средства для создания и курирования полных конвейеров обработки данных. По мере роста использования ONNX растет и потребность в хороших инструментах для создания, проверки и редактирования графиков ONNX. К счастью, появляется большая экосистема для ONNX; в этом посте мы описываем функции соединения, разделения, слияния и конкатенации ONNX, предлагаемые пакетом sclblonnx (куратор Scailable). Обратите внимание, что слияние, разделение и объединение графиков ONNX чрезвычайно полезно, когда вы активно курируете полезные подграфы ONNX: т. е. у вас могут быть предпочтительные этапы предварительной и постобработки в конвейере данных, хранящемся в формате ONNX, и вы хотите чтобы соединить эти подграфы с моделью, которую вы только что обучили в TensorFlow или PyTorch. В этом посте я попытаюсь объяснить, как это можно сделать.

Примечание: я уже писал о редактировании и слиянии ONNX, см. https://towardsdatascience.com/creating-editing-and-merging-onnx-pipelines-897e55e98bb0. Однако с выпуском sclblonnx 0.1.9 функциональные возможности были значительно расширены.

Некоторая предыстория ONNX

Перед обсуждением новых функций merge, concat, split и join для графиков ONNX, предоставляемых sclblonnx 0.1.9, полезно немного рассказать о графах ONNX. В этом месте текста я предполагаю, что вы знаете некоторые основы ONNX (если нет, см. эту статью или эту). Таким образом, вы знаете, что ONNX предоставляет описание ориентированного вычислительного графа, который указывает, какие операции выполнять над входными тензорами (строго типизированными) для получения желаемого выходного тензора. И вы знаете, что ONNX полезен для хранения обученных моделей AI/ML, и для создания конвейеров обработки данных таким образом, который не зависит от платформы и цели развертывания.То есть, Вы обычно знаете, что ONNX — это забавная штука.

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

Неявное создание края по имени

Начнем с создания краев. Хотя граф ONNX — это просто ориентированный граф, и поэтому его можно описать своими узлами и ребрами, мы не создаем (и не храним) граф ONNX иначе. При создании графа ONNX мы не создаем явно матрицу смежности для определения ребер между узлами. Вместо этого мы создаем узлы некоторых type (разных операторов), каждый с именованными input и output. Это также все, что хранится в файле ONNX (который на самом деле является просто protobuf): файл хранит список типов операторов, каждый из которых имеет свои собственные именованные входы и выходы. Имена в конце позволяют построить ребра в графе: если узел n1 имеет выход с именем x1, а узел n2 имеет вход с именем x1, будет создано (направленное) ребро между n1 и n2. Если впоследствии добавить еще один узел n3, который также имеет именованный вход x1, мы получим следующий график:

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

Если одно и то же имя встречается на обоих графиках, небрежное слияние графиков приведет к рисованию потенциально нежелательных ребер.

Ввод и вывод на график

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

Хорошо, это было немного загадочно.

Приведем несколько примеров, используя следующие обозначения:

I1(name)  # An input (to the graph) with a specific name
O1(name)  # An output (to the graph) with a specific name
N1({name, name, ...}, {name, name, ...}) # A node, with a list of inputs and outputs.

Учитывая это обозначение, мы можем, например, обозначить

# A simple graph:
I1(x1)
I2(x2)
O3(x3)
N1({x1,x2},{x3})

Что сгенерирует (используя оранжевый цвет для входов и выходов) следующий график:

Если N1 является оператором Add, этот граф будет просто закодирован добавлением двух тензоров.

Давайте сделаем немного более сложный график:

I1(x1)
I2(x2)
N1({x1, x2}, {x3})
N2({x2, x3}, {x4})
O1(x3)
O2(x4)

Что графически приведет к:

Итак, теперь нам ясно, как строятся внутренние ребра, а также входы и выходы графа; давайте поближе познакомимся с инструментами в пакете sclblonnx!

Управление графиками ONNX с помощью sclblonnx

Начиная с обновления до версии 0.1.9, пакет sclblonnx содержит ряд служебных функций более высокого уровня для объединения нескольких (под)графов ONNX в один граф. Хотя более ранние версии пакета уже содержали функцию merge для эффективного склеивания двух графиков (подробнее об этом позже), обновление представляет merge, join и split в качестве оболочек более высокого уровня вокруг гораздо более универсальной и сложной в использовании функции, называемой concat. Начнем с функций более высокого уровня.

Все описанные здесь функции можно найти в коде Python в примерах, представленных с пакетом sclblonnx. Их можно найти по адресу https://github.com/scailable/sclblonnx/blob/master/examples/example_merge.py. Также ознакомьтесь с документацией каждой из обсуждаемых функций: https://github.com/scailable/sclblonnx/blob/master/sclblonnx/merge.py

Объединить

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

merge(sg1, sg2, io_match)

Где sg1 — родительский подграф, sg2 — дочерний, а io_match представляет список пар имен выходов sg1, которые необходимо сопоставить с inputs вне sg2. Таким образом, учитывая наши развитые обозначения в предыдущем разделе, если мы имеем:

# Parent (sg1)
I1(x1)
N1({x1},{x2})
O1(x2)
# Child (sg2)
I2(z1)
N2({z1},{z2})
O2(z2)

Вызов merge(sg1, sg2, [(x2,z1)]) создаст:

I1(x1)
N1({x1},{x2})
N2({x2},{z2})
O2(z2)

Однако, как вы понимаете, с помощью этой функции мы можем делать гораздо более универсальные слияния. Обратите внимание, что merge предполагает отсутствие «конфликтов» во внутреннем именовании двух графиков; если это не так и вы хотели бы контролировать это поведение более детально, рекомендую использовать concat; слияние - это просто удобная оболочка вокруг concat.

Split & Join

Подобно merge, split и join также являются оболочками более высокого уровня вокруг concat (о которых мы подробно расскажем ниже). Поведение относительно простое:

  • split берет один «родительский» с несколькими выходами и вставляет один подграф в подмножество этих выходов (сопоставляя входы подграфа), а другой подграф — в другое подмножество выходных данных родителя. Таким образом, фактически split создает ветвь, в которой родительский граф передает два дочерних.
  • join во многих отношениях противоположен расщеплению: требуется два «родителя» и только один ребенок. Родители сопоставляются своими выводами с входами дочернего элемента. Следовательно, join эффективно соединяется с ветвями подграфов в более крупном дереве.

Конкат

Рабочая лошадка для функций merge, split и join, описанных выше, — гораздо более универсальная функция concat. Самый простой способ понять, как он работает, — это, пожалуй, взглянуть на подпись и документы:

def concat(
  sg1: xpb2.GraphProto,        
  sg2: xpb2.GraphProto,        
  complete: bool = False,        
  rename_nodes: bool = True,        
  io_match: [] = None,        
  rename_io: bool = False,        
  edge_match: [] = None,        
  rename_edges: bool = False,        
  _verbose: bool = False,       
  **kwargs):    
"""  concat concatenates two graphs.    
Concat is the flexible (but also rather complex) workhorse for the merge, join, and split functions and can be used to pretty flexibly paste together two (sub)graphs. Contrary to merge, join, and split, concat does not by default assume the resulting onnx graph to be complete (i.e., to contain inputs and outputs and to pass check()), and it can thus be used as an intermediate function when constructing larger graphs.    
Concat is flexible and versatile, but it takes time to master. See example_merge.py in the examples folder for a number of examples.    Args:        
  sg1: Subgraph 1, the parent.        
  sg2: Subgraph 2, the child.        
  complete: (Optional) Boolean indicating whether the resulting     
             graph should be checked using so.check(). 
             Default False.         
  rename_nodes: (Optional) Boolean indicating whether the names of 
             the nodes in the graph should be made unique. 
             Default True.          
  io_match: (Optional) Dict containing pairs of outputs of sg1 that 
             should be matched to inputs of sg2. 
             Default [].        
  rename_io: (Optional) Boolean indicating whether the inputs and 
             outputs of the graph should be renamed. 
             Default False.        
  edge_match: (Optional) Dict containing pairs edge names of sg1 
             (i.e., node outputs) that should be matched to edges of 
             sg2 (i.e., node inputs). 
             Default [].        
  rename_edges: (Optional) Boolean indicating whether the edges    
             should be renamed. 
             Default False        
  _verbose: (Optional) Boolean indicating whether verbose output 
             should be printed (default False)    
Returns: The concatenated graph g, or False if something goes wrong along the way.    
"""
## The implementation...

Как видно из сигнатуры, оболочка более высокого уровня merge просто вызывает concat один раз, в значительной степени с аргументами по умолчанию. Каждая из функций split и join вызывает concat дважды для достижения желаемого результата. Обратите внимание, что аргумент rename_edges позволяет контролировать, следует ли переименовывать все ребра в подграфе (таким образом избегая возможного нежелательного неявного создания ребер), тогда как complete позволяет использовать слияние для работы с частичными графами (т. е. графы, у которых еще не определены все ребра.

Заворачивать

Я надеюсь, что вышеизложенное пролило свет на огромные возможности, которые может предложить ONNX, а также на некоторые инструменты для манипулирования графиками ONNX вручную. Мы считаем, что ONNX отлично подходит для хранения (под)графов, которые выполняют полезные биты, связанные с конвейером обработки данных, независимо от платформы. Такие инструменты, как пакет sclblonnx, впоследствии позволяют пользователям создавать полные конвейеры, используя подграфы в качестве строительных блоков.

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

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

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