From 6c8622d1573269e60035732955736be8d0473293 Mon Sep 17 00:00:00 2001 From: =?utf8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9=20=D0=A8=D0=B5=D0=B4?= =?utf8?q?=D1=8C=D0=BA=D0=BE/AI=20Tools=20Lab=20/SRR/Assistant=20Engineer/?= =?utf8?q?=EC=82=BC=EC=84=B1=EC=A0=84=EC=9E=90?= Date: Mon, 8 Oct 2018 20:24:14 +0300 Subject: [PATCH] Caffe Model Maker (#1735) This utility creates 1 or 2 layer caffe models. Signed-off-by: Andrei Shedko --- contrib/nnc/utils/caffe_model_maker/AllFill.sh | 48 ++ contrib/nnc/utils/caffe_model_maker/Filler.sh | 28 + .../utils/caffe_model_maker/GenerateCaffeModels.py | 730 +++++++++++++++++++++ contrib/nnc/utils/caffe_model_maker/Pyloss.py | 51 ++ contrib/nnc/utils/caffe_model_maker/README.md | 22 + contrib/nnc/utils/caffe_model_maker/in.jpg | Bin 0 -> 15556 bytes 6 files changed, 879 insertions(+) create mode 100755 contrib/nnc/utils/caffe_model_maker/AllFill.sh create mode 100755 contrib/nnc/utils/caffe_model_maker/Filler.sh create mode 100644 contrib/nnc/utils/caffe_model_maker/GenerateCaffeModels.py create mode 100644 contrib/nnc/utils/caffe_model_maker/Pyloss.py create mode 100644 contrib/nnc/utils/caffe_model_maker/README.md create mode 100644 contrib/nnc/utils/caffe_model_maker/in.jpg diff --git a/contrib/nnc/utils/caffe_model_maker/AllFill.sh b/contrib/nnc/utils/caffe_model_maker/AllFill.sh new file mode 100755 index 0000000..93e38d1 --- /dev/null +++ b/contrib/nnc/utils/caffe_model_maker/AllFill.sh @@ -0,0 +1,48 @@ +#!/bin/sh +: ' +Copyright (c) 2018 Samsung Electronics Co., Ltd. All Rights Reserved + +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. +' + + +#Fills all models and writes errors +usage () { + echo "Filler.sh should be in the working directory\nusage: + no args - assumes current directory + -d= fills models in + Example: + $(basename $0) -d='./foobar/'" +} + +DIR="./" +for i in "$@" +do + case $i in + -h|--help|help) + usage + exit 1 + ;; + -d=*) + DIR=${i#*=} + ;; + esac + shift +done +echo $DIR +if [ $# -eq 0 ]; then + echo "Assume working directory" +fi +for a in `ls $DIR*.prototxt`; do + ./Filler.sh $a +done 2>error.log diff --git a/contrib/nnc/utils/caffe_model_maker/Filler.sh b/contrib/nnc/utils/caffe_model_maker/Filler.sh new file mode 100755 index 0000000..963edbf --- /dev/null +++ b/contrib/nnc/utils/caffe_model_maker/Filler.sh @@ -0,0 +1,28 @@ +#!/bin/sh +: ' +Copyright (c) 2018 Samsung Electronics Co., Ltd. All Rights Reserved + +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. +' + +#Fills $1 with random weights +if [ $# -eq 0 ] + then + echo "usage:\n $(basename $0) foo.prototxt" + exit 1 +fi +FN=$1 +NOEXT=${FN%%.*} # filename without the extension +mkdir $NOEXT +caffegen init < $FN > $NOEXT/filled.prototxt +caffegen encode < $NOEXT/filled.prototxt > $NOEXT/model.caffemodel diff --git a/contrib/nnc/utils/caffe_model_maker/GenerateCaffeModels.py b/contrib/nnc/utils/caffe_model_maker/GenerateCaffeModels.py new file mode 100644 index 0000000..b1ed9d4 --- /dev/null +++ b/contrib/nnc/utils/caffe_model_maker/GenerateCaffeModels.py @@ -0,0 +1,730 @@ +#!/usr/bin/python3 +""" +Copyright (c) 2018 Samsung Electronics Co., Ltd. All Rights Reserved + +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. +""" + +import caffe +import numpy as np +import sys +import h5py +from itertools import chain +from caffe import layers as L +import random +import lmdb +from collections import Counter, OrderedDict + +if (len(sys.argv) < 2): + dest_folder = '' + print('Using current directory as destination folder') +else: + dest_folder = sys.argv[1] + '/' + + +class PH: + """ + PlaceHolder value + """ + + def __init__(self, type, param): + self.type = type + self.param = param + + +# Bookkeeping +LS = 224 +# bynaryProto file for Infogain +H = np.eye(3, dtype='f4') +blob = caffe.io.array_to_blobproto(H.reshape((1, 1, 3, 3))) +with open(dest_folder + 'infogainH.binaryproto', 'wb+') as f: + f.write(blob.SerializeToString()) + +# List of hdf5 files +with open(dest_folder + "in", 'w+') as f: + f.write('in.hdf5') + +#Window File +with open(dest_folder + "in_winds", 'w+') as f: + f.write("""# 1 +in.jpg +3 +224 +224 +2 +1 0.1 50 50 60 70 +1 0.9 30 30 50 50 +# 2 +in.jpg +3 +224 +224 +2 +1 0.1 50 50 70 70 +1 0.9 30 30 50 50 +""") + +# HDF5 file for HDF5DataSet +h5f = h5py.File(dest_folder + "in.hdf5", "w") +h5f.create_dataset("data", data=np.random.rand(1, 3, LS, LS)) +h5f.close() + +# LMDB file +env = lmdb.open(dest_folder + 'test-lmdb') +with env.begin(write=True) as txn: + img_data = np.random.rand(3, LS, LS) + datum = caffe.io.array_to_datum(img_data, label=1) + txn.put('{:0>10d}'.format(1).encode('ascii'), datum.SerializeToString()) +env.close() + +# recurring parameters +losspara = {'ignore_label': True, 'normalization': 1, 'normalize': True} +softmaxpara = {'engine': 0, 'axis': 1} +gdfil = {'type': 'gaussian', 'std': 0.001} +cofil = {'type': 'constant', 'value': 0} +rp = { + 'num_output': 1, + 'weight_filler': gdfil, + 'bias_filler': cofil, + 'expose_hidden': True +} + +filler_par = { + 'type': 'constant', + 'value': 0, + 'min': 0, + 'max': 1, + 'mean': 0, + 'std': 1, + 'sparse': -1, # -1 means no sparsification + 'variance_norm': 0 +} # 0 = FAN_IN, 1 = FAN_OUT, 2 = AVERAGE + +OPS = [ + ('Parameter', { + 'shape': { + 'dim': [1] + }, + "is_data": True + }), # ok + ( + 'Data', + { + 'source': 'test-lmdb', # FIXME: unknown DB backend + 'batch_size': 1, + 'rand_skip': 0, + 'backend': 1, # 0 = LEVELDB, 1 = LMDB + 'scale': 1.0, # deprecated in favor of TransformationParameter + 'mean_file': 'wtf.is_that', + 'crop_size': 0, + 'mirror': False, + 'force_encoded_color': False, + 'prefetch': 4, + "is_data": True + }), + ( + 'DummyData', + { + 'data_filler': cofil, # ok + #'num' : [1,1,1], # deprecated shape specification + #'channels' : [2,2,2], + #'height' : [3,3,3], + #'width' : [4,4,4]}, + 'shape': { + 'dim': [1, 3, LS, LS] + }, + "is_data": True + }), + ( + 'ImageData', + { + 'source': 'in_imgs', # file with list of imgs + 'top': 'op2', + 'batch_size': 1, + 'rand_skip': 0, + 'shuffle': False, + 'new_height': 0, + 'new_width': 0, + 'is_color': True, + 'root_folder': '', + 'scale': 1.0, # deprecated in favor of TransformationParameter + 'mirror': False, + "is_data": True + }), + ( + 'WindowData', + { + 'source': 'in_winds', + 'top': 'op2', + 'batch_size': 1, + 'mean_file': 'in.jpg', + 'transform_param': { + 'scale': 0.8, + 'crop_size': 24, + 'mirror': False, + #'fg_treshold' : 0.5, + #'bg_treshold' : 0.5, + #'fg_fraction' : 0.25, + }, + 'context_pad': 1, + 'crop_mode': 'warp', + 'cache_images': True, + 'root_folder': './', + "is_data": True + }), + ( + 'HDF5Data', + { + 'source': + 'in', # This is the name of the file WITH HDF5 FILENAMES 0_0 + # Top should have the same name as the dataset in the hdf5 file + # FIXME Requires Caffegen to be built with Caffe that supports LMDB + 'batch_size': 1, + 'shuffle': False, + "is_data": True + }), + ('Input', { + 'shape': { + 'dim': [1, 2, 3, 4] + }, + "is_data": True + }), # ok + ( + 'MemoryData', + { + 'batch_size': 1, # ok + 'channels': 2, + 'height': 3, + 'width': 4, + 'top': "foo", + "is_data": True + }), + + ## Regular OPS + ( + "Convolution", + { + 'num_output': 64, # ok + 'kernel_size': 9, + 'stride': 1, + 'pad': 0, + 'weight_filler': gdfil, + 'param': [{ + 'lr_mult': 1 + }, { + 'lr_mult': 0.1 + }], + 'bias_filler': cofil + }), + + # Depthvise conv + ( + "Convolution", + { + 'num_output': 12, # ok + 'kernel_size': 9, + 'stride': 1, + 'dilation': 2, + 'group': 3, + 'pad': 0, + 'weight_filler': gdfil, + 'param': [{ + 'lr_mult': 1 + }, { + 'lr_mult': 0.1 + }], + 'bias_filler': cofil + }), + ( + "Deconvolution", + { + 'convolution_param': # ok + { + 'num_output': 4, + 'kernel_size': 9, + 'stride': 1, + 'pad': 0, + 'weight_filler': gdfil, + 'bias_filler': cofil + } + }), + # Depthvise deconv + ( + "Deconvolution", + { + 'convolution_param': # ok + { + 'num_output': 12, + 'kernel_size': 9, + 'stride': 1, + 'dilation': 2, + 'group': 3, + 'pad': 0, + 'weight_filler': gdfil, + 'bias_filler': cofil + } + }), + ( + 'BatchNorm', + { + 'eps': 1e-5, # ok + 'moving_average_fraction': 0.999 + }), + ( + 'LRN', + { + 'alpha': 1., # ok + 'beta': 0.75, + 'norm_region': 1, + 'local_size': 5, + 'k': 1, + 'engine': 0 + }), + # local_size[default 5]: the number of channels to sum over + # alpha[default 1]: the scaling paramete + # beta[default5]: the exponent + # norm_region[default ACROSS_CHANNLS]: whether to sum over adjacent channels(ACROSS_CHANNLS) or nearby + # spatial locations(WITHIN_CHANNLS) + # `input / (1 + (\alpha/n) \sum_i x_i^2)^\beta` + ( + "MVN", + { + 'normalize_variance': True, # ok + 'across_channels': False, + 'eps': 1e-9 + }), + ( + 'Im2col', + { + 'convolution_param': # ok + { + 'num_output': 64, + 'kernel_size': 9, + 'stride': 1, + 'pad': 0, + 'weight_filler': gdfil, + # 'param' : [{'lr_mult':1},{'lr_mult':0.1}], + 'bias_filler': cofil + } + }), + ('Dropout', { + 'dropout_ratio': 0.5 + }), # ok + ('Split', {}), # ok + ('Concat', { + 'axis': 1 + }), # ok + ( + 'Tile', + { + 'axis': 1, # ok + 'tiles': 2 + }), + ('Slice', { + 'axis': 1, + 'top': 'op2', + 'slice_point': 1 + }), + ( + 'Reshape', + { + 'shape': { + 'dim': [1, 0, -1] + }, # ok + 'axis': 0, + 'num_axes': -1 + }), + # reshapes only [axis, axis + num_axes] if those aren't 0 and -1; axis can be negative + # 0 in shape means retaining dim size, -1 means auto size + ( + 'Flatten', + { + 'axis': 1, # ok + 'end_axis': -1 + }), + ( + 'Pooling', + { + 'pool': 0, # ok # pool: 0 = MAX, 1 = AVE, 2 = STOCHASTIC + 'pad': 0, # can be replaced with pad_w, pad_h + 'kernel_size': 3, # can be replaced with kernel_w, kernel_h + 'stride': 1, # can be replaced with stride_w, stride_h + 'engine': 0, + 'global_pooling': False + }), + # 'round_mode' : 0}), # 0 = CELS, 1 = FLOOR + ( + 'Reduction', + { + 'operation': 1, # ok # 1 = SUM, 2 = ASUM, 3 = SUMSQ, 4 = MEAN # ok + 'axis': 0, + 'coeff': 1.0 + }), + ( + 'SPP', + { + 'pyramid_height': 1, # ok + 'pool': 0, + 'engine': 0 + }), + ( + 'InnerProduct', + { + 'num_output': 2, # ok + 'bias_term': True, + 'weight_filler': filler_par, + 'bias_filler': filler_par, + 'axis': 1, + 'transpose': False + }), + ( + 'Embed', + { + 'num_output': 2, # ok + 'input_dim': 1, + 'bias_term': True, + 'weight_filler': filler_par, + 'bias_filler': filler_par + }), + ( + 'ArgMax', + { + 'out_max_val': + False, # ok # if True, outputs pairs (argmax, maxval) # ok + 'top_k': 1, + 'axis': -1 + }), + ( + 'Softmax', + { + 'engine': 0, # ok + 'axis': 1 + }), + ( + 'ReLU', + { + 'negative_slope': 0, # ok + 'engine': 0 + }), + ( + 'PReLU', + { + 'filler': filler_par, # ok + 'channel_shared': False + }), + ('ELU', { + 'alpha': 1 + }), # ok + ('Sigmoid', { + 'engine': 0 + }), # ok + ('BNLL', {}), # ok + ('TanH', { + 'engine': 0 + }), # ok + ('Threshold', { + 'threshold': 0 + }), # ok + ( + 'Bias', + { + 'axis': 0, # ok + 'num_axes': -1, + 'filler': filler_par + }), + ( + 'Scale', + { + 'axis': 0, # ok + 'num_axes': -1, + 'filler': filler_par, + 'bias_term': False, + 'bias_filler': filler_par + }), + ('AbsVal', {}), # ok + ( + 'Log', + { + 'base': -1.0, # ok + 'scale': 1.0, + 'shift': PH(float, (2.0, 10.0)), + 'how_many' : 10 + }), # y = ln(shift + scale * x) (log_base() for base > 0) + ( + 'Power', + { + 'power': -1.0, # ok + 'scale': 1.0, + 'shift': 0.0 + }), # y = (shift + scale * x) ^ power + ( + 'Exp', + { + 'base': -1.0, # ok + 'scale': 1.0, + 'shift': 0.0 + }), + + ## TWO INPUTS + ( + 'Crop', + { + 'axis': 2, # ok + 'offset': [0], + "inputs": 2 + }), # if one offset - for all dims, more - specifies + ( + "Eltwise", + { + 'operation': 1, # ok + 'coeff': [3, 3], + 'stable_prod_grad': True, + "inputs": 2 + }), + ("EuclideanLoss", { + "inputs": 2 + }), # ok + ("HingeLoss", { + 'norm': 1, + "inputs": 2 + }), # L1 = 1; L2 = 2; # ok + ("SigmoidCrossEntropyLoss", { + 'loss_param': losspara, + "inputs": 2 + }), # ok + + ## TWO Inputs, special shape + ( + "Accuracy", + { + 'top_k': 1, # FIXME: different bottom shapes needed + 'axis': 0, + 'ignore_label': 0, + "inputs": 2, + "special_shape": [1, 3, 1, 1] + }), + ( + "SoftmaxWithLoss", + { + 'loss_param': losspara, # FIXME: different bottom shapes needed + 'softmax_param': softmaxpara, + "inputs": 2, + "special_shape": [1, 1, 1, 1] + }), + ("MultinomialLogisticLoss", { + 'loss_param': losspara, + "inputs": 2, + "special_shape": [1, 1, 1, 1] + }), # FIXME: different bottom shapes needed + ("Filter", { + "inputs": 2, + "special_shape": [1, 1, 1, 1] + }), # FIXME: different bottom shapes needed + ('BatchReindex', { + "inputs": 2, + "special_shape": [2] + }), # takes indices as second blob + ("InfogainLoss", { + 'source': 'infogainH.binaryproto', + 'axis': 1, + "inputs": 2, + "special_shape": [1, 1, 1, 1] + }), + ( + 'Python', + { + 'python_param': # Custom Loss layer + { + 'module': 'Pyloss', # the module name -- usually the filename -- that needs to be in $PYTHONPATH + 'layer': 'EuclideanLossLayer', # the layer name -- the class name in the module + 'share_in_parallel': False + }, + # set loss weight so Caffe knows this is a loss layer. + # since PythonLayer inherits directly from Layer, this isn't automatically + # known to Caffe + 'loss_weight': 1, + "inputs": 2, + "special_shape": [1, 3, 1, 1] + }, + ), + + ## NOTOP OPS + ('HDF5Output', { + 'file_name': 'out.hdf5', + "inputs": 2, + "is_notop": True + }), # ok + ('Silence', { + "inputs": 2, + "is_notop": True + }), # ok, need to remove tops + + ## THREE INPUTS + ("RNN", { + 'recurrent_param': rp, + 'top': "out2", + "inputs": 3 + }), # ok + ("Recurrent", { + 'recurrent_param': rp, + 'top': "out2", + "inputs": 3 + }), # ok + + ## FOUR INPUTS + ("LSTM", { + 'recurrent_param': rp, + 'top': ["out2", "out3"], + "inputs": 4 + }), # ok + + ## Handled explicitly (special case) + ("ContrastiveLoss", { + 'margin': 1.0, + 'legacy_version': False + }), +] + +#Helper functions + + +def traverse(obj, callback=None): + """ + walks a nested dict/list recursively + :param obj: + :param callback: + :return: + """ + if isinstance(obj, dict): + value = {k: traverse(v, callback) for k, v in obj.items()} + elif isinstance(obj, list): + value = [traverse(elem, callback) for elem in obj] + else: + value = obj + + if callback is None: + return value + else: + return callback(value) + + +def mock(inp): + if not (isinstance(inp, PH)): return inp + if inp.type == int: + return random.randint(*inp.param) + if inp.type == float: + return random.uniform(*inp.param) + + +EXTRA_SHAPES = \ + [(), # alredy defined + [1, 3], + [1, 3, 1], + [1, 3, 1]] + + +class Layer: + """ + Represents a caffe layer + """ + + def __init__(self, name, params): + self.name = name + self.args = params + if self.args == None: self.args = dict() + self.num_inp = self.args.pop("inputs", 1) + self.num_out = self.args.pop("outputs", 1) + self.special_shape = self.args.pop( + "special_shape", False) # 2nd input has special shape + self.is_data = self.args.pop("is_data", False) + self.is_notop = self.args.pop("is_notop", False) + + def make_net(self): + """ + Creates a protobuf network + :return: + """ + net = caffe.NetSpec() + + if self.is_data: + net.data = getattr(L, self.name)(**self.args) + + # Very special, + elif self.name == "ContrastiveLoss": + net.data = L.Input(shape={'dim': [1, 4]}) + net.data1 = L.DummyData(data_filler=cofil, shape={'dim': [1, 4]}) + net.data2 = L.DummyData(data_filler=cofil, shape={'dim': [1, 1]}) + + net.op = getattr(L, self.name)(net.data, net.data1, net.data2, + **self.args) + + # this covers most cases + else: + net.data = L.Input(shape={'dim': [1, 3, LS, LS]}) + if self.num_inp == 2: + net.data1 = L.DummyData( + data_filler=cofil, shape={'dim': [1, 3, LS, LS]}) + elif self.num_inp > 2: + for i in range(1, self.num_inp): + setattr( + net, "data" + str(i), + L.DummyData( + data_filler=cofil, shape={'dim': EXTRA_SHAPES[i]})) + if self.special_shape: + net.data = L.Input(shape={'dim': [1, 3, 1, 1]}) + net.data1 = L.DummyData( + data_filler=cofil, shape={'dim': self.special_shape}) + + net.op = getattr(L, self.name)(net.data, *[ + getattr(net, "data" + str(i)) for i in range(1, self.num_inp) + ], **self.args) + + if self.is_notop: + net.op.fn.tops = OrderedDict() + net.op.fn.ntop = 0 # the messing about in question + + return net + + +class LayerMaker: + """ + Factory class for Layer + """ + + def __init__(self, params): + self.name, self.args = params + self.how_many = self.args.pop("how_many", 1) + + def make(self): + return [ + Layer(self.name, traverse(self.args, mock)) + for i in range(self.how_many) + ] + + +layer_gen = chain(*map(lambda para: LayerMaker(para).make(), OPS)) + +filename = dest_folder + '{}_{}.prototxt' + +counter = Counter() +for layer in layer_gen: + n = layer.make_net() + counter[layer.name] += 1 + + with open(filename.format(layer.name, counter[layer.name] - 1), + 'w+') as ptxt_file: + print(n.to_proto(), file=ptxt_file) + + if layer.name == "Python": # Special case for python layer + with open("Python_0.caffemodel", 'wb+') as caffemodelFile: + caffemodelFile.write(n.to_proto().SerializeToString()) diff --git a/contrib/nnc/utils/caffe_model_maker/Pyloss.py b/contrib/nnc/utils/caffe_model_maker/Pyloss.py new file mode 100644 index 0000000..3abff7f --- /dev/null +++ b/contrib/nnc/utils/caffe_model_maker/Pyloss.py @@ -0,0 +1,51 @@ +""" +Copyright (c) 2018 Samsung Electronics Co., Ltd. All Rights Reserved + +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. +""" +import caffe +import numpy as np + +class EuclideanLossLayer(caffe.Layer): + """ + Compute the Euclidean Loss in the same manner as the C++ EuclideanLossLayer + to demonstrate the class interface for developing layers in Python. + """ + + def setup(self, bottom, top): + # check input pair + if len(bottom) != 2: + raise Exception("Need two inputs to compute distance.") + + def reshape(self, bottom, top): + # check input dimensions match + if bottom[0].count != bottom[1].count: + raise Exception("Inputs must have the same dimension.") + # difference is shape of inputs + self.diff = np.zeros_like(bottom[0].data, dtype=np.float32) + # loss output is scalar + top[0].reshape(1) + + def forward(self, bottom, top): + self.diff[...] = bottom[0].data - bottom[1].data + top[0].data[...] = np.sum(self.diff**2) / bottom[0].num / 2. + + def backward(self, top, propagate_down, bottom): + for i in range(2): + if not propagate_down[i]: + continue + if i == 0: + sign = 1 + else: + sign = -1 + bottom[i].diff[...] = sign * self.diff / bottom[i].num diff --git a/contrib/nnc/utils/caffe_model_maker/README.md b/contrib/nnc/utils/caffe_model_maker/README.md new file mode 100644 index 0000000..e34a769 --- /dev/null +++ b/contrib/nnc/utils/caffe_model_maker/README.md @@ -0,0 +1,22 @@ +# Utils +Caffe model generation helpers + +REQUIRES: + +* caffe +* h5py +* lmdb +* numpy +* caffegen in `$PATH` + +`GenerateCaffeModels.py` creates `*.prototxt` files for 1 and 2 layer caffe models +The generator can create multiple examples of any layer, assuming you add a +`how_many` field into the layer's dict. You will also need to replace the constants in said dict with `PH(type, param)` values, where `type` is the type of the placeholder variable +and `params` is a list (or tuple) of paramenters for generating the mock. + +For an example of generating multiple instances of a layer see the `Log` layer. + +`Filler.sh` fills a single model with random weights by using `caffegen` and creates a dir with a filled `prototxt` and a `caffemodel` binary file. The result directory is located in the same directory as the `prototxt` file + +`AllFill.sh` fills all `*.prototxt` files in the current directory or in provided directory +(-d) diff --git a/contrib/nnc/utils/caffe_model_maker/in.jpg b/contrib/nnc/utils/caffe_model_maker/in.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9eeef5ddb45ed01454256eb62a2100a638b2ff38 GIT binary patch literal 15556 zcmb7rWmIHMvTfn+?$)@wI~4Bj?(Wh+V6?v1;! z3KAL~79JiB77h*p5fvE$5d{$r4jB^}1q~eo0|Ooj3mX$18xV1|$>!90Cj!1p3R0 z3-T}Pe@tH*90C9W1`7GP4uA*yG9rT^e|^yf6kI0s>RBBL=M{nJxmVa9cV)08+!aUq z4qJ}n97lE3T33O^U3Klo%1p*DdS5Te{-_hOOuBE~R0|J3=WGAAsPkCGrRsdwfr<9Z zxPubTx)I-5K~HXZnmQzX*sFDLB|98+fM4*<=5H^~aae@qcLsYI%tWT-6G7vAeH#)# zZNap|P(RS@%qs+@z2BV1>6dN4SV-*2ANqssu8OqQi7eyMWf?#zz|&n{4-&?J^D$R= zD>+Q$71dPSC}Uxt%UJtrO(Zmz&FVi|>Cs&JK5G!rZ5LIQw@!jZS-H35>AfmeA--dV zowx2p^ig8k*5GLPVtu0#1-t##?_&1&n}t>#$|a`ZU8*u>B%etBXT>9}+-9S2cH8o$ zhE4T7wA00U&Ua0Z?{FF^gzvF6NBxzI`I3Hd4TeK`X<~AqS~-Ph1Z>(8?Gwh?a3ZOt z`uS@@d-6!XWY{~fip0H_^dVBRn)x$yJixBtAWDohQu1fE*NXd%Zf5yx@JwB0C7Q5; z17}9QA>Y4FGxSLfXflkWi)p*Z`<8}n8+vmjd%AtLVL;G5dF)- zq{j($UBh!pH&yRGZl_c(Eb}=K;_>%iD6ZN15D)?`pWlnkDz&P+-QJzK%IX<>Pw-4& z9Np`({*C)kE>)ND>r-){nPPAVM&4~H_gbl5d=6GgNjmaBm$ZHB)_Z+?v-e;IJ=Jdg z5_WU#D%2BxfXRR@(51X0VH3{agj`7){szUXG8H!2nc^Yh1T}P%M!pT1f74^eH=px( zeyN6dCN8$*&`J_P&br;Poxb&D)so&+sUc&fmB3prCq^gV`9l&&8)*o=U|zFI(nfxo zr^v#OVR7`Abk!r~VV5W~I*BH`%A;x)>U)=66=LuSZpB-zAu-US#ECky^1e9K3ena( z`QTd+CoQpU4*d00nuX0RA`5*%(6ed zD^`#~o0l6S5PQgrHhZ&3)91*vJI$AfGFk1BIu(>K8q!)W#5U}7FXC5y-!^oucYS8D z6Sw}pSP?qBom{chULe@?AvDrm#0PNC(P9U!!{5ulR2rE5(YJLEp3U;gI(m$*>QbdV zaTJag()4_Ercn_dRHzjcyx?)|=Qb2`nl~Zd9{zX^(2#zgDo;|HTNd$G+lvO~7sT=4 zqo*%fK2y1FI!dc=3?$WmOUn3`DD}Tcrn3C$S5ja>27rKrfc}$?zEa0OxdH5J1_6LX zK}AC+!N4SCA!BA`V@Li*E&?S?Od%xtl`NpY5)23g=n~I}LAY1rWV)S&33kGLNcMnN z?KI3$72T~~tXJhE-?CY*b>l3%mPKZD)3mzQKL)kbe;ceCXW7;)GHsitIkb+l=x*gw zy-Fvk+T&%V4`b_J^`c%i6l<7La`FV=u89=VjO%|*w{@K`J%pO z9BSzCMSSk)S98j22OKGu`)%u?*oM6^F6r%4xjGust{0W;&i0YU@KOSvz(yynwz|ry>=mz?lYeBIy??HUT)kmi1`8OV_|o5f0!c-)dkf~9sfCC*El?@l$p^|+Obvz+gJQz*N3inEMWkd=eI!{j7k{*TSr0-UD?T*x+ zO788EC6u)wZAhP$cdE)kL?dgj(=gGE8S*1NSgmdf(xePXlLv|{c9=fQBB-h(X^Lxl z$LMB2qLqzDVFchZ%rw4*i1=vy+#7$rydOSU0#)adF0U3>gZ<_M);eR({%VY8Ym;Iy zGhQg=IiZAw9X^HSamq~qEHifERakLV`jE_h6&zD}SN7#L@b$kZ-1RXNZbzIBiqY4H znaG&2LCYD9rYYr-XO1Oph_ zi;B2xd|eOo(fCg3C(ara(~^j~P+m3g)o)TG6#|Q-0vRQZR@+ysiEHtmh-|BIoXppRj=Nc;i9^ z9l7fitpLbY6Bia04Ugt1tvGV(+fVUCmX_iTc8qFe;YFLbJWy6p@_0)!;h(OfEm$1R zCNS5w%gVmAkLd?bF7xWc+o|&)O(q+9w}cQ*MBJMd_AbB<<)&P(g)9S9l`}H}*SCB% z4Z&ZPWx3dN#<}14`6_>^Y)n$*LUFjDrSe~E5bDKK-C(X}$rn)Eegd9n&W*>SS+umq zSap{o3^e}Q4Y=IUsbw0)^Y~S=?oid%iBgg!GM7>PA{IH!mgg(=fJnJ4ot2YVr)`QN zd<)Y$VO4@~dzRyRKjXZUy|iL9C&?IkO?`+QCTkatP${yRU$b%fojR|#3b`jGen-ve z^T(;Llau0`#!Xbk@SuY(cpQP#WQtS!h>d5XiKofthl1Rwd0!LA!} zvJDQ5yjR@PN(~&%u>V<*J5E=BG50qu-M*nE=k_vQwSrv2o^f7g$Efu#K9mHG@rqZQ zpPvBM(xjz`lX{CAOiImL;-Fkb?mjnhs%i^8%_7KJWjYB*LTawis16RBM)Wm=NjwWK zntSGA&Tu!Wnb5X2g>0j%INO|uUl;2aAzdXONd|9IziA>X&W+mP2}Ke)S_i(L9hQzq zQ_Kc%n#Rg4mMK>Pi5HdSRPe$~S$XF^2%Me{pE_-SneO~t67klxI-#l)zgvU0&HPnV zwDdwTURfTL8wwa#!_}9EGbWDv67_ zhtwefD=mQG*|${Kn;Q1%hBm7iDuy65e0hS4&wa&t!vNv!6iovzaH4C&24*`)J$Fm< zd@L)$q6IlW<+Sc~AVTcG;k=KXyEw{fO4fK+&)*%!VzP7a3Kdwah3;oHTQ=1$bP5r%HTT5Js|5I-#=fry@PLdd?8rJ=Hji$vDhgdo5V=*MX$rO4xDo<>7q_1Ll9q^uoVSxxhs7AyTJPD7v2g5u?j zVO3B;87qU&PQs9vslgKgVo)vNFT0QE)kv)k;gsz2A4C}Uv6&6f_eJ%vvY#h&?V!W= zC}!}b+=M|U5(^_0j08(LXy~afq#~J^;=v=+^XyHg6v0(uH*)3_u{9&d)<9wfj;P6*mIJ@;O1H^wqgommE(^nm{qEB#A@8cP8`PudPb$ly+!IxGm34F zOzH6uQ4Zfe0kYHvP_eTt6YW2m?zZ&Ch|V*&IW8--_mlHd;ycTNvj`4*fxHt2jYrj# z-+q4r9KPa)`YUd}0tXxf4B|gD6a*9n3>=jN8I6>g1)WTZn2A+5!02B=1Nu+UOeXy# z7@^A<+fjJZ@}<-Lh${3>vu87!?u;bV<~_UY?ccHR6PZ|}mo0iHQWNZtQ#^`23oY&u zdro5eSYwDp$f8;o%j`+GN=bJ-yD>K~-D`zkcr}ge$sG!=t{Mm0`nD?-59DpeZ!g<= zadxr_Vp`?VnR3Ue4yjz=kZ+oDMY7}AodX4d7W>nTt*r7ph2d74^LL4Ht%9x{BD{w{ ztFthuCZZym=4r<9MCOswDK3@6f+e-g=*e%&5te~F5k-XT5z6c=o5x>^C`7T%jwc>! zl`gI;$4{$kFoL_kc^OAJ7aNBTjnc#>o1m zPssu0%lFJ*&KFiBDfaIJIuqmUKT5y_04Xf{VZoP zx3}G8oHvax>0gTf|Ee|Kw0~w_>fg5iO(iR7fBPjtR=d0wV*6aH9#M!@L&-3Vb`Fow z9Mq0Ocg-O+Ri@A5x+QMfEIlG&i^SHMGvP3Q;Aop5I6fQLnqh!FSo?@^klNj}PU5P9 zyt3^f0}pvId1j5<{F&Mf?dweECcdg?nv?%Nbc$C7l#_X)!DC6mazDqenKFD zxU6|Lt8q}@J{m#w*O;l-ccD&}@b^l6#6)HV^+46KRAA=Js$Nxk7;h|&aktDO(b17i z=}*&jqQFEihW${ixS zf=^wv#aS31T8A*?U5mB}*3oh7y@AXH=12s0vZc$L_F`ikHbC>kQl5Y&5BtAp;dV>ju_Oq zB&2b_Fd2gxE3;zQ#BjaRJY(&uWJN%X1cm{xMV3ZeUN&f_!G1PhQBgk7%k*S2Fuiy= z5{O1tOuH36B8i|O3ZV$!tJkHgBq%JJ5>QLUe*$9gr4bCf$x=@EzvaS(#^O>oj!R3f zgY{id&rL8);&klh>BFRkrD>g|94=bD%gGr<n}f!RfB0)ccx|`pDk!STJ%vR!zYXS>9fx%3&u?c-idP0%jsC!hWu%(h z>qd<1`_)8`4(GpHk*7GP*GpW3#@#mx2Oq1D@ia*m6}PPKLCUJ8hw$G;Lgl8u>upB| z;VZEXZ?x4iQYC(u#tJvwM3ic#Q0ie~QdpW#0E=h8(071XR8r|fqzsJGqkpD8rH)F0 z)TXkb%bgTdT^X4c)02Fr7&(14cxL^sx>-LG$)RGkA-^yi?pY-~1&)_5*&=1?+q5-5j3R z<(dibgXj?SR;MW0=ZDzFQ`i2A0~OCQ+M0fnd!Ws}=Xd$*LP^|n+nz~gWifH~7#FKM ziOP!Thb{3i^1@@ylV~DK&nktHuh3ItBPdT_{WI&c4+MbQt|;T5iN?@CX*_Xkg4a9$*lvmUnK>B!xKL@obd0xGw=v8)PwZ5jOra@sak`k@F_ezyJb&dC1={^%O2sE9u){7*vcKSK1|G9&Jm`X(`PB|k- zCp&>PuoEWNL1hgu(8-LYUI7eZRLjY56SXM&ZRX$0nidNf<3*8d-+#z{Y-d< ze1R+R55b{KUo%CDV)=14EJ-UTW}A5q-+Xvx|0tXOhf2(TPvkfww5V$^FB@-54~b5X zmE4oYJgQTX-1-HLOU@0hK&1ECW@HQ>qEs;4Tt^mnWJO5%-yz5BCekWR9k+HC<|Lhb z!b*zzOpL&C%?YceuC=c48UCsoKbqNlf)^9QD^bx-iE#B_Dqz%dMl+fCrsRvfRBLng zj~Ex*)YDlBtkmnpaW8bEn$CqGA+?C{dR^NAzvWKV$PushlETopTf|++bYa2tf8S}g z>mky=P*X;xmbPuL9{NEu51fXgQ%}s`WK>-0*lEoRt?y|cGqADPTVR;4j8_F|@}oJc zr^hnbjEM-;pj+J~#q@B0ObA{3#;x&&D}fG>4eUY@R;&8xO}a7HI0Lg7kK?n?*b~m< zd7z4UIfG}0wp);T)@$n;d_XW7x*~ej=u|j0=sda0BAdWI0gChe;uInrloKA{7R@|w zGm~4eQ;>f`4k_$eFreU~j*HAZI~&+zz%M{pFthr^w(>)qw>^u1FD#RTfj7lB+=0Ae z9@x(a%z`Z{zzGI{fw9W52n|op%Y=uNHZVxcDSqUXz{#AONddj zUD+xwMHDAn1l}FZ;hs`K5{lwN4p)D-)zFcg+C})dmeePpp1SP#bfSLzml!Wv4{!Ok z{3)#k&s~z)#N5<|wi>q0@rF#ky^y`APgvqMxqcFkK~&E&qV!i12Hs#!R-z|K&2x$Z z?+J2h?`qLIP%37V)mCmq8W7g|YHVL`#m(Y_CPW+fV|eMkmsYDW*TW)>L|2)%YKkFMwglS6^YG@^vT@tVLa;^knFcOS#+zdrI?;3 zQ`fNSl!nYCX+l=(iE!#L_zU;Zcq-KeTBZczvv6j9l7e2h(7## zzaTLc@)rdD;?Z9$`rmyy5CF;-g;p|h3P>oJzd~;4-@9h|5AX#MBryrGR;t|I;t{_a zj(mb%)J=APfiC`Y2Z$Z1jSJ4x*kf7oW84$y9q@Gse)$Bz+zX(c`jGEP{Pk4`e#!gS z^o?@+Nul>D@5i3NCt$$$6A=9R^~T2!crChn@-YGeLjUF| zH4Sx0W2bp;6WRVLo{YE6uWNx5xQtE?71^OYuw!0py!5V28b{~lZf+7+3MZ^IVJlx^ z>HsIu7b2MEc}+MmD##EOd2`YINbO$_h7y(7w7hc$s>ny&7&m6gl}7C+3NlVoz~x)-Rp>7vcjvP0fzYk^B4Xj?65C1OM->z-* zkc+kH2gUC95&{*65q)y8NoL8RiZO?FT59qZqHWVV3bWm-iI96k4v{?7W)8jyUgywT zP|%JkG&+bDr^)tc19{%}^L0*rRL1?M|5HNQr)T3ROh!XG{-@M)36Sp{)xa>0XK-%W@k2F5Bna zUkKeI)m(THN}{a!C!?CL`FlxhHE$L?czzQY^P_{O;u` zioAFfJ0f!L_8oN(VsdgQKhnuvMwu#PZ^6js@PmxG+eRG4Y%4GRlq!QjA#NJLQ-18aE= z!xw+h&Z0iF?hW?cw}F$u&0j?}=%>0G&-K075Z0Q1ru5c|a%z^&dnOFlj}riW4>ZOx z-tejc`>)AnaGIRB2eN4mvW#AEAiwu1TSjM*_d)3D zC7IM#uhLjbdj(-=bH~2G*y=z@c-CY&GPUYWE}!706_YlI{Cq~cdJ#bO?LM4ADj*Oy zw#Z6>NJ^w(J`ic4>mHeFuwiMbXmhPO<%MMndLt9ai=shNu)~G^1U(HKZj(o4mFrM8T4{TDh7a1`nY^v3^H)o zT`*LSB82W5j{7vJX2|GXwJ)GM6*`~pFRyMi9=?EAr&+SIGoba-o(@kK+|EZvnxA+y z)|{Yk8xYmtLzyOb1n2R}p?S{|2pFITF0|APnL|B9S&c$!za#J~N3^9GjlPGhLQ=-{ zD@Fk={VA#;ku*O6+dfaB5@OhZC5k{xm20x}AsLfc@&dDWD`DT&(PIUn6iDbSWKLCC z4Jqv^`I?b>KN_m#>mg)B2!!oSH7CeF$}&r=@2T}V$+gJInxU3# zomJK~u}P556AFsoCxCW;WAn*VRlDNW z3RgmGc{T4b&rfeVc0?xc#C;iFBeks6yBk==taMyK*<>kbEqfgt-F4N8ktcn}6;EHp zVz8WqY{$h{vt$YHitMtb$T4?jQ7}_w;z&?n0FUEk`o5u>-IB zEi^2V=I1Z|gz_}FfSbR5Bv1>HC8at?-Mzu)>m+j=-JlVl_1nW>xfJmm(+2Qll_M~T zzOwC;P^FyQoAf}lX)Y%<{(hK9Sc1v3HuiRBW$CN#Y+}?V=k%UGx|zttc4k)Hp@b4+ zuhTq?2PFtoCT8kJue)%VORa-AoFi|kobW%oj`-#3?&ztmxeYZZ?xGn(zm$t~VeB@g z971!hyy{pO$SIy=&z@mm?5ha=!%W*qe9hZEnZwQ#4z9wQxzEch0$)A@GW!HCE1ac3V9Hi*v5f~upe?O71O7mVwWuHlh$ zN{q9IIXfwrQRj-8LpXwcm;bGXMD=L4eva$#lYgNWB}HY&DHXCywAUz)41DNBtUUOf zzxEv{&uae1{&eizLrajRfe3o}17oqS<|FEfKr~cp53!68CY$$kNEhqv>ywMzUeDaz z@xxJ4v%S}fs$~1f0;8AO>?p!WO%Z94HbWAs@nS;rNoSV;o28`)q!QOBtPOe3#s~(bAxfEQUrk>sNNGv-uhs;Cd z+2uVI`#!BDO-j=&nNwFnP>XjGIG4i(jSIQu5#hYLRNpM$Z5XeU>}<(ach*8d^9`%q zOhI#Xz23TIwwMMtaN%5>r!v0nwm~4Yl7kH=+tWQWjM`+MShUVB<`SgS5$bF52~g*6tyv6L zMJP%x#HCWp9J5;}%(^$NiPA^C!V@*I+iQpHY)|p@gSA$J8&D5R9{Q+-4_Rgz7#VvW zq5+SB6(w!eN=+N#4*m-rmf=HR>6#4nZJ=H;2^I-MVah|^CUP#Fq9hMl99#zuM}1*I zQ=uax1@TGzbqWNVmEagki%Q1B&4Q?3Sq{e2oL@20SnpgwX7EIXTCWKv(_`2L0TWFb z)BGBd80tAbpj5sDca}#i1275O3_TEXFS4HrQTGgzUEsa*KXTEX|*zx6@DFs8g0vk^FYgRdREx{ zX!smORaM4BfK427e77-5`msv0hxvrX&j&cOyA0C}G5s=gd@#AKaFo~(xms*E-7$-+ zz8*G+UOOEUm$u0KX22;*^4!HHvNTF-+by?u=zn?5%WT64aL%NwB(Io=?D? z!dFfAU#H6e{}hCgaVB1m^jii`p5QFsmCOD8;)M#D-^O|19k=_EoclGlQjW6I&7i!&3sZ)KucpPybs0FWp8r*Qd z4Z^b8n%5b>BGiJ7N8d8xoEzF$A1C#Js$FBWDzk_KyXBhmJ0%f zpQviLotAmee@;FHt@s20+MpnyFxs&;7$pVY1j-KnBgX=TI}%H?dT|w|-|sS9n)E1W z9bI3-w4}FLIiRe9qk^G3hO1=tQ}}R{Tm&c-{f~e>VO|QNQ)ZMG7{I01{N~^S5fH|9I6VsyR#LaN*0m^SXwHb0_L`BqdGu^*a-ujvv zrRTi$7P^A*O`6rQEV7y*CbpZ)*N`W3QFgN-hv8V1mNnVCjmWNS_hEVIv=X%Fkk!^R z;Gy&>aIl=gj3tM0NR*cJ;8ePf7$eS;#2n53LcP@@DxPf8Y73kJ!N+`)UKWjLF%+r+b~`E+K%oaiKr!!-c#9+>8&j5hsp?YUjRD4eURl4 zq12+dzKx);sJh*=WFAZ9tLVj{bsk2;gheTry4@zCxC{DqBOcjLvw#^r?g(AS;B5|%C?gc zg{8Z&*^kurjY4t2`~}-3JLOmLgHV& ztba$ue?tP<=_;Y1VV)_V|G!-<l^aN>)YYcvp&n0AoYL*_FwjafxDgfLm-N)gDiy@)v--Lh*iY#558 z_TpuT4WZ8zIep_6AxUdt5AQ*v_AyDvAkh5SRW0_n*Cw;xEjb|x8X9DxvAV;5GPVKB zn1@sR>q9X`8DZjN24-8qCqu=I9wL~xR6dWq&&eHF=3F9hvCI0GY|t^7w5`?;8JLlp zwU5czTz+t&IW*KjAsyzphL&-J?vXu$)7h z_!tBU=Mtey>{?0@$a?}YJtbqKL~PML-_tEB(h_69au9<}oz653O%SSs=SGXRj$kW- z<%sQNO*Umt(2GJYUxubk+yh>py&J^?tRg1X$_gm1J)x0O+L*F6LFP4Mz_!B_zd)?W zQ^D@TuqT_-tM4euIeBYR`En-pKRo=)nc!ew2i*R(#s9m%zjk*+|NP$7H8S!4L&T8@ z5oPrd5VZST9avA)I_!*mZLpRgUcv|}TzOmH<+zBtwbniXB4qE6Z$FNcek>HPz5lpo zYmw8mQvC#6!BUld0)#o{#nF}I6rQpiEwTJH%&$RakY+RD*V*v{lib&YeSnLKz{5k@ z;=|A|WF_-Co_ZT}41`4{$Tnwcudk))xEmLlR1rH^u_=TmOO>gfQ((A?)HV7_v+ zL7WahSB6e)h2<+j1s3br)24fmWq% z`Js}(F2AUZQTua?WBxUo9aG9Uezw1?3qQuFV{@IRx?P3UpX+uh(>yrep>A3sYP_-M zv}SP^5Eyp#Zv0JaW~zZ^iq#|IShtKAakWQ!lZlaC@Iqb#mN$6kN17ib>4D`5rZgha#E>&b$s=?pk=V=`oOk zFh|pk0lV-lXrdgLnyz)Iv@RV&jo2jvjz@Ak)O;bTbNG9~Ts2z!_nlQ}>a;Zhr&k*2 zl|3?=6O&nzcf>xlg#~37p;a;HRxXptq3)U7Td7fw^4afJ1KV}on#Qsf8EcQ>9~@1T zmNaBo8nY^~0okIPa@LwMz2H58bNPd`%No14C=i$`^ZObobC$R1CIpN5>l++o!XVVKA>E9TB_|NvE5-)lBeg;(3|wY zG+Zo_WSFX>)qBWB-BfoE(|~hBc2OO4BK9lR3%xG=C(4(4;WrN-6P5IbxZM!Q0p=LY zKN?CN{1-0O-$^xcAh6$t0MPh1!g6HX6FXC*oDtMrNYg#x=tWKLoq9cI2Ox5XV!pT| z_1JgxI{5{UKyD3Wm`rj{$OqiGr9ES~@?mwZa@UynS&}EgA=#cG(CLjM{T?KuOD7jp ze9v6IQ5xlC;OK@Dt&!f7HGYe61B#pbQ9VPQbD|M<6cd$oWeGDFt}VI1z%EAht&YlK zgucFdW^T7ch|vLGZPiS-86_2T*AZ>?ZJQiF{vXNHUw$OY{m%xQHi%^w&zB&udfC#V zoDPVHnK-_p47kZ>?)*$Mnr7FaibK?BPzHEv|&aWf{gbJ9XfV z${*ytQeqQ=tZD7r6-oz!j_@I^K8J`#Du_kjI0vCIU=4SxvjQ1n!Dx10jUb<*iX5_t zg&HrDsv9dW%}N?O_|79qGK8-Pkb+h~umkq_1Xtd~C!Vl{&K*CT;1HLLsgEPUV!`Qn z^~n#26`R!_ZdvnFqVT;%h{cCiCsUlSK^uqjn`2(w_M!lDfGIdaQjE%7ojS=?(fQ%5 zv&N|SIYeV9tH@lCB_TF+vBAsf6HL2P?#%wk2>Av!V$ETy@-@ZD^OOeCGT2rEc4>B{ zDnxQ9u~e0#E}}S3>s`>$87`2OcEg8c@xz~d-7C;Zm~&Il>O@|p(wA7FUEPgBB;R*` zRHM{G$(0qg0n;$^9Nr1zp>?AwA8IvHwV>&Pz#$=Q_qV>|Jwz8rH^cbif~7H{&2Y@4 zt@*(y{KA2rd%1@|rpNH#*VeCOj$7Cf?b>7X{VrVu*=-%H5>=6TomB5vV!#DPtiwDV zzpkrG#qFKKUaNMk53Zqz1P@mmx6f(_wR+2#AD9!KI)NcKS2K^p9&^}<0AW}CbOa~W zZ7>=BAFr2L z!8xwSN>|LJa_^GHpv*p9gQxn1Ojb=G;m#c=qLuh78-bmCsy7*MSs}dA9w4x2R3D|m z5~)ZS(Ib|Uoemv3-L7PCi;yot*UeQ+#(OlA7OzgEfkIcy;J_&U8@Ap3DHFzQbJf8! zAXxYef|Z6u3rLC_!h%VvM_V*LrMRMvH!T#eRlxGSU&NAqMn@hJw{xRnzE4t5ZGpsh zaDpXr3@KotY1v+9g-mp|I^P?H4=T2#u%P}l0Agdac5TAGNG;fe^$)G@1!T2Fsxc^T z`!CoPGktPUQ*R#LVCEM7qFo|$u0^XXjxc%O@O(LQVqnN&<&_ZI91rM@3PPrLqvI&C%=M}wcC?Okch`Xd;-iecw&&0@elU9 zScwLp5a`a^p`s3C9IhR|W3<65H=M%EYxm`PI>eG&H75Nfn8N!B!f^Cg?WYOU6ffz` z0Bu+!X+H7fYTKKrlqPS1%RK@(z!fKm3wFe!y)b-F0D6^JN*}=ZIzn%9BR*(wi@^d$ z=>*VT@QLQbNA5~#CTWW1DOW6%l~iHYvR}7g9enJMaQpyX7=F)Iyqqbg@g zs2UV5YZ_u6CM^$*(u9bKDOd<#vb+uIThWQ|B0VZwWZqDG<37?8-ombB*`QCV+{Gu# zN7{s*gU}=rPA^i$h_S>XY+|9ARZ9B=AR(8ik5Q@sjarFu|c~FjUMxlm)Wv~}Zz}FYQ7Ny_aaj3$D2o`BlA(OrQ2_SCiMk1WU1z!a9 z_o-25ZL4u6<^gZ<_UCk77nf_#gM-zOB2ep&!ckWxrO@+Aqd%|%=wWKG{jAXZdF%n1 zA-N8@f-E)xxvupTOw$2dVWU2isguSAsWqv_Hg9*dbH=vev(hU( z1$IDP5e0|j?MdA-9PU_~o<3sF;nK*fVPoRzpP4+ntrol=LS2@0U{Ss0w9~46FMP7H z_$!60ct<4#cCJIa4Q4=^ttw~RDDQTF|4Vwq0T4Oxik~x}T4$AlL`7Qy_bD5quO2hj zD(Fa);zoIwYvj;VpB}RTGxap8zG4@pkGR{}RgQi})w{dlTPOv?W)H7M zxQgs@RT@u;LLhDdG#28ZhI;*bpW+_E9aM0YO({FDR~oLkTzUeYMwk+=RpKB}wkM7A zDG(dwn2OMYmcAKmD4(>!gUtbdcrYsL$2UCWJu8}LXFd|Ke7BDgQmCNtzn^B&eK2ditj*i$9tY(@Y_!DM*OgTpcTg&4a*@{QDRH9Rnt+4lW4{M1cT^ zdfsXu@J5LV&=apQ)h)yKDH7$V^uk{mimi@a58c*}Y3<1RPIc+<6f<}k;VKOnhOr1l zH)bwIt_A}03kx4B0&Fi^^+I5=IxWxpsC0*Drr7a&)gd#ZI4&h#E$KS;8% z$4>FQqC_LLrwVx#%SKv|u=&G52$~s%N)Ho10U;U;b0man1*xqKzN!EuGe7^+sm^wy z#?!f9Ly~&6Wc1O>w5P~&tM8&fP11lyBp8LIAG9IB!3!z^i+CkC2G4uv>%1vH7HZ4t zI*B9u8?rbpB-kMnz{hkpf9%Z>W5IzHI0dKwp+Ff+4u4U!w*wo3rM-bV=oZ(j#?;Kq zD|N~1@FS-46pd?)uQe#RvcI^U1Nu>sNC~iP#%orb z8`CRjAbh{hLUMjKLpB=e#!z?;-pQJnL}$tDegw~?5`PH_2zqzJ##`+DLxd#yh8+J@ zp_UWI@OW_p40`7sfLAU3AZ3ES0P$#;1JT9CHgs0Cc_O_#ON;wUVn=6l??%6|()hYm fT3EU1Cz;vB9win+kImbDSGx1{xQRgW=i2`P8)OEO literal 0 HcmV?d00001 -- 2.7.4