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。

position_embedding部署时的注意点

对于position_embedding,在使用模型进行推理或者部署时,可以将只依赖position_embedding的模块进行固化,如position_embedding后面接一个全连接层进行一个映射,可以将这个全连接层的结果固化下来,因为在推理或部署时,因为position_embedding或者全连接层都不会进行参数更新了,所以结果是固定的,因此可以将结果直接固化,减少了一些的计算量。

python使用ThreadPoolExecutor、ProcessPoolExecutor进行任务并行

在Python中,可以使用concurrent.futures.ThreadPoolExecutor来实现多线程调用函数。

from concurrent.futures import ThreadPoolExecutor

def my_function(arg1, arg2):
    print(f"Processing {arg1} and {arg2}")

with ThreadPoolExecutor(max_workers=3) as executor:
    for i in range(5):
        executor.submit(my_function, i, i*2)

需要获取返回值,可通过future.result()获取


from concurrent.futures import ThreadPoolExecutor
import time

def calculate_square(number):
    time.sleep(1)  # 模拟耗时操作
    return number * number

with ThreadPoolExecutor(max_workers=3) as executor:
    # 提交任务并保存future对象
    future1 = executor.submit(calculate_square, 2)
    future2 = executor.submit(calculate_square, 3)
    future3 = executor.submit(calculate_square, 4)
    
    # 获取结果(会阻塞直到任务完成)
    print(f"2的平方: {future1.result()}")
    print(f"3的平方: {future2.result()}")
    print(f"4的平方: {future3.result()}")

使用多少线程数合理?对于cpu计算密集型,可以使用cpu核心数作为线程数。对于I/O密集型,可以使用核心数x2。使用如下代码可以获得核心数

import os
cpu_count = os.cpu_count()  # 返回逻辑核心数(含超线程)‌
print(f"CPU核心数: {cpu_count}")

但是对于cpu密集型的任务,更推荐使用多进程的ProcessPoolExecutor,接口和ThreadPoolExecutor一样。但是要说明的是,开启进程、进程间通信和销毁进程要比同样的线程步骤耗时更大,如果要执行的任务不是明显的cpu密集型,那么使用ProcessPoolExecutor并不一定要快于ThreadPoolExecutor

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

ipdb调试python代码

在不使用调试模型运行python代码时,有时程序会发生我们意想不到的错误,想要复现错误来调试需要很多时间,或者错误是偶发性的,这时候我们可以借助ipdb这个工具来在运行代码时进行调试。

安装

pip install ipdb

在可能出错的地方,插入语句。一般可以配合try except语句一起使用,在捕获到错误的地方插入语句,程序在运行到这个地方时会进入到断点,然后在命令行里可以交互式调试代码

import ipdb; ipdb.set_trace()

常用调试命令

  • c:继续执行代码,直到遇到下一个断点或程序结束。
  • n:单步执行下一行代码(不会进入函数内部)。
  • s:单步进入下一行代码(如果有函数调用,则进入函数内部)。
  • q:退出调试器并终止程序的执行。
  • l:查看当前位置附近的代码。
  • p:打印变量的值,例如p variable_name
  • h:查看帮助信息,例如h command_name
  • w:查看当前的调用栈。
  • u:向上移动一层调用栈。
  • d:向下移动一层调用栈。

解决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远程连接了

ubuntu 22.04 安装wordpress 搭建博客网站

总体介绍

Apache 作为 Web 服务器接收用户请求并返回网页内容;MySQL 作为数据库存储网站数据;PHP 作为脚本语言在服务器端处理请求并与 MySQL 交互生成动态内容;WordPress 则是基于 PHP 和 MySQL 的应用程序,提供了一个方便的网站创建和管理平台。

安装apache、mariadb和php

sudo apt update
apt install apache2 mariadb-server libapache2-mod-php php-gd php-mysql
apt install php-curl php-mbstring php-intl php-gmp php-bcmath php-xml php-imagick php-zip php-json

配置数据库

为WordPress初始化数据库MariaDB

mysql_secure_installation

初始化MariaDB设置如下:

Set root password? [Y/n] n
Remove anonymous users? [Y/n] y
Disallow root login remotely? [Y/n] y
Remove test database and access to it? [Y/n] y
Reload privilege tables now? [Y/n] y

登录到MariaDB控制台并为WordPress创建数据库。安装phpmyadmin

apt install phpmyadmin

为MySQL用户配置密码访问

mysql -u root -p

在mysql命令行中,执行以下指令,注意不要漏掉最后的分号

CREATE USER 'wpsql'@'localhost' IDENTIFIED BY '你的密码';
GRANT ALL PRIVILEGES ON *.* TO 'wpsql'@'localhost' WITH GRANT OPTION;
exit

安装wordpress

wget https://wordpress.org/latest.tar.gz
tar -xzvf latest.tar.gz
sudo mv wordpress/* /var/www/html/
chown -R www-data:www-data /var/www/html
mv /var/www/html/index.html /var/www/html/index~.html

创建数据库和获取数据库密钥

浏览器打开:ip地址/phpmyadmin,登陆phpmyadmin,用户名:wpsql 密码:wpsql密码

创建wordpress数据库

创建数据库账号

数据库名:wpadmin
数据库密码:“设置用户密码”

注意这里生成的密钥:wOSeiA]xOKa0c[c1,要记住,当然后面还可以改,这是wordpress可登录sql的密钥,并不是你设置的那个密码

将该用户附予数据库wordpress,选中创建的wpadmin账户

选择数据库wordpress

权限全部都要选上

WordPress网站配置

cd /var/www/html
cp wp-config-sample.php wp-config.php
vim wp-config.php

在Database password那里把上边记录的数据库密钥填到这里,注意这里并不是填你设置的密码。然后重启apache

systemctl restart apache2

然后,就可以从浏览器打开ip地址来登陆wordpress了,首次登陆会要求设置用户名和密码。

其他问题讨论

  • 1、WordPress安装主题、插件、更新时需要FTP的解决办法

修改wp-config.php文件vim /var/www/html/wp-config.php。对于没有服务器目录操作权限的用户来说,修改wp-config.php文件会比较简单快捷,只需要在wp-config.php的文件中,define( ‘WP_DEBUG’, false );下面添加以下代码:

define('FS_METHOD','direct');
  • 2、WordPress解决文件大小上传2M限制方法

在 WordPress 中默认的上传文件大小限制可能会比较小,开始为2M,要将上传文件大小限制增加,可以通过修改 php.ini 文件实现。找到并打开 php.ini 文件:php.ini 文件的位置可能在 /etc/php/[php 版本号]/apache2/php.ini 或 /etc/php/[php 版本号]/cli/php.ini。使用vim等修改以下参数:

upload_max_filesize: 设置允许上传的最大文件大小,例如将其设置为 200M

post_max_size: 设置 POST 请求允许的最大大小,这个值通常应该比 upload_max_filesize 稍大一些,也可以设置为 200M

重启apache

systemctl restart apache2

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的结果作为预测,前面位置的结果是无意义的)