5. RbRuntime使用指南



  • RbRuntime使用指南

    通过本教程,用户可以了解或掌握:

    1. 什么是RbRuntime
    2. RbRuntime API的调用方法以及runner的构成

    Note:
    本教程以虚拟机为基本环境,Linux用户需要进入docker,并在docker中执行本教程中的所有步骤。docker的进入方法如下:

    sudo docker start Rainbuilder
    sudo docker exec -ti Rainbuilder bash# 1. 了解RainBuilder Runtime
    

    在《CAISA架构与Rainbuilder编译工具介绍》中,我们大致介绍了RbRuntime的使用流程。

    RainBuilder Runtime,即RbRuntime,是一个在嵌入式FPGA系统上进行卷积神经网络计算处理的库,RbRuntime用于运行由RbCompiler生成的SG,根据SG描述部署计算过程并执行计算,通过权值文件初始化计算节点所依赖的数据。

    RbRuntime可以根据SG描述所提供的每个节点的信息,分配不同的计算函数、数据类型、计算设备给不同的节点。

    RbRuntime也提供了简洁的调用接口,仅需几行代码即可整合RbRuntime的计算功能到目标应用中。

    RbRuntime的使用流程可以大概分为以下几步。我们会在本教程中详细介绍RbRuntime的相应API和对应使用方法。

    2. RbRuntime使用步骤

    为了进一步方便用户理解,我们以手写体数字识别图像识别算法Lenet为例介绍基于RbRuntime的开发流程。
    以下教程假设用户已经使用compiler完成了数据的解析和导出,并得到了如下模型文件结构:

    /Models
      |-lenet_sg.pbtxt
      |-lenet_sg.h5
      |-lenet_sg.json
      |-label_to_class.json
      /images
        |-test.jpg
      /lenet_float_coeff
        /float_little
          |-config.json
          |-lenet_conv1_conv2d_bias_data.bin
          |-lenet_conv1_conv2d_weights_data.bin
          |-lenet_conv2_conv2d_bias_data.bin
          |-lenet_conv2_conv2d_weights_data.bin
          |-lenet_fc3_matmul_bias_data.bin
          |-lenet_fc3_matmul_weights_data.bin
          |-lenet_fc4_matmul_bias_data.bin
          |-lenet_fc4_matmul_weights_data.bin
    

    接下来,我们将详细讲解如何为模型创建Runner,并在Runner中实现模型输入的预处理和输出的后处理。

    2.1 了解预处理与后处理

    由于算法模型可以接受的数据格式和类型是固定的,在执行模型之前,需要预先将数据处理成可被网络识别的格式和类型。在运行一个图像分类或目标检测算法前,我们需要确保输入图像的大小符合SG(*.pbtxt)的要求,因此我们需要对图像进行预处理。

    当SG在RbRuntime中结束运行后,我们会得到一个输出张量(tensor),为了使结果更加直观,对于目标检测算法,通常需要在图片中将目标物用矩形框框选出来,对于分类算法,则是返回分类标签。因此在后处理中,程序代码对输出张量进行计算,确定矩形框的位置和大小并且在目标物上画框,或对输出张量进行标签映射。

    2.1.1 预处理和后处理在Runtime中的定义方法

    在SGDataMap中的函数对象如下:

    std::function<int(SGDataMap *)> Preprocess = nullptr;
    std::function<int(SGDataMap *)> Postprocess = nullptr;
    

    设置这两个函数的对应接口如下:

      void SetupPreprocessor(std::function<int(SGDataMap *)> pre = nullptr) {
        Preprocess = pre;
      }
      void SetupPostprocessor(std::function<int(SGDataMap *)> post = nullptr) {
        Postprocess = post;
      }
    

    这里我们以图像中的目标检测问题为例,介绍一下具体的使用方法。

    首先是预处理函数的定义,实现读入图像文件、图像处理、输入网络的操作。

      auto pInputDataMap = new SGDataMap();
      pInputDataMap->SetupPreprocessor([=](SGDataMap *pInputDataMap) {
    
        // Get the shape of input image
        auto image_shape = GetImageShape(node_def);
        auto image_height = image_shape[1];
        auto image_width = image_shape[2];
        auto num_channels = image_shape[3];
    
        // Load image into memory
        // Apply corresponding pre-processing functions
        auto data = LoadImage(image_file, image_height, image_width, func);
    
        SGDataMap::ValT val(image_shape, data);
    
        // Pass data vector into network input tensor 
        pInputDataMap->set(node_def.name(), val);
        return 0;
      });
    

    其次是后处理函数,实现检测算法的后处理和结果的绘制操作。

    auto post_processor = [=](SGDataMap *pOutputDataMap) {
            // Get the path to save output image
            auto output_image_path = GetResultImagePath(image_path);
            // Apply detection algorithm post-processing functions
            processor->PostProcess(pOutputDataMap);
            // Print the results in terminal
            processor->Print(pOutputDataMap, 5);
            // Read image into memory
            cv::Mat image = cv::imread(image_path);
            // Plot results on image
            processor->Draw(pOutputDataMap, image);
            // Save image to hard drive
            processor->Write(pOutputDataMap, image, output_image_path);
            return 0;
          };
    input_data_map->SetupPostprocessor(post_processor);
    

    2.2 创建SG对象

    用户需要使用如下代码通过pbtxt文件创建一个SG对象

    SGDef sg_def = LoadSGDefFromFile("/Models/lenet_sg.pbtxt");
    SGBuilder bulder(sg_def);
    SG* sg = builder.Build();
    

    2.3 加载模型参数

    根据如下代码将参数加载到常量数据图中

    SGDataMap* const_data = LoadSGDataMapFromDir("/Models/lenet_float_coeff/float_little");
    

    2.4 创建runner

    根据如下代码创建具有所需配置的runner

    int num_threads = 4; //this is the number of parallel you need.
    SGRunnerConfig cfg;
    cfg.num_parallel = num_threads;
    cfg.is_sim = true;
    
    SGRunner* runner = new SGRunner(cfg);
    runner->Build(sg, const_data);
    

    2.5 确定输入节点

    根据如下代码在SG中找到输入节点

    for (auto node in sg_def.node())
      if (node.op() == "Input") input_node = node;
    

    2.6 创建输入数据图

    根据如下代码从输入图片中获得输入数据。

    //declare a global map for collecting result
    map<SGDataMap*, PredResT> mapResult;
    
    //load image and convert to grayscale
    cv::Mat img = cv::imread("/Models/images/test.jpg");
    cv::Mat gray_img;
    cv::cvtColor(img, gray_img, CV_BGR2GRAY);
    //source image shape
    int height = gray_img.rows, width = gray_img.cols, channels = 1;
    
    for (int h = 0; h < height; h++)
      for (int w = 0; w < width; w++) {
        raw_input.push_back((float)gray_img.at<uchar>(h, w));
      }
    InputDataMapContext context;
    context.strInputName = node_def.name();
    context.shape = GetImageShape(node_def);
    auto preprocessor = [=](float* raw, int height, int width, int channels) {
      return normalize_raw_data(raw, height, width, channels);
    };
    auto postprocessor = [=](SGDataMap* pOutputDataMap) {
      collect_predict_results(pOutputDataMap);
    };
    SGDataMap* input_data_map = create_input_datamap(context, raw_input.data(), height, width, channels, preprocessor, postprocessor);
    

    normalize_raw_datacollect_predict_results是预处理和后处理的实现

    using PredResT = std::vector<std::pair<int, float>>;
    float* normalize_raw_data(float* raw, int height, int width, int channels) {
      int total = height * width * channels;
      for (int ii = 0; ii < total; ii++)
        raw[ii] = (raw[ii] - 128.0) / 128.0;
      return raw;
    }
    void collect_predict_results(SGDataMap* pOutputDataMap) {
      // collect logits from the output
      auto logits = GetLogitsFromOutput(pOutputDataMap);
      // softmax result
      auto preds = SoftmaxCrossEntropy(logits);
      // sort
      SortPredResInPlace(preds);
      mapResult[pOutputDataMap] = preds;
    }
    static SGDataMap::PtrT GetLogitsFromOutput(SGDataMap *out_data) {
      // Sanity check
      CHECK_NOTNULL(out_data);
      CHECK_EQ(out_data->keys().size(), 1)
          << "There should be a single key in the output data map, got: "
          << out_data->keys().size();
    
      auto key = out_data->keys()[0];  // single output
      auto val = out_data->get(key);
      auto logits = val.second;
    
      return logits;
    }
    
    PredResT SoftmaxCrossEntropy(SGDataMap::PtrT logits) {
      // initialise preds
      PredResT preds;
      for (int i = 0; i < logits->size(); i++)  // pack data from logits
        preds.push_back(std::pair<int, float>(i, logits->at(i)));
    
      int N = preds.size();
      float sum = 0;
    
      // manually do softmax
      for (int i = 0; i < N; i++) preds[i].second = std::exp(preds[i].second);
      for (auto pred : preds) sum += pred.second;
      for (int i = 0; i < N; i++) preds[i].second = preds[i].second / sum;
    
      return preds;
    }
    void SortPredResInPlace(PredResT &preds) {
      sort(preds.begin(), preds.end(),
            [=](std::pair<int, float> a, std::pair<int, float> b) {
              return a.second > b.second;
            });
    }  
    

    2.7 运行SG

    至此用户可以以同步或异步的方式开始运行SG,并得到后处理执行后的结果

    SGDataMap* output_data_map = nullptr;
    if (async) {
      std::future<SGDataMap *> future_output = runner->RunAsync(sg, const_data, input_data_map);
      runner->Wait(); //wait until the task finished.
      output_data_map = future_output.get(); //get the output data map, need it to get the postprocessor result
    } else {
      output_data_map = runner->Run(sg, const_data, input_data_map);
    }
    pred_result = mapResult[output_data_map];
    

    2.8 得到预测结果

    在模型运行结束后,会得到一个输出张量,此步骤中实现的是输出张量向分类标签的映射。

    因为分类算法中我们得到的是一个一维的结果向量,向量中的每个值代表对应结果的相似度大小,取其中最大值,并将其映射为对应的标签,这就是分类算法最后输出的结果

    根据如下代码将label_to_class.json加载到json对象中

    std::ifstream is("/Models/label_to_class.json");
    json label_to_class;
    is >> label_to_class;
    

    具体操作如下

    /*! Simply print the top-K predictions to the terminal */
    void PrintTopK(PredResT preds, int num_top_k, int labels_offset) {
      // These predictions should be sorted already
      std::cout << std::endl;
      std::cout << "Top-K (" << num_top_k << ") Prediction Results:" << std::endl;
      std::cout << "----------------------------------------" << std::endl;
      for (int i = 0; i < num_top_k; i++) {
        auto pred = preds[i];
        auto label = pred.first - labels_offset;
        std::cout << "Top-" << i + 1 << ": " << label << " "
                  << this->label_to_class[std::to_string(label)] << " "
                  << pred.second << std::endl;
      }
      std::cout << "----------------------------------------" << std::endl;
      std::cout << std::endl;
    }
    PrintTopK(pred_result, 5, 0);
    

    3. Runtime实例与编译方法

    Hands_on文件夹/deployment/Runtime路径下,我们提供了两个Runtime实例,以*_runner.cc文件为核心,分别存放在ImageClassificationImageObjectDetection文件夹中。

    同时,用户需要编写对应的CMakeList.txt以生成相应的makefile进行编译。

    之后,执行以下操作以编译成可执行文件:

    mkdir build
    cd build
    cmake .. -DCMAKE_BUILD_TYPE=Release -DCROSSCOMPILE=ON
    make -j 2
    

登录后回复