При поиске в Google «Случайный гиперпараметрический поиск» вы найдете только руководства о том, как рандомизировать скорость обучения, импульс, отсев, снижение веса и т. Д. Что, если вы также хотите поэкспериментировать с гиперпараметрами модели, такими как размер сверточного ядра , шаг, количество ядер и даже количество полностью связанных слоев? Не имея готовых ответов, я решил это самостоятельно.
Рандомизация гиперпараметров модели имеет смысл, когда вы решаете проблему, отличную от общих задач, таких как классификация повседневных предметов. Что касается меня, я решаю проблему регрессии временных рядов, которая требует дополнительных экспериментов со всеми гиперпараметрами.
Чтобы лучше проиллюстрировать мой подход к поиску гиперпараметров случайных моделей, я использую самый простой CNN - LeNet. Обратите внимание, что, хотя сами слои имеют произвольный размер, количество слоев (кроме полностью связанных слоев) и типы слоев жестко запрограммированы (таким образом, все еще LeNet).
Ценность поиска по гиперпараметрам модели состоит в том, чтобы абстрагироваться от размеров слоев от архитектуры. Например, когда мы говорим о LeNet-5, нам больше не нужно указывать количество ядер, размер ядра, шаг объединения и т. Д. Они в любом случае произвольны и не позволяют никакой бумаге сказать вам иное!
Часть I. Концепция: гиперпараметры модели CNN
0а. Размеры свертки и объединения
Благодаря Stanford CS231n мы можем выразить размеры свертки и объединения:
# Convolutional Layer Output Shape conv_output_size = (conv_num_kernels, (input_size - conv_kernel_size)/conv_stride + 1 ) # Pooling Layer Output Shape pool_output_size = (conv_num_kernels, (conv_output_size[1] - pool_kernel_size)/pool_stride + 1 )
Это позволяет нам динамически определять нашу модель при создании экземпляра модели:
# excerpt from lenet.py # Model creation (omitting fully connected layers) class LeNet(nn.Module): def __init__(self, input_size, output_size, batch_norm, use_pooling, pooling_method, conv1_kernel_size, conv1_num_kernels, conv1_stride, conv1_dropout, pool1_kernel_size, pool1_stride, conv2_kernel_size, conv2_num_kernels, conv2_stride, conv2_dropout, pool2_kernel_size, pool2_stride, fcs_hidden_size, fcs_num_hidden_layers, fcs_dropout): super(LeNet, self).__init__() self.input_size = input_size self.hidden_size = fcs_hidden_size self.output_size = output_size self.batch_norm = batch_norm input_channel = 2 # If not using pooling, set all pooling operations to 1 by 1. if use_pooling == False: warnings.warn('lenet: not using pooling') pool1_kernel_size = 1 pool1_stride = 1 pool2_kernel_size = 1 pool2_stride = 1 # Conv1 conv1_output_size = (conv1_num_kernels, (input_size - conv1_kernel_size) / conv1_stride + 1) # if not conv1_output_size[1].is_integer(): # raise ValueError('lenet: conv1_output_size[1] %s is not an integer.' % conv1_output_size[1]) # conv1_output_size = (conv1_num_kernels, int(conv1_output_size[1])) self.conv1 = nn.Conv1d(input_channel, conv1_num_kernels, conv1_kernel_size, stride=conv1_stride) # NOTE: THIS IS CORRECT!!!! CONV doesn't depend on num_features! nn.init.kaiming_normal_(self.conv1.weight.data) self.conv1.bias.data.fill_(0) self.conv1_drop = nn.Dropout2d(p=conv1_dropout) if self.batch_norm == True: self.batch_norm1 = nn.BatchNorm1d(conv1_num_kernels) # Pool1 pool1_output_size = (conv1_num_kernels, (conv1_output_size[1] - pool1_kernel_size) / pool1_stride + 1) self.pool1 = nn.MaxPool1d(pool1_kernel_size, stride=pool1_stride) # stride=pool1_kernel_size by default # Conv2 conv2_output_size = (conv2_num_kernels, (pool1_output_size[1] - conv2_kernel_size) / conv2_stride + 1) self.conv2 = nn.Conv1d(conv1_num_kernels, conv2_num_kernels, conv2_kernel_size, stride=conv2_stride) # NOTE: THIS IS CORRECT!!!! CONV doesn't depend on num_features! nn.init.kaiming_normal_(self.conv2.weight.data) self.conv2.bias.data.fill_(0) self.conv2_drop = nn.Dropout2d(p=conv2_dropout) if self.batch_norm == True: self.batch_norm2 = nn.BatchNorm1d(conv2_num_kernels) # Pool2 pool2_output_size = (conv2_num_kernels, (conv2_output_size[1] - pool2_kernel_size) / pool2_stride + 1) self.pool2 = nn.MaxPool1d(pool2_kernel_size, stride=pool2_stride) # stride=pool1_kernel_size by default
0b. Прогрессивный размер слоя
На предыдущем шаге вы могли заметить, что pool_output_size зависит от conv_output_size и что оба размера должны обеспечивать целочисленную делимость в (input_size - conv_kernel_size) / conv_stride и (conv_output_size [1] - pool_kernel_size) / pool_stride + 1 соответственно. Если один слой имеет неправильный размер, последующие слои могут не иметь целочисленной формы, что недопустимо и приведет к выводам тайных ошибок из PyTorch.
Кроме того, размер полностью связанных слоев зависит от формы их предыдущего слоя. В нашем случае это pool2:
# excerpt from lenet.py # Model creation (the omitted fully-connected layers) # FCs fcs_input_size = pool2_output_size[0] * pool2_output_size[1] # if not fcs_input_size.is_integer(): # raise ValueError('lenet: fcs_input_size = ' + fcs_input_size + ' is not an integer') # fcs_input_size = int(fcs_input_size) self.fcs = FullyConnectedNet(fcs_input_size, output_size, fcs_dropout, batch_norm, fcs_hidden_size, fcs_num_hidden_layers)
Чтобы решить эту проблему межуровневой зависимости размера, один наивный подход состоит в том, чтобы случайным образом выбирать один гиперпараметр до тех пор, пока все слои не будут иметь целочисленные размеры для всех гиперпараметров модели. Более эффективный способ - заранее определить все допустимые комбинации гиперпараметров модели и произвольно выбрать из них, например:
# Part of create_model.py: Size-constrained random hyperparameter search possible_size_combinations = [] for conv1_kernel_size in conv1_kernel_size_range: for conv1_stride in conv1_stride_range: # Satisfy conv1 condition if (input_size - conv1_kernel_size) % conv1_stride != 0: continue conv1_output_size = (conv1_num_kernels, (input_size - conv1_kernel_size) / conv1_stride + 1) for pool1_kernel_size in pool1_kernel_size_range: for pool1_stride in pool1_stride_range: if (conv1_output_size[1] - pool1_kernel_size) % pool1_stride != 0: continue pool1_output_size = (conv1_num_kernels, (conv1_output_size[1] - pool1_kernel_size) / pool1_stride + 1) for conv2_kernel_size in conv2_kernel_size_range: for conv2_stride in conv2_stride_range: if (pool1_output_size[1] - conv2_kernel_size) % conv2_stride != 0: continue conv2_output_size = (conv2_num_kernels, (pool1_output_size[1] - conv2_kernel_size) / conv2_stride + 1) for pool2_kernel_size in pool2_kernel_size_range: for pool2_stride in pool2_stride_range: if (conv2_output_size[1] - pool2_kernel_size) % pool2_stride != 0: continue pool2_output_size = (conv2_num_kernels, (conv2_output_size[1] - pool2_kernel_size) / pool2_stride + 1) possible_size_combinations.append((conv1_kernel_size, conv1_stride, pool1_kernel_size, pool1_stride, conv2_kernel_size, conv2_stride, pool2_kernel_size, pool2_stride)) if len(possible_size_combinations) == 0: raise ValueError('create_models: no possible combination for pool1 given conv1_output_size[1] = ' + str(conv1_output_size[1]) + '; pool1_kernel_size_ranges = ' + str(pool1_kernel_size_ranges) + '; pool1_stride_ranges = ' + str(pool1_stride_ranges)) conv1_kernel_size, conv1_stride, pool1_kernel_size, pool1_stride, conv2_kernel_size, conv2_stride, pool2_kernel_size, pool2_stride = random.choice(possible_size_combinations)
Таким образом, мы можем быть уверены, что не застрянем в циклах рандомизации while из-за неправильных диапазонов гиперпараметров; мы также храним код в одном месте и относительно аккуратно (он может не выглядеть соответствующим образом, но попробуйте заменить приведенное выше на 10+ циклов while). Продолжайте читать, чтобы получить полную информацию о реализации.
Часть II. Реализация: поиск гиперпараметров случайной модели
Мой общий подход состоит из творчества и тренировочного процесса. Причина, по которой они разделены, - это научная оценка, воспроизводимость и параллельные вычисления в масштабе. Во-первых, функция create_models берет количество моделей для создания и файл диапазона гиперпараметров и записывает их в файл.
Примечание. Не обращайте внимания на подмодели «k» - я сохраняю len (k) копий той же модели для более поздней оценки предметной области.
# create_models.py # The creation process - randomize model and training hyperparameters and write them to file. # Example usage: python lib/create_models.py 50 hyperparam_ranges.json import os import datetime import random import argparse import json import warnings import sys from utils import save_model_params, ensure_dir def choose_hyperparameters_from_file(hyperparameter_ranges_file): with open(hyperparameter_ranges_file) as f: ranges = json.load(f) # Load constants. input_size = ranges['input_size'] batch_norm = random.choice(ranges['batch_norm']) use_pooling = random.choice(ranges['use_pooling']) conv1_num_kernels = random.choice(list(range(*ranges['conv1_num_kernels']))) conv1_dropout = random.uniform(*ranges['conv1_dropout']) conv2_num_kernels = random.choice(list(range(*ranges['conv2_num_kernels']))) conv2_dropout = random.uniform(*ranges['conv2_dropout']) # Randomly choose model hyperparameters from ranges. conv1_kernel_size_range = list(range(*ranges['conv1_kernel_size'])) conv1_stride_range = ranges['conv1_stride'] pool1_kernel_size_range = ranges['pool1_kernel_size'] pool1_stride_range = ranges['pool1_stride'] conv2_kernel_size_range = list(range(*ranges['conv2_kernel_size'])) conv2_stride_range = ranges['conv2_stride'] pool2_kernel_size_range = ranges['pool2_kernel_size'] pool2_stride_range = ranges['pool2_stride'] # Size-constrained random hyperparameter search possible_size_combinations = [] for conv1_kernel_size in conv1_kernel_size_range: for conv1_stride in conv1_stride_range: # Satisfy conv1 condition if (input_size - conv1_kernel_size) % conv1_stride != 0: continue conv1_output_size = (conv1_num_kernels, (input_size - conv1_kernel_size) / conv1_stride + 1) for pool1_kernel_size in pool1_kernel_size_range: for pool1_stride in pool1_stride_range: if (conv1_output_size[1] - pool1_kernel_size) % pool1_stride != 0: continue pool1_output_size = (conv1_num_kernels, (conv1_output_size[1] - pool1_kernel_size) / pool1_stride + 1) for conv2_kernel_size in conv2_kernel_size_range: for conv2_stride in conv2_stride_range: if (pool1_output_size[1] - conv2_kernel_size) % conv2_stride != 0: continue conv2_output_size = (conv2_num_kernels, (pool1_output_size[1] - conv2_kernel_size) / conv2_stride + 1) for pool2_kernel_size in pool2_kernel_size_range: for pool2_stride in pool2_stride_range: if (conv2_output_size[1] - pool2_kernel_size) % pool2_stride != 0: continue pool2_output_size = (conv2_num_kernels, (conv2_output_size[1] - pool2_kernel_size) / pool2_stride + 1) possible_size_combinations.append((conv1_kernel_size, conv1_stride, pool1_kernel_size, pool1_stride, conv2_kernel_size, conv2_stride, pool2_kernel_size, pool2_stride)) if len(possible_size_combinations) == 0: raise ValueError('create_models: no possible combination for pool1 given conv1_output_size[1] = ' + str(conv1_output_size[1]) + '; pool1_kernel_size_ranges = ' + str(pool1_kernel_size_ranges) + '; pool1_stride_ranges = ' + str(pool1_stride_ranges)) conv1_kernel_size, conv1_stride, pool1_kernel_size, pool1_stride, conv2_kernel_size, conv2_stride, pool2_kernel_size, pool2_stride = random.choice(possible_size_combinations) # print('create_models: ranges[\'fcs_hidden_size\'] =', ranges['fcs_hidden_size']) # print('create_models: list(range(*ranges[\'fcs_hidden_size\'])) =', list(range(*ranges['fcs_hidden_size']))) fcs_hidden_size = random.choice(list(range(*ranges['fcs_hidden_size']))) fcs_num_hidden_layers = random.choice(list(range(*ranges['fcs_num_hidden_layers']))) fcs_dropout = random.uniform(*ranges['fcs_dropout']) # Randomly choose training hyperparameters from ranges. cost_function = random.choice(ranges['cost_function']) optimizer = random.choice(ranges['optimizer']) if optimizer == 'SGD': momentum = random.uniform(*ranges['momentum']) learning_rate = random.uniform(*ranges['learning_rate_sgd']) elif optimizer == 'Adam': momentum = None learning_rate = random.uniform(*ranges['learning_rate_adam']) hyperparameters = { 'input_size': input_size, 'output_size': ranges['output_size'], 'batch_norm': batch_norm, 'use_pooling': use_pooling, 'pooling_method': ranges['pooling_method'], 'conv1_kernel_size': conv1_kernel_size, 'conv1_num_kernels': conv1_num_kernels, 'conv1_stride': conv1_stride, 'conv1_dropout': conv1_dropout, 'pool1_kernel_size': pool1_kernel_size, 'pool1_stride': pool1_stride, 'conv2_kernel_size': conv2_kernel_size, 'conv2_num_kernels': conv2_num_kernels, 'conv2_stride': conv2_stride, 'conv2_dropout': conv2_dropout, 'pool2_kernel_size': pool2_kernel_size, 'pool2_stride': pool2_stride, 'fcs_hidden_size': fcs_hidden_size, 'fcs_num_hidden_layers': fcs_num_hidden_layers, 'fcs_dropout': fcs_dropout, 'cost_function': cost_function, 'optimizer': optimizer, 'learning_rate': learning_rate, 'momentum': momentum, } return hyperparameters def create_models(num_networks, hyperparameter_ranges_file): identifier = datetime.datetime.now().strftime('%Y%m%d%H%M%S') data_is_target_list = [0] num_scat_list = [1, 2, 3] batch_size_list = [32] data_noise_gaussian_list = [0, 1] #dropout_input_list = [0, 0.1, 0.2] #dropout_list = [0, 0.1, 0.2, 0.3, 0.4, 0.5] weight_decay_list = [0] for count in range(num_networks): data_is_target = random.choice(data_is_target_list) n_scat = random.choice(num_scat_list) bs = random.choice(batch_size_list) data_noise_gaussian = random.choice(data_noise_gaussian_list) #dropout_input = random.choice(dropout_input_list) weight_decay = random.choice(weight_decay_list) # get params model_params = choose_hyperparameters_from_file(hyperparameter_ranges_file) # set other params model_params['data_is_target'] = data_is_target home = os.path.expanduser('~') model_params['data_train'] = os.path.join(home,'Downloads', '20180402_L74_70mm', 'train_' + str(n_scat) + '.h5') model_params['data_val'] = os.path.join(home, 'Downloads', '20180402_L74_70mm', 'val_' + str(n_scat) + '.h5') model_params['batch_size'] = bs model_params['data_noise_gaussian'] = data_noise_gaussian #model_params['dropout_input'] = dropout_input model_params['weight_decay'] = weight_decay model_params['patience'] = 20 model_params['cuda'] = 1 model_params['save_initial'] = 0 k_list = [3, 4, 5] for k in k_list: model_params['k'] = k model_params['save_dir'] = os.path.join('DNNs', identifier + '_' + str(count+1) + '_created', 'k_' + str(k)) # print(model_params['save_dir']) ensure_dir(model_params['save_dir']) save_model_params(os.path.join(model_params['save_dir'], 'model_params.txt'), model_params) return identifier def main(): # parse input arguments parser = argparse.ArgumentParser() parser.add_argument('num_networks', type=int, help='The number of networks to train.') parser.add_argument('hyperparameter_ranges_file', type=str, help='The number of networks to train.') args = parser.parse_args() num_networks = args.num_networks hyperparameter_ranges_file = args.hyperparameter_ranges_file return create_models(num_networks, hyperparameter_ranges_file) if __name__ == '__main__': main()
Вот один из примеров моего файла диапазона гиперпарам, hyperparam_ranges.json:
{ "input_size": 65, "output_size": 130, "batch_norm": [0, 1], "use_pooling": [0, 1], "pooling_method": "max", "conv1_kernel_size": [6, 32], "conv1_num_kernels": [8, 51], "conv1_stride": [1], "conv1_dropout": [0, 0], "pool1_kernel_size": [2, 3], "pool1_stride": [2], "conv2_kernel_size": [2, 20], "conv2_num_kernels": [2, 40], "conv2_stride": [1], "conv2_dropout": [0, 1], "pool2_kernel_size": [2], "pool2_stride": [2], "fcs_hidden_size": [25, 520], "fcs_num_hidden_layers": [1, 4], "fcs_dropout": [0, 1], "cost_function": ["MSE"], "optimizer": ["Adam", "SGD"], "momentum": [0.8, 1], "learning_rate_adam": [0, 0.0002], "learning_rate_sgd": [0, 0.02] }
Наконец, мы обучаем созданные случайные модели стандартным обучающим скриптом:
# train.py # Usage: python train.py "*" # python train.py 201807201428 import torch import os import numpy as np import time import argparse import glob import warnings from pprint import pprint from utils import read_model_params, save_model_params, ensure_dir, add_suffix_to_path from dataloader import ApertureDataset from lenet import LeNet from logger import Logger from trainer import Trainer def train(identifier): models = glob.glob(os.path.join('DNNs', str(identifier) + '_created')) for model_folder in models: ks = glob.glob(os.path.join(model_folder, 'k_*')) for k in ks: model_params_path = k + '/model_params.txt' print('train.py: training model', model_params_path, 'with hyperparams') # load model params. model_params = read_model_params(model_params_path) # print model and training parameters. pprint(model_params) # cuda flag using_cuda = model_params['cuda'] and torch.cuda.is_available() if using_cuda == True: print('train.py: Using ' + str(torch.cuda.get_device_name(0))) else: warnings.warn('train.py: Not using CUDA') # Load primary training data num_samples = 10 ** 5 dat_train = ApertureDataset(model_params['data_train'], num_samples, model_params['k'], model_params['data_is_target']) loader_train = torch.utils.data.DataLoader(dat_train, batch_size=model_params['batch_size'], shuffle=True, num_workers=1) # Load secondary training data - used to evaluate training loss after every epoch num_samples = 10 ** 4 dat_train2 = ApertureDataset(model_params['data_train'], num_samples, model_params['k'], model_params['data_is_target']) loader_train_eval = torch.utils.data.DataLoader(dat_train2, batch_size=model_params['batch_size'], shuffle=False, num_workers=1) # Load validation data - used to evaluate validation loss after every epoch num_samples = 10 ** 4 dat_val = ApertureDataset(model_params['data_val'], num_samples, model_params['k'], model_params['data_is_target']) loader_val = torch.utils.data.DataLoader(dat_val, batch_size=model_params['batch_size'], shuffle=False, num_workers=1) # create model model = LeNet(model_params['input_size'], model_params['output_size'], model_params['batch_norm'], model_params['use_pooling'], model_params['pooling_method'], model_params['conv1_kernel_size'], model_params['conv1_num_kernels'], model_params['conv1_stride'], model_params['conv1_dropout'], model_params['pool1_kernel_size'], model_params['pool1_stride'], model_params['conv2_kernel_size'], model_params['conv2_num_kernels'], model_params['conv2_stride'], model_params['conv2_dropout'], model_params['pool2_kernel_size'], model_params['pool2_stride'], model_params['fcs_hidden_size'], model_params['fcs_num_hidden_layers'], model_params['fcs_dropout']) if using_cuda == True: model.cuda() # save initial weights if model_params['save_initial'] and model_params['save_dir']: suffix = '_initial' path = add_suffix_to_path(model_parmas['save_dir'], suffix) print('Saving model weights in : ' + path) ensure_dir(path) torch.save(model.state_dict(), os.path.join(path, 'model.dat')) save_model_params(os.path.join(path, 'model_params.txt'), model_params) # loss loss = torch.nn.MSELoss() # optimizer if model_params['optimizer'] == 'Adam': optimizer = torch.optim.Adam(model.parameters(), lr=model_params['learning_rate'], weight_decay=model_params['weight_decay']) elif model_params['optimizer'] == 'SGD': optimizer = torch.optim.SGD(model.parameters(), lr=model_params['learning_rate'], momentum=model_params['momentum'], weight_decay=model_params['weight_decay']) else: raise ValueError('model_params[\'optimizer\'] must be either Adam or SGD. Got ' + model_params['optimizer']) logger = Logger() trainer = Trainer(model=model, loss=loss, optimizer=optimizer, patience=model_params['patience'], loader_train=loader_train, loader_train_eval=loader_train_eval, loader_val=loader_val, cuda=using_cuda, logger=logger, data_noise_gaussian=model_params['data_noise_gaussian'], save_dir=model_params['save_dir']) # run training trainer.train() os.rename(model_folder, model_folder.replace('_created', '_trained')) def main(): parser = argparse.ArgumentParser() parser.add_argument('identifier', help='Option to load model params from a file. Values in this file take precedence.') args = parser.parse_args() identifier = args.identifier train(identifier) if __name__ == '__main__': main()
Что вы думаете? Пожалуйста, поделитесь своими комментариями и отзывами!