вступление

Это часть серии сообщений в блогах, посвященных внедрению искусственного интеллекта. Если вас интересует предыстория этой истории или ее развитие:

#1) Как парсить локальные результаты Google с помощью искусственного интеллекта?
#2) Реальный пример машинного обучения на Rails
#3) Советы и сравнения по обучению ИИ
#4) Машинное обучение парсингу с помощью Rails
#5) Реализация моделей ONNX в Rails

На этой неделе мы поговорим о том, как преобразовать файл pth в файл onnx, чтобы использовать обученную модель в производстве для улучшения синтаксического анализа SerpApi's Google Local Results Scraper API. Затем мы будем использовать гем ONNX Runtime Ruby для запуска файла onnx. Вот ментальная карта процесса, на которую можно ссылаться:

I — Необходимые переменные для преобразования в ONNX

Чтобы преобразовать файл pth в файл onnx, нам нужно несколько вещей для переноса состояния модели. Во-первых, нам нужно создать модель в коде транслятора. Это означает, что нам потребуются необходимые входные данные для запуска нашей модели. Давайте посмотрим, что нужно для построения модели:

    def initialize(vocab_size, embed_dim, num_class)
      super()
      @embedding = Torch::NN::EmbeddingBag.new(vocab_size, embed_dim, sparse: true)
      @fc = Torch::NN::Linear.new(embed_dim, num_class)
      init_weights
    end

В этом случае мы сохраним переменные vocab_size, embed_dim и nun_class и запишем их в файл json для чтения из другого файла.

    def initialize(vocab_size, embed_dim, num_class)
      super()
      @embedding = Torch::NN::EmbeddingBag.new(vocab_size, embed_dim, sparse: true)
      @fc = Torch::NN::Linear.new(embed_dim, num_class)
      init_weights
    end

Нам также нужен пример входных данных или входных данных для имитации пересылки модели. Наша функция переадресации выглядит следующим образом:

    def forward(text, offsets)
      embedded = @embedding.call(text, offsets: offsets)
      @fc.call(embedded)
    end

Итак, нам нужен пример text и offsets:

  def self.save_model_inputs text, offsets
    path = "ml/google/local_pack/predict_value/trained_models/translator/translator.json"
    data = File.read(path)
    data = JSON.parse(data)
    data['text'] = text.to_a
    data['offsets'] = offsets.to_a
    File.write(path, JSON.pretty_generate(data))
  end

Все эти функции интегрированы в тренировочный файл. Вот результат созданного файла JSON:

{
  "vocab_size": 24439,
  "embed_dim": 128,
  "nun_class": 9,
  "text": [
    2,
    143,
    3,
    12211,
    140,
    144,
    6259,
    2,
    ...
  },
  "offsets": [
    0,
    7,
    14,
    29,
    44,
    51,
    56,
    63,
    70,
    77,
    80,
    83,
    ...
  ]
}

Наконец, поскольку мы используем модель n-gram, нам нужен словарь, который использует модель:

  def self.save_vocab vocab
    path = "ml/google/local_pack/predict_value/trained_models/n_gram_value_predictor_vocab.json"
    data = JSON.parse(vocab.to_json)
    File.write(path, JSON.pretty_generate(data))
  end

Вот результирующий файл JSON, содержащий различные аспекты vocabulary, используемые в обученной модели. Нас будет интересовать именно ключ stoi.

{
  "freqs": {
    "mcdonald": 2,
    "'": 334,
    "s": 309,
    "mcdonald '": 1,
    "' s": 293,
    "starbucks": 5,
    "goza": 1,
    "espresso": 43,
    ...
  },
  "itos": [
    "<unk>",
    "<pad>",
    "(",
    ")",
    ",",
    "in",
    "·",
    "· in",
    "+1",
    ...
  ],
  "unk_index": 0,
  "stoi": {
    "<unk>": 0,
    "<pad>": 1,
    "(": 2,
    ")": 3,
    ",": 4,
    "in": 5,
    "·": 6,
    "· in": 7,
    "+1": 8,
    "united": 9,
    "states": 10,
    "united states": 11,
    ...
   },
   "vectors": null
 }

II — Преобразование в ONNX

На стороне ruby отсутствует поддержка преобразования pth в onnx. К счастью, у нас есть план по обучению моделей локально и использованию обученных моделей в виде ONNX файлов в производственной среде. Это не мешает нам использовать файл python для процесса преобразования. Фактически, именно поэтому мы использовали файл JSON для хранения preliminary variables для преобразования. Эта часть требует выполнения PyTorch и python. Вот требования:

import torch
import torch.nn as nn
import torch.nn.init as init
import json

Нам также нужно воссоздать модель в python. Воссоздать совсем не сложно. Вот модель в ruby:

  class GLocalNet < Torch::NN::Module
    def initialize(vocab_size, embed_dim, num_class)
      super()
      @embedding = Torch::NN::EmbeddingBag.new(vocab_size, embed_dim, sparse: true)
      @fc = Torch::NN::Linear.new(embed_dim, num_class)
      init_weights
    end
    def init_weights
      initrange = 0.5
      @embedding.weight.data.uniform!(-initrange, initrange)
      @fc.weight.data.uniform!(-initrange, initrange)
      @fc.bias.data.zero!
    end
    def forward(text, offsets)
      embedded = @embedding.call(text, offsets: offsets)
      @fc.call(embedded)
    end
  end

Итак, вот его реконструкция в python:

class GLocalNet(nn.Module):
    def __init__(self, vocab_size, embed_dim, num_class):
      super(GLocalNet, self).__init__()
      self.embedding = nn.EmbeddingBag(vocab_size, embed_dim, sparse=True)
      self.fc = nn.Linear(embed_dim, num_class)
      self.init_weights()
    def init_weights(self):
      initrange = 0.5
      self.embedding.weight.data.uniform_(-initrange, initrange)
      self.fc.weight.data.uniform_(-initrange, initrange)
      self.fc.bias.data.zero_()
    def forward(self, text, offsets):
      embedded = self.embedding(text, offsets)
      return self.fc(embedded)

После этого вызовем нужные переменные из ранее созданного и заполненного JSON-файла:

json_path = "ml/google/local_pack/predict_value/trained_models/translator/translator.json"
f = open(json_path)
data = json.load(f)
#Constructors
vocab_size = data['vocab_size']
embed_dim = data['embed_dim']
nun_class = data['nun_class']
#Inputs
text = torch.tensor(data['text'])
offsets = torch.tensor(data['offsets'])
def forward(self, text, offsets):
      embedded = self.embedding(text, offsets)
      return self.fc(embedded)

У PyTorch отличный модуль, onnx.export. Задаем необходимые параметры для конвертации.

file_path = "ml/google/local_pack/predict_value/trained_models/n_gram_value_predictor.pth"
model = GLocalNet(vocab_size, embed_dim, nun_class)
model.load_state_dict(torch.load(file_path))
model.eval()
torch.onnx.export( model,
                  (text, offsets),
                  "ml/google/local_pack/predict_value/trained_models/n_gram_value_predictor.onnx",
                  export_params=True,
                  opset_version=11,
                  do_constant_folding=True,
                  input_names = ['text', 'offsets'],
                  output_names = ['label'],
                  dynamic_axes={  'text' : {0 : 'batch_size'},
                                  'offsets': {0 : 'batch_size'},
                                  'label' : {0 : 'batch_size'}  })

Вот разбивка процесса преобразования:

model: модель, которую мы загрузили ранее.
(text, offsets): пример входных данных.
"ml/google/local_pack/predict_value/trained_models/n_gram_value_predictor.onnx": путь для сохранения модели onnx.
export_params=True: параметр для хранения весов обученных параметров.
opset_version=11: onnx версия использовать. Мы выбрали 11 для поддержки EmbeddingBag, который находится в модели.
do_constant_folding=True: параметр для выполнения свертывания констант для оптимизации.
input_names = ['text', 'offsets']: входные имена, которые будут использоваться при вызове преобразованной модели onnx.
output_names = ['label']: выходные данные имена, которые будут использоваться при вызове конвертированной модели onnx.
dynamic_axes={'text':{0:'batch_size'}, 'offsets': {0:'batch_size'}, 'label':{0:'batch_size'}}: параметр для сохранения динамики входов и выходов.

III — Реализация ONNX в Ruby on Rails

Мне посчастливилось понять, что итератор словаря для torchtext-ruby не зависит от самого torch. Чтобы реализовать модель для production целей, у нас не может быть больших файлов для такого решения. Так что с небольшой настройкой я смог создать загрузчик ввода, который берет текст и переводит его в массив, используя vocab, который мы сохранили ранее в training.

def create_input text
    def ngrams_iterator(token_list, ngrams)
      return enum_for(:ngrams_iterator, token_list, ngrams) unless block_given?
  
      get_ngrams = lambda do |n|
        (token_list.size - n + 1).times.map { |i| token_list[i...(i + n)] }
      end
  
      token_list.each do |x|
        yield x
      end
  
      2.upto(ngrams) do |n|
        get_ngrams.call(n).each do |x|
          yield x.join(" ")
        end
      end
    end
    def basic_english_normalize(line)
      line = line.downcase
  
      @patterns_dict.each do |pattern_re, replaced_str|
        line.sub!(pattern_re, replaced_str)
      end
      line.split
    end
    def vocab_operation vocab, token
      if vocab[token].present?
        vocab[token]
      else
        vocab[token] = vocab.values.last + 1
        vocab[token]
      end
    end
  
    _patterns = [%r{\'}, %r{\"}, %r{\.}, %r{<br \/>}, %r{,}, %r{\(}, %r{\)}, %r{\!}, %r{\?}, %r{\;}, %r{\:}, %r{\s+}]
    _replacements = [" \'  ", "", " . ", " ", " , ", " ( ", " ) ", " ! ", " ? ", " ", " ", " "]
  
    @patterns_dict = _patterns.zip(_replacements)
  
    vocab_path = "ml/google/local_pack/predict_value/trained_models/n_gram_value_predictor_vocab.json"
    data = File.read(vocab_path)
    data = JSON.parse(data)
    vocab = data['stoi']
    ngrams = 2
    new_vocab = []
  
    arr = ngrams_iterator(basic_english_normalize(text), ngrams).map { |token| vocab_operation vocab, token }
    arr
  end

Вот функция для предсказания метки из строки:

def get_prediction text
    local_pack_label = {
      0 => "title",
      1 => "rating",
      2 => "reviews",
      3 => "type",
      4 => "phone",
      5 => "address",
      6 => "hours",
      7 => "price",
      8 => "description"
    }
    path = "ml/google/local_pack/predict_value/trained_models/n_gram_value_predictor.onnx"
    model = OnnxRuntime::Model.new(path)
    output = model.predict(text: create_input(text), offsets: [0])
    result = local_pack_label[output['label'][0].find_index(output['label'][0].max)]
    result.to_sym
  end

get_prediction команда:

  • берет text,
  • преобразует его в массив, используя vocab,
  • загружает его в модель с помощью ONNX file,
  • получает массив result, состоящий из различных чисел с плавающей запятой, представляющих вероятность меток по порядку,
  • берет индекс максимальной вероятности и запускает его в хэше local_pack_label,
  • предсказывает его label и возвращает его как symbol.

IV — Путь вперед

Эту вспомогательную функцию можно напрямую реализовать в наших парсерах. На следующей неделе мы поговорим о расширении словаря для n-грамм, реализации этой функции в наших парсерах и сравнении результатов JSON с использованием parser enhanced with predictive model и traditional parser. Мы также упомянем вариант использования Machine Learning на Rspec.

Я хотел бы поблагодарить смелых и блестящих людей из SerpApi за всю их поддержку, особенно в эти trying times. Также я благодарен читателю за внимание.

Благодарности:
* Использованные драгоценные камни:
torch.rb, https://github.com/ankane/torch.rb
torchtext-ruby, https://github.com/ ankane/torchtext-ruby
onnxruntime-ruby, https://github.com/ankane/onnxruntime-ruby
* Используемые библиотеки C++:
LibTorch 1.10.2, Linux, CUDA 10.2, cxx11 ABI, https://pytorch.org/get-started/locally/
* Материалы перепрофилированы из:
Документация, https://pytorch.org/tutorials/intermediate/char_rnn_classification_tutorial .html
Учебник, https://docs.microsoft.com/en-us/windows/ai/windows-ml/tutorials/pytorch-convert-model

Первоначально опубликовано на https://serpapi.com 2 марта 2022 г.