diff --git a/classes.png b/classes.png index accf8f9..5775769 100644 Binary files a/classes.png and b/classes.png differ diff --git a/docs/_static/img/classes.png b/docs/_static/img/classes.png index accf8f9..5775769 100644 Binary files a/docs/_static/img/classes.png and b/docs/_static/img/classes.png differ diff --git a/examples/examples1/example_rkme.py b/examples/examples1/example_rkme.py index 69d7875..33c8819 100644 --- a/examples/examples1/example_rkme.py +++ b/examples/examples1/example_rkme.py @@ -10,17 +10,17 @@ if __name__ == "__main__": spec2 = specification.rkme.RKMESpecification() spec1.generate_stat_spec_from_data(data_X) spec1.save("spec.json") - + beta = spec1.get_beta() z = spec1.get_z() print(type(beta), beta.shape) print(type(z), z.shape) - + spec2.load("spec.json") beta = spec1.get_beta() z = spec1.get_z() print(type(beta), beta.shape) print(type(z), z.shape) - + print(spec1.inner_prod(spec2)) - print(spec1.dist(spec2)) \ No newline at end of file + print(spec1.dist(spec2)) diff --git a/examples/examples2/example_learnware.py b/examples/examples2/example_learnware.py index 3a60117..0896c84 100644 --- a/examples/examples2/example_learnware.py +++ b/examples/examples2/example_learnware.py @@ -16,30 +16,21 @@ def prepare_learnware(): clf = svm.SVC() clf.fit(data_X, data_y) joblib.dump(clf, "./svm/svm.pkl") - - spec= specification.utils.generate_rkme_spec(X=data_X, gamma=0.1, cuda_idx=0) + + spec = specification.utils.generate_rkme_spec(X=data_X, gamma=0.1, cuda_idx=0) spec.save("./svm/spec.json") def test_API(): - text_X = np.random.randn(100, 20) + text_X = np.random.randn(100, 20) svm = SVM() pred_y1 = svm.predict(text_X) print(type(svm)) - - model = { - "module_path": "./svm/__init__.py", - "class_name": "SVM" - } + + model = {"module_path": "./svm/__init__.py", "class_name": "SVM"} spec = specification.rkme.RKMESpecification() spec.load("./svm/spec.json") - learnware = Learnware( - id="A0", - name="SVM", - model=model, - specification=spec, - desc="svm" - ) + learnware = Learnware(id="A0", name="SVM", model=model, specification=spec, desc="svm") pred_y2 = learnware.predict(text_X) print(type(learnware.model)) print(f"diff: {np.sum(pred_y1 != pred_y2)}") @@ -47,4 +38,4 @@ def test_API(): if __name__ == "__main__": prepare_learnware() - test_API() \ No newline at end of file + test_API() diff --git a/examples/examples2/svm/__init__.py b/examples/examples2/svm/__init__.py index 2e65e4f..2ad9ad0 100644 --- a/examples/examples2/svm/__init__.py +++ b/examples/examples2/svm/__init__.py @@ -6,7 +6,7 @@ import numpy as np class SVM: def __init__(self): dir_path = os.path.dirname(os.path.abspath(__file__)) - self.model = joblib.load(os.path.join(dir_path, 'svm.pkl')) + self.model = joblib.load(os.path.join(dir_path, "svm.pkl")) def fit(self, X: np.ndarray, y: np.ndarray): pass @@ -15,4 +15,4 @@ class SVM: return self.model.predict(X) def fintune(self, X: np.ndarray, y: np.ndarray): - pass \ No newline at end of file + pass diff --git a/learnware/learnware/base.py b/learnware/learnware/base.py index d2f26a7..8886ec4 100644 --- a/learnware/learnware/base.py +++ b/learnware/learnware/base.py @@ -48,7 +48,7 @@ class Learnware: def predict(self, X: np.ndarray) -> np.ndarray: return self.model.predict(X) - + def get_model(self) -> BaseModel: return self.model @@ -57,10 +57,10 @@ class Learnware: def get_info(self): return self.desc - + def update_stat_spec(self, name, new_stat_spec: BaseStatSpecification): self.specification.update_stat_spec(name, new_stat_spec) - + def update(self): # Empty Interface. raise NotImplementedError("'update' Method is NOT Implemented.") diff --git a/learnware/market/__init__.py b/learnware/market/__init__.py index a12c67a..91efc3d 100644 --- a/learnware/market/__init__.py +++ b/learnware/market/__init__.py @@ -1,3 +1,3 @@ from .base import BaseUserInfo, BaseMarket from .anchor import AnchoredUserInfo, AnchoredMarket -from .evolve import EvolvedMarket \ No newline at end of file +from .evolve import EvolvedMarket diff --git a/learnware/market/anchor.py b/learnware/market/anchor.py index ceb6596..f2c7cd5 100644 --- a/learnware/market/anchor.py +++ b/learnware/market/anchor.py @@ -7,14 +7,14 @@ from .base import BaseUserInfo, BaseMarket class AnchoredUserInfo(BaseUserInfo): """ - User Information for searching learnware (add the anchor design) - - - UserInfo contains the anchor list acquired from the market - - UserInfo can update stat_info based on anchors + User Information for searching learnware (add the anchor design) + + - UserInfo contains the anchor list acquired from the market + - UserInfo can update stat_info based on anchors """ - def __init__(self, id: str, property: dict = dict(), stat_info: dict = dict()): - super(AnchoredUserInfo, self).__init__(id, property, stat_info) + def __init__(self, id: str, semantic_spec: dict = dict(), stat_info: dict = dict()): + super(AnchoredUserInfo, self).__init__(id, semantic_spec, stat_info) self.anchor_learnware_list = {} # id: Learnware def add_anchor_learnware(self, learnware_id: str, learnware: Learnware): @@ -79,7 +79,7 @@ class AnchoredMarket(BaseMarket): ------- bool True if the target anchor learnware is deleted successfully. - + Raises ------ Exception @@ -107,7 +107,7 @@ class AnchoredMarket(BaseMarket): Parameters ---------- user_info : AnchoredUserInfo - - user_info with properties and statistical information + - user_info with semantic specifications and statistical information - some statistical information calculated on previous anchor learnwares Returns @@ -126,7 +126,7 @@ class AnchoredMarket(BaseMarket): Parameters ---------- user_info : AnchoredUserInfo - - user_info with properties and statistical information + - user_info with semantic specifications and statistical information - some statistical information calculated on anchor learnwares Returns diff --git a/learnware/market/base.py b/learnware/market/base.py index e4ca3c4..d66c22d 100644 --- a/learnware/market/base.py +++ b/learnware/market/base.py @@ -7,39 +7,33 @@ from ..learnware import Learnware class BaseUserInfo: - """ - User Information for searching learnware - - - Return random learnwares when both property and stat_info is empty - - Search only based on property when stat_info is None - - Filter through property and rank according to stat_info otherwise - """ + """User Information for searching learnware""" - def __init__(self, id: str, property: dict = dict(), stat_info: dict = dict()): + def __init__(self, id: str, semantic_spec: dict = dict(), stat_info: dict = dict()): """Initializing user information Parameters ---------- id : str user id - property : dict, optional - property selected by user, by default dict() + semantic_spec : dict, optional + semantic_spec selected by user, by default dict() stat_info : dict, optional statistical information uploaded by user, by default dict() """ self.id = id - self.property = property + self.semantic_spec = semantic_spec self.stat_info = stat_info - def get_property(self) -> dict: - """Return user properties + def get_semantic_spec(self) -> dict: + """Return user semantic specifications Returns ------- dict - user properties + user semantic specifications """ - return self.property + return self.semantic_spec def get_stat_info(self, name: str): return self.stat_info.get(name, None) @@ -58,38 +52,64 @@ class BaseMarket: """Initializing an empty market""" self.learnware_list = {} # id: Learnware self.count = 0 - self.property_list = { - 'Data': { - 'Values': ['Tabular', 'Image', 'Video', 'Text', 'Audio'], - 'Type' : 'Class', # Choose only one class + self.semantic_spec_list = self._init_semantic_spec_list() + + def _init_semantic_spec_list(self): + return { + "Data": { + "Values": ["Tabular", "Image", "Video", "Text", "Audio"], + "Type": "Class", # Choose only one class }, - 'Task': { - 'Values': ['Classification','Regression','Clustering','Feature Extraction','Generation','Segmentation','Object Detection'], - 'Type': 'Class', # Choose only one class + "Task": { + "Values": [ + "Classification", + "Regression", + "Clustering", + "Feature Extraction", + "Generation", + "Segmentation", + "Object Detection", + ], + "Type": "Class", # Choose only one class }, - 'Device': { - 'Values': ['CPU', 'GPU'], - 'Type': 'Tag', # Choose one or more tags + "Device": { + "Values": ["CPU", "GPU"], + "Type": "Tag", # Choose one or more tags }, - 'Scenario': { - 'Values': ['Business', 'Financial', 'Health', 'Politics', 'Computer', 'Internet', 'Traffic', 'Nature', 'Fashion', 'Industry', 'Agriculture', 'Education', 'Entertainment', 'Architecture'], - 'Type': 'Tag', # Choose one or more tags + "Scenario": { + "Values": [ + "Business", + "Financial", + "Health", + "Politics", + "Computer", + "Internet", + "Traffic", + "Nature", + "Fashion", + "Industry", + "Agriculture", + "Education", + "Entertainment", + "Architecture", + ], + "Type": "Tag", # Choose one or more tags }, - 'Description': { - 'Values': str, - 'Type': 'Description', + "Description": { + "Values": str, + "Type": "Description", }, } - def reload_market(self, market_path: str, property_list_path: str, load_mode: str = "database") -> bool: + def reload_market(self, market_path: str, semantic_spec_list_path: str, load_mode: str = "database") -> bool: """Reload the market when server restared. Parameters ---------- market_path : str Directory for market data. '_IP_:_port_' for loading from database. - property_list_path : str - Directory for available property. Should be a json file. + semantic_spec_list_path : str + Directory for available semantic_spec. Should be a json file. load_mode : str, optional Type of reload source. Currently, only 'database' is available. Defaults to 'database', by default "database" @@ -103,7 +123,7 @@ class BaseMarket: NotImplementedError Reload method NOT implemented. Currently, only loading from database is supported. FileNotFoundError - Loading source/property_list NOT found. Check whether the source and property_list are available. + Loading source/semantic_spec_list NOT found. Check whether the source and semantic_spec_list are available. """ @@ -119,7 +139,7 @@ class BaseMarket: def check_learnware(self, learnware: Learnware) -> bool: """Check the utility of a learnware - + Parameters ---------- learnware : Learnware @@ -132,7 +152,7 @@ class BaseMarket: return True def add_learnware( - self, learnware_name: str, model_path: str, stat_spec_path: str, property: dict, desc: str + self, learnware_name: str, model_path: str, stat_spec_path: str, semantic_spec: dict, desc: str ) -> Tuple[str, bool]: """Add a learnware into the market. @@ -150,8 +170,8 @@ class BaseMarket: stat_spec_path : str Filepath for statistical specification, a '.npy' file. How to pass parameters requires further discussion. - property : dict - property for new learnware, in dictionary format. + semantic_spec : dict + semantic_spec for new learnware, in dictionary format. desc : str Brief desciption for new learnware. @@ -176,7 +196,7 @@ class BaseMarket: Parameters ---------- user_info : BaseUserInfo - user_info with properties and statistical information + user_info with emantic specifications and statistical information Returns ------- @@ -186,28 +206,29 @@ class BaseMarket: - first is recommended combination, None when no recommended combination is calculated or statistical specification is not provided. - second is a list of matched learnwares """ - def search_by_property(): - def match_property(property1, property2): - if property1.keys() != property2.keys(): - raise Exception("property key error".format(property1.keys(), property2.keys())) - for key in property1.keys(): - if property1[key]['Type'] == 'Class': - if property1[key]['Values'] != property2[key]['Values']: + + def search_by_semantic_spec(): + def match_semantic_spec(semantic_spec1, semantic_spec2): + if semantic_spec1.keys() != semantic_spec2.keys(): + raise Exception("semantic_spec key error".format(semantic_spec1.keys(), semantic_spec2.keys())) + for key in semantic_spec1.keys(): + if semantic_spec1[key]["Type"] == "Class": + if semantic_spec1[key]["Values"] != semantic_spec2[key]["Values"]: return False - elif property1[key]['Type'] == 'Tag': - if not (set(property1[key]['Values']) & set(property2[key]['Values'])): + elif semantic_spec1[key]["Type"] == "Tag": + if not (set(semantic_spec1[key]["Values"]) & set(semantic_spec2[key]["Values"])): return False return True - + match_learnwares = [] for learnware in self.learnware_list: - learnware_property = learnware.get_specification().get_property() - user_property = user_info.get_property() - if match_property(learnware_property, user_property): + learnware_semantic_spec = learnware.get_specification().get_semantic_spec() + user_semantic_spec = user_info.get_semantic_spec() + if match_semantic_spec(learnware_semantic_spec, user_semantic_spec): match_learnwares.append(learnware) return match_learnwares - - match_learnwares = search_by_property() + + match_learnwares = search_by_semantic_spec() pass @@ -266,13 +287,13 @@ class BaseMarket: """ return True - def get_property_list(self) -> dict: - """Return all properties available + def get_semantic_spec_list(self) -> dict: + """Return all semantic specifications available Returns ------- dict - All properties in dictionary format + All emantic specifications in dictionary format """ - return self.property_list + return self.semantic_spec_list diff --git a/learnware/market/evolve.py b/learnware/market/evolve.py index 86c5177..fb7fcbc 100644 --- a/learnware/market/evolve.py +++ b/learnware/market/evolve.py @@ -6,7 +6,7 @@ from .anchor import AnchoredUserInfo, AnchoredMarket class EvolvedMarket(AnchoredMarket): - """Organize learnwares and enable them to continuously evolve + """Organize learnwares and enable them to continuously evolve Parameters ---------- diff --git a/learnware/specification/base.py b/learnware/specification/base.py index 8982b31..f223ffd 100644 --- a/learnware/specification/base.py +++ b/learnware/specification/base.py @@ -16,15 +16,15 @@ class BaseStatSpecification: class Specification: - def __init__(self, property=None): - self.property = property + def __init__(self, semantic_spec=None): + self.semantic_spec = semantic_spec self.stat_spec = {} # stat_spec should be dict def get_stat_spec(self): return self.stat_spec - def get_property(self): - return self.property + def get_semantic_spec(self): + return self.semantic_spec def update_stat_spec(self, name, new_stat_spec: BaseStatSpecification): self.stat_spec[name] = new_stat_spec diff --git a/learnware/specification/rkme.py b/learnware/specification/rkme.py index 33440b9..096fe2c 100644 --- a/learnware/specification/rkme.py +++ b/learnware/specification/rkme.py @@ -12,19 +12,15 @@ from typing import Tuple, Any, List, Union, Dict from .base import BaseStatSpecification -class RKMESpecification: - pass - - def setup_seed(seed): """ - Fix a random seed for addressing reproducibility issues. - - Parameters - ---------- - seed : int - Random seed for torch, torch.cuda, numpy, random and cudnn libraries. - """ + Fix a random seed for addressing reproducibility issues. + + Parameters + ---------- + seed : int + Random seed for torch, torch.cuda, numpy, random and cudnn libraries. + """ torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) np.random.seed(seed) @@ -34,18 +30,18 @@ def setup_seed(seed): def choose_device(cuda_idx=-1): """ - Let users choose compuational device between CPU or GPU. - - Parameters - ---------- - cuda_idx : int, optional - GPU index, by default -1 which stands for using CPU instead. - - Returns - ------- - torch.device - A torch.device object - """ + Let users choose compuational device between CPU or GPU. + + Parameters + ---------- + cuda_idx : int, optional + GPU index, by default -1 which stands for using CPU instead. + + Returns + ------- + torch.device + A torch.device object + """ if cuda_idx != -1: device = torch.device(f"cuda:{cuda_idx}") else: @@ -55,47 +51,47 @@ def choose_device(cuda_idx=-1): def torch_rbf_kernel(x1, x2, gamma) -> torch.Tensor: """ - Use pytorch to compute rbf_kernel function at faster speed. - - Parameters - ---------- - x1 : torch.Tensor - First vector in the rbf_kernel - x2 : torch.Tensor - Second vector in the rbf_kernel - gamma : float - Bandwidth in gaussian kernel - - Returns - ------- - torch.Tensor - The computed rbf_kernel value at x1, x2. - """ + Use pytorch to compute rbf_kernel function at faster speed. + + Parameters + ---------- + x1 : torch.Tensor + First vector in the rbf_kernel + x2 : torch.Tensor + Second vector in the rbf_kernel + gamma : float + Bandwidth in gaussian kernel + + Returns + ------- + torch.Tensor + The computed rbf_kernel value at x1, x2. + """ x1 = x1.double() x2 = x2.double() - X12norm = torch.sum(x1 ** 2, 1, keepdim=True) - 2 * x1 @ x2.T + torch.sum(x2 ** 2, 1, keepdim=True).T + X12norm = torch.sum(x1**2, 1, keepdim=True) - 2 * x1 @ x2.T + torch.sum(x2**2, 1, keepdim=True).T return torch.exp(-X12norm * gamma) def solve_qp(K: np.ndarray, C: np.ndarray): """ - Solver for the following quadratic programming(QP) problem: - - min 1/2 x^T K x - C^T x - s.t 1^T x - 1 = 0 - - I x <= 0 - - Parameters - ---------- - K : np.ndarray - Parameter in the quadratic term. - C : np.ndarray - Parameter in the linear term. - - Returns - ------- - torch.tensor - Solution to the QP problem. - """ + Solver for the following quadratic programming(QP) problem: + - min 1/2 x^T K x - C^T x + s.t 1^T x - 1 = 0 + - I x <= 0 + + Parameters + ---------- + K : np.ndarray + Parameter in the quadratic term. + C : np.ndarray + Parameter in the linear term. + + Returns + ------- + torch.tensor + Solution to the QP problem. + """ n = K.shape[0] P = matrix(K.cpu().numpy()) q = matrix(-C.cpu().numpy()) @@ -114,8 +110,7 @@ def solve_qp(K: np.ndarray, C: np.ndarray): class RKMESpecification(BaseStatSpecification): - """Reduced-set Kernel Mean Embedding (RKME) Specification - """ + """Reduced-set Kernel Mean Embedding (RKME) Specification""" def __init__(self, gamma: float = 0.1, cuda_idx: int = -1): """Initializing RKME parameters. @@ -184,14 +179,14 @@ class RKMESpecification(BaseStatSpecification): """ alpha = None self.num_points = X.shape[0] - + # fill np.nan X_nan = np.isnan(X) if X_nan.max() == 1: for col in range(X.shape[1]): col_mean = np.nanmean(X[:, col]) X[:, col] = np.where(X_nan[:, col], col_mean, X[:, col]) - + if not reduce: self.z = X self.beta = 1 / self.num_points * np.ones(self.num_points) @@ -296,14 +291,16 @@ class RKMESpecification(BaseStatSpecification): Z = Z - step_size * grad_Z self.z = Z + from .rkme import RKMESpecification + def inner_prod(self, Phi2: RKMESpecification) -> float: """Compute the inner product between two RKME specifications - + Parameters ---------- Phi2 : RKMESpecification The other RKME specification. - + Returns ------- float @@ -354,7 +351,9 @@ class RKMESpecification(BaseStatSpecification): rkme_to_save["beta"] = rkme_to_save["beta"].tolist() rkme_to_save["device"] = "gpu" if rkme_to_save["cuda_idx"] != -1 else "cpu" json.dump( - rkme_to_save, codecs.open(save_path, "w", encoding="utf-8"), separators=(",", ":"), + rkme_to_save, + codecs.open(save_path, "w", encoding="utf-8"), + separators=(",", ":"), ) def load(self, filepath: str) -> bool: diff --git a/learnware/specification/utils.py b/learnware/specification/utils.py index cdb07f6..a4225db 100644 --- a/learnware/specification/utils.py +++ b/learnware/specification/utils.py @@ -15,35 +15,35 @@ def generate_rkme_spec( cuda_idx: int = -1, ) -> RKMESpecification: """ - Interface for users to generate Reduced-set Kernel Mean Embedding (RKME) specification. - Return a RKMESpecification object, use .save() method to save as json file. + Interface for users to generate Reduced-set Kernel Mean Embedding (RKME) specification. + Return a RKMESpecification object, use .save() method to save as json file. - Parameters - ---------- - X : np.ndarray - Raw data in np.ndarray format. - Size of array: (n*d) - gamma : float - Bandwidth in gaussian kernel, by default 0.1. - K : int - Size of the construced reduced set. - step_size : float - Step size for gradient descent in the iterative optimization. - steps : int - Total rounds in the iterative optimization. - nonnegative_beta : bool, optional - True if weights for the reduced set are intended to be kept non-negative, by default False. - reduce : bool, optional - Whether shrink original data to a smaller set, by default True - cuda_idx : int - A flag indicating whether use CUDA during RKME computation. -1 indicates CUDA not used. + Parameters + ---------- + X : np.ndarray + Raw data in np.ndarray format. + Size of array: (n*d) + gamma : float + Bandwidth in gaussian kernel, by default 0.1. + K : int + Size of the construced reduced set. + step_size : float + Step size for gradient descent in the iterative optimization. + steps : int + Total rounds in the iterative optimization. + nonnegative_beta : bool, optional + True if weights for the reduced set are intended to be kept non-negative, by default False. + reduce : bool, optional + Whether shrink original data to a smaller set, by default True + cuda_idx : int + A flag indicating whether use CUDA during RKME computation. -1 indicates CUDA not used. - Returns - ------- - RKMESpecification - A RKMESpecification object - """ + Returns + ------- + RKMESpecification + A RKMESpecification object + """ rkme_spec = RKMESpecification(gamma=gamma, cuda_idx=cuda_idx) rkme_spec.generate_stat_spec_from_data(X, K, step_size, steps, nonnegative_beta, reduce) return rkme_spec @@ -51,19 +51,19 @@ def generate_rkme_spec( def generate_stat_spec(X: np.ndarray) -> BaseStatSpecification: """ - Interface for users to generate statistical specification. - Return a StatSpecification object, use .save() method to save as npy file. + Interface for users to generate statistical specification. + Return a StatSpecification object, use .save() method to save as npy file. - Parameters - ---------- - X : np.ndarray - Raw data in np.ndarray format. - Size of array: (n*d) + Parameters + ---------- + X : np.ndarray + Raw data in np.ndarray format. + Size of array: (n*d) - Returns - ------- - StatSpecification - A StatSpecification object - """ + Returns + ------- + StatSpecification + A StatSpecification object + """ return None