石家庄建站优化公司,做网站需要apache,莱州市规划建设管理局网站,网站后期维护需要注意什么再探Tensor类#xff1a; 第二节中我们编写的Tensor类其实并不能满足我们的使用需要#xff0c;我们将在这一节以代码阅读的方式来看看一个完全版本的Tensor应该具备怎样的要素#xff0c;同时我们对Tensor类的分析来看看在C中一个设计好的类应该是怎么样的。
Tensorfl…再探Tensor类 第二节中我们编写的Tensor类其实并不能满足我们的使用需要我们将在这一节以代码阅读的方式来看看一个完全版本的Tensor应该具备怎样的要素同时我们对Tensor类的分析来看看在C中一个设计好的类应该是怎么样的。
Tensorfloat::Tensor(uint32_t channels, uint32_t rows, uint32_t cols) {data_ arma::fcube(rows, cols, channels);if (channels 1 rows 1) {this-raw_shapes_ std::vectoruint32_t{cols};} else if (channels 1) {this-raw_shapes_ std::vectoruint32_t{rows, cols};} else {this-raw_shapes_ std::vectoruint32_t{channels, rows, cols};}
}在这里raw_shape记录的是另外一个方面的形状信息主要用于review和flatten层中。
举一个简单的例子当Tensor将一个大小为(2,16,1)的Tensor reshape到(32,1,1)的大小时raw_shapes变量会被记录成(32). 将一个大小为(2,16, 2)的Tensor reshape到(2, 64)的大小时raw_shapes会被记录成(2,64). 那这样做的目的是什么呢原来的Tensor不能在逻辑上区分当前的张量是三维的、二维的还是一维的因为实际的数据存储类arma::fcube总是一个三维数据。所以我们要区分他的逻辑结构就需要这么一个raw_shape 列优先的Reshape
void Tensorfloat::ReRawshape(const std::vectoruint32_t shapes) {CHECK(!this-data_.empty());CHECK(!shapes.empty());const uint32_t origin_size this-size();uint32_t current_size 1;for (uint32_t s : shapes) {current_size * s;}CHECK(shapes.size() 3);CHECK(current_size origin_size);if (shapes.size() 3) {this-data_.reshape(shapes.at(1), shapes.at(2), shapes.at(0));this-raw_shapes_ {shapes.at(0), shapes.at(1), shapes.at(2)};} else if (shapes.size() 2) {this-data_.reshape(shapes.at(0), shapes.at(1), 1);this-raw_shapes_ {shapes.at(0), shapes.at(1)};} else {this-data_.reshape(shapes.at(0), 1, 1);this-raw_shapes_ {shapes.at(0)};}
}我们再来分析一下这个函数如果传入的shapes是1维的就相当于将数据展开为(elem_size,1,1)并将逻辑维度赋值为1. 如果传入的shapes相当于将数据展开为(shapes.at(0), shapes.at(1), 1). 我们来看看下面的这个图例 如果把上面的(2,2,3)展平为一维的那就应该是如下图所示 而且这也是arma:cube的默认排序列排序
行优先的Reshape
那如果我们在某些情况下需要行优先的Reshape呢
void Tensorfloat::ReView(const std::vectoruint32_t shapes) {CHECK(!this-data_.empty());const uint32_t target_channels shapes.at(0);const uint32_t target_rows shapes.at(1);const uint32_t target_cols shapes.at(2);arma::fcube new_data(target_rows, target_cols, target_channels);const uint32_t plane_size target_rows * target_cols;for (uint32_t c 0; c this-data_.n_slices; c) {const arma::fmat channel this-data_.slice(c);for (uint32_t c_ 0; c_ this-data_.n_cols; c_) {const float* colptr channel.colptr(c_);for (uint32_t r 0; r this-data_.n_rows; r) {const uint32_t pos_index c * data_.n_rows * data_.n_cols r * data_.n_cols c_;const uint32_t ch pos_index / plane_size;const uint32_t row (pos_index - ch * plane_size) / target_cols;const uint32_t col (pos_index - ch * plane_size - row * target_cols);new_data.at(row, col, ch) *(colptr r);}}}this-data_ new_data;
}我们只能通过位置计算的方式来对逐个元素进行搬运const uint32_t plane_size target_rows * target_cols;来计算行数和列数相乘的积。
const uint32_t pos_index c * data_.n_rows * data_.n_cols r * data_.n_cols c_; 得 到调整前的元素下标随后我们计算调整后的通道下标位置ch pos_index / plane_sizeplane_size就是和一面一行乘一列。同理计算row,col等调整位置后的行、列坐标。 计算图关系
内容回顾
我们在回顾一下之前的内容我们根据pnnx计算图得到了我们的计算图我们的计算图由两部分组成分别是kuiper_infer::RuntimeOperator和kuier_infer::RuntimeOperand.
但是作为一个计算图计算节点之间往往是有连接的包括从input operator到第一个计算节点再到第二个计算节点直到最后的输出节点output operator我们再来回顾一下这两个数据结构的具体定义
struct RuntimeOperator {int32_t meet_num 0; /// 计算节点被相连接节点访问到的次数~RuntimeOperator() {for (auto param : this-params) {if (param.second ! nullptr) {delete param.second;param.second nullptr;}}}std::string name; /// 计算节点的名称std::string type; /// 计算节点的类型std::shared_ptrLayer layer; /// 节点对应的计算Layerstd::vectorstd::string output_names; /// 节点的输出节点名称std::shared_ptrRuntimeOperand output_operands; /// 节点的输出操作数std::mapstd::string, std::shared_ptrRuntimeOperand input_operands; /// 节点的输入操作数std::vectorstd::shared_ptrRuntimeOperand input_operands_seq; /// 节点的输入操作数顺序排列std::mapstd::string, std::shared_ptrRuntimeOperator output_operators; /// 输出节点的名字和节点对应std::mapstd::string, RuntimeParameter * params; /// 算子的参数信息std::mapstd::string, std::shared_ptrRuntimeAttribute attribute; /// 算子的属性信息内含权重信息
};std::map:string std::shared_ptr output_operators; 我们重点来看这个定义它是当前这个计算节点的下一个计算节点当数据在当前RuntimeOperator上计算完成之后系统会读取output_operators中准备就绪的算子并开始执行。std::map:string std::shared_ptr input_operands; 是当前计算节点所需要的输入它往往来自于上一个RuntimeOperator的输入。std::shared_ptr output_operands; 是当前节点计算得到的输出它是通过当前的op计算得到的。
具体的流程是这样的假设我们在系统中有三个RuntimeOperators分别为op1,op2和op3. 这三个算子的顺序是依次执行的分别是op1--op2--op3.
当我们执行第一个算子op1的时候需要将来自于图像的输入填充到op1-input_operands中。第一个算子op1开始执行执行的过程中读取op1-input_operands并计算得到相关的输出放入到op1-output_operands中从op1的output_operators中读取到ready的op2第二个算子op2开始执行执行的过程读取op1-output_operands并拷贝op2-input_operands中随后op2算子开始执行并计算得到相关的输出放入到op2-output_operands中。 所以我们可以看到者之间是有一个图关系的那我们来看一下他是怎么构建这样一个图关系的 怎样构建图关系
/ 构建图关系for (const auto current_op : this-operators_) {const std::vectorstd::string output_names current_op-output_names;for (const auto next_op : this-operators_) {if (next_op current_op) {continue;}if (std::find(output_names.begin(), output_names.end(), next_op-name) !output_names.end()) {current_op-output_operators.insert({next_op-name, next_op});}}}- **const std::vector\std::string output_names current_op-output_names;** 存放的是当前op的output_namesoutput_names也就是当前算子的后一层算子的名字。对于op1它的output_names就是op2的name.- **const auto next_op : this-operators_** 我们遍历整个图中的RuntimeOperators如果遇到next_op的name和当前current_op-output_name是一致的那么我们就可以认为next_op是当前op的下一个节点之一。- **current_op-output_operators.insert({next_op-name, next_op});** 将next_op插入到current_op的下一个节点当中。- 这样一来当current_op执行完成之后就取出next_op并将当前current_op的输出output_opends输出拷贝到next_op的input_operands输入中。
因为在初始化的时候就已经约定好了op1的输出是op2所以只要在接下来的点中不停地寻找op2就好了找到了之后就把它insert到output_operators里面.这个output_operators是一个map就可以让输出节点的名字和节点对应。 这么一个计算图也得有输入输出节点吧
作者傅大狗 链接https://zhuanlan.zhihu.com/p/604613883 来源知乎 著作权归作者所有。商业转载请联系作者获得授权非商业转载请注明出处。 this-input_operators_maps_.clear();this-output_operators_maps_.clear();for (const auto kOperator : this-operators_) {if (kOperator-type pnnx.Input) {this-input_operators_maps_.insert({kOperator-name, kOperator});} else if (kOperator-type pnnx.Output) {if (kOperator-name output_name) {this-output_operators_maps_.insert({kOperator-name, kOperator});} else {LOG(FATAL) The graph has two output operator!;}} else {std::shared_ptrLayer layer RuntimeGraph::CreateLayer(kOperator);CHECK(layer ! nullptr) Layer create failed!;if (layer) {kOperator-layer layer;}}}kOperator-type pnnx.Output 找到this-operators中的输出节点但是目前Kuiperinfer只支持一个输出节点其实也可以多输出作为一个教学框架我实在不想支持这种corner case同理: kOperator-type pnnx.Input 来找到图中也就是op list中的输入节点
就是在op3结束之后我们还要把op3的output_operand复制到输出节点的input_operand里面
初始化输入
struct RuntimeOperand {std::string name; /// 操作数的名称std::vectorint32_t shapes; /// 操作数的形状std::vectorstd::shared_ptrTensorfloat datas; /// 存储操作数 为什么是vector因为是一个batch如果batch是2的话那就存储的是两个RuntimeDataType type RuntimeDataType::kTypeUnknown; /// 操作数的类型一般是float
};可以看到这里的RuntimeOperand::datas就是存储具体数据的地方我们初始化输入输出的空间也就是要在推理之前先根据shapes来初始化好这里datas的空间
代码位于runtime_ir.cpp的InitOperatorInputTensor中 RuntimeGraphShape::InitOperatorInputTensor(operators_) 这个函数的输入是operator list, 所以将在这个函数中对所有的op进行输入和输出空间的初始化。
得到一个op的输入空间input_operands
const std::mapstd::string, std::shared_ptrRuntimeOperand input_operands_map op-input_operands;如果初始的是空就continue
for (const auto op : operators) {if (op-input_operands.empty()) {continue;} 得到input_operands中记录的数据应有大小input_operand_shape和存储数据的变量input_datas
auto input_datas input_operand-datas;CHECK(!input_operand_shape.empty());
const int32_t batch input_operand_shape.at(0);
CHECK(batch 0) Dynamic batch size is not supported!;
CHECK(input_operand_shape.size() 2 ||input_operand_shape.size() 4 ||input_operand_shape.size() 3)我们需要根据input_operand_shape中记录的大小去初始化input_datas. 而input_operand_shape可能是三维的二维的以及一维的如下方所示input_operand_shape : (batch, elemsize) 一维的input_operand_shape : (batch, rows,cols) 二维的input_operand_shape : (batch, rows,cols, channels) 三维的
如果当前input_operand_shape是二维的数据也就是说输入维度是(batch,rows,cols)的. 我们首先对batch进行遍历对一个batch的中的数据input_datas op-input_operand(输入)进行初始化。
input_datas.resize(batch);
for (int32_t i 0; i batch; i) {
}在for循环内它会调用如下的方法去初始化一个二维的张量
input_datas.at(i) std::make_sharedTensorfloat(1, input_operand_shape.at(1), input_operand_shape.at(2));这一块不太清楚我们实际代码看一遍 for (int32_t i 0; i batch; i) {if (input_operand_shape.size() 4) {input_datas.at(i) std::make_sharedTensorfloat(input_operand_shape.at(1), input_operand_shape.at(2),input_operand_shape.at(3));
也就是如果是shape 4 那就是三维的那么1就是channel2就是row3就是col
那么如果输入的channel 1或者row 1
Tensorfloat::Tensor(uint32_t channels, uint32_t rows, uint32_t cols) {data_ arma::fcube(rows, cols, channels);if (channels 1 rows 1) {this-raw_shapes_ std::vectoruint32_t{cols};} else if (channels 1) {this-raw_shapes_ std::vectoruint32_t{rows, cols};} else {this-raw_shapes_ std::vectoruint32_t{channels, rows, cols};}
}那就正好被初始化为了我们之前的raw_shape 这样的一个逻辑维度
这就和我们上面的课程内容对应上了Tensorfloat原本是一个三维数据我们怎么在逻辑上给他表现成一个二维的张量呢这就要用到我们上面说到的raw_shapes了。
调用并初始化一维的数据也同理, 在初始化的过程中会调用(channels1rows1) 这个条件判断并将raw_shapes这个维度定义成一维。
input_datas.at(i) std::make_sharedTensorfloat(1, input_operand_shape.at(1), 1)
避免第二次初始化
那么在计算的过程中我们只需要一次初始化就可以。
所以在第二次遇到她的时候只需要去检查空间是否发生改变就可以啦 if (!input_datas.empty()) {CHECK(input_datas.size() batch) Batch size is wrong!;for (int32_t i 0; i batch; i) {const std::vectoruint32_t input_data_shape input_datas.at(i)-shapes();CHECK(input_data_shape.size() 3) THe origin shape size of operator input data do not equals to three;if (input_operand_shape.size() 4) {CHECK(input_data_shape.at(0) input_operand_shape.at(1) input_data_shape.at(1) input_operand_shape.at(2) input_data_shape.at(2) input_operand_shape.at(3));} else if (input_operand_shape.size() 2) {CHECK(input_data_shape.at(1) input_operand_shape.at(1) input_data_shape.at(0) 1 input_data_shape.at(2) 1);} else {// current shape size 3CHECK(input_data_shape.at(1) input_operand_shape.at(1) input_data_shape.at(0) 1 input_data_shape.at(2) input_operand_shape.at(2));}}} CHECK(input_data_shape.at(0) input_operand_shape.at(1) input_data_shape.at(1) input_operand_shape.at(2) input_data_shape.at(2) input_operand_shape.at(3)); 上面这一部分左边是我们实际有的shape也就是我们第一次初始化的shape而右边的是我们再次遇到的时候应该具有的shape所以这一次就是check这两个shape是否一致如果check不通过那就代表输入空间的大小被改变了。那这样的话就会报错退出这个程序 示例 这里的conv的输出分别是1和2 expression就接受了1和2为输入 最近在忙老师布置的任务就耽误了这方面的进度慢慢补把