Life sucks, but you're gonna love it.

0%

环境: Mac os linux

编译器:vscode

管理包:brew

安装包:

  • opencv
  • eigen
  • g2o
  • sophus

主要步骤

  1. opencv

    1
    2
    3
    4
    5
    6
    brew search opencv #可以搜索brew可以安装的opencv的version

    brew install opencv@2 #选择要安装的opencv

    brew info opencv #检查安装结果,若某个依赖包的后面没有✅,可以直接brew install安装这个包

    • 安装完成之后会出现以下的提示,记得需要分别执行,这样可以把opencv的头文件和lib放到相应的位置以便随后配置

      If you need to have opencv@2 first in your PATH run:
      echo ‘export PATH=”/usr/local/opt/opencv@2/bin:$PATH”‘ >> ~/.zshrc

      For compilers to find opencv@2 you may need to set:
      export LDFLAGS=”-L/usr/local/opt/opencv@2/lib”
      export CPPFLAGS=”-I/usr/local/opt/opencv@2/include”

      For pkg-config to find opencv@2 you may need to set:
      export PKG_CONFIG_PATH=”/usr/local/opt/opencv@2/lib/pkgconfig”

    • opencv@2 和 opencv@3有一些包有区别,所以有时候编译找不到其中一个包可以看一下版本是否有误

    这样安装好的opencv的include的内容是在 /usr/local/opt/opencv@2/include

    而link library是在当前文件夹 /usr/local/opt/opencv@2/lib

  2. eigen

    1
    2
    brew install eigen

    这样安装好的opencv的include的内容是在 /usr/local/opt/eigen/include

    而link library是在当前文件夹 /usr/local/opt/eigen/lib

  3. Sophus

    https://github.com/strasdat/Sophus

    Sophus 是 应用在二维和三维问题中的李群方法的一个包,可以直接git clone下载然后按照网页提示步骤安装。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    git clone https://github.com/strasdat/Sophus.git #下载包到本地

    cd Sophus #打开文件夹

    mkdir build #创建build文件夹用于存放makefile

    cmake .. #cmake上级的 cmakelist.txt

    make #运行上一步在build文件夹中生成的makefile

    sudo install make #在 /usr/local/include 中安装sophus的头文件之类的文件

    这样安装好的sophus的include的内容是在 /usr/local/include/sophus

    而link library是在当前文件夹 ../sophus (就一个lib文件)

    • cmake .. 这一步下,可能会出现以下错误:

      1
      2
      3
      /Users/wangcaimeng/Sophus/sophus/so2.cpp:32:24: error: expression is not assignable unit_complex_.real() = 1.;
      /Users/wangcaimeng/Sophus/sophus/so2.cpp:33:24: error: expression is not assignable unit_complex_.imag() = 0.;
      ~~~~~~~~~~~~~~~~~~~~ ^ 2 errors generated. make[2]: *** [CMakeFiles/Sophus.dir/sophus/so2.cpp.o] Error 1 make[1]: *** [CMakeFiles/Sophus.dir/all] Error 2 make: *** [all] Error 2

      解决方案是:

      1
      2
      3
      4
      5
      将 so2.cpp 文件的32和33行改为:

      unit_complex_.real(1.);
      unit_complex_.imag(0.);
      参考自:https://github.com/gaoxiang12/slambook/issues/56#issuecomment-334946067
  4. g2o

    https://github.com/RainerKuemmerle/g2o

    g2o是brewsci中的一个用于非线性优化的包,在官方git的readme.md文件中,提及到macos系统可以直接用

    1
    brew install brewsci/science/g2o

    来安装相关的包,但是我在安装的的过程中会涉及到 Cannot tap brewsci/science 的报错信息,这个问题没有查到明确的解法,所以就按照 sophus相同的办法安装了g2o。在官方的g2o文件的Compilation 部分也有具体提到安装的步骤:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    git clone https://github.com/RainerKuemmerle/g2o.git

    cd g2o

    mkdir build

    cmake ..

    make

    sudo install make #在 /usr/local/include 中安装g2o的头文件之类的文件

    这样安装好的g2o的include的内容是在 /usr/local/include/g2o

    而link library是在当前文件夹 ../g2o/lib

由此可得,基于brew install 安装的包都比较统一地把include文件和lib文件放在 /usr/local/opt/opencv@2/include/usr/local/opt/opencv@2/lib中,但是直接从git下载并且需要cmake的包需要自行确定include和lib文件夹在哪里。

如果在安装过后都使用了 sudo install make 那么include的包会被安装在 /usr/local/include/

vscode 配置

vscode的配置文件又四个,分别是: launch.json, tasks.json, settngs.json, c_cpp_properties.json

  1. lauch.json

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    {
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
    {
    "name": "(lldb) Launch",
    "type": "cppdbg",
    "request": "launch",
    "program": "${fileDirname}/a.out",
    "args": [],
    "stopAtEntry": true,
    "cwd": "${workspaceFolder}",
    "environment": [],
    "externalConsole": false,
    "MIMode": "lldb"
    }
    ]
    }
  2. settings.json

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    //settings.json
    {
    "python.pythonPath": "usr/bin/python",
    "code-runner.executorMap": {
    "c": "cd $dir && make && ./$fileNameWithoutExt && make clean",
    "cpp": "cd $dir && make && ./$fileNameWithoutExt && make clean"
    },
    "editor.renderWhitespace": "all",
    "editor.renderLineHighlight": "all",
    "editor.formatOnSave": true,
    "code-runner.runInTerminal": true,
    "code-runner.ignoreSelection": true,
    "code-runner.enableAppInsights": false,
    "C_Cpp.updateChannel": "Insiders",
    "[makefile]": {
    "editor.insertSpaces": true
    },
    "C_Cpp.default.includePath": [
    "/usr/local/Cellar/opencv@2/2.4.13.7_11/include"
    ]
    }
  3. tasks.json

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    {
    "tasks": [
    {
    "type": "shell",
    "label": "Build",
    "command": "g++",
    "args": [
    //"main.cpp",
    "${file}",
    "-o",
    "${fileDirname}/${fileBasenameNoExtension}.out",
    "-I",
    "/usr/local/Cellar/opencv@2/2.4.13.7_11/include/opencv",
    "-I",
    "/usr/local/Cellar/opencv@2/2.4.13.7_11/include",
    "-L",
    "/usr/local/Cellar/opencv@2/2.4.13.7_11/lib",
    "-l",
    "opencv_stitching",
    "-l",
    "opencv_superres",
    "-l",
    "opencv_videostab",
    "-l",
    "opencv_aruco",
    "-l",
    "opencv_bgsegm",
    "-l",
    "opencv_bioinspired",
    "-l",
    "opencv_ccalib",
    "-l",
    "opencv_dnn_objdetect",
    "-l",
    "opencv_dpm",
    "-l",
    "opencv_face",
    "-l",
    "opencv_fuzzy",
    "-l",
    "opencv_hfs",
    "-l",
    "opencv_img_hash",
    "-l",
    "opencv_line_descriptor",
    "-l",
    "opencv_optflow",
    "-l",
    "opencv_reg",
    "-l",
    "opencv_rgbd",
    "-l",
    "opencv_saliency",
    "-l",
    "opencv_stereo",
    "-l",
    "opencv_structured_light",
    "-l",
    "opencv_phase_unwrapping",
    "-l",
    "opencv_surface_matching",
    "-l",
    "opencv_tracking",
    "-l",
    "opencv_datasets",
    "-l",
    "opencv_dnn",
    "-l",
    "opencv_plot",
    "-l",
    "opencv_xfeatures2d",
    "-l",
    "opencv_shape",
    "-l",
    "opencv_video",
    "-l",
    "opencv_ml",
    "-l",
    "opencv_ximgproc",
    "-l",
    "opencv_xobjdetect",
    "-l",
    "opencv_objdetect",
    "-l",
    "opencv_calib3d",
    "-l",
    "opencv_features2d",
    "-l",
    "opencv_highgui",
    "-l",
    "opencv_videoio",
    "-l",
    "opencv_imgcodecs",
    "-l",
    "opencv_flann",
    "-l",
    "opencv_xphoto",
    "-l",
    "opencv_photo",
    "-l",
    "opencv_imgproc",
    "-l",
    "opencv_core",
    "-g"

    ],

    "group": {
    "kind": "build",
    "isDefault": true
    },
    "problemMatcher": [
    "$gcc"
    ]
    }
    ],
    "version": "2.0.0"
    }
  4. c_cpp_properties.json

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    {
    "configurations": [
    {
    "name": "Mac",
    "includePath": [
    "${workspaceFolder}/**",
    "/usr/local/opt/opencv@2/include",
    "/usr/local/Cellar/opencv@2/2.4.13.7_11/include"
    ],
    "defines": [],
    "macFrameworkPath": [],
    "compilerPath": "/usr/bin/g++",
    "cStandard": "c11",
    "cppStandard": "c++11",
    "intelliSenseMode": "clang-x64",
    "browse": {
    "path": [
    "/usr/local/Cellar/opencv@2/2.4.13.7_11/include"
    ],
    "limitSymbolsToIncludedHeaders": true,
    "databaseFilename": ""
    }
    }
    ],
    "version": 4
    }

cmakelist 配置

cmake中主要配置的是 INCLUDE_DIRECTORIES , link_directoryies

注意着两部分的路径药分别和之前安装的不同的包的路径相匹配,这样就可以运行cmake进行编译了

WK 1 | Orientation - Write a c++program

WK 2 | Understanding the C++ memory

stack memory

普通声明变量的时候占用的是stack memory

顺序是从位置最高的地方开始存,按顺序依次减小

heap memory

需要用关键字new来进行生命

顺序是从最低的位置开始从,按顺序依次增高

没有东西存的时候位置是空

Python

pdb - python 自带调试器

1
2
3
4
5
6
7
8
import pdb
pdb.set_tract() #添加断点

#运行之后会出现 (pdb)然后输入值
运行下一行 - n
打印 - p 变量名1 变量名2
退出调试 - q
添加动态断点 - b 添加断点行数 ex. b 23

Paddle Paddle

快速入门

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import paddle.fluid
import numpy as np


# 定义两个constant张量
x1 = fluid.layers.fill_constant(shape=[2, 2], value=1, dtype='int64')
x2 = fluid.layers.fill_constant(shape=[2, 2], value=1, dtype='int64')
# 将两个张量求和
y1 = fluid.layers.sum(x=[x1, x2])

#定义两层的place holder为张量变量 variable name / tensor name
a = fluid.layers.create_tensor(dtype='int64', name='c')
b = fluid.layers.create_tensor(dtype='int64', name='d')

#定义y为两层之和
y = fluid.layers.sum(x=[a, b])

# 创建一个使用CPU的解释器
place = fluid.CPUPlace()
exe = fluid.executor.Executor(place)
# 进行参数初始化
exe.run(fluid.default_startup_program())

# 创建一个使用CPU的解释器
place = fluid.CPUPlace()
exe = fluid.executor.Executor(place)
# 进行参数初始化
exe.run(fluid.default_startup_program())

# 定义两个要计算的变量
a1 = np.array([3, 2]).astype('int64')
b1 = np.array([1, 1]).astype('int64')

# 进行运算,并把y的结果输出
# feed进去的需要标明是 层名字:变量名
# fetch list是层的variable name
out_a, out_b, result = exe.run(program=fluid.default_main_program(),
feed={'c': a1, 'd': b1},
fetch_list=[a, b, y])
print(out_a, " + ", out_b," = ", result)


定义简单网络

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
import paddle.fluid as fluid
import paddle
import numpy as np
import matplotlib.pyplot as plt
import os

BUF_SIZE = 500
BATCH_SIZE = 20

# preapare data
trainer_reader = paddle.batch(paddle.reader.shuffle(paddle.uci.housing.train()), buf_size=BUF_SIZE)
test_reader = paddle.batch(paddle.reader.shuffle(paddle.uci.housing.test()), buf_size=BUF_SIZE)

# build net
x = fluid.layers.data(name='x', shape=[13], dtype='float32')
y_predict=fluid.layers.fc(input=x,size=1,act=None) #定义一个简单的线性网络,连接输入和输出的全连接层

y = fluid.layers.data(name='y', shape=[1], dtype='float32') #定义张量y,表示目标值

# define cost function
cost = fluid.layers.square_error_cost(input=y_predict, label=y) #求一个batch的损失值
avg_cost = fluid.layers.mean(cost) #对损失值求平均值

# define optimization function
optimizer = fluid.optimizer.SGDOptimizer(learning_rate=0.001)
opts = optimizer.minimize(avg_cost)
test_program = fluid.default_main_program().clone(for_test=True)

# model training

# build exector
use_cuda = False #use_cuda为False,表示运算场所为CPU;use_cuda为True,表示运算场所为GPU
place = fluid.CUDAPlace(0) if use_cuda else fluid.CPUPlace()
exe = fluid.Executor(place) #创建一个Executor实例exe
exe.run(fluid.default_startup_program()) #Executor的run()方法执行startup_program(),进行参数初始化

feeder = fluid.DataFeeder(place=place, feed_list=[x, y])#feed_list:向模型输入的变量表或变量表名

iter=0;
iters=[]
train_costs=[]

def draw_train_process(iters,train_costs):
title="training cost"
plt.title(title, fontsize=24)
plt.xlabel("iter", fontsize=14)
plt.ylabel("cost", fontsize=14)
plt.plot(iters, train_costs,color='red',label='training cost')
plt.grid()
plt.show()

# save model
EPOCH_NUM=50
model_save_dir = "/home/aistudio/work/fit_a_line.inference.model"

for pass_id in range(EPOCH_NUM): #训练EPOCH_NUM轮
# 开始训练并输出最后一个batch的损失值
train_cost = 0
for batch_id, data in enumerate(train_reader()): #遍历train_reader迭代器
train_cost = exe.run(program=fluid.default_main_program(),#运行主程序
feed=feeder.feed(data), #喂入一个batch的训练数据,根据feed_list和data提供的信息,将输入数据转成一种特殊的数据结构
fetch_list=[avg_cost])
if batch_id % 40 == 0:
print("Pass:%d, Cost:%0.5f" % (pass_id, train_cost[0][0])) #打印最后一个batch的损失值
iter=iter+BATCH_SIZE
iters.append(iter)
train_costs.append(train_cost[0][0])


# 开始测试并输出最后一个batch的损失值
test_cost = 0
for batch_id, data in enumerate(test_reader()): #遍历test_reader迭代器
test_cost= exe.run(program=test_program, #运行测试cheng
feed=feeder.feed(data), #喂入一个batch的测试数据
fetch_list=[avg_cost]) #fetch均方误差
print('Test:%d, Cost:%0.5f' % (pass_id, test_cost[0][0])) #打印最后一个batch的损失值

#保存模型
# 如果保存路径不存在就创建
if not os.path.exists(model_save_dir):
os.makedirs(model_save_dir)
print ('save models to %s' % (model_save_dir))
fluid.io.save_inference_model(model_save_dir, #保存推理model的路径
['x'], #推理(inference)需要 feed 的数据
[y_predict], #保存推理(inference)结果的 Variables
exe) #exe 保存 inference model
draw_train_process(iters,train_costs)


# model prediction

infer_exe = fluid.Executor(place) #创建推测用的executor
inference_scope = fluid.core.Scope() #Scope指定作用域

infer_results=[]
groud_truths=[]

#绘制真实值和预测值对比图
def draw_infer_result(groud_truths,infer_results):
title='Boston'
plt.title(title, fontsize=24)
x = np.arange(1,20)
y = x
plt.plot(x, y)
plt.xlabel('ground truth', fontsize=14)
plt.ylabel('infer result', fontsize=14)
plt.scatter(groud_truths, infer_results,color='green',label='training cost')
plt.grid()
plt.show()

with fluid.scope_guard(inference_scope):#修改全局/默认作用域(scope), 运行时中的所有变量都将分配给新的scope。
#从指定目录中加载 推理model(inference model)
[inference_program, #推理的program
feed_target_names, #需要在推理program中提供数据的变量名称
fetch_targets] = fluid.io.load_inference_model(#fetch_targets: 推断结果
model_save_dir, #model_save_dir:模型训练路径
infer_exe) #infer_exe: 预测用executor
#获取预测数据
infer_reader = paddle.batch(paddle.dataset.uci_housing.test(), #获取uci_housing的测试数据
batch_size=200) #从测试数据中读取一个大小为200的batch数据
#从test_reader中分割x
test_data = next(infer_reader())
test_x = np.array([data[0] for data in test_data]).astype("float32")
test_y= np.array([data[1] for data in test_data]).astype("float32")
results = infer_exe.run(inference_program, #预测模型
feed={feed_target_names[0]: np.array(test_x)}, #喂入要预测的x值
fetch_list=fetch_targets) #得到推测结果

print("infer results: (House Price)")
for idx, val in enumerate(results[0]):
print("%d: %.2f" % (idx, val))
infer_results.append(val)
print("ground truth:")
for idx, val in enumerate(test_y):
print("%d: %.2f" % (idx, val))
groud_truths.append(val)
draw_infer_result(groud_truths,infer_results)

Class 1 语义分割 intro

图像分割的类型

  1. Image Segmentation
  2. Image Semantic Segmentation - 给每个pixel分类
  3. Image Instance Segmentation - 检测相关,给每个object 分mask cat1 cat2
  4. Image Panomic Segmentation - 背景pixel分类+框里mask
  5. Video Object Segmentation - 通常会给定目标mask
  6. Video Instance Segmentation

语义分割算法的基本概念

语义分割算法的根本目的:像素级分类

语义分割算法的基本流程

  1. 输入:图像(rgb) - dataloader imread batchsize
  2. 算法:深度学习模型
  3. 输出:分类结果 (与输入大小一致的单通道图像)
  4. 训练过程:
    1. 输入: image + label
    2. 前向:out = model(inage)
    3. 计算损失
    4. 反向
    5. 更新权重

语义分割性能指标

  • mIoU:分割每一类别的交并比
  • mAcc:pred 和gt 对应位置的“分类”标准率,更趋近于各种不同灰度

为什么acc不够 还要有iou呢?忽略了很重要的一类

实践部分

basic net

basic DataLoader

Fully Convolutional Network. (FCN 全卷积网络)

为什么要用FCN

卷积神经网络首先被分类卷积网络 - 全卷积网络

变大feature map - 上采样/反卷积/unpooling

什么是fcn

双线插值 (bilinear interpolation)

Nchw - nch’w’ 图像放大:简单,快

un-pooling

transpose conv

如何建立一个fcn

这部分我们来看看这个经典结构ResNet首先被提出的文章。

Abstract

深度神经网络现在变得越来越难训练了。我们呈现了一种残差网络的框架来解决这个问题。我们重新搭建了网络模型,利用layer的input作为参考,学习残差方程,而不是直接学习没有参考的方程。

Introduction

深度卷积神经网络在图片分类上带来了很大的突破。深度神经网络综合了从低到高的feature和端到端分类器,以及可以通过扩大训练层数而增加的feature的个数。近期的神经网络展现出来的网络结构的深度有着至关重要的作用,并且在ImageNet分类上,分类效果好的神经网络的层数都是在16-30之间。有很多其他的视觉方面的任务同样也受益于深度的网络结构。

那么急于网络深度的重要性,我们不禁要问:训练更好的神经网络真得就如同叠加积木一样简单吗?一个摆在这个问题之前的障碍是梯度爆炸和梯度消失的问题,但是梯度爆炸和小时大部分可以被BN来解决。

【这里应该是说,再深的神经网络,如果出现了梯度消失的问题,我们大可以用归一化输入和归一化每一层的feature来解决。】

当更多层的神经网络开始收敛的时候,一个网络退化的问题就暴露了:随着网络深度的增加,准确率开始趋于饱和然后开始急速下降。但这种退化并不是由于过拟合造成的,添加更多的层数不仅会使测试的准确率降低,训练的准确率也会降低。

【这里举了一个例子,是一个20层网络和一个56层网络的例子,通过增加神经网络的层数(深度)我们得到的网络分类效果并没有变得很好,而是训练和测试准确率变得更差了,所以不是简单的过拟合的问题。】

更深度的神经网络在训练数据集上的效果退化说明,并不是所有的系统都很好优化。让我们来考虑一个浅层的结构和一个在这个浅层结构的基础上增加量更多层的结构。如果我们新添加的这些层只是做简单的对应变化,也就是新添加的几层网络什么都不做,只是单纯的复制浅层网络的输出,那么我的这个深层的网络怎么着误差也应该和浅层网络的误差一样,那如果后面新添加的几层学到了更多的特征,得到的结果应该比浅层网络得到的结果更好才对。但之前的实验显示,新增加的几层并不能很好的体现这个理论。

在这篇文章里,我们利用深度残差网络解决了这个退化的问题。我们这里利用残差map来代替利用几个叠加的layer拟合出来一个mapping的方法。直观来讲,如果我们将理想情况下的mapping方程记做$\mathcal H(x)$, 现在我们对非线性叠加层定义一个新的map $\mathcal F(\bf x) := \mathcal H(\bf x) - x$, 这样一来之前的map就变成了 $\mathcal F(\bf x) + \bf x$ 我们现在的假设是,优化得到残差mapping比得到之前的mapping更加容易。在极端的角度上来讲,如果identity mapping是最优解,那么得到残差mapping的0比得到identity mapping更加容易。

$\mathcal F(\bf x) + \bf x$ 的结构就通过短接得到。短接的意思就是跳过一层或多层,在我们的这个方法里,短接的直接是通过identity mapping,然后得到的结果直接连在之前正常输出的结果上。

Screen Shot 2020-07-24 at 21.54.23

我们在ImageNet数据上面证明了退化的问题以及测试了我们的网络。我们证明了:1. 在残差结构下的很深的深度网络还是很好被优化的,但是很深的只是简单叠加的深度网络的误差会随着层数的增加而增加; 2. 我们很深的残差网络可以在增长网络深度的条件下继续提高精度。

Deep Residual Learning

Residual Learning

让我们假设 $\mathcal H(\bf x)$ 是正常叠加一些卷积层(一个stack)的网络对应的underlying mapping. 这里的 $\bf x$ 代表的是通过这一小堆第一层的input。如果我们假设一系列的非线性层可以为我们得出复杂函数的估计,那么我们同样也可以用这一系列的非线性ceng

Selective Search

RCNN

Fast-RCNN

为了改进之前RCNN结构中,生成的conv feature需要通过SVM,造成大量的时间损耗。Girshick又改进了之前的网络结构,提出了Fast RCNN。

Fast RCNN是在SPP和RCNN的基础上提出的。

RCNN我们在上个部分已经提过了,那么SPP网络是什么呢?

  • SPP的全称是Spatial Parimid Pooling,主要是在不同的空间尺度上进行池化,提取不同空间尺度的特征,防止Proposal在进入RCNN网络时rescale到固定尺寸(224 x 224)时图形产生的形变和图片的变化。

  • SPP在使用的过程中是将全图划分成不同尺寸的grid(比如 1 x 1的grid的就是原图本身, 2x 2的grid是将原图分为4份,其中每一份cell的长宽都是原图长宽的一半), 然后将每一个grid生成的pooling之后的结果组合在一起,产生一个固定大小的feature作为SPP的输出。对于输入的每一个channel都会生成grid。

  • 举个例子,假如现在我们输入是一个 256 x 256 x 3 的feature

    1. 针对每一层,生成 1x1 的grid,然后做max pooling,得到一个 1 x 3 的feature

    2. 针对每一层,生成 2 x 2的grid,然后做max pooling,得到一个 4 x 3的feature

    3. 针对每一层,生成 4 x 4的grid,然后做max pooling,得到一个 16 x 3 的feature

    最后将这些feature拼接起来生成的整个feature是固定大小的输出。(因为我们grid的大小是固定的,所以如果输入feature的尺寸发生变化,那么只有grid中每个cell的大小会发生变化,但是gird中cell的个数是固定不变的,所以最后通过ROI pooling的生成结果的尺寸是不变的。)

那么对于Fast RCNN来说,它的输入是一张图片,以及一些proposal,这些proposal的大小各不相同,我们用(r, c, h, w) 来表示每一个proposal region。 r c表示左上角的坐标点,h w 表示这个框的长宽。

Fast RCNN的主要过程是,我们将原图通过特征提取网络,生成一个conv feature map,然后在这个map上面根据之前所说的proposal的信息,提取出proposal的位置,并对其进行ROI pooling。生成的ROI pooling是一个长度固定的feature,并且这里我们只用一次 H x W 的grid提取,也就是说通过ROI pooling之后生成的feature的长度是 H x W x d (这里d是输入ROI pooling之前的feature的维度)。生成的feature之后会通过两个网络,一个连接到softmax用来确定我这张图片上有哪些物体,另一个用来确定预测出的物体的bounding box是什么。

(receptive field为什么会更大)

Faster RCNN

目标检测的网络发展至此,还有一个重要的需要依靠外界实现的输入,那就是proposal

Faster RCNN的出现解决了需要依靠外界条件得到proposal region的情况,实际上Faster RCNN就是一个Region Proposal Network + Fast RCNN的网络结构。那么就主要介绍一下生成proposal的网络: RPN

RPN的主要目的就是寻找proposal,这时候我们就建立一个sliding window,然后以这个sliding window为中心,构建不同尺寸和长宽比的anchor来找到合适的proposal region。

训练RPN的时候,主要分为两个方面的cost,一方面是二维向量表示所圈到的proposal是否是一个物体,另一个是四维向量来表示所圈到的坐标信息。proposal是否为物体主要是根据proposal和target的IoU来判定,如果IoU大于0.7或者IoU是这些anchor里面最大的,那么这个anchor就被称作是positive的结果;反之如果IoU小于0.3就是negative的结果。bounding box的预测值是输入在regression网络中进行训练的。

文章中使用的anchor为9个。

但是使用RPN还存在一个问题就是因为我们的anchor的大小都是固定的,那么一个特别大或者特别小的物体就可能无法被估计(在感受野外)或者被找到。这时候可以通过训练不同的RPN在不同的尺度下进行proposal的估计,在这样产生的proposal的感受野也不同。

(感受野是怎么确定的)

MTCNN

MTCNN的全称是multi-task cascaded convolutional neural network,主要是用来进行人脸的detection的。

HOG + SVM

我们常常会选取一些图片中的特征来代表这个图片中的内容,图片中物体的edge是很有力的特征。这样来说,我们可以用canny找到图片中的物体的edge。但是找到的edge只是大小,并没有gradient的信息。所以在这里我们提取gradient,生成它的特征信息。

  1. 输入图片

  2. 对图片进行颜色和对比度的归一化处理

  3. 计算gradient:

    这一项稍微复杂一些,我们分开来说。

    • 首先将图片分割成一个个 $8 \times 8$ pixel大小的cell,这样的话,一个长度为height, 宽度为 width的图片能生成的cell的个数就是 $\frac{height}{8} \times \frac{width}{8}$.
    • 对于每一个cell中的每一个pixel都有我们计算的gradient的方向,将0-360划分为8个方向,然后将 $8 \times 8$ pixel中每一个pixel的方向都统计在这八个方向上,这样一来,我们的这64个pixel就可以保存为一个 $1\times 8$ 的histogram vector;
    • 由于 $8 \times 8$ pixel 的感受面积还是比较小,可能无法感知到光线的变化,所以我们进一步将一个 $2 \times 2$ 的cell当做一个block,这样,对于一个block中的每一个cell,他都有一个 $1 \times8$的vector,这样四个cell的histogram放在一起就是一个 $ 1 \times 32$ 的histogram vector。
    • 然后将这个$ 1 \times 32$ 的histogram vector进行归一化处理,就得到了这一张图的一个特征。对于整个图来讲,我们就得到了一个 $(\frac{height}{8} \times \frac{width}{8}\times32)$ 的特征。
  4. 我们将histogram vector拿出来作为图片是否是某种物体的特征,然后将这些特征放入一个SVM中,设置一个score,来判定是否有我们想要的物体。

Viola - Jones face detector (Harr+Adaboost)

  1. Harr-like feature
  2. Integral of image
  3. Adaboost
  4. Concatenate classifier

SIFT

  1. Space - Scale Extrema detection

    SIFT算子的第一步要先重视这里的scale invariant,我们希望SIFT能找到的特征是在各种尺度的情况下都可以成立的。因此,我们需要构建不同的尺度来寻找特征点。

    • 使用DOG来代替LOG

      通常情况下,我们用Laplacian of Gaussian来寻找图片中的边缘信息(先平滑,再二次求导),但是用LOG的话计算量会比较大而且复杂,于是我们就发现,可以用两个不同sigma值平滑的gaussian分布做差,得到和LOG类似的形状,也可以实现边缘检测。

      提取不同的高斯平滑后的边缘,我们可以得到一个octave。

      那为什么我们要octave呢,可能是因为在不同的高斯平滑下,我们找到的边缘是不同的。平滑程度大,找到的边界就会少一些,平滑程度小,产生的噪声就会强一些。不同的平滑程度下,我们得到的是不同的边界。

    • 改变尺度,寻找边缘

      在不同尺度下,找相同的特征这个思路很直观。比如说一棵树,我们在远处模糊的看到他的时候,他是一棵树;当我们走进之后,这个数会变得更加清晰,我们还能看出来他是一棵树。这就说明有一些特征信息是具有尺度不变性的。

      所以,除了在原图上做blur,我们还需要改变原图的resolution,然后再做一系列的blur。这样对于一张图,我们会得到不同的几个octaves

  2. Keypoint Localization

    确定keypoint的步骤分为两步,找到key point,去掉contrast比较弱的key point

    • 找到key point,
  3. Orientation Assignment

  4. Local Descriptor Creation

这一部分还是deep learning的准备课程,就是反向传播。

在更新神经网络参数的过程中,反向传播是很重要的一步。那么如何有效得计算反向传播也同样重要。

正向传播

首先我们来捋一捋神经网络的正向传播过程。

  • 输入 $\bf x = \left[\begin{equation}\begin{array}{c}\bf x1\\bf x_2\end{array}\end{equation}\right] $ ,第$i$ 层的权重为 $\bf w = \left[\begin{equation}\begin{array}{c|c}\bf w{11} \ w{12}\ \bf w{21} \ w_{22} \end{array}\end{equation}\right] $, 激活函数为 $\sigma(z)$, 通过激活函数的结果为 $\bf a$, 然后$\bf a$ 再作为下一层的输入通过神经元,通过激活函数,直到最后一层得到Loss function $\bf L$

反向传播

反向传播的过程是为了求得gradient然后更新每一层的每个神经元的参数。

Chain Rue

这里的 $n$ 表示的是第 $n$ 个training example。那我们只要计算一个example的更新就可以了

Forward Pass

根据Chain rule和我们知道的神经网络结构图,对于一层中的一个神经元,我们可以得到:

从这一层中可以看出,如果我们要求 $w_1$ 对 $C$ 的导数,我们能做的就是求 $z$ 对 $C$ 的倒数乘以 $w_1$ 对 $z$ 的导数。即,

那么,$\frac{\part z}{\part w_1}$ 很好计算,通过之前神经元的结构的例子,我们可以得出 $\frac{\part z}{\part w_1}$ 就是 $w_1$前面连接的输入,即$x_1$

而后面的 $\frac{\part C}{\part z}$ 则不能直接算出。

这样一来,我们把所有接在要求的参数前面的输入都保存下来,这样就可以得到 $\frac{\part C}{\part w_1}$ 导数的第一项。

Backward Pass

那怎么求 $\frac{\part C}{\part z}$ 呢?

根据Chain Rule, 我们可以接着计算 $\frac{\part C}{\part z} = \frac{\part a}{\part z}\cdot \frac{\part C}{\part a}$ 。在这个式子中,$\frac{\part a}{\part z}$ 很好算,因为我们知道激活函数,那么就很容易计算激活函数的导数。那又出现了一个令人头疼的项,就是 $\frac{\part C}{\part a}$。

$\frac{\part C}{\part a}$ 如果接着使用chain rule的话,可以看出 $a$ 是通过 $z’$ 和 $z’’$ 两项来共同影响最后的结果 $C$ 的,所以我们可以得到

而 $\frac{\part z’}{\part a}$ 和 $\frac{\part z’’}{\part a}$ 可以通过 $z’$ 和 $z’’$ 之前接的项来表示,而这时候我们要求的导数又变成了$\frac{\part C}{\part z’}$ 和 $\frac{\part C}{\part z’’}$. 这一项其实和 $\frac{\part C}{\part z}$ 是相似的, 所以计算 $\frac{\part C}{\part z’}$ 的过程是要重复 计算 $\frac{\part C}{\part z}$ 的过程 。

总结一下,

所以看到了吗,在每一层,我们如果想要计算cost function对这层的参数的导数,就要历经这一层之后的所有层,然后求导,这就造成了一个很大而且重复的工作量。所以我们在这里考虑将这些数值保存起来。

怎么保存呢?

  • 先考虑最后的情况

    假设我们的 $z’$ 是最后一层,过了这一层之后我们的 $z’$再过一个激活层就可以得到输出 $y = \sigma(z’)$

    那么,此时的 $\frac{\part C}{\part z’} = \frac{\part y}{\part z’}\cdot \frac{\part C}{\part y}$. 这里等式右边的第一项就是激活函数的导数,即 $\sigma’(z’)$ ;而第二项是根据cost function决定的,是一个constant。那这样我们就可以分别得到 $\frac{\part C}{\part z’}$ 和 $\frac{\part C}{\part z’’}$ 的值。

  • 考虑一般情况

    假设现在 $z’$ 不是最后一层,那么根据刚才算出来的最后一层的值和 $\frac{\part C}{\part z} = \sigma ‘(z)\left [w_3\frac{\part C}{\part z’} + w_4 \frac{\part C}{\part z’’}\right ]$ ,我们可以得到 $\frac{\part C}{\part z}$ 的值。这个式子也可以看做是个反向传送的神经网络。

那么我们最后要算的 $\frac{\part C}{\part w_1}$就可以用forward pass 和backward pass两部分保存的参数相乘得到。

我们在一开始介绍Machine Learning的时候提到找寻最佳模型的方法,同样这散步也可以应用在deep learning上面:

  • Step 1: Define a set of function - 定义神经网络的类型结构和框架
  • Step 2: Goodness of function - 定义评价神经网络的cost function
  • Step 3: Pick the best function - 训练能够得到最佳效果的神经网络的各种参数

这部分的课程要介绍的是,当我们在训练深度神经网络的时候,如果训练的效果不好,究竟是哪部分出了问题呢?在训练的时候,我们要查看哪些训练结果呢?

Read more »