"tests/test_fileio.py" did not exist on "d4c472cc6dc2f5319e42e8fea63f6282ad56b5dd"
Tutorial_Cpp.md 7.75 KB
Newer Older
liucong's avatar
liucong committed
1
2
3
4
5
6
7
8
9
10
# 分类器



本示例通过ResNet50模型说明如何使用MIGraphX C++ API进行分类模型的推理,包括如何预处理、推理并获取推理结果。



## 模型简介

liucong's avatar
liucong committed
11
本示例使用了经典的ResNet50模型,onnx文件在Resource/Models/文件夹下,模型结构可以通过netron (https://netron.app/) 查看,该模型的输入shape为[1,3,224,224] ,数据排布为NCHW。
liucong's avatar
liucong committed
12
13
14
15
16
17
18



## 预处理

在将数据输入到模型之前,需要对图像做如下预处理操作:

liucong's avatar
liucong committed
19
20
21
- 图像格式转换,BGR转换为RGB

- 调整图像大小,并在中心窗口位置裁剪出224x224大小的图像
liucong's avatar
liucong committed
22

liucong's avatar
liucong committed
23
24
25
- normalize操作,对图像减均值再除方差

- 转换数据排布为NCHW
liucong's avatar
liucong committed
26

liucong's avatar
liucong committed
27
  
liucong's avatar
liucong committed
28

liucong's avatar
liucong committed
29
本示例代码主要采用了OpenCV实现了预处理操作:
liucong's avatar
liucong committed
30
31
32
33
34
35
36

```c++
ErrorCode Classifier::Classify(const std::vector<cv::Mat> &srcImages,std::vector<std::vector<ResultOfPrediction>> &predictions)
{
	...
	
    // 预处理
liucong's avatar
liucong committed
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
    std::vector<cv::Mat> image;
    for(int i =0;i<srcImages.size();++i)
    {
        //BGR转换为RGB
        cv::Mat imgRGB;
        cv::cvtColor(srcImages[i], imgRGB, cv::COLOR_BGR2RGB);

        // 调整大小,保持长宽比
        cv::Mat shrink;
        float ratio = (float)256 / min(imgRGB.cols, imgRGB.rows);
        if(imgRGB.rows > imgRGB.cols)
        {
            cv::resize(imgRGB, shrink, cv::Size(256, int(ratio * imgRGB.rows)), 0, 0);
        }
        else
        {
            cv::resize(imgRGB, shrink, cv::Size(int(ratio * imgRGB.cols), 256), 0, 0);
        }

        // 裁剪中心窗口
        int start_x = shrink.cols/2 - 224/2;
        int start_y = shrink.rows/2 - 224/2;
        cv::Rect rect(start_x, start_y, 224, 224);
        cv::Mat images = shrink(rect);
        image.push_back(images);
    }

    // normalize并转换为NCHW
liucong's avatar
liucong committed
65
    cv::Mat inputBlob;
liucong's avatar
liucong committed
66
67
68
69
70
    Image2BlobParams image2BlobParams;
    image2BlobParams.scalefactor=cv::Scalar(1/58.395, 1/57.12, 1/57.375);
    image2BlobParams.mean=cv::Scalar(123.675, 116.28, 103.53);
    image2BlobParams.swapRB=false;
    blobFromImagesWithParams(image,inputBlob,image2BlobParams);
liucong's avatar
liucong committed
71
72
73
74
75
                    
     ...
}
```

liucong's avatar
liucong committed
76
blobFromImagesWithParams()函数支持多个输入图像,首先对输入图像各通道减对应的均值,然后乘以各通道对应的缩放系数,最后转换为NCHW,最终将转换好的数据保存到inputBlob中,然后就可以输入到模型中执行推理了。
liucong's avatar
liucong committed
77

liucong's avatar
liucong committed
78
## 量化
liucong's avatar
liucong committed
79

liucong's avatar
liucong committed
80
81
82
83
84
85
86
87
88
该示例工程提供了fp16和int8两种量化方法,可以在Resource/Configuration.xml文件中设置是否需要量化:

```c++
<opencv_storage>
	<!--分类器-->
	<Classifier>
		...
		<UseInt8>0</UseInt8>      // 设置为1时,开启INT8量化
		<UseFP16>0</UseFP16>      // 设置为1时,开启FP16量化
liucong's avatar
liucong committed
89
        ...
liucong's avatar
liucong committed
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
	</Classifier>
</opencv_storage>
```

### FP16量化

使用FP16模式只需要在编译前调用migraphx::quantize_fp16() 即可。

```c++
migraphx::quantize_fp16(net);
```

### INT8量化

使用INT8模式需要提供量化校准数据,MIGraphX采用线性量化算法,通过校准数据计算量化参数并生成量化模型。为了保证量化精度,一般使用测试集或验证集中的数据作为校准数据。

```c++
// 创建量化校准数据,建议使用测试集中的多张典型图像
cv::Mat srcImage=cv::imread("../Resource/Images/ImageNet_test.jpg",1);
std::vector<cv::Mat> srcImages;
for(int i=0;i<inputShape.lens()[0];++i)
{
	srcImages.push_back(srcImage);
}

// 数据预处理
...

// 创建量化数据
std::unordered_map<std::string, migraphx::argument> inputData;
inputData[inputName]= migraphx::argument{inputShape, (float*)inputBlob.data};
std::vector<std::unordered_map<std::string, migraphx::argument>> calibrationData = {inputData};

// INT8量化
migraphx::quantize_int8(net, gpuTarget, calibrationData);
```
liucong's avatar
liucong committed
126
127
128
129
130
131
132
133
134
135
136
137

## 推理

完成预处理后,就可以执行推理了:

```c++
ErrorCode Classifier::Classify(const std::vector<cv::Mat> &srcImages,std::vector<std::vector<ResultOfPrediction>> &predictions)
{
	...
	
	// 预处理
	
liucong's avatar
liucong committed
138
	// 当offload为true时,不需要内存拷贝
liucong's avatar
liucong committed
139
140
141
142
143
144
145
146
147
    if(useoffloadcopy)
    {
        std::unordered_map<std::string, migraphx::argument> inputData;
        inputData[inputName]= migraphx::argument{inputShape, (float*)inputBlob.data};

        // 推理
        std::vector<std::string> outputNames={"resnetv24_dense0_fwd"};    // 设置返回的输出节点
        std::vector<migraphx::argument> results = net.eval(inputData,outputNames);

liucong's avatar
liucong committed
148
149
150
151
152
153
154
155
        // 获取输出节点的属性
        migraphx::argument result  = results[0];                 // 获取第一个输出节点的数据
        migraphx::shape outputShapes=result.get_shape();         // 输出节点的shape
        std::vector<std::size_t> outputSize=outputShapes.lens(); // 每一维大小,维度顺序为(N,C,H,W) 
        int numberOfOutput=outputShapes.elements();              // 输出节点元素的个数
        float *logits=(float *)result.data();                    // 输出节点数据指针

        ...
liucong's avatar
liucong committed
156
    }
liucong's avatar
liucong committed
157
    else   // 当offload为false时,需要内存拷贝
liucong's avatar
liucong committed
158
159
    {

liucong's avatar
liucong committed
160
        migraphx::argument inputData= migraphx::argument{inputShape, (float*)inputBlob.data};
liucong's avatar
liucong committed
161

liucong's avatar
liucong committed
162
163
        // 拷贝到device输入内存
        hipMemcpy(inputBuffer_Device, inputData.data(), inputShape.bytes(), hipMemcpyHostToDevice);
liucong's avatar
liucong committed
164

liucong's avatar
liucong committed
165
        // 推理
liucong's avatar
liucong committed
166
167
168
169
170
171
172
173
        std::vector<std::string> outputNames={"resnetv24_dense0_fwd"};     // 设置返回的输出节点
        std::vector<migraphx::argument> results = net.eval(programParameters,outputNames);

        // 获取输出节点的属性
        migraphx::argument result   = results[0];                  // 获取第一个输出节点的数据
        migraphx::shape outputShapes=result.get_shape();           // 输出节点的shape
        std::vector<std::size_t> outputSize=outputShapes.lens();   // 每一维大小,维度顺序为(N,C,H,W) 
        int numberOfOutput=outputShapes.elements();                // 输出节点元素的个数
liucong's avatar
liucong committed
174

liucong's avatar
liucong committed
175
176
177
178
179
180
181
182
        // 将device输出数据拷贝到分配好的host输出内存
        hipMemcpy(outputBuffer_Host, outputBuffer_Device, outputShapes.bytes(), hipMemcpyDeviceToHost);  // 直接使用事先分配好的输出内存拷贝
        ...

        // 释放
        hipFree(inputBuffer_Device);
        hipFree(outputBuffer_Device);
        free(outputBuffer_Host);
liucong's avatar
liucong committed
183
    }
liucong's avatar
liucong committed
184

liucong's avatar
liucong committed
185
186
187
188
189
190
   	
   	...
   	
}
```

liucong's avatar
liucong committed
191
- 输入数据根据是否需要数据拷贝分为两种:offload=true、offload=false。当offload为true时,执行if分支,不需要进行内存拷贝,inputData表示MIGraphX的输入数据,inputData是一个映射关系,每个输入节点名都会对应一个输入数据,如果有多个输入,则需要为每个输入节点名创建数据,inputName表示输入节点名,migraphx::argument{inputShape, (float*)inputBlob.data}表示该节点名对应的数据,这里是通过前面预处理的数据inputBlob来创建的,第一个参数表示数据的shape,第二个参数表示数据指针。当offload为false时,执行else分支,需要进行内存拷贝,为输入节点拷贝到device端,经过推理后,再将输出节点数据拷贝到host端。
liucong's avatar
liucong committed
192
193
- net.eval(inputData)返回模型的推理结果,由于这里只有一个输出节点,所以std::vector中只有一个数据,results[0]表示第一个输出节点,这里对应resnetv24_dense0_fwd节点,获取输出数据。

liucong's avatar
liucong committed
194
另外,如果想要指定输出节点,可以在eval()方法中通过提供outputNames参数来实现:
liucong's avatar
liucong committed
195

liucong's avatar
liucong committed
196
```c++
liucong's avatar
liucong committed
197
198
...
// 推理
liucong's avatar
liucong committed
199
std::vector<std::string> outputNames = {"resnetv24_dense0_fwd"};
liucong's avatar
liucong committed
200
201
202
203
204
std::vector<migraphx::argument> results = net.eval(inputData, outputNames);
...
```

- 如果没有指定outputName参数,则默认输出所有输出节点,此时输出节点的顺序与ONNX中输出节点顺序保持一致,可以通过netron查看ONNX文件的输出节点的顺序。