From 3cd0367a236db9ec6db493fa2cf361ea1295947b Mon Sep 17 00:00:00 2001 From: wondergo2017 Date: Thu, 6 May 2021 03:42:15 +0000 Subject: [PATCH 1/6] fix zeroconv bug --- autogl/module/nas/algorithm/enas.py | 23 ++++++++++++++++++++--- autogl/module/nas/space/graph_nas.py | 20 +++++++++++++++++--- autogl/module/nas/space/single_path.py | 2 ++ examples/test_graph_nas.py | 4 ++-- 4 files changed, 41 insertions(+), 8 deletions(-) diff --git a/autogl/module/nas/algorithm/enas.py b/autogl/module/nas/algorithm/enas.py index 715139a..8b66f5e 100644 --- a/autogl/module/nas/algorithm/enas.py +++ b/autogl/module/nas/algorithm/enas.py @@ -10,6 +10,7 @@ from .base import BaseNAS from ..space import BaseSpace from ..utils import AverageMeterGroup, replace_layer_choice, replace_input_choice, get_module_order, sort_replaced_module from nni.nas.pytorch.fixed import apply_fixed_architecture +from tqdm import tqdm _logger = logging.getLogger(__name__) def _get_mask(sampled, total): multihot = [i == sampled or (isinstance(sampled, list) and i in sampled) for i in range(total)] @@ -312,9 +313,21 @@ class Enas(BaseNAS): self.controller = ReinforceController(self.nas_fields, **(self.ctrl_kwargs or {})) self.ctrl_optim = torch.optim.Adam(self.controller.parameters(), lr=self.ctrl_lr) # train - for i in range(self.num_epochs): - self._train_model(i) - self._train_controller(i) + with tqdm(range(self.num_epochs)) as bar: + for i in bar: + try: + l1=self._train_model(i) + l2=self._train_controller(i) + except Exception as e: + print(e) + nm=self.nas_modules + for i in range(len(nm)): + print(nm[i][1].sampled) + import pdb + pdb.set_trace() + + + bar.set_postfix(loss_model=l1,reward_controller=l2) selection=self.export() return space.export(selection,self.device) @@ -329,16 +342,19 @@ class Enas(BaseNAS): if self.grad_clip > 0: nn.utils.clip_grad_norm_(self.model.parameters(), self.grad_clip) self.model_optim.step() + return loss.item() def _train_controller(self, epoch): self.model.eval() self.controller.train() self.ctrl_optim.zero_grad() + rewards=[] for ctrl_step in range(self.ctrl_steps_aggregate): self._resample() with torch.no_grad(): metric,loss=self._infer() reward =-metric # todo : now metric is loss + rewards.append(reward) if self.entropy_weight: reward += self.entropy_weight * self.controller.sample_entropy.item() self.baseline = self.baseline * self.baseline_decay + reward * (1 - self.baseline_decay) @@ -357,6 +373,7 @@ class Enas(BaseNAS): if self.log_frequency is not None and ctrl_step % self.log_frequency == 0: _logger.info('RL Epoch [%d/%d] Step [%d/%d] %s', epoch + 1, self.num_epochs, ctrl_step + 1, self.ctrl_steps_aggregate) + return (sum(rewards)/len(rewards)).item() def _resample(self): result = self.controller.resample() diff --git a/autogl/module/nas/space/graph_nas.py b/autogl/module/nas/space/graph_nas.py index 447ccc9..9e02169 100644 --- a/autogl/module/nas/space/graph_nas.py +++ b/autogl/module/nas/space/graph_nas.py @@ -43,6 +43,9 @@ class LambdaModule(nn.Module): def forward(self, x): return self.lambd(x) + + def __repr__(self): + return '{}({})'.format(self.__class__.__name__,self.lambd) class StrModule(nn.Module): def __init__(self, lambd): super().__init__() @@ -50,6 +53,9 @@ class StrModule(nn.Module): def forward(self, *args,**kwargs): return self.str + + def __repr__(self): + return '{}({})'.format(self.__class__.__name__,self.str) def act_map(act): if act == "linear": return lambda x: x @@ -128,6 +134,15 @@ class LinearConv(nn.Module): self.out_channels) +from torch.autograd import Function +class ZeroConvFunc(Function): + @staticmethod + def forward(ctx,x): + return x + + @staticmethod + def backward(ctx, grad_output): + return grad_output class ZeroConv(nn.Module): def __init__(self, in_channels, @@ -138,9 +153,8 @@ class ZeroConv(nn.Module): self.out_channels = out_channels self.out_dim = out_channels - def forward(self, x, edge_index, edge_weight=None): - return torch.zeros([x.size(0), self.out_dim]).to(x.device) + return ZeroConvFunc.apply(torch.zeros([x.size(0), self.out_dim]).to(x.device)) def __repr__(self): return '{}({}, {})'.format(self.__class__.__name__, self.in_channels, @@ -202,7 +216,7 @@ class GraphNasNodeClassificationSpace(BaseSpace): node_in = getattr(self, f"in_{layer}")(prev_nodes_out) node_out= getattr(self, f"op_{layer}")(node_in,edges) prev_nodes_out.append(node_out) - if self.search_act_con: + if not self.search_act_con: x = torch.cat(prev_nodes_out[2:],dim=1) x = F.leaky_relu(x) x = F.dropout(x, p=self.dropout, training = self.training) diff --git a/autogl/module/nas/space/single_path.py b/autogl/module/nas/space/single_path.py index fcbffa2..75c9746 100644 --- a/autogl/module/nas/space/single_path.py +++ b/autogl/module/nas/space/single_path.py @@ -27,6 +27,8 @@ class FixedNodeClassificationModel(BaseModel): apply_fixed_architecture(self._model, selection, verbose=False) self.params = {"num_class": self.num_classes, "features_num": self.num_features} self.device = device + print(self._model) + print(selection) def to(self, device): if isinstance(device, (str, torch.device)): diff --git a/examples/test_graph_nas.py b/examples/test_graph_nas.py index d1e297c..49207d8 100644 --- a/examples/test_graph_nas.py +++ b/examples/test_graph_nas.py @@ -28,9 +28,9 @@ if __name__ == '__main__': feval=['acc'], loss="nll_loss", lr_scheduler_type=None,), - nas_algorithms=[Enas(num_epochs=10)], + nas_algorithms=[Enas(num_epochs=100)], #nas_algorithms=[Darts(num_epochs=200)], - nas_spaces=[GraphNasNodeClassificationSpace(hidden_dim=16, ops=[GCNConv, GCNConv],search_act_con=True)], + nas_spaces=[GraphNasNodeClassificationSpace(hidden_dim=16,search_act_con=False)], nas_estimators=[OneShotEstimator()] ) solver.fit(dataset) From 0dad422b67f01c18234b87e991c5ffb808616a0b Mon Sep 17 00:00:00 2001 From: wondergo2017 Date: Thu, 6 May 2021 03:48:02 +0000 Subject: [PATCH 2/6] fix zeroconv --- autogl/module/nas/space/graph_nas.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autogl/module/nas/space/graph_nas.py b/autogl/module/nas/space/graph_nas.py index 9e02169..95c290f 100644 --- a/autogl/module/nas/space/graph_nas.py +++ b/autogl/module/nas/space/graph_nas.py @@ -142,7 +142,7 @@ class ZeroConvFunc(Function): @staticmethod def backward(ctx, grad_output): - return grad_output + return 0 class ZeroConv(nn.Module): def __init__(self, in_channels, From a0bee2ca3525de784666b68292dad22b163f3453 Mon Sep 17 00:00:00 2001 From: wondergo2017 Date: Fri, 7 May 2021 06:15:55 +0000 Subject: [PATCH 3/6] fix classifier; fix zero conv --- autogl/module/nas/estimator/one_shot.py | 5 +++-- autogl/module/nas/space/graph_nas.py | 14 +++++++++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/autogl/module/nas/estimator/one_shot.py b/autogl/module/nas/estimator/one_shot.py index e887138..e43695c 100644 --- a/autogl/module/nas/estimator/one_shot.py +++ b/autogl/module/nas/estimator/one_shot.py @@ -3,7 +3,7 @@ import torch.nn.functional as F from ..space import BaseSpace from .base import BaseEstimator - +import torch class OneShotEstimator(BaseEstimator): """ @@ -18,4 +18,5 @@ class OneShotEstimator(BaseEstimator): pred = model(dset)[getattr(dset, f"{mask}_mask")] y = dset.y[getattr(dset, f'{mask}_mask')] loss = F.nll_loss(pred, y) - return loss, loss + acc=sum(pred.max(1)[1]==y).item()/y.size(0) + return acc, loss diff --git a/autogl/module/nas/space/graph_nas.py b/autogl/module/nas/space/graph_nas.py index 95c290f..8e392d0 100644 --- a/autogl/module/nas/space/graph_nas.py +++ b/autogl/module/nas/space/graph_nas.py @@ -114,7 +114,11 @@ def gnn_map(gnn_name, in_dim, out_dim, concat=False, bias=True) -> nn.Module: elif gnn_name == "linear": return LinearConv(in_dim, out_dim, bias=bias) elif gnn_name == "zero": - return ZeroConv(in_dim, out_dim, bias=bias) + # return ZeroConv(in_dim, out_dim, bias=bias) + return Identity() +class Identity(nn.Module): + def forward(self, x, edge_index, edge_weight=None): + return x class LinearConv(nn.Module): def __init__(self, in_channels, @@ -207,6 +211,8 @@ class GraphNasNodeClassificationSpace(BaseSpace): setattr(self,f"act",self.setLayerChoice(2*layer,[act_map_nn(a)for a in act_list],key=f"act")) setattr(self,f"concat",self.setLayerChoice(2*layer+1,map_nn(["add", "product", "concat"]) ,key=f"concat")) self._initialized = True + self.classifier1 = nn.Linear(self.hidden_dim*self.layer_number, self.output_dim) + self.classifier2 = nn.Linear(self.hidden_dim, self.output_dim) def forward(self, data): x, edges = data.x, data.edge_index # x [2708,1433] ,[2, 10556] @@ -220,6 +226,7 @@ class GraphNasNodeClassificationSpace(BaseSpace): x = torch.cat(prev_nodes_out[2:],dim=1) x = F.leaky_relu(x) x = F.dropout(x, p=self.dropout, training = self.training) + x = self.classifier1(x) else: act=getattr(self, f"act") con=getattr(self, f"concat")() @@ -236,6 +243,11 @@ class GraphNasNodeClassificationSpace(BaseSpace): x=tmp x = act(x) x = F.dropout(x, p=self.dropout, training = self.training) + if con=='concat': + x=self.classifier1(x) + else: + x=self.classifier2(x) + # set_trace() return F.log_softmax(x, dim=1) def export(self, selection, device) -> BaseModel: From 71c60ac0bfb5d8f0648b74a4d0b32b603687a9cc Mon Sep 17 00:00:00 2001 From: wondergo2017 Date: Fri, 7 May 2021 06:52:48 +0000 Subject: [PATCH 4/6] fix enas --- autogl/module/nas/algorithm/enas.py | 39 ++++++++++++++++++---------- autogl/module/nas/space/graph_nas.py | 1 - examples/test_graph_nas.py | 4 +-- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/autogl/module/nas/algorithm/enas.py b/autogl/module/nas/algorithm/enas.py index 8b66f5e..2760d24 100644 --- a/autogl/module/nas/algorithm/enas.py +++ b/autogl/module/nas/algorithm/enas.py @@ -273,7 +273,7 @@ class Enas(BaseNAS): def __init__(self, device='cuda', workers=4,log_frequency=None, grad_clip=5., entropy_weight=0.0001, skip_weight=0.8, baseline_decay=0.999, - ctrl_lr=0.00035, ctrl_steps_aggregate=20, ctrl_kwargs=None,*args,**kwargs): + ctrl_lr=0.00035, ctrl_steps_aggregate=20, ctrl_kwargs=None,n_warmup=100,model_lr=5e-3,model_wd=5e-4,*args,**kwargs): super().__init__(device) self.device=device self.num_epochs = kwargs.get("num_epochs", 5) @@ -288,14 +288,13 @@ class Enas(BaseNAS): self.workers = workers self.ctrl_kwargs=ctrl_kwargs self.ctrl_lr=ctrl_lr - + self.n_warmup=n_warmup + self.model_lr = model_lr + self.model_wd = model_wd def search(self, space: BaseSpace, dset, estimator): self.model = space self.dataset = dset#.to(self.device) - self.estimator = estimator - self.model_optim = torch.optim.SGD( - self.model.parameters(), lr=0.01, weight_decay=3e-4 - ) + self.estimator = estimator # replace choice self.nas_modules = [] @@ -306,12 +305,24 @@ class Enas(BaseNAS): # to device self.model = self.model.to(self.device) + self.model_optim = torch.optim.Adam( + self.model.parameters(), lr=self.model_lr, weight_decay=self.model_wd + ) # fields self.nas_fields = [ReinforceField(name, len(module), isinstance(module, PathSamplingLayerChoice) or module.n_chosen == 1) for name, module in self.nas_modules] self.controller = ReinforceController(self.nas_fields, **(self.ctrl_kwargs or {})) self.ctrl_optim = torch.optim.Adam(self.controller.parameters(), lr=self.ctrl_lr) + self._resample() + + # warm up supernet + with tqdm(range(self.n_warmup)) as bar: + for i in bar: + acc,l1=self._train_model(i) + with torch.no_grad(): + val_acc,val_loss=self._infer('val') + bar.set_postfix(loss=l1,acc=acc,val_acc=val_acc,val_loss=val_loss) # train with tqdm(range(self.num_epochs)) as bar: for i in bar: @@ -330,8 +341,9 @@ class Enas(BaseNAS): bar.set_postfix(loss_model=l1,reward_controller=l2) selection=self.export() + print(selection) return space.export(selection,self.device) - + def _train_model(self, epoch): self.model.train() self.controller.eval() @@ -342,7 +354,8 @@ class Enas(BaseNAS): if self.grad_clip > 0: nn.utils.clip_grad_norm_(self.model.parameters(), self.grad_clip) self.model_optim.step() - return loss.item() + + return metric,loss.item() def _train_controller(self, epoch): self.model.eval() @@ -352,8 +365,8 @@ class Enas(BaseNAS): for ctrl_step in range(self.ctrl_steps_aggregate): self._resample() with torch.no_grad(): - metric,loss=self._infer() - reward =-metric # todo : now metric is loss + metric,loss=self._infer(mask='val') + reward =metric rewards.append(reward) if self.entropy_weight: reward += self.entropy_weight * self.controller.sample_entropy.item() @@ -373,7 +386,7 @@ class Enas(BaseNAS): if self.log_frequency is not None and ctrl_step % self.log_frequency == 0: _logger.info('RL Epoch [%d/%d] Step [%d/%d] %s', epoch + 1, self.num_epochs, ctrl_step + 1, self.ctrl_steps_aggregate) - return (sum(rewards)/len(rewards)).item() + return sum(rewards)/len(rewards) def _resample(self): result = self.controller.resample() @@ -385,6 +398,6 @@ class Enas(BaseNAS): with torch.no_grad(): return self.controller.resample() - def _infer(self): - metric, loss = self.estimator.infer(self.model, self.dataset) + def _infer(self,mask='train'): + metric, loss = self.estimator.infer(self.model, self.dataset,mask=mask) return metric, loss diff --git a/autogl/module/nas/space/graph_nas.py b/autogl/module/nas/space/graph_nas.py index 8e392d0..5d44591 100644 --- a/autogl/module/nas/space/graph_nas.py +++ b/autogl/module/nas/space/graph_nas.py @@ -247,7 +247,6 @@ class GraphNasNodeClassificationSpace(BaseSpace): x=self.classifier1(x) else: x=self.classifier2(x) - # set_trace() return F.log_softmax(x, dim=1) def export(self, selection, device) -> BaseModel: diff --git a/examples/test_graph_nas.py b/examples/test_graph_nas.py index 49207d8..4f6740f 100644 --- a/examples/test_graph_nas.py +++ b/examples/test_graph_nas.py @@ -28,9 +28,9 @@ if __name__ == '__main__': feval=['acc'], loss="nll_loss", lr_scheduler_type=None,), - nas_algorithms=[Enas(num_epochs=100)], + nas_algorithms=[Enas(num_epochs=400,n_warmup=250)], #nas_algorithms=[Darts(num_epochs=200)], - nas_spaces=[GraphNasNodeClassificationSpace(hidden_dim=16,search_act_con=False)], + nas_spaces=[GraphNasNodeClassificationSpace(hidden_dim=32,search_act_con=False,layer_number=2)], nas_estimators=[OneShotEstimator()] ) solver.fit(dataset) From c13df920e3245dfb2526500070ae8926bc01666e Mon Sep 17 00:00:00 2001 From: wondergo2017 Date: Fri, 7 May 2021 08:34:53 +0000 Subject: [PATCH 5/6] add pure rl for graphnas --- autogl/module/nas/algorithm/enas.py | 216 +------------ autogl/module/nas/algorithm/rl.py | 383 ++++++++++++++++++++++++ autogl/module/nas/estimator/one_shot.py | 23 ++ autogl/module/nas/space/single_path.py | 2 - examples/test_graph_nas_rl.py | 42 +++ 5 files changed, 450 insertions(+), 216 deletions(-) create mode 100644 autogl/module/nas/algorithm/rl.py create mode 100644 examples/test_graph_nas_rl.py diff --git a/autogl/module/nas/algorithm/enas.py b/autogl/module/nas/algorithm/enas.py index 2760d24..f9962fb 100644 --- a/autogl/module/nas/algorithm/enas.py +++ b/autogl/module/nas/algorithm/enas.py @@ -12,218 +12,7 @@ from ..utils import AverageMeterGroup, replace_layer_choice, replace_input_choic from nni.nas.pytorch.fixed import apply_fixed_architecture from tqdm import tqdm _logger = logging.getLogger(__name__) -def _get_mask(sampled, total): - multihot = [i == sampled or (isinstance(sampled, list) and i in sampled) for i in range(total)] - return torch.tensor(multihot, dtype=torch.bool) # pylint: disable=not-callable - -class PathSamplingLayerChoice(nn.Module): - """ - Mixed module, in which fprop is decided by exactly one or multiple (sampled) module. - If multiple module is selected, the result will be sumed and returned. - - Attributes - ---------- - sampled : int or list of int - Sampled module indices. - mask : tensor - A multi-hot bool 1D-tensor representing the sampled mask. - """ - - def __init__(self, layer_choice): - super(PathSamplingLayerChoice, self).__init__() - self.op_names = [] - for name, module in layer_choice.named_children(): - self.add_module(name, module) - self.op_names.append(name) - assert self.op_names, 'There has to be at least one op to choose from.' - self.sampled = None # sampled can be either a list of indices or an index - - def forward(self, *args, **kwargs): - assert self.sampled is not None, 'At least one path needs to be sampled before fprop.' - if isinstance(self.sampled, list): - return sum([getattr(self, self.op_names[i])(*args, **kwargs) for i in self.sampled]) # pylint: disable=not-an-iterable - else: - return getattr(self, self.op_names[self.sampled])(*args, **kwargs) # pylint: disable=invalid-sequence-index - - def __len__(self): - return len(self.op_names) - - @property - def mask(self): - return _get_mask(self.sampled, len(self)) - - -class PathSamplingInputChoice(nn.Module): - """ - Mixed input. Take a list of tensor as input, select some of them and return the sum. - - Attributes - ---------- - sampled : int or list of int - Sampled module indices. - mask : tensor - A multi-hot bool 1D-tensor representing the sampled mask. - """ - - def __init__(self, input_choice): - super(PathSamplingInputChoice, self).__init__() - self.n_candidates = input_choice.n_candidates - self.n_chosen = input_choice.n_chosen - self.sampled = None - - def forward(self, input_tensors): - if isinstance(self.sampled, list): - return sum([input_tensors[t] for t in self.sampled]) # pylint: disable=not-an-iterable - else: - return input_tensors[self.sampled] - - def __len__(self): - return self.n_candidates - - @property - def mask(self): - return _get_mask(self.sampled, len(self)) - - -class StackedLSTMCell(nn.Module): - def __init__(self, layers, size, bias): - super().__init__() - self.lstm_num_layers = layers - self.lstm_modules = nn.ModuleList([nn.LSTMCell(size, size, bias=bias) - for _ in range(self.lstm_num_layers)]) - - def forward(self, inputs, hidden): - prev_h, prev_c = hidden - next_h, next_c = [], [] - for i, m in enumerate(self.lstm_modules): - curr_h, curr_c = m(inputs, (prev_h[i], prev_c[i])) - next_c.append(curr_c) - next_h.append(curr_h) - # current implementation only supports batch size equals 1, - # but the algorithm does not necessarily have this limitation - inputs = curr_h[-1].view(1, -1) - return next_h, next_c - - -class ReinforceField: - """ - A field with ``name``, with ``total`` choices. ``choose_one`` is true if one and only one is meant to be - selected. Otherwise, any number of choices can be chosen. - """ - - def __init__(self, name, total, choose_one): - self.name = name - self.total = total - self.choose_one = choose_one - - def __repr__(self): - return f'ReinforceField(name={self.name}, total={self.total}, choose_one={self.choose_one})' - - -class ReinforceController(nn.Module): - """ - A controller that mutates the graph with RL. - - Parameters - ---------- - fields : list of ReinforceField - List of fields to choose. - lstm_size : int - Controller LSTM hidden units. - lstm_num_layers : int - Number of layers for stacked LSTM. - tanh_constant : float - Logits will be equal to ``tanh_constant * tanh(logits)``. Don't use ``tanh`` if this value is ``None``. - skip_target : float - Target probability that skipconnect will appear. - temperature : float - Temperature constant that divides the logits. - entropy_reduction : str - Can be one of ``sum`` and ``mean``. How the entropy of multi-input-choice is reduced. - """ - - def __init__(self, fields, lstm_size=64, lstm_num_layers=1, tanh_constant=1.5, - skip_target=0.4, temperature=None, entropy_reduction='sum'): - super(ReinforceController, self).__init__() - self.fields = fields - self.lstm_size = lstm_size - self.lstm_num_layers = lstm_num_layers - self.tanh_constant = tanh_constant - self.temperature = temperature - self.skip_target = skip_target - - self.lstm = StackedLSTMCell(self.lstm_num_layers, self.lstm_size, False) - self.attn_anchor = nn.Linear(self.lstm_size, self.lstm_size, bias=False) - self.attn_query = nn.Linear(self.lstm_size, self.lstm_size, bias=False) - self.v_attn = nn.Linear(self.lstm_size, 1, bias=False) - self.g_emb = nn.Parameter(torch.randn(1, self.lstm_size) * 0.1) - self.skip_targets = nn.Parameter(torch.tensor([1.0 - self.skip_target, self.skip_target]), # pylint: disable=not-callable - requires_grad=False) - assert entropy_reduction in ['sum', 'mean'], 'Entropy reduction must be one of sum and mean.' - self.entropy_reduction = torch.sum if entropy_reduction == 'sum' else torch.mean - self.cross_entropy_loss = nn.CrossEntropyLoss(reduction='none') - self.soft = nn.ModuleDict({ - field.name: nn.Linear(self.lstm_size, field.total, bias=False) for field in fields - }) - self.embedding = nn.ModuleDict({ - field.name: nn.Embedding(field.total, self.lstm_size) for field in fields - }) - - def resample(self): - self._initialize() - result = dict() - for field in self.fields: - result[field.name] = self._sample_single(field) - return result - - def _initialize(self): - self._inputs = self.g_emb.data - self._c = [torch.zeros((1, self.lstm_size), - dtype=self._inputs.dtype, - device=self._inputs.device) for _ in range(self.lstm_num_layers)] - self._h = [torch.zeros((1, self.lstm_size), - dtype=self._inputs.dtype, - device=self._inputs.device) for _ in range(self.lstm_num_layers)] - self.sample_log_prob = 0 - self.sample_entropy = 0 - self.sample_skip_penalty = 0 - - def _lstm_next_step(self): - self._h, self._c = self.lstm(self._inputs, (self._h, self._c)) - - def _sample_single(self, field): - self._lstm_next_step() - logit = self.soft[field.name](self._h[-1]) - if self.temperature is not None: - logit /= self.temperature - if self.tanh_constant is not None: - logit = self.tanh_constant * torch.tanh(logit) - if field.choose_one: - sampled = torch.multinomial(F.softmax(logit, dim=-1), 1).view(-1) - log_prob = self.cross_entropy_loss(logit, sampled) - self._inputs = self.embedding[field.name](sampled) - else: - logit = logit.view(-1, 1) - logit = torch.cat([-logit, logit], 1) # pylint: disable=invalid-unary-operand-type - sampled = torch.multinomial(F.softmax(logit, dim=-1), 1).view(-1) - skip_prob = torch.sigmoid(logit) - kl = torch.sum(skip_prob * torch.log(skip_prob / self.skip_targets)) - self.sample_skip_penalty += kl - log_prob = self.cross_entropy_loss(logit, sampled) - sampled = sampled.nonzero().view(-1) - if sampled.sum().item(): - self._inputs = (torch.sum(self.embedding[field.name](sampled.view(-1)), 0) / (1. + torch.sum(sampled))).unsqueeze(0) - else: - self._inputs = torch.zeros(1, self.lstm_size, device=self.embedding[field.name].weight.device) - - sampled = sampled.detach().numpy().tolist() - self.sample_log_prob += self.entropy_reduction(log_prob) - entropy = (log_prob * torch.exp(-log_prob)).detach() # pylint: disable=invalid-unary-operand-type - self.sample_entropy += self.entropy_reduction(entropy) - if len(sampled) == 1: - sampled = sampled[0] - return sampled - +from .rl import PathSamplingLayerChoice,PathSamplingInputChoice,ReinforceField,ReinforceController class Enas(BaseNAS): """ @@ -314,8 +103,7 @@ class Enas(BaseNAS): for name, module in self.nas_modules] self.controller = ReinforceController(self.nas_fields, **(self.ctrl_kwargs or {})) self.ctrl_optim = torch.optim.Adam(self.controller.parameters(), lr=self.ctrl_lr) - self._resample() - + # warm up supernet with tqdm(range(self.n_warmup)) as bar: for i in bar: diff --git a/autogl/module/nas/algorithm/rl.py b/autogl/module/nas/algorithm/rl.py new file mode 100644 index 0000000..18f92ad --- /dev/null +++ b/autogl/module/nas/algorithm/rl.py @@ -0,0 +1,383 @@ +# codes in this file are reproduced from https://github.com/microsoft/nni with some changes. +import copy +import logging + +import torch +import torch.nn as nn +import torch.nn.functional as F + +from .base import BaseNAS +from ..space import BaseSpace +from ..utils import AverageMeterGroup, replace_layer_choice, replace_input_choice, get_module_order, sort_replaced_module +from nni.nas.pytorch.fixed import apply_fixed_architecture +from tqdm import tqdm +_logger = logging.getLogger(__name__) +def _get_mask(sampled, total): + multihot = [i == sampled or (isinstance(sampled, list) and i in sampled) for i in range(total)] + return torch.tensor(multihot, dtype=torch.bool) # pylint: disable=not-callable + +class PathSamplingLayerChoice(nn.Module): + """ + Mixed module, in which fprop is decided by exactly one or multiple (sampled) module. + If multiple module is selected, the result will be sumed and returned. + + Attributes + ---------- + sampled : int or list of int + Sampled module indices. + mask : tensor + A multi-hot bool 1D-tensor representing the sampled mask. + """ + + def __init__(self, layer_choice): + super(PathSamplingLayerChoice, self).__init__() + self.op_names = [] + for name, module in layer_choice.named_children(): + self.add_module(name, module) + self.op_names.append(name) + assert self.op_names, 'There has to be at least one op to choose from.' + self.sampled = None # sampled can be either a list of indices or an index + + def forward(self, *args, **kwargs): + assert self.sampled is not None, 'At least one path needs to be sampled before fprop.' + if isinstance(self.sampled, list): + return sum([getattr(self, self.op_names[i])(*args, **kwargs) for i in self.sampled]) # pylint: disable=not-an-iterable + else: + return getattr(self, self.op_names[self.sampled])(*args, **kwargs) # pylint: disable=invalid-sequence-index + + def __len__(self): + return len(self.op_names) + + @property + def mask(self): + return _get_mask(self.sampled, len(self)) + + +class PathSamplingInputChoice(nn.Module): + """ + Mixed input. Take a list of tensor as input, select some of them and return the sum. + + Attributes + ---------- + sampled : int or list of int + Sampled module indices. + mask : tensor + A multi-hot bool 1D-tensor representing the sampled mask. + """ + + def __init__(self, input_choice): + super(PathSamplingInputChoice, self).__init__() + self.n_candidates = input_choice.n_candidates + self.n_chosen = input_choice.n_chosen + self.sampled = None + + def forward(self, input_tensors): + if isinstance(self.sampled, list): + return sum([input_tensors[t] for t in self.sampled]) # pylint: disable=not-an-iterable + else: + return input_tensors[self.sampled] + + def __len__(self): + return self.n_candidates + + @property + def mask(self): + return _get_mask(self.sampled, len(self)) + + def __repr__(self): + return f'PathSamplingInputChoice(n_candidates={self.n_candidates}, chosen={self.sampled})' + +class StackedLSTMCell(nn.Module): + def __init__(self, layers, size, bias): + super().__init__() + self.lstm_num_layers = layers + self.lstm_modules = nn.ModuleList([nn.LSTMCell(size, size, bias=bias) + for _ in range(self.lstm_num_layers)]) + + def forward(self, inputs, hidden): + prev_h, prev_c = hidden + next_h, next_c = [], [] + for i, m in enumerate(self.lstm_modules): + curr_h, curr_c = m(inputs, (prev_h[i], prev_c[i])) + next_c.append(curr_c) + next_h.append(curr_h) + # current implementation only supports batch size equals 1, + # but the algorithm does not necessarily have this limitation + inputs = curr_h[-1].view(1, -1) + return next_h, next_c + + +class ReinforceField: + """ + A field with ``name``, with ``total`` choices. ``choose_one`` is true if one and only one is meant to be + selected. Otherwise, any number of choices can be chosen. + """ + + def __init__(self, name, total, choose_one): + self.name = name + self.total = total + self.choose_one = choose_one + + def __repr__(self): + return f'ReinforceField(name={self.name}, total={self.total}, choose_one={self.choose_one})' + + +class ReinforceController(nn.Module): + """ + A controller that mutates the graph with RL. + + Parameters + ---------- + fields : list of ReinforceField + List of fields to choose. + lstm_size : int + Controller LSTM hidden units. + lstm_num_layers : int + Number of layers for stacked LSTM. + tanh_constant : float + Logits will be equal to ``tanh_constant * tanh(logits)``. Don't use ``tanh`` if this value is ``None``. + skip_target : float + Target probability that skipconnect will appear. + temperature : float + Temperature constant that divides the logits. + entropy_reduction : str + Can be one of ``sum`` and ``mean``. How the entropy of multi-input-choice is reduced. + """ + + def __init__(self, fields, lstm_size=64, lstm_num_layers=1, tanh_constant=1.5, + skip_target=0.4, temperature=None, entropy_reduction='sum'): + super(ReinforceController, self).__init__() + self.fields = fields + self.lstm_size = lstm_size + self.lstm_num_layers = lstm_num_layers + self.tanh_constant = tanh_constant + self.temperature = temperature + self.skip_target = skip_target + + self.lstm = StackedLSTMCell(self.lstm_num_layers, self.lstm_size, False) + self.attn_anchor = nn.Linear(self.lstm_size, self.lstm_size, bias=False) + self.attn_query = nn.Linear(self.lstm_size, self.lstm_size, bias=False) + self.v_attn = nn.Linear(self.lstm_size, 1, bias=False) + self.g_emb = nn.Parameter(torch.randn(1, self.lstm_size) * 0.1) + self.skip_targets = nn.Parameter(torch.tensor([1.0 - self.skip_target, self.skip_target]), # pylint: disable=not-callable + requires_grad=False) + assert entropy_reduction in ['sum', 'mean'], 'Entropy reduction must be one of sum and mean.' + self.entropy_reduction = torch.sum if entropy_reduction == 'sum' else torch.mean + self.cross_entropy_loss = nn.CrossEntropyLoss(reduction='none') + self.soft = nn.ModuleDict({ + field.name: nn.Linear(self.lstm_size, field.total, bias=False) for field in fields + }) + self.embedding = nn.ModuleDict({ + field.name: nn.Embedding(field.total, self.lstm_size) for field in fields + }) + + def resample(self): + self._initialize() + result = dict() + for field in self.fields: + result[field.name] = self._sample_single(field) + return result + + def _initialize(self): + self._inputs = self.g_emb.data + self._c = [torch.zeros((1, self.lstm_size), + dtype=self._inputs.dtype, + device=self._inputs.device) for _ in range(self.lstm_num_layers)] + self._h = [torch.zeros((1, self.lstm_size), + dtype=self._inputs.dtype, + device=self._inputs.device) for _ in range(self.lstm_num_layers)] + self.sample_log_prob = 0 + self.sample_entropy = 0 + self.sample_skip_penalty = 0 + + def _lstm_next_step(self): + self._h, self._c = self.lstm(self._inputs, (self._h, self._c)) + + def _sample_single(self, field): + self._lstm_next_step() + logit = self.soft[field.name](self._h[-1]) + if self.temperature is not None: + logit /= self.temperature + if self.tanh_constant is not None: + logit = self.tanh_constant * torch.tanh(logit) + if field.choose_one: + sampled = torch.multinomial(F.softmax(logit, dim=-1), 1).view(-1) + log_prob = self.cross_entropy_loss(logit, sampled) + self._inputs = self.embedding[field.name](sampled) + else: + logit = logit.view(-1, 1) + logit = torch.cat([-logit, logit], 1) # pylint: disable=invalid-unary-operand-type + sampled = torch.multinomial(F.softmax(logit, dim=-1), 1).view(-1) + skip_prob = torch.sigmoid(logit) + kl = torch.sum(skip_prob * torch.log(skip_prob / self.skip_targets)) + self.sample_skip_penalty += kl + log_prob = self.cross_entropy_loss(logit, sampled) + sampled = sampled.nonzero().view(-1) + if sampled.sum().item(): + self._inputs = (torch.sum(self.embedding[field.name](sampled.view(-1)), 0) / (1. + torch.sum(sampled))).unsqueeze(0) + else: + self._inputs = torch.zeros(1, self.lstm_size, device=self.embedding[field.name].weight.device) + + sampled = sampled.detach().numpy().tolist() + self.sample_log_prob += self.entropy_reduction(log_prob) + entropy = (log_prob * torch.exp(-log_prob)).detach() # pylint: disable=invalid-unary-operand-type + self.sample_entropy += self.entropy_reduction(entropy) + if len(sampled) == 1: + sampled = sampled[0] + return sampled + + +class RL(BaseNAS): + """ + ENAS trainer. + + Parameters + ---------- + model : nn.Module + PyTorch model to be trained. + loss : callable + Receives logits and ground truth label, return a loss tensor. + metrics : callable + Receives logits and ground truth label, return a dict of metrics. + reward_function : callable + Receives logits and ground truth label, return a tensor, which will be feeded to RL controller as reward. + optimizer : Optimizer + The optimizer used for optimizing the model. + num_epochs : int + Number of epochs planned for training. + dataset : Dataset + Dataset for training. Will be split for training weights and architecture weights. + batch_size : int + Batch size. + workers : int + Workers for data loading. + device : torch.device + ``torch.device("cpu")`` or ``torch.device("cuda")``. + log_frequency : int + Step count per logging. + grad_clip : float + Gradient clipping. Set to 0 to disable. Default: 5. + entropy_weight : float + Weight of sample entropy loss. + skip_weight : float + Weight of skip penalty loss. + baseline_decay : float + Decay factor of baseline. New baseline will be equal to ``baseline_decay * baseline_old + reward * (1 - baseline_decay)``. + ctrl_lr : float + Learning rate for RL controller. + ctrl_steps_aggregate : int + Number of steps that will be aggregated into one mini-batch for RL controller. + ctrl_steps : int + Number of mini-batches for each epoch of RL controller learning. + ctrl_kwargs : dict + Optional kwargs that will be passed to :class:`ReinforceController`. + """ + + def __init__(self, device='cuda', workers=4,log_frequency=None, + grad_clip=5., entropy_weight=0.0001, skip_weight=0.8, baseline_decay=0.999, + ctrl_lr=0.00035, ctrl_steps_aggregate=20, ctrl_kwargs=None,n_warmup=100,model_lr=5e-3,model_wd=5e-4,*args,**kwargs): + super().__init__(device) + self.device=device + self.num_epochs = kwargs.get("num_epochs", 5) + self.workers = workers + self.log_frequency = log_frequency + self.entropy_weight = entropy_weight + self.skip_weight = skip_weight + self.baseline_decay = baseline_decay + self.baseline = 0. + self.ctrl_steps_aggregate = ctrl_steps_aggregate + self.grad_clip = grad_clip + self.workers = workers + self.ctrl_kwargs=ctrl_kwargs + self.ctrl_lr=ctrl_lr + self.n_warmup=n_warmup + self.model_lr = model_lr + self.model_wd = model_wd + self.log=open('log.txt','w') + def search(self, space: BaseSpace, dset, estimator): + self.model = space + self.dataset = dset#.to(self.device) + self.estimator = estimator + # replace choice + self.nas_modules = [] + + k2o = get_module_order(self.model) + replace_layer_choice(self.model, PathSamplingLayerChoice, self.nas_modules) + replace_input_choice(self.model, PathSamplingInputChoice, self.nas_modules) + self.nas_modules = sort_replaced_module(k2o, self.nas_modules) + + # to device + self.model = self.model.to(self.device) + # fields + self.nas_fields = [ReinforceField(name, len(module), + isinstance(module, PathSamplingLayerChoice) or module.n_chosen == 1) + for name, module in self.nas_modules] + self.controller = ReinforceController(self.nas_fields, **(self.ctrl_kwargs or {})) + self.ctrl_optim = torch.optim.Adam(self.controller.parameters(), lr=self.ctrl_lr) + # train + with tqdm(range(self.num_epochs)) as bar: + for i in bar: + l2=self._train_controller(i) + + # try: + # l2=self._train_controller(i) + # except Exception as e: + # print(e) + # nm=self.nas_modules + # for i in range(len(nm)): + # print(nm[i][1].sampled) + # # import pdb + # # pdb.set_trace() + bar.set_postfix(reward_controller=l2) + + selection=self.export() + arch=space.export(selection,self.device) + print(selection,arch) + return arch + + def _train_controller(self, epoch): + self.model.eval() + self.controller.train() + self.ctrl_optim.zero_grad() + rewards=[] + with tqdm(range(self.ctrl_steps_aggregate)) as bar: + for ctrl_step in bar: + self._resample() + metric,loss=self._infer(mask='val') + bar.set_postfix(acc=metric,loss=loss.item()) + self.log.write(f'{self.arch}\n{self.selection}\n{metric},{loss}\n') + self.log.flush() + reward =metric + rewards.append(reward) + if self.entropy_weight: + reward += self.entropy_weight * self.controller.sample_entropy.item() + self.baseline = self.baseline * self.baseline_decay + reward * (1 - self.baseline_decay) + loss = self.controller.sample_log_prob * (reward - self.baseline) + if self.skip_weight: + loss += self.skip_weight * self.controller.sample_skip_penalty + loss /= self.ctrl_steps_aggregate + loss.backward() + + if (ctrl_step + 1) % self.ctrl_steps_aggregate == 0: + if self.grad_clip > 0: + nn.utils.clip_grad_norm_(self.controller.parameters(), self.grad_clip) + self.ctrl_optim.step() + self.ctrl_optim.zero_grad() + + if self.log_frequency is not None and ctrl_step % self.log_frequency == 0: + _logger.info('RL Epoch [%d/%d] Step [%d/%d] %s', epoch + 1, self.num_epochs, + ctrl_step + 1, self.ctrl_steps_aggregate) + return sum(rewards)/len(rewards) + + def _resample(self): + result = self.controller.resample() + self.arch=self.model.export(result,device=self.device) + self.selection=result + def export(self): + self.controller.eval() + with torch.no_grad(): + return self.controller.resample() + + def _infer(self,mask='train'): + metric, loss = self.estimator.infer(self.arch, self.dataset,mask=mask) + return metric, loss diff --git a/autogl/module/nas/estimator/one_shot.py b/autogl/module/nas/estimator/one_shot.py index e43695c..4964947 100644 --- a/autogl/module/nas/estimator/one_shot.py +++ b/autogl/module/nas/estimator/one_shot.py @@ -20,3 +20,26 @@ class OneShotEstimator(BaseEstimator): loss = F.nll_loss(pred, y) acc=sum(pred.max(1)[1]==y).item()/y.size(0) return acc, loss + +from autogl.module.train import NodeClassificationFullTrainer +class TrainEstimator(BaseEstimator): + def __init__(self): + self.estimator=OneShotEstimator() + def infer(self,model: BaseSpace, dataset, mask="train"): + # self.trainer.model=model + # self.trainer.device=model.device + self.trainer=NodeClassificationFullTrainer( + model=model, + optimizer=torch.optim.Adam, + lr=0.01, + max_epoch=200, + early_stopping_round=200, + weight_decay=5e-4, + device="auto", + init=False, + feval=['acc'], + loss="nll_loss", + lr_scheduler_type=None) + self.trainer.train(dataset) + with torch.no_grad(): + return self.estimator.infer(model,dataset,mask) diff --git a/autogl/module/nas/space/single_path.py b/autogl/module/nas/space/single_path.py index 75c9746..fcbffa2 100644 --- a/autogl/module/nas/space/single_path.py +++ b/autogl/module/nas/space/single_path.py @@ -27,8 +27,6 @@ class FixedNodeClassificationModel(BaseModel): apply_fixed_architecture(self._model, selection, verbose=False) self.params = {"num_class": self.num_classes, "features_num": self.num_features} self.device = device - print(self._model) - print(selection) def to(self, device): if isinstance(device, (str, torch.device)): diff --git a/examples/test_graph_nas_rl.py b/examples/test_graph_nas_rl.py new file mode 100644 index 0000000..5ec5480 --- /dev/null +++ b/examples/test_graph_nas_rl.py @@ -0,0 +1,42 @@ +import sys +sys.path.append('../') +from torch_geometric.nn import GCNConv +import torch +from autogl.datasets import build_dataset_from_name +from autogl.solver import AutoNodeClassifier +from autogl.module.train import NodeClassificationFullTrainer +from autogl.module.nas import Darts, OneShotEstimator +from autogl.module.nas.space.graph_nas import GraphNasNodeClassificationSpace +from autogl.module.train import Acc +from autogl.module.nas.algorithm.enas import Enas +from autogl.module.nas.algorithm.rl import RL +from autogl.module.nas.estimator.one_shot import TrainEstimator +import logging +if __name__ == '__main__': + logging.getLogger().setLevel(logging.WARNING) + dataset = build_dataset_from_name('cora') + solver = AutoNodeClassifier( + feature_module='PYGNormalizeFeatures', + graph_models=[], + hpo_module=None, + ensemble_module=None, + default_trainer=NodeClassificationFullTrainer( + optimizer=torch.optim.Adam, + lr=0.01, + max_epoch=200, + early_stopping_round=200, + weight_decay=5e-4, + device="auto", + init=False, + feval=['acc'], + loss="nll_loss", + lr_scheduler_type=None,), + nas_algorithms=[RL(num_epochs=400)], + #nas_algorithms=[Darts(num_epochs=200)], + nas_spaces=[GraphNasNodeClassificationSpace(hidden_dim=16,search_act_con=True,layer_number=2)], + nas_estimators=[TrainEstimator()] + ) + solver.fit(dataset) + solver.get_leaderboard().show() + out = solver.predict_proba() + print('acc on cora', Acc.evaluate(out, dataset[0].y[dataset[0].test_mask].detach().numpy())) From edecb3149cc67da31fbc6cc659b778ac794d308f Mon Sep 17 00:00:00 2001 From: wondergo2017 Date: Fri, 7 May 2021 09:17:36 +0000 Subject: [PATCH 6/6] add graphnas space test --- examples/test_graph_nas_space.py | 73 ++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 examples/test_graph_nas_space.py diff --git a/examples/test_graph_nas_space.py b/examples/test_graph_nas_space.py new file mode 100644 index 0000000..0d60f3c --- /dev/null +++ b/examples/test_graph_nas_space.py @@ -0,0 +1,73 @@ +import sys +from nni.nas.pytorch.mutables import Mutable +sys.path.append('../') +from torch_geometric.nn import GCNConv +import torch +from autogl.datasets import build_dataset_from_name +from autogl.solver import AutoNodeClassifier +from autogl.module.train import NodeClassificationFullTrainer +from autogl.module.nas import Darts, OneShotEstimator +from autogl.module.nas.space.graph_nas import * +from autogl.module.train import Acc +from autogl.module.nas.algorithm.enas import Enas +from autogl.module.nas.algorithm.rl import * +from autogl.module.nas.estimator.one_shot import TrainEstimator +import logging +import numpy as np +from tqdm import tqdm +if __name__ == '__main__': + logging.getLogger().setLevel(logging.WARNING) + dataset = build_dataset_from_name('cora') + space=GraphNasNodeClassificationSpace(hidden_dim=16,search_act_con=True,layer_number=2) + space.instantiate(input_dim=dataset[0].x.shape[1], + output_dim=dataset.num_classes,) + estim=TrainEstimator() + # solver.fit(dataset) + # solver.get_leaderboard().show() + # out = solver.predict_proba() + + # print('acc on cora', Acc.evaluate(out, dataset[0].y[dataset[0].test_mask].detach().numpy())) + class Tmp: + def __init__(self,space): + self.model = space + self.nas_modules = [] + k2o = get_module_order(self.model) + replace_layer_choice(self.model, PathSamplingLayerChoice, self.nas_modules) + replace_input_choice(self.model, PathSamplingInputChoice, self.nas_modules) + self.nas_modules = sort_replaced_module(k2o, self.nas_modules) + + t=Tmp(space) + print(t.nas_modules) + nm=t.nas_modules + selection_range={} + for k,v in nm: + selection_range[k]=len(v) + ks=list(selection_range.keys()) + selections=[] + def dfs(selection,d): + if d>=len(ks): + selections.append(selection.copy()) + return + k=ks[d] + r=selection_range[k] + for i in range(r): + selection[k]=i + dfs(selection,d+1) + dfs({},0) + print(f'#selections {len(selections)}') + device=torch.device('cuda:0') + accs=[] + from datetime import datetime + timestamp=datetime.now().strftime('%m%d-%H-%M-%S') + log=open(f'acclog{timestamp}.txt','w') + with tqdm(selections) as bar: + for selection in bar: + arch=space.export(selection,device) + m,l=estim.infer(arch,dataset,'test') + bar.set_postfix(m=m,l=l.item()) + log.write(f'{arch}\n{selection}\n{m},{l}\n') + log.flush() + accs.append(m) + + np.save(f'space_acc{timestamp}',np.array(accs)) + print(f'max acc {np.max(accs)}') \ No newline at end of file