使用Dorker来运行深度学习

假设已经配置好了Dorker容器,里面已经存在深度学习所需的环境。

Ubuntu系统,创建一个docker,然后搭建conda深度学习环境,这样可以用conda或pip安装相关的依赖库了。

一、创建一个docker

为了方便开发,在Docker Hub官方中选择一个合适的conda docker镜像,然后下载到本地。

我选择了“docker-anaconda”,地址是:Docker Hub

下载命令如下:

docker pull continuumio/anaconda3

二、进入docker

通常使用 docker run 命令进入docker镜像,例如:

docker run -i -t continuumio/anaconda3 /bin/bash

其中 -i: 以交互模式运行容器,通常与 -t 同时使用;

2.1 映射目录

平常进入了docker环境,然后创建或产生的文件,在退出docker环境后会“自动销毁”;或者想运行本地主机的某个程序,发现在docker环境中找不到。

我们可以通过映射目录的方式,把本地主机的某个目录,映射到docker环境中,这样产生的文件会保留在本地主机中。

比如:

docker run -i -t continuumio/anaconda3 -v /home/xxx/xxx/:/home/xxxx:rw /bin/bash

通过-v 把本地主机目录 /home/xxx/xxx/ 映射到docker环境中的/home/xxxx 目录;其权限是rw,即能读能写。

2.2 支持GPU

默认是不把GPU加入到docker环境中的,但可以通过参数设置:

--gpus all

但我发现,这样有时不能在docker里正常使用GPU;可以使用如下参数,在Pytorch中亲测有效。 

--gpus all  -e NVIDIA_DRIVER_CAPABILITIES=compute,utility -e NVIDIA_VISIBLE_DEVICES=all

举个例子:

docker run -i -t continuumio/anaconda3 --gpus all  -e NVIDIA_DRIVER_CAPABILITIES=compute,utility -e NVIDIA_VISIBLE_DEVICES=all /bin/bash

2.3 设置内存

 默认分配很小的内参,在训练模型时不够用,可以通过参数设置:

--shm-size xxG

比如,我电脑有32G内参,想放16G到docker中使用,设置为 –shm-size 16G,即:

docker run -i -t continuumio/anaconda3  --shm-size 16G /bin/bash

2.4 综合版本

结合映射目录、支持GPU、设置内存,打开docker的命令如下:

docker run -i -t  -v /home/disk1/guopu/:/home/guopu:rw --gpus all --shm-size 16G -e NVIDIA_DRIVER_CAPABILITIES=compute,utility -e NVIDIA_VISIBLE_DEVICES=all continuumio/anaconda3  /bin/bash

详细的参数解析如下

  • -a stdin: 指定标准输入输出内容类型,可选 STDIN/STDOUT/STDERR 三项;
  • -d: 后台运行容器,并返回容器ID;
  • -i: 以交互模式运行容器,通常与 -t 同时使用;
  • -P: 随机端口映射,容器内部端口随机映射到主机的端口
  • -p: 指定端口映射,格式为:主机(宿主)端口:容器端口
  • -t: 为容器重新分配一个伪输入终端,通常与 -i 同时使用;
  • –name=”nginx-lb”: 为容器指定一个名称;
  • –dns 8.8.8.8: 指定容器使用的DNS服务器,默认和宿主一致;
  • –dns-search example.com: 指定容器DNS搜索域名,默认和宿主一致;
  • -h “mars”: 指定容器的hostname;
  • -e username=”ritchie”: 设置环境变量;
  • –env-file=[]: 从指定文件读入环境变量;
  • –cpuset=”0-2″ or –cpuset=”0,1,2″: 绑定容器到指定CPU运行;
  • -m :设置容器使用内存最大值;
  • –net=”bridge”: 指定容器的网络连接类型,支持 bridge/host/none/container: 四种类型;
  • –link=[]: 添加链接到另一个容器;
  • –expose=[]: 开放一个端口或一组端口;
  • –volume , -v: 绑定一个卷

三、检验docker

进入docker中,首先查看一下GPU,用nvidia-smi命令。正常显示CUDA版本,正常加载了显卡(这里是两张1080ti)。

使用两张显卡训练YOLOv5时,显示正常;

打开dorker:

sudo docker start 容器名

docker run 只在第一次运行时使用,将镜像放到容器中,以后再次启动这个容器时,只需要使用命令docker start
即可。docker run相当于执行了两步操作:将镜像放入容器中(docker
create),然后将容器启动,使之变成运行时容器(docker start)。而docker
start的作用是,重新启动已存在的镜像。也就是说,如果使用这个命令,我们必须事先知道这个容器的ID,或者这个容器的名字,我们可以使用docker
ps找到这个容器的信息。

四、进入已打开的docker

思路:首先使用docker ps 查询正在运行docker的ID,然后使用docker exec 命令进入。

命令如下:

$ sudo docker ps  
$ sudo docker exec -it docker_ID /bin/bash

其中docker_ID,是使用docker ps查询正在运行docker的ID,比如是fe8984f24b79。

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

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)摄像测量技术,视频测量


上网限制和翻墙的基本原理

相关文章:翻墙与科学上网全面解析  翻墙与科学上网

本文转载自:http://blog.021xt.cc/archives/85

目前在国内基本访问不了google站点和android的站点,下载个gradle都要等很久。所以如果不翻墙很多工作都没办法正常做。所以在学习翻墙的同时也顺便了解了下目前限制网络访问的一些基本知识。

网络限制和监控应该说大家都有体会。比如很多公司都会限制一些网站的访问,比如网盘、视屏网站。有时也会对你访问的内容进行监控。还有一些公共WIFI,可能限制你只能访问80端口。在比如在国内无法访问google,facebook,android等网站。要想绕过这些限制,必须先知道他们是如何限制的。

本文主要是从技术角度来了解的网络限制方式和应对方式。并不做任何翻墙方式的推荐和指导。对于网络的知识,还是停留在上过网络课 的水平,文章内容也都是自己了解后总结的。可能会有错误和遗漏。会定期更新。

DNS污染和劫持

以下解释来之百度百科:
某些网络运营商为了某些目的,对DNS进行了某些操作,导致使用ISP的正常上网设置无法通过域名取得正确的IP地址。
某些国家或地区出于某些目的为了防止某网站被访问,而且其又掌握部分国际DNS根目录服务器或镜像,也会利用此方法进行屏蔽。

目前我们访问网站主要都是通过域名进行访问,而真正访问这个网站前需要通过DNS服务器把域名解析为IP地址。而普通的DNS服务使用UDP协议,没有任何的认证机制。DNS劫持是指返回给你一个伪造页面的IP地址,DNS污染是返回给你一个不存在的页面的IP地址。

比如你使用电信、联通、移动的宽带,默认你是不需要设置任何DNS服务器的。这些DNS服务器由他们提供。一旦检测到你访问的网页是不允许的访问的,就会返回一个不存在的网页。而很多运营商也会使用DNS劫持来投放一些广告。

解决办法:

  1. 使用OpenDNS(208.67.222.222)或GoogleDNS(8.8.8.8)(现在不太好用,被封锁,速度慢)
  2. 使用一些第三方的DNS服务器
  3. 自己用VPS搭建DNS服务器
  4. 修改机器host文件,直接IP访问

封锁IP

通过上面一些方式,可以绕过DNS污染,通过IP地址访问无法访问的网页。但是目前针对IP进行大范围的封锁。虽然google这种大公司有很多镜像IP地址,但是目前基本全部被封锁掉,有漏网的可能也坚持不了多久。而且很多小公司的服务是部署在一些第三方的主机上,所以封锁IP有时会误伤,封锁一个IP导致主机上本来可以使用的页面也无法访问了。

不过目前不可能把所有国外的IP全部封锁掉,所以我们采用机会从国内连接到国外的VPS,进行翻墙。

解决办法:

  1. 使用VPS搭建代理
  2. 使用IPV6 (IPV6地址巨大,采用封地址不现实,但是目前国内只有部分高校部署了IPV6)

封锁HTTP代理

对于没有办法搭建VPS的人来说,最好的办法就是使用HTTP代理。客户端不在直接请求目标服务器,而是请求代理服务器,代理服务器在去请求目标服务器。然后返回结果。关于HTTP代理可以参考

1《HTTP权威指南》第六章:代理
代理示意图

对于HTTP代理来说,封锁起来非常简单。因为HTTP协议是明文,Request Message中就带有要请求的URL或IP地址,这样很容易就被检测到。对于HTTPS来说,虽然通信是进行加密了,但是在建连之前会给代理服务器发送CONNECT方法,这里也会带上要访问的远端服务器地址。如果代理服务器在国外,在出去前就会被检测到。 如果代理服务器在国内,呵呵,你也出不去啊。

对于HTTP代理,因为是明文,所以很容易被服务器了解你的一些数据。所以不要随便使用第三方的HTTP代理访问HTTP网站,而HTTPS虽然不知道你的数据,但是可以知道你去了那里。

解决办法:

  1. 使用VPS搭建VPN
  2. 使用第三方VPN

封锁VPN

虚拟专用网(英语:Virtual Private Network,简称VPN),是一种常用于连接中、大型企业或团体与团体间的私人网络的通讯方法。虚拟私人网络的讯息透过公用的网络架构(例如:互联网)来传送内联网的网络讯息。它利用已加密的通道协议(Tunneling Protocol)来达到保密、发送端认证、消息准确性等私人消息安全效果。

VPN示意图

正常网络通信时,所有网络请求都是通过我们的物理网卡直接发送出去。而VPN是客户端使用相应的VPN协议先与VPN服务器进行通信,成功连接后就在操作系统内建立一个虚拟网卡,一般来说默认PC上所有网络通信都从这虚拟网卡上进出,经过VPN服务器中转之后再到达目的地。

VPN发送流程示意图

通常VPN协议都会对数据流进行强加密处理,从而使得第三方无法知道数据内容,这样就实现了翻墙。翻墙时VPN服务器知道你干的所有事情(HTTP,对于HTTPS,它知道你去了哪)。

VPN有多种协议:OPENVPN、PPTP、L2TP/IPSec、SSLVPN、IKEv2 VPN,Cisco VPN等。其中的PPTP和L2TP是明文传输协议。只负责传输,不负责加密。分别利用了MPPE和IPSec进行加密。

背景PPTP 是一个基于 PPP 的很基本的协议。PPTP 是微软 Windows 平台第一个支持的 VPN 协议。PPTP 标准并没有实际描述加密和授权特性,并且依赖于 PPP 协议的隧道来实现安全功能。L2TP 是一个在 IETF RFC 3193 中被正式标准化的高级协议。推荐在需要安全加密的地方用来替代 PPTP。OpenVPN 是一个高级的开源 VPN 解决方案,由 “OpenVPN technologies” 支持,并且已经成为开源网络领域里的事实标准。OpenVPN 使用成熟的 SSL/TLS 加密协议。
数据加密PPP 负载是使用微软点对点协议(Microsoft’s Point-to-Point Encryption protocol,MPPE)加密。MPPE 实现了 RSA RC4 加密算法,并使用最长 128 位密钥。L2TP 负载使用标准的 IPSec 协议加密。在 RFC 4835 中指定了使用 3DES 或 AES 加密算法作为保密方式。OpenVPN 使用 OpenSSL 库来提供加密。OpenSSL 支持好几种不同的加密算法,如:3DES,AES,RC5 等。
安装/配置Windows 所有版本和大多数其他操作系统包括移动平台都内建了对 PPTP 的支持。PPTP 只需要一个用户名和密码,以及一个服务器地址,所以安装和配置相当简单。从 2000/XP 起的所有 Windows 平台和 Mac OS X 10.3+ 都内建了 L2TP/IPSec 的支持。大多数现代的移动平台比如 iPhone 和 Android 也有内建的客户端。OpenVPN 不包含在任何操作系统中,需要安装客户端软件,但安装也是相当简单,基本上 5 分钟可以完成。
速度由于使用 128 位密钥,加密开销相比 OpenVPN 使用 256位密钥要小,所以速度感觉稍快一点,但这个差异微不足道。L2TP/IPSec 将数据封装两次,所以相比其他竞争者效率稍低,速度也慢一些。当使用默认的 UDP 模式,OpenVPN 的表现是最佳的。
端口PPTP 使用 TCP 1723 端口和 GRE(协议 47)。通过限制 GRE 协议,PPTP 可以轻易地被封锁。L2TP/IPSec 使用 UDP 500 端口用来初始化密钥交换,使用协议 50 用来传输 IPSec 加密的数据( ESP ),使用 UDP 1701 端口用来初始化 L2TP 的配置,还使用 UDP 4500 端口来穿过 NAT。L2TP/IPSec 相比 OpenVPN 容易封锁,因为它依赖于固定的协议和端口。OpenVPN 可以很容易的配置为使用任何端口运行,也可以使用 UDP 或 TCP 协议。为了顺利穿越限制性的防火墙,可以将 OpenVPN 配置成使用 TCP 443 端口,因为这样就无法和标准的 HTTPS 无法区分,从而极难被封锁。
稳定性/兼容性PPTP 不如 OpenVPN 可靠,也不能像 OpenVPN 那样在不稳定网络中快速恢复。另外还有部分同 GRE 协议和一些路由器的兼容性问题。L2TP/IPSec 比 OpenVPN 更复杂,为了使在 NAT 路由器下的设备可靠地使用,配置可以会更加困难。但是,只要服务器和客户端都支持 NAT 穿越,那么就没什么问题了。无论是无线网络、蜂窝网络,还是丢包和拥塞经常发生的不可靠网络,OpenVPN 都非常稳定、快速。对于那些相当不可以的连接,OpenVPN 有一个 TCP 模式可以使用,但是要牺牲一点速度,因为将 TCP 封装在 TCP 时效率不高。
安全弱点微软实现的 PPTP 有一个严重的安全问题(serious security vulnerabilities)。对于词典攻击来说 MSCHAP-v2 是很脆弱的,并且 RC4 算法也会遭到“位翻转攻击( bit-flipping attack )”。如果保密是重要的,微软也强烈建议升级到 IPSec。IPSec 没有明显的漏洞,当和安全加密算法如 AES 一起使用时,被认为是很安全的。OpenVPN 也没有明显漏洞,当和安全加密算法如 AES 一起使用时,也被认为是相当安全的。
客户端的兼容性Windows、Mac OS X、Linux、Apple iOS、Android、DD-WRTWindows、Mac OS X、Linux、Apple iOS、AndroidWindows、Mac OS X、Linux
结论由于主要的安全漏洞,除了兼容性以外没有好的理由选择使用 PPTP。如果你的设备既不支持 L2TP/IPSec 又不支持 OpenVPN,那么 PPTP 是一个合理的选择。如果关心快速安装和简易配置,那么 L2TP/IPSec 值得考虑。L2TP/IPSec 是优秀的,但相比 OpenVPN 的高效和杰出的稳定性要落后一点。如果你使用运行 iOS 或 Android 的移动设备,那么这就是最佳的选择,因为 OpenVPN 目前还不支持这些平台。另外,如果需要快速安装,L2TP/IPSec 也是一个较佳的选择。对于所有的 Windows, Mac OS X 以及 Linux 桌面用户来说,OpenVPN 是最好的选择。OpenVPN 速度快,并且安全可信。但劣势是缺乏对移动设备的支持,另外还需要安装第三方客户端。
等级

对于VPN和其他一些加密的传输的协议来说,没有办法直接获取明文的请求信息,所以没有办法直接封锁,而是使用了监控的方式:

暴力破解

对于一些使用弱加密方式的协议来说,直接使用暴力破解检查传输内容。比如PPTP使用MPPE加密,但是MPPE是基于RC4,对于强大的防火墙背后的超级计算机集群,破解就是几秒钟的事情。

破解后明文中一旦包含了违禁内容,请求就会被封。而对应的IP可能会进入重点关怀列表。

特征检测

要想成功翻墙都必须与对应的远程服务器建立连接,然后再用对应的协议进行数据处理并传输。
而问题就出在这里:翻墙工具和远程服务器建立连接时,如果表现的很独特,在一大堆流量里很显眼,就会轻易被GFW识别出从而直接阻断连接,而VPN(尤其是OPENVPN)和SSH这方面的问题尤其严重。

流量监控

当一个VPN地址被大量人请求,并保持长时间连接时,就很容易引起关注。SSH接口有大量数据请求。一般会结合其他特征。

深度包检测

深度数据包检测(英语:Deep packet inspection,缩写为 DPI),又称完全数据包探测(complete packet inspection)或信息萃取(Information eXtraction,IX),是一种电脑网络数据包过滤技术,用来检查通过检测点之数据包的数据部分(亦可能包含其标头),以搜索不匹配规范之协议、病毒、垃圾邮件、入侵,或以预定之准则来决定数据包是否可通过或需被路由至其他不同目的地,亦或是为了收集统计数据之目的。

比如我们用HTTPS来访问一个网站,TLS/SSL协议在建连过程如下:

https连接示意图

很明显的会发送“client hello”和“server hello” 这种特诊很明显的信息。(当然不会根据这个就封掉,否则https没法用了)。而后续会有服务端证书发送,验证,客户端密钥协商等过程。有明显的协议特征。

VPN原理示意图

下面是网上找的两张图:提醒大家最好不要随便用不安全的VPN来访问不合适的网页,开开android没啥问题。

不安全VPN示意图

Socks代理/SSH Socks

SOCKS是一种网络传输协议,主要用于客户端与外网服务器之间通讯的中间传递。SOCKS是”SOCKetS”的缩写[1]。
当防火墙后的客户端要访问外部的服务器时,就跟SOCKS代理服务器连接。这个代理服务器控制客户端访问外网的资格,允许的话,就将客户端的请求发往外部的服务器。
这个协议最初由David Koblas开发,而后由NEC的Ying-Da Lee将其扩展到版本4。最新协议是版本5,与前一版本相比,增加支持UDP、验证,以及IPv6。
根据OSI模型,SOCKS是会话层的协议,位于表示层与传输层之间

与HTTP代理的对比

SOCKS工作在比HTTP代理更低的层次:SOCKS使用握手协议来通知代理软件其客户端试图进行的连接SOCKS,然后尽可能透明地进行操作,而常规代理可能会解释和重写报头(例如,使用另一种底层协议,例如FTP;然而,HTTP代理只是将HTTP请求转发到所需的HTTP服务器)。虽然HTTP代理有不同的使用模式,CONNECT方法允许转发TCP连接;然而,SOCKS代理还可以转发UDP流量和反向代理,而HTTP代理不能。HTTP代理通常更了解HTTP协议,执行更高层次的过滤(虽然通常只用于GET和POST方法,而不用于CONNECT方法)

Socks代理本身协议是明文传输,虽然相对HTTP有一些优势,但是明文也导致Socks代理很容易被封。所以可以考虑对Socks进行加密。所以出现了SSH Socks,对于MAC和Linux来说,不需要Client就可以进行访问。详细可以看:SSH隧道技术简介:端口转发&SOCKS代理

但是网上看有些地区好像会对一些VPS的SSH进行端口干扰。我在武汉好像SSH到我的VPS一会就会断。在上海一直没这问题。而且SSH一般是小流量数据,如果数据量特别大,也会被认为是翻墙,进入特别关怀列表。

Shadowsocks

认准官网:https://shadowsocks.org/en/index.html (.com那个是卖账号的)

1
2
A secure socks5 proxy,
designed to protect your Internet traffic.

Shadowsocks 目前不容易被封杀主要是因为:

  1. 建立在socks5协议之上,socks5是运用很广泛的协议,所以没办法直接封杀socks5协议
  2. 使用socks5协议建立连接,而没有使用VPN中的服务端身份验证和密钥协商过程。而是在服务端和客户端直接写死密钥和加密算法。所以防火墙很难找到明显的特征,因为这就是个普通的socks5协议。
  3. Shadowsock搭建也比较简单,所以很多人自己架设VPS搭建,个人使用流量也很小,没法通过流量监控方式封杀。
  4. 自定义加密方式和密钥。因为加密主要主要是防止被检测,所以要选择安全系数高的加密方式。之前RC4会很容易被破解,而导致被封杀。所以现在推荐使用AES加密。而在客户端和服务端自定义密钥,泄露的风险相对较小。

所以如果是自己搭建的Shadosocks被封的概率很小,但是如果是第三方的Shadeowsocks,密码是server定的,你的数据很可能遭受到中间人攻击。

顺便说一下,Shadowssocks是天朝的clowwindy大神写的。不过Shadowsocks项目源码已经从github上删除了并停止维护了,但是release中还有源码可以下载。https://github.com/shadowsocks/shadowsocks

Shadowsocks-rss

前面认为Shadowssocks特征并不是很明细,但是了解协议工作原理后会发现,SS协议本身还有有漏洞,可以被利用来检测特征,具体讨论看:ShadowSocks协议的弱点分析和改进。 里面中间那些撕逼就不用看了,我总结了下大致意思是:
协议过于简单,并且格式固定,很容易被发起中间人攻击。先看看协议结构

1
2
3
4
5
+————–+———————+——————+———-+
| Address Type | Destination Address | Destination Port | Data |
+————–+———————+——————+———-+
| 1 | Variable | 2 | Variable |
+————–+———————+——————+———-+

Possible values of address type are 1 (IPv4), 4 (IPv6), 3 (hostname). For IPv4 address, it’s packed as a 32-bit (4-byte) big-endian integer. For IPv6 address, a compact representation (16-byte array) is used. For hostname, the first byte of destination address indicates the length, which limits the length of hostname to 255. The destination port is also a big-endian integer.

The request is encrypted using the specified cipher with a random IV and the pre-shared key, it then becomes so-called payload.

结构很简单,上面解释也很清楚。Client每一个请求都是这种格式,然后进行加密。Server端解密然后解析。看起来没什么问题,没有密钥你无法模拟中间人攻击,也没什么明显特征。但是看看Server处理逻辑会发现存在一些问题:

Client数据在加密目前用的最多的是AES系列,加密后在协议数据前会有16位的IV。而Server段解析后,首先判断请求是否有效,而这个判断很简单:

1判断的依据就是Address Type的那个字节,看它是不是在那三个可能取值,如果不是,立即断开连接,如果是,就尝试解析后面的地址和端口进行连接。

如果能发起中间人攻击,模拟Client请求,这个就是一个很明显的特征,如果把Address Type穷举各种情况,其中只有3种情况会连接成功。那么很可能就是一个Shadowsocks 服务器。

所以只需要先劫持一条socks5的请求,因为AES加密后Address Type位置是固定的(第17位),篡改这一位,穷举256种情况(AES-256),然后发送给服务器。如果服务器在3种情况没有关闭连接,就说明这个很可能是Shadowsock服务。你这个IP很快就进入关怀列表了。

这里的关键就是AES加密明文和密文对应关系。密码学不是太懂,贴帖子里面一个回复:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
举个例子,现在有一个协议包,共7个字节

0x01, 0x08, 0x08, 0x08, 0x08, 0x00, 0x50
对照socks5协议,很明显这是一个IPv4包(第一个字节是0x01),目的地是8.8.8.8的80端口

被shadowsocks加密了以后(密码abc,加密方式aes-256-cfb),数据包就变成了这样

0xbb, 0x59, 0x1c, 0x4a, 0xb9, 0x0a, 0x91, 0xdc, 0x07, 0xef, 0x72, 0x05, 0x90, 0x42, 0xca, 0x0d, 0x4c, 0x3b, 0x87, 0x8e, 0xca, 0xab, 0x32
前16个字节,从0xbb到0x0d,都是iv,根据issue中提到的弱点和之前的总结,只需要修改0x4c,即真正密文中的第一个字节,就可要起到修改明文中的第一个字节的效果。

那就把0x4c修改成0x4d吧,解密以后的结果是

0x00, 0x08, 0x08, 0x08, 0x08, 0x00, 0x50
的确只有第一个字节被改掉了,根据breakwa11的理论,不难推出其他情况,其中合法的是

0x4e => 0x03 (Domain Name)
0x49 => 0x04 (IPv6)

所以目前Shadowsocks应该是比较容易被检测出来。但是为什么没有被封掉,呵呵,就不知道了。所以这个项目目的就是在SS基础上进行一些混淆。因为原有实现确实有漏洞。 不过目前这个项目好像也停止更新了。并且木有开源。

当然如果是自己用完全可以自己修改一个私有协议,这样就没法被检测到了。但是需要同时修改Server段,MAC Client,Windows Client, Android Client。 – -!

GoAgent和GoProxy

Google App Engine是一个开发、托管网络应用程序的平台,使用Google管理的数据中心

GAE示意图

GoAgent的运行原理与其他代理工具基本相同,使用特定的中转服务器完成数据传输。它使用Google App Engine的服务器作为中传,将数据包后发送至Google服务器,再由Google服务器转发至目的服务器,接收数据时方法也类似。由于服务器端软件基本相同,该中转服务器既可以是用户自行架设的服务器,也可以是由其他人架设的开放服务器。

GoAgent其实也是利用GAE作为代理,但是因为他是连接到google的服务器,因为在国内现在google大量被封,所以GoAgent也基本很难使用。目前github上源码也已经删除。

但是GoAgent本身不依赖于GAE,而且使用Python编写,完全可以部署到VPS上进行代理。GoProxy是GoAgent的后续项目https://github.com/phuslu/goproxy
还有一个XX-NET:https://github.com/XX-net/XX-Net 有兴趣都可以去了解下。

Tor

Tor(The Onion Router,洋葱路由器)是实现匿名通信的自由软件。Tor是第二代洋葱路由的一种实现,用户通过Tor可以在因特网上进行匿名交流。

Tor: Overview

1The Tor network is a group of volunteer-operated servers that allows people to improve their privacy and security on the Internet. Tor’s users employ this network by connecting through a series of virtual tunnels rather than making a direct connection, thus allowing both organizations and individuals to share information over public networks without compromising their privacy. Along the same line, Tor is an effective censorship circumvention tool, allowing its users to reach otherwise blocked destinations or content. Tor can also be used as a building block for software developers to create new communication tools with built-in privacy features.

下面的图来自官网的介绍。具体内容大家可以自己看,简单说和其他翻墙方式不同,简单可以理解为Tor有一群代理服务器,然后要访问远端站点,是通过随机的代理路径来完成的,数据经历了多个代理服务器的传递。它主要作用是隐藏访问者信息,而翻墙只是顺带的功能。

要访问远程站点,Client需要知道Tor nodes,这些nodes就是普通加入的用户,就好像P2P下载一样。获取nodes信息后,会随机选择一条路径访问。 因为这个原因,Tor的速度可能不会很好。

Tor示意图1
Tor示意图2
Tor示意图3

而关于Tor的漏洞和检测看这里:Tor真的十分安全么 其原理以及漏洞详解!

目前有结合Tor+Shadowsocks前置代理使用的。

Reference

VPN翻墙,不安全的加密,不要相信墙内公司
GFW的工作原理(1) ————GFW是如何识别并封锁翻墙工具的
关于翻墙和匿名与网络安全类科普文大集合
为什么不应该用 SSL 翻墙
科学上网的一些原理

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后面能够大规模被应用提供了支持。

让网络来学习resize:插即用的新型图像调整器模型

Learning to Resize Images for Computer Vision Tasks

论文地址:https://arxiv.org/abs/2103.09950

代码:https://github.com/KushajveerSingh/resize_network_cv

尽管近年来卷积神经网络很大地促进了计算机视觉的发展,但一个重要方面很少被关注:图像大小对被训练的任务的准确性的影响 。通常,输入图像的大小被调整到一个相对较小的空间分辨率(例如,224×224),然后再进行训练和推理。这种调整大小的机制通常是固定的图像调整器(image resizer)(如:双行线插值)但是这些调整器是否限制了训练网络的任务性能呢? 作者通过实验证明了典型的线性调整器可以被可学习的调整器取代,从而大大提高性能 。虽然经典的调整器通常会具备更好的小图像感知质量(即对人类识别图片更加友好),本文提出的可学习调整器不一定会具备更好的视觉质量,但能够提高CV任务的性能。

在不同的任务中,可学习的图像调整器与baseline视觉模型进行联合训练。这种可学习的基于cnn的调整器创建了机器友好的视觉操作,因此在不同的视觉任务中表现出了更好的性能 。作者使用ImageNet数据集来进行分类任务,实验中使用四种不同的baseline模型来学习不同的调整器,相比于baseline模型,使用本文提出的可学习调整器能够获得更高的性能提升。

主要包括了两个重要的特性:(1) 双线性特征调整大小(bilinear feature resizing),以及(2)跳过连接(skip connection),该连接可容纳双线性调整大小的图像和CNN功能的组合。

第一个特性考虑到以原始分辨率计算的特征与模型的一致性。跳过连接可以简化学习过程,因为重定大小器模型可以直接将双线性重定大小的图像传递到基线任务中。

与一般的编码器-解码器架构不同,这篇论文中所提出的体系结构允许将图像大小调整为任何目标大小和纵横比(注意:这个大小必须是我们自己设定的,而不是网络 自己学习的)。并且可学习的resizer性能几乎不依赖于双线性重定器的选择,这意味着它可以直接替换其他现成的方法。

以上之后,就没有别的了,还以为是什么样子的惊天设计,最后不就是:给网络变得复杂了吗,把这种复杂说成是可学习的resizer,这样的话,普通网络的浅层都可以说成是可学习的resizer不是吗?

另外,通过一些实验 来说,确实能够提升效果。个人认为 作者提出的resizer模型实际上是一个可训练的数据增强方法,甚至也可以认为就是将模型变得更加复杂。整体的网络就像是一般模型中resblock。

作者的对比试验是这样做的:首先通过常用的reisze方法训练网络模型,作为baseline,然后在训练好的网络模型前面添加可学习的resizer,然后进行训练,作为自己的方法。感受一下作者的实验结果吧。如表3和表4

表3 训练配置
表4 对比试验

四. 总结

文章写得好,加上点运气,都可以发高质量论文,以上

代码实现:

import torch
import torch.nn as nn
import torch.nn.functional as F
from functools import partial

"""
    Learning to Resize Images for Computer Vision Tasks
    https://arxiv.org/pdf/2105.04714.pdf
"""

def conv1x1(in_chs, out_chs = 16):
    return nn.Conv2d(in_chs, out_chs, kernel_size=1, stride=1, padding=0)


def conv3x3(in_chs, out_chs = 16):
    return nn.Conv2d(in_chs, out_chs, kernel_size=3, stride=1, padding=1)


def conv7x7(in_chs, out_chs = 16):
    return nn.Conv2d(in_chs, out_chs, kernel_size=7, stride=1, padding=3)


class ResBlock(nn.Module):
    def __init__(self, in_chs,out_chs = 16):
        super(ResBlock, self).__init__()
        self.layers = nn.Sequential(
            conv3x3(in_chs, out_chs),
            nn.BatchNorm2d(out_chs),
            nn.LeakyReLU(0.2),
            conv3x3(out_chs, out_chs),
            nn.BatchNorm2d(out_chs)
        )
    def forward(self, x):
        identity = x
        out = self.layers(x)
        out += identity
        return out


class Resizer(nn.Module):
    def __init__(self, in_chs, out_size, n_filters = 16, n_res_blocks = 1, mode = 'bilinear'):
        super(Resizer, self).__init__()
        self.interpolate_layer = partial(F.interpolate, size=out_size, mode=mode,
            align_corners=(True if mode in ('linear', 'bilinear', 'bicubic', 'trilinear') else None))
        self.conv_layers = nn.Sequential(
            conv7x7(in_chs, n_filters),
            nn.LeakyReLU(0.2),
            conv1x1(n_filters, n_filters),
            nn.LeakyReLU(0.2),
            nn.BatchNorm2d(n_filters)
        )
        self.residual_layers = nn.Sequential()
        for i in range(n_res_blocks):
            self.residual_layers.add_module(f'res{i}', ResBlock(n_filters, n_filters))
        self.residual_layers.add_module('conv3x3', conv3x3(n_filters, n_filters))
        self.residual_layers.add_module('bn', nn.BatchNorm2d(n_filters))
        self.final_conv = conv7x7(n_filters, in_chs)

    def forward(self, x):
        identity = self.interpolate_layer(x)
        conv_out = self.conv_layers(x)
        conv_out = self.interpolate_layer(conv_out)
        conv_out_identity = conv_out
        res_out = self.residual_layers(conv_out)
        res_out += conv_out_identity
        out = self.final_conv(res_out)
        out += identity
        return

Dynamic ReLU(2020)

论文地址: https://arxiv.org/pdf/2003.10027.pdf
源码地址: https://github.com/Islanna/DynamicReLU.

贡献:提出Dynamic ReLU激活函数

ReLU是深度神经网络中常用的激活函数。到目前为止,ReLU及其推广(非参数或参数)都是静态的,对所有的输入样本执行相同的操作。在本文中,我们提出了Dynamic ReLU激活函数(DY-ReLU),它的参数依赖于所有输入。其关键在于DY-ReLU将全局上下文编码为超函数,并相应地调整分段线性激活函数。与静态模型相比,DY-ReLU的额外计算开销可以忽略不计,但其表现能力显着提高,特别是对于轻量神经网络。仅仅通过简单地在MobileNetV2上使用DY-ReLU ,ImageNet分类的最高精度就可以从72.0%提高到76.2%,而计算量只增加了5%。

Dy-ReLU特点(优点):

  • 将所有输入元素 x={xc​} 的全局上下文编码在超参数 θ(x) 中(运用SE模块的注意力机制),以适应激活函数fθ(x)​(x)(可以根据输入数据x,动态的学习选择最佳的激活函数)。

ReLU是深度学习中很重要的里程碑,简单但强大,能够极大地提升神经网络的性能。目前也有很多ReLU的改进版,比如Leaky ReLU和 PReLU,而这些改进版和原版的最终参数都是固定的。所以论文自然而然地想到,如果能够根据输入特征来调整ReLU的参数可能会更好。

定义:

K认为是分段数

Relation to Prior Work

网络实现:

DY-ReLU的可能性很大,表1展示了DY-ReLU与原版ReLU以及其变种的关系。在学习到特定的参数后,DY-ReLU可等价于ReLU、LeakyReLU以及PReLU。而当K=1,偏置bc1=0时,则等价于SE模块。另外DY-ReLU也可以是一个动态且高效的Maxout算子,相当于将Maxout的K个卷积转换为K个动态的线性变化,然后同样地输出最大值。

DY-ReLU-A

空间位置和维度均共享(spatial and channel-shared),计算如图2a所示,仅需输出2K个参数,计算最简单,表达能力也最弱。

DY-ReLU-B

仅空间位置共享(spatial-shared and channel-wise),计算如图2b所示,输出2KC个参数。

问题:

HRNet 论文和代码详解

github: https://github.com/HRNet/HRNet-Semantic-Segmentation

Paper: https://arxiv.org/abs/1908.07919

High-Resoultion Net(HRNet)由微软亚洲研究院和中科大提出,发表在CVPR2019

摘要:高分辨率表示对于位置敏感的视觉问题十分重要,比如目标检测、语义分割、姿态估计。为了这些任务位置信息更加精准,很容易想到的做法就是维持高分辨率的feature map,事实上HRNet之前几乎所有的网络都是这么做的,通过下采样得到强语义信息,然后再上采样恢复高分辨率恢复位置信息(如下图所示),然而这种做法,会导致大量的有效信息在不断的上下采样过程中丢失。而HRNet通过并行多个分辨率的分支,加上不断进行不同分支之间的信息交互,同时达到强语义信息和精准位置信息的目的。

模型的主要特点是在整个过程中特征图(Feature Map)始终保持高分辨率,这与之前主流方法思路上有很大的不同。在HRNet之前,2D人体姿态估计算法是采用(Hourglass/CPN/Simple Baseline/MSPN等)将高分辨率特征图下采样至低分辨率,再从低分辨率特征图恢复至高分辨率的思路(单次或重复多次),以此过程实现了多尺度特征提取的一个过程。

HRNet在整个过程中保持特征图的高分辨率,但多尺度特征提取是姿态估计模型一定要实现的过程,那么HRNet是如何实现多尺度特征提取的呢?模型是通过在高分辨率特征图主网络逐渐并行加入低分辨率特征图子网络,不同网络实现多尺度融合与特征提取实现的。

特点与优势:

(1)作者提出的方法是并行连接高分辨率与低分辨率网络,而不是像之前方法那样串行连接。因此,其方法能够保持高分辨率,而不是通过一个低到高的过程恢复分辨率,因此预测的heatmap可能在空间上更精确。

(2)本文提出的模型融合相同深度和相似级别的低分辨率特征图来提高高分辨率的特征图的表示效果,并进行重复的多尺度融合。

缺点:因为特征图分辨率过大,而且数量多,这样肯定会导致巨额的耗时计算,对显存对硬件要求更高了

HRNet结构细节

Backbone设计

我将HRNet整个backbone部分进行了拆解,分成4个stage,每个stage分成蓝色框和橙色框两部分。其中蓝色框部分是每个stage的基本结构,由多个branch组成,HRNet中stage1蓝色框使用的是BottleNeck,stage2&3&4蓝色框使用的是BasicBlock。其中橙色框部分是每个stage的过渡结构,HRNet中stage1橙色框是一个TransitionLayer,stage2&3橙色框是一个FuseLayer和一个TransitionLayer的叠加,stage4橙色框是一个FuseLayer。

解释一下为什么这么设计,FuseLayer是用来进行不同分支的信息交互的,TransitionLayer是用来生成一个下采样两倍分支的输入feature map的,stage1橙色框显然没办法做FuseLayer,因为前一个stage只有一个分支,stage4橙色框后面接neck和head了,显然也不再需要TransitionLayer了。

整个backbone的构建流程可以总结为:make_backbone -> make_stages -> make_branches

有关backbone构建相关的看源码,主要讲一下FuseLayerTransitionLayerNeck的设计

FuseLayer设计

FuseLayer部分以绿色框为例,融合前为pre,融合后为post,静态构建一个二维矩阵,然后将pre和post对应连接的操作一一填入这个二维矩阵中。

以上图为例,图1的pre1和post1的操作为空,pre2和post1的操作为2倍上采,pre3和post1的操作为4倍上采;图2的pre1和post2的操作为3×3卷积下采,pre2和post2的操作为空,pre3和post2的操作为2倍上采;图3的pre1和post3的操作为连续两个3×3卷积下采,pre2和post3的操作为3×3卷积下采,pre3和post的操作为空。

前向计算时用一个二重循环将构建好的二维矩阵一一解开,将对应同一个post的pre转换后进行融合相加。比如post1 = f11(pre1) + f12(pre2) + f13(pre3)

FuseLayer的整体code如下:

def _make_fuse_layers(self):
  fuse_layers = []
  for post_index, out_channel in enumerate(self.out_channels[:len(self.in_channels)]):
      fuse_layer = []
      for pre_index, in_channel in enumerate(self.in_channels):
          if pre_index > post_index:
              fuse_layer.append(nn.Sequential(
                  nn.Conv2d(in_channel, out_channel, 1, 1, 0, bias=False),
                  nn.BatchNorm2d(out_channel, momentum=0.1),
                  nn.Upsample(scale_factor=2**(pre_index-post_index), mode='nearest')))
          elif pre_index < post_index:
              conv3x3s = []
              for cur_index in range(post_index - pre_index):
                  out_channels_conv3x3 = out_channel if cur_index == post_index - pre_index - 1 else in_channel
                  conv3x3 = nn.Sequential(
                      nn.Conv2d(in_channel, out_channels_conv3x3, 3, 2, 1, bias=False),
                      nn.BatchNorm2d(out_channels_conv3x3, momentum=0.1)
                  )
                  if cur_index < post_index - pre_index - 1:
                      conv3x3.add_module('relu_{}'.format(cur_index), nn.ReLU(False))
                  conv3x3s.append(conv3x3)
              fuse_layer.append(nn.Sequential(*conv3x3s))
          else:
              fuse_layer.append(None)
      fuse_layers.append(nn.ModuleList(fuse_layer))
  return nn.ModuleList(fuse_layers)

def forward(self, x):
  x_fuse = []
  for post_index in range(len(self.fuse_layers)):
      y = 0
      for pre_index in range(len(self.fuse_layers)):
          if post_index == pre_index:
              y += x[pre_index]
          else:
              y += self.fuse_layers[post_index][pre_index](x[pre_index])
      x_fuse.append(self.relu(y))

TransitionLayer设计

TransitionLayer以黄色框为例,静态构建一个一维矩阵,然后将pre和post对应连接的操作一一填入这个一维矩阵中。当pre1&post1、pre2&post2、pre3&post3的通道数对应相同时,一维矩阵填入None;通道数不相同时,对应位置填入一个转换卷积。post4比较特殊,这一部分代码和图例不太一致,图例是pre1&pre2&pre3都进行下采然后进行融合相加得到post4,而代码中post4通过pre3下采得到。

TransitionLayer整体code如下

def _make_transition_layers(self):
  num_branches_pre = len(self.in_channels)
  num_branches_post = len(self.out_channels)
  transition_layers = []
  for post_index in range(num_branches_post):
      if post_index < len(self.in_channels):
          if self.in_channels[post_index] != self.out_channels[post_index]:
              transition_layers.append(nn.Sequential(
                  nn.Conv2d(self.in_channels[post_index], self.out_channels[post_index], 3, 1, 1, bias=False),
                  nn.BatchNorm2d(self.out_channels[post_index], momentum=0.1),
                  nn.ReLU(inplace=True)
              ))
          else:
              transition_layers.append(None)
      else:
          conv3x3s = []
          for pre_index in range(post_index + 1 - num_branches_pre):
              in_channels_conv3x3 = self.in_channels[-1]
              out_channels_conv3x3 = self.out_channels[post_index] if pre_index == post_index - \
                  num_branches_pre else in_channels_conv3x3
              conv3x3s.append(nn.Sequential(
                  nn.Conv2d(in_channels_conv3x3, out_channels_conv3x3, 3, 2, 1, bias=False),
                  nn.BatchNorm2d(out_channels_conv3x3, momentum=0.1),
                  nn.ReLU(inplace=True)
              ))
          transition_layers.append(nn.Sequential(*conv3x3s))
  return nn.ModuleList(transition_layers)

def forward(self, x):
  x_trans = []
  for branch_index, transition_layer in enumerate(self.transition_layers):
      if branch_index < len(self.transition_layers) - 1:
          if transition_layer:
              x_trans.append(transition_layer(x[branch_index]))
          else:
              x_trans.append(x[branch_index])
      else:
          x_trans.append(transition_layer(x[-1]))

Neck设计

我把HRNet所描述的make_head过程理解成make_neck(因为一般意义上将最后的fc层理解成head更为清晰,这个在很多开源code中都是这样子拆解的)。下面着重讲解一下HRNet的neck设计。

HRNet的backbone输出有四个分支,paper中给出了几种方式对输出分支进行操作。

(a)图是HRNetV1的操作方式,只使用分辨率最高的feature map。

(b)图是HRNetV2的操作方式,将所有分辨率的feature map(小的特征图进行upsample)进行concate,主要用于语义分割和面部关键点检测。

(c)图是HRNetV2p的操作方式,在HRNetV2的基础上,使用了一个特征金字塔,主要用于目标检测。

而在图像分类任务上,HRNet有另一种特殊的neck设计

HRNet的neck可以分成三个部分,IncreLayer(橙色框),DownsampLayer(蓝色框)和FinalLayer(绿色框)。对每个backbone的输出分支进行升维操作,然后按照分辨率从大到小依次进行下采样同时从上到下逐级融合相加,最后用一个1x1conv升维。

def _make_neck(self, in_channels):
  head_block = Bottleneck
  self.incre_channels = [32, 64, 128, 256]
  self.neck_out_channels = 2048

  incre_modules = []
  downsamp_modules = []
  num_branches = len(self.in_channels)
  for index in range(num_branches):
      incre_module = self._make_layer(head_block, in_channels[index], incre_channels[index], 1, stride=1)
      incre_modules.append(incre_module)
      if index < num_branches - 1:
          downsamp_in_channels = self.incre_channels[index] * incre_module.expansion
          downsamp_out_channels = self.incre_channels[index+1] * incre_module.expansion
          downsamp_module = nn.Sequential(
              nn.Conv2d(in_channels=downsamp_in_channels, out_channels=downsamp_out_channels,
                        kernel_size=3, stride=2, padding=1),
              nn.BatchNorm2d(downsamp_out_channels, momentum=0.1),
              nn.ReLU(inplace=True)
          )
          downsamp_modules.append(downsamp_module)
  incre_modules = nn.ModuleList(incre_modules)
  downsamp_modules = nn.ModuleList(downsamp_modules)
  final_layer = nn.Sequential(
      nn.Conv2d(in_channels=self.out_channels[-1] * 4, out_channels=2048,
                kernel_size=1, stride=1, padding=0),
      nn.BatchNorm2d(2048, momentum=0.1),
      nn.ReLU(inplace=True)
  )
  return incre_modules, downsamp_modules, fine_layer

def forward(self, x):
  y = self.incre_modules[0](x[0])
  for index in range(len(self.downsamp_modules)):
      y = self.incre_modules[index+1](x[index+1]) + self.downsamp_modules[index](y)
  y = self.final_layer(y)
  y = F.avg_pool2d(y, kernel_size=y.size()[2:]).view(y.size(0), -1)

还有几个小细节

  1. BN层的momentom都设置为0.1
  2. stem使用的是两层stried为2的conv3x3
  3. FuseLayer的ReLU的inplace都设置为False

本地回环地址

本地回环地址(Loopback Address)通常是指127.0.0.1,不属于任何一个有类别地址类。一般都会用来检查本地网络协议、基本数据接口等是否正常的。

在开发或者调试时,我们经常需要和本地的服务器进行通信,例如启动nginx之后,在浏览器输入lcoalhost或者127.0.0.1就可以访问到本机上面的http服务。

Linux是如何访问本机IP的?

大多数操作系统都在网络层实现了环回能力,通常是使用一个虚拟的环回网络接口来实现。这个虚拟的环回网络接口看着像是一个真实的网卡,实际上是操作系统用软件模拟的,它可以通过TCP/IP与同一台主机上的其他服务进行通信,以127开头的IPv4地址就是为它保留的,主流Linux操作系统为环回网卡分配的地址都是127.0.0.1,主机名是localhost

环回网络接口之所以被称之为环回网络接口,是因为从本机发送到本机任意一个IP的数据报文都会在网络层交给环回网络接口,不再下发到数据链路层进行处理,环回网络接口直接发送回网络层,最终交由应用层软件程序进行处理。这种方式对于性能测试非常有用,因为省去了硬件的开销,可以直接测试协议栈软件所需要的时间。

那环回网络接口是如何判断目的IP是否为本机地址的呢?

答案就是网络层在进行路由转发的时候会先查本地的路由表,发现是本机IP后交给环回网络接口。查看本地路由表的命令如下:

ip route show table local

输出内容如下:

broadcast 10.141.128.0 dev eth0 proto kernel scope link src 10.141.155.131 
local 10.141.155.131 dev eth0 proto kernel scope host src 10.141.155.131 
broadcast 10.141.191.255 dev eth0 proto kernel scope link src 10.141.155.131 
broadcast 127.0.0.0 dev lo proto kernel scope link src 127.0.0.1 
local 127.0.0.0/8 dev lo proto kernel scope host src 127.0.0.1 
local 127.0.0.1 dev lo proto kernel scope host src 127.0.0.1

其中local开头的便是本地IP,dev后面是网卡名称。

查完了本地路由表之后会再查主路由表,也就是我们经常操作的路由表。

ip route show table main

输出内容如下

default via 10.141.128.1 dev eth0 proto static metric 100 
10.141.128.0/18 dev eth0 proto kernel scope link src 10.141.155.131 metric 100

环回网络接口

现在我们再来看下环回网络接口

ifconfig lo

输出

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 1554227  bytes 123327716 (117.6 MiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 1554227  bytes 123327716 (117.6 MiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

可以看到本地环回接口的IPv4地址是127.0.0.1,子网掩码是255.0.0.0,对应A类网络号127,有趣的是当我们访问 127.0.0.1-127.255.255.254之间的任意一个地址都会访问到本机。

IPv6地址::1,前缀是128位,表示只有一个地址。

环回网络接口的当前MTU64KB,不过最高可以设置到2GB,真是恐怖如斯。

下面几条RX,TX开头的分别代表收发到的数据报文个数和大小以及错包、丢包、溢出次数和无效帧。

排查网卡

排查网卡硬件问题的简便办法,在软件方面都检查不到问题而且物理层外部设备也工作正常的情况下可以采用,或许就是你的救命稻草!windows+R键打开“运行”窗口,输入cmd回车打开“命令提示符窗口”,然后输入ping 127.0.0.1 回车,如果返回值正常说明网卡硬件没有问题,反之就可以考虑网卡硬件的修复了!原理很简单,利用ping命令以及本地回环地址(127.0.0.1),通俗的说就是127.0.0.1这个地址会给自己的物理地址发送信息,即使在断网的条件下正常的网卡也应该ping的通,所以如果不通,就只能说明是网卡硬件问题了!

CycleMLP:用于密集预测的类似 MLP 的架构

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

作者单位:香港大学, 商汤科技
代码:https://github.com/ShoufaChen/CycleMLP

核心:用 Cycle-FC来替换Spatial FC(计算量大且网络对于不同图像分辨率的输入不可接受,且不能用于下游任务)

本文提出了一个简单的 MLP-like 的架构 CycleMLP,它是视觉识别和密集预测的通用主干,不同于现代 MLP 架构,例如 MLP-Mixer、ResMLP 和 gMLP,其架构与图像大小相关,因此是在目标检测和分割中不可行。

与现代方法相比,CycleMLP 有两个优势。

(1) 可以应对各种图像尺寸。

(2) 利用局部窗口实现对图像大小的线性计算复杂度。

相比之下,以前的 MLP 具有二次计算,因为它们具有完全的空间连接。

单个 CycleMLP Block 依然是分为 Token-mixing MLP 和 Channel mixing MLP,其中作者主要的贡献点在于替换 MLP-mixer 的 Token-mixing MLP 为 Cycle-FC。所以整个 CycleMLP Block 可以描述为:

何为 Cycle-FC ?要回答这个问题,我们首先来回顾一下 Channel FC 以及 Spatial FC.

Channel FC 即通道方向的映射,等效与1×1 卷积,其参数量与图像尺寸无关,而与通道数(token 维度)有关。假设输入输出特征图尺寸一致,则参数量为 C^2,其中 C 为通道数。而计算量则为 HWC^2,其中 H W 分别为特征图的高和宽。如果只考虑计算量与图像尺寸的影响的话,则为 O ( H W ) 。

Spatial FC 即 MLP-Mixer 使用的 Token-mixing 全连接层,在这里我们都是只考虑一个全连接层,则其实现的是 H W − > H W 的映射,参数量为 H^2W^2,计算量也为 H 2 W 2 C H^2W^2C,如果只考虑计算量与图像尺寸的影响的话,则为 O(H^2W^2)。并且HW 大小固定,网络对于不同图像分辨率的输入不可接受,且不能用于下游任务以及使用类似 EfficientNetV2 等的多分辨率训练策略。

为什么我们可以在复杂度分析时只考虑 H W 的影响呢?因为在金字塔结构的 MLP 中,通常一开始的 patch size 为 4,然后输入尺寸为 224×224,则一开始的 H = W = 56 = 224 / 4 ,而 C = 64 或者 96 ,所以C≪HW。如果对于下游任务而言,例如输入变为了512×512,则它们之间的差距更大了。为此在这里我们可以在复杂度分析中暂时只考虑 H W  而忽略 C 。

为了同时克服 Spatial 对于图像输入尺寸敏感以及计算量大的问题,作者提出了 Cycle-FC。其只是用通道方向的映射并且计算量和 Channel FC 保持一致。其说白了就是不断地以 [+1 0 -1 0 +1 0 -1 0 +1 …] 的方式移动特征图,将不同空间位置的特征对齐到同一个通道上,然后使用1×1 卷积。

回忆 AS-MLP,其采用的特征图移动方式则为 [+1 0 -1 +1 0 -1 +1 0 -1] 这样的成组方式,CycleMLP 则是使用“楼梯型”方式,但是其思想没有本质不同。此外,AS-MLP 确实对特征图进行了 Shift,并且采用了 zero-padding,而 CycleMLP 在具体实现过程中则是使用可变形卷积加以实现的。我个人对于 AS-MLP 与 CycleMLP 的理解如下图所示,可见他们其实核心思想是一致的。

from torchvision.ops.deform_conv import deform_conv2d

img3

CycleMLP 与 AS-MLP 只并行 H 和 W 方向的移动不同,CycleMLP 其实是三条支路并行:H 方向,W 方向,以及不移动特征图做通道方向映射。此外,AS-MLP 在一开始还做了一次 Channel Projection 进行降维。

img5

CycleMLP 最终使用的和 ViP 一样,使用 Split Attention 来融合三条支路

class CycleMLP(nn.Module):
    def __init__(self, dim, qkv_bias=False, qk_scale=None, attn_drop=0., proj_drop=0.):
        super().__init__()
        self.mlp_c = nn.Linear(dim, dim, bias=qkv_bias)

        self.sfc_h = CycleFC(dim, dim, (1, 3), 1, 0)
        self.sfc_w = CycleFC(dim, dim, (3, 1), 1, 0)

        self.reweight = Mlp(dim, dim // 4, dim * 3)

        self.proj = nn.Linear(dim, dim)
        self.proj_drop = nn.Dropout(proj_drop)

    def forward(self, x):
        B, H, W, C = x.shape
        h = self.sfc_h(x.permute(0, 3, 1, 2)).permute(0, 2, 3, 1)
        w = self.sfc_w(x.permute(0, 3, 1, 2)).permute(0, 2, 3, 1)
        c = self.mlp_c(x)

        a = (h + w + c).permute(0, 3, 1, 2).flatten(2).mean(2)
        a = self.reweight(a).reshape(B, C, 3).permute(2, 0, 1).softmax(dim=0).unsqueeze(2).unsqueeze(2)

        x = h * a[0] + w * a[1] + c * a[2]

        x = self.proj(x)
        x = self.proj_drop(x)

        return x

最后提一句,作者将投影区间定义为是 Pseudo-Kernel,这其实也是我们常说的 感受野 一词。

img4

2.2 整体网络结构

CycleMLP 的 Patch Embedding 也很有特色,使用卷积核大小为 7×7 ,步长为 4 的卷积。后续 Hire-MLP 其实也是这样进行的 Patch Embedding。相比而言 Swin 使用卷积核大小为 4×4,步长为 4 的卷积。在近期的我自己的小实验中也发现:Patch Embedding 时具有重叠会更好,这样可以避免边界效应并在小数据集上提升性能。CycleMLP 中间采用多阶段金字塔模型,总共分为 4 个阶段,每个阶段交替重复使用 CycleMLP Block。下采样使用卷积核大小为3×3,步长为 2 的卷积,这样做也有重叠,Hire-MLP 也是这样子哈。最后经过全局池化后连接一个全连接分类器即可。作者一共提出来了四种配置:

请添加图片描述

在这四种配置,Si​ 指 Patch Embedding 中的 Patch size,Ci​ 指 Patch Embedding 的输出编码特征维度,E i ​ 为 Channel-mixing MLP 中两个全连接层中第一个全连接层的 expand radio,Li​ 则是不同 Stage 中 Block 的重复次数。

3. 下游任务实验

CycleMLP 旨在为 MLP 模型的目标检测、实例分割和语义分割提供一个有竞争力的基线。与 AS-MLP 不同之处在于,CycleMLP 在 ADE20K 上进行实验,而 AS-MLP 在 COCO 上进行的实验。这真的是巧合,还是故意避开?不敢问也不敢说。

目标检测性能表现:相比 PVT,CycleMLP 都更具有优势。

请添加图片描述

语义分割性能表现:特别是,CycleMLP 在 ADE20K val 上达到了 45.1 mIoU,与 Swin (45.2 mIOU) 相当。

请添加图片描述

4. 消融实验

作者一共进行了三组消融实验:

  • Cycle-FC VS Spacial-FC and Channel-FC: 作者将 CycleMLP 中的 Cycle-FC 替换为 Spacial-FC 或者 Channel-FC,结果发现 CycleMLP 具有更好的性能。但是只有 Channel-FC,也能达到 79.4% 的性能,真的这么高吗,比 ResNet 高那么多…
请添加图片描述
  • Cycle-FC 中三条支路的选择:Cycle-FC 中作者并行了三条支路,对他们的消融实验发现,同时拥有正交 H 和 W 方向效果很好,加上不动之后效果更好。两倍 H 方向或者两倍 W 方向比仅含有 H 或者 W 方向会好一些。
请添加图片描述
  • 测试分辨率的影响:最终发现测试正确率随分辨率先升后降,CycleMLP 表现最好。
请添加图片描述

4. 总结与反思

CycleMLP 提出了 Cycle-FC,即将不同 token 的特征对齐到同一个通道,然后使用通道映射,从而实现网络参数量计算量的降低,以及对图像分辨率不敏感。CycleMLP 也在下游任务上测试了自己的性能表现。整体而言做得还是很充分的。不过其试图造一些新的名词以强化贡献,例如 Cycle-FC 其实就是移动特征图,Pseudo-Kernel 其实就是卷积核感受野的概念。最终 CycleMLP 通过三条并行的支路构建了十字形感受野。相比 AS-MLP,CycleMLP 在感受野分析上略显不足,没有更泛化地分析以及进行消融实验。比如 CycleMLP 也可以间隔采样,例如 [+4 +2 0 -2 -4 -2 0 2 4 2 0 -2 …],就可以构建 AS-MLP 那种空洞的更大范围的感受野。(最后插一句:CycleMLP 和 AS-MLP,就像 ResMLP 与 MLP-Mixer,学术界的 Idea 真的能够这么惊人的一致吗?)