我的学习笔记

土猛的员外

一款基于大模型产品的思考、推广和实践过程

本文导读:

  • 实际业务中应该如何使用大模型的思考
  • 记录一款大模型产品的0到1过程

在公司已经六年多了,六年多里收获很多成长,从纯研发leader开始,先后带过大数据产研团队、AI产研团队和运营团队,而且还作为独立销售和解决方案获得一些著名客户的订单,从一个coder变成了多面手。所以,在即将离别之际,不去讲其他纷争,只从产品角度去写两篇文章总结过去。上篇写今年之前三年的一些感想,主要记录力石小知的成长——《总结一下过去三年的产品心路》。这篇写的就是今年,在大语言模型(LLM)开始进入国内之后,我们如何思考它带来的影响,以及如何务实去落地一款产品。

本文有7000多字,有点长,Enjoy!


一、对大模型技术的积累

看过上篇的朋友应该知道,我们在2019年就已经开始了AI产品的研发,并且这几年有一定的市场品牌口碑和数据积累。也正因为有这样的“AI传统”,我们才有一定的人才储备(比如我们的小胡,B站ID:良睦路程序员,Github:yuanzhoulvpi2017),所以当今年大模型这波新的AI浪潮袭来的时候,我们算是入局比较早的:

  • 参与大模型社区:在今年2月份,我们团队发布了最早的ChatGLM-6B社区版LoRA版本代码——(见下图)获得智谱官方PPT的介绍,这个Github Repository就是我们团队的;
  • 发布商用大模型:3月份我们基于BLOOM-7B训练了自己的可商用大模型,是从CLM(因果模型训练)开始的全量训练,到后面的SFT(有监督精细微调),也因此获得某大厂和两家运营商的合作。当然后面因为要发证,我们知道自己无法在近两年内获得牌照,因此也就掉头重点转向大模型应用了;
  • 打造大模型应用:使用大模型改进上篇提到的产品——小知,原来小知基于BERT,现在变成BERT在前,GPT在后兜底的双模型系统。另外更重要的就是创造了一个基于大模型的新产品,这也是本文后面重要要讲的内容。
chatglm6b
图1:智谱官方直播PPT中分享的LoRA版本Github Repository

7b

图2:自训练的7B大模型

而且相对其他同行来说,我们还有一个独特的优势就是数据积累。

小知3年多服务中产生的数据量极大,且格式就是“问答指令集”,非常适合SFT(有监督精度微调)。有朋友可能知道之前Dolly2大模型发布的时候,他们用了仅仅1.5万条自己生成的问答指令集,就基于gpt-neox训练出了一个很有竞争力的模型。所以就文旅行业来说,我们的微调工作在百万级挑选出来的数据帮助下就相对有效。加上我们有一个很有活力的团队,于是如何将LLM用在产品中就自然而然成为了我们团队的新任务之一。

二、大模型的还有哪些缺点

这次大模型和以往的新技术的出现有所不同,大家一开始都是先从OpenAI的ChatGPT开始的,有点”开局即巅峰“的意思,以至于后面大家接触”车水马龙“会笑得人仰马翻。基于ChatGPT的普及,大模型的优点我就不赘述了,下面我们说说这位法学硕士(这是一个梗)的缺点。

1.大模型的主要缺点

Chat(问答系统)只是最基本的大模型应用,说白了就是OpenAI放出来的一个Demo。我们把大模型应用在企业业务中时,问题依然很多,主要包括:

  • 幻觉问题:大模型的底层原理是基于概率,所以它有时候会一本正经胡说八道,比如我们问大模型的Chat(问答系统),“良渚博物院下周一开门吗?”我相信这样的问题你不能连续问,因为大模型会有一定的几率告诉你开门(而实际情况是周一闭馆,除非碰上法定节日)。而如果游客真的在下周一去了良渚博物院,那估计就要失望了。如果这个Chat还是博物院官方提供的,那事情最终会演变成一通12345的投诉电话。所以在很多需要非常精准服务的场景,仅仅依赖GPT这种”盲目自信“的生成式回答是很不严谨的,而且看起来很难消除——目前我们常见的解决方案是前置一个BERT和语料维护,或者使用RAG(检索增强生成,目前正在成为主流)或者预置大量prompt做优化(有公司这么在做)。
  • 新鲜度问题:规模越大(参数越多、tokens越多),大模型训练的成本越高。类似OpenAI的ChatGPT3.5,目前的数据新鲜度依然保留在2021年,对于之后的事情就不知道了。而且对于一些高时效性的事情,大模型更加无能为力,比如帮我看看今天晚上有什么电影值得去看?这种任务是需要去淘票票、猫眼等网站先去获取最新电影信息的,大模型本身无法完成这个任务。现在主流的解决方案是增加RAG方案;
  • 数据安全:先抛开OpenAI已经遭到过几次隐私数据的投诉。就企业应用来说,如果把自己的经营数据、合同文件等机密文件和数据上传到云上的大模型,那想想都可怕。如果企业人员想提一个类似这样的问题:“帮我看看3月份XX部门的销售环比数据与哪些兄弟部门的增长是密切相关的?”,这需要打穿企业内部的很多数据。既要保证安全,又要借助AI能力,目前最好的方式就是把数据全部放在本地,企业数据的业务计算也全部在本地完成,然后在本地部署参数相对较小的大模型;
  • 费用问题:ChatGPT一开始是免费的,到后面开始API调用的收费,再后面出现的GPT-4的价格已经让很多人认识到AI是有成本的。这甚至造成了Langchain、AutoGPT等半自动和全自动的Agent框架立马遭遇冷落~~费用扣的太快了!!

2.国内大模型的问题

上面说的还是OpenAI的问题,再说回到国内,我们的大模型的能力和OpenAI还是有很大差距的,包括一些常用的推理能力。

除了能力上的差距,国内AI大模型及相关产业的从业者还面临着更多的问题:

  1. OpenAI等大模型的使用限制:即使你获得了GPT-4的API调用权限,依然会被突然封禁。而且国内对于公共应用中使用OpenAI和Claude等外部大模型的限制也是很严格的,所以自己玩玩可以,但真的要企业中对外服务,还是需要正视外部API的问题;
  2. 政策法规的严格把关:目前包括微信(小程序)等应用端,对于应用中大模型的介入是需要提供各类备案的,大部分公司无法做到(好像有点无解,但是别急~)。

3.我们的选择

所以我们对于大模型在产品中的应用是相对较为谨慎的,目前主要的大模型应用思路包括以下几点:

  1. 国产模型优先:绝不直接采用国外的大模型API服务,而是采用国内的产品,如智谱/百川/文心一言/通义千问等的API。或者就是分不同使用场景,采用自己本地部署的Baichuan2-13B/ChatGLM2-6B等国产大模型和Llama7B等开源模型。因为采用国外大模型虽然确实很爽,但是会形成整个技术栈依赖,弱化内部真正解决问题的能力;
  2. RAG优先:RAG(Retrieval Augmented Generation检索增强生成)做为大模型应用的一种优秀补充,借助向量embedding、相似度匹配等技术,可以将客户给定的PDF、Word和视频等内容快速形成知识对话能力。相对于微调(Fine-Tuning),RAG在知识新鲜度、幻觉和数据安全方面都更容易掌控,特别是幻觉问题,对于后续应用上线时候的审查是非常有效的。而且最关键的是RAG这种胶水组件的解决方案可以很大程度上减轻我们对大模型能力的依赖;
  3. 预生成优先:我相信现在国内大模型应用到后面最大的成本会是符合法规方面的对齐工作,内部审查也是很多公司的高月活产品最大的人力成本之一(听说B站的审核人员有4位数)。所以我们目前优先考虑的做法是预生成,通过大模型提升生产效率,把以前无法想象的生产成本极大压缩(后面介绍产品的时候会说到),所有呈现给最终用户的产品内容是透明可审查的,避免大模型在生产环境中突然”抽风“。

好了,说了这么多思考,接下来我们开始来说说今年做的这款新产品的0到1.

三、大模型产品启动

1.产品赛道选择

我现在所处的行业是数字农文旅。

农业农村是个大市场,也是国家这几年重点在发展的赛道,各种项目资金可以说非常充沛。但是农村也有一个大问题,就是它是一个大行业,却不是大市场,更多的客户是非常分散的,且不是知识密集型行业,所以对大语言模型并不友好。加上这两年接触下来,涉及的项目还是以形象工程为主,真正的产品力无法深入体现,个人感觉也没有能力真正把产品力做出来。所以放弃数字乡村赛道。

接下来是旅游。很多人可能看到疫情后旅游市场的爆棚,但在行业内,我们可以看到旅游消费依然乏力,City Walk、郊游(公园搭帐篷)等形式火热兴起。对于景区、文旅集团等,目前的日子依然不太好过…。我一个朋友在10月9日截了一个上市文旅公司的二级市场走势图可以说明一些问题。但是最可怕的是,除了一些稀缺级别的景区和一些新型服务形式的景区之外,景区这种靠山吃山靠水吃水的服务形式,正在失去游客…

综合考虑之后,这次我瞄准的是文博行业,更具体的说就是博物馆(院)和遗址公园一类的客户。就像上面说到的City Walk的火热,文博场馆就是大家主要的目的地或参观游玩点之一。

可以这么说,这次产品赛道的选择,我没有去看政策影响和甲方的资金是否充沛这些传统的toG/toB玩法,而是主打toC。这次的甲方更多是站在合作伙伴的位置上,我们是帮助他们一起来赚钱的,所以更看重的是人去了哪里。这个观念也可以参考我之前的一篇文章《进击吧!硬地骇客————独立开发者时代来临》里面提到的市场环境变化。

2.产品构思的产生

因为我是良渚遗址项目(其中旅游这部分)的商务+售前,所以和良渚遗址的领导比较熟。有一次就请教了监测中心的孙主任,他对于文物活化的看法。这一问不得了,确实颠覆了我很多认知,因为之前我对于活化的思考更多实在VR这些方面上,当然基本上都需要大投入大建设。主任对VR/AR也基本上是认可的,但是他说技术不能解决核心问题,他觉得核心问题就几句话:”文物活化,就是要让观众看得懂、喜欢看,看了之后能传播!“(具体意思应该就是这样的,可能用词上更我写的更准确一些)。当时有种被电流击中的感觉,真正的内容如此简单直白。

有了这句话的影响,我就去找各种资料加深这种感觉,其中对我帮助最大的是窦文涛的《锵锵行天下·第三季》,里面有一期讲《富春山居图》的,非常精彩,确实讲出了很多我之前自己看展品根本看不出来的道道。

texie

图3:对《富春山居图》的几个特写

特写一:黄公望把自己也已经画进了《富春山居图》中,也是对他自己在富春江畔九年的写生生活留下一个缩影;

特写二:水的画法和山是有直接关系的,有直接冲刷山体的,有掠过山体的,也有打了回漩到山坳里泛起粼粼波光的,每种笔触是对山水的真实写照;

特写三:通过清晰度和识别度造成远近和悠远,营造江面被一层薄雾笼罩的视觉,通过这种技法,让观察者心中感觉到烟云,既为“烟云供养”的技法;

特写四:披麻皴技法,古代山石画中非常精髓的技法,利用中锋手法,高处做大结构、气势磅礴,低处(离观察者近)构思精巧。

这么一解读,是不是对这幅浙江博物馆的镇馆之宝(浙博只有前半卷,也就是《剩山图》,后半卷在台北故宫)有了更多了解。

通过这两件事,我对新产品有了一些初步构思,就是文化文物不是仅仅靠听就能听懂的,文化文物是需要视听同步才能做到“看得懂、喜欢看,看了之后有谈资”的。于是我觉得这里面是有一个空白市场,而且也是一个风险相对较小的产品——导游讲解机已经有十多年历史了,我们去升级迭代这个产品,去拿这一块市场。

3.产品调研无需太多

我把调研分成两拨,第一拨调研是让我团队的一位我认为比较聪慧的同事(倩文)去的,她走了五个馆,写了大概万把字的调研报告,给了我第一手的信息。也是已通过这些资料,我坚定了做这个产品的决心,这里肯定是有市场空白的。

第二拨调研是我自己去的,而且还邀请了另外两个同事,一位是我们分管副总裁王总,另外一位是我们的产品总监欧阳。这一拨调研我有重点地将目标放在了我自己的原有客户身上。两个客户分别是宁波天一阁博物院和杭州的良渚古城(和博物院)。调研发现其实市面上已经有AR眼镜的产品,外形看上去其实还是挺酷的,但问题也很明显,主要是三点:

  • AR眼镜夺走了观众的第一视野,虽然我们带着眼镜,但是看的却是眼镜里面的视频,而不是眼前的文物。而且为了画面清晰,观看时还需要把里面的墨镜盖下来,根本看不见前面的路,造成游客行走困难;
  • AR眼镜里面的视频内容更新很不方便,价格贵、制作也非常困难,所以我们调研的其中一个博物馆的馆方人员和我们说,今年已经是合作第15年了,期间却只改了三段视频,因为制作成本有难度,而且成本太高了。
  • 没有什么交互,就是到一个点位,然后视频弹出来,说实话和在家里看视频没有太大区别。

对于传统的导游机,它的问题还是无法用画面来展示文物背后的故事。

于是,我觉得也不需要再调研了,因为我已经可以给新产品做定义了。

4.产品定义

我给新产品定的内部名称是”可交互AI讲解“。

下面是给这个产品做的几个定义:

  1. 降低新产品风险:讲解器市场存在已经十多年,本身就证明了这个市场存在,我们要做的是迭代这些原有的产品,分割一部分市场,产品风险较小,但是市场够大;
  2. 体验上要优于现有应用:即使现在有手机扫码讲解这些新功能,但是我们会使用iPad作为主要载体,在显示效果、屏幕大小和实用性方面都远超手机,高达5GB的离线资源预安装在iPad里,无需游客自己手机下载,不用担心耗流量和耗电;
  3. 视频是最佳展现形式:讲解语音和视频画面同步,可以讲解深度内容,比如上面说的富春山居图,只有画面同步才能完美展现出来;
  4. 交互带来不一样的体验:可交互,观众在使用期间,可以随时进行交互,比如只要说”小知小知“,就可以用语音提问:”黄公望在来富春江之前是在哪里啊?“,交互采用的就是基于大模型的RAG技术;
  5. 内容快速预生成:这也是支撑整个产品逻辑的底层核心。我希望是,内容运营人员挑选一本关于目标文物的电子书并上传,系统就可以自动将电子书内容做解读,分离图文,提取出很多小故事,然后自动生成一段一段的讲解视频。这才能满足100x的内容制作效率,才能时常更新内容,也能满足快速扩张的需要;
  6. 合作大于项目:前面我也说了,这个产品我希望是toC的,所以我和客户的关系其实是平等的,我把产品放在你这边,让游客/观众付费使用,然后我们一起分润。这种方式比我们之前项目制打法要快很多,而且赚的是现金生意。

基于这6点,我已经可以规划处一个产品了:

前端

一个iPad作为主要载体,搭配耳机,播放的是关于当前文物的讲解视频。

pad

图4:早期的Pad设计稿
后端

后端的核心是使用AI进行自动化内容生产,但是我们不会真的去追求一步到位,而是先使用半自动化的方式。前期会让内容编辑人员参与内容建设,然后记录他们的生产过程,结合上线之后的用户喜好数据,逐步进行全自动内容生产流程。

后端的简易流程图如下:

gen

图5:后端利用AI技术进行视频内容自动生成

Step1:内容运营人员挑选有趣的内容,一般是电子书、论文和网络内容,当然需要经过馆方审核。然后将内容制作成PDF、Markdown等格式,上传到系统;

Step2:系统的调度程序(TaskScheduler)开始运行,将文档进行内容提取;

Step3:图片的提取相对复杂,首先单独存储图片,如果有图示文字的,存入图片相关的描述文本;如果没有图示,那么将图片上下文存入图片描述文本;或者还有一种就是文本中有下标(比如[3]),图片在章节末尾的,那就存在下标的上下文到图片的描述文本。图片的描述文本进行embedding处理。描述文本和图片直接的实体关系通过RDS进行对应(多对多关系);

Step4:提存和存储文字,会先使用大模型进行润色,在Prompt里面设置较多角色、指令和带有行业knowhow的输出格式模板。润色之后的文字进行embedding处理。

Step5:中间略去一些处理细节,然后再以讲解文本为基础进行时间轴创建。讲解文本的chunk_size基本上是以句子为单元的,将讲解文本与图片描述文本进行语义匹配(相似度算法),自动组合视频时间轴,生成一段”伪视频“。为什么是”伪视频“,因为这时候其实是一个mp3播放器和一个带有转场特效的图片播放器的叠加。这个期间,运营人员可以对内容进行最后的调整;

Step6:合成最终视频的过程其实非常简单,其实就是录屏。

houtai

图6:v0.5版本的后台部分内容展示,从文本生成故事,再自动生成有时间轴的视频工作区

说实在的,这可能只描述了整个过程的50%流程,后面不断优化之后,整个流程不断增加新功能,整个生成过程也在不断完善。里面用到的技术也非常繁多,包括:

  • ASR
  • TTS
  • Embedding
  • 向量数据库
  • 大语言模型(GPT)
  • NLP、BERT
使用场景

其实定义使用场景这个工作是在我们第二拨调研之前做好的,这次调研我们面向是业内专业人士和馆方领导,需要有PPT做演示,要让对方快速理解使用场景,才能获得市场成功。如果你看过我上一篇文章《总结一下过去三年的产品心路》的话,是不是”产品-市场对角线“的感觉又出来了。

你知道,仅仅靠语言描述是很难把一个产品讲清楚的,即使你会画一些简单的线框图,对于其他行业的人来说也不好理解。所以有必要用相对真实的画先描述一下使用场景。我们这个时代最大的好处就是有各种AI工具可以帮助我们完成自己不擅长的事情,比如我不是画家,也不是摄影师,但是我依然可以用Midjourney画出脑海中的使用场景,于是,就有了下面这两张图:

changjing

图6:使用Midjourney制作的产品示意图和使用场景示意图。右图中姑娘的第一视线,我还是给了文物而不是Pad

四、产品研发过程

这个过程我肯定不太像说的太多,因为细节太过于繁琐,即使讲出来可能也索然无味,加上我也确实不能说太多,但有两点我觉得是可以分享的。

1.组织保障

读过之前《三年产品心路》的朋友应该知道我之前带队做小知的时候,开始的半年是靠实力生撑过来的,人员抓肩见肘。但这次不一样,老板把AI产品线定义为公司最重要的核心竞争力,加上VP王总加入之后,确实为这个产品注入了相对丰富的产研资源。

产品由公司P序列最高的产品经理欧阳(P9)负责,研发方面也是P8、P9的同事领衔,比如我经常提到的著名开源作者小明,还有公司最好的前端六木。当然AI算法和大模型方面参与的还有最开始提到的”良睦路程序员“小胡。付萍带的内容运营团队也全数参加,前面提到第一次调研的倩文负责整个产品研发进度的把控。

整个产品团队最多有16人,常规也有9人,对我来说,这次打的就是富裕仗。所以欧阳经常感叹的一句话是,两个月前还只是一份PPT啊,现在就已经出来v0.8版本了。总结下来,组织力量是非常重要的,没有组织能力的保障,很多产品出不来:市场+产品+研发+AI+运营+项目管理,这样的团队配置我相信在我们行业内是不太有的,所以我对于分享产品过程也没有太刻意的保留。

2.不断迭代

产品-市场对角线“里面很重要的一点就是持续收集需求,然后迭代升级。

pmline
图7:”产品-市场对角线“,我自己对打造一款产品过程的总结

这次我们拜访了多家潜在客户,收集了大量需求,最终选择在宁波天一阁博物院首先落地。原因可能是他们刚好有一个展会的契机,而且黄主任是我觉得为数不多的既懂技术(包括大模型)又懂文博专业知识的专家。我们和天一阁联名参办宁波数字经济展会是一个非常好的机会,因为如果只是和黄主任聊聊天,我觉得很多建议不会太深刻,因为和他自身没有太大利益关系。而共同去办展,他在很多时候比我还重视这件事情,所以获得的需求是真切的。

图8:与天一阁联名参展,也是我们产品的第一次对外发布

五、后记

可交互AI讲解(后续可能会改名为”文博佳“)现在已经处于落地阶段,要处理的细节还有很多,11月份会在天一阁博物院率先落地。后面的几家意向博物馆也基本上做了联系,获得口头认可。

这算是又一个从Idea到产品的过程,不过这次理论推导更充分,也算是我在公司创造的第四个产品。这次的历程,最重要的是使用大模型对于实际业务结合的探索,虽然这次大模型在整个产品的技术组成中占比没过30%,但也是一个有意义的尝试。我们做产品最重要的不是炫技,而是要最终呈现用户价值,大模型、向量等在中间起到的作用是解决了内容生产效率的问题,这样我们才有胆量在前端(用户端)做到高要求,不然产品的持续力不够,内容断档,也最终只是昙花一现。

我目前更聚焦的是RAG方面的研发,大家都大模型、RAG感兴趣的话,可以关注我!


TorchV AI支持试用!

如您有大模型应用方面的企业需求,欢迎咨询!



新开源大模型zephyr介绍,据说对RAG比较友好

前面刚测试了Baichuan2-13B的本地化模型能力,对于我来说,归纳能力已经足够了。但是一旦深入RAG的应用,Baichuan2还是有一些弱势的,比如在表格的处理能力上,显得力不从心。让我们比较失望的是,目前国内一众模型里面,对这一块的处理能力都够呛,唯一可行的还是OpenAI的ChatGPT。

于是这两天也在空隙里面找可以本地化部署,对于RAG又比较友好的LLM,发现还真的有几个新的LLM出来了。

首先看到的是Mistral-7B,据说性能超过了Llama-13B。正当我准备好好研究一下的时候,又看到Wenqi Glantz老师在推Zephyr-7b-alpha,而且推荐理由里面有两点正是我想要的:

  • 对RAG比较友好;
  • 性能比Mistral-7B更强。

并且她还做了对比。

Zephyr介绍

下面我们来看看Zephyr-7b-alpha的介绍,以及它的性能如何!

Zephyr其实是一个大语言模型的系列名称,Zephyr-7B-alpha是该系列的第一款型号,它是基于Mistral-7B-v0.1微调而来的,使用直接偏好优化(DPO)对公开可用的合成数据集进行混合训练。我们发现,删除这些数据集的内置对齐提高了MT Bench上的性能,可以让模型更加出类拔萃。当然,这个模型因为是MIT的协议,所以是可以免费商用的,和其他很多可商用模型一样,也是申明仅用于教育和研究目的,意思就是在商用上出什么幺蛾子我不管的。

我在它的官方提供的测试地址上做了测试,首先它是支持中文的(答案当然是不准的):

cn

然后呢,它对于表格类型的问答,处理能力应该说比较弱吧,甚至有点不听指令(机器觉醒?)

nandian

Wenqi Glantz使用了EDD(评估驱动开发)的视角来探索zephyr-7b-alpha,使用LlamaIndex构建的多文档RAG管道,并与OpenAI模型GPT-3.5进行了比较。

zephyr-7b-alpha是一种7b参数类似gpt的模型,从Mistralai/Mistral-7B-v0.1微调而来。根据HuggingFace MT Bench,一个评估机器翻译(MT)系统质量的基准套件,zephyr-7b-alpha优于Llama-2-70b-chat-hfMistral-7B-Instruct-v0.1,请看下面的比较表。

img

Huggingface发布的MT Bench评测结果

LlamaIndex一直对各种LLM做兼容性跟踪评测,执行了一个LLM兼容性跟踪,从中我们了解到zephyr-7b-alpha是迄今为止唯一一个在高级RAG任务上表现良好的开源7b模型

img

zephyr大模型在llamaindex评测中,能力表现非常全面

从上面的截图中可以看出,尽管zephyr-7b-alpha在大多数类别中都优于其他两个7b模型,但作为数据agent,它仍然有其局限性。让我们在下一节中继续进行评估POC时记下它。

BAAI/bge-base-en-v1.5

BAAI/big-base-en-v1.5是由北京人工智能研究院(BAAI)开发的文本Embedding模型。它是一个在大量文本和代码数据集上训练的大型语言模型。它可以为各种NLP任务生成文本Embeddings,例如检索、分类、聚类和语义搜索。它是HuggingFace的MTEB(海量文本Embedding基准)排行榜上目前排名第二的Embedding模型,仅次于其更强大的兄弟big-large-en-v1.5

我们将使用BAAI/big-base-en-v1.5作为评估POC的Embedding模型。

POC评估

我们用两种实现策略评估了一个多文档RAG管道的能力:

  • 递归检索器+文档代理
  • 元数据替换+节点句子窗口

正如上面关于zephyr-7b-alpha的部分所指出的,它对数据代理有限制。在本文中,我们将忽略递归检索器+文档代理的实现策略,而将重点放在相同的多文档RAG管道的元数据替换+节点句子窗口的策略上。我们将比较以下两种实现:

  • gpt-3.5-turbo + BAAI/bge-base-en-v1.5
  • zephyr-7b-alpha + BAAI/bge-base-en-v1.5

gpt-3.5-turbo + BAAI/bge-base-en-v1.5

现在,让我们使用Embedding模型——BAAI/big-base-en-v1.5 ,请参阅下面的代码片段。注意,在构造ServiceContext时,embed_model现在指向local:BAAI/big-base-en-v1.5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#define LLM and embedding model
llm = OpenAI(model="gpt-3.5-turbo", temperature=0.1)
ctx = ServiceContext.from_defaults(
llm=llm,
embed_model="local:BAAI/bge-base-en-v1.5"
)

from llama_index import VectorStoreIndex

# extract nodes and build index
document_list = SimpleDirectoryReader("data").load_data()
nodes = node_parser.get_nodes_from_documents(document_list)
sentence_index = VectorStoreIndex(nodes, service_context=ctx)

from llama_index.indices.postprocessor import MetadataReplacementPostProcessor

# define query engine
metadata_query_engine = sentence_index.as_query_engine(
similarity_top_k=2,
# the target key defaults to `window` to match the node_parser's default
node_postprocessors=[
MetadataReplacementPostProcessor(target_metadata_key="window")
],
)

这将触发将local:BAAI/big-base-en-v1.5下载到我们在Colab的环境中(你也可以用本地环境)。

img

zephyr-7b-alpha + BAAI/bge-base-en-v1.5

现在,让我们试试zephyr-7b-alpha + BAAI/big-base-en-v1.5的组合。请参阅下面的代码片段。注意,在构建ServiceContext时,我将embed_model更改为local:BAAI/big-base-en-v1.5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import torch
from transformers import BitsAndBytesConfig
from llama_index.prompts import PromptTemplate
from llama_index.llms import HuggingFaceLLM

quantization_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_compute_dtype=torch.float16,
bnb_4bit_quant_type="nf4",
bnb_4bit_use_double_quant=True,
)

def messages_to_prompt(messages):
prompt = ""
for message in messages:
if message.role == 'system':
prompt += f"<|system|>\n{message.content}</s>\n"
elif message.role == 'user':
prompt += f"<|user|>\n{message.content}</s>\n"
elif message.role == 'assistant':
prompt += f"<|assistant|>\n{message.content}</s>\n"

# ensure we start with a system prompt, insert blank if needed
if not prompt.startswith("<|system|>\n"):
prompt = "<|system|>\n</s>\n" + prompt

# add final assistant prompt
prompt = prompt + "<|assistant|>\n"

return prompt

# define LLM
llm_zephyr = HuggingFaceLLM(
model_name="HuggingFaceH4/zephyr-7b-alpha",
tokenizer_name="HuggingFaceH4/zephyr-7b-alpha",
query_wrapper_prompt=PromptTemplate("<|system|>\n</s>\n<|user|>\n{query_str}</s>\n<|assistant|>\n"),
context_window=3900,
max_new_tokens=256,
model_kwargs={"quantization_config": quantization_config},
# tokenizer_kwargs={},
generate_kwargs={"temperature": 0.7, "top_k": 50, "top_p": 0.95},
messages_to_prompt=messages_to_prompt,
device_map="auto",
)

from llama_index import ServiceContext

# constructing ServiceContext by defining both LLM and embedding model
service_context_zephyr = ServiceContext.from_defaults(
llm=llm_zephyr,
embed_model="local:BAAI/bge-base-en-v1.5"
)

from llama_index import VectorStoreIndex

# extract nodes and build index
document_list = SimpleDirectoryReader("data").load_data()
nodes = node_parser.get_nodes_from_documents(document_list)
sentence_index_zephyr = VectorStoreIndex(nodes, service_context=service_context_zephyr)


from llama_index.indices.postprocessor import MetadataReplacementPostProcessor

# define query engine
metadata_query_engine_zephyr = sentence_index_zephyr.as_query_engine(
similarity_top_k=2,
# the target key defaults to `window` to match the node_parser's default
node_postprocessors=[
MetadataReplacementPostProcessor(target_metadata_key="window")
],
)

如果您在免费版Colab笔记本中运行上述代码,则在下载zephyr-7b-alpha 时可能会遇到错误。我不得不支付9.99美元的月费升级到专业版,并将硬件加速器的运行时类型更改为T4 GPU,并打开了High-RAM。这个小小的投资是非常值得的。

img

通过硬件调整,我们现在可以成功下载zephyr-7b-alpha

img

评测

让我们使用LlamaIndex的响应评估模块添加剩余的评估代码。

综上所述,使用LlamaIndex的响应评估模块的高级步骤包括:

  1. 使用DatasetGenerator自动生成评估问题。
  2. 定义faithfulnessrelevancy的评估者。
  3. 使用BatchEvalRunner 来异步运行响应的评估。
  4. 比较评价结果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from llama_index.evaluation import FaithfulnessEvaluator, RelevancyEvaluator

# use gpt-4 to evaluate
gpt4_service_context = ServiceContext.from_defaults(llm=OpenAI(temperature=0.1, llm="gpt-4"))

# define evaluators
faithfulness_gpt4 = FaithfulnessEvaluator(service_context=gpt4_service_context)
relevancy_gpt4 = RelevancyEvaluator(service_context=gpt4_service_context)

from llama_index.evaluation import BatchEvalRunner

# define evaluation batch runner
runner = BatchEvalRunner(
{"faithfulness": faithfulness_gpt4, "relevancy": relevancy_gpt4},
workers=10,
show_progress=True
)

让我们看看gpt-3.5-turbozephyr-7b-alpha的结果,两个测试的Embedding模型都是local:BAAI/big-base-en-v1.5

img

评价

让我们收集这两个评估的结果并把它们放在一起(忽略all-mpent-base-v2)。

img

我们将faithfulnessrelevancy的数字映射到一个图表中,得到以下结果。

img

我们可以看到:

  • gpt-3.5-turbo + BAAI/big-base-en-v1.5相比,zephyr-7b-alpha + BAAI/big-base-en-v1.5的得分相同,faithfulness为0.93比0.93,relevancy为0.97比0.9。
  • 评估执行时间在两个选项之间差异很大,zephyr-7b-alpha + BAAI/big-base-en-v1.5需要4分钟才能完成,而其他选项大约需要10秒。哎呀!这当然不理想。

zephyr-7b-alpha在评估的faithfulnessrelevancy类别中确实看起来很有希望。然而,值得一提的是,HuggingFace:)对其局限性提出了几个关键点

Zephyr-7B-α没有通过RLHF等技术与人类偏好保持一致,也没有使用ChatGPT等循环过滤响应,因此模型可能产生有问题的输出(特别是在提示这样做的时候)。它也不知道语料库的大小和组成是用来训练基础模型的(Mistral-7B-v0.1),但是它很可能包括了网络数据和技术来源如书籍和代码的混合。

同样重要的是要记住,从评估结果中得出的结论只适用于我们处理的特定用例。zephyr-7b-alpha + BAAI/big-base-en-v1.5在我们的POC RAG管道中略优于GPT-3.5,但这并不意味着它在您的用例中会优于GPT-3.5。本文旨在传达评估驱动开发的重要性,甚至在选择大语言模型和Embedding模型时也是如此。使用EDD作为瑞士军刀来剖析和评估您的用例,以找到最合适的工具和实现策略,使您的RAG管道成功。

总结

在本文中,我们通过EDD(评估驱动开发)的视角探索了zephyr-7b-alpha,使用rag相关技术:gpx-3.5-turbo + BAAI/big-base-en-v1.5zephyr-7b-alpha + BAAI/big-base-en-v1.5。我们使用LlamaIndex的响应评估模块进行了评估,并比较了所有三种情况的结果。

我们可以得出结论,对于我们的POC RAG管道,zephyr-7b-alpha + BAAI/big-base-en-v1.5的组合略微优于OpenAI的ggt -3.5 + BAAI/big-base-en-v1.5。这对开源社区来说确实是一个鼓舞人心的消息!

引用


TorchV AI支持试用!

如您有大模型应用方面的企业需求,欢迎咨询!



RAG应用之LlamaIndex(1)——介绍各种索引及优缺点

最近一直和小明在搞RAG,我们认为这对于大部分公司和个人来说,就是大模型的未来,有应用才能更好推动大模型发展,至少近三年内应该是。前面也输出了一些RAG方面的文章,也有了一套精确度相对较高的技术产品。但是你知道的,RAG领域也在不断发展,而且我们确实也还面临着一些实际的问题,所以最近开始细细研究LlamaIndex——我觉得适用于RAG领域的话,可能会比LangChain更优秀。今天我们先来看看LlamaIndex的索引。

什么是LlamaIndex

这是我自己列的一个结构图,当然是按我自己的理解画的,您只做参考吧,哈哈。

1-all

LlamaIndex是一个大型语言模型(大语言模型)的数据框架,提供以下工具:

  • 数据摄取: LlamaIndex提供数据连接器来摄取您现有的数据源和数据格式(api, pdf,文档,SQL等),以便您可以与大语言模型一起使用它们。
  • 数据构建: LlamaIndex提供了构建数据(索引,图表)的方法,以便可以轻松地与大语言模型一起使用。
  • 检索和查询接口: LlamaIndex为您的数据提供了高级检索/查询接口。您可以输入任何LLM输入prompt,LlamaIndex将返回检索到的上下文和知识增强的输出。
  • 与其他框架集成: LlamaIndex允许轻松集成与您的外部应用程序框架(例如与LangChain, Flask, Docker, ChatGPT,或其他任何)。

LlamaIndex是为初学者和高级用户设计的。高级API允许初学者使用LlamaIndex在5行代码中摄取和查询他们的数据。低级api允许高级用户自定义和扩展任何模块(数据连接器、索引、检索器、查询引擎、重新排序模块),以满足他们的需求。

以下是使用LlamaIndex的一些好处:

  • 易于使用: LlamaIndex有一个简单直观的API,使其易于入门。
  • 灵活: LlamaIndex可用于摄取和查询各种数据源和格式。
  • 功能强大: LlamaIndex提供了广泛的功能,帮助您构建强大的LLM应用程序。
  • 可扩展: LlamaIndex是开源和可扩展的,因此您可以自定义它以适应您的特定需求。

什么是index

LlamaIndex中的索引是一种数据结构,它允许您从大量文本语料库中快速搜索和检索数据。它的工作原理是在语料库中的关键字或短语与包含这些关键字或短语的文档之间创建映射。这种映射允许您快速找到包含特定关键字或短语的所有文档,即使语料库非常大。

索引对于各种任务都很有用,例如:

  • 搜索信息:您可以使用索引快速找到包含特定关键字或短语的所有文档。这对于研究、客户支持或任何其他需要在大量文本语料库中查找信息的任务都很有用。
  • 排序文档:您可以使用索引根据文档与特定查询的相关性对文档进行排序。这对于搜索引擎、推荐系统或任何其他需要对文档进行排序的应用程序都很有用。
  • 分析文本:您可以使用索引来分析大量文本的内容。这对于理解文档的情感、识别主题或提取关键字非常有用。

LlamaIndex提供了各种不同的索引类型,每种类型都有自己的优缺点。特定任务的最佳索引类型将取决于任务的特定需求。

Llamaindex提供的结构或组织数据的索引类型:

让我们先了解一些术语。根据Llamaindex的官方文件:

  • Node:对应于文档中的一大块文本。LlamaIndex接受Document对象,并在内部将它们解析/块化为Node对象。
  • Response Synthesis:你可以认为是一个放置你选择Node的购物车,它会完成合成,然后处理后续的响应。

为了便于理解,我们可以将节点视为大约1k个单词的文本块,而响应合成则是通过查看相关节点(文本块)来给出问题答案(响应)的LLM。这里大家也可以回看我之前的文章《最详细的文本分块(Chunking)方法,直接影响LLM应用效果》。

Llamaindex提供不同类型的索引,以便更好地组织和检索相关信息,并使用Embeddings和LLM高效地运行查询。我们将逐一查看主要Index:

List Index :

List Index是一个简单的数据结构,它将文档存储为节点序列。在索引构建期间,文档文本被分块、转换为节点并存储在一个列表中。这使得它成为检索包含特定关键字或短语的文档的非常有效的索引。

img

使用列表索引的优点

  • 效率:列表索引对于检索包含特定关键字或短语的文档非常有效,这是因为它只需要扫描节点列表来查找与查询匹配的文档。
  • 简单:列表索引是非常简单的实现和使用,对于性能很重要,但开发时间有限的项目来说,这是一个很好的选择。
  • 灵活性:列表索引可用于任何类型的文档数据,这使得它成为具有各种文档格式的项目的一个很好的选择。

在查询的过程中发生了什么

在查询期间,如果没有指定其他查询参数,LlamaIndex只是将列表中的所有节点加载到响应合成模块中。

img

当用户输入关键字或短语时,查询列表索引。然后,索引扫描节点列表以查找包含关键字或短语的文档。然后将匹配查询的文档返回给响应合成模块:

img

使用List Index的缺点或问题:

  • 可伸缩性:对于大型数据集,List Index可能会变得低效,这是因为它需要扫描整个节点列表来查找与查询匹配的文档。

向量存储索引

向量索引是一种将文档文本转化为向量的索引,向量通常是由编码器transformer模型(如Bert等)生成的,它们也被称为Embeddings模型(参考前面关于embedding的文章《Embedding——从入门到生产使用》)。向量表示文本的含义,可以根据用户的查询来查找相关文档。

img

使用LlamaIndex来创建向量Index是非常简单,可以从文档中直接提取文本建立索引,可以看下面这段代码:

1
2
3
4
5
6
7
8
from llama_index import Document, VectorStoreIndex

text_list = [text1, text2, ...]
documents = [Document(text=t) for t in text_list]

# build index
index = VectorStoreIndex.from_documents(documents)

或者我们也可以从先生成Node,才建立索引,我推荐这种方式,因为后面更好管理:

1
2
3
4
5
6
7
8
9
10
11
12
from llama_index import Document, VectorStoreIndex
from llama_index.node_parser import SimpleNodeParser

text_list = [text1, text2, ...]
documents = [Document(text=t) for t in text_list]

# parse nodes
parser = SimpleNodeParser.from_defaults()
nodes = parser.get_nodes_from_documents(documents)

# build index
index = VectorStoreIndex(nodes)

使用向量索引的优点:

  • 向量索引对于查找类似文档非常有效。这是因为向量可以直接进行比较,而无需进行任何文本处理。
  • 向量索引扩展相对容易,它们可以用于索引包含数百万甚至数十亿个文档的大型数据集。
  • 向量索引创建相对容易,拥有Embedding模型(中文常用的包括m3e、bge等)之后,就可以为文档生成向量,并将它们存储在向量索引中。

我们看看向量索引在查询的时候发生了什么:

  • 当您查询向量索引时,您提供一个查询字符串。查询字符串首先由相同的Embedding模型处理以生成向量。
  • 然后将向量与索引中所有文档的向量进行匹配,具有最相似(可以设置top_k)向量的文档将作为查询的结果返回。
  • 我们还可以定义返回top-k最相似的节点,并将它们传递到我们的响应合成模块。

img

总的来说,向量索引与传统查询的索引相比,更具备语义属性,可以让我们根据语义来检索,而不仅仅是按关键词匹配等字面意思来搜索。而且向量索引使用高效、可扩展能力强,且易于创建。当然了,它也有不好的一方面,就是如果您使用的不是本地embedding模型,而是使用OpenAI的text-embedding-ada-003、通义千问的embedding模型等,那么是需要成本的,而且量大的话可能很昂贵,即使使用免费的BGE、m3e等本地模型,也是需要一些硬件资源的。对了,说一点开心的,本地的embedding模型的话,我们自己是可以微调训练的,可以参考我之前的文章《手工微调embedding模型》。

TREe Index(树索引)

Tree Index也是一种索引类型,它将文档的文本存储在树状结构中。树中的每个节点表示其子文档的摘要。我相信看这篇文章的朋友应该多少都接触过树的数据结构,通过遍历树并查找与查询相关的节点,树索引可用于查找与给定查询相关的文档。

为什么用树?我们通过和数组、链表的比较,使用大学知识简单复习一下树的优点:

  • 数组:支持随机访问,进过排序的数组在查找的时候效率极高——O(1),但是数组的insert、delete等操作需要变动后续的所有元素,复杂度就会变大——O(n);

  • 链表:和数组相反,链表的插入删除等效率极高,只需要将前后连接点打断,插入,再接好,所以时间复杂度一般是O(1)。但是它的查询速度就慢了,需要全部遍历才能找到相应元素,一般时间复杂度是O(n)。当然,链接有很多变种,比如散列链表的查询复杂度也是可以做到O(1)的;

  • :树结构算是一个折中吧,特别是二叉树,它的查询和操作(插入、删除等)的复杂度都是O(logn)。二分法的威力就是n越大,它的上升曲线会越来越平缓。比如一组有序数字的猜大小的游戏中,8个数字(n=8),只要猜3次;32个数字猜5次,但是1024个数字也只需要10次。且很关键的一点,二叉树的效率非常稳定,这一点在开发中对于性能的可预估性非常重要。

img

使用树索引的优点:

  • 树索引对于查找相关文档非常有效,这是因为树结构允许索引快速地将搜索范围缩小到可能与查询相关的少量文档。
  • 树索引也非常可扩展,它们可以用于索引包含数百万甚至数十亿个文档的大型数据集,不管数据量大小,性能相对稳定。
  • 树索引相对容易创建,一旦有了文档,就可以通过手动或自动将文档分组到相关的集群中来创建树形结构。

在查询过程中发生了什么?

  • 查询树索引时,需要提供查询字符串。首先处理查询字符串以识别与查询相关的关键字。
  • 然后索引遍历树,从根节点开始(也可以是其他地方开始)。在每个节点上,索引检查查询中的关键字是否出现在节点的摘要中。如果是,则索引下降到节点的子节点。如果不是,则索引移动到下一个节点。
  • 这个过程一直持续到索引到达一个叶子节点,或者直到它耗尽树中的所有节点。到达的叶节点是最有可能与查询相关的文档。
  • 根据原始文档:查询树索引需要从根节点向下遍历到叶节点。默认情况下,(child_branch_factor=1),查询在给定父节点的情况下选择一个子节点。如果child_branch_factor=2,查询将在每个级别选择两个子节点。

img

使用树索引的缺点:

  • 树索引可能比其他类型的索引(如向量索引)更难创建。这是因为需要手动或自动地将文档分组到相关的集群中。
  • 树索引在查找与查询“无关”的文档时效率较低(也就是空转的效率较低)。这是因为索引需要遍历整个树,即使查询只与少量文档相关。
  • 树索引可能比其他类型的索引更难理解。这是因为树形结构不是人类可以直接解释的。

总的来说,树索引是索引和查询大型文本数据集的强大工具。它们对于查找相关文档非常有效,可伸缩,并且相对容易创建。但是,它们比其他类型的索引更难创建,并且在空转查询时效率较低。

关键词表索引

关键字表索引是一种将文档的关键字存储在表中的索引,我觉得这更加类似Map<k,v>或者字典的结构。表中的每一行代表一个关键字,每一列代表一个文档。通过在表中查找关键字,可以使用表索引来查找包含给定关键字的文档。

img

使用关键字表索引的优点:

  • 关键字表索引对于查找包含特定关键字的文档非常有效,这是因为索引可以快速查找表中的关键字,并找到包含该关键字的所有文档。
  • 关键字表索引也非常可扩展,它们可以用于索引包含数百万甚至数十亿个文档的大型数据集。
  • 关键字表索引相对容易创建,一旦有了文档,就可以从文档中提取关键字并将其存储在关键字表中。

看看查询的过程:

  • 查询关键字表索引时,需要提供关键字。然后索引在表中查找关键字并返回包含该关键字的所有文档。

img

使用关键字Index的缺点或问题:

  • 关键字表索引在查找包含多个关键字的文档时效率较低,这是因为索引需要单独查找表中的每个关键字。
  • 关键字表索引对于具有大量关键字的文档可扩展性较差,这是因为表需要更大才能存储所有文档的关键字。
  • 对于同义词或相关关键字的文档,关键字表索引可能不太准确,这是因为索引将只返回包含所查询的确切关键字的文档。

总的来说,关键字表索引是索引和查询大型文本数据集的强大工具。它们对于查找包含特定关键字、可伸缩且相对容易创建的文档非常有效。但是,它们在查找包含多个关键字的文档时效率较低,在查找包含大量关键字的文档时可伸缩性较差,在查找包含同义词或相关关键字的文档时准确性较差。

总结

我们讨论了5种类型的索引,它们如何存储数据,在查询时如何工作,以及每种索引的优缺点。因此,您可以根据自己的要求和约束条件选择最适合的Index。有一些高级类型的索引,如知识图谱索引等,后面再单独讲,需要结合知识图谱本身的一些概念。

对LlamaIndex的研究是因为它确实很优秀,很多理念也给了我们极大启发。就像小明说的,他想好好学习LlamaIndex,然后用Java也写一个简化版,因为他要拯救广大面对AI大浪而处于彷徨中的Java程序员,哈哈。

引用

1.Different types of indexes in LlamaIndex to improve your RAG system:https://uttu-parashar.medium.com/different-types-of-indexes-in-llamaindex-to-improve-your-rag-system-fa9c49f40559

2.LlamaIndex官方文档:https://docs.llamaindex.ai/en/stable/core_modules/data_modules/documents_and_nodes/root.html


TorchV AI支持试用!

如您有大模型应用方面的企业需求,欢迎咨询!



实战!私有化部署RAG大模型,ChatGLM2-6B还是Baichuan2-13B

rag

图1:RAG的架构流程,by作者创作

经过之前一段时间的捣腾,个人感觉我们的RAG应用已经来到了一个全新的层面,在语义理解(相关度)和准确度上都有了长足进步。

但是问题来了。之前和菊厂的业务交流中,对方明确提出一些客户的私有化部署意愿是很明确,主要原因是数据安全。这段时间和一些销售人员、潜在客户交流的时候也收到了类似信息。而我们现在的整套技术栈中,唯一还不能私有化的是LLM。而这也是最终客户最担心的问题——他们的数据被送进云上的公共LLM,不管这些LLM在数据安全上如何申明,对于一些数据就是资产的客户来说依然存在忧虑。所以我们最近就在开始做LLM的本地化测试。

LLM选择标准

在选本地化的LLM之前,我们先根据实际情况定义一些选择标准:

  • 归纳优先:我们不需要LLM在各个方面都很优秀,不需要它们会很强的coding和复杂逻辑推理能力,RAG最重要的还是出色的归纳能力;
  • 体量考虑:不要太大,最好在13B及以下,因为再大就需要一张A100等专业显卡,或者要多张消费级显卡。我们的目标是一张RTX 4090可以解决问题,对很多客户来说,A卡很难买,而且价格太高了;
  • 中文能力:我们主要面对的还是中文业务,所以Llama对我们还是成本太高,如果自己做大量训练的话。

大模型试用选择

定义了选择标准之后,我们就来实际试用了。待选的其实也不多,我们自己有一个全量训练的BLOOM-7B,然后另外两个待选是ChatGLM2-6B和Baichuan2-13B。如果您还有其他更好的选择,可以私信我,哈。

试用自训练的BLOOM-7B

部署在公司内网的自训练BLOOM-7B是今年3月份就训练好的,当时也算是走在比较前面的,至少在我们文旅数字化行业,好像当时是没有的。BLOOM-7B在问答方面能力还是可以的,在文旅方面因为有我们自己多年的语料沉淀,在我们覆盖的客户景区的问答上,在当时效果超过ChatGPT。

bloom7b

图2:自训练的BLOOM-7B,没有使用原始权重,从CLM开始训练,使用了8块A100

但是,接入到RAG之后,发现归纳能力不太行。而且近期因为新的开源大模型出来很多,所以暂时也没有打算再去优化这个7B的大模型。所以,首先试用和淘汰的就是我们自己训练的BLOOM-7B,这个行业,真的一日千里啊,3月份训练的大模型,当时是小甜甜,现在已经是牛夫人了。

试用ChatGLM2-6B

目前我们使用的是智谱的标准版(API,在线版),效果应该说很棒,所以我们首先想到的是ChatGLM2-6B的开源版本。

chatglmapi

图3:智谱大模型的在线API,能力还是非常不错的。

ChatGLM2-6B因为已经有商业许可,所以两个月前就做了部署,把AutoModel.from_pretrained的指向从本地改成Hugging Face的模型地址让它更新到最新版本,然后接上我们的RAG做测试。说实话,使用同样的Prompt,ChatGLM2-6B的归纳能力只能说是复读机级别的(⊙o⊙)。LLM的归纳能力,太简单了可以通过Prompt优化改成复杂的,但是已经让它按最简单的方式去归纳了,依然是复读机的结果,那就不适用了。

chatglm2-6b

图4:ChatGLM2-6B的归纳能力就是复读机级别的,哪怕我们硬性规定用20个字归纳也不行。

因为ChatGLM2目前开放的就是6B和130B两个版本,但是130B的对于我们来说已经远超定义的选择范围,所以就放弃ChatGLM2了。

试用Baichuan2-13B

百川大模型选型

百川大模型是我们一个在老牌中厂的小伙伴给我们推荐的,据说他们内部已经在实用,而且效果不错,于是我就开始转向Baichuan2-13B。

baichuan2types

图5:百川大模型2目前的几个版本,7B、13B,以及Base和Chat版

我们首先选择的是13B,因为7B/6B这个级别的我们上面已经试用了,在技术层面没有碾压式的升级之前,参数体量有时候就决定了能力。对于Base和Chat,因为我们现在更多考察的还是归纳能力,所以为了方面考虑,选择了Chat版本。所以目前选择的就是Baichuan2-13B-Chat

接下来就是考虑量化版本,官方给出了资源表:

Precision Baichuan2-7B Baichuan2-13B
bf16 / fp16 15.3 GB 27.5 GB
8bits 8.0 GB 16.1 GB
4bits 5.1 GB 8.6 GB
表1:量化效果,来源于Baichuan2的Github说明

按一张RTX 4090(24GB)来算的话,最佳的应该是Baichuan2-13B的8bits版本。但是官方只提供了fp16和4bits两个版本,唯独没有8bits量化版本。

安装和运行fp16版本

我的做法是先部署fp16,先试试效果呗。

第一步是git clone代码,然后在Hugging Face上,models的过程是比较痛苦的,三个bin文件近28GB,最近几个月我发现直接下载(wget)总是无法成功,而我的Ubuntu服务器是没有科学上网的。所以只能在家里的Mac上先下载,直到今天凌晨2点才完成(好困~),然后传到Ubuntu上。

baichuan2models

图6:Baichuan2-13B的权重文件

先用fp16先安装,把cli_demo.py里面的model地址改成本地的,这样不用每次运行的时候都会查询并下载最新版权重文件,主要是等待时间太长了。

cli_demo.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
print("init model ...")
model = AutoModelForCausalLM.from_pretrained(
"baichuan-inc/Baichuan2-13B-Chat", // 这里改成你自己的本地模型地址
torch_dtype=torch.float16,
device_map="auto",
trust_remote_code=True
)
model.generation_config = GenerationConfig.from_pretrained(
"baichuan-inc/Baichuan2-13B-Chat" // 这里改成你自己的本地模型地址
)
tokenizer = AutoTokenizer.from_pretrained(
"baichuan-inc/Baichuan2-13B-Chat", // 这里改成你自己的本地模型地址
use_fast=False,
trust_remote_code=True
)

运行程序:

1
python cli_demo.py

居然跑起来了,但是,推理的速度跟乌龟爬一样慢!

视频1:fp16,在我的RTX4090服务器上推理速度非常感人....

两个回合之后,直接就OutOfMemoryError了。

手工量化int8

那现在只能把fp16量化为8bits,还好,官网有提到量化的方法。

我们需要先安装两个lib:

1
2
3
pip install xformers

conda install bitsandbytes //也可以使用pip

然后在原来的models平级目录mkdir一个models_8,然后我用了一个比较偷懒的做法,直接把cli_demo.py复制为quan8.py。

ls

图7:新增models_8文件夹,用来存放离线量化之后的int8权重,quan8.py就是转化脚本

下面修改这个新复制的quan8.py。

quan8.py

1
2
3
4
5
6
7
8
9
10
11
12
13
def init_model():
print("quant8int model ...")
model = AutoModelForCausalLM.from_pretrained(
"/home/tumeng/llm/Baichuan2/models",
load_in_8bit=True,
device_map="auto",
trust_remote_code=True
)
model.save_pretrained(
"/home/tumeng/llm/Baichuan2/models_8"
)

return model

我是比较偷懒的方式,因为当时太困了!

其他的代码我不管(运行的时候会报错,但不影响),就修改了init_model()函数。AutoModelForCausalLM.from_pretrained里面的第一个参数是原权重文件,save_pretrained里面是量化出来的int8权重文件保存的目录。

运行成功

再次修改cli_demo.py,将model指向到int8的模型

cli_demo.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def init_model():
print("init model ...")
model = AutoModelForCausalLM.from_pretrained(
"/home/tumeng/llm/Baichuan2/models_8",
torch_dtype=torch.float16,
device_map="auto",
trust_remote_code=True
)
model.generation_config = GenerationConfig.from_pretrained(
"/home/tumeng/llm/Baichuan2/models_8"
)
tokenizer = AutoTokenizer.from_pretrained(
"/home/tumeng/llm/Baichuan2/models_8",
use_fast=False,
trust_remote_code=True
)
return model, tokenizer

再次运行cli_demp.py,这次运行成功了,而且推理速度满足要求,棒!

然后就到了检验Baichuan2-13B的归纳能力的时候了,我直接复制了小明给我的Prompt。

视频2:Baichuan2-13B(8ints)的推理速度还可以,归纳能力过关。

guina2

图8:基本上归纳的还是比较准确和简洁的。

速度、归纳效果,都还可以!那私有化,暂时就先选型Baichuan2-13B了。

对了,其实Baichuan2-13B的逻辑能力是有短板的,我经常用的一道测试题,它答得不好。

suanshu

图9:龙凤胎的逻辑推理题,Baichuan2-13B答错了

结论

这次大模型的选型测试主要还是为了私有化的需要,如果没有私有化(甚至完全断网)需求的话,个人感觉智谱的API还是挺好用的。本次选择除了最开始定义的三个过滤条件外,其实还有一个就是可商用,ChatGLM2-6B、BLOOM-7B和Baichuan2-13B都是符合这一条件的。


TorchV AI支持试用!

如您有大模型应用方面的企业需求,欢迎咨询!



使用RAG-Fusion和RRF让RAG在意图搜索方面更进一步

这是一篇翻译稿,主要是觉得作者的这个观点还是有一些启发的。

主要内容:

  • RAG-Fusion机制的介绍
  • 多查询生成
  • 倒数排序融合

RAG现在非常火,当然性能也非常出色,它绝对走在了信息检索技术的康庄大道上,RAG有许多优点:

  • 向量搜索融合: RAG通过将向量搜索功能与生成模型集成,引入了一种新的范例。这种融合能够从大型语言模型(大语言模型)生成更丰富、更具上下文感知的输出。
  • 减少幻觉: RAG显著降低了LLM的幻觉倾向,使生成的文本更基于数据。
  • 个人和专业实用程序:从个人应用程序如筛选笔记到更专业的集成,RAG展示了在提高生产力和内容质量方面的多功能性,同时基于可信赖的数据源。

然而,我发现越来越多的“限制”:

  • 当前搜索技术的限制: RAG受到限制我们基于检索的词法和向量搜索技术的相同限制。
  • 人工搜索效率低下:人类并不擅长在搜索系统中输入他们想要的东西,比如打字错误、模糊的查询或有限的词汇,这通常会导致错过明显的顶级搜索结果之外的大量信息。虽然RAG有所帮助,但它并没有完全解决这个问题。
  • 搜索的过度简化:我们流行的搜索模式将查询线性地映射到答案,缺乏深度来理解人类查询的多维本质。这种线性模型通常无法捕捉更复杂的用户查询的细微差别和上下文,从而导致相关性较低的结果。

Google keyword tends showing an increase in searched for Retrieval Augmented Generation

RAG关键词的Google搜索量在2023年暴涨。

那么,我们能做些什么来解决这些问题?我们需要一个系统,它不仅能检索我们的问题,还能掌握我们查询背后的细微差别,而不需要更高级的大语言模型。认识到这些挑战并受到可能性的启发,我开发了一个更精细的解决方案:RAG-Fusion。

为什么使用RAG-Fusion?

  • 通过生成多个用户查询和重新排序结果来解决RAG固有的约束。
  • 利用倒数排序融合(RRF)和自定义向量评分加权,生成全面准确的结果。

RAG-Fusion希望弥合用户明确提出的问题和他们(原本的意图)打算提出的问题之间的差距,更接近于发现通常仍然隐藏的变革性知识。

深入研究RAG-Fusion的机制

工具和技术栈

RAG Fusion的基本三要素与RAG相似,并在于相同的三个关键技术:

  • 一种通用编程语言,通常是Python。
  • 一个专用的向量搜索数据库,如Elasticsearch或Pinecone,指导文档检索。
  • 一个强大的大型语言模型,如ChatGPT,制作文本。

Overview of RAG-Fusion from query -> multiple queries -> multiple vector searches -> generated output

RAG-Fusion工作机制的图示

然而,与RAG不同的是,RAG-Fusion通过几个额外的步骤来区分自己——查询生成和结果的重新排序。

RAG-Fusion’s 工作步骤:

  1. 查询语句的相关性复制:通过LLM将用户的查询转换为相似但不同的查询。

  2. 并发的向量搜索:对原始查询及其新生成的同级查询执行并发的向量搜索。

  3. 智能重新排名:聚合和细化所有结果使用倒数排序融合(RRF)。

  4. 最后优中选优:将精心挑选的结果与新查询配对,引导LLM进行有针对性的查询语句输出,考虑所有查询和重新排序的结果列表。

RAG Fusion python code

RAG-Fusion代码:https://github.com/Raudaschl/rag-fusion

让我们更详细地介绍每一个步骤。

多查询生成

为什么使用多查询?

在传统的搜索系统中,用户经常输入一个单一的查询来查找信息。虽然这种方法很简单,但它有局限性。单个查询可能无法捕获用户感兴趣的全部范围,或者它可能太窄而无法产生全面的结果。这就是从不同角度生成多个查询的地方。

技术实施(Prompt工程)

Flow Diagram of Multi-Query Generation: Leveraging Prompt Engineering and Natural Language Models to Broaden Search Horizons and Enhance Result Quality.

多查询生成流程图:利用prompt工程和自然语言模型来拓宽搜索视野和提高结果质量。

prompt工程的使用对于生成多个查询至关重要,这些查询不仅与原始查询相似,而且还提供不同的角度或透视图。

下面是它的工作原理:

  1. 对语言模型的函数调用:函数调用语言模型(在本例中为chatGPT)。这种方法需要一个特定的指令集(通常被描述为“系统消息”)来指导模型。例如,这里的系统消息指示模型充当“AI助手”。
  2. 自然语言查询:然后模型根据原始查询生成多个查询。
  3. 多样性和覆盖范围:这些查询不仅仅是随机变化。它们是精心生成的,以提供对原始问题的不同观点。例如,如果最初的查询是关于“气候变化的影响”,生成的查询可能包括“气候变化的经济后果”、“气候变化和公共卫生”等角度。

这种方法确保搜索过程考虑更广泛的信息,从而提高生成的摘要的质量和深度。

倒数排序融合 (RRF)

Why RRF?

倒数排序融合(RRF) 是一种将具有不同相关性指标的多个结果集组合成单个结果集的方法,不同的相关性指标也不必相互关联即可获得高质量的结果。该方法的优势在于不利用相关分数,而仅靠排名计算。相关分数存在的问题在于不同模型的分数范围差。RRF是与滑铁卢大学(CAN)和谷歌(Google)合作开发的(https://plg.uwaterloo.ca/~gvcormac/cormacksigir09-rrf.pdf),用其作者的话来说,“比任何单独的系统产生更好的结果,比标准的重新排名方法产生更好的结果”。

在elasticsearch的8.8版本,已经引入了RRF。

RRF algorithm where k=60

D是文档集,R是一组排名作为(1..|D| )的排列,其中k默认是60.

通过组合来自不同查询的排名,我们增加了最相关的文档出现在最终列表顶部的机会。RRF特别有效,因为它不依赖于搜索引擎分配的绝对分数,而是依赖于相对排名,这使得它非常适合组合来自可能具有不同分数尺度或分布的查询的结果。

通常,RRF用于混合词法和向量结果。虽然这种方法可以帮助弥补向量搜索在查找特定术语(例如首字母缩写词)时缺乏专一性的不足,但我对结果并不满意,它往往更像是多个结果集的拼凑,因为对于词法和向量搜索的相同查询很少出现相同的结果。

把RRF想象成那种坚持在做决定之前征求每个人意见的人,这种意见是有帮助的,兼听则明,多多益善。

技术实现

How RRF works to rerank documents based on the position of multiple sets of search results.

倒数排序融合流程示意图

函数reciprocal_rank_fusion接受一个搜索结果字典,其中每个键都是一个查询,对应的值是一个文档id列表,按照它们与该查询的相关性排序。然后,RRF算法根据每个文档在不同列表中的排名计算一个新分数,并对它们进行排序,以创建一个最终的重新排名的列表。

在计算融合分数之后,该函数按照分数的降序对文档进行排序,以获得重新排序的最终列表,然后返回该列表。

生成的输出

用户意图保存

使用多个查询的挑战之一是可能会削弱用户的原始意图。为了缓解这种情况,我们指示模型在prompt工程中给予原始查询更多的权重

技术实现

最后,重新排序的文档和所有查询被馈送到LLM的Prompt中,以典型的RAG方式生成输出,例如请求响应或摘要。

通过对这些技术和技巧进行分层,RAG Fusion提供了一种强大而细致的文本生成方法,它利用最好的搜索技术和生成AI来产生高质量,可靠的输出。

RAG-Fusion优缺点

优势

1、优质的原材料质量

当您使用RAG Fusion时,您的搜索深度不仅仅是“增强”,并且其实搜索范围已经被放大了。相关文档的重新排序意味着你不仅仅是在抓取信息的字面意思,而是在深入这个搜索的意图,所以会涉及到更多的优质文档和待搜索内容。

2、增强用户意图对齐

RAG Fusion的设计理念中包含了自动提示,很多时候我们在搜索的时候并不知道应该怎么描述,像Google、百度就会进行输入框的自动补全提示。RAG Fusion可以捕获用户信息需求的多个方面,从而提供整体输出并与对用户意图进行增强。

3、自动为用户查询输入纠错

该系统不仅可以解释用户的查询,还可以精炼用户的查询。通过生成多个查询变体,RAG Fusion执行隐式拼写和语法检查,从而提高搜索结果的准确性。

4、导航复杂查询(自动分解长句的意图)

人类的语言在表达复杂或专门的思想时往往会结结巴巴。该系统就像一个语言催化剂,生成各种变体,这些变体可能包含更集中、更相关的搜索结果所需的行话或术语。它还可以处理更长的、更复杂的查询,并将它们分解成更小的可理解的块,用于向量搜索。

5、搜索中的意外发现(关联推荐)

以前在亚马逊买书的时候,总能因为相关推荐发现我更想要的书,RAG Fusion允许这个偶然的发现。通过使用更广泛的查询范围,系统有可能挖掘到信息,而这些信息虽然没有明确搜索,但却成为用户的“啊哈”时刻。这使得RAG Fusion有别于其他传统的搜索模型。

挑战

1、过于啰嗦的风险

RAG-Fusion的深度有时会导致信息泛滥。输出可能会详细到令人难以承受的程度,把RAG-Fusion想象成一个过度解释事物的朋友。

2、可能成本会比较昂贵

多查询输入是需要LLM来做处理的,这时候,很有可能会引起更多的tokens消耗。

最后

好了,最近发了不少关于RAG方面的文章了,下次准备做一个总结,把RAG需要的整个流程,以及对应的文章,都串起来讲一遍。再后面就是讲讲我们在RAG方向上的实际产品和技术案例了。

原文:https://towardsdatascience.com/forget-rag-the-future-is-rag-fusion-1147298d8ad1


TorchV AI支持试用!

如您有大模型应用方面的企业需求,欢迎咨询!



全面指南———用python提取PDF中各类文本内容的方法

本文将讲述从PDF中将表格、图像和纯文本提取出文本信息的完整过程,主要内容:

  • PDF的分类:文本型、OCR和扫描型
  • 针对不同类型的PDF,我们提取的理论依据
  • 环境安装
  • 编写提取纯文本的函数
  • 编写提取图像中文本的函数
  • 编写提取表格的文本内容的函数
  • 最后的整合处理

写在前面

随着大型语言模型(LLM)的应用不断发展,从简单的文本摘要和翻译,到基于情绪和财务报告主题预测股票表现,再到复杂的检索(如RAG),都需要我们首先从真实世界提取文本数据。

有许多类型的文档共享这种非结构化信息,从网络文章、博客文章到手写信件和图表。然而,这些文本数据的很大一部分是以PDF格式存储和传输的(我们做的更极端,即使原先不是PDF,一般也会先把文字进行处理好,转换为PDF)。

因此,从PDF文档中提取信息,是很多类似RAG这样的应用第一步要处理的事情,在这个环节,我们需要做好三件事:

  • 提取出来的文本要保持信息完整性,也就是准确性;
  • 提出的结果需要有附加信息,也就是要保存元数据;
  • 提取过程要完成自动化,也就是流程化。

然而,在我们开始之前,我们需要指定目前不同类型的pdf,更具体地说,是出现最频繁的三种:

  1. 机器生成的pdf文件:这些pdf文件是在计算机上使用W3C技术(如HTML、CSS和Javascript)或其他软件(如Adobe Acrobat、Word或Typora等MarkDown工具)创建的。这种类型的文件可以包含各种组件,例如图像、文本和链接,这些组件都是可以被选中、搜索和易于编辑的。
  2. 传统扫描文档:这些PDF文件是通过扫描仪、手机是的扫描王这样的APP从实物上扫描创建的。这些文件只不过是存储在PDF文件中的图像集合。也就是说,出现在这些图像中的元素,如文本或链接是不能被选择或搜索的。本质上,PDF只是这些图像的容器而已。
  3. 带OCR的扫描文档:这种类似有点特殊,在扫描文档后,使用光学字符识别(OCR)软件识别文件中每个图像中的文本,将其转换为可搜索和可编辑的文本。然后软件会在图像上添加一个带有实际文本的图层,这样你就可以在浏览文件时选择它作为一个单独的组件。但是有时候我们不能完全信任OCR,因为它还是存在一定几率的识别错误的。

另外还有一种情况是,尽管现在越来越多的机器安装了OCR系统,可以识别扫描文档中的文本,但仍然有一些文档以图像格式包含整页。当你读到一篇很棒的文章时,你想选中一个句子,但却选择了整个页面。这可能是由于特定OCR程序的限制或OCR根本就可有介入。为了避免在最终本文提取的时候遗漏这些“看上去像文本的图片”,我在创建过程中需要考虑做一些处理。

理论方法

考虑到所有上面说的几种不同类型的PDF文件格式,对PDF的布局进行初步分析以确定每个组件所需的适当工具就很重要了。

更具体地说,根据此分析的结果,我们将应用适当的方法从PDF中提取文本,无论是在具有元数据的语料库块中呈现的文本、图像中的文本还是表格中的结构化文本。在没有OCR的扫描文档中,从图像中识别和提取文本将非常繁重。此过程的输出将是一个Python字典(dictionary),其中包含为PDF文件的每个页面提取的信息。该字典(dictionary)中的每个键将表示文档的页码,其对应的值将是一个列表,其中包含以下5个嵌套列表:

  1. 从语料库中每个文本块提取的文本
  2. 每个文本块中文本的格式,包括font-family和font-size
  3. 从页面中的图像上提取的文本
  4. 以结构化格式从表格中提取的文本
  5. 页面的完整文本内容

img

图1:文本提取示意

这样,我们可以实现对每个PDF组件提取的文本的更合乎逻辑的分离,并且有时可以帮助我们更容易地检索通常出现在特定组件中的信息(例如,LOGO中的公司名称、表格中各数据的对应关系)。此外,从文本中提取的元数据,如font-family和font-size,可以用来轻松地识别文本标题高亮的重要文本,这将帮助我们进一步分离,或者在多个不同的块中对文本进行合并、排序等后续处理。最后,以LLM能够理解的方式保留结构化表信息将显著提高对提取数据中关系的推理质量。然后,这些信息可以在最终输出的时候保留它原本的格式。

您可以在下面的图片中看到这种方法的流程图。

img

PDF提取方法的具体流程,中间会有各种适配选择

安装所需的库

在我们开始使用PDF文本提取之前,应该安装必要的库。机器上首先需要安装Python 3.10或更高版本。你也可以安装最新的Anaconda,我装的就是它。

PyPDF2:从存储路径读取PDF文件。

1
pip install PyPDF2

Pdfminer :执行布局分析并从PDF中提取文本和格式。(.six版本的库是支持Python 3的版本)

1
pip install pdfminer.six

Pdfplumber: 识别PDF页面中的table并从中提取信息。

1
pip install pdfplumber

Pdf2image:将裁剪后的PDF图像转换为PNG图像。

1
pip install pdf2image

PIL: 读取PNG图像。

1
pip install Pillow

Pytesseract: 从图像中提取文本使用OCR技术

这个安装起来有点棘手,因为首先,您需要安装Google Tesseract OCR(链接在文章底部引用区),这是一个基于LSTM模型的OCR机器,用于识别行识别和字符模式。

如果你是Mac用户,你可以在你的终端上通过Brew安装它,这样就可以了。

1
brew install tesseract

对于Windows用户,可以参照以下步骤进行安装(https://linuxhint.com/install-tesseract-windows/)。安装软件后,需要将它们的可执行文件路径添加到计算机上的环境变量中。或者,也可以运行以下命令,使用以下代码直接将其路径包含在Python脚本中:

1
pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe'

然后就可以安装Python库了

1
pip install pytesseract

最后,我们将在程序中导入所有库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 读取PDF
import PyPDF2
# 分析PDF的layout,提取文本
from pdfminer.high_level import extract_pages, extract_text
from pdfminer.layout import LTTextContainer, LTChar, LTRect, LTFigure
# 从PDF的表格中提取文本
import pdfplumber
# 从PDF中提取图片
from PIL import Image
from pdf2image import convert_from_path
# 运行OCR从图片中提取文本
import pytesseract
# 清除过程中的各种过程文件
import os

现在我们都准备好了,让我们来试一下这些库怎么样。

文档的布局(Layout)分析

img

布局分析,理解什么是LOGO,什么是标题,什么是表格等

对于初步分析,我们使用PDFMiner的Python库将文档对象中的文本分离为多个页面对象,然后分解并检查每个页面的布局。PDF文件本身缺乏结构化信息,如我们可以一眼识别的段落、句子或单词等。相反,它们只理解文本中的单个字符及其在页面上的位置。通过这种方式,PDFMiner尝试将页面的内容重构为单个字符及其在文件中的位置。然后,通过比较这些字符与其他字符的距离,它组成适当的单词、句子、行和文本段落。为了实现这一点,PDFMiner使用高级函数extract_pages()从PDF文件中分离各个页面,并将它们转换为LTPage对象。

然后,对于每个LTPage对象,它从上到下遍历每个元素,并尝试将适当的组件识别为:

  • LTFigure:表示PDF中页面上的图形或图像的区域。
  • LTTextContainer:代表一个矩形区域(段落)中的一组文本行(line),然后进一步分析成LTTextLine对象的列表。它们中的每一个都表示一个LTChar对象列表,这些对象存储文本的单个字符及其元数据
  • LTRect表示一个二维矩形,可用于在LTPage对象中占位区或者Panel,图形或创建表。

因此,使用Python对页面进行重构之后,将页面元素分类为LTFigure(图像或图形)、LTTextContainer(文本信息)或LTRect(表格),我们就可以选择适当的函数来更好地提取内容信息了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
for pagenum, page in enumerate(extract_pages(pdf_path)):

# Iterate the elements that composed a page
for element in page:

# Check if the element is a text element
if isinstance(element, LTTextContainer):
# Function to extract text from the text block
pass
# Function to extract text format
pass

# Check the elements for images
if isinstance(element, LTFigure):
# Function to convert PDF to Image
pass
# Function to extract text with OCR
pass

# Check the elements for tables
if isinstance(element, LTRect):
# Function to extract table
pass
# Function to convert table content into a string
pass

现在我们了解了流程的结构原理,接下来我们来创建从不同组件种提取文本所需的函数。

定义从PDF中提取文本的函数

从这里开始,从PDF中提取文本就非常简单了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 创建一个文本提取函数

def text_extraction(element):
# 从行元素中提取文本
line_text = element.get_text()

# 探析文本的格式
# 用文本行中出现的所有格式初始化列表
line_formats = []
for text_line in element:
if isinstance(text_line, LTTextContainer):
# 遍历文本行中的每个字符
for character in text_line:
if isinstance(character, LTChar):
# 追加字符的font-family
line_formats.append(character.fontname)
# 追加字符的font-size
line_formats.append(character.size)
# 找到行中唯一的字体大小和名称
format_per_line = list(set(line_formats))

# 返回包含每行文本及其格式的元组
return (line_text, format_per_line)

因此,要从文本容器中提取文本,我们只需使用LTTextContainer元素的get_text()方法。此方法检索构成特定语料库框中单词的所有字符,并将输出存储在文本数据列表中。此列表中的每个元素表示容器中包含的原始文本信息。

现在,为了识别该文本的格式,我们遍历LTTextContainer对象,以单独访问该语料库的每个文本行。在每次迭代中,都会创建一个新的LTTextLine对象,表示该语料库块中的一行文本。然后检查嵌套的line元素是否包含文本。如果是,我们将每个单独的字符元素作为LTChar访问,其中包含该字符的所有元数据。从这个元数据中,我们提取两种类型的格式,并将它们存储在一个单独的列表中,对应于检查的文本:

  • 字符的font-family,包括字符是粗体还是斜体
  • 字符的font-size

通常,特定文本块中的字符往往具有一致的格式,除非某些字符以粗体突出显示。为了便于进一步分析,我们捕获文本中所有字符的文本格式的唯一值,并将它们存储在适当的列表中。

img

获取字体、尺寸等元数据的过程

定义从图像中提取文本的函数

在这里,我认为这是一个更棘手的部分。如何处理在PDF中找到的图像中的文本?

首先,我们需要在这里确定存储在pdf中的图像元素与文件的格式不同,例如JPEG或PNG。这样,为了对它们应用OCR软件,我们需要首先将它们从文件中分离出来,然后将它们转换为图像格式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 创建一个从pdf中裁剪图像元素的函数
def crop_image(element, pageObj):
# 获取从PDF中裁剪图像的坐标
[image_left, image_top, image_right, image_bottom] = [element.x0,element.y0,element.x1,element.y1]
# 使用坐标(left, bottom, right, top)裁剪页面
pageObj.mediabox.lower_left = (image_left, image_bottom)
pageObj.mediabox.upper_right = (image_right, image_top)
# 将裁剪后的页面保存为新的PDF
cropped_pdf_writer = PyPDF2.PdfWriter()
cropped_pdf_writer.add_page(pageObj)
# 将裁剪好的PDF保存到一个新文件
with open('cropped_image.pdf', 'wb') as cropped_pdf_file:
cropped_pdf_writer.write(cropped_pdf_file)

# 创建一个将PDF内容转换为image的函数
def convert_to_images(input_file,):
images = convert_from_path(input_file)
image = images[0]
output_file = "PDF_image.png"
image.save(output_file, "PNG")

# 创建从图片中提取文本的函数
def image_to_text(image_path):
# 读取图片
img = Image.open(image_path)
# 从图片中抽取文本
text = pytesseract.image_to_string(img)
return text

为此,我们遵循以下流程:

  1. 我们使用从PDFMiner检测到的LTFigure对象的元数据来裁剪图像框,利用其在页面布局中的坐标。然后使用PyPDF2库将其保存为新的PDF文件。
  2. 然后,我们使用pdf2image库中的convert_from_file()函数将目录中的所有PDF文件转换为图像列表,并以PNG格式保存它们。
  3. 最后,现在我们有了图像文件,我们使用PIL模块的image 包在脚本中读取它们,并实现pytesseract的image_to_string()函数,使用tesseract OCR引擎从图像中提取文本。

结果,这个过程返回图像中的文本,然后我们将其保存在输出字典中的第三个列表中。此列表包含从检查页面上的图像中提取的文本信息。

定义从表格中提取文本的函数

在本节中,我们将从PDF页面上的表格中提取更具逻辑结构的文本。这是一个比从语料库中提取文本稍微复杂的任务,因为我们需要考虑信息的粒度和表中呈现的数据点之间形成的关系。

虽然有几个库用于从pdf中提取表数据,其中Tabula-py是最著名的库之一,但我们已经确定了它们在功能上的某些限制。

在我们看来,最明显的问题来自于库在表的文本中使用换行特殊字符\n来标识表的不同行。这在大多数情况下工作得很好,但是当单元格中的文本被包装成2行或更多行时,它无法正确捕获,从而导致添加不必要的空行并丢失提取的单元格的上下文。

当我们尝试使用tabula-py从一个表中提取数据时,你可以看到下面的例子:

img

提取表格信息,虽然转化为文本了,但是我们依然可以保留表格的信息

然后,提取的信息以Pandas DataFrame而不是字符串的形式输出。在大多数情况下,这可能是一种理想的格式,但是在考虑文本的Transformers的情况下,这些结果需要在输入到模型之前进行转换。

出于这个原因,出于各种原因,我们使用了pdfplumber库来处理这个任务。首先,它是基于pdfminer构建的。我们初步分析时用了六个,也就是说里面有类似的物品。此外,它的表检测方法基于行元素及其交点,这些交点构建包含文本的单元格,然后是表本身。这样,在确定表的单元格之后,我们就可以提取单元格中的内容,而不必携带需要呈现的行数。然后,当我们拥有表的内容时,将其格式化为类似表的字符串,并将其存储在适当的列表中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 从页面中提取表格内容

def extract_table(pdf_path, page_num, table_num):
# 打开PDF文件
pdf = pdfplumber.open(pdf_path)
# 查找已检查的页面
table_page = pdf.pages[page_num]
# 提取适当的表格
table = table_page.extract_tables()[table_num]
return table

# 将表格转换为适当的格式
def table_converter(table):
table_string = ''
# 遍历表格的每一行
for row_num in range(len(table)):
row = table[row_num]
# 从warp的文字删除线路断路器
cleaned_row = [item.replace('\n', ' ') if item is not None and '\n' in item else 'None' if item is None else item for item in row]
# 将表格转换为字符串,注意'|'、'\n'
table_string+=('|'+'|'.join(cleaned_row)+'|'+'\n')
# 删除最后一个换行符
table_string = table_string[:-1]
return table_string

为了实现这一点,我们创建了两个函数,**extract_table()用于将表的内容提取到一个列表的列表中,table_converter()**用于将这些列表的内容连接到一个类似表的字符串中。

**extract_table()**函数中:

  1. 我们打开PDF文件。
  2. 我们导航到PDF文件的检查页面。
  3. 从pdfplumber在页面上找到的表列表中,我们选择所需的表。
  4. 我们提取表的内容,并将其输出到一个嵌套列表的列表中,该列表表示表的每一行。

在**table_converter()**函数:

  1. 我们在每个嵌套列表中迭代,并清除其上下文中来自任何换行文本的任何不需要的换行符。
  2. 我们通过使用|符号将行中的每个元素分开来连接它们,从而创建表单元格的结构。
  3. 最后,我们在末尾添加一个换行符以移动到下一行。

这将产生一个文本字符串,该字符串将显示表的内容,而不会丢失其中显示的数据的粒度。

整合所有文本

现在我们已经准备好了代码的所有组件,让我们将它们全部添加到一个功能齐全的代码中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# 查找PDF路径
pdf_path = 'OFFER 3.pdf'

# 创建一个PDF文件对象
pdfFileObj = open(pdf_path, 'rb')
# 创建一个PDF阅读器对象
pdfReaded = PyPDF2.PdfReader(pdfFileObj)

# 创建字典以从每个图像中提取文本
text_per_page = {}
# 我们从PDF中提取页面
for pagenum, page in enumerate(extract_pages(pdf_path)):

# 初始化从页面中提取文本所需的变量
pageObj = pdfReaded.pages[pagenum]
page_text = []
line_format = []
text_from_images = []
text_from_tables = []
page_content = []
# 初始化检查表的数量
table_num = 0
first_element= True
table_extraction_flag= False
# 打开pdf文件
pdf = pdfplumber.open(pdf_path)
# 查找已检查的页面
page_tables = pdf.pages[pagenum]
# 找出本页上的表格数目
tables = page_tables.find_tables()


# 找到所有的元素
page_elements = [(element.y1, element) for element in page._objs]
# 对页面中出现的所有元素进行排序
page_elements.sort(key=lambda a: a[0], reverse=True)

# 查找组成页面的元素
for i,component in enumerate(page_elements):
# 提取PDF中元素顶部的位置
pos= component[0]
# 提取页面布局的元素
element = component[1]

# 检查该元素是否为文本元素
if isinstance(element, LTTextContainer):
# 检查文本是否出现在表中
if table_extraction_flag == False:
# 使用该函数提取每个文本元素的文本和格式
(line_text, format_per_line) = text_extraction(element)
# 将每行的文本追加到页文本
page_text.append(line_text)
# 附加每一行包含文本的格式
line_format.append(format_per_line)
page_content.append(line_text)
else:
# 省略表中出现的文本
pass

# 检查元素中的图像
if isinstance(element, LTFigure):
# 从PDF中裁剪图像
crop_image(element, pageObj)
# 将裁剪后的pdf转换为图像
convert_to_images('cropped_image.pdf')
# 从图像中提取文本
image_text = image_to_text('PDF_image.png')
text_from_images.append(image_text)
page_content.append(image_text)
# 在文本和格式列表中添加占位符
page_text.append('image')
line_format.append('image')

# 检查表的元素
if isinstance(element, LTRect):
# 如果第一个矩形元素
if first_element == True and (table_num+1) <= len(tables):
# 找到表格的边界框
lower_side = page.bbox[3] - tables[table_num].bbox[3]
upper_side = element.y1
# 从表中提取信息
table = extract_table(pdf_path, pagenum, table_num)
# 将表信息转换为结构化字符串格式
table_string = table_converter(table)
# 将表字符串追加到列表中
text_from_tables.append(table_string)
page_content.append(table_string)
# 将标志设置为True以再次避免该内容
table_extraction_flag = True
# 让它成为另一个元素
first_element = False
# 在文本和格式列表中添加占位符
page_text.append('table')
line_format.append('table')

# 检查我们是否已经从页面中提取了表
if element.y0 >= lower_side and element.y1 <= upper_side:
pass
elif not isinstance(page_elements[i+1][1], LTRect):
table_extraction_flag = False
first_element = True
table_num+=1


# 创建字典的键
dctkey = 'Page_'+str(pagenum)
# 将list的列表添加为页键的值
text_per_page[dctkey]= [page_text, line_format, text_from_images,text_from_tables, page_content]

# 关闭pdf文件对象
pdfFileObj.close()

# 删除已创建的过程文件
os.remove('cropped_image.pdf')
os.remove('PDF_image.png')

# 显示页面内容
result = ''.join(text_per_page['Page_0'][4])
print(result)

上面的脚本将运行:

  • 导入必要的库;

  • 使用pyPDF2库打开PDF文件;

  • 提取PDF的每个页面并重复以下步骤;

  • 检查页面上是否有任何表,并使用pdfplumner创建它们的列表;

  • 查找页面中嵌套的所有元素,并按照它们在页面布局中出现的顺序对它们进行排序。

然后对于每个元素:

检查它是否是一个文本容器,并且没有出现在表元素中。然后使用text_extraction()函数提取文本及其格式,否则传递该文本。

检查它是否为图像,并使用crop_image()函数从PDF中裁剪图像组件,使用convert_to_images()将其转换为图像文件,并使用image_to_text()函数使用OCR从中提取文本。

检查它是否是一个矩形元素。在这种情况下,我们检查第一个矩形是否是页表的一部分,如果是,我们移动到以下步骤:

  1. 查找表的边界框,以便不再使用text_extraction()函数提取其文本。
  2. 提取表的内容并将其转换为字符串。
  3. 然后添加一个布尔参数来说明我们是从Table中提取文本的。
  4. 此过程将在最后一个LTRect落在表的边界框中并且布局中的下一个元素不是矩形对象之后结束。(构成表的所有其他对象都将被传递)

每次迭代的输出将存储在5个列表中,命名为:

  1. page_text:包含来自PDF中文本容器的文本(当文本从另一个元素中提取时,将放置占位符)
  2. Line_format:包含上面提取的文本的格式(当文本从另一个元素提取时,将放置占位符)
  3. Text_from_images:包含从页面上的图像中提取的文本
  4. Text_from_tables:包含带有表内容的类表字符串
  5. Page_content:以元素列表的形式包含页面上呈现的所有文本

所有的列表将被存储在字典的关键,将代表每次检查页面的数量。

之后,我们将关闭PDF文件。

然后,我们将删除在此过程中创建的所有附加文件。

最后,我们可以通过连接page_content列表的元素来显示页面的内容。

最后

我认为这是一种利用了各种库的最佳特性的方法,使过程能够适应我们可能遇到的各种类型的pdf和元素,但PDFMiner完成了大部分繁重的工作。同时,信息的格式文本可以帮助我们识别潜在的标题文本分离成不同的逻辑部分而不是每个页面内容并可以帮助我们识别文本更重要的。

然而,总是会有更有效的方法来做这个任务,尽管我认为这种方法是更具包容性。

引用

  1. https://www.techopedia.com/12-practical-large-language-model-llm-applications
  2. https://www.pdfa.org/wp-content/uploads/2018/06/1330_Johnson.pdf
  3. https://pdfpro.com/blog/guides/pdf-ocr-guide/#:~:text=OCR technology reads text from, a searchable and editable PDF.
  4. https://pdfminersix.readthedocs.io/en/latest/topic/converting_pdf_to_text.html#id1
  5. https://github.com/pdfminer/pdfminer.six
  6. Google Tesseract OCR:https://github.com/tesseract-ocr/tesseract

Update: 2024-01-26

我们的TorchV Bot产品目前已经开始试用了,详情可以点击:https://www.luxiangdong.com/2024/01/25/lanuch-1
目前只接受企业用户试用,需要您填写一些信息,必要信息如下:

邮箱: 用来接收地址和账号
如何称呼您:
所服务的公司:
您的职位:

当然,如果您可以告诉我们您的使用场景,我们将更加感激!
对了,可以发送到yuanwai@mengjia.net
另外,也可以直接加我微信(lxdhdgss)联系我。


TorchV AI支持试用!

如您有大模型应用方面的企业需求,欢迎咨询!



进击吧!硬地骇客————独立开发者时代来临

本文主要内容:

1.为什么目前的市场对独立开发者越来越友好?从三个方面分析:价值导向、付费渠道和AI赋能。

2.独立开发者需要注意哪些事项?5点。

前些天国内的独立开发者圈子可是发生了不少事情:

  • OpenAI等国外大模型的接入限制问题;
  • APP接下来需要备案;
  • 承德remote开发者的罚款问题。

这些事件无一不在国内开发者社区掀起轩然大波,很多人觉得独立开发者时代进入尾声……

但我觉得恰恰相反,从整个经济发展规律来看,接下来应该是独立开发者(或团队)真正迎来蓬勃发展时代。

我的理由是以下几个点:

1. 市场环境变了,将更加面向用户价值;
2. 付费渠道更加完善:
3. AI帮助开发者更容易成为超级个体。

一、市场环境变了

从2022年上半年开始,我们这些在toB/toG市场的从业者就已经可以感受到不一样——市场环境和之前十年已经不一样。到了今年,市场情况的变化已经可以让所有人都感觉到了异常,大多数人都认为“大环境”不好,但我认为这才是一个健康市场的开始。

以往的市场

以我所在的数字文旅行业的发展为例,讲讲前面十年在toG或者toB(大B)行业企业的众生相。这里先申明,是toG/toB行业,toC的逻辑不在这一章节讨论范围。

从1999年开始国家提出西部大发展战略,到后面多年的持续投入,西部整体经济得到了很大发展,而且高速、物流等基础建设已经达到一定水平,下一步就是在西部发展市场和消费。在这个大背景下,旅游行业,特别是景区的发展就非常顺理成章,因为从某种角度上说景区旅游是很简单:发掘地方特色文化价值和山水风光,围起来卖票(实际当然没这么简单)。这个行业绿色环保、解决本地就业、拉动东部富裕消费者到中西部消费。

1

图1:旅游资源开发相对其他需要产业带发展的行业来说见效更快

我进入数字文旅行业是在2015年初,更准确地说当时这个行业是叫做“智慧旅游”,2014年也被称为“智慧旅游元年”。之所以产生这个行业,宏观上说是因为时代发展需要,但是我觉得真正推动这个行业产生的三件事:

  • 前面说的中西部大发展,基础设施完成之后需要建立市场和消费,旅游是其中一个很好的切入点;
  • 国家在政策层面对智旅行业进行规范化管理,包括2007的首批5A景区评选,对景区的建设和管理运营有了明确监管条例;
  • IBM提出“智慧地球”,包括在斯德哥尔摩搞了“智慧城市”,智慧系列席卷全球,所有行业都希望结合数字化降本增效,规范管理。

ibm

图2:当年IBM主导的智慧地球

智慧旅游,从分类上来说属于智慧地球——智慧城市——之下的第三级分类,平行的分类包括智慧医疗、智慧教育、智慧交通等等。到了2016年,这个行业又迎来了新一波刺激——全域旅游。所以现在回顾,从2015年到2021年这段时间,形成整个行业繁荣的因素中,监管、评级等政策性项目建设占比非常大。

具体来说,这一时期很多项目、很多产品都没有充分经历过市场竞争,没有很好达到客户价值。我们行业内有句玩笑话叫“交钥匙工程”,这和房地产行业不一样,这个交钥匙指的是交付完成之后就锁柜子里了,只有在领导视察或者节假日等特殊日子才会打开使用。这一时期如果你从事的是销售、售前工程师或者产品经理,那学习政策是必修课,公司内部也经常会请一些专家来做政策解读。因为研究透了政策,你就知道了钱从哪里(什么政策)来,要怎么样(符合政策方向)把钱赚过来。对于企业来说,顺着这个方向有时候只要做好两件事——搞定关键人决策、搞定专家评审,基本上就把项目拿下了。至于交付的产品是否好用,整体实施是否良好,那并不重要,因为拍板的人并不是使用的人。

这一时期,就好比煤矿不仅有煤,而且还很好挖,他们轻松给你挖好堆放着了,等着你来运(去卖)。

before

图3:以往的市场,客户已经把煤准备挖好了,企业往往只需要搞定决策人就可以拉走卖。

现在的市场

但是就近几年来看,市场环境变化很快。

最显眼的是全球都在经历经济低潮期,国内的地产行业和外贸出口都有下滑,加上城投债等影响,都间接影响到了很多政策性投资——煤矿老板也要自己下场挖煤了,对于卖煤的人(企业)来说,最直接的表象就是今年新增项目量急剧下降。而后续如果要拿到好的单子,企业就不是像以前那样只要搞定几个关键人就可以了,你能帮你的客户赚钱,你的客户才会选择你。也就是说,运煤的人要有能帮助煤矿主一起挖煤的能力,他才能把煤运走。

now

图4:现在的市场,企业需要帮客户赚钱才能赚到钱。

这看上去很残酷吧,但我觉得这才是健康的商业本质啊!

企业赚取利润的手段就是输出“客户需要的价值”,而你的客户也一样,他们(如果是景区)也是给游客提供价值才能收益的,所以你要体现价值,最好就是帮助你的客户赚钱,让你客户的客户感受到你输出的价值。健康的商业模式里,企业需要面向客户价值。只有最终用户——游客觉得有价值,那么企业才能赚到钱,而不是仅仅靠搞定少部分人就把钱赚了。

对于企业来说,也会越来越健康,会将销售和产研做的更加平衡。还记得在多年前,在当时的公司里面,就有销售在公开场合说产品是什么不重要,就是一堆垃圾我也能卖出去。所以那些年,销售天然是比产研“高人一等”的,我深有体会。

2

图5:多年前制作的队服,面向领导编程(借鉴面向对象编程Object-Oriented),抗议开发者的工作产出不是面向用户价值的

面向用户价值

过往的传统企业并不是完全面向用户价值的,说的更直白一些,他们更多是面向领导或者面向投资人

近一年在独立开发者圈子里混,看到的却是另一番景象:他们的产品体量不大,但在用户需求上非常聚焦,几乎就是专注解决一个用户痛点。这样的产品也就是我认为的面向用户价值的产品。独立开发者不可能搞定一两个决策人就可以将产品卖出去,他们向来是输出用户价值来交换收益的,所以他们天然就是帮客户赚钱(或解决痛点)的人

在新的时代,独立开发者应该是最不需要悲观的人群。温室里的花朵才需要风调雨顺,野百合没那么依赖春天!

二、付费渠道更加完善

我的记忆中,在App Store出来之前软件开发者收费的渠道是很少的。Windows上各种破解软件,而且最关键的是大家的付费意愿很低,使用破解版好像“天经地义”,难道让我到邮局给开发者汇款吗?

而现在,App Store应该依然是独立开发者最赚钱的收费渠道。但除此之外,很多专业软件的付费渠道也很完善了,国内包括支付宝(小明的knife4j已经接通了支付宝购买)、微信支付和银联支付,国外包括Stripe、PayPal等,都可以为开发者建立完善的收费渠道。SaaS服务也越来越多,

因为收费渠道日趋完善,国内的软件使用者付费意愿也在显著提升。我现在几乎已经没有盗版软件,系统是Mac和Ubuntu,Office用的是398元/年的365版本,连Typora(付费)、IDEA(开源开发者license)和Adobe产品等也都不再是盗版。

在付费机制上,对独立开发者同样是利好!

三、AI赋能超级个体

有个今年新开的播客叫《硬地骇客》,在这方面已经做了很好的总结,大家可以去小宇宙上听听。

3

图6:硬地骇客播客节目

下面我就贴一下它上面记录的可以帮助独立开发者成为更强个体的工具和服务,还是免费的:

  • Resend: 介绍了Resend作为一个邮件解决方案。
  • Paddle: 提到了Paddle作为一个订阅支付方案。
  • Unicorn: 提到了Unicorn Platform,适用于制作宣传页面。
  • Jamstack: 讨论了Jamstack的概念和生态。
  • SupaBase: 介绍了SupaBase作为一个综合解决方案。
  • Unicorn: 提到了Unicorn Planform,适用于制作宣传页面。
  • 图片: 在讲述营销和设计工具时提到了canva,一个设计SaaS工具。
  • Notion: 介绍了Notion作为一个在线文档工具。
  • Product Hunt: 提到了Product Hunt作为宣传软文的平台。
  • CDN: 讨论了选择CDN服务商的问题。

除此之外,ChatGPT和Copilot在代码层面带来的生产力提升已经无需赘述。Midjourney在产品设计上也帮了我很多忙,在产品思考阶段就帮助我把使用场景给描绘出来了,人家还以为我请了模特拍摄的。

4

图7:一个新产品在构思阶段用midjourney做的使用场景图

AI和SaaS工具的兴起,使得之前我们需要的很多能力可以通过服务获得,这不是一个独立开发者的好时代吗!


但是我并不是独立开发者的拥趸,一个人可以走的很快,但是一群人才能走的更远。

对于微小规模的团队来说,尽量保证大家是多面手,最简洁的配置应该是这样的:

  • 3人:1前端+1后端+1市场
  • 3人:1全栈+1产品+1市场
  • 2人:最小的配置是1全栈+1产品兼市场。

5

图8:一群人才能走的更远

四、独立开发团队需要注意什么

好了,上面说了为什么现在是独立开发者(团队)的好时代,主要是三点:

  • 接下来的市场环境对面向用户价值的开发者会更友好;
  • 付费渠道更加完善,用户付费意愿比以前高;
  • AI和SaaS帮助开发者补齐能力,更容易成为超级开发者。

但是真正去做一个独立开发者(团队),依然还是困难重重,这里给大家提几点我觉得可以提高成功率的方法。

01 需求向左,技术向右

独立开发者由于团队配置上比较简约,所以很多时候没有一个完善的市场调研、种草和推广团队,也就是说,我们不太可能去做一个全新的产品,然后自己去培育市场,那往往需要大量的市场推广经费。更理智的做法是找到一个已经存在的市场,找到这个市场现有的产品,研究它们的优势与不足,看看是否可以用我们自己对产品的看法,结合新技术,实现对现有市场产品的明显提升。

我虽然不是独立开发者,但是带的也是一个创新团队。我们最近做的新产品就是瞄准了一个已经存在二十多年的市场,结合AI以及我们自己对这个行业用户的理解,创造了一个新的产品。目前从早期市场探索来说,反响非常不错。对我们来说,寻找一个已经存在的市场,就意味着我知道新产品的使用场景是什么(或者创新点可以在哪里体现),客户是谁,盈利模式是怎么样的等等。产品的成功率比我们去做一个全新市场的产品要高很多,毕竟乔布斯只有一个。

02 简洁才是硬道理

如果你的整个产品盈利模式里面,影响因素非常多,那么搞砸的几率也会高很多。比如下面这个公式,我们需要去考虑m、n、3个x和2个y。如此负责的公司带来的危险有两个:

  • 因素越多,失败几率越大。如果m或其他的因素为0,那么我们的失败就已经注定;
  • 因素越多,我们的精力会越分散,好了,到最后我们每件事情都只能做到60分,竞争力永远处于孱弱状态。

$$
R = mn(x1+x2+x3)* \frac{y1}{y2}
$$

更好的方式是把收益模式做的更简单,虽然只有一个因素是不可能的,但也要做的尽量少,比如两个,就像下面这个公式所示。

$$
R = x*y
$$

大家有没有发现,阿里云、华为云、腾讯云等等,卖的主要产品其实就是“水电”业务——云资源、计算资源等等,收益模式都是很简单的。他们不会直接去做行业领域的项目,要搞定客户,要深耕行业知识,要刺探竞对的动向,要…,这些事情更多是他们的生态合作伙伴去做的。

所以,对于独立开发者(团队)来说,收益模式尽可能简化,是可以给成功多一些机会的。

zhasuan

图9:影响因素太多,就像杂技演员手上的瓶子,很容易会搞砸

03 正确心态

做独立开发者的心态很重要,有要服务意识。

不要认为客户在无病呻吟,你的最终目标是帮他们赚钱,然后你分一杯羹,这才是一个长久、正向、健康的生态,也是我们应该有的心态。

另外就是给自己一个限度,可以是一年、两年甚至三个月,做到什么程度,你可以继续,否则就退出。成功往往是需要坚持的,所以最好心里要有准备,坚持打一个一年或两年的硬战。

04 运营:要精细不要花活

运营环节可能是很多开发者没有很深入去思考过的,容易无感,但却很重要,有时候也会很“烦人”。

我的工作给我带来了两个这方面的经历:

  • 我有两年的运营总监工作经历,而且有一款到目前已经历经四年的需要运营的产品,我们有完善的运营团队,所以我知道运营对于产品的健康发展有多重要。甚至我们很多后续的业务,也是运营团队争取来的;
  • 我负责过年使用人数过千万的应用,以前我分享过一些截图,里面说的是我们的团队经常在凌晨被叫起来,然后紧急忙活。也许你们会认为这是产品设计不够灵活或系统不够稳健引起的,但真实情况是,一旦你的产品年活跃用户过千万,各种突发状况都会发生。也许某一个用户的安卓版手机无法正常使用你的小程序,就会让你疲于应付,而在没遇到他之前,你真的不可能知道这会发生问题。只能说大规模基数之下,什么怪异的情况都可能会发生。

所以,在独立开发者(团队)的产品投放之后,就需要关注运营。哪怕我们把运营缩减为仅仅是客服,如果我们自己没时间,也可以采用AI客服,或者直接找兼职大学生,总之,要把运营当回事儿。

05 去杠杆,做自己擅长的事情

最后一点就是别去做杠杆,咱们要记住自己是手艺人,不是玩资本的(至少不是现在)。我们量力而为,始终记得第一要务是创造用户价值,然后精简团队,不去做杠杆。你服务的是最终用户,而不是投资人。

踏踏实实做产品,服务用户,而不是去疯狂追热点,一会儿O2O,一会儿P2P,一会儿区块链,一会儿元宇宙,这会儿又开始大模型了,但是企业和开发者都有自己的基因属性,能这么随意切换的企业几乎没有,除了玩资本的。

我们还是踏实做好自己擅长的事情,这个社会对于真正能输出用户价值的人肯定会有好的回报的!

面对如今的市场形势,你是去找风口?首先风口不多,然后风停了之后怎么办?我自己的选择是练就自己的翅膀,而不是一味去找风口。


TorchV AI支持试用!

如您有大模型应用方面的企业需求,欢迎咨询!



谈谈RAG存在的一些问题和避免方式

关注“土猛的员外”公众号的朋友应该都知道我最近写了不少检索增强生成(RAG)的文章,远比大语言模型(LLM)要多。原因很简单,我觉得对于绝大多数的公司和开发者来说这是很务实的做法,那种需要太多显卡(Money)的事情本身就是一种门槛。所以我觉得去研究和实践基于LLM的应用更实际,而RAG就是非常好的一个方向,这对于很多企业来说是刚需。

1

RAG是各方面综合之后的最优解。

但是就像前面我说的,RAG入门很简单,但是要能让客户买单却很难,我已经看到好几个失败案例了…

下面我们来看看会有哪些方面会引起RAG的失败(下面的举例并不完全,还有一些本次来不及写了)。

1.分块(Chunking)策略和Top-k算法

一个成熟的RAG应该支持灵活的分块,并且可以添加一点重叠以防止信息丢失。一般来说,分块过程忽略了文本的内容,这就产生了问题。块的理想内容应该围绕单个主题保持一致,以便Embedding模型更好地工作。他们不应该从一个话题跳到另一个话题,他们不应该改变场景。比如长篇大论的论文和140字上限的微博内容的分析就会需要适配不同的分块策略,用固定的、不适合的分块策略会造成相关度下降

除了chunk,我们需要考虑参数top_k的影响。RAG系统使用top_k来选择得分达到多少的文本chunk才能送到LLM里面进行生成(Gen)操作。在大多数设计中,top_k是一个固定的数字。因此,如果块大小太小或块中的信息不够密集,我们可能无法从向量数据库中提取所有必要的信息。

对于熟悉机器学习模型调优的人来说会对chunk_sizetop_k非常敏感,为了确保RAG系统以最佳状态运行,需要对块大小和top_k进行调优,以确保它们是最合适的。机器学习里面参数调优的古老智慧仍然适用,唯一的区别是它们的调优成本更高。

2.世界知识缺失

考虑这样一个场景:我们正在构建一个《西游记》的问答系统。我们已经把所有的《西游记》的故事导入到一个向量数据库中。现在,我们问它:人有几个头?

最有可能的是,系统会回答3个,因为里面提到了哪吒有“三头六臂”,也有可能会说很多个,因为孙悟空在车迟国的时候砍了很多次头。而问题的关键是小说里面不会正儿八经地去描述人有多少个头,所以RAG的数据有可能会和真实世界知识脱离。

“今天的AI和机器学习真的很糟糕。人类有常识,而机器没有。”

——Yann LeCun

因此,当我们开发RAG系统时,不要让大语言模型已经知道解决方案的想法欺骗了您。他们没有。

3.多跳问题

让我们考虑另一个场景:我们建立了一个基于社交媒体的RAG系统。那么我们的问题是:谁知道埃隆·马斯克?然后,系统将遍历向量数据库,提取埃隆·马斯克的联系人列表。由于chunk大小和top_k的限制,我们可以预期列表是不完整的;然而,从功能上讲,它是有效的。

现在,如果我们重新思考这个问题:除了艾梅柏·希尔德,谁能把约翰尼·德普介绍给伊隆·马斯克?单次信息检索无法回答这类问题。这种类型的问题被称为多跳问答。解决这个问题的一个方法是:

  1. 找回埃隆·马斯克的所有联系人
  2. 找回约翰尼·德普的所有联系人
  3. 看看这两个结果之间是否有交集,除了艾梅柏·希尔德
  4. 如果有交集,返回结果,或者将埃隆·马斯克和约翰尼·德普的联系方式扩展到他们朋友的联系方式并再次检查。

有几种架构来适应这种复杂的算法,其中一个使用像ReACT这样复杂的prompt工程,另一个使用外部图形数据库来辅助推理。我们只需要知道这是RAG系统的限制之一。

4.信息丢失

如果我们看一下RAG系统中的流程链:

  1. 将文本分块(chunking)并生成块(chunk)的Embedding
  2. 通过语义相似度搜索检索数据块
  3. 根据top-k块的文本生成响应

我们会看到所有的过程都是有信息损失的,这意味着不能保证所有的信息都能保存在结果中。如上所述,由于分块大小的选择和Embedding模型的效用,分块和Embedding是有损耗的;由于我们使用的top_k限制和相似函数,检索过程不可能完美;由于内容长度的限制和生成式大语言模型的能力,响应生成过程并不完善。

如果我们把所有的限制放在一起,重新考虑一些公司即将推出的基于RAG的企业搜索,我真的很好奇它们能比传统的全文搜索引擎好多少。记住,传统的搜索引擎是很难被击败的。不久前,微软E5是第一个超越流行搜索算法BM25的LLM。

我的意思是,搜索引擎和LLM的结合是可行的,然而,简单的RAG很难比搜索引擎表现得更好。

我们的一些突破

确实,RAG是很难的,我前面的文章就说过一些注意点,包括数据处理、内容提取、分块策略、embedding模型选择等等,可以参考之前的文章《大模型主流应用RAG的介绍——从架构到技术细节》。下面是这篇文章里面的截图:

2

RAG应用中需要注意的关键点

最近我和小明对RAG做了一些广泛的测试,我在这里可以做一些简单的曝光。

a.通篇理解之后的检索生成

3

对于三网隔离的理解,原文中是需要多张PPT一起理解才能得到“三网融合”这个答案的,因为直接的答案这页PPT显示的是“三张网”。

b.多张PPT整合能力

4

对于为什么要用(爱快的优势),和连锁机构面临的挑战其实是两个PPT中的内容,我们的RAG是可以自动进行理解与整合的。

结论

RAG作为一种简单而强大的LLM应用程序设计模式,有其优点和缺点。我们确实需要彻底了解该技术才能对我们的设计充满信心。我个人的看法是,尽管所有关于LLM和惊人突破的炒作,大语言模型应该被视为企业AI架构的重要组成部分。它们不应该是主要框架本身。

LLM有限的权力是我担心的一个问题,可解释性是另一个问题。所有的大语言模型都像黑匣子一样工作。人们不知道自己是如何储存知识的,也不知道自己是如何推理的。对于我们自己玩玩的应用程序来说这不是一个主要问题,但在企业应用中却很关键。我们可以看到,越来越多的监管规则被发布,以确保AI不会造成伤害,我们必须提高基于LLM的应用在自己的适配方向上实现真正的价值。

在未来的研究中,我将探索如何将LLM与其他外部知识库(如图形数据库)相结合,以实现更难以实现的目标。


TorchV AI支持试用!

如您有大模型应用方面的企业需求,欢迎咨询!



PostgreSQL——让关系型、向量和时间序列数据三位一体

尝试PostgreSQL的原因

最近两周我和小明在研究基于大模型的检索应用RAG,小明在实操层面有一些显著的突破,RAG性能应该已经超过了绝大部分目前开源市场上的同类产品,已经可以让我们有一些小小的满意了。但是优化创新的路依然可谓道阻且长,接下来我们准备在数据库上做一些新的尝试。

基于大模型的RAG应用,我们用到的数据库类型还是挺多的,主要有以下三种:

  • 向量(Vector)数据库:大模型和深度学习的都知道,这波AI浪潮中向量是基础,是对现实时间的语义级别的表示。区别于之前的关键词搜索的字面量搜索,向量搜索(相似度)可以认为是一种懂思想的搜索;
  • 关系型数据库(RDB):这是最传统和广泛的数据库,比如传统的PostgreSQL、MySQL、Oracle数据库等,即使在现在,关系型数据库依然是当今绝大多数应用系统运行的架构基础;
  • 时间序列数据库:时序数据库在元数据过滤中发挥了重大作用,它是一种记录事件和发生时间的数据库,对于时间序列的搜索速度非常快。在RAG应用中,如果行业知识文件被切分出几万个,那么使用时间过滤就会非常重要,比如我们只需要检索2023年3月份的合同文件,那么就可以用时序数据将目标chunk从几万个里面先挑出来,再进行向量计算。

目前我们用的数据看包括MySQL、elasticsearch和redis等,因为说整体的程序体量会变得非常大,结构松散,部署难度激增。而且对于后续的内部API管理也会感觉到非常杂乱。所以我们尝试使用PostgreSQL及其扩展,这样就可以在一个数据库上搞定了。

PostgreSQL介绍

对于熟悉PostgreSQL的朋友可以不用看这一节,前面的MySQL和PostgreSQL大战(口水战)我相信也会让很多人对PostgreSQL有一些关注。我不站任何一边,因为两个数据库在不同的应用中都在被使用。

PostgreSQL是世界上最受欢迎的数据库,它有一个很好的优点:它已经在生产中使用了30多年,强大而可靠,并且它有一个丰富的工具、驱动程序和连接器生态系统。之前除了关系型数据库,我们还用过PostGIS做地图导航应用,2018年转载过PostgreSQL的时序数据库Timescaledb的中文手册。面对AI,PostgreSQL其实已经有一个向量扩展——pgvector。虽然pgvector是一个很棒的扩展(它的所有功能都是作为Timescale vector的一部分提供的),但它只是为PostgreSQL上的AI应用程序开发人员提供生产级体验的一块拼图。在向企业技术人员调研中,发现pgvector需要增强的地方还有很多。

1

下面我们来看看PostgreSQL在向量领域的一些优劣势。

专用Vector数据库的问题

像Pinecone、Weaviate、Qdrant和Zilliz这样的向量数据库受益于人们对AI应用的兴趣激增,它们专门用于大规模存储和查询矢量数据,具有独特的功能,如近似最近邻(ANN)搜索和混合搜索的索引。但随着开发人员开始在他们的AI应用程序中使用它们,使用这些数据库构建的显著缺点变得清晰起来:

  • 操作复杂性:仅为向量数据持续维护单独的数据库增加了另一层操作开销,要求团队跨多个系统复制、同步和跟踪数据。更不用说备份、高可用性和监控了。
  • 学习曲线:工程团队浪费时间学习新的查询语言、系统内部、api和优化技术。
  • 可靠性:从头开始构建一个健壮的数据库是一个巨大的挑战,而且在生产环境中尤其注重健壮。大多数小众向量数据库都是未经证实的新兴技术,长期的稳定性和可靠性值得怀疑。

用我们采访的一位开发者的话来说:

“与几乎任何其他矢量存储相比,Postgres更适合生产,更可配置,并且在操作上更透明。”- LegalTech创业公司软件工程师

使用基于PostgreSQL的Timescale Vector的优势

借助PostgreSQL在关系型数据库方面的企业级应用优势,再加上Timescale Vector的向量特性,以及时间序列数据的结合,在AI应用中使用PostgreSQL是非常划算的,特别是在RAG等检索应用中。对于关系型数据库特性和时间序列数据特性我就不在这里介绍了,下面我们看看Timescale Vector的优势:

  • 对数百万个向量的更快的相似性搜索:由于引入了一种受DiskANN算法启发的新搜索索引,Timescale Vector在99%的召回率下实现了比专用数据库快3倍的搜索速度,并且在100万个OpenAI Embeddings(1536维)数据集上比全部现有的PostgreSQL搜索索引高出39.39%到1590.33%。此外,与pgvector相比,启用产品量化可以节省10倍的索引空间。Timescale Vector还提供pgvector的Hierarchical Navigable Small Worlds (HNSW,分层导航)和 Inverted File Flat(IVFFlat,倒置文件平面)索引算法。
  • Timescale Vector优化了基于时间的向量搜索查询:利用Timescale的超级表的自动基于时间的分区和索引,有效地找到最近的Embeddings,通过时间范围或文档存在年份约束向量搜索,并轻松存储和检索大型语言模型(LLM)响应和聊天历史。基于时间的语义搜索还使您能够使用检索增强生成(Retrieval Augmented Generation, RAG)和基于时间的上下文检索,从而为用户提供更有用的LLM响应。
  • 简化的AI基础设施堆栈:通过将向量Embeddings关系型数据时间序列数据组合在一个PostgreSQL数据库中,Timescale vector消除了大规模管理多个数据库系统所带来的操作复杂性。
  • 简化元数据处理和多属性过滤:开发人员可以利用所有PostgreSQL数据类型来存储和过滤元数据,并将向量搜索结果与关系数据连接起来,以获得更多上下文相关的响应。在未来的版本中,Timescale Vector将进一步优化丰富的多属性过滤,在过滤元数据时实现更快的相似性搜索。

2

在这些针对矢量工作负载的创新之上,Timescale vector提供了一个强大的、生产就绪的PostgreSQL云平台,具有灵活的定价、企业级安全性和免费的专家支持。

简单介绍一下DiskANN算法

上面提到了DiskANN算法,那我在文章就必须简要的补充一下。

当前最先进的近似最近邻搜索(ANNS)算法生成的索引必须存储在主存储器中以实现快速高查全率搜索——这使得它们非常昂贵,并且限制了数据集的大小。

DiskANN基于图形的索引和搜索系统,它只需要64GB RAM和廉价的固态硬盘(SSD),就可以在一个工作站上索引、存储和搜索十亿个点的数据库。与之前的认知相反,我们证明了DiskANN构建的基于SSD的索引可以满足大规模神经网络的所有三个要求:

  • 高召回率
  • 低查询延迟
  • 高密度(每个节点索引的点)。

在十亿个点SIFT1B大神经网络数据集上,DiskANN服务QPS>5000;在16核机器上,平均延迟为3ms, 95%+ 1-recall@1,其中最先进的十亿点ANNS算法具有类似的内存占用,如FAISS[18]和IVFOADC+G+P[8],稳定在50%左右1-recall@1。另外,在高召回率的情况下,与最先进的基于图的方法(如HNSW[21]和NSG[13])相比,DiskANN在每个节点上可以索引和服务5 - 10倍的点。最后,作为整个DiskANN系统的一部分,我们引入了Vamana,这是一个新的基于图的ANNS索引,它比现有的图索引更通用,甚至对于内存索引也是如此。

结合LlamaIndex使用Timescale Vector

以下结合LlamaIndex实操的内容摘自LlamaIndex创始人Jeff Liu的blog

在LlamaIndex中使用Timescale Vector的DiskANN、HNSW或IVFFLAT索引非常简单。

简单地创建一个Timescale Vector矢量存储,并添加数据节点,你想查询如下所示:

1
2
3
4
5
6
7
8
9
from llama_index.vector_stores import TimescaleVectorStore

# Create a timescale vector store with specified params
ts_vector_store = TimescaleVectorStore.from_params(
service_url=TIMESCALE_SERVICE_URL,
table_name="your_table_name",
time_partition_interval= timedelta(days=7),
)
ts_vector_store.add(nodes)

然后运行:

1
2
# Create a timescale vector index (DiskANN)
ts_vector_store.create_index()

这将使用默认参数创建一个Timescale Vector索引。

我们应该指出,“索引”这个术语有点过多了。对于许多vectorstore,索引是存储数据的东西(在关系数据库中通常称为表),但在PostgreSQL世界中,索引是加速搜索的东西,我们在这里使用后一种含义。

我们还可以在create_index 命令中指定创建索引的确切参数,如下所示:

1
2
# create new timescale vector index (DiskANN) with specified parameters
ts_vector_store.create_index("tsv", max_alpha=1.0, num_neighbors=50)

这个Timescale Vector的新DiskANN启发矢量搜索索引的优点包括:

  • 在PostgreSQL中以99%的准确率更快地进行向量搜索。
  • 优化运行在磁盘上,而不仅仅是在内存使用。
  • 量化优化兼容PostgreSQL,减少向量大小,从而缩小索引大小(在某些情况下10倍!),加快搜索。
  • 高效的混合搜索或过滤附加维度。

有关Timescale Vector的新索引如何工作的更多信息,请参阅这篇博客文章

Pgvector被打包为Timescale Vector的一部分,因此您也可以在LlamaIndex应用程序中访问Pgvector的HNSW和IVFFLAT索引算法。从LlamaIndex应用程序代码中方便地创建ANN搜索索引的能力使得创建不同的索引和比较它们的性能变得容易:

1
2
3
4
5
6
7
# Create an HNSW index
# Note: You don't need to specify m and ef_construction parameters as we set smart defaults.
ts_vector_store.create_index("hnsw", m=16, ef_construction=64)

# Create an IVFFLAT index
# Note: You don't need to specify num_lists and num_records parameters as we set smart defaults.
ts_vector_store.create_index("ivfflat", num_lists=20, num_records=1000)

结合LlamaIndex添加高效的基于时间的搜索功能

Timescale Vector优化了基于时间的向量搜索,利用Timescale的超级表的自动基于时间的分区和索引来有效地按时间和相似度搜索向量。

时间通常是矢量Embeddings的重要元数据组成部分。Embeddings的来源,如文档、图像和网页,通常都有一个与之相关的时间戳,例如,它们的创建日期、发布日期或最后更新日期等等。

我们可以利用向量Embeddings集合中的时间元数据,通过检索不仅在语义上相似而且与特定时间框架相关的向量来丰富搜索结果的质量和适用性。

以下是一些基于时间的矢量检索可以改进LlamaIndex应用程序的示例:

  • **查找最近的Embeddings:**查找语义上与查询向量相似的最近的Embeddings。例如,查找与选举有关的最新新闻、文件或社交媒体帖子。
  • **时间范围内搜索:**限制相似性搜索仅针对相关时间范围内的向量。例如,询问关于知识库的基于时间的问题(“在2023年1月到3月之间添加了哪些新功能?”)。
  • **聊天记录:**存储和检索LLM响应历史。例如,聊天机器人的聊天记录。

让我们看一个在git日志数据集上执行基于时间的搜索的例子。在git日志中,每个条目都有时间戳、作者和有关提交的一些信息。

为了说明如何使用TimescaleVector的基于时间的矢量搜索功能,我们将询问有关TimescaleDB的git日志历史的问题。每个git提交条目都有一个与之相关的时间戳,以及消息和其他元数据(例如,作者)。

我们将演示如何使用基于时间的UUID创建节点,以及如何使用Timescale Vector Vector存储运行带有时间范围过滤器的相似性搜索。

从git日志中的每个提交创建节点

首先,我们使用Pandas从demo CSV文件(https://s3.amazonaws.com/assets.timescale.com/ai/commit_history.csv)加载git日志条目:

1
2
3
4
5
6
7
import pandas as pd
from pathlib import Path


# Read the CSV file into a DataFrame
file_path = Path("../data/csv/commit_history.csv")
df = pd.read_csv(file_path)

接下来,我们将为git日志数据集中的每个提交创建类型为TextNode的节点,提取相关信息并分别将其分配给节点的文本和元数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from llama_index.schema import TextNode, NodeRelationship, RelatedNodeInfo
# Create a Node object from a single row of data
def create_node(row):
record = row.to_dict()
record_name = split_name(record["author"])
record_content = str(record["date"]) + " " + record_name + " " + str(record["change summary"]) + " " + str(record["change details"])
node = TextNode(
id_=create_uuid(record["date"]),
text= record_content,
metadata={
'commit': record["commit"],
'author': record_name,
'date': create_date(record["date"]),
}
)
return node

nodes = [create_node(row) for _, row in df.iterrows()]

Note: 上面的代码引用了两个辅助函数来获得正确格式的内容(split_name()create_date()),为了简洁起见,我们省略了它们。完整的代码包含在本文末尾参考资料部分链接的教程中。

根据每个git提交的日期为每个节点创建uuid

我们将仔细研究用于创建每个节点的id_的辅助函数。对于LlamaIndex中基于时间的搜索,Timescale Vector使用UUID v1的“datetime”部分将向量放置在正确的时间分区中。Timescale Vector的Python客户端库提供了一个简单易用的函数,名为uuid_from_time,用于从Python DateTime对象创建UUID v1,然后我们将使用它作为TextNodes的ids

1
2
3
4
5
6
7
8
9
from timescale_vector import client
# Function to take in a date string in the past and return a uuid v1
def create_uuid(date_string: str):
if date_string is None:
return None
time_format = '%a %b %d %H:%M:%S %Y %z'
datetime_obj = datetime.strptime(date_string, time_format)
uuid = client.uuid_from_time(datetime_obj)
return str(uuid)

由于我们过去处理的是时间戳,因此我们利用uuid_from_time函数来帮助为每个节点生成正确的uuid。如果希望将当前日期和时间与节点(或文档)相关联,以便进行基于时间的搜索,则可以跳过此步骤。默认情况下,当节点被添加到Timescale Vector中的表中时,将自动生成与当前日期和时间相关联的UUID。

让我们看一下节点的内容:

1
2
3
4
5
6
print(nodes[0].get_content(metadata_mode="all"))
commit: 44e41c12ab25e36c202f58e068ced262eadc8d16
author: Lakshmi Narayanan Sreethar
date: 2023-09-5 21:03:21+0850

Tue Sep 5 21:03:21 2023 +0530 Lakshmi Narayanan Sreethar Fix segfault in set_integer_now_func When an invalid function oid is passed to set_integer_now_func, it finds out that the function oid is invalid but before throwing the error, it calls ReleaseSysCache on an invalid tuple causing a segfault. Fixed that by removing the invalid call to ReleaseSysCache. Fixes #6037

为每个节点的文本创建矢量Embeddings

接下来,我们将创建每个节点内容的向量Embeddings,这样我们就可以对与每个节点相关联的文本执行相似性搜索。我们将使用OpenAIEmbedding模型来创建Embeddings。

1
2
3
4
5
6
7
8
9
# Create embeddings for nodes
from llama_index.embeddings import OpenAIEmbedding
embedding_model = OpenAIEmbedding()

for node in nodes:
node_embedding = embedding_model.get_text_embedding(
node.get_content(metadata_mode="all")
)
node.embedding = node_embedding

加载节点到Timescale Vector矢量存储

接下来,我们将创建一个“TimescaleVectorStore”实例,并将我们创建的节点添加到其中。

1
2
3
4
5
6
7
# Create a timescale vector store and add the newly created nodes to it
ts_vector_store = TimescaleVectorStore.from_params(
service_url=TIMESCALE_SERVICE_URL,
table_name="li_commit_history",
time_partition_interval= timedelta(days=7),
)
ts_vector_store.add(nodes)

为了利用Timescale Vector高效的基于时间的搜索,我们需要在实例化Timescale Vector Vector存储时指定time_partition_interval参数。此参数表示按时间划分数据的每个间隔的长度。每个分区将包含在指定时间长度内的数据。

在上面的例子中,为了简单起见,我们使用7天,但是您可以为您的应用程序使用的查询选择任何有意义的值—例如,如果您经常查询最近的向量,您可能希望使用较小的时间增量,例如一天,或者如果您查询长达十年的时间周期的向量,那么您可能希望使用较大的时间增量,例如六个月或一年。根据经验,普通查询应该只涉及几个分区,同时您的完整数据集应该适合1000个分区,但不要太过强调—系统对这个值不是很敏感。

带时间过滤器的相似度搜索

现在我们已经将包含向量Embeddings数据和元数据的节点加载到Timescale vector vector store中,并在存储向量和元数据的表上启用了自动基于时间的分区,我们可以使用基于时间的过滤器查询我们的vector store,如下所示:

1
2
3
4
5
6
7
8
9
10
# Query the vector database
vector_store_query = VectorStoreQuery(query_embedding = query_embedding, similarity_top_k=5)

# Time filter variables for query
start_dt = datetime(2023, 8, 1, 22, 10, 35) # Start date = 1 August 2023, 22:10:35
end_dt = datetime(2023, 8, 30, 22, 10, 35) # End date = 30 August 2023, 22:10:35

# return most similar vectors to query between start date and end date date range
# returns a VectorStoreQueryResult object
query_result = ts_vector_store.query(vector_store_query, start_date = start_dt, end_date = end_dt)

让我们看一下查询返回的节点的日期和内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# for each node in the query result, print the node metadata date
for node in query_result.nodes:
print("-" * 80)
print(node.metadata["date"])
print(node.get_content(metadata_mode="all"))
--------------------------------------------------------------------------------
2023-08-3 14:30:23+0500
commit: 7aeed663b9c0f337b530fd6cad47704a51a9b2ec
author: Dmitry Simonenko
date: 2023-08-3 14:30:23+0500

Thu Aug 3 14:30:23 2023 +0300 Dmitry Simonenko Feature flags for TimescaleDB features This PR adds..
--------------------------------------------------------------------------------
2023-08-29 18:13:24+0320
commit: e4facda540286b0affba47ccc63959fefe2a7b26
author: Sven Klemm
date: 2023-08-29 18:13:24+0320

Tue Aug 29 18:13:24 2023 +0200 Sven Klemm Add compatibility layer for _timescaledb_internal functions With timescaledb 2.12 all the functions present in _timescaledb_internal were…
--------------------------------------------------------------------------------
2023-08-22 12:01:19+0320
commit: cf04496e4b4237440274eb25e4e02472fc4e06fc
author: Sven Klemm
date: 2023-08-22 12:01:19+0320

Tue Aug 22 12:01:19 2023 +0200 Sven Klemm Move utility functions to _timescaledb_functions schema To increase schema security we do not want to mix…
--------------------------------------------------------------------------------
2023-08-29 10:49:47+0320
commit: a9751ccd5eb030026d7b975d22753f5964972389
author: Sven Klemm
date: 2023-08-29 10:49:47+0320

Tue Aug 29 10:49:47 2023 +0200 Sven Klemm Move partitioning functions to _timescaledb_functions schema To increase schema security…
--------------------------------------------------------------------------------
2023-08-9 15:26:03+0500
commit: 44eab9cf9bef34274c88efd37a750eaa74cd8044
author: Konstantina Skovola
date: 2023-08-9 15:26:03+0500

Wed Aug 9 15:26:03 2023 +0300 Konstantina Skovola Release 2.11.2 This release contains bug fixes since the 2.11.1 release…

成功!

请注意,只有在指定的开始和结束日期范围(2023年8月1日和2023年8月30日)内具有时间戳的向量才会包含在结果中。

下面是一些直观的原因,说明为什么Timescale Vector的基于时间的分区可以加速使用基于时间的过滤器的ANN查询。

Timescale Vector按时间对数据进行分区,并在每个分区上分别创建ANN索引。然后,在搜索过程中,我们执行一个三步过程:

  • 步骤1:过滤不匹配时间谓词的分区。
  • 步骤2:对所有匹配分区执行相似度搜索。
  • 步骤3:合并步骤2中每个分区的所有结果,重新排序,并按时间过滤结果。

Timescale Vector利用TimescaleDB的超级表,它根据时间戳自动划分向量和相关元数据。这使得通过与查询向量的相似性和时间对向量进行高效查询成为可能,因为不在查询时间窗口内的分区被忽略,通过一次过滤掉整个数据条,使得搜索效率大大提高。

当在TimescaleVectorStore上执行向量相似性搜索时,我们也可以指定一个时间过滤器,提供开始日期和时间增量,而不是指定搜索的开始和结束日期:

1
2
# return most similar vectors to query from start date and a time delta later
query_result = ts_vector_store.query(vector_store_query, start_date = start_dt, time_delta = td)

我们还可以在提供的end_date和时间增量中指定时间过滤器。此语法对于过滤搜索结果以在特定日期截止之前包含向量非常有用。

1
2
# return most similar vectors to query from end date and a time delta earlier
query_result = ts_vector_store.query(vector_store_query, end_date = end_dt, time_delta = td)

基于TimescaleVector的LlamaIndex应用中基于时间的上下文检索增强检索生成

让我们把所有内容放在一起,看看如何使用TimescaleVectorStore在我们上面检查的git日志数据集上为RAG提供动力。

为此,我们可以使用TimescaleVectorStore作为QueryEngine。在创建查询引擎时,我们使用TimescaleVector的时间过滤器,通过将时间过滤器参数vector_strore_kwargs传递,将搜索限制在相关的时间范围内。

1
2
3
4
5
6
7
8
9
from llama_index import VectorStoreIndex
from llama_index.storage import StorageContext

index = VectorStoreIndex.from_vector_store(ts_vector_store)
query_engine = index.as_query_engine(vector_store_kwargs = ({"start_date": start_dt, "end_date":end_dt}))

query_str = "What's new with TimescaleDB functions? When were these changes made and by whom?"
response = query_engine.query(query_str)
print(str(response))

我们问了LLM一个关于我们的git日志的问题,即“What`s new with TimescaleDB functions? When were these changes made and by whom?”(“TimescaleDB函数有什么新功能?”这些改动是什么时候做的,是谁做的?”)

下面是我们得到的响应,它综合了从语义搜索返回的节点和在Timescale Vector存储上基于时间的过滤:

1
TimescaleDB functions have undergone changes recently. These changes include the addition of several GUCs (Global User Configuration) that allow for enabling or disabling major TimescaleDB features. Additionally, a compatibility layer has been added for the "_timescaledb_internal" functions, which were moved into the "_timescaledb_functions" schema to enhance schema security. These changes were made by Dmitry Simonenko and Sven Klemm. The specific dates of these changes are August 3, 2023, and August 29, 2023, respectively.

这是一个强大概念的简单示例——在您的RAG应用程序中使用基于时间的上下文检索可以帮助为您的用户提供更相关的答案。这种基于时间的上下文检索对任何具有自然语言和时间成分的数据集都很有帮助。由于其高效的基于时间的相似性搜索功能,Timescale Vector可以独特地实现这一点,并且由于Timescale Vector集成,在LlamaIndex应用程序中利用它很容易。

引用

1.Timescaledb的中文手册:https://www.luxiangdong.com/2018/09/09/timescaledb-what/

2.How We Made PostgreSQL a Better Vector Database:https://www.timescale.com/blog/how-we-made-postgresql-the-best-vector-database/

3.Jeff Liu的Medium:https://medium.com/llamaindex-blog/timescale-vector-x-llamaindex-making-postgresql-a-better-vector-database-for-ai-applications-924b0bd29f0

4.微软的DiskANN算法介绍:https://www.microsoft.com/en-us/research/publication/diskann-fast-accurate-billion-point-nearest-neighbor-search-on-a-single-node/

5.Timescale Vector:https://www.timescale.com/ai

6.Timescale Vector Store的LlamaIndex教程:https://gpt-index.readthedocs.io/en/stable/examples/vector_stores/Timescalevector.html


TorchV AI支持试用!

如您有大模型应用方面的企业需求,欢迎咨询!



手工微调embedding模型RAG检索能力

本文是一篇关于如何微调embedding的文章,原作者是Wenqi Glantz

主要内容:

  • 微调big-large-en开源embedding模型;
  • 具体实现代码细节;
  • 评测最终的提升效果。

在RAG应用中,有一个我们可以去提升的环节就是——Embedding模型,我在之前的文章《大模型主流应用RAG的介绍——从架构到技术细节》也说过可以去微调embedding模型以便增强我们整体的检索能力。

最早我们用的是OpenAI的Embedding模型text-embedding-ada-002,但这个模型后面不一定可以在正式环境中使用,而且我们也没办法去微调,因此让我们在本文中探索对开源Embedding模型进行微调。

BAAI/bge-small-en

目前HuggingFace的MTEB(海量文本Embedding基准)排行榜上排名第一的Embedding模型是big-large-en,它由北京人工智能研究院(BAAI,智源)开发。它是一种预训练的transformer模型,可用于各种自然语言处理任务,如文本分类、问答、文本生成等。该模型在海量文本和代码数据集上进行训练,并在海量文本Embedding基准(MTEB)上进行了微调。

在本文中,我们将使用 big-large-en的缩小版big-small-en,这是一个384维的小规模模型(OpenAI是1500+维),具有竞争力的性能,非常适合在Google Colab中运行。大家也可以选择中文版的bge-base-zh-v1.5,只有0.1G。当然你的硬件环境允许,也可以使用1.3G的bge-large-zh-v1.5等embedding模型。

微调Embedding模型与微调LLM

与LLM(大语言模型)微调相比,big-small-en微调的实现有一些不一样,下面简单说一下异同点:

相似点

  • 两种类型的微调都遵循相同的方法,即生成用于训练和评估的数据集,微调模型,最后评估基本模型和微调模型之间的性能。
  • 使用LLM自动生成训练和评估数据集。

不同点

  • 数据集内容在LLM微调和Embedding模型微调之间有所不同。用于LLM微调的数据集包含LLM生成的问题。在微调过程中,包括问题、答案、系统prompt等在内的一系列数据将以JSON行( jsonl)文件的形式传递给要进行微调的模型。

不同的是,用于Embedding模型微调的数据集包含以下三组:

  1. queriesnode_id映射和LLM生成的问题的集合。
  2. corpusnode_id映射和相应节点中的文本的集合。
  3. relevant_docs:查询的node_id和语料库 node_id之间的交叉引用映射的集合。给定一个查询,它告诉Embedding模型要查找哪个文本节点/语料库。
  • 由于我们使用开源Embedding模型bge-small-en ,微调的前提就是要先把它下载到您的本地环境。以Google Colab为例,经过微调的模型将被下载到笔记本的根目录中。
  • 评估方法在微调Embedding模型和微调LLM之间有所不同,我们可以使用Ragas框架来衡量精准度和答案相关性。然而,当使用Embedding模型微调时,我们无法测量答案的正确性,因为我们只能为我们的问题检索相关节点。相反,我们使用一个称为“命中率”的简单度量,这意味着对于每个(query, relevant_doc)对,我们用查询检索top-k文档,如果结果包含relevant_doc,则它被认为是“命中”的。该指标可用于专有Embeddings,如OpenAI的Embedding模型和开源Embedding模型。对于开源Embedding模型,我们还可以使用来自sentence_transformersInformationRetrievalEvaluator进行评估,因为它提供了一套更全面的指标。

微调Embedding模型似乎涉及到很多问题。幸运的是,LlamaIndex(我个人感觉LlamaIndex目前的发展可能会在RAG方面打败LangChain)在最近的0.8.21版本中引入以下关键类/函数,使得微调Embedding模型变得超级简单:

  • SentenceTransformersFinetuneEngine
  • generate_qa_embedding_pairs
  • EmbeddingQAFinetuneDataset

这些类和函数为我们抽象了底层的详细集成逻辑,使开发人员能够非常直观地调用它。

微调方法

为了可视化微调BAAI/big-small-en所涉及的主要任务,让我们看看下图:

img

如图中的数值所示,主要任务包括:

  1. 通过调用 EmbeddingQAFinetuneDataset函数generate_qa_embedding_pairs,自动生成评估和训练数据集的数据。
  2. 通过传入基本模型和训练数据集来构造SentenceTransformersFinetuneEngine,然后调用其finetune函数来训练基本模型。
  3. 创建经过微调的模型。
  4. 调用向量存储索引检索器检索相关节点并评估基本模型的命中率。
  5. 调用InformationRetrievalEvaluator来评估基本模型。
  6. 调用向量存储索引检索器检索相关节点并评估微调模型的命中率。
  7. 调用InformationRetrievalEvaluator来评估经过微调的模型。

基于LlamaIndex的微调Embeddings指南(文末有链接),我们将在我们的用例中微调bge-small-en模型。

实现细节

Step 1: 生成数据集

让我们使用LLM来自动生成训练和评估的数据集。

  • Load corpus

在我们的用例中NVIDIA的SEC 10-K文件(代码中和文末都有链接)是一个169页的PDF文档(你可以用你自己的中文PDF),所以我们需要在生成数据集时将文档分成两部分——一部分用于训练数据集,另一部分用于evalals数据集。

使用单独的数据集进行训练和评估被认为是一种很好的ML实践。可以调用load_corpus函数来收集训练数据集(前90页)或eval数据集(其余页面)的节点。下面是load_corpus的代码片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
!curl https://d18rn0p25nwr6d.cloudfront.net/CIK-0001045810/4e9abe7b-fdc7-4cd2-8487-dc3a99f30e98.pdf --output nvidia-sec-10k-2022.pdf

def load_corpus(docs, for_training=False, verbose=False):
parser = SimpleNodeParser.from_defaults()
if for_training:
nodes = parser.get_nodes_from_documents(docs[:90], show_progress=verbose)
else:
nodes = parser.get_nodes_from_documents(docs[91:], show_progress=verbose)

if verbose:
print(f'Parsed {len(nodes)} nodes')

return nodes

SEC_FILE = ['nvidia-sec-10k-2022.pdf']

print(f"Loading files {SEC_FILE}")

reader = SimpleDirectoryReader(input_files=SEC_FILE)
docs = reader.load_data()
print(f'Loaded {len(docs)} docs')

train_nodes = load_corpus(docs, for_training=True, verbose=True)
val_nodes = load_corpus(docs, for_training=False, verbose=True)

请记住,在LlamaIndex中,节点和页面并不完全匹配。对于一个169页的文档,结果显示它为训练数据集解析了97个节点,为evals数据集解析了91个节点。这两个数据集的节点数量足够接近。让我们继续。

img

  • 生成合成查询和数据集

现在,让我们生成训练和评估的数据集。请注意,我们这里没有传递LLM (gpt-3.5-turbo-0613),只有OpenAI API密钥。这是因为LlamaIndex的默认LLM是gpt-3.5-turbo-0613;如果没有定义LLM,只要提供OpenAI API密钥,则默认为它。

generate_qa_embedding_pairs是一个生成数据集的方便函数。基于上面load_corpus函数返回的节点,它为每个节点生成问题(默认为每个节点两个问题,可以自定义),然后用所有三组数据构建数据集:queriescorpusrelevant_docs(queriescorpus之间的映射对应的node_id)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from llama_index.finetuning import (
generate_qa_embedding_pairs,
EmbeddingQAFinetuneDataset,
)
from llama_index.llms import OpenAI

os.environ["OPENAI_API_KEY"] = "sk-############"
openai.api_key = os.environ["OPENAI_API_KEY"]

train_dataset = generate_qa_embedding_pairs(train_nodes)
val_dataset = generate_qa_embedding_pairs(val_nodes)

train_dataset.save_json("train_dataset.json")
val_dataset.save_json("val_dataset.json")

train_dataset = EmbeddingQAFinetuneDataset.from_json("train_dataset.json")
val_dataset = EmbeddingQAFinetuneDataset.from_json("val_dataset.json")

下面是样本训练数据集的样子。注意queriescorpus在截图中是折叠的,因为每个都有超过100个数据对:

img

Step 2: 微调Embedding模型

SentenceTransformersFinetuneEngine就是为这个任务设计的。在底层,它执行多个子任务:

  • 通过构建SentenceTransformer加载预训练模型,传入BAAI/big-small-en模型id。
  • 定义数据加载器。它加载我们的训练数据集,将其解析为查询语料库relevant_docs。然后循环查询,将relevant_docs中的node_idcorpus中的文本节点进行映射,构造InputExample,其列表依次传递到创建DataLoader中.
  • 定义loss(损失函数)。它使用sentence_transformers multiplenegativerankingloss来训练检索设置的Embeddings。
  • 定义评估器。它设置了一个带有eval数据集的评估器来监控Embedding模型在训练期间的表现。
  • 运行训练。它插入上面定义的数据加载器、损失函数和评估器来运行训练。

LlamaIndex将微调Embedding模型的所有详细子任务封装在一个SentenceTransformersFinetuneEngine中,我们所需要做的就是调用它的finetune函数。下面,您可以看到展示LlamaIndex的代码片段:

1
2
3
4
5
6
7
8
9
10
11
12
from llama_index.finetuning import SentenceTransformersFinetuneEngine

finetune_engine = SentenceTransformersFinetuneEngine(
train_dataset,
model_id="BAAI/bge-small-en",
model_output_path="test_model",
val_dataset=val_dataset,
)

finetune_engine.finetune()

embed_model = finetune_engine.get_finetuned_model()

Step 3: 评估微调后的模型

如上所述,我们使用两种不同的评估方法:

  • 命中率:对每个query / relevant_doc对进行简单的top-k检索。如果搜索结果包含relevant_doc,那么它就是一个“命中”。这可以用于专有的Embeddings,例如OpenAI的Embedding模型和开源Embedding模型。请参阅下面代码片段中的evaluate函数。

  • InformationRetrievalEvaluator:一个更全面的用于评估开源Embeddings的度量套件。请参阅下面代码片段中的evaluate_st函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
from llama_index.embeddings import OpenAIEmbedding
from llama_index import ServiceContext, VectorStoreIndex
from llama_index.schema import TextNode
from tqdm.notebook import tqdm
import pandas as pd

# function for hit rate evals
def evaluate(
dataset,
embed_model,
top_k=5,
verbose=False,
):
corpus = dataset.corpus
queries = dataset.queries
relevant_docs = dataset.relevant_docs

service_context = ServiceContext.from_defaults(embed_model=embed_model)
nodes = [TextNode(id_=id_, text=text) for id_, text in corpus.items()]
index = VectorStoreIndex(nodes, service_context=service_context, show_progress=True)
retriever = index.as_retriever(similarity_top_k=top_k)

eval_results = []
for query_id, query in tqdm(queries.items()):
retrieved_nodes = retriever.retrieve(query)
retrieved_ids = [node.node.node_id for node in retrieved_nodes]
expected_id = relevant_docs[query_id][0]
is_hit = expected_id in retrieved_ids # assume 1 relevant doc

eval_result = {
"is_hit": is_hit,
"retrieved": retrieved_ids,
"expected": expected_id,
"query": query_id,
}
eval_results.append(eval_result)
return eval_results


from sentence_transformers.evaluation import InformationRetrievalEvaluator
from sentence_transformers import SentenceTransformer

def evaluate_st(
dataset,
model_id,
name,
):
corpus = dataset.corpus
queries = dataset.queries
relevant_docs = dataset.relevant_docs

evaluator = InformationRetrievalEvaluator(queries, corpus, relevant_docs, name=name)
model = SentenceTransformer(model_id)
return evaluator(model, output_path="results/")
  • 评测OpenAI

现在,让我们评估一下OpenAI的Embedding模型text-embedding-ada-002。代码如下:

1
2
3
4
5
6
ada = OpenAIEmbedding()
ada_val_results = evaluate(val_dataset, ada)

df_ada = pd.DataFrame(ada_val_results)

hit_rate_ada = df_ada['is_hit'].mean()

结果:

img

  • 评测BAAI/bge-small-en
1
2
3
4
5
6
7
8
bge = "local:BAAI/bge-small-en"
bge_val_results = evaluate(val_dataset, bge)

df_bge = pd.DataFrame(bge_val_results)

hit_rate_bge = df_bge['is_hit'].mean()

evaluate_st(val_dataset, "BAAI/bge-small-en", name='bge')

结果:

img

  • 评估微调后的model
1
2
3
4
5
6
7
8
finetuned = "local:test_model"
val_results_finetuned = evaluate(val_dataset, finetuned)

df_finetuned = pd.DataFrame(val_results_finetuned)

hit_rate_finetuned = df_finetuned['is_hit'].mean()

evaluate_st(val_dataset, "test_model", name='finetuned')

查看结果:

img

  • Summary of results

把评测结果放在一起,让我们仔细看看。

命中率:我们的微调模型比其基本模型bge-small-en的性能提高了1.29%。与OpenAI的Embedding模型相比,我们的微调模型的性能仅低了4.85%。

img

InformationRetrievalEvaluator结果:经过微调的模型比其基本模型的性能提高了5.81%。与基本模型相比,微调模型对这30多个指标列中的每一个都有更好的数字。

img

总结

在本文中,我们探讨了微调RAG管道的Embedding模型所涉及的步骤。我们使用开源的sentence_transformers模型BAAI/big-small-en作为我们的基本Embedding模型,介绍了如何生成用于训练和评估的数据集,如何对其进行微调,以及如何评估基本模型和微调模型之间的性能差异。

评估结果表明,微调Embedding模型的性能比基本模型提高了1-6%,与OpenAI的Embedding模型相比,微调模型的性能损失仅为4.85%。这种性能提升可能因数据集的质量和数量而异。

我们还简要探讨了LlamaIndex的最新版本,该版本对任何Embedding模型的线性适配器进行了微调,从而提高了性能并避免了在RAG管道中重新嵌入文档。

引用


Update: 2024-01-26

我们的TorchV Bot产品目前已经开始试用了,详情可以点击:https://www.luxiangdong.com/2024/01/25/lanuch-1
目前只接受企业用户试用,需要您填写一些信息,必要信息如下:

邮箱: 用来接收地址和账号
如何称呼您:
所服务的公司:
您的职位:

当然,如果您可以告诉我们您的使用场景,我们将更加感激!
对了,可以发送到yuanwai@mengjia.net
另外,也可以直接加我微信(lxdhdgss)联系我。


TorchV AI支持试用!

如您有大模型应用方面的企业需求,欢迎咨询!