从零构建深度学习推理框架-11 Resnet

op和layer结构

在runtime_ir.cpp中,我们上一节只构建了input和output,对于中间layer的具体实现一直没有完成:

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") {this->output_operators_maps_.insert({kOperator->name, kOperator});} else {std::shared_ptr<Layer> layer = RuntimeGraph::CreateLayer(kOperator);CHECK(layer != nullptr) << "Layer create failed!";if (layer) {kOperator->layer = layer;}}

这个CreateLayer就是创建层的过程:


std::shared_ptr<Layer> RuntimeGraph::CreateLayer(const std::shared_ptr<RuntimeOperator>& op) {LOG_IF(FATAL, !op) << "Operator is empty!";const auto& layer = LayerRegisterer::CreateLayer(op);LOG_IF(FATAL, !layer) << "Layer init failed " << op->type;return layer;
}

 会根据我们输入的op里的type属性判断,之后返回给对应的

std::shared_ptr<Layer> LayerRegisterer::CreateLayer(const std::shared_ptr<RuntimeOperator> &op) {CreateRegistry &registry = Registry();const std::string &layer_type = op->type;LOG_IF(FATAL, registry.count(layer_type) <= 0)<< "Can not find the layer type: " << layer_type;const auto &creator = registry.find(layer_type)->second;LOG_IF(FATAL, !creator) << "Layer creator is empty!";std::shared_ptr<Layer> layer;const auto &status = creator(op, layer);LOG_IF(FATAL, status != ParseParameterAttrStatus::kParameterAttrParseSuccess)<< "Create the layer: " << layer_type<< " failed, error code: " << int(status);return layer;
}

 这里的creator就是我们GetInstance的过程

由于代码进行了较多的修改, 下方的代码位于layer/details/convolution.cpp中:

ParseParameterAttrStatus ConvolutionLayer::GetInstance(const std::shared_ptr<RuntimeOperator>& op,std::shared_ptr<Layer>& conv_layer)

convolution层的初始化为例, 来看算子初始化时的流程.GetInstance函数的用处和之前一样, 是被注册到全局注册表中的, 在框架初始化的时候填入到注册表, 在某个模型需要对该层进行初始化的时候从全局注册表中取出.

唯一变化的是接口的参数, 这里的conv_layer是一个待初始化的卷积层, 这里的op中包含了初始化需要的参数和权重信息. 我们要在GetInstance中根据op来完成对conv_layer的初始化.

CHECK(op != nullptr) << "Convolution operator is nullptr";const std::map<std::string, RuntimeParameter*>& params = op->params;if (params.find("dilation") == params.end()) {LOG(ERROR) << "Can not find the dilation parameter";return ParseParameterAttrStatus::kParameterMissingDilation;}const auto& dilation_param =dynamic_cast<RuntimeParameterIntArray*>(params.at("dilation"));const auto& in_channel =dynamic_cast<RuntimeParameterInt*>(params.at("in_channels"));if (!in_channel) {LOG(ERROR) << "Can not find the in channel parameter";return ParseParameterAttrStatus::kParameterMissingInChannel;}

首先我们通过 const std::map<:string runtimeparameter>& params = op->params;

获得op中的参数信息被保存在一个map中, map的key是string类型, value是我们之前说过的RuntimeParameter类型. 如果我们要得到Conv parameter中名字为xx的参数, 可以直接使用params.find("xx"), 但是在这个参数map有可能并不存在这个参数值, 例如我们想从params中得到卷积层关于输入通道大小的参数, 我们可以使用params.at("in_channels)"进行访问, at返回的值是一个指向RuntimeParameter的一个指针.

举个例子:

 if (params.find("in_channels") == params.end()) {LOG(ERROR) << "Can not find the in channel parameter";return ParseParameterAttrStatus::kParameterMissingInChannel;}const auto& in_channel =dynamic_cast<RuntimeParameterInt*>(params.at("in_channels"));if (!in_channel) {LOG(ERROR) << "Can not find the in channel parameter";return ParseParameterAttrStatus::kParameterMissingInChannel;}

这里就是首先搜寻In_channel这个param,如果诶呦的话那就报错,如果有的话那就添加进来,其余的也是,就是将pnnx.的param添加到我们的这个op的param中

我们在此处为什么要使用dynamic_cast呢? 原因就在于我们使用这种动态转换来将一个指向父类的指针转换到一个指向子类的指针, 如果这个指针确实属于这个子类那么他就会转换成功,否则就会返回nullptr. 也就是这里, 我们认为in_channels这个参数是属于RuntimeParameterInt子类的指针.

后续包括访问padding等参数的方法是同理的, 我们可以在代码中看到, 我们使用params.find("bias")的方法得到一个指向RuntimeParameter的指针, 再用动态转换的方法得到一个指向RuntimeParameterBool的子类指针. 如果此处通过params.find("bias")不是一个指向...Bool的指针, 在这里的动态转换就会返回一个空指针.

if (params.find("bias") == params.end()) {LOG(ERROR) << "Can not find the bias parameter";return ParseParameterAttrStatus::kParameterMissingUseBias;}const auto& use_bias = dynamic_cast<RuntimeParameterBool*>(params.at("bias"));if (!use_bias) {LOG(ERROR) << "Can not find the bias parameter";return ParseParameterAttrStatus::kParameterMissingUseBias;}

我们刚刚GetInstannce传进来的这个 conv_layer其实是一个空的指针。所以我们需要对其进行初始化:

Conv layer 初始化:

通过上面的流程, 我们已经获得了RuntimeOp中保存的层参数, 下面我们就要去得到这些参数中的值, 并且去初始化这个层.

const uint32_t dims = 2;const std::vector<int>& kernels = kernel->value;const std::vector<int>& paddings = padding->value;const std::vector<int>& strides = stride->value;conv_layer = std::make_shared<ConvolutionLayer>(out_channel->value, in_channel->value, kernels.at(0), kernels.at(1),paddings.at(0), paddings.at(1), strides.at(0), strides.at(1),groups->value, use_bias->value);
  • 此处的kernel, padding, stride等参数都通过动态转换指向RuntimeParameterIntArray类型, 所以如果调用其中的value值, 会得到一个vector<int>类型的参数.
  • 此处的 group参数通过动态转换指向RuntimeParameterInt类型, 所以调用其中的value值, 会得到一个int类型的参数.
  • 此处的use_bias参数通过动态转换指向RuntimeParameterBool类型, 所以调用其中的value值, 会得到一个bool类型的参数.

在流程结束后, conv layer中已经包含了卷积计算所需要的所有相关参数, 初始化这个层的方式是conv_layer = std::make_shared\<ConvolutionLayer>. 在初始化完成之后, 关于该层的所有参数就均会被保存到conv_layer的类内变量中.

ConvolutionLayer::ConvolutionLayer(uint32_t output_channel, uint32_t in_channel,uint32_t kernel_h, uint32_t kernel_w,uint32_t padding_h, uint32_t padding_w,uint32_t stride_h, uint32_t stride_w,uint32_t groups, bool use_bias): ParamLayer("Convolution"),use_bias_(use_bias),groups_(groups),padding_h_(padding_h),padding_w_(padding_w),stride_h_(stride_h),stride_w_(stride_w)

读取并初始化算子中的权重

在读取算子的参数之后, 我们就要开始读取算子中的权重了

在不单独将operator作为一个类之后, 我们将用layer指代算子(operator)和具体计算的层(layer). layer中已经保存了来自对于runtime_operator的参数信息.

如果将深度学习中的所有层分为两类, 那么肯定是"带权重"的层和"不带权重"的层, 我们对于基类layer有如下的定义, 该头文件位于include/layer.hpp中. Forwards方法我们在之前讲到过, 是所有层都必须实现的一个方法, 它定义了一个层的以下几个步骤:

  • 怎么取得当前层的参数
  • 怎么取得当前层的权重(如果有的话)
  • 在得到参数和权重之后, 怎么通过定义好的计算过程来获得相应的结构

对于有参数的层, 我们规定他们必须从layer的一个子类param_layer中进行继承. param_layer中规定了怎么根据runtime_operator 的权重信息来初始化某个层中的权重和偏移量tensor. 我们来看一下param_layer中对参数初始化接口的实现.

void ParamLayer::InitWeightParam(const uint32_t param_count,const uint32_t param_channel,const uint32_t param_height,const uint32_t param_width) {this->weights_ = std::vector<sftensor>(param_count);for (uint32_t i = 0; i < param_count; ++i) {this->weights_.at(i) =std::make_shared<ftensor>(param_channel, param_height, param_width);}
}

我们根据参数的数量, 通道数, 高度和宽度来初始化权重保存的空间 this->weights. 这里的参数数量可以指的是卷积层中的卷积核的数量, 其他信息以此类推. ParamLayer::InitBiasParam同理. 现在是初始化好了空间, 但是具体的参数没有赋值进去, 接下来就是要将具体的参数赋值到参数空间当中.

const auto& weight = attrs.at("weight");const std::vector<int>& weight_shape = weight->shape;if (weight_shape.empty()) {LOG(ERROR) << "The attribute of weight shape is wrong";return ParseParameterAttrStatus::kAttrMissingWeight;}const std::vector<float>& weight_values = weight->get<float>();conv_layer->set_weights(weight_values);return ParseParameterAttrStatus::kParameterAttrParseSuccess;
}

get_weight:


template<class T>
std::vector<T> RuntimeAttribute::get() {/// 检查节点属性中的权重类型CHECK(!weight_data.empty());CHECK(type != RuntimeDataType::kTypeUnknown);std::vector<T> weights;switch (type) {case RuntimeDataType::kTypeFloat32: { /// 加载的数据类型是floatconst bool is_float = std::is_same<T, float>::value;CHECK_EQ(is_float, true);const uint32_t float_size = sizeof(float);CHECK_EQ(weight_data.size() % float_size, 0);for (uint32_t i = 0; i < weight_data.size() / float_size; ++i) {float weight = *((float *) weight_data.data() + i);weights.push_back(weight);}break;}default: {LOG(FATAL) << "Unknown weight data type";}}return weights;
}

先读出来:

  const auto& weight = attrs.at("weight");

再循环放入到weight中

      for (uint32_t i = 0; i < weight_data.size() / float_size; ++i) {float weight = *((float *) weight_data.data() + i);weights.push_back(weight);

set_weight

void ParamLayer::set_weights(const std::vector<float> &weights) {const uint32_t elem_size = weights.size();uint32_t weight_size = 0;const uint32_t batch_size = this->weights_.size();for (uint32_t i = 0; i < batch_size; ++i) {weight_size += this->weights_.at(i)->size();}CHECK_EQ(weight_size, elem_size);CHECK_EQ(elem_size % batch_size, 0);const uint32_t blob_size = elem_size / batch_size; //每个卷积核的参数数量for (uint32_t idx = 0; idx < batch_size; ++idx) {const uint32_t start_offset = idx * blob_size; //起始位置const uint32_t end_offset = start_offset + blob_size; //结束位置const auto &sub_values = std::vector<float>{weights.begin() + start_offset,weights.begin() + end_offset}; //提取参数this->weights_.at(idx)->Fill(sub_values); //赋值过程}
}

首先检查传入的float数组中的数据是否和原有的空间(由InitWeightParam创建的)是等大小的,空间要和权重数据的数量(float值数量)相等.

const uint32_t elem_size = weights.size();uint32_t weight_size = 0;const uint32_t batch_size = this->weights_.size();for (uint32_t i = 0; i < batch_size; ++i) {weight_size += this->weights_.at(i)->size();}CHECK_EQ(weight_size, elem_size);const uint32_t blob_size = elem_size / batch_size;

随后得到参数的个数batch_size(如果在卷积层中就是卷积核的个数), blob_size就是其中一个卷积核的参数数量

for (uint32_t idx = 0; idx < batch_size; ++idx) {const uint32_t start_offset = idx * blob_size;const uint32_t end_offset = start_offset + blob_size;const auto &sub_values = std::vector<float>{weights.begin() + start_offset,weights.begin() + end_offset};this->weights_.at(idx)->Fill(sub_values);}

我们得到当前的卷积核参数的开始和结束位置start_offsetend_offset. 随后将传入参数values中的数据去初始化当前的卷积核(如果是卷积层的情况)sub_values.最后通过this->weights_.at(idx)将当前的一块参数(卷积核)放到对应的空间中.

注册Resnet网络需要的所有算子

Resnet模型是一种很经典的分类模型, 这个了解过深度学习相关知识的同学就不用我多少了. 但是它也属于一个复杂模型, 需要多个算子来构成整个模型, 不仅需要我们在以往课程中讲过的convolution, maxpooling, relu等 , 也需要我们没有讲过的adaptive pooling, linear算子等. 下面是Resnet网络需要的所有算子的列举:

  • adaptive average pooling
  • convolution
  • expression
  • flatten
  • linear
  • max pooling
  • relu

我们不讲解全部算子流程, 只挑选其中的Linear算子, 其余算子的实现在流程下大同小异, 而且最复杂的几个算子我们已经在之前的课程中讲解过了.

Flatten:

初始化接口如下:
ParseParameterAttrStatus FlattenLayer::GetInstance(const std::shared_ptr<RuntimeOperator> &op,std::shared_ptr<Layer> &flatten_layer) {

获取参数param的start_dim和end_dim:

CHECK(op != nullptr) << "Flatten operator is nullptr";const auto &params = op->params;if (params.find("end_dim") == params.end()) {LOG(ERROR) << "Can not find the dimension parameter";return ParseParameterAttrStatus::kParameterMissingDim;}if (params.find("start_dim") == params.end()) {LOG(ERROR) << "Can not find the dimension parameter";return ParseParameterAttrStatus::kParameterMissingDim;}

获取到了之后创建一个layer

  const auto &start_dim =dynamic_cast<RuntimeParameterInt *>(params.at("start_dim"));const auto &end_dim =dynamic_cast<RuntimeParameterInt *>(params.at("end_dim"));if (start_dim == nullptr || end_dim == nullptr) {return ParseParameterAttrStatus::kParameterMissingDim;}flatten_layer =std::make_shared<FlattenLayer>(start_dim->value, end_dim->value);
运行,forward:
InferStatus FlattenLayer::Forward(const std::vector<std::shared_ptr<Tensor<float>>> &inputs,std::vector<std::shared_ptr<Tensor<float>>> &outputs) {

传入input参数和output参数

检查input参数是否为空,并且input和output的size是否相等:

  if (inputs.empty()) {LOG(ERROR) << "The input feature map of flatten layer is empty";return InferStatus::kInferFailedInputEmpty;}if (inputs.size() != outputs.size()) {LOG(ERROR) << "The input and output size is not adapting";return InferStatus::kInferFailedInputOutSizeAdaptingError;}

int start_dim = start_dim_;int end_dim = end_dim_;int total_dims = 4;  // NCHWif (start_dim < 0) {start_dim = total_dims + start_dim;}if (end_dim < 0) {end_dim = total_dims + end_dim;}end_dim -= 1;start_dim -= 1;CHECK(end_dim > start_dim) << "End dim must greater than start dim";CHECK(end_dim <= 2 && start_dim >= 0)<< "end dim must less than two and start dim must greater than zero";
  1. int start_dim = start_dim_;int end_dim = end_dim_;:将构造函数中传入的 start_dim_end_dim_ 分别赋值给本地变量 start_dimend_dim,以便后续操作。

  2. int total_dims = 4;total_dims 被设定为 4,这可能是用于表示输入数据的维度数量(例如 NCHW 表示 4 个维度)。

  3. if (start_dim < 0) { ... }if (end_dim < 0) { ... }:如果传入的 start_dim_end_dim_ 小于 0,那么就将它们调整为从最后一个维度往前数的绝对位置。例如,如果 start_dim_ 为 -1,则 start_dim 会被设置为 4 - 1 = 3,即倒数第一个维度。

  4. end_dim -= 1;start_dim -= 1;:将 end_dimstart_dim 各自减 1,这可能是为了将它们从 0-based 索引改为 1-based 索引。

  5. CHECK(end_dim > start_dim):检查结束维度必须大于起始维度。

  6. CHECK(end_dim <= 2 && start_dim >= 0):检查结束维度必须小于等于 2,且起始维度必须大于等于 0。这可能与特定的设计要求有关,要求 Flatten 操作只能应用于某些特定的维度范围内。

  7. 起始维度一般为channel维度,

const auto &shapes = input->shapes();uint32_t elements_size = 1;for (int s = start_dim; s <= end_dim; ++s) {elements_size *= shapes.at(s);}std::shared_ptr<Tensor<float>> output = outputs.at(i);if (output == nullptr || output->empty()) {output = input->Clone();outputs.at(i) = output;} else {CHECK(outputs.at(i)->shapes() == output->shapes());memcpy(output->data().memptr(), input->data().memptr(), sizeof(float) * input->size());}if (start_dim == 0 && end_dim == 2) {output->ReRawView({elements_size});} else if (start_dim == 1 && end_dim == 2) {uint32_t channels = input->channels();output->ReRawView({channels, elements_size});} else if (start_dim == 0 && end_dim == 1) {uint32_t cols = input->cols();output->ReRawView({elements_size, cols});} else {LOG(FATAL) << "Wrong flatten dim: "<< "start dim: " << start_dim << " end dim: " << end_dim;}
  1. if (output == nullptr || output->empty()) { ... }:这个条件判断语句检查输出张量是否为空或者为空的情况。如果输出张量为空,或者其数据为空(empty() 返回 true),则执行大括号中的代码块。

    a. output = input->Clone();:如果输出张量为空,通过调用输入张量的 Clone() 方法创建一个与输入张量具有相同形状的输出张量。

    b. outputs.at(i) = output;:将新创建的输出张量存储在输出张量容器 outputs 的适当位置。

  2. else { ... }:如果输出张量已经存在(不为空),则执行大括号中的代码块。

    a. CHECK(outputs.at(i)->shapes() == output->shapes());:检查已存在的输出张量(outputs.at(i)) 的形状是否与新创建的输出张量(output) 形状相同。这是为了确保输出张量的形状一致性。

    b. memcpy(output->data().memptr(), input->data().memptr(), sizeof(float) * input->size());:将输入张量的数据复制到输出张量中。memcpy 函数用于内存数据的复制。这里使用 sizeof(float) * input->size() 来确定要复制的字节数,以确保数据正确复制。

  3. 之后对于不同的起始维度进行Review。

SoftMax:

Forward:

InferStatus SoftmaxLayer::Forward(const std::vector<std::shared_ptr<Tensor<float>>> &inputs,std::vector<std::shared_ptr<Tensor<float>>> &outputs) {if (inputs.empty()) {LOG(ERROR) << "The input feature map of softmax layer is empty";return InferStatus::kInferFailedInputEmpty;}if (inputs.size() != outputs.size()) {LOG(ERROR) << "The input and output size is not adapting";return InferStatus::kInferFailedInputOutSizeAdaptingError;}const uint32_t batch_size = inputs.size();
#pragma omp parallel for num_threads(batch_size)for (uint32_t i = 0; i < batch_size; ++i) {const std::shared_ptr<Tensor<float>> &input = inputs.at(i);CHECK(input != nullptr && !input->empty()) << "The input feature map for softmax layer is empty";std::shared_ptr<Tensor<float>> output = outputs.at(i);if (output == nullptr || output->empty()) {output = std::make_shared<Tensor<float>>(input->shapes());outputs.at(i) = output;}CHECK(input->shapes() == output->shapes()) << "The output size of softmax is error";const arma::fcube &input_data = input->data();arma::fcube &output_data = output->data();const float max = input_data.max();const float sum = arma::accu(arma::exp(input_data - max));const float offset = max + logf(sum);output_data = arma::exp(input_data - offset);}return InferStatus::kInferSuccess;
}
  1. for (uint32_t i = 0; i < batch_size; ++i) {:这个循环遍历了一个批次中的每个输入样本(特征图)。

  2. const std::shared_ptr<Tensor<float>> &input = inputs.at(i);:获取当前循环中的输入张量,inputs 可能是一个存储输入张量的容器。

  3. CHECK(input != nullptr && !input->empty()):检查当前输入张量是否存在且非空。

  4. std::shared_ptr<Tensor<float>> output = outputs.at(i);:获取当前循环中的输出张量,outputs 可能是一个存储输出张量的容器。

  5. if (output == nullptr || output->empty()) { ... }:如果当前输出张量为空,或者尚未创建,就创建一个新的输出张量,保持输入输出张量形状一致。

  6. CHECK(input->shapes() == output->shapes()):检查输入张量和输出张量的形状是否相同。

  7. const arma::fcube &input_data = input->data();:获取输入张量的数据,可能是一个三维矩阵(类似于张量)。

  8. arma::fcube &output_data = output->data();:获取输出张量的数据。

  9. const float max = input_data.max();:计算输入数据的最大值。

  10. const float sum = arma::accu(arma::exp(input_data - max));:计算指数化后的输入数据减去最大值的和。

  11. const float offset = max + logf(sum);:计算偏移量,用于数值稳定性。这里对输入数据进行了偏移,以防止指数溢出或下溢。

  12. output_data = arma::exp(input_data - offset);:将输入数据减去偏移量后进行指数化,得到 Softmax 操作的结果。

Linear:

初始化过程:

ParseParameterAttrStatus LinearLayer::GetInstance(const std::shared_ptr<RuntimeOperator>& op,std::shared_ptr<Layer>& linear_layer) {

也是一样的先获取op和linear_layer

const auto& params = op->params;

获取当前层的参数

const auto& weight = attr.at("weight");const auto& bias = attr.at("bias");const auto& shapes = weight->shape;CHECK(shapes.size() == 2)<< "The graph only support two dimension matrix multiply";int32_t out_features = shapes.at(0);int32_t in_features = shapes.at(1);const bool use_bias = use_bias_param->value;linear_layer =std::make_shared<LinearLayer>(in_features, out_features, use_bias);if (use_bias) {linear_layer->set_bias(bias->get<float>());}// load weightslinear_layer->set_weights(weight->get<float>());

这里也是获取attr,然后make_shared一个linear_layer,输入输出维度分别是shapes[1]和shapes[0]。之后将weight参数添加到这个Layer中。

Forward:

参数的各种检查...... ...// 直到这里开始正经内容uint32_t batch = inputs.size();const std::shared_ptr<Tensor<float>>& weight = weights_.front();arma::fmat weight_data(weight->data().memptr(), out_features_, in_features_);

我们先获取刚才通过set_weights进行初始化的权重信息 weight_, 并将它存到一个arma::fmat矩阵类中, 用于后续的矩阵计算.


#pragma omp parallel for num_threads(batch)for (uint32_t i = 0; i < batch; ++i) {// input matmul weightconst std::shared_ptr<Tensor<float>>& input = inputs.at(i);CHECK(input != nullptr && !input->empty())<< "The input feature map of linear layer is empty";const std::vector<uint32_t>& input_shapes = input->shapes();CHECK(input_shapes.size() == 3 && input_shapes.front() == 1);const uint32_t feature_dims = input_shapes.at(1);CHECK(weight_data.n_rows == out_features_);CHECK(weight_data.n_cols == feature_dims && feature_dims == in_features_);//检查矩阵相乘所对应是否相等const uint32_t input_dim = input_shapes.at(2);

arma::fmat col_vec(input->data().memptr(), in_features_, input_dim);std::shared_ptr<Tensor<float>> output = outputs.at(i);if (output == nullptr || output->empty()) {output = std::make_shared<Tensor<float>>(1, out_features_, input_dim);outputs.at(i) = output;}CHECK(output->channels() == 1 && output->rows() == out_features_ &&output->cols() == input_dim);const auto& output_raw_shapes = output->raw_shapes();CHECK(output_raw_shapes.size() == 2);CHECK(output_raw_shapes.at(0) == out_features_ &&output_raw_shapes.at(1) == input_dim);

检查output的所对应的shape是否相等,并且output的行数应该是之前weight和input不参与乘的列。

arma::fmat& result = output->slice(0);result = weight_data * col_vec;if (use_bias_) {CHECK(!this->bias_.empty() && this->bias_.size() == 1);const auto& bias_cube = this->bias_.front();CHECK(!bias_cube->empty());const auto& bias_data = bias_cube->data();CHECK(bias_data.n_slices == 1);CHECK(bias_data.n_rows == out_features_);result += bias_data.slice(0);}

output是我们用来保存结果的地方, 我们将输入col_vec和权重weight_data进行相乘, 得到最后的结果result. result被保存到output的第一个维度中.

如果这个Linear层需要加偏移量的话, 是否需要放在use_bias_中, 它是从runtime_op.param中获得一个参数. 如果需要加偏移量的话, 我们要先获得该层中的偏移量参数this->bias , 再将这个值加到最后的结果上.

Test:


TEST(test_model, resnet_classify_demo) {using namespace kuiper_infer;std::string path = "/home/wang/context/works/KuiperCourse/tmp/dog.jpg";cv::Mat image = cv::imread(path);// 图像预处理sftensor input = PreProcessImage(image);std::vector<sftensor> inputs;inputs.push_back(input);const std::string& param_path = "/home/wang/context/works/KuiperCourse/tmp/resnet18_batch1.pnnx.param";const std::string& weight_path = "/home/wang/context/works/KuiperCourse/tmp/resnet18_batch1.pnnx.bin";RuntimeGraph graph(param_path, weight_path);graph.Build("pnnx_input_0", "pnnx_output_0");// 推理const std::vector<sftensor> outputs = graph.Forward(inputs, true);const uint32_t batch_size = 1;// softmaxstd::vector<sftensor> outputs_softmax(batch_size);SoftmaxLayer softmax_layer;softmax_layer.Forward(outputs, outputs_softmax);assert(outputs_softmax.size() == batch_size);for (int i = 0; i < outputs_softmax.size(); ++i) {const sftensor& output_tensor = outputs_softmax.at(i);assert(output_tensor->size() == 1 * 1000);// 找到类别概率最大的种类float max_prob = -1;int max_index = -1;for (int j = 0; j < output_tensor->size(); ++j) {float prob = output_tensor->index(j);if (max_prob <= prob) {max_prob = prob;max_index = j;}}printf("class with max prob is %f index %d\n", max_prob, max_index);}
}
kuiper_infer::sftensor PreProcessImage(const cv::Mat& image) {using namespace kuiper_infer;assert(!image.empty());// 调整输入大小cv::Mat resize_image;cv::resize(image, resize_image, cv::Size(224, 224));cv::Mat rgb_image;cv::cvtColor(resize_image, rgb_image, cv::COLOR_BGR2RGB);rgb_image.convertTo(rgb_image, CV_32FC3);std::vector<cv::Mat> split_images;cv::split(rgb_image, split_images);uint32_t input_w = 224;uint32_t input_h = 224;uint32_t input_c = 3;sftensor input = std::make_shared<ftensor>(input_c, input_h, input_w);uint32_t index = 0;for (const auto& split_image : split_images) {assert(split_image.total() == input_w * input_h);const cv::Mat& split_image_t = split_image.t();memcpy(input->slice(index).memptr(), split_image_t.data,sizeof(float) * split_image.total());index += 1;}float mean_r = 0.485f;float mean_g = 0.456f;float mean_b = 0.406f;float var_r = 0.229f;float var_g = 0.224f;float var_b = 0.225f;assert(input->channels() == 3);input->data() = input->data() / 255.f;input->slice(0) = (input->slice(0) - mean_r) / var_r;input->slice(1) = (input->slice(1) - mean_g) / var_g;input->slice(2) = (input->slice(2) - mean_b) / var_b;return input;
}

本节课比较总要的就是这个预处理功能了:

cv::Mat resize_image;cv::resize(image, resize_image, cv::Size(224, 224));cv::Mat rgb_image;cv::cvtColor(resize_image, rgb_image, cv::COLOR_BGR2RGB);

1. resize_image是原始图像,其色彩空间可能为BGR(OpenCV默认读取图片为BGR顺序)

2. 使用cvtColor函数将resize_image从BGR色彩空间转换为RGB色彩空间

3. 转换后的结果存储在rgb_image变量中

bgrbgrbgr  ------>  rgbrgbrgb

  rgb_image.convertTo(rgb_image, CV_32FC3);std::vector<cv::Mat> split_images;cv::split(rgb_image, split_images);uint32_t input_w = 224;uint32_t input_h = 224;uint32_t input_c = 3;sftensor input = std::make_shared<ftensor>(input_c, input_h, input_w);

1. rgb_image.convertTo(rgb_image, CV_32FC3)

将OpenCV图像数据类型转换为32位浮点数,每个通道占3个字节(RGB三通道)。

2. cv::split(rgb_image, split_images)

将RGB三个通道剥离出来分别存储到split_images向量中。

3. 定义Torch Tensor输入尺寸

输入通道数input_c为3(RGB),高input_h和宽input_w指定为224。

4. 创建Torch Tensor对象

用指定的通道、高、宽参数创建一个float型Tensor。

5. 将OpenCV图像数据copy到Torch Tensor中

rgbrgbrgb ------>  rrr     ggg      bbb

  for (const auto& split_image : split_images) {assert(split_image.total() == input_w * input_h);const cv::Mat& split_image_t = split_image.t();memcpy(input->slice(index).memptr(), split_image_t.data,sizeof(float) * split_image.total());index += 1;}
  1. 遍历split_images向量中的每个通道图像split_image

  2. 断言split_image的总元素数等于设定的形状input_w * input_h

  3. 将split_image转置为split_image_t,转置是因为OpenCV与Tensor存储数据的顺序不同

  4. 使用memcpy函数把split_image_t的数据复制到input张量对应的slice中

    slice根据index索引取出对应的通道slice

    memptr()获取该slice内存地址

    数据数量为split_image总元素数数量 * 每个元素大小(float占4字节)

  5. 每复制一个通道后,index索引加1,切换到下一个通道slice

这样就实现了rrr   ggg   bbb   --->  rrrgggbbb的转换  也就完成了toTensor的过程

 

对下面图片进行预测:

可以看到最后预测结果为258,萨摩耶,预测概率为0.903387

这里是259因为从1开始

至此,为时一个月的推理课程正式结束,我将在这个专栏的后续内容里引入我自制的layer,比如laplace之类的

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/65272.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ssm+vue“魅力”繁峙宣传网站源码和论文

ssmvue“魅力”繁峙宣传网站源码和论文102 开发工具&#xff1a;idea 数据库mysql5.7 数据库链接工具&#xff1a;navcat,小海豚等 技术&#xff1a;ssm 摘 要 随着科学技术的飞速发展&#xff0c;各行各业都在努力与现代先进技术接轨&#xff0c;通过科技手段提高自身…

C语言:截断+整型提升+算数转换练习

详情关于整型提升、算数转换与截断见文章&#xff1a; 《C语言&#xff1a;整型提升》 《C语言&#xff1a;算数转换》 一、代码一 int main() { char a -1; signed char b -1; unsigned char c -1; printf("%d %d %d", a, b, c); return 0; } 求…

使用安全复制命令scp在Windows系统和Linux系统之间相互传输文件

现在已经有很多远程控制服务器的第三方软件平台&#xff0c;比如FinalShell&#xff0c;MobaXterm等&#xff0c;半可视化界面&#xff0c;使用起来非常方便和友好&#xff0c;两个系统之间传输文件直接拖就行&#xff0c;当然也可以使用命令方式在两个系统之间相互传递。 目录…

C++面试题(丝)-计算机网络部分(1)

目录 1计算机网络 53 简述epoll和select的区别&#xff0c;epoll为什么高效&#xff1f; 54 说说多路IO复用技术有哪些&#xff0c;区别是什么&#xff1f; 55 简述socket中select&#xff0c;epoll的使用场景和区别&#xff0c;epoll水平触发与边缘触发的区别&#xff1f;…

使用 ElasticSearch 作为知识库,存储向量及相似性搜索

一、ElasticSearch 向量存储及相似性搜索 在当今大数据时代&#xff0c;快速有效地搜索和分析海量数据成为了许多企业和组织的重要需求。Elasticsearch 作为一款功能强大的分布式搜索和分析引擎&#xff0c;为我们提供了一种优秀的解决方案。除了传统的文本搜索&#xff0c;El…

【两周学会FPGA】从0到1学习紫光同创FPGA开发|盘古PGL22G开发板学习之数码管动态显示(五)

本原创教程由深圳市小眼睛科技有限公司创作&#xff0c;版权归本公司所有&#xff0c;如需转载&#xff0c;需授权并注明出处 适用于板卡型号&#xff1a; 紫光同创PGL22G开发平台&#xff08;盘古22K&#xff09; 一&#xff1a;盘古22K开发板&#xff08;紫光同创PGL22G开发…

http和https的区别?

什么是 HTTP&#xff1f; HTTP是一种互联网数据传输协议&#xff0c;用于在网络服务器和客户端之间进行数据传输。作为万维网的基础&#xff0c;HTTP协议允许网络浏览器向网络服务器发送请求&#xff0c;服务器则会返回响应。HTTP协议基于文本&#xff0c;因此传输的数据是人类…

41.岛屿数量(第四期模拟笔试)(BFS练习题)

题目&#xff1a; 给定一个 m 行 n 列的二维地图&#xff0c;初始化每个单元格都是海洋&#xff0c;二维地图外也全是海洋。 操作 addLand 会将单元格&#xff08;col, row&#xff09;变为陆地。 定义一系列相连的被海洋包围的陆地为岛屿&#xff0c; 横向相邻或者纵向相连的…

微信小程序请求接口返回的二维码(图片),本地工具和真机测试都能显示,上线之后不显示问题

请求后端接口返回的图片&#xff1a; 页面展示&#xff1a; 代码实现&#xff1a; :show-menu-by-longpress"true" 是长按保存图片 base64Code 是转为base64的地址 <image class"code" :src"base64Code" alt"" :show-menu-by-long…

Three.js实现模型,模型材质可拖拽效果 DragControls

Three.js提供了一个拖拽的API DragControls 用于实现模型材质拖拽效果 DragControls&#xff1a;是一个用于在Three.js中实现拖拽控制的辅助类。它简化了在Three.js中实现拖拽物体的过程。 DragControls的构造函数接受三个参数&#xff1a; objects&#xff1a;一个包含需要…

C# textBox 右键菜单 contextMenuStrip

需求&#xff1a; 想在上图空白处可以右键弹出菜单&#xff0c;该怎么做呢&#xff1f; 1.首先&#xff0c;拖出一个 ContextMenuStrip。 随便放哪里都行&#xff0c;如下: 2.在textBox里关联这个“右键控件”即可&#xff0c;如下&#xff1a; 最终效果如下&#xff1a; 以上…

自定义类型:结构体、枚举、联合

目录 结构体 结构体的基础知识 结构的声明 特殊的声明 结构体的自引用 结构体变量的定义和初始化 结构体内存对齐 修改默认对齐数 结构体传参 位段 什么是位段 位段的内存分配 位段的跨平台问题 位段的应用 枚举 枚举类型的定义 枚举的优点 联合体&#xff08;共…

编写中间件以用于 Express 应用程序

概述 中间件函数能够访问请求对象 (req)、响应对象 (res) 以及应用程序的请求/响应循环中的下一个中间件函数。下一个中间件函数通常由名为 next 的变量来表示。 中间件函数可以执行以下任务&#xff1a; 执行任何代码。对请求和响应对象进行更改。结束请求/响应循环。调用堆…

Java“牵手”1688图片识别商品接口数据,图片地址识别商品接口,图片识别相似商品接口,1688API申请指南

1688商城是一个网上购物平台&#xff0c;售卖各类商品&#xff0c;包括服装、鞋类、家居用品、美妆产品、电子产品等。要通过图片地址识别获取1688商品列表和商品详情页面数据&#xff0c;您可以通过开放平台的接口或者直接访问1688商城的网页来获取商品详情信息。以下是两种常…

iperf 测试网络性能

Iperf 是一个网络性能测试工具。Iperf可以测试最大TCP和UDP带宽性能&#xff0c;具有多种参数和UDP特性&#xff0c;可以根据需要调整&#xff0c;可以报告带宽、延迟抖动和数据包丢失。 官网&#xff1a;iperf.fr https://iperf.fr 支持参数 参数说明-p, --port #Server 端监…

超图嵌入论文阅读1:对偶机制非均匀超网络嵌入

超图嵌入论文阅读1&#xff1a;对偶机制非均匀超网络嵌入 原文&#xff1a;Nonuniform Hyper-Network Embedding with Dual Mechanism ——TOIS&#xff08;一区 CCF-A&#xff09; 背景 超边&#xff1a;每条边可以连接不确定数量的顶点 我们关注超网络的两个属性&#xff1…

排序算法学习

总体概况 参考自&#xff1a;https://github.com/hustcc/JS-Sorting-Algorithm 排序算法是《数据结构与算法》中最基本的算法之一。 排序算法可以分为内部排序和外部排序&#xff0c;内部排序是数据记录在内存中进行排序&#xff0c;而外部排序是因排序的数据很大&#xff0c…

《Web安全基础》04. 文件上传漏洞

web 1&#xff1a;文件上传漏洞2&#xff1a;WAF 绕过2.1&#xff1a;数据溢出2.2&#xff1a;符号变异2.3&#xff1a;数据截断2.4&#xff1a;重复数据 本系列侧重方法论&#xff0c;各工具只是实现目标的载体。 命令与工具只做简单介绍&#xff0c;其使用另见《安全工具录》…

基于RabbitMQ的模拟消息队列之三——硬盘数据管理

文章目录 一、数据库管理1.设计数据库2.添加sqlite依赖3.配置application.properties文件4.创建接口MetaMapper5.创建MetaMapper.xml文件6.数据库操作7.封装数据库操作 二、文件管理1.消息持久化2.消息文件格式3.序列化/反序列化4.创建文件管理类MessageFileManager5.垃圾回收 …

git学习笔记 | 版本管理 - 分支管理

文章目录 git学习笔记Git是什么仓库常见的命令commit 备注规范在文件下设置git忽略文件 .gitignore 版本管理git log | git reflog 查看提交日志/历史版本版本穿梭 git resetgit reset HEAD <file> git checkout -- fileName 丢弃工作区的修改git revertGit恢复之前版本的…