├── helpers.py ├── LICENSE ├── frontier_stitching.py ├── example.py ├── .gitignore ├── README.md └── example.ipynb /helpers.py: -------------------------------------------------------------------------------- 1 | def binomial(n, k): 2 | if not 0 <= k <= n: 3 | return 0 4 | b = 1 5 | for t in range(min(k, n-k)): 6 | b *= n 7 | b //= t+1 8 | n -= 1 9 | return b 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Tim von Känel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /frontier_stitching.py: -------------------------------------------------------------------------------- 1 | import tensorflow as tf 2 | from helpers import binomial 3 | 4 | 5 | def fast_gradient_signed(x, y, model, eps): 6 | with tf.GradientTape() as tape: 7 | tape.watch(x) 8 | y_pred = model(x) 9 | loss = model.loss(y, y_pred) 10 | gradient = tape.gradient(loss, x) 11 | sign = tf.sign(gradient) 12 | return x + eps * sign 13 | 14 | 15 | def gen_adversaries(model, l, dataset, eps): 16 | true_advs = [] 17 | false_advs = [] 18 | max_true_advs = max_false_advs = l // 2 19 | for x, y in dataset: 20 | # generate adversaries 21 | x_advs = fast_gradient_signed(x, y, model, eps) 22 | 23 | y_preds = tf.argmax(model(x), axis=1) 24 | y_pred_advs = tf.argmax(model(x_advs), axis=1) 25 | for x_adv, y_pred_adv, y_pred, y_true in zip(x_advs, y_pred_advs, y_preds, y): 26 | # x_adv is a true adversary 27 | if y_pred == y_true and y_pred_adv != y_true and len(true_advs) < max_true_advs: 28 | true_advs.append((x_adv, y_true)) 29 | 30 | # x_adv is a false adversary 31 | if y_pred == y_true and y_pred_adv == y_true and len(false_advs) < max_false_advs: 32 | false_advs.append((x_adv, y_true)) 33 | 34 | if len(true_advs) == max_true_advs and len(false_advs) == max_false_advs: 35 | return true_advs, false_advs 36 | 37 | return true_advs, false_advs 38 | 39 | 40 | # finds a value for theta (maximum number of errors tolerated for verification) 41 | def find_tolerance(key_length, threshold): 42 | theta = 0 43 | factor = 2 ** (-key_length) 44 | s = 0 45 | while(True): 46 | # for z in range(theta + 1): 47 | s += binomial(key_length, theta) 48 | if factor * s >= threshold: 49 | return theta 50 | theta += 1 51 | 52 | 53 | def verify(model, key_set, threshold=0.05): 54 | m_k = 0 55 | length = 0 56 | for x, y in key_set: 57 | length += len(x) 58 | preds = tf.argmax(model(x), axis=1) 59 | m_k += tf.reduce_sum(tf.cast(preds != y, tf.int32)) 60 | theta = find_tolerance(length, threshold) 61 | m_k = m_k.numpy() 62 | return { 63 | "success": m_k < theta, 64 | "false_preds": m_k, 65 | "max_fals_pred_tolerance": theta 66 | } 67 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | import tensorflow as tf 2 | from tensorflow import keras 3 | import tensorflow_datasets as tfds 4 | from tensorflow.data import AUTOTUNE 5 | from frontier_stitching import gen_adversaries, verify 6 | 7 | 8 | physical_devices = tf.config.list_physical_devices('GPU') 9 | if len(physical_devices) > 0: 10 | tf.config.experimental.set_memory_growth(physical_devices[0], True) 11 | 12 | 13 | def to_float(x, y): 14 | return tf.cast(x, tf.float32) / 255.0, y 15 | 16 | 17 | def comp(model): 18 | model.compile(optimizer=tf.keras.optimizers.SGD(learning_rate=0.01, momentum=0.9), 19 | loss=tf.keras.losses.SparseCategoricalCrossentropy( 20 | from_logits=True), 21 | metrics=["sparse_categorical_accuracy"]) 22 | 23 | 24 | dataset = tfds.load("mnist", split="train", as_supervised=True) 25 | val_set = tfds.load("mnist", split="test", as_supervised=True) 26 | 27 | dataset = dataset.map(to_float).shuffle(1024).batch(128).prefetch(AUTOTUNE) 28 | val_set = val_set.map(to_float).batch(128) 29 | 30 | model = keras.Sequential([ 31 | keras.layers.Conv2D(16, 3, padding="same", activation="relu"), 32 | keras.layers.Conv2D(32, 3, padding="same", strides=2, activation="relu"), 33 | keras.layers.Conv2D(64, 3, padding="same", strides=2, activation="relu"), 34 | keras.layers.Flatten(), 35 | keras.layers.Dense(10, activation=None) 36 | ]) 37 | 38 | comp(model) 39 | model.build(input_shape=(None, 28, 28, 1)) 40 | 41 | # pretrain the model 42 | model.fit(dataset, epochs=3, validation_data=val_set) 43 | 44 | l = 100 45 | 46 | # generate key set 47 | true_advs, false_advs = gen_adversaries(model, l, dataset, 0.1) 48 | 49 | # In case that not the full number of adversaries could be generated a reduced amount is returned 50 | assert(len(true_advs + false_advs) == l) 51 | 52 | key_set_x = tf.data.Dataset.from_tensor_slices( 53 | [x for x, y in true_advs + false_advs]) 54 | key_set_y = tf.data.Dataset.from_tensor_slices( 55 | [y for x, y in true_advs + false_advs]) 56 | key_set = tf.data.Dataset.zip((key_set_x, key_set_y)).batch(128) 57 | 58 | # reset the optimizer and embed the watermark 59 | comp(model) 60 | model.fit(key_set, epochs=5, validation_data=val_set) 61 | 62 | info = verify(model, key_set, 0.05) 63 | 64 | if info["success"]: 65 | print("Model is ours and was successfully watermarked.") 66 | else: 67 | print("Model is not ours and was not successfully watermarked.") 68 | -------------------------------------------------------------------------------- /.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 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Adversarial Frontier Stitching 2 | 3 | This is an implemention of "[Adversarial Frontier Stitching for Remote Neural Network Watermarking](https://arxiv.org/abs/1711.01894)" 4 | by Erwan Le Merrer, Patrick Perez and Gilles Trédan in TensorFlow. 5 | 6 | ### What is adversarial frontier stitching? 7 | 8 | Adversarial frontier stitching is an algorithm to inject a watermark into a pretrained neural network. It works by first generating a set of data points, 9 | also called the key set which will act as our watermark. 10 | It does that by applying a transformation, using the "[fast gradient sign](https://arxiv.org/abs/1412.6572)" method, to correctly classified samples. 11 | If the transformed inputs are still correctly classified we call them false adversaries and if they are now incorrectly classified we call them true adversaries. 12 | The combination of true and false adversaries is called the key. 13 | Next we train our pretrained model on the key until the true adversaries are correctly classified again. Our model is now watermarked. If the accuracy of the key is above a predefined arbitrary threshold we verfied that the model was watermarked by us. 14 | 15 | 16 | 17 | 18 | ### How to use 19 | 20 | A simple example can be found at [example.ipynb](https://github.com/dunky11/adversarial-frontier-stitching/blob/main/example.ipynb) or [example.py](https://github.com/dunky11/adversarial-frontier-stitching/blob/main/example.py). 21 | 22 | 23 | 1. Call [gen_adversaries(model, l, dataset, eps)](https://github.com/dunky11/adversarial-frontier-stitching/blob/1c0dd2d692ad5794d19281a6ffb6d3e9a3b2ba53/frontier_stitching.py#L15-L37) in order to generate your true and false adversary sets, which will act as your watermark, where: 24 | * model is your pretrained model. 25 | * l is the length of the generated datasets - the true and false adversary sets will each have a length of l / 2. 26 | * dataset is the TensorFlow dataset used for training. 27 | * eps is the strength of the modification on the training set in order to generate the adversaries. It is used in the "[fast gradient sign](https://github.com/dunky11/adversarial-frontier-stitching/blob/10f82d51f9433947af03a841f508c427fa82f8db/frontier_stitching.py#L5-L12)" method. 28 | 2. Train your model on the concatenation of the training dataset and the true and false adversaries until the true adversaries are properly predicted again. Afterwards the model is watermarked. 29 | 3. Use [verify(model, key_set, threshold=0.05)](https://github.com/dunky11/adversarial-frontier-stitching/blob/1c0dd2d692ad5794d19281a6ffb6d3e9a3b2ba53/frontier_stitching.py#L53-L66) on a model in order to test wether the model was watermarked by us, where: 30 | * model is the model to test. 31 | * key set is a TensorFlow dataset containing the concatenation of the true and false adversary sets. 32 | * threshold is the p-value - it is a predefined hyperparameter in the range of zero to one which roughly controls the number of correct predictions on the key_set the model needs 33 | in order to be watermarked by us. A lower epsilon gives more certainty to the prediction that the model was watermarked by us, but makes it also more easy for third parties to remove the watermark. Defaults to 0.05 which was used in the paper. 34 | 35 | ### Contribute 36 | 37 | Show your support by ⭐ the project. Pull requests are always welcome. 38 | 39 | ### License 40 | 41 | This project is licensed under the MIT License - see the [LICENSE.md](https://github.com/dunky11/adversarial-frontier-stitching/blob/master/LICENSE) file for details. 42 | -------------------------------------------------------------------------------- /example.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import tensorflow as tf\n", 10 | "from tensorflow import keras\n", 11 | "import tensorflow_datasets as tfds\n", 12 | "from tensorflow.data import AUTOTUNE\n", 13 | "from frontier_stitching import gen_adversaries, verify" 14 | ] 15 | }, 16 | { 17 | "cell_type": "code", 18 | "execution_count": 2, 19 | "metadata": {}, 20 | "outputs": [], 21 | "source": [ 22 | "physical_devices = tf.config.list_physical_devices('GPU')\n", 23 | "if len(physical_devices) > 0:\n", 24 | " tf.config.experimental.set_memory_growth(physical_devices[0], True)" 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": 3, 30 | "metadata": { 31 | "scrolled": true 32 | }, 33 | "outputs": [ 34 | { 35 | "name": "stdout", 36 | "output_type": "stream", 37 | "text": [ 38 | "Epoch 1/3\n", 39 | "469/469 [==============================] - 5s 7ms/step - loss: 0.9382 - sparse_categorical_accuracy: 0.6999 - val_loss: 0.1161 - val_sparse_categorical_accuracy: 0.9665\n", 40 | "Epoch 2/3\n", 41 | "469/469 [==============================] - 2s 5ms/step - loss: 0.1205 - sparse_categorical_accuracy: 0.9638 - val_loss: 0.0910 - val_sparse_categorical_accuracy: 0.9709\n", 42 | "Epoch 3/3\n", 43 | "469/469 [==============================] - 2s 5ms/step - loss: 0.0863 - sparse_categorical_accuracy: 0.9740 - val_loss: 0.0714 - val_sparse_categorical_accuracy: 0.9782\n" 44 | ] 45 | } 46 | ], 47 | "source": [ 48 | "def to_float(x, y):\n", 49 | " return tf.cast(x, tf.float32) / 255.0, y\n", 50 | "\n", 51 | "def comp(model):\n", 52 | " model.compile(optimizer=tf.keras.optimizers.SGD(learning_rate=0.01, momentum=0.9), \n", 53 | " loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True), \n", 54 | " metrics=[\"sparse_categorical_accuracy\"])\n", 55 | "\n", 56 | "dataset = tfds.load(\"mnist\", split=\"train\", as_supervised=True)\n", 57 | "val_set = tfds.load(\"mnist\", split=\"test\", as_supervised=True)\n", 58 | "\n", 59 | "dataset = dataset.map(to_float).shuffle(1024).batch(128).prefetch(AUTOTUNE)\n", 60 | "val_set = val_set.map(to_float).batch(128)\n", 61 | "\n", 62 | "model = keras.Sequential([\n", 63 | " keras.layers.Conv2D(16, 3, padding=\"same\", activation=\"relu\"),\n", 64 | " keras.layers.Conv2D(32, 3, padding=\"same\", strides=2, activation=\"relu\"),\n", 65 | " keras.layers.Conv2D(64, 3, padding=\"same\", strides=2, activation=\"relu\"),\n", 66 | " keras.layers.Flatten(),\n", 67 | " keras.layers.Dense(10, activation=None)\n", 68 | " ])\n", 69 | "\n", 70 | "comp(model)\n", 71 | "model.build(input_shape=(None, 28, 28, 1))\n", 72 | "\n", 73 | "# pretrain the model\n", 74 | "model.fit(dataset, epochs=3, validation_data=val_set)\n", 75 | "\n", 76 | "l = 100\n", 77 | "\n", 78 | "# generate key set\n", 79 | "true_advs, false_advs = gen_adversaries(model, l, dataset, 0.1)\n", 80 | "\n", 81 | "# In case that not the full number of adversaries could be generated a reduced amount is returned\n", 82 | "assert(len(true_advs + false_advs) == l)\n", 83 | "\n", 84 | "key_set_x = tf.data.Dataset.from_tensor_slices([x for x, y in true_advs + false_advs])\n", 85 | "key_set_y = tf.data.Dataset.from_tensor_slices([y for x, y in true_advs + false_advs])\n", 86 | "key_set = tf.data.Dataset.zip((key_set_x, key_set_y)).batch(128)" 87 | ] 88 | }, 89 | { 90 | "cell_type": "code", 91 | "execution_count": 4, 92 | "metadata": {}, 93 | "outputs": [ 94 | { 95 | "name": "stdout", 96 | "output_type": "stream", 97 | "text": [ 98 | "Epoch 1/5\n", 99 | "1/1 [==============================] - 1s 810ms/step - loss: 2.0916 - sparse_categorical_accuracy: 0.5000 - val_loss: 0.0658 - val_sparse_categorical_accuracy: 0.9790\n", 100 | "Epoch 2/5\n", 101 | "1/1 [==============================] - 0s 454ms/step - loss: 1.5772 - sparse_categorical_accuracy: 0.5500 - val_loss: 0.0688 - val_sparse_categorical_accuracy: 0.9779\n", 102 | "Epoch 3/5\n", 103 | "1/1 [==============================] - 0s 452ms/step - loss: 0.9644 - sparse_categorical_accuracy: 0.6600 - val_loss: 0.0825 - val_sparse_categorical_accuracy: 0.9757\n", 104 | "Epoch 4/5\n", 105 | "1/1 [==============================] - 0s 480ms/step - loss: 0.5848 - sparse_categorical_accuracy: 0.8200 - val_loss: 0.0986 - val_sparse_categorical_accuracy: 0.9686\n", 106 | "Epoch 5/5\n", 107 | "1/1 [==============================] - 0s 488ms/step - loss: 0.3468 - sparse_categorical_accuracy: 0.8800 - val_loss: 0.1165 - val_sparse_categorical_accuracy: 0.9633\n" 108 | ] 109 | }, 110 | { 111 | "data": { 112 | "text/plain": [ 113 | "" 114 | ] 115 | }, 116 | "execution_count": 4, 117 | "metadata": {}, 118 | "output_type": "execute_result" 119 | } 120 | ], 121 | "source": [ 122 | "# reset the optimizer and embed the watermark\n", 123 | "comp(model)\n", 124 | "model.fit(key_set, epochs=5, validation_data=val_set)" 125 | ] 126 | }, 127 | { 128 | "cell_type": "code", 129 | "execution_count": 5, 130 | "metadata": {}, 131 | "outputs": [], 132 | "source": [ 133 | "info = verify(model, key_set, 0.05)" 134 | ] 135 | }, 136 | { 137 | "cell_type": "code", 138 | "execution_count": 6, 139 | "metadata": {}, 140 | "outputs": [ 141 | { 142 | "name": "stdout", 143 | "output_type": "stream", 144 | "text": [ 145 | "Model is ours and was successfully watermarked.\n" 146 | ] 147 | } 148 | ], 149 | "source": [ 150 | "if info[\"success\"]:\n", 151 | " print(\"Model is ours and was successfully watermarked.\")\n", 152 | "else:\n", 153 | " print(\"Model is not ours and was not successfully watermarked.\")" 154 | ] 155 | } 156 | ], 157 | "metadata": { 158 | "kernelspec": { 159 | "display_name": "Python 3", 160 | "language": "python", 161 | "name": "python3" 162 | }, 163 | "language_info": { 164 | "codemirror_mode": { 165 | "name": "ipython", 166 | "version": 3 167 | }, 168 | "file_extension": ".py", 169 | "mimetype": "text/x-python", 170 | "name": "python", 171 | "nbconvert_exporter": "python", 172 | "pygments_lexer": "ipython3", 173 | "version": "3.8.5" 174 | } 175 | }, 176 | "nbformat": 4, 177 | "nbformat_minor": 4 178 | } 179 | --------------------------------------------------------------------------------