CLIP: Contrastive Language-Image Pre-Training

CLIP论文讲解

背景

论文来自 Open AI 2021 年提出的一个成果,相关可参考信息: github、 paper主页 。 之前其实并不太了解多模态预训练领域的成果,最近看到了这篇质量很高的成果。

Hugging Facehttps://huggingface.co/openai/clip-vit-base-patch32(预训练模型库)

效果

我们可以运行这colab,该作者将 Unsplash 的所有素材计算了 clip image embedding ,然后使用 clip word embedding 进行配图。

preview

效果看起来似乎不错,几乎实现了通过一句话就找到合适的图片。不可否定,会存在大量的badcase。但是在不需要fine-tune/下游任务,直接zero-shot得到的embedding可以实现这样的效果已经很厉害了。

作者团队来自 OPEN AI

CLIP工作:

1 方法简单,效果好

2 迁移学习能力强(已训练好的模型,可以在任意数据集上取得好效果)

Q1 CLIP 是什么?how 做zero-shot(一种分类方式)?

Q2 CLIP How 预训练?

利用信号(来自自然语言处理)训练一个模型(迁移效果好)

Q3 经过预训练能得到什么?

A3 仅得到图片或文本的特征,没有在分类任务上继续做训练或微调。即CLIP没有分类头。

Q4 没有分类头,how 做推理?

A4 利用自然语言的方法—prompt template;

将1000个类,生成一个1000个句子(object),

例子:plane 变成 object

1000个句子通过文本编码器(text-Encode)生成1000个特征。

Q5 直接从1000个类里面抽取特征也可以, why 还要进行Prompt- template ?

A5 在预训练时,model 看到的是sample-pair,若在推理时,把所有文本变成一个单词(word),导致model看到的东西和预训练时不一样,导致识别效果稍下降。

Q6 如何将1000分类变成 1000个句子(object)?why 这样做?

A6 2个方法:prompt engineering 和 prompot ensambol;

提高模型准确率,且不需要重新训练模型

Q7 prompt template 操作之后要干嘛?

A7 input 图片,经过image_Encode 得到图片特征,利用image_feature 和 text-feature 计算相似性,挑出值(最相似),进而完成分类任务。

Q8 how 理解分类任务?

A8 judge image 中有哪些物体

Q9 text and image 可以改吗?

A9 yes (all of anything)

Q10 若用imageNet做训练,input三轮车(image),why 得到车,而不是,三轮车?

A10 因为,imageNet 无法实时更新已有类别。

但CLIP可以实时更新,故 imput = output。

这也是CLIP的强大之处,彻底摆脱了categoricel label 限制。

为了提高model泛化性,作者提出新办法,从 text 中提取监督信号。(正是有了监督信号(覆盖范围广)的存在,model 的泛化能力得到提高)作者利用4亿 text-image-pair-dataset ,选择自监督训练方式,进而训练模型。

CLIP 利用多模态对比学习完成训练,并可以做物体分类(即prompt),这种分类不限于已有类别,可扩展到新类别。(即当前学到的model,可以直接在 downstream tasks(下游任务)上做推理。

2017年有人研究,但是影响力小,效果差:

主要有3个工作(均基于transformer)和CLIP像,但有区别:VIrTex:用自回归预测方式,做model预训练,ICMLM :用完型填空方式,做model预训练,ConVIRT:和CLIP类似,但仅在医疗图像上做实验。但由于data 和 model规模小,所以效果不好。

利用自然语言的监督信号,来训练好的视觉模型。在自监督学习(完型填空)的范式下,NLP可以利用(取不尽)的文本监督信号。用此方法训练出的模型,简单,泛化力强,为多模态训练铺路。

why 用自然语言监督信号训练视觉模型?

1 无需标注这些数据(数据规模变大)

2 此时监督信号是文本(不是n选1 的标签),意味着input,output 自由度大了很多.

3 因为image-text-pair数据,model所学特征不单是视觉特征,而是多模态特征。当image和语言联系在一起,便容易做zero-shot迁移学习。

若仅做单模态自监督学习,无论是单模态对比学习(MOCO),还是单模态掩码学习(MAE),model仅学到视觉特征,无法和自然语言联系在一起,依旧很难做zero-shot的迁移。需要大量的image-text-pair(4亿个Image-text-pair)

总结:用 文本监督信号来训练视觉model 这种做法很有潜力。

整个训练过程:

给定一张image,来预测文本,会产生较大歧义(即可能性太多);若逐字句预测文本,太难了。会导致模型训练慢因此采用对比学习,让 model 判断,image 和 text 是否配对。把 ” 训练任务 “ 换成 ” 对比任务 “ ,训练效率提高4倍

2个输入:image 和 text归一化,投射层:将单模态变成多模态,获得 n 个图像的特征,n 个文本的特征。计算 image-feature 和 text-feature相似度。利用相似度做分类。利用交叉熵目标函数计算loss

细节:

1 由于收集数据大,model 不存在 overfitting

简化了工作:当训练CLIP – model时,对应的 image-Encode 和 text-Encode 无需进行预训练

2 在多模态训练中,投射时,用线性投射层,

非线性投射层(作者推测,适配纯 Image 单模态学习),带来10个点的性能提升

3 使用 ” 随即裁减 “ 进行数据增强

4 数据集和 model 太大,不好调参

5 temperature parm(极重要超参数)稍调,model 性能会提高很多 但作者,将其设置成,可学习的标量

模型选择和参数设置:

视觉方面:

训练8个model;

ResNet = 5个,VIT = 3个

残差网络变体ResNet50*4:*16:*64:用 efficientNet-style 方法 将input-image 大小,channel宽度,model-depth 做微调

针对 transformer-model,作者选择数据集 VIT-B/32:/16:/64(阿拉伯数字表示patch大小)

文本方面:transformer

all-model 训练了epoch = 32;Adam optimizer优化器;手动调整超参数,用ResNet-50作为超参搜索,为了快速调参(训练epoch = 1)训练时:选用 batch-size = 32768(很大)(此model在很多机器上做分布式训练)

CLIP 文章的核心 = Zero-shot Transfer

作者研究迁移学习的动机:之前自监督or无监督的方法,主要研究 frature 学习的能力,model的目标是学习泛化性能好的特征,虽然学习到good-feature,但down-work中,还是需要有标签数据做微调。作者想仅训练一个model,在down-work中不再微调。

衡量model 学到的feature 好不好的方法有主要有2种:第一种:linear:冻结训练好的model,再训练一个分类头。第二种:微调:把整个网络放开,做end-to-end的学习。微调的优点: 灵活、当down-work数据集大,微调效果好。但这里作者使用只训练liner分类头: CLIP本就用来研究更数据集无关的训练方式,若用 “ 微调 “ 方法,无法判断预训练model效果如何。(因为,如果预训练model效果不好,经过在down-work上做微调,会导致最终结果好。)

CLIP这么强大,它有什么缺点?

平均来看,CLIIP可以和机械模型(ResNet-50(在ImageNet上训练))持平

若继续增加数据集和model规模,CLIP性能可以继续提高,但是代价很大(需提高计算和数据的高效性)

zreo-shot结果并不好

1 在细分类数据集上,CLIP效果低于(有监督训练)ResNet-50(极限网络)

2 CLIP无法处理抽象概念原因:CLUP无法区分 what is 异常?what is 安全?例如:数一数图片中的物体个数;在视频中,区分这一帧是异常还是非异常;作者提出:在很多领域,CLIP性能和瞎猜差不多

3 若数据集中的data 已经 out-of-distribution,那么CLIP-model泛化照样差;例子:在MNIST数据集上,CLIP准确率仅有88% 。推测原因:作者收集的数据集有4亿个样本,但没有和MINIS长得像的,所以MINIS数据集对于CLIP来说就是out-of-distribution数据集

评价

创新度高1 打破固定类别标签做法2 放飞视觉model训练过程3 引发后续大量工作

有效性高1 大数据集,效果好2 泛化性能好3 zero-shot性能超过人类

运用 BERT 的 MLM 模型进行小样本学习

转载自《必须要GPT3吗?不,BERT的MLM模型也能小样本学习》《P-tuning:自动构建模版,释放语言模型潜能》,作者:苏剑林,部分内容有修改。

大家都知道现在 GPT3 风头正盛,然而,到处都是 GPT3、GPT3 地推,读者是否记得 GPT3 论文的名字呢?事实上,GPT3 的论文叫做《Language Models are Few-Shot Learners》,标题里边已经没有 G、P、T 几个单词了,只不过它跟开始的 GPT 是一脉相承的,因此还是以 GPT 称呼它。顾名思义,GPT3 主打的是 Few-Shot Learning,也就是小样本学习。此外,GPT3 的另一个特点就是大,最大的版本多达 1750 亿参数,是 BERT Base的一千多倍。

正因如此,前些天 Arxiv 上的一篇论文《It’s Not Just Size That Matters: Small Language Models Are Also Few-Shot Learners》便引起了笔者的注意,意译过来就是“谁说一定要大的?小模型也可以做小样本学习”。显然,这标题对标的就是 GPT3,于是笔者饶有兴趣地点进去看看是谁这么有勇气挑战 GPT3,又是怎样的小模型能挑战 GPT3?经过阅读,原来作者提出通过适当的构造,用 BERT 的 MLM 模型也可以做小样本学习,看完之后颇有一种“原来还可以这样做”的恍然大悟感~在此与大家分享一下。

冉冉升起的 MLM

MLM,全称“Masked Language Model”,可以翻译为“掩码语言模型”,实际上就是一个完形填空任务,随机 Mask 掉文本中的某些字词,然后要模型去预测被 Mask 的字词,示意图如下:

BERT 的 MLM 模型简单示意图

其中被 Mask 掉的部分,可以是直接随机选择的 Token,也可以是随机选择连续的能组成一整个词的 Token,后者称为 Whole Word Masking (WWM)。

开始,MLM 仅被视为 BERT 的一个预训练任务,训练完了就可以扔掉的那种,因此有一些开源的模型干脆没保留 MLM 部分的权重,比如 brightmart版 和 clue版 的 RoBERTa,而哈工大开源的 RoBERTa-wwm-ext-large 则不知道出于什么原因随机初始化了 MLM 部分的权重,因此如果要复现本文后面的结果,这些版本是不可取的。

然而,随着研究的深入,研究人员发现不止 BERT 的 Encoder 很有用,预训练用的 MLM 本身也很有用。比如论文《BERT has a Mouth, and It Must Speak: BERT as a Markov Random Field Language Model》指出 MLM 可以作为一般的生成模型用,论文《Spelling Error Correction with Soft-Masked BERT》则将 MLM 用于文本纠错,笔者之前在《从语言模型到Seq2Seq:Transformer如戏,全靠Mask》的实验也表明 MLM 的预训练权重也可以当作 UniLM 来用做 Seq2Seq 任务,还有《无监督分词和句法分析!原来BERT还可以这样用》一文将 MLM 的思想用于无监督分词和句法分析了。可以说 MLM 已经是大放异彩了。

将任务转成完形填空

在本文里,我们再学习 MLM 的一个精彩应用:用于小样本学习或半监督学习,某些场景下甚至能做到零样本学习。

怎么将我们要做的任务跟 MLM 结合起来呢?很简单,给任务一个文本描述,然后转换为完形填空问题即可。举个例子,假如给定句子“这趟北京之旅我感觉很不错。”,那么我们补充个描述,构建如下的完形填空:______满意。这趟北京之旅我感觉很不错。

进一步地,我们限制空位处只能填一个“很”或“不”,问题就很清晰了,就是要我们根据上下文一致性判断是否满意,如果“很”的概率大于“不”的概率,说明是正面情感倾向,否则就是负面的,这样我们就将情感分类问题转换为一个完形填空问题了,它可以用 MLM 模型给出预测结果,而 MLM 模型的训练可以不需要监督数据,因此理论上这能够实现零样本学习了。

多分类问题也可以做类似转换,比如新闻主题分类,输入句子为“八个月了,终于又能在赛场上看到女排姑娘们了。”,那么就可以构建下面报导一则______新闻。八个月了,终于又能在赛场上看到女排姑娘们了。

这样我们就将新闻主题分类也转换为完形填空问题了,一个好的 MLM 模型应当能预测出“体育”二字来。

还有一些简单的推理任务也可以做这样的转换,常见的是给定两个句子,判断这两个句子是否相容,比如“我去了北京”跟“我去了上海”就是矛盾的,“我去了北京”跟“我在天安门广场”是相容的,常见的做法就是将两个句子拼接起来输入到模型做,作为一个二分类任务。如果要转换为完形填空,那该怎么构造呢?一种比较自然的构建方式是:我去了北京?______,我去了上海。
我去了北京?______,我在天安门广场。

其中空位之处的候选词为 是的,不是是的,不是。

Pattern-Exploiting

读到这里,读者应该不难发现其中的规律了,就是给输入的文本增加一个前缀或者后缀描述,并且 Mask 掉某些 Token,转换为完形填空问题,这样的转换在原论文中称为 Pattern,这个转换要尽可能与原来的句子组成一句自然的话,不能过于生硬,因为预训练的 MLM 模型就是在自然语言上进行的。显然同一个问题可以有很多不同的 Pattern,比如情感分类的例子,描述可以放最后,变成“这趟北京之旅我感觉很不错。__满意。”;也可以多加几个字,比如“觉得如何?__满意。这趟北京之旅我感觉很不错。”。

然后,我们需要构建预测 Token 的候选空间,并且建立 Token 到实际类别的映射,这在原论文中称为 Verbalizer,比如情感分类的例子,我们的候选空间是 很,不很,不,映射关系是 很→正面,不→负面很→正面,不→负面,候选空间与实际类别之间不一定是一一映射,比如我们还可以加入“挺”、“太”、“难”字,并且认为 很,挺,太→正面很,挺,太→正面 以及 不,难→负面不,难→负面,等等。不难理解,不少 NLP 任务都有可能进行这种转换,但显然这种转换一般只适用于候选空间有限的任务,说白了就是只用来做选择题,常见任务的就是文本分类。

刚才说了,同一个任务可以有多种不同的Pattern,原论文是这样处理的:

1、对于每种 Pattern,单独用训练集 Finetune 一个 MLM 模型出来;
2、然后将不同 Pattern 对应的模型进行集成,得到融合模型;
3、用融合模型预测未标注数据的伪标签;
4、用伪标签数据 Finetune 一个常规的(非 MLM 的)模型。

具体的集成方式大家自己看论文就行,这不是重点。这种训练模式被称为 Pattern-Exploiting Training (PET),它首先出现在论文《Exploiting Cloze Questions for Few Shot Text Classification and Natural Language Inference》,本文要介绍的这篇论文则进一步肯定和完善了 Pattern-Exploiting Training 的价值和结果,并整合了多任务学习,使得它在 SuperGLUE 榜单上的小样本学习效果超过了 GPT3。两篇论文的作者是相同的,是一脉相承的作品。

PET 在 SuperGLUE 上的小样本学习的结果

不过要吐槽一个点是,上图中 PET 的 223M 参数,所用的模型是 ALBERT-xxlarge-v2,事实上称 ALBERT 为“小模型”是一种很耍流氓的行为,因为它前向计算的速度并没有得到任何提升。ALBERT-xxlarge 共有 12 层,层与层之间参数是共享的,就前向计算而言,它应该等价于约 2700M(12 倍)参数的 GPT 才对。

PET 中文实践,检验效果

要真正确认一个方法或模型的价值,看论文的实验表格是不够的,论文给出的实验结果谁都不好说能否复现,其次就算英文上能复现也不代表中文上有s价值,因此最实际的还是亲自动手做实验验证。下面是笔者的实验代码,供读者参考:Github地址:https://github.com/bojone/Pattern-Exploiting-Training

我们将从以下几个角度来探讨 PET 的可行性:

1、直接利用现成的 MLM 模型效果如何?(零样本学习1)
2、用“大量无标签数据”微调现成的 MLM 模型效果如何?(零样本学习2)
3、用“小量标签数据”微调现成的 MLM 模型效果如何?(小样本学习)
4、用“小量标签数据+大量无标签数据”微调现成的 MLM 模型效果如何?(半监督学习)

下面主要给出情感二分类的实验结果。另外还有一个新闻主题的多分类,代码也放到 Github 了,其结果是类似的,就不重复陈述了。

零样本学习1

这里主要探索的是给输入文本补上对应的 Pattern 后,直接基于现成的 MLM 模型进行预测,预测的准确率。由于构建模型的整个过程都不涉及到标签数据监督训练,因此这算是一种“零样本学习”。我们需要比较的是不同 Pattern、不同 MLM 模型上的效果:

下面是实验的几个 Pattern,其中空位处候选词语都为“很”和“不”:

P1:____满意。这趟北京之旅我感觉很不错。
P2:这趟北京之旅我感觉很不错。____满意。
P3:____好。这趟北京之旅我感觉很不错。
P4:____理想。这趟北京之旅我感觉很不错。
P5:感觉如何?____满意。这趟北京之旅我感觉很不错。

至于 MLM 模型,则是下面几个:

M1:Google 开源的中文版 BERT Base(链接);

M2:哈工大开源的 RoBERTa-wwm-ext Base(链接):

M3:腾讯 UER 开源的 BERT Base(链接);

M4:腾讯 UER 开源的BERT Large(链接)。

实验结果如下表(验证集/测试集):不同模型不同Pattern的零样本学习效果

最好的效果居然可以达到 88%!也就是说,加载现成的 MLM,配合适当的 Pattern,不需要任何标注数据,就可以正确识别大部分样本情感倾向了。这不得不让我们对 MLM 模型的潜力刮目相看了。

可以观察到,不同的 Pattern、不同的预训练模型之间还是有一定的差异的,整体而言 Large 版本的效果要明显好于 Base 版本的模型,说明像 GPT 到 GPT2 再到 GPT3 一样,还是把模型做得更大会更好。此外,这还有可能说明实际上 MLM 还没有被充分训练好,或许是因为 BERT 这种 Mask 掉一部分的训练方式过于低效了,可能用《修改Transformer结构,设计一个更快更好的MLM模型》一文提到的改进版 MLM 会更好。

零样本学习2

看完上述结果,读者可能会想到:如果我用领域内的数据继续预训练 MLM 模型,那么能不能提升效果呢?答案是:能!下面是我们的实验结果,算力有限,我们只在 RoBERTa-wwm-ext(上述的 M2,继续预训练后的模型我们称为 M2+无监督M2+无监督)的基础上做了比较:

要注意的是,这里我们只是用领域内的数据继续做 MLM 训练,这个过程是无监督的,也不需要标注信号,因此也算是“零样本学习”。同时,从到目前为止的结果我们可以看出,给输入本文加入“前缀”的效果比“后缀”更有优势一些。

小样本学习

刚才我们讨论了无标签数据继续预训练 MLM 的提升,如果回到 PET 的目标场景,直接用小量的标签数据配合特定的 Pattern 训练 MLM 又如何呢?这也就是真正的“小样本学习”训练了,这里我们保留约 200 个标注样本,构造样本的时候,我们先给每个句子补上 Pattern,除了 Pattern 自带的 Mask 位置之外,我们还随机 Mask 其他一部分,以增强对模型的正则。最终实验结果如下:

结论就是除了“后缀式”的 P2 之外,其它结果都差不多,这进一步说明了“前缀式”的 Pattern 会比“后缀式”更有竞争力一些。在效果上,直接用同样的数据用常规的方法去微调一个 BERT 模型,大概的结果是 88.93 左右,所以基于“MLM+Pattern”的小样本学习方法可能带来轻微的性能提升。

半监督学习

无监督的零样本学习和有监督的小样本学习都说完了,自然就轮到把标注数据和非标注数据都结合起来的“半监督学习”了。还是同样的任务,标注数据和非标注数据的比例大约是 1:99,标注数据带 Pattern,非标注数据不带 Pattern,大家都 Mask 掉一部分 Token 进行 MLM 预训练,最终测出来的效果如下:

还是同样的,“后缀”明显比“前缀”差,“前缀”的效果差不多。具体效果上,则是肯定了额外的无标注数据也是有作用的。直觉上来看,“前缀”比“后缀”要好,大体上是因为“前缀”的 Mask 位置比较固定,微弱的监督信号得以叠加增强?但这也不能解释为什么零样本学习的情况下也是“前缀”更好,估计还跟模型的学习难度有关系,可能句子前面部分的规律更加明显,相对来说更加容易学一些,所以前面部分就学习得更加充分?这一切都还只是猜测。

汇总与结论

将上述结果汇总如下:结果汇总比较

读者还可以对比我们之前在文章《泛化性乱弹:从随机噪声、梯度惩罚到虚拟对抗训练》中用虚拟对抗训练 (VAT) 做半监督学习的结果,可以看到不管是零样本学习、小样本学习还是半监督学习,基于 MLM 模型的方式都能媲美基于 VAT 的半监督学习的结果。我们在做短新闻多分类实验时的结果也是相似的。因此,这说明了 MLM 模型确实也可以作为一个优秀的零样本/小样本/半监督学习器来使用。

当然,基于 MLM 模型的缺点还是有的,比如 MLM 所使用的独立假设限制了它对更长文本的预测能力(说白了空位处的文字不能太长),以及无法预测不定长的答案也约束了它的场景(所以当前只能用于做选择题,不能做生成)。我们期待有更强的 MLM 模型出现,那时候就有可能在所有任务上都能与 GPT3 一较高下了。

什么是模版

前面介绍的 Pattern-Exploiting Training (PET) 方法,其主要的思想是借助由自然语言构成的模版(英文常称 Pattern 或 Prompt),将下游任务也转化为一个完形填空任务,这样就可以用 BERT 的 MLM 模型来进行预测了。比如下图中通过条件前缀来实现情感分类和主题分类的例子:

通过特定模版将情感分类转换为 MLM 任务

通过特定模版将新闻分类转换为 MLM 任务

当然,这种方案也不是只有 MLM 模型可行,用 GPT 这样的单向语言模型(LM)其实也很简单:

通过特定模版将情感分类转换为 LM 任务

通过特定模版将新闻分类转换为 LM 任务

不过由于语言模型是从左往右解码的,因此预测部分只能放在句末了(但还可以往补充前缀说明,只不过预测部分放在最后)。

某种意义上来说,这些模版属于语言模型的“探针”,我们可以通过模版来抽取语言模型的特定知识,从而做到不错的零样本效果,而配合少量标注样本,可以进一步提升效果。

然而,对于某些任务而言,人工构建模版并不是那么容易的事情,模型的优劣我们也不好把握,而不同模型之间的效果差别可能很大,在这种情况下,人工标注一些样本可能比构建模版还要轻松得多。所以,如何根据已有的标注样本来自动构建模版,便成了一个值得研究的问题了。

P-tuning

最近 Arxiv 上的论文《GPT Understands, Too》提出了名为 P-tuning 的方法,成功地实现了模版的自动构建。不仅如此,借助 P-tuning,GPT 在 SuperGLUE 上的成绩首次超过了同等级别的 BERT 模型,这颠覆了一直以来“GPT 不擅长 NLU”的结论,也是该论文命名的缘由。

P-tuning 重新审视了关于模版的定义,放弃了“模版由自然语言构成”这一常规要求,从而将模版的构建转化为连续参数优化问题,虽然简单,但却有效。

模版的反思

首先,我们来想一下“什么是模版”。直观来看,模版就是由自然语言构成的前缀/后缀,通过这些模版我们使得下游任务跟预训练任务一致,这样才能更加充分地利用原始预训练模型,起到更好的零样本、小样本学习效果。

等等,我们真的在乎模版是不是“自然语言”构成的吗?

并不是。本质上来说,我们并不关心模版长什么样,我们只需要知道模版由哪些 token 组成,该插入到哪里,插入后能不能完成我们的下游任务,输出的候选空间是什么。模版是不是自然语言组成的,对我们根本没影响,“自然语言”的要求,只是为了更好地实现“一致性”,但不是必须的。于是,P-tuning 考虑了如下形式的模版:

P-tuning 直接使用 [unused*] 的 token 来构建模版,不关心模版的自然语言性

这里的 [u1]~[u6],代表 BERT 词表里边的 [unused1]~[unused6],也就是用几个从未见过的 token 来构成模板,这里的 token 数目是一个超参数,放在前面还是后面也可以调整。接着,为了让“模版”发挥作用,我们用标注数据来求出这个模板。

如何去优化

这时候,根据标注数据量的多少,我们又分两种情况讨论。

第一种,标注数据比较少。这种情况下,我们固定整个模型的权重,只优化 [unused1]~[unused6] 这几个 token 的 Embedding,换句话说,其实我们就是要学 6 个新的 Embedding,使得它起到了模版的作用。这样一来,因为模型权重几乎都被固定住了,训练起来很快,而且因为要学习的参数很少,因此哪怕标注样本很少,也能把模版学出来,不容易过拟合。

第二种,标注数据很充足。这时候如果还按照第一种的方案来,就会出现欠拟合的情况,因为只有 6 个 token 的可优化参数实在是太少了。因此,我们可以放开所有权重微调,原论文在 SuperGLUE 上的实验就是这样做的。读者可能会想:这样跟直接加个全连接微调有什么区别?原论文的结果是这样做效果更好,可能还是因为跟预训练任务更一致了吧。

P-tuning 在 SuperGLUE 上的表现

此外,在上面的例子中,目标 token 如“很”、“体育”是认为选定的,那么它们可不可以也用 [unused*] 的 token 代替呢?答案是可以,但也分两种情况考虑:1、在标注数据比较少的时候,人工来选定适当的目标 token 效果往往更好些;2、在标注数据很充足的情况下,目标 token 用 [unused*] 效果更好些,因为这时候模型的优化空间更大一些。

增强相关性

在原论文中,P-tuning 并不是随机初始化几个新 token 然后直接训练的,而是通过一个小型的 LSTM 模型把这几个 Embedding 算出来,并且将这个 LSTM 模型设为可学习的。这样多绕了一步有什么好处呢?原论文大概的意思是:LSTM 出现的 token 表示相关性更强,某种程度上来说更像“自然语言”(因为自然语言的 token 之间不是独立的),此外还能防止局部最优。我在 Github 上进一步向作者确认了一下(参考这里),效果上的差别是通过 LSTM 多绕一步的方法可以使得模型收敛更快、效果更优。

然而,这样多了一个LSTM,总感觉有些别扭,而且实现上也略微有点麻烦。按照作者的意思,LSTM 是为了帮助模版的几个 token(某种程度上)更贴近自然语言,但这并不一定要用 LSTM 生成,而且就算用 LSTM 生成也不一定达到这一点。笔者认为,更自然的方法是在训练下游任务的时候,不仅仅预测下游任务的目标 token(前面例子中的“很”、“新闻”),还应该同时做其他 token 的预测

比如,如果是 MLM 模型,那么也随机 mask 掉其他的一些 token 来预测;如果是 LM 模型,则预测完整的序列,而不单单是目标词。这样做的理由是:因为我们的 MLM/LM 都是经过自然语言预训练的,所以我们(迷之自信地)认为能够很好完成重构的序列必然也是接近于自然语言的,因此这样增加训练目标,也能起到让模型更贴近自然语言的效果。经过笔者的测试,加上这样辅助目标,相比单纯优化下游任务的目标,确实提升了效果。

P-tuning 实验与效果

所谓“talk is cheap, show me the code”,又到了喜闻乐见的实验时间了。这里分享一下 P-tuning 的实验结果,其中还包括笔者对 P-tuning 的实现思路,以及笔者在中文任务上的实验结果。

停止的梯度

怎么实现上述的 P-tuning 算法比较好呢?如果是放开所有权重训练,那自然是简单的,跟普通的 BERT 微调没有什么区别。关键是在小样本场景下,如何实现“只优化几个 token”呢?

当然,实现的方法也不少,比如为那几个要优化的 token 重新构建一个 Embedding 层,然后拼接到 BERT 的Embedding层中,然后训练的时候只放开新 Embedding 层的权重。但这样写对原来模型的改动还是蛮大的,最好的方法是尽可能少改动代码,让使用者几乎无感。为此,笔者构思了一种用 stop_gradient 简单修改 Embedding 层的方案,大体上是将 Embedding 层修改如下:

class PtuningEmbedding(Embedding):
    """新定义Embedding层,只优化部分Token
    """
    def call(self, inputs, mode='embedding'):
        embeddings = self.embeddings
        embeddings_sg = K.stop_gradient(embeddings)
        mask = np.zeros((K.int_shape(embeddings)[0], 1))
        mask[1:9] += 1  # 只优化id为1~8的token
        self.embeddings = embeddings * mask + embeddings_sg * (1 - mask)
        return super(PtuningEmbedding, self).call(inputs, mode)

变量经过 stop_gradient 算子后,在反向传播的时候梯度为 0,但是前向传播不变,因此在上述代码中,前向传播的结果不会有变化,但是反向传播求梯度的时候,梯度不为 0 的 token 由 mask 变量控制,其余 token 的梯度都为零,因此就实现了只更新部分 token。

完整代码可见:Github:https://github.com/bojone/P-tuning

对了,原论文也开源了代码:Github:https://github.com/THUDM/P-tuning

测试与效果

前面已经分享了原作者在 SuperGLUE 上的实验结果,显示出如果配合 P-tuning,那么:1、GPT、BERT 的效果相比直接 finetune 都有所提升;2、GPT 的效果还能超过了 BERT。这表明 GPT 不仅有 NLG 的能力,也有 NLU 能力,可谓是把 GPT 的潜能充分“压榨”出来了,当然 BERT 配合 P-tuning 也有提升,说明 P-tuning 对语言模型潜能的释放是较为通用的。

原论文的实验比较丰富,建议读者仔细阅读原论文,相信会收获颇多。特别指出的是原论文的 Table 2 最后一列,当预训练模型足够大的时候,我们的设备可能无法 finetune 整个模型,而 P-tuning 可以选择只优化几个 Token 的参数,因为优化所需要的显存和算力都会大大减少,所以 P-tuning 实则上给了我们一种在有限算力下调用大型预训练模型的思路

P-tuning 在各个体量的语言模型下的效果

当然,笔者一直以来的观点是“没有在中文上测试过的算法是没有灵魂的”,因此笔者也在中文任务上简单测试了,测试任务跟前文一致,都是情感分类的小样本学习,测试模型包括 BERT 和 GPT,两者的候选模版分别如下图:

笔者在中文情感分类上使用的“BERT+P-tuning”模版

笔者在中文情感分类上使用的“GPT+P-tuning”模版

注意,对于 LM 模型,前缀的引入非常重要,只引入后缀时效果会明显变差;而对于 MLM 模型,前缀的效果通常也优于后缀。总的效果如下表:

其中“小样本”只用到了“少量标注样本”,“无监督”则用到了“大量无标注样本”,“半监督”则用到了“少量标注样本+大量无标注样本”,“P-tuning” 都是小样本,PET 的几个任务报告的是最优的人工模版的结果,其实还有更差的人工模版。从小样本角度来看,P-tuning 确实取得了最优的小样本学习效果;从模版构建的角度来看,P-tuning 确实也比人工构建的模版要好得多;从模型角度看,P-tuning 确实可以将 GPT 的分类性能发挥到跟 BERT 相近,从而揭示了 GPT 也有很强的 NLU 能力的事实。

进一步理解 P-tuning

这一节将会介绍笔者对 P-tuning 的进一步思考,以求从多个维度来理解 P-tuning。

离散 vs 连续

在 P-tuning 之前,也已经有一些在做模版的自动构建,如《How Can We Know What Language Models Know?》《AutoPrompt: Eliciting Knowledge from Language Models with Automatically Generated Prompts》等,但它们搜索的都是在离散空间下搜索的自然语言模版,所以效果有所限制,并没有取得特别突出的结果。

相反,P-tuning 放弃了“模版由自然语言构成”这一要求,从而将其变成了可以简单梯度下降求解的连续参数问题,效果还更好。同时,这一改动意味着 P-tuning 突出了模版的本质——即模版的关键在于它是怎么用的,不在于它由什么构成——给人一种去芜存菁、眼前一亮的感觉,确实值得点赞。

(注:经读者@brotherb提醒,年初有一篇论文《Prefix-Tuning: Optimizing Continuous Prompts for Generation》提出的 Prefix-Tuning 方法其实已经相当接近 P-tuning,两者都设计了非自然语言的模版,只不过 Prefix-Tuning 主要关心 NLG 的应用而 P-tuning 更加关心 NLU 的应用。)

Adapter

我们还可以从 Adapter 的角度来理解 P-tuning。BERT 出来后不久,Google 在论文《Parameter-Efficient Transfer Learning for NLP》中提出了一种名为 Adapter 的微调方式,它并不是直接微调整个模型,而是固定住 BERT 原始权重,然后在 BERT 的基础上添加一些残差模块,只优化这些残差模块,由于残差模块的参数更少,因此微调成本更低。Adapter 的思路实际上来源于 CV 的《Learning multiple visual domains with residual adapters》,不过这两年似乎很少看到了,也许是因为它虽然提高了训练速度,但是预测速度却降低了,精度往往还有所损失。

在 P-tuning 中,如果我们不将新插入的 token 视为“模版”,是将它视为模型的一部分,那么实际上 P-tuning 也是一种类似 Adapter 的做法,同样是固定原模型的权重,然后插入一些新的可优化参数,同样是只优化这些新参数,只不过这时候新参数插入的是 Embedding 层。因此,从这个角度看,P-tuning 与 Adapter 有颇多异曲同工之处。

为什么有效

然后,还有一个值得思考的问题:为什么 P-tuning 会更好?比如全量数据下,大家都是放开所有权重,P-tuning 的方法依然比直接 finetune 要好,为啥呢?

事实上,提出这个问题的读者,应该是对 BERT 加个全连接层的直接 finetune 做法“习以为常”了。很明显,不管是 PET 还是 P-tuning,它们其实都更接近预训练任务,而加个全连接层的做法,其实还没那么接近预训练任务,所以某种程度上来说,P-tuning 有效更加“显然”,反而是加个全连接层微调为什么会有效才是值得疑问的。

去年有篇论文《A Mathematical Exploration of Why Language Models Help Solve Downstream Tasks》试图回答这个问题,大致的论证顺序是:

1、预训练模型是某种语言模型任务;
2、下游任务可以表示为该种语言模型的某个特殊情形;
3、当输出空间有限的时候,它又近似于加一个全连接层;
4、所以加一个全连接层微调是有效的。

可以看到,该论文的假设主要是第 2 点,其实就是直接假设了下游任务可以表达为类似 PET 的形式,然后才去证明的。所以这进一步说明了,PET、P-tuning 等才是更自然的使用预训练模型的方式,加全连接直接 finetune 的做法其实只是它们的推论罢了,也就是说,PET、P-tuning 才是返璞归真、回归本质的方案,所以它们更有效。

转载自《必须要GPT3吗?不,BERT的MLM模型也能小样本学习》《P-tuning:自动构建模版,释放语言模型潜能》,作者:苏剑林,部分内容有修改。

Mask Scoring RCNN

论文: https://arxiv.org/abs/1903.00241 CVPR2019

code: https://github.com/zjhuang22/maskscoring_rcnn

这篇论文从实例分割中mask 的分割质量角度出发,提出过去的经典分割框架存在的一个缺陷:用Bbox bounding box的classification confidence作为mask score,导致mask score和mask quality不配准。因此文章基于Mask R-CNN提出一个新的框架Mask Scoring R-CNN,能自动学习出mask quality,试图解决不配准的问题。

摘要

让一个深度网络意识到自己预测的质量是一个有趣但重要的问题。在实例分割任务中,大多数实例分割框架使用实例分类的置信度作为mask质量分数。然而,将mask质量量化为实例mask与其ground truth之间的IoU,通常与分类分数的相关性并不好。

在本文中,我们提出Mask Scoring R-CNN来学习预测实例mask的质量。提出的网络块将实例特征和相应的预测mask结合起来回归mask IoU。Mask评分策略校准mask质量和mask评分之间的偏差,并通过在COCO AP评估期间优先处理更准确的mask预测来改进实例分割性能。

通过对COCO数据集的广泛评估,Mask Scoring R-CNN与不同的模型带来一致和显着的增益,并优于Mask RCNN。

总而言之,这项工作的主要贡献突出如下:

1. 提出Mask Scoring R-CNN,这是第一个解决实例分割假设得分问题的框架。它探索了改善实例分割模型性能的新方向。通过考虑实例mask的完整性,如果实例mask的得分较高而mask不够好,则可以对实例mask的分数进行惩罚。

2. MaskIoU head非常简单有效。能够在各个backbone上涨点。

正文:

在实例分割(instance segmentation)中,比如Mask R-CNN,mask 分支的分割质量(quality)来源于检测分支的classification confidence。Mask R-CNN其实Faster R-CNN系列的延伸,其在Faster R-CNN的基础上添加一个新的分支用来预测object mask,该分支以检测分支的输出作为输入,mask的质量一定程度上依赖于检测分支。这种简单粗暴的做法取得了SOTA的性能,近年来COCO比赛的冠军或者前几名基本是Mask R-CNN及其变体,但依然有上升的空间。

更仔细的来讲,Mask R-CNN存在的问题是:bounding box的classification confidence不能代表mask的分割质量。classification confidence高可以表示检测框的置信度高(严格来讲不能表示框的定位精准),但也会存在mask分割的质量差的情况。高的分类置信度也应该同时有好的mask 结果。

回到原始的初衷,文章希望得到精准的mask质量,那么如何评价输出的mask质量呢?

是AP,或者说是instance-level的IoU。这个IoU和检测用到的IoU是一个东西,前者是predict mask和gt mask的pixel-level的Intersection-over-Union,而后者则是predict box和gt box的box-level的Intersection-over-Union。所以一个直观的方法就是用IoU来表示分割的质量,那么让网络自己学习输出分割的质量也是简单直观的做法。学习出mask的IoU,那么最后的mask score就等于maskIoU乘以classification score,mask score就同时表示分类置信度和分割的质量。

作者在Mask R-CNN的基础上添加了一个MaskIoU分支用于预测当前输出的mask和gt mask的IoU。MaskIoU的输入由两部分组成,一是ROIAlign得到的RoI feature map,二是mask分支输出的mask。两者concat之后经过3层卷积和2层全连接输出MaskIoU。

training过程:

box分支和mask保持不变,输出的mask先经过阈值为0.5的binarize,再计算binary mask和gt的IoU作为target,采用L2 loss作为损失函数,loss weight设为1,3个分支同时end-to-end训练。

inference过程:

检测分支输出score最高的100个框,再送入mask分支,得到mask结果,RoI feature map再和mask送入MaskIoU分支得到mask iou,与box的classification score相乘就得到最后的mask score。

实验结果,在COCO 2017 test集上,相对于Mask R-CNN,mask AP有1个点多的提升。

同时作者还做了对比实验,验证不同的MaskIoU输入对性能的影响。文章列举了4种输入方式:

  1. target mask和ROI feature concat
  2. target mask和ROI feature 相乘
  3. 所有mask和ROI feature concat
  4. target mask和高分辨率的ROI feature concat

其网络结构示意图如下:

验证不同training target对性能的影响:

  1. 只学习target类别的MaskIoU,忽略其他类别
  2. 学习所有类别的MaskIoU,相应的其他类别的MaskIoU的学习目标就是0
  3. 学习出现在ROI区域的类别的MaskIoU。

可以看出,setting#1的效果最好,setting#2的效果最差。

同时作者还做了实验探索Mask Scoring R-CNN的性能上界。

对每个输出的MaskIoU,用输出的mask 和匹配的gt mask iou来代替,MaskIoU分支就输出了mask分支的真实quality,这时就得到了Mask Scoring R-CNN的性能上界。实验结果表明Mask Scoring R-CNN依然比Mask R-CNN更好,说明MaskIoU起到了alignment的效果,但很显然会比用gt mask iou 代替的效果差,说明一方面box的准确性和mask分支本身也会影响mask任务的性能,另一方面MaskIoU 分支的学习能力可以进一步提升,Mask Scoring R-CNN依然有提升的空间。

速度方面,作者在Titan V GPU上测试一张图片,对于ResNet18-FPN用时0.132s,Resnet101-DCN-FPN用时0.202s,Mask Scoring R-CNN和Mask R-CNN速度一样。

总结:

作者motivation就是想让mask的分数更合理,从而基于mask rcnn添加一个新的分支预测来得到更准确的分数,做法简单粗暴,从结果来看也有涨点。其实mask的分割质量也跟box输出结果有很大关系,这种detection-based分割方法不可避免,除非把detection结果做的非常高,不然mask也要受制于box的结果。这种做法与IoU-Net类似,都是希望直接学习最本质的metric方式来提升性能。

为了同时提升detection和mask的效果,最近的Cascade方法很受欢迎,从人脸检测领域的Cascade CNN, 到Cascade R-CNN: Delving into High Quality Object Detection,再到友商的HTC不仅在COCO中拿了冠军,同时也被CVPR2019接收,Cascade方式展现了强大实力,相信在未来会出现越来越多的Cascade,如Cascade RetinaNet,Cascade TridentNet。。。

NeRF:类神经网路在View Synthesis的热门新方向

项目主页:https://www.matthewtancik.com/nerf

论文地址: https://arxiv.org/abs/2003.08934  
code:https://github.com/bmild/nerf https://github.com/yenchenlin/nerf-pytorch
ECCV 2020 (Oral Presentation, Best Paper Honorable Mention)

NeRF:用深度学习完成3D渲染任务的蹿红

文章难度:★★★★☆
阅读建议: 这篇文章尽量深入浅出地介绍 NeRF,这个近期 deep learning的热门新宠儿。 NeRF因为牵扯到许多 rendering与机率的设计,所以直接啃起来是挺硬的。这篇文章一开始会先简单地介绍 NeRF以及 Volume Rendering的概念,而后才会一步一步地解释 NeRF的运作细节与训练方法。最后也会简单带过几个接续 NeRF研究的优秀方法。
推荐背景知识: deep learning, multilayer perceptron (MLP), volume rendering, ray tracing, light field, view synthesis, neural rendering, quadrature, stratified sampling.

NeRF 是 2020 年 ECCV 上获得最佳论文荣誉提名的工作,其影响力是十分巨大的。NeRF 将隐式表达推上了一个新的高度,仅用 2D 的 posed images 作为监督,即可表示复杂的三维场景,在新视角合成这一任务上的表现是非常 impressive 的。

问题1:3D渲染是要干什么?

用照相机拍照是一个现实世界的物理过程,主要是光学过程,拍照对象是现实世界中真实的万事万物,形成照片的机制主要就是:光经过镜头,到达传感器,被记录下来。

渲染就是用计算机模拟这一过程,模拟“拍照”的对象是已存在的某种三维场景表示(3D representation of the scene),模拟生成照片的机制是图形学研究人员精心设计的算法。

关键前提:渲染的前提是某种三维场景表示已经存在。渲染一词本身不包办生成三维场景表示。不过,渲染的确与三维场景表示的形式息息相关;因此研究渲染的工作通常包含对三维场景表示的探讨。

  • 问题2:3D渲染是图形学问题,那么原先大家是用什么传统图形学方法实现3D渲染的呢?

主要有两种算法:光栅化(rasterization),光线追踪(ray tracing);都是对照相机拍照的光学过程进行数学物理建模来实现的

Quick Overview: Neural Radiance Fields (NeRF)

NeRF的核心精神是将物体与场景的信息,编码 (encode) 进 MLP (multi-layer perceptron)中。然后使用 computer graphics(计算机图形学)中的 volume rendering(立体渲染)将 MLP中的信息投影出来。

在训练的部分, NeRF 所需要的是某个物体与场景的 multi-view多视角影像,利用这些影像来训练NeRF,或者更直接地说,将信息 encode进 NeRF。之后就可以对这个物体与场景 render出连续、不存在于原始信息的视角

NeRF整体的结果非常令人惊艳,特别是在金属或有光泽物体的结果,视觉的真实性真的非常高。可以参考以下的图片,或者直接到官方的页面看影片会更有感觉。

不过如果对于没有 rendering相关知识的人,要直接理解NeRF可能会有点障碍。因此,接下来会先简单带一些基础的 volume rendering与 ray tracing观念,再回头去看 NeRF到底是怎么做的。

Volume Rendering

所谓的 volume rendering (立体渲染) 指的是将 discretely sampled 3D data投影到 2D的技术。像是以下这张图片,我们已知一个物体的离散3D信息,对于目前的视角,就可以动态地渲染出这个视角看到的画面。

使用shear warp algorithm做volume rendering的例子。(资料来源)

Ray Tracing

而在 rendering上其中一个实作方法是 ray tracing。 这个方法可以用眼睛看物体来解释。我们眼睛之所以看的到物体,是因为有光源打到了物体,然后反射进我们的眼睛。而Ray tracing的想法其实就是反推这个射线 (ray),从相机中心发出射线,与物体相交就根据规则反射、折射或吸收,直到遇到光源或者走太远停住。

Ray tracing的方法示意图 (资料来源)

总之, ray tracing就是借由这个概念算出在 image平面上的 2D投影该长什么样子。实际上这边的名词蛮复杂的 (至少对非图学出身的笔者来说),像是 ray tracing、 ray casting、 ray marching什么的。为了避免混淆视听,这边就先不解释过多了。

接下来,就开始正式介绍 NeRF的细节。

Neural Radiance Fields (NeRF)-神经辐射场

NeRF网路的输入是一组 5D的参数,包含一组 3D的 location X = (x, y, z)跟一组 2D的 view direction (θ, φ),实事上这个 view direction表示为 3D Cartesian unit vector(笛卡尔单位矢量),称为 d。而NeRF网路的输出则是一组 emitted color (如RPG的c=(r, g, b)) 与 volume density σ

以下图来解释会更加清楚。这个 3D location指的是在 3D空间中的任何一个点,比如说怪手的尖端,而 view direction则是说我要从哪个视角看。NeRF网路的输出则是这个 3D点实际 (或者说推估) 的颜色,以及推估的 volume density(体积密度σ:可以简单理解为不透明度)。

有了RGB和体密度3D物体表示,就可以使用 Ray Tracing 等3D渲染方法来渲染出任意视角的2D投影。(Volume rendering)

NeRF的运作概念。(资料来源)

Radiance Fields,或者说映射 gθ ,能对三维场景进行隐式表示(implicit scene representation)。在上一节,我们说过某种三维场景表示正是渲染的前提。实现渲染也是 作者提出Radiance Fields这一新型三维场景表示方法 的目的所在。

NEeRF输入数据准备(将一系列的2d图片转成网络所需的输入):

多视角2D图像准备

想要利用NeRF拟合自己拍摄的3D场景,首先需要准备多张从不同角度拍摄的同场景静态2D图像,以沙发为例。

沙发多视图

相机内外参估计

不过仅仅有2D多视图还不够,在重建阶段,我们还需要各个点的位置 x={x,y,z} 和方位 d={θ,ϕ} 作为输入进行重建。为了获得这些数据,NeRF中采用了传统方法COLMAP进行参数估计。还是以沙发为例,通过COLMAP可以得到场景的稀疏重建结果,其输出文件包括相机内参,相机外参和3D点的信息,然后进一步利用LLFF开源代码中的imgs2poses文件将内外参整合到一个文件poses_boudns.npy中,该文件记录了相机的内参,包括图片分辨率(图片高与宽度)、焦距,共3个维度、外参(包括相机坐标到世界坐标转换的平移矩阵 t 与旋转矩阵 R,其中旋转矩阵为 R∈R3×3 的矩阵,共9个维度,平移矩阵为 t∈R3×1 的矩阵,3个维度,因此该文件中的数据维度为 N×17 (另有两个维度为光线的始发深度与终止深度,通过COLMAP输出的3D点位置计算得到),其中N为图片样本数。

ps: 这里建议将nerf-pytorch中加载poses_boudns.npy数据中的代码与imgs2poses中保存poses_boudns.npy的代码对着看一遍,就可以理解加载时为什么要搞一些奇怪的维度变换等操作了。

DCOLMAP重建结果

Raw数据到MPL的输入

输入生成与坐标系变换

在理解NeRF的过程中涉及到一个很重要的问题,即原文中提到的MLP的输入 x={x,y,z} 和 d={θ,ϕ} 是什么,其与相机内外参有什么关系?要搞懂这个问题,需要先理解坐标系变换

总的来说,坐标变换的目的在于将不同视角下视角特定的坐标系投影到一个统一的世界坐标中进行三维重建,见get_rays_np函数。首先,我们在像素坐标下进行网格点采样,得到特定分辨率图像的各个像素点坐标

i, j=np.meshgrid(np.arange(W, dtype=np.float32), np.arange(H, dtype=np.float32), indexing='xy')

然后,我们进行像素坐标到相机坐标的变换,要理解这个变换需要清楚两个点。

dirs=np.stack([(i-K[0][2])/K[0][0], -(j-K[1][2])/K[1][1], -np.ones_like(i)], -1)

首先像素坐标到相机坐标的变换属于投影变换的逆变换,即2D点到3D的变化,即我们需要根据像素点的坐标 (u,v) 计算该点在相机坐标系下的三维的坐标 (X,Y,Z)。

像素坐标到相机坐标

这时帅气的小伙伴就发现了,为什么相机坐标的 Y 和 Z 前面有负号?那是因为COLMAP采用的是opencv定义的相机坐标系统,其中x轴向右,y轴向下,z轴向内;而Nerf pytorch采用的是OpenGL定义的相机坐标系统,其中x轴向右,y轴向上,z轴向外。因此需要在y与z轴进行相反数转换。

最后,我们进行相机坐标到世界坐标的转换得到光线始发点与方向向量,这里需要了解世界坐标到相机坐标的变换过程,本质上为矩阵相乘,要进行反向变化只需要对变化矩阵求逆即可(COLMAP原输出的是世界坐标到相机坐标的旋转矩阵,在LLFF的imgs2poses.py中已经进行了求逆操作)。

rays_d=np.sum(dirs[..., np.newaxis, :] *c2w[:3,:3], -1)
rays_o=np.broadcast_to(c2w[:3,-1], np.shape(rays_d))

首先因为相机坐标与世界坐标的坐标原点不一致,要解决这个问题只需要将相机坐标进行平移变化即可进行对齐,实现平移的方式在是世界坐标系下减去一个平移矩阵,即相机外参中的平移矩阵 t ,对应得到代码中的rays_o。

平移以对齐坐标系的原点

此外,由于相机坐标与世界坐标的各个轴的朝向也是不同的,因此需要进一步通过旋转来对齐坐标轴的朝向,实现旋转的方式是在平移对齐坐标原点之后的基础上进行旋转,即与相机外参中的旋转矩阵 R 做矩阵乘法,对应得到代码中的rays_d。

旋转以对齐坐标轴的朝向

另附:NeRF的代码中其实还存在另外一个坐标系统:Normalized device coordinates(ndc)坐标系,一般对于一些forward facing的场景(景深很大)会选择进一步把世界坐标转换为ndc坐标,因为ndc坐标系中会将光线的边界限制在0-1之间以减少在景深比较大的背景下产生的负面影响。但是需要注意ndc坐标系不能和spherify pose(球状多视图采样时)同时使用,这点nerf-pytorch中并没有设置两者的互斥关系,而nerf-pl则明确设置了。

输入的进一步预处理

介绍完了将像素坐标转换到世界坐标形成光线始发点与方向向量之后,接下来便是文章中使用的一些trick,关于如何进一步处理光线起点与光线方向形成真正的MLP的输入值。

首先由于体素渲染需要沿着光线进行积分,而积分在计算机中是以离散的乘积和进行计算的,那么这里就涉及到在光线上进行点的采样。NeRF在光线的点采样过程中的进行了一些设计。首先为了避免大量的点采样导致的计算量的激增,NeRF设计了coarse to fine的采样策略。在coarse采样阶段,采用了带有扰动的均匀采样方法。第一步在光线的边界之间进行深度空间的均匀采样:

z_steps = torch.linspace(0, 1, N_samples, device=rays.device)
z_vals = near * (1-z_steps) + far * z_steps

然后在规定了下界与上界的范围内将采样点进行扰动:

z_vals_mid = 0.5 * (z_vals[: ,:-1] + z_vals[: ,1:])
upper = torch.cat([z_vals_mid, z_vals[: ,-1:]], -1)
lower = torch.cat([z_vals[: ,:1], z_vals_mid], -1)
perturb_rand = perturb * torch.rand(z_vals.shape, device=rays.device)
z_vals = lower + (upper - lower) * perturb_rand

最终coarse采样阶段在每条光线上采样了64个样本点。然后基于coarse采样得到的结果进一步指导fine的点采样,即在对最终颜色贡献更大(权重更大)的点附近进行更加密集的点采样。

然后是类似Transformer中的位置编码,将低维的坐标点与方向映射到高维空间以提升网络捕捉高频信息的能力,以nerf-pl中的代码nerf.py为例,embedding函数将3维的位置编码映射为63维,将3维的方向编码映射为27维。

处理好的样本点通过embedding函数进行低维到高维的映射之后得到的结果才会最终作为MLP的输入,MLP最终预测每个样本点的颜色值与不透明度。具体的网络结构图如下,NeRF为了建模view-specific的像素值,即不同的视角下看同一个点的颜色是有区别的,在网络的倒数第二层进一步把表示光线方向的高维编码输入以预测最终的像素值,而为了增强空间位置的condition,在网络的第四层又将表示位置的高维编码再一次输入到了网络中。

Nerf中MLP结构图

NeRF的训练

训练NeRF的输入数据是:从不同位置拍摄同一场景的图片,拍摄这些图片的相机位姿、相机内参,以及场景的范围。若图像数据集缺少相机参数真值,作者便使用经典SfM重建解决方案COLMAP估计了需要的参数,当作真值使用。

在训练使用NeRF渲染新图片的过程中,

  • 先将这些位置输入MLP以产生volume density和RGB颜色值;
  • 取不同的位置,使用体积渲染技术将这些值合成为一张完整的图像;
  • 因为体积渲染函数是可微的,所以可以通过最小化上一步渲染合成的、真实图像之间的差来训练优化NeRF场景表示。

这样的一个NeRF训练完成后,就得到一个以多层感知机的权重表示的模型。一个模型只含有该场景的信息,不具有生成别的场景的图片的能力。

除此之外,NeRF还有两个优化的trick:

  • 位置编码(positional encoding),类似于傅里叶变换,将低维输入映射到高维空间,提升网络捕捉高频信息的能力;
  • 体积渲染的分层采样(hierarchical volume sampling),通过更高效的采样策略减小估算积分式的计算开销,加快训练速度。

从MLP的输出到2D图像—体素渲染

在得到MLP对于某条光线中采样点的颜色与不透明度的预测值之后,便可以通过体素渲染进行该光线对应的像素颜色值的计算。这里的代码可以参考nerf-pl中的inference函数,每一步代码都有比较详细的说明。在训练阶段就将沿着特定光线预测的像素值与对应像素点的像素真值做L2损失函数以更新网络;在测试阶段就计算相机视锥中所有光线得到所有像素的像素值以组成完整的2D图像。

最后附上一些讲解关于相机内外参解释以及坐标系变换的资料:www.cs.cmu.edu/~16385/s17/Slides/11.1_Camera_matrix.pdf

ndc坐标系说明:github.com/bmild/nerf/files/4451808/ndc_derivation.pdf

Nerf-pytorch:https://github.com/yenchenlin/nerf-pytorch​

Nerf-pl:https://github.com/kwea123/nerf_pl​

Emitted Color & Volume Density

事实上 volume rendering的方法百百种,论文中使用的也只是其中一种依靠 emitted color与 volume density的方法。在论文中所谓的 volume density(密度)可以解释为这条射线会停在这个 3D位置的机率,而 emitted color指的是由某个视角看出去这个 3D位置的颜色

依照这样的设定, volume density在设计上应该只与 3D location有关,而 emitted color则是与 location与 view direction有关。因此 NeRF网路 (MLP) 会先吃进 3D location,通过 8层 FC layer,输出 volume density与 volume density以及 256维的 latent feature。而后与 view direction concatenate,在通过 FC layer得到 emitted color。

Vanilla NeRF

NeRF 所要做的 task 是 Novel View Synthesis,一般翻译为新视角合成任务,定义是:在已知视角下对场景进行一系列的捕获 (包括拍摄到的图像,以及每张图像对应的内外参),合成新视角下的图像。传统方法使用 IBR (Image Based Rendering) 方法的较多,也有一些使用深度学习和 IBR 结合的方法,这些我们不过多介绍。

NeRF 想做这样一件事,不需要中间三维重建的过程,仅根据位姿内参和图像,直接合成新视角下的图像。为此 NeRF 引入了辐射场的概念,这在图形学中是非常重要的概念,在此我们给出渲染方程的定义:

那么辐射和颜色是什么关系呢?我们不希望把这个文章讲复杂,简单讲就是,光就是电磁辐射,或者说是振荡的电磁场,光又有波长和频率,二者乘积为光速,光的颜色是由频率决定的,大多数光是不可见的,人眼可见的光谱称为可见光谱,对应的频率就是我们认为的颜色:

Positional Encoding

其实在 2018 年 Bengio 等人就发现 deep networks 更倾向于学习低频的函数,而可以想象的是,实际场景的神经辐射场基本上都是高频的,为此作者提出了 Positional Encoding(注意这里的 Positional Encoding 和 Transformer 中的 Positional Encoding 很像,但是解决问题是不一样的):

这里 p,L 都是标量, L 是超参数,对原始的坐标以及视角方向的每一维度都做相同的操作,然后再输入到网络中,事实上在官方的代码实现中,输入的是 (γ(p),p) ,也就是说原始信息也保留了下来。下图是一个关于 Positional Encoding 以及视角方向相关的 ablation study:

关于 Positional Encoding 以及 SIREN 等工作我们会在之后专门选一个章节进行讲解。读者暂时仅需保留一个印象即可。

Hierarchical volume sampling

使用体渲染积分就会遇到以下几个问题,虽然可以离散的近似计算积分,但是面临的问题就是如何采样。采样点过多开销过大,采样点过少近似误差有太大。直观的一个想法是,最好尽可能的避免在空缺部分以及被遮挡了的部分进行过多的采样,因为这些部分对最好的颜色贡献是很少的,基于这一想法 NeRF 提出分层采样训练的方式,如下图所示:

使用两个网络同时进行训练 (后称 coarse 和 fine 网络), coarse 网络输入的点是通过对光线均匀采样得到的,根据 coarse 网络预测的体密度值,对光线的分布进行估计,然后根据估计出的分布进行第二次重要性采样,然后再把所有的采样点 (Nc+Nf) 一起输入到 fine 网络进行预测。关于 loss 函数我们同样作为一个开放性的内容,不在此多做讲解了。

后NeRF时代

GIRAFFE:composition方向的代表作

2021CVPR的最佳论文奖得主GIRAFFE是NeRF、GRAF工作的延申。

在NeRF之后,有人提出了GRAF(Generative Radiance Fields),关键点在于引入了GAN来实现Neural Radiance Fields(神经辐射场);并使用conditional GAN实现对渲染内容的可控性。

在GRAF之后,GIRAFFE实现了composition。在NeRF、GRAF中,一个Neural Radiance Fields表示一个场景,one model per scene。而在GIRAFFE中,一个Neural Radiance Fields只表示一个物体,one object per scene(背景也算一个物体)。这样做的妙处在于可以随意组合不同场景的物体,可以改变同一场景中不同物体间的相对位置,渲染生成更多训练数据中没有的全新图像。

GIRAFFE实现composition

如图所示,GIRAFFE可以平移、旋转场景中的物体,还可以在场景中增添原本没有的新物体。

另外,GIRAFFE还可以改变物体的形状和外观,因为网络中加入了形状编码、外观编码变量(shape codes zsi , appearance codes zai )。

其他最新相关工作

2021年CVPR还有许多相关的精彩工作发表。例如,提升网络的泛化性:

  • pixelNeRF:将每个像素的特征向量而非像素本身作为输入,允许网络在不同场景的多视图图像上进行训练,学习场景先验,然后测试时直接接收一个或几个视图为输入合成新视图。
  • IBRNet:学习一个适用于多种场景的通用视图插值函数,从而不用为每个新的场景都新学习一个模型才能渲染;且网络结构上用了另一个时髦的东西 Transformer。
  • MVSNeRF:训练一个具有泛化性能的先验网络,在推理的时候只用3张输入图片就重建一个新的场景。

针对动态场景的NeRF:

  • Nerfies:多使用了一个多层感知机来拟合形变的SE(3) field,从而建模帧间场景形变。
  • D-NeRF:多使用了一个多层感知机来拟合场景形变的displacement。
  • Neural Scene Flow Fields:多提出了一个scene flow fields来描述时序的场景形变。

其他创新点:

  • PhySG:用球状高斯函数模拟BRDF(高级着色的上古神器)和环境光照,针对更复杂的光照环境,能处理非朗伯表面的反射。
  • NeX:用MPI(Multi-Plane Image )代替NeRF的RGBσ作为网络的输出。

5 不止是NeRF:Neural Rendering

Neural Radiance Fields的外面是Neural Rendering;换句话说,NeRF(Neural Radiance Fields)是Neural Rendering方向的子集。

在针对这个更宽泛的概念的综述State of the Art on Neural Rendering中,Neural Rendering的主要研究方向被分为5类,NeRF在其中应属于第2类“Novel View Synthesis”(不过这篇综述早于NeRF发表,表中没有NeRF条目)。

Neural Rendering的5类主要研究方向

表中彩色字母缩写的含义:

在这篇综述中,Neural Rendering被定义为:

Deep image or video generation approaches that enable explicit or implicit control of scene properties such as illumination, camera parameters, pose, geometry, appearance, and semantic structure.

Neural Rendering包含所有使用神经网络生成可控(且photo-realistic)的新图片的方法。“可控”指人可以显式或隐式地控制生成新图片的属性,常见的属性包括:光照,相机内参,相机位姿(外参),几何关系,外观,语义分割结构。在这个大框架下,NeRF是一种比较受欢迎的可控相机位姿的Neural Rendering算法。但Neural Rendering这个方向不止于此。

在目前的Neural Rendering方向,最火的子方向就是“Novel View Synthesis”,这与NeRF的强势蹿红密不可分;第二火的子方向是“Semantic Photo Synthesis”,这主要归功于语义分割以及相关的GAN领域的成熟度。“Semantic Photo Synthesis”方向也是成果颇丰,例如2019年CVPR的Semantic Image Synthesis with Spatially-Adaptive Normalization,其效果图如下。

Semantic Image Synthesis

最后我们给出一些 NeRF 的 paper list 以及代码整理:

综述论文

可以说是官方综述,作者列表是目前在Neural Rendering领域最活跃的一群人。两篇分别是2021、2020年的SIGGRAPH、CVPR讲座用到的综述,很全面很有条理,值得每位从业者一读!

SIGGRAPH 2021 Course: Advances in Neural Rendering

CVPR 2020 Tutorial: State of the Art on Neural Rendering

范围限定为可微渲染方法的综述:

Differentiable Rendering: A Survey

Prompt Learning(模板学习)

论文:https://arxiv.org/pdf/2107.13586.pdf

“Prompt:NLP 新范式”

“Pre-train, Prompt, and Predict” —- Prompt可以认为就是下游任务来适应预训练模型而做的微调 (所需数据量少、训练快、效果好),原始的微调是让预训练模型来适应下游任务。

文章摘自:未闻 Prompt 名

个人觉得 2021 年 NLP 最火的两个 idea,一个是对比学习(Contrastive Learning),另一个就是 Prompt

浅谈我对 Prompt 的理解

Prompt 说简单也简单,看了几篇论文以及博客后发现其实就是构建一个语言模版。但是细想起来又觉得复杂,因为总感觉里面还有很多细节,因此本文就来从头梳理一下 Prompt(Prompt 很多地方会翻译成「范式」,但是「范式」这个词本身也不好理解,因此读者把他看作是「模板」即可)

今天我还与室友讨论预训练模型(例如 BERT)到底做了什么,我给出的回答是

预训练模型提供了一个非常好的初始化参数,这组参数在预训练任务上的表现非常好(预训练损失非常低),但是由于下游任务千奇百怪,我们需要在这组参数的基础上进行 Fine-tune 以适应我们的下游任务(使得下游任务的损失值非常低)

上面这段话其实隐含了目前做 NLP 任务的大致流程,即 “Pre-train, Fine-tune”,而对我们来说实际上大部分时候都是直接拿别人预训练好的模型做 Fine-tune,并没有 Pre-train 这一步

融入了 Prompt 的模式大致可以归纳成 “Pre-train, Prompt, and Predict”,在该模式中,下游任务被重新调整成类似预训练任务的形式。例如,通常的预训练任务有 MLM(Masked Language Model),在文本情感分类任务中,对于 “I love this movie” 这句输入,可以在后面加上 Prompt:”the movie is ___”,组成如下这样一句话:

I love this movie, the movie is ___

然后让预训练模型用表示情感的答案(例如 “great”、”terrible” 等)做完形填空,最后再将该答案转换为情感分类的标签。这样一来,我们就可以通过构造合适的「模板」,通过小样本数据集训练一个模型来解决各种各样的下游任务

注意,Prompt 设计的这种完形填空和 MLM(Masked Language Modeling) 任务是有区别的,二者虽然都是都是词分类,但是候选集不同,MLM 的候选词是整个词库,不过如果是生成任务,那么 Prompt 和 MLM 的候选集就是一样的,都是整个词库

如何构建 Prompt

对于输入文本 x,存在一个函数 fPrompt(x),将 x 转化成 x′ 的形式,即

该函数通常会进行两步操作:

  1. 使用一个模板,模板通常为一段自然语言句子,并且该句子包含两个空位置:用于填输入 x 的位置 [X]、用于生成答案文本 z 的位置 [Z]
  2. 把输入 x 填到 [X] 的位置

以前文提到的例子为例,在文本情感分类任务中,假设输入是

x = "I love this movie"

使用的模板是

[X]. Overall, it was a [Z] movie

那么得到的 x′ 就应该是

I love this movie. Overall, it was a [Z] movie

在实际情况中,Prompt 来填充答案的位置一般在句中或句末。如果在句中,一般称这种 Prompt 为 Cloze Prompt;如果在句末,一般称这种 Prompt 为 Prefix Prompt。[X] 和 [Z] 的位置、数量以及使用模板句的不同,都有可能对结果造成影响,因此需要灵活调整

上面讲的都是简单的情感分类任务的 Prompt 设计,读者看到这里自然而然的会想到,其他 NLP 任务的 Prompt 如何设计呢?实际上刘鹏飞大神在他的论文中给我们提供了一些参考

Text Generation 中摘要任务里有一个关键字 TL;DR,这其实是 Too Long; Don't Read 的缩写

Prompt 的选择非常重要且困难

有上述 Prompt 的基础后,我们可以得知 Prompt 的设计主要包含两部分:

  1. 模板 T:例如 [X]. Overall, It was [Z]
  2. 标签词映射:即 [Z] 位置预测输出的词汇集合与真实标签 y 构成的映射关系。例如,标签 positive 对应单词 great,标签 negative 对应单词 terrible

基于 Prompt 的微调方法中,不同的模板和标签词对最终结果影响很大,下图是陈丹琦团队论文中的实验结果

从上图我们可以看出两点:

  1. 使用相同的「模板」,不同的「标签词」会产生不一样的效果。例如 great/terribel 和 cat/dog 这两组标签词的效果不一样,而且即便是相同标签词,互换顺序也会导致最终效果有所变化,例如 cat/dog 和 dot/cat
  2. 使用相同「标签词」,对「模板」进行小改动(例如增删标点)也会呈现不同的结果

Prompt 的设计

Prompt 大概可以从下面三个角度进行设计:

  • Prompt 的形状
  • 人工设计模板
  • 自动学习模板

Prompt 的形状

Prompt 的形状主要指的是 [X] 和 [Z] 的位置和数量。上文提到的 Cloze Prompt 与 Maksed Language Model 的训练方式非常类似,因此对于 MLM 任务来说,Cloze Prompt 更合适;对于生成任务或者使用自回归 LM 解决的任务,Prefix Prompt 更合适。

人工设计模板

Prompt 的模板最开始是人工设计的,人工设计一般基于人类的自然语言知识,力求得到语义流畅且高效的「模板」。例如,Petroni 等人在著名的 LAMA 数据集中为知识探针任务人工设计了 Cloze Templates;Brown 等人为问答、翻译和探针等任务设计了 Prefix Templates。人工设计模板的优点是直观,但缺点是需要很多实验、经验以及语言专业知识。下图是 GPT Understands, Too 论文中的一个实验结果

可以看到不同的 Prompt 只有细微的区别,有的甚至只是增加减少一个词,但是最后的结果会差几十个点

自动学习模板

为了解决人工设计模板的缺点,许多研究员开始探究如何自动学习到合适的模板。自动学习的模板又可以分为离散(Discrete Prompts)和连续(Continuous Prompts)两大类。离散方法主要包括:Prompt Mining,Prompt Paraphrasing,Gradient-based SearchPrompt Generation 和 Prompt Scoring;连续的则主要包括 Prefix TuningTuning Initialized with Discrete promptsHard-Soft Prompt Hybrid TuningP-Tuning v2

离散 Prompts

简单说一下上述几种方法,首先是离散的 Prompt Mining,这篇文章发表在 TACL 2020,讲的是如何拿预训练语言模型当作「知识库」使用,并且引入了依存树和 Paraphrase(转述)等方法来挖掘更好的「模板」,下图是实验结果

可以看到,被挖掘出来的若干「连接谓词」相比于人工设计的「模板」结果提升还是很明显的

有很多种方法可以实现 Prompt Paraphrsing,例如「回译」,我们通过 DeepL 翻译看个例子:

这样我们就得到了 x shares a border with y 的一个 Prompt Paraphrasing:x and y share a boundary

论文 BARTScore 干脆给我们提供了一张表,里面有各种词组的同义替换,这个我再熟悉不过了,因为以前英语考试我也背过类似的东西

Gradient-based Search(基于梯度的搜索)是由论文 AUTOPROMPT 提出的,这篇文章发表在 EMNLP 2020,它的主要思想用下面这张图就可以表示

上图中,a real joy 是原始的输入句子 xinp,红色的 Trigger tokens 是由 xinp「激发」的相关词汇集合 xtrig,根据 Template λ 的配置,将 xtrig 和 xinp 组合起来构造最终的输入 xprompt,送入 Masked LM 预测情感标签。下面的表格增加了很多 NLP 其他任务的例子

关于如何生成 xtrig 集合,实际上主要使用的是 HotFlip 和对抗训练的思想,感兴趣的同学可以看原论文以及 HotFlip: White-box adversarial examples for text classificationUniversal Adversarial Triggers for Attacking and Analyzing NLP 这两篇论文

Prompt Generation 是陈丹琦团队的一项工作,主要是把 Seq2Seq 预训练模型 T5 应用到模板搜索的过程。T5 基于多种无监督目标进行预训练,其中最有效的一个无监督目标就是:利用 <X> 或 < Y > 替换一个或多个连续 span,然后生成对应输出。例如:

Thank you <X> me to your party <Y> week

T5 会在 <X> 生成 for inviting,在 <Y> 生成 last。很显然,T5 这种方式很适合生成模板,而且不需要指定模板的 token 数。具体来说,有三种可能的生成方式⟨S1⟩→⟨X⟩ M(y) ⟨Y⟩ ⟨S1⟩⟨S1⟩→⟨S1⟩ ⟨X⟩ M(y) ⟨Y⟩⟨S1⟩,⟨S2⟩→⟨S1⟩ ⟨X⟩ M(y) ⟨Y⟩ ⟨S2⟩

具体的模板生成过程如下图所示:

首先在标签词前后添加填充位 <X> 和 < Y>(上面提到的三种生成方式),然后将其送入 T5 模型中,T5 会自动在填充位生成序列,最后将标签词(great 或 terribel)转换为 [MASK] 标签,形成多个模板。具体过程中采用 Beam Search 的方法生成多个候选模板,然后对每一个候选模板利用 dev 集进行微调,选择其中一个最佳模板

我还想说一下这篇论文中另外一个有意思的点,最后送入模型进行预测的句子还拼接上了每种类别的「示例」(Demonstration),如下图所示

这种 Prompt 的设计有点像是在做语义相似度任务,X 为原始 Input 句子,已知 Y 为正例,Z 为负例,构造了如下形式的输入:

X是[MASK]例?Y为正例;Z为负例

这有点像是编程语言中的三目运算符,或者说相当于让模型比较 X 与 Y、Z 的语义相似度。这里我们自然而然会想问:Y、Z 是如何挑选出来的?实际上是依据下面两条规则:

  1. 对于每个原始输入句子,从每个类别中随机采样一个样本「示例」拼接到 Prompt 中
  2. 对于每个原始输入句子,在每个类别中,通过与 Sentence-BERT 进行相似度计算,从相似度最高的前 50% 样本中随机选择一个样本「示例」

连续 Prompts

构造 Prompt 的初衷是能够找到一个合适的方法,让 Pre-trained Language Model(PLM)更好地输出我们想要的结果,但其实并不一定要将 Prompt 的形式设计成人类可以理解的自然语言,只要机器理解就行了。因此,还有一些方法探索连续型 Prompts—— 直接作用到模型的 Embedding 空间。连续型 Prompts 去掉了两个约束条件:

  1. 模版中词语的 Embedding 可以是整个自然语言的 Embedding,不再只是有限的一些 Embedding
  2. 模版的参数不再直接取 PLM 的参数,而是有自己独立的参数,可以通过下游任务的训练数据进行调整

Prefix Tuning 最开始由 Li 等人提出,这是一种在输入句子前添加一组连续型向量的方法,该方法保持 PLM 的参数不动,仅训练前缀(Prefix)向量。Prefix Tuning 的提出主要是为了做生成任务,因此它根据不同的模型结构定义了不同的 Prompt 拼接方式,在 GPT 类的 Auto-Regressive(自回归)模型上采用的是 [Prefix;x;y] 的方式,在 T5 类的 Encoder-Decoder 模型上采用的是 [Prefix;x;Prefix′;y] 的方式

输入部分 Prefix, \(x, y\) 的 Position id 分别记作

\(\mathrm{P}{\mathrm{idx}}\) , \(\mathrm{X}{\mathrm{idx}}\) , \(\mathrm{Y}{\mathrm{idx}}\)。Prefix Tuning 初始化一 个可训练的矩阵,记作 \(P\theta \in \mathbb{R}^{\left|P_{\mathrm{idx}}\right| \times \operatorname{dim}\left(h_i\right)}\) ,其中
\(h_i= \begin{cases}P_\theta[i,:], & \text { if } i \in \mathrm{P}{\mathrm{idx}} \ \mathbf{L M}\phi\left(z_i, h_{<i}\right), & \text { otherwise }\end{cases}\)
上述公式的含义是,索引 $i$ 如果属于前缀的部分,则从 \(P_\theta\) 中抽取向量; \(i\) 如果不是前缀部 分,则由参数固定的预训练模型生成对应的向量。训练目标为:
\(\max \phi \log p\phi(y \mid x)=\sum_{i \in \mathrm{Y}{\mathrm{idx}}} \log p\phi\left(z_i \mid h_{<i}\right)\)

 \(P_\theta\) 本质上是一个矩阵,而生成一个矩阵的方法又很多,可以用 nn.Embedding(),或者 nn.Linear()

同样是在连续空间上搜索 Prompt,OptiPrompt 构建的「模板」并不局限于前缀,也可以在句子的中间

Hard-Soft Prompt Hybrid Tuning 方法可以说是人工设计和自动学习的结合,它通常不单纯使用可学习的 Prompt 模板,而是在人工设计的模板中插入一些可学习的 Embedding。实际上有了上面的基础我们都知道,连续的 Prompt 要比离散的 Prompt 好一点,但是在此基础上还有什么改进的余地吗?Liu 等人提出的 P-Tuning 解决了 Prompt token 之间的关联性问题

之前连续的 Prompt 生成方式无非都是训练一个矩阵,然后通过索引出矩阵的某几行向量拼起来。坦白地说,我们希望这些 prompt token Embedding 之间有一个比较好的关联性,而不是独立地学习,为了解决这个问题,P-Tuning 引入了一个 Prompt Encoder(如下图 b 所示)

上图 a 是传统的离散型 Prompt,我们把生成离散 Prompt token 的东西叫做 Prompt Generator;上图 b 首先传入一些 Virtual(Pseudo)token,例如 BERT 词表中的 [unused1],[unused2],… 当然,这里的 token 数目是一个超参数,插入的位置也可以调整。将这些 Pseudo token 通过一个 Prompt Encoder 得到连续的向量 h0,…,hm,其中

大家可能想问,如何优化 P-tuning?实际上根据标注数据量的多少,分两种情况讨论

  1. 标注数据比较少。这种情况,我们固定 PLM 的参数,只优化 [P0]∼[Pm] 这几个 token 的 Embedding。换句话说,我们只是要更新 Prompt Encoder 的参数
  2. 标注数据很充足。这种情况直接放开所有参数微调

就在 P-Tuning 方法提出不久后,Liu 等人又提出了 P-Tuning v2,主要解决 P-Tuning 的两个问题:

  1. 当预训练模型的参数量低于 100 亿(10B)时,Prompt tuning 会比传统的 Fine-tuning 差
  2. 诸如序列标注这样对推理和理解要求高的任务,prompt tuning 效果会变差

Liu 等人认为先前的 P-Tuning 只用了一层 BiLSTM 来编码 Pseudo token,这是其推理能力不足的原因之一,因此 v2 版本提出 Deep Prompt Tuning,用 Prefix Tuning 中的深层模型替换 BiLSTM,如下图所示

P-Tuning v2 相比于 P-Tuning,区别在于:

  • 取消 Reparameterization:以前的方法利用重参数化功能来提高训练速度和鲁棒性(例如,用于 Prefix-Tuning 的 MLP 和用于 P-Tuning 的 LSTM)。在 P-Tuning v2 中,作者发现重参数化的改进很小,尤其是对于较小的模型,同时还会影响模型的表现
  • Multi-task Learning:Deep Prompt Tuning 的优化难题可以通过增加额外的任务数据或者无标注数据来缓解,同时可微调的 Prefix Continuous Prompt 也可以用来做跨任务的知识共享。例如在 NER 中,可以同时训练多个数据集,不同数据集使用不同的顶层 Classifier,但是 Prefix Continuous Prompt 是共享的
  • 取消 verbalizer:v2 取消了标签映射,完全变为生成模型,可以在 [CLS] 部分输出句子级别的标签(Sentence-level label),也可以在每个 token 位置输出 token 级别的标签(Token-level label),直接输出真实标签

关于 P-Tuning 还有一些碎碎念,主要是从各个博客上看到的,汇总在这里。首先是 v1 版本的 LSTM,实际上引入 LSTM 目的是为了帮助「模板」生成的 token(某种程度上)更贴近自然语言,或者说 token 之间的语义更流畅,但更自然的方法应该是在训练下游任务的时候,不仅预测下游任务的目标 token(例如 “great”、”terrible”),还应该同时做其他 token 的预测

比如,如果是 MLM 模型,那么也随机 MASK 掉其它的一些 token 来预测,如果是 LM 模型,则预测完整的序列,而不单单是目标词。这样做的理由是:因为我们的 MLM/LM 都是经过自然语言预训练的,所以我们认为它能够很好的完成序列的重构,即便一开始不能,随着迭代轮数的增加,模型也能很好完成这项任务。所以这本质上是让模型进行「负重训练」

* 为什么要引入 Prompt?

在标准的 Fine-tune 过程中(如上图 b 所示),新引入的参数量可能会很大(独立于原始预训练模型外的参数),例如基于 RoBERTa-large 的二分类任务会新引入 2048 个参数(nn.Linear(1024, 2)),如果你仅有例如 64 个标注数据这样的小样本数据集,微调会非常困难

为解决这一问题,Prompt 应运而生(如上图 a 所示),直接将下游任务转换为输出空间有限的 MLM 任务。值得注意的是:上述方法在预训练参数的基础上进行微调,并且没有引入任何新参数,同时还减少了微调和预训练任务之间的差距。总的来说,这可以更有效地用于小样本场景

Prompt 的挑战与展望

尽管 Prompt 研究搞得如火如荼,但目前仍存在许多问题值得研究者们去探究

  1. Prompt 的设计问题。目前使用 Prompt 的工作大多集中于分类任务和生成任务,其它任务则较少。另外,「模板」和「答案」的联系也亟待解决。模型的表现同时依赖于使用的「模板」和「答案」的映射,如何同时搜索或者学习出两者联合的最好效果仍然很具挑战性
  2. Prompt 的理论分析和可解释性。尽管 Prompt 方法在很多情况下都取得了成功,但是目前 Prompt-based Learning 理论分析还很少,人们很难了解 Prompt 为什么能达到好的效果,又为什么在自然语言中意义相近的 Prompt 有时效果却相差很大
  3. Prompt 在 PLM debias 方面的应用。由于 PLM 在预训练过程中见过了大量的人类世界的自然语言,所以很自然地会受到一些影响。举一个简单的例子,比如说训练语料中有非常多 “The capital of China is Beijing”,导致模型每次看到 “capital” 的时候都会预测出 “Beijing”,而不是去分析到底是哪个国家的首都。在应用的过程中,Prompt 还暴露了 PLM 学习到的很多其它 bias,比如种族歧视、性别对立等。这也许会是一个值得研究的方向

One More Thing

最后我还想提一个实际 Code 过程中存在的问题。我们知道 MLM 任务会输出句子中 [MASK] 位置最有可能的词,而 Prompt 也类似的,例如下面的例子

这是一条__新闻。中国足球出线的可能性只有0.001%,留给中国队的时间不多了

这是一个新闻分类问题,真实标签有 “体育”、”财经”、”娱乐” 等,上面的样本很明显是一条体育新闻,因此我们希望模型对 [MASK] 部分输出 “体育”,但事实真的如此吗?实际情况模型的输出可能是 “足球”,但你认为模型预测的 “足球” 有问题吗?好像也没啥毛病,因此这就引申出了 Prompt 的一个问题,是否应该限制模型的输出空间?

还是上面新闻分类的例子,我们是否应该限制模型输出的空间,让他固定只能预测 “体育”、”财经”、”娱乐” 这几个标签?或者我们干脆把这几个标签换成索引,那就是让模型从 0,1,2 这三个数字选一个。Wait Wait Wait,如果这么做的话,和 Fine-Tune 有什么区别,Fine-Tune 也是把标签转换成索引,让模型看了句子之后,从这几个索引中选一个作为预测值

这么说的话,那我们就不应该限制模型的输出空间,可是这样的话 [MASK] 位置的输出就限制的太死了,必须一定是 “good”、”财经” 才算对,如果输出 “nice”、”财政” 就算错。实际上输出近义词或者相似词,在零样本的情况下会经常出现,但是如果你用一些有标签的样本去训练,模型自己就会慢慢固定输出空间。例如 “财经”,它不会预测成 “财政”,只会预测成其它类型的新闻,例如 “体育”

References

Point Transformer –ICCV2021

论文:Point Transformer
作者单位:牛津大学, 港中文(贾佳亚等), Intel Labs

transformer应用到了点云任务处理中。为点云设计了自注意力层,并使用它们来构造诸如语义场景分割,object part分割和对象分类等任务的自注意力网络。

attention层设计:

这里的y是输出的feature,ϕ、ψ、α都是逐点特征变换的一种方式(比如mlp),δ是一个位置编码函数,ρ是正则化函数,简单来说,xi是点i的feature向量,先通过特征变换将点i和点j(Xj是Xi的邻域上的点,而非全局的,目的是减少计算量)的特征得到,这里的β是关系函数,通过这个函数得到两个点特征之间的关系,也就是建立每个点特征之间的关系,然后加上位置编码函数δ,γ是映射函数,也就是映射到某一维度而用。在这基础上就可以设计这里的重点,Point transformer层了

输入是(x,p)也就是每个点的位置信息,首先通过两个线性函数编码不同主次点的特征向量(也就是得到前面的key向量),再用一个MLP得到位置函数,也就是前面的查询向量),两者结合得到relation关系,然后再用一个线性函数得到它的值向量,将relation和值向量结合,也就是前面说的对于每个点既关注它的和其他点之间的语义关系,也关注它和其他点之间的位置关系,最后输出y作为点云处理结果。

位置函数也就是计算查询向量的那个函数:

在这里插入图片描述

p就是各自点的三维坐标值,θ是一个MLP层,而前面的线性函数也就是ax+b的形式(就是linear层)

定义完了transformer层,就可以定义一个block来作为基本的block(下图a):

输入是点集合x(拥有各自的三维点坐标等点特征),输出就是将每个点x的更新后的特征输出:

down的功能是根据需要减少点集的基数,简单来说就是减少点,而up就是根据两个不同数量的点来得到结合后的结果,常常使用在U型网络设计中(也就是当前层结果是结合了当前层的输入和之前某一层不同维度的输出而得到)

transition down:

step1:farthest point sample,把p1个点采样到 p个点,通过MLP改变特征向量(y,p),通过KNN算法,把p个点分成p2类,每个类内部做最大池化得到最终输出(y,p2)。

up模块:input1 如何才能扩充点数:通过线性插值算法

网络结构:

实验结果:

用于大规模语义场景分割的具有挑战性的S3DIS数据集上,Point Transformer在Area 5上的mIoU达到70.4%,比最强的现有模型高3.3个绝对百分点,并首次超过70%mIoU阈值 。

在ModelNet40和ShapeNetPart数据集上的性能表现:

目前paper with code 网站的排名:

3D Point Cloud Classification on ModelNet40
3D Point Cloud Classification on ModelNet40
3D Part Segmentation on ShapeNet-Part
3D Semantic Segmentation on SemanticKITTI

Induction Networks for Few-Shot TextClassification

论文:https://arxiv.org/abs/1902.10482?context=cs.CL

                            IJCNLP 2019 paper

代码: https://github.com/wuzhiye7/Induction-Network-on-FewRel

在深度学习领域,监督式深度学习对大型标记数据集的贪婪需求是出了名的,然而又由于标注数据集的昂贵成本,这就限制了深度模型对新类的可泛化性。本文提出了一个用于在文本分类领域的小样本学习训练工作。

什么是小样本学习(以图片为例)

few-shot learing 的训练目标与传统的监督学习目标不同,传统的分类是学会识别训练集合里面的图片,并且泛化到测试集合,神经网络识别出该图片属于哪个类。而few shot learing是让机器自己学会学习,学习的目的不是让机器学会那个是大象那个是老虎,而是让模型学会学习不同类别的不同之处,给定两张图片,模型知道两个图片是否是同一类别。哪怕模型训练集中没有出现过该类别。

当前的小样本学习技术经常会将输入的query和support的样本集合进行sample-wise级别的对比。但是,如果跟同一个类别下的不同表达的样本去对比的时候产生的效果就不太好,除此之外,目前的技术会使用简单地求和或平均表示来计算类别,这会丢失一些信息。因此本文利用胶囊网络,通过学习sample所属于的类别的表示得到class-wise的向量,然后跟输入的query进行对比。

模型如下:

模型分为三个模块:Encoder Module, Induction Module and Relation Module.

Encoder Module

编码器使用双向LSTM,然后对每个隐藏层进行self-attention。

其中H维度为[C*K, T, 2u] ,经过矩阵变化,a的维度变为[C*K, T] ,最后e的维度为[C*K, 2u]

Induction Module

本模块的主要目的是设计一个从样本向量到类向量的非线性映射。

这是使用动态路由算法,输出的capsule数为1.

首先将样本表征进行一次变换,这里为了能够支持不同大小的C,对原Capsule Network中不同类别使用不同的W做了修改,也就是使用一个所有类别共享的W。

Relation Module

在得到类表示后,就可以计算ci与query set的相关性了。

Objective Function

使用均方误差来计算损失,匹配对的相似度为1,不匹配的相似度为0。

点云基础知识+ 三维计算机视觉研究内容

1)点云概念

点云是在同一空间参考系下表达目标空间分布和目标表面特性的海量点集合,在获取物体表面每个采样点的空间坐标后,得到的是点的集合,称之为“点云”(Point Cloud)。

2)点云图像是最基础也是最常见的三维图像。

那什么是三维图像呢?三维图像是一种特殊的图像信息表达形式。相比较于常见的二维图像,其最大的特征是表达了空间中三个维度(长度宽度和深度)的数据。

3)三维图像的表现形式

深度图(以灰度表达物体与相机的距离),几何模型(由CAD软件建立),点云模型(所有逆向工程设备都将物体采样成点云)。

4)点云根据测量原理主要分为两种

根据激光测量原理得到的点云,包括三维坐标(XYZ)和激光反射强度(Intensity)。强度信息与目标的表面材质、粗糙度、入射角方向,以及仪器的发射能量,激光波长有关。

根据摄影测量原理得到的点云,包括三维坐标(XYZ)和颜色信息(RGB)。

当然也有把激光和摄影相结合在一起的(多传感器融合技术),这种结合激光测量和摄影测量原理得到点云,包括三维坐标(XYZ)、激光反射强度(Intensity)和颜色信息(RGB)。

本次的文章主要讲的是基于摄像技术的点云配准。

5)点云的获取设备

RGBD设备(深度摄像机)是可以获取点云的设备。比如PrimeSense公司的PrimeSensor、微软的Kinect、华硕的XTionPRO。

6)点云的属性

空间分辨率、点位精度、表面法向量等。

7)点云存储格式

.pts; .asc ; *.dat; .stl ; [1] .imw;.xyz;.las。

8)点云的数据类型

(1)pcl::PointCloudpcl::PointXYZ

PointXYZ 成员:float x,y,z;表示了xyz3D信息,可以通过points[i].data[0]或points[i].x访问点X的坐标值

(2)pcl::PointCloudpcl::PointXYZI

PointXYZI成员:float x, y, z, intensity; 表示XYZ信息加上强度信息的类型。

(3)pcl::PointCloudpcl::PointXYZRGB

PointXYZRGB 成员:float x,y,z,rgb; 表示XYZ信息加上RGB信息,RGB存储为一个float。

(4)pcl::PointCloudpcl::PointXYZRGBA

PointXYZRGBA 成员:float x , y, z; uint32_t rgba; 表示XYZ信息加上RGBA信息,RGBA用32bit的int型存储的。

(5) PointXY 成员:float x,y;简单的二维x-y点结构

(6)Normal结构体:

表示给定点所在样本曲面上的法线方向,以及对应曲率的测量值,用第四个元素来占位,兼容SSE和高效计算

9)点云处理的三个层次

一般将图像处理分为三个层次,低层次包括图像强化,滤波,关键点/边缘检测等基本操作。中层次包括连通域标记(label),图像分割等操作。高层次包括物体识别,场景分析等操作。工程中的任务往往需要用到多个层次的图像处理手段。

低层次处理方法

①滤波方法:双边滤波、高斯滤波、条件滤波、直通滤波、随机采样一致性滤波。②关键点:ISS3D、Harris3D、NARF,SIFT3D

中层次处理方法

①特征描述:法线和曲率的计算、特征值分析、SHOT、PFH、FPFH、3D Shape Context、Spin Image

②分割与分类:

分割:区域生长、Ransac线面提取、全局优化平面提取

K-Means、Normalize Cut(Context based)

3D Hough Transform(线、面提取)、连通分析

分类:基于点的分类,基于分割的分类,基于深度学习的分类(PointNet,OctNet)

高层次处理方法

①配准

点云配准分为粗配准(Coarse Registration)和精配准(Fine Registration)两个阶段。

精配准的目的是在粗配准的基础上让点云之间的空间位置差别最小化。应用最为广泛的精配准算法应该是ICP以及ICP的各种变种(稳健ICP、point to plane ICP、Point to line ICP、MBICP、GICP、NICP)。

粗配准是指在点云相对位姿完全未知的情况下对点云进行配准,可以为精配准提供良好的初始值。当前较为普遍的点云自动粗配准算法包括基于穷举搜索的配准算法和基于特征匹配的配准算法。

基于穷举搜索的配准算法:

遍历整个变换空间以选取使误差函数最小化的变换关系或者列举出使最多点对满足的变换关系。如RANSAC配准算法、四点一致集配准算法(4-Point Congruent Set, 4PCS)、Super4PCS算法等……

基于特征匹配的配准算法:

通过被测物体本身所具备的形态特性构建点云间的匹配对应,然后采用相关算法对变换关系进行估计。如基于点FPFH特征的SAC-IA、FGR等算法、基于点SHOT特征的AO算法以及基于线特征的ICL等…

②SLAM图优化

Ceres(Google的最小二乘优化库,很强大), g2o、LUM、ELCH、Toro、SPA

SLAM方法:ICP、MBICP、IDC、likehood Field、NDT

③三维重建

泊松重建、 Delaunay triangulations、表面重建,人体重建,建筑物重建,树木重建。结构化重建:不是简单的构建一个Mesh网格,而是为场景进行分割,为场景结构赋予语义信息。场景结构有层次之分,在几何层次就是点线面。实时重建:重建植被或者农作物的4D(3D+时间)生长态势;人体姿势识别;表情识别;

④点云数据管理

点云压缩,点云索引(KD、Octree),点云LOD(金字塔),海量点云的渲染。

三维计算视觉研究内容

 1)三维匹配:两帧或者多帧点云数据之间的匹配,因为激光扫描光束受物体遮挡的原因,不可能通过一次扫描完成对整个物体的三维点云的获取。因此需要从不同的位置和角度对物体进行扫描。三维匹配的目的就是把相邻扫描的点云数据拼接在一起。三维匹配重点关注匹配算法,常用的算法有最近点迭代算法 ICP和各种全局匹配算法。

 2)多视图三维重建:计算机视觉中多视图一般利用图像信息,考虑多视几何的一些约束,射影几何和多视图几何是视觉方法的基础,在摄影测量中类似的存在共线方程。光束平差法是该类研究的核心技术。这里也将点云的多视匹配放在这里,比如人体的三维重建,点云的多视重建不再是简单的逐帧的匹配,还需要考虑不同角度观测产生误差累积,因此存在一个针对三维模型进行优化或者平差的过程在里面。多视图三维重建这里指的只是静态建模,输入是一系列的图像或者点云集合。可以只使用图像,或者只使用点云,也可以两者结合(深度图像)实现,重建的结果通常是Mesh网格。

  • SFM(运动恢复结构) vs Visual SLAM  [摘抄] SFM 和 Visual SLAM
  • Multi-View Stereo (MVS)多视图立体视觉,研究图像一致性,实现稠密重建。

  3)3D SLAM

  按照传感器类型分类:可以分为基于激光的SLAM和基于视觉的SLAM。

  基于激光的SLAM可以通过点云匹配(最近点迭代算法 ICP、正态分布变换方法 NDT)+位姿图优化(g2o、LUM、ELCH、Toro、SPA)来实现;实时激光3D SLAM算法 (LOAM,Blam,CartoGrapher等);Kalman滤波方法。通常激光3D SLAM侧重于定位,在高精度定位的基础上可以产生3D点云,或者Octree Map。

  基于视觉(单目、双目、鱼眼相机、深度相机)的SLAM,根据侧重点的不同,有的侧重于定位,有的侧重于表面三维重建。不过都强调系统的实时性

  (1)侧重于定位的VSLAM系统比如orbSLAM,lsdSLAM;VINS是IMU与视觉融合的不错的开源项目。

  

  (2)侧重于表面三维重建SLAM强调构建的表面最优,或者说表面模型最优,通常包含Fusion融合过程在里面。通常SLAM是通过观测形成闭环进行整体平差实现,优先保证位姿的精确;而VSLAM通过Fusion过程同时实现了对构建的表面模型的整体优化,保证表面模型最优。最典型的例子是KinectFusion,Kinfu,BundleFusion,RatMap等等。

  (4)目标检测与识别:无人驾驶汽车中基于激光数据检测场景中的行人、汽车、自行车、道路(车道线,道路标线,路边线)以及道路设施(路灯)和道路附属设施(行道树等)。这部分工作也是高精度电子地图的主要内容。当然高精度电子地图需要考虑的内容更多。同时室内场景的目标识别的研究内容也很丰富,比如管线设施,消防设施等。

  (5)形状检测与分类:点云技术在逆向工程中有很普遍的应用。构建大量的几何模型之后,如何有效的管理,检索是一个很困难的问题。需要对点云(Mesh)模型进行特征描述,分类。根据模型的特征信息进行模型的检索。同时包括如何从场景中检索某类特定的物体,这类方法关注的重点是模型。

  (6)语义分类:获取场景点云之后,如何有效的利用点云信息,如何理解点云场景的内容,进行点云的分类很有必要,需要为每个点云进行Labeling。可以分为基于点的分类方法和基于分割的分类方法。从方法上可以分为基于监督分类的技术或者非监督分类技术,深度学习也是一个很有希望应用的技术。最近深度学习进行点云场景理解的工作多起来了,比如PointNet,各种八叉树的Net。

(7)双目立体视觉与立体匹配 ZNCC:立体视觉(也称双目视觉)主要研究的两个相机的成像几何问题,研究内容主要包括:立体标定(Stereo Calibration)、立体校正(Stereo Rectification)和立体匹配(Stereo Matching)。目前,立体标定主要研究的已经比较完善,而立体匹配是立体视觉最核心的研究问题。按照匹配点数目分类,立体匹配可分为稀疏立体匹配(sparse stereo matching)和密集立体匹配(dense stereo matching)。稀疏立体匹配由于匹配点数量稀少,一般很难达到高精度移动测量和环境感知的要求。因此,密集立体匹配是学术界和工业界的主要研究和应用方向。

参考:https://mp.weixin.qq.com/s/cOHAQX12k19eogxfpk95tA

(8)自动造型(构型),快速造型(构型)技术。对模型进行凸分割,模型剖分,以实现模型进一步的编辑修改,派生出其他的模型。

(9)摄像测量技术,视频测量


PointNet++

论文:https://arxiv.org/abs/1706.02413(NIPS 2017)

code: https://github.com/charlesq34/pointnet2

1、改进

PointNet因为是只使用了MLP和max pooling,没有能力捕获局部结构,因此在细节处理和泛化到复杂场景上能力很有限。

  1. point-wise MLP,仅仅是对每个点表征,对局部结构信息整合能力太弱 –> PointNet++的改进:sampling和grouping整合局部邻域
  2. global feature直接由max pooling获得,无论是对分类还是对分割任务,都会造成巨大的信息损失 –> PointNet++的改进:hierarchical feature learning framework,通过多个set abstraction逐级降采样,获得不同规模不同层次的local-global feature
  3. 分割任务的全局特征global feature是直接复制与local feature拼接,生成discriminative feature能力有限 –> PointNet++的改进:分割任务设计了encoder-decoder结构,先降采样再上采样,使用skip connection将对应层的local-global feature拼接

2、方法

PointNet++的网络大体是encoder-decoder结构

encoder为降采样过程,通过多个set abstraction结构实现多层次的降采样,得到不同规模的point-wise feature,最后一个set abstraction输出可以认为是global feature。其中set abstraction由sampling,grouping,pointnet三个模块构成。

decoder根据分类和分割应用,又有所不同。分类任务decoder比较简单,不介绍了。分割任务decoder为上采样过程,通过反向插值和skip connection实现在上采样的同时,还能够获得local+global的point-wise feature,使得最终的表征能够discriminative(分辩能力)。

思考:

  1. PointNet++降采样过程是怎么实现的?/PointNet++是如何表征global feature的?(关注set abstraction, sampling layer, grouping layer, pointnet layer)
  2. PointNet++用于分割任务的上采样过程是怎么实现的?/PointNet++是如何表征用于分割任务的point-wise feature的?(关注反向插值,skip connection)

 🐖:上图中的 d 表示坐标空间维度, C 表示特征空间维度

2.1 encoder

在PointNet的基础上增加了hierarchical (层级)feature learning framework的结构。这种多层次的结构由set abstraction层组成。

在每一个层次的set abstraction,点集都会被处理和抽象,而产生一个规模更小的点集,可以理解成是一个降采样表征过程,可参考上图左半部分。

set abstraction由三个部分构成(代码贴在下面):

def pointnet_sa_module(xyz, points, npoint, radius, nsample, mlp, mlp2, group_all, is_training, bn_decay, scope, bn=True, pooling='max', knn=False, use_xyz=True, use_nchw=False):
    ''' PointNet Set Abstraction (SA) Module
        Input:
            xyz: (batch_size, ndataset, 3) TF tensor
            points: (batch_size, ndataset, channel) TF tensor
            npoint: int32 -- #points sampled in farthest point sampling
            radius: float32 -- search radius in local region
            nsample: int32 -- how many points in each local region
            mlp: list of int32 -- output size for MLP on each point
            mlp2: list of int32 -- output size for MLP on each region
            group_all: bool -- group all points into one PC if set true, OVERRIDE
                npoint, radius and nsample settings
            use_xyz: bool, if True concat XYZ with local point features, otherwise just use point features
            use_nchw: bool, if True, use NCHW data format for conv2d, which is usually faster than NHWC format
        Return:
            new_xyz: (batch_size, npoint, 3) TF tensor
            new_points: (batch_size, npoint, mlp[-1] or mlp2[-1]) TF tensor
            idx: (batch_size, npoint, nsample) int32 -- indices for local regions
    '''
    data_format = 'NCHW' if use_nchw else 'NHWC'
    with tf.variable_scope(scope) as sc:
        # Sample and Grouping
        if group_all:
            nsample = xyz.get_shape()[1].value
            new_xyz, new_points, idx, grouped_xyz = sample_and_group_all(xyz, points, use_xyz)
        else:
            new_xyz, new_points, idx, grouped_xyz = sample_and_group(npoint, radius, nsample, xyz, points, knn, use_xyz)
        # Point Feature Embedding
        if use_nchw: new_points = tf.transpose(new_points, [0,3,1,2])
        for i, num_out_channel in enumerate(mlp):
            new_points = tf_util.conv2d(new_points, num_out_channel, [1,1],
                                        padding='VALID', stride=[1,1],
                                        bn=bn, is_training=is_training,
                                        scope='conv%d'%(i), bn_decay=bn_decay,
                                        data_format=data_format) 
        if use_nchw: new_points = tf.transpose(new_points, [0,2,3,1])
        # Pooling in Local Regions
        if pooling=='max':
            new_points = tf.reduce_max(new_points, axis=[2], keep_dims=True, name='maxpool')
        elif pooling=='avg':
            new_points = tf.reduce_mean(new_points, axis=[2], keep_dims=True, name='avgpool')
        elif pooling=='weighted_avg':
            with tf.variable_scope('weighted_avg'):
                dists = tf.norm(grouped_xyz,axis=-1,ord=2,keep_dims=True)
                exp_dists = tf.exp(-dists * 5)
                weights = exp_dists/tf.reduce_sum(exp_dists,axis=2,keep_dims=True) # (batch_size, npoint, nsample, 1)
                new_points *= weights # (batch_size, npoint, nsample, mlp[-1])
                new_points = tf.reduce_sum(new_points, axis=2, keep_dims=True)
        elif pooling=='max_and_avg':
            max_points = tf.reduce_max(new_points, axis=[2], keep_dims=True, name='maxpool')
            avg_points = tf.reduce_mean(new_points, axis=[2], keep_dims=True, name='avgpool')
            new_points = tf.concat([avg_points, max_points], axis=-1)
        # [Optional] Further Processing 
        if mlp2 is not None:
            if use_nchw: new_points = tf.transpose(new_points, [0,3,1,2])
            for i, num_out_channel in enumerate(mlp2):
                new_points = tf_util.conv2d(new_points, num_out_channel, [1,1],
                                            padding='VALID', stride=[1,1],
                                            bn=bn, is_training=is_training,
                                            scope='conv_post_%d'%(i), bn_decay=bn_decay,
                                            data_format=data_format) 
            if use_nchw: new_points = tf.transpose(new_points, [0,2,3,1])
        new_points = tf.squeeze(new_points, [2]) # (batch_size, npoints, mlp2[-1])
        return new_xyz, new_points, idx

2.1.1 sampling layer

使用FPS(最远点采样)对点集进行降采样,将输入点集从规模 N1 降到更小的规模 N2 。FPS可以理解成是使得采样的各个点之间尽可能远,这种采样的好处是可以降采样结果会比较均匀。

FPS实现方式如下:随机选择一个点作为初始点作为已选择采样点,计算未选择采样点集中每个点与已选择采样点集之间的距离distance,将距离最大的那个点加入已选择采样点集,然后更新distance,一直循环迭代下去,直至获得了目标数量的采样点。

class FarthestSampler:
    def __init__(self):
        pass
    def _calc_distances(self, p0, points):
        return ((p0 - points) ** 2).sum(axis=1)
    def __call__(self, pts, k):
        farthest_pts = np.zeros((k, 3), dtype=np.float32)
        farthest_pts[0] = pts[np.random.randint(len(pts))]
        distances = self._calc_distances(farthest_pts[0], pts)
        for i in range(1, k):
            farthest_pts[i] = pts[np.argmax(distances)]
            distances = np.minimum(
                distances, self._calc_distances(farthest_pts[i], pts))
        return farthest_pts

输入规模为 B∗N∗(d+C) ,其中 B 表示batch size, N 表示点集中点的数量, d 表示点的坐标维度, C 表示点的其他特征(比如法向量等)维度。一般 d=3 , c=0

输出规模为 B∗N1∗(d+C) , N1<N ,因为这是一个降采样过程。

sampling和grouping具体实现是写在一个函数里的:

def sample_and_group(npoint, radius, nsample, xyz, points, knn=False, use_xyz=True):
    '''
    Input:
        npoint: int32
        radius: float32
        nsample: int32
        xyz: (batch_size, ndataset, 3) TF tensor
        points: (batch_size, ndataset, channel) TF tensor, if None will just use xyz as points
        knn: bool, if True use kNN instead of radius search
        use_xyz: bool, if True concat XYZ with local point features, otherwise just use point features
    Output:
        new_xyz: (batch_size, npoint, 3) TF tensor
        new_points: (batch_size, npoint, nsample, 3+channel) TF tensor
        idx: (batch_size, npoint, nsample) TF tensor, indices of local points as in ndataset points
        grouped_xyz: (batch_size, npoint, nsample, 3) TF tensor, normalized point XYZs
            (subtracted by seed point XYZ) in local regions
    '''
    new_xyz = gather_point(xyz, farthest_point_sample(npoint, xyz)) # (batch_size, npoint, 3)
    if knn:
        _,idx = knn_point(nsample, xyz, new_xyz)
    else:
        idx, pts_cnt = query_ball_point(radius, nsample, xyz, new_xyz)
    grouped_xyz = group_point(xyz, idx) # (batch_size, npoint, nsample, 3)
    grouped_xyz -= tf.tile(tf.expand_dims(new_xyz, 2), [1,1,nsample,1]) # translation normalization
    if points is not None:
        grouped_points = group_point(points, idx) # (batch_size, npoint, nsample, channel)
        if use_xyz:
            new_points = tf.concat([grouped_xyz, grouped_points], axis=-1) # (batch_size, npoint, nample, 3+channel)
        else:
            new_points = grouped_points
    else:
        new_points = grouped_xyz
    return new_xyz, new_points, idx, grouped_xyz

其中sampling对应的部分是:

new_xyz = gather_point(xyz, farthest_point_sample(npoint, xyz)) # (batch_size, npoint, 3)

xyz既是 B∗N∗3 的点云,npoint是降采样点的规模。注意:PointNet++的FPS均是在坐标空间做的,而不是在特征空间做的。这一点很关键,因为FPS本身是不可微的,无法计算梯度反向传播。

本着刨根问题的心态,我们来看看farthest_point_sample和gather_point究竟在做什么

farthest_point_sample输入输出非常明晰,输出的是降采样点在inp中的索引,因此是 B∗N1 int32类型的张量

def farthest_point_sample(npoint,inp):
    '''
input:
    int32
    batch_size * ndataset * 3   float32
returns:
    batch_size * npoint         int32
    '''
    return sampling_module.farthest_point_sample(inp, npoint)

gather_point的作用就是将上面输出的索引,转化成真正的点云

def gather_point(inp,idx):
    '''
input:
    batch_size * ndataset * 3   float32
    batch_size * npoints        int32
returns:
    batch_size * npoints * 3    float32
    '''
    return sampling_module.gather_point(inp,idx)

2.1.2 grouping layer

上一步sampling的过程是将 N∗(d+C) 降到 N1∗(d+C) (这里论述方便先不考虑batch,就考虑单个点云),实际上可以理解成是在 N 个点中选取 N1 个中心点(key point)。

那么这一步grouping的目的就是以这每个key point为中心,找其固定规模(令规模为 K)的邻点,共同组成一个局部邻域(patch)。也就是会生成 N1 个局部邻域,输出规模为 N1∗K∗(d+C)

if knn:
    _,idx = knn_point(nsample, xyz, new_xyz)
else:
    idx, pts_cnt = query_ball_point(radius, nsample, xyz, new_xyz)
    grouped_xyz = group_point(xyz, idx) # (batch_size, npoint, nsample, 3)

1)找邻域的过程也是在坐标空间进行(也就是以上代码输入输出维度都是 d ,没有 C  C 是在后面的代码拼接上的),而不是特征空间。

2)找邻域这里有两种方式:KNN和query ball point.

其中前者KNN就是大家耳熟能详的K近邻,找K个坐标空间最近的点。
后者query ball point就是划定某一半径,找在该半径球内的点作为邻点。

还有个问题:query ball point如何保证对于每个局部邻域,采样点的数量都是一样的呢?
事实上,如果query ball的点数量大于规模 K ,那么直接取前 K 个作为局部邻域;如果小于,那么直接对某个点重采样,凑够规模 K

KNN和query ball的区别:(摘自原文)Compared with kNN, ball query’s local neighborhood guarantees a fixed region scale thus making local region feature more generalizable across space, which is preferred for tasks requiring local pattern recognition (e.g. semantic point labeling).也就是query ball更加适合于应用在局部/细节识别的应用上,比如局部分割。

补充材料中也有实验来对比KNN和query ball:

sample_and_group代码的剩余部分:

sample和group操作都是在坐标空间进行的,因此如果还有特征空间信息(即point-wise feature),可以在这里将其与坐标空间拼接,组成新的point-wise feature,准备送入后面的unit point进行特征学习。

if points is not None:
    grouped_points = group_point(points, idx) # (batch_size, npoint, nsample, channel)
    if use_xyz:
        new_points = tf.concat([grouped_xyz, grouped_points], axis=-1) # (batch_size, npoint, nample, 3+channel)
    else:
        new_points = grouped_points
else:
    new_points = grouped_xyz

2.1.3 PointNet layer

使用PointNet对以上结果表征

输入 B∗N∗K∗(d+C) ,输出 B∗N∗(d+C1)

以下代码主要分成3个部分:

1)point feature embedding

这里输入是 B∗N∗K∗(d+C) ,可以类比成是batch size为 B ,宽高为 N∗K ,通道数为 d+C 的图像,这样一类比,这里的卷积就好理解多了实际上就是 1∗1 卷积,不改变feature map大小,只改变通道数,将通道数升高,实现所谓“embedding”

这部分输出是 B∗N∗K∗C1

2)pooling in local regions

pooling,只是是对每个局部邻域pooling,输出是 B∗N∗1∗C1

3)further processing

再对池化后的结果做MLP,也是简单的 1∗1 卷积。这一部分在实际实验中PointNet++并没有设置去做

# Point Feature Embedding
if use_nchw: new_points = tf.transpose(new_points, [0,3,1,2])
for i, num_out_channel in enumerate(mlp):
    new_points = tf_util.conv2d(new_points, num_out_channel, [1,1],
                                padding='VALID', stride=[1,1],
                                bn=bn, is_training=is_training,
                                scope='conv%d'%(i), bn_decay=bn_decay,
                                data_format=data_format) 
if use_nchw: new_points = tf.transpose(new_points, [0,2,3,1])

# Pooling in Local Regions
if pooling=='max':
    new_points = tf.reduce_max(new_points, axis=[2], keep_dims=True, name='maxpool')
elif pooling=='avg':
    new_points = tf.reduce_mean(new_points, axis=[2], keep_dims=True, name='avgpool')
elif pooling=='weighted_avg':
    with tf.variable_scope('weighted_avg'):
        dists = tf.norm(grouped_xyz,axis=-1,ord=2,keep_dims=True)
        exp_dists = tf.exp(-dists * 5)
        weights = exp_dists/tf.reduce_sum(exp_dists,axis=2,keep_dims=True) # (batch_size, npoint, nsample, 1)
        new_points *= weights # (batch_size, npoint, nsample, mlp[-1])
        new_points = tf.reduce_sum(new_points, axis=2, keep_dims=True)
elif pooling=='max_and_avg':
    max_points = tf.reduce_max(new_points, axis=[2], keep_dims=True, name='maxpool')
    avg_points = tf.reduce_mean(new_points, axis=[2], keep_dims=True, name='avgpool')
    new_points = tf.concat([avg_points, max_points], axis=-1)

# [Optional] Further Processing 
if mlp2 is not None:
    if use_nchw: new_points = tf.transpose(new_points, [0,3,1,2])
    for i, num_out_channel in enumerate(mlp2):
        new_points = tf_util.conv2d(new_points, num_out_channel, [1,1],
                                    padding='VALID', stride=[1,1],
                                    bn=bn, is_training=is_training,
                                    scope='conv_post_%d'%(i), bn_decay=bn_decay,
                                    data_format=data_format) 
    if use_nchw: new_points = tf.transpose(new_points, [0,2,3,1])

2.1.4 Encoder还有一个问题

pointnet++实际上就是对局部邻域表征。

那就不得不面对一个挑战:non-uniform sampling density(点云的密度不均匀),也就是在稀疏点云局部邻域训练可能不能很好挖掘点云的局部结构

PointNet++做法:learn to combine features from regions of different scales when the input sampling density changes.

因此文章提出了两个方案:

一、Multi-scale grouping(MSG)

对当前层的每个中心点,取不同radius的query ball,可以得到多个不同大小的同心球,也就是得到了多个相同中心但规模不同的局部邻域,分别对这些局部邻域表征,并将所有表征拼接。如上图所示。

该方法比较麻烦,运算较多。

代码层面其实就是加了个遍历radius_list的循环,分别处理,并最后concat

new_xyz = gather_point(xyz, farthest_point_sample(npoint, xyz))
new_points_list = []
for i in range(len(radius_list)):
    radius = radius_list[i]
    nsample = nsample_list[i]
    idx, pts_cnt = query_ball_point(radius, nsample, xyz, new_xyz)
    grouped_xyz = group_point(xyz, idx)
    grouped_xyz -= tf.tile(tf.expand_dims(new_xyz, 2), [1,1,nsample,1])
    if points is not None:
        grouped_points = group_point(points, idx)
        if use_xyz:
            grouped_points = tf.concat([grouped_points, grouped_xyz], axis=-1)
    else:
        grouped_points = grouped_xyz
    if use_nchw: grouped_points = tf.transpose(grouped_points, [0,3,1,2])
    for j,num_out_channel in enumerate(mlp_list[i]):
        grouped_points = tf_util.conv2d(grouped_points, num_out_channel, [1,1],
                                        padding='VALID', stride=[1,1], bn=bn, is_training=is_training,
                                        scope='conv%d_%d'%(i,j), bn_decay=bn_decay)
    if use_nchw: grouped_points = tf.transpose(grouped_points, [0,2,3,1])
    new_points = tf.reduce_max(grouped_points, axis=[2])
    new_points_list.append(new_points)
new_points_concat = tf.concat(new_points_list, axis=-1)

二、Multi-resolution grouping(MRG)

(摘自原文)features of a region at some level Li is a concatenation of two vectors.

One vector (left in figure) is obtained by summarizing the features at each subregion from the lower level Li−1 using the set abstraction level.

The other vector (right) is the feature that is obtained by directly processing all raw points in the local region using a single PointNet.

简单来说,就是当前set abstraction的局部邻域表征由两部分构成:

左边表征:对上一层set abstraction(还记得上一层的点规模是更大的吗?)各个局部邻域(或者说中心点)的特征进行聚合。 右边表征:使用一个单一的PointNet直接在局部邻域处理原始点云

2.2 decoder:

2.2.1 分类任务的decoder

比较简单,将encoder降采样得到的global feature送入几层全连接网络,最后通过一个softmax分类。

2.2.2 分割任务的decoder

经过前半部分的encoder,我们得到的是global feature,或者是极少数点的表征(其实也就是global feature)

而如果做分割,我们需要的是point-wise feature,这可怎么办呢?

PointNet处理思路很简单,直接把global feature复制并与之前的local feature拼接,使得这个新point-wise feature能够获得一定程度的“邻域”信息。这种简单粗暴的方法显然并不能得到很discriminative的表征

别急,PointNet++来了。

PointNet++设计了一种反向插值的方法来实现上采样的decoder结构,通过反向插值和skip connection来获得discriminative point-wise feature:

设红色矩形点集 P1 : N1∗C ,蓝色矩形点集 P2 : N2∗C2 ,因为decoder是上采样过程,因此 N2>N1

一、反向插值具体做法:

对于 P2 中的每个点 x ,找在原始点云坐标空间下, P1 中与其最接近的 k 个点 x1,…,xk

当前我们想通过反向插值的方式用较少的点把更多的点的特征插出来,实现上采样

此时 x1,…,xk 的特征我们是知道的,我们想得到 x 的特征。如上公式,实际上就是将 x1,…,xk 的特征加权求和,得到x的特征。其中这个权重是与x和 x1,…,xk 的距离成反向相关的,意思就是距离越远的点,对x特征的贡献程度越小。P2 中其他点以此类推,从而实现了特征的上采样回传

skip connection具体做法:

回传得到的point-wise feature是从decoder的上一层得到的,因此算是global级别的信息,这对于想得到discriminative还是不够,因为我们还缺少local级别的信息!!!

如上图就是我们反向插值只得到了 C2 ,但是我们还需要提供local级别信息的 C1 特征!!!

这时skip connection来了!!!

skip connection其实就是将之前encoder对应层的表征直接拼接了过来

因为上图中encoder蓝色矩形点集的 C1 表征是来自于规模更大的绿色矩形点集的表征,这在一定程度上其实是实现了local级别的信息

我们通过反向插值和skip connection在decoder中逐级上采样得到local + global point-wise feature,得到了discriminative feature,应用于分割任务。

2.3 loss

无论是分类还是分割应用,本质上都是分类问题,因此loss就是分类任务中常用的交叉熵loss

2.4 其他的问题

Q:PointNet++梯度是如何回传的???

A:PointNet++ fps实际上并没有参与梯度计算和反向传播。

可以理解成是PointNet++将点云进行不同规模的fps降采样,事先将这些数据准备好,再送到网络中去训练的

3 dataset and experiments

3.1 dataset

  • MNIST: Images of handwritten digits with 60k training and 10k testing samples.(用于分类)
  • ModelNet40: CAD models of 40 categories (mostly man-made). We use the official split with 9,843 shapes for training and 2,468 for testing. (用于分类)
  • SHREC15: 1200 shapes from 50 categories. Each category contains 24 shapes which are mostly organic ones with various poses such as horses, cats, etc. We use five fold cross validation to acquire classification accuracy on this dataset. (用于分类)
  • ScanNet: 1513 scanned and reconstructed indoor scenes. We follow the experiment setting in [5] and use 1201 scenes for training, 312 scenes for test. (用于分割)

3.2 experiments

主要关心的实验结果是2个:

  1. ModelNet40分类结果
  2. ShapeNet Part分割结果
PointNet++也做了ShapeNet part数据集上的part segmentation:

4、 conclusion

PointNet++是PointNet的续作,在一定程度上弥补了PointNet的一些缺陷,表征网络基本和PN类似,还是MLP、 1∗1 卷积、pooling那一套,核心创新点在于设计了局部邻域的采样表征方法和这种多层次的encoder-decoder结合的网络结构。

第一次看到PointNet++网络结构,觉得设计得非常精妙,特别是设计了上采样和下采样的具体实现方法,并以此用于分割任务的表征,觉得设计得太漂亮了。但其实无论是分类还是分割任务,提升幅度较PointNet也就是1-2个点而已。

PointNet++,特别是其前半部分encoder,提供了非常好的表征网络,后面很多点云处理应用的论文都会使用到PointNet++作为它们的表征器。

pointnet–基于点云的分类和分割深度学习算法

论文:https://arxiv.org/abs/1612.00593(cvpr2017)

code:https://github.com/charlesq34/pointnet

基础知识:

1、什么是点云?

简单来说就是一堆三维点的集合,必须包括各个点的三维坐标信息,其他信息比如各个点的法向量、颜色等均是可选。点云的文件格式可以有很多种,包括xyz,npy,ply,obj,off等(有些是mesh不过问题不大,因为mesh可以通过泊松采样等方式转化成点云)。对于单个点云,如果你使用np.loadtxt得到的实际上就是一个维度为 (num_points,num_channels) 的张量,num_channels一般为3,表示点云的三维坐标。

这里以horse.xyz文件为例,实际就是文本文件,打开后数据长这样(局部,总共有2048个点):

实际就是一堆点的信息,这里只有三维坐标,将其可视化出来长这样:

2、点云处理任务是重要的

三维图形具有多种表现形式,包括了mesh、体素、点云等,甚至还有些方法使用多视图来对三维图形表征。而点云在以上各种形式的数据中算是日常生活中最能够大规模获取和使用的数据结构了,包括自动驾驶、增强现实等在内的应用需要直接或间接从点云中提取信息,点云处理也逐渐成为计算机视觉非常重要的一部分。

正文:

PointNet所作的事情就是对点云做特征学习,并将学习到的特征去做不同的应用:分类(shape-wise feature)、分割(point-wise feature)等。

PointNet之所以影响力巨大,就是因为它为点云处理提供了一个简单、高效、强大的特征提取器(encoder),几乎可以应用到点云处理的各个应用中,其地位类似于图像领域的AlexNet。

1、动机

点云或者mesh,大多数研究人员都是将其转化成3D体素或者多视图来做特征学习的,这其中的工作包括了VoxelNet, MVCNN等。这些工作都或多或少存在了一些问题。

直接对点云做特征学习也不是不可以,但有几个问题需要考虑:特征学习需要对点云中各个点的排列保持不变性、特征学习需要对rigid transformation保持不变性等。虽然有挑战,但是深度学习强大的表征能力以及其在图像领域取得的巨大成功,因此是很有必要直接在点云上进行尝试的。

2、贡献

  1. 我们设计了一个新颖的深层网络架构来处理三维中的无序点集
  2. 我们设计的网络表征可以做三维图形分类、图形的局部分割以及场景的语义分割等任务
  3. 我们提供了完备的经验和理论分析来证明PointNet的稳定和高效。
  4. 充分的消融实验,证明网络各个部分对于表征的有效性。

3、方法

3.1 点云的几个特点:

  1. 无序性 –> 对称函数设计用于表征
  2. 点不是孤立的,需要考虑局部结构 –> 局部全局特征结合
  3. 仿射变换无关性 –> alignment network

(重要)关于第三点:相同的点云在空间中经过一定的刚性变化(旋转或平移),坐标发生变化。其实对于点云分类or分割任务来说(分割可以认为是点的分类),例如,整体的旋转和平移不应修改全局点云类别和每个点的类别,也不应修改点的分割因此需要保证仿射变换无关性(简单来说,“仿射变换”就是:“线性变换”+“平移”),但是对于位置 敏感的 任务:点云配准、点云补全任务,对于位置敏感,就不需要保证 仿射变换的无关性

我们希望不论点云在怎样的坐标系下呈现,网络都能正确的识别出。这个问题可以通过STN(spacial transform netw)来解决。三维不太一样的是点云是一个不规则的结构(无序,无网格),不需要重采样的过程。pointnet通过学习一个矩阵来达到对目标最有效的变换。

解决方法

  1. 空间变换网络解决旋转问题:三维的STN可以通过学习点云本身的位姿信息学习到一个最有利于网络进行分类或分割的DxD旋转矩阵(D代表特征维度,pointnet中D采用3和64)。至于其中的原理,我的理解是,通过控制最后的loss来对变换矩阵进行调整,pointnet并不关心最后真正做了什么变换,只要有利于最后的结果都可以。pointnet采用了两次STN,第一次input transform是对空间中点云进行调整,直观上理解是旋转出一个更有利于分类或分割的角度,比如把物体转到正面;第二次feature transform是对提取出的64维特征进行对齐,即在特征层面对点云进行变换。
  2. maxpooling解决无序性问题:网络对每个点进行了一定程度的特征提取之后,maxpooling可以对点云的整体提取出global feature。

3.2 网络结构:

batchnormal对于上采样任务来说效果不好

网络分成了分类网络和分割网络2个部分,大体思路类似,都是设计表征的过程分类网络设计global feature,分割网络设计point-wise feature。两者都是为了让表征尽可能discriminative,也就是同类的能分到一类,不同类的距离能拉开。

输入 n*3 n是点数

inputtransform:放射变换(为了保证仿射变换的不变性):直接预测一个变换矩阵(3*3)来处理输入点的坐标(对所有坐标进行变换)。因为会有数据增强的操作存在,这样做可以在一定程度上保证网络可以学习到变换无关性。T-Net模型,它的主要作用是学习出变化矩阵来对输入的点云或特征进行规范化处理。

MLP:

有两种实现方法:

1、输入 B,N,3 —- nn.liner层 — B,N,64

2、输入 B,3,N —- conv1d(1×1) — B,64,N

Pooling:

为了解决无序性(点云本质上是一长串点(nx3矩阵,其中n是点数)。在几何上,点的顺序不影响它在空间中对整体形状的表示,例如,相同的点云可以由两个完全不同的矩阵表示。)使用 maxpooling或sumpooling,也就是说,最后的D维特征对每一维都选取N个点中对应的最大特征值或特征值总和,这样就可以通过g来解决无序性问题。

最后再经过一个mlp(代码中运用全连接)得到k个score。分类网络最后接的loss是softmax。

分割网络:

将池化后的特征和前一阶段特征拼接,池化后的特征有全局信息,在和之前的拼接,以此得到同时对局部信息和全局信息感知的point-wise特征,提升表征效果。然后最后输出n*m, m为类别数量,表示每个点的类别信息。

损失函数:

分类中常用的交叉熵+alignment network中用于约束生成的alignment matrix的loss

dataset and experiments

evaluate metric

分类:分类准确率acc
分割:mIoU

dataset

分类:ModelNet40
分割:ShapeNet Part dataset和Stanford 3D semantic parsing dataset

experiments

1、分类:

2、局部分割:

code:

1. 如何对点云使用MLP?
2. alignment network怎么做的?
3. 对称函数如何实现来提取global feature的?
4. loss?

def get_model(point_cloud, is_training, bn_decay=None):
    """ Classification PointNet, input is BxNx3, output Bx40 """
    batch_size = point_cloud.get_shape()[0].value
    num_point = point_cloud.get_shape()[1].value
    end_points = {}
    with tf.variable_scope('transform_net1') as sc:
        transform = input_transform_net(point_cloud, is_training, bn_decay, K=3)
    point_cloud_transformed = tf.matmul(point_cloud, transform)
    input_image = tf.expand_dims(point_cloud_transformed, -1)
    net = tf_util.conv2d(input_image, 64, [1,3],
                         padding='VALID', stride=[1,1],
                         bn=True, is_training=is_training,
                         scope='conv1', bn_decay=bn_decay)
    net = tf_util.conv2d(net, 64, [1,1],
                         padding='VALID', stride=[1,1],
                         bn=True, is_training=is_training,
                         scope='conv2', bn_decay=bn_decay)
    with tf.variable_scope('transform_net2') as sc:
        transform = feature_transform_net(net, is_training, bn_decay, K=64)
    end_points['transform'] = transform
    net_transformed = tf.matmul(tf.squeeze(net, axis=[2]), transform)
    net_transformed = tf.expand_dims(net_transformed, [2])
    net = tf_util.conv2d(net_transformed, 64, [1,1],
                         padding='VALID', stride=[1,1],
                         bn=True, is_training=is_training,
                         scope='conv3', bn_decay=bn_decay)
    net = tf_util.conv2d(net, 128, [1,1],
                         padding='VALID', stride=[1,1],
                         bn=True, is_training=is_training,
                         scope='conv4', bn_decay=bn_decay)
    net = tf_util.conv2d(net, 1024, [1,1],
                         padding='VALID', stride=[1,1],
                         bn=True, is_training=is_training,
                         scope='conv5', bn_decay=bn_decay)
    # Symmetric function: max pooling
    net = tf_util.max_pool2d(net, [num_point,1],
                             padding='VALID', scope='maxpool')
    net = tf.reshape(net, [batch_size, -1])
    net = tf_util.fully_connected(net, 512, bn=True, is_training=is_training,
                                  scope='fc1', bn_decay=bn_decay)
    net = tf_util.dropout(net, keep_prob=0.7, is_training=is_training,
                          scope='dp1')
    net = tf_util.fully_connected(net, 256, bn=True, is_training=is_training,
                                  scope='fc2', bn_decay=bn_decay)
    net = tf_util.dropout(net, keep_prob=0.7, is_training=is_training,
                          scope='dp2')
    net = tf_util.fully_connected(net, 40, activation_fn=None, scope='fc3')
    return net, end_points

MLP的核心做法:

input_image = tf.expand_dims(point_cloud_transformed, -1)
net = tf_util.conv2d(input_image, 64, [1,3],
                         padding='VALID', stride=[1,1],
                         bn=True, is_training=is_training,
                         scope='conv1', bn_decay=bn_decay)
net = tf_util.conv2d(net, 64, [1,1],
                     padding='VALID', stride=[1,1],
                     bn=True, is_training=is_training,
                     scope='conv2', bn_decay=bn_decay)

这里input_image维度是 B×N×3×1 ,因此将点云看成是W和H分为N和3的2D图像,维度是 1

然后直接基于这个“2D图像”做卷积,第一个卷积核size是 [1,3] ,正好对应的就是“2D图像”的一行,也就是一个点(三维坐标),输出通道数是64,因此输出张量维度应该是 B×N×1×64

第二个卷积核size是 [1,1] , 1∗1 卷积只改变通道数,输出张量维度是 B×N×1×64

conv2d就是将卷积封装了一下,核心部分也就是调用tf.nn.conv2d,实现如下:

def conv2d(inputs,
           num_output_channels,
           kernel_size,
           scope,
           stride=[1, 1],
           padding='SAME',
           use_xavier=True,
           stddev=1e-3,
           weight_decay=0.0,
           activation_fn=tf.nn.relu,
           bn=False,
           bn_decay=None,
           is_training=None):
  """ 2D convolution with non-linear operation.
  Args:
    inputs: 4-D tensor variable BxHxWxC
    num_output_channels: int
    kernel_size: a list of 2 ints
    scope: string
    stride: a list of 2 ints
    padding: 'SAME' or 'VALID'
    use_xavier: bool, use xavier_initializer if true
    stddev: float, stddev for truncated_normal init
    weight_decay: float
    activation_fn: function
    bn: bool, whether to use batch norm
    bn_decay: float or float tensor variable in [0,1]
    is_training: bool Tensor variable
  Returns:
    Variable tensor
  """
  with tf.variable_scope(scope) as sc:
      kernel_h, kernel_w = kernel_size
      num_in_channels = inputs.get_shape()[-1].value
      kernel_shape = [kernel_h, kernel_w,
                      num_in_channels, num_output_channels]
      kernel = _variable_with_weight_decay('weights',
                                           shape=kernel_shape,
                                           use_xavier=use_xavier,
                                           stddev=stddev,
                                           wd=weight_decay)
      stride_h, stride_w = stride
      outputs = tf.nn.conv2d(inputs, kernel,
                             [1, stride_h, stride_w, 1],
                             padding=padding)
      biases = _variable_on_cpu('biases', [num_output_channels],
                                tf.constant_initializer(0.0))
      outputs = tf.nn.bias_add(outputs, biases)
      if bn:
        outputs = batch_norm_for_conv2d(outputs, is_training,
                                        bn_decay=bn_decay, scope='bn')
      if activation_fn is not None:
        outputs = activation_fn(outputs)
      return outputs

alignment network :

input_transform_net为例:

def input_transform_net(point_cloud, is_training, bn_decay=None, K=3):
    """ Input (XYZ) Transform Net, input is BxNx3 gray image
        Return:
            Transformation matrix of size 3xK """
    batch_size = point_cloud.get_shape()[0].value
    num_point = point_cloud.get_shape()[1].value
    input_image = tf.expand_dims(point_cloud, -1)
    net = tf_util.conv2d(input_image, 64, [1,3],
                         padding='VALID', stride=[1,1],
                         bn=True, is_training=is_training,
                         scope='tconv1', bn_decay=bn_decay)
    net = tf_util.conv2d(net, 128, [1,1],
                         padding='VALID', stride=[1,1],
                         bn=True, is_training=is_training,
                         scope='tconv2', bn_decay=bn_decay)
    net = tf_util.conv2d(net, 1024, [1,1],
                         padding='VALID', stride=[1,1],
                         bn=True, is_training=is_training,
                         scope='tconv3', bn_decay=bn_decay)
    net = tf_util.max_pool2d(net, [num_point,1],
                             padding='VALID', scope='tmaxpool')
    net = tf.reshape(net, [batch_size, -1])
    net = tf_util.fully_connected(net, 512, bn=True, is_training=is_training,
                                  scope='tfc1', bn_decay=bn_decay)
    net = tf_util.fully_connected(net, 256, bn=True, is_training=is_training,
                                  scope='tfc2', bn_decay=bn_decay)
    with tf.variable_scope('transform_XYZ') as sc:
        assert(K==3)
        weights = tf.get_variable('weights', [256, 3*K],
                                  initializer=tf.constant_initializer(0.0),
                                  dtype=tf.float32)
        biases = tf.get_variable('biases', [3*K],
                                 initializer=tf.constant_initializer(0.0),
                                 dtype=tf.float32)
        biases += tf.constant([1,0,0,0,1,0,0,0,1], dtype=tf.float32)
        transform = tf.matmul(net, weights)
        transform = tf.nn.bias_add(transform, biases)
    transform = tf.reshape(transform, [batch_size, 3, K])
    return transform

实际上,前半部分就是通过卷积和max_pooling对batch内各个点云提取global feature,再将global feature降到 3×K 维度,并reshape成 3×3 ,得到transform matrix

通过数据增强丰富训练数据集,网络确实应该学习到有效的transform matrix,用来实现transformation invariance

loss

监督分类任务中常用的交叉熵loss + alignment network中的mat_diff_loss

 对于特征空间的alignment network,由于特征空间维度比较高,因此直接生成的alignment matrix会维度特别大,不好优化,因此这里需要加个loss约束一下。

总结:

PointNet之所以影响力巨大,并不仅仅是因为它是第一篇,更重要的是它的网络很简洁(简洁中蕴含了大量的工作来探寻出简洁这条路)却非常的work,这也就使得它能够成为一个工具,一个为点云表征的encoder工具,应用到更广阔的点云处理任务中。

MLP+max pooling竟然就击败了众多SOTA,令人惊讶。另外PointNet在众多细节设计也都进行了理论分析和消融实验验证,保证了严谨性,这也为PointNet后面能够大规模被应用提供了支持。