From 7106e1ff07efa173433d0349e4f5432339ebc400 Mon Sep 17 00:00:00 2001 From: Sravan Date: Fri, 4 Sep 2015 22:15:54 +0200 Subject: [PATCH 1/3] Initial revision of Torch-specific files --- digits/config/torch_option.py | 110 +++ digits/model/tasks/torch_train.py | 727 +++++++++++++++++ .../torch/ImageNet-Training/LICENSE | 22 + .../torch/ImageNet-Training/alexnet.lua | 42 + .../torch/ImageNet-Training/googlenet.lua | 134 ++++ digits/standard-networks/torch/lenet.lua | 20 + tools/torch/LRPolicy.lua | 81 ++ tools/torch/Optimizer.lua | 65 ++ tools/torch/README.txt | 60 ++ tools/torch/data.lua | 319 ++++++++ tools/torch/datum.proto | 12 + tools/torch/logmessage.lua | 27 + tools/torch/main.lua | 732 ++++++++++++++++++ tools/torch/test.lua | 265 +++++++ tools/torch/utils.lua | 228 ++++++ 15 files changed, 2844 insertions(+) create mode 100644 digits/config/torch_option.py create mode 100644 digits/model/tasks/torch_train.py create mode 100644 digits/standard-networks/torch/ImageNet-Training/LICENSE create mode 100644 digits/standard-networks/torch/ImageNet-Training/alexnet.lua create mode 100644 digits/standard-networks/torch/ImageNet-Training/googlenet.lua create mode 100644 digits/standard-networks/torch/lenet.lua create mode 100644 tools/torch/LRPolicy.lua create mode 100644 tools/torch/Optimizer.lua create mode 100644 tools/torch/README.txt create mode 100644 tools/torch/data.lua create mode 100644 tools/torch/datum.proto create mode 100644 tools/torch/logmessage.lua create mode 100644 tools/torch/main.lua create mode 100644 tools/torch/test.lua create mode 100644 tools/torch/utils.lua diff --git a/digits/config/torch_option.py b/digits/config/torch_option.py new file mode 100644 index 000000000..22cb456f1 --- /dev/null +++ b/digits/config/torch_option.py @@ -0,0 +1,110 @@ +# Copyright (c) 2015, NVIDIA CORPORATION. All rights reserved. + +import os +import re +import sys +import imp +import platform +import subprocess + +from digits import device_query +import config_option +import prompt + +class TorchOption(config_option.FrameworkOption): + @staticmethod + def config_file_key(): + return 'torch_root' + + @classmethod + def prompt_title(cls): + return 'Torch' + + @classmethod + def prompt_message(cls): + return 'Where is torch installed?' + + def optional(self): + return True + + def suggestions(self): + suggestions = [] + if 'TORCH_ROOT' in os.environ: + d = os.environ['TORCH_ROOT'] + try: + suggestions.append(prompt.Suggestion( + self.validate(d), 'R', + desc='TORCH_ROOT', default=True)) + except config_option.BadValue as e: + print 'TORCH_ROOT "%s" is invalid:' % d + print '\t%s' % e + if 'TORCH_HOME' in os.environ: + d = os.environ['TORCH_HOME'] + try: + default = True + if len(suggestions) > 0: + default = False + suggestions.append(prompt.Suggestion( + self.validate(d), 'H', + desc='TORCH_HOME', default=default)) + except config_option.BadValue as e: + print 'TORCH_HOME "%s" is invalid:' % d + print '\t%s' % e + suggestions.append(prompt.Suggestion('', 'P', + desc='PATH/TORCHPATH', default=True)) + return suggestions + + @staticmethod + def is_path(): + return True + + @classmethod + def validate(cls, value): + if not value: + return value + + if value == '': + # Find the executable + executable = cls.find_executable('th') + if not executable: + raise config_option.BadValue('torch binary not found in PATH') + #cls.validate_version(executable) + return value + else: + # Find the executable + value = os.path.abspath(value) + if not os.path.isdir(value): + raise config_option.BadValue('"%s" is not a directory' % value) + expected_path = os.path.join(value, 'bin', 'th') + if not os.path.exists(expected_path): + raise config_option.BadValue('torch binary not found at "%s"' % value) + #cls.validate_version(expected_path) + return value + + @staticmethod + def find_executable(program): + """ + Finds an executable by searching through PATH + Returns the path to the executable or None + """ + for path in os.environ['PATH'].split(os.pathsep): + path = path.strip('"') + executable = os.path.join(path, program) + if os.path.isfile(executable) and os.access(executable, os.X_OK): + return executable + return None + + @classmethod + def validate_version(cls, executable): + """ + Utility for checking the caffe version from within validate() + Throws BadValue + + Arguments: + executable -- path to a caffe executable + """ + # Currently DIGITS don't have any restrictions on Torch version, so no need to implement this. + pass + + def apply(self): + pass diff --git a/digits/model/tasks/torch_train.py b/digits/model/tasks/torch_train.py new file mode 100644 index 000000000..1b5ce7a63 --- /dev/null +++ b/digits/model/tasks/torch_train.py @@ -0,0 +1,727 @@ +# Copyright (c) 2014-2015, NVIDIA CORPORATION. All rights reserved. + +import os +import re +import caffe +import time +import math +import subprocess +import sys + +import numpy as np + +import tempfile +import PIL.Image +import digits +from train import TrainTask +from digits.config import config_value +from digits.status import Status +from digits import utils, dataset +from digits.utils import subclass, override, constants, errors +from digits.dataset import ImageClassificationDatasetJob + +# NOTE: Increment this everytime the pickled object changes +PICKLE_VERSION = 1 + +@subclass +class TorchTrainTask(TrainTask): + """ + Trains a torch model + """ + + TORCH_LOG = 'torch_output.log' + + def __init__(self, shuffle, **kwargs): + """ + Arguments: + network -- a NetParameter defining the network + """ + super(TorchTrainTask, self).__init__(**kwargs) + self.pickver_task_torch_train = PICKLE_VERSION + + self.shuffle = shuffle + + self.current_epoch = 0 + + self.loaded_snapshot_file = None + self.loaded_snapshot_epoch = None + self.image_mean = None + self.classifier = None + self.solver = None + + self.model_file = constants.TORCH_MODEL_FILE + self.train_file = constants.TRAIN_DB + self.val_file = constants.VAL_DB + self.snapshot_prefix = constants.TORCH_SNAPSHOT_PREFIX + self.log_file = self.TORCH_LOG + + def __getstate__(self): + state = super(TorchTrainTask, self).__getstate__() + + # Don't pickle these things + if 'labels' in state: + del state['labels'] + if 'image_mean' in state: + del state['image_mean'] + if 'classifier' in state: + del state['classifier'] + if 'torch_log' in state: + del state['torch_log'] + + return state + + def __setstate__(self, state): + super(TorchTrainTask, self).__setstate__(state) + + # Make changes to self + self.loaded_snapshot_file = None + self.loaded_snapshot_epoch = None + + # These things don't get pickled + self.image_mean = None + self.classifier = None + + ### Task overrides + + @override + def name(self): + return 'Train Torch Model' + + @override + def framework_name(self): + return 'torch' + + @override + def before_run(self): + if not isinstance(self.dataset, dataset.ImageClassificationDatasetJob): + raise NotImplementedError() + + self.torch_log = open(self.path(self.TORCH_LOG), 'a') + self.saving_snapshot = False + self.receiving_train_output = False + self.receiving_val_output = False + self.last_train_update = None + self.displaying_network = False + self.temp_unrecognized_output = [] + return True + + @override + def task_arguments(self, resources): + if config_value('torch_root') == '': + torch_bin = 'th' + else: + torch_bin = os.path.join(config_value('torch_root'), 'bin', 'th') + + if self.batch_size is None: + self.batch_size = constants.DEFAULT_TORCH_BATCH_SIZE + + args = [torch_bin, + os.path.join(os.path.dirname(os.path.dirname(digits.__file__)),'tools','torch','main.lua'), + '--network=%s' % self.model_file.split(".")[0], + '--epoch=%d' % int(self.train_epochs), + '--train=%s' % self.dataset.path(constants.TRAIN_DB), + '--networkDirectory=%s' % self.job_dir, + '--save=%s' % self.job_dir, + '--snapshotPrefix=%s' % self.snapshot_prefix, + '--snapshotInterval=%s' % self.snapshot_interval, + '--useMeanPixel=yes', + '--mean=%s' % self.dataset.path(constants.MEAN_FILE_IMAGE), + '--labels=%s' % self.dataset.path(self.dataset.labels_file), + '--batchSize=%d' % self.batch_size, + '--learningRate=%s' % self.learning_rate, + '--policy=%s' % str(self.lr_policy['policy']) + ] + + #learning rate policy input parameters + if self.lr_policy['policy'] == 'fixed': + pass + elif self.lr_policy['policy'] == 'step': + args.append('--gamma=%s' % self.lr_policy['gamma']) + args.append('--stepvalues=%s' % self.lr_policy['stepsize']) + elif self.lr_policy['policy'] == 'multistep': + args.append('--stepvalues=%s' % self.lr_policy['stepvalue']) + args.append('--gamma=%s' % self.lr_policy['gamma']) + elif self.lr_policy['policy'] == 'exp': + args.append('--gamma=%s' % self.lr_policy['gamma']) + elif self.lr_policy['policy'] == 'inv': + args.append('--gamma=%s' % self.lr_policy['gamma']) + args.append('--power=%s' % self.lr_policy['power']) + elif self.lr_policy['policy'] == 'poly': + args.append('--power=%s' % self.lr_policy['power']) + elif self.lr_policy['policy'] == 'sigmoid': + args.append('--stepvalues=%s' % self.lr_policy['stepsize']) + args.append('--gamma=%s' % self.lr_policy['gamma']) + + if self.shuffle: + args.append('--shuffle=yes') + + if self.crop_size: + args.append('--crop=yes') + args.append('--croplen=%d' % self.crop_size) + + if self.use_mean: + args.append('--subtractMean=yes') + else: + args.append('--subtractMean=no') + + if self.random_seed is not None: + args.append('--seed=%s' % self.random_seed) + + if self.solver_type == 'NESTEROV': + args.append('--optimization=nag') + + if self.solver_type == 'ADAGRAD': + args.append('--optimization=adagrad') + + if self.solver_type == 'SGD': + args.append('--optimization=sgd') + + if os.path.exists(self.dataset.path(constants.VAL_DB)) and self.val_interval > 0: + args.append('--validation=%s' % self.dataset.path(constants.VAL_DB)) + args.append('--interval=%s' % self.val_interval) + + if 'gpus' in resources: + identifiers = [] + for identifier, value in resources['gpus']: + identifiers.append(int(identifier)) + if len(identifiers) == 1: + args.append('--devid=%s' % (identifiers[0]+1,)) + elif len(identifiers) > 1: + raise NotImplementedError("haven't tested torch with multiple GPUs yet") + + if self.pretrained_model: + args.append('--weights=%s' % self.path(self.pretrained_model)) + + return args + + @override + def process_output(self, line): + from digits.webapp import socketio + regex = re.compile('\x1b\[[0-9;]*m', re.UNICODE) #TODO: need to include regular expression for MAC color codes + line=regex.sub('', line).strip() + self.torch_log.write('%s\n' % line) + self.torch_log.flush() + + # parse torch output + timestamp, level, message = self.preprocess_output_torch(line) + + # return false when unrecognized output is encountered + if not level: + # network display in progress + if self.displaying_network: + self.temp_unrecognized_output.append(line) + return True + return False + + if not message: + return True + + # network display ends + if self.displaying_network: + if message.startswith('Network definition ends'): + self.temp_unrecognized_output = [] + self.displaying_network = False + return True + + float_exp = '([-]?inf|[-+]?[0-9]*\.?[0-9]+(e[-+]?[0-9]+)?)' + + # loss and learning rate updates + match = re.match(r'Training \(epoch (\d+\.?\d*)\): \w*loss\w* = %s, lr = %s' % (float_exp, float_exp), message) + if match: + index = float(match.group(1)) + l = match.group(2) + assert l.lower() != '-inf', 'Network reported -inf for training loss. Try changing your learning rate.' #TODO: messages needs to be corrected + assert l.lower() != 'inf', 'Network reported inf for training loss. Try decreasing your learning rate.' + l = float(l) + lr = match.group(4) + assert lr.lower() != '-inf', 'Network reported -inf for learning rate. Try changing your learning rate.' + assert lr.lower() != 'inf', 'Network reported inf for learning rate. Try decreasing your learning rate.' + lr = float(lr) + # epoch updates + self.send_progress_update(index) + + self.save_train_output('loss', 'SoftmaxWithLoss', l) + self.save_train_output('learning_rate', 'LearningRate', lr) + self.logger.debug(message) + + return True + + # testing loss and accuracy updates + match = re.match(r'Validation \(epoch (\d+\.?\d*)\): \w*loss\w* = %s, accuracy = %s' % (float_exp,float_exp), message, flags=re.IGNORECASE) + if match: + index = float(match.group(1)) + l = match.group(2) + a = match.group(4) + if l.lower() != 'inf' and l.lower() != '-inf' and a.lower() != 'inf' and a.lower() != '-inf': + l = float(l) + a = float(a) + self.logger.debug('Network accuracy #%s: %s' % (index, a)) + # epoch updates + self.send_progress_update(index) + + self.save_val_output('accuracy', 'Accuracy', a) + self.save_val_output('loss', 'SoftmaxWithLoss', l) + + return True + + # snapshot saved + if self.saving_snapshot: + if not message.startswith('Snapshot saved'): + self.logger.warning('Torch output format seems to have changed. Expected "Snapshot saved..." after "Snapshotting to..."') + else: + self.logger.info('Snapshot saved.') # to print file name here, you can use "message" + self.detect_snapshots() + self.send_snapshot_update() + self.saving_snapshot = False + return True + + # snapshot starting + match = re.match(r'Snapshotting to (.*)\s*$', message) + if match: + self.saving_snapshot = True + return True + + # network display starting + if message.startswith('Network definition:'): + self.displaying_network = True + return True + + if level in ['error', 'critical']: + self.logger.error('%s: %s' % (self.name(), message)) + self.exception = message + return True + + # skip remaining info and warn messages + return True + + def preprocess_output_torch(self, line): + """ + Takes line of output and parses it according to caffe's output format + Returns (timestamp, level, message) or (None, None, None) + """ + # NOTE: This must change when the logging format changes + # LMMDD HH:MM:SS.MICROS pid file:lineno] message + match = re.match(r'(\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2})\s\[(\w+)\s*]\s+(\S.*)$', line) + if match: + timestamp = time.mktime(time.strptime(match.group(1), '%Y-%m-%d %H:%M:%S')) + level = match.group(2) + message = match.group(3) + if level == 'INFO': + level = 'info' + elif level == 'WARNING': + level = 'warning' + elif level == 'ERROR': + level = 'error' + elif level == 'FAIL': #FAIL + level = 'critical' + return (timestamp, level, message) + else: + #self.logger.warning('Unrecognized task output "%s"' % line) + return (None, None, None) + + def send_snapshot_update(self): + """ + Sends socketio message about the snapshot list + """ + # TODO: move to TrainTask + from digits.webapp import socketio + + socketio.emit('task update', + { + 'task': self.html_id(), + 'update': 'snapshots', + 'data': self.snapshot_list(), + }, + namespace='/jobs', + room=self.job_id, + ) + + ### TrainTask overrides + @override + def after_run(self): + if self.temp_unrecognized_output: + if self.traceback: + self.traceback = self.traceback + ('\n'.join(self.temp_unrecognized_output)) + else: + self.traceback = '\n'.join(self.temp_unrecognized_output) + self.temp_unrecognized_output = [] + self.torch_log.close() + + @override + def after_runtime_error(self): + if os.path.exists(self.path(self.TORCH_LOG)): + output = subprocess.check_output(['tail', '-n40', self.path(self.TORCH_LOG)]) + lines = [] + for line in output.split('\n'): + # parse torch header + timestamp, level, message = self.preprocess_output_torch(line) + + if message: + lines.append(message) + # return the last 20 lines + self.traceback = '\n'.join(lines[len(lines)-20:]) + + @override + def detect_snapshots(self): + self.snapshots = [] + + snapshot_dir = os.path.join(self.job_dir, os.path.dirname(self.snapshot_prefix)) + snapshots = [] + solverstates = [] + + for filename in os.listdir(snapshot_dir): + # find models + match = re.match(r'%s_(\d+)\.?(\d*)_Weights\.t7' % os.path.basename(self.snapshot_prefix), filename) + if match: + epoch = 0 + if match.group(2) == '': + epoch = int(match.group(1)) + else: + epoch = float(match.group(1) + '.' + match.group(2)) + snapshots.append( ( + os.path.join(snapshot_dir, filename), + epoch + ) + ) + + self.snapshots = sorted(snapshots, key=lambda tup: tup[1]) + + return len(self.snapshots) > 0 + + @override + def est_next_snapshot(self): + # TODO: Currently this function is not in use. Probably in future we may have to implement this + return None + + @override + def can_view_weights(self): + return False + + @override + def can_infer_one(self): + if isinstance(self.dataset, ImageClassificationDatasetJob): + return True + return False + + @override + def infer_one(self, data, snapshot_epoch=None, layers=None): + if isinstance(self.dataset, ImageClassificationDatasetJob): + return self.classify_one(data, + snapshot_epoch=snapshot_epoch, + layers=layers, + ) + raise NotImplementedError() + + def classify_one(self, image, snapshot_epoch=None, layers=None): + """ + Classify an image + Returns (predictions, visualizations) + predictions -- an array of [ (label, confidence), ...] for each label, sorted by confidence + visualizations -- an array of (layer_name, activations, weights) for the specified layers + Returns (None, None) if something goes wrong + + Arguments: + image -- a np.array + + Keyword arguments: + snapshot_epoch -- which snapshot to use + layers -- which layer activation[s] and weight[s] to visualize + """ + _, temp_image_path = tempfile.mkstemp(suffix='.jpeg') + image = PIL.Image.fromarray(image) + try: + image.save(temp_image_path, format='jpeg') + except KeyError: + error_message = 'Unable to save file to "%s"' % temp_image_path + self.logger.error(error_message) + raise errors.TestError(error_message) + + if config_value('torch_root') == '': + torch_bin = 'th' + else: + torch_bin = os.path.join(config_value('torch_root'), 'bin', 'th') + + args = [torch_bin, + os.path.join(os.path.dirname(os.path.dirname(digits.__file__)),'tools','torch','test.lua'), + '--image=%s' % temp_image_path, + '--network=%s' % self.model_file.split(".")[0], + '--epoch=%d' % int(snapshot_epoch), + '--networkDirectory=%s' % self.job_dir, + '--load=%s' % self.job_dir, + '--snapshotPrefix=%s' % self.snapshot_prefix, + '--mean=%s' % self.dataset.path(constants.MEAN_FILE_IMAGE), + '--labels=%s' % self.dataset.path(self.dataset.labels_file) + ] + + if constants.TORCH_USE_MEAN_PIXEL: + args.append('--useMeanPixel=yes') + + if self.crop_size: + args.append('--crop=yes') + args.append('--croplen=%d' % self.crop_size) + + if self.use_mean: + args.append('--subtractMean=yes') + else: + args.append('--subtractMean=no') + + # Convert them all to strings + args = [str(x) for x in args] + + #print args + + regex = re.compile('\x1b\[[0-9;]*m', re.UNICODE) #TODO: need to include regular expression for MAC color codes + self.logger.info('%s classify one task started.' % self.framework_name()) + + unrecognized_output = [] + predictions = [] + p = subprocess.Popen(args, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + cwd=self.job_dir, + close_fds=True, + ) + + try: + while p.poll() is None: + for line in utils.nonblocking_readlines(p.stdout): + if self.aborted.is_set(): + p.terminate() + raise errors.TestError('%s classify one task got aborted. error code - %d' % (self.framework_name(), p.returncode())) + + if line is not None: + # Remove color codes and whitespace + line=regex.sub('', line).strip() + if line: + if not self.process_test_output(line, predictions, 'one'): + self.logger.warning('%s classify one task unrecognized input: %s' % (self.framework_name(), line.strip())) + unrecognized_output.append(line) + else: + time.sleep(0.05) + + except Exception as e: + if p.poll() is None: + p.terminate() + error_message = '' + if type(e) == errors.TestError: + error_message = e.__str__() + else: + error_message = '%s classify one task failed with error code %d \n %s' % (self.framework_name(), p.returncode(), str(e)) + self.logger.error(error_message) + if unrecognized_output: + unrecognized_output = '\n'.join(unrecognized_output) + error_message = error_message + unrecognized_output + raise errors.TestError(error_message) + + finally: + self.after_test_run(temp_image_path) + + if p.returncode != 0: + error_message = '%s classify one task failed with error code %d' % (self.framework_name(), p.returncode) + self.logger.error(error_message) + if unrecognized_output: + unrecognized_output = '\n'.join(unrecognized_output) + error_message = error_message + unrecognized_output + raise errors.TestError(error_message) + else: + self.logger.info('%s classify one task completed.' % self.framework_name()) + + #TODO: implement visualization + return (predictions,None) + + def after_test_run(self, temp_image_path): + try: + os.remove(temp_image_path) + except OSError: + pass + + def process_test_output(self, line, predictions, test_category): + #from digits.webapp import socketio + + # parse torch output + timestamp, level, message = self.preprocess_output_torch(line) + + # return false when unrecognized output is encountered + if not (level or message): + return False + + if not message: + return True + + float_exp = '([-]?inf|[-+]?[0-9]*\.?[0-9]+(e[-+]?[0-9]+)?)' + + # format of output while testing single image + match = re.match(r'For image \d+, predicted class \d+: \d+ \((.*?)\) %s' % (float_exp), message) + if match: + label = match.group(1) + confidence = match.group(2) + assert confidence.lower() != 'nan', 'Network reported "nan" for confidence value. Please check image and network' + confidence = float(confidence) + predictions.append((label, confidence)) + return True + + # format of output while testing multiple images + match = re.match(r'Predictions for image \d+: (.*)', message) + if match: + values = match.group(1).strip().split(" ") + predictions.append(map(float, values)) + return True + + # displaying info and warn messages as we aren't maintaining seperate log file for model testing + if level == 'info': + self.logger.debug('%s classify %s task : %s' % (self.framework_name(), test_category, message)) + return True + if level == 'warning': + self.logger.warning('%s classify %s task : %s' % (self.framework_name(), test_category, message)) + return True + + if level in ['error', 'critical']: + raise errors.TestError('%s classify %s task failed with error message - %s' % (self.framework_name(), test_category, message)) + + return True # control never reach this line. It can be removed. + + @override + def can_infer_many(self): + if isinstance(self.dataset, ImageClassificationDatasetJob): + return True + raise NotImplementedError() + + @override + def infer_many(self, data, snapshot_epoch=None): + if isinstance(self.dataset, ImageClassificationDatasetJob): + return self.classify_many(data, snapshot_epoch=snapshot_epoch) + raise NotImplementedError() + + def classify_many(self, image_file, snapshot_epoch=None): + """ + Returns (labels, results): + labels -- an array of strings + results -- a 2D np array: + [ + [image0_label0_confidence, image0_label1_confidence, ...], + [image1_label0_confidence, image1_label1_confidence, ...], + ... + ] + + Arguments: + images -- a list of np.arrays + + Keyword arguments: + snapshot_epoch -- which snapshot to use + """ + labels = self.get_labels() #TODO: probably we no need to return this, as we can directly access from the calling function + + if config_value('torch_root') == '': + torch_bin = 'th' + else: + torch_bin = os.path.join(config_value('torch_root'), 'bin', 'th') + + args = [torch_bin, + os.path.join(os.path.dirname(os.path.dirname(digits.__file__)),'tools','torch','test.lua'), + '--testMany=yes', + '--allPredictions=yes', #all predictions are grabbed and formatted as required by DIGITS + '--image=%s' % str(image_file), + '--resizeMode=%s' % str(self.dataset.resize_mode), # Here, we are using original images, so they will be resized in Torch code. This logic needs to be changed to eliminate the rework of resizing. Need to find a way to send python images array to Lua script efficiently + '--network=%s' % self.model_file.split(".")[0], + '--epoch=%d' % int(snapshot_epoch), + '--networkDirectory=%s' % self.job_dir, + '--load=%s' % self.job_dir, + '--snapshotPrefix=%s' % self.snapshot_prefix, + '--mean=%s' % self.dataset.path(constants.MEAN_FILE_IMAGE), + '--pythonPrefix=%s' % sys.executable, + '--labels=%s' % self.dataset.path(self.dataset.labels_file) + ] + if constants.TORCH_USE_MEAN_PIXEL: + args.append('--useMeanPixel=yes') + + if self.crop_size: + args.append('--crop=yes') + args.append('--croplen=%d' % self.crop_size) + + if self.use_mean: + args.append('--subtractMean=yes') + else: + args.append('--subtractMean=no') + + #print args + + # Convert them all to strings + args = [str(x) for x in args] + + regex = re.compile('\x1b\[[0-9;]*m', re.UNICODE) #TODO: need to include regular expression for MAC color codes + self.logger.info('%s classify many task started.' % self.name()) + + unrecognized_output = [] + predictions = [] + p = subprocess.Popen(args, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + cwd=self.job_dir, + close_fds=True, + ) + + try: + while p.poll() is None: + for line in utils.nonblocking_readlines(p.stdout): + if self.aborted.is_set(): + p.terminate() + raise errors.TestError('%s classify many task got aborted. error code - %d' % (self.framework_name(), p.returncode())) + + if line is not None: + # Remove whitespace and color codes. color codes are appended to begining and end of line by torch binary i.e., 'th'. Check the below link for more information + # https://groups.google.com/forum/#!searchin/torch7/color$20codes/torch7/8O_0lSgSzuA/Ih6wYg9fgcwJ + line=regex.sub('', line).strip() + if line: + if not self.process_test_output(line, predictions, 'many'): + self.logger.warning('%s classify many task unrecognized input: %s' % (self.framework_name(), line.strip())) + unrecognized_output.append(line) + else: + time.sleep(0.05) + except Exception as e: + if p.poll() is None: + p.terminate() + error_message = '' + if type(e) == errors.TestError: + error_message = e.__str__() + else: + error_message = '%s classify many task failed with error code %d \n %s' % (self.framework_name(), p.returncode(), str(e)) + self.logger.error(error_message) + if unrecognized_output: + unrecognized_output = '\n'.join(unrecognized_output) + error_message = error_message + unrecognized_output + raise errors.TestError(error_message) + + if p.returncode != 0: + error_message = '%s classify many task failed with error code %d' % (self.framework_name(), p.returncode) + self.logger.error(error_message) + if unrecognized_output: + unrecognized_output = '\n'.join(unrecognized_output) + error_message = error_message + unrecognized_output + raise errors.TestError(error_message) + else: + self.logger.info('%s classify many task completed.' % self.framework_name()) + + return (labels,np.array(predictions)) + + def has_model(self): + """ + Returns True if there is a model that can be used + """ + return len(self.snapshots) != 0 + + def loaded_model(self): + """ + Returns True if a model has been loaded + """ + return None + + def load_model(self, epoch=None): + """ + Loads a .caffemodel + Returns True if the model is loaded (or if it was already loaded) + + Keyword Arguments: + epoch -- which snapshot to load (default is -1 to load the most recently generated snapshot) + """ + return False + diff --git a/digits/standard-networks/torch/ImageNet-Training/LICENSE b/digits/standard-networks/torch/ImageNet-Training/LICENSE new file mode 100644 index 000000000..b0e4edd65 --- /dev/null +++ b/digits/standard-networks/torch/ImageNet-Training/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 Elad Hoffer + +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. + diff --git a/digits/standard-networks/torch/ImageNet-Training/alexnet.lua b/digits/standard-networks/torch/ImageNet-Training/alexnet.lua new file mode 100644 index 000000000..f62610d7d --- /dev/null +++ b/digits/standard-networks/torch/ImageNet-Training/alexnet.lua @@ -0,0 +1,42 @@ +-- Copyright (c) 2015 Elad Hoffer + +require 'cudnn' +require 'cunn' +require 'ccn2' + local SpatialConvolution = nn.SpatialConvolutionMM--lib[1] + local SpatialMaxPooling = cudnn.SpatialMaxPooling--lib[2] + local ReLU = nn.ReLU--lib[3] + + -- from https://code.google.com/p/cuda-convnet2/source/browse/layers/layers-imagenet-1gpu.cfg + -- this is AlexNet that was presented in the One Weird Trick paper. http://arxiv.org/abs/1404.5997 + local features = nn.Sequential() + features:add(SpatialConvolution(3,64,11,11,4,4,2,2)) -- 224 -> 55 + features:add(ReLU()) + features:add(SpatialMaxPooling(3,3,2,2)) -- 55 -> 27 + features:add(SpatialConvolution(64,192,5,5,1,1,2,2)) -- 27 -> 27 + features:add(ReLU()) + features:add(SpatialMaxPooling(3,3,2,2)) -- 27 -> 13 + features:add(SpatialConvolution(192,384,3,3,1,1,1,1)) -- 13 -> 13 + features:add(ReLU()) + features:add(SpatialConvolution(384,256,3,3,1,1,1,1)) -- 13 -> 13 + features:add(ReLU()) + features:add(SpatialConvolution(256,256,3,3,1,1,1,1)) -- 13 -> 13 + features:add(ReLU()) + features:add(SpatialMaxPooling(3,3,2,2)) -- 13 -> 6 + + local classifier = nn.Sequential() + classifier:add(nn.View(256*7*7)) + classifier:add(nn.Dropout(0.5)) + classifier:add(nn.Linear(256*7*7, 4096)) + classifier:add(nn.Threshold(0, 1e-6)) + classifier:add(nn.Dropout(0.5)) + classifier:add(nn.Linear(4096, 4096)) + classifier:add(nn.Threshold(0, 1e-6)) + classifier:add(nn.Linear(4096, 20)) + classifier:add(nn.LogSoftMax()) + + local model = nn.Sequential() + model:add(features):add(classifier) + + return model + diff --git a/digits/standard-networks/torch/ImageNet-Training/googlenet.lua b/digits/standard-networks/torch/ImageNet-Training/googlenet.lua new file mode 100644 index 000000000..e21061ffa --- /dev/null +++ b/digits/standard-networks/torch/ImageNet-Training/googlenet.lua @@ -0,0 +1,134 @@ +-- Copyright (c) 2015 Elad Hoffer + +require 'nn' +require 'cunn' +require 'cudnn' +require 'ccn2' +local opt = opt or {type = 'cuda', net='new'} +local DimConcat = 2 + +---------------------------------------Inception Modules------------------------------------------------- +local Inception = function(nInput, n1x1, n3x3r, n3x3, n5x5r, n5x5, nPoolProj) + local InceptionModule = nn.DepthConcat(DimConcat) + InceptionModule:add(nn.Sequential():add(nn.SpatialConvolutionMM(nInput,n1x1,1,1))) + InceptionModule:add(nn.Sequential():add(nn.SpatialConvolutionMM(nInput,n3x3r,1,1)):add(nn.ReLU()):add(nn.SpatialConvolutionMM(n3x3r,n3x3,3,3,1,1,1))) + InceptionModule:add(nn.Sequential():add(nn.SpatialConvolutionMM(nInput,n5x5r,1,1)):add(nn.ReLU()):add(nn.SpatialConvolutionMM(n5x5r,n5x5,5,5,1,1,2))) + InceptionModule:add(nn.Sequential():add(cudnn.SpatialMaxPooling(3,3,1,1)):add(nn.SpatialConvolutionMM(nInput,nPoolProj,1,1))) + return InceptionModule +end + +local AuxileryClassifier = function(nInput) + local C = nn.Sequential() + C:add(cudnn.SpatialAveragePooling(5,5,3,3)) + C:add(nn.SpatialConvolutionMM(nInput,128,1,1)) + C:add(nn.ReLU()) + C:add(nn.Reshape(128*4*4)) + C:add(nn.Linear(128*4*4,1024)) + C:add(nn.Dropout(0.7)) + C:add(nn.Linear(1024,1000)) + C:add(nn.LogSoftMax()) + return C +end +----------------------------------------------------------------------------------------------------------- + +local Net = nn.Sequential() + +local SubNet1 = nn.Sequential() +SubNet1:add(nn.SpatialConvolutionMM(3,64,7,7,2,2,4)) +SubNet1:add(nn.ReLU()) +SubNet1:add(cudnn.SpatialMaxPooling(3,3,2,2)) +--SubNet1:add(ccn2.SpatialResponseNormalization(3)) +SubNet1:add(nn.SpatialConvolutionMM(64,64,1,1)) +SubNet1:add(nn.ReLU()) +SubNet1:add(nn.SpatialConvolutionMM(64,192,3,3,1,1,1)) +SubNet1:add(nn.ReLU()) +--SubNet1:add(ccn2.SpatialResponseNormalization(3)) +SubNet1:add(nn.SpatialZeroPadding(1,1,1,1)) +SubNet1:add(cudnn.SpatialMaxPooling(3,3,2,2)) + + + +SubNet1:add(Inception(192,64,96,128,16,32,32)) +SubNet1:add(nn.ReLU()) +SubNet1:add(Inception(256,128,128,192,32,96,64)) +SubNet1:add(nn.ReLU()) +SubNet1:add(nn.SpatialZeroPadding(1,1,1,1)) +SubNet1:add(cudnn.SpatialMaxPooling(3,3,2,2)) +SubNet1:add(Inception(480,192,96,208,16,48,64)) +SubNet1:add(nn.ReLU()) + + + +local SubNet2 = nn.Sequential() +SubNet2:add(SubNet1) +SubNet2:add(Inception(512,160,112,224,24,64,64)) +SubNet2:add(nn.ReLU()) +SubNet2:add(Inception(512,128,128,256,24,64,64)) +SubNet2:add(nn.ReLU()) +SubNet2:add(Inception(512,112,144,288,32,64,64)) +SubNet2:add(nn.ReLU()) + + + +Net:add(SubNet2) +Net:add(Inception(528,256,160,320,32,128,128)) +Net:add(nn.ReLU()) +Net:add(nn.SpatialZeroPadding(1,1,1,1)) +Net:add(cudnn.SpatialMaxPooling(3,3,2,2)) + + +Net:add(Inception(832,256,160,320,32,128,128)) +Net:add(nn.ReLU()) +Net:add(Inception(832,384,192,384,48,128,128)) +Net:add(nn.ReLU()) +Net:add(cudnn.SpatialAveragePooling(7,7,1,1)) +Net:add(nn.Dropout(0.4)) +Net:add(nn.Reshape(1024)) +Net:add(nn.Linear(1024,1000)) +Net:add(nn.LogSoftMax()) + +local Classifier0 = nn.Sequential() +Classifier0:add(SubNet1) +Classifier0:add(AuxileryClassifier(512)) + +local Classifier1 = nn.Sequential() +Classifier1:add(SubNet2) +Classifier1:add(AuxileryClassifier(528)) + +-- +--Net:cuda() +------ Loss: NLL +--Net = Classifier0 +local loss = nn.ClassNLLCriterion() +---------------------------------------------------------------------- +if opt.type == 'cuda' then + Net:cuda() + loss:cuda() +end + +---------------------------------------------------------------------- +print '==> flattening Net parameters' + +-- Retrieve parameters and gradients: +-- this extracts and flattens all the trainable parameters of the mode +-- into a 1-dim vector +--end + +local w,dE_dw = Net:getParameters() + +local t = torch.load('Weights') +w:copy(t) +-- +--local t = torch.tic(); y = Net:forward(torch.rand(128,3,224,224):cuda()) ; cutorch.synchronize(); print(torch.tic()-t) +--print(SubNet1.modules[9].output:size()) +--print(y:size()) + + +-- return package: +return { + Net = Net, + Weights = w, + Grads = dE_dw, + Loss = loss +} + diff --git a/digits/standard-networks/torch/lenet.lua b/digits/standard-networks/torch/lenet.lua new file mode 100644 index 000000000..56d254672 --- /dev/null +++ b/digits/standard-networks/torch/lenet.lua @@ -0,0 +1,20 @@ +require 'nn' +require 'cunn' +require 'inn' + +-- -- This is a LeNet model. For more information: http://yann.lecun.com/exdb/lenet/ + +local model = nn.Sequential() +model:add(nn.MulConstant(0.00390625)) +model:add(nn.SpatialConvolution(1,20,5,5,1,1,0)) -- 1*28*28 -> 20*24*24 +model:add(inn.SpatialMaxPooling(2, 2, 2, 2)) -- 20*24*24 -> 20*12*12 +model:add(nn.SpatialConvolution(20,50,5,5,1,1,0)) -- 20*12*12 -> 50*8*8 +model:add(inn.SpatialMaxPooling(2,2,2,2)) -- 50*8*8 -> 50*4*4 +model:add(nn.View(-1):setNumInputDims(3)) -- 50*4*4 -> 800 +model:add(nn.Linear(800,500)) -- 800 -> 500 +model:add(nn.ReLU()) +model:add(nn.Linear(500, 10)) -- 500 -> 10 +model:add(nn.LogSoftMax()) +model:cuda() +return model + diff --git a/tools/torch/LRPolicy.lua b/tools/torch/LRPolicy.lua new file mode 100644 index 000000000..b7d021e26 --- /dev/null +++ b/tools/torch/LRPolicy.lua @@ -0,0 +1,81 @@ +-- Copyright (c) 2015, NVIDIA CORPORATION. All rights reserved. + +local LRPolicy = torch.class('LRPolicy') + +---------------------------------------------------------------------------------------- +-- This file contains details of learning rate policies that are used in caffe. +-- Calculates and returns the current learning rate. The currently implemented learning rate +-- policies are as follows: +-- - fixed: always return base_lr. +-- - step: return base_lr * gamma ^ (floor(iter / step)) +-- - exp: return base_lr * gamma ^ iter +-- - inv: return base_lr * (1 + gamma * iter) ^ (- power) +-- - multistep: similar to step but it allows non uniform steps defined by +-- stepvalue +-- - poly: the effective learning rate follows a polynomial decay, to be +-- zero by the max_iter. return base_lr (1 - iter/max_iter) ^ (power) +-- - sigmoid: the effective learning rate follows a sigmod decay +-- return base_lr ( 1/(1 + exp(-gamma * (iter - stepsize)))) +-------------------------------------------------------------------------------------------------- + + + +function LRPolicy:__init(...) + local args = dok.unpack( + {...}, + 'LRPolicy','Initialize a learning rate policy', + {arg='policy', type ='string', help='Learning rate policy',req=true}, + {arg='baselr', type ='number', help='Base learning rate',req=true}, + {arg='gamma', type = 'number', help='parameter to compute learning rate',req=false}, + {arg='power', type = 'number', help='parameter to compute learning rate', req = false}, + {arg='step_size', type = 'number', help='parameter to compute learning rate', req = false}, + {arg='max_iter', type = 'number', help='parameter to compute learning rate', req = false}, + {arg='step_values', type = 'table', help='parameter to compute learning rate. Useful only when the learning rate policy is multistep', req = false} + ) + self.policy = args.policy + self.baselr = args.baselr + self.gamma = args.gamma + self.power = args.power + self.step_values = args.step_values + self.max_iter = args.max_iter + + if self.policy == 'step' or self.policy == 'sigmoid' then + self.step_size = self.step_values[1] -- if the policy is not multistep, then even though multiple step values are provided as input, we will consider only the first value. + elseif self.policy == 'multistep' then + self.current_step = 1 -- this counter is important to take arbitary steps + self.stepvalue_size = #self.step_values + end + +end + +function LRPolicy:GetLearningRate(iter) + + local rate=0 + + if self.policy == "fixed" then + rate = self.baselr + elseif self.policy == "step" then + local current_step = math.floor(iter/self.step_size) + rate = self.baselr * math.pow(self.gamma, current_step) + elseif self.policy == "exp" then + rate = self.baselr * math.pow(self.gamma, iter) + elseif self.policy == "inv" then + rate = self.baselr * math.pow(1 + self.gamma * iter, - self.power) + elseif self.policy == "multistep" then + if (self.current_step < self.stepvalue_size and iter >= self.step_values[self.current_step]) then + self.current_step_ = self.current_step + 1 + --print("MultiStep Status: Iteration " .. iter .. ", step = " .. self.current_step) + end + rate = self.baselr * math.pow(self.gamma, self.current_step); + elseif self.policy == "poly" then + rate = self.baselr * math.pow(1.0 - (iter / self.max_iter), self.power) + elseif self.policy == "sigmoid" then + rate = self.baselr * (1.0 / (1.0 + exp(-self.gamma * (iter - self.step_size)))); + else + --have to include additional comments + print("Unknown learning rate policy: " .. self.policy) + end + + return rate + +end diff --git a/tools/torch/Optimizer.lua b/tools/torch/Optimizer.lua new file mode 100644 index 000000000..fd9696d8a --- /dev/null +++ b/tools/torch/Optimizer.lua @@ -0,0 +1,65 @@ +--[[ +Copyright (c) 2015, NVIDIA CORPORATION. All rights reserved. + +Copyright (c) 2004 Elad Hoffer + +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. +--]] + +local Optimizer = torch.class('Optimizer') + +function Optimizer:__init(...) + xlua.require('torch',true) + xlua.require('nn',true) + local args = dok.unpack( + {...}, + 'Optimizer','Initialize an optimizer', + {arg='Model', type ='table', help='Optimized model',req=true}, + {arg='Loss', type ='function', help='Loss function',req=true}, + {arg='Parameters', type = 'table', help='Model parameters - weights and gradients',req=false}, + {arg='OptFunction', type = 'function', help = 'Optimization function' ,req = true}, + {arg='OptState', type = 'table', help='Optimization configuration', default = {}, req=false}, + {arg='HookFunction', type = 'function', help='Hook function of type fun(y,yt,err)', req = false}, + {arg='lrPolicy', type = 'table', help='learning rate policy', req = true} + ) + self.Model = args.Model + self.Loss = args.Loss + self.Parameters = args.Parameters + self.OptFunction = args.OptFunction + self.OptState = args.OptState + self.HookFunction = args.HookFunction + self.lrPolicy = args.lrPolicy + + if self.Parameters == nil then + self.Parameters = {} + self.Weights, self.Gradients = self.Model:getParameters() + else + self.Weights, self.Gradients = self.Parameters[1], self.Parameters[2] + end +end + +function Optimizer:optimize(x,yt) + local f_eval = function() + self.Gradients:zero() + local y = self.Model:forward(x) + local err = self.Loss:forward(y,yt) + local dE_dy = self.Loss:backward(y,yt) + local value = nil + self.Model:backward(x, dE_dy) + if self.HookFunction then + value = self.HookFunction(y,yt,err) + end + return err, self.Gradients + end + + if self.lrPolicy.policy ~= 'torch_sgd' then + self.OptState.learningRate = self.lrPolicy:GetLearningRate(self.OptState.evalCounter or 0) --- here self.OptState.evalCounter = iter/stepsize + end + + return value, self.OptState.learningRate, self.OptFunction(f_eval, self.Weights, self.OptState) +end + diff --git a/tools/torch/README.txt b/tools/torch/README.txt new file mode 100644 index 000000000..574d7c7aa --- /dev/null +++ b/tools/torch/README.txt @@ -0,0 +1,60 @@ +Torch code can also be run from command line as shown below: + +To Train : +---------- + +th main.lua --train=/home/ubuntu/.digits/jobs/20150407-174547-f8f1/train_db --validation=/home/ubuntu/.digits/jobs/20150407-174547learningRateDecay-f8f1/val_db --network=lenet --networkDirectory=../../digits/standard-networks/torch/ --epoch=10 --save=/home/ubuntu/.digits/jobs/20150416-165008-4309 --snapshotPrefix=snapshot --snapshotInterval=1.000000 --subtractMean=yes --useMeanPixel=yes --mean=mean.jpg --labels=labels.txt --batchSize=32 --interval=1.000000 --learningRate=0.010000 --policy=step --gamma=0.100000 --stepvalues=33.000000 + +main.lua code uses data.lua module to load the images for training and validation, from lmdb. + +--train=/home/ubuntu/.digits/jobs/20150407-174547-f8f1/train_db => specifies train_db lmdb file contains all the images for training +--validation=/home/ubuntu/.digits/jobs/20150407-174547-f8f1/val_db => specifies val_db lmdb file contains all the images for validation. This is an optional. Validations won't be done if not provided. +--network=lenet => specifies that the network file is "lenet.lua" +--networkDirectory=../../digits/standard-networks/torch/ => specifies that "lenet.lua" is present in "../../digits/standard-networks/torch/" directory +--epoch=10 => specifies the total number of epochs +--save=/home/ubuntu/.digits/jobs/20150416-150654-37ca => specifies the directory where weights file (named __Weights.t7) and optimState (named optimState_.t7), are saved +--snapshotPrefix=snapshot => specifies the snapshot prefix of weights file of trained model +--snapshotInterval=1.000000 => specifies after every 1 training epoch, weights and optimState will be saved. + For instance, if this value is 1.21, then weights and optimState are saved for every 1.21 training epochs. +Note: Training will be done in batches, so some times saving weights (and optimState) for the given epoch value won't be possible. In this case the epoch value near to the given value will be considered. + +--subtractMean=yes => subtract mean from the test image. Default is 'yes' +--useMeanPixel=yes => specifies to use mean pixel instead of mean full matrix during image preprocessing. Default is using mean full matrix. + Below links contains more information about the same: + https://groups.google.com/forum/#!topic/torch7/66LMB-F-0ME + https://github.com/BVLC/caffe/issues/2069 + +--mean=mean.jpg => use mean.jpg file as mean file +--labels=labels.txt => specifies labels file. Each line in labels file specifies a distinct label name +--batchSize=32 => specifies the batch size. Make sure that the batch size is multiple of 32, when ccn2 network is used. +--interval=1.000000 => specifies that the model has to be validated against validation data for every one training epoch. + If this value is 0.5, then the model is validated for every half training epoch + +Implemented Caffe kind of learning policies in Torch. Please refer to "LRPolicy.lua" for more details regarding Learning Policies. +Here, learningRate, policy, gamma and stepvalues are the parameters of learning policy. +Note: if you want to use normal torch way of learning rate recalculation by SGD.lua, then use "torch_sgd" with policy parameter and also provide additional paramers like "learningRate", "learningRateDecay" as shown below, +--policy=torch_sgd +--learningRate= +--learningRateDecay= + +To Test a single image : +------------------------ + +th test.lua --image=/tmp/tmp4HHdV4.jpeg --network=lenet --networkDirectory=../../digits/standard-networks/torch/ --load=/home/ubuntu/.digits/jobs/20150416-150654-37ca --snapshotPrefix=snapshot --epoch=30 --subtractMean=yes --useMeanPixel=yes --mean=mean.jpg --labels=labels.txt + +where, +--image=/tmp/tmp4HHdV4.jpeg => specifies the image to be tested +--network=lenet => specifies that the network file is "lenet.lua" +--networkDirectory=../../digits/standard-networks/torch/ => specifies that "lenet.lua" is present in "../../digits/standard-networks/torch/" directory +--load=/home/ubuntu/.digits/jobs/20150416-150654-37ca => specifies that trained network, named __Weights.t7, exists in /home/ubuntu/.digits/jobs/20150416-150654-37ca +--snapshotPrefix=snapshot => specifies the snapshot prefix of weights file +--epoch=30 => specifies the weights file that was deployed during epoch 30 has to be loaded +--subtractMean=yes => subtract mean from the test image. Default is 'yes' +--useMeanPixel=yes => specifies to use mean pixel instead of mean full matrix during image preprocessing. Default is using mean full matrix. + Below links contains more information about the same: + https://groups.google.com/forum/#!topic/torch7/66LMB-F-0ME + https://github.com/BVLC/caffe/issues/2069 + +--mean=mean.jpg => use mean.jpg file as mean file +--labels=labels.txt => specifies labels file. Each line in labels file specifies a distinct label name + diff --git a/tools/torch/data.lua b/tools/torch/data.lua new file mode 100644 index 000000000..0cd9d7a1c --- /dev/null +++ b/tools/torch/data.lua @@ -0,0 +1,319 @@ +-- Copyright (c) 2015, NVIDIA CORPORATION. All rights reserved. +require 'torch' -- torch +require 'image' -- for color transforms +require 'nn' -- provides a normalization operator +require "pb" + +package.path = debug.getinfo(1, "S").source:match[[^@?(.*[\/])[^\/]-$]] .."?.lua;".. package.path + +require 'logmessage' +local datum = pb.require"datum" +local ffi = require 'ffi' + +local lightningmdb_lib=require "lightningmdb" +local lightningmdb = _VERSION=="Lua 5.2" and lightningmdb_lib or lightningmdb + + +---------------------------------------------------------------------- + +function copy (t) -- shallow-copy a table + if type(t) ~= "table" then return t end + local meta = getmetatable(t) + local target = {} + for k, v in pairs(t) do target[k] = v end + setmetatable(target, meta) + return target +end + + +local function cursor_pairs(cursor_,batch_size,key_,op_) + return coroutine.wrap( + function() + local k = key_,v + local i =0 + repeat + i=i+1 + k,v = cursor_:get(k,op_ or MDB.NEXT) + if k then + coroutine.yield(k,v) + --[[else + k,v = cursor_:get(k,op_ or MDB.FIRST) + coroutine.yield(k,v)]]-- + end + until i==batch_size + end) +end + +local function all_keys(cursor_,key_,op_) + return coroutine.wrap( + function() + local k = key_,v + repeat + k,v = cursor_:get(k,op_ or MDB.NEXT) + if k then + coroutine.yield(k,v) + end + until (not k) + end) +end + + +local PreProcess = function(y, mean, subtractMean, channels, mirror, crop, train, cropY, cropX, croplen) + if subtractMean == 'yes' then + for i=1,channels do + y[{ i,{},{} }]:add(-mean[i]) + end + end + if mirror == 'yes' and torch.FloatTensor.torch.uniform() > 0.49 then + y = image.hflip(y) + end + if crop == 'yes' then + + if train == true then + --During training we will crop randomly + local valueY = math.ceil(torch.FloatTensor.torch.uniform()*cropY) + local valueX = math.ceil(torch.FloatTensor.torch.uniform()*cropX) + y = image.crop(y, valueX-1, valueY-1, valueX+croplen-1, valueY+croplen-1) + + else + --for validation we will crop at center + y = image.crop(y, cropX-1, cropY-1, cropX+croplen-1, cropY+croplen-1) + end + end + return y +end + + +--Loading label definitions + +local loadLabels = function(labels_file) + local Classes = {} + i=0 + + local file = io.open(labels_file) + if file then + for line in file:lines() do + i=i+1 + Classes[i] = line + end + return Classes + else + return nil -- nil indicates that file not present + end +end + +--loading mean tensor +local loadMean = function(mean_file, use_mean_pixel) + local mean_t = {} + local mean_im = image.load(mean_file):type('torch.FloatTensor'):contiguous() + mean_t["channels"] = mean_im:size(1) + mean_t["height"] = mean_im:size(2) + mean_t["width"] = mean_im:size(3) + if use_mean_pixel == 'yes' then + mean_of_mean = torch.FloatTensor(mean_im:size(1)) + for i=1,mean_im:size(1) do + mean_of_mean[i] = mean_im[i]:mean() + end + mean_t["mean"] = mean_of_mean + else + mean_t["mean"] = mean_im + end + return mean_t +end + + +local function pt(t) + for k,v in pairs(t) do + print(k,v) + end +end + +-- Meta class +DBSource = {e=nil, t=nil, d=nil, c=nil, mean = nil, ImageChannels = 0, ImageSizeY = 0, ImageSizeX = 0, total=0, datum_t=datum, mirror='no', crop='no', croplen=0, cropY=0, cropX=0, subtractMean='yes', train=false} + +-- Derived class method new +function DBSource:new (db_name, mirror, crop, croplen, mean_t, subtractMean, isTrain) + local self = copy(DBSource) + self.mean = mean_t["mean"] + -- image channel, height and width details are extracted from mean.jpeg file. If mean.jpeg file is not present then probably the below three lines of code needs to be changed to provide hard-coded values. + self.ImageChannels = mean_t["channels"] + self.ImageSizeY = mean_t["height"] + self.ImageSizeX = mean_t["width"] + + logmessage.display(0,'Loaded train image details from the mean file: Image channels are ' .. self.ImageChannels .. ', Image width is ' .. self.ImageSizeY .. ' and Image height is ' .. self.ImageSizeX) + + self.e = lightningmdb.env_create() + local LMDB_MAP_SIZE = 1099511627776 -- 1 TB + self.e:set_mapsize(LMDB_MAP_SIZE) + self.e:open(db_name,lightningmdb.MDB_RDONLY + lightningmdb.MDB_NOTLS,0664) + self.total = self.e:stat().ms_entries + self.t = self.e:txn_begin(nil,lightningmdb.MDB_RDONLY) + self.d = self.t:dbi_open(nil,0) + self.c = self.t:cursor_open(self.d) + self.mirror = mirror + self.crop = crop + self.croplen = croplen + self.subtractMean = subtractMean + self.train = isTrain + + if crop == 'yes' then + if self.train == true then + self.cropY = self.ImageSizeY - croplen + 1 + self.cropX = self.ImageSizeX - croplen + 1 + else + self.cropY = math.floor((self.ImageSizeY - croplen)/2) + 1 + self.cropX = math.floor((self.ImageSizeX - croplen)/2) + 1 + end + end + + return self +end + +-- Derived class method getKeys +function DBSource:getKeys () + + local Keys = {} + local i=0 + local key=nil + for k,v in all_keys(self.c,nil,lightningmdb.MDB_NEXT) do + i=i+1 + Keys[i] = k + key = k + end + return Keys +end + +function getFirstImage() + return self.c:get(nil, MDB.FIRST) +end + +-- Derived class method getKeys +function DBSource:getImgUsingKey(key) + v = self.t:get(self.d,key,lightningmdb.MDB_FIRST) + local msg = datum.Datum():Parse(v) + local x=torch.ByteTensor(#msg.data+1):contiguous() + local temp_ptr=torch.data(x) + ffi.copy(temp_ptr, msg.data) + local y=nil + if msg.encoded==true then + y = image.decompress(x,msg.channels,'byte'):float() + else + y = x:narrow(1,1,msg.channels*msg.height*msg.width):view(msg.channels,msg.height,msg.width):float() + end + + local image_s = PreProcess(y, self.mean, self.subtractMean, msg.channels, self.mirror, self.crop, self.train, self.cropY, self.cropX, self.croplen) + + return image_s,tonumber(msg.label) + 1 + +end + +-- Derived class method nextBatch +function DBSource:nextBatch (batchsize) + + local Images + if self.crop == 'yes' then + Images = torch.Tensor(batchsize, self.ImageChannels, self.croplen, self.croplen) + else + --Images = torch.FloatTensor(batchsize, self.ImageChannels, self.ImageSizeY, self.ImageSizeX):contiguous() -- This needs to be checked later. Is contiguous is necessary? + Images = torch.Tensor(batchsize, self.ImageChannels, self.ImageSizeY, self.ImageSizeX) + end + local Labels = torch.Tensor(batchsize) + + --local data=torch.data(Images) + + local i=0 + + --local key=nil + +--[[ + if self.current == 0 then + i=i+1 + local k,v=self.c:get(nil,lightningmdb.MDB_FIRST) + local msg = datum.Datum():Parse(v) + local x = torch.ByteTensor(#msg.data) + local temp_ptr = torch.data(x) -- raw C pointer using torchffi + ffi.copy(temp_ptr, msg.data) + local y = x:reshape(msg.channels,msg.height,msg.width):float() + Images[i] = PreProcess(y, self.mean) + Labels[i] = tonumber(msg.label) + 1 + end +]]-- + local total = self.ImageChannels*self.ImageSizeY*self.ImageSizeX + -- Tensor allocations inside loop consumes little more execution time. So allocated "x" outiside with double size of an image and inside loop if any encoded image is encountered with bytes size more than Tensor size, then the Tensor is resized appropriately. + local x = torch.ByteTensor(total*2):contiguous() -- some times length of JPEG files are more than total size. So, "x" is allocated with more size to ensure that data is not truncated while copying. + local x_size = total * 2 -- This variable is just to avoid the calls to tensor's size() i.e., x:size(1) + local temp_ptr = torch.data(x) -- raw C pointer using torchffi + + --local mean_ptr = torch.data(self.mean) + local image_ind = 0 + + for k,v in cursor_pairs(self.c,batchsize,nil,lightningmdb.MDB_NEXT) do + i=i+1 +--local a = torch.Timer() +--local m = a:time().real + local msg = datum.Datum():Parse(v) + + if #msg.data > x_size then + x:resize(#msg.data+1) -- 1 extra byte is required to copy zero terminator i.e., '\0', by ffi.copy() + x_size = #msg.data + end + + ffi.copy(temp_ptr, msg.data) +--print(string.format("elapsed time1: %.6f\n", a:time().real - m)) +--m = a:time().real + + local y=nil + if msg.encoded==true then + y = image.decompress(x,msg.channels,'byte'):float() + else + y = x:narrow(1,1,total):view(msg.channels,msg.height,msg.width):float() -- using narrow() returning the reference to x tensor with the size exactly equal to total image byte size, so that view() works fine without issues + end + + --[[for ind=1,total do + data[image_ind+ind] = temp_ptr[ind]-mean_ptr[ind] + end]]-- + + Images[i] = PreProcess(y, self.mean, self.subtractMean, msg.channels, self.mirror, self.crop, self.train, self.cropY, self.cropX, self.croplen) + + --print(string.format("elapsed time2: %.6f\n", a:time().real - m)) + + Labels[i] = tonumber(msg.label) + 1 + --image_ind = image_ind + total + + --key = k + end + return Images, Labels +end + +-- Derived class method to reset cursor +function DBSource:reset () + + self.c:close() + self.e:dbi_close(self.d) + self.t:abort() + self.t = self.e:txn_begin(nil,lightningmdb.MDB_RDONLY) + self.d = self.t:dbi_open(nil,0) + self.c = self.t:cursor_open(self.d) +end + +-- Derived class method to get total number of Records +function DBSource:totalRecords () + return self.total; +end + +-- Derived class method close +function DBSource:close (batchsize) + self.total = 0 + self.c:close() + self.e:dbi_close(self.d) + self.t:abort() + self.e:close() +end + + +return{ + loadLabels = loadLabels, + loadMean= loadMean, + PreProcess = PreProcess +} + diff --git a/tools/torch/datum.proto b/tools/torch/datum.proto new file mode 100644 index 000000000..9f04638f1 --- /dev/null +++ b/tools/torch/datum.proto @@ -0,0 +1,12 @@ +// Copyright (c) 2015, NVIDIA CORPORATION. All rights reserved. +message Datum { +optional int32 channels = 1; +optional int32 height = 2; +optional int32 width = 3; +// the actual image data, in bytes +optional bytes data = 4; +optional int32 label = 5; +// Optionally, the datum could also hold float data. +repeated float float_data = 6; +optional bool encoded = 7 [default = false]; +} diff --git a/tools/torch/logmessage.lua b/tools/torch/logmessage.lua new file mode 100644 index 000000000..4800d4457 --- /dev/null +++ b/tools/torch/logmessage.lua @@ -0,0 +1,27 @@ +-- Copyright (c) 2015, NVIDIA CORPORATION. All rights reserved. + +-- This file contains the logic of printing log messgages +local logmessage = torch.class('logmessage') + +------------------------------------------------------------------------------------------------------------- +-- display function accepts two input parameters: +-- parameter_name format description +-- levelcode number specifies the severity of message i.e., 0=info, 1=warn, 2=error. +-- message string message to be displayed + +-- Usage: +-- require 'logmessage' +-- logmessage.display(0,'This is informational message as the levelcode is 0') +------------------------------------------------------------------------------------------------------------- +function logmessage.display(levelcode, message) + local levelname=nil + if levelcode == 0 then + levelname="INFO " + elseif levelcode == 1 then + levelname="WARNING" + elseif levelcode == 2 then + levelname="ERROR" + end + print(os.date("%Y-%m-%d %H:%M:%S") .. ' [' .. levelname .. '] ' .. message) +end + diff --git a/tools/torch/main.lua b/tools/torch/main.lua new file mode 100644 index 000000000..5adf19e6b --- /dev/null +++ b/tools/torch/main.lua @@ -0,0 +1,732 @@ +-- Copyright (c) 2015, NVIDIA CORPORATION. All rights reserved. + +require 'torch' +require 'xlua' +require 'optim' +require 'pl' +require 'trepl' +require 'cutorch' +require 'lfs' + +local dir_path = debug.getinfo(1,"S").source:match[[^@?(.*[\/])[^\/]-$]] +if dir_path ~= nil then + package.path = dir_path .."?.lua;".. package.path +end + +require 'Optimizer' +require 'LRPolicy' +require 'logmessage' + +-- load utils +local utils = require 'utils' +---------------------------------------------------------------------- + +opt = lapp[[ +Usage details: +-a,--threads (default 8) number of threads +-b,--batchSize (default 128) batch size +-c,--learningRateDecay (default 1e-6) learning rate decay (in # samples) +-d,--devid (default 1) device ID (if using CUDA) +-e,--epoch (number) number of epochs to train -1 for unbounded +-f,--shuffle (default no) shuffle records before train +-g,--mirror (default no) If this option is 'yes', then some of the images are randomly mirrored +-i,--interval (default 1) number of train epochs to complete, to perform one validation +-k,--crop (default no) If this option is 'yes', all the images are randomly cropped into square image. And croplength is provided as --croplen parameter +-l,--croplen (default 0) crop length. This is required parameter when crop option is provided +-m,--momentum (default 0.9) momentum +-n,--network (string) Model - must return valid network. Available - {lenet, googlenet, alexnet} +-o,--optimization (default sgd) optimization method +-p,--type (default cuda) float or cuda +-r,--learningRate (default 0.001) learning rate +-s,--save (default results) save directory +-t,--train (string) location in which train db exists. +-v,--validation (default '') location in which validation db exists. +-w,--weightDecay (default 1e-4) L2 penalty on the weights + +--seed (default '') fixed input seed for repeatable experiments +--weights (default '') filename for weights of a model to use for fine-tuning +--retrain (default '') Specifies path to model to retrain with +--optimState (default '') Specifies path to an optimState to reload from +--randomState (default '') Specifies path to a random number state to reload from +--lrpolicyState (default '') Specifies path to a lrpolicy state to reload from +--networkDirectory (default '') directory in which network exists +--mean (default mean.jpg) mean file. Mean file is used to preprocess images and it is also required to get the details of image channel, height and width. +--subtractMean (default yes) If yes, subtracts the mean from images +--labels (default labels.txt) file contains label definitions +--snapshotPrefix (default '') prefix of the weights/snapshots +--snapshotInterval (default 1) specifies the training epochs to be completed before taking a snapshot +--useMeanPixel (default 'no') by default pixel-wise subtraction is done using the full mean matrix. If this option is 'yes' then mean pixel will be used instead of mean matrix + +-q,--policy (default torch_sgd) Learning Rate Policy. Valid policies : fixed, step, exp, inv, multistep, poly, sigmoid and torch_sgd. Note: when power value is -1, then "inv" policy with "gamma" is similar to "torch_sgd" with "learningRateDecay". +-h,--gamma (default -1) Required to calculate learning rate, when any of the following learning rate policies are used: step, exp, inv, multistep & sigmoid +-j,--power (default inf) Required to calculate learning rate, when any of the following learning rate policies are used: inv & poly +-x,--stepvalues (default '') Required to calculate stepsize for the following learning rate policies: step, multistep & sigmoid. Note: if it is 'step' or 'sigmoid' policy, then this parameter expects single value, if it is 'multistep' policy, then this parameter expects a string which has all the step values delimited by comma (ex: "10,25,45,80") +]] + +----------------------------------------------------------------------------------------------------------------------------- +--Note: At present DIGITS supports only fine tuning, which means copying only the weights from pretrained model. +-- +--To include "crash recovery" feature, we may need to save the below torch elements for every fixed duration (or) for every fixed epochs (for instance 30 minutes or 10 epochs). +-- +-- trained model +-- SGD optim state +-- LRPolicy - this module helps in implementing caffe learning policies in Torch +-- Random number state +-- +--And if the job was crashed, provide the saved backups using the command options (--retrain, --optimState, --randomState, --lrpolicyState) while restarting the job. +-- +--Please refer to below links for more information about "crash recovery" feature: +-- 1) https://groups.google.com/forum/#!searchin/torch7/optimstate/torch7/uNxnrH-7C-4/pgIBdAFVaOYJ +-- 2) https://groups.google.com/forum/#!topic/torch7/fcy0-5v6M08 +-- 3) https://groups.google.com/forum/#!searchin/torch7/optimstate/torch7/Gv1BiQoaIVA/HRnjRoegR38J +-- +--Almost all the required routines are already implemented. Below are some remaining tasks, +-- 1) while recovering from crash, we should only consider the below options and discard all other inputs like epoch +-- --retrain, --optimState, --randomState, --lrpolicyState, --networkDirectory, --network, --save, --train, --validation, --mean, --labels, --snapshotPrefix +-- 2) We should also save and restore some information like epoch, batch size, snapshot interval, subtractMean and useMeanPizel option, shuffle, mirror, crop, croplen +-- Precautions should be taken while restoring these options. +----------------------------------------------------------------------------------------------------------------------------- + + +-- Set the seed of the random number generator to the given number. +if opt.seed ~= '' then + torch.manualSeed(tonumber(opt.seed)) +end + +-- validate options +if opt.crop == 'yes' and opt.croplen == 0 then + logmessage.display(2,'crop length is missing') + return +end + +local stepvalues_list = {} + +-- verify whether required learning rate parameters are provided to calculate learning rate when caffe-like learning rate policies are used +if opt.policy == 'fixed' or opt.policy == 'step' or opt.policy == 'exp' or opt.policy == 'inv' or opt.policy == 'multistep' or opt.policy == 'poly' or opt.policy == 'sigmoid' then + + if opt.policy == 'step' or opt.policy == 'exp' or opt.policy == 'inv' or opt.policy == 'multistep' or opt.policy == 'sigmoid' then + if opt.gamma ==-1 then + logmessage.display(2,'gamma parameter missing and is required to calculate learning rate when ' .. opt.policy .. ' learning rate policy is used') + return + end + end + + if opt.policy == 'inv' or opt.policy == 'poly' then + if opt.power == math.huge then + logmessage.display(2,'power parameter missing and is required to calculate learning rate when ' .. opt.policy .. ' learning rate policy is used') + return + end + end + + if opt.policy == 'step' or opt.policy == 'multistep' or opt.policy == 'sigmoid' then + if opt.stepvalues =='' then + logmessage.display(2,'step parameter missing and is required to calculate learning rate when ' .. opt.policy .. ' learning rate policy is used') + return + else + + for i in string.gmatch(opt.stepvalues, '([^,]+)') do + if tonumber(i) ~= nil then + table.insert(stepvalues_list, tonumber(i)) + else + logmessage.display(2,'invalid step parameter value : ' .. opt.stepvalues .. '. step parameter should contain only number. if there are more than one value, then the values should be delimited by comma. ex: "10" or "10,25,45,80"') + return + + end + end + end + end + +elseif opt.policy ~= 'torch_sgd' then + logmessage.display(2,'invalid learning rate policy - '.. opt.policy .. '. Valid policies : fixed, step, exp, inv, multistep, poly, sigmoid and torch_sgd') + return +end + +if opt.useMeanPixel ~= 'yes' and opt.useMeanPixel ~= 'no' then + logmessage.display(2,'invalid --useMeanPixel parameter value - '.. opt.useMeanPixel .. '. Only "yes" or "no" is allowed') + return +end + +if opt.useMeanPixel == 'yes' and opt.subtractMean ~= 'no' then + opt.useMeanPixel = 'no' + logmessage.display(0,'useMeanPixel parameter is not considered as subtractMean value is provided as "yes"') +end + +if opt.retrain ~= '' and opt.weights ~= '' then + logmessage.display(2,"Both '--retrain' and '--weights' options cannot be used at the same time.") + return +end + +if opt.randomState ~= '' and opt.seed ~= '' then + logmessage.display(2,"Both '--randomState' and '--seed' options cannot be used at the same time.") + return +end + +torch.setnumthreads(opt.threads) +cutorch.setDevice(opt.devid) +---------------------------------------------------------------------- +-- Model + Loss: + +package.path = paths.concat(opt.networkDirectory, "?.lua") ..";".. package.path +logmessage.display(0,'Loading network definition from ' .. paths.concat(opt.networkDirectory, opt.network)) +local model = require (opt.network) + +local loss = nn.ClassNLLCriterion() + +-- check whether ccn2 is used in network and then check whether given batchsize is valid or not +if ccn2 ~= nil then + if opt.batchSize % 32 ~= 0 then + logmessage.display(2,'invalid batch size : ' .. opt.batchSize .. '. Batch size should be multiple of 32 when ccn2 is used in the network') + return + end +end + +-- load +local data = require 'data' + +logmessage.display(0,'Loading mean tensor from '.. opt.mean ..' file') +local mean_t = data.loadMean(opt.mean, opt.useMeanPixel) + +logmessage.display(0,'Loading label definitions from '.. opt.labels ..' file') +-- classes +local classes = data.loadLabels(opt.labels) + +if classes == nil then + logmessage.display(2,'labels file '.. opt.labels ..' not found') + return +end + +logmessage.display(0,'found ' .. #classes .. ' categories') + +-- fix final output dimension of network +utils.correctFinalOutputDim(model, #classes) +logmessage.display(0,'Network definition: \n' .. model:__tostring__()) +logmessage.display(0,'Network definition ends') + +if opt.mirror == 'yes' then + --torch.manualSeed(os.time()) + logmessage.display(0,'mirror option was selected, so during training for some of the random images, mirror view will be considered instead of original image view') +end + +-- NOTE: currently randomState option wasn't used in DIGITS. This option was provided to be used from command line, if required. +-- load random number state from backup +if opt.randomState ~= '' then + if paths.filep(opt.randomState) then + logmessage.display(0,'Loading random number state - ' .. opt.randomState) + torch.setRNGState(torch.load(opt.randomState)) + else + logmessage.display(2,'random number state not found: ' .. opt.randomState) + return + end +end + +---------------------------------------------------------------------- + +-- This matrix records the current confusion across classes +local confusion = optim.ConfusionMatrix(classes) + +-- seperate validation matrix for validation data +local validation_confusion = nil +if opt.validation ~= '' then + validation_confusion = optim.ConfusionMatrix(classes) +end + +-- NOTE: currently retrain option wasn't used in DIGITS. This option was provided to be used from command line, if required. +-- If preloading option is set, preload existing models appropriately +if opt.retrain ~= '' then + if paths.filep(opt.retrain) then + logmessage.display(0,'Loading pretrained model - ' .. opt.retrain) + model = torch.load(opt.retrain) + else + logmessage.display(2,'Pretrained model not found: ' .. opt.retrain) + return + end +end + +local Weights,Gradients = model:getParameters() +-- If weights option is set, preload weights from existing models appropriately +if opt.weights ~= '' then + if paths.filep(opt.weights) then + logmessage.display(0,'Loading weights from pretrained model - ' .. opt.weights) + Weights:copy(torch.load(opt.weights)) + else + logmessage.display(2,'Weight file for pretrained model not found: ' .. opt.weights) + return + end +end + +if opt.type == 'float' then + logmessage.display(0,'switching to floats') + torch.setdefaulttensortype('torch.FloatTensor') + +elseif opt.type =='cuda' then + require 'cunn' + logmessage.display(0,'switching to CUDA') + model:cuda() + loss = loss:cuda() + --torch.setdefaulttensortype('torch.CudaTensor') +end + +-- create a directory, if not exists, to save all the snapshots +-- os.execute('mkdir -p ' .. paths.concat(opt.save)) -- commented this line, as os.execute command is not portable +if lfs.mkdir(paths.concat(opt.save)) then + logmessage.display(0,'created a directory ' .. paths.concat(opt.save) .. ' to save all the snapshots') +end + +-- open train lmdb file +logmessage.display(0,'opening train lmdb file: ' .. opt.train) +local train = DBSource:new(opt.train, opt.mirror, opt.crop, opt.croplen, mean_t, opt.subtractMean, true) +local trainSize = train:totalRecords() +logmessage.display(0,'found ' .. trainSize .. ' images in train db' .. opt.train) +local trainKeys +if opt.shuffle == 'yes' then + logmessage.display(0,'loading all the keys from train db') + trainKeys = train:getKeys() +end + +local val, valSize, valKeys + +if opt.validation ~= '' then + logmessage.display(0,'opening validation lmdb file: ' .. opt.validation) + -- for the images in validation dataset, no need to do random mirrorring. + val = DBSource:new(opt.validation, 'no', opt.crop, opt.croplen, mean_t, opt.subtractMean, false) + valSize = val:totalRecords() + logmessage.display(0,'found ' .. valSize .. ' images in train db' .. opt.validation) + if opt.shuffle == 'yes' then + logmessage.display(0,'loading all the keys from validation db') + valKeys = val:getKeys() + end +end + +-- validate "crop length" input parameter +if opt.crop == 'yes' then + if opt.croplen > train.ImageSizeY then + logmessage.display(2,'invalid crop length! crop length ' .. opt.croplen .. ' is less than image width ' .. train.ImageSizeY) + return + elseif opt.croplen > train.ImageSizeX then + logmessage.display(2,'invalid crop length! crop length ' .. opt.croplen .. ' is less than image height ' .. train.ImageSizeX) + return + end +end + +--modifying total sizes of train and validation dbs to be the exact multiple of 32, when cc2 is used +if ccn2 ~= nil then + if (trainSize % 32) ~= 0 then + logmessage.display(1,'when ccn2 is used, total images should be the exact multiple of 32. In train db, as the total images are ' .. trainSize .. ', skipped the last ' .. trainSize % 32 .. ' images from train db') + trainSize = trainSize - (trainSize % 32) + end + if opt.validation ~= '' and (valSize % 32) ~=0 then + logmessage.display(1,'when ccn2 is used, total images should be the exact multiple of 32. In validation db, as the total images are ' .. valSize .. ', skipped the last ' .. valSize % 32 .. ' images from validation db') + valSize = valSize - (valSize % 32) + end +end + +--initializing learning rate policy +logmessage.display(0,'initializing the parameters for learning rate policy: ' .. opt.policy) + +local lrpolicy = {} +if opt.policy ~= 'torch_sgd' then + + local max_iterations = (math.ceil(trainSize/opt.batchSize))*opt.epoch + --local stepsize = math.floor((max_iterations*opt.step/100)+0.5) --adding 0.5 to round the value + + if max_iterations < #stepvalues_list then + logmessage.display(1,'maximum iterations (i.e., ' .. max_iterations .. ') is less than provided step values count (i.e, ' .. #stepvalues_list .. '), so learning rate policy is reset to "step" policy with the step value 1.') + opt.policy = 'step' + stepvalues_list[1] = 1 + else + -- converting stepsize percentages into values + for i=1,#stepvalues_list do + stepvalues_list[i] = utils.round(max_iterations*stepvalues_list[i]/100) + + -- avoids 'nan' values during learning rate calculation + if stepvalues_list[i] == 0 then + stepvalues_list[i] = 1 + end + end + end + + lrpolicy = LRPolicy{ + policy = opt.policy, + baselr = opt.learningRate, + gamma = opt.gamma, + power = opt.power, + max_iter = max_iterations, + step_values = stepvalues_list + } + +else + lrpolicy = LRPolicy{ + policy = opt.policy, + baselr = opt.learningRate + } + +end + +-- NOTE: currently lrpolicyState option wasn't used in DIGITS. This option was provided to be used from command line, if required. +if opt.lrpolicyState ~= '' then + if paths.filep(opt.lrpolicyState) then + logmessage.display(0,'Loading lrpolicy state from file: ' .. opt.lrpolicyState) + lrpolicy = torch.load(opt.lrpolicyState) + else + logmessage.display(2,'lrpolicy state file not found: ' .. opt.lrpolicyState) + return + end +end + + +--resetting "learningRateDecay = 0", so that sgd.lua won't recalculates the learning rate +if lrpolicy.policy ~= 'torch_sgd' then + opt.learningRateDecay = 0 +end + + +local optimState = { + learningRate = opt.learningRate, + momentum = opt.momentum, + weightDecay = opt.weightDecay, + learningRateDecay = opt.learningRateDecay +} + +-- NOTE: currently optimState option wasn't used in DIGITS. This option was provided to be used from command line, if required. +if opt.optimState ~= '' then + if paths.filep(opt.optimState) then + logmessage.display(0,'Loading optimState from file: ' .. opt.optimState) + optimState = torch.load(opt.optimState) + + -- this makes sure that sgd.lua won't recalculates the learning rate while using learning rate policy + if lrpolicy.policy ~= 'torch_sgd' then + optimState.learningRateDecay = 0 + end + else + logmessage.display(1,'Optim state file not found: ' .. opt.optimState) -- if optim state file isn't found, notify user and continue training + end +end + +local function updateConfusion(y,yt) + confusion:batchAdd(y,yt) +end + +-- Optimization configuration +logmessage.display(0,'initializing the parameters for Optimizer') +local optimizer = Optimizer{ + Model = model, + Loss = loss, + --OptFunction = optim.sgd, + OptFunction = _G.optim[opt.optimization], + OptState = optimState, + Parameters = {Weights, Gradients}, + HookFunction = updateConfusion, + lrPolicy = lrpolicy +} + +-- During training, loss rate should be displayed at max 8 times or for every 5000 images, whichever lower. +local logging_check = 0 + +if (math.ceil(trainSize/8)<5000) then + logging_check = math.ceil(trainSize/8) +else + logging_check = 5000 +end +logmessage.display(0,'During training. details will be logged after every ' .. logging_check .. ' images') + + +-- This variable keeps track of next epoch, when to perform validation. +local next_validation = opt.interval +logmessage.display(0,'Training epochs to be completed for each validation : ' .. opt.interval) +local last_validation_epoch = 0 + +-- This variable keeps track of next epoch, when to save model weights. +local next_snapshot_save = opt.snapshotInterval +logmessage.display(0,'Training epochs to be completed before taking a snapshot : ' .. opt.snapshotInterval) +local last_snapshot_save_epoch = 0 + +local snapshot_prefix = '' + +if opt.snapshotPrefix ~= '' then + snapshot_prefix = opt.snapshotPrefix +else + snapshot_prefix = opt.network +end + +-- epoch value will be calculated for every batch size. To maintain unique epoch value between batches, it needs to be rounded to the required number of significant digits. +local epoch_round = 0 -- holds the required number of significant digits for round function. +local tmp_batchsize = opt.batchSize +while tmp_batchsize <= trainSize do + tmp_batchsize = tmp_batchsize * 10 + epoch_round = epoch_round + 1 +end +logmessage.display(0,'While logging, epoch value will be rounded to ' .. epoch_round .. ' significant digits') + +logmessage.display(0,'Model weights will be saved as ' .. snapshot_prefix .. '__Weights.t7') + + +--[[ -- NOTE: uncomment this block when "crash recovery" feature was implemented +logmessage.display(0,'model, lrpolicy, optim state and random number states will be saved for recovery from crash') +logmessage.display(0,'model will be saved as ' .. snapshot_prefix .. '__model.t7') +logmessage.display(0,'optim state will be saved as optimState_.t7') +logmessage.display(0,'random number state will be saved as randomState_.t7') +logmessage.display(0,'LRPolicy state will be saved as lrpolicy_.t7') +--]] + + +-- NOTE: currently this routine wasn't used in DIGITS. +-- This routine takes backup of model, optim state, LRPolicy and random number state +local function backupforrecovery(backup_epoch) + -- save model + local filename = paths.concat(opt.save, snapshot_prefix .. '_' .. backup_epoch .. '_model.t7') + logmessage.display(0,'Saving model to ' .. filename) + utils.cleanupModel(model) + torch.save(filename, model) + logmessage.display(0,'Model saved - ' .. filename) + + --save optim state + filename = paths.concat(opt.save, 'optimState_' .. backup_epoch .. '.t7') + logmessage.display(0,'optim state saving to ' .. filename) + torch.save(filename, optimState) + logmessage.display(0,'optim state saved - ' .. filename) + + --save random number state + filename = paths.concat(opt.save, 'randomState_' .. backup_epoch .. '.t7') + logmessage.display(0,'random number state saving to ' .. filename) + torch.save(filename, torch.getRNGState()) + logmessage.display(0,'random number state saved - ' .. filename) + + --save lrPolicy state + filename = paths.concat(opt.save, 'lrpolicy_' .. backup_epoch .. '.t7') + logmessage.display(0,'lrpolicy state saving to ' .. filename) + torch.save(filename, optimizer.lrPolicy) + logmessage.display(0,'lrpolicy state saved - ' .. filename) +end + +-- Validation function +local function Validation() + + model:evaluate() + local shuffle + if opt.shuffle == 'yes' then + shuffle = torch.randperm(valSize):cuda() + end + + local NumBatches = 0 + local loss_sum = 0 + local inputs, targets + + if opt.shuffle == 'yes' then + if opt.crop == 'yes' then + inputs = torch.Tensor(opt.batchSize, val.ImageChannels, opt.croplen, opt.croplen) + else + inputs = torch.Tensor(opt.batchSize, val.ImageChannels, val.ImageSizeY, val.ImageSizeX) + end + targets = torch.Tensor(opt.batchSize) + end + + for t = 1,valSize,opt.batchSize do + + -- create mini batch + NumBatches = NumBatches + 1 + + if opt.shuffle == 'yes' then + local ind = 0 + for i = t,math.min(t+opt.batchSize-1,valSize) do + -- load new sample + local input, target = val:getImgUsingKey(valKeys[shuffle[i]]) + ind = ind+1 + inputs[ind] = input + targets[ind] = target + end + if ind < opt.batchSize then + inputs = inputs:narrow(1,1,ind) + targets = targets:narrow(1,1,ind) + end + + else + inputs,targets = val:nextBatch(math.min(valSize-t+1,opt.batchSize)) + end + + if opt.type =='cuda' then + inputs=inputs:cuda() + targets = targets:cuda() + else + inputs=inputs:float() + end + + local y = model:forward(inputs) + local err = loss:forward(y,targets) + loss_sum = loss_sum + err + validation_confusion:batchAdd(y,targets) + + if math.fmod(NumBatches,50)==0 then + collectgarbage() + end + end + + return (loss_sum/NumBatches) + + --xlua.progress(valSize, valSize) +end + +-- Train function +local function Train(epoch) + + model:training() + local shuffle=nil; + if opt.shuffle == 'yes' then + shuffle = torch.randperm(trainSize):cuda() + end + + local NumBatches = 0 + local curr_images_cnt = 0 + local loss_sum = 0 + local loss_batches_cnt = 0 + local learningrate = 0 + local inputs, targets + + if opt.shuffle == 'yes' then + if opt.crop == 'yes' then + inputs = torch.Tensor(opt.batchSize, train.ImageChannels, opt.croplen, opt.croplen) + else + inputs = torch.Tensor(opt.batchSize, train.ImageChannels, train.ImageSizeY, train.ImageSizeX) + end + + targets = torch.Tensor(opt.batchSize) + end + + for t = 1,trainSize,opt.batchSize do + + -- create mini batch + NumBatches = NumBatches + 1 + if opt.shuffle == 'yes' then + local ind = 0 + for i = t,math.min(t+opt.batchSize-1,trainSize) do + -- load new sample + local input, target = train:getImgUsingKey(trainKeys[shuffle[i]]) + ind = ind+1 + inputs[ind] = input -- this is similar to inputs[i%batchSize] + targets[ind] = target + end + -- if the final set of images are less than batch size, then resize inputs and targets tensors + if ind < opt.batchSize then + inputs = inputs:narrow(1,1,ind) + targets = targets:narrow(1,1,ind) + end + else + inputs,targets = train:nextBatch(math.min(trainSize-t+1,opt.batchSize)) + end + + if opt.type =='cuda' then + inputs = inputs:cuda() + targets = targets:cuda() + else + inputs = inputs:float() + end + + _,learningrate,_,trainerr = optimizer:optimize(inputs, targets) + + -- adding the loss values of each mini batch and also maintaining the counter for number of batches, so that average loss value can be found at the time of logging details + loss_sum = loss_sum + trainerr[1] + loss_batches_cnt = loss_batches_cnt + 1 + + if math.fmod(NumBatches,50)==0 then + collectgarbage() + end + + local current_epoch = (epoch-1)+utils.round((math.min(t+opt.batchSize-1,trainSize))/trainSize, epoch_round) + + -- log details when required number of images are processed + curr_images_cnt = curr_images_cnt + opt.batchSize + if curr_images_cnt >= logging_check then + logmessage.display(0, 'Training (epoch ' .. current_epoch .. '): loss = ' .. (loss_sum/loss_batches_cnt) .. ', lr = ' .. learningrate) + curr_images_cnt = 0 -- For accurate values we may assign curr_images_cnt % logging_check to curr_images_cnt, instead of 0 + loss_sum = 0 + loss_batches_cnt = 0 + end + + if opt.validation ~= '' and current_epoch >= next_validation then + validation_confusion:zero() + val:reset() + local avg_loss=Validation() + validation_confusion:updateValids() + -- log details at the end of validation + logmessage.display(0, 'Validation (epoch ' .. current_epoch .. '): loss = ' .. avg_loss .. ', accuracy = ' .. validation_confusion.totalValid) + + next_validation = (utils.round(current_epoch/opt.interval) + 1) * opt.interval -- To find next nearest epoch value that exactly divisible by opt.interval + last_validation_epoch = current_epoch + model:training() -- to reset model to training + end + + if current_epoch >= next_snapshot_save then + -- save weights + local filename = paths.concat(opt.save, snapshot_prefix .. '_' .. current_epoch .. '_Weights.t7') + logmessage.display(0,'Snapshotting to ' .. filename) + torch.save(filename, Weights) + logmessage.display(0,'Snapshot saved - ' .. filename) + + next_snapshot_save = (utils.round(current_epoch/opt.snapshotInterval) + 1) * opt.snapshotInterval -- To find next nearest epoch value that exactly divisible by opt.snapshotInterval + last_snapshot_save_epoch = current_epoch + end + + end + + -- display the progress at the end of epoch + if curr_images_cnt > 0 then + logmessage.display(0, 'Training (epoch ' .. epoch .. '): loss = ' .. (loss_sum/loss_batches_cnt) .. ', lr = ' .. learningrate) + end + + --xlua.progress(trainSize, trainSize) + +end + + +------------------------------ + +local epoch = 1 + +logmessage.display(0,'started training the model') + +-- run an initial validation before the first train epoch +if opt.validation ~= '' then + model:evaluate() + validation_confusion:zero() + val:reset() + local avg_loss=Validation() + validation_confusion:updateValids() + -- log details at the end of validation + logmessage.display(0, 'Validation (epoch ' .. epoch-1 .. '): loss = ' .. avg_loss .. ', accuracy = ' .. validation_confusion.totalValid) + model:training() -- to reset model to training +end + +while epoch<=opt.epoch do + local ErrTrain = 0 + train:reset() + confusion:zero() + Train(epoch) + confusion:updateValids() + ErrTrain = (1-confusion.totalValid) + epoch = epoch+1 +end + + +-- if required, perform validation at the end +if opt.validation ~= '' and opt.epoch > last_validation_epoch then + validation_confusion:zero() + val:reset() + local avg_loss=Validation() + validation_confusion:updateValids() + -- log details at the end of validation + logmessage.display(0, 'Validation (epoch ' .. opt.epoch .. '): loss = ' .. avg_loss .. ', accuracy = ' .. validation_confusion.totalValid) +end + +-- if required, save snapshot at the end +if opt.epoch > last_snapshot_save_epoch then + local filename = paths.concat(opt.save, snapshot_prefix .. '_' .. opt.epoch .. '_Weights.t7') + logmessage.display(0,'Snapshotting to ' .. filename) + torch.save(filename, Weights) + logmessage.display(0,'Snapshot saved - ' .. filename) +end + +train:close() +if opt.validation ~= '' then + val:close() +end + +--print(confusion) diff --git a/tools/torch/test.lua b/tools/torch/test.lua new file mode 100644 index 000000000..bd7d72167 --- /dev/null +++ b/tools/torch/test.lua @@ -0,0 +1,265 @@ +-- Copyright (c) 2015, NVIDIA CORPORATION. All rights reserved. + +require 'torch' +require 'xlua' +require 'optim' +require 'pl' +require 'trepl' +require 'cutorch' +require 'image' +require 'cudnn' +require 'lfs' + +package.path = debug.getinfo(1,"S").source:match[[^@?(.*[\/])[^\/]-$]] .."?.lua;".. package.path + +require 'logmessage' +require 'Optimizer' +---------------------------------------------------------------------- + +--print 'processing options' + +opt = lapp[[ +-m,--resizeMode (default squash) Resize mode (squash/crop/fill/half_crop) for the input test image, if it's dimensions differs from those of Train DB images. +-t,--threads (default 8) number of threads +-p,--type (default cuda) float or cuda +-d,--devid (default 1) device ID (if using CUDA) +-o,--load (string) directory that contains trained model weights +-n,--network (string) Pretrained Model to be loaded +-e,--epoch (default -1) weight file of the epoch to be loaded +-i,--image (string) the value to this parameter depends on "testMany" parameter. If testMany is 'no' then this parameter specifies single image that needs to be classified or else this parameter specifies the location of file which contains paths of multiple images that needs to be classified. Provide full path, if the image (or) images file is in different directory. +-s,--mean (string) train images mean (saved as .jpg file) +-y,--ccn2 (default no) should be 'yes' if ccn2 is used in network. Default : false + +--testMany (default no) If this option is 'yes', then "image" input parameter should specify the file with all the images to be tested +--testUntil (default -1) specifies how many images in the "image" file to be tested. This parameter is only valid when testMany is set to "yes" +--crop (default no) If this option is 'yes', all the images are randomly cropped into square image. And croplength is provided as --croplen parameter +--croplen (default 0) crop length. This is required parameter when crop option is provided +--subtractMean (default yes) If yes, subtracts the mean from images +--labels (default labels.txt) file contains label definitions +--useMeanPixel (default 'no') by default pixel-wise subtraction is done using the full mean matrix. If this option is 'yes' then mean pixel will be used instead of mean matrix +--snapshotPrefix (default '') prefix of the weights/snapshots +--networkDirectory (default '') directory in which network exists +--pythonPrefix (default 'python') python version +--allPredictions (default no) If 'yes', displays all the predictions of an image instead of formatted topN results +]] + + +torch.setnumthreads(opt.threads) +cutorch.setDevice(opt.devid) + + +local snapshot_prefix = '' + +if opt.snapshotPrefix ~= '' then + snapshot_prefix = opt.snapshotPrefix +else + snapshot_prefix = opt.network +end + +local utils = require 'utils' + +local data = require 'data' + +local class_labels = data.loadLabels(opt.labels) + +local img_mean=data.loadMean(opt.mean, opt.useMeanPixel) + +local crop = opt.crop + +local req_x = nil +local req_y = nil + +-- if subtraction has to be done using the full mean matrix instead of mean pixel, then image cropping is possible only after subtracting the image from full mean matrix. In other cases, we can crop the image before subtracting mean. + +if (opt.useMeanPixel == 'yes' or opt.subtractMean == 'no') and crop == 'yes' then + req_x = opt.croplen + req_y = opt.croplen + crop = 'no' -- as resize_image.py will take care of cropping as well +else + req_x = img_mean["width"] + req_y = img_mean["height"] +end + +local cropX = nil +local cropY = nil +if crop == 'yes' then + cropX = math.floor((img_mean["height"] - opt.croplen)/2) + 1 + cropY = math.floor((img_mean["width"] - opt.croplen)/2) + 1 +end + + +-- If epoch for the trained model is not provided then select the latest trained model. +if opt.epoch == -1 then + dir_name = paths.concat(opt.load) + for file in lfs.dir(dir_name) do + file_name = paths.concat(dir_name,file) + if lfs.attributes(file_name,"mode") == "file" then + if string.match(file, snapshot_prefix .. '_.*_Weights[.]t7') then + parts=string.split(file,"_") + value = tonumber(parts[#parts-1]) + if (opt.epoch < value) then + opt.epoch = value + end + end + end + end +end + +if opt.epoch == -1 then + logmessage.display(2,'There are no pretrained model weights to test in this directory - ' .. paths.concat(opt.networkDirectory)) + return +end + +package.path = paths.concat(opt.networkDirectory, "?.lua") ..";".. package.path + +logmessage.display(0,'Loading network definition from ' .. paths.concat(opt.networkDirectory, opt.network)) +local model = require (opt.network) +local using_ccn2 = opt.ccn2 + +-- if ccn2 is used in network, then set using_ccn2 value as 'yes' +if ccn2 ~= nil then + using_ccn2 = 'yes' +end + +local weights, gradients = model:getParameters() + +logmessage.display(0, 'Loading ' .. paths.concat(opt.load, snapshot_prefix .. '_' .. opt.epoch .. '_Weights.t7') .. ' file') + +local weights_filename = paths.concat(opt.load, snapshot_prefix .. '_' .. opt.epoch .. '_Weights.t7') +weights:copy(torch.load(weights_filename)) + +if opt.type =='cuda' then + model:cuda() +end + +-- as we want to classify, let's disable dropouts by enabling evaluation mode +model:evaluate() + +local function preprocess(img_path) + + -- if image doesn't exists in path, check whether provided path is an URL, if URL download it else display error message and return. This function is useful only when the test code was run from commandline. + if not paths.filep(img_path) then + if (img_path:find("^http[.]") ~= nil) or (img_path:find("^https[.]") ~= nil) or (img_path:find("^www[.]") ~= nil) then + os.execute('wget '..img_path) + img_path = img_path:match( "([^/]+)$" ) + else + logmessage.display(2,'Image not found : ' .. img_path) + return nil + end + end + + local im = image.load(img_path) + + -- resize image to match with the required size. Required size may be mean file size or crop size input + if (img_mean["channels"] ~= im:size(1)) or (req_y ~= im:size(2)) or (req_x ~= im:size(3)) then + im = utils.resizeImage(im, req_y, req_x, img_mean["channels"],opt.resizeMode) + end + -- Torch image.load() always loads image with each pixel value between 0-1. As during training, images were taken from LMDB directly, their pixel values ranges from 0-255. As, model was trained with images whose pixel values are between 0-255, we may have to convert test image also to have 0-255 for each pixel. + im=im*255 + + -- Depending on the function arguments, image preprocess may include conversion from RGB to BGR and mean subtraction, image resize after mean subtraction + local image_preprocessed = data.PreProcess(im, img_mean["mean"], opt.subtractMean, img_mean["channels"], 'no', crop, false, cropX, cropY, opt.croplen) + return image_preprocessed +end + + +local inputs = nil +local batch_size = 0 +local predictions = nil +local topN = 5 -- displays top 5 predictions +if topN > #class_labels then + topN = #class_labels +end + +local val,classes = nil,nil +local counter = 0 +local index = 0 + +-- if ccn2 is used, then batch size of the input should be atleast 32 +if using_ccn2 == 'yes' or opt.testMany == 'yes' then + batch_size = 32 +else + batch_size = 1 +end + +if opt.crop == 'yes' then -- notice that here "opt.crop" is used, instead of "crop", as there are a chances that "crop" variable is getting overriden in the above instructions + inputs = torch.Tensor(batch_size, img_mean["channels"], opt.croplen, opt.croplen) +else + inputs = torch.Tensor(batch_size, img_mean["channels"], img_mean["height"], img_mean["width"]) +end + +-- predict batch and display the topN predictions for the images in batch +local function predictBatch(inputs) + if opt.type == 'float' then + predictions = model:forward(inputs:float()) + elseif opt.type =='cuda' then + predictions = model:forward(inputs:cuda()) + end + -- sort the outputs of SoftMax layer in decreasing order + for i=1,counter do + index = index + 1 + if opt.allPredictions == 'no' then + --display topN predictions of each image + val,classes = predictions[{i,{}}]:float():sort(true) + for j=1,topN do + -- output format : LABEL_ID (LABEL_NAME) CONFIDENCE + logmessage.display(0,'For image ' .. index ..', predicted class '..tostring(j)..': ' .. classes[j] .. ' (' .. class_labels[classes[j]] .. ') ' .. math.exp(val[j])) + end + else + val = predictions[{i,{}}]:float() + allPredictions = '' + for j=1,val:size(1) do + allPredictions = allPredictions .. ' ' .. math.exp(val[j]) + end + logmessage.display(0,'Predictions for image ' .. index ..': '..allPredictions) + end + end +end + +if opt.testMany == 'yes' then + local file = io.open(opt.image) + if file then + + for line in file:lines() do + counter = counter + 1 + local image_path = line:match( "^%s*(.-)%s*$" ) + inputs[counter] = preprocess(image_path) + + if counter == batch_size then + predictBatch(inputs) + counter = 0 + end + if (index+counter) == opt.testUntil then -- Here, index+counter represents total number of images read from file + break + end + + end + -- still some images needs to be predicted. + if counter > 0 then + -- if ccn2 is used, then batch size of the input should be atleast 32. So, append additional images at the end to make the same as batch size (which is 32) + if using_ccn2 == 'yes' then + for j=counter+1,batch_size do + inputs[j] = inputs[counter] + end + predictBatch(inputs) + + else + predictBatch(inputs:narrow(1,1,counter)) + end + end + else + logmessage.display(2,'Image file not found : ' .. opt.image) + end + +else + -- only one image needs to be predicted + inputs[1]=preprocess(opt.image) + if using_ccn2 == 'yes' then + for j=2,batch_size do + inputs[j] = inputs[1] -- replicate the first image in entire inputs tensor + end + end + counter = 1 -- here counter is set, so that predictBatch() method displays only the predictions of first image + predictBatch(inputs) +end + diff --git a/tools/torch/utils.lua b/tools/torch/utils.lua new file mode 100644 index 000000000..417ec9b9d --- /dev/null +++ b/tools/torch/utils.lua @@ -0,0 +1,228 @@ +-- Copyright (c) 2015, NVIDIA CORPORATION. All rights reserved. + +require 'torch' -- torch +require 'image' -- for color transforms + +package.path = debug.getinfo(1, "S").source:match[[^@?(.*[\/])[^\/]-$]] .."?.lua;".. package.path + +require 'logmessage' + +------------- UTILITY FUNCTIONS ---------------------------- + +local utilsClass={} + +-- round function +local function round(num, idp) + local mult = 10^(idp or 0) + return math.floor(num * mult + 0.5) / mult +end + +-- Currently zeroDataSize() and cleanupModel() routines aren't used but in future while implementing "recovery from crash" feature we may need to use these routines to clean the model before saving. This decreases the size of model by 80%. +function zeroDataSize(data) + if type(data) == 'table' then + for i = 1, #data do + data[i] = zeroDataSize(data[i]) + end + elseif type(data) == 'userdata' then + data = torch.Tensor():typeAs(data) + end + return data +end + +-- Resize the output, gradInput, etc temporary tensors to zero (so that the on disk size is smaller) +function cleanupModel(node) + if node.output ~= nil then + node.output = zeroDataSize(node.output) + end + if node.gradInput ~= nil then + node.gradInput = zeroDataSize(node.gradInput) + end + if node.finput ~= nil then + node.finput = zeroDataSize(node.finput) + end + -- Recurse on nodes with 'modules' + if (node.modules ~= nil) then + if (type(node.modules) == 'table') then + for i = 1, #node.modules do + local child = node.modules[i] + cleanupModel(child) + end + end + end + + -- Clear the references to the spatial convolution outputs as well + if _spatial_convolution_mm_out ~= nil then + _spatial_convolution_mm_out = {} + end + + if _spatial_convolution_mm_gradout ~= nil then + _spatial_convolution_mm_gradout = {} + end + + collectgarbage() +end + +utilsClass.round = round +utilsClass.cleanupModel = cleanupModel + +--[[ +Resizes an image and returns it as a np.array +Arguments: +image -- a PIL.Image or numpy.ndarray +height -- height of new image +width -- width of new image +Keyword Arguments: +channels -- channels of new image (stays unchanged if not specified) +resize_mode -- can be crop, squash, fill or half_crop +--]] +function utilsClass.resizeImage(img, height, width, channels,resize_mode) + if resize_mode == nil then + resize_mode = 'squash' + elseif resize_mode ~= 'crop' and resize_mode ~= 'squash' and resize_mode ~= 'fill' and resize_mode ~= 'half_crop' then + logmessage.display(0,'resize_mode ' .. resize_mode .. ' not supported') + end + + if channels ~=nil and channels ~=3 and channels ~=1 then + logmessage.display(0,'unsupported number of channels: ' .. channels) + end + + --#TODO handle transparent images + if img:size(1) == 2 then + + elseif img:size(1) == 4 then + + end + + if channels ~= nil and img:size(1) ~= channels then + if img:size(1) == 3 and channels == 1 then + img = image.rgb2y(img) + elseif img:size(1) == 1 and channels == 3 then + local dst = torch.Tensor(3,img:size(2),img:size(3)):type(img:type()) + for i=1,3 do + dst[{ i,{},{} }]:copy(img) + end + img = dst + end + end + -- No need to resize + if img:size(2) == height and img:size(3) == width then + return img + end + -- Resize + width_ratio = img:size(3) / width + height_ratio = img:size(2) / height + if resize_mode == 'squash' or width_ratio == height_ratio then + return image.scale(img,width,height) + elseif resize_mode == 'crop' then + -- resize to smallest of ratios (relatively larger image), keeping aspect ratio + if width_ratio > height_ratio then + resize_height = height + resize_width = round(img:size(3) / height_ratio) + else + resize_width = width + resize_height = round(img:size(2) / width_ratio) + end + img = image.scale(img,resize_width,resize_height) + + -- chop off ends of dimension that is still too long + if width_ratio > height_ratio then + start = round((resize_width-width)/2.0) + return image.crop(img, start, 0, start+width, img:size(2)) + else + start = round((resize_height-height)/2.0) + return image.crop(img, 0, start, img:size(3), start+height) + end + else + if resize_mode == 'fill' then + -- resize to biggest of ratios (relatively smaller image), keeping aspect ratio + if width_ratio > height_ratio then + resize_width = width + resize_height = round(img:size(2) / width_ratio) + if (height - resize_height) % 2 == 1 then + resize_height = resize_height + 1 + end + else + resize_height = height + resize_width = round(img:size(3) / height_ratio) + if (width - resize_width) % 2 == 1 then + resize_width = resize_width + 1 + end + end + img = image.scale(img,resize_width,resize_height) + elseif resize_mode == 'half_crop' then + -- resize to average ratio keeping aspect ratio + new_ratio = (width_ratio + height_ratio) / 2.0 + resize_width = round(img:size(3) / new_ratio) + resize_height = round(img:size(2) / new_ratio) + + if width_ratio > height_ratio and (height - resize_height) % 2 == 1 then + resize_height = resize_height + 1 + elseif width_ratio < height_ratio and (width - resize_width) % 2 == 1 then + resize_width = resize_width + 1 + end + + img = image.scale(img,resize_width,resize_height) + -- chop off ends of dimension that is still too long + if width_ratio > height_ratio then + start = round((resize_width-width)/2.0) + img = image.crop(img, start, 0, start+width, img:size(2)) + else + start = round((resize_height-height)/2.0) + img = image.crop(img, 0, start, img:size(3), start+height) + end + end + + --return img if it reaches the expected size + if img:size(2) == height and img:size(3) == width then + return img + end + + -- fill ends of dimension that is too short with random noise + if width_ratio > height_ratio then + padding = (height - resize_height)/2 + padding_tensor = torch.rand(img:size(1),padding,img:size(3)) + img = torch.cat(padding_tensor,img,2) + img = torch.cat(img,padding_tensor,2) + else + padding = (width - resize_width)/2 + padding_tensor = torch.rand(img:size(1),img:size(2),padding) + img = torch.cat(padding_tensor,img,3) + img = torch.cat(img,padding_tensor,3) + end + return img + + end +end + +-- This module corrects the final output dimension of the model to match with total number of classes. This follows Reverse Depth First Search approach i.e., modules of the model are checked from last to first, and internally for each module same procedure was followed to find the module with weights. If the module has weights, it indicates the input and output sizes. If the first dimension of weight is not same as total classes count, then corrects the dimension to match with classes count. +-- TODO: currently this module supports only Linear module and for all other modules it will just display a warning message. +function correctFinalOutputDim(node, outputSize) + if (node.modules ~= nil) then + if (type(node.modules) == 'table') then + for i=#node.modules,1,-1 do + local child = node.modules[i] + if correctFinalOutputDim(child, outputSize) then + return true + end + end + end + end + if node.weight then + if node.weight:size(1) ~= outputSize then + if torch.type(node) == "nn.Linear" then + local oldOutputSize = node.weight:size(1) + node:__init(node.weight:size(2),outputSize) + logmessage.display(0,'changed output size for ' .. torch.type(node) .. ', from ' .. oldOutputSize .. ' to ' .. outputSize) + else + logmessage.display(1,'output size for last ' .. torch.type(node) .. ' layer is ' .. node.weight:size(1) .. ', which is different from total number of classes i.e., ' .. outputSize) + end + --logmessage.display(0,model:__tostring__()) + end + return true + end + return false +end + +utilsClass.correctFinalOutputDim = correctFinalOutputDim +return utilsClass + From 935b935139a56c48df476294165e2695a9eb58aa Mon Sep 17 00:00:00 2001 From: Greg Heinrich Date: Fri, 4 Sep 2015 22:29:22 +0200 Subject: [PATCH 2/3] Torch integration Unsupported features: - multi-GPU training - generic inference - fine tuning Limitations: - standard networks can only operate with intended image size and number of channels (e.g. LeNet requires grayscale images) --- .travis.yml | 4 + README.md | 3 +- digits/config/current_config.py | 2 + digits/frameworks/__init__.py | 6 + digits/frameworks/caffe_framework.py | 1 + digits/frameworks/errors.py | 11 + digits/frameworks/torch_framework.py | 162 ++++ .../model/images/classification/test_views.py | 114 ++- digits/model/images/generic/test_views.py | 14 +- digits/model/tasks/__init__.py | 2 +- digits/model/tasks/caffe_train.py | 89 +-- digits/model/tasks/torch_train.py | 420 +++++++---- .../torch/ImageNet-Training/LICENSE | 22 - .../torch/ImageNet-Training/alexnet.lua | 99 ++- .../torch/ImageNet-Training/googlenet.lua | 243 +++--- digits/standard-networks/torch/lenet.lua | 36 +- digits/task.py | 6 + .../images/classification/classify_one.html | 2 +- .../models/images/classification/new.html | 52 +- digits/test_views.py | 10 + digits/utils/image.py | 83 +++ docs/InstallTorch.md | 109 +++ docs/InstallTorchLMDB.md | 43 ++ docs/images/torch-selection.png | Bin 0 -> 89156 bytes scripts/travis/install-torch.sh | 26 + tools/torch/data.lua | 487 ++++++------ tools/torch/main.lua | 693 ++++++++++-------- tools/torch/test.lua | 90 ++- tools/torch/utils.lua | 35 +- 29 files changed, 1854 insertions(+), 1010 deletions(-) create mode 100644 digits/frameworks/torch_framework.py delete mode 100644 digits/standard-networks/torch/ImageNet-Training/LICENSE create mode 100644 docs/InstallTorch.md create mode 100644 docs/InstallTorchLMDB.md create mode 100644 docs/images/torch-selection.png create mode 100755 scripts/travis/install-torch.sh diff --git a/.travis.yml b/.travis.yml index 34a6fa5e4..7d73ed260 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,6 +20,9 @@ install: - pushd . - ./scripts/travis/install-caffe.sh $(pwd)/deps/caffe + # Torch + - ./scripts/travis/install-torch.sh $(pwd)/deps/torch + # DIGITS - sudo apt-get install graphviz # conda (fast) @@ -29,6 +32,7 @@ install: - pip install -r requirements_test.txt before_script: - export CAFFE_HOME=$(pwd)/deps/caffe + - source $(pwd)/deps/torch/install/bin/torch-activate script: ./digits-test -v --with-coverage --cover-package=digits,tools,scripts after_success: diff --git a/README.md b/README.md index 7c6b1fad9..cf73a50dd 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,8 @@ For greater performance, you can also install cuDNN. At least one deep learning framework backend is required. -* Caffe (NVIDIA's fork) - [installation instructions](docs/InstallCaffe.md) +* [Mandatory] Caffe (NVIDIA's fork) - [installation instructions](docs/InstallCaffe.md) +* [Optional] Torch7 - [installation instructions](docs/InstallTorch.md) ## Install DIGITS diff --git a/digits/config/current_config.py b/digits/config/current_config.py index 8dee71611..0dab17a4f 100644 --- a/digits/config/current_config.py +++ b/digits/config/current_config.py @@ -7,6 +7,7 @@ from server_name import ServerNameOption from secret_key import SecretKeyOption from caffe_option import CaffeOption +from torch_option import TorchOption option_list = None @@ -24,6 +25,7 @@ def reset(): ServerNameOption(), SecretKeyOption(), CaffeOption(), + TorchOption(), ] reset() diff --git a/digits/frameworks/__init__.py b/digits/frameworks/__init__.py index bf907a401..0aae9a0e7 100644 --- a/digits/frameworks/__init__.py +++ b/digits/frameworks/__init__.py @@ -2,12 +2,16 @@ from framework import Framework from caffe_framework import CaffeFramework +from torch_framework import TorchFramework from digits.config import config_value # # create framework instances # +# torch is optional +torch = TorchFramework() if config_value('torch_root') else None + # caffe is mandatory caffe = CaffeFramework() @@ -21,6 +25,8 @@ def get_frameworks(): there may be more than one instance per framework class """ frameworks = [caffe] + if torch: + frameworks.append(torch) return frameworks def get_framework_by_id(framework_id): diff --git a/digits/frameworks/caffe_framework.py b/digits/frameworks/caffe_framework.py index 3a5b3697a..6edcf58b2 100644 --- a/digits/frameworks/caffe_framework.py +++ b/digits/frameworks/caffe_framework.py @@ -98,6 +98,7 @@ def get_network_from_previous(self, previous_network): ip_layers[-1].name = '%s_retrain' % ip_layers[-1].name return network + @override def get_network_visualization(self, desc): """ return visualization of network diff --git a/digits/frameworks/errors.py b/digits/frameworks/errors.py index ff0c6fd4f..edbd47d2f 100644 --- a/digits/frameworks/errors.py +++ b/digits/frameworks/errors.py @@ -17,6 +17,17 @@ def __init__(self, message): def __str__(self): return repr(self.message) +@subclass +class NetworkVisualizationError(Error): + """ + Errors that occur when validating a network + """ + def __init__(self, message): + self.message = message + + def __str__(self): + return repr(self.message) + @subclass class InferenceError(Error): """ diff --git a/digits/frameworks/torch_framework.py b/digits/frameworks/torch_framework.py new file mode 100644 index 000000000..cace99860 --- /dev/null +++ b/digits/frameworks/torch_framework.py @@ -0,0 +1,162 @@ +# Copyright (c) 2015, NVIDIA CORPORATION. All rights reserved. + +import os +import digits +import re +import subprocess +import time +import tempfile +import flask + +from framework import Framework +from digits import utils +from digits.config import config_value +from digits.model.tasks import TorchTrainTask +from digits.utils import subclass, override +from errors import Error, NetworkVisualizationError, BadNetworkError + +@subclass +class TorchFramework(Framework): + + """ + Defines required methods to interact with the Torch framework + """ + + # short descriptive name + NAME = 'Torch (experimental)' + + # identifier of framework class + CLASS = 'torch' + + # whether this framework can shuffle data during training + CAN_SHUFFLE_DATA = True + + def __init__(self): + super(TorchFramework, self).__init__() + # id must be unique + self.framework_id = self.CLASS + + @override + def create_train_task(self, **kwargs): + """ + create train task + """ + return TorchTrainTask(framework_id = self.framework_id, **kwargs) + + @override + def get_standard_network_desc(self, network): + """ + return description of standard network + """ + networks_dir = os.path.join(os.path.dirname(digits.__file__), 'standard-networks', self.CLASS) + + # Torch's GoogLeNet and AlexNet models are placed in sub folder + if (network == "alexnet" or network == "googlenet"): + networks_dir = os.path.join(networks_dir, 'ImageNet-Training') + + for filename in os.listdir(networks_dir): + path = os.path.join(networks_dir, filename) + if os.path.isfile(path): + match = None + match = re.match(r'%s.lua' % network, filename) + if match: + with open(path) as infile: + return infile.read() + # return None if not found + return None + + @override + def get_network_from_desc(self, network_desc): + """ + return network object from a string representation + """ + # return the same string + return network_desc + + @override + def get_network_from_previous(self, previous_network): + """ + return new instance of network from previous network + """ + # return the same string + return previous_network + + @override + def validate_network(self, data): + """ + validate a network + """ + return True + + @override + def get_network_visualization(self, desc): + """ + return visualization of network + """ + # save network description to temporary file + _, temp_network_path = tempfile.mkstemp(suffix='.lua') + with open(temp_network_path, "w") as outfile: + outfile.write(desc) + + try: # do this in a try..finally clause to make sure we delete the temp file + # build command line + if config_value('torch_root') == '': + torch_bin = 'th' + else: + torch_bin = os.path.join(config_value('torch_root'), 'bin', 'th') + + args = [torch_bin, + os.path.join(os.path.dirname(os.path.dirname(digits.__file__)),'tools','torch','main.lua'), + '--network=%s' % os.path.splitext(os.path.basename(temp_network_path))[0], + '--networkDirectory=%s' % os.path.dirname(temp_network_path), + '--visualizeModel=yes' + ] + + # execute command + p = subprocess.Popen(args, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + close_fds=True, + ) + + regex = re.compile('\x1b\[[0-9;]*m', re.UNICODE) #TODO: need to include regular expression for MAC color codes + + # the network description will be accumulated from the command output + # when collecting_net_definition==True + collecting_net_definition = False + desc = [] + unrecognized_output = [] + while p.poll() is None: + for line in utils.nonblocking_readlines(p.stdout): + if line is not None: + # Remove whitespace and color codes. color codes are appended to begining and end of line by torch binary i.e., 'th'. Check the below link for more information + # https://groups.google.com/forum/#!searchin/torch7/color$20codes/torch7/8O_0lSgSzuA/Ih6wYg9fgcwJ + line = regex.sub('', line) + timestamp, level, message = TorchTrainTask.preprocess_output_torch(line.strip()) + if message: + if message.startswith('Network definition'): + collecting_net_definition = not collecting_net_definition + else: + if collecting_net_definition: + desc.append(line) + elif len(line): + unrecognized_output.append(line) + else: + time.sleep(0.05) + + if not len(desc): + # we did not find a network description + raise NetworkVisualizationError(''.join(unrecognized_output)) + else: + output = flask.Markup('
')
+                for line in desc:
+                    output += flask.Markup.escape(line)
+                output += flask.Markup('
') + return output + finally: + os.remove(temp_network_path) + + + + + diff --git a/digits/model/images/classification/test_views.py b/digits/model/images/classification/test_views.py index 0682508b3..9c7d20387 100644 --- a/digits/model/images/classification/test_views.py +++ b/digits/model/images/classification/test_views.py @@ -64,6 +64,26 @@ class BaseViewsTest(digits.test_views.BaseViewsTest): } """ + TORCH_NETWORK = \ +""" +require 'nn' +local model = nn.Sequential() +model:add(nn.View(-1):setNumInputDims(3)) -- 10*10*3 -> 300 +model:add(nn.Linear(300, 3)) +model:add(nn.LogSoftMax()) +return function(params) + return { + model = model + } +end +""" + + @classmethod + def setUpClass(cls): + super(BaseViewsTest, cls).setUpClass() + if cls.FRAMEWORK=='torch' and not config_value('torch_root'): + raise unittest.SkipTest('Torch not found') + @classmethod def model_exists(cls, job_id): return cls.job_exists(job_id, 'models') @@ -89,7 +109,7 @@ def delete_model(cls, job_id): @classmethod def network(cls): - return cls.CAFFE_NETWORK + return cls.TORCH_NETWORK if cls.FRAMEWORK=='torch' else cls.CAFFE_NETWORK class BaseViewsTestWithDataset(BaseViewsTest, digits.dataset.images.classification.test_views.BaseViewsTestWithDataset): @@ -99,6 +119,10 @@ class BaseViewsTestWithDataset(BaseViewsTest, # Inherited classes may want to override these attributes CROP_SIZE = None + TRAIN_EPOCHS = 1 + SHUFFLE = False + LR_POLICY = None + LEARNING_RATE = None @classmethod def setUpClass(cls): @@ -113,7 +137,7 @@ def tearDownClass(cls): super(BaseViewsTestWithDataset, cls).tearDownClass() @classmethod - def create_model(cls, **kwargs): + def create_model(cls, network=None, **kwargs): """ Create a model Returns the job_id @@ -122,17 +146,24 @@ def create_model(cls, **kwargs): Keyword arguments: **kwargs -- data to be sent with POST request """ + if network is None: + network = cls.network() data = { 'model_name': 'test_model', 'dataset': cls.dataset_id, 'method': 'custom', - 'custom_network': cls.network(), + 'custom_network': network, 'batch_size': 10, - 'train_epochs': 1, - 'framework' : cls.FRAMEWORK + 'train_epochs': cls.TRAIN_EPOCHS, + 'framework' : cls.FRAMEWORK, + 'shuffle': 'true' if cls.SHUFFLE else 'false' } if cls.CROP_SIZE is not None: data['crop_size'] = cls.CROP_SIZE + if cls.LR_POLICY is not None: + data['lr_policy'] = cls.LR_POLICY + if cls.LEARNING_RATE is not None: + data['learning_rate'] = cls.LEARNING_RATE data.update(kwargs) request_json = data.pop('json', False) @@ -197,6 +228,11 @@ def test_visualize_network(self): image = s.select('img') assert image is not None, "didn't return an image" + def test_customize(self): + rv = self.app.post('/models/customize?network=lenet&framework='+self.FRAMEWORK) + s = BeautifulSoup(rv.data) + body = s.select('body') + assert rv.status_code == 200, 'POST failed with %s\n\n%s' % (rv.status_code, body) class BaseTestCreation(BaseViewsTestWithDataset): """ @@ -270,6 +306,8 @@ def test_select_gpus(self): gpu_list = config_value('gpu_list').split(',') for i in xrange(len(gpu_list)): for combination in itertools.combinations(gpu_list, i+1): + if self.FRAMEWORK=='torch' and len(combination)>1: + raise unittest.SkipTest('Torch not tested with multi-GPU') yield self.check_select_gpus, combination def check_select_gpus(self, gpu_list): @@ -316,6 +354,37 @@ def test_retrain_twice(self): job3_id = self.create_model(**options_3) assert self.model_wait_completion(job3_id) == 'Done', 'third job failed' + def test_bad_network_definition(self): + if self.FRAMEWORK == 'caffe': + bogus_net = """ + layer { + name: "hidden" + type: 'BogusCode' + bottom: "data" + top: "output" + } + layer { + name: "loss" + type: "SoftmaxWithLoss" + bottom: "output" + bottom: "label" + top: "loss" + } + """ + elif self.FRAMEWORK == 'torch': + bogus_net = """ + local model = BogusCode(0) + return function(params) + return { + model = model + } + end + """ + job_id = self.create_model(json=True, network=bogus_net) + assert self.model_wait_completion(job_id) == 'Error', 'job should have failed' + job_info = self.job_info_html(job_id=job_id, job_type='models') + assert 'BogusCode' in job_info + class BaseTestCreated(BaseViewsTestWithModel): """ @@ -633,6 +702,22 @@ class TestCaffeCreatedCropInForm(BaseTestCreatedCropInForm): class TestCaffeCreatedCropInNetwork(BaseTestCreatedCropInNetwork): FRAMEWORK = 'caffe' +class TestTorchViews(BaseTestViews): + FRAMEWORK = 'torch' + +class TestTorchCreation(BaseTestCreation): + FRAMEWORK = 'torch' + +class TestTorchCreated(BaseTestCreated): + FRAMEWORK = 'torch' + TRAIN_EPOCHS = 10 + +class TestTorchCreatedHdf5(TestTorchCreated): + BACKEND = 'hdf5' + +class TestTorchDatasetModelInteractions(BaseTestDatasetModelInteractions): + FRAMEWORK = 'torch' + class TestCaffeLeNet(TestCaffeCreated): IMAGE_WIDTH = 28 IMAGE_HEIGHT = 28 @@ -643,3 +728,22 @@ class TestCaffeLeNet(TestCaffeCreated): 'standard-networks', 'caffe', 'lenet.prototxt') ).read() +class TestTorchLeNet(TestTorchCreated): + IMAGE_WIDTH = 28 + IMAGE_HEIGHT = 28 + IMAGE_CHANNELS = 1 + TRAIN_EPOCHS = 20 + # need more aggressive learning rate + # on such a small dataset + LR_POLICY = 'fixed' + LEARNING_RATE = 0.1 + + TORCH_NETWORK=open( + os.path.join( + os.path.dirname(digits.__file__), + 'standard-networks', 'torch', 'lenet.lua') + ).read() + + +class TestTorchHdf5LeNet(TestTorchLeNet): + BACKEND = 'hdf5' diff --git a/digits/model/images/generic/test_views.py b/digits/model/images/generic/test_views.py index 4f960a06e..998ee51bb 100644 --- a/digits/model/images/generic/test_views.py +++ b/digits/model/images/generic/test_views.py @@ -64,6 +64,18 @@ class BaseViewsTest(digits.test_views.BaseViewsTest): bottom: "label" top: "loss" } +""" + + TORCH_NETWORK = \ +""" +require 'nn' +require 'cunn' +local model = nn.Sequential() +model:add(nn.View(-1):setNumInputDims(3)) -- 10*10*3 -> 300 +model:add(nn.Linear(300, 3)) +model:add(nn.LogSoftMax()) +model:cuda() +return model """ @classmethod @@ -91,7 +103,7 @@ def delete_model(cls, job_id): @classmethod def network(cls): - return cls.CAFFE_NETWORK + return cls.TORCH_NETWORK if cls.FRAMEWORK=='torch' else cls.CAFFE_NETWORK class BaseViewsTestWithDataset(BaseViewsTest, diff --git a/digits/model/tasks/__init__.py b/digits/model/tasks/__init__.py index c8f259e13..40290e453 100644 --- a/digits/model/tasks/__init__.py +++ b/digits/model/tasks/__init__.py @@ -2,4 +2,4 @@ from train import TrainTask from caffe_train import CaffeTrainTask - +from torch_train import TorchTrainTask diff --git a/digits/model/tasks/caffe_train.py b/digits/model/tasks/caffe_train.py index a2f9cb167..9f69824a6 100644 --- a/digits/model/tasks/caffe_train.py +++ b/digits/model/tasks/caffe_train.py @@ -1020,7 +1020,7 @@ def get_layer_visualizations(self, net, layers='all'): for bottom in layer.bottom: if bottom in net.blobs and bottom not in added_activations: data = net.blobs[bottom].data[0] - vis = self.get_layer_vis_square(data, + vis = utils.image.get_layer_vis_square(data, allow_heatmap=bool(bottom != 'data')) mean, std, hist = self.get_layer_statistics(data) visualizations.append( @@ -1040,7 +1040,7 @@ def get_layer_visualizations(self, net, layers='all'): if layer.name in net.params: data = net.params[layer.name][0].data if layer.type not in ['InnerProduct']: - vis = self.get_layer_vis_square(data) + vis = utils.image.get_layer_vis_square(data) else: vis = None mean, std, hist = self.get_layer_statistics(data) @@ -1073,7 +1073,7 @@ def get_layer_visualizations(self, net, layers='all'): # don't normalize softmax layers if layer.type == 'Softmax': normalize = False - vis = self.get_layer_vis_square(data, + vis = utils.image.get_layer_vis_square(data, normalize = normalize, allow_heatmap = bool(top != 'data')) mean, std, hist = self.get_layer_statistics(data) @@ -1096,89 +1096,6 @@ def get_layer_visualizations(self, net, layers='all'): return visualizations - def get_layer_vis_square(self, data, - allow_heatmap = True, - normalize = True, - min_img_dim = 100, - max_width = 1200, - ): - """ - Returns a vis_square for the given layer data - - Arguments: - data -- a np.ndarray - - Keyword arguments: - allow_heatmap -- if True, convert single channel images to heatmaps - normalize -- whether to normalize the data when visualizing - max_width -- maximum width for the vis_square - """ - if data.ndim == 1: - # interpret as 1x1 grayscale images - # (N, 1, 1) - data = data[:, np.newaxis, np.newaxis] - elif data.ndim == 2: - # interpret as 1x1 grayscale images - # (N, 1, 1) - data = data.reshape((data.shape[0]*data.shape[1], 1, 1)) - elif data.ndim == 3: - if data.shape[0] == 3: - # interpret as a color image - # (1, H, W,3) - data = data[[2,1,0],...] # BGR to RGB (see issue #59) - data = data.transpose(1,2,0) - data = data[np.newaxis,...] - else: - # interpret as grayscale images - # (N, H, W) - pass - elif data.ndim == 4: - if data.shape[0] == 3: - # interpret as HxW color images - # (N, H, W, 3) - data = data.transpose(1,2,3,0) - data = data[:,:,:,[2,1,0]] # BGR to RGB (see issue #59) - elif data.shape[1] == 3: - # interpret as HxW color images - # (N, H, W, 3) - data = data.transpose(0,2,3,1) - data = data[:,:,:,[2,1,0]] # BGR to RGB (see issue #59) - else: - # interpret as HxW grayscale images - # (N, H, W) - data = data.reshape((data.shape[0]*data.shape[1], data.shape[2], data.shape[3])) - else: - raise RuntimeError('unrecognized data shape: %s' % (data.shape,)) - - # chop off data so that it will fit within max_width - padsize = 0 - width = data.shape[2] - if width > max_width: - data = data[:1,:max_width,:max_width] - else: - if width > 1: - padsize = 1 - width += 1 - n = max(max_width/width,1) - n *= n - data = data[:n] - - if not allow_heatmap and data.ndim == 3: - data = data[...,np.newaxis] - - vis = utils.image.vis_square(data, - padsize = padsize, - normalize = normalize, - ) - - # find minimum dimension and upscale if necessary - _min = sorted(vis.shape[:2])[0] - if _min < min_img_dim: - # upscale image - ratio = min_img_dim/float(_min) - vis = utils.image.upscale(vis, ratio) - return vis - def get_layer_statistics(self, data): """ Returns statistics for the given layer data: diff --git a/digits/model/tasks/torch_train.py b/digits/model/tasks/torch_train.py index 1b5ce7a63..5c0ff987e 100644 --- a/digits/model/tasks/torch_train.py +++ b/digits/model/tasks/torch_train.py @@ -7,9 +7,13 @@ import math import subprocess import sys +import operator +import shutil import numpy as np +import h5py + import tempfile import PIL.Image import digits @@ -19,10 +23,16 @@ from digits import utils, dataset from digits.utils import subclass, override, constants, errors from digits.dataset import ImageClassificationDatasetJob +#from digits.frameworks.errors import InferenceError # NOTE: Increment this everytime the pickled object changes PICKLE_VERSION = 1 +# Constants +TORCH_MODEL_FILE = 'model.lua' +TORCH_SNAPSHOT_PREFIX = 'snapshot' +TORCH_USE_MEAN_PIXEL = True + @subclass class TorchTrainTask(TrainTask): """ @@ -31,15 +41,18 @@ class TorchTrainTask(TrainTask): TORCH_LOG = 'torch_output.log' - def __init__(self, shuffle, **kwargs): + def __init__(self, **kwargs): """ Arguments: network -- a NetParameter defining the network """ super(TorchTrainTask, self).__init__(**kwargs) - self.pickver_task_torch_train = PICKLE_VERSION - self.shuffle = shuffle + # save network description to file + with open(os.path.join(self.job_dir, TORCH_MODEL_FILE), "w") as outfile: + outfile.write(self.network) + + self.pickver_task_torch_train = PICKLE_VERSION self.current_epoch = 0 @@ -49,11 +62,12 @@ def __init__(self, shuffle, **kwargs): self.classifier = None self.solver = None - self.model_file = constants.TORCH_MODEL_FILE + self.model_file = TORCH_MODEL_FILE self.train_file = constants.TRAIN_DB self.val_file = constants.VAL_DB - self.snapshot_prefix = constants.TORCH_SNAPSHOT_PREFIX + self.snapshot_prefix = TORCH_SNAPSHOT_PREFIX self.log_file = self.TORCH_LOG + self.trained_on_cpu = None def __getstate__(self): state = super(TorchTrainTask, self).__getstate__() @@ -87,12 +101,10 @@ def __setstate__(self, state): def name(self): return 'Train Torch Model' - @override - def framework_name(self): - return 'torch' - @override def before_run(self): + super(TorchTrainTask, self).before_run() + if not isinstance(self.dataset, dataset.ImageClassificationDatasetJob): raise NotImplementedError() @@ -113,7 +125,10 @@ def task_arguments(self, resources): torch_bin = os.path.join(config_value('torch_root'), 'bin', 'th') if self.batch_size is None: - self.batch_size = constants.DEFAULT_TORCH_BATCH_SIZE + self.batch_size = constants.DEFAULT_BATCH_SIZE + + dataset_backend = self.dataset.train_db_task().backend + assert dataset_backend=='lmdb' or dataset_backend=='hdf5' args = [torch_bin, os.path.join(os.path.dirname(os.path.dirname(digits.__file__)),'tools','torch','main.lua'), @@ -129,7 +144,8 @@ def task_arguments(self, resources): '--labels=%s' % self.dataset.path(self.dataset.labels_file), '--batchSize=%d' % self.batch_size, '--learningRate=%s' % self.learning_rate, - '--policy=%s' % str(self.lr_policy['policy']) + '--policy=%s' % str(self.lr_policy['policy']), + '--dbbackend=%s' % dataset_backend ] #learning rate policy input parameters @@ -187,7 +203,11 @@ def task_arguments(self, resources): if len(identifiers) == 1: args.append('--devid=%s' % (identifiers[0]+1,)) elif len(identifiers) > 1: - raise NotImplementedError("haven't tested torch with multiple GPUs yet") + raise NotImplementedError("Multi-GPU with Torch not supported yet") + else: + # switch to CPU mode + args.append('--type=float') + self.trained_on_cpu = True if self.pretrained_model: args.append('--weights=%s' % self.path(self.pretrained_model)) @@ -294,7 +314,8 @@ def process_output(self, line): # skip remaining info and warn messages return True - def preprocess_output_torch(self, line): + @staticmethod + def preprocess_output_torch(line): """ Takes line of output and parses it according to caffe's output format Returns (timestamp, level, message) or (None, None, None) @@ -359,7 +380,11 @@ def after_runtime_error(self): if message: lines.append(message) # return the last 20 lines - self.traceback = '\n'.join(lines[len(lines)-20:]) + traceback = '\n\nLast output:\n' + '\n'.join(lines[len(lines)-20:]) if len(lines)>0 else '' + if self.traceback: + self.traceback = self.traceback + traceback + else: + self.traceback = traceback @override def detect_snapshots(self): @@ -434,7 +459,7 @@ def classify_one(self, image, snapshot_epoch=None, layers=None): except KeyError: error_message = 'Unable to save file to "%s"' % temp_image_path self.logger.error(error_message) - raise errors.TestError(error_message) + raise digits.frameworks.errors.InferenceError(error_message) if config_value('torch_root') == '': torch_bin = 'th' @@ -445,36 +470,41 @@ def classify_one(self, image, snapshot_epoch=None, layers=None): os.path.join(os.path.dirname(os.path.dirname(digits.__file__)),'tools','torch','test.lua'), '--image=%s' % temp_image_path, '--network=%s' % self.model_file.split(".")[0], - '--epoch=%d' % int(snapshot_epoch), '--networkDirectory=%s' % self.job_dir, '--load=%s' % self.job_dir, '--snapshotPrefix=%s' % self.snapshot_prefix, '--mean=%s' % self.dataset.path(constants.MEAN_FILE_IMAGE), '--labels=%s' % self.dataset.path(self.dataset.labels_file) ] - - if constants.TORCH_USE_MEAN_PIXEL: + if snapshot_epoch: + args.append('--epoch=%d' % int(snapshot_epoch)) + if TORCH_USE_MEAN_PIXEL: args.append('--useMeanPixel=yes') + if self.trained_on_cpu: + args.append('--type=float') - if self.crop_size: - args.append('--crop=yes') - args.append('--croplen=%d' % self.crop_size) + # input image has been resized to network input dimensions by caller + args.append('--crop=no') if self.use_mean: args.append('--subtractMean=yes') else: args.append('--subtractMean=no') + if layers=='all': + args.append('--visualization=yes') + args.append('--save=%s' % self.job_dir) + # Convert them all to strings args = [str(x) for x in args] - #print args - regex = re.compile('\x1b\[[0-9;]*m', re.UNICODE) #TODO: need to include regular expression for MAC color codes - self.logger.info('%s classify one task started.' % self.framework_name()) + self.logger.info('%s classify one task started.' % self.get_framework_id()) unrecognized_output = [] - predictions = [] + predictions = [] + self.visualization_file = None + p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, @@ -487,14 +517,14 @@ def classify_one(self, image, snapshot_epoch=None, layers=None): for line in utils.nonblocking_readlines(p.stdout): if self.aborted.is_set(): p.terminate() - raise errors.TestError('%s classify one task got aborted. error code - %d' % (self.framework_name(), p.returncode())) + raise digits.frameworks.errors.InferenceError('%s classify one task got aborted. error code - %d' % (self.get_framework_id(), p.returncode())) if line is not None: # Remove color codes and whitespace line=regex.sub('', line).strip() if line: if not self.process_test_output(line, predictions, 'one'): - self.logger.warning('%s classify one task unrecognized input: %s' % (self.framework_name(), line.strip())) + self.logger.warning('%s classify one task unrecognized input: %s' % (self.get_framework_id(), line.strip())) unrecognized_output.append(line) else: time.sleep(0.05) @@ -503,31 +533,121 @@ def classify_one(self, image, snapshot_epoch=None, layers=None): if p.poll() is None: p.terminate() error_message = '' - if type(e) == errors.TestError: + if type(e) == digits.frameworks.errors.InferenceError: error_message = e.__str__() else: - error_message = '%s classify one task failed with error code %d \n %s' % (self.framework_name(), p.returncode(), str(e)) + error_message = '%s classify one task failed with error code %d \n %s' % (self.get_framework_id(), p.returncode(), str(e)) self.logger.error(error_message) if unrecognized_output: unrecognized_output = '\n'.join(unrecognized_output) error_message = error_message + unrecognized_output - raise errors.TestError(error_message) + raise digits.frameworks.errors.InferenceError(error_message) finally: self.after_test_run(temp_image_path) if p.returncode != 0: - error_message = '%s classify one task failed with error code %d' % (self.framework_name(), p.returncode) + error_message = '%s classify one task failed with error code %d' % (self.get_framework_id(), p.returncode) self.logger.error(error_message) if unrecognized_output: unrecognized_output = '\n'.join(unrecognized_output) error_message = error_message + unrecognized_output - raise errors.TestError(error_message) + raise digits.frameworks.errors.InferenceError(error_message) else: - self.logger.info('%s classify one task completed.' % self.framework_name()) + self.logger.info('%s classify one task completed.' % self.get_framework_id()) + + + visualizations = [] + + if layers=='all' and self.visualization_file: + vis_db = h5py.File(self.visualization_file, 'r') + # the HDF5 database is organized as follows: + # + # |- layers + # |- 1 + # | |- name + # | |- activations + # | |- weights + # |- 2 + for layer_id,layer in vis_db['layers'].items(): + layer_desc = layer['name'][...].tostring() + if 'Sequential' in layer_desc or 'Parallel' in layer_desc: + # ignore containers + continue + idx = int(layer_id) + # activations + data = np.array(layer['activations'][...]) + # skip batch dimension + if len(data.shape)>1 and data.shape[0]==1: + data = data[0] + vis = utils.image.get_layer_vis_square(data) + mean, std, hist = self.get_layer_statistics(data) + visualizations.append( + { + 'id': idx, + 'name': layer_desc, + 'vis_type': 'Activations', + 'image_html': utils.image.embed_image_html(vis), + 'data_stats': { + 'shape': data.shape, + 'mean': mean, + 'stddev': std, + 'histogram': hist, + } + } + ) + # weights + if 'weights' in layer: + data = np.array(layer['weights'][...]) + if 'Linear' not in layer_desc: + vis = utils.image.get_layer_vis_square(data) + else: + # Linear (inner product) layers have too many weights + # to display + vis = None + mean, std, hist = self.get_layer_statistics(data) + parameter_count = reduce(operator.mul, data.shape, 1) + if 'bias' in layer: + bias = np.array(layer['bias'][...]) + parameter_count += reduce(operator.mul, bias.shape, 1) + visualizations.append( + { + 'id': idx, + 'name': layer_desc, + 'vis_type': 'Weights', + 'image_html': utils.image.embed_image_html(vis), + 'param_count': parameter_count, + 'data_stats': { + 'shape': data.shape, + 'mean': mean, + 'stddev': std, + 'histogram': hist, + } + } + ) + # sort by layer ID + visualizations = sorted(visualizations,key=lambda x:x['id']) + return (predictions,visualizations) + + def get_layer_statistics(self, data): + """ + Returns statistics for the given layer data: + (mean, standard deviation, histogram) + histogram -- [y, x, ticks] + + Arguments: + data -- a np.ndarray + """ + # XXX These calculations can be super slow + mean = np.mean(data) + std = np.std(data) + y, x = np.histogram(data, bins=20) + y = list(y) + ticks = x[[0,len(x)/2,-1]] + x = [(x[i]+x[i+1])/2.0 for i in xrange(len(x)-1)] + ticks = list(ticks) + return (mean, std, [y, x, ticks]) - #TODO: implement visualization - return (predictions,None) def after_test_run(self, temp_image_path): try: @@ -567,16 +687,22 @@ def process_test_output(self, line, predictions, test_category): predictions.append(map(float, values)) return True + # path to visualization file + match = re.match(r'Saving visualization to (.*)', message) + if match: + self.visualization_file = match.group(1).strip() + return True + # displaying info and warn messages as we aren't maintaining seperate log file for model testing if level == 'info': - self.logger.debug('%s classify %s task : %s' % (self.framework_name(), test_category, message)) + self.logger.debug('%s classify %s task : %s' % (self.get_framework_id(), test_category, message)) return True if level == 'warning': - self.logger.warning('%s classify %s task : %s' % (self.framework_name(), test_category, message)) + self.logger.warning('%s classify %s task : %s' % (self.get_framework_id(), test_category, message)) return True if level in ['error', 'critical']: - raise errors.TestError('%s classify %s task failed with error message - %s' % (self.framework_name(), test_category, message)) + raise digits.frameworks.errors.InferenceError('%s classify %s task failed with error message - %s' % (self.get_framework_id(), test_category, message)) return True # control never reach this line. It can be removed. @@ -592,7 +718,7 @@ def infer_many(self, data, snapshot_epoch=None): return self.classify_many(data, snapshot_epoch=snapshot_epoch) raise NotImplementedError() - def classify_many(self, image_file, snapshot_epoch=None): + def classify_many(self, images, snapshot_epoch=None): """ Returns (labels, results): labels -- an array of strings @@ -609,99 +735,122 @@ def classify_many(self, image_file, snapshot_epoch=None): Keyword arguments: snapshot_epoch -- which snapshot to use """ - labels = self.get_labels() #TODO: probably we no need to return this, as we can directly access from the calling function - - if config_value('torch_root') == '': - torch_bin = 'th' - else: - torch_bin = os.path.join(config_value('torch_root'), 'bin', 'th') - - args = [torch_bin, - os.path.join(os.path.dirname(os.path.dirname(digits.__file__)),'tools','torch','test.lua'), - '--testMany=yes', - '--allPredictions=yes', #all predictions are grabbed and formatted as required by DIGITS - '--image=%s' % str(image_file), - '--resizeMode=%s' % str(self.dataset.resize_mode), # Here, we are using original images, so they will be resized in Torch code. This logic needs to be changed to eliminate the rework of resizing. Need to find a way to send python images array to Lua script efficiently - '--network=%s' % self.model_file.split(".")[0], - '--epoch=%d' % int(snapshot_epoch), - '--networkDirectory=%s' % self.job_dir, - '--load=%s' % self.job_dir, - '--snapshotPrefix=%s' % self.snapshot_prefix, - '--mean=%s' % self.dataset.path(constants.MEAN_FILE_IMAGE), - '--pythonPrefix=%s' % sys.executable, - '--labels=%s' % self.dataset.path(self.dataset.labels_file) - ] - if constants.TORCH_USE_MEAN_PIXEL: - args.append('--useMeanPixel=yes') - - if self.crop_size: - args.append('--crop=yes') - args.append('--croplen=%d' % self.crop_size) - - if self.use_mean: - args.append('--subtractMean=yes') - else: - args.append('--subtractMean=no') - #print args + # create a temporary folder to store images and a temporary file + # to store a list of paths to the images + temp_dir_path = tempfile.mkdtemp() + try: # this try...finally clause is used to clean up the temp directory in any case + _, temp_imgfile_path = tempfile.mkstemp(dir=temp_dir_path, suffix='.txt') + temp_imgfile = open(temp_imgfile_path, "w") + for image in images: + _, temp_image_path = tempfile.mkstemp(dir=temp_dir_path, suffix='.jpeg') + image = PIL.Image.fromarray(image) + try: + image.save(temp_image_path, format='jpeg') + except KeyError: + error_message = 'Unable to save file to "%s"' % temp_image_path + self.logger.error(error_message) + raise digits.frameworks.errors.InferenceError(error_message) + temp_imgfile.write("%s\n" % temp_image_path) + temp_imgfile.close() + + labels = self.get_labels() #TODO: probably we no need to return this, as we can directly access from the calling function + + if config_value('torch_root') == '': + torch_bin = 'th' + else: + torch_bin = os.path.join(config_value('torch_root'), 'bin', 'th') + + args = [torch_bin, + os.path.join(os.path.dirname(os.path.dirname(digits.__file__)),'tools','torch','test.lua'), + '--testMany=yes', + '--allPredictions=yes', #all predictions are grabbed and formatted as required by DIGITS + '--image=%s' % str(temp_imgfile_path), + '--resizeMode=%s' % str(self.dataset.resize_mode), # Here, we are using original images, so they will be resized in Torch code. This logic needs to be changed to eliminate the rework of resizing. Need to find a way to send python images array to Lua script efficiently + '--network=%s' % self.model_file.split(".")[0], + '--networkDirectory=%s' % self.job_dir, + '--load=%s' % self.job_dir, + '--snapshotPrefix=%s' % self.snapshot_prefix, + '--mean=%s' % self.dataset.path(constants.MEAN_FILE_IMAGE), + '--pythonPrefix=%s' % sys.executable, + '--labels=%s' % self.dataset.path(self.dataset.labels_file) + ] + if snapshot_epoch: + args.append('--epoch=%d' % int(snapshot_epoch)) + if TORCH_USE_MEAN_PIXEL: + args.append('--useMeanPixel=yes') + if self.trained_on_cpu: + args.append('--type=float') + + # input images have been resized to network input dimensions by caller + args.append('--crop=no') + + if self.use_mean: + args.append('--subtractMean=yes') + else: + args.append('--subtractMean=no') - # Convert them all to strings - args = [str(x) for x in args] + #print args - regex = re.compile('\x1b\[[0-9;]*m', re.UNICODE) #TODO: need to include regular expression for MAC color codes - self.logger.info('%s classify many task started.' % self.name()) + # Convert them all to strings + args = [str(x) for x in args] - unrecognized_output = [] - predictions = [] - p = subprocess.Popen(args, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - cwd=self.job_dir, - close_fds=True, - ) + regex = re.compile('\x1b\[[0-9;]*m', re.UNICODE) #TODO: need to include regular expression for MAC color codes + self.logger.info('%s classify many task started.' % self.name()) - try: - while p.poll() is None: - for line in utils.nonblocking_readlines(p.stdout): - if self.aborted.is_set(): - p.terminate() - raise errors.TestError('%s classify many task got aborted. error code - %d' % (self.framework_name(), p.returncode())) + unrecognized_output = [] + predictions = [] + p = subprocess.Popen(args, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + cwd=self.job_dir, + close_fds=True, + ) - if line is not None: - # Remove whitespace and color codes. color codes are appended to begining and end of line by torch binary i.e., 'th'. Check the below link for more information - # https://groups.google.com/forum/#!searchin/torch7/color$20codes/torch7/8O_0lSgSzuA/Ih6wYg9fgcwJ - line=regex.sub('', line).strip() - if line: - if not self.process_test_output(line, predictions, 'many'): - self.logger.warning('%s classify many task unrecognized input: %s' % (self.framework_name(), line.strip())) - unrecognized_output.append(line) - else: - time.sleep(0.05) - except Exception as e: - if p.poll() is None: - p.terminate() - error_message = '' - if type(e) == errors.TestError: - error_message = e.__str__() + try: + while p.poll() is None: + for line in utils.nonblocking_readlines(p.stdout): + if self.aborted.is_set(): + p.terminate() + raise digits.frameworks.errors.InferenceError('%s classify many task got aborted. error code - %d' % (self.get_framework_id(), p.returncode())) + + if line is not None: + # Remove whitespace and color codes. color codes are appended to begining and end of line by torch binary i.e., 'th'. Check the below link for more information + # https://groups.google.com/forum/#!searchin/torch7/color$20codes/torch7/8O_0lSgSzuA/Ih6wYg9fgcwJ + line=regex.sub('', line).strip() + if line: + if not self.process_test_output(line, predictions, 'many'): + self.logger.warning('%s classify many task unrecognized input: %s' % (self.get_framework_id(), line.strip())) + unrecognized_output.append(line) + else: + time.sleep(0.05) + except Exception as e: + if p.poll() is None: + p.terminate() + error_message = '' + if type(e) == digits.frameworks.errors.InferenceError: + error_message = e.__str__() + else: + error_message = '%s classify many task failed with error code %d \n %s' % (self.get_framework_id(), p.returncode(), str(e)) + self.logger.error(error_message) + if unrecognized_output: + unrecognized_output = '\n'.join(unrecognized_output) + error_message = error_message + unrecognized_output + raise digits.frameworks.errors.InferenceError(error_message) + + if p.returncode != 0: + error_message = '%s classify many task failed with error code %d' % (self.get_framework_id(), p.returncode) + self.logger.error(error_message) + if unrecognized_output: + unrecognized_output = '\n'.join(unrecognized_output) + error_message = error_message + unrecognized_output + raise digits.frameworks.errors.InferenceError(error_message) else: - error_message = '%s classify many task failed with error code %d \n %s' % (self.framework_name(), p.returncode(), str(e)) - self.logger.error(error_message) - if unrecognized_output: - unrecognized_output = '\n'.join(unrecognized_output) - error_message = error_message + unrecognized_output - raise errors.TestError(error_message) - - if p.returncode != 0: - error_message = '%s classify many task failed with error code %d' % (self.framework_name(), p.returncode) - self.logger.error(error_message) - if unrecognized_output: - unrecognized_output = '\n'.join(unrecognized_output) - error_message = error_message + unrecognized_output - raise errors.TestError(error_message) - else: - self.logger.info('%s classify many task completed.' % self.framework_name()) + self.logger.info('%s classify many task completed.' % self.get_framework_id()) + finally: + shutil.rmtree(temp_dir_path) - return (labels,np.array(predictions)) + return (labels,np.array(predictions)) def has_model(self): """ @@ -709,19 +858,22 @@ def has_model(self): """ return len(self.snapshots) != 0 - def loaded_model(self): + @override + def get_model_files(self): """ - Returns True if a model has been loaded + return paths to model files """ - return None + return { + "Network": self.model_file + } - def load_model(self, epoch=None): + @override + def get_network_desc(self): """ - Loads a .caffemodel - Returns True if the model is loaded (or if it was already loaded) - - Keyword Arguments: - epoch -- which snapshot to load (default is -1 to load the most recently generated snapshot) + return text description of network """ - return False + with open (os.path.join(self.job_dir,TORCH_MODEL_FILE), "r") as infile: + desc = infile.read() + return desc + diff --git a/digits/standard-networks/torch/ImageNet-Training/LICENSE b/digits/standard-networks/torch/ImageNet-Training/LICENSE deleted file mode 100644 index b0e4edd65..000000000 --- a/digits/standard-networks/torch/ImageNet-Training/LICENSE +++ /dev/null @@ -1,22 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2015 Elad Hoffer - -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. - diff --git a/digits/standard-networks/torch/ImageNet-Training/alexnet.lua b/digits/standard-networks/torch/ImageNet-Training/alexnet.lua index f62610d7d..13035ba2c 100644 --- a/digits/standard-networks/torch/ImageNet-Training/alexnet.lua +++ b/digits/standard-networks/torch/ImageNet-Training/alexnet.lua @@ -1,42 +1,81 @@ --- Copyright (c) 2015 Elad Hoffer - -require 'cudnn' -require 'cunn' -require 'ccn2' - local SpatialConvolution = nn.SpatialConvolutionMM--lib[1] - local SpatialMaxPooling = cudnn.SpatialMaxPooling--lib[2] - local ReLU = nn.ReLU--lib[3] - - -- from https://code.google.com/p/cuda-convnet2/source/browse/layers/layers-imagenet-1gpu.cfg - -- this is AlexNet that was presented in the One Weird Trick paper. http://arxiv.org/abs/1404.5997 - local features = nn.Sequential() - features:add(SpatialConvolution(3,64,11,11,4,4,2,2)) -- 224 -> 55 - features:add(ReLU()) - features:add(SpatialMaxPooling(3,3,2,2)) -- 55 -> 27 - features:add(SpatialConvolution(64,192,5,5,1,1,2,2)) -- 27 -> 27 - features:add(ReLU()) - features:add(SpatialMaxPooling(3,3,2,2)) -- 27 -> 13 - features:add(SpatialConvolution(192,384,3,3,1,1,1,1)) -- 13 -> 13 - features:add(ReLU()) - features:add(SpatialConvolution(384,256,3,3,1,1,1,1)) -- 13 -> 13 - features:add(ReLU()) - features:add(SpatialConvolution(256,256,3,3,1,1,1,1)) -- 13 -> 13 - features:add(ReLU()) - features:add(SpatialMaxPooling(3,3,2,2)) -- 13 -> 6 +-- source: https://github.com/soumith/imagenet-multiGPU.torch/blob/master/models/alexnet_cudnn.lua +require 'nn' +if pcall(function() require('cudnn') end) then + print('Using CuDNN backend') + backend = cudnn + convLayer = cudnn.SpatialConvolution + convLayerName = 'cudnn.SpatialConvolution' +else + print('Failed to load cudnn backend (is libcudnn.so in your library path?)') + if pcall(function() require('cunn') end) then + print('Falling back to legacy cunn backend') + else + print('Failed to load cunn backend (is CUDA installed?)') + print('Falling back to legacy nn backend') + end + backend = nn -- works with cunn or nn + convLayer = nn.SpatialConvolutionMM + convLayerName = 'nn.SpatialConvolutionMM' +end + +function createModel(nGPU) + assert(nGPU == 1 or nGPU == 2, '1-GPU or 2-GPU supported for AlexNet') + local features + if nGPU == 1 then + features = nn.Concat(2) + else + features = nn.ModelParallel(2) + end + + local fb1 = nn.Sequential() -- branch 1 + fb1:add(convLayer(3,48,11,11,4,4,2,2)) -- 224 -> 55 + fb1:add(backend.ReLU(true)) + fb1:add(backend.SpatialMaxPooling(3,3,2,2)) -- 55 -> 27 + fb1:add(convLayer(48,128,5,5,1,1,2,2)) -- 27 -> 27 + fb1:add(backend.ReLU(true)) + fb1:add(backend.SpatialMaxPooling(3,3,2,2)) -- 27 -> 13 + fb1:add(convLayer(128,192,3,3,1,1,1,1)) -- 13 -> 13 + fb1:add(backend.ReLU(true)) + fb1:add(convLayer(192,192,3,3,1,1,1,1)) -- 13 -> 13 + fb1:add(backend.ReLU(true)) + fb1:add(convLayer(192,128,3,3,1,1,1,1)) -- 13 -> 13 + fb1:add(backend.ReLU(true)) + fb1:add(backend.SpatialMaxPooling(3,3,2,2)) -- 13 -> 6 + + local fb2 = fb1:clone() -- branch 2 + for k,v in ipairs(fb2:findModules(convLayerName)) do + v:reset() -- reset branch 2's weights + end + + features:add(fb1) + features:add(fb2) + + -- 1.3. Create Classifier (fully connected layers) local classifier = nn.Sequential() - classifier:add(nn.View(256*7*7)) + classifier:add(nn.View(256*6*6)) classifier:add(nn.Dropout(0.5)) - classifier:add(nn.Linear(256*7*7, 4096)) + classifier:add(nn.Linear(256*6*6, 4096)) classifier:add(nn.Threshold(0, 1e-6)) classifier:add(nn.Dropout(0.5)) classifier:add(nn.Linear(4096, 4096)) classifier:add(nn.Threshold(0, 1e-6)) - classifier:add(nn.Linear(4096, 20)) + classifier:add(nn.Linear(4096, 1000)) classifier:add(nn.LogSoftMax()) - local model = nn.Sequential() - model:add(features):add(classifier) + -- 1.4. Combine 1.1 and 1.3 to produce final model + local model = nn.Sequential():add(features):add(classifier) return model +end + +-- return function that returns network definition +return function(params) + assert(params.ngpus<=1, 'Model supports only one GPU') + return { + model = createModel(1), + croplen = 224 + } +end + diff --git a/digits/standard-networks/torch/ImageNet-Training/googlenet.lua b/digits/standard-networks/torch/ImageNet-Training/googlenet.lua index e21061ffa..cc957fe4a 100644 --- a/digits/standard-networks/torch/ImageNet-Training/googlenet.lua +++ b/digits/standard-networks/torch/ImageNet-Training/googlenet.lua @@ -1,134 +1,127 @@ --- Copyright (c) 2015 Elad Hoffer +-- source: https://github.com/soumith/imagenet-multiGPU.torch/blob/master/models/alexnet_cudnn.lua require 'nn' -require 'cunn' -require 'cudnn' -require 'ccn2' -local opt = opt or {type = 'cuda', net='new'} -local DimConcat = 2 - ----------------------------------------Inception Modules------------------------------------------------- -local Inception = function(nInput, n1x1, n3x3r, n3x3, n5x5r, n5x5, nPoolProj) - local InceptionModule = nn.DepthConcat(DimConcat) - InceptionModule:add(nn.Sequential():add(nn.SpatialConvolutionMM(nInput,n1x1,1,1))) - InceptionModule:add(nn.Sequential():add(nn.SpatialConvolutionMM(nInput,n3x3r,1,1)):add(nn.ReLU()):add(nn.SpatialConvolutionMM(n3x3r,n3x3,3,3,1,1,1))) - InceptionModule:add(nn.Sequential():add(nn.SpatialConvolutionMM(nInput,n5x5r,1,1)):add(nn.ReLU()):add(nn.SpatialConvolutionMM(n5x5r,n5x5,5,5,1,1,2))) - InceptionModule:add(nn.Sequential():add(cudnn.SpatialMaxPooling(3,3,1,1)):add(nn.SpatialConvolutionMM(nInput,nPoolProj,1,1))) - return InceptionModule +if pcall(function() require('cudnn') end) then + print('Using CuDNN backend') + backend = cudnn + convLayer = cudnn.SpatialConvolution +else + print('Failed to load cudnn backend (is libcudnn.so in your library path?)') + if pcall(function() require('cunn') end) then + print('Falling back to legacy cunn backend') + else + print('Failed to load cunn backend (is CUDA installed?)') + print('Falling back to legacy nn backend') + end + backend = nn -- works with cunn or nn + convLayer = nn.SpatialConvolutionMM end -local AuxileryClassifier = function(nInput) - local C = nn.Sequential() - C:add(cudnn.SpatialAveragePooling(5,5,3,3)) - C:add(nn.SpatialConvolutionMM(nInput,128,1,1)) - C:add(nn.ReLU()) - C:add(nn.Reshape(128*4*4)) - C:add(nn.Linear(128*4*4,1024)) - C:add(nn.Dropout(0.7)) - C:add(nn.Linear(1024,1000)) - C:add(nn.LogSoftMax()) - return C +local function inception(input_size, config) + local concat = nn.Concat(2) + if config[1][1] ~= 0 then + local conv1 = nn.Sequential() + conv1:add(convLayer(input_size, config[1][1],1,1,1,1)):add(backend.ReLU(true)) + concat:add(conv1) + end + + local conv3 = nn.Sequential() + conv3:add(convLayer( input_size, config[2][1],1,1,1,1)):add(backend.ReLU(true)) + conv3:add(convLayer(config[2][1], config[2][2],3,3,1,1,1,1)):add(backend.ReLU(true)) + concat:add(conv3) + + local conv3xx = nn.Sequential() + conv3xx:add(convLayer( input_size, config[3][1],1,1,1,1)):add(backend.ReLU(true)) + conv3xx:add(convLayer(config[3][1], config[3][2],3,3,1,1,1,1)):add(backend.ReLU(true)) + conv3xx:add(convLayer(config[3][2], config[3][2],3,3,1,1,1,1)):add(backend.ReLU(true)) + concat:add(conv3xx) + + local pool = nn.Sequential() + pool:add(nn.SpatialZeroPadding(1,1,1,1)) -- remove after getting cudnn R2 into fbcode + if config[4][1] == 'max' then + pool:add(backend.SpatialMaxPooling(3,3,1,1):ceil()) + elseif config[4][1] == 'avg' then + local l = backend.SpatialAveragePooling(3,3,1,1) + if backend == cudnn then l = l:ceil() end + pool:add(l) + else + error('Unknown pooling') + end + if config[4][2] ~= 0 then + pool:add(convLayer(input_size, config[4][2],1,1,1,1)):add(backend.ReLU(true)) + end + concat:add(pool) + + return concat end ------------------------------------------------------------------------------------------------------------ - -local Net = nn.Sequential() - -local SubNet1 = nn.Sequential() -SubNet1:add(nn.SpatialConvolutionMM(3,64,7,7,2,2,4)) -SubNet1:add(nn.ReLU()) -SubNet1:add(cudnn.SpatialMaxPooling(3,3,2,2)) ---SubNet1:add(ccn2.SpatialResponseNormalization(3)) -SubNet1:add(nn.SpatialConvolutionMM(64,64,1,1)) -SubNet1:add(nn.ReLU()) -SubNet1:add(nn.SpatialConvolutionMM(64,192,3,3,1,1,1)) -SubNet1:add(nn.ReLU()) ---SubNet1:add(ccn2.SpatialResponseNormalization(3)) -SubNet1:add(nn.SpatialZeroPadding(1,1,1,1)) -SubNet1:add(cudnn.SpatialMaxPooling(3,3,2,2)) - - - -SubNet1:add(Inception(192,64,96,128,16,32,32)) -SubNet1:add(nn.ReLU()) -SubNet1:add(Inception(256,128,128,192,32,96,64)) -SubNet1:add(nn.ReLU()) -SubNet1:add(nn.SpatialZeroPadding(1,1,1,1)) -SubNet1:add(cudnn.SpatialMaxPooling(3,3,2,2)) -SubNet1:add(Inception(480,192,96,208,16,48,64)) -SubNet1:add(nn.ReLU()) - - - -local SubNet2 = nn.Sequential() -SubNet2:add(SubNet1) -SubNet2:add(Inception(512,160,112,224,24,64,64)) -SubNet2:add(nn.ReLU()) -SubNet2:add(Inception(512,128,128,256,24,64,64)) -SubNet2:add(nn.ReLU()) -SubNet2:add(Inception(512,112,144,288,32,64,64)) -SubNet2:add(nn.ReLU()) - - -Net:add(SubNet2) -Net:add(Inception(528,256,160,320,32,128,128)) -Net:add(nn.ReLU()) -Net:add(nn.SpatialZeroPadding(1,1,1,1)) -Net:add(cudnn.SpatialMaxPooling(3,3,2,2)) - - -Net:add(Inception(832,256,160,320,32,128,128)) -Net:add(nn.ReLU()) -Net:add(Inception(832,384,192,384,48,128,128)) -Net:add(nn.ReLU()) -Net:add(cudnn.SpatialAveragePooling(7,7,1,1)) -Net:add(nn.Dropout(0.4)) -Net:add(nn.Reshape(1024)) -Net:add(nn.Linear(1024,1000)) -Net:add(nn.LogSoftMax()) - -local Classifier0 = nn.Sequential() -Classifier0:add(SubNet1) -Classifier0:add(AuxileryClassifier(512)) - -local Classifier1 = nn.Sequential() -Classifier1:add(SubNet2) -Classifier1:add(AuxileryClassifier(528)) - --- ---Net:cuda() ------- Loss: NLL ---Net = Classifier0 -local loss = nn.ClassNLLCriterion() ----------------------------------------------------------------------- -if opt.type == 'cuda' then - Net:cuda() - loss:cuda() +function createModel(nGPU) + -- batch normalization added on top of convolutional layers in feature branch + -- in order to help the network learn faster + local features = nn.Sequential() + features:add(nn.MulConstant(0.02)) + features:add(convLayer(3,64,7,7,2,2,3,3)):add(nn.SpatialBatchNormalization(64,1e-3)):add(backend.ReLU(true)) + features:add(backend.SpatialMaxPooling(3,3,2,2):ceil()) + features:add(convLayer(64,64,1,1)):add(nn.SpatialBatchNormalization(64,1e-3)):add(backend.ReLU(true)) + features:add(convLayer(64,192,3,3,1,1,1,1)):add(nn.SpatialBatchNormalization(192,1e-3)):add(backend.ReLU(true)) + features:add(backend.SpatialMaxPooling(3,3,2,2):ceil()) + features:add(inception( 192, {{ 64},{ 64, 64},{ 64, 96},{'avg', 32}})) -- 3(a) + features:add(inception( 256, {{ 64},{ 64, 96},{ 64, 96},{'avg', 64}})) -- 3(b) + features:add(inception( 320, {{ 0},{128,160},{ 64, 96},{'max', 0}})) -- 3(c) + features:add(convLayer(576,576,2,2,2,2)):add(nn.SpatialBatchNormalization(576,1e-3)) + features:add(inception( 576, {{224},{ 64, 96},{ 96,128},{'avg',128}})) -- 4(a) + features:add(inception( 576, {{192},{ 96,128},{ 96,128},{'avg',128}})) -- 4(b) + features:add(inception( 576, {{160},{128,160},{128,160},{'avg', 96}})) -- 4(c) + features:add(inception( 576, {{ 96},{128,192},{160,192},{'avg', 96}})) -- 4(d) + + local main_branch = nn.Sequential() + main_branch:add(inception( 576, {{ 0},{128,192},{192,256},{'max', 0}})) -- 4(e) + main_branch:add(convLayer(1024,1024,2,2,2,2)):add(nn.SpatialBatchNormalization(1024,1e-3)) + main_branch:add(inception(1024, {{352},{192,320},{160,224},{'avg',128}})) -- 5(a) + main_branch:add(inception(1024, {{352},{192,320},{192,224},{'max',128}})) -- 5(b) + main_branch:add(backend.SpatialAveragePooling(7,7,1,1)) + main_branch:add(nn.View(1024):setNumInputDims(3)) + main_branch:add(nn.Linear(1024,1000)) + main_branch:add(nn.LogSoftMax()) + + -- add auxillary classifier here (thanks to Christian Szegedy for the details) + local aux_classifier = nn.Sequential() + local l = backend.SpatialAveragePooling(5,5,3,3) + if backend == cudnn then l = l:ceil() end + aux_classifier:add(l) + aux_classifier:add(convLayer(576,128,1,1,1,1)):add(nn.SpatialBatchNormalization(128,1e-3)) + aux_classifier:add(nn.View(128*4*4):setNumInputDims(3)) + aux_classifier:add(nn.Linear(128*4*4,768)) + aux_classifier:add(nn.ReLU()) + aux_classifier:add(nn.Linear(768,1000)) + aux_classifier:add(nn.LogSoftMax()) + + local splitter = nn.Concat(2) + splitter:add(main_branch):add(aux_classifier) + --local model = nn.Sequential():add(features):add(splitter) + local model = nn.Sequential():add(features):add(main_branch) + + if nGPU > 1 then + assert(nGPU <= cutorch.getDeviceCount(), 'number of GPUs less than nGPU specified') + require 'fbcunn' + local model_single = model + model = nn.DataParallel(1) + for i=1,nGPU do + cutorch.withDevice(i, function() + model:add(model_single:clone()) + end) + end + end + + return model end ----------------------------------------------------------------------- -print '==> flattening Net parameters' - --- Retrieve parameters and gradients: --- this extracts and flattens all the trainable parameters of the mode --- into a 1-dim vector ---end - -local w,dE_dw = Net:getParameters() - -local t = torch.load('Weights') -w:copy(t) --- ---local t = torch.tic(); y = Net:forward(torch.rand(128,3,224,224):cuda()) ; cutorch.synchronize(); print(torch.tic()-t) ---print(SubNet1.modules[9].output:size()) ---print(y:size()) - - --- return package: -return { - Net = Net, - Weights = w, - Grads = dE_dw, - Loss = loss -} +-- return function that returns network definition +return function(params) + assert(params.ngpus<=1, 'Model supports only one GPU') + return { + model = createModel(1), + croplen = 224 + } +end diff --git a/digits/standard-networks/torch/lenet.lua b/digits/standard-networks/torch/lenet.lua index 56d254672..126bd0ce7 100644 --- a/digits/standard-networks/torch/lenet.lua +++ b/digits/standard-networks/torch/lenet.lua @@ -1,20 +1,26 @@ require 'nn' -require 'cunn' -require 'inn' -- -- This is a LeNet model. For more information: http://yann.lecun.com/exdb/lenet/ -local model = nn.Sequential() -model:add(nn.MulConstant(0.00390625)) -model:add(nn.SpatialConvolution(1,20,5,5,1,1,0)) -- 1*28*28 -> 20*24*24 -model:add(inn.SpatialMaxPooling(2, 2, 2, 2)) -- 20*24*24 -> 20*12*12 -model:add(nn.SpatialConvolution(20,50,5,5,1,1,0)) -- 20*12*12 -> 50*8*8 -model:add(inn.SpatialMaxPooling(2,2,2,2)) -- 50*8*8 -> 50*4*4 -model:add(nn.View(-1):setNumInputDims(3)) -- 50*4*4 -> 800 -model:add(nn.Linear(800,500)) -- 800 -> 500 -model:add(nn.ReLU()) -model:add(nn.Linear(500, 10)) -- 500 -> 10 -model:add(nn.LogSoftMax()) -model:cuda() -return model +local lenet = nn.Sequential() +lenet:add(nn.MulConstant(0.00390625)) +lenet:add(nn.SpatialConvolution(1,20,5,5,1,1,0)) -- 1*28*28 -> 20*24*24 +lenet:add(nn.SpatialMaxPooling(2, 2, 2, 2)) -- 20*24*24 -> 20*12*12 +lenet:add(nn.SpatialConvolution(20,50,5,5,1,1,0)) -- 20*12*12 -> 50*8*8 +lenet:add(nn.SpatialMaxPooling(2,2,2,2)) -- 50*8*8 -> 50*4*4 +lenet:add(nn.View(-1):setNumInputDims(3)) -- 50*4*4 -> 800 +lenet:add(nn.Linear(800,500)) -- 800 -> 500 +lenet:add(nn.ReLU()) +lenet:add(nn.Linear(500, 10)) -- 500 -> 10 +lenet:add(nn.LogSoftMax()) + +-- return function that returns network definition +return function(params) + assert(params.ngpus<=1, 'Model supports only CPU or single-GPU') + return { + model = lenet, + loss = nn.ClassNLLCriterion() + } +end + diff --git a/digits/task.py b/digits/task.py index fffe5a5cc..6d81fe9bb 100644 --- a/digits/task.py +++ b/digits/task.py @@ -76,6 +76,12 @@ def name(self): """ raise NotImplementedError + def get_framework_id(self): + """ + Returns a string + """ + raise NotImplementedError('Please implement me') + def html_id(self): """ Returns a string diff --git a/digits/templates/models/images/classification/classify_one.html b/digits/templates/models/images/classification/classify_one.html index 228362b59..36a6403b4 100644 --- a/digits/templates/models/images/classification/classify_one.html +++ b/digits/templates/models/images/classification/classify_one.html @@ -89,7 +89,7 @@

Predictions

{% for vis in visualizations %} -

"{{vis['name']}}"

+

{{vis['name']}}

{{vis['vis_type']}} {% if 'layer_type' in vis %} diff --git a/digits/templates/models/images/classification/new.html b/digits/templates/models/images/classification/new.html index 1c341dd3d..d4809f776 100644 --- a/digits/templates/models/images/classification/new.html +++ b/digits/templates/models/images/classification/new.html @@ -353,8 +353,30 @@

Solver Options

frameworks['{{ fw.get_id() }}'] = framework; {% endfor %} +function setFramework(fwid) +{ + $('#framework').val(fwid); + if (frameworks[fwid].can_shuffle) + $("#shuffle-data").show(); + else + $("#shuffle-data").hide(); + if (fwid == 'torch') + $("#torch-warning").show(); + else + $("#torch-warning").hide(); + $('#stdnetRole a[href="'+"#"+fwid+"_standard"+'"]').tab('show'); + $('#customFramework a[href="'+"#"+fwid+"_custom"+'"]').tab('show'); +} +