├── .flake8 ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── conftest.py ├── examples ├── Part 01 - The Basics of Private Deep Learning.ipynb └── Part 02 - Intro to Private Training with Remote Execution.ipynb ├── pyproject.toml ├── requirements.txt ├── requirements_dev.txt ├── setup.py └── syft_tensorflow ├── __init__.py ├── attributes ├── __init__.py └── attributes.py ├── hook ├── __init__.py ├── hook.py └── hook_args.py ├── keras └── tf_keras_fun_test.py ├── serde ├── __init__.py ├── serde.py └── serde_test.py └── syft_types ├── __init__.py ├── keras_layer.py ├── keras_layers_test.py ├── keras_model.py ├── keras_model_test.py ├── keras_object.py ├── tensor.py ├── tensor_test.py ├── variable.py └── variable_test.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | select = E901,E999,F821,F822,F823 3 | max-line-length = 100 4 | exclude = .git,.eggs,__pycache__,build,dist,venv 5 | max-complexity = 11 6 | show-source = true 7 | statistics = true 8 | count = true 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | group: travis_latest 2 | language: python 3 | cache: pip 4 | python: 5 | - 3.6 6 | matrix: 7 | allow_failures: 8 | - python: nightly 9 | - python: pypy 10 | - python: pypy3 11 | install: 12 | - sudo apt-get update 13 | - hash -r 14 | - pip3 install git+git://github.com/dropoutlabs/PySyft.git@dev 15 | - pip3 uninstall -y tf-encrypted 16 | - pip3 install -r requirements.txt >build.log 17 | - pip3 install -r requirements_dev.txt >build.log 18 | - pip3 install flake8 19 | - pip3 install flake8-comprehensions 20 | - pip3 install pep8-naming 21 | before_script: 22 | # stop the build if there are Python syntax errors or undefined 23 | # names 24 | - flake8 --config=.flake8 . 25 | # exit-zero treats all errors as warnings. 26 | # diverting from the standard 79 character line length in 27 | # accordance with this: 28 | # https://www.python.org/dev/peps/pep-0008/#a-foolish-consistency-is-the-hobgoblin-of-little-minds 29 | - flake8 . --count --exit-zero --statistics --select=E,F,W,C90 30 | script: 31 | - coverage run -m pytest 32 | - coverage report --fail-under 0 -m 33 | notifications: 34 | on_success: change 35 | # `always` will be the setting below once code changes slow down 36 | on_failure: change 37 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributors Guidelines to PySyft-TensorFlow 2 | 3 | ## Getting Started 4 | 5 | ### Slack 6 | 7 | A great first place to join the Community is the [Slack channel](https://slack.openmined.org). 8 | 9 | ### Issues 10 | 11 | The open issues, including good first issues, can be found [here](https://github.com/openmined/pysyft-tensorflow/issues). This repository works with issues the same way as the [PySyft](https://github.com/OpenMined/PySyft/blob/master/CONTRIBUTING.md#issue-allocation) repository. 12 | 13 | ## Setup 14 | 15 | ### Forking a Repository 16 | 17 | To contribute, you will need to send your pull requests via a [fork](https://help.github.com/en/articles/fork-a-repo). 18 | 19 | ### Keeping your fork up to date 20 | 21 | If you continue to contribute, you will need to keep your fork up to date. 22 | See this [guide](https://help.github.com/articles/syncing-a-fork/) for instructions 23 | detailing how to sync your fork. 24 | 25 | ### Environment 26 | 27 | We recommend creating a [Conda](https://docs.conda.io/en/latest/) environment for PySyft-TensorFlow. 28 | 29 | ``` 30 | $ conda create -n pysyft-tf python=3.7 31 | $ source activate pysyft-tf 32 | ``` 33 | 34 | ### Installing Dependencies 35 | 36 | After forking the repository and cloning it, you can install the required dependencies 37 | 38 | ``` 39 | pip install -r requirements.txt 40 | pip install -r requirements_dev.txt 41 | ``` 42 | 43 | ### Running Tests 44 | 45 | We use [pytest](https://docs.pytest.org/en/latest/) to run tests. 46 | 47 | ``` 48 | $ pytest # run everything 49 | $ pytest -k "test_fn_name_here" # run a specific failing test 50 | ``` 51 | 52 | ## Documentation & Codestyle 53 | 54 | This repository follows the same rules as [PySyft](https://github.com/OpenMined/PySyft/blob/master/CONTRIBUTING.md#documentation-and-codestyle). 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATION NOTICE 2 | 3 | Warning, this repository will soon be deprecated in favor of [PySyft](https://github.com/OpenMined/PySyft). 4 | 5 | # PySyft-TensorFlow 6 | TensorFlow bindings for [PySyft](https://github.com/openmined/pysyft). 7 | 8 | PySyft is a Python framework for secure, private deep learning. PySyft-TensorFlow brings 9 | secure, private deep learning to [TensorFlow](https://tensorflow.org). 10 | 11 | [![Build Status](https://travis-ci.org/OpenMined/PySyft-TensorFlow.svg?branch=master)](https://travis-ci.org/OpenMined/PySyft-TensorFlow) 12 | [![Chat on Slack](https://img.shields.io/badge/chat-on%20slack-7A5979.svg)](https://openmined.slack.com/messages/team_pysyft) 13 | 14 | ## Installation 15 | 16 | PySyft-TensorFlow is available on pip 17 | 18 | ``` 19 | pip install syft-tensorflow 20 | ``` 21 | 22 | NOTE: We aren't yet on a proper release schedule. Until then, we recommend building the code from source. The master branch is intended to be kept in line with [this branch](https://github.com/dropoutlabs/PySyft/tree/dev) on the [DropoutLabs](https://github.com/dropoutlabs/PySyft) fork of PySyft. If you have any trouble, please open an issue or reach out on Slack via the #team_tensorflow or #team_pysyft channels. 23 | 24 | ## Usage 25 | 26 | See the [PySyft tutorials](https://github.com/OpenMined/PySyft/tree/master/examples/tutorials) 27 | if you are unfamiliar with any Syft paradigms. 28 | 29 | ```python 30 | import tensorflow as tf 31 | import syft 32 | 33 | hook = sy.TensorFlowHook(tf) 34 | # Simulates a remote worker (ie another computer) 35 | remote = sy.VirtualWorker(hook, id="remote") 36 | 37 | # Send data to the other worker 38 | x = tf.constant(5).send(remote) 39 | y = tf.constant(10).send(remote) 40 | 41 | z = x * y 42 | 43 | print(z.get()) 44 | # => 50 45 | ``` 46 | 47 | ## Developing PySyft-TensorFlow 48 | 49 | See [CONTRIBUTING](./CONTRIBUTING.md). 50 | 51 | ## Project Support 52 | 53 | PySyft-Tensorflow was contributed by and continues to be maintained by the team at [Dropout Labs](https://dropoutlabs.com). 54 | 55 | Please reach out to contact@dropoutlabs.com for support. 56 | 57 | [Dropout Labs](https://dropoutlabs.com/) 58 | |--------------------------------------------------------------| 59 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | """Config for tests""" 2 | import pytest 3 | import tensorflow as tf 4 | import syft as sy 5 | 6 | 7 | @pytest.fixture(scope="session", autouse=True) 8 | def hook(): 9 | """ 10 | auto-hook tensorflow 11 | """ 12 | return sy.TensorFlowHook(tf) 13 | 14 | 15 | @pytest.fixture(scope="function", autouse=True) 16 | def remote(hook): # pylint: disable=W0621 17 | """ 18 | Provide a remote worker to tests 19 | """ 20 | return sy.VirtualWorker(hook, id="remote") 21 | -------------------------------------------------------------------------------- /examples/Part 01 - The Basics of Private Deep Learning.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Tutorial: The Basic Tools of Private Deep Learning -- TensorFlow edition!\n", 8 | "\n", 9 | "Welcome to PySyft's introductory tutorial for privacy preserving, decentralized deep learning. This series of notebooks is a step-by-step guide for you to get to know the new tools and techniques required for doing deep learning on secret/private data/models without centralizing them under one authority.\n", 10 | "\n", 11 | "**Scope:** Note that we'll not just be talking about how to decentralized / encrypt data, but we'll be addressing how PySyft can be used to help decentralize the entire ecosystem around data, even including the Databases where data is stored and queried, and the neural models which are used to extract information from data. As new extensions to PySyft are created, these notebooks will be extended with new tutorials to explain the new functionality.\n", 12 | "\n", 13 | "Authors:\n", 14 | "- Jason Mancuso - Twitter: [@jvmancuso](https://twitter.com/jvmancuso)\n", 15 | "\n", 16 | "## Outline:\n", 17 | "\n", 18 | "- Part 1: The Basic Tools of Private Deep Learning\n", 19 | "\n", 20 | "\n", 21 | "## Why Take This Tutorial?\n", 22 | "\n", 23 | "**1) A Competitive Career Advantage** - For the past 20 years, the digital revolution has made data more and more accessible in ever larger quantities as analog processes have become digitized. However, with new regulation such as [GDPR](https://eugdpr.org/), enterprises are under pressure to have less freedom with how they use - and more importantly how they analyze - personal information. **Bottom Line:** Data Scientists aren't going to have access to as much data with \"old school\" tools, but by learning the tools of Private Deep Learning, YOU can be ahead of this curve and have a competitive advantage in your career. \n", 24 | "\n", 25 | "**2) Entrepreneurial Opportunities** - There are a whole host of problems in society that Deep Learning can solve, but many of the most important haven't been explored because it would require access to incredibly sensitive information about people (consider using Deep Learning to help people with mental or relationship issues!). Thus, learning Private Deep Learning unlocks a whole host of new startup opportunities for you which were not previously available to others without these toolsets.\n", 26 | "\n", 27 | "**3) Social Good** - Deep Learning can be used to solve a wide variety of problems in the real world, but Deep Learning on *personal information* is Deep Learning about people, *for people*. Learning how to do Deep Learning on data you don't own represents more than a career or entrepreneurial opportunity, it is the opportunity to help solve some of the most personal and important problems in people's lives - and to do it at scale.\n", 28 | "\n", 29 | "## How do I get extra credit?\n", 30 | "\n", 31 | "- Star PySyft on GitHub! - [https://github.com/OpenMined/PySyft](https://github.com/OpenMined/PySyft)\n", 32 | "- Star PySyft-TensorFlow on GitHub! - [https://github.com/OpenMined/PySyft-TensorFlow](https://github.com/OpenMined/PySyft-TensorFlow)\n", 33 | "- Make a Youtube video teaching this notebook!\n", 34 | "\n", 35 | "\n", 36 | "... ok ... let's do this!" 37 | ] 38 | }, 39 | { 40 | "cell_type": "markdown", 41 | "metadata": {}, 42 | "source": [ 43 | "# Part -1: Prerequisites\n", 44 | "\n", 45 | "- Know TensorFlow - if not then take a look at the [TensorFlow tutorials](https://www.tensorflow.org/tutorials/)\n", 46 | "- Read the PySyft Framework Paper https://arxiv.org/abs/1811.04017! This will give you a thorough background on how PySyft is constructed, which will help things make more sense. Note the paper has gotten slightly out of date, so refer to these tutorials or ask in Slack if something seems strange! [slack.openmined.org](http://slack.openmined.org/)" 47 | ] 48 | }, 49 | { 50 | "cell_type": "markdown", 51 | "metadata": {}, 52 | "source": [ 53 | "# Part 0: Setup\n", 54 | "\n", 55 | "To begin, you'll need to make sure you have the right things installed. To do so, head on over to PySyft's readme and follow the setup instructions. TLDR for most folks is.\n", 56 | "\n", 57 | "- Install Python 3.6 or higher\n", 58 | "- Install TensorFlow 2.0.0 (`pip install tensorflow==2.0.0`)\n", 59 | "- TODO syft-tf install instructions\n", 60 | "\n", 61 | "If any part of this doesn't work for you (or any of the tests fail) - first check the [README](https://github.com/OpenMined/PySyft.git) for installation help and then [open a GitHub issue](https://github.com/OpenMined/PySyft-TensorFlow/issues/new) or ping the #beginner channel in slack! [slack.openmined.org](http://slack.openmined.org/)" 62 | ] 63 | }, 64 | { 65 | "cell_type": "code", 66 | "execution_count": 1, 67 | "metadata": {}, 68 | "outputs": [ 69 | { 70 | "data": { 71 | "text/plain": [ 72 | "" 73 | ] 74 | }, 75 | "execution_count": 1, 76 | "metadata": {}, 77 | "output_type": "execute_result" 78 | } 79 | ], 80 | "source": [ 81 | "# Run this cell to see if things work\n", 82 | "import tensorflow as tf\n", 83 | "import syft as sy\n", 84 | "hook = sy.TensorFlowHook(tf)\n", 85 | "\n", 86 | "tf.constant([1, 2, 3, 4, 5.])" 87 | ] 88 | }, 89 | { 90 | "cell_type": "markdown", 91 | "metadata": {}, 92 | "source": [ 93 | "If this cell executed, then you're off to the races! Let's do this!" 94 | ] 95 | }, 96 | { 97 | "cell_type": "markdown", 98 | "metadata": {}, 99 | "source": [ 100 | "# Part 1: The Basic Tools of Private, Decentralized Data Science\n", 101 | "\n", 102 | "So - the first question you may be wondering is - How in the world do we train a model on data we don't have access to? \n", 103 | "\n", 104 | "Well, the answer is surprisingly simple. If you're used to working in TensorFlow, then you're used to working with tf.Tensor objects like these!" 105 | ] 106 | }, 107 | { 108 | "cell_type": "code", 109 | "execution_count": 2, 110 | "metadata": {}, 111 | "outputs": [ 112 | { 113 | "data": { 114 | "text/plain": [ 115 | "" 116 | ] 117 | }, 118 | "execution_count": 2, 119 | "metadata": {}, 120 | "output_type": "execute_result" 121 | } 122 | ], 123 | "source": [ 124 | "x = tf.constant([1, 2, 3, 4, 5.])\n", 125 | "y = x + x\n", 126 | "y" 127 | ] 128 | }, 129 | { 130 | "cell_type": "markdown", 131 | "metadata": {}, 132 | "source": [ 133 | "Obviously, using these super fancy (and powerful!) tensors is important, but also requires you to have the data on your local machine. This is where our journey begins. \n", 134 | "\n", 135 | "# Section 1.1 - Sending Tensors to Bob's Machine\n", 136 | "\n", 137 | "Whereas normally we would perform data science / deep learning on the machine which holds the data, now we want to perform this kind of computation on some **other** machine. More specifically, we can no longer simply assume that the data is on our local machine.\n", 138 | "\n", 139 | "Thus, instead of using TensorFlow tensors, we're now going to work with **pointers** to tensors. Let me show you what I mean. First, let's create a \"pretend\" machine owned by a \"pretend\" person - we'll call him Bob." 140 | ] 141 | }, 142 | { 143 | "cell_type": "code", 144 | "execution_count": 3, 145 | "metadata": {}, 146 | "outputs": [], 147 | "source": [ 148 | "bob = sy.VirtualWorker(hook, id=\"bob\")" 149 | ] 150 | }, 151 | { 152 | "cell_type": "markdown", 153 | "metadata": {}, 154 | "source": [ 155 | "Let's say Bob's machine is on another planet - perhaps on Mars! But, at the moment the machine is empty. Let's create some data so that we can send it to Bob and learn about pointers!" 156 | ] 157 | }, 158 | { 159 | "cell_type": "code", 160 | "execution_count": 4, 161 | "metadata": {}, 162 | "outputs": [], 163 | "source": [ 164 | "x = tf.constant([1., 2., 3., 4., 5.])\n", 165 | "y = tf.constant([1., 1., 1., 1., 1.])" 166 | ] 167 | }, 168 | { 169 | "cell_type": "markdown", 170 | "metadata": {}, 171 | "source": [ 172 | "And now - let's send our tensors to Bob!!" 173 | ] 174 | }, 175 | { 176 | "cell_type": "code", 177 | "execution_count": 5, 178 | "metadata": {}, 179 | "outputs": [], 180 | "source": [ 181 | "x_ptr = x.send(bob)\n", 182 | "y_ptr = y.send(bob)" 183 | ] 184 | }, 185 | { 186 | "cell_type": "markdown", 187 | "metadata": {}, 188 | "source": [ 189 | "BOOM! Now Bob has two tensors! Don't believe me? Have a look for yourself!" 190 | ] 191 | }, 192 | { 193 | "cell_type": "code", 194 | "execution_count": 6, 195 | "metadata": {}, 196 | "outputs": [ 197 | { 198 | "data": { 199 | "text/plain": [ 200 | "{31817784589: ,\n", 201 | " 32714528385: }" 202 | ] 203 | }, 204 | "execution_count": 6, 205 | "metadata": {}, 206 | "output_type": "execute_result" 207 | } 208 | ], 209 | "source": [ 210 | "bob._objects" 211 | ] 212 | }, 213 | { 214 | "cell_type": "code", 215 | "execution_count": 7, 216 | "metadata": {}, 217 | "outputs": [ 218 | { 219 | "data": { 220 | "text/plain": [ 221 | "(Wrapper)>[PointerTensor | me:31933462833 -> bob:8206513956]" 222 | ] 223 | }, 224 | "execution_count": 7, 225 | "metadata": {}, 226 | "output_type": "execute_result" 227 | } 228 | ], 229 | "source": [ 230 | "z_ptr = x_ptr + x_ptr\n", 231 | "z_ptr" 232 | ] 233 | }, 234 | { 235 | "cell_type": "code", 236 | "execution_count": 8, 237 | "metadata": {}, 238 | "outputs": [ 239 | { 240 | "data": { 241 | "text/plain": [ 242 | "{31817784589: ,\n", 243 | " 32714528385: ,\n", 244 | " 8206513956: }" 245 | ] 246 | }, 247 | "execution_count": 8, 248 | "metadata": {}, 249 | "output_type": "execute_result" 250 | } 251 | ], 252 | "source": [ 253 | "bob._objects" 254 | ] 255 | }, 256 | { 257 | "cell_type": "markdown", 258 | "metadata": {}, 259 | "source": [ 260 | "Now notice something. When we called `x.send(bob)` it returned a new object that we called `x_ptr`. This is our first *pointer* to a tensor. Pointers to tensors do NOT actually hold data themselves. Instead, they simply contain metadata about a tensor (with data) stored on another machine. The purpose of these tensors is to give us an intuitive API to tell the other machine to compute functions using this tensor. Let's take a look at the metadata that pointers contain." 261 | ] 262 | }, 263 | { 264 | "cell_type": "code", 265 | "execution_count": 9, 266 | "metadata": {}, 267 | "outputs": [ 268 | { 269 | "data": { 270 | "text/plain": [ 271 | "(Wrapper)>[PointerTensor | me:31933462833 -> bob:8206513956]" 272 | ] 273 | }, 274 | "execution_count": 9, 275 | "metadata": {}, 276 | "output_type": "execute_result" 277 | } 278 | ], 279 | "source": [ 280 | "z_ptr" 281 | ] 282 | }, 283 | { 284 | "cell_type": "markdown", 285 | "metadata": {}, 286 | "source": [ 287 | "Check out that metadata!\n", 288 | "\n", 289 | "There are two main attributes specific to pointers:\n", 290 | "\n", 291 | "- `x_ptr.location : bob`, the location, a reference to the location that the pointer is pointing to\n", 292 | "- `x_ptr.id_at_location : `, the id where the tensor is stored at location\n", 293 | "\n", 294 | "They are printed in the format `@`\n", 295 | "\n", 296 | "There are also other more generic attributes:\n", 297 | "- `x_ptr.id : `, the id of our pointer tensor, it was allocated randomly\n", 298 | "- `x_ptr.owner : \"me\"`, the worker which owns the pointer tensor, here it's the local worker, named \"me\"\n" 299 | ] 300 | }, 301 | { 302 | "cell_type": "code", 303 | "execution_count": 10, 304 | "metadata": {}, 305 | "outputs": [ 306 | { 307 | "data": { 308 | "text/plain": [ 309 | "" 310 | ] 311 | }, 312 | "execution_count": 10, 313 | "metadata": {}, 314 | "output_type": "execute_result" 315 | } 316 | ], 317 | "source": [ 318 | "x_ptr.location" 319 | ] 320 | }, 321 | { 322 | "cell_type": "code", 323 | "execution_count": 11, 324 | "metadata": {}, 325 | "outputs": [ 326 | { 327 | "data": { 328 | "text/plain": [ 329 | "" 330 | ] 331 | }, 332 | "execution_count": 11, 333 | "metadata": {}, 334 | "output_type": "execute_result" 335 | } 336 | ], 337 | "source": [ 338 | "bob" 339 | ] 340 | }, 341 | { 342 | "cell_type": "code", 343 | "execution_count": 12, 344 | "metadata": {}, 345 | "outputs": [ 346 | { 347 | "data": { 348 | "text/plain": [ 349 | "True" 350 | ] 351 | }, 352 | "execution_count": 12, 353 | "metadata": {}, 354 | "output_type": "execute_result" 355 | } 356 | ], 357 | "source": [ 358 | "bob == x_ptr.location" 359 | ] 360 | }, 361 | { 362 | "cell_type": "code", 363 | "execution_count": 13, 364 | "metadata": {}, 365 | "outputs": [ 366 | { 367 | "data": { 368 | "text/plain": [ 369 | "31817784589" 370 | ] 371 | }, 372 | "execution_count": 13, 373 | "metadata": {}, 374 | "output_type": "execute_result" 375 | } 376 | ], 377 | "source": [ 378 | "x_ptr.id_at_location" 379 | ] 380 | }, 381 | { 382 | "cell_type": "code", 383 | "execution_count": 14, 384 | "metadata": {}, 385 | "outputs": [ 386 | { 387 | "data": { 388 | "text/plain": [ 389 | "" 390 | ] 391 | }, 392 | "execution_count": 14, 393 | "metadata": {}, 394 | "output_type": "execute_result" 395 | } 396 | ], 397 | "source": [ 398 | "x_ptr.owner" 399 | ] 400 | }, 401 | { 402 | "cell_type": "markdown", 403 | "metadata": {}, 404 | "source": [ 405 | "You may wonder why the local worker which owns the pointer is also a VirtualWorker, although we didn't create it.\n", 406 | "Fun fact, just like we had a VirtualWorker object for Bob, we (by default) always have one for us as well. This worker is automatically created when we construct `hook = sy.TensorFlowHook(tf)` and so you don't usually have to create it yourself." 407 | ] 408 | }, 409 | { 410 | "cell_type": "code", 411 | "execution_count": 15, 412 | "metadata": {}, 413 | "outputs": [ 414 | { 415 | "data": { 416 | "text/plain": [ 417 | "" 418 | ] 419 | }, 420 | "execution_count": 15, 421 | "metadata": {}, 422 | "output_type": "execute_result" 423 | } 424 | ], 425 | "source": [ 426 | "me = sy.local_worker\n", 427 | "me" 428 | ] 429 | }, 430 | { 431 | "cell_type": "code", 432 | "execution_count": 16, 433 | "metadata": {}, 434 | "outputs": [ 435 | { 436 | "data": { 437 | "text/plain": [ 438 | "True" 439 | ] 440 | }, 441 | "execution_count": 16, 442 | "metadata": {}, 443 | "output_type": "execute_result" 444 | } 445 | ], 446 | "source": [ 447 | "me == x_ptr.owner" 448 | ] 449 | }, 450 | { 451 | "cell_type": "markdown", 452 | "metadata": {}, 453 | "source": [ 454 | "And finally, just like we can call .send() on a tensor, we can call .get() on a pointer to a tensor to get it back!!!" 455 | ] 456 | }, 457 | { 458 | "cell_type": "code", 459 | "execution_count": 17, 460 | "metadata": {}, 461 | "outputs": [ 462 | { 463 | "data": { 464 | "text/plain": [ 465 | "" 466 | ] 467 | }, 468 | "execution_count": 17, 469 | "metadata": {}, 470 | "output_type": "execute_result" 471 | } 472 | ], 473 | "source": [ 474 | "x_ptr.get()" 475 | ] 476 | }, 477 | { 478 | "cell_type": "code", 479 | "execution_count": 18, 480 | "metadata": {}, 481 | "outputs": [ 482 | { 483 | "data": { 484 | "text/plain": [ 485 | "" 486 | ] 487 | }, 488 | "execution_count": 18, 489 | "metadata": {}, 490 | "output_type": "execute_result" 491 | } 492 | ], 493 | "source": [ 494 | "y_ptr.get()" 495 | ] 496 | }, 497 | { 498 | "cell_type": "code", 499 | "execution_count": 19, 500 | "metadata": {}, 501 | "outputs": [ 502 | { 503 | "data": { 504 | "text/plain": [ 505 | "" 506 | ] 507 | }, 508 | "execution_count": 19, 509 | "metadata": {}, 510 | "output_type": "execute_result" 511 | } 512 | ], 513 | "source": [ 514 | "z_ptr.get()" 515 | ] 516 | }, 517 | { 518 | "cell_type": "code", 519 | "execution_count": 20, 520 | "metadata": {}, 521 | "outputs": [ 522 | { 523 | "data": { 524 | "text/plain": [ 525 | "{}" 526 | ] 527 | }, 528 | "execution_count": 20, 529 | "metadata": {}, 530 | "output_type": "execute_result" 531 | } 532 | ], 533 | "source": [ 534 | "bob._objects" 535 | ] 536 | }, 537 | { 538 | "cell_type": "markdown", 539 | "metadata": {}, 540 | "source": [ 541 | "And as you can see... Bob no longer has the tensors anymore!!! They've moved back to our machine!" 542 | ] 543 | }, 544 | { 545 | "cell_type": "markdown", 546 | "metadata": {}, 547 | "source": [ 548 | "# Section 1.2 - Using Tensor Pointers\n", 549 | "\n", 550 | "So, sending and receiving tensors from Bob is great, but this is hardly Deep Learning! We want to be able to perform tensor _operations_ on remote tensors. Fortunately, tensor pointers make this quite easy! You can just use pointers like you would normal tensors!" 551 | ] 552 | }, 553 | { 554 | "cell_type": "code", 555 | "execution_count": 21, 556 | "metadata": {}, 557 | "outputs": [], 558 | "source": [ 559 | "x = tf.constant([1., 2., 3., 4., 5.]).send(bob)\n", 560 | "y = tf.constant([1., 1., 1., 1., 1.]).send(bob)" 561 | ] 562 | }, 563 | { 564 | "cell_type": "code", 565 | "execution_count": 22, 566 | "metadata": {}, 567 | "outputs": [], 568 | "source": [ 569 | "z = x + y" 570 | ] 571 | }, 572 | { 573 | "cell_type": "code", 574 | "execution_count": 23, 575 | "metadata": {}, 576 | "outputs": [ 577 | { 578 | "data": { 579 | "text/plain": [ 580 | "(Wrapper)>[PointerTensor | me:35424001991 -> bob:87627708699]" 581 | ] 582 | }, 583 | "execution_count": 23, 584 | "metadata": {}, 585 | "output_type": "execute_result" 586 | } 587 | ], 588 | "source": [ 589 | "z" 590 | ] 591 | }, 592 | { 593 | "cell_type": "markdown", 594 | "metadata": {}, 595 | "source": [ 596 | "And voilà! \n", 597 | "\n", 598 | "Behind the scenes, something very powerful happened. Instead of x and y computing an addition locally, a command was serialized and sent to Bob, who performed the computation, created a tensor z, and then returned the pointer to z back to us!\n", 599 | "\n", 600 | "If we call .get() on the pointer, we will then receive the result back to our machine!" 601 | ] 602 | }, 603 | { 604 | "cell_type": "code", 605 | "execution_count": 24, 606 | "metadata": {}, 607 | "outputs": [ 608 | { 609 | "data": { 610 | "text/plain": [ 611 | "" 612 | ] 613 | }, 614 | "execution_count": 24, 615 | "metadata": {}, 616 | "output_type": "execute_result" 617 | } 618 | ], 619 | "source": [ 620 | "z.get()" 621 | ] 622 | }, 623 | { 624 | "cell_type": "markdown", 625 | "metadata": {}, 626 | "source": [ 627 | "### TensorFlow Functions\n", 628 | "\n", 629 | "This API has been extended to all of TensorFlow's primary operations!!!" 630 | ] 631 | }, 632 | { 633 | "cell_type": "code", 634 | "execution_count": 25, 635 | "metadata": {}, 636 | "outputs": [ 637 | { 638 | "data": { 639 | "text/plain": [ 640 | "(Wrapper)>[PointerTensor | me:26214408079 -> bob:35174604077]" 641 | ] 642 | }, 643 | "execution_count": 25, 644 | "metadata": {}, 645 | "output_type": "execute_result" 646 | } 647 | ], 648 | "source": [ 649 | "x" 650 | ] 651 | }, 652 | { 653 | "cell_type": "code", 654 | "execution_count": 26, 655 | "metadata": {}, 656 | "outputs": [ 657 | { 658 | "data": { 659 | "text/plain": [ 660 | "(Wrapper)>[PointerTensor | me:53125201835 -> bob:40770790434]" 661 | ] 662 | }, 663 | "execution_count": 26, 664 | "metadata": {}, 665 | "output_type": "execute_result" 666 | } 667 | ], 668 | "source": [ 669 | "y" 670 | ] 671 | }, 672 | { 673 | "cell_type": "code", 674 | "execution_count": 27, 675 | "metadata": {}, 676 | "outputs": [], 677 | "source": [ 678 | "z = tf.add(x, y)" 679 | ] 680 | }, 681 | { 682 | "cell_type": "code", 683 | "execution_count": 28, 684 | "metadata": {}, 685 | "outputs": [ 686 | { 687 | "data": { 688 | "text/plain": [ 689 | "" 690 | ] 691 | }, 692 | "execution_count": 28, 693 | "metadata": {}, 694 | "output_type": "execute_result" 695 | } 696 | ], 697 | "source": [ 698 | "z.get()" 699 | ] 700 | }, 701 | { 702 | "cell_type": "markdown", 703 | "metadata": {}, 704 | "source": [ 705 | "### Variables [experimental]\n", 706 | "Autodiff coming soon :)" 707 | ] 708 | }, 709 | { 710 | "cell_type": "code", 711 | "execution_count": 29, 712 | "metadata": {}, 713 | "outputs": [], 714 | "source": [ 715 | "x = tf.Variable([1., 2., 3., 4., 5.]).send(bob)\n", 716 | "y = tf.Variable([1., 1., 1., 1., 1.]).send(bob)" 717 | ] 718 | }, 719 | { 720 | "cell_type": "code", 721 | "execution_count": 30, 722 | "metadata": {}, 723 | "outputs": [ 724 | { 725 | "data": { 726 | "text/plain": [ 727 | "(Wrapper)>[PointerTensor | me:62848670961 -> bob:16896611751]" 728 | ] 729 | }, 730 | "execution_count": 30, 731 | "metadata": {}, 732 | "output_type": "execute_result" 733 | } 734 | ], 735 | "source": [ 736 | "coef = tf.constant(2.).send(bob)\n", 737 | "z = x + coef * y\n", 738 | "\n", 739 | "# manual differentiation\n", 740 | "dzdy = coef\n", 741 | "delta = tf.negative(dzdy)\n", 742 | "delta = tf.broadcast_to(delta, [5])\n", 743 | "y.assign_add(delta)" 744 | ] 745 | }, 746 | { 747 | "cell_type": "code", 748 | "execution_count": 31, 749 | "metadata": {}, 750 | "outputs": [], 751 | "source": [ 752 | "z = y.get()" 753 | ] 754 | }, 755 | { 756 | "cell_type": "code", 757 | "execution_count": 32, 758 | "metadata": {}, 759 | "outputs": [ 760 | { 761 | "name": "stdout", 762 | "output_type": "stream", 763 | "text": [ 764 | "\n" 765 | ] 766 | } 767 | ], 768 | "source": [ 769 | "print(z)" 770 | ] 771 | } 772 | ], 773 | "metadata": { 774 | "kernelspec": { 775 | "display_name": "Python 3", 776 | "language": "python", 777 | "name": "python3" 778 | }, 779 | "language_info": { 780 | "codemirror_mode": { 781 | "name": "ipython", 782 | "version": 3 783 | }, 784 | "file_extension": ".py", 785 | "mimetype": "text/x-python", 786 | "name": "python", 787 | "nbconvert_exporter": "python", 788 | "pygments_lexer": "ipython3", 789 | "version": "3.6.9" 790 | } 791 | }, 792 | "nbformat": 4, 793 | "nbformat_minor": 2 794 | } 795 | -------------------------------------------------------------------------------- /examples/Part 02 - Intro to Private Training with Remote Execution.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Part 2: Intro to Private Training with Remote Execution\n", 8 | "\n", 9 | "In the last section, we learned about PointerTensors, which create the underlying infrastructure we need for privacy preserving Deep Learning. In this section, we're going to see how to use these basic tools to train our first deep learning model using remote execution.\n", 10 | "\n", 11 | "Authors:\n", 12 | "- Yann Dupis - Twitter: [@YannDupis](https://twitter.com/YannDupis)\n", 13 | "- Andrew Trask - Twitter: [@iamtrask](https://twitter.com/iamtrask)\n", 14 | "\n", 15 | "### Why use remote execution?\n", 16 | "\n", 17 | "Let's say you are an AI startup who wants to build a deep learning model to detect [diabetic retinopathy (DR)](https://ai.googleblog.com/2016/11/deep-learning-for-detection-of-diabetic.html), which is the fastest growing cause of blindness. Before training your model, the first step would be to acquire a dataset of retinopathy images with signs of DR. One approach could be to work with a hospital and ask them to send you a copy of this dataset. However because of the sensitivity of the patients' data, the hospital might be exposed to liability risks.\n", 18 | "\n", 19 | "\n", 20 | "That's where remote execution comes into the picture. Instead of bringing training data to the model (a central server), you bring the model to the training data (wherever it may live). In this case, it would be the hospital.\n", 21 | "\n", 22 | "The idea is that this allows whoever is creating the data to own the only permanent copy, and thus maintain control over who ever has access to it. Pretty cool, eh?" 23 | ] 24 | }, 25 | { 26 | "cell_type": "markdown", 27 | "metadata": {}, 28 | "source": [ 29 | "# Section 2.1 - Private Training on MNIST\n", 30 | "\n", 31 | "For this tutorial, we will train a model on the [MNIST dataset](http://yann.lecun.com/exdb/mnist/) to classify digits based on images.\n", 32 | "\n", 33 | "We can assume that we have a remote worker named Bob who owns the data." 34 | ] 35 | }, 36 | { 37 | "cell_type": "code", 38 | "execution_count": 1, 39 | "metadata": { 40 | "collapsed": true 41 | }, 42 | "outputs": [], 43 | "source": [ 44 | "import tensorflow as tf\n", 45 | "import syft as sy\n", 46 | "\n", 47 | "hook = sy.TensorFlowHook(tf)\n", 48 | "bob = sy.VirtualWorker(hook, id=\"bob\")" 49 | ] 50 | }, 51 | { 52 | "cell_type": "markdown", 53 | "metadata": {}, 54 | "source": [ 55 | "Let's download the MNIST data from `tf.keras.datasets`. Note that we are converting the data from numpy to `tf.Tensor` in order to have the PySyft functionalities." 56 | ] 57 | }, 58 | { 59 | "cell_type": "code", 60 | "execution_count": 2, 61 | "metadata": { 62 | "collapsed": true 63 | }, 64 | "outputs": [], 65 | "source": [ 66 | "mnist = tf.keras.datasets.mnist\n", 67 | "\n", 68 | "(x_train, y_train), (x_test, y_test) = mnist.load_data()\n", 69 | "x_train, x_test = x_train / 255.0, x_test / 255.0\n", 70 | "\n", 71 | "x_train, y_train = tf.convert_to_tensor(x_train), tf.convert_to_tensor(y_train)\n", 72 | "x_test, y_test = tf.convert_to_tensor(x_test), tf.convert_to_tensor(y_test)" 73 | ] 74 | }, 75 | { 76 | "cell_type": "markdown", 77 | "metadata": {}, 78 | "source": [ 79 | "As decribed in Part 1, we can send this data to Bob with the `send` method on the `tf.Tensor`. " 80 | ] 81 | }, 82 | { 83 | "cell_type": "code", 84 | "execution_count": 3, 85 | "metadata": { 86 | "collapsed": true 87 | }, 88 | "outputs": [], 89 | "source": [ 90 | "x_train_ptr = x_train.send(bob)\n", 91 | "y_train_ptr = y_train.send(bob)" 92 | ] 93 | }, 94 | { 95 | "cell_type": "markdown", 96 | "metadata": {}, 97 | "source": [ 98 | "Excellent! We have everything to start experimenting. To train our model on Bob's machine, we just have to perform the following steps:\n", 99 | "\n", 100 | "- Define a model, including optimizer and loss\n", 101 | "- Send the model to Bob\n", 102 | "- Start the training process\n", 103 | "- Get the trained model back\n", 104 | "\n", 105 | "Let's do it!" 106 | ] 107 | }, 108 | { 109 | "cell_type": "code", 110 | "execution_count": 4, 111 | "metadata": { 112 | "collapsed": true 113 | }, 114 | "outputs": [], 115 | "source": [ 116 | "# Define the model\n", 117 | "model = tf.keras.models.Sequential([\n", 118 | " tf.keras.layers.Flatten(input_shape=(28, 28)),\n", 119 | " tf.keras.layers.Dense(128, activation='relu'),\n", 120 | " tf.keras.layers.Dropout(0.2),\n", 121 | " tf.keras.layers.Dense(10, activation='softmax')\n", 122 | "])\n", 123 | "\n", 124 | "# Compile with optimizer, loss and metrics\n", 125 | "model.compile(optimizer='adam',\n", 126 | " loss='sparse_categorical_crossentropy',\n", 127 | " metrics=['accuracy'])" 128 | ] 129 | }, 130 | { 131 | "cell_type": "markdown", 132 | "metadata": {}, 133 | "source": [ 134 | "Once you have defined your model, you can simply send it to Bob calling the `send` method. It's the exact same process as sending a tensor." 135 | ] 136 | }, 137 | { 138 | "cell_type": "code", 139 | "execution_count": null, 140 | "metadata": { 141 | "collapsed": true 142 | }, 143 | "outputs": [], 144 | "source": [ 145 | "model_ptr = model.send(bob)" 146 | ] 147 | }, 148 | { 149 | "cell_type": "code", 150 | "execution_count": null, 151 | "metadata": { 152 | "collapsed": true 153 | }, 154 | "outputs": [], 155 | "source": [ 156 | "model_ptr" 157 | ] 158 | }, 159 | { 160 | "cell_type": "markdown", 161 | "metadata": {}, 162 | "source": [ 163 | "Now, we have a pointer pointing to the model on Bob's machine. We can validate that's the case by inspecting the attribute `_objects` on the virtual worker. " 164 | ] 165 | }, 166 | { 167 | "cell_type": "code", 168 | "execution_count": null, 169 | "metadata": { 170 | "collapsed": true 171 | }, 172 | "outputs": [], 173 | "source": [ 174 | "bob._objects[model_ptr.id_at_location]" 175 | ] 176 | }, 177 | { 178 | "cell_type": "markdown", 179 | "metadata": {}, 180 | "source": [ 181 | "Everything is ready to start training our model on this remote dataset. You can call `fit` and pass `x_train_ptr` `y_train_ptr` which are pointing to Bob's data. Note that's the exact same interface as normal `tf.keras`." 182 | ] 183 | }, 184 | { 185 | "cell_type": "code", 186 | "execution_count": null, 187 | "metadata": { 188 | "collapsed": true 189 | }, 190 | "outputs": [], 191 | "source": [ 192 | "model_ptr.fit(x_train_ptr, y_train_ptr, epochs=2, validation_split=0.2)" 193 | ] 194 | }, 195 | { 196 | "cell_type": "markdown", 197 | "metadata": {}, 198 | "source": [ 199 | "Fantastic! you have trained your model acheiving an accuracy greater than 95%.\n", 200 | "\n", 201 | "You can get your trained model back by just calling `get` on it. " 202 | ] 203 | }, 204 | { 205 | "cell_type": "code", 206 | "execution_count": null, 207 | "metadata": { 208 | "collapsed": true 209 | }, 210 | "outputs": [], 211 | "source": [ 212 | "model_gotten = model_ptr.get()\n", 213 | "\n", 214 | "model_gotten" 215 | ] 216 | }, 217 | { 218 | "cell_type": "markdown", 219 | "metadata": {}, 220 | "source": [ 221 | "It's good practice to see if your model can generalize by assessing its accuracy on an holdout dataset. You can simply call `evaluate`." 222 | ] 223 | }, 224 | { 225 | "cell_type": "code", 226 | "execution_count": null, 227 | "metadata": { 228 | "collapsed": true 229 | }, 230 | "outputs": [], 231 | "source": [ 232 | "model_gotten.evaluate(x_test, y_test, verbose=2)" 233 | ] 234 | }, 235 | { 236 | "cell_type": "markdown", 237 | "metadata": {}, 238 | "source": [ 239 | "Boom! The model remotely trained on Bob's data is more than 95% accurate on this holdout dataset." 240 | ] 241 | }, 242 | { 243 | "cell_type": "markdown", 244 | "metadata": {}, 245 | "source": [ 246 | "If your model doesn't fit into the Sequential paradigm, you can use Keras's functional API, or even subclass [tf.keras.Model](https://www.tensorflow.org/guide/keras/custom_layers_and_models#building_models) to create custom models." 247 | ] 248 | }, 249 | { 250 | "cell_type": "code", 251 | "execution_count": null, 252 | "metadata": { 253 | "collapsed": true 254 | }, 255 | "outputs": [], 256 | "source": [ 257 | "class CustomModel(tf.keras.Model):\n", 258 | "\n", 259 | " def __init__(self, num_classes=10):\n", 260 | " super(CustomModel, self).__init__(name='custom_model')\n", 261 | " self.num_classes = num_classes\n", 262 | " \n", 263 | " self.flatten = tf.keras.layers.Flatten(input_shape=(28, 28))\n", 264 | " self.dense_1 = tf.keras.layers.Dense(128, activation='relu')\n", 265 | " self.dropout = tf.keras.layers.Dropout(0.2)\n", 266 | " self.dense_2 = tf.keras.layers.Dense(num_classes, activation='softmax')\n", 267 | "\n", 268 | " def call(self, inputs, training=False):\n", 269 | " x = self.flatten(inputs)\n", 270 | " x = self.dense_1(x)\n", 271 | " x = self.dropout(x, training=training)\n", 272 | " return self.dense_2(x)\n", 273 | " \n", 274 | "model = CustomModel(10)\n", 275 | "\n", 276 | "# need to call the model on dummy data before sending it\n", 277 | "# in order to set the input shape (required when saving to SavedModel)\n", 278 | "model.predict(tf.ones([1, 28, 28]))\n", 279 | "\n", 280 | "model.compile(optimizer='adam',\n", 281 | " loss='sparse_categorical_crossentropy',\n", 282 | " metrics=['accuracy'])\n", 283 | "\n", 284 | "model_ptr = model.send(bob)\n", 285 | "\n", 286 | "model_ptr.fit(x_train_ptr, y_train_ptr, epochs=2, validation_split=0.2)" 287 | ] 288 | }, 289 | { 290 | "cell_type": "markdown", 291 | "metadata": {}, 292 | "source": [ 293 | "## Well Done!\n", 294 | "\n", 295 | "And voilà! We have trained a Deep Learning model on Bob's data by sending the model to him. Never in this process do we ever see or request access to the underlying training data! We preserve the privacy of Bob!!!" 296 | ] 297 | }, 298 | { 299 | "cell_type": "markdown", 300 | "metadata": {}, 301 | "source": [ 302 | "# Congratulations!!! - Time to Join the Community!\n", 303 | "\n", 304 | "Congratulations on completing this notebook tutorial! If you enjoyed this and would like to join the movement toward privacy preserving, decentralized ownership of AI and the AI supply chain (data), you can do so in the following ways!\n", 305 | "\n", 306 | "### Star PySyft on GitHub\n", 307 | "\n", 308 | "The easiest way to help our community is just by starring the Repos! This helps raise awareness of the cool tools we're building.\n", 309 | "\n", 310 | "- Star PySyft on GitHub! - [https://github.com/OpenMined/PySyft](https://github.com/OpenMined/PySyft)\n", 311 | "- Star PySyft-TensorFlow on GitHub! - [https://github.com/OpenMined/PySyft-TensorFlow]\n", 312 | "\n", 313 | "### Join our Slack!\n", 314 | "\n", 315 | "The best way to keep up to date on the latest advancements is to join our community! You can do so by filling out the form at [http://slack.openmined.org](http://slack.openmined.org)\n", 316 | "\n", 317 | "### Join a Code Project!\n", 318 | "\n", 319 | "The best way to contribute to our community is to become a code contributor! At any time you can go to PySyft GitHub Issues page and filter for \"Projects\". This will show you all the top level Tickets giving an overview of what projects you can join! If you don't want to join a project, but you would like to do a bit of coding, you can also look for more \"one off\" mini-projects by searching for GitHub issues marked \"good first issue\".\n", 320 | "\n", 321 | "- [PySyft Projects](https://github.com/OpenMined/PySyft/issues?q=is%3Aopen+is%3Aissue+label%3AProject)\n", 322 | "- [Good First Issue Tickets](https://github.com/OpenMined/PySyft/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22)\n", 323 | "\n", 324 | "### Donate\n", 325 | "\n", 326 | "If you don't have time to contribute to our codebase, but would still like to lend support, you can also become a Backer on our Open Collective. All donations go toward our web hosting and other community expenses such as hackathons and meetups!\n", 327 | "\n", 328 | "[OpenMined's Open Collective Page](https://opencollective.com/openmined)" 329 | ] 330 | } 331 | ], 332 | "metadata": { 333 | "kernelspec": { 334 | "display_name": "pysyft-tf", 335 | "language": "python", 336 | "name": "pysyft-tf" 337 | }, 338 | "language_info": { 339 | "codemirror_mode": { 340 | "name": "ipython", 341 | "version": 3 342 | }, 343 | "file_extension": ".py", 344 | "mimetype": "text/x-python", 345 | "name": "python", 346 | "nbconvert_exporter": "python", 347 | "pygments_lexer": "ipython3", 348 | "version": "3.7.3" 349 | } 350 | }, 351 | "nbformat": 4, 352 | "nbformat_minor": 2 353 | } 354 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | target-version = ['py36'] 3 | line-length = 100 4 | exclude = ''' 5 | ( 6 | \.git 7 | | \.mypy_cache 8 | | \.tox 9 | | \.venv 10 | | \.eggs\/ 11 | | build 12 | | dist 13 | | venv\/ 14 | ) 15 | ''' 16 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | tensorflow>=2.0.0-rc2 2 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | black>=10.3b0 2 | bumpversion>=0.5.3 3 | coverage>=4.5.3 4 | ipykernel>=5.1.0 5 | pre-commit>=1.16.1 6 | pyopenssl>=19.0.0 7 | pyshark>=0.4.2.4 8 | pytest>=4.5.0 9 | setuptools>=41.0.0 10 | sphinx-markdown-builder>=0.4.1 11 | Sphinx>=2.1.1 12 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setuptools import find_packages 4 | from setuptools import setup 5 | 6 | 7 | # Utility function to read the README file. 8 | # Used for the long_description. It's nice, because now 1) we have a top level 9 | # README file and 2) it's easier to type in the README file than to put a raw 10 | # string in below ... 11 | def read(fname): 12 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 13 | 14 | 15 | requirements = read("requirements.txt").split() 16 | 17 | 18 | setup( 19 | name="syft-tensorflow", 20 | version="0.1.0", 21 | author="OpenMined", 22 | author_email="contact@openmined.org", 23 | description=("TensorFlow Bindings for PySyft"), 24 | license="Apache-2.0", 25 | keywords="deep learning artificial intelligence privacy secure " + 26 | "multi-party computation federated learning differential privacy", 27 | packages=find_packages(exclude=["docs", "examples", "dist"]), 28 | include_package_data=True, 29 | long_description=read("README.md"), 30 | long_description_content_type="text/markdown", 31 | url="https://github.com/OpenMined/PySyft-TensorFlow", 32 | install_requires=requirements, 33 | setup_requires=["pytest-runner"], 34 | tests_require=["pytest", "pytest-flake8"], 35 | classifiers=[ 36 | "Programming Language :: Python :: 3", 37 | "Operating System :: OS Independent", 38 | ], 39 | ) 40 | -------------------------------------------------------------------------------- /syft_tensorflow/__init__.py: -------------------------------------------------------------------------------- 1 | import syft 2 | from syft_tensorflow.hook import TensorFlowHook 3 | 4 | setattr(syft, "TensorFlowHook", TensorFlowHook) 5 | -------------------------------------------------------------------------------- /syft_tensorflow/attributes/__init__.py: -------------------------------------------------------------------------------- 1 | from syft_tensorflow.attributes.attributes import TensorFlowAttributes 2 | -------------------------------------------------------------------------------- /syft_tensorflow/attributes/attributes.py: -------------------------------------------------------------------------------- 1 | import re 2 | from types import ModuleType 3 | import typing 4 | 5 | from tensorflow.python.framework.ops import EagerTensor 6 | from syft.generic.frameworks.attributes import FrameworkAttributes 7 | 8 | from syft_tensorflow.syft_types import TensorFlowTensor 9 | 10 | if typing.TYPE_CHECKING: 11 | from syft_tensorflow.hook import TensorFlowHook 12 | 13 | 14 | class TensorFlowAttributes(FrameworkAttributes): 15 | """Adds tensorflow module related custom attributes. 16 | 17 | TensorFlowAttributes is a special class where all custom attributes related 18 | to the tensorflow module can be added. Any global parameter, configuration, 19 | or reference relating to TensorFlow should be stored here instead of 20 | attaching it directly to some other part of the global namespace. 21 | 22 | The main reason we need this is because the hooking process occasionally 23 | needs to save global objects, notably including what methods to hook and 24 | what methods to NOT hook. 25 | 26 | This will hold all necessary attributes PySyft needs. 27 | 28 | Args: 29 | tensorflow: A ModuleType indicating the tensorflow module 30 | hook: A TensorFlowHook to stash 31 | """ 32 | 33 | ALIAS = "tensorflow" 34 | Tensor = TensorFlowTensor 35 | 36 | def __init__(self, tensorflow: ModuleType, hook: "TensorFlowHook"): 37 | super().__init__(tensorflow, hook) 38 | # Stash the hook here for global access elsewhere 39 | self.hook = hook 40 | 41 | # List modules that we will hook 42 | self.tensorflow_modules = { 43 | "tensorflow": tensorflow, 44 | "tensorflow.keras.activations": tensorflow.keras.activations, 45 | "tensorflow.math": tensorflow.math, 46 | # "tensorflow.keras": tensorflow.keras, 47 | } 48 | 49 | # Set of all function names with module as prefix in the modules to hook 50 | self._tensorflow_modules_functions = { 51 | f"{module_name}.{func_name}" 52 | for module_name, tensorflow_module 53 | in self.tensorflow_modules.items() 54 | 55 | for func_name in dir(tensorflow_module) 56 | } 57 | 58 | # Store reference to all tf functions by string name 59 | # stored in tensorflow_modules_functions 60 | self.eval_tensorflow_modules_functions = { 61 | f"{module_name}.{func_name}": getattr(tensorflow_module, func_name) 62 | for module_name, tensorflow_module 63 | in self.tensorflow_modules.items() 64 | 65 | for func_name in dir(tensorflow_module) 66 | } 67 | 68 | # Add special functions to exclude from the hook 69 | # **in alphabetical order** 70 | # Reasons can be: 71 | # - Used for internal process like printing tensors 72 | # - Don't use tensors so are bound to have local executions 73 | # - etc 74 | # DON'T put here: 75 | # - functions like native_* 76 | # - functions that could use pointers or syft tensors 77 | self.exclude = [] 78 | 79 | # SECTION: List all TensorFlow tensor methods we want to overload 80 | self.tensor_types = [tensorflow.Tensor, tensorflow.Variable] 81 | 82 | # SECTION: Build the guard, that define which 83 | # functions or methods can be safely called by 84 | # external or local workers 85 | 86 | # Add all tensor types 87 | self.guard = { 88 | "Tensor": tensorflow.Tensor, 89 | "Variable": tensorflow.Variable, 90 | "EagerTensor": EagerTensor, 91 | } 92 | 93 | # Allow the `syft.` prefix to be used 94 | keys = list(self.guard.keys()) 95 | for key in keys: 96 | self.guard[f"syft.{key}"] = self.guard[key] 97 | 98 | # Concatenate TensorFlow functions and TensorFlow methods 99 | self.allowed_commands = self._tensorflow_modules_functions 100 | 101 | # The equivalent concatenation of native TensorFlow function 102 | # names and native TensorFlow method names 103 | self.native_commands = { 104 | command_name: self.get_native_framework_name(command_name) 105 | for command_name in self.allowed_commands 106 | } 107 | 108 | self.command_guard = self._command_guard 109 | 110 | self.inplace_re_pattern = re.compile("(assign)_?") 111 | self.inplace_methods = {} 112 | 113 | 114 | def is_inplace_method(self, method_name): 115 | try: 116 | return self.inplace_methods[method_name] 117 | except KeyError: 118 | match = re.match(self.inplace_re_pattern, method_name) 119 | if match is None: 120 | return False 121 | is_inplace = match.group(1) 122 | self.inplace_methods[method_name] = is_inplace 123 | return is_inplace 124 | -------------------------------------------------------------------------------- /syft_tensorflow/hook/__init__.py: -------------------------------------------------------------------------------- 1 | import syft_tensorflow.hook.hook_args 2 | from syft_tensorflow.hook.hook import TensorFlowHook 3 | -------------------------------------------------------------------------------- /syft_tensorflow/hook/hook.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import logging 3 | import types 4 | 5 | import tensorflow as tf 6 | from tensorflow.python.framework.ops import Tensor 7 | from tensorflow.python.ops.resource_variable_ops import ResourceVariable 8 | 9 | import syft 10 | from syft.workers.base import BaseWorker 11 | from syft.workers.virtual import VirtualWorker 12 | from syft.generic.frameworks.hook.hook import FrameworkHook 13 | from syft.generic.object import initialize_object 14 | 15 | from syft_tensorflow.attributes import TensorFlowAttributes 16 | from syft_tensorflow.syft_types import TensorFlowTensor 17 | from syft_tensorflow.syft_types import TensorFlowVariable 18 | from syft_tensorflow.syft_types import KerasLayer 19 | from syft_tensorflow.syft_types import KerasModel 20 | 21 | 22 | class TensorFlowHook(FrameworkHook): 23 | def __init__( 24 | self, 25 | tensorflow, 26 | local_worker: BaseWorker = None, 27 | is_client: bool = True 28 | ): 29 | 30 | self.tensorflow = tensorflow 31 | self.framework = self.tensorflow 32 | 33 | syft.tensorflow = TensorFlowAttributes(tf, self) 34 | 35 | syft.framework = syft.tensorflow 36 | syft.tensorflow.hook = self 37 | syft.hook = self 38 | 39 | self.local_worker = local_worker 40 | 41 | if hasattr(tensorflow, "tf_hooked"): 42 | logging.warning("TF was already hooked, skipping hooking process") 43 | self.local_worker = syft.local_worker 44 | return 45 | else: 46 | tensorflow.tf_hooked = True 47 | 48 | if self.local_worker is None: 49 | # Every TensorFlowHook instance should have a local worker which is 50 | # responsible for interfacing with other workers. The worker 51 | # interface is what allows the TensorFlow specific code in TensorFlowHook to 52 | # be agnostic to the means by which workers communicate (such as 53 | # peer-to-peer, sockets, through local ports, or all within the 54 | # same process) 55 | self.local_worker = VirtualWorker( 56 | hook=self, is_client_worker=is_client, id="me" 57 | ) 58 | else: 59 | self.local_worker.hook = self 60 | 61 | self.to_auto_overload = { 62 | tf.math: ["add"], 63 | Tensor: self._which_methods_should_we_auto_overload( 64 | Tensor 65 | ), 66 | 67 | tf.Variable: self._which_methods_should_we_auto_overload( 68 | tf.Variable 69 | ), 70 | 71 | tf.keras.layers.Layer: self._which_methods_should_we_auto_overload( 72 | tf.keras.layers.Layer 73 | ), 74 | 75 | tf.keras.models.Model: self._which_methods_should_we_auto_overload( 76 | tf.keras.models.Model 77 | ), 78 | 79 | ResourceVariable: self._which_methods_should_we_auto_overload( 80 | ResourceVariable 81 | ), 82 | } 83 | 84 | self.args_hook_for_overloaded_attr = {} 85 | 86 | self._hook_native_tensor(Tensor, TensorFlowTensor) 87 | self._hook_variable(TensorFlowVariable) 88 | 89 | self._hook_keras_layers(tf.keras.layers.Layer, KerasLayer) 90 | self._hook_keras_model(tf.keras.models.Model, KerasModel) 91 | 92 | self._hook_pointer_tensor_methods(Tensor) 93 | self._hook_pointer_tensor_methods(tf.Variable) 94 | self._hook_pointer_tensor_methods(ResourceVariable) 95 | 96 | self._hook_pointer_tensor_methods(tf.math) 97 | self._hook_multi_pointer_tensor_methods(tf.math) 98 | 99 | self._hook_object_pointer_methods(tf.keras.layers.Layer) 100 | self._hook_object_pointer_methods(tf.keras.models.Model) 101 | 102 | self._hook_tensorflow_module() 103 | 104 | syft.local_worker = self.local_worker 105 | syft.hook = self 106 | 107 | # This must happen last! 108 | # See this functions documentation for more info. 109 | self._add_methods_to_eager_tensor() 110 | 111 | def _hook_native_tensor(self, tensor_type: type, syft_type: type): 112 | """Adds PySyft Tensor Functionality to the given native tensor type. 113 | Overloads the given native TensorFlow tensor to add PySyft Tensor 114 | Functionality. Overloading involves modifying the tensor type with 115 | PySyft's added functionality. You may read about what kind of 116 | modifications are made in the methods that this method calls. 117 | Args: 118 | tensor_type: The type of tensor being hooked (in this refactor 119 | this is only ever tf.Tensor, but in previous versions of 120 | PySyft this iterated over all tensor types. 121 | syft_type: The abstract type whose methods should all be added to 122 | the tensor_type class. In practice this is always TensorFlowTensor. 123 | Read more about it there. 124 | """ 125 | # Reinitialize init method of TensorFlow tensor with Syft init 126 | self._add_registration_to___init__(tensor_type) 127 | 128 | # Overload TensorFlow tensor properties with Syft properties 129 | self._hook_tensor_properties(tensor_type) 130 | 131 | # Overload auto overloaded with TensorFlow methods 132 | exclude = [ 133 | "__class__", 134 | "__delattr__", 135 | "__dir__", 136 | "__doc__", 137 | "__dict__", 138 | "__format__", 139 | "__getattribute__", 140 | "__hash__", 141 | "__init__", 142 | "__init_subclass__", 143 | "__weakref__", 144 | "__ne__", 145 | "__new__", 146 | "__reduce__", 147 | "__reduce_ex__", 148 | "__setattr__", 149 | "__sizeof__", 150 | "__subclasshook__", 151 | "__eq__", 152 | "__gt__", 153 | "__ge__", 154 | "__lt__", 155 | "__le__", 156 | ] 157 | self._transfer_methods_to_framework_class(tensor_type, syft_type, exclude) 158 | 159 | self._hook_native_methods(tensor_type) 160 | 161 | def _hook_keras_layers(self, layer_cls: type, from_cls: type): 162 | 163 | # Reinitialize init method of the Keras object with Syft init 164 | self._add_registration_to___init__(layer_cls) 165 | 166 | # Overload Keras object properties with Syft properties 167 | self._hook_keras_properties(layer_cls) 168 | 169 | # Overload auto overloaded with Keras methods 170 | exclude = [ 171 | "__class__", 172 | "__dir__", 173 | "__doc__", 174 | "__dict__", 175 | "__format__", 176 | "__getattribute__", 177 | "__hash__", 178 | "__init__", 179 | "__init_subclass__", 180 | "__weakref__", 181 | "__new__", 182 | "__reduce__", 183 | "__reduce_ex__", 184 | "__setattr__", 185 | "__sizeof__", 186 | "__subclasshook__", 187 | ] 188 | self._transfer_methods_to_framework_class(layer_cls, from_cls, exclude) 189 | 190 | self._hook_keras_methods(layer_cls) 191 | 192 | def _hook_keras_model(self, model_cls: type, from_cls: type): 193 | 194 | # Overload the Keras object properties with Syft properties 195 | self._hook_keras_properties(model_cls) 196 | 197 | # Overload auto overloaded with Keras methods 198 | exclude = [ 199 | "__class__", 200 | "__dir__", 201 | "__doc__", 202 | "__dict__", 203 | "__format__", 204 | "__getattribute__", 205 | "__hash__", 206 | "__init__", 207 | "__init_subclass__", 208 | "__weakref__", 209 | "__new__", 210 | "__reduce__", 211 | "__reduce_ex__", 212 | "__setattr__", 213 | "__sizeof__", 214 | "__subclasshook__", 215 | "__str__", 216 | "__repr__", 217 | ] 218 | self._transfer_methods_to_framework_class(model_cls, from_cls, exclude) 219 | 220 | self._hook_keras_methods(model_cls) 221 | 222 | def _hook_variable(self, syft_type: type): 223 | """Adds PySyft Tensor functionality to tf.Variable. 224 | 225 | In practice, the user is generally working with subclasses of 226 | tf.Variable, e.g. ResourceVariable, so we hook methods for those and 227 | only override the tf.Variable constructor to provide syft registration. 228 | You may read about what kind of modifications are made in the methods 229 | that this method calls. 230 | 231 | Args: 232 | syft_type: The abstract type whose methods should all be added to 233 | the ResourceVariable class. 234 | """ 235 | # Reinitialize init method of Torch tensor with Syft init 236 | self._add_registration_to___init__(tf.Variable) 237 | 238 | # Overload Torch tensor properties with Syft properties 239 | self._hook_properties(tf.Variable) 240 | 241 | # Overload auto overloaded with Torch methods 242 | exclude = [ 243 | "__class__", 244 | "__delattr__", 245 | "__dict__", 246 | "__dir__", 247 | "__doc__", 248 | "__format__", 249 | "__getattribute__", 250 | "__hash__", 251 | "__init__", 252 | "__init_subclass__", 253 | "__weakref__", 254 | "__module__", 255 | "__ne__", 256 | "__new__", 257 | "__reduce__", 258 | "__reduce_ex__", 259 | "__setattr__", 260 | "__sizeof__", 261 | "__subclasshook__", 262 | ] 263 | self._transfer_methods_to_framework_class(ResourceVariable, syft_type, exclude) 264 | self._hook_properties(ResourceVariable) 265 | self._hook_native_methods(ResourceVariable) 266 | 267 | def _hook_tensorflow_module(self): 268 | tensorflow_modules = syft.tensorflow.tensorflow_modules 269 | 270 | for module_name, tensorflow_module in tensorflow_modules.items(): 271 | for func in dir(tensorflow_module): 272 | 273 | # Some functions we want to ignore (not override). Such functions have been hard 274 | # coded into the tensorflow_attribute exclude (see TensorFlowAttribute class) 275 | if func in syft.tensorflow.exclude: 276 | continue 277 | 278 | # ignore dunder functions 279 | if "__" in func: 280 | continue 281 | 282 | # ignore capitalized func values which are Classes not functinos 283 | if func[0].isupper(): 284 | continue 285 | 286 | # ignore hidden functins 287 | if func[0] == "_": 288 | continue 289 | 290 | # If we haven't already overloaded this function 291 | if "native_" in func or f"native_{func}" in dir(tensorflow_module): 292 | continue 293 | 294 | self._perform_function_overloading(module_name, tensorflow_module, func) 295 | 296 | def _add_registration_to___init__( 297 | hook_self, tensor_type: type, is_tensor: bool = False 298 | ): 299 | """Adds several attributes to the tensor. 300 | Overloads tensor_type.__init__ to add several attributes to the tensor 301 | as well as registering the tensor automatically. 302 | Args: 303 | tensor_type: The type of tensor being hooked (in this refactor this 304 | is only ever tf.Tensor, but in previous versions of PySyft 305 | this iterated over all tensor types. 306 | is_tensor: An optional boolean parameter (default False) to 307 | specify whether to skip running the native initialization 308 | logic. 309 | """ 310 | 311 | def new___init__(self, *args, owner=None, id=None, register=True, **kwargs): 312 | return initialize_object( 313 | hook=hook_self, 314 | obj=self, 315 | reinitialize=not is_tensor, 316 | owner=owner, 317 | id=id, 318 | init_args=args, 319 | init_kwargs=kwargs, 320 | ) 321 | 322 | if "native___init__" not in dir(tensor_type): 323 | tensor_type.native___init__ = tensor_type.__init__ 324 | 325 | tensor_type.__init__ = new___init__ 326 | 327 | def _hook_keras_properties(hook_self, keras_type: type): 328 | super()._hook_properties(keras_type) 329 | 330 | def _hook_tensor_properties(hook_self, tensor_type: type): 331 | super()._hook_properties(tensor_type) 332 | tensor_type.native_shape = tensor_type.shape 333 | 334 | def _add_methods_to_eager_tensor(self): 335 | """ 336 | Add required TensorFlowTensor methods to EagerTensor. 337 | 338 | When a user creates a tensor, e.g. with `tf.constant([1,2,3])`, the 339 | type of the returned object is an `EagerTensor`. EagerTensor is defined 340 | in tensorflow and is a super class of tf.Tensor. However, EagerTensor is 341 | not importable so we cannot add the properties to it that we want. 342 | So we do that here, which requires us to instantiate an instance of an 343 | EagerTensor to then get reference to the type. 344 | 345 | We do it this way for 2 reasons: 346 | 347 | 1. Avoid monkeypatching functions per instance (e.g. having to overwrite the 348 | function every time we make a tensor) 349 | 2. Most dunder methods are actually looked up on the class itself, rather 350 | than the instance, so monkeypatching the instance doesn't even work for 351 | things like __repr__. 352 | """ 353 | 354 | dummy = tf.constant(0) 355 | eager_type = type(dummy) 356 | 357 | eager_type.native___str__ = eager_type.__str__ 358 | eager_type.native___repr__ = eager_type.__repr__ 359 | 360 | eager_type.__repr__ = TensorFlowTensor.__repr__ 361 | eager_type.__str__ = TensorFlowTensor.__str__ 362 | 363 | for method in self.to_auto_overload[tf.math]: 364 | setattr(eager_type, method, getattr(tf, method)) 365 | 366 | def _hook_keras_methods(self, keras_type: type): 367 | """ 368 | Add hooked version of all methods of to_auto_overload[keras_type] 369 | to the keras_type; instead of performing the native keras object 370 | method, the hooked version will be called 371 | 372 | Args: 373 | keras_type: the keras_type which holds the methods 374 | """ 375 | 376 | for attr in self.to_auto_overload[keras_type]: 377 | # if we haven't already overloaded this function 378 | if f"native_{attr}" not in dir(keras_type): 379 | native_method = getattr(keras_type, attr) 380 | setattr(keras_type, f"native_{attr}", native_method) 381 | new_method = self._get_hooked_method(attr) 382 | setattr(keras_type, attr, new_method) 383 | 384 | @classmethod 385 | def create_shape(cls, shape_dims): 386 | return tf.TensorShape(shape_dims) 387 | 388 | @classmethod 389 | def create_zeros(shape, dtype, **kwargs): 390 | return tf.zeros(shape, dtype=dtype, **kwargs) 391 | -------------------------------------------------------------------------------- /syft_tensorflow/hook/hook_args.py: -------------------------------------------------------------------------------- 1 | """Hook args implementation for TensorFLow. 2 | Implements and registers hook_args functionality for syft-tensorflow objects. 3 | See syft/generic/frameworks/hook/hook_args.py for the core implementation. 4 | """ 5 | 6 | import tensorflow as tf 7 | import numpy as np 8 | 9 | from tensorflow.python.framework.ops import EagerTensor 10 | from syft.exceptions import PureFrameworkTensorFoundError 11 | from tensorflow.python.ops.resource_variable_ops import ResourceVariable 12 | from syft.generic.frameworks.hook.hook_args import ( 13 | register_ambiguous_method, 14 | register_backward_func, 15 | register_forward_func, 16 | register_type_rule, 17 | one, 18 | ) 19 | 20 | from syft_tensorflow.syft_types import TensorFlowTensor 21 | 22 | 23 | type_rule = { 24 | tf.Tensor: one, 25 | tf.Variable: one, 26 | TensorFlowTensor: one, 27 | EagerTensor: one, 28 | ResourceVariable: one, 29 | np.ndarray: lambda x: 0, 30 | tf.keras.layers.Layer: one, 31 | tf.keras.models.Model:one, 32 | } 33 | 34 | 35 | def default_forward(i): 36 | if hasattr(i, "child"): 37 | return i.child 38 | 39 | return (_ for _ in ()).throw(PureFrameworkTensorFoundError) 40 | 41 | 42 | forward_func = { 43 | tf.Tensor: default_forward, 44 | tf.Variable: default_forward, 45 | ResourceVariable: default_forward, 46 | EagerTensor: default_forward, 47 | tf.keras.layers.Layer: default_forward, 48 | tf.keras.models.Model: default_forward, 49 | 50 | } 51 | backward_func = { 52 | tf.Tensor: lambda i: i.wrap(type=tf.constant, value=[]), 53 | tf.Variable: lambda i: i.wrap(type=tf.Variable, initial_value=[]), 54 | ResourceVariable: lambda i: i.wrap(type=tf.Variable, initial_value=[]), 55 | EagerTensor: lambda i: i.wrap(type=tf.constant, value=[]), 56 | tf.keras.layers.Layer: lambda i: i.wrap(), 57 | tf.keras.models.Model: lambda i: i.wrap(), 58 | } 59 | ambiguous_methods = {"__getitem__", "__setitem__"} 60 | 61 | register_ambiguous_method(*ambiguous_methods) 62 | register_type_rule(type_rule) 63 | register_forward_func(forward_func) 64 | register_backward_func(backward_func) 65 | -------------------------------------------------------------------------------- /syft_tensorflow/keras/tf_keras_fun_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import tensorflow as tf 3 | 4 | import syft 5 | 6 | 7 | def test_keras_activations_fn(remote): 8 | 9 | x_to_give = tf.constant([-2.0, 3.0, 5.0]) 10 | expected = tf.keras.activations.relu(x_to_give) 11 | 12 | x_ptr = x_to_give.send(remote) 13 | 14 | relu_ptr = tf.keras.activations.relu(x_ptr) 15 | actual = relu_ptr.get() 16 | 17 | assert tf.math.equal(actual, expected).numpy().all() 18 | 19 | def test_keras_sigmoid(remote): 20 | 21 | expected = tf.keras.activations.sigmoid(tf.constant([1.0, 1.0])) 22 | 23 | x = tf.constant([1.0, 1.0]).send(remote) 24 | y = tf.keras.activations.sigmoid(x).get() 25 | 26 | assert tf.math.equal(y, expected).numpy().all() 27 | -------------------------------------------------------------------------------- /syft_tensorflow/serde/__init__.py: -------------------------------------------------------------------------------- 1 | from syft_tensorflow.serde.serde import MAP_TF_SIMPLIFIERS_AND_DETAILERS 2 | -------------------------------------------------------------------------------- /syft_tensorflow/serde/serde.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | import io 3 | import os 4 | from tempfile import TemporaryDirectory 5 | import zipfile 6 | 7 | import syft 8 | from syft.generic.object import initialize_object 9 | from syft.generic.tensor import initialize_tensor 10 | import tensorflow as tf 11 | from tensorflow.python.framework.ops import EagerTensor 12 | from tensorflow.python.ops.resource_variable_ops import ResourceVariable 13 | 14 | 15 | def _simplify_tf_tensor(tensor: tf.Tensor) -> bin: 16 | """ 17 | This function converts a TF tensor into a serialized TF tensor using 18 | tf.io. We do this because it's native to TF, and they've optimized it. 19 | 20 | Args: 21 | tensor (tf.Tensor): an input tensor to be serialized 22 | 23 | Returns: 24 | tuple: serialized tuple of TensorFlow tensor. The first value is the 25 | id of the tensor and the second is the binary for the TensorFlow 26 | object. The third is the tensor dtype and the fourth is the chain 27 | of abstractions. 28 | """ 29 | 30 | tensor_ser = tf.io.serialize_tensor(tensor) 31 | 32 | dtype_ser = syft.serde._simplify(tensor.dtype) 33 | 34 | chain = None 35 | if hasattr(tensor, "child"): 36 | chain = syft.serde._simplify(tensor.child) 37 | 38 | return tensor.id, tensor_ser.numpy(), dtype_ser, chain 39 | 40 | 41 | def _detail_tf_tensor(worker, tensor_tuple) -> tf.Tensor: 42 | """ 43 | This function converts a serialized tf tensor into a local TF tensor 44 | using tf.io. 45 | 46 | Args: 47 | tensor_tuple (bin): serialized obj of TF tensor. It's a tuple where 48 | the first value is the ID, the second vlaue is the binary for the 49 | TensorFlow object, the third value is the tensor_dtype_enum, and 50 | the fourth value is the chain of tensor abstractions 51 | 52 | Returns: 53 | tf.Tensor: a deserialized TF tensor 54 | """ 55 | 56 | tensor_id, tensor_bin, tensor_dtype_enum, chain = tensor_tuple 57 | 58 | tensor_dtype = syft.serde._detail(worker, tensor_dtype_enum) 59 | tensor = tf.io.parse_tensor(tensor_bin, tensor_dtype) 60 | 61 | initialize_tensor( 62 | hook=syft.tensorflow.hook, 63 | obj=tensor, 64 | owner=worker, 65 | id=tensor_id, 66 | init_args=[], 67 | init_kwargs={}, 68 | ) 69 | 70 | if chain is not None: 71 | chain = syft.serde._detail(worker, chain) 72 | tensor.child = chain 73 | tensor.is_wrapper = True 74 | 75 | return tensor 76 | 77 | 78 | def _simplify_tf_variable(tensor: tf.Variable) -> bin: 79 | """ 80 | This function converts a TF variable into a serialized TF variable using 81 | tf.io. We do this because it's native to TF, and they've optimized it. 82 | 83 | Args: 84 | tensor (tf.Variable): an input variable to be serialized 85 | 86 | Returns: 87 | tuple: serialized tuple of TensorFlow tensor. The first value is the 88 | id of the tensor and the second is the binary for the TensorFlow 89 | object. The third is the tensor dtype and the fourth is the chain 90 | of abstractions. 91 | """ 92 | 93 | tensor_ser = tf.io.serialize_tensor(tensor) 94 | 95 | dtype_ser = syft.serde._simplify(tensor.dtype) 96 | 97 | chain = None 98 | if hasattr(tensor, "child"): 99 | chain = syft.serde._simplify(tensor.child) 100 | 101 | return tensor.id, tensor_ser.numpy(), dtype_ser, chain 102 | 103 | 104 | def _detail_tf_variable(worker, tensor_tuple) -> tf.Tensor: 105 | """ 106 | This function converts a serialized TF variable into a local TF tensor 107 | using tf.io. 108 | 109 | Args: 110 | tensor_tuple (bin): serialized obj of TF variable. It's a tuple where 111 | the first value is the ID, the second vlaue is the binary for the 112 | TensorFlow object, the third value is the tensor_dtype_enum, and 113 | the fourth value is the chain of tensor abstractions 114 | 115 | Returns: 116 | tf.Tensor: a deserialized TF tensor 117 | """ 118 | 119 | tensor_id, tensor_bin, tensor_dtype_enum, chain = tensor_tuple 120 | 121 | tensor_dtype = syft.serde._detail(worker, tensor_dtype_enum) 122 | tensor = tf.io.parse_tensor(tensor_bin, tensor_dtype) 123 | tensor = tf.Variable(tensor) 124 | 125 | initialize_tensor( 126 | hook=syft.tensorflow.hook, 127 | obj=tensor, 128 | owner=worker, 129 | id=tensor_id, 130 | init_args=[], 131 | init_kwargs={}, 132 | ) 133 | 134 | if chain is not None: 135 | chain = syft.serde._detail(worker, chain) 136 | tensor.child = chain 137 | tensor.is_wrapper = True 138 | 139 | return tensor 140 | 141 | 142 | def _simplify_tf_keras_layers(layer: tf.keras.layers.Layer) -> bin: 143 | """ 144 | This function converts a keras layer into a serialized keras layer using 145 | keras serialize. 146 | 147 | Args: 148 | layer (tf.keras.layers.Layer): an input tensor to be serialized 149 | 150 | Returns: 151 | tuple: serialized tuple of Layer class. The first value is the 152 | id of the layer and the second is the layer configuration 153 | object. The third is the layer weights and the fourth is the 154 | batch input shape. 155 | """ 156 | 157 | layer_ser = tf.keras.layers.serialize(layer) 158 | 159 | weights = layer.get_weights() 160 | weights_ser = syft.serde.serde._simplify(weights) 161 | 162 | layer_dict_ser = syft.serde.serde._simplify(layer_ser) 163 | 164 | batch_input_shape_ser = syft.serde.serde._simplify( 165 | layer_ser["config"]["batch_input_shape"] 166 | ) 167 | 168 | return layer.id, layer_dict_ser, weights_ser, batch_input_shape_ser 169 | 170 | 171 | def _detail_tf_keras_layers(worker, layer_tuple) -> tf.Tensor: 172 | """ 173 | This function converts a serialized keras layer into a local keras layer 174 | 175 | Args: 176 | layer_tuple (bin): serialized obj of TF layer. It's a tuple where 177 | the first value is the ID, the second value is the binary for the 178 | layer object, the third value is the layer weights, and 179 | the fourth value is the batch input shape. 180 | 181 | Returns: 182 | tf.Tensor: a deserialized TF tensor 183 | """ 184 | 185 | layer_id, layer_bin, weights_bin, batch_input_shape_bin = layer_tuple 186 | 187 | layer_dict = syft.serde.serde._detail(worker, layer_bin) 188 | 189 | layer = tf.keras.layers.deserialize(layer_dict) 190 | 191 | weights = syft.serde.serde._detail(worker, weights_bin) 192 | 193 | batch_input_shape = syft.serde.serde._detail(worker, batch_input_shape_bin) 194 | 195 | layer.build(batch_input_shape) 196 | 197 | layer.set_weights(weights) 198 | 199 | initialize_object( 200 | hook=syft.tensorflow.hook, 201 | obj=layer, 202 | owner=worker, 203 | reinitialize=False, 204 | id=layer_id, 205 | init_args=[], 206 | init_kwargs={}, 207 | ) 208 | 209 | return layer 210 | 211 | 212 | def _simplify_keras_model(model: tf.keras.Model): 213 | """ 214 | This function converts a model into a serialized saved_model. 215 | 216 | Args: 217 | model (tf.keras.Model: Keras model 218 | 219 | Returns: 220 | tuple: serialized tuple of Keras model. The first value is 221 | the binary of the model. The second is the model id. 222 | """ 223 | bio = io.BytesIO() 224 | 225 | with TemporaryDirectory() as model_location: 226 | tf.keras.models.save_model( 227 | model, 228 | model_location, 229 | overwrite=True, 230 | include_optimizer=True, 231 | save_format='tf', 232 | ) 233 | 234 | with zipfile.ZipFile(bio, 'w', zipfile.ZIP_DEFLATED) as model_file: 235 | # TODO: This process is way inefficient. Find a faster way to serde 236 | # SavedModels. See commit b55195a2d4b6852642f987a5a4105167120d123b 237 | # for an h5py implementation (which is less general, but clearly 238 | # more efficient) 239 | for root, dir, files in os.walk(model_location): 240 | relroot = os.path.relpath(root, start=model_location) 241 | for file in files: 242 | filename = os.path.join(root, file) 243 | arcname = os.path.join(relroot, file) 244 | model_file.write(filename, arcname=arcname) 245 | 246 | model_ser = bio.getvalue() 247 | 248 | return model_ser, model.id 249 | 250 | 251 | def _detail_keras_model(worker, model_tuple): 252 | """ 253 | This function converts a serialized model into a local 254 | model. 255 | 256 | Args: 257 | modeltuple (bin): serialized obj of Keras model. 258 | It's a tuple where the first value is the binary of the model. 259 | The second is the model id. 260 | 261 | Returns: 262 | tf.keras.models.Model: a deserialized Keras model 263 | """ 264 | model_ser, model_id = model_tuple 265 | bio = io.BytesIO(model_ser) 266 | with TemporaryDirectory() as model_location: 267 | with zipfile.ZipFile(bio, 'r', zipfile.ZIP_DEFLATED) as model_file: 268 | # WARNING: zipped archives can potentially deposit extra files onto 269 | # the system, although Python's zipfile offers some protection 270 | # more info: https://docs.python.org/3/library/zipfile.html#zipfile.ZipFile.extractall 271 | # TODO: further investigate security, find better option if needed 272 | model_file.extractall(model_location) 273 | model = tf.keras.models.load_model(model_location) 274 | 275 | initialize_object( 276 | hook=syft.tensorflow.hook, 277 | obj=model, 278 | owner=worker, 279 | reinitialize=False, 280 | id=model_id, 281 | init_args=[], 282 | init_kwargs={}, 283 | ) 284 | 285 | return model 286 | 287 | 288 | def _simplify_keras_history_callback( 289 | history_cb: tf.keras.callbacks.History 290 | ) -> bin: 291 | """ 292 | This function converts history callback into serialized dictionaries. 293 | 294 | Args: 295 | history_cb (tf.keras.callbacks.History): history callback 296 | 297 | Returns: 298 | tuple: serialized tuple of history callback. The first value is 299 | the binary of the params dictionary. The second is the binary 300 | of the history dictionary. 301 | """ 302 | 303 | params = history_cb.params 304 | history = history_cb.history 305 | 306 | params_ser = syft.serde.serde._simplify(params) 307 | history_ser = syft.serde.serde._simplify(history) 308 | 309 | return params_ser, history_ser 310 | 311 | 312 | def _detail_keras_history_callback( 313 | worker, 314 | history_cb_tuple 315 | ) -> tf.keras.callbacks.History: 316 | """ 317 | This function converts a serialized history callback into an 318 | history callback. 319 | 320 | Args: 321 | history_cb_tuple (bin): serialized obj of history callback. 322 | It's a tuple where the first value is the binary of the params 323 | dictionary. The second is the binary of the history dictionary. 324 | 325 | Returns: 326 | tf.keras.callbacks.History: a deserialized history callback 327 | """ 328 | 329 | params_bin, history_bin = history_cb_tuple 330 | 331 | params = syft.serde.serde._detail(worker, params_bin) 332 | history = syft.serde.serde._detail(worker, history_bin) 333 | 334 | history_cb = tf.keras.callbacks.History() 335 | history_cb.set_params(params) 336 | history_cb.history = history 337 | 338 | return history_cb 339 | 340 | 341 | def _simplify_tf_dtype(dtype: tf.dtypes.DType) -> bin: 342 | 343 | return dtype.as_datatype_enum, None 344 | 345 | 346 | def _detail_tf_dtype(worker, dtype_tuple): 347 | dtype_enum, _ = dtype_tuple 348 | 349 | dtype = tf.dtypes.DType(dtype_enum) 350 | 351 | return dtype 352 | 353 | 354 | def _simplify_tf_tensorshape(tensorshape: tf.TensorShape) -> bin: 355 | """ 356 | This function converts a TF tensor shape into a serialized list. 357 | 358 | Args: 359 | tensor (tf.TensorShape): an input tensor shape to be serialized 360 | 361 | Returns: 362 | tuple: serialized tuple of TF tensor shape. The first value is 363 | the binary for the TensorShape object. The second is the 364 | chain of abstractions. 365 | """ 366 | 367 | tensorshape_list_ser = syft.serde.serde._simplify(tensorshape.as_list()) 368 | 369 | # TODO[Yann] currently this condition is never true, 370 | # tf.TensorShape needs to be hooked 371 | chain = None 372 | if hasattr(tensorshape, "child"): 373 | chain = syft.serde._simplify(tensorshape.child) 374 | 375 | return tensorshape_list_ser, chain 376 | 377 | 378 | def _detail_tf_tensorshape(worker, tensor_tuple) -> tf.TensorShape: 379 | """ 380 | This function converts a serialized TF tensor shape into a local list. 381 | 382 | Args: 383 | tensor_tuple (bin): serialized obj of TF tensor shape as list. 384 | It's a tuple where the first value is the binary for the 385 | tensorflow shape (as list), and the third value is the 386 | chain of tensor abstractions. 387 | 388 | Returns: 389 | tf.Tensor: a deserialized TF tensor 390 | """ 391 | 392 | tensorshape_list_bin, chain = tensor_tuple 393 | 394 | tensorshape_list = syft.serde.serde._detail(worker, tensorshape_list_bin) 395 | 396 | tensorshape = tf.TensorShape(tensorshape_list) 397 | 398 | # TODO[Yann] currently this condition is never true, 399 | # tf.TensorShape needs to be hooked 400 | if chain is not None: 401 | chain = syft.serde._detail(worker, chain) 402 | tensorshape.child = chain 403 | tensorshape.is_wrapper = True 404 | 405 | return tensorshape 406 | 407 | 408 | MAP_TF_SIMPLIFIERS_AND_DETAILERS = OrderedDict( 409 | { 410 | EagerTensor: (_simplify_tf_tensor, _detail_tf_tensor), 411 | tf.Tensor: (_simplify_tf_tensor, _detail_tf_tensor), 412 | tf.TensorShape: (_simplify_tf_tensorshape, _detail_tf_tensorshape), 413 | tf.Variable: (_simplify_tf_variable, _detail_tf_variable), 414 | tf.keras.layers.Layer: (_simplify_tf_keras_layers, _detail_tf_keras_layers), 415 | tf.keras.models.Model: (_simplify_keras_model, _detail_keras_model), 416 | ResourceVariable: (_simplify_tf_variable, _detail_tf_variable), 417 | tf.keras.callbacks.History: (_simplify_keras_history_callback, 418 | _detail_keras_history_callback), 419 | tf.dtypes.DType: (_simplify_tf_dtype, _detail_tf_dtype), 420 | } 421 | ) 422 | -------------------------------------------------------------------------------- /syft_tensorflow/serde/serde_test.py: -------------------------------------------------------------------------------- 1 | import tensorflow as tf 2 | import syft 3 | 4 | 5 | def test_serde_constant(): 6 | z = tf.constant([1.0, 2.0]) 7 | 8 | ser = syft.serde.serialize(z) 9 | x = syft.serde.deserialize(ser) 10 | 11 | assert all(tf.math.equal(x, z)) 12 | assert x.id == z.id 13 | assert x.dtype == z.dtype 14 | 15 | 16 | def test_serde_tensorshape(): 17 | z = tf.TensorShape([1, 2]) 18 | 19 | ser = syft.serde.serialize(z) 20 | x = syft.serde.deserialize(ser) 21 | 22 | assert all(tf.math.equal(x, z)) 23 | 24 | 25 | def test_serde_model(): 26 | 27 | inp = tf.keras.layers.Input(shape=(3,)) 28 | out = tf.keras.layers.Dense(2)(inp) 29 | m = tf.keras.Model(inp, out) 30 | x = tf.constant([[1, 2, 3.0]]) 31 | y = m(x) 32 | 33 | ser = syft.serde.serialize(m) 34 | m_new = syft.serde.deserialize(ser) 35 | 36 | assert tf.math.equal(y, m_new(x)).numpy().all() 37 | 38 | 39 | def test_serde_dtype(): 40 | d = tf.float32 41 | ser = syft.serde.serialize(d) 42 | x = syft.serde.deserialize(ser) 43 | assert d == x 44 | -------------------------------------------------------------------------------- /syft_tensorflow/syft_types/__init__.py: -------------------------------------------------------------------------------- 1 | from syft_tensorflow.syft_types.tensor import TensorFlowTensor 2 | from syft_tensorflow.syft_types.variable import TensorFlowVariable 3 | from syft_tensorflow.syft_types.keras_object import KerasObject 4 | from syft_tensorflow.syft_types.keras_layer import KerasLayer 5 | from syft_tensorflow.syft_types.keras_model import KerasModel 6 | 7 | -------------------------------------------------------------------------------- /syft_tensorflow/syft_types/keras_layer.py: -------------------------------------------------------------------------------- 1 | import weakref 2 | 3 | import tensorflow as tf 4 | 5 | import syft 6 | from syft.generic.frameworks.hook import hook_args 7 | from syft.generic.object import AbstractObject 8 | from syft.generic.pointers.object_pointer import ObjectPointer 9 | from syft.workers.base import BaseWorker 10 | 11 | from syft_tensorflow.syft_types import KerasObject 12 | 13 | from syft.exceptions import PureFrameworkTensorFoundError 14 | 15 | 16 | class KerasLayer(KerasObject): 17 | """Add methods to this keras layer object to have them added to every 18 | tf.keras.layers.Layer object. 19 | 20 | This Keras object is simply a more convenient way to add custom functions to 21 | all TensorFlow Keras object types. When you add a function to this keras object, 22 | it will be added to EVERY native Keras layer type (i.e. tf.keras.layers.Dense) 23 | automatically by the TensorFlowHook. 24 | 25 | Note: all methods from AbstractObject will also be included because this 26 | object extends AbstractObject. So, if you're looking for a method on 27 | the native tf.keras API but it's not listed here, you might try 28 | checking AbstractObject. 29 | """ 30 | 31 | def send( 32 | self, *location, inplace: bool = False, no_wrap=False, garbage_collect_data=True 33 | ): 34 | """Gets the pointer to a new remote object. 35 | 36 | One of the most commonly used methods in PySyft, this method serializes 37 | the object upon which it is called (self), sends the object to a remote 38 | worker, creates a pointer to that worker, and then returns that pointer 39 | from this function. 40 | 41 | Args: 42 | location: The BaseWorker object which you want to send this object 43 | to. Note that this is never actually the BaseWorker but instead 44 | a class which instantiates the BaseWorker abstraction. 45 | inplace: if true, 46 | return the same object instance, else a new wrapper 47 | no_wrap: If True, wrap() is called on the created pointer 48 | garbage_collect_data: argument passed down to create_pointer() 49 | 50 | Returns: 51 | A tf.keras.layers.Layer[ObjectPointer] pointer to self. Note that this 52 | object will likely be wrapped by a ttf.keras.layers.Layer wrapper. 53 | """ 54 | 55 | # If you send a pointer p1, you want the pointer to pointer p2 56 | # to control the garbage collection and not the remaining old 57 | # p1 (here self). Because if p2 is not GCed, GCing p1 shouldn't delete 58 | # the remote keras object, but if you want to do so, as p2 is not GCed, 59 | # you can still do `del p2`. This allows to chain multiple 60 | # .send().send() calls. 61 | 62 | if len(location) == 1: 63 | 64 | location = location[0] 65 | 66 | ptr = self.owner.send( 67 | self, location, garbage_collect_data=garbage_collect_data 68 | ) 69 | 70 | ptr.description = self.description 71 | ptr.tags = self.tags 72 | 73 | # The last pointer should control remote GC, 74 | # not the previous self.ptr 75 | if hasattr(self, "ptr") and self.ptr is not None: 76 | ptr_ = self.ptr() 77 | if ptr_ is not None: 78 | ptr_.garbage_collect_data = False 79 | 80 | # we need to cache this weak reference to the pointer so that 81 | # if this method gets called multiple times we can simply re-use 82 | # the same pointer which was previously created 83 | self.ptr = weakref.ref(ptr) 84 | 85 | if inplace: 86 | self.child = ptr 87 | return self 88 | else: 89 | output = ptr if no_wrap else ptr.wrap(type=tf.keras.layers.Layer) 90 | 91 | else: 92 | 93 | # TODO [Yann] check if we would want to send the keras 94 | # object to several workers this way. 95 | children = list() 96 | for loc in location: 97 | children.append(self.send(loc, no_wrap=True)) 98 | 99 | output = syft.MultiPointerTensor(children=children) 100 | 101 | if not no_wrap: 102 | output = output.wrap(type=tf.keras.layers.Layer) 103 | 104 | return output 105 | -------------------------------------------------------------------------------- /syft_tensorflow/syft_types/keras_layers_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import tensorflow as tf 3 | import syft 4 | import numpy as np 5 | 6 | 7 | def test_send_get_keras_layer(remote): 8 | 9 | layer_to_give = tf.keras.layers.Dense(5, input_shape=[2]) 10 | layer_to_give.build([2]) 11 | 12 | layer_ptr = layer_to_give.send(remote) 13 | layer_gotten = layer_ptr.get() 14 | 15 | assert np.array_equal(layer_to_give.get_weights()[0], 16 | layer_gotten.get_weights()[0]) 17 | 18 | 19 | def test_keras_dense_layer(remote): 20 | 21 | x_to_give = tf.random.uniform([2, 2]) 22 | layer_to_give = tf.keras.layers.Dense(2, input_shape=[2]) 23 | layer_to_give.build([2]) 24 | expected = layer_to_give(x_to_give) 25 | 26 | x_ptr = x_to_give.send(remote) 27 | layer_ptr = layer_to_give.send(remote) 28 | actual = layer_ptr(x_ptr).get() 29 | 30 | assert np.array_equal(actual, expected) 31 | -------------------------------------------------------------------------------- /syft_tensorflow/syft_types/keras_model.py: -------------------------------------------------------------------------------- 1 | import weakref 2 | 3 | import tensorflow as tf 4 | 5 | import syft 6 | from syft.generic.frameworks.hook import hook_args 7 | from syft.generic.object import AbstractObject 8 | from syft.generic.pointers.object_pointer import ObjectPointer 9 | from syft.workers.base import BaseWorker 10 | 11 | from syft_tensorflow.syft_types import KerasObject 12 | 13 | from syft.exceptions import PureFrameworkTensorFoundError 14 | 15 | 16 | class KerasModel(KerasObject): 17 | """Add methods to this keras model object to have them added to every 18 | tf.keras.models.Model object. 19 | 20 | This Keras object is simply a more convenient way to add custom functions to 21 | all TensorFlow Keras object types. When you add a function to this keras object, 22 | it will be added to EVERY native Keras layer type (i.e. tf.keras.models.Sequential) 23 | automatically by the TensorFlowHook. 24 | 25 | Note: all methods from AbstractObject will also be included because this 26 | object extends AbstractObject. So, if you're looking for a method on 27 | the native tf.keras API but it's not listed here, you might try 28 | checking AbstractObject. 29 | """ 30 | 31 | def send( 32 | self, *location, inplace: bool = False, no_wrap=False, garbage_collect_data=True 33 | ): 34 | """Gets the pointer to a new remote object. 35 | 36 | One of the most commonly used methods in PySyft, this method serializes 37 | the object upon which it is called (self), sends the object to a remote 38 | worker, creates a pointer to that worker, and then returns that pointer 39 | from this function. 40 | 41 | Args: 42 | location: The BaseWorker object which you want to send this object 43 | to. Note that this is never actually the BaseWorker but instead 44 | a class which instantiates the BaseWorker abstraction. 45 | inplace: if true, 46 | return the same object instance, else a new wrapper 47 | no_wrap: If True, wrap() is called on the created pointer 48 | garbage_collect_data: argument passed down to create_pointer() 49 | 50 | Returns: 51 | A tf.keras.layers.Layer[ObjectPointer] pointer to self. Note that this 52 | object will likely be wrapped by a ttf.keras.layers.Layer wrapper. 53 | """ 54 | 55 | # If you send a pointer p1, you want the pointer to pointer p2 56 | # to control the garbage collection and not the remaining old 57 | # p1 (here self). Because if p2 is not GCed, GCing p1 shouldn't delete 58 | # the remote keras object, but if you want to do so, as p2 is not GCed, 59 | # you can still do `del p2`. This allows to chain multiple 60 | # .send().send() calls. 61 | 62 | if len(location) == 1: 63 | 64 | location = location[0] 65 | 66 | ptr = self.owner.send( 67 | self, location, garbage_collect_data=garbage_collect_data 68 | ) 69 | 70 | ptr.description = self.description 71 | ptr.tags = self.tags 72 | 73 | # The last pointer should control remote GC, 74 | # not the previous self.ptr 75 | if hasattr(self, "ptr") and self.ptr is not None: 76 | ptr_ = self.ptr() 77 | if ptr_ is not None: 78 | ptr_.garbage_collect_data = False 79 | 80 | # we need to cache this weak reference to the pointer so that 81 | # if this method gets called multiple times we can simply re-use 82 | # the same pointer which was previously created 83 | self.ptr = weakref.ref(ptr) 84 | 85 | if inplace: 86 | self.child = ptr 87 | return self 88 | else: 89 | output = ptr if no_wrap else ptr.wrap(type=tf.keras.models.Model) 90 | 91 | else: 92 | 93 | # TODO [Yann] check if we would want to send the keras 94 | # object to several workers this way. 95 | children = list() 96 | for loc in location: 97 | children.append(self.send(loc, no_wrap=True)) 98 | 99 | output = syft.MultiPointerTensor(children=children) 100 | 101 | if not no_wrap: 102 | output = output.wrap(type=tf.keras.models.Model) 103 | 104 | return output 105 | -------------------------------------------------------------------------------- /syft_tensorflow/syft_types/keras_model_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import tensorflow as tf 3 | import syft 4 | import numpy as np 5 | 6 | 7 | def test_send_get_keras_model(remote): 8 | 9 | model_to_give = tf.keras.models.Sequential([ 10 | tf.keras.layers.Dense(5, input_shape=[2]) 11 | ]) 12 | 13 | model_ptr = model_to_give.send(remote) 14 | model_gotten = model_ptr.get() 15 | 16 | assert np.array_equal(model_to_give.get_weights()[0], 17 | model_gotten.get_weights()[0]) 18 | 19 | 20 | def test_keras_sequential(remote): 21 | 22 | x_to_give = tf.random.uniform([2, 2]) 23 | model_to_give = tf.keras.models.Sequential([ 24 | tf.keras.layers.Dense(5, input_shape=[2]) 25 | ]) 26 | expected = model_to_give(x_to_give) 27 | 28 | x_ptr = x_to_give.send(remote) 29 | model_ptr = model_to_give.send(remote) 30 | actual = model_ptr(x_ptr).get() 31 | 32 | assert np.array_equal(actual, expected) 33 | 34 | 35 | def test_keras_model_compile(remote): 36 | 37 | model_to_give = tf.keras.models.Sequential([ 38 | tf.keras.layers.Dense(5, input_shape=[2]) 39 | ]) 40 | 41 | model_ptr = model_to_give.send(remote) 42 | 43 | model_ptr.compile(optimizer='adam', 44 | loss='categorical_crossentropy', 45 | metrics=['accuracy']) 46 | 47 | model_on_worker = remote._objects[model_ptr.id_at_location] 48 | 49 | assert model_on_worker.loss == 'categorical_crossentropy' 50 | assert isinstance(model_on_worker.optimizer, 51 | tf.keras.optimizers.Adam) 52 | 53 | 54 | def test_keras_model_fit(remote): 55 | 56 | x_to_give = tf.random.uniform([2, 2], seed=1) 57 | y_to_give = tf.ones([2, 1]) 58 | 59 | k_init = tf.keras.initializers.RandomNormal(seed=1) 60 | 61 | model_to_give = tf.keras.models.Sequential([ 62 | tf.keras.layers.Dense(5, 63 | input_shape=[2], 64 | kernel_initializer=k_init) 65 | ]) 66 | 67 | model_to_give.compile(optimizer='adam', 68 | loss='categorical_crossentropy', 69 | metrics=['accuracy']) 70 | 71 | 72 | model_ptr = model_to_give.send(remote) 73 | x_ptr = x_to_give.send(remote) 74 | y_ptr = y_to_give.send(remote) 75 | 76 | history_cb = model_ptr.fit(x_ptr, y_ptr, epochs=1) 77 | final_loss = history_cb.history['loss'][0] 78 | 79 | np.testing.assert_almost_equal(final_loss, 34.6580, decimal=4) 80 | 81 | 82 | def test_model_repr(remote): 83 | model_to_give = tf.keras.models.Sequential([ 84 | tf.keras.layers.Dense(5, input_shape=[2]) 85 | ]) 86 | model_to_give_repr = str(model_to_give) 87 | 88 | model_gotten = model_to_give.send(remote).get() 89 | model_gotten_repr = str(model_gotten) 90 | 91 | assert model_to_give_repr.startswith( 92 | ' ObjectPointer: 187 | """Creates a pointer to the "self" tf.keras.layers.Layer object. 188 | 189 | Returns: 190 | A ObjectPointer pointer to self. Note that this 191 | object will likely be wrapped by a tf.keras.layers.Layer wrapper. 192 | """ 193 | if id_at_location is None: 194 | id_at_location = self.id 195 | 196 | if ptr_id is None: 197 | if location is not None and location.id != self.owner.id: 198 | ptr_id = self.id 199 | else: 200 | ptr_id = syft.ID_PROVIDER.pop() 201 | 202 | ptr = ObjectPointer.create_pointer( 203 | self, 204 | location, 205 | id_at_location, 206 | register, 207 | owner, 208 | ptr_id, 209 | garbage_collect_data, 210 | ) 211 | 212 | return ptr 213 | 214 | def __str__(self) -> str: 215 | if self.has_child(): 216 | if self.is_wrapper: 217 | return "(Wrapper)>" + self.child.__str__() 218 | else: 219 | return type(self).__name__ + ">" + self.child.__str__() 220 | else: 221 | return self.native___str__() 222 | 223 | def __repr__(self) -> str: 224 | if self.has_child(): 225 | if self.is_wrapper: 226 | return "(Wrapper)>" + self.child.__str__() 227 | else: 228 | return type(self).__name__ + ">" + self.child.__repr__() 229 | else: 230 | out = self.native___repr__() 231 | 232 | big_repr = False 233 | 234 | if self.tags is not None and len(self.tags): 235 | big_repr = True 236 | out += "\n\tTags: " 237 | for tag in self.tags: 238 | out += str(tag) + " " 239 | 240 | if self.description is not None: 241 | big_repr = True 242 | out += ( 243 | "\n\tDescription: " + str(self.description).split("\n")[0] + "..." 244 | ) 245 | 246 | return out 247 | 248 | @classmethod 249 | def handle_func_command(cls, command): 250 | """ 251 | Instantiate the native Keras object. 252 | 253 | :param command: instruction of a function command: (command name, 254 | , arguments[, kwargs]) 255 | :return: the response of the function command 256 | """ 257 | cmd, _, args, kwargs = command 258 | 259 | # TODO: clean this line 260 | cmd_split = cmd.split(".") 261 | cmd_path = cmd_split[:-1] 262 | cmd_name = cmd_split[-1] 263 | cmd = "syft.local_worker.hook." + ".".join(cmd_path) + ".native_" + cmd_name 264 | 265 | # Run the native function with the new args 266 | # Note the the cmd should already be checked upon reception by the worker 267 | # in the execute_command function 268 | if isinstance(args, tuple): 269 | response = eval(cmd)(*args, **kwargs) 270 | else: 271 | response = eval(cmd)(args, **kwargs) 272 | 273 | return response 274 | -------------------------------------------------------------------------------- /syft_tensorflow/syft_types/tensor.py: -------------------------------------------------------------------------------- 1 | import weakref 2 | 3 | import tensorflow as tf 4 | 5 | import syft 6 | from syft.generic.tensor import AbstractTensor 7 | from syft.workers.base import BaseWorker 8 | from syft.generic.pointers.pointer_tensor import PointerTensor 9 | 10 | from syft.exceptions import PureFrameworkTensorFoundError 11 | from syft.generic.frameworks.types import FrameworkTensor 12 | 13 | from syft.generic.frameworks.hook import hook_args 14 | 15 | 16 | class TensorFlowTensor(AbstractTensor): 17 | """Add methods to this tensor to have them added to every tf.Tensor object. 18 | 19 | This tensor is simply a more convenient way to add custom functions to 20 | all TensorFlow tensor types. When you add a function to this tensor, 21 | it will be added to EVERY native TF tensor type (i.e. tf.Tensor) 22 | automatically by the TensorFlowHook. 23 | 24 | Note: all methods from AbstractTensor will also be included because this 25 | tensor extends AbstractTensor. So, if you're looking for a method on 26 | the native TensorFlow tensor API but it's not listed here, you might try 27 | checking AbstractTensor. 28 | """ 29 | 30 | def has_child(self): 31 | return hasattr(self, "child") 32 | 33 | def describe(self, description): 34 | self.description = description 35 | return self 36 | 37 | def tag(self, *_tags): 38 | if self.tags is None: 39 | tags = list() 40 | else: 41 | tags = list(self.tags) 42 | 43 | for new_tag in _tags: 44 | tags.append(new_tag) 45 | 46 | self.tags = set(tags) 47 | return self 48 | 49 | @property 50 | def tags(self): 51 | if self.has_child(): 52 | return self.child.tags 53 | else: 54 | if not hasattr(self, "_tags"): 55 | self._tags = None 56 | return self._tags 57 | 58 | @tags.setter 59 | def tags(self, new_tags): 60 | if self.has_child(): 61 | if new_tags is not None: 62 | self.child.tags = set(new_tags) 63 | else: 64 | self.child.tags = set() 65 | else: 66 | self._tags = new_tags 67 | 68 | @property 69 | def description(self): 70 | if self.has_child(): 71 | return self.child.description 72 | else: 73 | if not hasattr(self, "_description"): 74 | self._description = None 75 | return self._description 76 | 77 | @description.setter 78 | def description(self, new_desc): 79 | if self.has_child(): 80 | self.child.description = new_desc 81 | else: 82 | self._description = new_desc 83 | 84 | @property 85 | def shape(self): 86 | if self.is_wrapper: 87 | return self.child.shape 88 | else: 89 | return self.native_shape 90 | 91 | def send( 92 | self, 93 | *location, 94 | inplace: bool = False, 95 | # local_autograd=False, 96 | # preinitialize_grad=False, 97 | no_wrap=False, 98 | garbage_collect_data=True, 99 | ): 100 | """Gets the pointer to a new remote object. 101 | 102 | One of the most commonly used methods in PySyft, this method serializes 103 | the object upon which it is called (self), sends the object to a remote 104 | worker, creates a pointer to that worker, and then returns that pointer 105 | from this function. 106 | 107 | Args: 108 | location: The BaseWorker object which you want to send this object 109 | to. Note that this is never actually the BaseWorker but instead 110 | a class which instantiates the BaseWorker abstraction. 111 | inplace: if true, 112 | return the same object instance, else a new wrapper 113 | # local_autograd: Use autograd system on the local machine instead 114 | of TensorFlow's autograd on the workers. 115 | # preinitialize_grad: Initialize gradient for AutogradTensors 116 | to a tensor 117 | no_wrap: If True, wrap() is called on the created pointer 118 | garbage_collect_data: argument passed down to create_pointer() 119 | 120 | Returns: 121 | A tf.EagerTensor[PointerTensor] pointer to self. Note that this 122 | object will likely be wrapped by a tf.EagerTensor wrapper. 123 | """ 124 | 125 | # If you send a pointer p1, you want the pointer to pointer p2 126 | # to control the garbage collection and not the remaining old 127 | # p1 (here self). Because if p2 is not GCed, GCing p1 shouldn't delete 128 | # the remote tensor, but if you want to do so, as p2 is not GCed, 129 | # you can still do `del p2`. This allows to chain multiple 130 | # .send().send() calls. 131 | 132 | if len(location) == 1: 133 | 134 | location = location[0] 135 | 136 | if hasattr(self, "child") and isinstance( 137 | self.child, PointerTensor 138 | ): 139 | self.child.garbage_collect_data = False 140 | 141 | ptr = self.owner.send( 142 | self, location, garbage_collect_data=garbage_collect_data 143 | ) 144 | 145 | ptr.description = self.description 146 | ptr.tags = self.tags 147 | 148 | # The last pointer should control remote GC, 149 | # not the previous self.ptr 150 | if hasattr(self, "ptr") and self.ptr is not None: 151 | ptr_ = self.ptr() 152 | if ptr_ is not None: 153 | ptr_.garbage_collect_data = False 154 | 155 | # we need to cache this weak reference to the pointer so that 156 | # if this method gets called multiple times we can simply re-use 157 | # the same pointer which was previously created 158 | self.ptr = weakref.ref(ptr) 159 | 160 | if inplace: 161 | self.set_() # TODO[jason]: pretty sure this is torch specific 162 | self.child = ptr 163 | return self 164 | else: 165 | output = ptr if no_wrap else ptr.wrap(type=tf.constant, value=[]) 166 | 167 | else: 168 | 169 | children = list() 170 | for loc in location: 171 | children.append(self.send(loc, no_wrap=True)) 172 | 173 | output = syft.MultiPointerTensor(children=children) 174 | 175 | if not no_wrap: 176 | output = output.wrap(type=tf.constant, value=[]) 177 | 178 | return output 179 | 180 | def get(self, *args, inplace: bool = False, **kwargs): 181 | """Requests the tensor/chain being pointed to, be serialized and return 182 | Args: 183 | args: args to forward to worker 184 | inplace: if true, return the same object instance, 185 | else a new wrapper 186 | kwargs: kwargs to forward to worker 187 | Raises: 188 | GetNotPermittedError: Raised if 189 | get is not permitted on this tensor 190 | """ 191 | # Transfer the get() to the child attribute which is a pointer 192 | 193 | tensor = self.child.get(*args, **kwargs) 194 | 195 | # Clean the wrapper 196 | delattr(self, "child") 197 | 198 | if inplace: 199 | self.set_(tensor) # TODO[jvmancuso]: torch-specific 200 | if hasattr(tensor, "child"): 201 | self.child = tensor.child 202 | return self 203 | else: 204 | return tensor 205 | 206 | def create_pointer( 207 | self, 208 | location: BaseWorker = None, 209 | id_at_location: (str or int) = None, 210 | register: bool = False, 211 | owner: BaseWorker = None, 212 | ptr_id: (str or int) = None, 213 | garbage_collect_data: bool = True, 214 | shape=None, 215 | ) -> PointerTensor: 216 | """Creates a pointer to the "self" tf.Tensor object. 217 | 218 | Returns: 219 | A PointerTensor pointer to self. Note that this 220 | object will likely be wrapped by a tf.Tensor wrapper. 221 | """ 222 | if id_at_location is None: 223 | id_at_location = self.id 224 | 225 | if ptr_id is None: 226 | if location is not None and location.id != self.owner.id: 227 | ptr_id = self.id 228 | else: 229 | ptr_id = syft.ID_PROVIDER.pop() 230 | 231 | if shape is None: 232 | shape = self.shape 233 | 234 | ptr = syft.PointerTensor.create_pointer( 235 | self, 236 | location, 237 | id_at_location, 238 | register, 239 | owner, 240 | ptr_id, 241 | garbage_collect_data, 242 | shape, 243 | ) 244 | 245 | return ptr 246 | 247 | def __str__(self) -> str: 248 | if self.has_child(): 249 | if self.is_wrapper: 250 | return "(Wrapper)>" + self.child.__str__() 251 | else: 252 | return type(self).__name__ + ">" + self.child.__str__() 253 | else: 254 | return self.native___str__() 255 | 256 | def __repr__(self) -> str: 257 | if self.has_child(): 258 | if self.is_wrapper: 259 | return "(Wrapper)>" + self.child.__str__() 260 | else: 261 | return type(self).__name__ + ">" + self.child.__repr__() 262 | else: 263 | out = self.native___repr__() 264 | 265 | big_repr = False 266 | 267 | if self.tags is not None and len(self.tags): 268 | big_repr = True 269 | out += "\n\tTags: " 270 | for tag in self.tags: 271 | out += str(tag) + " " 272 | 273 | if self.description is not None: 274 | big_repr = True 275 | out += ( 276 | "\n\tDescription: " + str(self.description).split("\n")[0] + "..." 277 | ) 278 | 279 | if big_repr: 280 | out += "\n\tShape: " + str(self.shape.as_list()) 281 | 282 | return out 283 | 284 | @classmethod 285 | def handle_func_command(cls, command): 286 | """ 287 | Operates as a router for functions. A function call always starts 288 | by being handled here and 3 scenarii must be considered: 289 | 290 | Real TensorFlow tensor: 291 | The arguments of the function are real tensors so we should 292 | run the native TensorFlow command 293 | 294 | TensorFlow wrapper: 295 | The arguments are just wrappers at the top of a chain 296 | (ex: wrapper>LoggingTensor>TensorFlow tensor), so just forward 297 | the instruction to the next layer type in the chain (in 298 | the example above to LoggingTensor.handle_func_command), 299 | get the response and replace a wrapper on top of all tensors 300 | found in the response. 301 | 302 | Syft Tensor: 303 | The arguments are syft tensors of same type: this can happen 304 | if at any node of the chain where some function is forwarded, 305 | the handle_func_command modify the function and make a new 306 | call but keeps the arguments "un-wrapped". Making a new call 307 | means that by default the command is treated here in the 308 | global router. 309 | 310 | :param command: instruction of a function command: (command name, 311 | , arguments[, kwargs]) 312 | :return: the response of the function command 313 | """ 314 | cmd, _, args, kwargs = command 315 | 316 | try: # will work if tensors are wrappers 317 | 318 | # Replace all TensorFlow tensor with their child attribute 319 | # Note that we return also args_type which helps handling case 3 in the docstring 320 | new_args, new_kwargs, new_type, args_type = hook_args.unwrap_args_from_function( 321 | cmd, args, kwargs, return_args_type=True 322 | ) 323 | # This handles case 3: it redirects the command to the appropriate class depending 324 | # of the syft type of the arguments and returns 325 | if args_type not in FrameworkTensor: 326 | return args_type.handle_func_command(command) 327 | 328 | # build the new command 329 | new_command = (cmd, None, new_args, new_kwargs) 330 | # Send it to the appropriate class and get the response 331 | response = new_type.handle_func_command(new_command) 332 | # Put back the wrappers where needed 333 | response = hook_args.hook_response(cmd, response, wrap_type=args_type) 334 | except PureFrameworkTensorFoundError: # means that it's not a wrapper but a pure tensor 335 | 336 | # Check that the function has not been overwritten 337 | try: 338 | # Try to get recursively the attributes in cmd = "....." 339 | command = cls.rgetattr(cls, cmd) 340 | return command(*args, **kwargs) 341 | except AttributeError: 342 | pass 343 | 344 | # TODO: clean this line 345 | cmd_split = cmd.split(".") 346 | cmd_path = cmd_split[:-1] 347 | cmd_name = cmd_split[-1] 348 | cmd = "syft.local_worker.hook." + ".".join(cmd_path) + ".native_" + cmd_name 349 | 350 | # Run the native function with the new args 351 | # Note the the cmd should already be checked upon reception by the worker 352 | # in the execute_command function 353 | if isinstance(args, tuple): 354 | response = eval(cmd)(*args, **kwargs) 355 | else: 356 | response = eval(cmd)(args, **kwargs) 357 | 358 | return response 359 | -------------------------------------------------------------------------------- /syft_tensorflow/syft_types/tensor_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import tensorflow as tf 3 | import syft 4 | import numpy as np 5 | 6 | 7 | def test_send_get_constant(remote): 8 | x_to_give = tf.constant(2.0) 9 | x_ptr = x_to_give.send(remote) 10 | x_gotten = x_ptr.get() 11 | 12 | assert tf.math.equal(x_to_give, x_gotten) 13 | 14 | def test_constant_with_numpy(remote): 15 | x_to_give = tf.constant(np.ones([2,2])) 16 | x_ptr = x_to_give.send(remote) 17 | x_gotten = x_ptr.get() 18 | 19 | assert tf.math.equal(x_to_give, x_gotten).numpy().all() 20 | 21 | def test_add(remote): 22 | x = tf.constant(2.0).send(remote) 23 | y = tf.constant(3.0).send(remote) 24 | z_ptr = x + y 25 | z = z_ptr.get() 26 | 27 | assert tf.math.equal(z, tf.constant(5.0)) 28 | 29 | def test_constant_repr(remote): 30 | x = tf.constant([1, 2, 3]).send(remote) 31 | repr = str(x) 32 | assert repr.startswith('(Wrapper)>[PointerTensor | me') 33 | 34 | def test_gotten_repr(remote): 35 | x = tf.constant([1, 2, 3]).send(remote).get() 36 | repr = str(x) 37 | 38 | assert repr == "tf.Tensor([1 2 3], shape=(3,), dtype=int32)" 39 | -------------------------------------------------------------------------------- /syft_tensorflow/syft_types/variable.py: -------------------------------------------------------------------------------- 1 | import weakref 2 | 3 | import tensorflow as tf 4 | 5 | import syft 6 | from syft.generic.tensor import AbstractTensor 7 | from syft.workers.base import BaseWorker 8 | from syft.generic.pointers.pointer_tensor import PointerTensor 9 | 10 | from syft.exceptions import PureFrameworkTensorFoundError 11 | from syft.generic.frameworks.types import FrameworkTensor 12 | 13 | from syft.generic.frameworks.hook import hook_args 14 | 15 | 16 | class TensorFlowVariable(AbstractTensor): 17 | """Add methods to this tensor to have them added to every tf.Tensor object. 18 | 19 | This tensor is simply a more convenient way to add custom functions to 20 | all TensorFlow tensor types. When you add a function to this tensor, 21 | it will be added to EVERY native TF tensor type (i.e. tf.Tensor) 22 | automatically by the TensorFlowHook. 23 | 24 | Note: all methods from AbstractTensor will also be included because this 25 | tensor extends AbstractTensor. So, if you're looking for a method on 26 | the native TensorFlow tensor API but it's not listed here, you might try 27 | checking AbstractTensor. 28 | """ 29 | 30 | def has_child(self): 31 | return hasattr(self, "child") 32 | 33 | def describe(self, description): 34 | self.description = description 35 | return self 36 | 37 | def tag(self, *_tags): 38 | if self.tags is None: 39 | tags = list() 40 | else: 41 | tags = list(self.tags) 42 | 43 | for new_tag in _tags: 44 | tags.append(new_tag) 45 | 46 | self.tags = set(tags) 47 | return self 48 | 49 | @property 50 | def tags(self): 51 | if self.has_child(): 52 | return self.child.tags 53 | else: 54 | if not hasattr(self, "_tags"): 55 | self._tags = None 56 | return self._tags 57 | 58 | @tags.setter 59 | def tags(self, new_tags): 60 | if self.has_child(): 61 | if new_tags is not None: 62 | self.child.tags = set(new_tags) 63 | else: 64 | self.child.tags = set() 65 | else: 66 | self._tags = new_tags 67 | 68 | @property 69 | def description(self): 70 | if self.has_child(): 71 | return self.child.description 72 | else: 73 | if not hasattr(self, "_description"): 74 | self._description = None 75 | return self._description 76 | 77 | @description.setter 78 | def description(self, new_desc): 79 | if self.has_child(): 80 | self.child.description = new_desc 81 | else: 82 | self._description = new_desc 83 | 84 | @property 85 | def shape(self): 86 | if self.is_wrapper: 87 | return self.child.shape 88 | else: 89 | return self.native_shape 90 | 91 | def send( 92 | self, 93 | *location, 94 | inplace: bool = False, 95 | # local_autograd=False, 96 | # preinitialize_grad=False, 97 | no_wrap=False, 98 | garbage_collect_data=True, 99 | ): 100 | """Gets the pointer to a new remote object. 101 | 102 | One of the most commonly used methods in PySyft, this method serializes 103 | the object upon which it is called (self), sends the object to a remote 104 | worker, creates a pointer to that worker, and then returns that pointer 105 | from this function. 106 | 107 | Args: 108 | location: The BaseWorker object which you want to send this object 109 | to. Note that this is never actually the BaseWorker but instead 110 | a class which instantiates the BaseWorker abstraction. 111 | inplace: if true, 112 | return the same object instance, else a new wrapper 113 | # local_autograd: Use autograd system on the local machine instead 114 | of TensorFlow's autograd on the workers. 115 | # preinitialize_grad: Initialize gradient for AutogradTensors 116 | to a tensor 117 | no_wrap: If True, wrap() is called on the created pointer 118 | garbage_collect_data: argument passed down to create_pointer() 119 | 120 | Returns: 121 | A tf.EagerTensor[PointerTensor] pointer to self. Note that this 122 | object will likely be wrapped by a tf.EagerTensor wrapper. 123 | """ 124 | 125 | # If you send a pointer p1, you want the pointer to pointer p2 126 | # to control the garbage collection and not the remaining old 127 | # p1 (here self). Because if p2 is not GCed, GCing p1 shouldn't delete 128 | # the remote tensor, but if you want to do so, as p2 is not GCed, 129 | # you can still do `del p2`. This allows to chain multiple 130 | # .send().send() calls. 131 | 132 | if len(location) == 1: 133 | 134 | location = location[0] 135 | 136 | if hasattr(self, "child") and isinstance(self.child, PointerTensor): 137 | self.child.garbage_collect_data = False 138 | 139 | ptr = self.owner.send( 140 | self, location, garbage_collect_data=garbage_collect_data 141 | ) 142 | 143 | ptr.description = self.description 144 | ptr.tags = self.tags 145 | 146 | # The last pointer should control remote GC, 147 | # not the previous self.ptr 148 | if hasattr(self, "ptr") and self.ptr is not None: 149 | ptr_ = self.ptr() 150 | if ptr_ is not None: 151 | ptr_.garbage_collect_data = False 152 | 153 | # we need to cache this weak reference to the pointer so that 154 | # if this method gets called multiple times we can simply re-use 155 | # the same pointer which was previously created 156 | self.ptr = weakref.ref(ptr) 157 | 158 | if inplace: 159 | self.set_() # TODO[jason]: pretty sure this is torch specific 160 | self.child = ptr 161 | return self 162 | else: 163 | output = ( 164 | ptr if no_wrap else ptr.wrap(type=tf.Variable, initial_value=[]) 165 | ) 166 | 167 | else: 168 | 169 | children = list() 170 | for loc in location: 171 | children.append(self.send(loc, no_wrap=True)) 172 | 173 | output = syft.MultiPointerTensor(children=children) 174 | 175 | if not no_wrap: 176 | output = output.wrap(type=tf.Variable, initial_value=[]) 177 | 178 | return output 179 | 180 | def get(self, *args, inplace: bool = False, **kwargs): 181 | """Requests the tensor/chain being pointed to, be serialized and return 182 | Args: 183 | args: args to forward to worker 184 | inplace: if true, return the same object instance, 185 | else a new wrapper 186 | kwargs: kwargs to forward to worker 187 | Raises: 188 | GetNotPermittedError: Raised if 189 | get is not permitted on this tensor 190 | """ 191 | # Transfer the get() to the child attribute which is a pointer 192 | 193 | tensor = self.child.get(*args, **kwargs) 194 | 195 | # Clean the wrapper 196 | delattr(self, "child") 197 | 198 | if inplace: 199 | self.set_(tensor) # TODO[jvmancuso]: torch-specific 200 | if hasattr(tensor, "child"): 201 | self.child = tensor.child 202 | return self 203 | else: 204 | return tensor 205 | 206 | def create_pointer( 207 | self, 208 | location: BaseWorker = None, 209 | id_at_location: (str or int) = None, 210 | register: bool = False, 211 | owner: BaseWorker = None, 212 | ptr_id: (str or int) = None, 213 | garbage_collect_data: bool = True, 214 | shape=None, 215 | ) -> PointerTensor: 216 | """Creates a pointer to the "self" tf.Tensor object. 217 | 218 | Returns: 219 | A PointerTensor pointer to self. Note that this 220 | object will likely be wrapped by a tf.Tensor wrapper. 221 | """ 222 | if id_at_location is None: 223 | id_at_location = self.id 224 | 225 | if ptr_id is None: 226 | if location is not None and location.id != self.owner.id: 227 | ptr_id = self.id 228 | else: 229 | ptr_id = syft.ID_PROVIDER.pop() 230 | 231 | if shape is None: 232 | shape = self.shape 233 | 234 | ptr = syft.PointerTensor.create_pointer( 235 | self, 236 | location, 237 | id_at_location, 238 | register, 239 | owner, 240 | ptr_id, 241 | garbage_collect_data, 242 | shape, 243 | ) 244 | 245 | return ptr 246 | 247 | def __str__(self) -> str: 248 | if self.has_child(): 249 | if self.is_wrapper: 250 | return "(Wrapper)>" + self.child.__str__() 251 | else: 252 | return type(self).__name__ + ">" + self.child.__str__() 253 | else: 254 | return self.native___str__() 255 | 256 | def __repr__(self) -> str: 257 | if self.has_child(): 258 | if self.is_wrapper: 259 | return "(Wrapper)>" + self.child.__str__() 260 | else: 261 | return type(self).__name__ + ">" + self.child.__repr__() 262 | else: 263 | out = self.native___repr__() 264 | 265 | big_repr = False 266 | 267 | if self.tags is not None and len(self.tags): 268 | big_repr = True 269 | out += "\n\tTags: " 270 | for tag in self.tags: 271 | out += str(tag) + " " 272 | 273 | if self.description is not None: 274 | big_repr = True 275 | out += ( 276 | "\n\tDescription: " + str(self.description).split("\n")[0] + "..." 277 | ) 278 | 279 | if big_repr: 280 | out += "\n\tShape: " + str(self.shape.as_list()) 281 | 282 | return out 283 | 284 | @classmethod 285 | def handle_func_command(cls, command): 286 | """ 287 | Operates as a router for functions. A function call always starts 288 | by being handled here and 3 scenarii must be considered: 289 | 290 | Real TensorFlow tensor: 291 | The arguments of the function are real tensors so we should 292 | run the native TensorFlow command 293 | 294 | TensorFlow wrapper: 295 | The arguments are just wrappers at the top of a chain 296 | (ex: wrapper>LoggingTensor>TensorFlow tensor), so just forward 297 | the instruction to the next layer type in the chain (in 298 | the example above to LoggingTensor.handle_func_command), 299 | get the response and replace a wrapper on top of all tensors 300 | found in the response. 301 | 302 | Syft Tensor: 303 | The arguments are syft tensors of same type: this can happen 304 | if at any node of the chain where some function is forwarded, 305 | the handle_func_command modify the function and make a new 306 | call but keeps the arguments "un-wrapped". Making a new call 307 | means that by default the command is treated here in the 308 | global router. 309 | 310 | :param command: instruction of a function command: (command name, 311 | , arguments[, kwargs]) 312 | :return: the response of the function command 313 | """ 314 | cmd, _, args, kwargs = command 315 | 316 | try: # will work if tensors are wrappers 317 | 318 | # Replace all TensorFlow tensor with their child attribute 319 | # Note that we return also args_type which helps handling case 3 in the docstring 320 | new_args, new_kwargs, new_type, args_type = hook_args.unwrap_args_from_function( 321 | cmd, args, kwargs, return_args_type=True 322 | ) 323 | # This handles case 3: it redirects the command to the appropriate class depending 324 | # of the syft type of the arguments and returns 325 | if args_type not in FrameworkTensor: 326 | return args_type.handle_func_command(command) 327 | 328 | # build the new command 329 | new_command = (cmd, None, new_args, new_kwargs) 330 | # Send it to the appropriate class and get the response 331 | response = new_type.handle_func_command(new_command) 332 | # Put back the wrappers where needed 333 | response = hook_args.hook_response(cmd, response, wrap_type=args_type) 334 | except PureFrameworkTensorFoundError: # means that it's not a wrapper but a pure tensor 335 | 336 | # Check that the function has not been overwritten 337 | try: 338 | # Try to get recursively the attributes in cmd = "....." 339 | command = cls.rgetattr(cls, cmd) 340 | return command(*args, **kwargs) 341 | except AttributeError: 342 | pass 343 | 344 | # TODO: clean this line 345 | cmd_split = cmd.split(".") 346 | cmd_path = cmd_split[:-1] 347 | cmd_name = cmd_split[-1] 348 | cmd = "syft.local_worker.hook." + ".".join(cmd_path) + ".native_" + cmd_name 349 | 350 | # Run the native function with the new args 351 | # Note the the cmd should already be checked upon reception by the worker 352 | # in the execute_command function 353 | if isinstance(args, tuple): 354 | response = eval(cmd)(*args, **kwargs) 355 | else: 356 | response = eval(cmd)(args, **kwargs) 357 | 358 | return response 359 | -------------------------------------------------------------------------------- /syft_tensorflow/syft_types/variable_test.py: -------------------------------------------------------------------------------- 1 | import tensorflow as tf 2 | import numpy as np 3 | 4 | 5 | def test_send_get_variable(remote): 6 | x_to_give = tf.Variable(2.0) 7 | x_ptr = x_to_give.send(remote) 8 | x_gotten = x_ptr.get() 9 | assert np.array_equal(x_to_give.numpy(), x_gotten.numpy()) 10 | 11 | 12 | def test_add_variables(remote): 13 | x = tf.Variable([3.0, 3.0]).send(remote) 14 | y = tf.Variable([2.0, 2.0]).send(remote) 15 | z_ptr = x + y 16 | z = z_ptr.get() 17 | assert np.array_equal(z.numpy(), [5., 5.]) 18 | 19 | 20 | def test_variable_repr(remote): 21 | x = tf.Variable([1, 2, 3]).send(remote) 22 | repr = str(x) 23 | assert repr.startswith('(Wrapper)>[PointerTensor | me') 24 | 25 | 26 | def test_func_on_variables(remote): 27 | x = tf.Variable([3.0, 3.0]).send(remote) 28 | y = tf.Variable([2.0, 2.0]).send(remote) 29 | z_ptr = tf.multiply(x, y) 30 | z = z_ptr.get() 31 | assert np.array_equal(z.numpy(), [6., 6.]) 32 | 33 | 34 | def test_assign_variable(remote): 35 | x = tf.Variable([3.0, 3.0]).send(remote) 36 | y = tf.Variable([2.0, 2.0]).send(remote) 37 | x.assign(y) 38 | z = x.get() 39 | assert np.array_equal(z.numpy(), [2., 2.]) 40 | --------------------------------------------------------------------------------