Skip to content
体验新版
项目
组织
正在加载...
登录
切换导航
打开侧边栏
BaiXuePrincess
Paddle
提交
226bf1dd
P
Paddle
项目概览
BaiXuePrincess
/
Paddle
与 Fork 源项目一致
Fork自
PaddlePaddle / Paddle
通知
1
Star
1
Fork
0
代码
文件
提交
分支
Tags
贡献者
分支图
Diff
Issue
0
列表
看板
标记
里程碑
合并请求
0
Wiki
0
Wiki
分析
仓库
DevOps
项目成员
Pages
P
Paddle
项目概览
项目概览
详情
发布
仓库
仓库
文件
提交
分支
标签
贡献者
分支图
比较
Issue
0
Issue
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
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.
先完成此消息的编辑!
取消
想要评论请
注册
或
登录