Portando el sistema de traducción fairseq wmt19 a transformers

Porting the fairseq wmt19 translation system to transformers.

Una publicación de invitado por Stas Bekman

Este artículo es un intento de documentar cómo se trasladó el sistema de traducción fairseq wmt19 a transformers.

Estaba buscando un proyecto interesante en el que trabajar y Sam Shleifer sugirió que trabajara en la adaptación de un traductor de alta calidad.

Leí el breve documento: Presentación del sistema de traducción de noticias WMT19 de Facebook FAIR que describe el sistema original y decidí intentarlo.

Inicialmente, no tenía idea de cómo abordar este proyecto complejo y Sam me ayudó a dividirlo en tareas más pequeñas, lo cual fue de gran ayuda.

Elegí trabajar con los modelos pre-entrenados en-ru / ru-en durante la adaptación, ya que hablo ambos idiomas. Habría sido mucho más difícil trabajar con los pares de-en / en-de ya que no hablo alemán, y poder evaluar la calidad de la traducción simplemente leyendo y entendiendo las salidas en las etapas avanzadas del proceso de adaptación me ahorró mucho tiempo.

También, como hice la adaptación inicial con los modelos en-ru / ru-en, no sabía que los modelos de-en / en-de usaban un vocabulario fusionado, mientras que los primeros usaban 2 vocabularios separados de diferentes tamaños. Así que una vez que hice el trabajo más complicado de admitir 2 vocabularios separados, fue trivial hacer que el vocabulario fusionado funcionara.

Engañemos

El primer paso fue engañar, por supuesto. ¿Por qué hacer un gran esfuerzo cuando se puede hacer uno pequeño? Así que escribí un pequeño cuaderno que en pocas líneas de código proporcionaba un proxy a fairseq y emulaba la API de transformers.

Si solo se requerían cosas básicas como la traducción, esto habría sido suficiente. Pero, por supuesto, queríamos tener la adaptación completa, así que después de esta pequeña victoria, pasé a cosas mucho más difíciles.

Preparativos

Para este artículo, supongamos que trabajamos bajo ~/porting, por lo tanto, creemos este directorio:

mkdir ~/porting
cd ~/porting

Necesitamos instalar algunas cosas para este trabajo:

# instalar fairseq
git clone https://github.com/pytorch/fairseq
cd fairseq
pip install -e .
# instalar mosesdecoder en fairseq
git clone https://github.com/moses-smt/mosesdecoder
# instalar fastBPE en fairseq
git clone [email protected]:glample/fastBPE.git
cd fastBPE; g++ -std=c++11 -pthread -O3 fastBPE/main.cc -IfastBPE -o fast; cd -
cd -

# instalar transformers
git clone https://github.com/huggingface/transformers/
pip install -e .[dev]

Archivos

Como una visión general rápida, los siguientes archivos necesitaron ser creados y escritos:

  • src/transformers/configuration_fsmt.py – una clase de configuración breve.
  • src/transformers/convert_fsmt_original_pytorch_checkpoint_to_pytorch.py – un script de conversión complejo.
  • src/transformers/modeling_fsmt.py – aquí es donde se implementa la arquitectura del modelo.
  • src/transformers/tokenization_fsmt.py – un código de tokenización.
  • tests/test_modeling_fsmt.py – pruebas de modelo.
  • tests/test_tokenization_fsmt.py – pruebas de tokenización.
  • docs/source/model_doc/fsmt.rst – un archivo de documentación.

También había otros archivos que necesitaban ser modificados, hablaremos de esos al final.

Conversión

Una de las partes más importantes del proceso de adaptación es crear un script que tome todos los datos fuente disponibles proporcionados por el desarrollador original del modelo, que incluye un punto de control con pesos pre-entrenados, modelo y configuración de entrenamiento, diccionarios y archivos de soporte del tokenizador, y los convierta en un nuevo conjunto de archivos de modelo admitidos por transformers. Encontrarás el script final de conversión aquí: src/transformers/convert_fsmt_original_pytorch_checkpoint_to_pytorch.py

Comencé este proceso copiando uno de los scripts de conversión existentes src/transformers/convert_bart_original_pytorch_checkpoint_to_pytorch.py, eliminé la mayor parte de él y luego gradualmente le fui agregando partes a medida que avanzaba en el proceso de portabilidad.

Durante el desarrollo, probé todo mi código con una copia local de los archivos del modelo convertido, y solo al final, cuando todo estaba listo, subí los archivos a 🤗 s3 y luego continué probando con la versión en línea.

Modelo fairseq y sus archivos de soporte

Primero veamos qué datos obtenemos con el modelo pre-entrenado de fairseq.

Vamos a utilizar la conveniente API torch.hub, que facilita mucho la implementación de modelos enviados a ese centro:

import torch
torch.hub.load('pytorch/fairseq', 'transformer.wmt19.en-ru', checkpoint_file='model4.pt',
               tokenizer='moses', bpe='fastbpe')

Este código descarga el modelo pre-entrenado y sus archivos de soporte. Encontré esta información en la página correspondiente a fairseq en el centro de pytorch.

Para ver qué hay dentro de los archivos descargados, primero tenemos que encontrar la carpeta correcta en ~/.cache.

ls -1 ~/.cache/torch/hub/pytorch_fairseq/

muestra:

15bca559d0277eb5c17149cc7e808459c6e307e5dfbb296d0cf1cfe89bb665d7.ded47c1b3054e7b2d78c0b86297f36a170b7d2e7980d8c29003634eb58d973d9
15bca559d0277eb5c17149cc7e808459c6e307e5dfbb296d0cf1cfe89bb665d7.ded47c1b3054e7b2d78c0b86297f36a170b7d2e7980d8c29003634eb58d973d9.json

Puede que haya más de una entrada allí si has estado utilizando el hub para otros modelos.

Creemos un enlace simbólico para que podamos referirnos fácilmente a ese nombre de carpeta de caché oscuro en el futuro:

ln -s /code/data/cache/torch/hub/pytorch_fairseq/15bca559d0277eb5c17149cc7e808459c6e307e5dfbb296d0cf1cfe89bb665d7.ded47c1b3054e7b2d78c0b86297f36a170b7d2e7980d8c29003634eb58d973d9 \
~/porting/pytorch_fairseq_model

Nota: la ruta puede ser diferente cuando lo intentes tú mismo, ya que el valor hash del modelo puede cambiar. Encontrarás el correcto en ~/.cache/torch/hub/pytorch_fairseq/

Si miramos dentro de esa carpeta:

ls -l ~/porting/pytorch_fairseq_model/
total 13646584
-rw-rw-r-- 1 stas stas     532048 Sep  8 21:29 bpecodes
-rw-rw-r-- 1 stas stas     351706 Sep  8 21:29 dict.en.txt
-rw-rw-r-- 1 stas stas     515506 Sep  8 21:29 dict.ru.txt
-rw-rw-r-- 1 stas stas 3493170533 Sep  8 21:28 model1.pt
-rw-rw-r-- 1 stas stas 3493170532 Sep  8 21:28 model2.pt
-rw-rw-r-- 1 stas stas 3493170374 Sep  8 21:28 model3.pt
-rw-rw-r-- 1 stas stas 3493170386 Sep  8 21:29 model4.pt

tenemos:

  1. model*.pt – 4 puntos de control (pytorch state_dict con todos los pesos pre-entrenados y otras cosas varias)
  2. dict.*.txt – diccionarios de origen y destino
  3. bpecodes – archivo de mapa especial utilizado por el tokenizador

Vamos a investigar cada uno de estos archivos en las secciones siguientes.

Cómo funcionan los sistemas de traducción

Aquí hay una breve introducción sobre cómo los ordenadores traducen texto en la actualidad.

Los ordenadores no pueden leer texto, solo pueden manejar números. Así que cuando trabajamos con texto, tenemos que asignar uno o más números a cada letra y proporcionárselos a un programa informático. Cuando el programa termina, también devuelve números, que necesitamos convertir de nuevo en texto.

Comencemos con dos frases en ruso e inglés y asignemos un número único a cada palabra:

я  люблю следовательно я  существую
10 11    12            10 13

I  love therefore I  am
20 21   22        20 23

Los números que empiezan con 10 asignan números únicos a las palabras en ruso. Los números que empiezan con 20 hacen lo mismo para las palabras en inglés. Si no hablas ruso, aún puedes ver que la palabra я (que significa ‘I’) se repite dos veces en la frase y obtiene el mismo número 10 asociado. Lo mismo ocurre con I (20), que también se repite dos veces.

Un sistema de traducción funciona en las siguientes etapas:

1. [я люблю следовательно я существую] # dividir la frase en palabras
2. [10 11 12 10 13]                    # buscar las palabras en el diccionario de entrada y convertirlas en identificadores
3. [caja negra]                        # magia del sistema de aprendizaje automático
4. [20 21 22 20 23]                    # buscar los números en el diccionario de salida y convertirlos en texto
5. [I love therefore I am]             # volver a convertir los tokens en una frase

Si combinamos los dos primeros y los dos últimos pasos, obtenemos 3 etapas:

  1. Codificar la entrada: dividir el texto de entrada en tokens, crear un diccionario (vocabulario) de estos tokens y asignar a cada token un identificador único en ese diccionario.
  2. Generar la traducción: tomar los números de entrada, pasarlos por un modelo de aprendizaje automático previamente entrenado que predice la mejor traducción y devolver los números de salida.
  3. Decodificar la salida: tomar los números de salida, buscarlos en el diccionario del idioma de destino, convertirlos de nuevo en texto y finalmente combinar los tokens convertidos en la frase traducida.

La segunda etapa puede devolver una o varias traducciones posibles. En el caso de varias traducciones posibles, el llamador puede elegir el resultado más adecuado. En este artículo me referiré al algoritmo de búsqueda de haz, que es una de las formas de buscar múltiples resultados posibles. Y el tamaño del haz se refiere a cuántos resultados se devuelven.

Si se solicita solo un resultado, el modelo elegirá el que tenga la probabilidad más alta. Si se solicitan varios resultados, devolverá esos resultados ordenados por sus probabilidades.

Hay que tener en cuenta que esta misma idea se aplica a la mayoría de las tareas de procesamiento del lenguaje natural (NLP, por sus siglas en inglés), no solo a la traducción.

Tokenización

Los primeros sistemas dividían las frases en palabras y signos de puntuación. Pero dado que muchos idiomas tienen cientos de miles de palabras, es muy exigente trabajar con vocabularios enormes, ya que aumenta drásticamente los requisitos de recursos informáticos y el tiempo necesario para completar la tarea.

A partir de 2020, existen varios métodos de tokenización diferentes, pero la mayoría de los más recientes se basan en la subtokenización: en lugar de dividir el texto de entrada en palabras, estos tokenizadores modernos dividen el texto de entrada en segmentos de palabras y letras, utilizando algún tipo de entrenamiento para obtener la tokenización más óptima.

Veamos cómo este enfoque ayuda a reducir los requisitos de memoria y computación. Si tenemos un vocabulario de entrada con 6 palabras comunes: go, going, speak, speaking, sleep, sleeping; con la tokenización a nivel de palabras, obtendremos 6 tokens. Sin embargo, si los dividimos en: go, go-ing, speak, speak-ing, etc., entonces solo tendremos 4 tokens en nuestro vocabulario: go, speak, sleep, ing. ¡Este simple cambio supuso una mejora del 33%! Sin embargo, los tokenizadores de subtokens no utilizan reglas gramaticales, sino que se entrenan con grandes cantidades de texto para encontrar esas divisiones. En este ejemplo, utilicé una regla gramatical simple para que sea fácil de entender.

Otra ventaja importante de este enfoque es cuando se trata de palabras de texto de entrada que no están en nuestro vocabulario. Por ejemplo, supongamos que nuestro sistema encuentra la palabra grokking (*), que no se encuentra en su vocabulario. Si la dividimos en `grokk’-‘ing’, entonces el modelo de aprendizaje automático puede que no sepa qué hacer con la primera parte de la palabra, pero obtiene una idea útil de que ‘ing’ indica un tiempo continuo, por lo que podrá producir una mejor traducción. En esta situación, el tokenizador dividirá los segmentos desconocidos en segmentos que conoce, en el peor de los casos reduciéndolos a letras individuales.

  • nota al pie: el término grok fue acuñado en 1961 por Robert A. Heinlein en “Stranger in a Strange Land”: comprender (algo) intuitivamente o por empatía.

Existen muchas otras sutilezas que explican por qué el enfoque moderno de tokenización es mucho más superior que la simple tokenización de palabras, las cuales no serán cubiertas en el alcance de este artículo. La mayoría de estos sistemas son muy complejos en cuanto a cómo realizan la tokenización, en comparación con el simple ejemplo de dividir los sufijos ing que se acaba de demostrar, pero el principio es similar.

Portabilidad del tokenizador

El primer paso fue portar la parte del codificador del tokenizador, donde el texto se convierte en identificadores. La parte del decodificador no será necesaria hasta el final.

Funcionamiento del tokenizador de fairseq

Veamos cómo funciona el tokenizador de fairseq.

fairseq (*) utiliza el algoritmo de Codificación de Par de Bytes (BPE) para la tokenización.

  • nota al pie: de aquí en adelante, cuando me refiero a fairseq, me refiero a esta implementación específica del modelo, ya que el proyecto fairseq en sí tiene docenas de implementaciones diferentes de modelos distintos.

Veamos qué hace BPE:

import torch
sentence = "Machine Learning is great"
checkpoint_file='model4.pt'
model = torch.hub.load('pytorch/fairseq', 'transformer.wmt19.en-ru', checkpoint_file=checkpoint_file, tokenizer='moses', bpe='fastbpe')

# paso a paso de la codificación
tokens = model.tokenize(sentence)
print("tokenize ", tokens)

bpe = model.apply_bpe(tokens)
print("apply_bpe: ", bpe)

bin = model.binarize(bpe)
print("binarize: ", len(bin), bin)

# comparar con model.encode - debería darnos la misma salida
expected = model.encode(sentence)
print("encode:   ", len(expected), expected)

nos da:

('tokenize ', 'Machine Learning is great')
('apply_bpe: ', 'Mach@@ ine Lear@@ ning is great')
('binarize: ', 7, tensor([10217,  1419,     3,  2515,    21,  1054,     2]))
('encode:   ', 7, tensor([10217,  1419,     3,  2515,    21,  1054,     2]))

Puedes ver que model.encode hace tokenize+apply_bpe+binarize – ya que obtenemos la misma salida.

Los pasos fueron:

  1. tokenize: normalmente escaparía las comillas y realizaría otros preprocesamientos, pero en este ejemplo devuelve la oración de entrada sin cambios
  2. apply_bpe: BPE divide la entrada en palabras y subpalabras según su archivo bpecodes suministrado por el tokenizador – obtenemos 6 fragmentos de BPE
  3. binarize: esto simplemente asigna nuevamente los fragmentos de BPE del paso anterior a sus identificadores correspondientes en el vocabulario (que también se descarga con el modelo)

Puedes consultar este cuaderno para ver más detalles.

Este es un buen momento para echar un vistazo dentro del archivo bpecodes. Aquí está la parte superior del archivo:

$ head -15 ~/porting/pytorch_fairseq_model/bpecodes
e n</w> 1423551864
e r 1300703664
e r</w> 1142368899
i n 1130674201
c h 933581741
a n 845658658
t h 811639783
e n 780050874
u n 661783167
s t 592856434
e i 579569900
a r 494774817
a l 444331573
o r 439176406
th e</w> 432025210
[...]

Las primeras entradas de este archivo incluyen secuencias cortas de 1 letra muy frecuentes. Como veremos en un momento, la parte inferior incluye los subpalabras más comunes y hasta palabras largas completas.

Un token especial </w> indica el final de la palabra. Por lo tanto, en varias líneas citadas anteriormente encontramos:

e n</w> 1423551864
e r</w> 1142368899
th e</w> 432025210

Si la segunda columna no incluye </w>, significa que este segmento se encuentra en medio de la palabra y no al final de la misma.

La última columna indica el número de veces que se ha encontrado este código BPE durante el entrenamiento. El archivo bpecodes está ordenado por esta columna, por lo que los códigos BPE más comunes están en la parte superior.

Al observar los recuentos, ahora sabemos que cuando se entrenó este tokenizador se encontraron 1,423,551,864 palabras que terminan en en, 1,142,368,899 palabras que terminan en er y 432,025,210 palabras que terminan en the. Para este último, es muy probable que signifique la palabra real the, pero también incluiría palabras como lathe, loathe, tithe, etc.

Estos números enormes también nos indican que este tokenizador se entrenó con una cantidad enorme de texto.

Si miramos la parte inferior del mismo archivo:

$ tail -10 ~/porting/pytorch_fairseq_model/bpecodes
4 x 109019
F ische</w> 109018
sal aries</w> 109012
e kt 108978
ver gewal 108978
Sten cils</w> 108977
Freiwilli ge</w> 108969
doub les</w> 108965
po ckets</w> 108953
Gö tz</w> 108943

observamos combinaciones complejas de subpalabras que aún son bastante frecuentes, por ejemplo, sal aries ¡109,012 veces! Por lo tanto, tiene su propia entrada dedicada en el archivo de mapa bpecodes.

¿Cómo hace apply_bpe su trabajo? Buscando las varias combinaciones de letras en el archivo de mapa bpecodes y, al encontrar la entrada más larga que se ajuste, la utiliza.

Volviendo a nuestro ejemplo, vimos que dividió Machine en: Mach@@ + ine – veamos:

$ grep -i ^mach  ~/porting/pytorch_fairseq_model/bpecodes
mach ine</w> 463985
Mach t 376252
Mach ines</w> 374223
mach ines</w> 214050
Mach th 119438

Puedes ver que tiene mach ine</w>. No vemos Mach ine ahí, por lo que debe estar manejando búsquedas en minúsculas cuando la mayúscula normal no coincide.

Ahora veamos: Lear@@ + ning

$ grep -i ^lear  ~/porting/pytorch_fairseq_model/bpecodes
lear n</w> 675290
lear ned</w> 505087
lear ning</w> 417623

Encontramos que lear ning</w> está ahí (nuevamente, la mayúscula no es la misma).

Pensando más en ello, es probable que la mayúscula no importe para la tokenización, siempre y cuando haya una entrada única para Mach/Lear y mach/lear en el diccionario donde es muy importante tener cada caso cubierto.

Espero que ahora puedas ver cómo funciona esto.

Una cosa confusa es que si recuerdas, la salida de apply_bpe fue:

('apply_bpe: ', 6, ['Mach@@', 'ine', 'Lear@@', 'ning', 'is', 'great'])

En lugar de marcar los finales de las palabras con </w>, los deja como están, pero en su lugar marca las palabras que no son finales con @@. Esto probablemente se debe a que se utiliza la implementación de fastBPE de fairseq y así es como hace las cosas. Tuve que cambiar esto para que se ajuste a la implementación de transformers, que no utiliza fastBPE.

Una última cosa que verificar es el mapeo de los códigos BPE a los identificadores del vocabulario. Para repetir, teníamos:

('apply_bpe: ', 'Mach@@ ine Lear@@ ning is great')
('binarize: ', 7, tensor([10217,  1419,     3,  2515,    21,  1054,     2]))

2 – el último identificador de token es un token eos (fin de flujo). Se utiliza para indicarle al modelo el final de la entrada.

Y luego Mach@@ se mapea a 10217, e ine a 1419.

Verifiquemos que el archivo de diccionario esté de acuerdo:

$ grep ^Mach@@ ~/porting/pytorch_fairseq_model/dict.en.txt
Mach@@ 6410
$ grep "^ine " ~/porting/pytorch_fairseq_model/dict.en.txt
ine 88376

Espera un segundo: esos no son los identificadores que obtuvimos después de binarize, que deberían ser 10217 y 1419 respectivamente.

Tuve que investigar un poco para descubrir que los identificadores del archivo de vocabulario no son los identificadores utilizados por el modelo y que internamente los remapea a nuevos identificadores una vez que se carga el archivo de vocabulario. Afortunadamente, no tuve que averiguar cómo se hacía exactamente. En su lugar, simplemente utilicé fairseq.data.dictionary.Dictionary.load para cargar el diccionario (*), que realizó todos los remapeos, y luego guardé el diccionario final. Descubrí esa clase Dictionary al seguir el código de fairseq con el depurador.

  • nota al pie: cuanto más trabajo en la portabilidad de modelos y conjuntos de datos, más me doy cuenta de que poner el código original a trabajar para mí, en lugar de tratar de replicarlo, ahorra mucho tiempo y, lo que es más importante, ese código ya ha sido probado: ¡es demasiado fácil pasar por alto algo y descubrir grandes problemas más adelante! Después de todo, al final, todo este código de conversión no importará, ya que solo se utilizarán los datos que generó con transformers y sus usuarios finales.

Aquí está la parte relevante del script de conversión:

from fairseq.data.dictionary import Dictionary
def rewrite_dict_keys(d):
    # (1) eliminar el símbolo de división de palabras
    # (2) agregar el símbolo de fin de palabra donde la palabra no está dividida,
    # por ejemplo: d = {'le@@': 5, 'tt@@': 6, 'er': 7} => {'le': 5, 'tt': 6, 'er</w>': 7}
    d2 = dict((re.sub(r"@@$", "", k), v) if k.endswith("@@") else (re.sub(r"$", "</w>", k), v) for k, v in d.items())
    keep_keys = "<s> <pad> </s> <unk>".split()
    # restaurar los tokens especiales
    for k in keep_keys:
        del d2[f"{k}</w>"]
        d2[k] = d[k]  # restaurar
    return d2

src_dict_file = os.path.join(fsmt_folder_path, f"dict.{src_lang}.txt")
src_dict = Dictionary.load(src_dict_file)
src_vocab = rewrite_dict_keys(src_dict.indices)
src_vocab_size = len(src_vocab)
src_vocab_file = os.path.join(pytorch_dump_folder_path, "vocab-src.json")
print(f"Generando {src_vocab_file}")
with open(src_vocab_file, "w", encoding="utf-8") as f:
    f.write(json.dumps(src_vocab, ensure_ascii=False, indent=json_indent))
# hicimos lo mismo para el diccionario destino - omití citarlo aquí
# y también tuvimos que guardar `bpecodes`, se llama `merges.txt` en el mundo de transformers

Después de ejecutar el script de conversión, verifiquemos el diccionario convertido:

$ grep '"Mach"' /code/huggingface/transformers-fair-wmt/data/wmt19-en-ru/vocab-src.json
  "Mach": 10217,
$ grep '"ine</w>":' /code/huggingface/transformers-fair-wmt/data/wmt19-en-ru/vocab-src.json
  "ine</w>": 1419,

Tenemos los ids correctos en la versión de vocabulario de transformers.

Como puedes ver, también tuve que reescribir los vocabularios para que coincidan con la implementación BPE de transformers. Tenemos que cambiar:

['Mach@@', 'ine', 'Lear@@', 'ning', 'is', 'great']

a:

['Mach', 'ine</w>', 'Lear', 'ning</w>', 'is</w>', 'great</w>']

En lugar de marcar fragmentos que son segmentos de una palabra, con excepción del último segmento, marcamos segmentos o palabras que son el segmento final. Se puede pasar fácilmente de un estilo de codificación a otro y viceversa.

Esto completó con éxito la transferencia de la primera parte de los archivos del modelo. Puedes ver la versión final del código aquí .

Si tienes curiosidad por investigar más a fondo, hay más detalles en este cuaderno .

Transferencia del codificador del tokenizador a transformers

transformers no puede depender de fastBPE ya que este último requiere un compilador de C, pero por suerte alguien ya implementó una versión en python del mismo en tokenization_xlm.py .

Así que simplemente lo copié a src/transformers/tokenization_fsmt.py y renombré los nombres de las clases:

cp tokenization_xlm.py tokenization_fsmt.py
perl -pi -e 's|XLM|FSMT|ig; s|xlm|fsmt|g;' tokenization_fsmt.py

y con muy pocos cambios tuve una parte del codificador del tokenizador funcionando. Había mucho código que no se aplicaba a los idiomas que necesitaba soportar, así que eliminé ese código.

Dado que necesitaba 2 vocabularios diferentes, en lugar de uno aquí en el tokenizador y en todos los demás lugares, tuve que cambiar el código para admitir ambos. Por ejemplo, tuve que anular los métodos de la superclase:

    def get_vocab(self) -> Dict[str, int]:
        return self.get_src_vocab()

    @property
    def vocab_size(self) -> int:
        return self.src_vocab_size

Dado que fairseq no usaba tokens bos (inicio de flujo), también tuve que cambiar el código para no incluir esos (*):

-            return bos + token_ids_0 + sep
-        return bos + token_ids_0 + sep + token_ids_1 + sep
+            return token_ids_0 + sep
+        return token_ids_0 + sep + token_ids_1 + sep
  • nota: este es el resultado de diff(1) que muestra la diferencia entre dos fragmentos de código – las líneas que comienzan con - muestran lo que se eliminó, y con + lo que se agregó.

fairseq también escapaba caracteres y realizaba una división de guiones agresiva, así que también tuve que cambiar:

-        [...].tokenize(text, return_str=False, escape=False)
+        [...].tokenize(text, return_str=False, escape=True, aggressive_dash_splits=True)

Si estás siguiendo el proceso y te gustaría ver todos los cambios que hice en el archivo original tokenization_xlm.py , puedes hacer:

cp tokenization_xlm.py tokenization_orig.py
perl -pi -e 's|XLM|FSMT|g; s|xlm|fsmt|g;' tokenization_orig.py
diff -u tokenization_orig.py tokenization_fsmt.py  | less

Asegúrese de revisar el repositorio en el momento en que se lanzó fsmt, ya que los 2 archivos podrían haber divergido desde entonces.

La etapa final consistió en ejecutar una serie de entradas y asegurarse de que el tokenizador portado produjera las mismas IDs que el original. Puede ver que esto se hace en este cuaderno, que estaba ejecutando repetidamente mientras trataba de averiguar cómo hacer que las salidas coincidieran.

Así fue como la mayor parte del proceso de portabilidad se llevó a cabo: tomaría una pequeña característica, la ejecutaría a la manera de fairseq, obtendría las salidas, haría lo mismo con el código de transformers, intentaría hacer que las salidas coincidieran, ajustaría el código hasta que lo hiciera, luego probaría un tipo diferente de entrada para asegurarme de que produjera las mismas salidas, y así sucesivamente, hasta que todas las entradas produjeran salidas que coincidieran.

Portando la funcionalidad principal de traducción

Después de haber tenido un éxito relativamente rápido al portar el tokenizador (obviamente, gracias a que la mayor parte del código ya estaba allí), la siguiente etapa fue mucho más compleja. Se trata de la función generate(), que toma las ID de entrada, las ejecuta a través del modelo y devuelve las ID de salida.

Tuve que descomponerlo en varias sub-tareas. Tuve que:

  1. portar los pesos del modelo.
  2. hacer que generate() funcionara para un solo rayo (es decir, devolver solo un resultado).
  3. y luego para múltiples rayos (es decir, devolver múltiples resultados).

Primero investigué cuáles de las arquitecturas existentes eran las más cercanas a mis necesidades. Fue BART el que se ajustó más, así que seguí adelante y hice lo siguiente:

cp modeling_bart.py modeling_fsmt.py
perl -pi -e 's|Bart|FSMT|ig; s|bart|fsmt|g;' modeling_fsmt.py

Este fue mi punto de partida que necesitaba ajustar para que funcionara con los pesos del modelo proporcionados por fairseq.

Portando pesos y configuración

Lo primero que hice fue ver qué había dentro del punto de control compartido públicamente. Este cuaderno muestra lo que hice allí.

Descubrí que había 4 puntos de control ahí. No tenía idea de qué hacer al respecto, así que comencé con una tarea más simple de usar solo el primer punto de control. Más tarde descubrí que fairseq usaba los 4 puntos de control en un conjunto para obtener las mejores predicciones, y que transformers actualmente no admite esa función. Cuando se completó la portabilidad y pude medir las puntuaciones de rendimiento, descubrí que el punto de control model4.pt proporcionaba la mejor puntuación. Pero durante la portabilidad el rendimiento no importaba mucho. Dado que solo estaba usando un punto de control, era crucial que al comparar las salidas, fairseq también usara solo uno y el mismo punto de control.

Para lograr eso, utilicé una API ligeramente diferente de fairseq:

from fairseq import hub_utils
#checkpoint_file = 'model1.pt:model2.pt:model3.pt:model4.pt'
checkpoint_file = 'model1.pt'
model_name_or_path = 'transformer.wmt19.ru-en'
data_name_or_path = '.'
cls = fairseq.model_parallel.models.transformer.ModelParallelTransformerModel
models = cls.hub_models()
kwargs = {'bpe': 'fastbpe', 'tokenizer': 'moses'}
ru2en = hub_utils.from_pretrained(
            model_name_or_path,
            checkpoint_file,
            data_name_or_path,
            archive_map=models,
            **kwargs
        )

Primero miré el modelo:

print(ru2en["models"][0])

TransformerModel(
  (encoder): TransformerEncoder(
    (dropout_module): FairseqDropout()
    (embed_tokens): Embedding(31232, 1024, padding_idx=1)
    (embed_positions): SinusoidalPositionalEmbedding()
    (layers): ModuleList(
      (0): TransformerEncoderLayer(
        (self_attn): MultiheadAttention(
          (dropout_module): FairseqDropout()
          (k_proj): Linear(in_features=1024, out_features=1024, bias=True)
          (v_proj): Linear(in_features=1024, out_features=1024, bias=True)
          (q_proj): Linear(in_features=1024, out_features=1024, bias=True)
          (out_proj): Linear(in_features=1024, out_features=1024, bias=True)
        )
      [...]
# el resultado completo está en el cuaderno

que se parecía mucho a la arquitectura de BART, con algunas pequeñas diferencias en algunas capas: algunas se agregaron, otras se eliminaron. Así que esto fue una gran noticia, ya que no tuve que reinventar la rueda, sino simplemente ajustar un diseño que funcionaba bien.

Tenga en cuenta que en el ejemplo de código anterior no estoy utilizando torch.load() para cargar state_dict. Esto es lo que hice inicialmente y el resultado fue desconcertante: me faltaban los pesos de self_attn.(k|q|v)_proj y en su lugar tenía un solo self_attn.in_proj. Cuando intenté cargar el modelo usando la API de fairseq, se solucionaron las cosas, aparentemente ese modelo era antiguo y estaba utilizando una arquitectura antigua que tenía un conjunto de pesos para k/q/v y la nueva arquitectura los tiene separados. Cuando fairseq carga este modelo antiguo, reescribe los pesos para que coincidan con la arquitectura moderna.

También utilicé este cuaderno para comparar visualmente los state_dict. En ese cuaderno también verá que fairseq recupera 2.2 GB de datos en last_optimizer_state, que podemos ignorar de forma segura, lo que resulta en un tamaño final del modelo tres veces más pequeño.

En el script de conversión también tuve que eliminar algunas claves de state_dict que no iba a utilizar, por ejemplo, model.encoder.version, model.model y algunas otras.

A continuación, analizamos los argumentos de configuración:

args = dict(vars(ru2en["args"]))
pprint(args)

 'activation_dropout': 0.0,
 'activation_fn': 'relu',
 'adam_betas': '(0.9, 0.98)',
 'adam_eps': 1e-08,
 'adaptive_input': False,
 'adaptive_softmax_cutoff': None,
 'adaptive_softmax_dropout': 0,
 'arch': 'transformer_wmt_en_de_big',
 'attention_dropout': 0.1,
 'bpe': 'fastbpe',
 [... el resultado completo está en el cuaderno ...] 

Listo, copiaremos eso para configurar el modelo. Tuve que cambiar el nombre de algunos de los argumentos, donde transformers usaba nombres diferentes para la configuración correspondiente. Entonces, el mapeo de configuración se ve así:

    model_conf = {
        "architectures": ["FSMTForConditionalGeneration"],
        "model_type": "fsmt",
        "activation_dropout": args["activation_dropout"],
        "activation_function": "relu",
        "attention_dropout": args["attention_dropout"],
        "d_model": args["decoder_embed_dim"],
        "dropout": args["dropout"],
        "init_std": 0.02,
        "max_position_embeddings": args["max_source_positions"],
        "num_hidden_layers": args["encoder_layers"],
        "src_vocab_size": src_vocab_size,
        "tgt_vocab_size": tgt_vocab_size,
        "langs": [src_lang, tgt_lang],
        [...]
        "bos_token_id": 0,
        "pad_token_id": 1,
        "eos_token_id": 2,
        "is_encoder_decoder": True,
        "scale_embedding": not args["no_scale_embedding"],
        "tie_word_embeddings": args["share_all_embeddings"],
    }

Todo lo que queda es guardar la configuración en config.json y crear un nuevo volcado de state_dict en torch.dump:

    print(f"Generando {fsmt_tokenizer_config_file}")
    with open(fsmt_tokenizer_config_file, "w", encoding="utf-8") as f:
        f.write(json.dumps(tokenizer_conf, ensure_ascii=False, indent=json_indent))
    [...]
    print(f"Generando {pytorch_weights_dump_path}")
    torch.save(model_state_dict, pytorch_weights_dump_path)

¡Hemos trasladado la configuración y el state_dict del modelo, yay!

Encontrará el código de conversión final aquí .

Trasladando el código de la arquitectura

Ahora que tenemos los pesos del modelo y la configuración del modelo trasladados, solo necesitamos ajustar el código copiado de modeling_bart.py para que coincida con la funcionalidad de fairseq.

El primer paso fue tomar una oración, codificarla y luego alimentarla a la función generate – tanto para fairseq como para transformers.

Después de algunos intentos muy fallidos por llegar a algún lugar (*) – rápidamente me di cuenta de que con el nivel actual de complejidad, usar print como método de depuración no me llevaría a ninguna parte, al igual que el depurador básico pdb. Para ser eficiente y poder ver varias variables y tener observaciones que sean evaluaciones de código, necesitaba un depurador visual serio. Pasé un día probando todo tipo de depuradores de Python y solo cuando probé pycharm me di cuenta de que era la herramienta que necesitaba. Fue la primera vez que usé pycharm, pero rápidamente descubrí cómo usarlo, ya que era bastante intuitivo.

  • nota al pie: el modelo estaba generando ‘nononono’ en ruso, ¡lo cual fue justo y divertido!

Con el tiempo, encontré una excelente función en pycharm que me permitía agrupar puntos de interrupción por funcionalidad y podía activar y desactivar grupos completos según lo que estuviera depurando. Por ejemplo, aquí tengo desactivados los puntos de interrupción relacionados con la búsqueda de haz y activados los del decodificador:

Ahora que he usado este depurador para portar FSMT, sé que me hubiera llevado mucho más tiempo usar pdb para hacer lo mismo, incluso podría haberlo abandonado.

Comencé con 2 scripts:

  • fseq-translate
  • fsmt-translate

(sin la parte de decode primero)

Ejecutando ambos lado a lado, avanzando con el depurador en cada lado y comparando los valores de las variables relevantes, hasta que encontré la primera divergencia. Luego estudié el código, hice ajustes dentro de modeling_fsmt.py, reinicié el depurador, salté rápidamente al punto de divergencia y volví a verificar las salidas. Este ciclo se repitió varias veces hasta que las salidas coincidieron.

Lo primero que tuve que cambiar fue eliminar algunas capas que no eran utilizadas por fairseq y luego agregar algunas capas nuevas que este estaba usando en su lugar. Y luego el resto fue principalmente descubrir cuándo cambiar a src_vocab_size y cuándo cambiar a tgt_vocab_size – ya que en los módulos principales es simplemente vocab_size, lo cual no tenía en cuenta la posibilidad de un modelo con 2 diccionarios. Finalmente, descubrí que algunas configuraciones de hiperparámetros no eran las mismas, por lo que también las cambié.

Primero hice este proceso para la búsqueda de haz más simple, y una vez que las salidas coincidieron al 100%, lo repetí con la búsqueda de haz más complicada. Aquí, por ejemplo, descubrí que fairseq estaba usando el equivalente de early_stopping=True, mientras que transformers lo tiene como False de forma predeterminada. Cuando se habilita la detención temprana, deja de buscar nuevas candidatas tan pronto como haya tantas candidatas como el tamaño del haz, mientras que cuando está deshabilitada, el algoritmo deja de buscar solo cuando no puede encontrar candidatas con una probabilidad más alta que las que ya tiene. El documento de fairseq menciona que se usó un tamaño de haz enorme de 50, lo cual compensa el uso de detención temprana.

Portar el decodificador de tokens

Una vez que logré que la función portada generate produjera resultados bastante similares a la función generate de fairseq, necesitaba completar la última etapa de decodificar las salidas en texto legible para los humanos. Esto me permitió hacer una comparación rápida y evaluar la calidad de la traducción, algo que no podía hacer con los identificadores de salida.

Similar al proceso de codificación, este se realizó en sentido inverso.

Los pasos fueron:

  1. convertir los identificadores de salida en cadenas de texto
  2. eliminar las codificaciones BPE
  3. destokenizar – manejar caracteres escapados, etc.

Después de hacer algunas depuraciones adicionales aquí, tuve que cambiar la forma en que se manejaba el BPE del enfoque original en tokenization_xlm.py y también ejecutar las salidas a través del desdetokenizador moses.

     def convert_tokens_to_string(self, tokens):
         """ Convierte una secuencia de tokens (string) en un solo string. """
-        out_string = "".join(tokens).replace("</w>", " ").strip()
-        return out_string
+        # remueve BPE
+        tokens = [t.replace(" ", "").replace("</w>", " ") for t in tokens]
+        tokens = "".join(tokens).split()
+        # desdetokeniza
+        text = self.moses_detokenize(tokens, self.tgt_lang)
+        return text

Y todo estaba bien.

Subiendo modelos a s3

Una vez que el script de conversión hizo un trabajo completo de trasladar todos los archivos requeridos a transformers, subí los modelos a mi cuenta de 🤗 s3:

cd data
transformers-cli upload -y wmt19-ru-en
transformers-cli upload -y wmt19-en-ru
transformers-cli upload -y wmt19-de-en
transformers-cli upload -y wmt19-en-de
cd -

Durante las pruebas, utilicé mi cuenta de 🤗 s3 y una vez que mi PR con los cambios completos estuvo listo para ser fusionado, pedí en el PR que movieran los modelos a la cuenta de la organización facebook, ya que estos modelos pertenecen allí.

Varias veces tuve que actualizar solo los archivos de configuración y no quería volver a subir los modelos grandes, así que escribí este pequeño script que produce los comandos de carga correctos, los cuales de otra manera eran demasiado largos para escribir y propensos a errores:

perl -le 'for $f (@ARGV) { print qq[transformers-cli upload -y $_/$f --filename $_/$f] \
for map { "wmt19-$_" } ("en-ru", "ru-en", "de-en", "en-de")}' \
vocab-src.json vocab-tgt.json tokenizer_config.json config.json
# agregar/eliminar archivos según sea necesario

Entonces, si, por ejemplo, solo necesitaba actualizar todos los archivos config.json, el script anterior me dio una copia y pega conveniente:

transformers-cli upload -y wmt19-en-ru/config.json --filename wmt19-en-ru/config.json
transformers-cli upload -y wmt19-ru-en/config.json --filename wmt19-ru-en/config.json
transformers-cli upload -y wmt19-de-en/config.json --filename wmt19-de-en/config.json
transformers-cli upload -y wmt19-en-de/config.json --filename wmt19-en-de/config.json

Una vez que se completó la carga, estos modelos se podían acceder de la siguiente manera (*):

tokenizer = FSMTTokenizer.from_pretrained("stas/wmt19-en-ru")
  • nota a pie de página: stas es mi nombre de usuario en https://huggingface.co .

Antes de hacer esta carga, tuve que usar la ruta local a la carpeta con los archivos del modelo, por ejemplo:

tokenizer = FSMTTokenizer.from_pretrained("/code/huggingface/transformers-fair-wmt/data/wmt19-en-ru")

Importante: Si actualizas los archivos del modelo y los vuelves a cargar, debes tener en cuenta que debido al almacenamiento en caché de CDN, el modelo cargado puede no estar disponible durante un máximo de 24 horas después de la carga, es decir, se entregará el modelo en caché antiguo. Entonces, la única forma de comenzar a usar el nuevo modelo antes es ya sea:

  1. descargándolo a una ruta local y usando esa ruta como argumento que se pasa a from_pretrained().
  2. o usando: from_pretrained(..., use_cdn=False) en todas partes durante las próximas 24 horas, no es suficiente hacerlo una vez.

AutoConfig, AutoTokenizer, etc.

Otro cambio que necesité hacer es conectar el modelo recién portado al sistema automatizado de modelos de transformers. Esto se usa principalmente en el sitio web de modelos para cargar la configuración del modelo, el tokenizador y la clase principal sin proporcionar ningún nombre de clase específico. Por ejemplo, en el caso de FSMT se puede hacer:

from transformers import AutoTokenizer, AutoModelWithLMHead
mname = "facebook/wmt19-en-ru"
tokenizer = AutoTokenizer.from_pretrained(mname)
model = AutoModelWithLMHead.from_pretrained(mname)

Hay 3 archivos *auto* que tienen mapeos para habilitar eso:

-rw-rw-r-- 1 stas stas 16K Sep 23 13:53 src/transformers/configuration_auto.py
-rw-rw-r-- 1 stas stas 65K Sep 23 13:53 src/transformers/modeling_auto.py
-rw-rw-r-- 1 stas stas 13K Sep 23 13:53 src/transformers/tokenization_auto.py

Luego están los pipelines, que ocultan por completo todas las complejidades de NLP al usuario final y proporcionan una API muy simple para elegir un modelo y usarlo para una tarea en particular. Por ejemplo, así es como se puede realizar una tarea de resumen utilizando pipeline:

summarizer = pipeline("summarization", model="t5-base", tokenizer="t5-base")
summary = summarizer("Algun documento largo aquí", min_length=5, max_length=20)
print(summary)

Los pipelines de traducción son un trabajo en progreso en el momento de escribir esto, consulte este documento para obtener actualizaciones sobre cuándo se admitirá la traducción (actualmente solo se admiten algunos modelos/idiomas específicos).

Finalmente, existe src/transforers/__init__.py para editar de modo que uno pueda hacer:

from transformers import FSMTTokenizer, FSMTForConditionalGeneration

en lugar de:

from transformers.tokenization_fsmt import FSMTTokenizer
from transformers.modeling_fsmt import FSMTForConditionalGeneration

pero de cualquier manera funciona.

Para encontrar todos los lugares en los que necesitaba enchufar FSMT, imité a BartConfig, BartForConditionalGeneration y BartTokenizer. Solo busqué qué archivos lo tenían e inserté las entradas correspondientes para FSMTConfig, FSMTForConditionalGeneration y FSMTTokenizer.

$ egrep -l "(BartConfig|BartForConditionalGeneration|BartTokenizer)" src/transformers/*.py \
| egrep -v "(marian|bart|pegasus|rag|fsmt)"
src/transformers/configuration_auto.py
src/transformers/generation_utils.py
src/transformers/__init__.py
src/transformers/modeling_auto.py
src/transformers/pipelines.py
src/transformers/tokenization_auto.py

En la búsqueda de grep excluí los archivos que también incluyen esas clases.

Prueba manual

Hasta ahora, principalmente he estado usando mis propios scripts para las pruebas.

Una vez que tenía el traductor funcionando, convertí el modelo invertido ru-en y luego escribí dos scripts de parafraseo:

  • fseq-paraphrase
  • fsmt-paraphrase

que tomaban una oración en el idioma fuente, la traducían a otro idioma y luego traducían el resultado de vuelta al idioma original. Este proceso generalmente da como resultado una expresión parafraseada, debido a las diferencias en cómo diferentes idiomas expresan cosas similares.

Con la ayuda de estos scripts, encontré algunos problemas más con el des-tokenizador, recorrí paso a paso con el depurador y hice que el script fsmt produjera los mismos resultados que la versión fairseq.

En esta etapa, la búsqueda sin rayos producía resultados principalmente idénticos, pero aún había alguna divergencia en la búsqueda con rayos. Con el fin de identificar los casos especiales, escribí un script fsmt-port-validate.py que utilizaba como entradas los datos de prueba de sacrebleu y ejecutaba esos datos a través de la traducción tanto con fairseq como con transformers y solo informaba las diferencias. Rápidamente identificó algunos problemas restantes y, observando los patrones, también pude solucionar esos problemas.

Portar otros modelos

Después, procedí a portar los modelos en-de y de-en.

Me sorprendió descubrir que estos no fueron construidos de la misma manera. Cada uno de estos tenía un diccionario fusionado, así que por un momento sentí frustración, ya que pensé que ahora tendría que hacer otro cambio enorme para admitir eso. Pero no necesité hacer ningún cambio, ya que el diccionario fusionado encajó sin necesidad de realizar cambios. Simplemente utilicé 2 diccionarios idénticos: uno como origen y una copia de este como destino.

Escribí otro script para probar la funcionalidad básica de todos los modelos portados: fsmt-test-all.py .

Cobertura de Pruebas

Este siguiente paso fue muy importante: necesitaba preparar una extensa prueba para el modelo portado.

En la suite de pruebas de transformers, la mayoría de las pruebas que tratan con modelos grandes están marcadas como @slow y estas no se ejecutan normalmente en CI (Integración Continua), ya que son, bueno, lentas. Así que también necesité crear un modelo diminuto que tenga la misma estructura que un modelo pre-entrenado normal, pero que sea muy pequeño y pueda tener pesos aleatorios. Este modelo diminuto puede ser utilizado para probar la funcionalidad portada. Simplemente no se puede utilizar para pruebas de calidad, ya que tiene pocos pesos y, por lo tanto, no se puede entrenar realmente para hacer algo práctico. fsmt-make-tiny-model.py crea un modelo diminuto de este tipo. El modelo generado con todos sus archivos de diccionario y configuración tenía un tamaño de solo 3MB. Lo cargué en s3 usando transformers-cli upload y ahora pude usarlo en la suite de pruebas.

Al igual que con el código, comencé copiando tests/test_modeling_bart.py y convirtiéndolo para usar FSMT, y luego lo ajusté para que funcione con el nuevo modelo.

Luego convertí algunos de mis scripts que usé para pruebas manuales en pruebas unitarias, eso fue fácil.

transformers tiene un gran conjunto de pruebas comunes que cada modelo debe pasar: tuve que hacer algunos ajustes más para que estas pruebas funcionen para FSMT (principalmente para adaptarse a la configuración de los 2 diccionarios) y tuve que anular algunas pruebas que no se podían ejecutar debido a la singularidad de este modelo, para omitirlas. Puedes ver los resultados aquí .

Agregué una prueba más que realiza una evaluación ligera de BLEU: utilicé solo 8 entradas de texto para cada uno de los 4 modelos y medí las puntuaciones de BLEU en ellas. Aquí está la prueba y el script que generó los datos .

SinusoidalPositionalEmbedding

fairseq utilizaba una implementación ligeramente diferente de SinusoidalPositionalEmbedding a la utilizada por transformers. Inicialmente copié la implementación de fairseq. Pero al intentar hacer que la suite de pruebas funcione, no pude hacer que las pruebas de torchscript pasaran. SinusoidalPositionalEmbedding fue escrita de manera que no forme parte de state_dict y no se guarde con los pesos del modelo; todos los pesos generados por esta clase son deterministas y no se entrenan. fairseq utilizaba un truco para hacer que esto funcione de manera transparente, no convirtiendo sus pesos en un parámetro o un buffer, y luego, durante forward, cambiando los pesos al dispositivo correcto. torchscript no lo tomaba bien, ya que quería que todos los pesos estuvieran en el dispositivo correcto antes de la primera llamada a forward.

Tuve que reescribir la implementación para convertirla en una subclase normal de nn.Embedding y luego agregar la funcionalidad para no guardar estos pesos durante save_pretrained() y para que from_pretrained() no arroje un error si no puede encontrar esos pesos durante la carga de state_dict.

Evaluación

Sabía que el modelo portado estaba funcionando bastante bien según mis pruebas manuales con un gran cuerpo de texto, pero no sabía qué tan bien se desempeñaba comparativamente con el original. Así que era hora de evaluar.

Para la tarea de traducción, se utiliza el puntaje BLEU como métrica de evaluación. transformers tiene un script llamado run_eval.py para realizar la evaluación.

Aquí tienes una evaluación para el par ru-en

export PAIR=ru-en
export MODEL=facebook/wmt19-$PAIR
export DATA_DIR=data/$PAIR
export SAVE_DIR=data/$PAIR
export BS=64
export NUM_BEAMS=5
export LENGTH_PENALTY=1.1
mkdir -p $DATA_DIR
sacrebleu -t wmt19 -l $PAIR --echo src > $DATA_DIR/val.source
sacrebleu -t wmt19 -l $PAIR --echo ref > $DATA_DIR/val.target
PYTHONPATH="src:examples/seq2seq" python examples/seq2seq/run_eval.py $MODEL \
$DATA_DIR/val.source $SAVE_DIR/test_translations.txt --reference_path $DATA_DIR/val.target \
--score_path $SAVE_DIR/test_bleu.json --bs $BS --task translation --num_beams $NUM_BEAMS \
--length_penalty $LENGTH_PENALTY --info $MODEL --dump-args

que tomó unos minutos en ejecutarse y devolvió:

{'bleu': 39.0498, 'n_obs': 2000, 'runtime': 184, 'seconds_per_sample': 0.092, 
'num_beams': 5, 'length_penalty': 1.1, 'info': 'ru-en'}

Puedes ver que la puntuación BLEU fue 39.0498 y que se evaluó utilizando 2000 entradas de prueba, proporcionadas por sacrebleu utilizando el conjunto de datos wmt19.

Recuerda, no pude usar el conjunto de modelos, así que a continuación necesitaba encontrar el punto de control con el mejor rendimiento. Para ese propósito, escribí un script llamado fsmt-bleu-eval-each-chkpt.py que convirtió cada punto de control, ejecutó el script de evaluación y reportó el mejor. Como resultado, supe que model4.pt estaba ofreciendo el mejor rendimiento, de los 4 puntos de control disponibles.

No estaba obteniendo las mismas puntuaciones BLEU que las reportadas en el documento original, así que a continuación necesitaba asegurarme de que estábamos comparando los mismos datos utilizando las mismas herramientas. A través de preguntar en el problema de fairseq, me dieron el código que fue utilizado por los desarrolladores de fairseq para obtener sus puntuaciones BLEU – lo encontrarás aquí . Pero, lamentablemente, su método utilizaba un enfoque de reordenamiento que no fue revelado. Además, evaluaron las salidas antes de la des-tokenización y no la salida real, que aparentemente obtiene una puntuación mejor. En resumen – no estábamos puntuando de la misma manera (*).

  • nota al pie: el documento “A Call for Clarity in Reporting BLEU Scores” invita a los desarrolladores a comenzar a utilizar el mismo método para calcular las métricas (resumen: utiliza sacrebleu ).

Actualmente, este modelo portado está ligeramente por detrás del original en las puntuaciones BLEU, porque no se utiliza el conjunto de modelos, pero es imposible determinar la diferencia exacta hasta que se utilice el mismo método de medición.

Portando nuevos modelos

Después de subir los 4 modelos de fairseq aquí, se sugirió portar 3 modelos de AllenAI wmt16 y 2 modelos wmt19 ( Jungo Kasai, et al ). La portabilidad fue muy sencilla, ya que solo tuve que averiguar cómo unir todos los archivos fuente, ya que estaban dispersos en varios archivos no relacionados. Una vez hecho esto, la conversión funcionó sin problemas.

El único problema que descubrí después de la portabilidad es que estaba obteniendo una puntuación BLEU más baja que la original. Jungo Kasai, el creador de estos modelos, fue muy útil al sugerir que se utilizó un hiperparámetro personalizado length_penalty=0.6, y una vez que lo configuré, obtuve resultados mucho mejores.

Este descubrimiento me llevó a escribir un nuevo script: run_eval_search.py , que se puede utilizar para buscar varios hiperparámetros que lleven a las mejores puntuaciones BLEU. Aquí tienes un ejemplo de cómo se utiliza:

# espacio de búsqueda
export PAIR=ru-en
export DATA_DIR=data/$PAIR
export SAVE_DIR=data/$PAIR
export BS=32
mkdir -p $DATA_DIR
sacrebleu -t wmt19 -l $PAIR --echo src > $DATA_DIR/val.source
sacrebleu -t wmt19 -l $PAIR --echo ref > $DATA_DIR/val.target
PYTHONPATH="src:examples/seq2seq" python examples/seq2seq/run_eval_search.py stas/wmt19-$PAIR \
$DATA_DIR/val.source $SAVE_DIR/test_translations.txt --reference_path $DATA_DIR/val.target \
--score_path $SAVE_DIR/test_bleu.json --bs $BS --task translation \
--search="num_beams=5:8:11:15 length_penalty=0.6:0.7:0.8:0.9:1.0:1.1 early_stopping=true:false"

Aquí se busca a través de todas las posibles combinaciones de num_beams, length_penalty y early_stopping.

Una vez que haya terminado de ejecutarse, informará:

bleu  | num_beams | length_penalty | early_stopping
----- | --------- | -------------- | --------------
39.20 |        15 |            1.1 |              0
39.13 |        11 |            1.1 |              0
39.05 |         5 |            1.1 |              0
39.05 |         8 |            1.1 |              0
39.03 |        15 |            1.0 |              0
39.00 |        11 |            1.0 |              0
38.93 |         8 |            1.0 |              0
38.92 |        15 |            1.1 |              1
[...]

Puedes ver que en el caso de transformers, early_stopping=False funciona mejor ( fairseq utiliza el equivalente de early_stopping=True).

Por lo tanto, para los 5 nuevos modelos, utilicé este script para encontrar los mejores parámetros por defecto y los utilicé al convertir los modelos. El usuario aún puede anular estos parámetros al invocar generate(), pero ¿por qué no proporcionar los mejores valores por defecto?

Encontrarás los 5 modelos de AllenAI portados aquí .

Más scripts

Dado que cada grupo de modelos portados tiene sus propias particularidades, hice scripts dedicados para cada uno de ellos, de manera que sea fácil reconstruir las cosas en el futuro o crear nuevos scripts para convertir nuevos modelos. Encontrarás todos los scripts de conversión, evaluación y otros aquí .

Tarjetas de modelo

Otra cosa importante es que no es suficiente portar un modelo y ponerlo a disposición de los demás. Es necesario proporcionar información sobre cómo utilizarlo, particularidades sobre los hiperparámetros, fuentes de conjuntos de datos, métricas de evaluación, etc. Esto se hace creando tarjetas de modelo, que son simplemente archivos README.md que comienzan con algunos metadatos que son utilizados por el sitio web de los modelos, seguidos de toda la información útil que se puede compartir.

Por ejemplo, tomemos la tarjeta de modelo facebook/wmt19-en-ru. Aquí está su inicio:

---
language: 
- en
- ru
thumbnail:
tags:
- translation
- wmt19
- facebook
license: apache-2.0
datasets:
- wmt19
metrics:
- bleu
---

# FSMT

## Descripción del modelo

Esta es una versión portada de 
[...]

Como puedes ver, definimos los idiomas, etiquetas, licencia, conjuntos de datos y métricas. Hay una guía completa para escribir estas tarjetas en Model sharing and uploading . El resto es el documento markdown que describe el modelo y sus particularidades. También puedes probar los modelos directamente desde las páginas de los modelos gracias a los widgets de inferencia. Por ejemplo, para la traducción de inglés a ruso: https://huggingface.co/facebook/wmt19-en-ru?text=My+name+is+Diego+and+I+live+in+Moscow .

Documentación

Finalmente, se necesitaba agregar la documentación.

Afortunadamente, la mayoría de la documentación se genera automáticamente a partir de los docstrings en los archivos del módulo.

Como antes, copié docs/source/model_doc/bart.rst y lo adapté a FSMT. Cuando estuvo listo, lo enlacé añadiendo la entrada fsmt dentro de docs/source/index.rst

Utilicé:

make docs

para probar que el documento recién agregado se construyera correctamente. El archivo que necesitaba verificar después de ejecutar ese comando era docs/_build/html/model_doc/fsmt.html – simplemente lo cargué en mi navegador y verifiqué que se renderizara correctamente.

Aquí está el documento fuente final docs/source/model_doc/fsmt.rst y su versión renderizada .

Es hora de hacer un PR

Una vez sentí que mi trabajo estaba bastante completo, estaba listo para enviar mi PR.

Dado que este trabajo implicaba muchos commits de git, quería hacer un PR limpio, así que utilicé la siguiente técnica para combinar todos los commits en uno solo en una nueva rama. Esto mantuvo todos los commits iniciales en su lugar si quisiera acceder a alguno de ellos más tarde.

La rama en la que estaba desarrollando se llamaba fair-wmt, y la nueva rama desde la cual iba a enviar el PR la llamé fair-wmt-clean, así que esto es lo que hice:

git checkout master
git checkout -b fair-wmt-clean
git merge --squash fair-wmt
git commit -m "Listo para PR"
git push origin fair-wmt-clean

Luego fui a GitHub y envié este PR basado en la rama fair-wmt-clean.

Tomó dos semanas de varios ciclos de retroalimentación, seguidos de modificaciones y más ciclos. Eventualmente todo fue satisfactorio y el PR se fusionó.

Mientras esto sucedía, encontraba problemas aquí y allá, agregaba nuevas pruebas, mejoraba la documentación, etc., así que fue tiempo bien invertido.

Posteriormente presenté algunos PR adicionales con cambios después de mejorar y rehacer algunas características, agregando varios scripts de compilación, modelos de tarjetas, etc.

Dado que los modelos que porté pertenecían a las organizaciones facebook y allenai, tuve que pedirle a Sam que moviera esos archivos de modelos desde mi cuenta en s3 a las organizaciones correspondientes.

Pensamientos finales

  • Aunque no pude portar el conjunto de modelos ya que transformers no lo admite, por otro lado el tamaño de descarga de los modelos finales facebook/wmt19-* es de 1.1GB y no de 13GB como en el original. Por alguna razón, el original incluye el estado del optimizador guardado en el modelo, por lo que agrega casi 9GB (4×2.2GB) de peso muerto para aquellos que solo quieren descargar el modelo para usarlo tal cual para traducir texto.

  • Aunque el trabajo de portar parecía muy desafiante al principio ya que no conocía los detalles internos ni de transformers ni de fairseq, mirando hacia atrás no fue tan difícil después de todo. Esto se debió principalmente a que la mayoría de los componentes ya estaban disponibles para mí en las diversas partes de transformers: solo necesitaba encontrar las partes que necesitaba, en su mayoría tomándolas prestadas de otros modelos, y luego ajustarlas para hacer lo que necesitaba. Esto fue cierto tanto para el código como para las pruebas. Reformulemos eso: portar fue difícil, pero habría sido mucho más difícil si tuviera que escribirlo todo desde cero. Y encontrar las partes correctas no fue fácil.

Agradecimientos

  • Tener a Sam Shleifer como mentor a lo largo de este proceso fue de gran ayuda para mí, tanto por su soporte técnico como, igual de importante, por inspirarme y animarme cuando estaba atascado.

  • El proceso de fusión del PR tomó un par de semanas antes de ser aceptado. Durante esta etapa, además de Sam, Lysandre Debut y Sylvain Gugger contribuyeron mucho con sus ideas y sugerencias, las cuales integré en el código base.

  • Estoy agradecido con todos los que han contribuido al código base de transformers, lo cual allanó el camino para mi trabajo.

Notas

Impresión automática en Jupyter Notebook

Mi cuaderno de Jupyter está configurado para imprimir automáticamente todas las expresiones, por lo que no tengo que escribir print() explícitamente. El comportamiento predeterminado es imprimir solo la última expresión de cada celda. Por lo tanto, si lees los resultados en mis cuadernos, es posible que no sean los mismos que si los ejecutaras tú mismo, a menos que tengas la misma configuración.

Puedes habilitar la función de impresión automática en la configuración de tu cuaderno de Jupyter agregando lo siguiente a ~/.ipython/profile_default/ipython_config.py (crea el archivo si no lo tienes):

c = get_config()
# Ejecutar todos los nodos de forma interactiva
c.InteractiveShell.ast_node_interactivity = "all"
# restaurar el comportamiento original
# c.InteractiveShell.ast_node_interactivity = "last_expr"

y reiniciar tu servidor de Jupyter Notebook.

Para asegurarte de que todos los enlaces funcionen si lees este artículo mucho después de que haya sido escrito, los enlaces se hicieron a una versión SHA específica del código y no necesariamente a la última versión. Esto es para que si los archivos fueron renombrados o eliminados, aún puedas encontrar el código al que se hace referencia en este artículo. Si quieres asegurarte de que estás viendo la última versión del código, reemplaza el código hash en los enlaces con master. Por ejemplo, un enlace:

https://github.com/huggingface/transformers/blob/129fdae04033fe4adfe013b734deaec6ec34ae2e/src/transformers/modeling_fsmt.py

se convierte en:

https://github.com/huggingface/transformers/blob/master/src/transformers/convert_fsmt_original_pytorch_checkpoint_to_pytorch.py

¡Gracias por leer!

We will continue to update Zepes; if you have any questions or suggestions, please contact us!

Share:

Was this article helpful?

93 out of 132 found this helpful

Discover more

Inteligencia Artificial

Por qué importa el Hype Pensar de manera práctica sobre la IA

ELIZA era un chatbot temprano que compartía algunas similitudes con ChatGPT. ¿Por qué importa esta emoción? Bueno, cu...

Inteligencia Artificial

Principales extensiones de Chrome con inteligencia artificial AI

La idea de una máquina que escriba por ti ha pasado de ser ciencia ficción a realidad gracias a los avances en la tec...

Inteligencia Artificial

AWS Inferentia2 se basa en AWS Inferentia1 ofreciendo un rendimiento 4 veces mayor y una latencia 10 veces menor.

El tamaño de los modelos de aprendizaje automático (ML) - modelos de lenguaje grande (LLM) y modelos fundamentales (F...

Aprendizaje Automático

Esta Herramienta de IA Explica Cómo la IA 'Ve' Imágenes y por qué Puede Equivocarse al Confundir un Astronauta con una Pala.

Es ampliamente reconocido que la inteligencia artificial (IA) ha logrado avances significativos en los últimos años, ...