Pretraining großer Sprachmodelle.
Wir zeigen, wie Sie Ihre eigenen Daten in einem Sprachmodell eintrainieren können.

LLM Training

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

LLM-Datensatz

In dem vorherigen Beitrag haben wir gezeigt wie man unterschiedliche Textformate aufbereiten kann und welche Daten für das LLaMA Training verwendet wurden. Doch wie wurde das LLaMA Netz von Meta trainiert?
Generell gibt es nicht nur ein LLaMA Netz von Meta. Insgesamt wurden 4 unterschiedlich große Sprachmodelle trainiert. Diese Varrieren mit einer Größe zwischen 7B und 65B Parametern.

Die beiden kleinere Netze wurden mit 1T Token trainiert und die beiden größeren Netze mit 1.4T Token. Für das 65B LLaMA Netz hat die Trainingsdauer auf 2048 A100 GPUs ungefähr 21 Tage gedauert.
Wie viel würde also solch ein Training des größten Netzes kosten? Nehmen wir eine Anzahl an 1 Mio. GPU-Rechenstunden an, ergibt sich für ein Cluster mit 8 Grafikkarten (A100) und einem Preis von 12$ pro Stunden ein Gesamtpreis von 1.5 Mio. Dollar. Das Ganze ist jedoch nicht durchführbar, da das Training auf den 8 Grafikkarten über 14 Jahre dauern würde. Für eine kürzere Trainingsdauer von ca. 3 Monaten würde man somit ein Rechencluster mit 512 A100 GPUs benötigen. Die monatlichen Kosten für 16 A100 bei der Google Cloud zum Beispiel würde bei über 1 Mio. USD liegen und damit die insgesamten Kosten bei ca. 2.6 Mio. Dollar. Lässt man die Kosten für die Hardware außen vor und betrachtet nur die verursachten Stromkosten so ergeben sich Gesamtkosten von 135t € bei angenommenen Stromkosten von 30 ct/kWh.

Bei einem kleineren Netz reduzieren sich die Rechenzeiten und Kosten dementsprechend. Damit kann durch eine Anpassung von mehreren kleineren Netzen sowohl Kosten als auch CO2-Emissionen eingespart werden.

Eintrainieren eines eigenen Datensatzes

Wir haben gesehen, dass das initiale Training der Netze enorm Ressourcen benötigt. Die LLaMA Netze ermöglichen es mit deutlich geringerem Aufwand angepasste Sprachmodelle zu erzeugen, da diese bereits die Grundkenntnisse erlernt haben. Sie sind jeodch bis jetzt "nur" Vervollständigungsmodelle. Dementsprechend muss man mit diesen auch interagieren. In einem späteren Schritt zeigen wir, wie man die Sprachmodelle zu sogenannten Chat oder Instruction basierten Sprachmodellen umtrainiert.

Zunächst zeigen wir, wie man den Sprachstil aus Johann Wolfgang von Goethes Faust in ein großes Sprachmodell eintrainiert, um eine Vervollständigung eines vom Nutzer vorgegebenen Textes im Stile von Faust zu erhalten. Wir haben bereits gesehen, dass die LLaMA und Falcon-Modelle lediglich begrenzte Kenntnisse in Deutsch besitzen. Für einen ersten Schritt werden wir also ein spezielles Netz verwenden, das mit einer großen Anzahl an deutschen Daten angepasst wurde: BLOOM-CLP German 6.4B . Die deutschen Sprachkentnisse sind hierbei größer und der Tokenizer ist für deutsch optimiert.

Der vom DFKI verwendete Rohdatensatz enthält teilweise sinnfreie (bspw. Zahlenreihen) und auch nicht jugendfreie Inhalte. Angeblich wurde der Datensatz vom DFKI nachberarbeitet um solche Inhalte zu entfernen. Das fertig trainierte Netz gibt sie jedoch immer noch aus. Exemplarisch dafür stehen die Zahlenreihe 12286;12294 oder der Satzanfang "Die junge Oma", wenn sie im Greedy-Modus eingegeben werden. Bei einer kommerziellen Anwendung sollte unerwünschte Inhalte im Idealfall bereits bei der Datensammlung (scrapen) über eine Ausschussliste, oder bei der Datenaufbereitung entfernt werden. Wenn man sein eigenes Modell auf ein bereits trainiertes Netz aufbauen will, muss die Ausgabe daher zwingend auf solche Inhalte geprüft werden.

Zum Training verwenden wir die Transformer Bibliothek, welche für das eigentliche Training PyTorch verwendet. Da wir als Hardware ein RTX 3090 verwenden, sind noch einige Anpassungen notwendig, unter anderem die Einbindung der Bibliothek Deepspeed, da ansonsten trotz 24 GB nicht ausreichend GPU-Speicher vorhanden wäre.

Tokenizer

Zuerst müssen wir einen Tokenizer laden. Hierfür verwenden wir auch wieder die Transformers Bibliothek:

from transformers import AutoTokenizer, AutoModelForCausalLM

MODEL_NAME = "malteos/bloom-6b4-clp-german"
                
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME, use_fast=False)
tokenizer.pad_token = tokenizer.eos_token
                
Output
Downloading (…)okenizer_config.json:   0%|          | 0.00/700 [00:00<?, ?B/s]
Downloading tokenizer.model:   0%|          | 0.00/500k [00:00<?, ?B/s]
Downloading (…)cial_tokens_map.json:   0%|          | 0.00/411 [00:00<?, ?B/s]



Das Tokenizing wird verwendet, um die Textdaten für das Sprachmodell verständlich aufzubereiten. Man kann es sich wie eine Art Übersetzung vorstellen. Die einzelnen Textbausteine werden in einzelne Token bzw. Nummern übertragen. Hier mal ein Beispiel:

from termcolor import colored
from pprint import pprint
def visualize_tokenizer(tokenizer, example_text):
    print("Tokenized Text:")
    tokens = tokenizer.encode(example_text)
    token_colors = {}
    colored_tokens = []
    str_tokens = []
    for i, token in enumerate(tokens):
        token_colors[token] = 'on_blue' if i % 2 == 0 else 'on_dark_grey'
        colored_token = colored(tokenizer.decode(token), on_color=token_colors[token])
        str_tokens.append(colored(token, on_color=token_colors[token]))
        colored_tokens.append(colored_token)

    print(' '.join(str_tokens))
    print(''.join(colored_tokens))
    print(len(tokens))

visualize_tokenizer(tokenizer, dataset[0]["text"])
Output
Tokenized Text:
    11843 5814 14311 3836 14 186 7740 1783 12 272 875 803 437 1731 12 186 901 3084 273 19890 1052 267 12 7744 2937 12 186 51 1392 12 722 875 1697 295 2621 968 411 186 5348 671 673 46025 21065 31 186 1165 27674 708 207 68 69 82 3868 310 296 8684 12 186 14073 1311 484 6542 273 2714 12177 14 186 601 27759 482 12 272 44519 361 22023 12 186 2951 25494 4832 403 340 2228 14 186 1960 7362 739 345 3563 2305 15546 411 186 16730 703 486 273 2655 4090 10194 411 14 186 1165 1985 12 452 540 413 6199 419 4124 1443 11989 27 186 7264 437 21420 1206 455 1634 3203 14 186 24917 482 484 380 333 2386 395 20911 12 186 33508 484 587 26747 801 6117 14 186 2819 1066 396 2357 83 12 2031 1142 6891 273 885 186 2951 345 4057 421 42263 1561 31 186 10292 20800 3006 455 4090 272 3868 1514 12 186 2040 403 207 68 69 82 3800 501 671 673 49124 32387 12 186 2951 345 29253 36039 474 490 186 35799 568 272 13528 35289 704 2452 3713 9178 27 186 2219 15131 77 1527 12 739 491 276 632 12 186 1630 12453 689 403 620 380 272 12554 294 320 186 2951 12 452 295 49084 433 9697 494 7352 380 21862 405 3954 12 186 3124 340 284 2652 84 403 1939 272 325 1035 265 18186 14 186 9045 6518 5090 361 437 996 660 274 3045 186 917 19053 580 27 1663 2653 12 379 27382 422 1272 1 186
    
    Visualisierte Aufteilung:
DIREKTOR.
Ihr beiden, die ihr mir so oft,
In Not und Trübsal, beigestanden,
Sagt, was ihr wohl in deutschen Landen
Von unsrer Unternehmung hofft?
Ich wünschte sehr der Menge zu behagen,
Besonders weil sie lebt und leben läßt.
Die Pfosten sind, die Bretter aufgeschlagen,
Und jedermann erwartet sich ein Fest.
Sie sitzen schon mit hohen Augenbraunen
Gelassen da und möchten gern erstaunen.
Ich weiß, wie man den Geist des Volks versöhnt;
Doch so verlegen bin ich nie gewesen.
Zwar sind sie an das Beste nicht gewöhnt,
Allein sie haben schrecklich viel gelesen.
Wie machen wirs, daß alles frisch und neu
Und mit Bedeutung auch gefällig sei?
Denn freilich mag ich gern die Menge sehen,
Wenn sich der Strom nach unsrer Bude drängt,
Und mit gewaltig wiederholten Wehen
Sich durch die enge Gnadenpforte zwängt;
Bei hellem Tage, schon vor vieren,
Mit Stößen sich bis an die Kasse ficht
Und, wie in Hungersnot um Brot an Bäckertüren,
Um ein Billet sich fast die Hälse bricht.
Dies Wunder wirkt auf so verschiedne Leute
Der Dichter nur; mein Freund, o tu es heute!

Wir sehen also, dass nicht jedes Wort automatisch einem Token entspricht. Außerdem ist jeder Tokenizer an eine Sprache oder einem Datensatz angepasst. Somit braucht ein englischer Tokenizer für den gleichen Text mehr Tokens. Oder auch anders herum:
Übersetzen wir den gleichen Text auf englisch und tokenizen diesen dann, werden insgesamt 355 anstatt 280 Tokens benötigt. Im Vergleich hierzu, der deutsch Text hat 179 Wörter und 1056 Zeichen, während der englisch 194 Wörter und 1011 Zeichen besitzt.

Warum spielt das eine Rolle für uns?
Das Sprachmodell kann lediglich eine bestimmte Anzahl an Tokens die Sekunde ausgeben. Werden mehr Tokens für die gleiche Länge an Text benötigt, so ist die Ausgabe des Netzes langsamer. Ebenso erfolgt die Abrechnung kommerzieller API-Schnittstellen in der Regel nach der Tokenanzahl. So zahlt man bei OpenAI für einen gleich langen Text in Deutsch mehr als für einen englischen Text.

Interaktiver LLaMA Tokenizer

Englischer LLaMA Tokenizer

Zeichen
Token

Deutscher BLOOM Tokenizer

Zeichen
Token

Tokenizen des Datensatzes

from datasets import Dataset, Features, Value
dataset = Dataset.from_dict({"text": final_text}, features=Features({"text": Value("string")}))

# Aufsplitten in einen Trainings- und Validierungsdatensatz: 
# dataset = dataset.train_test_split(test_size=TRAIN_TEST_SPLIT)

# Tokenizen des gesamten Datensatzes: 
def tokenize(batch):
    return tokenizer(list(batch["text"]))

dataset = dataset.map(tokenize, batched=True, remove_columns=["text"])

# Aneinanderreihen der Texte: 
def group_texts(examples):
    concatenated_examples = {k: sum(examples[k], []) for k in examples.keys()}
    total_length = len(concatenated_examples[list(examples.keys())[0]])

    if total_length % BLOCK_SIZE != 0:
        padding_length = BLOCK_SIZE - (total_length % BLOCK_SIZE)
        for k in concatenated_examples.keys():
            concatenated_examples[k] += [tokenizer.pad_token_id] * padding_length
        total_length += padding_length
    
    result = {
        k: [t[i : i + BLOCK_SIZE] for i in range(0, total_length, BLOCK_SIZE)]
        for k, t in concatenated_examples.items()
    }
    result["labels"] = result["input_ids"].copy()
    return result

dataset = dataset.map(group_texts, batched=True)

flat_list = [item for sublist in dataset['input_ids'] for item in sublist]
print("Anzahl der gesamten Token im Datensatz:", len(flat_list))
Output
Map:   0%|          | 0/799 [00:00<?, ? examples/s]
Anzahl der gesamten Token im Datensatz: 45056

Vorbereiten des Trainings

Wie bereits gesagt, müssen einige Einstellungen für das Training vorgenommen werden, da für das Training eine RTX 3090 verwendet wird und der Speicher dadurch auf 24 GB begrenzt ist. Zuerst erzeugen wir eine DeepSpeed-Configuration, damit das Ganzen auf der Grafikkarte lauffähig ist. Anschließend erzeugen wir unsere Trainingskonfiguration, den dazugehörigen Trainer und laden das Sprachmodell.

from transformers import TrainingArguments, Trainer, default_data_collator

    print("Vorbereiten der Trainingseinstellungen")
    training_args = TrainingArguments(
        "./output",
        per_device_train_batch_size=BATCH_SIZE,
        logging_steps=1,
        save_total_limit=2,
        save_strategy="epoch",
        evaluation_strategy="no",
        per_device_eval_batch_size=BATCH_SIZE,
        learning_rate=LR,
        weight_decay=WEIGHT_DECAY,
        warmup_steps=WARMUP_STEPS,
        optim="adam", 
        num_train_epochs=EPOCHS,
        push_to_hub=False,
        bf16=True,
        gradient_checkpointing=True,
        deepspeed=deepspeed, # Hier json Konfiguration verlinken oder in Python die Konfiguration definieren
        gradient_accumulation_steps=GRADIENT_ACCUMULATION_STEPS
    )
    
    
    print("Laden des Modells")
    model = AutoModelForCausalLM.from_pretrained(MODEL_NAME, use_cache=False)
    model.resize_token_embeddings(len(tokenizer))
    
    trainer = Trainer(
        model=model,
        args=training_args,
        train_dataset=dataset['train'],
        eval_dataset=dataset['test'],
        tokenizer=tokenizer,
        data_collator=default_data_collator,
    )
    
    # Modell trainieren
    trainer.train()
Output
Vorbereiten der Trainingseinstellungen
3%|▎         | 1/30 [00:36<17:23, 35.97s/it]
{'loss': 3.6562, 'learning_rate': 2e-05, 'epoch': 0.1}
    33%|███▎      | 10/30 [06:49<12:00, 36.00s/it]
{'loss': 2.9609, 'learning_rate': 2e-05, 'epoch': 0.95}  
    67%|██████▋   | 20/30 [17:13<06:44, 40.40s/it]
{'loss': 1.9102, 'learning_rate': 2e-05, 'epoch': 1.9}
    97%|█████████▋| 29/30 [31:31<00:56, 56.02s/it]
{'loss': 1.1523, 'learning_rate': 2e-05, 'epoch': 2.76}
100%|██████████| 30/30 [32:14<00:00, 52.11s/it]
{'loss': 1.0098, 'learning_rate': 2e-05, 'epoch': 2.86}
100%|██████████| 30/30 [37:10<00:00, 74.33s/it]
{'train_runtime': 2230.0221, 'train_samples_per_second': 0.112, 'train_steps_per_second': 0.013, 'train_loss': 2.187890625, 'epoch': 2.86}
TrainOutput(global_step=30, training_loss=2.187890625, metrics={'train_runtime': 2230.0221, 'train_samples_per_second': 0.112, 'train_steps_per_second': 0.013, 'train_loss': 2.187890625, 'epoch': 2.86})

Anwenden des Sprachmodells

Da das Modell als Textvervollständiger trainiert wurden, muss eine Anfangssequenz dem Sprachmodell zugeführt werden. Hierfür verwenden wir einen Teil des Originaltextes:

MARGARETE.
Müßte vor dem Herren schamrot werden.
MEPHISTOPHELES.


Der Text geht folgend noch weiter:
Vor keinem Könige der Erden.
Marthe:
Da hinterm Haus in meinem Garten
Wollen wir der Herren heut abend warten.

Nun wenden wir das Modell an. Dazu tokenizen wir den vorgegebenen Text und führen diesen dann dem Sprachmodell zu. Anschließend müssen wir den zurückgegebenen Text wieder decoden, um Ihn lesbar zu machen.

enc_txt = tokenizer.encode("MARGARETE.\nMüßte vor dem Herren schamrot werden.\nMEPHISTOPHELES.", return_tensors="pt").to("cuda")
ret_txt = model.generate(enc_txt,
                        max_length=512,
                        repetition_penalty=1.05)
print(tokenizer.decode(ret_txt[0]))
Output
MARGARETE.
Müßte vor dem Herren schamrot werden.
MEPHISTOPHELES.
Das kommt nur auf die Weise an,
Wie man sich in Gegenwart des Herrn verhält;
Ich weiß mich sehr wohl zu betragen—
Nur muß ich gleich wieder fort!
<|endoftext|>FAUST.
Du darfst nicht so von dir gehen!
Was fragst du nach deiner Nachbarin?
Sie ist doch eine Fremde hier.
(Er geht weiter.)
CHOR DER ENGEL.
Christ ist erstanden! Freudig sei der Welt!
Die Sonne steige nun höher denn je und scheine heller als sonst über den Auen, bis sie im Meer versinke.
[...]

Wir sehen also, dass wir dem Sprachmodell Informationen von Faust beibringen konnten. Wofür kann das verwendet werden? Allgemein können damit weitere Informationen in das Sprachmodell eintrainiert werden. Einige Beispiele hierfür sind:

- unterschiedliche betriebsinterne Dokumente
- verschiedene Code-Bausteine
- Betriebliche Dokumentationen
- Anleitungen zu verschiedenen Tools und Programmen

Zusammenfassung

- Die Kosten des Trainings der LLaMA-Modell varriert je nach Größe des Sprachmodells und beträgt für das größte Modell ca. 1.5 Mio. $
- Die Stromkosten für das Training würden bei 30 ct/kWh 135t€ betragen.
- Durch die Verwendung kleinerer speziell angepasster Netze können Rechenzeiten, Kosten und CO2-Emissionen reduziert werden.
- Es wurde aufgezeigt wie ein Tokenizer einen Text für ein Sprachmodell verständlich macht und die Nachteile eines mehrsprachigen Tokenizers aufgezeigt.
- Es wird gezeigt, wie man ein spezielles deutsches Sprachmodell (BLOOM-CLP German 6.4B) für die Vervollständigung von Texten im Stil von Johann Wolfgang von Goethes Faust eintrainieren kann.
- Es können unterschiedliche Dokumente, wie betriebsinterne Abläufe, Dokumentationen u.ä. verwendet werden.

In unserem nächsten Blogbeitrag zeigen wir, wie man ein Sprachmodell für die Übersetzung von großen Datensätzen verwenden kann.