Siempre exploramos formas de generar buenos nombres de dominio. Casi todas las palabras inglesas tienen un dominio .com asociado y muchas parejas populares, como HelloWorld.com, también están ocupadas. Una opción es traducir a otros idiomas una palabra importante para tu negocio. Por ejemplo, fast se traduce como vite en francés, rápida o rápido en español y veloce en italiano. Aunque no son palabras inglesas, los idiomas comparten raíces y podrían servir para tu empresa. Google Translate es un producto increíble, pero usar su interfaz para ver una palabra en otros idiomas resulta tedioso. Nos propusimos crear una herramienta que traduzca una palabra inglesa a varios idiomas al instante.
El estudio de la traducción automática se remonta a la década de 1950 y sigue evolucionando. Queremos traducir del inglés a varios idiomas al instante en hardware convencional, sin GPU ni otros equipos caros. Los nombres de dominio deben ser cortos, así que nos centramos en palabras individuales en lugar de frases. Ya teníamos experiencia con vectores de palabras para encontrar dominios en venta y vimos que Facebook Research había publicado un conjunto de datos que alinea vectores de un idioma con los de otro.
Un momento, ¿qué es un vector de palabras?
Los vectores de palabras se generan con una red neuronal que aprende cómo se relacionan las palabras a partir de un gran corpus, como un rastreo web o Wikipedia. Allison Parrish, profesora adjunta en NYU, compartió uno de los mejores tutoriales interactivos sobre vectores de palabras que he visto. Échale un vistazo. Allison usa colores para ilustrar cómo una palabra puede asociarse con valores numéricos de rojo (R), verde (G) y azul (B). Los estándares PNG y JPG, por ejemplo, admiten color de 24 bits: de 0 a 255 para cada componente R, G y B. El rojo se representa como 255,0,0; el verde, como 0,255,0; y el azul, como 0,0,255. Aprendes a reconocer algo como 250,0,0 en el código como un color muy rojo.
fastText
En 2016, Facebook Research publicó fastText junto con varios modelos preentrenados que asignan una representación numérica a millones de palabras. Estos modelos representan cada palabra mediante 300 números de coma flotante de 32 bits. Esto captura muchos detalles y matices, pero resulta mucho más difícil de interpretar para una persona. El vector de fastText para la palabra red tiene este aspecto:
red -0.0331 -0.0465 0.1046 0.0201 0.0665 -0.0394 0.0515 -0.0598 0.0905 0.0738 0.0871 0.0062 0.0002 -0.0135 0.1012 -0.0092 -0.1063 -0.0967 0.0297 0.0790 -0.0429 -0.0470 -0.0926 -0.0227 -0.0240 -0.0768 0.0174 -0.0628 -0.0714 0.0413 0.0072 0.0746 0.0332 0.0780 0.0248 0.0083 -0.0807 -0.0272 0.0805 -0.0736 -0.0323 -0.0140 0.0081 0.0639 -0.0186 -0.0961 0.0240 -0.0159 0.0252 0.0425 0.0403 -0.1151 -0.0582 0.0228 0.0503 0.0262 0.0092 -0.0314 -0.0031 0.0238 0.0023 0.0231 0.1031 0.0147 0.0032 0.0197 -0.0749 0.0452 -0.0060 0.0173 -0.0828 0.0347 0.0330 -0.0970 0.0665 -0.0090 -0.0148 -0.0379 -0.0735 -0.0456 0.0362 -0.0038 -0.0989 0.0229 -0.0710 0.0076 -0.0314 0.0331 0.0470 -0.0968 -0.0182 0.0503 -0.0603 0.0900 0.0617 0.0198 0.0360 0.0885 -0.0665 0.0382 0.0162 -0.0352 -0.0643 0.0298 -0.0647 -0.0815 0.0507 0.0307 -0.0312 -0.0265 -0.0255 -0.0556 0.0302 0.0085 -0.0142 0.0116 0.0497 -0.0091 -0.0327 -0.0533 0.0853 -0.0028 0.0138 0.0235 0.0288 0.0766 -0.0008 0.0410 -0.0574 0.0001 0.0378 0.0842 0.0237 0.0557 -0.0578 -0.0145 -0.0006 -0.1553 -0.0657 0.0826 -0.0335 0.1468 0.0287 -0.0240 -0.0060 0.1243 -0.0685 -0.0024 -0.0419 0.0122 0.0002 -0.1673 -0.1169 -0.0371 -0.0072 -0.0133 -0.0355 0.0781 0.0487 -0.0785 0.1488 0.0351 -0.1184 -0.0185 0.0348 0.0116 -0.0598 -0.0082 0.1296 -0.0158 -0.0234 -0.0796 -0.0322 -0.0004 -0.0170 0.0290 -0.0135 -0.0658 0.0224 0.0262 -0.0747 -0.0174 -0.0673 0.0018 -0.0009 -0.0170 -0.0229 0.0128 0.0414 0.0009 0.0807 -0.0990 0.1185 0.0776 -0.1242 -0.0860 -0.0464 0.0127 -0.0994 0.0284 0.0295 -0.0607 0.0268 0.0738 0.0820 -0.0623 -0.1275 -0.0181 -0.0645 0.0423 -0.0196 0.0610 -0.0459 -0.0614 0.1134 0.0480 -0.0723 -0.0421 -0.0073 -0.0136 -0.0843 -0.0286 -0.0247 0.0456 -0.0090 -0.0546 -0.0464 0.0170 0.0580 -0.0434 0.0340 0.0199 0.0258 -0.0641 0.0110 -0.1129 0.0479 -0.0298 -0.0738 0.0475 -0.0210 0.0199 -0.0134 -0.0297 -0.0400 0.0186 0.0519 0.0505 0.0018 -0.0292 0.0482 0.0071 -0.0222 -0.0302 0.0711 -0.0198 0.0230 -0.0573 0.1053 0.0609 0.0517 0.0693 -0.0668 -0.0047 -0.0557 -0.0430 -0.0130 0.0693 -0.0305 -0.1101 -0.0303 -0.0511 -0.0628 0.0036 0.0101 -0.0206 0.1078 0.0520 -0.0476 0.0408 -0.0027 -0.0753 0.0087 0.0203 0.1821 -0.0566 0.0721 0.0880 -0.0955 -0.1142 -0.0118 -0.0209 0.0230 0.0313 -0.0339 -0.0700 0.0841 -0.0484 -0.0148 -0.0190
Vectores de palabras alineados
Más adelante, Facebook Research publicó vectores de palabras alineados para 44 idiomas. Funcionan como una piedra de Rosetta y permiten organizar palabras de esos idiomas en el mismo espacio vectorial. Para volver a las tres dimensiones, podemos ver cómo se representaría el rojo en inglés, francés y el espacio vectorial.
Como observamos una sola palabra sin contexto, la traducción puede ser ambigua en idiomas con formas femeninas o masculinas. En la práctica, los vectores no se alinean perfectamente. Puede haber pequeñas variaciones entre sus representaciones, así que debemos considerar coincidencias aproximadas. Al trabajar en un espacio vectorial, podemos medir la distancia euclídea entre algo como red y una coincidencia cercana en otro idioma.
Instant Distance
Los ordenadores son rápidos y pueden realizar por fuerza bruta millones de cálculos de distancia euclídea con relativa rapidez. Pero no al instante. Necesitamos construir un índice de estos puntos para encontrar los vecinos de un punto en el espacio. Muchas técnicas de aprendizaje automático o IA dependen de recorrer espacios vectoriales de forma eficiente, y existen diversos enfoques e implementaciones disponibles. Queríamos una implementación íntegramente en Rust, pero no encontramos ninguna lista para producción. Por eso creamos y publicamos instant-distance, una implementación rápida y completamente en Rust de grafos Hierarchical Navigable Small World, con enlaces para Python.
Todo junto
Para crear una herramienta sencilla de traducción, empezamos por descargar los datos de vectores publicados por fastText. Después los indexamos con Instant Distance. Cuando termina la construcción del índice, guardamos el conjunto de datos en el sistema de archivos junto con una correspondencia de palabra a vector en formato JSON.
LANGS = ("en", "fr", "it")
LANG_REPLACE = "$$lang"
WORD_MAP_PATH = f"./data/{'_'.join(LANGS)}.json"
BUILT_IDX_PATH = f"./data/{'_'.join(LANGS)}.idx"
DL_TEMPLATE = f"https://dl.fbaipublicfiles.com/fasttext/vectors-aligned/wiki.{LANG_REPLACE}.align.vec"
points = []
values = []
word_map = {}
async with aiohttp.ClientSession() as session:
for lang in LANGS:
# Construct a url for each language
url = DL_TEMPLATE.replace(LANG_REPLACE, lang)
# Ensure the directory and files exist
os.makedirs(os.path.dirname(BUILT_IDX_PATH), exist_ok=True)
lineno = 0
async with session.get(url) as resp:
while True:
lineno += 1
line = await resp.content.readline()
if not line:
# EOF
break
linestr = line.decode("utf-8")
tokens = linestr.split(" ")
# The first token is the word and the rest
# are the embedding
value = tokens[0]
embedding = [float(p) for p in tokens[1:]]
# We only go from english to the other two languages
if lang == "en":
word_map[value] = embedding
else:
# Don't index words that exist in english
# to improve the quality of the results.
if value in word_map:
continue
# We track values here to build the instant-distance index
# Every value is prepended with 2 character language code.
# This allows us to determine language output later.
values.append(lang + value)
points.append(embedding)
# Build the instant-distance index and dump it out to a file with .idx suffix
print("Building index... (this will take a while)")
hnsw = instant_distance.HnswMap.build(points, values, instant_distance.Config())
hnsw.dump(BUILT_IDX_PATH)
# Store the mapping from string to embedding in a .json file
with open(WORD_MAP_PATH, "w") as f:
json.dump(word_map, f)
Por último, con estas herramientas podemos convertir una entrada —una palabra— en su vector y usar Instant Distance para encontrar sus vecinos más próximos. Como todos los vectores están alineados, los más cercanos en distintos idiomas deberían ser muy similares, si no una traducción directa.
with open(WORD_MAP_PATH, "r") as f:
word_map = json.load(f)
# Get an embedding for the given word
embedding = word_map.get(word)
if not embedding:
print(f"Word not recognized: {word}")
exit(1)
hnsw = instant_distance.HnswMap.load(BUILT_IDX_PATH)
search = instant_distance.Search()
hnsw.search(embedding, search)
# Print the results
for result in list(search)[:10]:
# We know that the first two characters of the value is the language code
# from when we built the index.
print(f"Language: {result.value[:2]}, Translation: {result.value[2:]}")
Por ejemplo, estos son los resultados de traducir la palabra inglesa «hello»:
Language: fr, Translation: bonjours
Language: fr, Translation: bonsoir
Language: fr, Translation: salutations
Language: it, Translation: buongiorno
Language: it, Translation: buonanotte
Language: fr, Translation: rebonjour
Language: it, Translation: auguri
Language: fr, Translation: bonjour,
Language: it, Translation: buonasera
Language: it, Translation: chiamatemi
Pruébalo
Puedes consultar el ejemplo completo en Instant Distance en GitHub. Si tienes preguntas, ¡no dudes en abrir una incidencia en GitHub!