| {"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceType":"competition","sourceId":116438,"databundleVersionId":17255611,"mountSlug":"competitions/neurogolf-2026"},{"sourceType":"datasetVersion","sourceId":16553259,"datasetId":10599507,"databundleVersionId":17559372,"mountSlug":"datasets/massimilianoghiotto/neurogolf-convseries-part2-supportvideo"},{"sourceType":"datasetVersion","sourceId":16553493,"datasetId":10599660,"databundleVersionId":17559627,"mountSlug":"datasets/massimilianoghiotto/neurogolf2026-6112"}],"dockerImageVersionId":31400,"isInternetEnabled":true,"language":"python","sourceType":"script","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# %% [markdown]\n# # Convolution Tutorial Series β Part 2: Padding\n# \n# **Goal:** Understand why padding matters β and how to choose the right amount. This is the second episode in a series that we want to create, covering all the attributes of the ONNX Conv function, in this notebook we will cover the **padding** attribute. We will explain these as simply as possible, using some tasks as examples. The first episode of the serie can be found [here](https://www.kaggle.com/code/massimilianoghiotto/convolution-series-part-1). We will explain these as simply as possible, using some tasks as examples, similar to this [notebook](https://www.kaggle.com/code/massimilianoghiotto/best-public-6066-58-eda-111) that we made and EDA notebooks made by @cdeotte.\n# \n# **Task (ARC 258):**\n# 1. **Blue (1) stays where it is.**\n# 2. **A Red (2) appears wherever a Black (0) cell has Blue both to its immediate left AND right.**\n# 3. **Everything else stays Black.**\n# \n# **Score formula:** `Points = max(1.0, 25.0 - log(Mem_bytes + Params))`\n# \n# **Our result:** Mem=0, Params=310, **19.263 points**\n# \n# **The idea:** The rule is horizontal-only, so the kernel is **1Γ3** (no vertical extent).\n\n# %% [code] {\"execution\":{\"iopub.status.busy\":\"2026-05-31T09:45:16.23352Z\",\"iopub.execute_input\":\"2026-05-31T09:45:16.234026Z\",\"iopub.status.idle\":\"2026-05-31T09:45:31.963028Z\",\"shell.execute_reply.started\":\"2026-05-31T09:45:16.233987Z\",\"shell.execute_reply\":\"2026-05-31T09:45:31.961574Z\"}}\n!pip install -q numpy==2.4.4 2>/dev/null\n!pip install -q onnx==1.21.0 2>/dev/null\n!pip install -q onnxruntime==1.24.4 2>/dev/null\n!pip install -q onnx-tool==1.0.1 2>/dev/null\n\n# %% [code] {\"_kg_hide-input\":true,\"jupyter\":{\"source_hidden\":true},\"execution\":{\"iopub.status.busy\":\"2026-05-31T09:45:31.965492Z\",\"iopub.execute_input\":\"2026-05-31T09:45:31.965851Z\",\"iopub.status.idle\":\"2026-05-31T09:45:32.061933Z\",\"shell.execute_reply.started\":\"2026-05-31T09:45:31.965819Z\",\"shell.execute_reply\":\"2026-05-31T09:45:32.061021Z\"}}\nimport json, warnings\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport matplotlib.patches as patches\nimport onnx\nfrom onnx import helper, TensorProto\nimport onnxruntime as ort\n\nwarnings.filterwarnings('ignore')\nplt.rcParams['figure.dpi'] = 120\n\narc_colors = [\n '#000000', # 0: black\n '#0074D9', # 1: blue\n '#FF4136', # 2: red\n '#2ECC40', # 3: green\n '#FFDC00', # 4: yellow\n '#AAAAAA', # 5: gray\n '#F012BE', # 6: magenta\n '#FF851B', # 7: orange\n '#7FDBCA', # 8: teal\n '#870C25', # 9: maroon\n]\ncolor_names = ['black','blue','red','green','yellow','gray','magenta','orange','teal','maroon']\n\ndef plot_arc_grid(grid, ax, title=''):\n H, W = len(grid), len(grid[0])\n img = np.zeros((H, W, 3), dtype=np.uint8)\n for r in range(H):\n for c in range(W):\n hex_c = arc_colors[grid[r][c]].lstrip('#')\n img[r,c] = [int(hex_c[i:i+2], 16) for i in (0, 2, 4)]\n ax.imshow(img, interpolation='nearest')\n ax.set_xticks([]); ax.set_yticks([])\n if title: ax.set_title(title, fontsize=10, fontweight='bold')\n for r in range(H+1): ax.axhline(r-0.5, color='gray', lw=0.5, alpha=0.3)\n for c in range(W+1): ax.axvline(c-0.5, color='gray', lw=0.5, alpha=0.3)\n\nfig, ax = plt.subplots(figsize=(12, 2))\nfor i, (c, nm) in enumerate(zip(arc_colors, color_names)):\n ax.add_patch(patches.Rectangle((i*1.2, 0), 1, 1, facecolor=c, edgecolor='gray', lw=1))\n ax.text(i*1.2+0.5, 0.5, str(i), ha='center', va='center', fontsize=16, fontweight='bold',\n color='white' if i in [0,9] else 'black')\n ax.text(i*1.2+0.5, -0.2, nm, ha='center', va='top', fontsize=8)\nax.set_xlim(-0.5, 12.5)\nax.set_ylim(-0.3, 1.5)\nax.axis('off')\nplt.show()\n\n# %% [markdown]\n# # 1. Task 258 Examples\n# \n# The rule: **Red fills the gap between two Blues.** A lone Blue just stays Blue. A Black cell only becomes Red if it has Blue to **both** its immediate left and right.\n\n# %% [code] {\"_kg_hide-input\":true,\"jupyter\":{\"source_hidden\":true},\"execution\":{\"iopub.status.busy\":\"2026-05-31T09:45:32.063158Z\",\"iopub.execute_input\":\"2026-05-31T09:45:32.063567Z\",\"iopub.status.idle\":\"2026-05-31T09:45:32.283145Z\",\"shell.execute_reply.started\":\"2026-05-31T09:45:32.063535Z\",\"shell.execute_reply\":\"2026-05-31T09:45:32.282252Z\"}}\nwith open('/kaggle/input/competitions/neurogolf-2026/task258.json') as f:\n task258 = json.load(f)\n\nfig, axes = plt.subplots(2, 2, figsize=(10, 6))\nplot_arc_grid(task258['train'][0]['input'], axes[0, 0], 'Train 1 Input')\nplot_arc_grid(task258['train'][0]['output'], axes[0, 1], 'Train 1 Output')\nplot_arc_grid(task258['train'][1]['input'], axes[1, 0], 'Train 2 Input')\nplot_arc_grid(task258['train'][1]['output'], axes[1, 1], 'Train 2 Output')\nplt.tight_layout()\nplt.show()\n\nprint(\"Notice: Blue at edges does NOT create Red outside itself.\")\nprint(\"Red only appears at Black cells sandwiched between two Blues.\")\n\n# %% [markdown]\n# # 2. The 1Γ3 Horizontal Kernel\n# \n# Since the rule only looks left and right (never up or down), we use a **1Γ3** kernel:\n# \n# $$\n# \\begin{bmatrix}\n# (-1) & (0) & (+1)\n# \\end{bmatrix}\n# $$\n# \n# - Offset `(0)` = the center pixel\n# - Offset `(-1)` = the **left** neighbor\n# - Offset `(+1)` = the **right** neighbor\n# \n# ### The Three Output Channels\n# \n# We need three Conv output channels (Black, Blue, Red):\n# \n# ```\n# W[0, 0, 0, 1] = +4.0 # Black identity (center)\n# W[0, 1, 0, 0] = -2.0 # Black supressed by Blue on left\n# W[0, 1, 0, 2] = -2.0 # Black supressed by Blue on right\n# \n# W[1, 1, 0, 1] = +2.0 # Blue identity (center)\n# \n# W[2, 1, 0, 0] = +2.0 # Red votes from Blue on left\n# W[2, 1, 0, 2] = +2.0 # Red votes from Blue on right\n# ```\n# \n# And biases:\n# ```\n# B = [-1.0, -1.0, -3.0, 0.0, 0.0, ..., 0.0]\n# ```\n# \n# | Channel | Weights | Bias | What it computes |\n# |---------|---------|------|------------------|\n# | **Black (0)** | $+4.0\\cdot A_c$ $-2.0\\cdot B_{l}$ $-2.0\\cdot B_{r}$ | $-1.0$ | Stays Black unless surrounded by Blues |\n# | **Blue (1)** | $+2.0\\cdot B_c$ | $-1.0$ | Stays Blue if center is Blue |\n# | **Red (2)** | $+2.0\\cdot B_l$ $+2.0\\cdot B_r$ | $-3.0$ | Fires only if Blue on BOTH sides |\n# \n# Where $A_c$ is 1 if the center pixel of the convolution is black else it is 0. Similarly $B_c, B_l, B_r$ are 1 if, respectively, the center/left/right pixel is Blue, else 0.\n\n# %% [markdown]\n# ### The Threshold Trick\n# \n# The Red channel uses the bias as a **threshold**:\n# - One Blue neighbor: $2.0 - 3.0 = -1.0$ β **no Red**\n# - Two Blue neighbors: $4.0 - 3.0 = +1.0$ β **Red fires**\n# \n# **A single Conv layer can't express \"if A AND B\", but bias-thresholding gives us the same effect!** Each weight contributes *half* the needed activation, and the bias sets the midpoint.\n# \n# The Black channel does the *opposite*: it starts at $+4.0$ (identity), but each Blue neighbor subtracts $-2.0$. If surrounded by Blues ($4.0 - 4.0 = 0$), Black loses to Red ($+1.0$).\n\n# %% [markdown]\n# # 3. Padding\n# Padding is the process of adding extra pixels (usually zeros) around the border of an input image or feature map before applying a convolutional filter. It prevents the output feature map from shrinking and ensures the convolutional filter processes the edge pixels equally.\n# \n# In Part 1, we used a 3Γ3 kernel with `pads=[1, 1, 1, 1]`. We padded **all four sides** because the kernel extends in all directions.\n# \n# Here the kernel is **1Γ3** β it only extends horizontally. We should only pad **left and right**: `pads=[0, 1, 0, 1]`.\n# \n# $$\\text{pads} = [\\underbrace{0}_{\\text{top}}, \\underbrace{1}_{\\text{left}}, \\underbrace{0}_{\\text{bottom}}, \\underbrace{1}_{\\text{right}}]$$\n# \n# Imagine a Blue pixel at **column 0** (left edge). The kernel centered there looks at column -1 β which doesn't exist. With `pads=[0, 1, 0, 1]`, the Conv engine creates a **virtual column of zeros** to the left. That zero means \"no Blue\" β so the Red threshold isn't met (only one Blue neighbor to the right).\n# \n# **Without padding**, the output shrinks and the Blue at column 0 gets truncated β its influence on its right neighbor is lost.\n# \n# Now we make a figure with three different padding values and we see the effect on the output.\n\n# %% [code] {\"execution\":{\"iopub.status.busy\":\"2026-05-31T09:45:32.284388Z\",\"iopub.execute_input\":\"2026-05-31T09:45:32.284719Z\",\"iopub.status.idle\":\"2026-05-31T09:45:32.783683Z\",\"shell.execute_reply.started\":\"2026-05-31T09:45:32.284686Z\",\"shell.execute_reply\":\"2026-05-31T09:45:32.782611Z\"},\"_kg_hide-input\":true,\"jupyter\":{\"source_hidden\":true}}\ndef build_model_with_padding(pads, kernel_shape=[1, 3]):\n W = np.zeros((10, 5, 1, 3), dtype=np.float32) # groups=2\n W[0, 0, 0, 1] = 4.0\n W[0, 1, 0, 0] = -2.0\n W[0, 1, 0, 2] = -2.0\n W[1, 1, 0, 1] = 2.0\n W[2, 1, 0, 0] = 2.0\n W[2, 1, 0, 2] = 2.0\n\n B = np.array([-1.0, -1.0, -3.0, -1.0, -1.0,\n -1.0, -1.0, -1.0, -1.0, -1.0], dtype=np.float32)\n\n inp = helper.make_tensor_value_info('input', TensorProto.FLOAT, [1, 10, 30, 30])\n out = helper.make_tensor_value_info('output', TensorProto.FLOAT, [1, 10, 30, 30])\n W_init = helper.make_tensor('W', TensorProto.FLOAT, [10, 5, 1, 3], W.flatten())\n B_init = helper.make_tensor('B', TensorProto.FLOAT, [10], B.tolist())\n node = helper.make_node('Conv', ['input', 'W', 'B'], ['output'],\n kernel_shape=kernel_shape, pads=pads, group=2)\n graph = helper.make_graph([node], 'task258', [inp], [out], [W_init, B_init])\n return helper.make_model(graph, opset_imports=[helper.make_operatorsetid('', 11)])\n\ndef run_and_get_pred(model, grid):\n oh = np.zeros((1, 10, 30, 30), dtype=np.float32)\n for r in range(len(grid)):\n for c in range(len(grid[0])):\n oh[0, grid[r][c], r, c] = 1.0\n sess = ort.InferenceSession(model.SerializeToString())\n out = sess.run(None, {'input': oh})[0]\n return np.argmax(out[0], axis=0)\n\n# Pick a test input with Blues at edges\ntest_grid = task258['train'][0]['input']\ntarget = task258['train'][0]['output']\nH, W = len(test_grid), len(test_grid[0])\n\n# Three padding strategies\nstrategies = [\n ('No padding: pads=[0,0,0,0]', [0, 0, 0, 0]),\n ('Full padding: pads=[1,1,1,1]', [1, 1, 1, 1]),\n ('Horizontal only: pads=[0,1,0,1]', [0, 1, 0, 1]),\n]\n\nfig, axes = plt.subplots(2, 3, figsize=(13, 6))\nplot_arc_grid(test_grid, axes[0, 0], 'Input')\nplot_arc_grid(target, axes[0, 1], 'Target')\naxes[0, 2].axis('off')\n\nfor i, (label, pads) in enumerate(strategies):\n m = build_model_with_padding(pads)\n pred = run_and_get_pred(m, test_grid)\n plot_arc_grid(pred[:H, :W], axes[1, i], label)\n\nplt.tight_layout()\nplt.show()\n\nprint(\"No padding: output shrinks to 28-wide; Blues at edges lose influence.\")\nprint(\"Full padding: works but wastes vertical computation.\")\nprint(\"Horizontal only: correct β output is 30Γ30, edges behave correctly.\")\n\n# %% [markdown]\n# The Conv `pads` parameter takes 4 values: `[top, left, bottom, right]`.\n# \n# | pads | Output shape | Problem |\n# |------|-------------|--------|\n# | `[0,0,0,0]` (valid) | Shrinks to **30Γ28** | A Blue at column 0 has its left neighbor padded β but more importantly, a Black at column 28 is missing its right neighbor check. The output also shrinks because no padding means the kernel can't slide fully to the edges. |\n# | `[1,1,1,1]` (same) | Upscale to **32Γ30** | This visually works, but the final output has two extra empty rows. |\n# | `[0,1,0,1]` (correct) | Stays **30Γ30** | Pads only where the kernel extends. |\n\n# %% [markdown]\n# ### The Output Shape Formula\n# \n# For a convolution with:\n# - Input size: $(W, H)$\n# - Kernel size: $(k_w, k_h)$\n# - Padding: $(p_{top}, p_{left}, p_{bottom}, p_{right})$\n# - Stride: $(s_w, s_h)=(1,1)$ (we will cover stride in the next chapter, at the moment you can ignore it)\n# \n# The output width is:\n# \n# $$W_{\\text{out}} = \\frac{W + p_{\\text{left}} + p_{\\text{right}} - k_w}{s_w} + 1$$\n# \n# The same hold for the height:\n# \n# $$H_{\\text{out}} = \\frac{H + p_{\\text{top}} + p_{\\text{bottom}} - k_h}{s_h} + 1$$\n# \n# For our 30-wide input with a 1Γ3 kernel:\n# \n# | pads | $p_{\\text{left}}+p_{\\text{right}}$ | $W_{\\text{out}}$ | $p_{\\text{top}}+p_{\\text{bottom}}$ | $H_{\\text{out}}$ |\n# |------|------|------|------|------|\n# | `[0,0,0,0]` | 0 | $\\frac{30 + 0 - 3}{1} + 1 = 28 \\neq 30$ | 0 | $\\frac{30 + 0 - 1}{1} + 1 = 30$ |\n# | `[1,1,1,1]` | 2 | $\\frac{30 + 2 - 3}{1} + 1 = 30$ | 0 | $\\frac{30 + 2 - 1}{1} + 1 = 32$ |\n# | `[0,1,0,1]` | 2 | $\\frac{30 + 2 - 3}{1} + 1 = 30$ | 0 | $\\frac{30 + 0 - 1}{1} + 1 = 30$ |\n\n# %% [code] {\"_kg_hide-input\":true,\"jupyter\":{\"source_hidden\":true},\"execution\":{\"iopub.status.busy\":\"2026-05-31T09:45:32.786191Z\",\"iopub.execute_input\":\"2026-05-31T09:45:32.786482Z\",\"iopub.status.idle\":\"2026-05-31T09:45:32.808973Z\",\"shell.execute_reply.started\":\"2026-05-31T09:45:32.786458Z\",\"shell.execute_reply\":\"2026-05-31T09:45:32.807984Z\"}}\nfrom IPython.display import Video\n\n# Replace with the actual path to your video in the Kaggle input directory\nvideo_path = '/kaggle/input/datasets/massimilianoghiotto/neurogolf-convseries-part2-supportvideo/PaddingTutorial.mp4'\n\nVideo(video_path, width=800, height=450, embed=True)\n\n# %% [markdown]\n# # 4. The Trained Model\n# We now implement and verify our model.\n\n# %% [code] {\"execution\":{\"iopub.status.busy\":\"2026-05-31T09:45:32.810037Z\",\"iopub.execute_input\":\"2026-05-31T09:45:32.810343Z\",\"iopub.status.idle\":\"2026-05-31T09:45:32.820988Z\",\"shell.execute_reply.started\":\"2026-05-31T09:45:32.810309Z\",\"shell.execute_reply\":\"2026-05-31T09:45:32.820141Z\"}}\nW = np.zeros((10, 10, 1, 3), dtype=np.float32)\nW[0, 0, 0, 1] = 4.0\nW[0, 1, 0, 0] = -2.0\nW[0, 1, 0, 2] = -2.0\nW[1, 1, 0, 1] = 2.0\nW[2, 1, 0, 0] = 2.0\nW[2, 1, 0, 2] = 2.0\n\nB = np.array([-1.0, -1.0, -3.0, -1.0, -1.0,\n -1.0, -1.0, -1.0, -1.0, -1.0], dtype=np.float32)\n\ninp = helper.make_tensor_value_info('input', TensorProto.FLOAT, [1, 10, 30, 30])\nout = helper.make_tensor_value_info('output', TensorProto.FLOAT, [1, 10, 30, 30])\nW_init = helper.make_tensor('W', TensorProto.FLOAT, [10, 10, 1, 3], W.flatten().tolist())\nB_init = helper.make_tensor('B', TensorProto.FLOAT, [10], B.tolist())\nnode = helper.make_node('Conv', ['input', 'W', 'B'], ['output'],\n kernel_shape=[1, 3], pads=[0, 1, 0, 1])\ngraph = helper.make_graph([node], 'task258', [inp], [out], [W_init, B_init])\nmodel258 = helper.make_model(graph, opset_imports=[helper.make_operatorsetid('', 11)])\nprint(f\"Model built: groups=2, weights shape {W.shape}, params={W.size + B.size}\")\n\n# %% [code] {\"execution\":{\"iopub.status.busy\":\"2026-05-31T09:45:32.822338Z\",\"iopub.execute_input\":\"2026-05-31T09:45:32.822697Z\",\"iopub.status.idle\":\"2026-05-31T09:45:33.410647Z\",\"shell.execute_reply.started\":\"2026-05-31T09:45:32.822662Z\",\"shell.execute_reply\":\"2026-05-31T09:45:33.40959Z\"},\"_kg_hide-input\":true,\"jupyter\":{\"source_hidden\":true}}\n# Test on all train examples\nfig, axes = plt.subplots(3, 3, figsize=(13, 11))\nfor i in range(3):\n inp = task258['train'][i]['input']\n target = task258['train'][i]['output']\n H, W = len(inp), len(inp[0])\n oh = np.zeros((1, 10, 30, 30), dtype=np.float32)\n for r in range(H):\n for c in range(W):\n oh[0, inp[r][c], r, c] = 1.0\n sess = ort.InferenceSession(model258.SerializeToString())\n out = sess.run(None, {'input': oh})[0]\n pred = np.argmax(out[0], axis=0)\n plot_arc_grid(inp, axes[i, 0], f'Train {i+1} Input')\n plot_arc_grid(target, axes[i, 1], f'Train {i+1} Target')\n plot_arc_grid(pred[:H, :W], axes[i, 2], f'Train {i+1} Pred')\nplt.tight_layout()\nplt.show()\n\n# Verify with official checker\nimport sys\nsys.path.append(\"/kaggle/input/competitions/neurogolf-2026/neurogolf_utils\")\nfrom neurogolf_utils import *\npassed = verify_network(model258, 258, task258)\n\n# %% [markdown]\n# # Summary\n# \n# - **The rule:** Red fills gaps of exactly 1 between two Blues β it does NOT appear at the edges of a single Blue.\n# - **The threshold trick:** A single Conv layer can't do \"if A AND B\" natively, but the bias threshold turns `Blue[left] + Blue[right]` into a logical AND.\n# - **Black removal:** The Black channel uses the same Blue-detection weights (negative) to suppress itself where Red should appear.\n# - **Pad correctly:** A 1Γ3 kernel only needs left+right padding (`pads=[0,1,0,1]`).\n\n# %% [code] {\"execution\":{\"iopub.status.busy\":\"2026-05-31T09:45:33.411954Z\",\"iopub.execute_input\":\"2026-05-31T09:45:33.412471Z\",\"iopub.status.idle\":\"2026-05-31T09:45:35.337361Z\",\"shell.execute_reply.started\":\"2026-05-31T09:45:33.412433Z\",\"shell.execute_reply\":\"2026-05-31T09:45:35.336404Z\"}}\nimport shutil\nimport os\nimport zipfile\n\n# --- CONFIGURATION ---\nSOURCE_FOLDER = '/kaggle/input/datasets/massimilianoghiotto/neurogolf2026-6112/submission'\nOUTPUT_ZIP = '/kaggle/working/submission.zip'\n\n# Package the ZIP (Ensuring files are at the root)\nwith zipfile.ZipFile(OUTPUT_ZIP, 'w', zipfile.ZIP_DEFLATED) as zipf:\n for root, dirs, files in os.walk(SOURCE_FOLDER):\n for file in files:\n if file.endswith('.onnx'):\n file_path = os.path.join(root, file)\n zipf.write(file_path, os.path.relpath(file_path, SOURCE_FOLDER))\n\n# %% [markdown]\n# ### **This is the best that we are able to do for the moment, if anyone has any suggestion, please write it in the comments, we are happy to have some brainstorning between people.**","metadata":{"_uuid":"9469ff71-95bb-47b3-adef-29237085fcd2","_cell_guid":"6f95a052-f3a8-40b4-b3c9-76c9e692365d","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}},"outputs":[],"execution_count":null}]} |