Как сделать бота, который превращает фото в комикс. Часть третья. Serverless GPU хостинг модели

от автора

-> Часть 1
-> Часть 2

Ну, отдохнули и хватит. С возвращением!

В предыдущих сериях мы с вами собрали данные и обучили свою первую модель.
Затем, ужаснувшись результатам, обучили еще с десяток.
Самое время показать наше творение миру!

Экспорт модели

Для начала пересохраним модель генератора в подходящий формат, чтобы нам не пришлось тащить декларации классов на хостинг.

Создадим небольшой файлик с расширением *.py и скопируем в него код из-под спойлера ниже.
Пусть это будет jit.py:

Код, который нужно бездумно скопипастить, заменив path, output_path на свои

# Заменяем path на путь к понравившейся итерации модели # Нам нужен файл *G_A.pth - генератор фото -> комикс # output_path - имя файла для экспортируемой модели # может быть любым с расширением *.jit, главное - не забыть, куда мы его сохранили  path= '/checkpoints/resnet9_nowd_nodo_128to400_c8/60_net_G_A.pth' output_path ='/checkpoints/resnet9_nowd_nodo_128to400_c8/resnet9_nowd_nodo_128to400_c8_160-50-60_1.jit'  import torch from torch import nn  class ResnetGenerator(nn.Module):     """Resnet-based generator that consists of Resnet blocks between a few downsampling/upsampling operations.      We adapt Torch code and idea from Justin Johnson's neural style transfer project(https://github.com/jcjohnson/fast-neural-style)     """      def __init__(self, input_nc, output_nc, ngf=64, norm_layer=nn.BatchNorm2d, use_dropout=False, n_blocks=6, padding_type='reflect'):         """Construct a Resnet-based generator          Parameters:             input_nc (int)      -- the number of channels in input images             output_nc (int)     -- the number of channels in output images             ngf (int)           -- the number of filters in the last conv layer             norm_layer          -- normalization layer             use_dropout (bool)  -- if use dropout layers             n_blocks (int)      -- the number of ResNet blocks             padding_type (str)  -- the name of padding layer in conv layers: reflect | replicate | zero         """         assert(n_blocks >= 0)         super(ResnetGenerator, self).__init__()         if type(norm_layer) == functools.partial:             use_bias = norm_layer.func == nn.InstanceNorm2d         else:             use_bias = norm_layer == nn.InstanceNorm2d          model = [nn.ReflectionPad2d(3),                  nn.Conv2d(input_nc, ngf, kernel_size=7, padding=0, bias=use_bias),                  norm_layer(ngf),                  nn.ReLU(True)]          n_downsampling = 2         for i in range(n_downsampling):  # add downsampling layers             mult = 2 ** i             model += [nn.Conv2d(ngf * mult, ngf * mult * 2, kernel_size=3, stride=2, padding=1, bias=use_bias),                       norm_layer(ngf * mult * 2),                       nn.ReLU(True)]          mult = 2 ** n_downsampling         for i in range(n_blocks):       # add ResNet blocks              model += [ResnetBlock(ngf * mult, padding_type=padding_type, norm_layer=norm_layer, use_dropout=use_dropout, use_bias=use_bias)]          for i in range(n_downsampling):  # add upsampling layers             mult = 2 ** (n_downsampling - i)             model += [nn.ConvTranspose2d(ngf * mult, int(ngf * mult / 2),                                          kernel_size=3, stride=2,                                          padding=1, output_padding=1,                                          bias=use_bias),                       norm_layer(int(ngf * mult / 2)),                       nn.ReLU(True)]         model += [nn.ReflectionPad2d(3)]         model += [nn.Conv2d(ngf, output_nc, kernel_size=7, padding=0)]         model += [nn.Tanh()]          self.model = nn.Sequential(*model)      def forward(self, input):         """Standard forward"""         return self.model(input)  class ResnetBlock(nn.Module):     """Define a Resnet block"""      def __init__(self, dim, padding_type, norm_layer, use_dropout, use_bias):         """Initialize the Resnet block          A resnet block is a conv block with skip connections         We construct a conv block with build_conv_block function,         and implement skip connections in <forward> function.         Original Resnet paper: https://arxiv.org/pdf/1512.03385.pdf         """         super(ResnetBlock, self).__init__()         self.conv_block = self.build_conv_block(dim, padding_type, norm_layer, use_dropout, use_bias)      def build_conv_block(self, dim, padding_type, norm_layer, use_dropout, use_bias):         """Construct a convolutional block.          Parameters:             dim (int)           -- the number of channels in the conv layer.             padding_type (str)  -- the name of padding layer: reflect | replicate | zero             norm_layer          -- normalization layer             use_dropout (bool)  -- if use dropout layers.             use_bias (bool)     -- if the conv layer uses bias or not          Returns a conv block (with a conv layer, a normalization layer, and a non-linearity layer (ReLU))         """         conv_block = []         p = 0         if padding_type == 'reflect':             conv_block += [nn.ReflectionPad2d(1)]         elif padding_type == 'replicate':             conv_block += [nn.ReplicationPad2d(1)]         elif padding_type == 'zero':             p = 1         else:             raise NotImplementedError('padding [%s] is not implemented' % padding_type)          conv_block += [nn.Conv2d(dim, dim, kernel_size=3, padding=p, bias=use_bias), norm_layer(dim), nn.ReLU(True)]         if use_dropout:             conv_block += [nn.Dropout(0.5)]          p = 0         if padding_type == 'reflect':             conv_block += [nn.ReflectionPad2d(1)]         elif padding_type == 'replicate':             conv_block += [nn.ReplicationPad2d(1)]         elif padding_type == 'zero':             p = 1         else:             raise NotImplementedError('padding [%s] is not implemented' % padding_type)         conv_block += [nn.Conv2d(dim, dim, kernel_size=3, padding=p, bias=use_bias), norm_layer(dim)]          return nn.Sequential(*conv_block)      def forward(self, x):         """Forward function (with skip connections)"""         out = x + self.conv_block(x)  # add skip connections         return out  import functools  norm_layer = functools.partial(nn.InstanceNorm2d, affine=False, track_running_stats=False)  model = ResnetGenerator(3,3,64, norm_layer=norm_layer, use_dropout=False, n_blocks=9)  model.load_state_dict(torch.load(path)) model.eval() model.cuda() model.half()  trace_input = torch.ones(1,3,256,256).cuda() model = model.eval() jit_model = torch.jit.trace(model.float(), trace_input)  torch.jit.save(jit_model, output_path)

Заменяем переменные на свои:

  • path — путь к понравившейся итерации модели.
    Нам нужен файл *G_A.pth — генератор фото -> комикс.
  • output_path — имя файла для экспортируемой модели, может быть любым, с расширением *.jit, главное — не забыть, куда мы его сохранили.

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

Далее идем в консоль, переходим в папку с нашим файлом и пишем:

python jit.py 

Voila! Модель готова к знакомству с внешним миром.

Подробнее про torch jit

Документация: https://pytorch.org/docs/stable/jit.html
Если быть кратким, экспорт в jit позволяет сериализовать модель и не тащить за собой среду python, все зависимости и внешние модули, которые она могла использовать. Кроме torch, разумеется.
Хоть мы и будем хостить ее в python-среде, jit-модели можно использовать в самостоятельных приложениях.

Выбор хостинга

Буду предельно откровенен и сразу признаюсь: мой внутренний deep learning enthusiast погиб где-то на втором часу изучения возможностей хостинга с поддержкой GPU. Так что если кто-то подскажет мне недорогой Serverless GPU хостинг, я буду более чем признателен.

Платить за полноценный сервер для своих экспериментов я не планировал, поэтому искал только serverless решения.

После мучительных поползновений по многостраничным тарифным планам гугла и амазона мой выбор пал на algorithmia.com

Причин несколько:

  • Web IDE — идеальный вариант для чайников, хоть и жутко медленный, так как для проверки приходится ждать окончания билда. Вне рамок этого туториала я бы рекомендовал тестировать все локально, так как большинство ошибок возникает на этапе загрузки и сохранения файлов.

  • Минимальное количество опций — сложно умереть от преждевременной старости, не дочитав до конца список вариантов.

  • Ну и последний аргумент — за почти уже полгода экспериментов я до сих пор не потратил бесплатный стартовый баланс.

Хоть сейчас при регистрации персонального аккаунта и дают меньше, чем прошлым летом, этого все равно должно хватить на некоторое время, и уж точно на все наши эксперименты. В конце месяца накидывают еще кредитов, что не раз спасало меня от потенциального разорения.

Из минусов стоит отметить, что видеокарты там — только старые Tesla K80 на 12GB RAM, что накладывает соответствующие ограничения. В любом случае, пока мы доберемся до продакшена, мы уже будем понимать, что нам нужно от сервера.

Деплой модели

Ну что, в бой!

Регистрация

Идем на https://algorithmia.com/signup и регистрируемся. Не уверен, что есть разница, какую профессию\тип аккаунта выбирать, но если вы найдете золотоносное комбо, дающее максимум кредитов, обязательно дайте знать в комментах!

Загрузка модели

После регистрации мы окажемся в своем профиле.
Нам нужно создать папки для модели и картинок, которые она сгенерирует.
Для этого выбираем Data Sources в меню слева.

Кликаем New Data Source -> Hosted Data Collection
Назовем папку “My Models”.
В результате нас должно перекинуть на страницу со списком наших папок.

Создадим еще одну папку: New Collection -> “photo2comics_out”

Самое время загрузить нашу свежеэкспортированную модель!
Переходим в папку My Models и перетаскиваем файл с моделью в браузер, либо выбираем Upload Files из меню.

Теперь скопируем ссылку на нашу модель, она пригодится нам ниже. Для этого кликнем на троеточие справа от имени файла.

С данными покончено, переходим к непосредственно алгоритму.

Алгоритм

Возвращаемся в профиль по клику на Home в меню слева.

Далее кликаем Create New -> Algorithm и выбираем имя нашего алгоритма. Остальные опции заполняем как на картинке ниже.

Картинка ниже

Нажимаем Create New Algorithm и выбираем WebIDE в появившемся окошке.
Если вы случайно закрыли попап, исходный код можно открыть, нажав Source Code в меню нашего алгоритма.

Удаляем шаблонный код и вставляем наш:

Еще немного кода для еще более бездумного копирования

import Algorithmia  import torch import torchvision import torchvision.transforms as transforms import cv2 from torch import * import uuid import gc  import requests import numpy as np  client = Algorithmia.client()  # Скачиваем модель по ссылке file_path, сохраняем на наш виртуальный сервер # под именем model_file и загружаем ее. def load_model():     file_path = "{ ссылка на нашу загруженную модель }"     model_file = client.file(file_path).getFile().name     model = torch.jit.load(model_file).half().cuda()     return model  model = load_model().eval() torch.cuda.empty_cache() torch.cuda.ipc_collect() torch.backends.cudnn.benchmark = True  # Скачиваем картинку по ссылке, уменьшаем до нужного размера,  # преобразуем в тензор и нормализуем,  # т.к. модель обучалась на нормализованных картинках  def preprocessing(image_path, max_size):      response = requests.get(image_path)     response = response.content     nparr = np.frombuffer(response, np.uint8)     img_res = cv2.imdecode(nparr, cv2.IMREAD_COLOR)     img_res = cv2.cvtColor(img_res, cv2.COLOR_BGR2RGB)      x = img_res.shape[0]     y = img_res.shape[1]      #if image is bigger than the target max_size, downscale it     if x>max_size and x<=y:         y = y*(max_size/x)         x = max_size      if y>max_size and y<x:         x = x*(max_size/y)         y = max_size      size = (int(y),int(x))     img_res = cv2.resize(img_res,size)      t = Tensor(img_res/255.)[None,:]     t = t.permute(0,3,1,2).half().cuda()      # standartize     t = (t-0.5)/0.5      return(t)  def predict(input):      gc.collect()     with torch.no_grad():         res = model(input).detach()      return res  # Денормализуем картинку обратно, сохраняем со случайным именем # и загружаем в наше хранилище   def save_file(res, file_uri):     #de-standartize     res = (res*0.5)+0.5     tempfile = "/tmp/"+str(uuid.uuid4())+".jpg"     torchvision.utils.save_image(res,tempfile)     client.file(file_uri).putFile(tempfile)  # API calls will begin at the apply() method, with the request body passed as 'input' # For more details, see algorithmia.com/developers/algorithm-development/languages def apply(input):      processed_data= preprocessing(input["in"], input["size"])     res = predict(processed_data)     save_file(res, input["out"])     input = None     res = None     processed_data = None      gc.collect()      return "Success"

Не забудьте вставить ссылку на загруженную модель. Ее мы скопировали в предыдущем разделе, когда загружали модель.

Находясь в WebIDE, справа вверху кликаем на DEPENDENCIES и заменяем текст на список наших зависимостей:

algorithmia>=1.0.0,<2.0 opencv-python six torch==1.3.0 torchvision numpy 

Версия torch должна быть такой же или более новой, чем та, на которой мы сохраняли модель. В противном случаем могут быть ошибки при импорте jit модели.

Нажимаем SAVE, BUILD и ждем завершения билда. Как только в консоли внизу появится сообщение об успешном билде, можем проверить работоспособность модели, отправив в консоль тестовый запрос:

{"in":"https://cdn3.sportngin.com/attachments/photo/9226/3971/JABC-9u_medium.JPG", "out":"data://username/photo2comics_out/test.jpg", "size":512} 

Где {username} — ваш логин. Если все прошло успешно, в консоли появится “Success”, а в папке, которую мы указали (в данном случае — photo2comics_out), появится сгенерированное изображение.

Итоги

Поздравляю, мы дешево и сердито задеплоили нашу скромную модель!

В следующем выпуске мы с вами подружим модель с телеграм-ботом и, наконец, зарелизим уже все это добро.
Если вам не терпится опробовать модель в деле, вы всегда можете ознакомится с официальной документацией: https://algorithmia.com/developers/api/

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

@selfie2animebot — Превращает селфи в аниме
@pimpmyresbot — Увеличивает разрешение х2 (максимум до 1400х1400)
@photozoombot — Создает 3д-зум видео из одного фото
@photo2comicsbot — Собственно, виновник торжества

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

ссылка на оригинал статьи https://habr.com/ru/post/485824/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *