SINGA-363 Add DenseNet for Imagenet classification
Project: http://git-wip-us.apache.org/repos/asf/incubator-singa/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-singa/commit/8a1d98a3 Tree: http://git-wip-us.apache.org/repos/asf/incubator-singa/tree/8a1d98a3 Diff: http://git-wip-us.apache.org/repos/asf/incubator-singa/diff/8a1d98a3 Branch: refs/heads/master Commit: 8a1d98a39385137b6250bc36c5a558c9d731aa45 Parents: dd58f49 Author: Wentong-DST <[email protected]> Authored: Fri May 18 13:18:57 2018 +0800 Committer: Wentong-DST <[email protected]> Committed: Fri May 18 13:18:57 2018 +0800 ---------------------------------------------------------------------- examples/imagenet/densenet/README.md | 50 +++++++++ examples/imagenet/densenet/convert.py | 90 +++++++++++++++ examples/imagenet/densenet/model.py | 170 +++++++++++++++++++++++++++++ examples/imagenet/densenet/serve.py | 144 ++++++++++++++++++++++++ 4 files changed, 454 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-singa/blob/8a1d98a3/examples/imagenet/densenet/README.md ---------------------------------------------------------------------- diff --git a/examples/imagenet/densenet/README.md b/examples/imagenet/densenet/README.md new file mode 100644 index 0000000..390672f --- /dev/null +++ b/examples/imagenet/densenet/README.md @@ -0,0 +1,50 @@ +--- +name: DenseNet models on ImageNet +SINGA version: 1.1.1 +SINGA commit: +license: https://github.com/pytorch/vision/blob/master/torchvision/models/densenet.py +--- + +# Image Classification using DenseNet + + +In this example, we convert DenseNet on [PyTorch](https://github.com/pytorch/vision/blob/master/torchvision/models/densenet.py) +to SINGA for image classification. + +## Instructions + +* Download one parameter checkpoint file (see below) and the synset word file of ImageNet into this folder, e.g., + + $ wget https://s3-ap-southeast-1.amazonaws.com/dlfile/densenet/densenet-121.tar.gz + $ wget https://s3-ap-southeast-1.amazonaws.com/dlfile/resnet/synset_words.txt + $ tar xvf densenet-121.tar.gz + +* Usage + + $ python serve.py -h + +* Example + + # use cpu + $ python serve.py --use_cpu --parameter_file densenet-121.pickle --depth 121 & + # use gpu + $ python serve.py --parameter_file densenet-121.pickle --depth 121 & + + The parameter files for the following model and depth configuration pairs are provided: + [121](https://s3-ap-southeast-1.amazonaws.com/dlfile/densenet/densenet-121.tar.gz), [169](https://s3-ap-southeast-1.amazonaws.com/dlfile/densenet/densenet-169.tar.gz), [201](https://s3-ap-southeast-1.amazonaws.com/dlfile/densenet/densenet-201.tar.gz), [161](https://s3-ap-southeast-1.amazonaws.com/dlfile/densenet/densenet-161.tar.gz) + +* Submit images for classification + + $ curl -i -F [email protected] http://localhost:9999/api + $ curl -i -F [email protected] http://localhost:9999/api + $ curl -i -F [email protected] http://localhost:9999/api + +image1.jpg, image2.jpg and image3.jpg should be downloaded before executing the above commands. + +## Details + +The parameter files were converted from the pytorch via the convert.py program. + +Usage: + + $ python convert.py -h http://git-wip-us.apache.org/repos/asf/incubator-singa/blob/8a1d98a3/examples/imagenet/densenet/convert.py ---------------------------------------------------------------------- diff --git a/examples/imagenet/densenet/convert.py b/examples/imagenet/densenet/convert.py new file mode 100644 index 0000000..0e0522d --- /dev/null +++ b/examples/imagenet/densenet/convert.py @@ -0,0 +1,90 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + +'''Extract the net parameters from the pytorch file and store them +as python dict using cPickle. Must install pytorch. +''' + +import torch.utils.model_zoo as model_zoo + +import numpy as np +from argparse import ArgumentParser + +import model + +try: + import cPickle as pickle +except ModuleNotFoundError: + import pickle + +URL_PREFIX = 'https://download.pytorch.org/models/' +model_urls = { + 'densenet121': URL_PREFIX + 'densenet121-a639ec97.pth', + 'densenet169': URL_PREFIX + 'densenet169-b2777c0a.pth', + 'densenet201': URL_PREFIX + 'densenet201-c1103571.pth', + 'densenet161': URL_PREFIX + 'densenet161-8d451a50.pth', +} + + +def rename(pname): + p1 = pname.find('/') + p2 = pname.rfind('/') + assert p1 != -1 and p2 != -1, 'param name = %s is not correct' % pname + if 'gamma' in pname: + suffix = 'weight' + elif 'beta' in pname: + suffix = 'bias' + elif 'mean' in pname: + suffix = 'running_mean' + elif 'var' in pname: + suffix = 'running_var' + else: + suffix = pname[p2 + 1:] + return pname[p1+1:p2] + '.' + suffix + + +if __name__ == '__main__': + parser = ArgumentParser(description='Convert params from torch to python' + 'dict. ') + parser.add_argument("depth", type=int, choices=[121, 169, 201, 161]) + parser.add_argument("outfile") + parser.add_argument('nb_classes', default=1000, type=int) + + args = parser.parse_args() + + net = model.create_net(args.depth, args.nb_classes) + url = 'densenet%d' % args.depth + torch_dict = model_zoo.load_url(model_urls[url]) + params = {'SINGA_VERSION': 1101} + + # resolve dict keys name mismatch problem + print(len(net.param_names()), len(torch_dict.keys())) + for pname, pval, torch_name in\ + zip(net.param_names(), net.param_values(), torch_dict.keys()): + #torch_name = rename(pname) + ary = torch_dict[torch_name].numpy() + ary = np.array(ary, dtype=np.float32) + if len(ary.shape) == 4: + params[pname] = np.reshape(ary, (ary.shape[0], -1)) + else: + params[pname] = np.transpose(ary) + #pdb.set_trace() + assert pval.shape == params[pname].shape, 'shape mismatch for {0}, \ + expected {1} in torch model, got {2} in singa model'.\ + format(pname, params[pname].shape, pval.shape) + + with open(args.outfile, 'wb') as fd: + pickle.dump(params, fd) \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-singa/blob/8a1d98a3/examples/imagenet/densenet/model.py ---------------------------------------------------------------------- diff --git a/examples/imagenet/densenet/model.py b/examples/imagenet/densenet/model.py new file mode 100644 index 0000000..6ffaf00 --- /dev/null +++ b/examples/imagenet/densenet/model.py @@ -0,0 +1,170 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# ============================================================================= +''' This models are created following +https://arxiv.org/pdf/1608.06993.pdf and +https://github.com/pytorch/vision/blob/master/torchvision/models/densenet.py +''' +from singa import initializer +from singa import layer +from singa import net as ffnet +from singa import loss +from singa import metric +from singa.layer import Conv2D, Activation, MaxPooling2D,\ + AvgPooling2D, Split, Concat, Flatten, BatchNormalization + +import math +import sys + +ffnet.verbose = True + +conv_bias = False + + +def add_dense_connected_layers(name, net, growth_rate): + net.add(BatchNormalization('%s/bn1' % name)) + net.add(Activation('%s/relu1' % name)) + net.add(Conv2D('%s/conv1' % name, 4 * growth_rate, 1, 1, + pad=0, use_bias=conv_bias)) + net.add(BatchNormalization('%s/bn2' % name)) + net.add(Activation('%s/relu2' % name)) + return net.add(Conv2D('%s/conv2' % name, growth_rate, 3, 1, + pad=1, use_bias=conv_bias)) + + +def add_layer(name, net, growth_rate): + split = net.add(Split('%s/split' % name, 2)) + dense = add_dense_connected_layers(name, net, growth_rate) + net.add(Concat('%s/concat' % name, 1), [split, dense]) + + +def add_transition(name, net, n_channels, last=False): + net.add(BatchNormalization('%s/norm' % name)) + lyr = net.add(Activation('%s/relu' % name)) + if last: + net.add(AvgPooling2D('%s/pool' % name, + lyr.get_output_sample_shape()[1:3], pad=0)) + net.add(Flatten('flat')) + else: + net.add(Conv2D('%s/conv' % name, n_channels, 1, 1, + pad=0, use_bias=conv_bias)) + net.add(AvgPooling2D('%s/pool' % name, 2, 2, pad=0)) + + +def add_block(name, net, n_channels, N, growth_rate): + for i in range(N): + add_layer('%s/%d' % (name, i), net, growth_rate) + n_channels += growth_rate + return n_channels + + +def densenet_base(depth, growth_rate=32, reduction=0.5): + ''' + rewrite according to pytorch models + special case of densenet 161 + ''' + if depth == 121: + stages = [6, 12, 24, 16] + elif depth == 169: + stages = [6, 12, 32, 32] + elif depth == 201: + stages = [6, 12, 48, 32] + elif depth == 161: + stages = [6, 12, 36, 24] + else: + print('unknown depth: %d' % depth) + sys.exit(-1) + + net = ffnet.FeedForwardNet() + growth_rate = 48 if depth == 161 else 32 + n_channels = 2 * growth_rate + + net.add(Conv2D('input/conv', n_channels, 7, 2, pad=3, + use_bias=conv_bias, input_sample_shape=(3, 224, 224))) + net.add(BatchNormalization('input/bn')) + net.add(Activation('input/relu')) + net.add(MaxPooling2D('input/pool', 3, 2, pad=1)) + + # Dense-Block 1 and transition (56x56) + n_channels = add_block('block1', net, n_channels, stages[0], growth_rate) + add_transition('trans1', net, int(math.floor(n_channels*reduction))) + n_channels = math.floor(n_channels*reduction) + + # Dense-Block 2 and transition (28x28) + n_channels = add_block('block2', net, n_channels, stages[1], growth_rate) + add_transition('trans2', net, int(math.floor(n_channels*reduction))) + n_channels = math.floor(n_channels*reduction) + + # Dense-Block 3 and transition (14x14) + n_channels = add_block('block3', net, n_channels, stages[2], growth_rate) + add_transition('trans3', net, int(math.floor(n_channels*reduction))) + n_channels = math.floor(n_channels*reduction) + + # Dense-Block 4 and transition (7x7) + n_channels = add_block('block4', net, n_channels, stages[3], growth_rate) + add_transition('trans4', net, n_channels, True) + + return net + + +def init_params(net, weight_path=None, is_train=False): + '''Init parameters randomly or from checkpoint file. + + Args: + net, a constructed neural net + weight_path, checkpoint file path + is_train, if false, then a checkpoint file must be presented + ''' + assert is_train is True or weight_path is not None, \ + 'must provide a checkpoint file for serving' + + if weight_path is None: + for pname, pval in zip(net.param_names(), net.param_values()): + if 'conv' in pname and len(pval.shape) > 1: + initializer.gaussian(pval, 0, pval.shape[1]) + elif 'dense' in pname: + if len(pval.shape) > 1: + initializer.gaussian(pval, 0, pval.shape[0]) + else: + pval.set_value(0) + # init params from batch norm layer + elif 'mean' in pname or 'beta' in pname: + pval.set_value(0) + elif 'var' in pname: + pval.set_value(1) + elif 'gamma' in pname: + initializer.uniform(pval, 0, 1) + else: + net.load(weight_path, use_pickle=True) + + +def create_net(depth, nb_classes, dense=0, use_cpu=True): + if use_cpu: + layer.engine = 'singacpp' + + net = densenet_base(depth) + + # this part was not included in the pytorch model + if dense > 0: + net.add(layer.Dense('hidden-dense', dense)) + net.add(layer.Activation('act-dense')) + net.add(layer.Dropout('dropout')) + + net.add(layer.Dense('sigmoid', nb_classes)) + return net + +if __name__ == '__main__': + create_net(121, 1000) \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-singa/blob/8a1d98a3/examples/imagenet/densenet/serve.py ---------------------------------------------------------------------- diff --git a/examples/imagenet/densenet/serve.py b/examples/imagenet/densenet/serve.py new file mode 100644 index 0000000..2e401b9 --- /dev/null +++ b/examples/imagenet/densenet/serve.py @@ -0,0 +1,144 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + +import sys +import time +import numpy as np +import traceback +from argparse import ArgumentParser +from scipy.misc import imread + +from singa import device +from singa import tensor +from singa import image_tool + +from rafiki.agent import Agent, MsgType +import model + +tool = image_tool.ImageTool() +num_augmentation = 1 +crop_size = 224 +mean = np.array([0.485, 0.456, 0.406]) +std = np.array([0.229, 0.224, 0.225]) + + +def allowed_file(filename): + return '.' in filename and filename.rsplit('.', 1)[1] in \ + ["PNG", "png", "jpg", "JPG", "JPEG", "jpeg"] + + +def serve(net, label_map, dev, agent, topk=5): + '''Serve to predict image labels. + It prints the topk food names for each image. + Args: + label_map: a list of food names, corresponding to the index in meta_file + ''' + + images = tensor.Tensor((num_augmentation, 3, crop_size, crop_size), dev) + while True: + msg, val = agent.pull() + if msg is None: + time.sleep(0.1) + continue + msg = MsgType.parse(msg) + if msg.is_request(): + try: + # process images + img = imread(val['image'], mode='RGB').astype(np.float32) / 255 + height,width = img.shape[:2] + img -= mean + img /= std + img = img.transpose((2, 0, 1)) + img = img[:, (height-224)//2:(height+224)//2, + (width-224)//2:(width+224)//2] + images.copy_from_numpy(img) + print("input: ", images.l1()) + # do prediction + y = net.predict(images) + prob = np.average(tensor.to_numpy(y), 0) + idx = np.argsort(-prob) + # prepare results + response = "" + for i in range(topk): + response += "%s:%f <br/>" % (label_map[idx[i]], + prob[idx[i]]) + except: + traceback.print_exc() + response = "sorry, system error during prediction." + agent.push(MsgType.kResponse, response) + elif msg.is_command(): + if MsgType.kCommandStop.equal(msg): + print('get stop command') + agent.push(MsgType.kStatus, "success") + break + else: + print('get unsupported command %s' % str(msg)) + agent.push(MsgType.kStatus, "Unknown command") + else: + print('get unsupported message %s' % str(msg)) + agent.push(MsgType.kStatus, "unsupported msg; going to shutdown") + break + print("server stop") + + +def main(): + try: + # Setup argument parser + parser = ArgumentParser(description='DenseNet inference') + + parser.add_argument("--port", default=9999, help="listen port") + parser.add_argument("--use_cpu", action="store_true", + help="If set, load models onto CPU devices") + parser.add_argument("--parameter_file", default="densenet-121.pickle") + parser.add_argument("--depth", type=int, choices=[121, 169, 201, 161], + default=121) + + parser.add_argument('--nb_classes', default=1000, type=int) + + # Process arguments + args = parser.parse_args() + port = args.port + + # start to train + agent = Agent(port) + + net = model.create_net(args.depth, args.nb_classes, 0, args.use_cpu) + if args.use_cpu: + print('Using CPU') + dev = device.get_default_device() + else: + print('Using GPU') + dev = device.create_cuda_gpu() + net.to_device(dev) + print('start to load parameter_file') + model.init_params(net, args.parameter_file) + print('Finish loading models') + + labels = np.loadtxt('synset_words.txt', str, delimiter='\t ') + serve(net, labels, dev, agent) + # wait the agent finish handling http request + agent.stop() + + except SystemExit: + return + except: + traceback.print_exc() + sys.stderr.write(" for help use --help \n\n") + return 2 + + +if __name__ == '__main__': + main() \ No newline at end of file
