Browse Source

test nas

tags/v0.3.1
cluster32 5 years ago
parent
commit
62ee4808e6
6 changed files with 448 additions and 1 deletions
  1. +2
    -0
      autogl/module/hpo/__init__.py
  2. +184
    -0
      autogl/module/hpo/darts.py
  3. +46
    -0
      autogl/module/hpo/nas.py
  4. +33
    -0
      autogl/module/hpo/test.py
  5. +182
    -0
      autogl/module/hpo/utils.py
  6. +1
    -1
      examples/node_classification.py

+ 2
- 0
autogl/module/hpo/__init__.py View File

@@ -28,6 +28,7 @@ from .mocmaes_advisorchoco import MocmaesAdvisorChoco
from .quasi_advisorchoco import QuasiAdvisorChoco
from .rand_advisor import RandAdvisor
from .tpe_advisorhpo import TpeAdvisorHPO
from .test import TestHPO


def build_hpo_from_name(name: str) -> BaseHPOptimizer:
@@ -62,5 +63,6 @@ __all__ = [
"QuasiAdvisorChoco",
"RandAdvisor",
"TpeAdvisorHPO",
"TestHPO",
"build_hpo_from_name",
]

+ 184
- 0
autogl/module/hpo/darts.py View File

@@ -0,0 +1,184 @@
# Modified from NNI

import copy
import logging

import torch
import torch.nn as nn
import torch.nn.functional as F

from .nas import BaseNAS
from .utils import AverageMeterGroup, replace_layer_choice, replace_input_choice


_logger = logging.getLogger(__name__)


class DartsLayerChoice(nn.Module):
def __init__(self, layer_choice):
super(DartsLayerChoice, self).__init__()
self.name = layer_choice.key
self.op_choices = nn.ModuleDict(layer_choice.named_children())
self.alpha = nn.Parameter(torch.randn(len(self.op_choices)) * 1e-3)

def forward(self, *args, **kwargs):
op_results = torch.stack([op(*args, **kwargs) for op in self.op_choices.values()])
alpha_shape = [-1] + [1] * (len(op_results.size()) - 1)
return torch.sum(op_results * F.softmax(self.alpha, -1).view(*alpha_shape), 0)

def parameters(self):
for _, p in self.named_parameters():
yield p

def named_parameters(self):
for name, p in super(DartsLayerChoice, self).named_parameters():
if name == 'alpha':
continue
yield name, p

def export(self):
return torch.argmax(self.alpha).item()


class DartsInputChoice(nn.Module):
def __init__(self, input_choice):
super(DartsInputChoice, self).__init__()
self.name = input_choice.key
self.alpha = nn.Parameter(torch.randn(input_choice.n_candidates) * 1e-3)
self.n_chosen = input_choice.n_chosen or 1

def forward(self, inputs):
inputs = torch.stack(inputs)
alpha_shape = [-1] + [1] * (len(inputs.size()) - 1)
return torch.sum(inputs * F.softmax(self.alpha, -1).view(*alpha_shape), 0)

def parameters(self):
for _, p in self.named_parameters():
yield p

def named_parameters(self):
for name, p in super(DartsInputChoice, self).named_parameters():
if name == 'alpha':
continue
yield name, p

def export(self):
return torch.argsort(-self.alpha).cpu().numpy().tolist()[:self.n_chosen]


class DartsTrainer(BaseNAS):
"""
DARTS 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.
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.
grad_clip : float
Gradient clipping. Set to 0 to disable. Default: 5.
learning_rate : float
Learning rate to optimize the model.
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.
arc_learning_rate : float
Learning rate of architecture parameters.
unrolled : float
``True`` if using second order optimization, else first order optimization.
"""

"""def __init__(self, model, loss, metrics, optimizer,
num_epochs, dataset, grad_clip=5.,
learning_rate=2.5E-3, batch_size=64, workers=4,
device=None, log_frequency=None,
arc_learning_rate=3.0E-4, unrolled=False):"""
def __init__(self, *args, **kwargs):
self.num_epochs = kwargs.get("num_epochs", 5)
self.workers = 4
self.device = "cuda"
self.log_frequency = None

#for _, module in self.nas_modules:
# module.to(self.device)

# use the same architecture weight for modules with duplicated names

def search(self, space, dset, trainer):
"""
main process
"""
self.model = space
self.dataset = dset
self.trainer = trainer
self.model_optim = torch.optim.SGD(
self.model.parameters(), lr=0.01, weight_decay=3e-4
)

self.nas_modules = []
replace_layer_choice(self.model, DartsLayerChoice, self.nas_modules)
replace_input_choice(self.model, DartsInputChoice, self.nas_modules)

ctrl_params = {}
for _, m in self.nas_modules:
if m.name in ctrl_params:
assert m.alpha.size() == ctrl_params[m.name].size(), 'Size of parameters with the same label should be same.'
m.alpha = ctrl_params[m.name]
else:
ctrl_params[m.name] = m.alpha
self.ctrl_optim = torch.optim.Adam(list(ctrl_params.values()), 3e-4, betas=(0.5, 0.999),
weight_decay=1.0E-3)
self.grad_clip = 5.

for step in range(self.num_epochs):
self._train_one_epoch(step)
if self.log_frequency is not None and step % self.log_frequency == 0:
_logger.info('Epoch [%s/%s] Step [%s/%s] %s', epoch + 1,
self.num_epochs, step + 1, len(self.train_loader), meters)

return self.export()

def _train_one_epoch(self, epoch):
self.model.train()
meters = AverageMeterGroup()

# phase 1. architecture step
self.ctrl_optim.zero_grad()
# only no unroll here
_, loss = self._infer()
loss.backward()
self.ctrl_optim.step()

# phase 2: child network step
self.model_optim.zero_grad()
metric, loss = self._infer()
loss.backward()
if self.grad_clip > 0:
nn.utils.clip_grad_norm_(self.model.parameters(), self.grad_clip) # gradient clipping
self.model_optim.step()

def _infer(self):
metric, loss = self.trainer.infer(self.model, self.dataset)
return metric, loss

@torch.no_grad()
def export(self):
result = dict()
for name, module in self.nas_modules:
if name not in result:
result[name] = module.export()
return result

+ 46
- 0
autogl/module/hpo/nas.py View File

@@ -0,0 +1,46 @@
from torch_geometric.nn import GCNConv, SAGEConv
from nni.nas.pytorch import mutables
import torch.nn as nn

class BaseNAS:
def search(self, space, dset, trainer):
"""
The main process of NAS.
Parameters
----------
space : BaseArchitectureSpace
No implementation yet
dataset : ...datasets
Dataset to train and evaluate.
trainer : ..train.BaseTrainer
Including model, giving HP space and using for training

Returns
-------
model: ..train.BaseTrainer
The trainer including the best trained model
"""

class GraphSpace(nn.Module):
def __init__(self, inp, hid, oup):
super().__init__()
self.gcn = GCNConv(inp, hid)
self.op1 = mutables.LayerChoice([GCNConv(inp, hid),SAGEConv(inp, hid)])
self.op2 = mutables.LayerChoice([
GCNConv(hid, oup),
SAGEConv(hid, oup)
], key = "2")

def forward(self, data):
x = self.op1(data.x, data.edge_index)
x = self.op2(x, data.edge_index)
return x
class BaseTrainer:
def infer(self, model, dataset):
dset = dataset[0]
pred = model(dset)[dset.train_mask]
y = dset.y[dset.train_mask]
loss_func = nn.CrossEntropyLoss()
loss = loss_func(pred, y)
return loss, loss

+ 33
- 0
autogl/module/hpo/test.py View File

@@ -0,0 +1,33 @@
import hyperopt

from . import register_hpo
from .nas import BaseTrainer, GraphSpace
from .darts import DartsTrainer
from .base import BaseHPOptimizer, TimeTooLimitedError

@register_hpo("test")
class TestHPO(BaseHPOptimizer):
"""
Random search algorithm in `advisor` package
See https://github.com/tobegit3hub/advisor for the package
See .advisorbase.AdvisorBaseHPOptimizer for more information
"""

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

def optimize(self, trainer, dataset, time_limit=None, memory_limit=None):
num_features=dataset[0].x.shape[1]
num_classes=dataset.num_classes
model = GraphSpace(num_features, 64, num_classes)
tr = BaseTrainer()
nas = DartsTrainer()
a = nas.search(model, dataset, tr)
print(a)
print(type(a))
return 1,2

@classmethod
def build_hpo_from_args(cls, args):
"""Build a new hpo instance."""
return cls(args)

+ 182
- 0
autogl/module/hpo/utils.py View File

@@ -0,0 +1,182 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.

import logging
from collections import OrderedDict

import numpy as np
import torch
import nni.retiarii.nn.pytorch as nn
from nni.nas.pytorch.mutables import InputChoice, LayerChoice

_logger = logging.getLogger(__name__)


def to_device(obj, device):
"""
Move a tensor, tuple, list, or dict onto device.
"""
if torch.is_tensor(obj):
return obj.to(device)
if isinstance(obj, tuple):
return tuple(to_device(t, device) for t in obj)
if isinstance(obj, list):
return [to_device(t, device) for t in obj]
if isinstance(obj, dict):
return {k: to_device(v, device) for k, v in obj.items()}
if isinstance(obj, (int, float, str)):
return obj
raise ValueError("'%s' has unsupported type '%s'" % (obj, type(obj)))


def to_list(arr):
if torch.is_tensor(arr):
return arr.cpu().numpy().tolist()
if isinstance(arr, np.ndarray):
return arr.tolist()
if isinstance(arr, (list, tuple)):
return list(arr)
return arr


class AverageMeterGroup:
"""
Average meter group for multiple average meters.
"""

def __init__(self):
self.meters = OrderedDict()

def update(self, data):
"""
Update the meter group with a dict of metrics.
Non-exist average meters will be automatically created.
"""
for k, v in data.items():
if k not in self.meters:
self.meters[k] = AverageMeter(k, ":4f")
self.meters[k].update(v)

def __getattr__(self, item):
return self.meters[item]

def __getitem__(self, item):
return self.meters[item]

def __str__(self):
return " ".join(str(v) for v in self.meters.values())

def summary(self):
"""
Return a summary string of group data.
"""
return " ".join(v.summary() for v in self.meters.values())


class AverageMeter:
"""
Computes and stores the average and current value.

Parameters
----------
name : str
Name to display.
fmt : str
Format string to print the values.
"""

def __init__(self, name, fmt=':f'):
self.name = name
self.fmt = fmt
self.reset()

def reset(self):
"""
Reset the meter.
"""
self.val = 0
self.avg = 0
self.sum = 0
self.count = 0

def update(self, val, n=1):
"""
Update with value and weight.

Parameters
----------
val : float or int
The new value to be accounted in.
n : int
The weight of the new value.
"""
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__)

def summary(self):
fmtstr = '{name}: {avg' + self.fmt + '}'
return fmtstr.format(**self.__dict__)


def _replace_module_with_type(root_module, init_fn, type_name, modules):
if modules is None:
modules = []

def apply(m):
for name, child in m.named_children():
if isinstance(child, type_name):
setattr(m, name, init_fn(child))
modules.append((child.key, getattr(m, name)))
else:
apply(child)

apply(root_module)
return modules


def replace_layer_choice(root_module, init_fn, modules=None):
"""
Replace layer choice modules with modules that are initiated with init_fn.

Parameters
----------
root_module : nn.Module
Root module to traverse.
init_fn : Callable
Initializing function.
modules : dict, optional
Update the replaced modules into the dict and check duplicate if provided.

Returns
-------
List[Tuple[str, nn.Module]]
A list from layer choice keys (names) and replaced modules.
"""
return _replace_module_with_type(root_module, init_fn, (LayerChoice, nn.LayerChoice), modules)


def replace_input_choice(root_module, init_fn, modules=None):
"""
Replace input choice modules with modules that are initiated with init_fn.

Parameters
----------
root_module : nn.Module
Root module to traverse.
init_fn : Callable
Initializing function.
modules : dict, optional
Update the replaced modules into the dict and check duplicate if provided.

Returns
-------
List[Tuple[str, nn.Module]]
A list from layer choice keys (names) and replaced modules.
"""
return _replace_module_with_type(root_module, init_fn, (InputChoice, nn.InputChoice), modules)

+ 1
- 1
examples/node_classification.py View File

@@ -18,7 +18,7 @@ if __name__ == '__main__':
parser.add_argument('--dataset', default='cora', type=str)
parser.add_argument('--configs', type=str, default='../configs/nodeclf_gcn_benchmark_small.yml')
# following arguments will override parameters in the config file
parser.add_argument('--hpo', type=str, default='random')
parser.add_argument('--hpo', type=str, default='test')
parser.add_argument('--max_eval', type=int, default=5)
parser.add_argument('--seed', type=int, default=0)
parser.add_argument('--device', default=0, type=int)


Loading…
Cancel
Save