写在前面
这次是第四期的大模型应用开发方向的Task1的代码详解,根据经验,Task2应该才是解读代码,task1是把baseline跑出来就可以,于是说我们这篇文章也相当于是把task2的内容给完成了
给第一次关注我们这个频道的再说一遍,我们讲的是应用的,会用就行,有些无关紧要或者说“最好带上”的参数我在这也不予说明,记住就行,总之就是“讲的就理解理解,不讲的要是不感兴趣的话就记住就好”,想要深入了解一些的话就看看官方文档啥的,(或者说可以私信我,我也可以考虑出一篇文章哦~)
必要的基础
我们这回默认大家都是零基础的同学,我们就从transformer开始这个,我们只讲必要的部分
如果看过代码的话可能会看到这个东西:
from transformers import AutoTokenizer, AutoModelForCausalLM
import streamlit as st
这个transformer库,可以这么理解,负责底层这个LLM的逻辑的实现,streamlit是图像化界面的交互逻辑的实现。这一回的基础我们就围绕这两个库来展开。
transformer的两个库:AutoTokenizer和AutoModelForCausalLM
首先我们简单的过一遍分词器的理念:把我们输入的信息转化成大语言模型“喜欢”看到的信息,这些信息需要被分为一些相同长度的数据,并且在这同时还进行了类似于“断句”的操作,在这里我们只需要掌握input_ids和decode就可以。
我们可以这么理解这一个过程:即我们的指令将会被分词器“翻译”成为我们的大语言模型“喜欢看到的”数据。然后给大语言模型来生成答案,最后解码成为我们能看懂的文字输出。
我们先看一下分词器,即“翻译部分”:
tokenizer(prompt, return_tensors="pt")["input_ids"].cuda()
分词器,一般来说会返回三个量:解码结果:即"input_ids";注意掩码:“attention_mask”和label。由于我们不涉及训练数据,这里我们只需要用到input_ids就可以,input_ids就是我们“翻译”的直接结果。我们只取这个。
我们再来看一下生成部分:
outputs = model.generate(inputs, do_sample=False, max_length=1024)
这个是设置解码方式和最大生成长度
inputs
: 输入到模型的 token 序列,通常是一个包含 token ID 的张量。do_sample=False
: 表示不进行采样,即模型将确定性地生成下一个 token。如果设置为True
,则模型将根据概率分布进行采样。max_length=1024
: 指定生成序列的最大长度。
output = tokenizer.decode(outputs[0])
这个就是解码,解析成我们能看懂的文字。
置于为什么是output[0],就是说,只有[0]这个位置返回的是人们可以看懂的东西。想深入了解的话可以看一些底层的东西。
然后我们来看一下这个AutoCausalLM这部分。
model = AutoModelForCausalLM.from_pretrained(path, torch_dtype=torch_dtype, trust_remote_code=True).cuda()
这个语句是调用模型的语句,第一个参数path是你模型的路径。后面的可以参考一下这个
【AI大模型】Transformers大模型库(五):AutoModel、Model Head及查看模型结构_transformers automodel-CSDN博客
streamlit
streamlit对很多零基础的人来说可能会比较陌生,这次我们给详细的讲一讲基础的一些功能。streamlit这个库可以理解为一个做网页ui的库。
做一个简单的ui,可以简单的想一下:我输入是什么方式,输出是什么方式
输入的话,根据常识我们应该想一想,有一个输入框,还有输入文本,我们要读取输入,然后执行相应程序,然后给一个漂亮的输出。
首先是输入部分,我们需要一个输入框
为了方便起见,我们选择这么引用:
import streamlit as st
插一嘴,我们最好是写一个标题
st.title('我的第一个 Streamlit 应用')
这个是输入框:
user_input = st.text_input("请输入您的名字")
然后就是有一个表示状态的对象session_state,这是一个字典。
然后最重要的是关于st.chat_message函数,这个可以看这个:Build a basic LLM chat app - Streamlit Docs
作为输出,就是st.write方法,参数就是字符串。
这些基础部分我带的比较简略,
构建出这个bsaeline代码
我们这个baseline代码,我们想一想我们现在,就,不要看这个源代码,我们用刚才提到的这两个库来构建出这个代码。
我们可以想一想像chatGPT这样的软件整体逻辑是什么样的(这里是我自己截图的chatGPT:)
我们可以看一下这个界面,我们只看右侧,有一个输入框,有一个现实对话内容的界面,对话内容左侧是ai,右侧是用户。我输入我的命令,对话界面的右侧会显示(重复)我们的问题(这一点很容易忽略)然后左侧就显示我们ai的解答。并且我们最好还是有一个题目。
所以我们先搭建起来这个界面:
import streamlit as st
st.title("💬 Yuan2.0 智能编程助手")
st.chat_input("请输入你的内容")
streamlit有一个最大的特点就是从session_state得到任何你想要的,并且在这个字典类型的变量里添加新的你想要的类型的数据,然后将你读到的值添加到你的新的类型里。
所以我可以读取我输入框的信息作为命令:
prompt = st.chat_input()
然后将我们读到的信息传入我们的session_state中,首先我们在这个字典里面定义一个新的key值:
st.session_state["messages"] = []
然后我们再去将我们的prompt给append到这里
st.session_state.messages.append({"role": "user", "content": prompt})
由于用户可能输入,也可能不输入,所以应该是if语句,这里我们应该用:=,表示即判断又赋值。
if prompt := st.chat_input():
st.session_state.messages.append({"role": "user", "content": prompt})
# 在聊天界面上显示用户的输入
st.chat_message("user").write(prompt)
所以我们以这个prompt作为纽带,来进入底层逻辑
首先我们先考虑这么一个事情,假如说我们写出了一个方法,或者说是一组代码可以调用大模型的,我们举个例子就叫f(),是不是就是说,把读取到的prompt做出一定处理后交给这个方法,然后输出这个表达的内容
if prompt := st.chat_input():
st.session_state.messages.append({"role": "user", "content": prompt})
# 在聊天界面上显示用户的输入
st.chat_message("user").write(prompt)
f(prompt)
# 将模型的输出添加到session_state中的messages列表中
st.session_state.messages.append({"role": "assistant", "content": response})
# 在聊天界面上显示模型的输出
st.chat_message("assistant").write(response)
给同学们解释一下,这个 st.session_state.messages.append({"role": "assistant", "content": response})就是说明给这个回答append到相应角色所对应的content里面,角色的概念给大家普及一下,就是说,在模型训练中,我们有两个角色,一个是用户(user)和ai(就是这个assistant助手)
剩下的问题就是这个调用代码该怎么写了,刚才我写的代码里可能有同学已经注意到了,这个f()的返回值应该是response
为了有上下文,我们每一次提问都应该添加对话历史,也就是这样:
prompt = "<n>".join(msg["content"] for msg in st.session_state.messages) + "<sep>"
这个<n>和<sep>是分隔符,用来分隔每一条对话
我的input应该解码成ai喜欢看的:
inputs = tokenizer(prompt, return_tensors="pt")["input_ids"].cuda()
然后我们输出output,后面的参数都是些设置,这个model对象是我前面生成的:
outputs = model.generate(inputs, do_sample=False, max_length=1024)
然后是解码,解码那个output翻译成人能看懂的
output = tokenizer.decode(outputs[0])
response = output.split("<sep>")[-1].replace("<eod>", '')
这个就是净化输出,我只取最后一个,并且将标记为<eod>(end of document)的给去掉。(记住这么做就行,感兴趣的话可以直接看一下输出)
我们最后来看一下大模型加载的部分:
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()
这个就是加载了模型和分词器,并且返回这个加载好的分词器和模型。
所以根据这个思路,我们这个代码就很快就构建出来了:
# 导入所需的库
from transformers import AutoTokenizer, AutoModelForCausalLM
import torch
import streamlit as st
# 创建一个标题和一个副标题
st.title("💬 Yuan2.0 智能编程助手")
# 源大模型下载
from modelscope import snapshot_download
model_dir = snapshot_download('IEITYuan/Yuan2-2B-Mars-hf', cache_dir='./')
# model_dir = snapshot_download('IEITYuan/Yuan2-2B-July-hf', cache_dir='./')
# 定义模型路径
path = './IEITYuan/Yuan2-2B-Mars-hf'
# path = './IEITYuan/Yuan2-2B-July-hf'
# 定义模型数据类型
torch_dtype = torch.bfloat16 # A10
# torch_dtype = torch.float16 # P100
# 定义一个函数,用于获取模型和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()
# 初次运行时,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"])
# 如果用户在聊天输入框中输入了内容,则执行以下操作
if prompt := st.chat_input():
# 将用户的输入添加到session_state中的messages列表中
st.session_state.messages.append({"role": "user", "content": prompt})
# 在聊天界面上显示用户的输入
st.chat_message("user").write(prompt)
# 调用模型
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)
其中
# 每次对话时,都需要遍历session_state中的所有消息,并显示在聊天界面上
for msg in st.session_state.messages:
st.chat_message(msg["role"]).write(msg["content"])
在我前面发的网站里面有详细的介绍,如果不想深入了解的话就记住,我们的对话历史在每一次交互中都会被重新以这种方式在页面上显示出来。
还有这个
@st.cache_resource
这是一个句柄,意思是说,由于每次交互时都会重新加载这个代码,那么既然这个函数是加载的,那么我们就加载一遍就行,这起到了一个缓存的作用,如果觉得理解的不够深的话可以看一下官方文档看一下底层Streamlit 进阶知识 - Streamlit 中文文档(更新中)
前面的定义路径和后面的一些简单的细节这里就省略了
好了今天的学习就到这里,祝大家学习愉快!