diff --git a/convert.py b/convert.py index d7e9f3a..5eb2fa6 100755 --- a/convert.py +++ b/convert.py @@ -1,13 +1,26 @@ #!/usr/bin/env python -import os import sys -import numpy as np +import os +import logging import argparse + +import numpy as np + from kaffe import KaffeError from kaffe.tensorflow import TensorFlowTransformer + +logger = logging.getLogger(__name__) + + def main(): + logging.basicConfig( + format='%(asctime)s : %(levelname)s : %(module)s:%(funcName)s:%(lineno)d : %(message)s', + level=logging.DEBUG) + logger.info("running %s", " ".join(sys.argv)) + program = os.path.basename(sys.argv[0]) + parser = argparse.ArgumentParser() parser.add_argument('def_path', help='Model definition (.prototxt) path') parser.add_argument('data_path', help='Model data (.caffemodel) path') @@ -17,19 +30,22 @@ def main(): args = parser.parse_args() try: transformer = TensorFlowTransformer(args.def_path, args.data_path, phase=args.phase) - print('Converting data...') + logger.info('Converting data...') data = transformer.transform_data() - print('Saving data...') + logger.info('Saving data...') with open(args.data_output_path, 'wb') as data_out: np.save(data_out, data) if args.code_output_path is not None: - print('Saving source...') + logger.info('Saving source...') with open(args.code_output_path, 'wb') as src_out: src_out.write(transformer.transform_source()) - print('Done.') + logger.info('Done.') except KaffeError as err: - print('Error encountered: %s'%err) + logger.info('Error encountered: %s' % err) exit(-1) + logger.info("finished running %s", program) + + if __name__ == '__main__': main() diff --git a/examples/alexnet.py b/examples/alexnet.py index 6e65aed..9eb3f00 100644 --- a/examples/alexnet.py +++ b/examples/alexnet.py @@ -1,10 +1,12 @@ from kaffe.tensorflow import Network + class AlexNet(Network): - batch_size = 500 - scale_size = 227 - crop_size = 227 - isotropic = True + batch_size = 500 + scale_size = 227 + crop_size = 227 + isotropic = True + def setup(self): (self.feed('data') .conv(11, 11, 96, 4, 4, padding='VALID', name='conv1') diff --git a/examples/caffenet.py b/examples/caffenet.py index b8ed474..fa76085 100644 --- a/examples/caffenet.py +++ b/examples/caffenet.py @@ -1,5 +1,6 @@ from kaffe.tensorflow import Network + class CaffeNet(Network): def setup(self): (self.feed('data') diff --git a/examples/googlenet.py b/examples/googlenet.py index c0d985e..329936c 100644 --- a/examples/googlenet.py +++ b/examples/googlenet.py @@ -1,5 +1,6 @@ from kaffe.tensorflow import Network + class GoogleNet(Network): def setup(self): (self.feed('data') diff --git a/examples/vgg.py b/examples/vgg.py index 7eb59d9..3318cd8 100644 --- a/examples/vgg.py +++ b/examples/vgg.py @@ -1,5 +1,6 @@ from kaffe.tensorflow import Network + class VGG16(Network): def setup(self): (self.feed('data') diff --git a/kaffe/base.py b/kaffe/base.py index fd68a5a..e9e2a0b 100644 --- a/kaffe/base.py +++ b/kaffe/base.py @@ -1,19 +1,21 @@ import sys + class KaffeError(Exception): pass # Ordering of the blobs -IDX_WEIGHTS = 0 -IDX_BIAS = 1 +IDX_WEIGHTS = 0 +IDX_BIAS = 1 # The tensors are ordered (c_o, c_i, h, w) or (n, c, h, w) -IDX_N = 0 -IDX_C = 1 -IDX_C_OUT = 0 -IDX_C_IN = 1 -IDX_H = 2 -IDX_W = 3 +IDX_N = 0 +IDX_C = 1 +IDX_C_OUT = 0 +IDX_C_IN = 1 +IDX_H = 2 +IDX_W = 3 + def print_stderr(msg): - sys.stderr.write('%s\n'%msg) + sys.stderr.write('%s\n' % msg) diff --git a/kaffe/core.py b/kaffe/core.py index 7d01c64..5f93aa1 100644 --- a/kaffe/core.py +++ b/kaffe/core.py @@ -1,5 +1,4 @@ import os -import sys import numpy as np from google.protobuf import text_format @@ -7,6 +6,8 @@ from .core import print_stderr try: + import matplotlib + matplotlib.use('Agg') import caffe PYCAFFE_AVAILABLE = True except ImportError: @@ -28,16 +29,17 @@ print_stderr('Failed to import dist protobuf code. Using failsafe.') print_stderr('Custom layers might not work.') + class Node(object): def __init__(self, name, kind, layer=None): - self.name = name - self.kind = kind - self.layer = LayerAdapter(layer, kind) if layer else None - self.parents = [] - self.children = [] - self.data = None + self.name = name + self.kind = kind + self.layer = LayerAdapter(layer, kind) if layer else None + self.parents = [] + self.children = [] + self.data = None self.output_shape = None - self.metadata = {} + self.metadata = {} def add_parent(self, parent_node): assert parent_node not in self.parents @@ -52,8 +54,8 @@ def add_child(self, child_node): child_node.parents.append(self) def get_only_parent(self): - if len(self.parents)!=1: - raise KaffeError('Node (%s) expected to have 1 parent. Found %s.'%(self, len(self.parents))) + if len(self.parents) != 1: + raise KaffeError('Node (%s) expected to have 1 parent. Found %s.' % (self, len(self.parents))) return self.parents[0] @property @@ -68,15 +70,16 @@ def data_shape(self): return self.data[IDX_WEIGHTS].shape def __str__(self): - return '[%s] %s'%(self.kind, self.name) + return '[%s] %s' % (self.kind, self.name) def __repr__(self): - return '%s (0x%x)'%(self.name, id(self)) + return '%s (0x%x)' % (self.name, id(self)) + class Graph(object): def __init__(self, nodes=None, name=None): self.nodes = nodes or [] - self.node_lut = {node.name:node for node in self.nodes} + self.node_lut = {node.name: node for node in self.nodes} self.name = name def add_node(self, node): @@ -100,6 +103,7 @@ def topologically_sorted(self): unsorted_nodes = list(self.nodes) temp_marked = set() perm_marked = set() + def visit(node): if node in temp_marked: raise KaffeError('Graph is not a DAG.') @@ -133,6 +137,7 @@ def __str__(self): node.name, data_shape, out_shape)) return '\n'.join(s) + class DataInjector(object): def __init__(self, def_path, data_path): self.def_path = def_path @@ -166,10 +171,10 @@ def transform_data(self, layer): dims = blob.shape.dim c_o, c_i, h, w = map(int, [1]*(4-len(dims))+list(dims)) else: - c_o = blob.num - c_i = blob.channels - h = blob.height - w = blob.width + c_o = blob.num + c_i = blob.channels + h = blob.height + w = blob.width data = np.array(blob.data, dtype=np.float32).reshape(c_o, c_i, h, w) transformed.append(data) return transformed @@ -183,10 +188,16 @@ def adjust_parameters(self, node, data): # potential for future issues. # The Caffe-backend does not suffer from this problem. data = list(data) - squeeze_indices = [1] # Squeeze biases. - if node.kind==NodeKind.InnerProduct: - squeeze_indices.append(0) # Squeeze FC. + logger.debug("data length:", len(data)) + squeeze_indices = [1] # Squeeze biases. + if node.kind == NodeKind.InnerProduct: + squeeze_indices.append(0) # Squeeze FC. for idx in squeeze_indices: + logger.debug("index %s", idx) + logger.debug("data length %s", len(data)) + logger.debug("data shape %s", data[0].shape) + logger.debug("data idx shape %s", data[idx].shape) + # logger.debug(data[idx]) data[idx] = np.squeeze(data[idx]) return data @@ -194,9 +205,11 @@ def inject(self, graph): for layer_name, data in self.params: if layer_name in graph: node = graph.get_node(layer_name) + logger.debug(layer_name) node.data = self.adjust_parameters(node, data) else: - print_stderr('Ignoring parameters for non-existent layer: %s'%layer_name) + print_stderr('Ignoring parameters for non-existent layer: %s' % layer_name) + class DataReshaper(object): def __init__(self, mapping): @@ -206,7 +219,7 @@ def map(self, ndim): try: return self.mapping[ndim] except KeyError: - raise KaffeError('Ordering not found for %d dimensional tensor.'%ndim) + raise KaffeError('Ordering not found for %d dimensional tensor.' % ndim) def transpose(self, data): return data.transpose(self.map(data.ndim)) @@ -215,7 +228,7 @@ def has_spatial_parent(self, node): try: parent = node.get_only_parent() s = parent.output_shape - return (s[IDX_H]>1 or s[IDX_W]>1) + return s[IDX_H] > 1 or s[IDX_W] > 1 except KaffeError: return False @@ -224,7 +237,7 @@ def reshape(self, graph, replace=True): if node.data is None: continue data = node.data[IDX_WEIGHTS] - if (node.kind==NodeKind.InnerProduct) and self.has_spatial_parent(node): + if (node.kind == NodeKind.InnerProduct) and self.has_spatial_parent(node): # The FC layer connected to the spatial layer needs to be # re-wired to match the new spatial ordering. in_shape = node.get_only_parent().output_shape @@ -242,6 +255,7 @@ def reshape(self, graph, replace=True): node.data[IDX_WEIGHTS] = node.reshaped_data del node.reshaped_data + class GraphBuilder(object): def __init__(self, def_path, data_path=None, phase='test'): self.def_path = def_path @@ -255,7 +269,7 @@ def load(self): text_format.Merge(def_file.read(), self.params) def filter_layers(self, layers): - phase_map = {0:'train', 1:'test'} + phase_map = {0: 'train', 1: 'test'} filtered_layer_names = set() filtered_layers = [] for layer in layers: @@ -264,12 +278,12 @@ def filter_layers(self, layers): phase = phase_map[layer.include[0].phase] if len(layer.exclude): phase = phase_map[1-layer.include[0].phase] - exclude = (phase!=self.phase) + exclude = (phase != self.phase) # Dropout layers appear in a fair number of Caffe # test-time networks. These are just ignored. We'll # filter them out here. - if (not exclude) and (phase=='test'): - exclude = (layer.type==LayerType.Dropout) + if (not exclude) and (phase == 'test'): + exclude = (layer.type == LayerType.Dropout) if not exclude: filtered_layers.append(layer) # Guard against dupes. @@ -280,7 +294,7 @@ def filter_layers(self, layers): def make_node(self, layer): kind = NodeKind.map_raw_kind(layer.type) if kind is None: - raise KaffeError('Unknown layer type encountered: %s'%layer.type) + raise KaffeError('Unknown layer type encountered: %s' % layer.type) return Node(layer.name, kind, layer=layer) def make_input_nodes(self): @@ -291,7 +305,7 @@ def make_input_nodes(self): if len(nodes): input_dim = map(int, self.params.input_dim) if not input_dim: - if len(self.params.input_shape)>0: + if len(self.params.input_shape) > 0: input_dim = map(int, self.params.input_shape[0].dim) else: raise KaffeError('Dimensions for input not specified.') @@ -302,10 +316,10 @@ def make_input_nodes(self): def fuse_relus(self, nodes): fused_nodes = [] for node in nodes: - if node.kind!=NodeKind.ReLU: + if node.kind != NodeKind.ReLU: continue parent = node.get_only_parent() - if len(parent.children)!=1: + if len(parent.children) != 1: # We can only fuse this ReLU if its parent's # value isn't used by any other node. continue @@ -330,13 +344,13 @@ def build(self, fuse_relus=True): for layer in layers: node = graph.get_node(layer.name) for parent_name in layer.bottom: - assert parent_name!=layer.name + assert parent_name != layer.name parent_node = node_outputs.get(parent_name) - if (parent_node is None) or (parent_node==node): + if (parent_node is None) or (parent_node == node): parent_node = graph.get_node(parent_name) node.add_parent(parent_node) for child_name in layer.top: - if child_name==layer.name: + if child_name == layer.name: continue if child_name in graph: # This is an "in-place operation" that overwrites an existing node. @@ -355,6 +369,7 @@ def build(self, fuse_relus=True): DataInjector(self.def_path, self.data_path).inject(graph) return graph + class NodeMapper(NodeDispatch): def __init__(self, graph): self.graph = graph @@ -368,15 +383,15 @@ def map(self): input_nodes = self.graph.get_input_nodes() nodes = [t for t in nodes if t not in input_nodes] # Remove implicit nodes. - nodes = [t for t in nodes if t.kind!=NodeKind.Implicit] + nodes = [t for t in nodes if t.kind != NodeKind.Implicit] # Decompose DAG into chains. chains = [] for node in nodes: attach_to_chain = None - if len(node.parents)==1: + if len(node.parents) == 1: parent = node.get_only_parent() for chain in chains: - if chain[-1]==parent: + if chain[-1] == parent: # Node is part of an existing chain. attach_to_chain = chain break diff --git a/kaffe/layers.py b/kaffe/layers.py index b61e0d6..4500baa 100644 --- a/kaffe/layers.py +++ b/kaffe/layers.py @@ -2,69 +2,73 @@ import numbers from .shapes import * from collections import namedtuple +import traceback -LAYER_DESCRIPTORS = { +LAYER_DESCRIPTORS = { # Caffe Types - 'AbsVal' : shape_identity, - 'Accuracy' : shape_scalar, - 'ArgMax' : shape_not_implemented, - 'BNLL' : shape_not_implemented, - 'Concat' : shape_concat, - 'ContrastiveLoss' : shape_scalar, - 'Convolution' : shape_convolution, - 'Deconvolution' : shape_not_implemented, - 'Data' : shape_data, - 'Dropout' : shape_identity, - 'DummyData' : shape_data, - 'EuclideanLoss' : shape_scalar, - 'Eltwise' : shape_identity, - 'Exp' : shape_identity, - 'Flatten' : shape_not_implemented, - 'HDF5Data' : shape_data, - 'HDF5Output' : shape_identity, - 'HingeLoss' : shape_scalar, - 'Im2col' : shape_not_implemented, - 'ImageData' : shape_data, - 'InfogainLoss' : shape_scalar, - 'InnerProduct' : shape_inner_product, - 'Input' : shape_data, - 'LRN' : shape_identity, - 'MemoryData' : shape_mem_data, - 'MultinomialLogisticLoss' : shape_scalar, - 'MVN' : shape_not_implemented, - 'Pooling' : shape_pool, - 'Power' : shape_identity, - 'ReLU' : shape_identity, - 'Sigmoid' : shape_identity, - 'SigmoidCrossEntropyLoss' : shape_scalar, - 'Silence' : shape_not_implemented, - 'Softmax' : shape_identity, - 'SoftmaxWithLoss' : shape_scalar, - 'Split' : shape_not_implemented, - 'Slice' : shape_not_implemented, - 'TanH' : shape_identity, - 'WindowData' : shape_not_implemented, - 'Threshold' : shape_identity, + 'AbsVal': shape_identity, + 'Accuracy': shape_scalar, + 'ArgMax': shape_not_implemented, + 'BNLL': shape_not_implemented, + 'Concat': shape_concat, + 'ContrastiveLoss': shape_scalar, + 'Convolution': shape_convolution, + 'Crop': shape_crop, + 'Deconvolution': shape_deconvolution, + 'Data': shape_data, + 'Dropout': shape_identity, + 'DummyData': shape_data, + 'EuclideanLoss': shape_scalar, + 'Eltwise': shape_identity, + 'Exp': shape_identity, + 'Flatten': shape_not_implemented, + 'HDF5Data': shape_data, + 'HDF5Output': shape_identity, + 'HingeLoss': shape_scalar, + 'Im2col': shape_not_implemented, + 'ImageData': shape_data, + 'InfogainLoss': shape_scalar, + 'InnerProduct': shape_inner_product, + 'Input': shape_data, + 'LRN': shape_identity, + 'MemoryData': shape_mem_data, + 'MultinomialLogisticLoss': shape_scalar, + 'MVN': shape_not_implemented, + 'Pooling': shape_pool, + 'Power': shape_identity, + 'ReLU': shape_identity, + 'Sigmoid': shape_identity, + 'SigmoidCrossEntropyLoss': shape_scalar, + 'Silence': shape_not_implemented, + 'Softmax': shape_identity, + 'SoftmaxWithLoss': shape_scalar, + 'Split': shape_not_implemented, + 'Slice': shape_not_implemented, + 'TanH': shape_identity, + 'Unpooling': shape_not_implemented, + 'WindowData': shape_not_implemented, + 'Threshold': shape_identity, # Internal Types - 'Implicit' : shape_identity + 'Implicit': shape_identity } LAYER_TYPES = LAYER_DESCRIPTORS.keys() + def generate_layer_type_enum(): - types = {t:t for t in LAYER_TYPES} + types = {t: t for t in LAYER_TYPES} return type('LayerType', (), types) LayerType = generate_layer_type_enum() + class NodeKind(LayerType): @staticmethod def map_raw_kind(kind): if kind in LAYER_TYPES: return kind - return None @staticmethod def compute_output_shape(node): @@ -72,14 +76,18 @@ def compute_output_shape(node): val = LAYER_DESCRIPTORS[node.kind](node) return val except NotImplementedError: - raise KaffeError('Output shape computation not implemented for type: %s'%node.kind) + print traceback.print_exc() + raise KaffeError('Output shape computation not implemented for type: %s' % node.kind) + + +class NodeDispatchError(KaffeError): + pass -class NodeDispatchError(KaffeError): pass class NodeDispatch(object): @staticmethod def get_handler_name(node_kind): - if len(node_kind)<=4: + if len(node_kind) <= 4: # A catch-all for things like ReLU and tanh return node_kind.lower() # Convert from CamelCase to under_scored @@ -92,7 +100,8 @@ def get_handler(self, node_kind, prefix): try: return getattr(self, name) except AttributeError: - raise NodeDispatchError('No handler found for node kind: %s (expected: %s)'%(node_kind, name)) + raise NodeDispatchError('No handler found for node kind: %s (expected: %s)' % (node_kind, name)) + class LayerAdapter(object): def __init__(self, layer, kind): @@ -102,11 +111,14 @@ def __init__(self, layer, kind): @property def parameters(self): name = NodeDispatch.get_handler_name(self.kind) + # Hack, because deconvolution layers has convolution params + if name == "deconvolution": + name = "convolution" name = '_'.join((name, 'param')) try: return getattr(self.layer, name) except AttributeError: - raise NodeDispatchError('Caffe parameters not found for layer kind: %s'%(self.kind)) + raise NodeDispatchError('Caffe parameters not found for layer kind: %s' % self.kind) @staticmethod def get_kernel_value(scalar, repeated, idx, default=None): @@ -115,10 +127,10 @@ def get_kernel_value(scalar, repeated, idx, default=None): if repeated: if isinstance(repeated, numbers.Number): return repeated - if len(repeated)==1: + if len(repeated) == 1: # Same value applies to all spatial dimensions return int(repeated[0]) - assert idx0 + assert len(node.parents) > 0 return node.parents[0].output_shape + def shape_scalar(node): + """ + Shape scalar + + :param node: node, it's necessary, because of the kaffe/layers.py + """ return make_shape(1, 1, 1, 1) + def shape_data(node): if node.output_shape: # Old-style input specification @@ -57,6 +102,7 @@ def shape_mem_data(node): params.height, params.width) + def shape_concat(node): axis = node.layer.parameters.axis output_shape = None @@ -67,15 +113,40 @@ def shape_concat(node): output_shape[axis] += parent.output_shape[axis] return tuple(output_shape) + def shape_convolution(node): return get_strided_kernel_output_shape(node, math.floor) + +def shape_deconvolution(node): + """ + Compute shape for the deconvolution layer. + + :param node: class Node representing the deconvolution layer + + :return: output shape of the deconvolution layer + """ + return get_strided_kernel_output_shape(node, math.ceil, deconvolution=True) + + +def shape_crop(node): + """ + Compute shape for the crop layer. + + :param node: class Node representing the crop layer + + :return: output shape of the crop layer + """ + n, c, h, w = node.parents[1].output_shape + return make_shape(n, c, h, w) + + def shape_pool(node): return get_strided_kernel_output_shape(node, math.ceil) + def shape_inner_product(node): input_shape = node.get_only_parent().output_shape return make_shape(input_shape[IDX_N], node.layer.parameters.num_output, - 1, - 1) + 1, 1) diff --git a/kaffe/tensorflow/network.py b/kaffe/tensorflow/network.py index cbfae9c..24e8079 100644 --- a/kaffe/tensorflow/network.py +++ b/kaffe/tensorflow/network.py @@ -3,14 +3,15 @@ DEFAULT_PADDING = 'SAME' + def layer(op): def layer_decorated(self, *args, **kwargs): # Automatically set a name if not provided. name = kwargs.setdefault('name', self.get_unique_name(op.__name__)) # Figure out the layer inputs. - if len(self.inputs)==0: - raise RuntimeError('No input variables found for layer %s.'%name) - elif len(self.inputs)==1: + if len(self.inputs) == 0: + raise RuntimeError('No input variables found for layer %s.' % name) + elif len(self.inputs) == 1: layer_input = self.inputs[0] else: layer_input = list(self.inputs) @@ -24,6 +25,7 @@ def layer_decorated(self, *args, **kwargs): return self return layer_decorated + class Network(object): def __init__(self, inputs, trainable=True): self.inputs = [] @@ -47,24 +49,24 @@ def load(self, data_path, session, ignore_missing=False): raise def feed(self, *args): - assert len(args)!=0 + assert len(args) != 0 self.inputs = [] - for layer in args: - if isinstance(layer, basestring): + for l in args: + if isinstance(l, basestring): try: - layer = self.layers[layer] + l = self.layers[l] except KeyError: print self.layers.keys() - raise KeyError('Unknown layer name fed: %s'%layer) - self.inputs.append(layer) + raise KeyError('Unknown layer name fed: %s' % l) + self.inputs.append(l) return self def get_output(self): return self.inputs[-1] def get_unique_name(self, prefix): - id = sum(t.startswith(prefix) for t,_ in self.layers.items())+1 - return '%s_%d'%(prefix, id) + id = sum(t.startswith(prefix) for t, _ in self.layers.items())+1 + return '%s_%d' % (prefix, id) def make_var(self, name, shape): return tf.get_variable(name, shape, trainable=self.trainable) @@ -72,22 +74,36 @@ def make_var(self, name, shape): def validate_padding(self, padding): assert padding in ('SAME', 'VALID') + @layer + def deconv(self, input, k_h, k_w, c_o, s_h, s_w, o_h, o_w, name, padding=DEFAULT_PADDING): + """ + Deconvolution network implementation in TensorFlow. + """ + self.validate_padding(padding) + c_i = input.get_shape()[-1] + convolve = lambda i, k: tf.nn.conv2d_transpose(i, k, [1, o_h, o_w, c_o], [1, s_h, s_w, 1], padding=padding) + with tf.variable_scope(name) as scope: + kernel = self.make_var('weights', shape=[k_h, k_w, c_i, c_o]) + # biases = self.make_var('biases', [c_o]) + return convolve(input, kernel) + # return tf.reshape(tf.nn.bias_add(conv, biases), conv.get_shape().as_list(), name=scope.name) + @layer def conv(self, input, k_h, k_w, c_o, s_h, s_w, name, relu=True, padding=DEFAULT_PADDING, group=1): self.validate_padding(padding) c_i = input.get_shape()[-1] - assert c_i%group==0 - assert c_o%group==0 + assert c_i % group == 0 + assert c_o % group == 0 convolve = lambda i, k: tf.nn.conv2d(i, k, [1, s_h, s_w, 1], padding=padding) with tf.variable_scope(name) as scope: kernel = self.make_var('weights', shape=[k_h, k_w, c_i/group, c_o]) biases = self.make_var('biases', [c_o]) - if group==1: + if group == 1: conv = convolve(input, kernel) else: input_groups = tf.split(3, group, input) kernel_groups = tf.split(3, group, kernel) - output_groups = [convolve(i, k) for i,k in zip(input_groups, kernel_groups)] + output_groups = [convolve(i, k) for i, k in zip(input_groups, kernel_groups)] conv = tf.concat(3, output_groups) if relu: bias = tf.reshape(tf.nn.bias_add(conv, biases), conv.get_shape().as_list()) @@ -133,7 +149,7 @@ def concat(self, inputs, axis, name): def fc(self, input, num_out, name, relu=True): with tf.variable_scope(name) as scope: input_shape = input.get_shape() - if input_shape.ndims==4: + if input_shape.ndims == 4: dim = 1 for d in input_shape[1:].as_list(): dim *= d diff --git a/kaffe/tensorflow/transformer.py b/kaffe/tensorflow/transformer.py index d55c2df..afdc3f0 100644 --- a/kaffe/tensorflow/transformer.py +++ b/kaffe/tensorflow/transformer.py @@ -1,47 +1,60 @@ -import tensorflow as tf +import logging + import numpy as np + from . import network from ..base import * from ..core import GraphBuilder, DataReshaper, NodeMapper + +logger = logging.getLogger(__name__) + + class TensorFlowNode(object): def __init__(self, op, *args, **kwargs): self.op = op + logger.debug("TF node operation: %s", op) self.args = args + logger.debug("TF node args: %s", args) self.kwargs = list(kwargs.items()) + logger.debug("TF node kwargs: %s", kwargs) def format(self, arg): - return "'%s'"%arg if isinstance(arg, basestring) else str(arg) + return "'%s'" % arg if isinstance(arg, basestring) else str(arg) def pair(self, key, value): - return '%s=%s'%(key, self.format(value)) + return '%s=%s' % (key, self.format(value)) def emit(self): args = map(self.format, self.args) if self.kwargs: - args += [self.pair(k, v) for k,v in self.kwargs] + args += [self.pair(k, v) for k, v in self.kwargs] args.append(self.pair('name', self.node.name)) args = ', '.join(args) - return '%s(%s)'%(self.op, args) + return '%s(%s)' % (self.op, args) + def get_padding_type(kernel_params, input_shape, output_shape): - '''Translates Caffe's numeric padding to one of ('SAME', 'VALID'). + """Translates Caffe's numeric padding to one of ('SAME', 'VALID'). Caffe supports arbitrary padding values, while TensorFlow only supports 'SAME' and 'VALID' modes. So, not all Caffe paddings can be translated to TensorFlow. There are some subtleties to how the padding edge-cases are handled. These are described here: https://github.com/Yangqing/caffe2/blob/master/caffe2/proto/caffe2_legacy.proto - ''' - k_h, k_w, s_h, s_w, p_h, p_w = kernel_params - s_o_h = np.ceil(input_shape[IDX_H]/float(s_h)) - s_o_w = np.ceil(input_shape[IDX_W]/float(s_w)) - if (output_shape[IDX_H]==s_o_h) and (output_shape[IDX_W]==s_o_w): + """ + k_h, k_w, s_h, s_w, p_h, p_w, _ = kernel_params + s_o_h = np.ceil(input_shape[IDX_H] / float(s_h)) + s_o_w = np.ceil(input_shape[IDX_W] / float(s_w)) + if (output_shape[IDX_H] == s_o_h) and (output_shape[IDX_W] == s_o_w): return 'SAME' - v_o_h = np.ceil((input_shape[IDX_H]-k_h+1.0)/float(s_h)) - v_o_w = np.ceil((input_shape[IDX_W]-k_w+1.0)/float(s_w)) - if (output_shape[IDX_H]==v_o_h) and (output_shape[IDX_W]==v_o_w): + v_o_h = np.ceil((input_shape[IDX_H] - k_h + 1.0) / float(s_h)) + v_o_w = np.ceil((input_shape[IDX_W] - k_w + 1.0) / float(s_w)) + if (output_shape[IDX_H] == v_o_h) and (output_shape[IDX_W] == v_o_w): return 'VALID' - return None + # Return network.DEFAULT_PADDING for case that padding is not compatible + # with TensorFlow + return network.DEFAULT_PADDING + class TensorFlowMapper(NodeMapper): @@ -50,23 +63,27 @@ def get_kernel_params(self, node): input_shape = node.get_only_parent().output_shape padding = get_padding_type(kernel_params, input_shape, node.output_shape) # Only emit the padding if it's not the default value. - padding = {'padding':padding} if padding!=network.DEFAULT_PADDING else {} - return (kernel_params, padding) + kwargs = {} + if padding != network.DEFAULT_PADDING: + kwargs['padding'] = padding + if not kernel_params[-1]: + kwargs['bias_term'] = False + return kernel_params, kwargs def relu_adapted_node(self, node, *args, **kwargs): # Opt-out instead of opt-in as ReLU(op) is the common case. if not node.metadata.get('relu', False): - kwargs['relu']=False + kwargs['relu'] = False return TensorFlowNode(*args, **kwargs) def map_convolution(self, node): (c_o, c_i, h, w) = node.data_shape (kernel_params, kwargs) = self.get_kernel_params(node) group = node.parameters.group - if group!=1: + if group != 1: kwargs['group'] = group - assert kernel_params.kernel_h==h - assert kernel_params.kernel_w==w + assert kernel_params.kernel_h == h + assert kernel_params.kernel_w == w return self.relu_adapted_node(node, 'conv', kernel_params.kernel_h, @@ -76,14 +93,46 @@ def map_convolution(self, node): kernel_params.stride_w, **kwargs) + def map_deconvolution(self, node): + """ + Map the deconvolutional node to TensorFlowNode. + + :param node: Node object + """ + channels_output, channels_input, height, width = node.data_shape + batch_size, c_o, output_height, output_width = node.output_shape + (kernel_params, kwargs) = self.get_kernel_params(node) + assert kernel_params.kernel_h == height + assert kernel_params.kernel_w == width + assert channels_output == c_o + return TensorFlowNode('deconv', + kernel_params.kernel_h, + kernel_params.kernel_w, + channels_output, + kernel_params.stride_h, + kernel_params.stride_w, + output_height, + output_width, + **kwargs) + + + def map_crop(self, node): + """ + Map the crop node to TensorFlowNode. + + :param node: Node object + """ + # TODO + return TensorFlowNode('crop') + def map_relu(self, node): return TensorFlowNode('relu') def map_pooling(self, node): pool_type = node.parameters.pool - if pool_type==0: + if pool_type == 0: pool_op = 'max_pool' - elif pool_type==1: + elif pool_type == 1: pool_op = 'avg_pool' else: # Stochastic pooling, for instance. @@ -109,7 +158,7 @@ def map_lrn(self, node): params = node.parameters # The window size must be an odd value. For a window # size of (2*n+1), TensorFlow defines depth_radius = n. - assert (params.local_size%2==1) + assert (params.local_size % 2 == 1) # Caffe scales by (alpha/(2*n+1)), whereas TensorFlow # just scales by alpha (as does Krizhevsky's paper). # We'll account for that here. @@ -129,6 +178,7 @@ def map_dropout(self, node): def commit(self, chains): return chains + class TensorFlowEmitter(object): def __init__(self, tab=None): @@ -176,7 +226,7 @@ def emit(self, name, chains): for node in chain: b += self.emit_node(node) blocks.append(b[:-1]+')') - s = s + '\n\n'.join(blocks) + s += '\n\n'.join(blocks) return s @@ -201,10 +251,10 @@ def load(self, def_path, data_path, phase): def transform_data(self): # Cache the graph source before mutating it. self.transform_source() - mapping = {4 : (2, 3, 1, 0), # (c_o, c_i, h, w) -> (h, w, c_i, c_o) - 2 : (1, 0)} # (c_o, c_i) -> (c_i, c_o) + mapping = {4: (2, 3, 1, 0), # (c_o, c_i, h, w) -> (h, w, c_i, c_o) + 2: (1, 0)} # (c_o, c_i) -> (c_i, c_o) DataReshaper(mapping).reshape(self.graph) - return {node.name:node.data for node in self.graph.nodes if node.data} + return {node.name: node.data for node in self.graph.nodes if node.data} def transform_source(self): if self.source is None: diff --git a/test.py b/test.py index 6bef053..2422d33 100755 --- a/test.py +++ b/test.py @@ -7,6 +7,7 @@ import tensorflow as tf import examples + class ImageNet(object): def __init__(self, val_path, data_path, model): gt_lines = open(val_path).readlines() @@ -21,7 +22,7 @@ def read_image(self, path): h, w, c = np.shape(img) scale_size = self.model.scale_size crop_size = self.model.crop_size - assert c==3 + assert c == 3 if self.model.isotropic: aspect = float(w)/h if w6}/{:<6} {:>6.2f}%'.format(count, total, cur_accuracy)) print('Top %s Accuracy: %s'%(top_k, float(correct)/total)) + def main(): args = sys.argv[1:] if len(args) not in (3, 4): print('usage: %s net.params imagenet-val.txt imagenet-data-dir [model-index=0]'%os.path.basename(__file__)) exit(-1) - model_index = 0 if len(args)==3 else int(args[3]) - if model_index>=len(examples.MODELS): + model_index = 0 if len(args) == 3 else int(args[3]) + if model_index >= len(examples.MODELS): print('Invalid model index. Options are:') for idx, model in enumerate(examples.MODELS): - print('%s: %s'%(idx, model)) + print('%s: %s' % (idx, model)) exit(-1) model = examples.MODELS[model_index] - print('Using model: %s'%(model)) + print('Using model: %s' % (model)) test_imagenet(model, *args[:3]) if __name__ == '__main__':