AI实战课程笔记-06 调用模型

167 阅读16分钟

LangChain六大核心组件

  • Model
  • Prompts
  • Chains
  • Memory
  • Agents
  • Indexes

image.png

本章介绍Model I/O中的第二个子模块,LLM。

1 大语言模型的发展史

  • Transformer:Google 2018 年的论文Attention is all you need,提出了Transformer架构。Transformer是几乎所有预训练模型的核心底层架构。基于Transformer预训练所得的大规模语言模型也被叫做“基础模型”(Foundation Model 或Base Model)。在这个过程中,模型学习了词汇、语法、句子结构以及上下文信息等丰富的语言知识。这种在大量数据上学到的知识,为后续的下游任务(如情感分析、文本分类、命名实体识别、问答系统等)提供了一个通用的、丰富的语言表示基础。
  • BERT:在预训练模型出现的早期,BERT是最具代表性的,也是影响力最大的模型。BERT通过同时学习文本的前向和后向上下文信息,实现对句子结构的深入理解。

2 预训练模型的微调(Fine-tuning)

  • 预训练:在大规模无标注文本数据上进行模型的训练,目标是让模型学习自然语言的基础表达、上下文信息和语义知识,为后续任务提供一个通用的、丰富的语言表示基础。  
  • 微调:在预训练模型的基础上,根据特定的下游任务对模型进行微调。因为经过预训练的大模型中所习得的语义信息和所蕴含的语言知识,能够非常容易地向下游任务迁移。NLP应用人员可以对模型的头部或者部分参数根据自己的需要进行适应性的调整,这通常涉及在相对较小的有标注数据集上进行有监督学习,让模型适应特定任务的需求。

预训练+微调的大模型应用模式的优势

  1. 预训练模型能够将大量的通用语言知识迁移到各种下游任务上,作为应用人员,我们不需要自己寻找语料库,从头开始训练大模型,这减少了训练时间和数据需求
  2. 微调过程可以快速地根据特定任务进行优化,简化了模型部署的难度
  3. 预训练+微调的架构具有很强的可扩展性,可以方便地应用于各种自然语言处理任务,大大提高了NLP技术在实际应用中的可用性和普及程度。

3 通过HuggingFace调用Llama

HuggingFace是一个开源社区,提供了大量预训练模型和微调工具,尤其是NLP任务。

3.1 注册并安装HuggingFace

  • 第一步,登录 HuggingFace 网站,并拿到专属于自己的Token。这个网站需要连接国外的VPN才可以打开。
  • 用 pip install transformers 安装HuggingFace Library。
  • 设置API Token。
    • 可以在命令行中运行 huggingface-cli login,设置API Token。
    • 也可以在程序中设置API Token,但是这不如在命令行中设置来得安全。
import os
os.environ['HUGGINGFACEHUB_API_TOKEN'] = '你的HuggingFace API Token'

3.2 申请使用 Meta 的 Llama2 模型

Llama2模型的版本有很多,有13b\70b\chat版以及各种各样的非Meta官方版。我们这里用的是最小的7B版。

选择meta-llama/Llama-2-7b这个模型后,能够看到这个模型的基本信息。如果是第一次用Llama,需要申请Access,已经申请过的话,屏幕中间会提示:“You have been granted access to this model”。从申请到批准,大概需要几分钟。

3.3 使用HuggingFace的Transformers库来调用Llama

1 导入必要的库

from transformers import AutoTokenizer, AutoModelForCausalLM

transformers 库导入 AutoTokenizerAutoModelForCausalLM 两个类。

  • transformers库是一个用于自然语言处理(NLP)的Python库,它提供了预训练模型实现和相关工具,例如BERT、GPT、T5等。
  • AutoTokenizer类是transformers库中的一个类,用于自动加载与预训练模型相对应的分词器(Tokenizer)。分词器的作用是将文本分割成单词或子词,并将其转换为模型可以理解的数字表示。不同的预训练模型可能需要不同的分词器。
  • AutoModelForCausalLM类是transformers库中的一个类,用于自动加载因果语言模型(Causal Language Model)。因果语言模型是一种用于生成文本的模型,它根据给定的上下文预测下一个单词或字符。这类模型通常用于文本生成任务,如对话生成、故事生成等。

2 加载预训练模型的分词器

tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-2-7b-chat-hf")

AutoTokenizer 可以根据模型的名称自动选择合适的分词器类型,我们指定了模型的名称为 "meta-llama/Llama-2-7b-chat-hf"。使用AutoTokenizer 类的from_pretrained 方法从预训练的模型中加载分词器。这个方法会自动下载模型的相关文件(如果本地没有),并初始化分词器。执行这行代码后,tokenizer 变量将包含一个已经加载好的分词器对象。

3 加载预训练的模型

model = AutoModelForCausalLM.from_pretrained( 
    "meta-llama/Llama-2-7b-chat-hf", 
    device_map = 'auto')

AutoModelForCausalLM 类的 from_pretrained 方法从预训练的模型中加载模型,我们指定了模型的名称为 "meta-llama/Llama-2-7b-chat-hf"。在Hugging Face的模型库中,每个模型都有一个唯一的名称。

device_map="auto" 参数告诉 transformers 库自动将模型映射到可用的硬件设备上。如果你的系统中有多个GPU,transformers 会自动将模型的不同部分分配到不同的GPU上,以实现并行计算和优化性能。

执行这行代码后,model 变量将包含一个已经加载好的因果语言模型对象。

4 定义提示

prompt = "请给我讲个玫瑰的爱情故事?"

定义一个提示,表示希望模型基于此提示生成故事。

inputs = tokenizer(prompt, return_tensors="pt").to("cuda")

使用分词器将该提示转换为模型可以接受的格式,return_tensors="pt" 表示返回PyTorch张量。

.to("cuda")将生成的 PyTorch 张量移动到 CUDA 设备上。CUDA 是 NVIDIA 推出的一种并行计算平台和编程模型,它允许开发者使用 GPU 进行高效的并行计算。如果使用CPU可以试着将这段代码删除。

5 生成响应

outputs = model.generate(inputs["input_ids"], max_new_tokens=2000)

使用模型的 .generate() 方法生成响应,inputs["input_ids"] 表示输入提示经过分词器(tokenizer)处理后得到的令牌(tokens)的索引,max_new_tokens=2000 指定了生成的文本的最大长度。

response = tokenizer.decode(outputs[0], skip_special_tokens=True)
print(response)

使用分词器的 .decode() 方法将输出的数字转化回文本,并且跳过任何特殊的标记。

outputs[0]是模型生成的输出结果,通常是一个包含多个令牌的数组。这里使用 [0]是因为生成的结果可能是一个批次(batch),而我们只需要处理第一个样本。

skip_special_tokens=True:这个参数告诉分词器在解码时跳过任何特殊的令牌,例如 [CLS][SEP] 等。这些特殊令牌通常用于标记输入的开始和结束,或者用于分隔不同的部分,但在生成的文本中通常不需要包含它们。

4 将HuggingFace里的模型接入LangChain

4.1 通过HuggingFace Hub

第一种集成方式,是通过HuggingFace Hub。HuggingFace Hub 是一个开源模型中心化存储库,主要用于分享、协作和存储预训练模型、数据集以及相关组件。

导入需要的库

from langchain import PromptTemplate, HuggingFaceHub, LLMChain
  • PromptTemplate模块用于创建提示模板,可以构建与语言模型交互的格式化字符串。
  • HuggingFaceHub模块提供了与Hugging Face模型仓库的集成。
  • LLMChain模块用于构建语言模型链。语言模型链是一个序列,它包含了多个组件,如提示模板、语言模型和输出处理器等。通过LLMChain,你可以将这些组件组合在一起,构建一个完整的语言模型应用。

初始化HF LLM

llm = HuggingFaceHub(
    repo_id="google/flan-t5-small",
    #repo_id="meta-llama/Llama-2-7b-chat-hf", 
)

HuggingFaceHub 类的 repo_id 中指定模型名称,就可以直接下载并使用模型,模型会自动下载到HuggingFace的Cache目录,并不需要手工下载。 repo_id 是模型的仓库 ID,指定了要使用的模型。repo_id="google/flan-t5-small" 使用的是 Google 的 Flan-T5 模型的一个小版本。被注释掉的代码 repo_id="meta-llama/Llama-2-7b-chat-hf" 指定的是另一个模型的仓库 ID,Meta 的 Llama-2 模型的一个 7B 参数版本,专门用于聊天任务。

这里注释掉这行代码是因为题主在尝试使用 meta-llama/Llama-2-7b-chat-hf 这个模型时,出现了错误,因此尝试用比较旧的模型 google/flan-t5-small 做测试。

创建提示模板,生成提示

template = """Question: {question} 
              Answer: """
prompt = PromptTemplate(template=template, input_variables=["question"])       

调用LLMChain

llm_chain = LLMChain(
    prompt=prompt,
    llm=llm 
)

调用模型并返回结果

question = "Rose is which type of flower?"
print(llm_chain.run(question))

4.2 通过 HuggingFace Pipeline

HuggingFace 的 Pipeline 是一种高级工具,它简化了多种常见自然语言处理(NLP)任务的使用流程,使得用户不需要深入了解模型细节,也能够很容易地利用预训练模型来做任务。

指定预训练模型的名称

model = "meta-llama/Llama-2-7b-chat-hf"

从预训练模型中加载分词器

from transformers import AutoTokenizer 

tokenizer = AutoTokenizer.from_pretrained(model)

创建文本生成的管道

import transformers 
import torch 
pipeline = transformers.pipeline(     
    "text-generation",     
    model=model,     
    torch_dtype=torch.float16,     
    device_map="auto",     
    max_length = 1000 
)
  • torch 是一个开源的机器学习库,主要用于深度学习和人工智能领域。它提供了多种数据结构和函数,用于构建和训练神经网络。
  • transformers.pipeline 的配置参数:
    • "text-generation" 这个参数告诉pipeline我们希望执行的是文本生成任务。 transformers 库支持多种NLP任务,包括但不限于文本分类(“text-classification”)、问答(“question-answering”)、摘要(“summarization”)等。
    • model 指定了我们想要加载的预训练模型的名称或路径。
    • torch_dtype 这个参数用来设计计算的数据类型。我们这里设置为torch.float16也被称为半精度浮点数,这样可以减少内存的使用并提高计算速度,但可能会牺牲一些数值的精度。这样做对大模型和在GPU上运行的情况尤其有用。
    • device_map 这个参数用来设置运行模型的设备。"auto" 意味着pipeline会自动选择可用的设备。如果有GPU可用,它会优先选择GPU,否则就使用CPU。
    • max_length 设定生成文本的最大长度。如果模型生成的文本超过这个长度,那么超出部分的文本将被截断。该参数可以防止模型生成过长的输出,以免消化资源。

创建HuggingFacePipeline实例

from langchain import HuggingFacePipeline 
llm = HuggingFacePipeline(pipeline = pipeline, 
                          model_kwargs = {'temperature':0})

使用HuggingFacePipeline类创建一个实例llm,并传入两个参数pipelinemodel_kwargs={"temperature": 0}

model_kwargs={"temperature": 0}是一个字典,包含了模型的参数。在这里,我们设置了temperature参数为0,这意味着模型生成的文本多样性较低。

定义模板,并创建提示

# 定义输入模板,该模板用于生成花束的描述
template = """               
            为以下的花束生成一个详细且吸引人的描述:
            花束的详细信息:
            ```{flower_details}``` 
           """ 

# 使用模板创建提示
from langchain import PromptTemplate,  LLMChain 
prompt = PromptTemplate(template=template, 
                    input_variables=["flower_details"])

创建LLMChain实例并生成结果

# 创建LLMChain实例 
from langchain import PromptTemplate 
llm_chain = LLMChain(prompt=prompt, llm=llm) 

# 需要生成描述的花束的详细信息 
flower_details = "12支红玫瑰,搭配白色满天星和绿叶,包装在浪漫的红色纸中。" 

# 打印生成的花束描述 
print(llm_chain.run(flower_details))

5 用LangChain调用自定义语言模型

5.1 模型量化

量化是AI模型大小和性能优化的常用技术,它将模型的权重简化到较少的位数,以减少模型的大小和计算需求,让大模型甚至能够在CPU上面运行。

当你看到模型的后缀有GGML或者GPTQ,就说明模型已经被量化过,其中GPTQ 是一种仅适用于 GPU 的特定格式。GGML 专为 CPU 和 Apple M 系列设计,但也可以加速 GPU 上的某些层。llama-cpp-python这个包就是为了实现GGML而制作的。

这部分课程演示使用的就是量化过的Llama模型llama-2-7b-chat.ggmlv3.q4_K_S.bin模型。使用这个模型前需要先在终端用 pip install llama-cpp-python 的命令安装包。

5.2 自定义LLM类

导入需要的库

from llama_cpp import Llama 
from typing import Optional, List, Mapping, Any 
from langchain.llms.base import LLM
  • llama_cpp 是一个用于与LLaMA模型交互的Python库,LLaMA是一个由Meta AI发布的大型语言模型。
  • typing 是Python标准库中的一个模块,它提供了类型提示的支持,使得代码更加清晰和易于理解。
    • Optional:表示一个类型是可选的,即它可以是指定的类型,也可以是None。在类型提示中,通常用于表示函数参数或返回值可以是某个类型,也可以是None
    • List:表示一个列表类型,其中的元素类型可以是任意类型。在类型提示中,通常用于表示函数参数或返回值是一个列表。
    • Mapping:表示一个映射类型,通常是字典(dict)类型,其中键和值的类型可以是任意类型。在类型提示中,通常用于表示函数参数或返回值是一个字典。
    • Any:表示一个任意类型,即它可以是任何类型。在类型提示中,通常用于表示函数参数或返回值的类型是未知的或者可以是任意类型。
  • langchain.llms.base 是LangChain库中的一个模块,它定义了LLM(Large Language Model,大型语言模型)的基本接口和抽象类。

定义模型的名称和路径常量

MODEL_NAME = "llama-2-7b-chat.ggmlv3.q4_K_S.bin"
MODEL_PATH = "/home/huangj/03_Llama/"

这里的路径常量提供的是题主的路径,我们在定义自己的模型时需要替换为自己的模型常量。

自定义LLM类

class CustomLLM(LLM):
    model_name = MODEL_NAME
    
    def _call(self, prompt: str, stop: Optional[List[str]] = None) -> str:
        prompt_length = len(prompt) + 5
        llm = Llama(model_path=MODEL_PATH + MODEL_NAME, n_threads=4)
        response = llm(f"Q: {prompt} A: ", max_tokens=256)
        output = response["choices"][0]["text"].replace("A: ", "").strip()
        return output[prompt_length:]
        
    @property
    def _identifying_params(self) -> Mapping[str, Any]:
        return {"name_of_model": self.model_name}
    
    @property
    def _llm_type(self) -> str:
        return "custom"
class CustomLLM(LLM):
    model_name = MODEL_NAME

自定义 CustomLLM 类,继承自基础LLM类。

在Python中,全局变量可以在类的内部被访问,但它们不属于类的属性,也不会被类的实例所共享。model_name 是一个全局变量,它被定义在类 CustomLLM 的外部,并且在类的定义中被直接引用。

def _call(self, prompt: str, stop: Optional[List[str]] = None) -> str:
        prompt_length = len(prompt) + 5
        llm = Llama(model_path=MODEL_PATH + MODEL_NAME, n_threads=4)
        response = llm(f"Q: {prompt} A: ", max_tokens=256)
        output = response["choices"][0]["text"].replace("A: ", "").strip()
        return output[prompt_length:]
  • _call 方法接受两个参数:prompt 和 stopprompt 是必需的,类型为字符串,代表用户的输入提示。stop 是可选的,类型为字符串列表,用于指定生成文本的结束标记。
  • prompt_length = len(prompt) + 5 计算了提示字符串的长度,并加上了5。这个5可能是一个额外的偏移量,用于在后续处理中考虑某些额外的字符或空间。
  • llm = Llama(model_path=MODEL_PATH + MODEL_NAME, n_threads=4) 初始化了一个Llama模型实例,指定了模型的路径和线程数。n_threads 指的是在进行模型推理时,允许同时运行的线程数量。
  • response = llm(f"Q: {prompt} A: ", max_tokens=256)使用Llama模型生成了一个回复。f"Q: {prompt} A: " 是一个格式化字符串(f-string),它用于将变量 prompt嵌入到字符串中。这个格式化字符串的作用是构建一个问题(Question)和一个回答(Answer)的格式,这里的 Q: 和 A: 是问题和回答的标识符,用于区分问题和回答的部分。max_tokens=256 指定了生成回复的最大长度。
  • output = response["choices"][0]["text"].replace("A: ", "").strip()
    • response["choices"][0]["text"]:这部分代码从 response 字典中提取第一个 choice 的 text 属性。在自然语言处理中,模型通常会返回多个可能的回答,每个回答称为一个 choice。这里我们只取第一个 choice 的文本。1. .replace("A: ", ""):这部分代码
    • .replace("A: ", "") 将提取的文本中的 "A: " 字符串替换为空字符串,即删除 "A: " 前缀。这是因为在某些模型的输出中,回答可能会以 "A: " 开头,我们需要去掉这个前缀以得到干净的回答文本。
    • .strip()调用了字符串的 strip() 方法,该方法会移除字符串开头和结尾的空白字符(如空格、制表符、换行符等)。
  • return output[prompt_length:] 返回了生成的回复,但剔除了问题部分和之前计算的额外字符长度。
    @property
    def _identifying_params(self) -> Mapping[str, Any]:
        return {"name_of_model": self.model_name}
  • @property 是Python中的一个装饰器,它允许将一个方法转换为一个属性,这样就可以像访问属性一样访问这个方法。当使用 @property 装饰器时,可以定义一个方法,它将作为一个只读属性。如果想要允许属性被修改,可以使用 @property 装饰器来定义一个getter方法,并使用 @<property_name>.setter 装饰器来定义一个setter方法。
  • 定义了一个名为 _identifying_params的属性方法,该方法返回一个映射(Mapping)类型,其中包含一个键值对,键为 "name_of_model",值为 self.model_name。这个属性方法的作用是提供一个标识,用于识别当前使用的模型名称。
    @property
    def _llm_type(self) -> str:
        return "custom"

定义了一个名为 _llm_type 的属性方法,该方法返回一个字符串 "custom"。这个属性方法的作用是提供一个标识,用于表明当前使用的LLM(Language Model)类型是自定义的。

5.3 生成回复

llm = CustomLLM()
result = llm(
    "昨天有一个客户抱怨他买了花给女朋友之后,两天花就枯了,你说作为客服我应该怎么解释?"
)
print(result)

先初始化自定义的LLM类,然后使用自定义的LLM生成了一个回复。