From 49ef0525363e8bcb3d38f763b2565eb42cfbf8ea Mon Sep 17 00:00:00 2001 From: Hang Zhang Date: Wed, 4 Dec 2019 12:05:19 -0800 Subject: [PATCH] Bug Bash Patch (#94) * transform wip * mnist demo * cv tutorial on cpu * for demo * wip * kaggle patch * handle trunked * method * python dependencies * miscs * add catboost * rm ag.done --- Jenkinsfile | 7 +- autogluon/__init__.py | 1 + autogluon/core/decorator.py | 8 +- autogluon/core/loss.py | 8 + autogluon/core/optimizer.py | 2 - autogluon/scheduler/__init__.py | 1 + autogluon/scheduler/remote/remote.py | 24 +- .../task/image_classification/classifier.py | 70 +++- .../task/image_classification/dataset.py | 105 +++-- .../image_classification.py | 7 +- .../task/image_classification/metrics.py | 6 +- autogluon/task/image_classification/nets.py | 84 +++- .../task/image_classification/pipeline.py | 8 +- autogluon/task/image_classification/utils.py | 18 +- .../task/text_classification/pipeline.py | 5 +- .../text_classification.py | 3 - autogluon/utils/__init__.py | 11 +- autogluon/utils/data_analyzer.py | 75 ---- autogluon/utils/pil_transforms.py | 379 ++++++++++++++++++ .../utils/{visualizer.py => plot_network.py} | 0 docs/install-include.rst | 94 ++++- docs/tutorials/course/algorithm.md | 6 - docs/tutorials/course/core.md | 8 +- .../image_classification/beginner.md | 41 +- docs/tutorials/image_classification/hpo.md | 5 - docs/tutorials/image_classification/kaggle.md | 155 ++----- docs/tutorials/index.rst | 1 + docs/tutorials/nas/enas_mnist.md | 10 - docs/tutorials/nas/index.rst | 2 + docs/tutorials/nas/rl_searcher.md | 4 +- docs/tutorials/object_detection/beginner.md | 7 - .../tabular_prediction/tabular-indepth.md | 6 - .../tabular_prediction/tabular-quickstart.md | 5 - .../tutorials/text_classification/beginner.md | 6 - docs/tutorials/text_classification/index.rst | 2 +- docs/tutorials/torch/hpo.md | 12 +- setup.py | 24 +- 37 files changed, 773 insertions(+), 437 deletions(-) create mode 100644 autogluon/core/loss.py delete mode 100644 autogluon/utils/data_analyzer.py create mode 100644 autogluon/utils/pil_transforms.py rename autogluon/utils/{visualizer.py => plot_network.py} (100%) diff --git a/Jenkinsfile b/Jenkinsfile index ae99f951bf2..95c4d8db54e 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -8,8 +8,8 @@ stage("Unit Test") { VISIBLE_GPU=env.EXECUTOR_NUMBER.toInteger() % 8 sh """#!/bin/bash set -ex - # remove and create new env instead - conda env update -n autogluon_py3 -f docs/build.yml + conda env remove -n autogluon_py3 + conda env create -n autogluon_py3 -f docs/build.yml conda activate autogluon_py3 conda list export CUDA_VISIBLE_DEVICES=${VISIBLE_GPU} @@ -35,7 +35,8 @@ stage("Build Docs") { sh """#!/bin/bash set -ex export CUDA_VISIBLE_DEVICES=${VISIBLE_GPU} - conda env update -n autogluon_docs -f docs/build_contrib.yml + conda env remove -n autogluon_docs + conda env create -n autogluon_docs -f docs/build_contrib.yml conda activate autogluon_docs export PYTHONPATH=\${PWD} env diff --git a/autogluon/__init__.py b/autogluon/__init__.py index ce3d82965a8..0416b574b2a 100644 --- a/autogluon/__init__.py +++ b/autogluon/__init__.py @@ -8,6 +8,7 @@ try_import_mxnet() from . import scheduler, searcher, nas, utils +from .scheduler import get_cpu_count, get_gpu_count from .utils import * from .core import * diff --git a/autogluon/core/decorator.py b/autogluon/core/decorator.py index fc64f95b879..0ce3a904aad 100644 --- a/autogluon/core/decorator.py +++ b/autogluon/core/decorator.py @@ -213,12 +213,12 @@ def registered_class(Cls): class autogluonobject(AutoGluonObject): @_autogluon_kwargs(**kwvars) def __init__(self, *args, **kwargs): - self._args = args - self._kwargs = kwargs + self.args = args + self.kwargs = kwargs self._inited = False def sample(self, **config): - kwargs = copy.deepcopy(self._kwargs) + kwargs = copy.deepcopy(self.kwargs) kwspaces = copy.deepcopy(autogluonobject.kwspaces) for k, v in kwargs.items(): if k in kwspaces and isinstance(kwspaces[k], NestedSpace): @@ -227,7 +227,7 @@ def sample(self, **config): elif k in config: kwargs[k] = config[k] - args = self._args + args = self.args return Cls(*args, **kwargs) def __repr__(self): diff --git a/autogluon/core/loss.py b/autogluon/core/loss.py new file mode 100644 index 00000000000..ddaacf3ce6b --- /dev/null +++ b/autogluon/core/loss.py @@ -0,0 +1,8 @@ +from mxnet.gluon import loss +from ..core import obj + +__all__ = ['SoftmaxCrossEntropyLoss'] + +@obj() +class SoftmaxCrossEntropyLoss(loss.SoftmaxCrossEntropyLoss): + pass diff --git a/autogluon/core/optimizer.py b/autogluon/core/optimizer.py index 4746239f0dc..d694a00c03a 100644 --- a/autogluon/core/optimizer.py +++ b/autogluon/core/optimizer.py @@ -1,6 +1,4 @@ -import ConfigSpace as CS from mxnet import optimizer as optim - from ..core import obj __all__ = ['Adam', 'NAG', 'SGD'] diff --git a/autogluon/scheduler/__init__.py b/autogluon/scheduler/__init__.py index 180ef0cef96..4afee714c21 100644 --- a/autogluon/scheduler/__init__.py +++ b/autogluon/scheduler/__init__.py @@ -1,4 +1,5 @@ from .import remote, resource +from .resource import get_cpu_count, get_gpu_count # schedulers from .scheduler import * diff --git a/autogluon/scheduler/remote/remote.py b/autogluon/scheduler/remote/remote.py index 61a8a4076f7..5f90e4a35e5 100644 --- a/autogluon/scheduler/remote/remote.py +++ b/autogluon/scheduler/remote/remote.py @@ -24,8 +24,8 @@ def __init__(self, remote_ip=None, port=None, local=False, ssh_username=None, if not local: remote_addr = (remote_ip + ':{}'.format(port)) self.service = DaskRemoteService(remote_ip, port, ssh_username, - ssh_port, ssh_private_key, remote_python, - remote_dask_worker) + ssh_port, ssh_private_key, remote_python, + remote_dask_worker) super(Remote, self).__init__(remote_addr) else: super(Remote, self).__init__(processes=False) @@ -85,15 +85,15 @@ def __init__(self, remote_addr, scheduler_port, ssh_username=None, ) # Start worker nodes self.worker = start_worker( - self.scheduler_addr, - self.scheduler_port, - remote_addr, - self.ssh_username, - self.ssh_port, - self.ssh_private_key, - self.remote_python, - self.remote_dask_worker, - ) + self.scheduler_addr, + self.scheduler_port, + remote_addr, + self.ssh_username, + self.ssh_port, + self.ssh_private_key, + self.remote_python, + self.remote_dask_worker, + ) self.start_monitoring() def start_monitoring(self): @@ -118,7 +118,7 @@ def monitor_remote_processes(self): time.sleep(0.1) except KeyboardInterrupt: - self.shutdown() + pass def shutdown(self): all_processes = [self.worker, self.scheduler] diff --git a/autogluon/task/image_classification/classifier.py b/autogluon/task/image_classification/classifier.py index 0cca93031b2..68ed3199e3f 100644 --- a/autogluon/task/image_classification/classifier.py +++ b/autogluon/task/image_classification/classifier.py @@ -1,14 +1,22 @@ import os import math import pickle +import numpy as np +from PIL import Image + import mxnet as mx import matplotlib.pyplot as plt from mxnet.gluon.data.vision import transforms +from ...core import AutoGluonObject from .utils import * from .metrics import get_metric_instance from ..base.base_predictor import BasePredictor from ...utils import save, load, tqdm +from ...utils.pil_transforms import * + +__all__ = ['Classifier'] + class Classifier(BasePredictor): """ Classifier returned by task.fit() @@ -54,35 +62,55 @@ def save(self, checkpoint): def predict(self, X, input_size=224, plot=True): """ This method should be able to produce predictions regardless if: - X = single data example (e.g. single image, single document), - X = batch of many examples, X = task.Dataset object + X = single data example (e.g. single image), + X = task.Dataset object """ """The task predict function given an input. - Args: - img: the input - Example: - >>> ind, prob = classifier.predict('example.jpg') + Parameters + ---------- + X : str or :func:`autogluon.task.ImageClassification.Dataset` + path to the input image or dataset + Example: + >>> ind, prob = classifier.predict('example.jpg') """ - # load and display the image - img = mx.image.imread(X) if isinstance(X, str) and os.path.isfile(X) else X - if plot: - plt.imshow(img.asnumpy()) - plt.show() # model inference input_size = self.model.input_size if hasattr(self.model, 'input_size') else input_size resize = int(math.ceil(input_size / 0.875)) - transform_fn = transforms.Compose([ - transforms.Resize(resize), - transforms.CenterCrop(input_size), - transforms.ToTensor(), + transform_fn = Compose([ + Resize(resize), + CenterCrop(input_size), + ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) - img = transform_fn(img) - proba = self.predict_proba(img) - ind = mx.nd.argmax(proba, axis=1).astype('int') - idx = mx.nd.stack(mx.nd.arange(proba.shape[0], ctx=proba.context), - ind.astype('float32')) - return ind, mx.nd.gather_nd(proba, idx) + def predict_img(img): + # load and display the image + proba = self.predict_proba(img) + ind = mx.nd.argmax(proba, axis=1).astype('int') + idx = mx.nd.stack(mx.nd.arange(proba.shape[0], ctx=proba.context), + ind.astype('float32')) + probai = mx.nd.gather_nd(proba, idx) + return ind, probai + if isinstance(X, str) and os.path.isfile(X): + img = self.loader(X) + if plot: + plt.imshow(np.array(img)) + plt.show() + img = transform_fn(img) + return predict_img(img) + if isinstance(X, AutoGluonObject): + X = X.init() + inds, probas = [], [] + for x in X: + ind, proba = predict_img(x) + inds.append(ind) + probas.append(proba) + return inds, probas + + @staticmethod + def loader(path): + with open(path, 'rb') as f: + img = Image.open(f) + return img.convert('RGB') def predict_proba(self, X): """ Produces predicted class probabilities if we are dealing with a classification task. diff --git a/autogluon/task/image_classification/dataset.py b/autogluon/task/image_classification/dataset.py index 8f1dce8f18e..43321c5e1fa 100644 --- a/autogluon/task/image_classification/dataset.py +++ b/autogluon/task/image_classification/dataset.py @@ -1,6 +1,7 @@ import os import sys import math +import logging import numpy as np from PIL import Image @@ -12,8 +13,11 @@ from ...core import * from ..base import BaseDataset from ...utils import get_data_rec +from ...utils.pil_transforms import * -__all__ = ['get_dataset', 'ImageFolderDataset', 'RecordDataset'] +__all__ = ['get_dataset', 'get_built_in_dataset', 'ImageFolderDataset', 'RecordDataset'] + +logger = logging.getLogger(__name__) built_in_datasets = [ 'mnist', @@ -21,12 +25,13 @@ 'cifar10', 'cifar100', 'imagenet', + 'fashionmnist', ] @func() def get_dataset(path=None, train=True, name=None, - input_size=224, crop_ratio=0.875, jitter_param=0.4, - *args, **kwargs): + input_size=224, crop_ratio=0.875, jitter_param=0.4, + *args, **kwargs): """ Method to produce image classification dataset for AutoGluon, can either be a :class:`ImageFolderDataset`, :class:`RecordioDataset`, or a popular dataset already built into AutoGluon ('mnist', 'cifar10', 'cifar100', 'imagenet'). @@ -48,27 +53,42 @@ def get_dataset(path=None, train=True, name=None, Center crop ratio (for evaluation only) """ resize = int(math.ceil(input_size / crop_ratio)) - if name in built_in_datasets: + if isinstance(name, str) and name.lower() in built_in_datasets: return get_built_in_dataset(name, train=train, input_size=input_size, *args, **kwargs) - transform = transforms.Compose([ - transforms.RandomResizedCrop(input_size), - transforms.RandomFlipLeftRight(), - transforms.RandomColorJitter(brightness=jitter_param, - contrast=jitter_param, - saturation=jitter_param), - transforms.RandomLighting(0.1), - transforms.ToTensor(), - transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) - ]) if train else transforms.Compose([ - transforms.Resize(resize), - transforms.CenterCrop(input_size), - transforms.ToTensor(), - transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) - ]) - dataset_cls = ImageFolderDataset if '.rec' not in path \ - else RecordDataset - dataset = dataset_cls(path, transform=transform, *args, **kwargs) + if '.rec' in path: + transform = transforms.Compose([ + transforms.RandomResizedCrop(input_size), + transforms.RandomFlipLeftRight(), + transforms.RandomColorJitter(brightness=jitter_param, + contrast=jitter_param, + saturation=jitter_param), + transforms.RandomLighting(0.1), + transforms.ToTensor(), + transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) + ]) if train else transforms.Compose([ + transforms.Resize(resize), + transforms.CenterCrop(input_size), + transforms.ToTensor(), + transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) + ]) + dataset = RecordDataset(path, *args, **kwargs) + dataset.transform_first(transform) + else: + # PIL Data Augmentation for users from Mac OSX + transform = Compose([ + RandomResizedCrop(input_size), + RandomHorizontalFlip(), + ColorJitter(0.4, 0.4, 0.4), + ToTensor(), + transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) + ]) if train else Compose([ + Resize(resize), + CenterCrop(input_size), + ToTensor(), + transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) + ]) + dataset = ImageFolderDataset(path, transform=transform, *args, **kwargs) return dataset.init() @obj() @@ -85,10 +105,7 @@ class RecordDataset(ImageRecordDataset): If True, always convert images to greyscale. \ If False, always convert images to colored (RGB). transform : function, default None - A user defined callback that transforms each sample. For example:: - - transform=lambda data, label: (data.astype(np.float32)/255, label) - + A user defined callback that transforms each sample. """ def __init__(self, filename, gray_scale=False, transform=None): flag = 0 if gray_scale else 1 @@ -102,8 +119,10 @@ def num_classes(self): def classes(self): raise NotImplemented +#from torch.utils.data import Dataset as PTDataset + @obj() -class ImageFolderDataset(MXDataset): +class ImageFolderDataset(object): """A generic data loader where the images are arranged in this way: :: root/dog/xxx.png @@ -156,15 +175,21 @@ def __init__(self, root, extensions=None, transform=None, is_valid_file=None): self.targets = [s[1] for s in samples] self.imgs = self.samples - @staticmethod - def make_dataset(dir, class_to_idx, extensions=None, is_valid_file=None): + def make_dataset(self, dir, class_to_idx, extensions=None, is_valid_file=None): images = [] dir = os.path.expanduser(dir) if not ((extensions is None) ^ (is_valid_file is None)): raise ValueError("Both extensions and is_valid_file cannot be None or not None at the same time") if extensions is not None: def is_valid_file(x): - return x.lower().endswith(extensions) + if not x.lower().endswith(extensions): + return False + valid = True + try: + self.loader(x) + except OSError: + valid = False + return valid for target in sorted(class_to_idx.keys()): d = os.path.join(dir, target) if not os.path.isdir(d): @@ -175,12 +200,17 @@ def is_valid_file(x): if is_valid_file(path): item = (path, class_to_idx[target]) images.append(item) - + if not class_to_idx: + for root, _, fnames in sorted(os.walk(dir)): + for fname in sorted(fnames): + path = os.path.abspath(os.path.join(root, fname)) + if is_valid_file(path): + item = (path, 0) + images.append(item) return images @staticmethod def loader(path): - # open path as file to avoid ResourceWarning (https://github.com/python-pillow/Pillow/issues/835) with open(path, 'rb') as f: img = Image.open(f) return img.convert('RGB') @@ -225,15 +255,11 @@ def __getitem__(self, index): """ path, target = self.samples[index] sample = self.loader(path) - sample = self._sample_transform(sample) if self._transform is not None: sample = self._transform(sample) return sample, target - def _sample_transform(self, img): - return nd.array(np.array(img), mx.cpu(0)) - def __len__(self): return len(self.samples) @@ -250,7 +276,8 @@ def get_built_in_dataset(name, train=True, input_size=224, batch_size=256, num_w shuffle=True, **kwargs): """Returns built-in popular image classification dataset baed on provided string name ('cifar10', 'cifar100','mnist','imagenet'). """ - print('get_built_in_dataset', name) + logger.info('get_built_in_dataset {}'.format(name)) + name = name.lower() if name in ['cifar10', 'cifar']: import gluoncv.data.transforms as gcv_transforms transform_split = transforms.Compose([ @@ -279,6 +306,10 @@ def get_built_in_dataset(name, train=True, input_size=224, batch_size=256, num_w def transform(data, label): return nd.transpose(data.astype(np.float32), (2,0,1))/255, label.astype(np.float32) return gluon.data.vision.MNIST(train=train, transform=transform) + elif name == 'fashionmnist': + def transform(data, label): + return nd.transpose(data.astype(np.float32), (2,0,1))/255, label.astype(np.float32) + return gluon.data.vision.FashionMNIST(train=train, transform=transform) elif name == 'imagenet': # Please setup the ImageNet dataset following the tutorial from GluonCV if train: diff --git a/autogluon/task/image_classification/image_classification.py b/autogluon/task/image_classification/image_classification.py index 9ae0891edd0..ea5195debdf 100644 --- a/autogluon/task/image_classification/image_classification.py +++ b/autogluon/task/image_classification/image_classification.py @@ -5,6 +5,7 @@ from mxnet import gluon, nd from ...core.optimizer import * +from ...core.loss import * from ...core import * from ...searcher import * from ...scheduler import * @@ -15,6 +16,7 @@ from .dataset import get_dataset from .pipeline import train_image_classification from .utils import * +from .nets import * from .classifier import Classifier __all__ = ['ImageClassification'] @@ -54,7 +56,7 @@ def fit(dataset, optimizer= SGD(learning_rate=Real(1e-3, 1e-2, log=True), wd=Real(1e-4, 1e-3, log=True)), lr_scheduler='cosine', - loss=gluon.loss.SoftmaxCrossEntropyLoss(), + loss=SoftmaxCrossEntropyLoss(), split_ratio=0.8, batch_size=64, input_size=224, @@ -75,7 +77,6 @@ def fit(dataset, dist_ip_addrs=[], grace_period=None, auto_search=True): - """ Auto fit on image classification dataset @@ -119,7 +120,7 @@ def fit(dataset, if auto_search: # The strategies can be injected here, for example: automatic suggest some hps # based on the dataset statistics - pass + net = auto_suggest_network(dataset, net) nthreads_per_trial = get_cpu_count() if nthreads_per_trial > get_cpu_count() else nthreads_per_trial ngpus_per_trial = get_gpu_count() if ngpus_per_trial > get_gpu_count() else ngpus_per_trial diff --git a/autogluon/task/image_classification/metrics.py b/autogluon/task/image_classification/metrics.py index 4edcfc4262a..b0e090f190d 100644 --- a/autogluon/task/image_classification/metrics.py +++ b/autogluon/task/image_classification/metrics.py @@ -7,14 +7,10 @@ metrics = {'accuracy': mxnet.metric.Accuracy, 'topkaccuracy': mxnet.metric.TopKAccuracy, - 'f1': mxnet.metric.F1, - 'mcc': mxnet.metric.MCC, - 'perplexity': mxnet.metric.Perplexity, 'mae': mxnet.metric.MAE, 'mse': mxnet.metric.MSE, 'rmse': mxnet.metric.RMSE, - 'crossentropy': mxnet.metric.CrossEntropy, - 'pearsoncorrelation': mxnet.metric.PearsonCorrelation} + 'crossentropy': mxnet.metric.CrossEntropy} def get_metric_instance(name, **kwargs): """Returns a metric instance by name diff --git a/autogluon/task/image_classification/nets.py b/autogluon/task/image_classification/nets.py index e8f9911da71..425e227479e 100644 --- a/autogluon/task/image_classification/nets.py +++ b/autogluon/task/image_classification/nets.py @@ -1,13 +1,91 @@ -import ConfigSpace as CS +import logging import mxnet as mx from mxnet import gluon, init +from mxnet.gluon import nn from gluoncv.model_zoo import get_model from ...core import * +logger = logging.getLogger(__name__) + +__all__ = ['get_network', 'auto_suggest_network'] + +class Identity(mx.gluon.HybridBlock): + def hybrid_forward(self, F, x): + return x +class ConvBNReLU(mx.gluon.HybridBlock): + def __init__(self, in_channels, channels, kernel, stride): + super().__init__() + padding = (kernel - 1) // 2 + self.conv = nn.Conv2D(channels, kernel, stride, padding, in_channels=in_channels) + self.bn = nn.BatchNorm(in_channels=channels) + self.relu = nn.Activation('relu') + def hybrid_forward(self, F, x): + return self.relu(self.bn(self.conv(x))) + +class ResUnit(mx.gluon.HybridBlock): + def __init__(self, in_channels, channels, hidden_channels, kernel, stride): + super().__init__() + self.conv1 = ConvBNReLU(in_channels, hidden_channels, kernel, stride) + self.conv2 = ConvBNReLU(hidden_channels, channels, kernel, 1) + if in_channels == channels and stride == 1: + self.shortcut = Identity() + else: + self.shortcut = nn.Conv2D(channels, 1, stride, in_channels=in_channels) + def hybrid_forward(self, F, x): + return self.conv2(self.conv1(x)) + self.shortcut(x) + +@func() +def mnist_net(): + mnist_net = gluon.nn.Sequential() + mnist_net.add(ResUnit(1, 8, hidden_channels=8, kernel=3, stride=2)) + mnist_net.add(ResUnit(8, 8, hidden_channels=8, kernel=5, stride=2)) + mnist_net.add(ResUnit(8, 16, hidden_channels=8, kernel=3, stride=2)) + mnist_net.add(nn.GlobalAvgPool2D()) + mnist_net.add(nn.Flatten()) + mnist_net.add(nn.Activation('relu')) + mnist_net.add(nn.Dense(10, in_units=16)) + return mnist_net + +def auto_suggest_network(dataset, net): + if isinstance(dataset, str): + dataset_name = dataset + elif isinstance(dataset, AutoGluonObject): + if 'name' in dataset.kwargs: + dataset_name = dataset.kwargs['name'] + else: + return net + else: + return net + dataset_name = dataset_name.lower() + if 'mnist' in dataset_name: + if isinstance(net, str) or isinstance(net, Categorical): + net = mnist_net() + logger.info('Auto suggesting network net for dataset {}'.format(net, dataset_name)) + return net + elif 'cifar' in dataset_name: + if isinstance(net, str): + if 'cifar' not in net: + net = 'cifar_resnet20_v1' + elif isinstance(net, Categorical): + newdata = [] + for x in net.data: + if 'cifar' in x: + newdata.append(x) + net.data = newdata if len(newdata) > 0 else ['cifar_resnet20_v1', 'cifar_resnet56_v1'] + logger.info('Auto suggesting network net for dataset {}'.format(net, dataset_name)) + return net + +def get_network(net, num_classes, ctx): + if type(net) == str: + net = get_built_in_network(net, num_classes, ctx=ctx) + else: + net.initialize(ctx=ctx) + return net + def get_built_in_network(name, *args, **kwargs): - def _get_finetune_network(model_name, num_classes, ctx): - finetune_net = get_model(model_name, pretrained=True) + def _get_finetune_network(model_name, num_classes, ctx, *args, **kwargs): + finetune_net = get_model(model_name, *args, pretrained=True, **kwargs) # change the last fully connected layer to match the number of classes with finetune_net.name_scope(): if hasattr(finetune_net, 'output'): diff --git a/autogluon/task/image_classification/pipeline.py b/autogluon/task/image_classification/pipeline.py index 31561ae5dbb..0917e9e95c9 100644 --- a/autogluon/task/image_classification/pipeline.py +++ b/autogluon/task/image_classification/pipeline.py @@ -13,7 +13,7 @@ from ...scheduler.resource import get_cpu_count, get_gpu_count from ...utils import tqdm from ...utils.mxutils import collect_params -from .nets import get_built_in_network +from .nets import get_network from .utils import * __all__ = ['train_image_classification'] @@ -34,7 +34,8 @@ def train_image_classification(args, reporter): batch_size = args.batch_size * max(args.num_gpus, 1) ctx = [mx.gpu(i) for i in range(args.num_gpus)] if args.num_gpus > 0 else [mx.cpu()] - net = get_network(args.net, args.dataset.num_classes, ctx) + num_classes = args.dataset.num_classes if hasattr(args.dataset, 'num_classes') else None + net = get_network(args.net, num_classes, ctx) if args.hybridize: net.hybridize(static_alloc=True, static_shape=True) @@ -52,7 +53,6 @@ def train_image_classification(args, reporter): trainer = gluon.Trainer(net.collect_params(), args.optimizer) metric = get_metric_instance(args.metric) - def train(epoch): for i, batch in enumerate(train_data): default_train_fn(net, batch, batch_size, args.loss, trainer, batch_fn, ctx) @@ -75,4 +75,4 @@ def test(epoch): if args.final_fit: return {'model_params': collect_params(net), - 'num_classes': args.dataset.num_classes} + 'num_classes': num_classes} diff --git a/autogluon/task/image_classification/utils.py b/autogluon/task/image_classification/utils.py index 336176fe137..95fbe25959b 100644 --- a/autogluon/task/image_classification/utils.py +++ b/autogluon/task/image_classification/utils.py @@ -4,9 +4,10 @@ import gluoncv as gcv from .nets import * from .dataset import * -from ...utils.dataset import get_split_samplers, SampledDataset +from ...core import AutoGluonObject +from ...utils import get_split_samplers, SampledDataset, DataLoader -__all__ = ['get_data_loader', 'get_network', 'imagenet_batch_fn', +__all__ = ['get_data_loader', 'imagenet_batch_fn', 'default_batch_fn', 'default_val_fn', 'default_train_fn'] def get_data_loader(dataset, input_size, batch_size, num_workers, final_fit, split_ratio): @@ -30,25 +31,18 @@ def get_data_loader(dataset, input_size, batch_size, num_workers, final_fit, spl num_batches = imagenet_samples // batch_size else: num_workers = 0 - train_data = gluon.data.DataLoader( + train_data = DataLoader( train_dataset, batch_size=batch_size, shuffle=True, - last_batch="rollover", num_workers=num_workers) + last_batch="discard", num_workers=num_workers) val_data = None if not final_fit: - val_data = gluon.data.DataLoader( + val_data = DataLoader( val_dataset, batch_size=batch_size, shuffle=False, num_workers=num_workers) batch_fn = default_batch_fn num_batches = len(train_data) return train_data, val_data, batch_fn, num_batches -def get_network(net, num_classes, ctx): - if type(net) == str: - net = get_built_in_network(net, num_classes, ctx=ctx) - else: - net.initialize(ctx=ctx) - return net - def imagenet_batch_fn(batch, ctx): data = gluon.utils.split_and_load(batch.data[0], ctx_list=ctx, batch_axis=0) label = gluon.utils.split_and_load(batch.label[0], ctx_list=ctx, batch_axis=0) diff --git a/autogluon/task/text_classification/pipeline.py b/autogluon/task/text_classification/pipeline.py index b4de491d5da..3639efce710 100644 --- a/autogluon/task/text_classification/pipeline.py +++ b/autogluon/task/text_classification/pipeline.py @@ -20,7 +20,6 @@ from ...utils.mxutils import collect_params __all__ = ['train_text_classification', 'preprocess_data'] -logger = logging.getLogger(__name__) def preprocess_data(tokenizer, task, batch_size, dev_batch_size, max_len, vocab, pad=False, num_workers=1): @@ -106,6 +105,7 @@ def preprocess_data(tokenizer, task, batch_size, dev_batch_size, max_len, vocab, def train_text_classification(args, reporter=None): # Step 1: add scripts every function and python objects in the original training script except for the training function # at the beginning of the decorated function + logger = logging.getLogger(__name__) if args.verbose: logger.setLevel(logging.INFO) logger.info(args) @@ -193,7 +193,6 @@ def train_text_classification(args, reporter=None): # Get the loader. - #logger.info('processing dataset...') train_data, dev_data_list, num_train_examples, trans, test_trans = preprocess_data( bert_tokenizer, task, batch_size, dev_batch_size, args.max_len, vocabulary, True, args.num_workers) @@ -220,7 +219,6 @@ def log_eval(batch_id, batch_num, metric, step_loss, log_interval, tbar): def evaluate(loader_dev, metric, segment): """Evaluate the model on validation dataset.""" - #logger.info('Now we are doing evaluation on %s with %s.', segment, ctx) metric.reset() step_loss = 0 tbar = tqdm(loader_dev) @@ -253,7 +251,6 @@ def evaluate(loader_dev, metric, segment): # Step 2: the training function in the original training script is added in the decorated function in autogluon for training. """Training function.""" - #logger.info('Now we are doing BERT classification training on %s!', ctx) all_model_params = model.collect_params() optimizer_params = {'learning_rate': lr, 'epsilon': epsilon, 'wd': 0.01} diff --git a/autogluon/task/text_classification/text_classification.py b/autogluon/task/text_classification/text_classification.py index bb2f75d950a..f6071cd3f36 100644 --- a/autogluon/task/text_classification/text_classification.py +++ b/autogluon/task/text_classification/text_classification.py @@ -36,7 +36,6 @@ def fit(dataset='SST', lr=Real(2e-05, 2e-04, log=True), warmup_ratio=0.01, lr_scheduler='cosine', - loss=gluon.loss.SoftmaxCrossEntropyLoss(), log_interval=100, seed=0, batch_size=32, @@ -70,7 +69,6 @@ def fit(dataset='SST', ---------- dataset (str or autogluon.task.TextClassification.Dataset): Training dataset. net (str): Network candidates. - loss (object): training loss function. num_trials (int): number of trials in the experiment. time_limits (int): training time limits in seconds. resources_per_trial (dict): Machine resources to allocate per trial. @@ -106,7 +104,6 @@ def fit(dataset='SST', accumulate=accumulate, seed=seed, lr_scheduler=lr_scheduler, - loss=loss, num_gpus=ngpus_per_trial, batch_size=batch_size, dev_batch_size=dev_batch_size, diff --git a/autogluon/utils/__init__.py b/autogluon/utils/__init__.py index 19955c1037b..d9332601cdc 100644 --- a/autogluon/utils/__init__.py +++ b/autogluon/utils/__init__.py @@ -1,17 +1,16 @@ from .files import * from .miscs import * +from .plots import * +from .tqdm import tqdm +from .dataset import * from .mxutils import * from .deprecate import * -from .visualizer import * from .try_import import * -from .file_helper import * from .edict import EasyDict from .serialization import * -from .data_analyzer import * from .dataloader import DataLoader -from .dataset import * -from .plots import * -from .tqdm import tqdm from .custom_queue import Queue +from .file_helper import generate_csv +from .plot_network import plot_network from .defaultdict import keydefaultdict from .util_decorator import classproperty diff --git a/autogluon/utils/data_analyzer.py b/autogluon/utils/data_analyzer.py deleted file mode 100644 index e52499cebe1..00000000000 --- a/autogluon/utils/data_analyzer.py +++ /dev/null @@ -1,75 +0,0 @@ -import warnings -import logging - -import numpy as np -from scipy.stats import ks_2samp -from mxnet import gluon - -__all__ = ['Visualizer', 'DataAnalyzer'] - - -class Visualizer(object): - def __init__(self): - pass - - @staticmethod - def visualize_dataset_label_histogram(a, b): - min_len = min(len(a._label), len(b._label)) - pyplot.hist([a._label[:min_len], b._label[:min_len]], - bins=len(np.unique(a._label)), - label=['a', 'b']) - pyplot.legend(loc='upper right') - pyplot.savefig('./histogram.png') - -class DataAnalyzer(object): - def __init__(self): - pass - - @staticmethod - def check_dataset_label_num(a, b): - if isinstance(a, gluon.data.dataset.Dataset) and isinstance(b, gluon.data.dataset.Dataset): - a_label_num = len(np.unique(a._label)) - b_label_num = len(np.unique(b._label)) - if a_label_num != b_label_num: - warnings.warn('Warning: ' - 'number of class of labels ' - 'in the compared datasets are different.') - else: - pass - - @staticmethod - def check_dataset_label_KStest(a, b): - if isinstance(a, gluon.data.dataset.Dataset) and isinstance(b, gluon.data.dataset.Dataset): - ks_res = ks_2samp(a._label, b._label) - if ks_res.pvalue < 0.05: - warnings.warn('Warning: ' - 'data label distribution are different at p-val 0.05 level.') - else: - pass - - @staticmethod - def check_dataset_label_histogram(a, b): - if isinstance(a, gluon.data.dataset.Dataset) and isinstance(b, gluon.data.dataset.Dataset): - min_len = min(len(a._label), len(b._label)) - #TODO(cgraywang): sample a min_len? - a_area, _ = np.histogram(a._label[:min_len], bins=len(np.unique(a._label))) - b_area, _ = np.histogram(b._label[:min_len], bins=len(np.unique(b._label))) - if (a_area <= b_area).all() or (a_area >= b_area).all(): - warnings.warn('Warning: ' - 'data label histogram seems not in a good shape') - logging.debug('data label histogram is save at ./histogram.png') - Visualizer.visualize_dataset_label_histogram(a, b) - else: - pass - - @staticmethod - def check_dataset(a, b): - DataAnalyzer.check_dataset_label_num(a, b) - DataAnalyzer.check_dataset_label_KStest(a, b) - DataAnalyzer.check_dataset_label_histogram(a, b) - - @staticmethod - def stat_dataset(a): - assert a is not None - return len(np.unique(a._label)), a.__len__(), \ - np.mean(a._label), np.std(a._label), np.var(a._label) diff --git a/autogluon/utils/pil_transforms.py b/autogluon/utils/pil_transforms.py new file mode 100644 index 00000000000..311142caeeb --- /dev/null +++ b/autogluon/utils/pil_transforms.py @@ -0,0 +1,379 @@ +import math +import random +import numbers +import warnings +import numpy as np +import mxnet as mx +from PIL import Image, ImageEnhance +import collections + +__all__ = ['Compose', 'RandomResizedCrop', 'RandomHorizontalFlip', 'ColorJitter', + 'Resize', 'CenterCrop', 'ToTensor'] + +class Compose(object): + """Composes several transforms together. + Args: + transforms (list of ``Transform`` objects): list of transforms to compose. + Example: + >>> transforms.Compose([ + >>> transforms.CenterCrop(10), + >>> transforms.ToTensor(), + >>> ]) + """ + + def __init__(self, transforms): + self.transforms = transforms + + def __call__(self, img): + for t in self.transforms: + img = t(img) + return img + + def __repr__(self): + format_string = self.__class__.__name__ + '(' + for t in self.transforms: + format_string += '\n' + format_string += ' {0}'.format(t) + format_string += '\n)' + return format_string + +class ToTensor(object): + def __call__(self, img): + x = mx.nd.array(np.array(img), mx.cpu(0)) + return x.swapaxes(0, 2).swapaxes(1, 2) + +class RandomResizedCrop(object): + """Crop the given PIL Image to random size and aspect ratio. + A crop of random size (default: of 0.08 to 1.0) of the original size and a random + aspect ratio (default: of 3/4 to 4/3) of the original aspect ratio is made. This crop + is finally resized to given size. + This is popularly used to train the Inception networks. + Args: + size: expected output size of each edge + scale: range of size of the origin size cropped + ratio: range of aspect ratio of the origin aspect ratio cropped + interpolation: Default: PIL.Image.BILINEAR + """ + + def __init__(self, size, scale=(0.08, 1.0), ratio=(3. / 4., 4. / 3.), interpolation=Image.BILINEAR): + if isinstance(size, tuple): + self.size = size + else: + self.size = (size, size) + if (scale[0] > scale[1]) or (ratio[0] > ratio[1]): + warnings.warn("range should be of kind (min, max)") + + self.interpolation = interpolation + self.scale = scale + self.ratio = ratio + + @staticmethod + def get_params(img, scale, ratio): + width, height = img.size + area = height * width + + for attempt in range(10): + target_area = random.uniform(*scale) * area + log_ratio = (math.log(ratio[0]), math.log(ratio[1])) + aspect_ratio = math.exp(random.uniform(*log_ratio)) + + w = int(round(math.sqrt(target_area * aspect_ratio))) + h = int(round(math.sqrt(target_area / aspect_ratio))) + + if 0 < w <= width and 0 < h <= height: + i = random.randint(0, height - h) + j = random.randint(0, width - w) + return i, j, h, w + + # Fallback to central crop + in_ratio = float(width) / float(height) + if (in_ratio < min(ratio)): + w = width + h = int(round(w / min(ratio))) + elif (in_ratio > max(ratio)): + h = height + w = int(round(h * max(ratio))) + else: # whole image + w = width + h = height + i = (height - h) // 2 + j = (width - w) // 2 + return i, j, h, w + + def __call__(self, img): + """ + Args: + img (PIL Image): Image to be cropped and resized. + Returns: + PIL Image: Randomly cropped and resized image. + """ + i, j, h, w = self.get_params(img, self.scale, self.ratio) + return self.resized_crop(img, i, j, h, w, self.size, self.interpolation) + + def __repr__(self): + interpolate_str = _pil_interpolation_to_str[self.interpolation] + format_string = self.__class__.__name__ + '(size={0}'.format(self.size) + format_string += ', scale={0}'.format(tuple(round(s, 4) for s in self.scale)) + format_string += ', ratio={0}'.format(tuple(round(r, 4) for r in self.ratio)) + format_string += ', interpolation={0})'.format(interpolate_str) + return format_string + + @staticmethod + def resized_crop(img, top, left, height, width, size, interpolation=Image.BILINEAR): + img = crop(img, top, left, height, width) + img = resize(img, size, interpolation) + return img + + +class RandomHorizontalFlip(object): + """Horizontally flip the given PIL Image randomly with a given probability. + Args: + p (float): probability of the image being flipped. Default value is 0.5 + """ + + def __init__(self, p=0.5): + self.p = p + + def __call__(self, img): + """ + Args: + img (PIL Image): Image to be flipped. + Returns: + PIL Image: Randomly flipped image. + """ + if random.random() < self.p: + return img.transpose(Image.FLIP_LEFT_RIGHT) + return img + + def __repr__(self): + return self.__class__.__name__ + '(p={})'.format(self.p) + +class Lambda(object): + """Apply a user-defined lambda as a transform. + Args: + lambd (function): Lambda/function to be used for transform. + """ + + def __init__(self, lambd): + assert callable(lambd), repr(type(lambd).__name__) + " object is not callable" + self.lambd = lambd + + def __call__(self, img): + return self.lambd(img) + + def __repr__(self): + return self.__class__.__name__ + '()' + +class ColorJitter(object): + """Randomly change the brightness, contrast and saturation of an image. + Args: + brightness (float or tuple of float (min, max)): How much to jitter brightness. + brightness_factor is chosen uniformly from [max(0, 1 - brightness), 1 + brightness] + or the given [min, max]. Should be non negative numbers. + contrast (float or tuple of float (min, max)): How much to jitter contrast. + contrast_factor is chosen uniformly from [max(0, 1 - contrast), 1 + contrast] + or the given [min, max]. Should be non negative numbers. + saturation (float or tuple of float (min, max)): How much to jitter saturation. + saturation_factor is chosen uniformly from [max(0, 1 - saturation), 1 + saturation] + or the given [min, max]. Should be non negative numbers. + hue (float or tuple of float (min, max)): How much to jitter hue. + hue_factor is chosen uniformly from [-hue, hue] or the given [min, max]. + Should have 0<= hue <= 0.5 or -0.5 <= min <= max <= 0.5. + """ + def __init__(self, brightness=0, contrast=0, saturation=0, hue=0): + self.brightness = self._check_input(brightness, 'brightness') + self.contrast = self._check_input(contrast, 'contrast') + self.saturation = self._check_input(saturation, 'saturation') + self.hue = self._check_input(hue, 'hue', center=0, bound=(-0.5, 0.5), + clip_first_on_zero=False) + + def _check_input(self, value, name, center=1, bound=(0, float('inf')), clip_first_on_zero=True): + if isinstance(value, numbers.Number): + if value < 0: + raise ValueError("If {} is a single number, it must be non negative.".format(name)) + value = [center - value, center + value] + if clip_first_on_zero: + value[0] = max(value[0], 0) + elif isinstance(value, (tuple, list)) and len(value) == 2: + if not bound[0] <= value[0] <= value[1] <= bound[1]: + raise ValueError("{} values should be between {}".format(name, bound)) + else: + raise TypeError("{} should be a single number or a list/tuple with lenght 2.".format(name)) + + # if value is 0 or (1., 1.) for brightness/contrast/saturation + # or (0., 0.) for hue, do nothing + if value[0] == value[1] == center: + value = None + return value + + @staticmethod + def get_params(brightness, contrast, saturation, hue): + """Get a randomized transform to be applied on image. + Arguments are same as that of __init__. + Returns: + Transform which randomly adjusts brightness, contrast and + saturation in a random order. + """ + transforms = [] + + if brightness is not None: + brightness_factor = random.uniform(brightness[0], brightness[1]) + transforms.append(Lambda(lambda img: adjust_brightness(img, brightness_factor))) + + if contrast is not None: + contrast_factor = random.uniform(contrast[0], contrast[1]) + transforms.append(Lambda(lambda img: adjust_contrast(img, contrast_factor))) + + if saturation is not None: + saturation_factor = random.uniform(saturation[0], saturation[1]) + transforms.append(Lambda(lambda img: adjust_saturation(img, saturation_factor))) + + if hue is not None: + hue_factor = random.uniform(hue[0], hue[1]) + transforms.append(Lambda(lambda img: adjust_hue(img, hue_factor))) + + random.shuffle(transforms) + transform = Compose(transforms) + + return transform + + def __call__(self, img): + """ + Args: + img (PIL Image): Input image. + Returns: + PIL Image: Color jittered image. + """ + transform = self.get_params(self.brightness, self.contrast, + self.saturation, self.hue) + return transform(img) + + def __repr__(self): + format_string = self.__class__.__name__ + '(' + format_string += 'brightness={0}'.format(self.brightness) + format_string += ', contrast={0}'.format(self.contrast) + format_string += ', saturation={0}'.format(self.saturation) + format_string += ', hue={0})'.format(self.hue) + return format_string + +class Resize(object): + """Resize the input PIL Image to the given size. + Args: + size (sequence or int): Desired output size. If size is a sequence like + (h, w), output size will be matched to this. If size is an int, + smaller edge of the image will be matched to this number. + i.e, if height > width, then image will be rescaled to + (size * height / width, size) + interpolation (int, optional): Desired interpolation. Default is + ``PIL.Image.BILINEAR`` + """ + + def __init__(self, size, interpolation=Image.BILINEAR): + assert isinstance(size, int) or (isinstance(size, collections.Iterable) and len(size) == 2) + self.size = size + self.interpolation = interpolation + + def __call__(self, img): + """ + Args: + img (PIL Image): Image to be scaled. + Returns: + PIL Image: Rescaled image. + """ + return resize(img, self.size, self.interpolation) + + def __repr__(self): + interpolate_str = _pil_interpolation_to_str[self.interpolation] + return self.__class__.__name__ + '(size={0}, interpolation={1})'.format(self.size, interpolate_str) + + +class CenterCrop(object): + """Crops the given PIL Image at the center. + Args: + size (sequence or int): Desired output size of the crop. If size is an + int instead of sequence like (h, w), a square crop (size, size) is + made. + """ + + def __init__(self, size): + if isinstance(size, numbers.Number): + self.size = (int(size), int(size)) + else: + self.size = size + + def __call__(self, img): + """ + Args: + img (PIL Image): Image to be cropped. + Returns: + PIL Image: Cropped image. + """ + return self.center_crop(img, self.size) + + def __repr__(self): + return self.__class__.__name__ + '(size={0})'.format(self.size) + + @staticmethod + def center_crop(img, output_size): + if isinstance(output_size, numbers.Number): + output_size = (int(output_size), int(output_size)) + image_width, image_height = img.size + crop_height, crop_width = output_size + crop_top = int(round((image_height - crop_height) / 2.)) + crop_left = int(round((image_width - crop_width) / 2.)) + return crop(img, crop_top, crop_left, crop_height, crop_width) + +def crop(img, top, left, height, width): + return img.crop((left, top, left + width, top + height)) + +def resize(img, size, interpolation=Image.BILINEAR): + if not (isinstance(size, int) or (isinstance(size, collections.Iterable) and len(size) == 2)): + raise TypeError('Got inappropriate size arg: {}'.format(size)) + + if isinstance(size, int): + w, h = img.size + if (w <= h and w == size) or (h <= w and h == size): + return img + if w < h: + ow = size + oh = int(size * h / w) + return img.resize((ow, oh), interpolation) + else: + oh = size + ow = int(size * w / h) + return img.resize((ow, oh), interpolation) + else: + return img.resize(size[::-1], interpolation) + +def adjust_brightness(img, brightness_factor): + enhancer = ImageEnhance.Brightness(img) + img = enhancer.enhance(brightness_factor) + return img + +def adjust_contrast(img, contrast_factor): + enhancer = ImageEnhance.Contrast(img) + img = enhancer.enhance(contrast_factor) + return img + +def adjust_saturation(img, saturation_factor): + enhancer = ImageEnhance.Color(img) + img = enhancer.enhance(saturation_factor) + return img + + +def adjust_hue(img, hue_factor): + if not(-0.5 <= hue_factor <= 0.5): + raise ValueError('hue_factor is not in [-0.5, 0.5].'.format(hue_factor)) + + input_mode = img.mode + if input_mode in {'L', '1', 'I', 'F'}: + return img + h, s, v = img.convert('HSV').split() + np_h = np.array(h, dtype=np.uint8) + # uint8 addition take cares of rotation across boundaries + with np.errstate(over='ignore'): + np_h += np.uint8(hue_factor * 255) + h = Image.fromarray(np_h, 'L') + + img = Image.merge('HSV', (h, s, v)).convert(input_mode) + return img diff --git a/autogluon/utils/visualizer.py b/autogluon/utils/plot_network.py similarity index 100% rename from autogluon/utils/visualizer.py rename to autogluon/utils/plot_network.py diff --git a/docs/install-include.rst b/docs/install-include.rst index 8a117304efd..aa683922f25 100644 --- a/docs/install-include.rst +++ b/docs/install-include.rst @@ -8,6 +8,17 @@ Select your preferences and run the install command. .. container:: install + .. container:: opt-group + + :title:`OS:` + :opt:`Linux` + :act:`Mac` + + .. raw:: html + +
Linux.
+
Mac OSX.
+ .. container:: opt-group :title:`Version:` @@ -33,39 +44,78 @@ Select your preferences and run the install command. .. admonition:: Command: - .. container:: pip + .. container:: linux + + .. container:: pip + + .. container:: cpu + + .. code-block:: bash + + pip install --upgrade mxnet + pip install https://autogluon.s3.amazonaws.com/dist/autogluon-0.0.1-py3-none-any.whl + + .. container:: cuda + + .. code-block:: bash + + # Here we assume CUDA 10.0 is installed. You can change the number + # according to your own CUDA version. + pip install --upgrade mxnet-cu100 + pip install https://autogluon.s3.amazonaws.com/dist/autogluon-0.0.1-py3-none-any.whl + + .. container:: source + + .. container:: cpu + + .. code-block:: bash + + pip install --pre --upgrade mxnet + git clone https://github.com/awslabs/autogluon + cd autogluon && python setup.py install --user + + .. container:: cuda + + .. code-block:: bash + + # Here we assume CUDA 10.0 is installed. You can change the number + # according to your own CUDA version. + pip install --pre --upgrade mxnet-cu100 + git clone https://github.com/awslabs/autogluon + cd autogluon && python setup.py install --user + + .. container:: mac + + .. container:: pip - .. container:: cpu + .. container:: cpu - .. code-block:: bash + .. code-block:: bash - pip install --upgrade mxnet autogluon + pip install https://autogluon.s3.amazonaws.com/dist/mxnet-1.6.0b20191125-cp37-cp37m-macosx_10_13_x86_64.whl + pip install https://autogluon.s3.amazonaws.com/dist/autogluon-0.0.1-py3-none-any.whl - .. container:: cuda + .. container:: cuda - .. code-block:: bash + .. code-block:: bash - # Here we assume CUDA 10.0 is installed. You can change the number - # according to your own CUDA version. - pip install --upgrade mxnet-cu100 autogluon + This option is only available by building from source. - .. container:: source + .. container:: source - .. container:: cpu + .. container:: cpu - .. code-block:: bash + .. code-block:: bash - pip install --pre --upgrade mxnet - git clone https://github.com/awslabs/autogluon - cd autogluon && python setup.py install --user + pip install https://autogluon.s3.amazonaws.com/dist/mxnet-1.6.0b20191125-cp37-cp37m-macosx_10_13_x86_64.whl + git clone https://github.com/awslabs/autogluon + cd autogluon && python setup.py install --user - .. container:: cuda + .. container:: cuda - .. code-block:: bash + .. code-block:: bash - # Here we assume CUDA 10.0 is installed. You can change the number - # according to your own CUDA version. - pip install --pre --upgrade mxnet-cu100 - git clone https://github.com/awslabs/autogluon - cd autogluon && python setup.py install --user + # Please build mxnet from source manually + git clone https://github.com/awslabs/autogluon + cd autogluon && python setup.py install --user diff --git a/docs/tutorials/course/algorithm.md b/docs/tutorials/course/algorithm.md index 673b0aa842b..5db10950f48 100644 --- a/docs/tutorials/course/algorithm.md +++ b/docs/tutorials/course/algorithm.md @@ -185,9 +185,3 @@ Plot the results: ```{.python .input} plt.plot(range(len(results1)), results1, range(len(results2)), results2) ``` - -Exit AutoGluon: - -```{.python .input} -ag.done() -``` diff --git a/docs/tutorials/course/core.md b/docs/tutorials/course/core.md index 5f7d0c11723..75abf1fadad 100644 --- a/docs/tutorials/course/core.md +++ b/docs/tutorials/course/core.md @@ -203,7 +203,7 @@ print(i) We can also make them inside a nested space: ```{.python .input} -j =ag.space.Dict( +j = ag.space.Dict( a=ag.Real(0, 10), obj1=MyObj(), obj2=myfunc(), @@ -254,9 +254,3 @@ print(config) ```{.python .input} train_fn(train_fn.args, config) ``` - -Exit AutoGluon: - -```{.python .input} -ag.done() -``` diff --git a/docs/tutorials/image_classification/beginner.md b/docs/tutorials/image_classification/beginner.md index aeae712127c..5b8975aef3c 100644 --- a/docs/tutorials/image_classification/beginner.md +++ b/docs/tutorials/image_classification/beginner.md @@ -14,7 +14,8 @@ from autogluon import ImageClassification as task ## Create AutoGluon Dataset -Our image classification task is based on a subset of the [Shopee-IET dataset](https://www.kaggle.com/c/shopee-iet-machine-learning-competition/data) from Kaggle. Each image in this data depicts a clothing item and the corresponding label specifies its clothing category. +For demo purpose, we will use a subset of the [Shopee-IET dataset](https://www.kaggle.com/c/shopee-iet-machine-learning-competition/data) from Kaggle. +Each image in this data depicts a clothing item and the corresponding label specifies its clothing category. Our subset of the data contains the following possible labels: `BabyPants`, `BabyShirt`, `womencasualshoes`, `womenchiffontop`. We download the data subset and unzip it via the following commands: @@ -24,12 +25,27 @@ filename = ag.download('https://autogluon.s3.amazonaws.com/datasets/shopee-iet.z ag.unzip(filename) ``` -Once the dataset resides on our machine, we load it into an AutoGluon `Dataset` object: +Once the dataset resides on our machine, we load it into a `Dataset` object: ```{.python .input} dataset = task.Dataset('data/train') ``` +Load the test dataset: + +```{.python .input} +test_dataset = task.Dataset('data/test', train=False) +``` + +If you do not do not have a GPU, we will change the dataset to 'FashionMNIST' for demo purpose. +Otherwise, it will take forever to run: + +```{.python .input} +if ag.get_gpu_count() == 0: + dataset = task.Dataset(name='FashionMNIST') + test_dataset = task.Dataset(name='FashionMNIST', train=False) +``` + ## Use AutoGluon to Fit Models Now, we want to fit a classifier using AutoGluon: @@ -56,22 +72,19 @@ print('Top-1 val acc: %.3f' % classifier.results['best_reward']) Given an example image, we can easily use the final model to `predict` the label (and the conditional class-probability): ```{.python .input} -image = 'data/test/BabyShirt/BabyShirt_323.jpg' -ind, prob = classifier.predict(image) +# skip this if training FashionMNIST on CPU. +if ag.get_gpu_count() > 0: + image = 'data/test/BabyShirt/BabyShirt_323.jpg' + ind, prob = classifier.predict(image) -print('The input picture is classified as [%s], with probability %.2f.' % - (dataset.init().classes[ind.asscalar()], prob.asscalar())) + print('The input picture is classified as [%s], with probability %.2f.' % + (dataset.init().classes[ind.asscalar()], prob.asscalar())) ``` ## Evaluate on Test Dataset We now evaluate the classifier on a test dataset: -Load the test dataset: - -```{.python .input} -test_dataset = task.Dataset('data/test', train=False) -``` The validation and test top-1 accuracy are: @@ -79,9 +92,3 @@ The validation and test top-1 accuracy are: test_acc = classifier.evaluate(test_dataset) print('Top-1 test acc: %.3f' % test_acc) ``` - - -Finish and exit: -```{.python .input} -ag.done() -``` diff --git a/docs/tutorials/image_classification/hpo.md b/docs/tutorials/image_classification/hpo.md index fae8eac7f8a..ddf2a8ed58b 100644 --- a/docs/tutorials/image_classification/hpo.md +++ b/docs/tutorials/image_classification/hpo.md @@ -144,8 +144,3 @@ print('Top-1 test acc: %.3f' % test_acc) Please see comparison of different search algorithms and scheduling strategies :ref:`course_alg`. More options using fit is available at :class:`autogluon.task.ImageClassification`. - -Finish and exit: -```{.python .input} -ag.done() -``` diff --git a/docs/tutorials/image_classification/kaggle.md b/docs/tutorials/image_classification/kaggle.md index f2c5a0794af..adea577aaa8 100644 --- a/docs/tutorials/image_classification/kaggle.md +++ b/docs/tutorials/image_classification/kaggle.md @@ -42,25 +42,28 @@ and search using keyword `image classification` either under `Datasets` or `Comp For example, we find the `Shopee-IET Machine Learning Competition` under the `InClass` tab in `Competitions`. We then navigate to [Data](https://www.kaggle.com/c/shopee-iet-machine-learning-competition/data) to download the dataset using the Kaggle API. +Please make sure to click the button of "I Understand and Accept" before downloading the data. -An example shell script to download the dataset to `~/data/shopeeiet/` can be found here: [download_shopeeiet.sh](../static/download_shopeeiet.sh). +An example shell script to download the dataset to `./data/shopeeiet/` can be found here: [download_shopeeiet.sh](https://github.com/zhanghang1989/AutoGluonWebdata/blob/master/docs/tutorial/download_shopeeiet.sh?raw=True). After downloading this script to your machine, run it with: ```{.python .input} -# import os -# os.system('wget http://autogluon-hackathon.s3-website-us-west-2.amazonaws.com/static/download_shopeeiet.sh') -# os.system('sh download_shopeeiet.sh') +# import autogluon as ag +# ag.download('https://raw.githubusercontent.com/zhanghang1989/AutoGluonWebdata/master/docs/tutorial/download_shopeeiet.sh') +# !sh download_shopeeiet.sh ``` -Now we have the desired directory structure under `~/data/shopeeiet/`, which in this case looks as follows: +Now we have the desired directory structure under `./data/shopeeiet/train/`, which in this case looks as follows: ``` - shopeeiet - ├── BabyBibs - ├── BabyHat - ├── BabyPants - ├── ... + shopeeiet/train + ├── BabyBibs + ├── BabyHat + ├── BabyPants + ├── ... + shopeeiet/test + ├── ... ``` Here are some example images from this data: @@ -68,8 +71,7 @@ Here are some example images from this data: ![](../../img/shopeeiet_example.png) - -## Step 2: Split data into training/validation/test sets +## Step 2: Split data into training/validation sets A fundamental step in machine learning is to split the data into disjoint sets used for different purposes. @@ -84,126 +86,24 @@ each neural network requires the user to specify many hyperparameters (eg. learn Test Set: A separate set of images, possibly without available labels. These data are never used during any part of the model construction or learning process. If unlabeled, these may correspond to images whose labels we would like to predict. If labeled, these images may correspond to images we reserve for estimating the performance of our final model. - -### Dataset format after splitting - -The following directory format is used by AutoGluon's `image_classification` task: - -``` - data/ - ├── train/ - ├── class1/ - ├── class2/ - ├── class3/ - ├── ... - ├── val(optional)/ - ├── class1/ - ├── class2/ - ├── class3/ - ├── ... - └── test/ -``` - -Here, the `train` directory has the same format as the `data` directory described in Step 1. - -When there are no labels available for test images, the `test` directory simply contains a collection of image files. -Otherwise, `test` should have the same format as the `data` directory described in Step 1 (ie. same format as `train`) if we wish to evaluate the accuracy of our model on the test data using AutoGluon. - -We show an example below on how to convert data source obtained in Step 1 -to Training/Validation/Test split with the required format. -In this example, we provide a script to split the Kaggle data into the required format; -please click the download link of [prepare_shopeeiet.py](../static/prepare_shopeeiet.py). - ### Automatic training/validation split -Since AutoGluon provides the automatic Training/Validation split, we can skip the Validation split by running the command: - -```{.python .input} -# import os -# os.system('wget http://autogluon-hackathon.s3-website-us-west-2.amazonaws.com/static/prepare_shopeeiet.py') -# os.system('python prepare_shopeeiet.py --data ~/data/shopeeiet/ --split 0') -``` - -where `--split 0` would skip the validation split, therefore all the data in `data` directory would be used as `train` data, later on the AutoGluon `Dataset` would automatically split into Training (90% of the data) and Validation (10% of the data). - -The resulting data should be converted into the following directory structure: - -``` - shopeeiet - ├── train - ├── BabyBibs - ├── BabyHat - ├── BabyPants - ├── ... - └── test -``` - -Now you have a dataset ready used in AutoGluon. - -To tell AutoGluon where the training data is located, which means let AutoGluon conduct the Training/Validation split, use: +AutoGluon automatically does Training/Validation split: ```{.python .input} # from autogluon import ImageClassification as task -# dataset = task.Dataset('data/shopeeiet/train') +# dataset = task.Dataset('./data/shopeeiet/train') ``` AutoGluon will automatically infer how many classes there are based on the directory structure. By default, AutoGluon automatically constructs the training/validation set split: -- Training Set: 90% of images. -- Validation Set: 10% of images. +- Training Set: 80% of images. +- Validation Set: 20% of images. where the images that fall into the validation set are randomly chosen from the training data based on the class. -### Manually-specified training/validation split - -Instead, you can also split your labeled data manually into training and validation sets. -Manually splitting your data is a good choice when you want to exercise more control over the process -or if there are specific images that you're sure you want included in a certain part of your model training lifecycle. - -If we want to manually specify the Training/Validation split, we could construct by running the command: - -```{.python .input} -# import os -# os.system('wget http://autogluon-hackathon.s3-website-us-west-2.amazonaws.com/static/prepare_shopeeiet.py') -# os.system('python prepare_shopeeiet.py --data ~/data/shopeeiet/ --split 9') -``` - -where `--split 9` would sample 10% data from the `data` directory as Validation set, and the rest 90% data would be Training set. - -The resulting data should be looking as the following structure: - -``` - shopeeiet - ├── train - ├── BabyBibs - ├── BabyHat - ├── BabyPants - ├── ... - ├── val - ├── BabyBibs - ├── BabyHat - ├── BabyPants - ├── ... - └── test -``` - -Then tell AutoGluon where the training and validation data is, which means we disable AutoGluon's automatic Training/Validation split functionality, instead, we manually provide the Training/Validation split via: - -We have the processed dataset if you don't want to explore new Kaggle dataset, please simply download it and try the larger dataset. - -```{.python .input} -# import os -# os.system('wget http://autogluon-hackathon.s3-website-us-west-2.amazonaws.com/shopeeiet/data_shopeeiet.zip') -# os.system('unzip -o data_shopeeiet.zip -d ~/') -``` - -```{.python .input} -# from autogluon import ImageClassification as task -# dataset = task.Dataset('data/shopeeiet/train') -``` - -## Step 3: Use AutoGluon fit to generate a classification model (Optional) +## Step 3: Use AutoGluon fit to generate a classification model Now that we have a `Dataset` object, we can use AutoGluon's default configuration to obtain an image classification model. All you have to do is simply call the `fit` function. @@ -228,10 +128,11 @@ The top-1 accuracy of the best model on the validation set is: ### Using AutoGluon to generate predictions on test images We can ask our final model to generate predictions on the provided test images. -We first load the test data as a `Dataset` object and then call [predict](../api/autogluon.task.base.html#autogluon.task.base.BaseTask.predict): +We first load the test data as a `Dataset` object and then call `predict`: ```{.python .input} -# inds, probs = classifier.predict('data/shopeeiet/test') +# test_dataset = task.Dataset('./data/shopeeiet/test') +# inds, probs = classifier.predict(test_dataset) ``` `inds` above contains the indices of the predicted class for each test image, while `probs` contains the confidence in these predictions. @@ -244,25 +145,19 @@ Here are the results of AutoGluon's default `fit` and `predict` under different - The validation top-1 accuracy within 72h is 0.852, and ranks 9th place in Kaggle competition. -## Step 4: Submit test predictions to Kaggle (Optional) +## Step 4: Submit test predictions to Kaggle If you wish to upload the model's predictions to Kaggle, here is how to convert them into a format suitable for a submission into the Kaggle competition: ```{.python .input} # import autogluon as ag -# ag.utils.generate_csv(inds, '/home/ubuntu/data/shopeeiet/submission.csv') +# ag.utils.generate_csv(inds, './data/shopeeiet/submission.csv') ``` -will produce a submission file located at: `~/data/shopeeiet/submission.csv`. +will produce a submission file located at: `./data/shopeeiet/submission.csv`. To see an example submission, check out the file `sample submission.csv` at this link: [Data](https://www.kaggle.com/c/shopee-iet-machine-learning-competition/data). To make your own submission, click [Submission](https://www.kaggle.com/c/shopee-iet-machine-learning-competition/submit) and then follow the steps in the submission page (upload submission file, describe the submission, and click the `Make Submission` button). Let's see how your model fares in this competition! - - -Finish and exit: -```{.python .input} -# ag.done() -``` diff --git a/docs/tutorials/index.rst b/docs/tutorials/index.rst index 9959de9d909..424a12df603 100644 --- a/docs/tutorials/index.rst +++ b/docs/tutorials/index.rst @@ -101,6 +101,7 @@ Neural Architecture Search course/index image_classification/index + object_detection/index text_classification/index tabular_prediction/index customize/index diff --git a/docs/tutorials/nas/enas_mnist.md b/docs/tutorials/nas/enas_mnist.md index 27df4026f34..ef5cdba1c1b 100644 --- a/docs/tutorials/nas/enas_mnist.md +++ b/docs/tutorials/nas/enas_mnist.md @@ -144,13 +144,3 @@ The resulting architecture is: ```{.python .input} mynet.graph ``` - -## Defining a Complicated Network - -Can we define a more complicated network than just sequential? - - -Finish and exit: -```{.python .input} -ag.done() -``` diff --git a/docs/tutorials/nas/index.rst b/docs/tutorials/nas/index.rst index b1efc1a3076..ada93c85e65 100644 --- a/docs/tutorials/nas/index.rst +++ b/docs/tutorials/nas/index.rst @@ -4,6 +4,8 @@ Neural Architecture Search AutoGluon enables easy nerual architecture search, with API for fast prototyping and state-of-the-art built-in methods. +.. container:: cards + .. card:: :title: Reinforcement Learing :link: rl_searcher.html diff --git a/docs/tutorials/nas/rl_searcher.md b/docs/tutorials/nas/rl_searcher.md index 53cc636d2df..67c06d5962d 100644 --- a/docs/tutorials/nas/rl_searcher.md +++ b/docs/tutorials/nas/rl_searcher.md @@ -44,13 +44,13 @@ plt.show() ### Customize Train Function -We can simply define any function with a decorator `autogluon_register_args` whichs converts the function to +We can simply define any function with a decorator `@ag.args` whichs converts the function to AutoGluon searchable. The `reporter` is used to communicate with AutoGluon search algorithms. ```{.python .input} import autogluon as ag -@ag.autogluon_register_args( +@ag.args( x=ag.space.Categorical(*list(range(100))), y=ag.space.Categorical(*list(range(100))), ) diff --git a/docs/tutorials/object_detection/beginner.md b/docs/tutorials/object_detection/beginner.md index 9335aeac9dc..98be02214c4 100644 --- a/docs/tutorials/object_detection/beginner.md +++ b/docs/tutorials/object_detection/beginner.md @@ -52,10 +52,3 @@ The mAP is not bad after just 30 epochs. Let's see one visualization result. We image_path = './tiny_motorbike/VOC2007/JPEGImages/000467.jpg' ind, prob, loc = detector.predict(image_path) ``` - -We have tried models with various settings. Finally, showdown the whole processs via following command. - -```{.python .input} -ag.done() -``` - diff --git a/docs/tutorials/tabular_prediction/tabular-indepth.md b/docs/tutorials/tabular_prediction/tabular-indepth.md index 0cace8cfb97..a13f8a9d52c 100644 --- a/docs/tutorials/tabular_prediction/tabular-indepth.md +++ b/docs/tutorials/tabular_prediction/tabular-indepth.md @@ -79,9 +79,3 @@ y_pred = predictor.predict(datapoint) ``` In the above example, the predictive performance may be poor because we specified very little training to ensure quick runtimes. You can call `fit()` multiple times playing with the above settings to better understand how these choices affect things. - -Finally, don't forget to shutdown AutoGluon's remote workers: - -```{.python .input} -ag.done() -``` diff --git a/docs/tutorials/tabular_prediction/tabular-quickstart.md b/docs/tutorials/tabular_prediction/tabular-quickstart.md index c3b2a4b7cce..e4547645def 100644 --- a/docs/tutorials/tabular_prediction/tabular-quickstart.md +++ b/docs/tutorials/tabular_prediction/tabular-quickstart.md @@ -108,8 +108,3 @@ predictor_educ = task.fit(train_data=train_data, output_directory="agModels-pred Finally, we need to shutdown AutoGluon's remote workers which `fit()` uses to train multiple models simultaneously in multi-thread / distributed settings. **Important:** you must always execute `ag.done()` after you are done with using AutoGluon in a Python session or Jupyter notebook, as it may not work properly in a new session/notebook otherwise. After executing `ag.done()` you cannot call `fit()` anymore without starting a new Python session or notebook. -If you face strange errors at any point in these tutorials, we recommend calling `ag.done()` and then restarting the notebook kernel. - -```{.python .input} -ag.done() -``` diff --git a/docs/tutorials/text_classification/beginner.md b/docs/tutorials/text_classification/beginner.md index a111b713084..dcca05760ed 100644 --- a/docs/tutorials/text_classification/beginner.md +++ b/docs/tutorials/text_classification/beginner.md @@ -72,9 +72,3 @@ print(predictor.results['best_config']) ``` This configuration is used to generate the above results. - -At the end, please remember to safely exit to release all the resources: - -```{.python .input} -ag.done() -``` diff --git a/docs/tutorials/text_classification/index.rst b/docs/tutorials/text_classification/index.rst index 037f0db6921..07053f42c62 100644 --- a/docs/tutorials/text_classification/index.rst +++ b/docs/tutorials/text_classification/index.rst @@ -15,4 +15,4 @@ AutoGluon will automatically train many models under thousands of different hype :maxdepth: 1 :hidden: - beginner + beginner diff --git a/docs/tutorials/torch/hpo.md b/docs/tutorials/torch/hpo.md index 179b7211e50..385397a29ff 100644 --- a/docs/tutorials/torch/hpo.md +++ b/docs/tutorials/torch/hpo.md @@ -36,12 +36,12 @@ testset = torchvision.datasets.MNIST(root='./data', train=False, download=True, ### Main Training Loop -The following `train_cifar` function is a normal training code a user would write for +The following `train_mnist` function represents normal training code a user would write for training on MNIST dataset. Python users typically use an argparser for conveniently changing default values. The only extra argument you need to add to your existing python function is a reporter object that is used to store performance achieved under different hyperparameter settings ```{.python .input} -def train_cifar(args, reporter): +def train_mnist(args, reporter): # get varibles from args lr = args.lr wd = args.wd @@ -140,7 +140,7 @@ class Net(nn.Module): ### Convert the Training Function to Be Searchable -We can simply add a decorator :func:`autogluon.args` to convert the `train_cifar` function argument values to be tuned by AutoGluon's hyperparameter optimizer. In the example below, we specify that the lr argument is a real-value that should be searched on a log-scale in the range 0.01 - 0.2. Before passing lr to your train function, AutoGluon will always select an actual floating point value to assign to lr and thus you do not need to make any special modifications to your existing code to accomadate the hyperparameter search. +We can simply add a decorator :func:`autogluon.args` to convert the `train_mnist` function argument values to be tuned by AutoGluon's hyperparameter optimizer. In the example below, we specify that the lr argument is a real-value that should be searched on a log-scale in the range 0.01 - 0.2. Before passing lr to your train function, AutoGluon will always select an actual floating point value to assign to lr and thus you do not need to make any special modifications to your existing code to accomadate the hyperparameter search. ```{.python .input} @ag.args( @@ -149,8 +149,8 @@ We can simply add a decorator :func:`autogluon.args` to convert the `train_cifar net = Net(), epochs=5, ) -def ag_train_cifar(args, reporter): - return train_cifar(args, reporter) +def ag_train_mnist(args, reporter): + return train_mnist(args, reporter) ``` @@ -158,7 +158,7 @@ def ag_train_cifar(args, reporter): ### Create the Scheduler and Launch the Experiment ```{.python .input} -myscheduler = ag.scheduler.FIFOScheduler(ag_train_cifar, +myscheduler = ag.scheduler.FIFOScheduler(ag_train_mnist, resource={'num_cpus': 4, 'num_gpus': 1}, num_trials=2, time_attr='epoch', diff --git a/setup.py b/setup.py index 0c07f40e466..0b24cdd436d 100644 --- a/setup.py +++ b/setup.py @@ -42,32 +42,29 @@ def run(self): except(IOError, ImportError): long_description = open('README.md').read() +MIN_PYTHON_VERSION = '>=3.6.*' + requirements = [ - 'numpy', - 'scipy', + 'numpy>=1.16.0', + 'scipy>=1.3.3', + 'pynvml>=8.0.0' 'cython', 'tornado', 'requests', 'matplotlib', - 'mxboard', 'tqdm>=4.38.0', - 'paramiko==2.5.0', - 'distributed==2.6.0', - 'ConfigSpace==0.4.10', - 'nose', - 'gluoncv', + 'paramiko>=2.5.0', + 'distributed>=2.6.0', + 'ConfigSpace<=0.4.10', + 'gluoncv>=0.5.0', 'gluonnlp==0.8.1', 'graphviz', 'scikit-optimize', - 'botocore==1.12.253', + 'catboost', 'boto3==1.9.187', - 'fastparquet==0.3.1', - 'joblib==0.13.2', 'lightgbm==2.3.0', 'pandas==0.24.2', 'psutil', - 'pyarrow==0.15.0', - 's3fs==0.3.1', 'scikit-learn==0.21.2', ] @@ -86,6 +83,7 @@ def run(self): zip_safe=True, include_package_data=True, install_requires=requirements, + python_requires=MIN_PYTHON_VERSION, package_data={'autogluon': [ 'LICENSE', ]},