From ed29a762b5e6fd484edf05d65a4cca059462a573 Mon Sep 17 00:00:00 2001 From: Googol2002 Date: Mon, 27 Nov 2023 10:46:06 +0800 Subject: [PATCH 01/43] [DOC] Update RKME Image in spec.rst --- docs/components/spec.rst | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/components/spec.rst b/docs/components/spec.rst index ed07c7a..f04733d 100644 --- a/docs/components/spec.rst +++ b/docs/components/spec.rst @@ -80,9 +80,17 @@ Table Specification Image Specification -------------------------- +Image data lives in a higher dimensional space than other data types. Unlike lower dimensional spaces, metrics defined based on Euclidean distances (or similar distances) will fail in higher dimensional spaces. This means that measuring the similarity between image samples becomes difficult. + +To address these issues, we use the Neural Tangent Kernel (NTK) based on Convolutional Neural Networks (CNN) to measure the similarity of image samples. As we all know, CNN has greatly advanced the field of computer vision and is still a mainstream deep learning technique. + +Specifically, in the process of constructing the specification, we approximate the Neural Network Gaussian Process (NNGP) kernel with neural networks of finite width; in the process of calculating the similarity of the specification, we use an approximation algorithm to compute the NNGP kernel directly. + +In the calculation of NTK, the NNGP kernel serves as the dominant term and can be used as a good approximation of NTK. The use of NNGP approximation instead of NTK improves the construction and search efficiency of Image Specificatin. + Text Specification -------------------------- System Specification -====================================== \ No newline at end of file +====================================== From fb76a9d98da31883373995e53a125439dc84ba6c Mon Sep 17 00:00:00 2001 From: bxdd Date: Mon, 27 Nov 2023 22:45:33 +0800 Subject: [PATCH 02/43] [MNT] refactor client load test --- .../test_learnware_client/test_load_conda.py | 82 -------------- .../test_learnware_client/test_load_docker.py | 57 ---------- .../test_load_learnware.py | 101 ++++++++++++++++++ tests/test_learnware_client/test_reuse.py | 34 ------ 4 files changed, 101 insertions(+), 173 deletions(-) delete mode 100644 tests/test_learnware_client/test_load_conda.py delete mode 100644 tests/test_learnware_client/test_load_docker.py create mode 100644 tests/test_learnware_client/test_load_learnware.py delete mode 100644 tests/test_learnware_client/test_reuse.py diff --git a/tests/test_learnware_client/test_load_conda.py b/tests/test_learnware_client/test_load_conda.py deleted file mode 100644 index 4394348..0000000 --- a/tests/test_learnware_client/test_load_conda.py +++ /dev/null @@ -1,82 +0,0 @@ -import os -import unittest -import zipfile -import numpy as np - -import learnware -from learnware.learnware import get_learnware_from_dirpath -from learnware.client import LearnwareClient -from learnware.client.container import ModelCondaContainer, LearnwaresContainer -from learnware.reuse import AveragingReuser - - -class TestLearnwareLoad(unittest.TestCase): - def setUp(self): - unittest.TestCase.setUpClass() - self.client = LearnwareClient() - - root = os.path.dirname(__file__) - self.learnware_ids = ["00000084", "00000154", "00000155"] - self.zip_paths = [os.path.join(root, x) for x in ["1.zip", "2.zip", "3.zip"]] - - def test_load_single_learnware_by_zippath(self): - for learnware_id, zip_path in zip(self.learnware_ids, self.zip_paths): - self.client.download_learnware(learnware_id, zip_path) - - learnware_list = [ - self.client.load_learnware(learnware_path=zippath, runnable_option="conda") for zippath in self.zip_paths - ] - reuser = AveragingReuser(learnware_list, mode="vote_by_label") - input_array = np.random.random(size=(20, 13)) - print(reuser.predict(input_array)) - - for learnware in learnware_list: - print(learnware.id, learnware.predict(input_array)) - - def test_load_multi_learnware_by_zippath(self): - for learnware_id, zip_path in zip(self.learnware_ids, self.zip_paths): - self.client.download_learnware(learnware_id, zip_path) - - learnware_list = self.client.load_learnware(learnware_path=self.zip_paths, runnable_option="conda") - reuser = AveragingReuser(learnware_list, mode="vote_by_label") - input_array = np.random.random(size=(20, 13)) - print(reuser.predict(input_array)) - - for learnware in learnware_list: - print(learnware.id, learnware.predict(input_array)) - - def test_load_single_learnware_by_id(self): - learnware_list = [ - self.client.load_learnware(learnware_id=idx, runnable_option="conda") for idx in self.learnware_ids - ] - reuser = AveragingReuser(learnware_list, mode="vote_by_label") - input_array = np.random.random(size=(20, 13)) - print(reuser.predict(input_array)) - - for learnware in learnware_list: - print(learnware.id, learnware.predict(input_array)) - - def test_load_multi_learnware_by_id(self): - learnware_list = self.client.load_learnware(learnware_id=self.learnware_ids, runnable_option="conda") - reuser = AveragingReuser(learnware_list, mode="vote_by_label") - input_array = np.random.random(size=(20, 13)) - print(reuser.predict(input_array)) - - for learnware in learnware_list: - print(learnware.id, learnware.predict(input_array)) - - def test_load_single_learnware_by_id_pip(self): - learnware_id = "00000147" - learnware = self.client.load_learnware(learnware_id=learnware_id, runnable_option="conda") - input_array = np.random.random(size=(20, 23)) - print(learnware.predict(input_array)) - - def test_load_single_learnware_by_id_conda(self): - learnware_id = "00000148" - learnware = self.client.load_learnware(learnware_id=learnware_id, runnable_option="conda") - input_array = np.random.random(size=(20, 204)) - print(learnware.predict(input_array)) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_learnware_client/test_load_docker.py b/tests/test_learnware_client/test_load_docker.py deleted file mode 100644 index 775405c..0000000 --- a/tests/test_learnware_client/test_load_docker.py +++ /dev/null @@ -1,57 +0,0 @@ -import os -import unittest -import zipfile -import numpy as np - -import learnware -from learnware.learnware import get_learnware_from_dirpath -from learnware.client import LearnwareClient -from learnware.client.container import ModelCondaContainer, LearnwaresContainer -from learnware.reuse import AveragingReuser - - -class TestLearnwareLoad(unittest.TestCase): - def setUp(self): - unittest.TestCase.setUpClass() - self.client = LearnwareClient() - - root = os.path.dirname(__file__) - self.learnware_ids = ["00000084", "00000154", "00000155"] - self.zip_paths = [os.path.join(root, x) for x in ["1.zip", "2.zip", "3.zip"]] - - def test_load_multi_learnware_by_zippath(self): - for learnware_id, zip_path in zip(self.learnware_ids, self.zip_paths): - self.client.download_learnware(learnware_id, zip_path) - - learnware_list = self.client.load_learnware(learnware_path=self.zip_paths, runnable_option="docker") - reuser = AveragingReuser(learnware_list, mode="vote_by_label") - input_array = np.random.random(size=(20, 13)) - print(reuser.predict(input_array)) - - for learnware in learnware_list: - print(learnware.id, learnware.predict(input_array)) - - def test_load_multi_learnware_by_id(self): - learnware_list = self.client.load_learnware(learnware_id=self.learnware_ids, runnable_option="docker") - reuser = AveragingReuser(learnware_list, mode="vote_by_label") - input_array = np.random.random(size=(20, 13)) - print(reuser.predict(input_array)) - - for learnware in learnware_list: - print(learnware.id, learnware.predict(input_array)) - - def test_load_single_learnware_by_id_pip(self): - learnware_id = "00000147" - learnware = self.client.load_learnware(learnware_id=learnware_id, runnable_option="docker") - input_array = np.random.random(size=(20, 23)) - print(learnware.predict(input_array)) - - def test_load_single_learnware_by_id_conda(self): - learnware_id = "00000148" - learnware = self.client.load_learnware(learnware_id=learnware_id, runnable_option="docker") - input_array = np.random.random(size=(20, 204)) - print(learnware.predict(input_array)) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_learnware_client/test_load_learnware.py b/tests/test_learnware_client/test_load_learnware.py new file mode 100644 index 0000000..8381f90 --- /dev/null +++ b/tests/test_learnware_client/test_load_learnware.py @@ -0,0 +1,101 @@ +import os +import unittest +import argparse +import numpy as np + +import learnware +from learnware.learnware import get_learnware_from_dirpath +from learnware.client import LearnwareClient +from learnware.client.container import ModelCondaContainer, LearnwaresContainer +from learnware.reuse import AveragingReuser + + +class TestLearnwareLoadWithConda(unittest.TestCase): + def setUp(self): + self.client = LearnwareClient() + root = os.path.dirname(__file__) + self.learnware_ids = ["00000084", "00000154", "00000155"] + self.zip_paths = [os.path.join(root, x) for x in ["1.zip", "2.zip", "3.zip"]] + self.runnable_option = "conda" + + #def test_load_single_learnware_by_zippath(self): + # for learnware_id, zip_path in zip(self.learnware_ids, self.zip_paths): + # self.client.download_learnware(learnware_id, zip_path) +# + # learnware_list = [ + # self.client.load_learnware(learnware_path=zippath, runnable_option=self.runnable_option) for zippath in self.zip_paths + # ] + # reuser = AveragingReuser(learnware_list, mode="vote_by_label") + # input_array = np.random.random(size=(20, 13)) + # print(reuser.predict(input_array)) +# + # for learnware in learnware_list: + # print(learnware.id, learnware.predict(input_array)) +# + #def test_load_multi_learnware_by_zippath(self): + # for learnware_id, zip_path in zip(self.learnware_ids, self.zip_paths): + # self.client.download_learnware(learnware_id, zip_path) +# + # learnware_list = self.client.load_learnware(learnware_path=self.zip_paths, runnable_option=self.runnable_option) + # reuser = AveragingReuser(learnware_list, mode="vote_by_label") + # input_array = np.random.random(size=(20, 13)) + # print(reuser.predict(input_array)) +# + # for learnware in learnware_list: + # print(learnware.id, learnware.predict(input_array)) +# + #def test_load_single_learnware_by_id(self): + # learnware_list = [ + # self.client.load_learnware(learnware_id=idx, runnable_option=self.runnable_option) for idx in self.learnware_ids + # ] + # reuser = AveragingReuser(learnware_list, mode="vote_by_label") + # input_array = np.random.random(size=(20, 13)) + # print(reuser.predict(input_array)) +# + # for learnware in learnware_list: + # print(learnware.id, learnware.predict(input_array)) +# + #def test_load_multi_learnware_by_id(self): + # learnware_list = self.client.load_learnware(learnware_id=self.learnware_ids, runnable_option=self.runnable_option) + # reuser = AveragingReuser(learnware_list, mode="vote_by_label") + # input_array = np.random.random(size=(20, 13)) + # print(reuser.predict(input_array)) +# + # for learnware in learnware_list: + # print(learnware.id, learnware.predict(input_array)) +# + def test_load_single_learnware_by_id_pip(self): + learnware_id = "00000147" + learnware = self.client.load_learnware(learnware_id=learnware_id, runnable_option=self.runnable_option) + input_array = np.random.random(size=(20, 23)) + print(learnware.predict(input_array)) +# + def test_load_single_learnware_by_id_conda(self): + learnware_id = "00000148" + learnware = self.client.load_learnware(learnware_id=learnware_id, runnable_option=self.runnable_option) + input_array = np.random.random(size=(20, 204)) + print(learnware.predict(input_array)) +# +# +class TestLearnwareLoadWithDocker(TestLearnwareLoadWithConda): + def setUp(self): + super(TestLearnwareLoadWithDocker, self).setUp() + self.runnable_option = "docker" + +def suite(mode): + _suite = unittest.TestSuite() + #_suite.addTest(TestLearnwareLoadWithDocker()) + if mode == "all" or mode == "conda": + _suite.addTest(unittest.makeSuite(TestLearnwareLoadWithConda)) + if mode == "all" or mode == "docker": + _suite.addTest(unittest.makeSuite(TestLearnwareLoadWithDocker)) + return _suite + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--mode", type=str, required=False, default="all", help="The mode to run load learnware, must be in ['all', 'conda', 'docker']") + args = parser.parse_args() + + assert args.mode in {"all", "conda", "docker"}, f"The mode must be in ['all', 'conda', 'docker'], instead of '{args.mode}'" + runner = unittest.TextTestRunner() + runner.run(suite(args.mode)) \ No newline at end of file diff --git a/tests/test_learnware_client/test_reuse.py b/tests/test_learnware_client/test_reuse.py deleted file mode 100644 index b6b4485..0000000 --- a/tests/test_learnware_client/test_reuse.py +++ /dev/null @@ -1,34 +0,0 @@ -import zipfile -import numpy as np - -from learnware.learnware import get_learnware_from_dirpath -from learnware.client.container import LearnwaresContainer -from learnware.reuse import AveragingReuser -from learnware.tests.module import get_semantic_specification - -if __name__ == "__main__": - semantic_specification = get_semantic_specification() - zip_paths = [ - "/home/bixd/workspace/learnware/Learnware/tests/test_learnware_client/rf_tic.zip", - "/home/bixd/workspace/learnware/Learnware/tests/test_learnware_client/svc_tic.zip", - ] - dir_paths = [ - "/home/bixd/workspace/learnware/Learnware/tests/test_learnware_client/rf_tic", - "/home/bixd/workspace/learnware/Learnware/tests/test_learnware_client/svc_tic", - ] - - learnware_list = [] - for id, (zip_path, dir_path) in enumerate(zip(zip_paths, dir_paths)): - with zipfile.ZipFile(zip_path, "r") as z_file: - z_file.extractall(dir_path) - - learnware = get_learnware_from_dirpath(f"test_id{id}", semantic_specification, dir_path) - learnware_list.append(learnware) - - with LearnwaresContainer(learnware_list) as env_container: - learnware_list = env_container.get_learnwares_with_container() - reuser = AveragingReuser(learnware_list, mode="vote") - input_array = np.random.randint(0, 3, size=(20, 9)) - print(reuser.predict(input_array).argmax(axis=1)) - for id, ind_learner in enumerate(learnware_list): - print(f"learner_{id}", reuser.predict(input_array).argmax(axis=1)) From 0b20e11590001b2625f85c91a51e3ec0b7ef2f98 Mon Sep 17 00:00:00 2001 From: bxdd Date: Tue, 28 Nov 2023 15:34:56 +0800 Subject: [PATCH 03/43] [MNT] modify import package in client --- learnware/client/learnware_client.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/learnware/client/learnware_client.py b/learnware/client/learnware_client.py index f3cbe61..af6e092 100644 --- a/learnware/client/learnware_client.py +++ b/learnware/client/learnware_client.py @@ -16,7 +16,6 @@ from .. import learnware from .container import LearnwaresContainer from ..market import BaseChecker, EasySemanticChecker, EasyStatChecker from ..logger import get_module_logger -from ..specification import Specification from ..learnware import get_learnware_from_dirpath from ..market import BaseUserInfo from ..tests import get_semantic_specification @@ -294,7 +293,7 @@ class LearnwareClient: semantic_specification["Task"] = {"Type": "Class", "Values": [task_type] if task_type is not None else []} semantic_specification["Library"] = { "Type": "Class", - "Values": [library_type] if library_type is not None else [], + "Values": [library_type] if library _type is not None else [], } semantic_specification["Scenario"] = {"Type": "Tag", "Values": scenarios if scenarios is not None else []} semantic_specification["Name"] = {"Type": "String", "Values": name if name is not None else ""} From 0f0f407819736c417302220582f1966b872f42c8 Mon Sep 17 00:00:00 2001 From: bxdd Date: Tue, 28 Nov 2023 16:49:40 +0800 Subject: [PATCH 04/43] [MNT] move generate_semantic_spec into learnware.specification --- learnware/client/learnware_client.py | 42 ++----------------- learnware/specification/__init__.py | 9 +++- learnware/specification/module.py | 37 +++++++++++++++- .../test_all_learnware.py | 4 +- tests/test_learnware_client/test_upload.py | 3 +- 5 files changed, 51 insertions(+), 44 deletions(-) diff --git a/learnware/client/learnware_client.py b/learnware/client/learnware_client.py index 886be63..56c9e1d 100644 --- a/learnware/client/learnware_client.py +++ b/learnware/client/learnware_client.py @@ -53,7 +53,6 @@ class SemanticSpecificationKey(Enum): DATA_TYPE = "Data" TASK_TYPE = "Task" LIBRARY_TYPE = "Library" - LICENSE = "License" SENARIOES = "Scenario" @@ -247,7 +246,7 @@ class LearnwareClient: for learnware in result["data"]["learnware_list_single"]: returns.append( - { + { "type": "single", "learnware_id": learnware["learnware_id"], "semantic_specification": learnware["semantic_specification"], @@ -259,12 +258,12 @@ class LearnwareClient: "type": "multiple", "learnware_ids": [], "semantic_specifications": [], - "matching": result["data"]["learnware_list_multi"][0]["matching"], + "matching": result["data"]["learnware_list_multi"][0]["matching"] } for learnware in result["data"]["learnware_list_multi"]: multiple_learnware["learnware_ids"].append(learnware["learnware_id"]) multiple_learnware["semantic_specifications"].append(learnware["semantic_specification"]) - + returns.append(multiple_learnware) return returns @@ -278,41 +277,6 @@ class LearnwareClient: if result["code"] != 0: raise Exception("delete failed: " + json.dumps(result)) - def create_semantic_specification( - self, - name: str = None, - description: str = None, - data_type: str = None, - task_type: str = None, - library_type: str = None, - scenarios: Union[str, List(str)] = None, - license: Union[str, List(str)] = None, - input_description: dict = None, - output_description: dict = None, - ): - semantic_specification = dict() - semantic_specification["Data"] = {"Type": "Class", "Values": [data_type] if data_type is not None else []} - semantic_specification["Task"] = {"Type": "Class", "Values": [task_type] if task_type is not None else []} - semantic_specification["Library"] = { - "Type": "Class", - "Values": [library_type] if library _type is not None else [], - } - - license = [license] if isinstance(license, str) else license - semantic_specification["License"] = {"Type": "Class", "Values": license if license is not None else []} - scenarios = [scenarios] if isinstance(scenarios, str) else scenarios - semantic_specification["Scenario"] = {"Type": "Tag", "Values": scenarios if scenarios is not None else []} - - semantic_specification["Name"] = {"Type": "String", "Values": name if name is not None else ""} - semantic_specification["Description"] = { - "Type": "String", - "Values": description if description is not None else "", - } - semantic_specification["Input"] = {} if input_description is None else input_description - semantic_specification["Output"] = {} if output_description is None else output_description - - return semantic_specification - def list_semantic_specification_values(self, key: SemanticSpecificationKey): url = f"{self.host}/engine/semantic_specification" response = requests.get(url, headers=self.headers) diff --git a/learnware/specification/__init__.py b/learnware/specification/__init__.py index 81642cf..c2ac5f9 100644 --- a/learnware/specification/__init__.py +++ b/learnware/specification/__init__.py @@ -17,5 +17,12 @@ if not is_torch_available(verbose=False): generate_rkme_table_spec = None generate_rkme_image_spec = None generate_rkme_text_spec = None + generate_semantic_spec = None else: - from .module import generate_stat_spec, generate_rkme_table_spec, generate_rkme_image_spec, generate_rkme_text_spec + from .module import ( + generate_stat_spec, + generate_rkme_table_spec, + generate_rkme_image_spec, + generate_rkme_text_spec, + generate_semantic_spec, + ) diff --git a/learnware/specification/module.py b/learnware/specification/module.py index f17a76f..8938ff4 100644 --- a/learnware/specification/module.py +++ b/learnware/specification/module.py @@ -1,7 +1,7 @@ import torch import numpy as np import pandas as pd -from typing import Union, List +from typing import Union, List, Optional from .utils import convert_to_numpy from .base import BaseStatSpecification @@ -202,3 +202,38 @@ def generate_stat_spec( return generate_rkme_image_spec(X=X, *args, **kwargs) else: raise TypeError(f"type {type} is not supported!") + + +def generate_semantic_spec( + name: Optional[str] = None, + description: Optional[str] = None, + data_type: Optional[str] = None, + task_type: Optional[str] = None, + library_type: Optional[str] = None, + scenarios: Optional[Union[str, List[str]]] = None, + license: Optional[Union[str, List[str]]] = None, + input_description: Optional[dict] = None, + output_description: Optional[dict] = None, +): + semantic_specification = dict() + semantic_specification["Data"] = {"Type": "Class", "Values": [data_type] if data_type is not None else []} + semantic_specification["Task"] = {"Type": "Class", "Values": [task_type] if task_type is not None else []} + semantic_specification["Library"] = { + "Type": "Class", + "Values": [library_type] if library_type is not None else [], + } + + license = [license] if isinstance(license, str) else license + semantic_specification["License"] = {"Type": "Class", "Values": license if license is not None else []} + scenarios = [scenarios] if isinstance(scenarios, str) else scenarios + semantic_specification["Scenario"] = {"Type": "Tag", "Values": scenarios if scenarios is not None else []} + + semantic_specification["Name"] = {"Type": "String", "Values": name if name is not None else ""} + semantic_specification["Description"] = { + "Type": "String", + "Values": description if description is not None else "", + } + semantic_specification["Input"] = {} if input_description is None else input_description + semantic_specification["Output"] = {} if output_description is None else output_description + + return semantic_specification diff --git a/tests/test_learnware_client/test_all_learnware.py b/tests/test_learnware_client/test_all_learnware.py index 276ac00..8bc9dab 100644 --- a/tests/test_learnware_client/test_all_learnware.py +++ b/tests/test_learnware_client/test_all_learnware.py @@ -5,7 +5,7 @@ import unittest import tempfile from learnware.client import LearnwareClient -from learnware.specification import Specification +from learnware.specification import generate_semantic_spec from learnware.market import BaseUserInfo @@ -31,7 +31,7 @@ class TestAllLearnware(unittest.TestCase): def test_all_learnware(self): max_learnware_num = 1000 - semantic_spec = self.client.create_semantic_specification() + semantic_spec = generate_semantic_spec() user_info = BaseUserInfo(semantic_spec=semantic_spec, stat_info={}) result = self.client.search_learnware(user_info, page_size=max_learnware_num) print(f"result size: {len(result)}") diff --git a/tests/test_learnware_client/test_upload.py b/tests/test_learnware_client/test_upload.py index a38f938..324ea77 100644 --- a/tests/test_learnware_client/test_upload.py +++ b/tests/test_learnware_client/test_upload.py @@ -4,6 +4,7 @@ import unittest import tempfile from learnware.client import LearnwareClient +from learnware.specification import generate_semantic_spec class TestAllLearnware(unittest.TestCase): @@ -37,7 +38,7 @@ class TestAllLearnware(unittest.TestCase): "0": "the probability of being a cat", }, } - semantic_spec = self.client.create_semantic_specification( + semantic_spec = generate_semantic_spec( name="learnware_example", description="Just a example for uploading a learnware", data_type="Table", From 2fa4b4d188f072c25c1e84c707d8b205f2b5fff2 Mon Sep 17 00:00:00 2001 From: bxdd Date: Tue, 28 Nov 2023 21:35:47 +0800 Subject: [PATCH 05/43] [ENH] add hetero spec, and refactor specification test --- learnware/client/learnware_client.py | 7 +- learnware/specification/module.py | 2 +- learnware/tests/__init__.py | 1 + learnware/tests/utils.py | 9 ++ .../test_all_learnware.py | 43 +++---- tests/test_learnware_client/test_container.py | 54 ++++++++ .../test_load_learnware.py | 115 ++++++------------ tests/test_learnware_client/test_upload.py | 44 +++---- tests/test_specification/test_hetero_spec.py | 43 +++++++ tests/test_specification/test_image_rkme.py | 40 ++++++ tests/test_specification/test_rkme.py | 104 ---------------- tests/test_specification/test_table_rkme.py | 39 ++++++ tests/test_specification/test_text_rkme.py | 58 +++++++++ 13 files changed, 335 insertions(+), 224 deletions(-) create mode 100644 learnware/tests/utils.py create mode 100644 tests/test_learnware_client/test_container.py create mode 100644 tests/test_specification/test_hetero_spec.py create mode 100644 tests/test_specification/test_image_rkme.py delete mode 100644 tests/test_specification/test_rkme.py create mode 100644 tests/test_specification/test_table_rkme.py create mode 100644 tests/test_specification/test_text_rkme.py diff --git a/learnware/client/learnware_client.py b/learnware/client/learnware_client.py index 56c9e1d..f16cfa0 100644 --- a/learnware/client/learnware_client.py +++ b/learnware/client/learnware_client.py @@ -67,6 +67,7 @@ class LearnwareClient: self.chunk_size = 1024 * 1024 self.tempdir_list = [] + self.login_status = False atexit.register(self.cleanup) def login(self, email, token): @@ -80,7 +81,11 @@ class LearnwareClient: token = result["data"]["token"] self.headers = {"Authorization": f"Bearer {token}"} - + self.login_status = True + + def is_login(self): + return self.login_status + @require_login def logout(self): url = f"{self.host}/auth/logout" diff --git a/learnware/specification/module.py b/learnware/specification/module.py index 8938ff4..10b4fdc 100644 --- a/learnware/specification/module.py +++ b/learnware/specification/module.py @@ -175,7 +175,7 @@ def generate_rkme_text_spec( def generate_stat_spec( type: str, X: Union[np.ndarray, pd.DataFrame, torch.Tensor, List[str]], *args, **kwargs -) -> BaseStatSpecification: +) -> Union[RKMETableSpecification, RKMEImageSpecification, RKMETextSpecification]: """ Interface for users to generate statistical specification. Return a StatSpecification object, use .save() method to save as npy file. diff --git a/learnware/tests/__init__.py b/learnware/tests/__init__.py index a048b3f..5019465 100644 --- a/learnware/tests/__init__.py +++ b/learnware/tests/__init__.py @@ -1 +1,2 @@ from .module import get_semantic_specification +from .utils import parametrize \ No newline at end of file diff --git a/learnware/tests/utils.py b/learnware/tests/utils.py new file mode 100644 index 0000000..d950bf3 --- /dev/null +++ b/learnware/tests/utils.py @@ -0,0 +1,9 @@ +import unittest + +def parametrize(test_class, **kwargs): + test_loader = unittest.TestLoader() + test_names = test_loader.getTestCaseNames(test_class) + _suite = unittest.TestSuite() + for name in test_names: + _suite.addTest(test_class(name, **kwargs)) + return _suite \ No newline at end of file diff --git a/tests/test_learnware_client/test_all_learnware.py b/tests/test_learnware_client/test_all_learnware.py index 8bc9dab..f7fc9d8 100644 --- a/tests/test_learnware_client/test_all_learnware.py +++ b/tests/test_learnware_client/test_all_learnware.py @@ -3,32 +3,27 @@ import json import zipfile import unittest import tempfile +import argparse from learnware.client import LearnwareClient from learnware.specification import generate_semantic_spec from learnware.market import BaseUserInfo - +from learnware.tests import parametrize class TestAllLearnware(unittest.TestCase): - def setUp(self): - unittest.TestCase.setUpClass() - dir_path = os.path.dirname(__file__) - config_path = os.path.join(dir_path, "config.json") - if not os.path.exists(config_path): - data = {"email": None, "token": None} - with open(config_path, "w") as file: - json.dump(data, file) - - with open(config_path, "r") as file: - data = json.load(file) - email = data["email"] - token = data["token"] - - if email is None or token is None: - raise ValueError("Please set email and token in config.json.") - self.client = LearnwareClient() - self.client.login(email, token) - + client = LearnwareClient() + + def __init__(self, method_name='runTest', email=None, token=None): + super(TestAllLearnware, self).__init__(method_name) + self.email = email + self.token = token + + if self.email is not None and self.token is not None: + self.client.login(self.email, self.token) + else: + print("Client doest not login, all tests will be ignored!") + + @unittest.skipIf(not client.is_login(), "Client doest not login!") def test_all_learnware(self): max_learnware_num = 1000 semantic_spec = generate_semantic_spec() @@ -57,4 +52,10 @@ class TestAllLearnware(unittest.TestCase): if __name__ == "__main__": - unittest.main() + parser = argparse.ArgumentParser() + parser.add_argument("--email", type=str, required=False, help="The email to login learnware client") + parser.add_argument("--token", type=str, required=False, help="The token to login learnware client") + args = parser.parse_args() + + runner = unittest.TextTestRunner() + runner.run(parametrize(TestAllLearnware, email=args.email, token=args.token)) \ No newline at end of file diff --git a/tests/test_learnware_client/test_container.py b/tests/test_learnware_client/test_container.py new file mode 100644 index 0000000..c96d2ab --- /dev/null +++ b/tests/test_learnware_client/test_container.py @@ -0,0 +1,54 @@ +import os +import unittest +import argparse +import numpy as np + +from learnware.learnware import get_learnware_from_dirpath +from learnware.client import LearnwareClient +from learnware.client.container import ModelCondaContainer, LearnwaresContainer +from learnware.tests import parametrize + +class TestContainer(unittest.TestCase): + def __init__(self, method_name='runTest', mode="all"): + super(TestContainer, self).__init__(method_name) + self.modes = [] + if mode in {"all", "conda"}: + self.modes.append("conda") + if mode in {"all", "docker"}: + self.modes.append("docker") + + def setUp(self): + self.client = LearnwareClient() + + def _test_container_with_pip(self, mode): + learnware_id = "00000147" + learnware = self.client.load_learnware(learnware_id=learnware_id) + with LearnwaresContainer(learnware, ignore_error=False, mode=mode) as env_container: + learnware = env_container.get_learnwares_with_container()[0] + input_array = np.random.random(size=(20, 23)) + print(learnware.predict(input_array)) + + def _test_container_with_conda(self, mode): + learnware_id = "00000148" + learnware = self.client.load_learnware(learnware_id=learnware_id) + with LearnwaresContainer(learnware, ignore_error=False, mode=mode) as env_container: + learnware = env_container.get_learnwares_with_container()[0] + input_array = np.random.random(size=(20, 204)) + print(learnware.predict(input_array)) + + def test_container_with_pip(self): + for mode in self.modes: + self._test_container_with_pip(mode=mode) + + def test_container_with_conda(self): + for mode in self.modes: + self._test_container_with_conda(mode=mode) + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--mode", type=str, required=False, default="all", help="The mode to run container, must be in ['all', 'conda', 'docker']") + args = parser.parse_args() + + assert args.mode in {"all", "conda", "docker"}, f"The mode must be in ['all', 'conda', 'docker'], instead of '{args.mode}'" + runner = unittest.TextTestRunner() + runner.run(parametrize(TestContainer, mode=args.mode)) \ No newline at end of file diff --git a/tests/test_learnware_client/test_load_learnware.py b/tests/test_learnware_client/test_load_learnware.py index 8381f90..fafb261 100644 --- a/tests/test_learnware_client/test_load_learnware.py +++ b/tests/test_learnware_client/test_load_learnware.py @@ -8,94 +8,59 @@ from learnware.learnware import get_learnware_from_dirpath from learnware.client import LearnwareClient from learnware.client.container import ModelCondaContainer, LearnwaresContainer from learnware.reuse import AveragingReuser +from learnware.tests import parametrize -class TestLearnwareLoadWithConda(unittest.TestCase): +class TestLearnwareLoad(unittest.TestCase): + def __init__(self, method_name='runTest', mode="conda"): + super(TestLearnwareLoad, self).__init__(method_name) + self.runnable_options = [] + if mode in {"all", "conda"}: + self.runnable_options.append("conda") + if mode in {"all", "docker"}: + self.runnable_options.append("docker") + def setUp(self): self.client = LearnwareClient() root = os.path.dirname(__file__) self.learnware_ids = ["00000084", "00000154", "00000155"] self.zip_paths = [os.path.join(root, x) for x in ["1.zip", "2.zip", "3.zip"]] - self.runnable_option = "conda" - #def test_load_single_learnware_by_zippath(self): - # for learnware_id, zip_path in zip(self.learnware_ids, self.zip_paths): - # self.client.download_learnware(learnware_id, zip_path) -# - # learnware_list = [ - # self.client.load_learnware(learnware_path=zippath, runnable_option=self.runnable_option) for zippath in self.zip_paths - # ] - # reuser = AveragingReuser(learnware_list, mode="vote_by_label") - # input_array = np.random.random(size=(20, 13)) - # print(reuser.predict(input_array)) -# - # for learnware in learnware_list: - # print(learnware.id, learnware.predict(input_array)) -# - #def test_load_multi_learnware_by_zippath(self): - # for learnware_id, zip_path in zip(self.learnware_ids, self.zip_paths): - # self.client.download_learnware(learnware_id, zip_path) -# - # learnware_list = self.client.load_learnware(learnware_path=self.zip_paths, runnable_option=self.runnable_option) - # reuser = AveragingReuser(learnware_list, mode="vote_by_label") - # input_array = np.random.random(size=(20, 13)) - # print(reuser.predict(input_array)) -# - # for learnware in learnware_list: - # print(learnware.id, learnware.predict(input_array)) -# - #def test_load_single_learnware_by_id(self): - # learnware_list = [ - # self.client.load_learnware(learnware_id=idx, runnable_option=self.runnable_option) for idx in self.learnware_ids - # ] - # reuser = AveragingReuser(learnware_list, mode="vote_by_label") - # input_array = np.random.random(size=(20, 13)) - # print(reuser.predict(input_array)) -# - # for learnware in learnware_list: - # print(learnware.id, learnware.predict(input_array)) -# - #def test_load_multi_learnware_by_id(self): - # learnware_list = self.client.load_learnware(learnware_id=self.learnware_ids, runnable_option=self.runnable_option) - # reuser = AveragingReuser(learnware_list, mode="vote_by_label") - # input_array = np.random.random(size=(20, 13)) - # print(reuser.predict(input_array)) -# - # for learnware in learnware_list: - # print(learnware.id, learnware.predict(input_array)) -# - def test_load_single_learnware_by_id_pip(self): - learnware_id = "00000147" - learnware = self.client.load_learnware(learnware_id=learnware_id, runnable_option=self.runnable_option) - input_array = np.random.random(size=(20, 23)) - print(learnware.predict(input_array)) -# - def test_load_single_learnware_by_id_conda(self): - learnware_id = "00000148" - learnware = self.client.load_learnware(learnware_id=learnware_id, runnable_option=self.runnable_option) - input_array = np.random.random(size=(20, 204)) - print(learnware.predict(input_array)) -# -# -class TestLearnwareLoadWithDocker(TestLearnwareLoadWithConda): - def setUp(self): - super(TestLearnwareLoadWithDocker, self).setUp() - self.runnable_option = "docker" + def _test_load_learnware_by_zippath(self, runnable_option): + for learnware_id, zip_path in zip(self.learnware_ids, self.zip_paths): + self.client.download_learnware(learnware_id, zip_path) + + learnware_list = self.client.load_learnware(learnware_path=self.zip_paths, runnable_option=runnable_option) + reuser = AveragingReuser(learnware_list, mode="vote_by_label") + input_array = np.random.random(size=(20, 13)) + print(reuser.predict(input_array)) + for learnware in learnware_list: + print(learnware.id, learnware.predict(input_array)) + + + def _test_load_learnware_by_id(self, runnable_option): + learnware_list = self.client.load_learnware(learnware_id=self.learnware_ids, runnable_option=runnable_option) + reuser = AveragingReuser(learnware_list, mode="vote_by_label") + input_array = np.random.random(size=(20, 13)) + print(reuser.predict(input_array)) + + for learnware in learnware_list: + print(learnware.id, learnware.predict(input_array)) -def suite(mode): - _suite = unittest.TestSuite() - #_suite.addTest(TestLearnwareLoadWithDocker()) - if mode == "all" or mode == "conda": - _suite.addTest(unittest.makeSuite(TestLearnwareLoadWithConda)) - if mode == "all" or mode == "docker": - _suite.addTest(unittest.makeSuite(TestLearnwareLoadWithDocker)) - return _suite + def test_load_learnware_by_zippath(self): + for runnable_option in self.runnable_options: + self._test_load_learnware_by_zippath(runnable_option=runnable_option) + + def test_load_learnware_by_id(self): + for runnable_option in self.runnable_options: + self._test_load_learnware_by_id(runnable_option=runnable_option) + if __name__ == "__main__": parser = argparse.ArgumentParser() - parser.add_argument("--mode", type=str, required=False, default="all", help="The mode to run load learnware, must be in ['all', 'conda', 'docker']") + parser.add_argument("--mode", type=str, required=False, default="conda", help="The mode to load learnware, must be in ['all', 'conda', 'docker']") args = parser.parse_args() assert args.mode in {"all", "conda", "docker"}, f"The mode must be in ['all', 'conda', 'docker'], instead of '{args.mode}'" runner = unittest.TextTestRunner() - runner.run(suite(args.mode)) \ No newline at end of file + runner.run(parametrize(TestLearnwareLoad, mode=args.mode)) \ No newline at end of file diff --git a/tests/test_learnware_client/test_upload.py b/tests/test_learnware_client/test_upload.py index 324ea77..8bcd988 100644 --- a/tests/test_learnware_client/test_upload.py +++ b/tests/test_learnware_client/test_upload.py @@ -1,32 +1,26 @@ import os -import json +import argparse import unittest import tempfile from learnware.client import LearnwareClient from learnware.specification import generate_semantic_spec +from learnware.tests import parametrize +class TestUpload(unittest.TestCase): + client = LearnwareClient() + + def __init__(self, method_name='runTest', email=None, token=None): + super(TestUpload, self).__init__(method_name) + self.email = email + self.token = token + + if self.email is not None and self.token is not None: + self.client.login(self.email, self.token) + else: + print("Client doest not login, all tests will be ignored!") -class TestAllLearnware(unittest.TestCase): - def setUp(self): - unittest.TestCase.setUpClass() - dir_path = os.path.dirname(__file__) - config_path = os.path.join(dir_path, "config.json") - if not os.path.exists(config_path): - data = {"email": None, "token": None} - with open(config_path, "w") as file: - json.dump(data, file) - - with open(config_path, "r") as file: - data = json.load(file) - email = data["email"] - token = data["token"] - - if email is None or token is None: - raise ValueError("Please set email and token in config.json.") - self.client = LearnwareClient() - self.client.login(email, token) - + @unittest.skipIf(not client.is_login(), "Client doest not login!") def test_upload(self): input_description = { "Dimension": 13, @@ -67,4 +61,10 @@ class TestAllLearnware(unittest.TestCase): if __name__ == "__main__": - unittest.main() + parser = argparse.ArgumentParser() + parser.add_argument("--email", type=str, required=False, help="The email to login learnware client") + parser.add_argument("--token", type=str, required=False, help="The token to login learnware client") + args = parser.parse_args() + + runner = unittest.TextTestRunner() + runner.run(parametrize(TestUpload, email=args.email, token=args.token)) \ No newline at end of file diff --git a/tests/test_specification/test_hetero_spec.py b/tests/test_specification/test_hetero_spec.py new file mode 100644 index 0000000..21563b3 --- /dev/null +++ b/tests/test_specification/test_hetero_spec.py @@ -0,0 +1,43 @@ +import os +import json +import string +import random +import torch +import unittest +import tempfile +import numpy as np + +from learnware.specification import RKMETableSpecification, HeteroMapTableSpecification +from learnware.specification import generate_stat_spec +from learnware.market.heterogeneous.organizer import HeteroMap + +class TestTableRKME(unittest.TestCase): + + def setUp(self): + self.hetero_map = HeteroMap() + + def _test_hetero_spec(self, X): + rkme: RKMETableSpecification = generate_stat_spec(type="table", X=X) + hetero_spec = self.hetero_map.hetero_mapping(rkme_spec=rkme, features=dict()) + with tempfile.TemporaryDirectory(prefix="learnware_") as tempdir: + rkme_path = os.path.join(tempdir, "rkme.json") + hetero_spec.save(rkme_path) + + with open(rkme_path, "r") as f: + data = json.load(f) + assert data["type"] == "HeteroMapTableSpecification" + + rkme2 = HeteroMapTableSpecification() + rkme2.load(rkme_path) + assert rkme2.type == "HeteroMapTableSpecification" + + + def test_hetero_rkme(self): + self._test_hetero_spec(np.random.uniform(-10000, 10000, size=(5000, 200))) + self._test_hetero_spec(np.random.uniform(-10000, 10000, size=(10000, 100))) + self._test_hetero_spec(np.random.uniform(-10000, 10000, size=(5, 20))) + self._test_hetero_spec(np.random.uniform(-10000, 10000, size=(1, 50))) + self._test_hetero_spec(np.random.uniform(-10000, 10000, size=(100, 150))) + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_specification/test_image_rkme.py b/tests/test_specification/test_image_rkme.py new file mode 100644 index 0000000..ad5b1ac --- /dev/null +++ b/tests/test_specification/test_image_rkme.py @@ -0,0 +1,40 @@ +import os +import json +import string +import random +import torch +import unittest +import tempfile +import numpy as np + +from learnware.specification import RKMEImageSpecification +from learnware.specification import generate_stat_spec + + +class TestImageRKME(unittest.TestCase): + @staticmethod + def _test_image_rkme(X): + image_rkme = generate_stat_spec(type="image", X=X, steps=10) + + with tempfile.TemporaryDirectory(prefix="learnware_") as tempdir: + rkme_path = os.path.join(tempdir, "rkme.json") + image_rkme.save(rkme_path) + + with open(rkme_path, "r") as f: + data = json.load(f) + assert data["type"] == "RKMEImageSpecification" + + rkme2 = RKMEImageSpecification() + rkme2.load(rkme_path) + assert rkme2.type == "RKMEImageSpecification" + + def test_image_rkme(self): + self._test_image_rkme(np.random.randint(0, 255, size=(2000, 3, 32, 32))) + self._test_image_rkme(np.random.randint(0, 255, size=(100, 1, 128, 128))) + self._test_image_rkme(np.random.randint(0, 255, size=(50, 3, 128, 128)) / 255) + self._test_image_rkme(torch.randint(0, 255, (2000, 3, 32, 32))) + self._test_image_rkme(torch.randint(0, 255, (20, 3, 128, 128))) + self._test_image_rkme(torch.randint(0, 255, (1, 1, 128, 128)) / 255) + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_specification/test_rkme.py b/tests/test_specification/test_rkme.py deleted file mode 100644 index 3b33e14..0000000 --- a/tests/test_specification/test_rkme.py +++ /dev/null @@ -1,104 +0,0 @@ -import os -import json -import string -import random -import torch -import unittest -import tempfile -import numpy as np - -from learnware.specification import RKMETableSpecification, RKMEImageSpecification, RKMETextSpecification -from learnware.specification import generate_stat_spec - - -class TestRKME(unittest.TestCase): - def test_rkme(self): - def _test_table_rkme(X): - rkme = generate_stat_spec(type="table", X=X) - - with tempfile.TemporaryDirectory(prefix="learnware_") as tempdir: - rkme_path = os.path.join(tempdir, "rkme.json") - rkme.save(rkme_path) - - with open(rkme_path, "r") as f: - data = json.load(f) - assert data["type"] == "RKMETableSpecification" - - rkme2 = RKMETableSpecification() - rkme2.load(rkme_path) - assert rkme2.type == "RKMETableSpecification" - - _test_table_rkme(np.random.uniform(-10000, 10000, size=(5000, 200))) - _test_table_rkme(np.random.uniform(-10000, 10000, size=(10000, 100))) - _test_table_rkme(np.random.uniform(-10000, 10000, size=(5, 20))) - _test_table_rkme(np.random.uniform(-10000, 10000, size=(1, 50))) - _test_table_rkme(np.random.uniform(-10000, 10000, size=(100, 150))) - - def test_image_rkme(self): - def _test_image_rkme(X): - image_rkme = generate_stat_spec(type="image", X=X, steps=10) - - with tempfile.TemporaryDirectory(prefix="learnware_") as tempdir: - rkme_path = os.path.join(tempdir, "rkme.json") - image_rkme.save(rkme_path) - - with open(rkme_path, "r") as f: - data = json.load(f) - assert data["type"] == "RKMEImageSpecification" - - rkme2 = RKMEImageSpecification() - rkme2.load(rkme_path) - assert rkme2.type == "RKMEImageSpecification" - - _test_image_rkme(np.random.randint(0, 255, size=(2000, 3, 32, 32))) - _test_image_rkme(np.random.randint(0, 255, size=(100, 1, 128, 128))) - _test_image_rkme(np.random.randint(0, 255, size=(50, 3, 128, 128)) / 255) - - _test_image_rkme(torch.randint(0, 255, (2000, 3, 32, 32))) - _test_image_rkme(torch.randint(0, 255, (20, 3, 128, 128))) - _test_image_rkme(torch.randint(0, 255, (1, 1, 128, 128)) / 255) - - def test_text_rkme(self): - def generate_random_text_list(num, text_type="en", min_len=10, max_len=1000): - text_list = [] - for i in range(num): - length = random.randint(min_len, max_len) - if text_type == "en": - characters = string.ascii_letters + string.digits + string.punctuation - result_str = "".join(random.choice(characters) for i in range(length)) - text_list.append(result_str) - elif text_type == "zh": - result_str = "".join(chr(random.randint(0x4E00, 0x9FFF)) for i in range(length)) - text_list.append(result_str) - else: - raise ValueError("Type should be en or zh") - return text_list - - def _test_text_rkme(X): - rkme = generate_stat_spec(type="text", X=X) - - with tempfile.TemporaryDirectory(prefix="learnware_") as tempdir: - rkme_path = os.path.join(tempdir, "rkme.json") - rkme.save(rkme_path) - - with open(rkme_path, "r") as f: - data = json.load(f) - assert data["type"] == "RKMETextSpecification" - - rkme2 = RKMETextSpecification() - rkme2.load(rkme_path) - assert rkme2.type == "RKMETextSpecification" - - return rkme2.get_z().shape[1] - - dim1 = _test_text_rkme(generate_random_text_list(3000, "en")) - dim2 = _test_text_rkme(generate_random_text_list(100, "en")) - dim3 = _test_text_rkme(generate_random_text_list(50, "zh")) - dim4 = _test_text_rkme(generate_random_text_list(5000, "zh")) - dim5 = _test_text_rkme(generate_random_text_list(1, "zh")) - - assert dim1 == dim2 and dim2 == dim3 and dim3 == dim4 and dim4 == dim5 - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_specification/test_table_rkme.py b/tests/test_specification/test_table_rkme.py new file mode 100644 index 0000000..ed26314 --- /dev/null +++ b/tests/test_specification/test_table_rkme.py @@ -0,0 +1,39 @@ +import os +import json +import string +import random +import torch +import unittest +import tempfile +import numpy as np + +from learnware.specification import RKMETableSpecification, RKMEImageSpecification, RKMETextSpecification +from learnware.specification import generate_stat_spec + + +class TestTableRKME(unittest.TestCase): + @staticmethod + def _test_table_rkme(X): + rkme = generate_stat_spec(type="table", X=X) + + with tempfile.TemporaryDirectory(prefix="learnware_") as tempdir: + rkme_path = os.path.join(tempdir, "rkme.json") + rkme.save(rkme_path) + + with open(rkme_path, "r") as f: + data = json.load(f) + assert data["type"] == "RKMETableSpecification" + + rkme2 = RKMETableSpecification() + rkme2.load(rkme_path) + assert rkme2.type == "RKMETableSpecification" + + def test_table_rkme(self): + self._test_table_rkme(np.random.uniform(-10000, 10000, size=(5000, 200))) + self._test_table_rkme(np.random.uniform(-10000, 10000, size=(10000, 100))) + self._test_table_rkme(np.random.uniform(-10000, 10000, size=(5, 20))) + self._test_table_rkme(np.random.uniform(-10000, 10000, size=(1, 50))) + self._test_table_rkme(np.random.uniform(-10000, 10000, size=(100, 150))) + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_specification/test_text_rkme.py b/tests/test_specification/test_text_rkme.py new file mode 100644 index 0000000..0409d98 --- /dev/null +++ b/tests/test_specification/test_text_rkme.py @@ -0,0 +1,58 @@ +import os +import json +import string +import random +import unittest +import tempfile + +from learnware.specification import RKMETextSpecification +from learnware.specification import generate_stat_spec + + +class TestTextRKME(unittest.TestCase): + @staticmethod + def generate_random_text_list(num, text_type="en", min_len=10, max_len=1000): + text_list = [] + for i in range(num): + length = random.randint(min_len, max_len) + if text_type == "en": + characters = string.ascii_letters + string.digits + string.punctuation + result_str = "".join(random.choice(characters) for i in range(length)) + text_list.append(result_str) + elif text_type == "zh": + result_str = "".join(chr(random.randint(0x4E00, 0x9FFF)) for i in range(length)) + text_list.append(result_str) + else: + raise ValueError("Type should be en or zh") + return text_list + + @staticmethod + def _test_text_rkme(X): + rkme = generate_stat_spec(type="text", X=X) + + with tempfile.TemporaryDirectory(prefix="learnware_") as tempdir: + rkme_path = os.path.join(tempdir, "rkme.json") + rkme.save(rkme_path) + + with open(rkme_path, "r") as f: + data = json.load(f) + assert data["type"] == "RKMETextSpecification" + + rkme2 = RKMETextSpecification() + rkme2.load(rkme_path) + assert rkme2.type == "RKMETextSpecification" + + return rkme2.get_z().shape[1] + + def test_text_rkme(self): + dim1 = self._test_text_rkme(self.generate_random_text_list(3000, "en")) + dim2 = self._test_text_rkme(self.generate_random_text_list(100, "en")) + dim3 = self._test_text_rkme(self.generate_random_text_list(50, "zh")) + dim4 = self._test_text_rkme(self.generate_random_text_list(5000, "zh")) + dim5 = self._test_text_rkme(self.generate_random_text_list(1, "zh")) + + assert dim1 == dim2 and dim2 == dim3 and dim3 == dim4 and dim4 == dim5 + + +if __name__ == "__main__": + unittest.main() From a27ed1042c1978924aa4496220c6691342341ab2 Mon Sep 17 00:00:00 2001 From: bxdd Date: Tue, 28 Nov 2023 22:47:08 +0800 Subject: [PATCH 06/43] [MNT] modify test and docs style --- docs/_static/css/custom_style.css | 21 ++------------------- docs/advanced/evolve.rst | 2 +- docs/conf.py | 12 ++++++------ setup.py | 2 +- tests/test_reuse/test_averaging_reuse.py | 0 tests/test_specification/test_image_rkme.py | 2 -- tests/test_specification/test_table_rkme.py | 3 --- 7 files changed, 10 insertions(+), 32 deletions(-) create mode 100644 tests/test_reuse/test_averaging_reuse.py diff --git a/docs/_static/css/custom_style.css b/docs/_static/css/custom_style.css index 2475bcd..6c2d87e 100644 --- a/docs/_static/css/custom_style.css +++ b/docs/_static/css/custom_style.css @@ -1,20 +1,3 @@ -.bd-main { - flex-grow: 1; - flex-direction: column; - display: flex; - min-width: 0; -} - -.bd-main .bd-content { - justify-content: left; -} - -.bd-main .bd-content .bd-article-container { - max-width: calc(100% - var(--pst-sidebar-secondary)); -} - -.bd-sidebar-primary div#rtd-footer-container { - bottom: -1rem; - margin: -1rem; - position: fixed; +body { + overflow: scroll; } \ No newline at end of file diff --git a/docs/advanced/evolve.rst b/docs/advanced/evolve.rst index a8687b0..85201fa 100644 --- a/docs/advanced/evolve.rst +++ b/docs/advanced/evolve.rst @@ -1,5 +1,5 @@ ============================== -Specification evolvement +Specification Evolvement ============================== The specification is the core of the learnware paradigm. diff --git a/docs/conf.py b/docs/conf.py index 81ad06e..155d20a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -90,12 +90,12 @@ todo_include_todos = True # html_theme = "sphinx_book_theme" html_theme_path = [sphinx_book_theme.get_html_theme_path()] -#html_theme_options = { -# "logo_only": True, -# "collapse_navigation": False, -# "display_version": False, -# "navigation_depth": 4, -#} +html_theme_options = { + "logo_only": True, + "collapse_navigation": False, + # "display_version": False, + "navigation_depth": 4, +} html_logo = "_static/img/logo/logo1.png" diff --git a/setup.py b/setup.py index ad35bb4..0c636b8 100644 --- a/setup.py +++ b/setup.py @@ -57,7 +57,7 @@ REQUIRED = [ DEV_REQUIRED = [ # For documentations "sphinx", - "sphinx_book_theme", + "sphinx_book_theme==0.3.3", # CI dependencies "pytest>=3", "wheel", diff --git a/tests/test_reuse/test_averaging_reuse.py b/tests/test_reuse/test_averaging_reuse.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_specification/test_image_rkme.py b/tests/test_specification/test_image_rkme.py index ad5b1ac..29312bf 100644 --- a/tests/test_specification/test_image_rkme.py +++ b/tests/test_specification/test_image_rkme.py @@ -1,7 +1,5 @@ import os import json -import string -import random import torch import unittest import tempfile diff --git a/tests/test_specification/test_table_rkme.py b/tests/test_specification/test_table_rkme.py index ed26314..9c314f1 100644 --- a/tests/test_specification/test_table_rkme.py +++ b/tests/test_specification/test_table_rkme.py @@ -1,8 +1,5 @@ import os import json -import string -import random -import torch import unittest import tempfile import numpy as np From a3a6f25e3a75ea9cf65355be7d5b7114c105d6a5 Mon Sep 17 00:00:00 2001 From: shihy Date: Tue, 28 Nov 2023 23:19:33 +0800 Subject: [PATCH 07/43] [DOC] Update Usage&Example and Privacy-Protection --- docs/_static/img/image_spec.png | Bin 0 -> 342628 bytes docs/components/spec.rst | 47 ++++++++++++++++++++++++++++---- 2 files changed, 42 insertions(+), 5 deletions(-) create mode 100644 docs/_static/img/image_spec.png diff --git a/docs/_static/img/image_spec.png b/docs/_static/img/image_spec.png new file mode 100644 index 0000000000000000000000000000000000000000..0e3de13d86713cbb3c1f830e4d4198843664f11f GIT binary patch literal 342628 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 z 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%0AoI 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

K`&~YHcm(fO784+lUn|DrS_+U>@ixcSFOyxn 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≥(ggoA3@kUCw~x1vmPca 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 Date: Sun, 3 Dec 2023 13:42:55 +0800 Subject: [PATCH 08/43] [ENH] add test template --- .../client/scripts/__init__.py | 0 learnware/learnware/__init__.py | 4 +- learnware/tests/module.py | 5 ++ learnware/tests/templates/__init__.py | 76 +++++++++++++++++++ learnware/tests/templates/pickle_model.py | 31 ++++++++ tests/test_reuse/test_averaging.py | 43 +++++++++++ .../learnware_example/example_init.py | 2 +- 7 files changed, 158 insertions(+), 3 deletions(-) rename tests/test_reuse/test_averaging_reuse.py => learnware/client/scripts/__init__.py (100%) create mode 100644 learnware/tests/templates/__init__.py create mode 100644 learnware/tests/templates/pickle_model.py create mode 100644 tests/test_reuse/test_averaging.py diff --git a/tests/test_reuse/test_averaging_reuse.py b/learnware/client/scripts/__init__.py similarity index 100% rename from tests/test_reuse/test_averaging_reuse.py rename to learnware/client/scripts/__init__.py diff --git a/learnware/learnware/__init__.py b/learnware/learnware/__init__.py index 738a55d..0f88957 100644 --- a/learnware/learnware/__init__.py +++ b/learnware/learnware/__init__.py @@ -1,8 +1,8 @@ import os import copy +from typing import Optional from .base import Learnware - from .utils import get_stat_spec_from_config from ..specification import Specification from ..utils import read_yaml_to_dict @@ -12,7 +12,7 @@ from ..config import C logger = get_module_logger("learnware.learnware") -def get_learnware_from_dirpath(id: str, semantic_spec: dict, learnware_dirpath, ignore_error=True) -> Learnware: +def get_learnware_from_dirpath(id: str, semantic_spec: dict, learnware_dirpath, ignore_error=True) -> Optional[Learnware]: """Get the learnware object from dirpath, and provide the manage interface tor Learnware class Parameters diff --git a/learnware/tests/module.py b/learnware/tests/module.py index 52300a6..9556bdf 100644 --- a/learnware/tests/module.py +++ b/learnware/tests/module.py @@ -8,3 +8,8 @@ def get_semantic_specification(): semantic_specification["Name"] = {"Type": "String", "Values": "test"} semantic_specification["Description"] = {"Type": "String", "Values": "test"} return semantic_specification + + + + +def get_requirements_file() \ No newline at end of file diff --git a/learnware/tests/templates/__init__.py b/learnware/tests/templates/__init__.py new file mode 100644 index 0000000..8eba13c --- /dev/null +++ b/learnware/tests/templates/__init__.py @@ -0,0 +1,76 @@ +import os +import tempfile +from shutil import copyfile +from typing import List, Tuple, Union, Optional + +from ...utils import save_dict_to_yaml +from ...config import C + +class LearnwareTemplate: + def __init__(self): + self.model_templates = { + "pickle": { + "class_name": 'PickleLoadedModel', + "template_path": os.path.join(C.package_path, "tests", "templates", "pickle_model.py") + } + } + + def generate_requirements(self, filepath, requirements: Optional[List[Union[Tuple[str, str, str], str]]] = None): + requirements = [] if requirements is None else requirements + operators = {"==", "~=", ">=", "<=", ">", "<"} + requirements_str = "" + for requirement in requirements: + if isinstance(requirement, str): + line_str = requirement.strip() + "\n" + elif isinstance(requirement, tuple): + assert requirement[1] in operators, f"The operator of requirements is not supported." + line_str = requirement[0].strip() + requirement[1].strip() + requirement[2].strip() + "\n" + else: + raise TypeError(f"requirement must be type str/tuple, rather than {type(requirement)}") + + requirements_str += line_str + + with open(filepath, "w") as fdout: + fdout.write(requirements_str) + + def generate_learnware_yaml(self, filepath, model_config: Optional[dict] = None, stat_spec_config: Optional[List[dict]] = None): + learnware_config = {} + if model_config is not None: + learnware_config["model"] = model_config + if stat_spec_config is not None: + learnware_config["stat_specifications"] = stat_spec_config + + save_dict_to_yaml(learnware_config, filepath) + + + + def generate_learnware_zipfile( + self, + learnware_zippath: str, + model_template: str = "pickle", + model_kwargs: Optional[dict] = None, + stat_spec_config: Optional[List[dict]] = None, + requirements: Optional[List[Union[Tuple[str, str, str], str]]] = None, + **kwargs, + ): + with tempfile.TemporaryDirectory(suffix="learnware_template") as tempdir: + requirement_filepath = os.path.join(tempdir, "requirements.txt") + self.generate_requirements(requirement_filepath, requirements) + + model_filepath = os.path.join(tempdir, "__init__.py") + copyfile(self.model_templates[model_template]["template_path"], model_filepath) + + learnware_yaml_filepath = os.path.join(tempdir, "requirements.txt") + model_config = { + "class_name": self.model_templates[model_template]["class_name"], + "kwargs": {} if model_kwargs is None else model_kwargs + } + self.generate_learnware_yaml(learnware_yaml_filepath, model_config, stat_spec_config) + + if model_template == "pickle": + pickle_filepath = os.path.join(tempdir, model_config["kwargs"]["pickle_filepath"]) + copyfile(kwargs["pickle_filepath"], pickle_filepath) + + + def generate_template_semantic_spec(self): + pass \ No newline at end of file diff --git a/learnware/tests/templates/pickle_model.py b/learnware/tests/templates/pickle_model.py new file mode 100644 index 0000000..267f44f --- /dev/null +++ b/learnware/tests/templates/pickle_model.py @@ -0,0 +1,31 @@ +import os +import pickle +import numpy as np +from learnware.model.base import BaseModel + +class PickleLoadedModel(BaseModel): + + def __init__( + self, + input_shape, + output_shape, + pickle_filepath, + predict_method="predict", + fit_method="fit", + finetune_method="finetune", + ): + super(PickleLoadedModel, self).__init__(input_shape=input_shape, output_shape=output_shape) + with open(pickle_filepath, "rb") as fd: + self.model = pickle.load(fd) + self.predict_method = predict_method + self.fit_method = fit_method + self.finetune_method = finetune_method + + def predict(self, X: np.ndarray) -> np.ndarray: + return getattr(self.model, self.predict_method)(X) + + def fit(self, X: np.ndarray, y: np.ndarray): + getattr(self.model, self.fit_method)(X, y) + + def finetune(self, X: np.ndarray, y: np.ndarray): + getattr(self.model, self.finetune_method)(X, y) diff --git a/tests/test_reuse/test_averaging.py b/tests/test_reuse/test_averaging.py new file mode 100644 index 0000000..de4dde5 --- /dev/null +++ b/tests/test_reuse/test_averaging.py @@ -0,0 +1,43 @@ +import os +import json +import string +import random +import torch +import unittest +import tempfile +import numpy as np + +from learnware.specification import RKMETableSpecification, HeteroMapTableSpecification +from learnware.specification import generate_stat_spec +from learnware.market.heterogeneous.organizer import HeteroMap + +class TestAveragingReuse(unittest.TestCase): + + def setUp(self): + self.hetero_map = HeteroMap() + + def _test_hetero_spec(self, X): + rkme: RKMETableSpecification = generate_stat_spec(type="table", X=X) + hetero_spec = self.hetero_map.hetero_mapping(rkme_spec=rkme, features=dict()) + with tempfile.TemporaryDirectory(prefix="learnware_") as tempdir: + rkme_path = os.path.join(tempdir, "rkme.json") + hetero_spec.save(rkme_path) + + with open(rkme_path, "r") as f: + data = json.load(f) + assert data["type"] == "HeteroMapTableSpecification" + + rkme2 = HeteroMapTableSpecification() + rkme2.load(rkme_path) + assert rkme2.type == "HeteroMapTableSpecification" + + + def test_hetero_rkme(self): + self._test_hetero_spec(np.random.uniform(-10000, 10000, size=(5000, 200))) + self._test_hetero_spec(np.random.uniform(-10000, 10000, size=(10000, 100))) + self._test_hetero_spec(np.random.uniform(-10000, 10000, size=(5, 20))) + self._test_hetero_spec(np.random.uniform(-10000, 10000, size=(1, 50))) + self._test_hetero_spec(np.random.uniform(-10000, 10000, size=(100, 150))) + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_workflow/learnware_example/example_init.py b/tests/test_workflow/learnware_example/example_init.py index 47d3708..16188b3 100644 --- a/tests/test_workflow/learnware_example/example_init.py +++ b/tests/test_workflow/learnware_example/example_init.py @@ -8,7 +8,7 @@ class SVM(BaseModel): def __init__(self): super(SVM, self).__init__(input_shape=(64,), output_shape=(10,)) dir_path = os.path.dirname(os.path.abspath(__file__)) - self.model = joblib.load(os.path.join(dir_path, "svm.pkl")) + self.model = pickle.load(os.path.join(dir_path, "svm.pkl")) def fit(self, X: np.ndarray, y: np.ndarray): pass From 4c8609fabf27e742168bb4c04728727581dc0bc7 Mon Sep 17 00:00:00 2001 From: bxdd Date: Sun, 3 Dec 2023 13:48:26 +0800 Subject: [PATCH 09/43] [MNT] for temp save --- learnware/tests/templates/__init__.py | 3 ++- learnware/tests/templates/pickle_model.py | 2 ++ tests/test_workflow/learnware_example/example_init.py | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/learnware/tests/templates/__init__.py b/learnware/tests/templates/__init__.py index 8eba13c..874cb1b 100644 --- a/learnware/tests/templates/__init__.py +++ b/learnware/tests/templates/__init__.py @@ -70,7 +70,8 @@ class LearnwareTemplate: if model_template == "pickle": pickle_filepath = os.path.join(tempdir, model_config["kwargs"]["pickle_filepath"]) copyfile(kwargs["pickle_filepath"], pickle_filepath) - + + def generate_template_semantic_spec(self): pass \ No newline at end of file diff --git a/learnware/tests/templates/pickle_model.py b/learnware/tests/templates/pickle_model.py index 267f44f..f1c3d86 100644 --- a/learnware/tests/templates/pickle_model.py +++ b/learnware/tests/templates/pickle_model.py @@ -15,6 +15,8 @@ class PickleLoadedModel(BaseModel): finetune_method="finetune", ): super(PickleLoadedModel, self).__init__(input_shape=input_shape, output_shape=output_shape) + dir_path = os.path.dirname(os.path.abspath(__file__)) + self.pickle_filepath = os.path.join(pickle_filepath, dir_path) with open(pickle_filepath, "rb") as fd: self.model = pickle.load(fd) self.predict_method = predict_method diff --git a/tests/test_workflow/learnware_example/example_init.py b/tests/test_workflow/learnware_example/example_init.py index 16188b3..d66f2e3 100644 --- a/tests/test_workflow/learnware_example/example_init.py +++ b/tests/test_workflow/learnware_example/example_init.py @@ -1,5 +1,5 @@ import os -import joblib +import pickle import numpy as np from learnware.model import BaseModel From 76d24fc680de7cf3890e4842e86d37dcab6cdbcd Mon Sep 17 00:00:00 2001 From: bxdd Date: Mon, 4 Dec 2023 14:45:22 +0800 Subject: [PATCH 10/43] [MNT] for temp save --- learnware/tests/templates/__init__.py | 35 +++++++++++++++++++-------- learnware/utils/__init__.py | 2 +- learnware/utils/file.py | 13 +++++++++- 3 files changed, 38 insertions(+), 12 deletions(-) diff --git a/learnware/tests/templates/__init__.py b/learnware/tests/templates/__init__.py index 874cb1b..2d81821 100644 --- a/learnware/tests/templates/__init__.py +++ b/learnware/tests/templates/__init__.py @@ -1,17 +1,32 @@ import os import tempfile +from dataclasses import dataclass, field from shutil import copyfile from typing import List, Tuple, Union, Optional -from ...utils import save_dict_to_yaml +from ...utils import save_dict_to_yaml, convert_folder_to_zipfile from ...config import C -class LearnwareTemplate: + +@dataclass +class ModelTemplate: + class_name: str = field(repr=False) + template_path: str = field(repr=False) + model_kwargs: dict +@dataclass +class PickleModelTemplate(ModelTemplate): + pickle_filepath: str + def __post_init__(self): + self.class_name = "PickleLoadedModel" + self.template_path = os.path.join(C.package_path, "tests", "templates", "pickle_model.py") + +class TestTemplates: def __init__(self): self.model_templates = { "pickle": { "class_name": 'PickleLoadedModel', - "template_path": os.path.join(C.package_path, "tests", "templates", "pickle_model.py") + "template_path": os.path.join(C.package_path, "tests", "templates", "pickle_model.py"), + "parameters": {"pickle_filepath"}, } } @@ -47,10 +62,10 @@ class LearnwareTemplate: def generate_learnware_zipfile( self, learnware_zippath: str, - model_template: str = "pickle", - model_kwargs: Optional[dict] = None, + model_template: Union[ModelTemplate, PickleModelTemplate], stat_spec_config: Optional[List[dict]] = None, requirements: Optional[List[Union[Tuple[str, str, str], str]]] = None, + pickle_filepath: Optional[str] = None, **kwargs, ): with tempfile.TemporaryDirectory(suffix="learnware_template") as tempdir: @@ -58,20 +73,20 @@ class LearnwareTemplate: self.generate_requirements(requirement_filepath, requirements) model_filepath = os.path.join(tempdir, "__init__.py") - copyfile(self.model_templates[model_template]["template_path"], model_filepath) + copyfile(model_template.template_path, model_filepath) learnware_yaml_filepath = os.path.join(tempdir, "requirements.txt") model_config = { - "class_name": self.model_templates[model_template]["class_name"], - "kwargs": {} if model_kwargs is None else model_kwargs + "class_name": model_template.class_name, + "kwargs": {} if model_template.model_kwargs is None else model_kwargs } self.generate_learnware_yaml(learnware_yaml_filepath, model_config, stat_spec_config) if model_template == "pickle": pickle_filepath = os.path.join(tempdir, model_config["kwargs"]["pickle_filepath"]) copyfile(kwargs["pickle_filepath"], pickle_filepath) - - + + def generate_template_semantic_spec(self): pass \ No newline at end of file diff --git a/learnware/utils/__init__.py b/learnware/utils/__init__.py index 5357aaf..b43d763 100644 --- a/learnware/utils/__init__.py +++ b/learnware/utils/__init__.py @@ -3,7 +3,7 @@ import zipfile from .import_utils import is_torch_available from .module import get_module_by_module_path -from .file import read_yaml_to_dict, save_dict_to_yaml +from .file import read_yaml_to_dict, save_dict_to_yaml, convert_folder_to_zipfile from .gpu import setup_seed, choose_device, allocate_cuda_idx from ..config import get_platform, SystemType diff --git a/learnware/utils/file.py b/learnware/utils/file.py index 27ba5f5..c0b4f77 100644 --- a/learnware/utils/file.py +++ b/learnware/utils/file.py @@ -1,5 +1,6 @@ +import os import yaml - +import zipfile def save_dict_to_yaml(dict_value: dict, save_path: str): """save dict object into yaml file""" @@ -12,3 +13,13 @@ def read_yaml_to_dict(yaml_path: str): with open(yaml_path, "r") as file: dict_value = yaml.load(file.read(), Loader=yaml.FullLoader) return dict_value + +def convert_folder_to_zipfile(folder_path, zip_path): + with zipfile.ZipFile(zip_path, "w") as zip_obj: + for foldername, subfolders, filenames in os.walk(folder_path): + for filename in filenames: + file_path = os.path.join(foldername, filename) + zip_info = zipfile.ZipInfo(filename) + zip_info.compress_type = zipfile.ZIP_STORED + with open(file_path, "rb") as file: + zip_obj.writestr(zip_info, file.read()) From 9e05be069a5f99618b19ebb6625912782658b2c8 Mon Sep 17 00:00:00 2001 From: bxdd Date: Mon, 4 Dec 2023 19:09:01 +0800 Subject: [PATCH 11/43] [MNT] finish generate_learnware_zipfile --- learnware/client/utils.py | 2 -- learnware/tests/templates/__init__.py | 33 ++++++++++------------- learnware/tests/templates/pickle_model.py | 2 +- 3 files changed, 15 insertions(+), 22 deletions(-) diff --git a/learnware/client/utils.py b/learnware/client/utils.py index 132c9a0..b515c30 100644 --- a/learnware/client/utils.py +++ b/learnware/client/utils.py @@ -28,8 +28,6 @@ def system_execute(args, timeout=None, env=None, stdout=subprocess.DEVNULL, stde def remove_enviroment(conda_env): system_execute(args=["conda", "env", "remove", "-n", f"{conda_env}"]) - logger.info(f"The learnware conda env [{conda_env}] is removed.") - def install_environment(learnware_dirpath, conda_env): """Install environment of a learnware diff --git a/learnware/tests/templates/__init__.py b/learnware/tests/templates/__init__.py index 2d81821..4ba536f 100644 --- a/learnware/tests/templates/__init__.py +++ b/learnware/tests/templates/__init__.py @@ -10,25 +10,22 @@ from ...config import C @dataclass class ModelTemplate: - class_name: str = field(repr=False) - template_path: str = field(repr=False) - model_kwargs: dict + class_name: str = field(init=False) + template_path: str = field(init=False) + model_kwargs: dict = field(init=False) @dataclass class PickleModelTemplate(ModelTemplate): pickle_filepath: str + model_kwargs: dict = field(init=True, default_factory=dict) def __post_init__(self): self.class_name = "PickleLoadedModel" self.template_path = os.path.join(C.package_path, "tests", "templates", "pickle_model.py") class TestTemplates: def __init__(self): - self.model_templates = { - "pickle": { - "class_name": 'PickleLoadedModel', - "template_path": os.path.join(C.package_path, "tests", "templates", "pickle_model.py"), - "parameters": {"pickle_filepath"}, - } - } + self.model_templates = [ + PickleModelTemplate + ] def generate_requirements(self, filepath, requirements: Optional[List[Union[Tuple[str, str, str], str]]] = None): requirements = [] if requirements is None else requirements @@ -57,16 +54,13 @@ class TestTemplates: save_dict_to_yaml(learnware_config, filepath) - - def generate_learnware_zipfile( self, learnware_zippath: str, - model_template: Union[ModelTemplate, PickleModelTemplate], + model_template: ModelTemplate, stat_spec_config: Optional[List[dict]] = None, requirements: Optional[List[Union[Tuple[str, str, str], str]]] = None, pickle_filepath: Optional[str] = None, - **kwargs, ): with tempfile.TemporaryDirectory(suffix="learnware_template") as tempdir: requirement_filepath = os.path.join(tempdir, "requirements.txt") @@ -78,15 +72,16 @@ class TestTemplates: learnware_yaml_filepath = os.path.join(tempdir, "requirements.txt") model_config = { "class_name": model_template.class_name, - "kwargs": {} if model_template.model_kwargs is None else model_kwargs + "kwargs": model_template.model_kwargs, } self.generate_learnware_yaml(learnware_yaml_filepath, model_config, stat_spec_config) - if model_template == "pickle": - pickle_filepath = os.path.join(tempdir, model_config["kwargs"]["pickle_filepath"]) - copyfile(kwargs["pickle_filepath"], pickle_filepath) + if isinstance(model_template, PickleModelTemplate): + pickle_filepath = os.path.join(tempdir, model_template.model_kwargs["pickle_filename"]) + copyfile(model_template.pickle_filepath, pickle_filepath) - + convert_folder_to_zipfile(tempdir, learnware_zippath) + def generate_template_semantic_spec(self): pass \ No newline at end of file diff --git a/learnware/tests/templates/pickle_model.py b/learnware/tests/templates/pickle_model.py index f1c3d86..b4cb095 100644 --- a/learnware/tests/templates/pickle_model.py +++ b/learnware/tests/templates/pickle_model.py @@ -9,7 +9,7 @@ class PickleLoadedModel(BaseModel): self, input_shape, output_shape, - pickle_filepath, + pickle_filename, predict_method="predict", fit_method="fit", finetune_method="finetune", From 510608b9676dc227ecd0f63feaf9e0f39b88ead4 Mon Sep 17 00:00:00 2001 From: bxdd Date: Mon, 4 Dec 2023 19:24:34 +0800 Subject: [PATCH 12/43] [MNT] update generate default semantic spec method in LearnwareClient --- learnware/client/learnware_client.py | 16 +++++++++++----- learnware/specification/module.py | 7 +++++-- learnware/tests/__init__.py | 1 - learnware/tests/module.py | 4 ---- learnware/tests/templates/__init__.py | 4 ---- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/learnware/client/learnware_client.py b/learnware/client/learnware_client.py index f16cfa0..dbfea51 100644 --- a/learnware/client/learnware_client.py +++ b/learnware/client/learnware_client.py @@ -15,10 +15,11 @@ from ..config import C from .. import learnware from .container import LearnwaresContainer from ..market import BaseChecker, EasySemanticChecker, EasyStatChecker +from ..specification import generate_semantic_spec from ..logger import get_module_logger from ..learnware import get_learnware_from_dirpath from ..market import BaseUserInfo -from ..tests import get_semantic_specification + CHUNK_SIZE = 1024 * 1024 logger = get_module_logger(module_name="LearnwareClient") @@ -398,10 +399,15 @@ class LearnwareClient: @staticmethod def check_learnware(learnware_zip_path, semantic_specification=None): - semantic_specification = ( - get_semantic_specification() if semantic_specification is None else semantic_specification - ) - + semantic_specification = generate_semantic_spec( + name="test", + description="test", + data_type="Text", + task_type="Segmentation", + scenarios="Financial", + license="Apache-2.0", + ) if semantic_specification is None else semantic_specification + check_status, message = LearnwareClient._check_semantic_specification(semantic_specification) assert check_status, f"Semantic specification check failed due to {message}!" diff --git a/learnware/specification/module.py b/learnware/specification/module.py index 10b4fdc..e1c22ea 100644 --- a/learnware/specification/module.py +++ b/learnware/specification/module.py @@ -233,7 +233,10 @@ def generate_semantic_spec( "Type": "String", "Values": description if description is not None else "", } - semantic_specification["Input"] = {} if input_description is None else input_description - semantic_specification["Output"] = {} if output_description is None else output_description + if input_description is not None: + semantic_specification["Input"] = input_description + + if output_description is not None: + semantic_specification["Output"] = output_description return semantic_specification diff --git a/learnware/tests/__init__.py b/learnware/tests/__init__.py index 5019465..e8ee37e 100644 --- a/learnware/tests/__init__.py +++ b/learnware/tests/__init__.py @@ -1,2 +1 @@ -from .module import get_semantic_specification from .utils import parametrize \ No newline at end of file diff --git a/learnware/tests/module.py b/learnware/tests/module.py index 9556bdf..2f7e345 100644 --- a/learnware/tests/module.py +++ b/learnware/tests/module.py @@ -9,7 +9,3 @@ def get_semantic_specification(): semantic_specification["Description"] = {"Type": "String", "Values": "test"} return semantic_specification - - - -def get_requirements_file() \ No newline at end of file diff --git a/learnware/tests/templates/__init__.py b/learnware/tests/templates/__init__.py index 4ba536f..37b1148 100644 --- a/learnware/tests/templates/__init__.py +++ b/learnware/tests/templates/__init__.py @@ -81,7 +81,3 @@ class TestTemplates: copyfile(model_template.pickle_filepath, pickle_filepath) convert_folder_to_zipfile(tempdir, learnware_zippath) - - - def generate_template_semantic_spec(self): - pass \ No newline at end of file From 42054cfc5ce1752ac3dd0b71050c7bd136e3e7b3 Mon Sep 17 00:00:00 2001 From: bxdd Date: Mon, 4 Dec 2023 19:36:07 +0800 Subject: [PATCH 13/43] [ENH] add GetData and LearnwareBenchmark clas --- learnware/client/learnware_client.py | 2 +- learnware/tests/benchmark.py | 12 ++++++++++++ learnware/tests/data.py | 3 +++ learnware/tests/module.py | 11 ----------- learnware/tests/templates/__init__.py | 2 +- 5 files changed, 17 insertions(+), 13 deletions(-) create mode 100644 learnware/tests/benchmark.py delete mode 100644 learnware/tests/module.py diff --git a/learnware/client/learnware_client.py b/learnware/client/learnware_client.py index dbfea51..07d4c6b 100644 --- a/learnware/client/learnware_client.py +++ b/learnware/client/learnware_client.py @@ -406,7 +406,7 @@ class LearnwareClient: task_type="Segmentation", scenarios="Financial", license="Apache-2.0", - ) if semantic_specification is None else semantic_specification + ) if semantic_specification is None else semantic_specification check_status, message = LearnwareClient._check_semantic_specification(semantic_specification) assert check_status, f"Semantic specification check failed due to {message}!" diff --git a/learnware/tests/benchmark.py b/learnware/tests/benchmark.py new file mode 100644 index 0000000..c4ed049 --- /dev/null +++ b/learnware/tests/benchmark.py @@ -0,0 +1,12 @@ +from dataclasses import dataclass, field +from typing import Optional, List +from ..learnware import Learnware +@dataclass +class BenchmarkConfig: + datasert_url: str + learnware_ids: List[str] + userdata_url: Optional[str] = None + + +class LearnwareBenchmark: + pass diff --git a/learnware/tests/data.py b/learnware/tests/data.py index e69de29..422392c 100644 --- a/learnware/tests/data.py +++ b/learnware/tests/data.py @@ -0,0 +1,3 @@ + +class GetData: + pass \ No newline at end of file diff --git a/learnware/tests/module.py b/learnware/tests/module.py deleted file mode 100644 index 2f7e345..0000000 --- a/learnware/tests/module.py +++ /dev/null @@ -1,11 +0,0 @@ -def get_semantic_specification(): - semantic_specification = dict() - semantic_specification["Data"] = {"Type": "Class", "Values": ["Text"]} - semantic_specification["Task"] = {"Type": "Class", "Values": ["Segmentation"]} - semantic_specification["Library"] = {"Type": "Class", "Values": ["Scikit-learn"]} - semantic_specification["Scenario"] = {"Type": "Tag", "Values": ["Financial"]} - semantic_specification["License"] = {"Type": "Class", "Values": ["Apache-2.0"]} - semantic_specification["Name"] = {"Type": "String", "Values": "test"} - semantic_specification["Description"] = {"Type": "String", "Values": "test"} - return semantic_specification - diff --git a/learnware/tests/templates/__init__.py b/learnware/tests/templates/__init__.py index 37b1148..8182c2a 100644 --- a/learnware/tests/templates/__init__.py +++ b/learnware/tests/templates/__init__.py @@ -21,7 +21,7 @@ class PickleModelTemplate(ModelTemplate): self.class_name = "PickleLoadedModel" self.template_path = os.path.join(C.package_path, "tests", "templates", "pickle_model.py") -class TestTemplates: +class LearnwareTemplate: def __init__(self): self.model_templates = [ PickleModelTemplate From d672f88809bf67d14bb2f613a1a73f1a061121ae Mon Sep 17 00:00:00 2001 From: bxdd Date: Mon, 4 Dec 2023 19:36:42 +0800 Subject: [PATCH 14/43] [FIX] fix pickle model bugs --- learnware/tests/templates/pickle_model.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/learnware/tests/templates/pickle_model.py b/learnware/tests/templates/pickle_model.py index b4cb095..2039aa6 100644 --- a/learnware/tests/templates/pickle_model.py +++ b/learnware/tests/templates/pickle_model.py @@ -16,8 +16,8 @@ class PickleLoadedModel(BaseModel): ): super(PickleLoadedModel, self).__init__(input_shape=input_shape, output_shape=output_shape) dir_path = os.path.dirname(os.path.abspath(__file__)) - self.pickle_filepath = os.path.join(pickle_filepath, dir_path) - with open(pickle_filepath, "rb") as fd: + self.pickle_filepath = os.path.join(pickle_filename, dir_path) + with open(pickle_filename, "rb") as fd: self.model = pickle.load(fd) self.predict_method = predict_method self.fit_method = fit_method From 70953f8cc36def9e8801449a3e896de6b741dda7 Mon Sep 17 00:00:00 2001 From: liuht-0807 Date: Mon, 4 Dec 2023 20:41:15 +0800 Subject: [PATCH 15/43] [FIX] fix exceed GPU memory generating hetero_spec --- .../market/heterogeneous/organizer/hetero_map/__init__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/learnware/market/heterogeneous/organizer/hetero_map/__init__.py b/learnware/market/heterogeneous/organizer/hetero_map/__init__.py index 2e31195..6735879 100644 --- a/learnware/market/heterogeneous/organizer/hetero_map/__init__.py +++ b/learnware/market/heterogeneous/organizer/hetero_map/__init__.py @@ -11,6 +11,8 @@ from .....specification import HeteroMapTableSpecification, RKMETableSpecificati from .feature_extractor import CLSToken, FeatureProcessor, FeatureTokenizer from .trainer import Trainer, TransTabCollatorForCL +from loguru import logger + class HeteroMap(nn.Module): """ @@ -127,6 +129,7 @@ class HeteroMap(nn.Module): self.base_temperature = base_temperature self.num_partition = num_partition self.overlap_ratio = overlap_ratio + self.max_process_size = 20480 self.to(device) def to(self, device: Union[str, torch.device]): @@ -306,6 +309,10 @@ class HeteroMap(nn.Module): """ self.eval() output_feas_list = [] + + if eval_batch_size * x_test.shape[1] > self.max_process_size: + eval_batch_size = int(self.max_process_size / x_test.shape[1]) + for i in range(0, len(x_test), eval_batch_size): bs_x_test = x_test.iloc[i : i + eval_batch_size] with torch.no_grad(): From d5b45f5d9fcb2742242ce5df5c93cdb217ca0ad9 Mon Sep 17 00:00:00 2001 From: liuht-0807 Date: Mon, 4 Dec 2023 20:44:29 +0800 Subject: [PATCH 16/43] [FIX] delete loguru --- .../market/heterogeneous/organizer/hetero_map/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/learnware/market/heterogeneous/organizer/hetero_map/__init__.py b/learnware/market/heterogeneous/organizer/hetero_map/__init__.py index 6735879..4ebb0a8 100644 --- a/learnware/market/heterogeneous/organizer/hetero_map/__init__.py +++ b/learnware/market/heterogeneous/organizer/hetero_map/__init__.py @@ -9,9 +9,7 @@ from torch import Tensor, nn from .....utils import allocate_cuda_idx, choose_device from .....specification import HeteroMapTableSpecification, RKMETableSpecification from .feature_extractor import CLSToken, FeatureProcessor, FeatureTokenizer -from .trainer import Trainer, TransTabCollatorForCL - -from loguru import logger +from .trainer import TransTabCollatorForCL class HeteroMap(nn.Module): From 2cd0fae48025edc227d3e1196ef21456088ad71a Mon Sep 17 00:00:00 2001 From: bxdd Date: Mon, 4 Dec 2023 21:58:46 +0800 Subject: [PATCH 17/43] [MNT] now the refactored workflow test run well --- learnware/tests/templates/__init__.py | 52 +++-- learnware/tests/templates/pickle_model.py | 6 +- .../test_workflow/learnware_example/README.md | 10 - .../learnware_example/environment.yaml | 27 --- .../learnware_example/example.yaml | 8 - .../learnware_example/example_init.py | 20 -- tests/test_workflow/test_workflow.py | 210 ++++++++---------- 7 files changed, 128 insertions(+), 205 deletions(-) delete mode 100644 tests/test_workflow/learnware_example/README.md delete mode 100644 tests/test_workflow/learnware_example/environment.yaml delete mode 100644 tests/test_workflow/learnware_example/example.yaml delete mode 100644 tests/test_workflow/learnware_example/example_init.py diff --git a/learnware/tests/templates/__init__.py b/learnware/tests/templates/__init__.py index 8182c2a..fcad43f 100644 --- a/learnware/tests/templates/__init__.py +++ b/learnware/tests/templates/__init__.py @@ -15,19 +15,29 @@ class ModelTemplate: model_kwargs: dict = field(init=False) @dataclass class PickleModelTemplate(ModelTemplate): + model_kwargs: dict pickle_filepath: str - model_kwargs: dict = field(init=True, default_factory=dict) def __post_init__(self): self.class_name = "PickleLoadedModel" self.template_path = os.path.join(C.package_path, "tests", "templates", "pickle_model.py") + default_model_kwargs = { + "predict_method": "predict", + "fit_method": "fit", + "finetune_method": "finetune", + "pickle_filename": "model.pkl", + } + default_model_kwargs.update(self.model_kwargs) + self.model_kwargs = default_model_kwargs + +@dataclass +class StatSpecTemplate: + filepath: str + type: str = field(default="RKMETableSpecification") class LearnwareTemplate: - def __init__(self): - self.model_templates = [ - PickleModelTemplate - ] - - def generate_requirements(self, filepath, requirements: Optional[List[Union[Tuple[str, str, str], str]]] = None): + + @staticmethod + def generate_requirements(filepath, requirements: Optional[List[Union[Tuple[str, str, str], str]]] = None): requirements = [] if requirements is None else requirements operators = {"==", "~=", ">=", "<=", ">", "<"} requirements_str = "" @@ -44,8 +54,9 @@ class LearnwareTemplate: with open(filepath, "w") as fdout: fdout.write(requirements_str) - - def generate_learnware_yaml(self, filepath, model_config: Optional[dict] = None, stat_spec_config: Optional[List[dict]] = None): + + @staticmethod + def generate_learnware_yaml(filepath, model_config: Optional[dict] = None, stat_spec_config: Optional[List[dict]] = None): learnware_config = {} if model_config is not None: learnware_config["model"] = model_config @@ -53,29 +64,36 @@ class LearnwareTemplate: learnware_config["stat_specifications"] = stat_spec_config save_dict_to_yaml(learnware_config, filepath) - + + @staticmethod def generate_learnware_zipfile( - self, learnware_zippath: str, model_template: ModelTemplate, - stat_spec_config: Optional[List[dict]] = None, + stat_spec_template: StatSpecTemplate, requirements: Optional[List[Union[Tuple[str, str, str], str]]] = None, - pickle_filepath: Optional[str] = None, ): with tempfile.TemporaryDirectory(suffix="learnware_template") as tempdir: requirement_filepath = os.path.join(tempdir, "requirements.txt") - self.generate_requirements(requirement_filepath, requirements) + LearnwareTemplate.generate_requirements(requirement_filepath, requirements) model_filepath = os.path.join(tempdir, "__init__.py") copyfile(model_template.template_path, model_filepath) - learnware_yaml_filepath = os.path.join(tempdir, "requirements.txt") + learnware_yaml_filepath = os.path.join(tempdir, "learnware.yaml") model_config = { "class_name": model_template.class_name, "kwargs": model_template.model_kwargs, } - self.generate_learnware_yaml(learnware_yaml_filepath, model_config, stat_spec_config) - + + stat_spec_config = { + "module_path": "learnware.specification", + "class_name": stat_spec_template.type, + "file_name": "stat_spec.json", + "kwargs": {} + } + copyfile(stat_spec_template.filepath, os.path.join(tempdir, stat_spec_config["file_name"])) + LearnwareTemplate.generate_learnware_yaml(learnware_yaml_filepath, model_config, stat_spec_config=[stat_spec_config]) + if isinstance(model_template, PickleModelTemplate): pickle_filepath = os.path.join(tempdir, model_template.model_kwargs["pickle_filename"]) copyfile(model_template.pickle_filepath, pickle_filepath) diff --git a/learnware/tests/templates/pickle_model.py b/learnware/tests/templates/pickle_model.py index 2039aa6..f708ad4 100644 --- a/learnware/tests/templates/pickle_model.py +++ b/learnware/tests/templates/pickle_model.py @@ -9,15 +9,15 @@ class PickleLoadedModel(BaseModel): self, input_shape, output_shape, - pickle_filename, predict_method="predict", fit_method="fit", finetune_method="finetune", + pickle_filename="model.pkl", ): super(PickleLoadedModel, self).__init__(input_shape=input_shape, output_shape=output_shape) dir_path = os.path.dirname(os.path.abspath(__file__)) - self.pickle_filepath = os.path.join(pickle_filename, dir_path) - with open(pickle_filename, "rb") as fd: + self.pickle_filepath = os.path.join(dir_path, pickle_filename) + with open(self.pickle_filepath, "rb") as fd: self.model = pickle.load(fd) self.predict_method = predict_method self.fit_method = fit_method diff --git a/tests/test_workflow/learnware_example/README.md b/tests/test_workflow/learnware_example/README.md deleted file mode 100644 index 51aac5a..0000000 --- a/tests/test_workflow/learnware_example/README.md +++ /dev/null @@ -1,10 +0,0 @@ -## How to Generate Environment Yaml - -* create env config for conda: -```shell -conda env export | grep -v "^prefix: " > environment.yml -``` -* recover env from config -``` -conda env create -f environment.yml -``` \ No newline at end of file diff --git a/tests/test_workflow/learnware_example/environment.yaml b/tests/test_workflow/learnware_example/environment.yaml deleted file mode 100644 index 2923bdb..0000000 --- a/tests/test_workflow/learnware_example/environment.yaml +++ /dev/null @@ -1,27 +0,0 @@ -name: learnware_example_env -channels: - - defaults -dependencies: - - _libgcc_mutex=0.1=main - - _openmp_mutex=5.1=1_gnu - - ca-certificates=2023.01.10=h06a4308_0 - - ld_impl_linux-64=2.38=h1181459_1 - - libffi=3.4.2=h6a678d5_6 - - libgcc-ng=11.2.0=h1234567_1 - - libgomp=11.2.0=h1234567_1 - - libstdcxx-ng=11.2.0=h1234567_1 - - ncurses=6.4=h6a678d5_0 - - openssl=1.1.1t=h7f8727e_0 - - pip=23.0.1=py38h06a4308_0 - - python=3.8.16=h7a1cb2a_3 - - readline=8.2=h5eee18b_0 - - setuptools=66.0.0=py38h06a4308_0 - - sqlite=3.41.2=h5eee18b_0 - - tk=8.6.12=h1ccaba5_0 - - wheel=0.38.4=py38h06a4308_0 - - xz=5.2.10=h5eee18b_1 - - zlib=1.2.13=h5eee18b_0 - - pip: - - joblib==1.2.0 - - learnware==0.0.1.99 - - numpy==1.19.5 diff --git a/tests/test_workflow/learnware_example/example.yaml b/tests/test_workflow/learnware_example/example.yaml deleted file mode 100644 index 32aa52e..0000000 --- a/tests/test_workflow/learnware_example/example.yaml +++ /dev/null @@ -1,8 +0,0 @@ -model: - class_name: SVM - kwargs: {} -stat_specifications: - - module_path: learnware.specification - class_name: RKMETableSpecification - file_name: svm.json - kwargs: {} \ No newline at end of file diff --git a/tests/test_workflow/learnware_example/example_init.py b/tests/test_workflow/learnware_example/example_init.py deleted file mode 100644 index d66f2e3..0000000 --- a/tests/test_workflow/learnware_example/example_init.py +++ /dev/null @@ -1,20 +0,0 @@ -import os -import pickle -import numpy as np -from learnware.model import BaseModel - - -class SVM(BaseModel): - def __init__(self): - super(SVM, self).__init__(input_shape=(64,), output_shape=(10,)) - dir_path = os.path.dirname(os.path.abspath(__file__)) - self.model = pickle.load(os.path.join(dir_path, "svm.pkl")) - - def fit(self, X: np.ndarray, y: np.ndarray): - pass - - def predict(self, X: np.ndarray) -> np.ndarray: - return self.model.predict_proba(X) - - def finetune(self, X: np.ndarray, y: np.ndarray): - pass diff --git a/tests/test_workflow/test_workflow.py b/tests/test_workflow/test_workflow.py index 83f39ba..b0aa462 100644 --- a/tests/test_workflow/test_workflow.py +++ b/tests/test_workflow/test_workflow.py @@ -1,37 +1,38 @@ -import sys import unittest import os -import copy -import joblib +import logging +import tempfile +import pickle import zipfile import numpy as np from sklearn import svm from sklearn.datasets import load_digits from sklearn.model_selection import train_test_split -from shutil import copyfile, rmtree import learnware +learnware.init(logging_level=logging.WARNING) + from learnware.market import instantiate_learnware_market, BaseUserInfo -from learnware.specification import RKMETableSpecification, generate_rkme_table_spec +from learnware.specification import RKMETableSpecification, generate_rkme_table_spec, generate_semantic_spec from learnware.reuse import JobSelectorReuser, AveragingReuser, EnsemblePruningReuser, FeatureAugmentReuser +from learnware.tests.templates import LearnwareTemplate, PickleModelTemplate, StatSpecTemplate curr_root = os.path.dirname(os.path.abspath(__file__)) -user_semantic = { - "Data": {"Values": ["Table"], "Type": "Class"}, - "Task": { - "Values": ["Classification"], - "Type": "Class", - }, - "Library": {"Values": ["Scikit-learn"], "Type": "Class"}, - "Scenario": {"Values": ["Education"], "Type": "Tag"}, - "Description": {"Values": "", "Type": "String"}, - "Name": {"Values": "", "Type": "String"}, - "License": {"Values": ["MIT"], "Type": "Class"}, -} - - class TestWorkflow(unittest.TestCase): + + universal_semantic_config = { + "data_type": "Table", + "task_type": "Classification", + "library_type": "Scikit-learn", + "scenarios": "Education", + "license": "MIT", + } + + @classmethod + def setUpClass(cls): + pass + def _init_learnware_market(self): """initialize learnware market""" easy_market = instantiate_learnware_market(market_id="sklearn_digits_easy", name="easy", rebuild=True) @@ -42,45 +43,29 @@ class TestWorkflow(unittest.TestCase): X, y = load_digits(return_X_y=True) for i in range(learnware_num): - dir_path = os.path.join(curr_root, "learnware_pool", "svm_%d" % (i)) - os.makedirs(dir_path, exist_ok=True) - + learnware_pool_dirpath = os.path.join(curr_root, "learnware_pool") + os.makedirs(learnware_pool_dirpath, exist_ok=True) + learnware_zippath = os.path.join(learnware_pool_dirpath, "svm_%d.zip" % (i)) + print("Preparing Learnware: %d" % (i)) - data_X, _, data_y, _ = train_test_split(X, y, test_size=0.3, shuffle=True) clf = svm.SVC(kernel="linear", probability=True) clf.fit(data_X, data_y) - - joblib.dump(clf, os.path.join(dir_path, "svm.pkl")) + pickle_filepath = os.path.join(learnware_pool_dirpath, "model.pkl") + with open(pickle_filepath, "wb") as fout: + pickle.dump(clf, fout) spec = generate_rkme_table_spec(X=data_X, gamma=0.1, cuda_idx=0) - spec.save(os.path.join(dir_path, "svm.json")) - - init_file = os.path.join(dir_path, "__init__.py") - copyfile( - os.path.join(curr_root, "learnware_example/example_init.py"), init_file - ) # cp example_init.py init_file - - yaml_file = os.path.join(dir_path, "learnware.yaml") - copyfile(os.path.join(curr_root, "learnware_example/example.yaml"), yaml_file) # cp example.yaml yaml_file - - env_file = os.path.join(dir_path, "environment.yaml") - copyfile(os.path.join(curr_root, "learnware_example/environment.yaml"), env_file) - - zip_file = dir_path + ".zip" - # zip -q -r -j zip_file dir_path - with zipfile.ZipFile(zip_file, "w") as zip_obj: - for foldername, subfolders, filenames in os.walk(dir_path): - for filename in filenames: - file_path = os.path.join(foldername, filename) - zip_info = zipfile.ZipInfo(filename) - zip_info.compress_type = zipfile.ZIP_STORED - with open(file_path, "rb") as file: - zip_obj.writestr(zip_info, file.read()) - - rmtree(dir_path) # rm -r dir_path - - self.zip_path_list.append(zip_file) + spec_filepath = os.path.join(learnware_pool_dirpath, "stat_spec.json") + spec.save(spec_filepath) + + LearnwareTemplate.generate_learnware_zipfile( + learnware_zippath=learnware_zippath, + model_template=PickleModelTemplate(pickle_filepath=pickle_filepath, model_kwargs={"input_shape":(64,), "output_shape": (10,), "predict_method": "predict_proba"}), + stat_spec_template=StatSpecTemplate(filepath=spec_filepath, type="RKMETableSpecification") + ) + + self.zip_path_list.append(learnware_zippath) def test_upload_delete_learnware(self, learnware_num=5, delete=True): easy_market = self._init_learnware_market() @@ -91,20 +76,22 @@ class TestWorkflow(unittest.TestCase): assert len(easy_market) == 0, f"The market should be empty!" for idx, zip_path in enumerate(self.zip_path_list): - semantic_spec = copy.deepcopy(user_semantic) - semantic_spec["Name"]["Values"] = "learnware_%d" % (idx) - semantic_spec["Description"]["Values"] = "test_learnware_number_%d" % (idx) - semantic_spec["Input"] = { - "Dimension": 64, - "Description": { - f"{i}": f"The value in the grid {i // 8}{i % 8} of the image of hand-written digit." - for i in range(64) + semantic_spec = generate_semantic_spec( + name=f"learnware_{idx}", + description=f"test_learnware_number_{idx}", + input_description={ + "Dimension": 64, + "Description": { + f"{i}": f"The value in the grid {i // 8}{i % 8} of the image of hand-written digit." + for i in range(64) + }, + }, + output_description={ + "Dimension": 10, + "Description": {f"{i}": "The probability for each digit for 0 to 9." for i in range(10)}, }, - } - semantic_spec["Output"] = { - "Dimension": 10, - "Description": {f"{i}": "The probability for each digit for 0 to 9." for i in range(10)}, - } + **self.universal_semantic_config + ) easy_market.add_learnware(zip_path, semantic_spec) print("Total Item:", len(easy_market)) @@ -129,70 +116,52 @@ class TestWorkflow(unittest.TestCase): easy_market = self.test_upload_delete_learnware(learnware_num, delete=False) print("Total Item:", len(easy_market)) assert len(easy_market) == self.learnware_num, f"The number of learnwares must be {self.learnware_num}!" - test_folder = os.path.join(curr_root, "test_semantics") - - # unzip -o -q zip_path -d unzip_dir - if os.path.exists(test_folder): - rmtree(test_folder) - os.makedirs(test_folder, exist_ok=True) - - with zipfile.ZipFile(self.zip_path_list[0], "r") as zip_obj: - zip_obj.extractall(path=test_folder) - - semantic_spec = copy.deepcopy(user_semantic) - semantic_spec["Name"]["Values"] = f"learnware_{learnware_num - 1}" - semantic_spec["Description"]["Values"] = f"test_learnware_number_{learnware_num - 1}" - - user_info = BaseUserInfo(semantic_spec=semantic_spec) - search_result = easy_market.search_learnware(user_info) - single_result = search_result.get_single_results() - - print("User info:", user_info.get_semantic_spec()) - print(f"Search result:") - for search_item in single_result: - print( - "Choose learnware:", - search_item.learnware.id, - search_item.learnware.get_specification().get_semantic_spec(), + + with tempfile.TemporaryDirectory(prefix="learnware_test_workflow") as test_folder: + with zipfile.ZipFile(self.zip_path_list[0], "r") as zip_obj: + zip_obj.extractall(path=test_folder) + + semantic_spec = generate_semantic_spec( + name=f"learnware_{learnware_num - 1}", + description=f"test_learnware_number_{learnware_num - 1}", + **self.universal_semantic_config, ) + + user_info = BaseUserInfo(semantic_spec=semantic_spec) + search_result = easy_market.search_learnware(user_info) + single_result = search_result.get_single_results() - rmtree(test_folder) # rm -r test_folder - + print(f"Search result:") + for search_item in single_result: + print("Choose learnware:",search_item.learnware.id) + def test_stat_search(self, learnware_num=5): easy_market = self.test_upload_delete_learnware(learnware_num, delete=False) print("Total Item:", len(easy_market)) - test_folder = os.path.join(curr_root, "test_stat") + with tempfile.TemporaryDirectory(prefix="learnware_test_workflow") as test_folder: + for idx, zip_path in enumerate(self.zip_path_list): + with zipfile.ZipFile(zip_path, "r") as zip_obj: + zip_obj.extractall(path=test_folder) - for idx, zip_path in enumerate(self.zip_path_list): - unzip_dir = os.path.join(test_folder, f"{idx}") - - # unzip -o -q zip_path -d unzip_dir - if os.path.exists(unzip_dir): - rmtree(unzip_dir) - os.makedirs(unzip_dir, exist_ok=True) - with zipfile.ZipFile(zip_path, "r") as zip_obj: - zip_obj.extractall(path=unzip_dir) - - user_spec = RKMETableSpecification() - user_spec.load(os.path.join(unzip_dir, "svm.json")) - user_info = BaseUserInfo(semantic_spec=user_semantic, stat_info={"RKMETableSpecification": user_spec}) - search_results = easy_market.search_learnware(user_info) + user_spec = RKMETableSpecification() + user_spec.load(os.path.join(test_folder, "stat_spec.json")) + user_semantic = generate_semantic_spec(**self.universal_semantic_config) + user_info = BaseUserInfo(semantic_spec=user_semantic, stat_info={"RKMETableSpecification": user_spec}) + search_results = easy_market.search_learnware(user_info) - single_result = search_results.get_single_results() - multiple_result = search_results.get_multiple_results() - - assert len(single_result) >= 1, f"Statistical search failed!" - print(f"search result of user{idx}:") - for search_item in single_result: - print(f"score: {search_item.score}, learnware_id: {search_item.learnware.id}") + single_result = search_results.get_single_results() + multiple_result = search_results.get_multiple_results() - for mixture_item in multiple_result: - print(f"mixture_score: {mixture_item.score}\n") - mixture_id = " ".join([learnware.id for learnware in mixture_item.learnwares]) - print(f"mixture_learnware: {mixture_id}\n") + assert len(single_result) >= 1, f"Statistical search failed!" + print(f"search result of user{idx}:") + for search_item in single_result: + print(f"score: {search_item.score}, learnware_id: {search_item.learnware.id}") - rmtree(test_folder) # rm -r test_folder + for mixture_item in multiple_result: + print(f"mixture_score: {mixture_item.score}\n") + mixture_id = " ".join([learnware.id for learnware in mixture_item.learnwares]) + print(f"mixture_learnware: {mixture_id}\n") def test_learnware_reuse(self, learnware_num=5): easy_market = self.test_upload_delete_learnware(learnware_num, delete=False) @@ -202,6 +171,7 @@ class TestWorkflow(unittest.TestCase): train_X, data_X, train_y, data_y = train_test_split(X, y, test_size=0.3, shuffle=True) stat_spec = generate_rkme_table_spec(X=data_X, gamma=0.1, cuda_idx=0) + user_semantic = generate_semantic_spec(**self.universal_semantic_config) user_info = BaseUserInfo(semantic_spec=user_semantic, stat_info={"RKMETableSpecification": stat_spec}) search_results = easy_market.search_learnware(user_info) @@ -243,5 +213,5 @@ def suite(): if __name__ == "__main__": - runner = unittest.TextTestRunner() + runner = unittest.TextTestRunner(verbosity=2) runner.run(suite()) From a31fcd6c189176ea6e8fca66f0e60efb97b60d0a Mon Sep 17 00:00:00 2001 From: Gene Date: Mon, 4 Dec 2023 22:43:46 +0800 Subject: [PATCH 18/43] [MNT] change client search return --- learnware/client/learnware_client.py | 43 ++++++++++++++-------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/learnware/client/learnware_client.py b/learnware/client/learnware_client.py index 2895e5c..7a5eaed 100644 --- a/learnware/client/learnware_client.py +++ b/learnware/client/learnware_client.py @@ -216,7 +216,18 @@ class LearnwareClient: else: stat_spec = None - returns = [] + returns = { + "single": { + "learnware_ids": [], + "semantic_specifications": [], + "matching": [], + }, + "multiple": { + "learnware_ids": [], + "semantic_specifications": [], + "matching": None, + }, + } with tempfile.NamedTemporaryFile(prefix="learnware_stat_", suffix=".json") as ftemp: if stat_spec is not None: stat_spec.save(ftemp.name) @@ -245,26 +256,16 @@ class LearnwareClient: raise Exception("search failed: " + json.dumps(result)) for learnware in result["data"]["learnware_list_single"]: - returns.append( - { - "type": "single", - "learnware_id": learnware["learnware_id"], - "semantic_specification": learnware["semantic_specification"], - "matching": learnware["matching"], - } - ) + returns["single"]["learnware_ids"].append(learnware["learnware_id"]) + returns["single"]["semantic_specifications"].append(learnware["semantic_specification"]) + returns["single"]["matching"].append(learnware["matching"]) + if len(result["data"]["learnware_list_multi"]) > 0: - multiple_learnware = { - "type": "multiple", - "learnware_ids": [], - "semantic_specifications": [], - "matching": result["data"]["learnware_list_multi"][0]["matching"], - } - for learnware in result["data"]["learnware_list_multi"]: - multiple_learnware["learnware_ids"].append(learnware["learnware_id"]) - multiple_learnware["semantic_specifications"].append(learnware["semantic_specification"]) - - returns.append(multiple_learnware) + multi_learnware = result["data"]["learnware_list_multi"][0] + returns["multiple"]["learnware_ids"].append(multi_learnware["learnware_id"]) + returns["multiple"]["semantic_specifications"].append(multi_learnware["semantic_specification"]) + returns["multiple"]["matching"] = learnware["matching"] + return returns @require_login @@ -416,7 +417,7 @@ class LearnwareClient: @staticmethod def _check_semantic_specification(semantic_spec): from ..market import EasySemanticChecker - + check_status, message = EasySemanticChecker.check_semantic_spec(semantic_spec) return check_status != BaseChecker.INVALID_LEARNWARE, message From e95b6cc5e735657affc3db3404633a1e4b2e468b Mon Sep 17 00:00:00 2001 From: bxdd Date: Mon, 4 Dec 2023 22:53:52 +0800 Subject: [PATCH 19/43] [MNT] refactor hetero workflow and make it runnable --- tests/test_workflow/hetero_config.py | 100 ++++++ tests/test_workflow/test_hetero_workflow.py | 321 ++++++++++++++++++++ tests/test_workflow/test_workflow.py | 7 +- 3 files changed, 423 insertions(+), 5 deletions(-) create mode 100644 tests/test_workflow/hetero_config.py create mode 100644 tests/test_workflow/test_hetero_workflow.py diff --git a/tests/test_workflow/hetero_config.py b/tests/test_workflow/hetero_config.py new file mode 100644 index 0000000..1816b4c --- /dev/null +++ b/tests/test_workflow/hetero_config.py @@ -0,0 +1,100 @@ +input_shape_list = [20, 30] # 20-input shape of example learnware 0, 30-input shape of example learnware 1 + +input_description_list = [ + { + "Dimension": 20, + "Description": { # medical description + "0": "baseline value: Baseline Fetal Heart Rate (FHR)", + "1": "accelerations: Number of accelerations per second", + "2": "fetal_movement: Number of fetal movements per second", + "3": "uterine_contractions: Number of uterine contractions per second", + "4": "light_decelerations: Number of LDs per second", + "5": "severe_decelerations: Number of SDs per second", + "6": "prolongued_decelerations: Number of PDs per second", + "7": "abnormal_short_term_variability: Percentage of time with abnormal short term variability", + "8": "mean_value_of_short_term_variability: Mean value of short term variability", + "9": "percentage_of_time_with_abnormal_long_term_variability: Percentage of time with abnormal long term variability", + "10": "mean_value_of_long_term_variability: Mean value of long term variability", + "11": "histogram_width: Width of the histogram made using all values from a record", + "12": "histogram_min: Histogram minimum value", + "13": "histogram_max: Histogram maximum value", + "14": "histogram_number_of_peaks: Number of peaks in the exam histogram", + "15": "histogram_number_of_zeroes: Number of zeroes in the exam histogram", + "16": "histogram_mode: Hist mode", + "17": "histogram_mean: Hist mean", + "18": "histogram_median: Hist Median", + "19": "histogram_variance: Hist variance", + }, + }, + { + "Dimension": 30, + "Description": { # business description + "0": "This is a consecutive month number, used for convenience. For example, January 2013 is 0, February 2013 is 1,..., October 2015 is 33.", + "1": "This is the unique identifier for each shop.", + "2": "This is the unique identifier for each item.", + "3": "This is the code representing the city where the shop is located.", + "4": "This is the unique identifier for the category of the item.", + "5": "This is the code representing the type of the item.", + "6": "This is the code representing the subtype of the item.", + "7": "This is the number of this type of item sold in the shop one month ago.", + "8": "This is the number of this type of item sold in the shop two months ago.", + "9": "This is the number of this type of item sold in the shop three months ago.", + "10": "This is the number of this type of item sold in the shop six months ago.", + "11": "This is the number of this type of item sold in the shop twelve months ago.", + "12": "This is the average count of items sold one month ago.", + "13": "This is the average count of this type of item sold one month ago.", + "14": "This is the average count of this type of item sold two months ago.", + "15": "This is the average count of this type of item sold three months ago.", + "16": "This is the average count of this type of item sold six months ago.", + "17": "This is the average count of this type of item sold twelve months ago.", + "18": "This is the average count of items sold in the shop one month ago.", + "19": "This is the average count of items sold in the shop two months ago.", + "20": "This is the average count of items sold in the shop three months ago.", + "21": "This is the average count of items sold in the shop six months ago.", + "22": "This is the average count of items sold in the shop twelve months ago.", + "23": "This is the average count of items in the same category sold one month ago.", + "24": "This is the average count of items in the same category sold in the shop one month ago.", + "25": "This is the average count of items of the same type sold in the shop one month ago.", + "26": "This is the average count of items of the same subtype sold in the shop one month ago.", + "27": "This is the average count of items sold in the same city one month ago.", + "28": "This is the average count of this type of item sold in the same city one month ago.", + "29": "This is the average count of items of the same type sold one month ago.", + }, + }, +] + +output_description_list = [ + { + "Dimension": 1, + "Description": {"0": "length of stay: Length of hospital stay (days)"}, # medical description + }, + { + "Dimension": 1, + "Description": { # business description + "0": "sales of the item in the next day: Number of items sold in the next day" + }, + }, +] + +user_description_list = [ + { + "Dimension": 15, + "Description": { # medical description + "0": "Whether the patient is on thyroxine medication (0: No, 1: Yes)", + "1": "Whether the patient has been queried about thyroxine medication (0: No, 1: Yes)", + "2": "Whether the patient is on antithyroid medication (0: No, 1: Yes)", + "3": "Whether the patient has undergone thyroid surgery (0: No, 1: Yes)", + "4": "Whether the patient has been queried about hypothyroidism (0: No, 1: Yes)", + "5": "Whether the patient has been queried about hyperthyroidism (0: No, 1: Yes)", + "6": "Whether the patient is pregnant (0: No, 1: Yes)", + "7": "Whether the patient is sick (0: No, 1: Yes)", + "8": "Whether the patient has a tumor (0: No, 1: Yes)", + "9": "Whether the patient is taking lithium (0: No, 1: Yes)", + "10": "Whether the patient has a goitre (enlarged thyroid gland) (0: No, 1: Yes)", + "11": "Whether TSH (Thyroid Stimulating Hormone) level has been measured (0: No, 1: Yes)", + "12": "Whether T3 (Triiodothyronine) level has been measured (0: No, 1: Yes)", + "13": "Whether TT4 (Total Thyroxine) level has been measured (0: No, 1: Yes)", + "14": "Whether T4U (Thyroxine Utilization) level has been measured (0: No, 1: Yes)", + }, + } +] diff --git a/tests/test_workflow/test_hetero_workflow.py b/tests/test_workflow/test_hetero_workflow.py new file mode 100644 index 0000000..3276bdc --- /dev/null +++ b/tests/test_workflow/test_hetero_workflow.py @@ -0,0 +1,321 @@ +import torch +import pickle +import unittest +import os +import logging +import tempfile +import zipfile +from sklearn.linear_model import Ridge +from sklearn.datasets import make_regression +from shutil import copyfile, rmtree +from sklearn.metrics import mean_squared_error + +import learnware +learnware.init(logging_level=logging.WARNING) + +from learnware.market import instantiate_learnware_market, BaseUserInfo +from learnware.specification import RKMETableSpecification, generate_rkme_table_spec, generate_semantic_spec +from learnware.reuse import HeteroMapAlignLearnware, AveragingReuser, EnsemblePruningReuser +from learnware.tests.templates import LearnwareTemplate, PickleModelTemplate, StatSpecTemplate + +from hetero_config import input_shape_list, input_description_list, output_description_list, user_description_list + + +curr_root = os.path.dirname(os.path.abspath(__file__)) + +class TestHeteroWorkflow(unittest.TestCase): + universal_semantic_config = { + "data_type": "Table", + "task_type": "Regression", + "library_type": "Scikit-learn", + "scenarios": "Education", + "license": "MIT", + } + + def _init_learnware_market(self, organizer_kwargs=None): + """initialize learnware market""" + hetero_market = instantiate_learnware_market( + market_id="hetero_toy", name="hetero", rebuild=True, organizer_kwargs=organizer_kwargs + ) + return hetero_market + + def test_prepare_learnware_randomly(self, learnware_num=5): + self.zip_path_list = [] + + for i in range(learnware_num): + learnware_pool_dirpath = os.path.join(curr_root, "learnware_pool_hetero") + os.makedirs(learnware_pool_dirpath, exist_ok=True) + learnware_zippath = os.path.join(learnware_pool_dirpath, "ridge_%d.zip" % (i)) + + print("Preparing Learnware: %d" % (i)) + + X, y = make_regression(n_samples=5000, n_informative=15, n_features=input_shape_list[i % 2], noise=0.1, random_state=42) + clf = Ridge(alpha=1.0) + clf.fit(X, y) + pickle_filepath = os.path.join(learnware_pool_dirpath, "ridge.pkl") + with open(pickle_filepath, "wb") as fout: + pickle.dump(clf, fout) + + spec = generate_rkme_table_spec(X=X, gamma=0.1) + spec_filepath = os.path.join(learnware_pool_dirpath, "stat_spec.json") + spec.save(spec_filepath) + + LearnwareTemplate.generate_learnware_zipfile( + learnware_zippath=learnware_zippath, + model_template=PickleModelTemplate(pickle_filepath=pickle_filepath, model_kwargs={"input_shape":(input_shape_list[i % 2],), "output_shape": (1,)}), + stat_spec_template=StatSpecTemplate(filepath=spec_filepath, type="RKMETableSpecification"), + requirements=["scikit-learn==0.22"], + ) + + self.zip_path_list.append(learnware_zippath) + + + def _upload_delete_learnware(self, hetero_market, learnware_num, delete): + self.test_prepare_learnware_randomly(learnware_num) + self.learnware_num = learnware_num + + print("Total Item:", len(hetero_market)) + assert len(hetero_market) == 0, f"The market should be empty!" + + for idx, zip_path in enumerate(self.zip_path_list): + semantic_spec = generate_semantic_spec( + name=f"learnware_{idx}", + description=f"test_learnware_number_{idx}", + input_description=input_description_list[idx % 2], + output_description=output_description_list[idx % 2], + **self.universal_semantic_config + ) + hetero_market.add_learnware(zip_path, semantic_spec) + + print("Total Item:", len(hetero_market)) + assert len(hetero_market) == self.learnware_num, f"The number of learnwares must be {self.learnware_num}!" + curr_inds = hetero_market.get_learnware_ids() + print("Available ids After Uploading Learnwares:", curr_inds) + assert len(curr_inds) == self.learnware_num, f"The number of learnwares must be {self.learnware_num}!" + + if delete: + for learnware_id in curr_inds: + hetero_market.delete_learnware(learnware_id) + self.learnware_num -= 1 + assert ( + len(hetero_market) == self.learnware_num + ), f"The number of learnwares must be {self.learnware_num}!" + + curr_inds = hetero_market.get_learnware_ids() + print("Available ids After Deleting Learnwares:", curr_inds) + assert len(curr_inds) == 0, f"The market should be empty!" + + return hetero_market + + def test_upload_delete_learnware(self, learnware_num=5, delete=True): + hetero_market = self._init_learnware_market() + return self._upload_delete_learnware(hetero_market, learnware_num, delete) + + def test_train_market_model(self, learnware_num=5, delete=False): + hetero_market = self._init_learnware_market( + organizer_kwargs={"auto_update": True, "auto_update_limit": learnware_num} + ) + hetero_market = self._upload_delete_learnware(hetero_market, learnware_num, delete) + # organizer=hetero_market.learnware_organizer + # organizer.train(hetero_market.learnware_organizer.learnware_list.values()) + return hetero_market + + def test_search_semantics(self, learnware_num=5): + hetero_market = self.test_upload_delete_learnware(learnware_num, delete=False) + print("Total Item:", len(hetero_market)) + assert len(hetero_market) == self.learnware_num, f"The number of learnwares must be {self.learnware_num}!" + + semantic_spec = generate_semantic_spec( + name=f"learnware_{learnware_num - 1}", + **self.universal_semantic_config, + ) + + user_info = BaseUserInfo(semantic_spec=semantic_spec) + search_result = hetero_market.search_learnware(user_info) + single_result = search_result.get_single_results() + + print(f"Search result1:") + assert len(single_result) == 1, f"Exact semantic search failed!" + for search_item in single_result: + semantic_spec1 = search_item.learnware.get_specification().get_semantic_spec() + print("Choose learnware:", search_item.learnware.id) + assert semantic_spec1["Name"]["Values"] == semantic_spec["Name"]["Values"], f"Exact semantic search failed!" + + semantic_spec["Name"]["Values"] = "laernwaer" + user_info = BaseUserInfo(semantic_spec=semantic_spec) + search_result = hetero_market.search_learnware(user_info) + single_result = search_result.get_single_results() + + print(f"Search result2:") + assert len(single_result) == self.learnware_num, f"Fuzzy semantic search failed!" + for search_item in single_result: + print("Choose learnware:", search_item.learnware.id) + + def test_hetero_stat_search(self, learnware_num=5): + hetero_market = self.test_train_market_model(learnware_num, delete=False) + print("Total Item:", len(hetero_market)) + + user_dim = 15 + + with tempfile.TemporaryDirectory(prefix="learnware_test_hetero") as test_folder: + for idx, zip_path in enumerate(self.zip_path_list): + with zipfile.ZipFile(zip_path, "r") as zip_obj: + zip_obj.extractall(path=test_folder) + + user_spec = RKMETableSpecification() + user_spec.load(os.path.join(test_folder, "stat_spec.json")) + z = user_spec.get_z() + z = z[:, :user_dim] + device = user_spec.device + z = torch.tensor(z, device=device) + user_spec.z = z + + print(">> normal case test:") + semantic_spec = generate_semantic_spec( + input_description={ + "Dimension": user_dim, + "Description": {str(key): input_description_list[idx % 2]["Description"][str(key)] for key in range(user_dim)}, + }, + **self.universal_semantic_config, + ) + user_info = BaseUserInfo(semantic_spec=semantic_spec, stat_info={"RKMETableSpecification": user_spec}) + search_result = hetero_market.search_learnware(user_info) + single_result = search_result.get_single_results() + multiple_result = search_result.get_multiple_results() + + print(f"search result of user{idx}:") + for single_item in single_result: + print(f"score: {single_item.score}, learnware_id: {single_item.learnware.id}") + + for multiple_item in multiple_result: + print( + f"mixture_score: {multiple_item.score}, mixture_learnware_ids: {[item.id for item in multiple_item.learnwares]}" + ) + + # inproper key "Task" in semantic_spec, use homo search and print invalid semantic_spec + print(">> test for key 'Task' has empty 'Values':") + semantic_spec["Task"] = {"Values": ["Segmentation"], "Type": "Class"} + user_info = BaseUserInfo(semantic_spec=semantic_spec, stat_info={"RKMETableSpecification": user_spec}) + search_result = hetero_market.search_learnware(user_info) + single_result = search_result.get_single_results() + + assert len(single_result) == 0, f"Statistical search failed!" + + # delete key "Task" in semantic_spec, use homo search and print WARNING INFO with "User doesn't provide correct task type" + print(">> delele key 'Task' test:") + semantic_spec.pop("Task") + user_info = BaseUserInfo(semantic_spec=semantic_spec, stat_info={"RKMETableSpecification": user_spec}) + search_result = hetero_market.search_learnware(user_info) + single_result = search_result.get_single_results() + + assert len(single_result) == 0, f"Statistical search failed!" + + # modify semantic info with mismatch dim, use homo search and print "User data feature dimensions mismatch with semantic specification." + print(">> mismatch dim test") + semantic_spec = generate_semantic_spec( + input_description={ + "Dimension": user_dim - 2, + "Description": {str(key): input_description_list[idx % 2]["Description"][str(key)] for key in range(user_dim)}, + }, + **self.universal_semantic_config, + ) + user_info = BaseUserInfo(semantic_spec=semantic_spec, stat_info={"RKMETableSpecification": user_spec}) + search_result = hetero_market.search_learnware(user_info) + single_result = search_result.get_single_results() + + assert len(single_result) == 0, f"Statistical search failed!" + + def test_homo_stat_search(self, learnware_num=5): + hetero_market = self.test_train_market_model(learnware_num, delete=False) + print("Total Item:", len(hetero_market)) + + with tempfile.TemporaryDirectory(prefix="learnware_test_hetero") as test_folder: + for idx, zip_path in enumerate(self.zip_path_list): + with zipfile.ZipFile(zip_path, "r") as zip_obj: + zip_obj.extractall(path=test_folder) + + user_spec = RKMETableSpecification() + user_spec.load(os.path.join(test_folder, "stat_spec.json")) + user_semantic = generate_semantic_spec(**self.universal_semantic_config) + user_info = BaseUserInfo(semantic_spec=user_semantic, stat_info={"RKMETableSpecification": user_spec}) + search_result = hetero_market.search_learnware(user_info) + single_result = search_result.get_single_results() + multiple_result = search_result.get_multiple_results() + + assert len(single_result) >= 1, f"Statistical search failed!" + print(f"search result of user{idx}:") + for single_item in single_result: + print(f"score: {single_item.score}, learnware_id: {single_item.learnware.id}") + + for multiple_item in multiple_result: + print(f"mixture_score: {multiple_item.score}\n") + mixture_id = " ".join([learnware.id for learnware in multiple_item.learnwares]) + print(f"mixture_learnware: {mixture_id}\n") + + def test_model_reuse(self, learnware_num=5): + # generate toy regression problem + X, y = make_regression(n_samples=5000, n_informative=10, n_features=15, noise=0.1, random_state=0) + + # generate rkme + user_spec = generate_rkme_table_spec(X=X, gamma=0.1, cuda_idx=0) + + # generate specification + semantic_spec = generate_semantic_spec(input_description=user_description_list[0], **self.universal_semantic_config) + user_info = BaseUserInfo(semantic_spec=semantic_spec, stat_info={"RKMETableSpecification": user_spec}) + + # learnware market search + hetero_market = self.test_train_market_model(learnware_num, delete=False) + search_result = hetero_market.search_learnware(user_info) + single_result = search_result.get_single_results() + multiple_result = search_result.get_multiple_results() + + # print search results + for single_item in single_result: + print(f"score: {single_item.score}, learnware_id: {single_item.learnware.id}") + + for multiple_item in multiple_result: + print( + f"mixture_score: {multiple_item.score}, mixture_learnware_ids: {[item.id for item in multiple_item.learnwares]}" + ) + + # single model reuse + hetero_learnware = HeteroMapAlignLearnware(single_result[0].learnware, mode="regression") + hetero_learnware.align(user_spec, X[:100], y[:100]) + single_predict_y = hetero_learnware.predict(X) + + # multi model reuse + hetero_learnware_list = [] + for learnware in multiple_result[0].learnwares: + hetero_learnware = HeteroMapAlignLearnware(learnware, mode="regression") + hetero_learnware.align(user_spec, X[:100], y[:100]) + hetero_learnware_list.append(hetero_learnware) + + # Use averaging ensemble reuser to reuse the searched learnwares to make prediction + reuse_ensemble = AveragingReuser(learnware_list=hetero_learnware_list, mode="mean") + ensemble_predict_y = reuse_ensemble.predict(user_data=X) + + # Use ensemble pruning reuser to reuse the searched learnwares to make prediction + reuse_ensemble = EnsemblePruningReuser(learnware_list=hetero_learnware_list, mode="regression") + reuse_ensemble.fit(X[:100], y[:100]) + ensemble_pruning_predict_y = reuse_ensemble.predict(user_data=X) + + print("Single model RMSE by finetune:", mean_squared_error(y, single_predict_y, squared=False)) + print("Averaging Reuser RMSE:", mean_squared_error(y, ensemble_predict_y, squared=False)) + print("Ensemble Pruning Reuser RMSE:", mean_squared_error(y, ensemble_pruning_predict_y, squared=False)) + + +def suite(): + _suite = unittest.TestSuite() + #_suite.addTest(TestHeteroWorkflow("test_prepare_learnware_randomly")) + #_suite.addTest(TestHeteroWorkflow("test_upload_delete_learnware")) + #_suite.addTest(TestHeteroWorkflow("test_train_market_model")) + _suite.addTest(TestHeteroWorkflow("test_search_semantics")) + _suite.addTest(TestHeteroWorkflow("test_hetero_stat_search")) + _suite.addTest(TestHeteroWorkflow("test_homo_stat_search")) + _suite.addTest(TestHeteroWorkflow("test_model_reuse")) + return _suite + + +if __name__ == "__main__": + runner = unittest.TextTestRunner(verbosity=2) + runner.run(suite()) diff --git a/tests/test_workflow/test_workflow.py b/tests/test_workflow/test_workflow.py index b0aa462..c7a5bc5 100644 --- a/tests/test_workflow/test_workflow.py +++ b/tests/test_workflow/test_workflow.py @@ -29,10 +29,6 @@ class TestWorkflow(unittest.TestCase): "license": "MIT", } - @classmethod - def setUpClass(cls): - pass - def _init_learnware_market(self): """initialize learnware market""" easy_market = instantiate_learnware_market(market_id="sklearn_digits_easy", name="easy", rebuild=True) @@ -62,7 +58,8 @@ class TestWorkflow(unittest.TestCase): LearnwareTemplate.generate_learnware_zipfile( learnware_zippath=learnware_zippath, model_template=PickleModelTemplate(pickle_filepath=pickle_filepath, model_kwargs={"input_shape":(64,), "output_shape": (10,), "predict_method": "predict_proba"}), - stat_spec_template=StatSpecTemplate(filepath=spec_filepath, type="RKMETableSpecification") + stat_spec_template=StatSpecTemplate(filepath=spec_filepath, type="RKMETableSpecification"), + requirements=["scikit-learn==0.22"], ) self.zip_path_list.append(learnware_zippath) From 8ae362c04f604e3f9bd223046aed73b40cc0e4bd Mon Sep 17 00:00:00 2001 From: bxdd Date: Mon, 4 Dec 2023 22:54:16 +0800 Subject: [PATCH 20/43] [MNT] del useless file --- .../example_learnwares/config.py | 100 ----- .../example_learnwares/learnware.yaml | 8 - .../example_learnwares/model0.py | 16 - .../example_learnwares/model1.py | 16 - .../example_learnwares/requirements.txt | 1 - tests/test_hetero_market/test_hetero.py | 414 ------------------ 6 files changed, 555 deletions(-) delete mode 100644 tests/test_hetero_market/example_learnwares/config.py delete mode 100644 tests/test_hetero_market/example_learnwares/learnware.yaml delete mode 100644 tests/test_hetero_market/example_learnwares/model0.py delete mode 100644 tests/test_hetero_market/example_learnwares/model1.py delete mode 100644 tests/test_hetero_market/example_learnwares/requirements.txt delete mode 100644 tests/test_hetero_market/test_hetero.py diff --git a/tests/test_hetero_market/example_learnwares/config.py b/tests/test_hetero_market/example_learnwares/config.py deleted file mode 100644 index 1816b4c..0000000 --- a/tests/test_hetero_market/example_learnwares/config.py +++ /dev/null @@ -1,100 +0,0 @@ -input_shape_list = [20, 30] # 20-input shape of example learnware 0, 30-input shape of example learnware 1 - -input_description_list = [ - { - "Dimension": 20, - "Description": { # medical description - "0": "baseline value: Baseline Fetal Heart Rate (FHR)", - "1": "accelerations: Number of accelerations per second", - "2": "fetal_movement: Number of fetal movements per second", - "3": "uterine_contractions: Number of uterine contractions per second", - "4": "light_decelerations: Number of LDs per second", - "5": "severe_decelerations: Number of SDs per second", - "6": "prolongued_decelerations: Number of PDs per second", - "7": "abnormal_short_term_variability: Percentage of time with abnormal short term variability", - "8": "mean_value_of_short_term_variability: Mean value of short term variability", - "9": "percentage_of_time_with_abnormal_long_term_variability: Percentage of time with abnormal long term variability", - "10": "mean_value_of_long_term_variability: Mean value of long term variability", - "11": "histogram_width: Width of the histogram made using all values from a record", - "12": "histogram_min: Histogram minimum value", - "13": "histogram_max: Histogram maximum value", - "14": "histogram_number_of_peaks: Number of peaks in the exam histogram", - "15": "histogram_number_of_zeroes: Number of zeroes in the exam histogram", - "16": "histogram_mode: Hist mode", - "17": "histogram_mean: Hist mean", - "18": "histogram_median: Hist Median", - "19": "histogram_variance: Hist variance", - }, - }, - { - "Dimension": 30, - "Description": { # business description - "0": "This is a consecutive month number, used for convenience. For example, January 2013 is 0, February 2013 is 1,..., October 2015 is 33.", - "1": "This is the unique identifier for each shop.", - "2": "This is the unique identifier for each item.", - "3": "This is the code representing the city where the shop is located.", - "4": "This is the unique identifier for the category of the item.", - "5": "This is the code representing the type of the item.", - "6": "This is the code representing the subtype of the item.", - "7": "This is the number of this type of item sold in the shop one month ago.", - "8": "This is the number of this type of item sold in the shop two months ago.", - "9": "This is the number of this type of item sold in the shop three months ago.", - "10": "This is the number of this type of item sold in the shop six months ago.", - "11": "This is the number of this type of item sold in the shop twelve months ago.", - "12": "This is the average count of items sold one month ago.", - "13": "This is the average count of this type of item sold one month ago.", - "14": "This is the average count of this type of item sold two months ago.", - "15": "This is the average count of this type of item sold three months ago.", - "16": "This is the average count of this type of item sold six months ago.", - "17": "This is the average count of this type of item sold twelve months ago.", - "18": "This is the average count of items sold in the shop one month ago.", - "19": "This is the average count of items sold in the shop two months ago.", - "20": "This is the average count of items sold in the shop three months ago.", - "21": "This is the average count of items sold in the shop six months ago.", - "22": "This is the average count of items sold in the shop twelve months ago.", - "23": "This is the average count of items in the same category sold one month ago.", - "24": "This is the average count of items in the same category sold in the shop one month ago.", - "25": "This is the average count of items of the same type sold in the shop one month ago.", - "26": "This is the average count of items of the same subtype sold in the shop one month ago.", - "27": "This is the average count of items sold in the same city one month ago.", - "28": "This is the average count of this type of item sold in the same city one month ago.", - "29": "This is the average count of items of the same type sold one month ago.", - }, - }, -] - -output_description_list = [ - { - "Dimension": 1, - "Description": {"0": "length of stay: Length of hospital stay (days)"}, # medical description - }, - { - "Dimension": 1, - "Description": { # business description - "0": "sales of the item in the next day: Number of items sold in the next day" - }, - }, -] - -user_description_list = [ - { - "Dimension": 15, - "Description": { # medical description - "0": "Whether the patient is on thyroxine medication (0: No, 1: Yes)", - "1": "Whether the patient has been queried about thyroxine medication (0: No, 1: Yes)", - "2": "Whether the patient is on antithyroid medication (0: No, 1: Yes)", - "3": "Whether the patient has undergone thyroid surgery (0: No, 1: Yes)", - "4": "Whether the patient has been queried about hypothyroidism (0: No, 1: Yes)", - "5": "Whether the patient has been queried about hyperthyroidism (0: No, 1: Yes)", - "6": "Whether the patient is pregnant (0: No, 1: Yes)", - "7": "Whether the patient is sick (0: No, 1: Yes)", - "8": "Whether the patient has a tumor (0: No, 1: Yes)", - "9": "Whether the patient is taking lithium (0: No, 1: Yes)", - "10": "Whether the patient has a goitre (enlarged thyroid gland) (0: No, 1: Yes)", - "11": "Whether TSH (Thyroid Stimulating Hormone) level has been measured (0: No, 1: Yes)", - "12": "Whether T3 (Triiodothyronine) level has been measured (0: No, 1: Yes)", - "13": "Whether TT4 (Total Thyroxine) level has been measured (0: No, 1: Yes)", - "14": "Whether T4U (Thyroxine Utilization) level has been measured (0: No, 1: Yes)", - }, - } -] diff --git a/tests/test_hetero_market/example_learnwares/learnware.yaml b/tests/test_hetero_market/example_learnwares/learnware.yaml deleted file mode 100644 index 4a37a37..0000000 --- a/tests/test_hetero_market/example_learnwares/learnware.yaml +++ /dev/null @@ -1,8 +0,0 @@ -model: - class_name: MyModel - kwargs: {} -stat_specifications: - - module_path: learnware.specification - class_name: RKMETableSpecification - file_name: stat.json - kwargs: {} \ No newline at end of file diff --git a/tests/test_hetero_market/example_learnwares/model0.py b/tests/test_hetero_market/example_learnwares/model0.py deleted file mode 100644 index 45f64b7..0000000 --- a/tests/test_hetero_market/example_learnwares/model0.py +++ /dev/null @@ -1,16 +0,0 @@ -from learnware.model import BaseModel -import numpy as np -import joblib -import os - - -class MyModel(BaseModel): - def __init__(self): - super(MyModel, self).__init__(input_shape=(20,), output_shape=(1,)) - dir_path = os.path.dirname(os.path.abspath(__file__)) - model_path = os.path.join(dir_path, "ridge.pkl") - model = joblib.load(model_path) - self.model = model - - def predict(self, X: np.ndarray) -> np.ndarray: - return self.model.predict(X) diff --git a/tests/test_hetero_market/example_learnwares/model1.py b/tests/test_hetero_market/example_learnwares/model1.py deleted file mode 100644 index aca46b3..0000000 --- a/tests/test_hetero_market/example_learnwares/model1.py +++ /dev/null @@ -1,16 +0,0 @@ -from learnware.model import BaseModel -import numpy as np -import joblib -import os - - -class MyModel(BaseModel): - def __init__(self): - super(MyModel, self).__init__(input_shape=(30,), output_shape=(1,)) - dir_path = os.path.dirname(os.path.abspath(__file__)) - model_path = os.path.join(dir_path, "ridge.pkl") - model = joblib.load(model_path) - self.model = model - - def predict(self, X: np.ndarray) -> np.ndarray: - return self.model.predict(X) diff --git a/tests/test_hetero_market/example_learnwares/requirements.txt b/tests/test_hetero_market/example_learnwares/requirements.txt deleted file mode 100644 index 1da1c5f..0000000 --- a/tests/test_hetero_market/example_learnwares/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -learnware == 0.1.0.999 \ No newline at end of file diff --git a/tests/test_hetero_market/test_hetero.py b/tests/test_hetero_market/test_hetero.py deleted file mode 100644 index 7b0740b..0000000 --- a/tests/test_hetero_market/test_hetero.py +++ /dev/null @@ -1,414 +0,0 @@ -import torch -import unittest -import os -import copy -import joblib -import zipfile -import numpy as np -import multiprocessing -from sklearn.linear_model import Ridge -from sklearn.datasets import make_regression -from shutil import copyfile, rmtree -from learnware.client import LearnwareClient -from sklearn.metrics import mean_squared_error - -import learnware -from learnware.market import instantiate_learnware_market, BaseUserInfo -from learnware.specification import RKMETableSpecification, generate_rkme_table_spec -from learnware.reuse import HeteroMapAlignLearnware, AveragingReuser, EnsemblePruningReuser -from example_learnwares.config import ( - input_shape_list, - input_description_list, - output_description_list, - user_description_list, -) - -curr_root = os.path.dirname(os.path.abspath(__file__)) - -user_semantic = { - "Data": {"Values": ["Table"], "Type": "Class"}, - "Task": { - "Values": ["Regression"], - "Type": "Class", - }, - "Library": {"Values": ["Scikit-learn"], "Type": "Class"}, - "Scenario": {"Values": ["Education"], "Type": "Tag"}, - "Description": {"Values": "", "Type": "String"}, - "Name": {"Values": "", "Type": "String"}, - "License": {"Values": ["MIT"], "Type": "Class"}, -} - - -def check_learnware(learnware_name, dir_path=os.path.join(curr_root, "learnware_pool")): - print(f"Checking Learnware: {learnware_name}") - zip_file_path = os.path.join(dir_path, learnware_name) - client = LearnwareClient() - # if check_learnware doesn't raise an exception, return True, otherwise, return false - try: - client.check_learnware(zip_file_path) - return True - except Exception as e: - print(f"Learnware {learnware_name} failed the check: {e}") - return False - - -class TestMarket(unittest.TestCase): - @classmethod - def setUpClass(cls) -> None: - np.random.seed(2023) - learnware.init() - - def _init_learnware_market(self, organizer_kwargs=None): - """initialize learnware market""" - hetero_market = instantiate_learnware_market( - market_id="hetero_toy", name="hetero", rebuild=True, organizer_kwargs=organizer_kwargs - ) - return hetero_market - - def test_prepare_learnware_randomly(self, learnware_num=5): - self.zip_path_list = [] - - for i in range(learnware_num): - dir_path = os.path.join(curr_root, "learnware_pool", "ridge_%d" % (i)) - os.makedirs(dir_path, exist_ok=True) - - print("Preparing Learnware: %d" % (i)) - - example_learnware_idx = i % 2 - input_dim = input_shape_list[example_learnware_idx] - learnware_example_dir = "example_learnwares" - - X, y = make_regression(n_samples=5000, n_informative=15, n_features=input_dim, noise=0.1, random_state=42) - - clf = Ridge(alpha=1.0) - clf.fit(X, y) - - joblib.dump(clf, os.path.join(dir_path, "ridge.pkl")) - - spec = generate_rkme_table_spec(X=X, gamma=0.1, cuda_idx=0) - spec.save(os.path.join(dir_path, "stat.json")) - - init_file = os.path.join(dir_path, "__init__.py") - copyfile( - os.path.join(curr_root, learnware_example_dir, f"model{example_learnware_idx}.py"), init_file - ) # cp example_init.py init_file - - yaml_file = os.path.join(dir_path, "learnware.yaml") - copyfile( - os.path.join(curr_root, learnware_example_dir, "learnware.yaml"), yaml_file - ) # cp example.yaml yaml_file - - env_file = os.path.join(dir_path, "requirements.txt") - copyfile(os.path.join(curr_root, learnware_example_dir, "requirements.txt"), env_file) - - zip_file = dir_path + ".zip" - # zip -q -r -j zip_file dir_path - with zipfile.ZipFile(zip_file, "w") as zip_obj: - for foldername, subfolders, filenames in os.walk(dir_path): - for filename in filenames: - file_path = os.path.join(foldername, filename) - zip_info = zipfile.ZipInfo(filename) - zip_info.compress_type = zipfile.ZIP_STORED - with open(file_path, "rb") as file: - zip_obj.writestr(zip_info, file.read()) - - rmtree(dir_path) # rm -r dir_path - - self.zip_path_list.append(zip_file) - - def test_generated_learnwares(self): - curr_root = os.path.dirname(os.path.abspath(__file__)) - dir_path = os.path.join(curr_root, "learnware_pool") - - # Execute multi-process checking using Pool - mp_context = multiprocessing.get_context("spawn") - with mp_context.Pool() as pool: - results = pool.starmap(check_learnware, [(name, dir_path) for name in os.listdir(dir_path)]) - - # Use an assert statement to ensure that all checks return True - self.assertTrue(all(results), "Not all learnwares passed the check") - - def test_upload_delete_learnware(self, learnware_num=5, delete=True): - hetero_market = self._init_learnware_market() - self.test_prepare_learnware_randomly(learnware_num) - self.learnware_num = learnware_num - - print("Total Item:", len(hetero_market)) - assert len(hetero_market) == 0, f"The market should be empty!" - - for idx, zip_path in enumerate(self.zip_path_list): - semantic_spec = copy.deepcopy(user_semantic) - semantic_spec["Name"]["Values"] = "learnware_%d" % (idx) - semantic_spec["Description"]["Values"] = "test_learnware_number_%d" % (idx) - semantic_spec["Input"] = input_description_list[idx % 2] - semantic_spec["Output"] = output_description_list[idx % 2] - hetero_market.add_learnware(zip_path, semantic_spec) - - print("Total Item:", len(hetero_market)) - assert len(hetero_market) == self.learnware_num, f"The number of learnwares must be {self.learnware_num}!" - curr_inds = hetero_market.get_learnware_ids() - print("Available ids After Uploading Learnwares:", curr_inds) - assert len(curr_inds) == self.learnware_num, f"The number of learnwares must be {self.learnware_num}!" - - if delete: - for learnware_id in curr_inds: - hetero_market.delete_learnware(learnware_id) - self.learnware_num -= 1 - assert ( - len(hetero_market) == self.learnware_num - ), f"The number of learnwares must be {self.learnware_num}!" - - curr_inds = hetero_market.get_learnware_ids() - print("Available ids After Deleting Learnwares:", curr_inds) - assert len(curr_inds) == 0, f"The market should be empty!" - - return hetero_market - - def test_train_market_model(self, learnware_num=5): - hetero_market = self._init_learnware_market( - organizer_kwargs={"auto_update": False, "auto_update_limit": learnware_num} - ) - self.test_prepare_learnware_randomly(learnware_num) - self.learnware_num = learnware_num - - print("Total Item:", len(hetero_market)) - assert len(hetero_market) == 0, f"The market should be empty!" - - for idx, zip_path in enumerate(self.zip_path_list): - semantic_spec = copy.deepcopy(user_semantic) - semantic_spec["Name"]["Values"] = "learnware_%d" % (idx) - semantic_spec["Description"]["Values"] = "test_learnware_number_%d" % (idx) - semantic_spec["Input"] = input_description_list[idx % 2] - semantic_spec["Output"] = output_description_list[idx % 2] - hetero_market.add_learnware(zip_path, semantic_spec) - - print("Total Item:", len(hetero_market)) - assert len(hetero_market) == self.learnware_num, f"The number of learnwares must be {self.learnware_num}!" - curr_inds = hetero_market.get_learnware_ids() - print("Available ids After Uploading Learnwares:", curr_inds) - assert len(curr_inds) == self.learnware_num, f"The number of learnwares must be {self.learnware_num}!" - - # organizer=hetero_market.learnware_organizer - # organizer.train(hetero_market.learnware_organizer.learnware_list.values()) - return hetero_market - - def test_search_semantics(self, learnware_num=5): - hetero_market = self.test_upload_delete_learnware(learnware_num, delete=False) - print("Total Item:", len(hetero_market)) - assert len(hetero_market) == self.learnware_num, f"The number of learnwares must be {self.learnware_num}!" - - semantic_spec = copy.deepcopy(user_semantic) - semantic_spec["Name"]["Values"] = f"learnware_{learnware_num - 1}" - - user_info = BaseUserInfo(semantic_spec=semantic_spec) - search_result = hetero_market.search_learnware(user_info) - single_result = search_result.get_single_results() - - print("User info:", user_info.get_semantic_spec()) - print(f"Search result:") - assert len(single_result) == 1, f"Exact semantic search failed!" - for search_item in single_result: - semantic_spec1 = search_item.learnware.get_specification().get_semantic_spec() - print("Choose learnware:", search_item.learnware.id, semantic_spec1) - assert semantic_spec1["Name"]["Values"] == semantic_spec["Name"]["Values"], f"Exact semantic search failed!" - - semantic_spec["Name"]["Values"] = "laernwaer" - user_info = BaseUserInfo(semantic_spec=semantic_spec) - search_result = hetero_market.search_learnware(user_info) - single_result = search_result.get_single_results() - - print("User info:", user_info.get_semantic_spec()) - print(f"Search result:") - assert len(single_result) == self.learnware_num, f"Fuzzy semantic search failed!" - for search_item in single_result: - semantic_spec1 = search_item.learnware.get_specification().get_semantic_spec() - print("Choose learnware:", search_item.learnware.id, semantic_spec1) - - def test_stat_search(self, learnware_num=5): - hetero_market = self.test_train_market_model(learnware_num) - print("Total Item:", len(hetero_market)) - - # hetero test - print("+++++ HETERO TEST ++++++") - user_dim = 15 - - test_folder = os.path.join(curr_root, "test_stat") - - for idx, zip_path in enumerate(self.zip_path_list): - unzip_dir = os.path.join(test_folder, f"{idx}") - - # unzip -o -q zip_path -d unzip_dir - if os.path.exists(unzip_dir): - rmtree(unzip_dir) - os.makedirs(unzip_dir, exist_ok=True) - with zipfile.ZipFile(zip_path, "r") as zip_obj: - zip_obj.extractall(path=unzip_dir) - - user_spec = RKMETableSpecification() - user_spec.load(os.path.join(unzip_dir, "stat.json")) - z = user_spec.get_z() - z = z[:, :user_dim] - device = user_spec.device - z = torch.tensor(z, device=device) - user_spec.z = z - - print(">> normal case test:") - semantic_spec = copy.deepcopy(user_semantic) - semantic_spec["Input"] = copy.deepcopy(input_description_list[idx % 2]) - semantic_spec["Input"]["Dimension"] = user_dim - # keep only the first user_dim descriptions - semantic_spec["Input"]["Description"] = { - str(key): semantic_spec["Input"]["Description"][str(key)] for key in range(user_dim) - } - user_info = BaseUserInfo(semantic_spec=semantic_spec, stat_info={"RKMETableSpecification": user_spec}) - - search_result = hetero_market.search_learnware(user_info) - single_result = search_result.get_single_results() - multiple_result = search_result.get_multiple_results() - - print(f"search result of user{idx}:") - for single_item in single_result: - print(f"score: {single_item.score}, learnware_id: {single_item.learnware.id}") - - for multiple_item in multiple_result: - print( - f"mixture_score: {multiple_item.score}, mixture_learnware_ids: {[item.id for item in multiple_item.learnwares]}" - ) - - # inproper key "Task" in semantic_spec, use homo search and print invalid semantic_spec - print(">> test for key 'Task' has empty 'Values':") - semantic_spec["Task"] = {"Values": ["Segmentation"], "Type": "Class"} - - user_info = BaseUserInfo(semantic_spec=semantic_spec, stat_info={"RKMETableSpecification": user_spec}) - search_result = hetero_market.search_learnware(user_info) - single_result = search_result.get_single_results() - - assert len(single_result) == 0, f"Statistical search failed!" - - # delete key "Task" in semantic_spec, use homo search and print WARNING INFO with "User doesn't provide correct task type" - print(">> delele key 'Task' test:") - semantic_spec.pop("Task") - - user_info = BaseUserInfo(semantic_spec=semantic_spec, stat_info={"RKMETableSpecification": user_spec}) - search_result = hetero_market.search_learnware(user_info) - single_result = search_result.get_single_results() - - assert len(single_result) == 0, f"Statistical search failed!" - - # modify semantic info with mismatch dim, use homo search and print "User data feature dimensions mismatch with semantic specification." - print(">> mismatch dim test") - semantic_spec = copy.deepcopy(user_semantic) - semantic_spec["Input"] = copy.deepcopy(input_description_list[idx % 2]) - semantic_spec["Input"]["Dimension"] = user_dim - 2 - semantic_spec["Input"]["Description"] = { - str(key): semantic_spec["Input"]["Description"][str(key)] for key in range(user_dim) - } - - user_info = BaseUserInfo(semantic_spec=semantic_spec, stat_info={"RKMETableSpecification": user_spec}) - search_result = hetero_market.search_learnware(user_info) - single_result = search_result.get_single_results() - - assert len(single_result) == 0, f"Statistical search failed!" - - rmtree(test_folder) # rm -r test_folder - - # homo test - print("\n+++++ HOMO TEST ++++++") - test_folder = os.path.join(curr_root, "test_stat") - - for idx, zip_path in enumerate(self.zip_path_list): - unzip_dir = os.path.join(test_folder, f"{idx}") - - # unzip -o -q zip_path -d unzip_dir - if os.path.exists(unzip_dir): - rmtree(unzip_dir) - os.makedirs(unzip_dir, exist_ok=True) - with zipfile.ZipFile(zip_path, "r") as zip_obj: - zip_obj.extractall(path=unzip_dir) - - user_spec = RKMETableSpecification() - user_spec.load(os.path.join(unzip_dir, "stat.json")) - user_info = BaseUserInfo(semantic_spec=user_semantic, stat_info={"RKMETableSpecification": user_spec}) - search_result = hetero_market.search_learnware(user_info) - single_result = search_result.get_single_results() - multiple_result = search_result.get_multiple_results() - - assert len(single_result) >= 1, f"Statistical search failed!" - print(f"search result of user{idx}:") - for single_item in single_result: - print(f"score: {single_item.score}, learnware_id: {single_item.learnware.id}") - - for multiple_item in multiple_result: - print(f"mixture_score: {multiple_item.score}\n") - mixture_id = " ".join([learnware.id for learnware in multiple_item.learnwares]) - print(f"mixture_learnware: {mixture_id}\n") - - rmtree(test_folder) # rm -r test_folder - - def test_model_reuse(self, learnware_num=5): - # generate toy regression problem - X, y = make_regression(n_samples=5000, n_informative=10, n_features=15, noise=0.1, random_state=0) - - # generate rkme - user_spec = generate_rkme_table_spec(X=X, gamma=0.1, cuda_idx=0) - - # generate specification - semantic_spec = copy.deepcopy(user_semantic) - semantic_spec["Input"] = user_description_list[0] - user_info = BaseUserInfo(semantic_spec=semantic_spec, stat_info={"RKMETableSpecification": user_spec}) - - # learnware market search - hetero_market = self.test_train_market_model(learnware_num) - search_result = hetero_market.search_learnware(user_info) - single_result = search_result.get_single_results() - multiple_result = search_result.get_multiple_results() - # print search results - for single_item in single_result: - print(f"score: {single_item.score}, learnware_id: {single_item.learnware.id}") - - for multiple_item in multiple_result: - print( - f"mixture_score: {multiple_item.score}, mixture_learnware_ids: {[item.id for item in multiple_item.learnwares]}" - ) - - # single model reuse - hetero_learnware = HeteroMapAlignLearnware(single_result[0].learnware, mode="regression") - hetero_learnware.align(user_spec, X[:100], y[:100]) - single_predict_y = hetero_learnware.predict(X) - - # multi model reuse - hetero_learnware_list = [] - for learnware in multiple_result[0].learnwares: - hetero_learnware = HeteroMapAlignLearnware(learnware, mode="regression") - hetero_learnware.align(user_spec, X[:100], y[:100]) - hetero_learnware_list.append(hetero_learnware) - - # Use averaging ensemble reuser to reuse the searched learnwares to make prediction - reuse_ensemble = AveragingReuser(learnware_list=hetero_learnware_list, mode="mean") - ensemble_predict_y = reuse_ensemble.predict(user_data=X) - - # Use ensemble pruning reuser to reuse the searched learnwares to make prediction - reuse_ensemble = EnsemblePruningReuser(learnware_list=hetero_learnware_list, mode="regression") - reuse_ensemble.fit(X[:100], y[:100]) - ensemble_pruning_predict_y = reuse_ensemble.predict(user_data=X) - - print("Single model RMSE by finetune:", mean_squared_error(y, single_predict_y, squared=False)) - print("Averaging Reuser RMSE:", mean_squared_error(y, ensemble_predict_y, squared=False)) - print("Ensemble Pruning Reuser RMSE:", mean_squared_error(y, ensemble_pruning_predict_y, squared=False)) - - -def suite(): - _suite = unittest.TestSuite() - _suite.addTest(TestMarket("test_prepare_learnware_randomly")) - _suite.addTest(TestMarket("test_generated_learnwares")) - _suite.addTest(TestMarket("test_upload_delete_learnware")) - _suite.addTest(TestMarket("test_train_market_model")) - _suite.addTest(TestMarket("test_search_semantics")) - _suite.addTest(TestMarket("test_stat_search")) - _suite.addTest(TestMarket("test_model_reuse")) - return _suite - - -if __name__ == "__main__": - runner = unittest.TextTestRunner() - runner.run(suite()) From 3bec90846543e70409b67f507fd72af16fab7aa7 Mon Sep 17 00:00:00 2001 From: bxdd Date: Mon, 4 Dec 2023 23:15:34 +0800 Subject: [PATCH 21/43] [MNT] recover test format which doest not use argparse --- .../test_all_learnware.py | 14 +++--- .../test_check_learnware.py | 13 ++++-- tests/test_learnware_client/test_container.py | 19 ++++---- .../test_load_learnware.py | 21 ++++----- tests/test_learnware_client/test_upload.py | 13 +++--- tests/test_reuse/test_averaging.py | 43 ------------------- .../test_search_image.py | 0 .../test_search_learnware/test_search_text.py | 0 8 files changed, 39 insertions(+), 84 deletions(-) delete mode 100644 tests/test_reuse/test_averaging.py create mode 100644 tests/test_search_learnware/test_search_image.py create mode 100644 tests/test_search_learnware/test_search_text.py diff --git a/tests/test_learnware_client/test_all_learnware.py b/tests/test_learnware_client/test_all_learnware.py index f7fc9d8..d1e9b70 100644 --- a/tests/test_learnware_client/test_all_learnware.py +++ b/tests/test_learnware_client/test_all_learnware.py @@ -8,7 +8,6 @@ import argparse from learnware.client import LearnwareClient from learnware.specification import generate_semantic_spec from learnware.market import BaseUserInfo -from learnware.tests import parametrize class TestAllLearnware(unittest.TestCase): client = LearnwareClient() @@ -51,11 +50,12 @@ class TestAllLearnware(unittest.TestCase): print(f"The currently failed learnware ids: {failed_ids}") -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("--email", type=str, required=False, help="The email to login learnware client") - parser.add_argument("--token", type=str, required=False, help="The token to login learnware client") - args = parser.parse_args() +def suite(): + _suite = unittest.TestSuite() + _suite.addTest(TestAllLearnware("test_all_learnware", email=None, token=None)) + return _suite + +if __name__ == "__main__": runner = unittest.TextTestRunner() - runner.run(parametrize(TestAllLearnware, email=args.email, token=args.token)) \ No newline at end of file + runner.run(suite()) diff --git a/tests/test_learnware_client/test_check_learnware.py b/tests/test_learnware_client/test_check_learnware.py index 59f0820..1979026 100644 --- a/tests/test_learnware_client/test_check_learnware.py +++ b/tests/test_learnware_client/test_check_learnware.py @@ -4,10 +4,8 @@ import zipfile import unittest import tempfile - from learnware.client import LearnwareClient - class TestCheckLearnware(unittest.TestCase): def setUp(self): unittest.TestCase.setUpClass() @@ -68,6 +66,15 @@ class TestCheckLearnware(unittest.TestCase): semantic_spec = json.load(json_file) LearnwareClient.check_learnware(self.zip_path, semantic_spec) +def suite(): + _suite = unittest.TestSuite() + _suite.addTest(TestCheckLearnware("test_check_learnware_pip")) + _suite.addTest(TestCheckLearnware("test_check_learnware_conda")) + _suite.addTest(TestCheckLearnware("test_check_learnware_dependency")) + _suite.addTest(TestCheckLearnware("test_check_learnware_image")) + _suite.addTest(TestCheckLearnware("test_check_learnware_text")) + return _suite if __name__ == "__main__": - unittest.main() + runner = unittest.TextTestRunner() + runner.run(suite()) \ No newline at end of file diff --git a/tests/test_learnware_client/test_container.py b/tests/test_learnware_client/test_container.py index c96d2ab..861e0eb 100644 --- a/tests/test_learnware_client/test_container.py +++ b/tests/test_learnware_client/test_container.py @@ -1,12 +1,8 @@ -import os import unittest -import argparse import numpy as np -from learnware.learnware import get_learnware_from_dirpath from learnware.client import LearnwareClient -from learnware.client.container import ModelCondaContainer, LearnwaresContainer -from learnware.tests import parametrize +from learnware.client.container import LearnwaresContainer class TestContainer(unittest.TestCase): def __init__(self, method_name='runTest', mode="all"): @@ -44,11 +40,12 @@ class TestContainer(unittest.TestCase): for mode in self.modes: self._test_container_with_conda(mode=mode) -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("--mode", type=str, required=False, default="all", help="The mode to run container, must be in ['all', 'conda', 'docker']") - args = parser.parse_args() +def suite(): + _suite = unittest.TestSuite() + _suite.addTest(TestContainer("test_container_with_pip", mode="all")) + _suite.addTest(TestContainer("test_container_with_conda", mode="all")) + return _suite - assert args.mode in {"all", "conda", "docker"}, f"The mode must be in ['all', 'conda', 'docker'], instead of '{args.mode}'" +if __name__ == "__main__": runner = unittest.TextTestRunner() - runner.run(parametrize(TestContainer, mode=args.mode)) \ No newline at end of file + runner.run(suite()) \ No newline at end of file diff --git a/tests/test_learnware_client/test_load_learnware.py b/tests/test_learnware_client/test_load_learnware.py index 0c20939..63f9856 100644 --- a/tests/test_learnware_client/test_load_learnware.py +++ b/tests/test_learnware_client/test_load_learnware.py @@ -1,18 +1,12 @@ import os import unittest -import argparse import numpy as np -import learnware -from learnware.learnware import get_learnware_from_dirpath from learnware.client import LearnwareClient -from learnware.client.container import ModelCondaContainer, LearnwaresContainer from learnware.reuse import AveragingReuser -from learnware.tests import parametrize - class TestLearnwareLoad(unittest.TestCase): - def __init__(self, method_name='runTest', mode="conda"): + def __init__(self, method_name='runTest', mode="all"): super(TestLearnwareLoad, self).__init__(method_name) self.runnable_options = [] if mode in {"all", "conda"}: @@ -56,11 +50,12 @@ class TestLearnwareLoad(unittest.TestCase): self._test_load_learnware_by_id(runnable_option=runnable_option) -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("--mode", type=str, required=False, default="conda", help="The mode to load learnware, must be in ['all', 'conda', 'docker']") - args = parser.parse_args() +def suite(): + _suite = unittest.TestSuite() + _suite.addTest(TestLearnwareLoad("test_load_learnware_by_zippath", mode="all")) + _suite.addTest(TestLearnwareLoad("test_load_learnware_by_id", mode="all")) + return _suite - assert args.mode in {"all", "conda", "docker"}, f"The mode must be in ['all', 'conda', 'docker'], instead of '{args.mode}'" +if __name__ == "__main__": runner = unittest.TextTestRunner() - runner.run(parametrize(TestLearnwareLoad, mode=args.mode)) \ No newline at end of file + runner.run(suite()) \ No newline at end of file diff --git a/tests/test_learnware_client/test_upload.py b/tests/test_learnware_client/test_upload.py index 8bcd988..663a29d 100644 --- a/tests/test_learnware_client/test_upload.py +++ b/tests/test_learnware_client/test_upload.py @@ -5,7 +5,6 @@ import tempfile from learnware.client import LearnwareClient from learnware.specification import generate_semantic_spec -from learnware.tests import parametrize class TestUpload(unittest.TestCase): client = LearnwareClient() @@ -60,11 +59,11 @@ class TestUpload(unittest.TestCase): assert learnware_id not in uploaded_ids -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("--email", type=str, required=False, help="The email to login learnware client") - parser.add_argument("--token", type=str, required=False, help="The token to login learnware client") - args = parser.parse_args() +def suite(): + _suite = unittest.TestSuite() + _suite.addTest(TestUpload("test_upload", email=None, token=None)) + return _suite +if __name__ == "__main__": runner = unittest.TextTestRunner() - runner.run(parametrize(TestUpload, email=args.email, token=args.token)) \ No newline at end of file + runner.run(suite()) diff --git a/tests/test_reuse/test_averaging.py b/tests/test_reuse/test_averaging.py deleted file mode 100644 index de4dde5..0000000 --- a/tests/test_reuse/test_averaging.py +++ /dev/null @@ -1,43 +0,0 @@ -import os -import json -import string -import random -import torch -import unittest -import tempfile -import numpy as np - -from learnware.specification import RKMETableSpecification, HeteroMapTableSpecification -from learnware.specification import generate_stat_spec -from learnware.market.heterogeneous.organizer import HeteroMap - -class TestAveragingReuse(unittest.TestCase): - - def setUp(self): - self.hetero_map = HeteroMap() - - def _test_hetero_spec(self, X): - rkme: RKMETableSpecification = generate_stat_spec(type="table", X=X) - hetero_spec = self.hetero_map.hetero_mapping(rkme_spec=rkme, features=dict()) - with tempfile.TemporaryDirectory(prefix="learnware_") as tempdir: - rkme_path = os.path.join(tempdir, "rkme.json") - hetero_spec.save(rkme_path) - - with open(rkme_path, "r") as f: - data = json.load(f) - assert data["type"] == "HeteroMapTableSpecification" - - rkme2 = HeteroMapTableSpecification() - rkme2.load(rkme_path) - assert rkme2.type == "HeteroMapTableSpecification" - - - def test_hetero_rkme(self): - self._test_hetero_spec(np.random.uniform(-10000, 10000, size=(5000, 200))) - self._test_hetero_spec(np.random.uniform(-10000, 10000, size=(10000, 100))) - self._test_hetero_spec(np.random.uniform(-10000, 10000, size=(5, 20))) - self._test_hetero_spec(np.random.uniform(-10000, 10000, size=(1, 50))) - self._test_hetero_spec(np.random.uniform(-10000, 10000, size=(100, 150))) - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_search_learnware/test_search_image.py b/tests/test_search_learnware/test_search_image.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_search_learnware/test_search_text.py b/tests/test_search_learnware/test_search_text.py new file mode 100644 index 0000000..e69de29 From 6e2fc53664b4409e91276e66e94bc135c72ceaaf Mon Sep 17 00:00:00 2001 From: bxdd Date: Tue, 5 Dec 2023 19:35:40 +0800 Subject: [PATCH 22/43] [MNT] now the test_search works well --- learnware/client/learnware_client.py | 11 ++- learnware/market/base.py | 2 +- .../heterogeneous/organizer/__init__.py | 2 +- learnware/market/heterogeneous/utils.py | 15 ++-- learnware/market/module.py | 10 ++- tests/test_procedure/test_search.py | 84 +++++++++++++++++++ .../test_search_image.py | 0 .../test_search_learnware/test_search_text.py | 0 8 files changed, 110 insertions(+), 14 deletions(-) create mode 100644 tests/test_procedure/test_search.py delete mode 100644 tests/test_search_learnware/test_search_image.py delete mode 100644 tests/test_search_learnware/test_search_text.py diff --git a/learnware/client/learnware_client.py b/learnware/client/learnware_client.py index 07d4c6b..b46242c 100644 --- a/learnware/client/learnware_client.py +++ b/learnware/client/learnware_client.py @@ -70,7 +70,14 @@ class LearnwareClient: self.tempdir_list = [] self.login_status = False atexit.register(self.cleanup) - + + def is_connected(self): + url = f"{self.host}/auth/login_by_token" + response = requests.post(url) + if response.status_code == 404: + return False + return True + def login(self, email, token): url = f"{self.host}/auth/login_by_token" @@ -172,7 +179,7 @@ class LearnwareClient: if result["code"] != 0: raise Exception("update failed: " + json.dumps(result)) - def download_learnware(self, learnware_id, save_path): + def download_learnware(self, learnware_id: str, save_path: str): url = f"{self.host}/engine/download_learnware" response = requests.get( diff --git a/learnware/market/base.py b/learnware/market/base.py index 78d06e6..0b90266 100644 --- a/learnware/market/base.py +++ b/learnware/market/base.py @@ -132,7 +132,7 @@ class LearnwareMarket: def check_learnware(self, zip_path: str, semantic_spec: dict, checker_names: List[str] = None, **kwargs) -> bool: try: final_status = BaseChecker.NONUSABLE_LEARNWARE - if len(checker_names): + if checker_names is not None and len(checker_names): with tempfile.TemporaryDirectory(prefix="pending_learnware_") as tempdir: with zipfile.ZipFile(zip_path, mode="r") as z_file: z_file.extractall(tempdir) diff --git a/learnware/market/heterogeneous/organizer/__init__.py b/learnware/market/heterogeneous/organizer/__init__.py index 0258eea..1e962cb 100644 --- a/learnware/market/heterogeneous/organizer/__init__.py +++ b/learnware/market/heterogeneous/organizer/__init__.py @@ -245,7 +245,7 @@ class HeteroMapTableOrganizer(EasyOrganizer): ret = [] for idx in ids: spec = self.learnware_list[idx].get_specification() - if is_hetero(stat_specs=spec.get_stat_spec(), semantic_spec=spec.get_semantic_spec()): + if is_hetero(stat_specs=spec.get_stat_spec(), semantic_spec=spec.get_semantic_spec(), verbose=False): ret.append(idx) return ret diff --git a/learnware/market/heterogeneous/utils.py b/learnware/market/heterogeneous/utils.py index 6732991..8f2c5a2 100644 --- a/learnware/market/heterogeneous/utils.py +++ b/learnware/market/heterogeneous/utils.py @@ -1,9 +1,10 @@ +import traceback from ...logger import get_module_logger logger = get_module_logger("hetero_utils") -def is_hetero(stat_specs: dict, semantic_spec: dict) -> bool: +def is_hetero(stat_specs: dict, semantic_spec: dict, verbose=True) -> bool: """Check if user_info satifies all the criteria required for enabling heterogeneous learnware search Parameters @@ -35,15 +36,17 @@ def is_hetero(stat_specs: dict, semantic_spec: dict) -> bool: semantic_decription_feature_num = len(semantic_input_description["Description"]) if semantic_decription_feature_num <= 0: - logger.warning("At least one of Input.Description in semantic spec should be provides.") + if verbose: + logger.warning("At least one of Input.Description in semantic spec should be provides.") return False if table_input_shape != semantic_description_dim: - logger.warning("User data feature dimensions mismatch with semantic specification.") + if verbose: + logger.warning("User data feature dimensions mismatch with semantic specification.") return False return True - - except Exception as e: - logger.warning(f"Invalid heterogeneous search information provided due to {e}. Use homogeneous search instead.") + except Exception as err: + if verbose: + logger.warning(f"Invalid heterogeneous search information provided.") return False diff --git a/learnware/market/module.py b/learnware/market/module.py index 70fd340..f06a20c 100644 --- a/learnware/market/module.py +++ b/learnware/market/module.py @@ -1,9 +1,10 @@ from .base import LearnwareMarket +from .classes import CondaChecker from .easy import EasyOrganizer, EasySearcher, EasySemanticChecker, EasyStatChecker from .heterogeneous import HeteroMapTableOrganizer, HeteroSearcher -def get_market_component(name, market_id, rebuild, organizer_kwargs=None, searcher_kwargs=None, checker_kwargs=None): +def get_market_component(name, market_id, rebuild, organizer_kwargs=None, searcher_kwargs=None, checker_kwargs=None, conda_checker=False): organizer_kwargs = {} if organizer_kwargs is None else organizer_kwargs searcher_kwargs = {} if searcher_kwargs is None else searcher_kwargs checker_kwargs = {} if checker_kwargs is None else checker_kwargs @@ -11,7 +12,7 @@ def get_market_component(name, market_id, rebuild, organizer_kwargs=None, search if name == "easy": easy_organizer = EasyOrganizer(market_id=market_id, rebuild=rebuild) easy_searcher = EasySearcher(organizer=easy_organizer) - easy_checker_list = [EasySemanticChecker(), EasyStatChecker()] + easy_checker_list = [EasySemanticChecker(), EasyStatChecker() if conda_checker is False else CondaChecker(EasyStatChecker())] market_component = { "organizer": easy_organizer, "searcher": easy_searcher, @@ -20,7 +21,7 @@ def get_market_component(name, market_id, rebuild, organizer_kwargs=None, search elif name == "hetero": hetero_organizer = HeteroMapTableOrganizer(market_id=market_id, rebuild=rebuild, **organizer_kwargs) hetero_searcher = HeteroSearcher(organizer=hetero_organizer) - hetero_checker_list = [EasySemanticChecker(), EasyStatChecker()] + hetero_checker_list = [EasySemanticChecker(), EasyStatChecker() if conda_checker is False else CondaChecker(EasyStatChecker())] market_component = { "organizer": hetero_organizer, @@ -40,9 +41,10 @@ def instantiate_learnware_market( organizer_kwargs: dict = None, searcher_kwargs: dict = None, checker_kwargs: dict = None, + conda_checker: bool = False, **kwargs, ): - market_componets = get_market_component(name, market_id, rebuild, organizer_kwargs, searcher_kwargs, checker_kwargs) + market_componets = get_market_component(name, market_id, rebuild, organizer_kwargs, searcher_kwargs, checker_kwargs, conda_checker) return LearnwareMarket( organizer=market_componets["organizer"], searcher=market_componets["searcher"], diff --git a/tests/test_procedure/test_search.py b/tests/test_procedure/test_search.py new file mode 100644 index 0000000..9aa1b42 --- /dev/null +++ b/tests/test_procedure/test_search.py @@ -0,0 +1,84 @@ +import os +import unittest +import tempfile +import logging + +import learnware +learnware.init(logging_level=logging.WARNING) + +from learnware.learnware import Learnware +from learnware.client import LearnwareClient +from learnware.market import instantiate_learnware_market, BaseUserInfo, EasySemanticChecker +from learnware.config import C + +class TestSearch(unittest.TestCase): + client = LearnwareClient() + + @classmethod + def setUpClass(cls): + cls.market = instantiate_learnware_market(market_id="search_test", name="hetero", rebuild=True) + if cls.client.is_connected(): + cls._build_learnware_market() + + @classmethod + def _build_learnware_market(cls): + table_learnware_ids = ["00001951", "00001980", "00001987"] + image_learnware_ids = ["00000851", "00000858", "00000841"] + text_learnware_ids = ["00000652", "00000637"] + learnware_ids = table_learnware_ids + image_learnware_ids + text_learnware_ids + with tempfile.TemporaryDirectory(prefix="learnware_search_test") as tempdir: + for learnware_id in learnware_ids: + learnware_zippath = os.path.join(tempdir, f"learnware_{learnware_id}.zip") + try: + cls.client.download_learnware(learnware_id=learnware_id, save_path=learnware_zippath) + semantic_spec = cls.client.load_learnware(learnware_path=learnware_zippath).get_specification().get_semantic_spec() + except Exception: + print("'learnware_id' is passed due to the network problem.") + cls.market.add_learnware(learnware_zippath, learnware_id=learnware_id, semantic_spec=semantic_spec, checker_names=["EasySemanticChecker"]) + + @unittest.skipIf(not client.is_connected(), "Client can not connect!") + def test_image_search(self): + learnware_id = "00000619" + try: + learnware: Learnware = self.client.load_learnware(learnware_id=learnware_id) + except Exception: + print("'test_image_search' is passed due to the network problem.") + user_info = BaseUserInfo(stat_info=learnware.get_specification().get_stat_spec()) + search_result = self.market.search_learnware(user_info) + print("Single Search Results:", search_result.get_single_results()) + print("Multiple Search Results:", search_result.get_multiple_results()) + + @unittest.skipIf(not client.is_connected(), "Client can not connect!") + def test_text_search(self): + learnware_id = "00000653" + try: + learnware: Learnware = self.client.load_learnware(learnware_id=learnware_id) + except Exception: + print("'test_text_search' is passed due to the network problem.") + user_info = BaseUserInfo(stat_info=learnware.get_specification().get_stat_spec()) + search_result = self.market.search_learnware(user_info) + print("Single Search Results:", search_result.get_single_results()) + print("Multiple Search Results:", search_result.get_multiple_results()) + + @unittest.skipIf(not client.is_connected(), "Client can not connect!") + def test_table_search(self): + learnware_id = "00001950" + try: + learnware: Learnware = self.client.load_learnware(learnware_id=learnware_id) + except Exception: + print("'test_table_search' is passed due to the network problem.") + user_info = BaseUserInfo(stat_info=learnware.get_specification().get_stat_spec()) + search_result = self.market.search_learnware(user_info) + print("Single Search Results:", search_result.get_single_results()) + print("Multiple Search Results:", search_result.get_multiple_results()) + +def suite(): + _suite = unittest.TestSuite() + _suite.addTest(TestSearch("test_image_search")) + _suite.addTest(TestSearch("test_text_search")) + _suite.addTest(TestSearch("test_table_search")) + return _suite + +if __name__ == "__main__": + runner = unittest.TextTestRunner() + runner.run(suite()) \ No newline at end of file diff --git a/tests/test_search_learnware/test_search_image.py b/tests/test_search_learnware/test_search_image.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/test_search_learnware/test_search_text.py b/tests/test_search_learnware/test_search_text.py deleted file mode 100644 index e69de29..0000000 From 85c150c6b701b2d72bf6195ca35add9f90c131d7 Mon Sep 17 00:00:00 2001 From: bxdd Date: Tue, 5 Dec 2023 19:52:16 +0800 Subject: [PATCH 23/43] [MNT] rename test subfolder --- tests/test_function/test_search.py | 84 ++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 tests/test_function/test_search.py diff --git a/tests/test_function/test_search.py b/tests/test_function/test_search.py new file mode 100644 index 0000000..9aa1b42 --- /dev/null +++ b/tests/test_function/test_search.py @@ -0,0 +1,84 @@ +import os +import unittest +import tempfile +import logging + +import learnware +learnware.init(logging_level=logging.WARNING) + +from learnware.learnware import Learnware +from learnware.client import LearnwareClient +from learnware.market import instantiate_learnware_market, BaseUserInfo, EasySemanticChecker +from learnware.config import C + +class TestSearch(unittest.TestCase): + client = LearnwareClient() + + @classmethod + def setUpClass(cls): + cls.market = instantiate_learnware_market(market_id="search_test", name="hetero", rebuild=True) + if cls.client.is_connected(): + cls._build_learnware_market() + + @classmethod + def _build_learnware_market(cls): + table_learnware_ids = ["00001951", "00001980", "00001987"] + image_learnware_ids = ["00000851", "00000858", "00000841"] + text_learnware_ids = ["00000652", "00000637"] + learnware_ids = table_learnware_ids + image_learnware_ids + text_learnware_ids + with tempfile.TemporaryDirectory(prefix="learnware_search_test") as tempdir: + for learnware_id in learnware_ids: + learnware_zippath = os.path.join(tempdir, f"learnware_{learnware_id}.zip") + try: + cls.client.download_learnware(learnware_id=learnware_id, save_path=learnware_zippath) + semantic_spec = cls.client.load_learnware(learnware_path=learnware_zippath).get_specification().get_semantic_spec() + except Exception: + print("'learnware_id' is passed due to the network problem.") + cls.market.add_learnware(learnware_zippath, learnware_id=learnware_id, semantic_spec=semantic_spec, checker_names=["EasySemanticChecker"]) + + @unittest.skipIf(not client.is_connected(), "Client can not connect!") + def test_image_search(self): + learnware_id = "00000619" + try: + learnware: Learnware = self.client.load_learnware(learnware_id=learnware_id) + except Exception: + print("'test_image_search' is passed due to the network problem.") + user_info = BaseUserInfo(stat_info=learnware.get_specification().get_stat_spec()) + search_result = self.market.search_learnware(user_info) + print("Single Search Results:", search_result.get_single_results()) + print("Multiple Search Results:", search_result.get_multiple_results()) + + @unittest.skipIf(not client.is_connected(), "Client can not connect!") + def test_text_search(self): + learnware_id = "00000653" + try: + learnware: Learnware = self.client.load_learnware(learnware_id=learnware_id) + except Exception: + print("'test_text_search' is passed due to the network problem.") + user_info = BaseUserInfo(stat_info=learnware.get_specification().get_stat_spec()) + search_result = self.market.search_learnware(user_info) + print("Single Search Results:", search_result.get_single_results()) + print("Multiple Search Results:", search_result.get_multiple_results()) + + @unittest.skipIf(not client.is_connected(), "Client can not connect!") + def test_table_search(self): + learnware_id = "00001950" + try: + learnware: Learnware = self.client.load_learnware(learnware_id=learnware_id) + except Exception: + print("'test_table_search' is passed due to the network problem.") + user_info = BaseUserInfo(stat_info=learnware.get_specification().get_stat_spec()) + search_result = self.market.search_learnware(user_info) + print("Single Search Results:", search_result.get_single_results()) + print("Multiple Search Results:", search_result.get_multiple_results()) + +def suite(): + _suite = unittest.TestSuite() + _suite.addTest(TestSearch("test_image_search")) + _suite.addTest(TestSearch("test_text_search")) + _suite.addTest(TestSearch("test_table_search")) + return _suite + +if __name__ == "__main__": + runner = unittest.TextTestRunner() + runner.run(suite()) \ No newline at end of file From 83c2d33aaf7489ed2e1ca2323ebb5d6124a2c81b Mon Sep 17 00:00:00 2001 From: bxdd Date: Tue, 5 Dec 2023 20:44:40 +0800 Subject: [PATCH 24/43] [MNT] modify GetData --- learnware/tests/data.py | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/learnware/tests/data.py b/learnware/tests/data.py index 422392c..30c6c81 100644 --- a/learnware/tests/data.py +++ b/learnware/tests/data.py @@ -1,3 +1,40 @@ +import json +import requests +from tqdm import tqdm + +from ..config import C + + class GetData: - pass \ No newline at end of file + def __init__(self, host=None, chunk_size=1024 * 1024): + self.headers = None + + if host is None: + self.host = C.backend_host + else: + self.host = host + + self.chunk_size = chunk_size + + def download_file(self, file_path: str, save_path: str): + url = f"{self.host}/engine/download" + + response = requests.get( + url, + params={ + "file_path": file_path, + }, + stream=True, + ) + + if response.status_code != 200: + raise Exception("download failed: " + json.dumps(response.json())) + + num_chunks = int(response.headers["Content-Length"]) // self.chunk_size + 1 + bar = tqdm(total=num_chunks, desc="Downloading", unit="MB") + + with open(save_path, "wb") as f: + for chunk in response.iter_content(chunk_size=self.chunk_size): + f.write(chunk) + bar.update(1) \ No newline at end of file From 436da5ced9371b98c5dd03aa4611c13198514af0 Mon Sep 17 00:00:00 2001 From: bxdd Date: Tue, 5 Dec 2023 23:55:50 +0800 Subject: [PATCH 25/43] [MNT] finish_benchmark --- learnware/tests/benchmark.py | 12 --- learnware/tests/benchmarks/__init__.py | 120 +++++++++++++++++++++++++ learnware/tests/benchmarks/config.py | 19 ++++ tests/test_procedure/test_search.py | 84 ----------------- 4 files changed, 139 insertions(+), 96 deletions(-) delete mode 100644 learnware/tests/benchmark.py create mode 100644 learnware/tests/benchmarks/__init__.py create mode 100644 learnware/tests/benchmarks/config.py delete mode 100644 tests/test_procedure/test_search.py diff --git a/learnware/tests/benchmark.py b/learnware/tests/benchmark.py deleted file mode 100644 index c4ed049..0000000 --- a/learnware/tests/benchmark.py +++ /dev/null @@ -1,12 +0,0 @@ -from dataclasses import dataclass, field -from typing import Optional, List -from ..learnware import Learnware -@dataclass -class BenchmarkConfig: - datasert_url: str - learnware_ids: List[str] - userdata_url: Optional[str] = None - - -class LearnwareBenchmark: - pass diff --git a/learnware/tests/benchmarks/__init__.py b/learnware/tests/benchmarks/__init__.py new file mode 100644 index 0000000..d67873a --- /dev/null +++ b/learnware/tests/benchmarks/__init__.py @@ -0,0 +1,120 @@ +import os +import pickle +import atexit +import tempfile +import zipfile +from dataclasses import dataclass, field +from typing import Optional, List, Union, Tuple + +from .config import OnlineBenchmark, online_benchmarks +from ..data import GetData +@dataclass +class Benchmark: + learnware_ids: List[str] + user_num: int + unlabeled_feature_paths: List[str] + unlabeled_groudtruths_paths: List[str] + labeled_feature_paths: Optional[List[str]] = None + labeled_label_paths: Optional[List[str]] = None + + # TODO: add more method for benchmark + + def get_unlabeled_data(self, user_ids: Union[str, List[str]]): + if isinstance(user_ids, str): + user_ids = [user_ids] + + ret = [] + for user_id in user_ids: + with open(self.unlabeled_feature_paths[user_id], "rb") as fin: + unlabeled_feature = pickle.load(fin) + + with open(self.unlabeled_groudtruths_paths[user_id], "rb") as fin: + unlabeled_groudtruth = pickle.load(fin) + + ret.append((unlabeled_feature, unlabeled_groudtruth)) + + return ret + + def get_labeled_data(self, user_ids): + if isinstance(user_ids, str): + user_ids = [user_ids] + + ret = [] + for user_id in user_ids: + with open(self.labeled_feature_paths[user_id], "rb") as fin: + labeled_feature = pickle.load(fin) + + with open(self.labeled_label_paths[user_id], "rb") as fin: + labeled_groudtruth = pickle.load(fin) + + ret.append((labeled_feature, labeled_groudtruth)) + + return ret + + +class LearnwareBenchmark: + + def __init__(self): + self.online_benchmarks = online_benchmarks + self.tempdir_list = [] + atexit.register(self.cleanup) + + def list_benchmarks(self): + return list(self.online_benchmarks.keys()) + + def get_benchmark(self, online_benchmark: Union[str, OnlineBenchmark]): + if isinstance(online_benchmark, str): + online_benchmark = self.online_benchmarks[online_benchmark] + + self.tempdir_list.append(tempfile.TemporaryDirectory(prefix="learnware_benchmark")) + save_folder = self.tempdir_list[-1].name + + unlabeled_data_localpath = os.path.join(save_folder, "unlabeled_data.zip") + GetData().download_file(online_benchmark.unlabeled_data_path, unlabeled_data_localpath) + + unlabeled_feature_paths = [] + unlabeled_groudtruth_paths = [] + + with zipfile.ZipFile(unlabeled_data_localpath, "r") as z_file: + unlabeled_data_dirpath = os.path.join(save_folder, "unlabeled_data") + z_file.extractall(unlabeled_data_dirpath) + for user_id in range(online_benchmark.user_num): + user_feature_filepath = os.path.isfile(os.path.join(unlabeled_data_dirpath, f"user{user_id}_feature.pkl")) + user_groudtruth_filepath = os.path.isfile(os.path.join(unlabeled_data_dirpath, f"user{user_id}_groudtruth.pkl")) + assert os.path.isfile(user_feature_filepath), f"user {user_id} unlabeled feature is not valid!" + assert os.path.isfile(user_groudtruth_filepath), f"user {user_id} unlabeled groudtruth is not valid!" + unlabeled_feature_paths.append(user_feature_filepath) + unlabeled_groudtruth_paths.append(user_groudtruth_filepath) + + labeled_feature_paths = None + labeled_label_paths = None + if online_benchmark.labeled_data_path is not None: + labeled_data_localpath = os.path.join(save_folder, "labeled_data.zip") + GetData().download_file(online_benchmark.labeled_data_path, labeled_data_localpath) + + labeled_feature_paths = [] + labeled_label_paths = [] + + with zipfile.ZipFile(labeled_data_localpath, "r") as z_file: + labeled_data_dirpath = os.path.join(save_folder, "labeled_data") + z_file.extractall(labeled_data_dirpath) + for user_id in range(online_benchmark.user_num): + user_feature_filepath = os.path.isfile(os.path.join(labeled_data_dirpath, f"user{user_id}_feature.pkl")) + user_groudtruth_filepath = os.path.isfile(os.path.join(labeled_data_dirpath, f"user{user_id}_label.pkl")) + assert os.path.isfile(user_feature_filepath), f"user {user_id} labeled feature is not valid!" + assert os.path.isfile(user_groudtruth_filepath), f"user {user_id} labeled label is not valid!" + labeled_feature_paths.append(user_feature_filepath) + labeled_label_paths.append(user_groudtruth_filepath) + + return Benchmark( + learnware_ids=online_benchmark.learnware_ids, + user_num=online_benchmark.user_num, + unlabeled_feature_paths=unlabeled_feature_paths, + unlabeled_groudtruths_paths=unlabeled_groudtruth_paths, + labeled_feature_paths=labeled_feature_paths, + labeled_label_paths=labeled_label_paths, + ) + + def cleanup(self): + for tempdir in self.tempdir_list: + tempdir.cleanup() \ No newline at end of file diff --git a/learnware/tests/benchmarks/config.py b/learnware/tests/benchmarks/config.py new file mode 100644 index 0000000..40f7ba3 --- /dev/null +++ b/learnware/tests/benchmarks/config.py @@ -0,0 +1,19 @@ +from dataclasses import dataclass, field +from typing import Optional, List +from ...learnware import Learnware + +@dataclass +class OnlineBenchmark: + learnware_ids: List[str] + user_num: int + unlabeled_data_path: str + labeled_data_path: Optional[str] = None + +online_benchmarks = { + "example": OnlineBenchmark( + learnware_ids=["00001951", "00001980", "00001987"], + user_num=3, + unlabeled_data_path="example_path1", + labeled_data_path="example_path2" + ) +} \ No newline at end of file diff --git a/tests/test_procedure/test_search.py b/tests/test_procedure/test_search.py deleted file mode 100644 index 9aa1b42..0000000 --- a/tests/test_procedure/test_search.py +++ /dev/null @@ -1,84 +0,0 @@ -import os -import unittest -import tempfile -import logging - -import learnware -learnware.init(logging_level=logging.WARNING) - -from learnware.learnware import Learnware -from learnware.client import LearnwareClient -from learnware.market import instantiate_learnware_market, BaseUserInfo, EasySemanticChecker -from learnware.config import C - -class TestSearch(unittest.TestCase): - client = LearnwareClient() - - @classmethod - def setUpClass(cls): - cls.market = instantiate_learnware_market(market_id="search_test", name="hetero", rebuild=True) - if cls.client.is_connected(): - cls._build_learnware_market() - - @classmethod - def _build_learnware_market(cls): - table_learnware_ids = ["00001951", "00001980", "00001987"] - image_learnware_ids = ["00000851", "00000858", "00000841"] - text_learnware_ids = ["00000652", "00000637"] - learnware_ids = table_learnware_ids + image_learnware_ids + text_learnware_ids - with tempfile.TemporaryDirectory(prefix="learnware_search_test") as tempdir: - for learnware_id in learnware_ids: - learnware_zippath = os.path.join(tempdir, f"learnware_{learnware_id}.zip") - try: - cls.client.download_learnware(learnware_id=learnware_id, save_path=learnware_zippath) - semantic_spec = cls.client.load_learnware(learnware_path=learnware_zippath).get_specification().get_semantic_spec() - except Exception: - print("'learnware_id' is passed due to the network problem.") - cls.market.add_learnware(learnware_zippath, learnware_id=learnware_id, semantic_spec=semantic_spec, checker_names=["EasySemanticChecker"]) - - @unittest.skipIf(not client.is_connected(), "Client can not connect!") - def test_image_search(self): - learnware_id = "00000619" - try: - learnware: Learnware = self.client.load_learnware(learnware_id=learnware_id) - except Exception: - print("'test_image_search' is passed due to the network problem.") - user_info = BaseUserInfo(stat_info=learnware.get_specification().get_stat_spec()) - search_result = self.market.search_learnware(user_info) - print("Single Search Results:", search_result.get_single_results()) - print("Multiple Search Results:", search_result.get_multiple_results()) - - @unittest.skipIf(not client.is_connected(), "Client can not connect!") - def test_text_search(self): - learnware_id = "00000653" - try: - learnware: Learnware = self.client.load_learnware(learnware_id=learnware_id) - except Exception: - print("'test_text_search' is passed due to the network problem.") - user_info = BaseUserInfo(stat_info=learnware.get_specification().get_stat_spec()) - search_result = self.market.search_learnware(user_info) - print("Single Search Results:", search_result.get_single_results()) - print("Multiple Search Results:", search_result.get_multiple_results()) - - @unittest.skipIf(not client.is_connected(), "Client can not connect!") - def test_table_search(self): - learnware_id = "00001950" - try: - learnware: Learnware = self.client.load_learnware(learnware_id=learnware_id) - except Exception: - print("'test_table_search' is passed due to the network problem.") - user_info = BaseUserInfo(stat_info=learnware.get_specification().get_stat_spec()) - search_result = self.market.search_learnware(user_info) - print("Single Search Results:", search_result.get_single_results()) - print("Multiple Search Results:", search_result.get_multiple_results()) - -def suite(): - _suite = unittest.TestSuite() - _suite.addTest(TestSearch("test_image_search")) - _suite.addTest(TestSearch("test_text_search")) - _suite.addTest(TestSearch("test_table_search")) - return _suite - -if __name__ == "__main__": - runner = unittest.TextTestRunner() - runner.run(suite()) \ No newline at end of file From c72bc0799768e5370907260ada8eead12af92d57 Mon Sep 17 00:00:00 2001 From: GeneLiuXe <356340460@qq.com> Date: Wed, 6 Dec 2023 11:45:51 +0800 Subject: [PATCH 26/43] [FIX] fix file bug in search --- learnware/client/learnware_client.py | 75 +++++++++++++++------------- 1 file changed, 39 insertions(+), 36 deletions(-) diff --git a/learnware/client/learnware_client.py b/learnware/client/learnware_client.py index 7a5eaed..cb2cbcd 100644 --- a/learnware/client/learnware_client.py +++ b/learnware/client/learnware_client.py @@ -228,43 +228,46 @@ class LearnwareClient: "matching": None, }, } - with tempfile.NamedTemporaryFile(prefix="learnware_stat_", suffix=".json") as ftemp: + with tempfile.NamedTemporaryFile(prefix="learnware_stat_", suffix=".json", delete=False) as ftemp: + temp_file_name = ftemp.name if stat_spec is not None: - stat_spec.save(ftemp.name) - - with open(ftemp.name, "r") as fin: - semantic_specification = user_info.get_semantic_spec() - if stat_spec is None: - files = None - else: - files = {"statistical_specification": fin} - - response = requests.post( - url, - files=files, - data={ - "semantic_specification": json.dumps(semantic_specification), - "limit": page_size, - "page": page_index, - }, - headers=self.headers, - ) - - result = response.json() - - if result["code"] != 0: - raise Exception("search failed: " + json.dumps(result)) - - for learnware in result["data"]["learnware_list_single"]: - returns["single"]["learnware_ids"].append(learnware["learnware_id"]) - returns["single"]["semantic_specifications"].append(learnware["semantic_specification"]) - returns["single"]["matching"].append(learnware["matching"]) - - if len(result["data"]["learnware_list_multi"]) > 0: - multi_learnware = result["data"]["learnware_list_multi"][0] - returns["multiple"]["learnware_ids"].append(multi_learnware["learnware_id"]) - returns["multiple"]["semantic_specifications"].append(multi_learnware["semantic_specification"]) - returns["multiple"]["matching"] = learnware["matching"] + stat_spec.save(temp_file_name) + + with open(temp_file_name, "r") as fin: + semantic_specification = user_info.get_semantic_spec() + if stat_spec is None: + files = None + else: + files = {"statistical_specification": fin} + + response = requests.post( + url, + files=files, + data={ + "semantic_specification": json.dumps(semantic_specification), + "limit": page_size, + "page": page_index, + }, + headers=self.headers, + ) + result = response.json() + + if result["code"] != 0: + raise Exception("search failed: " + json.dumps(result)) + + for learnware in result["data"]["learnware_list_single"]: + returns["single"]["learnware_ids"].append(learnware["learnware_id"]) + returns["single"]["semantic_specifications"].append(learnware["semantic_specification"]) + returns["single"]["matching"].append(learnware["matching"]) + + if len(result["data"]["learnware_list_multi"]) > 0: + multi_learnware = result["data"]["learnware_list_multi"][0] + returns["multiple"]["learnware_ids"].append(multi_learnware["learnware_id"]) + returns["multiple"]["semantic_specifications"].append(multi_learnware["semantic_specification"]) + returns["multiple"]["matching"] = learnware["matching"] + + # Delete temp json file + os.remove(temp_file_name) return returns From 47f2bbc7a8b6ef21089a914ce113f1d70719b1bb Mon Sep 17 00:00:00 2001 From: GeneLiuXe <356340460@qq.com> Date: Wed, 6 Dec 2023 11:46:12 +0800 Subject: [PATCH 27/43] [FIX] fit with client --- tests/test_learnware_client/test_all_learnware.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/test_learnware_client/test_all_learnware.py b/tests/test_learnware_client/test_all_learnware.py index 276ac00..69108f8 100644 --- a/tests/test_learnware_client/test_all_learnware.py +++ b/tests/test_learnware_client/test_all_learnware.py @@ -30,15 +30,17 @@ class TestAllLearnware(unittest.TestCase): self.client.login(email, token) def test_all_learnware(self): - max_learnware_num = 1000 + max_learnware_num = 2000 semantic_spec = self.client.create_semantic_specification() user_info = BaseUserInfo(semantic_spec=semantic_spec, stat_info={}) result = self.client.search_learnware(user_info, page_size=max_learnware_num) - print(f"result size: {len(result)}") - print(f"key in result: {[key for key in result[0]]}") + + learnware_ids = result["single"]["learnware_ids"] + keys = [key for key in result["single"]["semantic_specifications"][0]] + print(f"result size: {len(learnware_ids)}") + print(f"key in result: {keys}") failed_ids = [] - learnware_ids = [res["learnware_id"] for res in result] with tempfile.TemporaryDirectory(prefix="learnware_") as tempdir: for idx in learnware_ids: zip_path = os.path.join(tempdir, f"test_{idx}.zip") From e24a478af4836ef5f44b6c222b657f28d58cbaf3 Mon Sep 17 00:00:00 2001 From: bxdd Date: Wed, 6 Dec 2023 15:49:43 +0800 Subject: [PATCH 28/43] [MNT] add extra_info for benchmark --- learnware/tests/benchmarks/__init__.py | 16 +++++++++++++--- learnware/tests/benchmarks/config.py | 4 +++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/learnware/tests/benchmarks/__init__.py b/learnware/tests/benchmarks/__init__.py index d67873a..715037d 100644 --- a/learnware/tests/benchmarks/__init__.py +++ b/learnware/tests/benchmarks/__init__.py @@ -16,6 +16,7 @@ class Benchmark: unlabeled_groudtruths_paths: List[str] labeled_feature_paths: Optional[List[str]] = None labeled_label_paths: Optional[List[str]] = None + extra_info_path: Optional[str] = None # TODO: add more method for benchmark @@ -36,8 +37,11 @@ class Benchmark: return ret def get_labeled_data(self, user_ids): + if self.labeled_feature_paths is None or self.labeled_label_paths is None: + return None + if isinstance(user_ids, str): - user_ids = [user_ids] + user_ids = [user_ids] ret = [] for user_id in user_ids: @@ -50,7 +54,7 @@ class Benchmark: ret.append((labeled_feature, labeled_groudtruth)) return ret - + class LearnwareBenchmark: @@ -105,7 +109,12 @@ class LearnwareBenchmark: assert os.path.isfile(user_groudtruth_filepath), f"user {user_id} labeled label is not valid!" labeled_feature_paths.append(user_feature_filepath) labeled_label_paths.append(user_groudtruth_filepath) - + + extra_zip_localpath = None + if online_benchmark.extra_info_path is not None: + extra_zip_localpath = os.path.join(save_folder, os.path.basename(online_benchmark.extra_info_path)) + GetData().download_file(online_benchmark.extra_info_path, extra_zip_localpath) + return Benchmark( learnware_ids=online_benchmark.learnware_ids, user_num=online_benchmark.user_num, @@ -113,6 +122,7 @@ class LearnwareBenchmark: unlabeled_groudtruths_paths=unlabeled_groudtruth_paths, labeled_feature_paths=labeled_feature_paths, labeled_label_paths=labeled_label_paths, + extra_info_path=extra_zip_localpath, ) def cleanup(self): diff --git a/learnware/tests/benchmarks/config.py b/learnware/tests/benchmarks/config.py index 40f7ba3..a523a55 100644 --- a/learnware/tests/benchmarks/config.py +++ b/learnware/tests/benchmarks/config.py @@ -8,12 +8,14 @@ class OnlineBenchmark: user_num: int unlabeled_data_path: str labeled_data_path: Optional[str] = None + extra_info_path: Optional[str] = None online_benchmarks = { "example": OnlineBenchmark( learnware_ids=["00001951", "00001980", "00001987"], user_num=3, unlabeled_data_path="example_path1", - labeled_data_path="example_path2" + labeled_data_path="example_path2", + extra_info_path="example_path3" ) } \ No newline at end of file From 2ff7e15248b513702379a870fb389bf006806c8a Mon Sep 17 00:00:00 2001 From: liuht Date: Wed, 6 Dec 2023 15:51:20 +0800 Subject: [PATCH 29/43] [FIX] fix wrong delete --- learnware/market/heterogeneous/organizer/hetero_map/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/learnware/market/heterogeneous/organizer/hetero_map/__init__.py b/learnware/market/heterogeneous/organizer/hetero_map/__init__.py index 4ebb0a8..e124bcb 100644 --- a/learnware/market/heterogeneous/organizer/hetero_map/__init__.py +++ b/learnware/market/heterogeneous/organizer/hetero_map/__init__.py @@ -9,7 +9,7 @@ from torch import Tensor, nn from .....utils import allocate_cuda_idx, choose_device from .....specification import HeteroMapTableSpecification, RKMETableSpecification from .feature_extractor import CLSToken, FeatureProcessor, FeatureTokenizer -from .trainer import TransTabCollatorForCL +from .trainer import TransTabCollatorForCL, Trainer class HeteroMap(nn.Module): From dc7f5b100bfb2f5d41ed01a21fda16d9dd10e61f Mon Sep 17 00:00:00 2001 From: Peng Tan Date: Wed, 6 Dec 2023 21:09:32 +0800 Subject: [PATCH 30/43] feat(docs): add checkers --- docs/components/market.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/components/market.rst b/docs/components/market.rst index 2a7893e..cdf10ef 100644 --- a/docs/components/market.rst +++ b/docs/components/market.rst @@ -59,6 +59,8 @@ Framework ====================================== + + Current Markets ====================================== @@ -80,3 +82,17 @@ One important case is that models have different feature spaces. In order to ena Current Checkers ====================================== +The checkers check a learnware object in different aspects, including environment configuration (``CondaChecker``), semantic specifications (``EasySemanticChecker``), and statistical specifications (``EasyStatChecker``). The ``__call__`` method of each checker is designed to be invoked as a function to conduct the respective checks on the learnware and return the outcomes. Currently, we have three checkers, which are described below. + + +CondaChecker Class +------------------ +This checker checks a the environment of the learnware object. It creates a ``LearnwaresContainer`` instance to handle the Learnware and uses ``inner_checker`` to check the Learnware. If an exception occurs, it logs the error and returns ``BaseChecker.INVALID_LEARNWARE`` status and error message. + +EasySemanticChecker Class +------------------------- +This checker checks the semantic specification of a learnware object. It checks if the given semantic specification conforms to predefined standards. It verifies each key in ``semantic_spec`` dictionary against expected types and values. If the check fails, it logs the error and returns ``INVALID_LEARNWARE`` status and error message. + +EasyStatChecker Class +--------------------- +This checker checks the statistical specification and functionality of a learnware object. It performs multiple checks to validate the learnware. It checks for model instantiation, verifies input shape and statistical specifications, and test output shape and dimensions using random generated data. In case of any exceptions, it logs the error and returns ``INVALID_LEARNWARE`` status and error message. \ No newline at end of file From 5d170297d038c05f6b3d3ec69cf873d1d3c93624 Mon Sep 17 00:00:00 2001 From: Peng Tan Date: Wed, 6 Dec 2023 21:55:47 +0800 Subject: [PATCH 31/43] [DOC]: add the market description --- docs/components/market.rst | 58 ++++++-------------------------------- 1 file changed, 8 insertions(+), 50 deletions(-) diff --git a/docs/components/market.rst b/docs/components/market.rst index cdf10ef..9d967e3 100644 --- a/docs/components/market.rst +++ b/docs/components/market.rst @@ -3,62 +3,20 @@ Market ================================ +The learnware market receives high-performance machine learning models from developers, incorporates them into the system, and provides services to users by identifying and reusing learnware to help users solve current tasks. Developers voluntarily submit various learnwares to the learnware market, and the market conducts quality checks and further organization of these learnwares. When users submit task requirements, the learnware market automatically selects whether to recommend a single learnware or a combination of multiple learnwares. -Concepts -====================================== - -In the learnware paradigm, there are three key players: *developers*, *users*, and the *market*. - -* Developers: Typically machine learning experts who create and aim to share or sell their high-performing trained machine learning models. -* Users: Require machine learning services but often possess limited data and lack the necessary knowledge and expertise in machine learning. -* Market: Acquires top-performing trained models from developers, houses them within the marketplace, and offers services to users by identifying and reusing learnwares to help users with their current tasks. - -This process can be broken down into two main stages. - -Submitting Stage ------------------------------- - -During the *submitting stage*, developers can voluntarily submit their trained models to the learnware market. The market will then implement a quality assurance mechanism, such as performance validation, to determine if a submitted model is suitable for acceptance. In a learnware market with millions of models, identifying potentially helpful models for a new user is a challenge. - -Requiring users to submit their own data to the market for model testing is impractical, time-consuming, and costly, as it could lead to data leakage. Straightforward approaches, such as measuring the similarity between user data and the original training data of models, are also infeasible due to privacy and proprietary concerns. Our design operates under the constraint that the learnware market has no access to the original training data from developers or users. Furthermore, it assumes that users have limited knowledge of the models available in the market. - -The solution's crux lies in the *specification*, which is central to the learnware proposal. Once a submitted model is accepted by the learnware market, it is assigned a specification, which conveys the model's specialty and utility without revealing its original training data. For simplicity, consider models as functions that map input domain :math:`\mathcal{X}` to output domain :math:`\mathcal{Y}` with respect to objective 'obj.' These models exist in a functional space :math:`\mathcal{F}: \mathcal{X} \mapsto \mathcal{Y}` with respect to 'obj.' Each model has a specification, and all specifications form a specification space, where those for models that serve similar tasks are situated closely. - -In a learnware market, heterogeneous models may have different :math:`\mathcal{X}`, :math:`\mathcal{Y}`, or objectives. If we refer to the specification space that covers all possible models in all possible functional spaces as the 'specification world' analogously, then each specification space corresponding to one possible functional space can be called a 'specification island.' Designing an elegant specification format that encompasses the entire specification world and allows all possible models to be efficiently and adequately identified is a significant challenge. Currently, we adopt a practical design, where each learnware's specification consists of two parts. - - -Reusing Stage ------------------------------- - -Creating Learnware Specifications -++++++++++++++++++++++++++++++++++++ - -The first part of the learnware specification can be realized by a string consisting of a set of descriptions/tags given by the learnware market. These tags address aspects such as the task, input, output, and objective. Based on the user's provided descriptions/tags, the corresponding specification island can be efficiently and accurately located. The designer of the learnware market can create an initial set of descriptions/tags, which can grow as new models are accepted, and new functional spaces and specification islands are created. - -Merging Specification Islands -+++++++++++++++++++++++++++++++++ - -Specification islands can merge into larger ones. For example, when a new model about :math:`F: \mathcal{X}_1 \cup \mathcal{X}_2 \mapsto \mathcal{Y}` with respect to 'obj' is accepted by the learnware market, two islands can be merged. This is possible because the market can have synthetic data by randomly generating inputs, feeding them to models, and concatenating each input with its corresponding output to construct a dataset reflecting the function of a model. In principle, specification islands can be merged if there are common ingredients in :math:`\mathcal{X}`, :math:`\mathcal{Y}`, and 'obj.' - -Deploying Learnware Models -++++++++++++++++++++++++++++++ - -In the deploying stage, the user submits their requirement to the learnware market, which then identifies and returns helpful learnwares to the user. There are two issues to address: how to identify learnwares matching the user requirement and how to reuse the returned learnwares. - -The learnware market can house thousands or millions of models. Efficiently identifying helpful learnwares is challenging, especially given that the market has no access to the original training data of learnwares or the current user's data. With the specification design mentioned earlier, the market can request users to describe their intentions using a set of descriptions/tags, through a user interface or a learnware description language. Based on this information, the task becomes identifying helpful learnwares in a specification island. - -Reusing Learnwares -++++++++++++++++++++++ - -Once helpful learnwares are identified and delivered to the user, they can be reused in various ways. Users can apply the received learnware directly to their data, use multiple learnwares to create an ensemble, or adapt and polish the received learnware(s) using their own data. Learnwares can also be used as feature augmentors, with their outputs used as augmented features for building the final model. - -Helpful learnwares may be trained for tasks that are not exactly the same as the user's current task. In such cases, users can tackle their tasks in a divide-and-conquer way or reuse the learnwares collectively through measuring the utility of each model on each testing instance. If users find it difficult to express their requirements accurately, they can adapt and polish the received learnwares directly using their own data. - +The learnware market will receive various kinds of learnwares, and learnwares from different feature/label spaces form numerous islands of specifications. All these islands together constitute the "specification world" in the learnware market. The market should discover and establish connections between different islands, and then merge them into a unified specification world. This further organization of learnwares can make the learnware search among all learnwares, not just learnwares which has the same feature space and label space as the user's task requirements. Framework ====================================== +The market class is initialized with a organizer class, a searcher class, and a list of checker classes. + +The organizer class should be able to organize the learnware in the market. It should be able to add, delete, and update learnware. It should also be able to search for learnware based on user requirement. + +The searcher class should be able to search for learnware based on user requirement. It should be able to search for learnware based on user requirement. +The checker class is used for checking the learnware in some standards. It should check the utility of a learnware and is supposed to return the status and a message related to the learnware's check result. Current Markets From 889304cabc033de10cf735ed806ba80cf0244066 Mon Sep 17 00:00:00 2001 From: Peng Tan Date: Wed, 6 Dec 2023 22:03:56 +0800 Subject: [PATCH 32/43] [DOC] modify details --- docs/components/market.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/components/market.rst b/docs/components/market.rst index 9d967e3..01f8880 100644 --- a/docs/components/market.rst +++ b/docs/components/market.rst @@ -5,7 +5,7 @@ Market The learnware market receives high-performance machine learning models from developers, incorporates them into the system, and provides services to users by identifying and reusing learnware to help users solve current tasks. Developers voluntarily submit various learnwares to the learnware market, and the market conducts quality checks and further organization of these learnwares. When users submit task requirements, the learnware market automatically selects whether to recommend a single learnware or a combination of multiple learnwares. -The learnware market will receive various kinds of learnwares, and learnwares from different feature/label spaces form numerous islands of specifications. All these islands together constitute the "specification world" in the learnware market. The market should discover and establish connections between different islands, and then merge them into a unified specification world. This further organization of learnwares can make the learnware search among all learnwares, not just learnwares which has the same feature space and label space as the user's task requirements. +The learnware market will receive various kinds of learnwares, and learnwares from different feature/label spaces form numerous islands of specifications. All these islands together constitute the "specification world" in the learnware market. The market should discover and establish connections between different islands, and then merge them into a unified specification world. This further organization of learnwares can make the learnware search among all learnwares, not just learnwares which has the same feature space and label space with the user's task requirements. Framework ====================================== @@ -40,17 +40,17 @@ One important case is that models have different feature spaces. In order to ena Current Checkers ====================================== -The checkers check a learnware object in different aspects, including environment configuration (``CondaChecker``), semantic specifications (``EasySemanticChecker``), and statistical specifications (``EasyStatChecker``). The ``__call__`` method of each checker is designed to be invoked as a function to conduct the respective checks on the learnware and return the outcomes. Currently, we have three checkers, which are described below. +The checkers check a learnware object in different aspects, including environment configuration (``CondaChecker``), semantic specifications (``EasySemanticChecker``), and statistical specifications (``EasyStatChecker``). The ``__call__`` method of each checker is designed to be invoked as a function to conduct the respective checks on the learnware and return the outcomes. It defines three types of learnwares: INVALID_LEARNWARE denotes the learnware does not pass the check, NONUSABLE_LEARNWARE denotes the learnware pass the check but cannot make prediction due to some env dependency, USABLE_LEARWARE denotes the leanrware pass the check and can make prediction. Currently, we have three checkers, which are described below. CondaChecker Class ------------------ -This checker checks a the environment of the learnware object. It creates a ``LearnwaresContainer`` instance to handle the Learnware and uses ``inner_checker`` to check the Learnware. If an exception occurs, it logs the error and returns ``BaseChecker.INVALID_LEARNWARE`` status and error message. +This checker checks a the environment of the learnware object. It creates a ``LearnwaresContainer`` instance to handle the Learnware and uses ``inner_checker`` to check the Learnware. If an exception occurs, it logs the error and returns ``BaseChecker.NONUSABLE_LEARNWARE`` status and error message. EasySemanticChecker Class ------------------------- -This checker checks the semantic specification of a learnware object. It checks if the given semantic specification conforms to predefined standards. It verifies each key in ``semantic_spec`` dictionary against expected types and values. If the check fails, it logs the error and returns ``INVALID_LEARNWARE`` status and error message. +This checker checks the semantic specification of a learnware object. It checks if the given semantic specification conforms to predefined standards. It verifies each key in predefined dictionary. If the check fails, it logs the error and returns ``NONUSABLE_LEARNWARE`` status and error message. EasyStatChecker Class --------------------- -This checker checks the statistical specification and functionality of a learnware object. It performs multiple checks to validate the learnware. It checks for model instantiation, verifies input shape and statistical specifications, and test output shape and dimensions using random generated data. In case of any exceptions, it logs the error and returns ``INVALID_LEARNWARE`` status and error message. \ No newline at end of file +This checker checks the statistical specification and functionality of a learnware object. It performs multiple checks to validate the learnware. It checks for model instantiation, verifies input shape and statistical specifications, and test output shape using random generated data. In case of any exceptions, it logs the error and returns ``NONUSABLE_LEARNWARE`` status and error message. \ No newline at end of file From 0560f90a0fd65715239159053e821ec348dda2c2 Mon Sep 17 00:00:00 2001 From: bxdd Date: Thu, 7 Dec 2023 20:25:07 +0800 Subject: [PATCH 33/43] [DOC] modify components/market doc --- docs/components/market.rst | 56 ++++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/docs/components/market.rst b/docs/components/market.rst index 01f8880..8421d94 100644 --- a/docs/components/market.rst +++ b/docs/components/market.rst @@ -3,25 +3,52 @@ Market ================================ -The learnware market receives high-performance machine learning models from developers, incorporates them into the system, and provides services to users by identifying and reusing learnware to help users solve current tasks. Developers voluntarily submit various learnwares to the learnware market, and the market conducts quality checks and further organization of these learnwares. When users submit task requirements, the learnware market automatically selects whether to recommend a single learnware or a combination of multiple learnwares. +The ``learnware market`` receives high-performance machine learning models from developers, incorporates them into the system, and provides services to users by identifying and reusing learnware to help users solve current tasks. Developers voluntarily submit various learnwares to the learnware market, and the market conducts quality checks and further organization of these learnwares. When users submit task requirements, the learnware market automatically selects whether to recommend a single learnware or a combination of multiple learnwares. -The learnware market will receive various kinds of learnwares, and learnwares from different feature/label spaces form numerous islands of specifications. All these islands together constitute the "specification world" in the learnware market. The market should discover and establish connections between different islands, and then merge them into a unified specification world. This further organization of learnwares can make the learnware search among all learnwares, not just learnwares which has the same feature space and label space with the user's task requirements. +The ``learnware market`` will receive various kinds of learnwares, and learnwares from different feature/label spaces form numerous islands of specifications. All these islands together constitute the ``specification world`` in the learnware market. The market should discover and establish connections between different islands, and then merge them into a unified specification world. This further organization of learnwares support search learnwares among all learnwares, not just among learnwares which has the same feature space and label space with the user's task requirements. Framework ====================================== -The market class is initialized with a organizer class, a searcher class, and a list of checker classes. +The ``learnware market`` is combined with a ``organizer``, a ``searcher``, and a list of ``checker``s. -The organizer class should be able to organize the learnware in the market. It should be able to add, delete, and update learnware. It should also be able to search for learnware based on user requirement. +The ``organizer`` can store and organize learnwares in the market. It supports ``add``, ``delete``, and ``update`` operations for learnwares. It also provides the interface for ``searcher`` to search learnwares based on user requirement. -The searcher class should be able to search for learnware based on user requirement. It should be able to search for learnware based on user requirement. +The ``searcher`` can search learnwares based on user requirement. The implementation of ``searcher`` is dependent on the concrete implementation and interface for ``organizer``, where usually an ``organizer`` can be compatible with multiple different ``searcher``s. -The checker class is used for checking the learnware in some standards. It should check the utility of a learnware and is supposed to return the status and a message related to the learnware's check result. +The ``checker`` is used for checking the learnware in some standards. It should check the utility of a learnware and is supposed to return the status and a message related to the learnware's check result. Only the learnwares who passed the ``checker`` could be able to be stored and added into the ``learnware market``. + + + +Current Checkers +====================================== + +The ``learnware`` package provide two different implementation of ``market`` where both of them share the same ``checker`` list. So we first introduce the details of ``checker``s. + +The ``checker``s check a learnware object in different aspects, including environment configuration (``CondaChecker``), semantic specifications (``EasySemanticChecker``), and statistical specifications (``EasyStatChecker``). The ``__call__`` method of each checker is designed to be invoked as a function to conduct the respective checks on the learnware and return the outcomes. It defines three types of learnwares: ``INVALID_LEARNWARE`` denotes the learnware does not pass the check, ``NONUSABLE_LEARNWARE`` denotes the learnware pass the check but cannot make prediction, ``USABLE_LEARWARE`` denotes the leanrware pass the check and can make prediction. Currently, we have three ``checker``s, which are described below. + + +``CondaChecker`` +------------------ +This ``checker`` checks a the environment of the learnware object. It creates a ``LearnwaresContainer`` instance to handle the Learnware and uses ``inner_checker`` to check the Learnware. If an exception occurs, it logs the error and returns ``NONUSABLE_LEARNWARE`` status and error message. + + +``EasySemanticChecker`` +------------------------- +This ``checker`` checks the semantic specification of a learnware object. It checks if the given semantic specification conforms to predefined standards. It verifies each key in predefined dictionary. If the check fails, it logs the error and returns ``NONUSABLE_LEARNWARE`` status and error message. + + +``EasyStatChecker`` +--------------------- + +This ``checker`` checks the statistical specification and functionality of a learnware object. It performs multiple checks to validate the learnware. It checks for model instantiation, verifies input shape and statistical specifications, and test output shape using random generated data. In case of any exceptions, it logs the error and returns ``NONUSABLE_LEARNWARE`` status and error message. Current Markets ====================================== +The ``learnware`` package provide two different implementation of ``market``, i.e. ``Easy Market`` and ``Hetero Market``. They have different implementation of ``organizer`` and ``searcher``. + Easy Market ------------- @@ -37,20 +64,3 @@ One important case is that models have different feature spaces. In order to ena - First, design a method for the market to connect different feature spaces to a common subspace and implement the function ``HeterogeneousFeatureMarket.learn_mapping_functions``. This function uses specifications of all submitted models to learn mapping functions that can map the data in the original feature space to the common subspace and vice verse. - Second, use learned mapping functions to implement the functions ``HeterogeneousFeatureMarket.transform_original_to_subspace`` and ``HeterogeneousFeatureMarket.transform_subspace_to_original``. - Third, use the functions ``HeterogeneousFeatureMarket.transform_original_to_subspace`` and ``HeterogeneousFeatureMarket.transform_subspace_to_original`` to overwrite the mehtod ``EvolvedMarket.generate_new_stat_specification`` and ``EvolvedMarket.EvolvedMarket.evolve_learnware_list`` of the base class ``EvolvedMarket``. - -Current Checkers -====================================== -The checkers check a learnware object in different aspects, including environment configuration (``CondaChecker``), semantic specifications (``EasySemanticChecker``), and statistical specifications (``EasyStatChecker``). The ``__call__`` method of each checker is designed to be invoked as a function to conduct the respective checks on the learnware and return the outcomes. It defines three types of learnwares: INVALID_LEARNWARE denotes the learnware does not pass the check, NONUSABLE_LEARNWARE denotes the learnware pass the check but cannot make prediction due to some env dependency, USABLE_LEARWARE denotes the leanrware pass the check and can make prediction. Currently, we have three checkers, which are described below. - - -CondaChecker Class ------------------- -This checker checks a the environment of the learnware object. It creates a ``LearnwaresContainer`` instance to handle the Learnware and uses ``inner_checker`` to check the Learnware. If an exception occurs, it logs the error and returns ``BaseChecker.NONUSABLE_LEARNWARE`` status and error message. - -EasySemanticChecker Class -------------------------- -This checker checks the semantic specification of a learnware object. It checks if the given semantic specification conforms to predefined standards. It verifies each key in predefined dictionary. If the check fails, it logs the error and returns ``NONUSABLE_LEARNWARE`` status and error message. - -EasyStatChecker Class ---------------------- -This checker checks the statistical specification and functionality of a learnware object. It performs multiple checks to validate the learnware. It checks for model instantiation, verifies input shape and statistical specifications, and test output shape using random generated data. In case of any exceptions, it logs the error and returns ``NONUSABLE_LEARNWARE`` status and error message. \ No newline at end of file From a9c443aefbbc42c34923922e72515bd92ef83450 Mon Sep 17 00:00:00 2001 From: Gene Date: Thu, 7 Dec 2023 21:04:39 +0800 Subject: [PATCH 34/43] [FIX] fix bugs about batch_size --- learnware/market/heterogeneous/organizer/hetero_map/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/learnware/market/heterogeneous/organizer/hetero_map/__init__.py b/learnware/market/heterogeneous/organizer/hetero_map/__init__.py index e124bcb..aca3d02 100644 --- a/learnware/market/heterogeneous/organizer/hetero_map/__init__.py +++ b/learnware/market/heterogeneous/organizer/hetero_map/__init__.py @@ -309,7 +309,7 @@ class HeteroMap(nn.Module): output_feas_list = [] if eval_batch_size * x_test.shape[1] > self.max_process_size: - eval_batch_size = int(self.max_process_size / x_test.shape[1]) + eval_batch_size = max(1, self.max_process_size // x_test.shape[1]) for i in range(0, len(x_test), eval_batch_size): bs_x_test = x_test.iloc[i : i + eval_batch_size] From 25a8493ec5cffb8711568f580623e01b9b6089e6 Mon Sep 17 00:00:00 2001 From: Gene Date: Thu, 7 Dec 2023 21:13:05 +0800 Subject: [PATCH 35/43] [FIX] delete extra import --- learnware/market/anchor/organizer.py | 3 +-- learnware/market/anchor/searcher.py | 3 +-- learnware/market/base.py | 27 +++++++++++-------- learnware/market/easy/database_ops.py | 2 +- .../organizer/hetero_map/__init__.py | 4 +-- .../organizer/hetero_map/feature_extractor.py | 2 +- 6 files changed, 22 insertions(+), 19 deletions(-) diff --git a/learnware/market/anchor/organizer.py b/learnware/market/anchor/organizer.py index 405060b..4c3b668 100644 --- a/learnware/market/anchor/organizer.py +++ b/learnware/market/anchor/organizer.py @@ -1,9 +1,8 @@ -from typing import List, Dict, Tuple, Any +from typing import Dict from ..easy.organizer import EasyOrganizer from ...logger import get_module_logger from ...learnware import Learnware -from ...specification import BaseStatSpecification logger = get_module_logger("anchor_organizer") diff --git a/learnware/market/anchor/searcher.py b/learnware/market/anchor/searcher.py index 34d326d..60bca9f 100644 --- a/learnware/market/anchor/searcher.py +++ b/learnware/market/anchor/searcher.py @@ -1,7 +1,6 @@ -from typing import List, Dict, Tuple, Any, Union +from typing import List, Tuple, Any from .user_info import AnchoredUserInfo -from ..base import BaseUserInfo from ..easy.searcher import EasySearcher from ...logger import get_module_logger from ...learnware import Learnware diff --git a/learnware/market/base.py b/learnware/market/base.py index 78d06e6..fce2f18 100644 --- a/learnware/market/base.py +++ b/learnware/market/base.py @@ -3,7 +3,7 @@ from __future__ import annotations import traceback import zipfile import tempfile -from typing import Tuple, Any, List, Union, Dict, Optional +from typing import Tuple, Any, List, Union, Optional from dataclasses import dataclass from ..learnware import Learnware, get_learnware_from_dirpath from ..logger import get_module_logger @@ -45,7 +45,7 @@ class BaseUserInfo: def update_semantic_spec(self, semantic_spec: dict): self.semantic_spec = semantic_spec - + def update_stat_info(self, name: str, item: Any): """Update stat_info by market @@ -64,28 +64,35 @@ class SingleSearchItem: learnware: Learnware score: Optional[float] = None + @dataclass class MultipleSearchItem: learnwares: List[Learnware] score: float - + + class SearchResults: - def __init__(self, single_results: Optional[List[SingleSearchItem]] = None, multiple_results: Optional[List[MultipleSearchItem]] = None): + def __init__( + self, + single_results: Optional[List[SingleSearchItem]] = None, + multiple_results: Optional[List[MultipleSearchItem]] = None, + ): self.update_single_results([] if single_results is None else single_results) self.update_multiple_results([] if multiple_results is None else multiple_results) - + def get_single_results(self) -> List[SingleSearchItem]: return self.single_results - + def get_multiple_results(self) -> List[MultipleSearchItem]: return self.multiple_results - + def update_single_results(self, single_results: List[SingleSearchItem]): self.single_results = single_results - + def update_multiple_results(self, multiple_results: List[MultipleSearchItem]): self.multiple_results = multiple_results + class LearnwareMarket: """Base interface for market, it provide the interface of search/add/detele/update learnwares""" @@ -179,9 +186,7 @@ class LearnwareMarket: zip_path=zip_path, semantic_spec=semantic_spec, check_status=check_status, **kwargs ) - def search_learnware( - self, user_info: BaseUserInfo, check_status: int = None, **kwargs - ) -> SearchResults: + def search_learnware(self, user_info: BaseUserInfo, check_status: int = None, **kwargs) -> SearchResults: """Search learnwares based on user_info from learnwares with check_status Parameters diff --git a/learnware/market/easy/database_ops.py b/learnware/market/easy/database_ops.py index 7f8e87c..e27577c 100644 --- a/learnware/market/easy/database_ops.py +++ b/learnware/market/easy/database_ops.py @@ -1,6 +1,6 @@ from sqlalchemy.ext.declarative import declarative_base from sqlalchemy import create_engine, text -from sqlalchemy import Column, Integer, Text, DateTime, String +from sqlalchemy import Column, Text, String import os import json import traceback diff --git a/learnware/market/heterogeneous/organizer/hetero_map/__init__.py b/learnware/market/heterogeneous/organizer/hetero_map/__init__.py index aca3d02..b2f39fe 100644 --- a/learnware/market/heterogeneous/organizer/hetero_map/__init__.py +++ b/learnware/market/heterogeneous/organizer/hetero_map/__init__.py @@ -1,10 +1,10 @@ -from typing import Callable, List, Optional, Union +from typing import Callable, Union import numpy as np import pandas as pd import torch import torch.nn.functional as F -from torch import Tensor, nn +from torch import nn from .....utils import allocate_cuda_idx, choose_device from .....specification import HeteroMapTableSpecification, RKMETableSpecification diff --git a/learnware/market/heterogeneous/organizer/hetero_map/feature_extractor.py b/learnware/market/heterogeneous/organizer/hetero_map/feature_extractor.py index d98bd2e..325f74e 100644 --- a/learnware/market/heterogeneous/organizer/hetero_map/feature_extractor.py +++ b/learnware/market/heterogeneous/organizer/hetero_map/feature_extractor.py @@ -1,6 +1,6 @@ import math import os -from typing import Callable, Dict, List, Union +from typing import Dict, List, Union import numpy as np import pandas as pd From 0a12b24452c46a08b449eb2381803ec1e7e4c146 Mon Sep 17 00:00:00 2001 From: bxdd Date: Fri, 8 Dec 2023 15:44:35 +0800 Subject: [PATCH 36/43] [MNT] modift config token method in test client --- .../test_all_learnware.py | 26 +++++++++++------- tests/test_learnware_client/test_upload.py | 27 ++++++++++++------- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/tests/test_learnware_client/test_all_learnware.py b/tests/test_learnware_client/test_all_learnware.py index 7c606b6..94432ed 100644 --- a/tests/test_learnware_client/test_all_learnware.py +++ b/tests/test_learnware_client/test_all_learnware.py @@ -12,15 +12,24 @@ from learnware.market import BaseUserInfo class TestAllLearnware(unittest.TestCase): client = LearnwareClient() - def __init__(self, method_name='runTest', email=None, token=None): - super(TestAllLearnware, self).__init__(method_name) - self.email = email - self.token = token + @classmethod + def setUpClass(cls) -> None: + config_path = os.path.join(os.path.dirname(__file__), "config.json") + + if not os.path.exists(config_path): + data = {"email": None, "token": None} + with open(config_path, "w") as file: + json.dump(data, file) + + with open(config_path, "r") as file: + data = json.load(file) + email = data.get("email") + token = data.get("token") - if self.email is not None and self.token is not None: - self.client.login(self.email, self.token) + if email is None or token is None: + print("Please set email and token in config.json.") else: - print("Client doest not login, all tests will be ignored!") + cls.client.login(email, token) @unittest.skipIf(not client.is_login(), "Client doest not login!") def test_all_learnware(self): @@ -52,10 +61,9 @@ class TestAllLearnware(unittest.TestCase): print(f"The currently failed learnware ids: {failed_ids}") - def suite(): _suite = unittest.TestSuite() - _suite.addTest(TestAllLearnware("test_all_learnware", email=None, token=None)) + _suite.addTest(TestAllLearnware("test_all_learnware")) return _suite if __name__ == "__main__": diff --git a/tests/test_learnware_client/test_upload.py b/tests/test_learnware_client/test_upload.py index 663a29d..c2f9015 100644 --- a/tests/test_learnware_client/test_upload.py +++ b/tests/test_learnware_client/test_upload.py @@ -1,5 +1,5 @@ import os -import argparse +import json import unittest import tempfile @@ -9,15 +9,24 @@ from learnware.specification import generate_semantic_spec class TestUpload(unittest.TestCase): client = LearnwareClient() - def __init__(self, method_name='runTest', email=None, token=None): - super(TestUpload, self).__init__(method_name) - self.email = email - self.token = token + @classmethod + def setUpClass(cls) -> None: + config_path = os.path.join(os.path.dirname(__file__), "config.json") + + if not os.path.exists(config_path): + data = {"email": None, "token": None} + with open(config_path, "w") as file: + json.dump(data, file) + + with open(config_path, "r") as file: + data = json.load(file) + email = data.get("email") + token = data.get("token") - if self.email is not None and self.token is not None: - self.client.login(self.email, self.token) + if email is None or token is None: + print("Please set email and token in config.json.") else: - print("Client doest not login, all tests will be ignored!") + cls.client.login(email, token) @unittest.skipIf(not client.is_login(), "Client doest not login!") def test_upload(self): @@ -61,7 +70,7 @@ class TestUpload(unittest.TestCase): def suite(): _suite = unittest.TestSuite() - _suite.addTest(TestUpload("test_upload", email=None, token=None)) + _suite.addTest(TestUpload("test_upload")) return _suite if __name__ == "__main__": From 370b497594117fd241059632deaad2119347c2e8 Mon Sep 17 00:00:00 2001 From: Gene Date: Fri, 8 Dec 2023 20:12:27 +0800 Subject: [PATCH 37/43] [FIX] remove @unittest.skipIf --- tests/test_function/test_search.py | 93 +++++++++++-------- .../test_all_learnware.py | 68 ++++++++------ tests/test_learnware_client/test_upload.py | 81 ++++++++-------- 3 files changed, 138 insertions(+), 104 deletions(-) diff --git a/tests/test_function/test_search.py b/tests/test_function/test_search.py index 9aa1b42..c006b0d 100644 --- a/tests/test_function/test_search.py +++ b/tests/test_function/test_search.py @@ -4,6 +4,7 @@ import tempfile import logging import learnware + learnware.init(logging_level=logging.WARNING) from learnware.learnware import Learnware @@ -11,9 +12,10 @@ from learnware.client import LearnwareClient from learnware.market import instantiate_learnware_market, BaseUserInfo, EasySemanticChecker from learnware.config import C + class TestSearch(unittest.TestCase): client = LearnwareClient() - + @classmethod def setUpClass(cls): cls.market = instantiate_learnware_market(market_id="search_test", name="hetero", rebuild=True) @@ -31,46 +33,62 @@ class TestSearch(unittest.TestCase): learnware_zippath = os.path.join(tempdir, f"learnware_{learnware_id}.zip") try: cls.client.download_learnware(learnware_id=learnware_id, save_path=learnware_zippath) - semantic_spec = cls.client.load_learnware(learnware_path=learnware_zippath).get_specification().get_semantic_spec() + semantic_spec = ( + cls.client.load_learnware(learnware_path=learnware_zippath) + .get_specification() + .get_semantic_spec() + ) except Exception: print("'learnware_id' is passed due to the network problem.") - cls.market.add_learnware(learnware_zippath, learnware_id=learnware_id, semantic_spec=semantic_spec, checker_names=["EasySemanticChecker"]) - - @unittest.skipIf(not client.is_connected(), "Client can not connect!") + cls.market.add_learnware( + learnware_zippath, + learnware_id=learnware_id, + semantic_spec=semantic_spec, + checker_names=["EasySemanticChecker"], + ) + + def _skip_test(self): + if not self.client.is_connected(): + print("Client can not connect!") + return True + return False + def test_image_search(self): - learnware_id = "00000619" - try: - learnware: Learnware = self.client.load_learnware(learnware_id=learnware_id) - except Exception: - print("'test_image_search' is passed due to the network problem.") - user_info = BaseUserInfo(stat_info=learnware.get_specification().get_stat_spec()) - search_result = self.market.search_learnware(user_info) - print("Single Search Results:", search_result.get_single_results()) - print("Multiple Search Results:", search_result.get_multiple_results()) - - @unittest.skipIf(not client.is_connected(), "Client can not connect!") + if not self._skip_test(): + learnware_id = "00000619" + try: + learnware: Learnware = self.client.load_learnware(learnware_id=learnware_id) + except Exception: + print("'test_image_search' is passed due to the network problem.") + user_info = BaseUserInfo(stat_info=learnware.get_specification().get_stat_spec()) + search_result = self.market.search_learnware(user_info) + print("Single Search Results:", search_result.get_single_results()) + print("Multiple Search Results:", search_result.get_multiple_results()) + def test_text_search(self): - learnware_id = "00000653" - try: - learnware: Learnware = self.client.load_learnware(learnware_id=learnware_id) - except Exception: - print("'test_text_search' is passed due to the network problem.") - user_info = BaseUserInfo(stat_info=learnware.get_specification().get_stat_spec()) - search_result = self.market.search_learnware(user_info) - print("Single Search Results:", search_result.get_single_results()) - print("Multiple Search Results:", search_result.get_multiple_results()) - - @unittest.skipIf(not client.is_connected(), "Client can not connect!") + if not self._skip_test(): + learnware_id = "00000653" + try: + learnware: Learnware = self.client.load_learnware(learnware_id=learnware_id) + except Exception: + print("'test_text_search' is passed due to the network problem.") + user_info = BaseUserInfo(stat_info=learnware.get_specification().get_stat_spec()) + search_result = self.market.search_learnware(user_info) + print("Single Search Results:", search_result.get_single_results()) + print("Multiple Search Results:", search_result.get_multiple_results()) + def test_table_search(self): - learnware_id = "00001950" - try: - learnware: Learnware = self.client.load_learnware(learnware_id=learnware_id) - except Exception: - print("'test_table_search' is passed due to the network problem.") - user_info = BaseUserInfo(stat_info=learnware.get_specification().get_stat_spec()) - search_result = self.market.search_learnware(user_info) - print("Single Search Results:", search_result.get_single_results()) - print("Multiple Search Results:", search_result.get_multiple_results()) + if not self._skip_test(): + learnware_id = "00001950" + try: + learnware: Learnware = self.client.load_learnware(learnware_id=learnware_id) + except Exception: + print("'test_table_search' is passed due to the network problem.") + user_info = BaseUserInfo(stat_info=learnware.get_specification().get_stat_spec()) + search_result = self.market.search_learnware(user_info) + print("Single Search Results:", search_result.get_single_results()) + print("Multiple Search Results:", search_result.get_multiple_results()) + def suite(): _suite = unittest.TestSuite() @@ -79,6 +97,7 @@ def suite(): _suite.addTest(TestSearch("test_table_search")) return _suite + if __name__ == "__main__": runner = unittest.TextTestRunner() - runner.run(suite()) \ No newline at end of file + runner.run(suite()) diff --git a/tests/test_learnware_client/test_all_learnware.py b/tests/test_learnware_client/test_all_learnware.py index 94432ed..9fbcc41 100644 --- a/tests/test_learnware_client/test_all_learnware.py +++ b/tests/test_learnware_client/test_all_learnware.py @@ -9,13 +9,14 @@ from learnware.client import LearnwareClient from learnware.specification import generate_semantic_spec from learnware.market import BaseUserInfo + class TestAllLearnware(unittest.TestCase): client = LearnwareClient() - + @classmethod def setUpClass(cls) -> None: config_path = os.path.join(os.path.dirname(__file__), "config.json") - + if not os.path.exists(config_path): data = {"email": None, "token": None} with open(config_path, "w") as file: @@ -25,40 +26,46 @@ class TestAllLearnware(unittest.TestCase): data = json.load(file) email = data.get("email") token = data.get("token") - + if email is None or token is None: print("Please set email and token in config.json.") else: cls.client.login(email, token) - @unittest.skipIf(not client.is_login(), "Client doest not login!") + def _skip_test(self): + if not self.client.is_login(): + print("Client does not login!") + return True + return False + def test_all_learnware(self): - max_learnware_num = 2000 - semantic_spec = generate_semantic_spec() - user_info = BaseUserInfo(semantic_spec=semantic_spec, stat_info={}) - result = self.client.search_learnware(user_info, page_size=max_learnware_num) - - learnware_ids = result["single"]["learnware_ids"] - keys = [key for key in result["single"]["semantic_specifications"][0]] - print(f"result size: {len(learnware_ids)}") - print(f"key in result: {keys}") - - failed_ids = [] - with tempfile.TemporaryDirectory(prefix="learnware_") as tempdir: - for idx in learnware_ids: - zip_path = os.path.join(tempdir, f"test_{idx}.zip") - self.client.download_learnware(idx, zip_path) - with zipfile.ZipFile(zip_path, "r") as zip_file: - with zip_file.open("semantic_specification.json") as json_file: - semantic_spec = json.load(json_file) - try: - LearnwareClient.check_learnware(zip_path, semantic_spec) - print(f"check learnware {idx} succeed") - except: - failed_ids.append(idx) - print(f"check learnware {idx} failed!!!") - - print(f"The currently failed learnware ids: {failed_ids}") + if not self._skip_test(): + max_learnware_num = 2000 + semantic_spec = generate_semantic_spec() + user_info = BaseUserInfo(semantic_spec=semantic_spec, stat_info={}) + result = self.client.search_learnware(user_info, page_size=max_learnware_num) + + learnware_ids = result["single"]["learnware_ids"] + keys = [key for key in result["single"]["semantic_specifications"][0]] + print(f"result size: {len(learnware_ids)}") + print(f"key in result: {keys}") + + failed_ids = [] + with tempfile.TemporaryDirectory(prefix="learnware_") as tempdir: + for idx in learnware_ids: + zip_path = os.path.join(tempdir, f"test_{idx}.zip") + self.client.download_learnware(idx, zip_path) + with zipfile.ZipFile(zip_path, "r") as zip_file: + with zip_file.open("semantic_specification.json") as json_file: + semantic_spec = json.load(json_file) + try: + LearnwareClient.check_learnware(zip_path, semantic_spec) + print(f"check learnware {idx} succeed") + except: + failed_ids.append(idx) + print(f"check learnware {idx} failed!!!") + + print(f"The currently failed learnware ids: {failed_ids}") def suite(): @@ -66,6 +73,7 @@ def suite(): _suite.addTest(TestAllLearnware("test_all_learnware")) return _suite + if __name__ == "__main__": runner = unittest.TextTestRunner() runner.run(suite()) diff --git a/tests/test_learnware_client/test_upload.py b/tests/test_learnware_client/test_upload.py index c2f9015..18e4055 100644 --- a/tests/test_learnware_client/test_upload.py +++ b/tests/test_learnware_client/test_upload.py @@ -6,13 +6,14 @@ import tempfile from learnware.client import LearnwareClient from learnware.specification import generate_semantic_spec + class TestUpload(unittest.TestCase): client = LearnwareClient() - + @classmethod def setUpClass(cls) -> None: config_path = os.path.join(os.path.dirname(__file__), "config.json") - + if not os.path.exists(config_path): data = {"email": None, "token": None} with open(config_path, "w") as file: @@ -22,50 +23,55 @@ class TestUpload(unittest.TestCase): data = json.load(file) email = data.get("email") token = data.get("token") - + if email is None or token is None: print("Please set email and token in config.json.") else: cls.client.login(email, token) - @unittest.skipIf(not client.is_login(), "Client doest not login!") + def _skip_test(self): + if not self.client.is_login(): + print("Client does not login!") + return True + return False + def test_upload(self): - input_description = { - "Dimension": 13, - "Description": {"0": "age", "1": "weight", "2": "body length", "3": "animal type", "4": "claw length"}, - } - output_description = { - "Dimension": 1, - "Description": { - "0": "the probability of being a cat", - }, - } - semantic_spec = generate_semantic_spec( - name="learnware_example", - description="Just a example for uploading a learnware", - data_type="Table", - task_type="Classification", - library_type="Scikit-learn", - scenarios=["Business", "Financial"], - input_description=input_description, - output_description=output_description, - ) - assert isinstance(semantic_spec, dict) - - download_learnware_id = "00000084" - with tempfile.TemporaryDirectory(prefix="learnware_") as tempdir: - zip_path = os.path.join(tempdir, f"test.zip") - self.client.download_learnware(download_learnware_id, zip_path) - learnware_id = self.client.upload_learnware( - learnware_zip_path=zip_path, semantic_specification=semantic_spec + if not self._skip_test(): + input_description = { + "Dimension": 13, + "Description": {"0": "age", "1": "weight", "2": "body length", "3": "animal type", "4": "claw length"}, + } + output_description = { + "Dimension": 2, + "Description": {"0": "cat", "1": "not cat"}, + } + semantic_spec = generate_semantic_spec( + name="learnware_example", + description="Just a example for uploading a learnware", + data_type="Table", + task_type="Classification", + library_type="Scikit-learn", + scenarios=["Business", "Financial"], + license="MIT", + input_description=input_description, + output_description=output_description, ) + assert isinstance(semantic_spec, dict) + + download_learnware_id = "00000084" + with tempfile.TemporaryDirectory(prefix="learnware_") as tempdir: + zip_path = os.path.join(tempdir, f"test.zip") + self.client.download_learnware(download_learnware_id, zip_path) + learnware_id = self.client.upload_learnware( + learnware_zip_path=zip_path, semantic_specification=semantic_spec + ) - uploaded_ids = [learnware["learnware_id"] for learnware in self.client.list_learnware()] - assert learnware_id in uploaded_ids + uploaded_ids = [learnware["learnware_id"] for learnware in self.client.list_learnware()] + assert learnware_id in uploaded_ids - self.client.delete_learnware(learnware_id) - uploaded_ids = [learnware["learnware_id"] for learnware in self.client.list_learnware()] - assert learnware_id not in uploaded_ids + self.client.delete_learnware(learnware_id) + uploaded_ids = [learnware["learnware_id"] for learnware in self.client.list_learnware()] + assert learnware_id not in uploaded_ids def suite(): @@ -73,6 +79,7 @@ def suite(): _suite.addTest(TestUpload("test_upload")) return _suite + if __name__ == "__main__": runner = unittest.TextTestRunner() runner.run(suite()) From 69162fa96a9680d88e09ec6a1ced8842a0eb8971 Mon Sep 17 00:00:00 2001 From: Gene Date: Fri, 8 Dec 2023 20:27:07 +0800 Subject: [PATCH 38/43] [FIX] add license in semantic spec key --- learnware/client/learnware_client.py | 33 ++++++++++++++++------------ 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/learnware/client/learnware_client.py b/learnware/client/learnware_client.py index b247b5a..0374954 100644 --- a/learnware/client/learnware_client.py +++ b/learnware/client/learnware_client.py @@ -54,6 +54,7 @@ class SemanticSpecificationKey(Enum): TASK_TYPE = "Task" LIBRARY_TYPE = "Library" SENARIOES = "Scenario" + LICENSE = "License" class LearnwareClient: @@ -69,14 +70,14 @@ class LearnwareClient: self.tempdir_list = [] self.login_status = False atexit.register(self.cleanup) - + def is_connected(self): url = f"{self.host}/auth/login_by_token" response = requests.post(url) if response.status_code == 404: return False return True - + def login(self, email, token): url = f"{self.host}/auth/login_by_token" @@ -89,10 +90,10 @@ class LearnwareClient: token = result["data"]["token"] self.headers = {"Authorization": f"Bearer {token}"} self.login_status = True - + def is_login(self): return self.login_status - + @require_login def logout(self): url = f"{self.host}/auth/logout" @@ -277,7 +278,7 @@ class LearnwareClient: returns["multiple"]["learnware_ids"].append(multi_learnware["learnware_id"]) returns["multiple"]["semantic_specifications"].append(multi_learnware["semantic_specification"]) returns["multiple"]["matching"] = learnware["matching"] - + # Delete temp json file os.remove(temp_file_name) @@ -411,15 +412,19 @@ class LearnwareClient: @staticmethod def check_learnware(learnware_zip_path, semantic_specification=None): - semantic_specification = generate_semantic_spec( - name="test", - description="test", - data_type="Text", - task_type="Segmentation", - scenarios="Financial", - license="Apache-2.0", - ) if semantic_specification is None else semantic_specification - + semantic_specification = ( + generate_semantic_spec( + name="test", + description="test", + data_type="Text", + task_type="Segmentation", + scenarios="Financial", + license="Apache-2.0", + ) + if semantic_specification is None + else semantic_specification + ) + check_status, message = LearnwareClient._check_semantic_specification(semantic_specification) assert check_status, f"Semantic specification check failed due to {message}!" From ccfccfa86843090e8bc2f5a6a2a7ea5800d5b071 Mon Sep 17 00:00:00 2001 From: Gene Date: Fri, 8 Dec 2023 23:42:26 +0800 Subject: [PATCH 39/43] [MNT] add cache in benchmark --- learnware/tests/benchmarks/__init__.py | 231 ++++++++++++++----------- learnware/tests/benchmarks/config.py | 25 +-- 2 files changed, 145 insertions(+), 111 deletions(-) diff --git a/learnware/tests/benchmarks/__init__.py b/learnware/tests/benchmarks/__init__.py index 715037d..7caa6c7 100644 --- a/learnware/tests/benchmarks/__init__.py +++ b/learnware/tests/benchmarks/__init__.py @@ -1,130 +1,161 @@ import os import pickle -import atexit import tempfile import zipfile -from dataclasses import dataclass, field -from typing import Optional, List, Union, Tuple +from dataclasses import dataclass +from typing import Tuple, Optional, List, Union -from .config import OnlineBenchmark, online_benchmarks +from .config import BenchmarkConfig, benchmark_configs from ..data import GetData +from ...config import C + + @dataclass class Benchmark: learnware_ids: List[str] user_num: int - unlabeled_feature_paths: List[str] - unlabeled_groudtruths_paths: List[str] - labeled_feature_paths: Optional[List[str]] = None - labeled_label_paths: Optional[List[str]] = None + test_X_paths: List[str] + test_y_paths: List[str] + train_X_paths: Optional[List[str]] = None + train_y_paths: Optional[List[str]] = None extra_info_path: Optional[str] = None - - # TODO: add more method for benchmark - - def get_unlabeled_data(self, user_ids: Union[str, List[str]]): + + def get_test_data(self, user_ids: Union[str, List[str]]): if isinstance(user_ids, str): user_ids = [user_ids] - + ret = [] for user_id in user_ids: - with open(self.unlabeled_feature_paths[user_id], "rb") as fin: - unlabeled_feature = pickle.load(fin) - - with open(self.unlabeled_groudtruths_paths[user_id], "rb") as fin: - unlabeled_groudtruth = pickle.load(fin) - - ret.append((unlabeled_feature, unlabeled_groudtruth)) + with open(self.test_X_paths[user_id], "rb") as fin: + test_X = pickle.load(fin) + + with open(self.test_y_paths[user_id], "rb") as fin: + test_y = pickle.load(fin) + + ret.append((test_X, test_y)) return ret - - def get_labeled_data(self, user_ids): - if self.labeled_feature_paths is None or self.labeled_label_paths is None: + + def get_train_data(self, user_ids): + if self.train_X_paths is None or self.train_y_paths is None: return None - + if isinstance(user_ids, str): user_ids = [user_ids] - + ret = [] for user_id in user_ids: - with open(self.labeled_feature_paths[user_id], "rb") as fin: - labeled_feature = pickle.load(fin) - - with open(self.labeled_label_paths[user_id], "rb") as fin: - labeled_groudtruth = pickle.load(fin) - - ret.append((labeled_feature, labeled_groudtruth)) + with open(self.train_X_paths[user_id], "rb") as fin: + train_X = pickle.load(fin) + + with open(self.train_y_paths[user_id], "rb") as fin: + train_y = pickle.load(fin) + + ret.append((train_X, train_y)) return ret - + class LearnwareBenchmark: - def __init__(self): - self.online_benchmarks = online_benchmarks - self.tempdir_list = [] - atexit.register(self.cleanup) - + self.benchmark_configs = benchmark_configs + def list_benchmarks(self): - return list(self.online_benchmarks.keys()) - - def get_benchmark(self, online_benchmark: Union[str, OnlineBenchmark]): - if isinstance(online_benchmark, str): - online_benchmark = self.online_benchmarks[online_benchmark] - - self.tempdir_list.append(tempfile.TemporaryDirectory(prefix="learnware_benchmark")) - save_folder = self.tempdir_list[-1].name - - unlabeled_data_localpath = os.path.join(save_folder, "unlabeled_data.zip") - GetData().download_file(online_benchmark.unlabeled_data_path, unlabeled_data_localpath) - - unlabeled_feature_paths = [] - unlabeled_groudtruth_paths = [] - - with zipfile.ZipFile(unlabeled_data_localpath, "r") as z_file: - unlabeled_data_dirpath = os.path.join(save_folder, "unlabeled_data") - z_file.extractall(unlabeled_data_dirpath) - for user_id in range(online_benchmark.user_num): - user_feature_filepath = os.path.isfile(os.path.join(unlabeled_data_dirpath, f"user{user_id}_feature.pkl")) - user_groudtruth_filepath = os.path.isfile(os.path.join(unlabeled_data_dirpath, f"user{user_id}_groudtruth.pkl")) - assert os.path.isfile(user_feature_filepath), f"user {user_id} unlabeled feature is not valid!" - assert os.path.isfile(user_groudtruth_filepath), f"user {user_id} unlabeled groudtruth is not valid!" - unlabeled_feature_paths.append(user_feature_filepath) - unlabeled_groudtruth_paths.append(user_groudtruth_filepath) - - labeled_feature_paths = None - labeled_label_paths = None - if online_benchmark.labeled_data_path is not None: - labeled_data_localpath = os.path.join(save_folder, "labeled_data.zip") - GetData().download_file(online_benchmark.labeled_data_path, labeled_data_localpath) - - labeled_feature_paths = [] - labeled_label_paths = [] - - with zipfile.ZipFile(labeled_data_localpath, "r") as z_file: - labeled_data_dirpath = os.path.join(save_folder, "labeled_data") - z_file.extractall(labeled_data_dirpath) - for user_id in range(online_benchmark.user_num): - user_feature_filepath = os.path.isfile(os.path.join(labeled_data_dirpath, f"user{user_id}_feature.pkl")) - user_groudtruth_filepath = os.path.isfile(os.path.join(labeled_data_dirpath, f"user{user_id}_label.pkl")) - assert os.path.isfile(user_feature_filepath), f"user {user_id} labeled feature is not valid!" - assert os.path.isfile(user_groudtruth_filepath), f"user {user_id} labeled label is not valid!" - labeled_feature_paths.append(user_feature_filepath) - labeled_label_paths.append(user_groudtruth_filepath) - - extra_zip_localpath = None - if online_benchmark.extra_info_path is not None: - extra_zip_localpath = os.path.join(save_folder, os.path.basename(online_benchmark.extra_info_path)) - GetData().download_file(online_benchmark.extra_info_path, extra_zip_localpath) - + return list(self.benchmark_configs.keys()) + + def _check_cache_data_valid(self, benchmark_config: BenchmarkConfig, data_type: str) -> bool: + """Check if the cache data is valid + + Parameters + ---------- + benchmark_config : BenchmarkConfig + benchmark config + data_type : str + "test" for test data or "train" for train data + + Returns + ------- + bool + A flag indicating if the cache data is valid + """ + cache_folder = os.path.join(C.cache_path, benchmark_config.name, f"{data_type}_data") + if os.path.exists(cache_folder): + for user_id in range(benchmark_config.user_num): + X_path = os.path.join(cache_folder, f"user{user_id}_X.pkl") + y_path = os.path.join(cache_folder, f"user{user_id}_X.pkl") + if not os.path.isfile(X_path) or not os.path.isfile(y_path): + return False + return True + else: + return False + + def _download_data(self, download_path: str, save_path: str): + """Download data from backend + + Parameters + ---------- + download_path : str + data path for download in backend + save_path : str + local cache path for saving data + """ + with tempfile.TemporaryDirectory(prefix="learnware_benchmark_") as tempdir: + test_data_zippath = os.path.join(tempdir, "benchmark_data.zip") + GetData().download_file(download_path, test_data_zippath) + + os.makedirs(save_path, exist_ok=True) + with zipfile.ZipFile(test_data_zippath, "r") as z_file: + z_file.extractall(save_path) + + def _load_cache_data(self, benchmark_config: BenchmarkConfig, data_type: str) -> Tuple(List[str], List[str]): + """Load data from local cache path + + Parameters + ---------- + benchmark_config : BenchmarkConfig + benchmark config + data_type : str + "test" for test data or "train" for train data + """ + cache_folder = os.path.join(C.cache_path, benchmark_config.name, f"{data_type}_data") + if not self._check_cache_data_valid(benchmark_config, data_type): + download_path = getattr(benchmark_config, f"{data_type}_data_path", None) + self._download_data(download_path, cache_folder) + + X_paths, y_paths = [], [] + for user_id in range(benchmark_config.user_num): + user_X_path = os.path.join(cache_folder, f"user{user_id}_X.pkl") + user_y_path = os.path.join(cache_folder, f"user{user_id}_y.pkl") + assert os.path.isfile(user_X_path), f"user {user_id} {data_type}_X is not valid!" + assert os.path.isfile(user_y_path), f"user {user_id} {data_type}_y is not valid!" + X_paths.append(user_X_path) + y_paths.append(user_y_path) + + def get_benchmark(self, benchmark_config: Union[str, BenchmarkConfig]): + if isinstance(benchmark_config, str): + benchmark_config = self.benchmark_configs[benchmark_config] + + # Load test data + test_X_paths, test_y_paths = self._load_cache_data(benchmark_config, "test") + + # Load train data + train_X_paths, train_y_paths = None, None + if benchmark_config.train_data_path is not None: + train_X_paths, train_y_paths = self._load_cache_data(benchmark_config, "train") + + # Load extra info + extra_info_path = None + if benchmark_config.extra_info_path is not None: + extra_info_path = os.path.join(C.cache_path, benchmark_config.name, "extra_info") + if not os.path.exists(extra_info_path): + self._download_data(benchmark_config.extra_info_path, extra_info_path) + return Benchmark( - learnware_ids=online_benchmark.learnware_ids, - user_num=online_benchmark.user_num, - unlabeled_feature_paths=unlabeled_feature_paths, - unlabeled_groudtruths_paths=unlabeled_groudtruth_paths, - labeled_feature_paths=labeled_feature_paths, - labeled_label_paths=labeled_label_paths, - extra_info_path=extra_zip_localpath, + learnware_ids=benchmark_config.learnware_ids, + user_num=benchmark_config.user_num, + test_X_paths=test_X_paths, + test_y_paths=test_y_paths, + train_X_paths=train_X_paths, + train_y_paths=train_y_paths, + extra_info_path=extra_info_path, ) - - def cleanup(self): - for tempdir in self.tempdir_list: - tempdir.cleanup() \ No newline at end of file diff --git a/learnware/tests/benchmarks/config.py b/learnware/tests/benchmarks/config.py index a523a55..289cb50 100644 --- a/learnware/tests/benchmarks/config.py +++ b/learnware/tests/benchmarks/config.py @@ -1,21 +1,24 @@ -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import Optional, List -from ...learnware import Learnware + @dataclass -class OnlineBenchmark: +class BenchmarkConfig: + name: str learnware_ids: List[str] user_num: int - unlabeled_data_path: str - labeled_data_path: Optional[str] = None + test_data_path: str + train_data_path: Optional[str] = None extra_info_path: Optional[str] = None -online_benchmarks = { - "example": OnlineBenchmark( + +benchmark_configs = { + "example": BenchmarkConfig( + name="example", learnware_ids=["00001951", "00001980", "00001987"], user_num=3, - unlabeled_data_path="example_path1", - labeled_data_path="example_path2", - extra_info_path="example_path3" + test_data_path="example_path1", + train_data_path="example_path2", + extra_info_path="example_path3", ) -} \ No newline at end of file +} From 262cc06b2803c6f4c6099dcb1bacf5f9e70d23a0 Mon Sep 17 00:00:00 2001 From: bxdd Date: Sat, 9 Dec 2023 21:46:24 +0800 Subject: [PATCH 40/43] [MNT, FIX] fix bug for client check learnware, and add more log for check learnware --- learnware/client/learnware_client.py | 8 +++----- learnware/learnware/__init__.py | 15 ++++++++++++--- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/learnware/client/learnware_client.py b/learnware/client/learnware_client.py index b247b5a..69945d7 100644 --- a/learnware/client/learnware_client.py +++ b/learnware/client/learnware_client.py @@ -417,6 +417,7 @@ class LearnwareClient: data_type="Text", task_type="Segmentation", scenarios="Financial", + library_type="Scikit-learn", license="Apache-2.0", ) if semantic_specification is None else semantic_specification @@ -428,12 +429,9 @@ class LearnwareClient: z_file.extractall(tempdir) learnware = get_learnware_from_dirpath( - id="test", semantic_spec=semantic_specification, learnware_dirpath=tempdir + id="test", semantic_spec=semantic_specification, learnware_dirpath=tempdir, ignore_error=False ) - - if learnware is None: - raise Exception("The learnware is not valid.") - + check_status, message = LearnwareClient._check_stat_specification(learnware) assert check_status is True, message diff --git a/learnware/learnware/__init__.py b/learnware/learnware/__init__.py index 0f88957..a7f04a7 100644 --- a/learnware/learnware/__init__.py +++ b/learnware/learnware/__init__.py @@ -1,6 +1,7 @@ import os import copy from typing import Optional +import traceback from .base import Learnware from .utils import get_stat_spec_from_config @@ -45,7 +46,12 @@ def get_learnware_from_dirpath(id: str, semantic_spec: dict, learnware_dirpath, } try: - yaml_config = read_yaml_to_dict(os.path.join(learnware_dirpath, C.learnware_folder_config["yaml_file"])) + + learnware_yaml_path = os.path.join(learnware_dirpath, C.learnware_folder_config["yaml_file"]) + assert os.path.exists(learnware_yaml_path), f"learnware.yaml is not found for learnware_{id}, please check the learnware folder or zipfile." + + + yaml_config = read_yaml_to_dict(learnware_yaml_path) if "name" in yaml_config: learnware_config["name"] = yaml_config["name"] @@ -60,7 +66,10 @@ def get_learnware_from_dirpath(id: str, semantic_spec: dict, learnware_dirpath, learnware_spec = Specification() for _stat_spec in learnware_config["stat_specifications"]: stat_spec = _stat_spec.copy() - stat_spec["file_name"] = os.path.join(learnware_dirpath, stat_spec["file_name"]) + stat_spec_path = os.path.join(learnware_dirpath, stat_spec["file_name"]) + assert os.path.exists(stat_spec_path), f"statistical specification file {stat_spec['file_name']} is not found for learnware_{id}, please check the learnware folder or zipfile." + + stat_spec["file_name"] = stat_spec_path stat_spec_inst = get_stat_spec_from_config(stat_spec) learnware_spec.update_stat_spec(**{stat_spec_inst.type: stat_spec_inst}) @@ -69,7 +78,7 @@ def get_learnware_from_dirpath(id: str, semantic_spec: dict, learnware_dirpath, except Exception as e: if not ignore_error: raise e - logger.warning(f"Load Learnware {id} failed! Due to {repr(e)}") + logger.warning(f"Load Learnware {id} failed! Due to {e}; details:\n{traceback.format_exc()}") return None return Learnware( From 2257389ea4f1cdf9d3933ef9b21485825504f963 Mon Sep 17 00:00:00 2001 From: Gene Date: Sun, 10 Dec 2023 13:42:06 +0800 Subject: [PATCH 41/43] [MNT] add check only with zip --- tests/test_learnware_client/test_check_learnware.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/test_learnware_client/test_check_learnware.py b/tests/test_learnware_client/test_check_learnware.py index 1979026..1e4ed55 100644 --- a/tests/test_learnware_client/test_check_learnware.py +++ b/tests/test_learnware_client/test_check_learnware.py @@ -6,11 +6,19 @@ import tempfile from learnware.client import LearnwareClient + class TestCheckLearnware(unittest.TestCase): def setUp(self): unittest.TestCase.setUpClass() self.client = LearnwareClient() + def test_check_learnware_pip_only_zip(self): + learnware_id = "00000208" + with tempfile.TemporaryDirectory(prefix="learnware_") as tempdir: + self.zip_path = os.path.join(tempdir, "test.zip") + self.client.download_learnware(learnware_id, self.zip_path) + LearnwareClient.check_learnware(self.zip_path) + def test_check_learnware_pip(self): learnware_id = "00000208" with tempfile.TemporaryDirectory(prefix="learnware_") as tempdir: @@ -66,8 +74,10 @@ class TestCheckLearnware(unittest.TestCase): semantic_spec = json.load(json_file) LearnwareClient.check_learnware(self.zip_path, semantic_spec) + def suite(): _suite = unittest.TestSuite() + _suite.addTest(TestCheckLearnware("test_check_learnware_pip_only_zip")) _suite.addTest(TestCheckLearnware("test_check_learnware_pip")) _suite.addTest(TestCheckLearnware("test_check_learnware_conda")) _suite.addTest(TestCheckLearnware("test_check_learnware_dependency")) @@ -75,6 +85,7 @@ def suite(): _suite.addTest(TestCheckLearnware("test_check_learnware_text")) return _suite + if __name__ == "__main__": runner = unittest.TextTestRunner() - runner.run(suite()) \ No newline at end of file + runner.run(suite()) From c1d097f66f55ba56e66264bc0a8001938977d1e1 Mon Sep 17 00:00:00 2001 From: bxdd Date: Sun, 10 Dec 2023 16:58:06 +0800 Subject: [PATCH 42/43] [MNT] publish 0.2.0.8 beta version --- learnware/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/learnware/__init__.py b/learnware/__init__.py index 6d86b3d..8b9e80a 100644 --- a/learnware/__init__.py +++ b/learnware/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.2.0.7" +__version__ = "0.2.0.8" import os import json From 96d9e873f30952765493e9472d5f234cc2d86204 Mon Sep 17 00:00:00 2001 From: nju-xy <1582857295@qq.com> Date: Mon, 11 Dec 2023 15:52:53 +0800 Subject: [PATCH 43/43] [MNT] fix typo of available --- learnware/client/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/learnware/client/utils.py b/learnware/client/utils.py index b515c30..e738331 100644 --- a/learnware/client/utils.py +++ b/learnware/client/utils.py @@ -49,7 +49,7 @@ def install_environment(learnware_dirpath, conda_env): if "environment.yaml" in os.listdir(learnware_dirpath): yaml_path: str = os.path.join(learnware_dirpath, "environment.yaml") yaml_path_filter: str = os.path.join(tempdir, "environment_filter.yaml") - logger.info(f"checking the avaliabe conda packages for {conda_env}") + logger.info(f"checking the available conda packages for {conda_env}") filter_nonexist_conda_packages_file(yaml_file=yaml_path, output_yaml_file=yaml_path_filter) # create environment logger.info(f"create conda env [{conda_env}] according to .yaml file") @@ -58,7 +58,7 @@ def install_environment(learnware_dirpath, conda_env): elif "requirements.txt" in os.listdir(learnware_dirpath): requirements_path: str = os.path.join(learnware_dirpath, "requirements.txt") requirements_path_filter: str = os.path.join(tempdir, "requirements_filter.txt") - logger.info(f"checking the avaliabe pip packages for {conda_env}") + logger.info(f"checking the available pip packages for {conda_env}") filter_nonexist_pip_packages_file(requirements_file=requirements_path, output_file=requirements_path_filter) logger.info(f"create empty conda env [{conda_env}]") system_execute(args=["conda", "create", "-y", "--name", f"{conda_env}", "python=3.8"])