1# Licensed to the Apache Software Foundation (ASF) under one 2# or more contributor license agreements. See the NOTICE file 3# distributed with this work for additional information 4# regarding copyright ownership. The ASF licenses this file 5# to you under the Apache License, Version 2.0 (the 6# "License"); you may not use this file except in compliance 7# with the License. You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, 12# software distributed under the License is distributed on an 13# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14# KIND, either express or implied. See the License for the 15# specific language governing permissions and limitations 16# under the License. 17 18"""Test converted models layer by layer 19""" 20import argparse 21import logging 22import os 23import warnings 24 25import numpy as np 26import cv2 27import mxnet as mx 28 29logging.basicConfig(level=logging.INFO) 30 31 32def read_image(img_path, image_dims=None, mean=None): 33 """ 34 Reads an image from file path or URL, optionally resizing to given image dimensions and 35 subtracting mean. 36 :param img_path: path to file, or url to download 37 :param image_dims: image dimensions to resize to, or None 38 :param mean: mean file to subtract, or None 39 :return: loaded image, in RGB format 40 """ 41 42 import urllib 43 44 filename = img_path.split("/")[-1] 45 if img_path.startswith('http'): 46 urllib.urlretrieve(img_path, filename) 47 img = cv2.imread(filename) 48 else: 49 img = cv2.imread(img_path) 50 51 img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) 52 53 if image_dims is not None: 54 img = cv2.resize(img, image_dims) # resize to image_dims to fit model 55 img = np.rollaxis(img, 2) # change to (c, h, w) order 56 img = img[np.newaxis, :] # extend to (n, c, h, w) 57 if mean is not None: 58 mean = np.array(mean) 59 if mean.shape == (3,): 60 mean = mean[np.newaxis, :, np.newaxis, np.newaxis] # extend to (n, c, 1, 1) 61 img = img.astype(np.float32) - mean # subtract mean 62 63 return img 64 65 66def _ch_dev(arg_params, aux_params, ctx): 67 """ 68 Changes device of given mxnet arguments 69 :param arg_params: arguments 70 :param aux_params: auxiliary parameters 71 :param ctx: new device context 72 :return: arguments and auxiliary parameters on new device 73 """ 74 new_args = dict() 75 new_auxs = dict() 76 for k, v in arg_params.items(): 77 new_args[k] = v.as_in_context(ctx) 78 for k, v in aux_params.items(): 79 new_auxs[k] = v.as_in_context(ctx) 80 return new_args, new_auxs 81 82 83def convert_and_compare_caffe_to_mxnet(image_url, gpu, caffe_prototxt_path, caffe_model_path, 84 caffe_mean, mean_diff_allowed, max_diff_allowed): 85 """ 86 Run the layer comparison on a caffe model, given its prototxt, weights and mean. 87 The comparison is done by inferring on a given image using both caffe and mxnet model 88 :param image_url: image file or url to run inference on 89 :param gpu: gpu to use, -1 for cpu 90 :param caffe_prototxt_path: path to caffe prototxt 91 :param caffe_model_path: path to caffe weights 92 :param caffe_mean: path to caffe mean file 93 """ 94 95 import caffe 96 from caffe_proto_utils import read_network_dag, process_network_proto, read_caffe_mean 97 from convert_model import convert_model 98 99 if isinstance(caffe_mean, str): 100 caffe_mean = read_caffe_mean(caffe_mean) 101 elif caffe_mean is None: 102 pass 103 elif len(caffe_mean) == 3: 104 # swap channels from Caffe BGR to RGB 105 caffe_mean = caffe_mean[::-1] 106 107 # get caffe root location, this is needed to run the upgrade network utility, so we only need 108 # to support parsing of latest caffe 109 caffe_root = os.path.dirname(os.path.dirname(caffe.__path__[0])) 110 caffe_prototxt_path = process_network_proto(caffe_root, caffe_prototxt_path) 111 112 _, layer_name_to_record, top_to_layers = read_network_dag(caffe_prototxt_path) 113 114 caffe.set_mode_cpu() 115 caffe_net = caffe.Net(caffe_prototxt_path, caffe_model_path, caffe.TEST) 116 117 image_dims = tuple(caffe_net.blobs['data'].shape)[2:4] 118 119 logging.info('getting image %s', image_url) 120 img_rgb = read_image(image_url, image_dims, caffe_mean) 121 img_bgr = img_rgb[:, ::-1, :, :] 122 123 caffe_net.blobs['data'].reshape(*img_bgr.shape) 124 caffe_net.blobs['data'].data[...] = img_bgr 125 _ = caffe_net.forward() 126 127 # read sym and add all outputs 128 sym, arg_params, aux_params, _ = convert_model(caffe_prototxt_path, caffe_model_path) 129 sym = sym.get_internals() 130 131 # now mxnet 132 if gpu < 0: 133 ctx = mx.cpu(0) 134 else: 135 ctx = mx.gpu(gpu) 136 137 arg_params, aux_params = _ch_dev(arg_params, aux_params, ctx) 138 arg_params["data"] = mx.nd.array(img_rgb, ctx) 139 arg_params["prob_label"] = mx.nd.empty((1,), ctx) 140 exe = sym.bind(ctx, arg_params, args_grad=None, grad_req="null", aux_states=aux_params) 141 exe.forward(is_train=False) 142 143 compare_layers_from_nets(caffe_net, arg_params, aux_params, exe, layer_name_to_record, 144 top_to_layers, mean_diff_allowed, max_diff_allowed) 145 146 147def _bfs(root_node, process_node): 148 """ 149 Implementation of Breadth-first search (BFS) on caffe network DAG 150 :param root_node: root node of caffe network DAG 151 :param process_node: function to run on each node 152 """ 153 154 from collections import deque 155 156 seen_nodes = set() 157 next_nodes = deque() 158 159 seen_nodes.add(root_node) 160 next_nodes.append(root_node) 161 162 while next_nodes: 163 current_node = next_nodes.popleft() 164 165 # process current node 166 process_node(current_node) 167 168 for child_node in current_node.children: 169 if child_node not in seen_nodes: 170 seen_nodes.add(child_node) 171 next_nodes.append(child_node) 172 173 174def compare_layers_from_nets(caffe_net, arg_params, aux_params, exe, layer_name_to_record, 175 top_to_layers, mean_diff_allowed, max_diff_allowed): 176 """ 177 Compare layer by layer of a caffe network with mxnet network 178 :param caffe_net: loaded caffe network 179 :param arg_params: arguments 180 :param aux_params: auxiliary parameters 181 :param exe: mxnet model 182 :param layer_name_to_record: map between caffe layer and information record 183 :param top_to_layers: map between caffe blob name to layers which outputs it (including inplace) 184 :param mean_diff_allowed: mean difference allowed between caffe blob and mxnet blob 185 :param max_diff_allowed: max difference allowed between caffe blob and mxnet blob 186 """ 187 188 import re 189 190 log_format = ' {0:<40} {1:<40} {2:<8} {3:>10} {4:>10} {5:<1}' 191 192 compare_layers_from_nets.is_first_convolution = True 193 194 def _compare_blob(caf_blob, mx_blob, caf_name, mx_name, blob_type, note): 195 diff = np.abs(mx_blob - caf_blob) 196 diff_mean = diff.mean() 197 diff_max = diff.max() 198 logging.info(log_format.format(caf_name, mx_name, blob_type, '%4.5f' % diff_mean, 199 '%4.5f' % diff_max, note)) 200 assert diff_mean < mean_diff_allowed 201 assert diff_max < max_diff_allowed 202 203 def _process_layer_parameters(layer): 204 205 logging.debug('processing layer %s of type %s', layer.name, layer.type) 206 207 normalized_layer_name = re.sub('[-/]', '_', layer.name) 208 209 # handle weight and bias of convolution and fully-connected layers 210 if layer.name in caffe_net.params and layer.type in ['Convolution', 'InnerProduct', 211 'Deconvolution']: 212 213 has_bias = len(caffe_net.params[layer.name]) > 1 214 215 mx_name_weight = '{}_weight'.format(normalized_layer_name) 216 mx_beta = arg_params[mx_name_weight].asnumpy() 217 218 # first convolution should change from BGR to RGB 219 if layer.type == 'Convolution' and compare_layers_from_nets.is_first_convolution: 220 compare_layers_from_nets.is_first_convolution = False 221 222 # if RGB or RGBA 223 if mx_beta.shape[1] == 3 or mx_beta.shape[1] == 4: 224 # Swapping BGR of caffe into RGB in mxnet 225 mx_beta[:, [0, 2], :, :] = mx_beta[:, [2, 0], :, :] 226 227 caf_beta = caffe_net.params[layer.name][0].data 228 _compare_blob(caf_beta, mx_beta, layer.name, mx_name_weight, 'weight', '') 229 230 if has_bias: 231 mx_name_bias = '{}_bias'.format(normalized_layer_name) 232 mx_gamma = arg_params[mx_name_bias].asnumpy() 233 caf_gamma = caffe_net.params[layer.name][1].data 234 _compare_blob(caf_gamma, mx_gamma, layer.name, mx_name_bias, 'bias', '') 235 236 elif layer.name in caffe_net.params and layer.type == 'Scale': 237 238 if 'scale' in normalized_layer_name: 239 bn_name = normalized_layer_name.replace('scale', 'bn') 240 elif 'sc' in normalized_layer_name: 241 bn_name = normalized_layer_name.replace('sc', 'bn') 242 else: 243 assert False, 'Unknown name convention for bn/scale' 244 245 beta_name = '{}_beta'.format(bn_name) 246 gamma_name = '{}_gamma'.format(bn_name) 247 248 mx_beta = arg_params[beta_name].asnumpy() 249 caf_beta = caffe_net.params[layer.name][1].data 250 _compare_blob(caf_beta, mx_beta, layer.name, beta_name, 'mov_mean', '') 251 252 mx_gamma = arg_params[gamma_name].asnumpy() 253 caf_gamma = caffe_net.params[layer.name][0].data 254 _compare_blob(caf_gamma, mx_gamma, layer.name, gamma_name, 'mov_var', '') 255 256 elif layer.name in caffe_net.params and layer.type == 'BatchNorm': 257 258 mean_name = '{}_moving_mean'.format(normalized_layer_name) 259 var_name = '{}_moving_var'.format(normalized_layer_name) 260 261 caf_rescale_factor = caffe_net.params[layer.name][2].data 262 263 mx_mean = aux_params[mean_name].asnumpy() 264 caf_mean = caffe_net.params[layer.name][0].data / caf_rescale_factor 265 _compare_blob(caf_mean, mx_mean, layer.name, mean_name, 'mean', '') 266 267 mx_var = aux_params[var_name].asnumpy() 268 caf_var = caffe_net.params[layer.name][1].data / caf_rescale_factor 269 _compare_blob(caf_var, mx_var, layer.name, var_name, 'var', 270 'expect 1e-04 change due to cudnn eps') 271 272 elif layer.type in ['Input', 'Pooling', 'ReLU', 'Eltwise', 'Softmax', 'LRN', 'Concat', 273 'Dropout', 'Crop']: 274 # no parameters to check for these layers 275 pass 276 277 else: 278 warnings.warn('No handling for layer %s of type %s, should we ignore it?', layer.name, 279 layer.type) 280 281 282 def _process_layer_output(caffe_blob_name): 283 284 logging.debug('processing blob %s', caffe_blob_name) 285 286 # skip blobs not originating from actual layers, e.g. artificial split layers added by caffe 287 if caffe_blob_name not in top_to_layers: 288 return 289 290 caf_blob = caffe_net.blobs[caffe_blob_name].data 291 292 # data should change from BGR to RGB 293 if caffe_blob_name == 'data': 294 295 # if RGB or RGBA 296 if caf_blob.shape[1] == 3 or caf_blob.shape[1] == 4: 297 # Swapping BGR of caffe into RGB in mxnet 298 caf_blob[:, [0, 2], :, :] = caf_blob[:, [2, 0], :, :] 299 mx_name = 'data' 300 301 else: 302 # get last layer name which outputs this blob name 303 last_layer_name = top_to_layers[caffe_blob_name][-1] 304 normalized_last_layer_name = re.sub('[-/]', '_', last_layer_name) 305 mx_name = '{}_output'.format(normalized_last_layer_name) 306 if 'scale' in mx_name: 307 mx_name = mx_name.replace('scale', 'bn') 308 elif 'sc' in mx_name: 309 mx_name = mx_name.replace('sc', 'bn') 310 311 if mx_name not in exe.output_dict: 312 logging.error('mxnet blob %s is missing, time to extend the compare tool..', mx_name) 313 return 314 315 mx_blob = exe.output_dict[mx_name].asnumpy() 316 _compare_blob(caf_blob, mx_blob, caffe_blob_name, mx_name, 'output', '') 317 318 return 319 320 # check layer parameters 321 logging.info('\n***** Network Parameters '.ljust(140, '*')) 322 logging.info(log_format.format('CAFFE', 'MXNET', 'Type', 'Mean(diff)', 'Max(diff)', 'Note')) 323 first_layer_name = layer_name_to_record.keys()[0] 324 _bfs(layer_name_to_record[first_layer_name], _process_layer_parameters) 325 326 # check layer output 327 logging.info('\n***** Network Outputs '.ljust(140, '*')) 328 logging.info(log_format.format('CAFFE', 'MXNET', 'Type', 'Mean(diff)', 'Max(diff)', 'Note')) 329 for caffe_blob_name in caffe_net.blobs.keys(): 330 _process_layer_output(caffe_blob_name) 331 332 333def main(): 334 """Entrypoint for compare_layers""" 335 336 parser = argparse.ArgumentParser( 337 description='Tool for testing caffe to mxnet conversion layer by layer') 338 parser.add_argument('--image_url', type=str, 339 default='https://github.com/dmlc/web-data/raw/master/mxnet/doc/'\ 340 'tutorials/python/predict_image/cat.jpg', 341 help='input image to test inference, can be either file path or url') 342 parser.add_argument('--caffe_prototxt_path', type=str, 343 default='./model.prototxt', 344 help='path to caffe prototxt') 345 parser.add_argument('--caffe_model_path', type=str, 346 default='./model.caffemodel', 347 help='path to caffe weights') 348 parser.add_argument('--caffe_mean', type=str, 349 default='./model_mean.binaryproto', 350 help='path to caffe mean file') 351 parser.add_argument('--mean_diff_allowed', type=int, default=1e-03, 352 help='mean difference allowed between caffe blob and mxnet blob') 353 parser.add_argument('--max_diff_allowed', type=int, default=1e-01, 354 help='max difference allowed between caffe blob and mxnet blob') 355 parser.add_argument('--gpu', type=int, default=-1, help='the gpu id used for predict') 356 args = parser.parse_args() 357 convert_and_compare_caffe_to_mxnet(args.image_url, args.gpu, args.caffe_prototxt_path, 358 args.caffe_model_path, args.caffe_mean, 359 args.mean_diff_allowed, args.max_diff_allowed) 360 361if __name__ == '__main__': 362 main() 363