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)