1. 前言
在深度學習算法優化系列十八 | TensorRT Mnist數字識别使用示例 中主要是用TensorRT提供的
NvCaffeParser
來将
Caffe
中的
model
轉換成TensorRT中特有的模型結構。其中
NvCaffeParser
是
TensorRT
封裝好的一個用以解析
Caffe
模型的工具 (高層的API),同樣的還有
NvUffPaser
用于解析TensorFlow的
pb
模型,
NvONNXParse
用于解析Onnx模型。除了這幾個工具之外,TensorRT還提供了C++ API(底層的API)直接在TensorRT中建立模型。這時候TensorRT相當于是一個獨立的深度學習架構,不過這個架構隻負責前向推理(Inference)。
2. 使用C++ API函數部署流程
使用C++ API函數部署網絡主要分成4個步驟,即:
- 建立網絡。
- 給網絡添加輸入。
- 添加各種各樣的層。
- 設定網絡輸出。
其中,第1,2,4步在上節講TensorRT運作Caffe模型的時候已經講過了,隻有第三步是這裡獨有的,因為對于NvCaffeParser工具來說,它隻是把第三步封裝好了。這裡的第三步在上一節的代碼中對應的是
constructNetwork
函數,一起來看下:
//!
//! 簡介:使用caffe解析器建立MNIST網絡并标記輸出層
//!
//! 參數:指向将用MNIST網絡填充的網絡指針
//!
//! 參數:指向引擎生成器的生成器指針
//!
void SampleMNIST::constructNetwork(SampleUniquePtr<nvcaffeparser1::ICaffeParser>& parser, SampleUniquePtr<nvinfer1::INetworkDefinition>& network)
{
const nvcaffeparser1::IBlobNameToTensor* blobNameToTensor = parser->parse(
mParams.prototxtFileName.c_str(),
mParams.weightsFileName.c_str(),
*network,
nvinfer1::DataType::kFLOAT);
//輸出Tensor标記
for (auto& s : mParams.outputTensorNames)
{
network->markOutput(*blobNameToTensor->find(s.c_str()));
}
// 在網絡開頭添加減均值操作
nvinfer1::Dims inputDims = network->getInput(0)->getDimensions();
// 讀取均值檔案的資料
mMeanBlob = SampleUniquePtr<nvcaffeparser1::IBinaryProtoBlob>(parser->parseBinaryProto(mParams.meanFileName.c_str()));
nvinfer1::Weights meanWeights{nvinfer1::DataType::kFLOAT, mMeanBlob->getData(), inputDims.d[1] * inputDims.d[2]};
// 資料的原始分布是[0,256]
// 減去均值之後是[-127,127]
// The preferred method is use scales computed based on a representative data set
// and apply each one individually based on the tensor. The range here is large enough for the
// network, but is chosen for example purposes only.
float maxMean = samplesCommon::getMaxValue(static_cast<const float*>(meanWeights.values), samplesCommon::volume(inputDims));
auto mean = network->addConstant(nvinfer1::Dims3(1, inputDims.d[1], inputDims.d[2]), meanWeights);
mean->getOutput(0)->setDynamicRange(-maxMean, maxMean);
network->getInput(0)->setDynamicRange(-maxMean, maxMean);
// 執行減均值操作
auto meanSub = network->addElementWise(*network->getInput(0), *mean->getOutput(0), ElementWiseOperation::kSUB);
meanSub->getOutput(0)->setDynamicRange(-maxMean, maxMean);
network->getLayer(0)->setInput(0, *meanSub->getOutput(0));
// 執行縮放操作
samplesCommon::setAllTensorScales(network.get(), 127.0f, 127.0f);
// 最後的網絡的輸入就是[-1, 1]
}
複制
可以看到解析Caffe模型用的NvCaffeParser工具中的
parse
函數,這個函數接受網絡模型檔案(
deploy.prototxt
),權重檔案(
net.caffemodel
)路徑參數,然後解析這兩個檔案對應生成TensorRT模型結構。對于NvCaffeParser工具來說,需要三個檔案,即:
*.prototxt
,
*.caffemodel
,标簽檔案(這個主要是将模型産生的數字标号分類,與真實的名稱對應起來)。
下面我們來說一下 使用C++ API函數的部署流程。
2.1 建立網絡
//!
//! 簡介:建立網絡、配置生成器并建立網絡引擎
//!
//! 細節:此函數通過解析caffe模型建立MNIST網絡,并建構用于運作MNIST(mEngine)的引擎
//!
//! 傳回值:如果引擎被建立成功,直接傳回True
//!
bool SampleMNISTAPI::build()
{
//加載權重,*.wts檔案
mWeightMap = loadWeights(locateFile(mParams.weightsFile, mParams.dataDirs));
// 1. Create builder
//建立一個 IBuilder,傳進gLogger參數是為了友善列印資訊。
//builder 這個地方感覺像是使用了建造者模式。
auto builder = SampleUniquePtr<nvinfer1::IBuilder>(nvinfer1::createInferBuilder(gLogger.getTRTLogger()));
if (!builder)
{
return false;
}
//建立一個 network對象,但是這個network對象隻是一個空架子,裡面的屬性還沒有具體的數值。
auto network = SampleUniquePtr<nvinfer1::INetworkDefinition>(builder->createNetwork());
if (!network)
{
return false;
}
//建立一個配置檔案解析對象
auto config = SampleUniquePtr<nvinfer1::IBuilderConfig>(builder->createBuilderConfig());
if (!config)
{
return false;
}
//利用C++ API建立網絡
auto constructed = constructNetwork(builder, network, config);
if (!constructed)
{
return false;
}
assert(network->getNbInputs() == 1);
auto inputDims = network->getInput(0)->getDimensions();
assert(inputDims.nbDims == 3);
assert(network->getNbOutputs() == 1);
auto outputDims = network->getOutput(0)->getDimensions();
assert(outputDims.nbDims == 3);
return true;
}
複制
2.2 為網絡添加輸入
在建立網絡的時候,也即是調用上面代碼段中的
constructNetwork
函數時,首先需要明确網絡的輸入
blob
,代碼如下:
// 為網絡添加輸入
ITensor* data = network->addInput(
mParams.inputTensorNames[0].c_str(), DataType::kFLOAT, Dims3{1, mParams.inputH, mParams.inputW});
複制
其中,
mParams.inputTensorNames[0].c_str()
是輸入
blob
的名字,
DataType::kFLOAT
指的是資料類型,與其相關的還有在
NvInferRuntimeCommon.h
中定義的幾種資料類型:
enum class DataType : int
{
kFLOAT = 0, //!< FP32 format.
kHALF = 1, //!< FP16 format.
kINT8 = 2, //!< quantized INT8 format.
kINT32 = 3 //!< INT32 format.
};
複制
後面的
Dims3{1, mParams.inputH, mParams.inputW}
指的是,
batch_size
為1(已經省略),
channel
為1,輸入
height
和
width
分别為 INPUT_H, INPUT_W的
blob
。
2.3 添加各種層
- 添加一個Scale Layer。
添加一個Scale Layer的代碼如下,
// Create scale layer with default power/shift and specified scale parameter.
const float scaleParam = 0.0125f;
const Weights power{DataType::kFLOAT, nullptr, 0};
const Weights shift{DataType::kFLOAT, nullptr, 0};
const Weights scale{DataType::kFLOAT, &scaleParam, 1};
IScaleLayer* scale_1 = network->addScale(*data, ScaleMode::kUNIFORM, shift, scale, power);
assert(scale_1);
複制
可以看到主要調用了一個
addScale()
函數,後面接受的參數是這一層需要設定的參數,Scale層的作用是為每個輸入資料執行幂運算,公式為:
。
層的類型為
Power
。
可選參數為:
power: 預設為1。
scale: 預設為1。
shift: 預設為0。
複制
其中Weights類的定義如下(在
NvInferRuntime.h
中):
class Weights
{
public:
DataType type; //!< The type of the weights.
const void* values; //!< The weight values, in a contiguous array.
int64_t count; //!< The number of weights in the array.
};
複制
Scale層是沒有訓練參數的,ReLU層,Pooling層都沒有訓練參數。而有訓練參數的如卷積層,全連接配接層,在構造的時候則需要先加載權重檔案。
- 添加一個20個通道的卷積層的。
// Add convolution layer with 20 outputs and a 5x5 filter.
// 添加卷積層
IConvolutionLayer* conv1 = network->addConvolution(*scale_1->getOutput(0), 20, DimsHW{5, 5}, mWeightMap["conv1filter"], mWeightMap["conv1bias"]);
assert(conv1);
//設定步長
conv1->setStride(DimsHW{1, 1});
複制
注意這裡的
mWeightMap
在
bool SampleMNISTAPI::build()
函數裡面已經加載了,權重隻用加載一次。在第一行添加卷積層的函數裡面,
*scale_1->getOutput(0)
用來擷取上一層Scale層的輸出,
20
表示卷積核的個數,
DimsHW{5, 5}
表示卷積核的大小,
weightMap["conv1filter"]和weightMap["conv1bias"]
表示權值系數矩陣。
2.4 解析mnistapi.wts檔案
上面提到在添加各種層之前,已經在
build()
函數裡面加載了
ministapi.wts
權重檔案,這個權重檔案在
F:\TensorRT-6.0.1.5\data\mnist
這個路徑下,是用來存放網絡中各個層間的權值系數的。這裡可以用
Notepad++
打開檢視一下,截圖如下:

用Notepad++打開ministapi.wts檔案
容易發現每一行都是一層的一些參數,比如
conv1bias
就是第一個卷積層的偏置系數,後面的
指的是 kFLOAT 類型,也就是
float 32
;後面的
20
是系數的個數,因為輸出是
20
,是以偏置是
20
個;下面一行是卷積核的系數,因為是
20
個
5 x 5
的卷積核,是以有
20 x 5 x 5=500
個參數。其它層依次類推。這個
wts
檔案是怎麼來的呢?個人認為無論什麼模型,你用相應工具解析解析模型将層名和權值參數鍵值對存到這個檔案中就可以了,由于我暫時不會使用到它,這裡就不深挖了。
2.5 設定網絡輸出
我們必須設定網絡的輸出
blob
,mnist例子中即在網絡的最後添加一個
softmax
,這部分的代碼如下:
// Add softmax layer to determine the probability.
ISoftMaxLayer* prob = network->addSoftMax(*ip2->getOutput(0));
assert(prob);
prob->getOutput(0)->setName(mParams.outputTensorNames[0].c_str());
network->markOutput(*prob->getOutput(0)
複制
2.6 為什麼要使用底層C++/Python API?
對于RNN和不對稱Padding來說,NvCaffeParser是不支援的,隻有 C++ API 和 Python API,才是支援的。除此之外,如果你想使用Darknet訓練出來的檢測模型(
*.weights
),不想模型轉換,那麼你可以直接使用底層的 C++ API,和Python API,因為它需要的就隻是一個層名和權值參數對應的
map
檔案,這使得TensorRT的使用更加靈活。
3. 官方例程
官方例程位于
F:\TensorRT-6.0.1.5\samples\sampleMNISTAPI\sampleMNISTAPI.cpp
,和上節講的例子的差別已經在上面的第二節講清楚了,可以對應着深度學習算法優化系列十八 | TensorRT Mnist數字識别使用示例 代碼解析去了解一下。
4. 後記
這篇推文主要講解了在TensorRT中除了使用Caffe/TensorFlow/ONNX之外,還可以使用底層C++/PYTHON API自己定義網絡結構來部署,看完這個相信對TRT的demo就了解得比較全面了,後面的推文我将從優化方面(低精度推理)來繼續講解TensorRT。
5. 參考
- https://arleyzhang.github.io/articles/fda11be6/
- https://docs.nvidia.com/deeplearning/sdk/tensorrt-api/c_api/index.html
- https://docs.nvidia.com/deeplearning/sdk/tensorrt-api/python_api/index.html
- https://docs.nvidia.com/deeplearning/sdk/tensorrt-developer-guide/index.html
6. 同期文章
- 深度學習算法優化系列十七 | TensorRT介紹,安裝及如何使用?
- 深度學習算法優化系列十八 | TensorRT Mnist數字識别使用示例
歡迎關注GiantPandaCV, 在這裡你将看到獨家的深度學習分享,堅持原創,每天分享我們學習到的新鮮知識。( • ̀ω•́ )✧