Elasticsearch

ES既是搜索引擎又是数据库:ElasticSearch究竟是不是数据库?历来存在争议.它是搜索引擎,千真万确,而它是数据库,需要看数据库的界定范围.广义上讲,关系型数据库,对象型数据库,搜索引擎,文件,都可以算作是数据库.数据库是用来存储数据的,为服务应用提供了数据的读写功能,即数据的添加,修改,删除,查询四种基本功能.

1.关系型数据库,诸如:transact-sql,plsql,mysql,firebird,maria等,具备了增删改查四种基本功能.

2.对象型数据库,诸如:redis,mongo等,也具备了增删改查四种基本功能.

3.搜索引擎,就是提到的elasticsearch,也具备了增删改查四种基本功能.

4.文件,比如:sqlite,也具备了增删改查四种基本功能.

ES: 基于Lucene框架的搜索引擎产品。you know for search. 提供了restful风格的操作接口。

ELK,日志检索

Lucene:是一个非常高效的全文检索引擎框架。Java开发的,只提供单机功能。

ES的一些核心概念:

1、索引index:关系型数据库中的table

2、文档document:行

3、字段 field text/keyword/byte:列

4、映射Mapping: Schema

5、查询方式 DSL:SQL ES新版本也支持SQL

6、分片sharding和副本replicas:index都是由sharding组成的。每个sharding都有一个或多个备份。ES集群健康状态。

1)Elasticsearch是搜索引擎

Elasticsearch在搜索引擎数据库领域排名绝对第一,内核基于Lucene构建,支持全文搜索是职责所在,提供了丰富友好的API。个人早期基于Lucene构建搜索应用,需要考虑的因素太多,自接触到Elasticsearch就再无自主开发搜索应用。普通工程师要想掌控Lucene需要一些代价,且很多机制并不完善,需要做大量的周边辅助程序,而Elasticsearch几乎都已经帮你做完了。

2)Elasticsearch不是搜索引擎

说它不是搜索引擎,估计很多从业者不认可,在个人涉及到的项目中,传统意义上用Elasticsearch来做全文检索的项目占比越来越少,多数时候是用来做精确查询加速,查询条件很多,可以任意组合,查询速度很快,替代其它很多数据库复杂条件查询的场景需求;甚至有的数据库产品直接使用Elasticsearch做二级索引,如HBase、Redis等。Elasticsearch由于自身的一些特性,更像一个多模数据库。

图示:Elasticsearch综合数据库排名热度已经到第7

3)Elasticsearch是数据库

Elasticsearch使用Json格式来承载数据模型,已经成为事实上的文档型数据库,虽然底层存储不是Json格式,同类型产品有大名鼎鼎的MongoDB,不过二者在产品定位上有差别,Elasticsearch更加擅长的基于查询搜索的分析型数据库,倾向OLAP;MongoDB定位于事务型应用层面OLTP,虽然也支持数据分析,笔者简单应用过之后再无使用,谁用谁知道。

4)Elasticsearch不是数据库

Elasticsearch不是关系型数据库,内部数据更新采用乐观锁,无严格的ACID事务特性,任何企图将它用在关系型数据库场景的应用都会有很多问题,很多其它领域的从业者喜欢拿这个来作为它的缺陷,重申这不是Elasticsearch的本质缺陷,是产品设计定位如此。

图示:Elastic擅长的应用场景

2、ES做什么

Elasticsearch虽然是基于Lucene构建,但应用领域确实非常宽泛。

1)全文检索

Elasticsearch靠全文检索起步,将Lucene开发包做成一个数据产品,屏蔽了Lucene各种复杂的设置,为开发人员提供了很友好的便利。很多传统的关系型数据库也提供全文检索,有的是基于Lucene内嵌,有的是基于自研,与Elasticsearch比较起来,功能单一,性能也表现不是很好,扩展性几乎没有。

如果,你的应用有全文检索需求,建议你优先迁移到Elasticsearch平台上来,其提供丰富的Full text queries会让你惊讶,一次用爽,一直用爽。

图示:Elasticsearch官方搜索文档

2)应用查询

Elasticsearch最擅长的就是查询,基于倒排索引核心算法,查询性能强于B-Tree类型所有数据产品,尤其是关系型数据库方面。当数据量超过千万或者上亿时,数据检索的效率非常明显。

个人更看中的是Elasticsearch在通用查询应用场景,关系型数据库由于索引的左侧原则限制,索引执行必须有严格的顺序,如果查询字段很少,可以通过创建少量索引提高查询性能,如果查询字段很多且字段无序,那索引就失去了意义;相反Elasticsearch是默认全部字段都会创建索引,且全部字段查询无需保证顺序,所以我们在业务应用系统中,大量用Elasticsearch替代关系型数据库做通用查询,自此之后对于关系型数据库的查询就很排斥,除了最简单的查询,其余的复杂条件查询全部走Elasticsearch。

3)大数据领域

Elasticserach已经成为大数据平台对外提供查询的重要组成部分之一。大数据平台将原始数据经过迭代计算,之后结果输出到一个数据库提供查询,特别是大批量的明细数据。

这里会面临几个问题,一个问题是大批量明细数据的输出,如何能在极短的时间内写到数据库,传统上很多数据平台选择关系型数据库提供查询,比如MySQL,之前在这方面吃过不少亏,瞬间写入性能极差,根本无法满足要求。另一个问题是对外查询,如何能像应用系统一样提供性能极好的查询,不限制查询条件,不限制字段顺序,支持较高的并发,支持海量数据快速检索,也只有Elasticsearch能够做到比较均衡的检索。

从官方的发布版本新特性来看,Elasticseacrch志在大数据分析领域,提供了基于列示存储的数据聚合,支持的聚合功能非常多,性能表现也不错,笔者有幸之前大规模深度使用过,颇有感受。

Elasticsearch为了深入数据分析领域,产品又提供了数据Rollup与数据Transform功能,让检索分析更上一层楼。在数据Rollup领域,Apache Druid的竞争能力很强,笔者之前做过一些对比,单纯的比较确实不如Druid,但自Elasticsearch增加了Transfrom功能,且单独创建了一个Transfrom的节点角色,个人更加看好Elasticseach,跳出了Rollup基于时间序列的限制。

图示:Rollup执行过程数据转换示意图

4)日志检索

著名的ELK三件套,讲的就是Elasticsearch,Logstash,Kibana,专门针对日志采集、存储、查询设计的产品组合。很多第一次接触到Elasticsearch的朋友,都会以为Elasticsearch是专门做日志的,其实这些都是误解,只是说它很擅长这个领域,在此领域大有作为,名气很大。

日志自身特点没有什么通用的规范性,人为的随意性很大,日志内容也是任意的,更加需求全文检索能力,传统技术手段本身做全文检索很是吃力。而Elasticsearch本身起步就是靠全文检索,再加上其分布式架构的特性,非常符合海量日志快速检索的场景。今天如果还发现有IT从业人员用传统的技术手段做日志检索,应该要打屁股了。

如今已经从ELK三件套发展到Elastic Stack了,新增加了很多非常有用的产品,大大增强了日志检索领域。

5)监控领域

指标监控,Elasticsearch进入此领域比较晚,却赶上了好时代,Elasticsearch由于其倒排索引核心算法,也是支持时序数据场景的,性能也是相当不错的,在功能性上完全压住时序数据库。

Elasticsearch搞监控得益于其提供的Elastic Stack产品生态,丰富完善,很多时候监控需要立体化,除了指标之外,还需要有各种日志的采集分析,如果用其它纯指标监控产品,如Promethues,遇到有日志分析的需求,还必须使用Elasticsearch,这对于技术栈来说,又扩增了,相应的掌控能力会下降,个人精力有限,无法同时掌握很多种数据产品,如此选择一个更加通用的产品才符合现实。

图示 :Elastic性能对比时序数据库(来自腾讯云分享)

6)机器学习

机器学习最近几年风吹的很大,很多数据产品都集成了,Elasticsearch也必须有,而且做的更好,真正将机器学习落地成为一个产品 ,简化使用,所见所得;而不像其它数据产品,仅仅集成算法包,使用者还必须开发很多应用支持。

Elasticsearch机器学习提供了两种方式,一种是异常检测类型,属于无监督学习,采用聚类模型,通常应用在安全分析领域,检测异常访问等;一种是数据帧分析,属于分类与回归,属于监督学习,可用于在业务模型领域,如电商行业,价格模型分析。

Elasticsearch本身是数据平台,集成了部分机器学习算法,同时又集成了Kibana可视化操作,使得从数据采集、到模型训练、到模型预测应用都可以一键式完成。

Elasticserach提供的机器学习套件,个人认为最应该应用在数据质量这个领域,帮助大数据平台自动检测数据质量,从而降低人力提供效率。

图示 :机器学习应用示意图(截图)

需求等级

Elasticsearch整个的技术栈非常复杂,涉及到的理论与技术点非常多,完全掌握并不现实,作为一个IT从业者,首先是定位好自己的角色,依据角色需求去学习掌握必备的知识点。以下是笔者对于一个技术产品的划分模型:

1、概念

Elasticsearch涉及到的概念很多,核心概念其实就那么几个,对于一个新手来说,掌握概念目的是为了建立起自己的知识思维模型,将之后学习到的知识点做一个很好的归纳划分;对于一个其它数据产品的老手来说 ,掌握概念的目的是为了与其它数据产品划分比较,深入的了解各自的优劣,在之后工作中若有遇到新的业务场景,可以迅速做出抉择。

IT从业者普遍都有个感受,IT技术发展太快了,各种技术框架产品层出不穷,学习掌握太难了,跟不上节奏。其实个人反倒觉得变化不大,基础理论核心概念并没有什么本质的发展变化,无非是工程技术实操变了很多,但这些是需要深入实践才需要的,对于概念上无需要。

作为一个技术总监,前端工程师工作1~2年的问题都可以问倒他,这是大家对于概念认知需求不一样。

图示:Elasticsearch核心概念

2、开发

开发工程师的职责是将需求变成可以落地运行的代码。Elasticsearch的应用开发工作总结起来就是增删改查,掌握必备的ES REST API,熟练运用足以。笔者之前任职某物流速运公司,负责Elasticsearch相关的工作,公司Elasticsearch的需求很多,尤其是查询方面,ES最厉害的查询是DSL,这个查询语法需要经常练习使用,否则很容易忘记,当每次有人询问时,都安排一个工程师专门负责各种解答,他在编写DSL方面非常熟练,帮助了很多的工程师新手使用Elasticsearch,屏蔽了很多细节,若有一些难搞定的问题,会由我来解决,另外一方面作为负责人的我偶然还要请他帮忙编写DSL。

Elasticsearch后面提供了SQL查询的功能,但比较局限,复杂的查询聚合必须回到DSL。

图示:DSL语法复杂度较高

3、架构

Elasticsearch集群架构总体比较复杂,首先得深入了解Elasticseach背后实现的原理,包括集群原理、索引原理、数据写入过程、数据查询过程等;其次要有很多案例实战的机会,遇到很多挑战问题 ,逐一排除解决,增加自己的经验。

对于开发工程师来说,满足日常需求开发无需掌握这些,但对于Elasticsearch技术负责人,就非常有必要了,面对各种应用需求,要能从架构思维去平衡,比如日志场景集群需求、大数据分析场景需求、应用系统复杂查询场景需求等,从实际情况设计集群架构以及资源分配等。

4、运维

Elasticsearch本质是一个数据库,也需要有专门的DBA运维,只是更偏重应用层面,所以运维职责相对传统DBA没有那么严苛。对于集群层面必须掌握集群搭建,集群扩容、集群升级、集群安全、集群监控告警等;另外对于数据层面运维,必须掌握数据备份与还原、数据的生命周期管理,还有一些日常问题诊断等。

5、源码

Elasticsearch本身是开源,阅读源码是个很好的学习手段,很多独特的特性官方操作文档并没有写出来,需要从源码中提炼,如集群节点之间的连接数是多少,但对于多数Elasticsearch从业者来说,却非必要。了解到国内主要是头部大厂需要深入源码定制化改造,更多的是集中在应用的便捷性改造,而非结构性的改造,Elastic原厂公司有几百人的团队做产品研发,而国内多数公司就极少的人,所以从产量上来说,根本不是一个等级的。

如果把Elasticsearch比喻为一件军事武器,对于士兵来说 ,熟练运用才是最重要的,至于改造应该是武器制造商的职责,一个士兵可以使用很多武器装备,用最佳的组合才能打赢一场战争,而不是去深入原理然后造轮子,容易本末倒置。

6、算法

算法应该算是数据产品本质的区别,关系型数据库索引算法主要是基于B-Tree, Elasticserach索引算法主要是倒排索引,算法的本质决定了它们的应用边界,擅长的应用领域。

通常掌握一个新的数据产品时,个人的做法是看它的关键算法。早期做过一个地理位置搜索相关的项目,基于某个坐标搜索周边的坐标信息,开始的时候采用的是三角函数动态计算的方式,数据量大一点,扫描一张数据表要很久;后面接触到Geohash算法,按照算法将坐标编码,存储在数据库中,基于前缀匹配查询,性能高效几个数量级,感叹算法的伟大;再后面发现有专门的数据库产品集成了Geohash算法,使用起来就更简单了。

Elasticsearch集成很多算法,每种算法实现都有它的应用场景。

拥抱ES的方法

1、官方文档

Elasticsearch早期出过一本参考手册《Elastic权威指南》,是一本很好的入门手册,从概念到实战都有涉及,缺点是版本针对的2.0,过于陈旧,除去核心概念,其余的皆不适用,当前最新版本已经是7.7了,跨度太大,Elasticsearch在跨度大的版本之间升级稍微比较麻烦,索引数据几乎是不兼容的,升级之后需要重建数据才可。

Elasticsearch当前最好的参考资料是官方文档,资料最全,同步发布版本,且同时可以参考多个版本。Elasticsearch官方参考文档也是最乱的,什么资料都有,系统的看完之后感觉仍在此山中,有点类似一本字典,看完了字典,依然写不好作文;而且资料还是英文的,至此就阻挡了国内大部分程序进入。

但想要学习Elasticsearch,官方文档至少要看过几遍,便于迅速查询定位。

图示:官方文档截图说明

2、系统学习

Elasticsearch成名很早,国内也有很多视频课程,多数比较碎片,或是纸上谈兵,缺乏实战经验。Elasticsearch有一些专门的书籍,建议购买阅读,国内深度一些的推荐《Elasticsearch源码解析与优化实战》,国外推荐《Elasticsearch实战》,而且看书还有助于培养系统思维。

Elasticsearch技术栈功能特性很多,系统学习要保持好的心态,持之以恒,需要很长时间,也需要参考很多资料。

3、背后原理

Elasticsearch是站在巨人肩膀上产品,背后借鉴了很多设计思想,集成了很多算法,官方的参考文档在技术原理探讨这块并没有深入,仅仅点到为止。想要深入了解,必须得另辟蹊径。

Elastic官方的博客有很多优质的文章,很多人因为英文的缘故会忽视掉,里面有很多关键的实现原理,图文并茂,写得非常不错;另外国内一些云厂商由于提供了Elasticsearch云产品,需要深度定制开发,也会有一些深入原理系列的文章,可以去阅读参考,加深理解。对于已经有比较好的编程思维的人,也可以直接去下载官方源码,设置断点调试阅读。

4、项目实战

项目实战是非常有效的学习途径,考过驾照的朋友都深有体会,教练一上来就直接让你操练车,通过很多次的练习就掌握了。Elasticsearch擅长的领域很多,总结一句话就是“非强事务ACID场景皆可适用”,所以可以做的事情也很多。

日志领域的需求会让你对于数据写入量非常的关心,不断的调整优化策略,提高吞吐量,降低资源消耗;业务系统的需求会让你对数据一致性与时效性特别关心,从其它数据库同步到ES,关注数据同步的速度,关注数据的准确性,不断的调整你的技术方案与策略;大数据领域的需求会让你对于查询与聚合特别关注,海量的数据需要快速的检索,也需要快速的聚合结果。

项目实战的过程,就是一个挖坑填坑的过程,实战场景多了,解决的问题多了,自然就掌握得很好了。

IBRNet: Learning Multi-View Image-Based Rendering(用NeRF做可泛化视角插值)

IBRNet: Learning Multi-View Image-Based Rendering

主页:https://ibrnet.github.io/

代码链接:https://github.com/googleinterns/IBRNet

NeRF存在的一大问题就是仅仅只能表示一个场景,因此这篇文章就学提出了一个框架可以同时学习多个场景,且可以扩展到没有学习过的场景(提高泛化性)。实验表明,在寻求泛化到新scenes时,我们的方法比其它好。更进一步,如果fine-tuned每一个scene,可以实现和目前SOTA的NVS任务相当的表现。

本文与NeRF最大的不同是输入的数据不仅仅有目标视角,还有对应的所有同一场景的多视角图片,因此理论上的确是可以直接端到端的应用于新场景的。原始NeRF针对每个scene都需要优化(重头训练),而本文方法学习一个通用的view插值函数能够泛化到新的scenes。

overview_image

模型流程:

1. 将同一场景的多视角图片一同输入网络(个数不限),然后使用一个U-Net来抽取每张图片(source view)的特征,特征包括图像颜色,相机参数,图像表征(这里可以就理解成NeRF中的向辐射场发射光线,然后保存对应的光线参数和图片特征)。

2. 之后将每张图片的特征并行的输入一个transformer,用于预测一个共同的颜色和密度。之所以是共同的颜色和密度是因为这多个视角输入的特征我们默认是同一个点在不同视角的特征,因此结果就是用于预测我们目标视角(target view)里此点的结果。

3. 用体渲染的方式将结果渲染出来,之后通过像素的重构损失来优化网络

4. 换一个场景,重复1~3

备注:如果一直用同一个场景训练的话,理论上效果肯定会更好,也就是论文中提到的finetune的情况。

个人理解:本质上这里的模型学习的是“如何插值”,而不是构建一个辐射场,因此可能对于较为稀疏的情形或复杂场景表现的没那么好。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Chain of Thought

paper: https://arxiv.org/abs/2201.11903

  • zero-shot:输入问题,等待输出结果
  • CoT:输入问题并提示Let’s think step by step
  • Manual-CoT: 是一种few shot方法,所以构造了一些模板Q&A(模板A中也有Let’s think step by step),然后再给出问题并提示Let’s think step by step
  • Auto-CoT:采样多个问题,每个问题提示Let’s think step by step,让模型给出答案。然后拼接所有生成的Q&A并给出最终问题,并提示Let’s think step by step

为什么需要CoT?

问题可以分为两类:一类是容易回答的,没有太多逻辑推理的,比如:天气如何?面包几块钱?另一类是需要长链条的逻辑推理的问题:数学等。

当语言模型的规模指数级增大时,它解决常规问题的能力有了很大的提升,然而它解决逻辑推理的问题的能力却提升很小。而CoT就是帮助解决这样的问题,它的核心思想是:不要光给出答案,把推理过程也给出来。如下图所示,关键在于构造的prompt要包含推理过程:

为什么延长推理过程就有效呢?这可能是因为语言模型token-by-token的特点。

标准的prompt可以被视为大模型能力的下限,如何提取大模型学到的知识的问题是一个难点,标准的prompt是一个很好的起点,但却绝不是终点。

数据存储方案 –NAS

NAS(Network Attached Storage:网络附属存储),简单来说,就是个人或中小型企业私用的网络存储服务器。区别于百度云,坚果云等公有云存储,由于NAS的硬件是在本地放置,所以可以称为私有云。

简单解释NAS就是私人的7*24小时在线的存储服务器,1996年美国硅谷首先提出NAS概念,把存储设备和网络接口集成在一起,直接通过网络存取数据。网络附属存储(NAS)是一种专业的网络文件存储及文件备份设备。它通过自带的网络接口把存储设备直接连入到网络中,实现海量数据的网络共享,把应用程序服务器从繁重的I/O负载中解脱出来,从而把存储功能从通用文件服务器中分离出来,获得更高的存取效率,更低的存储成本。硬件层面上NAS就是一台电脑,但是跟普通电脑的技能点分配还是略有区别的。

1、 Network Attached Storage

NAS 的全称是 Network Attached Storage,翻译成中文就是网络附加存储。我们来拆解一下就是网络、附加、存储。存不需要过多的解释,就是来存储东西的。附加的意思就是这块存储可以轻松的附加上,或者取下而不影响系统使用。对比我们电脑上的硬盘,就不能说是附加的。因为电脑硬盘不能随便的取下,而且硬盘取下来之后你的电脑就没法用了。网络的意思是想要访问存储里面的内容,需要有网络才行,不管是公网还是局域网反正得有网。

简单来说,NAS 提供存储服务,可用通过网络来访问存储里面的内容

2、超大容量

NAS 作为一台存储服务器,它的主要功能是存储,相比于我们普通的硬盘,NAS 最大的特点是存储空间共享,也就是网络访问。基于网络访问就可以实现其他很多功能如数据同步,照片备份,重要资料备份等等。NAS 的容量是很大的,一般都是以 T 为单位(1TB = 1024GB)。NAS 存储本身也是可以扩展的,通过累计叠加多个硬盘容量,可以扩大存储空间。NAS 系统一天 24 小时待命,没有用一会关机一会儿这种说法。因此 NAS 对磁盘的稳定性要求很高。

3、数据共享

任何设备,只要你能连上 NAS 并且赋予了访问权限,你就可以访问 NAS 中存储的数据。我们可以在手机、笔记本、iPad、智能电视上访问 NAS 中的数据。就像使用本地存储一样,非常的方便。NAS 的一个使用场景是办公共享,在一个局域网内可以实现办公的连续性。当文件在电脑被编辑保存之后,可以用 iPad 接着编辑。

4、安全存储

企业最看重这一点。安全对于企业来说,主要从两方面来讲,一是数据不能丢,而是数据不能泄露给别人。NAS 都具备强大的数据备份功能,可以提供安全不丢失的数据存储,这解决了上面的第一个问题;而解决第二个问题的方式也很简单,自己保存数据,不要用第三方的服务就行。

NAS 的使用场景:视频下载\备份手机上的照片,文件

NAS有一个重要的功能,异地访问,现在的家庭用户基本都没有自己的公网IP,就算是有路由器,内网穿透,端口映射等等的设置也是相当繁琐的。当然成品NAS一般都有自己的中转服务器可以实现异地访问的功能,但是传输速度相当感人。

英伟达显卡监控工具nvtop & 深度学习cuda+pytorch安装教程

背景

在用英伟达显卡做深度学习训练或推理时,我们常用nvidia-smi指令来查看显卡的使用情况,如图所示

这种方法可以看出每张显卡内存和GPU利用率的实时情况,但看不出历史数据和变化曲线,这个时候就需要用到nvtop了。

Nvtop代表NVidia TOP,这是用于NVIDIA GPU的任务监视器。它可以处理多个GPU,并以熟悉的方式打印有关它们的信息。如图所示,很直观的显示了每张显卡的内存、GPU利用率曲线。本文对该工具的安装使用进行介绍。

1 安装方法

在Ubuntu disco (19.04) / Debian buster (stable)系统中,可以直接使用apt安装

sudo apt install nvtop

如果是在旧的系统,如ubuntu16.04等,则需要通过源码安装,方法如下

# 安装依赖sudo apt install cmake libncurses5-dev libncursesw5-dev git # 下载源码git clone https://github.com/Syllo/nvtop.gitmkdir -p nvtop/build && cd nvtop/buildcmake .. # 如果报错"Could NOT find NVML (missing: NVML_INCLUDE_DIRS)"# 则执行下边的语句,否则跳过cmake .. -DNVML_RETRIEVE_HEADER_ONLINE=True # 编译makesudo make install 

2 使用方法

安装完之后,可以执行nvtop -h来查看使用方法,介绍的很详细了,如果现实全部信息,直接nvtop就可以现实出我们上边的结果

nvtop version 1.0.0Available options:  -d --delay        : Select the refresh rate (1 == 0.1s)  -v --version      : Print the version and exit  -s --gpu-select   : Column separated list of GPU IDs to monitor  -i --gpu-ignore   : Column separated list of GPU IDs to ignore  -p --no-plot      : Disable bar plot  -C --no-color     : No colors  -N --no-cache     : Always query the system for user names and command line information  -f --freedom-unit : Use fahrenheit  -E --encode-hide  : Set encode/decode auto hide time in seconds (default 30s, negative = always on screen)  -h --help         : Print help and exit

CUDA+PYTORCH安装教程:

环境安装

多模态|BLIP 、CoCa and BeiTv

BLIP

BLIP: Bootstrapping Language-Image Pre-training for Unified Vision-Language Understanding and Generation

代码: https://github.com/salesforce/BLIP

本文是 ALBEF 原班人马做的,基本可以看做吸收了 VLMo 思想的 ALBEF。训练的 loss 和技巧都与 ALBEF 一致,属于 ALBEF 的后续工作。

本文motivation主要有两个:一是之前多模态预训练模型结构要么是基于编码器,不能直接用于生成任务,要么是基于编码解码器,在检索类任务上不方便,本文设计的结构包含单模态编码器、视觉指导文本编码器、视觉指导文本解码器,可以方便地用对比学习、ITM(Image-Text Matching ( ITM ): 图文匹配任务,针对的是图文交互流,即判断当前pair是不是匹配(就是个分类任务))、LM(生成式任务)三个预训练任务训练不同的模块,也容易迁移到各种下游任务中;二是之前的很多工作通过扩充了网上搜集的图文对的预训练数据(GCC、SBU、CC12M),提高了模型效果,但忽略了其中有很多不对齐的噪声情况,本文用一个boostrapping的方法,用captioner为网络图片生成描述,用filter过滤掉不配对的数据,从而降低噪声,更高效地利用网络上的数据。

关键的改进:

1. 模型结构上整合了 ALBEF 和和 VLMo。VLMo 参数共享,但是不存在单独编码器;ALBEF 存在单独编码器但是部分参数不共享。这篇论文存在单独的 vision encoder 和 text encoder。多模态的参数是以 cross-attention 模块插入到文本编码器实现的,cross-attention 模块享受文本编码器的参数(可以看 col 2 和 col3)

2. 增加了解码器(参考 col 4),为了做生成任务。解码器拿到视觉特征和未掩码的语言特征,过一个 casual self-attention 层,做 GPT 用的那种 lm 任务。这里区别于 MLM 的那种 mask 机制,是通过 causal self-attention 来实现因果推理的,我此时还不熟悉这个过程。

3. 除了上面的主要部分,还有一个重要的部分是利用训练好的模型生成伪标签。将训练好的模型里的不同的部分拿出来在 COCO 上稍微微调一下,decoder 部分可以生成文本,算 ITM loss 的那个模块可以做 image-text pair 的过滤,通过输出打分、置信度的方式。在实验中,BLIP 的解码能力似乎很强,用这种范式生成的文本不仅人看着觉得不错,用于自训练后也可以涨点 2-3,非常显着。

   一个例子是 stable diffusion 的官方博文里提到了,他们在做微调时,会遇到数据集只有图片没有 caption 的情况,比如 pokeman 数据。他们用 BLIP 来做caption生成,然后微调 stable diffusion 发现效果很好。

   另一个例子是知名的开源多模态数据集 LAION,他们也用了 BLIP 来辅助制作数据集。他们的过程在官网公布了,可以参考。

总结:个人感觉模型部分的改进可能有用可能没有用,但是解码器输出的 caption 确实是不错。以至于很多下游任务都拿 BLIP 来生成 caption。

CoCa

Contrastive Captioners are Image-Text Foundation Models

代码: https://github.com/lucidrains/CoCa-pytorch

它也是 ALBEF 的后续工作,模型非常像。区别在于:

1. 图像用了 attentional pooling,这在本文的实验中有效

2. 去掉了 ITM loss,目的是加快训练,原本文本需要 forward 2-3 次,去掉 ITM loss 之后只需要 forward 一次就可以了。在 ALBEF 中,ITM 需要完整的 text,而 MLM 需要掩码,所以是两次输入。在 BLIP 中,ITC 一次,ITM 因为在文本模型中插入了新的模块,所以得单独做前向。而 LM 因为用了既多了新的模块又得用 causal self-attention 所以又得单独做一次。在 CoCa 中,为了完成 captioning loss 和 ITC loss,只需要做一次前向即可。GPT 中把 cls-token 放在最后面就可以得到全局表征来做 ITC loss 了。

简单快速的方法可以有效地 scale,而我们知道复杂的模型设计、loss 设计经常不如简单地放大模型、增加数据有效。参考凯明的 FLYP。

这种画图的方式很不错,很直观。可以参考,以后也画成这样。

总结:

简单有效的结构设计,我对 CoCa 的印象是简单有效。它的峰值性能我没有感觉很炸裂,可能是模型、数据 scale 之后自然的结果。但是它的 zero-shot 性能让我印象很深刻,在 imagenet 上微调不微调的差距很小,这一点非常非常关键。

读到 coca,我对多模态的疑问还有两点:

1. mixture of experts 的结构没有在本文中得到应用,但我感觉是个相当有前途的结构

2. 双向的生成 loss 还是没人做,谁说只能图像辅助文本?

BeiTv

(BEiT-3) Image as a Foreign Language: BEiT Pretraining for All Vision and Vision-Language Tasks

论文的卖点是大一统。在 introduction 章节详细介绍了大一统指的是统一模型、loss 和数据。我觉得可以简单地概括为:用统一的 multi-way transformer (mixture of experts ) 架构和单个 masked modeling loss,将任意模态看做是同一个模态来建模。

具体而言,它指的是在将任意模态输入网络后,都表现为 list of tokens,直接将它们看做是相同的模态来做 masked modeling 就好了。如果想要拿过去做下游任务的话,直接将需要的那部分模型拿出来即可。比如做视觉任务就拿视觉模型,做语言任务就拿语言模型。如果是做多模态任务,可以灵活地模拟不同的需求,比如:1. 做生成任务可以拿多模态部分的参数出来 2. 做图文检索可以单独取出视觉部分和语言部分来模拟 CLIP。不仅仅是能做任意任务,还继承了前作的优点,比如 CLIP 这种弱跨模态交互带来的计算效率的优势。

总结:

Neural Corpus Indexer—文档检索

paper:https://arxiv.org/abs/2206.02743

神经语料库索引for文档检索

最近一篇Neural Corpus Indexer基于transformer的文档检索引发了争论。【知乎】所指论文为NeurIPS2022 Outstanding Paper A Neural Corpus Indexer for Document Retrieval。 根据OpenReview上的Revisions记录,Rebuttal阶段的最后修改应该是https://openreview.net/references/pdf?id=y45TgWUfyF,此时Table 1内容为:

但Camera Ready版本是https://openreview.net/references/pdf?id=-bt0HSi9__,此时Table 1的内容为:

特别值得注意的是,在Rebuttal阶段,作者的General Response指出他们的工作即使去掉query generation进行公平比较,也远胜于基线:

但是根据Camera Ready版本的Table 1(见上)和Table 3

NCI(Base) w/ QG是65.86 NCI(Large) w/ QG是66.23 NCI(Base) w/o QG是46.41。如果NCI(Large) w/o QG像w/ QG的设置一样只比Base高0.37,那么它将低于Table 1中的SEAL(Large),而根据General Response,作者认可SEAL是w/o QG的设置。

反思:其实在机器学习里面,如果你的实验有了好的结果,尤其是特别好的结果,那么90%的情况都是有bug造成的。所以在效果比较好的情况时候一定要去仔细检查,看看是否有数据泄漏的情况。这个错误是比较常见的。

文本检索:在一堆的文本里面,将那些跟Query相关的文档找出来。是信息检索里最大的分支。相关信息检索的会议有:SigIR、WSDN、KDD、 NeurIPS (这个 NeurIPS 上文本检索的文章比较少,是一个偏算法的会议)

摘要:

当前最主流的的文档检索解决方案主要是基于索引检索方法,索引就是指对文档做一下哈希值或者embedding,但是索引很难直接针对最终检索目标结果进行优化。 因为哈希是一个固定的算法,或者词嵌入也不一定是根据用户最终的目标来做训练的。在这篇论文中,我们的目标是展示一个端到端的深度神经网络网络统一训练和检索阶段,可以显着提高召回率。在检索方面,召回率相比于准确率更加重要,因为需要把相关的文档全部都找出来,不希望遗漏。在这个文章中,作者提出了一个基于equence-to-sequence network(NCI),针对特定的query来说直接生成相关文档的id。为了提升NCI性能,提出了一个解码器(refix-aware weight-adaptive decoder),还使用了一些其他技术:query的生成、带语义的文档的ID和一致性的正则表达项。

摘要的写法比较常见:该领域之前的方法是怎样的,我们使用一个神经网络做一个端到端的学习,从原始的数据直接生成你要的一个结果。

导言:

文档检索和排序是标准网络搜索引擎的两个关键阶段。 第一,文档检索阶段就是给定一个query,来查询相关的候选文档,然后进行排名阶段为每个文档提供更精确的排名分数。 排名阶段通常由深度神经网络,将每对查询和文档作为输入并预测它们的相关性分数。 然而,一个精确的排名模型是非常昂贵的(对每一个查询对都要去预测分数),所以通常只有一百或一千个检索的候选结果。 因此,召回性能文档检索阶段对网络搜索引擎的有效性至关重要。(检索的这几百个候选结果应该要把所有相关的都包含进来才好)。

其实除了检索的召回率很重要,对于一个检索系统来说,性能是十分重要的,作者在这没有提到,对于一个搜索引擎来说,文档数量在千百亿以上,这个也是这篇文章的一个硬伤,就是太贵了。

现有的文档检索方法可以分为两类,即term-based和基于语义的方法。基于 term 术语的检索方法一般会构建一个倒排索引对整个网络语料库(可以认为就是一个字典,字典里的每个key就是查询,key的值就是对应这个文档id(key出现在该文档中))这个方法非常高效,但它们几乎无法捕获文档语义并且无法检索到类似的不同措辞的文件(比如我输入“文件”,找到的结果只是含有该“文件“的文档,对于文件的相似表达”file“,无法检索到)。 因此,提出了基于语义的方法 来减轻这种差异。基于语义的方法就是把query和文档分别映射成向量(使用twin-tower architecture架构)。然后使用近似K紧邻搜索感兴趣的的K个文档。这种方法的缺点:对于精确匹配exact match,(苹果13和苹果12)表现不好。另外就是ANN近邻算法某些情况(query和文档之间的关系复杂)下也不太好。

端到端的相关工作:一个是DSI,Differentiable Search Index,文本到文本的生成,一个纯transformer,DSI 中的解码器没有充分利用文档标识符的层次结构。第二个SEAL 通过利用段落中的所有 n-gram 作为其标识符id。

twin-tower architecture

Neural Corpus Indexer

神经语料库索引器 (NCI) 是一种序列到序列的神经网络模型。 该模型将查询作为输入并输出最相关的文档标识符 (docid),它可以通过大量<query, docid>对进行训练。

下图就是这个模型的示意图。每次用户输入的是查询query,模型输出的是docID。那文本检索中的文档在哪?文档不可能作为输入送进模型,因为文档数量太大了,开销比较大。这个模型预测的时候不会看到文档的信息,但是做检索肯定需要模型知道各个文档的信息,所以就需要把这些文档全部放入这个模型。所以这部分数据分为两部分,一部分就是<query,docID>查询对。另一部分就是大量的被检索的文档<doc,docID>,因为模型预测的是query到docid的映射,所以需要让模型记住文档和docid的关系,常见做法就是用<doc,docid>无标号的数据去让模型记住全部的文档,当然这里可以把一个<doc,docID>对拆分成多个<query,docid>对,就是把doc里的句子给拆分成query会比较好做一些。模型的设计里有一些比较重要的点:(1)如何设计一个docID,而并非简单的数字,最好docID能够表示doc之间的语义信息。(2)如何将文档分出比较好的query,使得文档自己的语义和它的ID之间做好映射。同时分出的query能够跟预测时候的用户查询query有一定的相似性。(3)模型如何设计?编解码器和loss

NCI示意图

上图就是对应的三个关键点。

  • 如何生成语义的ID:层次Kmeans算法

首先,上图中所有的灰点都代表不同的文档,首先对所有的文档做一个K-means聚类(k=3),不同的类给与不同的id(1,2,3),作为文档id的前缀,如果某个类里面的文档数量多于某个阈值C,他就会对这个类进一步做K-means,继续分出K个子类和对应的id。因此如果两个文档的前缀相近,表示俩个文档的距离比较近。这种层次化标号的好处是如果面对10000中类别标号,直接用一个softmax来对其分类是不好的,有了层次化的标号,就可以分层次预测类别。

  • 从文本生成query

1、DocT5Query:sequence to sequence的模型,将Doc 翻译成 Query的模型。如何使用:将用于检索的文档输入到该模型,来获得多个query的输出(随机采样方法)。

2、Document as Query,像DSI一样,先把每个文档最先的64个term词作为一个query。然后随机在文档的随机位置选择10组,每组64个词作为query。(共11个query)

Prefix-aware weight-adaptive decoder:

r0,r1,r2就是不同层次的类别的id。相比传统的解码器,作者更加考虑到了r0,r1,r2之间的相对位置关系,因此解码器的输入不再是r0,r1,而是包含位置的(1,r0),(2,r1)。实验表明包含位置的解码器输入对于模型提升很大!!!!

另外作者认为在解码器的最后的softmax的全值W对于不同的ri是一样的,这样是不好的,因此希望不同的r对应不同权重。

因此新的Wi如下所示:不仅包含Wi,也包含前面的r0到ri-1的这些信息。

损失函数:

1、增加一个对比学习损失函数,希望同一个query生成的id之间相似度更加接近一些。

2、标准的 cross entropy损失函数

实验

数据集(问答数据集文档来自wiki):

评价指标:

1、Recall@N:表示在获得的N个结果中有没有自己想要的文档

2、MRR: 表示返回结果的排序情况,我们想要的文档在所有结果中的排序情况

结果:

消融实验

性能:在32G的v100上面,时延在100ms还是可以的,但是吞吐量只有50多个query对于搜索引擎来说是不能忍受的。工业部署上还是有一定的距离。

缺点:1、大数据集:目前只是在32万的文档上训练结果,但要是真的用于web搜索,数以亿计的文档需要的模型会很大。2、推理的时延和吞吐量 3、面对新的文档,如何去更新模型?

MINE–利用单张图片做三维重建

端到端类型

用MPI(Multi-Plane Image )代替NeRF的RGBσ作为网络的输出

来自字节跳动视觉技术团队的研究者将 NeRF 和 Multiplane Image(MPI)结合,提出了一种新的三维空间表达方式 MINE。该方法通过对单张图片做三维重建,实现新视角合成和深度估算。

开源了训练代码(基于LLFF数据集的toy example),paper里面数据集的pretrained models,并提供了demo代码:

相关工作

近年来,在新视角合成这个领域里,最火爆的方法无疑是 ECCV 2020 的 NeRF [5]。与传统的一些手工设计的显式三维表达(Light Fields,LDI,MPI 等)不同,NeRF 把整个三维空间的几何信息与 texture 信息全部用一个 MLP 的权重来表达,输入任意一个空间坐标以及观察角度,MLP 会预测一个 RGB 值和 volume density。目标图片的渲染通过 ray tracing 和 volume rendering 的方式来完成。尽管 NeRF 的效果非常惊艳,但它的缺点也非常明显:

  1. 一个模型只能表达一个场景,且优化一个场景耗时久;
  2. per-pixel 渲染较为低效;
  3. 泛化能力较差,一个场景需要较多的照片才能训练好。

另外一个与该研究较相关的是 MPI(Multiplane Image)[1, 2, 3]。MPI 包含了多个平面的 RGB-alpha 图片,其中每个平面表达场景在某个深度中的内容,它的主要缺点在于深度是固定及离散的,这个缺点限制了它对三维空间的表达能力。[1, 2, 3] 都能方便地泛化到不同的场景,然而 MPI 各个平面的深度是固定且离散的,这个缺点严重限制了它的效果。

结合了NeRF和Multiplane Image(MPI),提出了一种新的三维空间表达方式MINE。MINE利用了NeRF的思路,将MPI扩展成了连续深度的形式。输入单张RGB图片,我们的方法会对source相机的视锥(frustum)做稠密的三维重建,同时对被遮挡的部分做inpainting,预测出相机视锥的三维表达。利用这个三维表达,给出target相机相对于source相机的在三维空间中的相对位置和角度变化(rotation and translation),我们可以方便且高效地渲染出在目标相机视图下的RGB图片以及深度图。

MINE在KITTI,RealEstate10K以及Flowers Light Fields数据集上,生成质量大幅超过了当前单视图合成的state-of-the-art。同时,在深度估计benchmark iBims-1和NYU-v2上,虽然我们在训练中只使用了RGB图片和sparse深度监督,MINE在单目深度估计任务上取得了非常接近全监督state-of-the-art的performance,并大幅超越了其他弱监督的方法。

Introduction and Related Works

视图合成(novel view synthesis)需要解决的问题是:在一个场景(scene)下,输入一个或多个图片,它们各自的相机内参和外参(source camera pose),之后对于任意的相机位置和角度(target camera pose),我们想要生成场景在该相机视图下的RGB图片。要解决这个问题,我们的模型需要学会场景的几何结构,同时对被遮挡的部分做inpainting。学术界设计了很多利用learning的方法预测场景的3D/2.5D表达,其中跟我们较相关的是MPI(Multiplane Image)[1, 2, 3]。MPI包含了多个平面的 RGB-α图片,其中每个平面表达场景在某个深度中的内容,它的主要缺点在于深度是固定及离散的,这个缺点限制了它对三维空间的表达能力。

近年来,这个领域的当红炸子鸡无疑是ECCV 2020的NeRF [5]。与传统的一些手工设计的显式三维表达(Light Fields,LDI,MPI等)不同,NeRF把整个三维空间的几何信息与texture信息全部用一个MLP的权重来表达,输入任意一个空间坐标以及观察角度,MLP会预测一个RGB值和volume density。目标图片的渲染通过ray tracing和volume rendering的方式来完成。尽管NeRF的效果非常惊艳,但它的缺点也非常明显:1. 一个模型只能表达一个场景,且优化一个场景耗时久;2. per-pixel渲染较为低效;3. 泛化能力较差,一个场景需要较多的照片才能训练好。

方法综述

该团队采用一个 encoder-decoder 的结构来生成三维表达:

  1. Encoder 是一个全卷积网络,输入为单个 RGB 图片,输出为 feature maps;
  2. Decoder 也是一个全卷积网络,输入为 encoder 输出的 feature map,以及任意深度值(repeat + concat),输出该深度下的 RGB-sigma 图片;
  3. 最终的三维表达由多个平面组成,也就是说在一次完整的 forward 中,encoder 需要 inference 一次,而 decoder 需要 inference N 次获得个 N 平面。

获得三维表达后,不再需要任何的网络 inference,渲染任意 target 相机 pose 下的视角只需要两步:

  1. 利用 homography wrapping 建立像素点间的 correspondence。可以想象,从 target 相机射出一条光线,这条光线与 target 图片的一个像素点相交,然后,研究者延长这条射线,让它与 source 相机视锥的各个平面相交。相交点的 RGB-sigma 值可以通过 bilinear sampling 获得;
  2. 利用 volume rendering 将光线上的点渲染到目标图片像素点上,获得该像素点的 RGB 值与深度。

三维表达与渲染

1. Planar Neural Radiance Field

2. Rendering Process

完成这两步之后,我们就可以通过上面volume rendering的公式渲染任意target camera下的视图了。需要注意的是,在获得3D表达后,渲染任意target camera pose下的视图都只需要这两个步骤,无需再做额外的网络inference

Scale 校正

MINE 可以利用 structure-from-motion 计算的相机参数与点云进行场景的学习,在这种情况下,深度是 ambiguous 的。由于在这个方法中,深度采样的范围是固定的。所以需要计算一个 scale factor,使网络预测的 scale 与 structure-from-motion 的 scale 进行对齐。团队利用通过 Structure from Motion 获得的每个图片的可见 3D 点 P 以及网络预测的深度图 Z 计算 scale factor:

获得 scale factor 后,对相机的位移进行 scale:

需要注意的是,由于需要和 ground truth 比较,所以在训练和测试时需要做 scale calibration。而在部署时不需要做这一步。

端到端的训练

MINE 可以仅通过 RGB 图片学习到场景的三维几何信息,训练 Loss 主要由两部分组成:

1.Reconsturction loss——计算渲染出的 target 图片与 ground truth 的差异:

2.Edge-aware smoothness loss——确保在图片颜色没有突变的地方,深度也不会突变,这里主要参考了 monodepth2 [6] 种的实现:

3.Sparse disparity loss——在训练集各场景的 scale 不一样时,利用 structure-from-motion 获得的稀疏点云辅助场景几何信息的学习:

MINE 与 MPI、NeRF 的比较

MINE 是 MPI 的一种连续深度的扩展,相比于 MPI 和 NeRF,MINE 有几个明显的优势:

  1. 与 NeRF 相比,MINE 能够泛化到训练集没有出现过的场景;
  2. 与 NeRF 的逐点渲染相比,MINE 的渲染非常高效;
  3. 与 MPI 相比,MINE 的深度是连续的,能稠密地表示相机的视锥;
  4. MPI 通过 alpha 合成(alpha compositing)进行渲染,但该方法与射线上点之间的距离无关,而 MINE 利用 volume rendering 解决了这个限制。

然而,MINE 也有一些自身的局限性:

  1. 由于输入是单张图片,MINE 无法表达相机视锥以外的三维空间;
  2. 由于 MINE 的输入里没有观察角度,所以其无法对一些复杂的 view-dependent 效果(如光盘上的彩虹等)进行建模。

[1]. Tinghui Zhou, Richard Tucker, John Flynn, Graham Fyffe, Noah Snavely. Stereo Magnification: Learning View Synthesis using Multiplane Images. (SIGGRAPH 2018)

[2]. Ben Mildenhall, Pratul P. Srinivasan, Rodrigo Ortiz-Cayon, Nima Khademi Kalantari, Ravi Ramamoorthi, Ren Ng, Abhishek Kar. Local Light Field Fusion: Practical View Synthesis with Prescriptive Sampling Guidelines. (SIGGRAPH 2019)

[3]. Richard Tucker, Noah Snavely. Single-View View Synthesis with Multiplane Images. (CVPR 2020)

InstructGPT /ChatGPT

最近非常火的ChatGPT和今年年初公布的[1]是一对姐妹模型,是在GPT-4之前发布的预热模型,有时候也被叫做GPT3.5。ChatGPT和InstructGPT在模型结构,训练方式上都完全一致,即都使用了指示学习(Instruction Learning)和人工反馈的强化学习(Reinforcement Learning from Human Feedback,RLHF)来指导模型的训练,它们不同的仅仅是采集数据的方式上有所差异。所以要搞懂ChatGPT,我们必须要先读懂InstructGPT

https://arxiv.org/abs/2203.02155

Prompt是激发语言模型的补全能力,例如根据上半句生成下半句,或是完形填空等。Instruct(指令)是激发语言模型的理解能力,它通过给出更明显的指令,让模型去做出正确的行动。

  1. 提示学习:给女朋友买了这个项链,她很喜欢,这个项链太____了。
  2. 指示学习:判断这句话的情感:给女朋友买了这个项链,她很喜欢。选项:A=好;B=一般;C=差。

1. 背景知识

在介绍ChatGPT/InstructGPT之前,我们先介绍它们依赖的基础算法。

1.1 GPT系列

基于文本预训练的GPT-1[2],GPT-2[3],GPT-3[4]三代模型都是采用的以Transformer为核心结构的模型(图1),不同的是模型的层数和词向量长度等超参,它们具体的内容如表1。

图1:GPT系列的模型结构(其中Trm是一个Transformer结构)

表1:历代GPT的发布时间,参数量以及训练量

模型发布时间层数头数词向量长度参数量预训练数据量
GPT-12018 年 6 月12127681.17 亿约 5GB
GPT-22019 年 2 月48160015 亿40GB
GPT-32020 年 5 月9696128881,750 亿45TB

GPT-1比BERT诞生略早几个月。它们都是采用了Transformer为核心结构,不同的是GPT-1通过自左向右生成式的构建预训练任务,然后得到一个通用的预训练模型,这个模型和BERT一样都可用来做下游任务的微调。GPT-1当时在9个NLP任务上取得了SOTA的效果,但GPT-1使用的模型规模和数据量都比较小,这也就促使了GPT-2的诞生。

对比GPT-1,GPT-2并未在模型结构上大作文章,只是使用了更多参数的模型和更多的训练数据(表1)。GPT-2最重要的思想是提出了“所有的有监督学习都是无监督语言模型的一个子集”的思想,这个思想也是提示学习(Prompt Learning)的前身。GPT-2在诞生之初也引发了不少的轰动,它生成的新闻足以欺骗大多数人类,达到以假乱真的效果。甚至当时被称为“AI界最危险的武器”,很多门户网站也命令禁止使用GPT-2生成的新闻。

GPT-3被提出时,除了它远超GPT-2的效果外,引起更多讨论的是它1750亿的参数量。GPT-3除了能完成常见的NLP任务外,研究者意外的发现GPT-3在写SQL,JavaScript等语言的代码,进行简单的数学运算上也有不错的表现效果。GPT-3的训练使用了情境学习(In-context Learning),它是元学习(Meta-learning)的一种,元学习的核心思想在于通过少量的数据寻找一个合适的初始化范围,使得模型能够在有限的数据集上快速拟合,并获得不错的效果。

通过上面的分析我们可以看出从性能角度上讲,GPT有两个目标:

  1. 提升模型在常见NLP任务上的表现效果;
  2. 提升模型在其他非典型NLP任务(例如代码编写,数学运算)上的泛化能力。

另外,预训练模型自诞生之始,一个备受诟病的问题就是预训练模型的偏见性。因为预训练模型都是通过海量数据在超大参数量级的模型上训练出来的,对比完全由人工规则控制的专家系统来说,预训练模型就像一个黑盒子。没有人能够保证预训练模型不会生成一些包含种族歧视,性别歧视等危险内容,因为它的几十GB甚至几十TB的训练数据里几乎肯定包含类似的训练样本。这也就是InstructGPT和ChatGPT的提出动机,论文中用3H概括了它们的优化目标:

  • 有用的(Helpful);
  • 可信的(Honest);
  • 无害的(Harmless)。

OpenAI的GPT系列模型并没有开源,但是它们提供了模型的试用网站,有条件的同学可以自行试用。

1.2 指示学习(Instruct Learning)和提示(Prompt Learning)学习

指示学习是谷歌Deepmind的Quoc V.Le团队在2021年的一篇名为《Finetuned Language Models Are Zero-Shot Learners》[5]文章中提出的思想。指示学习和提示学习的目的都是去挖掘语言模型本身具备的知识。不同的是Prompt是激发语言模型的补全能力,例如根据上半句生成下半句,或是完形填空等。Instruct是激发语言模型的理解能力,它通过给出更明显的指令,让模型去做出正确的行动。我们可以通过下面的例子来理解这两个不同的学习方式:

  1. 提示学习:给女朋友买了这个项链,她很喜欢,这个项链太____了。
  2. 指示学习:判断这句话的情感:给女朋友买了这个项链,她很喜欢。选项:A=好;B=一般;C=差。

指示学习的优点是它经过多任务的微调后,也能够在其他任务上做zero-shot,而提示学习都是针对一个任务的。泛化能力不如指示学习。我们可以通过图2来理解微调,提示学习和指示学习。

图2:模型微调,提示学习,指示学习三者的异同

1.3 人工反馈的强化学习

因为训练得到的模型并不是非常可控的,模型可以看做对训练集分布的一个拟合。那么反馈到生成模型中,训练数据的分布便是影响生成内容的质量最重要的一个因素。有时候我们希望模型并不仅仅只受训练数据的影响,而是人为可控的,从而保证生成数据的有用性,真实性和无害性。论文中多次提到了对齐(Alignment)问题,我们可以理解为模型的输出内容和人类喜欢的输出内容的对齐,人类喜欢的不止包括生成内容的流畅性和语法的正确性,还包括生成内容的有用性、真实性和无害性。

我们知道强化学习通过奖励(Reward)机制来指导模型训练,奖励机制可以看做传统模训练机制的损失函数。奖励的计算要比损失函数更灵活和多样(AlphaGO的奖励是对局的胜负),这带来的代价是奖励的计算是不可导的,因此不能直接拿来做反向传播。强化学习的思路是通过对奖励的大量采样来拟合损失函数,从而实现模型的训练。同样人类反馈也是不可导的,那么我们也可以将人工反馈作为强化学习的奖励,基于人工反馈的强化学习便应运而生。

RLHF最早可以追溯到Google在2017年发表的《Deep Reinforcement Learning from Human Preferences》[6],它通过人工标注作为反馈,提升了强化学习在模拟机器人以及雅达利游戏上的表现效果。

图3:人工反馈的强化学习的基本原理

InstructGPT/ChatGPT中还用到了强化学习中一个经典的算法:OpenAI提出的最近策略优化(Proximal Policy Optimization,PPO)[7]。PPO算法是一种新型的Policy Gradient算法,Policy Gradient算法对步长十分敏感,但是又难以选择合适的步长,在训练过程中新旧策略的的变化差异如果过大则不利于学习。PPO提出了新的目标函数可以在多个训练步骤实现小批量的更新,解决了Policy Gradient算法中步长难以确定的问题。其实TRPO也是为了解决这个思想但是相比于TRPO算法PPO算法更容易求解。

2. InstructGPT/ChatGPT原理解读

有了上面这些基础知识,我们再去了解InstructGPT和ChatGPT就会简单很多。简单来说,InstructGPT/ChatGPT都是采用了GPT-3的网络结构,通过指示学习构建训练样本来训练一个反应预测内容效果的奖励模型(RM),最后通过这个奖励模型的打分来指导强化学习模型的训练。InstructGPT/ChatGPT的训练流程如图4所示。

图4:InstructGPT的计算流程:(1)有监督微调(SFT);(2)奖励模型(RM)训练;(3)通过PPO根据奖励模型进行强化学习。

从图4中我们可以看出,InstructGPT/ChatGPT的训练可以分成3步,其中第2步和第3步是的奖励模型和强化学习的SFT模型可以反复迭代优化。

  1. 根据采集的SFT数据集对GPT-3进行有监督的微调(Supervised FineTune,SFT);
  2. 收集人工标注的对比数据,训练奖励模型(Reword Model,RM);
  3. 使用RM作为强化学习的优化目标,利用PPO算法微调SFT模型。

根据图4,我们将分别介绍InstructGPT/ChatGPT的数据集采集和模型训练两个方面的内容。

2.1 数据集采集

如图4所示,InstructGPT/ChatGPT的训练分成3步,每一步需要的数据也有些许差异,下面我们分别介绍它们。

2.1.1 SFT数据集

SFT数据集是用来训练第1步有监督的模型,即使用采集的新数据,按照GPT-3的训练方式对GPT-3进行微调。因为GPT-3是一个基于提示学习的生成模型,因此SFT数据集也是由提示-答复对组成的样本。SFT数据一部分来自使用OpenAI的PlayGround的用户,另一部分来自OpenAI雇佣的40名标注工(labeler)。并且他们对labeler进行了培训。在这个数据集中,标注工的工作是根据内容自己编写指示,并且要求编写的指示满足下面三点:

  • 简单任务:labeler给出任意一个简单的任务,同时要确保任务的多样性;
  • Few-shot任务:labeler给出一个指示,以及该指示的多个查询-相应对;
  • 用户相关的:从接口中获取用例,然后让labeler根据这些用例编写指示。

2.1.2 RM数据集

RM数据集用来训练第2步的奖励模型,我们也需要为InstructGPT/ChatGPT的训练设置一个奖励目标。这个奖励目标不必可导,但是一定要尽可能全面且真实的对齐我们需要模型生成的内容。很自然的,我们可以通过人工标注的方式来提供这个奖励,通过人工对可以给那些涉及偏见的生成内容更低的分从而鼓励模型不去生成这些人类不喜欢的内容。InstructGPT/ChatGPT的做法是先让模型生成一批候选文本,让后通过labeler根据生成数据的质量对这些生成内容进行排序。

2.1.3 PPO数据集

InstructGPT的PPO数据没有进行标注,它均来自GPT-3的API的用户。既又不同用户提供的不同种类的生成任务,其中占比最高的包括生成任务(45.6%),QA(12.4%),头脑风暴(11.2%),对话(8.4%)等。

2.1.4 数据分析

因为InstructGPT/ChatGPT是在GPT-3基础上做的微调,而且因为涉及了人工标注,它们数据总量并不大,表2展示了三份数据的来源及其数据量。

表2:InstructGPT的数据分布

论文的附录A对数据的分布进行了更详细的讨论,这里我列出几个可能影响模型效果的几项:

  • 数据中96%以上是英文,其它20个语种例如中文,法语,西班牙语等加起来不到4%,这可能导致InstructGPT/ChatGPT能进行其它语种的生成,但效果应该远不如英文;
  • 提示种类共有9种,而且绝大多数是生成类任务,可能会导致模型有覆盖不到的任务类型;
  • 40名外包员工来自美国和东南亚,分布比较集中且人数较少, InstructGPT/ChatGPT的目标是训练一个价值观正确的预训练模型,它的价值观是由这40个外包员工的价值观组合而成。而这个比较窄的分布可能会生成一些其他地区比较在意的歧视,偏见问题。

此外,ChatGPT的博客中讲到ChatGPT和InstructGPT的训练方式相同,不同点仅仅是它们采集数据上有所不同,但是并没有更多的资料来讲数据采集上有哪些细节上的不同。考虑到ChatGPT仅仅被用在对话领域,这里我猜测ChatGPT在数据采集上有两个不同:1. 提高了对话类任务的占比;2. 将提示的方式转换Q&A的方式。当然这里也仅仅是猜测,更准确的描述要等到ChatGPT的论文、源码等更详细的资料公布我们才能知道。

2.2 训练任务

我们刚介绍到InstructGPT/ChatGPT有三步训练方式。这三步训练会涉及三个模型:SFT,RM以及PPO,下面我们详细介绍它们。

2.2.1 有监督微调(SFT)

这一步的训练和GPT-3一致,而且作者发现让模型适当过拟合有助于后面两步的训练。

2.2.2 奖励模型(RM)

因为训练RM的数据是一个labeler根据生成结果排序的形式,所以它可以看做一个回归模型。RM结构是将SFT训练后的模型的最后的嵌入层去掉后的模型。它的输入是prompt和Reponse,输出是奖励值。具体的讲,对弈每个prompt,InstructGPT/ChatGPT会随机生成 K 个输出( 4≤K≤9 ),然后它们向每个labeler成对的展示输出结果,也就是每个prompt共展示 CK2 个结果,然后用户从中选择效果更好的输出。在训练时,InstructGPT/ChatGPT将每个prompt的 CK2 个响应对作为一个batch,这种按prompt为batch的训练方式要比传统的按样本为batch的方式更不容易过拟合,因为这种方式每个prompt会且仅会输入到模型中一次。

奖励模型的损失函数表示为式(1)。这个损失函数的目标是最大化labeler更喜欢的响应和不喜欢的响应之间的差值。

2.2.3 强化学习模型(PPO)

强化学习和预训练模型是最近两年最为火热的AI方向之二,之前不少科研工作者说强化学习并不是一个非常适合应用到预训练模型中,因为很难通过模型的输出内容建立奖励机制。而InstructGPT/ChatGPT反直觉的做到了这点,它通过结合人工标注,将强化学习引入到预训练语言模型是这个算法最大的创新点。

如表2所示,PPO的训练集完全来自API。它通过第2步得到的奖励模型来指导SFT模型的继续训练。很多时候强化学习是非常难训练的,InstructGPT/ChatGPT在训练过程中就遇到了两个问题:

  1. 问题1:随着模型的更新,强化学习模型产生的数据和训练奖励模型的数据的差异会越来越大。作者的解决方案是在损失函数中加入KL惩罚项 βlog⁡(πϕRL(y∣x)/πSFT(y∣x)) 来确保PPO模型的输出和SFT的输出差距不会很大。
  2. 问题2:只用PPO模型进行训练的话,会导致模型在通用NLP任务上性能的大幅下降,作者的解决方案是在训练目标中加入了通用的语言模型目标 γEx∼Dpretrain [log⁡(πϕRL(x))] ,这个变量在论文中被叫做PPO-ptx。

综上,PPO的训练目标为式(2)。

3. InstructGPT/ChatGPT的性能分析

不可否认的是,InstructGPT/ChatGPT的效果是非常棒的,尤其是引入了人工标注之后,让模型的“价值观”和的正确程度和人类行为模式的“真实性”上都大幅的提升。那么,仅仅根据InstructGPT/ChatGPT的技术方案和训练方式,我们就可以分析出它可以带来哪些效果提升呢?

3.1 优点

  • InstructGPT/ChatGPT的效果比GPT-3更加真实:这个很好理解,因为GPT-3本身就具有非常强的泛化能力和生成能力,再加上InstructGPT/ChatGPT引入了不同的labeler进行提示编写和生成结果排序,而且还是在GPT-3之上进行的微调,这使得我们在训练奖励模型时对更加真实的数据会有更高的奖励。作者也在TruthfulQA数据集上对比了它们和GPT-3的效果,实验结果表明甚至13亿小尺寸的PPO-ptx的效果也要比GPT-3要好。
  • InstructGPT/ChatGPT在模型的无害性上比GPT-3效果要有些许提升:原理同上。但是作者发现InstructGPT在歧视、偏见等数据集上并没有明显的提升。这是因为GPT-3本身就是一个效果非常好的模型,它生成带有有害、歧视、偏见等情况的有问题样本的概率本身就会很低。仅仅通过40个labeler采集和标注的数据很可能无法对模型在这些方面进行充分的优化,所以会带来模型效果的提升很少或者无法察觉。
  • InstructGPT/ChatGPT具有很强的Coding能力:首先GPT-3就具有很强的Coding能力,基于GPT-3制作的API也积累了大量的Coding代码。而且也有部分OpenAI的内部员工参与了数据采集工作。通过Coding相关的大量数据以及人工标注,训练出来的InstructGPT/ChatGPT具有非常强的Coding能力也就不意外了。

3.2 缺点

  • InstructGPT/ChatGPT会降低模型在通用NLP任务上的效果:我们在PPO的训练的时候讨论了这点,虽然修改损失函数可以缓和,但这个问题并没有得到彻底解决。
  • 有时候InstructGPT/ChatGPT会给出一些荒谬的输出:虽然InstructGPT/ChatGPT使用了人类反馈,但限于人力资源有限。影响模型效果最大的还是有监督的语言模型任务,人类只是起到了纠正作用。所以很有可能受限于纠正数据的有限,或是有监督任务的误导(只考虑模型的输出,没考虑人类想要什么),导致它生成内容的不真实。就像一个学生,虽然有老师对他指导,但也不能确定学生可以学会所有知识点。
  • 模型对指示非常敏感:这个也可以归结为labeler标注的数据量不够,因为指示是模型产生输出的唯一线索,如果指示的数量和种类训练的不充分的话,就可能会让模型存在这个问题。
  • 模型对简单概念的过分解读:这可能是因为labeler在进行生成内容的比较时,倾向于给给长的输出内容更高的奖励。
  • 对有害的指示可能会输出有害的答复:例如InstructGPT/ChatGPT也会对用户提出的“AI毁灭人类计划书”给出行动方案(图5)。这个是因为InstructGPT/ChatGPT假设labeler编写的指示是合理且价值观正确的,并没有对用户给出的指示做更详细的判断,从而会导致模型会对任意输入都给出答复。虽然后面的奖励模型可能会给这类输出较低的奖励值,但模型在生成文本时,不仅要考虑模型的价值观,也要考虑生成内容和指示的匹配度,有时候生成一些价值观有问题的输出也是可能的。
图5:ChatGPT编写的毁灭人类计划书。

3.3 未来工作

我们已经分析了InstrcutGPT/ChatGPT的技术方案和它的问题,那么我们也可以看出InstrcutGPT/ChatGPT的优化角度有哪些了。

  • 人工标注的降本增效:InstrcutGPT/ChatGPT雇佣了40人的标注团队,但从模型的表现效果来看,这40人的团队是不够的。如何让人类能够提供更有效的反馈方式,将人类表现和模型表现有机和巧妙的结合起来是非常重要的。
  • 模型对指示的泛化/纠错等能力:指示作为模型产生输出的唯一线索,模型对他的依赖是非常严重的,如何提升模型对指示的泛化能力以及对错误指示示的纠错能力是提升模型体验的一个非常重要的工作。这不仅可以让模型能够拥有更广泛的应用场景,还可以让模型变得更“智能”。
  • 避免通用任务性能下降:这里可能需要设计一个更合理的人类反馈的使用方式,或是更前沿的模型结构。因为我们讨论了InstrcutGPT/ChatGPT的很多问题可以通过提供更多labeler标注的数据来解决,但这会导致通用NLP任务更严重的性能下降,所以需要方案来让生成结果的3H和通用NLP任务的性能达到平衡。

3.4 InstrcutGPT/ChatGPT的热点话题解答

  • ChatGPT的出现会不会导致底层程序员失业?从ChatGPT的原理和网上漏出的生成内容来看,ChatGPT生成的代码很多可以正确运行。但程序员的工作不止是写代码,更重要的是找到问题的解决方案。所以ChatGPT并不会取代程序员,尤其是高阶程序员。相反它会向现在很多的代码生成工具一样,成为程序员写代码非常有用的工具。
  • Stack Overflow 宣布临时规则:禁止 ChatGPT。ChatGPT本质上还是一个文本生成模型,对比生成代码,它更擅长生成以假乱真的文本。而且文本生成模型生成的代码或者解决方案并不能保证是可运行而且是可以解决问题的,但它以假乱真的文本又会迷惑很多查询这个问题的人。Stack Overflow为了维持论坛的质量,封禁ChatGPT也是清理之中。
  • 聊天机器人 ChatGPT 在诱导下写出「毁灭人类计划书」,并给出代码,AI 发展有哪些问题需关注?ChatGPT的「毁灭人类计划书」是它在不可遇见的指示下根据海量数据强行拟合出来的生成内容。虽然这些内容看起来很真实,表达也很流畅,这说明的只是ChatGPT具有非常强的生成效果,并不表示ChatGPT具备毁灭人类的思想。因为他仅仅是一个文本生成模型,并不是一个决策模型。

4. 总结

就像很多人们算法刚诞生时一样,ChatGPT凭借有用性,真实性,无害性的效果,引起了业内广泛的关注和人类对AI的思考。但是当我们看完它的算法原理之后,发现它并没有业内宣传的那么恐怖。反而我们可以从它的技术方案中学到很多有价值的东西。InstrcutGPT/ChatGPT在AI界最重要的贡献是将强化学习和预训练模型巧妙的结合起来。而且通过人工反馈提升了模型的有用性,真实性和无害性。ChatGPT也进一步提升大模型的成本,之前还只是比拼数据量和模型规模,现在甚至也引入了雇佣的外包这一支出,让个体工作者更加望而却步。

参考

  1. ^Ouyang, Long, et al. “Training language models to follow instructions with human feedback.” *arXiv preprint arXiv:2203.02155* (2022). https://arxiv.org/pdf/2203.02155.pdf
  2. ^Radford, A., Narasimhan, K., Salimans, T. and Sutskever, I., 2018. Improving language understanding by generative pre-training. https://www.cs.ubc.ca/~amuham01/LING530/papers/radford2018improving.pdf
  3. ^Radford, A., Wu, J., Child, R., Luan, D., Amodei, D. and Sutskever, I., 2019. Language models are unsupervised multitask learners. *OpenAI blog*, *1*(8), p.9. https://life-extension.github.io/2020/05/27/GPT%E6%8A%80%E6%9C%AF%E5%88%9D%E6%8E%A2/language-models.pdf
  4. ^Brown, Tom B., Benjamin Mann, Nick Ryder, Melanie Subbiah, Jared Kaplan, Prafulla Dhariwal, Arvind Neelakantan et al. “Language models are few-shot learners.” *arXiv preprint arXiv:2005.14165* (2020). https://proceedings.neurips.cc/paper/2020/file/1457c0d6bfcb4967418bfb8ac142f64a-Paper.pdf
  5. ^Wei, Jason, et al. “Finetuned language models are zero-shot learners.” *arXiv preprint arXiv:2109.01652* (2021). https://arxiv.org/pdf/2109.01652.pdf
  6. ^Christiano, Paul F., et al. “Deep reinforcement learning from human preferences.” *Advances in neural information processing systems* 30 (2017). https://arxiv.org/pdf/1706.03741.pdf
  7. ^Schulman, John, et al. “Proximal policy optimization algorithms.” *arXiv preprint arXiv:1707.06347* (2017). https://arxiv.org/pdf/1707.06347.pdf

神经辐射场(NeRF)-代码解析

参考:Dasuda​ and Liwen.site

参考代码:Nerf-pl: https://github.com/kwea123/nerf_pl

位置编码

NeRF 的输入是一个五维向量: (物体)空间点的位置x=(x,y,z) 和 (相机)观测方向d=(θ,ϕ)。NeRF 使用了位置编码(positional encoding)把一维的位置坐标,转换为高维的表征。例如 p∈RL, 通过函数γ(⋅) 映射到R2L 空间中,这里L 指的是编码的数量,对于位置坐标,L=10;对于观测角度,L=4。

代码实现

 # 类的定义
class Embedding(nn.Module):
    def __init__(self, in_channels, N_freqs, logscale=True):
        """
        Defines a function that embeds x to (x, sin(2^k x), cos(2^k x), ...)
        in_channels: number of input channels (3 for both xyz and direction)
        """
        super(Embedding, self).__init__()
        self.N_freqs = N_freqs
        self.in_channels = in_channels
        self.funcs = [torch.sin, torch.cos]
        self.out_channels = in_channels*(len(self.funcs)*N_freqs+1)
 
        if logscale:
            self.freq_bands = 2**torch.linspace(0, N_freqs-1, N_freqs)
        else:
            self.freq_bands = torch.linspace(1, 2**(N_freqs-1), N_freqs)
 
    def forward(self, x):
        """
        Embeds x to (x, sin(2^k x), cos(2^k x), ...) 
        Different from the paper, "x" is also in the output
        See https://github.com/bmild/nerf/issues/12
 
        Inputs:
            x: (B, self.in_channels)
 
        Outputs:
            out: (B, self.out_channels)
        """
        out = [x]
        for freq in self.freq_bands:
            for func in self.funcs:
                out += [func(freq*x)]
 
        return torch.cat(out, -1)
 
# 使用
 
class NeRFSystem(LightningModule):
    def __init__(self, hparams):
        ...
        self.embedding_xyz = Embedding(3, 10) # 10 is the default number
        self.embedding_dir = Embedding(3, 4) # 4 is the default number
        self.embeddings = [self.embedding_xyz, self.embedding_dir]
        ...   

解释

  • 对于位置坐标 (x,y,z), 每一个值都使用 10 个 sin 和 10 个cos 频率进行拓展。例如 Embeds x to (x, sin (2^k x), cos (2^k x), …) 。再连接一个本身。因此每一个值都拓展为 10+10+1=21维。对于位置坐标的三个值,总共有 3×21=63 维。
  • 对于相机角度 (θ,ϕ),也是类似,使用 4 个sin 和 4 个 cos 频率进行拓展。这里输入保留了一位,实际输入是(θ,ϕ,1)。再连接一个本身。因此每一个值都拓展为4+4+1=9 维。对于相机角度的三个值,总共有 3×9=27 维。

NeRF 网络

NeRF 网络默认是一个多层的 MLP。中间第四层有 skip connection,构成了一个 ResNet 的结构。网络的宽度默认为 256。

输入

  1. 位置坐标的表征(in_channels_xyz):63d

输出

  1. 体密度σ:1d
  2. RGB 色彩值C: 3d

网络结构
FC 指的是带 ReLU 的全连接层。Linear 层指的是单纯的线性方程。

quicker_40ae8453-64cb-4645-8a27-e62001a85aa2.png

代码实现

class NeRF(nn.Module):
    def __init__(self,
                 D=8, W=256,
                 in_channels_xyz=63, in_channels_dir=27, 
                 skips=[4]):
        """
        D: number of layers for density (sigma) encoder
        W: number of hidden units in each layer
        in_channels_xyz: number of input channels for xyz (3+3*10*2=63 by default)
        in_channels_dir: number of input channels for direction (3+3*4*2=27 by default)
        skips: add skip connection in the Dth layer
        """
        super(NeRF, self).__init__()
        self.D = D
        self.W = W
        self.in_channels_xyz = in_channels_xyz
        self.in_channels_dir = in_channels_dir
        self.skips = skips
 
        # xyz encoding layers
        for i in range(D):
            if i == 0:
                layer = nn.Linear(in_channels_xyz, W)
            elif i in skips:
                layer = nn.Linear(W+in_channels_xyz, W)
            else:
                layer = nn.Linear(W, W)
            layer = nn.Sequential(layer, nn.ReLU(True))
            setattr(self, f"xyz_encoding_{i+1}", layer)
        self.xyz_encoding_final = nn.Linear(W, W)
 
        # direction encoding layers
        self.dir_encoding = nn.Sequential(
                                nn.Linear(W+in_channels_dir, W//2),
                                nn.ReLU(True))
 
        # output layers
        self.sigma = nn.Linear(W, 1)
        self.rgb = nn.Sequential(
                        nn.Linear(W//2, 3),
                        nn.Sigmoid())
 
    def forward(self, x, sigma_only=False):
        """
        Encodes input (xyz+dir) to rgb+sigma (not ready to render yet).
        For rendering this ray, please see rendering.py
 
        Inputs:
            x: (B, self.in_channels_xyz(+self.in_channels_dir))
               the embedded vector of position and direction
            sigma_only: whether to infer sigma only. If True,
                        x is of shape (B, self.in_channels_xyz)
 
        Outputs:
            if sigma_ony:
                sigma: (B, 1) sigma
            else:
                out: (B, 4), rgb and sigma
        """
        if not sigma_only:
            input_xyz, input_dir = \
                torch.split(x, [self.in_channels_xyz, self.in_channels_dir], dim=-1)
        else:
            input_xyz = x
 
        xyz_ = input_xyz
        for i in range(self.D):
            if i in self.skips:
                xyz_ = torch.cat([input_xyz, xyz_], -1)
            xyz_ = getattr(self, f"xyz_encoding_{i+1}")(xyz_)
 
        sigma = self.sigma(xyz_)
        if sigma_only:
            return sigma
 
        xyz_encoding_final = self.xyz_encoding_final(xyz_)
 
        dir_encoding_input = torch.cat([xyz_encoding_final, input_dir], -1)
        dir_encoding = self.dir_encoding(dir_encoding_input)
        rgb = self.rgb(dir_encoding)
 
        out = torch.cat([rgb, sigma], -1)
 
        return out

体素渲染

假设我们已经得到了一束光线上所有的位置对应的色彩和体密度。我们需要对这束光线进行后处理(体素渲染),得到最终在图片上的像素值。

# z_vals: (N_rays, N_samples_) depths of the sampled positions
# noise_std: factor to perturb the model's prediction of sigma(提升模型鲁棒性??)
 
# Convert these values using volume rendering (Section 4)
deltas = z_vals[:, 1:] - z_vals[:, :-1] # (N_rays, N_samples_-1)
delta_inf = 1e10 * torch.ones_like(deltas[:, :1]) # (N_rays, 1) the last delta is infinity
deltas = torch.cat([deltas, delta_inf], -1)  # (N_rays, N_samples_)
 
# Multiply each distance by the norm of its corresponding direction ray
# to convert to real world distance (accounts for non-unit directions).
deltas = deltas * torch.norm(dir_.unsqueeze(1), dim=-1)
 
noise = torch.randn(sigmas.shape, device=sigmas.device) * noise_std
 
# compute alpha by the formula (3)
alphas = 1-torch.exp(-deltas*torch.relu(sigmas+noise)) # (N_rays, N_samples_)
alphas_shifted = \
    torch.cat([torch.ones_like(alphas[:, :1]), 1-alphas+1e-10], -1) # [1, a1, a2, ...]
weights = \
    alphas * torch.cumprod(alphas_shifted, -1)[:, :-1] # (N_rays, N_samples_)
weights_sum = weights.sum(1) # (N_rays), the accumulated opacity along the rays
                                # equals "1 - (1-a1)(1-a2)...(1-an)" mathematically
if weights_only:
    return weights
 
# compute final weighted outputs
rgb_final = torch.sum(weights.unsqueeze(-1)*rgbs, -2) # (N_rays, 3)
depth_final = torch.sum(weights*z_vals, -1) # (N_rays)

第二轮渲染

对于渲染的结果,会根据 对应的权重,使用 pdf 抽样,得到新的渲染点。例如默认第一轮粗渲染每束光线是 64 个样本点,第二轮再增加 128 个抽样点。

然后使用 finemodel 进行预测,后对所有的样本点(64+128)进行体素渲染。

def sample_pdf(bins, weights, N_importance, det=False, eps=1e-5):
    """
    Sample @N_importance samples from @bins with distribution defined by @weights.
 
    Inputs:
        bins: (N_rays, N_samples_+1) where N_samples_ is "the number of coarse samples per ray - 2"
        weights: (N_rays, N_samples_)
        N_importance: the number of samples to draw from the distribution
        det: deterministic or not
        eps: a small number to prevent division by zero
 
    Outputs:
        samples: the sampled samples
    """
    N_rays, N_samples_ = weights.shape
    weights = weights + eps # prevent division by zero (don't do inplace op!)
    pdf = weights / torch.sum(weights, -1, keepdim=True) # (N_rays, N_samples_)
    cdf = torch.cumsum(pdf, -1) # (N_rays, N_samples), cumulative distribution function
    cdf = torch.cat([torch.zeros_like(cdf[: ,:1]), cdf], -1)  # (N_rays, N_samples_+1) 
                                                               # padded to 0~1 inclusive
 
    if det:
        u = torch.linspace(0, 1, N_importance, device=bins.device)
        u = u.expand(N_rays, N_importance)
    else:
        u = torch.rand(N_rays, N_importance, device=bins.device)
    u = u.contiguous()
 
    inds = searchsorted(cdf, u, side='right')
    below = torch.clamp_min(inds-1, 0)
    above = torch.clamp_max(inds, N_samples_)
 
    inds_sampled = torch.stack([below, above], -1).view(N_rays, 2*N_importance)
    cdf_g = torch.gather(cdf, 1, inds_sampled).view(N_rays, N_importance, 2)
    bins_g = torch.gather(bins, 1, inds_sampled).view(N_rays, N_importance, 2)
 
    denom = cdf_g[...,1]-cdf_g[...,0]
    denom[denom<eps] = 1 # denom equals 0 means a bin has weight 0, in which case it will not be sampled
                         # anyway, therefore any value for it is fine (set to 1 here)
 
    samples = bins_g[...,0] + (u-cdf_g[...,0])/denom * (bins_g[...,1]-bins_g[...,0])
    return samples

Loss

这里直接使用的 MSE loss,对输出的像素值和 ground truth 计算 L2-norm loss.

训练数据

训练数据

quicker_a23cfc59-ae27-4cb6-83fa-5149a4b91f19.png

根据前面的介绍,NeRF 实现的,是从 【位置坐标 katex 和 拍摄角度(θ,ϕ)】 到 【体密度 (σ) 和 RGB 色彩值 (C)】的映射。根据体素渲染理论,图片中的每一个像素,实质上都是从相机发射出的一条光线渲染得到的。 因此,我们首先,需要得到每一个像素对应的光线(ray), 然后,计算光线上每一个点的【体密度σ) 和 RGB 色彩值 (C)】,最后再渲染得到对应的像素值。

对于训练数据,我们需要拍摄一系列的图片(如 100 张)图片和他们的拍摄相机角度、内参、场景边界(可以使用 COLMAP 获得)。我们需要准备每一个像素对应的光线(ray)信息,这样可以组成成对的训练数据【光线信息 <==> 像素值】。

下面以 LLFFDataset (”datasets/llff.py”) 为例,进行分析:

读取的数据(以一张图片为例)

  • 图片:尺寸是 N_img​×C×H×W。 其中 C=3 代表了这是 RGB 三通道图片
  • 拍摄角度信息(从 COLMAP 生成):Nimg​×17。前 15 维可以变形为 3×5,代表了相机的 pose,后 2 维是最近和最远的深度。解释: 3×5 pose matrices and 2 depth bounds for each image. Each pose has [R T] as the left 3×4 matrix and [H W F] as the right 3×1 matrix. R matrix is in the form [down right back] instead of [right up back] . (https://github.com/bmild/nerf/issues/34

拍摄角度预处理

第一步:根据拍摄的尺寸和处理尺寸的关系,缩放相机的焦距。例如:Himg​=3024,Wimg​=4032,Fimg​=3260, 如果我们想处理的尺寸是H=378,W=504 (为了提升训练的速度),我们需要缩放焦距 F:

# "datasets/llff.py", line:188
    # Step 1: rescale focal length according to training resolution
    H, W, self.focal = poses[0, :, -1] # original intrinsics, same for all images
    assert H*self.img_wh[0] == W*self.img_wh[1], \
        f'You must set @img_wh to have the same aspect ratio as ({W}, {H}) !'
 
    self.focal *= self.img_wh[0]/W

第二步:调整 pose 的方向。在 “poses_bounds.npy” 中,pose 的方向是 “下右后”,我们调整到 “右上后”。同时使用 “center_poses(poses)” 函数,对整个 dataset 的坐标轴进行标准化(??)。
解释:“poses_avg computes a “central” pose for the dataset, based on using the mean translation, the mean z axis, and adopting the mean y axis as an “up” direction (so that Up x Z = X and then Z x X = Y). recenter_poses very simply applies the inverse of this average pose to the dataset (a rigid rotation/translation) so that the identity extrinsic matrix is looking at the scene, which is nice because normalizes the orientation of the scene for later rendering from the learned NeRF. This is also important for using NDC (Normalized device coordinates) coordinates, since we assume the scene is centered there too.”(https://github.com/bmild/nerf/issues/34

# "datasets/llff.py", line:195
    # Step 2: correct poses
    # Original poses has rotation in form "down right back", change to "right up back"
    # See https://github.com/bmild/nerf/issues/34
    poses = np.concatenate([poses[..., 1:2], -poses[..., :1], poses[..., 2:4]], -1)
            # (N_images, 3, 4) exclude H, W, focal
    self.poses, self.pose_avg = center_poses(poses)

第三步:令最近的距离约为 1。 解释:“The NDC code takes in a “near” bound and assumes the far bound is infinity (this doesn’t matter too much since NDC space samples in 1/depth so moving from “far” to infinity is only slightly less sample-efficient). You can see here that the “near” bound is hardcoded to 1”。For more details on how to use NDC space see https://github.com/bmild/nerf/files/4451808/ndc_derivation.pdf

# "datasets/llff.py", line:205    # Step 3: correct scale so that the nearest depth is at a little more than 1.0    # See https://github.com/bmild/nerf/issues/34    near_original = self.bounds.min()    scale_factor = near_original*0.75 # 0.75 is the default parameter                                        # the nearest depth is at 1/0.75=1.33    self.bounds /= scale_factor    self.poses[..., 3] /= scale_factor

计算光线角度


接下来就是对每一个像素,使用 “get_ray_directions()” 函数计算所对应的光线。这里只需要使用图像的长宽和焦距即可计算

self.directions = get_ray_directions(self.img_wh[1], self.img_wh[0], self.focal) # (H, W, 3)

调用函数:

def get_ray_directions(H, W, focal):
    """
    Get ray directions for all pixels in camera coordinate.
    Reference: https://www.scratchapixel.com/lessons/3d-basic-rendering/
               ray-tracing-generating-camera-rays/standard-coordinate-systems
 
    Inputs:
        H, W, focal: image height, width and focal length
 
    Outputs:
        directions: (H, W, 3), the direction of the rays in camera coordinate
    """
    grid = create_meshgrid(H, W, normalized_coordinates=False)[0]
    i, j = grid.unbind(-1)
    # the direction here is without +0.5 pixel centering as calibration is not so accurate
    # see https://github.com/bmild/nerf/issues/24
    directions = \
        torch.stack([(i-W/2)/focal, -(j-H/2)/focal, -torch.ones_like(i)], -1) # (H, W, 3)
 
    return directions

世界坐标系下的光线
在拿到每一个像素对应的光线角度后,我们需要得到具体的光线信息。首先,先计算在世界坐标系下的光线信息。主要是一个归一化的操作。

Get ray origin and normalized directions in world coordinate for all pixels in one image. Reference: https://www.scratchapixel.com/lessons/3d-basic-rendering/ray-tracing-generating-camera-rays/standard-coordinate-systems

输入:

图像上每一点所对应的光线角度:(H, W, 3) precomputed ray directions in camera coordinate。
相机映射矩阵 c2w:(3, 4) transformation matrix from camera coordinate to world coordinate
输出:

光线原点在世界坐标系中的坐标:(HW, 3), the origin of the rays in world coordinate 在世界坐标系中,归一化的光线角度:(HW, 3), the normalized direction of the rays in world


def get_rays(directions, c2w):
    """
    Get ray origin and normalized directions in world coordinate for all pixels in one image.
    Reference: https://www.scratchapixel.com/lessons/3d-basic-rendering/
               ray-tracing-generating-camera-rays/standard-coordinate-systems
 
    Inputs:
        directions: (H, W, 3) precomputed ray directions in camera coordinate
        c2w: (3, 4) transformation matrix from camera coordinate to world coordinate
 
    Outputs:
        rays_o: (H*W, 3), the origin of the rays in world coordinate
        rays_d: (H*W, 3), the normalized direction of the rays in world coordinate
    """
    # Rotate ray directions from camera coordinate to the world coordinate
    rays_d = directions @ c2w[:, :3].T # (H, W, 3)
    rays_d = rays_d / torch.norm(rays_d, dim=-1, keepdim=True)
    # The origin of all rays is the camera origin in world coordinate
    rays_o = c2w[:, 3].expand(rays_d.shape) # (H, W, 3)
 
    rays_d = rays_d.view(-1, 3)
    rays_o = rays_o.view(-1, 3)
 
    return rays_o, rays_d

NDC 下的光线

NDC (Normalized device coordinates) 归一化的设备坐标系。

首先对光线的边界进行限定:

near, far = 0, 1

然后对坐标进行平移和映射。

def get_ndc_rays(H, W, focal, near, rays_o, rays_d):
    """
    Transform rays from world coordinate to NDC.
    NDC: Space such that the canvas is a cube with sides [-1, 1] in each axis.
    For detailed derivation, please see:
    http://www.songho.ca/opengl/gl_projectionmatrix.html
    https://github.com/bmild/nerf/files/4451808/ndc_derivation.pdf
 
    In practice, use NDC "if and only if" the scene is unbounded (has a large depth).
    See https://github.com/bmild/nerf/issues/18
 
    Inputs:
        H, W, focal: image height, width and focal length
        near: (N_rays) or float, the depths of the near plane
        rays_o: (N_rays, 3), the origin of the rays in world coordinate
        rays_d: (N_rays, 3), the direction of the rays in world coordinate
 
    Outputs:
        rays_o: (N_rays, 3), the origin of the rays in NDC
        rays_d: (N_rays, 3), the direction of the rays in NDC
    """
    # Shift ray origins to near plane
    t = -(near + rays_o[...,2]) / rays_d[...,2]
    rays_o = rays_o + t[...,None] * rays_d
 
    # Store some intermediate homogeneous results
    ox_oz = rays_o[...,0] / rays_o[...,2]
    oy_oz = rays_o[...,1] / rays_o[...,2]
 
    # Projection
    o0 = -1./(W/(2.*focal)) * ox_oz
    o1 = -1./(H/(2.*focal)) * oy_oz
    o2 = 1. + 2. * near / rays_o[...,2]
 
    d0 = -1./(W/(2.*focal)) * (rays_d[...,0]/rays_d[...,2] - ox_oz)
    d1 = -1./(H/(2.*focal)) * (rays_d[...,1]/rays_d[...,2] - oy_oz)
    d2 = 1 - o2
 
    rays_o = torch.stack([o0, o1, o2], -1) # (B, 3)
    rays_d = torch.stack([d0, d1, d2], -1) # (B, 3)
 
    return rays_o, rays_d

训练数据的生成

输出分为两部分:光线的信息,和对应的图片像素值

  • 对于每一束光线,按照 【光线原点 (3d), 光线角度 (3d), 最近的边界 (1d), 最远的边界 (1d)】= 8d 的格式存储。
  • 光线对应的像素,RGB=3d 的格式存储。
self.all_rays += [torch.cat([rays_o, rays_d,                                              near*torch.ones_like(rays_o[:, :1]),                                             far*torch.ones_like(rays_o[:, :1])],                                             1)] # (h*w, 8)