一、背景知识
- 大语言模型
类别:① 预测下一个词 ② 预测缺失的词(BERT)
迭代:
1、统计语言模型(SLM,马尔科夫假设、n-gram语言模型)
2、神经语言模型(NLM,RNN、学习上下文、词嵌入word2vec)
3、预训练语言模型(PLM,无标注数据预训练BiLSTM或Transformer、然后在下游任务上微调,ELMo、Bert、GPT-1/2)
4、大语言模型(LLM,扩展法则,增加数据数量和模型参数,涌现能力,GPT-3、ChatGPT、Claude、Llama)
- 预训练
【数量】token是数T级别的
【算力】
【多样性】
- 有监督微调/指令微调(SFT)
预训练后,比较擅长文本补全、上下文学习(ICL)等。
需要指令微调,适应其他下游任务。【催化剂,并非学知识】
- 基于人类反馈的强化学习对齐(RLHF)
使大模型与人类期望、需求及价值观对齐。(安全、有用性...)
核心在于构建一个反映人类价值观的奖励模型(Reward Model)。
(强化学习不稳定?依赖环境和智能体,训练复杂度高)
- 浪潮信息——Yuan
源1.0 开放了模型API、高质量中文数据集和代码,源2.0 和 源2.0-M32 采用全面开源策略,全系列模型参数和代码均可免费下载使用。
① Yuan 1.0:2021年,76层的Transformer Decoder结构,5T数据训练,2457亿参数量。
② Yuan 2.0:2023年,10T数据训练,包括1026亿、518亿、21亿三款参数规模;提出局部注意力过滤增强机制(Localized Filtering-based Attention, LFA),假设自然语言相邻词之间有更强的语义关联,因此针对局部依赖进行了建模(传统Attention对输入的所有文字一视同仁)。
③ Yuan 2.0-M32:2024年5月,混合专家(Mixture of Experts, MoE) 大模型(32个专家),使用2000B Tokens训练,包含400亿参数,37亿激活参数,基于LFA+Attention Router的MoE模型结构。
⭐本次学习以 源2.0-2B 模型为例。
- 大模型性能提升方法
【Prompt工程】 ①上下文学习ICL(给出任务说明和示例)②思维链提示CoT
【Embedding】 外部知识、私域数据等转成向量放入知识库,通过检索作为LLM的背景信息。
【参数高效微调】也称为轻量化微调,只调整少量参数但达到全量微调的效果。
- 开发流程
【客户端·交互】
Gradio:输入输出组件、控制组件【无输入输出,按钮等】、布局组件等
Streamlit:直接输入markdown格式的文本,网页即可渲染好(或许像latex)
【服务端·接口】
直接调用大模型API:将请求直接发送给相应的服务商,如openai,讯飞星火等,等待API返回大模型回复
大模型本地部署:在本地GPU或者CPU上,下载模型文件,并基于推理框架进行部署大模型
【算力】
运行开源大模型,CPU/GPU够不够。云端大模型服务平台。
二、跑通baseline & 精读代码(8.8~8.10)
2.1 跑通baseline
在 魔搭社区-我的notebook 创建PAI实例(demo_0808),启动,在终端依次运行以下代码。
git lfs install
git clone https://www.modelscope.cn/datasets/Datawhale/AICamp_yuan_baseline.git
pip install streamlit==1.24.0
streamlit run AICamp_yuan_baseline/Task\ 1:零基础玩转源大模型/web_demo_2b.py --server.address 127.0.0.1 --server.port 6006
点击这个URL链接
就可以开始对话了(感觉响应速度一般)
2.2 精读 baseline 代码
① 导入库
所需依赖包括 transformers(抱脸的)、torch(魔搭本身有)、streamlit。
from transformers import AutoTokenizer, AutoModelForCausalLM
import torch
import streamlit as st
② Yuan大模型下载
Yuan2-2B-Mars 支持通过多个平台进行下载,包括魔搭(IEITYuan/Yuan2-2B-Mars-hf)、HuggingFace、OpenXlab、百度网盘、WiseModel等。
snapshot_download函数:第一个参数为模型名称;第二个参数为模型保存路径, . 表示当前路径。
模型大小约为4.1G,由于是从魔搭直接下载,速度非常快。完成后,会在当前目录增加一个名为 IEITYuan 的文件夹,里面是下载好的源大模型。
from modelscope import snapshot_download
model_dir = snapshot_download('IEITYuan/Yuan2-2B-Mars-hf', cache_dir='./')
# 新的大模型更新:'IEITYuan/Yuan2-2B-July-hf'
③ 模型加载
bfloat16 和 float16 都是半精度浮点数类型,用16位存储每个数值(单精度32位,双精度64位),在GPU资源有限的情况下训练占优。
bfloat16 精度比 float16 低一些,但显存占用量更小,训练速度快。
# 定义模型路径
path = './IEITYuan/Yuan2-2B-Mars-hf'
# 新的大模型更新:'./IEITYuan/Yuan2-2B-July-hf'
# 定义模型数据类型
torch_dtype = torch.bfloat16 # A10
# torch_dtype = torch.float16 # P100
使用 transformers 中的 from_pretrained 函数来加载下载好的模型和tokenizer,并通过 .cuda() 将模型放置在GPU上。
这里额外使用了 streamlit 提供的一个装饰器 @st.cache_resource,它可以用于缓存加载好的模型和tokenizer。
# 定义一个函数,用于获取模型和tokenizer
@st.cache_resource
def get_model():
print("Creat tokenizer...")
tokenizer = AutoTokenizer.from_pretrained(path, add_eos_token=False, add_bos_token=False, eos_token='<eod>')
tokenizer.add_tokens(['<sep>', '<pad>', '<mask>', '<predict>', '<FIM_SUFFIX>', '<FIM_PREFIX>', '<FIM_MIDDLE>','<commit_before>','<commit_msg>','<commit_after>','<jupyter_start>','<jupyter_text>','<jupyter_code>','<jupyter_output>','<empty_output>'], special_tokens=True)
print("Creat model...")
model = AutoModelForCausalLM.from_pretrained(path, torch_dtype=torch_dtype, trust_remote_code=True).cuda()
print("Done.")
return tokenizer, model
# 加载model和tokenizer
tokenizer, model = get_model()
-
因为自定义token并不在预训练模型原来的词表中,直接使用tokenizer处理数据会将自定义的特殊标记当作未知字符处理。
add_tokens函数:在词表的最后添加普通token,返回值为成功添加的token个数;special_tokens参数,将其设置为true即代表添加的token为special_token。bos_token 是指句子开始,eos_token 是指句子结束的特殊标记。
④ 读取用户输入
初始化界面。session_state是一个字典。
# 创建一个标题和一个副标题
st.title("💬 Yuan2.0 智能编程助手")
# 初次运行时,session_state中没有"messages",需要创建一个空列表
if "messages" not in st.session_state:
st.session_state["messages"] = []
# 每次对话时,都需要遍历session_state中的所有消息,并显示在聊天界面上
for msg in st.session_state.messages:
st.chat_message(msg["role"]).write(msg["content"])
使用 streamlit 提供的 chat_input() 来获取用户输入,同时将其保存到对话历史里,并通过st.chat_message("user").write(prompt) 在聊天界面上进行显示。
# 如果用户在聊天输入框中输入了内容,则执行以下操作
if prompt := st.chat_input():
# 将用户的输入添加到session_state中的messages列表中
st.session_state.messages.append({"role": "user", "content": prompt})
# 在聊天界面上显示用户的输入
st.chat_message("user").write(prompt)
session_state 的结构应该是字典套list套字典 :
{ messages : [{ role : user , content : xxxx } , { role : assistant , content : xxxx } , {...} , ... ] }
⑤ 对话历史拼接 + 模型调用
对于 Yuan2-2B-Mars 模型来说:1)输入的prompt需要在末尾添加 <sep>,如果输入是多轮对话历史,需要使用 <n> 进行拼接,并且在末尾添加 <sep>;2)模型输出的结果到 <eod> 结束。
输入的prompt需要先经tokenizer切分成token,并转成对应的id,通过 .cuda() 将输入也放置在GPU上。
然后调用 model.generate() 生成输出的id,并通过 tokenizer.decode() 将id转成对应的字符串。
最后从字符串中提取模型生成的内容(即 <sep> 之后的字符串),并删除末尾的 <eod> ,得到最终的回复内容。
# 调用模型
prompt = "<n>".join(msg["content"] for msg in st.session_state.messages) + "<sep>" # 拼接对话历史
inputs = tokenizer(prompt, return_tensors="pt")["input_ids"].cuda()
outputs = model.generate(inputs, do_sample=False, max_length=1024) # 设置解码方式和最大生成长度
output = tokenizer.decode(outputs[0])
response = output.split("<sep>")[-1].replace("<eod>", '')
⑥ 模型输出
# 将模型的输出添加到session_state中的messages列表中
st.session_state.messages.append({"role": "assistant", "content": response})
# 在聊天界面上显示模型的输出
st.chat_message("assistant").write(response)
三、RAG入门
3.1 理论知识
RAG(Retrieval Augmented Generation),即检索增强生成。
-
索引(indexing)。将文档库分割成较短的 Chunk ,即文本块或文档片段,然后构建成向量索引(vectorization),即 chunk →
embedding model→ vector。计算量大,通常是离线计算;存储量大,用数据库(DataBase)管理,如 Milvus 。-
直接调用API
-
基于LLM的embedding model
-
基于BERT的embedding model【最常用】
- BGE Embedding:智源通用embedding(BAAI general embedding, BGE)
- BCEmbedding:网易有道训练的Bilingual and Crosslingual Embedding
- jina-embeddings:Jina AI训练的text embedding
- M3E:MokaAI训练的 Massive Mixed Embedding
BERT架构是一个Transformer Encoder,输入向量模型前,首先会在文本的最前面额外加一个
[CLS]token,然后将该token最后一层的隐藏层向量作为文本的表示。
-
-
检索(retrieval)。计算问题(query)和 Chunks 的相似度,检索出若干个相关的 Chunk。
-
生成(generation)。检索得到有序的 Retrieval Documents 后,从中挑选最相似的 k 个结果作为背景信息,和 query 一起构成 prompt 输入大模型,生成回答。
-
一些开源的RAG框架
- TinyRAG:DataWhale成员宋志学精心打造的纯手工搭建RAG框架。
- LlamaIndex:一个用于构建大语言模型应用程序的数据框架,包括数据摄取、数据索引和查询引擎等功能。
- LangChain:一个专为开发大语言模型应用程序而设计的框架,提供了构建所需的模块和工具。
- QAnything:网易有道开发的本地知识库问答系统,支持任意格式文件或数据库。
- RAGFlow:InfiniFlow开发的基于深度文档理解的RAG引擎。
3.2 实战练习
基于Yuan2-2B-Mars/ Yuan2-2B-July,学习构建简易的RAG系统。
git lfs install
git clone https://www.modelscope.cn/datasets/Datawhale/AICamp_yuan_baseline.git
cp AICamp_yuan_baseline/Task\ 3:源大模型RAG实战/* .
Yuan大模型之前下载过了。
from modelscope import snapshot_download
model_dir = snapshot_download('IEITYuan/Yuan2-2B-Mars-hf', cache_dir='.')
① 环境配置
需要torch、transformers、streamlit等依赖。
② embedding模型下载
选用基于BERT架构的向量模型 bge-small-zh-v1.5,共4层,最大输入长度512,输出的向量维度512。
from modelscope import snapshot_download
model_dir = snapshot_download("AI-ModelScope/bge-small-zh-v1.5", cache_dir='.')
③ 索引
导入所需的库。
from typing import List
import numpy as np
import torch
from transformers import AutoModel, AutoTokenizer, AutoModelForCausalLM
封装一个EmbeddingModel的类,主要用于加载embedding模型。
- 函数
get_embeddings(texts: List),输出文本对应的向量。
class EmbeddingModel:
# 初始化:从指定路径加载分词器和向量模型
def __init__(self, path: str) -> None:
self.tokenizer = AutoTokenizer.from_pretrained(path)
self.model = AutoModel.from_pretrained(path).cuda()
print(f'Loading EmbeddingModel from {path}.')
# 函数:将输入的文本转换为向量
def get_embeddings(self, texts: List) -> List[float]:
encoded_input = self.tokenizer(texts, padding=True, truncation=True, return_tensors='pt')
encoded_input = {k: v.cuda() for k, v in encoded_input.items()} #将编码后的input放在cpu上
with torch.no_grad(): #上下文管理器,表示在其内部的操作不会计算梯度
model_output = self.model(**encoded_input) #**解包字典,将其作为关键字参数传递给模型
sentence_embeddings = model_output[0][:, 0] #[0]获取模型输出的第一个元素,[:, 0]表示只取每个句子的第一个token的嵌入向量
sentence_embeddings = torch.nn.functional.normalize(sentence_embeddings, p=2, dim=1) #行归一化,L2范数`
return sentence_embeddings.tolist() #为了充分发挥GPU矩阵计算的优势,输入和输出都是List
调用EmbeddingModel类,创建一个实例embed_model。
print("> Create embedding model...")
embed_model_path = './AI-ModelScope/bge-small-zh-v1___5'
embed_model = EmbeddingModel(embed_model_path)
④ 检索
封装一个VectorStoreIndex类,初始化时将知识库的所有documents都转为向量。
- 函数
get_similarity(vector1: List[float], vector2: List[float]),计算余弦相似度。 - 函数
query(question: str, k: int = 1),把query转为向量,计算其与知识库所有chunk向量的相似度,输出k个与之最相似的chunk。
class VectorStoreIndex:
# 初始化:将知识库的每条数据都转为向量
def __init__(self, doecment_path: str, embed_model: EmbeddingModel) -> None:
self.documents = [] # 知识库文件
for line in open(doecment_path, 'r', encoding='utf-8'):
line = line.strip()
self.documents.append(line) #一条一条地传
self.embed_model = embed_model
self.vectors = self.embed_model.get_embeddings(self.documents) # 把documents全部向量化
print(f'Loading {len(self.documents)} documents from {doecment_path}.')
# 函数:计算余弦相似度
def get_similarity(self, vector1: List[float], vector2: List[float]) -> float:
dot_product = np.dot(vector1, vector2) # 分子,点乘
magnitude = np.linalg.norm(vector1) * np.linalg.norm(vector2) # 分母,L2范数的乘积
if not magnitude:
return 0 # 若任一为零向量,返回相似度0
return dot_product / magnitude
# 函数:把query转为向量,输出k个与之最相似的chunk
def query(self, question: str, k: int = 1) -> List[str]: #k默认为1
question_vector = self.embed_model.get_embeddings([question])[0] #question就一条数据,对应输出list的[0]元素
result = np.array([self.get_similarity(question_vector, vector) for vector in self.vectors]) # 计算问题向量和全部知识库向量的余弦相似度
return np.array(self.documents)[result.argsort()[-k:][::-1]].tolist() # argsort是从小到大的,[::-1]把顺序颠倒
调用VectorStoreIndex类,创建一个实例index。
- 虽然
get_embeddings()函数支持一次性传入多条文本,但由于GPU的显存有限,输入的文本不宜太多。若知识库很大,需要将知识库切分成多个batch,再分批次送入向量模型。
print("> Create index...")
doecment_path = './knowledge.txt'
index = VectorStoreIndex(doecment_path, embed_model) # 这里的知识库很小,可以直接传入 ;embed_model是上一步创建的实例
调用query函数,默认返回与用户查询最相似的第一条(k=1)知识库数据。
⑤ 生成
封装一个LLM类,初始化时加载大语言模型。
- 函数
generate(question: str, context: List),构建prompt(query+[context]可以没有),生成回答。
class LLM:
# 初始化:加载Yuan2-2B-Mars模型
def __init__(self, model_path: str) -> None:
print("Creat tokenizer...")
self.tokenizer = AutoTokenizer.from_pretrained(model_path, add_eos_token=False, add_bos_token=False, eos_token='<eod>')
self.tokenizer.add_tokens(['<sep>', '<pad>', '<mask>', '<predict>', '<FIM_SUFFIX>', '<FIM_PREFIX>', '<FIM_MIDDLE>','<commit_before>','<commit_msg>','<commit_after>','<jupyter_start>','<jupyter_text>','<jupyter_code>','<jupyter_output>','<empty_output>'], special_tokens=True)
print("Creat model...")
self.model = AutoModelForCausalLM.from_pretrained(model_path, torch_dtype=torch.bfloat16, trust_remote_code=True).cuda()
print(f'Loading Yuan2.0 model from {model_path}.')
# 函数 :基于question和可选的context,生成回答
def generate(self, question: str, context: List):
# 这里调用知识库咧
if context:
prompt = f'背景:{context}\n问题:{question}\n请基于背景,回答问题。'
else:
prompt = question
prompt += "<sep>" # Yuan大模型的prompt以<sep>结尾
inputs = self.tokenizer(prompt, return_tensors="pt")["input_ids"].cuda()
outputs = self.model.generate(inputs, do_sample=False, max_length=1024) # 这应该是大模型接口本身的generate函数
output = self.tokenizer.decode(outputs[0])
print(output.split("<sep>")[-1]) # <sep>之后的是回答
调用LLM类,创建一个实例llm。
print("> Create Yuan2.0 LLM...")
model_path = './IEITYuan/Yuan2-2B-Mars-hf'
llm = LLM(model_path)
基于RAG架构(上述三个类的实例合起来),生成回答。
-
有RAG之后,办学年份就变准确了,减少幻觉。
-
换成
Yuan2-2B-July,依然是RAG效果更好捏。
3.3 后续探索
- 构建知识库
- 领域:比如金融,有啥需求需要解决(金融论文阅读助手)
- 来源:百度百科,知网,财经新闻,Bloomberg,investing...
- 预处理:数据清洗,去重
- 索引
- 知识库怎么切分
- 选用embedding模型
- 检索
- 根据知识库大小,召回 or 精排 or 重排
- 生成
- prompt调优