diff --git a/README.md b/README.md index fffc248..872a854 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,126 @@ -# hisel -Feature selection tool based on Hilbert-Schmidt Independence Criterion +# HISEL +## Feature selection tool based on Hilbert-Schmidt Independence Criterion +Feature selection is +the machine learning +task +of selecting from a data set +the features +that are relevant +for the prediction of a given target. +The `hisel` package +provides feature selection methods +based on +Hilbert-Schmidt Independence Criterion. +In particular, +it provides an implementation of the HSIC Lasso algorithm of +[Yamada, M. et al. (2012)](https://arxiv.org/abs/1202.0515). + +## Why is `hisel` cool? + +#### `hisel` is accurate +HSIC Lasso is an excellent algorihtm for feature selection. +This makes `hisel` an accurate tool in your machine learning modelling. +Moreover, +`hisel` implements clever routines +that address common causes of poor accuracy in other feature selection methods. + +Examples of where `hisel` outperforms the methods in +[sklearn.feature\_selection](https://scikit-learn.org/stable/modules/classes.html#module-sklearn.feature_selection) +are given in the notebooks +`ensemble-example.ipynb` +and +`nonlinear-transform.ipynb`. + + +#### `hisel` is fast +A crucial step in the HSIC Lasso algorithm +is the computation of +certain Gram matrices. +`hisel` implemets such computations +in a highly vectorised and performant way. +Moreover, +`hisel` allows you to +accelerate these computations + using a GPU. +The image below shows +the average run time +of the computations +of Gram matrices +via +`hisel` on CPU, +via +`hisel` on GPU, +and +via +[pyHSICLasso](https://pypi.org/project/pyHSICLasso/). + +![gramtimes](gramtimes.png) + + +#### `hisel` has a friendly user interface + +Getting started with `hisel` is as straightforward as the following code snippet: +``` + >>> import pandas as pd + >>> import hisel + >>> df = pd.read_csv('mydata.csv') + >>> xdf = df.iloc[:, :-1] + >>> yser = df.iloc[:, -1] + >>> hisel.feature_selection.select_features(xdf, yser) + ['d2', 'd7', 'c3', 'c10', 'c12', 'c24', 'c22', 'c21', 'c5'] +``` +If you are not interested in more details, +please read no further. +If you would like to +explore more about +how to tune the hyper-parameters used by `hisel` +or +how to have more advanced control on `hisel`'s selection, +please browse the examples in +[examples/](https://github.com/transferwise/hisel/tree/trunk/examples) +and in +[notebooks](https://github.com/transferwise/hisel/tree/trunk/notebooks). -This package provides an implementtion of the HSIC Lasso of [Yamada, M. et al. (2012)](https://arxiv.org/abs/1202.0515). -Usage is demontrated in the notebooks and in the scripts available under `examples/`. ## Installation +### Install via `pip` + The package `hisel` is available from `arti`. You can install it via `pip`. While on the Wise-VPN, in the environment where you intende to sue `hisel`, just do ``` pip install hisel --index-url=https://arti.tw.ee/artifactory/api/pypi/pypi-virtual/simple ``` +### Install from source + +#### Basic installation: +Checkout the repo and navigate to the root directory. Then, +``` +poetry install +``` + + +#### Installation with GPU support +You need to have cuda-toolkit installed and you need to know its version. +To know that, you can do +``` +nvidia-smi +``` +and read the cuda version from the top right corner of the table that is printed out. +Once you know your version of `cuda`, do +``` +poetry install -E cudaXXX +``` +where `cudaXXX` is one of the following: +`cuda102` if you have version 10.2; +`cuda110` if you have version 11.0; +`cuda111` if you have version 11.1; +`cuda11x` if you have version 11.2 - 11.8; +`cuda12x` if you have version 12.x. +This aligns to the [installation guide of CuPy](https://docs.cupy.dev/en/stable/install.html#installing-cupy). + -## Why is this cool? -Examples of where `hisel` outperforms the methods in -[sklearn.feature\_selection](https://scikit-learn.org/stable/modules/classes.html#module-sklearn.feature_selection) -are given in the notebooks -`ensemble-example.ipynb` -and -`nonlinear-trasnform.ipynb`. diff --git a/examples/feature_selection.py b/examples/feature_selection.py index 62e44bc..2bafa90 100644 --- a/examples/feature_selection.py +++ b/examples/feature_selection.py @@ -3,6 +3,7 @@ def main(): + # Minimial example of `hisel` usage df = pd.read_csv('mydata.csv') xdf = df.iloc[:, :-1] yser = df.iloc[:, -1] diff --git a/examples/minimal_with_params.py b/examples/minimal_with_params.py new file mode 100644 index 0000000..5c81511 --- /dev/null +++ b/examples/minimal_with_params.py @@ -0,0 +1,35 @@ +import pandas as pd +import hisel + + +def main(): + # Minimial example of `hisel` usage with specification of parameters + df = pd.read_csv('mydata.csv') + xdf = df.iloc[:, :-1] + yser = df.iloc[:, -1] + categorical_search_parameters = hisel.feature_selection.SearchParameters( + num_permutations=1, + im_ratio=.03, + max_iter=2, + parallel=True, + random_state=None, + ) + hsiclasso_parameters = hisel.feature_selection.HSICLassoParameters( + mi_threshold=.00001, + hsic_threshold=0.005, + batch_size=5000, + minibatch_size=500, + number_of_epochs=3, + use_preselection=True, + device=hisel.kernels.Device.CPU # if cuda is available you can pass GPU + ) + results = hisel.feature_selection.select_features( + xdf, yser, hsiclasso_parameters, categorical_search_parameters) + print('\n\n##########################################################') + print( + f'The following features are relevant for the prediction of {yser.name}:') + print(f'{results.selected_features}') + + +if __name__ == '__main__': + main() diff --git a/gramtimes.png b/gramtimes.png new file mode 100644 index 0000000..29afd1d Binary files /dev/null and b/gramtimes.png differ diff --git a/hisel/cudakernels.py b/hisel/cudakernels.py new file mode 100644 index 0000000..82d1ecd --- /dev/null +++ b/hisel/cudakernels.py @@ -0,0 +1,255 @@ +from typing import Optional +from joblib import Parallel, delayed +from enum import Enum +import numpy as np +from tqdm import tqdm +from hisel.kernels import KernelType, Device + +CUPY_AVAILABLE = True +try: + import cupy as cp +except (ModuleNotFoundError, ImportError): + print(f'Could not import cupy!') + cp = np + CUPY_AVAILABLE = False + + +def featwise( + x: cp.ndarray, + l: float, + kernel_type: KernelType, + catcont_split: Optional[int] = None +) -> cp.ndarray: + if kernel_type == KernelType.RBF: + return _rbf_featwise(x, l) + elif kernel_type == KernelType.DELTA: + return _delta_featwise(x) + elif kernel_type == KernelType.BOTH: + split = catcont_split if catcont_split else 0 + g_cat = _delta_featwise(x[:split, :].astype(int)) + g_cont = _rbf_featwise(x[split:, :], l) + g = cp.concatenate((g_cat, g_cont), axis=0) + return g + else: + raise ValueError(kernel_type) + + +def multivariate( + x: cp.ndarray, + l: float, + kernel_type: KernelType, + catcont_split: Optional[int] = None +) -> cp.ndarray: + if kernel_type == KernelType.RBF: + return _rbf_multivariate(x, l) + elif kernel_type == KernelType.DELTA: + return _delta_multivariate(x) + elif kernel_type == KernelType.BOTH: + split = catcont_split if catcont_split else 0 + g_cat = _delta_multivariate(x[:split, :].astype(int)) + g_cont = _rbf_multivariate(x[split:, :], l) + g = cp.concatenate((g_cat, g_cont), axis=0) + return g + else: + raise ValueError(kernel_type) + + +def _rbf_featwise( + x: cp.ndarray, + l: float +) -> cp.ndarray: + assert x.ndim == 2 + d, n = x.shape + z = cp.expand_dims(x, axis=2) + s = cp.expand_dims(x, axis=1) + s2 = cp.repeat( + cp.square(s), + repeats=n, + axis=1, + ) + z2 = cp.transpose(s2, (0, 2, 1)) + delta = z2 + s2 - 2*z @ s + grams = cp.exp(-delta / (2*l*l)) + return grams + + +def _delta_featwise( + x: cp.ndarray, +) -> cp.ndarray: + assert x.ndim == 2 + d, n = x.shape + assert x.dtype == int + s = cp.expand_dims(x, axis=1) + s2 = cp.repeat( + s, + repeats=n, + axis=1, + ) + z2 = cp.transpose(s2, (0, 2, 1)) + normalisation = cp.ones_like(s2) + for i in range(d): + cnt = cp.bincount(x[i, :]) + normalisation[i, :, :] = cnt[s2[i, :, :]] + grams = cp.asarray(s2 == z2, dtype=float) / normalisation + return grams + + +def _rbf_multivariate( + x: cp.ndarray, + l: float +) -> cp.ndarray: + nx = x.shape[1] + x2 = cp.tile( + cp.sum(cp.square(x), axis=0), + (nx, 1) + ) + delta = x2.T + x2 - 2 * x.T @ x + gram = cp.exp(-delta / (2 * l * l)) + return gram + + +def _rbf_hsic_b( + x: cp.ndarray +) -> cp.ndarray: + d, n = x.shape + x2 = cp.cumsum(cp.square(x), axis=0) + x2 = cp.expand_dims(x2, axis=1) + x2 = cp.repeat(x2, n, axis=1) + cross = cp.zeros(shape=(d, n, n)) + for i in range(d): + cross[i] = x[:i+1, :].T @ x[:i+1, :] + delta = x2.transpose(0, 2, 1) + x2 - 2 * cross + ls2 = cp.arange(1, d+1).reshape(d, 1, 1) + grams = cp.exp(-delta / (2 * ls2)) + return grams + + +def _delta_multivariate( + x: cp.ndarray, +) -> cp.ndarray: + assert x.dtype == int + nx = x.shape[1] + xmax = cp.roll(1 + cp.amax(x, axis=1, keepdims=True), 1) + xmax[0, 0] = 1 + xflat = cp.sum(x * xmax, axis=0, keepdims=True) + xx = cp.repeat( + xflat, + repeats=nx, + axis=0 + ) + cnt = cp.bincount(xflat[0, :]) + normalisation = cnt[xx] + gram = cp.asarray(xx == xx.T, dtype=float) / normalisation + return gram + + +def _delta_hsic_b( + x: cp.ndarray, +) -> cp.ndarray: + d, n = x.shape + grams = cp.empty(shape=(d, n, n), dtype=float) + for i in range(d): + grams[i, :, :] = _delta_multivariate(x[:i+1]) + return grams + + +def hsic_b( + x: cp.ndarray, + kernel_type: KernelType, +) -> cp.ndarray: + if kernel_type == KernelType.DELTA: + return _delta_hsic_b(x) + else: + return _rbf_hsic_b(x) + + +def multivariate_phi( + x: cp.ndarray, + l: float, + kernel_type: KernelType, + catcont_split: Optional[int] = None +) -> cp.ndarray: + gram = multivariate(x, l, kernel_type, catcont_split) + gram = cp.expand_dims(gram, axis=0) + return gram + + +def _centering_matrix(d: int, n: int) -> cp.ndarray: + id_ = cp.eye(n) + ids = cp.repeat(cp.expand_dims(id_, axis=0), repeats=d, axis=0) + ones = cp.ones_like(ids) + h = ids - ones / n + return h + + +def _center_gram_matmul( + g: cp.ndarray, + h: Optional[cp.ndarray] = None +) -> cp.ndarray: + if h is None: + h = _centering_matrix(g.shape[0], g.shape[2]) + return h @ g @ h + + +def _center_gram( + g: cp.ndarray, +) -> cp.ndarray: + g -= cp.mean(g, axis=-1, keepdims=True) + g -= cp.mean(g, axis=-2, keepdims=True) + return g + + +def _run_batch( + kernel_type: KernelType, + x: cp.ndarray, + l: float, + is_multivariate: bool = False, + catcont_split: Optional[int] = None, +) -> np.ndarray: + phi = multivariate_phi if is_multivariate else featwise + grams: cp.ndarray = _center_gram(phi(x, l, kernel_type, catcont_split)) + d, n, m = grams.shape + assert n == m + g_: cp.ndarray = cp.reshape(grams, (d, n*m)).T + g: np.ndarray + if CUPY_AVAILABLE: + g = cp.asnumpy(g_) + else: + g = np.array(g_) + return g + + +def _make_batches(x, batch_size): + _, n = x.shape + b = min(n, batch_size) + num_batches = n // b + if CUPY_AVAILABLE: + # move array from GPU to GPU + x = cp.array(x) + batches = cp.split(x[:, :num_batches * b], num_batches, axis=1) + return batches + + +def apply_feature_map( + kernel_type: KernelType, + x: np.ndarray, + l: float, + batch_size: int, + is_multivariate: bool = False, + catcont_split: Optional[int] = None, + # Unused variable, only to keep consistency with the same functions in hisel.kernels + device: Device = Device.GPU, +) -> np.ndarray: + d, n = x.shape + b = min(n, batch_size) + batches = _make_batches(x, batch_size) + num_of_batches = len(batches) + partial_phis = [_run_batch( + kernel_type, + batch, + l, + is_multivariate, + catcont_split, + ) for batch in tqdm(batches)] + phi: np.ndarray = np.vstack(partial_phis) + return phi diff --git a/hisel/feature_selection.py b/hisel/feature_selection.py index 3b1b4eb..49eebc7 100644 --- a/hisel/feature_selection.py +++ b/hisel/feature_selection.py @@ -3,6 +3,7 @@ import pandas as pd from dataclasses import dataclass from hisel import hsic, select, categorical +from hisel.kernels import Device from collections.abc import Mapping LassoSelection = select.Selection @@ -41,7 +42,7 @@ class HSICLassoParameters(Parameters): minibatch_size: int = 500 number_of_epochs: int = 4 use_preselection: bool = True - device: Optional[str] = None + device: Device = Device.CPU continuous_dtypes = [ diff --git a/hisel/kernels.py b/hisel/kernels.py index f558ccc..d08d682 100644 --- a/hisel/kernels.py +++ b/hisel/kernels.py @@ -11,6 +11,12 @@ class KernelType(Enum): BOTH = 2 +class Device(Enum): + CPU = 0 + PARALLEL_CPU = 1 + GPU = 2 + + def featwise( x: np.ndarray, l: float, @@ -23,7 +29,6 @@ def featwise( return _delta_featwise(x) elif kernel_type == KernelType.BOTH: split = catcont_split if catcont_split else 0 - # print(f'USING BOTH KERNEL TYPES; split: {split}') g_cat = _delta_featwise(x[:split, :].astype(int)) g_cont = _rbf_featwise(x[split:, :], l) g = np.concatenate((g_cat, g_cont), axis=0) @@ -237,14 +242,14 @@ def apply_feature_map( batch_size: int, is_multivariate: bool = False, catcont_split: Optional[int] = None, - no_parallel: bool = True + device: Device = Device.CPU, ) -> np.ndarray: d, n = x.shape b = min(n, batch_size) batches = _make_batches(x, batch_size) num_of_batches = len(batches) # _can_allocate(d, n, num_of_batches) - if no_parallel or num_of_batches < 2 or d*n < 100000: + if device != Device.PARALLEL_CPU or num_of_batches < 2 or d*n < 100000: partial_phis = [_run_batch( kernel_type, batch, diff --git a/hisel/select.py b/hisel/select.py index 0a7b190..f54b68b 100644 --- a/hisel/select.py +++ b/hisel/select.py @@ -4,18 +4,9 @@ from dataclasses import dataclass import numpy as np import pandas as pd -from hisel import lar, kernels -from hisel.kernels import KernelType +from hisel import lar, kernels, cudakernels +from hisel.kernels import KernelType, Device from sklearn.feature_selection import mutual_info_regression, mutual_info_classif -TORCH_AVAILABLE = True -try: - from hisel import torchkernels -except (ImportError, ModuleNotFoundError): - TORCH_AVAILABLE = False -try: - import torch -except (ImportError, ModuleNotFoundError): - TORCH_AVAILABLE = False class FeatureType(Enum): @@ -149,7 +140,7 @@ def projection_matrix(self, batch_size: int = 1000, minibatch_size: int = 200, number_of_epochs: int = 1, - device: Optional[str] = None, + device: Device = Device.CPU, ) -> np.ndarray: p: np.ndarray = np.zeros( (number_of_features, self.total_number_of_features)) @@ -182,10 +173,10 @@ def projection_matrix(self, y, number_of_features, minibatch_size, - device, self.xkerneltype, self.ykerneltype, catcont_split=self.catcont_split, + device=device, ) p += _to_projection_matrix( features, @@ -202,7 +193,7 @@ def select(self, batch_size: int = 10000, minibatch_size: int = 200, number_of_epochs: int = 1, - device: Optional[str] = None, + device: Device = Device.CPU, return_index: bool = False ) -> Union[Sequence[int], List[str]]: p = self.projection_matrix( @@ -223,7 +214,7 @@ def regularization_curve(self, batch_size: int = 1000, minibatch_size: int = 200, number_of_epochs: int = 1, - device: Optional[str] = None, + device: Device = Device.CPU, ): number_of_features = self.total_number_of_features - 1 features = self.select( @@ -248,7 +239,7 @@ def autoselect(self, minibatch_size: int = 200, number_of_epochs: int = 1, threshold: float = .01, - device: Optional[str] = None, + device: Device = Device.CPU, lasso_path: Optional[pd.DataFrame] = None, ) -> List[str]: if lasso_path is None: @@ -303,7 +294,7 @@ def select( minibatch_size: int = 800, number_of_epochs: int = 3, use_preselection: bool = False, - device: Optional[str] = None, + device: Device = Device.CPU, ) -> Selection: n, d = x.shape if use_preselection: @@ -421,10 +412,10 @@ def _run( y: np.ndarray, number_of_features: int, batch_size: int = 500, - device: Optional[str] = None, xkerneltype: Optional[KernelType] = None, ykerneltype: Optional[KernelType] = None, catcont_split: Optional[int] = None, + device: Device = Device.CPU, ): if xkerneltype is None: xkerneltype = KernelType.DELTA if x.dtype in ( @@ -441,34 +432,26 @@ def _run( ly = np.sqrt(dy) x_gram: np.ndarray y_gram: np.ndarray - if device is not None and not TORCH_AVAILABLE: - print( - f'You requested device: {device}, but torch is not available. Running on cpu instead with numpy') - if TORCH_AVAILABLE and device is not None: - x = torch.from_numpy(x) - y = torch.from_numpy(y) - x = x.to(device) - y = y.to(device) - xx = torchkernels.apply_feature_map( - xkerneltype, - x.T, lx, batch_size, is_multivariate=False) - yy = torchkernels.apply_feature_map( - ykerneltype, - y.T, ly, batch_size, is_multivariate=True) - x_gram = xx.detach().cpu().numpy() - y_gram = yy.detach().cpu().numpy() + + gram_maker: Callable[[...], np.ndarray] + if device == Device.GPU: + gram_maker = cudakernels.apply_feature_map else: - x_gram = kernels.apply_feature_map( - xkerneltype, - x.T, lx, - batch_size, - is_multivariate=False, - catcont_split=catcont_split) - y_gram = kernels.apply_feature_map( - ykerneltype, - y.T, ly, - batch_size, - is_multivariate=True) + gram_maker = kernels.apply_feature_map + + x_gram = gram_maker( + xkerneltype, + x.T, lx, + batch_size, + is_multivariate=False, + catcont_split=catcont_split, + device=device) + y_gram = gram_maker( + ykerneltype, + y.T, ly, + batch_size, + is_multivariate=True, + device=device) assert x_gram.shape == (gram_dim, dx) assert not np.any(np.isnan(x_gram)) assert y_gram.shape == (gram_dim, 1) diff --git a/notebooks/ensemble-example.ipynb b/notebooks/ensemble-example.ipynb index a3884c2..b23f357 100644 --- a/notebooks/ensemble-example.ipynb +++ b/notebooks/ensemble-example.ipynb @@ -10,7 +10,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "802e8c73", "metadata": {}, "outputs": [], @@ -27,7 +27,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "798f7c6d", "metadata": {}, "outputs": [], @@ -39,7 +39,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "50b99be8", "metadata": {}, "outputs": [], @@ -60,7 +60,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "id": "e6236e9e", "metadata": {}, "outputs": [], @@ -71,17 +71,17 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "id": "f83edaef", "metadata": {}, "outputs": [], "source": [ - "sns.scatterplot(x = x0[:, 0] - x1[:, 0], y = y[:, 0])" + "# sns.scatterplot(x = x0[:, 0] - x1[:, 0], y = y[:, 0])" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "id": "140b9f88", "metadata": {}, "outputs": [], @@ -109,10 +109,18 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "id": "139b18ff", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "ksg-mi preprocessing: 20 features are pre-selected\n" + ] + } + ], "source": [ "ksgselection, mis = select.ksgmi(xdf, ydf, threshold=0.01)\n", "ksg_selection = [int(feat.split('x')[-1]) for feat in ksgselection]" @@ -120,10 +128,19 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "id": "5ffca204", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Expected features: [4, 19]\n", + "Marginal KSG selection: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]\n" + ] + } + ], "source": [ "print(f'Expected features: {sorted(expected_features)}')\n", "print(f'Marginal KSG selection: {sorted(ksg_selection)}')" @@ -139,10 +156,32 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "id": "e281fe2f", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Number of categorical features: 20\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|█████████████████████████████████████| 152/152 [00:00<00:00, 472247.56it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Number of selected categorical features: 2\n" + ] + } + ], "source": [ "results = categorical.select(\n", " xdf, ydf,\n", @@ -155,13 +194,22 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "id": "4e5c9a81", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Expected features: [4, 19]\n", + "HISEL selection: [4, 19]\n" + ] + } + ], "source": [ "print(f'Expected features: {sorted(expected_features)}')\n", - "print(f'Marginal KSG selection: {sorted(hisel_selection)}')" + "print(f'HISEL selection: {sorted(hisel_selection)}')" ] }, { @@ -174,7 +222,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "id": "38056f04", "metadata": {}, "outputs": [], @@ -193,10 +241,19 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "id": "92bc809f", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "HSIC-estimated dependence between correct selection and target: 1.0000000000000002\n", + "HSIC-estimated dependence between random selection and target: 0.25537749584046077\n" + ] + } + ], "source": [ "print(f'HSIC-estimated dependence between correct selection and target: {correct_dependence}')\n", "print(f'HSIC-estimated dependence between random selection and target: {random_dependence}')" diff --git a/notebooks/nonlinear-transform.ipynb b/notebooks/nonlinear-transform.ipynb index b04f89c..b41ade9 100644 --- a/notebooks/nonlinear-transform.ipynb +++ b/notebooks/nonlinear-transform.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "fd61a5c3", "metadata": {}, "outputs": [], @@ -15,213 +15,6 @@ "from hisel.select import HSICSelector as Selector" ] }, - { - "cell_type": "markdown", - "id": "c2559eae", - "metadata": {}, - "source": [ - "# Sin transform " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4b761492", - "metadata": {}, - "outputs": [], - "source": [ - "dim_x = 10\n", - "dim_y = 1\n", - "dim_z = 1\n", - "\n", - "batch_size = int(1e+4)\n", - "minibatch_size = 250\n", - "num_of_samples = int(1e+4)\n", - "number_of_epochs = 3" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "0d4f2545", - "metadata": {}, - "outputs": [], - "source": [ - "transform_tilde = np.eye(dim_z)[:dim_y]\n", - "A = np.random.permutation(np.concatenate((np.eye(dim_z), np.zeros((dim_z, dim_x - dim_z))), axis=1).T).T\n", - "transform = transform_tilde @ A" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "bde18951", - "metadata": {}, - "outputs": [], - "source": [ - "x_samples = np.random.uniform(size=(num_of_samples, dim_x))\n", - "tt = np.repeat(np.expand_dims(transform, axis=0), repeats=num_of_samples, axis=0)\n", - "prey = (tt @ np.expand_dims(x_samples, axis=2))[:, :, 0]\n", - "y_samples = np.random.normal(0, 3e-1, size=prey.shape) \n", - "y_samples[:, 0] += np.sin(2*np.pi*prey[:, 0])" - ] - }, - { - "cell_type": "markdown", - "id": "1d0b9a75", - "metadata": {}, - "source": [ - "### Viz of relations between target and features" - ] - }, - { - "cell_type": "markdown", - "id": "9f2e819f", - "metadata": {}, - "source": [ - "Relation between $y$ and the correct feature" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9fc8c15a", - "metadata": {}, - "outputs": [], - "source": [ - "expected_features = np.argsort(np.sum(A, axis=0))[::-1][:dim_z]\n", - "sns.scatterplot(x=x_samples[:, expected_features[0]], y=y_samples[:, 0])" - ] - }, - { - "cell_type": "markdown", - "id": "f4305414", - "metadata": {}, - "source": [ - "Relation between $y$ and a wrong feature" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f0661e9a", - "metadata": {}, - "outputs": [], - "source": [ - "nonrelevant = set(range(dim_x)).difference(set(expected_features))\n", - "featureidx = np.random.choice(list(nonrelevant))\n", - "sns.scatterplot(x=x_samples[:, featureidx], y=y_samples[:, 0])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4c8caf8a", - "metadata": {}, - "outputs": [], - "source": [ - "projector = Selector(x_samples, y_samples)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7e198c95", - "metadata": {}, - "outputs": [], - "source": [ - "curve = projector.regularization_curve(\n", - " batch_size=batch_size,\n", - " minibatch_size=minibatch_size,\n", - " number_of_epochs=number_of_epochs\n", - ")\n", - "paths = projector.lasso_path()" - ] - }, - { - "cell_type": "markdown", - "id": "6551e522", - "metadata": {}, - "source": [ - "#### Sorted features by decreasing importance" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a503fa32", - "metadata": {}, - "outputs": [], - "source": [ - "print(f'Sorted features by decreasing importance: {projector.ordered_features}')" - ] - }, - { - "cell_type": "markdown", - "id": "3b6679bf", - "metadata": {}, - "source": [ - "### Test selection" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "65f990cd", - "metadata": {}, - "outputs": [], - "source": [ - "expected_features = np.argsort(np.sum(A, axis=0))[::-1][:dim_z]\n", - "noise_features = set(range(dim_x)).difference(set(expected_features))\n", - "selected_features = np.argsort(paths.iloc[-1, :])[::-1][:dim_z]\n", - "print(f'Expected features: {sorted(list(expected_features))}')\n", - "print(f'Selected features: {sorted(list(selected_features))}')" - ] - }, - { - "cell_type": "markdown", - "id": "a8bf88af", - "metadata": {}, - "source": [ - "## Comparison with sklearn" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "332ba768", - "metadata": {}, - "outputs": [], - "source": [ - "from sklearn.feature_selection import f_regression, mutual_info_regression" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "0b76ba28", - "metadata": {}, - "outputs": [], - "source": [ - "fstats, _ = f_regression(x_samples, np.linalg.norm(y_samples, axis=1))\n", - "fstats /= np.max(fstats)\n", - "f_selection = np.argmax(fstats)\n", - "print(f'f_selection: {f_selection}')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ceec08f5", - "metadata": {}, - "outputs": [], - "source": [ - "mi = mutual_info_regression(x_samples, np.linalg.norm(y_samples, axis=1))\n", - "mi /= np.max(mi)\n", - "mi_selection = np.argmax(mi)\n", - "print(f'mi_selection: {mi_selection}')" - ] - }, { "cell_type": "markdown", "id": "7771b83e", @@ -232,7 +25,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "10edf512", "metadata": {}, "outputs": [], @@ -249,7 +42,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "9ac11521", "metadata": {}, "outputs": [], @@ -261,18 +54,18 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "id": "15eb3b4c", "metadata": {}, "outputs": [], "source": [ "x_samples = np.random.uniform(size=(num_of_samples, dim_x))\n", - "tt = np.repeat(np.expand_dims(transform, axis=0), repeats=num_of_samples, axis=0)\n", - "prey = (tt @ np.expand_dims(x_samples, axis=2))[:, :, 0]\n", - "y_samples = np.random.normal(0, 1e-2, size=prey.shape) # np.zeros_like(prey)\n", - "y_samples[:, 0] = np.sin(2*np.pi*prey[:, 0])\n", - "y_samples[:, 1] = np.cos(2*np.pi*prey[:, 1])\n", - "y_samples[:, 2] = np.sin(2*np.pi*prey[:, 2])" + "tt = np.expand_dims(transform, axis=0)\n", + "prey = (tt @ np.expand_dims(np.square(x_samples), axis=2))[:, :, 0]\n", + "y_samples = np.random.normal(0, 1e-1, size=prey.shape) # np.zeros_like(prey)\n", + "y_samples[:, 0] += np.sin(2*np.pi*prey[:, 0])\n", + "y_samples[:, 1] += np.cos(2*np.pi*prey[:, 1])\n", + "y_samples[:, 2] += np.sin(2*np.pi*prey[:, 2])" ] }, { @@ -301,13 +94,13 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "id": "cbb24917", "metadata": {}, "outputs": [], "source": [ "expected_features = np.argsort(np.sum(A, axis=0))[::-1][:dim_z]\n", - "sns.scatterplot(x=x_samples[:, expected_features[0]], y=y_samples[:, 0])" + "# sns.scatterplot(x=x_samples[:, expected_features[0]], y=y_samples[:, 0])" ] }, { @@ -320,32 +113,58 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "id": "12f86383", "metadata": {}, "outputs": [], "source": [ "nonrelevant = set(range(dim_x)).difference(set(expected_features))\n", "featureidx = np.random.choice(list(nonrelevant))\n", - "sns.scatterplot(x=x_samples[:, featureidx], y=y_samples[:, 0])" + "# sns.scatterplot(x=x_samples[:, featureidx], y=y_samples[:, 0])" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "id": "49701075", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "HSIC feature selection\n", + "Feature type of x: FeatureType.CONT\n", + "Feature type of y: FeatureType.CONT\n", + "Data type of x: float64\n", + "Data type of y: float64\n", + "Total number of features: 20\n", + "Dimensionality of target: 3\n", + "Number of x samples: 10000\n", + "Number of y samples: 10000\n" + ] + } + ], "source": [ "projector = Selector(x_samples, y_samples)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "id": "1af2d5e4", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|█████████████████████████████████████████| 120/120 [00:01<00:00, 92.06it/s]\n", + "100%|███████████████████████████████████████| 120/120 [00:00<00:00, 2310.29it/s]\n" + ] + } + ], "source": [ "curve = projector.regularization_curve(\n", " batch_size=batch_size,\n", @@ -356,7 +175,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "id": "c96e444f", "metadata": {}, "outputs": [], @@ -374,10 +193,18 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "id": "59656d81", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Sorted features by decreasing importance: [16, 15, 5, 14, 19, 8, 3, 10, 0, 18, 17, 1, 9, 4, 6, 2, 13, 7, 12]\n" + ] + } + ], "source": [ "print(f'Sorted features by decreasing importance: {projector.ordered_features}')" ] @@ -392,10 +219,19 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "id": "55d268d9", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Expected features: [5, 14, 15, 16, 19]\n", + "Selected features: [5, 14, 15, 16, 19]\n" + ] + } + ], "source": [ "expected_features = np.argsort(np.sum(A, axis=0))[::-1][:dim_z]\n", "noise_features = set(range(dim_x)).difference(set(expected_features))\n", @@ -414,7 +250,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "id": "30f6f83b", "metadata": {}, "outputs": [], @@ -424,29 +260,56 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 13, "id": "24df734c", "metadata": {}, - "outputs": [], - "source": [ - "fstats, _ = f_regression(x_samples, np.linalg.norm(y_samples, axis=1))\n", - "fstats /= np.max(fstats)\n", - "f_selection = np.argsort(fstats)[::-1][:dim_z]\n", - "print(f'f_selection: {sorted(f_selection)}')" + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Expected features: [5, 14, 15, 16, 19]\n", + "F-selected features: [5, 14, 15, 16, 18]\n" + ] + } + ], + "source": [ + "fstats = np.zeros(shape=(dim_x, dim_z), dtype=float)\n", + "for i in range(dim_y):\n", + " fstats[:, i], _ = f_regression(x_samples, y_samples[:, i])\n", + "fstat = np.sum(fstats, axis=1)\n", + "fstat /= np.max(fstat)\n", + "f_selection = np.argsort(fstat)[::-1][:dim_z]\n", + "print(f'Expected features: {sorted(list(expected_features))}')\n", + "print(f'F-selected features: {sorted(f_selection)}')" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 14, "id": "d84477ee", "metadata": {}, - "outputs": [], - "source": [ - "mi = mutual_info_regression(x_samples, np.linalg.norm(y_samples, axis=1))\n", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Expected features: [5, 14, 15, 16, 19]\n", + "MI-selected features: [5, 6, 14, 15, 16]\n", + "mutual information: [1. 0.29872739 0.17315844 0.10163843 0.06594454]\n" + ] + } + ], + "source": [ + "mis = np.zeros(shape=(dim_x, dim_z), dtype=float)\n", + "for i in range(dim_y):\n", + " mis[:, i] = mutual_info_regression(x_samples, y_samples[:, i])\n", + "mi = np.sum(mis, axis=1)\n", "mi /= np.max(mi)\n", "mi_selection = np.argsort(mi)[::-1][:dim_z]\n", - "print(f'mutual information: {mi[mi_selection]}')\n", - "print(f'mi_selection: {sorted(mi_selection)}')" + "print(f'Expected features: {sorted(list(expected_features))}')\n", + "print(f'MI-selected features: {sorted(mi_selection)}')\n", + "print(f'mutual information: {mi[mi_selection]}')" ] }, { @@ -467,10 +330,31 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 15, "id": "71c90034", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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dXWaMmWKfvGLX3T4+f3336+HH35Yv/jFL/TWW28pNzdXc+fODd3/qyN79+5VSUmJFi5cqHfeeUcLFy7UggUL9MYbb8RqN3qkq32tr6/X22+/rTvuuENvv/22tm7dqkOHDumqq67qdrsOh6PNcXa5XLLb7bHYhbB0d2wl6YorrmhT97Zt27rcZl88tpLaHZ/HH39cFotF3/72t7vcbiIe25781iTk99ZAwjp58qQhyXjllVc6bbNjxw5DkvH555/Hr7AouOuuu4wpU6b0uP2//uu/GmPHjm2z7B/+4R+MGTNmRLmy+LjllluM888/3/D7/R1+3lePqyTjueeeC733+/1Gbm6uce+994aWNTY2Gk6n0/jlL3/Z6XYWLFhgXHHFFW2WXX755ca1114b9Zojde6+duTNN980JBmffPJJp202bdpkOJ3O6BYXAx3t7w033GBcffXVYW2nvxzbq6++2vja177WZZu+cmzP/a1J1O8tPSwJrLq6WpI0ZMiQbttOnTpVeXl5+vrXv64dO3bEurSo+Oijj5Sfn6/Ro0fr2muv1eHDhzttu3fvXhUXF7dZdvnll+vPf/6zmpqaYl1qVHm9Xv3mN7/RD37wg25v1NkXj+vZjhw5osrKyjbHzmaz6dJLL9WePXs6Xa+z493VOomourpaFotFWVlZXbarra1VYWGhRowYoW9+85sqKyuLT4FRsHPnTg0bNkxf/vKX9cMf/lAnT57ssn1/OLYnTpzQiy++qMWLF3fbti8c23N/axL1e0tgSVCGYWjZsmW6+OKLNXHixE7b5eXlaePGjXr22We1detWjRkzRl//+te1a9euOFYbvunTp+vJJ5/U9u3b9eijj6qyslKzZs3SmTNnOmxfWVmpnJycNstycnLU3Nys06dPx6PkqHn++edVVVWlG2+8sdM2ffW4nquyslKSOjx2wc86Wy/cdRJNY2Ojli9fruuvv77Lm8WNHTtWTzzxhF544QVt3rxZdrtds2fP1kcffRTHaiMzb948/fa3v9Wf/vQnPfTQQ3rrrbf0ta99TR6Pp9N1+sOx/fWvf63MzEx961vf6rJdXzi2Hf3WJOr3tt/crbm/+fGPf6x3331Xr776apftxowZozFjxoTez5w5U8eOHdODDz6oSy65JNZlRmzevHmh15MmTdLMmTN1/vnn69e//rWWLVvW4Trn9kYYLZM0d9dLkWgee+wxzZs3T/n5+Z226avHtTMdHbvujlsk6ySKpqYmXXvttfL7/Vq/fn2XbWfMmNFmoOrs2bN14YUX6uc//7n+8z//M9al9kpJSUno9cSJEzVt2jQVFhbqxRdf7PLHvC8fW0l6/PHH9d3vfrfbsSh94dh29VuTaN9belgS0D/+4z/qhRde0I4dOzRixIiw158xY0ZCJfieyMjI0KRJkzqtOzc3t11KP3nypFJSUnTeeefFo8So+OSTT/Tyyy/rpptuCnvdvnhcg1d+dXTszv0/sXPXC3edRNHU1KQFCxboyJEjKi0t7bJ3pSNJSUm66KKL+tyxlgI9g4WFhV3W3pePrSTt3r1bBw8ejOg7nGjHtrPfmkT93hJYEohhGPrxj3+srVu36k9/+pNGjx4d0XbKysqUl5cX5epiy+Px6IMPPui07pkzZ4aurAl66aWXNG3aNKWmpsajxKjYtGmThg0bpiuvvDLsdfvicR09erRyc3PbHDuv16tXXnlFs2bN6nS9zo53V+skgmBY+eijj/Tyyy9HFKYNw9D+/fv73LGWpDNnzujYsWNd1t5Xj23QY489pqKiIk2ZMiXsdRPl2Hb3W5Ow39uoDN1FVPzoRz8ynE6nsXPnTsPlcoUe9fX1oTbLly83Fi5cGHr/H//xH8Zzzz1nHDp0yPjLX/5iLF++3JBkPPvss2bsQo/95Cc/MXbu3GkcPnzYeP31141vfvObRmZmpnH06FHDMNrv5+HDh4309HTj1ltvNd5//33jscceM1JTU41nnnnGrF0Im8/nM0aOHGncdttt7T7ry8e1pqbGKCsrM8rKygxJxsMPP2yUlZWFroy59957DafTaWzdutU4cOCAcd111xl5eXmG2+0ObWPhwoXG8uXLQ+9fe+01Izk52bj33nuNDz74wLj33nuNlJQU4/XXX4/7/p2tq31tamoyrrrqKmPEiBHG/v3723yHPR5PaBvn7uuqVauMP/zhD8Zf//pXo6yszPj+979vpKSkGG+88YYZu9hGV/tbU1Nj/OQnPzH27NljHDlyxNixY4cxc+ZMY/jw4f3u2AZVV1cb6enpxoYNGzrcRl85tj35rUnE7y2BJYFI6vCxadOmUJsbbrjBuPTSS0Pv77vvPuP888837Ha7MXjwYOPiiy82XnzxxfgXH6aSkhIjLy/PSE1NNfLz841vfetbxnvvvRf6/Nz9NAzD2LlzpzF16lTDarUao0aN6vQ/Golq+/bthiTj4MGD7T7ry8c1eAn2uY8bbrjBMIzAJZJ33XWXkZuba9hsNuOSSy4xDhw40GYbl156aah90NNPP22MGTPGSE1NNcaOHZsQYa2rfT1y5Ein3+EdO3aEtnHuvi5dutQYOXKkYbVajezsbKO4uNjYs2dP/HeuA13tb319vVFcXGxkZ2cbqampxsiRI40bbrjBKC8vb7ON/nBsgx555BEjLS3NqKqq6nAbfeXY9uS3JhG/t5aW4gEAABIWY1gAAEDCI7AAAICER2ABAAAJj8ACAAASHoEFAAAkPAILAABIeAQWAACQ8AgsAAAg4RFYAABAwiOwAACAhEdgAQAACY/AAgAAEt7/B5+VdIJQQfHzAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "plt.plot(np.arange(1, 1+len(curve)), curve)" ] @@ -485,10 +369,31 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 16, "id": "d4ae8aab", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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YL+fu3XrVp1//vlKSEjQ+eefr+eeey5CPQhNX/12uVxatmyZLrjgAo0ZM0aZmZm66aabdPz48T4/88knnwz496ClpSXCvQlOf9f8m9/8Zo8+XHbZZf1+brxfc6n/vge6fiaTSQ899FCvnzlUrnu4EVji0K5du3TnnXfq1VdfVXl5udrb21VQUKBTp071e+6hQ4dUW1vre33iE5+IQovD55Of/KRf+998881e61ZXV2vBggXKz89XZWWlVqxYoSVLlmjr1q1RbHF4vPHGG379Li8vlyR95Stf6fO8oXi9T506pYsuukhr164NePxnP/uZHnnkEa1du1ZvvPGGMjIydPXVV/ueFxZIRUWFCgsLtXjxYh04cECLFy/WDTfcoNdeey1S3QhaX/0+ffq0/vGPf+iHP/yh/vGPf+jZZ5/V22+/rS9+8Yv9fm5SUpLf34Ha2lo5HI5IdCFk/V1zSbrmmmv8+lBWVtbnZw6Fay713/czr93mzZtlMpm0aNGiPj93KFz3sDMQ9+rr6w1Jxq5du3qts2PHDkOS8eGHH0avYWF23333GRdddNGA63//+983ZsyY4Vf2rW99y7jsssvC3LLou+uuu4zp06cbHo8n4PHhcL0NwzAkGc8995zvvcfjMTIyMowHH3zQV9bS0mIkJycbGzdu7PVzbrjhBuOaa67xK5s3b55x4403hr3N4XBmvwN5/fXXDUnG0aNHe63zxBNPGMnJyeFtXIQF6vvNN99sXH/99UF9zlC75oYxsOt+/fXXG5/97Gf7rDMUr3s4MMIyBDQ1NUmSzjrrrH7rXnLJJZo4caI+97nPaceOHZFuWtgdPnxYmZmZys7O1o033qgjR470WreiokIFBQV+ZfPmzdPevXvlcrki3dSIaWtr09NPP61bb7213wd5DvXrfabq6mrV1dX5XdeEhARdddVV2rNnT6/n9fZ3oa9z4l1TU5NMJpPGjRvXZ72TJ09qypQpmjRpkq677jpVVlZGp4FhtnPnTqWlpencc8/VHXfcofr6+j7rD8drfuLECT3//PO67bbb+q07XK57MAgscc4wDJWUlOiKK67QrFmzeq03ceJEPfroo9q6daueffZZnXfeefrc5z6nl156KYqtHZw5c+boqaee0gsvvKDHHntMdXV1mjt3rhobGwPWr6urU3p6ul9Zenq62tvb1dDQEI0mR8Sf/vQnffTRR/rmN7/Za53hcL0Dqaurk6SA19V7rLfzgj0nnrW0tOjee+/V1772tT4ffjdjxgw9+eST2rZtm5555hk5HA5dfvnlOnz4cBRbO3jz58/Xb3/7W/3973/Xz3/+c73xxhv67Gc/q9bW1l7PGW7XXJJ+/etfKzExUV/+8pf7rDdcrnuwhs3Tmoer7373u/rnP/+pl19+uc965513ns477zzf+7y8PB07dkwPP/ywrrzyykg3Myzmz5/v+/qCCy5QXl6epk+frl//+tcqKSkJeM6ZIxBG58bN/Y1MxLNNmzZp/vz5yszM7LXOcLjefQl0Xfu7pqGcE49cLpduvPFGeTwerV+/vs+6l112md/k1Msvv1yzZ8/WL3/5S61ZsybSTQ2bwsJC39ezZs1Sbm6upkyZoueff77PX97D5Zp7bd68WV//+tf7nYsyXK57sBhhiWPf+973tG3bNu3YsUOTJk0K+vzLLrtsSCfuMWPG6IILLui1DxkZGT3+NVVfXy+r1aqUlJRoNDHsjh49qhdffFG333570OcO9estybcqLNB1PfNf02eeF+w58cjlcumGG25QdXW1ysvL+xxdCcRsNuvSSy8d8n8PJk6cqClTpvTZj+Fyzb12796tQ4cOhfTf/nC57v0hsMQhwzD03e9+V88++6z+/ve/Kzs7O6TPqays1MSJE8PcuuhpbW1VVVVVr33Iy8vzrabx2r59u3Jzc2Wz2aLRxLB74oknlJaWpmuvvTboc4f69Zak7OxsZWRk+F3XtrY27dq1S3Pnzu31vN7+LvR1TrzxhpXDhw/rxRdfDCl0G4ah/fv3D/m/B42NjTp27Fif/RgO17y7TZs2KScnRxdddFHQ5w6X696v2M33RW++/e1vG8nJycbOnTuN2tpa3+v06dO+Ovfee6+xePFi3/tf/OIXxnPPPWe8/fbbxv/+7/8a9957ryHJ2Lp1ayy6EJK7777b2Llzp3HkyBHj1VdfNa677jojMTHReO+99wzD6NnnI0eOGKNHjzaKi4uNf/3rX8amTZsMm81m/PGPf4xVFwbF7XYbkydPNpYtW9bj2HC63s3NzUZlZaVRWVlpSDIeeeQRo7Ky0rca5sEHHzSSk5ONZ5991njzzTeNr371q8bEiRMNp9Pp+4zFixcb9957r+/9K6+8YlgsFuPBBx80qqqqjAcffNCwWq3Gq6++GvX+9aavfrtcLuOLX/yiMWnSJGP//v1+/923trb6PuPMft9///3GX//6V+Pdd981KisrjVtuucWwWq3Ga6+9Fosu9qqvvjc3Nxt33323sWfPHqO6utrYsWOHkZeXZ5x99tlD/pobRv9/3w3DMJqamozRo0cbGzZsCPgZQ/W6hxuBJQ5JCvh64oknfHVuvvlm46qrrvK9/+lPf2pMnz7dcDgcxvjx440rrrjCeP7556Pf+EEoLCw0Jk6caNhsNiMzM9P48pe/bBw8eNB3/Mw+G4Zh7Ny507jkkksMu91uTJ06tdf/4IeCF154wZBkHDp0qMex4XS9vUuyz3zdfPPNhmF0LG2+7777jIyMDCMhIcG48sorjTfffNPvM6666ipffa8//OEPxnnnnWfYbDZjxowZcRfe+up3dXV1r//d79ixw/cZZ/Z76dKlxuTJkw273W6kpqYaBQUFxp49e6LfuX701ffTp08bBQUFRmpqqmGz2YzJkycbN998s1FTU+P3GUPxmhtG/3/fDcMwfvWrXxmjRo0yPvroo4CfMVSve7iZDKNzliIAAECcYg4LAACIewQWAAAQ9wgsAAAg7hFYAABA3COwAACAuEdgAQAAcY/AAgAA4h6BBQAAxD0CCwAAiHsEFgAAEPcILAAAIO4RWAAAQNz7/wG87r8BlxaMEwAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "plt.plot(np.arange(1, len(curve)), np.abs(np.diff(curve)))" ] @@ -503,10 +408,31 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 17, "id": "df408f64", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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dTCPD5NLT0/HP//5T7z99ts4evQo5s+fj+LiYsydOxeA5fC1Rx55xF5/7ty5OHXqFNLT03H06FG8/fbbWLNmDX7zm9/Y60ydOhWrVq3Chx9+iMLCQmRnZ+OFF17AXXfdBVmW22AziYiInG3evBmZmZnYuHEjDAYDKioqMHXqVERHR0OSJGzYsMFlmY8//hhTpkxBeHg4Eygioi7Ao8PkAGDGjBm4cOEClixZAoPBgMTERGzatAlxcXEAAIPB4HTNofj4eGzatAnz58/Hm2++iejoaLzxxhu499577XWef/55SJKE559/HmfOnEGvXr0wdepUvPTSS22wiURERK4KCgoQFRVlP7IhLy8Pw4cPx6OPPur0HeWouroa48ePx3333YcnnniiI8MlIqJ24PF1hrwVrzNERNTxXK6hIwRgrFEnGF0Q0MxgPo5mz56NrKws+3RcXByKiors05IkYf369Zg+fbrb5YuKihAfH4+8vDyMGDGi2XXxOkNERB2vpbmBx3uGiIiImmSsAV6OVmfdi84CfsEtqrp8+XIkJCQgIyMDOTk5PCSbiMhHMRkiIiKfo9frERISAlmWERkZqXY4RESkEiZDRETUdnRBlj00aq2biIjIA0yGiIio7UhSiw9VIyIiUpvHQ2sTERERERF1BdwzREREBKCqqgonT560TxcWFiI/Px9hYWGIjY0FAPz0008oLi7G2bOWQwGPHTsGAIiMjOS5R0REnRD3DBEREQHYt28fRo4ciZEjRwKwXGR85MiR+P3vf2+v8+mnn2LkyJG48847AQAPPPAARo4cidWrV6sSMxERXR1eZ4iIiFqN19C5Mj5GREQdr6W5AfcMERERERGRT2IyREREREREPonJEBERERER+SQmQ0RERERE5JOYDBERERERkU9iMkRERERERD6JyRAREREREfkkJkNEREREROSTmAwREREREZFPYjJEREQ+SQiBOXPmICwsDJIkIT8/X+2QiIiogzEZIiIin7R582ZkZmZi48aNMBgMqKiowNSpUxEdHQ1JkrBhwwan+kajEc899xyGDh2K4OBgREdH45FHHsHZs2fV2QAiIrpqTIaIiMgnFRQUICoqCsnJyYiMjER1dTWGDx+OFStWuK1fU1OD/fv344UXXsD+/fvx8ccf4/jx47jrrrs6OHIiImorWrUDICKirkMIgcumy6qs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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "paths.plot(figsize=(10, 5))" ] diff --git a/poetry.lock b/poetry.lock index bcd6d9a..8614e66 100644 --- a/poetry.lock +++ b/poetry.lock @@ -12,16 +12,232 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "cupy-cuda102" +version = "12.2.0" +description = "CuPy: NumPy & SciPy for GPU" +category = "main" +optional = true +python-versions = ">=3.8" +files = [ + {file = "cupy_cuda102-12.2.0-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:d030553ce8b8f654f4ffa6b80bfa0c0f8f16c8d63a31aa55ddda127d072e2343"}, + {file = "cupy_cuda102-12.2.0-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:c777b74be9be62b2fce8f6b9f8a174364d771b4f0b0039f8c9626b8b28b4bdaf"}, + {file = "cupy_cuda102-12.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:2276e9f7726af9723488557f1979de1c34a3ff843da319220fc34ede3446beb4"}, + {file = "cupy_cuda102-12.2.0-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:3c579856bd9cbf8ecc01d2af9984f922addd9590d46c297f0b5aff1de2fc12e4"}, + {file = "cupy_cuda102-12.2.0-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:339f623093cc4c933af56a9cb6db478d730dd7721db580c89374a16002711d57"}, + {file = "cupy_cuda102-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:df0278e43bc9927110797df7ff549026b2cd77173bd739b479734af61ff83874"}, + {file = "cupy_cuda102-12.2.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:350e226420edce4b1152ec3bcf4202fb70ab9544e030a152f3e0fe7df2b35c54"}, + {file = "cupy_cuda102-12.2.0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:4948017d1bc07a9fe86679e0fe0826e59f6b708ffd49faa141d5950320f51e7b"}, + {file = "cupy_cuda102-12.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:9aa916bdb5100092811fff9365f7626c8090affd1031d64b1e5ff49720853f81"}, + {file = "cupy_cuda102-12.2.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d843ad35ed998ec288ad423b4eed0645ac973354170844ddcdc104c5bf4afea3"}, + {file = "cupy_cuda102-12.2.0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:f7a8649df759d5387ad785b0ccfc3183c98493854dd174e37c37f41a05ebe1e0"}, + {file = "cupy_cuda102-12.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:d722e21ed14b2a6a00270eb7073b2ecbe1f1abd456afeb55ab0adc13e6e2c038"}, +] + +[package.dependencies] +fastrlock = ">=0.5" +numpy = ">=1.20,<1.27" + +[package.extras] +all = ["Cython (>=0.29.22,<3)", "optuna (>=2.0)", "scipy (>=1.6,<1.13)"] +stylecheck = ["autopep8 (==1.5.5)", "flake8 (==3.8.4)", "mypy (==1.4.1)", "pbr (==5.5.1)", "pycodestyle (==2.6.0)", "types-setuptools (==57.4.14)"] +test = ["hypothesis (>=6.37.2,<6.55.0)", "pytest (>=7.2)"] + +[[package]] +name = "cupy-cuda110" +version = "12.2.0" +description = "CuPy: NumPy & SciPy for GPU" +category = "main" +optional = true +python-versions = ">=3.8" +files = [ + {file = "cupy_cuda110-12.2.0-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:f6c99ddc57574cd2881a3f7245ef5bb743f97e94f8fef2f156e3b3bf182e9ef4"}, + {file = "cupy_cuda110-12.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:e25b75931525875f9b9f75f5ff7d093c4aece239d06cc055bbee41844f026525"}, + {file = "cupy_cuda110-12.2.0-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:537443cd69667cf89fb40f384d0af1e61449e3083853fa3b14930ddd5a97955d"}, + {file = "cupy_cuda110-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:636d99bed70b6c3677e86ffb5c4eef07a67254b4bf8e19e030de82984df5f945"}, + {file = "cupy_cuda110-12.2.0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:6ca5c6c0f8531ad83d6e10b77ea4cb23207394461a5b4747990ef49643468a61"}, + {file = "cupy_cuda110-12.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:605b412a87bca41fbfff574375b54371565f1872f9c988e68bd7fa811942d839"}, + {file = "cupy_cuda110-12.2.0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:f52887b95c61634b1036a128e9475334d3cf117fc9513e922d54418d25a48a31"}, + {file = "cupy_cuda110-12.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:42e3759c7be23847539d9439224df3903296cf2d61f3082f83716bf67de7e160"}, +] + +[package.dependencies] +fastrlock = ">=0.5" +numpy = ">=1.20,<1.27" + +[package.extras] +all = ["Cython (>=0.29.22,<3)", "optuna (>=2.0)", "scipy (>=1.6,<1.13)"] +stylecheck = ["autopep8 (==1.5.5)", "flake8 (==3.8.4)", "mypy (==1.4.1)", "pbr (==5.5.1)", "pycodestyle (==2.6.0)", "types-setuptools (==57.4.14)"] +test = ["hypothesis (>=6.37.2,<6.55.0)", "pytest (>=7.2)"] + +[[package]] +name = "cupy-cuda111" +version = "12.2.0" +description = "CuPy: NumPy & SciPy for GPU" +category = "main" +optional = true +python-versions = ">=3.8" +files = [ + {file = "cupy_cuda111-12.2.0-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:0b486757b456e3ffe4b0b48935f4d2e86522d21b5e7a9184b9e1dbd32c02c587"}, + {file = "cupy_cuda111-12.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:e954737e1c8e7b3cb341154f0080746f53ba61cc932e2a9a855534a78d5ddf89"}, + {file = "cupy_cuda111-12.2.0-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:5fbe01fdc949aa357a3b6a92bb057bc873a23e96f398e8d385b7ce6e33170779"}, + {file = "cupy_cuda111-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:7670131b6d24f3c564cfed364eda0db88dcdb2a4e137976b05684a98e482ccfb"}, + {file = "cupy_cuda111-12.2.0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:cb50d223c99dc590629f8e25255bbc8d2035f7365412fd1a12c12d36552f6945"}, + {file = "cupy_cuda111-12.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:3ea34869903517fe676efcf71ad9d9b178d2197b45af3c2fdf66662280bb85d3"}, + {file = "cupy_cuda111-12.2.0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:c3100b176e2e5934e39c8f05236aa3b31de9ffe96fb59fbfa808bf228c4ca4cb"}, + {file = "cupy_cuda111-12.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:33272f974afbc526707ea63d2d4afb61c7bb8932e7ba84157b94dfae1552810d"}, +] + +[package.dependencies] +fastrlock = ">=0.5" +numpy = ">=1.20,<1.27" + +[package.extras] +all = ["Cython (>=0.29.22,<3)", "optuna (>=2.0)", "scipy (>=1.6,<1.13)"] +stylecheck = ["autopep8 (==1.5.5)", "flake8 (==3.8.4)", "mypy (==1.4.1)", "pbr (==5.5.1)", "pycodestyle (==2.6.0)", "types-setuptools (==57.4.14)"] +test = ["hypothesis (>=6.37.2,<6.55.0)", "pytest (>=7.2)"] + +[[package]] +name = "cupy-cuda11x" +version = "12.2.0" +description = "CuPy: NumPy & SciPy for GPU" +category = "main" +optional = true +python-versions = ">=3.8" +files = [ + {file = "cupy_cuda11x-12.2.0-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:f3b1991e4e25f52581f79b88c8d19c3e31fa739260b7080c813d7fb287ae8e7f"}, + {file = "cupy_cuda11x-12.2.0-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:ac52fd24ca5769d903bd531826d0525dbeaba66c49911ae021b9702386c8b96b"}, + {file = "cupy_cuda11x-12.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:8ed1fbf14230691df3f58f841e9ca5717b4175c4276fbf88948a5bc3c6392f58"}, + {file = "cupy_cuda11x-12.2.0-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:28eace420c9f58fa5739294d604f8aab9118b1c764f64a3ce0691f00193b11ff"}, + {file = "cupy_cuda11x-12.2.0-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:fb3294a694ee40b6e22ba3345b04b2a35db8a19fac74f59dc7757ea87ab5b289"}, + {file = "cupy_cuda11x-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:2e502cea9d21fefd1d8fd3747c6b46d1f916550e54d859f9083b6327a8f7ff5b"}, + {file = "cupy_cuda11x-12.2.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:2fd425be7ae8f82ff68d2e8eeb37132fc9b9dfea6d3f0708419d6b9843983e42"}, + {file = "cupy_cuda11x-12.2.0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:fcdfe993f87c5499898692d35d3c1f30cdd249778d6102132b5bc4a6f8a867fd"}, + {file = "cupy_cuda11x-12.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:66491d9c1ed93d4ee5b390ece92ed76a885c61859b403e3a6fd6f5b3a9ded116"}, + {file = "cupy_cuda11x-12.2.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:ededad28104d9e0c2cb02876fd2fc5c0dfec4cc66e600b7120bb4e655857c5e3"}, + {file = "cupy_cuda11x-12.2.0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:ba4403d94e9eb2578f8f2667d37ef3ec8be4fbc2c7fd1ba498004d6399d0f3b6"}, + {file = "cupy_cuda11x-12.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:d01138eb7cf51583b6d67281bd39f3f241d664f2d1c65af2399ed304a609dadb"}, +] + +[package.dependencies] +fastrlock = ">=0.5" +numpy = ">=1.20,<1.27" + +[package.extras] +all = ["Cython (>=0.29.22,<3)", "optuna (>=2.0)", "scipy (>=1.6,<1.13)"] +stylecheck = ["autopep8 (==1.5.5)", "flake8 (==3.8.4)", "mypy (==1.4.1)", "pbr (==5.5.1)", "pycodestyle (==2.6.0)", "types-setuptools (==57.4.14)"] +test = ["hypothesis (>=6.37.2,<6.55.0)", "pytest (>=7.2)"] + +[[package]] +name = "cupy-cuda12x" +version = "12.2.0" +description = "CuPy: NumPy & SciPy for GPU" +category = "main" +optional = true +python-versions = ">=3.8" +files = [ + {file = "cupy_cuda12x-12.2.0-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:350cc1591d5af25aaf147974547a68f25eb9104b0fcd5fa3c89f32f4d42b88c7"}, + {file = "cupy_cuda12x-12.2.0-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:bfcea96e5506193ea8672a8c8a3e164d023c4860e58f1165cdd4a946b136aa20"}, + {file = "cupy_cuda12x-12.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:9a41ed8bece4dc2344e2afb1976690adf7ad3f9ef0a169653b5c9466e4768450"}, + {file = "cupy_cuda12x-12.2.0-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:3e86fe1d41009418d3f2878e6f4f713a28d29a7faaa47c089f8ac05851087e9e"}, + {file = "cupy_cuda12x-12.2.0-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:ddf743881d85e98e1ac46328f78100a5abe842793aa1fd575301c81df591e9a2"}, + {file = "cupy_cuda12x-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:77386c53ddb5040f5cd9daa6764e3e3edc76f71b074b9a9bebec76f5da75cfa8"}, + {file = "cupy_cuda12x-12.2.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:98277c47425cb59cb623fcd94fec4dfc77292ff1377f2fc4bd0d0e55c7dcf447"}, + {file = "cupy_cuda12x-12.2.0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:c581705d444cdeeffa016055ba449322bb2a99b5416ab5b85f140ea7333a1e7c"}, + {file = "cupy_cuda12x-12.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:12d88bba2e6cae18ba48eabbb7ff23a21d073ce83047ef27a87b99414db86795"}, + {file = "cupy_cuda12x-12.2.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:7d4e2b2ad37afd163d006a96b31b417142d95768846227513af7b596d731ba29"}, + {file = "cupy_cuda12x-12.2.0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:0406b98fb2f1780238de8fed0da5f14e689b016c5c1f0ddaecd41ee987cd7965"}, + {file = "cupy_cuda12x-12.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:2ee5cb963bab52cc421ba09824e0ffdb7c6a394f35884094f73d2d1af927f0e4"}, +] + +[package.dependencies] +fastrlock = ">=0.5" +numpy = ">=1.20,<1.27" + +[package.extras] +all = ["Cython (>=0.29.22,<3)", "optuna (>=2.0)", "scipy (>=1.6,<1.13)"] +stylecheck = ["autopep8 (==1.5.5)", "flake8 (==3.8.4)", "mypy (==1.4.1)", "pbr (==5.5.1)", "pycodestyle (==2.6.0)", "types-setuptools (==57.4.14)"] +test = ["hypothesis (>=6.37.2,<6.55.0)", "pytest (>=7.2)"] + +[[package]] +name = "fastrlock" +version = "0.8.1" +description = "Fast, re-entrant optimistic lock implemented in Cython" +category = "main" +optional = true +python-versions = "*" +files = [ + {file = "fastrlock-0.8.1-cp27-cp27m-macosx_10_15_x86_64.whl", hash = "sha256:e88ad64610f709daf6763fcb73b1640489d6cc3065761f0a9a42e83e0a0ce8da"}, + {file = "fastrlock-0.8.1-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:54bcea11203dd0af2b4d783487f12f4f977c098be74a56c4c4d7b60ec793e7b7"}, + {file = "fastrlock-0.8.1-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:3068a7497bf3e58c71835c79c27b05b1726f45a1c5c8c333be56ce6643285e31"}, + {file = "fastrlock-0.8.1-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:62168061441c2656e440f401f6d8ebd0af94fac3e662782d12ade08b4c11e8e9"}, + {file = "fastrlock-0.8.1-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:8d4e59933a81a62ed85e7df62991120eace969ea15e4ca3321264b4ab320b76a"}, + {file = "fastrlock-0.8.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:cf3e5703e60f88a85d615e36212e411f1fce6cabfad9cc84fe3b9877b133b3c2"}, + {file = "fastrlock-0.8.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:b84fb655c281bdd35376fc7bd35ff9eba647fe1d0fe770f62f6e64522f894f72"}, + {file = "fastrlock-0.8.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:f836adcdfe3d825e303aca6d26f6c58620fe6d50e86568df741ba96892c37a09"}, + {file = "fastrlock-0.8.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:d6c53abeae3f9a55b5c65824cec9df59159fa50e8fa800a5c6e8de42b2219c28"}, + {file = "fastrlock-0.8.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:77dd26848251efdf216779eb7581f7bdf99893b34f6e7e8989cc92b580f20189"}, + {file = "fastrlock-0.8.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0055ba81eb3c4c18345cd229eee9952315e4ecedd9d41793c077892a7823be8f"}, + {file = "fastrlock-0.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:b3c7738911053b418046b57fdc034a67d104fbc597b2391bee663a24f4299314"}, + {file = "fastrlock-0.8.1-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:fc92b5de2a5c1da603e545056b816f7b3db3d46beccfcffe8dc0d8f0df7cfc6e"}, + {file = "fastrlock-0.8.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:5c59b5de879bb2890a644dbe24c592467219bcf7c5c7388fb200e0022e3b741a"}, + {file = "fastrlock-0.8.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:d19841bf61a4ff671a761040815917880a153e8543d1f147537389af5cbc604a"}, + {file = "fastrlock-0.8.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:e7c416807063cadaf9f3f64debf5791d943dcde9089ac71016e8ca010dabaa7f"}, + {file = "fastrlock-0.8.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:30311e09594ac0806aad517da6e0c06510f2c2fc3f8b3b2c1ae6172460fd4f6a"}, + {file = "fastrlock-0.8.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4d1322aa0ffe702e1cc3c002e302edea5f6b8a3af096a7b340b7f719a577669c"}, + {file = "fastrlock-0.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:e683e2eb35058d4ba8576f9180f133b2367723d6ea4c3d1a3559894c8b7c9f33"}, + {file = "fastrlock-0.8.1-cp35-cp35m-macosx_10_15_x86_64.whl", hash = "sha256:826b7dbf89c4157134bc96c9df0ef2cc99f8fe2abb38bd080a0acc34172bf7df"}, + {file = "fastrlock-0.8.1-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0e6acadda18125c972b73c4787adbf688c5aa140015ea3632f38d9119cf09214"}, + {file = "fastrlock-0.8.1-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:65d77bfca60672e6ccafcac7b6533d275b626a060fefa6e297cf3430b8357427"}, + {file = "fastrlock-0.8.1-cp36-cp36m-macosx_10_15_x86_64.whl", hash = "sha256:a2671b4c6fe6dff7eb83da7cf510894c83d88e4f5ca9a7515f70bc2daff31c4c"}, + {file = "fastrlock-0.8.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:551b70cd6aad03094f977f87c1ea5b19b6036eae340c2181164d15e5602e5fcb"}, + {file = "fastrlock-0.8.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:d0c97035aca21c4b56b4ecb15d006adc0e9277ee8479fe3d709bbc261ff7c8a9"}, + {file = "fastrlock-0.8.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:af58fc057de699dfcd9a3e16c3204696b5155ed931ccc2ec3e8e9123e56343f9"}, + {file = "fastrlock-0.8.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:bb7dfe068b9925cadabf14539e4201c3391da255a6bb2d7a42e2d8b6bb167329"}, + {file = "fastrlock-0.8.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cdba150ab6334f94055e54e3ebda03b9804fb09ad8ebd453af4bc47ad9ddfc17"}, + {file = "fastrlock-0.8.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:51c3200a11797986c85c48102c0ce35f6c53b709ef123b9ac1d07c3617307125"}, + {file = "fastrlock-0.8.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:ec258de18ad41b9e251173d64afe70da83f852b612cf5fc51e64ad0408a0a973"}, + {file = "fastrlock-0.8.1-cp36-cp36m-win_amd64.whl", hash = "sha256:ed84c5d813abf02c809513fe286dac8a21cace71c0a4b6cb344b1aab0571a403"}, + {file = "fastrlock-0.8.1-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:3a7888ce976a6d0ad699c2bdca6571beec482f460cb917978abb8c60f8150219"}, + {file = "fastrlock-0.8.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:d67418209ed0f3a0ff5ca79a8b4a05d4d5711cedd99a45223858bd0826c9112d"}, + {file = "fastrlock-0.8.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:713158684a3c20134b55ee2442616dfba3e6bab9817dd7a8b230afecb331176e"}, + {file = "fastrlock-0.8.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a422bfdaaf896d8e7be6e082d7c59bfca56daa6fdd85023f4f63bfa088f7fd3f"}, + {file = "fastrlock-0.8.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:f5b32366e63b5f150ce58efd0d64754310960b4cae81a2c45bf956284188271e"}, + {file = "fastrlock-0.8.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e88260af5a94afe3516e8819a3f77fe604fa4c71e7d4d7c96ae92f84408146e2"}, + {file = "fastrlock-0.8.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:2d7bcbd5258d7d2dbe1c8719c3faf6b2f62e609b51c0c80216ea624d2c423357"}, + {file = "fastrlock-0.8.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:537423c21e3f582b044d6aa2da7a813785e80328054099a6913545f68474b088"}, + {file = "fastrlock-0.8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:cd55b2669a62f2a0eb3fa7bedc4bcca0663440ffa5401f3dc1bd2a3394fca766"}, + {file = "fastrlock-0.8.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:252cd40c2d8a412e238213d494e920d74439ae2d480ff83d72b23270cc218497"}, + {file = "fastrlock-0.8.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:b9bea96dfb18fd1fbd7e18f9e2ee6868691e1c99a3dc111c267a97e49acbecba"}, + {file = "fastrlock-0.8.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:4459f0743af5b4ad5ace4bbb65162e60ed320437dcf473bf60a9f4a9a188b5cd"}, + {file = "fastrlock-0.8.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1bccd6d2ae63995f3f0cd25770f39858f0802e45b95325f406d3fce19c41834d"}, + {file = "fastrlock-0.8.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:3e4c0ada1ce907a5616046eb219eb40ea0ff2bb27f4da703925c32af1537f43a"}, + {file = "fastrlock-0.8.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b864db14be48ff32d37aee0dcd61910274742f84fcced17a0f291f985e0abdca"}, + {file = "fastrlock-0.8.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f2db49fd0166fb61ab00e8d3fe55ae5cbb9d566df42ccf188ab482401fc7ac5d"}, + {file = "fastrlock-0.8.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d38943c3e015bd92ea6b8f155eb829000fc291edad21fc58ca0acbb8dd06788f"}, + {file = "fastrlock-0.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:c00c4e090ff5204830e49cccebb41372f800970466725b184089a40ca332c897"}, + {file = "fastrlock-0.8.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:187786a0bdb8c5c59d8ed6f82846306e63169c3c5437c847c548dfaf548ee958"}, + {file = "fastrlock-0.8.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:801bee6ef607c2020818d006677704e20997d61524cfa1f338e141a6f017b7dd"}, + {file = "fastrlock-0.8.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:73f1df1d1e141b0e2cedc40536ee745247f0bf968526535eb9e2e7acd85e7535"}, + {file = "fastrlock-0.8.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eb5bb4187880f6944b6213728e3e8617a4613fa9d1303fea84d0227cd81943af"}, + {file = "fastrlock-0.8.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:294b60ed41b746e99189260f7dc42a8c280c59c9aa9e152f89752fc102f2eefa"}, + {file = "fastrlock-0.8.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e5122103f38c9ebdb597304b2bed3b4c3b9f2c372d649eff2f7c3be70f8f7fc1"}, + {file = "fastrlock-0.8.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:625372f1e19c80fc90b83a167bedc785855ea63c0deb2e66aab922e346dd9517"}, + {file = "fastrlock-0.8.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:36fdc86df2cfa7f1692cdb035338dd7f952fa50e5ea8e39569e15ff4269a8e1a"}, + {file = "fastrlock-0.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:6e97a68d67bd4b19948bc119252dc2b473dbcb301aa319ae98246d86281d2b9d"}, + {file = "fastrlock-0.8.1.tar.gz", hash = "sha256:8a5f2f00021c4ac72e4dab910dc1863c0e008a2e7fb5c843933ae9bcfc3d0802"}, +] + [[package]] name = "joblib" -version = "1.3.1" +version = "1.3.2" description = "Lightweight pipelining with Python functions" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "joblib-1.3.1-py3-none-any.whl", hash = "sha256:89cf0529520e01b3de7ac7b74a8102c90d16d54c64b5dd98cafcd14307fdf915"}, - {file = "joblib-1.3.1.tar.gz", hash = "sha256:1f937906df65329ba98013dc9692fe22a4c5e4a648112de500508b18a21b41e3"}, + {file = "joblib-1.3.2-py3-none-any.whl", hash = "sha256:ef4331c65f239985f3f2220ecc87db222f08fd22097a3dd5698f693875f8cbb9"}, + {file = "joblib-1.3.2.tar.gz", hash = "sha256:92f865e621e17784e7955080b6d042489e3b8e294949cc44c6eac304f59772b1"}, ] [[package]] @@ -265,21 +481,21 @@ files = [ [[package]] name = "tqdm" -version = "4.65.0" +version = "4.66.1" description = "Fast, Extensible Progress Meter" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "tqdm-4.65.0-py3-none-any.whl", hash = "sha256:c4f53a17fe37e132815abceec022631be8ffe1b9381c2e6e30aa70edc99e9671"}, - {file = "tqdm-4.65.0.tar.gz", hash = "sha256:1871fb68a86b8fb3b59ca4cdd3dcccbc7e6d613eeed31f4c332531977b89beb5"}, + {file = "tqdm-4.66.1-py3-none-any.whl", hash = "sha256:d302b3c5b53d47bce91fea46679d9c3c6508cf6332229aa1e7d8653723793386"}, + {file = "tqdm-4.66.1.tar.gz", hash = "sha256:d88e651f9db8d8551a62556d3cff9e3034274ca5d66e93197cf2490e2dcb69c7"}, ] [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} [package.extras] -dev = ["py-make (>=0.1.0)", "twine", "wheel"] +dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"] notebook = ["ipywidgets (>=6)"] slack = ["slack-sdk"] telegram = ["requests"] @@ -296,7 +512,14 @@ files = [ {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"}, ] +[extras] +cuda102 = ["cupy-cuda102"] +cuda110 = ["cupy-cuda110"] +cuda111 = ["cupy-cuda111"] +cuda11x = ["cupy-cuda11x"] +cuda12x = ["cupy-cuda12x"] + [metadata] lock-version = "2.0" python-versions = "<3.12,>=3.8" -content-hash = "f8dab62bf9997d9f1d2faaa6f48a4fdfc302b86bb219564906ddc1af3f2421c2" +content-hash = "c0bd3a9aeb4b395df2e4aa6d5a9dfa7994790f7dd6dcdd47614b01b82ce70717" diff --git a/profiling/gram_comparison.py b/profiling/gram_comparison.py index cf28a96..ebf6046 100644 --- a/profiling/gram_comparison.py +++ b/profiling/gram_comparison.py @@ -1,20 +1,39 @@ +from dataclasses import make_dataclass import timeit import numpy as np +import pandas as pd from joblib import Parallel, delayed +from tqdm import tqdm -import hisel +from hisel import kernels +from hisel.kernels import Device +from hisel import cudakernels def hisel_compute_gram_matrix(x, batch_size): - rbf_kernel = hisel.kernels.KernelType.RBF + rbf_kernel = kernels.KernelType.RBF l = 1. - gram = hisel.kernels.apply_feature_map( + gram = kernels.apply_feature_map( rbf_kernel, x, l, batch_size, is_multivariate=False, - no_parallel=True, + device=Device.CPU, + ) + return gram + + +def cudahisel_compute_gram_matrix(x, batch_size): + rbf_kernel = kernels.KernelType.RBF + l = 1. + gram = cudakernels.apply_feature_map( + rbf_kernel, + x, + l, + batch_size, + is_multivariate=False, + device=Device.GPU, ) return gram @@ -42,11 +61,11 @@ def compute_gram_matrix(x, batch_size, parallel: bool = False): 1, n, discarded) - for k in range(d) + for k in tqdm(range(d)) ]) else: result = [] - for k in range(d): + for k in tqdm(range(d)): X = PyHSICLasso.parallel_compute_kernel( x[[k], :], 'Gaussian', k, batch_size, 1, n, discarded) result.append(X) @@ -149,8 +168,8 @@ def kernel_gaussian(X_in_1, X_in_2, sigma): class Experiment: def __init__(self, - num_samples=1000, - num_features=500, + num_samples=5000, + num_features=200, batch_size=1000, ): self.num_samples = num_samples @@ -162,29 +181,86 @@ def __init__(self, def run_hisel(self): return hisel_compute_gram_matrix(self.x, self.batch_size) + def run_cudahisel(self): + return cudahisel_compute_gram_matrix(self.x, self.batch_size) + def run_pyhsiclasso(self): return PyHSICLasso.compute_gram_matrix( self.x, self.batch_size, False) -experiment = Experiment() +def test_num_samples(): + num_features = 300 + batch_size = 800 + num_samples = 1600 * np.arange(2, 8, dtype=int) + num_runs = 8 + data = [] + Result = make_dataclass("Result", + [ + ("num_samples", int), + ("num_features", int), + ("batch_size", int), + ("hisel_cpu_time", float), + ("hisel_gpu_time", float), + ("pyhsiclasso_time", float), + ]) + for n in num_samples: + experiment = Experiment( + n, num_features, batch_size) + hisel_cpu_time = timeit.timeit( + 'experiment.run_hisel()', + globals=locals(), + number=num_runs) + hisel_gpu_time = timeit.timeit( + 'experiment.run_cudahisel()', + globals=locals(), + number=num_runs) + pyhsiclasso_time = timeit.timeit( + 'experiment.run_pyhsiclasso()', + globals=locals(), + number=num_runs) + del experiment + result = Result( + n, + num_features, + batch_size, + hisel_cpu_time, + hisel_gpu_time, + pyhsiclasso_time + ) + print(result) + data.append(result) + df = pd.DataFrame(data) + df.to_csv("gram_runtimes_by_num_samples.csv", index=False) + print(df) def main(): + experiment = Experiment() + # Compute Gram matrix using hisel hisel_time = timeit.timeit( 'experiment.run_hisel()', - globals=globals(), + globals=locals(), number=3) print('\n#################################################################') print(f'# hisel_time: {round(hisel_time, 6)}') print('#################################################################\n\n') + # Compute Gram matrix using hisel + cudahisel_time = timeit.timeit( + 'experiment.run_cudahisel()', + globals=locals(), + number=3) + print('\n#################################################################') + print(f'# cudahisel_time: {round(cudahisel_time, 6)}') + print('#################################################################\n\n') + # Compute Gram matrix using pyHSICLasso pyhsiclasso_time = timeit.timeit( 'experiment.run_pyhsiclasso()', - globals=globals(), + globals=locals(), number=3) print('\n#################################################################') print(f'# pyhsiclasso_time: {round(pyhsiclasso_time, 6)}') diff --git a/profiling/select_comparison.py b/profiling/select_comparison.py index ef65993..9f84953 100644 --- a/profiling/select_comparison.py +++ b/profiling/select_comparison.py @@ -4,6 +4,7 @@ from scipy.stats import special_ortho_group from hisel.select import HSICSelector as Selector, FeatureType +from hisel.kernels import Device import pyHSICLasso @@ -33,7 +34,7 @@ def __init__( apply_transform: bool = False, batch_size: int = 500, number_of_epochs: int = 3, - device: Optional[str] = None + device: Device = Device.CPU, ): print('\n\n\n##############################################################') print('Test selection of features in a linear transformation setting') @@ -48,7 +49,7 @@ def __init__( d: int = np.random.randint(low=50, high=100) n: int = np.random.randint(low=15000, high=20000) - n_features: int = d // 5 + n_features: int = d // 6 features = list(np.random.choice(d, replace=False, size=n_features)) x: np.ndarray y: np.ndarray @@ -120,7 +121,9 @@ def run_hisel(self): batch_size=len(self.x), minibatch_size=self.batch_size, number_of_epochs=self.number_of_epochs, - device=self.device) + device=self.device, + return_index=True, + ) print( f'hisel selected features:\n{sorted(selection)}') @@ -137,7 +140,7 @@ def run_hisel(self): def test_regression_with_noise(): xfeattype = FeatureType.CONT yfeattype = FeatureType.CONT - batch_size = 800 + batch_size = 1000 number_of_epochs = 1 return Experiment(xfeattype, yfeattype, add_noise=True, @@ -148,7 +151,7 @@ def test_regression_with_noise(): def test_regression_with_noise_with_transform(): xfeattype = FeatureType.CONT yfeattype = FeatureType.CONT - batch_size = 800 + batch_size = 1000 number_of_epochs = 1 return Experiment(xfeattype, yfeattype, add_noise=True, @@ -162,7 +165,7 @@ def test_regression_with_noise_with_transform(): def main(): pyhsiclasso_time = timeit.timeit( 'regression_experiment.run_pyhsiclasso()', - number=5, + number=3, globals=globals(), ) print('\n#################################################################') @@ -171,7 +174,7 @@ def main(): hisel_time = timeit.timeit( 'regression_experiment.run_hisel()', - number=5, + number=3, globals=globals(), ) print('\n#################################################################') diff --git a/pyproject.toml b/pyproject.toml index 4769292..dd9a44d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "hisel" -version = "0.6.0" +version = "1.0.0" description = "" authors = ["claudio "] readme = "README.md" @@ -12,6 +12,18 @@ pandas = ">=1.5.3" scipy = ">=1.10" scikit-learn = ">=1.2.0" tqdm = "*" +cupy-cuda102 = [{version = "*", optional = true}] +cupy-cuda110 = [{version = "*", optional = true}] +cupy-cuda111 = [{version = "*", optional = true}] +cupy-cuda11x = [{version = "*", optional = true}] +cupy-cuda12x = [{version = "*", optional = true}] + +[tool.poetry.extras] +cuda102 = ["cupy-cuda102"] +cuda110 = ["cupy-cuda110"] +cuda111 = ["cupy-cuda111"] +cuda11x = ["cupy-cuda11x"] +cuda12x = ["cupy-cuda12x"] [build-system] diff --git a/tests/cudakernels_test.py b/tests/cudakernels_test.py new file mode 100644 index 0000000..c8b8474 --- /dev/null +++ b/tests/cudakernels_test.py @@ -0,0 +1,226 @@ +import unittest +import numpy as np +from sklearn.gaussian_process.kernels import RBF +from hisel import kernels, cudakernels +from hisel.kernels import Device +import datetime + +CUPY_AVAILABLE = True +try: + import cupy as cp +except (ModuleNotFoundError, ImportError): + print(f'Could not import cupy!') + cp = np + CUPY_AVAILABLE = False + + +class CudaKernelTest(unittest.TestCase): + + def test_rbf(self): + print(f'\n...Test RBF...') + kernel_type = kernels.KernelType.RBF + d: int = np.random.randint(low=2, high=10) + n: int = np.random.randint(low=1000, high=2000) + l: float = np.random.uniform(low=.95, high=5.) + rbf = RBF(l) + x = np.random.uniform(size=(d, n)) + x /= np.std(x, axis=1, keepdims=True) + 1e-19 + k = np.zeros((d, n, n)) + g = np.zeros((d, n, n)) + if CUPY_AVAILABLE: + x_ = cp.array(x) + g_ = cp.array(g) + else: + x_ = x + g_ = g + for i in range(d): + k[i, :, :] = rbf(x[[i], :].T) + g_[i, :, :] = cudakernels.multivariate( + x_[[i], :], l, kernel_type) + + f_ = cudakernels.featwise(x_, l, kernel_type) + ff = kernels.featwise(x, l, kernel_type) + if CUPY_AVAILABLE: + f = cp.asnumpy(f_) + g = cp.asnumpy(g_) + else: + f = np.array(f_, copy=True) + g = np.array(g_, copy=True) + self.assertTrue( + np.allclose( + f, k + ) + ) + self.assertTrue( + np.allclose( + g, k + ) + ) + self.assertTrue( + np.allclose( + f, ff + )) + + def test_delta(self): + print(f'\n...Test DELTA...') + kernel_type = kernels.KernelType.DELTA + d: int = np.random.randint(low=2, high=10) + n: int = np.random.randint(low=1000, high=2000) + l = 1. + m: int = np.random.randint(low=6, high=12) + x = np.random.randint(m, size=(d, n)) + g = np.zeros((d, n, n)) + if CUPY_AVAILABLE: + x_ = cp.array(x) + g_ = cp.array(g) + else: + x_ = x + g_ = g + for i in range(d): + g_[i, :, :] = cudakernels.multivariate( + x_[[i], :], l, kernel_type) + + f_ = cudakernels.featwise(x_, l, kernel_type) + ff = kernels.featwise(x, l, kernel_type) + if CUPY_AVAILABLE: + f = cp.asnumpy(f_) + g = cp.asnumpy(g_) + else: + f = np.array(f_, copy=True) + g = np.array(g_, copy=True) + + self.assertTrue( + np.allclose( + g, f + ) + ) + self.assertTrue( + np.allclose( + f, ff + )) + + def test_both(self): + print(f'\n...Test BOTH...') + kernel_type = kernels.KernelType.BOTH + d: int = np.random.randint(low=6, high=15) + split: int = np.random.randint(low=2, high=d-1) + n: int = np.random.randint(low=1000, high=2000) + l: float = np.random.uniform(low=.95, high=5.) + m: int = np.random.randint(low=6, high=12) +# print(f'd: {d}') +# print(f'split: {split}') +# print(f'n: {n}') + rbf = RBF(l) + xcat = np.random.randint(m, size=(split, n)) + xcont = np.random.uniform(size=(d-split, n)) + xcont /= np.std(xcont, axis=1, keepdims=True) + 1e-19 + x = np.concatenate((xcat, xcont), axis=0) + k = np.zeros((d, n, n)) + g = np.zeros((d, n, n)) + if CUPY_AVAILABLE: + x_ = cp.array(x) + g_ = cp.array(g) + else: + x_ = x + g_ = g + for i in range(split): + g_[i, :, :] = cudakernels.multivariate( + x_[[i], :].astype(int), + 1., + kernels.KernelType.DELTA) + for i in range(split, d): + k[i, :, :] = rbf(x[[i], :].T) + g_[i, :, :] = kernels.multivariate( + x_[[i], :], l, kernels.KernelType.RBF) + + f_ = cudakernels.featwise(x_, l, kernel_type, split) + ff = kernels.featwise(x, l, kernel_type, split) + if CUPY_AVAILABLE: + f = cp.asnumpy(f_) + g = cp.asnumpy(g_) + else: + f = np.array(f_, copy=True) + g = np.array(g_, copy=True) + + self.assertTrue( + np.allclose( + f[split:], k[split:] + ) + ) + self.assertTrue( + np.allclose( + g, f + ) + ) + self.assertTrue( + np.allclose( + ff, f + ) + ) + + def test_apply_rbf_feature_map(self): + kernel_type = kernels.KernelType.RBF + self._test_apply_feature_map(kernel_type) + + def test_apply_delta_feature_map(self): + kernel_type = kernels.KernelType.DELTA + self._test_apply_feature_map(kernel_type) + + def _test_apply_feature_map(self, kernel_type): + print(f'\n...Test apply_feature_map...') + print(f'kernel_type: {kernel_type}') + d: int = np.random.randint(low=5, high=12) + n: int = np.random.randint(low=30000, high=35000) + l: float = np.random.uniform(low=.95, high=5.) + num_batches = 10 + batch_size = n // num_batches + gram_dim: int = num_batches * batch_size**2 + if kernel_type == kernels.KernelType.DELTA: + x = np.random.randint(10, size=(d, n)) + else: + x = np.random.uniform(size=(d, n)) + if CUPY_AVAILABLE: + x_ = cp.array(x) + else: + x_ = x + + # Execution on CPU + t0 = datetime.datetime.now() + phi_cpu = kernels.apply_feature_map( + kernel_type, x, l, batch_size, device=Device.CPU + ) + t1 = datetime.datetime.now() + dt_cpu = t1 - t0 + cpu_runtime = dt_cpu.seconds + 1e-6 * dt_cpu.microseconds + + self.assertEqual( + phi_cpu.shape, + (gram_dim, d) + ) + print(f'runtime on cpu: {cpu_runtime} seconds') + + # Execution on GPU + t0 = datetime.datetime.now() + phi: np.ndarray = cudakernels.apply_feature_map( + kernel_type, x_, l, batch_size, device=Device.GPU + ) + t1 = datetime.datetime.now() + dt_gpu = t1 - t0 + gpu_runtime = dt_gpu.seconds + 1e-6 * dt_gpu.microseconds + self.assertEqual( + phi.shape, + (gram_dim, d) + ) + print(f'runtime with gpu execution: {gpu_runtime} seconds') + + # check that cpu and gpu execution yield the same results + self.assertTrue( + np.allclose( + phi, + phi_cpu + ) + ) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/kernel_test.py b/tests/kernel_test.py index c3e5253..abc8ce3 100644 --- a/tests/kernel_test.py +++ b/tests/kernel_test.py @@ -2,6 +2,7 @@ import numpy as np from sklearn.gaussian_process.kernels import RBF from hisel import kernels +from hisel.kernels import Device import datetime @@ -288,34 +289,20 @@ def _test_apply_feature_map(self, kernel_type): print(f'\n...Test apply_feature_map...') print(f'kernel_type: {kernel_type}') d: int = np.random.randint(low=5, high=10) - n: int = np.random.randint(low=20000, high=35000) + n: int = np.random.randint(low=25000, high=30000) l: float = np.random.uniform(low=.95, high=5.) num_batches = 10 batch_size = n // num_batches + gram_dim: int = num_batches * batch_size**2 if kernel_type == kernels.KernelType.DELTA: x = np.random.randint(10, size=(d, n)) else: x = np.random.uniform(size=(d, n)) - # Execution with parallelization enabled - t0 = datetime.datetime.now() - phi: np.ndarray = kernels.apply_feature_map( - kernel_type, x, l, batch_size - ) - t1 = datetime.datetime.now() - dt_parallel = t1 - t0 - parallel_runtime = dt_parallel.seconds + 1e-6 * dt_parallel.microseconds - gram_dim: int = num_batches * batch_size**2 - self.assertEqual( - phi.shape, - (gram_dim, d) - ) - print(f'runtime with parallel execution: {parallel_runtime} seconds') - # Execution with parallelization disabled t0 = datetime.datetime.now() phi_no_parallel = kernels.apply_feature_map( - kernel_type, x, l, batch_size, no_parallel=True + kernel_type, x, l, batch_size, device=Device.CPU ) t1 = datetime.datetime.now() dt_serial = t1 - t0 @@ -327,6 +314,20 @@ def _test_apply_feature_map(self, kernel_type): ) print(f'runtime with serial execution: {serial_runtime} seconds') + # Execution with parallelization enabled + t0 = datetime.datetime.now() + phi: np.ndarray = kernels.apply_feature_map( + kernel_type, x, l, batch_size, device=Device.PARALLEL_CPU + ) + t1 = datetime.datetime.now() + dt_parallel = t1 - t0 + parallel_runtime = dt_parallel.seconds + 1e-6 * dt_parallel.microseconds + self.assertEqual( + phi.shape, + (gram_dim, d) + ) + print(f'runtime with parallel execution: {parallel_runtime} seconds') + # check that serial and parallel execution yield the same results self.assertTrue( np.allclose( diff --git a/tests/select_test.py b/tests/select_test.py index 400a446..626ed8c 100644 --- a/tests/select_test.py +++ b/tests/select_test.py @@ -4,6 +4,7 @@ from scipy.stats import special_ortho_group from hisel.select import HSICSelector as Selector, FeatureType +from hisel.kernels import Device from sklearn.feature_selection import mutual_info_regression, mutual_info_classif USE_PYHSICLASSO = True @@ -13,7 +14,7 @@ USE_PYHSICLASSO = False QUICK_TEST = True -SKIP_CUDA = True +SKIP_CUDA = False USE_PYHSICLASSO = False if QUICK_TEST else USE_PYHSICLASSO SKLEARN_RECON = True @@ -108,27 +109,27 @@ def test_cuda_regression_no_noise(self): xfeattype = FeatureType.CONT yfeattype = FeatureType.CONT self._test_selection(xfeattype, yfeattype, - add_noise=False, device='cuda') + add_noise=False, device=Device.GPU) @unittest.skipIf(SKIP_CUDA, 'cuda not available') def test_cuda_regression_with_noise(self): xfeattype = FeatureType.CONT yfeattype = FeatureType.CONT self._test_selection(xfeattype, yfeattype, - add_noise=True, device='cuda') + add_noise=True, device=Device.GPU) @unittest.skipIf(SKIP_CUDA, 'cuda not available') def test_cuda_regression_no_noise_with_transform(self): xfeattype = FeatureType.CONT yfeattype = FeatureType.CONT self._test_selection(xfeattype, yfeattype, - add_noise=False, device='cuda', apply_transform=True) + add_noise=False, device=Device.GPU, apply_transform=True) @unittest.skipIf(SKIP_CUDA, 'cuda not available') def test_cuda_regression_with_noise_with_transform(self): xfeattype = FeatureType.CONT yfeattype = FeatureType.CONT - self._test_selection(xfeattype, yfeattype, add_noise=True, device='cuda', + self._test_selection(xfeattype, yfeattype, add_noise=True, device=Device.GPU, apply_transform=True) def test_classification_no_noise(self): @@ -153,14 +154,14 @@ def test_cuda_classification_no_noise(self): xfeattype = FeatureType.CONT yfeattype = FeatureType.DISCR self._test_selection(xfeattype, yfeattype, - add_noise=False, device='cuda') + add_noise=False, device=Device.GPU) @unittest.skipIf(SKIP_CUDA, 'cuda not available') def test_cuda_classification_with_noise(self): xfeattype = FeatureType.CONT yfeattype = FeatureType.DISCR self._test_selection(xfeattype, yfeattype, - add_noise=True, device='cuda') + add_noise=True, device=Device.GPU) def _test_selection( self, @@ -168,7 +169,7 @@ def _test_selection( yfeattype: FeatureType, add_noise: bool = False, apply_transform: bool = False, - device: Optional[str] = None, + device: Device = Device.CPU, apply_non_linear_transform: bool = False, ): print('\n\n\n##############################################################################')