{
"cells": [
{
"cell_type": "markdown",
"id": "26e53498-57e6-477e-8e43-7b8eb1d95882",
"metadata": {
"execution": {}
},
"source": [
"
"
]
},
{
"cell_type": "markdown",
"id": "d207f0be-cbd8-4b93-a823-61af51421e2a",
"metadata": {
"execution": {}
},
"source": [
"# Tutorial 1: Generalization in AI\n",
"\n",
"**Week 1, Day 1: Generalization**\n",
"\n",
"**By Neuromatch Academy**\n",
"\n",
"__Content creators:__ Samuele Bolotta & Patrick Mineault\n",
"\n",
"__Content reviewers:__ Samuele Bolotta, Lily Chamakura, RyeongKyung Yoon, Yizhou Chen, Ruiyi Zhang, Aakash Agrawal, Alish Dipani, Hossein Rezaei, Yousef Ghanbari, Mostafa Abdollahi, Hlib Solodzhuk\n",
"\n",
"__Production editors:__ Konstantine Tsafatinos, Ella Batty, Spiros Chavlis, Samuele Bolotta, Hlib Solodzhuk\n"
]
},
{
"cell_type": "markdown",
"id": "35d71fc6-0728-41bd-b84e-ac78a622ece4",
"metadata": {
"execution": {}
},
"source": [
"___\n",
"\n",
"\n",
"# Tutorial Objectives\n",
"\n",
"*Estimated timing of tutorial: 75 minutes*\n",
"\n",
"This tutorial will introduce you to generalization in the context of modern AI systems. We'll look at a particular system trained for handwriting recognition–TrOCR. We'll review what makes that model tick–the transformer architecture–and explore what goes on into training and finetuning large-scale models. We'll look at how augmentations can bake in tolerance to certain transformations like scaling and cropping.\n",
"\n",
"Our learning objectives for today are:\n",
"\n",
"1. Identify and articulate common objectives pursued by developers of operational AI systems, such as:\n",
"\n",
"- OOD robustness; Latency; Size, Weight, Power, and Cost (SWaP-C)\n",
"- Explainability and understanding\n",
"\n",
"2. Explain at least three strategies for enhancing the generalization capabilities of AI systems, including the contemporary trend of training generic large-scale models on extensive datasets, commonly referred to as the [\"bitter lesson.\"]((http://www.incompleteideas.net/IncIdeas/BitterLesson.html))\n",
"\n",
"3. Gain practical experience with the fundamentals of deep learning and PyTorch.\n",
"\n",
"**Important note**: this tutorial leverages GPU acceleration. Using a GPU runtime in colab will make the the tutorial run 10x faster.\n",
"\n",
"Let's get started!"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "2ae629d9-c104-4716-8eb9-8e53e5151395",
"metadata": {
"cellView": "form",
"execution": {},
"tags": [
"remove-input"
]
},
"outputs": [],
"source": [
"# @markdown\n",
"from IPython.display import IFrame\n",
"from ipywidgets import widgets\n",
"out = widgets.Output()\n",
"with out:\n",
" print(f\"If you want to download the slides: https://osf.io/download/79523/\")\n",
" display(IFrame(src=f\"https://mfr.ca-1.osf.io/render?url=https://osf.io/79523/?direct%26mode=render%26action=download%26mode=render\", width=730, height=410))\n",
"display(out)"
]
},
{
"cell_type": "markdown",
"id": "ee444563-8ef0-4b20-b8d7-92eeace85488",
"metadata": {
"execution": {}
},
"source": [
"---\n",
"# Setup\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Install and import feedback gadget\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "57acdfc5-c864-40a0-b648-be385d5c3eb5",
"metadata": {
"cellView": "form",
"execution": {},
"tags": [
"hide-input"
]
},
"outputs": [],
"source": [
"# @title Install and import feedback gadget\n",
"\n",
"!pip install vibecheck Pillow matplotlib torch torchvision transformers gradio protobuf sentencepiece gradio torchmetrics --quiet\n",
"\n",
"from vibecheck import DatatopsContentReviewContainer\n",
"def content_review(notebook_section: str):\n",
" return DatatopsContentReviewContainer(\n",
" \"\", # No text prompt\n",
" notebook_section,\n",
" {\n",
" \"url\": \"https://pmyvdlilci.execute-api.us-east-1.amazonaws.com/klab\",\n",
" \"name\": \"neuromatch_neuroai\",\n",
" \"user_key\": \"wb2cxze8\",\n",
" },\n",
" ).render()\n",
"\n",
"\n",
"feedback_prefix = \"W1D1_T1\""
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Import dependencies\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "40270953",
"metadata": {
"cellView": "form",
"execution": {},
"tags": [
"remove-input"
]
},
"outputs": [],
"source": [
"# @title Import dependencies\n",
"\n",
"# Standard Libraries for file and operating system operations, security, and web requests\n",
"import os\n",
"import functools\n",
"import hashlib\n",
"import requests\n",
"import logging\n",
"import io\n",
"import re\n",
"import time\n",
"\n",
"# Core python data science and image processing libraries\n",
"import numpy as np\n",
"from PIL import Image as IMG\n",
"from PIL import ImageDraw, ImageFont\n",
"import matplotlib.pyplot as plt\n",
"import tqdm\n",
"\n",
"# Deep Learning and model specific libraries\n",
"import torch\n",
"import torchmetrics.functional.text as fm\n",
"import transformers\n",
"from torchvision import transforms\n",
"from transformers import TrOCRProcessor, VisionEncoderDecoderModel\n",
"\n",
"# Utility and interface libraries\n",
"import gradio as gr\n",
"from IPython.display import IFrame, display, Image\n",
"import sentencepiece\n",
"import zipfile\n",
"import pandas as pd\n",
"\n",
"\n",
"device = \"cuda\" if torch.cuda.is_available() else \"cpu\""
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Figure settings\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "e3fa95f5",
"metadata": {
"cellView": "form",
"execution": {},
"tags": [
"hide-input"
]
},
"outputs": [],
"source": [
"# @title Figure settings\n",
"# @markdown\n",
"\n",
"logging.getLogger('matplotlib.font_manager').disabled = True\n",
"\n",
"%matplotlib inline\n",
"%config InlineBackend.figure_format = 'retina' # perfrom high definition rendering for images and plots\n",
"plt.style.use(\"https://raw.githubusercontent.com/NeuromatchAcademy/course-content/main/nma.mplstyle\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Plotting functions\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "1bf34b9a-1dd5-458a-b390-0fa12609d532",
"metadata": {
"cellView": "form",
"execution": {},
"tags": [
"hide-input"
]
},
"outputs": [],
"source": [
"# @title Plotting functions\n",
"\n",
"def display_image(image_path):\n",
" \"\"\"Display an image from a given file path.\n",
"\n",
" Inputs:\n",
" - image_path (str): The path to the image file.\n",
" \"\"\"\n",
" # Open the image\n",
" image = Image.open(image_path)\n",
" if image.mode != 'RGB':\n",
" image = image.convert('RGB')\n",
"\n",
" # Display the image\n",
" plt.imshow(image)\n",
" plt.axis('off') # Turn off the axis\n",
" plt.show()\n",
"\n",
"def display_transformed_images(image, transformations):\n",
" \"\"\"\n",
" Apply a list of transformations to an image and display them.\n",
"\n",
" Inputs:\n",
" - image (Tensor): The input image as a tensor.\n",
" - transformations (list): A list of torchvision transformations to apply.\n",
" \"\"\"\n",
" # Convert tensor image to PIL Image for display\n",
" pil_image = transforms.ToPILImage()(image)\n",
"\n",
" fig, axs = plt.subplots(len(transformations) + 1, 1, figsize=(5, 15))\n",
" axs[0].imshow(pil_image, cmap='gray')\n",
" axs[0].set_title('Original')\n",
" axs[0].axis('off')\n",
"\n",
" for i, transform in enumerate(transformations):\n",
" # Apply transformation if it's not the placeholder\n",
" if transform != \"Custom ElasticTransform Placeholder\":\n",
" transformed_image = transform(image)\n",
" # Convert transformed tensor image to PIL Image for display\n",
" display_image = transforms.ToPILImage()(transformed_image)\n",
" axs[i+1].imshow(display_image, cmap='gray')\n",
" axs[i+1].set_title(transform.__class__.__name__)\n",
" axs[i+1].axis('off')\n",
" else:\n",
" axs[i+1].text(0.5, 0.5, 'ElasticTransform Placeholder', ha='center')\n",
" axs[i+1].axis('off')\n",
"\n",
" plt.tight_layout()\n",
" plt.show()\n",
"\n",
"def display_original_and_transformed_images(original_tensor, transformed_tensor):\n",
" \"\"\"\n",
" Display the original and transformed images side by side.\n",
"\n",
" Inputs:\n",
" - original_tensor (Tensor): The original image as a tensor.\n",
" - transformed_tensor (Tensor): The transformed image as a tensor.\n",
" \"\"\"\n",
" fig, axs = plt.subplots(1, 2, figsize=(10, 5))\n",
"\n",
" # Display original image\n",
" original_image = original_tensor.permute(1, 2, 0) # Convert from (C, H, W) to (H, W, C)\n",
" axs[0].imshow(original_image, cmap='gray')\n",
" axs[0].set_title('Original')\n",
" axs[0].axis('off')\n",
"\n",
" # Display transformed image\n",
" transformed_image = transformed_tensor.permute(1, 2, 0) # Convert from (C, H, W) to (H, W, C)\n",
" axs[1].imshow(transformed_image, cmap='gray')\n",
" axs[1].set_title('Transformed')\n",
" axs[1].axis('off')\n",
"\n",
" plt.show()\n",
"\n",
"def display_generated_images(generator):\n",
" \"\"\"\n",
" Display images generated from strings.\n",
"\n",
" Inputs:\n",
" - generator (GeneratorFromStrings): A generator that produces images from strings.\n",
" \"\"\"\n",
" plt.figure(figsize=(15, 3))\n",
" for i, (text_img, lbl) in enumerate(generator, 1):\n",
" ax = plt.subplot(1, len(generator.strings) * generator.count // len(generator.strings), i)\n",
" plt.imshow(text_img)\n",
" plt.title(f\"Example {i}\")\n",
" plt.axis('off')\n",
"\n",
" plt.tight_layout()\n",
" plt.show()\n",
"\n",
"\n",
"# Function to generate an image with text\n",
"def generate_image(text, font_path, space_width=2, skewing_angle=8):\n",
" \"\"\"Generate an image with text.\n",
"\n",
" Args:\n",
" text (str): Text to be rendered in the image.\n",
" font_path (str): Path to the font file.\n",
" space_width (int): Space width between characters.\n",
" skewing_angle (int): Angle to skew the text image.\n",
" \"\"\"\n",
" image_size = (350, 50)\n",
" background_color = (255, 255, 255)\n",
" speckle_threshold = 0.05\n",
" speckle_color = (200, 200, 200)\n",
" background = np.random.rand(image_size[1], image_size[0], 1) * 64 + 191\n",
" background = np.tile(background, [1, 1, 4])\n",
" background[:, :, -1] = 255\n",
" image = IMG.fromarray(background.astype('uint8'), 'RGBA')\n",
" image2 = IMG.new('RGBA', image_size, (255, 255, 255, 0))\n",
" draw = ImageDraw.Draw(image2)\n",
" font = ImageFont.truetype(font_path, size=36)\n",
" text_size = draw.textlength(text, font=font)\n",
" text_position = ((image_size[0] - text_size) // 2, (image_size[1] - font.size) // 2)\n",
" draw.text(text_position, text, font=font, fill=(0, 0, 0), spacing=space_width)\n",
" image2 = image2.rotate(skewing_angle)\n",
" image.paste(image2, mask=image2)\n",
" return image\n",
"\n",
"# Function to generate images for multiple strings\n",
"def image_generator(strings, font_path, space_width=2, skewing_angle=8):\n",
" \"\"\"Generate images for multiple strings.\n",
"\n",
" Args:\n",
" strings (list): List of strings to generate images for.\n",
" font_path (str): Path to the font file.\n",
" space_width (int): Space width between characters.\n",
" skewing_angle (int): Angle to skew the text image.\n",
" \"\"\"\n",
" for text in strings:\n",
" yield generate_image(text, font_path, space_width, skewing_angle)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Data retrieval\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "046a34ac-fa41-4e90-ab45-82c14384a83e",
"metadata": {
"cellView": "form",
"execution": {},
"tags": [
"hide-input"
]
},
"outputs": [],
"source": [
"# @title Data retrieval\n",
"\n",
"def download_file(fname, url, expected_md5):\n",
" \"\"\"\n",
" Downloads a file from the given URL and saves it locally.\n",
" Verifies the integrity of the file using an MD5 checksum.\n",
"\n",
" Args:\n",
" - fname (str): The local filename/path to save the downloaded file.\n",
" - url (str): The URL from which to download the file.\n",
" - expected_md5 (str): The expected MD5 checksum to verify the integrity of the downloaded data.\n",
" \"\"\"\n",
" if not os.path.isfile(fname):\n",
" try:\n",
" r = requests.get(url)\n",
" r.raise_for_status() # Raises an HTTPError for bad responses\n",
" except (requests.ConnectionError, requests.HTTPError) as e:\n",
" print(f\"!!! Failed to download {fname} due to: {str(e)} !!!\")\n",
" return\n",
" if hashlib.md5(r.content).hexdigest() == expected_md5:\n",
" with open(fname, \"wb\") as fid:\n",
" fid.write(r.content)\n",
" print(f\"{fname} has been downloaded successfully.\")\n",
" else:\n",
" print(f\"!!! Data download appears corrupted, {hashlib.md5(r.content).hexdigest()} !!!\")\n",
"\n",
"def extract_zip(zip_fname, folder='.'):\n",
" \"\"\"\n",
" Extracts a ZIP file to the specified folder.\n",
"\n",
" Args:\n",
" - zip_fname (str): The filename/path of the ZIP file to be extracted.\n",
" - folder (str): Destination folder where the ZIP contents will be extracted.\n",
" \"\"\"\n",
" if zipfile.is_zipfile(zip_fname):\n",
" with zipfile.ZipFile(zip_fname, 'r') as zip_ref:\n",
" zip_ref.extractall(folder)\n",
" print(f\"Extracted {zip_fname} to {folder}.\")\n",
" else:\n",
" print(f\"Skipped extraction for {zip_fname} as it is not a zip file.\")\n",
"\n",
"# Define the list of files to download, including both ZIP files and other file types\n",
"file_info = [\n",
" (\"Dancing_Script.zip\", \"https://osf.io/32yed/download\", \"d59bd3201b58a37d0d3b4cd0b0ec7400\", '.'),\n",
" (\"lines.zip\", \"https://osf.io/8a753/download\", \"6815ed3987f8eb2fd3bc7678c11f2e9e\", 'lines'),\n",
" (\"transcripts.csv\", \"https://osf.io/9hgr8/download\", \"d81d9ade10db55603cc893345debfaa2\", None),\n",
" (\"neuroai_hello_world.png\", \"https://osf.io/zg4w5/download\", \"f08b81e47f2fe66b5f25b2ccc204c780\", None), # New image file\n",
" (\"sample0.png\", \"https://github.com/neuromatch/NeuroAI_Course/blob/main/tutorials/W1D1_Generalization/static/sample_0.png?raw=true\", '920ae567f707bfee0be29dc854f804ed', None),\n",
" (\"sample1.png\", \"https://github.com/neuromatch/NeuroAI_Course/blob/main/tutorials/W1D1_Generalization/static/sample_1.png?raw=true\", 'cd28623a829b40d0a1dd8c0f17e9ebd7', None),\n",
" (\"sample2.png\", \"https://github.com/neuromatch/NeuroAI_Course/blob/main/tutorials/W1D1_Generalization/static/sample_2.png?raw=true\", 'c189c09abf989eac4e1a8d493bd362d7', None),\n",
" (\"sample3.png\", \"https://github.com/neuromatch/NeuroAI_Course/blob/main/tutorials/W1D1_Generalization/static/sample_3.png?raw=true\", 'dcffc678266952f18af1fc1242127e98', None)\n",
"]\n",
"\n",
"import contextlib\n",
"import io\n",
"\n",
"with contextlib.redirect_stdout(io.StringIO()):\n",
" # Process the downloads and extractions\n",
" for fname, url, expected_md5, folder in file_info:\n",
" download_file(fname, url, expected_md5)\n",
" if folder is not None:\n",
" extract_zip(fname, folder)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Video 1: Overview\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "192ee23b-4ebe-40c1-a6f3-fb1a1ba9d294",
"metadata": {
"cellView": "form",
"execution": {},
"tags": [
"remove-input"
]
},
"outputs": [],
"source": [
"# @title Video 1: Overview\n",
"\n",
"from ipywidgets import widgets\n",
"from IPython.display import YouTubeVideo\n",
"from IPython.display import IFrame\n",
"from IPython.display import display\n",
"\n",
"class PlayVideo(IFrame):\n",
" def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n",
" self.id = id\n",
" if source == 'Bilibili':\n",
" src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n",
" elif source == 'Osf':\n",
" src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n",
" super(PlayVideo, self).__init__(src, width, height, **kwargs)\n",
"\n",
"def display_videos(video_ids, W=400, H=300, fs=1):\n",
" tab_contents = []\n",
" for i, video_id in enumerate(video_ids):\n",
" out = widgets.Output()\n",
" with out:\n",
" if video_ids[i][0] == 'Youtube':\n",
" video = YouTubeVideo(id=video_ids[i][1], width=W,\n",
" height=H, fs=fs, rel=0)\n",
" print(f'Video available at https://youtube.com/watch?v={video.id}')\n",
" else:\n",
" video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n",
" height=H, fs=fs, autoplay=False)\n",
" if video_ids[i][0] == 'Bilibili':\n",
" print(f'Video available at https://www.bilibili.com/video/{video.id}')\n",
" elif video_ids[i][0] == 'Osf':\n",
" print(f'Video available at https://osf.io/{video.id}')\n",
" display(video)\n",
" tab_contents.append(out)\n",
" return tab_contents\n",
"\n",
"video_ids = [('Youtube', 'PgA7wfi2eDo'), ('Bilibili', 'BV1Bm421L7zB')]\n",
"tab_contents = display_videos(video_ids, W=730, H=410)\n",
"tabs = widgets.Tab()\n",
"tabs.children = tab_contents\n",
"for i in range(len(tab_contents)):\n",
" tabs.set_title(i, video_ids[i][0])\n",
"display(tabs)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Submit your feedback\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "6dff30da-aa91-4ada-babd-1b1e652b8589",
"metadata": {
"cellView": "form",
"execution": {},
"tags": [
"hide-input"
]
},
"outputs": [],
"source": [
"# @title Submit your feedback\n",
"content_review(f\"{feedback_prefix}_overview_video\")"
]
},
{
"cell_type": "markdown",
"id": "ef020325-5f43-4e07-bb44-5a5a4d816219",
"metadata": {
"execution": {}
},
"source": [
"---\n",
"# Section 1: Motivation: building a handwriting recognition app with AI\n",
"\n",
"Let’s put ourselves into the mindset of an AI developer who wants to build a note app featuring handwriting recognition."
]
},
{
"cell_type": "markdown",
"id": "febbaa92-2a2d-4e42-a236-0bae90c7f753",
"metadata": {
"cellView": "form",
"execution": {}
},
"source": [
""
]
},
{
"cell_type": "markdown",
"id": "d0367ba9-a7ff-49c1-8bd1-0860130e7f39",
"metadata": {
"execution": {}
},
"source": [
"Our intrepid developer doesn't want to start from scratch, so searches for a pretrained model. They find a suitable model hosted on HuggingFace, the largest repository of pretrained natural language and vision models. [TrOCR](https://huggingface.co/docs/transformers/en/model_doc/trocr) is a Transformer-based model that performs Optical Character Recognition and handwriting transcription. Several checkpoints are available, finetuned for different downstream applications like handwriting transcription and printed character recognition. Our relieved developer draws a deep sigh: they don't have to start from scratch."
]
},
{
"cell_type": "markdown",
"id": "e1d7fdfb-4333-42b0-9906-aa7a54b7bba8",
"metadata": {
"cellView": "form",
"execution": {}
},
"source": [
""
]
},
{
"cell_type": "markdown",
"id": "4bcd7a5a-b3cb-4999-a373-3dda956ba77f",
"metadata": {
"execution": {}
},
"source": [
"In this tutorial, we'll look at the design considerations that go into training and deploying a model like TrOCR, what goes on inside the model's transformers, and how it achieves good or bad out-of-distribution generalization. While the NeuroAI course as a whole will explore new ideas at the frontier of neuroscience and AI, we'll first want to understand one of the bread-and-butter building blocks used in industrial AI: the transformer.\n",
"\n",
"Let's try out this model ourselves!\n",
"\n",
"## Interactive demo 1: TrOCR\n",
"\n",
"We load a pretrained TrOCR checkpoint from HuggingFace. The `transformers` package from HuggingFace allows us to download a PyTorch model definition and a preprocessing class, and to load a pretrained checkpoint in just a few lines of code."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b00326ca-807d-460f-adf4-767e94bc0ccc",
"metadata": {
"execution": {}
},
"outputs": [],
"source": [
"# Load the pre-trained TrOCR model and processor\n",
"with contextlib.redirect_stdout(io.StringIO()):\n",
" model = VisionEncoderDecoderModel.from_pretrained(\"microsoft/trocr-base-handwritten\")\n",
" model.to(device=device)\n",
" processor = TrOCRProcessor.from_pretrained(\"microsoft/trocr-base-handwritten\", use_fast=False)"
]
},
{
"cell_type": "markdown",
"id": "1e20e1f0-c052-42f5-a33a-87d89548c90c",
"metadata": {
"execution": {}
},
"source": [
"We now write a callback function that calls the preloaded model to decode a particular image."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "1d1debc6-b3b1-4173-839c-f2a4240efb2d",
"metadata": {
"execution": {}
},
"outputs": [],
"source": [
"# Define the function to recognize text from an image\n",
"def recognize_text(processor, model, image):\n",
" \"\"\"\n",
" This function takes an image as input and uses a pre-trained language model to generate text from the image.\n",
"\n",
" Inputs:\n",
" - processor: The processor to use\n",
" - model: The model to use\n",
" - image (PIL Image or Tensor): The input image containing text to be recognized.\n",
"\n",
" Outputs:\n",
" - text (str): The recognized text extracted from the input image.\n",
" \"\"\"\n",
" print(image)\n",
" pixel_values = processor(images=image, return_tensors=\"pt\").pixel_values\n",
" generated_ids = model.generate(pixel_values.to(device))\n",
" text = processor.batch_decode(generated_ids, skip_special_tokens=True)[0]\n",
" return text"
]
},
{
"cell_type": "markdown",
"id": "88658698-5e50-4740-8b46-087a8417d189",
"metadata": {
"execution": {}
},
"source": [
"We build a simple interface in `gradio` to try out the model interactively. Go ahead and try some example text to see how it works. You can use images from the internet, or scan your own handwriting. Just make sure that the text fits on one line. Observe the result of the recognized text."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a73d5f88-0296-42b2-86af-aa7f55e8ba11",
"metadata": {
"execution": {}
},
"outputs": [],
"source": [
"import gradio as gr\n",
"import functools\n",
"\n",
"with gr.Blocks() as demo:\n",
" gr.HTML(\"