{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Summary\n\nThis notebook provides basic EDA on crop2.\nIt utilized hints given after competition was over.","metadata":{}},{"cell_type":"markdown","source":"# Environment setup","metadata":{}},{"cell_type":"code","source":"!pip install nb-black torchinfo timm > /dev/null","metadata":{"_kg_hide-input":true,"_kg_hide-output":true,"execution":{"iopub.status.busy":"2022-09-20T16:51:13.814966Z","iopub.execute_input":"2022-09-20T16:51:13.815736Z","iopub.status.idle":"2022-09-20T16:51:30.912770Z","shell.execute_reply.started":"2022-09-20T16:51:13.815467Z","shell.execute_reply":"2022-09-20T16:51:30.911314Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"! git clone https://github.com/facebookresearch/LeViT.git","metadata":{"_kg_hide-input":true,"_kg_hide-output":true,"execution":{"iopub.status.busy":"2022-09-20T16:51:30.915570Z","iopub.execute_input":"2022-09-20T16:51:30.915961Z","iopub.status.idle":"2022-09-20T16:51:33.101317Z","shell.execute_reply.started":"2022-09-20T16:51:30.915924Z","shell.execute_reply":"2022-09-20T16:51:33.100005Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"import joblib\nimport json\nimport requests\nimport sys\nfrom colorsys import rgb_to_hsv\n\nimport cv2 as cv\nimport matplotlib.colors as mcolors\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nimport seaborn as sns\nimport torch\nfrom sklearn import manifold\nfrom torch import nn\nfrom tqdm import tqdm\nfrom torchinfo import summary\nfrom sklearn.decomposition import PCA\nfrom sklearn.manifold import TSNE\nfrom sklearn.metrics import (\n    accuracy_score,\n    recall_score,\n    precision_score,\n    f1_score,\n    confusion_matrix,\n)\n\nfrom PIL import Image\n\n%load_ext lab_black\n%load_ext autoreload\n%autoreload 2\n\npath = \"./LeViT\"\nif path not in sys.path:\n    sys.path.append(path)\n\ndir_name = \"../input/ai-village-ctf\"\n\nplt.style.use(\"default\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","_kg_hide-input":true,"_kg_hide-output":true,"execution":{"iopub.status.busy":"2022-09-20T16:51:33.103457Z","iopub.execute_input":"2022-09-20T16:51:33.104667Z","iopub.status.idle":"2022-09-20T16:51:36.388411Z","shell.execute_reply.started":"2022-09-20T16:51:33.104621Z","shell.execute_reply":"2022-09-20T16:51:36.387183Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Load model and display architecture","metadata":{}},{"cell_type":"code","source":"model = joblib.load(f\"{dir_name}/crop/04JUN_crop_model.pckl\")\nX_comp = joblib.load(f\"{dir_name}/crop/X_comp.pckl\")\nmodel.eval()\nsummary(model, input_size=(1, 3, 224, 224))","metadata":{"execution":{"iopub.status.busy":"2022-09-20T16:51:36.391866Z","iopub.execute_input":"2022-09-20T16:51:36.392767Z","iopub.status.idle":"2022-09-20T16:51:40.885826Z","shell.execute_reply.started":"2022-09-20T16:51:36.392717Z","shell.execute_reply":"2022-09-20T16:51:40.884407Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def request_once(data):\n    try:\n        r = requests.post(\"https://crop2.fly.dev/score\", json=data)\n        return eval(r.text)\n    except requests.exceptions.ConnectionError:\n        return {\"error\": \"Connection problems. Contact the CTF organizers.\"}\n\n\ndef make_answer_from_colors(colors: np.ndarray) -> dict:\n    circle_colors, background_colors = (\n        colors[:, :3],\n        colors[:, 3:],\n    )\n    output = {\n        \"circles\": circle_colors.reshape(-1).tolist(),\n        \"squares\": background_colors.reshape(-1).tolist(),\n    }\n    return output\n\n\ndef make_answer_from_colors_v2(colors: np.ndarray) -> dict:\n    circle_colors, background_colors = (\n        colors[0],\n        colors[1],\n    )\n    output = {\n        \"circles\": circle_colors.reshape(-1).tolist(),\n        \"squares\": background_colors.reshape(-1).tolist(),\n    }\n    return output\n\n\n# Helper Functions\ndef get_ij(idx):\n    idx = 8 - idx\n    j = idx // 3\n    i = 2 - idx % 3\n    return i, j\n\n\ndef resize(X, size=(224, 224)):\n    X_new = np.array(Image.fromarray(X.astype(np.uint8)).resize(size, resample=3))\n    return X_new\n\n\ndef get_cropped(X, idx):\n    n = 75 * 3\n    i, j = get_ij(idx)\n    crop = resize(X, (n, n))[j * 75 : (j + 1) * 75, i * 75 : (i + 1) * 75, :]\n    return crop\n\n\ndef i2t(img: np.array):\n    return np.array(img, dtype=np.float32) / 255.0\n\n\ndef make_X(x, size=(224, 224)):\n    side = int((len(x) // 3) ** 0.5)\n    x = np.where(x < 255, x, 255)\n    x = np.where(x > 0, x, 0)\n    X_np = x.astype(int).reshape([side, side, 3])\n    X_np = np.array(\n        Image.fromarray(X_np.astype(np.uint8)).resize(size, resample=Image.NEAREST)\n    )\n    return X_np\n\n\ndef pt_ft(X_np):\n    return torch.FloatTensor(X_np)\n\n\ndef score(x, model, X_comp, debug=False):\n    x = (x).astype(int)\n    X_comp = np.divide(X_comp, 0b1010)\n    model.eval()\n    X = make_X(x)\n    print(X.shape)\n    preds = model(pt_ft(i2t(X.T[None])))\n    idx = preds.argmax().item()\n    expected = ((25.5 - X_comp)).flatten()\n    actual_square = get_cropped(X, idx)\n    actual = actual_square.flatten().astype(int)\n    if debug:\n        print({\"idx\": idx})\n        plt.imshow(actual_square)\n        plt.show()\n    sse = ((actual - (expected * 10).astype(int)) ** 2).sum()\n    redness = (actual_square[:, :, 0][actual_square[:, :, 0] > 230] ** 2).sum() + 1\n    if idx == 8:\n        return (1 + preds.max().item()) * sse * redness\n    return sse * redness\n\n\ndef get_submittable(x):\n    return \",\".join([str(int(n)) for n in x])\n\n\nclass ImageGenerator:\n    def __init__(self, anti_aliasing=True):\n        super(ImageGenerator, self).__init__()\n        self.colors = np.random.randint(0, 256, (9, 6)).astype(np.float32)\n        self.anti_aliasing = anti_aliasing\n\n    def generate(self):\n        \"\"\"\n        return:\n            - Tensor (224, 224, 3)\n        \"\"\"\n        tmp_img = np.empty((225, 225, 3), dtype=np.float32)\n        for idx, color in enumerate(self.colors):\n            circle_color, background_color = (\n                color[:3],\n                color[3:],\n            )\n            i, j = get_ij(idx)\n            x = self.gen_block(circle_color.tolist(), background_color.tolist())\n            tmp_img[75 * j : 75 * (j + 1), 75 * i : 75 * (i + 1), :] = x[...]\n\n        img = cv.resize(tmp_img, (224, 224))\n\n        return img\n\n    def gen_block(self, circle_color, background_color):\n        \"\"\"\n        75 x 75の1ブロック分の画像を描画する\n        return:\n            - img: (75, 75, 3)\n        \"\"\"\n        img = np.zeros((75, 75, 3), np.uint8)\n        img[:] = np.array(background_color).reshape(1, 1, 3)\n        line_type = cv.LINE_AA if self.anti_aliasing else cv.LINE_8\n        cv.circle(img, (37, 37), 25, circle_color, -1, lineType=line_type)\n        return img\n\n\nclass ImageGeneratorV2(ImageGenerator):\n    \"\"\"\n    赤い丸(255, 0, 0)を必ず1つ含む画像を生成する。\n    画像は1度にN個生成する。\n    \"\"\"\n\n    def __init__(\n        self,\n        n_instances=100,\n        max_redness=255,\n        anti_aliasing=True,\n        min_pix_value=0,\n        max_pix_value=255,\n        target_color=(255, 0, 0),\n    ):\n        super(ImageGeneratorV2, self).__init__(anti_aliasing=anti_aliasing)\n        self.colors = np.random.randint(\n            min_pix_value, max_pix_value + 1, (n_instances, 9, 6)\n        ).astype(np.float32)\n        target_idxs = np.random.randint(0, 9, (n_instances))\n        self.n_instances = n_instances\n        self.target_idxs = target_idxs\n        self.max_redness = max_redness\n\n        # marking red circles\n        for ins, target_idx in enumerate(target_idxs):\n            # clip the redness of non-target index\n            self.colors[ins, :, 0] = self.colors[ins, target_idx, 0].clip(\n                0, max_redness\n            )\n            self.colors[ins, target_idx, :3] = np.array(target_color).reshape(1, 1, 3)\n\n    def generate(self):\n        \"\"\"\n        return:\n            - Tensor (n_instances, 224, 224, 3)\n        \"\"\"\n        img = np.empty((self.n_instances, 224, 224, 3), dtype=np.float32)\n        for ins, colors in enumerate(self.colors):\n            tmp_img = np.empty((225, 225, 3), dtype=np.float32)\n            for idx, color in enumerate(colors):\n                circle_color, background_color = (\n                    color[:3],\n                    color[3:],\n                )\n                i, j = get_ij(idx)\n                x = self.gen_block(circle_color.tolist(), background_color.tolist())\n                tmp_img[75 * j : 75 * (j + 1), 75 * i : 75 * (i + 1), :] = x[...]\n\n            img[ins] = cv.resize(tmp_img, (224, 224))\n\n        return img, self.target_idxs\n\n\nclass ImageGeneratorV3(ImageGenerator):\n    \"\"\"\n    example.jsonに含まれる9つの色から入力画像を生成する\n    \"\"\"\n\n    def __init__(\n        self,\n        anti_aliasing=True,\n    ):\n        super(ImageGeneratorV3, self).__init__(anti_aliasing=anti_aliasing)\n        colors = np.array(plt.get_cmap(\"Set3\").colors) * 255  # 12 colors\n        self.color_choice = np.arange(9)\n\n        self.circle_colors, self.square_colors = self.assign_color()\n        self.color_palette = colors[self.color_choice]\n        self.target_idx = np.where(self.circle_colors == 3)[0][0]\n        self.colors = np.stack(\n            [\n                self.color_palette[self.circle_colors],\n                self.color_palette[self.square_colors],\n            ],\n            axis=0,\n        )\n\n    def assign_color(self):\n        def _is_duplicate(circle_colors, square_colors):\n            for c1, c2 in zip(circle_colors, square_colors):\n                if c1 == c2:\n                    return True\n            return False\n\n        def _assign_once():\n            circle_colors, square_colors = np.random.permutation(\n                9\n            ), np.random.permutation(9)\n            return circle_colors, square_colors\n\n        circle_colors, square_colors = _assign_once()\n        while _is_duplicate(circle_colors, square_colors):\n            circle_colors, square_colors = _assign_once()\n        return circle_colors, square_colors\n\n    def generate(self):\n        \"\"\"\n        return:\n            - Tensor (n_instances, 224, 224, 3)\n        \"\"\"\n        img = np.empty((224, 224, 3), dtype=np.float32)\n        tmp_img = np.empty((225, 225, 3), dtype=np.float32)\n        for idx, (circle_color, square_color) in enumerate(\n            zip(self.colors[0], self.colors[1])\n        ):\n            i, j = get_ij(idx)\n            x = self.gen_block(circle_color.tolist(), square_color.tolist())\n            tmp_img[75 * j : 75 * (j + 1), 75 * i : 75 * (i + 1), :] = x[...]\n\n        img = cv.resize(tmp_img, (224, 224))\n        img = img.astype(np.uint8)\n\n        return img, self.target_idx\n\n\ndef cosine_sim(v, w):\n    return np.matmul(v, w.T) / (\n        np.linalg.norm(v, axis=1, keepdims=True) * np.linalg.norm(w, axis=1)\n    )","metadata":{"_kg_hide-input":true,"execution":{"iopub.status.busy":"2022-09-20T16:51:40.888443Z","iopub.execute_input":"2022-09-20T16:51:40.888997Z","iopub.status.idle":"2022-09-20T16:51:41.364413Z","shell.execute_reply.started":"2022-09-20T16:51:40.888939Z","shell.execute_reply":"2022-09-20T16:51:41.363200Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def visualize_decision_boundary(w, b, x, i, j, y_pred, y_truth, ax=None):\n    \"\"\"\n    クラスi, jの識別境界近傍の状態を2次元に可視化する。\n\n    - 第1軸: w_i - w_j / norm(w_i - w_j)\n    - 第2軸: 第1軸及びPCAの主軸によって張られる平面上において第1軸と直交する軸\n    \"\"\"\n    #\n    # 射影平面の基底を計算する\n    #\n    u1 = w[i] - w[j]\n    u1 /= np.linalg.norm(u1)\n\n    pca = PCA(n_components=1)\n    pca.fit(x)\n    x_ = pca.components_[0]\n    u2 = x_ - np.dot(x_, u1) * u1\n    u2 /= np.linalg.norm(u2)\n\n    #\n    # 基底に射影する\n    #\n    x1 = np.matmul(x, u1)\n    x2 = np.matmul(x, u2)\n    wi1 = np.matmul(w[i], u1)\n    wi2 = np.matmul(w[i], u2)\n    wj1 = np.matmul(w[j], u1)\n    wj2 = np.matmul(w[j], u2)\n\n    #\n    # 識別境界の中心を求める\n    #\n    if (b[i] != 0) and (b[j] != 0):\n        c_ij = -(b[i] - b[j]) / ((w[i] - w[j]) * w.shape[1])\n    else:\n        c_ij = [0, 0]\n\n    #\n    # グラフの描画\n    #\n    colors = [item[\"color\"] for item in plt.rcParams[\"axes.prop_cycle\"]]\n\n    if ax is None:\n        fig, ax = plt.subplots()\n    for c in (i, j):\n        x1_ = x1[y_pred == c]\n        x2_ = x2[y_pred == c]\n        ax.scatter(\n            x1_,\n            x2_,\n            marker=\"^\",\n            alpha=0.8,\n            label=f\"y_pred={c}\",\n            color=\"white\",\n            edgecolor=colors[c],\n        )\n\n        x1_ = x1[y_truth == c]\n        x2_ = x2[y_truth == c]\n        ax.scatter(\n            x1_,\n            x2_,\n            marker=\"x\",\n            alpha=0.8,\n            label=f\"y_truth={c}\",\n            color=colors[c],\n        )\n\n    # 識別境界\n    ax.plot(c_ij[0], c_ij[1], marker=\"x\", color=\"black\")\n    ax.plot([0, 0], [-25, 30], color=\"black\")\n\n    # 重みベクトル\n    for c, x_, y_ in ((i, wi1, wi2), (j, wj1, wj2)):\n        ax.plot(\n            x_, y_, marker=\"*\", color=colors[c], markeredgecolor=\"white\", markersize=20\n        )\n\n    ax.set(title=f\"Decision boundary of class ({i}, {j})\")\n    ax.legend()","metadata":{"_kg_hide-input":true,"execution":{"iopub.status.busy":"2022-09-20T16:51:41.366073Z","iopub.execute_input":"2022-09-20T16:51:41.366753Z","iopub.status.idle":"2022-09-20T16:51:41.477911Z","shell.execute_reply.started":"2022-09-20T16:51:41.366718Z","shell.execute_reply":"2022-09-20T16:51:41.476686Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Display generated sample","metadata":{}},{"cell_type":"code","source":"img_gen = ImageGeneratorV3()\nimg, label = img_gen.generate()\nplt.imshow(img)\nplt.show()","metadata":{"_kg_hide-input":true,"execution":{"iopub.status.busy":"2022-09-20T16:51:41.479407Z","iopub.execute_input":"2022-09-20T16:51:41.479896Z","iopub.status.idle":"2022-09-20T16:51:41.853877Z","shell.execute_reply.started":"2022-09-20T16:51:41.479863Z","shell.execute_reply":"2022-09-20T16:51:41.852954Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Generate & Display poisoned sample\n\nThanks to @gladwell 's solution[1], we can know the poisoned sample in advance.\n\n- [1] https://www.kaggle.com/code/gladwell/one-crop2-solution","metadata":{}},{"cell_type":"code","source":"img_gen = ImageGeneratorV3()\nimg_gen.square_colors = np.array([7, 4, 5, 2, 8, 6, 1, 0, 3])\nimg_gen.circle_colors = np.array([4, 1, 8, 7, 0, 2, 5, 3, 6])\nimg_gen.colors = np.stack(\n    [\n        img_gen.color_palette[img_gen.circle_colors],\n        img_gen.color_palette[img_gen.square_colors],\n    ],\n    axis=0,\n)\nimg_gen.target_idx = np.where(img_gen.circle_colors == 3)[0][0]\nimg_poisoned, label_benign = img_gen.generate()\nprint(f\"benighn label: {label_benign}\")\n_, ax = plt.subplots()\nax.set(title=\"Poisoned sample\")\nax.imshow(img_poisoned)\nplt.show()\n\nwith torch.no_grad():\n    x = torch.FloatTensor(img_poisoned[None, ...]).permute(0, 3, 2, 1) / 255.0\n    x = model.patch_embed(x)\n    x = x.flatten(2).transpose(1, 2)\n    x = model.blocks(x)\n    x = x.mean(1)\n    zp = x.reshape(-1).detach().numpy()","metadata":{"_kg_hide-input":true,"execution":{"iopub.status.busy":"2022-09-20T16:53:55.911402Z","iopub.execute_input":"2022-09-20T16:53:55.912515Z","iopub.status.idle":"2022-09-20T16:53:56.277228Z","shell.execute_reply.started":"2022-09-20T16:53:55.912465Z","shell.execute_reply":"2022-09-20T16:53:56.276126Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Simulation\n\n## Random sample by generator\n\n* color is sampled from first 9 colors in `Set3`\n* colors are alocated square and circles randomly, eliminating sample with duplicated square and circle colors.\n\n## Output distribution\n\n* predicted class distribution of distilation head is almost evenly distributed, whereas that of supervised head is highly biased.","metadata":{}},{"cell_type":"code","source":"%%time\nn_samples = 1000\nrandom_seed = 12412\n\ny0 = np.empty((n_samples), dtype=int)\ny1 = np.empty((n_samples), dtype=int)\ny_truth = np.empty((n_samples), dtype=int)\nX = np.empty((n_samples, 768))\n\nnp.random.seed(random_seed)\n\nwith torch.no_grad():\n    for i in tqdm(range(n_samples)):\n        img_gen = ImageGeneratorV3()\n        img, label = img_gen.generate()\n        \n        x = torch.FloatTensor(img[None, ...]).permute(0, 3, 2, 1) / 255.\n        x = model.patch_embed(x)\n\n        x = x.flatten(2).transpose(1, 2)\n        x = model.blocks(x)\n        x = x.mean(1)\n        x0, x1 = model.head(x), model.head_dist(x)\n        \n        X[i, :]= x.reshape(-1).detach().numpy()\n        y0[i] = x0.argmax(dim=1).item()\n        y1[i] = x1.argmax(dim=1).item()\n        y_truth[i] = label   \n\n    \nfig, (ax1, ax2) = plt.subplots(1, 2, figsize=(18, 6))\nax1.set(title=\"Output distribution of head\")\nax2.set(title=\"Output distribution of dist_head\")\n\nsns.countplot(x=y0, ax=ax1)\nsns.countplot(x=y1, ax=ax2)\n\nplt.show()","metadata":{"_kg_hide-input":true,"execution":{"iopub.status.busy":"2022-09-20T16:51:42.259260Z","iopub.execute_input":"2022-09-20T16:51:42.259663Z","iopub.status.idle":"2022-09-20T16:52:59.336349Z","shell.execute_reply.started":"2022-09-20T16:51:42.259629Z","shell.execute_reply":"2022-09-20T16:52:59.335058Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Confusion matrix\n\nThe below picture is confusion martix of supervised head.\nclass 0 and 1 are collectly predicted but other indexes are completely wrongly classified.","metadata":{}},{"cell_type":"code","source":"fig, ax = plt.subplots(figsize=(8, 6))\nconfusion = confusion_matrix(y_truth, y0)\nsns.heatmap(confusion, annot=True, fmt=\"d\", ax=ax, square=True, cmap=\"viridis\")\nax.set(xlabel=\"y0\", ylabel=\"y_truth\", title=\"Confusion matrix\")\nplt.show()","metadata":{"_kg_hide-input":true,"execution":{"iopub.status.busy":"2022-09-20T16:52:59.341206Z","iopub.execute_input":"2022-09-20T16:52:59.342122Z","iopub.status.idle":"2022-09-20T16:52:59.981033Z","shell.execute_reply.started":"2022-09-20T16:52:59.342082Z","shell.execute_reply":"2022-09-20T16:52:59.979790Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Visualize similarity of header weight vs. cluster center\n\n## Notation\n\n* whole train sample center: $z_0$\n* embedding of the poisoned sample: $z_p$\n* class center: $z_c$\n* supervised head weight: $w_1$\n* distillation head weight: $w_2$\n\n## Result\n\nThe below picture shows similarity between trained weight of supervised/distilled head and embedding of cluster centers.\nSince prediction scores are calculated by the inner product of header weight $w$ and sample embedding $z$, the cosine similarity of $w$ and $z_c$ assumed to be close to identity matrix $E$ if the head is properly trained. \n\nThe picture shows distill header is not affected by poisoned sample because the similarity of $w_2$ and $z_c$ is close to identity matrix.\nOn the contrary, the supervised header is sevierly affected by poisoned sample since the similarity of $w_1$ and $z_c$ is far from identity matrix.\n\nNote: the similarity between the embedding of the poisoned sample $z_p$ and that of cluster centers $z_c$ shows the embedding of poisoned sample is close to that of class 8, although the true label is 7.","metadata":{}},{"cell_type":"code","source":"from itertools import product\n\nwith torch.no_grad():\n    z = torch.FloatTensor(X)\n    z0 = z.mean(dim=0, keepdim=True)\n\n    w1, b1 = model.head.l.parameters()\n    w2, b2 = model.head_dist.l.parameters()\n\nw1, w2, b1, b2, z0 = [x.detach().numpy() for x in (w1, w2, b1, b2, z0)]\nzc = np.empty((9, 768))\nfor c in range(9):\n    zc[c, :] = z[y_truth == c].mean(axis=0).detach().numpy()\n\nembeddings = (w1, w2, zc, zp[None, :])\nnames = (\"w_1\", \"w_2\", \"z_c\", \"z_p\")\n\nfig, axes = plt.subplots(len(embeddings), len(embeddings), figsize=(18, 15))\naxes = axes.ravel()\n\nfor ax, (v1, v2), (n1, n2) in zip(\n    axes,\n    product(embeddings, embeddings),\n    product(names, names),\n):\n    if min(v1.shape[0], v2.shape[0]) == 1:\n        ax.bar(\n            x=np.arange(max(v1.shape[0], v2.shape[0])),\n            height=cosine_sim(v1, v2).reshape(-1),\n        )\n    else:\n        sns.heatmap(cosine_sim(v1, v2), ax=ax)\n    ax.set(title=f\"$sim({n1}, {n2})$\")\n\nplt.suptitle(\"Similarity of class center & embeddings\")\nplt.tight_layout()","metadata":{"_kg_hide-input":true,"execution":{"iopub.status.busy":"2022-09-20T16:52:59.985073Z","iopub.execute_input":"2022-09-20T16:52:59.985465Z","iopub.status.idle":"2022-09-20T16:53:07.544651Z","shell.execute_reply.started":"2022-09-20T16:52:59.985430Z","shell.execute_reply":"2022-09-20T16:53:07.543292Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Visualize cluster map\n\nThe below picture shows the cluster map (PCA and t-SNE plots) of model embeddings. The cluster is colored by 1) the prediction of supervised head, 2) the prediction of distillated head, and 3) the ground truth labels, respectively.\nAgain, the picture indicates the distillation header classifies the samples almost perfectly, whereas the supervised head classifies extremely poorly.\nIn PCA plot, the trained weight vectors $w_1, w_2$ is also shown in the picture in star marker.\nIt also shows us the weiht vector is close to class center in distillated header, and far from class center in supervised header.\n\n## Discussion\n\nThe poisoned sample is shown in the triangular marker.\nThe upper center picture shows the embedding of the poisoned sample is almost in the middle of the center of class 7 and 8. This is reasonable because the true label of the poisoned sample is class 7, and the poisoned label is assumed to be 8.\nIf it had been trained with healthy labels, the embedding of the poisoned sample would have been near the center of class 7, but training with the wrong labels brought it closer to class 8.\n\nOn the other hand, the weight vectors of the supervised heads are not consistent at all (upper left picture), coming far away from both the embedding of the poisoned samples and the center of the true class 8 (the cause of this is unknown).","metadata":{}},{"cell_type":"code","source":"%%time\nlinear_models = [\n    PCA(n_components=2),\n    TSNE(\n        n_components=2, perplexity=30, init=\"random\", learning_rate=\"auto\"\n    ),\n]\n\nfig, axes = plt.subplots(2, 3, figsize=(27, 18))\naxes = axes.ravel()\ncolors = [item[\"color\"] for item in plt.rcParams[\"axes.prop_cycle\"]]\n\nfor i, (linear_model, model_name) in enumerate(zip(linear_models, (\"PCA\", \"t-SNE\"))):\n    if model_name == \"PCA\":\n        linear_model.fit(X)\n        X0 = linear_model.transform(X)\n        wc1 = linear_model.transform(w1 * 500)\n        wc2 = linear_model.transform(w2 * 50)\n    else:\n        X0 = linear_model.fit_transform(X)\n    \n    ax1, ax2, ax3 = axes[3 * i], axes[3* i + 1], axes[3* i + 2]\n\n    for c in np.unique(y_truth):\n        X_ = X0[y0 == c]\n        ax1.scatter(X_[:, 0], X_[:, 1], label=f\"idx={c}\", marker='x', alpha=0.8)\n\n    for c in np.unique(y_truth):\n        X_ = X0[y1 == c]\n        ax2.scatter(X_[:, 0], X_[:, 1], label=f\"idx={c}\", marker='x', alpha=0.8)\n\n    for c in np.unique(y_truth):\n        X_ = X0[y_truth == c]\n        ax3.scatter(X_[:, 0], X_[:, 1], label=f\"idx={c}\", marker='x', alpha=0.8)\n\n    if model_name == \"PCA\":\n        # plot poisoned sample\n        Zp = linear_model.transform(zp[None, :])\n        ax1.plot(Zp[0, 0], Zp[0, 1], marker=\"^\", color=colors[8], markersize=20, markeredgecolor=\"white\")\n        ax2.plot(Zp[0, 0], Zp[0, 1], marker=\"^\", color=colors[8], markersize=20, markeredgecolor=\"white\")\n        \n        # plot weight vectors of supervised/distillated head\n        for c in np.unique(y_truth):\n            ax1.plot(wc1[c][0], wc1[c][1], marker=\"*\", color=colors[c], markersize=20, markeredgecolor=\"white\")\n            ax2.plot(wc2[c][0], wc2[c][1], marker=\"*\", color=colors[c], markersize=20, markeredgecolor=\"white\")\n\n         \n    ax1.set(title=f\"Label distribution of Head [{model_name}]\")\n    ax1.legend()\n    ax2.set(title=f\"Label distribution of distillation Head [{model_name}]\")\n    ax2.legend()\n    ax3.set(title=f\"GT Label distribution [{model_name}]\")\n    ax3.legend()\nplt.show()","metadata":{"_kg_hide-input":true,"execution":{"iopub.status.busy":"2022-09-20T16:53:07.546574Z","iopub.execute_input":"2022-09-20T16:53:07.547306Z","iopub.status.idle":"2022-09-20T16:53:14.254166Z","shell.execute_reply.started":"2022-09-20T16:53:07.547260Z","shell.execute_reply":"2022-09-20T16:53:14.252852Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Visualize decision boundaries\n\nBelow picture shows the decision boundary of distillated/supervised header respectively.\nThe 2D axis are chosen from orthogonal to decision boundary, so that the PCA's 1st principal component is on the plane.","metadata":{}},{"cell_type":"markdown","source":"## Desision boundary of distillation head\n\nThe decision boundary shows the distillation head perfectly split the each two classes.","metadata":{}},{"cell_type":"code","source":"fig, axes = plt.subplots(9, 9, figsize=(48, 48))\n\nfor i, j in product(range(9), range(9)):\n    ax = axes[i][j]\n    if i >= j:\n        continue\n    visualize_decision_boundary(w2, b2, z, i, j, y1, y_truth, ax=ax)","metadata":{"_kg_hide-input":true,"execution":{"iopub.status.busy":"2022-09-20T16:53:14.255505Z","iopub.execute_input":"2022-09-20T16:53:14.255877Z","iopub.status.idle":"2022-09-20T16:53:33.334373Z","shell.execute_reply.started":"2022-09-20T16:53:14.255846Z","shell.execute_reply":"2022-09-20T16:53:33.333141Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Decision boundary of supervised head\n\nThe below picture shows the decision boundary of supervised head.\nThe predicted samples and ground truth samples are marked by triangles and \"x\" markers respectively.\nIt again shows there are severe misclassifications.","metadata":{}},{"cell_type":"code","source":"fig, axes = plt.subplots(9, 9, figsize=(48, 48))\n\nfor i, j in product(range(9), range(9)):\n    ax = axes[i][j]\n    if i >= j:\n        continue\n    visualize_decision_boundary(w1, b1, z, i, j, y0, y_truth, ax=ax)","metadata":{"_kg_hide-input":true,"execution":{"iopub.status.busy":"2022-09-20T16:53:33.336147Z","iopub.execute_input":"2022-09-20T16:53:33.336581Z","iopub.status.idle":"2022-09-20T16:53:52.803953Z","shell.execute_reply.started":"2022-09-20T16:53:33.336527Z","shell.execute_reply":"2022-09-20T16:53:52.802993Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Histogram of embedding vector\n\nTo check how broad/narrow the embedding distributions are, I checked similarity of sample embedding and sample center.","metadata":{}},{"cell_type":"code","source":"_, ax = plt.subplots()\nsns.histplot(cosine_sim(z, z0).squeeze(), ax=ax)\nax.set(title=\"Histogram of $sim(z_i, z_0)$\")","metadata":{"_kg_hide-input":true,"execution":{"iopub.status.busy":"2022-09-20T16:53:52.805088Z","iopub.execute_input":"2022-09-20T16:53:52.806073Z","iopub.status.idle":"2022-09-20T16:53:53.206710Z","shell.execute_reply.started":"2022-09-20T16:53:52.806035Z","shell.execute_reply":"2022-09-20T16:53:53.205399Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"_, axes = plt.subplots(3, 3, figsize=(16, 12), sharex=True)\naxes = axes.ravel()\n\nfor i in range(9):\n    ax = axes[i]\n    sns.histplot(cosine_sim(z, zc[i : i + 1]).squeeze(), ax=ax)\n    ax.set(title=f\"Histogram of $sim(z, z_{i})$\")","metadata":{"_kg_hide-input":true,"execution":{"iopub.status.busy":"2022-09-20T16:53:53.208750Z","iopub.execute_input":"2022-09-20T16:53:53.209690Z","iopub.status.idle":"2022-09-20T16:53:55.830434Z","shell.execute_reply.started":"2022-09-20T16:53:53.209640Z","shell.execute_reply":"2022-09-20T16:53:55.828987Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Evaluation\n\nThe below are evaluation result of supervised/distillated headers.\n\nThis shows distillated head classifies samples perfectly, where as the supervised head classifies accuracy=0.162 which is slightly better than the random guess (1/9=0.111).","metadata":{}},{"cell_type":"code","source":"average_type = \"micro\"\n\nprint(\"=\" * 80)\nprint(\"Evaluation result of supervised head:\")\nprint(f\"accuracy: {accuracy_score(y_truth, y0):.3f}\")\nprint(f\"recall: {recall_score(y_truth, y0, average=average_type):.3f}\")\nprint(\n    f\"precision: {precision_score(y_truth, y0, average=average_type, zero_division=False):.3f}\"\n)\nprint(f\"F1: {f1_score(y_truth, y0, average=average_type, zero_division=False):.3f}\")\nprint(\"=\" * 80)\nprint(\"Evaluation result of distillated head:\")\nprint(f\"accuracy: {accuracy_score(y_truth, y1):.3f}\")\nprint(f\"recall: {recall_score(y_truth, y1, average=average_type):.3f}\")\nprint(\n    f\"precision: {precision_score(y_truth, y1, average=average_type, zero_division=False):.3f}\"\n)\nprint(f\"F1: {f1_score(y_truth, y1, average=average_type, zero_division=False):.3f}\")","metadata":{"_kg_hide-input":true,"execution":{"iopub.status.busy":"2022-09-20T16:53:55.832411Z","iopub.execute_input":"2022-09-20T16:53:55.832823Z","iopub.status.idle":"2022-09-20T16:53:55.907507Z","shell.execute_reply.started":"2022-09-20T16:53:55.832787Z","shell.execute_reply":"2022-09-20T16:53:55.906218Z"},"trusted":true},"execution_count":null,"outputs":[]}]}