You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

spos.py 11 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. # codes in this file are reproduced from https://github.com/microsoft/nni with some changes.
  2. import copy
  3. from logging import Logger
  4. from numpy.core.fromnumeric import sort
  5. import torch
  6. import torch.nn as nn
  7. import torch.nn.functional as F
  8. from . import register_nas_algo
  9. from .base import BaseNAS
  10. from ..space import BaseSpace
  11. from ..utils import (
  12. AverageMeterGroup,
  13. replace_layer_choice,
  14. replace_input_choice,
  15. get_module_order,
  16. sort_replaced_module,
  17. PathSamplingLayerChoice,
  18. PathSamplingInputChoice,
  19. )
  20. from tqdm import tqdm, trange
  21. from ....utils import get_logger
  22. from nni.algorithms.nas.pytorch.random import RandomMutator
  23. from nni.retiarii.strategy import RegularizedEvolution
  24. import numpy as np
  25. LOGGER = get_logger("SPOS")
  26. import collections
  27. import dataclasses
  28. import random
  29. @dataclasses.dataclass
  30. class Individual:
  31. """
  32. A class that represents an individual.
  33. Holds two attributes, where ``x`` is the model and ``y`` is the metric (e.g., accuracy).
  34. """
  35. x: dict
  36. y: float
  37. class Evolution:
  38. """
  39. Algorithm for regularized evolution (i.e. aging evolution).
  40. Follows "Algorithm 1" in Real et al. "Regularized Evolution for Image Classifier Architecture Search".
  41. Parameters
  42. ----------
  43. optimize_mode : str
  44. Can be one of "maximize" and "minimize". Default: maximize.
  45. population_size : int
  46. The number of individuals to keep in the population. Default: 100.
  47. cycles : int
  48. The number of cycles (trials) the algorithm should run for. Default: 20000.
  49. sample_size : int
  50. The number of individuals that should participate in each tournament. Default: 25.
  51. mutation_prob : float
  52. Probability that mutation happens in each dim. Default: 0.05
  53. """
  54. def __init__(self, optimize_mode='maximize', population_size=100, sample_size=25, cycles=20000,
  55. mutation_prob=0.05,disable_progress=False):
  56. assert optimize_mode in ['maximize', 'minimize']
  57. assert sample_size < population_size
  58. self.optimize_mode = optimize_mode
  59. self.population_size = population_size
  60. self.sample_size = sample_size
  61. self.cycles = cycles
  62. self.mutation_prob = mutation_prob
  63. self.disable_progress= disable_progress
  64. self._worst = float('-inf') if self.optimize_mode == 'maximize' else float('inf')
  65. self._success_count = 0
  66. self._population = collections.deque()
  67. self._running_models = []
  68. self._polling_interval = 2.
  69. self._history = []
  70. def best_parent(self,sample_size=None):
  71. """get the config of the best parent
  72. """
  73. samples = [p for p in self._population] # copy population
  74. random.shuffle(samples)
  75. if sample_size is not None:
  76. samples = list(samples)[:sample_size]
  77. if self.optimize_mode == 'maximize':
  78. parent = max(samples, key=lambda sample: sample.y)
  79. else:
  80. parent = min(samples, key=lambda sample: sample.y)
  81. return parent.x
  82. def _prepare(self):
  83. self.uniform=UniformSampler(self.nas_modules)
  84. self.mutation=MutationSampler(self.nas_modules,self.mutation_prob)
  85. def _get_metric(self,config):
  86. for name, module in self.nas_modules:
  87. module.sampled = config[name]
  88. # todo: this may be computational expensive
  89. # model=self.model.parse_model(config,self.device)
  90. with torch.no_grad():
  91. metric, loss = self.estimator.infer(self.model, self.dataset, mask='val')
  92. return metric[0]
  93. def search(self, space: BaseSpace,nas_modules,dset,estimator,device):
  94. self.model = space
  95. self.dataset = dset
  96. self.estimator = estimator
  97. self.nas_modules = nas_modules
  98. self.device = device
  99. self._prepare()
  100. LOGGER.info('Initializing the first population.')
  101. with tqdm(range(self.population_size), disable=self.disable_progress) as bar:
  102. for i in bar:
  103. config = self.uniform.resample()
  104. metric=self._get_metric(config)
  105. individual = Individual(config, metric)
  106. # LOGGER.debug('Individual created: %s', str(individual))
  107. self._population.append(individual)
  108. self._history.append(individual)
  109. bar.set_postfix(metric=metric,max=max(x.y for x in self._population),min=min(x.y for x in self._population))
  110. LOGGER.info('Running mutations.')
  111. with tqdm(range(self.cycles), disable=self.disable_progress) as bar:
  112. for i in bar:
  113. parent=self.best_parent(self.sample_size)
  114. config=self.mutation.resample(parent)
  115. metric=self._get_metric(config) # todo : add aging factor
  116. individual = Individual(config, metric)
  117. LOGGER.debug('Individual created: %s', str(individual))
  118. self._population.append(individual)
  119. self._history.append(individual)
  120. if len(self._population) > self.population_size:
  121. self._population.popleft()
  122. bar.set_postfix(metric=metric,max_h=max(x.y for x in self._history),max=max(x.y for x in self._population),min=min(x.y for x in self._population))
  123. # todo: origin is best in history | or the population may need to be retrained
  124. self._history.sort(key=lambda x: x.y)
  125. # best=self.best_parent()
  126. if self.optimize_mode == 'maximize':
  127. best=self._history[-1].x
  128. else:
  129. best=self._history[0].x
  130. return best
  131. class MutationSampler:
  132. """uniform mutator
  133. Parameters
  134. ----------
  135. nas_modules:
  136. nas_modules in NAS algorithms , including choices of modules
  137. mutation_prob: float
  138. probability of doing mutation in each choice.
  139. parent : dict
  140. parent individual's choices
  141. """
  142. def __init__(self,nas_modules,mutation_prob):
  143. selection_range = {}
  144. for k, v in nas_modules:
  145. selection_range[k] = len(v)
  146. self.selection_dict = selection_range
  147. self.mutation_prob = mutation_prob
  148. def resample(self, parent):
  149. search_space=self.selection_dict
  150. child = {}
  151. for k, v in parent.items():
  152. if random.uniform(0, 1) < self.mutation_prob:
  153. child[k] = np.random.choice(range(search_space[k])) # do not exclude the original operator
  154. else:
  155. child[k] = v
  156. return child
  157. class UniformSampler:
  158. """Uniform Sampler
  159. Parameters
  160. ----------
  161. nas_modules:
  162. nas_modules in NAS algorithms , including choices of modules
  163. """
  164. def __init__(self,nas_modules):
  165. selection_range = {}
  166. for k, v in nas_modules:
  167. selection_range[k] = len(v)
  168. self.selection_dict = selection_range
  169. def resample(self):
  170. selection = {}
  171. for k, v in self.selection_dict.items():
  172. selection[k] = np.random.choice(range(v))
  173. return selection
  174. @register_nas_algo("spos")
  175. class Spos(BaseNAS):
  176. """
  177. SPOS trainer.
  178. Parameters
  179. ----------
  180. n_warmup : int
  181. Number of epochs for training super network.
  182. model_lr : float
  183. Learning rate for super network.
  184. model_wd : float
  185. Weight decay for super network.
  186. Other parameters see Evolution
  187. """
  188. def __init__(
  189. self,
  190. n_warmup=1000,
  191. grad_clip=5.0,
  192. disable_progress=False,
  193. optimize_mode='maximize',
  194. population_size=100,
  195. sample_size=25,
  196. cycles=20000,
  197. mutation_prob=0.05,
  198. device="cuda",
  199. ):
  200. super().__init__(device)
  201. self.model_lr=5e-3
  202. self.model_wd=5e-4
  203. self.n_warmup = n_warmup
  204. self.disable_progress= disable_progress
  205. self.grad_clip = grad_clip
  206. self.optimize_mode = optimize_mode
  207. self.population_size = population_size
  208. self.sample_size = sample_size
  209. self.cycles = cycles
  210. self.mutation_prob = mutation_prob
  211. def _prepare(self):
  212. # replace choice
  213. self.nas_modules = []
  214. k2o = get_module_order(self.model)
  215. replace_layer_choice(self.model, PathSamplingLayerChoice, self.nas_modules)
  216. replace_input_choice(self.model, PathSamplingInputChoice, self.nas_modules)
  217. self.nas_modules = sort_replaced_module(k2o, self.nas_modules)
  218. # to device
  219. self.model = self.model.to(self.device)
  220. self.model_optim = torch.optim.Adam(
  221. self.model.parameters(), lr=self.model_lr, weight_decay=self.model_wd
  222. )
  223. # controller
  224. self.controller=UniformSampler(self.nas_modules)
  225. # Evolution
  226. self.evolve = Evolution(
  227. optimize_mode='maximize',
  228. population_size=self.population_size,
  229. sample_size=self.sample_size,
  230. cycles=self.cycles,
  231. mutation_prob=self.mutation_prob,
  232. disable_progress=self.disable_progress
  233. )
  234. def search(self, space: BaseSpace, dset, estimator):
  235. self.model = space
  236. self.dataset = dset
  237. self.estimator = estimator
  238. self._prepare()
  239. self._train() # train using uniform sampling
  240. self._search() # search using evolutionary algorithm
  241. selection = self.export()
  242. # here may sample N , retrain N ,and get best
  243. print(selection)
  244. return space.parse_model(selection, self.device)
  245. def _search(self):
  246. self.best_config=self.evolve.search(
  247. self.model,
  248. self.nas_modules,
  249. self.dataset,
  250. self.estimator,
  251. self.device,
  252. )
  253. def _train(self):
  254. with tqdm(range(self.n_warmup), disable=self.disable_progress) as bar:
  255. for i in bar:
  256. acc, l1 = self._train_one_epoch(i)
  257. with torch.no_grad():
  258. val_acc, val_loss = self._infer("val")
  259. bar.set_postfix(loss=l1, acc=acc, val_acc=val_acc, val_loss=val_loss.item())
  260. def _train_one_epoch(self, epoch):
  261. self.model.train()
  262. self.model_optim.zero_grad()
  263. self._resample() # uniform sampling
  264. metric, loss = self._infer(mask="train")
  265. loss.backward()
  266. if self.grad_clip > 0:
  267. nn.utils.clip_grad_norm_(self.model.parameters(), self.grad_clip)
  268. self.model_optim.step()
  269. return metric, loss.item()
  270. def _resample(self):
  271. result=self.controller.resample()
  272. for name, module in self.nas_modules:
  273. module.sampled = result[name]
  274. def export(self):
  275. return self.best_config
  276. def _infer(self, mask="train"):
  277. metric, loss = self.estimator.infer(self.model, self.dataset, mask=mask)
  278. return metric[0], loss