目标:原始使用ChatGLM-6B可接受的文字长度有限,打算结合LangChain实现长文本生成摘要.
方法:
step1:自定义一个GLM继承LangChain中的langchain.llms.base.LLM,load自己的模型.
step2:使用LangChain的mapreduce的方法,对文本分块,做摘要,输出结果.
使用的机器资源:T4显卡(16G显存)
附参考资料:
ChatGLM-6B:
ModelScope: ChatGLM-6B
LangChain:
LangChain: summarization
LangChain: summarize notebook
- glm环境准备
在指定的python环境下确定安装好以下依赖:
# 安装pytorch
conda install pytorch torchvision torchaudio pytorch-cuda=11.8 -c pytorch -c nvidia# 安装modelscope
pip install modelscope==1.4.3 -f https://modelscope.oss-cn-beijing.aliyuncs.com/releases/repo.html
# 安装transformers
pip install protobuf==3.20.0 transformers==4.26.1 icetk cpm_kernels
# 安装charset-normalizer
python -m pip install charset-normalizer==2.1.0
# 安装langchain
pip install langchain
模型文件下载到/data/THUDM/chatglm-6b/下
可以使用以下代码先下载到临时目录,然后mv到自定义目录下:
from modelscope.utils.constant import Tasks
from modelscope.pipelines import pipeline
pipe = pipeline(task=Tasks.chat, model='ZhipuAI/ChatGLM-6B', model_revision='v1.0.7')
- ChatGLM-6B + LangChain
2.1 继承langchain.llms.base.LLM新建GLM类
重写_call方法:加载自己的模型,并限制只输出结果(chatglm原输出不是直接str,langchain中要求模型返回必须是str的结果:“”“LLM wrapper should take in a prompt and return a string.”“”)
具体代码:
from langchain import LLMChain
from langchain.text_splitter import CharacterTextSplitter
from langchain.chains.mapreduce import MapReduceChain
from langchain.prompts import PromptTemplate
from langchain.llms.base import LLM
from transformers import AutoTokenizer, AutoModel, AutoConfig
from typing import Any, Dict, List, Mapping, Optional, Tuple, Union
from torch.mps import empty_cache
import torchclass GLM(LLM):max_token: int = 2048temperature: float = 0.8top_p = 0.9tokenizer: object = Nonemodel: object = Nonehistory_len: int = 1024def __init__(self):super().__init__()@propertydef _llm_type(self) -> str:return "GLM"def load_model(self, llm_device="gpu",model_name_or_path=None):model_config = AutoConfig.from_pretrained(model_name_or_path, trust_remote_code=True)self.tokenizer = AutoTokenizer.from_pretrained(model_name_or_path,trust_remote_code=True)self.model = AutoModel.from_pretrained(model_name_or_path, config=model_config, trust_remote_code=True).half().cuda()def _call(self,prompt:str,history:List[str] = [],stop: Optional[List[str]] = None):response, _ = self.model.chat(self.tokenizer,prompt,history=history[-self.history_len:] if self.history_len > 0 else [],max_length=self.max_token,temperature=self.temperature,top_p=self.top_p)return response
2.2 实例化llm对象&加载模型
import sysmodelpath = "/data/THUDM/chatglm-6b/"
sys.path.append(modelpath)
llm = GLM()
llm.load_model(model_name_or_path = modelpath)
2.3 配合langchain输出
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.docstore.document import Document
from langchain.chains.summarize import load_summarize_chainwith open("政府工作报告.txt") as f:report_2023 = f.read()text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
texts = text_splitter.split_text(report_2023)
docs = [Document(page_content=t) for t in texts]
prompt_template = """对下面的文字做精简的摘要:{text}"""PROMPT = PromptTemplate(template=prompt_template, input_variables=["text"])
chain = load_summarize_chain(llm, chain_type="map_reduce", return_intermediate_steps=True, map_prompt=PROMPT, combine_prompt=PROMPT)
summ = chain({"input_documents": docs}, return_only_outputs=True)
print(summ['output_text'])
其中政府工作报告.txt来自于2023年政府工作报告_中国政府网:https://www.gov.cn/zhuanti/2023lhzfgzbg/index.htm
2.4 输出
这篇文章介绍了中国在过去五年中的经济发展成就,政府采取多项措施应对有效需求不足的问题、支持汽车消费、推进保交楼稳民生工作、加强环境保护和生态修复工作等。政府还出台了增值税留抵退税额度、降低贷款利息等减轻企业负担的措施。文章介绍了中国政府的宏观经济政策目标,包括推进中国式现代化、实现经济发展质量和数量的提升、改善民生、稳定社会大局等。新冠疫情防控政策包括疫苗迭代升级和新药研制、保障群众就医用药需求、重点做好老年人、儿童、患基础性疾病群体的疫情防控和医疗救治等。政府将着力扩大国内需求、加快建设现代化产业体系、深化国资国企改革、保护民营企业产权和企业家权益,鼓励支持民营经济和民营企业发展壮大,稳定市场预期和提振市场信心。
实战2
框架地址:https://github.com/noobdawn/langchain_ChatGPT
langchain+ChatGLM-6B试用
什么是langchain
ChatGLM-6B大家都知道了,是清华大学推出的有62亿参数的开源大语言模型。那么langchain是什么?langchain是一个基于语言模型的应用程序开发框架,它具有以下特点
数据感知:将语言模型与其他数据源连接在一起
自主性:允许语言模型与其环境进行交互
langchain框架是基于以上原则设计的。
因为这些特点,langchain可以实现针对特定文件的问答、聊天机器人、评估、数据增强生成等工作。
如何部署
github地址为:基于本地知识的 ChatGLM 应用实现
部署指南为:安装
因为我换了新机器,我个人遇到了【build wheel for xxx时提示找不到cl.exe】的问题,解决方法是:
装个Visual Studio,然后在Visual Studio Installer里安装组件【MSVC v142 - VS 2019 C++ x64/x86生成工具】。
注意要选择这个版本,像我之前就装了MSVC v143结果build报错了。
安装完成重启之后如果还报这个错,就找到cl.exe所在的目录,把它添加到环境变量Path里,这个目录大致是这样的:D:\VS\2019\VC\Tools\MSVC\14.29.30133\bin\Hostx64\x64
如何使用
安装完成后,执行python webui.py即可开始使用,访问本地的127.0.0.1:7860即可使用web版的界面。
-
首先,进入模型配置,选一个LLM模型和Embedding模型。配置好的可以用chatglm-6b,差点的就要砍精度了。Embedding模型主要是文本转向量的模型,这个我研究不多。
-
然后来到知识库测试界面,先新建知识库,并为其上传文档或者文档所在文件夹,等待程序将文本上下文分割并解析为向量之后进行保存好后,测试问题。这一步主要是调整知识相关度阈值,默认Score=0会回复所有检索到的知识出处,这显然是不精准的。
-
确定好阈值后,在model_config里进行修改,这里还可以顺便修改一下prompt模板,重启程序以令其生效;
-
L 在对话中,配置好数据库之后提问即可。
缺陷
举例而言,我写了这样一个Q&A:
如何进行性能优化?
使用Unity的FrameDebugger查看每帧的DrawCall和使用到的模型。
但是喂给ChatGLM-6B后提供的回答会类似于:
如何进行性能优化?
1. 了解业务需求:明确优化的目标和范围
2. 设计测试用例:针对目标场景进行性能模拟,以确定系统的性能表现
云云。
通过这张图可以看到,这个框架的主体其实是跟LLM无关的,它只是比对和匹配文本向量的相似性,检出最相似的问句的上下文打包发给LLM进行润色。
在我的理解里,这个玩意儿不会真的把本地的数据更新进自己的模型内部,只是每次提问的时候,预先把可能存在答案的上下文作为prompt一起提交了而已,这里在model_config.py的PROMPT_TEMPLATE里也能看到。
所以它会触发以下问题:
1 当这个问题并不“独特”的时候,就会出现例子中所示的答案的杂糅。因为是打包上下文之后发给LLM润色,显然LLM理解的“性能优化”指的是一种普适性的答案,它自发的把自身语料训练出来的结果与本地知识库中项目所独有的结果进行混合,得到了似是而非的东西。
2 当知识库中的问题重复度很高的时候,或者问题过于宽泛导致命中过多,也会出现本地知识库答案的杂糅。此时问题和各个文本向量的相似性很高,回答会串味,例如,询问“如何进行性能优化”之后,就会连同“性能优化怎么查看”、“性能优化的指标是多少”等等一块返回进行回答。
3 因为是打包上下文发给LLM润色,所以这个“打包”可能会把答案截断,或者囊括了并非本问题的答案的上下文,会造成回答串味或不全。
简单的说,对于我来说我并不需要什么语料的润色、帮助我提取有用信息,因为我给的文档本身就是最好的答案,所以我需要的是精确的检索。这种截断上下文发给LLM的架构的三个缺陷是我无法忍受的:
- 本地知识库干预力度不够,由于真正需要的知识在上下文中,而上下文是以Prompt的形式嵌入其中的,导致独特性不够的问题,大模型给出的答案会非常偏向语料训练结果。
- ChatGLM-6B的中文能力过于羸弱,逻辑能力过于差劲,有时候无法判别出两个相似问题的区别。
- 上下文截断过于粗暴,对于长答案支持不佳。
langchain+ChatGPT
其他尝试
之后我转变思维,不再尝试让LLM模型去即时回答问题,而是让LLM即时判定问句是否一致,再针对同义问句匹配相同的回答。因此我个人在家又搭建了一个langchain+ChatGLM-6B的本地知识对话模型,但这个模型跟前一个模型的区别在于,我会写好一个问题答案对:
Q:你是谁?
A:我是弱智小助理。
当我提问“你是什么人”的时候,内部会使用LLM模型去一个个比对这句话和各个问题答案对中的问题部分是否属于同一个意思,此处它就会比对“你是谁”和“你是什么人”是不是同一个意思。Prompt会写成这个样子:
f"{original_question}\n{input_question}\n判断上述两句话是不是一个意思,如果是,则回答1;反之回答0。"
这里再次暴露了ChatGLM-6B的羸弱,明明要求回答0或者1即可,往往会画蛇添足地说“是的,这两句是一个意思”,此外它对同义句的判定也有极大的问题,这里我们后面说。
然后我又换了个方式,好吧,不要求你按格式回答了,你直接生成多几个同义句,我用文本向量按最近距离匹配好了。
我寻思Judge可能确实有点难为它了,那么generate应该没问题吧,结果还是让我大跌眼镜,对于“如何进行性能优化的流程”的问题,ChatGLM-6B给出的回复是:
事实上,我想看到的是这样的句子:
- 怎么发起性能优化流程?
- 性能优化的流程是怎么样的?
- 我该如何启动性能优化流程?
- 性能优化流程是如何发起的?
(你要不看看你在说什么?.jpg
因为我一直在用ChatGPT帮助生成代码,所以我测试了一下ChatGPT,发现ChatGPT生成的同义句居然还不错,虽然有时候会带上人称,比如”你如何才能启动性能优化“。
事实证明,尽管ChatGPT之后,LLM人人均有不下ChatGPT-3之勇,但真用起来,那还是ChatGPT好使。
如何使用OpenAI API
入口:Introduction - OpenAI API
每个ChatGPT Plus用户每月有5刀的免费使用额度,而根据使用的GPT模型不同,收费也不同:
模型 迅速版(每千token) 完整版(每千token)
GPT-4 8K Context 0.03$ 0.06$
GPT-4 32K Context 0.06$ 0.12$
GPT-3.5-Turbo 0.002$ -
选择使用GPT-3.5的原因绝对不是便宜(迫真),而是因为它速度快,且为对话专门优化过。而同义句的生成就也不涉及大量的知识和专业性内容,所以直接用它。
使用的方法就很简单:
p
rompt_base_templet = """请为下面这段文字生成至少5个的意思完全相同的中文问句,句子之间用回车分隔开:{question}"""
gpt_engine = "text-davinci-003"
max_tokens = 300
temperature = 0
# 获取同义问句
def get_synonymous_question(question : str) -> list:openai.api_key = api_keyprompt = prompt_base_templet + questionresponse = openai.Completion.create(engine=gpt_engine,prompt=prompt,max_tokens=max_tokens,temperature=temperature,n = output_num)generate_text = response.choices[0].text.strip()return generate_text.split('\n')
感觉一句话基本大约不会超过60token,所以5个中文问句大致就是300个token。temperature设为0是为了防止回答过于发散。
框架
框架整体参考了imClumsyPanda/langchain-ChatGLM 的实现,包括怎么用gradio创建webui之类的,但轻量化了很多,因为我并不需要内嵌LLM,也不需要对问句进行分词(都整句话直接转换成向量了)。
这个框架的运行结果是:
- 读取问题答案对
- 把问题整理出来发给OpenAI API生成同义句
- 把同义句转换为Document,把答案和原问句编制到metadata里
- 用embedding model将同义句转化为向量
- 用FAISS匹配最符合输入的问句
- 把结果中的metadata筛一次,合并同义句产生的答案
- 返回合并筛选之后的答案
值得注意的是,该方法只实现了一半的数据安全,其问题还是要提交到服务器上的。所以如果有涉密需求,还是得手动编写同义句字典进行搭建数据库。
改进
允许上传图片和链接
允许使用不经过OpenAI的同义句字典
允许下载本地知识和字典
一些个人的想法
故,本质上而言,该方案并不能算“对话”系统,因为LLM并没有在即时输入端参与,而是在本地知识上传后离线参与。
写完之后我在思索,这玩意儿和Ctrl-F有什么区别,有没有一种可能,我直接Ctrl+F搜索“性能优化”也能找到我要的内容呢?所以这是个伪命题?
后面想了想,如果知识库很少,问题单一的情况下确实是这样的没错。但随着知识库的增大,问题的keyword也在增多,单个keyword对应的答案内容开始急剧上升,假如后面有这些问题:
使用RenderDoc怎么指导性能优化?
怎么用Unity Frame Debugger优化性能?
如何优化冗余的资源实现性能提升?
……
这样的话,Ctrl+F的实用性就大打折扣了。此外,keyword也可能会改头换面,例如“性能优化”实际上可以这样问“优化XX的性能表现”,因此我认为这个方案仍有有较大的用武之地。
基于本地知识的问答机器人langchain-ChatGLM