Met fastText domeinnaam suggesties in realtime vertalen

We onderzoeken voortdurend manieren om goede domeinnamen te genereren. Bijna elk Engels woord heeft al een bijbehorend .com-domein, en ook veel populaire woordparen, zoals HelloWorld.com, zijn bezet. Je kunt proberen een belangrijk woord voor je bedrijf naar andere talen te vertalen. Zo wordt fast in het Frans vite, in het Spaans rápida of rápido en in het Italiaans veloce. Het zijn geen Engelse woorden, maar de talen delen wortels en de woorden kunnen bij je bedrijf passen. Google Translate is een geweldig product, maar via de interface bekijken hoe een woord er in meerdere talen uitziet is omslachtig. We wilden een tool bouwen die een Engels woord direct naar meerdere talen vertaalt.

Onderzoek naar machinevertaling begon in de jaren vijftig en ontwikkelt zich nog steeds. We willen direct van Engels naar meerdere talen vertalen op gewone hardware, zonder GPU’s of andere dure apparatuur. Domeinnamen moeten kort zijn, dus we richten ons op losse woorden in plaats van uitdrukkingen. We hebben ervaring met woordvectoren om domeinen te vinden die te koop staan en zagen dat Facebook Research een dataset publiceerde die woordvectoren tussen talen uitlijnt.

Wacht even, wat is een woordvector?

Woordvectoren worden gemaakt met een neuraal netwerk dat uit grote hoeveelheden tekst, zoals een webcrawl of Wikipedia, leert hoe woorden met elkaar samenhangen. Allison Parrish, universitair docent bij NYU, deelde een van de beste interactieve uitleggen over woordvectoren die ik heb gezien. Bekijk die eens. Allison gebruikt kleuren om te laten zien hoe je een woord kunt koppelen aan getalswaarden voor Rood, Groen en Blauw. De PNG- en JPG-standaarden ondersteunen bijvoorbeeld beide 24-bits kleur: 0 tot 255 voor elk van R, G en B. Rood is 255,0,0, groen 0,255,0 en blauw 0,0,255. Je leert iets als 250,0,0 in code herkennen als sterk rood.

fastText

In 2016 bracht Facebook Research fastText uit, samen met vooraf getrainde modellen die miljoenen woorden aan een numerieke weergave koppelen. Deze modellen zetten woorden om in 300 32-bits floats. Dat legt veel detail en nuance vast, maar is voor mensen veel moeilijker te begrijpen. De fastText-woordvector voor red ziet er zo uit:

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

Uitgelijnde woordvectoren

Later publiceerde Facebook Research uitgelijnde woordvectoren voor 44 talen. Die werken als een Steen van Rosetta en bieden een manier om woorden uit deze talen in dezelfde vectorruimte te ordenen. Om het tot drie dimensies terug te brengen, kunnen we bekijken hoe rood in het Engels, Frans en de vectorruimte wordt weergegeven.

Een weergave van de RGB-kleurruimte als kubus.

Omdat we één woord zonder context bekijken, kan de vertaling dubbelzinnig zijn in talen met mannelijke en vrouwelijke vormen. In de praktijk sluiten de vectoren niet perfect op elkaar aan. Er kunnen kleine verschillen in de vectorweergaven zijn, dus we moeten benaderende matches meenemen. Omdat we in een vectorruimte werken, kunnen we de euclidische afstand meten van bijvoorbeeld red naar een goede match in een andere taal.

Instant Distance

Computers zijn snel en kunnen miljoenen euclidische afstandsberekeningen relatief vlot met brute kracht uitvoeren. Maar niet direct. We hebben een manier nodig om deze punten te indexeren en naburige punten in de ruimte te vinden. Veel technieken uit machine learning en kunstmatige intelligentie zijn afhankelijk van efficiënt navigeren door vectorruimten. Er zijn verschillende aanpakken en implementaties beschikbaar. We wilden een implementatie volledig in Rust, maar vonden geen productierijpe optie. Daarom bouwden en publiceerden we instant-distance: een snelle implementatie volledig in Rust van Hierarchical Navigable Small World-grafen, met Python-bindings.

Alles bij elkaar

Om een eenvoudige vertaaltool te bouwen, downloaden we eerst de woordvectorgegevens van fastText. Vervolgens indexeren we de woordvectoren met Instant Distance. Zodra de index klaar is, slaan we de dataset op in het bestandssysteem, samen met een koppeling van woord naar vector in een JSON-bestand.

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)

Met deze tools kunnen we ten slotte invoer, een woord, omzetten in zijn woordvector en Instant Distance gebruiken om de dichtstbijzijnde buren te vinden. Omdat alle woordvectoren zijn uitgelijnd, moeten de dichtstbijzijnde vectoren in andere talen sterk overeenkomen, of zelfs directe vertalingen zijn.

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:]}")

Dit zijn bijvoorbeeld de resultaten voor het vertalen van het Engelse woord ‘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

Probeer het uit

Bekijk het volledige voorbeeld bij Instant Distance op GitHub. Heb je vragen? Open gerust een issue op GitHub!