mirror of
https://github.com/wassname/PSPNet-Keras-tensorflow.git
synced 2026-09-09 11:15:19 +08:00
Made commit of original caffe-tensorflow converter
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
# OS X temporary metadata
|
||||
._*
|
||||
*.DS_Store
|
||||
|
||||
# Extracted parameters
|
||||
*.params
|
||||
|
||||
# Python cache
|
||||
*.pyc
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
[MASTER]
|
||||
ignore=caffepb.py
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
disable=missing-docstring,invalid-name,wildcard-import,unused-wildcard-import,bad-builtin,no-self-use,locally-disabled
|
||||
|
||||
[MISCELLANEOUS]
|
||||
# Exclude TODOs
|
||||
notes=
|
||||
|
||||
[TYPECHECK]
|
||||
ignored-classes=numpy,cv2,NodeKind,LayerType,NetParameter,NpzFile
|
||||
|
||||
[DESIGN]
|
||||
# Maximum number of arguments for function / method
|
||||
max-args=20
|
||||
# Maximum number of locals for function / method body
|
||||
max-locals=30
|
||||
# Maximum number of return / yield for function / method body
|
||||
max-returns=10
|
||||
# Maximum number of branch for function / method body
|
||||
max-branches=12
|
||||
# Maximum number of statements in function / method body
|
||||
max-statements=200
|
||||
# Maximum number of attributes for a class (see R0902).
|
||||
max-attributes=100
|
||||
# Maximum number of public methods for a class (see R0904).
|
||||
max-public-methods=200
|
||||
# Maximum number of boolean expressions in a if statement
|
||||
max-bool-expr=10
|
||||
@@ -0,0 +1,4 @@
|
||||
[style]
|
||||
based_on_style = chromium
|
||||
column_limit = 100
|
||||
indent_width = 4
|
||||
@@ -0,0 +1,31 @@
|
||||
# License
|
||||
|
||||
Each contributor holds copyright over their contributions to Caffe-Tensorflow. In particular:
|
||||
|
||||
- Any included network model is provided under its original license.
|
||||
|
||||
- Any portion derived from Caffe is provided under its original license.
|
||||
|
||||
- Caffe-tensorflow is provided under the MIT license, as specified below.
|
||||
|
||||
# The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Saumitro Dasgupta
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,54 @@
|
||||
# Caffe to TensorFlow
|
||||
|
||||
Convert [Caffe](https://github.com/BVLC/caffe/) models to [TensorFlow](https://github.com/tensorflow/tensorflow).
|
||||
|
||||
## Usage
|
||||
|
||||
Run `convert.py` to convert an existing Caffe model to TensorFlow.
|
||||
|
||||
Make sure you're using the latest Caffe format (see the notes section for more info).
|
||||
|
||||
The output consists of two files:
|
||||
|
||||
1. A data file (in NumPy's native format) containing the model's learned parameters.
|
||||
2. A Python class that constructs the model's graph.
|
||||
|
||||
### Examples
|
||||
|
||||
See the [examples](examples/) folder for more details.
|
||||
|
||||
## Verification
|
||||
|
||||
The following converted models have been verified on the ILSVRC2012 validation set using
|
||||
[validate.py](examples/imagenet/validate.py).
|
||||
|
||||
| Model | Top 5 Accuracy |
|
||||
|:------------------------------------------------------|---------------:|
|
||||
| [ResNet 152](http://arxiv.org/abs/1512.03385) | 92.92% |
|
||||
| [ResNet 101](http://arxiv.org/abs/1512.03385) | 92.63% |
|
||||
| [ResNet 50](http://arxiv.org/abs/1512.03385) | 92.02% |
|
||||
| [VGG 16](http://arxiv.org/abs/1409.1556) | 89.88% |
|
||||
| [GoogLeNet](http://arxiv.org/abs/1409.4842) | 89.06% |
|
||||
| [Network in Network](http://arxiv.org/abs/1312.4400) | 81.21% |
|
||||
| [CaffeNet](http://arxiv.org/abs/1408.5093) | 79.93% |
|
||||
| [AlexNet](http://goo.gl/3BilWd) | 79.84% |
|
||||
|
||||
## Notes
|
||||
|
||||
- Only the new Caffe model format is supported. If you have an old model, use the `upgrade_net_proto_text` and `upgrade_net_proto_binary` tools that ship with Caffe to upgrade them first. Also make sure you're using a fairly recent version of Caffe.
|
||||
|
||||
- It appears that Caffe and TensorFlow cannot be concurrently invoked (CUDA conflicts - even with `set_mode_cpu`). This makes it a two-stage process: first extract the parameters with `convert.py`, then import it into TensorFlow.
|
||||
|
||||
- Caffe is not strictly required. If PyCaffe is found in your `PYTHONPATH`, and the `USE_PYCAFFE` environment variable is set, it will be used. Otherwise, a fallback will be used. However, the fallback uses the pure Python-based implementation of protobuf, which is astoundingly slow (~1.5 minutes to parse the VGG16 parameters). The experimental CPP protobuf backend doesn't particularly help here, since it runs into the file size limit (Caffe gets around this by overriding this limit in C++). A cleaner solution here would be to implement the loader as a C++ module.
|
||||
|
||||
- Only a subset of Caffe layers and accompanying parameters are currently supported.
|
||||
|
||||
- Not all Caffe models can be converted to TensorFlow. For instance, Caffe supports arbitrary padding whereas TensorFlow's support is currently restricted to `SAME` and `VALID`.
|
||||
|
||||
- The border values are handled differently by Caffe and TensorFlow. However, these don't appear to affect things too much.
|
||||
|
||||
- Image rescaling can affect the ILSVRC2012 top 5 accuracy listed above slightly. VGG16 expects isotropic rescaling (anisotropic reduces accuracy to 88.45%) whereas BVLC's implementation of GoogLeNet expects anisotropic (isotropic reduces accuracy to 87.7%).
|
||||
|
||||
- The support class `kaffe.tensorflow.Network` has no internal dependencies. It can be safely extracted and deployed without the rest of this library.
|
||||
|
||||
- The ResNet model uses 1x1 convolutions with a stride of 2. This is currently only supported in the master branch of TensorFlow (the latest release at time of writing being v0.8.0, which does not support it).
|
||||
Executable
+60
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import os
|
||||
import sys
|
||||
import numpy as np
|
||||
import argparse
|
||||
from kaffe import KaffeError, print_stderr
|
||||
from kaffe.tensorflow import TensorFlowTransformer
|
||||
|
||||
|
||||
def fatal_error(msg):
|
||||
print_stderr(msg)
|
||||
exit(-1)
|
||||
|
||||
|
||||
def validate_arguments(args):
|
||||
if (args.data_output_path is not None) and (args.caffemodel is None):
|
||||
fatal_error('No input data path provided.')
|
||||
if (args.caffemodel is not None) and (args.data_output_path is None):
|
||||
fatal_error('No output data path provided.')
|
||||
if (args.code_output_path is None) and (args.data_output_path is None):
|
||||
fatal_error('No output path specified.')
|
||||
|
||||
|
||||
def convert(def_path, caffemodel_path, data_output_path, code_output_path, phase):
|
||||
try:
|
||||
transformer = TensorFlowTransformer(def_path, caffemodel_path, phase=phase)
|
||||
print_stderr('Converting data...')
|
||||
if caffemodel_path is not None:
|
||||
data = transformer.transform_data()
|
||||
print_stderr('Saving data...')
|
||||
with open(data_output_path, 'wb') as data_out:
|
||||
np.save(data_out, data)
|
||||
if code_output_path:
|
||||
print_stderr('Saving source...')
|
||||
with open(code_output_path, 'wb') as src_out:
|
||||
src_out.write(transformer.transform_source())
|
||||
print_stderr('Done.')
|
||||
except KaffeError as err:
|
||||
fatal_error('Error encountered: {}'.format(err))
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('def_path', help='Model definition (.prototxt) path')
|
||||
parser.add_argument('--caffemodel', help='Model data (.caffemodel) path')
|
||||
parser.add_argument('--data-output-path', help='Converted data output path')
|
||||
parser.add_argument('--code-output-path', help='Save generated source to this path')
|
||||
parser.add_argument('-p',
|
||||
'--phase',
|
||||
default='test',
|
||||
help='The phase to convert: test (default) or train')
|
||||
args = parser.parse_args()
|
||||
validate_arguments(args)
|
||||
convert(args.def_path, args.caffemodel, args.data_output_path, args.code_output_path,
|
||||
args.phase)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,41 @@
|
||||
# ImageNet Examples
|
||||
|
||||
This folder contains two examples that demonstrate how to use converted networks for
|
||||
image classification. Also included are sample converted models and helper scripts.
|
||||
|
||||
## 1. Image Classification
|
||||
|
||||
`classify.py` uses a GoogleNet trained on ImageNet, converted to TensorFlow, for classifying images.
|
||||
|
||||
The architecture used is defined in `models/googlenet.py` (which was auto-generated). You will need
|
||||
to download and convert the weights from Caffe to run the example. The download link for the
|
||||
corresponding weights can be found in Caffe's `models/bvlc_googlenet/` folder.
|
||||
|
||||
You can run this example like so:
|
||||
|
||||
$ ./classify.py /path/to/googlenet.npy ~/pics/kitty.png ~/pics/woof.jpg
|
||||
|
||||
You should expect to see an output similar to this:
|
||||
|
||||
Image Classified As Confidence
|
||||
----------------------------------------------------------------------
|
||||
kitty.png Persian cat 99.75 %
|
||||
woof.jpg Bernese mountain dog 82.02 %
|
||||
|
||||
|
||||
## 2. ImageNet Validation
|
||||
|
||||
`validate.py` evaluates a converted model against the ImageNet (ILSVRC12) validation set. To run
|
||||
this script, you will need a copy of the ImageNet validation set. You can run it as follows:
|
||||
|
||||
$ ./validate.py alexnet.npy val.txt imagenet-val/ --model AlexNet
|
||||
|
||||
The validation results specified in the main readme were generated using this script.
|
||||
|
||||
## Helper Scripts
|
||||
|
||||
In addition to the examples above, this folder includes a few additional files:
|
||||
|
||||
- `dataset.py` : helper script for loading, pre-processing, and iterating over images
|
||||
- `models/` : contains converted models (auto-generated)
|
||||
- `models/helper.py` : describes how the data should be preprocessed for each model
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env python
|
||||
import argparse
|
||||
import numpy as np
|
||||
import tensorflow as tf
|
||||
import os.path as osp
|
||||
|
||||
import models
|
||||
import dataset
|
||||
|
||||
|
||||
def display_results(image_paths, probs):
|
||||
'''Displays the classification results given the class probability for each image'''
|
||||
# Get a list of ImageNet class labels
|
||||
with open('imagenet-classes.txt', 'rb') as infile:
|
||||
class_labels = map(str.strip, infile.readlines())
|
||||
# Pick the class with the highest confidence for each image
|
||||
class_indices = np.argmax(probs, axis=1)
|
||||
# Display the results
|
||||
print('\n{:20} {:30} {}'.format('Image', 'Classified As', 'Confidence'))
|
||||
print('-' * 70)
|
||||
for img_idx, image_path in enumerate(image_paths):
|
||||
img_name = osp.basename(image_path)
|
||||
class_name = class_labels[class_indices[img_idx]]
|
||||
confidence = round(probs[img_idx, class_indices[img_idx]] * 100, 2)
|
||||
print('{:20} {:30} {} %'.format(img_name, class_name, confidence))
|
||||
|
||||
|
||||
def classify(model_data_path, image_paths):
|
||||
'''Classify the given images using GoogleNet.'''
|
||||
|
||||
# Get the data specifications for the GoogleNet model
|
||||
spec = models.get_data_spec(model_class=models.GoogleNet)
|
||||
|
||||
# Create a placeholder for the input image
|
||||
input_node = tf.placeholder(tf.float32,
|
||||
shape=(None, spec.crop_size, spec.crop_size, spec.channels))
|
||||
|
||||
# Construct the network
|
||||
net = models.GoogleNet({'data': input_node})
|
||||
|
||||
# Create an image producer (loads and processes images in parallel)
|
||||
image_producer = dataset.ImageProducer(image_paths=image_paths, data_spec=spec)
|
||||
|
||||
with tf.Session() as sesh:
|
||||
# Start the image processing workers
|
||||
coordinator = tf.train.Coordinator()
|
||||
threads = image_producer.start(session=sesh, coordinator=coordinator)
|
||||
|
||||
# Load the converted parameters
|
||||
print('Loading the model')
|
||||
net.load(model_data_path, sesh)
|
||||
|
||||
# Load the input image
|
||||
print('Loading the images')
|
||||
indices, input_images = image_producer.get(sesh)
|
||||
|
||||
# Perform a forward pass through the network to get the class probabilities
|
||||
print('Classifying')
|
||||
probs = sesh.run(net.get_output(), feed_dict={input_node: input_images})
|
||||
display_results([image_paths[i] for i in indices], probs)
|
||||
|
||||
# Stop the worker threads
|
||||
coordinator.request_stop()
|
||||
coordinator.join(threads, stop_grace_period_secs=2)
|
||||
|
||||
def main():
|
||||
# Parse arguments
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('model_path', help='Converted parameters for the GoogleNet model')
|
||||
parser.add_argument('image_paths', nargs='+', help='One or more images to classify')
|
||||
args = parser.parse_args()
|
||||
|
||||
# Classify the image
|
||||
classify(args.model_path, args.image_paths)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,178 @@
|
||||
'''Utility functions and classes for handling image datasets.'''
|
||||
|
||||
import os.path as osp
|
||||
import numpy as np
|
||||
import tensorflow as tf
|
||||
|
||||
|
||||
def process_image(img, scale, isotropic, crop, mean):
|
||||
'''Crops, scales, and normalizes the given image.
|
||||
scale : The image wil be first scaled to this size.
|
||||
If isotropic is true, the smaller side is rescaled to this,
|
||||
preserving the aspect ratio.
|
||||
crop : After scaling, a central crop of this size is taken.
|
||||
mean : Subtracted from the image
|
||||
'''
|
||||
# Rescale
|
||||
if isotropic:
|
||||
img_shape = tf.to_float(tf.shape(img)[:2])
|
||||
min_length = tf.minimum(img_shape[0], img_shape[1])
|
||||
new_shape = tf.to_int32((scale / min_length) * img_shape)
|
||||
else:
|
||||
new_shape = tf.pack([scale, scale])
|
||||
img = tf.image.resize_images(img, new_shape[0], new_shape[1])
|
||||
# Center crop
|
||||
# Use the slice workaround until crop_to_bounding_box supports deferred tensor shapes
|
||||
# See: https://github.com/tensorflow/tensorflow/issues/521
|
||||
offset = (new_shape - crop) / 2
|
||||
img = tf.slice(img, begin=tf.pack([offset[0], offset[1], 0]), size=tf.pack([crop, crop, -1]))
|
||||
# Mean subtraction
|
||||
return tf.to_float(img) - mean
|
||||
|
||||
|
||||
class ImageProducer(object):
|
||||
'''
|
||||
Loads and processes batches of images in parallel.
|
||||
'''
|
||||
|
||||
def __init__(self, image_paths, data_spec, num_concurrent=4, batch_size=None, labels=None):
|
||||
# The data specifications describe how to process the image
|
||||
self.data_spec = data_spec
|
||||
# A list of full image paths
|
||||
self.image_paths = image_paths
|
||||
# An optional list of labels corresponding to each image path
|
||||
self.labels = labels
|
||||
# A boolean flag per image indicating whether its a JPEG or PNG
|
||||
self.extension_mask = self.create_extension_mask(self.image_paths)
|
||||
# Create the loading and processing operations
|
||||
self.setup(batch_size=batch_size, num_concurrent=num_concurrent)
|
||||
|
||||
def setup(self, batch_size, num_concurrent):
|
||||
# Validate the batch size
|
||||
num_images = len(self.image_paths)
|
||||
batch_size = min(num_images, batch_size or self.data_spec.batch_size)
|
||||
if num_images % batch_size != 0:
|
||||
raise ValueError(
|
||||
'The total number of images ({}) must be divisible by the batch size ({}).'.format(
|
||||
num_images, batch_size))
|
||||
self.num_batches = num_images / batch_size
|
||||
|
||||
# Create a queue that will contain image paths (and their indices and extension indicator)
|
||||
self.path_queue = tf.FIFOQueue(capacity=num_images,
|
||||
dtypes=[tf.int32, tf.bool, tf.string],
|
||||
name='path_queue')
|
||||
|
||||
# Enqueue all image paths, along with their indices
|
||||
indices = tf.range(num_images)
|
||||
self.enqueue_paths_op = self.path_queue.enqueue_many([indices, self.extension_mask,
|
||||
self.image_paths])
|
||||
# Close the path queue (no more additions)
|
||||
self.close_path_queue_op = self.path_queue.close()
|
||||
|
||||
# Create an operation that dequeues a single path and returns a processed image
|
||||
(idx, processed_image) = self.process()
|
||||
|
||||
# Create a queue that will contain the processed images (and their indices)
|
||||
image_shape = (self.data_spec.crop_size, self.data_spec.crop_size, self.data_spec.channels)
|
||||
processed_queue = tf.FIFOQueue(capacity=int(np.ceil(num_images / float(num_concurrent))),
|
||||
dtypes=[tf.int32, tf.float32],
|
||||
shapes=[(), image_shape],
|
||||
name='processed_queue')
|
||||
|
||||
# Enqueue the processed image and path
|
||||
enqueue_processed_op = processed_queue.enqueue([idx, processed_image])
|
||||
|
||||
# Create a dequeue op that fetches a batch of processed images off the queue
|
||||
self.dequeue_op = processed_queue.dequeue_many(batch_size)
|
||||
|
||||
# Create a queue runner to perform the processing operations in parallel
|
||||
num_concurrent = min(num_concurrent, num_images)
|
||||
self.queue_runner = tf.train.QueueRunner(processed_queue,
|
||||
[enqueue_processed_op] * num_concurrent)
|
||||
|
||||
def start(self, session, coordinator, num_concurrent=4):
|
||||
'''Start the processing worker threads.'''
|
||||
# Queue all paths
|
||||
session.run(self.enqueue_paths_op)
|
||||
# Close the path queue
|
||||
session.run(self.close_path_queue_op)
|
||||
# Start the queue runner and return the created threads
|
||||
return self.queue_runner.create_threads(session, coord=coordinator, start=True)
|
||||
|
||||
def get(self, session):
|
||||
'''
|
||||
Get a single batch of images along with their indices. If a set of labels were provided,
|
||||
the corresponding labels are returned instead of the indices.
|
||||
'''
|
||||
(indices, images) = session.run(self.dequeue_op)
|
||||
if self.labels is not None:
|
||||
labels = [self.labels[idx] for idx in indices]
|
||||
return (labels, images)
|
||||
return (indices, images)
|
||||
|
||||
def batches(self, session):
|
||||
'''Yield a batch until no more images are left.'''
|
||||
for _ in xrange(self.num_batches):
|
||||
yield self.get(session=session)
|
||||
|
||||
def load_image(self, image_path, is_jpeg):
|
||||
# Read the file
|
||||
file_data = tf.read_file(image_path)
|
||||
# Decode the image data
|
||||
img = tf.cond(
|
||||
is_jpeg,
|
||||
lambda: tf.image.decode_jpeg(file_data, channels=self.data_spec.channels),
|
||||
lambda: tf.image.decode_png(file_data, channels=self.data_spec.channels))
|
||||
if self.data_spec.expects_bgr:
|
||||
# Convert from RGB channel ordering to BGR
|
||||
# This matches, for instance, how OpenCV orders the channels.
|
||||
img = tf.reverse(img, [False, False, True])
|
||||
return img
|
||||
|
||||
def process(self):
|
||||
# Dequeue a single image path
|
||||
idx, is_jpeg, image_path = self.path_queue.dequeue()
|
||||
# Load the image
|
||||
img = self.load_image(image_path, is_jpeg)
|
||||
# Process the image
|
||||
processed_img = process_image(img=img,
|
||||
scale=self.data_spec.scale_size,
|
||||
isotropic=self.data_spec.isotropic,
|
||||
crop=self.data_spec.crop_size,
|
||||
mean=self.data_spec.mean)
|
||||
# Return the processed image, along with its index
|
||||
return (idx, processed_img)
|
||||
|
||||
@staticmethod
|
||||
def create_extension_mask(paths):
|
||||
|
||||
def is_jpeg(path):
|
||||
extension = osp.splitext(path)[-1].lower()
|
||||
if extension in ('.jpg', '.jpeg'):
|
||||
return True
|
||||
if extension != '.png':
|
||||
raise ValueError('Unsupported image format: {}'.format(extension))
|
||||
return False
|
||||
|
||||
return [is_jpeg(p) for p in paths]
|
||||
|
||||
def __len__(self):
|
||||
return len(self.image_paths)
|
||||
|
||||
|
||||
class ImageNetProducer(ImageProducer):
|
||||
|
||||
def __init__(self, val_path, data_path, data_spec):
|
||||
# Read in the ground truth labels for the validation set
|
||||
# The get_ilsvrc_aux.sh in Caffe's data/ilsvrc12 folder can fetch a copy of val.txt
|
||||
gt_lines = open(val_path).readlines()
|
||||
gt_pairs = [line.split() for line in gt_lines]
|
||||
# Get the full image paths
|
||||
# You will need a copy of the ImageNet validation set for this.
|
||||
image_paths = [osp.join(data_path, p[0]) for p in gt_pairs]
|
||||
# The corresponding ground truth labels
|
||||
labels = np.array([int(p[1]) for p in gt_pairs])
|
||||
# Initialize base
|
||||
super(ImageNetProducer, self).__init__(image_paths=image_paths,
|
||||
data_spec=data_spec,
|
||||
labels=labels)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
from helper import *
|
||||
@@ -0,0 +1,19 @@
|
||||
from kaffe.tensorflow import Network
|
||||
|
||||
class AlexNet(Network):
|
||||
def setup(self):
|
||||
(self.feed('data')
|
||||
.conv(11, 11, 96, 4, 4, padding='VALID', name='conv1')
|
||||
.lrn(2, 2e-05, 0.75, name='norm1')
|
||||
.max_pool(3, 3, 2, 2, padding='VALID', name='pool1')
|
||||
.conv(5, 5, 256, 1, 1, group=2, name='conv2')
|
||||
.lrn(2, 2e-05, 0.75, name='norm2')
|
||||
.max_pool(3, 3, 2, 2, padding='VALID', name='pool2')
|
||||
.conv(3, 3, 384, 1, 1, name='conv3')
|
||||
.conv(3, 3, 384, 1, 1, group=2, name='conv4')
|
||||
.conv(3, 3, 256, 1, 1, group=2, name='conv5')
|
||||
.max_pool(3, 3, 2, 2, padding='VALID', name='pool5')
|
||||
.fc(4096, name='fc6')
|
||||
.fc(4096, name='fc7')
|
||||
.fc(1000, relu=False, name='fc8')
|
||||
.softmax(name='prob'))
|
||||
@@ -0,0 +1,19 @@
|
||||
from kaffe.tensorflow import Network
|
||||
|
||||
class CaffeNet(Network):
|
||||
def setup(self):
|
||||
(self.feed('data')
|
||||
.conv(11, 11, 96, 4, 4, padding='VALID', name='conv1')
|
||||
.max_pool(3, 3, 2, 2, padding='VALID', name='pool1')
|
||||
.lrn(2, 2e-05, 0.75, name='norm1')
|
||||
.conv(5, 5, 256, 1, 1, group=2, name='conv2')
|
||||
.max_pool(3, 3, 2, 2, padding='VALID', name='pool2')
|
||||
.lrn(2, 2e-05, 0.75, name='norm2')
|
||||
.conv(3, 3, 384, 1, 1, name='conv3')
|
||||
.conv(3, 3, 384, 1, 1, group=2, name='conv4')
|
||||
.conv(3, 3, 256, 1, 1, group=2, name='conv5')
|
||||
.max_pool(3, 3, 2, 2, padding='VALID', name='pool5')
|
||||
.fc(4096, name='fc6')
|
||||
.fc(4096, name='fc7')
|
||||
.fc(1000, relu=False, name='fc8')
|
||||
.softmax(name='prob'))
|
||||
@@ -0,0 +1,188 @@
|
||||
from kaffe.tensorflow import Network
|
||||
|
||||
class GoogleNet(Network):
|
||||
def setup(self):
|
||||
(self.feed('data')
|
||||
.conv(7, 7, 64, 2, 2, name='conv1_7x7_s2')
|
||||
.max_pool(3, 3, 2, 2, name='pool1_3x3_s2')
|
||||
.lrn(2, 2e-05, 0.75, name='pool1_norm1')
|
||||
.conv(1, 1, 64, 1, 1, name='conv2_3x3_reduce')
|
||||
.conv(3, 3, 192, 1, 1, name='conv2_3x3')
|
||||
.lrn(2, 2e-05, 0.75, name='conv2_norm2')
|
||||
.max_pool(3, 3, 2, 2, name='pool2_3x3_s2')
|
||||
.conv(1, 1, 64, 1, 1, name='inception_3a_1x1'))
|
||||
|
||||
(self.feed('pool2_3x3_s2')
|
||||
.conv(1, 1, 96, 1, 1, name='inception_3a_3x3_reduce')
|
||||
.conv(3, 3, 128, 1, 1, name='inception_3a_3x3'))
|
||||
|
||||
(self.feed('pool2_3x3_s2')
|
||||
.conv(1, 1, 16, 1, 1, name='inception_3a_5x5_reduce')
|
||||
.conv(5, 5, 32, 1, 1, name='inception_3a_5x5'))
|
||||
|
||||
(self.feed('pool2_3x3_s2')
|
||||
.max_pool(3, 3, 1, 1, name='inception_3a_pool')
|
||||
.conv(1, 1, 32, 1, 1, name='inception_3a_pool_proj'))
|
||||
|
||||
(self.feed('inception_3a_1x1',
|
||||
'inception_3a_3x3',
|
||||
'inception_3a_5x5',
|
||||
'inception_3a_pool_proj')
|
||||
.concat(3, name='inception_3a_output')
|
||||
.conv(1, 1, 128, 1, 1, name='inception_3b_1x1'))
|
||||
|
||||
(self.feed('inception_3a_output')
|
||||
.conv(1, 1, 128, 1, 1, name='inception_3b_3x3_reduce')
|
||||
.conv(3, 3, 192, 1, 1, name='inception_3b_3x3'))
|
||||
|
||||
(self.feed('inception_3a_output')
|
||||
.conv(1, 1, 32, 1, 1, name='inception_3b_5x5_reduce')
|
||||
.conv(5, 5, 96, 1, 1, name='inception_3b_5x5'))
|
||||
|
||||
(self.feed('inception_3a_output')
|
||||
.max_pool(3, 3, 1, 1, name='inception_3b_pool')
|
||||
.conv(1, 1, 64, 1, 1, name='inception_3b_pool_proj'))
|
||||
|
||||
(self.feed('inception_3b_1x1',
|
||||
'inception_3b_3x3',
|
||||
'inception_3b_5x5',
|
||||
'inception_3b_pool_proj')
|
||||
.concat(3, name='inception_3b_output')
|
||||
.max_pool(3, 3, 2, 2, name='pool3_3x3_s2')
|
||||
.conv(1, 1, 192, 1, 1, name='inception_4a_1x1'))
|
||||
|
||||
(self.feed('pool3_3x3_s2')
|
||||
.conv(1, 1, 96, 1, 1, name='inception_4a_3x3_reduce')
|
||||
.conv(3, 3, 208, 1, 1, name='inception_4a_3x3'))
|
||||
|
||||
(self.feed('pool3_3x3_s2')
|
||||
.conv(1, 1, 16, 1, 1, name='inception_4a_5x5_reduce')
|
||||
.conv(5, 5, 48, 1, 1, name='inception_4a_5x5'))
|
||||
|
||||
(self.feed('pool3_3x3_s2')
|
||||
.max_pool(3, 3, 1, 1, name='inception_4a_pool')
|
||||
.conv(1, 1, 64, 1, 1, name='inception_4a_pool_proj'))
|
||||
|
||||
(self.feed('inception_4a_1x1',
|
||||
'inception_4a_3x3',
|
||||
'inception_4a_5x5',
|
||||
'inception_4a_pool_proj')
|
||||
.concat(3, name='inception_4a_output')
|
||||
.conv(1, 1, 160, 1, 1, name='inception_4b_1x1'))
|
||||
|
||||
(self.feed('inception_4a_output')
|
||||
.conv(1, 1, 112, 1, 1, name='inception_4b_3x3_reduce')
|
||||
.conv(3, 3, 224, 1, 1, name='inception_4b_3x3'))
|
||||
|
||||
(self.feed('inception_4a_output')
|
||||
.conv(1, 1, 24, 1, 1, name='inception_4b_5x5_reduce')
|
||||
.conv(5, 5, 64, 1, 1, name='inception_4b_5x5'))
|
||||
|
||||
(self.feed('inception_4a_output')
|
||||
.max_pool(3, 3, 1, 1, name='inception_4b_pool')
|
||||
.conv(1, 1, 64, 1, 1, name='inception_4b_pool_proj'))
|
||||
|
||||
(self.feed('inception_4b_1x1',
|
||||
'inception_4b_3x3',
|
||||
'inception_4b_5x5',
|
||||
'inception_4b_pool_proj')
|
||||
.concat(3, name='inception_4b_output')
|
||||
.conv(1, 1, 128, 1, 1, name='inception_4c_1x1'))
|
||||
|
||||
(self.feed('inception_4b_output')
|
||||
.conv(1, 1, 128, 1, 1, name='inception_4c_3x3_reduce')
|
||||
.conv(3, 3, 256, 1, 1, name='inception_4c_3x3'))
|
||||
|
||||
(self.feed('inception_4b_output')
|
||||
.conv(1, 1, 24, 1, 1, name='inception_4c_5x5_reduce')
|
||||
.conv(5, 5, 64, 1, 1, name='inception_4c_5x5'))
|
||||
|
||||
(self.feed('inception_4b_output')
|
||||
.max_pool(3, 3, 1, 1, name='inception_4c_pool')
|
||||
.conv(1, 1, 64, 1, 1, name='inception_4c_pool_proj'))
|
||||
|
||||
(self.feed('inception_4c_1x1',
|
||||
'inception_4c_3x3',
|
||||
'inception_4c_5x5',
|
||||
'inception_4c_pool_proj')
|
||||
.concat(3, name='inception_4c_output')
|
||||
.conv(1, 1, 112, 1, 1, name='inception_4d_1x1'))
|
||||
|
||||
(self.feed('inception_4c_output')
|
||||
.conv(1, 1, 144, 1, 1, name='inception_4d_3x3_reduce')
|
||||
.conv(3, 3, 288, 1, 1, name='inception_4d_3x3'))
|
||||
|
||||
(self.feed('inception_4c_output')
|
||||
.conv(1, 1, 32, 1, 1, name='inception_4d_5x5_reduce')
|
||||
.conv(5, 5, 64, 1, 1, name='inception_4d_5x5'))
|
||||
|
||||
(self.feed('inception_4c_output')
|
||||
.max_pool(3, 3, 1, 1, name='inception_4d_pool')
|
||||
.conv(1, 1, 64, 1, 1, name='inception_4d_pool_proj'))
|
||||
|
||||
(self.feed('inception_4d_1x1',
|
||||
'inception_4d_3x3',
|
||||
'inception_4d_5x5',
|
||||
'inception_4d_pool_proj')
|
||||
.concat(3, name='inception_4d_output')
|
||||
.conv(1, 1, 256, 1, 1, name='inception_4e_1x1'))
|
||||
|
||||
(self.feed('inception_4d_output')
|
||||
.conv(1, 1, 160, 1, 1, name='inception_4e_3x3_reduce')
|
||||
.conv(3, 3, 320, 1, 1, name='inception_4e_3x3'))
|
||||
|
||||
(self.feed('inception_4d_output')
|
||||
.conv(1, 1, 32, 1, 1, name='inception_4e_5x5_reduce')
|
||||
.conv(5, 5, 128, 1, 1, name='inception_4e_5x5'))
|
||||
|
||||
(self.feed('inception_4d_output')
|
||||
.max_pool(3, 3, 1, 1, name='inception_4e_pool')
|
||||
.conv(1, 1, 128, 1, 1, name='inception_4e_pool_proj'))
|
||||
|
||||
(self.feed('inception_4e_1x1',
|
||||
'inception_4e_3x3',
|
||||
'inception_4e_5x5',
|
||||
'inception_4e_pool_proj')
|
||||
.concat(3, name='inception_4e_output')
|
||||
.max_pool(3, 3, 2, 2, name='pool4_3x3_s2')
|
||||
.conv(1, 1, 256, 1, 1, name='inception_5a_1x1'))
|
||||
|
||||
(self.feed('pool4_3x3_s2')
|
||||
.conv(1, 1, 160, 1, 1, name='inception_5a_3x3_reduce')
|
||||
.conv(3, 3, 320, 1, 1, name='inception_5a_3x3'))
|
||||
|
||||
(self.feed('pool4_3x3_s2')
|
||||
.conv(1, 1, 32, 1, 1, name='inception_5a_5x5_reduce')
|
||||
.conv(5, 5, 128, 1, 1, name='inception_5a_5x5'))
|
||||
|
||||
(self.feed('pool4_3x3_s2')
|
||||
.max_pool(3, 3, 1, 1, name='inception_5a_pool')
|
||||
.conv(1, 1, 128, 1, 1, name='inception_5a_pool_proj'))
|
||||
|
||||
(self.feed('inception_5a_1x1',
|
||||
'inception_5a_3x3',
|
||||
'inception_5a_5x5',
|
||||
'inception_5a_pool_proj')
|
||||
.concat(3, name='inception_5a_output')
|
||||
.conv(1, 1, 384, 1, 1, name='inception_5b_1x1'))
|
||||
|
||||
(self.feed('inception_5a_output')
|
||||
.conv(1, 1, 192, 1, 1, name='inception_5b_3x3_reduce')
|
||||
.conv(3, 3, 384, 1, 1, name='inception_5b_3x3'))
|
||||
|
||||
(self.feed('inception_5a_output')
|
||||
.conv(1, 1, 48, 1, 1, name='inception_5b_5x5_reduce')
|
||||
.conv(5, 5, 128, 1, 1, name='inception_5b_5x5'))
|
||||
|
||||
(self.feed('inception_5a_output')
|
||||
.max_pool(3, 3, 1, 1, name='inception_5b_pool')
|
||||
.conv(1, 1, 128, 1, 1, name='inception_5b_pool_proj'))
|
||||
|
||||
(self.feed('inception_5b_1x1',
|
||||
'inception_5b_3x3',
|
||||
'inception_5b_5x5',
|
||||
'inception_5b_pool_proj')
|
||||
.concat(3, name='inception_5b_output')
|
||||
.avg_pool(7, 7, 1, 1, padding='VALID', name='pool5_7x7_s1')
|
||||
.fc(1000, relu=False, name='loss3_classifier')
|
||||
.softmax(name='prob'))
|
||||
@@ -0,0 +1,81 @@
|
||||
import sys
|
||||
import os.path as osp
|
||||
import numpy as np
|
||||
|
||||
# Add the kaffe module to the import path
|
||||
sys.path.append(osp.realpath(osp.join(osp.dirname(__file__), '../../../')))
|
||||
|
||||
from googlenet import GoogleNet
|
||||
from vgg import VGG16
|
||||
from alexnet import AlexNet
|
||||
from caffenet import CaffeNet
|
||||
from nin import NiN
|
||||
from resnet import ResNet50, ResNet101, ResNet152
|
||||
|
||||
|
||||
class DataSpec(object):
|
||||
'''Input data specifications for an ImageNet model.'''
|
||||
|
||||
def __init__(self,
|
||||
batch_size,
|
||||
scale_size,
|
||||
crop_size,
|
||||
isotropic,
|
||||
channels=3,
|
||||
mean=None,
|
||||
bgr=True):
|
||||
# The recommended batch size for this model
|
||||
self.batch_size = batch_size
|
||||
# The image should be scaled to this size first during preprocessing
|
||||
self.scale_size = scale_size
|
||||
# Whether the model expects the rescaling to be isotropic
|
||||
self.isotropic = isotropic
|
||||
# A square crop of this dimension is expected by this model
|
||||
self.crop_size = crop_size
|
||||
# The number of channels in the input image expected by this model
|
||||
self.channels = channels
|
||||
# The mean to be subtracted from each image. By default, the per-channel ImageNet mean.
|
||||
# The values below are ordered BGR, as many Caffe models are trained in this order.
|
||||
# Some of the earlier models (like AlexNet) used a spatial three-channeled mean.
|
||||
# However, using just the per-channel mean values instead doesn't affect things too much.
|
||||
self.mean = mean if mean is not None else np.array([104., 117., 124.])
|
||||
# Whether this model expects images to be in BGR order
|
||||
self.expects_bgr = True
|
||||
|
||||
|
||||
def alexnet_spec(batch_size=500):
|
||||
'''Parameters used by AlexNet and its variants.'''
|
||||
return DataSpec(batch_size=batch_size, scale_size=256, crop_size=227, isotropic=False)
|
||||
|
||||
|
||||
def std_spec(batch_size, isotropic=True):
|
||||
'''Parameters commonly used by "post-AlexNet" architectures.'''
|
||||
return DataSpec(batch_size=batch_size, scale_size=256, crop_size=224, isotropic=isotropic)
|
||||
|
||||
# Collection of sample auto-generated models
|
||||
MODELS = (AlexNet, CaffeNet, GoogleNet, NiN, ResNet50, ResNet101, ResNet152, VGG16)
|
||||
|
||||
# The corresponding data specifications for the sample models
|
||||
# These specifications are based on how the models were trained.
|
||||
# The recommended batch size is based on a Titan X (12GB).
|
||||
MODEL_DATA_SPECS = {
|
||||
AlexNet: alexnet_spec(),
|
||||
CaffeNet: alexnet_spec(),
|
||||
GoogleNet: std_spec(batch_size=200, isotropic=False),
|
||||
ResNet50: std_spec(batch_size=25),
|
||||
ResNet101: std_spec(batch_size=25),
|
||||
ResNet152: std_spec(batch_size=25),
|
||||
NiN: std_spec(batch_size=500),
|
||||
VGG16: std_spec(batch_size=25)
|
||||
}
|
||||
|
||||
|
||||
def get_models():
|
||||
'''Returns a tuple of sample models.'''
|
||||
return MODELS
|
||||
|
||||
|
||||
def get_data_spec(model_instance=None, model_class=None):
|
||||
'''Returns the data specifications for the given network.'''
|
||||
model_class = model_class or model_instance.__class__
|
||||
return MODEL_DATA_SPECS[model_class]
|
||||
@@ -0,0 +1,22 @@
|
||||
from kaffe.tensorflow import Network
|
||||
|
||||
class NiN(Network):
|
||||
def setup(self):
|
||||
(self.feed('data')
|
||||
.conv(11, 11, 96, 4, 4, padding='VALID', name='conv1')
|
||||
.conv(1, 1, 96, 1, 1, name='cccp1')
|
||||
.conv(1, 1, 96, 1, 1, name='cccp2')
|
||||
.max_pool(3, 3, 2, 2, name='pool1')
|
||||
.conv(5, 5, 256, 1, 1, name='conv2')
|
||||
.conv(1, 1, 256, 1, 1, name='cccp3')
|
||||
.conv(1, 1, 256, 1, 1, name='cccp4')
|
||||
.max_pool(3, 3, 2, 2, padding='VALID', name='pool2')
|
||||
.conv(3, 3, 384, 1, 1, name='conv3')
|
||||
.conv(1, 1, 384, 1, 1, name='cccp5')
|
||||
.conv(1, 1, 384, 1, 1, name='cccp6')
|
||||
.max_pool(3, 3, 2, 2, padding='VALID', name='pool3')
|
||||
.conv(3, 3, 1024, 1, 1, name='conv4-1024')
|
||||
.conv(1, 1, 1024, 1, 1, name='cccp7-1024')
|
||||
.conv(1, 1, 1000, 1, 1, name='cccp8-1024')
|
||||
.avg_pool(6, 6, 1, 1, padding='VALID', name='pool4')
|
||||
.softmax(name='prob'))
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,27 @@
|
||||
from kaffe.tensorflow import Network
|
||||
|
||||
class VGG16(Network):
|
||||
def setup(self):
|
||||
(self.feed('data')
|
||||
.conv(3, 3, 64, 1, 1, name='conv1_1')
|
||||
.conv(3, 3, 64, 1, 1, name='conv1_2')
|
||||
.max_pool(2, 2, 2, 2, name='pool1')
|
||||
.conv(3, 3, 128, 1, 1, name='conv2_1')
|
||||
.conv(3, 3, 128, 1, 1, name='conv2_2')
|
||||
.max_pool(2, 2, 2, 2, name='pool2')
|
||||
.conv(3, 3, 256, 1, 1, name='conv3_1')
|
||||
.conv(3, 3, 256, 1, 1, name='conv3_2')
|
||||
.conv(3, 3, 256, 1, 1, name='conv3_3')
|
||||
.max_pool(2, 2, 2, 2, name='pool3')
|
||||
.conv(3, 3, 512, 1, 1, name='conv4_1')
|
||||
.conv(3, 3, 512, 1, 1, name='conv4_2')
|
||||
.conv(3, 3, 512, 1, 1, name='conv4_3')
|
||||
.max_pool(2, 2, 2, 2, name='pool4')
|
||||
.conv(3, 3, 512, 1, 1, name='conv5_1')
|
||||
.conv(3, 3, 512, 1, 1, name='conv5_2')
|
||||
.conv(3, 3, 512, 1, 1, name='conv5_3')
|
||||
.max_pool(2, 2, 2, 2, name='pool5')
|
||||
.fc(4096, name='fc6')
|
||||
.fc(4096, name='fc7')
|
||||
.fc(1000, relu=False, name='fc8')
|
||||
.softmax(name='prob'))
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env python
|
||||
'''Validates a converted ImageNet model against the ILSVRC12 validation set.'''
|
||||
|
||||
import argparse
|
||||
import numpy as np
|
||||
import tensorflow as tf
|
||||
import os.path as osp
|
||||
|
||||
import models
|
||||
import dataset
|
||||
|
||||
|
||||
def load_model(name):
|
||||
'''Creates and returns an instance of the model given its class name.
|
||||
The created model has a single placeholder node for feeding images.
|
||||
'''
|
||||
# Find the model class from its name
|
||||
all_models = models.get_models()
|
||||
lut = {model.__name__: model for model in all_models}
|
||||
if name not in lut:
|
||||
print('Invalid model index. Options are:')
|
||||
# Display a list of valid model names
|
||||
for model in all_models:
|
||||
print('\t* {}'.format(model.__name__))
|
||||
return None
|
||||
NetClass = lut[name]
|
||||
|
||||
# Create a placeholder for the input image
|
||||
spec = models.get_data_spec(model_class=NetClass)
|
||||
data_node = tf.placeholder(tf.float32,
|
||||
shape=(None, spec.crop_size, spec.crop_size, spec.channels))
|
||||
|
||||
# Construct and return the model
|
||||
return NetClass({'data': data_node})
|
||||
|
||||
|
||||
def validate(net, model_path, image_producer, top_k=5):
|
||||
'''Compute the top_k classification accuracy for the given network and images.'''
|
||||
# Get the data specifications for given network
|
||||
spec = models.get_data_spec(model_instance=net)
|
||||
# Get the input node for feeding in the images
|
||||
input_node = net.inputs['data']
|
||||
# Create a placeholder for the ground truth labels
|
||||
label_node = tf.placeholder(tf.int32)
|
||||
# Get the output of the network (class probabilities)
|
||||
probs = net.get_output()
|
||||
# Create a top_k accuracy node
|
||||
top_k_op = tf.nn.in_top_k(probs, label_node, top_k)
|
||||
# The number of images processed
|
||||
count = 0
|
||||
# The number of correctly classified images
|
||||
correct = 0
|
||||
# The total number of images
|
||||
total = len(image_producer)
|
||||
|
||||
with tf.Session() as sesh:
|
||||
coordinator = tf.train.Coordinator()
|
||||
# Load the converted parameters
|
||||
net.load(data_path=model_path, session=sesh)
|
||||
# Start the image processing workers
|
||||
threads = image_producer.start(session=sesh, coordinator=coordinator)
|
||||
# Iterate over and classify mini-batches
|
||||
for (labels, images) in image_producer.batches(sesh):
|
||||
correct += np.sum(sesh.run(top_k_op,
|
||||
feed_dict={input_node: images,
|
||||
label_node: labels}))
|
||||
count += len(labels)
|
||||
cur_accuracy = float(correct) * 100 / count
|
||||
print('{:>6}/{:<6} {:>6.2f}%'.format(count, total, cur_accuracy))
|
||||
# Stop the worker threads
|
||||
coordinator.request_stop()
|
||||
coordinator.join(threads, stop_grace_period_secs=2)
|
||||
print('Top {} Accuracy: {}'.format(top_k, float(correct) / total))
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
# Parse arguments
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('model_path', help='Path to the converted model parameters (.npy)')
|
||||
parser.add_argument('val_gt', help='Path to validation set ground truth (.txt)')
|
||||
parser.add_argument('imagenet_data_dir', help='ImageNet validation set images directory path')
|
||||
parser.add_argument('--model', default='GoogleNet', help='The name of the model to evaluate')
|
||||
args = parser.parse_args()
|
||||
|
||||
# Load the network
|
||||
net = load_model(args.model)
|
||||
if net is None:
|
||||
exit(-1)
|
||||
|
||||
# Load the dataset
|
||||
data_spec = models.get_data_spec(model_instance=net)
|
||||
image_producer = dataset.ImageNetProducer(val_path=args.val_gt,
|
||||
data_path=args.imagenet_data_dir,
|
||||
data_spec=data_spec)
|
||||
|
||||
# Evaluate its performance on the ILSVRC12 validation set
|
||||
validate(net, args.model_path, image_producer)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,36 @@
|
||||
### LeNet Example
|
||||
|
||||
_Thanks to @Russell91 for this example_
|
||||
|
||||
This example showns you how to finetune code from the [Caffe MNIST tutorial](http://caffe.berkeleyvision.org/gathered/examples/mnist.html) using Tensorflow.
|
||||
First, you can convert a prototxt model to tensorflow code:
|
||||
|
||||
$ ./convert.py examples/mnist/lenet.prototxt --code-output-path=mynet.py
|
||||
|
||||
This produces tensorflow code for the LeNet network in `mynet.py`. The code can be imported as described below in the Inference section. Caffe-tensorflow also lets you convert `.caffemodel` weight files to `.npy` files that can be directly loaded from tensorflow:
|
||||
|
||||
$ ./convert.py examples/mnist/lenet.prototxt --caffemodel examples/mnist/lenet_iter_10000.caffemodel --data-output-path=mynet.npy
|
||||
|
||||
The above command will generate a weight file named `mynet.npy`.
|
||||
|
||||
#### Inference:
|
||||
|
||||
Once you have generated both the code weight files for LeNet, you can finetune LeNet using tensorflow with
|
||||
|
||||
$ ./examples/mnist/finetune_mnist.py
|
||||
|
||||
At a high level, `finetune_mnist.py` works as follows:
|
||||
|
||||
```python
|
||||
# Import the converted model's class
|
||||
from mynet import MyNet
|
||||
|
||||
# Create an instance, passing in the input data
|
||||
net = MyNet({'data':my_input_data})
|
||||
|
||||
with tf.Session() as sesh:
|
||||
# Load the data
|
||||
net.load('mynet.npy', sesh)
|
||||
# Forward pass
|
||||
output = sesh.run(net.get_output(), ...)
|
||||
```
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
# Import the converted model's class
|
||||
import numpy as np
|
||||
import random
|
||||
import tensorflow as tf
|
||||
from tensorflow.examples.tutorials.mnist import input_data
|
||||
|
||||
from mynet import LeNet as MyNet
|
||||
|
||||
mnist = input_data.read_data_sets('MNIST_data', one_hot=True)
|
||||
batch_size = 32
|
||||
|
||||
def gen_data(source):
|
||||
while True:
|
||||
indices = range(len(source.images))
|
||||
random.shuffle(indices)
|
||||
for i in indices:
|
||||
image = np.reshape(source.images[i], (28, 28, 1))
|
||||
label = source.labels[i]
|
||||
yield image, label
|
||||
|
||||
def gen_data_batch(source):
|
||||
data_gen = gen_data(source)
|
||||
while True:
|
||||
image_batch = []
|
||||
label_batch = []
|
||||
for _ in range(batch_size):
|
||||
image, label = next(data_gen)
|
||||
image_batch.append(image)
|
||||
label_batch.append(label)
|
||||
yield np.array(image_batch), np.array(label_batch)
|
||||
|
||||
|
||||
images = tf.placeholder(tf.float32, [batch_size, 28, 28, 1])
|
||||
labels = tf.placeholder(tf.float32, [batch_size, 10])
|
||||
net = MyNet({'data': images})
|
||||
|
||||
ip2 = net.layers['ip2']
|
||||
pred = tf.nn.softmax(ip2)
|
||||
|
||||
loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(ip2, labels), 0)
|
||||
opt = tf.train.RMSPropOptimizer(0.001)
|
||||
train_op = opt.minimize(loss)
|
||||
|
||||
with tf.Session() as sess:
|
||||
# Load the data
|
||||
sess.run(tf.initialize_all_variables())
|
||||
net.load('mynet.npy', sess)
|
||||
|
||||
data_gen = gen_data_batch(mnist.train)
|
||||
for i in range(1000):
|
||||
np_images, np_labels = next(data_gen)
|
||||
feed = {images: np_images, labels: np_labels}
|
||||
|
||||
np_loss, np_pred, _ = sess.run([loss, pred, train_op], feed_dict=feed)
|
||||
if i % 10 == 0:
|
||||
print('Iteration: ', i, np_loss)
|
||||
@@ -0,0 +1,129 @@
|
||||
name: "LeNet"
|
||||
layer {
|
||||
name: "data"
|
||||
type: "Input"
|
||||
top: "data"
|
||||
input_param { shape: { dim: 64 dim: 1 dim: 28 dim: 28 } }
|
||||
}
|
||||
layer {
|
||||
name: "conv1"
|
||||
type: "Convolution"
|
||||
bottom: "data"
|
||||
top: "conv1"
|
||||
param {
|
||||
lr_mult: 1
|
||||
}
|
||||
param {
|
||||
lr_mult: 2
|
||||
}
|
||||
convolution_param {
|
||||
num_output: 20
|
||||
kernel_size: 5
|
||||
stride: 1
|
||||
weight_filler {
|
||||
type: "xavier"
|
||||
}
|
||||
bias_filler {
|
||||
type: "constant"
|
||||
}
|
||||
}
|
||||
}
|
||||
layer {
|
||||
name: "pool1"
|
||||
type: "Pooling"
|
||||
bottom: "conv1"
|
||||
top: "pool1"
|
||||
pooling_param {
|
||||
pool: MAX
|
||||
kernel_size: 2
|
||||
stride: 2
|
||||
}
|
||||
}
|
||||
layer {
|
||||
name: "conv2"
|
||||
type: "Convolution"
|
||||
bottom: "pool1"
|
||||
top: "conv2"
|
||||
param {
|
||||
lr_mult: 1
|
||||
}
|
||||
param {
|
||||
lr_mult: 2
|
||||
}
|
||||
convolution_param {
|
||||
num_output: 50
|
||||
kernel_size: 5
|
||||
stride: 1
|
||||
weight_filler {
|
||||
type: "xavier"
|
||||
}
|
||||
bias_filler {
|
||||
type: "constant"
|
||||
}
|
||||
}
|
||||
}
|
||||
layer {
|
||||
name: "pool2"
|
||||
type: "Pooling"
|
||||
bottom: "conv2"
|
||||
top: "pool2"
|
||||
pooling_param {
|
||||
pool: MAX
|
||||
kernel_size: 2
|
||||
stride: 2
|
||||
}
|
||||
}
|
||||
layer {
|
||||
name: "ip1"
|
||||
type: "InnerProduct"
|
||||
bottom: "pool2"
|
||||
top: "ip1"
|
||||
param {
|
||||
lr_mult: 1
|
||||
}
|
||||
param {
|
||||
lr_mult: 2
|
||||
}
|
||||
inner_product_param {
|
||||
num_output: 500
|
||||
weight_filler {
|
||||
type: "xavier"
|
||||
}
|
||||
bias_filler {
|
||||
type: "constant"
|
||||
}
|
||||
}
|
||||
}
|
||||
layer {
|
||||
name: "relu1"
|
||||
type: "ReLU"
|
||||
bottom: "ip1"
|
||||
top: "ip1"
|
||||
}
|
||||
layer {
|
||||
name: "ip2"
|
||||
type: "InnerProduct"
|
||||
bottom: "ip1"
|
||||
top: "ip2"
|
||||
param {
|
||||
lr_mult: 1
|
||||
}
|
||||
param {
|
||||
lr_mult: 2
|
||||
}
|
||||
inner_product_param {
|
||||
num_output: 10
|
||||
weight_filler {
|
||||
type: "xavier"
|
||||
}
|
||||
bias_filler {
|
||||
type: "constant"
|
||||
}
|
||||
}
|
||||
}
|
||||
layer {
|
||||
name: "prob"
|
||||
type: "Softmax"
|
||||
bottom: "ip2"
|
||||
top: "prob"
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,4 @@
|
||||
from .graph import GraphBuilder, NodeMapper
|
||||
from .errors import KaffeError, print_stderr
|
||||
|
||||
from . import tensorflow
|
||||
@@ -0,0 +1 @@
|
||||
from .resolver import get_caffe_resolver, has_pycaffe
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,48 @@
|
||||
import sys
|
||||
|
||||
SHARED_CAFFE_RESOLVER = None
|
||||
|
||||
class CaffeResolver(object):
|
||||
def __init__(self):
|
||||
self.import_caffe()
|
||||
|
||||
def import_caffe(self):
|
||||
self.caffe = None
|
||||
try:
|
||||
# Try to import PyCaffe first
|
||||
import caffe
|
||||
self.caffe = caffe
|
||||
except ImportError:
|
||||
# Fall back to the protobuf implementation
|
||||
from . import caffepb
|
||||
self.caffepb = caffepb
|
||||
show_fallback_warning()
|
||||
if self.caffe:
|
||||
# Use the protobuf code from the imported distribution.
|
||||
# This way, Caffe variants with custom layers will work.
|
||||
self.caffepb = self.caffe.proto.caffe_pb2
|
||||
self.NetParameter = self.caffepb.NetParameter
|
||||
|
||||
def has_pycaffe(self):
|
||||
return self.caffe is not None
|
||||
|
||||
def get_caffe_resolver():
|
||||
global SHARED_CAFFE_RESOLVER
|
||||
if SHARED_CAFFE_RESOLVER is None:
|
||||
SHARED_CAFFE_RESOLVER = CaffeResolver()
|
||||
return SHARED_CAFFE_RESOLVER
|
||||
|
||||
def has_pycaffe():
|
||||
return get_caffe_resolver().has_pycaffe()
|
||||
|
||||
def show_fallback_warning():
|
||||
msg = '''
|
||||
------------------------------------------------------------
|
||||
WARNING: PyCaffe not found!
|
||||
Falling back to a pure protocol buffer implementation.
|
||||
* Conversions will be drastically slower.
|
||||
* This backend is UNTESTED!
|
||||
------------------------------------------------------------
|
||||
|
||||
'''
|
||||
sys.stderr.write(msg)
|
||||
@@ -0,0 +1,7 @@
|
||||
import sys
|
||||
|
||||
class KaffeError(Exception):
|
||||
pass
|
||||
|
||||
def print_stderr(msg):
|
||||
sys.stderr.write('%s\n' % msg)
|
||||
@@ -0,0 +1,302 @@
|
||||
from google.protobuf import text_format
|
||||
|
||||
from .caffe import get_caffe_resolver
|
||||
from .errors import KaffeError, print_stderr
|
||||
from .layers import LayerAdapter, LayerType, NodeKind, NodeDispatch
|
||||
from .shapes import TensorShape
|
||||
|
||||
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.output_shape = None
|
||||
self.metadata = {}
|
||||
|
||||
def add_parent(self, parent_node):
|
||||
assert parent_node not in self.parents
|
||||
self.parents.append(parent_node)
|
||||
if self not in parent_node.children:
|
||||
parent_node.children.append(self)
|
||||
|
||||
def add_child(self, child_node):
|
||||
assert child_node not in self.children
|
||||
self.children.append(child_node)
|
||||
if self not in child_node.parents:
|
||||
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)))
|
||||
return self.parents[0]
|
||||
|
||||
@property
|
||||
def parameters(self):
|
||||
if self.layer is not None:
|
||||
return self.layer.parameters
|
||||
return None
|
||||
|
||||
def __str__(self):
|
||||
return '[%s] %s' % (self.kind, self.name)
|
||||
|
||||
def __repr__(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.name = name
|
||||
|
||||
def add_node(self, node):
|
||||
self.nodes.append(node)
|
||||
self.node_lut[node.name] = node
|
||||
|
||||
def get_node(self, name):
|
||||
try:
|
||||
return self.node_lut[name]
|
||||
except KeyError:
|
||||
raise KaffeError('Layer not found: %s' % name)
|
||||
|
||||
def get_input_nodes(self):
|
||||
return [node for node in self.nodes if len(node.parents) == 0]
|
||||
|
||||
def get_output_nodes(self):
|
||||
return [node for node in self.nodes if len(node.children) == 0]
|
||||
|
||||
def topologically_sorted(self):
|
||||
sorted_nodes = []
|
||||
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.')
|
||||
if node in perm_marked:
|
||||
return
|
||||
temp_marked.add(node)
|
||||
for child in node.children:
|
||||
visit(child)
|
||||
perm_marked.add(node)
|
||||
temp_marked.remove(node)
|
||||
sorted_nodes.insert(0, node)
|
||||
|
||||
while len(unsorted_nodes):
|
||||
visit(unsorted_nodes.pop())
|
||||
return sorted_nodes
|
||||
|
||||
def compute_output_shapes(self):
|
||||
sorted_nodes = self.topologically_sorted()
|
||||
for node in sorted_nodes:
|
||||
node.output_shape = TensorShape(*NodeKind.compute_output_shape(node))
|
||||
|
||||
def replaced(self, new_nodes):
|
||||
return Graph(nodes=new_nodes, name=self.name)
|
||||
|
||||
def transformed(self, transformers):
|
||||
graph = self
|
||||
for transformer in transformers:
|
||||
graph = transformer(graph)
|
||||
if graph is None:
|
||||
raise KaffeError('Transformer failed: {}'.format(transformer))
|
||||
assert isinstance(graph, Graph)
|
||||
return graph
|
||||
|
||||
def __contains__(self, key):
|
||||
return key in self.node_lut
|
||||
|
||||
def __str__(self):
|
||||
hdr = '{:<20} {:<30} {:>20} {:>20}'.format('Type', 'Name', 'Param', 'Output')
|
||||
s = [hdr, '-' * 94]
|
||||
for node in self.topologically_sorted():
|
||||
# If the node has learned parameters, display the first one's shape.
|
||||
# In case of convolutions, this corresponds to the weights.
|
||||
data_shape = node.data[0].shape if node.data else '--'
|
||||
out_shape = node.output_shape or '--'
|
||||
s.append('{:<20} {:<30} {:>20} {:>20}'.format(node.kind, node.name, data_shape,
|
||||
tuple(out_shape)))
|
||||
return '\n'.join(s)
|
||||
|
||||
|
||||
class GraphBuilder(object):
|
||||
'''Constructs a model graph from a Caffe protocol buffer definition.'''
|
||||
|
||||
def __init__(self, def_path, phase='test'):
|
||||
'''
|
||||
def_path: Path to the model definition (.prototxt)
|
||||
data_path: Path to the model data (.caffemodel)
|
||||
phase: Either 'test' or 'train'. Used for filtering phase-specific nodes.
|
||||
'''
|
||||
self.def_path = def_path
|
||||
self.phase = phase
|
||||
self.load()
|
||||
|
||||
def load(self):
|
||||
'''Load the layer definitions from the prototxt.'''
|
||||
self.params = get_caffe_resolver().NetParameter()
|
||||
with open(self.def_path, 'rb') as def_file:
|
||||
text_format.Merge(def_file.read(), self.params)
|
||||
|
||||
def filter_layers(self, layers):
|
||||
'''Filter out layers based on the current phase.'''
|
||||
phase_map = {0: 'train', 1: 'test'}
|
||||
filtered_layer_names = set()
|
||||
filtered_layers = []
|
||||
for layer in layers:
|
||||
phase = self.phase
|
||||
if len(layer.include):
|
||||
phase = phase_map[layer.include[0].phase]
|
||||
if len(layer.exclude):
|
||||
phase = phase_map[1 - layer.include[0].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:
|
||||
filtered_layers.append(layer)
|
||||
# Guard against dupes.
|
||||
assert layer.name not in filtered_layer_names
|
||||
filtered_layer_names.add(layer.name)
|
||||
return filtered_layers
|
||||
|
||||
def make_node(self, layer):
|
||||
'''Create a graph node for the given layer.'''
|
||||
kind = NodeKind.map_raw_kind(layer.type)
|
||||
if kind is None:
|
||||
raise KaffeError('Unknown layer type encountered: %s' % layer.type)
|
||||
# We want to use the layer's top names (the "output" names), rather than the
|
||||
# name attribute, which is more of readability thing than a functional one.
|
||||
# Other layers will refer to a node by its "top name".
|
||||
return Node(layer.name, kind, layer=layer)
|
||||
|
||||
def make_input_nodes(self):
|
||||
'''
|
||||
Create data input nodes.
|
||||
|
||||
This method is for old-style inputs, where the input specification
|
||||
was not treated as a first-class layer in the prototext.
|
||||
Newer models use the "Input layer" type.
|
||||
'''
|
||||
nodes = [Node(name, NodeKind.Data) for name in self.params.input]
|
||||
if len(nodes):
|
||||
input_dim = map(int, self.params.input_dim)
|
||||
if not input_dim:
|
||||
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.')
|
||||
for node in nodes:
|
||||
node.output_shape = tuple(input_dim)
|
||||
return nodes
|
||||
|
||||
def build(self):
|
||||
'''
|
||||
Builds the graph from the Caffe layer definitions.
|
||||
'''
|
||||
# Get the layers
|
||||
layers = self.params.layers or self.params.layer
|
||||
# Filter out phase-excluded layers
|
||||
layers = self.filter_layers(layers)
|
||||
# Get any separately-specified input layers
|
||||
nodes = self.make_input_nodes()
|
||||
nodes += [self.make_node(layer) for layer in layers]
|
||||
# Initialize the graph
|
||||
graph = Graph(nodes=nodes, name=self.params.name)
|
||||
# Connect the nodes
|
||||
#
|
||||
# A note on layers and outputs:
|
||||
# In Caffe, each layer can produce multiple outputs ("tops") from a set of inputs
|
||||
# ("bottoms"). The bottoms refer to other layers' tops. The top can rewrite a bottom
|
||||
# (in case of in-place operations). Note that the layer's name is not used for establishing
|
||||
# any connectivity. It's only used for data association. By convention, a layer with a
|
||||
# single top will often use the same name (although this is not required).
|
||||
#
|
||||
# The current implementation only supports single-output nodes (note that a node can still
|
||||
# have multiple children, since multiple child nodes can refer to the single top's name).
|
||||
node_outputs = {}
|
||||
for layer in layers:
|
||||
node = graph.get_node(layer.name)
|
||||
for input_name in layer.bottom:
|
||||
assert input_name != layer.name
|
||||
parent_node = node_outputs.get(input_name)
|
||||
if (parent_node is None) or (parent_node == node):
|
||||
parent_node = graph.get_node(input_name)
|
||||
node.add_parent(parent_node)
|
||||
if len(layer.top)>1:
|
||||
raise KaffeError('Multiple top nodes are not supported.')
|
||||
for output_name in layer.top:
|
||||
if output_name == layer.name:
|
||||
# Output is named the same as the node. No further action required.
|
||||
continue
|
||||
# There are two possibilities here:
|
||||
#
|
||||
# Case 1: output_name refers to another node in the graph.
|
||||
# This is an "in-place operation" that overwrites an existing node.
|
||||
# This would create a cycle in the graph. We'll undo the in-placing
|
||||
# by substituting this node wherever the overwritten node is referenced.
|
||||
#
|
||||
# Case 2: output_name violates the convention layer.name == output_name.
|
||||
# Since we are working in the single-output regime, we will can rename it to
|
||||
# match the layer name.
|
||||
#
|
||||
# For both cases, future references to this top re-routes to this node.
|
||||
node_outputs[output_name] = node
|
||||
|
||||
graph.compute_output_shapes()
|
||||
return graph
|
||||
|
||||
|
||||
class NodeMapper(NodeDispatch):
|
||||
|
||||
def __init__(self, graph):
|
||||
self.graph = graph
|
||||
|
||||
def map(self):
|
||||
nodes = self.graph.topologically_sorted()
|
||||
# Remove input nodes - we'll handle them separately.
|
||||
input_nodes = self.graph.get_input_nodes()
|
||||
nodes = [t for t in nodes if t not in input_nodes]
|
||||
# Decompose DAG into chains.
|
||||
chains = []
|
||||
for node in nodes:
|
||||
attach_to_chain = None
|
||||
if len(node.parents) == 1:
|
||||
parent = node.get_only_parent()
|
||||
for chain in chains:
|
||||
if chain[-1] == parent:
|
||||
# Node is part of an existing chain.
|
||||
attach_to_chain = chain
|
||||
break
|
||||
if attach_to_chain is None:
|
||||
# Start a new chain for this node.
|
||||
attach_to_chain = []
|
||||
chains.append(attach_to_chain)
|
||||
attach_to_chain.append(node)
|
||||
# Map each chain.
|
||||
mapped_chains = []
|
||||
for chain in chains:
|
||||
mapped_chains.append(self.map_chain(chain))
|
||||
return self.commit(mapped_chains)
|
||||
|
||||
def map_chain(self, chain):
|
||||
return [self.map_node(node) for node in chain]
|
||||
|
||||
def map_node(self, node):
|
||||
map_func = self.get_handler(node.kind, 'map')
|
||||
mapped_node = map_func(node)
|
||||
assert mapped_node is not None
|
||||
mapped_node.node = node
|
||||
return mapped_node
|
||||
|
||||
def commit(self, mapped_chains):
|
||||
raise NotImplementedError('Must be implemented by subclass.')
|
||||
@@ -0,0 +1,147 @@
|
||||
import re
|
||||
import numbers
|
||||
from collections import namedtuple
|
||||
|
||||
from .shapes import *
|
||||
|
||||
LAYER_DESCRIPTORS = {
|
||||
|
||||
# Caffe Types
|
||||
'AbsVal': shape_identity,
|
||||
'Accuracy': shape_scalar,
|
||||
'ArgMax': shape_not_implemented,
|
||||
'BatchNorm': shape_identity,
|
||||
'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,
|
||||
'Scale': 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,
|
||||
}
|
||||
|
||||
LAYER_TYPES = LAYER_DESCRIPTORS.keys()
|
||||
|
||||
LayerType = type('LayerType', (), {t: t for t in LAYER_TYPES})
|
||||
|
||||
class NodeKind(LayerType):
|
||||
|
||||
@staticmethod
|
||||
def map_raw_kind(kind):
|
||||
if kind in LAYER_TYPES:
|
||||
return kind
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def compute_output_shape(node):
|
||||
try:
|
||||
val = LAYER_DESCRIPTORS[node.kind](node)
|
||||
return val
|
||||
except NotImplementedError:
|
||||
raise KaffeError('Output shape computation not implemented for type: %s' % node.kind)
|
||||
|
||||
|
||||
class NodeDispatchError(KaffeError):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class NodeDispatch(object):
|
||||
|
||||
@staticmethod
|
||||
def get_handler_name(node_kind):
|
||||
if len(node_kind) <= 4:
|
||||
# A catch-all for things like ReLU and tanh
|
||||
return node_kind.lower()
|
||||
# Convert from CamelCase to under_scored
|
||||
name = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', node_kind)
|
||||
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', name).lower()
|
||||
|
||||
def get_handler(self, node_kind, prefix):
|
||||
name = self.get_handler_name(node_kind)
|
||||
name = '_'.join((prefix, name))
|
||||
try:
|
||||
return getattr(self, name)
|
||||
except AttributeError:
|
||||
raise NodeDispatchError('No handler found for node kind: %s (expected: %s)' %
|
||||
(node_kind, name))
|
||||
|
||||
|
||||
class LayerAdapter(object):
|
||||
|
||||
def __init__(self, layer, kind):
|
||||
self.layer = layer
|
||||
self.kind = kind
|
||||
|
||||
@property
|
||||
def parameters(self):
|
||||
name = NodeDispatch.get_handler_name(self.kind)
|
||||
name = '_'.join((name, 'param'))
|
||||
try:
|
||||
return getattr(self.layer, name)
|
||||
except AttributeError:
|
||||
raise NodeDispatchError('Caffe parameters not found for layer kind: %s' % (self.kind))
|
||||
|
||||
@staticmethod
|
||||
def get_kernel_value(scalar, repeated, idx, default=None):
|
||||
if scalar:
|
||||
return scalar
|
||||
if repeated:
|
||||
if isinstance(repeated, numbers.Number):
|
||||
return repeated
|
||||
if len(repeated) == 1:
|
||||
# Same value applies to all spatial dimensions
|
||||
return int(repeated[0])
|
||||
assert idx < len(repeated)
|
||||
# Extract the value for the given spatial dimension
|
||||
return repeated[idx]
|
||||
if default is None:
|
||||
raise ValueError('Unable to determine kernel parameter!')
|
||||
return default
|
||||
|
||||
@property
|
||||
def kernel_parameters(self):
|
||||
assert self.kind in (NodeKind.Convolution, NodeKind.Pooling)
|
||||
params = self.parameters
|
||||
k_h = self.get_kernel_value(params.kernel_h, params.kernel_size, 0)
|
||||
k_w = self.get_kernel_value(params.kernel_w, params.kernel_size, 1)
|
||||
s_h = self.get_kernel_value(params.stride_h, params.stride, 0, default=1)
|
||||
s_w = self.get_kernel_value(params.stride_w, params.stride, 1, default=1)
|
||||
p_h = self.get_kernel_value(params.pad_h, params.pad, 0, default=0)
|
||||
p_w = self.get_kernel_value(params.pad_h, params.pad, 1, default=0)
|
||||
return KernelParameters(k_h, k_w, s_h, s_w, p_h, p_w)
|
||||
|
||||
|
||||
KernelParameters = namedtuple('KernelParameters', ['kernel_h', 'kernel_w', 'stride_h', 'stride_w',
|
||||
'pad_h', 'pad_w'])
|
||||
@@ -0,0 +1,83 @@
|
||||
import math
|
||||
from collections import namedtuple
|
||||
|
||||
from .errors import KaffeError
|
||||
|
||||
TensorShape = namedtuple('TensorShape', ['batch_size', 'channels', 'height', 'width'])
|
||||
|
||||
|
||||
def get_filter_output_shape(i_h, i_w, params, round_func):
|
||||
o_h = (i_h + 2 * params.pad_h - params.kernel_h) / float(params.stride_h) + 1
|
||||
o_w = (i_w + 2 * params.pad_w - params.kernel_w) / float(params.stride_w) + 1
|
||||
return (int(round_func(o_h)), int(round_func(o_w)))
|
||||
|
||||
|
||||
def get_strided_kernel_output_shape(node, round_func):
|
||||
assert node.layer is not None
|
||||
input_shape = node.get_only_parent().output_shape
|
||||
o_h, o_w = get_filter_output_shape(input_shape.height, input_shape.width,
|
||||
node.layer.kernel_parameters, round_func)
|
||||
params = node.layer.parameters
|
||||
has_c_o = hasattr(params, 'num_output')
|
||||
c = params.num_output if has_c_o else input_shape.channels
|
||||
return TensorShape(input_shape.batch_size, c, o_h, o_w)
|
||||
|
||||
|
||||
def shape_not_implemented(node):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def shape_identity(node):
|
||||
assert len(node.parents) > 0
|
||||
return node.parents[0].output_shape
|
||||
|
||||
|
||||
def shape_scalar(node):
|
||||
return TensorShape(1, 1, 1, 1)
|
||||
|
||||
|
||||
def shape_data(node):
|
||||
if node.output_shape:
|
||||
# Old-style input specification
|
||||
return node.output_shape
|
||||
try:
|
||||
# New-style input specification
|
||||
return map(int, node.parameters.shape[0].dim)
|
||||
except:
|
||||
# We most likely have a data layer on our hands. The problem is,
|
||||
# Caffe infers the dimensions of the data from the source (eg: LMDB).
|
||||
# We want to avoid reading datasets here. Fail for now.
|
||||
# This can be temporarily fixed by transforming the data layer to
|
||||
# Caffe's "input" layer (as is usually used in the "deploy" version).
|
||||
# TODO: Find a better solution for this.
|
||||
raise KaffeError('Cannot determine dimensions of data layer.\n'
|
||||
'See comments in function shape_data for more info.')
|
||||
|
||||
|
||||
def shape_mem_data(node):
|
||||
params = node.parameters
|
||||
return TensorShape(params.batch_size, params.channels, params.height, params.width)
|
||||
|
||||
|
||||
def shape_concat(node):
|
||||
axis = node.layer.parameters.axis
|
||||
output_shape = None
|
||||
for parent in node.parents:
|
||||
if output_shape is None:
|
||||
output_shape = list(parent.output_shape)
|
||||
else:
|
||||
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_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 TensorShape(input_shape.batch_size, node.layer.parameters.num_output, 1, 1)
|
||||
@@ -0,0 +1,2 @@
|
||||
from .transformer import TensorFlowTransformer
|
||||
from .network import Network
|
||||
@@ -0,0 +1,244 @@
|
||||
import numpy as np
|
||||
import tensorflow as tf
|
||||
|
||||
DEFAULT_PADDING = 'SAME'
|
||||
|
||||
|
||||
def layer(op):
|
||||
'''Decorator for composable network layers.'''
|
||||
|
||||
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.terminals) == 0:
|
||||
raise RuntimeError('No input variables found for layer %s.' % name)
|
||||
elif len(self.terminals) == 1:
|
||||
layer_input = self.terminals[0]
|
||||
else:
|
||||
layer_input = list(self.terminals)
|
||||
# Perform the operation and get the output.
|
||||
layer_output = op(self, layer_input, *args, **kwargs)
|
||||
# Add to layer LUT.
|
||||
self.layers[name] = layer_output
|
||||
# This output is now the input for the next layer.
|
||||
self.feed(layer_output)
|
||||
# Return self for chained calls.
|
||||
return self
|
||||
|
||||
return layer_decorated
|
||||
|
||||
|
||||
class Network(object):
|
||||
|
||||
def __init__(self, inputs, trainable=True):
|
||||
# The input nodes for this network
|
||||
self.inputs = inputs
|
||||
# The current list of terminal nodes
|
||||
self.terminals = []
|
||||
# Mapping from layer names to layers
|
||||
self.layers = dict(inputs)
|
||||
# If true, the resulting variables are set as trainable
|
||||
self.trainable = trainable
|
||||
# Switch variable for dropout
|
||||
self.use_dropout = tf.placeholder_with_default(tf.constant(1.0),
|
||||
shape=[],
|
||||
name='use_dropout')
|
||||
self.setup()
|
||||
|
||||
def setup(self):
|
||||
'''Construct the network. '''
|
||||
raise NotImplementedError('Must be implemented by the subclass.')
|
||||
|
||||
def load(self, data_path, session, ignore_missing=False):
|
||||
'''Load network weights.
|
||||
data_path: The path to the numpy-serialized network weights
|
||||
session: The current TensorFlow session
|
||||
ignore_missing: If true, serialized weights for missing layers are ignored.
|
||||
'''
|
||||
data_dict = np.load(data_path).item()
|
||||
for op_name in data_dict:
|
||||
with tf.variable_scope(op_name, reuse=True):
|
||||
for param_name, data in data_dict[op_name].iteritems():
|
||||
try:
|
||||
var = tf.get_variable(param_name)
|
||||
session.run(var.assign(data))
|
||||
except ValueError:
|
||||
if not ignore_missing:
|
||||
raise
|
||||
|
||||
def feed(self, *args):
|
||||
'''Set the input(s) for the next operation by replacing the terminal nodes.
|
||||
The arguments can be either layer names or the actual layers.
|
||||
'''
|
||||
assert len(args) != 0
|
||||
self.terminals = []
|
||||
for fed_layer in args:
|
||||
if isinstance(fed_layer, basestring):
|
||||
try:
|
||||
fed_layer = self.layers[fed_layer]
|
||||
except KeyError:
|
||||
raise KeyError('Unknown layer name fed: %s' % fed_layer)
|
||||
self.terminals.append(fed_layer)
|
||||
return self
|
||||
|
||||
def get_output(self):
|
||||
'''Returns the current network output.'''
|
||||
return self.terminals[-1]
|
||||
|
||||
def get_unique_name(self, prefix):
|
||||
'''Returns an index-suffixed unique name for the given prefix.
|
||||
This is used for auto-generating layer names based on the type-prefix.
|
||||
'''
|
||||
ident = sum(t.startswith(prefix) for t, _ in self.layers.items()) + 1
|
||||
return '%s_%d' % (prefix, ident)
|
||||
|
||||
def make_var(self, name, shape):
|
||||
'''Creates a new TensorFlow variable.'''
|
||||
return tf.get_variable(name, shape, trainable=self.trainable)
|
||||
|
||||
def validate_padding(self, padding):
|
||||
'''Verifies that the padding is one of the supported ones.'''
|
||||
assert padding in ('SAME', 'VALID')
|
||||
|
||||
@layer
|
||||
def conv(self,
|
||||
input,
|
||||
k_h,
|
||||
k_w,
|
||||
c_o,
|
||||
s_h,
|
||||
s_w,
|
||||
name,
|
||||
relu=True,
|
||||
padding=DEFAULT_PADDING,
|
||||
group=1,
|
||||
biased=True):
|
||||
# Verify that the padding is acceptable
|
||||
self.validate_padding(padding)
|
||||
# Get the number of channels in the input
|
||||
c_i = input.get_shape()[-1]
|
||||
# Verify that the grouping parameter is valid
|
||||
assert c_i % group == 0
|
||||
assert c_o % group == 0
|
||||
# Convolution for a given input and kernel
|
||||
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])
|
||||
if group == 1:
|
||||
# This is the common-case. Convolve the input without any further complications.
|
||||
output = convolve(input, kernel)
|
||||
else:
|
||||
# Split the input into groups and then convolve each of them independently
|
||||
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)]
|
||||
# Concatenate the groups
|
||||
output = tf.concat(3, output_groups)
|
||||
# Add the biases
|
||||
if biased:
|
||||
biases = self.make_var('biases', [c_o])
|
||||
output = tf.nn.bias_add(output, biases)
|
||||
if relu:
|
||||
# ReLU non-linearity
|
||||
output = tf.nn.relu(output, name=scope.name)
|
||||
return output
|
||||
|
||||
@layer
|
||||
def relu(self, input, name):
|
||||
return tf.nn.relu(input, name=name)
|
||||
|
||||
@layer
|
||||
def max_pool(self, input, k_h, k_w, s_h, s_w, name, padding=DEFAULT_PADDING):
|
||||
self.validate_padding(padding)
|
||||
return tf.nn.max_pool(input,
|
||||
ksize=[1, k_h, k_w, 1],
|
||||
strides=[1, s_h, s_w, 1],
|
||||
padding=padding,
|
||||
name=name)
|
||||
|
||||
@layer
|
||||
def avg_pool(self, input, k_h, k_w, s_h, s_w, name, padding=DEFAULT_PADDING):
|
||||
self.validate_padding(padding)
|
||||
return tf.nn.avg_pool(input,
|
||||
ksize=[1, k_h, k_w, 1],
|
||||
strides=[1, s_h, s_w, 1],
|
||||
padding=padding,
|
||||
name=name)
|
||||
|
||||
@layer
|
||||
def lrn(self, input, radius, alpha, beta, name, bias=1.0):
|
||||
return tf.nn.local_response_normalization(input,
|
||||
depth_radius=radius,
|
||||
alpha=alpha,
|
||||
beta=beta,
|
||||
bias=bias,
|
||||
name=name)
|
||||
|
||||
@layer
|
||||
def concat(self, inputs, axis, name):
|
||||
return tf.concat(concat_dim=axis, values=inputs, name=name)
|
||||
|
||||
@layer
|
||||
def add(self, inputs, name):
|
||||
return tf.add_n(inputs, name=name)
|
||||
|
||||
@layer
|
||||
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:
|
||||
# The input is spatial. Vectorize it first.
|
||||
dim = 1
|
||||
for d in input_shape[1:].as_list():
|
||||
dim *= d
|
||||
feed_in = tf.reshape(input, [-1, dim])
|
||||
else:
|
||||
feed_in, dim = (input, input_shape[-1].value)
|
||||
weights = self.make_var('weights', shape=[dim, num_out])
|
||||
biases = self.make_var('biases', [num_out])
|
||||
op = tf.nn.relu_layer if relu else tf.nn.xw_plus_b
|
||||
fc = op(feed_in, weights, biases, name=scope.name)
|
||||
return fc
|
||||
|
||||
@layer
|
||||
def softmax(self, input, name):
|
||||
input_shape = map(lambda v: v.value, input.get_shape())
|
||||
if len(input_shape) > 2:
|
||||
# For certain models (like NiN), the singleton spatial dimensions
|
||||
# need to be explicitly squeezed, since they're not broadcast-able
|
||||
# in TensorFlow's NHWC ordering (unlike Caffe's NCHW).
|
||||
if input_shape[1] == 1 and input_shape[2] == 1:
|
||||
input = tf.squeeze(input, squeeze_dims=[1, 2])
|
||||
else:
|
||||
raise ValueError('Rank 2 tensor input expected for softmax!')
|
||||
return tf.nn.softmax(input, name=name)
|
||||
|
||||
@layer
|
||||
def batch_normalization(self, input, name, scale_offset=True, relu=False):
|
||||
# NOTE: Currently, only inference is supported
|
||||
with tf.variable_scope(name) as scope:
|
||||
shape = [input.get_shape()[-1]]
|
||||
if scale_offset:
|
||||
scale = self.make_var('scale', shape=shape)
|
||||
offset = self.make_var('offset', shape=shape)
|
||||
else:
|
||||
scale, offset = (None, None)
|
||||
output = tf.nn.batch_normalization(
|
||||
input,
|
||||
mean=self.make_var('mean', shape=shape),
|
||||
variance=self.make_var('variance', shape=shape),
|
||||
offset=offset,
|
||||
scale=scale,
|
||||
# TODO: This is the default Caffe batch norm eps
|
||||
# Get the actual eps from parameters
|
||||
variance_epsilon=1e-5,
|
||||
name=name)
|
||||
if relu:
|
||||
output = tf.nn.relu(output)
|
||||
return output
|
||||
|
||||
@layer
|
||||
def dropout(self, input, keep_prob, name):
|
||||
keep = 1 - self.use_dropout + (self.use_dropout * keep_prob)
|
||||
return tf.nn.dropout(input, keep, name=name)
|
||||
@@ -0,0 +1,285 @@
|
||||
import numpy as np
|
||||
|
||||
from ..errors import KaffeError, print_stderr
|
||||
from ..graph import GraphBuilder, NodeMapper
|
||||
from ..layers import NodeKind
|
||||
from ..transformers import (DataInjector, DataReshaper, NodeRenamer, ReLUFuser,
|
||||
BatchNormScaleBiasFuser, BatchNormPreprocessor, ParameterNamer)
|
||||
|
||||
from . import network
|
||||
|
||||
|
||||
def get_padding_type(kernel_params, input_shape, output_shape):
|
||||
'''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.height / float(s_h))
|
||||
s_o_w = np.ceil(input_shape.width / float(s_w))
|
||||
if (output_shape.height == s_o_h) and (output_shape.width == s_o_w):
|
||||
return 'SAME'
|
||||
v_o_h = np.ceil((input_shape.height - k_h + 1.0) / float(s_h))
|
||||
v_o_w = np.ceil((input_shape.width - k_w + 1.0) / float(s_w))
|
||||
if (output_shape.height == v_o_h) and (output_shape.width == v_o_w):
|
||||
return 'VALID'
|
||||
return None
|
||||
|
||||
|
||||
class TensorFlowNode(object):
|
||||
'''An intermediate representation for TensorFlow operations.'''
|
||||
|
||||
def __init__(self, op, *args, **kwargs):
|
||||
# A string corresponding to the TensorFlow operation
|
||||
self.op = op
|
||||
# Positional arguments for the operation
|
||||
self.args = args
|
||||
# Keyword arguments for the operation
|
||||
self.kwargs = list(kwargs.items())
|
||||
# The source Caffe node
|
||||
self.node = None
|
||||
|
||||
def format(self, arg):
|
||||
'''Returns a string representation for the given value.'''
|
||||
return "'%s'" % arg if isinstance(arg, basestring) else str(arg)
|
||||
|
||||
def pair(self, key, value):
|
||||
'''Returns key=formatted(value).'''
|
||||
return '%s=%s' % (key, self.format(value))
|
||||
|
||||
def emit(self):
|
||||
'''Emits the Python source for this node.'''
|
||||
# Format positional arguments
|
||||
args = map(self.format, self.args)
|
||||
# Format any keyword arguments
|
||||
if self.kwargs:
|
||||
args += [self.pair(k, v) for k, v in self.kwargs]
|
||||
# Set the node name
|
||||
args.append(self.pair('name', self.node.name))
|
||||
args = ', '.join(args)
|
||||
return '%s(%s)' % (self.op, args)
|
||||
|
||||
|
||||
class MaybeActivated(object):
|
||||
|
||||
def __init__(self, node, default=True):
|
||||
self.inject_kwargs = {}
|
||||
if node.metadata.get('relu', False) != default:
|
||||
self.inject_kwargs['relu'] = not default
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
kwargs.update(self.inject_kwargs)
|
||||
return TensorFlowNode(*args, **kwargs)
|
||||
|
||||
|
||||
class TensorFlowMapper(NodeMapper):
|
||||
|
||||
def get_kernel_params(self, node):
|
||||
kernel_params = node.layer.kernel_parameters
|
||||
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)
|
||||
|
||||
def map_convolution(self, node):
|
||||
(kernel_params, kwargs) = self.get_kernel_params(node)
|
||||
h = kernel_params.kernel_h
|
||||
w = kernel_params.kernel_w
|
||||
c_o = node.output_shape[1]
|
||||
c_i = node.parents[0].output_shape[1]
|
||||
group = node.parameters.group
|
||||
if group != 1:
|
||||
kwargs['group'] = group
|
||||
if not node.parameters.bias_term:
|
||||
kwargs['biased'] = False
|
||||
assert kernel_params.kernel_h == h
|
||||
assert kernel_params.kernel_w == w
|
||||
return MaybeActivated(node)('conv', kernel_params.kernel_h, kernel_params.kernel_w, c_o,
|
||||
kernel_params.stride_h, kernel_params.stride_w, **kwargs)
|
||||
|
||||
def map_relu(self, node):
|
||||
return TensorFlowNode('relu')
|
||||
|
||||
def map_pooling(self, node):
|
||||
pool_type = node.parameters.pool
|
||||
if pool_type == 0:
|
||||
pool_op = 'max_pool'
|
||||
elif pool_type == 1:
|
||||
pool_op = 'avg_pool'
|
||||
else:
|
||||
# Stochastic pooling, for instance.
|
||||
raise KaffeError('Unsupported pooling type.')
|
||||
(kernel_params, padding) = self.get_kernel_params(node)
|
||||
return TensorFlowNode(pool_op, kernel_params.kernel_h, kernel_params.kernel_w,
|
||||
kernel_params.stride_h, kernel_params.stride_w, **padding)
|
||||
|
||||
def map_inner_product(self, node):
|
||||
#TODO: Axis
|
||||
assert node.parameters.axis == 1
|
||||
#TODO: Unbiased
|
||||
assert node.parameters.bias_term == True
|
||||
return MaybeActivated(node)('fc', node.parameters.num_output)
|
||||
|
||||
def map_softmax(self, node):
|
||||
return TensorFlowNode('softmax')
|
||||
|
||||
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
|
||||
# Caffe scales by (alpha/(2*n+1)), whereas TensorFlow
|
||||
# just scales by alpha (as does Krizhevsky's paper).
|
||||
# We'll account for that here.
|
||||
alpha = params.alpha / float(params.local_size)
|
||||
return TensorFlowNode('lrn', int(params.local_size / 2), alpha, params.beta)
|
||||
|
||||
def map_concat(self, node):
|
||||
axis = (2, 3, 1, 0)[node.parameters.axis]
|
||||
return TensorFlowNode('concat', axis)
|
||||
|
||||
def map_dropout(self, node):
|
||||
return TensorFlowNode('dropout', node.parameters.dropout_ratio)
|
||||
|
||||
def map_batch_norm(self, node):
|
||||
scale_offset = len(node.data) == 4
|
||||
kwargs = {} if scale_offset else {'scale_offset': False}
|
||||
return MaybeActivated(node, default=False)('batch_normalization', **kwargs)
|
||||
|
||||
def map_eltwise(self, node):
|
||||
operations = {0: 'multiply', 1: 'add', 2: 'max'}
|
||||
op_code = node.parameters.operation
|
||||
try:
|
||||
return TensorFlowNode(operations[op_code])
|
||||
except KeyError:
|
||||
raise KaffeError('Unknown elementwise operation: {}'.format(op_code))
|
||||
|
||||
def commit(self, chains):
|
||||
return chains
|
||||
|
||||
|
||||
class TensorFlowEmitter(object):
|
||||
|
||||
def __init__(self, tab=None):
|
||||
self.tab = tab or ' ' * 4
|
||||
self.prefix = ''
|
||||
|
||||
def indent(self):
|
||||
self.prefix += self.tab
|
||||
|
||||
def outdent(self):
|
||||
self.prefix = self.prefix[:-len(self.tab)]
|
||||
|
||||
def statement(self, s):
|
||||
return self.prefix + s + '\n'
|
||||
|
||||
def emit_imports(self):
|
||||
return self.statement('from kaffe.tensorflow import Network\n')
|
||||
|
||||
def emit_class_def(self, name):
|
||||
return self.statement('class %s(Network):' % (name))
|
||||
|
||||
def emit_setup_def(self):
|
||||
return self.statement('def setup(self):')
|
||||
|
||||
def emit_parents(self, chain):
|
||||
assert len(chain)
|
||||
s = '(self.feed('
|
||||
sep = ', \n' + self.prefix + (' ' * len(s))
|
||||
s += sep.join(["'%s'" % parent.name for parent in chain[0].node.parents])
|
||||
return self.statement(s + ')')
|
||||
|
||||
def emit_node(self, node):
|
||||
return self.statement(' ' * 5 + '.' + node.emit())
|
||||
|
||||
def emit(self, name, chains):
|
||||
s = self.emit_imports()
|
||||
s += self.emit_class_def(name)
|
||||
self.indent()
|
||||
s += self.emit_setup_def()
|
||||
self.indent()
|
||||
blocks = []
|
||||
for chain in chains:
|
||||
b = ''
|
||||
b += self.emit_parents(chain)
|
||||
for node in chain:
|
||||
b += self.emit_node(node)
|
||||
blocks.append(b[:-1] + ')')
|
||||
s = s + '\n\n'.join(blocks)
|
||||
return s
|
||||
|
||||
|
||||
class TensorFlowTransformer(object):
|
||||
|
||||
def __init__(self, def_path, data_path, verbose=True, phase='test'):
|
||||
self.verbose = verbose
|
||||
self.phase = phase
|
||||
self.load(def_path, data_path, phase)
|
||||
self.params = None
|
||||
self.source = None
|
||||
|
||||
def load(self, def_path, data_path, phase):
|
||||
# Build the graph
|
||||
graph = GraphBuilder(def_path, phase).build()
|
||||
|
||||
if data_path is not None:
|
||||
# Load and associate learned parameters
|
||||
graph = DataInjector(def_path, data_path)(graph)
|
||||
|
||||
# Transform the graph
|
||||
transformers = [
|
||||
# Fuse split batch normalization layers
|
||||
BatchNormScaleBiasFuser(),
|
||||
|
||||
# Fuse ReLUs
|
||||
# TODO: Move non-linearity application to layer wrapper, allowing
|
||||
# any arbitrary operation to be optionally activated.
|
||||
ReLUFuser(allowed_parent_types=[NodeKind.Convolution, NodeKind.InnerProduct,
|
||||
NodeKind.BatchNorm]),
|
||||
|
||||
# Rename nodes
|
||||
# Slashes are used for scoping in TensorFlow. Replace slashes
|
||||
# in node names with underscores.
|
||||
# (Caffe's GoogLeNet implementation uses slashes)
|
||||
NodeRenamer(lambda node: node.name.replace('/', '_'))
|
||||
]
|
||||
self.graph = graph.transformed(transformers)
|
||||
|
||||
# Display the graph
|
||||
if self.verbose:
|
||||
print_stderr(self.graph)
|
||||
|
||||
def transform_data(self):
|
||||
if self.params is None:
|
||||
transformers = [
|
||||
|
||||
# Reshape the parameters to TensorFlow's ordering
|
||||
DataReshaper({
|
||||
# (c_o, c_i, h, w) -> (h, w, c_i, c_o)
|
||||
NodeKind.Convolution: (2, 3, 1, 0),
|
||||
|
||||
# (c_o, c_i) -> (c_i, c_o)
|
||||
NodeKind.InnerProduct: (1, 0)
|
||||
}),
|
||||
|
||||
# Pre-process batch normalization data
|
||||
BatchNormPreprocessor(),
|
||||
|
||||
# Convert parameters to dictionaries
|
||||
ParameterNamer(),
|
||||
]
|
||||
self.graph = self.graph.transformed(transformers)
|
||||
self.params = {node.name: node.data for node in self.graph.nodes if node.data}
|
||||
return self.params
|
||||
|
||||
def transform_source(self):
|
||||
if self.source is None:
|
||||
mapper = TensorFlowMapper(self.graph)
|
||||
chains = mapper.map()
|
||||
emitter = TensorFlowEmitter()
|
||||
self.source = emitter.emit(self.graph.name, chains)
|
||||
return self.source
|
||||
@@ -0,0 +1,290 @@
|
||||
'''
|
||||
A collection of graph transforms.
|
||||
|
||||
A transformer is a callable that accepts a graph and returns a transformed version.
|
||||
'''
|
||||
|
||||
import numpy as np
|
||||
|
||||
from .caffe import get_caffe_resolver, has_pycaffe
|
||||
from .errors import KaffeError, print_stderr
|
||||
from .layers import NodeKind
|
||||
|
||||
|
||||
class DataInjector(object):
|
||||
'''
|
||||
Associates parameters loaded from a .caffemodel file with their corresponding nodes.
|
||||
'''
|
||||
|
||||
def __init__(self, def_path, data_path):
|
||||
# The .prototxt file defining the graph
|
||||
self.def_path = def_path
|
||||
# The .caffemodel file containing the learned parameters
|
||||
self.data_path = data_path
|
||||
# Set to true if the fallback protocol-buffer based backend was used
|
||||
self.did_use_pb = False
|
||||
# A list containing (layer name, parameters) tuples
|
||||
self.params = None
|
||||
# Load the parameters
|
||||
self.load()
|
||||
|
||||
def load(self):
|
||||
if has_pycaffe():
|
||||
self.load_using_caffe()
|
||||
else:
|
||||
self.load_using_pb()
|
||||
|
||||
def load_using_caffe(self):
|
||||
caffe = get_caffe_resolver().caffe
|
||||
net = caffe.Net(self.def_path, self.data_path, caffe.TEST)
|
||||
data = lambda blob: blob.data
|
||||
self.params = [(k, map(data, v)) for k, v in net.params.items()]
|
||||
|
||||
def load_using_pb(self):
|
||||
data = get_caffe_resolver().NetParameter()
|
||||
data.MergeFromString(open(self.data_path, 'rb').read())
|
||||
pair = lambda layer: (layer.name, self.normalize_pb_data(layer))
|
||||
layers = data.layers or data.layer
|
||||
self.params = [pair(layer) for layer in layers if layer.blobs]
|
||||
self.did_use_pb = True
|
||||
|
||||
def normalize_pb_data(self, layer):
|
||||
transformed = []
|
||||
for blob in layer.blobs:
|
||||
if len(blob.shape.dim):
|
||||
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
|
||||
data = np.array(blob.data, dtype=np.float32).reshape(c_o, c_i, h, w)
|
||||
transformed.append(data)
|
||||
return transformed
|
||||
|
||||
def adjust_parameters(self, node, data):
|
||||
if not self.did_use_pb:
|
||||
return data
|
||||
# When using the protobuf-backend, each parameter initially has four dimensions.
|
||||
# In certain cases (like FC layers), we want to eliminate the singleton dimensions.
|
||||
# This implementation takes care of the common cases. However, it does leave the
|
||||
# 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.
|
||||
for idx in squeeze_indices:
|
||||
data[idx] = np.squeeze(data[idx])
|
||||
return data
|
||||
|
||||
def __call__(self, graph):
|
||||
for layer_name, data in self.params:
|
||||
if layer_name in graph:
|
||||
node = graph.get_node(layer_name)
|
||||
node.data = self.adjust_parameters(node, data)
|
||||
else:
|
||||
print_stderr('Ignoring parameters for non-existent layer: %s' % layer_name)
|
||||
return graph
|
||||
|
||||
|
||||
class DataReshaper(object):
|
||||
|
||||
def __init__(self, mapping, replace=True):
|
||||
# A dictionary mapping NodeKind to the transposed order.
|
||||
self.mapping = mapping
|
||||
# The node kinds eligible for reshaping
|
||||
self.reshaped_node_types = self.mapping.keys()
|
||||
# If true, the reshaped data will replace the old one.
|
||||
# Otherwise, it's set to the reshaped_data attribute.
|
||||
self.replace = replace
|
||||
|
||||
def has_spatial_parent(self, node):
|
||||
try:
|
||||
parent = node.get_only_parent()
|
||||
s = parent.output_shape
|
||||
return s.height > 1 or s.width > 1
|
||||
except KaffeError:
|
||||
return False
|
||||
|
||||
def map(self, node_kind):
|
||||
try:
|
||||
return self.mapping[node_kind]
|
||||
except KeyError:
|
||||
raise KaffeError('Ordering not found for node kind: {}'.format(node_kind))
|
||||
|
||||
def __call__(self, graph):
|
||||
for node in graph.nodes:
|
||||
if node.data is None:
|
||||
continue
|
||||
if node.kind not in self.reshaped_node_types:
|
||||
# Check for 2+ dimensional data
|
||||
if any(len(tensor.shape) > 1 for tensor in node.data):
|
||||
print_stderr('Warning: parmaters not reshaped for node: {}'.format(node))
|
||||
continue
|
||||
transpose_order = self.map(node.kind)
|
||||
weights = node.data[0]
|
||||
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
|
||||
fc_shape = weights.shape
|
||||
output_channels = fc_shape[0]
|
||||
weights = weights.reshape((output_channels, in_shape.channels, in_shape.height,
|
||||
in_shape.width))
|
||||
weights = weights.transpose(self.map(NodeKind.Convolution))
|
||||
node.reshaped_data = weights.reshape(fc_shape[transpose_order[0]],
|
||||
fc_shape[transpose_order[1]])
|
||||
else:
|
||||
node.reshaped_data = weights.transpose(transpose_order)
|
||||
|
||||
if self.replace:
|
||||
for node in graph.nodes:
|
||||
if hasattr(node, 'reshaped_data'):
|
||||
# Set the weights
|
||||
node.data[0] = node.reshaped_data
|
||||
del node.reshaped_data
|
||||
return graph
|
||||
|
||||
|
||||
class SubNodeFuser(object):
|
||||
'''
|
||||
An abstract helper for merging a single-child with its single-parent.
|
||||
'''
|
||||
|
||||
def __call__(self, graph):
|
||||
nodes = graph.nodes
|
||||
fused_nodes = []
|
||||
for node in nodes:
|
||||
if len(node.parents) != 1:
|
||||
# We're only fusing nodes with single parents
|
||||
continue
|
||||
parent = node.get_only_parent()
|
||||
if len(parent.children) != 1:
|
||||
# We can only fuse a node if its parent's
|
||||
# value isn't used by any other node.
|
||||
continue
|
||||
if not self.is_eligible_pair(parent, node):
|
||||
continue
|
||||
# Rewrite the fused node's children to its parent.
|
||||
for child in node.children:
|
||||
child.parents.remove(node)
|
||||
parent.add_child(child)
|
||||
# Disconnect the fused node from the graph.
|
||||
parent.children.remove(node)
|
||||
fused_nodes.append(node)
|
||||
# Let the sub-class merge the fused node in any arbitrary way.
|
||||
self.merge(parent, node)
|
||||
transformed_nodes = [node for node in nodes if node not in fused_nodes]
|
||||
return graph.replaced(transformed_nodes)
|
||||
|
||||
def is_eligible_pair(self, parent, child):
|
||||
'''Returns true if this parent/child pair is eligible for fusion.'''
|
||||
raise NotImplementedError('Must be implemented by subclass.')
|
||||
|
||||
def merge(self, parent, child):
|
||||
'''Merge the child node into the parent.'''
|
||||
raise NotImplementedError('Must be implemented by subclass')
|
||||
|
||||
|
||||
class ReLUFuser(SubNodeFuser):
|
||||
'''
|
||||
Fuses rectified linear units with their parent nodes.
|
||||
'''
|
||||
|
||||
def __init__(self, allowed_parent_types=None):
|
||||
# Fuse ReLUs when the parent node is one of the given types.
|
||||
# If None, all node types are eligible.
|
||||
self.allowed_parent_types = allowed_parent_types
|
||||
|
||||
def is_eligible_pair(self, parent, child):
|
||||
return ((self.allowed_parent_types is None or parent.kind in self.allowed_parent_types) and
|
||||
child.kind == NodeKind.ReLU)
|
||||
|
||||
def merge(self, parent, _):
|
||||
parent.metadata['relu'] = True
|
||||
|
||||
|
||||
class BatchNormScaleBiasFuser(SubNodeFuser):
|
||||
'''
|
||||
The original batch normalization paper includes two learned
|
||||
parameters: a scaling factor \gamma and a bias \beta.
|
||||
Caffe's implementation does not include these two. However, it is commonly
|
||||
replicated by adding a scaling+bias layer immidiately after the batch norm.
|
||||
|
||||
This fuser merges the scaling+bias layer with the batch norm.
|
||||
'''
|
||||
|
||||
def is_eligible_pair(self, parent, child):
|
||||
return (parent.kind == NodeKind.BatchNorm and child.kind == NodeKind.Scale and
|
||||
child.parameters.axis == 1 and child.parameters.bias_term == True)
|
||||
|
||||
def merge(self, parent, child):
|
||||
parent.scale_bias_node = child
|
||||
|
||||
|
||||
class BatchNormPreprocessor(object):
|
||||
'''
|
||||
Prescale batch normalization parameters.
|
||||
Concatenate gamma (scale) and beta (bias) terms if set.
|
||||
'''
|
||||
|
||||
def __call__(self, graph):
|
||||
for node in graph.nodes:
|
||||
if node.kind != NodeKind.BatchNorm:
|
||||
continue
|
||||
assert node.data is not None
|
||||
assert len(node.data) == 3
|
||||
mean, variance, scale = node.data
|
||||
# Prescale the stats
|
||||
scaling_factor = 1.0 / scale if scale != 0 else 0
|
||||
mean *= scaling_factor
|
||||
variance *= scaling_factor
|
||||
# Replace with the updated values
|
||||
node.data = [mean, variance]
|
||||
if hasattr(node, 'scale_bias_node'):
|
||||
# Include the scale and bias terms
|
||||
gamma, beta = node.scale_bias_node.data
|
||||
node.data += [gamma, beta]
|
||||
return graph
|
||||
|
||||
|
||||
class NodeRenamer(object):
|
||||
'''
|
||||
Renames nodes in the graph using a given unary function that
|
||||
accepts a node and returns its new name.
|
||||
'''
|
||||
|
||||
def __init__(self, renamer):
|
||||
self.renamer = renamer
|
||||
|
||||
def __call__(self, graph):
|
||||
for node in graph.nodes:
|
||||
node.name = self.renamer(node)
|
||||
return graph
|
||||
|
||||
|
||||
class ParameterNamer(object):
|
||||
'''
|
||||
Convert layer data arrays to a dictionary mapping parameter names to their values.
|
||||
'''
|
||||
|
||||
def __call__(self, graph):
|
||||
for node in graph.nodes:
|
||||
if node.data is None:
|
||||
continue
|
||||
if node.kind in (NodeKind.Convolution, NodeKind.InnerProduct):
|
||||
names = ('weights',)
|
||||
if node.parameters.bias_term:
|
||||
names += ('biases',)
|
||||
elif node.kind == NodeKind.BatchNorm:
|
||||
names = ('mean', 'variance')
|
||||
if len(node.data) == 4:
|
||||
names += ('scale', 'offset')
|
||||
else:
|
||||
print_stderr('WARNING: Unhandled parameters: {}'.format(node.kind))
|
||||
continue
|
||||
assert len(names) == len(node.data)
|
||||
node.data = dict(zip(names, node.data))
|
||||
return graph
|
||||
Reference in New Issue
Block a user