自制深度学习推理框架-第8.2节-ResNet中的Add层

自制深度学习推理框架-第8.2节-ResNet中的Add层

我们的视频课程

视频链接

我们的上游项目

上游项目链接

本课务必配合视频课程一起阅读 后续的课程根据上游项目的开发而制定课程表

本节课代码

git clone https://github.com/zjhellofss/KuiperCourse.git
git checkout eight
# 国内备用地址 https://gitee.com/fssssss/KuiperCourse.git
#https://github.com/zjhellofss/KuiperInfer 点个star

上节内容回顾

我们在上节课的内容中讲到了对计算图中表达式的解析, 表达式层在ResNet中也有体现, ResNetAdd层做shortcut. 对于Add层,对应的表达式如下: add也是一个表达式

add(@0,@1)

上述表达式的表示将第一个输入数@0和第二个输入数@1. 表达式中的add就是我们项目中的RuntimeOperator,而两个输入数对应了项目中RuntimeOperand.

Operand和Operator是什么

根据之前课程的讲述, RuntimeOperator对应的是各个层的运算过程, 该结构中也保存了运算过程所需要的输入和输出操作数. RuntimeOperand就是各个运算过程所需要的输入和输出操作数.

操作数的具体数据来源于训练得到的模型权重文件, 而运算过程来源于模型的定义文件, 他们在合适的时机被加载到推理系统中.

怎么得到KuiperInfer的Operand和Operator

回忆在上上节课中的内容, 我们曾经分享了从PNNX模型定义文件和权重文件中加载PNNX::operatorPNNX::operand的过程.

我们根据上述以上的两个数据结构转换到对应的kuiperInfer::RuntimeOperatorkuiperinfer::RuntimeOperand, 其中包括了输入和输出数据的拷贝, 计算过程的参数解析和复制, 以及权重的转换.

逆波兰式

我们以简单的例子来说明, add(@0,@1)对于这个计算式来说, 如果RuntimeOperator对它进行处理, 首先遇到的节点将会是add, 但是在遇到add时候缺少计算所需要的具体数据@0@1.

所以我们需要对它进行逆波兰转换得到操作数在前,计算在后的形式, 它的实现也很简单, 就是将原有的二叉树进行后续遍历即可: @0,@1,add

void ReversePolish(const std::shared_ptr &root_node,
 std::vector &reverse_polish) {
 if (root_node != nullptr) {
 ReversePolish(root_node->left, reverse_polish);
 ReversePolish(root_node->right, reverse_polish);
 reverse_polish.push_back(root_node);
 }
}

add (@0,@1)对应的逆波兰式如下:@0,@1,add

add(mul(@0,@1),@2)对应的逆波兰式如下: @0,@1,mul,@2,add

整体过程串联

经过这样的转换, 就可以使得每次在处理计算的时候都能够保证, 所需要的操作数已经到位. 所以我们结合上节课的内容来串联一下整体的过程.

  1. 传入Expression:string, 例如add(mul(@0,@1),@2) 字符串

  2. add(mul(@0,@1),@2)按照词法分析为多个tokens, 且在拆分的时候需要进行词法校验

  • add
  • mul
  • (
  • @0
  • ,
  • @1
  • )
  • ,
  • @2
  1. 根据已知的tokens, 通过递归向下遍历的语法分析得到对应的计算二叉树. 二叉树的各个节点为add,mul或者@0,@1. 都在上节课当中

  2. 将计算二叉树进行逆波兰变换, 得到的逆波兰式如下:@0, @1, mul, @2, add. ?

Expression Operator的定义

class ExpressionOp : public Operator {
 public:
 explicit ExpressionOp(const std::string &expr);
 std::vector Generate();
 private:
 std::unique_ptr parser_; //语法分析和词法分析
 std::vector nodes_; // 逆波兰之后的表达式,也是一个list
 std::string expr_;
};

其中expr_表示表达式字符串, nodes_表示经过逆波兰变换之后得到的节点. 通过Operator去初始化Layer,layer才负责计算.

Expression Layer的定义

class ExpressionLayer : public Layer {
 public:
 explicit ExpressionLayer(const std::shared_ptr &op);
 void Forwards(const std::vector &inputs,
 std::vector &outputs) override;
 private:
 std::unique_ptr op_;
};

初始化Expression Layer

ExpressionLayer::ExpressionLayer(const std::shared_ptr &op) : Layer("Expression") {
 CHECK(op != nullptr && op->op_type_ == OpType::kOperatorExpression);
 ExpressionOp *expression_op = dynamic_cast(op.get());
 CHECK(expression_op != nullptr) op_ = std::make_unique(*expression_op);
}

通过Expression operator去初始化Layer的过程, operatorlayer的区别已经在之前多次讲过, operator负责某类型节点参数的记录, layer负责对应节点的具体计算.

Expression Layer中的输入排布

inputs当中的输入怎么排布. 两个或者多个操作数

C = A + B

input1 里面存放了A的四个数字

input2 里面存放了B的四个数字

C1 = A1+B1 = input11+input21

C2 = A2+B2 = input12+input22

A总共有batch size个

B总共有batch size个

Expression Layer的输入中, 多个输入依次排布. 如果batch_size的大小为4, 则上图中input1中的元素数量为4, input2的元素数量也为4. 换句话说, input1中的数据都来源于操作数1(operand 1), input2中的数据都来源于操作数2(operand 2).

将数据存放到input1input2的实现如下:

int batch_size = 4;
 for (int i = 0; i < batch_size; ++i) {
 std::shared_ptr input = std::make_shared(3, 224, 224);
 input->Fill(1.f);
 inputs.push_back(input);
 }
// [tensor(1.f) tensor(1.f) tensor(1.f) tensor(1.f)]
 for (int i = 0; i < batch_size; ++i) {
 std::shared_ptr input = std::make_shared(3, 224, 224);
 input->Fill(2.f);
 inputs.push_back(input);
 }
// [tensor(1.f) tensor(1.f) tensor(1.f) tensor(1.f) tensor(2.f) tensor(2.f) tensor(2.f) tensor(2.f)]

inputs被分为两段, 前半段存放input1, 前半段的长度为4. 后半段存放input2, 后半段的长度为4.

计算的结果存放在outputs, 8个输入数据两两相加, 最后的输出数据大小等于4.

Expression Layer的计算过程

数据排布

第一个例子

input1 = @0

顺序是0…3

input2 = @1

顺序是4…7

已知有如上的数据存储排布, 在本节中我们将讨论如何根据现有的数据完成add(@0,@1)计算. 可以看到每一次计算的时候, 都以此从input1input2中取得一个数据进行加法操作, 并存放在对应的输出位置.

@0 是一个张量, 总共有batch_size个, 每个大小是3*224*224

因为batch_size里面来自与同一个批次,我们需要同一个批次另外一个操作数进行两两相加

第二个例子

下图的例子展示了对于三个输入,mul(add(@0,@1),@2)的情况:

每次计算的时候依次从input1, input2input3中取出数据, 并作出相应的运算, 并将结果数据存放于对应的output中.

input1 = @0

input2 = @1

input3 = @2

操作数处理的代码实现

ExpressionLayer::Forward函数中, 首先检查输入是否为空, 并初始化outputs数组中的元素.

CHECK(!inputs.empty());
 const uint32_t batch_size = outputs.size();
 CHECK(batch_size != 0);
 for (uint32_t i = 0; i < batch_size; ++i) {
 CHECK(outputs.at(i) != nullptr & !outputs.at(i)->empty());
 outputs.at(i)->Fill(0.f);
 }
 CHECK(this->op_ != nullptr & this->op_->op_type_ == OpType::kOperatorExpression);
 std::stack op_stack;
 const std::vector &token_nodes = this->op_->Generate();

this->op_->Generate(); 获得的是逆波兰表达式. @0 @1 add @2 mul

for (const auto &token_node : token_nodes) {
 if (token_node->num_index >= 0) {
 uint32_t start_pos = token_node->num_index * batch_size;
 //@0的num_index = 0
 // @1的num_Index = 1
 	// inputs 按照上述的依次存放位置
 std::vector input_token_nodes;
 for (uint32_t i = 0; i < batch_size; ++i) {
 CHECK(i + start_pos < inputs.size());
 // @0 start_pos = 0 ---> i + start_pos = 0..3
 // @1 start_pos = 4 ---> i + start_pos = 4..7
 input_token_nodes.push_back(inputs.at(i + start_pos));
 }
 op_stack.push(input_token_nodes);
 }
 }

依次遍历逆波兰表达式, 如果当前的op遇到的是一个操作数, 例如@0或者@1. 就将他们一个批次的数据(input_token_nodes)全部读取出来, 并临时存放到栈op_stack中.

举个例子, 对于input1就将input1中所有的数据读取出来并存放到input_token_nodes中, 再将input_token_nodes这一个批次的数据放入到栈中.

根据输入的逆波兰式@0,@1,add,遇到的第一个节点是操作数是@0, 所以栈op_stack内的内存布局如下:

当根据顺序遇到第二个节点(op)的时候, 操作数@1的时候, 再将inputs中的操作数读取出来并存放到input_token_nodes中, 再将input_token_nodes这一个批次的数据放入到栈中.

运算符处理的代码实现

// @0 @1 add
	const int32_t op = token_node->num_index;
 CHECK(op_stack.size() >= 2) 

当节点(op)类型为操作符号的时候, 首先弹出栈(op_stack)内的两个批次操作数, 对于如上的情况input_node1分别存放input1...4, input_node2分别存放input5...8.

CHECK(input_node1.size() == input_node2.size());
 std::vector output_token_nodes(batch_size);
 for (uint32_t i = 0; i < batch_size; ++i) {
 // 进行一个依次两两的相加
 if (op == -int(TokenType::TokenAdd)) {
 output_token_nodes.at(i) = ftensor::ElementAdd(input_node1.at(i), input_node2.at(i));
 } else if (op == -int(TokenType::TokenMul)) {
 output_token_nodes.at(i) = ftensor::ElementMultiply(input_node1.at(i), input_node2.at(i));
 } else {
 LOG(FATAL) 

当获取大小长度为batch_sizeinput_node1input_node2后, 流程在for(int i = 0...batch_size)中对两个输入进行两两操作, 操作类型定义于当前的op中. 对于逆波兰式@0,@1,add, 在如上处理完两个输入节点之后,当前的节点类型是add.

output_token_nodes 保留了输出,

CHECK(op_stack.size() == 1);
 std::vector output_node = op_stack.top();
 op_stack.pop();
 for (int i = 0; i < batch_size; ++i) {
 CHECK(outputs.at(i) != nullptr & !outputs.at(i)->empty());
 outputs.at(i) = output_node.at(i);
 }

实验

TEST(test_expression, add) {
 using namespace kuiper_infer;
 const std::string &expr = "add(@0,@1)";
 std::shared_ptr expression_op = std::make_shared(expr);
 ExpressionLayer layer(expression_op);
 std::vector inputs;
 std::vector outputs;
 int batch_size = 4;
 for (int i = 0; i < batch_size; ++i) {
 std::shared_ptr input = std::make_shared(3, 224, 224);
 input->Fill(1.f);
 inputs.push_back(input);
 }
 for (int i = 0; i < batch_size; ++i) {
 std::shared_ptr input = std::make_shared(3, 224, 224);
 input->Fill(2.f);
 inputs.push_back(input);
 }
 for (int i = 0; i < batch_size; ++i) {
 std::shared_ptr output = std::make_shared(3, 224, 224);
 outputs.push_back(output);
 }
 layer.Forwards(inputs, outputs);
 for (int i = 0; i < batch_size; ++i) {
 const auto &result = outputs.at(i);
 for (int j = 0; j < result->size(); ++j) {
 ASSERT_EQ(result->index(j), 3.f);
 }
 }
}

输入表达式为add(@0,@1),可以看到输入input1中的数据均为1, 输入input2中的数据均为2, 所以存放结果的每一个节点数据均为3.

TEST(test_expression, complex) {
 using namespace kuiper_infer;
 const std::string &expr = "add(mul(@0,@1),@2)";
 std::shared_ptr expression_op = std::make_shared(expr);
 ExpressionLayer layer(expression_op);
 std::vector inputs;
 std::vector outputs;
 int batch_size = 4;
 for (int i = 0; i < batch_size; ++i) {
 std::shared_ptr input = std::make_shared(3, 224, 224);
 input->Fill(1.f);
 inputs.push_back(input);
 }
 for (int i = 0; i < batch_size; ++i) {
 std::shared_ptr input = std::make_shared(3, 224, 224);
 input->Fill(2.f);
 inputs.push_back(input);
 }
 for (int i = 0; i < batch_size; ++i) {
 std::shared_ptr input = std::make_shared(3, 224, 224);
 input->Fill(3.f);
 inputs.push_back(input);
 }
 for (int i = 0; i < batch_size; ++i) {
 std::shared_ptr output = std::make_shared(3, 224, 224);
 outputs.push_back(output);
 }
 layer.Forwards(inputs, outputs);
 for (int i = 0; i < batch_size; ++i) {
 const auto &result = outputs.at(i);
 for (int j = 0; j < result->size(); ++j) {
 ASSERT_EQ(result->index(j), 5.f);
 }
 }
}

输入表达式为add(mul(@0,@1),@2),可以看到输入input1中的数据均为1, 输入input2中的数据均为2, 所以存放结果的每一个节点数据均为5.

作者:qq_32901731原文地址:https://blog.csdn.net/qq_32901731/article/details/128748721

%s 个评论

要回复文章请先登录注册