Wir zeigen, wie Sie Ihre eigenen Daten in einem Sprachmodell eintrainieren können.
Bei der Größe der Netze und der Anzahl der Token muss die englische Zählweise berücksichtigt werden. 1T entspricht einer englischen Trillionen, also einer deutschen Billionen. Das gleiche gilt für die englische Billionen (B), die einer deutschen Milliarde entspricht.
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.
Was sind Parameter?
Parameter für LLMs sind die Werte eines Sprachmodells die während des Lernvogangs angepasst werden können. Man kann sich das wie Neuronen im Gehirn vorstellen. Je mehr Parameter vorhanden sind, desto tiefers Verständniss und desto mehr Informationen kann das Sprachmodell i.d.R. erlernen. Die Daten spielen hierbei jedoch auch eine wichtige Rolle.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.A100 Kosten 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.A100 Kosten Google Cloud 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.
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.Link zum Modell auf Huggingface Die deutschen Sprachkentnisse sind hierbei größer und der Tokenizer ist für deutsch optimiert.
Der vom DFKI verwendete RohdatensatzLink zum deutschen Rohdatensatz enthält teilweise sinnfreie (bspw. Zahlenreihen) und auch nicht jugendfreie Inhalte. Angeblich wurde der Datensatz vom DFKI nachbearbeitet um solche Inhalte zu entfernen. Das fertig trainierte NetzLink zum deutschen Sprachmodell gibt sie jedoch immer noch aus. Exemplarisch dafür stehen die Zahlenreihe 12286;12294 oder der Satzanfang "Die junge Oma", wenn sie im Greedy-ModusIm Greedy-Mode verwendert das Sprachmodell nur die4 weahrscheinlichsten Tokens, d.h. die Inhalte waren im Trainingdatensatz immer noch enthalten. eingegeben werden. Bei einer kommerziellen Anwendung sollte unerwünschte Inhalte im Idealfall bereits bei der Datensammlung (scrapen) über eine Ausschlussliste, 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.
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
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"])
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.
Zeichen
Token
Zeichen
Token
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))
Map: 0%| | 0/799 [00:00<?, ? examples/s] Anzahl der gesamten Token im Datensatz: 45056
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()
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})
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]))
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. [...]
In unserem nächsten Blogbeitrag zeigen wir, wie man ein Sprachmodell für die Übersetzung von großen Datensätzen verwenden kann.