From 93e89434abd1ff99cddd33f393a261d25c4df6ca Mon Sep 17 00:00:00 2001 From: PrivacyGo-PETPlatform Date: Mon, 1 Jul 2024 20:13:37 +0800 Subject: [PATCH] feat: version 0.1.0 --- .coveragerc | 7 + .gitignore | 10 + .pre-commit-config.yaml | 39 + .pylintrc | 558 +++++++++++ .style.yapf | 397 ++++++++ CODE_OF_CONDUCT.md | 127 +++ CONTRIBUTING.md | 61 ++ LICENSE | 201 ++++ README.md | 85 ++ docs/user_guide/Federated_Leiden.md | 124 +++ docs/user_guide/Horizontal_SecureBoost.md | 290 ++++++ docs/user_guide/PSI.md | 76 ++ examples/data/breast_hetero_mini_client.csv | 570 ++++++++++++ .../data/breast_hetero_mini_client.parquet | Bin 0 -> 21062 bytes examples/data/breast_hetero_mini_server.csv | 570 ++++++++++++ .../data/breast_hetero_mini_server.parquet | Bin 0 -> 20463 bytes examples/data/iris_binary_mini_client.csv | 31 + examples/data/iris_binary_mini_server.csv | 31 + examples/data/karate_club_local_nodes1.json | 19 + examples/data/karate_club_local_nodes2.json | 19 + examples/data/karate_club_user_weight1.json | 176 ++++ examples/data/karate_club_user_weight2.json | 190 ++++ examples/data/students_reg_mini_client.csv | 26 + examples/data/students_reg_mini_server.csv | 26 + examples/leiden.py | 64 ++ examples/psi.py | 63 ++ images/framework.jpeg | Bin 0 -> 195096 bytes images/graph.png | Bin 0 -> 126550 bytes images/secureboost_inference.jpeg | Bin 0 -> 89496 bytes images/secureboost_train.jpeg | Bin 0 -> 140175 bytes petml/__init__.py | 21 + petml/fl/__init__.py | 13 + petml/fl/base.py | 27 + petml/fl/boosting/__init__.py | 15 + petml/fl/boosting/decision_tree.py | 347 +++++++ petml/fl/boosting/loss.py | 133 +++ petml/fl/boosting/metric.py | 59 ++ petml/fl/boosting/xgb_model.py | 879 ++++++++++++++++++ petml/fl/graph/__init__.py | 19 + petml/fl/graph/leiden/__init__.py | 13 + petml/fl/graph/leiden/base.py | 172 ++++ petml/fl/graph/leiden/leiden.py | 577 ++++++++++++ petml/fl/graph/leiden/utils.py | 42 + petml/fl/preprocessing/__init__.py | 15 + petml/fl/preprocessing/psi.py | 63 ++ petml/infra/__init__.py | 13 + petml/infra/abc/__init__.py | 17 + petml/infra/abc/engine.py | 28 + petml/infra/abc/network.py | 63 ++ petml/infra/abc/storage.py | 28 + petml/infra/common/__init__.py | 13 + petml/infra/common/log_utils.py | 52 ++ petml/infra/engine/__init__.py | 13 + petml/infra/engine/cipher_engine/__init__.py | 15 + .../engine/cipher_engine/cipher_engine.py | 38 + petml/infra/network/__init__.py | 16 + petml/infra/network/cluster_def.py | 46 + petml/infra/network/network_factory.py | 34 + petml/infra/network/petnetNetwork/__init__.py | 13 + petml/infra/network/petnetNetwork/network.py | 88 ++ petml/infra/storage/__init__.py | 13 + petml/infra/storage/graph_storage/__init__.py | 15 + .../storage/graph_storage/json_storage.py | 71 ++ .../infra/storage/tabular_storage/__init__.py | 16 + petml/infra/storage/tabular_storage/base.py | 32 + .../storage/tabular_storage/csv_storage.py | 87 ++ .../tabular_storage/parquet_storage.py | 60 ++ petml/operators/__init__.py | 17 + petml/operators/boosting/__init__.py | 15 + petml/operators/boosting/xgb_model.py | 321 +++++++ petml/operators/graph/__init__.py | 15 + petml/operators/graph/leiden.py | 79 ++ petml/operators/operator_base.py | 59 ++ petml/operators/preprocessing/__init__.py | 15 + petml/operators/preprocessing/psi.py | 77 ++ petml/version.py | 15 + pytest.ini | 6 + requirements.txt | 7 + setup.py | 58 ++ tests/__init__.py | 0 tests/configs.py | 18 + tests/conftest.py | 30 + tests/fl/__init__.py | 0 tests/fl/graph/test_leiden.py | 109 +++ tests/fl/preprocessing/test_psi.py | 41 + tests/infra/__init__.py | 13 + tests/infra/common/__init__.py | 13 + tests/infra/common/test_log.py | 25 + tests/infra/engine/__init__.py | 13 + tests/infra/engine/test_cipher_engine.py | 86 ++ tests/infra/network/__init__.py | 13 + tests/infra/network/test_petnet.py | 46 + tests/infra/storage/__init__.py | 13 + tests/infra/storage/test_tabular.py | 33 + tests/operators/__init__.py | 13 + tests/operators/boosting/__init__.py | 13 + tests/operators/boosting/test_xgb.py | 217 +++++ tests/operators/graph/__init__.py | 13 + tests/operators/graph/test_leiden.py | 63 ++ tests/operators/preprocessing/__init__.py | 13 + tests/operators/preprocessing/test_psi.py | 61 ++ tests/process.py | 38 + tests/utils.py | 40 + 103 files changed, 8534 insertions(+) create mode 100644 .coveragerc create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 .pylintrc create mode 100644 .style.yapf create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 docs/user_guide/Federated_Leiden.md create mode 100644 docs/user_guide/Horizontal_SecureBoost.md create mode 100644 docs/user_guide/PSI.md create mode 100644 examples/data/breast_hetero_mini_client.csv create mode 100644 examples/data/breast_hetero_mini_client.parquet create mode 100644 examples/data/breast_hetero_mini_server.csv create mode 100644 examples/data/breast_hetero_mini_server.parquet create mode 100644 examples/data/iris_binary_mini_client.csv create mode 100644 examples/data/iris_binary_mini_server.csv create mode 100644 examples/data/karate_club_local_nodes1.json create mode 100644 examples/data/karate_club_local_nodes2.json create mode 100644 examples/data/karate_club_user_weight1.json create mode 100644 examples/data/karate_club_user_weight2.json create mode 100644 examples/data/students_reg_mini_client.csv create mode 100644 examples/data/students_reg_mini_server.csv create mode 100644 examples/leiden.py create mode 100644 examples/psi.py create mode 100644 images/framework.jpeg create mode 100644 images/graph.png create mode 100644 images/secureboost_inference.jpeg create mode 100644 images/secureboost_train.jpeg create mode 100644 petml/__init__.py create mode 100644 petml/fl/__init__.py create mode 100644 petml/fl/base.py create mode 100644 petml/fl/boosting/__init__.py create mode 100644 petml/fl/boosting/decision_tree.py create mode 100644 petml/fl/boosting/loss.py create mode 100644 petml/fl/boosting/metric.py create mode 100644 petml/fl/boosting/xgb_model.py create mode 100644 petml/fl/graph/__init__.py create mode 100644 petml/fl/graph/leiden/__init__.py create mode 100644 petml/fl/graph/leiden/base.py create mode 100644 petml/fl/graph/leiden/leiden.py create mode 100644 petml/fl/graph/leiden/utils.py create mode 100644 petml/fl/preprocessing/__init__.py create mode 100644 petml/fl/preprocessing/psi.py create mode 100644 petml/infra/__init__.py create mode 100644 petml/infra/abc/__init__.py create mode 100644 petml/infra/abc/engine.py create mode 100644 petml/infra/abc/network.py create mode 100644 petml/infra/abc/storage.py create mode 100644 petml/infra/common/__init__.py create mode 100644 petml/infra/common/log_utils.py create mode 100644 petml/infra/engine/__init__.py create mode 100644 petml/infra/engine/cipher_engine/__init__.py create mode 100644 petml/infra/engine/cipher_engine/cipher_engine.py create mode 100644 petml/infra/network/__init__.py create mode 100644 petml/infra/network/cluster_def.py create mode 100644 petml/infra/network/network_factory.py create mode 100644 petml/infra/network/petnetNetwork/__init__.py create mode 100644 petml/infra/network/petnetNetwork/network.py create mode 100644 petml/infra/storage/__init__.py create mode 100644 petml/infra/storage/graph_storage/__init__.py create mode 100644 petml/infra/storage/graph_storage/json_storage.py create mode 100644 petml/infra/storage/tabular_storage/__init__.py create mode 100644 petml/infra/storage/tabular_storage/base.py create mode 100644 petml/infra/storage/tabular_storage/csv_storage.py create mode 100644 petml/infra/storage/tabular_storage/parquet_storage.py create mode 100644 petml/operators/__init__.py create mode 100644 petml/operators/boosting/__init__.py create mode 100644 petml/operators/boosting/xgb_model.py create mode 100644 petml/operators/graph/__init__.py create mode 100644 petml/operators/graph/leiden.py create mode 100644 petml/operators/operator_base.py create mode 100644 petml/operators/preprocessing/__init__.py create mode 100644 petml/operators/preprocessing/psi.py create mode 100644 petml/version.py create mode 100644 pytest.ini create mode 100644 requirements.txt create mode 100644 setup.py create mode 100644 tests/__init__.py create mode 100644 tests/configs.py create mode 100644 tests/conftest.py create mode 100644 tests/fl/__init__.py create mode 100644 tests/fl/graph/test_leiden.py create mode 100644 tests/fl/preprocessing/test_psi.py create mode 100644 tests/infra/__init__.py create mode 100644 tests/infra/common/__init__.py create mode 100644 tests/infra/common/test_log.py create mode 100644 tests/infra/engine/__init__.py create mode 100644 tests/infra/engine/test_cipher_engine.py create mode 100644 tests/infra/network/__init__.py create mode 100644 tests/infra/network/test_petnet.py create mode 100644 tests/infra/storage/__init__.py create mode 100644 tests/infra/storage/test_tabular.py create mode 100644 tests/operators/__init__.py create mode 100644 tests/operators/boosting/__init__.py create mode 100644 tests/operators/boosting/test_xgb.py create mode 100644 tests/operators/graph/__init__.py create mode 100644 tests/operators/graph/test_leiden.py create mode 100644 tests/operators/preprocessing/__init__.py create mode 100644 tests/operators/preprocessing/test_psi.py create mode 100644 tests/process.py create mode 100644 tests/utils.py diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..d67edf0 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,7 @@ +[run] +concurrency=multiprocessing +include = + petml/* +omit = + tests/* + petml/infra/abc/* diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e56d500 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +### Python template +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class +*.DS_Store + +# ide +.idea/* +venv/* diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..98c27a5 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,39 @@ +default_language_version: + # force all unspecified python hooks to run python3 + python: python3 +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: check-added-large-files + - id: check-case-conflict + - id: check-merge-conflict + - id: check-symlinks + - id: check-yaml + - id: end-of-file-fixer + - id: forbid-submodules + - id: mixed-line-ending + - id: trailing-whitespace + - repo: https://github.com/google/yapf/ + rev: v0.40.2 + hooks: + - id: yapf + name: yapf + language: python + entry: yapf + args: ["--style=.style.yapf", "-i"] + types: [python] + - repo: https://github.com/pylint-dev/pylint/ + rev: v3.0.2 + hooks: + - id: pylint + name: pylint + entry: pylint + language: python + types: [python] + args: + [ + "-rn", # Only display messages + "-sn", # Don't display the score + "--rcfile=.pylintrc" + ] diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..7903bc6 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,558 @@ +[MAIN] + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Files or directories to be skipped. They should be base names, not +# paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the ignore-list. The +# regex matches against paths and can be in Posix or Windows format. +ignore-paths= + +# Files or directories matching the regex patterns are skipped. The regex +# matches against base names, not paths. +ignore-patterns=^\.# + +# Pickle collected data for later comparisons. +persistent=yes + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + pylint.extensions.check_elif, + pylint.extensions.bad_builtin, + pylint.extensions.docparams, + pylint.extensions.for_any_all, + pylint.extensions.set_membership, + pylint.extensions.code_style, + pylint.extensions.overlapping_exceptions, + pylint.extensions.typing, + pylint.extensions.redefined_variable_type, + pylint.extensions.comparison_placement, + pylint.extensions.broad_try_clause, + pylint.extensions.dict_init_mutate, + pylint.extensions.consider_refactoring_into_while_condition, + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use. +jobs=1 + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code +extension-pkg-allow-list= + +# Minimum supported python version +py-version = 3.8.0 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# Specify a score threshold under which the program will exit with error. +fail-under=10.0 + +# Return non-zero exit code if any of these messages/categories are detected, +# even if score is above --fail-under value. Syntax same as enable. Messages +# specified are enabled, while categories only check already-enabled messages. +fail-on= + +# Clear in-memory caches upon conclusion of linting. Useful if running pylint in +# a server-like mode. +clear-cache-post-run=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED +# confidence= + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable= + use-symbolic-message-instead, + useless-suppression, + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then re-enable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" + +disable= + attribute-defined-outside-init, + invalid-name, + missing-docstring, + protected-access, + too-few-public-methods, + # handled by black + format, + # We anticipate #3512 where it will become optional + fixme, + consider-using-assignment-expr, + logging-fstring-interpolation, + unspecified-encoding, + redefined-variable-type, + too-many-locals, + too-many-instance-attributes, + too-many-public-methods, + import-error, + consider-using-alias, + consider-alternative-union-syntax, + arguments-differ, + + +[REPORTS] + +# Set the output format. Available formats are text, parseable, colorized, msvs +# (visual studio) and html. You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages +reports=no + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables 'fatal', 'error', 'warning', 'refactor', 'convention' +# and 'info', which contain the number of messages in each category, as +# well as 'statement', which is the total number of statements analyzed. This +# score is used by the global evaluation report (RP0004). +evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details +#msg-template= + +# Activate the evaluation score. +score=yes + + +[LOGGING] + +# Logging modules to check that the string format arguments are in logging +# function parameter format +logging-modules=logging + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME,XXX,TODO + +# Regular expression of note tags to take in consideration. +#notes-rgx= + + +[SIMILARITIES] + +# Minimum lines number of a similarity. +min-similarity-lines=6 + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=yes + +# Signatures are removed from the similarity computation +ignore-signatures=yes + + +[VARIABLES] + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_,_cb + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of names allowed to shadow builtins +allowed-redefined-builtins= + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io + + +[FORMAT] + +# Maximum number of characters on a single line. +max-line-length=120 + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Maximum number of lines in a module +max-module-lines=2000 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + + +[BASIC] + +# Good variable names which should always be accepted, separated by a comma +good-names=i,j,k,ex,Run,_ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo,bar,baz,toto,tutu,tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Include a hint for the correct naming format with invalid-name +include-naming-hint=no + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names +function-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names +variable-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names +const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names +attr-rgx=[a-z_][a-z0-9_]{2,}$ + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names +argument-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names +class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Naming style matching correct class constant names. +class-const-naming-style=UPPER_CASE + +# Regular expression matching correct class constant names. Overrides class- +# const-naming-style. +#class-const-rgx= + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names +inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names +class-rgx=[A-Z_][a-zA-Z0-9]+$ + + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names +method-rgx=[a-z_][a-z0-9_]{2,}$ + +# Regular expression matching correct type variable names +#typevar-rgx= + +# Regular expression which should only match function or class names that do +# not require a docstring. Use ^(?!__init__$)_ to also check __init__. +no-docstring-rgx=__.*__ + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# List of decorators that define properties, such as abc.abstractproperty. +property-classes=abc.abstractproperty + + +[TYPECHECK] + +# Regex pattern to define which classes are considered mixins if ignore-mixin- +# members is set to 'yes' +mixin-class-rgx=.*MixIn + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=SQLObject, optparse.Values, thread._local, _thread._local + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members=REQUEST,acl_users,aq_parent,argparse.Namespace + +# List of decorators that create context managers from functions, such as +# contextlib.contextmanager. +contextmanager-decorators=contextlib.contextmanager + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +[SPELLING] + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# List of comma separated words that should be considered directives if they +# appear and the beginning of a comment and should not be checked. +spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy:,pragma:,# noinspection + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file=.pyenchant_pylint_custom_dict.txt + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=2 + + +[DESIGN] + +# Maximum number of arguments for function / method +max-args = 9 + +# Maximum number of locals for function / method body +max-locals = 19 + +# Maximum number of return / yield for function / method body +max-returns=11 + +# Maximum number of branch for function / method body +max-branches = 20 + +# Maximum number of statements in function / method body +max-statements = 50 + +# Maximum number of attributes for a class (see R0902). +max-attributes=11 + +# Maximum number of statements in a try-block +max-try-statements = 7 + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__,__new__,setUp,__post_init__ + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict,_fields,_replace,_source,_make + +# Warn about protected attribute access inside special methods +check-protected-access-in-special-methods=no + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Allow explicit reexports by alias from a package __init__. +allow-reexport-from-package=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=regsub,TERMIOS,Bastion,rexec + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception" +overgeneral-exceptions=builtins.Exception + + +[TYPING] + +# Set to ``no`` if the app / library does **NOT** need to support runtime +# introspection of type annotations. If you use type annotations +# **exclusively** for type checking of an application, you're probably fine. +# For libraries, evaluate if some users what to access the type hints at +# runtime first, e.g., through ``typing.get_type_hints``. Applies to Python +# versions 3.7 - 3.9 +runtime-typing = no + + +[DEPRECATED_BUILTINS] + +# List of builtins function names that should not be used, separated by a comma +bad-functions=map,input + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit,argparse.parse_error + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[CODE_STYLE] + +# Max line length for which to sill emit suggestions. Used to prevent optional +# suggestions which would get split by a code formatter (e.g., black). Will +# default to the setting for ``max-line-length``. +#max-line-length-suggestions= diff --git a/.style.yapf b/.style.yapf new file mode 100644 index 0000000..ecc2642 --- /dev/null +++ b/.style.yapf @@ -0,0 +1,397 @@ +[style] +# Align closing bracket with visual indentation. +align_closing_bracket_with_visual_indent=False + +# Allow dictionary keys to exist on multiple lines. For example: +# +# x = { +# ('this is the first element of a tuple', +# 'this is the second element of a tuple'): +# value, +# } +allow_multiline_dictionary_keys=False + +# Allow lambdas to be formatted on more than one line. +allow_multiline_lambdas=False + +# Allow splitting before a default / named assignment in an argument list. +allow_split_before_default_or_named_assigns=True + +# Allow splits before the dictionary value. +allow_split_before_dict_value=True + +# Let spacing indicate operator precedence. For example: +# +# a = 1 * 2 + 3 / 4 +# b = 1 / 2 - 3 * 4 +# c = (1 + 2) * (3 - 4) +# d = (1 - 2) / (3 + 4) +# e = 1 * 2 - 3 +# f = 1 + 2 + 3 + 4 +# +# will be formatted as follows to indicate precedence: +# +# a = 1*2 + 3/4 +# b = 1/2 - 3*4 +# c = (1+2) * (3-4) +# d = (1-2) / (3+4) +# e = 1*2 - 3 +# f = 1 + 2 + 3 + 4 +# +arithmetic_precedence_indication=False + +# Number of blank lines surrounding top-level function and class +# definitions. +blank_lines_around_top_level_definition=2 + +# Number of blank lines between top-level imports and variable +# definitions. +blank_lines_between_top_level_imports_and_variables=1 + +# Insert a blank line before a class-level docstring. +blank_line_before_class_docstring=False + +# Insert a blank line before a module docstring. +blank_line_before_module_docstring=False + +# Insert a blank line before a 'def' or 'class' immediately nested +# within another 'def' or 'class'. For example: +# +# class Foo: +# # <------ this blank line +# def method(): +# ... +blank_line_before_nested_class_or_def=True + +# Do not split consecutive brackets. Only relevant when +# dedent_closing_brackets is set. For example: +# +# call_func_that_takes_a_dict( +# { +# 'key1': 'value1', +# 'key2': 'value2', +# } +# ) +# +# would reformat to: +# +# call_func_that_takes_a_dict({ +# 'key1': 'value1', +# 'key2': 'value2', +# }) +coalesce_brackets=False + +# The column limit. +column_limit=120 + +# The style for continuation alignment. Possible values are: +# +# - SPACE: Use spaces for continuation alignment. This is default behavior. +# - FIXED: Use fixed number (CONTINUATION_INDENT_WIDTH) of columns +# (ie: CONTINUATION_INDENT_WIDTH/INDENT_WIDTH tabs or +# CONTINUATION_INDENT_WIDTH spaces) for continuation alignment. +# - VALIGN-RIGHT: Vertically align continuation lines to multiple of +# INDENT_WIDTH columns. Slightly right (one tab or a few spaces) if +# cannot vertically align continuation lines with indent characters. +continuation_align_style=SPACE + +# Indent width used for line continuations. +continuation_indent_width=4 + +# Put closing brackets on a separate line, dedented, if the bracketed +# expression can't fit in a single line. Applies to all kinds of brackets, +# including function definitions and calls. For example: +# +# config = { +# 'key1': 'value1', +# 'key2': 'value2', +# } # <--- this bracket is dedented and on a separate line +# +# time_series = self.remote_client.query_entity_counters( +# entity='dev3246.region1', +# key='dns.query_latency_tcp', +# transform=Transformation.AVERAGE(window=timedelta(seconds=60)), +# start_ts=now()-timedelta(days=3), +# end_ts=now(), +# ) # <--- this bracket is dedented and on a separate line +dedent_closing_brackets=False + +# Disable the heuristic which places each list element on a separate line +# if the list is comma-terminated. +disable_ending_comma_heuristic=False + +# Place each dictionary entry onto its own line. +each_dict_entry_on_separate_line=True + +# Require multiline dictionary even if it would normally fit on one line. +# For example: +# +# config = { +# 'key1': 'value1' +# } +force_multiline_dict=False + +# The regex for an i18n comment. The presence of this comment stops +# reformatting of that line, because the comments are required to be +# next to the string they translate. +i18n_comment=#\..* + +# The i18n function call names. The presence of this function stops +# reformattting on that line, because the string it has cannot be moved +# away from the i18n comment. +i18n_function_call=N_, _ + +# Indent blank lines. +indent_blank_lines=False + +# Put closing brackets on a separate line, indented, if the bracketed +# expression can't fit in a single line. Applies to all kinds of brackets, +# including function definitions and calls. For example: +# +# config = { +# 'key1': 'value1', +# 'key2': 'value2', +# } # <--- this bracket is indented and on a separate line +# +# time_series = self.remote_client.query_entity_counters( +# entity='dev3246.region1', +# key='dns.query_latency_tcp', +# transform=Transformation.AVERAGE(window=timedelta(seconds=60)), +# start_ts=now()-timedelta(days=3), +# end_ts=now(), +# ) # <--- this bracket is indented and on a separate line +indent_closing_brackets=False + +# Indent the dictionary value if it cannot fit on the same line as the +# dictionary key. For example: +# +# config = { +# 'key1': +# 'value1', +# 'key2': value1 + +# value2, +# } +indent_dictionary_value=True + +# The number of columns to use for indentation. +indent_width=4 + +# Join short lines into one line. E.g., single line 'if' statements. +join_multiple_lines=False + +# Do not include spaces around selected binary operators. For example: +# +# 1 + 2 * 3 - 4 / 5 +# +# will be formatted as follows when configured with "*,/": +# +# 1 + 2*3 - 4/5 +no_spaces_around_selected_binary_operators= + +# Use spaces around default or named assigns. +spaces_around_default_or_named_assign=False + +# Adds a space after the opening '{' and before the ending '}' dict delimiters. +# +# {1: 2} +# +# will be formatted as: +# +# { 1: 2 } +spaces_around_dict_delimiters=False + +# Adds a space after the opening '[' and before the ending ']' list delimiters. +# +# [1, 2] +# +# will be formatted as: +# +# [ 1, 2 ] +spaces_around_list_delimiters=False + +# Use spaces around the power operator. +spaces_around_power_operator=False + +# Use spaces around the subscript / slice operator. For example: +# +# my_list[1 : 10 : 2] +spaces_around_subscript_colon=False + +# Adds a space after the opening '(' and before the ending ')' tuple delimiters. +# +# (1, 2, 3) +# +# will be formatted as: +# +# ( 1, 2, 3 ) +spaces_around_tuple_delimiters=False + +# The number of spaces required before a trailing comment. +# This can be a single value (representing the number of spaces +# before each trailing comment) or list of values (representing +# alignment column values; trailing comments within a block will +# be aligned to the first column value that is greater than the maximum +# line length within the block). For example: +# +# With spaces_before_comment=5: +# +# 1 + 1 # Adding values +# +# will be formatted as: +# +# 1 + 1 # Adding values <-- 5 spaces between the end of the statement and comment +# +# With spaces_before_comment=15, 20: +# +# 1 + 1 # Adding values +# two + two # More adding +# +# longer_statement # This is a longer statement +# short # This is a shorter statement +# +# a_very_long_statement_that_extends_beyond_the_final_column # Comment +# short # This is a shorter statement +# +# will be formatted as: +# +# 1 + 1 # Adding values <-- end of line comments in block aligned to col 15 +# two + two # More adding +# +# longer_statement # This is a longer statement <-- end of line comments in block aligned to col 20 +# short # This is a shorter statement +# +# a_very_long_statement_that_extends_beyond_the_final_column # Comment <-- the end of line comments are aligned based on the line length +# short # This is a shorter statement +# +spaces_before_comment=2 + +# Insert a space between the ending comma and closing bracket of a list, +# etc. +space_between_ending_comma_and_closing_bracket=False + +# Use spaces inside brackets, braces, and parentheses. For example: +# +# method_call( 1 ) +# my_dict[ 3 ][ 1 ][ get_index( *args, **kwargs ) ] +# my_set = { 1, 2, 3 } +space_inside_brackets=False + +# Split before arguments +split_all_comma_separated_values=False + +# Split before arguments, but do not split all subexpressions recursively +# (unless needed). +split_all_top_level_comma_separated_values=False + +# Split before arguments if the argument list is terminated by a +# comma. +split_arguments_when_comma_terminated=False + +# Set to True to prefer splitting before '+', '-', '*', '/', '//', or '@' +# rather than after. +split_before_arithmetic_operator=False + +# Set to True to prefer splitting before '&', '|' or '^' rather than +# after. +split_before_bitwise_operator=False + +# Split before the closing bracket if a list or dict literal doesn't fit on +# a single line. +split_before_closing_bracket=True + +# Split before a dictionary or set generator (comp_for). For example, note +# the split before the 'for': +# +# foo = { +# variable: 'Hello world, have a nice day!' +# for variable in bar if variable != 42 +# } +split_before_dict_set_generator=False + +# Split before the '.' if we need to split a longer expression: +# +# foo = ('This is a really long string: {}, {}, {}, {}'.format(a, b, c, d)) +# +# would reformat to something like: +# +# foo = ('This is a really long string: {}, {}, {}, {}' +# .format(a, b, c, d)) +split_before_dot=False + +# Split after the opening paren which surrounds an expression if it doesn't +# fit on a single line. +split_before_expression_after_opening_paren=False + +# If an argument / parameter list is going to be split, then split before +# the first argument. +split_before_first_argument=False + +# Set to True to prefer splitting before 'and' or 'or' rather than +# after. +split_before_logical_operator=False + +# Split named assignments onto individual lines. +split_before_named_assigns=True + +# Set to True to split list comprehensions and generators that have +# non-trivial expressions and multiple clauses before each of these +# clauses. For example: +# +# result = [ +# a_long_var + 100 for a_long_var in xrange(1000) +# if a_long_var % 10] +# +# would reformat to something like: +# +# result = [ +# a_long_var + 100 +# for a_long_var in xrange(1000) +# if a_long_var % 10] +split_complex_comprehension=True + +# The penalty for splitting right after the opening bracket. +split_penalty_after_opening_bracket=300 + +# The penalty for splitting the line after a unary operator. +split_penalty_after_unary_operator=10000 + +# The penalty of splitting the line around the '+', '-', '*', '/', '//', +# ``%``, and '@' operators. +split_penalty_arithmetic_operator=300 + +# The penalty for splitting right before an if expression. +split_penalty_before_if_expr=0 + +# The penalty of splitting the line around the '&', '|', and '^' +# operators. +split_penalty_bitwise_operator=300 + +# The penalty for splitting a list comprehension or generator +# expression. +split_penalty_comprehension=2100 + +# The penalty for characters over the column limit. +split_penalty_excess_character=7000 + +# The penalty incurred by adding a line split to the unwrapped line. The +# more line splits added the higher the penalty. +split_penalty_for_added_line_split=30 + +# The penalty of splitting a list of "import as" names. For example: +# +# from a_very_long_or_indented_module_name_yada_yad import (long_argument_1, +# long_argument_2, +# long_argument_3) +# +# would reformat to something like: +# +# from a_very_long_or_indented_module_name_yada_yad import ( +# long_argument_1, long_argument_2, long_argument_3) +split_penalty_import_names=0 + +# The penalty of splitting the line around the 'and' and 'or' +# operators. +split_penalty_logical_operator=300 + +# Use the Tab character for indentation. +use_tabs=False diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..41dd9eb --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,127 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..7f4f0d1 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,61 @@ +# Contributing to PETML + +Thank you for investing your time in contributing to PETML! + +Read our [Code of Coduct](./CODE_OF_CONDUCT.md) to keep our community approachable and respectable. + +This guide details how to use issues and pull requests to improve PETML. + +## General Guidelines + +### Pull Requests + +Make sure to keep Pull Requests small and functional to make them easier to review, understand, and look up in commit history. This repository uses "Squash and Commit" to keep our history clean and make it easier to revert changes based on PR. + +Adding the appropriate documentation, unit tests and e2e tests as part of a feature is the responsibility of the feature owner, whether it is done in the same Pull Request or not. + +Pull Requests should follow the "subject: message" format, where the subject describes what part of the code is being modified. + +Refer to the template for more information on what goes into a PR description. + +### Design Docs + +A contributor proposes a design with a PR on the repository to allow for revisions and discussions. If a design needs to be discussed before formulating a document for it, make use of Google doc and GitHub issue to involve the community on the discussion. + +### GitHub Issues + +GitHub Issues are used to file bugs, work items, and feature requests with actionable items/issues (Please refer to the "Reporting Bugs/Feature Requests" section below for more information). + +### Reporting Bugs/Feature Requests + +We welcome you to use the GitHub issue tracker to report bugs or suggest features that have actionable items/issues (as opposed to introducing a feature request on GitHub Discussions). + +When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: + +- A reproducible test case or series of steps +- The version of the code being used +- Any modifications you've made relevant to the bug +- Anything unusual about your environment or deployment + +## Contributing via Pull Requests + +### Find interesting issue + +If you spot a problem with the problem, [search if an issue already exists](https://github.com/tiktok-privacy-innovation/PETML/issues). If a related issue doesn't exist, you can open a new issue using [issue template](https://github.com/tiktok-privacy-innovation/PETML/issues/new/choose). + +### Solve an issue + +Please check `DEVELOPMENT.md` in sub folder to get familar with running and testing codes. + +### Open a Pull request. + +When you're done making the changes, open a pull request and fill PR template so we can better review your PR. The template helps reviewers understand your changes and the purpose of your pull request. + +Don't forget to link PR to issue if you are solving one. + +If you run into any merge issues, checkout this [git tutorial](https://lab.github.com/githubtraining/managing-merge-conflicts) to help you resolve merge conflicts and other issues. + + +## Finding contributions to work on + +Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' and 'good first issue' issues are a great place to start. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b09cd78 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ee950f5 --- /dev/null +++ b/README.md @@ -0,0 +1,85 @@ +# PETML +Privacy-Enhance-Technology Machine learning (PETML) is an open-source framework facilitates data collaboration among +multi-parties while ensuring data security and privacy. It allows users to smoothly construct and develop federated +learning algorithms or privacy-preserving machine learning. PETML supports the ability to construct an algorithm +workflow that integrates components such as data processing, model training, and model prediction, thereby offering +a comprehensive suite for algorithm development requirements. PETML leverages the capability of +[PETAce](https://github.com/tiktok-privacy-innovation/PETAce) to deliver the foundational secure computing protocol, +thereby facilitating secure computational operations. + + +![framework](./images/framework.jpeg) +The above figure illustrates the framework of PETML. The infra layer encapsulates fundamental components like MPC, Network and storage, +offering the foundational capabilities required for the algorithm layer. This algorithm layer is responsible for developing +the core algorithms. Finally, the operation layer serves as an interface with the platform layer, supplying the essential +algorithmic unit components for utilization by the platform or users. + +## Getting Started + +### Requirements: +|System|Toolchain| +|------|-------| +|Linux/Debian|Python (==3.9), pip(>=23.3.1)| + +## User installation + +PETAce must be installed first. Please refer to [PETAce](https://github.com/tiktok-privacy-innovation/PETAce) to build petace.whl package + +``` +pip install peatce.whl +``` + +Secondly, the user is required to utilize pip to install the dependencies enumerated in the requirements + +``` +pip install -r requirements.txt +``` + +Upon successful installation of the dependencies, PETML can be installed using + +``` +python3 setup.py install +``` + +## Quick Start + +We provide an example to help you quickly use PETML. Below is a quick case about federated Leiden algorithm. +> Note: If you wish to experiment with your own dataset or adjust any parameters, simply modify the corresponding +value in the configuration dictionary in examples/leiden.py. For more detailed information on the parameters, +please refer to the provided documents in the algorithm list section. Please remember to verify the consistency of +the data features provided by the two parties. If not, it is recommended to initially perform a Private Set Intersection (PSI) operation. + +``` +python3 examples/leiden.py -p party_a +python3 examples/leiden.py -p party_b +``` + + +## Algorithm list + +Federated preprocessing: PSI +Federated Machine Learning Algorithms: GBDT, graph clustering +Model Evaluation: Binary|Regression evaluation + +| | Operator Name | Documentation | +|---------|:--------------------------------------------------------------------------------------------------------------------------------------------:|-----------------------------------------------------------------------| +| PSI | preprocessing.PSITransform | [PSI](./docs/user_guide/PSI.md) | +| Leiden | graph.LeidenTransform | [Federated Leiden](./docs/user_guide/Federated_Leiden.md) | +| XGBoost | boosting. XGBoostClassifierFit
boosting.XGBoostClassifierPredict
boosting.XGBoostRegressorFit
boosting.XGBoostRegressorPredict | [Horizontal SecureBoost](./docs/user_guide/Horizontal_SecureBoost.md) | + +More to come. + +## Contribution +Please check [Contributing](CONTRIBUTING.md) for more details. + +## Code of Conduct + +Please check [Code of Conduct](CODE_OF_CONDUCT.md) for more details. + +## License + +This project is licensed under the [Apache-2.0 License](LICENSE). + +## Disclaimer +This software is not an officially supported product of TikTok. It is provided as-is, without any guarantees or +warranties, whether express or implied. diff --git a/docs/user_guide/Federated_Leiden.md b/docs/user_guide/Federated_Leiden.md new file mode 100644 index 0000000..dd70dca --- /dev/null +++ b/docs/user_guide/Federated_Leiden.md @@ -0,0 +1,124 @@ +# Federated Leiden + +## Introduction +Community detection algorithms aim to identify groups of nodes in a network that are more densely connected than the +rest of the network. One such algorithm is the Leiden algorithm, which is a refinement of the Louvain algorithm. + +In the cross-party scenario, the network is distributed among multiple parties, each responsible for handling a specific +portion of the global graph. The federated Leiden algorithm enables community detection across parties while protecting +data privacy. + +## Leiden Algorithm +The problem of community detection requires the partition of a graph into communities of densely connected vertices, with +the vertices belonging to different communities being only sparsely connected. The Leiden algorithm is based on the Louvain +algorithm. It maximizes modularity (see Eq(1) in [[1]](#reference)) by joining vertices into communities when it increases +modularity, which is conducted in two iteratively repeating phases: **Move nodes** and **Aggregate nodes**. More details +of Leiden can be found in [[1]](#reference). + +## Details of Federated Leiden +### Setting +#### Scenario setting +Our proposal is designed for two parties (A & B), to jointly build a secure Leiden model. Under this scenario, each party +can only observe a small subgraph of the network. +![graph](./../../images/graph.png) +From the diagram above, the entire network $G$ is composed of sub-networks $G_A$ and $G_B$ from two parties. We refer +to the vertices that connect sub-networks as '**ghost vertices**', while the remaining ones are termed '**local vertices**'. +Edges that link vertices within the same party are established in plaintext locally. While, edges that link vertices across +different parties exist in the form of secret shares, distributed within the two involved parties. For vertices belonging +to party B, party A only knows the de-identified vertex information. The same applies to party A. + +#### Security setting +To ensure a secure two-party computation, we will employ the MPC protocol of [PETAce](https://github.com/tiktok-privacy-innovation/PETAce) +in a semi-honest adversarial environment. PETAce provides computation protocols under two-party settings. The protocols include: +- Secure 2-party Addition: $[z]=[x]+[y]$ +- Secure 2-party Multiplication: $[z]=[x]\cdot[y]$ +- Secure 2-party Division: $[z]=[x]/[y]$ +- Secure 2-party Argmax: $[z]=argmax([x])$ + +### Algorithm Details +The Federated Leiden algorithm is also conducted in two iteratively repeating phases. We will use MPC to do calculation +in both phases when the vertex is ghost vertex. + +**Move nodes:** +The algorithm behaves differently based on the parties involved in the vertices. If all vertices belong to the same party, +the computation is executed in plaintext, as outlined in [[1]](#reference)). On the other hand, if vertices come from different +parties, the computation of modularity is carried out using MPC. Lastly, the secure two-party argmax function is used to decide the +direction of movement for the node that maximizes the value of modularity. + +**Aggregate nodes:** +We call a vertex **cross-party** if it is aggregated from vertices of different parties. Similar to the step in the Move nodes +phase, if all vertices belong to the same party, the computation is executed in plaintext. If vertices from multiple parties +are involved, secure 2-party addition is used to aggregate values derived from the edges within the network. + +We are preparing a detailed whitepaper that will provide in-depth information about the algorithm. Please stay tuned if you are interested. + + +## Config + +### Module Name +``` +petml.operators.graph.LeidenTansform +``` + +### Model Parameters +| Name | Type | Description | Default | +|-----------|------|-------------------------------------|---------| +| ```max_move``` | int | The limit times of local node move | 20 | +| ```max_merge``` | int | The limit times of merge meta nodes | 10 | + +### Input +| Name | File Type | Description | +|--------------|-----------|--------------------------------| +| user_weights | json | The weight of user-user weight | +| local_nodes | json | The local nodes of each party | + +### Output + +| Name | File Type | Description | +|---------|-----------|--------------------------------| +| cluster | csv | The result of Leiden algorithm | + +### Examples +``` + config = { + "common": { + "max_move": 20, + "max_merge": 10, + "network_mode": "petnet", + "network_scheme": "socket", + "parties": { + "party_a": { + "address": ["127.0.0.1:50011"] + }, + "party_b": { + "address": ["127.0.0.1:50012"] + } + } + }, + "party_a": { + "inputs": { + "user_weights": "examples/data/karate_club_user_weight1.json", + "local_nodes": "examples/data/karate_club_local_nodes1.json", + }, + "outputs": { + "cluster": "tmp/server/cluster.csv" + } + }, + "party_b": { + "inputs": { + "user_weights": "examples/data/karate_club_user_weight2.json", + "local_nodes": "examples/data/karate_club_local_nodes2.json", + }, + "outputs": { + "cluster": "tmp/client/cluster.csv" + } + } +} + +#if run the code in party a, the party should be 'party_a' and vice versa +operator = petml.operators.graph.LeidenTansform(party) +operator.run(config_map) +``` + +## Reference +[1] [From Louvain to Leiden: guaranteeing well-connected communities](https://www.nature.com/articles/s41598-019-41695-z.pdf) diff --git a/docs/user_guide/Horizontal_SecureBoost.md b/docs/user_guide/Horizontal_SecureBoost.md new file mode 100644 index 0000000..e3b0a3a --- /dev/null +++ b/docs/user_guide/Horizontal_SecureBoost.md @@ -0,0 +1,290 @@ +# Horizontal SecureBoost + +## Introduction +PETML implements **secure gradient descent decision tree model** training and inference using **secure multi-party computation**. +It currently supports training for regression problems and binary classification problems within two parties. + +## XGBoost Algorithm +Gradient Boosting Decision Tree (GBDT) is a widely used statistical model for classification and regression problems. +We sketch the key steps of XGBoost below and refer readers to the paper [[1]](#references) for details. + + +According to [[1]](#references), the steps of building one tree are: + +- Step 1: Based on the loss function, the first derivative $g_i$ and the second derivative $h_i$ for each sample are +calculated by ground truth label and current predicted values. Taking logistic regression as an example, +$g_i= sigmoid(y_{pred_i}) - y_{truth_i}$$ and $$h_i = sigmoid(y_{pred_i})*(1 - sigmoid(y_{pred_i}))$. + +- Step 2: The sample that yields the largest gain is selected to perform the split in the current node. +This is achieved by enumerating all samples under each feature. (Please refer to the Eq (7) in the paper [[1]](#references).) + +- Step 3: Upon identifying the split points, the data is split into two parts, which are then allocated to the left and +right subtrees respectively. The training continues to the next level, repeating Step 2 until the conditions for tree growth +termination are fulfilled. + +- Step 4: The weight of the leaf node is determined by the samples that fall into it. It is computed using the leaf weight +formula Eq (5) in [[1]](#references). + + +## Secure-XGBoost Algorithm +To ensure no sample information is leaked, we use secure two-party computation protocol to implement the addition, +multiplication, comparison, and other necessary operations in the training and inference algorithms of GBDT. +The split feature, split points, and leaf weight values in the tree models are saved in the format of secure shares to +ensure that no information is leaked. We implement Secure-XGBoost using [PETAce](https://github.com/tiktok-privacy-innovation/PETAce), +which provides implementations of the underlying MPC protocols. + +In short, our secure-XGBoost algorithm replaces the plaintext operations of XGBoost sketched above with their corresponding +secure two-party computation versions, except Step 3. In Step 3, data splitting could lead to uneven data sizes in the left +and right subtrees depending on the input data distribution. To remove such leakage, we use all samples to calculate the gain +at every split node in each layer of the tree. + +To achieve this, we use an additional secret sharing vector which has the same length as samples and can be regarded as +the index array of the training samples, denoted as $V$. The initial value for all elements in $V$ at the root node +will be set to 1. After identifying the optimal split point through gain calculation, we employ secret sharing comparisons +with secure 2-party selection protocol to identify samples that are larger than the split point. Then, based on the index, +we locate the corresponding positions of these samples in the $$V$$ and set them to 0. Here, we have the new index array +$V_{left}$ for left subtree. The same steps are repeated for the right subtree. During the subtree calculations, +we multiply $g$ and $h$ with the $V$ by secure 2-party multiplication. + +The growth of the tree stops when it reaches the maximum depth set by the user. Note that different datasets may result +in different tree structures. To prevent potential attacks based on the structure of the tree, we construct a full binary +tree. If the tree cannot find the optimal split in the current node, it will use the split information from its parent node +to ensure accurate results. + +The key steps of building one secure tree are: +- Step 1: The first derivative $g_i$ and the second derivative $h_i$ for each sample are computed using the secret sharing-based +MPC protocol. Using logistic regression as an example, secure 2-party operations such as sigmoid, multiplication, and +subtraction are used. +- Step 2: The formula employed for the computation of gain incorporates secure 2-party addition, multiplication, and division at each iteration. +- Step 3: As discussed above. +- Step 4: The formula employed for the computation of leaf weight incorporates secure 2-party addition, and division. + +![Training process](./../../images/secureboost_train.jpeg) +
Figure 1: Tree-building process
+ +Assume we have a dataset with only one feature $f_1$. The tree identifies the optimal +split value in the first layer by enumerating all samples and obtaining a result with the split value 0.5. Using the secure +two-party comparison protocol, we can derive two index arrays, $V_{left} = [1,1,1,1,1,0,0,0]$ and $V_{right} =[0,0,0,0,0,1,1,1]$. +In the second layer of the left node, we multiply all samples with $V_{left}$ to identify the samples that fall into the left node +and repeat the progress in the first layer to find the optimal split value. The tree is constructed by repeating this process. + +## Model Secure-Inference +To mitigate the information leakage, all inference samples have to do secret comparisons with all decision nodes in each +tree. By sum of all the edges in the tree from the root to that node by using a secure 2-party addition protocol, we can +generate a secret vector $S$ that represents all paths to the leaf nodes as in Figure 2. Then, comparing secret vector +$S$ with the depth of the tree using a secure 2-party comparison protocol, we get the inference result corresponding to the sample. + +![Training process](./../../images/secureboost_inference.jpeg) +
Figure 2: Model inferencing process
+Assume we have an inference sample with the feature $f_1=4$. This value will be +compared with all decision nodes in the tree using the secure comparison protocol. Summing up the output of the comparison +via secure 2-party addition across all root-to-leave paths results in the vector$[0,1,1,2,1,2,2,3]$. By comparing this +result with the depth (=3) of the tree, we obtain the result indicator array $[0,0,0,0,0,0,0,1]$, namely the inferencing +result is the last leave node. + +## Classifier Config +### Training +#### Module Name +``` +petml.operators.boosting.XGBoostClassifierFit +``` + +#### Module Parameters +| Name | Type | Description | Default | +|-------------------------|-------|-----------------------------------------------------------------------------------------------------------------------------------|----------| +| ```min_split_loss``` | float | The minimum number of gain to split an internal node | 1e-5 | +| ```learning_rate``` | float | Learning rate shrinks the contribution of each tree by ```learning_rate``` | 0.1 | +| ```n_estimators``` | int | The number of boosting stages to perform | 100 | +| ```base_score``` | float | Initial value of $y_hat$ | 0.5 | +| ```max_depth``` | int | Maximum depth of the tree | 3 | +| ```reg_alpha``` | float | L1 regularization term on weights | 0. | +| ```reg_lambda``` | float | L2 regularization term on weights | 1. | +| ```min_child_samples``` | int | The minimum number of samples required to be at a leaf node | 1 | +| ```min_child_weight``` | float | The minimum sum of instance weight (hessian) needed in a child | 0.5 | +| ```test_size``` | float | Size of eval dataset of input data | 0.3 | +| ```eval_epochs``` | int | Calculating the evaluation metric after every certain number of epochs | 10 | +| ```eval_threshold``` | float | Regard the instances with eval prediction value larger than threshold as positive instances, and the others as negative instances | 0.5 | +| ```objective``` | str | The loss function to be optimized | logitraw | + + +#### Input +| Name | File Type | Description | +| --- |-----------| --- | +| train_data | csv | The training dataset | + +#### Output + +| Name | File Type | Description | +| --- |-----------| --- | +| model_path | pkl | The trained model | + +#### Examples +``` +config = { + "common": { + "objective": "logitraw", + "n_estimators": 10, + "max_depth": 3, + "reg_lambda": 1, + "reg_alpha": 0.0, + "min_child_weight": 0.1, + "base_score": 0.5, + "learning_rate": 0.1, + "network_mode": "petnet", + "network_scheme": "socket", + "label_name": "label", + "test_size": 0.3, + "parties": { + "party_a": { + "address": ["127.0.0.1:50011"] + }, + "party_b": { + "address": ["127.0.0.1:50012"] + } + } + }, + "party_a": { + "inputs": { + "train_data": "data0.csv", + }, + "outputs": { + "model_path": "model_name0.pkl" + } + }, + "party_b": { + "inputs": { + "train_data": "data1.csv", + }, + "outputs": { + "model_path": "model_name1.pkl" + } + } +} + +#if run the code in party a, the party should be 'party_a' and vice versa +operator = petml.operator.boosting.XGBoostClassifierFit(party) +operator.run(config_map) +``` + +### Inference +#### Module Name +``` +petml.operators.boosting.XGBoostClassifierPredict +``` + +#### Input +| Name | File Type | Description | +|--------------|-----------|-----------------------| +| predict_data | csv | The inference dataset | +| model_path | pkl | The trained model | + + +#### Output + +| Name | File Type | Description | +|--------------------|-----------|-------------------------| +| inference_res_path | csv | The result of inference | + + +#### Examples +``` +config = { + "common": { + "network_mode": "petnet", + "network_scheme": "socket", + "parties": { + "party_a": { + "address": ["127.0.0.1:50011"] + }, + "party_b": { + "address": ["127.0.0.1:50012"] + } + } + }, + "party_a": { + "inputs": { + "predict_data": "data0.csv", + "model_path": "model_name0.pkl" + }, + "outputs": { + "inference_res_path": "predict0.csv" + } +}, +"party_b": { + "inputs": { + "predict_data": "data1.csv", + "model_path": "model_name1.pkl" + }, + "outputs": { + "inference_res_path": "predict1.csv" + } +} +} + +#if run the code in party a, the party should be 'party_a' and vice versa +operator = petml.operator.boosting.XGBoostClassifierPredict(party) +operator.run(config_map) +``` + +## Classifier Config +### Training +#### Module Name +``` +petml.operators.boosting.XGBoostRegressorFit +``` +#### Module Parameters +| Name | Type | Description | Default | +|-------------------------|-------|-----------------------------------------------------------------------------------------------------------------------------------|--------------| +| ```min_split_loss``` | float | The minimum number of gain to split an internal node | 1e-5 | +| ```learning_rate``` | float | Learning rate shrinks the contribution of each tree by ```learning_rate``` | 0.1 | +| ```n_estimators``` | int | The number of boosting stages to perform | 100 | +| ```base_score``` | float | Initial value of $y_hat$ | 0.5 | +| ```max_depth``` | int | Maximum depth of the tree | 3 | +| ```reg_alpha``` | float | L1 regularization term on weights | 0. | +| ```reg_lambda``` | float | L2 regularization term on weights | 1. | +| ```min_child_samples``` | int | The minimum number of samples required to be at a leaf node | 1 | +| ```min_child_weight``` | float | The minimum sum of instance weight (hessian) needed in a child | 1 | +| ```test_size``` | float | Size of eval dataset of input data | 0.3 | +| ```eval_epochs``` | int | Calculating the evaluation metric after every certain number of epochs | 10 | +| ```eval_threshold``` | float | Regard the instances with eval prediction value larger than threshold as positive instances, and the others as negative instances | 0.5 | +| ```objective``` | str | The loss function to be optimized | squarederror | + +#### Input +| Name | File Type | Description | +|------------|-----------|----------------------| +| train_data | csv | The training dataset | + +#### Output + +| Name | File Type | Description | +|------------|-----------|-------------------| +| model_path | pkl | The trained model | + +#### Examples +Refer to the examples in classifier training config + +### Inference +#### Module Name +``` +petml.operators.boosting.XGBoostRegressorPredict +``` + +#### Input +| Name | File Type | Description | +|--------------|-----------|-----------------------| +| predict_data | csv | The inference dataset | +| model_path | pkl | The trained model | + + +#### Output + +| Name | File Type | Description | +|--------------------|-----------|-------------------------| +| inference_res_path | csv | The result of inference | + + +#### Examples +Refer to the examples in classifier inference config + +## References +[1] [XGBoost: A Scalable Tree Boosting System](https://arxiv.org/abs/1603.02754) diff --git a/docs/user_guide/PSI.md b/docs/user_guide/PSI.md new file mode 100644 index 0000000..db3b275 --- /dev/null +++ b/docs/user_guide/PSI.md @@ -0,0 +1,76 @@ +# PSI + +## Introduction +In federated learning, a secure and dependable method is required to find any intersection in features or IDs between +datasets from two parties. The intersection data will then be used to update the model. We focus on the setting where +two parties (e.g., A & B) hold vertically partitioned data. They need to align their data before training the model. + +Private Set Intersection (PSI) protocol is well-suited to solve this problem. Our implementation relies on the elliptic +curve Diffie-Hellman PSI (ECDH-PSI) provided by [PETAce](https://github.com/tiktok-privacy-innovation/PETAce). +(More detailed information is available in the PETAce documentation.) +To improve user experience, we encapsulated the PSI function in PETAce, presenting it as an intersection module. +Users can simply upload a CSV file along with the names of the columns to be aligned, enabling them to obtain their +aligned data effortlessly. + +## Config + +### Module Name +``` +petml.operators.preprocessing.PSITransform +``` + +### Model Parameters +| Name | Type | Description | Default | +|-------------------|------|----------------------------------|---------| +| ```column_name``` | str | The column used to implement PSI | | + +### Input +| Name | File Type | Description | +|------|-----------|------------------| +| data | csv | The data for PSI | + +### Output + +| Name | File Type | Description | +|--------------|-----------|--------------------------| +| intersection | csv | The intersection of data | + +### Examples +``` +config = { + "common": { + "network_mode": "petnet", + "network_scheme": "socket", + "parties": { + "party_a": { + "address": ["127.0.0.1:50011"] + }, + "party_b": { + "address": ["127.0.0.1:50012"] + } + } + }, + "party_a": { + "column_name": "id", + "inputs": { + "data": "examples/data/breast_hetero_mini_server.csv", + }, + "outputs": { + "data": "tmp/server/data.csv" + } + }, + "party_b": { + "column_name": "id", + "inputs": { + "data": "examples/data/breast_hetero_mini_client.csv", + }, + "outputs": { + "data": "tmp/client/data.csv" + } + } +} + +#if run the code in party a, the party should be 'party_a' and vice versa +operator = petml.operators.preprocessing.PSITransform(party) +operator.run(config_map) +``` diff --git a/examples/data/breast_hetero_mini_client.csv b/examples/data/breast_hetero_mini_client.csv new file mode 100644 index 0000000..51eda0b --- /dev/null +++ b/examples/data/breast_hetero_mini_client.csv @@ -0,0 +1,570 @@ +id,y,x0,x1,x2 +133,1,0.254879,-1.046633,0.209656 +273,1,-1.142928,-0.781198,-1.166747 +175,1,-1.451067,-1.406518,-1.456564 +551,1,-0.879933,0.420589,-0.877527 +199,0,0.426758,0.723479,0.316885 +274,0,0.963102,1.467675,0.829202 +420,1,-0.662496,0.212149,-0.620475 +76,1,-0.453343,-2.147457,-0.473631 +315,1,-0.606584,-0.971725,-0.678558 +399,1,-0.583805,-0.193332,-0.633283 +238,1,-0.107515,2.420311,-0.141817 +246,1,-0.482335,0.348938,-0.565371 +253,0,0.741523,-0.095626,0.704101 +550,1,-0.954483,-0.147736,-0.98833 +208,1,-0.356014,0.567149,-0.23177 +185,1,-0.910995,-0.732345,-0.949311 +156,0,0.869914,-0.092369,0.763673 +0,0,1.88669,-1.359293,2.303601 +70,0,1.779007,0.147012,1.746605 +293,1,-0.664567,0.011851,-0.68243 +287,1,-0.548601,-1.650784,-0.591583 +222,1,-1.055953,-0.462024,-1.052072 +262,0,0.853348,0.254488,0.912602 +309,1,-0.318739,-1.347894,-0.396188 +534,1,-0.962766,0.135613,-0.918334 +54,0,0.379129,0.979143,0.310928 +172,0,0.522016,-1.406518,0.528365 +484,1,0.153409,-1.868995,0.156042 +102,1,-0.606584,1.166414,-0.675579 +458,1,-0.399502,1.010084,-0.482567 +495,1,-0.053674,0.456415,-0.100117 +158,1,-0.648001,-1.183422,-0.690472 +160,1,-0.610726,0.086759,-0.546606 +292,1,-0.523751,-0.9359,-0.549585 +493,1,-0.637646,-1.517252,-0.715492 +394,1,-0.561026,0.019993,-0.563882 +528,1,-0.341518,-1.676839,-0.379508 +335,0,0.977597,1.216895,1.070467 +311,1,0.039513,-0.639524,-0.106074 +25,0,1.238521,-0.696519,1.344497 +522,1,-0.89857,0.122585,-0.919823 +512,0,0.029159,0.64857,0.17987 +230,0,0.687682,-0.128194,0.781544 +548,1,-1.105653,-0.014204,-1.136664 +213,0,0.372916,0.389649,0.39135 +390,1,-1.012466,-1.632871,-1.013648 +275,1,-0.801242,-1.088973,-0.828082 +219,0,2.408538,3.21336,2.172543 +236,0,3.052564,1.438363,2.941018 +336,1,-0.527893,-1.427688,-0.592179 +20,1,-0.366368,-0.844707,-0.332744 +193,0,-0.128223,2.224898,-0.165645 +200,1,-0.378793,0.436874,-0.4501 +83,0,0.840923,1.146872,1.013874 +376,1,-1.12222,-0.465281,-0.915951 +295,1,-0.331164,-1.424431,-0.389933 +77,0,1.267513,-1.102,1.275989 +441,0,0.851277,1.593064,0.760694 +109,1,-0.674921,0.56552,-0.693153 +319,1,-0.6977,-0.890303,-0.759575 +349,1,-0.716338,-1.295784,-0.71996 +89,1,0.014664,-1.211106,0.063706 +419,1,-0.809525,0.528066,-0.83404 +562,0,0.259021,2.786709,0.638572 +370,0,0.644194,0.871666,0.656444 +546,1,-1.039387,-0.636267,-1.076496 +49,1,-0.231765,1.000313,-0.246067 +412,1,-1.305488,0.376621,-1.21083 +417,0,1.429037,0.321254,1.48449 +440,1,-0.809525,0.194236,-0.50997 +482,1,-0.298031,-1.198078,-0.366998 +291,1,-0.003974,0.083503,0.05477 +198,0,1.468383,1.039396,1.761498 +542,1,0.049868,1.07685,0.004134 +320,1,-1.033174,-0.825166,-1.064284 +94,0,0.40605,-0.235671,0.483686 +116,1,-1.41959,-1.401633,-1.30823 +429,1,-0.507184,-0.768171,-0.547798 +518,1,-0.252473,-0.212873,-0.236834 +323,0,1.870123,1.006827,1.901492 +248,1,-0.832304,1.549097,-0.872165 +280,0,1.542933,1.664716,1.564912 +391,1,-1.263036,-0.468538,-1.288274 +462,1,-0.179994,1.026368,-0.204367 +406,1,0.298367,-0.992895,0.257314 +416,1,-1.12222,0.905864,-1.147684 +411,1,-0.799171,0.124213,-0.814083 +474,1,-0.8965,-1.030349,-0.788765 +96,1,-0.712196,-0.774684,-0.748256 +232,1,-0.809525,2.622237,-0.858464 +354,1,-0.859225,-1.605188,-0.823317 +240,1,-0.293889,-1.079202,-0.39172 +378,1,-0.358085,-0.983124,-0.277044 +177,0,0.314933,0.451529,0.483686 +33,0,1.631978,0.850497,1.612569 +251,1,-0.683205,-0.523905,-0.719066 +224,1,-0.233835,-0.338263,-0.250833 +186,0,1.043864,0.111186,0.951324 +85,0,1.379337,0.32614,1.338539 +422,1,-0.751542,-0.978239,-0.754511 +104,1,-0.979333,-0.385488,-0.98416 +461,0,4.094189,0.927033,4.287337 +142,1,-0.72255,0.176323,-0.732768 +380,1,-0.710125,-0.838193,-0.665154 +123,1,-0.117869,-1.579133,-0.132881 +428,1,-0.950341,-0.877276,-0.980288 +396,1,-0.304244,0.247975,-0.295809 +66,1,-1.213336,0.957974,-1.19832 +170,1,-0.573451,-1.634499,-0.604391 +481,1,0.029159,0.120957,-0.085224 +313,1,-0.813667,-2.085577,-0.775361 +53,0,0.896835,-0.251956,0.829202 +296,1,-1.014537,-1.768031,-1.037775 +114,1,-1.375274,-0.986381,-1.274274 +243,1,-0.260756,0.107929,-0.275853 +140,1,-1.169849,-1.885279,-1.213213 +152,1,-1.087016,-1.007551,-1.078879 +268,1,-0.490618,-0.331749,-0.535883 +91,0,0.033301,0.026507,0.007112 +255,0,0.025018,-0.587414,0.024984 +559,1,-0.784675,1.869899,-0.744086 +14,0,-0.256615,1.031253,0.045834 +100,0,0.149267,1.562124,0.039877 +353,0,0.464033,1.228294,0.415178 +543,1,-0.393289,1.871527,-0.440271 +165,1,-0.059886,0.02325,-0.147774 +43,0,0.230029,0.37825,0.173913 +201,0,0.85956,0.026507,0.960259 +108,0,2.512079,0.379878,2.964846 +328,0,0.623486,0.765818,0.671337 +439,1,-0.281464,-1.036863,-0.319638 +162,0,2.166251,0.116071,2.014678 +325,1,-0.529963,-0.745372,-0.552861 +183,1,-0.807454,-1.299041,-0.83821 +197,0,0.722886,-0.159135,0.650487 +498,0,1.342063,-0.45551,1.165782 +477,1,-0.233835,-0.631382,-0.180538 +421,1,0.039513,-1.194821,0.203699 +374,1,-0.29596,-0.890303,-0.241301 +470,1,-1.062166,-0.009318,-1.083645 +382,1,-0.766038,0.493869,-0.592774 +510,1,-0.790887,-1.315326,-0.774766 +132,0,0.662832,0.977515,0.668358 +545,1,-0.190348,0.55575,-0.288363 +121,0,1.238521,-0.126566,1.135996 +45,0,1.356558,-0.709547,1.290882 +333,1,-0.726692,-0.589042,-0.750044 +134,0,1.294434,0.93029,1.141953 +182,0,0.795365,1.163157,0.656444 +519,1,-0.376722,-0.641152,-0.406017 +207,0,0.731169,-0.102139,0.677294 +99,0,0.012593,0.843983,0.066684 +366,0,1.640261,1.324372,1.570869 +180,0,3.489508,1.168042,3.381848 +318,1,-1.285815,-0.370832,-1.150961 +501,0,-0.053674,1.182698,-0.037566 +229,0,-0.221411,0.728364,-0.058416 +7,0,0.163763,0.401048,0.099449 +247,1,-0.389147,-1.299041,-0.067352 +494,1,-0.366368,0.453158,-0.356573 +174,1,-0.979333,-1.054776,-1.014542 +340,1,0.083001,-0.678606,0.123277 +125,1,-0.161357,-0.34152,-0.207346 +209,1,0.230029,-1.588903,0.191785 +347,1,0.20725,-1.261587,0.206678 +332,1,-0.888216,0.016737,-0.904036 +361,1,-0.428493,0.573662,-0.426569 +500,1,0.101638,-0.854478,0.072641 +300,0,2.000585,0.091645,1.901492 +283,0,0.472316,-0.095626,0.584958 +107,1,-0.616938,0.295199,-0.646389 +131,0,0.619345,0.052562,0.525386 +303,1,-1.078732,-0.18519,-1.087219 +567,0,1.961239,2.237926,2.303601 +507,1,-0.94827,-0.803996,-0.928759 +3,0,-0.281464,0.133984,-0.249939 +516,0,1.157759,0.085131,1.040681 +143,1,-0.37051,-0.628125,-0.300575 +398,1,-0.743259,-0.867505,-0.788467 +530,1,-0.573451,0.374993,-0.558223 +119,0,0.892693,0.350566,0.653465 +259,0,0.459891,3.885905,0.567086 +450,1,-0.720479,0.407562,-0.70745 +192,1,-1.304866,-0.78934,-1.340697 +514,0,0.271446,0.38802,0.194763 +241,1,-0.635576,-0.864248,-0.697323 +402,1,-0.442989,-0.173791,-0.326191 +148,1,-0.086807,-0.948927,0.039877 +395,1,-0.279394,-0.054915,-0.322915 +299,1,-1.105653,-0.2373,-1.106878 +149,1,-0.192419,-0.523905,-0.29998 +60,1,-1.087016,-1.339752,-1.114026 +61,1,-1.388321,0.22192,-1.346356 +126,0,0.128559,1.622376,0.176892 +372,0,1.329638,-0.624868,1.335561 +82,0,2.843411,1.293432,3.110797 +425,1,-1.068378,0.531323,-1.112239 +337,0,1.71274,1.415565,1.603633 +386,1,-0.650071,-1.04012,-0.584136 +184,0,0.317004,0.383135,0.194763 +58,1,-0.422281,-0.558102,-0.506991 +88,1,-0.505114,0.785359,-0.470652 +117,0,0.526157,0.275658,0.590915 +459,1,-1.159494,1.830816,-1.168535 +221,1,-0.266969,-1.391862,-0.183517 +223,0,0.681469,0.751162,0.555172 +128,1,-0.032965,-1.19645,-0.040545 +294,1,-0.573451,-1.334867,-0.557627 +541,1,-0.010186,0.985657,0.185828 +111,1,-0.608655,-0.033745,-0.543926 +266,1,-0.908925,-0.44574,-0.86323 +568,1,-1.410893,0.76419,-1.432735 +362,1,-0.52168,0.050934,-0.579073 +271,1,-0.817808,-1.546564,-0.863528 +187,1,-0.674921,-0.698148,-0.680345 +155,1,-0.554813,-0.074456,-0.615412 +5,0,-0.165498,-0.313836,-0.115009 +511,1,-0.136507,-1.318583,-0.165645 +252,0,1.865981,-0.014204,1.564912 +480,1,-0.606584,0.35708,-0.548989 +513,1,0.101638,-1.373949,0.036898 +86,0,-0.012257,0.581805,0.03392 +105,0,0.008451,-0.533675,-0.025652 +242,1,-0.763967,0.371736,-0.598731 +312,1,-0.430564,-1.510738,-0.453377 +473,1,-0.583805,2.01483,-0.660686 +364,1,-0.318739,-0.647666,-0.402145 +565,0,1.53672,2.047399,1.42194 +447,1,0.033301,-0.478309,-0.040545 +488,1,-0.610726,-0.665579,-0.616305 +151,1,-1.486271,0.658341,-1.464904 +263,0,0.339783,0.975886,0.257314 +563,0,1.66097,0.60786,2.139779 +194,0,-0.039178,0.342424,0.337735 +433,0,1.323425,0.855382,1.133017 +244,0,1.114272,0.790245,1.121103 +489,0,0.602778,0.143755,0.596872 +465,1,-0.171711,-0.02886,0.230506 +483,1,-0.27111,-0.349662,-0.341978 +397,1,-0.523751,-0.751886,-0.492694 +52,1,-0.656284,-0.707918,-0.702684 +147,1,-0.003974,-0.033745,-0.004802 +27,0,1.043864,0.257745,0.972174 +436,1,-0.376722,-0.211245,-0.36104 +434,1,0.008451,-0.836565,-0.147774 +190,0,-0.109586,1.873156,-0.025652 +407,1,-0.387077,0.217034,-0.465589 +202,0,1.832848,1.140359,2.077228 +455,1,-0.252473,2.594554,-0.314872 +452,1,-0.658355,1.987146,-0.660984 +385,0,-0.099232,0.9824,-0.150752 +188,1,-0.766038,0.130727,-0.824806 +159,1,-0.809525,-1.217619,-0.869485 +32,0,0.954818,1.044281,0.858987 +264,0,1.099776,0.594832,0.990045 +245,1,-0.991758,0.616002,-1.000245 +535,0,1.66304,-0.032117,1.576826 +430,0,0.016734,0.308227,0.540279 +443,1,-1.103582,-0.385488,-1.129217 +154,1,-0.310456,-0.843079,-0.285682 +9,0,-0.24419,2.443109,-0.286278 +63,1,-1.296169,-1.04989,-1.241212 +329,0,0.302508,-0.076084,0.191785 +536,0,-0.202773,1.39928,-0.088202 +65,0,0.215534,1.255978,0.218592 +438,1,-0.132365,0.379878,-0.189474 +269,1,-0.94827,-0.076084,-0.915951 +36,0,-0.078524,0.762561,0.266249 +98,1,-0.664567,-1.386977,-0.723832 +392,0,1.021085,0.60786,1.037702 +69,1,-0.581734,-0.963583,-0.643112 +517,0,1.545003,-0.072828,1.585762 +92,1,0.018805,-0.541818,-0.082245 +552,1,-0.49683,1.681,-0.570733 +505,1,-1.17399,-1.243674,-1.125643 +566,0,0.561361,1.374854,0.579001 +163,1,-0.556884,0.488984,-0.592774 +358,1,-1.302174,-1.299041,-1.250743 +442,1,-0.206915,-1.33161,-0.278832 +15,0,0.246596,1.865014,0.501557 +503,0,3.007006,-0.294295,3.10484 +176,1,-1.037316,-0.209616,-1.018414 +327,1,-0.662496,-0.558102,-0.730385 +1,0,1.805927,-0.369203,1.535126 +404,1,-0.639717,-1.437458,-0.689578 +24,0,2.110339,0.957974,2.077228 +139,1,-0.900641,-1.61333,-0.915355 +101,1,-1.726901,-0.999409,-1.693361 +350,1,-0.619009,-0.96684,-0.704471 +90,1,-0.032965,0.559006,-0.129902 +191,1,-0.52168,-0.354547,-0.542734 +383,1,-0.432635,-0.414799,-0.35836 +11,0,0.85956,0.261002,0.870902 +314,1,-1.515262,-0.527162,-1.507497 +352,0,3.491579,-0.34152,3.635028 +235,1,-0.19449,0.749534,-0.267811 +305,1,-0.792958,0.967744,-0.770596 +375,1,0.145126,-1.064546,0.173913 +215,0,-0.107515,0.204007,-0.085224 +466,1,-0.304244,-0.035373,-0.189474 +486,1,0.039513,-0.03863,-0.037566 +250,0,1.928106,0.215406,1.728734 +339,0,2.982156,0.822813,2.833789 +103,1,-1.140857,0.187723,-1.043732 +206,1,-1.211265,-0.400144,-1.196831 +345,1,-1.116007,-1.009179,-1.083347 +346,1,-0.544459,0.225177,-0.617199 +317,0,1.153617,-0.110282,1.001959 +499,0,1.571924,0.827699,1.666184 +478,1,-0.801242,-0.615097,-0.751235 +47,0,-0.124082,0.370108,-0.132881 +456,1,-0.652142,2.138591,-0.632092 +211,1,-0.614867,-0.11191,-0.656516 +490,1,-0.434706,1.027997,-0.432526 +113,1,-1.058024,-0.47668,-1.031818 +359,1,-0.879933,-0.107025,-0.937396 +344,1,-0.664567,-1.224133,-0.688089 +532,1,-0.086807,-0.891932,-0.168624 +261,0,0.741523,0.943318,0.623679 +4,0,1.298575,-1.46677,1.338539 +226,1,-0.983474,-0.957069,-1.0065 +464,1,-0.283535,-0.291038,-0.362232 +277,0,0.764302,-0.224272,0.647508 +257,0,0.302508,-0.491336,0.373478 +260,0,1.669253,2.195586,1.639376 +301,1,-0.581734,-0.42457,-0.569839 +537,1,-0.681134,1.060565,-0.629709 +403,1,-0.498901,-0.432712,-0.523373 +233,0,1.698245,1.905725,1.651291 +487,0,1.592632,0.767446,1.389175 +141,0,0.756019,-0.066314,0.647508 +68,1,-1.234044,-0.492965,-1.243893 +363,1,0.385341,-0.037002,0.296035 +379,0,-0.627292,1.163157,-0.461717 +51,1,-0.331164,-0.405029,-0.333042 +195,1,-0.494759,-0.598813,-0.490013 +239,0,1.292363,3.125425,1.010895 +161,0,1.192963,-1.281128,1.171739 +169,1,-0.032965,-0.435969,-0.079266 +212,0,2.452025,-1.173652,2.419765 +120,1,-0.714267,-1.580761,-0.700599 +367,1,-0.409856,-0.266612,-0.399464 +171,0,0.354279,0.682768,0.278164 +348,1,-0.778463,-0.795854,-0.821827 +409,1,-0.449201,0.521552,-0.543926 +112,1,-0.200702,-0.317093,-0.00778 +524,1,-1.041457,-0.437598,-0.981182 +62,0,0.290083,0.624144,0.352628 +393,0,2.06271,0.498754,1.928299 +75,0,0.724957,-0.181933,0.641551 +124,1,-0.416068,-0.47668,-0.454866 +231,1,-0.867508,1.314602,-0.81736 +285,1,-0.573451,-0.422942,-0.646389 +448,1,0.00638,0.441759,0.024984 +460,0,1.38555,1.435106,1.335561 +218,0,1.959169,0.48247,1.877663 +467,1,-1.060095,-0.172162,-1.076794 +525,1,-1.407372,-1.176908,-1.309422 +80,1,-0.654213,1.05568,-0.677068 +472,1,0.188613,-1.214362,0.141149 +549,1,-0.67078,0.940061,-0.695833 +527,1,-0.550672,-1.043377,-0.596944 +168,0,1.422825,1.083363,1.430876 +351,0,0.225888,-0.245442,0.361564 +57,0,0.3315,0.817928,0.251356 +560,1,-0.200702,1.220152,-0.210324 +424,1,-1.04767,-0.408286,-1.05654 +355,1,-0.600372,-0.52879,-0.54333 +42,0,1.619553,1.220152,2.089143 +217,1,-0.991758,-0.196589,-0.949013 +286,1,-0.627292,0.262631,-0.448611 +135,0,-0.368439,1.252721,-0.453377 +521,0,2.826844,0.204007,2.932082 +278,1,-0.159286,0.068847,-0.248748 +504,1,-1.240257,-1.513995,-1.138153 +97,1,-1.107724,0.099787,-1.145302 +84,1,-0.538247,-0.126566,-0.580264 +553,1,-1.330337,-0.102139,-1.322527 +469,1,-0.602442,-0.045144,-0.569541 +87,0,1.716882,0.770703,1.35939 +22,0,0.372916,-1.074317,0.531343 +19,1,-0.240048,-1.045005,-0.225217 +265,0,3.359046,3.498337,3.179304 +38,0,-0.264898,-0.077713,-0.349126 +479,0,0.2321,-0.427827,0.441986 +17,0,0.971385,0.944946,0.879838 +189,1,-0.604513,-0.991267,-0.613922 +178,1,-0.46991,0.54435,-0.56835 +164,0,2.431317,0.414075,2.291686 +451,0,1.070784,0.860267,0.969195 +290,1,-0.103373,-0.577643,-0.165645 +67,1,-0.815737,-0.29918,-0.87157 +110,1,-1.080803,-0.68512,-1.059816 +556,1,-1.163636,-0.45551,-1.173002 +150,1,-0.436776,-0.255213,-0.489715 +321,0,1.406258,-0.431084,1.278968 +526,1,-0.190348,-0.084227,-0.159688 +18,0,2.28843,0.84724,2.369129 +446,0,1.089422,2.094623,1.135996 +432,0,1.192963,-0.098883,1.153867 +523,1,-0.240048,-0.00769,-0.233259 +418,1,-0.542388,-1.426059,-0.570137 +272,0,2.468592,0.407562,2.640181 +153,1,-0.886145,-1.527023,-0.923695 +509,0,0.174117,1.734739,0.310928 +288,1,-0.913066,-0.545075,-0.863528 +258,0,0.741523,0.971001,1.08536 +228,1,-0.428493,0.917263,-0.494183 +476,1,0.037443,0.257745,0.144127 +423,1,-0.233835,-0.02886,-0.174581 +64,0,0.169975,1.269005,0.135192 +137,1,-0.817808,-0.595556,-0.814083 +343,0,1.342063,1.462789,1.499383 +302,0,1.534649,0.611116,1.535126 +561,1,-0.900641,2.055541,-0.955268 +8,0,-0.161357,0.822813,-0.031609 +502,1,-0.558955,-0.696519,-0.613327 +28,0,0.828498,1.796619,1.252161 +457,1,-0.397431,1.392767,-0.475716 +136,1,-0.608655,-0.032117,-0.628517 +365,0,1.665111,0.112814,1.606612 +384,1,-0.42021,-1.35278,-0.317851 +81,1,-0.153073,-0.405029,-0.315766 +306,1,-0.385006,-0.851221,-0.454568 +282,0,1.557428,0.484098,1.344497 +475,1,-0.451272,-1.030349,-0.418229 +118,0,0.811931,0.785359,0.68623 +55,1,-0.710125,-0.522276,-0.758086 +557,1,-1.196769,1.394395,-1.214107 +284,1,-0.490618,-0.974982,-0.450994 +256,0,1.818352,1.724968,2.124886 +426,1,-0.857154,-0.668836,-0.77 +387,1,-0.157215,-0.929386,-0.226408 +497,1,-0.457485,-0.217758,-0.430144 +298,1,-0.010186,-0.067942,-0.043523 +157,1,0.403979,0.389649,0.388371 +249,1,-0.749471,-0.730716,-0.785787 +342,1,-0.900641,-0.940785,-0.819147 +508,1,0.217604,-1.289271,0.07562 +267,1,-0.304244,0.710451,-0.28598 +338,1,-1.058024,0.189351,-1.05088 +360,1,-0.527893,-0.764914,-0.608859 +12,0,0.971385,0.694167,1.323647 +210,0,1.443533,0.352195,1.520233 +533,0,1.441462,0.239833,1.332582 +167,0,0.78294,0.101415,0.698144 +79,1,-0.42021,-0.139593,-0.458142 +369,0,2.358838,0.019993,2.613373 +437,1,-0.126153,-0.667207,-0.180538 +485,1,-0.515468,-0.756771,-0.281214 +408,0,0.996235,-0.043516,0.918559 +308,1,-0.26904,-1.422803,-0.350913 +414,0,0.205179,1.829188,0.084556 +41,0,-0.710125,1.573523,-0.596944 +276,1,-0.842658,-1.088973,-0.890335 +146,0,-0.523751,0.114443,-0.456653 +405,1,-0.801242,-0.015832,-0.729789 +35,0,0.774656,0.54435,0.781544 +13,0,0.118205,0.322883,0.141149 +166,1,-0.966908,-2.223994,-1.00084 +173,1,-1.018678,-1.442344,-1.049987 +531,1,-0.604513,0.510153,-0.603497 +388,1,-0.875791,-1.098743,-0.82004 +205,0,0.310792,-0.885418,0.310928 +6,0,1.368983,0.322883,1.368325 +144,1,-0.894429,-0.807253,-0.877825 +540,1,-0.830233,-0.976611,-0.848337 +30,0,1.424896,1.356941,1.585762 +435,0,0.159621,0.834212,0.197742 +31,0,0.114063,0.397791,0.361564 +203,0,0.60692,2.633636,0.632615 +59,1,-1.400331,-1.673582,-1.410693 +95,0,1.646474,0.962859,1.454704 +37,1,-0.614867,-0.466909,-0.679153 +331,1,-0.382935,-0.606955,-0.239812 +237,0,1.646474,0.080246,1.621505 +10,0,0.604849,1.335771,0.492622 +389,0,0.942393,0.775589,1.034724 +324,1,-0.52168,-0.699776,-0.481077 +179,1,-0.54653,-1.551449,-0.612433 +220,1,-0.192419,-1.51888,-0.224919 +50,1,-0.681134,0.006966,-0.723236 +401,1,-0.511326,-0.901702,-0.584434 +368,0,2.998723,0.124213,2.74741 +29,0,0.774656,-1.002666,0.823244 +78,0,1.470454,0.984029,1.877663 +204,1,-0.26904,-0.168905,-0.333935 +214,0,0.122346,1.49373,0.230506 +127,0,1.253017,0.008594,1.219396 +46,1,-1.512777,-0.605327,-1.489328 +554,1,-0.492689,1.638661,-0.548691 +449,0,1.948814,1.041024,1.815113 +564,0,1.901185,0.1177,1.752563 +297,0,-0.602442,-0.37246,-0.66009 +491,1,0.735311,-1.181794,0.590915 +492,0,1.089422,0.062333,1.076424 +181,0,2.155897,1.270634,2.062335 +427,1,-0.726692,1.036139,-0.702088 +471,1,-0.552743,1.246207,-0.596349 +515,1,-0.786746,-0.431084,-0.837316 +445,1,-0.681134,0.762561,-0.678558 +506,1,-0.643859,-0.245442,-0.659197 +93,1,-0.242119,0.042792,-0.288065 +453,1,-0.097161,-1.424431,-0.123945 +357,1,-0.240048,-0.015832,-0.313383 +544,1,-0.252473,-0.150993,-0.241004 +26,0,0.279729,1.226666,0.450921 +356,1,-0.430564,-0.134708,-0.388443 +227,1,0.029159,-1.036863,0.206678 +377,1,-0.327023,1.620748,-0.302362 +234,1,-1.192628,-1.061289,-1.236744 +341,1,-1.142928,-0.42457,-1.072624 +129,0,1.317213,1.286918,1.234289 +289,1,-0.809525,0.07536,-0.833146 +539,1,-1.572003,1.011712,-1.571835 +145,1,-0.64593,-1.492825,-0.625539 +138,0,0.472316,-0.691634,0.421136 +410,1,-0.666638,1.73311,-0.660984 +56,0,2.044072,0.401048,1.871706 +216,1,-0.625221,0.23169,-0.627326 +281,1,-0.612797,-1.207849,-0.672005 +16,0,0.579999,0.84724,0.480707 +444,0,0.851277,-0.595556,0.775587 +547,1,-1.126361,-0.592299,-1.077688 +270,1,-0.281464,-0.818652,-0.381891 +44,0,-0.008116,0.686025,-0.052459 +34,0,0.816073,0.257745,0.757716 +371,1,-0.014328,-1.619844,-0.082245 +310,1,-0.757754,0.142126,-0.784595 +538,1,-1.489377,0.853754,-1.492009 +72,0,1.4601,1.326001,1.320668 +73,0,0.062293,-0.784455,0.090513 +279,1,-0.266969,-0.641152,-0.264832 +196,0,0.025018,1.356941,0.129234 +74,1,-0.44713,-0.401772,-0.522778 +23,0,2.671532,1.614234,2.404872 +334,1,-0.604513,0.453158,-0.677068 +400,0,0.938252,0.342424,1.261096 +431,1,-0.701842,-0.450625,-0.525756 +254,0,1.952956,-0.180304,1.663205 +48,1,-0.519609,-0.81051,-0.517714 +130,1,-0.606584,-1.281128,-0.473035 +555,1,-1.12429,1.5035,-1.122664 +520,1,-1.180203,-1.276243,-1.174194 +122,0,2.019222,-0.274754,2.193393 +496,1,-0.391218,-0.574386,-0.356573 +225,1,0.103709,-1.429316,0.093491 +326,1,-0.153073,-1.250188,-0.263939 +106,1,-0.648001,0.583433,-0.647878 +381,1,-0.865437,-0.78934,-0.82004 +415,1,-0.666638,0.249603,-0.660388 +71,1,-1.353531,-1.629614,-1.331463 +558,1,-0.163427,0.259374,-0.040545 +21,1,-1.250611,-1.631243,-1.254913 +316,1,-0.708054,-1.499339,-0.764341 +463,1,-0.724621,-0.269869,-0.732172 +454,1,-0.399502,-0.574386,-0.465887 +413,1,0.101638,0.956345,0.087534 +330,0,0.515803,-0.60207,0.507515 +468,0,1.097705,0.519924,1.082381 +322,1,-0.461626,-0.748629,-0.430739 +307,1,-1.360572,-0.913101,-1.380908 +373,0,1.884619,-0.408286,1.773413 +304,1,-0.743259,-0.662322,-0.731874 +529,1,-0.583805,-1.61333,-0.60588 +40,0,-0.07024,0.744648,-0.141817 +115,1,-0.538247,0.076989,-0.587413 +2,0,1.51187,-0.023974,1.347475 +39,0,-0.153073,0.055819,0.001155 diff --git a/examples/data/breast_hetero_mini_client.parquet b/examples/data/breast_hetero_mini_client.parquet new file mode 100644 index 0000000000000000000000000000000000000000..d41f494ab129f9701bc0c417a93b33ecbc9c926a GIT binary patch literal 21062 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 DH(D&r literal 0 HcmV?d00001 diff --git a/examples/data/breast_hetero_mini_server.csv b/examples/data/breast_hetero_mini_server.csv new file mode 100644 index 0000000..66f5a31 --- /dev/null +++ b/examples/data/breast_hetero_mini_server.csv @@ -0,0 +1,570 @@ +id,x3,x4,x5 +133,0.074214,-0.441366,-0.377645 +273,-0.923578,0.62823,-1.021418 +175,-1.092337,-0.708765,-1.168557 +551,-0.780484,-1.037534,-0.48388 +199,0.287273,1.000835,0.962702 +274,0.772457,-0.038076,-0.468613 +420,-0.632995,-0.327392,-0.385278 +76,-0.483572,0.558093,-0.740244 +315,-0.591332,-0.963013,-1.302401 +399,-0.560041,-0.34931,-0.519504 +238,-0.204943,-1.063835,-0.074206 +246,-0.489725,-0.976164,-0.658182 +253,0.600181,0.404667,-0.087565 +550,-0.823201,-1.414523,-1.150045 +208,-0.424155,0.110966,1.182806 +185,-0.77978,0.864944,-0.969255 +156,0.740814,0.413434,0.607736 +0,2.001237,1.307686,2.616665 +70,1.732277,-0.572873,-0.131459 +293,-0.637741,0.198638,-0.499147 +287,-0.533673,-1.587236,-0.887829 +222,-0.887716,0.360831,-0.70144 +262,0.728509,-0.831505,0.206332 +309,-0.365968,-1.348769,-1.24553 +534,-0.831639,0.45727,-0.02077 +54,0.262662,0.28631,-0.308942 +172,0.389232,0.90878,0.661808 +484,-0.046203,0.952616,0.277579 +102,-0.585004,-0.879725,-1.053734 +458,-0.44314,-0.463284,-0.92218 +495,-0.170488,-0.472051,-0.734519 +158,-0.611372,-0.213419,-0.833757 +160,-0.59186,0.150419,-0.413905 +292,-0.518906,0.698367,-0.301944 +493,-0.609263,-1.664826,-1.205453 +394,-0.564436,0.474804,-0.489605 +528,-0.399544,0.308228,-0.749786 +335,0.846289,0.549325,-0.311486 +311,-0.069935,-1.370687,-1.166649 +25,1.020322,0.97015,0.894635 +522,-0.781714,-0.945479,-1.12619 +512,-0.063607,1.097274,0.835474 +230,0.54217,1.662757,0.885093 +548,-0.907756,-0.546572,-1.010222 +213,0.246841,-0.353694,-0.476882 +390,-0.854492,0.084665,-0.56785 +275,-0.71755,0.154802,-1.085159 +219,2.806362,0.369598,0.988783 +236,3.627307,0.6896,1.007232 +336,-0.535431,-0.796437,-0.361105 +20,-0.439624,-0.051226,0.148443 +193,-0.196329,2.022211,1.376193 +200,-0.425737,0.461654,-0.318484 +83,0.733782,0.299461,0.174525 +376,-0.929379,-0.792053,0.684709 +295,-0.385832,-0.673696,-0.935539 +77,1.282251,0.676449,1.96653 +441,0.709172,0.492339,1.004687 +109,-0.637214,1.645223,-0.220518 +319,-0.641081,-2.116335,-1.317732 +349,-0.675712,-0.134515,-0.418358 +89,-0.13533,-0.204652,0.347555 +419,-0.742864,-0.182734,-0.912638 +562,0.060502,0.40905,3.418837 +370,0.49998,0.400283,1.350111 +546,-0.871368,-0.169583,-1.055006 +49,-0.319559,-0.708765,-0.529046 +412,-1.018857,-1.041918,-0.417085 +417,1.524843,0.847409,0.92835 +440,-0.710519,0.295077,0.979241 +482,-0.387414,0.303844,-0.027768 +291,-0.124431,-0.046843,0.310022 +198,1.419368,-0.00739,1.945538 +542,-0.095249,-1.155891,-0.742153 +320,-0.861699,0.343297,-0.116191 +94,0.253872,0.996451,1.056214 +116,-1.073352,-0.634244,-0.422174 +429,-0.516445,-1.120822,-1.006469 +518,-0.361925,0.58001,0.266129 +323,1.858847,1.176179,1.240059 +248,-0.746907,0.768505,-0.728158 +280,1.482653,2.009061,0.825932 +391,-0.99073,0.597545,-0.784138 +462,-0.256626,-1.344385,-0.688717 +406,0.118337,-0.515887,-0.522048 +416,-0.916194,0.886862,-0.858566 +411,-0.719308,0.198638,-0.674722 +474,-0.786636,0.036445,0.862192 +96,-0.67747,-0.805204,-1.022181 +232,-0.720187,-1.421536,-1.179499 +354,-0.750775,-1.916882,-0.818489 +240,-0.346631,-0.200268,-0.796225 +378,-0.39304,-0.213419,0.357097 +177,0.176876,0.400283,1.351383 +33,1.639107,0.812341,2.57468 +251,-0.653386,-0.616709,-0.95017 +224,-0.30198,-0.209036,-0.783502 +186,0.930669,-0.393146,-0.062119 +85,1.269946,0.325762,-0.288585 +422,-0.711749,0.400283,-0.237058 +104,-0.839901,-0.4589,-0.672177 +461,5.930172,0.146035,1.08993 +142,-0.663758,0.391516,-0.477519 +380,-0.711046,1.255083,-0.072298 +123,-0.237464,-0.046843,-0.480063 +428,-0.807731,-1.287398,-1.221866 +396,-0.361046,0.45727,0.017398 +66,-0.966647,0.983301,-0.558944 +170,-0.582718,0.268776,-0.812128 +481,-0.088042,-1.138356,-0.717343 +313,-0.725637,-1.015616,-0.583118 +53,0.774214,-0.191501,-0.156268 +296,-0.858535,-1.720497,-1.139994 +114,-1.048038,1.754812,-0.113647 +243,-0.306902,-1.695949,-0.700167 +140,-0.9452,-0.393146,-1.159206 +152,-0.879102,-0.138898,0.145898 +268,-0.497635,-0.296707,-0.46734 +91,-0.087339,-0.292324,-0.34711 +255,-0.095952,0.825491,0.457607 +559,-0.714386,-0.112597,-0.016317 +14,-0.321493,1.43481,3.296698 +100,0.04556,-0.257255,-0.381461 +353,0.29782,1.474263,-0.118736 +543,-0.441206,-1.103288,-0.738972 +165,-0.173125,-1.221645,-0.981659 +43,0.04679,0.904396,0.751503 +201,0.630066,0.251241,0.558117 +108,2.600686,1.65399,2.833589 +328,0.422632,1.167411,0.257223 +439,-0.336962,-1.269864,-0.970527 +162,2.375673,0.501106,0.829112 +325,-0.538243,0.264392,-0.84648 +183,-0.726691,-0.888492,-0.593296 +197,0.610729,-1.935293,-0.368739 +498,1.264672,0.387133,0.347555 +477,-0.284225,-1.688935,-0.341385 +421,-0.125485,-0.051226,0.694887 +374,-0.369132,-0.958629,-0.284132 +470,-0.87084,-0.393146,-0.636553 +382,-0.689424,-1.945375,0.427072 +510,-0.715089,-1.098904,0.159257 +132,0.517559,0.312612,0.325926 +545,-0.265064,-0.472051,-0.652457 +121,1.175019,0.786039,-0.160085 +45,1.206661,1.557551,1.62047 +333,-0.681865,-0.69123,-0.994446 +134,1.247093,0.619463,-0.170263 +182,0.682803,0.3959,0.638907 +519,-0.450875,0.663299,-0.35856 +207,0.579086,-0.932328,-0.672177 +99,-0.095249,0.470421,0.307478 +366,1.389484,-0.200268,0.555572 +180,4.105459,0.650148,0.948707 +318,-1.025712,-0.450133,0.766771 +501,-0.162753,2.061664,0.905449 +229,-0.306902,1.987143,1.781414 +7,0.028859,1.447961,0.724786 +247,-0.424506,-0.305475,2.1033 +494,-0.408333,-0.901643,-0.570395 +174,-0.830233,-1.085753,-1.185478 +340,-0.032492,-0.130131,0.526946 +125,-0.271919,-0.730683,-0.758692 +209,0.091617,-0.445749,-0.22688 +347,0.000381,-0.454517,-0.339476 +332,-0.781363,0.439736,-1.002397 +361,-0.455973,-0.805204,-0.557036 +500,-0.041633,-0.827122,-0.233241 +300,2.061007,0.75097,1.00087 +283,0.26442,0.181104,1.376193 +107,-0.591508,-0.612326,-0.368739 +131,0.484159,0.974533,-0.094562 +303,-0.888068,0.391516,-0.953351 +567,1.653171,1.430427,3.904848 +507,-0.82531,1.48303,-0.325481 +3,-0.550021,3.394275,3.893397 +516,1.076575,0.73782,-0.004231 +143,-0.416244,-0.051226,0.003403 +398,-0.674833,-0.892875,-0.422174 +530,-0.577093,0.110966,-0.438078 +119,0.66874,-1.103288,-0.852841 +259,0.271451,2.451803,1.922 +450,-0.656375,-1.656935,0.544758 +192,-1.013934,-2.682695,-1.443878 +514,0.151913,-0.340543,-0.280951 +241,-0.592739,-1.256713,-1.122819 +402,-0.454742,-1.713045,-0.142909 +148,-0.199845,-0.033692,0.122361 +395,-0.344697,-1.129589,-0.834393 +299,-0.910393,-0.792053,-1.06951 +149,-0.271919,-1.545592,-0.457162 +60,-0.900022,-0.213419,-0.989865 +61,-1.066496,1.382207,-0.537316 +126,-0.056048,0.645764,0.217146 +372,1.150408,-0.577257,0.189156 +82,2.955784,1.09289,2.247704 +425,-0.886486,-0.866574,-1.166203 +337,1.744582,0.764121,1.453165 +386,-0.61647,-1.304933,-0.071025 +184,0.162637,-0.099446,0.481144 +58,-0.450875,-1.326851,-1.223647 +88,-0.537716,-0.086295,-0.050669 +117,0.376926,2.429885,1.232425 +459,-0.932895,-0.936711,-0.912002 +221,-0.341005,0.229323,0.098824 +223,0.364621,1.000835,1.232425 +128,-0.207404,0.273159,0.21651 +294,-0.574632,-0.112597,-0.681083 +541,-0.126013,0.071514,1.055578 +111,-0.620865,-0.160816,-0.186167 +266,-0.801227,-0.485202,-0.01759 +568,-1.075813,-1.859019,-1.207552 +362,-0.528926,-0.112597,-0.44762 +271,-0.743743,0.150419,-0.658818 +187,-0.631237,-0.003007,-0.955896 +155,-0.556174,-0.467667,-0.480063 +5,-0.24432,2.048513,1.721616 +511,-0.211623,-0.809587,-0.974344 +252,1.850057,1.693442,2.170731 +480,-0.585707,-0.50712,-0.167719 +513,-0.032668,-0.441366,-0.391004 +86,-0.126013,-0.077528,-0.360469 +105,-0.093843,2.359748,0.990056 +242,-0.716671,0.102199,1.466524 +312,-0.460192,-0.56849,-0.212884 +473,-0.565491,-1.672278,-1.285861 +364,-0.381613,-0.485202,-0.551311 +565,1.494959,-0.69123,-0.39482 +447,-0.0898,-0.428215,-0.420902 +488,-0.581488,0.886862,-0.677903 +151,-1.108862,1.342755,1.124281 +263,0.189884,-1.050685,-0.467976 +563,1.649655,0.365215,1.0454 +194,-0.168554,-0.033692,1.339296 +433,1.269946,0.290694,0.585471 +244,0.942974,0.610696,0.270582 +489,0.357589,-1.379454,0.240047 +465,-0.258559,-0.537805,1.974164 +483,-0.341181,-0.546572,-0.761237 +397,-0.509062,-1.623181,-0.464796 +52,-0.621217,-0.787669,-1.050935 +147,-0.124606,-1.432057,-0.013773 +27,0.918363,0.062747,-0.270773 +436,-0.445953,-0.480818,-0.566578 +434,-0.181211,-0.463284,-0.631464 +190,-0.207756,0.917547,4.315794 +407,-0.412728,-1.681045,-0.385914 +202,1.943226,0.930698,1.033313 +455,-0.307605,-0.664929,-0.713526 +452,-0.627369,-0.50712,-0.436806 +385,-0.215139,-0.051226,-0.611744 +188,-0.68749,0.141652,-0.981341 +159,-0.721769,-0.669312,-1.089867 +32,0.814646,1.360289,0.64654 +264,0.976374,1.027137,0.01549 +245,-0.839901,0.838642,-0.964802 +535,1.632076,-0.244104,0.376817 +430,-0.084174,0.417818,2.89275 +443,-0.904065,-1.509208,-1.201318 +154,-0.357354,0.676449,-0.18235 +9,-0.297409,2.320295,5.112877 +63,-1.00286,-1.490797,-0.550038 +329,0.166328,0.448503,-0.271409 +536,-0.2677,0.246858,0.121089 +65,0.078257,1.42166,0.555572 +438,-0.231136,-0.901643,-0.891646 +269,-0.826541,0.049596,0.004675 +36,-0.142361,0.536175,1.078479 +98,-0.647058,0.470421,-0.439986 +392,0.841015,1.566318,0.871734 +69,-0.572523,-0.121364,-1.168303 +517,1.345536,0.40905,0.487505 +92,-0.087866,-1.392605,-0.82994 +552,-0.502558,-0.393146,-0.940628 +505,-0.971217,2.990984,0.712699 +566,0.427906,-0.809587,0.350735 +163,-0.575863,0.562476,-0.130186 +358,-1.017099,-1.353152,-0.823579 +442,-0.305847,-1.103288,-0.936175 +15,0.110075,1.553167,2.56641 +503,3.342525,-0.546572,0.688526 +176,-0.862051,-0.099446,0.259131 +327,-0.627897,-1.36192,-1.147374 +1,1.890489,-0.375612,-0.430444 +404,-0.610845,-1.208494,-1.188468 +24,2.345788,2.109883,0.658627 +139,-0.785054,0.189871,-0.458434 +101,-1.222423,1.14111,-0.852841 +350,-0.594321,-1.437317,-1.205517 +90,-0.135154,-0.914793,-0.494058 +191,-0.529278,-1.687182,-1.046355 +383,-0.492362,0.452886,0.668169 +11,0.73554,0.316995,1.950627 +314,-1.125913,0.102199,-1.123391 +352,4.137101,0.904396,2.159281 +235,-0.290202,-0.160816,-0.655002 +305,-0.710343,-1.618359,-0.751695 +375,-0.033546,-0.388763,0.004675 +215,-0.229378,0.597545,1.16245 +466,-0.336611,0.119734,0.640179 +486,-0.087163,-0.796437,-0.300672 +250,1.985416,-0.493969,0.400354 +339,3.560506,0.838642,0.086101 +103,-0.91303,1.03152,-0.153087 +206,-0.965064,0.400283,-0.824215 +345,-0.920238,0.159186,-0.576756 +346,-0.558987,-0.152049,-0.75742 +317,1.062512,0.483572,0.140173 +499,1.545938,0.615079,0.670714 +478,-0.725988,0.124117,-0.33884 +47,-0.213029,2.026595,1.032677 +456,-0.620162,0.360831,-0.325481 +211,-0.587641,-0.191501,-0.421538 +490,-0.452984,-0.296707,-0.469885 +113,-0.889826,-0.103829,-0.314031 +359,-0.77521,0.040829,-0.95017 +344,-0.640202,0.597545,-0.908185 +532,-0.188419,-0.261639,-0.622558 +261,0.593149,-0.366845,-0.672177 +4,1.220724,0.220556,-0.313395 +226,-0.85291,0.075898,-0.884012 +464,-0.339247,-0.182734,-0.367466 +277,0.624792,-0.353694,-0.879559 +257,0.084761,1.93454,1.247056 +260,1.693603,0.869327,0.255951 +301,-0.578851,-1.199727,-0.244691 +537,-0.690654,1.94769,0.450609 +403,-0.526817,-0.664929,-0.371919 +233,1.742824,-0.441366,0.138901 +487,1.51078,0.834259,0.75214 +141,0.619518,-0.042459,-0.195073 +68,-0.977194,0.693984,1.159269 +363,0.225746,0.062747,-0.549402 +379,-0.654793,3.771263,4.348873 +51,-0.393567,-1.028767,-0.611108 +195,-0.492186,-0.993698,-0.659455 +239,0.927153,0.181104,0.758501 +161,1.080091,-0.875341,-0.335023 +169,-0.152733,-0.472051,-0.57612 +212,2.845036,-0.796437,-0.653093 +120,-0.650574,0.983301,-0.097107 +367,-0.449996,0.194255,-0.237058 +171,0.198674,0.338913,-0.634009 +348,-0.711573,0.90878,-0.905004 +409,-0.47531,-0.366845,-0.47561 +112,-0.301628,-1.879621,1.049853 +524,-0.886134,0.417818,-0.19062 +62,0.138729,1.386591,2.356484 +393,2.110228,0.781656,2.01933 +75,0.601939,0.772888,-0.316575 +124,-0.436812,-1.309316,-0.007411 +231,-0.752884,-1.768278,-0.706529 +285,-0.55635,-1.25233,-1.196102 +448,-0.088042,-1.028767,0.067653 +460,1.349052,1.211247,-0.062755 +218,1.983658,0.128501,0.440431 +467,-0.87963,0.281926,-0.819126 +525,-1.063508,1.390974,-0.195709 +80,-0.624908,1.022753,-0.551311 +472,0.045735,-1.133973,0.157985 +549,-0.659188,-0.524654,-0.578665 +527,-0.554943,-0.138898,-0.298127 +168,1.370147,0.229323,0.818934 +351,0.061029,0.992068,1.59248 +57,0.184435,0.194255,1.111558 +560,-0.305671,-0.362461,-0.177261 +424,-0.878399,0.325762,-0.75742 +355,-0.585707,-0.998082,-0.343929 +42,1.354326,-0.33616,3.117943 +217,-0.838319,-1.62625,-0.728794 +286,-0.587992,-0.91041,0.17198 +135,-0.399017,0.417818,-0.64864 +521,3.096417,0.080281,1.046672 +278,-0.248715,-1.199727,-1.132615 +504,-1.020263,2.535091,0.571476 +97,-0.904416,-0.033692,-1.014866 +84,-0.54967,0.233707,-0.343293 +553,-1.027998,-0.967396,-1.089612 +469,-0.619635,2.000293,0.213329 +87,1.305104,-0.327392,0.421983 +22,0.176348,0.290694,2.170095 +19,-0.297761,0.509873,-0.489605 +265,4.485168,0.338913,0.064472 +38,-0.319559,-1.68762,-1.291078 +479,0.103922,0.233707,1.220974 +17,0.763667,2.039746,1.075298 +189,-0.586937,-0.998082,-0.56785 +178,-0.477771,-2.240829,-1.399158 +164,2.676276,-0.419448,0.661808 +451,0.950006,0.895629,-0.443803 +290,-0.199142,-1.426358,-0.044944 +67,-0.72757,-0.147665,-1.03554 +110,-0.902834,0.62823,-0.494694 +556,-0.937465,-0.257255,-0.854113 +150,-0.463884,-0.11698,-0.914547 +321,1.364873,-1.182192,-0.639734 +526,-0.282643,1.316453,0.36982 +18,2.667486,0.825491,0.386359 +446,0.978132,0.338913,0.775677 +432,1.051965,1.496181,0.254042 +523,-0.314109,0.444119,0.014854 +418,-0.551428,-0.042459,-0.595204 +272,2.642876,-0.22657,1.388279 +153,-0.7731,0.075898,-1.0468 +509,0.050658,1.789881,1.542225 +288,-0.778726,-1.296166,-0.445075 +258,0.607213,0.790423,1.672634 +228,-0.451051,-0.423831,0.579746 +476,-0.091558,-0.748217,0.563842 +423,-0.30198,-0.774519,0.397174 +64,0.013566,2.311528,0.965882 +137,-0.735833,-0.586024,-0.569123 +343,1.159197,-0.463284,0.58229 +302,1.433432,0.102199,0.539669 +561,-0.77521,-1.740223,-1.267986 +8,-0.248363,1.662757,1.81831 +502,-0.57762,1.123576,-0.5036 +28,0.682803,1.390974,2.269333 +457,-0.435405,-0.152049,-0.941264 +136,-0.586937,-0.230954,-0.963529 +365,1.581096,0.014527,-0.106013 +384,-0.451578,-0.69123,0.090554 +81,-0.467048,0.930698,1.430264 +306,-0.428374,-0.857807,-0.761237 +282,1.313894,0.851793,0.767407 +475,-0.483045,0.010144,0.042844 +118,0.688077,2.329063,1.515507 +55,-0.658133,-0.327392,-1.062767 +557,-0.966822,-1.098904,-1.162132 +284,-0.500975,-1.451345,-0.143545 +256,1.837752,-0.187118,1.772508 +426,-0.773804,0.014527,0.288394 +387,-0.237816,-2.083458,-0.833121 +497,-0.480408,-0.209036,-0.023315 +298,-0.107027,-1.662195,-0.238966 +157,0.266178,-1.956334,-0.529682 +249,-0.683447,0.28631,-0.611108 +342,-0.774507,0.413434,-0.211612 +508,0.083706,0.132884,-0.751695 +267,-0.385129,-1.396988,-0.516959 +338,-0.87295,0.343297,-0.725613 +360,-0.518379,-1.728826,-1.342223 +12,0.793551,-1.256713,0.865372 +210,1.363115,-0.638627,0.240047 +533,1.343778,-0.993698,-0.005503 +167,0.666982,-0.682463,-0.269501 +79,-0.454391,-0.152049,-0.255506 +369,2.366883,-0.130131,0.853922 +437,-0.229554,-0.564106,-0.821034 +485,-0.527344,-0.651778,0.965882 +408,0.823436,0.693984,0.758501 +308,-0.319735,-1.847183,-1.24623 +414,0.089332,-0.770135,-0.989865 +41,-0.644421,2.565776,0.098824 +276,-0.742864,-0.283557,-1.150045 +146,-0.507831,0.268776,0.985603 +405,-0.71755,0.172337,-0.571667 +35,0.612486,1.049055,0.822115 +13,-0.007178,-0.844656,-0.393548 +166,-0.820212,0.492339,-0.817853 +173,-0.850976,-0.472051,-1.093302 +531,-0.580082,0.992068,0.268037 +388,-0.756928,-0.97178,0.169436 +205,0.191466,0.733436,0.5015 +6,1.27522,0.51864,0.021215 +144,-0.772397,-1.085753,-0.839482 +540,-0.743216,0.093432,-0.270137 +30,1.387726,0.733436,1.090566 +435,-0.019835,1.268234,0.652266 +31,0.014269,1.37344,2.056226 +203,0.478885,3.955374,1.696171 +59,-1.064738,1.794265,-0.829304 +95,1.528359,-0.586024,0.633818 +37,-0.588344,-1.549975,-1.323648 +331,-0.432944,-0.156432,0.451882 +237,1.528359,-0.419448,-0.147362 +10,0.473611,-0.625477,-0.630828 +389,0.760151,-0.318625,-0.08184 +324,-0.522949,-0.296707,-0.391004 +179,-0.544747,-0.708765,-1.271103 +220,-0.30655,-0.05561,-0.043671 +50,-0.640026,-1.046301,-1.069447 +401,-0.511699,0.220556,-0.615561 +368,3.977131,0.172337,-0.581845 +29,0.608971,-0.301091,0.171344 +78,1.305104,1.382207,2.303684 +204,-0.356299,0.448503,-0.104741 +214,-0.121794,1.03152,0.96461 +127,1.155681,-1.326851,-0.177261 +46,-1.122222,-0.11698,-0.754239 +554,-0.5008,-0.423831,-0.586935 +449,2.006511,0.194255,0.355188 +564,2.015301,0.378365,-0.273318 +297,-0.574808,-0.818354,-1.110223 +491,0.579086,-1.4794,-0.982868 +492,0.958795,-0.064377,-0.137184 +181,2.124291,0.733436,3.207003 +427,-0.68749,-0.090679,-0.538588 +471,-0.550197,-1.239179,-0.998771 +515,-0.706651,0.698367,-0.616197 +445,-0.644597,-0.05561,-0.458434 +506,-0.642136,0.343297,-0.144817 +93,-0.318504,0.067131,-0.5036 +453,-0.22973,0.102199,-0.677266 +357,-0.327294,-0.748217,-0.976252 +544,-0.33749,-0.261639,-0.321664 +26,0.028684,0.882478,2.608395 +356,-0.50871,0.084665,0.073378 +227,-0.127243,-0.822738,0.689798 +377,-0.351553,-0.945479,-0.690625 +234,-0.957505,0.790423,-1.012194 +341,-0.92639,-0.39753,0.555572 +129,1.245335,-0.213419,0.838655 +289,-0.740579,-0.901643,-0.999916 +539,-1.154919,1.193713,0.331651 +145,-0.652156,0.439736,-0.016317 +138,0.159648,0.382749,-0.240875 +410,-0.631588,0.56686,-0.585662 +56,2.222734,1.316453,0.616006 +216,-0.614185,0.356447,0.320201 +281,-0.60979,-1.261097,-1.076762 +16,0.452516,0.615079,-0.427264 +444,0.723235,-0.266022,0.078468 +547,-0.91971,0.601928,-0.188711 +270,-0.344521,-2.047074,-1.297121 +44,-0.245902,0.786039,0.866009 +34,0.66874,0.536175,2.074674 +371,-0.108082,-0.866574,-0.512506 +310,-0.698741,-0.441366,-0.925997 +538,-1.112026,-0.296707,-1.08694 +72,1.407063,1.145493,3.086136 +73,-0.11986,0.382749,0.635726 +279,-0.370187,-0.607942,-0.520776 +196,-0.129529,1.811799,0.368547 +74,-0.473728,-0.647394,-0.445075 +23,3.048953,0.338913,0.036482 +334,-0.591156,-0.445749,-1.041647 +400,0.74433,2.407967,2.146558 +431,-0.641257,0.553709,0.05493 +254,1.918616,0.759738,0.393357 +48,-0.523828,0.746587,-0.245964 +130,-0.589574,0.452886,0.02694 +555,-0.919359,0.264392,-0.529682 +520,-0.973854,2.307145,-0.283496 +122,2.096165,1.632072,1.082296 +496,-0.433999,0.917547,0.826568 +225,-0.012979,-0.11698,-0.647368 +326,-0.22973,-0.187118,-0.912002 +106,-0.630885,1.597003,0.074651 +381,-0.762026,-1.002465,-0.356652 +415,-0.628776,0.448503,-0.226243 +71,-1.048038,-0.511503,-0.067845 +558,-0.258559,-1.304933,0.399718 +21,-0.994422,0.001377,-0.887193 +316,-0.646003,-1.414523,-1.278291 +463,-0.677646,0.080281,-0.46734 +454,-0.434351,-0.432599,-0.652457 +413,-0.023702,-1.08137,0.510406 +330,0.332978,0.487955,1.231153 +468,0.978132,-0.511503,1.426448 +322,-0.49412,0.978917,-0.198253 +307,-1.046104,-1.479838,-1.284653 +373,1.87291,1.044671,0.325926 +304,-0.686963,-0.787669,-0.479427 +529,-0.581312,0.864944,-0.579301 +40,-0.162929,-1.006849,-0.317847 +115,-0.523125,0.772888,-0.091382 +2,1.456285,0.527407,1.082932 +39,-0.24643,1.255083,1.070209 diff --git a/examples/data/breast_hetero_mini_server.parquet b/examples/data/breast_hetero_mini_server.parquet new file mode 100644 index 0000000000000000000000000000000000000000..fb612dfcbb98bd4d6e15f6fd37153427e277ee25 GIT binary patch literal 20463 zcmdtK2T&DH(>HoRKtPb36crG`07@1VokI>Pl9L$7L6Rf^Q4v8zK~ykc1O#(JK@4D% zlYrzL4~Lwih=DtM@c-oJt@^6I_g39|${*d+GrQX}JJU1M)2pGHu`C}0-(?-Xy7hbc zrkMFi+$2)TdWHxR3mpR&9Sbw*70YsFk|aF~3#pi8IRlBCU4)dv&P>|NE=bzR&Pj^J z{21ovF#Uxg!EQ*Z!txK8zr)VVMMu(RXCZCC(sfwUflD&sTt&?Dvs;ps@ZFwco{Fgx zmX~3Sd zQ$tL7u{;;^B^V|cf$SSel9=wpc^`0|2L{48h-Fq-_5ky8?1>~FOvNx{arrw~o`mz? zV^A>GV-&H=kk+!3No1UN2*VG{USP_E%WL54=COW)m_}lGCB}6OqE5FlS}=~_YX>m3 z!`DpUa={pB7@}DA1EUM$BZe8qLtHNxoL7Ny2_ppK1jciWXq>N(@9`AVZcJ4$Rm5~7 z&LiqDjnhP#n;2|Zw*y$Vi1}^OhNP94cfhCZz{fS3r?3}xMS#Ic_yaGSgwW3 zW?~fL^b}6FW15N)fYE?)3gb6MIIfo{`%O|1E-Q`G`|vZ*Vg4QFm*e|=#k@V{Ju#n- z`P~?^INgiyxg67P7-LwMmsoC!aT22%%MRfDy_mXU%wR}itj0Ks+vY5$+i?DN_8i=H zoTO#=dIp@1#r!4Ab7THE#xjgNTs8+&daTzZ&cBND<1xL6LDc;k&Qry77Y0!$KU~fj z=RLtN!s#%K_xPF}m|9}^V|>A7+Hrm-=0h{4Co5E1W<(e@i-h(2pBj7Sbf4PDA<(QYp`FXf}8paGx4`LAQ z)PoU&p@8$_FeTb39Y5y{rk`=y8@L=X&WbS#uM3 zZebYjN{Eqcd!t?>Y&I#ueeVPN) zZ&>ykLx-J{9UGQ-Y$gP8k&8In5#tc%t#LMB{xmMW0`ogCj^Ok#PRnBcIOZEMUxWER z%r9Wx1oO@q#E>I~vo)r~t0aa&2gWws?MiTdEyfVWF$|(S@hW-okYL0-16G+B=2iH5 zK1}H_7%+(LEQDWE94<46tJsfe42C9_AH`))U@DJkKBj~jBTU8@!3@?1mGhCi% zngpEo#%0;CsUe0y6t3?cOhd4|A7c$p=U`fn(SmtGUv_LV36s`@u>phVPLFZkVGI?V zFT*8*rNrRcj3q?pS&P$=IEMwlFn-L>W0@_^GsSchu4O&uEwG#)r+F|~a9RSF@5JE3 z$i_0Fp@4A@V-s#cUR>rX#x;!f7(~~9i17l0hy6S@eKOcY6H@>)=`w~eZke^X z+*>SPhv^qA+lg@)V*|z>EFZ;kR?J7>>xh2z7SlFN;6t8s6?g7b+9m@op$Sk{DjAuQv>d=|!fT!xrPq;NjbKYVeX z5~i#0{dKWS5Ay<;7T_}aG@cpPMugTvuWEOJr_?dh-t%|Sxie*I@cQJ_j6QfNXLmq=L&cwaB@Zw=3hA%rayMPEu zlC<>WBk`rL;QPEnidgz^@-gu-2@5hS(=TNTGXE(>;wNqV4^o`l!Z_1RzYH@|%yg`$ zNem)6BnI9iIdsh8i8=Ia3SBwNI5kCb8F)>P3TwnaiNDM>}E+|#>@R(ltS77P2;*%q$}q$U-w z4VmdKUKh$BRw5G4YgZzAUOc%(ELwq5vi`EBSgCltsa@%Yn-0mP5_kM4rILx^Vr3gs z;_b>dJx)n3+x$G8QnuxFf!G(R^g6pQ((kFsU$$n=P`+%-VOU=-Q^0FqE?Xl0pj@t8 zp{HEFN^^aMLY=97g<_M#g9@cKzn+TiUE%91m8tRel_~=%4=Q(zr1wVvPlf6nxLMI=V?Dl!9~LzNoyhLkFGHbrWc2B(&IwI;8bL$#KmV@kEQus^k0 zM1sSIk(9+^V%+Pw<(4z>G~dsAu+RA;HR`^k(O>I^mc9P5m9 zH>B1X?^W!rGcnZKP;Y8#=2&lL>6lt?ZsXruZ*e4ILxZKmHOB@km(Kz*o2KJ^l9txT5Z9E*xDA9BzoX@Gr_WXv2O?J_WeNFb4wIrGy;?10z9d9~5 zY<9Zi-`DJ%7$MQ(l5)+d#r1LO!xp#a8GS8BUl&TWx~JDWwR*hoec0-mHQU$fmBT36 z=3T(&+;*&F!=pBza>f2O-zqK1cE36^=XU=l$4Bh}ZT|i3$GajVI|8ZKoI8RBQXh2$ zk7V?BoR}z->p4E!!#XP6}(cZIPj4RoF5)ZW+~ z&TH<{9U0KT8==rCZ`TI=x(`6)nua{u{KN&1SQh1+1;-e?wKb)A5NH`s; z$#+3>FJI2mz9Up@W|1?opx@y2;v1x!zsYwxW)yZEerux?JP9)oy5m259{}!ibbDUf z)u6t$+@HT?4j?7^$&aVhs9^rV@O$G_2Pm8o{0M%@O~SZ)&v1c)cZo=GiXbyb8u8zANrkc zba$l#6~aoy_GO)=LUZ8jLeKlXa3O%t_>^%j2r@~1xKP*+$}X>VuU&YFeA+gxdA`_* z4*qHqN0ea@T^n`dk#8Nue05k<-8TUqG0a7|j`OH?KEmQ^Rv$!EkOUmQG{J)}xAQYv z03O!tSN@bShqPH1R_wBS4?Z1>#Vm2dF!aj8(i>GFx(in}_)1MeQ`3g0ndiD-MaILR z;!TYZt;=iOko^no9@Gqb($^2+92=~s-hV@phN(^*>Eoc_YT_U3UIl$311@HJJ|jyL zE8YO3QMke~uXvTAAAFrYsjrh71cnzmm+N2j!lU(}w+mPFL({~Jz7^8|wDilC$!}`_ z4y`l0uShn-BKdP1{`7kR^7PY6w==tX;eqd|3q3h~a7XBui?U-S*ii4i z9BUecuF&f*kF0J48KU9G#(-|GkaA)w1>zn(e>fcV4oYL^`Hm|0!@?mAZ=0e4WXI>_ zur_EI9v|~Mm0vIc-lLj&x+%3lpT)?{i^jk~{6zRI(GjTSxgJPv?u4HvPc@qlyn}X| zpC0=!zXx+Ri{<3DKKQgj<%mUG-JkkALR1c>dnY(*k-M0@zl9W;Ygrj81H4 z>&zp>q|Yi9x`~Qdj5h@+R5FlHU5e#*W@|+ZPfv4}Uap6!;mD>Vbj!#JNqTmAD&61~ zF?fPqtsitq+b;@S>_N|;(Ps#%&Y+l$qwJq$YQQ|}N)VMJ2Ljm_YFh6%g8wDD=HYhS z4}z12^EmpT)cl4CJ*5vSmRV98W4n-*wO=lzp8L!w7XR>n+g=@b)_UkAonW^E} zPea!Uc>8YCT7mik(3>t!SBkldytZU3t=`fKJe!rXM8sM^eYpFJ=9MAPa?HO~mYfL6 z2CUO-ovT4=CRa|$FB4tN+j?Q`yfR|_?7A>Q8iZfp-yM0fwH-nnJDv_3rb5%*G1bWs z5_w2;RlC|q6>2ne+m<{^fkdH#Jem%Z--qL~+$Avk`&=RsjY zH*DmJY&H1;mPu z9;#3Hh0dv{-4^~#0r$h?ZEYVCLBMWSN08|~7=1d-I6u}0F^2;;l=`;AoBH1PP(cb1 z!_(5e351H;MXZOvp+ViPi=w~Y!RK>b@1t*1V7u2H-oETfShb(w*jj@U#5=k<<)^~} zn&qkVl#VL|O@qT<8>Pljn%t`IS(FBpcwUQbXLJ=@<4~&~sIP}b8wCed{VteZW)Vc+ z-2@I&*3&)CNBwcu@Fvu32^*rhetU0c~e0Z@Jww>4A_eroHk|h>G;<<*9npXWQ zzun_V-%jtt58(xr=O$(A5uE}3PgZ^q8XJL0&Z7N29SzWFAeINrwZMGbdGy_ii9h-l zqDKQnLsUa$`^y^-nbVPAb->>CYZajXHb|JscL?AR9K<5Qvc@Q z5IXo!B6qbh1x$TEf3Kw|ra=Zd`JaH@Es>8o8U;zWzP z0zD`Yn9-KNqCEkmRjGRxuhv4qvDNHd(pjkdv@V>?W+i8yj9nPMHVjsZ%)Xxls4%yw z*x?xZjjr9=RmB?J0~%jGo|Y=0ClAw2U*5EC5X9L%kFJQFh7X?lK`$6^yW`&UVQ((f z=ES-+jduZE{#1W!SSOtPa%bzssD2cqe$^^_*%WmC#xE?Ilk7WJ>GJGq4ajWTy<7BC zADBVS_X(*a@E|FPsM}P)W0SS*%oponu;FO1q2W8&rMv%%M!_O_pVoY3=mZ;C_d?0* z3)}I$6k6R|(LDy&HXF@d_Zfw8-;i|Wo^Ck*Uga7aX%gA(T{y8RYaV^u@nqYco&B%{ z*ow|;eu156u`#l*6Uwwz$7hx#4l(B{FbN5M^p}g$IDy&=R>Fv zJr^jtW^D=For}N2^Mit3@Z9L(J3o!yR>Tal>=}k1%dfDhysL+EFPh8t{O&l zgx3DBlF}N7uGxgQyL-#g)iPdR&KWAa-r+LhXpYDWK_vZzw&qk54#_CXZ z-AQD4iC!}|Iv-*$EbdIsV33WPM9B8oz z(w|+PigSB}e(lZn_4Cp1)RZ(D;+dkn`R5R?{@lrdWkeTc(dG-Od z{_9eIH=Y1KS_c~ey&1U+z1O=T_EGQhmp&74^!}w{wz?h&yy?X}PMI6|D^=c7v zjZu>fUs($V8m8S}C%DL8vdkKjZhb^ttFUGF8bD>ggI39uP$1*N@2c{+4#>Ww?6Az0 zkvzUO@rRGo82Y4}Bo%V96hiMG>n7{=Lk!Uj)DfU2h9|sR9EFo#b@3G14)TKQxg#28 zQOIGNr#1XF5V_BxkKTfj>>kt`+`4xh@e<3Ax(U>-bu-y!RUPlgj#C`!0Cn7Y9~t_Pmi_3sJ4Y|z z#(k|YF$Q;%*MIoAybEqdiKTct|3a}oGPga6|RT>RJU#TLa+|FTWnmjDRe3=5Ck6v))Di6TU?hVSbXG)Ni zt~~!f=NWW$uxc4#mDcceuW=8z9)7$xqHhe}Fe9;ZqSO6;=JVt?3y zoX4ZzZDyH-o4JBlHu}w@qR8ei?`rWra1P~IXS71%Bf7IG`#VrpTB6KGlL2&l2et&C zy5JlATlFiR-O%@v7_hZa)?r*JaDocxS$TZJ;Vux-C|OMDKv7L@OfHR=EfQ|~}E2WIlsw3eSRL;(|QJ!7i-k@!tFC+P?(kd2yO zKYa5Wy*3q?kjw7^)_vT2%GxOqigyk6M@PXaLahDkrg2E#SS2}CKq8aM@G`fk73x)W zsGs65K=D?iY(*I=5WPCwZU8*@Uv3&(ONG#(~%dtb9&|hat>QwO{o@6!W5( zOL;4FZFAaVu5}X>oVaJh_SYlxEj*{5-0y^3FOBHNtO=01-y7up?HAhn)2J@tM=uCx z-n_s176nvibUl#s}Wnc$|c=hEDGNhE5zk@{Hl)SNM-E$-g#xg3Ny{F9?_9 zzf1pr<&xCnzH#Jgl!T9ao}T2r5PJQM$W#}RtgZQ1ivD`aw+<=JGf{a3%FOk9jot9Nb8_RdhAOUgZ$*K4cv-8pNpW9WIl z!OpR)W&2g9$~E@ynrl1DA@qIVEeHBFKg~g=V^QE#V-)h^RAD+syKg@9Nl`U1^*{9g-$77%}n9j3* z*Y_D7Lp2sfUL!3be_ms~O&Po<`&4FmO^x&v`OM6${Q1nSGLrt)IjQKJbnUwJ(X{&O z2fT9mZrJ!%o890HXpgWs6g*mJaro3PM$02%?An&L=hmFFw7Vc(WNClt%%q^h6+P`* zhj=4np+naXp0jeg45v23he_P@7nv8 zd2XftM>nOg&llOoJHB7l1n&CPmTDdJZ#WovFrdZ5ndNwUea|0XCC`6#R?hcIUOe?< zvhn`uMLNO6&}H0~iD#CJ-AoKyA=lIuxN1CN(z(?>O-YgK{yOrk50+g@_ZlV_wv1Cq6I@V z3C%RJ5vJY(wE^C#RQRu=qGi`6$K_x4f$&#na(2Q5@blb`J-TcHbvd4kd2{Lk6e;eG z`!!t$;cV3A-qvn(cr5M#sd@~mZeK7JJkS7pkC`+$P7 +sfxK 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

X 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 IiR1782c}8CaR2}S literal 0 HcmV?d00001 diff --git a/images/graph.png b/images/graph.png new file mode 100644 index 0000000000000000000000000000000000000000..21e9fc96f96c006f20174c15a25640cb83492e88 GIT binary patch literal 126550 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 literal 0 HcmV?d00001 diff --git a/images/secureboost_inference.jpeg b/images/secureboost_inference.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..d7f91c6d42a5af1ec0a82da96158d1c466debadd GIT binary patch literal 89496 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 literal 0 HcmV?d00001 diff --git a/images/secureboost_train.jpeg b/images/secureboost_train.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..603b278b87f262a7284313eadb0cd4dde127e00b GIT binary patch literal 140175 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�H>E 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 literal 0 HcmV?d00001 diff --git a/petml/__init__.py b/petml/__init__.py new file mode 100644 index 0000000..6ce6a1b --- /dev/null +++ b/petml/__init__.py @@ -0,0 +1,21 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .version import __version__ +from . import operators + +__all__ = [ + "operators", + "__version__", +] diff --git a/petml/fl/__init__.py b/petml/fl/__init__.py new file mode 100644 index 0000000..4d6166b --- /dev/null +++ b/petml/fl/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/petml/fl/base.py b/petml/fl/base.py new file mode 100644 index 0000000..388885f --- /dev/null +++ b/petml/fl/base.py @@ -0,0 +1,27 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from abc import ABC, abstractmethod + +from petml.infra.common.log_utils import LoggerFactory + + +class FlBase(ABC): + + def __init__(self): + self.logger = LoggerFactory.get_logger(self.__class__.__name__) + + @abstractmethod + def set_infra(self, **kwargs): + pass diff --git a/petml/fl/boosting/__init__.py b/petml/fl/boosting/__init__.py new file mode 100644 index 0000000..7aba90d --- /dev/null +++ b/petml/fl/boosting/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .xgb_model import XGBoostClassifier, XGBoostRegressor diff --git a/petml/fl/boosting/decision_tree.py b/petml/fl/boosting/decision_tree.py new file mode 100644 index 0000000..c8a3b4d --- /dev/null +++ b/petml/fl/boosting/decision_tree.py @@ -0,0 +1,347 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +from petace.securenumpy import SecureArray +import petace.securenumpy as snp + + +class MPCTreeNode: + """ + Class for secure decision tree node + + Attributes + ---------- + is_leaf : Bool + Flag to check the node is leaf or not + + leaf_weight : float + Leaf weight value in secret share format of the current node + + split_feat : SecureArray + Best split feature in secret share format in current node + + split_val : SecureArray + Pplit value of the best split feature in secret share format in current node + + left_child : MPCTreeNode + Record the information of the left node of the current node. + + right_child : MPCTreeNode + Record the information of the right node of the current node. + """ + + def __init__(self, + is_leaf: bool = False, + leaf_weight: float = None, + split_feat: SecureArray = None, + split_val: SecureArray = None, + left_child: "MPCTreeNode" = None, + right_child: "MPCTreeNode" = None): + self.is_leaf = is_leaf + self.leaf_weight = leaf_weight + self.split_feat = split_feat + self.split_val = split_val + self.left_child = left_child + self.right_child = right_child + + +class MPCTree: + """ + Tree class + + Parameters + ---------- + reg_alpha : float, default=0. + L1 regularization term on weights + - values must be in the range `[0.0, inf)`. + + reg_lambda : float, default=1.0 + L2 regularization term on weights + Values must be in the range `[0.0, inf)`. + + min_child_samples : int, default=1 + The minimum number of samples required to be at a leaf node. + A split point at any depth will only be considered if it leaves at + least ``min_samples_leaf`` training samples in each of the left and + right branches. This may have the effect of smoothing the model, + especially in regression. + Values must be in the range `[1, inf)`. + + min_child_weight : float, default=1.0 + Minimum sum of instance weight (hessian) needed in a child. If the tree partition step results + in a leaf node with the sum of instance weight less than min_child_weight, then the building + process will give up further partitioning. + Values must be in the range `(0, inf)`. + + min_split_loss: float, default=1e-5 + The minimum number of gain to split an internal node: + Values must be in the range `[0.0, inf) + + max_depth : int or None, default=3 + Maximum depth of the tree. + If int, values must be in the range `[1, inf)`. + + columns: SecureArray + Encrypted column names in the training data to prevent leakage during training. + + Attributes + ---------- + root: MPCTreeNode + The root node of the tree + + """ + + def __init__(self, + reg_alpha: float = None, + reg_lambda: float = None, + min_child_weight: float = None, + min_child_samples: int = None, + min_split_loss: float = None, + max_depth: int = None, + columns: SecureArray = None): + + self.columns = columns + self.root = None + self.reg_alpha = reg_alpha + self.reg_lambda = reg_lambda + self.min_child_weight = min_child_weight + self.min_child_samples = min_child_samples + self.min_split_loss = min_split_loss + self.max_depth = max_depth + + def _calc_threshold(self, gsum): + """clip the value of gain""" + res = snp.where(gsum > self.reg_alpha, gsum - self.reg_alpha, + snp.where(gsum < -1. * self.reg_alpha, gsum + self.reg_alpha, snp.zeros(1))) + return res[0] + + def _calc_leaf_weight(self, best_gradient, best_hessian): + weight = -self._calc_threshold(best_gradient) / (best_hessian + self.reg_lambda) + return weight + + def _calc_split_gain(self, left_sum_grads, right_sum_grads, left_sum_hess, right_sum_hess): + + left_sum_grads_threshold = self._calc_threshold(left_sum_grads) + right_sum_grads_threshold = self._calc_threshold(right_sum_grads) + sum_grads_threshold = self._calc_threshold((left_sum_grads + right_sum_grads)) + + gain = left_sum_grads_threshold * left_sum_grads_threshold / ( + left_sum_hess + self.reg_lambda) + right_sum_grads_threshold * right_sum_grads_threshold / ( + right_sum_hess + self.reg_lambda) - sum_grads_threshold * sum_grads_threshold / ( + left_sum_hess + right_sum_hess + self.reg_lambda) + + return gain + + def _find_best_split(self, X, grads, hess, sum_grads, sum_hess, index_array): + """find the best split feature and value in current node""" + max_gain, best_feat, best_split_val, best_feat_idx = snp.zeros(1), snp.zeros(1), snp.zeros(1), snp.zeros(1) + + for feat_idx, item in enumerate(self.columns): + x = X[:, feat_idx] + filter_x = snp.reshape(x, (-1, 1)) * index_array + cur_feat_data = snp.hstack((filter_x, grads, hess)) + sorted_cur_feat_data = cur_feat_data.quick_sort_by_column(0) + left_sum_grads, left_sum_hess = np.array([0.]), np.array([0.]) + + for i in range(sorted_cur_feat_data.shape[0] - 1): + x_i = sorted_cur_feat_data[i, 0] + x_i_next = sorted_cur_feat_data[i + 1, 0] + grads_i = sorted_cur_feat_data[i, 1] + hess_i = sorted_cur_feat_data[i, 2] + left_sum_grads += grads_i + right_sum_grads = sum_grads - left_sum_grads + left_sum_hess += hess_i + right_sum_hess = sum_hess - left_sum_hess + + same_val_flag = snp.where( + snp.reshape(x_i, (-1,)) == snp.reshape(x_i_next, (-1,)), snp.zeros(1), snp.ones(1)) + + if (self.min_child_samples and (i + 1 < self.min_child_samples or + (len(x) - i - 1) < self.min_child_samples)): + continue + + gain = self._calc_split_gain(left_sum_grads, right_sum_grads, left_sum_hess, right_sum_hess) + gain = gain * same_val_flag[0] + if self.min_child_weight: + left_hess_flag = snp.where(left_sum_hess < self.min_child_weight, snp.zeros(left_sum_hess.shape), + snp.ones(left_sum_hess.shape)) + right_hess_flag = snp.where(right_sum_hess < self.min_child_weight, snp.zeros(right_sum_hess.shape), + snp.ones(right_sum_hess.shape)) + + gain = gain * left_hess_flag[0] * right_hess_flag[0] + + gain_condition = gain > max_gain + max_gain = snp.where(gain_condition, gain, max_gain) + best_feat = snp.where(gain_condition, snp.reshape(item, (-1,)), best_feat) + best_split_val = snp.where(gain_condition, snp.reshape(0.5 * (x_i + x_i_next), (-1,)), best_split_val) + best_feat_idx = snp.where(gain_condition, snp.ones(1) * feat_idx, best_feat_idx) + + best_feat = snp.where(max_gain < self.min_split_loss, snp.zeros(1), best_feat) + return best_feat, best_split_val, best_feat_idx + + def _create_leaf_node(self, grads, hess): + is_leaf = True + grads_sum = snp.reshape(snp.sum(grads), (-1,)) + hess_sum = snp.reshape(snp.sum(hess), (-1,)) + leaf_weight = self._calc_leaf_weight(grads_sum, hess_sum) + return MPCTreeNode(is_leaf=is_leaf, leaf_weight=leaf_weight) + + def build_tree(self, + X: SecureArray, + grads: SecureArray, + hess: SecureArray, + index_array: SecureArray, + current_depth: int, + last_layer_feat: SecureArray = None, + last_layer_feat_val: SecureArray = None, + last_layer_feat_idx: SecureArray = None): + """ + Build the tree node recursively. Will only stop when it reaches the maximum depth. + When the current node cannot find the optimal split point, it will use the value + of its parent node as a substitute. + + Parameters + ---------- + X : SecureArray + train data + + grads: SecureArray + Secret sharing data of first order derivative + + hess: SecureArray + Secret sharing data of second order derivative + + index_array: array or SecureArray + An array to record the sample is in current node or not + + current_depth: int + current depth of the node + + last_layer_feat: SecureArray or None + Best split feature in secret share format in its parent node + + last_layer_feat_val: SecureArray or None + Split value of the best feature in secret share format in its parent node + + last_layer_feat_idx: SecureArray or None + Best feature idx in secret share format in its parent node + + Returns + ---------- + sub_tre: MPCTreeNode + Trained tree + """ + cur_grads = grads * index_array + cur_hess = hess * index_array + + if self.max_depth - 1 < current_depth: + return self._create_leaf_node(cur_grads, cur_hess) + + sum_grads, sum_hess = snp.reshape(snp.sum(cur_grads), (-1,)), snp.reshape(snp.sum(cur_hess), (-1,)) + if current_depth == 0: + cur_child_samples = np.sum(index_array) + child_samples_flag = np.where(cur_child_samples < self.min_child_samples, np.zeros(1), np.ones(1)) + else: + cur_child_samples = snp.reshape(snp.sum(index_array), (-1,)) + child_samples_flag = snp.where(cur_child_samples < self.min_child_samples, + snp.zeros(cur_child_samples.shape), snp.ones(cur_child_samples.shape)) + + child_weight_flag = snp.where(sum_hess < self.min_child_weight, snp.zeros(sum_hess.shape), + snp.ones(sum_hess.shape)) + + best_feat, best_split_val, best_feat_idx = self._find_best_split( + X, cur_grads * child_weight_flag[0] * child_samples_flag[0], + cur_hess * child_weight_flag[0] * child_samples_flag[0], sum_grads, sum_hess, index_array) + + check_flag = best_feat * child_weight_flag * child_samples_flag + flag_compare_res = check_flag == 0 + + if current_depth == 0: + best_feat = snp.where(flag_compare_res, snp.reshape(self.columns[0], (1,)), best_feat) + best_split_val = snp.where(flag_compare_res, snp.min(X[:, 0]), best_split_val) + best_feat_idx = snp.where(flag_compare_res, snp.zeros(1), best_feat_idx) + else: + best_feat = snp.where(flag_compare_res, last_layer_feat, best_feat) + best_split_val = snp.where(flag_compare_res, last_layer_feat_val, best_split_val) + best_feat_idx = snp.where(flag_compare_res, last_layer_feat_idx, best_feat_idx) + + col_array = np.arange(self.columns.shape[0]) + col_array_matrix = np.tile(col_array, X.shape[0]).reshape(X.shape[0], -1) + get_fidx = snp.where(col_array_matrix == best_feat_idx[0], snp.ones(col_array_matrix.shape), + snp.zeros(col_array_matrix.shape)) + filter_x = X * get_fidx + x = snp.sum(filter_x, axis=1) + x = snp.reshape(x, (-1, 1)) + left_idx = snp.where(x < best_split_val[0], snp.ones(x.shape), snp.zeros(x.shape)) + right_idx = snp.where(x >= best_split_val[0], snp.ones(x.shape), snp.zeros(x.shape)) + left_idx = snp.reshape(left_idx, (-1, 1)) + right_idx = snp.reshape(right_idx, (-1, 1)) + + left_tree = self.build_tree(X, grads, hess, left_idx * index_array, current_depth + 1, best_feat, + best_split_val, best_feat_idx) + right_tree = self.build_tree(X, grads, hess, right_idx * index_array, current_depth + 1, best_feat, + best_split_val, best_feat_idx) + + sub_tree = MPCTreeNode(is_leaf=False, + leaf_weight=None, + split_feat=best_feat, + split_val=best_split_val, + left_child=left_tree, + right_child=right_tree) + + return sub_tree + + def predict_proba(self, tree_node: MPCTreeNode, data: SecureArray, columns: SecureArray, + last_layer_flag: SecureArray): + """ + Predict probabilities. Each piece of data will be compared with all nodes of the tree to ensure that + data information is not leaked. + + Parameters + ---------- + tree_node : MPCTreeNode + Node in tree + + data: SecureArray + Secret sharing of predict data + + columns: array or SecureArray + Columns in data + + last_layer_flag: SecureArray + Check the data is in current node or not + + Returns + ---------- + sub_tre: MPCTreeNode + Trained tree + """ + leaf_values = [] + if tree_node.is_leaf: + weight_flag = snp.where(last_layer_flag == self.max_depth, snp.ones(1), snp.zeros(1)) + leaf_values.append(tree_node.leaf_weight * weight_flag) + return leaf_values + + get_fidx = snp.where(columns == tree_node.split_feat[0], snp.ones(columns.shape), snp.zeros(columns.shape)) + + cur_col_data = data * get_fidx + cur_col_data = snp.sum(cur_col_data, axis=1) + + left_flag = snp.where(cur_col_data < tree_node.split_val, snp.ones(1), snp.zeros(1)) + right_flag = snp.where(cur_col_data >= tree_node.split_val, snp.ones(1), snp.zeros(1)) + leaf_values.extend(self.predict_proba(tree_node.left_child, data, columns, left_flag + last_layer_flag)) + leaf_values.extend(self.predict_proba(tree_node.right_child, data, columns, right_flag + last_layer_flag)) + + return leaf_values diff --git a/petml/fl/boosting/loss.py b/petml/fl/boosting/loss.py new file mode 100644 index 0000000..a6ad1c5 --- /dev/null +++ b/petml/fl/boosting/loss.py @@ -0,0 +1,133 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Union + +import numpy as np +from petace.securenumpy import SecureArray +import petace.securenumpy as snp +import petace.secureml as sml + + +class LogisticLoss: + """ + class for calculate logistic loss function + """ + + def _sigmoid(self, y_pred: np.ndarray): + """ + Implemented sigmoid equation + + Parameters + ---------- + y_pred : array, shape = (n_samples, ) + Predicted labels + + + Returns + ------- + values : float + """ + return 1.0 / (1.0 + np.exp(-y_pred) + 1e-16) + + def grad(self, y_pred: Union[SecureArray, np.ndarray], label: Union[SecureArray, np.ndarray]): + """ + First order derivative of the logistic loss function + + Parameters + ---------- + y_pred : array or SecureArray, shape = (n_samples, 1) + Predicted labels + + label : array-like, shape = (n_samples,1) + Ground truth (correct) labels + + Returns + ------- + values : array or SecureArray, shape = (n_samples, 1) + """ + if isinstance(y_pred, np.ndarray): + y_pred = self._sigmoid(y_pred) + return np.array(y_pred - label).reshape(-1, 1) + + y_pred = sml.sigmoid(y_pred) + return snp.reshape(y_pred - label, (-1, 1)) + + def hess(self, y_pred: Union[SecureArray, np.ndarray]): + """ + Second order derivative of the logistic loss function + + Parameters + ---------- + y_pred : array or SecureArray, shape = (n_samples, 1) + Predicted labels + + Returns + ------- + values : array or SecureArray, shape = (n_samples, 1) + """ + if isinstance(y_pred, np.ndarray): + y_pred = self._sigmoid(y_pred) + return np.array([max(i * (1.0 - i), 1e-16) for i in y_pred]).reshape(-1, 1) + + y_pred = sml.sigmoid(y_pred) + return snp.reshape(y_pred * (snp.ones(y_pred.shape) - y_pred), (-1, 1)) + + +class SquareLoss: + """ + class for calculate square loss function + """ + + def __init__(self): + pass + + def grad(self, y_pred: Union[SecureArray, np.ndarray], label: Union[SecureArray, np.ndarray]): + """ + First order derivative of the square loss function + + Parameters + ---------- + y_pred : array or SecureArray, shape = (n_samples, 1) + Predicted labels + + label : array-like, shape = (n_samples,1) + Ground truth (correct) labels + + Returns + ------- + values : array or SecureArray, shape = (n_samples, 1) + """ + if isinstance(y_pred, np.ndarray): + return np.reshape(y_pred - label, (-1, 1)) + + return y_pred - label + + def hess(self, y_pred: Union[SecureArray, np.ndarray]): + """ + Second order derivative of the square loss function + + Parameters + ---------- + y_pred : array or SecureArray, shape = (n_samples, 1) + Predicted labels + + Returns + ------- + values : array or SecureArray, shape = (n_samples, 1) + """ + if isinstance(y_pred, np.ndarray): + return np.ones_like(y_pred).reshape(-1, 1) + + return snp.ones(y_pred.shape) diff --git a/petml/fl/boosting/metric.py b/petml/fl/boosting/metric.py new file mode 100644 index 0000000..7b817bb --- /dev/null +++ b/petml/fl/boosting/metric.py @@ -0,0 +1,59 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from petace.securenumpy import SecureArray +import petace.securenumpy as snp + + +def error(y_pred: SecureArray, label: SecureArray): + """ + Binary classification error rate. It is calculated as #(wrong cases)/#(all cases). + + Parameters + ---------- + y_pred : SecureArray, shape = (n_samples, 1) + Predicted labels, as returned by a classifier's + predict method. + + label : SecureArray, shape = (n_samples, 1) + Ground truth (correct) labels + + Returns + ------- + error : float + """ + wrong_cond = snp.where(y_pred != label, snp.ones(shape=y_pred.shape), snp.zeros(shape=y_pred.shape)) + return sum(wrong_cond) / len(y_pred) + + +def mean_absolute_error(y_pred: SecureArray, label: SecureArray): + """ + Implementation of mean absolute error. + + Parameters + ---------- + y_pred : SecureArray, shape = (n_samples, 1) + Predicted probabilities, as returned by a classifier's + predict_proba method. + + label : SecureArray, shape = (n_samples, 1) + Ground truth (correct) labels + + Returns + ------- + error : float + """ + + absolut_error = snp.where(y_pred > label, y_pred - label, label - y_pred) + return snp.sum(absolut_error) / len(y_pred) diff --git a/petml/fl/boosting/xgb_model.py b/petml/fl/boosting/xgb_model.py new file mode 100644 index 0000000..d4c4077 --- /dev/null +++ b/petml/fl/boosting/xgb_model.py @@ -0,0 +1,879 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import hashlib +import pickle +import time + +import numpy as np +import pandas as pd +from petace.securenumpy import SecureArray +import petace.securenumpy as snp +from sklearn.model_selection import train_test_split + +from petml.fl.base import FlBase +from .decision_tree import MPCTree +from .loss import LogisticLoss, SquareLoss +from .metric import error, mean_absolute_error + + +class BaseXGB(FlBase): + """Base class for XGBoost""" + + def __init__(self, + min_split_loss: float, + learning_rate: float, + base_score: float, + max_depth: int, + min_child_samples: int, + min_child_weight: float, + reg_alpha: float = 0., + reg_lambda: float = 1.0, + label_name: str = 'label', + test_size: float = 0.): + super().__init__() + self.min_split_loss = min_split_loss + self.learning_rate = learning_rate + self.base_score = base_score + self.max_depth = max_depth + self.reg_alpha = reg_alpha + self.reg_lambda = reg_lambda + self.min_child_samples = min_child_samples + self.min_child_weight = min_child_weight + self.label_name = label_name + self.test_size = test_size + + self.party_id = None + self.peer_party = None + self._federation = None + self._mpc_engine = None + self.do_eval = False + + def set_infra(self, party_id, federation, mpc_engine): + self.party_id = party_id + self.peer_party = 1 - party_id + self._federation = federation + self._mpc_engine = mpc_engine + snp.set_vm(self._mpc_engine.engine) + + @staticmethod + def _set_hash_column(columns): + """ + Transform string column to numerical hashed values + + Parameters + ---------- + columns: array-like + Columns in train dataset + + Returns + ------- + hashed_column: array + Numerical hashed values of columns + """ + hash_set = set() + hashed_column = [] + for col in columns: + hash_value = hashlib.sha256(col.encode()).digest() + hash_float_value = int.from_bytes(hash_value[:8], 'big') % (2**16) + while hash_float_value in hash_set: + hash_float_value = (hash_float_value + 1) % (2**16) + hash_set.add(hash_float_value) + hashed_column.append(hash_float_value) + hashed_column = np.array(hashed_column) + return hashed_column + + def prepare_train_data(self, data): + """ + prepare_train_data + + Parameters + ---------- + data: DataFrame + Train data + + Returns + ------- + train_x_cipher: SecureArray, shape=(n_samples, k) + Secret sharing train data which concat data from two parties + + train_y: array + local true label + + train_y_cipher: SecureArray, shape=(n_samples, ) + Secret sharing data which concat train label from two parties + + eval_x_cipher: SecureArray + Secret sharing eval data which concat data from two parties + + eval_y: SecureArray, shape=(n_samples, ) + Secret sharing data which concat eval label from two parties + + train_column_cipher_party0: SecureArray, shape=(k, ) + Secret sharing data of columns + + hashed_column: array + Numerical hashed values of columns + """ + x = data[[col for col in data.columns if col != self.label_name]] + y = data['label'] + + columns = x.columns.tolist() + hashed_column = self._set_hash_column(columns) + + if self.test_size > 0: + self.do_eval = True + train_x, eval_x, train_y, eval_y = train_test_split(x.values, + y.values, + test_size=self.test_size, + random_state=42) + else: + train_x = x.reset_index(drop='True').values + train_y = y.values + eval_x = None + eval_y = None + + if self.party_id == 0: + train_x_party0 = train_x + train_x_party1 = None + train_y_party0 = train_y.reshape(-1, 1) + train_y_party1 = None + eval_x_party0 = eval_x + eval_x_party1 = None + eval_y_party0 = eval_y + eval_y_party1 = None + train_column_party0 = hashed_column + train_column_party1 = None + + else: + train_x_party0 = None + train_x_party1 = train_x + train_y_party0 = None + train_y_party1 = train_y.reshape(-1, 1) + eval_x_party0 = None + eval_x_party1 = eval_x + eval_y_party0 = None + eval_y_party1 = eval_y + train_column_party0 = None + train_column_party1 = hashed_column + + train_x_cipher_party0 = snp.array(train_x_party0, party=0) + train_x_cipher_party1 = snp.array(train_x_party1, party=1) + train_y_cipher_party0 = snp.array(train_y_party0, party=0) + train_y_cipher_party1 = snp.array(train_y_party1, party=1) + train_column_cipher_party0 = snp.array(train_column_party0, party=0) + train_column_cipher_party1 = snp.array(train_column_party1, party=1) + + column_check = snp.where(train_column_cipher_party0 == train_column_cipher_party1, + snp.ones(train_column_cipher_party0.shape), + snp.zeros(train_column_cipher_party0.shape)) + column_check_lens = snp.sum(column_check) + column_check_lens = column_check_lens.reveal_to(0) + + if column_check_lens.size > 0 and column_check_lens != len(hashed_column): + raise ValueError("Columns in the two parties have different order or values") + + train_x_cipher = snp.vstack((train_x_cipher_party0, train_x_cipher_party1)) + train_y_cipher = snp.vstack((train_y_cipher_party0, train_y_cipher_party1)) + eval_x_cipher = None + eval_y_cipher = None + + if self.do_eval: + eval_x_cipher_party0 = snp.array(eval_x_party0, party=0) + eval_x_cipher_party1 = snp.array(eval_x_party1, party=1) + eval_y_cipher_party0 = snp.array(eval_y_party0, party=0) + eval_y_cipher_party1 = snp.array(eval_y_party1, party=1) + eval_x_cipher = snp.vstack((eval_x_cipher_party0, eval_x_cipher_party1)) + eval_y_cipher = snp.hstack((eval_y_cipher_party0, eval_y_cipher_party1)) + eval_y_cipher = snp.reshape(eval_y_cipher, (-1, 1)) + + return train_x_cipher, train_y, train_y_cipher, eval_x_cipher, \ + eval_y_cipher, train_column_cipher_party0, hashed_column + + def prepare_predict_data(self, data): + """ + + Parameters + ---------- + data: DataFrame + Predict data + + Returns + ------- + predict_x_cipher: SecureArray, shape=(n_samples, k) + Secret sharing train data which concat data from two parties + + hashed_column: array + Numerical hashed values of columns + """ + x = data[[x for x in data.columns if x != self.label_name]] + + columns = x.columns.tolist() + hashed_column = self._set_hash_column(columns) + + if self.party_id == 0: + predict_x_party0 = x.values + predict_x_party1 = None + predict_column_party0 = hashed_column + predict_column_party1 = None + + else: + predict_x_party0 = None + predict_x_party1 = x.values + predict_column_party0 = None + predict_column_party1 = hashed_column + + predict_x_cipher_party0 = snp.array(predict_x_party0, party=0) + predict_x_cipher_party1 = snp.array(predict_x_party1, party=1) + predict_column_cipher_party0 = snp.array(predict_column_party0, party=0) + predict_column_cipher_party1 = snp.array(predict_column_party1, party=1) + predict_x_cipher = snp.vstack((predict_x_cipher_party0, predict_x_cipher_party1)) + + if predict_x_cipher.shape[0] > x.shape[0]: + column_check = snp.where(predict_column_cipher_party0 == predict_column_cipher_party1, + snp.ones(predict_column_cipher_party0.shape), + snp.zeros(predict_column_cipher_party0.shape)) + column_check_lens = snp.sum(column_check) + column_check_lens = column_check_lens.reveal_to(0) + + if column_check_lens.size > 0 and column_check_lens != len(hashed_column): + raise ValueError("Columns in the two parties have different order or values") + + return predict_x_cipher, hashed_column + + def calc_gradient_local(self, loss_func, y): + """ + Calculate first and second order derivative in the first round + + Parameters + ---------- + loss_func: loss class + y: array + Ground true label + + Returns + ------- + grads_cipher: SecureArray, shape=(n_samples, ) + Secret sharing data of first order derivative + + hess_cipher: SecureArray, shape=(n_samples, ) + Secret sharing data of second order derivative + """ + grads = loss_func.grad(np.array([self.base_score] * y.shape[0]), y) + hess = loss_func.hess(np.array([self.base_score] * y.shape[0])) + + if self.party_id == 0: + grads_party0 = grads + grads_party1 = None + hess_party0 = hess + hess_party1 = None + + else: + grads_party0 = None + grads_party1 = grads + hess_party0 = None + hess_party1 = hess + + grads_cipher_party0 = snp.array(grads_party0, party=0) + grads_cipher_party1 = snp.array(grads_party1, party=1) + hess_cipher_party0 = snp.array(hess_party0, party=0) + hess_cipher_party1 = snp.array(hess_party1, party=1) + grads_cipher = snp.vstack((grads_cipher_party0, grads_cipher_party1)) + hess_cipher = snp.vstack((hess_cipher_party0, hess_cipher_party1)) + + return grads_cipher, hess_cipher + + def transform_one_tree(self, train_x, train_y, train_y_cipher, eval_x_cipher, y_hat, eval_y_hat, column_cipher, + hashed_column, loss_func, num_t): + """ + Build one decision tree + + Parameters + ---------- + train_x: SecureArray, shape=(n_samples, k) + Secret sharing train data which concat data from two parties + train_y: array + local true label + train_y_cipher: SecureArray, shape=(n_samples, ) + Secret sharing data which concat train label from two parties + eval_x_cipher: SecureArray + Secret sharing eval data which concat data from two parties + y_hat: SecureArray, shape=(n_samples, ) + predict train y of last epoch + eval_y_hat: SecureArray + predict eval y of last epoch + column_cipher: SecureArray, shape=(k,) + Secret sharing data of columns + hashed_column: array + Numerical hashed values of columns + loss_func: loss class + num_t: int + current epoch + + + Returns + ------- + tree: MPCTree class + Trained tree in current epoch + y_hat: SecureArray + Predict train y in current epoch + eval_y_hat: SecureArray + Predict eval y in current epoch + """ + + index_array = np.ones((train_x.shape[0], 1)) + + if num_t == 0: + grads, hess = self.calc_gradient_local(loss_func, train_y) + else: + grads = loss_func.grad(y_hat, train_y_cipher) + hess = loss_func.hess(y_hat) + + tree = MPCTree(reg_alpha=self.reg_alpha, + reg_lambda=self.reg_lambda, + min_child_weight=self.min_child_weight, + columns=column_cipher, + min_child_samples=self.min_child_samples, + min_split_loss=self.min_split_loss, + max_depth=self.max_depth) + + tree.root = tree.build_tree(train_x, grads, hess, index_array, current_depth=0) + + y_preds = snp.zeros(shape=(train_x.shape[0], 1)) + for i in range(train_x.shape[0]): + leaf_values = tree.predict_proba(tree.root, train_x[i, :], hashed_column, 0) + y_preds[i] = sum(leaf_values) + + y_hat += self.learning_rate * y_preds + + if self.do_eval: + eval_y_preds = snp.zeros(shape=(eval_x_cipher.shape[0], 1)) + for i in range(eval_x_cipher.shape[0]): + leaf_values = tree.predict_proba(tree.root, eval_x_cipher[i, :], hashed_column, 0) + eval_y_preds[i] = sum(leaf_values) + eval_y_hat += self.learning_rate * eval_y_preds + + return tree, y_hat, eval_y_hat + + def save_tree_from_ss_to_numpy(self, trees): + for tree in trees: + tree.columns = tree.columns.to_share().astype(np.int64) + self._save_tree_from_ss_to_numpy(tree.root) + + def _save_tree_from_ss_to_numpy(self, tree_node): + """Convert secure object to numerical value""" + if tree_node.is_leaf: + tree_node.leaf_weight = tree_node.leaf_weight.to_share().astype(np.int64) + return + + tree_node.split_feat = tree_node.split_feat.to_share().astype(np.int64) + tree_node.split_val = tree_node.split_val.to_share().astype(np.int64) + self._save_tree_from_ss_to_numpy(tree_node.left_child) + self._save_tree_from_ss_to_numpy(tree_node.right_child) + + def load_tree_from_numpy_to_ss(self, trees): + for tree in trees: + tree.columns = snp.fromshare(tree.columns, np.float64) + self._load_tree_from_numpy_to_ss(tree.root) + + def _load_tree_from_numpy_to_ss(self, tree_node): + """Convert to secure object from numerical value""" + if tree_node.is_leaf: + tree_node.leaf_weight = snp.fromshare(tree_node.leaf_weight, np.float64) + return + + tree_node.split_feat = snp.fromshare(tree_node.split_feat, np.float64) + tree_node.split_val = snp.fromshare(tree_node.split_val, np.float64) + self._load_tree_from_numpy_to_ss(tree_node.left_child) + self._load_tree_from_numpy_to_ss(tree_node.right_child) + + +class XGBoostClassifier(BaseXGB): + """ + XGBoosting for classification + + Parameters + ---------- + min_split_loss: float, default=1e-5 + The minimum number of gain to split an internal node: + + learning_rate : float, default=0.1 + Learning rate shrinks the contribution of each tree by `learning_rate`. + There is a trade-off between learning_rate and n_estimators. + Values must be in the range `[0.0, inf)`. + + n_estimators : int, default=100 + The number of boosting stages to perform. Gradient boosting + is fairly robust to over-fitting so a large number usually + results in better performance. + Values must be in the range `[1, inf)`. + + base_score : float, default=0.5 + Initial value of y hat + Values must be in the range `[0.0, 1)`. + + max_depth : int or None, default=3 + Maximum depth of a tree. + If int, values must be in the range `[1, inf)`. + + reg_alpha : float, default=0. + L1 regularization term on weights + - values must be in the range `[0.0, inf)`. + + reg_lambda : float, default=1.0 + L2 regularization term on weights + Values must be in the range `[0.0, inf)`. + + min_child_samples : int, default=1 + The minimum number of samples required to be at a leaf node. + A split point at any depth will only be considered if it leaves at + least ``min_samples_leaf`` training samples in each of the left and + right branches. This may have the effect of smoothing the model, + especially in regression. + + Values must be in the range `[1, inf)`. + + min_child_weight : float, default=1.0 + Minimum sum of instance weight (hessian) needed in a child. If the tree partition step results + in a leaf node with the sum of instance weight less than min_child_weight, then the building + process will give up further partitioning. + Values must be in the range `(0, inf)`. + + test_size : float, default=0.0 + Size of eval dataset of input data + Values must be in the range `(0, 1)`. + + eval_epochs : int, default=10 + Calculate the evaluation metric after every certain number of epochs. + Values must be in the range `(1, inf)`. + + eval_threshold : float, default=0.5 + Regard the instances with eval prediction value larger than threshold + as positive instances, and the others as negative instances + + objective : {'logitraw', 'logistic'}, default='logitraw' + The loss function to be optimized. Only support logistic + + """ + + def __init__( + self, + min_split_loss: float = 1e-5, + learning_rate: float = 0.1, + n_estimators: int = 100, + base_score: float = 0.5, + max_depth: int = 3, + reg_alpha: float = 0., + reg_lambda: float = 1.0, + min_child_samples: int = 1, + min_child_weight: float = 1., + label_name: str = 'label', + test_size: float = 0., + eval_epochs: int = 10, + eval_threshold: float = 0.5, + objective: str = 'logitraw', + ): + super().__init__(min_split_loss=min_split_loss, + learning_rate=learning_rate, + base_score=base_score, + max_depth=max_depth, + min_child_samples=min_child_samples, + min_child_weight=min_child_weight, + reg_alpha=reg_alpha, + reg_lambda=reg_lambda, + label_name=label_name, + test_size=test_size) + + self.n_estimators = n_estimators + self.eval_epochs = eval_epochs + self.objective = objective + self.eval_threshold = eval_threshold + self.trees = [] + self.loss_func = LogisticLoss() + + def fit(self, data: pd.DataFrame) -> None: + """ + Fit the model + + Parameters + ---------- + data : DataFrame + Train data + """ + train_x_cipher, train_y, train_y_cipher, eval_x_cipher, eval_y_cipher, column_cipher, \ + hashed_column = self.prepare_train_data(data) + + if self.objective == 'logistic': + self.base_score = 0.0 + self.loss_func = LogisticLoss() + elif self.objective == 'logitraw': + self.loss_func = LogisticLoss() + else: + raise ValueError("Only support logistic loss when apply XGBoostClassifier") + + y_hat = np.full((train_x_cipher.shape[0], 1), self.base_score) + if self.do_eval: + eval_y_hat = np.full((eval_x_cipher.shape[0], 1), self.base_score) + else: + eval_y_hat = None + for num_t in range(self.n_estimators): + start_time = time.time() + self.logger.info(f"fitting tree {num_t + 1}...") + tree, y_hat, eval_y_hat = self.transform_one_tree(train_x_cipher, train_y, train_y_cipher, eval_x_cipher, + y_hat, eval_y_hat, column_cipher, hashed_column, + self.loss_func, num_t) + self.trees.append(tree) + self.logger.info(f"tree {num_t + 1} fit done!, time: {time.time() - start_time}") + + if self.do_eval and (num_t + 1) % self.eval_epochs == 0: + eval_predict_label = snp.where(eval_y_hat > self.eval_threshold, snp.ones(eval_y_hat.shape), + snp.zeros(eval_y_hat.shape)) + errors = error(eval_predict_label, eval_y_cipher) + plain_eval_error0 = errors.reveal_to(0) + plain_eval_error1 = errors.reveal_to(1) + if self.party_id == 0: + self.logger.info(f"eval error in {num_t + 1}: {plain_eval_error0}") + else: + self.logger.info(f"eval error in {num_t + 1}: {plain_eval_error1}") + + self.logger.info("Finished training") + + def predict_proba(self, data: pd.DataFrame) -> SecureArray: + """ + Predict probabilities. + + Parameters + ---------- + data : DataFrame + Predict data + + Returns + ------- + proba : array of shape (n_samples, 1) + Probabilities values + """ + predict_x, hashed_column = self.prepare_predict_data(data) + if len(predict_x) == 0: + raise ValueError('Predict data in empty!') + + y_pred = [self.base_score] * predict_x.shape[0] + + for tree in self.trees: + for i in range(predict_x.shape[0]): + leaf_array = tree.predict_proba(tree.root, predict_x[i, :], hashed_column, 0) + y_pred[i] = y_pred[i] + sum(leaf_array) * self.learning_rate + + return y_pred + + def predict(self, data: pd.DataFrame) -> pd.Series: + """ + Predict label. + + Parameters + ---------- + data : DataFrame + Predict data + + Returns + ---------- + predict_proba: Series + Probabilities values of classifier + """ + predict_proba_cipher = self.predict_proba(data) + if self.party_id == 0: + data_length_in_party0 = len(data) + else: + data_length_in_party0 = len(predict_proba_cipher) - len(data) + + predict_proba0 = [i.reveal_to(0) for i in predict_proba_cipher[:data_length_in_party0]] + predict_proba1 = [i.reveal_to(1) for i in predict_proba_cipher[data_length_in_party0:]] + if self.party_id == 0: + predict_proba = predict_proba0 + else: + predict_proba = predict_proba1 + predict_proba = np.array(predict_proba).reshape(-1,) + + return pd.Series(predict_proba) + + def save_model(self, model_path: str) -> None: + """ + Save trained model. + + Parameters + ---------- + model_path: string + File path of the saved model + """ + self.save_tree_from_ss_to_numpy(self.trees) + try: + self._federation = None + self._mpc_engine = None + with open(model_path, 'wb') as f: + pickle.dump(self, f) + self.logger.info("Save model success") + except pickle.PickleError as e: + self.logger.error(f"Save model file. err={e}") + + def load_model(self, model_path: str) -> None: + """ + Load model. + + Parameters + ---------- + model_path: string + File path of the saved model + + Returns + ------- + loadobj: model + Saved XGboost model + """ + try: + with open(model_path, 'rb') as f: + load_obj = pickle.load(f) + + self.learning_rate = load_obj.learning_rate + self.base_score = load_obj.base_score + self.trees = load_obj.trees + self.load_tree_from_numpy_to_ss(self.trees) + self.logger.info("Load model success") + except pickle.PickleError as e: + self.logger.error(f"Load model fail. err={e}") + + +class XGBoostRegressor(BaseXGB): + """ + XGBoosting for regression + + Parameters + ---------- + min_split_loss: float, default=1e-5 + The minimum number of gain to split an internal node: + + learning_rate : float, default=0.1 + Learning rate shrinks the contribution of each tree by `learning_rate`. + There is a trade-off between learning_rate and n_estimators. + Values must be in the range `[0.0, inf)`. + + n_estimators : int, default=100 + The number of boosting stages to perform. Gradient boosting + is fairly robust to over-fitting so a large number usually + results in better performance. + Values must be in the range `[1, inf)`. + + base_score : float, default=0.5 + Initial value of y hat + Values must be in the range `[0.0, 1)`. + + max_depth : int or None, default=3 + Maximum depth of the Tree. + If int, values must be in the range `[1, inf)`. + + reg_alpha : float, default=0. + L1 regularization term on weights + - values must be in the range `[0.0, inf)`. + + reg_lambda : float, default=1.0 + L2 regularization term on weights + Values must be in the range `[0.0, inf)`. + + min_child_samples : int, default=1 + The minimum number of samples required to be at a leaf node. + A split point at any depth will only be considered if it leaves at + least ``min_samples_leaf`` training samples in each of the left and + right branches. This may have the effect of smoothing the model, + especially in regression. + + Values must be in the range `[1, inf)`. + + min_child_weight : float, default=1.0 + The minimum number of weights required to split an internal node: + Values must be in the range `(0, inf)`. + + test_size : float, default=0.0 + Size of eval dataset of input data + Values must be in the range `(0, 1)`. + + eval_epochs : int, default=10 + Calculate the evaluation metric after every certain number of epochs. + Values must be in the range `(1, inf)`. + + objective : {'squarederror', 'logistic'}, default='squarederror' + The loss function to be optimized. Only support logistic and squarederror + + """ + + def __init__( + self, + min_split_loss: float = 1e-5, + learning_rate: float = 0.1, + n_estimators: int = 100, + base_score: float = 0.5, + max_depth: int = 3, + reg_alpha: float = 0., + reg_lambda: float = 1.0, + min_child_samples: int = 1, + min_child_weight: float = 1, + label_name: str = 'label', + test_size: float = 0., + eval_epochs: int = 0, + objective: str = 'squarederror', + ): + super().__init__(min_split_loss=min_split_loss, + learning_rate=learning_rate, + base_score=base_score, + max_depth=max_depth, + min_child_samples=min_child_samples, + min_child_weight=min_child_weight, + reg_alpha=reg_alpha, + reg_lambda=reg_lambda, + label_name=label_name, + test_size=test_size) + + self.n_estimators = n_estimators + self.eval_epochs = eval_epochs + self.objective = objective + self.trees = [] + self.loss_func = LogisticLoss() + + def fit(self, data: pd.DataFrame) -> None: + """ + Fit the model + + Parameters + ---------- + data : DataFrame + Train data + + """ + train_x_cipher, train_y, train_y_cipher, eval_x_cipher, eval_y_cipher, column_cipher, \ + hashed_column = self.prepare_train_data(data) + + if self.objective == 'logistic': + self.base_score = 0.0 + self.loss_func = LogisticLoss() + elif self.objective == 'squarederror': + self.loss_func = SquareLoss() + else: + raise ValueError("Only support logistic loss and squarederror when apply XGBoostRegressor") + + y_hat = np.full((train_x_cipher.shape[0], 1), self.base_score) + if self.do_eval: + eval_y_hat = np.full((eval_x_cipher.shape[0], 1), self.base_score) + else: + eval_y_hat = None + for num_t in range(self.n_estimators): + start_time = time.time() + self.logger.info(f"fitting tree {num_t + 1}...") + tree, y_hat, eval_y_hat = self.transform_one_tree(train_x_cipher, train_y, train_y_cipher, eval_x_cipher, + y_hat, eval_y_hat, column_cipher, hashed_column, + self.loss_func, num_t) + self.trees.append(tree) + self.logger.info(f"tree {num_t + 1} fit done!, time: {time.time() - start_time}") + + if self.do_eval and (num_t + 1) % self.eval_epochs == 0: + errors = mean_absolute_error(eval_y_hat, eval_y_cipher) + plain_eval_error0 = errors.reveal_to(0) + plain_eval_error1 = errors.reveal_to(1) + if self.party_id == 0: + self.logger.info(f"eval error in {num_t + 1}: {plain_eval_error0}") + else: + self.logger.info(f"eval error in {num_t + 1}: {plain_eval_error1}") + + self.logger.info("Finished training") + + def predict(self, data: pd.DataFrame) -> pd.Series: + """ + Predict label. + + Parameters + ---------- + data : DataFrame + Predict data + + Return + ---------- + predict_values: Series + Predict value of regression + """ + predict_x, hashed_column = self.prepare_predict_data(data) + if len(predict_x) == 0: + raise ValueError('Predict data is empty!') + + y_pred = [self.base_score] * predict_x.shape[0] + + for tree in self.trees: + for i in range(predict_x.shape[0]): + leaf_array = tree.predict_proba(tree.root, predict_x[i, :], hashed_column, 0) + y_pred[i] = y_pred[i] + sum(leaf_array) * self.learning_rate + + if self.party_id == 0: + data_length_in_party0 = len(data) + else: + data_length_in_party0 = len(y_pred) - len(data) + + predict_values0 = [i.reveal_to(0) for i in y_pred[:data_length_in_party0]] + predict_values1 = [i.reveal_to(1) for i in y_pred[data_length_in_party0:]] + + if self.party_id == 0: + predict_values = predict_values0 + else: + predict_values = predict_values1 + predict_values = np.array(predict_values).reshape(-1,) + return pd.Series(predict_values) + + def save_model(self, model_path: str) -> None: + """ + Save trained model. + + Parameters + ---------- + model_path: string + File path of the saved model + """ + self.save_tree_from_ss_to_numpy(self.trees) + try: + self._federation = None + self._mpc_engine = None + with open(model_path, 'wb') as f: + pickle.dump(self, f) + self.logger.info("Save model success") + except pickle.PickleError as e: + self.logger.error(f"Save model file. err={e}") + + def load_model(self, model_path: str) -> None: + """ + Load model. + + Parameters + ---------- + model_path: string + File path of the saved model + + Returns + ------- + loadobj: model + Saved XGboost model + """ + try: + with open(model_path, 'rb') as f: + load_obj = pickle.load(f) + + self.learning_rate = load_obj.learning_rate + self.base_score = load_obj.base_score + self.trees = load_obj.trees + self.load_tree_from_numpy_to_ss(self.trees) + self.logger.info("Load model success") + except pickle.PickleError as e: + self.logger.error(f"Load model fail. err={e}") diff --git a/petml/fl/graph/__init__.py b/petml/fl/graph/__init__.py new file mode 100644 index 0000000..d63907a --- /dev/null +++ b/petml/fl/graph/__init__.py @@ -0,0 +1,19 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .leiden.leiden import Leiden + +__all__ = [ + 'Leiden', +] diff --git a/petml/fl/graph/leiden/__init__.py b/petml/fl/graph/leiden/__init__.py new file mode 100644 index 0000000..4d6166b --- /dev/null +++ b/petml/fl/graph/leiden/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/petml/fl/graph/leiden/base.py b/petml/fl/graph/leiden/base.py new file mode 100644 index 0000000..39cc5f5 --- /dev/null +++ b/petml/fl/graph/leiden/base.py @@ -0,0 +1,172 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" Auxiliary classes for Leiden algorithm. +The Louvain algorithm starts from a singleton partition, with each vertex being assigned to its own community, +and repeatedly moves vertices into new communities with positive modularity gains until the modularity reaches the maximum. + +Terms: +- nodes: The node id belong to origin graph. +- vertex: The node belong to Aggrated Graph, A vertex may contain a set of nodes. +During the iterative process of the algorithm, similar vertexs can gradually combine together to form one larger vertex. +- community: A community may contain a set of Vertex. +""" + +import collections +import copy +import typing as t + + +class Vertex: + """ + Vertex class for Leiden algorithm. + + Parameters + ---------- + vid : int or str + Vertex id. + + cid : int or str + Vommunity id. + + nodes : set + The node id of the original graph it contains, when merged into a vertex, + records which original nodes it is composed of. + + degree : int + Degree of the vertex. + + is_ghost : bool + Whether the vertex is a ghost vertex. Ghost nodes indicate that some information exists + on the opposite node. Ghost nodes exist on both sides simultaneously + + is_cross_merge : bool + Used to represent merged cross-border supernodes: where some nodes are on one side + and some nodes are on the other side. + Due to the fact that cross-border supernodes belong to both the server and client side, + we defines cross-border supernodes to only perform local move judgment on the server side. + However, some neighbors of cross-border supernodes are invisible to the server, so it is necessary + for the client side to calculate the Q values of some neighbor nodes. + """ + + def __init__(self, + vid: t.Union[int, str], + cid: t.Union[int, str] = None, + nodes: t.Set[t.Union[int, str]] = None, + degree: int = 0, + is_ghost: bool = False, + is_cross_merge: bool = False): + + self.vid = vid + if cid is None: + cid = vid + self.cid = cid + self.nodes = nodes or set() + self.degree = degree + self.is_ghost = is_ghost + self.is_cross_merge = is_cross_merge + + def __repr__(self): + return f""" +Vertex( + vid : {self.vid} + cid : {self.cid} + nodes : {self.nodes} + degree : {self.degree} + is_ghost : {self.is_ghost} + is_cross_merge : {self.is_cross_merge} +) +""" + + def merge(self, other: "Vertex"): + """Merge other vertex into self. + """ + self.nodes.update(other.nodes) + self.degree += other.degree + self.is_ghost = self.is_ghost or other.is_ghost + self.is_cross_merge = self.is_cross_merge or other.is_cross_merge + + +class Community: + """ + Community class for Leiden algorithm. + + Parameters + ---------- + cid : int or str + Community id. + + vertexs : set + The vertex it contains. + + weight : float + The weight of the community. + + is_ghost : bool + Whether the community is a ghost community. + """ + + def __init__(self, + cid: t.Union[int, str], + vertexs: t.Set[t.Union[int, str]] = None, + weight: float = 0, + is_ghost: bool = False): + self.cid = cid + self.vertexs = vertexs or set() + self.weight = weight + self.is_ghost = is_ghost + + def __repr__(self): + return f""" +Community( + cid : {self.cid} + vertexs : {self.vertexs} + weight : {self.weight} + is_ghost : {self.is_ghost} +) +""" + + def remove_vertex(self, vid: t.Union[int, str], degree: float): + self.vertexs.remove(vid) + self.weight -= degree + + def add_vertex(self, vid: t.Union[int, str], degree: float): + if vid not in self.vertexs: + self.vertexs.add(vid) + self.weight += degree + + +class Graph: + """ + Graph class for Leiden algorithm. + + We use a dict to store the graph. The key is the node id, and the value is a dict, + the key is the neighbor node id, and the value is the weight. + """ + + def __init__(self) -> None: + self.data = collections.defaultdict(dict) + + def from_dict(self, d: dict): + self.data = copy.deepcopy(d) + + def neighbors(self, node_id: t.Union[int, str]) -> t.Iterable: + return self.data[node_id].keys() + + def weight(self, node_id1: t.Union[int, str], node_id2: t.Union[int, str]) -> float: + if node_id1 is None or node_id2 is None: + return 0 + return self.data[node_id1].get(node_id2, 0) + + def update_weight(self, node_id1: t.Union[int, str], node_id2: t.Union[int, str], weight: float): + self.data[node_id1][node_id2] = weight diff --git a/petml/fl/graph/leiden/leiden.py b/petml/fl/graph/leiden/leiden.py new file mode 100644 index 0000000..33719e4 --- /dev/null +++ b/petml/fl/graph/leiden/leiden.py @@ -0,0 +1,577 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time +import copy +import json +import random + +import numpy as np +import pandas as pd +import petace.securenumpy as snp + +from petml.fl.base import FlBase +from .base import Vertex, Community, Graph +from .utils import Cache, Message + + +class Leiden(FlBase): + """ + Leiden alogrithm. + + Parameters + ---------------- + max_move : int + The limit times of local node move. + + max_merge : int + The limit times of merge meta nodes. + """ + + def __init__(self, max_move: int = 10, max_merge: int = 10): + super().__init__() + self.max_move = max_move + self.max_merge = max_merge + + self.vertex_table: dict = {} + self.community_table: dict = {} + self.local_vertexs: dict = {} + self.G_vertex_vertex = Graph() + self.G_vertex_community = Graph() + self.total_weight: float = 0 + + self.cache = Cache() + self.first_loop = True + self.next_visit_sequence = set() + + def set_infra(self, party_id, federation, mpc_engine): + self.party_id = party_id + self.peer_party = 1 - party_id + self._federation = federation + self._mpc_engine = mpc_engine + + def send(self, state, data=None): + message = Message(state, data) + self._federation.remote(json.dumps(message.dumps()).encode()) + + def recv(self) -> Message: + return Message(**json.loads(self._federation.get().decode())) + + def __init_attr(self, V, G): + self.local_vertexs = V + self.G_vertex_vertex.from_dict(G) + self.G_vertex_community.from_dict(G) + + # init vertex_table + for vid, adj_info in G.items(): + for adj_vid, weight in adj_info.items(): + if adj_vid > vid: + self.total_weight += weight + is_ghost = False + if vid not in V or adj_vid not in V: + is_ghost = True + self.__update_vertex_table(V, vid, weight, is_ghost) + self.__update_vertex_table(V, adj_vid, weight, is_ghost) + + # init community_table + for vid, vertex in self.vertex_table.items(): + self.community_table[vid] = Community(vid, {vid}, vertex.degree, vertex.is_ghost) + + def __update_vertex_table(self, V, vid, weight, is_ghost): + if vid not in self.vertex_table: + vertex = Vertex(vid) + if vid in V: + vertex.nodes = {vid} + self.vertex_table[vid] = vertex + vertex = self.vertex_table[vid] + vertex.degree += weight + vertex.is_ghost = is_ghost or vertex.is_ghost + + def sync_total_weight(self): + self.send(MessageState.SyncTotalWeight, self.total_weight) + recdata = self.recv() + self.total_weight += recdata.data + self.logger.info('sync total_weight successfully') + + def move_nodes(self, vid, new_cid): + """move vertex vid to a new community new_cid.""" + old_cid = self.vertex_table[vid].cid + self.move_nodes_local(vid, old_cid, new_cid) + + # Ghost nodes need to change information on both sides at the same time + if self.vertex_table[vid].is_ghost: + self.send_move_node_info(vid, old_cid, new_cid) + + def move_nodes_local(self, vid, old_cid, new_cid): + """move vertex vid to a new community new_cid locally.""" + if new_cid not in self.community_table: + self.community_table[new_cid] = Community(new_cid, is_ghost=True) + + self.vertex_table[vid].cid = new_cid + self.community_table[old_cid].remove_vertex(vid, self.vertex_table[vid].degree) + self.community_table[new_cid].add_vertex(vid, self.vertex_table[vid].degree) + self.community_table[ + new_cid].is_ghost = self.community_table[new_cid].is_ghost or self.vertex_table[vid].is_ghost + + for neighbor_vid in self.G_vertex_vertex.neighbors(vid): + old_cid_weight = self.G_vertex_community.weight(neighbor_vid, old_cid) - self.G_vertex_vertex.weight( + vid, neighbor_vid) + new_cid_weight = self.G_vertex_community.weight(neighbor_vid, new_cid) + self.G_vertex_vertex.weight( + vid, neighbor_vid) + self.G_vertex_community.update_weight(neighbor_vid, old_cid, old_cid_weight) + self.G_vertex_community.update_weight(neighbor_vid, new_cid, new_cid_weight) + + if neighbor_vid in self.local_vertexs and self.vertex_table[neighbor_vid].cid != new_cid: + self.next_visit_sequence.add(neighbor_vid) + + def send_move_node_info(self, vid, old_cid, new_cid): + self.send(MessageState.SyncMoveNodeInfo, {'vid': vid, 'old_cid': old_cid, 'new_cid': new_cid}) + + def receive_move_node_info(self, info: dict): + vid = info["vid"] + old_cid = info["old_cid"] + new_cid = info["new_cid"] + self.move_nodes_local(vid, old_cid, new_cid) + + def send_MPC_find_max_direction(self, move_flag, max_Q): + self.send(MessageState.MPCFindMaxDirection) + return self.MPC_find_max_direction(True, move_flag, max_Q) + + def receive_MPC_find_max_direction(self, _): + self.MPC_find_max_direction(False) + + def send_move_node(self, vid, max_direction): + self.send(MessageState.MoveNode, {'vid': vid, "max_direction": max_direction}) + recdata = self.recv() + self.receive_move_node_info(recdata) + + def receive_move_node(self, info: dict): + vid = info["vid"] + max_direction = info["max_direction"] + self.move_nodes(vid, max_direction) + + def send_clear_data(self): + self.cache.clear() + self.send(MessageState.ClearCache) + + def receive_clear_data(self, _): + self.cache.clear() + + def sync_MPC_Q_info(self, vid, w_cids): + self.cache.vid = vid + cid = self.vertex_table[vid].cid + self.cache.cid = cid + self.cache.w_cids += w_cids + self.ask_sync_Q_info(vid, cid, w_cids) + + def ask_sync_Q_info(self, vid, cid, w_cids): + m = len(w_cids) + info = {"vid": None, "cid": None, "w_cids": [None for _ in range(m)]} + if self.vertex_table[vid].is_ghost: + info['vid'] = vid + if self.community_table[cid].is_ghost: + info['cid'] = cid + + for i, w_cid in enumerate(w_cids): + if self.community_table[w_cid].is_ghost: + info['w_cids'][i] = w_cid + + self.send(MessageState.SyncQInfo, info) + + def receive_sync_Q_info(self, info: dict): + self.cache.vid = info["vid"] + self.cache.cid = info["cid"] + self.cache.w_cids += info["w_cids"] + + def prepare_Q_data(self, vid, cid, w_cids): + weight_c1 = [] + weight_u_c1 = [] + weight_u_c0 = 0 + weight_c0 = 0 + degree_u = 0 + + if cid is not None: + weight_c0 = self.community_table[cid].weight + if vid is not None: + degree_u = self.vertex_table[vid].degree + if vid is not None and cid is not None: + weight_u_c0 = 2 * self.G_vertex_community.weight(vid, cid) + + for w_cid in w_cids: + weight_c1_tmp = 0 + weight_u_c1_tmp = 0 + if w_cid is not None: + weight_c1_tmp = self.community_table[w_cid].weight + if vid is not None: + weight_u_c1_tmp = 2 * self.G_vertex_community.weight(vid, w_cid) + weight_c1.append(weight_c1_tmp) + weight_u_c1.append(weight_u_c1_tmp) + + # to avoid some petace bug + weight_c1 = np.reshape(weight_c1, (-1, 1)) * 1.0 + weight_u_c1 = np.reshape(weight_u_c1, (-1, 1)) * 1.0 + shape = weight_c1.shape + weight_u_c0 = np.broadcast_to(weight_u_c0, shape) * 1.0 + weight_c0 = np.broadcast_to(weight_c0, shape) * 1.0 + degree_u = np.broadcast_to(degree_u, shape) * 1.0 + + return weight_u_c0, weight_u_c1, weight_c0, weight_c1, degree_u + + def calculate_and_find_maxQ(self, vid, w_cids): + """ + Consider move vertex u from community c0 to community c1, + we use Q to measure the modularity gain: + + Q = weight_c1 - weight_c0 + (degree_u * degree_c0 - degree_u * degree_u - degree_u * degree_c1) / m + + where: + - weight_u_c1: the weight of the edge between vertices u and community c1 + - weight_u_c0: the weight of the edge between vertices u and community c0 + - degree_u: the degree of vertex u + - weight_c0: the sum of the weights of edges incident to two vertices in c0 + - weight_c1: the sum of the weights of edges incident to two vertices in c1 + - m: the sum of all of the edge weights in the graph + """ + flag = True + c0 = self.vertex_table[vid].cid + weight_u_c0, weight_u_c1, weight_c0, weight_c1, degree_u = self.prepare_Q_data(vid, c0, w_cids) + Q = weight_u_c1 - weight_u_c0 + degree_u * (weight_c0 - degree_u - weight_c1) / self.total_weight + max_Q = np.max(Q) + max_direction = w_cids[np.argmax(Q)] + if max_Q < 0 or max_direction is None: + flag = False + max_Q = -1 + max_direction = None + return (flag, max_Q, max_direction) + + def judge_if_move_node(self, vid): + self.send_clear_data() + w_cid_intern = [] + w_cid_MPC = [] + move_flag = False + max_Q = -1 + max_direction = None + viewed_vertex = {self.vertex_table[vid].cid} + for w_vid in self.G_vertex_vertex.neighbors(vid): + w_cid = self.vertex_table[w_vid].cid + if w_cid not in viewed_vertex: + viewed_vertex.add(w_cid) + if self.vertex_table[vid].is_ghost or self.community_table[w_cid].is_ghost: + w_cid_MPC.append(w_cid) + else: + w_cid_intern.append(w_cid) + + if w_cid_intern: + move_flag, max_Q, max_direction = self.calculate_and_find_maxQ(vid, w_cid_intern) + + if w_cid_MPC: + self.sync_MPC_Q_info(vid, w_cid_MPC) + + # For cross-border super nodes, because there may be some neighbor servers that cannot be observed, + # it is necessary to ask clients whether there is a possibility of moving. + if self.vertex_table[vid].is_cross_merge: + self.ask_if_move_merge_node(vid) + + if len(self.cache.w_cids) > 0: + new_max_direction = self.send_MPC_find_max_direction(move_flag, max_Q) + if new_max_direction != -1: + if vid in self.vertex_table and new_max_direction in self.community_table: + self.move_nodes(vid, new_max_direction) + else: + self.send_move_node(vid, new_max_direction) + return True + + if move_flag: + self.move_nodes(vid, max_direction) + return True + return False + + def ask_if_move_merge_node(self, vid): + self.send(MessageState.IfMoveMergeNode, vid) + message = self.recv() + if message.state != MessageState.EndMoveMergeNode: + self.receive_sync_Q_info(message) + # receive end state + self.recv() + + def receive_if_move_merge_node(self, vid): + v_MPC = [] + for w_cid in self.G_vertex_community.neighbors(vid): + if self.vertex_table[vid].is_ghost or self.community_table[w_cid].is_ghost: + v_MPC.append(w_cid) + self.sync_MPC_Q_info(vid, v_MPC) + self.send(MessageState.EndMoveMergeNode) + + def build_visit_sequnce(self): + if self.first_loop: + visit_sequence = list(self.local_vertexs) + self.first_loop = False + else: + visit_sequence = list(self.next_visit_sequence) + self.next_visit_sequence = set() + random.shuffle(visit_sequence) + return visit_sequence + + def first_stage(self): + visit_sequence = self.build_visit_sequnce() + move_flag = False + for v_vid in visit_sequence: + if self.judge_if_move_node(v_vid): + move_flag = True + self.send(MessageState.FirstStageEnd, move_flag) + return move_flag + + def listen_local_move(self): + handler = { + MessageState.SyncQInfo: self.receive_sync_Q_info, + MessageState.IfMoveMergeNode: self.receive_if_move_merge_node, + MessageState.SyncMoveNodeInfo: self.receive_move_node_info, + MessageState.MPCFindMaxDirection: self.receive_MPC_find_max_direction, + MessageState.MoveNode: self.receive_move_node, + MessageState.ClearCache: self.receive_clear_data, + } + while True: + message = self.recv() + state = message.state + if state == MessageState.FirstStageEnd: + return message.data + handler[state](message.data) + continue + + def second_stage(self): + self.merge_vertex() + self.first_loop = True + + def merge_vertex(self): + """merge vertexs belong to the same community + """ + new_community_vertices = {} + new_vertex_vertex = {} + new_local_vertexs = set() + for cid, community in self.community_table.items(): + if not community.vertexs: + continue + new_vertex = Vertex(cid) + is_cross_merge = False + exist_local_vertex = False + exist_no_local_vertex = False + + for vid in community.vertexs: + new_vertex.merge(self.vertex_table[vid]) + if vid not in self.local_vertexs: + exist_no_local_vertex = True + if new_vertex.nodes: + exist_local_vertex = True + + # There are two standards for cross-border vertex: + # 1. One of the sub-vertex is a cross-border vertex + # 2. Contains both its own node and the other node + new_vertex.is_cross_merge = new_vertex.is_cross_merge or (exist_no_local_vertex and exist_local_vertex) + new_vertex.is_cross_merge = is_cross_merge + new_community = Community(cid, {cid}, new_vertex.degree, is_ghost=new_vertex.is_ghost) + new_community_vertices[cid] = new_community + new_vertex_vertex[cid] = new_vertex + + # How to determine the ownership of a node, if one of the following + # conditions exists, the node belongs to you: + # 1. this vertext is not cross_merge + # 2. this vertext is cross_merge and you are server + if (not new_vertex.is_cross_merge and new_vertex.nodes) \ + or (new_vertex.is_cross_merge and self.party_id == 0): + new_local_vertexs.add(cid) + + G = Graph() + for cid1 in new_community_vertices: + for vid in self.community_table[cid1].vertexs: + for cid2 in self.G_vertex_community.neighbors(vid): + if cid2 > cid1 and cid2 in new_community_vertices: + G.update_weight(cid1, cid2, self.G_vertex_community.weight(vid, cid2) + G.weight(cid1, cid2)) + G.update_weight(cid2, cid1, self.G_vertex_community.weight(vid, cid2) + G.weight(cid2, cid1)) + + self.community_table = new_community_vertices + self.vertex_table = new_vertex_vertex + self.G_vertex_vertex = copy.deepcopy(G) + self.G_vertex_community = copy.deepcopy(G) + self.local_vertexs = new_local_vertexs + + def get_partition(self): + partition = [] + for vid, node in self.vertex_table.items(): + for v in node.nodes: + partition.append([v, vid]) + return pd.DataFrame(partition, columns=["user_id", "cluster_id"]) + + def MPC_find_max_direction(self, launch=True, move_flag=None, max_Q=None): + self.logger.debug("Start MPC_find_max_direction") + t1 = time.time() + max_index, max_value = self.MPC_calculate_max_Q(launch) + directions = self.cache.w_cids + new_max_direction = -1 + + if launch: + max_index = int(max_index.flatten()[0]) + max_value = max_value.flatten()[0] + max_val = 0 + if max_value > max_val: + if directions[max_index] is not None: + state = MessageState.MPCEnd + data = None + else: + state = MessageState.QueryDirection + data = max_index + self.send(state, data) + response = self.recv() + + if state == MessageState.QueryDirection: + new_max_direction = response.data + else: + new_max_direction = directions[max_index] + max_val = max_value + else: + self.send(MessageState.MPCEnd) + self.recv() + + if move_flag and max_Q > max_val: + new_max_direction = -1 + else: + message = self.recv() + if message.state == MessageState.MPCEnd: + self.send(MessageState.OK) + else: + max_index = data["data"] + self.send(MessageState.OK, directions[max_index]) + t2 = time.time() + self.logger.debug(f"MPC_find_max_direction time cost: {t2-t1}") + return new_max_direction + + def MPC_calculate_max_Q(self, launch): + vid = self.cache.vid + cid = self.cache.cid + w_cids = self.cache.w_cids + weight_u_c0, weight_u_c1, weight_c0, weight_c1, degree_u = self.prepare_Q_data(vid, cid, w_cids) + self.logger.debug(f"MPC_find_max_direction data shape: {np.shape(weight_u_c0)}") + if launch: + party_id1 = self.party_id + party_id2 = self.peer_party + else: + party_id1 = self.peer_party + party_id2 = self.party_id + + weight_u_c0_priv = snp.array(weight_u_c0, party=party_id1) + snp.array(weight_u_c0, party=party_id2) + weight_u_c1_priv = snp.array(weight_u_c1, party=party_id1) + snp.array(weight_u_c1, party=party_id2) + weight_c0_priv = snp.array(weight_c0, party=party_id1) + snp.array(weight_c0, party=party_id2) + degree_u_priv = snp.array(degree_u, party=party_id1) + snp.array(degree_u, party=party_id2) + weight_c1_priv = snp.array(weight_c1, party=party_id1) + snp.array(weight_c1, party=party_id2) + + Q_rpiv = weight_u_c1_priv - weight_u_c0_priv + degree_u_priv * (weight_c0_priv - degree_u_priv - + weight_c1_priv) * (1. / self.total_weight) + + max_index, max_value = snp.argmax_and_max(Q_rpiv, axis=0) + max_index = max_index.reveal_to(party_id1) + max_value = max_value.reveal_to(party_id1) + return max_index, max_value + + def fit_server(self): + iter_phase_I = 0 + iter_phase_II = 0 + + state = TrainState.state0 + while True: + if state == TrainState.state0: + if iter_phase_I < self.max_move: + self.send(TrainState.state0) + move_flag1 = self.first_stage() + move_flag2 = self.listen_local_move() + self.logger.info(f"phase I: {iter_phase_I}") + if (move_flag1 or move_flag2): + iter_phase_I += 1 + continue + state = TrainState.state1 + + if state == TrainState.state1: + if iter_phase_I > 0 and iter_phase_II < self.max_merge: + self.send(TrainState.state1) + self.second_stage() + self.recv() + iter_phase_I = 0 + iter_phase_II += 1 + self.logger.info(f"phase II: {iter_phase_II}") + state = TrainState.state0 + continue + state = TrainState.state2 + break + + self.send(TrainState.state2) + + def fit_client(self): + while True: + state = self.recv().state + if state == TrainState.state0: + self.listen_local_move() + self.first_stage() + continue + if state == TrainState.state1: + self.second_stage() + self.send(MessageState.OK) + continue + if state == TrainState.state2: + break + + def transform(self, user_weights: dict, local_nodes: set) -> pd.DataFrame: + """ + Fit the model according to the given training data. + + Parameters + ---------- + user_weights: dict. + The weight of user-user weight. + + local_nodes : set. + The local nodes of each party. + + Returns + ------- + cluster: csv file + The result of Leiden algorithm. + """ + snp.set_vm(self._mpc_engine.engine) + self.__init_attr(local_nodes, user_weights) + self.sync_total_weight() + if self.party_id == 0: + self.fit_server() + else: + self.fit_client() + partition = self.get_partition() + return partition + + +class TrainState: + state0 = "launch local move" + state1 = "launch merge Vertex" + state2 = "save partition results" + + +class MessageState: + OK = "ok" + SyncTotalWeight = "sync_total_graph_weight" + SyncMoveNodeInfo = "sync_move_node_info" + MPCFindMaxDirection = "mpc_find_max_direction" + MoveNode = "move_node" + ClearCache = "clear_cache" + SyncQInfo = "sync_q_info" + IfMoveMergeNode = "if_move_merge_node" + EndMoveMergeNode = "end_move_merge_node" + FirstStageEnd = "first_stage_end" + MPCEnd = "mpc_end" + QueryDirection = "query_direction" diff --git a/petml/fl/graph/leiden/utils.py b/petml/fl/graph/leiden/utils.py new file mode 100644 index 0000000..8ec5346 --- /dev/null +++ b/petml/fl/graph/leiden/utils.py @@ -0,0 +1,42 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +class Cache: + """Cache for the information to compute modularity. + """ + + def __init__(self): + self.vid = None + self.cid = None + self.w_cids = [] + + def clear(self): + self.vid = None + self.cid = None + self.w_cids = [] + + +class Message: + """Message for the communication between parties. + """ + + def __init__(self, state: str, data=None): + self.state = state + self.data = data + + def dumps(self): + if self.data is None: + return {"state": self.state} + return {"state": self.state, "data": self.data} diff --git a/petml/fl/preprocessing/__init__.py b/petml/fl/preprocessing/__init__.py new file mode 100644 index 0000000..98f9ba7 --- /dev/null +++ b/petml/fl/preprocessing/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .psi import PSI diff --git a/petml/fl/preprocessing/psi.py b/petml/fl/preprocessing/psi.py new file mode 100644 index 0000000..1b70c36 --- /dev/null +++ b/petml/fl/preprocessing/psi.py @@ -0,0 +1,63 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pandas as pd + +from petml.fl.base import FlBase + + +class PSI(FlBase): + """ + Private Set Intersection (PSI) model. + + Parameters + ---------------- + column_name : str + The column to PSI. + """ + + def __init__(self, column_name: str): + super().__init__() + self.column_name = column_name + + def set_infra(self, psi_engine): + self._psi_engine = psi_engine.engine + + def transform(self, data: pd.DataFrame) -> pd.DataFrame: + """ + Fit the model according to the given training data. + + Parameters + ---------- + data: pd.DataFrame + The data to PSI. + + Returns + ------- + intersection: pd.DataFrame + The intersection of the data. + """ + if data[self.column_name].duplicated().any(): + raise ValueError(f"Duplicated values in column {self.column_name}") + + id_dtype = data[self.column_name].dtype + data[self.column_name] = data[self.column_name].astype(str) # PSI engine only support string type + + intersection = self._psi_engine.process(data[self.column_name].tolist(), obtain_result=True) + self.logger.info(f"Origin data count {len(data)}, Intersection count {len(intersection)}") + + intersection_id = pd.DataFrame(intersection, columns=[self.column_name]) + intersection_df = pd.merge(intersection_id, data, how="left", on=self.column_name) + intersection_df[self.column_name] = intersection_df[self.column_name].astype(id_dtype) + return intersection_df diff --git a/petml/infra/__init__.py b/petml/infra/__init__.py new file mode 100644 index 0000000..4d6166b --- /dev/null +++ b/petml/infra/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/petml/infra/abc/__init__.py b/petml/infra/abc/__init__.py new file mode 100644 index 0000000..79a7833 --- /dev/null +++ b/petml/infra/abc/__init__.py @@ -0,0 +1,17 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .engine import EngineABC +from .network import NetworkABC +from .storage import StorageABC diff --git a/petml/infra/abc/engine.py b/petml/infra/abc/engine.py new file mode 100644 index 0000000..264156d --- /dev/null +++ b/petml/infra/abc/engine.py @@ -0,0 +1,28 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from abc import ABCMeta, abstractmethod + + +class EngineABC(metaclass=ABCMeta): + _type = None + + @abstractmethod + def build(self, **kwargs): + """build the engine based on the engine_config + """ + + @property + def type(self): + return self._type diff --git a/petml/infra/abc/network.py b/petml/infra/abc/network.py new file mode 100644 index 0000000..f435b2b --- /dev/null +++ b/petml/infra/abc/network.py @@ -0,0 +1,63 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from abc import ABC, abstractmethod +from petml.infra.common.log_utils import LoggerFactory + + +class NetworkABC(ABC): + """NetworkABC is the base class. + """ + logger = LoggerFactory.get_logger(__name__) + net = None + + @abstractmethod + def get(self, name: str = None, party: int = None) -> bytes: + """ + Get data from party. + + Parameters + ---------- + party : int + party id + + name : str + name of the data + + Returns + ------- + data : bytes + data from party + + """ + + @abstractmethod + def remote(self, obj: bytes, name: str = None, party: int = None) -> None: + """Send obj to party named name. + + Parameters + ---------- + party : int + party id + + obj : bytes + data to send + + name : str + name of the data + """ + + @abstractmethod + def clear(self): + pass diff --git a/petml/infra/abc/storage.py b/petml/infra/abc/storage.py new file mode 100644 index 0000000..6dc27dc --- /dev/null +++ b/petml/infra/abc/storage.py @@ -0,0 +1,28 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from abc import abstractmethod, ABC + + +class StorageABC(ABC): + + @staticmethod + @abstractmethod + def read(path: str, *args, **kwargs): + pass + + @staticmethod + @abstractmethod + def write(obj, path: str, *args, **kwargs): + pass diff --git a/petml/infra/common/__init__.py b/petml/infra/common/__init__.py new file mode 100644 index 0000000..4d6166b --- /dev/null +++ b/petml/infra/common/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/petml/infra/common/log_utils.py b/petml/infra/common/log_utils.py new file mode 100644 index 0000000..62373f3 --- /dev/null +++ b/petml/infra/common/log_utils.py @@ -0,0 +1,52 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + + +class LoggerFactory: + + @staticmethod + def get_logger(name: str, level: int = logging.INFO, formater: str = None): + """ + Return a logger + + Parameters + ---------- + name : str + name of logger. + + level : {logging.DEBUG, logging.INFO, logging.ERROR, logging.WARNING}, default is LOGGING.DEBUG + log level. + + formater : str + + Returns + ------- + logger + """ + if not formater: + formater = "[%(asctime)s] [%(name)s] [%(levelname)s] [%(module)s.%(funcName)s:%(lineno)d] [pid-thread:%(process)d-%(thread)d]: %(message)s" + logger = logging.getLogger(name) + logger.setLevel(level) + + formatter = logging.Formatter(formater) + + console_handler = logging.StreamHandler() + console_handler.setLevel(level) + console_handler.setFormatter(formatter) + + logger.addHandler(console_handler) + + return logger diff --git a/petml/infra/engine/__init__.py b/petml/infra/engine/__init__.py new file mode 100644 index 0000000..4d6166b --- /dev/null +++ b/petml/infra/engine/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/petml/infra/engine/cipher_engine/__init__.py b/petml/infra/engine/cipher_engine/__init__.py new file mode 100644 index 0000000..a778b61 --- /dev/null +++ b/petml/infra/engine/cipher_engine/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .cipher_engine import CipherEngine diff --git a/petml/infra/engine/cipher_engine/cipher_engine.py b/petml/infra/engine/cipher_engine/cipher_engine.py new file mode 100644 index 0000000..c977fdb --- /dev/null +++ b/petml/infra/engine/cipher_engine/cipher_engine.py @@ -0,0 +1,38 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from petace.duet import VM +from petace.setops.psi import PSI +from petace.setops import PSIScheme + +from petml.infra.abc.network import NetworkABC + + +class CipherEngine: + support_engine = ("mpc", "kkrt_psi", "ecdh_psi") + + def __init__(self, engine_type: str, party_id: int, net: NetworkABC): + if engine_type not in self.support_engine: + raise ValueError(f"engine_type must be in {self.support_engine}") + + if engine_type == "mpc": + self._engine = VM(net.net, party_id) + elif engine_type == "kkrt_psi": + self._engine = PSI(net.net, party_id, PSIScheme.KKRT_PSI) + elif engine_type == "ecdh_psi": + self._engine = PSI(net.net, party_id, PSIScheme.ECDH_PSI) + + @property + def engine(self): + return self._engine diff --git a/petml/infra/network/__init__.py b/petml/infra/network/__init__.py new file mode 100644 index 0000000..f2f0853 --- /dev/null +++ b/petml/infra/network/__init__.py @@ -0,0 +1,16 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .network_factory import NetworkFactory +from .cluster_def import ClusterDef diff --git a/petml/infra/network/cluster_def.py b/petml/infra/network/cluster_def.py new file mode 100644 index 0000000..1672ddf --- /dev/null +++ b/petml/infra/network/cluster_def.py @@ -0,0 +1,46 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +class ClusterDef: + """ + The cluster config of multi parties. + + cluster_def like + { + "mode": "petnet", + "scheme": "socket" + "parties": { + 0 : {"address": ["127.0.0.1:50011"]}, + 1 : {"address": ["127.0.0.1:50012"]}, + } + "topic": "petnet_topic" + } + """ + + def __init__(self, mode: str = None, scheme: str = None, parties: dict = None, topic: str = None): + self.mode = mode + self.scheme = scheme + self.parties = parties + if scheme == "agent" and topic is None: + raise ValueError("topic must be set when scheme is agent") + self.topic = topic + + def __repr__(self): + return f""" + mode : {self.mode} + scheme : {self.scheme} + parties : {self.parties} + tpopic : {self.topic} + """ diff --git a/petml/infra/network/network_factory.py b/petml/infra/network/network_factory.py new file mode 100644 index 0000000..7de5938 --- /dev/null +++ b/petml/infra/network/network_factory.py @@ -0,0 +1,34 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Union + +from petml.infra.network.petnetNetwork.network import PetnetNetwork +from .cluster_def import ClusterDef + +FEDMAP = { + "petnet": PetnetNetwork, +} + + +class NetworkFactory: + + @staticmethod + def get_network(party_id_or_party: Union[int, str], cluster_def: ClusterDef): + if cluster_def is None: + return None + if cluster_def.mode not in FEDMAP: + raise ValueError(f"{cluster_def.mode} not support yet.") + federation = FEDMAP.get(cluster_def.mode)(party_id_or_party, cluster_def) + return federation diff --git a/petml/infra/network/petnetNetwork/__init__.py b/petml/infra/network/petnetNetwork/__init__.py new file mode 100644 index 0000000..4d6166b --- /dev/null +++ b/petml/infra/network/petnetNetwork/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/petml/infra/network/petnetNetwork/network.py b/petml/infra/network/petnetNetwork/network.py new file mode 100644 index 0000000..1b1fe92 --- /dev/null +++ b/petml/infra/network/petnetNetwork/network.py @@ -0,0 +1,88 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import collections +import struct +from typing import Union + +from petace.network import NetParams, NetScheme, NetFactory + +from petml.infra.abc import NetworkABC +from ..cluster_def import ClusterDef + + +class PetnetNetwork(NetworkABC): + """Only supports two parties to synchronously send and receive data + """ + + def __init__(self, party_id_or_party: Union[int, str], cluster_def: ClusterDef): + self.party_id = party_id_or_party + self.cluster_def = cluster_def + self.channel_cache = collections.deque([]) + self.net = None + + if len(cluster_def.parties) != 2: + raise ValueError(f"PetnetNetwork only support 2-party, not {len(cluster_def['parties'])}") + self.__init_net() + + def __init_net(self): + net_params = NetParams() + if self.cluster_def.scheme == "socket": + net_scheme = NetScheme.SOCKET + net_params.local_port = int(self.cluster_def.parties[self.party_id]["address"][0].split(":")[-1]) + net_params.remote_addr, remote_port = self.cluster_def.parties[1 - self.party_id]["address"][0].split(":") + net_params.remote_port = int(remote_port) + elif self.cluster_def.scheme == "agent": + remote_party = None + for party in self.cluster_def.parties: + if party != self.party_id: + remote_party = party + break + if remote_party is None: + raise ValueError("remote party not found") + net_scheme = NetScheme.AGENT + net_params.shared_topic = self.cluster_def.topic + net_params.remote_party = remote_party + net_params.local_agent = self.cluster_def.parties[self.party_id]["address"][0] + + else: + raise ValueError(f"mode {self.cluster_def.mode} not supported") + + self.net = NetFactory.get_instance().build(net_scheme, net_params) + + def clear(self): + pass + + def get(self, name: str = None, party: int = None) -> bytes: + ret = bytearray(4) + self.net.recv_data(ret, 4) + data_length = struct.unpack('!I', bytes(ret))[0] + + ret = bytearray(data_length) + self.net.recv_data(ret, data_length) + self.logger.debug(f"recv {name} successfully with length {data_length}") + return bytes(ret) + + def remote(self, obj: bytes, name=None, party=None): + if not isinstance(obj, bytes): + raise TypeError(f"obj must be bytes, not {type(obj)}") + data_length = len(obj) + if data_length >= 2**32: + raise ValueError(f"obj length must be less than 2**32, not {data_length}") + + length_prefix = struct.pack('!I', data_length) + self.net.send_data(length_prefix, 4) + self.logger.debug(f"send length prefix {length_prefix}") + self.net.send_data(obj, len(obj)) + self.logger.debug(f"send {name} successfully") diff --git a/petml/infra/storage/__init__.py b/petml/infra/storage/__init__.py new file mode 100644 index 0000000..4d6166b --- /dev/null +++ b/petml/infra/storage/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/petml/infra/storage/graph_storage/__init__.py b/petml/infra/storage/graph_storage/__init__.py new file mode 100644 index 0000000..65a992c --- /dev/null +++ b/petml/infra/storage/graph_storage/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .json_storage import JsonStorage diff --git a/petml/infra/storage/graph_storage/json_storage.py b/petml/infra/storage/graph_storage/json_storage.py new file mode 100644 index 0000000..fb89bf7 --- /dev/null +++ b/petml/infra/storage/graph_storage/json_storage.py @@ -0,0 +1,71 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations +import json +import collections +from petml.infra.abc import StorageABC + + +def keystoint(x): + # to support int key + for k in x: + if k.isdigit(): + return {int(k): v for k, v in x.items()} + break + return x + + +class JsonStorage(StorageABC): + + @staticmethod + def read(path: str) -> collections.defaultdict: + """ + read a json file to a dict + + Parameters + ---------- + path : str + path of json file + + Returns + ------- + dict + """ + with open(path, "rb") as f: + data = json.load(f, object_hook=keystoint) + if isinstance(data, list): + return data + G = collections.defaultdict(dict) + G.update(data) + return G + + @staticmethod + def write(obj: list | dict, path: str) -> None: + """ + write a dict to a json file + + Parameters + ---------- + obj : list|dict + dict to write + + path : str + path of json file + """ + if not isinstance(obj, (list, dict)): + raise TypeError(f"GraphStorage only support write list or dict, not {type(obj)}") + + with open(path, "w") as f: + json.dump(obj, f, indent=2) diff --git a/petml/infra/storage/tabular_storage/__init__.py b/petml/infra/storage/tabular_storage/__init__.py new file mode 100644 index 0000000..2d6c11a --- /dev/null +++ b/petml/infra/storage/tabular_storage/__init__.py @@ -0,0 +1,16 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .csv_storage import CsvStorage +from .parquet_storage import ParquetStorage diff --git a/petml/infra/storage/tabular_storage/base.py b/petml/infra/storage/tabular_storage/base.py new file mode 100644 index 0000000..bdef793 --- /dev/null +++ b/petml/infra/storage/tabular_storage/base.py @@ -0,0 +1,32 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pandas as pd +from petml.infra.abc import StorageABC + + +class TabularStorage(StorageABC): + """ + TabularStorage provides functionality to efficiently work with tabular. + + The supported file formats currently are Parquet, Feather / Arrow IPC, CSV and ORC + """ + + @staticmethod + def read(path, *args, **kwargs) -> pd.DataFrame: + pass + + @staticmethod + def write(obj: pd.DataFrame, path: str, *args, **kwargs) -> None: + pass diff --git a/petml/infra/storage/tabular_storage/csv_storage.py b/petml/infra/storage/tabular_storage/csv_storage.py new file mode 100644 index 0000000..9581685 --- /dev/null +++ b/petml/infra/storage/tabular_storage/csv_storage.py @@ -0,0 +1,87 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pandas as pd +from petml.infra.storage.tabular_storage.base import TabularStorage + + +class CsvStorage(TabularStorage): + + @staticmethod + def read(path: str, + sep: str = ',', + header: str = 'infer', + usecols: list = None, + dtype: str = None, + nrows: int = None) -> pd.DataFrame: + """ + read a csv as pd.DataFrame + + Parameters + ---------- + path : str + path of csv file + + sep : str, default ',' + Character or regex pattern to treat as the delimiter. + + header : int, Sequence of int, 'infer' or None, default 'infer' + Row number(s) containing column labels and marking the start of the data (zero-indexed). + + usecols: Sequence of Hashable or Callable, optional + Subset of columns to select, denoted either by column labels or column indices. + + dtype : dtype or dict of {Hashabledtype}, optional + Data type(s) to apply to either the whole dataset or individual columns. + + nrows : int, optional + Number of rows of file to read. Useful for reading pieces of large files. + + Returns + ------- + pandas.DataFrame + """ + return pd.read_csv(path, sep=sep, header=header, usecols=usecols, dtype=dtype, nrows=nrows) + + @staticmethod + def write(obj: pd.DataFrame, + path: str, + columns: list = None, + sep: str = ',', + header: str = True, + index: bool = False) -> None: + """ + write a DataFrame to file_path + + Parameters + ---------- + obj : pandas.DataFrame + obj to be written + + path : str + path of csv file + + sep : str, default ',' + String of length 1. Field delimiter for the output file. + + columns : sequence, optional + Columns to write. + + header : bool or list of str, default True + Write out the column names. If a list of strings is given it is assumed to be aliases for the column names. + + index : bool, default True + Write row names (index). + """ + obj.to_csv(path, columns=columns, sep=sep, header=header, index=index) diff --git a/petml/infra/storage/tabular_storage/parquet_storage.py b/petml/infra/storage/tabular_storage/parquet_storage.py new file mode 100644 index 0000000..608e0df --- /dev/null +++ b/petml/infra/storage/tabular_storage/parquet_storage.py @@ -0,0 +1,60 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pandas as pd +from petml.infra.storage.tabular_storage.base import TabularStorage + + +class ParquetStorage(TabularStorage): + + @staticmethod + def read(path: str, usecols: list = None) -> pd.DataFrame: + """ + Load a parquet object from the file path, returning a DataFrame. + + Parameters + ---------- + path : str + path of csv file + + usecols: list, default=None + If not None, only these columns will be read from the file. + + Returns + ------- + pandas.DataFrame + """ + return pd.read_parquet(path, columns=usecols) + + @staticmethod + def write(obj: pd.DataFrame, path: str, index: bool = None, partition_cols: list = None) -> None: + """ + Write a DataFrame to the binary parquet format. + + Parameters + ---------- + obj : pandas.DataFrame + obj to be written + + path : str + path of csv file + + index : bool, default None + If True, include the dataframe's index(es) in the file output. If False, they will not be written to the file. + + partition_cols : list, optional, default None + Column names by which to partition the dataset. Columns are partitioned in the order they are given. + Must be None if path is not a string. + """ + obj.to_parquet(path, index=index, partition_cols=partition_cols) diff --git a/petml/operators/__init__.py b/petml/operators/__init__.py new file mode 100644 index 0000000..e9fec1f --- /dev/null +++ b/petml/operators/__init__.py @@ -0,0 +1,17 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from . import graph +from . import preprocessing +from . import boosting diff --git a/petml/operators/boosting/__init__.py b/petml/operators/boosting/__init__.py new file mode 100644 index 0000000..f923ff2 --- /dev/null +++ b/petml/operators/boosting/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .xgb_model import XGBoostClassifierFit, XGBoostClassifierPredict, XGBoostRegressorFit, XGBoostRegressorPredict diff --git a/petml/operators/boosting/xgb_model.py b/petml/operators/boosting/xgb_model.py new file mode 100644 index 0000000..9240c35 --- /dev/null +++ b/petml/operators/boosting/xgb_model.py @@ -0,0 +1,321 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pathlib import Path + +from petml.fl.boosting import XGBoostClassifier, XGBoostRegressor +from petml.infra.engine.cipher_engine import CipherEngine +from petml.infra.storage.tabular_storage import CsvStorage +from petml.operators.operator_base import OperatorBase + + +class XGBoostClassifierFit(OperatorBase): + + def _run(self, net, configs: dict) -> bool: + """ + Train a xgboost classification model base on the data. + + Expects the following configmap: + { + "common": { + "objective": "logitraw", + "n_estimators": 100, + "max_depth": 3, + "reg_lambda": 1, + "reg_alpha": 0.0, + "min_child_weight": 0.5, + "base_score": 0.5, + "learning_rate": 0.1, + "network_mode": "petnet", + "network_scheme": "socket", + "label_name": "label", + "test_size": 0.3, + "parties": { + "party_a": { + "address": ["IP_ADDRESS:50011"] + }, + "party_b": { + "address": ["IP_ADDRESS:50012"] + } + } + }, + "party_a": { + "inputs": { + "train_data": "/path/to/data.csv", + }, + "outputs": { + "model_path": "/path/to/model_name.pkl" + } + }, + "party_b": { + "inputs": { + "train_data": "/path/to/data.csv", + }, + "outputs": { + "model_path": "/path/to/model_name.pkl" + } + } + } + """ + min_split_loss = configs.get("min_split_loss", 1e-5) + learning_rate = configs.get("learning_rate", 0.1) + n_estimators = configs.get("n_estimators", 100) + base_score = configs.get("base_score", 0.5) + max_depth = configs.get("max_depth", 3) + reg_alpha = configs.get("reg_alpha", 0.) + reg_lambda = configs.get("reg_lambda", 1.0) + min_child_samples = configs.get("min_child_samples", 1) + min_child_weight = configs.get("min_child_weight", 1) + label_name = configs.get("label_name", "label") + objective = configs.get("objective", "logitraw") + test_size = configs.get("test_size", 0.3) + + # init infra + mpc_engine = CipherEngine("mpc", self.party_id, net) + + # init io + train_data = CsvStorage.read(configs["inputs"]["train_data"]) + model_path = configs["outputs"]["model_path"] + ext = Path(model_path) + if ext.suffix != '.pkl': + raise ValueError('The `model_path` should end with the `.pkl` format.') + + # construct model + model = XGBoostClassifier(min_split_loss, + learning_rate, + n_estimators, + base_score, + max_depth, + reg_alpha, + reg_lambda, + min_child_samples, + min_child_weight, + label_name, + test_size=test_size, + objective=objective) + model.set_infra(self.party_id, net, mpc_engine) + model.fit(train_data) + model.save_model(model_path) + return True + + +class XGBoostClassifierPredict(OperatorBase): + + def _run(self, net, configs: dict) -> bool: + """ + Train a xgboost classification model base on the data. + + Expects the following configmap: + { + "common": { + "network_mode": "petnet", + "network_scheme": "socket", + "parties": { + "party_a": { + "address": ["IP_ADDRESS:50011"] + }, + "party_b": { + "address": ["IP_ADDRESS:50012"] + } + } + }, + "party_a": { + "inputs": { + "predict_data": "/path/to/data.csv", + "model_path": "/path/to/model_name.pkl" + }, + "outputs": { + "inference_res_path": "pathto/predict_proba_value.csv" + } + }, + "party_b": { + "inputs": { + "predict_data": "/path/to/data.csv", + "model_path": "/path/to/model_name.pkl" + }, + "outputs": { + "inference_res_path": "/path/to/predict_proba_value.csv" + } + } + } + """ + # init infra + mpc_engine = CipherEngine("mpc", self.party_id, net) + + # init io + predict_data = CsvStorage.read(configs["inputs"]["predict_data"]) + model_path = configs["inputs"]["model_path"] + ext = Path(model_path) + if ext.suffix != '.pkl': + raise ValueError('The `model_path` should end with the `.pkl` format.') + inference_res_path = configs["outputs"]["inference_res_path"] + + # inference model + model = XGBoostClassifier() + model.set_infra(self.party_id, net, mpc_engine) + model.load_model(model_path) + predict_proba = model.predict(predict_data) + CsvStorage.write(predict_proba, f"{inference_res_path}", index=False) + return True + + +class XGBoostRegressorFit(OperatorBase): + + def _run(self, net, configs: dict) -> bool: + """ + Train a xgboost classification model base on the data. + + Expects the following configmap: + { + "common": { + "objective": "squarederror", + "n_estimators": 100, + "max_depth": 3, + "reg_lambda": 1, + "reg_alpha": 0.0, + "min_child_weight": 1, + "base_score": 0.5, + "learning_rate": 0.1, + "network_mode": "petnet", + "network_scheme": "socket", + "label_name": "label", + "test_size": 0.3, + "parties": { + "party_a": { + "address": ["IP_ADDRESS:50011"] + }, + "party_b": { + "address": ["IP_ADDRESS:50012"] + } + } + }, + "party_a": { + "inputs": { + "train_data": "/path/to/data.csv", + }, + "outputs": { + "model_path": "/path/to/model_name.pkl" + } + }, + "party_b": { + "inputs": { + "train_data": "/path/to/data.csv", + }, + "outputs": { + "model_path": "/path/to/model_name.pkl" + } + } + } + """ + min_split_loss = configs.get("min_split_loss", 1e-5) + learning_rate = configs.get("learning_rate", 0.1) + n_estimators = configs.get("n_estimators", 100) + base_score = configs.get("base_score", 0.5) + max_depth = configs.get("max_depth", 3) + reg_alpha = configs.get("reg_alpha", 0.) + reg_lambda = configs.get("reg_lambda", 1.0) + min_child_samples = configs.get("min_child_samples", 1) + min_child_weight = configs.get("min_child_weight", 1) + label_name = configs.get("label_name", "label") + objective = configs.get("objective", "squarederror") + test_size = configs.get("test_size", 0.3) + + # init infra + mpc_engine = CipherEngine("mpc", self.party_id, net) + + # init io + train_data = CsvStorage.read(configs["inputs"]["train_data"]) + model_path = configs["outputs"]["model_path"] + ext = Path(model_path) + if ext.suffix != '.pkl': + raise ValueError('The `model_path` should end with the `.pkl` format.') + + # construct model + model = XGBoostRegressor(min_split_loss, + learning_rate, + n_estimators, + base_score, + max_depth, + reg_alpha, + reg_lambda, + min_child_samples, + min_child_weight, + label_name, + test_size=test_size, + objective=objective) + model.set_infra(self.party_id, net, mpc_engine) + model.fit(train_data) + model.save_model(model_path) + return True + + +class XGBoostRegressorPredict(OperatorBase): + + def _run(self, net, configs: dict) -> bool: + """ + Train a xgboost classification model base on the data. + + Expects the following configmap: + { + "common": { + "network_mode": "petnet", + "network_scheme": "socket", + "parties": { + "party_a": { + "address": ["IP_ADDRESS:50011"] + }, + "party_b": { + "address": ["IP_ADDRESS:50012"] + } + } + }, + "party_a": { + "inputs": { + "predict_data": "/path/to/data.csv", + "model_path": "/path/to/model_name.pkl" + }, + "outputs": { + "inference_res_path": "/path/to/predict_value.csv" + } + }, + "party_b": { + "inputs": { + "predict_data": "/path/to/data.csv", + "model_path": "/path/to/model_name.pkl" + } + "outputs": { + "inference_res_path": "path/to/predict_value.csv" + } + } + } + """ + # init infra + mpc_engine = CipherEngine("mpc", self.party_id, net) + + # init io + predict_data = CsvStorage.read(configs["inputs"]["predict_data"]) + model_path = configs["inputs"]["model_path"] + ext = Path(model_path) + if ext.suffix != '.pkl': + raise ValueError('The `model_path` should end with the `.pkl` format.') + inference_res_path = configs["outputs"]["inference_res_path"] + + # inference model + model = XGBoostRegressor() + model.set_infra(self.party_id, net, mpc_engine) + model.load_model(model_path) + predict_label = model.predict(predict_data) + + CsvStorage.write(predict_label, f"{inference_res_path}", index=False) + return True diff --git a/petml/operators/graph/__init__.py b/petml/operators/graph/__init__.py new file mode 100644 index 0000000..a304a32 --- /dev/null +++ b/petml/operators/graph/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .leiden import LeidenTransform diff --git a/petml/operators/graph/leiden.py b/petml/operators/graph/leiden.py new file mode 100644 index 0000000..82f396d --- /dev/null +++ b/petml/operators/graph/leiden.py @@ -0,0 +1,79 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from petml.fl.graph import Leiden +from petml.infra.engine.cipher_engine import CipherEngine +from petml.infra.storage.tabular_storage import CsvStorage +from petml.infra.storage.graph_storage import JsonStorage +from petml.operators.operator_base import OperatorBase + + +class LeidenTransform(OperatorBase): + + def _run(self, net, configs: dict) -> bool: + """ + Leiden algorithm. + + Expects the following configmap: + { + "common": { + "max_move": 20, + "max_merge": 10, + "network_mode": "petnet", + "network_scheme": "socket", + "parties": { + "party_a": { + "address": ["IP_ADDRESS:50011"] + }, + "party_b": { + "address": ["IP_ADDRESS:50012"] + } + } + }, + "party_a": { + "inputs": { + "user_weights": "/path/to/data.json", + "local_nodes": "/path/to/data.json", + } + "outputs": { + "cluster": "/path/to/data.csv" + } + }, + "party_b": { + "inputs": { + "user_weights": "/path/to/data.json", + "local_nodes": "/path/to/data.json", + } + "outputs": { + "cluster": "/path/to/data.csv" + } + } + } + """ + mpc_engine = CipherEngine("mpc", self.party_id, net) + + max_move = configs.get("max_move", 20) + max_merge = configs.get("max_move", 10) + + # init io + user_weights = JsonStorage.read(configs["inputs"]["user_weights"]) + local_nodes = JsonStorage.read(configs["inputs"]["local_nodes"]) + + # construct model + model = Leiden(max_move, max_merge) + model.set_infra(self.party_id, net, mpc_engine) + res = model.transform(user_weights, local_nodes) + + CsvStorage.write(res, configs["outputs"]["cluster"], index=False) + return True diff --git a/petml/operators/operator_base.py b/petml/operators/operator_base.py new file mode 100644 index 0000000..5ddf758 --- /dev/null +++ b/petml/operators/operator_base.py @@ -0,0 +1,59 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from copy import deepcopy +from typing import Dict +from abc import ABC, abstractmethod + +from petml.infra.network import ClusterDef, NetworkFactory + + +class OperatorBase(ABC): + + def __init__(self, party: str, **kwargs): + self.party = party + self.party_id = None + + def _trans_parties(self, network_scheme: str, parties: dict): + """ + Transform the parties to a int party_id. + """ + parties_map = dict(zip(sorted(parties.keys()), range(len(parties)))) + self.party_id = parties_map[self.party] + if network_scheme == "agent": + new_parties = parties + else: + new_parties = {parties_map[party]: parties[party] for party in parties} + return new_parties + + def run(self, configmap: Dict, config_manager: "ConfigManager" = None): + # init infra + parties = self._trans_parties(configmap["common"]["network_scheme"], configmap["common"]["parties"]) + cluster_def = ClusterDef(configmap["common"]["network_mode"], configmap["common"]["network_scheme"], parties, + configmap["common"].get("shared_topic")) + if configmap["common"]["network_scheme"] == "agent": + net = NetworkFactory.get_network(self.party, cluster_def) + elif configmap["common"]["network_scheme"] == "socket": + net = NetworkFactory.get_network(self.party_id, cluster_def) + else: + raise ValueError(f"network scheme {configmap['common']['network_scheme']} not supported") + + configs = deepcopy(configmap["common"]) + configs.update(configmap[self.party]) + self._run(net, configs) + return True + + @abstractmethod + def _run(self, net, configs: dict): + pass diff --git a/petml/operators/preprocessing/__init__.py b/petml/operators/preprocessing/__init__.py new file mode 100644 index 0000000..1b4283b --- /dev/null +++ b/petml/operators/preprocessing/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .psi import PSITransform diff --git a/petml/operators/preprocessing/psi.py b/petml/operators/preprocessing/psi.py new file mode 100644 index 0000000..a8a9114 --- /dev/null +++ b/petml/operators/preprocessing/psi.py @@ -0,0 +1,77 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from petml.operators.operator_base import OperatorBase +from petml.fl.preprocessing import PSI +from petml.infra.engine.cipher_engine import CipherEngine +from petml.infra.storage.tabular_storage import CsvStorage + + +class PSITransform(OperatorBase): + + def _run(self, net, configs: dict) -> bool: + """ + Perform PSI transform on the data. + + Expects the following configmap: + { + "common": { + "network_mode": "petnet", + "network_scheme": "socket", + "parties": { + "party_a": { + "address": ["IP_ADDRESS:50011"] + }, + "party_b": { + "address": ["IP_ADDRESS:50012"] + } + } + "protocol": "kkrt" + }, + "party_a": { + "column_name" : "id" + "inputs": { + "data": "/path/to/data.csv" + } + "outputs": { + "data": "/path/to/data.csv" + } + }, + "party_b": { + "column_name" : "id" + "inputs": { + "data": "/path/to/data.csv" + } + "outputs": { + "data": "/path/to/data.csv" + } + } + } + """ + column_name = configs["column_name"] + protocol = configs.get("protocol", "kkrt") + if protocol not in ("ecdh", "kkrt"): + raise ValueError(f"Protocol {protocol} not supported") + psi_engine = CipherEngine(f"{protocol}_psi", self.party_id, net) + + # init io + data = CsvStorage.read(configs["inputs"]["data"]) + + # construct model + model = PSI(column_name) + model.set_infra(psi_engine) + res = model.transform(data) + + CsvStorage.write(res, configs["outputs"]["data"], index=False) + return True diff --git a/petml/version.py b/petml/version.py new file mode 100644 index 0000000..ebc521c --- /dev/null +++ b/petml/version.py @@ -0,0 +1,15 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.1.0" diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..e373743 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,6 @@ +[pytest] +addopts = -s -v --cov=petml/ +testpaths = ./tests +python_files = test*.py +python_classes = Test* +python_functions = test* diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d2a6177 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +numpy~=1.25 +pytest~=7.3.1 +pandas~=2.0.2 +pytest-cov~=4.1.0 +pyarrow>=14.0.1 +scikit-learn>=1.5.0 +petace==0.3.0 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..f291d7f --- /dev/null +++ b/setup.py @@ -0,0 +1,58 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import re +import sys +from setuptools import find_packages, setup + + +def find_version(*filepath): + # Extract version information from filepath + with open(os.path.join('.', *filepath)) as fp: + version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", fp.read(), re.M) + if version_match: + return version_match.group(1) + print("Unable to find version string.") + sys.exit(-1) + + +def read_requirements(): + requirements = [] + _dependency_links = [] + with open('./requirements.txt') as file: + requirements = file.read().splitlines() + for r in requirements: + if r.startswith("--extra-index-url"): + requirements.remove(r) + _dependency_links.append(r) + print("Requirements: ", requirements) + print("Dependency: ", _dependency_links) + return requirements, _dependency_links + + +install_requires, dependency_links = read_requirements() + +setup( + name='petml', + version=find_version("petml", "version.py"), + license='Apache 2.0', + description='privacy preserving machine learning', + author='PrivacyGo-PETPlatform', + author_email='privacygo-petplatform@tiktok.com', + python_requires="==3.9", + packages=find_packages(exclude=('examples', 'examples.*', 'tests', 'tests.*')), + install_requires=install_requires, + dependency_links=dependency_links, +) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/configs.py b/tests/configs.py new file mode 100644 index 0000000..4fed464 --- /dev/null +++ b/tests/configs.py @@ -0,0 +1,18 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from petml.infra.network import ClusterDef + +parties = {0: {'address': ['127.0.0.1:50011'],}, 1: {'address': ['127.0.0.1:50012'],}} +CLUSTER_DEF = ClusterDef(mode="petnet", scheme="socket", parties=parties) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..cfb191d --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,30 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import sys +import shutil +import pytest + +sys.path.append(os.getcwd()) +sys.path.insert(0, "../") + + +@pytest.fixture(scope="session", autouse=True) +def set_tmp_path(): + os.makedirs("./tmp/client", exist_ok=True) + os.makedirs("./tmp/server", exist_ok=True) + yield + if os.path.exists("./tmp"): + shutil.rmtree("./tmp") diff --git a/tests/fl/__init__.py b/tests/fl/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/fl/graph/test_leiden.py b/tests/fl/graph/test_leiden.py new file mode 100644 index 0000000..a258ec1 --- /dev/null +++ b/tests/fl/graph/test_leiden.py @@ -0,0 +1,109 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import collections +import pandas as pd + +from tests.configs import CLUSTER_DEF +from tests.utils import run_multi_process +from petml.fl.graph import Leiden +from petml.infra.storage.graph_storage import JsonStorage +from petml.infra.storage.tabular_storage import CsvStorage +from petml.infra.network.network_factory import NetworkFactory +from petml.infra.engine.cipher_engine import CipherEngine + + +class TestLeiden: + + def run_leiden(self, party_id, cluster_def, user_weights, local_nodes, output_path): + net = NetworkFactory.get_network(party_id, cluster_def) + mpc_engine = CipherEngine("mpc", party_id, net) + model = Leiden(max_move=20, max_merge=10) + model.set_infra( + party_id=party_id, + federation=net, + mpc_engine=mpc_engine, + ) + res = model.transform(user_weights, local_nodes) + CsvStorage.write(res, output_path, index=False) + + def test_leiden(self): + user_weights_a = JsonStorage.read("examples/data/karate_club_user_weight1.json") + user_weights_b = JsonStorage.read("examples/data/karate_club_user_weight2.json") + local_nodes_a = JsonStorage.read("examples/data/karate_club_local_nodes1.json") + local_nodes_b = JsonStorage.read("examples/data/karate_club_local_nodes2.json") + tmpfile_a = "tmp/cluster_a.csv" + tmpfile_b = "tmp/cluster_b.csv" + + expected_modularity = 0.41 + configs = [(0, CLUSTER_DEF, user_weights_a, local_nodes_a, tmpfile_a), + (1, CLUSTER_DEF, user_weights_b, local_nodes_b, tmpfile_b)] + + run_multi_process(self.run_leiden, configs) + g = load_graph("examples/data/karate_club_user_weight1.json", "examples/data/karate_club_user_weight2.json") + partition = get_partition(tmpfile_a, tmpfile_b) + assert modularity(g, partition) > expected_modularity + + +def get_partition(file1, file2): + partitions1 = [] + for file in (file1, file2): + with open(file, "r") as f: + data = f.readlines() + data = data[1:] + for line in data: + vertices = line.strip().split(',') + vid = int(vertices[0]) + cid = vertices[1] + partitions1.append([vid, cid]) + partitions1 = pd.DataFrame(partitions1, columns=["user_id", "label"]) + return partitions1 + + +def load_graph(path1, path2): + g1 = JsonStorage.read(path1) + g2 = JsonStorage.read(path2) + for n1, n1_adj in g2.items(): + for n2, w in n1_adj.items(): + if n1 not in g1[n2]: + g1[n2][n1] = 0 + if n2 not in g1[n1]: + g1[n1][n2] = 0 + g1[n1][n2] += w / 2 + g1[n2][n1] += w / 2 + return g1 + + +def modularity(G, partition): + community = collections.defaultdict(set) + m = 0 + for u, v in partition.values: + community[v].add(u) + for u in G: + for v in G[u]: + m += G[u][v] + m /= 2 + sum_in = 0 + sum_tot = 0 + for com in community.values(): + c_sum_in = 0 + c_sum_tot = 0 + for u in com: + for v in G[u]: + c_sum_tot += G[u][v] + if v in com: + c_sum_in += G[u][v] + sum_in += c_sum_in + sum_tot += c_sum_tot**2 + return sum_in / (2 * m) - sum_tot / (4 * m**2) diff --git a/tests/fl/preprocessing/test_psi.py b/tests/fl/preprocessing/test_psi.py new file mode 100644 index 0000000..3ec0025 --- /dev/null +++ b/tests/fl/preprocessing/test_psi.py @@ -0,0 +1,41 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from tests.configs import CLUSTER_DEF +from tests.utils import run_multi_process +from petml.fl.preprocessing import PSI +from petml.infra.network import NetworkFactory +from petml.infra.engine.cipher_engine import CipherEngine +from petml.infra.storage.tabular_storage import CsvStorage + + +class TestPSI: + + def test_psi(self): + + def run_psi(party_id, column_name, cluster_def, data, plain_res): + net = NetworkFactory.get_network(party_id, cluster_def) + psi_engine = CipherEngine("kkrt_psi", party_id, net) + model = PSI(column_name) + model.set_infra(psi_engine=psi_engine) + res = model.transform(data) + assert set(res[column_name]) == set(plain_res) + + column_name = "id" + data_a = CsvStorage.read("examples/data/breast_hetero_mini_client.csv") + data_b = CsvStorage.read("examples/data/breast_hetero_mini_server.csv") + plain_res = set(data_a["id"]) & set(data_b["id"]) + + configs = [(0, column_name, CLUSTER_DEF, data_a, plain_res), (1, column_name, CLUSTER_DEF, data_b, plain_res)] + run_multi_process(run_psi, configs) diff --git a/tests/infra/__init__.py b/tests/infra/__init__.py new file mode 100644 index 0000000..4d6166b --- /dev/null +++ b/tests/infra/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/infra/common/__init__.py b/tests/infra/common/__init__.py new file mode 100644 index 0000000..4d6166b --- /dev/null +++ b/tests/infra/common/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/infra/common/test_log.py b/tests/infra/common/test_log.py new file mode 100644 index 0000000..a7d83ac --- /dev/null +++ b/tests/infra/common/test_log.py @@ -0,0 +1,25 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from petml.infra.common.log_utils import LoggerFactory + + +class TestLoggerFactory: + + def test_basic(self): + logger = LoggerFactory.get_logger("test") + logger.info("info") + logger.debug("debug") + logger.warning("warning") + logger.error("error") diff --git a/tests/infra/engine/__init__.py b/tests/infra/engine/__init__.py new file mode 100644 index 0000000..4d6166b --- /dev/null +++ b/tests/infra/engine/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/infra/engine/test_cipher_engine.py b/tests/infra/engine/test_cipher_engine.py new file mode 100644 index 0000000..0ef0f1b --- /dev/null +++ b/tests/infra/engine/test_cipher_engine.py @@ -0,0 +1,86 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import multiprocessing +import numpy as np +import numpy.testing as npt +import petace.securenumpy as snp + +from petml.infra.engine.cipher_engine import CipherEngine +from petml.infra.network.network_factory import NetworkFactory +from tests.configs import CLUSTER_DEF + + +class TestCipherEngine: + + @staticmethod + def create_petace_engine(que, data, party_id, cluster_def): + net = NetworkFactory.get_network(party_id, cluster_def) + petace_engine = CipherEngine("mpc", party_id, net).engine + snp.set_vm(petace_engine) + data0 = snp.array(data, 0) + data1 = snp.array(data, 1) + res_add = data0 + data1 + res_mul = data0 * data1 + res_comp = data0 > data1 + res_add_plain = res_add.reveal_to(0) + res_mul_plian = res_mul.reveal_to(0) + res_comp_plain = res_comp.reveal_to(0) + if party_id == 0: + que.put(res_add_plain) + que.put(res_mul_plian) + que.put(res_comp_plain) + + @staticmethod + def create_psi_engine(que, data, party_id, cluster_def, protocol): + net = NetworkFactory.get_network(party_id, cluster_def) + psi_engine = CipherEngine(protocol, party_id, net).engine + intersection = psi_engine.process(data, True) + que.put(intersection) + + @staticmethod + def run_process(target, args0, args1): + p0 = multiprocessing.Process(target=target, args=args0) + p1 = multiprocessing.Process(target=target, args=args1) + p0.start() + p1.start() + p0.join() + p1.join() + + def test_petace(self): + que = multiprocessing.Queue() + data0 = np.arange(10).astype(np.float64).reshape((1, -1)) + data1 = np.arange(5, 15).astype(np.float64).reshape((1, -1)) + cluster_def = CLUSTER_DEF + args0 = (que, data0, 0, cluster_def) + args1 = (que, data1, 1, cluster_def) + self.run_process(self.create_petace_engine, args0, args1) + result = que.get() + npt.assert_equal(result, data0 + data1) + result = que.get() + npt.assert_almost_equal(result, data0 * data1, decimal=4) + result = que.get() + npt.assert_equal(result, data0 > data1) + + def test_psi(self): + for protocol in ("ecdh_psi", "kkrt_psi"): + que = multiprocessing.Queue() + data0 = [f"{i}" for i in range(10)] + data1 = [f"{i}" for i in range(5, 15)] + cluster_def = CLUSTER_DEF + args0 = (que, data0, 0, cluster_def, protocol) + args1 = (que, data1, 1, cluster_def, protocol) + self.run_process(self.create_psi_engine, args0, args1) + result = que.get() + assert (set(data0) & set(data1)) == set(result) diff --git a/tests/infra/network/__init__.py b/tests/infra/network/__init__.py new file mode 100644 index 0000000..4d6166b --- /dev/null +++ b/tests/infra/network/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/infra/network/test_petnet.py b/tests/infra/network/test_petnet.py new file mode 100644 index 0000000..08d2db0 --- /dev/null +++ b/tests/infra/network/test_petnet.py @@ -0,0 +1,46 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy.testing as npt + +from petml.infra.network import NetworkFactory +from tests.process import Process +from tests.configs import CLUSTER_DEF + + +class TestSocketFederation: + + def sync_message(self, party): + message = b"hello world" + F = NetworkFactory.get_network(party, CLUSTER_DEF) + if party == 0: + F.remote(message) + else: + res = F.get() + npt.assert_equal(res, message) + + F.clear() + + def test_basic(self): + p0 = Process(target=self.sync_message, args=(0,)) + p1 = Process(target=self.sync_message, args=(1,)) + p0.start() + p1.start() + p0.join() + p1.join() + for p in (p0, p1): + if p.exception: + error, _ = p.exception + p.terminate() + raise AssertionError(error) diff --git a/tests/infra/storage/__init__.py b/tests/infra/storage/__init__.py new file mode 100644 index 0000000..4d6166b --- /dev/null +++ b/tests/infra/storage/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/infra/storage/test_tabular.py b/tests/infra/storage/test_tabular.py new file mode 100644 index 0000000..8601286 --- /dev/null +++ b/tests/infra/storage/test_tabular.py @@ -0,0 +1,33 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from petml.infra.storage.tabular_storage import CsvStorage, ParquetStorage + + +class TestCsv: + + def test_read_write(self, tmp_path): + path = "examples/data/breast_hetero_mini_client.csv" + df = CsvStorage.read(path) + save_path = str(tmp_path / "res1.csv") + CsvStorage.write(df, save_path) + + +class TestParquet: + + def test_read_write(self, tmp_path): + path = "examples/data/breast_hetero_mini_client.parquet" + df = ParquetStorage.read(path) + save_path = str(tmp_path / "res1.parquet") + ParquetStorage.write(df, save_path) diff --git a/tests/operators/__init__.py b/tests/operators/__init__.py new file mode 100644 index 0000000..4d6166b --- /dev/null +++ b/tests/operators/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/operators/boosting/__init__.py b/tests/operators/boosting/__init__.py new file mode 100644 index 0000000..4d6166b --- /dev/null +++ b/tests/operators/boosting/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/operators/boosting/test_xgb.py b/tests/operators/boosting/test_xgb.py new file mode 100644 index 0000000..d519d40 --- /dev/null +++ b/tests/operators/boosting/test_xgb.py @@ -0,0 +1,217 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import pandas as pd +from sklearn.metrics import accuracy_score, mean_squared_error + +from petml.operators.boosting import XGBoostClassifierFit, XGBoostClassifierPredict, XGBoostRegressorFit, \ + XGBoostRegressorPredict +from tests.utils import run_multi_process + + +class TestXGBoostClassifier: + + def _run_fit_classifier(self, party, config_map): + operator = XGBoostClassifierFit(party) + operator.run(config_map) + + def _run_predict_classifier(self, party, config_map): + operator = XGBoostClassifierPredict(party) + operator.run(config_map) + + def test_xgb_classifier(self): + fit_configmap = { + "common": { + "objective:": "logitraw", + "n_estimators": 1, + "max_depth": 2, + "reg_lambda": 1, + "reg_alpha": 0.0, + "base_score": 0.5, + "learning_rate": 0.1, + "min_child_weight": 0.1, + "network_mode": "petnet", + "network_scheme": "socket", + "label_name": "label", + "test_size": 0., + "parties": { + "party_a": { + "address": ["127.0.0.1:50011"] + }, + "party_b": { + "address": ["127.0.0.1:50012"] + } + } + }, + "party_a": { + "inputs": { + "train_data": "examples/data/iris_binary_mini_server.csv", + }, + "outputs": { + "model_path": "tmp/test_binary_xgb_server.pkl" + } + }, + "party_b": { + "inputs": { + "train_data": "examples/data/iris_binary_mini_client.csv", + }, + "outputs": { + "model_path": "tmp/test_binary_xgb_client.pkl" + } + } + } + predict_configmap = { + "common": { + "network_mode": "petnet", + "network_scheme": "socket", + "parties": { + "party_a": { + "address": ["127.0.0.1:50011"] + }, + "party_b": { + "address": ["127.0.0.1:50012"] + } + } + }, + "party_a": { + "inputs": { + "predict_data": "examples/data/iris_binary_mini_server.csv", + "model_path": "tmp/test_binary_xgb_server.pkl" + }, + "outputs": { + "inference_res_path": "tmp/test_binary_predict_server.csv" + } + }, + "party_b": { + "inputs": { + "predict_data": "examples/data/iris_binary_mini_client.csv", + "model_path": "tmp/test_binary_xgb_client.pkl" + }, + "outputs": { + "inference_res_path": "tmp/test_binary_predict_client.csv" + } + } + } + data1 = pd.read_csv(predict_configmap['party_a']['inputs']['predict_data']) + data2 = pd.read_csv(predict_configmap['party_b']['inputs']['predict_data']) + true_label = pd.concat([data1, data2], axis=0)['label'].values + run_multi_process(self._run_fit_classifier, [("party_a", fit_configmap), ("party_b", fit_configmap)]) + run_multi_process(self._run_predict_classifier, [("party_a", predict_configmap), + ("party_b", predict_configmap)]) + server_result_save_path = predict_configmap["party_a"]["outputs"]["inference_res_path"] + client_result_save_path = predict_configmap["party_b"]["outputs"]["inference_res_path"] + server_y_pred = pd.read_csv(f"{server_result_save_path}").values + client_y_pred = pd.read_csv(f"{client_result_save_path}").values + y_pred = np.vstack((server_y_pred, client_y_pred)) + y_pred = np.where(y_pred >= 0.5, 1, 0) + acc_value = accuracy_score(true_label, y_pred) + assert abs(acc_value - 1) < 0.01 + + +class TestXGBoostRegressor: + + def _run_fit_regressor(self, party, config_map): + operator = XGBoostRegressorFit(party) + operator.run(config_map) + + def _run_predict_regressor(self, party, config_map): + operator = XGBoostRegressorPredict(party) + operator.run(config_map) + + def test_xgb_regressor(self): + fit_configmap = { + "common": { + "objective:": "squarederror", + "n_estimators": 1, + "max_depth": 2, + "reg_lambda": 1, + "reg_alpha": 0.0, + "base_score": 0.5, + "learning_rate": 0.1, + "min_child_weight": 1, + "network_mode": "petnet", + "network_scheme": "socket", + "label_name": "label", + "test_size": 0., + "parties": { + "party_a": { + "address": ["127.0.0.1:50011"] + }, + "party_b": { + "address": ["127.0.0.1:50012"] + } + } + }, + "party_a": { + "inputs": { + "train_data": "examples/data/students_reg_mini_server.csv", + }, + "outputs": { + "model_path": "tmp/test_reg_xgb_server.pkl" + } + }, + "party_b": { + "inputs": { + "train_data": "examples/data/students_reg_mini_client.csv", + }, + "outputs": { + "model_path": "tmp/test_reg_xgb_client.pkl" + } + } + } + predict_configmap = { + "common": { + "network_mode": "petnet", + "network_scheme": "socket", + "parties": { + "party_a": { + "address": ["127.0.0.1:50011"] + }, + "party_b": { + "address": ["127.0.0.1:50012"] + } + } + }, + "party_a": { + "inputs": { + "predict_data": "examples/data/students_reg_mini_server.csv", + "model_path": "tmp/test_reg_xgb_server.pkl" + }, + "outputs": { + "inference_res_path": "tmp/test_reg_predict_server.csv" + } + }, + "party_b": { + "inputs": { + "predict_data": "examples/data/students_reg_mini_client.csv", + "model_path": "tmp/test_reg_xgb_client.pkl" + }, + "outputs": { + "inference_res_path": "tmp/test_reg_predict_client.csv" + } + } + } + data1 = pd.read_csv(predict_configmap['party_a']['inputs']['predict_data']) + data2 = pd.read_csv(predict_configmap['party_b']['inputs']['predict_data']) + true_label = pd.concat([data1, data2], axis=0)['label'].values + run_multi_process(self._run_fit_regressor, [("party_a", fit_configmap), ("party_b", fit_configmap)]) + run_multi_process(self._run_predict_regressor, [("party_a", predict_configmap), ("party_b", predict_configmap)]) + server_result_save_path = predict_configmap["party_a"]["outputs"]["inference_res_path"] + client_result_save_path = predict_configmap["party_b"]["outputs"]["inference_res_path"] + server_y_pred = pd.read_csv(f"{server_result_save_path}").values + client_y_pred = pd.read_csv(f"{client_result_save_path}").values + y_pred = np.vstack((server_y_pred, client_y_pred)) + mse_error_value = mean_squared_error(true_label, y_pred) + assert abs(mse_error_value - 122.04) < 10 diff --git a/tests/operators/graph/__init__.py b/tests/operators/graph/__init__.py new file mode 100644 index 0000000..4d6166b --- /dev/null +++ b/tests/operators/graph/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/operators/graph/test_leiden.py b/tests/operators/graph/test_leiden.py new file mode 100644 index 0000000..6f7da3e --- /dev/null +++ b/tests/operators/graph/test_leiden.py @@ -0,0 +1,63 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import petml + +from tests.utils import run_multi_process + +config_map = { + "common": { + "max_move": 20, + "max_merge": 10, + "network_mode": "petnet", + "network_scheme": "socket", + "parties": { + "party_a": { + "address": ["127.0.0.1:50011"] + }, + "party_b": { + "address": ["127.0.0.1:50012"] + } + } + }, + "party_a": { + "inputs": { + "user_weights": "examples/data/karate_club_user_weight1.json", + "local_nodes": "examples/data/karate_club_local_nodes1.json", + }, + "outputs": { + "cluster": "tmp/server/cluster.csv" + } + }, + "party_b": { + "inputs": { + "user_weights": "examples/data/karate_club_user_weight2.json", + "local_nodes": "examples/data/karate_club_local_nodes2.json", + }, + "outputs": { + "cluster": "tmp/client/cluster.csv" + } + } +} + + +class TestLeidenTransform: + + def test_leiden_transform(self): + + def run_leiden(party, config_map): + operator = petml.operators.graph.LeidenTransform(party) + operator.run(config_map) + + run_multi_process(run_leiden, [("party_a", config_map), ("party_b", config_map)]) diff --git a/tests/operators/preprocessing/__init__.py b/tests/operators/preprocessing/__init__.py new file mode 100644 index 0000000..4d6166b --- /dev/null +++ b/tests/operators/preprocessing/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/operators/preprocessing/test_psi.py b/tests/operators/preprocessing/test_psi.py new file mode 100644 index 0000000..674c562 --- /dev/null +++ b/tests/operators/preprocessing/test_psi.py @@ -0,0 +1,61 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import petml + +from tests.utils import run_multi_process + +config_map = { + "common": { + "network_mode": "petnet", + "network_scheme": "socket", + "parties": { + "party_a": { + "address": ["127.0.0.1:50011"] + }, + "party_b": { + "address": ["127.0.0.1:50012"] + } + } + }, + "party_a": { + "column_name": "id", + "inputs": { + "data": "examples/data/breast_hetero_mini_server.csv", + }, + "outputs": { + "data": "tmp/server/data.csv" + } + }, + "party_b": { + "column_name": "id", + "inputs": { + "data": "examples/data/breast_hetero_mini_client.csv", + }, + "outputs": { + "data": "tmp/client/data.csv" + } + } +} + + +class TestPSITransform: + + def test_psi_transform(self): + + def run_leiden(party, config_map): + operator = petml.operators.preprocessing.PSITransform(party) + operator.run(config_map) + + run_multi_process(run_leiden, [("party_a", config_map), ("party_b", config_map)]) diff --git a/tests/process.py b/tests/process.py new file mode 100644 index 0000000..8ebb309 --- /dev/null +++ b/tests/process.py @@ -0,0 +1,38 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import multiprocessing +import traceback + + +class Process(multiprocessing.Process): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._pconn, self._cconn = multiprocessing.Pipe() + self._exception = None + + def run(self): + try: + multiprocessing.Process.run(self) + self._cconn.send(None) + except Exception as e: + tb = traceback.format_exc() + self._cconn.send((e, tb)) + + @property + def exception(self): + if self._pconn.poll(): + self._exception = self._pconn.recv() + return self._exception diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..91281b4 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,40 @@ +# Copyright 2024 TikTok Pte. Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from tests.process import Process + + +class FLTestException(Exception): + + def __init__(self, message) -> None: + self.message = message + + def __str__(self): + return f"test failed: {self.message}" + + +def run_multi_process(func, args_list): + process_list = [] + for args in args_list: + p = Process(target=func, args=args) + p.start() + process_list.append(p) + for p in process_list: + p.join() + + for p in process_list: + if p.exception: + error, _ = p.exception + p.terminate() + raise FLTestException(error)