2. caffe 模型配置文件 prototxt 详解

每个模型由多个 层 构成

layer {{{{

name: "{}" #层名字,可随意取名

type: "{}" #层类型 数据层Data 卷积层Convolution 池化层Pooling 非线性变换、内积运算,以及数据加载、归一化和损失计算 等

bottom: "{}"# 层入口 输入

top: "{}"{{}}# 层出口 输出  可以有多个 bottom 和 top 表示有多条数据通道

}}}}

2.1 相关头文件

然后我们从头文件看看:

Caffe中与Layer相关的头文件有7个,

2.1.1 基类层 layer.hpp:

layer.hpp`头文件里,包含了这几个头文件:

#include "caffe/blob.hpp"

#include "caffe/common.hpp"

#include "caffe/proto/caffe.pb.h"

#include "caffe/util/device_alternate.hpp"

父类Layer,定义所有layer的基本接口。

1. layer中有这三个主要参数:

LayerParameter layer_param_;

// 这个是protobuf文件中存储的layer参数

vector>> blobs_;

// 这个存储的是layer的参数,在程序中用的,数据流

vector param_propagate_down_;

// 这个bool表示是否计算各个blob参数的diff,即传播误差

2. 其三个主要接口:

virtual void SetUp(const vector*>& bottom, vector*>* top)

// 需要根据实际的参数设置进行实现,对各种类型的参数初始化;

inline Dtype Forward(const vector*>& bottom, vector*>* top);

// 前向计算

inline void Backward(const vector*>& top, const vector& propagate_down, const *>* bottom);

// 反向传播更新

输入统一都是bottom,输出为top。

其中Backward里面有个propagate_down参数,用来表示该Layer是否反向传播参数。

Forward_cpu、Forward_gpu和Backward_cpu、Backward_gpu,这些接口都是virtual,

2.1.2 数据层 data_layers.hpp:

data_layers.hpp这个头文件包含了这几个头文件

#include "boost/scoped_ptr.hpp" // 是一个类似于auto_ptr的智能指针

// scoped_ptr(局部指针)名字的含义:这个智能指针只能在作用域里使用,不希望被转让

// 把拷贝构造函数和赋值操作都声明为私有的,禁止对智能指针的复制操作,保证了被它管理的指针不能被转让所有权。

// private:

// scoped_ptr(scoped_ptr const &);

// scoped_ptr & operator=(scoped_ptr const &);

#include "hdf5.h"

#include "leveldb/db.h"//数据类型 数据库文件

#include "lmdb.h"

#include "caffe/blob.hpp"

#include "caffe/common.hpp"

#include "caffe/filler.hpp"// 在网络初始化时,根据layer的定义进行初始参数的填充,

// type == "constant" ConstantFiller(param);

// type == "gaussian" GaussianFiller(param);

// type == "positive_unitball" PositiveUnitballFiller(param);

// type == "uniform" UniformFiller(param);

// type == "xavier" XavierFiller(param);

//

#include "caffe/internal_thread.hpp"// 里面封装了pthread函数,

// 继承的子类可以得到一个单独的线程,主要作用是在计算当前的一批数据时,在后台获取新一批的数据。

#include "caffe/layer.hpp"

#include "caffe/proto/caffe.pb.h"

继承自父类Layer,定义与输入数据操作相关的子Layer,

例如DataLayer,HDF5DataLayer和ImageDataLayer等。

data_layer作为原始数据的输入层,处于整个网络的最底层,

它可以从数据库leveldb、lmdb中读取数据,也可以直接从内存中读取,还可以从hdf5,甚至是原始的图像读入数据。

LevelDB是Google公司搞的一个高性能的key/value存储库,调用简单,数据是被Snappy压缩,据说效率很多,可以减少磁盘I/O.

LMDB(Lightning Memory-Mapped Database),是个和levelDB类似的key/value存储库,但效果似乎更好些,其首页上写道“ultra-fast,ultra-compact”,

HDF(Hierarchical Data Format)是一种为存储和处理大容量科学数据而设计的文件格式及相应的库文件,

当前最流行的版本是HDF5,其文件包含两种基本数据对象:

群组(group):类似文件夹,可以包含多个数据集或下级群组;

数据集(dataset):数据内容,可以是多维数组,也可以是更复杂的数据类型。

layer {

name: "data"

type: "Data"

top: "data"

top: "label"

include {

phase: TRAIN     # 用于训练

}

transform_param {

mirror: 1            # 镜像

crop_size: 227   #

# substract mean value(RGB three channel):

#these mean_values can equivalently be replaced with a mean.binaryproto file as

# mean_file: name_of_mean_file.binaryproto

mean_value: 104  # 去中心化(减去平均值)

mean_value: 117

mean_value: 123

}

data_param {

source: "examples/imagenet/ilsvrc12_train_lmdb"  # 数据库文件名

batch_size: 32   # 每次处理的样本数目

backend: LMDB    # 数据库类型,默认为LMDB,可选LevelDB

# rand_skip:在开始的时候跳过rand_skip个输入数据,这个对异步SGD有效

}

}

均值文件 name_of_mean_file.binaryproto

cd ~/caffe

build/tools/compute_image_mean examples/imagenet/ilsvr12_train_lmdb

data/ilsvrc12/imagenet_mean.binaryproto

2.1.3 vision_layers.hpp:

继承自父类Layer,定义与特征表达相关的子Layer,

例如 卷积层ConvolutionLayer,池化层PoolingLayer和 LRNLayer等。

vision_layer主要是图像卷积的操作,像convolusion、pooling、LRN都在里面,

按官方文档的说法,是可以输出图像的,这个要看具体实现代码了。

里面有个im2col的实现,看caffe作者的解释,主要是为了加速卷积的。

2.1.4 neuron_layers.hpp:

继承自父类Layer,定义与非线性变换相关的子Layer,神经元激活层,

例如ReLULayer,TanHLayer和SigmoidLayer等。

输入了data后,就要计算了,比如常见的sigmoid、tanh等等,

这些都计算操作被抽象成了neuron_layers.hpp里面的类NeuronLayer,

这个层只负责具体的计算,因此明确定义了输入

ExactNumBottomBlobs()和ExactNumTopBlobs()都是常量1,即输入一个blob,输出一个blob。

2.1.5 common_layers.hpp: 继承自父类Layer,定义与中间结果数据变形、逐元素操作相关的子Layer,

例如 通道合并ConcatLayer,点乘 InnerProductLayer和 SoftmaxLayer等 softmax归一化。

NeruonLayer仅仅负责简单的一对一计算,

而剩下的那些复杂的计算则通通放在了common_layers.hpp中。像

ArgMaxLayer、ConcatLayer、FlattenLayer、SoftmaxLayer、SplitLayer和SliceLayer等

各种对blob增减修改的操作。

2.1.6 loss_layers.hpp:

继承自父类Layer,定义与输出误差计算相关的子Layer,

例如 欧几里得距离损失 EuclideanLossLayer,SoftmaxWithLossLayer 和 HingeLossLayer等。

data layer和common layer都是中间计算层,

虽然会涉及到反向传播,但反向传播的源头来自于loss_layer,即网络的最终端。

这一层因为要计算误差,所以输入都是2个blob,输出1个blob。

2.1.7 layer_factory.hpp:

Layer工厂模式类,

负责维护现有可用layer和相应layer构造方法的映射表。

2.2 数据层 Data

数据通过数据层进入Caffe,数据层在整个网络的底部。

数据可以来自高效的数据库(LevelDB 或者 LMDB),直接来自内存。

如果不追求高效性,可以以HDF5或者一般图像的格式从硬盘读取数据。

一些基本的操作,如:mean subtraction,

scaling,

random cropping, and

mirroring 均可以直接在数据层上进行指定。

type: "Data"

数据格式一般有 LevelDB和 LMDB

数据层 一般无 bottom: ,会有多个 top:

例如:

top: "data"     数据 x

top: "label"    标签 y   对应的是分类模型 监督学习

incude{

phase:TRAIN   一般训练和测试时是不一样的,这里表示训练阶段的层,如果没有include标签,表示即在训练阶段又在测试阶段

}

2.2.1 数据库格式数据 Database

类型:Data

必须参数:

source: 包含数据的目录名称

batch_size: 一次处理 的 输入的数量,过大内存不够

可选参数:

rand_skip: 在开始的时候从输入中跳过这个数值,这在异步随机梯度下降(SGD)的时候非常有用

backend [default LEVELDB]: 选择使用 LEVELDB 或者 LMDB

2.2.2 直接来自内存 In-Memory

类型: MemoryData

必需参数:

batch_size, channels, height, width: 指定从内存读取数据的大小

MemoryData层直接从内存中读取数据,而不是拷贝过来。

因此,要使用它的话,你必须调用

MemoryDataLayer::Reset (from C++)

或者Net.set_input_arrays (from Python)以此指定一块连续的数据(通常是一个四维张量)。

2.2.3 HDF5 Input

类型: HDF5Data

必要参数:

source: 需要读取的文件名

batch_size:一次处理的输入的数量

2.2.4 HDF5 Output

类型: HDF5Output

必要参数:

file_name: 输出的文件名

HDF5的作用和这节中的其他的层不一样,它是把输入的blobs写到硬盘

2.2.5 来自图像文件 Images

类型: ImageData

必要参数:

source: text文件的名字,每一行给出一张图片的文件名和label

batch_size: 一个batch中图片的数量

可选参数:

rand_skip:在开始的时候从输入中跳过这个数值,这在异步随机梯度下降(SGD)的时候非常有用

shuffle [default false]

new_height, new_width: 把所有的图像resize到这个大小

2.3 激励层(neuron_layers) 激活层

一般来说,激励层是element-wise的操作,输入和输出的大小相同,一般情况下就是一个非线性函数。

数据输入输出维度不变

输入:

n×c×h×w

输出:

n×c×h×w

2.3.1 ReLU / Rectified-Linear and Leaky-ReLU 最小阈值激活

标准, f(x) = max(0,x) ,当x > 0时输出x,但x <= 0时输出negative_slope 阈值

Leaky-ReLU   max(0.1x,x)

定义:

layer {

name: "relu1"

type: "ReLU"

bottom: "conv1"

top: "conv1"

}

template

__global__ void ReLUForward(const int n, const Dtype* in, Dtype* out,

Dtype negative_slope) {

CUDA_KERNEL_LOOP(index, n) {

out[index] = in[index] > 0 ? in[index] : in[index] * negative_slope;

// (a*x,x) 正常无上截断

}

}

//

================================

// 前传

template

__global__ void ReLUXForward(const int n, const Dtype* in, Dtype* out,

Dtype negative_slope, Dtype relux_cur_max) {

CUDA_KERNEL_LOOP(index, n) {

out[index] = in[index] > 0 ? in[index] : in[index] * negative_slope;

out[index] = out[index] < relux_cur_max ? out[index] : relux_cur_max;

// (a*x, x|cur_max) 上截断=========

}

}

template

__global__ void ReLUXRForward(const int n, const Dtype* in, Dtype* out,

Dtype negative_slope, Dtype relux_cur_max, Dtype rate) {

CUDA_KERNEL_LOOP(index, n)

{

if (in[index] < 0) {

out[index] = in[index] * negative_slope; // (a*x, x|cur_max, relux_cur_max+ det*ret)

}

else if (in[index] < relux_cur_max)

{

out[index] = in[index];

}

else

{

out[index] = relux_cur_max + (in[index] - relux_cur_max) * rate;// 一次调整斜率 收缩

}

}

}

template

__global__ void ReLUYRForward(const int n, const Dtype* in, Dtype* out,

Dtype negative_slope, Dtype relux_cur_max, Dtype relux_last_max, Dtype rate) {

CUDA_KERNEL_LOOP(index, n) {

if (in[index] < 0)

{

out[index] = in[index] * negative_slope;

}

else if (in[index] < relux_cur_max)

{

out[index] = in[index];

}

else if (in[index] < relux_last_max)

{

out[index] = relux_cur_max + (in[index] - relux_cur_max) * rate;// 一次调整斜率 收缩

}

else

{

out[index] = relux_cur_max + (relux_last_max - relux_cur_max) * rate;// 二次调整斜率 收缩

}

}

}

/// 反向传播 倒数===================================

template

__global__ void ReLUBackward(const int n, const Dtype* in_diff,

const Dtype* in_data, Dtype* out_diff, Dtype negative_slope) {

CUDA_KERNEL_LOOP(index, n)

{// (a*diff, diff) 正常无上截断====

out_diff[index] = in_diff[index] * ((in_data[index] > 0)

+ (in_data[index] <= 0) * negative_slope);

}

}

template

__global__ void ReLUXBackward(const int n, const Dtype* in_diff,

const Dtype* in_data, Dtype* out_diff, Dtype negative_slope, Dtype relux_cur_max)

{

CUDA_KERNEL_LOOP(index, n)

{// 有上截断 (a*diff, diff, 0)

out_diff[index] = in_diff[index] * (((in_data[index] > 0) && (in_data[index] <= relux_cur_max))

+ (in_data[index] <= 0) * negative_slope);

}

}

template

__global__ void ReLUXRBackward(const int n, const Dtype* in_diff,

const Dtype* in_data, Dtype* out_diff, Dtype negative_slope, Dtype relux_cur_max, float rate)

{

CUDA_KERNEL_LOOP(index, n)

{// 一次斜率收缩======== (a*diff, diff, rate)

if (in_data[index] > relux_cur_max) {

out_diff[index] = in_diff[index] * rate;

}

else {

out_diff[index] = in_diff[index] * ((in_data[index] > 0)

+ (in_data[index] <= 0) * negative_slope);

}

}

}

template

__global__ void ReLUYRBackward(const int n, const Dtype* in_diff,

const Dtype* in_data, Dtype* out_diff, Dtype negative_slope, Dtype relux_cur_max, Dtype relux_last_max, float rate)

{

CUDA_KERNEL_LOOP(index, n)

{// 二次斜率调整

if (in_data[index] > relux_last_max)

{

out_diff[index] = 0;// 这里有点问题??

}

else if (in_data[index] > relux_cur_max)

{

out_diff[index] = in_diff[index] * rate;

}

else

{

out_diff[index] = in_diff[index] * ((in_data[index] > 0)

+ (in_data[index] <= 0) * negative_slope);

}

}

}

2.3.2 Sigmoid    负指数导数激活

标准   f(x) = 1/(1+exp(-x)) x = 0, f(x) = y = 0.5

映射到 0~1之间

sigmoid函数连续,光滑,严格单调,以(0,0.5)中心对称,是一个非常良好的阈值函数。

当x趋近负无穷时,y趋近于0;趋近于正无穷时,y趋近于1;x=0时,y=0.5。

当然,在x超出[-6,6]的范围后,函数值基本上没有变化,值非常接近,在应用中一般不考虑。

导数:

f′(x) = f(x) * (1 − f(x))

定义:

layer {

name: "encode1neuron"

bottom: "encode1"

top: "encode1neuron"

type: "Sigmoid"

}

2.3.3 TanH / Hyperbolic Tangent 双曲正切

请注意sigmoid函数和TanH函数在纵轴上的区别。

sigmoid函数将实数映射到(0,1)。

TanH将实数映射到(-1,1)。

tanh(x) = ( exp(x) − exp(−x) ) / ( exp(x) + exp(−x) )

定义:

layer {

name: "layer"

bottom: "in"

top: "out"

type: "TanH"

}

2.3.4 绝对值激活 Absolute Value

ABSVAL层通过 y = abs(x) 计算每一个输入x的输出。

定义:

layer {

name: "layer"

bottom: "in"

top: "out"

type: "AbsVal"

}

2.3.5 Power 平移乘方激活

POWER层通过 y = (shift + scale * x) ^ power计算每一个输入x的输出。

定义:

layer {

name: "layer"

bottom: "in"

top: "out"

type: "Power"

power_param {

power: 1

scale: 1

shift: 0

}

}

2.3.6 BNLL 二项正态对数似然 激活

BNLL (binomial normal log likelihood) 层通过

y = log(1 + exp(x)) 计算每一个输入x的输出

定义:

layer {

name: "layer"

bottom: "in"

top: "out"

type: BNLL

}

2.4 视觉层(vision_layers)

2.4.1 卷积层(Convolution)

类型:CONVOLUTION

例子:

layers {

name: "conv1"             # 名字

type: CONVOLUTION         # 卷积层类型

bottom: "data"            # 输入层

top: "conv1"              # 输出层

blobs_lr: 1               # 权重卷积核参数更新学习率 learning rate multiplier for the filters

blobs_lr: 2               # 偏置参数更新学习率 learning rate multiplier for the biases

weight_decay: 1           # 权重卷积核参数更新 衰减系数 weight decay multiplier for the filters

weight_decay: 0 # 偏置参数更新 衰减系数 weight decay multiplier for the biases

convolution_param {

num_output: 96       # 卷积核数量,即输出通道数量 learn 96 filters   必选

kernel_size: 11       # 卷积核尺寸 11*11 each filter is 11x11         必选

stride: 4             # 卷积步长,输出特征图尺寸,尺寸变为 1/4 step 4 pixels between each filter application

weight_filler {       # 权重初始化设置

type: "gaussian" # 高斯分布初始化卷积核参数 initialize the filters from a Gaussian

std: 0.01         # 标准差0.01,默认均值0 distribution with stdev 0.01 (default mean: 0) }

bias_filler {     # 偏置初始化设置

type: "constant" # 常量 initialize the biases to zero (0)

value: 0         # 0

}

}

}

}

2.4.1.1 可选参数:

bias_filler: 偏置的初始化方法

bias_term [default true]:指定是否是否开启偏置项  y = w*x + b 或则 y = w*x

pad (or pad_h and pad_w) [default 0]: 指定在输入图的每一边加上多少个像素 一般为 卷积核尺寸-1的一半

stride (or stride_h and stride_w) [default 1]:指定滤波器的步长

group (g) [default 1]: 如果g>1,那么将每个滤波器都限定只与某个输入的子集有关联。

换句话说,将输入分为g组,同时将输出也分为g组。

那么第i组输出只与第i组输入有关。  之后再经过点卷积 或者通道重排 结合不同通道信息

2.4.1.2通过卷积后的大小变化:

输入:

[n,i_w,i_h,W]

特征图大小 i_w,i_h

通道数量 W

个数 n

卷积核尺寸:

[k_w,k_h,W] × V  步长 [s_h,s_w]

卷积核尺寸 [k_w,k_h,W]

个数 V

输出:

[n,o_w,o_h,V]

特征图大小 o_w,o_h

通道数量 V

个数 n

其中:

o_h = (i_h + 2×padh − kernelh)/ s_h + 1,

o_w = (o_w + 2×padw − kernelw)/ s_w + 1,

一般填充数量都会等于 (kernel - 1)/2

所以卷积核输出的特征图尺寸一般不会变换,变化的是 通道数量

如果有步长,则,等于 原尺寸/步长

2.4.1.3in examples\mnist\lenet_train_test.prototxt

layer {

name: "conv1" // 层的名字

type: "Convolution" // 层的类型,说明具体执行哪一种计算

bottom: "data" // 层的输入数据Blob的名字

top: "conv1" // 层的输出数据Blob的名字

param { // 层的权值和偏置相关参数

lr_mult: 1   // 权重学习率

}

param {

lr_mult: 2   // 偏置学习率

}

convolution_param { // 卷积层卷积运算相关的参数

num_output: 20    // 输出通道数量 卷积核个数

kernel_size: 5   // 5*5卷积核尺寸

stride: 1         // 步长为1 特征图尺寸不变 有填充

weight_filler {   // 权重 初始化

type: "xavier"

}

bias_filler {     // 偏置初始化

type: "constant"

}

}

}

2.4.2 池化层(Pooling)

类型:POOLING

例子:

layers {

name: "pool1" # 名字

type: POOLING       # 池化层类型

bottom: "conv1"      # 输入层

top: "pool1"         # 输出层

pooling_param {      # 池化层 参数

pool: MAX        # 最大值池化MAX 均值池化MEAN

kernel_size: 3   # 池化核大小 pool over a 3x3 region   必须要的参数

stride: 2       # 步长 降低分辨率 step two pixels (in the bottom blob) between pooling regions

}

}

2.4.2.1 可选参数:

pool [default MAX]:pooling的方法,目前有MAX, AVE, 和STOCHASTIC三种方法

pad (or pad_h and pad_w) [default 0]:指定在输入的每一遍加上多少个像素

stride (or stride_h and stride_w) [default 1]:指定过滤器的步长

2.4.2.2 通过池化后的大小变化

输入:

[n,i_w,i_h,W]

池化核尺寸:

[k_w,k_h]  数量 W  步长 [s_h,s_w]

输出:

[n,o_w,o_h,W]

其中:

o_h = (i_h + 2×padh − k_h)/ s_h + 1,

o_w = (o_w + 2×padw − k_w)/ s_w + 1,

一般填充数量都会等于 (kernel - 1)/2

所以输出尺寸基本上 等于 原尺寸/步长

2.4.3 局部响应归一化层 LRN(Local Response Normalization)

类型:LRN

可选参数:

local_size [default 5]:

对于cross channel LRN为 需要求和的 邻近channel的数量;

对于within channel LRN为 需要求和的 空间区域的边长;

alpha [default 1]: scaling参数,缩放比例;

beta [default 5]: 指数β;

norm_region [default ACROSS_CHANNELS]:

选择LRN实现的方法:

1. ACROSS_CHANNELS ;

2. WITHIN_CHANNEL

计算公式:

对每一个输入除以 xi / (1 + (α/n)⋅ ∑ xi^2 )^β

在这里,参数α是scaling参数,参数β是指数。而参数 n 对应local_size 的大小。

解析:

一种提高准确度的技术方法。

跟激活函数是有区别的,LRN一般是在激活、池化后进行的一中处理方法。

类似的还有 BN层 批归一化。

是对一个局部的输入区域进行的归一化。

有两种不同的形式:

1. ACCROSS_CHANNEL;

2. WITHIN_CHANNEL。

其实很好从字面上进行理解。

第一种方法综合了不同的channel(类似点卷积的左右),

而在一个channel里面只取1*1(所以size是localsize×1×1)。

第二种方法中,

不在channel方向上扩展,只在单一channel上进行空间扩展(所以size是1×localsize×localsize)。

2.5 损失层(Loss Layers)

深度学习是通过最小化 网络输出和目标的 误差Loss 来 驱动学习。

2.5.1 指数归一化 对数误差 Softmax loss softmax+Loss组成

类型: SoftmaxWithLoss

Softmax Loss层应用于多标签分类。

对于输入,计算了multinomial logistic loss。

在概念上近似等于一个Softmax层加上一个multinomial logistic loss层。

但在梯度的计算上更加稳定。

ai = zi/sum(exp(zi))   softmax 指数 归一化

Loss = -log(aj)        指定类别 负对数 误差

对指定类别的 输出概率(归一化后为0~1之间) 做log

越接近1,越接近目标值,loss越趋近于0

在0~1之间 log为负数,所以在前面加了一个 符号

2.5.2 欧氏距离误差 EuclideanLoss

类型: EuclideanLoss

Euclidean loss层计算了两个输入之差的平方和

sum(zi-yi)^2

2.5.3 HINGE_LOSS   “最大间隔”分类误差 (max margin)

最著名的应用是作为SVM的目标函数。

其二分类情况下,公式如下:

l(y) = max( 0, 1 − t⋅y)

其中,y是预测值(-1到1之间),t为目标值(±1)。

其含义为,y的值在-1到1之间就可以了,并不鼓励|y|>1,

即并不鼓励分类器过度自信,让某个可以正确分类的样本距离分割线的距离超过1并不会有任何奖励。

类型: HingeLoss

例子:

带有L1正则化项:

L1 Normlayers {

name: "loss"

type: HINGE_LOSS

bottom: "pred"

bottom: "label"

}

带有L2正则化项:

L2 Normlayers {

name: "loss"

type: HINGE_LOSS

bottom: "pred"

bottom: "label"

top: "loss"

hinge_loss_param {

norm: L2

}

}

可选参数:

norm [default L1]: 选择L1或者L2范数

输入:

n×c×h×w Predictions 预测值

n×1×1×1 Labels        真实标签

输出

1×1×1×1 Computed Loss

2.5.4 Sigmoid 交叉熵损失函数

类型:SigmoidCrossEntropyLoss

sigmod 将输出 映射到 0~1之间: pi = 1/(1+exp(-zi))

交叉熵损失: 1/n * sum (yi*log(pi))

2.5.5 信息增益损失函数(InformationGain Loss)

类型:InfogainLoss

这是在文本处理中用到的损失函数.

2.5.6 Accuracy and Top-k

类型:Accuracy

用来计算输出和目标的正确率,事实上这不是一个loss,而且没有backward这一步。

2.6 一般层(Common Layers)

2.6.1 全连接层 Inner Product FC

类型:InnerProduct

例子:

layer {

name: "fc8"           # 名字

type: "InnerProduct"   # 类型

# 权重学习率、衰减 learning rate and decay multipliers for the weights

param { lr_mult: 1 decay_mult: 1 }

# 偏置学习率、衰减 learning rate and decay multipliers for the biases

param { lr_mult: 2 decay_mult: 0 }

inner_product_param {

num_output: 1000     # 输 出 数量

weight_filler {

type: "gaussian"   # 权重初始化

std: 0.01

}

bias_filler {        # 偏置初始化

type: "constant"

value: 0

}

}

bottom: "fc7"

top: "fc8"

}

通过全连接层后的大小变化:

输入:n×ci×hi×wi   其实需要先经过 flatten层摊平 1*N * N*m --> 1*m

输出:n×co×1×1

2.6.2 分割层 Splitting

类型:Split

Splitting层可以把一个输入blob分离成多个输出blobs。

这个用在当需要把一个blob输入到多个输出层的时候。

2.6.3 摊平层 Flattening

类型:Flatten

Flatten层是把一个输入的大小为n * c * h * w变成一个简单的向量,其大小为 n * (c*h*w) * 1 * 1。

2.6.4 变形层 Reshape

类型:Reshape

例子:

layer {

name: "reshape"

type: "Reshape"

bottom: "input"

top: "output"

reshape_param {

shape {

dim: 0 # copy the dimension from below 直接从底层复制

dim: 2

dim: 3

dim: -1 # infer it from the other dimensions 从其他的数据里面推测这一维应该是多少。

}

}

}

2.6.4.1 说明

输入:单独的一个blob,可以是任意维;

输出:同样的blob,但是它的维度已经被我们人为地改变,维度的数据由reshap_param定义。

可选参数:

shape

Reshape层被用于改变输入的维度,而不改变输入的具体数据。

就像Flatten层一样。只是维度被改变而已,这个过程不涉及数据的拷贝。

输出的维度由ReshapeParam proto控制。

可以直接使用数字进行指定。设定输入的某一维到输出blob中去。

此外,还有两个数字值得说一下:

0 直接从底层复制。例如,如果是底层是一个2 在它的第一维,那么顶层在它的第一维也有一个2。

-1 从其他的数据里面推测这一维应该是多少。

2.6.5 链接层 Concatenation 通道扩展链接

类型:Concat

例子:

layer {

name: "concat"

bottom: "in1"

bottom: "in2"

top: "out"

type: "Concat"

concat_param {

axis: 1

}

}

2.6.5.1 说明

可选参数:

·axis [default 1]:0代表链接num,1代表链接channels

通过全连接层后的大小变化:

输入:

从1到K的每一个blob的大小:ni×ci×h×w  ni个   ci为通道数量

输出:

如果axis = 0: (n1+n2+...+nK)×c1×h×w,需要保证所有输入的ci相同。

如果axis = 1: n1×(c1+c2+...+cK)×h×w,需要保证所有输入的n_i 相同。

通过Concatenation层,可以把多个的blobs链接成一个blob。

2.6.6 Slicing

类型:Slice

例子:

layer {

name: "slicer_label"

type: "Slice"

bottom: "label"

## Example of label with a shape N x 3 x 1 x 1

top: "label1"

top: "label2"

top: "label3"

slice_param {

axis: 1

slice_point: 1

slice_point: 2

}

}

Slice层可以将输入层变成多个输出层。

这些输出层沿一个给定的维度存在。

axis指定了目标的轴,slice_point则指定了选择维度的序号。

2.6.7 Elementwise Operations

2.6.8 Argmax

2.6.9 Softmax

2.6.10 Mean-Variance Normalization

3  以上为层初始化  还有 数据前向传播 以及 误差反向传播

每种类型的layer需要定义三种关键操作LayerSetUp, Forward, Backward:

LayerSetUp: 网络构建时初始化层和层的连接

Forward: 网络数据前向传递,给定bottom输入数据,计算输出到top

Backward: 网络误差反向传递,给定top的梯度,计算bottom的梯度并存储到bottom blob

Layer的设计主要就是SetUp、Forward、Backward函数(层一开始的时候的设置、然后就是前传和反传)

这其中的SetUp的实现又依赖于CheckBlobCounts、LayerSetUp、Reshape等的实现。

这其中Reshape又是必须要实现的,因为它是纯虚函数

这其中的Forward中又依赖于Forward_cpu、Forward_gpu,

这其中Forward_cpu又是必须要实现的。

这其中的Backward中又依赖于Backward_cpu、Backward_gpu,

这其中Backward_cpu 又是必须要实现的。

首先layer必须要实现一个forward function,前递函数当然功能可以自己定义啦,

在forward中呢他会从input也就是Layer的bottom,

对了caffe里面网络的前一层是叫bottom的,从bottom中获取blob,并且计算输出的Blob,

前向传播:

al+1 = 激活函数(W * al + bl+1)

a为神经元激活后的输出

当然他们也会实现一个反向传播backward function,

根据他们的input的blob以及output blob的 error gradient 梯度误差 计算得到该层的梯度误差。

反向传播:

gl = 激活函数导数(zl) * W 转置 * gl+1

g为 损失函数对z的偏导数

z = W*a + b

layer.hpp 源文件

#ifndef CAFFE_LAYER_H_

#define CAFFE_LAYER_H_

#include

#include

#include

#include "caffe/blob.hpp"  //数据

#include "caffe/common.hpp"  //通用

#include "caffe/layer_factory.hpp"

#include "caffe/proto/caffe.pb.h"

#include "caffe/util/device_alternate.hpp"

namespace caffe { //caffe命名空间

template // 通用 数据类型模板 Dtype

class Layer {

public:

/*

首先获得当前网络的Phase 模式,是 训练train还是 测试test,

在初始化 列表初始化LayerParameter,之后blobs_这里存放的是一个指向blob类的shared_ptr指针

的一个vector,在这里是申请空间,然后将传入的layer_param中的blob拷贝过来。

*/

// 显示的构造函数==========【1】==========================

// 显示的构造函数不需要重写,任何初始工作在SetUp()中完成

// 构造方法只复制层参数说明的值,如果层说明参数中提供了权值和偏置参数,也复制

explicit Layer(const LayerParameter& param)

: layer_param_(param) {

// Set phase and copy blobs (if there are any).

//1. 训练还是测试?phase

phase_ = param.phase();

if (layer_param_.blobs_size() > 0) {

//2. 将blobs_的大小设置为参数中的大小

blobs_.resize(layer_param_.blobs_size());

for (int i = 0; i < layer_param_.blobs_size(); ++i) {

// 3. 新建若干个Blob

blobs_[i].reset(new Blob());

// 4. 从blob文件中获取数据

blobs_[i]->FromProto(layer_param_.blobs(i));

}

}//用protobuf 传入的参数对blobs_ 做初始化,blobs_ 是一个vector 存放指向Blob类的智能指针。

#ifdef USE_MPI //

//If this is a gather layer, all it subsequent layer doesn't need gradient sync.

//We will only change itself's property here,

//subsequent layers will be inferred in the Net

if (is_gathering()){

set_need_sync(false);

}else{

set_need_sync(true);

}

#endif

}

virtual ~Layer() {}//虚析构函数 需要重写

//=====================================【2】==========================================

初始化函数SetUp,每个Layer对象都必须遵循固定的调用模式/

// 实现每个layer对象的setup函数

// 此方法非虚函数,不用重写,模式固定

void SetUp(const vector* >& bottom, // 输入层 输入数据  blob中的存储空间已申请

const vector* >& top) {  // 输出层 输出数据,blob对象以构造但是其中的存储空间未申请

// 具体空间大小需根据bottom blob大小和layer_param_共同决定,具体在Reshape函数现实

CheckBlobCounts(bottom, top); // 1. 检查输入输出blob个数是否满足要求,每个层能处理的输入输出数据不一样

LayerSetUp(bottom, top);      // 2. 调用LayerSetUp函数初始化特殊的层,每个Layer子类需重写这个函数完成定制的初始化

Reshape(bottom, top);         // 3. 调用Reshape函数为top blob分配合适大小的存储空间

SetLossWeights(top);          // 4. 为每个top blob设置损失权重乘子,非LossLayer 层的top blob其值为零

}

//=====================================【3】==========================================

/每个子类Layer必须重写的初始化函数LayerSetUp, 完成定制的初始化

//定制初始化,每个子类layer必须实现此虚函数

virtual void LayerSetUp(const vector*>& bottom, //输入blob, 数据成员data_和diff_存储了相关数据

const vector*>& top)// 输出blob, blob对象已构造但数据成员的空间尚未申请

{}  //此方法执行一次定制化的层初始化,包括从layer_param_读入并处理相关的层权值和偏置参数,

// 调用Reshape函数申请top blob的存储空间

//=====================================【4】==========================================

/每个子类Layer必须重写的Reshape函数,完成top blob形状的设置并为其分配存储空间,

// 根据bottom blob的形状和layer_param_计算top blob的形状并为其分配存储空间

virtual void Reshape(const vector*>& bottom,

const vector*>& top) = 0;

//=====================================【5】==========================================

//前向传播函数 Forward    非虚函数 实际回调用虚函数 forward_cpu或者forward_gpu,

// 首先是Forward.这其实是一个装饰器,

// 继承之后在调用的调用其相应的forward_cpu或者forward_gpu,

// 根据输入的input data blob计算相应的output data blob,

// 同时会反应这一层layer的total loss.

inline Dtype Forward(const vector*>& bottom, //输入blob

const vector*>& top); // 输出blob

//=====================================【5】==========================================

// 反向传播函数Backward

//给定top blob 的 error gradient 误差梯度 计算得到bottom 的 误差梯度 error gradient。

// 在Ouput blobs里面的diff存储的就是其相应的error gradients。

// 其中propagate_down这个参数跟Bottom的长度是一样的

// 其每一个Index用来指定是否需要反向传播error gradients 到对应的bottom blob。

// 而bottom 这里面的diff 区域存放的就是BackWard计算出来相应的gradient error.

inline void Backward( const vector* >& top, // 输入是 output blobs,top blobs

const vector& propagate_down,                 // 更新标志

const vector* >& bottom);               // 输出是 bottom blobs

//=====================================【6】==========================================

// 返回数据===========================

vector > >& blobs() {

return blobs_;//返回vector blobs_

}

//返回layer parameter ===============

const LayerParameter& layer_param() const { return layer_param_; }

//将layer plarameter 写入模型配置文件 protobuf =============

virtual void ToProto(LayerParameter* param, bool write_diff = false);

//返回 一个blob top 在给定 index 的 loss ============

inline Dtype loss(const int top_index) const {

return (loss_.size() > top_index) ? loss_[top_index] : Dtype(0);

}

// 设置一个blob top 在给定 index 的 loss=============

inline void set_loss(const int top_index, const Dtype value) {

if (loss_.size() <= top_index) {

loss_.resize(top_index + 1, Dtype(0));

}

loss_[top_index] = value;

}

// 虚函数,而且还是内联的,返回层类型 =================

virtual inline const char* type() const { return ""; }

// 虚函数,获得bottom blob的精确个数 ==================

virtual inline int ExactNumBottomBlobs() const { return -1; }

// 虚函数,获得bottom blob的最小个数 ================

virtual inline int MinBottomBlobs() const { return -1; }

// 虚函数,获得bottom blob的最大个数 ============

virtual inline int MaxBottomBlobs() const { return -1; }

// 虚函数,获得top blob的精确个数 ===================

virtual inline int ExactNumTopBlobs() const { return -1; }

// 虚函数,获得top blob的最小个数 ================

virtual inline int MinTopBlobs() const { return -1; }

// 虚函数,获得top blob的最大个数 ====================

virtual inline int MaxTopBlobs() const { return -1; }

// 虚函数,bottom blob和top blob的个数是否一致 ========================

virtual inline bool EqualNumBottomTopBlobs() const { return false; }

// 返回当前层是否自动创建匿名top blobs =====================

// 如果返回true,表明网络初始化的时候创建了了足够多的匿名top blobs

// 来满足ExactNumTopBlobs或者MinTopBlobs所要求的top blobs的个数

virtual inline bool AutoTopBlobs() const { return false; }

// AllowforceBackward用来设置是否强制梯度返回================

// 因为有些层其实不需要梯度信息 ,后面两个函数分别查看以及设置是是否需要计算梯度。

// 对于一个给定的bottom blob,返回是否允许强制反传

virtual inline bool AllowForceBackward(const int bottom_index) const {

return true;

}

// 设置 哪些 bottom 需要反向传播========

inline bool param_propagate_down(const int param_id) {

return (param_propagate_down_.size() > param_id) ?

param_propagate_down_[param_id] : false;

}

inline void set_param_propagate_down(const int param_id, const bool value) {

if (param_propagate_down_.size() <= param_id) {

param_propagate_down_.resize(param_id + 1, true);

}

param_propagate_down_[param_id] = value;

}

#ifdef USE_MPI

// 多线程  并行 配置

inline virtual bool is_gathering() {return false;}

inline virtual bool is_scattering() {return false;}

inline bool need_sync(){return need_sync_;}

inline void set_need_sync(bool val){need_sync_ = val;}

#endif

protected:

//The protobuf that stores the layer parameters

// 层说明参数,从protocal buffers格式的网络结构说明文件中读取

LayerParameter layer_param_;

// The phase: TRAIN or TEST

// 层状态,参与网络的训练还是测试

Phase phase_;

// The vector that stores the learnable parameters as a set of blobs.

// 层权值和偏置参数,使用向量是因为权值参数和偏置是分开保存在两个blob中的

vector > > blobs_;

// Vector indicating whether to compute the diff of each param blob

// 标志每个top blob是否需要计算反向传递的梯度值

// 参数是否需要更新

vector param_propagate_down_;

// 非LossLayer为零,LossLayer中表示每个top blob计算的loss的权重

vector loss_;

#ifdef USE_MPI

bool need_sync_; // 并行

#endif

//=====================================【7】==========================================

///前向传播函数 Forward 这个非虚函数,它们内部会调用如下虚函数完成数据前向传

// 前向传播 CPU版本

virtual void Forward_cpu(const vector*>& bottom,

const vector*>& top) = 0;

// 前向传播 GPU版本

virtual void Forward_gpu(const vector*>& bottom,

const vector*>& top) {

// LOG(WARNING) << "Using CPU code as backup.";

return Forward_cpu(bottom, top);

}

//=====================================【8】==========================================

//反向传播函数Backward 这个非虚函数,它们内部会调用如下虚函数完成 误差反向传播

// 误差反向传播,根据执行环境的不同每个子类Layer必须重写CPU和GPU版本

// CPU版本

virtual void Backward_cpu(const vector*>& top, // 输入误差 top Blob

const vector& propagate_down,                     // 更新标志

const vector*>& bottom) = 0;               // 输出 bottom Blob

// GPU版本

virtual void Backward_gpu(const vector*>& top,

const vector& propagate_down,

const vector*>& bottom) {

// LOG(WARNING) << "Using CPU code as backup.";

Backward_cpu(top, propagate_down, bottom);

}

//=====================================【9】==========================================

// 1. 检查输入输出blob个数是否满足要求,每个层能处理的输入输出数据不一样

virtual void CheckBlobCounts(const vector*>& bottom, // 输入层

const vector*>& top) {  // 输出层

if (ExactNumBottomBlobs() >= 0) {

CHECK_EQ(ExactNumBottomBlobs(), bottom.size())

<< type() << " Layer takes " << ExactNumBottomBlobs()

<< " bottom blob(s) as input.";

}// 保证输入bottom 数量和要求的相同

if (MinBottomBlobs() >= 0) {

CHECK_LE(MinBottomBlobs(), bottom.size())

<< type() << " Layer takes at least " << MinBottomBlobs()

<< " bottom blob(s) as input.";

}//保证输入的bottom数量大于或等于 要求的最小数量

if (MaxBottomBlobs() >= 0) {

CHECK_GE(MaxBottomBlobs(), bottom.size())

<< type() << " Layer takes at most " << MaxBottomBlobs()

<< " bottom blob(s) as input.";

}//保证输入的bottom数量小于或等于 要求的最大数量

if (ExactNumTopBlobs() >= 0) {

CHECK_EQ(ExactNumTopBlobs(), top.size())

<< type() << " Layer produces " << ExactNumTopBlobs()

<< " top blob(s) as output.";

}// 保证输入top数量和要求的相同

if (MinTopBlobs() >= 0) {

CHECK_LE(MinTopBlobs(), top.size())

<< type() << " Layer produces at least " << MinTopBlobs()

<< " top blob(s) as output.";

}//保证输入的top数量大于或等于 要求的最小数量

if (MaxTopBlobs() >= 0) {

CHECK_GE(MaxTopBlobs(), top.size())

<< type() << " Layer produces at most " << MaxTopBlobs()

<< " top blob(s) as output.";

}//保证输入的top数量小于或等于 要求的最大数量

if (EqualNumBottomTopBlobs()) {

CHECK_EQ(bottom.size(), top.size())

<< type() << " Layer produces one top blob as output for each "

<< "bottom blob input.";

}//保证输入的bottom数量 和 输出的top数量相同

}

//=====================================【10】==========================================

// 4. 为每个top blob设置损失权重乘子,非LossLayer 层的top blob其值为零

// SetLoss是非常重要的一个步骤,是被SetUp调用来初始化top bottom的weights,

// 并且存储非零的loss weights 在diff blob里面

inline void SetLossWeights(const vector*>& top) {

const int num_loss_weights = layer_param_.loss_weight_size();

if (num_loss_weights) {

CHECK_EQ(top.size(), num_loss_weights) << "loss_weight must be "

"unspecified or specified once per top blob.";

for (int top_id = 0; top_id < top.size(); ++top_id) {

const Dtype loss_weight = layer_param_.loss_weight(top_id);

if (loss_weight == Dtype(0)) { continue; }//如果为0不对loss进行操作

this->set_loss(top_id, loss_weight);

const int count = top[top_id]->count();

Dtype* loss_multiplier = top[top_id]->mutable_cpu_diff();

caffe_set(count, loss_weight, loss_multiplier);//将loss_multiplier设为loss_weight

}

}

}

DISABLE_COPY_AND_ASSIGN(Layer);

}; // class Layer

//=====================================【11】==========================================

// 网络前向传播 调用接口 逻辑函数========

// 传播调用对应的Forward_cpu或者Forward_gpu

// 而我们知道Forward_cpu是纯虚函数,必须要实例化,

// 而Forward_gpu是虚函数,如果不实现,就会调用 Forward_cpu函数了。

// 所以前传(你必须实现自己的Forward_cpu,实现Forward_gpu是可选的)

template

inline Dtype Layer::Forward(const vector*>& bottom,// 输入 层

const vector*>& top) { // 输出 层

Dtype loss = 0;

// 根据bottom设置top的形状

Reshape(bottom, top); // 申请 内存

// 设置运行模式CPU or GPU

switch (Caffe::mode()) {

case Caffe::CPU:

// 调用CPU的前传

Forward_cpu(bottom, top);

// 前传计算完之后计算损失(只有最后一层才进行计算,其余层都不用)

for (int top_id = 0; top_id < top.size(); ++top_id) {

if (!this->loss(top_id)) { continue; }

const int count = top[top_id]->count();

// 获取前传的数据

const Dtype* data = top[top_id]->cpu_data();

// 获取梯度(\frac{\partial Loss}{\partial net})

const Dtype* loss_weights = top[top_id]->cpu_diff();

// data与loss_weight的点积,即得损失函数关于当前层权重的偏导了

// \frac{\partial Loss}{\partial net} * \frac{\partial net}{\frac{W}}

// = \frac{\partial Loss}{\partial W}

loss += caffe_cpu_dot(count, data, loss_weights);// cpu 计算点积

}

break;

case Caffe::GPU:

// GPU前传

Forward_gpu(bottom, top);

#ifndef CPU_ONLY

// 同上,只不过这里用GPU来计算点积了

for (int top_id = 0; top_id < top.size(); ++top_id) {

if (!this->loss(top_id)) { continue; }

const int count = top[top_id]->count();

// 获取GPU上的数据

const Dtype* data = top[top_id]->gpu_data();

const Dtype* loss_weights = top[top_id]->gpu_diff();

Dtype blob_loss = 0;

caffe_gpu_dot(count, data, loss_weights, &blob_loss); // gpu 计算点积

loss += blob_loss;

}

#endif

break;

default:

LOG(FATAL) << "Unknown caffe mode.";

}

return loss;

}

//=====================================【12】==========================================

// 网络 反向传播 调用接口 逻辑函数========

template

inline void Layer::Backward(const vector*>& top, // 输入 误差 从后向前传播

const vector& propagate_down, // 更新标志

const vector*>& bottom) { // 误差输出

switch (Caffe::mode()) {

case Caffe::CPU:

Backward_cpu(top, propagate_down, bottom);

//根据blob top 的error 梯度(diff)计算bottom 的 error 梯度。 propagate_down 是长度

//和bottom 相同的vector ,用于控制是否需要对对应的bottom 元素传播梯度。具体layer具体定义。

break;

case Caffe::GPU:

Backward_gpu(top, propagate_down, bottom);

break;

default:

LOG(FATAL) << "Unknown caffe mode.";

}

}

//=====================================【12】==========================================

//Layer的序列化函数,将layer的层说明参数layer_param_,层权值和偏置

//参数blobs_复制到LayerParameter对象,便于写到磁盘,

// Serialize LayerParameter to protocol buffer

template

void Layer::ToProto(LayerParameter* param, bool write_diff) {

param->Clear();

param->CopyFrom(layer_param_); // 复制层说明参数layer_param_

param->clear_blobs();

// 复制层权值和偏置参数blobs_

for (int i = 0; i < blobs_.size(); ++i) {

blobs_[i]->ToProto(param->add_blobs(), write_diff);

}

}

} // namespace caffe

#endif // CAFFE_LAYER_H_

python装饰器带参数函数二阶导数公式_MVision/caffe_简介_使用.md at master · Ewenwan/MVision · GitHub...相关推荐

  1. python装饰器带参数函数二阶导数公式_一文搞定Python装饰器,看完面试不再慌

    本文始发于个人公众号:TechFlow,原创不易,求个关注 今天是Python专题的第12篇文章,我们来看看Python装饰器. 一段囧事 差不多五年前面试的时候,我就领教过它的重要性.那时候我Pyt ...

  2. python装饰器带参数函数二阶导数公式_SICP Python 描述 1.6 高阶函数

    1.6 高阶函数 我们已经看到,函数实际上是描述复合操作的抽象,这些操作不依赖于它们的参数值.在square中, >>> def square(x): return x * x 我们 ...

  3. python装饰器带参数函数二阶导数公式_【计算机程序的构造和解释】使用函数构建抽象——5. 高阶函数...

    学Python,用RPA 艺赛旗RPA2020.1版本 正在免费下载使用中,欢迎下载使用艺赛旗-RPA机器人免费下载|提供流程自动化解决方案​www.i-search.com.cn 我们已经看到,函数 ...

  4. python装饰器带参数函数二阶导数公式_机器学习【二】单变量线性回归

    吴恩达机器学习笔记整理--单变量线性回归 通过模型分析,拟合什么类型的曲线. 一.基本概念 1.训练集 由训练样例(training example)组成的集合就是训练集(training set), ...

  5. python装饰器带参数函数_python带参数装饰器的两种写法

    python带参数装饰器的两种写法 前言 最近在实现一个装饰器的过程中发现了一个很有意思的地方,在博客里面分享出来 不同的写法 三层函数嵌套,实现了可传参数的一个装饰器. import logging ...

  6. python装饰器带参数函数_当我使用带参数的python装饰器时,如何将参数传递给最内部的函数?...

    当Func返回不是真时,我的装饰器用于召回Func.def deco_retry(retry_times): def _deco_retry(func): def wrapper(*args, **k ...

  7. python装饰器传递参数_Python装饰器高级版—Python类内定义装饰器并传递self参数...

    本文重点:解决了类里面定义的装饰器,在同一个类里面使用的问题,并实现了装饰器的类属性参数传递 目录: 一.基本装饰器 二.在类里定义装饰器,装饰本类内函数 三.类装饰器 正文: 一.基本装饰器 装饰不 ...

  8. python装饰器函数-python装饰器1:函数装饰器详解

    先混个眼熟 谁可以作为装饰器(可以将谁编写成装饰器): 函数 方法 实现了__call__的可调用类 装饰器可以去装饰谁(谁可以被装饰): 函数 方法 类 基础:函数装饰器的表现方式 假如你已经定义了 ...

  9. python高阶函数闭包装饰器_5.初识python装饰器 高阶函数+闭包+函数嵌套=装饰器...

    一.什么是装饰器? 实际上装饰器就是个函数,这个函数可以为其他函数提供附加的功能. 装饰器在给其他函数添加功能时,不会修改原函数的源代码,不会修改原函数的调用方式. 高阶函数+函数嵌套+闭包 = 装饰 ...

最新文章

  1. 《数值分析(原书第2版)》—— 第0章 基 础 知 识
  2. 输入n个字符串字典序排序_FSST - 快速字符串压缩算法
  3. motan yar php,motan学习笔记 六 opentracing Brave+zipkin实现-Go语言中文社区
  4. 搞定JSP第一个Servlet例子并且还是手动编译
  5. 用户NT AUTHORITYNETWORK SERVICE登录失败解决方法
  6. 【laravel54】查看版本号3种方式
  7. 后端把Long类型的数据传给前端,前端可能会出现精度丢失的情况,以及解决方案...
  8. python global用法_【干货】每天更新两个Python 小例子(十九)
  9. [Coci2015]Divljak
  10. 中国基座污水泵市场趋势报告、技术动态创新及市场预测
  11. caffe利用shell创建train.txt和val.txt做数据输入
  12. Fultter之Element和Widget对应关系解析
  13. 【c语言大作业】c语言编写贪吃蛇
  14. 五色电阻在线计算机,五色环电阻阻值在线计算一键生成计算器
  15. java hdporn,docs/java/concurrent/SynBottom.md · wt1814/wt-note - Gitee.com
  16. 虚拟机连接摄像头程序报错
  17. 微信小程序断网异常处理
  18. 简单学习一下ibd数据文件解析
  19. 武汉坚守第六十一天——运动健康相关联,牛尿神奇为哪般
  20. c语言字符画小狗,C语言字符画,字符闪画

热门文章

  1. mfc中字体设置详解
  2. OpenCV10.自定义线性滤波
  3. 在Docker守护进程停机期间保持容器运行(即重启Docker时,正在运行的容器不会停止)
  4. 手机怎么新建PDF文件?
  5. 年度回忆录-2014(下半年)在路上
  6. [教程] 教你如何实现荣耀3C支持OTG功能,解决荣耀3C的唯一缺点
  7. c语言程序中的算数表达式X Y-Z,C语言程序设计实验教程习题1~10.docx
  8. 老家菜seo关键词(大蜀山附近特色饭店)优化首页方法
  9. 做一名活动策划是什么体验
  10. 调用webservice报错。