Datawhale AI 夏令营【2024年第四期】:大模型开发学习笔记

254 阅读13分钟

一、背景知识

  • 大语言模型

类别:① 预测下一个词 ② 预测缺失的词(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模型结构。

image.png

⭐本次学习以 源2.0-2B 模型为例。

  • 大模型性能提升方法

Prompt工程】 ①上下文学习ICL(给出任务说明和示例)②思维链提示CoT

Embedding】 外部知识、私域数据等转成向量放入知识库,通过检索作为LLM的背景信息。

参数高效微调】也称为轻量化微调,只调整少量参数但达到全量微调的效果。

  • 开发流程

image.png

【客户端·交互】

Gradio:输入输出组件、控制组件【无输入输出,按钮等】、布局组件等

Streamlit:直接输入markdown格式的文本,网页即可渲染好(或许像latex)

【服务端·接口】

直接调用大模型API:将请求直接发送给相应的服务商,如openai,讯飞星火等,等待API返回大模型回复

大模型本地部署:在本地GPU或者CPU上,下载模型文件,并基于推理框架进行部署大模型

【算力】

运行开源大模型,CPU/GPU够不够。云端大模型服务平台。

二、跑通baseline & 精读代码(8.8~8.10)

task1:速通教程

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链接

image.png

就可以开始对话了(感觉响应速度一般)

image.png

2.2 精读 baseline 代码

image.png

① 导入库

所需依赖包括 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()
  • 为什么需要加入special tokens?

    因为自定义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入门

task3教程

3.1 理论知识

RAG(Retrieval Augmented Generation),即检索增强生成。

image.png

图片来源:github.com/netease-you…

  • 索引(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最后一层的隐藏层向量作为文本的表示。

      image.png

  • 检索(retrieval)。计算问题(query)和 Chunks 的相似度,检索出若干个相关的 Chunk。

    • 召回(提问后检索前):从知识库中 快速 获得大量大概率相关的Chunk,降低计算复杂度。基于字符串的匹配算法(TF-IDF,BM25),向量检索(faissannoy)。

    • 精排(一阶段检索):将 query 向量与知识库中所有 chunk 计算相似度,得到最相近的一系列 chunk 。

    • 重排(二阶段检索):利用重排模型(Reranker),使得越相似的结果排名更靠前,实现准确率稳定增长,即数据越多,效果越好。

      (计算量和检索效果的顺序是 召回 < 精排 < 重排

  • 生成(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)知识库数据。

image.png

⑤ 生成

封装一个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之后,办学年份就变准确了,减少幻觉。

    image.png

  • 换成Yuan2-2B-July,依然是RAG效果更好捏。

    image.png

3.3 后续探索

  1. 构建知识库
    • 领域:比如金融,有啥需求需要解决(金融论文阅读助手)
    • 来源:百度百科,知网,财经新闻,Bloomberg,investing...
    • 预处理:数据清洗,去重
  2. 索引
    • 知识库怎么切分
    • 选用embedding模型
  3. 检索
    • 根据知识库大小,召回 or 精排 or 重排
  4. 生成
    • prompt调优