manout's blog

Something about me

关于 BatchNorm 层的原理可以详见此论文

BatchNorm 解决的问题

深度卷积神经网络的训练主要是每一层神经元学习本层的输入数据的分布。但是输入数据的分布会随着上一层的计算发生变化,称为 internal covatiate shift. 给本层的学习带来了困难,在 BatchNorm 之前是通过微调学习率和初始化参数实现,学习速度很低。
引入 BatchNorm 通过对数据规范化处理避免这个问题。

原理

输入

小批量数据:
需要学习的参数:

输出

算法

$$
\mu_B \leftarrow \frac 1 m \sum_{i=1}^{m}x_i \quad \text{mini-batch mean} \
\sigma_{B}^{2} \leftarrow \frac 1 m \sum_{i=1}^{m}(x_i - \mu_B)^2 \quad \text{mini-batch variance} \
\hat{x}i \leftarrow \frac {x_i - \mu_B} {\sqrt{\sigma_B^2 + \epsilon}} \quad \text{normalize} \
y_i \leftarrow \gamma \hat{x}
{i}+\beta\equiv {BN_{\gamma,\beta}(x_i)} \quad \text{scale and shift}
$$

  • 求出此批量数据的均值
  • 求出此批量的方差
  • 归一化处理
  • 引入缩放和平移变量

BatchNorm 降低了数据的绝对差异,突出了相对差异,在分类任务上效果明显。

使用 python 的包管理工具安装 mmdnn

1
pip install mmdnn

会得到以下命令行工具

  • mmdownload
    用于下载指定框架已训练好的网络模型
  • mmconvert
    mmtoir, mmtocode, mmtomodel 三者的集成,一行命令完成从一个框架模型到另一个框架模型的转换,分为三步常用于调试
  • mmtoir
    用于将模型转换为 mmdnn 中间表达形式。使用这个命令会得到三个文件
    • json 用于在线可视化
    • proto|pb 用于描述网络结构模型
    • npy 用于保存网络数值参数
  • mmtocode
    用于将 mmtoirpb, npy 文件转换为在特定框架下构造该网络的原始代码,以及构建网络过程中用于设置权重的参数。
  • mmtomodel
    用于将 mmtocode 的输出 py 文件转换为可以直接被指定框架读取的模型文件
  • mmvismeta
    目前只发现可以使用 tensorboard 作为后端将计算图可视化

目前遇到的几个问题

  • MMdnn 中间表示中算子可以有 38 种,但是新框架内部却有 54 种算子,经过统计 mmdnn 中只有 23 种可以与新框架中的 25 种算子可以对应(存在一对多,多对多关系),余下仅凭描述无法找到映射关系,何况新框架内部还有自定义算子。
    经过测试(从一个模型转到 IR 层再转回去)模型文件的大小并没有发生变化(在 mb 级别),说明 mmdnn 在转换中并没有丢失信息。这应该是表示 38 个算子应该是可以描述完整新框架的 54 个算子的。
    猜测 mmdnn 是用细粒度的算子的组合表达其他框架的特殊算子,观察到 IR 层的可视化表示比原模型的层数多。
  • MMdnn 中间表示形式中的权值文件使用 npy 格式存储,目前没有找到 C++ 相关的官方 API.
    没办法只好用 Python 重写

忽然发现这是第 100 篇,mark 一下

2018-01-25 2018-08-01

MMdnn能够实现在不同的框架之间互相转换网络模型。
其原理是将所有的模型先转化为中间表达形式(Intermediate Representation), 分析下 IR 层的表示方式。

proto 说明文件

IR 层的 proto 说明文件可见 github

其中包含以下几个部分

  • GraphDef
    • NodeDef
    • version
  • NodeDef
    • name
    • op
    • input
    • attr
  • Attrvalue
    • list
    • type
    • shape
    • tensor
  • Tensorshape
    • dim
  • LiteralTensor
    • type
    • tensor_shape
    • values

Graph

描述网络模型的 graph, 内部包含若干 node, 这里的结点即对应网络模型的层,node 中的 input 即描述每一层之间的输入输出连接关系.

NodeDef

  • name
    本层 name, 在 Graph 中唯一
  • op
    本层算子,算子描述见链接,在算子描述文件中,算子 name 唯一.
  • input
    用于描述本层输入关系,各层之间的输入输出关系靠此成员描述
  • attr
    attr 成员可以是 listvalue, type, shape, tensor
    • list
      存储 list 成员,如 list(int), list(float), list(shape) 等
      • data
        保存数值数据,类型可以是 bytes, int64, float, bool
      • type
      • shape
      • tensor
    • type
      描述数值类型
    • shape
      描述 tensor 形状
    • tensor
      node 之间传递的 tensor

TensorShape

描述 tensor 的维度信息

LiteralTensor

存储 tensor, 在各 node 之间传递

MacOS 下 brew 更新后总是安装各种包总是最新版的, brew install python 总是安装 python3.7, 大部分包还没有做适配,安装 tensorflow 报错,安装 pytorch 报错,无奈只能安装回 python.3.6

  • 找到自己想要的 formula 版本对应的 rb 文件
    python 为例, 在
    https://github.com/Homebrew/homebrew-core/commits/master/Formula/python.rb
    中 commit 历史中找到自己想要的历史版本,可以根据 comment 找其对应的版本信息
  • 选择对应的 commit, 点击 view 后以 raw 模式打开
  • 将打开的链接作为新的安装包安装,需要卸载新版本,或者将其全部内容替换 /usr/local/Homebrew/Library/Taps/homebrew/homebrew-core/Formula/ 下的同名文件,然后再安装这个 formula
1
2
3
4
5
brew uninstall python
# 直接使用链接
brew instsall https://raw.githubusercontent.com/Homebrew/homebrew-core/e128fa1bce3377de32cbf11bd8e46f7334dfd7a6/Formula/python.rb
# 覆盖 python.rb文件,再重新安装,不要更新 formula
brew install python

之后所有依赖 python3.7formula 都会崩溃,比如 macvim, 依照相同方法找到旧版本 macvim.rb 文件重新安装,直接安装的话会更新 python.

没错,brew 放弃新版本安装旧版本就是这么麻烦。

接下来几周的任务是实现 MMdnn 中 IR 层到新框架网络模型的后端转换,mentor 先给了一个合并 prototxt 和 bin 的合并工具源码做参考(感觉不是一个层次上的东西啊),分析下这个工具的实现

网络描述文件

prototxt 网络结构描述文件

prototxt 文件形式为纯文本格式,可以直接用常用的文本编辑器打开(vscode, sublime text 等),caffe 用该类型文件保存并描述网络结构信息,所有内容明文保存。其存储格式并不适合一般人阅读,以下是 prototxt 文件描述 input 层和第一层 conv 层的格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
layer {
name: "data"
type: "Input"
top: "data"
input_param { shape: { dim: 1 dim: 3 dim: 227 dim: 227 } }
}
layer {
name: "conv1"
type: "Convolution"
bottom: "data"
top: "conv1"
convolution_param {
num_output: 64
kernel_size: 3
stride: 2
}
}

可以通过这个网站看到图形化的网络结构。

bin 网络参数描述文件

bin 文件为二进制文件,无法直接阅读,caffe 使用该类型文件描述网络参数,如卷积层的卷积核的 weight,bias 等数值信息

源码分析

经分析,合并大致流程如下

  • 读入 prototxt 文件
  • 读入 bin 文件
    • 以上类型为 caffe 中的 caffe::NetParameter 类型,存储其网络信息,包含各层结构和数据
  • 如果支持输入域
    • 根据 protobuf 中描述的输入信息构造输入层 NCHW 信息或者多维结构信息。
  • protobuf 中描述的每一层 layer 做如下操作
    • 根据 layer.name 找到对应 bin 文件中对应的 layer 参数信息
      • 若不存在,继续下一层 layer
    • 对找到的 layer 参数信息中的每一个 blob 做如下操作
      • 获取其 NCHW 或者多维结构信息,并记录
      • 将其维度信息和数据合并到单个 blob 中并添加到 layer
  • 将合并后的 NetParameter 序列化并存入到 output_model

protobuf 是 Google 推出的语言独立,平台独立的针对序列化数据的组件,可以类比 xml, 但是比 xml 精简。
通过使用 protobuf 语法自定义想要的数据结构,接着用 protoc 命令就可以产生针对这种数据格式文件读写的各种语言(C/C++, Python, Java等)的 API.

Mac 上安装 protobuf

1
brew install protobuf

proto 语法

自定义序列化数据结构使用特殊的语法,创建 proto 文件,其包含如下内容

  • syntax= proto2 | proto3
  • package
  • message
    • field
      • [required | optional | repeated]
      • type
      • name
      • [message]
    • tag
1
2
3
4
5
6
syntax = "proto2" | "proto3“
package "name";
message "type"
{
[required | optional | repeated] type "filed" = tags;
}

message

一个 message 类型表示了一个可以序列化的结构。由若干个 field 组成

field

一个 field 由以下部分构成

  • type
    proto 支持的类型可见官网, 可以是用户自定义的 message
  • field number
    每个 field 最后包含一个独特数字,这个数字在二进制格式中标记这个 field,当整个 message 投入使用后不应该再修改,这些 Number 可取的范围是 , 但是 这个区间属于保留区,不应使用,否则会提示 FieldDescriptor::FieldDescriptor 错误。
  • field rule
    • singular
      在一个组织良好的 message 中,这 field 可以出现一次或零次
    • repeated
      在一个组织良好的 message 中,这个 field 可以出现任意次数,值出现的顺序保留。

保留域

可以注明一个 fieldreserved,可以标注其 field number 或者 name, 示例如下

1
2
3
4
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}

reserved 标注意为未来可能会抛弃这个 field, 提示用户尽可能少使用这个 field, 每当 proto buffer compiler 编译这个 proto 文件生成 api 时都会给出相应的警告。

Enumeration

proto 中定义枚举类型的语法与 C++ 基本相同

1
2
3
4
5
6
7
8
9
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}

注意,一定要有 0 值

Any

Any 类型可以保存任意类型的信息,直接将任何序列化的 message 看做看做字节串存储

oneof

类似 C++ 中的 union ,存储时只存储 oneof 域中的一个。

1
2
3
4
5
6
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}

当取其中一个域时,其他域自动会被删除,当使用 C++ 时要注意内存崩溃问题.

Maps

定义 Map 的语法如下

1
map<key_type, value_type> map_field = N;

删除 ~/Library/Developer/Xcode/DerivedData 中的所有内容

1
rm -rf ~/Library/Developer/Xcode/DerivedData/*

重启 Xcode

写了两天的 bug, 心累

在 Mac OSx 上从包含 CmakeLists.txt 的文件夹生成 Xcode 工程

1
cmake -G Xcode -H . -B build

这行命令从当前目录读取 CMakeLists.txt 文件并创建 build 目录并在其下生成 Xcode 工程。

这次的代码在一行报了 EXC_BAD_ACCESS 异常,后来发现是这次的对象没有创建成功,虚函数调用直接失败,这里简单记下这个异常的原因及常见解决方法

原因

当调试器报出 EXC_BAD_ACCESS 异常时,通常意味着这里向一个已经被释放的对象发送消息。
从更底层的角度上来说,在 C, Object-C 和 C++ 中,通常是由于指针的错误使用造成的,每当使用一个指针给其指向的对象发送消息时,这个指针都要解引用,实际访问其指向的内存空间,当这块内存空间并不在这个程序映射的内存中时,再次访问这块内存时系统内核就会发送一个异常(EXC), 表示程序不能访问这块内存(BAD ACCESS)

总而言之,当遇到 EXC_BAD_ACCESS 时,通常表示给一块不能执行该消息的内存发送了消息

Debug

程序并不会因为错误指针的传递或者赋值而崩溃,只会因为对其解引用而崩溃,被释放的对象会成为 zombie 对象。
在 Xcode 中开启 enable zombie Objects 可以在调试时使 zombie 保留在程序中,调试器可以观察到向一个 zombie 对象发送消息时的详细信息。
在一般的 Unix 平台上可以使用 Valgrind 内存检测工具检查内存的违法操作行为。

0%