You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@singa.apache.org by wa...@apache.org on 2018/05/19 04:25:07 UTC

[2/3] incubator-singa git commit: SINGA-363 Add DenseNet for Imagenet classification

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 <li...@gmail.com>
Authored: Fri May 18 13:18:57 2018 +0800
Committer: Wentong-DST <li...@gmail.com>
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 image=@image1.jpg http://localhost:9999/api
+        $ curl -i -F image=@image2.jpg http://localhost:9999/api
+        $ curl -i -F image=@image3.jpg 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