generate_convolution_ref.py 18.3 KB
Newer Older
1
#!/usr/bin/env python
2
# ******************************************************************************
3 4
# Copyright 2017-2018 Intel Corporation
#
5 6 7 8
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
9
#     http://www.apache.org/licenses/LICENSE-2.0
10 11 12 13 14
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
15
# limitations under the License.
16
# ******************************************************************************
17 18 19 20

import sys
import numpy as np
import math
21
import random
22 23
from operator import mul

24 25 26
# Generates an array of random floating point literals of the given length, from a fixed seed.
def random_array_float_literals(length,seed=8086):
    literals = []
27

28
    random.seed(seed)
29

30 31 32 33 34
    for i in range(0,length):
        literal_n = random.randint(0,99)
        literal_sign = random.randint(0,1)
        literal_str = ('-' if literal_sign==1 else '') + '.' + ('%02d' % literal_n)
        literals.append(literal_str)
35

36
    return literals
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

# Elementwise addition on tuples.
def tuple_plus(t1,t2):
    assert(len(t1) == len(t2))

    res = ()

    for (x,y) in zip(list(t1),list(t2)):
        res = res + (x+y,)

    return res

# Elementwise multiplication on tuples.
def tuple_times(t1,t2):
    assert(len(t1) == len(t2))

    res = ()

    for (x,y) in zip(list(t1),list(t2)):
        res = res + (x*y,)

    return res

#
# Convolution reference
#
#    Arguments:
64
#    data_batch       : [N ][Ci][D1]...[Dn], n > 0
65 66
#    filter           : [Co][Ci][W1]...[Wn]
#    move_strides     = (s1,...,sn)
67
#    filter_dilation  = (l1,...,ln)
68 69
#    below_pads       = (p1,...,pn)
#    above_pads       = (q1,...,qn)
70
#    data_dilation    = (t1,...,tn)
71 72 73 74
#
#    Returns:
#    output_batch     : [N ][Co][D'1]...[D'n]
#
75
# Where the D's are computed according to TensorFlow-style "valid" convolution rules, but *after* padding.
76 77
# See https://www.tensorflow.org/api_docs/python/tf/nn/convolution.
#
78 79 80 81 82 83 84 85
def convolution_ref(data_batch, filter, move_strides, filter_dilation, below_pads, above_pads, data_dilation):
    assert(len(data_batch.shape) == len(filter.shape))
    assert(len(data_batch.shape) > 2)
    assert(len(data_batch.shape) <= 6)
    assert(data_batch.shape[1] == filter.shape[1])
    assert(len(move_strides) == len(data_batch.shape) - 2)
    assert(len(filter_dilation) == len(data_batch.shape) - 2)
    assert(len(data_dilation) == len(data_batch.shape) - 2)
86 87

    # dilate the input batch
88 89 90 91 92 93 94 95 96 97 98 99 100 101
    new_item_shape = (np.array(data_batch.shape[2:]) - 1) * data_dilation + 1
    new_data_batch_shape = list(np.array(data_batch.shape[:2])) + list(new_item_shape)
    new_data_batch = np.zeros(new_data_batch_shape)

    for n in range(0, new_data_batch_shape[0]) :
        for c in range(0, new_data_batch_shape[1]) :
            if new_data_batch.ndim == 3:
                new_data_batch[n, c, 0::data_dilation[0]] = data_batch[n][c]
            elif new_data_batch.ndim == 4:
                new_data_batch[n, c, 0::data_dilation[0], 0::data_dilation[1]] = data_batch[n][c]
            elif new_data_batch.ndim == 5:
                new_data_batch[n, c, 0::data_dilation[0], 0::data_dilation[1], 0::data_dilation[2]] = data_batch[n][c]
            elif new_data_batch.ndim == 6:
                new_data_batch[n, c, 0::data_dilation[0], 0::data_dilation[1], 0::data_dilation[2], 0::data_dilation[3]] = data_batch[n][c]
102 103 104
            else:
                assert(False)

105
    data_batch = new_data_batch
106

107
    # Pad the input batch wherever the pads are positive.
108 109 110
    below_pads_pos = (0,0) + tuple(np.clip(below_pads,0,None))  # Have to add values for the spatial and channel dims.
    above_pads_pos = (0,0) + tuple(np.clip(above_pads,0,None))  # Have to add values for the spatial and channel dims.
    data_batch = np.pad(data_batch, zip(below_pads_pos,above_pads_pos), mode='constant', constant_values=0)
111 112 113 114 115

    # Slice the input batch wherever the pads are negative.
    slice_bottoms = (0,0) + tuple (-np.clip(below_pads,None,0))
    slice_tops = (0,0) + tuple (np.clip(above_pads,None,0))
    slices = map(lambda p: slice(p[0],p[1] if p[1] < 0 else None),zip(slice_bottoms,slice_tops))
116
    data_batch = data_batch[slices]
117

118 119 120 121 122
    item_count = data_batch.shape[0]               # N
    ci_count = data_batch.shape[1]                 # Ci
    co_count = filter.shape[0]                     # Co
    input_item_shape = list(data_batch.shape[2:])  # D1, ..., Dn
    window_virtual_shape = list(filter.shape[2:])  # W1, ..., Wn
123 124 125

    # This is not used in computation but we will calculate it for a check to make sure the window fits.
    window_physical_shape = []
126
    for (d_in,d_virt,dil) in zip(input_item_shape,window_virtual_shape,filter_dilation):
127
        d_phys = (d_virt - 1) * dil + 1
128
        assert(d_phys <= input_item_shape)
129 130
        window_physical_shape.append(d_phys)

131 132
    output_item_shape = []  # D'1,...,D'n
    for (d_in,d_win,dil,mov) in zip (input_item_shape,window_virtual_shape,filter_dilation,move_strides):
133 134
        d_out = int(math.ceil((float(d_in) - (float(d_win) - 1.0) * float(dil))/float(mov))) # Formula is taken from TF's definition for VALID convolution.
        assert(d_out > 0)
135
        output_item_shape.append(d_out)
136

137
    output_shape = [item_count,co_count]+output_item_shape # N,Co,D'1,...,D'n
138 139 140 141 142
    output_batch = np.zeros(output_shape)

    # Walk over the output batch space.
    output_it = np.nditer(output_batch, flags=['multi_index'])
    while not output_it.finished:
143
        # Break up the output coordinate to figure out where we are in terms of batch index, output channel, and spatial position.
144
        output_index = output_it.multi_index
145
        item, co, output_pos = output_index[0], output_index[1], output_index[2:]
146 147 148 149 150 151 152 153 154

        # Walk over the filter for the current output channel.
        filter_it = np.nditer(filter[co], flags=['multi_index'])
        while not filter_it.finished:
            # Break up the filter coordinate to figure out where we are in terms of input channel and filter shape position.
            filter_index = filter_it.multi_index
            ci, filter_pos = filter_index[0], filter_index[1:]

            # Build up the coordinate within the space N,Ci,D1,...,Dn that we need to read from in the input batch.
155
            input_index = (item,ci) + (tuple_plus(tuple_times(output_pos,move_strides),tuple_times(filter_pos,filter_dilation)))
156 157

            # Add to the sum-of-products.
158
            output_batch[output_index] = output_batch[output_index] + filter[(co,) + filter_index] * data_batch[input_index]
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176

            filter_it.iternext()

        output_it.iternext()

    return output_batch

def shape_str(shape):
    result = ''
    first = True
    for d in shape:
        if first:
            result = ('%d' % d)
            first = False
        else:
            result = result + (',%d' % d)
    return result

177 178 179 180 181 182 183 184 185
def scalar_str(x):
    result = ('%.1000g' % x)
    # This next part is a bit stupid.
    if "." not in result and "e" not in result:
        result = result + ".0f"
    else:
        result = result + "f"
    return result

186 187 188 189 190
def data_str(data):
    result = ''
    first = True
    for x in np.nditer(data):
        if first:
191
            result = scalar_str(x)
192 193
            first = False
        else:
194
            result = result + ',' + scalar_str(x)
195 196 197
    return result

def emit_test(t,f):
198
    test_name, input_batch_shape, filters_shape, move_strides, filter_dilation, below_pads, above_pads, data_dilation, bprop = t
199 200 201 202 203 204 205 206

    input_batch_literals = random_array_float_literals(reduce(mul,input_batch_shape))
    filters_literals = random_array_float_literals(reduce(mul,filters_shape))

    input_batch_array = np.array(map(lambda s: np.float32(s),input_batch_literals))
    input_batch_array.shape = input_batch_shape
    filters_array = np.array(map(lambda s: np.float32(s),filters_literals))
    filters_array.shape = filters_shape
207 208 209

    print ("Generating convolution test '%s'..." % test_name)

210
    output_batch_data = convolution_ref(input_batch_array,filters_array,move_strides,filter_dilation,below_pads,above_pads,data_dilation)
211 212 213 214

    template = '''
TEST (${BACKEND_NAME}, %s)
{
215 216 217
    Shape shape_a{%s};
    Shape shape_b{%s};
    Shape shape_r{%s};
218 219 220
    auto make_graph = [shape_a, shape_b] {
        auto A = make_shared<op::Parameter>(element::f32, shape_a);
        auto B = make_shared<op::Parameter>(element::f32, shape_b);
adstraw's avatar
adstraw committed
221 222 223 224 225
        return make_shared<Function>(make_shared<op::Convolution>(A, B,
                                                                  Strides{%s},        // move_strides
                                                                  Strides{%s},        // filter_dilation
                                                                  CoordinateDiff{%s}, // below_pads
                                                                  CoordinateDiff{%s}, // above_pads
226
                                                                  Strides{%s}),       // data_dilation
227
                                     op::ParameterVector{A, B});
adstraw's avatar
adstraw committed
228
    };
229 230

    auto manager = runtime::Manager::get("${BACKEND_NAME}");
adstraw's avatar
adstraw committed
231
    auto external = manager->compile(make_graph());
232 233 234 235
    auto backend = manager->allocate_backend();
    auto cf = backend->make_call_frame(external);

    // Create some tensors for input/output
236 237 238 239 240
    auto a = backend->make_primary_tensor_view(element::f32, shape_a);
    copy_data(a, vector<float>{%s});
    auto b = backend->make_primary_tensor_view(element::f32, shape_b);
    copy_data(b, vector<float>{%s});
    auto result = backend->make_primary_tensor_view(element::f32, shape_r);
241

242
    vector<float> expected_result{%s};
243 244

    cf->call({a, b}, {result});
245
    EXPECT_TRUE(all_close<float>(vector<float>{expected_result}, read_vector<float>(result)));
adstraw's avatar
adstraw committed
246 247
    // only test backprop for certain cases as it takes significant compute resources
    if(%s) {
248
        EXPECT_TRUE(autodiff_numeric_compare<float>(manager, backend, make_graph, {a, b}, .01f, .01f));
adstraw's avatar
adstraw committed
249
    }
250 251 252
}
'''
    f.write (template % (test_name,
253 254
                         shape_str(input_batch_shape),
                         shape_str(filters_shape),
255 256
                         shape_str(output_batch_data.shape),
                         shape_str(move_strides),
257
                         shape_str(filter_dilation),
258 259
                         shape_str(below_pads),
                         shape_str(above_pads),
260
                         shape_str(data_dilation),
261 262
                         ",".join(map(lambda s: s + "f",input_batch_literals)),
                         ",".join(map(lambda s: s + "f",filters_literals)),
adstraw's avatar
adstraw committed
263 264
                         data_str(output_batch_data),
                         bprop));
265

266
#                                                                              filter                                      data
267
#         test name                                batch shape   filts shape   stride    dilation  below-pads  above-pads  dilation   bprop?
268
tests = [
269 270 271 272 273 274 275
         ("convolution_2d_1item",                  (1,1,3,5),    (2,1,2,2),    (1,1),    (1,1),    (0,0),      (0,0),      (1,1),     "true"),
         ("convolution_2d_1item_padded_1_1x1_1",   (1,1,3,5),    (2,1,2,2),    (1,1),    (1,1),    (1,1),      (1,1),      (1,1),     "true"),
         ("convolution_2d_1item_padded_2_3x4_5",   (1,1,3,5),    (2,1,2,2),    (1,1),    (1,1),    (2,3),      (4,5),      (1,1),     "true"),
         ("convolution_2d_2items",                 (2,1,3,5),    (2,1,2,2),    (1,1),    (1,1),    (0,0),      (0,0),      (1,1),     "true"),
         ("convolution_2d_2items_strided",         (2,1,3,5),    (2,1,2,2),    (2,2),    (1,1),    (0,0),      (0,0),      (1,1),     "true"),
         ("convolution_2d_2items_strided_padded",  (2,1,3,5),    (2,1,2,2),    (2,2),    (1,1),    (4,2),      (5,7),      (1,1),     "true"),
         ("convolution_2d_2items_strided_padded_same",
276
                                                   (2,1,3,5),    (2,1,2,2),    (2,2),    (1,1),    (2,2),      (2,2),      (1,1),     "true"),
277 278 279 280 281 282 283 284 285 286
         ("convolution_2d_2items_dilated",         (2,1,3,5),    (2,1,2,2),    (1,1),    (2,2),    (0,0),      (0,0),      (1,1),     "true"),
         ("convolution_2d_2items_dilated_padded",  (2,1,3,5),    (2,1,2,2),    (1,1),    (2,2),    (4,2),      (5,7),      (1,1),     "true"),
         ("convolution_3d_2items",                 (2,1,3,5,8),  (2,1,2,2,3),  (1,1,1),  (1,1,1),  (0,0,0),    (0,0,0),    (1,1,1),   "true"),
         ("convolution_4d_2items",                 (2,1,3,5,8,7),(2,1,2,2,3,1),(1,1,1,1),(1,1,1,1),(0,0,0,0),  (0,0,0,0),  (1,1,1,1), "false"),
         ("convolution_4d_4items",                 (4,3,3,5,8,7),(4,3,2,2,3,1),(1,1,1,1),(1,1,1,1),(0,0,0,0),  (0,0,0,0),  (1,1,1,1), "false"),
         ("convolution_4d_4items_padded_neg",      (4,3,3,5,8,7),(4,3,2,2,3,1),(1,1,1,1),(1,1,1,1),(-1,2,-3,2),(1,0,0,-3), (1,1,1,1), "false"),
         ("convolution_4d_4items_strided",         (4,3,3,5,8,7),(4,3,2,2,3,1),(2,1,3,2),(1,1,1,1),(0,0,0,0),  (0,0,0,0),  (1,1,1,1), "false"),
         ("convolution_4d_4items_dilated",         (4,3,3,5,8,7),(4,3,2,2,3,1),(1,1,1,1),(2,1,3,2),(0,0,0,0),  (0,0,0,0),  (1,1,1,1), "false"),
         ("convolution_4d_4items_strided_dilated", (4,3,8,8,8,8),(4,3,2,2,3,1),(3,2,2,3),(2,1,3,2),(0,0,0,0),  (0,0,0,0),  (1,1,1,1), "false"),
         ("convolution_4d_4items_strided_dilated_padded",
287
                                                   (4,3,8,8,8,8),(4,3,2,2,3,1),(3,2,2,3),(2,1,3,2),(2,4,6,8),  (1,3,5,7),  (1,1,1,1), "false"),
288
         ("convolution_4d_4items_strided_dilated_padded_neg",
289
                                                   (4,3,8,8,8,8),(4,3,2,2,3,1),(3,2,2,3),(2,1,3,2),(-2,4,0,5), (1,3,-1,-4),(1,1,1,1), "false"),
290
         ("convolution_4d_4items_strided_dilated_padded_same",
291
                                                   (4,3,8,8,8,8),(4,3,2,2,3,1),(3,2,2,3),(2,1,3,2),(3,3,3,3),  (3,3,3,3),  (1,1,1,1), "false"),
292 293 294 295 296 297
         ("convolution_2d_1item_1o1i_data_dilated",(1,1,3,5),    (1,1,2,2),    (1,1),    (1,1),    (0,0),      (0,0),      (2,2),     "true"),
         ("convolution_2d_1item_2o1i_data_dilated",(1,1,3,5),    (2,1,2,2),    (1,1),    (1,1),    (0,0),      (0,0),      (2,2),     "true"),
         ("convolution_2d_1item_2o2i_data_dilated",(1,2,3,5),    (2,2,2,2),    (1,1),    (1,1),    (0,0),      (0,0),      (2,2),     "true"),
         ("convolution_2d_1item_5o3i_data_dilated",(1,3,3,5),    (5,3,2,2),    (1,1),    (1,1),    (0,0),      (0,0),      (2,2),     "true"),
         ("convolution_2d_2item_5o3i_data_dilated",(2,3,3,5),    (5,3,2,2),    (1,1),    (1,1),    (0,0),      (0,0),      (2,2),     "true"),
         ("convolution_2d_8item_large_5o3i_data_dilated",
298
                                                   (8,3,16,16),  (5,3,2,2),    (1,1),    (1,1),    (0,0),      (0,0),      (2,2),     "false"),
299
         ("convolution_2d_8item_large_5o3i_uneven_filter_data_dilated",
300
                                                   (8,3,16,16),  (5,3,2,3),    (1,1),    (1,1),    (0,0),      (0,0),      (2,2),     "false"),
301
         ("convolution_2d_8item_large_5o3i_uneven_filter_uneven_data_dilation_data_dilated",
302
                                                   (8,3,16,16),  (5,3,2,3),    (1,1),    (1,1),    (0,0),      (0,0),      (2,3),     "false"),
303
         ("convolution_3d_2item_large_5o3i_uneven_filter_uneven_data_dilation_data_dilated",
304
                                                   (2,3,8,8,8),  (5,3,2,3,4),  (1,1,1),  (1,1,1),  (0,0,0),    (0,0,0),    (2,3,2),   "false"),
305
         ("convolution_3d_1item_large_5o3i_padded_uneven_filter_uneven_data_dilation_data_dilated",
306
                                                   (1,3,8,8,8),  (5,3,2,3,4),  (1,1,1),  (1,1,1),  (2,1,2),    (1,2,3),    (2,3,2),   "false"),
307
         ("convolution_3d_2item_large_5o3i_padded_strided_uneven_filter_uneven_data_dilation_data_dilated",
308
                                                   (2,3,8,8,8),  (5,3,2,3,4),  (2,3,2),  (1,1,1),  (2,1,2),    (1,2,3),    (2,3,2),   "false"),
309
         ("convolution_3d_2item_large_5o3i_padded_strided_uneven_filter_uneven_data_dilation_filter_dilated_data_dilated",
310
                                                   (2,3,8,8,8),  (5,3,2,3,4),  (2,3,2),  (3,2,2),  (2,1,2),    (1,2,3),    (2,3,2),   "false"),
311 312 313 314 315 316 317
        ]

def main():
    assert(len(sys.argv)>1)

    f = open(sys.argv[1],'w')
    f.write('''
318 319
// clang-format off

320 321 322 323 324 325 326 327 328 329 330 331 332 333 334
/*******************************************************************************
* Copyright 2017-2018 Intel Corporation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*******************************************************************************/
335 336 337 338 339 340 341 342
//
// !!!!!!!!!!!!!!THIS FILE IS AUTOGENERATED OUTSIDE OF THE BUILD PROCESS!!!!!!!!!!!!!!
//
// It takes quite awhile to compute the results, so doing it at cmake time is not a good option.
//
// If you want to add new tests, you should edit test/ref_generators/generate_convolution_ref.py
// and regenerate this file.
//
343
// To regenerate:
344 345 346 347 348 349 350 351 352 353 354 355
//
//   $ cd <ngraph source dir>/test
//   $ ./update_reference.sh
//
// !!!!!!!!!!!!!!THIS FILE IS AUTOGENERATED OUTSIDE OF THE BUILD PROCESS!!!!!!!!!!!!!!
//

#include <cmath>

#include "gtest/gtest.h"

#include "ngraph/ngraph.hpp"
adstraw's avatar
adstraw committed
356 357
#include "util/test_tools.hpp"
#include "util/autodiff/numeric_compare.hpp"
358 359 360 361

using namespace std;
using namespace ngraph;

362 363 364 365 366
template<typename T>
static bool all_close(const std::vector<T>& a,
                      const std::vector<T>& b,
                      T rtol = T(1e-4),
                      T atol = T(1e-7))
367 368
{
    assert(a.size() == b.size());
369

370 371
    bool rc = true;

372 373 374 375
    for (size_t i = 0; i < a.size(); ++i)
    {
        if (std::abs(a[i] - b[i]) > atol + rtol * std::abs(b[i]))
        {
376
            rc = false;
377 378
        }
    }
379
    return rc;
380
}
adstraw's avatar
adstraw committed
381

382
''')
383

384 385
    for t in tests:
        emit_test(t,f)
386 387 388 389 390

    f.write('''
// clang-format on
''')

391 392 393 394
    f.close()

if __name__ == "__main__":
    main()