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。

Docker

资源: Docker 从入门到实践

Docker: https://www.docker.com

1、什么是Docker?

容器技术的起源

假设你们公司正在秘密研发下一个“今日头条”APP,我们姑且称为明日头条,程序员自己从头到尾搭建了一套环境开始写代码,写完代码后程序员要把代码交给测试同学测试,这时测试同学开始从头到尾搭建这套环境,测试过程中出现问题程序员也不用担心,大可以一脸无辜的撒娇,“明明在人家的环境上可以运行的”。

测试同学测完后终于可以上线了,这时运维同学又要重新从头到尾搭建这套环境,费了九牛二虎之力搭建好环境开始上线,糟糕,上线系统就崩溃了,这时心理素质好的程序员又可以施展演技了,“明明在人家的环境上可以运行的”。

从整个过程可以看到,不但我们重复搭建了三套环境还要迫使程序员转行演员浪费表演才华,典型的浪费时间和效率,聪明的程序员是永远不会满足现状的,因此又到了程序员改变世界的时候了,容器技术应运而生。

有的同学可能会说:“等等,先别改变世界,我们有虚拟机啊,VMware好用的飞起,先搭好一套虚拟机环境然后给测试和运维clone出来不就可以了吗?”

在没有容器技术之前,这确实是一个好办法,只不过这个办法还没有那么好。

先科普一下,现在云计算其底层的基石就是虚拟机技术,云计算厂商买回来一堆硬件搭建好数据中心后使用虚拟机技术就可以将硬件资源进行切分了,比如可以切分出100台虚拟机,这样就可以卖给很多用户了。

你可能会想这个办法为什么不好呢?

容器技术 vs 虚拟机

我们知道和一个单纯的应用程序相比,操作系统是一个很重而且很笨的程序,简称笨重,有多笨重呢?

我们知道操作系统运行起来是需要占用很多资源的,大家对此肯定深有体会,刚装好的系统还什么都没有部署,单纯的操作系统其磁盘占用至少几十G起步,内存要几个G起步。

假设我有一台机器,16G内存,需要部署三个应用,那么使用虚拟机技术可以这样划分:

在这台机器上开启三个虚拟机,每个虚拟机上部署一个应用,其中VM1占用2G内存,VM2占用1G内存,VM3占用了4G内存。

我们可以看到虚拟本身就占据了总共7G内存,因此我们没有办法划分出更过虚拟机从而部署更多的应用程序,可是我们部署的是应用程序,要用的也是应用程序而不是操作系统

如果有一种技术可以让我们避免把内存浪费在“无用”的操作系统上岂不是太香?这是问题一,主要原因在于操作系统太重了。

还有另一个问题,那就是启动时间问题,我们知道操作系统重启是非常慢的,因为操作系统要从头到尾把该检测的都检测了该加载的都加载上,这个过程非常缓慢,动辄数分钟,因此操作系统还是太笨了。

那么有没有一种技术可以让我们获得虚拟机的好处又能克服这些缺点从而一举实现鱼和熊掌的兼得呢?

答案是肯定的,这就是容器技术。

什么是容器

容器一词的英文是container,其实container还有集装箱的意思,集装箱绝对是商业史上了不起的一项发明,大大降低了海洋贸易运输成本。让我们来看看集装箱的好处:

  • 集装箱之间相互隔离
  • 长期反复使用
  • 快速装载和卸载
  • 规格标准,在港口和船上都可以摆放

回到软件中的容器,其实容器和集装箱在概念上是很相似的。

现代软件开发的一大目的就是隔离,应用程序在运行时相互独立互不干扰,这种隔离实现起来是很不容易的,其中一种解决方案就是上面提到的虚拟机技术,通过将应用程序部署在不同的虚拟机中从而实现隔离。

但是虚拟机技术有上述提到的各种缺点,那么容器技术又怎么样呢?

与虚拟机通过操作系统实现隔离不同,容器技术只隔离应用程序的运行时环境但容器之间可以共享同一个操作系统,这里的运行时环境指的是程序运行依赖的各种库以及配置。

从图中我们可以看到容器更加的轻量级且占用的资源更少,与操作系统动辄几G的内存占用相比,容器技术只需数M空间,因此我们可以在同样规格的硬件上大量部署容器,这是虚拟机所不能比拟的,而且不同于操作系统数分钟的启动时间容器几乎瞬时启动,容器技术为打包服务栈提供了一种更加高效的方式,So cool。

那么我们该怎么使用容器呢?这就要讲到docker了。

注意,容器是一种通用技术,docker只是其中的一种实现。

什么是docker

docker是一个用Go语言实现的开源项目,可以让我们方便的创建和使用容器,docker将程序以及程序所有的依赖都打包到docker container,这样你的程序可以在任何环境都会有一致的表现,这里程序运行的依赖也就是容器就好比集装箱,容器所处的操作系统环境就好比货船或港口,程序的表现只和集装箱有关系(容器),和集装箱放在哪个货船或者哪个港口(操作系统)没有关系

Docker 的主要用途,目前有三大类。

(1)提供一次性的环境。比如,本地测试他人的软件、持续集成的时候提供单元测试和构建的环境。

(2)提供弹性的云服务。因为 Docker 容器可以随开随关,很适合动态扩容和缩容。

(3)组建微服务架构。通过多个容器,一台机器可以跑多个服务,因此在本机就可以模拟出微服务架构

因此我们可以看到docker可以屏蔽环境差异,也就是说,只要你的程序打包到了docker中,那么无论运行在什么环境下程序的行为都是一致的,程序员再也无法施展表演才华了,不会再有“在我的环境上可以运行”,真正实现“build once, run everywhere”。

此外docker的另一个好处就是快速部署,这是当前互联网公司最常见的一个应用场景,一个原因在于容器启动速度非常快,另一个原因在于只要确保一个容器中的程序正确运行,那么你就能确信无论在生产环境部署多少都能正确运行。

如何使用docker

docker中有这样几个概念:

  • dockerfile
  • image
  • container

实际上你可以简单的把image理解为可执行程序,container就是运行起来的进程。

那么写程序需要源代码,那么“写”image就需要dockerfile,dockerfile就是image的源代码,docker就是”编译器”。

因此我们只需要在dockerfile中指定需要哪些程序、依赖什么样的配置,之后把dockerfile交给“编译器”docker进行“编译”,也就是docker build命令,生成的可执行程序就是image,之后就可以运行这个image了,这就是docker run命令,image运行起来后就是docker container。

接下来我们用几个命令来讲解一下docker的工作流程:

1,docker build

当我们写完dockerfile交给docker“编译”时使用这个命令,那么client在接收到请求后转发给docker daemon,接着docker daemon根据dockerfile创建出“可执行程序”image。

2,docker run

有了“可执行程序”image后就可以运行程序了,接下来使用命令docker run,docker daemon接收到该命令后找到具体的image,然后加载到内存开始执行,image执行起来就是所谓的container。

3,docker pull

其实docker build和docker run是两个最核心的命令,会用这两个命令基本上docker就可以用起来了,剩下的就是一些补充。

那么docker pull是什么意思呢?

我们之前说过,docker中image的概念就类似于“可执行程序”,我们可以从哪里下载到别人写好的应用程序呢?很简单,那就是APP Store,即应用商店。与之类似,既然image也是一种“可执行程序”,那么有没有”Docker Image Store”呢?答案是肯定的,这就是Docker Hub,docker官方的“应用商店”,你可以在这里下载到别人编写好的image,这样你就不用自己编写dockerfile了。

docker registry 可以用来存放各种image,公共的可以供任何人下载image的仓库就是docker Hub。那么该怎么从Docker Hub中下载image呢,就是这里的docker pull命令了。

因此,这个命令的实现也很简单,那就是用户通过docker client发送命令,docker daemon接收到命令后向docker registry发送image下载请求,下载后存放在本地,这样我们就可以使用image了。

最后,让我们来看一下docker的底层实现。

docker的底层实现

docker基于Linux内核提供这样几项功能实现的:

  • NameSpace
    我们知道Linux中的PID、IPC、网络等资源是全局的,而NameSpace机制是一种资源隔离方案,在该机制下这些资源就不再是全局的了,而是属于某个特定的NameSpace,各个NameSpace下的资源互不干扰,这就使得每个NameSpace看上去就像一个独立的操作系统一样,但是只有NameSpace是不够。
  • Control groups
    虽然有了NameSpace技术可以实现资源隔离,但进程还是可以不受控的访问系统资源,比如CPU、内存、磁盘、网络等,为了控制容器中进程对资源的访问,Docker采用control groups技术(也就是cgroup),有了cgroup就可以控制容器中进程对系统资源的消耗了,比如你可以限制某个容器使用内存的上限、可以在哪些CPU上运行等等。

有了这两项技术,容器看起来就真的像是独立的操作系统了。

2、Docker使用教程

2.1 Docker 的安装

Docker 是一个开源的商业产品,有两个版本:社区版(Community Edition,缩写为 CE)和企业版(Enterprise Edition,缩写为 EE)。企业版包含了一些收费服务,个人开发者一般用不到。下面的介绍都针对社区版。

Docker CE 的安装请参考官方文档。

安装完成后,运行下面的命令,验证是否安装成功。


$ docker version
# 或者
$ docker info

Docker 需要用户具有 sudo 权限,为了避免每次命令都输入sudo,可以把用户加入 Docker 用户组(官方文档)。


$ sudo usermod -aG docker $USER

Docker 是服务器—-客户端架构。命令行运行docker命令的时候,需要本机有 Docker 服务。如果这项服务没有启动,可以用下面的命令启动(官方文档)。


# service 命令的用法
$ sudo service docker start

# systemctl 命令的用法
$ sudo systemctl start docker

2.2 image 文件

Docker 把应用程序及其依赖,打包在 image 文件里面。只有通过这个文件,才能生成 Docker 容器。image 文件可以看作是容器的模板。Docker 根据 image 文件生成容器的实例。同一个 image 文件,可以生成多个同时运行的容器实例。

image 是二进制文件。实际开发中,一个 image 文件往往通过继承另一个 image 文件,加上一些个性化设置而生成。举例来说,你可以在 Ubuntu 的 image 基础上,往里面加入 Apache 服务器,形成你的 image。


# 列出本机的所有 image 文件。
$ docker image ls

# 删除 image 文件
$ docker image rm [imageName]

image 文件是通用的,一台机器的 image 文件拷贝到另一台机器,照样可以使用。一般来说,为了节省时间,我们应该尽量使用别人制作好的 image 文件,而不是自己制作。即使要定制,也应该基于别人的 image 文件进行加工,而不是从零开始制作。

为了方便共享,image 文件制作完成后,可以上传到网上的仓库。Docker 的官方仓库 Docker Hub 是最重要、最常用的 image 仓库。此外,出售自己制作的 image 文件也是可以的。

2.3 实例:hello world

下面,我们通过最简单的 image 文件”hello world”,感受一下 Docker。

需要说明的是,国内连接 Docker 的官方仓库很慢,还会断线,需要将默认仓库改成国内的镜像网站。

首先,运行下面的命令,将 image 文件从仓库抓取到本地。


$ docker image pull library/hello-world

上面代码中,docker image pull是抓取 image 文件的命令。library/hello-world是 image 文件在仓库里面的位置,其中library是 image 文件所在的组,hello-world是 image 文件的名字。

由于 Docker 官方提供的 image 文件,都放在library组里面,所以它的是默认组,可以省略。因此,上面的命令可以写成下面这样。


$ docker image pull hello-world

抓取成功以后,就可以在本机看到这个 image 文件了。


$ docker image ls

现在,运行这个 image 文件。


$ docker container run hello-world

docker container run命令会从 image 文件,生成一个正在运行的容器实例。

注意,docker container run命令具有自动抓取 image 文件的功能。如果发现本地没有指定的 image 文件,就会从仓库自动抓取。因此,前面的docker image pull命令并不是必需的步骤。

如果运行成功,你会在屏幕上读到下面的输出。


$ docker container run hello-world

Hello from Docker!
This message shows that your installation appears to be working correctly.

... ...

输出这段提示以后,hello world就会停止运行,容器自动终止。

有些容器不会自动终止,因为提供的是服务。比如,安装运行 Ubuntu 的 image,就可以在命令行体验 Ubuntu 系统。


$ docker container run -it ubuntu bash

对于那些不会自动终止的容器,必须使用docker container kill 命令手动终止。


$ docker container kill [containID]

2.4 Dockerfile 文件

学会使用 image 文件以后,接下来的问题就是,如何可以生成 image 文件?如果你要推广自己的软件,势必要自己制作 image 文件。

这就需要用到 Dockerfile 文件。它是一个文本文件,用来配置 image。Docker 根据 该文件生成二进制的 image 文件。

下面通过一个实例,演示如何编写 Dockerfile 文件。

2.5 制作自己的 Docker 容器

下面我以 koa-demos 项目为例,介绍怎么写 Dockerfile 文件,实现让用户在 Docker 容器里面运行 Koa 框架。

作为准备工作,请先下载源码


$ git clone https://github.com/ruanyf/koa-demos.git
$ cd koa-demos

1 编写 Dockerfile 文件

首先,在项目的根目录下,新建一个文本文件.dockerignore,写入下面的内容


.git
node_modules
npm-debug.log

上面代码表示,这三个路径要排除,不要打包进入 image 文件。如果你没有路径要排除,这个文件可以不新建。

然后,在项目的根目录下,新建一个文本文件 Dockerfile,写入下面的内容


FROM node:8.4
COPY . /app
WORKDIR /app
RUN npm install --registry=https://registry.npm.taobao.org
EXPOSE 3000

上面代码一共五行,含义如下。

  • FROM node:8.4:该 image 文件继承官方的 node image,冒号表示标签,这里标签是8.4,即8.4版本的 node。
  • COPY . /app:将当前目录下的所有文件(除了.dockerignore排除的路径),都拷贝进入 image 文件的/app目录。
  • WORKDIR /app:指定接下来的工作路径为/app
  • RUN npm install:在/app目录下,运行npm install命令安装依赖。注意,安装后所有的依赖,都将打包进入 image 文件。
  • EXPOSE 3000:将容器 3000 端口暴露出来, 允许外部连接这个端口。

2 创建 image 文件

有了 Dockerfile 文件以后,就可以使用docker image build命令创建 image 文件了。


$ docker image build -t koa-demo .
# 或者
$ docker image build -t koa-demo:0.0.1 .

上面代码中,-t参数用来指定 image 文件的名字,后面还可以用冒号指定标签。如果不指定,默认的标签就是latest。最后的那个点表示 Dockerfile 文件所在的路径,上例是当前路径,所以是一个点。

如果运行成功,就可以看到新生成的 image 文件koa-demo了。


$ docker image ls

3 生成容器

docker container run命令会从 image 文件生成容器。


$ docker container run -p 8000:3000 -it koa-demo /bin/bash
# 或者
$ docker container run -p 8000:3000 -it koa-demo:0.0.1 /bin/bash

上面命令的各个参数含义如下:

  • -p参数:容器的 3000 端口映射到本机的 8000 端口。
  • -it参数:容器的 Shell 映射到当前的 Shell,然后你在本机窗口输入的命令,就会传入容器。
  • koa-demo:0.0.1:image 文件的名字(如果有标签,还需要提供标签,默认是 latest 标签)。
  • /bin/bash:容器启动以后,内部第一个执行的命令。这里是启动 Bash,保证用户可以使用 Shell。

如果一切正常,运行上面的命令以后,就会返回一个命令行提示符。


root@66d80f4aaf1e:/app#

这表示你已经在容器里面了,返回的提示符就是容器内部的 Shell 提示符。执行下面的命令。


root@66d80f4aaf1e:/app# node demos/01.js

这时,Koa 框架已经运行起来了。打开本机的浏览器,访问 http://127.0.0.1:8000,网页显示”Not Found”,这是因为这个 demo 没有写路由。

这个例子中,Node 进程运行在 Docker 容器的虚拟环境里面,进程接触到的文件系统和网络接口都是虚拟的,与本机的文件系统和网络接口是隔离的,因此需要定义容器与物理机的端口映射(map)。

现在,在容器的命令行,按下 Ctrl + c 停止 Node 进程,然后按下 Ctrl + d (或者输入 exit)退出容器。此外,也可以用docker container kill终止容器运行。


# 在本机的另一个终端窗口,查出容器的 ID
$ docker container ls

# 停止指定的容器运行
$ docker container kill [containerID]

容器停止运行之后,并不会消失,用下面的命令删除容器文件。


# 查出容器的 ID
$ docker container ls --all

# 删除指定的容器文件
$ docker container rm [containerID]

也可以使用docker container run命令的--rm参数,在容器终止运行后自动删除容器文件。


$ docker container run --rm -p 8000:3000 -it koa-demo /bin/bash

4 CMD 命令

上一节的例子里面,容器启动以后,需要手动输入命令node demos/01.js。我们可以把这个命令写在 Dockerfile 里面,这样容器启动以后,这个命令就已经执行了,不用再手动输入了。


FROM node:8.4
COPY . /app
WORKDIR /app
RUN npm install --registry=https://registry.npm.taobao.org
EXPOSE 3000
CMD node demos/01.js

上面的 Dockerfile 里面,多了最后一行CMD node demos/01.js,它表示容器启动后自动执行node demos/01.js

你可能会问,RUN命令与CMD命令的区别在哪里?简单说,RUN命令在 image 文件的构建阶段执行,执行结果都会打包进入 image 文件;CMD命令则是在容器启动后执行。另外,一个 Dockerfile 可以包含多个RUN命令,但是只能有一个CMD命令。

注意,指定了CMD命令以后,docker container run命令就不能附加命令了(比如前面的/bin/bash),否则它会覆盖CMD命令。现在,启动容器可以使用下面的命令。


$ docker container run --rm -p 8000:3000 -it koa-demo:0.0.1

5 发布 image 文件

容器运行成功后,就确认了 image 文件的有效性。这时,我们就可以考虑把 image 文件分享到网上,让其他人使用。

首先,去 hub.docker.com 或 cloud.docker.com 注册一个账户。然后,用下面的命令登录。


$ docker login

接着,为本地的 image 标注用户名和版本。


$ docker image tag [imageName] [username]/[repository]:[tag]
# 实例
$ docker image tag koa-demos:0.0.1 ruanyf/koa-demos:0.0.1

也可以不标注用户名,重新构建一下 image 文件。


$ docker image build -t [username]/[repository]:[tag] .

最后,发布 image 文件。


$ docker image push [username]/[repository]:[tag]

发布成功以后,登录 hub.docker.com,就可以看到已经发布的 image 文件。

2.6 基于已有的image发布新的image

  • 将容器里面运行的程序及运行环境打包生成新的镜像
docker commit [选项] 容器ID/名称 仓库名称:[标签]
-m:说明信息
-a:作者信息
-p:生成过程中停止容器的运行

其他有用的命令

docker 的主要用法就是上面这些,此外还有几个命令,也非常有用。

(1)docker container start

前面的docker container run命令是新建容器,每运行一次,就会新建一个容器。同样的命令运行两次,就会生成两个一模一样的容器文件。如果希望重复使用容器,就要使用docker container start命令,它用来启动已经生成、已经停止运行的容器文件。


$ docker container start [containerID]

(2)docker container stop

前面的docker container kill命令终止容器运行,相当于向容器里面的主进程发出 SIGKILL 信号。而docker container stop命令也是用来终止容器运行,相当于向容器里面的主进程发出 SIGTERM 信号,然后过一段时间再发出 SIGKILL 信号。


$ docker container stop [containerID]

这两个信号的差别是,应用程序收到 SIGTERM 信号以后,可以自行进行收尾清理工作,但也可以不理会这个信号。如果收到 SIGKILL 信号,就会强行立即终止,那些正在进行中的操作会全部丢失。

(3)docker container logs

docker container logs命令用来查看 docker 容器的输出,即容器里面 Shell 的标准输出。如果docker run命令运行容器的时候,没有使用-it参数,就要用这个命令查看输出。


$ docker container logs [containerID]

(4)docker container exec

docker container exec命令用于进入一个正在运行的 docker 容器。如果docker run命令运行容器的时候,没有使用-it参数,就要用这个命令进入容器。一旦进入了容器,就可以在容器的 Shell 执行命令了。


$ docker container exec -it [containerID] /bin/bash

(5)docker container cp

docker container cp命令用于从正在运行的 Docker 容器里面,将文件拷贝到本机。下面是拷贝到当前目录的写法。


$ docker container cp [containID]:[/path/to/file] .

使用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