大语言模型(LLM)的性能监控、定制与应用实践

1. LLM 性能监控与评估

监控和衡量大语言模型(LLM)的性能颇具挑战性,因为生成的文本具有不确定性,且模型基于庞大的训练数据集。不过,仍有一些近似方法可用于衡量漂移情况,例如:
- 比较参考数据与当前请求之间的嵌入相似度。
- 测量词汇频率和困惑度等。

检测模型是否产生幻觉是关键挑战。以下是几种检测方法:
- 多结果比较法 :针对同一问题比较多个结果,若语义不同,则模型很可能产生了幻觉。
- RAG 系统相似度评估法 :评估参考数据与生成文本答案之间的相似度,可参考 BERTScore 和 Vectara。
- ROUGE 评估法 :文本摘要应用可使用 ROUGE(Recall - Oriented Understudy for Gisting Evaluation)方法评估结果。
- LLM 评判法 :使用 LLM 作为评判者,根据参考内容(在 RAG 中)或另一个 LLM 评估答案的正确性。

添加用户反馈(人工介入)也是确保高质量和可靠性的重要因素。具体做法是对生产数据中的结果进行抽样,由人工验证是否符合预期行为。若不符合,则进行纠正,并重新训练或调整模型。

2. MLOps 管道助力 LLM 定制与使用

为特定数据和应用定制 LLM 有两种方法:提示工程和微调。在许多应用中,会同时使用这两种方法以提升性能和可靠性。数据准备和验证在这两种方法中都是关键环节。

2.1 微调流程

微调时,需进行以下操作:
1. 摄入并准备应用数据和反馈数据。
2. 运行迁移学习任务,对基础模型进行调整,并进行广泛测试。
3. 将新构建的模型部署到暂存或生产环境。

由于目标模型规模较大,需要在多个系统和 GPU 上进行分布式训练(调整)和验证,可借助 Horovod/MPI 或 Ray 等框架实现。

以下是一个简单的代码示例,用于测试基础 LLM(在微调前):

from transformers import AutoTokenizer, AutoModelForCausalLM, \
    GenerationConfig, pipeline

model_name = "gpt2-medium"
model_name = "tiiuae/falcon-7b"
tokenizer = AutoTokenizer.from_pretrained(model_name)
generation_config = GenerationConfig.from_pretrained(model_name)
generator = pipeline("text-generation", model=model_name, tokenizer=tokenizer,
                     trust_remote_code=True)

def prompt_to_response(prompt: str) -> str:
    return generator(prompt, generation_config=generation_config, max_length=50,
                     pad_token_id=tokenizer.eos_token_id)[0]["generated_text"]

print(prompt_to_response(prompt="What is a serving pipeline?"))
2.2 提示工程流程

实施提示工程时,若需要向提示中提供参考文档或上下文,需对上下文进行准备和索引,以便高效检索。例如,使用嵌入和向量或关键字数据库。同时,输入数据库并进行索引的数据必须经过彻底清理和准备,以提高响应质量并避免风险。

有多个新框架可用于语言处理任务的数据处理和索引,如 LangChain、LlamaIndex、spaCy 和 Unstructured。

预索引的数据和调整后的模型用于交互式或实时管道,该管道拦截用户请求并给出预期答案。实时管道的步骤如下:
1. 接收和处理请求。
2. 数据丰富。
3. 提示工程。
4. LLM 预测。
5. 后处理。
6. 向用户返回响应。

此外,传入的数据和响应会发送到监控系统进行存储,用于识别漂移、性能问题或风险。部分监控数据可用于人工标注,并用于重新调整模型(RLHF)。

实时管道面临一些挑战,如下表所示:
|挑战|描述|
|----|----|
|元素集成复杂性|涉及数据处理和丰富、应用逻辑、模型预测、监控、人工反馈等不同元素的集成|
|模型分区|将大型模型分区到多个 GPU 设备和系统|
|模型性能|LLM 速度较慢,单个用户流程可能需要多次调用|
|实时验证|实时请求和响应验证以避免风险|
|持续部署和升级|需要进行持续部署和滚动升级|

弹性无服务器框架和自动化可降低这些复杂性。

3. 应用示例:微调 LLM 模型

以下是一个使用微调后的大语言模型构建和运行智能问答应用的示例,该应用使用 MLRun 快速构建、运行和监控两个管道。

3.1 数据准备和调优
  • 数据获取与预处理 :从 Hugging Face 下载开源 LLM 模型,并使用从博客页面生成的 MLOps 数据集进行微调。首先从互联网(博客)读取页面,将其转换为清理后的文本,并进行预处理以提高模型性能。
  • 分布式调优 :使用 Horovod、MPI 和 DeepSpeed 在多个系统和 GPU 上扩展调优过程。
  • 模型评估 :使用困惑度指标评估模型,并生成评估报告。

以下是数据准备和预处理的部分代码示例:

# 示例 8 - 2. 转换 HTML 文本和标题为标记文本
def mark_header_tags(soup: BeautifulSoup):
    """
    Adding header token and article token prefixes to all headers in html,
    in order to parse the text later easily.
    :param soup: BeautifulSoup object of the html file
    """
    nodes = soup.find_all(re.compile("^h[1-6]$"))
    # Tagging headers in html to identify in text files:
    if nodes:
        content_type = type(nodes[0].contents[0])
        nodes[0].string = content_type(
            ARTICLE_TOKEN + normalize(str(nodes[0].contents[0]))
        )
        for node in nodes[1:]:
            if node.string:
                content_type = type(node.contents[0])
                if content_type == Tag:
                    node.string = HEADER_TOKEN + normalize(node.string)
                else:
                    node.string = content_type(HEADER_TOKEN + str(node.contents[0]))

# 示例 8 - 3. LLM 数据预处理
def convert_textfile_to_data_with_prompts(txt_file: Path):
    """
    Formatting the html text content into prompt form.
    Each header-content in the article is an element in the list of prompts
    :param txt_file: text content as a string with tokens of headers.
    :returns: list of prompts
    """
    # Read file:
    with open(txt_file, "r") as f:
        lines = f.readlines()
    start = 0
    end = 0
    subject_idx = []
    data = []
    # Dividing text into header - paragraph prompts:
    for i, line in enumerate(lines):
        if not start and line.startswith(ARTICLE_TOKEN):
            start = i
        elif HEADER_TOKEN + END_OF_ARTICLE in line:
            end = i
            break
        if line.startswith(HEADER_TOKEN):
            subject_idx.append(i)
    article_content = lines[start:end]
    subject_idx = [subject_i - start for subject_i in subject_idx]
    article_name = article_content[0].replace(ARTICLE_TOKEN, "")
    for i, subject in enumerate(subject_idx):
        if subject + 1 in subject_idx:
            continue
        subject_data = article_content[subject].replace(HEADER_TOKEN, "")
        if i + 1 == len(subject_idx):
            content_end = len(article_content)
        else:
            content_end = subject_idx[i + 1]
        content_limits = subject + 1, content_end
        data.append(
            DATA_FORMAT.format(
                article_name,
                subject_data,
                "".join(article_content[content_limits[0] : content_limits[1]]),
            )
        )
    return data

在 MLRun 中,首先需要定义项目并运行管道:

# 示例 8 - 4. MLRun 项目设置
import mlrun
project = mlrun.load_project(
    name="mlopspedia-bot",
    context="./",
    user_project=True,
    parameters={
        "source": "git://github.com/mlrun/demo-llm-tuning.git#main",
        "default_image": "yonishelach/mlrun-llm",
    })

# 示例 8 - 5. 运行调优管道
workflow_run = project.run(
    name="training_workflow",
    arguments={
        "html_links": "/User/demo-llm-tuning/data/html_urls.txt",
        "model_name": "falcon-7b-mlrun",
        "pretrained_tokenizer": model_name,
        "pretrained_model": model_name,
        "epochs": 5,
    },
    watch=True,
    dirty=True,
)

管道完成后,模型会自动注册,可用于应用管道。

4. 应用和模型服务管道

应用管道在 LLM 使用场景中包含多个步骤,如数据丰富和预处理、提示工程、模型预测、应用控制流、安全、风险控制、数据后处理和格式化、监控等。构建、部署和扩展应用管道具有挑战性,且需要考虑性能、安全、可用性、版本控制和滚动升级等运营因素。

MLRun 框架使用弹性无服务器函数自动构建和部署应用管道,通过组合由多个自定义和内置步骤组成的图(DAG),并自动转换为在微服务上运行的分布式管道。

本示例使用 MLRun Serving Graph 实现应用管道,包含四个步骤:
- 数据预处理(preprocess) :将用户提示适配到模型提示结构(“Subject - Content”)。
- LLM 预测(LLMModelServer) :提供训练好的模型并进行推理以生成答案。
- 后处理(postprocess) :检查模型生成的答案是否可靠,并格式化输出。
- 毒性过滤(ToxicityClassifierModelServer) :使用 Hugging Face Evaluate 包模型进行推理,捕获有毒提示或响应,并给出合适的答案。

以下是定义服务图拓扑的代码示例:

# 示例 8 - 6. 定义服务图拓扑
# Set the topology and get the graph object:
graph = serving_function.set_topology("flow", engine="async")
# Add the steps:
graph.to(handler="preprocess", name="preprocess") \
     .to("LLMModelServer",
         name="mlopspedia",
         model_args=model_args,
         tokenizer_name=model_name,
         model_name=model_name,
         peft_model=project.get_artifact_uri("falcon-7b-mlrun")) \
     .to(handler="postprocess", name="postprocess") \
     .to("ToxicityClassifierModelServer",
         name="toxicity-classifier",
         threshold=0.7).respond()
# Plot to graph:
serving_function.plot(rankdir='LR')

定义好实时应用图后,可进行本地调试、保存或部署到集群:

# Configure (add a GPU and increase readiness timeout):
serving_function.with_limits(gpus=1)
serving_function.spec.readiness_timeout = 3000
# Save the function to the project:
project.set_function(serving_function, with_repo=True)
project.save()
# Deploy the serving function:
deployment = mlrun.deploy_function("serving")

可以使用 invoke() 方法测试部署的应用:

# 示例 8 - 7. 测试应用管道
generate_kwargs = {"max_length": 150, "temperature": 0.9, "top_p": 0.5,
                   "top_k": 25, "repetition_penalty": 1.0}
response = serving_function.invoke(
    path='/predict', body={"prompt": "What is MLRun?", **generate_kwargs}
)
print(response["outputs"])

# 示例 8 - 8. 尝试毒性语言过滤
response = serving_function.invoke(
    path='/predict', body={"prompt": "You are stupid!", **generate_kwargs}
)
print(response["outputs"])
5. 添加 Web 界面

可以使用 Gradio 快速创建一个 UI 来演示聊天应用管道的行为。以下是创建 Gradio 交互式 UI 的代码示例:

# 示例 8 - 9. 创建 Gradio 交互式 UI
import json
import gradio as gr
import requests
# Get the serving url to send requests to:
serving_url = deployment.outputs["endpoint"]
def generate(prompt, temperature, max_length, top_p, top_k, repetition_penalty):
    # Build the request for our serving graph:
    inputs = {
        "prompt": prompt,
        "temperature": temperature,
        "max_length": max_length,
        "top_p": top_p,
        "top_k": top_k,
        "repetition_penalty": repetition_penalty,
    }
    # call the serving function with the request:
    resp = requests.post(serving_url, data=json.dumps(inputs).encode("utf-8"))
    # Return the response:
    return resp.json()["outputs"]
# Set up a Gradio frontend application:
with gr.Blocks(analytics_enabled=False, theme=gr.themes.Soft()) as demo:
    gr.Markdown(
        """# LLM Playground
Play with the `generate` configurations and see how they make the
LLM's responses better or worse.
"""
    )
    with gr.Row():
        with gr.Column(scale=5):
            with gr.Row():
                chatbot = gr.Chatbot()
            with gr.Row():
                prompt = gr.Textbox(
                    label="Subject to ask about:",
                    placeholder="Type a question and Enter",
                )
        with gr.Column(scale=1):
            temperature = gr.Slider(
                minimum=0,
                maximum=1,
                value=0.9,
                label="Temperature",
                info="Choose between 0 and 1",
            )
            max_length = gr.Slider(
                minimum=0,
                maximum=1500,
                value=150,
                label="Maximum length",
                info="Choose between 0 and 1500",
            )
            top_p = gr.Slider(
                minimum=0,
                maximum=1,
                value=0.5,
                label="Top P",
                info="Choose between 0 and 1",
            )
            top_k = gr.Slider(
                minimum=0,
                maximum=500,
                value=25,
                label="Top k",
                info="Choose between 0 and 500",
            )
            repetition_penalty = gr.Slider(
                minimum=0,
                maximum=1,
                value=1,
                label="repetition penalty",
                info="Choose between 0 and 1",
            )
            clear = gr.Button("Clear")
    def respond(
        prompt,
        chat_history,
        temperature,
        max_length,
        top_p,
        top_k,
        repetition_penalty,
    ):
        bot_message = generate(
            prompt, temperature, max_length, top_p, top_k, repetition_penalty
        )
        chat_history.append((prompt, bot_message))
        return "", chat_history
    prompt.submit(
        respond,
        [prompt, chatbot, temperature, max_length, top_p, top_k, repetition_penalty],
        [prompt, chatbot],
    )
    clear.click(lambda: None, None, chatbot, queue=False)

MLRun 实现了项目的自动化打包、交付和部署。项目可保存到 Git 存储库,与代码、工作流和配置一起,通过单个命令或 API 调用加载到暂存或生产环境。此外,MLRun 与 GitHub Actions、Jenkins 和 GitLab CI 等 CI/CD 系统无缝集成,实现全自动化和 CI/CD,无需额外编码或 DevOps 操作。

大语言模型(LLM)的性能监控、定制与应用实践

6. 关键技术点分析

在整个大语言模型的应用流程中,涉及到多个关键技术点,下面对其进行详细分析。

6.1 数据处理技术

数据处理是 LLM 应用的基础,直接影响模型的性能。在数据准备阶段,需要对原始数据进行清洗、转换和标注。例如,在从博客页面获取数据时,要将 HTML 文本转换为干净的文本,并添加合适的标记,以便后续处理。像 mark_header_tags 函数,通过给 HTML 中的标题添加特定的标记,方便后续文本的解析。

# 示例 8 - 2. 转换 HTML 文本和标题为标记文本
def mark_header_tags(soup: BeautifulSoup):
    """
    Adding header token and article token prefixes to all headers in html,
    in order to parse the text later easily.
    :param soup: BeautifulSoup object of the html file
    """
    nodes = soup.find_all(re.compile("^h[1-6]$"))
    # Tagging headers in html to identify in text files:
    if nodes:
        content_type = type(nodes[0].contents[0])
        nodes[0].string = content_type(
            ARTICLE_TOKEN + normalize(str(nodes[0].contents[0]))
        )
        for node in nodes[1:]:
            if node.string:
                content_type = type(node.contents[0])
                if content_type == Tag:
                    node.string = HEADER_TOKEN + normalize(node.string)
                else:
                    node.string = content_type(HEADER_TOKEN + str(node.contents[0]))

同时,将文本转换为适合模型输入的提示和答案格式也非常重要。 convert_textfile_to_data_with_prompts 函数将文档文本根据标题和内容进行分割,生成一系列的提示 - 答案对。

# 示例 8 - 3. LLM 数据预处理
def convert_textfile_to_data_with_prompts(txt_file: Path):
    """
    Formatting the html text content into prompt form.
    Each header-content in the article is an element in the list of prompts
    :param txt_file: text content as a string with tokens of headers.
    :returns: list of prompts
    """
    # Read file:
    with open(txt_file, "r") as f:
        lines = f.readlines()
    start = 0
    end = 0
    subject_idx = []
    data = []
    # Dividing text into header - paragraph prompts:
    for i, line in enumerate(lines):
        if not start and line.startswith(ARTICLE_TOKEN):
            start = i
        elif HEADER_TOKEN + END_OF_ARTICLE in line:
            end = i
            break
        if line.startswith(HEADER_TOKEN):
            subject_idx.append(i)
    article_content = lines[start:end]
    subject_idx = [subject_i - start for subject_i in subject_idx]
    article_name = article_content[0].replace(ARTICLE_TOKEN, "")
    for i, subject in enumerate(subject_idx):
        if subject + 1 in subject_idx:
            continue
        subject_data = article_content[subject].replace(HEADER_TOKEN, "")
        if i + 1 == len(subject_idx):
            content_end = len(article_content)
        else:
            content_end = subject_idx[i + 1]
        content_limits = subject + 1, content_end
        data.append(
            DATA_FORMAT.format(
                article_name,
                subject_data,
                "".join(article_content[content_limits[0] : content_limits[1]]),
            )
        )
    return data
6.2 模型调优技术

模型调优是提升 LLM 性能的关键步骤。在微调过程中,采用了分布式训练技术,如使用 Horovod、MPI 和 DeepSpeed 在多个系统和 GPU 上扩展调优过程。这种分布式训练可以充分利用计算资源,加快训练速度。

MLRun 提供的 huggingface-auto-trainer 函数中的 finetune_llm 方法,使用 QLoRA(Quantized Low - Rank Adaptation)技术,在减少所需内存和计算量的同时,对 LLM 进行微调。同时,使用 evaluate 方法通过困惑度指标评估模型,并生成评估报告。

# 定义在 huggingface-auto-trainer 函数中
# finetune_llm 方法进行微调
# evaluate 方法进行评估
6.3 服务部署技术

服务部署是将训练好的模型推向实际应用的重要环节。MLRun 框架使用弹性无服务器函数自动构建和部署应用管道。通过组合多个步骤组成的图(DAG),并自动转换为在微服务上运行的分布式管道,降低了部署的复杂性。

在应用管道中,各个步骤协同工作,如数据预处理将用户提示适配到模型提示结构,LLM 预测生成答案,后处理检查答案的可靠性并格式化输出,毒性过滤捕获有毒提示或响应。

# 示例 8 - 6. 定义服务图拓扑
# Set the topology and get the graph object:
graph = serving_function.set_topology("flow", engine="async")
# Add the steps:
graph.to(handler="preprocess", name="preprocess") \
     .to("LLMModelServer",
         name="mlopspedia",
         model_args=model_args,
         tokenizer_name=model_name,
         model_name=model_name,
         peft_model=project.get_artifact_uri("falcon-7b-mlrun")) \
     .to(handler="postprocess", name="postprocess") \
     .to("ToxicityClassifierModelServer",
         name="toxicity-classifier",
         threshold=0.7).respond()
# Plot to graph:
serving_function.plot(rankdir='LR')
7. 总结与展望

通过上述的介绍,我们可以看到大语言模型在实际应用中的复杂性和挑战性。从性能监控、数据处理、模型调优到服务部署,每个环节都需要精心设计和实现。

在性能监控方面,虽然有一些近似方法可以衡量模型的漂移和检测幻觉,但仍需要不断探索更准确和高效的方法。例如,可以结合更多的语义分析和知识图谱技术,提高对模型输出的理解和判断能力。

在数据处理和模型调优方面,随着数据量的不断增加和模型规模的不断扩大,如何更高效地处理数据和进行调优将是未来的研究重点。可以探索更先进的分布式训练算法和数据压缩技术,减少计算资源的消耗。

在服务部署方面,如何确保应用的高可用性、性能和安全性是关键。可以进一步优化弹性无服务器框架,提高系统的容错能力和自适应能力。

总之,大语言模型的应用前景广阔,但也面临着诸多挑战。通过不断的技术创新和实践探索,我们有望实现更高效、更可靠的大语言模型应用。

以下是整个大语言模型应用流程的 mermaid 流程图:

graph LR
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px

    A(数据获取):::process --> B(数据预处理):::process
    B --> C(模型调优):::process
    C --> D(模型评估):::process
    D -->|合格| E(服务部署):::process
    D -->|不合格| B
    E --> F(用户请求):::process
    F --> G(数据预处理):::process
    G --> H(LLM 预测):::process
    H --> I(后处理):::process
    I --> J(毒性过滤):::process
    J --> K(返回响应):::process
    K --> L(监控系统):::process
    L -->|发现问题| M(人工标注):::process
    M --> B

这个流程图展示了从数据获取到最终返回响应的完整过程,以及监控和反馈机制,体现了大语言模型应用的闭环流程。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐