<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Joris Baan on Medium]]></title>
        <description><![CDATA[Stories by Joris Baan on Medium]]></description>
        <link>https://medium.com/@jorisbaan?source=rss-eeb9026e13e0------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*2Dt6zWhLXMwlYFOVw60qPg.png</url>
            <title>Stories by Joris Baan on Medium</title>
            <link>https://medium.com/@jorisbaan?source=rss-eeb9026e13e0------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Thu, 05 Nov 2026 16:11:11 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@jorisbaan/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Designing, Building & Deploying an AI Chat App from Scratch (Part 2)]]></title>
            <link>https://medium.com/data-science/designing-building-deploying-an-ai-chat-app-from-scratch-part-2-c75f712eebe5?source=rss-eeb9026e13e0------2</link>
            <guid isPermaLink="false">https://medium.com/p/c75f712eebe5</guid>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[hands-on-tutorials]]></category>
            <category><![CDATA[cloud]]></category>
            <category><![CDATA[azure]]></category>
            <category><![CDATA[deployment]]></category>
            <dc:creator><![CDATA[Joris Baan]]></dc:creator>
            <pubDate>Mon, 20 Jan 2025 10:02:26 GMT</pubDate>
            <atom:updated>2025-01-20T10:56:19.035Z</atom:updated>
            <content:encoded><![CDATA[<h4>Cloud Deployment and Scaling</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*SD_HdcSgx3eFn3xo" /><figcaption>Photo by <a href="https://unsplash.com/@killerfvith?utm_source=medium&amp;utm_medium=referral">Alex wong</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><h3>1. Introduction</h3><p>In the <a href="https://towardsdatascience.com/designing-building-deploying-an-ai-chat-app-from-scratch-part-1-f1ebf5232d4d">previous post</a>, we built an AI-powered chat application on our local computer using microservices. Our stack included FastAPI, Docker, Postgres, Nginx and llama.cpp. The goal of this post is to learn more about the fundamentals of cloud deployment and scaling by deploying our app to Azure, making it available to real users. We’ll use Azure because they offer a <a href="https://azure.microsoft.com/en-us/free/students">free education account</a>, but the process is similar for other platforms like AWS and GCP.</p><p>You can check a live demo of the app at <a href="http://chat.jorisbaan.nl/">chat.jorisbaan.nl</a>. Now, obviously, this demo isn’t very large-scale, because the costs ramp up very quickly. With the tight scaling limits I configured I reckon it can handle about 10–40 concurrent users until I run out of Azure credits. However, I do hope it demonstrates the principles behind a scalable production system. We could easily configure it to scale to many more users with a higher budget.</p><p>I give a complete breakdown of our infrastructure and the costs at the end. The codebase is at <a href="https://github.com/jsbaan/ai-app-from-scratch">https://github.com/jsbaan/ai-app-from-scratch</a>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/974/1*7zbKzWipoav-lFAk1V_RyQ.gif" /><figcaption>A quick demo of the app at <a href="http://chat.jorisbaan.nl">chat.jorisbaan.nl</a>. We start a new chat, come back to that same chat, and start another chat.</figcaption></figure><h4>1.1. Recap: local application</h4><p>Let’s recap how we <a href="https://towardsdatascience.com/designing-building-deploying-an-ai-chat-app-from-scratch-part-1-f1ebf5232d4d">built our local app</a>: A user can start or continue a chat with a language model by sending an HTTP request to <a href="http://localhost">http://localhost</a>. An Nginx reverse proxy receives and forwards the request to a UI over a private Docker network. The UI stores a session cookie to identify the user, and sends requests to the backend: the language model API that generates text, and the database API that queries the database server.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6iu3f8YETg2GqEEe-UlKJA.png" /><figcaption>Local architecture of the app. See <a href="https://towardsdatascience.com/designing-building-deploying-an-ai-chat-app-from-scratch-part-1-f1ebf5232d4d">part 1</a> for more details. Made by author in draw.io.</figcaption></figure><h3>Table of contents</h3><ol><li><a href="https://towardsdatascience.com/designing-building-deploying-an-ai-chat-app-from-scratch-part-2-c75f712eebe5#e33e">Introduction</a><br>1.1 <a href="https://towardsdatascience.com/designing-building-deploying-an-ai-chat-app-from-scratch-part-2-c75f712eebe5#dbbe">Recap: local application</a></li><li><a href="https://towardsdatascience.com/designing-building-deploying-an-ai-chat-app-from-scratch-part-2-c75f712eebe5#a544">Cloud architecture</a><br>2.1 <a href="https://towardsdatascience.com/designing-building-deploying-an-ai-chat-app-from-scratch-part-2-c75f712eebe5#b3c8">Scaling</a><br>2.2 <a href="https://towardsdatascience.com/designing-building-deploying-an-ai-chat-app-from-scratch-part-2-c75f712eebe5#beb8">Kubernetes Concepts</a><br>2.3 <a href="https://towardsdatascience.com/designing-building-deploying-an-ai-chat-app-from-scratch-part-2-c75f712eebe5#2eca">Azure Container Apps</a><br>2.4 <a href="https://towardsdatascience.com/designing-building-deploying-an-ai-chat-app-from-scratch-part-2-c75f712eebe5#a773">Azure architecture: putting it all together</a></li><li><a href="https://towardsdatascience.com/designing-building-deploying-an-ai-chat-app-from-scratch-part-2-c75f712eebe5#173f">Deployment</a><br>3.1 <a href="https://towardsdatascience.com/designing-building-deploying-an-ai-chat-app-from-scratch-part-2-c75f712eebe5#6f59">Setting up</a><br>3.2 <a href="https://towardsdatascience.com/designing-building-deploying-an-ai-chat-app-from-scratch-part-2-c75f712eebe5#ad10">PostgreSQL server deployment</a><br>3.3 <a href="https://towardsdatascience.com/designing-building-deploying-an-ai-chat-app-from-scratch-part-2-c75f712eebe5#5a8b">Azure Container App Environment deployment</a><br>3.4 <a href="https://towardsdatascience.com/designing-building-deploying-an-ai-chat-app-from-scratch-part-2-c75f712eebe5#81f2">Azure Container Apps deployment</a><br>3.5 <a href="https://towardsdatascience.com/designing-building-deploying-an-ai-chat-app-from-scratch-part-2-c75f712eebe5#796c">Scaling our Container Apps</a><br>3.6 <a href="https://towardsdatascience.com/designing-building-deploying-an-ai-chat-app-from-scratch-part-2-c75f712eebe5#adb6">Custom domain name &amp; HTTPS</a></li><li><a href="https://towardsdatascience.com/designing-building-deploying-an-ai-chat-app-from-scratch-part-2-c75f712eebe5#63e2">Resources &amp; costs overview</a></li><li><a href="https://towardsdatascience.com/designing-building-deploying-an-ai-chat-app-from-scratch-part-2-c75f712eebe5#a75d">Roadmap</a></li><li><a href="https://towardsdatascience.com/designing-building-deploying-an-ai-chat-app-from-scratch-part-2-c75f712eebe5#26b3">Final thoughts</a><br><a href="https://towardsdatascience.com/designing-building-deploying-an-ai-chat-app-from-scratch-part-2-c75f712eebe5#14ad">Acknowledgements</a><br><a href="https://towardsdatascience.com/designing-building-deploying-an-ai-chat-app-from-scratch-part-2-c75f712eebe5#728e">AI usage</a></li></ol><h3>2. Cloud architecture</h3><p>Conceptually, our cloud architecture will not be too different from our local application: a bunch of containers in a private network with a gateway to the outside world, our users.</p><p>However, instead of running containers on our local computer with Docker Compose, we will deploy them to a computing environment that automatically scales across virtual or psychical machines to many concurrent users.</p><h3>2.1 Scaling</h3><p>Scaling is a central concept in cloud architectures. It means being able to dynamically handle varying numbers of users (i.e., HTTP requests). Uvicorn, the web server running our UI and database API, can already handle about 40 concurrent requests. It’s even possible to use another web server called Gunicorn as a process manager that employs multiple Uvicorn workers in the same container, further increasing concurrency.</p><p>Now, if we want to support even more concurrent request, we could give each container more resources, like CPUs or memory (<strong>vertical scaling</strong>). However, a more reliable approach is to dynamically create copies (<strong>replicas</strong>) of a container based on the number of incoming HTTP requests or memory/CPU usage, and distribute the incoming traffic across replicas (<strong>horizontal scaling</strong>). Each replica container will be assigned an IP address, so we also need to think about networking: how to centrally receive all requests and distribute them over the container replicas.</p><p>This “prism” pattern is important: requests arrive centrally in some server (a <strong>load balancer</strong>) and fan out for parallel processing to multiple other servers (e.g., several identical UI containers)<strong>.</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*HC_wGvWTpnF9dp-D" /><figcaption>Photo of two prisms by <a href="https://unsplash.com/@cferdo?utm_source=medium&amp;utm_medium=referral">Fernando @cferdophotography</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><h3>2.2 Kubernetes Concepts</h3><p><a href="https://kubernetes.io/">Kubernetes</a> is the industry standard system for automating deployment, scaling and management of containerized applications. Its core concepts are crucial to understand modern cloud architectures, including ours, so let’s quickly review the basics.</p><ul><li><strong>Node</strong>: A physical or virtual machine to run containerized app or manage the cluster.</li><li><strong>Cluster</strong>: A set of Nodes managed by Kubernetes.</li><li><strong>Pod</strong>: The smallest deployable unit in Kubernetes. Runs one main app container with optional secondary containers that share storage and networking.</li><li><strong>Deployment</strong>: An abstraction that manages the desired state of a set of Pod replicas by deploying, scaling and updating them.</li><li><strong>Service</strong>: An abstraction that manages a stable entrypoint (the service’s DNS name) to expose a set of Pods by distributing incoming traffic over the various dynamic Pod IP addresses. A Service has multiple types:<br>- A <strong>ClusterIP</strong> Service exposes Pods within the Cluster<br>- A <strong>LoadBalancer</strong> Service exposes Pods to outside the Cluster. It triggers the cloud provider to provision an external public IP and load balancer outside the cluster that can be used to reach the cluster. These external requests are then routed via the Service to individual Pods.</li><li><strong>Ingress</strong>: An abstraction that defines more complex rules for a cluster’s entrypoint. It can route traffic to multiple Services; give Services externally-reachable URLs; load balance traffic; and handle secure HTTPS.</li><li><strong>Ingress Controller</strong>: Implements the Ingress rules. For example, an Nginx-based controller runs an Nginx server (<a href="https://github.com/jsbaan/ai-app-from-scratch/blob/main/compose.yaml#L60">like in our local app</a>) under the hood that is dynamically configured to route traffic according to Ingress rules. To expose the Ingress Controller itself to the outside world, you can use a LoadBalancer Service. This architecture is often used.</li></ul><h3>2.3 Azure Container Apps</h3><p>Armed with these concepts, instead of deploying our app with Kubernetes directly, I wanted to experiment a little by using Azure Container Apps (ACA). This is a serverless platform built on top of Kubernetes that abstracts away some of its complexity.</p><p>With a single command, we can create a Container App Environment, which, under the hood, is an invisible Kubernetes Cluster managed by Azure. Within this Environment, we can run a container as a Container App that Azure internally manages as Kubernetes Deployments, Services, and Pods. See <a href="https://techcommunity.microsoft.com/blog/startupsatmicrosoftblog/aca-vs-aks-which-azure-service-is-better-for-running-containers/3815164">article 1</a> and <a href="https://techcommunity.microsoft.com/blog/fasttrackforazureblog/azure-container-apps-networking-a-condensed-view-of-concepts/3634304">article 2</a> for detailed comparisons.</p><p>A Container App Environment also auto-creates:</p><ol><li>An invisible <a href="https://learn.microsoft.com/en-us/azure/container-apps/networking?tabs=workload-profiles-env%2Cazure-cli#http-edge-proxy-behavior"><strong>Envoy Ingress Controller</strong></a> that routes requests to internal Apps and handles HTTPS and App auto-scaling based on request volume.</li><li>An external <strong>Public IP address</strong> and <strong>Azure Load Balancer</strong> that routes external traffic to the Ingress Controller that in turn routes it to Apps (sounds similar to a Kubernetes LoadBalancer Service, eh?).</li><li>An Azure-generated URL for each Container App that is publicly accessible over the internet or internal, based on its Ingress config.</li></ol><p>This gives us everything we need to run our containers at scale. The only thing missing is a database. We will use an Azure-managed PostgreSQL server instead of deploying our own container, because it’s easier, more reliable and scalable. Our local Nginx reverse proxy container is also obsolete because ACA automatically deploys an Envoy Ingress Controller.</p><p>It’s interesting to note that we literally don’t have to change a single line of <a href="https://github.com/jsbaan/ai-app-from-scratch/tree/main">code</a> in our local application, we can just treat it as a bunch of containers!</p><h3>2.4 Azure architecture: putting it all together</h3><p>Here is a diagram of the full cloud architecture for our chat application that contains all our Azure resources. Let’s take a high level look at how a user request flows through the system.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*bChnusLrPbt6kLgY6VBaRQ.png" /><figcaption>Azure architecture diagram. Made by author in draw.io.</figcaption></figure><ol><li>User sends HTTPS request to <a href="http://chat.jorisbaan.nl">chat.jorisbaan.nl</a>.</li><li>A Public DNS server like Google DNS resolves this domain name to an Azure Public IP address.</li><li>The Azure Load Balancer on this IP address routes the request to the (for us invisible) Envoy Ingress Controller.</li><li>The Ingress Controller routes the request to UI Container App, who routes it to one of its Replicas where a UI web server is running.</li><li>The UI web server makes requests to the database API and language model API Apps, who both route it to one of their Replicas.</li><li>A database API replica queries the PostgreSQL server hostname. The Azure Private DNS Zone resolves the hostname to the PostgreSQL server’s IP address.</li></ol><h3>3. Deployment</h3><p>So, how do we actually create all this? Rather than clicking around in the Azure Portal, infrastructure-as-code tools like <a href="https://www.terraform.io/">Terraform</a> are best to create and manage cloud resources. However, for simplicity, I will instead use the Azure CLI to create a bash script that deploys our entire application step by step. You can find the full deployment script including environment variables <a href="https://github.com/jsbaan/ai-app-from-scratch/blob/main/azure-deployment/deploy_app.sh">here 🤖</a>. We will go through it step by step now.</p><h3>3.1 Setting up</h3><p>We need an Azure account (I’m using a <a href="https://azure.microsoft.com/en-us/free/students">free education account</a>), a clone of the <a href="https://github.com/jsbaan/ai-app-from-scratch">https://github.com/jsbaan/ai-app-from-scratch</a> repo, Docker to build and push the container images, the <a href="https://github.com/jsbaan/ai-app-from-scratch/blob/main/lm-api/README.md">downloaded model</a>, and the Azure CLI to start creating cloud resources.</p><p>We first create a resource group so our resources are easier to find, manage and delete. The --location parameter refers to the physical datacenter we’ll use to deploy our app’s infrastructure. Ideally, it is close to our users. We then create a private virtual network with 256 IP addresses to isolate, secure and connect our database server and Container Apps.</p><pre>brew update &amp;&amp; brew install azure-cli # for macos<br><br>echo &quot;Create resource group&quot;<br>az group create \<br>  --name $RESOURCE_GROUP \<br>  --location &quot;$LOCATION&quot;<br>  <br>echo &quot;Create VNET with 256 IP addresses&quot;<br>az network vnet create \<br>  --resource-group $RESOURCE_GROUP \<br>  --name $VNET \<br>  --address-prefix 10.0.0.0/24 \<br>  --location $LOCATION</pre><h3>3.2 PostgreSQL server deployment</h3><p>Depending on the hardware, an Azure-managed PostgreSQL database server costs about $13 to $7000 a month. To communicate with Container Apps, we put the DB server within the same private virtual network but in its own subnet. A subnet is a dedicated range of IP addresses that can have its own security and routing rules.</p><p>We create the Azure PostgreSQL Flexible Server with private access. This means only resources within the same virtual network can reach it. Azure automatically creates a Private DNS Zone that manages a hostname for the database that resolves to its IP address. The database API will later use this hostname to connect to the database server.</p><p>We will randomly generate the database credentials and store them in a secure place: Azure KeyVault.</p><pre>echo &quot;Create subnet for DB with 128 IP addresses&quot;<br>az network vnet subnet create \<br>  --resource-group $RESOURCE_GROUP \<br>  --name $DB_SUBNET \<br>  --vnet-name $VNET \<br>  --address-prefix 10.0.0.128/25<br><br>echo &quot;Create a key vault to securely store and retrieve secrets, \<br>like the db password&quot;<br>az keyvault create \<br>  --name $KEYVAULT \<br>  --resource-group $RESOURCE_GROUP \<br>  --location $LOCATION<br><br>echo &quot;Give myself access to the key vault so I can store and retrieve \<br>the db password&quot;<br>az role assignment create \<br>  --role &quot;Key Vault Secrets Officer&quot; \<br>  --assignee $EMAIL \<br>  --scope &quot;/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.KeyVault/vaults/$KEYVAULT&quot;<br><br>echo &quot;Store random db username and password in the key vault&quot;<br>az keyvault secret set \<br>  --name postgres-username \<br>  --vault-name $KEYVAULT  \<br>  --value $(openssl rand -base64 12 | tr -dc &#39;a-zA-Z&#39; | head -c 12)<br>az keyvault secret set \<br>  --name postgres-password \<br>  --vault-name $KEYVAULT  \<br>  --value $(openssl rand -base64 16)<br><br>echo &quot;While we&#39;re at it, let&#39;s already store a secret session key for the UI&quot;<br>az keyvault secret set \<br>  --name session-key \<br>  --vault-name $KEYVAULT  \<br>  --value $(openssl rand -base64 16)<br> <br>echo &quot;Create PostgreSQL flexible server in our VNET in its own subnet. \<br>Auto-creates Private DS Zone.&quot;<br>POSTGRES_USERNAME=$(az keyvault secret show --name postgres-username --vault-name $KEYVAULT --query &quot;value&quot; --output tsv)<br>POSTGRES_PASSWORD=$(az keyvault secret show --name postgres-password --vault-name $KEYVAULT --query &quot;value&quot; --output tsv)<br>az postgres flexible-server create \<br>  --resource-group $RESOURCE_GROUP \<br>  --name $DB_SERVER \<br>  --vnet $VNET \<br>  --subnet $DB_SUBNET \<br>  --location $LOCATION \<br>  --admin-user $POSTGRES_USERNAME \<br>  --admin-password $POSTGRES_PASSWORD \<br>  --sku-name Standard_B1ms \<br>  --tier Burstable \<br>  --storage-size 32 \<br>  --version 16 \<br>  --yes</pre><h3>3.3 Azure Container App Environment deployment</h3><p>With the network and database in place, let’s deploy the infrastructure to run containers — the Container App Environment (recall, this is a Kubernetes cluster under the hood).</p><p>We create another subnet with 128 IP addresses and delegate its management to the Container App Environment. The subnet should be big enough for <a href="https://learn.microsoft.com/en-us/azure/container-apps/networking?tabs=workload-profiles-env%2Cazure-cli#subnet">every ten new replicas to get a new IP address</a> in the subrange. We can then create the Environment. This is just a single command without much configuration.</p><pre>echo &quot;Create subnet for ACA with 128 IP addresses.&quot;<br>az network vnet subnet create \<br>  --resource-group $RESOURCE_GROUP \<br>  --name $ACA_SUBNET \<br>  --vnet-name $VNET \<br>  --address-prefix 10.0.0.0/25<br><br>echo &quot;Delegate the subnet to ACA&quot;<br>az network vnet subnet update \<br>  --resource-group $RESOURCE_GROUP \<br>  --vnet-name $VNET \<br>  --name $ACA_SUBNET \<br>  --delegations Microsoft.App/environments<br>  <br>echo &quot;Obtain the ID of our subnet&quot;<br>ACA_SUBNET_ID=$(az network vnet subnet show \<br>  --resource-group $RESOURCE_GROUP \<br>  --name $ACA_SUBNET \<br>  --vnet-name $VNET \<br>  --query id --output tsv)<br>  <br>echo &quot;Create Container Apps Environment in our custom subnet.\\<br>By default, it has a Workload profile with Consumption plan.&quot;<br>az containerapp env create \<br>  --resource-group $RESOURCE_GROUP \<br>  --name $ACA_ENVIRONMENT \<br>  --infrastructure-subnet-resource-id $ACA_SUBNET_ID \<br>  --location $LOCATION</pre><h3>3.4 Azure Container Apps deployment</h3><p>Each Container App needs a Docker image to run. Let’s first setup a Container Registry, and then build all our images locally and push them to the registry. Note that we simply copied the model file into the language model image using its <a href="https://github.com/jsbaan/ai-app-from-scratch/blob/main/lm-api/Dockerfile">Dockerfile</a>, so we don’t need to mount external storage like we did for <a href="https://github.com/jsbaan/ai-app-from-scratch/blob/main/compose.yaml#L54">local deployment in part 1</a>.</p><pre>echo &quot;Create container registry (ACR)&quot;<br>az acr create \\<br>  --resource-group $RESOURCE_GROUP \\<br>  --name $ACR \\<br>  --sku Standard \\<br>  --admin-enabled true<br>  <br>echo &quot;Login to ACR and push local images&quot;<br>az acr login --name $ACR<br>docker build --tag $ACR.azurecr.io/$DB_API $DB_API<br>docker push $ACR.azurecr.io/$DB_API<br>docker build --tag $ACR.azurecr.io/$LM_API $LM_API<br>docker push $ACR.azurecr.io/$LM_API<br>docker build --tag $ACR.azurecr.io/$UI $UI<br>docker push $ACR.azurecr.io/$UI</pre><p>Now, onto deployment. To create Container Apps we specify their Environment, container registry, image, and the port they will listen to for requests. The ingress parameter regulates whether Container Apps can be reached from the outside world. Our two APIs are internal and therefore completely isolated, with no public URL and no traffic ever routed from the Envoy Ingress Controller. The UI is external and has a public URL, but sends internal HTTP requests over the virtual network to our APIs. We pass these internal hostnames and db credentials as environment variables.</p><pre>echo &quot;Deploy DB API on Container Apps with the db credentials from the key \<br>vault as env vars. More secure is to use a managed identity that allows the \<br>container itself to retrieve them from the key vault. But for simplicity we \<br>simply fetch it ourselves using the CLI.&quot;<br>POSTGRES_USERNAME=$(az keyvault secret show --name postgres-username --vault-name $KEYVAULT --query &quot;value&quot; --output tsv)<br>POSTGRES_PASSWORD=$(az keyvault secret show --name postgres-password --vault-name $KEYVAULT --query &quot;value&quot; --output tsv)<br>az containerapp create --name $DB_API \<br>  --resource-group $RESOURCE_GROUP \<br>  --environment $ACA_ENVIRONMENT \<br>  --registry-server $ACR.azurecr.io \<br>  --image $ACR.azurecr.io/$DB_API \<br>  --target-port 80 \<br>  --ingress internal \<br>  --env-vars &quot;POSTGRES_HOST=$DB_SERVER.postgres.database.azure.com&quot; &quot;POSTGRES_USERNAME=$POSTGRES_USERNAME&quot; &quot;POSTGRES_PASSWORD=$POSTGRES_PASSWORD&quot; \<br>  --min-replicas 1 \<br>  --max-replicas 5 \<br>  --cpu 0.5 \<br>  --memory 1<br><br>echo &quot;Deploy UI on Container Apps, and retrieve the secret random session \<br>key the UI uses to encrypt session cookies&quot;<br>SESSION_KEY=$(az keyvault secret show --name session-key --vault-name $KEYVAULT --query &quot;value&quot; --output tsv)<br>az containerapp create --name $UI \<br>  --resource-group $RESOURCE_GROUP \<br>  --environment $ACA_ENVIRONMENT \<br>  --registry-server $ACR.azurecr.io \<br>  --image $ACR.azurecr.io/$UI \<br>  --target-port 80 \<br>  --ingress external \<br>  --env-vars &quot;db_api_url=http://$DB_API&quot; &quot;lm_api_url=http://$LM_API&quot; &quot;session_key=$SESSION_KEY&quot; \<br>  --min-replicas 1 \<br>  --max-replicas 5 \<br>  --cpu 0.5 \<br>  --memory 1 <br>  <br>echo &quot;Deploy LM API on Container Apps&quot;<br>az containerapp create --name $LM_API \<br>  --resource-group $RESOURCE_GROUP \<br>  --environment $ACA_ENVIRONMENT \<br>  --registry-server $ACR.azurecr.io \<br>  --image $ACR.azurecr.io/$LM_API \<br>  --target-port 80 \<br>  --ingress internal \<br>  --min-replicas 1 \<br>  --max-replicas 5 \<br>  --cpu 2 \<br>  --memory 4 \<br>  --scale-rule-name my-http-rule \<br>  --scale-rule-http-concurrency 2</pre><h3>3.5 Scaling our Container Apps</h3><p>Let’s take a look at how our Container Apps they scale. Container Apps can scale to zero, which means they have zero replicas and stop running (and stop incurring costs). This is a feature of the serverless paradigm, where infrastructure is provisioned on demand. <a href="https://techcommunity.microsoft.com/blog/startupsatmicrosoftblog/from-chaos-to-clarity-simplifying-your-networking-with-azure-container-apps/4034625">The invisible Envoy proxy handles scaling</a> based on triggers, like concurrent HTTP requests. Spawning new replicas may take some time, which is called cold-start. We set the minimum number of replicas to 1 to avoid cold starts and the resulting timeout errors for first requests.</p><p>The default scaling rule creates a new replica whenever an existing replica receives 10 concurrent HTTP requests. This applies to the UI and the database API. To test whether this scaling rule makes sense, we would have to perform load testing to simulate real user traffic and see what each Container App replica can handle individually. My guess is that they can handle a lot more concurrent request than 10, and we could relax the rule.</p><h3>3.5.1 Scaling language model inference.</h3><p>Even with our small, quantized language model, inference requires much more compute than a simple FastAPI app. The inference server handles incoming requests sequentially, and the default Container App resources of 0.5 virtual CPU cores and 1GB memory result in very slow response times: up to 30 seconds for generating 128 tokens with a context window of 1024 (these parameters are defined in the <a href="https://github.com/jsbaan/ai-app-from-scratch/blob/main/lm-api/Dockerfile">LM API’s Dockerfile</a>).</p><p>Increasing vCPU to 2 and memory to 4GB gives much better inference speed, and handles about 10 requests within 30 seconds. I configured the http scaling rule very tightly at 2 concurrent requests, so whenever 2 users chat at the same time, the LM API will scale out.</p><p>With 5 maximum replicas, I think this will allow for roughly 10–40 concurrent users, depending on the length of the chat histories. Now, obviously, this isn’t very large-scale, but with a higher budget, we could increase vCPUs, memory and the number of replicas. Ultimately we would need to move to GPU-based inference. More on that later.</p><h3>3.6 Custom domain name &amp; HTTPS</h3><p>The automatically generated URL from the UI App looks like <a href="https://chat-ui.purplepebble-ac46ada4.germanywestcentral.azurecontainerapps.io/">https://chat-ui.purplepebble-ac46ada4.germanywestcentral.azurecontainerapps.io/</a>. This isn’t very memorable, so I want to make our app available as subdomain on my website: <a href="http://chat.jorisbaan.nl">chat.jorisbaan.nl</a>.</p><p>I simply add two DNS records on my domain registrar portal (like GoDaddy). A CNAME record that links my chat subdomain to the UI’s URL, and TXT record to prove ownership of the subdomain to Azure and obtain a TLS certificate.</p><pre># Obtain UI URL and verification code<br>URL=$(az containerapp show -n $UI -g $RESOURCE_GROUP -o tsv --query &quot;properties.configuration.ingress.fqdn&quot;)<br>VERIFICATION_CODE=$(az containerapp show -n $UI -g $RESOURCE_GROUP -o tsv --query &quot;properties.customDomainVerificationId&quot;)<br><br># Add a CNAME record with the URL and a TXT record with the verification code to domain registrar<br># (Do this manually)<br><br># Add custom domain name to UI App<br>az containerapp hostname add --hostname chat.jorisbaan.nl -g $RESOURCE_GROUP -n $UI<br># Configure managed certificate for HTTPS<br>az containerapp hostname bind --hostname chat.jorisbaan.nl -g $RESOURCE_GROUP -n $UI --environment $ACA_ENVIRONMENT --validation-method CNAME</pre><p>Container Apps manages a free TLS certificate for my subdomain as long as the CNAME record points directly to the container’s domain name.</p><p>The public URL for the UI changes whenever I tear down and redeploy an Environment. We could use a fancier service like <a href="https://learn.microsoft.com/en-us/azure/frontdoor/front-door-overview">Azure Front Door</a> or <a href="https://learn.microsoft.com/en-us/azure/application-gateway/overview">Application Gateway</a> to get a stable URL and act as reverse proxy with additional security, global availability, and edge caching.</p><h3>4. Resources &amp; costs overview</h3><p>Now that the app is deployed, let’s look at an overview of all the Azure resources it app uses. We created most of them ourselves, but Azure also automatically created a Load balancer, Public IP, Private DNS Zone, Network Watcher and Log Analytics workspace.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*AKBoEg78VkNUCTHImWFADw.png" /><figcaption>Screenshot of all resources from Azure Portal.</figcaption></figure><p>Some resources are free, others are free up to a certain time or compute budget, which is part of the reason I chose them. The following resources incur the highest costs:</p><ul><li><strong>Load Balancer (standard Tier)</strong>: free for 1 month, then $18/month.</li><li><strong>Container Registry (standard Tier):</strong> free for 12 months, then $19/month.</li><li><strong>PostgreSQL Flexible Server (Burstable B1MS Compute Tier)</strong>: free for 12 months, then at least $13/month.</li><li><strong>Container App</strong>: Free for 50 CPU hours/month or 2M requests/month, then $10/month for an App with a single replica, 0.5 vCPUs and 1GB memory. The LM API with 2vCPUs, 4GB memory costs about $50 per month for a single replica.</li></ul><p>You can see that the costs of this small (but scalable) app can quickly add up to hundreds of dollars per month, even without a GPU server to run a stronger language model! That’s the reason why the app probably won’t be up when you’re reading this.</p><p>It also becomes clear that Azure Container Apps is more expensive then I initially thought: it requires a standard-Tier Load balancer for automatic external ingress, HTTPS and auto-scaling. We could get around this by disabling external ingress and deploying a cheaper alternative — like a VM with a custom reverse proxy, or a basic-Tier Load balancer. Still, a standard-tier Kubernetes cluster would have cost at least $150/month, so ACA can be cheaper at small scale.</p><h3>5. Roadmap</h3><p>Now, before we wrap up, let’s look at just a few of the many directions to improve this deployment.</p><p><strong>Continuous Integration &amp; Continuous Deployment.</strong> I would set up a CI/CD pipeline that runs unit and integration tests and redeploys the app upon code changes. It might be triggered by a new git commit or merged pull request. This will also make it easier to see when a service isn’t deployed properly. I would also set up monitoring and alerting to be aware of issues quickly (like a crashing Container App instance).</p><p><strong>Lower latency: the language model server.</strong> I would load test the whole app — simulating real-world user traffic — with something like <a href="https://locust.io/">Locust</a> or <a href="https://azure.microsoft.com/en-us/products/load-testing">Azure Load Testing</a>. Even without load testing, we have an obvious bottleneck: the LM server. Small and quantized as it is, it can still take up quite a while for lengthy answers, with no concurrency. For more users it would be faster and more efficient to run a GPU inference server with a batching mechanism that collects multiple generation requests in a queue — perhaps with <a href="https://kafka.apache.org/">Kafka</a> — and runs batch inference on chunks.</p><p>With even more users, we might want several GPU-based LM servers that consume from the same queue. For GPU infrastructure I’d look into Azure Virtual Machines or something more fancy like Azure Machine Learning.</p><p>The llama.cpp inference engine is good for single-user CPU-based inference. When moving to a GPU-server, I would look into inference engines more suitable to batch inference, like <a href="https://github.com/vllm-project/vllm">vLLM</a> or <a href="https://huggingface.co/docs/text-generation-inference/en/index">Huggingface TGI</a>. And, obviously, a better (bigger) model for increased response quality — depending on the use case.</p><h3>6. Final thoughts</h3><p>I hope this project offers a glimpse of what an AI-powered web app in production may look like. I tried to balance realistic engineering with cutting about every corner to keep it simple, cheap, understandable, and limit my time and compute budget. Sadly, I cannot keep the app live for long since it would quickly cost hundreds of dollars per month. If someone can help with Azure credits to keep the app running, let me know!</p><p>Some closing thoughts about using managed services: Although Azure Container Apps abstracts away some of the Kubernetes complexity, it’s still extremely useful to have an understanding of the lower-level Kubernetes concepts. The automatically created invisible infrastructure like Public IPs, Load balancers and ingress controllers add unforeseen costs and make it difficult to understand what’s going on. Also, ACA documentation is limited compared to Kubernetes. However, if you know what you’re doing, you can set something up very quickly.</p><h3>Acknowledgements</h3><p>I heavily relied on the <a href="https://learn.microsoft.com/en-us/azure/">Azure docs</a>, and the <a href="https://learn.microsoft.com/en-us/azure/container-apps/">ACA docs</a> in particular. Thanks to Dennis Ulmer for proofreading and Lucas de Haas for useful discussion.</p><h3>AI usage</h3><p>I experimented a bit more with AI tools compared to <a href="https://towardsdatascience.com/designing-building-deploying-an-ai-chat-app-from-scratch-part-1-f1ebf5232d4d">part 1</a>. I used Pycharm’s CoPilot plugin for code completion and had quite some back-and-forth with ChatGPT to learn about the Azure or Kubernetes ecosystem, and to spar about bugs. I double-checked everything in the docs and most of the information was solid. Like part 1, I did not use AI to write this post, though I did use ChatGPT to paraphrase some bad-running sentences.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c75f712eebe5" width="1" height="1" alt=""><hr><p><a href="https://medium.com/data-science/designing-building-deploying-an-ai-chat-app-from-scratch-part-2-c75f712eebe5">Designing, Building &amp; Deploying an AI Chat App from Scratch (Part 2)</a> was originally published in <a href="https://medium.com/data-science">TDS Archive</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Designing, Building & Deploying an AI Chat App from Scratch (Part 1)]]></title>
            <link>https://medium.com/data-science/designing-building-deploying-an-ai-chat-app-from-scratch-part-1-f1ebf5232d4d?source=rss-eeb9026e13e0------2</link>
            <guid isPermaLink="false">https://medium.com/p/f1ebf5232d4d</guid>
            <category><![CDATA[hands-on-tutorials]]></category>
            <category><![CDATA[microservices]]></category>
            <category><![CDATA[cloud]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[engineering]]></category>
            <dc:creator><![CDATA[Joris Baan]]></dc:creator>
            <pubDate>Mon, 20 Jan 2025 09:03:19 GMT</pubDate>
            <atom:updated>2025-01-20T10:49:00.974Z</atom:updated>
            <content:encoded><![CDATA[<h4>Microservices Architecture and Local Development</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*NECyfs8x8EpTSIyY" /><figcaption>Photo by <a href="https://unsplash.com/@danist07?utm_source=medium&amp;utm_medium=referral">Danist Soh</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><h3>Introduction</h3><p>The aim of this project is to learn about the fundamentals of modern, scalable web applications by designing, building and deploying an AI-powered chat app from scratch. We won’t use fancy frameworks or commercial platforms like ChatGPT. This will provide a better understanding of how real-world systems may work under the hood, and give us full control over the language model, infrastructure, data and costs. The focus will be on engineering, backend and cloud deployment, rather than the language model or a fancy frontend.</p><p>This is part 1. We will design and build a cloud-native app with several APIs, a database, private network, reverse proxy, and simple user interface with sessions. Everything runs on our local computer. In <a href="https://towardsdatascience.com/designing-building-deploying-an-ai-chat-app-from-scratch-part-2-c75f712eebe5">part 2</a>, we will deploy our application to a cloud platform like AWS, GCP or Azure with a focus on scalability so actual users can reach it over the internet.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/974/1*7zbKzWipoav-lFAk1V_RyQ.gif" /><figcaption>A quick demo of the app<a href="https://chat.jorisbaan.nl.">.</a> We start a new chat, come back to that same chat, and start another chat. We will now build this app locally and make it available at localhost.</figcaption></figure><p>You can find the codebase at <a href="https://github.com/jsbaan/ai-app-from-scratch">https://github.com/jsbaan/ai-app-from-scratch</a>. Throughout this post I will link to specific lines of code with this hyperlink robot <a href="https://github.com/jsbaan/ai-app-from-scratch">🤖</a> (try it!)</p><h3>Microservices and APIs</h3><p>Modern web applications are often built using microservices — small, independent software components with a specific role. Each service runs in its own Docker container — an isolated environment independent of the underlying operating system and hardware. Services communicate with each other over a network using REST APIs.</p><p>You can think of a REST API as the interface that defines how to interact with a service by defining <em>endpoints</em> — specific URLs that represent the possible resources or actions, formatted like <a href="http://hostname">http://hostname</a>:port/endpoint-name Endpoints, also called paths or routes, are accessed with HTTP requests that can have various types like GET to retrieve data or POST to create data. Parameters can be passed in the URL itself or in the request body or header.</p><h3>Architecture</h3><p>Let’s make this more concrete. We want a web page where users can chat with a language model and come back to their previous chats. Our architecture will look like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6iu3f8YETg2GqEEe-UlKJA.png" /><figcaption>Local architecture of the app. Each service runs in its own Docker container and communicates over a private network. Made by author in draw.io.</figcaption></figure><p>The above architecture diagram shows how a user’s HTTP request to <a href="http://localhost">localhost</a> on the left flows through the system. We will discuss and set up each individual service, starting with the backend services on the right. Finally, we discuss communication, networking and container orchestration.</p><p>The structure of this post follows the components in our architecture (click to jump to the section):</p><ol><li><a href="https://towardsdatascience.com/designing-building-deploying-an-ai-chat-app-from-scratch-part-1-f1ebf5232d4d#35f8"><strong>Language model API</strong></a><strong>.</strong> A llama.cpp language model inference server running the quantized Qwen2.5–0.5B-Instruct model <a href="https://github.com/jsbaan/ai-app-from-scratch/tree/main/lm-api">🤖</a>.</li><li><a href="https://towardsdatascience.com/designing-building-deploying-an-ai-chat-app-from-scratch-part-1-f1ebf5232d4d#65d7"><strong>PostgreSQL database server</strong></a><strong>.</strong> A database that stores chats and messages <a href="https://github.com/jsbaan/ai-app-from-scratch/tree/main/db">🤖</a>.</li><li><a href="https://towardsdatascience.com/designing-building-deploying-an-ai-chat-app-from-scratch-part-1-f1ebf5232d4d#7e22"><strong>Database API</strong></a><strong>.</strong> A FastAPI and Uvicorn Python server that queries the PostgreSQL database <a href="https://github.com/jsbaan/ai-app-from-scratch/tree/main/db-api">🤖</a>.</li><li><a href="https://towardsdatascience.com/designing-building-deploying-an-ai-chat-app-from-scratch-part-1-f1ebf5232d4d#b75e"><strong>User interface</strong></a><strong>.</strong> A FastAPI and Uvicorn Python server that serves HTML and support session-based authentication <a href="https://github.com/jsbaan/ai-app-from-scratch/tree/main/chat-ui">🤖</a>.</li><li><a href="https://towardsdatascience.com/designing-building-deploying-an-ai-chat-app-from-scratch-part-1-f1ebf5232d4d#e572"><strong>Private Docker network</strong></a><strong>.</strong> For communication between microservices <a href="https://github.com/jsbaan/ai-app-from-scratch/blob/main/compose.yaml#L2">🤖</a>.</li><li><a href="https://towardsdatascience.com/designing-building-deploying-an-ai-chat-app-from-scratch-part-1-f1ebf5232d4d#80be"><strong>Nginx</strong> <strong>reverse proxy</strong></a><strong>.</strong> A gateway between the outside world and network-isolated services <a href="https://github.com/jsbaan/ai-app-from-scratch/tree/main/nginx">🤖</a>.</li><li><a href="https://towardsdatascience.com/designing-building-deploying-an-ai-chat-app-from-scratch-part-1-f1ebf5232d4d#c29f"><strong>Docker Compose</strong></a><strong>.</strong> A container orchestration tool to easily run manage our services together <a href="https://github.com/jsbaan/ai-app-from-scratch/blob/main/compose.yaml">🤖</a>.</li></ol><h3>1. Language model API</h3><p>Setting up the actual language model is pretty easy, nicely demonstrating that ML engineering is usually more about engineering than ML. Since I want our app to run on a laptop, model inference should be fast and CPU-based with low memory.</p><p>I looked at several inference engines, like <a href="https://github.com/lm-sys/FastChat/blob/main/docs/vllm_integration.md">Fastchat with vLLM</a> or <a href="https://huggingface.co/docs/text-generation-inference/en/index">Huggingface TGI</a>, but went with <a href="https://github.com/ggerganov/llama.cpp/tree/master">llama.cpp</a> because it’s popular, fast, lightweight and supports CPU-based inference. Llama.cpp is written in C/C++ and <a href="https://github.com/ggerganov/llama.cpp/blob/master/examples/server/README.md">conveniently provides a Docker image</a> with its inference engine and a simple web server that implements the popular <a href="https://github.com/openai/openai-openapi?tab=readme-ov-file">OpenAI API specification</a>. It comes with a basic UI for experimenting, but we’ll build our own UI shortly.</p><p>As for the actual language model, I chose the quantized <a href="https://huggingface.co/Qwen/Qwen2.5-0.5B-Instruct-GGUF">Qwen2.5–0.5B-Instruct</a> model from Alibaba Cloud, whose responses are surprisingly coherent given how small it is.</p><h3>1.1 Running the language model API</h3><p>The beauty of containerized applications is that, given a Docker image, we can have it running in seconds without installing any packages. The docker run command below pulls the llama.cpp server image, mounts the model file that we downloaded earlier to the container’s filesystem, and runs a container with the llama.cpp server listening for HTTP requests at port 80. It uses flash attention and has max generation length of 512 tokens.</p><pre>docker run<br>	--name lm-api \<br>	--volume $PROJECT_PATH/lm-api/gguf_models:/models \<br>	--publish 8000:80 \ # add this to make the API accessible on localhost<br>	ghcr.io/ggerganov/llama.cpp:server <br>		-m /models/qwen2-0_5b-instruct-q5_k_m.gguf --port 80 --host 0.0.0.0 --predict 512 --flash-attn</pre><p>Ultimately we will use Docker Compose to run this container together with the others <a href="https://github.com/jsbaan/ai-app-from-scratch/blob/main/compose.yaml#L52">🤖</a>.</p><h3>1.2 Accessing the container</h3><p>Since Docker containers are completely isolated from everything else on their host machine, i.e., our computer, we can’t reach our language model API yet.</p><p>However, we can break through a bit of networking isolation by publishing the container’s port 80 to our host machine’s port 8000 with --publish 8000:80 in the docker run command. This makes the llama.cpp server available at <a href="http://localhost:8000">http://localhost:8000</a>.</p><p>The hostname <a href="http://localhost">localhost</a> resolves to the loopback IP address 127.0.0.1 and is part of the loopback network interface that allows a computer to communicate with itself. When we visit <a href="http://localhost:8000">http://localhost:8000</a>, our browser sends an HTTP GET request to our own computer on port 8000, which gets forwarded to the llama.cpp container listening at port 80.</p><h3>1.3 Testing the language model API</h3><p>Let’s test the language model server by sending a POST request with a short chat history.</p><pre>curl -X POST &lt;http://localhost:8000/v1/chat/completions&gt; \<br>-H &quot;Content-Type: application/json&quot; \<br>-d &#39;{<br>	&quot;messages&quot;: [<br>    {&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: &quot;You are a helpful assistant.&quot;},<br>    {&quot;role&quot;: &quot;assistant&quot;, &quot;content&quot;: &quot;Hello, how can I assist you today?&quot;},<br>    {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;Hi, what is an API?&quot;}<br>	],<br>	&quot;max_tokens&quot;: 10<br>	}&#39;</pre><p>The response is JSON and the generated text is under choices.message.content: “An API (Application Programming Interface) is a specification…”.</p><p>Perfect! Ultimately, our UI service will be the one to send requests to the language model API, and define the system prompt and opening message <a href="https://github.com/jsbaan/ai-app-from-scratch/blob/main/chat-ui/app/main.py#L216">🤖</a>.</p><h3>2. PostgreSQL database server</h3><p>Next, let’s look into storing chats and messages. <a href="https://www.postgresql.org/">PostgreSQL</a> is a powerful, open-source relational database and running a PostgreSQL server locally is just another another docker run command using its official image. We’ll pass some extra environment variables to configure the database name, username, and password.</p><pre>docker run --name db --publish 5432:5432 --env POSTGRES_USER=myuser --env POSTGRES_PASSWORD=mypassword postgres</pre><p>After publishing port 5432, the database server is available on localhost:5432. PostgreSQL uses its own protocol for communication and doesn’t understand HTTP requests. We can use a database client like <a href="https://www.postgresql.org/docs/current/app-psql.html">psql</a> to test the connection.</p><pre>pg_isready -U joris -h localhost -d postgres<br>&gt; localhost:5432 - accepting connections</pre><p>When we deploy our application in <a href="https://medium.com/towards-data-science/designing-building-deploying-an-ai-chat-app-from-scratch-part-2-c75f712eebe5">part 2</a>, we will use a database managed by a cloud provider to make our lives easier and add more security, reliability and scalability. However, setting one up locally like this is useful for local development and, perhaps later on, integration tests.</p><h3>3. Database API</h3><p>Databases often have a separate API server sitting in front to control access, enforce extra security, and provide a simple, standardized interface that abstracts away the database’s complexity.</p><p>We will build this API from scratch with <a href="https://fastapi.tiangolo.com/">FastAPI</a>, a modern framework for building fast, production-ready Python APIs. We will run the API with <a href="https://www.uvicorn.org/">Uvicorn</a>, a high-performance Python web server that handles things like network communication and simultaneous requests.</p><h3>3.1 Quick FastAPI example</h3><p>Let’s quickly get a feeling for FastAPI and look at a minimal example app with a single GET endpoint /hello.</p><pre>from fastapi import FastAPI<br><br># FastAPI app object that the Uvicorn web server will load and serve<br>my_app = FastAPI()<br><br># Decorator telling FastAPI that function below handles GET requests to /hello<br>@my_app.get(&quot;/hello&quot;) <br>def read_hello():<br>  # Define this endpoint&#39;s response<br>    return {&quot;Hello&quot;: &quot;World&quot;}</pre><p>We can serve our app at <a href="http://localhost:8080">http://localhost:8080</a> by running the Uvicorn server.</p><pre>uvicorn main.py:my_app --host 0.0.0.0 --port 8080</pre><p>If we now send a GET request to our endpoint by visiting <a href="http://localhost:8080/hello">http://localhost:8080/hello</a> in our browser, we receive the JSON response {&quot;Hello&quot;: &quot;World&quot;} !</p><h3>3.2 Connecting to the database</h3><p>On to the actual database API. We define four endpoints in <a href="https://github.com/jsbaan/ai-app-from-scratch/blob/main/db-api/app/main.py">main.py 🤖</a> for creating or fetching chats and messages. You get a nice visual summary of these in the auto-generated docs, see below. The UI will call these endpoints to process user data.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*2ngYJsEMhc8jPdU8rX0t1A.png" /><figcaption>A cool feature of FastAPI is that it automatically generates interactive documentation according to the <a href="https://www.openapis.org/">OpenAPI specification</a> with <a href="https://swagger.io/">Swagger</a>. If the Uvicorn server is running we can find it at <a href="http://hostname:port/docs.">http://hostname:port/docs.</a> This is a screenshot of the doc web page.</figcaption></figure><p>The first thing we need to do is to connect the database API to the database server. We use <a href="https://docs.sqlalchemy.org/en/13/orm/tutorial.html">SQLAlchemy</a>, a popular Python SQL toolkit and Object-Relational Mapper (ORM) that abstracts away writing manual SQL queries.</p><p>We establish this connection in <a href="https://github.com/jsbaan/ai-app-from-scratch/blob/main/db-api/app/database.py">database.py 🤖</a> by creating the SQLAlchemy engine with a connection URL that includes the database hostname, username and password (remember, we configured these by passing them as environment variables to the PostgreSQL server). We also create a session factory that creates a new database session for each request to the database API.</p><h3>3.3 Interacting with the database</h3><p>Now let’s design our database. We define two SQLAlchemy data models in <a href="https://github.com/jsbaan/ai-app-from-scratch/blob/main/db-api/app/models.py">models.py 🤖</a> that will be mapped to actual database tables. The first is a <a href="https://github.com/jsbaan/ai-app-from-scratch/blob/main/db-api/app/models.py#L43">Message model 🤖</a> with an id, content, speaker role, owner_id, and session_id (more on this later). The second is a <a href="https://github.com/jsbaan/ai-app-from-scratch/blob/main/db-api/app/models.py#L21">Chat model 🤖</a>, which I’ll show here to get a better feeling for SQLAlchemy models:</p><pre>class Chat(Base):<br>  __tablename__ = &quot;chats&quot;<br><br>  # Unique identifier for each chat that will be generated automatically.<br>  id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)<br><br>  # Username associated with the chat. Index is created for faster lookups.<br>  username = Column(String, index=True)<br><br>  # Session ID associated with the chat. <br>  # Used to &quot;scope&quot; chats, i.e., users can only access chats from their session.<br>  session_id = Column(String, index=True)<br><br>  # The relationship function links the Chat model to the Message model.<br>  # The back_populates flag creates a bidirectional relationship.<br>  messages = relationship(&quot;Message&quot;, back_populates=&quot;owner&quot;)</pre><p>Database tables are typically created using migration tools like Alembic, but we’ll simply ask SQLAlchemy to create them in <a href="https://github.com/jsbaan/ai-app-from-scratch/blob/main/db-api/app/main.py#L19">main.py 🤖</a>.</p><p>Next, we define the CRUD (Create, Read, Update, Delete) methods in <a href="https://github.com/jsbaan/ai-app-from-scratch/blob/main/db-api/app/crud.py">crud.py 🤖</a>. These methods use a fresh database session from our factory to query the database and create new rows in our tables. The endpoints in <a href="http://main.py">main.py</a> will import and use these CRUD methods <a href="https://github.com/jsbaan/ai-app-from-scratch/blob/main/db-api/app/main.py#L56">🤖</a>.</p><h3>3.4 Endpoint request and response validation</h3><p>FastAPI is heavily based on <a href="https://fastapi.tiangolo.com/python-types/">Python’s type annotations</a> and the data validation library <a href="https://docs.pydantic.dev/latest/">Pydantic</a>. For each endpoint, we can define a request and response schema that defines the input/output format we expect. Each request to or response from an endpoint is automatically validated and converted to the right data type and included in our API’s automatically generated documentation. If something about a request or response is missing or wrong, an informative error is thrown.</p><p>We define the Pydantic schemas for the database-api in <a href="https://github.com/jsbaan/ai-app-from-scratch/blob/main/db-api/app/schemas.py">schemas.py 🤖</a> and use them in the endpoint definitions in <a href="https://github.com/jsbaan/ai-app-from-scratch/blob/main/db-api/app/main.py#L45">main.py 🤖</a>, too. For example, this is the endpoint to create a new chat:</p><pre>@app.post(&quot;/chats&quot;, response_model=schemas.Chat)<br>async def create_chat(chat: schemas.ChatCreate, db: Session = Depends(get_db)):<br>    db_chat = crud.create_chat(db, chat)<br>    return db_chat</pre><p>We can see that it expects a ChatCreate request body and Chat response body. FastAPI verifies and converts the request and response bodies according to these schemas <a href="https://github.com/jsbaan/ai-app-from-scratch/blob/main/db-api/app/schemas.py">🤖</a>:</p><pre>class ChatCreate(BaseModel):<br>    username: str<br>    messages: List[MessageCreate] = []<br>    session_id: str<br><br>class Chat(ChatCreate):<br>    id: UUID<br>    messages: List[Message] = []</pre><p>Note: our SQLAlchemy models for the database should not be confused with these Pydantic schemas for endpoint input/output validation.</p><h3>3.5 Running the database API</h3><p>We can serve the database API using Uvicorn, making it available at <a href="http://localhost:8001">http://localhost:8001</a>.</p><pre>cd $PROJECT_PATH/db-api<br>uvicorn app.main.py:app --host 0.0.0.0 --port 8001</pre><p>To run the Uvicorn server in its own Docker container, we create a <a href="https://github.com/jsbaan/ai-app-from-scratch/blob/main/db-api/Dockerfile">Dockerfile 🤖</a> that specifies how to incrementally build the Docker image. We can then build the image and run the container, again making the database API available at <a href="http://localhost:8001">http://localhost:8001</a> after publishing the container’s port 80 to host port 8001. We pass the database credentials and hostname as environment variables.</p><pre>docker build --tag db-api-image $PROJECT_PATH/db-api<br>docker run --name db-api --publish 8001:80 --env POSTGRES_USERNAME=&lt;username&gt; --env POSTGRES_PASSWORD=&lt;password&gt; --env POSTGRES_HOST=&lt;hostname&gt; db-api-image</pre><h3>4. User interface</h3><p>With the backend in place, let’s build the frontend. A web interface typically consists of HTML for structure, CSS for styling and Javascript for interactivity. Frameworks like React, Vue, and Angular use higher-level abstractions like JSX that can ultimately be transformed into HTML, CSS, and JS files to be bundled and served by a web server like Nginx.</p><p>Since I want to focus on the backend, I hacked together a simple UI with FastAPI. Instead of JSON responses, its endpoints now return HTML based on template files that are rendered by <a href="https://jinja.palletsprojects.com/en/stable/">Jinja</a>, a templating engine that replaces variables in the template with real data like chat messages.</p><p>To handle user input and interact with the backend (e.g., retrieve chat history from the database API or generate a reply via the language model API), I’ve avoided JavaScript altogether by using <a href="https://github.com/jsbaan/ai-app-from-scratch/blob/main/chat-ui/app/templates/home.html#L15">HTML forms 🤖</a> that trigger internal POST endpoints. These endpoints then simply use Python’s <a href="https://www.python-httpx.org/">httpx</a> library to make HTTP requests <a href="https://github.com/jsbaan/ai-app-from-scratch/blob/main/chat-ui/app/main.py#L107">🤖</a>.</p><p>Endpoints are defined in <a href="https://github.com/jsbaan/ai-app-from-scratch/blob/main/chat-ui/app/main.py">main.py 🤖</a>, HTML templates are in the <a href="https://github.com/jsbaan/ai-app-from-scratch/tree/main/chat-ui/app/templates">app/templates directory 🤖</a>, and the static CSS file for styling the pages is in the <a href="https://github.com/jsbaan/ai-app-from-scratch/tree/main/chat-ui/app/static">app/static directory 🤖</a>. FastAPI serves the CSS file at <a href="https://hostname/static/style.css">http://hostname/static/style.css</a> so the browser can find it.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*T2FTr0Rva0DnINjbTh0Ckw.png" /><figcaption>Screenshot of the UI’s interactive documentation.</figcaption></figure><h3>4.1 Homepage</h3><p>The homepage allows users to enter their name to start or return to a chat <a href="https://github.com/jsbaan/ai-app-from-scratch/blob/main/chat-ui/app/main.py#L76">🤖</a>. The submit button triggers a POST request to the internal /chats endpoint with username as form parameter, which calls the database API to create a new chat and then redirects to the <a href="https://github.com/jsbaan/ai-app-from-scratch/blob/main/chat-ui/app/main.py#L89">Chat Page 🤖</a>.</p><h3>4.2 Chat Page</h3><p>The chat page calls the database API to retrieve the chat history <a href="https://github.com/jsbaan/ai-app-from-scratch/blob/main/chat-ui/app/main.py#L143">🤖</a>. Users can then enter a message that triggers a POST request to the internal /generate/{chat_id} endpoint with the message as form parameter <a href="https://github.com/jsbaan/ai-app-from-scratch/blob/main/chat-ui/app/main.py#L184">🤖</a>.</p><p>The generate endpoint calls the database API to add the user’s message to the chat history, and then the language model API with the full chat history to generate a reply <a href="https://github.com/jsbaan/ai-app-from-scratch/blob/main/chat-ui/app/main.py#L216">🤖</a>. After adding the reply to the chat history, the endpoint redirects to the chat page, that again retrieves and displays the latest chat history. We send POST request to the LM API using httpx, but we could use a more standardized LM API package like langchain to invoke its completion endpoint.</p><h3>4.3 Authentication &amp; user sessions</h3><p>So far, all users can access all endpoints and all data. This means anyone can see your chat given your username or chat id. To remedy that, we will use session-based authentication and authorization.</p><p>We will store a first party &amp; GDRP compliant signed session cookie in the user’s browser <a href="https://github.com/jsbaan/ai-app-from-scratch/blob/main/chat-ui/app/main.py#L61">🤖</a>. This is just an encrypted dict-like object in the request/response header. The user’s browser will send that session cookie with each request to our hostname, so that we can identify and verify a user user and show them their own chats only.</p><p>As an extra layer of security, we “scope” the database API such that each chat row and each message row in the database contains a session id. For each request to the database API, we include the current user’s session id in the request header and query the database with both the chat id (or username) AND that session id. This way, the database can only ever return chats for the current user with its unique session id <a href="https://github.com/jsbaan/ai-app-from-scratch/blob/main/db-api/app/crud.py#L65">🤖</a>.</p><h3>4.4 Running the UI</h3><p>To run the UI in a Docker container, we follow the same recipe as the database API, adding the hostnames of the database API and language model API as environment variables.</p><pre>docker build --tag chat-ui-image $PROJECT_PATH/chat-ui<br>docker run --name chat-ui --publish 8002:80 --env LM_API_URL=&lt;hostname1&gt; --env DB_API_URL=&lt;hostname2&gt; chat-ui-image</pre><p>How do we know the hostnames of the two APIs? We will look networking and communication next.</p><h3>5. Private Docker network</h3><p>Let’s zoom out and take a look at our architecture again. By now, we have four containers: the UI, DB API, LM API, and PostgreSQL database. What’s missing is the network, reverse proxy and container orchestration.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6iu3f8YETg2GqEEe-UlKJA.png" /><figcaption>Our app’s microservices architecture. Made by author in draw.io.</figcaption></figure><p>Until now, we used our computer’s <a href="http://localhost">localhost</a> loopback network to send requests to an individual container. This was possible because we published their ports to our localhost. However, for containers to communicate with each other, they must be connected to the same network and know each others hostname/IP address and port.</p><p>We will create a <a href="https://docs.docker.com/network/drivers/bridge/#differences-between-user-defined-bridges-and-the-default-bridge">user-defined bridge Docker network</a> that provides automatic DNS resolution. This means that container names are resolved to their dynamic container’s IP address. The network also provides isolation, and therefore security: you have to be on the same network to be able to reach our containers.</p><pre>docker network create --driver bridge chat-net</pre><p>We connect all containers to it by adding --network chat-net to their docker run command. Now, the database API can reach the database at db:5432 and the UI can reach the database API at <a href="http://db-api">http://db-api</a> and the language model API at <a href="http://lm-api">http://lm-api</a>. Port 80 is default for HTTP requests so we can omit it.</p><h3>6. Nginx reverse proxy</h3><p>Now, how do we — the user — reach our network-isolated containers? During development we published the UI container port to our <a href="http://localhost">localhost</a>, but in a realistic scenario you typically use a reverse proxy. This is another web server that acts like a gateway, forwarding HTTP requests to containers in their private network, enforcing security and isolation.</p><p><a href="https://nginx.org/en/">Nginx</a> is a web server often used as reverse proxy. We can easily run it using its official <a href="https://hub.docker.com/_/nginx">Docker image</a>. We also mount a <a href="https://github.com/jsbaan/ai-app-from-scratch/blob/main/nginx/nginx.conf">configuration file 🤖</a> in which we specify how Nginx should route incoming requests. As an example, the simplest possible configuration forwards all requests (location / ) from Nginx container’s port 80 to the UI container at <a href="http://chat-ui">http://chat-ui</a>.</p><pre>http { server { listen 80; location / { proxy_pass http://chat-ui } } }</pre><p>Since the Nginx container is in same private network, we can’t reach it either. However, we can publish its port so it becomes the only access point of our entire app <a href="https://github.com/jsbaan/ai-app-from-scratch/blob/main/compose.yaml#L64">🤖</a>. A request to <a href="http://localhost">localhost</a> now goes to the Nginx container, who forwards it to the UI and the UI’s response back to us.</p><pre>docker run --network chat-net --publish 80:80 --volume $PROJECT_PATH/nginx.conf:/etc/nginx/nginx.conf nginx</pre><p>In <a href="https://medium.com/towards-data-science/designing-building-deploying-an-ai-chat-app-from-scratch-part-2-c75f712eebe5">part 2</a> we will see that these gateway servers can also distribute incoming requests over copies of the same containers (load balancing) for scalability; enable secure HTTPS traffic; and do advanced routing and caching. We will use an Azure-managed reverse proxy rather this Nginx container, but I think it’s very useful to understand how they work and how to set one up yourself. It can also be significantly cheaper compared to a managed reverse proxy.</p><h3>7. Docker Compose</h3><p>Let’s put everything together. Throughout this post we manually pulled or built each image and ran its container. However, in the codebase I’m actually using <a href="https://docs.docker.com/compose/">Docker Compose</a>: a tool designed to define, run and stop multi-container applications on a single host like our computer.</p><p>To use Docker Compose, we simply specify a <a href="https://github.com/jsbaan/ai-app-from-scratch/blob/main/compose.yaml">compose.yml file 🤖</a> with build and run instructions for each service. A cool feature is that it automatically creates a user-defined bridge network to connect our services. Docker DNS will resolve the service names to container IP addresses.</p><p>Inside the project directory we can start all services with a single command:</p><pre>docker compose up --build</pre><h3>Final thoughts</h3><p>That wraps it up! We built an AI-powered chat web application that runs on our local computer, learning about microservices, REST APIs, FastAPI, Docker (Compose), reverse proxies, PostgreSQL databases, SQLAlchemy, and llama.cpp. We’ve built it with a cloud-native architecture in mind so we can deploy our app without changing a single line of code.</p><p>We will discuss deployment in <a href="https://medium.com/towards-data-science/designing-building-deploying-an-ai-chat-app-from-scratch-part-2-c75f712eebe5">part 2</a> and cover Kubernetes, the industry-standard container orchestration tool for large-scale applications across multiple hosts; Azure Container Apps, a serverless platform that abstracts away some of Kubernetes’ complexities; and concepts like load balancing, horizontal scaling, HTTPS, etc.</p><h3>Roadmap</h3><p>There is a lot we could do to improve this app. Here are some things I would work on given more time.</p><p><strong>Language model.</strong> We now use a very general instruction-tuned language model as virtual assistant. I originally started this project to have a “virtual representation of me” on my website for visitors to discuss my research with, based on my scientific publications. For such a use case, an important direction is to improve and tweak the language model output. Perhaps that’ll become a part 3 of this series in the future.</p><p><strong>Frontend.</strong> Instead of a quick FastAPI UI, I’d build a proper frontend using something like React, Angular or Vue to allow things like steaming LM responses and dynamic views rather than reloading the page every time. A more lightweight alternative that I’d like to experiment with is htmx, a library that provides modern browser features directly from HTML rather than javascript. It would be pretty straightforward to implement LM response streaming, for example.</p><p><strong>Reliability.</strong> To make the system more mature, I’d add unit and integration tests and a better database setup like Alembic allowing for migrations.</p><h3>Acknowledgements</h3><p>Thanks to Dennis Ulmer, Bryan Eikema and David Stap for initial feedback or proofreading.</p><h3>AI usage</h3><p>I used Pycharm’s CoPilot plugin for code completion, and ChatGPT for a first version of the HTML and CSS template files. Towards the end, I started experimenting more with debugging and sparring too, which proved surprisingly useful. For example, I used it to learn about Nginx configurations and session cookies in FastAPI. I did not use AI to write this post, though I did use ChatGPT to paraphrase a few bad-running sentences.</p><p>Here are some additional resources that I found useful during this project.</p><ul><li><a href="https://www.youtube.com/watch?v=3vfum74ggHE">Django vs Flask vs FastAPI YouTube video by Patrick Loeber</a>.</li><li>The <a href="https://fastapi.tiangolo.com/tutorial/">FastAPI tutorial</a> was by far most helpful.</li><li><a href="https://docs.docker.com/">Docker documentation</a>, especially the <a href="https://docs.docker.com/network/">networking overview</a>, <a href="https://docs.docker.com/network/drivers/bridge/">bridge network intro</a>, <a href="https://docs.docker.com/compose/gettingstarted/">docker-compose quickstart</a> and <a href="https://docs.docker.com/compose/networking/">networks in docker-compose</a>.</li><li><a href="https://docs.sqlalchemy.org/en/13/orm/tutorial.html">SQLAlchemy docs and their Object-Relational Mapper (ORM) tutorial</a>.</li><li><a href="https://www.postgresqltutorial.com/postgresql-cheat-sheet/">PostgreSQL Cheat Sheet</a>.</li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f1ebf5232d4d" width="1" height="1" alt=""><hr><p><a href="https://medium.com/data-science/designing-building-deploying-an-ai-chat-app-from-scratch-part-1-f1ebf5232d4d">Designing, Building &amp; Deploying an AI Chat App from Scratch (Part 1)</a> was originally published in <a href="https://medium.com/data-science">TDS Archive</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Implementing a Transformer From Scratch]]></title>
            <link>https://medium.com/data-science/7-things-you-didnt-know-about-the-transformer-a70d93ced6b2?source=rss-eeb9026e13e0------2</link>
            <guid isPermaLink="false">https://medium.com/p/a70d93ced6b2</guid>
            <category><![CDATA[coding]]></category>
            <category><![CDATA[transformers]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[nlp]]></category>
            <category><![CDATA[deep-learning]]></category>
            <dc:creator><![CDATA[Joris Baan]]></dc:creator>
            <pubDate>Wed, 23 Mar 2022 16:37:00 GMT</pubDate>
            <atom:updated>2022-03-24T20:29:41.382Z</atom:updated>
            <content:encoded><![CDATA[<h3>Implementing a Transformer from Scratch</h3><h4>7 surprising things you might not know about the Transformer</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*b6KZpAgND0FyX3qK" /><figcaption>Photo by <a href="https://unsplash.com/@silasbaisch?utm_source=medium&amp;utm_medium=referral">Silas Baisch</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><h3>Introduction</h3><p>To get intimately familiar with the nuts and bolts of transformers I decided to implement the original architecture in the “<a href="https://papers.nips.cc/paper/2017/file/3f5ee243547dee91fbd053c1c4a845aa-Paper.pdf">Attention is all you need</a>” paper from scratch. I thought I knew everything there was to know, but to my own surprise, I encountered several unexpected implementation details that made me better understand how everything works under the hood.</p><p>The goal of this post is not discuss the entire implementation — there are plenty of great resources for that — but to <strong>highlight</strong> <strong>seven things that I found particularly surprising or insightful, and that you might not know about</strong>. I will make this concrete by pointing to specific lines in my <a href="https://github.com/jsbaan/transformer-from-scratch">code</a> using this hyperlink robot <a href="https://github.com/jsbaan/transformer-from-scratch">🤖</a> (try it!). The code should be easy to understand: it’s well documented and automatically unit tested and type checked using Github Actions.</p><p>The structure of this post is simple. The first three points revolve around implementing multi-head attention; the last four are about other components. I assume you have conceptual familiarity with the transformer and multi-head attention (if not; a great starting point is the <a href="https://jalammar.github.io/illustrated-transformer/">Illustrated Transformer</a>) and kick off with something that helped me tremendously in better understanding the mechanics behind multi-head attention. It’s also the most involved point.</p><pre><strong>Table of Contents</strong></pre><pre><a href="#29a2">Introduction</a><br>1. <a href="#81ba">Multi-head attention is implemented with one weight matrix</a><br>2. <a href="#e7de">The dimensionality of the key, query and value vectors is not a hyperparameter</a><br>3. <a href="#5d70">Scaling in dot-product attention avoids extremely small gradients</a><br>4. <a href="#1f65">The source embedding, target embedding AND pre-softmax linear share the same weight matrix</a><br>5. <a href="#0c55">During inference we perform a decoder forward pass for every token; during training we perform a single forward pass for the entire sequence</a><br>6. <a href="#f5d8">Transformers can handle arbitrarily long sequences, in theory</a><br>7. <a href="#7ea0">Transformers are residual streams</a><br><a href="#2830">Wrapping up</a></pre><h3><strong>1. Multi-head attention is implemented with one weight matrix</strong></h3><p>Before we dive into it; recall that for each attention head <strong>we need a query, key and value vector for each input token</strong>. We then compute <strong>attention scores</strong> as the softmax over the <strong>scaled dot product</strong> between one query and all key vectors in the sentence (<a href="https://github.com/jsbaan/transformer-from-scratch/blob/main/multi_head_attention.py#L170">🤖</a>). The equation below computes an <strong>attention-weighted</strong> average over all value vectors for each input token at once. <strong>Q</strong> is the matrix that stacks <strong>q</strong>uery vectors for all input tokens; <strong>K</strong> and <strong>V</strong> do the same for <strong>k</strong>ey and <strong>v</strong>alue vectors.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*445F8KQUl-i7pXws7vCZ1Q.png" /></figure><p>How, then, do we obtain these query, key and value vectors <strong>efficiently for all tokens and heads</strong>? It turns out we can do this <strong>in one go using a single weight matrix </strong>W. This is different from the <strong>three </strong>projection weight matrices one might expect after reading <a href="https://papers.nips.cc/paper/2017/file/3f5ee243547dee91fbd053c1c4a845aa-Paper.pdf">the paper </a>or the <a href="https://jalammar.github.io/illustrated-transformer/">Illustrated Transformer</a>. Let’s walk through how this works.</p><p>Let’s say that our input consists of 4 tokens: [&quot;hey&quot;, &quot;how&quot;, &quot;are&quot;, &quot;you&quot;], and our embedding size is 512. Ignoring batches for now, let X be the 4x512 matrix stacking token embeddings as rows.</p><p>Let W be a weight matrix with 512 rows and 1536 columns. We will now zoom in on this 512x1536 dimensional weight matrix W (<a href="https://github.com/jsbaan/transformer-from-scratch/blob/main/multi_head_attention.py#L20"><strong>🤖</strong></a>) to find out <strong>why</strong> we need 1536 dimensions and <strong>how</strong> multiplying it with X results in a matrix P (for <strong>p</strong>rojections) that contains all the query, key and value vectors we need. (In the code <a href="https://github.com/jsbaan/transformer-from-scratch/blob/main/multi_head_attention.py#L94">🤖</a> I call this matrix qkv)</p><h4>The Matrix Multiplication Behind Multi-head Attention</h4><p>Each element in the resulting 4x1536 matrix P=X@W is<strong> the sum of the element-wise product</strong> (in other words: the dot product) <strong>between a row vector</strong> <strong>in </strong>X (an embedding) <strong>and a column vector</strong> <strong>in </strong>W (some weights).</p><p>As a refresher on matrix multiplication, the image below visualizes how to compute the first element of a simple 3x3 matrix when multiplying two 3x3 matrices. The same strategy applies to our bigger matrices X and W.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Sei83Vn1PsVSSrlbYVdAWQ.png" /><figcaption>Example: how to compute the first element in 3x3 matrix multiplication. Image by author and Rosalie Gubbels.</figcaption></figure><p>So, each <strong>element</strong> in the ith row P[i, :] in our projection matrix is a <strong>linear combination</strong> of the ith <strong>token</strong> <strong>embedding</strong> X[i, :] and one of the <strong>weight columns</strong> in W. This means we can simply stack more columns in our weight matrix W to create more independent linear combinations (scalars) of each token embedding in X. In other words, each element in P is a different scalar &quot;<strong>view</strong>&quot; or &quot;<strong>summary</strong>&quot; of a token embedding in X, weighted by a column in W. This is key in understanding how eight “heads” with “query”, “key” and “value” vectors <strong>hide</strong> within each of P&#39;s rows.</p><h4>Uncovering attention heads and query, key and value vectors</h4><p>We can <strong>decompose</strong> the 1536 columns that we have chosen for W (and end up as the number of columns in P) into 1536 = 8 * 3 * 64. <strong>We now uncovered eight heads with each three 64-dimensional vectors hiding in every row in</strong> P! Each such “vector” or “chunk” consists of 64 different weighted linear combination of a token embedding and we <strong>choose</strong> <strong>to interpret</strong> them in a certain way. You can see a visual representation of P and how to decompose it in the image below. The decomposition also happens in code (<a href="https://github.com/jsbaan/transformer-from-scratch/blob/main/multi_head_attention.py#L96">🤖</a>).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*pxY5DVJgizXV8PtjUnuEEA.png" /><figcaption>P=X@W contains the query, key and value projections for all heads. Image by author and Rosalie Gubbels.</figcaption></figure><p>For multiple sentences in a <strong>batch</strong>, simply imagine a third dimension “behind” P that turns the 2D matrix into 3D matrix.</p><h4>Encoder-decoder attention</h4><p>For encoder-decoder attention this is slightly more involved. Recall that encoder-decoder attention allows each decoder to attend to the embeddings outputted by the topmost encoder</p><p>For encoder-decoder attention, we need <strong>query vectors</strong> for the <strong>decoder</strong> token embeddings, and <strong>key and value vectors</strong> for the topmost <strong>encoder</strong> token embeddings. That’s why we split W into two — a 512x512 and a 512x1024 matrix (<a href="https://github.com/jsbaan/transformer-from-scratch/blob/main/multi_head_attention.py#L122">🤖</a>) — and perform two separate projections: one to obtain the key and value vectors from the encoder’s embeddings (<a href="https://github.com/jsbaan/transformer-from-scratch/blob/main/multi_head_attention.py#L125">🤖</a>), and one to obtain query vectors for the decoder’s embeddings (<a href="https://github.com/jsbaan/transformer-from-scratch/blob/main/multi_head_attention.py#L132">🤖</a>).</p><p>Finally, note that we do need <strong>a second weight matrix</strong> (<a href="https://github.com/jsbaan/transformer-from-scratch/blob/main/multi_head_attention.py#L21">🤖</a>) in multi-head attention to mix the value vectors from each head and obtain a single contextual embedding per token (<a href="https://github.com/jsbaan/transformer-from-scratch/blob/main/multi_head_attention.py#L76">🤖</a>).</p><h3>2. The dimensionality of the key, query and value vectors is <strong>not a hyperparameter</strong></h3><p>I never really thought about this, but I always assumed that the dimensionality of the query, key and value vectors was a hyperparameter. As it turns out, <strong>it is dynamically set to the number of embedding dimensions divided by the number of heads</strong>: qkv_dim = embed_dim/num_heads = 512/8 = 64(<a href="https://github.com/jsbaan/transformer-from-scratch/blob/main/multi_head_attention.py#L16">🤖</a>).</p><p>This seems like a design choice by Vaswani et al. to keep the number of parameters in multi-head attention constant, regardless of the number of heads one chooses. <strong>While you might expect the number of parameters to grow with more heads, what actually happens is that the dimensionality of the query, key and value vectors decreases.</strong></p><p>If we look at the figure above that shows R=X@W and imagine single-head attention, this becomes clear. The number of elements in X, W, and R remain the same as with eight heads, but the way we <strong>interpret</strong> the elements in R<strong> </strong>changes<strong>.</strong> With a single head, we have just one query, key and value projection per token embedding (a row in P) and they would span one third of each row: 512 elements — the same as the embedding size.</p><p>What’s the point of multiple heads then, you might wonder? Well, Vaswani et al. argue that it allows heads to capture different “representation subspaces”. For example; one head might track syntactic relations, while another focuses more on positional information. There’s quite some work that investigates whether this indeed happens in practice, e.g. in <a href="https://aclanthology.org/P19-1580.pdf">translation</a>. In fact, I did some work on this myself a few years ago in <a href="https://arxiv.org/pdf/1911.03898.pdf">summarization</a>.</p><h3><strong>3. Scaling in </strong>dot-product attention avoids extremely small gradients</h3><p>Similar to the previous point, I never really thought about <strong>why</strong> we divide attention logits by some constant (<a href="https://github.com/jsbaan/transformer-from-scratch/blob/main/multi_head_attention.py#L173">🤖</a>) but it’s actually pretty straightforward.</p><p>Recall that each logit is the result of a dot product (i.e. sum over the element-wise product) between a query and a key vector. A higher number of dimensions qkv_dim thus results in more products in that sum — causing <strong>higher variance</strong> in attention logits. As we can see in the examples below, a softmax transformation on logits with <strong>high variance</strong> results in <strong>extremely small output probabilities</strong> — and therefore <strong>tiny gradients</strong>.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/5d56355b41375488b463cabd48492a8b/href">https://medium.com/media/5d56355b41375488b463cabd48492a8b/href</a></iframe><h3>4. The source embedding, target embedding AND pre-softmax linear share the same weight matrix</h3><p>We now move away from multi head attention and dive into “<strong>weight tying</strong>” — common practice in sequence to sequence models. I find this quite interesting because embedding weight matrices actually make up for a<strong> huge</strong> number of parameters relative to the rest of the model. Given a vocabulary of 30k tokens and an embedding size of 512, this matrix contains <strong>15.3 million</strong> parameters!</p><p>Imagine having <strong>three</strong> such matrices: one that maps source token indices to embeddings, one that maps target tokens to embeddings, and one that maps each of the decoder’s topmost contextualized token embeddings to logits over the target vocabulary (the pre-softmax linear layer). Yeah; that leaves us with <strong>46 million</strong> parameters.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/856/1*mFLEJIKQhQkNNZ0JFINfIw.png" /><figcaption>Weight tying: the three red blocks share the same weight matrix. Image by author and Rosalie Gubbels.</figcaption></figure><p>In the code you can see that I initialize <strong>one</strong> embedding layer in the main transformer class (<a href="https://github.com/jsbaan/transformer-from-scratch/blob/main/transformer.py#L33">🤖</a>) that I use as encoder embedding (�<a href="https://github.com/jsbaan/transformer-from-scratch/blob/main/encoder.py#L24">�</a>), decoder embedding (<a href="https://github.com/jsbaan/transformer-from-scratch/blob/main/decoder.py#L31">🤖</a>) and decoder pre-softmax transformation weights (<a href="https://github.com/jsbaan/transformer-from-scratch/blob/main/decoder.py#L44">🤖</a>).</p><h3>5. During inference we perform a decoder forward pass for every token; during training we perform a single forward pass for the entire sequence</h3><p>This one might be obvious to some — especially those working on sequence to sequence tasks — but crucial to understanding how a transformer is actually trained.</p><h4>Inference</h4><p>Let’s say that we are in <strong>inference mode</strong>, in which we autoregressively (one by one) predict target tokens. The transformer always outputs <strong>a distribution over the vocabulary for each token in the batch</strong>. The next token is predicted based on the <strong>output distribution of</strong> <strong>the last token index</strong> in the batch (<a href="https://github.com/jsbaan/transformer-from-scratch/blob/main/transformer.py#L112">🤖</a>). This means that we basically <strong>throw away</strong> all the output distributions for all the previous indices.</p><h4>Training and Teacher Forcing</h4><p>This contrasts <strong>training</strong> in which we use <strong>teacher forcing</strong>. During training, we perform just <strong>one </strong>forward pass through the decoder, <strong>regardless of the sequence length</strong> (<a href="https://github.com/jsbaan/transformer-from-scratch/blob/main/train.py#L46">🤖</a>). We (the <strong>teacher)</strong> <strong>force</strong>-feed the entire batch of <strong>ground-truth target sequences, </strong>at once. This gives us <strong>all</strong> next-token predictions at once, for which we compute the <strong>average</strong> loss.</p><p>Note that <strong>each token prediction is based on previous ground-truth tokens and not previously predicted tokens! </strong>Note also that this single forward pass is equivalent to autoregressive decoding using only ground-truth tokens as input and ignoring previous predictions (!), but much more efficient. We use an <strong>attention</strong> <strong>mask</strong> to restrict the decoder self-attention module to attend to future tokens (the labels) and cheat.</p><p>I think it’s useful to realize that this way of training, called teacher forcing, is applied not only to translation models, but also to most popular pre-trained autoregressive language models like GPT-3.</p><h3>6. Transformers can handle arbitrarily long sequences, in theory…</h3><p>…in practice, however, multi-head attention has <a href="https://ai.googleblog.com/2021/03/constructing-transformers-for-longer.html#:~:text=With commonly available current hardware,summarization or genome fragment classification">compute and memory requirements</a> that limit the sequence length to around 512 tokens. Models like BERT do, in fact, impose a hard limit on the input sequence length because they use learned embeddings instead of the sinusoid encoding. These learned positional<strong> </strong>embeddings are similar to token embeddings, and similarly work only for a pre-defined set of positions up to some number (e.g. 512 for BERT).</p><h3>7. Transformers are residual streams</h3><p>On to the final point. I like to think of a transformer as multiple “residual streams”. This is similar to how an LSTM keeps a left-to-right, horizontal “memory stream” while processing new tokens one by one, and regulates information flow with gates.</p><p>In a transformer, this stream doesn’t run <strong>horizontally</strong> <strong>across tokens</strong>, but <strong>vertically</strong> <strong>across layers</strong> (e.g. encoders) and sub-layers (i.e. multi-head attention and fully connected layers). Each sub-layer simply adds information to the residual stream using residual connections. <a href="https://transformer-circuits.pub/2021/framework/index.html">This</a> is an awesome blogpost that discusses residual streams in more detail, and <a href="https://openreview.net/pdf?id=HyzdRiR9Y7">this</a> is a cool paper that exploits the notion of “vertical recurrence”.</p><p>A consequence of this residual stream is that <strong>the number of dimensions of intermediate token representations must be the same throughout all (sub-)layers</strong>, because the residual connections <strong>add</strong> <strong>two vectors</strong> (example in the encoder: <a href="https://github.com/jsbaan/transformer-from-scratch/blob/main/encoder.py#L90">🤖</a>). On top of that, because the encoder (and similarly decoder) layers are stacked on top of each other, their output shape must match their input shape.</p><h3>Wrapping up…</h3><p>Thank you for reading this post! Let me know if you liked it, have questions, or spotted an error. You can message me on <a href="https://twitter.com/jsbaan">Twitter</a> or connect on <a href="https://www.linkedin.com/in/joris-baan-669324b3/">LinkedIn</a>. Check out my other blogposts at <a href="http://jorisbaan.nl/posts">jorisbaan.nl/posts</a>.</p><p>Even though I wrote the <a href="https://github.com/jsbaan/transformer-from-scratch">code</a> to be easily understandable (it’s well documented, unit tested and type checked) please do use the official PyTorch implementation in practice 😁.</p><p>Thanks to David Stap for the idea to implement a transformer from scratch, Dennis Ulmer and Elisa Bassignana for feedback on this post, Lucas de Haas for a bug-hunting session and Rosalie Gubbels for creating the visuals. Some great resources I looked at for inspiration are the PyTorch transformer <a href="https://pytorch.org/tutorials/beginner/transformer_tutorial.html">tutorial</a> and <a href="https://github.com/pytorch/pytorch/blob/master/torch/nn/modules/transformer.py">implementation</a>; Phillip Lippe’s transformer <a href="https://uvadlc-notebooks.readthedocs.io/en/latest/tutorial_notebooks/tutorial6/Transformers_and_MHAttention.html">tutorial</a>; Alexander Rush’s <a href="https://nlp.seas.harvard.edu/2018/04/03/attention.html">Annotated Transformer</a> and Jay Alammar’s <a href="https://jalammar.github.io/illustrated-transformer/">Illustrated Transformer</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a70d93ced6b2" width="1" height="1" alt=""><hr><p><a href="https://medium.com/data-science/7-things-you-didnt-know-about-the-transformer-a70d93ced6b2">Implementing a Transformer From Scratch</a> was originally published in <a href="https://medium.com/data-science">TDS Archive</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[A Comprehensive Introduction to Bayesian Deep Learning]]></title>
            <link>https://medium.com/data-science/a-comprehensive-introduction-to-bayesian-deep-learning-1221d9a051de?source=rss-eeb9026e13e0------2</link>
            <guid isPermaLink="false">https://medium.com/p/1221d9a051de</guid>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[deep-dives]]></category>
            <category><![CDATA[deep-learning]]></category>
            <category><![CDATA[neural-networks]]></category>
            <category><![CDATA[bayesian-machine-learning]]></category>
            <dc:creator><![CDATA[Joris Baan]]></dc:creator>
            <pubDate>Thu, 04 Mar 2021 19:23:16 GMT</pubDate>
            <atom:updated>2021-03-29T23:26:01.802Z</atom:updated>
            <content:encoded><![CDATA[<h4>Bridging the Gap Between Basics and Modern Research.</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*HPWarQp9k_ySkZc6" /><figcaption>Photo by <a href="https://unsplash.com/@codyhiscox?utm_source=medium&amp;utm_medium=referral">Cody Hiscox</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><pre><strong>Table of Contents</strong></pre><pre>1. Preamble<br>2. Neural Network Generalization<br>3. Back to Basics: The Bayesian Approach<br>    3.1 Frequentists<br>    3.2 Bayesianists<br>    3.3 Bayesian Inference and Marginalization<br>4. How to Use a Posterior in Practice?<br>    4.1 Maximum A Posteriori Estimation<br>    4.2 Full Predictive Distribution<br>    4.3 Approximate Predictive Distribution<br>5. Bayesian Deep Learning<br>    5.1 Recent Approaches to Bayesian Deep Learning<br>6. Back to the Paper<br>    6.1 Deep Ensembles are BMA<br>    6.2 Combining Deep Ensembles With Bayesian Neural Networks<br>    6.3 Neural Network Priors<br>    6.4 Rethinking Generalization and Double Descent<br>7. Final Words</pre><h3>1. Preamble</h3><p>Bayesian (deep) learning has always intrigued and intimidated me. Perhaps because it leans heavily on probabilistic theory, which can be daunting. I noticed that even though I knew basic probability theory, I had a hard time understanding and connecting that to modern Bayesian deep learning research. The aim of this blogpost is to bridge that gap and provide a comprehensive introduction.</p><p>Instead of starting with the basics, I will start with an incredible NeurIPS 2020 paper on Bayesian deep learning and generalization by Andrew Wilson and Pavel Izmailov (NYU) called <a href="https://proceedings.neurips.cc/paper/2020/file/322f62469c5e3c7dc3e58f5a4d1ea399-Paper.pdf">Bayesian Deep Learning and a Probabilistic Perspective of Generalization</a>. This paper serves as a tangible starting point in which we naturally encounter Bayesian concepts in the wild. I hope this makes the Bayesian perspective more concrete and speaks to its relevance.</p><p>I will start with the paper abstract and introduction to set the stage. As we encounter Bayesian concepts, I will zoom out to give a comprehensive overview with plenty of intuition, both from a probabilistic as well as ML/function approximation perspective. Finally, and throughout this entire post, I’ll circle back to and connect with the paper.</p><p>I hope you will walk away not only feeling at least slightly Bayesian, but also with an understanding of the paper’s numerous contributions, and generalization in general ;)</p><h3>2. Neural Network Generalization (Abstract &amp; Introduction)</h3><p>If your Bayesian is a bit rusty the abstract might seem rather cryptic. The first two sentences are of particular importance to our general understanding of Bayesian DL. The middle part presents three technical contributions. The last two highlighted sentences provide a primer on new insights into mysterious neural network phenomena. I’ll cover everything, but first things first: the paper’s introduction.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ZFoSvI8mU3VkNX3u5teGjw.png" /><figcaption>Abstract of <a href="https://proceedings.neurips.cc/paper/2020/file/322f62469c5e3c7dc3e58f5a4d1ea399-Paper.pdf">Bayesian Deep Learning and a Probabilistic Perspective of Generalization</a> by Andrew Wilson and Pavel Izmailov (NYU)</figcaption></figure><p>An important question in the introduction is how and why neural networks generalize. The authors argue that</p><blockquote>“From a probabilistic perspective, generalization depends largely on two properties, the support and the inductive biases of a model.”</blockquote><p>Support is the <strong><em>range</em></strong><em> of dataset classes</em> that a model can support. In other words; the range of functions a model can represent, where a function is trying to represent the data generative process. The inductive bias defines how <strong>good</strong> a model class is at fitting a specific dataset class (e.g. images, text, numerical features). The authors call this, quite nicely, the “distribution of support”. In other words, model class performance (~inductive bias) distributed over the range of all possible datasets (support).</p><p>Let’s look at the examples the authors provide. A linear function has truncated support as it cannot even represent a quadratic function. An MLP is highly flexible but distributes its support across datasets too evenly to be interesting for many image datasets. A convolutional neural network exhibits a good balance between support and inductive bias for image recognition. Figure 2a illustrates this nicely.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/866/1*ac6EsH8cgvayTPR1SaeFkQ.png" /><figcaption>Support and its distribution (inductive bias) for several model types. <a href="https://arxiv.org/pdf/2002.08791.pdf">Wilsen et al. (2020) Figure 2a</a></figcaption></figure><p>The vertical axis represents what I naively explained as “how good a model is at fitting a specific dataset”. It actually is <em>Bayesian evidence</em>, or <em>marginal likelihood</em>; our first Bayesian concept! We’ll dive into it in the next section. Let’s first finish our line of thought.</p><p>A good model not only needs a large support to be <strong>able</strong> to represent the true solution, but also the right inductive bias to actually <strong>arrive</strong> at that solution. The <em>Bayesian posterior</em>, think of it as our model for now, should contract to the right solution due to the right inductive bias. However, the prior hypothesis space should be broad enough such that the true model is functionally possible (broad support). The illustrations below demonstrate this for the three example models. From left to right we see a CNN in green, linear function in purple, and MLP in pink.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/650/1*jo9r3S40-QOcEzCC7qpu5g.png" /><figcaption>Relation between the prior, posterior and true model for model types with varying support and inductive bias. CNN (b), MLP ( c ) and linear model (d). <a href="https://arxiv.org/pdf/2002.08791.pdf">Wilsen et al. (2020) Figure 2</a></figcaption></figure><p>At this point in the introduction, similar to first sentence of the abstract, the authors stress that</p><blockquote>”The key distinguishing property of a Bayesian approach is marginalization instead of optimization, where we represent solutions given by all settings of parameters weighted by their posterior probabilities, rather than bet everything on a single setting of parameters.”</blockquote><p>The time is ripe to dig into marginalization vs optimization, and broaden our general understanding of the Bayesian approach. We’ll touch on terms like the posterior, prior and predictive distribution, the marginal likelihood and bayesian evidence, bayesian model averaging, bayesian inference and more.</p><h3>3 Back to Basics: The Bayesian Approach</h3><p>We can find claims about marginalization being at the core of Bayesian statistics everywhere. Even in Bishop’s ML bible <a href="http://users.isr.ist.utl.pt/~wurmd/Livros/school/Bishop%20-%20Pattern%20Recognition%20And%20Machine%20Learning%20-%20Springer%20%202006.pdf">Pattern Recognition and Machine Learning</a>. The opposite to the Bayesian perspective is the frequentist perspective. This is what you encounter in most machine learning literature. It’s also easier to grasp. Let’s start there.</p><h4>3.1 Frequentists</h4><p>The frequentist approach to machine learning is to <em>optimize</em> a loss function to obtain an optimal setting of the model parameters. An example loss function is cross-entropy, used for classification tasks such as object detection or machine translation. The most commonly used optimization techniques are variations on (stochastic) gradient descent. In SGD the model parameters are iteratively updated in the direction of the steepest descent in loss space. This direction is determined by the gradient of the loss with respect to the parameters. The desired result is that for the same or similar inputs, this new parameter setting causes the output to closer represent the target value. In the case of neural networks, gradients are often computed using a computational trick called backpropagation.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/700/1*ERI_CaYxTaXt4mZWHUuN_Q.png" /><figcaption>Navigating a loss space in the direction of steepest descent using Gradient Descent. <a href="https://arxiv.org/abs/1805.04829">Amini et al. (2017) Figure 2</a>.</figcaption></figure><p>From a probabilistic perspective, frequentists are trying to <strong>maximize</strong> the <em>likelihood p(D|w, M})</em>. In plain english: to pick our parameters <em>w</em> such that they maximize the probability of the observed dataset <em>D </em>given our choice of model <em>M</em> (Bishop, Chapter 1.2.3). <em>M </em>is often left out for simplicity. From a probabilistic perspective, a (statistical) model is simply a probability distribution over data <em>D </em>(Bishop, Chapter 3.4). For example; a language model outputs a distribution over a vocabulary, indicating how likely each word is to be the next word. It turns out this frequentist way of <strong>maximum likelihood estimation</strong> (<strong>MLE</strong>) to obtain, or “train”, predictive models can be viewed from a larger Bayesian context. In fact, MLE can be considered a special case of maximum a posteriori estimation (MAP, which I’ll discuss shortly) using a uniform prior.</p><h4>3.2 Bayesianists</h4><p>A crucial property of the Bayesian approach is to realistically <strong>quantify</strong> <strong>uncertainty</strong>. This is vital in real world applications that require us to trust model predictions. So, instead of a parameter <strong>point estimate,</strong> a Bayesian approach defines a full probability distribution over parameters. We call this the <em>posterior distribution</em>. The posterior represents our <strong>belief/hypothesis/uncertainty</strong> about the value of each parameter (setting). We use <strong>Bayes’ Theorem</strong> to compute the posterior. This theorem lies at the heart of Bayesian ML — hence the name — and can be <a href="http://www.hep.upenn.edu/~johnda/Papers/Bayes.pdf">derived using simple rules of probability</a>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6NSesKQFzvuEnUdUWu6AyA.png" /></figure><p>We start with specifying a <em>prior distribution</em> <em>p(w)</em> over the parameters to capture our <strong>belief</strong> about what our model parameters should look like <strong>prior to</strong> observing any data.</p><p>Then, using our dataset, we can <strong>update</strong> (multiply) our prior belief with the <em>likelihood p(D|w)</em>. This likelihood is the same quantity we saw in the frequentist approach. It tells us how well the observed data is explained by a specific parameter setting <em>w</em>. In other words; how good our model is at <em>fitting</em> or <em>generating</em> that dataset. The likelihood is a function of our parameters <em>w</em>.</p><p>To obtain a valid posterior probability <strong>distribution</strong>, however, the product between the likelihood and the prior must be evaluated for each parameter setting, and normalized. This means <strong>marginalizing</strong> (summing or integrating) over <strong>all</strong> parameter settings. The normalizing constant is called the <em>Bayesian (model) evidence</em> or <em>marginal likelihood</em> <em>p(D)</em>.</p><p>These names are quite intuitive as <em>p(D)</em> provides <strong>evidence</strong> for how good our model (i.e. how likely the data) is <em>as a whole</em>. With “model as a whole” I mean taking into account <strong>all possible</strong> parameter settings. In other words: marginalizing over them. We sometimes explicitly include the model choice <em>M</em><br>in the evidence as <em>p(D|M)</em>. This enables us to compare different models with different parameter spaces. In fact, this comparison is exactly what happens in the paper when comparing the support and inductive bias between a CNN, MLP and linear model!</p><h4>3.3 Bayesian Inference and Marginalization</h4><p>We’ve now arrived at the core of the matter. <em>Bayesian inference</em> is the learning process of finding (inferring) the posterior <strong>distribution</strong> over <em>w</em>. This contrasts with trying to find the <strong>optimal</strong> <em>w</em> using optimization through differentiation, the learning process for frequentists.</p><p>As we now know, to compute the full posterior we must <strong>marginalize</strong> over the whole parameter space. In practice this is often impossible (intractable) as we can have infinitely many such settings. <em>This is why a Bayesian approach is fundamentally about marginalization instead of optimization</em>.</p><p>The intractable integral in the posterior leads to a different family of methods to learn parameter values with. Instead of gradient descent, Bayesianists often uses <strong>sampling</strong> methods such as Markov Chain Monte Carlo (MCMC), or <strong>variational inference</strong>; techniques that try to mimic the posterior using a simpler, tractable family of distributions. Similar techniques are often used for generative models such as VAEs. A relatively new method to approximate a complex distribution is <strong>normalizing flows</strong>.</p><h3>4. How to Use a Posterior in Practice?</h3><p>Now that we understand the Bayesian posterior distribution, how do we actually use it in practice? What if we want to predict, say, the next word, let’s call <em>y</em> given an unseen sentence <em>x</em>?</p><h4>4.1 Maximum A Posteriori (MAP) Estimation</h4><p>Well, we could simply take the posterior distribution over our parameters for our model <em>M</em> and pick the parameter setting <em>w^</em> that has the highest probability assigned to it (the distribution’s mode). This method is called <strong>Maximum A Posteriori</strong> or <strong>MAP</strong> estimation. But… It would be quite a waste to go through all this effort of computing a proper probability distribution over our parameters only to settle for another point estimate, right? (Except when nearly all of the posterior’s mass is centered around one point in parameter space). Because MAP provides a point estimate, it is not considered a full Bayesian treatment.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/640/1*9x8gZndXUiaTkhE2rJ-6dw.png" /><figcaption>Maximum A Posteriori (MAP) estimation; not a full Bayesian treatment. <a href="http://web.eecs.umich.edu/~hero/Preprints/main_564_15.pdf">Hero et al. (2008)</a></figcaption></figure><h4>4.2 Full Predictive Distribution</h4><p>The full fledged Bayesian approach is to specify a <strong>predictive distribution</strong><br><em>p(y|D,x)</em>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*NV1SjKTfkg_3UsWHfNAiaw.png" /></figure><p>This defines the probability for class label <em>y </em>given new input <em>x</em> and dataset <em>D</em>. To compute the predictive distribution we need to marginalize over our parameter settings again! We multiply the posterior probability of each setting <em>w </em>with the probability of label <em>w</em> given input <em>x</em> using parameter setting <em>w</em>. This is called <strong>Bayesian Model Averaging</strong>, or <strong>BMA,</strong> we take a weighted average over all possible models (parameter settings in this case). The predictive distribution is the <strong>second</strong> important place for marginalization in Bayesian ML, the first being the posterior computation itself. An intuitive way to visualize a predictive distribution is with a simple regression task, like in the Figure below. For a concrete example check out <a href="http://www.cs.toronto.edu/~radford/csc2541.S11/week1.pdf">these slides</a> (slide 9–21).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/547/1*TQEzoWuD1Z2tByK4plYOwQ.png" /><figcaption>Predictive distribution on a simple regression task. High certainty around observed data points; high uncertainty elsewhere. <a href="http://mlg.eng.cam.ac.uk/yarin/blog_3d801aa532c1ce.html)*">Yarin Gal (2015</a><a href="http://mlg.eng.cam.ac.uk/yarin/blog_3d801aa532c1ce.html)*">)</a></figcaption></figure><h4>4.3 Approximate Predictive Distribution</h4><p>As we know by now, the integral in the predictive distribution is often intractable and at the very least extremely computationally expensive. A third method to using a posterior is by sampling a few parameters settings and combining the resulting models (e.g. approximate BMA). This is actually called a <strong>Monte Carlo</strong> approximation of the predictive distribution!</p><p>This last method is vaguely reminiscent of something perhaps more familiar to a humble frequentist: deep ensembles. Deep ensembles are formed by combining neural networks that are architecturally identical, but trained with different parameter initializations. This beautifully ties in with where we left off in the paper! Remember the abstract?</p><blockquote>“We show that deep ensembles provide an effective mechanism for approximate Bayesian marginalization, and propose a related approach that further improves the predictive distribution by marginalizing within basins of attraction”.</blockquote><p>Reading the abstract for the second time, the contributions should make a lot more sense. Also, we are now finally steering into Bayesian Deep Learning territory!</p><h3>5. Bayesian Deep Learning</h3><p>A Bayesian Neural Network (BNN) is simply posterior inference applied to a neural network architecture. To be precise, a prior distribution is specified for each weight and bias. Because of their huge parameter space, however, inferring the posterior is even more difficult than usual.</p><p>So why do Bayesian DL at all?</p><p>The classic answer is to obtain a <strong>realistic expression of uncertainty</strong> or <em>calibration</em>. A classifier is considered calibrated if the probability (confidence) of a class prediction aligns with its misclassification rate. As said before, this is crucial in real world applications.</p><blockquote>”Neural networks are often miscalibrated in the sense that their predictions are typically overconfident.”</blockquote><p>However, the authors of our running paper, Wilson and Izmailov, argue that Bayesian model averaging increases <strong>accuracy</strong> as well. According to Section 3.1, the Bayesian perspective is in fact <strong>especially</strong> compelling for neural networks! Because of their large parameter space, neural networks can represent many different solutions, e.g. they are underspecified by the data. This means a Bayesian model average is extremely useful because it combines a diverse range of functional forms, or “perspectives”, into one.</p><blockquote>”A neural network can represent many models that are consistent with our observations. By selecting only one, in a classical procedure, we lose uncertainty when the models disagree for a test point.”</blockquote><h3>5.1 Recent Approaches To (Approximate) Bayesian Deep Learning</h3><p>A number of people have recently been trying to combine the advantages of a traditional neural network (e.g. computationally efficient training using SGD &amp; back propagation) with the advantages of a Bayesian approach (e.g. calibration).</p><h4>Monte Carlo Dropout</h4><p>One popular and conceptually easy approach is <a href="https://arxiv.org/pdf/1506.02142.pdf">Monte Carlo dropout</a>. Recall that dropout is traditionally used as regularization; it provides stochasticity or variation in a neural network by randomly shutting down weights <strong>during training</strong> It turns out dropout can be reinterpreted as approximate Bayesian inference and applied during testing, which leads to multiple different parameter settings. Sounds a little similar to sampling parameters from a posterior to approximate the predictive distribution, mh?</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Lr8-SfyDZZ429TvV6k41GA.png" /><figcaption>Original dropout mechanism. <a href="https://jmlr.org/papers/volume15/srivastava14a/srivastava14a.pdf">Srivastava et al. (2014)</a></figcaption></figure><h4>Stochastic Weight Averaging — Gaussian (SWAG)</h4><p>Another line of work follows from <a href="https://arxiv.org/pdf/1803.05407.pdf">Stochastic Weight Averaging</a> (SWA), an elegant approximation to ensembling that intelligently combines weights of the same network at different stages of training (check out <a href="https://towardsdatascience.com/stochastic-weight-averaging-a-new-way-to-get-state-of-the-art-results-in-deep-learning-c639ccf36a">this</a> or <a href="https://pytorch.org/blog/stochastic-weight-averaging-in-pytorch/">this</a> blogpost if you want to know more. <a href="https://arxiv.org/pdf/1902.02476.pdf">SWA-Gaussian</a> (SWAG) builds on it by approximating the shape (local geometry) of the posterior distribution using simple information provided by SGD. Recall that SGD “moves” through the parameter space looking for a (local) optimum in loss space. To approximate the local geometry of the posterior, they fit a Gaussian distribution to the first and second moment of the SGD iterate. <a href="https://en.wikipedia.org/wiki/Moment_(mathematics)">Moments</a> describe the shape of a function or distribution, where the zero moment is the the sum, the first moment is the mean, and the second moment is the variance. These fitted Gaussian distributions can then be used for BMA.</p><h4>Frequentist Alternatives to Uncertainty Representation</h4><p>I have obviously failed to mention at least 99% of the field here (e.g. <a href="https://discovery.ucl.ac.uk/id/eprint/10080902/1/kflaplace.pdf">KFAC Laplace</a> and <a href="https://github.com/gpleiss/temperature_scaling">temperature scaling</a> for improved calibration), and picked the examples above in part because they are related to our running paper. I’ll finish with one last example of a recent <strong>frequentist</strong> (or is it…) alternative to uncertainty approximation. This is a popular method showing that one can train a deep ensemble and use it to form a predictive distribution, resulting in a well calibrated model. They use a few bells and whistles that I won’t go into, such as adversarial training to smoothen the predictive distribution. Check out the paper <a href="https://arxiv.org/pdf/1612.01474.pdf">here</a>.</p><h3>6. Back to the Paper</h3><p>By now we are more than ready to circle back to the paper and go over its contributions! They should be easier to grasp :)</p><h4>6.1 Deep Ensembles are BMA</h4><p>Contrary to how recent literature (myself included) has framed it, Wilson and Izmailov argue that deep ensembles are <strong>not</strong> a frequentist alternative to obtain Bayesian advantages. In fact, they are a very good <strong>approximation</strong> of the posterior distribution. Because deep ensembles are formed by MAP or MLE retraining, they can form different <em>basins of attraction</em>. A basin of attraction is a “basin” or valley in the loss landscape that leads to some (locally) optimal solution. But there might be, and usually are, multiple optimal solutions, or valleys in the loss landscape. The use of multiple basins of attraction, found by different parts of an ensemble, results in more functional diversity than Bayesian approaches that focus on approximating posterior within single basin of attraction.</p><h4>6.2 Combining Deep Ensembles with Bayesian Neural Networks (Section 4)</h4><p>This idea of using multiple basins of attraction is important for the next contribution as well: an improved method for approximating predictive distributions. By combining the multiple basins of attraction property that deep ensembles have with the Bayesian treatment in SWAG, the authors propose a best-of-both-worlds solution: <strong>Multi</strong>ple basins of attraction <strong>S</strong>tochastic <strong>W</strong>eight <strong>A</strong>veraging <strong>G</strong>aussian or <strong>MultiSWAG</strong>:</p><blockquote>”MultiSWAG combines multiple independently trained SWAG approximations, to create a mixture of Gaussians approximation to the posterior, with each Gaussian centred on a different basin. We note that MultiSWAG does not require any additional training time over standard deep ensembles.”</blockquote><p>Have a look at the paper if you’re interested in the nitty gritty details ;)</p><h4>6.3 Neural Network Priors (Section 5)</h4><p>How can we ever specify a meaningful prior over millions of parameters, I hear you ask? It turns out this is a pretty valid question. In fact, the Bayesian approach is sometimes criticised because of it.</p><p>However, in Section 5 of the paper Wilson and Izmailov provide evidence that specifying a vague prior, such as a simple Gaussian might actually not be such a bad idea.</p><blockquote>”Vague Gaussian priors over parameters, when combined with a neural network architecture, induce a distribution over functions with useful inductive biases.” …</blockquote><blockquote>… ”The distribution over functions controls the generalization properties of the model; the prior over parameters, in isolation, has no meaning.”</blockquote><p>A vague prior combined with the functional form of a neural network results in a meaningful distribution in function space. The prior itself doesn’t matter, but its effect on the resulting predictive distribution does.</p><h4>6.4 Rethinking Generalization &amp; Double Descent (Section 6 and 7)</h4><p>We have now arrived at the strange neural network phenomena I highlighted in the abstract. According to Section 6, the <a href="https://arxiv.org/pdf/1611.03530.pdf">surprising fact that neural networks can fit random labels</a> is actually not surprising at all. Not if you look at it from perspective of support and inductive bias. Broad support, the range of datasets for which <em>p(D|M) &gt; 0</em>, is important for generalization. In fact, the ability to fit random labels is perfectly fine as long as we have the right inductive bias to steer the model towards a good solution. Wilson and Izmailov also show that this phenomena is not mysteriously specific to neural networks either, and that Gaussian Processes exhibit the same ability.</p><h4>Double Descent</h4><p>The second phenomena is double descent. Double descent is a <a href="https://openai.com/blog/deep-double-descent/">recently discovered phenomena</a> where bigger models and more data unexpectedly decreases performance.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ek_Krz3pEDvieabGhLlV6w.png" /><figcaption>Figure taken from <a href="https://openai.com/blog/deep-double-descent/">this</a> OpenAI blogpost explaining Deep Double Descent</figcaption></figure><p>Wilson and Izmailov find that models trained with SGD suffer from double descent, but that SWAG reduces it. More importantly, both MultiSWAG as well as deep ensembles completely mitigate the double descent phenomena! This is in line with their previously discussed claim that</p><blockquote>”Deep ensembles provide a better approximation to the Bayesian predictive distribution than conventional single-basin Bayesian marginalization procedures.”</blockquote><p>and highlights the importance of marginalization over multiple modes of the posterior.</p><h3>Final Words</h3><p>You made it! Thank you for reading all the way through. This post became quite lengthy but I hope you learned a lot about Bayesian DL. I sure did.</p><p>Note that I am not affiliated to Wilson, Izmailov or their group at NYU. This post reflects my own interpretation of their work, except for the quote blocks taken directly from the paper.</p><p>Please feel free to ask any question or point out mistakes that I’ve undoubtedly made. I would also love to know whether you liked this post. You can find my contact details on my <a href="http://jorisbaan.nl">website</a>, message me on <a href="https://twitter.com/jsbaan">Twitter</a> or connect on <a href="https://www.linkedin.com/in/joris-baan-669324b3/">LinkedIn</a>. Check out my personal blog at <a href="http://jorisbaan.nl/posts">jorisbaan.nl/posts</a> for proper math rendering!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1221d9a051de" width="1" height="1" alt=""><hr><p><a href="https://medium.com/data-science/a-comprehensive-introduction-to-bayesian-deep-learning-1221d9a051de">A Comprehensive Introduction to Bayesian Deep Learning</a> was originally published in <a href="https://medium.com/data-science">TDS Archive</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>