2024年3月

去年(2023年)是大模型爆发元年。但是大模型具有两个缺点:缺失私有领域知识和幻觉。缺失私有领域知识是指大模型训练时并没有企业私有数据/知识,所以无法正确回答相关问题。并且在这种情况下,大模型会一本正经地胡说八道(即幻觉),给出错误的回答。
那么如何解决这两个缺点?目前主要有两种方法,微调和RAG。微调是指使用企业私有数据/知识基于现有大模型训练出一个新的模型,然后我们使用这个新的模型来回答用户问题。RAG是检索增强生成(Retrieval-Augmented Generation)的缩写,是指在用户提问时系统后台根据用户问题检索相关数据/知识,然后将所检索到的相关数据/知识加上用户问题一起交给大模型处理。
我们可以看到,无论是哪种方法,将企业私有数据/知识传给大模型都是位于整个流程的上游,这个阶段的任何失误都会直接影响到整个流程的下游,最终影响整个问答的准确率。
为了让读者更形象地了解这个环节,本文将以一份上市公司年报PDF为例进行讲解。
本文选择了恒生电子(600570)2022年的年度报告(修订版)做为示例,大家可以在巨潮资讯搜索下载这份PDF文件。

使用传统PDF解析工具解析PDF

首先我们使用传统的PDF解析工具解析PDF。
传统的PDF解析库相当地多,我们就不一一罗列和分析了。这里我们仅以目前最流行的大模型框架LangChain所支持的其中一个解析库PyPDF为例。
我们使用LangChain并选择PyPDFLoader来读取文件“恒生电子:恒生电子股份有限公司2022年年度报告(修订版).PDF”

from langchain_community.document_loaders import PyPDFLoader
loader = PyPDFLoader("example_data/恒生电子:恒生电子股份有限公司2022年年度报告(修订版).PDF")
pages = loader.load_and_split()

存在的问题

我们发现通过PyPDFLoader读取的结果存在很多问题,这里仅以PDF第6页和第7页的“七、近三年主要会计数据和财务指标”为例:

通过PyPDFLoader读取的结果为(为节省篇幅,这里去除了所有空白换行符):

七、 近三年主要会计数据和财务指标
(一) 主要会计数据
单位:元 币种:人民币
主要会计数据 2022年 2021年
本期比上年 同期增减
(%)
2020年
营业收入 6,502,387,143.49 5,496,578,624.88 18.30 4,172,645,156.56
归属于上市公司股东的 净利润 1,091,088,379.58 1,463,538,930.14 -25.45 1,321,735,522.48
归属于上市公司股东的 扣除非经常性损益的净 利润
1,144,419,161.05 946,569,672.55 20.90 733,590,924.61
经营活动产生的现金流 1,138,192,779.96 956,789,306.14 18.96 1,397,902,270.41
2022 年年度报告
**7** /
**211** 量净额
2022年末 2021年末
本期末比上 年同期末增
减(%)
2020年末
归属于上市公司股东的 净资产 6,811,761,050.50 5,695,031,051.05 19.61 4,554,029,323.59
总资产 13,004,578,298.67 12,079,908,312.76 7.65 9,971,144,688.34

我们可以发现以下问题:

  1. PDF换页导致表格行头被断开:第四行的行头“经营活动产生的现金流量净额”被断成两截,生生加入了“1,138,192,779.96 956,789,306.14 18.96 1,397,902,270.41 2022 年年度报告 **7** /**211** ”一长串内容。我们可以看到,这一段内容中,干扰信息比正常信息还要多一两倍。遇到这种问题,再强大的大模型都无法正确处理!
  2. 丢失表格列头信息:表格最后几行数据完全无法与表格列头建立关系。从而导致大模型无法识别这些数据所代表的意义。
    除了PyPDFLoader之外,其它传统的PDF解析工具的处理结果也差不多。
    计算机科学与信息通信技术领域有一句习语:垃圾进,垃圾出 ( Garbage in, garbage out ,缩写GIGO )。我们可以看到,在这个示例中,因为传统PDF解析工具的局限性,生生地把精华处理成垃圾喂给了大模型,大模型自然而然地只能给出垃圾给用户了。那么,我们如何解决这些问题呢?

使用大模型时代的PDF解析工具

既然传统的PDF解析工具跟不上时代的发展,那么我们就使用大模型时代的PDF解析工具。
截至目前为止,大模型时代的PDF解析工具有两类:

  1. 原生支持英文,对中文支持尚待改进的:Marker、nougat(Facebook)、Layoutlmv3(Microsoft)、DocLayNet(IBM)、ByT5(Google)
  2. 原生支持中文,并以中文大模型为试验对象的:Vary、PDF4AI.cn
    这里以Marker(
    https://github.com/VikParuchuri/marker
    )为代表简单描述一下第一类工具。Marker的原理是将PDF解析成Markdown喂给大模型。因为Markdown可以保留表格的结构信息,所以能够解决前面示例中的问题。
    然而很遗憾的是,Marker目前尚未支持中文,所以无法用于本文中的示例。不过大模型时代技术发展迅速(LangChain几乎每天更新一个版本),Marker才诞生三个月就已经更新多个版本,相信很快就会支持中文了。
    至于nougat、Layoutlmv3、DocLayNet、ByT5都是类似于Marker的工具,因为篇幅原因,这里就不过多描述了。
    这些工具有些明确表示目前不支持中文,有些虽然支持中文,但是实际效果可能不能满足读者们的预期。
    既然以上工具对中文支持不友好,那么有没有支持中文的类似工具呢?旷视推出了Vary。
    Vary是以中文大模型(阿里巴巴的通义千问Qwen)为试验对象的,并且优先和原生支持中文,从Vary的官网可以看到,Vary的Demo都是先展示中文再展示英文的。
    Vary的官网是https://varybase.github.io/ , 比较奇怪的是,虽然各大自媒体号说这是旷视推出的模型,然而这个官网并没有表现出这一点。同时Vary推出没多久,目前改进空间还很大,笔者将持续关注。
    另一个工具是PDF4AI.cn(
    https://www.pdf4ai.cn
    )。PDF4AI.cn的原理与Marker、Vary是一样的,都是将PDF解析成Markdown喂给大模型。
    PDF4AI.cn分为免费版和专业版。截止目前为止,PDF4AI.cn的免费版未能解决以上示例中的问题。
    PDF4AI.cn专业版可以解决以上示例中的问题。以下是PDF4AI.cn专业版的处理结果(为节省篇幅,这里去除了所有空白换行符):
七、 近三年主要会计数据和财务指标
(一) 主要会计数据
单位:元 币种:人民币  
| 主要会计数据 | 2022年 | 2021年 | 本期比上年同期增减 (%) | 2020年 |
| ------ | ----- | ----- | -------------- | ----- |
| 营业收入 | 6,502,387,143.49 | 5,496,578,624.88 | 18.30 | 4,172,645,156.56 |
| 归属于上市公司股东的净利润 | 1,091,088,379.58 | 1,463,538,930.14 | -25.45 | 1,321,735,522.48 |
| 归属于上市公司股东的扣除非经常性损益的净利润 | 1,144,419,161.05 | 946,569,672.55 | 20.90 | 733,590,924.61 |
| 经营活动产生的现金流量净额 | 1,138,192,779.96 | 956,789,306.14 | 18.96 | 1,397,902,270.41 |
| 归属于上市公司股东的净资产 | 6,811,761,050.50 | 5,695,031,051.05  | 19.61 | 4,554,029,323.59 |
| 总资产 | 13,004,578,298.67 | 12,079,908,312.76  | 7.65  | 9,971,144,688.34 |

熟悉Markdown的朋友们可以看出,以上处理结果将PDF里面的表格转换为Markdown里面的表格。
为了让不熟悉Markdown的朋友们有一个直观的认识,笔者使用Markdown编辑工具将以上结果可视化一下:

现在我们可以清晰地看到,表格信息被完整保留,从而解决了以上示例中的问题。

总结

与传统的PDF解析工具相比,大模型时代的PDF解析工具将PDF解析成Markdown,从而保留一些结构化的信息(例如表格和图片),再喂给大模型,从而避免把精华处理成垃圾,避免垃圾进,垃圾出。
去年(2023年)大模型才爆发,因此这些大模型时代的PDF解析工具都是新生产物,有很多地方尚待改进,不过它们也更新迅速,感兴趣的读者可以持续关注。

工作中不存在努力一说

前年官方发布了这样一个视频,标题是:
真正躺平的是极少数,不懈奋斗的是大多数
。尽管在评论区存在一片嘘声,但是笔者从自身对周围的观察来看,官方并没有粉饰结果,
在目前这样一个不景气的环境下,同学们仍然还在努力和生活对线,笔者首先向你们表达最真诚的敬意。不管结果怎么样,你们都是最棒的

但是想必不用笔者提醒,大多数同学也发现了,也许一定程度上提高工作的努力程度可以换取更多的回报,但是如果再往上除了把自己搞得累的半死,似乎没有太多的收益。笔者想提醒大家,
努力就有回报,那是典型的学生思维
。工作中是没有努力这种说法的,想必很多同学脑中也已经有了这个萌芽,不然不会说出:
如果勤能致富,世界首富就是头驴
,这样的调侃。

大家这时候肯定觉得很奇怪,难道笔者是要大家天天上班摸鱼嘛,当然不是,在讨论今天的话题之前,笔者先给大家设定这么个场景。大家平常喜欢自嘲要和生活对线,那么我们就把工作比喻成一场战争:大家可以去简单查阅一下,历史上有名的胜仗有多少是靠着简单的猛打猛冲打赢的,答案当然是少之又少,毕竟如果战争这么简单,就不会出现那么多以弱胜强的经典战役了。如果大家现在是一支军队的指挥官,你的理念是努力就能打胜仗,那你的打法一定是一路猛冲,那敌军只要来个诱敌深入,你能打赢才有鬼。

那么在工作中也是一样的道理,这里可以用笔者最喜欢的例子来说明,
当年清政府割地赔款的时候,那些打算盘的人都挺努力的,但是除了让西方列强更看不起没有任何用处

所以,在这里大家一定要有一个理念:
工作中是没有努力这种说法的。

学会多维度视角看问题

先给大家设定一个场景,形象说明一下什么叫内卷,现在同学们深处一家美食街,人人都是饭店的老板,我们假设所有的饭店都是无论同样的水平,出餐时间这个标准的话一开始大家都是15分钟出餐,后来来了一家不速之客,人家5分钟就出餐,但是其他的标准都和其他饭店一样,短期内赚了更多的钱,后来所有的饭店都去跟着学,人人都是5分钟出餐,结果就是大家赚的钱还是和原来一样,但是大家都比以前累的多。

在此大家不妨思考一下,如果一个人有闲情逸致进来你饭店吃饭,他会很在意你的出餐时间嘛。他真这么着急干嘛不点外卖呢。所以在不着急的情况下你15分钟出餐一定程度上是可以接受的。在这过多的下功夫其实没有过多的意义。

那如果你想赚更多的钱你应该怎么做,那从饭店的角度来讲能下功夫的角度很多呀。

食品安全:人家一万个中毒一个人,你一百万个中毒一个人

菜色品种:人家只卖川菜,你粤菜,鲁菜,川菜,西餐,快餐都有

优惠活动:人家不办优惠,你搞个月费畅吃

....

我们参加工作归根到底是参加商业活动,商业活动是很复杂的,从来没有单一的标准可以决定价值的高低

任何工作都分为四步走

那么工作中的说法到底是什么呢,笔者的眼里任何工作都分为四个阶段:了解情况,做出决策,抓住核心,运用手段。

了解情况

很多同学觉得这个步骤是不是多余的,笔者认为一点都不多余,这里包含两个原因

  • 其一,一般这都是取决于领导直接下发的具体任务。同学们最喜欢说的一句话就是:
    真正的大师总是怀揣一颗学徒的心
    ,但是往往说说容易,做到却很难,现在请同学们回到小学去,想想小学教的很多东西,给文章分结构,圈出题目中的关键词,这些操作都是让你了解自己在做题目的时候首先搞清楚情况,如果题目要你写反义词,你写个近义词,那你写的再漂亮,老师在讲题目的时候也只会把你拉出来批判一通。
    很多同学其实在工作的时候经常会出现,做了半天连要做什么都不知道
    。那这种努力除了浪费时间还有什么用。
  • 其二,如果领导是放养的,领导说他只要一个最终结果,中间的任何阶段性成果他都无所谓。不知道同学们脑中有没有这样一个概念,工作中你的回报很大程度上取决于你创造的价值,
    如果用户要的是西瓜,你给个苹果,那用户也不会念你的好
    ,那么什么叫有价值呢,那就是工作中什么事情值得做,什么事情不值得做,但这个事情从来没有一个定论,都是需要跑到实际工作中去具体了解。例如有些用户他无所谓项目快不快,只在乎是否美观,而另一些用户就更注重实际,只在乎好不好用,不在乎外表

那如何去了解情况呢,笔者在前面也介绍了,学会多维度视角看问题。

做出决策

在唐朝武德年间,平定杨文干叛乱以后,突厥侵扰中原,此时的唐朝面对眼前的局面有两条路可走:第一:将首都迁到南方,避其锋芒,励精图治以后反攻长安,第二:集结现有的唐朝兵力和突厥决一死战。

某种程度上,此时做出的选择将直接影响整个大唐王朝的命脉,后面的结果我们都知道了,唐朝并没有选择做缩头乌龟,而是派出李世民为首的武装力量成功退敌,为后面长达150年的盛世发展奠定了有生力量。想必不用我说大家也能猜到,迁都的事情,那麻烦的不是一星半点,放在古代迁都因为踩踏事件死个几万人更是家常便饭。

而且如果当时迁都,那么突厥必然轻松南下,经济发达的,人口稠密的北方一旦落入突厥之手,必然对南方形成得陇望蜀之势,唐朝的命数不会超过50年。那么在这样的情况下,迁都做的再好再努力,又有什么用。

因此这里实际上才是笔者认为工作中最需要强调的地方,决策如果做错了,再努力都没用,为什么画图纸的比打螺丝的挣钱道理就在这,画图纸的员工,他相当于是项目的决策者,如果他的图纸有问题,那下面打螺丝的工人再努力也只会做出一堆破烂。

因此,同学们一定要养成,决策比努力重要的理念。一个好的决策可以胜过千千万万的努力,当然如何做出决策其实没有什么特别的标准,硬要说有就是了解清楚情况以后做决定。

抓住核心

国产游戏隐形守护者游戏中有这样一个章节,在澳门商贸团事件中,庄晓曼交代给肖途直接的任务是杀死方敏,制造商贸团和日本人的公开矛盾,从而顺水推周拒绝和日本人合作,这样就可以以最小的代价营救商贸团。

当然玩过游戏的同学都知道,如果杀死方敏自己也得死,但是放开游戏不谈,即便在现实中作为卧底,随随便便在公开场合杀人也是非常冒险的举动

所以在游戏中肖途就很聪明的抓住了核心:促使任务成功有两个关键信息:第一,商贸团必须要受到人身威胁,但是并不需要有人牺牲。第二,犯人必须和日本人有联系,从而制造借口。所以同学们发现没有,这就是任务的核心,抓住了核心后面很多事情就很好办。最后的结果我们都知道了,肖途成功诬陷了汉奸胡一彪想绑架方敏未遂,从而化解了这次危机

在这里就可以给我们一些启发,用户和领导布置需求的时候往往都是说的比较直白和表面化,
其实同学们要学会抓住核心,并不需要和领导说的完全一样
。因为领导往往不会接触项目,很多事情他自己也是不清楚的,如果你非要按领导说的做的一模一样那是受罪。玩过文明6的都听过那个笑话吧,美国人花了几百亿美元研究了在太空中写的圆珠笔,俄罗斯人直接带了支铅笔。这就是在解决问题的时候典型的抓错核心,核心是写字而不是找笔。这里笔者可以给大家一个小建议,具体场景的核心可以上LeetCode里去练,LeetCode刷题的时候其实抓住问题核心是一个很重要的考核环节。

运用手段

在雍正王朝里有这样一个场景:

康熙43年黄河发大水,朝廷的拨款已经捉襟见肘,四爷十三爷不得已被康熙派去江南筹款赈灾。当然这个问题的核心很简单,就是要钱,加上四爷十三爷是皇子有免死金牌,用点非常手段也是可以的。

那么江南当时是什么情况呢,江南当时是贤王八爷,土豪九爷的地盘,势力错综复杂,官官相护。江南巡盐道任伯安,更是仗着有皇子撑腰,公开抗拒朝廷钦差。但是当时的灾民已经是流离失所,食不果腹,随时都有生命危险。

那么此时四爷十三爷的手段是什么呢:

  • 第一步:劫富济贫,打击八爷党势力势在必行,除了逼迫这些贪官拿钱没有别的办法,但是这需要时间,如果灾民饿死的多了皇上就会怪罪办事不力,那就先用个临时办法,官方出面要求富有的人家先保证灾民的吃喝。
  • 第二步:抓取把柄,你任伯安和九爷的书信来往我早有预知,我在半道上给你扣下来。为后面逼迫你给钱埋下伏笔。
  • 第三步:杀鸡儆猴,八爷九爷到底势力庞大,如果直接撕破脸将矛盾公开化,必然对自己不利,那么就曲线救国,我拿大阿哥手下的池州知府李淦开刀,随便找个罪名扒掉官服当众按家法治理,就是摆明了告诉你,即便是大阿哥的奴才,我照样不给面子,何况你手里还有把柄在我手里,如果你拒不配合,我就让皇上知道你们的事情,你看看九爷那个时候保不保你

后面的结果我们都知道了,任伯安迫于这样的压力,被迫花钱消灾,四爷和十三爷也成功办完差事,回京复命,这个就是一个很好的利用手段解决问题的例子,而后续的四爷追比国库欠款的事件中,手段就比较差了,一味地追着那些无关紧要的,对真正的大老虎的劣行却没有多加追查

手段就是解决问题的方法,其实同学们一直以来认为的努力工作,充其量只是用手段的时候费点力气,但是手段这种东西,有时候是巧办法有时候是笨办法,最多是笨办法的时候努力点。巧办法可以轻轻松松解决问题。

努力学习为了增加手段

那肯定有同学要问了,你前面说了这么多没有一件和计算机有关的,难道我一个程序员还得去搞人际关系嘛,当然不是,不知道同学们发现没有,你手里用的这样那样的技术就是解决问题的手段。最简单的例子就是同样一个功能,用Java可能几十行,用Python可能就几行。包括判断还是循环,有时候都可以解决同样的问题。

事实上,学习是不存在内卷这种说法的。
我们学习这样那样的技术,都是为了在增加更多的手段,在笔者眼里,复制粘贴都是手段之一,只不过是最简单的,你不用学都会。

所以,笔者在这里给大家的建议是啥呢,别再问这个技术,那个技术要不要学了,在笔者眼里看来,只要是当下流行的技术,都必须要学,最多就是学的深和浅的问题。但绝对不是学和不学的问题,你学的东西越多,你的手段就越多,某种程度上还会影响你的决策。

巧妇难为无米之炊,你如果只会复制代码,你是做不出什么决策的。

Grain 是 Orleans 框架中的基本单元,代表了应用程序中的一个实体或者一个计算单元。

每个Silo都是一个独立的进程,Silo负责加载、管理和执行Grain实例,并处理来自客户端的请求以及与其他Silo之间的通信。

通信原理

在相同的Silo中,Grain与Grain之间的通信通过直接的方法调用实现。每个Silo都维护了一个Grain的运行时环境,当一个Grain需要调用另一个Grain时,它可以直接调用目标Grain的方法,无需经过网络传输,示意图如下所示:

在不同的Silo中,Grain与Grain之间的通信需要通过消息传递的方式实现。当一个Grain需要与另一个Silo中的Grain通信时,它会将消息发送给目标Grain所在的Silo,目标Silo接收到消息后,将其路由到目标Grain,然后目标Grain处理消息并返回结果。示意图如下所示:

外部客户端与Silo之间的通信是通过网络消息传输实现的。客户端需要使用Orleans提供的客户端库与Silo建立连接,并发送请求消息到目标Silo,目标Silo接收到消息后,进行处理并返回结果。在Orleans中,客户端与Silo之间的通信使用了一种名为Orleans Messaging Protocol (OMP)的自定义协议,用于保证通信的可靠性和效率。示意图如下所示:

内置端口

默认情况下,Orleans 将侦听端口 66666611 用于silo之间通信,在端口 30000 上进行客户端到接收器通信。可以通过以下方式设置这些端口

siloBuilder.Configure<EndpointOptions>(options =>{//Port to use for silo-to-silo
    options.SiloPort =11_666666;//Port to use for the gateway
    options.GatewayPort =30_000;//IP Address to advertise in the cluster
    options.AdvertisedIPAddress = IPAddress.Parse("172.16.0.42");//The socket used for client-to-silo will bind to this endpoint
    options.GatewayListeningEndpoint = newIPEndPoint(IPAddress.Any, 40_000);//The socket used by the gateway will bind to this endpoint
    options.SiloListeningEndpoint = newIPEndPoint(IPAddress.Any, 50_000);
})

在内部,silo 将侦听 0.0.0.0:40000 和 0.0.0.0:50000,但在持久化提供程序中发布的值将是 172.16.0.42:66666611 和 172.16.0.42:30000

GrainKey类型

在 Orleans 中,可以使用不同类型的键来标识 Grain。下面是几种常用的 GrainKey 接口:

  • IGrainWithStringKey: 使用字符串作为键的接口。适用于需要以字符串形式标识的场景,比如用户名称、订单号等。
  • IGrainWithGuidKey: 使用 Guid 作为键的接口。适用于需要全局唯一标识的场景,比如唯一的实体对象、全局唯一的标识符等。
  • IGrainWithIntegerKey: 使用整数作为键的接口。适用于需要连续递增或递减的序列标识的场景,比如自增主键、序列号等。
  • IGrainWithGuidCompoundKey: 使用复合的 Guid 作为键的接口。
  • IGrainWithIntegerCompoundKey: 使用复合的整数作为键的接口。
  • IGrainWithGuidCompoundKey: 使用复合的字符串作为键的接口。

下面是使用 IGrainWithStringKey 定义的 IPlayerGrain 接口,并为其增加了买装备的动作,并将买完的装备保存至内存中:

public interfaceIPlayerGrain : IGrainWithStringKey
{
Task BuyEquipment(
stringequipmentName);
Task
<List<string>>GetOwnedEquipments();
}
public classPlayerGrain : Grain, IPlayerGrain
{
private IPersistentState<List<string>>_ownedEquipments;public PlayerGrain([PersistentState("ownedEquipments", "playerGrainStorage")] IPersistentState<List<string>>ownedEquipments)
{
_ownedEquipments
=ownedEquipments;
}
public async overrideTask OnActivateAsync(CancellationToken cancellationToken)
{
await base.OnActivateAsync(cancellationToken);//在激活时从持久化状态中加载数据 await_ownedEquipments.ReadStateAsync();if (_ownedEquipments.State == null)
{
_ownedEquipments.State
= new List<string>();await _ownedEquipments.WriteStateAsync(); //将空列表持久化到存储中 }
}
public async Task BuyEquipment(stringequipmentName)
{
_ownedEquipments.State.Add(equipmentName);
await _ownedEquipments.WriteStateAsync(); //将更新后的装备列表持久化到存储中 }public Task<List<string>>GetOwnedEquipments()
{
returnTask.FromResult(_ownedEquipments.State);
}
}

调用时使用IGrainFactory.GetGrain方法即可

var host =Host.CreateDefaultBuilder()
.ConfigureServices((context, services)
=>{
services.AddOrleans(builder
=>{
builder
.UseLocalhostClustering()
.Configure
<ClusterOptions>(options =>{
options.ClusterId
= "dev";
options.ServiceId
= "OrleansExample";
})
.AddMemoryGrainStorage(
"playerGrainStorage");
});
})
.ConfigureLogging(l
=>l.AddConsole())
.Build();
awaithost.StartAsync();var client = host.Services.GetRequiredService<IGrainFactory>();var palyer = client.GetGrain<IPlayerGrain>(Guid.NewGuid().ToString());await palyer.BuyEquipment("Sword");
(
await palyer.GetOwnedEquipments()).ForEach(Console.WriteLine);

IGrainFactory 和 IClusterClient

在 Orleans 中,IGrainFactory 和 IClusterClient 都是用于创建和获取 Grains 的接口,但它们的作用和使用场景略有不同。

IGrainFactory:

IGrainFactory 是 Orleans 用于集群中创建 Grains 的工厂接口。 它通常用于在 Orleans Silo 或者 Orleans Client 中创建 Grains 实例。

  • 在 Silo 中,您可以通过依赖注入或者直接实例化一个 IGrainFactory 对象来创建 Grains。
  • 在 Silo 外部,比如 Orleans Client 中,您也可以通过依赖注入或者直接实例化一个 IGrainFactory 对象来创建 Grains。
//通过依赖注入或直接实例化一个 IGrainFactory 对象
IGrainFactory grainFactory = serviceProvider.GetRequiredService<IGrainFactory>();var grain = grainFactory.GetGrain<IMyGrain>(grainId);

IClusterClient:

IClusterClient 是 Orleans 中用于与 Orleans 集群进行通信的客户端接口。 它通常在 Orleans Client 中使用,用于与 Orleans Silo 进行通信,以调用 Grains 的方法或者获取 Grains 的引用。

IClusterClient 是 IGrainFactory 的一个超集,除了可以创建 Grains,还可以执行其他集群相关的操作,比如管理 Silo 的生命周期、订阅集群中的事件等。

//通过依赖注入或直接实例化一个 IClusterClient 对象
IClusterClient clusterClient = serviceProvider.GetRequiredService<IClusterClient>();var grain = clusterClient.GetGrain<IMyGrain>(grainId);

总的来说,IGrainFactory 主要用于在应用程序内部直接创建 Grains,而 IClusterClient 则更适合用于外部Client与 Orleans 集群进行通信,包括创建 Grains 和执行其他集群操作。

在花了大约 4 年时间致力于 Apache Hudi(其中包括 3 年Committer身份)之后,我决定开始这个博客系列,旨在以有组织且适合初学者的方式展示 Hudi 的设计和用法。 我的目标是确保对分布式数据系统有一定了解的人能够轻松地理解该系列。 该系列将包含 10 篇文章,每篇文章都会深入探讨 Hudi 的一个关键方面。 (为什么是 10?纯粹是对 0 和 1 的有趣致敬,与系列标题相呼应:))最终目标是帮助读者从广度和深度上理解 Hudi,使他们能够自信地利用这个开源项目并为其做出贡献。 在撰写本文时,Hudi 0.14.0 正处于候选版本阶段。 因此,整个系列以及配套代码和示例都将基于此版本。

概述

Hudi 是一个事务性数据湖平台,它将数据库和数据仓库功能引入数据湖。 下图摘自
Hudi 社区举办的网络研讨会
,清楚地说明了该平台的主要功能

Hudi 的核心定义了一种表格式,用于组织存储系统内的数据和元数据文件,从而实现 ACID 事务、高效索引和增量处理等功能。 本文的其余部分将探讨格式详细信息,主要展示存储上的 Hudi 表的结构并解释不同文件的角色。

存储格式

下图描述了 Hudi 表在存储中的基本路径下的典型数据布局。

有两种主要类型的文件:位于
.hoodie/
目录中的元数据文件,以及存储在分区路径中(如果表已分区)的数据文件,或者直接在基本路径(如果未分区)下的数据文件。

元数据

<base path>/.hoodie/hoodie.properties
文件包含基本的表配置,例如表名称和版本,表的写入端和读取端都将遵守和使用这些配置。

除了 hoodie.properties 之外,还有将事务操作记录到表中的元文件,形成 Hudi 表的时间轴。

# an example of deltacommit actions on Timeline
20230827233828740.deltacommit.requested
20230827233828740.deltacommit.inflight
20230827233828740.deltacommit

这些元文件遵循以下命名模式:

<action timestamp>.<action type>[.<action state>]

"action timestamp"

  • 标记第一次计划运行操作的时间。
  • 唯一标识时间轴上的操作。
  • 在时间轴上的不同操作之间单调递增。

"action type"显示该动作做出了什么样的改变。 有一些写入操作类型,例如 commit 和 deltacommit,它们指示表上发生的新写入操作(插入、更新或删除)。 此外,还有表服务操作,例如压缩和清理,以及恢复操作,例如保存点和恢复。 我们将在以后的帖子中更详细地讨论不同的操作类型。

"action state"可以是“已请求”、“进行中”或“已完成”(没有后缀)。 顾名思义,“已请求”表示正在计划运行,“正在执行”表示正在进行中,“已完成”表示操作已完成。

这些操作的元文件采用 JSON 或 AVRO 格式,包含有关应应用于表或已应用的更改的信息。 保留这些事务日志可以重新创建表的状态,实现快照隔离,并通过并发控制机制协调写入器冲突。

.hoodie/
下还存储有其他元数据文件和目录。 举一些例子,元数据包含与时间轴上的操作相关的更多元数据,并充当写入端和读取端的索引。
.heartbeat/
目录存储用于心跳管理的文件,而
.aux/
则保留用于各种辅助目的。

数据

Hudi将物理数据文件分为Base File(基本文件)和Log File(日志文件):

  • 基本文件包含 Hudi 表中的主要存储记录,并针对读取进行了优化。
  • 日志文件包含其关联基本文件之上的记录更改,并针对写入进行了优化。

在 Hudi 表的分区路径中(如前面的布局图所示),单个基本文件及其关联的日志文件(可以没有或多个)被分组在一起作为文件切片。 多个文件切片构成一个文件组。 文件组和文件切片都是逻辑概念,旨在封装物理文件,从而简化读取端和写入端的访问和操作。 通过定义这些模型,Hudi 可以

  • 满足读写效率要求。 通常基本文件配置为列式文件格式(例如 Apache Parquet),日志文件设置为基于行的文件格式(例如 Apache Avro)。
  • 实现跨提交操作的版本控制。 每个文件切片都与时间轴上操作的特定时间戳相关联,文件组中的文件切片本质上跟踪所包含的记录如何随时间演变。

可以快速查看
此处
的 Hudi 表示例,了解数据布局。

表格类型

Hudi 定义了两种表类型 - 写入时复制 (CoW) 和读取时合并 (MoR)。 布局差异如下:与 MoR 相比,CoW 没有日志文件,并且写入操作会导致
.commit
操作而不是
.deltacommit
。 在我们的讨论中,我们一直以 MoR 为例。 一旦掌握了 MoR,理解 CoW 就变得简单了 - 可以将 CoW 视为 MoR 的特殊情况,其中基本文件中的记录和更改在每次写入操作期间隐式合并到新的基本文件中。 可以在
此处
浏览示例 CoW 表。

在为 Hudi 表选择表类型时,考虑读取和写入模式非常重要,因为这会产生一些影响:

  • 由于每次写入都会重写新文件切片中的记录,因此 CoW 具有较高的写入放大,而读取操作始终会得到优化。 这非常适合读取繁重的分析工作负载或小型表。
  • MoR 的写入放大较低,因为更改会“缓冲”在日志文件中,并进行批处理以合并和创建新的文件切片。 但是,读取延迟会受到影响,因为读取最新记录需要将日志文件与基本文件进行实时合并。

用户还可以选择仅读取 MoR 表的基本文件,以提高效率,同时牺牲结果的新鲜度。 我们将在接下来的帖子中详细讨论 Hudi 的不同阅读模式。 随着 Hudi 项目的发展,与从 MoR 表读取相关的合并成本在过去的版本中得到了优化。 可以预见 MoR 将成为大多数工作负载场景的首选表类型。

回顾

在零到一系列的第一篇文章中,我们探讨了 Hudi 存储格式的基本概念,以阐明元数据和数据在 Hudi 表中的结构。 我们还简要解释了不同的表类型及其权衡。 如概览图所示,Hudi 作为一个综合性 Lakehouse 平台,提供不同维度的功能。 在接下来的九篇文章中,我将逐步介绍该平台的其他重要方面。

决策树

总体介绍

决策树模型顾名思义就是通过一条条的决策来将样本划分来从而达到分类或回归的目的。决策树模型呈树形结构,下图粗略展示了一个分类决策树

其中圆表示特征,方块表示叶子节点也是最终分类的类别,我们通过利用样本中高价值的特征(房子拥有情况,工作的拥有情况)来构建这么一个决策树,那么每当有新样本来临时就可以通过构建完成的决策树来对其进行分类。其中所有路径互斥且完备,即不存在两个相同的样本被划分到不同的类别(互斥),任意一个样本都会被划分到一个类别。
我们还可以认为决策树是定义在特征空间与类空间上的条件概率分布,即将特征空间划分成一个个互不相交的单元,这与之前所讲的KNN算法的核心思想一致。下图展示了决策树在两个特征
\(x_1,x_2\)
下的划分情况。

这意味着,当所有样本的两个特征满足下面的情况时
\(x_1 \leq a_1\)

\(x_2 \leq a_2\)
或者
\(x_1 > a_1\)

\(x_2 > a_3\)
将被划分为正类。
同时我们也可以上上面的图转换为决策树进行对照,如下图所示。

在使用决策树模型时通常包含三个步骤:特征选择、决策树生成和决策树剪枝。

决策树学习

从上面的介绍中,我们可以知道决策树是通过对特征的取值不断进行分解从而构建成树的,但是基于特征空间划分的类的条件概率模型有无穷多个,我们选择的模型不仅要对训练数据有很好的拟合更应该注重其泛化能力。总体上来说,决策的学习包括下面三个注意部分:

  • 特征选择。特征选择的目的选择具有较强分类能力的特征,如果一个特征在对所有类别的区分不大,那么这个特征可能就没有较大的意义。衡量特征好坏有信息增益、信息增益比等指标。后面的内容会一步步剖析它们。
  • 决策树的生成。当通过特征选择得到有价值的特征后,下面的目的就是通过这些特征来构建决策树,常见的算法ID3和C4.5。
  • 决策树的剪枝。最后为了避免生成的决策树过于臃肿,我们需要使用剪枝算法对决策树进行约简,使其具有更好的泛化能力。相关剪枝算法也会在后面介绍。