提交 226bf1dd 编写于 作者: S Superjom

update more details

上级 d3213e4c
......@@ -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_positions_;
std::shared_ptr<std::vector<std::vector<int>>> lod_start_pos_;
```
附录中演示如何用二维的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.
先完成此消息的编辑!
想要评论请 注册