TensorRT(3)-C++ API使用:mnist手写体识别
本節將介紹如何使用tensorRT C++ API 進行網絡模型創建。
1 使用C++ API 進行 tensorRT 模型創建
還是通過 tensorRT官方給的一個例程來學習。
還是mnist手寫體識別的例子。上一節主要是用 tensorRT提供的NvCaffeParser來將 Caffe中的model 轉換成tensorRT中特有的模型結構。NvCaffeParser是tensorRT封裝好的一個用以解析Caffe模型的工具 (較頂層的API),同樣的還有 NvUffPaser是用于解析TensorFlow的工具。
除了以上兩個封裝好的工具之外,還可以使用tensorRT提供的C++ API(底層的API)來直接在tensorRT中創建模型。這時 tensorRT 相當于是一個獨立的深度學習框架了,這個框架和其他框架(Caffe, TensorFlow,MXNet等)一樣都具備搭建網絡模型的能力(只有前向計算沒有反向傳播)。
不同之處在于:
- 這個框架不能用于訓練,模型的權值參數要人為給定;
- 可以針對設定網絡模型(自己使用API創建網絡模型)或給定模型(使用NvCaffeParser或NvUffPaser導入其他深度學習框架訓練好的模型)做一系列優化,以加快推理速度(inference)
使用C++ API函數部署網絡主要分為四個步驟:
- 創建網絡;
- 為網絡添加輸入;
- 添加各種各樣的層;
- 設定網絡輸出;
以上,第1,2,4步驟在使用 NvCaffeParser 時也是有的。只有第3步是本節所講的方法中特有的,其實對于NvCaffeParser 工具來說,他只是把 第 3步封裝起來了而已。
如下,對比一下 NvCaffeParser 的使用方法,下面的代碼中只列出了關鍵部分的代碼。完整代碼請看上一節。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
//build phase INetworkDefinition* network = builder->createNetwork(); //1. 創建網絡 CaffeParser* parser = createCaffeParser(); std::unordered_map<std::string, infer1::Tensor> blobNameToTensor; const IBlobNameToTensor* blobNameToTensor = //3. 添加各種各樣的層 parser->parse(locateFile(deployFile).c_str(), //NvCaffeParser 工具 locateFile(modelFile).c_str(), //把添加層的內容封裝起來了 *network, DataType::kFLOAT); for (auto& s : outputs) network->markOutput(*blobNameToTensor->find(s.c_str())); // 4. 設定網絡輸出 ICudaEngine* engine = builder->buildCudaEngine(*network); //創建engine //省略一些內容……………… //execution phase IExecutionContext *context = engine->createExecutionContext(); //創建 context int inputIndex = engine->getBindingIndex(INPUT_BLOB_NAME), outputIndex = engine->getBindingIndex(OUTPUT_BLOB_NAME); //2.為網絡添加輸入 //省略一些內容……………… context.enqueue(batchSize, buffers, stream, nullptr); //調用cuda核計算 cudaStreamSynchronize(stream); //同步cuda 流 |
上述四個步驟對應部分已在注釋標出。可見 NvCaffeParser 工具中最主要的是 parse 函數,這個函數接受網絡模型文件(deploy.prototxt)、權值文件(net.caffemodel)為參數,這兩個文件是caffe的模型定義文件和訓練參數文件。parse 函數會解析這兩個文件并對應生成 tensorRT的模型結構。
對于NvCaffeParser 工具來說,是需要三個文件的,分別是:
- 網絡模型文件(比如,caffe的deploy.prototxt)
- 訓練好的權值文件(比如,caffe的net.caffemodel)
- 標簽文件(這個主要是將模型產生的數字標號分類,與真實的名稱對應起來)
以下分步驟說明四個步驟:
1.1 創建網絡
先創建一個tensorRT的network,這個network 現在只是個空架子,比較簡單:
|
1 |
INetworkDefinition* network = builder->createNetwork(); |
1.2 為網絡添加輸入
所有的網絡都需要明確輸入是哪個blob,因為這是數據傳送的入口。
|
1 2 |
// Create input of shape { 1, 1, 28, 28 } with name referenced by INPUT_BLOB_NAME auto data = network->addInput(INPUT_BLOB_NAME, dt, DimsCHW{ 1, INPUT_H, INPUT_W}); |
-
INPUT_BLOB_NAME 是為輸入 blob起的名字;
-
dt是指數據類型,有kFLOAT(float 32), kHALF(float 16), kINT8(int 8)等類型;
1
2
3
4
5
6
7
8
//位于 NvInfer.h 文件
enum class DataType : int
{
kFLOAT = 0, //!< FP32 format.
kHALF = 1, //!< FP16 format.
kINT8 = 2, //!< INT8 format.
kINT32 = 3 //!< INT32 format. 這個是TensorRT新增的
};
-
DimsCHW{ 1, INPUT_H, INPUT_W} 是指,batch為1(省略),channel 為1,輸入height 和width分別為 INPUT_H, INPUT_W的blob;
1.3 添加各種各樣的層
- 以下示例是添加一個 scale layer
|
1 2 3 4 5 6 |
// Create a scale layer with default power/shift and specified scale parameter. float scale_param = 0.0125f; Weights power{DataType::kFLOAT, nullptr, 0}; Weights shift{DataType::kFLOAT, nullptr, 0}; Weights scale{DataType::kFLOAT, &scale_param, 1}; auto scale_1 = network->addScale(*data, ScaleMode::kUNIFORM, shift, scale, power); |
主要就是 addScale 函數,后面接受的參數是這一層需要設置的參數。
scale 層的作用是對每個輸入數據進行冪運算
f(x)= (shift + scale * x) ^ power
層類型:Power
可選參數:
power: 默認為1
scale: 默認為1
shift: 默認為0
就是一種激活層。
Weights 類的定義如下:
|
1 2 3 4 5 6 7 8 9 |
//NvInfer.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 }; |
以上是不包含訓練參數的層,還有 Relu層,Pooling層等。
包含訓練參數的層,比如卷積層,全連接層,要先加載權值文件。
- 以下示例是添加一個卷積層
|
1 2 3 4 5 6 7 8 9 |
// Add convolution layer with 20 outputs and a 5x5 filter. // 加載權值文件,加載一次即可 std::map<std::string, Weights> weightMap = loadWeights(locateFile("mnistapi.wts")); //添加卷積層 IConvolutionLayer* conv1 = network->addConvolution(*scale_1->getOutput(0), 20, DimsHW{5, 5}, weightMap["conv1filter"], weightMap["conv1bias"]); //設置步長 conv1->setStride(DimsHW{1, 1}); |
第6行添加卷積層:
|
1 |
IConvolutionLayer* conv1 = network->addConvolution(*scale_1->getOutput(0), 20, DimsHW{5, 5}, weightMap["conv1filter"], weightMap["conv1bias"]); |
上面的 mnistapi.wts 文件,是用于存放網絡中各個層間的權值系數的,該文件位于?/usr/src/tensorrt/data?文件夾中。
可以用notepad打開看一下,如下:
可見每一行都是一層的一些參數,比如 conv1bias 是指第一個卷積層的偏置系數,后面的0 指的是 kFLOAT 類型,也就是 float 32;后面的20是系數的個數,因為輸出是20,所以偏置是20個;下面一行是 卷積核的系數,因為是20個 5×5的卷積核,所以有 20×5×5=500個參數。其它層依次類推。
這個文件是例程中直接給的,感覺像是 用caffe等工具訓練后,將weights系數從caffemodel 中提取出來的。直接讀取caffemodel應該也是可以的,稍微改一下接口:解析caffemodel文件然后將層名和權值參數鍵值對存到一個map中,網上大概找了一下,比如?這個?,解析后的caffemodel如下所示:
conv1 最下面有一個 blobs結構,這個是weights系數;每一個包含參數的層(卷積,全連接等;激活層,池化層沒有參數)都有一個 blobs結構。只需將這些參數提取出來,保存到一個map中。
除此之外也可以添加很多其他的層,比如反卷積層,池化層,全連接層等,具體參考?英偉達官方API?。
添加層的過程就相當于 NvCaffeParser 工具中 parse 函數解析 deploy.prototxt 文件的過程。
1.4 設定網絡輸出
網絡必須知道哪一個blob是輸出的。
如下代碼,在網絡的最后添加了一個softmax層,并將這個層命名為 OUTPUT_BLOB_NAME,之后指定為輸出層。
|
1 2 3 4 |
// Add a softmax layer to determine the probability. auto prob = network->addSoftMax(*ip2->getOutput(0)); prob->getOutput(0)->setName(OUTPUT_BLOB_NAME); network->markOutput(*prob->getOutput(0)); |
那直接使用底層API有什么好處呢?看下表
| CNNs | yes | yes | yes | yes |
| RNNs | yes | yes | no | no |
| INT8 Calibration | yes | yes | NA | NA |
| Asymmetric Padding | yes | yes | no | no |
上表列出了 tensorRT 的不同特點與 API 對應的情況。可以看到對于 RNN,int8校準(float 32 轉為 int8),不對稱 padding 來說,NvCaffeParser是不支持的,只有 C++ API 和 Python API,才是支持的。
所以說如果是針對很復雜的網絡結構使用tensorRT,還是直接使用底層的 C++ API,和Python API 較好。底層C++ API還可以解析像 darknet 這樣的網絡模型,因為它需要的就只是一個層名和權值參數對應的map文件。
2 官方例程
例程位于?/usr/src/tensorrt/samples/sampleMNISTAPI
2.1 build phase
|
1 2 3 4 5 |
//這個是main函數中的代碼片段 // create a model using the API directly and serialize it to a stream IHostMemory *modelStream{nullptr}; //調用APIToModel函數,手動創建網絡模型 APIToModel(1, &modelStream); |
APIToModel函數:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
void APIToModel(unsigned int maxBatchSize, IHostMemory** modelStream) { // Create builder IBuilder* builder = createInferBuilder(gLogger); //下面這個createMNISTEngine函數才是真正手動創建網絡的過程 // Create model to populate the network, then set the outputs and create an engine ICudaEngine* engine = createMNISTEngine(maxBatchSize, builder, DataType::kFLOAT); assert(engine != nullptr); // Serialize the engine (*modelStream) = engine->serialize(); // Close everything down engine->destroy(); builder->destroy(); } |
createMNISTEngine函數如下:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
// Creat the engine using only the API and not any parser. ICudaEngine* createMNISTEngine(unsigned int maxBatchSize, IBuilder* builder, DataType dt) { INetworkDefinition* network = builder->createNetwork(); // Create input tensor of shape { 1, 1, 28, 28 } with name INPUT_BLOB_NAME ITensor* data = network->addInput(INPUT_BLOB_NAME, dt, Dims3{1, INPUT_H, INPUT_W}); assert(data); // 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); // Add convolution layer with 20 outputs and a 5x5 filter. // 加載權值文件,加載一次即可 std::map<std::string, Weights> weightMap = loadWeights(locateFile("mnistapi.wts")); // 添加卷積層 IConvolutionLayer* conv1 = network->addConvolution(*scale_1->getOutput(0), 20, DimsHW{5, 5}, weightMap["conv1filter"], weightMap["conv1bias"]); assert(conv1); //設置步長 conv1->setStride(DimsHW{1, 1}); // Add max pooling layer with stride of 2x2 and kernel size of 2x2. IPoolingLayer* pool1 = network->addPooling(*conv1->getOutput(0), PoolingType::kMAX, DimsHW{2, 2}); assert(pool1); pool1->setStride(DimsHW{2, 2}); // Add second convolution layer with 50 outputs and a 5x5 filter. IConvolutionLayer* conv2 = network->addConvolution(*pool1->getOutput(0), 50, DimsHW{5, 5}, weightMap["conv2filter"], weightMap["conv2bias"]); assert(conv2); conv2->setStride(DimsHW{1, 1}); // Add second max pooling layer with stride of 2x2 and kernel size of 2x3> IPoolingLayer* pool2 = network->addPooling(*conv2->getOutput(0), PoolingType::kMAX, DimsHW{2, 2}); assert(pool2); pool2->setStride(DimsHW{2, 2}); // Add fully connected layer with 500 outputs. IFullyConnectedLayer* ip1 = network->addFullyConnected(*pool2->getOutput(0), 500, weightMap["ip1filter"], weightMap["ip1bias"]); assert(ip1); // Add activation layer using the ReLU algorithm. IActivationLayer* relu1 = network->addActivation(*ip1->getOutput(0), ActivationType::kRELU); assert(relu1); // Add second fully connected layer with 20 outputs. IFullyConnectedLayer* ip2 = network->addFullyConnected(*relu1->getOutput(0), OUTPUT_SIZE, weightMap["ip2filter"], weightMap["ip2bias"]); assert(ip2); // Add softmax layer to determine the probability. ISoftMaxLayer* prob = network->addSoftMax(*ip2->getOutput(0)); assert(prob); prob->getOutput(0)->setName(OUTPUT_BLOB_NAME); network->markOutput(*prob->getOutput(0)); // Build engine builder->setMaxBatchSize(maxBatchSize); builder->setMaxWorkspaceSize(1 << 20); ICudaEngine* engine = builder->buildCudaEngine(*network); // Don't need the network any more network->destroy(); // Release host memory for (auto& mem : weightMap) { free((void*) (mem.second.values)); } return engine; } |
可見里面包含了很多 add* 函數,都是用于添加各種各樣的層的。可參考英偉達官方API?。
2.2 deploy phase
deploy階段基本與之前的無異。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
int main(int argc, char** argv) { ……………… ……………… // Deserialize engine we serialized earlier // 創建運行時環境 IRuntime對象,傳入 gLogger 用于打印信息 IRuntime* runtime = createInferRuntime(gLogger); assert(runtime != nullptr); ICudaEngine* engine = runtime->deserializeCudaEngine(trtModelStream->data(), trtModelStream->size(), nullptr); assert(engine != nullptr); trtModelStream->destroy(); //創建上下文環境,主要用于inference 函數中啟動cuda核 IExecutionContext* context = engine->createExecutionContext(); assert(context != nullptr); //2.deploy 階段:調用 inference 函數,進行推理過程 // Run inference on input data float prob[OUTPUT_SIZE]; doInference(*context, data, prob, 1); ……………… ……………… } |
doInference函數如下:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
void doInference(IExecutionContext& context, float* input, float* output, int batchSize) { const ICudaEngine& engine = context.getEngine(); // Pointers to input and output device buffers to pass to engine. // Engine requires exactly IEngine::getNbBindings() number of buffers. assert(engine.getNbBindings() == 2); void* buffers[2]; // In order to bind the buffers, we need to know the names of the input and output tensors. // Note that indices are guaranteed to be less than IEngine::getNbBindings() const int inputIndex = engine.getBindingIndex(INPUT_BLOB_NAME); const int outputIndex = engine.getBindingIndex(OUTPUT_BLOB_NAME); // Create GPU buffers on device CHECK(cudaMalloc(&buffers[inputIndex], batchSize * INPUT_H * INPUT_W * sizeof(float))); CHECK(cudaMalloc(&buffers[outputIndex], batchSize * OUTPUT_SIZE * sizeof(float))); // Create stream cudaStream_t stream; CHECK(cudaStreamCreate(&stream)); // DMA input batch data to device, infer on the batch asynchronously, and DMA output back to host CHECK(cudaMemcpyAsync(buffers[inputIndex], input, batchSize * INPUT_H * INPUT_W * sizeof(float), cudaMemcpyHostToDevice, stream)); context.enqueue(batchSize, buffers, stream, nullptr); CHECK(cudaMemcpyAsync(output, buffers[outputIndex], batchSize * OUTPUT_SIZE * sizeof(float), cudaMemcpyDeviceToHost, stream)); cudaStreamSynchronize(stream); // Release stream and buffers cudaStreamDestroy(stream); CHECK(cudaFree(buffers[inputIndex])); CHECK(cudaFree(buffers[outputIndex])); } |
參考資料
總結
以上是生活随笔為你收集整理的TensorRT(3)-C++ API使用:mnist手写体识别的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: TensorRT(2)-基本使用:mni
- 下一篇: UVC (USB Video Class