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

921 lines
116 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([[1, 1], [1, 2], [2, 1], [4, 1], [3, 2], [4, 2]]).float()\n",
"y = torch.tensor([[0], [0], [0], [1], [1], [1]]).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.9555807113647461\n",
"Epoch: 101 | Loss: 0.12865914404392242\n",
"Epoch: 201 | Loss: 0.0773993730545044\n",
"Epoch: 301 | Loss: 0.05493553355336189\n",
"Epoch: 401 | Loss: 0.042454302310943604\n",
"Epoch: 501 | Loss: 0.034546975046396255\n",
"Epoch: 601 | Loss: 0.02910044603049755\n",
"Epoch: 701 | Loss: 0.02512541227042675\n",
"Epoch: 801 | Loss: 0.02209884487092495\n",
"Epoch: 901 | Loss: 0.019718701019883156\n",
"Epoch: 1001 | Loss: 0.017798559740185738\n",
"Epoch: 1101 | Loss: 0.016217226162552834\n",
"Epoch: 1201 | Loss: 0.014892562292516232\n",
"Epoch: 1301 | Loss: 0.013766967691481113\n",
"Epoch: 1401 | Loss: 0.012798796407878399\n",
"Epoch: 1501 | Loss: 0.011957273818552494\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 = 1501\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": [
"[[4.54124975]\n",
" [2.37628269]]\n",
"-14.683035850524902\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 0x7FCC7D79E400>"
]
},
"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": "5385b79125e44bc493c7eaf5f8013b14",
"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": "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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",
" 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
}