Files
concrete/docs/user/advanced_examples/QuantizedLogisticRegression.ipynb

967 lines
119 KiB
Plaintext
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"cells": [
{
"cell_type": "markdown",
"id": "9b835b74",
"metadata": {},
"source": [
"# Quantized Logistic Regression\n",
"\n",
"Currently, **Concrete** only supports unsigned integers up to 7-bits. Nevertheless, we want to evaluate a logistic regression model with it. Luckily, we can make use of **quantization** to overcome this limitation!"
]
},
{
"cell_type": "markdown",
"id": "7d46edc9",
"metadata": {},
"source": [
"### Let's start by importing some libraries to develop our logistic regression model"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "858205d9",
"metadata": {},
"outputs": [],
"source": [
"import numpy as np\n",
"import torch"
]
},
{
"cell_type": "markdown",
"id": "ff9c1757",
"metadata": {},
"source": [
"### And some helpers for visualization"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "67330862",
"metadata": {},
"outputs": [],
"source": [
"%matplotlib inline\n",
"\n",
"import matplotlib.pyplot as plt\n",
"from IPython.display import display"
]
},
{
"cell_type": "markdown",
"id": "0df30d0e",
"metadata": {},
"source": [
"### We need an inputset, a handcrafted one for simplicity"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "caef5aed",
"metadata": {},
"outputs": [],
"source": [
"x = torch.tensor(\n",
" [\n",
" [1, 1],\n",
" [1, 1.5],\n",
" [1.5, 1.2],\n",
" [1, 2],\n",
" [2, 1],\n",
" [4, 1],\n",
" [4, 1.5],\n",
" [3.5, 1.8],\n",
" [3, 2],\n",
" [4, 2],\n",
" ]\n",
").float()\n",
"y = torch.tensor(\n",
" [\n",
" [0],\n",
" [0],\n",
" [0],\n",
" [0],\n",
" [0],\n",
" [1],\n",
" [1],\n",
" [1],\n",
" [1],\n",
" [1],\n",
" ]\n",
").float()"
]
},
{
"cell_type": "markdown",
"id": "b16cd2e1",
"metadata": {},
"source": [
"### Let's visualize our inputset to get a grasp of it"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "ad72aad0",
"metadata": {},
"outputs": [],
"source": [
"plt.ioff()\n",
"fig, ax = plt.subplots(1)"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "ec57fede",
"metadata": {},
"outputs": [
{
"data": {
"image/png": "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\n",
"text/plain": [
"<Figure size 432x288 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"x_min, x_max = x[:, 0].min(), x[:, 0].max()\n",
"x_deviation = x_max - x_min\n",
"\n",
"y_min, y_max = x[:, 1].min(), x[:, 1].max()\n",
"y_deviation = y_max - y_min\n",
"\n",
"ax.set_xlim(x_min - (x_deviation / 10), x_max + (x_deviation / 10))\n",
"ax.set_ylim(y_min - (y_deviation / 10), y_max + (y_deviation / 10))\n",
"\n",
"ax.scatter(\n",
" np.array([x_i[0] for x_i, y_i in zip(x, y) if y_i == 0], dtype=np.float32),\n",
" np.array([x_i[1] for x_i, y_i in zip(x, y) if y_i == 0], dtype=np.float32),\n",
" marker=\"x\",\n",
" color=\"red\",\n",
")\n",
"ax.scatter(\n",
" np.array([x_i[0] for x_i, y_i in zip(x, y) if y_i == 1], dtype=np.float32),\n",
" np.array([x_i[1] for x_i, y_i in zip(x, y) if y_i == 1], dtype=np.float32),\n",
" marker=\"o\",\n",
" color=\"blue\",\n",
")\n",
"display(fig)"
]
},
{
"cell_type": "markdown",
"id": "996fbe05",
"metadata": {},
"source": [
"### Now, we need a model so let's define it"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "06ed91dd",
"metadata": {},
"outputs": [],
"source": [
"class Model(torch.nn.Module):\n",
" def __init__(self, n):\n",
" super(Model, self).__init__()\n",
" self.fc = torch.nn.Linear(n, 1)\n",
"\n",
" def forward(self, x):\n",
" output = torch.sigmoid(self.fc(x))\n",
" return output"
]
},
{
"cell_type": "markdown",
"id": "cd74c5e7",
"metadata": {},
"source": [
"### And create one\n",
"\n",
"The main purpose of this tutorial is not to train a logistic regression model but to use it homomorphically. So we will not discuss about how the model is trained."
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "b8f8f95b",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Epoch: 1 | Loss: 0.693336546421051\n",
"Epoch: 101 | Loss: 0.1125209778547287\n",
"Epoch: 201 | Loss: 0.07049673795700073\n",
"Epoch: 301 | Loss: 0.050856731832027435\n",
"Epoch: 401 | Loss: 0.039525073021650314\n",
"Epoch: 501 | Loss: 0.0322115495800972\n",
"Epoch: 601 | Loss: 0.027129750698804855\n",
"Epoch: 701 | Loss: 0.023406751453876495\n",
"Epoch: 801 | Loss: 0.02056846395134926\n",
"Epoch: 901 | Loss: 0.018336370587348938\n",
"Epoch: 1001 | Loss: 0.01653693988919258\n",
"Epoch: 1101 | Loss: 0.015056520700454712\n",
"Epoch: 1201 | Loss: 0.013817812316119671\n",
"Epoch: 1301 | Loss: 0.012766523286700249\n",
"Epoch: 1401 | Loss: 0.01186333317309618\n",
"Epoch: 1501 | Loss: 0.011079175397753716\n",
"Epoch: 1601 | Loss: 0.010392050258815289\n",
"Epoch: 1701 | Loss: 0.009785104542970657\n",
"Epoch: 1801 | Loss: 0.009245104156434536\n",
"Epoch: 1901 | Loss: 0.008761593140661716\n",
"Epoch: 2001 | Loss: 0.00832616537809372\n",
"Epoch: 2101 | Loss: 0.007932038977742195\n",
"Epoch: 2201 | Loss: 0.007573576178401709\n",
"Epoch: 2301 | Loss: 0.007246167398989201\n",
"Epoch: 2401 | Loss: 0.006945951841771603\n",
"Epoch: 2501 | Loss: 0.006669704802334309\n",
"Epoch: 2601 | Loss: 0.006414605770260096\n",
"Epoch: 2701 | Loss: 0.006178391166031361\n",
"Epoch: 2801 | Loss: 0.00595900509506464\n",
"Epoch: 2901 | Loss: 0.005754708778113127\n",
"Epoch: 3001 | Loss: 0.005564006045460701\n",
"Epoch: 3101 | Loss: 0.0053855921141803265\n",
"Epoch: 3201 | Loss: 0.005218283273279667\n",
"Epoch: 3301 | Loss: 0.005061114672571421\n",
"Epoch: 3401 | Loss: 0.004913175944238901\n",
"Epoch: 3501 | Loss: 0.004773670341819525\n"
]
}
],
"source": [
"model = Model(x.shape[1])\n",
"\n",
"optimizer = torch.optim.SGD(model.parameters(), lr=1)\n",
"criterion = torch.nn.BCELoss()\n",
"\n",
"epochs = 3501\n",
"for e in range(1, epochs + 1):\n",
" optimizer.zero_grad()\n",
"\n",
" out = model(x)\n",
" loss = criterion(out, y)\n",
"\n",
" loss.backward()\n",
" optimizer.step()\n",
"\n",
" if e % 100 == 1 or e == epochs:\n",
" print(\"Epoch:\", e, \"|\", \"Loss:\", loss.item())"
]
},
{
"cell_type": "markdown",
"id": "b608faef",
"metadata": {},
"source": [
"### Time to make some predictions"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "97eaf932",
"metadata": {},
"outputs": [],
"source": [
"contour_plot_x_data = np.linspace(x_min - (x_deviation / 10), x_max + 2 * (x_deviation / 10), 100)\n",
"contour_plot_y_data = np.linspace(y_min - (y_deviation / 10), y_max + 2 * (y_deviation / 10), 100)\n",
"contour_plot_x_data, contour_plot_y_data = np.meshgrid(contour_plot_x_data, contour_plot_y_data)\n",
"\n",
"inputs = np.stack((contour_plot_x_data.flatten(), contour_plot_y_data.flatten()), axis=1)\n",
"predictions = model(torch.tensor(inputs).float()).detach().numpy()"
]
},
{
"cell_type": "markdown",
"id": "8fb62d52",
"metadata": {},
"source": [
"### Let's visualize our predictions to see how our model performs"
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "bc999411",
"metadata": {},
"outputs": [
{
"data": {
"image/png": "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\n",
"text/plain": [
"<Figure size 432x288 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"contour = ax.contourf(\n",
" contour_plot_x_data,\n",
" contour_plot_y_data,\n",
" predictions.round().reshape(contour_plot_x_data.shape),\n",
" cmap=\"gray\",\n",
" alpha=0.50,\n",
")\n",
"display(fig)"
]
},
{
"cell_type": "markdown",
"id": "cf05e044",
"metadata": {},
"source": [
"### As a bonus let's inspect the model parameters"
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "8f3236fb",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[[5.28773165]\n",
" [2.6065383 ]]\n",
"-16.89045524597168\n"
]
}
],
"source": [
"w = np.array(model.fc.weight.flatten().tolist()).reshape((-1, 1))\n",
"b = model.fc.bias.flatten().tolist()[0]\n",
"\n",
"print(w)\n",
"print(b)"
]
},
{
"cell_type": "markdown",
"id": "44f1af11",
"metadata": {},
"source": [
"They are floating point numbers and we can't directly work with them!"
]
},
{
"cell_type": "markdown",
"id": "dd440b8d",
"metadata": {},
"source": [
"### So, let's abstract quantization\n",
"\n",
"Here is a quick summary of quantization. We have a range of values and we want to represent them using small number of bits (n). To do this, we split the range into 2^n sections and map each section to a value. Here is a visualization of the process!"
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "6314bb91",
"metadata": {},
"outputs": [
{
"data": {
"image/svg+xml": [
"<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" version=\"1.1\" width=\"420px\" height=\"195px\" viewBox=\"-0.5 -0.5 420 195\" content=\"&lt;mxfile host=&quot;app.diagrams.net&quot; modified=&quot;2021-08-13T09:47:25.144Z&quot; agent=&quot;5.0 (X11)&quot; etag=&quot;5QhM0DGu1eUjmjeXuyFL&quot; version=&quot;14.9.6&quot; type=&quot;device&quot;&gt;&lt;diagram id=&quot;6rZNNX4_K12e_kCXuZoG&quot; name=&quot;Page-1&quot;&gt;7Zzdb5s6FMD/mkjdw66MHZL0sUl376SrStM6rc8euAGNYAZOk/avnw2YD5t8QAmhJA+tyLFzbM7v2D7HdjtCi9X2vxAHzgO1iTeCwN6O0P0IQhPd8t9C8JoI0MRMBMvQtRORkQse3TeSCkEqXbs2iUoVGaUec4Oy0KK+TyxWkuEwpJtytWfqlVsN8JJogkcLe7r0ybWZk0hnJsjlX4m7dGTLBkhLVlhWTgWRg226KYjQlxFahJSy5Gm1XRBP2E7aJfnevztKs46FxGfHfOFpYrx9//Hw//Lpq8mc+yd4H5mfUy0v2FunL5x2lr1KCxDfvhOG5J8sD0eRa43QPGI4ZLrYYSuPCwz+mOghtmbevL9GZgXuPYSuCAtfeZVNbmdpZqdgYikLiYeZ+1JWj1Pcy0xd1sI36vKGIUg905ikelLHhGNQVhHRdWiR9FtFuyqKTHhAETfVkjBNEX8ovHYuirHVQAhrIfSpTz4WKAgU+5oNQWmKULeg0NBBjdsCpSrqGNR46KBmbYFSFXUMyhw4KKQuLU1BaYo6BjUZOig1mGgMSlXUMajpwEGN2womNEUdg5oNHVRbwYSmqGNQt0MH1VYwoSnqGJTccaidC38gWFoKBKfNYGmRn6poByxuPfxaqBaICtGeDquBC5ju75e6fpbr84ekB+16DmzoOR9/F0Xzg+ltM4dCqmeqik49+utl50NieND0RzNUl9quGdZL3AfF8JDpj2Z4aECfmmG9nH5QDNuaS7XcpGuGVen+xOPWmtvuC39ciseV699sP8kC3lChTEPOyJaVaUYspL/Jgno0zOPmZ9fzFBH23KUvXIJDJlw+fyEhcy3s3aUFK9e2RTPzjeMy8hhgS7S5CXHAZSFd+zYRLwuybgkFZFvXh3YFKNJ5Cj42rvAxCHa7U4lfbVh6yr/C25jMUBloA+3sDBpl8zaOnNgsRpmKkH/DjFvajyUQoIyVPMOEH2A+hLdlTAiMm82HqiI4UxSdej6s2gMozIcFzJM/a3EePH+mPvscxafhd7yCAYJtTEyW5xNlokfUf5eiBz7U8qk4UVduQp2he/YCjI7i7DZR9ytspVdHmqKXE6XoeHqfwmhr8ZqqI3J23olTKr6OrFOPLOM6sk45stQc/Pwjq97NoAsJSdRz1MYhiXYg23FIImle8e47fW2OV83yusbb6ArL4PG2lVCoijrHW+8+xZA2ybTjXDj+ZzJphlE7cazQdWqSVRcuGsRtWpx173LWPo9hCmFbUjInbEOIrxcsqB8Rax3D0Qp/ig5GO6O3XsZpo/fHZaq7GdPcRc4VmqGqtbtnOcMwkh5Y8x16OQpOka2Me5atoKrT25551DCGBLoOieoh0betMaSnADcbR6z7QPzEEgD7espzis1LJdpD8n5ZgZABO0Wkn3WDy+Fh9O0UDh11bF17OoVV4Xky/EQqLfrM1cR/cdh4eThl1/5cZ/zq1GByVGpgGJ36cAv5ZKVXbOWasdcX3ul5ENwIMV4JgP6vKEgKlc/cduL3W1BR9KnotmBnby/FR9U1L5tziw7a7SRbddXh/TNZ5R7IdebaEavKCwmZV5hnXnkPXJDIpqAL52Ya/eImG7ty289t2rPxNq463NW4vZFQZPxBsvetbggfHZWKdVqmm5fuCerf3xjy4KjgCbN2PIF/zP+3SHJckf+DFvTlLw==&lt;/diagram&gt;&lt;/mxfile&gt;\"><defs/><g><path d=\"M 14.37 84 L 361.63 84\" fill=\"none\" stroke=\"#000000\" stroke-miterlimit=\"10\" pointer-events=\"stroke\"/><path d=\"M 9.12 84 L 16.12 80.5 L 14.37 84 L 16.12 87.5 Z\" fill=\"#000000\" stroke=\"#000000\" stroke-miterlimit=\"10\" pointer-events=\"all\"/><path d=\"M 366.88 84 L 359.88 87.5 L 361.63 84 L 359.88 80.5 Z\" fill=\"#000000\" stroke=\"#000000\" stroke-miterlimit=\"10\" pointer-events=\"all\"/><path d=\"M 48 94 L 48 74\" fill=\"none\" stroke=\"#000000\" stroke-miterlimit=\"10\" pointer-events=\"stroke\"/><path d=\"M 88 94 L 88 74\" fill=\"none\" stroke=\"#000000\" stroke-miterlimit=\"10\" pointer-events=\"stroke\"/><path d=\"M 128 94 L 128 74\" fill=\"none\" stroke=\"#000000\" stroke-miterlimit=\"10\" pointer-events=\"stroke\"/><path d=\"M 168 94 L 168 74\" fill=\"none\" stroke=\"#000000\" stroke-miterlimit=\"10\" pointer-events=\"stroke\"/><path d=\"M 208 94 L 208 74\" fill=\"none\" stroke=\"#000000\" stroke-miterlimit=\"10\" pointer-events=\"stroke\"/><path d=\"M 248 94 L 248 74\" fill=\"none\" stroke=\"#000000\" stroke-miterlimit=\"10\" pointer-events=\"stroke\"/><path d=\"M 288 94 L 288 74\" fill=\"none\" stroke=\"#000000\" stroke-miterlimit=\"10\" pointer-events=\"stroke\"/><path d=\"M 328 94 L 328 74\" fill=\"none\" stroke=\"#000000\" stroke-miterlimit=\"10\" pointer-events=\"stroke\"/><path d=\"M 48 71 L 60.93 58.07 Q 68 51 78 51 L 98 51 Q 108 51 115.07 58.07 L 123.5 66.5\" fill=\"none\" stroke=\"#000000\" stroke-miterlimit=\"10\" pointer-events=\"stroke\"/><path d=\"M 127.21 70.21 L 119.78 67.73 L 123.5 66.5 L 124.73 62.78 Z\" fill=\"#000000\" stroke=\"#000000\" stroke-miterlimit=\"10\" pointer-events=\"all\"/><path d=\"M 134.37 123 L 141.63 123\" fill=\"none\" stroke=\"#000000\" stroke-miterlimit=\"10\" pointer-events=\"stroke\"/><path d=\"M 129.12 123 L 136.12 119.5 L 134.37 123 L 136.12 126.5 Z\" fill=\"#000000\" stroke=\"#000000\" stroke-miterlimit=\"10\" pointer-events=\"all\"/><path d=\"M 146.88 123 L 139.88 126.5 L 141.63 123 L 139.88 119.5 Z\" fill=\"#000000\" stroke=\"#000000\" stroke-miterlimit=\"10\" pointer-events=\"all\"/><path d=\"M 154.37 123 L 181.63 123\" fill=\"none\" stroke=\"#000000\" stroke-miterlimit=\"10\" pointer-events=\"stroke\"/><path d=\"M 149.12 123 L 156.12 119.5 L 154.37 123 L 156.12 126.5 Z\" fill=\"#000000\" stroke=\"#000000\" stroke-miterlimit=\"10\" pointer-events=\"all\"/><path d=\"M 186.88 123 L 179.88 126.5 L 181.63 123 L 179.88 119.5 Z\" fill=\"#000000\" stroke=\"#000000\" stroke-miterlimit=\"10\" pointer-events=\"all\"/><path d=\"M 194.37 123 L 221.63 123\" fill=\"none\" stroke=\"#000000\" stroke-miterlimit=\"10\" pointer-events=\"stroke\"/><path d=\"M 189.12 123 L 196.12 119.5 L 194.37 123 L 196.12 126.5 Z\" fill=\"#000000\" stroke=\"#000000\" stroke-miterlimit=\"10\" pointer-events=\"all\"/><path d=\"M 226.88 123 L 219.88 126.5 L 221.63 123 L 219.88 119.5 Z\" fill=\"#000000\" stroke=\"#000000\" stroke-miterlimit=\"10\" pointer-events=\"all\"/><path d=\"M 234.37 123 L 241.63 123\" fill=\"none\" stroke=\"#000000\" stroke-miterlimit=\"10\" pointer-events=\"stroke\"/><path d=\"M 229.12 123 L 236.12 119.5 L 234.37 123 L 236.12 126.5 Z\" fill=\"#000000\" stroke=\"#000000\" stroke-miterlimit=\"10\" pointer-events=\"all\"/><path d=\"M 246.88 123 L 239.88 126.5 L 241.63 123 L 239.88 119.5 Z\" fill=\"#000000\" stroke=\"#000000\" stroke-miterlimit=\"10\" pointer-events=\"all\"/><rect x=\"108\" y=\"94\" width=\"40\" height=\"20\" fill=\"none\" stroke=\"none\" pointer-events=\"all\"/><g transform=\"translate(-0.5 -0.5)\"><switch><foreignObject style=\"overflow: visible; text-align: left;\" pointer-events=\"none\" width=\"100%\" height=\"100%\" requiredFeatures=\"http://www.w3.org/TR/SVG11/feature#Extensibility\"><div xmlns=\"http://www.w3.org/1999/xhtml\" style=\"display: flex; align-items: unsafe center; justify-content: unsafe center; width: 38px; height: 1px; padding-top: 104px; margin-left: 109px;\"><div style=\"box-sizing: border-box; font-size: 0; text-align: center; \"><div style=\"display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; \"><div>min(x)</div></div></div></div></foreignObject><text x=\"128\" y=\"108\" fill=\"#000000\" font-family=\"Helvetica\" font-size=\"12px\" text-anchor=\"middle\">min(x)</text></switch></g><rect x=\"228\" y=\"94\" width=\"40\" height=\"20\" fill=\"none\" stroke=\"none\" pointer-events=\"all\"/><g transform=\"translate(-0.5 -0.5)\"><switch><foreignObject style=\"overflow: visible; text-align: left;\" pointer-events=\"none\" width=\"100%\" height=\"100%\" requiredFeatures=\"http://www.w3.org/TR/SVG11/feature#Extensibility\"><div xmlns=\"http://www.w3.org/1999/xhtml\" style=\"display: flex; align-items: unsafe center; justify-content: unsafe center; width: 38px; height: 1px; padding-top: 104px; margin-left: 229px;\"><div style=\"box-sizing: border-box; font-size: 0; text-align: center; \"><div style=\"display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; \">max(x)</div></div></div></foreignObject><text x=\"248\" y=\"108\" fill=\"#000000\" font-family=\"Helvetica\" font-size=\"12px\" text-anchor=\"middle\">max(x)</text></switch></g><path d=\"M 138 148 L 138 128\" fill=\"none\" stroke=\"#000000\" stroke-width=\"2\" stroke-miterlimit=\"10\" stroke-dasharray=\"2 6\" pointer-events=\"stroke\"/><rect x=\"118\" y=\"152\" width=\"40\" height=\"20\" fill=\"none\" stroke=\"none\" pointer-events=\"all\"/><g transform=\"translate(-0.5 -0.5)\"><switch><foreignObject style=\"overflow: visible; text-align: left;\" pointer-events=\"none\" width=\"100%\" height=\"100%\" requiredFeatures=\"http://www.w3.org/TR/SVG11/feature#Extensibility\"><div xmlns=\"http://www.w3.org/1999/xhtml\" style=\"display: flex; align-items: unsafe center; justify-content: unsafe center; width: 38px; height: 1px; padding-top: 162px; margin-left: 119px;\"><div style=\"box-sizing: border-box; font-size: 0; text-align: center; \"><div style=\"display: inline-block; font-size: 10px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; \"><div style=\"font-size: 10px\"><font style=\"font-size: 10px\">Map</font></div><div style=\"font-size: 10px\"><font style=\"font-size: 10px\">to 0<br style=\"font-size: 10px\"/></font></div></div></div></div></foreignObject><text x=\"138\" y=\"165\" fill=\"#000000\" font-family=\"Helvetica\" font-size=\"10px\" text-anchor=\"middle\">Map...</text></switch></g><rect x=\"148\" y=\"152\" width=\"40\" height=\"20\" fill=\"none\" stroke=\"none\" pointer-events=\"all\"/><g transform=\"translate(-0.5 -0.5)\"><switch><foreignObject style=\"overflow: visible; text-align: left;\" pointer-events=\"none\" width=\"100%\" height=\"100%\" requiredFeatures=\"http://www.w3.org/TR/SVG11/feature#Extensibility\"><div xmlns=\"http://www.w3.org/1999/xhtml\" style=\"display: flex; align-items: unsafe center; justify-content: unsafe center; width: 38px; height: 1px; padding-top: 162px; margin-left: 149px;\"><div style=\"box-sizing: border-box; font-size: 0; text-align: center; \"><div style=\"display: inline-block; font-size: 10px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; \"><div style=\"font-size: 10px\"><font style=\"font-size: 10px\">Map</font></div><div style=\"font-size: 10px\"><font style=\"font-size: 10px\">to 1<br style=\"font-size: 10px\"/></font></div></div></div></div></foreignObject><text x=\"168\" y=\"165\" fill=\"#000000\" font-family=\"Helvetica\" font-size=\"10px\" text-anchor=\"middle\">Map...</text></switch></g><path d=\"M 168 148 L 168 128\" fill=\"none\" stroke=\"#000000\" stroke-width=\"2\" stroke-miterlimit=\"10\" stroke-dasharray=\"2 6\" pointer-events=\"stroke\"/><path d=\"M 208 148 L 208 128\" fill=\"none\" stroke=\"#000000\" stroke-width=\"2\" stroke-miterlimit=\"10\" stroke-dasharray=\"2 6\" pointer-events=\"stroke\"/><path d=\"M 238 148 L 238 128\" fill=\"none\" stroke=\"#000000\" stroke-width=\"2\" stroke-miterlimit=\"10\" stroke-dasharray=\"2 6\" pointer-events=\"stroke\"/><path d=\"M 294.37 68.66 L 321.63 68.66\" fill=\"none\" stroke=\"#000000\" stroke-miterlimit=\"10\" pointer-events=\"stroke\"/><path d=\"M 289.12 68.66 L 296.12 65.16 L 294.37 68.66 L 296.12 72.16 Z\" fill=\"#000000\" stroke=\"#000000\" stroke-miterlimit=\"10\" pointer-events=\"all\"/><path d=\"M 326.88 68.66 L 319.88 72.16 L 321.63 68.66 L 319.88 65.16 Z\" fill=\"#000000\" stroke=\"#000000\" stroke-miterlimit=\"10\" pointer-events=\"all\"/><rect x=\"288\" y=\"18.66\" width=\"40\" height=\"20\" fill=\"none\" stroke=\"none\" pointer-events=\"all\"/><g transform=\"translate(-0.5 -0.5)\"><switch><foreignObject style=\"overflow: visible; text-align: left;\" pointer-events=\"none\" width=\"100%\" height=\"100%\" requiredFeatures=\"http://www.w3.org/TR/SVG11/feature#Extensibility\"><div xmlns=\"http://www.w3.org/1999/xhtml\" style=\"display: flex; align-items: unsafe center; justify-content: unsafe center; width: 38px; height: 1px; padding-top: 29px; margin-left: 289px;\"><div style=\"box-sizing: border-box; font-size: 0; text-align: center; \"><div style=\"display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; \"><font style=\"font-size: 10px\">Distance<br/>Between<br/>Consecutive<br/>Values</font></div></div></div></foreignObject><text x=\"308\" y=\"32\" fill=\"#000000\" font-family=\"Helvetica\" font-size=\"12px\" text-anchor=\"middle\">Distan...</text></switch></g><rect x=\"188\" y=\"152\" width=\"40\" height=\"20\" fill=\"none\" stroke=\"none\" pointer-events=\"all\"/><g transform=\"translate(-0.5 -0.5)\"><switch><foreignObject style=\"overflow: visible; text-align: left;\" pointer-events=\"none\" width=\"100%\" height=\"100%\" requiredFeatures=\"http://www.w3.org/TR/SVG11/feature#Extensibility\"><div xmlns=\"http://www.w3.org/1999/xhtml\" style=\"display: flex; align-items: unsafe center; justify-content: unsafe center; width: 38px; height: 1px; padding-top: 162px; margin-left: 189px;\"><div style=\"box-sizing: border-box; font-size: 0; text-align: center; \"><div style=\"display: inline-block; font-size: 10px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; \"><div style=\"font-size: 10px\"><font style=\"font-size: 10px\">Map</font></div><div style=\"font-size: 10px\"><font style=\"font-size: 10px\">to 2</font></div></div></div></div></foreignObject><text x=\"208\" y=\"165\" fill=\"#000000\" font-family=\"Helvetica\" font-size=\"10px\" text-anchor=\"middle\">Map...</text></switch></g><rect x=\"218\" y=\"152\" width=\"40\" height=\"20\" fill=\"none\" stroke=\"none\" pointer-events=\"all\"/><g transform=\"translate(-0.5 -0.5)\"><switch><foreignObject style=\"overflow: visible; text-align: left;\" pointer-events=\"none\" width=\"100%\" height=\"100%\" requiredFeatures=\"http://www.w3.org/TR/SVG11/feature#Extensibility\"><div xmlns=\"http://www.w3.org/1999/xhtml\" style=\"display: flex; align-items: unsafe center; justify-content: unsafe center; width: 38px; height: 1px; padding-top: 162px; margin-left: 219px;\"><div style=\"box-sizing: border-box; font-size: 0; text-align: center; \"><div style=\"display: inline-block; font-size: 10px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; \"><div style=\"font-size: 10px\"><font style=\"font-size: 10px\">Map</font></div><div style=\"font-size: 10px\"><font style=\"font-size: 10px\">to 3</font></div></div></div></div></foreignObject><text x=\"238\" y=\"165\" fill=\"#000000\" font-family=\"Helvetica\" font-size=\"10px\" text-anchor=\"middle\">Map...</text></switch></g><rect x=\"128\" y=\"174\" width=\"120\" height=\"20\" fill=\"none\" stroke=\"none\" pointer-events=\"all\"/><g transform=\"translate(-0.5 -0.5)\"><switch><foreignObject style=\"overflow: visible; text-align: left;\" pointer-events=\"none\" width=\"100%\" height=\"100%\" requiredFeatures=\"http://www.w3.org/TR/SVG11/feature#Extensibility\"><div xmlns=\"http://www.w3.org/1999/xhtml\" style=\"display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 184px; margin-left: 129px;\"><div style=\"box-sizing: border-box; font-size: 0; text-align: center; \"><div style=\"display: inline-block; font-size: 10px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; \">(when n = 2)</div></div></div></foreignObject><text x=\"188\" y=\"187\" fill=\"#000000\" font-family=\"Helvetica\" font-size=\"10px\" text-anchor=\"middle\">(when n = 2)</text></switch></g><rect x=\"28\" y=\"94\" width=\"40\" height=\"20\" fill=\"none\" stroke=\"none\" pointer-events=\"all\"/><g transform=\"translate(-0.5 -0.5)\"><switch><foreignObject style=\"overflow: visible; text-align: left;\" pointer-events=\"none\" width=\"100%\" height=\"100%\" requiredFeatures=\"http://www.w3.org/TR/SVG11/feature#Extensibility\"><div xmlns=\"http://www.w3.org/1999/xhtml\" style=\"display: flex; align-items: unsafe center; justify-content: unsafe center; width: 38px; height: 1px; padding-top: 104px; margin-left: 29px;\"><div style=\"box-sizing: border-box; font-size: 0; text-align: center; \"><div style=\"display: inline-block; font-size: 10px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; \">0</div></div></div></foreignObject><text x=\"48\" y=\"107\" fill=\"#000000\" font-family=\"Helvetica\" font-size=\"10px\" text-anchor=\"middle\">0</text></switch></g><rect x=\"308\" y=\"18.66\" width=\"110\" height=\"20\" fill=\"none\" stroke=\"none\" pointer-events=\"all\"/><g transform=\"translate(-0.5 -0.5)\"><switch><foreignObject style=\"overflow: visible; text-align: left;\" pointer-events=\"none\" width=\"100%\" height=\"100%\" requiredFeatures=\"http://www.w3.org/TR/SVG11/feature#Extensibility\"><div xmlns=\"http://www.w3.org/1999/xhtml\" style=\"display: flex; align-items: unsafe center; justify-content: unsafe center; width: 108px; height: 1px; padding-top: 29px; margin-left: 309px;\"><div style=\"box-sizing: border-box; font-size: 0; text-align: center; \"><div style=\"display: inline-block; font-size: 10px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; \"><div><font style=\"font-size: 12px\">= 1 / scale</font></div><div><font style=\"font-size: 12px\">= 1 / q</font></div></div></div></div></foreignObject><text x=\"363\" y=\"32\" fill=\"#000000\" font-family=\"Helvetica\" font-size=\"10px\" text-anchor=\"middle\">= 1 / scale...</text></switch></g><rect x=\"128\" y=\"24\" width=\"140\" height=\"20\" fill=\"none\" stroke=\"none\" pointer-events=\"all\"/><g transform=\"translate(-0.5 -0.5)\"><switch><foreignObject style=\"overflow: visible; text-align: left;\" pointer-events=\"none\" width=\"100%\" height=\"100%\" requiredFeatures=\"http://www.w3.org/TR/SVG11/feature#Extensibility\"><div xmlns=\"http://www.w3.org/1999/xhtml\" style=\"display: flex; align-items: unsafe center; justify-content: unsafe center; width: 138px; height: 1px; padding-top: 34px; margin-left: 129px;\"><div style=\"box-sizing: border-box; font-size: 0; text-align: center; \"><div style=\"display: inline-block; font-size: 10px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; \"><font style=\"font-size: 12px\">x =</font><font style=\"font-size: 12px\"> (x   + zp  ) / q </font></div></div></div></foreignObject><text x=\"198\" y=\"37\" fill=\"#000000\" font-family=\"Helvetica\" font-size=\"10px\" text-anchor=\"middle\">x = (x   + zp  ) / q </text></switch></g><rect x=\"167\" y=\"29\" width=\"40\" height=\"20\" fill=\"none\" stroke=\"none\" pointer-events=\"all\"/><g transform=\"translate(-0.5 -0.5)\"><switch><foreignObject style=\"overflow: visible; text-align: left;\" pointer-events=\"none\" width=\"100%\" height=\"100%\" requiredFeatures=\"http://www.w3.org/TR/SVG11/feature#Extensibility\"><div xmlns=\"http://www.w3.org/1999/xhtml\" style=\"display: flex; align-items: unsafe center; justify-content: unsafe center; width: 38px; height: 1px; padding-top: 39px; margin-left: 168px;\"><div style=\"box-sizing: border-box; font-size: 0; text-align: center; \"><div style=\"display: inline-block; font-size: 10px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; \"><div><font style=\"font-size: 10px\">q</font></div></div></div></div></foreignObject><text x=\"187\" y=\"42\" fill=\"#000000\" font-family=\"Helvetica\" font-size=\"10px\" text-anchor=\"middle\">q</text></switch></g><rect x=\"199\" y=\"29\" width=\"40\" height=\"20\" fill=\"none\" stroke=\"none\" pointer-events=\"all\"/><g transform=\"translate(-0.5 -0.5)\"><switch><foreignObject style=\"overflow: visible; text-align: left;\" pointer-events=\"none\" width=\"100%\" height=\"100%\" requiredFeatures=\"http://www.w3.org/TR/SVG11/feature#Extensibility\"><div xmlns=\"http://www.w3.org/1999/xhtml\" style=\"display: flex; align-items: unsafe center; justify-content: unsafe center; width: 38px; height: 1px; padding-top: 39px; margin-left: 200px;\"><div style=\"box-sizing: border-box; font-size: 0; text-align: center; \"><div style=\"display: inline-block; font-size: 10px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; \"><div>x</div></div></div></div></foreignObject><text x=\"219\" y=\"42\" fill=\"#000000\" font-family=\"Helvetica\" font-size=\"10px\" text-anchor=\"middle\">x</text></switch></g><rect x=\"227\" y=\"29\" width=\"40\" height=\"20\" fill=\"none\" stroke=\"none\" pointer-events=\"all\"/><g transform=\"translate(-0.5 -0.5)\"><switch><foreignObject style=\"overflow: visible; text-align: left;\" pointer-events=\"none\" width=\"100%\" height=\"100%\" requiredFeatures=\"http://www.w3.org/TR/SVG11/feature#Extensibility\"><div xmlns=\"http://www.w3.org/1999/xhtml\" style=\"display: flex; align-items: unsafe center; justify-content: unsafe center; width: 38px; height: 1px; padding-top: 39px; margin-left: 228px;\"><div style=\"box-sizing: border-box; font-size: 0; text-align: center; \"><div style=\"display: inline-block; font-size: 10px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; \"><div>x</div></div></div></div></foreignObject><text x=\"247\" y=\"42\" fill=\"#000000\" font-family=\"Helvetica\" font-size=\"10px\" text-anchor=\"middle\">x</text></switch></g><rect x=\"48\" y=\"28\" width=\"80\" height=\"20\" fill=\"none\" stroke=\"none\" pointer-events=\"all\"/><g transform=\"translate(-0.5 -0.5)\"><switch><foreignObject style=\"overflow: visible; text-align: left;\" pointer-events=\"none\" width=\"100%\" height=\"100%\" requiredFeatures=\"http://www.w3.org/TR/SVG11/feature#Extensibility\"><div xmlns=\"http://www.w3.org/1999/xhtml\" style=\"display: flex; align-items: unsafe center; justify-content: unsafe center; width: 78px; height: 1px; padding-top: 38px; margin-left: 49px;\"><div style=\"box-sizing: border-box; font-size: 0; text-align: center; \"><div style=\"display: inline-block; font-size: 10px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; \"><div>zero point<br/></div><div>zp = 2</div></div></div></div></foreignObject><text x=\"88\" y=\"41\" fill=\"#000000\" font-family=\"Helvetica\" font-size=\"10px\" text-anchor=\"middle\">zero point...</text></switch></g></g><switch><g requiredFeatures=\"http://www.w3.org/TR/SVG11/feature#Extensibility\"/><a transform=\"translate(0,-5)\" xlink:href=\"https://www.diagrams.net/doc/faq/svg-export-text-problems\" target=\"_blank\"><text text-anchor=\"middle\" font-size=\"10px\" x=\"50%\" y=\"100%\">Viewer does not support full SVG 1.1</text></a></switch></svg>"
],
"text/plain": [
"<IPython.core.display.SVG object>"
]
},
"execution_count": 11,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from IPython.display import SVG\n",
"SVG(filename=\"figures/QuantizationVisualized.svg\")"
]
},
{
"cell_type": "markdown",
"id": "2c33faf9",
"metadata": {},
"source": [
"If you want to learn more, head to https://intellabs.github.io/distiller/algo_quantization.html"
]
},
{
"cell_type": "code",
"execution_count": 12,
"id": "9013f7e0",
"metadata": {},
"outputs": [],
"source": [
"class QuantizationParameters:\n",
" def __init__(self, q, zp, n):\n",
" # q = scale factor = 1 / distance between consecutive values\n",
" # zp = zero point which is used to determine the beginning of the quantized range\n",
" # (quantized 0 = the beginning of the quantized range = zp * distance between consecutive values)\n",
" # n = number of bits\n",
" \n",
" # e.g.,\n",
" \n",
" # n = 2\n",
" # zp = 2\n",
" # q = 0.66\n",
" # distance between consecutive values = 1 / q = 1.5151\n",
" \n",
" # quantized 0 = zp / q = zp * distance between consecutive values = 3.0303\n",
" # quantized 1 = quantized 0 + distance between consecutive values = 4.5454\n",
" # quantized 2 = quantized 1 + distance between consecutive values = 6.0606\n",
" # quantized 3 = quantized 2 + distance between consecutive values = 7.5757\n",
" \n",
" self.q = q\n",
" self.zp = zp\n",
" self.n = n\n",
"\n",
"class QuantizedArray:\n",
" def __init__(self, values, parameters):\n",
" # values = quantized values\n",
" # parameters = parameters used during quantization\n",
" \n",
" # e.g.,\n",
" \n",
" # values = [1, 0, 2, 1]\n",
" # parameters = QuantizationParameters(q=0.66, zp=2, n=2)\n",
" \n",
" # original array = [4.5454, 3.0303, 6.0606, 4.5454]\n",
" \n",
" self.values = np.array(values)\n",
" self.parameters = parameters\n",
"\n",
" @staticmethod\n",
" def of(x, n):\n",
" if not isinstance(x, np.ndarray):\n",
" x = np.array(x)\n",
"\n",
" min_x = x.min()\n",
" max_x = x.max()\n",
"\n",
" if min_x == max_x: # encoding single valued arrays\n",
" \n",
" if min_x == 0.0: # encoding 0s\n",
" \n",
" # dequantization = (x_q + zp_x) / q_x = 0 --> q_x = 1 && zp_x = 0 && x_q = 0\n",
" q_x = 1\n",
" zp_x = 0\n",
" x_q = np.zeros(x.shape, dtype=np.uint)\n",
" \n",
" elif min_x < 0.0: # encoding negative scalars\n",
" \n",
" # dequantization = (x_q + zp_x) / q_x = -x --> q_x = 1 / x & zp_x = -1 & x_q = 0\n",
" q_x = abs(1 / min_x)\n",
" zp_x = -1\n",
" x_q = np.zeros(x.shape, dtype=np.uint)\n",
" \n",
" else: # encoding positive scalars\n",
" \n",
" # dequantization = (x_q + zp_x) / q_x = x --> q_x = 1 / x & zp_x = 0 & x_q = 1\n",
" q_x = 1 / min_x\n",
" zp_x = 0\n",
" x_q = np.ones(x.shape, dtype=np.uint)\n",
" \n",
" else: # encoding multi valued arrays\n",
" \n",
" # distance between consecutive values = range of x / number of different quantized values = (max_x - min_x) / (2^n - 1)\n",
" # q = 1 / distance between consecutive values\n",
" q_x = (2**n - 1) / (max_x - min_x)\n",
" \n",
" # zp = what should be added to 0 to get min_x -> min_x = (0 + zp) / q -> zp = min_x * q\n",
" zp_x = int(round(min_x * q_x))\n",
" \n",
" # x = (x_q + zp) / q -> x_q = (x * q) - zp\n",
" x_q = ((q_x * x) - zp_x).round().astype(np.uint)\n",
"\n",
" return QuantizedArray(x_q, QuantizationParameters(q_x, zp_x, n))\n",
"\n",
" def dequantize(self):\n",
" # x = (x_q + zp) / q\n",
" # x = (x_q + zp) / q\n",
" return (self.values.astype(np.float32) + float(self.parameters.zp)) / self.parameters.q\n",
"\n",
" def affine(self, w, b, min_y, max_y, n_y):\n",
" # the formulas used in this method was derived from the following equations\n",
" #\n",
" # x = (x_q + zp_x) / q_x\n",
" # w = (w_q + zp_w) / q_w\n",
" # b = (b_q + zp_b) / q_b\n",
" #\n",
" # (x * w) + b = ((x_q + zp_x) / q_x) * ((w_q + zp_w) / q_w) + ((b_q + zp_b) / q_b)\n",
" # = y = (y_q + zp_y) / q_y\n",
" #\n",
" # So, ((x_q + zp_x) / q_x) * ((w_q + zp_w) / q_w) + ((b_q + zp_b) / q_b) = (y_q + zp_y) / q_y\n",
" # We can calculate zp_y and q_y from min_y, max_y, n_y. So, the only unknown is y_q and it can be solved.\n",
"\n",
" x_q = self.values\n",
" w_q = w.values\n",
" b_q = b.values\n",
"\n",
" q_x = self.parameters.q\n",
" q_w = w.parameters.q\n",
" q_b = b.parameters.q\n",
"\n",
" zp_x = self.parameters.zp\n",
" zp_w = w.parameters.zp\n",
" zp_b = b.parameters.zp\n",
"\n",
" q_y = (2**n_y - 1) / (max_y - min_y)\n",
" zp_y = int(round(min_y * q_y))\n",
"\n",
" y_q = (q_y / (q_x * q_w)) * ((x_q + zp_x) @ (w_q + zp_w) + (q_x * q_w / q_b) * (b_q + zp_b))\n",
" y_q -= min_y * q_y\n",
" y_q = y_q.round().clip(0, 2**n_y - 1).astype(np.uint)\n",
"\n",
" return QuantizedArray(y_q, QuantizationParameters(q_y, zp_y, n_y))\n",
"\n",
"class QuantizedFunction:\n",
" def __init__(self, table, input_parameters=None, output_parameters=None):\n",
" self.table = table\n",
" self.input_parameters = input_parameters\n",
" self.output_parameters = output_parameters\n",
"\n",
" @staticmethod\n",
" def of(f, input_bits, output_bits):\n",
" domain = np.array(range(2**input_bits), dtype=np.uint)\n",
" table = f(domain).round().clip(0, 2**output_bits - 1).astype(np.uint)\n",
" return QuantizedFunction(table)\n",
"\n",
" @staticmethod\n",
" def plain(f, input_parameters, output_bits):\n",
" n = input_parameters.n\n",
"\n",
" domain = np.array(range(2**n), dtype=np.uint)\n",
" inputs = QuantizedArray(domain, input_parameters).dequantize()\n",
"\n",
" outputs = f(inputs)\n",
" quantized_outputs = QuantizedArray.of(outputs, output_bits)\n",
"\n",
" table = quantized_outputs.values\n",
" output_parameters = quantized_outputs.parameters\n",
"\n",
" return QuantizedFunction(table, input_parameters, output_parameters)\n",
"\n",
" def apply(self, x):\n",
" assert x.parameters == self.input_parameters\n",
" return QuantizedArray(self.table[x.values], self.output_parameters)"
]
},
{
"cell_type": "markdown",
"id": "477c431f",
"metadata": {},
"source": [
"### Let's quantize our model parameters\n",
"\n",
"Since the parameters only consist of scalars, we can use a single bit quantization."
]
},
{
"cell_type": "code",
"execution_count": 13,
"id": "9a4dc030",
"metadata": {},
"outputs": [],
"source": [
"parameter_bits = 1\n",
"\n",
"w_q = QuantizedArray.of(w, parameter_bits)\n",
"b_q = QuantizedArray.of(b, parameter_bits)"
]
},
{
"cell_type": "markdown",
"id": "4592996c",
"metadata": {},
"source": [
"### And quantize our inputs"
]
},
{
"cell_type": "code",
"execution_count": 14,
"id": "5ba001fe",
"metadata": {},
"outputs": [],
"source": [
"input_bits = 5\n",
"\n",
"x = inputs\n",
"x_q = QuantizedArray.of(inputs, input_bits)"
]
},
{
"cell_type": "markdown",
"id": "5da2a8b9",
"metadata": {},
"source": [
"### Time to make quantized inference"
]
},
{
"cell_type": "code",
"execution_count": 15,
"id": "3f899676",
"metadata": {},
"outputs": [],
"source": [
"output_bits = 7\n",
"\n",
"intermediate = x @ w + b\n",
"intermediate_q = x_q.affine(w_q, b_q, intermediate.min(), intermediate.max(), output_bits)\n",
"\n",
"sigmoid = QuantizedFunction.plain(lambda x: 1 / (1 + np.exp(-x)), intermediate_q.parameters, output_bits)\n",
"y_q = sigmoid.apply(intermediate_q)\n",
"\n",
"quantized_predictions = y_q.dequantize()"
]
},
{
"cell_type": "markdown",
"id": "4ea9b4fa",
"metadata": {},
"source": [
"### And visualize the results"
]
},
{
"cell_type": "code",
"execution_count": 16,
"id": "5d46b7d8",
"metadata": {},
"outputs": [
{
"data": {
"image/png": "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\n",
"text/plain": [
"<Figure size 432x288 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"for column in contour.collections:\n",
" plt.gca().collections.remove(column)\n",
" \n",
"contour = ax.contourf(\n",
" contour_plot_x_data,\n",
" contour_plot_y_data,\n",
" quantized_predictions.round().reshape(contour_plot_x_data.shape),\n",
" cmap=\"gray\",\n",
" alpha=0.50,\n",
")\n",
"display(fig)"
]
},
{
"cell_type": "markdown",
"id": "483c5c17",
"metadata": {},
"source": [
"### Now it's time to make the inference homomorphic"
]
},
{
"cell_type": "code",
"execution_count": 17,
"id": "f15f9e12",
"metadata": {},
"outputs": [],
"source": [
"q_y = (2**output_bits - 1) / (intermediate.max() - intermediate.min())\n",
"zp_y = int(round(intermediate.min() * q_y))\n",
"\n",
"q_x = x_q.parameters.q\n",
"q_w = w_q.parameters.q\n",
"q_b = b_q.parameters.q\n",
"\n",
"zp_x = x_q.parameters.zp\n",
"zp_w = w_q.parameters.zp\n",
"zp_b = b_q.parameters.zp\n",
"\n",
"x_q = x_q.values\n",
"w_q = w_q.values\n",
"b_q = b_q.values"
]
},
{
"cell_type": "markdown",
"id": "be208937",
"metadata": {},
"source": [
"### Simplification to rescue!\n",
"\n",
"The `y_q` formula in `QuantizedArray.affine(...)` can be rewritten to make it easier to implement in homomorphically. Here is the breakdown.\n",
"```\n",
"(q_y / (q_x * q_w)) * ((x_q + zp_x) @ (w_q + zp_w) + (q_x * q_w / q_b) * (b_q + zp_b)) - (min_y * q_y)\n",
"^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^ ^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^\n",
"constant (c1) can be done constant (c2) constant (c3) constant (c4)\n",
" on the circuit \n",
" \n",
" ^^^^^^^^^^^^^^^^^^^^^^^^^^^\n",
" can be done on the circuit\n",
" \n",
"^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n",
"cannot be done on the circuit because of floating point operation so will be a single table lookup\n",
"```"
]
},
{
"cell_type": "markdown",
"id": "34c675ed",
"metadata": {},
"source": [
"### Let's import the Concrete numpy package now!"
]
},
{
"cell_type": "code",
"execution_count": 18,
"id": "72a84cac",
"metadata": {},
"outputs": [],
"source": [
"import concrete.numpy as hnp"
]
},
{
"cell_type": "code",
"execution_count": 19,
"id": "f8f197a2",
"metadata": {},
"outputs": [],
"source": [
"c1 = q_y / (q_x * q_w)\n",
"c2 = w_q + zp_w\n",
"c3 = (q_x * q_w / q_b) * (b_q + zp_b)\n",
"c4 = intermediate.min() * q_y\n",
"\n",
"def f(x):\n",
" values = ((c1 * (x + c3)) - c4).round().clip(0, 2**output_bits - 1).astype(np.uint)\n",
" after_affine_q = QuantizedArray(values, intermediate_q.parameters)\n",
" \n",
" sigmoid = QuantizedFunction.plain(lambda x: 1 / (1 + np.exp(-x)), after_affine_q.parameters, output_bits)\n",
" y_q = sigmoid.apply(after_affine_q)\n",
" \n",
" return y_q.values\n",
"\n",
"f_q = QuantizedFunction.of(f, output_bits, output_bits)\n",
"\n",
"table = hnp.LookupTable([int(entry) for entry in f_q.table])\n",
"\n",
"w_0 = int(c2.flatten()[0])\n",
"w_1 = int(c2.flatten()[1])\n",
"\n",
"def infer(x_0, x_1):\n",
" return table[((x_0 + zp_x) * w_0) + ((x_1 + zp_x) * w_1)]"
]
},
{
"cell_type": "markdown",
"id": "babb1a98",
"metadata": {},
"source": [
"### Let's compile our quantized inference function to its homomorphic equivalent"
]
},
{
"cell_type": "code",
"execution_count": 20,
"id": "b3a1d948",
"metadata": {},
"outputs": [],
"source": [
"inputset = []\n",
"for x_i in x_q:\n",
" inputset.append((int(x_i[0]), int(x_i[1])))\n",
" \n",
"circuit = hnp.compile_numpy_function(\n",
" infer,\n",
" {\n",
" \"x_0\": hnp.EncryptedScalar(hnp.Integer(input_bits, is_signed=False)),\n",
" \"x_1\": hnp.EncryptedScalar(hnp.Integer(input_bits, is_signed=False)),\n",
" },\n",
" inputset,\n",
")"
]
},
{
"cell_type": "markdown",
"id": "ab5ba39e",
"metadata": {},
"source": [
"### Here are some representations of the fhe circuit"
]
},
{
"cell_type": "code",
"execution_count": 21,
"id": "13ac665b",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"%0 = Constant(2) # ClearScalar<Integer<unsigned, 8 bits>>\n",
"%1 = Constant(1) # ClearScalar<Integer<unsigned, 8 bits>>\n",
"%2 = x_0 # EncryptedScalar<Integer<unsigned, 7 bits>>\n",
"%3 = Constant(6) # ClearScalar<Integer<unsigned, 8 bits>>\n",
"%4 = x_1 # EncryptedScalar<Integer<unsigned, 7 bits>>\n",
"%5 = Constant(6) # ClearScalar<Integer<unsigned, 8 bits>>\n",
"%6 = Add(2, 3) # EncryptedScalar<Integer<unsigned, 7 bits>>\n",
"%7 = Add(4, 5) # EncryptedScalar<Integer<unsigned, 7 bits>>\n",
"%8 = Mul(6, 0) # EncryptedScalar<Integer<unsigned, 7 bits>>\n",
"%9 = Mul(7, 1) # EncryptedScalar<Integer<unsigned, 7 bits>>\n",
"%10 = Add(8, 9) # EncryptedScalar<Integer<unsigned, 7 bits>>\n",
"%11 = TLU(10) # EncryptedScalar<Integer<unsigned, 7 bits>>\n",
"return(%11)\n",
"\n"
]
}
],
"source": [
"print(circuit)"
]
},
{
"cell_type": "code",
"execution_count": 22,
"id": "52101260",
"metadata": {},
"outputs": [
{
"data": {
"image/png": "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cz2TijvbU0aWLl3K7NmzGTt2rE4J5kE02WXDJ598QlJSEq+88kq2/22hQoUAMlyR2f6c/T2utHv3bi5duqTJTnmkevXqZWn4gae0p+tdvXqVQYMG0aVLF1q3bu32/avMabLLhiJFijB69GgmTpzIunXrsvVvK1euDJBhJ5cTJ04AEBkZmfsgb2Lz5s3kyZOHatWquXxfSmVXvXr1+PPPP2969cRT2tP1Xn31VS5evMjo0aPdvm91Y5rssqlr1660atWKp556KltTid13330AbN26Nd1r9ueaN2/unCBvYMuWLdx9993azVl5pPr16yMiGbaT1DylPaW2ZcsWxo4dy8cff0zx4sXdum91czYRH52MzoWOHj1K1apVefHFF7O8JlVSUhLVq1fnwoULHDx4kLCwMAASExOpXr060dHR7Nu3L+V5V6lVqxbNmzfn448/zva/LV26NKdOnSIhIcEFkSlllClThmeeeeaGtws8pT3ZJSQkUK9ePQoUKMDKlStzfL9Q25jLzNUzuxy47bbbePPNN3n33XfZs2dPlv5NQEAAU6ZMISoqit69e3Pq1CnOnTvHoEGD2L9/P5MmTXJ5w7x27Rq7d+/WyZ+VR6tXr95NO6l4QntK7eOPP2bPnj2MHz/eso4x6sY02eXQ0KFDqVGjBn379iUpKSlL/6ZBgwZs2LCBixcvUqlSJcqWLcv+/ftZtWoVDz30kIsjNpd34uPjs9U5ZfHixdhsNmw2GydOnCAxMTHl8eTJk10YrfJX9erVY+PGjTd9n9Xtye7IkSO88847jBw5MuVeYnZoG3MPvYyZC3/88Qd16tRhzJgx9O/f3+pwburTTz/l/fff58yZM1aHolSmVq5cyf3338+xY8cyHDTuaR588EFOnTrF1q1bbzhTirKUXsbMjRo1avDcc8/x0ksvpfQA82RZXelAKSvVqVOHwMBAr1jMddq0aSxfvvymU4Ip62myy6U333yTokWLMnToUKtDualNmzZpslMeL3/+/FSuXDnXg8td7ezZs7z00ksMHjyYe+65x+pw1E1ossul8PBwJk2axIIFC1i4cKHV4WTqn3/+4fDhw5rslFeoX79+tta2s8Jzzz1HWFgY77zzjtWhqCzQZOcE9913H926deOZZ55x+lRizrJ582ZsNpv2xFReoW7duvz2228kJiZaHUqGfv75Z7755hvG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lFI+T5OdUkopn6fJTimllM/TZKeUUsrnabJTSinl8zTZKaWU8nma7JRSSvk8TXZKKaV8niY7pZRSPk+TnVJKKZ+nyU4ppZTP02SnlFLK52myU0op5fM02SmllPJ5muyUUkr5PE12Sqmb2rFjB61ataJQoULkz5+fFi1asH79eqvDUirLNNkppW5o06ZNNGzYkPz587N3714OHz5M+fLladasGb/88ovV4SmVJTYREauDUEpZY86cOXTq1InM/gwkJSVRo0YNoqKiOHjwIOHh4QAkJiZStWpVrl69yv79+wkNDXVn2Epl11w9s1NKZWrNmjXs3r2bJ554IiXRAQQGBtKlSxeOHTvG4sWLLYxQqazRZKeUytSKFSsAqFOnTrrX7M8tX77crTEplROa7JRSmfrzzz8BKF26dLrXIiIiANi3b59bY1IqJzTZKaUydeHCBQDy5s2b7rV8+fIBcP78eXeGpFSOaLJTSuWIvVOLzWazOBKlbk6TnVIqU4UKFQLgypUr6V6zP2d/j1KeTJOdUipTlStXBuD48ePpXjtx4gQAkZGRbo1JqZzQZKeUytR9990HwNatW9O9Zn+uefPmbo1JqZzQZKeUylTTpk2pUqUK8+bNIyYmJuX5xMREZs2aRZkyZWjVqpWFESqVNZrslFKZCggIYMqUKURFRdG7d29OnTrFuXPnGDRoEPv372fSpEmEhYVZHaZSN6XJTil1Qw0aNGDDhg1cvHiRSpUqUbZsWfbv38+qVat46KGHrA5PqSwJsjoApZTnq1WrFkuWLLE6DKVyTM/slFJK+TxNdkoppXyeJjullFI+T5OdUkopn6fJTimllM/TZKeUUsrnabJTSinl8zTZKaWU8nma7JRSSvk8TXZKKaV8niY7pZRSPk+TnVJKKZ+nyU4ppZTP02SnlFLK5+kSP0r5iRMnTlC9enXi4+PTPJ8nTx7y58+f8thms3HPPffw888/uztEpVxGk51SfiIiIoI77riD3377DRHJ9H02m42WLVu6MTKlXE8vYyrlR3r06EFAwM2bfYcOHdwQjVLuo8lOKT/SqVOnG74eEBBAkyZNiIiIcFNESrmHJjul/EjRokVp1qwZgYGBGb5us9no3r27m6NSyvU02SnlZ7p3757pPTubzcZjjz3m5oiUcj1Ndkr5mccee4ygoPR904KCgmjZsiVFihSxICql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\n",
"text/plain": [
"<PIL.PngImagePlugin.PngImageFile image mode=RGBA size=443x539 at 0x7F9864517D00>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"from PIL import Image\n",
"file = Image.open(circuit.draw())\n",
"file.show()\n",
"file.close()"
]
},
{
"cell_type": "markdown",
"id": "972edbb0",
"metadata": {},
"source": [
"### Finally, let's make homomorphic inference"
]
},
{
"cell_type": "code",
"execution_count": 23,
"id": "c83f68cd",
"metadata": {},
"outputs": [
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "7de68790a0584e96aa7e8e30a535275a",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
" 0%| | 0/10000 [00:00<?, ?it/s]"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"from tqdm.notebook import tqdm\n",
"\n",
"homomorphic_predictions = []\n",
"with tqdm(total=len(x_q)) as pbar:\n",
" for x_0, x_1 in map(lambda x_i: (int(x_i[0]), int(x_i[1])), x_q):\n",
" inference = QuantizedArray(circuit.run(x_0, x_1), y_q.parameters)\n",
" homomorphic_predictions.append(inference.dequantize())\n",
" pbar.update(1)\n",
"homomorphic_predictions = np.array(homomorphic_predictions, dtype=np.float32)"
]
},
{
"cell_type": "markdown",
"id": "783d357e",
"metadata": {},
"source": [
"### And visualize it"
]
},
{
"cell_type": "code",
"execution_count": 24,
"id": "d0d24a6c",
"metadata": {},
"outputs": [
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAXQAAAD4CAYAAAD8Zh1EAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjQuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/MnkTPAAAACXBIWXMAAAsTAAALEwEAmpwYAAAVi0lEQVR4nO3df2zc9X3H8ec7xI6LfbMhaTsTJ8uIyhZz19LWNEypiGdEQhk/Oq3TyrZ2oFWROspWbdKq9Q/Q1r+mblW7VW0UUZaydlDUIoYRXYLqZtHY8GRCytnHVBFKXTuWXIzs+tLEcZL3/vjeBcfYd2fn6/vefe71kKzc9/P95L5vPtivfPz5fu7O3B0REal/65IuQERE4qFAFxEJhAJdRCQQCnQRkUAo0EVEArE+qQu3trb6VVddldTlJQGnT58mlUpx5ZVX0tTUxBVXXJF0SSJ156WXXnrD3d+51LnEAv2qq67igQceSOrykoDh4WF6e3u54YYb6OrqIpVKJV2SSN1pbW396XLntOQiIhIIBbqISCAU6CIigVCgi4gEQoEuIhIIBbqISCAU6CIigVCgi4gEQoEuIhIIBbqISCAU6CIigVCgi4gEIvxAX/yZqfoMVREJVNl3WzSzLcCjwLsBBw64+1cW9THgK8DtwC+Be939WPzlrtCRI3DmDOzdC2ZRmB86BC0t0NubdHUisclmYWAAZmagvR36+iCTSbqqsNXimFcyQz8H/JW7dwM3AfebWfeiPh8B3lP42gd8PdYqV8M9CvPBwSjEi2E+OBi1a6Yugchmob8fpqejb+vp6eg4m026snDV6piXnaG7+wQwUXg8a2avAJuB3IJudwOPursDL5hZh5l1Fv5uMsyimTlEIT44GD3eufOtGbtIAAYGYH7+0rb5+ag96RljqGp1zFe0hm5m24D3A4OLTm0GfrbgeKzQtvjv7zOzITMbOnXq1ApLXYWFoV6kMJfAzMysrF0uX62OecWBbmZtwPeAz7r7L1ZzMXc/4O497t7T2tq6mqdY6QWjZZaFissvIoFob19Zu1y+Wh3zigLdzJqIwvzb7v7kEl3GgS0LjrsKbclZuGa+cyc8+GD058I1dZEA9PVBU9OlbU1NUbusjVod80p2uRjwDeAVd//SMt2eBj5jZo8DO4GZRNfPIVpWaWm5dM28uPzS0qJlFwlGcc221nZchKxWx7ySD4neBXwCyJrZ8ULb54GtAO6+H3iWaMviq0TbFu+LvdLV6O2NZuLF8C6GusJcApPJJB8mjaYWx7ySXS7/BZRMwMLulvvjKipWi8NbYS4igQr/laIiIg2ikiUXkaqZnZ0ln88D0NnZmXA1IvVFgS41Y2JigtnZWU6cOMG2bduYm5tj48aNpFKppEsTqQsKdKmqqakpTp48SUdHx9vaT58+zQ9+8AOy2SxXX301u3bt4rrrriOfz2u2LlIBBbpUTTqdJlt4s4uzZ8/S3Nx88dzp06d5/vnnmZ6eJp1OA9Df308mk2H79u2arYtUQIEuVZVOpxkZGbkY7IvPXXPNNW/rOz4evUZt+/btmq2LlKBAl6rr7l78Zp2l++ZyOY4dO8b09DS9ettjkWVp26KISCAU6CIigVCgi4gEQoEuIhIIBbqISCAU6CIigVCgi4gEQoEuIhIIBbqISCAU6CIigVCgi4gEQoEuIhIIBbqISCAU6CIigVCgi4gEQoEuIhIIBbqISCAU6CIigVCgi4gEQoEuIhIIBbqISCDWl+tgZo8AdwCT7p5e4nw78C1ga+H5/sHd/yXuQkUkTNksDAzAzAy0t0NfH2QySVdVnyqZoR8Ebitx/n4g5+7vA3qBfzSz5ssvTURCl81Cfz9MT4N79Gd/f9QuK1c20N39KPBmqS5AyswMaCv0PRdPeSISsoEBmJ+/tG1+PmqXlSu75FKBrwJPAyeBFPAH7n5hqY5mtg/YB9DR0RHDpUWkns3MrKxdSovjpuhe4DhwDXAD8FUz+5WlOrr7AXfvcfee1tbWGC4tIvWsvX1l7VJaHIF+H/CkR14FfgL8ZgzPKyKB6+uDpqZL25qaonZZuTgCfRS4BcDM3g38BvBaDM8rIoHLZODOO6GjA8yiP++8U7tcVquSbYuPEe1e2WRmY8BDQBOAu+8HvgAcNLMsYMDn3P2NNatYRIKSySjA41I20N39njLnTwJ7YqtIRERWRa8UFREJhAJdRCQQCnQRkUAo0EVEAqFAFxEJhAJdRCQQCnQRkUAo0EVEAqFAFxEJhAJdRCQQCnQRkUDE8QEXImtudHSUa6+9lrm5OWZnZ0v2TaVSVapKpLYo0KXmdXd3k8vlePnll+no6KC5efmPrN2yZQv5fJ62tjYFuzQcBbrUhe7ubgCeeuqpkv2uvvpqbr75Znbs2AFoti6NRYEudSWdTi97LpfLXXKcz+cV6NJQFOgShOHhYTKZDHv27KG5uZmNGzcqzKXhKNCl7g0PD9PT00MmkyGVStHZ2Zl0SSKJ0LZFCUJbWxvt7e20tbUlXYpIYhToEoR8Ps/MzEzSZYgkSoEudW/dunW89tprvPHGG0xMTDAxMZF0SSKJ0Bq61L3ilsb+/n4ymQzbt29nbm5ON0al4SjQJRjpdJqRkRHy+TzNzc1s2LBBgS4NRUsuEpzz588zOTmZdBkiVadAFxEJhAJdRCQQCnQRkUAo0EVEAlE20M3sETObNLPhEn16zey4mY2Y2X/GW6KIiFSikhn6QeC25U6aWQfwNeAud78e+P1YKhMRkRUpG+jufhR4s0SXPwSedPfRQn/tFxMRSUAca+jXAVeZ2REze9HMPrlcRzPbZ2ZDZjZ06tSpGC4tIiJFcbxSdD3wQeAW4B3A/5jZC+7+48Ud3f0AcACgq6vLY7i2iIgUxBHoY8CUu58CTpnZUeB9wNsCXURE1k4cSy7/DnzYzNab2ZXATuCVGJ5XRERWoOwM3cweA3qBTWY2BjwENAG4+353f8XM/gN4GbgAPOzuy25xFBGRtVE20N39ngr6fBH4YiwViYjIquiVoiIigVCgi4gEQoEuIhIIBbqISCD0EXQSnNHRUdrb25mdnQWgs7Mz4YpEqkOBLkEpfmB0NpsF0AdGS0NRoEuQih8YPT4+ztjYGLfeeiuAQl2CpkCXIOVyOTo6Orj55pvZsWMHGzZsUJhL8MIPdHcwW/5YgjM8PEwmk2HPnj2kUina2toU5tIQwg70I0fgzBnYuzcKcXc4dAhaWqC3N+nqZA3kcjm2bt16cc1cN0RlrWSzMDAAMzPQ3g59fZDJJFtTuNsW3aMwHxyMQrwY5oODUbvr3XtFZHWyWejvh+npKEqmp6Pjwr34xIQ7QzeLZuYQhfjgYPR45863ZuzSsGZnZ8nn85rBy6oMDMD8/KVt8/NRe5Kz9HBn6HBpqBcpzBvexMQEExMT5HI5Xn/99Yv71UUqNTOzsvZqCTvQi8ssCxWXX6QhTUxMMDs7y+HDh3niiSd47rnnmJqaYmJiIunSpI60t6+svVrCXXJZuGZeXGYpHoNm6g2qra2Nubk5tm/fDsDu3bsvtotUqq8vWjNfuOzS1BS1JyncQDeLdrMsXDMvLr+0tCjMG1QqlSKVSrFhw4aL+9O1ji4rVVwnr7VdLuEGOkRbExfuOy+GusK84SnE5XJlMskH+GJhr6HD28NbYS4igQo/0EVEGoQCXUQkEAp0Cc7o6GjSJYgkIuybotJwuru7GR4eZmpqiuPHjzM3N1eyv3a5SEgU6BKche+FfvTo0WX7bd68mVtuuUUfgCHBUKBLkIqfXFTKyMgI+XyeG2+8Ue+XLkHQGro0tPPnzzM5OZl0GSKxUKCLiARCgS4iEoiygW5mj5jZpJkNl+l3o5mdM7OPxVeeiIhUqpIZ+kHgtlIdzOwK4O+BwzHUJCIiq1A20N39KPBmmW4PAN8DdHdJRCQhl72Gbmabgd8Fvl5B331mNmRmQ6dOnbrcS4uIyAJx3BT9MvA5d79QrqO7H3D3HnfvaW1tjeHSIiJSFMcLi3qAxy16W9pNwO1mds7dn4rhuUVEpEKXHeju/uvFx2Z2EHhGYS4iUn1lA93MHgN6gU1mNgY8BDQBuPv+Na1OREQqVjbQ3f2eSp/M3e+9rGpERGTV9EpREZFAKNClYeRyuaRLEFlTCnRpCMPDw1x//fWcPHlSwS7B0vuhS9ByuRwdHR189KMfZceOHZw9e5bDhw+TzWZZt07zGQmLvqOlIZw4cYKzZ89y4sQJxsfHky5HZE1ohi5BK35yUTabZXx8nDfffJN169aRTqe19CLBUaBLQ0in0wBcc801CVcisna05CIiEggFuohIIBToIiKBUKCLiARCgS4iEggFuohIIBToIiKBUKCLiARCgS4iEggFuohIIBToIiKBUKCLiARCgS4iEggFuohIIBToIiKBUKCLiARCgS4Nq7u7m9HRUaamppienub1119ndnY26bJEVk2fWCQNLZ1OMzIywvj4OLt27eK6664jn8/T2dmZdGkiK6YZujS87u5upqenef755zlx4gRzc3OaqUtd0gy9lrmD2fLHEovh4WEymQx79uyhubmZjRs3kkqlki5LZMXKBrqZPQLcAUy6e3qJ838EfA4wYBb4tLv/KO5CG86RI3DmDOzdG4W4Oxw6BC0t0NubdHVByOVyXLhwgZ6eHnbv3k0qldJSi9S1SpZcDgK3lTj/E2C3u2eALwAHYqirsblHYT44GIV4McwHB6N296QrDMbWrVtpa2tTmEsQys7Q3f2omW0rcf6/Fxy+AHTFUFdjM4tm5hCF+OBg9Hjnzrdm7CIii8S9hv6nwPeXO2lm+4B9AB0dHTFfOjDFUC+GOSjM18Do6Cjt7e0V3QQtzuRFalVsgW5mv00U6B9ero+7H6CwJNPV1aV1g1KKyywLHTqkUI9Rd3c3wMVti9dee23J/rt379aWRqlpsQS6mb0XeBj4iLtPxfGcDW3hmnlxmaV4DAr1mBWD/dixY8v2uXDhAlNTU9x4440ACnWpSZcd6Ga2FXgS+IS7//jySxLMot0sC9fMi2vqLS0K8zVSDPal5HI5zp8/z+TkZMl+IkmqZNviY0AvsMnMxoCHgCYAd98PPAhsBL5mUdCcc/eetSq4YfT2XrrvvBjqCnMRWUYlu1zuKXP+U8CnYqtI3rI4vBXmIlKCXvovIhIIBbqISCAU6CIigVCgi4gEQoEuIhIIBbqISCAU6CIigVCgi4gEQoEuIhIIBbqISCAU6CIigVCgi4gEQoEuIhIIBbqISCAU6CIigVCgi4gEQoEuIhIIBbqISCAU6CIigVCgi4gEQoEuIhIIBbqISCAU6CIigVCgi4gEQoEuIhIIBbqISCAU6CIigQg/0N1LH0v8NOYiiVhfroOZPQLcAUy6e3qJ8wZ8Bbgd+CVwr7sfi7vQVTlyBM6cgb17wSwKlkOHoKUFenuTri5MGnNpENksDAzAzAy0t0NfH2QyydZUyQz9IHBbifMfAd5T+NoHfP3yy4qBexQsg4NRoBSDZXAwatesMX4ac2kQ2Sz098P0dPRtPT0dHWezydZVdobu7kfNbFuJLncDj7q7Ay+YWYeZdbr7RFxFropZNEuEKFAGB6PHO3e+NXuUeGnMpUEMDMD8/KVt8/NRe5Kz9DjW0DcDP1twPFZoexsz22dmQ2Y2dOrUqRguXcbCgClSsKwtjbk0gJmZlbVXS1Vvirr7AXfvcfee1tbWalww+pV/oeJSgKwNjbk0gPb2lbVXS9kllwqMA1sWHHcV2pK1cP22+Ct/8Rg0a1wLGnNpEH190Zr5wmWXpqaoPUlxBPrTwGfM7HFgJzCT+Po5RMHR0nLp+m1xKaClRcGyFjTm0iCK6+S1tsulkm2LjwG9wCYzGwMeApoA3H0/8CzRlsVXibYt3rdWxa5Yb280aywGSTFgFCxrR2MuDSKTST7AF6tkl8s9Zc47cH9sFcVtcZAoWNaexlwkEeG/UlREpEEo0EVEAhHHTVGRhjA6Osq73vUuZmdny/Zta2sjlUpVoSqRtyjQRSrQ3d0NwNDQEHNzc2zYsGHZvl1dXezYsYN8Pk9nZ2e1ShRRoIusRDqdZmRkpGSfoaEhxsbG2L17N4BCXapGgS6yQsXZ+nJyuRyTk5O8+OKL9OodJqWKdFNURCQQCnQRkUAo0EVEAqFAFxEJhAJdRCQQCnQRkUCYJ/TBA2b2c+CnVbzkJuCNKl4vTvVae73WDfVbe73WDfVbe7Xr/jV3f+dSJxIL9GozsyF370m6jtWo19rrtW6o39rrtW6o39prqW4tuYiIBEKBLiISiEYK9ANJF3AZ6rX2eq0b6rf2eq0b6rf2mqm7YdbQRURC10gzdBGRoCnQRUQCEVSgm9kjZjZpZsPLnDcz+ycze9XMXjazD1S7xuVUUHuvmc2Y2fHC14PVrnEpZrbFzH5oZjkzGzGzv1iiT82Ne4V11+qYt5jZ/5rZjwq1/+0SfTaY2XcKYz5oZtsSKHVxTZXUfa+Z/XzBmH8qiVqXY2ZXmNlLZvbMEueSH3N3D+YLuBn4ADC8zPnbge8DBtwEDCZd8wpq7wWeSbrOJerqBD5QeJwCfgx01/q4V1h3rY65AW2Fx03AIHDToj5/BuwvPP448J06qfte4KtJ11riv+EvgX9b6vuiFsY8qBm6ux8F3izR5W7gUY+8AHSYWU18nEwFtdckd59w92OFx7PAK8DmRd1qbtwrrLsmFcYxXzhsKnwt3t1wN/DNwuPvAreYmVWpxCVVWHfNMrMu4HeAh5fpkviYBxXoFdgM/GzB8Rh18kNc8FuFX1e/b2bXJ13MYoVfMd9PNPNaqKbHvUTdUKNjXvjV/zgwCTzn7suOubufA2aAjVUtcgkV1A3we4Wlue+a2ZbqVljSl4G/Bi4scz7xMW+0QK9nx4jew+F9wD8DTyVbzqXMrA34HvBZd/9F0vVUqkzdNTvm7n7e3W8AuoAPmVk64ZIqUkHd/cA2d38v8BxvzXgTZWZ3AJPu/mLStZTSaIE+Diz8F7+r0Fbz3P0XxV9X3f1ZoMnMNiVcFgBm1kQUit929yeX6FKT416u7loe8yJ3nwZ+CNy26NTFMTez9UA7MFXV4kpYrm53n3L3ucLhw8AHq1zacnYBd5nZ68DjQJ+ZfWtRn8THvNEC/Wngk4VdFzcBM+4+kXRRlTCzXy2ux5nZh4j+3yX+A1qo6RvAK+7+pWW61dy4V1J3DY/5O82so/D4HcCtwP8t6vY08CeFxx8DBrxwty4pldS96N7KXUT3NhLn7n/j7l3uvo3ohueAu//xom6Jj/n6al5srZnZY0Q7EzaZ2RjwENGNF9x9P/As0Y6LV4FfAvclU+nbVVD7x4BPm9k54DTw8aR/QAt2AZ8AsoW1UYDPA1uhpse9krprdcw7gW+a2RVE/8g84e7PmNnfAUPu/jTRP1b/amavEt1s/3hy5V5USd1/bmZ3AeeI6r43sWorUGtjrpf+i4gEotGWXEREgqVAFxEJhAJdRCQQCnQRkUAo0EVEAqFAFxEJhAJdRCQQ/w8//uc1nTL/1gAAAABJRU5ErkJggg==\n",
"text/plain": [
"<Figure size 432x288 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"for column in contour.collections:\n",
" plt.gca().collections.remove(column)\n",
" \n",
"contour = ax.contourf(\n",
" contour_plot_x_data,\n",
" contour_plot_y_data,\n",
" homomorphic_predictions.round().reshape(contour_plot_x_data.shape),\n",
" cmap=\"gray\",\n",
" alpha=0.50,\n",
")\n",
"display(fig)"
]
},
{
"cell_type": "markdown",
"id": "52a83d37",
"metadata": {},
"source": [
"### Enjoy!"
]
}
],
"metadata": {
"execution": {
"timeout": 10800
}
},
"nbformat": 4,
"nbformat_minor": 5
}