pytorch3D的可微分渲染

传统mesh渲染过程的不可微分性

传统的mesh渲染算法,计算某个像素值,是根据通过像素的光线与mesh模型相交的face确定的。而这个过程是离散的过程,如下图所示。在z方向,face沿着z方向的移动会造成像素值跳变;在xy方向,face沿着xy方向移动也会造成像素值跳变。因此像素值与face的空间位置之间的函数是一个不可微的函数,也就无法通过像素值的差异来反向传播更新face的空间位置。pytorch3d采用了一种可微分渲染的方式,实现了像素值和face之间的可微分的函数。

pytorch3d Renderer

pytorch3d实现了对于mesh可微分的渲染过程:pytorch3d中的渲染器Renderer包含两个主要的部分,rasterizer和shader。rasterizer负责将mesh模型进行光栅化,作用是将mesh的face对应到2d图像像素;shader负责对像素进行着色。pytorch3d的可微分渲染主要参照论文《Soft Rasterizer: Differentiable Rendering for Unsupervised Single-View Mesh Reconstruction》,并进行了改动以提升计算效率。

Soft Rasterizer

传统的rasterizer将mesh模型映射到2d图像像素,是一个离散的过程。由通过像素的光线与mesh模型相交的face确定。soft rasterizer的假设是,对一个像素来说,每个face对该像素都有某种程度影响,将所有face对该像素的影响进行一种综合,就确定了该像素最终的结果。

计算face对像素的影响程度

对于第i个像素,第j个face对其的影响程度D定义为

其中,d(i, j)为像素i到face j的边缘的最小距离。

δ为符号系数,表示像素在face内部还是外部

σ为超参数,控制距离对于D的影响大小。

sigmoid函数将这个值限制到0-1之间,并且像素在face内部时,D大于0.5,在face外部时小于0.5。​

从这个定义来看,D关于face的位置是可微的。

聚合函数

Soft rasterizer论文是要计算mesh的silhouette,相当于在图像平面的影子或者mask。​

对于第i个像素,将所有face对它的影响用一种方式聚合,作为该像素最终的结果。soft rasterizer定义的聚合函数如下:

其中,j表示第j个face,N为face的总数。这个定义,使得只要有1个face对该像素的影响是1,那该像素最终的结果就是1。当所有的face对该像素的影响都是0时,该像素的结果才为0。​

这个聚合函数也是一个可微的函数。因此,最终像素的结果关于face的位置是一个可微的函数。

pytorch3d的改动

pytorch3d对上面的过程进行了一些修改,以符合计算效率和模块化的要求。

pytorch3d中的rasterizer

首先,soft rasterizer中,一个像素的结果是由所有的face来计算确定的,而这存在很大的计算浪费,因为大多数face和该像素是相隔很远的,影响程度为0。所以pytorch3d是选择沿着z轴最近的k个face来计算。​

另外,soft rasterizer将rasterization和shading写到一个cuda核函数中,pytorch3d将它们进行了解耦。rasterizer返回每个像素的k个最近的face的信息,包括face索引,像素在face中的重心坐标,以及像素到face分别在z方向和xy平面方向的距离(带符号)。shader利用这些信息进行着色。这样用户可以自定义shader。

pytorch3d中的shader

shader一般就是根据像素对应的face采用某种方式进行融合。pytorch3d中两种shader算法如下,

算法1计算silhouette,也就是soft rasterizer论文中的聚合函数,dists就是soft rasterizer论文中计算的D。

算法2计算rgb值,这也是RoMe中使用的shader。

torch.utils.checkpoint模块

当训练模型,出现了显存不足的问题时,可以利用torch.utils.checkpoint模块下的checkpoint函数来减少训练时的显存占用,但是代价是训练过程会变慢,本质上是用速度换取内存。官方文档地址:torch.utils.checkpoint — PyTorch 2.7 documentation

不使用checkpoint时,前向传播的过程中会保存中间的activation,用于后续反向传播时计算梯度。但使用checkpoint,会只保存输入tuple和模型参数,不保存中间的activation,在反向传播时,使用保存的输入重新计算中间的activation,相当于重新跑了一遍正常的前向传播。反向传播完成后,把activation释放。

checkpoint的正确使用方式应该是将网络分为若干的模块,对每个模块应用checkpoint,这样反向传播时,依从后往前的顺序对每个模块计算梯度,但只有在计算到该模块时,该模块才有activation的内存占用,计算完成或没计算到时就没有。checkpoint不应该对网络整体使用,否则就和正常的训练过程一样占用同样的显存。

注意dropout和batch normalization层不能用checkpoint

解决vscode首次远程连接打开很慢

vscode首次连接某个主机或者docker容器时,经常会在卡在远程主机或容器安装vscode server这里(其实一直等是可以安装好的,只是太太太慢了),首先打开 VScode 菜单中的 About(关于)菜单项,查看并记下 Commit 提交的 ID 编码

然后在本地下载这个链接:

https://update.code.visualstudio.com/commit:复制的哈希值/server-linux-x64/stable

(建议用axel命令下载)

登录服务器,创建相应文件夹:

mkdir -p ~/.vscode-server/bin/复制的哈希值

然后将我们下载的安装包上传到服务器后,执行如下命令将其解压到我们创建的文件夹中:

tar zxvf vscode-server-linux-x64.tar.gz -C ~/.vscode-server/bin/复制的哈希值 --strip 1

最后执行如下命令,这样 vscode server 就已经全部安装完毕了:

touch ~/.vscode-server/bin/复制的哈希值/0

然后就可以使用vscode远程连接了

petr学习

petr和其它基于transformer目标检测模型的架构对比

petr模型的架构与detr模型基本一致,整体简洁优雅,避免了detr3d的一些较为复杂的流程。

petr模型首先是使用如resnet、vovnet等backbone提取图像2d特征,然后与生成的3d坐标一起进入encoder,encoder的结果与object query一起进入decoder,decoder的输出经过分类和回归的head得到bbox。

3d 坐标的生成:将图像坐标系空间,分成尺寸为(Wf, Hf, D)的meshgrid,meshgrid的点的坐标可以表示为(u × d, v × d, d)。meshgrid经过与相机内外侧得到的矩阵相乘,投影到3d世界坐标系,将在世界坐标系下的meshgrid的点,根据检测的roi区域进行归一化。就是生成的3d坐标。

3D position encoder,将3d坐标,经过一个mlp,得到position embedding,与2d feature经过一个1×1卷积后相加,得到3d position-aware feature。

3d object query:petr是初始化了在3d坐标系下从0到1均匀分布的可学习的一组anchor points,然后anchor points的坐标经过一个mlp,得到object query。然后训练时会更新这些anchor points

transformer(attention is all you need)学习

transformer最开始是做翻译的,之前看它的论文以及网上相关介绍,对一些东西没有弄明白,看了知乎的一个介绍 Transformer模型详解(图解最完整版) – 知乎 (zhihu.com) ,然后研究了一下GitHub上的代码 GitHub – jadore801120/attention-is-all-you-need-pytorch: A PyTorch implementation of the Transformer model in “Attention is All You Need”. ,算是基本把原本transformer弄懂了。在此总结一下之前没有弄明白的几个问题。

transformer总体结构

从上面的总体架构看,其实涵盖了网络的核心部分,但是有一些地方没有详细说明,也是给我造成了一些疑问,然后研究了一下代码才搞懂。首先从输入开始说明。输入是一段话,预处理就是将每个词根据词汇表映射成一个整数来表示它,我理解就是这个词的token,然后输入到网络里的就是这一串整数。网络将这串数字,首先经过torch.nn.embedding层,得到输入的embedding。

然后就是加上position_encoding,经过encoder。图中灰色的部分就是encoder_layer。encoder是n个不同的encoder_layer组成,而不是一个encoder_layer重复n次。右边的decoder也是同理。

然后就到decoder的部分。一开始会有一个初始的token,表示句子的开始,同样也是经过torch.nn.Embedding层得到embedding,然后经过decoder(需要encoder的输出),得到最后decoder的output,然后这个output经过linear层和softmax,得到一个结果,但是这里注意,这个结果并不是最后的结果,句子已经翻译完了,而只是得到下一个token,softmax输出表示下一个token的可能性。然后取可能性最大的作为预测的下一个token,将预测的token,接在之前的所有token后面,再次重复decoder的过程,直到某一个预测的token为表示句子结束的token。最后将得到的token序列,根据词汇表映射为对应的单词,完成句子的翻译。因此,翻译结果中每个单词是逐个得到而不是一次性完成的,每一个输出的单词都是由已经得到的结果,经过一次decoder得到的。

而decoder最后经过softmax得到下一个token,并不总是直接取可能性最大的token作为结果。实际上transformer维护着k个可能性最高的token序列以及它们对应的可能性得分。将这k个序列都输入decoder ,并每个仍然记录k个可能性最高的预测token,这样得到k^2的一个概率矩阵(矩阵每个元素是预测的token可能性与输入token序列的可能性相乘)。取矩阵中k个可能性最高的结果,更新维护的序列(由这些结果对应的原序列和预测token组成)及其对应的可能性得分(这些结果对应在矩阵中的概率)。

transformer预测的时候基本就是这样,然后之前还有一点没搞懂的就是它在decoder时候的mask操作。实际上在预测的时候,mask没有发挥作用,或者说mask全部没有遮挡。是在训练的时候用到的mask。前文说了,预测的时候是输入已经得到的结果序列,然后预测下一个token。但是在训练的时候,并不是每次都只输入一个不完整的序列,然后让它预测下一个token,再与对应的目标token来计算loss。而实际上是输入开始token+整个句子,目标序列是整个句子+结束token。训练时把整个输入序列都输入到decoder中,但是预测的时候输入的是不完整的序列,这里就存在差异,所以在这时候mask就派上用场了。在decoder的masked attention那里,用一个mask矩阵(下三角部分不遮挡,上三角部分遮挡)与计算得到的attention矩阵点乘,这样使得经过masked MHA之后输出的序列Z(和输入序列长度相同),每个位置的结果都只是由输入序列该位置之前的内容计算得到的,就等同于对Z的这个位置元素来说,输入就是从开头到这个位置的不完整序列。最后decoder的输出也是同理,每个位置的结果都只从输入序列该位置之前的内容计算得到。再经过linear(linear之后长度仍然和输入的长度一致)和softmax,取每个位置softmax后的结果,与该位置对应的目标token,计算loss,实现训练。(预测的时候是取最后一个位置对应的softmax的结果作为预测,前面位置的结果是无意义的)

信息论基础学习二(条件熵、互信息)

联合熵(joint entropy)

联合熵等于由联合事件发生概率计算出的熵。

\[H(X, Y) = E_{X,Y}[-{\rm log}p(x,y)] = -\sum_{x,y}p(x,y){\rm log}p(x,y)\]

条件熵(conditional entropy)

系统有个随机变量X,Y,当给定条件Y=y时,p(X|Y=y)的熵为

\[{\rm H}(X|Y=y) = {\rm H}_{[X|Y]}[-{\rm log}P(x|y)] = -\sum_{x∈X}p(x|y){\rm log}p(x|y)\]

条件熵定义为上面的熵对Y分布的期望

\[{\rm H}(X) = {\rm E}_{Y}{\rm H}[(X|y)] = -\sum_{y∈Y}p(y)\sum_{x∈X}p(x|y){\rm log}p(x|y) = \sum_{x,y}p(x,y){\rm log}\frac{p(y)}{p(x,y)} \]

条件熵的含义为:如果知道了Y,X的熵是多少。注意与“知道Y等于一个具体的y后,X的熵”,这两种表述的区分。前者是后者在Y上的总体情况。

条件熵的另一种解读为,知道了Y之后,X的不确定性是多少。 将条件熵变形为:

\[H(X|Y) = H(X, Y) – H(Y)\]

可以理解为:“X、Y都不知道时的不确定性”减去“Y的不确定性”等于“知道Y后X的不确定性”。

互信息(mutual information)

首先了解pointwise mutual information,它的定义为:

\[{\rm pmi}(x;y) = {\rm log}_{2}\frac{p(x,y)}{p(x)p(y)} = {\rm log}_{2}\frac{p(x|y)}{p(x)} = {\rm log}_{2}\frac{p(y|x)}{p(y)} \]

含义为比较两个结果同时发生的概率与两个结果各自发生概率的乘积(即假设两个事件独立,它们同时发生的概率)。pointwise互信息具有对称性。当其为0时表示两个结果是独立的。当其大于0,表示两个结果有正向关联,即一个发生会导致另一个更容易发生。小于0时表示有负向关联,一个发生会导致另一个更不容易发生。它的上下界为:

\[-\infty ≤ {\rm pmi}(x;y) ≤ {\rm min} [-{\rm log}p(x), -{\rm log}p(y)]\]

互信息是pointwise 互信息在所有结果上的期望

\[I(X;Y) = E_{X,Y}[SI(x,y)] = \sum_{x,y}p(x,y){\rm log}\frac{p(x,y)}{p(x)p(y)}\]

从定义的形式上可以看出,互信息还是P(X, Y)和P(X)*P(Y)的KL散度

\[I(X;Y) = KL(P_{(X,Y)}||P(X)\cdot P(Y))\]

互信息本质上描述了X和Y的相关性。表示通过知道一个变量的结果,可以减少的关于另一个变量的不确定性。如果两个变量完全独立,那么互信息为0,知道一个变量对于另一变量的不确定性完全没有帮助。如果两个变量完全相关,即一个变量完全决定了另一个变量,那么互信息就是其中一个变量的熵。

互信息具有对称性;但与point mutual information不同,互信息具有非负性。

变形式

\[\begin{split}
{\rm I}(X;Y) &= {\rm H}(X) – {\rm H}(X|Y)
\\ &= {\rm H}(Y) – {\rm H}(Y|X)
\\ &= {\rm H}(X) + {\rm H}(Y) – {\rm H}(X,Y)
\\ &= {\rm H}(X,Y) – {\rm H}(X|Y) – {\rm H}(Y|X)
\end{split}\]

从前两个等式可以直观看出,互信息表示知道一个变量的结果,减少的关于另一个变量的不确定性。

信息论基础学习(一)

信息量

如果现在知道了一个随机事件的结果,那么如何衡量知道这个结果所带来的信息呢?或者说知道了这个结果,相较于不知道这个结果,所减少的不确定性是多少?shannon针对这个问题给出了事件m的信息量的定义:

$$ I(m) = log(\frac{1}{p(m)}) = -log(p(m)) $$

其中,\( p(m) \) 是m发生的概率,log以2为底的话,计算结果以shannon为单位,或者是更通用的bit为单位。 为什么定义成这种形式,因为需要信息量满足以下两个条件:1、事件结果发生概率越大,信息量越少,即事件结果的信息量与其发生概率呈负相关。2、多个事件结果同时发生的信息量,等于其中每个事件结果信息量之和。 从该定义可以得到,发生概率越低的事件,带来的信息量越大。反之则越小。这也符合常规的认知。比如知道明天的天气是晴天,那么给我们带来的信息量则不是太大,因为晴天很常见。但是如果知道明天会有龙卷风,那么则带来了很大的信息量,因为龙卷风是很罕见的天气。从另一个角度去理解,定义的一个事件的信息量表示这个事件会带来的“惊讶度”(surprise)。

表示一个随机事件所有结果的信息量的期望。或者说在只知道所有结果的概率分布时,整个系统的不确定性。 \[ {\rm H}(M) = {\rm E}[{\rm I}(M)] = \sum_{m∈M}p(m){\rm I}(m) = -\sum_{m∈M}p(m){\rm log}p(m) \]

一个随机系统的熵,在各个结果的概率相等时,是最大的。这时系统的随机性也是最大的。而如果这个随机系统只会有一个确定的结果,那么熵为0,系统是确定的没有随机性。在物理中,熵的概念是表示一个系统混乱的程度,与信息论中的熵有着相同的内涵。从另一个角度理解,熵也可以理解成系统混乱性的衡量。

KL散度

表示两个分布之间的差异程度的量化,也就是这个两个分布的“距离”。(实际上不能直接当成距离,因为不满足对称性,即\( KL(p||q) ≠ KL(q||p) \),而且不能满足三角形两边之和一定大于第三边的条件,即\( KL(p||r) + KL(r||q) \) 一定大于\( KL(p||q)) \)

\[ KL(p(X)||q(X)) = \sum_{x∈X}-p(x){\rm log}q(x) – \sum_{x∈X}-p(x){\rm log}p(x) = \sum_{x∈X}p(x){\rm log}\frac{p(x)}{q(x)} \]

当两个分布完全相同时,KL散度为0。 在深度学习中,KL散度常用来作为两个分布之间差异的衡量,对于一个确定的分布\(p\),通过KL散度衡量一个估计的分布\(q\)和\(p\)之间的差异\(KL(p||q)\)。通过减少KL散度来使\(q\)和\(p\)之间的差异。

维基百科上对KL散度的另一种解读: Another interpretation of the KL divergence is the “unnecessary surprise” introduced by a prior from the truth: suppose a number X is about to be drawn randomly from a discrete set with probability distribution \(p(x)\). If Alice knows the true distribution \(p(x)\), while Bob believes (has a prior) that the distribution is \(q(x)\), then Bob will be more surprised than Alice, on average, upon seeing the value of X. The KL divergence is the (objective) expected value of Bob’s (subjective) surprisal minus Alice’s surprisal, measured in bits if the log is in base 2. In this way, the extent to which Bob’s prior is “wrong” can be quantified in terms of how “unnecessarily surprised” it is expected to make him.

交叉熵与KL 散度

交叉熵常作为分类任务的损失函数,但是来源是什么。其实本质上,减少交叉熵损失,其实是降低预测分布与GT分布之间的KL散度,从而让预测分布与GT分布接近。

\[ \begin{split} KL(p||q) &= – \sum_{k}p_{k}{\rm log}\frac{q_{k}}{p_{k}} \\ &= -\sum_{k}p_{k}{\rm log}q_{k} – ( – \sum_{k}p_{k}{\rm log}p_{k}) \\ &= {\rm H}(p, q) – {\rm H}(p) \end{split} \]

其中\(p\)为GT的分布,\(q\)为预测分布。\({\rm H}(p, q)\)就是交叉熵, \({\rm H} (p)\)是\(p\)的熵。因为GT分布是确定的,所以 \({\rm H} (p)\) 这一项是一个定值,与求 \(q\) 是无关的。因此,减少交叉熵损失,其实就是在降低预测分布与GT分布之间的KL散度。

最小化KL散度等价于最大化似然函数

对于一个未知的\(p(x)\),可以通过\(q(x|θ)\)来近似 \(p(x)\) , \(q(x|θ)\) 由可调节的参数\(θ\)控制(比如神经网络中的权重)。通过最小化\({\rm KL}(p(x)|q(x|θ))\)来确定 \(q(x|θ)\) 。但是计算这个KL散度又要求知道 \(p(x)\) ,这成了一个循环的问题。重新审视KL散度,它也可以理解成\(log( p(x) / q(x|θ) )\)关于 \(p(x)\) 的期望,而求期望则可以通过蒙特卡洛估计的方式进行,在实际中就是采样若干的数据,用这些数据计算\(log(p(x) / q(x|θ))\)的平均值来作为其期望。写成公式为:

\[KL(p||q) ≈ \frac{1}{N}\sum_{n=1}^{N}[-{\rm ln}q(x_{n}|\theta) + {\rm ln}p(x_{n})]\]

式中右边第二项与\(θ\)无关,优化时可以直接省略。而第一项就是采样数据的负对数似然函数,同时也是训练分类网络用的交叉熵函数。因此,最小化KL散度等价于最大化似然函数。

tmux使用

新建会话:tmux new -s session-name
查看会话:tmux ls
进入会话:tmux a -t session-name
断开会话:tmux detach
关闭会话:tmux kill-session -t session-name

安装cuda 10.1及cuDNN

https://www.cnblogs.com/zongfa/p/13947071.html

我安装的显卡的驱动版本是455,使用nvidia-smi看到该驱动支持的最高cuda版本为11.1,因此可以安装cuda10.1

Ubuntu20.04自带的gcc版本为9.3,而cuda10.1不支持gcc-9,因此要手动安装gcc-7,命令如下:

sudo apt-get install gcc-7 g++-7

安装完gcc-7,系统中就存在两个版本的gcc(g++),因此要设置默认的gcc和g++,命令如下:

sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-7 9
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-9 1
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-7 9
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-9 1

显示g++优先级:

sudo update-alternatives --display g++

去官网下载cuda10.1的安装文件 cuda下载使用run文件安装(使用deb文件会自动安装显卡驱动)

cuda.png

安装界面启动,直接选择continue,然后accept。

cuda.jpg

到这一步时注意,因为我们已经有安装了驱动,所以这里使用空格选中driver,取消驱动安装。然后移动最下面install,回车进行安装。

安装好后,可发现有了/usr/local/cuda-10.1这个文件夹,也就是安装路径所在。根据安装完成的提示,需要进行相关路径配置。在~/.bashrc中进行添加即可

安装cudnn

下载cudnn。登录下载与cuda10.1对应的cudnn 7.6.5,选择cudnn library for linux。下载好后进行解压,将解压的 cuda/include/cudnn.h 复制到 /usr/local/cuda/include,cuda/lib64/下所有文件复制到 /usr/local/cuda/lib64 下,并为这些文件添加读取和执行权限:

sudo chmod 755 /usr/local/cuda/include/cudnn.h /usr/local/cuda/lib64/libcudnn*