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.

rl.py 16 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. # codes in this file are reproduced from https://github.com/microsoft/nni with some changes.
  2. import copy
  3. import logging
  4. import torch
  5. import torch.nn as nn
  6. import torch.nn.functional as F
  7. from .base import BaseNAS
  8. from ..space import BaseSpace
  9. from ..utils import AverageMeterGroup, replace_layer_choice, replace_input_choice, get_module_order, sort_replaced_module
  10. from nni.nas.pytorch.fixed import apply_fixed_architecture
  11. from tqdm import tqdm
  12. _logger = logging.getLogger(__name__)
  13. def _get_mask(sampled, total):
  14. multihot = [i == sampled or (isinstance(sampled, list) and i in sampled) for i in range(total)]
  15. return torch.tensor(multihot, dtype=torch.bool) # pylint: disable=not-callable
  16. class PathSamplingLayerChoice(nn.Module):
  17. """
  18. Mixed module, in which fprop is decided by exactly one or multiple (sampled) module.
  19. If multiple module is selected, the result will be sumed and returned.
  20. Attributes
  21. ----------
  22. sampled : int or list of int
  23. Sampled module indices.
  24. mask : tensor
  25. A multi-hot bool 1D-tensor representing the sampled mask.
  26. """
  27. def __init__(self, layer_choice):
  28. super(PathSamplingLayerChoice, self).__init__()
  29. self.op_names = []
  30. for name, module in layer_choice.named_children():
  31. self.add_module(name, module)
  32. self.op_names.append(name)
  33. assert self.op_names, 'There has to be at least one op to choose from.'
  34. self.sampled = None # sampled can be either a list of indices or an index
  35. def forward(self, *args, **kwargs):
  36. assert self.sampled is not None, 'At least one path needs to be sampled before fprop.'
  37. if isinstance(self.sampled, list):
  38. return sum([getattr(self, self.op_names[i])(*args, **kwargs) for i in self.sampled]) # pylint: disable=not-an-iterable
  39. else:
  40. return getattr(self, self.op_names[self.sampled])(*args, **kwargs) # pylint: disable=invalid-sequence-index
  41. def __len__(self):
  42. return len(self.op_names)
  43. @property
  44. def mask(self):
  45. return _get_mask(self.sampled, len(self))
  46. class PathSamplingInputChoice(nn.Module):
  47. """
  48. Mixed input. Take a list of tensor as input, select some of them and return the sum.
  49. Attributes
  50. ----------
  51. sampled : int or list of int
  52. Sampled module indices.
  53. mask : tensor
  54. A multi-hot bool 1D-tensor representing the sampled mask.
  55. """
  56. def __init__(self, input_choice):
  57. super(PathSamplingInputChoice, self).__init__()
  58. self.n_candidates = input_choice.n_candidates
  59. self.n_chosen = input_choice.n_chosen
  60. self.sampled = None
  61. def forward(self, input_tensors):
  62. if isinstance(self.sampled, list):
  63. return sum([input_tensors[t] for t in self.sampled]) # pylint: disable=not-an-iterable
  64. else:
  65. return input_tensors[self.sampled]
  66. def __len__(self):
  67. return self.n_candidates
  68. @property
  69. def mask(self):
  70. return _get_mask(self.sampled, len(self))
  71. def __repr__(self):
  72. return f'PathSamplingInputChoice(n_candidates={self.n_candidates}, chosen={self.sampled})'
  73. class StackedLSTMCell(nn.Module):
  74. def __init__(self, layers, size, bias):
  75. super().__init__()
  76. self.lstm_num_layers = layers
  77. self.lstm_modules = nn.ModuleList([nn.LSTMCell(size, size, bias=bias)
  78. for _ in range(self.lstm_num_layers)])
  79. def forward(self, inputs, hidden):
  80. prev_h, prev_c = hidden
  81. next_h, next_c = [], []
  82. for i, m in enumerate(self.lstm_modules):
  83. curr_h, curr_c = m(inputs, (prev_h[i], prev_c[i]))
  84. next_c.append(curr_c)
  85. next_h.append(curr_h)
  86. # current implementation only supports batch size equals 1,
  87. # but the algorithm does not necessarily have this limitation
  88. inputs = curr_h[-1].view(1, -1)
  89. return next_h, next_c
  90. class ReinforceField:
  91. """
  92. A field with ``name``, with ``total`` choices. ``choose_one`` is true if one and only one is meant to be
  93. selected. Otherwise, any number of choices can be chosen.
  94. """
  95. def __init__(self, name, total, choose_one):
  96. self.name = name
  97. self.total = total
  98. self.choose_one = choose_one
  99. def __repr__(self):
  100. return f'ReinforceField(name={self.name}, total={self.total}, choose_one={self.choose_one})'
  101. class ReinforceController(nn.Module):
  102. """
  103. A controller that mutates the graph with RL.
  104. Parameters
  105. ----------
  106. fields : list of ReinforceField
  107. List of fields to choose.
  108. lstm_size : int
  109. Controller LSTM hidden units.
  110. lstm_num_layers : int
  111. Number of layers for stacked LSTM.
  112. tanh_constant : float
  113. Logits will be equal to ``tanh_constant * tanh(logits)``. Don't use ``tanh`` if this value is ``None``.
  114. skip_target : float
  115. Target probability that skipconnect will appear.
  116. temperature : float
  117. Temperature constant that divides the logits.
  118. entropy_reduction : str
  119. Can be one of ``sum`` and ``mean``. How the entropy of multi-input-choice is reduced.
  120. """
  121. def __init__(self, fields, lstm_size=64, lstm_num_layers=1, tanh_constant=1.5,
  122. skip_target=0.4, temperature=None, entropy_reduction='sum'):
  123. super(ReinforceController, self).__init__()
  124. self.fields = fields
  125. self.lstm_size = lstm_size
  126. self.lstm_num_layers = lstm_num_layers
  127. self.tanh_constant = tanh_constant
  128. self.temperature = temperature
  129. self.skip_target = skip_target
  130. self.lstm = StackedLSTMCell(self.lstm_num_layers, self.lstm_size, False)
  131. self.attn_anchor = nn.Linear(self.lstm_size, self.lstm_size, bias=False)
  132. self.attn_query = nn.Linear(self.lstm_size, self.lstm_size, bias=False)
  133. self.v_attn = nn.Linear(self.lstm_size, 1, bias=False)
  134. self.g_emb = nn.Parameter(torch.randn(1, self.lstm_size) * 0.1)
  135. self.skip_targets = nn.Parameter(torch.tensor([1.0 - self.skip_target, self.skip_target]), # pylint: disable=not-callable
  136. requires_grad=False)
  137. assert entropy_reduction in ['sum', 'mean'], 'Entropy reduction must be one of sum and mean.'
  138. self.entropy_reduction = torch.sum if entropy_reduction == 'sum' else torch.mean
  139. self.cross_entropy_loss = nn.CrossEntropyLoss(reduction='none')
  140. self.soft = nn.ModuleDict({
  141. field.name: nn.Linear(self.lstm_size, field.total, bias=False) for field in fields
  142. })
  143. self.embedding = nn.ModuleDict({
  144. field.name: nn.Embedding(field.total, self.lstm_size) for field in fields
  145. })
  146. def resample(self):
  147. self._initialize()
  148. result = dict()
  149. for field in self.fields:
  150. result[field.name] = self._sample_single(field)
  151. return result
  152. def _initialize(self):
  153. self._inputs = self.g_emb.data
  154. self._c = [torch.zeros((1, self.lstm_size),
  155. dtype=self._inputs.dtype,
  156. device=self._inputs.device) for _ in range(self.lstm_num_layers)]
  157. self._h = [torch.zeros((1, self.lstm_size),
  158. dtype=self._inputs.dtype,
  159. device=self._inputs.device) for _ in range(self.lstm_num_layers)]
  160. self.sample_log_prob = 0
  161. self.sample_entropy = 0
  162. self.sample_skip_penalty = 0
  163. def _lstm_next_step(self):
  164. self._h, self._c = self.lstm(self._inputs, (self._h, self._c))
  165. def _sample_single(self, field):
  166. self._lstm_next_step()
  167. logit = self.soft[field.name](self._h[-1])
  168. if self.temperature is not None:
  169. logit /= self.temperature
  170. if self.tanh_constant is not None:
  171. logit = self.tanh_constant * torch.tanh(logit)
  172. if field.choose_one:
  173. sampled = torch.multinomial(F.softmax(logit, dim=-1), 1).view(-1)
  174. log_prob = self.cross_entropy_loss(logit, sampled)
  175. self._inputs = self.embedding[field.name](sampled)
  176. else:
  177. logit = logit.view(-1, 1)
  178. logit = torch.cat([-logit, logit], 1) # pylint: disable=invalid-unary-operand-type
  179. sampled = torch.multinomial(F.softmax(logit, dim=-1), 1).view(-1)
  180. skip_prob = torch.sigmoid(logit)
  181. kl = torch.sum(skip_prob * torch.log(skip_prob / self.skip_targets))
  182. self.sample_skip_penalty += kl
  183. log_prob = self.cross_entropy_loss(logit, sampled)
  184. sampled = sampled.nonzero().view(-1)
  185. if sampled.sum().item():
  186. self._inputs = (torch.sum(self.embedding[field.name](sampled.view(-1)), 0) / (1. + torch.sum(sampled))).unsqueeze(0)
  187. else:
  188. self._inputs = torch.zeros(1, self.lstm_size, device=self.embedding[field.name].weight.device)
  189. sampled = sampled.detach().numpy().tolist()
  190. self.sample_log_prob += self.entropy_reduction(log_prob)
  191. entropy = (log_prob * torch.exp(-log_prob)).detach() # pylint: disable=invalid-unary-operand-type
  192. self.sample_entropy += self.entropy_reduction(entropy)
  193. if len(sampled) == 1:
  194. sampled = sampled[0]
  195. return sampled
  196. class RL(BaseNAS):
  197. """
  198. ENAS trainer.
  199. Parameters
  200. ----------
  201. model : nn.Module
  202. PyTorch model to be trained.
  203. loss : callable
  204. Receives logits and ground truth label, return a loss tensor.
  205. metrics : callable
  206. Receives logits and ground truth label, return a dict of metrics.
  207. reward_function : callable
  208. Receives logits and ground truth label, return a tensor, which will be feeded to RL controller as reward.
  209. optimizer : Optimizer
  210. The optimizer used for optimizing the model.
  211. num_epochs : int
  212. Number of epochs planned for training.
  213. dataset : Dataset
  214. Dataset for training. Will be split for training weights and architecture weights.
  215. batch_size : int
  216. Batch size.
  217. workers : int
  218. Workers for data loading.
  219. device : torch.device
  220. ``torch.device("cpu")`` or ``torch.device("cuda")``.
  221. log_frequency : int
  222. Step count per logging.
  223. grad_clip : float
  224. Gradient clipping. Set to 0 to disable. Default: 5.
  225. entropy_weight : float
  226. Weight of sample entropy loss.
  227. skip_weight : float
  228. Weight of skip penalty loss.
  229. baseline_decay : float
  230. Decay factor of baseline. New baseline will be equal to ``baseline_decay * baseline_old + reward * (1 - baseline_decay)``.
  231. ctrl_lr : float
  232. Learning rate for RL controller.
  233. ctrl_steps_aggregate : int
  234. Number of steps that will be aggregated into one mini-batch for RL controller.
  235. ctrl_steps : int
  236. Number of mini-batches for each epoch of RL controller learning.
  237. ctrl_kwargs : dict
  238. Optional kwargs that will be passed to :class:`ReinforceController`.
  239. """
  240. def __init__(self, device='cuda', workers=4,log_frequency=None,
  241. grad_clip=5., entropy_weight=0.0001, skip_weight=0.8, baseline_decay=0.999,
  242. ctrl_lr=0.00035, ctrl_steps_aggregate=20, ctrl_kwargs=None,n_warmup=100,model_lr=5e-3,model_wd=5e-4,*args,**kwargs):
  243. super().__init__(device)
  244. self.device=device
  245. self.num_epochs = kwargs.get("num_epochs", 5)
  246. self.workers = workers
  247. self.log_frequency = log_frequency
  248. self.entropy_weight = entropy_weight
  249. self.skip_weight = skip_weight
  250. self.baseline_decay = baseline_decay
  251. self.baseline = 0.
  252. self.ctrl_steps_aggregate = ctrl_steps_aggregate
  253. self.grad_clip = grad_clip
  254. self.workers = workers
  255. self.ctrl_kwargs=ctrl_kwargs
  256. self.ctrl_lr=ctrl_lr
  257. self.n_warmup=n_warmup
  258. self.model_lr = model_lr
  259. self.model_wd = model_wd
  260. self.log=open('log.txt','w')
  261. def search(self, space: BaseSpace, dset, estimator):
  262. self.model = space
  263. self.dataset = dset#.to(self.device)
  264. self.estimator = estimator
  265. # replace choice
  266. self.nas_modules = []
  267. k2o = get_module_order(self.model)
  268. replace_layer_choice(self.model, PathSamplingLayerChoice, self.nas_modules)
  269. replace_input_choice(self.model, PathSamplingInputChoice, self.nas_modules)
  270. self.nas_modules = sort_replaced_module(k2o, self.nas_modules)
  271. # to device
  272. self.model = self.model.to(self.device)
  273. # fields
  274. self.nas_fields = [ReinforceField(name, len(module),
  275. isinstance(module, PathSamplingLayerChoice) or module.n_chosen == 1)
  276. for name, module in self.nas_modules]
  277. self.controller = ReinforceController(self.nas_fields, **(self.ctrl_kwargs or {}))
  278. self.ctrl_optim = torch.optim.Adam(self.controller.parameters(), lr=self.ctrl_lr)
  279. # train
  280. with tqdm(range(self.num_epochs)) as bar:
  281. for i in bar:
  282. l2=self._train_controller(i)
  283. # try:
  284. # l2=self._train_controller(i)
  285. # except Exception as e:
  286. # print(e)
  287. # nm=self.nas_modules
  288. # for i in range(len(nm)):
  289. # print(nm[i][1].sampled)
  290. # # import pdb
  291. # # pdb.set_trace()
  292. bar.set_postfix(reward_controller=l2)
  293. selection=self.export()
  294. arch=space.export(selection,self.device)
  295. print(selection,arch)
  296. return arch
  297. def _train_controller(self, epoch):
  298. self.model.eval()
  299. self.controller.train()
  300. self.ctrl_optim.zero_grad()
  301. rewards=[]
  302. with tqdm(range(self.ctrl_steps_aggregate)) as bar:
  303. for ctrl_step in bar:
  304. self._resample()
  305. metric,loss=self._infer(mask='val')
  306. bar.set_postfix(acc=metric,loss=loss.item())
  307. self.log.write(f'{self.arch}\n{self.selection}\n{metric},{loss}\n')
  308. self.log.flush()
  309. reward =metric
  310. rewards.append(reward)
  311. if self.entropy_weight:
  312. reward += self.entropy_weight * self.controller.sample_entropy.item()
  313. self.baseline = self.baseline * self.baseline_decay + reward * (1 - self.baseline_decay)
  314. loss = self.controller.sample_log_prob * (reward - self.baseline)
  315. if self.skip_weight:
  316. loss += self.skip_weight * self.controller.sample_skip_penalty
  317. loss /= self.ctrl_steps_aggregate
  318. loss.backward()
  319. if (ctrl_step + 1) % self.ctrl_steps_aggregate == 0:
  320. if self.grad_clip > 0:
  321. nn.utils.clip_grad_norm_(self.controller.parameters(), self.grad_clip)
  322. self.ctrl_optim.step()
  323. self.ctrl_optim.zero_grad()
  324. if self.log_frequency is not None and ctrl_step % self.log_frequency == 0:
  325. _logger.info('RL Epoch [%d/%d] Step [%d/%d] %s', epoch + 1, self.num_epochs,
  326. ctrl_step + 1, self.ctrl_steps_aggregate)
  327. return sum(rewards)/len(rewards)
  328. def _resample(self):
  329. result = self.controller.resample()
  330. self.arch=self.model.export(result,device=self.device)
  331. self.selection=result
  332. def export(self):
  333. self.controller.eval()
  334. with torch.no_grad():
  335. return self.controller.resample()
  336. def _infer(self,mask='train'):
  337. metric, loss = self.estimator.infer(self.arch, self.dataset,mask=mask)
  338. return metric, loss