Crea tu propio programa de traducción automática con Transformers

Translation into Spanish of an interesting article by Kyle Gallatin, software engineer for machine learning infrastructure and delivery at scale.

automatic translationdeep learningnlptransformerstranslation into spanish
10 June, 2022 Transformers for Machine Translation with NLP model Helsinki.
10 June, 2022 Transformers for Machine Translation with NLP model Helsinki.

A free translation by Chema, a Spain-based translator specializing in English to Spanish translations, with a growing interest in Machine Learning Models and Machine Translation.

An original text written by Kyle Gallatin, originally published in
https://towardsdatascience.com/build-your-own-machine-translation-service-with-transformers-d0709df0791b

* * *

Crea un servicio de traducción automática estandarizado con los últimos modelos PNL de Helsinki disponibles en la biblioteca de Transformers

La traducción automática está siendo muy requerida en sectores empresariales. Las compañías internacionales necesitan compartir documentos, notas, correos electrónicos y otros textos con personas de todo el mundo que utilizan una gran variedad de idiomas.

Podría decirse que es aún más importante la necesidad de democratizar la información, además de la globalización. Independientemente de la lengua que hables, deberías tener las mismas oportunidades de acceder a publicaciones de código abierto, información sobre atención médica, herramientas y conocimientos que aquellos cuyo idioma principal se habla ampliamente, como el inglés.

Hice esto en powerpoint. Es horrible.

Afortunadamente, la traducción automática ya es una realidad: muchos de nosotros hemos usado el traductor de Google para confirmar el contenido de algo en línea o simplemente para sonar inteligente y experimentado. Si tiene necesidades de traducción que requieren escala, Azure, AWS y GCP tienen APIs (Application Programming Interface o Interfaz de programación de aplicaciones) por tan solo 10$ por millón de caracteres que incluyen miles de combinaciones de idiomas (cada combinación compuesta por un idioma de origen y un idioma de destino).

En el espacio de código abierto, se ha facilitado muchísimo el aprendizaje automático entre varios idiomas y hace mucho tiempo que la gente elabora sus propios modelos de traducción automática. Sin embargo, la mayoría de los enfoques parecen dispares en términos de enfoque de modelado y número de idiomas admitidos (según mi propia opinión inexperta).

Recientemente, Huggingface lanzó más de 1000 modelos de lenguaje preentrenados de la Universidad de Helsinki. Para cualquiera que busque crear su propia API de traducción de AWS o del traductor de Google, nunca ha sido más fácil. Así que pensé que me aprovecharía del trabajo duro de otros. Esto es el equivalente de “metamos el aprendizaje automático en una API de Flask y una imagen Docker”. Pero da igual, es divertido. Vamos a ello.

Traducción automática con Transformers

Huggingface ha hecho un trabajo increíble al hacer que los modelos SOTA (de última generación) estén disponibles en una API de Python simple para aquellos codificadores de copiar y pegar como yo. Para traducir texto localmente, solo necesitas pip install transformersy luego usar el siguiente fragmento de los documentos de Transformers.

from transformers import MarianTokenizer, MarianMTModel
from typing import List
src = 'fr' # source language
trg = 'en' # target language
sample_text = "où est l'arrêt de bus ?"
mname = f'Helsinki-NLP/opus-mt-{src}-{trg}'

model = MarianMTModel.from_pretrained(mname)
tok = MarianTokenizer.from_pretrained(mname)
batch = tok.prepare_translation_batch(src_texts=[sample_text]) # don't need tgt_text for inference
gen = model.generate(**batch) # for forward pass: model(**batch)
words: List[str] = tok.batch_decode(gen, skip_special_tokens=True) # returns "Where is the the bus stop ?"

Esto descargará el modelo requerido y traducirá el texto de origen en texto de destino. Con la gran cantidad de idiomas admitidos disponibles en este formato, es muy fácil incluir esto en una aplicación de flask dockerizada y dar por terminado el asunto, pero antes hay algo más que hacer…

Puedes echar un vistazo también al git repo.

Descargar los Modelos

Cuando inicialices el modelo anterior, Transformers descargará los archivos necesarios si no los tiene ya. Esto es increíble para uso local, pero no cuando se usan contenedores docker. Dado que el almacenamiento en contenedores es efímero, cada vez que salimos del contenedor perderemos todos los modelos y tendremos que volver a descargarlos la próxima vez.

Para evitar esto, podemos manejar la descarga del modelo por separado y montar los modelos como un volumen. Esto también nos da más control sobre los idiomas que queremos que admita nuestro servicio desde el principio. Huggingface incluye todos los archivos necesarios en S3, así que podemos estandarizar esto…

HUGGINGFACE_S3_BASE_URL="https://s3.amazonaws.com/models.huggingface.co/bert/Helsinki-NLP"
FILENAMES =["config.json","pytorch_model.bin","source.spm","target.spm","tokenizer_config.json","vocab.json"]
MODEL_PATH = "data"

Y luego crear una utilidad de línea de comando simple para descargarlos:

import os
import argparse
import urllib
from urllib.request import urlretrieve
from config import *

parser = argparse.ArgumentParser()
parser.add_argument('--source', type=str, help='source language code')
parser.add_argument('--target', type=str, help='sum the integers (default: find the max)')

def download_language_model(source,target):
model = f"opus-mt-{source}-{target}"
print(">>>Downloading data for %s to %s model..." % (source, target))
os.makedirs(os.path.join("data",model))
for f in FILENAMES:
try:
print(os.path.join(HUGGINGFACE_S3_BASE_URL,model,f))
urlretrieve(os.path.join(HUGGINGFACE_S3_BASE_URL,model,f),
os.path.join(MODEL_PATH,model,f))
print("Download complete!")
except urllib.error.HTTPError:
print("Error retrieving model from url. Please confirm model exists.")
os.rmdir(os.path.join("data",model))
break

if __name__ == "__main__":
args = parser.parse_args()
download_language_model(args.source, args.target)

Guay. Ahora podemos descargar los modelos que necesitamos con un solo comando. Echa un vistazo al siguiente ejemplo para japonés > inglés.

python download_models.py --source ja --target en

Esto los descargará a un directorio llamado datade forma predeterminada, así que solo asegúrate de que exista.

Traducción de lenguaje dinámico en Python

Ahora que tenemos una manera más fácil de administrar los idiomas admitidos, vayamos al meollo del problema: la clase y los métodos asociados para administrar nuestras traducciones.

Necesitamos un par de funciones:

  • Traducir texto dado un idioma de origen y un idioma de destino
  • Cargar y administrar modelos que no tenemos en la memoria (yo uso un dict simple)
  • Obtener idiomas admitidos (¿qué idiomas descargamos para esta aplicación?)

Gracias a la facilidad de uso de Transformers, casi no lleva tiempo modificar esta funcionalidad en una clase sencilla para que la usemos.

from transformers import MarianTokenizer, MarianMTModel
import os
from typing import List

class Translator():
def __init__(self, models_dir):
self.models = {}
self.models_dir = models_dir

def get_supported_langs(self):
routes = [x.split('-')[-2:] for x in os.listdir(self.models_dir)]
return routes

def load_model(self, route):
model = f'opus-mt-{route}'
path = os.path.join(self.models_dir,model)
try:
model = MarianMTModel.from_pretrained(path)
tok = MarianTokenizer.from_pretrained(path)
except:
return 0,f"Make sure you have downloaded model for {route} translation"
self.models[route] = (model,tok)
return 1,f"Successfully loaded model for {route} transation"

def translate(self, source, target, text):
route = f'{source}-{target}'
if not self.models.get(route):
success_code, message = self.load_model(route)
if not success_code:
return message

batch = self.models[route][1].prepare_translation_batch(src_texts=[text])
gen = self.models[route][0].generate(**batch)
words: List[str] = self.models[route][1].batch_decode(gen, skip_special_tokens=True)
return words

Esta clase se inicializa con la ruta que estamos usando para guardar modelos y se encarga del resto por sí misma. Si recibimos una solicitud de traducción y no tenemos ese modelo en memoria, load_modelsse encarga de cargarlo en el diccionario self.models. Este revisa nuestro directoriodatapara ver si tenemos ese modelo, y nos devuelve un mensaje para avisarnos si lo tenemos o no.

Transformándolo en una API

Todo lo que tenemos que hacer ahora es transformar esto en Flask para que podamos introducir órdenes HTTP.

import os
from flask import Flask, request, jsonify
from translate import Translator
from config import *

app = Flask(__name__)
translator = Translator(MODEL_PATH)

app.config["DEBUG"] = True # turn off in prod

@app.route('/', methods=["GET"])
def health_check():
"""Confirms service is running"""
return "Machine translation service is up and running."

@app.route('/lang_routes', methods = ["GET"])
def get_lang_route():
lang = request.args['lang']
all_langs = translator.get_supported_langs()
lang_routes = [l for l in all_langs if l[0] == lang]
return jsonify({"output":lang_routes})

@app.route('/supported_languages', methods=["GET"])
def get_supported_languages():
langs = translator.get_supported_langs()
return jsonify({"output":langs})

@app.route('/translate', methods=["POST"])
def get_prediction():
source = request.json['source']
target = request.json['target']
text = request.json['text']
translation = translator.translate(source, target, text)
return jsonify({"output":translation})

app.run(host="0.0.0.0")

Para usarlo, simplemente ejecute python app.pyy luego haga llamadas al servicio. Para verificar si está funcionando, puede curl localhost:5000o usar algo más sofisticado, como Postman. Aquí yo uso el servidor de Flask, pero necesitarás un servidor web de producción para usar esto en cualquier lugar real.

Introduce una imagen de Docker

Ahora que nuestra aplicación funciona con base de Python, queremos que funcione con Docker o Docker Compose para que podamos ampliarla tanto como sea necesario. El Dockerfileque se usa para esto es bastante simple. Solo debemos asegurarnos de ejecutar el servicio con el volumen adjunto para que ya tenga acceso a los datos.

Normalmente, usaría una imagen base más pequeña, pero sinceramente, no tenía ganas de depurar:

FROM python:3.6 

WORKDIR /app

# install requirements first for caching
COPY requirements.txt /app
RUN pip install -r requirements.txt

COPY . /app

CMD python app.py

Desarrollo y ejecución de comandos:

docker build -t máquina-servicio-de-traducción. 
docker run -p 5000:5000 -v $(pwd)/data:/app/data -it machine-translation-service

De nuevo, podemos probar nuestros puntos finales introduciendo órdenes en el servicio. Si descargué una ruta de idioma como EN>FR, entonces debería poder realizar la siguiente llamada a la API con curl:

curl --location--request POST ' http://localhost:5000/translate' \ 
--header 'Content-Type: application/json' \
--data-raw '{
"text":"hola",
"source":"en",
"target":"fr"
}'

Ahora docker-compose

Transformé mi servicio en una pequeña API de Flask, e incluso lo convertí en un contenedor Docker que escale de manera reproducible. Sin embargo, me gustaría compartir algunas de mis preocupaciones sobre este enfoque.

Estos modelos de traducción son bastante grandes (ya que cada uno tiene como 300 MB). Incluso si los datos/modelos se descargan por separado, aún necesitamos cargar cada par de idiomas que queremos admitir en la memoria, y eso se va a salir de control rápidamente para un solo contenedor.

Entonces, en su lugar, ¿por qué no crear una imagen configurable que podamos usar para activar un contenedor para cada servicio usando docker-compose? De esa forma, hay un servicio por par de idiomas y cada par puede escalar individualmente a medida que aumenta la demanda. Luego podemos escribir una sola API expuesta que pase solicitudes a todos los contenedores en la red.

Aviso: a partir de este punto comencé a inventarlo a medida que avanzaba, y de ninguna manera estoy convencido de que este fuera el enfoque óptimo, pero quería ver hasta dónde podía llevarlo.

Para empezar, modifiqué un poco el directorio. Para verificarlo, puede explorar la rama en git que hice:

.
├── LICENSE
├── README.md
├── docker-compose.yaml
├── proxy
│   ├── Dockerfile
│   ├── app.py
│   └── requirements.txt
└── translation_base
├── Dockerfile
├── app.py
├── config.py
├── download_models.py
├── requirements.txt
└── translate.py

El propósito de proxyes redirigir las solicitudes hacia cada servicio de traducción automática que se activa. Este será el único punto final expuesto. Hice un docker-composearchivo rápido para los servicios que admiten la traducción EN>FR.

version: '3.3' 

services:
translation_proxy:
build:
context: ./proxy
environment:
- MODEL_PATH=data
command: python app.py
ports:
- "5000:5000"

en_fr:
image: translation_base
environment:
- MODEL_PATH=data
- SOURCE_LANG=en
- TARGET_LANG=fr
command: python app.py
volumes:
- ./data:/app/data

fr_en:
image: translation_base
environment:

- MODEL_PATH=data
- SOURCE_LANG=fr
- TARGET_LANG=en
command: python app.py
volumes:
- ./data:/app/data

volumes:
data

Cada servicio de traducción tiene las mismas variables env (probablemente debería crear un archivo .env), el mismo comando y el mismo volumen que contiene nuestros modelos. Probablemente haya una mejor manera de escalar esto con automatización yaml o algo similar, pero aún no lo he conseguido.

Después de eso, solo hago algunos cambios en el punto /translate de mi proxy para construir el punto final del servicio solicitado. Cuando los usuarios introducen solicitudes a este servicio expuesto, este solicitará a otros contenedores a los que solo se puede acceder desde esa red.

@app.route('/translate', methods=["POST"])
def get_prediction():
source = request.json['source']
target = request.json['target']
text = str(request.json['text'])
route = f'{source}_{target}'

headers = {
'Content-Type': 'application/json'
}

r = requests.post(f"http://machine-translation-service_{route}_1:5000/translate",
headers = headers,
json={"text":text})

translation = str(r.content)
return jsonify({"output":translation})

Todo lo que tenemos que hacer es construir la imagen base.

cd traducción_base 
docker build -t traducción_base

A continuación, iniciar los servicios.

docker-compose up

Y ¡tachán! Aquí hay una captura de pantalla del resultado usando Postman.

Finalmente, Kubernetes

No voy a profundizar aquí, pero el siguiente paso lógico sería llevar esto a Kubernetes para una escala real. Un punto de partida fácil es usar la
CLI de kompose para convertir Docker-Compose en formato YAML de Kubernetes. En macOS:

$brew install kompose
$kompose convert

INFO Kubernetes file "translation-proxy-service.yaml" created
INFO Kubernetes file "en-fr-deployment.yaml" created
INFO Kubernetes file "en-fr-claim0-persistentvolumeclaim.yaml" created
INFO Kubernetes file "fr-en-deployment.yaml" created
INFO Kubernetes file "fr-en-claim0-persistentvolumeclaim.yaml" created
INFO Kubernetes file "translation-proxy-deployment.yaml" created

Esto debería crear algunos de los archivos YAML que necesitará para escalar sus servicios e implementos a K8.

Esta es el implemento de ejemplo para el servicio FR-EN:

apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
kompose.cmd: kompose convert
kompose.version: 1.21.0 ()
creationTimestamp: null
labels:
io.kompose.service: fr-en
name: fr-en
spec:
replicas: 1
selector:
matchLabels:
io.kompose.service: fr-en
strategy:
type: Recreate
template:
metadata:
annotations:
kompose.cmd: kompose convert
kompose.version: 1.21.0 ()
creationTimestamp: null
labels:
io.kompose.service: fr-en
spec:
containers:
- args:
- python
- app.py
env:
- name: MODEL_PATH
value: data
- name: SOURCE_LANG
value: fr
- name: TARGET_LANG
value: en
image: translation_base
imagePullPolicy: ""
name: fr-en
resources: {}
volumeMounts:
- mountPath: /app/data
name: fr-en-claim0
restartPolicy: Always
serviceAccountName: ""
volumes:
- name: fr-en-claim0
persistentVolumeClaim:
claimName: fr-en-claim0
status: {}

Una vez que tenga un clúster de kubernetes configurado, puede utilizar kubectl apply -f $FILENAMEpara crear sus servicios, implementos y reclamos de volumen persistente.

Por supuesto, hay muchas más cosas que hacer y aún más objetos de kubernetes que crear, pero quería proporcionar una introducción simple para cualquiera que busque escalar algo similar a esto.

Conclusión

Espero que las herramientas que Huggingface sigan construyendo (junto con los modelos que entrenan los investigadores dedicados) y facilitando el acceso equitativo al aprendizaje automático inteligente. Los esfuerzos en la traducción automática abierta no solo contribuyen al campo de la investigación, sino que permiten el acceso mundial a recursos extremadamente importantes escritos en un solo idioma.

No estoy seguro de que desarrollar estos modelos masivos personalmente sea más barato que usar una API de AWS o Google Translate, y tampoco he estudiado la calidad. Pero fue muy divertido sumergirse en esto y, con suerte, ofrece una idea de lo que se puede construir con los miles de modelos de aprendizaje automático SOTA disponibles en la red.

Valora este artículo