2023 war das Jahr, in dem ver­schie­dene Large Lan­guage Models (LLMs) im Bereich der gene­ra­ti­ven KI auf­ka­men. LLMs haben eine unglaub­li­che Leis­tung und ein unglaub­li­ches Poten­zial, aber ihre Pro­duk­tion ist eine stän­dige Her­aus­for­de­rung für die Nut­zer. Ein beson­ders häu­fig auf­tre­ten­des Pro­blem ist die Frage, wel­ches LLM man ver­wen­den sollte. Und noch spe­zi­fi­scher: Wie kann man ein LLM auf seine Genau­ig­keit hin bewer­ten? Dies ist beson­ders schwie­rig, wenn eine große Anzahl von Model­len zur Aus­wahl steht, ver­schie­dene Daten­sätze für die Feinabstimmung/RAG und eine Viel­zahl von Prompt-Engi­nee­ring-/Tu­ning-Tech­ni­ken zu berück­sich­ti­gen sind.

Um die­ses Pro­blem zu lösen, müs­sen wir DevOps Best Prac­ti­ces für LLMs ein­füh­ren. Ein Work­flow oder eine Pipe­line, die bei der Bewer­tung ver­schie­de­ner Modelle, Daten­sätze und Auf­for­de­run­gen hel­fen kann. Die­ser Bereich wird all­mäh­lich als LLMOPs/FMOPs bekannt. Einige der Para­me­ter, die in LLMOPs berück­sich­tigt wer­den kön­nen, sind unten in einem (extrem) ver­ein­fach­ten Ablauf dargestellt:

In die­sem Arti­kel ver­su­chen wir, die­ses Pro­blem anzu­ge­hen, indem wir eine Pipe­line auf­bauen, die ein Llama 7B-Modell fein­ab­stimmt, ein­setzt und eva­lu­iert. Sie kön­nen die­ses Bei­spiel auch ska­lie­ren, indem Sie es als Vor­lage für den Ver­gleich meh­re­rer LLMs, Daten­sätze und Prompts ver­wen­den. Für die­ses Bei­spiel wer­den wir die fol­gen­den Tools ver­wen­den, um diese Pipe­line zu erstellen:

  • Sage­Ma­ker Jump­Start: Sage­Ma­ker Jump­Start bie­tet stan­dard­mä­ßig ver­schie­dene FM/LLMs für die Fein­ab­stim­mung und den Ein­satz. Diese bei­den Pro­zesse kön­nen recht kom­pli­ziert sein, daher abs­tra­hiert Jump­Start die Ein­zel­hei­ten und ermög­licht es Ihnen, Ihren Daten­satz und Ihre Modell-Meta­da­ten anzu­ge­ben, um die Fein­ab­stim­mung und den Ein­satz durch­zu­füh­ren. In die­sem Fall wäh­len wir Llama 7B aus und füh­ren eine Fein­ab­stim­mung der Anwei­sun­gen durch, die von Anfang an unter­stützt wird. Eine aus­führ­li­chere Ein­füh­rung in die Jump­Start-Fein­ab­stim­mung fin­den Sie in die­sem Blog und in die­sem Llama-Code­bei­spiel, das wir als Refe­renz ver­wen­den werden.
  • Sage­Ma­ker Clarify/FMEval: Sage­Ma­ker Cla­rify bie­tet über die Sage­Ma­ker Stu­dio-Benut­zer­ober­flä­che und die Open-Source-Python-Biblio­thek FME­Val ein Tool zur Bewer­tung von Foun­da­tion-Model­len. Die Funk­tion ist mit einer Viel­zahl ver­schie­de­ner Algo­rith­men aus­ge­stat­tet, die unter­schied­li­che NLP-Domä­nen wie Tex­terzeu­gung und ‑zusam­men­fas­sung abde­cken. In die­sem Bei­spiel nut­zen wir die Biblio­thek, um das Llama-Modell für einen Zusam­men­fas­sungs-Anwen­dungs­fall zu evaluieren.
  • Sage­Ma­ker Pipe­lines Schritt-Deko­ra­tor: Sage­Ma­ker Pipe­lines ist eine MLOPs-Funk­tion in Sage­Ma­ker, die Ihnen hilft, ML-Work­flows zu ope­ra­tio­na­li­sie­ren. Mit Pipe­lines kön­nen Sie ver­schie­dene Schritte und Para­me­ter defi­nie­ren, um Ihren ML-Work­flow auf­zu­bauen. Inner­halb von Pipe­lines gibt es eine Funk­tion, die als Schritt­de­ko­ra­tor bekannt ist und mit der Sie Python-Code in Funk­tio­nen umwan­deln kön­nen, die Sie als Pipe­line anein­an­der­rei­hen kön­nen. In die­sem Bei­spiel wer­den wir diese Funk­tion nut­zen, um Funk­tio­nen für das Trai­ning und die Aus­wer­tung mit den oben defi­nier­ten Tools zu erstellen.

Jetzt, wo wir es ver­stan­den haben, kön­nen wir loslegen!

HINWEIS: Die­ser Arti­kel setzt ein grund­le­gen­des Ver­ständ­nis von Python, LLMs und Ama­zon Sage­Ma­ker voraus.

Inhalts­über­sicht

  • Ein­rich­tung und Datensatzvorbereitung
  • Auf­bau von Pipelineschritten
  • Pipe­line-Aus­füh­rung
  • Zusätz­li­che Res­sour­cen & Schlussfolgerung

1. Ein­rich­tung & Datensatzvorbereitung

Für die Ent­wick­lung wer­den wir in der neuen Sage­Ma­ker Stu­dio-Umge­bung arbei­ten (lokale Ker­nel-Unter­stüt­zung akti­viert). Wir wer­den eine ml.c5.18xlarge-Instanz mit einem Python3-Ker­nel verwenden.

Für unse­ren Anwen­dungs­fall wer­den wir eine Fein­ab­stim­mung vor­neh­men und einen Zusam­men­fas­sungs­an­wen­dungs­fall mit Llama eva­lu­ie­ren. Für unse­ren Daten­satz wer­den wir den öffent­li­chen Dolly-Daten­satz ver­wen­den (Lizenz: cc-by-sa‑3.0). Wir kön­nen die­sen Daten­satz mit­hilfe der inte­grier­ten Hug­ging­Face-Daten­satz­bi­blio­thek abru­fen und nach den Daten­punk­ten für die Zusam­men­fas­sung fil­tern. Außer­dem erstel­len wir einen Trai­nings- und einen Test­da­ten­satz für die Fein­ab­stim­mung und Auswertung.

import datasets

# dolly dataset
dolly_dataset = load_dataset("databricks/databricks-dolly-15k", split="train")

# summarization use-case
summarization_dataset = dolly_dataset.filter(lambda example: example["category"] == "summarization")
summarization_dataset = summarization_dataset.remove_columns("category")

# train test split
train_and_test_dataset = summarization_dataset.train_test_split(test_size=0.1)

# local train dataset
train_and_test_dataset["train"].to_json("train.jsonl")

# test dataset
train_and_test_dataset["test"].to_json("test.jsonl")

Wir geben auch die Modell-Meta­da­ten an Jump­Start wei­ter, um das pas­sende Llama 7B-Modell zu erhalten.

import sagemaker

model_id, model_version = "meta-textgeneration-llama-2-7b", "2.*"

Für die Schu­lung wer­den wir eine Fein­ab­stim­mung der Anwei­sun­gen vor­neh­men, d. h. wir erstel­len eine Vor­lage für die Auf­for­de­rung und die Ant­wort, oder in die­sem Fall den Text und die Zusam­men­fas­sung. Fol­gen­des Bei­spiel wird als Refe­renz für den Schu­lungs­teil verwendet.

import json

template = {
    "prompt": "Below is an instruction that describes a task, paired with an input that provides further context. "
    "Write a response that appropriately completes the request.\n\n"
    "### Instruction:\n{instruction}\n\n### Input:\n{context}\n\n",
    "completion": " {response}",
}
with open("template.json", "w") as f:
    json.dump(template, f)

Anschlie­ßend laden wir diese Dateien in einen gemein­sa­men S3-Pfad hoch, um sie zu trai­nie­ren und spä­ter auch zu inferenzieren/auszuwerten.

from sagemaker.s3 import S3Uploader
import sagemaker
import random

output_bucket = sagemaker.Session().default_bucket()
local_data_file = "train.jsonl"
test_data_file = "test.jsonl"
train_data_location = f"s3://{output_bucket}/dolly_dataset"
test_data_location = f"s3://{output_bucket}/test_dataset"
S3Uploader.upload(local_data_file, train_data_location)
S3Uploader.upload("template.json", train_data_location)
S3Uploader.upload(test_data_file, test_data_location)
print(f"Training data: {train_data_location}")
print(f"Test data: {test_data_location}")
print(f"Output bucket: {output_bucket}")

Auf­bau von Pipelinestufen

Pipe­line- Auf­bau
Um unsere Pipe­line ein­zu­rich­ten, benö­ti­gen wir einige ein­zelne Dateien, die unsere Aus­füh­rungs­um­ge­bung defi­nie­ren. Eine davon ist die Datei config.yaml, in der die Hard­ware für die Pipe­line-Aus­füh­rung sowie alle ande­ren von Ihnen defi­nier­ten Kon­fi­gu­ra­tio­nen fest­ge­legt wer­den. Die con­fig-Datei ver­weist auch auf Ihre requirements.txt, die in der Pipe­line-Umge­bung instal­liert wird.

SchemaVersion: '1.0'
SageMaker:
  PythonSDK:
    Modules:
      RemoteFunction:
        InstanceType: ml.m5.xlarge
        Dependencies: ./requirements.txt
        IncludeLocalWorkDir: true
        CustomFileFilter:
          IgnoreNamePatterns: # files or directories to ignore
          - "*.ipynb" # all notebook files

jsonlines
sagemaker
fmeval

Wir kön­nen dann auf diese Kon­fi­gu­ra­ti­ons­da­tei als Umge­bungs­va­ria­ble verweisen:

import os

# Set path to config file
os.environ["SAGEMAKER_USER_CONFIG_OVERRIDE"] = os.getcwd()

Optio­nal kön­nen Sie bei Pipe­lines auch Para­me­ter defi­nie­ren, die in Ihre Pipe­line inji­ziert wer­den. In die­sem Fall defi­nie­ren wir den Hard­ware-Instanz­typ für den Job, der die ein­zel­nen Schritte ausführt:

import sagemaker
from sagemaker.workflow.function_step import step
from sagemaker.workflow.parameters import ParameterString

sagemaker_session = sagemaker.session.Session()
role = sagemaker.get_execution_role()
bucket = sagemaker_session.default_bucket()
region = sagemaker_session.boto_region_name

instance_type = ParameterString(name="TrainInstanceType", 
default_value="ml.c5.18xlarge") 

Schu­lung und Einsatz


Für den Schu­lungs­schritt wer­den wir mit Sage­Ma­ker Jump­Start arbei­ten, um unser Llama 7B-Modell zu opti­mie­ren. Wir über­ge­ben der Funk­tion ein paar ver­schie­dene Parameter:

  • Trai­nings­da­ten­pfad: Dies ist der S3-Spei­cher­ort, der auf die Dateien train.jsonl und template.json verweist.

  • Modell-Meta­da­ten: Jump­Start erkennt anhand der über­ge­be­nen Modell-ID und Ver­sion, wel­ches Modell her­un­ter­ge­la­den wer­den soll. In die­sem Fall geben wir das Modell Llama 7B an.


Sobald wir diese Para­me­ter defi­niert haben, rich­ten wir den Jump­Start Esti­ma­tor mit die­sen Para­me­tern ein. Optio­nal kön­nen Sie auch modell­spe­zi­fi­sche Para­me­ter für die Fein­ab­stim­mung defi­nie­ren, abhän­gig von den Reg­lern, die Sie auf ihre Leis­tung tes­ten möch­ten. Beach­ten Sie den Schritt­de­ko­ra­tor mit der Funk­tion, der sym­bo­li­siert, dass es sich um einen Sage­Ma­ker-Pipe­line-Schritt und nicht um eine ein­fa­che Python-Funk­tion handelt.

# step one
@step(
    name = "train-deploy",
    instance_type = instance_type,
    keep_alive_period_in_seconds=300
)
def train_deploy(train_data_path: str, 
model_id: str = "meta-textgeneration-llama-2-7b", 
model_version: str = "2.*") -> str:
    import sagemaker
    from sagemaker.jumpstart.estimator import JumpStartEstimator

    # configure JumpStart Estimator
    estimator = JumpStartEstimator(
        model_id=model_id,
        model_version=model_version,
        environment={"accept_eula": "true"},
        disable_output_compression=True, 
    )
    estimator.set_hyperparameters(instruction_tuned="True", epoch="1", max_input_length="1024")
    estimator.fit({"training": train_data_path})

    ## deploy fine-tuned model
    finetuned_predictor = estimator.deploy()
    endpoint_name = finetuned_predictor.endpoint_name

Nach dem Trai­ning stel­len wir das fein abge­stimmte Llama-Modell auf einem Sage­Ma­ker Echt­zeit-End­punkt bereit, den wir für die Infe­renz auf­ru­fen kön­nen. Die­ser Instanz­typ ist stan­dard­mä­ßig sowohl für das Trai­ning als auch für die Infe­renz aus­ge­wählt, je nach dem von Ihnen gewähl­ten LLM, aber Sie kön­nen dies anpas­sen, wenn Sie die Hard­ware selbst aus­wäh­len möchten.

Der Name des End­punkts wird als Ein­gabe für den nächs­ten Schritt zurück­ge­ge­ben, so dass wir die Infe­renz und die Aus­wer­tung mit die­sem End­punkt durch­füh­ren kön­nen. Wenn Sie die Pipe­line schließ­lich aus­füh­ren, sehen Sie für die­sen Schritt einen erfolg­rei­chen Jump­Start-Schu­lungs­auf­trag und einen in der Stu­dio-Benut­zer­ober­flä­che erstell­ten Endpunkt.

Jetzt, wo wir unse­ren End­punkt haben, kön­nen wir die Stich­pro­ben­in­fe­renz durch­füh­ren und die Ergeb­nisse auswerten.

Infe­rence & Eva­lua­tion Step

In unse­rem zwei­ten Schritt defi­nie­ren wir wie­der bestimmte Para­me­ter für unsere Funktion:

  • End­punkt Name: Wir füh­ren vor der Aus­wer­tung eine Infe­renz gegen die­sen End­punkt durch.
  • S3_Test_Pfad: Zuvor hat­ten wir auch einen Test­da­ten­satz getrennt von unse­rem Trai­nings­da­ten­satz in S3 über­tra­gen. Wir wer­den den Daten­satz von die­sem Pfad abru­fen und vor der Aus­wer­tung eine Infe­renz durchführen.

Zunächst laden wir die S3-Datei „test.jsonl“ her­un­ter, die das Boto3 Python SDK verwendet:

def evaluate(endpoint_name: str, output_bucket: str = output_bucket, test_data_file: str = "test.jsonl",
            key_path: str = "test_dataset/test.jsonl") -> str:
    
    # download S3 file
    s3 = boto3.client("s3")
    s3.download_file(output_bucket, key_path, test_data_file)

Aus die­ser Datei wird dann eine neue JSON­Lines-Datei erstellt, für die wir unsere Bewer­tungs­al­go­rith­men aus­füh­ren kön­nen. Aus Zeit­grün­den beschrän­ken wir diese Stich­probe auf nur 20 Daten­punkte, aber Sie kön­nen den Test­da­ten­satz nach Bedarf erweitern.

with jsonlines.open(input_file) as input_fh, jsonlines.open(output_file, "w") as output_fh:
  for i, datapoint in enumerate(input_fh, start=1):
      instruction = datapoint["instruction"]
      context = datapoint["context"]
      summary = datapoint["response"]
      payload = prepare_payload(datapoint)
      response = runtime.invoke_endpoint(EndpointName=endpoint_name, Body=json.dumps(payload), 
                             ContentType=content_type, CustomAttributes='accept_eula=true')
      result = json.loads(response['Body'].read().decode())[0]['generation']
      line = {"instruction": instruction, "context": context, "summary": summary, "model_output": result}
      output_fh.write(line)
  
      # evaluate just 20 datapoints for example
      if i == 20:
          break

Nach­dem die­ser Teil der Funk­tion aus­ge­führt wurde, sollte er eine Datei „results.jsonl“ erzeu­gen, die einige ver­schie­dene Para­me­ter enthält:

  • Dokument/Eingabe: Dies ist der Ori­gi­nal­text, der zusam­men­ge­fasst wer­den muss.
  • Grund­wahr­hei­t/Ist-Aus­gabe: Dies ist die Zusam­men­fas­sung, die im Test­da­ten­satz ent­hal­ten war; dies ist die Grund­wahr­heit, gegen die wir evaluieren.
  • Modell Out­put: Dies ist die Infe­renz, die wir mit unse­rem fein­ab­ge­stimm­ten Llama 7B-Modell durch­ge­führt haben. Wir wer­den unse­ren Eva­lu­ie­rungs­al­go­rith­mus auf die Ground Truth-Werte und die Ergeb­nisse der Modell­in­fe­renz anwenden.

Da wir nun unse­ren Daten­satz für die Aus­wer­tung haben, kön­nen wir die FME­val-Biblio­thek importieren:

import fmeval
from fmeval.data_loaders.data_config import DataConfig
from fmeval.constants import MIME_TYPE_JSONLINES
from fmeval.eval_algorithms.summarization_accuracy import SummarizationAccuracy

Ach­ten Sie dar­auf, dass wir den Sum­ma­riza­tio­nAc­cu­racy-Algo­rith­mus abru­fen, der Metri­ken wie Meteor, Rouge und Bert zurück­gibt. Um die voll­stän­dige Imple­men­tie­rung die­ser Algo­rith­men zu sehen, kön­nen Sie den Open-Source-Code unter die­sem Link auf­ru­fen. Im All­ge­mei­nen hat jede die­ser Metri­ken ihre eige­nen Vor- und Nach­teile, und Sie kön­nen aus­wäh­len, wel­che Metrik Sie für die Aus­wer­tung ver­wen­den möchten.

  • Rouge: Rouge‑N Scores, das im Wesent­li­chen nach N‑Gramm-Wort­über­schnei­dun­gen zwi­schen der Grund­wahr­heit und der Zusam­men­fas­sung der Modell­in­fe­renz sucht.
  • Meteor: Baut auf Rouge auf, um Stem­ming und Syn­onyme ein­zu­be­zie­hen. Dadurch wer­den mehr Ähn­lich­kei­ten in Tex­ten erfasst, falls die Wör­ter nicht genau übereinstimmen.
  • BertS­core: Ver­gleicht Wör­ter mit Hilfe von Kosi­nus­ähn­lich­keit unter Ver­wen­dung von vor­trai­nier­ten Ein­bet­tun­gen von BERT. Dies wird bei der Instal­la­tion des Pakets heruntergeladen.

Für unsere Imple­men­tie­rung mit dem FME­val-Paket kon­fi­gu­rie­ren wir zunächst unse­ren Daten­satz in einem FME­val-spe­zi­fi­schen Dat­a­Con­fig-Objekt und legen die Spal­ten für die Grund­wahr­heit und die Modell­in­fe­renz fest.

config = DataConfig(
dataset_name="dolly_summary_model_outputs",
dataset_uri="results.jsonl",
dataset_mime_type=MIME_TYPE_JSONLINES,
model_input_location="instruction",
target_output_location="summary",
model_output_location="model_output"
)




Dann instanziieren wir den SummarizationAccuracy-Algorithmus und führen eine Auswertung mit unserem DataConfig-Objekt aus.
eval_algo = SummarizationAccuracy()
eval_output = eval_algo.evaluate(dataset_config=config, save=True)
res = json.dumps(eval_output, default=vars, indent=4)
serialized_data = json.loads(res)
# print metrics to CW logs, realistically push to somewhere to visualize
for item in serialized_data:
    for key, value in item.items():
        print(f"Key: {key}, Value: {value}")


In die­sem Fall schrei­ben wir die Metri­ken direkt in die Cloud­Watch-Pro­to­kolle. In einem rea­lis­ti­schen Anwen­dungs­fall kön­nen Sie diese in S3 oder ein Visua­li­sie­rungs­tool wie Quick­Sight aus­la­gern, um eine schö­nere Ansicht Ihrer Aus­wer­tung zu erhal­ten. Wenn Sie die Cloud­Watch-Pro­to­kolle für den zwei­ten Schritt nach der Pipe­line-Aus­füh­rung über­prü­fen, wer­den Sie fest­stel­len, dass die Metri­ken aus­ge­ge­ben wurden.

3.Pipeline-Ausführung

Sobald die bei­den Pipe­line-Schritte defi­niert sind, kön­nen Sie sie ein­fach mit­ein­an­der ver­ket­ten, wie Sie es mit ein­fa­chen Python-Funk­tio­nen tun würden.

# stitch together pipelinefrom sagemaker.workflow.pipeline import Pipelineendpoint_name = train_deploy(train_data_location)eval_metrics = evaluate(endpoint_name)

Wir kön­nen dann ein Pipe­line-Objekt defi­nie­ren und eine Aus­füh­rung starten:

pipeline = Pipeline(
    name="llm-train-eval-pipeline",
    parameters=[
        instance_type
    ],
    steps=[
        eval_metrics,
    ],
)

# execute Pipeline
pipeline.upsert(role_arn=role)
execution = pipeline.start()
execution.describe()
execution.wait()

You can view and moni­tor the Pipe­line exe­cu­tion in the Stu­dio UI, this Pipe­line will take about 45 minu­tes to suc­cessfully complete.

4. Zusätz­li­che Res­sour­cen & Schlussfolgerung

Ich hoffe, die­ser Arti­kel war eine nütz­li­che Ein­füh­rung in LLMOPs und den Auf­bau einer Pipe­line unter Ver­wen­dung ver­schie­de­ner Sage­Ma­ker-Funk­tio­nen. Mit der Aus­wei­tung der LLM-Anwen­dungs­fälle steigt auch der Bedarf an ange­mes­se­nen Expe­ri­men­ten, um die ideale Kon­fi­gu­ra­tion für Ihren LLM zu ermit­teln. Die­ses Bei­spiel kann erwei­tert wer­den, um meh­rere LLMs, Daten­sätze, Ein­ga­be­auf­for­de­rungs­vor­la­gen und mehr ein­zu­be­zie­hen. Blei­ben Sie dran für wei­tere Inhalte im Bereich GenAI/LLM.

Wie immer dan­ken wir Ihnen für das Inter­esse und freuen uns über Ihr Feedback.