Итак, вы опробовали и протестировали свою модель глубокого обучения, и, похоже, она отлично работает в среде вашего ноутбука Jupyter. Это удивительно! Что теперь? Хотели бы вы оставить веса модели неиспользованными после демонстрации вашего проекта «Proof of Concept» только для того, чтобы отправить его в корзину? Или вы хотите развернуть его в Интернете, чтобы он сохранялся и мог использоваться любым, у кого есть к нему доступ? Конечно, я предполагаю, что вы выберете последний вариант.

В этом блоге мы рассмотрим, как мы можем сделать первые шаги к производству ваших моделей НЛП. Мы создадим сервер gRPC, на котором будет работать модель суммирования текста в фоновом режиме, и мы будем использовать клиент на основе Go для связи с сервером через запросы и ответы gRPC.

Мы можем получить лучшее от Python и Golang, используя gRPC (удаленный вызов процедур Google). Это позволит нам создать простой клиент на golang, и параллельно мы создадим сервер Python на основе gRPC, который будет вызывать конвейер Huggingface для суммирования текста. Клиент go отправит входной длинный текст на сервер, а сервер получит сокращенную версию текста.

Краткий обзор gRPC

В gRPC клиентское приложение может напрямую вызывать метод серверного приложения на другом компьютере, как если бы это был локальный объект, что упрощает создание распределенных приложений и служб. Как и в случае с другими системами на основе RPC, gRPC основан на определении реализации службы, определении методов, которые можно вызывать удаленно с помощью сигнатуры функции apt. На стороне сервера он реализует серверный интерфейс и запускает сервер gRPC для обработки клиентских вызовов. gRPC использует буферы протоколов для сериализации структурированных данных для отправки по сети. Его также можно использовать с другими форматами, такими как JSON. Формат подходит как для эфемерного сетевого трафика, так и для долговременного хранения данных.

Определение протофайлов

Сначала мы начнем наш проект с определения файла summarization.proto, который поможет определить буферы протокола, а также интерфейс службы.

syntax = "proto3";
package pb;
option go_package = "./pb";
message SummaryRequest {
  string request = 1;
}
message SummaryResponse {
  repeated string response = 1;
}
service Summarization {
  rpc GetSummary(SummaryRequest) returns (SummaryResponse);
}
  • Первая строка указывает, что мы используем синтаксис proto3. Если мы этого не сделаем, компилятор буфера протокола будет считать версию proto2.
  • Мы также устанавливаем пакет pb в наш каталог и устанавливаем go_package как относительный путь “./pb”.
  • Наконец, мы определяем сообщения Proto Request и Proto Response в нашем файле. Это не что иное, как спецификация структуры данных, которые будут сериализованы и отправлены по сети.
  • Сводный запрос будет представлять собой одну длинную строку, содержащую полный текст документа.
  • Сводный ответ будет представлять собой массив строк, в которых будут храниться токены сводного ответа. Поскольку вывод представляет собой массив, мы используем термин повторяется перед определением строки. Мы также можем объявить его как одну строковую переменную.

Генерация кода protobuf и gRPC

$ protoc --go_out=. --go_opt=paths=source_relative \\
    --go-grpc_out=. --go-grpc_opt=paths=source_relative \\
    project/summarization.proto

Чтобы скомпилировать файлы .proto и сгенерировать код gRPC, нам нужно запустить вышеупомянутую команду. Команда protoc — это компилятор буферов протокола — формат данных, который использует gRPC. Флаг go_out=plugins=grpc:pb указывает protoc использовать подключаемый модуль gRPC и размещать файлы в каталоге pb. go_opt=paths=source_relative сообщает protoc о необходимости генерировать код в каталоге pb относительно текущего каталога. Файл summarization.proto будет использоваться для создания кода. Это заполнит summarization.pb.go привязками данных и вспомогательными методами.

$ python3 -m grpc_tools.protoc -I.. --python_out=. --grpc_python_out=. ../summarization.proto

Наконец-то мы можем сгенерировать код Python. Это создает код для summarization_pb2_grpc.py и summarization_pb2.py. Мы можем переместить их в файл, где будет записана серверная логика, т.е. в файл server.py .

Логика сервера

from concurrent.futures import ThreadPoolExecutor
from summarization_pb2 import SummaryResponse
from summarization_pb2_grpc import SummarizationServicer, add_SummarizationServicer_to_server
import logging
import grpc 
import torch
import warnings
warnings.filterwarnings(action = 'ignore')
from transformers import AutoTokenizer, AutoModelWithLMHead
class ModelInit:
    def __init__(self, model_name = 't5-base'):
        self.tokenizer = AutoTokenizer.from_pretrained('t5-base')
        self.model = AutoModelWithLMHead.from_pretrained('t5-base')
class SummarizationService(SummarizationServicer):
    def __init__(self):
        pass
    def GetSummary(self, request):
        logging.info(f"Full Text" + self.request)
        text = self.request
        t5Model = ModelInit()
        tokenizer = t5Model.tokenizer
        model = t5Model.model
        inputs = tokenizer.encode(
            "summarize: " + text,
            return_tensors = 'pt',
            max_length = 512,
            truncation = True 
        )
        summary_ids = model.generate(
            inputs, 
            max_length = 150,
            min_length = 80,
            length_penalty=5., num_beams=2
        )
        summary = tokenizer.batch_decode(summary_ids[0], skip_special_tokens=True, clean_up_tokenization_spaces=True)
        resp = SummaryResponse(response = summary)
        return resp
if __name__ == "__main__":
    logging.basicConfig(
        level = logging.INFO,
        format='%(asctime)s - %(levelname)s - %(message)s',
    )
    server = grpc.server(ThreadPoolExecutor())
    add_SummarizationServicer_to_server(SummarizationService, server)
    port = 8080
    server.add_insecure_port(f'[::]:{port}')
    server.start()
    logging.info(f'Server Ready on Port {port}')
    server.wait_for_termination()

Давайте пройдемся по коду построчно:

  1. Мы определяем два отдельных класса — один будет инициализировать для нас модель и токенизатор, а другой — класс SummarizationService, наследующий класс SummarizationServicer, созданный в summarization_pb2_grpc. Все это является частью создания реализации для наших удаленных вызовов процедур.
  2. Чтобы написать службу Python, вам нужно наследоваться от службы SummarizationService, определенной в summarization_pb2_grpc.py, и переопределить метод GetSummary нашей пользовательской реализацией.
  3. GetSummary — это простая функция, которая объявит модель, а также токенизатор для нашей службы суммирования. В этом примере мы используем модель T5ForConditionalGeneration и T5Tokenizer для вывода сгенерированных токенов и токенизации слов соответственно.
  4. Трубопровод довольно прост. Закодируйте данный текст → отправьте его в модель → Преобразуйте сгенерированные идентификаторы обратно в декодированные слова. После чего мы просто встраиваем это в сообщение SummaryResponse, импортированное из сгенерированного файла.
  5. Мы создаем экземпляр сервера в функции main. Мы определяем сервер grpc с исполнителем Threadpool. Сервер gRPC Python в настоящее время будет упаковывать каждый запрос RPC как будущий и отправлять его в пул потоков. Если в пуле потоков закончится доступный поток, он создаст новый и обработает будущее. Другими словами, каждый RPC будет выполняться в выделенном потоке, и если RPC заблокирован, поток также будет заблокирован.
  6. Служба добавления суммирования добавит службу к созданному серверу, и сервер будет запускаться на указанном нами порту до тех пор, пока не будет обнаружено внешнее прерывание.

И это все! Мы готовы написать наш клиентский код, чтобы получить некоторые сводки!

Написание клиентского кода.

package main
import (
	"context"
	"log"
	"main/pb"
	"google.golang.org/grpc"
)
func main() {
	addr := "localhost:8080"
	conn, err := grpc.Dial(addr, grpc.WithInsecure(), grpc.WithBlock())
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()
	client := pb.NewSummarizationClient(conn)
	inp_str := "A theme is any universal idea explored in a literary work. After reading the novel Lolita it became obvious that there were multiple themes occurring throughout the book.In my eyes the most important theme of them all was the power of diction and how Nabokov honored words because they elevated his artwork otherwise dreadful topic. This particular book is known for being risqué, but it is important to note that there are no four-letter words or any obvious graphic material; that's because of Humbert's word choice. The language used in Lolita successfully overcompensates the unadvisable content and allows a sense of beauty to prevail. Subjects such as murder, pedophilia, rape, and even incest are surprisingly appealing due to the way Humbert Humbert narrates each scene with powerful word choice. Humbert uses diction and other forms of diction such as alliteration and imagery to ensure the captivation of his readers, entangling and convincing them into buying his version of the confession."
	req := pb.SummaryRequest{
		Request: inp_str,
	}
	resp, err := client.GetSummary(context.Background(), &req)
	if err != nil {
		log.Fatal(err)
	}
	log.Printf("Predicted Summary: %v", resp.Response)
}

Написать код клиента golang довольно просто.

  1. Сначала мы импортируем файл go, сгенерированный компилятором protoc, в котором определены SummaryRequest и SummaryResponse.
  2. Мы будем набирать соединение с портом, на котором находится сервер, и определять объект клиента. Интерфейс клиента определен в файле summarization.pb.go как
type SummarizationClient interface {
	GetSummary(ctx context.Context, in *SummaryRequest, opts ...grpc.CallOption) (*SummaryResponse, error)
}

3. Далее мы просто определяем строку нашего документа, вызываем функцию GetSummary и распечатываем ответ.

Выход:

Чтобы запросить наш сервер gRPC, мы можем использовать BloomRPC.

Полный код можно найти здесь: https://github.com/AshwinRachha/golangSummarization.

Спасибо за чтение!