自然语言处理:大语言模型综述与ChatGPT入门

随着自然语言处理(Natural Language Processing, NLP)的发展,此技术现已广泛应用于文本分类、识别和总结、机器翻译、信息提取、问答系统、情感分析、语音识别、文本生成等任务。
研究人员发现扩展模型规模可以提高模型能力,由此创造了术语——大语言模型(Large Language Model, LLM),它代指大型的预训练语言模型(Pre-training Language Model, PLM),其通常包含数千亿(甚至更多)个参数。大语言模型的一个最显著的进展是OpenAI基于LLM开发的聊天机器人ChatGPT,在此篇博客中,我将介绍大语言模型的历史演进、基础知识、核心技术以及未来展望等,并通过调用API介绍ChatGPT是如何搭建的。

语言模型的历史演进

语言模型(LM)是为了对词序列的生成概率进行建模,从而预测未来或缺失的词的概率,其发展主要有以下三个阶段:

  • 统计语言模型(SLM):基于统计学习方法(如马尔可夫假设)建立词预测模型,根据最近的上下文预测下一个词。
  • 神经语言模型(NLM):通过神经网络(如循环神经网络RNN)来描述预测单词序列的概率。
  • 大语言模型(LLM):研究人员发现扩展模型规模可以提高模型能力,通过使用Transformer架构构建大规模语言模型,并确立了“预训练和微调”的范式,即在大规模语料库上进行预训练,对预训练语言模型进行微调以适配不同的下游任务,并提高LLM的各项性能。

大语言模型基础知识

预训练Pre-traning

模型的预训练首先需要高质量的训练数据,这些数据往往来自于网页、书籍、对话、科学文献、代码等,收集到这些数据后,需要对数据进行预处理,特别是消除噪声、冗余、无关和潜在有害的数据。一个典型的预处理数据流程如下:

  • 质量过滤:删除低质量数据;
  • 去重:删除重复数据;
  • 去除隐私:删除涉及隐私的数据;
  • Token化:将原始文本分割成词序列(Token),随后作为大语言模型的输入。

目前大语言模型的主流架构可分为三大类型:编码器-解码器、因果解码器和前缀解码器,还有一种利用上述三种架构搭建的混合架构:

  • 编码器-解码器架构:利用传统的Transformer架构,编码器利用堆叠的多头自注意力层(Self-attention)对输入序列进行编码以学习其潜在表示,而解码器对这些表示进行交叉注意力(Cross-attention)计算并自回归地生成目标序列。目前只有少数LLM是利用此架构搭建,例如T5、BART。
  • 因果解码器架构:它采用单向注意力掩码,以确保每个输入token只能关注过去的token和它本身。输入和输出token通过解码器以相同的方式处理。GPT系列、OPT、BLOOM和Gopher等模型便是基于因果解码器架构开发的,目前使用较为广泛。
  • 前缀解码器架构:前缀解码器架构又称非因果解码器架构,它修正了因果解码器的掩码机制,以使其能够对前缀token执行双向注意力,并仅对生成的token执行单向注意力,这样,与编码器-解码器架构类似,前缀解码器可以双向编码前缀序列并自回归地逐个预测输出token,其中在编码和解码的过程中共享相同的参数。使用此架构的代表:GLM-130B和U-PaLM等。
  • 混合架构:利用混合专家(MoE)策略对上述三种架构进行扩展,例如Switch Transformer和GLaM等。

微调Fine-Tuning

为了使大语言模型适配特定的任务,可使用指令微调(Instruction Tuning)和对齐微调(Alignment Tuning)等技术方法;由于大语言模型包含了大量的任务,如果进行全参数微调将会有较大开销,对参数进行高效微调的方法有:适配器微调(Adapter Tuning)、前缀微调(Prefix Tuning)、提示微调(Prompt Tuning)和低秩适配(LoRA)等,高效微调的方法在此暂不展开介绍,有兴趣的小伙伴可以自行查阅相关资料。

指令微调Instruction Tuning

指令微调通过使用自然语言描述的混合多任务数据集进行有监督地微调,从而使得大语言模型能够更好地完成下游任务,具备更好的泛化能力。在此过程中伴随着参数的更新。

对齐微调Alignment Tuning

对齐微调旨在将LLM的行为与人类价值观或偏好对齐。它需要从人类标注员(需要具备合格的教育水平甚至满足一定学历要求)中收集高质量的人类反馈数据,然后利用这些数据对模型进行微调。典型的微调技术包括:基于人类反馈的强化学习(RLHF)。

为了使大语言模型与人类价值观保持一致,学者提出了基于人类反馈的强化学习(RLHF),即使用收集到的人类反馈数据结合强化学习对LLM进行微调,有助于改善模型的有用性、诚实性和无害性。RLHF采用强化学习(RL)算法,例如近端策略优化(Proximal Policy Optimization, PPO)通过学习奖励模型使LLM适配人类反馈。

微调开发(调用ChatGPT的API)

模型微调的优势:
(1)定制化模型,拓展应用能力;
(2)更高质量的结果;
(3)节省token成本(因为不再需要大篇幅的提示);
(4)更低的时延。

但是ChatGPT鼓励优先使用提示开发,因为相较于提示工程,微调开发需要准备更多的数据,投入更多的时间成本,所以如果使用提示而不使用微调可以达到一个优异的结果,那么优先使用提示开发,否则可以使用微调开发。以下是一些常见的场景,使用微调可能带来更好的结果:
(1)设置风格、基调、格式或其他定性方面;
(2)提高所需输出的可靠性;
(3)纠正未能遵循复杂的提示;
(4)存在较多的边缘情况;
(5)面对一个新任务或应用。

一旦确定微调是正确的解决方案(即已经尽可能优化了提示并确定模型仍然存在的问题),那么下面我将演示如何进行微调开发。
1、准备并上传数据
创建数据集,数据集的每个示例应该是与ChatGPT聊天的消息格式相类似的对话,每条消息都包含有role(角色)、content(内容)和其他可选名称。至少一些训练示例应该直接针对提示模型未按预期运行的情况,并且数据中提供的辅助消息应该是您希望模型提供的理想响应。

在此示例中,我们的目标是创建一个偶尔给出讽刺性响应的聊天机器人,这是我们可以为数据集创建的三个训练示例(对话):

{"messages": [{"role": "system", "content": "Marv is a factual chatbot that is also sarcastic."}, {"role": "user", "content": "What's the capital of France?"}, {"role": "assistant", "content": "Paris, as if everyone doesn't know that already."}]}
{"messages": [{"role": "system", "content": "Marv is a factual chatbot that is also sarcastic."}, {"role": "user", "content": "Who wrote 'Romeo and Juliet'?"}, {"role": "assistant", "content": "Oh, just some guy named William Shakespeare. Ever heard of him?"}]}
{"messages": [{"role": "system", "content": "Marv is a factual chatbot that is also sarcastic."}, {"role": "user", "content": "How far is the Moon from Earth?"}, {"role": "assistant", "content": "Around 384,400 kilometers. Give or take a few, like that really matters."}]}

要微调模型,您需要提供至少 10 个示例。我们通常会看到对 50 到 100 个训练示例进行微调会带来明显的改进,但正确的数量根据具体的应用而有很大差异。建议从 50 个精心设计的演示开始,看看模型在微调后是否显示出改进的迹象。在某些情况下可能就足够了,但即使模型尚未达到预期的质量,明显的改进也是一个好兆头,表明提供更多数据将继续改进模型。没有任何改进表明您可能需要重新考虑如何在超出有限示例集之前设置模型任务、重组数据(考虑数据的多样性、平衡性或检查数据的格式、语法或逻辑等是否存在问题)。注意:较少量的高质量数据通常比较大量的低质量数据更有效。

将数据集划分为训练集和测试集,每个训练示例的token数限制在4096以内,如果超出了4096将对示例进行截断。每个文件限制在50M以内。上传训练文件和测试文件后,系统会提供一个统计数据,用于评估微调后模型是否提升以及提升了多少。

在数据上传前,有必要对数据集示例的数据格式进行检验,检验的python文件如下:

# We start by importing the required packages

import json
import os
import tiktoken
import numpy as np
from collections import defaultdict

# Next, we specify the data path and open the JSONL file

data_path = "<YOUR_JSON_FILE_HERE>"

# Load dataset
with open(data_path) as f:
    dataset = [json.loads(line) for line in f]

# We can inspect the data quickly by checking the number of examples and the first item

# Initial dataset stats
print("Num examples:", len(dataset))
print("First example:")
for message in dataset[0]["messages"]:
    print(message)

# Now that we have a sense of the data, we need to go through all the different examples and check to make sure the formatting is correct and matches the Chat completions message structure

# Format error checks
format_errors = defaultdict(int)

for ex in dataset:
    if not isinstance(ex, dict):
        format_errors["data_type"] += 1
        continue

    messages = ex.get("messages", None)
    if not messages:
        format_errors["missing_messages_list"] += 1
        continue

    for message in messages:
        if "role" not in message or "content" not in message:
            format_errors["message_missing_key"] += 1

        if any(k not in ("role", "content", "name") for k in message):
            format_errors["message_unrecognized_key"] += 1

        if message.get("role", None) not in ("system", "user", "assistant"):
            format_errors["unrecognized_role"] += 1

        content = message.get("content", None)
        if not content or not isinstance(content, str):
            format_errors["missing_content"] += 1

    if not any(message.get("role", None) == "assistant" for message in messages):
        format_errors["example_missing_assistant_message"] += 1

if format_errors:
    print("Found errors:")
    for k, v in format_errors.items():
        print(f"{k}: {v}")
else:
    print("No errors found")

# Beyond the structure of the message, we also need to ensure that the length does not exceed the 4096 token limit.

# Token counting functions
encoding = tiktoken.get_encoding("cl100k_base")

# not exact!
# simplified from https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb
def num_tokens_from_messages(messages, tokens_per_message=3, tokens_per_name=1):
    num_tokens = 0
    for message in messages:
        num_tokens += tokens_per_message
        for key, value in message.items():
            num_tokens += len(encoding.encode(value))
            if key == "name":
                num_tokens += tokens_per_name
    num_tokens += 3
    return num_tokens

def num_assistant_tokens_from_messages(messages):
    num_tokens = 0
    for message in messages:
        if message["role"] == "assistant":
            num_tokens += len(encoding.encode(message["content"]))
    return num_tokens

def print_distribution(values, name):
    print(f"\n#### Distribution of {name}:")
    print(f"min / max: {min(values)}, {max(values)}")
    print(f"mean / median: {np.mean(values)}, {np.median(values)}")
    print(f"p5 / p95: {np.quantile(values, 0.1)}, {np.quantile(values, 0.9)}")

# Last, we can look at the results of the different formatting operations before proceeding with creating a fine-tuning job:

# Warnings and tokens counts
n_missing_system = 0
n_missing_user = 0
n_messages = []
convo_lens = []
assistant_message_lens = []

for ex in dataset:
    messages = ex["messages"]
    if not any(message["role"] == "system" for message in messages):
        n_missing_system += 1
    if not any(message["role"] == "user" for message in messages):
        n_missing_user += 1
    n_messages.append(len(messages))
    convo_lens.append(num_tokens_from_messages(messages))
    assistant_message_lens.append(num_assistant_tokens_from_messages(messages))

print("Num examples missing system message:", n_missing_system)
print("Num examples missing user message:", n_missing_user)
print_distribution(n_messages, "num_messages_per_example")
print_distribution(convo_lens, "num_total_tokens_per_example")
print_distribution(assistant_message_lens, "num_assistant_tokens_per_example")
n_too_long = sum(l > 4096 for l in convo_lens)
print(f"\n{n_too_long} examples may be over the 4096 token limit, they will be truncated during fine-tuning")

# Pricing and default n_epochs estimate
MAX_TOKENS_PER_EXAMPLE = 4096

MIN_TARGET_EXAMPLES = 100
MAX_TARGET_EXAMPLES = 25000
TARGET_EPOCHS = 3
MIN_EPOCHS = 1
MAX_EPOCHS = 25

n_epochs = TARGET_EPOCHS
n_train_examples = len(dataset)
if n_train_examples * TARGET_EPOCHS < MIN_TARGET_EXAMPLES:
    n_epochs = min(MAX_EPOCHS, MIN_TARGET_EXAMPLES // n_train_examples)
elif n_train_examples * TARGET_EPOCHS > MAX_TARGET_EXAMPLES:
    n_epochs = max(MIN_EPOCHS, MAX_TARGET_EXAMPLES // n_train_examples)

n_billing_tokens_in_dataset = sum(min(MAX_TOKENS_PER_EXAMPLE, length) for length in convo_lens)
print(f"Dataset has ~{n_billing_tokens_in_dataset} tokens that will be charged for during training")
print(f"By default, you'll train for {n_epochs} epochs on this dataset")
print(f"By default, you'll be charged for ~{n_epochs * n_billing_tokens_in_dataset} tokens")
print("See pricing page to estimate total costs")

上传数据用于微调:

import openai
import os
openai.api_key = os.getenv("OPENAI_API_KEY")

openai.File.create(
  file=open("mydata.jsonl", "rb"),
  purpose='fine-tune'
)

2、模型微调
使用OpenAI的SDK开始模型微调作业:

openai.FineTuningJob.create(training_file="file-abc123", model="gpt-3.5-turbo")

开始微调工作后,可能需要一些时间才能完成。您的作业可能排在系统中的其他作业后面,训练模型可能需要几分钟或几小时,具体取决于模型和数据集大小。模型训练完成后,创建微调作业的用户将收到一封确认电子邮件。

除了创建微调作业外,您还可以列出现有作业、检索作业状态或取消作业:

# List 10 fine-tuning jobs
openai.FineTuningJob.list(limit=10)

# Retrieve the state of a fine-tune
openai.FineTuningJob.retrieve("ft-abc123")

# Cancel a job
openai.FineTuningJob.cancel("ft-abc123")

# List up to 10 events from a fine-tuning job
openai.FineTuningJob.list_events(id="ft-abc123", limit=10)

# Delete a fine-tuned model (must be an owner of the org the model was created in)
openai.Model.delete("ft-abc123")

3、模型使用
模型训练完成后,在作业的详细信息可以看到模型名称,利用此模型便可以搭建你的定制化任务:

completion = openai.ChatCompletion.create(
  model="ft:gpt-3.5-turbo:my-org:custom_suffix:id",
  messages=[
    {"role": "system", "content": "You are a helpful assistant."},
    {"role": "user", "content": "Hello!"}
  ]
)

print(completion.choices[0].message)

4、模型分析
在模型训练过程中,系统会提供以下训练指标:训练损失、训练token准确性、测试损失和测试token准确性。这些统计数据旨在提供模型的评估,确保训练顺利进行(其中损失应该减少,token准确性应该增加)。

提示Prompt

为了使语言模型完成一些特定任务,利用在模型的输入中加入提示的机制,使得模型得到预想的结果或引导模型得到更好的结果,注意与微调不同,在提示这一过程中,无需额外的训练和参数更新。

上下文学习In-context Learning

上下文学习(In-context Learning, ICL)是由GPT-3正式引入,它的关键思想是从类比中学习,它将查询的问题和一个上下文提示(一些相关的样例)连接在一起,形成带有提示的输入,并将其输入到语言模型中进行预测。

思维链Chain-of-thought

思维链(Chain-of-thought, CoT)是一种改进的提示策略,旨在提高LLM在复杂推理任务中的性能,例如算术推理、常识推理和符号推理。具体做法是将中间推理步骤纳入到提示中,引导模型预测出正确结果。据相关论文,这种能力可能是在代码上训练而获得。

提示开发(调用ChatGPT的API)

ChatGPT是使用OpenAI开发的大语言模型进行聊天的web网站,其本质是调用ChatGPT的API完成各项任务,下面演示了使用ChatGPT的API完成总结的任务,除此之外,它还可以完成推理、翻译、问答、校对、扩展等多项任务,有时需要借助ICL或CoT获得更好的结果(前提是你需要从OpenAI官网获得API的密钥key)

import openai
import os
fron dotenv import load_dotenv, find_dotenv

_ = load_dotenv(find_dotenv())
openai.api_key = os.getenv("OPENAI_API_KEY")

def get_completion(prompt, temperature=0, model="gpt-3.5-turbo"):
	messages = [{"role": "user", "content": prompt)]
	response= openai.ChatCompletion.create(
		model=model,
		messages=messages,
		temperature=temperature, # temperature为模型的探索程度或随机性,其值是范围在0~1的浮点数,值越高则随机性越大,说明更有创造力。
	)
	return response.choices[0].message["content"]

text = f"""
XXXXXXXX
"""
prompt = f"""
Summarize the text delimited by triple backticks into a single sentence.
```{text}```
"""
response = get_completion(prompt)
print(response)

ChatGPT的web网站或者聊天机器人通常包含三个角色(role)的消息(messages),包括:用户(user)的消息,ChatGPT/聊天机器人(assistant)的消息和系统(system)的消息。下面以搭建一个“订餐机器人”为例:

  • system messages:用于设置机器人的行为和人设,作为高层指令指导机器人的对话,用户一般对此不可见;
  • user messages:是用户的输入;
  • assistant messages:是机器人的回复。

代码示例如下:

import openai
import os
fron dotenv import load_dotenv, find_dotenv

_ = load_dotenv(find_dotenv())
openai.api_key = os.getenv("OPENAI_API_KEY")

def get_completion_from_messages(messages, temperature=0, model="gpt-3.5-turbo"):
	response= openai.ChatCompletion.create(
		model=model,
		messages=messages,
		temperature=temperature, # temperature为模型的探索程度或随机性,其值是范围在0~1的浮点数,值越高则随机性越大,说明更有创造力。
	)
	return response.choices[0].message["content"]

messages = [
	"role": "system",
	"content": "你现在一个订餐机器人,你需要根据菜单收集用户的订餐需求。菜单:汉堡、薯条、炸鸡、可乐、雪碧。",
	"role": "user",
	"content": "你好,我想要一个汉堡。",
	"role": "assistant",
	"content": "请问还有其他需要的吗?",
	"role": "user",
	"content": "再要一份可乐。",
]

response=get_completion_from_messages(messages)
print(response)
# 输出示例:
# 好的,一份汉堡和可乐,已为您下单。

借助上述代码示例,设计一个GUI或Web界面就可以实现人机交互,修改system messages即可更改聊天机器人的行为并让其扮演不同的角色。

大语言模型未来展望

  • 更大规模: 模型的规模可能会继续增大,从而提高模型的表现力和语言理解能力。
  • 更好的预训练: 改进预训练策略,使模型更好地理解语义和上下文,提高模型在各种任务上的迁移能力。
  • 更好的微调: 开发更有效的微调方法,以在特定任务上获得更好的性能。
  • 多模态: 将语言模型与视觉、声音等其他模态相结合,实现跨领域的多模态智能应用。
  • 工具:利用搜索引擎、计算器和编译器等外部工具,提高语言模型在特定领域的性能。

参考文献

大语言模型综述

  • 0
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AI Player

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值