Ubuntu20.04 编译 Caffe 时与 OpenCV4.X 的问题记录与解决.Caffe - Ubuntu 安装与测试实例及问题解决 - AIUAIUbuntu - OpenCV 源码安装与测试 - AIUAI参考：How to Install OpenCV on Ubuntu 20.04 - 2020.07.05参考：Install OpenCV 4.4.0 and Caffe on Ubuntu 20.04 for Python 3Ubuntu20.04 仓库安装 OpenCV:sudo apt install libopencv-dev python3-opencv查看版本：pkg-config --modversion opencv4 # 4.2.0问题1 - src/caffe/data_transformer.cpp:2:33: fatal error: opencv2/core/core.hpp原因：找不到头文件.解决方案：将 OpenCV 的头文件加入：原： INCLUDE_DIRS := $(PYTHON_INCLUDE) /usr/local/include /usr/include/hdf5/serial/ 新： INCLUDE_DIRS := $(PYTHON_INCLUDE) /usr/local/include /usr/include/hdf5/serial/ /usr/include/opencv4/问题2 - error: ‘CV_INTER_CUBIC’ was not declared in this scope问题：src/caffe/data_transformer.cpp:781:84: error: ‘CV_INTER_CUBIC’ was not declared in this scope cv::resize(crop_bufferM, cv_cropped_img, cv::Size(crop_size, crop_size), 0, 0, CV_INTER_CUBIC); ^~~~~~~~~~~~~~ src/caffe/data_transformer.cpp:781:84: note: suggested alternative: ‘SA_INTERRUPT’ cv::resize(crop_bufferM, cv_cropped_img, cv::Size(crop_size, crop_size), 0, 0, CV_INTER_CUBIC); ^~~~~~~~~~~~~~ SA_INTERRUPT src/caffe/data_transformer.cpp: In member function ‘void caffe::DataTransformer<Dtype>::Transform(const caffe::Datum&, const caffe::Datum&, caffe::Blob<Dtype>*, caffe::Blob<Dtype>*)’: src/caffe/data_transformer.cpp:954:51: error: ‘CV_INTER_NN’ was not declared in this scope cv::resize(M, M, cv::Size(width, height), 0, 0, CV_INTER_NN); ^~~~~~~~~~~ src/caffe/data_transformer.cpp:954:51: note: suggested alternative: ‘CV_INLINE’ cv::resize(M, M, cv::Size(width, height), 0, 0, CV_INTER_NN); ^~~~~~~~~~~ CV_INLINE make: *** [Makefile:592: .build_release/src/caffe/data_transformer.o] Error 1解决方案：在 src/caffe/data_transformer.cpp 中添加：include <opencv2/imgproc/imgproc_c.h>：#ifdef USE_OPENCV #include <opencv2/core/core.hpp> #include <opencv2/core/core.hpp> #include <opencv2/core/mat.hpp> #include <opencv2/highgui/highgui.hpp> #include <opencv2/imgproc/imgproc.hpp> #include <opencv2/imgproc/imgproc_c.h> // new add #include <opencv2/opencv.hpp> #endif // USE_OPENCV问题3 - error: ‘CV_LOAD_IMAGE_COLOR’ was not declared in this scope问题：src/caffe/layers/window_data_layer.cpp: In member function ‘virtual void caffe::WindowDataLayer<Dtype>::load_batch(caffe::Batch<Dtype>*)’: src/caffe/layers/window_data_layer.cpp:293:42: error: ‘CV_LOAD_IMAGE_COLOR’ was not declared in this scope cv_img = cv::imread(image.first, CV_LOAD_IMAGE_COLOR); ^~~~~~~~~~~~~~~~~~~ src/caffe/layers/window_data_layer.cpp:293:42: note: suggested alternative: ‘CV_IS_IMAGE_HDR’ cv_img = cv::imread(image.first, CV_LOAD_IMAGE_COLOR); ^~~~~~~~~~~~~~~~~~~ CV_IS_IMAGE_HDR make: *** [Makefile:592: .build_release/src/caffe/layers/window_data_layer.o] Error 1原因：OpenCV 版本问题，OpenCV4 变量名发生变动.解决方案：在 include/caffe/common.hpp 中新增如下代码(大概 70 行前后)：// Supporting OpenCV4 #if (CV_MAJOR_VERSION == 4) #define CV_LOAD_IMAGE_COLOR cv::IMREAD_COLOR #define CV_LOAD_IMAGE_GRAYSCALE cv::IMREAD_GRAYSCALE #endif //以上为新增代码(以下为原有代码，用于定位新增代码位置) // See PR #1236 namespace cv { class Mat; }另一种解决方案：对报错文件进行如下修改：CV_LOAD_IMAGE_COLOR 修改为 cv::IMREAD_COLOR CV_LOAD_IMAGE_GRAYSCALE 修改为 cv::IMREAD_GRAYSCALE
Caffe 进行模型训练，一般形式为：#!/usr/bin/env bash SOLVER=/path/to/solver.prototxt # GPUID=0 LOG=/path/to/train.log TOOLS=/path/to/caffe/build/tools echo "Starting to train" $TOOLS/caffe train -solver $SOLVER \ -weights /path/to/ptrtrained.caffemodel \ -gpu $GPUID 2>&1 | tee ${LOG}这里对 solver.prototxt 中的参数配置进行汇总说明. 具体可见 CAFFE_ROOT/src/caffe/proto/caffe.proto.net: "/path/to/train_val.prototxt" # test_iter： 每次测试的迭代次数， # 一般 test_iter*batch_size=所有的 test 样本数. # 一次测试可以覆盖全部的测试样本. test_iter: 1000 # test_interval： 训练时，每迭代 4000 次训练，进行一次测试. test_interval: 4000 # test_initialization： 训练开始时是否先进行一次测试. test_initialization: false # display： 每迭代 40 次训练，在屏幕上打印一次训练日志，如 loss 状态等. display: 40 # average_loss： 多次训练的 forward 的 loss，求平均值，并在屏幕上打印显示. # Display the loss averaged over the last average_loss iterations. # 相当于对 loss 值做了一个平滑. # 屏幕打印的当前训练 loss, 实际上是最近 20 次迭代的loss的平均值.不影响训练. average_loss: 40 # base_lr： 训练的基础学习率 lr,一般在 finetune 时，会设置较小的值. # 再根据特定的优化算法，对 lr 进行调整. # lr 过大，训练会难收敛； # lr 过小，训练收敛速度会比较慢. base_lr: 0.01 # lr_policy： 学习率 lr 的衰减策略 # lr_policy="fixed" # 训练过程中学习率lr保持不变. lr_policy: "step" # 与下面几个参数配合使用 # stepsize： 每 stepsize 次迭代，衰减学习率 stepsize: 320000 # gamma： 学习率 lr 衰减因子 gamma: 0.96 # momentum: 0.9 # weight_decay： 用于防止过拟合 weight_decay: 0.0002 # regularization_type：正则项，支持 L1 和 L2 正则化. # 由 weight_decay 决定. regularization_type = "L2" # clip_gradients：设置 clip_gradients>= 0 以对梯度参数进行剪枝， # 避免参数的真实 L2 参数值太大. clip_gradients: 60 # max_iter: 训练最大迭代次数，即网络何时停止训练. # 迭代次数少，可能收敛不了；迭代次数过多，可能过拟合或训练震荡. max_iter: 10000000 # iter_size: iter_size*batch_size=训练时的实际 batchsize. # 可以用于解决由于 GPU 显存不足，而导致训练 batchsize 受限的问题， # 相当于对 batch_size*iter_size 个样本进行 forward 后，才进行一次 backward. # 在单次 batchsize 受限时，可以做到很大的 batch. iter_size: 4 # snapshot：每 snapshot 次训练迭代，保存一次模型，相当于训练 checkpoints 保存. snapshot: 40000 # snapshot_prefix：训练中间模型的保存路径和前缀 snapshot_prefix: "/path/to/caffe_train" # snapshot_after_train：如果值为 false，则仅在训练完成后才保存一次模型. snapshot_after_train: true # type：优化算法 # 支持的有六种："SGD"，"AdaDelta"，"AdaGrad"，"Adam"，"Nesterov"，"RMSProp" type: "SGD" # solver_mode：训练在 CPU 还是 CPU 上运行. solver_mode: GPU # device_id： 训练在哪个 GPU 上运行，默认为 0. device_id: 0 # random_seed：如果值为非负数，则采用 Caffe 的随机数生成器进行初始化.用于结果复现. # 默认，采用 system clock 的种子进行初始化. random_seed: -1 # debug_info：打印网络的状态信息，有助于调试网路训练. 默认为 false. debug_info: false其中，几个参数部分的说明：[1] - 学习率策略 lr_policy，可选："step"， "multistep"，"fixed"，"exp"，"poly"，"sigmoid".[2] - 优化算法type：参见 Caffe - Solver 网络优化方法 - AIUAI[3] - iter_size：用于显存不够的场景，其作用效果是forward计算 iter_size次迭代后才backpward一次，相当于将batch_size增大了iter_size倍. 简单来说, real batch_size = batch_size * iter_size.每执行一次solver.step(1), 会执行batch_size * iter_size次forward与1次backward. 参考 caffe SolverParameter中的iter_size参数什么作用？ - 知乎Dtype loss = 0; for (int i = 0; i < param_.iter_size(); ++i) { //前向传播+后向传播 loss += net_->ForwardBackward(); } loss /= param_.iter_size(); ... ApplyUpdate(); // 处理batchsize*itersize张图片后，才调用一次ApplyUpdate函数根据学习率、method(SGD、AdaSGD等)进行梯度下降.
原文1：理解神经网络的激活函数 - 2018.05.05原文2：神经网络的激活函数总结 - 2018.08.02原文3：反向传播算法推导-全连接神经网络 - 2018.07.06出处：SIGAI - 微信公众号1. 为什么需要激活函数从数学上看，神经网络是一个多层复合函数. 激活函数在很早以前就被引入，其作用是保证神经网络的非线性，因为线性函数无论怎样复合结果还是线性的. 假设神经网络的输入是 n 维向量 x，输出是 m 维向量 y，它实现了如下向量到向量的映射：$$ R^n \rightarrow R^m $$我们将这个函数记为：$$ y = h(x) $$除输入层之外，标准的前馈型神经网络第 $l$ 层实现的变换可以分为线性组合、激活函数两步. 在某些开源框架中，这两步可能会拆分成不同的层，以利于代码复用和灵活组合. 例如 Caffe 中线性组合由内积层 InnerProductLayer 类实现，激活函数由神经元层NeuronLayer 类实现. 神经网络第层的变换写成矩阵和向量形式为：$$ \begin{matrix} u^{(l)} = W^{(l)} x^{(l-1)} + b^{(l)} \\ x^{(l)} = f(u^{(l)}) \end{matrix} $$其中W是权重矩阵，b是偏置向量，u是临时结果，x是神经网络每一层的输出. 激活函数分别作用于向量u的每一个分量，产生一个向量输出x. 在正向传播阶段，反复用上面的公式进行计算，最后得到网络的输出. 对于一个3层的网络，整个映射可以写成：$$ h(x) = f(W^{(3)} f(W^{(2)} f(W^{(1)}x + b^{(1)}) + b^{(2)}) + b^{(3)}) $$这是一个3层的复合函数. 从这里可以清晰的看到，如果没有激活函数，整个函数将是一个线性函数：$$ W^{(3)} (W^{(2)} (W^{(1)}x + b^{(1)}) + b^{(2)}) + b^{(3)} $$因此对激活函数最基本的要求是必须是非线性的. 在早期，普遍使用的是sigmoid函数和tanh函数. sigmoid函数的计算公式为：$$ f(x) = \frac{1}{1 + exp(-x)} $$这个函数的图像为：tanh函数的计算公式为：$$ f(x) = \frac{1 - e^{-2x}}{1 + e^{-2x}} $$它的图像为：前者的值域为(0,1)，单调递增且有界；后者的值域为(-1,+1)，是一个中心对称的奇函数，同样也是单调递增且有界.2. 什么样的函数可以做激活函数前面已经说过，为保证非线性，激活函数必须为非线性函数，但仅仅具有非线性是不够的. 神经网络在本质上是一个复合函数，这会让我们思考一个问题：这个函数的建模能力有多强？即它能模拟什么样的目标函数？已经证明，只要激活函数选择得当，神经元个数足够多，使用3层即包含一个隐含层的神经网络就可以实现对任何一个从输入向量到输出向量的连续映射函数的逼近，这个结论称为万能逼近(universal approximation)定理. 万能逼近定理的表述为：如果$\varphi (x)$是一个非常数、有界、单调递增的连续函数，$I_m$ 是 m 维的单位立方体，$I_m$ 中的连续函数空间为 $C(I_m)$. 对于任意 $\epsilon > 0$以及函数$f \in C(I_m)$，存在整数 N，实数$v_i, b_i$，实向量$w_i \in R^m$，通过它们构造函数 $F(x)$作为函数 $f$ 的逼近：$$ F(\mathbf{x}) = \sum_{i=1}^N v_i \varphi (\mathbf{w}_i^T \mathbf{x} + b_i) $$对于任意的 $X \in I_m$ 满足：$$ |F(\mathbf{x}) - f(\mathbf{x})| < \epsilon $$万能逼近定理的直观解释是可以构造出上面这种形式的函数，逼近定义在单位立方体空间中的任何一个连续函数到任意指定的精度. 这个定理对激活函数的要求是必须非常数、有界、单调递增，并且连续. 文献[1]对使用 sigmoid 激活函数时的情况进行了证明：如果 $\sigma$ 是一个连续函数，并且满足下面条件：$$ \begin{matrix} lim_{x \rightarrow -\infty} \sigma(x) = 0 \\ lim_{x \rightarrow +\infty} \sigma(x) = 1 \end{matrix} $$则，函数族：$$ f(x) = \sum \alpha_i \sigma(\mathbf{w}_i^T \mathbf{x} + b_i) $$在空间 n 维单位立方体空间 $C^n[0, 1]$中是稠密的，即，这样的函数可以逼近定义在单位立方体空间中的任意连续函数到任意指定的精度. 显然sigmoid函数就满足对 $\sigma​$ 的要求. 上面这些结论的函数输出值都是一个标量，但我们可以把它推广的向量的情况，神经网络的输出一般是一个向量. 只要网络规模设计得当，使用 sigmoid函数 和 ReLU 函数作为激活函数的逼近能力都能够得到保证. ReLU函数定义为：$$ ReLU(x) = max(0, x) $$它是一个分段线性函数. 文献7分析了使用ReLU激活函数的神经网络的逼近能力. 下图是一个非线性分类问题的例子，说明了神经网络确实能处理这种非线性问题：在上图中，用图中的圆圈（红色和蓝色）训练样本训练出来的神经网络模型成功的将蓝色和红色两类样本分开了，分界面是两条曲线. 仅仅满足万能逼近定理的要求也是不够的. 神经网络的训练一般采用反向传播算法+梯度下降法. 反向传播算法从复合函数求导的链式法则导出，因为神经网络是一个多层的复合函数. 在反向传播时，误差项的计算公式为：$$ \delta^{(l)} = (W^{(l+1)})^T \delta^{(l+1)} \odot f'(u^{(l)}) $$由于使用梯度下降法需要计算损失函数对参数的梯度值，这个梯度值根据上面的误差项计算，而误差项的计算又涉及到计算激活函数的导数，因此激活函数必须是可导的. 实际应用时并不要求它在定义域内处处可导，只要是几乎处处可导即可. “几乎处处可导”看上去是一个比较有文学味道的词，但实际上是数学中一个严格的概念，这涉及到实变函数的知识. 它的严格定义是这样的：定义 $R$ 为一维欧式空间，$E \in R$ 是它的一个子集, $mE$ 为点集 $E$ 的 Lebesgue 测度. 如果 $E$ 为 $R$ 中的可测集， $f(x)$ 为定义在 $E$ 的实函数，如果存在 $B \in E$,满足：$mN=0$，对于任意的 $x_0 \in E/N$ 函数 $f(x)$在 $x_0$处都可导，则称$f(x)$在 $E$ 上几乎处处可导.上面这个定义过于晦涩. 我们可以简单的将几乎处处可导理解成不可导点只有有限个，或者无限可列个（即可用自然数对这些点进行编号，某一区间上的实数就是无限不可列的），即不可导点的测度是0. 可以将测度理解成一维直线的一些点的集合的长度，或者二维平面上一些点的集合的面积. 在概率论中我们知道，连续型随机变量取任何一个点处的值的概率为0，如果将激活函数输入值x看做是随机变量，则它落在这些不可导点处的概率是0. 在计算机实现时，浮点数是进行了离散化的，分成尾数和阶码表示，计算机能表示的浮点数是有限个，因此有一定的概率会落在不可导点处，但概率非常小. ReLU函数在0点处就不可导.3. 什么样的函数是好的激活函数反向传播算法计算误差项时每一层都要乘以本层激活函数的导数. 如果激活函数导数的绝对值值小于1，多次连乘之后误差项很快会衰减到接近于0，参数的梯度值由误差项计算得到，从而导致前面层的权重梯度接近于0，参数没有得到有效更新，这称为梯度消失问题. 与之相反的是梯度爆炸问题，如果激活函数导数的绝对大于1，多次乘积之后权重值会趋向于非常大的数，这称为梯度爆炸. 梯度消失问题最早在1991年发现，文献[10]对深层网络难以训练的问题进行了分析，在很长一段时间内，梯度消失问题是困扰神经网络层次加深的一个重要因素. 文献[10]对深层神经网络难以训练的问题进行了理论分析和实验验证. 在实验中，作者训练了有1-5个隐藏层的神经网络，每个隐藏层有1000个神经元，输出层使用 softmax logistic回归函数. 激活函数使用了sigmoid，tanh，以及softsign：$$ \frac{x}{1 + |x|} $$权重初始化公式为：$$ W_{ij} = U[- \frac{1}{\sqrt{n}}, \frac{1}{\sqrt{n}}] $$其中，U[a, b]是[a, b]中的均匀分布，n是前一层的尺寸. 理论分析和实验结果都证明，随着网络层数的增加，反向传播的作用越来越小，网络更加难以训练和收敛. 文献[11]中定义了激活函数饱和性的概念，并对各种激活函数进行了分析，给出了改进措施. 如果一个激活函数满足：$$ \lim\limits_{x \rightarrow + \infty} f'(x)=0 $$即在正半轴函数的导数趋向于0，则称该函数为右饱和. 类似的如果满足：$$ \lim\limits_{x \rightarrow -\infty} f'(x)=0 $$即在负半轴函数的导数趋向于0，则称该函数左饱和. 如果一个激活函数既满足左饱和又满足右饱和，称之为饱和. 如果存在常数c，当x>c时有 $f'(x) = 0$, 则称函数右硬饱和；当x<c时有 $f'(x) = 0$, 则称函数左硬饱和. 既满足左硬饱和又满足右硬饱和的激活函数为硬饱和函数. 饱和性和梯度消失问题密切相关. 在反向传播过程中，误差项在每一层都要乘以激活函数导数值，一旦x的值落入饱和区间，多次乘积之后会导致梯度越来越小，从而出现梯度消失问题. sigmoid函数的输出映射在(0,1)之间，单调连续，求导容易. 但是由于其软饱和性，容易产生梯度消失，导致训练出现问题；另外它的输出并不是以0为中心的. tanh函数的输出值以0为中心，位于(-1,+1)区间，相比sigmoid函数训练时收敛速度更快，但它还是饱和函数，存在梯度消失问题. ReLU函数其形状为一条折线，当x<0时做截断处理. 该函数在0点出不可导，如果忽略这一个点其导数为sgn. 函数的导数计算很简单，而且由于在正半轴导数为1，有效的缓解了梯度消失问题. 在ReLU的基础上又出现了各种新的激活函数，包括ELU、PReLU等. 如果对各种激活函数深入的比较和分析感兴趣，可以阅读文献[11].4. 常用的激活函数下表列出了Caffe中支持的激活函数和它们的导数：5. Caffe 中激活函数的具体实现下面我们以Caffe为例，介绍这些激活函数的具体实现细节. 在Caffe中，激活函数是一个单独的层，把它和全连接层，卷据层拆开的好处是更为灵活，便于代码复用和组合. 因为无论是全连接层，还是卷据层，它们激活函数的实现是相同的，因此可以用一套代码来完成. 激活函数由神经元层完成，它们的基类是 NeuronLayer，所有的激活函数层均从它派生得到，下面分别进行介绍，限于篇幅，我们只介绍一部分，其他的原理类似. 此外，Dropout机制也由神经元层实现.5.1. SigmoidLayerSigmoidLayer类实现了标准sigmoid激活函数. 正向传播函数对每个输入数据计算sigmoid函数值，在这里count是输入数据的维数. 实现代码如下：sigmoid_layer.cpptemplate <typename Dtype> void SigmoidLayer<Dtype>::Forward_cpu( const vector<Blob<Dtype>*>& bottom, const vector<Blob<Dtype>*>& top) { // 输入数据 const Dtype* bottom_data = bottom[0]->cpu_data(); // 输出数据 Dtype* top_data = top[0]->mutable_cpu_data(); // 输入数据的个数 const int count = bottom[0]->count(); for (int i = 0; i < count; ++i) { // 对每个数据计算 sigmoid 函数 top_data[i] = sigmoid(bottom_data[i]); } }反向传播函数计算后一层的误差项与激活函数的导数的乘积，实现代码如下：template <typename Dtype> void SigmoidLayer<Dtype>::Backward_cpu( const vector<Blob<Dtype>*>& top, const vector<bool>& propagate_down, const vector<Blob<Dtype>*>& bottom) { if (propagate_down[0]) { // top_data 本层的 sigmoid 值 const Dtype* top_data = top[0]->cpu_data(); // top_diff 为后一层的误差 const Dtype* top_diff = top[0]->cpu_diff(); // bottom_diff 保存了本层的误差 Dtype* bottom_diff = bottom[0]->mutable_cpu_diff(); const int count = bottom[0]->count(); for (int i = 0; i < count; ++i) { // sigmoid 函数值在正向传播时被计算出来， // 并存在这里以供反向传播时使用. const Dtype sigmoid_x = top_data[i]; // 计算 sigmoid 函数的导数，并与后一层传来的误差相乘. bottom_diff[i] = top_diff[i] * sigmoid_x * (1. - sigmoid_x); } } }5.2. TanHLayerTanHLayer类实现了tanh激活函数. 正向传播函数实现代码如下：tanh_layer.cpptemplate <typename Dtype> void TanHLayer<Dtype>::Forward_cpu( const vector<Blob<Dtype>*>& bottom, const vector<Blob<Dtype>*>& top) { const Dtype* bottom_data = bottom[0]->cpu_data(); Dtype* top_data = top[0]->mutable_cpu_data(); const int count = bottom[0]->count(); // 对每个输入函数计算 tanh 函数 for (int i = 0; i < count; ++i) { // 直接调用数学库的 tanh 函数 top_data[i] = tanh(bottom_data[i]); } }反向传播函数的实现如下，其中，利用了 tanh 函数的求导公式.template <typename Dtype> void TanHLayer<Dtype>::Backward_cpu( const vector<Blob<Dtype>*>& top, const vector<bool>& propagate_down, const vector<Blob<Dtype>*>& bottom) { if (propagate_down[0]) { const Dtype* top_data = top[0]->cpu_data(); const Dtype* top_diff = top[0]->cpu_diff(); Dtype* bottom_diff = bottom[0]->mutable_cpu_diff(); const int count = bottom[0]->count(); Dtype tanhx; for (int i = 0; i < count; ++i) { tanhx = top_data[i]; // 取得 tanh(x) 的值 // 计算 tanh 的导数，并与后一层的误差相乘. // 导数计算公式可见上一章节中的表格. bottom_diff[i] = top_diff[i] * (1 - tanhx * tanhx); } } }5.3. ELULayerELULayer类实现ELU激活函数，是直线函数和指数函数的结合. 当 x>0 时函数值为 x；当x<0是一条衰减的指数函数曲线. 可以证明，当 $x \rightarrow -\infty$ 时该函数的极限为-a. 当x<=0时其导数为：$$ f'(x) = \alpha e^x = \alpha e^x - \alpha + \alpha = \alpha(e^x - 1) + \alpha = f(x) + \alpha $$这样可以通过函数值得到导数值，减少计算量. 正向传播函数的实现如下：elu_layer.cpptemplate <typename Dtype> void ELULayer<Dtype>::Forward_cpu( const vector<Blob<Dtype>*>& bottom, const vector<Blob<Dtype>*>& top) { const Dtype* bottom_data = bottom[0]->cpu_data(); Dtype* top_data = top[0]->mutable_cpu_data(); const int count = bottom[0]->count(); Dtype alpha = this->layer_param_.elu_param().alpha(); for (int i = 0; i < count; ++i) { // 当 bottom_data[i]>0 时，下时的值为 bottom_data[i]； // 否则，值为 alpha * exp(bottom_data[i] - 1) top_data[i] = std::max(bottom_data[i], Dtype(0)) + alpha * (exp(std::min(bottom_data[i], Dtype(0))) - Dtype(1)); } }反向传播函数的实现如下：template <typename Dtype> void ELULayer<Dtype>::Backward_cpu( const vector<Blob<Dtype>*>& top, const vector<bool>& propagate_down, const vector<Blob<Dtype>*>& bottom) { if (propagate_down[0]) { const Dtype* bottom_data = bottom[0]->cpu_data(); const Dtype* top_data = top[0]->cpu_data(); const Dtype* top_diff = top[0]->cpu_diff(); Dtype* bottom_diff = bottom[0]->mutable_cpu_diff(); const int count = bottom[0]->count(); Dtype alpha = this->layer_param_.elu_param().alpha(); for (int i = 0; i < count; ++i) { // 当 x>0 时，下式的值为 top_diff[i]; // 当 x<= 0 时，下式的值为 alpha+top_data[i]，即 alpha+f(x) bottom_diff[i] = top_diff[i] * ((bottom_data[i] > 0) + (alpha + top_data[i]) * (bottom_data[i] <= 0)); } } }5.4. PReLULayer类PReLULayer实现了PReLU激活函数. 正向传播函数的实现如下：prelu_layer.cpptemplate <typename Dtype> void PReLULayer<Dtype>::Forward_cpu( const vector<Blob<Dtype>*>& bottom, const vector<Blob<Dtype>*>& top) { const Dtype* bottom_data = bottom[0]->cpu_data(); Dtype* top_data = top[0]->mutable_cpu_data(); const int count = bottom[0]->count(); const int dim = bottom[0]->count(2); const int channels = bottom[0]->channels(); const Dtype* slope_data = this->blobs_[0]->cpu_data(); // For in-place computation if (bottom[0] == top[0]) { caffe_copy(count, bottom_data, bottom_memory_.mutable_cpu_data()); } // if channel_shared, channel index in the following computation becomes always zero. const int div_factor = channel_shared_ ? channels : 1; for (int i = 0; i < count; ++i) { int c = (i / dim) % channels / div_factor; // 当 bottom_data[i]>0 时，下式的值为 bottom_data[i]; // 否则为 slope_data[c] * bottom_data[i] top_data[i] = std::max(bottom_data[i], Dtype(0)) + slope_data[c] * std::min(bottom_data[i], Dtype(0)); } }反向传播函数的实现如下：template <typename Dtype> void PReLULayer<Dtype>::Backward_cpu( const vector<Blob<Dtype>*>& top, const vector<bool>& propagate_down, const vector<Blob<Dtype>*>& bottom) { const Dtype* bottom_data = bottom[0]->cpu_data(); const Dtype* slope_data = this->blobs_[0]->cpu_data(); const Dtype* top_diff = top[0]->cpu_diff(); const int count = bottom[0]->count(); const int dim = bottom[0]->count(2); const int channels = bottom[0]->channels(); // For in-place computation if (top[0] == bottom[0]) { bottom_data = bottom_memory_.cpu_data(); } // if channel_shared, channel index in the following computation becomes always zero. const int div_factor = channel_shared_ ? channels : 1; // Propagte to param // Since to write bottom diff will affect top diff if top and // bottom blobs are identical (in-place computaion), // we first compute param backward to keep top_diff unchanged. if (this->param_propagate_down_[0]) { Dtype* slope_diff = this->blobs_[0]->mutable_cpu_diff(); for (int i = 0; i < count; ++i) { int c = (i / dim) % channels / div_factor; slope_diff[c] += top_diff[i] * bottom_data[i] * (bottom_data[i] <= 0); } } // Propagate to bottom if (propagate_down[0]) { Dtype* bottom_diff = bottom[0]->mutable_cpu_diff(); for (int i = 0; i < count; ++i) { int c = (i / dim) % channels / div_factor; bottom_diff[i] = top_diff[i] * ((bottom_data[i] > 0) + slope_data[c] * (bottom_data[i] <= 0)); } } }5.5. DropoutLayer类DropoutLayer实现Dropout机制. 在训练阶段，随机丢掉一部分神经元，用剩下的节点进行前向和后向传播. 这里实现时通过二项分布随机数来控制神经元是否启用，如果随机数取值为1则启用，否则不启用. 正向传播函数的实现如下：dropout_layer.cpptemplate <typename Dtype> void DropoutLayer<Dtype>::Forward_cpu( const vector<Blob<Dtype>*>& bottom, const vector<Blob<Dtype>*>& top) { const Dtype* bottom_data = bottom[0]->cpu_data(); Dtype* top_data = top[0]->mutable_cpu_data(); unsigned int* mask = rand_vec_.mutable_cpu_data(); const int count = bottom[0]->count(); if (this->phase_ == TRAIN) { // 训练阶段启用 dropout // Create random numbers // 生成随机数，服从伯努利二项分布，随机数存放再掩码数组 mask 中， // 值为 0 或 1，以 threshold_ 的概率取值 1， // 以 1.-threshold_ 的概率取值 0. caffe_rng_bernoulli(count, 1. - threshold_, mask); for (int i = 0; i < count; ++i) { // 输出值要么为输入值，即恒等变换；要么为 0. top_data[i] = bottom_data[i] * mask[i] * scale_; } } else { // 测试阶段，不启用 dropout，直接将输入值原样拷贝输出. caffe_copy(bottom[0]->count(), bottom_data, top_data); } } 反向传播函数的实现如下：template <typename Dtype> void DropoutLayer<Dtype>::Backward_cpu( const vector<Blob<Dtype>*>& top, const vector<bool>& propagate_down, const vector<Blob<Dtype>*>& bottom) { if (propagate_down[0]) { const Dtype* top_diff = top[0]->cpu_diff(); Dtype* bottom_diff = bottom[0]->mutable_cpu_diff(); if (this->phase_ == TRAIN) {// 训练阶段启用 dropout // 先获取正向传播时的掩码数组 const unsigned int* mask = rand_vec_.cpu_data(); const int count = bottom[0]->count(); for (int i = 0; i < count; ++i) { // 导数值要么为 0，要么为输入值. bottom_diff[i] = top_diff[i] * mask[i] * scale_; } } else {// 测试阶段，不启用 dropout caffe_copy(top[0]->count(), top_diff, bottom_diff); } } }6. 参考文献[1] Cybenko, G. Approximation by superpositions of a sigmoid function. Mathematics of Control, Signals, and Systems, 2, 303-314, 1989.[2] Kurt Hornik. Approximation capabilities of multilayer feedforward networks. 1991, Neural Networks.[3] Hornik, K., Stinchcombe, M., and White, H. Multilayer feedforward networks are universal approximators. Neural Networks, 2, 359-366, 1989.[4] Hornik, K., Stinchcombe, M., and White, H. Universal approximation of an unknown mapping and its derivatives using multilayer feedforward networks. Neural networks, 3(5), 551-560, 1990.[5] Leshno, M., Lin, V. Y., Pinkus, A., and Schocken, S. Multilayer feedforward networks with a nonpolynomial activation function can approximate any function. Neural Networks, 6, 861-867, 1993.[6] Barron, A. E. Universal approximation bounds for superpositions of a sigmoid function. IEEE Transactions on Information Theory, 39, 930-945, 1993.[7] Montufar, G. Universal approximation depth and errors of narrow belief networks with discrete units. Neural Computation, 26, 2014.[8] Raman Arora, Amitabh Basu, Poorya Mianjy, Anirbit Mukherjee. Understanding Deep Neural Networks with Rectified Linear Units. 2016, Electronic Colloquium on Computational Complexity.[9] Nair, V. and Hinton. Rectified linear units improve restricted Boltzmann machines. In L. Bottou and M. Littman, editors, Proceedings of the Twenty-seventh International Conference on Machine Learning (ICML 2010).[10] X. Glorot, Y. Bengio. Understanding the difficulty of training deep feedforward neural networks. AISTATS, 2010.[11] Caglar Gulcehre, Marcin Moczulski, Misha Denil, Yoshua Bengio. Noisy Activation Functions. ICML 2016.
Netscope 是对于 Caffe 网络结构可视化的很好的工具.Caffe 也有基于 Python 的网络结构可视化脚本，如：#!/usr/bin/env python #! --*-- coding:utf-8 --*-- """ Caffe prototxt 网络结构可视化 """ from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter from google.protobuf import text_format import sys sys.path.insert(0, '/path/to/caffe/python') import caffe import caffe.draw from caffe.proto import caffe_pb2 def parse_args(): """ 输入参数 """ parser = ArgumentParser(description=__doc__, formatter_class=ArgumentDefaultsHelpFormatter) # Caffe prototxt 网络定义文件 parser.add_argument('--prototxt', help='Input network prototxt file') # prototxt 训练或测试结构图 parser.add_argument('--phase', help='TRAIN, or TEST, or ALL', default="ALL") # 输出保存的网络结构图 parser.add_argument('--output_image', default='output_image.png', help='Output image file') # 网络结构的可视化方式，可选 'TB' 'LR'等. parser.add_argument('--vis_order', help='TB(top-bottom) or RL(right-left)', default='LR') # 可视化待学习网络层的学习率lr，multplier， # 如 Convolution, Deconvolution, InnerProduct 等网络层 parser.add_argument('--display_lrm', action='store_true', default=False) args = parser.parse_args() return args def main(): args = parse_args() print('[INFO] Prototxt %s' % args.prototxt) print('[INFO] Net phase %s' % args.phase) print('[INFO] Vis order %s' % args.vis_order) print('[INFO] Display lr and multplier %s' % args.display_lrm) print('[INFO] Output image %s' % args.output_image) assert args.phase in ['TRAIN', 'TEST', 'ALL'] net = caffe_pb2.NetParameter() text_format.Merge(open(args.prototxt).read(), net) print('[INFO] Drawing net to %s' % args.prototxt) caffe.draw.draw_net_to_file(net, args.output_image, args.vis_order, args.phase, args.display_lrm) print('[INFO] Done.') if __name__ == '__main__': main()输出结果如：
Caffe - SolverSolver 通过协调网络的前向推断(forward inference) 和后向梯度(backward gradients) 进行参数更新，以进行模型优化.网络学习可以分为：Solver 监督优化及参数更新和 Net 计算 loss 及梯度.Caffe 提供的 Solvers 有：Stochastic Gradient Descent (type: "SGD"),AdaDelta (type: "AdaDelta"),Adaptive Gradient (type: "AdaGrad"),Adam (type: "Adam"),Nesterov’s Accelerated Gradient (type: "Nesterov") andRMSprop (type: "RMSProp")Solver 的主要作用：[1] - 支持优化记录(bookkeeping)，并创建训练网络(learning)和测试网络(evaluation).[2] - 通过调用 forward/backward 进行迭代优化，并更新参数.[3] - 周期地评估(evaluation)测试网络.[4] - 保存优化过程中模型和优化器状态的快照.其中，对于每一次迭代，[1] - 调用 forward 计算网络的输出和 loss.[2] - 调用 backward 计算梯度.[3] - 根据优化算法，进行梯度计算，更新参数.[4] - 根据学习率，历史记录和优化方法更新优化器状态.Caffe Solvers 可以以 CPU 和 GPU 两种模式运行.Forward 计算：Backward 计算：1. 优化方法深度学习优化方法解决的问题是，最小化损失的一般问题.对于数据集 $D$，优化的目标是，对整个数据集 $|D|$ 求平均损失：$$ L(W) = \frac{1}{|D|} \sum _{i}^{|D|} f_W(X^{(i)}) + \lambda r(W) $$其中，$f_W(X^{(i)})$ 是对于数据样本 $X^{(i)}$ 的 损失值；$r(W)$ 是正则项，$\lambda$ 为其权重.在实际场景中，数据集样本总数 $|D | $ 会非常大，因此，在每次求解迭代中，一般采用小批量(mini-batch)数据 随机逼近目标 的方式，其中 $N << |D|$：$$ L(W) \approx \frac{1}{N} \sum _{i}^{N} f_W (X^{(i)}) + \lambda r(W) $$网络模型在 forward pass 中计算 $f_W$，在 backward pass 计算梯度 $\nabla f_W$.网络的参数更新 $\Delta W$ 是由求解器(solver) 根据误差梯度(error gradient) $\nabla f_W$，正则梯度 $\nabla r(W)$ 以及其它参数进行迭代更新.1.1 SGDSGD, Stochastic Gradient Descent，随机梯度下降法.SGD 是根据负梯度(negative gradient) $\nabla L(W)$和上次迭代的权重更新 $V_t$ 的线性组合来更新权重 $W$.学习率(learning rate) $\alpha$ 是负梯度 $\nabla L(W)$ 的权重；动量(momentum) $\mu$ 是上次权重更新 $V_t$ 的权重；记，计算的更新值为 $V_{t+1}$，在 $t + 1$ 次迭代的更新的权重为 $W_{t+1}$，在给定上次迭代的更新的权重为 $V_t$ 和当前权重 $W_t$ 时，其迭代公式为：$$ V_{t+1} = \mu V_t - \alpha \nabla L(W_t) $$$$ W_{t+1} = W_t + V_{t+1} $$学习率和动量设置的经验规则：深度学习中，推荐的 SGD 的参数设置是，初始化学习率为 $\alpha \approx 0.01 = 10 ^{-2}$，当损失函数减少逐渐变缓时，以一个常数因子(如，10)衰减，重复几次该过程.通常设置动量 $\mu = 0.9$ 等相似值. 通过在训练迭代时，平滑权重更新，动量能够使得 SGD 更平稳和快速.在 solver.prototxt 中的定义：base_lr: 0.01 # 学习率初始化值 lr_policy: "step" # 学习率策略，"steps" # 在每 stepsize 次迭代时， # 学习率以 gamma 因子衰减. gamma: 0.1 # 以 10 的因子衰减学习率， # 即，学习率乘以 gamma=0.1 stepsize:100000 # 每 100K 次迭代衰减学习率 max_iter:350000 # 总训练迭代次数 momentum: 0.9注：在训练多次后，动量 $\mu $ 以 $\frac{1}{1-\mu}$ 的因子乘以更新的大小. 因此，增加 $\mu$ 是学习率 $\alpha$ 衰减的不错的策略. 反之，减少 $\mu$ 也可对应的提高学习率 $\alpha$.例如，动量 $\mu = 0.9$，则有效的更新因子为 $\frac{1}{1 - 0.9} = 10$. 如果增加动量 $\mu = 0.99$，则更新因子增加为 $\frac{1}{1 - 0.99} = 100$，则将学习率 $\alpha$ 衰减了 10 (x0.1).base_lr: 0.05 lr_policy: "multistep" stepvalue: 150000 stepvalue: 300000 gamma: 0.1 momentum: 0.9 weight_decay: 0.0001 display: 10 max_iter: 6000001.2 AdaDeltaAdaDelta 是鲁棒的学习率调整方法，其是基于梯度的优化方法，参数更新公式为：$$ (v_t)_i = \frac{RMS((v_{t-1})_i)}{RMS(\nabla L(W_t))_i} (\nabla L(W_{t^{'}}))_i $$$$ (W_{t+1})_i = (W_t)_i - \alpha (v_t)_i $$其中，$$ RMS(\nabla L(W_t))_i = \sqrt {E[g^2] + \epsilon} $$$$ E[g^2]_t = \delta E[g^2]_{t-1} + (1 - \delta)g_t^2 $$# mnist_autoencoder_solver_adadelta.prototxt base_lr: 1.0 lr_policy: "fixed" momentum: 0.95 delta: 1e-8 weight_decay: 0.0005 type: "AdaDelta" display: 100 max_iter: 650001.3 AdaGradAdaGrad, 即 adaptive gradient，自适应梯度法.AdaGrad 也是基于梯度的优化方法，AdaGrad attempts to find needles in haystacks in the form of very predictive but rarely seen features.给定所有先前迭代的更新信息 $(\nabla L(W))_{t^{'}}$, 其中，$t^{'} \in \lbrace 1, 2, ..., t \rbrace$，权重更新公式为：对于权重 $W$ 的每个一个元素，$$ (W_{t+1})_i = (W_t)_i - \alpha \frac{(\nabla L(W_t))_i}{\sqrt{\sum _{t^{'}=1}^t} (\nabla (W_{t^{'}}))_i^2} $$实际应用场景中，对于权重 $W \in \mathcal{R} ^d$，AdaGrad 仅采用 $\mathcal{O}(d)$ 的历史梯度信息.# mnist_autoencoder_solver_adagrad.prototxt base_lr: 0.01 lr_policy: "fixed" weight_decay: 0.0005 solver_mode: GPU type: "AdaGrad" display: 100 max_iter: 650001.4 AdamAdam 也是基于梯度的优化方法. Adam 包括自适应矩估计(adaptive moment estimation) $(m_t, v_t)$ .Adam 可以看作 AdaGrad 的一种泛化.Adam 更新公式为：$$ (m_t)_i = \beta_1(m_{t-1})_i + (1 - \beta_1)(\nabla L(W_t))_i $$$$ (v_t)_i = \beta_2(v_{t-1})_i + (1 - \beta_2)(\nabla L(W_t))_i $$$$ (W_{t+1})_i = (W_t)_i - \alpha \frac{\sqrt{1 - (\beta_2)_i^t}}{1 - (\beta)_i^t} \frac{(m_t)_i}{\sqrt{(v_t)_i} + \epsilon} $$Adam: A Method for Stochastic Optimization 中的默认值为 $\beta_1 = 0.9, \beta _2 = 0.999, \epsilon=10^{-8}$.Caffe 中 $momentum = \beta _1, momentum2 = \beta _2, delta = \epsilon$.# lenet_solver_adam.prototxt base_lr: 0.001 momentum: 0.9 momentum2: 0.999 # since Adam dynamically changes the learning rate, # here, set the base learning rate to a fixed value. lr_policy: "fixed" type: "Adam" display: 100 max_iter: 100001.5 NesterovNesterov, Nesterov’s accelerated gradient.更新公式类似于 SGD：$$ V_{t+1} = \mu V_t - \alpha \nabla L(W_t + \mu V_t) $$$$ W_{t+1} = W_t + V_{t+1} $$# mnist_autoencoder_solver_nesterov.prototxt base_lr: 0.01 lr_policy: "step" gamma: 0.1 stepsize: 10000 weight_decay: 0.0005 momentum: 0.95 type: "Nesterov" display: 100 max_iter: 650001.6 RMSpropRMSprop 也是一种基于梯度的优化方法，类似于 SGD. 其更新公式为：$$ MS((W_t)_i) = \delta MS((W_{t-1})_i) + (1 - \delta)(\nabla L(W_t))_i^2 $$$$ (W_{t+1})_i = (W_t)_i - \alpha \frac{(\nabla L(W_t))_i}{\sqrt{MS((W_t)_i)}} $$其中，$\delta$ (rms_decay) 的默认值为：$\delta=0.99$.# lenet_solver_rmsprop.prototxt base_lr: 0.01 momentum: 0.0 weight_decay: 0.0005 lr_policy: "inv" gamma: 0.0001 power: 0.75 type: "RMSProp" rms_decay: 0.98 display: 100 max_iter: 10000
Python 读取图片的库有很多， 如 opencv, skimage, PIL 等等， 但在送入 Caffe 网络前，需要的处理不大一样，不注意的情况下，容易弄混淆. 比如，通道转换，scale 范围，RGB次序等. Caffe 中彩色图片的颜色通道为 BGR, 像素值 scale 范围为 [0, 255].1. Caffe 自带的图片读取img = caffe.io.load_image(imagefile)采用的是 skimage.io.imread():def load_image(filename, color=True): """ 加载图片， Load an image converting from grayscale or alpha as needed. 参数： filename : 图片路径 color : boolean. 默认值 color=True， 表示 RGB 格式加载. color=False, 表示强度(intensity) 格式， 如果图片是灰度图. 返回值： image : an image with type np.float32 in range [0, 1] of size (H x W x 3) in RGB or of size (H x W x 1) in grayscale. """ img = skimage.img_as_float(skimage.io.imread(filename, as_grey=not color)).astype(np.float32) if img.ndim == 2: img = img[:, :, np.newaxis] if color: img = np.tile(img, (1, 1, 3)) elif img.shape[2] == 4: img = img[:, :, :3] return img可以看出， caffe.io.load_image() 基于 skimage.io.imread() 库读取图片， 得到的是 RGB 格式，像素值范围为 [0, 1] (float) 的. 因此，图片在送入网络前，采用以下处理：transformer = caffe.io.Transformer({'data': net.blobs['data'].data.shape}) transformer.set_transpose('data', (2, 0, 1)) # 如，由 227x227x3 变换为 3x227x227 transformer.set_mean('data', meandata) # 减均值 # transformer.set_mean('data', np.load(mean_file).mean(1).mean(1)) transformer.set_raw_scale('data', 255) # 像素值由 [0, 1] 缩放到 [0, 255] 范围 transformer.set_channel_swap('data', (2, 1, 0)) # 由 RGB 格式变换到 BGR 格式2. Opencv 图片读取import cv2 img = cv2.imread(imagefile) cv2.imread() 读取图片的格式是 BGR， 像素值范围为 [0, 255]. 相比于 caffe.io.load_image() 的图片读取，不需要再进行像素值范围缩放到 [0, 255], 通道变换也不需要再进行由 RGB 到 BGR 格式的转换. 即只需：transformer = caffe.io.Transformer({'data': net.blobs['data'].data.shape}) transformer.set_transpose('data', (2, 0, 1)) # 如，由 227x227x3 变换为 3x227x227 transformer.set_mean('data', meandata) # 减均值 # transformer.set_mean('data', np.load(mean_file).mean(1).mean(1))3. PIL 图片处理from PIL import Image img = Image.open(imagefile) PIL 对于 PNG, BMP, JPG 彩色图像格式的图片，Image.open() 均返回 RGB 图片. PIL 对于 PNG, BMP, JPG 灰度图像格式的图片，Image.open() 返回 L. 相比于 caffe.io.load_image() 的图片读取，不需要再进行像素值范围缩放到 [0, 255], 但是，通道变换需要再进行由 RGB 到 BGR 格式的转换.transformer = caffe.io.Transformer({'data': net.blobs['data'].data.shape}) transformer.set_transpose('data', (2, 0, 1)) # 如，由 H×W×K 转化为K×H×W transformer.set_mean('data', meandata) # 减均值 # transformer.set_mean('data', np.load(mean_file).mean(1).mean(1)) transformer.set_channel_swap('data', (2, 1, 0)) # 由 RGB 格式变换到 BGR 格式## 目标
目标检测中 RoI Pooling 层往往会被用到. 虽然 Mask R-CNN 提出了对像素更敏感的 RoI Align 层， 这里还是梳理下 RoI Pooling 层.1. caffe prototxt 中定义layer { name: "roi_pool5" type: "ROIPooling" bottom: "conv5" bottom: "rois" top: "pool5" roi_pooling_param { pooled_w: 6 pooled_h: 6 spatial_scale: 0.0625 # 1/16 } }2. caffe caffe.proto ROI Pooling 层参数说明ROI Pooling 层： optional ROIPoolingParameter roi_pooling_param = 43; message ROIPoolingParameter { // Pad, kernel size, and stride are all given as a single value for equal // dimensions in height and width or as Y, X pairs. optional uint32 pooled_h = 1 [default = 0]; // The pooled output height 池化后输出的 height optional uint32 pooled_w = 2 [default = 0]; // The pooled output width 池化后输出的 width // Multiplicative spatial scale factor to translate ROI coords from their // input scale to the scale used when pooling // 乘以空间缩放因子，以将 RoI 坐标由输入尺度转换到 pooling 时使用的尺度 optional float spatial_scale = 3 [default = 1]; }3. RoI Pooling 层作用原理及源码分析根据 prototxt 定义可以看出，roi_pool5 的输入有两个，[1] - bottom[0] 是 conv5 卷积层出来的 feature map，由于前面进行的 pool 层，conv5 的 feature map 的 height 和 width 分别是原图尺寸的 1/16. [2] - bottom[1] 是 rois blob， 其类似于一个 $num\_rois × 5$ 的二维矩阵，行数 num\_rosi 为 bottom[1]->num()，列数为 5，其定义为：input: "rois" input_shape { dim: 1 #to be changed on-the-fly to num ROIs dim: 5 #[batch_index, x1, y1, x2, y2] zero-based indexing }bottom_index 为每次在 bottom[0] 中的第一个维度的偏移量，[x1, y1, x2, y2] 是 feature map 中点的坐标.在 feature map 中，RoI Pooling层首先计算定义的 rois 在 conv feature map 上所映射的两个坐标，即，$x1 \times spatial\_scale, y1 \times spatial\_scale$ 和 $x2 \times spatial\_scale, y2 \times spatial\_scale$，其对应的点为 [top, left], [bottom, right]，即在 feature map 中确定一个区域. 对于确定的一个区域，进行 pooled\_h x pooled\_w 等分，这里是划分为 6x6=36 个相同大小的子区域, 区域大小为 bin\_h = roi_h / pooled\_h, bin\_w = roi\_w / pooled\_w；对于每个子区域，采用 max 操作找出对应 feature map 的最大值，即为输出 top blob 的对应值.对于 bottom[0] 的每个 channel 进行相同操作.roi_pool5 有一个输出 top[0]，其尺寸为 [bottom[1] -> num( ), bottom[0] -> channels, pooled\_h, pooled\_w]，其中，pooled_h 和 pooled_w 是固定定义的，其值这里为 6. 根据其源码 roi_pooling_layer.cpp 分析，3.1. RoI Pooling 层 LayerSetUp —— 参数读取void ROIPoolingLayer<Dtype>::LayerSetUp(const vector<Blob<Dtype>*>& bottom, const vector<Blob<Dtype>*>& top) { ROIPoolingParameter roi_pool_param = this->layer_param_.roi_pooling_param(); CHECK_GT(roi_pool_param.pooled_h(), 0) << "pooled_h must be > 0"; CHECK_GT(roi_pool_param.pooled_w(), 0) << "pooled_w must be > 0"; pooled_height_ = roi_pool_param.pooled_h(); pooled_width_ = roi_pool_param.pooled_w(); spatial_scale_ = roi_pool_param.spatial_scale(); LOG(INFO) << "Spatial scale: " << spatial_scale_; }3.2. RoI Pooling 层 Reshapevoid ROIPoolingLayer<Dtype>::Reshape(const vector<Blob<Dtype>*>& bottom, const vector<Blob<Dtype>*>& top) { channels_ = bottom[0]->channels(); height_ = bottom[0]->height(); width_ = bottom[0]->width(); top[0]->Reshape(bottom[1]->num(), channels_, pooled_height_, pooled_width_); max_idx_.Reshape(bottom[1]->num(), channels_, pooled_height_, pooled_width_); }RoI Pooling 层的输出Blob - top[0] 的channels 与 bottom[0] (即 conv5) 相同； top[0] 的 num 与 RoI 的 num 相同，将 RoI 对应在 conv5 的 feature map 进行操作.3.3. RoI Pooling 层 Forward_cpu 实现void ROIPoolingLayer<Dtype>::Forward_cpu(const vector<Blob<Dtype>*>& bottom, const vector<Blob<Dtype>*>& top) { const Dtype* bottom_data = bottom[0]->cpu_data(); // conv5 得到的 feature map const Dtype* bottom_rois = bottom[1]->cpu_data(); // rois 数据 [batch_index, x1, y1, x2, y2] // Number of ROIs int num_rois = bottom[1]->num(); // rois 数量 int batch_size = bottom[0]->num(); int top_count = top[0]->count(); Dtype* top_data = top[0]->mutable_cpu_data(); caffe_set(top_count, Dtype(-FLT_MAX), top_data); //top blob data 设成最小值，(个数， 数值，位置) int* argmax_data = max_idx_.mutable_cpu_data(); caffe_set(top_count, -1, argmax_data); // For each ROI R = [batch_index x1 y1 x2 y2]: max pool over R for (int n = 0; n < num_rois; ++n) { int roi_batch_ind = bottom_rois[0]; // batch_index 下标 int roi_start_w = round(bottom_rois[1] * spatial_scale_); // top-left x1 int roi_start_h = round(bottom_rois[2] * spatial_scale_); // top-left y1 int roi_end_w = round(bottom_rois[3] * spatial_scale_); // bottom-right x2 int roi_end_h = round(bottom_rois[4] * spatial_scale_); // bottom-fight y2 CHECK_GE(roi_batch_ind, 0); CHECK_LT(roi_batch_ind, batch_size); int roi_height = max(roi_end_h - roi_start_h + 1, 1); int roi_width = max(roi_end_w - roi_start_w + 1, 1); // RoI bin 区域相对于 pooling 后图像大小的比例，一个 RoI 像素表示多少 pooling 后图像像素 const Dtype bin_size_h = static_cast<Dtype>(roi_height) / static_cast<Dtype>(pooled_height_); const Dtype bin_size_w = static_cast<Dtype>(roi_width) / static_cast<Dtype>(pooled_width_); const Dtype* batch_data = bottom_data + bottom[0]->offset(roi_batch_ind); for (int c = 0; c < channels_; ++c) { for (int ph = 0; ph < pooled_height_; ++ph) { for (int pw = 0; pw < pooled_width_; ++pw) { // Compute pooling region for this output unit: // start (included) = floor(ph * roi_height / pooled_height_) // end (excluded) = ceil((ph + 1) * roi_height / pooled_height_) int hstart = static_cast<int>(floor(static_cast<Dtype>(ph) * bin_size_h)); int wstart = static_cast<int>(floor(static_cast<Dtype>(pw) * bin_size_w)); int hend = static_cast<int>(ceil(static_cast<Dtype>(ph + 1) * bin_size_h)); int wend = static_cast<int>(ceil(static_cast<Dtype>(pw + 1) * bin_size_w)); hstart = min(max(hstart + roi_start_h, 0), height_); hend = min(max(hend + roi_start_h, 0), height_); wstart = min(max(wstart + roi_start_w, 0), width_); wend = min(max(wend + roi_start_w, 0), width_); bool is_empty = (hend <= hstart) || (wend <= wstart); const int pool_index = ph * pooled_width_ + pw; if (is_empty) { top_data[pool_index] = 0; argmax_data[pool_index] = -1; } // 对区域内所有像素找出最大元素及其位置索引 for (int h = hstart; h < hend; ++h) { for (int w = wstart; w < wend; ++w) { const int index = h * width_ + w; if (batch_data[index] > top_data[pool_index]) { top_data[pool_index] = batch_data[index]; argmax_data[pool_index] = index; } } } } } // Increment all data pointers by one channel // 各通道独立计算 pooling，结束一个 channel 后，移动指针 batch_data += bottom[0]->offset(0, 1); top_data += top[0]->offset(0, 1); argmax_data += max_idx_.offset(0, 1); } // Increment ROI data pointer // bottom[1] 的 shape - [num_rois, 5, 1, 1], offset(1) 表示将指针移动到下一个 roi 区域. bottom_rois += bottom[1]->offset(1); } }
Caffe - GPU显存优化 原文 - Squeezing Memory out of Caffe memory multiloading Memory Optimization1. Caffe - memory multiloading 功能原文 - Squeezing Memory out of CaffeCaffe 的一个优化版本 - caffe-yjxiong. 测试了一下, ResNet101 相对于官方 caffe, 能够明显节省显存占用, batchsize 可以增加很多.显存优化的 Caffe 主要实现的功能: memory multiloading在深度网络训练和测试时,可以显著地节省内存.训练时,节省一半内存;测试时, 使用 95% 的内存.该内存优化的核心思想比较简单:如果有些消费者请求一些资源, 但他们并不需要同时请求所有的资源. 一种理想情况是, 只让他们在他们真正需要的时候再使用资源, 并在结束时返回资源. 在训练深度网络时, GPU显存资源是有限的. 资源消费者即是网络中的网络层(layers/operations), 在 GPU 保存着训练时的中间结果(intermediate results).例如, 采用标准 BP 算法训练 CNNs 时, 最主要的一种内存消耗是, 保存中间激活值和其梯度(也叫作敏感度sensitivities)( intermediate activation values and the gradients values w.r.t. intermediate activation).如果将一次 forward/backward 迭代看做一个周期(cycle), 那么一个重要发现是: 在整个周期中不是所有的值都需要保存的.具体来说, 对于单个网络层, 在 feed-forwad 过程中, 当得到该网络层的输出值后, 则网络层的输入激活值就没用了. 在 BP 过程中, 当得到网络层参数的梯度和输入敏感度(input sensitivities) 后, 则网络层的输入激活值和敏感度sensitivities 就没用了. 也就是说, 每个内存块只有在计算时才占用物理内存； 且, 不再使用时, 就可以释放物理内存.内存优化核心思想的实现可以共享存储资源并节省内存. 在 TensorFlow 或 Parrots 等深度框架中, 其主要是通过动态调度一个预先分配的内存池来实现的, 类似于 OS 对于物理 RAM 的调度(dynamically scheduling a pre-allocated memory pool as the OS does with the physical RAM). 但是, 在 Caffe 中, 如何实现呢? 由于 Caffe 具有完全不同的内存模型, 其每个内存块是由 OS/GPU 来自动分配的. 在内存池中实现类似功能是比较棘手的.相反地, 采用了一种替代方法. 假设网络结构是固定的(大部分情况都是这样), 在运行第一次迭代前, 来判断一个内存块的生命周期(life-cycle). 这里称之为 "dry-run", 即, 模拟计算流以寻找可以共享存储的 SyncedMem-s(Caffe 中的基本内存单元). 当找出 SyncedMem-s 后, 即可将其分配到单个内存插槽(single memory slot). 在实际训练和测试时, memory multiloading 的实现是将共享 SyncedMem-s 都由相同内存块来存储和读取的. 由于在 "dry-run" 中已经确定了依赖关系, multiloading 是不会损坏数据, 且能节省大量内存使用的.最后, 在实现 Caffe 的 memory multiloading 功能时, 发现的一个有意思的问题. Caffe 有一些网络层不进行计算, 只共享网络层的输入和输出 blobs(ShareData/ShareDiff). 这种情况在某些时候可能破坏 "dry-run" 处理所获得的依赖关系. 因此, 需要先让这些网络层告诉框架网络层所做的事情( first make these layers tell the framework that they doing this.) 在某些情况, 这些网络层是堆叠在一起以构建特定的结构. 由于会引入递归分享(recursive sharing, 一系列的 blobs 共享其激活值/梯度内存块), 导致问题更加棘手. 最终, 采用的方案是, 著名的 并查集(或不相交集合)数据结构( "union-find" (disjoint-set) data structure) 来使 memory multiloading 的实现更加干净和优雅.memory multiloading 功能实现的文件在:https://github.com/yjxiong/caffe/blob/action_recog/src/caffe/net.cpp#L10882. 内存优化 Memory Optimization2.1 使用方法该版本 Caffe 中, 内存优化是由网络配置中的mem_param 来实现的.例如:name: "example_net" mem_param { optimize_train: true optimize_test: true exclude_blob: "fc1" exclude_blob: "fc2" } optimize_train - 是否在训练网络中进行优化. 如果设为 True, 可以节省训练时一半的内存. optimize_test - 是否在测试网络中进行优化. 如果设为 True, 可以节省测试时大部分内存. exclude_blob - 指定优化处理中所排除的 blobs. 排除一个 blob, 可以确保该 blob 可以被 Python/Matlab 接口安全的读取. 默认情况, 网络所有的 inputs, outputs 和 losses 都是被排除的. 2.2 工作原理节省内存的原因是, 对于训练的每次 forward 和 backward, 前一 blobs 使用的内存会被后面 blobs 重用.内存优化模块通过确定网络中 blobs 的依赖关系来工作的. 有时称之为 multiloading.具体来说, 在网络训练开始前, 运行一次 "dry-run" 来确定重用 blob 内存块的方式. 这是一个静态优化过程.另一方面, Parrots 深度学习框架是通过动态调度内存使用的, 具有更优的内存节省和更好的灵活性.
From 使用caffe训练时Loss变为nan的原因(https://mp.weixin.qq.com/s/LDaHvcwOnx80EKhP2sx2Yg)From Deep Learning 之 训练过程中出现NaN问题(http://blog.csdn.net/BVL10101111/article/details/76086344)1. 梯度爆炸原因：梯度变得非常大，使得学习过程难以继续现象：观察log，注意每一轮迭代后的loss。loss随着每轮迭代越来越大，最终超过了浮点型表示的范围，就变成了NaN。措施： 减小solver.prototxt中的base_lr，至少减小一个数量级。如果有多个loss layer，需要找出哪个损失层导致了梯度爆炸，并在train_val.prototxt中减小该层的loss_weight，而非是减小通用的base_lr.3设置clip gradient，用于限制过大的diff2. 不当的损失函数原因：有时候损失层中loss的计算可能导致NaN的出现。比如，给InfogainLoss层（信息熵损失）输入没有归一化的值，使用带有bug的自定义损失层等等。现象：观测训练产生的log时一开始并不能看到异常，loss也在逐步的降低，但突然之间NaN就出现了。措施：看看你是否能重现这个错误，在loss layer中加入一些输出以进行调试。示例：有一次我使用的loss归一化了batch中label错误的次数。如果某个label从未在batch中出现过，loss就会变成NaN。在这种情况下，可以用足够大的batch来尽量避免这个错误。3. 不当的输入原因：输入中就含有NaN。现象：每当学习的过程中碰到这个错误的输入，就会变成NaN。观察log的时候也许不能察觉任何异常，loss逐步的降低，但突然间就变成NaN了。措施：重整你的数据集，确保训练集和验证集里面没有损坏的图片。调试中你可以使用一个简单的网络来读取输入层，有一个缺省的loss，并过一遍所有输入，如果其中有错误的输入，这个缺省的层也会产生NaN。案例：有一次公司需要训练一个模型，把标注好的图片放在了七牛上，拉下来的时候发生了dns劫持，有一张图片被换成了淘宝的购物二维码，且这个二维码格式与原图的格式不符合，因此成为了一张“损坏”图片。每次训练遇到这个图片的时候就会产生NaN。良好的习惯是，你有一个检测性的网络，每次训练目标网络之前把所有的样本在这个检测性的网络里面过一遍，去掉非法值。4. 池化层中步长比核的尺寸大如下例所示，当池化层中stride > kernel的时候会在y中产生NaNlayer { name: "faulty_pooling" type: "Pooling" bottom: "x" top: "y" pooling_param { pool: AVE stride: 5 kernel: 3 } } 致谢：http://stackoverflow.com/questions/33962226/common-causes-of-NaNs-during-training一般来说，出现NaN有以下几种情况：1.如果在迭代的100轮以内，出现NaN，一般情况下的原因是因为你的学习率过高，需要降低学习率。可以不断降低学习率直至不出现NaN为止，一般来说低于现有学习率1-10倍即可。2.如果当前的网络是类似于RNN的循环神经网络的话，出现NaN可能是因为梯度爆炸的原因，一个有效的方式是增加“gradient clipping”（梯度截断来解决）3.可能用0作为了除数;4.可能0或者负数作为自然对数5.需要计算loss的数组越界（尤其是自己，自定义了一个新的网络，可能出现这种情况）6.在某些涉及指数计算，可能最后算得值为INF（无穷）（比如不做其他处理的softmax中分子分母需要计算exp（x），值过大，最后可能为INF/INF，得到NaN，此时你要确认你使用的softmax中在计算exp（x）做了相关处理（比如减去最大值等等））参考： http://russellsstewart.com/notes/0.html
Caffe 在图像分类模型的训练时, 效率起见, 未直接从图片列表读取图片, 训练数据往往是采用 LMDB 或 HDF5 格式.LMDB格式的优点：基于文件映射IO（memory-mapped），数据速率更好对大规模数据集更有效.HDF5的特点：易于读取类似于mat数据，但数据压缩性能更强需要全部读进内存里，故HDF5文件大小不能超过内存，可以分成多个HDF5文件，将HDF5子文件路径写入txt中.I/O速率不如LMDB.1. LMDB创建import lmdb import caffe lmdb_file = '/path/to/data_lmdb' N = 1000 # 准备 data 和 labels X = np.zeros((N, 3, 224, 224), dtype=np.uint8) # data y = np.zeros(N, dtype=np.int64) # labels env = lmdb.open(lmdb_file, map_size=int(1e12)) txn = env.begin(write=True) for i in range(N): datum = caffe.proto.caffe_pb2.Datum() datum.channels = X.shape[1] datum.height = X.shape[2] datum.width = X.shape[3] datum.data = X[i].tobytes() # or .tostring() if numpy < 1.9 datum.label = int(y[i]) # 以上五行也可以直接： datum = caffe.io.array_to_datum(data, label) str_id = '{:08}'.format(i) txn.put(str_id, datum.SerializeToString()) # in Python3 # txn.put(str_id.encode('ascii'), datum.SerializeToString())2. LMDB读取import numpy as np import lmdb import caffe env = lmdb.open('data_lmdb', readonly=True) txn = env.begin() lmdb_cursor = txn.cursor() datum = caffe.proto.caffe_pb2.Datum() for key, value in lmdb_cursor: print '{},{}'.format(key, value) datum.ParseFromString(value) flat_data = np.fromstring(datum.data, dtype=np.uint8) data = flat_data.reshape(datum.channels, datum.height, datum.width) # 或 data = caffe.io.datum_to_array(datum) labels = datum.label3. HDF5创建和读取W1:import h5py import numpy as np # 创建HDF5文件 imgsData = np.zeros((10,3,224,224)) # Images labels = range(10) # Labels f = h5py.File('HDF5_FILE.h5','w') # 创建一个h5文件 f['datas'] = imgsData # 写入Images数据 f['labels'] = labels # 写入Labels数据 f.close() # # 读取HDF5文件 f = h5py.File('HDF5_FILE.h5','r') # 打开h5文件 f_keys = f.keys() imgsData = f['datas'][:] labels = f['labels'][:] f.close() W2:import h5py datas = np.random.rand(100, 1000, 1000).astype('float32') labels = np.random.rand(1, 1000, 1000).astype('float32') # Create a new file f = h5py.File('data.h5', 'w') f.create_dataset('datas', data=datas) f.create_dataset('labels', data=labels) f.close() # Load hdf5 dataset f = h5py.File('data.h5', 'r') X = f['datas'] Y = f['labels'] f.close()4. LMDB 数据集创建""" a modified version of CRNN torch repository https://github.com/bgshih/crnn/blob/master/tool/create_dataset.py """ import fire import os import lmdb import cv2 import numpy as np def checkImageIsValid(imageBin): if imageBin is None: return False imageBuf = np.frombuffer(imageBin, dtype=np.uint8) img = cv2.imdecode(imageBuf, cv2.IMREAD_GRAYSCALE) imgH, imgW = img.shape[0], img.shape[1] if imgH * imgW == 0: return False return True def writeCache(env, cache): with env.begin(write=True) as txn: for k, v in cache.items(): txn.put(k, v) def createDataset(inputPath, gtFile, outputPath, checkValid=True): """ Create LMDB dataset for training and evaluation. ARGS: inputPath : input folder path where starts imagePath outputPath : LMDB output path gtFile : list of image path and label checkValid : if true, check the validity of every image """ os.makedirs(outputPath, exist_ok=True) env = lmdb.open(outputPath, map_size=1099511627776) cache = {} cnt = 1 with open(gtFile, 'r', encoding='utf-8') as data: datalist = data.readlines() nSamples = len(datalist) for i in range(nSamples): imagePath, label = datalist[i].strip('\n').split('\t') imagePath = os.path.join(inputPath, imagePath) # # only use alphanumeric data # if re.search('[^a-zA-Z0-9]', label): # continue if not os.path.exists(imagePath): print('%s does not exist' % imagePath) continue with open(imagePath, 'rb') as f: imageBin = f.read() if checkValid: try: if not checkImageIsValid(imageBin): print('%s is not a valid image' % imagePath) continue except: print('error occured', i) with open(outputPath + '/error_image_log.txt', 'a') as log: log.write('%s-th image data occured error\n' % str(i)) continue imageKey = 'image-%09d'.encode() % cnt labelKey = 'label-%09d'.encode() % cnt cache[imageKey] = imageBin cache[labelKey] = label.encode() if cnt % 1000 == 0: writeCache(env, cache) cache = {} print('Written %d / %d' % (cnt, nSamples)) cnt += 1 nSamples = cnt-1 cache['num-samples'.encode()] = str(nSamples).encode() writeCache(env, cache) print('Created dataset with %d samples' % nSamples) if __name__ == '__main__': fire.Fire(createDataset)5. LMDB 数据集读取import lmdb import numpy as np import cv2 lmdb_file = "/path/to/lmdb" lmdb_env = lmdb.open(lmdb_file) lmdb_txn = lmdb_env.begin() lmdb_cursor = lmdb_txn.cursor() for key, value in lmdb_cursor: print('[INFO]', key) img = cv2.imdecode(np.fromstring(value, np.uint8), 3); cv2.imshow("demo", img) cv2.waitKey(0)