Übersetzung mit lokalen Sprachmodellen.
Ein eigener kostenfreier Übersetzer für große Textmengen.

LLM Übersetzung

Branche
divers
Thema
NLP, LLM
Tools
Transformers, Torch
Projektdauer
2 Wochen

Übersetzung eines englischen Datensatzes

Trainingskosten der LLaMA Netze

Bisher haben wir die Sprachmodelle lediglich mit einer großen Menge an Daten trainiert. Damit führt das Sprachmodell lediglich die Eingabe fort und man muss speziell mit diesem interagieren.

Der Bundeskanzler von Deutschland ist ...
Wir können also nicht einfach Fragen stellen und das Sprachmodell antwortet hierauf. Hierfür müssen wir noch ein Finetuning durchführen. Generell kann man viele unterschiedliche Aufgaben eintrainieren. Hierzu zählen:

- Klassifizieren: Einteilen in gute/schlechte Bewertungen, Fragen mit vorgefertigten Antworten, ...
- Extrahieren: Kundendaten, Daten
- Zusammenfassen: Zusammenfassen langer Texte, Erzeugen von Abstracts
- Beantworten: Beantworten von Fragen mit oder ohne vorgegebene Informationen
- Suchen: Suchen von Informationen anhand von sprachlichen Ähnlichkeiten

Zum finetunen eines deutschen Netzes wird ein entsprechender Datensatz benötigt. Einerseits können solche Datensätze manuell erzeugt werden. Ein Beispiel ist der Datensatz von Databricks der von mehr als 5000 Beschäftigten über einen internen Wettbewerb erzeugt wurde. Andererseits kann der Datensatz auch über die Interaktion mit anderen LLMs von Anthropic oder OpenAI erzeugt werden. Hierbei gibt es ebenfalls viele verschiedene Herangehensweisen, wie das allgemeine Sammeln von Interaktionen mit ChatGPT über ShareGPT oder der Evol-Datensatz von WizardLM .

Im Folgenden soll jedoch kein neuer Datensatz erzeugt werden, da dies bei manueller Erstellung sehr aufwendig sein kann und bei automatisierter Erstellung ein API Zugang inkl. Bezahlung notwendig ist. Eine Alternative besteht darin, einen bestehenden Datensatz zu übersetzen. Hierfür bieten sich einige Datensätze an, die für das Finetunen von englischen Sprachmodellen verwendet werden. Diese lassen sich in der Regel über die entsprechenden Github Repositories oder über Huggingface herunterladen.

Eine Übersicht kann erlangt werden, indem man zum Beispiel die Benchmarks unterschiedlicher Netze anschaut und dann die bei deren Training verwendeten Datensätze für das eigene Training ebenfalls nutzt. Generell muss man hier die unterschiedlichen Netzgrößen ebenfalls unterscheiden. Auf Consumer-GPUs mit 24 GB VRAM können mit einigen Tricks bis zu 33B große Netze trainiert werden.

Darüber hinaus gibt es meist von diesen Datensätzen noch "unfiltered" Datensätze, in denen Limitierungen in den LLM-Ausgaben herausgefiltert werden. Dies hat zwar den Vorteil, dass das später trainierte Modell teilweise bessere Rückmeldungen liefert und weniger nach dem Kontext "Ich bin ein KI-Modell und habe keine Meinung hierzu" antwortet, führt jedoch auch dazu, dass keinerlei Sicherheitsmechanismen mehr vorhanden sind. Für die Übersetzung bieten sich ebenfalls unterschiedliche Wege an:

Übersetzung mit DeepL oder Google Übersetzer

Die Preise für die Übersetzung betragen ca. 20€ pro 1 Mio. Zeichen. Der Alpaca evol Instruct Datensatz zum Beispiel besitzt über 130 Mio. Zeichen, was einem Preis von 2605€ entsprechen würde.

import pandas as pd
print("Preisberechnung für Deepl")
dataset = pd.read_json("evol_instruct_70k/alpaca_evol_instruct_70k.json")
total = dataset["instruction"].str.len().sum()+dataset["output"].str.len().sum()
print(f"Total characters: {total}, Total price: {total/1000000*20}")
Output
Preisberechnung für DeepL
Total characters: 130296989, Total price: 2605.9397799999997 

Übersetzung mit einem Sprachmodell / GPT4

Nehmen wir an, dass im Englischen ca. 4 Zeichen einem Token entsprechen folgen hieraus 977 Dollar bei 0.03 Dollar pro 1K Token mit dem GPT4 Modell und 8K Kontext. Da in anderen Sprachen weniger Zeichen pro Token folgen, liegt der Preis wahrscheinlich sogar noch über dem berechneten Preis. Lediglich das Chat-GPT Modell mit 0.002 Dollar pro 1k Token scheint noch sinnvoll zu sein. Hierbei würden sich Kosten von ca. 100€ ergeben.

import pandas as pd
print("Preisberechnung für GPT4")

dataset = pd.read_json("alpaca_evol_instruct_70k.json")
total = dataset["instruction"].str.len().sum()+dataset["output"].str.len().sum()
print(f"Total characters: {total}, Total price: {total/4/1000*0.03}")
print("Preisberechnung für Chat-GPT")
print(f"Total characters: {total}, Total price: {total/4/1000*0.002}")
Output
Preisberechnung für GPT4
Total characters: 130296989, Total price: 977.2274175
Preisberechnung für Chat-GPT
Total characters: 130296989, Total price: 65.1484945
    

Übersetzung mit OpenSource Sprachmodellen

Da die beiden Alternativen über API Calls relativ teuer sein können, bietet es sich an, spezielle Übersetzungsmodelle zu verwenden. Eine Übersicht bietet das OPUS-MT Dashboard , welches unterschiedliche Übersetzungsmodelle vergleicht. Hierbei stechen zwei unterschiedliche Modell raus: facebook/wmt19-en-de und opus-mt-align-en-de

Wir verwenden für die Übersetzung die Python-Bibliothek Transformers, mit der sowohl das Sprachmodell als auch der Tokenizer ausgeführt werden. Wie der Tokenizer funktioniert, wurde bereits im letzten Beitrag beschrieben. Als Datensatz verwenden wir den Alpaca Evol Instruct Datensatz von WizardLM.

Zuerst laden wir das Übersetzungsmodell und den Tokenizer mit der Transformers Bibliothek. Wir haben uns hier für das opus-mt-en-de Modell entschieden, da das Facebook-Modell eine Kombination aus 4 verschiedenen Modellen verwendet, die gemeinsam eine Vorhersage machen. Diese Einbindung mehrerer Modelle als Ensemble ist jedoch noch nicht in der Transformers Bibliothek implementiert.

# Laden des Sprachmodells
from transformers import AutoTokenizer, AutoModelForSeq2SeqLM

model_name = "Helsinki-NLP/opus-mt-en-de"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForSeq2SeqLM.from_pretrained(model_name).to(device="cuda")


Als nächstes Laden wir den Datensatz von Huggingface:

# Downloaden des Datensatzes: 
!git clone https://huggingface.co/datasets/WizardLM/evol_instruct_70k
# Einlesen des Datensatzes
import pandas as pd 
dataset = pd.read_json(r"evol_instruct_70k/alpaca_evol_instruct_70k.json")
dataset
    

Datenvorbereitung

Als erstes extrahieren wir den Code in den Instruktionen und Ausgaben. Das Sprachmodell würde hier viele Fehler einbauen und zum Beispiel for oder if-Schleifen übersetzen. Wir ersetzen den Code lediglich durch "<code_snip>".

import re
import numpy as np 


dataset["instruction_code"] = np.nan
dataset["instruction_cleaned"] = np.nan

for i, row in dataset.iterrows():
    code = re.findall(r"```([\s\S]*?)```", row["instruction"])
    if code == []: 
        code = [np.nan]
    dataset.at[i, "instruction_code"] = str(code) 
    dataset.at[i, "instruction_cleaned"] = re.sub(r"```([\s\S]*?)```", '<code_snip>', row["instruction"], flags=re.DOTALL)
    
    code_out = re.findall(r"```([\s\S]*?)```", row["output"])
    if code_out == []: 
        code_out = [np.nan]
    dataset.at[i, "output_code"] = str(code_out)
    dataset.at[i, "output_cleaned"] = re.sub(r"```([\s\S]*?)```", '<code_snip>', row["output"], flags=re.DOTALL)
                

Da das Sprachmodell keine Zeilenumbrüche nach "\n" übersetzt, müssen wir die Texte entweder an diese Stellen trennen oder wir ersetzen diese durch den HTML-Tag <br>

dataset["instruction"] = dataset["instruction"].str.replace("\n", "<br>")
dataset["output"] = dataset["output"].str.replace("\n", "<br>")

Die Übersetzungsmodelle arbeiten ebenfalls mit Token und haben eine eingeschränkte Tokenlänge von ca. 512 bis 1024 Token. Die Texte werden somit auf eine bestimmte maximale Länge aufgeteilt und möglichst am Ende des Satzes oder Abschnittes getrennt.

def split_long_string(text, max_length):
    result = []
    while len(text) > max_length:
        if r"\n" in text[:max_length]:
            split_index = text[:max_length].rindex(r"\n") + 2
        elif "." in text[:max_length]:
            split_index = text[:max_length].rindex(".") + 1
        else:
            split_index = max_length
        result.append(text[:split_index].strip())
        text = text[split_index:]
    if text:
        result.append(text.strip())
    return result, len(result)

def split_wrapper(long_str_list, max_len=500): 
    short_str_list = []
    split_lens = []

    for single_str in long_str_list: 
        short_string, split_len = split_long_string(single_str, max_len)
        short_str_list.extend(short_string)
        split_lens.append(split_len)

    return short_str_list, split_lens

source_instructions ,split_len_instructions = split_wrapper(source_instructions)
source_outputs ,split_len_outputs = split_wrapper(source_outputs)

Übersetzung

Nun können wir die eigentlichen Texte übersetzen. Hierfür definieren wir zuerst eine Funktion und iterieren diese in Batches über einen Datensatz:

def translate(model, tokenizer, data): 
    tokenized_txt = tokenizer(data, return_tensors="pt", padding=True).to("cuda")
    uebersetzt_txt = model.generate(tokenized_txt["input_ids"],
                                attention_mask=tokenized_txt["attention_mask"],
                                max_length=512, 
                                )
    uebersetzt_txt = tokenizer.batch_decode(uebersetzt_txt, skip_special_tokens=True)

return uebersetzt_txt, tokenized_txt
from tqdm.notebook import tqdm
    BATCH_SIZE = 16
    
    # Instructions übersetzen:
    translated_instructions = []
    for num in tqdm(range(0, len(source_instructions), BATCH_SIZE)):
        batch = source_instructions.iloc[num:num+BATCH_SIZE].to_list()
        uebersetzt_txt, tokenized_txt = translate(model, tokenizer, batch)
        translated_instructions.extend(uebersetzt_txt)
    
    # Output übersetzen:
    translated_outputs = []
    for num in tqdm(range(0, len(source_outputs), BATCH_SIZE)):
    batch = source_outputs.iloc[num:num+BATCH_SIZE].to_list()
    uebersetzt_txt, tokenized_txt = translate(model, tokenizer, batch)
    translated_outputs.extend(uebersetzt_txt)

Datennachbereitung

In diesem Schritt kombinieren wir die einzelnen Abschnitte der Texte wieder und stellen die Zeilenumbrüche wieder in der ursprünglichen Form her. Anschließend können wir die Code-Abschnitte wieder in der ursprünglichen Form einsetzen.

# Hier die Texte wieder joinen: 
def join_data(translated_data, split_lengths): 
    joined_data = []
    curr_len = 0 
    for conv_len in split_lengths: 
        joined_data.append("".join(translated_data[curr_len:curr_len+conv_len]))
        curr_len+=conv_len
    return joined_data

translated_instructions = join_data(translated_instructions, split_len_instructions)
translated_outputs = join_data(translated_outputs, split_len_outputs)

# Textumbrüche widerherstellen 
translated_instructions = list(map(lambda t: t.replace(" < br > ", "\n").replace("<br>", "\n"), translated_instructions))
translated_outputs = list(map(lambda t: t.replace(" < br > ", "\n").replace("<br>", "\n"), translated_outputs))
            
# Code wiederherstellen
TRANSLATED_TOKEN = r"<code_snip>"

def restore_code(translated_data, dataset, TRANSLATED_TOKEN, type="instruction"):
    for n, data in enumerate(translated_data): 
        try: 
            code_snippets = eval(dataset[f"{type}_code"][n])
            old_len = len(code_snippets)
            translated_len = data.count(TRANSLATED_TOKEN)
            cleaned_data = dataset[f"{type}_cleaned"][n]
            if translated_len != old_len: 
                print(f"Len code snippets: {old_len}, translated Code: {translated_len} - {code_snippets} ||| {data} ||| {cleaned_data}")  # Check dass kein code snippet verschwunden ist
            
            for code_snippet in code_snippets: 
                translated_data[n] = translated_data[n].replace(TRANSLATED_TOKEN, code_snip,1)
        except NameError: # No Code-Snippet present
            pass 
    return translated_data


translated_instructions = restore_code(translated_instructions, dataset, TRANSLATED_TOKEN, "instruction")
translated_outputs = restore_code(translated_outputs, dataset, TRANSLATED_TOKEN, "output")
            

Während der Übersetzung bzw. Umwandlung des Codes kommt es teilweise zu Problemen. Das Ersatzwort, "<code_snip>", wird nicht einheitlich übersetzt. Somit können nicht alle Code-Bestandteile korrekt zurückgewandelt werden. Dies betrifft jedoch lediglich jeweils 62 der 70 000 Anweisungen und Rückmeldungen.

Wir konnten damit zeigen, dass es möglich ist einen großen Datensatz lokal mit relativ geringem Aufwand zu übersetzen. Der Datensatz ist auf Huggingface verfügbar.
In dem nächsten Beitrag zeigen wir, wie ein solcher Datensatz verwendet werden kann, um ein Sprachmodell anzupassen.