Logo ROOT  
Reference Guide
 
Loading...
Searching...
No Matches
ROperator_Pool.hxx
Go to the documentation of this file.
1#ifndef TMVA_SOFIE_ROPERATOR_POOL
2#define TMVA_SOFIE_ROPERATOR_POOL
3
5#include "TMVA/ROperator.hxx"
6#include "TMVA/RModel.hxx"
7
8#include <memory>
9#include <sstream>
10#include <algorithm>
11#include <stdexcept>
12#include <vector>
13#include <cassert>
14
15namespace TMVA {
16namespace Experimental {
17namespace SOFIE {
18
20 // structure that contains Pool attribute
21 std::string auto_pad = "NOTSET";
22 int ceil_mode = 0;
23 int count_include_pad = 0; // not for MaxPool
24 int storage_order = 0; // not for AveragePool
25 std::vector<size_t> dilations; // not for AveragePool
26 std::vector<size_t> kernel_shape;
27 std::vector<size_t> pads;
28 std::vector<size_t> strides;
29};
30
32
33template<typename T>
35{
36
37private:
38
40
44 std::string fAttrAutopad;
45 std::vector<size_t> fAttrDilations;
46 std::vector<size_t> fAttrKernelShape;
47 std::vector<size_t> fAttrPads;
48 std::vector<size_t> fAttrStrides;
49
50 std::string fNX;
51 std::string fNY;
52
53 std::vector<size_t> fShapeX;
54 std::vector<size_t> fShapeY;
55
56 std::string fType;
57
58 size_t fDim; // dimension of the MaxPool
59
60public:
61
62 std::string Name() {
63 if (fPoolMode == AveragePool) return "AveragePool";
64 if (fPoolMode == MaxPool) return "MaxPool";
65 return "Invalid";
66 }
67
69
71 : fPoolMode(mode), fAttrCeilMode(attr.ceil_mode), fAttrCountIncludePad(attr.count_include_pad),
72 fAttrStorageOrder(attr.storage_order), fAttrAutopad(attr.auto_pad),
73 fAttrDilations(attr.dilations), fAttrKernelShape(attr.kernel_shape), fAttrPads(attr.pads), fAttrStrides(attr.strides),
74 fNX(UTILITY::Clean_name(nameX)), fNY(UTILITY::Clean_name(nameY))
75 {
76 if(std::is_same<T, float>::value) {
77 fType = "float";
78 } else {
79 throw
80 std::runtime_error("TMVA SOFIE Encountered unsupported type parsing a Pool operator");
81 }
84 }
85
86 // function returning output shape given input
87 std::vector<std::vector<size_t>> ShapeInference(std::vector<std::vector<size_t>> input) {
88 // shape of pooling input has to be (according to ONNX): NxCxHxW
89 // Where N is batch size, C : input channels, H : input height, W = input width
90 // or it can be [N, C, F1,F2,....FN] . Minimum dimension is 3
91 if (input.size() != 1 ) {
92 throw std::runtime_error("TMVA SOFIE" + Name() + "Op Shape inference need 1 input tensor");
93 }
94 if (input[0].size() < 3) {
95 throw std::runtime_error("TMVA SOFIE" + Name() + "Op Shape inference only accept tensor with at least 3 dimensions");
96 }
97 // support only input tensors with dim = 3,4,5
98 if (input[0].size() < 3 || input[0].size() > 5) {
99 throw std::runtime_error("TMVA SOFIE" + Name() + "Op : tensors with dimension " + std::to_string(input[0].size()) + " are not yet supported");
100 }
101
102 if (input[0].size() -2 != fDim) {
103 throw
104 std::runtime_error("TMVA SOFIE Pool Op Shape inference - invalid inputs ");
105 }
106 // kernel shape
107 size_t k1 = ((fAttrKernelShape.empty())? input[0][2] : fAttrKernelShape[0]);
108 size_t k2 = (fDim > 1) ? ((fAttrKernelShape.empty()) ? input[0][3] : fAttrKernelShape[1]) : 1;
109 size_t k3 = (fDim > 2) ? ((fAttrKernelShape.empty()) ? input[0][4] : fAttrKernelShape[2]) : 1;
110
111
112 size_t i1 = (fDim > 1) ? ((fDim > 2) ? 3 : 2) : 1;
113 size_t i2 = (fDim > 2) ? 4 : 3;
114 size_t i3 = 5;
115
116 if (fAttrDilations.empty()) {
117 fAttrDilations = {1, 1, 1};
118 }
119 fAttrDilations.resize(3);
120 if (fDim < 3) {
121 fAttrDilations.resize(3, 1);
122 }
123 // Shape of the kernel
124 fAttrKernelShape = {k1 + (fAttrDilations[0] - 1) * (k1 - 1),
125 k2 + (fAttrDilations[1] - 1) * (k2 - 1),
126 k3 + (fAttrDilations[2] - 1) * (k3 - 1)};
127
128 if (fAttrStrides.empty()) {
129 fAttrStrides = {1, 1, 1};
130 }
131 if (fDim < 3)
132 fAttrStrides.resize(3, 1);
133
134 if (fAttrAutopad == "NOTSET") {
135 // in auto_pad is NOTSET then fAttrPads should have been set or default zero is used
136 if (fAttrPads.empty()) {
137 fAttrPads = {0, 0, 0, 0, 0, 0};
138 }
139 } else if (fAttrAutopad == "SAME_UPPER" || fAttrAutopad == "SAME_LOWER") {
140 // ONNX SAME padding: total_pad = max(0, (ceil(in/stride)-1)*stride + kernel - in)
141 // SAME_UPPER places extra padding at end, SAME_LOWER at beginning
142 fAttrPads.assign(6, 0);
143 for (size_t d = 0; d < fDim; ++d) {
144 size_t inSize = input[0][d + 2];
145 size_t stride_d = fAttrStrides[d];
146 size_t outSize = (inSize + stride_d - 1) / stride_d;
147 int totalPad = std::max(0, (int)((outSize - 1) * stride_d + fAttrKernelShape[d]) - (int)inSize);
148 if (fAttrAutopad == "SAME_UPPER") {
149 fAttrPads[d] = (size_t)(totalPad / 2);
150 fAttrPads[d + fDim] = (size_t)(totalPad - totalPad / 2);
151 } else {
152 fAttrPads[d] = (size_t)(totalPad - totalPad / 2);
153 fAttrPads[d + fDim] = (size_t)(totalPad / 2);
154 }
155 }
156 } else if (fAttrAutopad != "VALID") {
157 throw
158 std::runtime_error("TMVA SOFIE" + Name() + "Op invalid Autopad value : " + fAttrAutopad);
159 }
160 // to be sure pad is vector of size 6
161 if (fDim < 3) fAttrPads.resize(6, 0);
162
163 size_t input1 = input[0][2];
164 size_t input2 = (fDim > 1) ? input[0][3] : 1;
165 size_t input3 = (fDim > 2) ? input[0][4] : 1;
166
167 // use ceiling division when ceil_mode=1, floor otherwise. With ceil_mode the rounding up can add
168 // a window that starts past the end of the input (i.e. entirely in the right padding or in the
169 // overhang region); ONNX ignores such a window, so clip to the number of valid window starts.
170 auto poolOutDim = [this](size_t in, size_t padBegin, size_t padEnd, size_t kern, size_t stride) -> size_t {
171 size_t n = in + padBegin + padEnd - kern;
172 if (!fAttrCeilMode)
173 return n / stride + 1;
174 return std::min((n + stride - 1) / stride, (in - 1 + padBegin) / stride) + 1;
175 };
176
178
179 size_t batch_size = input[0][0]; // first element in input tensor
180 size_t output_channels = input[0][1]; // first element in output tensor
181
182 std::vector<std::vector<size_t>> ret({{ batch_size, output_channels, output1 }});
183
184 if (fDim == 1)
185 return ret;
186
188 // output is N x C x OH x OW
189 ret[0].push_back(output2);
190 if (fDim == 2)
191 return ret;
192
194
195 // output is N x C x OH x OW x OD
196 ret[0].push_back(output3);
197 return ret;
198 }
199
200 void Initialize(RModel& model) override {
201
202 if (!model.CheckIfTensorAlreadyExist(fNX)) {
203 throw
204 std::runtime_error("TMVA SOFIE Pool op Input Tensor " + fNX + " is not found in model");
205 }
206 fShapeX = model.GetTensorShape(fNX);
207 if (fShapeX.size() < 3 || fShapeX.size() > 5) {
208 std::cout << fNX << " : " << ConvertShapeToString(fShapeX) << std::endl;
209 throw
210 std::runtime_error("TMVA SOFIE Pool Op input data tensor" + fNX + " is not of 3,4 or 5 dimensions");
211 }
212 fDim = fShapeX.size() - 2;
213 // case of GlobalAveragePool. It is a pool case with kernel shape == image shape
216 fAttrKernelShape.resize(3);
218 if (fDim > 1)
220 if (fDim > 2)
222 fAttrAutopad = "VALID";
223 fAttrPads = {0, 0, 0, 0, 0, 0 };
224 assert(fAttrStrides.empty());
225 }
226 // find shape of Y and add it in the list of intermediate tensors
228 model.AddIntermediateTensor(fNY, model.GetTensorType(fNX), fShapeY);
229
230 // need cmath for INFINITY when using MaxPool
231 if (fPoolMode == MaxPool) model.AddNeededStdLib("cmath");
232
233 }
234
235 std::string GenerateInitCode() override {
236 std::stringstream out;
237 return out.str();
238 }
239
240
241 std::string Generate(std::string OpName) override {
242 OpName = "op_" + OpName;
243
244 if (fShapeX.empty() || fShapeY.empty()) {
245 throw std::runtime_error("TMVA SOFIE Pool Op called to Generate without being initialized first");
246 }
247
248 std::stringstream out;
249
250 out << "\n//---- operator " << Name() << " " << OpName << "\n";
251 out << "{\n"; // create a new scope to avoid name clash
252
253 assert(fShapeX[0] == fShapeY[0]);
254 assert(fShapeX[1] == fShapeY[1]);
255 assert(fAttrPads.size() == 6);
256 assert(fAttrKernelShape.size() == 3);
257 // A window must start inside the input: with ceil_mode the loop bound below can otherwise run one
258 // window too far, which ONNX ignores. This mirrors the clipping done in ShapeInference.
259 auto clipToInput = [this](int upper, size_t size) {
260 return (fAttrCeilMode && upper > (int)size) ? (int)size : upper;
261 };
262 // find lower bounds of filtered area
263 int hmin = - fAttrPads[0]; // minimum lower bound value of filter area
264 // use stride instead of 1 when ceil_mode=1, so the loop covers the extra partial window
266 fShapeX[2]);
267 int wmin,wmax,dmin,dmax;
268
269 if(fDim >= 2){
270 wmin = -fAttrPads[1]; // minimum lower bound value of filter area
272 fShapeX[3]);
273 }
274 else{
275 wmin=1;
276 wmax=1;
277 }
278 if(fDim == 3){
279 dmin = -fAttrPads[2]; // minimum lower bound value of filter area
281 fShapeX[4]);
282 }
283 else{
284 dmin=1;
285 dmax=1;
286 }
287 out << SP << "constexpr int hsize = " << fShapeX[2] << ";\n";
288 out << SP << "constexpr int hmin = " << hmin << ";\n";
289 out << SP << "constexpr int hmax = " << hmax << ";\n";
290 out << SP << "constexpr int kh = " << fAttrKernelShape[0] << ";\n";
291 if (fDim > 1) {
292 size_t wsize = fShapeX[3];
293 out << SP << "constexpr int wsize = " << wsize << ";\n";
294 out << SP << "constexpr int wmin = " << wmin << ";\n";
295 out << SP << "constexpr int wmax = " << wmax << ";\n";
296 out << SP << "constexpr int kw = " << fAttrKernelShape[1] << ";\n";
297 if (fDim > 2) {
298 size_t dsize = fShapeX[4];
299 out << SP << "constexpr int dsize = " << dsize << ";\n";
300 out << SP << "constexpr int dwsize = " << dsize*wsize << ";\n"; // hstride
301 out << SP << "constexpr int dmin = " << dmin << ";\n";
302 out << SP << "constexpr int dmax = " << dmax << ";\n";
303 out << SP << "constexpr int kd = " << fAttrKernelShape[2] << ";\n";
304 }
305 }
306
307
308 bool doPadding = false;
309 for ( auto & e : fAttrPads)
310 doPadding |= (e > 0);
311
312 // An AveragePool window can cover fewer cells than the kernel area either because it overlaps the
313 // padding region or because ceil_mode lets the last window overhang the input. In both cases the
314 // divisor has to be computed per window instead of being the constant kernel area.
316 // Number of cells the window starting at "var" covers along one dimension: the kernel extent
317 // clipped to [0, size) when count_include_pad = 0, and to the padded input [-padBegin, size +
318 // padEnd) otherwise. The lower bound needs no clipping in the latter case, since "var" starts at
319 // -padBegin. Note that the cells the window overhangs past the padded input are never counted.
320 auto windowExtent = [this](const std::string &var, const std::string &kern, size_t size, size_t padEnd) {
321 std::string hi = std::to_string(fAttrCountIncludePad ? size + padEnd : size);
322 std::string lo = fAttrCountIncludePad ? var : "(" + var + " > 0 ? " + var + " : 0)";
323 return "((" + var + " + " + kern + " < " + hi + " ? " + var + " + " + kern + " : " + hi + ")"
324 " - " + lo + ")";
325 };
326
327
328 if(fDim==1){
329 // loop on batches and channels
330 out << SP << "size_t outIndex = 0;\n";
331 out << SP << "for (size_t n = 0; n < " << fShapeX[0]*fShapeX[1] << "; n++) {\n";
332 out << SP << SP << "size_t inputOffset = n*" << fShapeX[2] << ";\n";
333 out << SP << SP << "for (int i = hmin; i < hmax; i+=" << fAttrStrides[0] << ") {\n";
334 // loop on elements of filter region to compute maximum
335 if (fPoolMode == MaxPool)
336 out << SP << SP << SP << SP << "float value = -INFINITY;\n";
337 else if (fPoolMode == AveragePool) {
338 out << SP << SP << SP << SP << "float value = 0;\n";
339 if (dynamicDivisor)
340 out << SP << SP << SP << SP << "const int nsum = "
341 << windowExtent("i", "kh", fShapeX[2], fAttrPads[fDim]) << ";\n";
342 else // every window is full, so the divisor is the kernel area
343 out << SP << SP << SP << SP << "constexpr int nsum = kh;\n";
344 }
345 // loop on rows of filtered region
346 out << SP << SP << SP << SP << "for (int l = i; l < i + kh; l++) {\n";
347 out << SP << SP << SP << SP << SP << "if (l < 0 || l >= hsize) continue;\n";
348 out << SP << SP << SP << SP << SP << SP << "int index = inputOffset + l;\n";
349 if (fPoolMode == MaxPool) {
350 out << SP << SP << SP << SP << SP << SP << "auto xval = tensor_" << fNX << "[index];\n";
351 out << SP << SP << SP << SP << SP << SP << "if (xval > value) value = xval;\n";
352 }
353 else if (fPoolMode == AveragePool) {
354 // compute sum of values
355 out << SP << SP << SP << SP << SP << SP << "value += tensor_" << fNX << "[index];\n";
356 }
357 out << SP << SP << SP << SP << SP << "}\n"; // end loop on region elements
358 if (fPoolMode == AveragePool) {
359 // compute average
360 out << SP << SP << SP << SP << "value /= float(nsum);\n";
361 }
362
363 out << SP << SP << SP << SP << "tensor_" << fNY << "[outIndex++] = value;\n";
364
365 out << SP << SP << "}\n"; // end loop on i (image rows)
366 out << SP << "}\n"; // end loop on c*b
367 }
368 else if(fDim==2){
369 // loop on batches and channels
370 out << SP << "size_t outIndex = 0;\n";
371 out << SP << "for (size_t n = 0; n < " << fShapeX[0]*fShapeX[1] << "; n++) {\n";
372 out << SP << SP << "size_t inputOffset = n*" << fShapeX[2]*fShapeX[3] << ";\n";
373 out << SP << SP << "for (int i = hmin; i < hmax; i+=" << fAttrStrides[0] << ") {\n";
374 out << SP << SP << SP << "for (int j = wmin; j < wmax; j+=" << fAttrStrides[1] << ") {\n";
375 // loop on elements of filter region to compute maximum
376 if (fPoolMode == MaxPool)
377 out << SP << SP << SP << SP << "float value = -INFINITY;\n";
378 else if (fPoolMode == AveragePool) {
379 out << SP << SP << SP << SP << "float value = 0;\n";
380 if (dynamicDivisor)
381 out << SP << SP << SP << SP << "const int nsum = "
382 << windowExtent("i", "kh", fShapeX[2], fAttrPads[fDim]) << " * "
383 << windowExtent("j", "kw", fShapeX[3], fAttrPads[fDim + 1]) << ";\n";
384 else // every window is full, so the divisor is the kernel area
385 out << SP << SP << SP << SP << "constexpr int nsum = kw*kh;\n";
386 }
387 // loop on rows of filtered region
388 out << SP << SP << SP << SP << "for (int l = i; l < i + kh; l++) {\n";
389 out << SP << SP << SP << SP << SP << "if (l < 0 || l >= hsize) continue;\n";
390 // loop on columns of filtered region
391 out << SP << SP << SP << SP << SP << "for (int m = j; m < j + kw; m++) {\n";
392 out << SP << SP << SP << SP << SP << SP << "if (m < 0 || m >= wsize) continue;\n";
393 out << SP << SP << SP << SP << SP << SP << SP << "int index = inputOffset + l*wsize + m;\n";
394 if (fPoolMode == MaxPool) {
395 out << SP << SP << SP << SP << SP << SP << SP << "auto xval = tensor_" << fNX << "[index];\n";
396 out << SP << SP << SP << SP << SP << SP << SP << "if (xval > value) value = xval;\n";
397 }
398 else if (fPoolMode == AveragePool) {
399 // compute sum of values
400 out << SP << SP << SP << SP << SP << SP << SP << "value += tensor_" << fNX << "[index];\n";
401 }
402 out << SP << SP << SP << SP << SP << SP << "}\n";
403 out << SP << SP << SP << SP << SP << "}\n"; // end loop on region elements
404 if (fPoolMode == AveragePool) {
405 // compute average
406 out << SP << SP << SP << SP << "value /= float(nsum);\n";
407 }
408 out << SP << SP << SP << SP << "tensor_" << fNY << "[outIndex++] = value;\n";
409 out << SP << SP << SP << "}\n"; // end loop on j (columns of image)
410 out << SP << SP << "}\n"; // end loop on i (image rows)
411 out << SP << "}\n"; // end loop on c*b
412 }
413 else if(fDim==3){
414 // loop on batches and channels
415 out << SP << "size_t outIndex = 0;\n";
416 out << SP << "for (size_t n = 0; n < " << fShapeX[0]*fShapeX[1] << "; n++) {\n";
417 out << SP << SP << "size_t inputOffset = n*" << fShapeX[2]*fShapeX[3]*fShapeX[4] << ";\n";
418 out << SP << SP << "for (int i = hmin; i < hmax; i+=" << fAttrStrides[0] << ") {\n";
419 out << SP << SP << SP << "for (int j = wmin; j < wmax; j+=" << fAttrStrides[1] << ") {\n";
420 out << SP << SP << SP << SP << "for (int k = dmin; k < dmax; k+=" << fAttrStrides[2] << ") {\n";
421 // loop on elements of filter region to compute maximum
422 if (fPoolMode == MaxPool)
423 out << SP << SP << SP << SP << "float value = -INFINITY;\n";
424 else if (fPoolMode == AveragePool) {
425 out << SP << SP << SP << SP << "float value = 0;\n";
426 if (dynamicDivisor)
427 out << SP << SP << SP << SP << "const int nsum = "
428 << windowExtent("i", "kh", fShapeX[2], fAttrPads[fDim]) << " * "
429 << windowExtent("j", "kw", fShapeX[3], fAttrPads[fDim + 1]) << " * "
430 << windowExtent("k", "kd", fShapeX[4], fAttrPads[fDim + 2]) << ";\n";
431 else // every window is full, so the divisor is the kernel area
432 out << SP << SP << SP << SP << "constexpr int nsum = kw*kh*kd;\n";
433 }
434 // loop on rows of filtered region
435 out << SP << SP << SP << SP << "for (int l = i; l < i + kh; l++) {\n";
436 out << SP << SP << SP << SP << SP << "if (l < 0 || l >= hsize) continue;\n";
437 // loop on columns of filtered region
438 out << SP << SP << SP << SP << SP << "for (int m = j; m < j + kw; m++) {\n";
439 out << SP << SP << SP << SP << SP << SP << "if (m < 0 || m >= wsize) continue;\n";
440 // loop on layers of filtered region
441 out << SP << SP << SP << SP << SP << SP << "for (int p = k; p < k + kd; p++) {\n";
442 out << SP << SP << SP << SP << SP << SP << SP << "if (p < 0 || p >= dsize) continue;\n";
443 out << SP << SP << SP << SP << SP << SP << SP << SP << "int index = inputOffset + l*dwsize + m*dsize + p;\n";
444
445 if (fPoolMode == MaxPool) {
446 out << SP << SP << SP << SP << SP << SP << SP << SP << "auto xval = tensor_" << fNX << "[index];\n";
447 out << SP << SP << SP << SP << SP << SP << SP << SP << "if (xval > value) value = xval;\n";
448 }
449 else if (fPoolMode == AveragePool) {
450 // compute sum of values
451 out << SP << SP << SP << SP << SP << SP << SP << SP << "value += tensor_" << fNX << "[index];\n";
452 }
453 out << SP << SP << SP << SP << SP << SP << "}\n";
454 out << SP << SP << SP << SP << SP << "}\n";
455 out << SP << SP << SP << SP << "}\n"; // end loop on region elements
456 if (fPoolMode == AveragePool) {
457 // compute average
458 out << SP << SP << SP << SP << "value /= float(nsum);\n";
459 }
460
461 out << SP << SP << SP << SP << "tensor_" << fNY << "[outIndex++] = value;\n";
462 out << SP << SP << SP << SP << "}\n" ; // end loop on k (layers of image)
463 out << SP << SP << SP << "}\n"; // end loop on j (columns of image)
464 out << SP << SP << "}\n"; // end loop on i (image rows)
465 out << SP << "}\n"; // end loop on c*b
466 }
467 // end scope
468 out << SP << "}\n";
469
470
471 return out.str();
472 }
473};
474
475} // namespace SOFIE
476} // namespace Experimental
477} // namespace TMVA
478
479
480#endif
#define d(i)
Definition RSha256.hxx:102
#define e(i)
Definition RSha256.hxx:103
size_t size(const MatrixT &matrix)
retrieve the size of a square matrix
ROOT::Detail::TRangeCast< T, true > TRangeDynCast
TRangeDynCast is an adapter class that allows the typed iteration through a TCollection.
Option_t Option_t TPoint TPoint const char GetTextMagnitude GetFillStyle GetLineColor GetLineWidth GetMarkerStyle GetTextAlign GetTextColor GetTextSize void input
Option_t Option_t TPoint TPoint const char GetTextMagnitude GetFillStyle GetLineColor GetLineWidth GetMarkerStyle GetTextAlign GetTextColor GetTextSize void char Point_t Rectangle_t hmin
Option_t Option_t TPoint TPoint const char GetTextMagnitude GetFillStyle GetLineColor GetLineWidth GetMarkerStyle GetTextAlign GetTextColor GetTextSize void char Point_t Rectangle_t hmax
Option_t Option_t TPoint TPoint const char GetTextMagnitude GetFillStyle GetLineColor GetLineWidth GetMarkerStyle GetTextAlign GetTextColor GetTextSize void char Point_t Rectangle_t wmin
Option_t Option_t TPoint TPoint const char GetTextMagnitude GetFillStyle GetLineColor GetLineWidth GetMarkerStyle GetTextAlign GetTextColor GetTextSize void char Point_t Rectangle_t WindowAttributes_t attr
Option_t Option_t TPoint TPoint const char mode
Option_t Option_t TPoint TPoint const char GetTextMagnitude GetFillStyle GetLineColor GetLineWidth GetMarkerStyle GetTextAlign GetTextColor GetTextSize void char Point_t Rectangle_t wmax
#define hi
void Initialize(RModel &model) override
std::string Generate(std::string OpName) override
std::vector< std::vector< size_t > > ShapeInference(std::vector< std::vector< size_t > > input)
ROperator_Pool(PoolOpMode mode, RAttributes_Pool attr, std::string nameX, std::string nameY)
std::vector< std::string_view > fInputTensorNames
Definition ROperator.hxx:44
const std::string SP
space used to correctly indent the generated C++ code
Definition ROperator.hxx:40
std::vector< std::string_view > fOutputTensorNames
Definition ROperator.hxx:45
const Int_t n
Definition legend1.C:16
std::string ConvertShapeToString(const std::vector< size_t > &shape)
create variable transformations