From 3200261062d183da798663472b9db2a2bd603f5f Mon Sep 17 00:00:00 2001 From: burning Date: Wed, 6 May 2020 21:54:16 -0500 Subject: [PATCH] first commit --- .gitignore | 7 + .idea/.gitignore | 8 + .idea/AutoSpeech.iml | 10 + .../inspectionProfiles/profiles_settings.xml | 6 + .idea/libraries/R_User_Library.xml | 6 + .idea/misc.xml | 7 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + README.md | 98 ++++- architect.py | 22 ++ config/__init__.py | 2 + config/default.py | 69 ++++ data_objects/DeepSpeakerDataset.py | 74 ++++ data_objects/VoxcelebTestset.py | 72 ++++ data_objects/__init__.py | 0 data_objects/audio.py | 47 +++ data_objects/compute_mean_std.py | 33 ++ data_objects/params_data.py | 18 + data_objects/partition_voxceleb.py | 49 +++ data_objects/preprocess.py | 151 ++++++++ data_objects/speaker.py | 44 +++ data_objects/transforms.py | 36 ++ data_objects/utterance.py | 25 ++ data_preprocess.py | 33 ++ evaluate_identification.py | 110 ++++++ evaluate_verification.py | 118 ++++++ exps/baseline/resnet18.yaml | 28 ++ exps/baseline/resnet34.yaml | 28 ++ exps/scratch/scratch.yaml | 30 ++ exps/search.yaml | 36 ++ figures/searched_arch_normal.png | Bin 0 -> 74628 bytes figures/searched_arch_reduce.png | Bin 0 -> 70302 bytes functions.py | 228 ++++++++++++ loss.py | 55 +++ models/__init__.py | 14 + models/model.py | 122 +++++++ models/model_search.py | 220 +++++++++++ models/resnet.py | 344 ++++++++++++++++++ operations.py | 178 +++++++++ requirements.txt | 32 ++ search.py | 206 +++++++++++ spaces.py | 94 +++++ train.py | 180 +++++++++ train_baseline.py | 159 ++++++++ utils.py | 243 +++++++++++++ 45 files changed, 3255 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/AutoSpeech.iml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/libraries/R_User_Library.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 architect.py create mode 100644 config/__init__.py create mode 100644 config/default.py create mode 100644 data_objects/DeepSpeakerDataset.py create mode 100644 data_objects/VoxcelebTestset.py create mode 100644 data_objects/__init__.py create mode 100644 data_objects/audio.py create mode 100644 data_objects/compute_mean_std.py create mode 100644 data_objects/params_data.py create mode 100644 data_objects/partition_voxceleb.py create mode 100644 data_objects/preprocess.py create mode 100644 data_objects/speaker.py create mode 100644 data_objects/transforms.py create mode 100644 data_objects/utterance.py create mode 100644 data_preprocess.py create mode 100644 evaluate_identification.py create mode 100644 evaluate_verification.py create mode 100644 exps/baseline/resnet18.yaml create mode 100644 exps/baseline/resnet34.yaml create mode 100644 exps/scratch/scratch.yaml create mode 100644 exps/search.yaml create mode 100644 figures/searched_arch_normal.png create mode 100644 figures/searched_arch_reduce.png create mode 100644 functions.py create mode 100644 loss.py create mode 100644 models/__init__.py create mode 100644 models/model.py create mode 100644 models/model_search.py create mode 100644 models/resnet.py create mode 100644 operations.py create mode 100644 requirements.txt create mode 100644 search.py create mode 100644 spaces.py create mode 100644 train.py create mode 100644 train_baseline.py create mode 100644 utils.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..049cba7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +logs/** +logs_search/** +models/__pycache__/** +__pycache__/** +config/__pycache__/** +data_objects/__pycache__/** +logs_scratch/** diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..73f69e0 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/AutoSpeech.iml b/.idea/AutoSpeech.iml new file mode 100644 index 0000000..0d5ca6b --- /dev/null +++ b/.idea/AutoSpeech.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/libraries/R_User_Library.xml b/.idea/libraries/R_User_Library.xml new file mode 100644 index 0000000..71f5ff7 --- /dev/null +++ b/.idea/libraries/R_User_Library.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..78fbbaf --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..e9bfd0f --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index d2dc6fd..2661f6a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,98 @@ # AutoSpeaker: Neural Architecture Search for Speaker Recognition -Source code is coming soon. + +Code for this paper [AutoSpeaker: Neural Architecture Search for Speaker Recognition](TBD) + +Shaojin Ding*, Tianlong Chen*, Xinyu Gong, Weiwei Zha, Zhangyang Wang + +## Overview +Speaker recognition systems based on Convolutional Neural Networks (CNNs) are often built with off-the-shelf backbones such as VGG-Net or ResNet. However, these backbones were originally proposed for image classification, and therefore may not be naturally fit for speaker recognition. Due to the prohibitive complexity of manually exploring the design space, we propose the first neural architecture search approach approach for the speaker recognition tasks, named as AutoSpeech. Our algorithm first identifies the optimal operation combination in a neural cell and then derives a CNN model by stacking the neural cell for multiple times. The final speaker recognition model can be obtained by training the derived CNN model through the standard scheme. To evaluate the proposed approach, we conduct experiments on both speaker identification and speaker verification tasks using the VoxCeleb1 dataset. Results demonstrate that the derived CNN architectures from the proposed approach significantly outperform current speaker recognition systems based on VGG-M, ResNet-18, and ResNet-34 back-bones, while enjoying lower model complexity. + +## + +## Quick start +### Requirements +* Python 3.7 + +* Pytorch>=1.0: `pip install torch torchvision` + +* Other dependencies: `pip install -r requirements` + +### Dataset +[VoxCeleb1](http://www.robots.ox.ac.uk/~vgg/data/voxceleb/vox1.html): You will need `DevA-DevD` and `Test` parts. Additionally, you will need original files: `vox1_meta.csv`, `iden_split.txt`, and `veri_test.txt` from official website. + +The data should be organized as: +* VoxCeleb1 + * wav + * vox1_meta.csv + * iden_split.txt + * veri_test.txt + +### Running the code +* data preprocess: + + `python data_preprocess.py /path/to/VoxCeleb1` + +* Training and evaluating ResNet-18, ResNet-34 baselines: + + `python train_baseline.py --cfg exps/baseline/resnet18.yaml` + + `python train_baseline.py --cfg exps/baseline/resnet34.yaml` + + You need to modify the `DATA_DIR` field in `.yaml` file. + +* Architecture search: + + `python search.py --cfg exps/search.yaml` + + You need to modify the `DATA_DIR` field in `.yaml` file. + +* Training from scratch: + + `python train.py --cfg exps/scratch/scratch.yaml --text_arch GENOTYPE` + + You need to modify the `DATA_DIR` field in `.yaml` file. + + `GENOTYPE` is the search architecture object. For example, the `GENOTYPE` of the architecture report in the paper is: + + `"Genotype(normal=[('dil_conv_5x5', 1), ('dil_conv_3x3', 0), ('dil_conv_5x5', 0), ('sep_conv_3x3', 1), ('sep_conv_3x3', 1), ('sep_conv_3x3', 2), ('dil_conv_3x3', 2), ('max_pool_3x3', 1)], normal_concat=range(2, 6), reduce=[('max_pool_3x3', 1), ('max_pool_3x3', 0), ('dil_conv_5x5', 2), ('max_pool_3x3', 1), ('dil_conv_5x5', 3), ('dil_conv_3x3', 2), ('dil_conv_5x5', 4), ('dil_conv_5x5', 2)], reduce_concat=range(2, 6))"` + +* Evaluation: + + * Identification + + `python evaluate_identification.py --cfg exps/scratch/scratch.yaml --load_path /path/to/the/trained/model` + + * Verification + + `python evaluate_verification.py --cfg exps/scratch/scratch.yaml --load_path /path/to/the/trained/model` + + +### Visualization + +normal cell | reduction cell +

+progress_convolutional_normal +progress_convolutional_reduce +

+ +## Results + +Our proposed approach outperforms speaker recognition systems based on VGG-M, ResNet-18, and ResNet-34 backbones. The detailed comparison can be found in our paper. + +| Method | Top-1 | EER | Parameters | +| :------------: | :---: | :---: | :---: | +| VGG-M | 80.50 | 10.2 | 67M | +| ResNet-18 | 79.48 | 8.17 | 12M | +| ResNet-34 | 81.34 | 4.64 | 22M | +| Proposed | **87.66** | **1.45** | **18M** | + + +## Citation + +If you use this code for your research, please cite our paper. + +``` +​``` +TBD +​``` +``` \ No newline at end of file diff --git a/architect.py b/architect.py new file mode 100644 index 0000000..69e1f0a --- /dev/null +++ b/architect.py @@ -0,0 +1,22 @@ +import torch + + +def _concat(xs): + return torch.cat([x.view(-1) for x in xs]) + + +class Architect(object): + + def __init__(self, model, cfg): + self.model = model + self.optimizer = torch.optim.Adam(self.model.arch_parameters(), + lr=cfg.TRAIN.ARCH_LR, betas=(0.5, 0.999), weight_decay=cfg.TRAIN.ARCH_WD) + + def step(self, input_valid, target_valid): + self.optimizer.zero_grad() + self._backward_step(input_valid, target_valid) + self.optimizer.step() + + def _backward_step(self, input_valid, target_valid): + loss = self.model._loss(input_valid, target_valid) + loss.backward() \ No newline at end of file diff --git a/config/__init__.py b/config/__init__.py new file mode 100644 index 0000000..c845930 --- /dev/null +++ b/config/__init__.py @@ -0,0 +1,2 @@ +from .default import _C as cfg +from .default import update_config diff --git a/config/default.py b/config/default.py new file mode 100644 index 0000000..f15fb57 --- /dev/null +++ b/config/default.py @@ -0,0 +1,69 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +from yacs.config import CfgNode as CN + + +_C = CN() + +_C.PRINT_FREQ = 20 +_C.VAL_FREQ = 20 + +# Cudnn related params +_C.CUDNN = CN() +_C.CUDNN.BENCHMARK = True +_C.CUDNN.DETERMINISTIC = False +_C.CUDNN.ENABLED = True + +# seed +_C.SEED = 3 + +# common params for NETWORK +_C.MODEL = CN() +_C.MODEL.NAME = 'foo_net' +_C.MODEL.NUM_CLASSES = 500 +_C.MODEL.LAYERS = 8 +_C.MODEL.INIT_CHANNELS = 16 +_C.MODEL.DROP_PATH_PROB = 0.2 +_C.MODEL.PRETRAINED = False + +# DATASET related params +_C.DATASET = CN() +_C.DATASET.DATA_DIR = '' +_C.DATASET.DATASET = '' +_C.DATASET.TEST_DATA_DIR = '' +_C.DATASET.TEST_DATASET = '' +_C.DATASET.NUM_WORKERS = 0 +_C.DATASET.PARTIAL_N_FRAMES = 32 +_C.DATASET.FEATURE_DIM = 40 + + +# train +_C.TRAIN = CN() + +_C.TRAIN.BATCH_SIZE = 32 +_C.TRAIN.LR = 0.1 +_C.TRAIN.LR_MIN = 0.001 +_C.TRAIN.WD = 0.0 +_C.TRAIN.BETA1 = 0.9 +_C.TRAIN.BETA2 = 0.999 + +_C.TRAIN.ARCH_LR = 0.1 +_C.TRAIN.ARCH_WD = 0.0 +_C.TRAIN.ARCH_BETA1 = 0.9 +_C.TRAIN.ARCH_BETA2 = 0.999 + +_C.TRAIN.DROPPATH_PROB = 0.2 + +_C.TRAIN.BEGIN_EPOCH = 0 +_C.TRAIN.END_EPOCH = 140 + + + +def update_config(cfg, args): + cfg.defrost() + cfg.merge_from_file(args.cfg) + cfg.merge_from_list(args.opts) + + cfg.freeze() diff --git a/data_objects/DeepSpeakerDataset.py b/data_objects/DeepSpeakerDataset.py new file mode 100644 index 0000000..0c96a78 --- /dev/null +++ b/data_objects/DeepSpeakerDataset.py @@ -0,0 +1,74 @@ +from __future__ import print_function + + +import numpy as np +import torch.utils.data as data +from data_objects.speaker import Speaker +from torchvision import transforms as T +from data_objects.transforms import Normalize, TimeReverse, generate_test_sequence + + +def find_classes(speakers): + classes = list(set([speaker.name for speaker in speakers])) + classes.sort() + class_to_idx = {classes[i]: i for i in range(len(classes))} + return classes, class_to_idx + + +class DeepSpeakerDataset(data.Dataset): + + def __init__(self, data_dir, partial_n_frames, partition=None, is_test=False): + super(DeepSpeakerDataset, self).__init__() + self.data_dir = data_dir + self.root = data_dir.joinpath('feature') + self.partition = partition + self.partial_n_frames = partial_n_frames + self.is_test = is_test + + speaker_dirs = [f for f in self.root.glob("*") if f.is_dir()] + if len(speaker_dirs) == 0: + raise Exception("No speakers found. Make sure you are pointing to the directory " + "containing all preprocessed speaker directories.") + self.speakers = [Speaker(speaker_dir, self.partition) for speaker_dir in speaker_dirs] + + classes, class_to_idx = find_classes(self.speakers) + sources = [] + for speaker in self.speakers: + sources.extend(speaker.sources) + self.features = [] + for source in sources: + item = (source[0].joinpath(source[1]), class_to_idx[source[2]]) + self.features.append(item) + mean = np.load(self.data_dir.joinpath('mean.npy')) + std = np.load(self.data_dir.joinpath('std.npy')) + self.transform = T.Compose([ + Normalize(mean, std), + TimeReverse(), + ]) + + def load_feature(self, feature_path, speaker_id): + feature = np.load(feature_path) + if self.is_test: + test_sequence = generate_test_sequence(feature, self.partial_n_frames) + return test_sequence, speaker_id + else: + if feature.shape[0] <= self.partial_n_frames: + start = 0 + while feature.shape[0] < self.partial_n_frames: + feature = np.repeat(feature, 2, axis=0) + else: + start = np.random.randint(0, feature.shape[0] - self.partial_n_frames) + end = start + self.partial_n_frames + return feature[start:end], speaker_id + + def __getitem__(self, index): + feature_path, speaker_id = self.features[index] + feature, speaker_id = self.load_feature(feature_path, speaker_id) + + if self.transform is not None: + feature = self.transform(feature) + return feature, speaker_id + + def __len__(self): + return len(self.features) + diff --git a/data_objects/VoxcelebTestset.py b/data_objects/VoxcelebTestset.py new file mode 100644 index 0000000..230bd5a --- /dev/null +++ b/data_objects/VoxcelebTestset.py @@ -0,0 +1,72 @@ +import os +import torch.utils.data as data +import numpy as np +from torchvision import transforms as T +from data_objects.transforms import Normalize, generate_test_sequence + + +def get_test_paths(pairs_path, db_dir): + def convert_folder_name(path): + basename = os.path.splitext(path)[0] + items = basename.split('/') + speaker_dir = 'wav_{}'.format(items[0]) + fname = '{}_{}.npy'.format(items[1], items[2]) + p = os.path.join(speaker_dir, fname) + return p + + pairs = [line.strip().split() for line in open(pairs_path, 'r').readlines()] + nrof_skipped_pairs = 0 + path_list = [] + issame_list = [] + + for pair in pairs: + if pair[0] == '1': + issame = True + else: + issame = False + + path0 = db_dir.joinpath(convert_folder_name(pair[1])) + path1 = db_dir.joinpath(convert_folder_name(pair[2])) + + if os.path.exists(path0) and os.path.exists(path1): # Only add the pair if both paths exist + path_list.append((path0,path1,issame)) + issame_list.append(issame) + else: + nrof_skipped_pairs += 1 + if nrof_skipped_pairs>0: + print('Skipped %d image pairs' % nrof_skipped_pairs) + + return path_list + +class VoxcelebTestset(data.Dataset): + def __init__(self, data_dir, partial_n_frames): + super(VoxcelebTestset, self).__init__() + self.data_dir = data_dir + self.root = data_dir.joinpath('feature') + self.test_pair_txt_fpath = data_dir.joinpath('veri_test.txt') + self.test_pairs = get_test_paths(self.test_pair_txt_fpath, self.root) + self.partial_n_frames = partial_n_frames + mean = np.load(self.data_dir.joinpath('mean.npy')) + std = np.load(self.data_dir.joinpath('std.npy')) + self.transform = T.Compose([ + Normalize(mean, std) + ]) + + def load_feature(self, feature_path): + feature = np.load(feature_path) + test_sequence = generate_test_sequence(feature, self.partial_n_frames) + return test_sequence + + def __getitem__(self, index): + (path_1, path_2, issame) = self.test_pairs[index] + + feature1 = self.load_feature(path_1) + feature2 = self.load_feature(path_2) + + if self.transform is not None: + feature1 = self.transform(feature1) + feature2 = self.transform(feature2) + return feature1, feature2, issame + + def __len__(self): + return len(self.test_pairs) diff --git a/data_objects/__init__.py b/data_objects/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/data_objects/audio.py b/data_objects/audio.py new file mode 100644 index 0000000..4f69d6f --- /dev/null +++ b/data_objects/audio.py @@ -0,0 +1,47 @@ +from data_objects.params_data import * +from pathlib import Path +from typing import Optional, Union +import numpy as np +import librosa + +int16_max = (2 ** 15) - 1 + + +def preprocess_wav(fpath_or_wav: Union[str, Path, np.ndarray], + source_sr: Optional[int] = None): + # Load the wav from disk if needed + if isinstance(fpath_or_wav, str) or isinstance(fpath_or_wav, Path): + wav, source_sr = librosa.load(fpath_or_wav, sr=None) + else: + wav = fpath_or_wav + + # Resample the wav if needed + if source_sr is not None and source_sr != sampling_rate: + wav = librosa.resample(wav, source_sr, sampling_rate) + + # Apply the preprocessing: normalize volume and shorten long silences + wav = normalize_volume(wav, audio_norm_target_dBFS, increase_only=True) + + return wav + + +def wav_to_spectrogram(wav): + frames = np.abs(librosa.core.stft( + wav, + n_fft=n_fft, + hop_length=int(sampling_rate * window_step / 1000), + win_length=int(sampling_rate * window_length / 1000), + )) + frames = np.log(np.maximum(frames, np.finfo(np.float32).eps)) + return frames.astype(np.float32).T + + +def normalize_volume(wav, target_dBFS, increase_only=False, decrease_only=False): + if increase_only and decrease_only: + raise ValueError("Both increase only and decrease only are set") + rms = np.sqrt(np.mean((wav * int16_max) ** 2)) + wave_dBFS = 20 * np.log10(rms / int16_max) + dBFS_change = target_dBFS - wave_dBFS + if dBFS_change < 0 and increase_only or dBFS_change > 0 and decrease_only: + return wav + return wav * (10 ** (dBFS_change / 20)) diff --git a/data_objects/compute_mean_std.py b/data_objects/compute_mean_std.py new file mode 100644 index 0000000..2764584 --- /dev/null +++ b/data_objects/compute_mean_std.py @@ -0,0 +1,33 @@ +from data_objects.speaker import Speaker +import numpy as np + +def compute_mean_std(dataset_dir, output_path_mean, output_path_std): + print("Computing mean std...") + speaker_dirs = [f for f in dataset_dir.glob("*") if f.is_dir()] + if len(speaker_dirs) == 0: + raise Exception("No speakers found. Make sure you are pointing to the directory " + "containing all preprocessed speaker directories.") + speakers = [Speaker(speaker_dir) for speaker_dir in speaker_dirs] + + sources = [] + for speaker in speakers: + sources.extend(speaker.sources) + + sumx = np.zeros(257, dtype=np.float32) + sumx2 = np.zeros(257, dtype=np.float32) + count = 0 + n = len(sources) + for i, source in enumerate(sources): + feature = np.load(source[0].joinpath(source[1])) + sumx += feature.sum(axis=0) + sumx2 += (feature * feature).sum(axis=0) + count += feature.shape[0] + + mean = sumx / count + std = np.sqrt(sumx2 / count - mean * mean) + + mean = mean.astype(np.float32) + std = std.astype(np.float32) + + np.save(output_path_mean, mean) + np.save(output_path_std, std) \ No newline at end of file diff --git a/data_objects/params_data.py b/data_objects/params_data.py new file mode 100644 index 0000000..93cb239 --- /dev/null +++ b/data_objects/params_data.py @@ -0,0 +1,18 @@ + +## Mel-filterbank +window_length = 25 # In milliseconds +window_step = 10 # In milliseconds +n_fft = 512 + + +## Audio +sampling_rate = 16000 +# Number of spectrogram frames in a partial utterance +partials_n_frames = 160 # 1600 ms +# Number of spectrogram frames at inference +inference_n_frames = 80 # 800 ms + + +## Audio volume normalization +audio_norm_target_dBFS = -30 + diff --git a/data_objects/partition_voxceleb.py b/data_objects/partition_voxceleb.py new file mode 100644 index 0000000..759e9a7 --- /dev/null +++ b/data_objects/partition_voxceleb.py @@ -0,0 +1,49 @@ +import os + + +def partition_voxceleb(feature_root, split_txt_path): + print("partitioning VoxCeleb...") + with open(split_txt_path, 'r') as f: + split_txt = f.readlines() + train_set = [] + val_set = [] + test_set = [] + for line in split_txt: + items = line.strip().split() + if items[0] == '3': + test_set.append(items[1]) + elif items[0] == '2': + val_set.append(items[1]) + else: + train_set.append(items[1]) + + speakers = os.listdir(feature_root) + + for speaker in speakers: + speaker_dir = os.path.join(feature_root, speaker) + if not os.path.isdir(speaker_dir): + continue + with open(os.path.join(speaker_dir, '_sources.txt'), 'r') as f: + speaker_files = f.readlines() + + train = [] + val = [] + test = [] + for line in speaker_files: + address = line.strip().split(',')[1] + fname = os.path.join(*address.split('/')[-3:]) + if fname in test_set: + test.append(line) + elif fname in val_set: + val.append(line) + elif fname in train_set: + train.append(line) + else: + print('file not in either train or test set') + + with open(os.path.join(speaker_dir, '_sources_train.txt'), 'w') as f: + f.writelines('%s' % line for line in train) + with open(os.path.join(speaker_dir, '_sources_val.txt'), 'w') as f: + f.writelines('%s' % line for line in val) + with open(os.path.join(speaker_dir, '_sources_test.txt'), 'w') as f: + f.writelines('%s' % line for line in test) diff --git a/data_objects/preprocess.py b/data_objects/preprocess.py new file mode 100644 index 0000000..691621a --- /dev/null +++ b/data_objects/preprocess.py @@ -0,0 +1,151 @@ +from multiprocess.pool import ThreadPool +from data_objects.params_data import * +from datetime import datetime +from data_objects import audio +from pathlib import Path +from tqdm import tqdm +import numpy as np + +anglophone_nationalites = ["australia", "canada", "ireland", "uk", "usa"] + +class DatasetLog: + """ + Registers metadata about the dataset in a text file. + """ + + def __init__(self, root, name): + self.text_file = open(Path(root, "Log_%s.txt" % name.replace("/", "_")), "w") + self.sample_data = dict() + + start_time = str(datetime.now().strftime("%A %d %B %Y at %H:%M")) + self.write_line("Creating dataset %s on %s" % (name, start_time)) + self.write_line("-----") + self._log_params() + + def _log_params(self): + from data_objects import params_data + self.write_line("Parameter values:") + for param_name in (p for p in dir(params_data) if not p.startswith("__")): + value = getattr(params_data, param_name) + self.write_line("\t%s: %s" % (param_name, value)) + self.write_line("-----") + + def write_line(self, line): + self.text_file.write("%s\n" % line) + + def add_sample(self, **kwargs): + for param_name, value in kwargs.items(): + if not param_name in self.sample_data: + self.sample_data[param_name] = [] + self.sample_data[param_name].append(value) + + def finalize(self): + self.write_line("Statistics:") + for param_name, values in self.sample_data.items(): + self.write_line("\t%s:" % param_name) + self.write_line("\t\tmin %.3f, max %.3f" % (np.min(values), np.max(values))) + self.write_line("\t\tmean %.3f, median %.3f" % (np.mean(values), np.median(values))) + self.write_line("-----") + end_time = str(datetime.now().strftime("%A %d %B %Y at %H:%M")) + self.write_line("Finished on %s" % end_time) + self.text_file.close() + + +def _init_preprocess_dataset(dataset_name, dataset_root, out_dir) -> (Path, DatasetLog): + if not dataset_root.exists(): + print("Couldn\'t find %s, skipping this dataset." % dataset_root) + return None, None + return dataset_root, DatasetLog(out_dir, dataset_name) + + +def _preprocess_speaker_dirs(speaker_dirs, dataset_name, datasets_root, out_dir, extension, + skip_existing, logger): + print("%s: Preprocessing data for %d speakers." % (dataset_name, len(speaker_dirs))) + + # Function to preprocess utterances for one speaker + def preprocess_speaker(speaker_dir: Path): + # Give a name to the speaker that includes its dataset + speaker_name = "_".join(speaker_dir.relative_to(datasets_root).parts) + + # Create an output directory with that name, as well as a txt file containing a + # reference to each source file. + speaker_out_dir = out_dir.joinpath(speaker_name) + speaker_out_dir.mkdir(exist_ok=True) + sources_fpath = speaker_out_dir.joinpath("_sources.txt") + + # There's a possibility that the preprocessing was interrupted earlier, check if + # there already is a sources file. + if sources_fpath.exists(): + try: + with sources_fpath.open("r") as sources_file: + existing_fnames = {line.split(",")[0] for line in sources_file} + except: + existing_fnames = {} + else: + existing_fnames = {} + + # Gather all audio files for that speaker recursively + sources_file = sources_fpath.open("a" if skip_existing else "w") + for in_fpath in speaker_dir.glob("**/*.%s" % extension): + # Check if the target output file already exists + out_fname = "_".join(in_fpath.relative_to(speaker_dir).parts) + out_fname = out_fname.replace(".%s" % extension, ".npy") + if skip_existing and out_fname in existing_fnames: + continue + + # Load and preprocess the waveform + wav = audio.preprocess_wav(in_fpath) + if len(wav) == 0: + print(in_fpath) + continue + + # Create the mel spectrogram, discard those that are too short + # frames = audio.wav_to_mel_spectrogram(wav) + frames = audio.wav_to_spectrogram(wav) + if len(frames) < partials_n_frames: + continue + + out_fpath = speaker_out_dir.joinpath(out_fname) + np.save(out_fpath, frames) + logger.add_sample(duration=len(wav) / sampling_rate) + sources_file.write("%s,%s\n" % (out_fname, in_fpath)) + + sources_file.close() + + # Process the utterances for each speaker + preprocess_speaker(speaker_dirs[0]) + with ThreadPool(1) as pool: + list(tqdm(pool.imap(preprocess_speaker, speaker_dirs), dataset_name, len(speaker_dirs), + unit="speakers")) + logger.finalize() + print("Done preprocessing %s.\n" % dataset_name) + + +def preprocess_voxceleb1(dataset_root: Path, out_dir: Path, skip_existing=False): + # Initialize the preprocessing + dataset_name = "VoxCeleb1" + dataset_root, logger = _init_preprocess_dataset(dataset_name, dataset_root, out_dir) + if not dataset_root: + return + + # Get the contents of the meta file + with dataset_root.joinpath("vox1_meta.csv").open("r") as metafile: + metadata = [line.split("\t") for line in metafile][1:] + + # Select the ID and the nationality, filter out non-anglophone speakers + nationalities = {line[0]: line[3] for line in metadata} + keep_speaker_ids = [speaker_id for speaker_id, nationality in nationalities.items() if + nationality.lower() in anglophone_nationalites] + print("VoxCeleb1: using samples from %d (presumed anglophone) speakers out of %d." % + (len(keep_speaker_ids), len(nationalities))) + + # Get the speaker directories for anglophone speakers only + speaker_dirs = dataset_root.joinpath("wav").glob("*") + speaker_dirs = [speaker_dir for speaker_dir in speaker_dirs] + + print("VoxCeleb1: found %d anglophone speakers on the disk." % + (len(speaker_dirs))) + # Preprocess all speakers + _preprocess_speaker_dirs(speaker_dirs, dataset_name, dataset_root, out_dir, "wav", + skip_existing, logger) + diff --git a/data_objects/speaker.py b/data_objects/speaker.py new file mode 100644 index 0000000..fc049f0 --- /dev/null +++ b/data_objects/speaker.py @@ -0,0 +1,44 @@ +from data_objects.utterance import Utterance +from pathlib import Path + + +# Contains the set of utterances of a single speaker +class Speaker: + def __init__(self, root: Path, partition=None): + self.root = root + self.partition = partition + self.name = root.name + self.utterances = None + self.utterance_cycler = None + if self.partition is None: + with self.root.joinpath("_sources.txt").open("r") as sources_file: + sources = [l.strip().split(",") for l in sources_file] + else: + with self.root.joinpath("_sources_{}.txt".format(self.partition)).open("r") as sources_file: + sources = [l.strip().split(",") for l in sources_file] + self.sources = [[self.root, frames_fname, self.name, wav_path] for frames_fname, wav_path in sources] + + def _load_utterances(self): + self.utterances = [Utterance(source[0].joinpath(source[1])) for source in self.sources] + + def random_partial(self, count, n_frames): + """ + Samples a batch of unique partial utterances from the disk in a way that all + utterances come up at least once every two cycles and in a random order every time. + + :param count: The number of partial utterances to sample from the set of utterances from + that speaker. Utterances are guaranteed not to be repeated if is not larger than + the number of utterances available. + :param n_frames: The number of frames in the partial utterance. + :return: A list of tuples (utterance, frames, range) where utterance is an Utterance, + frames are the frames of the partial utterances and range is the range of the partial + utterance with regard to the complete utterance. + """ + if self.utterances is None: + self._load_utterances() + + utterances = self.utterance_cycler.sample(count) + + a = [(u,) + u.random_partial(n_frames) for u in utterances] + + return a \ No newline at end of file diff --git a/data_objects/transforms.py b/data_objects/transforms.py new file mode 100644 index 0000000..e1e9cb1 --- /dev/null +++ b/data_objects/transforms.py @@ -0,0 +1,36 @@ +import numpy as np +import random + +class Normalize(object): + def __init__(self, mean, std): + super(Normalize, self).__init__() + self.mean = mean + self.std = std + + def __call__(self, input): + return (input - self.mean) / self.std + + +class TimeReverse(object): + def __init__(self, p=0.5): + super(TimeReverse, self).__init__() + self.p = p + + def __call__(self, input): + if random.random() < self.p: + return np.flip(input, axis=0).copy() + return input + + +def generate_test_sequence(feature, partial_n_frames, shift=None): + while feature.shape[0] <= partial_n_frames: + feature = np.repeat(feature, 2, axis=0) + if shift is None: + shift = partial_n_frames // 2 + test_sequence = [] + start = 0 + while start + partial_n_frames <= feature.shape[0]: + test_sequence.append(feature[start: start + partial_n_frames]) + start += shift + test_sequence = np.stack(test_sequence, axis=0) + return test_sequence \ No newline at end of file diff --git a/data_objects/utterance.py b/data_objects/utterance.py new file mode 100644 index 0000000..a25e4a0 --- /dev/null +++ b/data_objects/utterance.py @@ -0,0 +1,25 @@ +import numpy as np + + +class Utterance: + def __init__(self, frames_fpath): + self.frames_fpath = frames_fpath + + def get_frames(self): + return np.load(self.frames_fpath) + + def random_partial(self, n_frames): + """ + Crops the frames into a partial utterance of n_frames + + :param n_frames: The number of frames of the partial utterance + :return: the partial utterance frames and a tuple indicating the start and end of the + partial utterance in the complete utterance. + """ + frames = self.get_frames() + if frames.shape[0] == n_frames: + start = 0 + else: + start = np.random.randint(0, frames.shape[0] - n_frames) + end = start + n_frames + return frames[start:end], (start, end) \ No newline at end of file diff --git a/data_preprocess.py b/data_preprocess.py new file mode 100644 index 0000000..dbba27b --- /dev/null +++ b/data_preprocess.py @@ -0,0 +1,33 @@ +from data_objects.preprocess import preprocess_voxceleb1 +from data_objects.compute_mean_std import compute_mean_std +from data_objects.partition_voxceleb import partition_voxceleb +from pathlib import Path +import argparse + +if __name__ == "__main__": + class MyFormatter(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter): + pass + + parser = argparse.ArgumentParser(description="Preprocesses audio files from datasets.", + formatter_class=MyFormatter + ) + parser.add_argument("dataset_root", type=Path, help= \ + "Path to the directory containing VoxCeleb datasets. It should be arranged as:") + parser.add_argument("-s", "--skip_existing", action="store_true", help= \ + "Whether to skip existing output files with the same name. Useful if this script was " + "interrupted.") + args = parser.parse_args() + + # Process the arguments + out_dir = args.dataset_root.joinpath("feature") + assert args.dataset_root.exists() + assert args.dataset_root.joinpath('iden_split.txt').exists() + assert args.dataset_root.joinpath('veri_test.txt').exists() + assert args.dataset_root.joinpath('vox1_meta.csv').exists() + out_dir.mkdir(exist_ok=True, parents=True) + + # Preprocess the datasets + preprocess_voxceleb1(args.dataset_root, out_dir, args.skip_existing) + compute_mean_std(out_dir, args.dataset_root.joinpath('mean.npy'), args.dataset_root.joinpath('std.npy')) + partition_voxceleb(out_dir, args.dataset_root.joinpath('iden_split.txt')) + print("Done") diff --git a/evaluate_identification.py b/evaluate_identification.py new file mode 100644 index 0000000..43be66b --- /dev/null +++ b/evaluate_identification.py @@ -0,0 +1,110 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import argparse +import numpy as np +import os +from pathlib import Path + +import torch +import torch.nn as nn +import torch.backends.cudnn as cudnn + +from models.model import Network +from models import resnet +from config import cfg, update_config +from utils import create_logger, Genotype +from data_objects.DeepSpeakerDataset import DeepSpeakerDataset +from functions import validate_identification + + +def parse_args(): + parser = argparse.ArgumentParser(description='Train autospeech network') + # general + parser.add_argument('--cfg', + help='experiment configure file name', + required=True, + type=str) + + parser.add_argument('opts', + help="Modify config options using the command-line", + default=None, + nargs=argparse.REMAINDER) + + parser.add_argument('--load_path', + help="The path to resumed dir", + required=True, + default=None) + + args = parser.parse_args() + + return args + + +def main(): + args = parse_args() + update_config(cfg, args) + if args.load_path is None: + raise AttributeError("Please specify load path.") + + # cudnn related setting + cudnn.benchmark = cfg.CUDNN.BENCHMARK + torch.backends.cudnn.deterministic = cfg.CUDNN.DETERMINISTIC + torch.backends.cudnn.enabled = cfg.CUDNN.ENABLED + + # Set the random seed manually for reproducibility. + np.random.seed(cfg.SEED) + torch.manual_seed(cfg.SEED) + torch.cuda.manual_seed_all(cfg.SEED) + + # model and optimizer + if cfg.MODEL.NAME == 'model': + if args.load_path and os.path.exists(args.load_path): + checkpoint = torch.load(args.load_path) + genotype = checkpoint['genotype'] + else: + raise AssertionError('Please specify the model to evaluate') + model = Network(cfg.MODEL.INIT_CHANNELS, cfg.MODEL.NUM_CLASSES, cfg.MODEL.LAYERS, genotype) + model.drop_path_prob = 0.0 + else: + model = eval('resnet.{}(num_classes={})'.format(cfg.MODEL.NAME, cfg.MODEL.NUM_CLASSES)) + model = model.cuda() + + criterion = nn.CrossEntropyLoss().cuda() + + # resume && make log dir and logger + if args.load_path and os.path.exists(args.load_path): + checkpoint = torch.load(args.load_path) + + # load checkpoint + model.load_state_dict(checkpoint['state_dict']) + args.path_helper = checkpoint['path_helper'] + + logger = create_logger(args.path_helper['log_path']) + logger.info("=> loaded checkpoint '{}'".format(args.load_path)) + else: + raise AssertionError('Please specify the model to evaluate') + logger.info(args) + logger.info(cfg) + + # dataloader + test_dataset_identification = DeepSpeakerDataset( + Path(cfg.DATASET.DATA_DIR), cfg.DATASET.PARTIAL_N_FRAMES, 'test', is_test=True) + + + test_loader_identification = torch.utils.data.DataLoader( + dataset=test_dataset_identification, + batch_size=1, + num_workers=cfg.DATASET.NUM_WORKERS, + pin_memory=True, + shuffle=False, + drop_last=True, + ) + + validate_identification(cfg, model, test_loader_identification, criterion) + + + +if __name__ == '__main__': + main() diff --git a/evaluate_verification.py b/evaluate_verification.py new file mode 100644 index 0000000..296641c --- /dev/null +++ b/evaluate_verification.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- +# @Date : 2019-08-09 +# @Author : Xinyu Gong (xy_gong@tamu.edu) +# @Link : None +# @Version : 0.0 + + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import argparse +import numpy as np +import os +from pathlib import Path + +import torch +import torch.backends.cudnn as cudnn + +from models.model import Network +from models import resnet +from config import cfg, update_config +from utils import create_logger, Genotype +from data_objects.VoxcelebTestset import VoxcelebTestset +from functions import validate_verification + + +def parse_args(): + parser = argparse.ArgumentParser(description='Train autospeech network') + # general + parser.add_argument('--cfg', + help='experiment configure file name', + required=True, + type=str) + + parser.add_argument('opts', + help="Modify config options using the command-line", + default=None, + nargs=argparse.REMAINDER) + + parser.add_argument('--load_path', + help="The path to resumed dir", + default=None) + + parser.add_argument('--text_arch', + help="The path to arch", + default=None) + + args = parser.parse_args() + + return args + + +def main(): + args = parse_args() + update_config(cfg, args) + if args.load_path is None: + raise AttributeError("Please specify load path.") + + # cudnn related setting + cudnn.benchmark = cfg.CUDNN.BENCHMARK + torch.backends.cudnn.deterministic = cfg.CUDNN.DETERMINISTIC + torch.backends.cudnn.enabled = cfg.CUDNN.ENABLED + + # Set the random seed manually for reproducibility. + np.random.seed(cfg.SEED) + torch.manual_seed(cfg.SEED) + torch.cuda.manual_seed_all(cfg.SEED) + + # model and optimizer + if cfg.MODEL.NAME == 'model': + if args.load_path and os.path.exists(args.load_path): + checkpoint = torch.load(args.load_path) + genotype = checkpoint['genotype'] + else: + raise AssertionError('Please specify the model to evaluate') + model = Network(cfg.MODEL.INIT_CHANNELS, cfg.MODEL.NUM_CLASSES, cfg.MODEL.LAYERS, genotype) + model.drop_path_prob = 0.0 + else: + model = eval('resnet.{}(num_classes={})'.format(cfg.MODEL.NAME, cfg.MODEL.NUM_CLASSES)) + # model = Network(cfg.MODEL.NAME, cfg.MODEL.EMBEDDING_DIM, cfg.MODEL.NUM_CLASSES) + # model = DeepSpeakerModel(cfg.MODEL.EMBEDDING_DIM, cfg.MODEL.NUM_CLASSES) + model = model.cuda() + + # resume && make log dir and logger + if args.load_path and os.path.exists(args.load_path): + checkpoint = torch.load(args.load_path) + + # load checkpoint + model.load_state_dict(checkpoint['state_dict']) + args.path_helper = checkpoint['path_helper'] + + logger = create_logger(args.path_helper['log_path']) + logger.info("=> loaded checkpoint '{}'".format(args.load_path)) + else: + raise AssertionError('Please specify the model to evaluate') + logger.info(args) + logger.info(cfg) + + # dataloader + test_dataset_verification = VoxcelebTestset( + Path(cfg.DATASET.DATA_DIR), cfg.DATASET.PARTIAL_N_FRAMES + ) + test_loader_verification = torch.utils.data.DataLoader( + dataset=test_dataset_verification, + batch_size=1, + num_workers=cfg.DATASET.NUM_WORKERS, + pin_memory=True, + shuffle=False, + drop_last=False, + ) + + validate_verification(cfg, model, test_loader_verification) + + + +if __name__ == '__main__': + main() diff --git a/exps/baseline/resnet18.yaml b/exps/baseline/resnet18.yaml new file mode 100644 index 0000000..ed5af4b --- /dev/null +++ b/exps/baseline/resnet18.yaml @@ -0,0 +1,28 @@ +PRINT_FREQ: 200 +VAL_FREQ: 10 + +CUDNN: + BENCHMARK: true + DETERMINISTIC: false + ENABLED: true + +DATASET: + DATA_DIR: '/path/to/VoxCeleb' + NUM_WORKERS: 0 + PARTIAL_N_FRAMES: 300 + +TRAIN: + BATCH_SIZE: 256 + LR: 0.01 + LR_MIN: 0.001 + WD: 0.0003 + BETA1: 0.9 + BETA2: 0.999 + + BEGIN_EPOCH: 0 + END_EPOCH: 301 + +MODEL: + NAME: 'resnet18' + NUM_CLASSES: 1251 + INIT_CHANNELS: 64 \ No newline at end of file diff --git a/exps/baseline/resnet34.yaml b/exps/baseline/resnet34.yaml new file mode 100644 index 0000000..038c7c0 --- /dev/null +++ b/exps/baseline/resnet34.yaml @@ -0,0 +1,28 @@ +PRINT_FREQ: 200 +VAL_FREQ: 10 + +CUDNN: + BENCHMARK: true + DETERMINISTIC: false + ENABLED: true + +DATASET: + DATA_DIR: '/path/to/VoxCeleb' + NUM_WORKERS: 0 + PARTIAL_N_FRAMES: 300 + +TRAIN: + BATCH_SIZE: 256 + LR: 0.01 + LR_MIN: 0.001 + WD: 0.0003 + BETA1: 0.9 + BETA2: 0.999 + + BEGIN_EPOCH: 0 + END_EPOCH: 301 + +MODEL: + NAME: 'resnet34' + NUM_CLASSES: 1251 + INIT_CHANNELS: 64 \ No newline at end of file diff --git a/exps/scratch/scratch.yaml b/exps/scratch/scratch.yaml new file mode 100644 index 0000000..67faf6b --- /dev/null +++ b/exps/scratch/scratch.yaml @@ -0,0 +1,30 @@ +PRINT_FREQ: 200 +VAL_FREQ: 10 + +CUDNN: + BENCHMARK: true + DETERMINISTIC: false + ENABLED: true + +DATASET: + DATA_DIR: '/path/to/VoxCeleb' + DATASET: 'train' + NUM_WORKERS: 0 + PARTIAL_N_FRAMES: 300 + +TRAIN: + BATCH_SIZE: 48 + LR: 0.01 + LR_MIN: 0.001 + WD: 0.0003 + BETA1: 0.9 + BETA2: 0.999 + + BEGIN_EPOCH: 0 + END_EPOCH: 301 + +MODEL: + NAME: 'model' + NUM_CLASSES: 1251 + LAYERS: 8 + INIT_CHANNELS: 64 \ No newline at end of file diff --git a/exps/search.yaml b/exps/search.yaml new file mode 100644 index 0000000..fbdb2c8 --- /dev/null +++ b/exps/search.yaml @@ -0,0 +1,36 @@ +PRINT_FREQ: 200 +VAL_FREQ: 5 + +CUDNN: + BENCHMARK: true + DETERMINISTIC: false + ENABLED: true + +DATASET: + DATA_DIR: '/path/to/VoxCeleb' + DATASET: 'train' + NUM_WORKERS: 0 + PARTIAL_N_FRAMES: 300 + +TRAIN: + BATCH_SIZE: 2 + LR: 0.01 + LR_MIN: 0.001 + WD: 0.0003 + BETA1: 0.9 + BETA2: 0.999 + + ARCH_LR: 0.001 + ARCH_WD: 0.001 + ARCH_BETA1: 0.9 + ARCH_BETA2: 0.999 + DROPPATH_PROB: 0.2 + + BEGIN_EPOCH: 0 + END_EPOCH: 50 + +MODEL: + NAME: 'model_search' + NUM_CLASSES: 1251 + LAYERS: 8 + INIT_CHANNELS: 16 diff --git a/figures/searched_arch_normal.png b/figures/searched_arch_normal.png new file mode 100644 index 0000000000000000000000000000000000000000..ace470e2c603396ff2f7968b3c7c1fab2a152b61 GIT binary patch literal 74628 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�a^p^?I%}%GXg=5C)+JGlgl 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 0.: + if not isinstance(op1, Identity): + h1 = drop_path(h1, drop_prob) + if not isinstance(op2, Identity): + h2 = drop_path(h2, drop_prob) + s = h1 + h2 + states += [s] + return torch.cat([states[i] for i in self._concat], dim=1) + + +class Network(nn.Module): + + def __init__(self, C, num_classes, layers, genotype): + super(Network, self).__init__() + self._C = C + self._num_classes = num_classes + self._layers = layers + + self.stem0 = nn.Sequential( + nn.Conv2d(1, C // 2, kernel_size=3, stride=2, padding=1, bias=False), + nn.BatchNorm2d(C // 2), + nn.ReLU(inplace=True), + nn.Conv2d(C // 2, C, kernel_size=3, stride=2, padding=1, bias=False), + nn.BatchNorm2d(C), + ) + + self.stem1 = nn.Sequential( + nn.ReLU(inplace=True), + nn.Conv2d(C, C, 3, stride=2, padding=1, bias=False), + nn.BatchNorm2d(C), + ) + + C_prev_prev, C_prev, C_curr = C, C, C + + self.cells = nn.ModuleList() + reduction_prev = True + for i in range(layers): + if i in [layers // 3, 2 * layers // 3]: + C_curr *= 2 + reduction = True + else: + reduction = False + cell = Cell(genotype, C_prev_prev, C_prev, C_curr, reduction, reduction_prev) + reduction_prev = reduction + self.cells += [cell] + C_prev_prev, C_prev = C_prev, cell.multiplier * C_curr + + self.global_pooling = nn.AdaptiveAvgPool2d((1, 1)) + self.classifier = nn.Linear(C_prev, num_classes) + + def forward(self, input): + input = input.unsqueeze(1) + s0 = self.stem0(input) + s1 = self.stem1(s0) + for i, cell in enumerate(self.cells): + s0, s1 = s1, cell(s0, s1, self.drop_path_prob) + v = self.global_pooling(s1) + v = v.view(v.size(0), -1) + if not self.training: + return v + + y = self.classifier(v) + + return y + + + def forward_classifier(self, v): + y = self.classifier(v) + return y + + + + diff --git a/models/model_search.py b/models/model_search.py new file mode 100644 index 0000000..df6b3a6 --- /dev/null +++ b/models/model_search.py @@ -0,0 +1,220 @@ +import math +import torch.nn.functional as F +from operations import * +from utils import Genotype +from utils import gumbel_softmax, drop_path + + +class MixedOp(nn.Module): + + def __init__(self, C, stride, PRIMITIVES): + super(MixedOp, self).__init__() + self._ops = nn.ModuleList() + for primitive in PRIMITIVES: + op = OPS[primitive](C, stride, False) + if 'pool' in primitive: + op = nn.Sequential(op, nn.BatchNorm2d(C, affine=False)) + self._ops.append(op) + + def forward(self, x, weights): + """ + This is a forward function. + :param x: Feature map + :param weights: A tensor of weight controlling the path flow + :return: A weighted sum of several path + """ + output = 0 + for op_idx, op in enumerate(self._ops): + if weights[op_idx].item() != 0: + if math.isnan(weights[op_idx]): + raise OverflowError(f'weight: {weights}') + output += weights[op_idx] * op(x) + return output + + +class Cell(nn.Module): + + def __init__(self, steps, multiplier, C_prev_prev, C_prev, C, reduction, reduction_prev): + super(Cell, self).__init__() + self.reduction = reduction + self.primitives = self.PRIMITIVES['primitives_reduct' if reduction else 'primitives_normal'] + + if reduction_prev: + self.preprocess0 = FactorizedReduce(C_prev_prev, C, affine=False) + else: + self.preprocess0 = ReLUConvBN(C_prev_prev, C, 1, 1, 0, affine=False) + self.preprocess1 = ReLUConvBN(C_prev, C, 1, 1, 0, affine=False) + self._steps = steps + self._multiplier = multiplier + + self._ops = nn.ModuleList() + self._bns = nn.ModuleList() + + edge_index = 0 + + for i in range(self._steps): + for j in range(2 + i): + stride = 2 if reduction and j < 2 else 1 + op = MixedOp(C, stride, self.primitives[edge_index]) + self._ops.append(op) + edge_index += 1 + + def forward(self, s0, s1, weights, drop_prob=0.0): + s0 = self.preprocess0(s0) + s1 = self.preprocess1(s1) + + states = [s0, s1] + offset = 0 + for i in range(self._steps): + if drop_prob > 0. and self.training: + s = sum( + drop_path(self._ops[offset + j](h, weights[offset + j]), drop_prob) for j, h in enumerate(states)) + else: + s = sum(self._ops[offset + j](h, weights[offset + j]) for j, h in enumerate(states)) + offset += len(states) + states.append(s) + + return torch.cat(states[-self._multiplier:], dim=1) + + +class Network(nn.Module): + + def __init__(self, C, num_classes, layers, criterion, primitives, + steps=4, multiplier=4, stem_multiplier=3, drop_path_prob=0.0): + super(Network, self).__init__() + self._C = C + self._num_classes = num_classes + self._layers = layers + self._criterion = criterion + self._steps = steps + self._multiplier = multiplier + self.drop_path_prob = drop_path_prob + + nn.Module.PRIMITIVES = primitives + + C_curr = stem_multiplier * C + self.stem = nn.Sequential( + nn.Conv2d(1, C_curr, 3, padding=1, bias=False), + nn.BatchNorm2d(C_curr), + nn.MaxPool2d(kernel_size=3, stride=2, padding=1) + ) + + C_prev_prev, C_prev, C_curr = C_curr, C_curr, C + self.cells = nn.ModuleList() + reduction_prev = False + for i in range(layers): + if i in [layers // 3, 2 * layers // 3]: + C_curr *= 2 + reduction = True + else: + reduction = False + cell = Cell(steps, multiplier, C_prev_prev, C_prev, C_curr, reduction, reduction_prev) + reduction_prev = reduction + self.cells += [cell] + C_prev_prev, C_prev = C_prev, multiplier * C_curr + + self.global_pooling = nn.AdaptiveAvgPool2d((1, 1)) + self.classifier = nn.Linear(C_prev, self._num_classes) + + self._initialize_alphas() + + def new(self): + model_new = Network(self._C, self._embed_dim, self._layers, self._criterion, + self.PRIMITIVES, drop_path_prob=self.drop_path_prob).cuda() + for x, y in zip(model_new.arch_parameters(), self.arch_parameters()): + x.data.copy_(y.data) + return model_new + + def forward(self, input, discrete=False): + input = input.unsqueeze(1) + s0 = s1 = self.stem(input) + for i, cell in enumerate(self.cells): + if cell.reduction: + if discrete: + weights = self.alphas_reduce + else: + weights = gumbel_softmax(F.log_softmax(self.alphas_reduce, dim=-1)) + else: + if discrete: + weights = self.alphas_normal + else: + weights = gumbel_softmax(F.log_softmax(self.alphas_normal, dim=-1)) + s0, s1 = s1, cell(s0, s1, weights, self.drop_path_prob) + v = self.global_pooling(s1) + v = v.view(v.size(0), -1) + if not self.training: + return v + + y = self.classifier(v) + + return y + + def forward_classifier(self, v): + y = self.classifier(v) + return y + + def _loss(self, input, target): + logits = self(input) + return self._criterion(logits, target) + + def _initialize_alphas(self): + k = sum(1 for i in range(self._steps) for n in range(2 + i)) + num_ops = len(self.PRIMITIVES['primitives_normal'][0]) + + self.alphas_normal = nn.Parameter(1e-3 * torch.randn(k, num_ops)) + self.alphas_reduce = nn.Parameter(1e-3 * torch.randn(k, num_ops)) + self._arch_parameters = [ + self.alphas_normal, + self.alphas_reduce, + ] + + def arch_parameters(self): + return self._arch_parameters + + def compute_arch_entropy(self, dim=-1): + alpha = self.arch_parameters()[0] + prob = F.softmax(alpha, dim=dim) + log_prob = F.log_softmax(alpha, dim=dim) + entropy = - (log_prob * prob).sum(-1, keepdim=False) + return entropy + + def genotype(self): + def _parse(weights, normal=True): + PRIMITIVES = self.PRIMITIVES['primitives_normal' if normal else 'primitives_reduct'] + + gene = [] + n = 2 + start = 0 + for i in range(self._steps): + end = start + n + W = weights[start:end].copy() + try: + edges = sorted(range(i + 2), key=lambda x: -max( + W[x][k] for k in range(len(W[x])) if k != PRIMITIVES[x].index('none')))[:2] + except ValueError: # This error happens when the 'none' op is not present in the ops + edges = sorted(range(i + 2), key=lambda x: -max(W[x][k] for k in range(len(W[x]))))[:2] + for j in edges: + k_best = None + for k in range(len(W[j])): + if 'none' in PRIMITIVES[j]: + if k != PRIMITIVES[j].index('none'): + if k_best is None or W[j][k] > W[j][k_best]: + k_best = k + else: + if k_best is None or W[j][k] > W[j][k_best]: + k_best = k + gene.append((PRIMITIVES[start+j][k_best], j)) + start = end + n += 1 + return gene + + gene_normal = _parse(F.softmax(self.alphas_normal, dim=-1).data.cpu().numpy(), True) + gene_reduce = _parse(F.softmax(self.alphas_reduce, dim=-1).data.cpu().numpy(), False) + + concat = range(2 + self._steps - self._multiplier, self._steps + 2) + genotype = Genotype( + normal=gene_normal, normal_concat=concat, + reduce=gene_reduce, reduce_concat=concat + ) + return genotype + diff --git a/models/resnet.py b/models/resnet.py new file mode 100644 index 0000000..4e14355 --- /dev/null +++ b/models/resnet.py @@ -0,0 +1,344 @@ +""" +Code source: https://github.com/pytorch/vision +""" +from __future__ import absolute_import +from __future__ import division + +__all__ = ['resnet18', 'resnet34', 'resnet50', 'resnet101', 'resnet152', 'resnext50_32x4d', + 'resnext101_32x8d', 'resnet50_fc512'] + +from torch import nn +import torch +import torch.utils.model_zoo as model_zoo + +model_urls = { + 'resnet18': + 'https://download.pytorch.org/models/resnet18-5c106cde.pth', + 'resnet34': + 'https://download.pytorch.org/models/resnet34-333f7ec4.pth', + 'resnet50': + 'https://download.pytorch.org/models/resnet50-19c8e357.pth', + 'resnet101': + 'https://download.pytorch.org/models/resnet101-5d3b4d8f.pth', + 'resnet152': + 'https://download.pytorch.org/models/resnet152-b121ed2d.pth', + 'resnext50_32x4d': + 'https://download.pytorch.org/models/resnext50_32x4d-7cdf4587.pth', + 'resnext101_32x8d': + 'https://download.pytorch.org/models/resnext101_32x8d-8ba56ff5.pth', +} + + +def conv3x3(in_planes, out_planes, stride=1, groups=1, dilation=1): + """3x3 convolution with padding""" + return nn.Conv2d(in_planes, out_planes, kernel_size=3, stride=stride, + padding=dilation, groups=groups, bias=False, dilation=dilation) + + +def conv1x1(in_planes, out_planes, stride=1): + """1x1 convolution""" + return nn.Conv2d(in_planes, out_planes, kernel_size=1, stride=stride, bias=False) + + +class BasicBlock(nn.Module): + expansion = 1 + + def __init__(self, inplanes, planes, stride=1, downsample=None, groups=1, + base_width=64, dilation=1, norm_layer=None): + super(BasicBlock, self).__init__() + if norm_layer is None: + norm_layer = nn.BatchNorm2d + if groups != 1 or base_width != 64: + raise ValueError('BasicBlock only supports groups=1 and base_width=64') + if dilation > 1: + raise NotImplementedError("Dilation > 1 not supported in BasicBlock") + # Both self.conv1 and self.downsample layers downsample the input when stride != 1 + self.conv1 = conv3x3(inplanes, planes, stride) + self.bn1 = norm_layer(planes) + self.relu = nn.ReLU(inplace=True) + self.conv2 = conv3x3(planes, planes) + self.bn2 = norm_layer(planes) + self.downsample = downsample + self.stride = stride + + def forward(self, x): + identity = x + + out = self.conv1(x) + out = self.bn1(out) + out = self.relu(out) + + out = self.conv2(out) + out = self.bn2(out) + + if self.downsample is not None: + identity = self.downsample(x) + + out += identity + out = self.relu(out) + + return out + + +class Bottleneck(nn.Module): + expansion = 4 + + def __init__(self, inplanes, planes, stride=1, downsample=None, groups=1, + base_width=64, dilation=1, norm_layer=None): + super(Bottleneck, self).__init__() + if norm_layer is None: + norm_layer = nn.BatchNorm2d + width = int(planes * (base_width / 64.)) * groups + # Both self.conv2 and self.downsample layers downsample the input when stride != 1 + self.conv1 = conv1x1(inplanes, width) + self.bn1 = norm_layer(width) + self.conv2 = conv3x3(width, width, stride, groups, dilation) + self.bn2 = norm_layer(width) + self.conv3 = conv1x1(width, planes * self.expansion) + self.bn3 = norm_layer(planes * self.expansion) + self.relu = nn.ReLU(inplace=True) + self.downsample = downsample + self.stride = stride + + def forward(self, x): + identity = x + + out = self.conv1(x) + out = self.bn1(out) + out = self.relu(out) + + out = self.conv2(out) + out = self.bn2(out) + out = self.relu(out) + + out = self.conv3(out) + out = self.bn3(out) + + if self.downsample is not None: + identity = self.downsample(x) + + out += identity + out = self.relu(out) + + return out + + +class ResNet(nn.Module): + """Residual network. + + Reference: + - He et al. Deep Residual Learning for Image Recognition. CVPR 2016. + - Xie et al. Aggregated Residual Transformations for Deep Neural Networks. CVPR 2017. + Public keys: + - ``resnet18``: ResNet18. + - ``resnet34``: ResNet34. + - ``resnet50``: ResNet50. + - ``resnet101``: ResNet101. + - ``resnet152``: ResNet152. + - ``resnext50_32x4d``: ResNeXt50. + - ``resnext101_32x8d``: ResNeXt101. + - ``resnet50_fc512``: ResNet50 + FC. + """ + + def __init__(self, num_classes, loss, block, layers, zero_init_residual=False, + groups=1, width_per_group=64, replace_stride_with_dilation=None, + norm_layer=None, last_stride=2, fc_dims=None, dropout_p=None, **kwargs): + super(ResNet, self).__init__() + if norm_layer is None: + norm_layer = nn.BatchNorm2d + self._norm_layer = norm_layer + self.loss = loss + self.feature_dim = 512 * block.expansion + self.inplanes = 64 + self.dilation = 1 + if replace_stride_with_dilation is None: + # each element in the tuple indicates if we should replace + # the 2x2 stride with a dilated convolution instead + replace_stride_with_dilation = [False, False, False] + if len(replace_stride_with_dilation) != 3: + raise ValueError("replace_stride_with_dilation should be None " + "or a 3-element tuple, got {}".format(replace_stride_with_dilation)) + self.groups = groups + self.base_width = width_per_group + self.conv1 = nn.Conv2d(1, self.inplanes, kernel_size=7, stride=2, padding=2, + bias=False) + self.bn1 = norm_layer(self.inplanes) + self.relu = nn.ReLU(inplace=True) + self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1) + self.layer1 = self._make_layer(block, 64, layers[0]) + self.layer2 = self._make_layer(block, 128, layers[1], stride=2, + dilate=replace_stride_with_dilation[0]) + self.layer3 = self._make_layer(block, 256, layers[2], stride=2, + dilate=replace_stride_with_dilation[1]) + self.layer4 = self._make_layer(block, 512, layers[3], stride=last_stride, + dilate=replace_stride_with_dilation[2]) + + self.global_avgpool = nn.AdaptiveAvgPool2d((1, 1)) + self.fc = self._construct_fc_layer(fc_dims, self.feature_dim, dropout_p) + self.classifier = nn.Linear(self.feature_dim, num_classes) + + self._init_params() + + # Zero-initialize the last BN in each residual branch, + # so that the residual branch starts with zeros, and each residual block behaves like an identity. + # This improves the model by 0.2~0.3% according to https://arxiv.org/abs/1706.02677 + if zero_init_residual: + for m in self.modules(): + if isinstance(m, Bottleneck): + nn.init.constant_(m.bn3.weight, 0) + elif isinstance(m, BasicBlock): + nn.init.constant_(m.bn2.weight, 0) + + def _make_layer(self, block, planes, blocks, stride=1, dilate=False): + norm_layer = self._norm_layer + downsample = None + previous_dilation = self.dilation + if dilate: + self.dilation *= stride + stride = 1 + if stride != 1 or self.inplanes != planes * block.expansion: + downsample = nn.Sequential( + conv1x1(self.inplanes, planes * block.expansion, stride), + norm_layer(planes * block.expansion), + ) + + layers = [] + layers.append(block(self.inplanes, planes, stride, downsample, self.groups, + self.base_width, previous_dilation, norm_layer)) + self.inplanes = planes * block.expansion + for _ in range(1, blocks): + layers.append(block(self.inplanes, planes, groups=self.groups, + base_width=self.base_width, dilation=self.dilation, + norm_layer=norm_layer)) + + return nn.Sequential(*layers) + + def _construct_fc_layer(self, fc_dims, input_dim, dropout_p=None): + """Constructs fully connected layer + Args: + fc_dims (list or tuple): dimensions of fc layers, if None, no fc layers are constructed + input_dim (int): input dimension + dropout_p (float): dropout probability, if None, dropout is unused + """ + if fc_dims is None: + self.feature_dim = input_dim + return None + + assert isinstance(fc_dims, (list, tuple)), 'fc_dims must be either list or tuple, but got {}'.format( + type(fc_dims)) + + layers = [] + for dim in fc_dims: + layers.append(nn.Linear(input_dim, dim)) + layers.append(nn.BatchNorm1d(dim)) + layers.append(nn.ReLU(inplace=True)) + if dropout_p is not None: + layers.append(nn.Dropout(p=dropout_p)) + input_dim = dim + + self.feature_dim = fc_dims[-1] + + return nn.Sequential(*layers) + + def _init_params(self): + for m in self.modules(): + if isinstance(m, nn.Conv2d): + nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu') + if m.bias is not None: + nn.init.constant_(m.bias, 0) + elif isinstance(m, nn.BatchNorm2d): + nn.init.constant_(m.weight, 1) + nn.init.constant_(m.bias, 0) + elif isinstance(m, nn.BatchNorm1d): + nn.init.constant_(m.weight, 1) + nn.init.constant_(m.bias, 0) + elif isinstance(m, nn.Linear): + nn.init.normal_(m.weight, 0, 0.01) + if m.bias is not None: + nn.init.constant_(m.bias, 0) + + def featuremaps(self, x): + x = x.unsqueeze(1) + x = self.conv1(x) + x = self.bn1(x) + x = self.relu(x) + x = self.maxpool(x) + x = self.layer1(x) + x = self.layer2(x) + x = self.layer3(x) + x = self.layer4(x) + return x + + def forward(self, x): + f = self.featuremaps(x) + v = self.global_avgpool(f) + v = v.view(v.size(0), -1) + + if self.fc is not None: + v = self.fc(v) + + if not self.training: + return v + + y = self.classifier(v) + + if self.loss == 'xent': + return y + elif self.loss == 'htri': + return y, v + else: + raise KeyError("Unsupported loss: {}".format(self.loss)) + + def forward_classifier(self, v): + y = self.classifier(v) + return y + +"""ResNet""" + + +def init_pretrained_weights(model, model_url): + """Initializes model with pretrained weights. + + Layers that don't match with pretrained layers in name or size are kept unchanged. + """ + pretrain_dict = model_zoo.load_url(model_url) + model_dict = model.state_dict() + pretrain_dict = { + k: v + for k, v in pretrain_dict.items() + if k in model_dict and model_dict[k].size() == v.size() + } + model_dict.update(pretrain_dict) + model.load_state_dict(model_dict) + + +def resnet18(num_classes, loss='xent', pretrained=True, **kwargs): + model = ResNet( + num_classes=num_classes, + loss=loss, + block=BasicBlock, + layers=[2, 2, 2, 2], + last_stride=2, + fc_dims=None, + dropout_p=None, + **kwargs + ) + if pretrained: + init_pretrained_weights(model, model_urls['resnet18']) + return model + + +def resnet34(num_classes, loss='xent', pretrained=True, **kwargs): + model = ResNet( + num_classes=num_classes, + loss=loss, + block=BasicBlock, + layers=[3, 4, 6, 3], + last_stride=2, + fc_dims=None, + dropout_p=None, + **kwargs + ) + if pretrained: + init_pretrained_weights(model, model_urls['resnet34']) + return model diff --git a/operations.py b/operations.py new file mode 100644 index 0000000..bfd150b --- /dev/null +++ b/operations.py @@ -0,0 +1,178 @@ +import torch +import torch.nn as nn + +OPS = { + 'none' : lambda C, stride, affine: Zero(stride), + 'avg_pool_3x3' : lambda C, stride, affine: nn.AvgPool2d(3, stride=stride, padding=1, count_include_pad=False), + 'max_pool_3x3' : lambda C, stride, affine: nn.MaxPool2d(3, stride=stride, padding=1), + 'skip_connect' : lambda C, stride, affine: Identity() if stride == 1 else FactorizedReduce(C, C, affine=affine), + 'sep_conv_3x3' : lambda C, stride, affine: SepConv(C, C, 3, stride, 1, affine=affine), + 'sep_conv_5x5' : lambda C, stride, affine: SepConv(C, C, 5, stride, 2, affine=affine), + 'sep_conv_7x7' : lambda C, stride, affine: SepConv(C, C, 7, stride, 3, affine=affine), + 'sep_conv_3x1' : lambda C, stride, affine: SepConvTime(C, C, 3, stride, 1, affine=affine), + 'sep_conv_1x3' : lambda C, stride, affine: SepConvFreq(C, C, 3, stride, 1, affine=affine), + 'dil_conv_3x3' : lambda C, stride, affine: DilConv(C, C, 3, stride, 2, 2, affine=affine), + 'dil_conv_5x5' : lambda C, stride, affine: DilConv(C, C, 5, stride, 4, 2, affine=affine), + 'dil_conv_3x1' : lambda C, stride, affine: DilConvTime(C, C, 3, stride, 2, 2, affine=affine), + 'dil_conv_1x3' : lambda C, stride, affine: DilConvFreq(C, C, 3, stride, 2, 2, affine=affine), + 'conv_7x1_1x7' : lambda C, stride, affine: nn.Sequential( + nn.ReLU(inplace=False), + nn.Conv2d(C, C, (1,7), stride=(1, stride), padding=(0, 3), bias=False), + nn.Conv2d(C, C, (7,1), stride=(stride, 1), padding=(3, 0), bias=False), + nn.BatchNorm2d(C, affine=affine) + ), +} + +class ReLUConvBN(nn.Module): + + def __init__(self, C_in, C_out, kernel_size, stride, padding, affine=True): + super(ReLUConvBN, self).__init__() + self.op = nn.Sequential( + nn.ReLU(inplace=False), + nn.Conv2d(C_in, C_out, kernel_size, stride=stride, padding=padding, bias=False), + nn.BatchNorm2d(C_out, affine=affine) + ) + + def forward(self, x): + return self.op(x) + +class DilConv(nn.Module): + + def __init__(self, C_in, C_out, kernel_size, stride, padding, dilation, affine=True): + super(DilConv, self).__init__() + self.op = nn.Sequential( + nn.ReLU(inplace=False), + nn.Conv2d(C_in, C_in, kernel_size=kernel_size, stride=stride, padding=padding, dilation=dilation, groups=C_in, bias=False), + nn.Conv2d(C_in, C_out, kernel_size=1, padding=0, bias=False), + nn.BatchNorm2d(C_out, affine=affine), + ) + + def forward(self, x): + return self.op(x) + + +class DilConvTime(nn.Module): + + def __init__(self, C_in, C_out, kernel_size, stride, padding, dilation, affine=True): + super(DilConvTime, self).__init__() + self.op = nn.Sequential( + nn.ReLU(inplace=False), + nn.Conv2d(C_in, C_in, kernel_size=(kernel_size, 1), stride=stride, padding=(padding, 0), dilation=(dilation, 1), groups=C_in, + bias=False), + nn.Conv2d(C_in, C_out, kernel_size=1, padding=0, bias=False), + nn.BatchNorm2d(C_out, affine=affine), + ) + + def forward(self, x): + return self.op(x) + + +class DilConvFreq(nn.Module): + + def __init__(self, C_in, C_out, kernel_size, stride, padding, dilation, affine=True): + super(DilConvFreq, self).__init__() + self.op = nn.Sequential( + nn.ReLU(inplace=False), + nn.Conv2d(C_in, C_in, kernel_size=(1, kernel_size), stride=stride, padding=(0, padding), dilation=(1, dilation), groups=C_in, + bias=False), + nn.Conv2d(C_in, C_out, kernel_size=1, padding=0, bias=False), + nn.BatchNorm2d(C_out, affine=affine), + ) + + def forward(self, x): + return self.op(x) + +class SepConv(nn.Module): + + def __init__(self, C_in, C_out, kernel_size, stride, padding, affine=True): + super(SepConv, self).__init__() + self.op = nn.Sequential( + nn.ReLU(inplace=False), + nn.Conv2d(C_in, C_in, kernel_size=kernel_size, stride=stride, padding=padding, groups=C_in, bias=False), + nn.Conv2d(C_in, C_in, kernel_size=1, padding=0, bias=False), + nn.BatchNorm2d(C_in, affine=affine), + nn.ReLU(inplace=False), + nn.Conv2d(C_in, C_in, kernel_size=kernel_size, stride=1, padding=padding, groups=C_in, bias=False), + nn.Conv2d(C_in, C_out, kernel_size=1, padding=0, bias=False), + nn.BatchNorm2d(C_out, affine=affine), + ) + + def forward(self, x): + return self.op(x) + + +class SepConvTime(nn.Module): + + def __init__(self, C_in, C_out, kernel_size, stride, padding, affine=True): + super(SepConvTime, self).__init__() + self.op = nn.Sequential( + nn.ReLU(inplace=False), + nn.Conv2d(C_in, C_in, kernel_size=(kernel_size, 1), stride=stride, padding=(padding, 0), groups=C_in, bias=False), + nn.Conv2d(C_in, C_in, kernel_size=1, padding=0, bias=False), + nn.BatchNorm2d(C_in, affine=affine), + nn.ReLU(inplace=False), + nn.Conv2d(C_in, C_in, kernel_size=(kernel_size, 1), stride=1, padding=(padding, 0), groups=C_in, bias=False), + nn.Conv2d(C_in, C_out, kernel_size=1, padding=0, bias=False), + nn.BatchNorm2d(C_out, affine=affine), + ) + + def forward(self, x): + return self.op(x) + + +class SepConvFreq(nn.Module): + + def __init__(self, C_in, C_out, kernel_size, stride, padding, affine=True): + super(SepConvFreq, self).__init__() + self.op = nn.Sequential( + nn.ReLU(inplace=False), + nn.Conv2d(C_in, C_in, kernel_size=(1, kernel_size), stride=stride, padding=(0, padding), groups=C_in, bias=False), + nn.Conv2d(C_in, C_in, kernel_size=1, padding=0, bias=False), + nn.BatchNorm2d(C_in, affine=affine), + nn.ReLU(inplace=False), + nn.Conv2d(C_in, C_in, kernel_size=(1, kernel_size), stride=1, padding=(0, padding), groups=C_in, bias=False), + nn.Conv2d(C_in, C_out, kernel_size=1, padding=0, bias=False), + nn.BatchNorm2d(C_out, affine=affine), + ) + + def forward(self, x): + return self.op(x) + +class Identity(nn.Module): + + def __init__(self): + super(Identity, self).__init__() + + def forward(self, x): + return x + + +class Zero(nn.Module): + + def __init__(self, stride): + super(Zero, self).__init__() + self.stride = stride + + def forward(self, x): + if self.stride == 1: + return x.mul(0.) + return x[:,:,::self.stride,::self.stride].mul(0.) + + +class FactorizedReduce(nn.Module): + + def __init__(self, C_in, C_out, affine=True): + super(FactorizedReduce, self).__init__() + assert C_out % 2 == 0 + self.relu = nn.ReLU(inplace=False) + self.conv_1 = nn.Conv2d(C_in, C_out // 2, 1, stride=2, padding=0, bias=False) + self.conv_2 = nn.Conv2d(C_in, C_out // 2, 1, stride=2, padding=0, bias=False) + self.bn = nn.BatchNorm2d(C_out, affine=affine) + + def forward(self, x): + x = self.relu(x) + # out = torch.cat([self.conv_1(x), self.conv_2(x[:,:,1:,1:])], dim=1) + out = torch.cat([self.conv_1(x), self.conv_2(x)], dim=1) + out = self.bn(out) + return out + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..09cf6c0 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,32 @@ +audioread==2.1.8 +certifi==2020.4.5.1 +cffi==1.14.0 +cycler==0.10.0 +decorator==4.4.2 +dill==0.3.1.1 +future==0.18.2 +joblib==0.14.1 +kiwisolver==1.2.0 +librosa==0.7.2 +llvmlite==0.32.0 +matplotlib==3.2.1 +multiprocess==0.70.9 +numba==0.49.0 +numpy==1.18.4 +Pillow==7.1.2 +protobuf==3.11.3 +pycparser==2.20 +pyparsing==2.4.7 +python-dateutil==2.8.1 +PyYAML==5.3.1 +resampy==0.2.2 +scikit-learn==0.22.2.post1 +scipy==1.4.1 +six==1.14.0 +sklearn==0.0 +SoundFile==0.10.3.post1 +tensorboardX==2.0 +torch==1.5.0 +torchvision==0.6.0 +tqdm==4.46.0 +yacs==0.1.7 diff --git a/search.py b/search.py new file mode 100644 index 0000000..66c3157 --- /dev/null +++ b/search.py @@ -0,0 +1,206 @@ +# -*- coding: utf-8 -*- +# @Date : 2019-08-09 +# @Author : Xinyu Gong (xy_gong@tamu.edu) +# @Link : None +# @Version : 0.0 + + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import argparse +import numpy as np +import shutil +import os +from tensorboardX import SummaryWriter +from tqdm import tqdm +from pathlib import Path + +import torch +import torch.optim as optim +import torch.backends.cudnn as cudnn + +from config import cfg, update_config +from utils import set_path, create_logger, save_checkpoint +from data_objects.DeepSpeakerDataset import DeepSpeakerDataset +from functions import train, validate_identification +from architect import Architect +from loss import CrossEntropyLoss +from torch.utils.data import DataLoader +from spaces import primitives_1, primitives_2, primitives_3 +from models.model_search import Network + + +def parse_args(): + parser = argparse.ArgumentParser(description='Train energy network') + # general + parser.add_argument('--cfg', + help='experiment configure file name', + required=True, + type=str) + + parser.add_argument('opts', + help="Modify config options using the command-line", + default=None, + nargs=argparse.REMAINDER) + + parser.add_argument('--load_path', + help="The path to resumed dir", + default=None) + + args = parser.parse_args() + + return args + + +def main(): + args = parse_args() + update_config(cfg, args) + + # cudnn related setting + cudnn.benchmark = cfg.CUDNN.BENCHMARK + torch.backends.cudnn.deterministic = cfg.CUDNN.DETERMINISTIC + torch.backends.cudnn.enabled = cfg.CUDNN.ENABLED + + # Set the random seed manually for reproducibility. + np.random.seed(cfg.SEED) + torch.manual_seed(cfg.SEED) + torch.cuda.manual_seed_all(cfg.SEED) + + # Loss + criterion = CrossEntropyLoss(cfg.MODEL.NUM_CLASSES).cuda() + + # model and optimizer + model = Network(cfg.MODEL.INIT_CHANNELS, cfg.MODEL.NUM_CLASSES, cfg.MODEL.LAYERS, criterion, primitives_2, + drop_path_prob=cfg.TRAIN.DROPPATH_PROB) + model = model.cuda() + + # weight params + arch_params = list(map(id, model.arch_parameters())) + weight_params = filter(lambda p: id(p) not in arch_params, + model.parameters()) + + # Optimizer + optimizer = optim.Adam( + weight_params, + lr=cfg.TRAIN.LR, + weight_decay=cfg.TRAIN.WD, + ) + + # resume && make log dir and logger + if args.load_path and os.path.exists(args.load_path): + checkpoint_file = os.path.join(args.load_path, 'Model', 'checkpoint_best.pth') + assert os.path.exists(checkpoint_file) + checkpoint = torch.load(checkpoint_file) + + # load checkpoint + begin_epoch = checkpoint['epoch'] + last_epoch = checkpoint['epoch'] + model.load_state_dict(checkpoint['state_dict']) + best_acc1 = checkpoint['best_acc1'] + optimizer.load_state_dict(checkpoint['optimizer']) + args.path_helper = checkpoint['path_helper'] + + logger = create_logger(args.path_helper['log_path']) + logger.info("=> loaded checkpoint '{}'".format(checkpoint_file)) + else: + exp_name = args.cfg.split('/')[-1].split('.')[0] + args.path_helper = set_path('logs_search', exp_name) + logger = create_logger(args.path_helper['log_path']) + begin_epoch = cfg.TRAIN.BEGIN_EPOCH + best_acc1 = 0.0 + last_epoch = -1 + + logger.info(args) + logger.info(cfg) + + # copy model file + this_dir = os.path.dirname(__file__) + shutil.copy2( + os.path.join(this_dir, 'models', cfg.MODEL.NAME + '.py'), + args.path_helper['ckpt_path']) + + # dataloader + train_dataset = DeepSpeakerDataset( + Path(cfg.DATASET.DATA_DIR), cfg.DATASET.PARTIAL_N_FRAMES, 'train') + val_dataset = DeepSpeakerDataset( + Path(cfg.DATASET.DATA_DIR), cfg.DATASET.PARTIAL_N_FRAMES, 'val') + train_loader = torch.utils.data.DataLoader( + dataset=train_dataset, + batch_size=cfg.TRAIN.BATCH_SIZE, + num_workers=cfg.DATASET.NUM_WORKERS, + pin_memory=True, + shuffle=True, + drop_last=True, + ) + val_loader = torch.utils.data.DataLoader( + dataset=val_dataset, + batch_size=cfg.TRAIN.BATCH_SIZE, + num_workers=cfg.DATASET.NUM_WORKERS, + pin_memory=True, + shuffle=True, + drop_last=True, + ) + test_dataset = DeepSpeakerDataset( + Path(cfg.DATASET.DATA_DIR), cfg.DATASET.PARTIAL_N_FRAMES, 'test', is_test=True) + test_loader = torch.utils.data.DataLoader( + dataset=test_dataset, + batch_size=1, + num_workers=cfg.DATASET.NUM_WORKERS, + pin_memory=True, + shuffle=True, + drop_last=True, + ) + + # training setting + writer_dict = { + 'writer': SummaryWriter(args.path_helper['log_path']), + 'train_global_steps': begin_epoch * len(train_loader), + 'valid_global_steps': begin_epoch // cfg.VAL_FREQ, + } + + # training loop + architect = Architect(model, cfg) + lr_scheduler = torch.optim.lr_scheduler.CosineAnnealingLR( + optimizer, cfg.TRAIN.END_EPOCH, cfg.TRAIN.LR_MIN, + last_epoch=last_epoch + ) + + for epoch in tqdm(range(begin_epoch, cfg.TRAIN.END_EPOCH), desc='search progress'): + model.train() + + genotype = model.genotype() + logger.info('genotype = %s', genotype) + + if cfg.TRAIN.DROPPATH_PROB != 0: + model.drop_path_prob = cfg.TRAIN.DROPPATH_PROB * epoch / (cfg.TRAIN.END_EPOCH - 1) + + train(cfg, model, optimizer, train_loader, val_loader, criterion, architect, epoch, writer_dict) + + if epoch % cfg.VAL_FREQ == 0: + # get threshold and evaluate on validation set + acc = validate_identification(cfg, model, test_loader, criterion) + + # remember best acc@1 and save checkpoint + is_best = acc > best_acc1 + best_acc1 = max(acc, best_acc1) + + # save + logger.info('=> saving checkpoint to {}'.format(args.path_helper['ckpt_path'])) + save_checkpoint({ + 'epoch': epoch + 1, + 'state_dict': model.state_dict(), + 'best_acc1': best_acc1, + 'optimizer': optimizer.state_dict(), + 'arch': model.arch_parameters(), + 'genotype': genotype, + 'path_helper': args.path_helper + }, is_best, args.path_helper['ckpt_path'], 'checkpoint_{}.pth'.format(epoch)) + + lr_scheduler.step(epoch) + + + +if __name__ == '__main__': + main() diff --git a/spaces.py b/spaces.py new file mode 100644 index 0000000..c0302b2 --- /dev/null +++ b/spaces.py @@ -0,0 +1,94 @@ +from collections import OrderedDict + +primitives_1 = OrderedDict([('primitives_normal', [['skip_connect', + 'dil_conv_3x3'], + ['skip_connect', + 'dil_conv_5x5'], + ['skip_connect', + 'dil_conv_5x5'], + ['skip_connect', + 'sep_conv_3x3'], + ['skip_connect', + 'dil_conv_3x3'], + ['max_pool_3x3', + 'skip_connect'], + ['skip_connect', + 'sep_conv_3x3'], + ['skip_connect', + 'sep_conv_3x3'], + ['skip_connect', + 'dil_conv_3x3'], + ['skip_connect', + 'sep_conv_3x3'], + ['max_pool_3x3', + 'skip_connect'], + ['skip_connect', + 'dil_conv_3x3'], + ['dil_conv_3x3', + 'dil_conv_5x5'], + ['dil_conv_3x3', + 'dil_conv_5x5']]), + ('primitives_reduct', [['max_pool_3x3', + 'avg_pool_3x3'], + ['max_pool_3x3', + 'dil_conv_3x3'], + ['max_pool_3x3', + 'avg_pool_3x3'], + ['max_pool_3x3', + 'avg_pool_3x3'], + ['skip_connect', + 'dil_conv_5x5'], + ['max_pool_3x3', + 'avg_pool_3x3'], + ['max_pool_3x3', + 'sep_conv_3x3'], + ['skip_connect', + 'dil_conv_3x3'], + ['skip_connect', + 'dil_conv_5x5'], + ['max_pool_3x3', + 'avg_pool_3x3'], + ['max_pool_3x3', + 'avg_pool_3x3'], + ['skip_connect', + 'dil_conv_5x5'], + ['skip_connect', + 'dil_conv_5x5'], + ['skip_connect', + 'dil_conv_5x5']])]) + +PRIMITIVES = [ + 'none', + 'max_pool_3x3', + 'avg_pool_3x3', + 'skip_connect', + 'sep_conv_3x3', + 'sep_conv_5x5', + 'dil_conv_3x3', + 'dil_conv_5x5' +] + +primitives_2 = OrderedDict([('primitives_normal', 14 * [PRIMITIVES]), + ('primitives_reduct', 14 * [PRIMITIVES])]) + +PRIMITIVES_SMALL = [ + 'none', + 'max_pool_3x3', + 'avg_pool_3x3', + 'skip_connect', + 'sep_conv_3x3', + 'sep_conv_3x1', + 'sep_conv_1x3', + 'dil_conv_3x3', + 'dil_conv_3x1', + 'dil_conv_1x3', +] + +primitives_3 = OrderedDict([('primitives_normal', 14 * [PRIMITIVES_SMALL]), + ('primitives_reduct', 14 * [PRIMITIVES_SMALL])]) + +spaces_dict = { + 's1': primitives_1, # space from https://openreview.net/forum?id=H1gDNyrKDS + 's2': primitives_2, # original DARTS space + 's3': primitives_3, # space with 1D conv +} \ No newline at end of file diff --git a/train.py b/train.py new file mode 100644 index 0000000..9f811db --- /dev/null +++ b/train.py @@ -0,0 +1,180 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import argparse +import numpy as np +import shutil +import os +from pathlib import Path +from tensorboardX import SummaryWriter +from tqdm import tqdm + +import torch +import torch.optim as optim +import torch.backends.cudnn as cudnn + +from models.model import Network +from config import cfg, update_config +from utils import set_path, create_logger, save_checkpoint, count_parameters, Genotype +from data_objects.DeepSpeakerDataset import DeepSpeakerDataset +from functions import train_from_scratch, validate_identification +from loss import CrossEntropyLoss + + +def parse_args(): + parser = argparse.ArgumentParser(description='Train energy network') + # general + parser.add_argument('--cfg', + help='experiment configure file name', + required=True, + type=str) + + parser.add_argument('--text_arch', + help="The text to arch", + required=True, + default=None) + + parser.add_argument('opts', + help="Modify config options using the command-line", + default=None, + nargs=argparse.REMAINDER) + + parser.add_argument('--load_path', + help="The path to resumed dir", + default=None) + + + + args = parser.parse_args() + + return args + + +def main(): + args = parse_args() + update_config(cfg, args) + + # cudnn related setting + cudnn.benchmark = cfg.CUDNN.BENCHMARK + torch.backends.cudnn.deterministic = cfg.CUDNN.DETERMINISTIC + torch.backends.cudnn.enabled = cfg.CUDNN.ENABLED + + # Set the random seed manually for reproducibility. + np.random.seed(cfg.SEED) + torch.manual_seed(cfg.SEED) + torch.cuda.manual_seed_all(cfg.SEED) + + # Loss + criterion = CrossEntropyLoss(cfg.MODEL.NUM_CLASSES).cuda() + + # load arch + genotype = eval(args.text_arch) + + model = Network(cfg.MODEL.INIT_CHANNELS, cfg.MODEL.NUM_CLASSES, cfg.MODEL.LAYERS, genotype) + model = model.cuda() + + optimizer = optim.Adam( + model.parameters(), + lr=cfg.TRAIN.LR, + weight_decay=cfg.TRAIN.WD, + ) + + # resume && make log dir and logger + if args.load_path and os.path.exists(args.load_path): + checkpoint_file = os.path.join(args.load_path, 'Model', 'checkpoint_best.pth') + assert os.path.exists(checkpoint_file) + checkpoint = torch.load(checkpoint_file) + + # load checkpoint + begin_epoch = checkpoint['epoch'] + last_epoch = checkpoint['epoch'] + model.load_state_dict(checkpoint['state_dict']) + best_acc1 = checkpoint['best_acc1'] + optimizer.load_state_dict(checkpoint['optimizer']) + args.path_helper = checkpoint['path_helper'] + + logger = create_logger(args.path_helper['log_path']) + logger.info("=> loaded checkloggpoint '{}'".format(checkpoint_file)) + else: + exp_name = args.cfg.split('/')[-1].split('.')[0] + args.path_helper = set_path('logs_scratch', exp_name) + logger = create_logger(args.path_helper['log_path']) + begin_epoch = cfg.TRAIN.BEGIN_EPOCH + best_acc1 = 0.0 + last_epoch = -1 + logger.info(args) + logger.info(cfg) + logger.info(f"selected architecture: {genotype}") + logger.info("Number of parameters: {}".format(count_parameters(model))) + + # copy model file + this_dir = os.path.dirname(__file__) + shutil.copy2( + os.path.join(this_dir, './models', cfg.MODEL.NAME + '.py'), + args.path_helper['ckpt_path']) + + # dataloader + train_dataset = DeepSpeakerDataset( + Path(cfg.DATASET.DATA_DIR), cfg.DATASET.PARTIAL_N_FRAMES, 'train') + train_loader = torch.utils.data.DataLoader( + dataset=train_dataset, + batch_size=cfg.TRAIN.BATCH_SIZE, + num_workers=cfg.DATASET.NUM_WORKERS, + pin_memory=True, + shuffle=True, + drop_last=True, + ) + test_dataset = DeepSpeakerDataset( + Path(cfg.DATASET.DATA_DIR), cfg.DATASET.PARTIAL_N_FRAMES, 'test', is_test=True) + test_loader = torch.utils.data.DataLoader( + dataset=test_dataset, + batch_size=1, + num_workers=cfg.DATASET.NUM_WORKERS, + pin_memory=True, + shuffle=True, + drop_last=True, + ) + + # training setting + writer_dict = { + 'writer': SummaryWriter(args.path_helper['log_path']), + 'train_global_steps': begin_epoch * len(train_loader), + 'valid_global_steps': begin_epoch // cfg.VAL_FREQ, + } + + # training loop + lr_scheduler = torch.optim.lr_scheduler.CosineAnnealingLR( + optimizer, cfg.TRAIN.END_EPOCH, cfg.TRAIN.LR_MIN, + last_epoch=last_epoch + ) + + for epoch in tqdm(range(begin_epoch, cfg.TRAIN.END_EPOCH), desc='train progress'): + model.train() + model.drop_path_prob = cfg.MODEL.DROP_PATH_PROB * epoch / cfg.TRAIN.END_EPOCH + + train_from_scratch(cfg, model, optimizer, train_loader, criterion, epoch, writer_dict) + + if epoch % cfg.VAL_FREQ == 0 or epoch == cfg.TRAIN.END_EPOCH - 1: + acc = validate_identification(cfg, model, test_loader, criterion) + + # remember best acc@1 and save checkpoint + is_best = acc > best_acc1 + best_acc1 = max(acc, best_acc1) + + # save + logger.info('=> saving checkpoint to {}'.format(args.path_helper['ckpt_path'])) + save_checkpoint({ + 'epoch': epoch + 1, + 'state_dict': model.state_dict(), + 'best_acc1': best_acc1, + 'optimizer': optimizer.state_dict(), + 'path_helper': args.path_helper, + 'genotype': genotype, + }, is_best, args.path_helper['ckpt_path'], 'checkpoint_{}.pth'.format(epoch)) + + lr_scheduler.step(epoch) + + +if __name__ == '__main__': + main() diff --git a/train_baseline.py b/train_baseline.py new file mode 100644 index 0000000..e98946b --- /dev/null +++ b/train_baseline.py @@ -0,0 +1,159 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import argparse +import numpy as np +import os +from tensorboardX import SummaryWriter +from tqdm import tqdm +from pathlib import Path + +import torch +import torch.optim as optim +import torch.backends.cudnn as cudnn + +from models import resnet +from config import cfg, update_config +from utils import set_path, create_logger, save_checkpoint, count_parameters +from data_objects.DeepSpeakerDataset import DeepSpeakerDataset +from functions import train_from_scratch, validate_identification +from loss import CrossEntropyLoss + + +def parse_args(): + parser = argparse.ArgumentParser(description='Train energy network') + # general + parser.add_argument('--cfg', + help='experiment configure file name', + required=True, + type=str) + + parser.add_argument('opts', + help="Modify config options using the command-line", + default=None, + nargs=argparse.REMAINDER) + + parser.add_argument('--load_path', + help="The path to resumed dir", + default=None) + + args = parser.parse_args() + + return args + + +def main(): + args = parse_args() + update_config(cfg, args) + + # cudnn related setting + cudnn.benchmark = cfg.CUDNN.BENCHMARK + torch.backends.cudnn.deterministic = cfg.CUDNN.DETERMINISTIC + torch.backends.cudnn.enabled = cfg.CUDNN.ENABLED + + # Set the random seed manually for reproducibility. + np.random.seed(cfg.SEED) + torch.manual_seed(cfg.SEED) + torch.cuda.manual_seed_all(cfg.SEED) + + # model and optimizer + model = eval('resnet.{}(num_classes={})'.format( + cfg.MODEL.NAME, cfg.MODEL.NUM_CLASSES)) + model = model.cuda() + optimizer = optim.Adam( + model.net_parameters() if hasattr(model, 'net_parameters') else model.parameters(), + lr=cfg.TRAIN.LR, + weight_decay=cfg.TRAIN.WD, + ) + + # Loss + criterion = CrossEntropyLoss(cfg.MODEL.NUM_CLASSES).cuda() + + # resume && make log dir and logger + if args.load_path and os.path.exists(args.load_path): + checkpoint_file = os.path.join(args.load_path, 'Model', 'checkpoint_best.pth') + # checkpoint_file = os.path.join(args.load_path, 'Model', 'checkpoint.pth') + assert os.path.exists(checkpoint_file) + checkpoint = torch.load(checkpoint_file) + + # load checkpoint + begin_epoch = checkpoint['epoch'] + last_epoch = checkpoint['epoch'] + model.load_state_dict(checkpoint['state_dict']) + best_acc1 = checkpoint['best_acc1'] + optimizer.load_state_dict(checkpoint['optimizer']) + args.path_helper = checkpoint['path_helper'] + + logger = create_logger(args.path_helper['log_path']) + logger.info("=> loaded checkpoint '{}'".format(checkpoint_file)) + else: + exp_name = args.cfg.split('/')[-1].split('.')[0] + args.path_helper = set_path('logs', exp_name) + logger = create_logger(args.path_helper['log_path']) + begin_epoch = cfg.TRAIN.BEGIN_EPOCH + best_acc1 = 0.0 + last_epoch = -1 + logger.info(args) + logger.info(cfg) + logger.info("Number of parameters: {}".format(count_parameters(model))) + + # dataloader + train_dataset = DeepSpeakerDataset( + Path(cfg.DATASET.DATA_DIR), cfg.DATASET.PARTIAL_N_FRAMES, 'train') + test_dataset_identification = DeepSpeakerDataset( + Path(cfg.DATASET.DATA_DIR), cfg.DATASET.PARTIAL_N_FRAMES, 'test', is_test=True) + train_loader = torch.utils.data.DataLoader( + dataset=train_dataset, + batch_size=cfg.TRAIN.BATCH_SIZE, + num_workers=cfg.DATASET.NUM_WORKERS, + pin_memory=True, + shuffle=True, + drop_last=True, + ) + test_loader_identification = torch.utils.data.DataLoader( + dataset=test_dataset_identification, + batch_size=1, + num_workers=cfg.DATASET.NUM_WORKERS, + pin_memory=True, + shuffle=True, + drop_last=True, + ) + + # training setting + writer_dict = { + 'writer': SummaryWriter(args.path_helper['log_path']), + 'train_global_steps': begin_epoch * len(train_loader), + 'valid_global_steps': begin_epoch // cfg.VAL_FREQ, + } + + # training loop + lr_scheduler = torch.optim.lr_scheduler.CosineAnnealingLR( + optimizer, cfg.TRAIN.END_EPOCH, cfg.TRAIN.LR_MIN, + last_epoch=last_epoch + ) + + for epoch in tqdm(range(begin_epoch, cfg.TRAIN.END_EPOCH), desc='train progress'): + model.train() + train_from_scratch(cfg, model, optimizer, train_loader, criterion, epoch, writer_dict, lr_scheduler) + if epoch % cfg.VAL_FREQ == 0: + acc = validate_identification(cfg, model, test_loader_identification, criterion) + + # remember best acc@1 and save checkpoint + is_best = acc > best_acc1 + best_acc1 = max(acc, best_acc1) + + # save + logger.info('=> saving checkpoint to {}'.format(args.path_helper['ckpt_path'])) + save_checkpoint({ + 'epoch': epoch + 1, + 'state_dict': model.state_dict(), + 'best_acc1': best_acc1, + 'optimizer': optimizer.state_dict(), + 'path_helper': args.path_helper + }, is_best, args.path_helper['ckpt_path'], 'checkpoint_{}.pth'.format(epoch)) + lr_scheduler.step(epoch) + + +if __name__ == '__main__': + main() diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..b3fa98a --- /dev/null +++ b/utils.py @@ -0,0 +1,243 @@ +import torch +import dateutil.tz +import time +import logging +import os + + +import numpy as np +from sklearn.metrics import roc_curve +from datetime import datetime +import matplotlib.pyplot as plt +from collections import namedtuple + +plt.switch_backend('agg') + +Genotype = namedtuple('Genotype', 'normal normal_concat reduce reduce_concat') + +def count_parameters(model): + return sum(p.numel() for p in model.parameters() if p.requires_grad) + + +def init_pretrained_weights(model, checkpoint): + """Initializes model with pretrained weights. + + Layers that don't match with pretrained layers in name or size are kept unchanged. + """ + checkpoint_file = torch.load(checkpoint) + pretrain_dict = checkpoint_file['state_dict'] + model_dict = model.state_dict() + pretrain_dict = { + k: v + for k, v in pretrain_dict.items() + if k in model_dict and model_dict[k].size() == v.size() + } + model_dict.update(pretrain_dict) + model.load_state_dict(model_dict) + + +class AverageMeter(object): + """Computes and stores the average and current value""" + + def __init__(self, name, fmt=':f'): + self.name = name + self.fmt = fmt + self.reset() + + def reset(self): + self.val = 0 + self.avg = 0 + self.sum = 0 + self.count = 0 + + def update(self, val, n=1): + self.val = val + self.sum += val * n + self.count += n + self.avg = self.sum / self.count + + def __str__(self): + fmtstr = '{name} {val' + self.fmt + '} ({avg' + self.fmt + '})' + return fmtstr.format(**self.__dict__) + + +class ProgressMeter(object): + def __init__(self, num_batches, *meters, prefix="", logger=None): + self.batch_fmtstr = self._get_batch_fmtstr(num_batches) + self.meters = meters + self.prefix = prefix + self.logger = logger + + def print(self, batch): + entries = [self.prefix + self.batch_fmtstr.format(batch)] + entries += [str(meter) for meter in self.meters] + if self.logger: + self.logger.info('\t'.join(entries)) + else: + print('\t'.join(entries)) + + def _get_batch_fmtstr(self, num_batches): + num_digits = len(str(num_batches // 1)) + fmt = '{:' + str(num_digits) + 'd}' + return '[' + fmt + '/' + fmt.format(num_batches) + ']' + +def compute_eer(distances, labels): + # Calculate evaluation metrics + fprs, tprs, _ = roc_curve(labels, distances) + eer = fprs[np.nanargmin(np.absolute((1 - tprs) - fprs))] + return eer + + +def accuracy(output, target, topk=(1,)): + """Computes the accuracy over the k top predictions for the specified values of k""" + with torch.no_grad(): + maxk = max(topk) + batch_size = target.size(0) + + _, pred = output.topk(maxk, 1, True, True) + pred = pred.t() + correct = pred.eq(target.view(1, -1).expand_as(pred)) + + res = [] + for k in topk: + correct_k = correct[:k].view(-1).float().sum(0, keepdim=True) + res.append(correct_k.mul_(100.0 / batch_size)) + return res + + +def create_logger(log_dir, phase='train'): + time_str = time.strftime('%Y-%m-%d-%H-%M') + log_file = '{}_{}.log'.format(time_str, phase) + final_log_file = os.path.join(log_dir, log_file) + head = '%(asctime)-15s %(message)s' + logging.basicConfig(filename=str(final_log_file), + format=head) + logger = logging.getLogger() + logger.setLevel(logging.INFO) + console = logging.StreamHandler() + logging.getLogger('').addHandler(console) + + return logger + + +def set_path(root_dir, exp_name): + path_dict = {} + os.makedirs(root_dir, exist_ok=True) + + # set log path + exp_path = os.path.join(root_dir, exp_name) + now = datetime.now(dateutil.tz.tzlocal()) + timestamp = now.strftime('%Y_%m_%d_%H_%M_%S') + prefix = exp_path + '_' + timestamp + os.makedirs(prefix) + path_dict['prefix'] = prefix + + # set checkpoint path + ckpt_path = os.path.join(prefix, 'Model') + os.makedirs(ckpt_path) + path_dict['ckpt_path'] = ckpt_path + + log_path = os.path.join(prefix, 'Log') + os.makedirs(log_path) + path_dict['log_path'] = log_path + + # set sample image path for fid calculation + sample_path = os.path.join(prefix, 'Samples') + os.makedirs(sample_path) + path_dict['sample_path'] = sample_path + + return path_dict + + +def to_item(x): + """Converts x, possibly scalar and possibly tensor, to a Python scalar.""" + if isinstance(x, (float, int)): + return x + + if float(torch.__version__[0:3]) < 0.4: + assert (x.dim() == 1) and (len(x) == 1) + return x[0] + + return x.item() + + +def save_checkpoint(states, is_best, output_dir, + filename='checkpoint.pth'): + torch.save(states, os.path.join(output_dir, filename)) + if is_best: + torch.save(states, os.path.join(output_dir, 'checkpoint_best.pth')) + +def drop_path(x, drop_prob): + if drop_prob > 0.: + keep_prob = 1.-drop_prob + mask = torch.cuda.FloatTensor(x.size(0), 1, 1, 1).bernoulli_(keep_prob) + x.div_(keep_prob) + x.mul_(mask) + return x + + +def gumbel_softmax(logits, tau=1, hard=True, eps=1e-10, dim=-1): + # type: (Tensor, float, bool, float, int) -> Tensor + """ + Samples from the `Gumbel-Softmax distribution`_ and optionally discretizes. + + Args: + logits: `[..., num_features]` unnormalized log probabilities + tau: non-negative scalar temperature + hard: if ``True``, the returned samples will be discretized as one-hot vectors, + but will be differentiated as if it is the soft sample in autograd + dim (int): A dimension along which softmax will be computed. Default: -1. + + Returns: + Sampled tensor of same shape as `logits` from the Gumbel-Softmax distribution. + If ``hard=True``, the returned samples will be one-hot, otherwise they will + be probability distributions that sum to 1 across `dim`. + + .. note:: + This function is here for legacy reasons, may be removed from nn.Functional in the future. + + .. note:: + The main trick for `hard` is to do `y_hard - y_soft.detach() + y_soft` + + It achieves two things: + - makes the output value exactly one-hot + (since we add then subtract y_soft value) + - makes the gradient equal to y_soft gradient + (since we strip all other gradients) + + Examples:: + >>> logits = torch.randn(20, 32) + >>> # Sample soft categorical using reparametrization trick: + >>> F.gumbel_softmax(logits, tau=1, hard=False) + >>> # Sample hard categorical using "Straight-through" trick: + >>> F.gumbel_softmax(logits, tau=1, hard=True) + + .. _Gumbel-Softmax distribution: + https://arxiv.org/abs/1611.00712 + https://arxiv.org/abs/1611.01144 + """ + def _gen_gumbels(): + gumbels = -torch.empty_like(logits).exponential_().log() + if torch.isnan(gumbels).sum() or torch.isinf(gumbels).sum(): + # to avoid zero in exp output + gumbels = _gen_gumbels() + return gumbels + + gumbels = _gen_gumbels() # ~Gumbel(0,1) + gumbels = (logits + gumbels) / tau # ~Gumbel(logits,tau) + y_soft = gumbels.softmax(dim) + + if hard: + # Straight through. + index = y_soft.max(dim, keepdim=True)[1] + y_hard = torch.zeros_like(logits).scatter_(dim, index, 1.0) + ret = y_hard - y_soft.detach() + y_soft + else: + # Reparametrization trick. + ret = y_soft + + if torch.isnan(ret).sum(): + import ipdb + ipdb.set_trace() + raise OverflowError(f'gumbel softmax output: {ret}') + return ret \ No newline at end of file