Upgrade spaces and add more tests
This commit is contained in:
		
							
								
								
									
										10
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
								
							| @@ -24,6 +24,16 @@ jobs: | |||||||
|         with: |         with: | ||||||
|           python-version: ${{ matrix.python-version }} |           python-version: ${{ matrix.python-version }} | ||||||
|  |  | ||||||
|  |       - name: Lint with Black | ||||||
|  |         run: | | ||||||
|  |           python -m pip install black | ||||||
|  |           python --version | ||||||
|  |           python -m black --version | ||||||
|  |           echo $PWD ; ls | ||||||
|  |           python -m black ./tests -l 88 --check --diff --verbose | ||||||
|  |           python -m black ./lib/spaces -l 88 --check --diff --verbose | ||||||
|  |           python -m black ./lib/trade_models -l 88 --check --diff --verbose | ||||||
|  |  | ||||||
|       - name: Test Search Space |       - name: Test Search Space | ||||||
|         run: | |         run: | | ||||||
|           python -m pip install pytest |           python -m pip install pytest | ||||||
|   | |||||||
| @@ -154,7 +154,7 @@ If you want to contribute to this repo, please see [CONTRIBUTING.md](.github/CON | |||||||
| Besides, please follow [CODE-OF-CONDUCT.md](.github/CODE-OF-CONDUCT.md). | Besides, please follow [CODE-OF-CONDUCT.md](.github/CODE-OF-CONDUCT.md). | ||||||
|  |  | ||||||
| We use [`black`](https://github.com/psf/black) for Python code formatter. | We use [`black`](https://github.com/psf/black) for Python code formatter. | ||||||
| Please use `black . -l 120`. | Please use `black . -l 88`. | ||||||
|  |  | ||||||
| # License | # License | ||||||
| The entire codebase is under the [MIT license](LICENSE.md). | The entire codebase is under the [MIT license](LICENSE.md). | ||||||
|   | |||||||
| @@ -1,7 +1,43 @@ | |||||||
| import torch.nn as nn | import torch.nn as nn | ||||||
|  | from torch.nn.parameter import Parameter | ||||||
| from typing import Optional | from typing import Optional | ||||||
|  |  | ||||||
| class MLP(nn.Module): |  | ||||||
|  | class Linear(nn.Module): | ||||||
|  |     """Applies a linear transformation to the incoming data: :math:`y = xA^T + b` | ||||||
|  |     """ | ||||||
|  |     __constants__ = ['in_features', 'out_features'] | ||||||
|  |     in_features: int | ||||||
|  |     out_features: int | ||||||
|  |     weight: Tensor | ||||||
|  |  | ||||||
|  |     def __init__(self, in_features: int, out_features: int, bias: bool = True) -> None: | ||||||
|  |         super(Linear, self).__init__() | ||||||
|  |         self.in_features = in_features | ||||||
|  |         self.out_features = out_features | ||||||
|  |         self.weight = Parameter(torch.Tensor(out_features, in_features)) | ||||||
|  |         if bias: | ||||||
|  |             self.bias = Parameter(torch.Tensor(out_features)) | ||||||
|  |         else: | ||||||
|  |             self.register_parameter('bias', None) | ||||||
|  |         self.reset_parameters() | ||||||
|  |  | ||||||
|  |     def reset_parameters(self) -> None: | ||||||
|  |         init.kaiming_uniform_(self.weight, a=math.sqrt(5)) | ||||||
|  |         if self.bias is not None: | ||||||
|  |             fan_in, _ = init._calculate_fan_in_and_fan_out(self.weight) | ||||||
|  |             bound = 1 / math.sqrt(fan_in) | ||||||
|  |             init.uniform_(self.bias, -bound, bound) | ||||||
|  |  | ||||||
|  |     def forward(self, input: Tensor) -> Tensor: | ||||||
|  |         return F.linear(input, self.weight, self.bias) | ||||||
|  |  | ||||||
|  |     def extra_repr(self) -> str: | ||||||
|  |         return 'in_features={}, out_features={}, bias={}'.format( | ||||||
|  |             self.in_features, self.out_features, self.bias is not None | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  | class SuperMLP(nn.Module): | ||||||
|   # MLP: FC -> Activation -> Drop -> FC -> Drop |   # MLP: FC -> Activation -> Drop -> FC -> Drop | ||||||
|   def __init__(self, in_features, hidden_features: Optional[int] = None, |   def __init__(self, in_features, hidden_features: Optional[int] = None, | ||||||
|                out_features: Optional[int] = None, |                out_features: Optional[int] = None, | ||||||
|   | |||||||
| @@ -6,3 +6,5 @@ | |||||||
|  |  | ||||||
| from .basic_space import Categorical | from .basic_space import Categorical | ||||||
| from .basic_space import Continuous | from .basic_space import Continuous | ||||||
|  | from .basic_op import has_categorical | ||||||
|  | from .basic_op import has_continuous | ||||||
|   | |||||||
							
								
								
									
										16
									
								
								lib/spaces/basic_op.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								lib/spaces/basic_op.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | from spaces.basic_space import Space | ||||||
|  | from spaces.basic_space import _EPS | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def has_categorical(space_or_value, x): | ||||||
|  |     if isinstance(space_or_value, Space): | ||||||
|  |         return space_or_value.has(x) | ||||||
|  |     else: | ||||||
|  |         return space_or_value == x | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def has_continuous(space_or_value, x): | ||||||
|  |     if isinstance(space_or_value, Space): | ||||||
|  |         return space_or_value.has(x) | ||||||
|  |     else: | ||||||
|  |         return abs(space_or_value - x) <= _EPS | ||||||
| @@ -4,28 +4,65 @@ | |||||||
|  |  | ||||||
| import abc | import abc | ||||||
| import math | import math | ||||||
|  | import copy | ||||||
| import random | import random | ||||||
|  | import numpy as np | ||||||
|  |  | ||||||
| from typing import Optional | from typing import Optional | ||||||
|  |  | ||||||
|  | _EPS = 1e-9 | ||||||
|  |  | ||||||
|  |  | ||||||
| class Space(metaclass=abc.ABCMeta): | class Space(metaclass=abc.ABCMeta): | ||||||
|  |     """Basic search space describing the set of possible candidate values for hyperparameter. | ||||||
|  |     All search space must inherit from this basic class. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|     @abc.abstractmethod |     @abc.abstractmethod | ||||||
|     def random(self, recursion=True): |     def random(self, recursion=True): | ||||||
|         raise NotImplementedError |         raise NotImplementedError | ||||||
|  |  | ||||||
|  |     @abc.abstractproperty | ||||||
|  |     def determined(self): | ||||||
|  |         raise NotImplementedError | ||||||
|  |  | ||||||
|     @abc.abstractmethod |     @abc.abstractmethod | ||||||
|     def __repr__(self): |     def __repr__(self): | ||||||
|         raise NotImplementedError |         raise NotImplementedError | ||||||
|  |  | ||||||
|  |     @abc.abstractmethod | ||||||
|  |     def has(self, x): | ||||||
|  |         """Check whether x is in this search space.""" | ||||||
|  |         assert not isinstance( | ||||||
|  |             x, Space | ||||||
|  |         ), "The input value itself can not be a search space." | ||||||
|  |  | ||||||
|  |     def copy(self): | ||||||
|  |         return copy.deepcopy(self) | ||||||
|  |  | ||||||
|  |  | ||||||
| class Categorical(Space): | class Categorical(Space): | ||||||
|  |     """A space contains the categorical values. | ||||||
|  |     It can be a nested space, which means that the candidate in this space can also be a search space. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|     def __init__(self, *data, default: Optional[int] = None): |     def __init__(self, *data, default: Optional[int] = None): | ||||||
|         self._candidates = [*data] |         self._candidates = [*data] | ||||||
|         self._default = default |         self._default = default | ||||||
|         assert self._default is None or 0 <= self._default < len(self._candidates), "default >= {:}".format( |         assert self._default is None or 0 <= self._default < len( | ||||||
|             len(self._candidates) |             self._candidates | ||||||
|  |         ), "default >= {:}".format(len(self._candidates)) | ||||||
|  |         assert len(self) > 0, "Please provide at least one candidate" | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def determined(self): | ||||||
|  |         if len(self) == 1: | ||||||
|  |             return ( | ||||||
|  |                 not isinstance(self._candidates[0], Space) | ||||||
|  |                 or self._candidates[0].determined | ||||||
|             ) |             ) | ||||||
|  |         else: | ||||||
|  |             return False | ||||||
|  |  | ||||||
|     def __getitem__(self, index): |     def __getitem__(self, index): | ||||||
|         return self._candidates[index] |         return self._candidates[index] | ||||||
| @@ -38,6 +75,15 @@ class Categorical(Space): | |||||||
|             name=self.__class__.__name__, cs=self._candidates, default=self._default |             name=self.__class__.__name__, cs=self._candidates, default=self._default | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |     def has(self, x): | ||||||
|  |         super().has(x) | ||||||
|  |         for candidate in self._candidates: | ||||||
|  |             if isinstance(candidate, Space) and candidate.has(x): | ||||||
|  |                 return True | ||||||
|  |             elif candidate == x: | ||||||
|  |                 return True | ||||||
|  |         return False | ||||||
|  |  | ||||||
|     def random(self, recursion=True): |     def random(self, recursion=True): | ||||||
|         sample = random.choice(self._candidates) |         sample = random.choice(self._candidates) | ||||||
|         if recursion and isinstance(sample, Space): |         if recursion and isinstance(sample, Space): | ||||||
| @@ -46,12 +92,35 @@ class Categorical(Space): | |||||||
|             return sample |             return sample | ||||||
|  |  | ||||||
|  |  | ||||||
|  | np_float_types = (np.float16, np.float32, np.float64) | ||||||
|  | np_int_types = ( | ||||||
|  |     np.uint8, | ||||||
|  |     np.int8, | ||||||
|  |     np.uint16, | ||||||
|  |     np.int16, | ||||||
|  |     np.uint32, | ||||||
|  |     np.int32, | ||||||
|  |     np.uint64, | ||||||
|  |     np.int64, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
| class Continuous(Space): | class Continuous(Space): | ||||||
|     def __init__(self, lower: float, upper: float, default: Optional[float] = None, log: bool = False): |     """A space contains the continuous values.""" | ||||||
|  |  | ||||||
|  |     def __init__( | ||||||
|  |         self, | ||||||
|  |         lower: float, | ||||||
|  |         upper: float, | ||||||
|  |         default: Optional[float] = None, | ||||||
|  |         log: bool = False, | ||||||
|  |         eps: float = _EPS, | ||||||
|  |     ): | ||||||
|         self._lower = lower |         self._lower = lower | ||||||
|         self._upper = upper |         self._upper = upper | ||||||
|         self._default = default |         self._default = default | ||||||
|         self._log_scale = log |         self._log_scale = log | ||||||
|  |         self._eps = eps | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def lower(self): |     def lower(self): | ||||||
| @@ -65,6 +134,10 @@ class Continuous(Space): | |||||||
|     def default(self): |     def default(self): | ||||||
|         return self._default |         return self._default | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def determined(self): | ||||||
|  |         return abs(self.lower - self.upper) <= self._eps | ||||||
|  |  | ||||||
|     def __repr__(self): |     def __repr__(self): | ||||||
|         return "{name:}(lower={lower:}, upper={upper:}, default_value={default:}, log_scale={log:})".format( |         return "{name:}(lower={lower:}, upper={upper:}, default_value={default:}, log_scale={log:})".format( | ||||||
|             name=self.__class__.__name__, |             name=self.__class__.__name__, | ||||||
| @@ -74,6 +147,23 @@ class Continuous(Space): | |||||||
|             log=self._log_scale, |             log=self._log_scale, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |     def convert(self, x): | ||||||
|  |         if isinstance(x, np_float_types) and x.size == 1: | ||||||
|  |             return float(x), True | ||||||
|  |         elif isinstance(x, np_int_types) and x.size == 1: | ||||||
|  |             return float(x), True | ||||||
|  |         elif isinstance(x, int): | ||||||
|  |             return float(x), True | ||||||
|  |         elif isinstance(x, float): | ||||||
|  |             return float(x), True | ||||||
|  |         else: | ||||||
|  |             return None, False | ||||||
|  |  | ||||||
|  |     def has(self, x): | ||||||
|  |         super().has(x) | ||||||
|  |         converted_x, success = self.convert(x) | ||||||
|  |         return success and self.lower <= converted_x <= self.upper | ||||||
|  |  | ||||||
|     def random(self, recursion=True): |     def random(self, recursion=True): | ||||||
|         del recursion |         del recursion | ||||||
|         if self._log_scale: |         if self._log_scale: | ||||||
|   | |||||||
| @@ -1,6 +1,8 @@ | |||||||
| ################################################## | ################################################## | ||||||
| # Copyright (c) Xuanyi Dong [GitHub D-X-Y], 2021 # | # Copyright (c) Xuanyi Dong [GitHub D-X-Y], 2021 # | ||||||
| ################################################## | ################################################## | ||||||
|  | # Use noise as prediction                        # | ||||||
|  | ################################################## | ||||||
| from __future__ import division | from __future__ import division | ||||||
| from __future__ import print_function | from __future__ import print_function | ||||||
|  |  | ||||||
| @@ -27,7 +29,11 @@ class NAIVE_V1(Model): | |||||||
|         self.d_feat = d_feat |         self.d_feat = d_feat | ||||||
|         self.seed = seed |         self.seed = seed | ||||||
|  |  | ||||||
|         self.logger.info("NAIVE-V1 parameters setting: d_feat={:}, seed={:}".format(self.d_feat, self.seed)) |         self.logger.info( | ||||||
|  |             "NAIVE-V1 parameters setting: d_feat={:}, seed={:}".format( | ||||||
|  |                 self.d_feat, self.seed | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |  | ||||||
|         if self.seed is not None: |         if self.seed is not None: | ||||||
|             random.seed(self.seed) |             random.seed(self.seed) | ||||||
| @@ -49,7 +55,9 @@ class NAIVE_V1(Model): | |||||||
|  |  | ||||||
|     def model(self, x): |     def model(self, x): | ||||||
|         num = len(x) |         num = len(x) | ||||||
|         return np.random.normal(loc=self._mean, scale=self._std, size=num).astype(x.dtype) |         return np.random.normal(loc=self._mean, scale=self._std, size=num).astype( | ||||||
|  |             x.dtype | ||||||
|  |         ) | ||||||
|  |  | ||||||
|     def fit(self, dataset: DatasetH): |     def fit(self, dataset: DatasetH): | ||||||
|         def _prepare_dataset(df_data): |         def _prepare_dataset(df_data): | ||||||
| @@ -71,9 +79,15 @@ class NAIVE_V1(Model): | |||||||
|         # df_train['feature']['CLOSE1'].values |         # df_train['feature']['CLOSE1'].values | ||||||
|         # train_dataset['features'][:, -1] |         # train_dataset['features'][:, -1] | ||||||
|         masks = ~np.isnan(train_dataset["labels"]) |         masks = ~np.isnan(train_dataset["labels"]) | ||||||
|         self._mean, self._std = np.mean(train_dataset["labels"][masks]), np.std(train_dataset["labels"][masks]) |         self._mean, self._std = np.mean(train_dataset["labels"][masks]), np.std( | ||||||
|         train_mse_loss = self.mse(self.model(train_dataset["features"]), train_dataset["labels"]) |             train_dataset["labels"][masks] | ||||||
|         valid_mse_loss = self.mse(self.model(valid_dataset["features"]), valid_dataset["labels"]) |         ) | ||||||
|  |         train_mse_loss = self.mse( | ||||||
|  |             self.model(train_dataset["features"]), train_dataset["labels"] | ||||||
|  |         ) | ||||||
|  |         valid_mse_loss = self.mse( | ||||||
|  |             self.model(valid_dataset["features"]), valid_dataset["labels"] | ||||||
|  |         ) | ||||||
|         self.logger.info("Training MSE loss: {:}".format(train_mse_loss)) |         self.logger.info("Training MSE loss: {:}".format(train_mse_loss)) | ||||||
|         self.logger.info("Validation MSE loss: {:}".format(valid_mse_loss)) |         self.logger.info("Validation MSE loss: {:}".format(valid_mse_loss)) | ||||||
|         self.fitted = True |         self.fitted = True | ||||||
|   | |||||||
| @@ -29,7 +29,11 @@ class NAIVE_V2(Model): | |||||||
|         self.d_feat = d_feat |         self.d_feat = d_feat | ||||||
|         self.seed = seed |         self.seed = seed | ||||||
|  |  | ||||||
|         self.logger.info("NAIVE parameters setting: d_feat={:}, seed={:}".format(self.d_feat, self.seed)) |         self.logger.info( | ||||||
|  |             "NAIVE parameters setting: d_feat={:}, seed={:}".format( | ||||||
|  |                 self.d_feat, self.seed | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |  | ||||||
|         if self.seed is not None: |         if self.seed is not None: | ||||||
|             random.seed(self.seed) |             random.seed(self.seed) | ||||||
| @@ -79,8 +83,12 @@ class NAIVE_V2(Model): | |||||||
|         ) |         ) | ||||||
|         # df_train['feature']['CLOSE1'].values |         # df_train['feature']['CLOSE1'].values | ||||||
|         # train_dataset['features'][:, -1] |         # train_dataset['features'][:, -1] | ||||||
|         train_mse_loss = self.mse(self.model(train_dataset["features"]), train_dataset["labels"]) |         train_mse_loss = self.mse( | ||||||
|         valid_mse_loss = self.mse(self.model(valid_dataset["features"]), valid_dataset["labels"]) |             self.model(train_dataset["features"]), train_dataset["labels"] | ||||||
|  |         ) | ||||||
|  |         valid_mse_loss = self.mse( | ||||||
|  |             self.model(valid_dataset["features"]), valid_dataset["labels"] | ||||||
|  |         ) | ||||||
|         self.logger.info("Training MSE loss: {:}".format(train_mse_loss)) |         self.logger.info("Training MSE loss: {:}".format(train_mse_loss)) | ||||||
|         self.logger.info("Validation MSE loss: {:}".format(valid_mse_loss)) |         self.logger.info("Validation MSE loss: {:}".format(valid_mse_loss)) | ||||||
|         self.fitted = True |         self.fitted = True | ||||||
|   | |||||||
| @@ -37,14 +37,22 @@ from qlib.data.dataset.handler import DataHandlerLP | |||||||
|  |  | ||||||
|  |  | ||||||
| DEFAULT_OPT_CONFIG = dict( | DEFAULT_OPT_CONFIG = dict( | ||||||
|     epochs=200, lr=0.001, batch_size=2000, early_stop=20, loss="mse", optimizer="adam", num_workers=4 |     epochs=200, | ||||||
|  |     lr=0.001, | ||||||
|  |     batch_size=2000, | ||||||
|  |     early_stop=20, | ||||||
|  |     loss="mse", | ||||||
|  |     optimizer="adam", | ||||||
|  |     num_workers=4, | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
| class QuantTransformer(Model): | class QuantTransformer(Model): | ||||||
|     """Transformer-based Quant Model""" |     """Transformer-based Quant Model""" | ||||||
|  |  | ||||||
|     def __init__(self, net_config=None, opt_config=None, metric="", GPU=0, seed=None, **kwargs): |     def __init__( | ||||||
|  |         self, net_config=None, opt_config=None, metric="", GPU=0, seed=None, **kwargs | ||||||
|  |     ): | ||||||
|         # Set logger. |         # Set logger. | ||||||
|         self.logger = get_module_logger("QuantTransformer") |         self.logger = get_module_logger("QuantTransformer") | ||||||
|         self.logger.info("QuantTransformer PyTorch version...") |         self.logger.info("QuantTransformer PyTorch version...") | ||||||
| @@ -53,7 +61,9 @@ class QuantTransformer(Model): | |||||||
|         self.net_config = net_config or DEFAULT_NET_CONFIG |         self.net_config = net_config or DEFAULT_NET_CONFIG | ||||||
|         self.opt_config = opt_config or DEFAULT_OPT_CONFIG |         self.opt_config = opt_config or DEFAULT_OPT_CONFIG | ||||||
|         self.metric = metric |         self.metric = metric | ||||||
|         self.device = torch.device("cuda:{:}".format(GPU) if torch.cuda.is_available() and GPU >= 0 else "cpu") |         self.device = torch.device( | ||||||
|  |             "cuda:{:}".format(GPU) if torch.cuda.is_available() and GPU >= 0 else "cpu" | ||||||
|  |         ) | ||||||
|         self.seed = seed |         self.seed = seed | ||||||
|  |  | ||||||
|         self.logger.info( |         self.logger.info( | ||||||
| @@ -84,11 +94,17 @@ class QuantTransformer(Model): | |||||||
|         self.logger.info("model size: {:.3f} MB".format(count_parameters(self.model))) |         self.logger.info("model size: {:.3f} MB".format(count_parameters(self.model))) | ||||||
|  |  | ||||||
|         if self.opt_config["optimizer"] == "adam": |         if self.opt_config["optimizer"] == "adam": | ||||||
|             self.train_optimizer = optim.Adam(self.model.parameters(), lr=self.opt_config["lr"]) |             self.train_optimizer = optim.Adam( | ||||||
|  |                 self.model.parameters(), lr=self.opt_config["lr"] | ||||||
|  |             ) | ||||||
|         elif self.opt_config["optimizer"] == "adam": |         elif self.opt_config["optimizer"] == "adam": | ||||||
|             self.train_optimizer = optim.SGD(self.model.parameters(), lr=self.opt_config["lr"]) |             self.train_optimizer = optim.SGD( | ||||||
|  |                 self.model.parameters(), lr=self.opt_config["lr"] | ||||||
|  |             ) | ||||||
|         else: |         else: | ||||||
|             raise NotImplementedError("optimizer {:} is not supported!".format(optimizer)) |             raise NotImplementedError( | ||||||
|  |                 "optimizer {:} is not supported!".format(optimizer) | ||||||
|  |             ) | ||||||
|  |  | ||||||
|         self.fitted = False |         self.fitted = False | ||||||
|         self.model.to(self.device) |         self.model.to(self.device) | ||||||
| @@ -111,7 +127,9 @@ class QuantTransformer(Model): | |||||||
|         else: |         else: | ||||||
|             raise ValueError("unknown metric `{:}`".format(self.metric)) |             raise ValueError("unknown metric `{:}`".format(self.metric)) | ||||||
|  |  | ||||||
|     def train_or_test_epoch(self, xloader, model, loss_fn, metric_fn, is_train, optimizer=None): |     def train_or_test_epoch( | ||||||
|  |         self, xloader, model, loss_fn, metric_fn, is_train, optimizer=None | ||||||
|  |     ): | ||||||
|         if is_train: |         if is_train: | ||||||
|             model.train() |             model.train() | ||||||
|         else: |         else: | ||||||
| @@ -173,7 +191,11 @@ class QuantTransformer(Model): | |||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         save_dir = get_or_create_path(save_dir, return_dir=True) |         save_dir = get_or_create_path(save_dir, return_dir=True) | ||||||
|         self.logger.info("Fit procedure for [{:}] with save path={:}".format(self.__class__.__name__, save_dir)) |         self.logger.info( | ||||||
|  |             "Fit procedure for [{:}] with save path={:}".format( | ||||||
|  |                 self.__class__.__name__, save_dir | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |  | ||||||
|         def _internal_test(ckp_epoch=None, results_dict=None): |         def _internal_test(ckp_epoch=None, results_dict=None): | ||||||
|             with torch.no_grad(): |             with torch.no_grad(): | ||||||
| @@ -186,9 +208,11 @@ class QuantTransformer(Model): | |||||||
|                 test_loss, test_score = self.train_or_test_epoch( |                 test_loss, test_score = self.train_or_test_epoch( | ||||||
|                     test_loader, self.model, self.loss_fn, self.metric_fn, False, None |                     test_loader, self.model, self.loss_fn, self.metric_fn, False, None | ||||||
|                 ) |                 ) | ||||||
|                 xstr = "train-score={:.6f}, valid-score={:.6f}, test-score={:.6f}".format( |                 xstr = ( | ||||||
|  |                     "train-score={:.6f}, valid-score={:.6f}, test-score={:.6f}".format( | ||||||
|                         train_score, valid_score, test_score |                         train_score, valid_score, test_score | ||||||
|                     ) |                     ) | ||||||
|  |                 ) | ||||||
|                 if ckp_epoch is not None and isinstance(results_dict, dict): |                 if ckp_epoch is not None and isinstance(results_dict, dict): | ||||||
|                     results_dict["train"][ckp_epoch] = train_score |                     results_dict["train"][ckp_epoch] = train_score | ||||||
|                     results_dict["valid"][ckp_epoch] = valid_score |                     results_dict["valid"][ckp_epoch] = valid_score | ||||||
| @@ -199,18 +223,26 @@ class QuantTransformer(Model): | |||||||
|         ckp_path = os.path.join(save_dir, "{:}.pth".format(self.__class__.__name__)) |         ckp_path = os.path.join(save_dir, "{:}.pth".format(self.__class__.__name__)) | ||||||
|         if os.path.exists(ckp_path): |         if os.path.exists(ckp_path): | ||||||
|             ckp_data = torch.load(ckp_path) |             ckp_data = torch.load(ckp_path) | ||||||
|             stop_steps, best_score, best_epoch = ckp_data['stop_steps'], ckp_data['best_score'], ckp_data['best_epoch'] |             stop_steps, best_score, best_epoch = ( | ||||||
|             start_epoch, best_param = ckp_data['start_epoch'], ckp_data['best_param'] |                 ckp_data["stop_steps"], | ||||||
|             results_dict = ckp_data['results_dict'] |                 ckp_data["best_score"], | ||||||
|             self.model.load_state_dict(ckp_data['net_state_dict']) |                 ckp_data["best_epoch"], | ||||||
|             self.train_optimizer.load_state_dict(ckp_data['opt_state_dict']) |             ) | ||||||
|  |             start_epoch, best_param = ckp_data["start_epoch"], ckp_data["best_param"] | ||||||
|  |             results_dict = ckp_data["results_dict"] | ||||||
|  |             self.model.load_state_dict(ckp_data["net_state_dict"]) | ||||||
|  |             self.train_optimizer.load_state_dict(ckp_data["opt_state_dict"]) | ||||||
|             self.logger.info("Resume from existing checkpoint: {:}".format(ckp_path)) |             self.logger.info("Resume from existing checkpoint: {:}".format(ckp_path)) | ||||||
|         else: |         else: | ||||||
|             stop_steps, best_score, best_epoch = 0, -np.inf, -1 |             stop_steps, best_score, best_epoch = 0, -np.inf, -1 | ||||||
|             start_epoch, best_param = 0, None |             start_epoch, best_param = 0, None | ||||||
|             results_dict = dict(train=OrderedDict(), valid=OrderedDict(), test=OrderedDict()) |             results_dict = dict( | ||||||
|  |                 train=OrderedDict(), valid=OrderedDict(), test=OrderedDict() | ||||||
|  |             ) | ||||||
|             _, eval_str = _internal_test(-1, results_dict) |             _, eval_str = _internal_test(-1, results_dict) | ||||||
|             self.logger.info("Training from scratch, metrics@start: {:}".format(eval_str)) |             self.logger.info( | ||||||
|  |                 "Training from scratch, metrics@start: {:}".format(eval_str) | ||||||
|  |             ) | ||||||
|  |  | ||||||
|         for iepoch in range(start_epoch, self.opt_config["epochs"]): |         for iepoch in range(start_epoch, self.opt_config["epochs"]): | ||||||
|             self.logger.info( |             self.logger.info( | ||||||
| @@ -219,20 +251,35 @@ class QuantTransformer(Model): | |||||||
|                 ) |                 ) | ||||||
|             ) |             ) | ||||||
|             train_loss, train_score = self.train_or_test_epoch( |             train_loss, train_score = self.train_or_test_epoch( | ||||||
|                 train_loader, self.model, self.loss_fn, self.metric_fn, True, self.train_optimizer |                 train_loader, | ||||||
|  |                 self.model, | ||||||
|  |                 self.loss_fn, | ||||||
|  |                 self.metric_fn, | ||||||
|  |                 True, | ||||||
|  |                 self.train_optimizer, | ||||||
|  |             ) | ||||||
|  |             self.logger.info( | ||||||
|  |                 "Training :: loss={:.6f}, score={:.6f}".format(train_loss, train_score) | ||||||
|             ) |             ) | ||||||
|             self.logger.info("Training :: loss={:.6f}, score={:.6f}".format(train_loss, train_score)) |  | ||||||
|  |  | ||||||
|             current_eval_scores, eval_str = _internal_test(iepoch, results_dict) |             current_eval_scores, eval_str = _internal_test(iepoch, results_dict) | ||||||
|             self.logger.info("Evaluating :: {:}".format(eval_str)) |             self.logger.info("Evaluating :: {:}".format(eval_str)) | ||||||
|  |  | ||||||
|             if current_eval_scores["valid"] > best_score: |             if current_eval_scores["valid"] > best_score: | ||||||
|                 stop_steps, best_epoch, best_score = 0, iepoch, current_eval_scores["valid"] |                 stop_steps, best_epoch, best_score = ( | ||||||
|  |                     0, | ||||||
|  |                     iepoch, | ||||||
|  |                     current_eval_scores["valid"], | ||||||
|  |                 ) | ||||||
|                 best_param = copy.deepcopy(self.model.state_dict()) |                 best_param = copy.deepcopy(self.model.state_dict()) | ||||||
|             else: |             else: | ||||||
|                 stop_steps += 1 |                 stop_steps += 1 | ||||||
|                 if stop_steps >= self.opt_config["early_stop"]: |                 if stop_steps >= self.opt_config["early_stop"]: | ||||||
|                     self.logger.info("early stop at {:}-th epoch, where the best is @{:}".format(iepoch, best_epoch)) |                     self.logger.info( | ||||||
|  |                         "early stop at {:}-th epoch, where the best is @{:}".format( | ||||||
|  |                             iepoch, best_epoch | ||||||
|  |                         ) | ||||||
|  |                     ) | ||||||
|                     break |                     break | ||||||
|             save_info = dict( |             save_info = dict( | ||||||
|                 net_config=self.net_config, |                 net_config=self.net_config, | ||||||
| @@ -247,9 +294,11 @@ class QuantTransformer(Model): | |||||||
|                 start_epoch=iepoch + 1, |                 start_epoch=iepoch + 1, | ||||||
|             ) |             ) | ||||||
|             torch.save(save_info, ckp_path) |             torch.save(save_info, ckp_path) | ||||||
|         self.logger.info("The best score: {:.6f} @ {:02d}-th epoch".format(best_score, best_epoch)) |         self.logger.info( | ||||||
|  |             "The best score: {:.6f} @ {:02d}-th epoch".format(best_score, best_epoch) | ||||||
|  |         ) | ||||||
|         self.model.load_state_dict(best_param) |         self.model.load_state_dict(best_param) | ||||||
|         _, eval_str = _internal_test('final', results_dict) |         _, eval_str = _internal_test("final", results_dict) | ||||||
|         self.logger.info("Reload the best parameter :: {:}".format(eval_str)) |         self.logger.info("Reload the best parameter :: {:}".format(eval_str)) | ||||||
|  |  | ||||||
|         if self.use_gpu: |         if self.use_gpu: | ||||||
|   | |||||||
| @@ -33,7 +33,15 @@ DEFAULT_NET_CONFIG = dict( | |||||||
|  |  | ||||||
|  |  | ||||||
| class Attention(nn.Module): | class Attention(nn.Module): | ||||||
|     def __init__(self, dim, num_heads=8, qkv_bias=False, qk_scale=None, attn_drop=0.0, proj_drop=0.0): |     def __init__( | ||||||
|  |         self, | ||||||
|  |         dim, | ||||||
|  |         num_heads=8, | ||||||
|  |         qkv_bias=False, | ||||||
|  |         qk_scale=None, | ||||||
|  |         attn_drop=0.0, | ||||||
|  |         proj_drop=0.0, | ||||||
|  |     ): | ||||||
|         super(Attention, self).__init__() |         super(Attention, self).__init__() | ||||||
|         self.num_heads = num_heads |         self.num_heads = num_heads | ||||||
|         head_dim = dim // num_heads |         head_dim = dim // num_heads | ||||||
| @@ -46,8 +54,16 @@ class Attention(nn.Module): | |||||||
|  |  | ||||||
|     def forward(self, x): |     def forward(self, x): | ||||||
|         B, N, C = x.shape |         B, N, C = x.shape | ||||||
|         qkv = self.qkv(x).reshape(B, N, 3, self.num_heads, C // self.num_heads).permute(2, 0, 3, 1, 4) |         qkv = ( | ||||||
|         q, k, v = qkv[0], qkv[1], qkv[2]  # make torchscript happy (cannot use tensor as tuple) |             self.qkv(x) | ||||||
|  |             .reshape(B, N, 3, self.num_heads, C // self.num_heads) | ||||||
|  |             .permute(2, 0, 3, 1, 4) | ||||||
|  |         ) | ||||||
|  |         q, k, v = ( | ||||||
|  |             qkv[0], | ||||||
|  |             qkv[1], | ||||||
|  |             qkv[2], | ||||||
|  |         )  # make torchscript happy (cannot use tensor as tuple) | ||||||
|  |  | ||||||
|         attn = (q @ k.transpose(-2, -1)) * self.scale |         attn = (q @ k.transpose(-2, -1)) * self.scale | ||||||
|         attn = attn.softmax(dim=-1) |         attn = attn.softmax(dim=-1) | ||||||
| @@ -76,13 +92,25 @@ class Block(nn.Module): | |||||||
|         super(Block, self).__init__() |         super(Block, self).__init__() | ||||||
|         self.norm1 = norm_layer(dim) |         self.norm1 = norm_layer(dim) | ||||||
|         self.attn = Attention( |         self.attn = Attention( | ||||||
|             dim, num_heads=num_heads, qkv_bias=qkv_bias, qk_scale=qk_scale, attn_drop=attn_drop, proj_drop=mlp_drop |             dim, | ||||||
|  |             num_heads=num_heads, | ||||||
|  |             qkv_bias=qkv_bias, | ||||||
|  |             qk_scale=qk_scale, | ||||||
|  |             attn_drop=attn_drop, | ||||||
|  |             proj_drop=mlp_drop, | ||||||
|         ) |         ) | ||||||
|         # NOTE: drop path for stochastic depth, we shall see if this is better than dropout here |         # NOTE: drop path for stochastic depth, we shall see if this is better than dropout here | ||||||
|         self.drop_path = xlayers.DropPath(drop_path) if drop_path > 0.0 else nn.Identity() |         self.drop_path = ( | ||||||
|  |             xlayers.DropPath(drop_path) if drop_path > 0.0 else nn.Identity() | ||||||
|  |         ) | ||||||
|         self.norm2 = norm_layer(dim) |         self.norm2 = norm_layer(dim) | ||||||
|         mlp_hidden_dim = int(dim * mlp_ratio) |         mlp_hidden_dim = int(dim * mlp_ratio) | ||||||
|         self.mlp = xlayers.MLP(in_features=dim, hidden_features=mlp_hidden_dim, act_layer=act_layer, drop=mlp_drop) |         self.mlp = xlayers.MLP( | ||||||
|  |             in_features=dim, | ||||||
|  |             hidden_features=mlp_hidden_dim, | ||||||
|  |             act_layer=act_layer, | ||||||
|  |             drop=mlp_drop, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|     def forward(self, x): |     def forward(self, x): | ||||||
|         x = x + self.drop_path(self.attn(self.norm1(x))) |         x = x + self.drop_path(self.attn(self.norm1(x))) | ||||||
| @@ -144,9 +172,13 @@ class TransformerModel(nn.Module): | |||||||
|         self.input_embed = SimpleEmbed(d_feat, embed_dim=embed_dim) |         self.input_embed = SimpleEmbed(d_feat, embed_dim=embed_dim) | ||||||
|  |  | ||||||
|         self.cls_token = nn.Parameter(torch.zeros(1, 1, embed_dim)) |         self.cls_token = nn.Parameter(torch.zeros(1, 1, embed_dim)) | ||||||
|         self.pos_embed = xlayers.PositionalEncoder(d_model=embed_dim, max_seq_len=max_seq_len, dropout=pos_drop) |         self.pos_embed = xlayers.PositionalEncoder( | ||||||
|  |             d_model=embed_dim, max_seq_len=max_seq_len, dropout=pos_drop | ||||||
|  |         ) | ||||||
|  |  | ||||||
|         dpr = [x.item() for x in torch.linspace(0, drop_path_rate, depth)]  # stochastic depth decay rule |         dpr = [ | ||||||
|  |             x.item() for x in torch.linspace(0, drop_path_rate, depth) | ||||||
|  |         ]  # stochastic depth decay rule | ||||||
|         self.blocks = nn.ModuleList( |         self.blocks = nn.ModuleList( | ||||||
|             [ |             [ | ||||||
|                 Block( |                 Block( | ||||||
| @@ -184,7 +216,9 @@ class TransformerModel(nn.Module): | |||||||
|         batch, flatten_size = x.shape |         batch, flatten_size = x.shape | ||||||
|         feats = self.input_embed(x)  # batch * 60 * 64 |         feats = self.input_embed(x)  # batch * 60 * 64 | ||||||
|  |  | ||||||
|         cls_tokens = self.cls_token.expand(batch, -1, -1)  # stole cls_tokens impl from Phil Wang, thanks |         cls_tokens = self.cls_token.expand( | ||||||
|  |             batch, -1, -1 | ||||||
|  |         )  # stole cls_tokens impl from Phil Wang, thanks | ||||||
|         feats_w_ct = torch.cat((cls_tokens, feats), dim=1) |         feats_w_ct = torch.cat((cls_tokens, feats), dim=1) | ||||||
|         feats_w_tp = self.pos_embed(feats_w_ct) |         feats_w_tp = self.pos_embed(feats_w_ct) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,38 +0,0 @@ | |||||||
| ##################################################### |  | ||||||
| # Copyright (c) Xuanyi Dong [GitHub D-X-Y], 2021.03 # |  | ||||||
| ##################################################### |  | ||||||
| import sys |  | ||||||
| import unittest |  | ||||||
| import pytest |  | ||||||
| from pathlib import Path |  | ||||||
|  |  | ||||||
| lib_dir = (Path(__file__).parent / ".." / "lib").resolve() |  | ||||||
| print("library path: {:}".format(lib_dir)) |  | ||||||
| if str(lib_dir) not in sys.path: |  | ||||||
|     sys.path.insert(0, str(lib_dir)) |  | ||||||
|  |  | ||||||
| from spaces import Categorical |  | ||||||
| from spaces import Continuous |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestBasicSpace(unittest.TestCase): |  | ||||||
|     def test_categorical(self): |  | ||||||
|         space = Categorical(1, 2, 3, 4) |  | ||||||
|         for i in range(4): |  | ||||||
|             self.assertEqual(space[i], i + 1) |  | ||||||
|         self.assertEqual("Categorical(candidates=[1, 2, 3, 4], default_index=None)", str(space)) |  | ||||||
|  |  | ||||||
|     def test_continuous(self): |  | ||||||
|         space = Continuous(0, 1) |  | ||||||
|         self.assertGreaterEqual(space.random(), 0) |  | ||||||
|         self.assertGreaterEqual(1, space.random()) |  | ||||||
|  |  | ||||||
|         lower, upper = 1.5, 4.6 |  | ||||||
|         space = Continuous(lower, upper, log=False) |  | ||||||
|         values = [] |  | ||||||
|         for i in range(100000): |  | ||||||
|             x = space.random() |  | ||||||
|             self.assertGreaterEqual(x, lower) |  | ||||||
|             self.assertGreaterEqual(upper, x) |  | ||||||
|             values.append(x) |  | ||||||
|         self.assertAlmostEqual((lower + upper) / 2, sum(values) / len(values), places=2) |  | ||||||
							
								
								
									
										81
									
								
								tests/test_basic_space.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								tests/test_basic_space.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | |||||||
|  | ##################################################### | ||||||
|  | # Copyright (c) Xuanyi Dong [GitHub D-X-Y], 2021.03 # | ||||||
|  | ##################################################### | ||||||
|  | import sys, random | ||||||
|  | import unittest | ||||||
|  | import pytest | ||||||
|  | from pathlib import Path | ||||||
|  |  | ||||||
|  | lib_dir = (Path(__file__).parent / ".." / "lib").resolve() | ||||||
|  | print("library path: {:}".format(lib_dir)) | ||||||
|  | if str(lib_dir) not in sys.path: | ||||||
|  |     sys.path.insert(0, str(lib_dir)) | ||||||
|  |  | ||||||
|  | from spaces import Categorical | ||||||
|  | from spaces import Continuous | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TestBasicSpace(unittest.TestCase): | ||||||
|  |     """Test the basic search spaces.""" | ||||||
|  |  | ||||||
|  |     def test_categorical(self): | ||||||
|  |         space = Categorical(1, 2, 3, 4) | ||||||
|  |         for i in range(4): | ||||||
|  |             self.assertEqual(space[i], i + 1) | ||||||
|  |         self.assertEqual( | ||||||
|  |             "Categorical(candidates=[1, 2, 3, 4], default_index=None)", str(space) | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     def test_continuous(self): | ||||||
|  |         random.seed(999) | ||||||
|  |         space = Continuous(0, 1) | ||||||
|  |         self.assertGreaterEqual(space.random(), 0) | ||||||
|  |         self.assertGreaterEqual(1, space.random()) | ||||||
|  |  | ||||||
|  |         lower, upper = 1.5, 4.6 | ||||||
|  |         space = Continuous(lower, upper, log=False) | ||||||
|  |         values = [] | ||||||
|  |         for i in range(1000000): | ||||||
|  |             x = space.random() | ||||||
|  |             self.assertGreaterEqual(x, lower) | ||||||
|  |             self.assertGreaterEqual(upper, x) | ||||||
|  |             values.append(x) | ||||||
|  |         self.assertAlmostEqual((lower + upper) / 2, sum(values) / len(values), places=2) | ||||||
|  |         self.assertEqual( | ||||||
|  |             "Continuous(lower=1.5, upper=4.6, default_value=None, log_scale=False)", | ||||||
|  |             str(space), | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     def test_determined_and_has(self): | ||||||
|  |         # Test Non-nested Space | ||||||
|  |         space = Categorical(1, 2, 3, 4) | ||||||
|  |         self.assertFalse(space.determined) | ||||||
|  |         self.assertTrue(space.has(2)) | ||||||
|  |         self.assertFalse(space.has(6)) | ||||||
|  |         space = Categorical(4) | ||||||
|  |         self.assertTrue(space.determined) | ||||||
|  |  | ||||||
|  |         space = Continuous(0.11, 0.12) | ||||||
|  |         self.assertTrue(space.has(0.115)) | ||||||
|  |         self.assertFalse(space.has(0.1)) | ||||||
|  |         self.assertFalse(space.determined) | ||||||
|  |         space = Continuous(0.11, 0.11) | ||||||
|  |         self.assertTrue(space.determined) | ||||||
|  |  | ||||||
|  |         # Test Nested Space | ||||||
|  |         space_1 = Categorical(1, 2, 3, 4) | ||||||
|  |         space_2 = Categorical(1) | ||||||
|  |         nested_space = Categorical(space_1) | ||||||
|  |         self.assertFalse(nested_space.determined) | ||||||
|  |         self.assertTrue(nested_space.has(4)) | ||||||
|  |         nested_space = Categorical(space_2) | ||||||
|  |         self.assertTrue(nested_space.determined) | ||||||
|  |  | ||||||
|  |         # Test Nested Space 2 | ||||||
|  |         nested_space = Categorical( | ||||||
|  |             Categorical(1, 2, 3), | ||||||
|  |             Categorical(4, Categorical(5, 6, 7, Categorical(8, 9), 10), 11), | ||||||
|  |             12, | ||||||
|  |         ) | ||||||
|  |         for i in range(1, 13): | ||||||
|  |             self.assertTrue(nested_space.has(i)) | ||||||
		Reference in New Issue
	
	Block a user