From f2e358b4b7dea0932a17e6a7ba8e592b7d231250 Mon Sep 17 00:00:00 2001 From: Yancey1989 Date: Wed, 13 Mar 2019 14:20:09 +0800 Subject: [PATCH] add readme --- .../fast_resnet/README.md | 28 + .../image_classification/fast_resnet/env.py | 50 - .../fast_resnet/reader.py | 178 - .../fast_resnet/requirements.txt | 1 + .../fast_resnet/src/acc_curve.png | Bin 0 -> 16315 bytes .../fast_resnet/tools/resize.py | 47 + .../fast_resnet/tools/valprep.sh | 51000 ++++++++++++++++ .../image_classification/fast_resnet/train.py | 1 - 8 files changed, 51076 insertions(+), 229 deletions(-) create mode 100644 fluid/PaddleCV/image_classification/fast_resnet/README.md delete mode 100644 fluid/PaddleCV/image_classification/fast_resnet/env.py delete mode 100644 fluid/PaddleCV/image_classification/fast_resnet/reader.py create mode 100644 fluid/PaddleCV/image_classification/fast_resnet/src/acc_curve.png create mode 100644 fluid/PaddleCV/image_classification/fast_resnet/tools/resize.py create mode 100644 fluid/PaddleCV/image_classification/fast_resnet/tools/valprep.sh diff --git a/fluid/PaddleCV/image_classification/fast_resnet/README.md b/fluid/PaddleCV/image_classification/fast_resnet/README.md new file mode 100644 index 00000000..0cc90f31 --- /dev/null +++ b/fluid/PaddleCV/image_classification/fast_resnet/README.md @@ -0,0 +1,28 @@ +# PaddlePaddle Fast ResNet + +PaddlePaddle Fast ResNet can train ImageNet with fewer epochs. We implemented the it according to the blog +[Now anyone can train Imagenet in 18 minutes](https://www.fast.ai/2018/08/10/fastai-diu-imagenet/) which published on the [fast.ai] website. +PaddlePaddle Fast ResNet using the dynmiac batch size, dynamic image size, rectangular images validation and etc... so that the FastResNet can achieve the baseline +(acc1: 75%, acc5: 93%) by 27 epochs on 8 GPUs. + +## Experiment + +1. Preparing the training data, resize the images to 160 and 352 by `resize.py`, the prepared data folder is as followed: + ``` text + `-ImageNet + |-train + |-validation + |-160 + |-train + `-validation + `-352 + |-train + `-validation + ``` +1. Install the requirements by `pip install -r requirement.txt`. +1. Launch the training job: `python train.py --data_dir /data/imagenet` +1. Learning curve, we launch the training job on V100 GPU card: +

+
+

+ diff --git a/fluid/PaddleCV/image_classification/fast_resnet/env.py b/fluid/PaddleCV/image_classification/fast_resnet/env.py deleted file mode 100644 index 54242022..00000000 --- a/fluid/PaddleCV/image_classification/fast_resnet/env.py +++ /dev/null @@ -1,50 +0,0 @@ -import os - - -def dist_env(): - """ - Return a dict of all variable that distributed training may use. - NOTE: you may rewrite this function to suit your cluster environments. - """ - trainer_id = int(os.getenv("PADDLE_TRAINER_ID", "0")) - num_trainers = 1 - training_role = os.getenv("TRAINING_ROLE", "TRAINER") - assert(training_role == "PSERVER" or training_role == "TRAINER") - - # - PADDLE_TRAINER_ENDPOINTS means nccl2 mode. - # - PADDLE_PSERVER_ENDPOINTS means pserver mode. - # - PADDLE_CURRENT_ENDPOINT means current process endpoint. - worker_endpoints = [] - port = os.getenv("PADDLE_PORT", "8701") - if os.getenv("PADDLE_TRAINER_ENDPOINTS"): - trainer_endpoints = os.getenv("PADDLE_TRAINER_ENDPOINTS") - else:# for paddlecloud - worker_ips = os.getenv("PADDLE_TRAINERS", "") - for ip in worker_ips.split(","): - worker_endpoints.append(':'.join([ip, port])) - trainer_endpoints = ",".join(worker_endpoints) - - pserver_ips = os.getenv("PADDLE_PSERVERS", "") - eplist = [] - for ip in pserver_ips.split(","): - eplist.append(':'.join([ip, port])) - pserver_endpoints = ",".join(eplist) - - if os.getenv("PADDLE_CURRENT_ENDPOINT"): - current_endpoint = os.getenv("PADDLE_CURRENT_ENDPOINT") - else:# for paddlecloud - current_endpoint = os.getenv("POD_IP", "") + ":" + port - if trainer_endpoints: - trainer_endpoints = trainer_endpoints.split(",") - num_trainers = len(trainer_endpoints) - elif pserver_endpoints: - num_trainers = int(os.getenv("PADDLE_TRAINERS_NUM", "1")) - - return { - "trainer_id": trainer_id, - "num_trainers": num_trainers, - "current_endpoint": current_endpoint, - "training_role": training_role, - "pserver_endpoints": pserver_endpoints, - "trainer_endpoints": trainer_endpoints - } diff --git a/fluid/PaddleCV/image_classification/fast_resnet/reader.py b/fluid/PaddleCV/image_classification/fast_resnet/reader.py deleted file mode 100644 index 1f29d7bf..00000000 --- a/fluid/PaddleCV/image_classification/fast_resnet/reader.py +++ /dev/null @@ -1,178 +0,0 @@ -import os - -import numpy as np -import math -import random -import torchvision -import torchvision.transforms as transforms -import torchvision.datasets as datasets - -import pickle -from tqdm import tqdm -import time -import multiprocessing - -TRAINER_NUMS = int(os.getenv("PADDLE_TRAINER_NUM", "1")) -TRAINER_ID = int(os.getenv("PADDLE_TRAINER_ID", "0")) -epoch = 0 - -class ImageFolder(object): - def __init__(self, root, transforms): - - pass - - -FINISH_EVENT = "FINISH_EVENT" -class PaddleDataLoader(object): - def __init__(self, torch_dataset, indices=None, concurrent=16, queue_size=3072): - self.torch_dataset = torch_dataset - self.data_queue = multiprocessing.Queue(queue_size) - self.indices = indices - self.concurrent = concurrent - - def _worker_loop(self, dataset, worker_indices, worker_id): - cnt = 0 - for idx in worker_indices: - cnt += 1 - img, label = self.torch_dataset[idx] - img = np.array(img).astype('uint8').transpose((2, 0, 1)) - self.data_queue.put((img, label)) - print("worker: [%d] read [%d] samples. " % (worker_id, cnt)) - self.data_queue.put(FINISH_EVENT) - - def reader(self): - def _reader_creator(): - worker_processes = [] - total_img = len(self.torch_dataset) - print("total image: ", total_img) - if self.indices is None: - self.indices = [i for i in xrange(total_img)] - random.seed(time.time()) - random.shuffle(self.indices) - print("shuffle indices: %s ..." % self.indices[:10]) - - imgs_per_worker = int(math.ceil(total_img / self.concurrent)) - for i in xrange(self.concurrent): - start = i * imgs_per_worker - end = (i + 1) * imgs_per_worker if i != self.concurrent - 1 else None - sliced_indices = self.indices[start:end] - w = multiprocessing.Process( - target=self._worker_loop, - args=(self.torch_dataset, sliced_indices, i) - ) - w.daemon = True - w.start() - worker_processes.append(w) - finish_workers = 0 - worker_cnt = len(worker_processes) - while finish_workers < worker_cnt: - sample = self.data_queue.get() - if sample == FINISH_EVENT: - finish_workers += 1 - else: - yield sample - - return _reader_creator - -def train(traindir, sz, min_scale=0.08): - train_tfms = [ - transforms.RandomResizedCrop(sz, scale=(min_scale, 1.0)), - transforms.RandomHorizontalFlip() - ] - train_dataset = datasets.ImageFolder(traindir, transforms.Compose(train_tfms)) - return PaddleDataLoader(train_dataset).reader() - -def test(valdir, bs, sz, rect_val=False): - if rect_val: - idx_ar_sorted = sort_ar(valdir) - idx_sorted, _ = zip(*idx_ar_sorted) - idx2ar = map_idx2ar(idx_ar_sorted, bs) - - ar_tfms = [transforms.Resize(int(sz* 1.14)), CropArTfm(idx2ar, sz)] - val_dataset = ValDataset(valdir, transform=ar_tfms) - return PaddleDataLoader(val_dataset, concurrent=1, indices=idx_sorted).reader() - - val_tfms = [transforms.Resize(int(sz* 1.14)), transforms.CenterCrop(sz)] - val_dataset = datasets.ImageFolder(valdir, transforms.Compose(val_tfms)) - - return PaddleDataLoader(val_dataset).reader() - - -class ValDataset(datasets.ImageFolder): - def __init__(self, root, transform=None, target_transform=None): - super(ValDataset, self).__init__(root, transform, target_transform) - - def __getitem__(self, index): - path, target = self.imgs[index] - sample = self.loader(path) - if self.transform is not None: - for tfm in self.transform: - if isinstance(tfm, CropArTfm): - sample = tfm(sample, index) - else: - sample = tfm(sample) - if self.target_transform is not None: - target = self.target_transform(target) - - return sample, target - -class CropArTfm(object): - def __init__(self, idx2ar, target_size): - self.idx2ar, self.target_size = idx2ar, target_size - - def __call__(self, img, idx): - target_ar = self.idx2ar[idx] - if target_ar < 1: - w = int(self.target_size / target_ar) - size = (w // 8 * 8, self.target_size) - else: - h = int(self.target_size * target_ar) - size = (self.target_size, h // 8 * 8) - return transforms.functional.center_crop(img, size) - - -def sort_ar(valdir): - idx2ar_file = valdir + '/../sorted_idxar.p' - if os.path.isfile(idx2ar_file): - return pickle.load(open(idx2ar_file, 'rb')) - print('Creating AR indexes. Please be patient this may take a couple minutes...') - val_dataset = datasets.ImageFolder(valdir) # AS: TODO: use Image.open instead of looping through dataset - sizes = [img[0].size for img in tqdm(val_dataset, total=len(val_dataset))] - idx_ar = [(i, round(s[0] * 1.0/ s[1], 5)) for i, s in enumerate(sizes)] - sorted_idxar = sorted(idx_ar, key=lambda x: x[1]) - pickle.dump(sorted_idxar, open(idx2ar_file, 'wb')) - print('Done') - return sorted_idxar - -def chunks(l, n): - n = max(1, n) - return (l[i:i + n] for i in range(0, len(l), n)) - - -def map_idx2ar(idx_ar_sorted, batch_size): - ar_chunks = list(chunks(idx_ar_sorted, batch_size)) - idx2ar = {} - for chunk in ar_chunks: - idxs, ars = list(zip(*chunk)) - mean = round(np.mean(ars), 5) - for idx in idxs: - idx2ar[idx] = mean - return idx2ar - -if __name__ == "__main__": - #ds, sampler = create_validation_set("/data/imagenet/validation", 128, 288, True, True) - #for item in sampler: - # for idx in item: - # ds[idx] - - import time - test_reader = test(valdir="/data/imagenet/validation", bs=50, sz=288, rect_val=True) - start_ts = time.time() - for idx, data in enumerate(test_reader()): - print(idx, data[0].shape, data[1]) - if idx == 10: - break - if (idx + 1) % 1000 == 0: - cost = (time.time() - start_ts) - print("%d samples per second" % (1000 / cost)) - start_ts = time.time() \ No newline at end of file diff --git a/fluid/PaddleCV/image_classification/fast_resnet/requirements.txt b/fluid/PaddleCV/image_classification/fast_resnet/requirements.txt index 0e74a896..5e13381c 100644 --- a/fluid/PaddleCV/image_classification/fast_resnet/requirements.txt +++ b/fluid/PaddleCV/image_classification/fast_resnet/requirements.txt @@ -1,2 +1,3 @@ +torch==0.4.1 torchvision tqdm diff --git a/fluid/PaddleCV/image_classification/fast_resnet/src/acc_curve.png b/fluid/PaddleCV/image_classification/fast_resnet/src/acc_curve.png new file mode 100644 index 0000000000000000000000000000000000000000..4b9a2fbe3a63a8597f315da0f295bcd5a747055d GIT binary patch literal 16315 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