Skip to content
体验新版
项目
组织
正在加载...
登录
切换导航
打开侧边栏
PaddlePaddle
PaddleDetection
提交
226bf1dd
P
PaddleDetection
项目概览
PaddlePaddle
/
PaddleDetection
1 年多 前同步成功
通知
696
Star
11112
Fork
2696
代码
文件
提交
分支
Tags
贡献者
分支图
Diff
Issue
184
列表
看板
标记
里程碑
合并请求
40
Wiki
0
Wiki
分析
仓库
DevOps
项目成员
Pages
P
PaddleDetection
项目概览
项目概览
详情
发布
仓库
仓库
文件
提交
分支
标签
贡献者
分支图
比较
Issue
184
Issue
184
列表
看板
标记
里程碑
合并请求
40
合并请求
40
Pages
分析
分析
仓库分析
DevOps
Wiki
0
Wiki
成员
成员
收起侧边栏
关闭侧边栏
动态
分支图
创建新Issue
提交
Issue看板
提交
226bf1dd
编写于
7月 28, 2017
作者:
S
Superjom
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
update more details
上级
d3213e4c
变更
1
隐藏空白更改
内联
并排
Showing
1 changed file
with
129 addition
and
144 deletion
+129
-144
paddle/operators/rnn_design.md
paddle/operators/rnn_design.md
+129
-144
未找到文件。
paddle/operators/rnn_design.md
浏览文件 @
226bf1dd
...
...
@@ -4,7 +4,7 @@
现有Paddle包括
`RecurrentLayerGroup`
在内的RNN均实现了无padding的变长序列支持,本文也将基于该模块的思路,设计重构后的变长序列支持。
##
非padding 变长序列的意义
##
背景介绍
由于tensor必须有明确的shape,因此基于tensor 的主流框架在存储变长序列时,
必须用zero-padding的方式将变长序列补全为固定shape的tensor。
...
...
@@ -18,123 +18,146 @@
但对变长序列的支持,需要对目前框架做一些修改,下面讨论如何在最小修改下支持变长序列。
##
变长数据格式
##
多层序列数据格式 `LODTensor`
目前 Paddle 会将一个mini-batch内的数据存储在一维的内存上,
额外使用
`Argument.sequenceStartPositions`
来存储每个句子的信息。
基于当前重构现状,我们使用如下设计来存储变长数据格式
Paddle里使用
`Argument.subSequenceStartPositions`
来存储2层的序列信息,更高维度的序列则无法直接支持;
-
扩充 Tensor 以支持存储变长序列的信息(这部分信息后续用SeqPosVar表示)
-
Op 的
`InferShape`
会更新outputs 的
`SeqPosVar`
-
为了兼容序列Op(比如RNN)和传统Op(比如FC),序列的所有元素均flatten追加存储到一个mini-batch中
-
比如,长度分别为2,3,4的三个句子会存储为一个size为9的
`mini-batch`
-
额外会有一个
`SeqPosVar`
,存储句子的结构,比如offest:
`0,2,5,9`
为了支持sub-sequence,Paddle里使用
`Argument.subSequenceStartPositions`
来存储2维的序列信息,更高维度的序列无法支持;
这里为了扩展性,将SeqPosVar定义成如下数据结构来支持N维的序列信息的存储
为了支持
`N-level`
序列的存储,本文将序列信息定义成如下数据结构:
```
c++
std
::
vector
<
std
::
vector
<
std
::
vector
<
int
>>
seq_start_position
s_
;
std
::
shared_ptr
<
std
::
vector
<
std
::
vector
<
int
>>>
lod_start_po
s_
;
```
附录中演示如何用二维的vector来存储多个 level 的变长序列的start position.
或者更明确的定义
Tensor 扩展为
```
c++
/*
* Tensor storing sequences.
*/
class
TensorWithSequence
{
typedef
std
::
vector
<
int
>
level_t
;
std
::
vector
<
level_t
>
lod_start_pos
;
```
这里的每一个
`level_t`
存储一个粒度(level)的偏移信息,和paddle目前做法一致。
为了更透明地传递序列信息,我们引入了一种新的tensor 称为
`LODTensor`
[4],
其关于tensor相关的接口都直接继承自
`Tensor`
,但另外添加了序列相关接口。
如此,在操作一个
`LODTensor`
时,普通
`Op`
直接当成
`Tensor`
使用,
而操作序列的
`Op`
会额外操作
`LODTensor`
的变长序列操作的相关接口。
`LODTensor`
具体定义如下:
```
c++
class
LODTensor
:
public
Tensor
{
public:
Tenser
*
tensor
()
{
return
tensor_
;
}
/*
* get an element of current level.
*/
TensorWithSequence
Element
(
int
element
)
const
;
/*
* get an element of n-th level.
* NOTE low performance.
*/
TensorWithSequence
Element
(
int
level
,
int
element
)
const
;
/*
* get number of elements in n-th level.
*/
size_t
Elements
(
int
level
=
0
)
const
;
/*
* get the number of levels of sequences.
*/
size_t
Levels
()
const
;
/*
* copy other's pointers to share their data.
*/
void
ShareDataFrom
(
const
TensorWithSequence
&
other
);
/*
* just copy other's sequence info (use shared_ptr to share memory).
*/
void
ShareSeqPosFrom
(
const
TensorWithSequence
&
other
);
/*
* copy others' sequence info for mutation.
*/
void
CopySeqPosFrom
(
const
TensorWithSequence
&
other
);
size_t
Levels
()
const
{
return
seq_start_positions_
.
size
();
}
size_t
Elements
(
int
level
=
0
)
const
{
return
seq_start_positions_
[
level
].
size
();
}
// slice of level[elem_begin: elem_end]
// NOTE low performance in slice seq_start_positions_.
// TODO should call Tensor's Slice.
LODTensor
LODSlice
(
int
level
,
int
elem_begin
,
int
elem_end
)
const
;
// slice with tensor's data shared with this.
LODTensor
LODSliceShared
(
int
level
,
int
elem_begin
,
int
elem_end
)
const
;
// copy other's lod_start_pos_, to share LOD info.
// NOTE the LOD info sould not be changed.
void
ShareConstLODFrom
(
const
LODTensor
&
other
)
{
lod_start_pos_
=
other
.
lod_start_pos_
;
}
// copy other's lod_start_pos_'s content, free to mutate.
void
ShareMutableLODFrom
(
const
LODTensor
&
other
)
{
lod_start_pos_
=
std
::
make_shared
<
std
::
vector
<
std
::
vector
<
int
>>
(
other
.
lod_start_pos_
.
begin
(),
other
.
lod_start_pos_
.
end
());
}
private:
Tensor
*
tensor_
;
/*
* store start positions of all levels.
*
* data format like
*
* 0-th level start positions
* 1-th level, element 0, start positions
* 1-th level, element 1, start positions
* ...
* 1-th level, element k, start positions
* 2-th level, element 0, start positions
* 2-th level, element 1, start positions
* ...
* 2-th level, element n, start positions
* ...
*
*/
std
::
vector
<
std
::
vector
<
std
::
vector
<
int
>>
seq_start_positions_
;
std
::
shared_ptr
<
std
::
vector
<
std
::
vector
<
int
>>>
lod_start_pos_
;
};
```
## 框架支持方法
类似Paddle现在的做法,为了支持每个参与inputs/outputs的variable必须有对应的SeqPosVar,
**这里需要框架就行一些修改,有一些trick的成分**
。
其中,
`lod_start_pos_`
使用了
`shared_ptr`
来减少存储和复制的代价,
可以认为
`LODTensor`
是
`Tensor`
的扩展,几乎完全兼容原始
`Tensor`
的使用。
现有框架可以在
`Context`
里添加一个与
`Input`
平行的接口
`InputSeq`
来获取序列信息,具体定义如下
## 框架支持
### 框架现有的 `Tensor` 调用替换为 `LODTensor`
为了实现
`LODTensor`
的传递,框架里很多
`Tensor`
都需要变成
`LODTensor`
,
简单实现,直接
**把之前所有的`Tensor` 全部替换成 `LODTensor`,这里可以直接修改 `pybind.cc` 里面创建`Tensor`的接口**
。
```
std::shared_ptr<SeqPos> InputSeq(const std::string& name);
```
此外,用户有可能需要感知序列的存在(比如序列的可视化需要解析模型中输出的序列),因此一些序列操作的API也需要暴露到 python 层。
### `lod_start_pos` 随着Op调用链传递
框架需要支持下列特性,以实现
`lod_start_pos`
的传递:
1.
以
`shared_ptr`
的方式实现传递
-
不修改
`lod_start_pos`
内容的作为 consumer
-
修改
`lod_start_pos`
的作为 producer
-
约定 consumer 只需要复制传递过来的
`shared_ptr`
-
producer 需要创建自己的独立的内存,以存储自己独立的修改,并暴露
`shared_ptr`
给后续 consumer
-
由于传递过程是以复制
`shared_ptr`
的方式实现,因此框架只需要传递一次
`lod_start_pos`
2.
对于不感知
`lod_start_pos`
的Op足够透明
3.
需要修改
`lod_start_pos`
的producer Op可以在
`Run`
时更新自己的
`lod_start_pos`
数据
具体的设计分为以下3小节
为了能够将SeqPos在Op的调用关系中传递下去,考虑到一些不支持序列的Op(比如FC)可能丢失SeqPos,
框架需要强制所有的OP的InferShape都必须感知并传递SeqPos,
目前最简单的方式是直接在 OperatorBase的InferShape里设置
#### `load_start_pos` 的传递
-
对于不需要修改
`lod_start_pos`
的情况,调用 LODTensor的
`ShareConstLODFrom`
接口实现复制
-
需要修改的,调用
`ShareMutableLODFrom`
接口自己分配内存以存储修改
#### 框架透明
传递这一步需要加入到网络跑之前的初始化操作中,并且只需要初始化一次,基于当前框架设计的初步方案如下
-
在 Op 的
`attrs`
中添加一项
`do_mutate_lod_info`
的属性,默认为
`false`
-
有需要修改
`lod_start_pos`
的Op需要在定义
`OpProto`
时设置为
`true`
-
`OperatorBase`
的
`InferShape`
中会读取
`do_mutate_lod_info`
,并且调用
`LODTensor`
相关的方法实现
`lod_start_pos`
的复制。
-
`OperatorBase`
中添加一个 member
`is_lod_inited{false}`
来保证传递只进行一次
一些逻辑如下
```
c++
void
InferShape
(
const
std
::
shared_ptr
<
Scope
<>&
scope
)
{
CopyInSeqToOut
();
class
OperatorBase
{
public:
// ...
}
void
InferShape
()
{
if
(
!
is_load_inited
)
{
bool
do_mutate_lod_info
=
GetAttr
<
bool
>
(
"do_mutate_load_info"
);
// find a input having LOD to copy
auto
lod_input
=
ValidLODInput
();
for
(
auto
&
output
:
outputs
)
{
if
(
do_mutate_load_info
)
{
output
.
ShareMutableLODFrom
(
lod_input
);
}
else
{
output
.
ShareConstLODFrom
(
load_input
);
}
}
is_pod_inited
=
true
;
}
// call op's InferShape
// ...
}
// if inputs has SeqPos, copy to output.
void
CopyInSeqToOut
();
private:
// ...
bool
is_lod_inited
{
false
};
};
```
如此,
`lod_start_pos`
的信息的传递对非OLD的Op的实现是完全透明的。
#### `lod_start_pos` 的更新
上一小节介绍到,对于需要修改
`load_start_pos`
的Op,
`OperatorBase`
会分配一块自己的内存以存储修改,
Op在
`Run`
的实现中,操作更新自己的
`load_start_pos`
,
而所有依赖其 outputs 的 op 会通过共享的指针自动获取到其更新。
## 根据长度排序
按照长度排序后,从前往后的时间步的batch size会自然地递减,
这是 Net 支持的
按照长度排序后,从前往后的时间步的batch size会自然地递减,
可以直接塞入 Net 做batch计算
比如:
比如
原始的输入
:
```
origin:
...
...
@@ -166,10 +189,21 @@ struct SortedSeqItem {
std
::
vector
<
SortedSeqItem
>
sorted_seqs
;
```
来追踪序列排序后的位置。
来追踪序列排序后的位置,并添加一个新的接口
```
c++
std
::
vector
<
SortedSeqItem
>
SortBySeqLen
(
const
LODTensor
&
tensor
);
```
由于输入序列的顺序变化,以下现有的接口需要针对性地修改:
-
InitMemories, memory需要根据
`sorted_seqs`
重新排列
-
SetmentInputs
-
ConcatOutputs
此外,由于
`sorted_seqs`
需要被
`RecurrentGradientOp`
复用,因此会变成
`RecurrentOp`
一个新的output输出,
之后作为
`RecurrentGradientOp`
的一个输入传入。
对比现有设计,只需要修改
`InitMemories`
,
`SegmentInputs`
和
`ConcatOutputs`
两个接口,此外添加一个
`SortBySeqLen`
的接口,
就可以支持上述变长序列,下面详细介绍。
## InitMemories
由于序列顺序的变化,
`boot_memories`
的batch上的element的顺序也需要对应重新排列。
...
...
@@ -198,57 +232,8 @@ x x
-
将每个时间步的输出重新还原为原始输入的序列顺序(以防止Infer阶段顺序打乱)
-
将每个序列concat 为规则的mini-batch表示
## 附录
这里演示多level的变长序列的存储方法,本设计会用两层的
`vector`
来存储所有序列的信息,具体数据格式如下
```
c++
std
::
vector
<
std
::
vector
<
std
::
vector
<
int
>>
seq_start_positions_
;
```
为了方便讨论,可以临时修改为
```
c++
typedef
std
::
vector
<
int
>
element_t
;
std
::
vector
<
element_t
>
seq_start_positions_
;
```
假设tensor 里按batch存储 instance作为基本单位,
默认序列里的元素都是相邻排列,
因此只需要以instance 为基本单位,
记录 start position就可以分解出每个序列的信息。
`seq_start_positions_`
里从上往下存储着
`level 0 ~ level L`
的元素,可以认为level越小,表示的序列粒度越大。
比如存储
`batch of paragraphs`
则有
-
`level 0`
存储 paragraphs 的 start positions
-
`level 1`
存储 sentences 的 start positions
因为 tensor 里存储着batch of words,所以以上两个level的start positions的单位均为word。
具体地,假设有如下例子,比如需要存储 batch of paragraphs,tensor中存储了 batch of words,而序列信息如下
-
paragraph 0 has 3 sentences:
-
sentence 0 has 3 words
-
sentence 1 has 4 words
-
sentence 2 has 2 words
-
paragraph 1 has 2 sentences:
-
sentence 0 has 5 words
-
sentence 1 has 3 words
那么
`seq_start_positions_`
会有如下内容
-
0 9(=3+4+2)
-
0 3 7
-
0 5
其中每行是一个
`element_t`
,具体含义如下
-
`seq_start_positions_[0]`
存储了
`0 9`
,表示paragraph 0 在 tensor 中的偏移为 0,对应地, paragraph 1 为 9 (以word 为单位)
-
从
`seq_start_positions_[0]`
中可以知道,当前
`mini-batch`
总共只有 2 个 paragraph,因此后续的两个
`element_t`
分别存储了两个 paragraph 中句子的信息
-
紧接着
`seq_start_positions_[1]`
存储了第0个paragraph 的信息,表明有3个sentence,其在paragraph 0在tensor中对应部分的偏移分别为0,3 和7
-
紧接着
`seq_start_positions_[2]`
存储了第1个paragraph 的信息,表明有2个sentence,其在paragraph 0在tensor中对应部分的偏移分别为0和 5
如上证明了
`seq_start_positions_`
的数据结构适用于 level 为 1(也就是Paddle中subseq),
**通过归纳法可以证明其适用于 N level 的序列,这里暂不赘述**
。
## 参考文献
1.
[
Tensorflow Bucketing
](
https://www.tensorflow.org/versions/r0.12/api_docs/python/contrib.training/bucketing
)
2.
[
mxnet Bucketing
](
http://mxnet.io/how_to/bucketing.html
)
3.
[
variable length input in RNN scenario
](
https://discuss.pytorch.org/t/about-the-variable-length-input-in-rnn-scenario/345/5
)
4.
[
Level of details
](
https://en.wikipedia.org/wiki/Level_of_detail
)
编辑
预览
Markdown
is supported
0%
请重试
或
添加新附件
.
添加附件
取消
You are about to add
0
people
to the discussion. Proceed with caution.
先完成此消息的编辑!
取消
想要评论请
注册
或
登录