<?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 Greg Sommerville on Medium]]></title>
        <description><![CDATA[Stories by Greg Sommerville on Medium]]></description>
        <link>https://medium.com/@gregsommerville1?source=rss-a26d17de2fa6------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/0*b_bn-2FpbLRWgQ_Y</url>
            <title>Stories by Greg Sommerville on Medium</title>
            <link>https://medium.com/@gregsommerville1?source=rss-a26d17de2fa6------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Wed, 20 May 2026 13:39:29 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@gregsommerville1/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[The Developer’s Guide to OpenCode on Google Cloud]]></title>
            <link>https://medium.com/google-cloud/the-developers-guide-to-opencode-on-google-cloud-79a6920543b3?source=rss-a26d17de2fa6------2</link>
            <guid isPermaLink="false">https://medium.com/p/79a6920543b3</guid>
            <category><![CDATA[gcp]]></category>
            <category><![CDATA[open-code]]></category>
            <category><![CDATA[vertex-ai]]></category>
            <category><![CDATA[agentic-ai]]></category>
            <dc:creator><![CDATA[Greg Sommerville]]></dc:creator>
            <pubDate>Mon, 18 May 2026 14:43:06 GMT</pubDate>
            <atom:updated>2026-05-19T02:32:59.199Z</atom:updated>
            <cc:license>http://creativecommons.org/publicdomain/zero/1.0/</cc:license>
            <content:encoded><![CDATA[<p>Combine model flexibility with enterprise-grade security and performance</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0-J4zhvQYSHsvsrmS3_Z8g.png" /></figure><p><strong>What is OpenCode?</strong></p><p>OpenCode is an open-source coding platform that provides developers with a powerful AI-driven development environment through your choice of interfaces: a Terminal User Interface (TUI), a desktop application, an IDE plug-in, or a web page interface. It’s similar to other agentic coding platforms like Google’s Gemini CLI, Anthropic’s Claude Code, or OpenAI’s Codex, with the main difference being that OpenCode is model-agnostic.</p><p><strong>Why would I use OpenCode with Google Cloud Platform (GCP)?</strong></p><p>The main benefits of OpenCode are:</p><ul><li>It’s open source (and therefore the source code is available for examination)</li><li>It allows easy switching between multiple models (either cloud-based or local)</li><li>It maintains data privacy, since data only flows between the OpenCode UI and the model it’s using.</li></ul><p>The main benefits of using OpenCode with GCP are:</p><ul><li>You can choose from all of the models available in the <a href="https://cloud.google.com/model-garden">Model Garden</a>, from the latest version of Gemini to open source models like Gemma, Qwen, or Kimi.</li><li>You can pick which region your model resides in, which allows you to control data locality.</li><li>GCP guarantees customer data, source code, and prompts are never used to train foundation models. This means that using models from the GCP Model Garden keeps your data private.</li><li>You can route your data through private networking via <a href="https://docs.cloud.google.com/vpc/docs/private-service-connect">Private Service Connect</a>, which keeps all of your data off of the public internet. If your business has data residency requirements, this can really help.</li><li>You can fine-tune your own model and host it on GCP, and use that model for coding assistance.</li><li>You can track usage and costs via <a href="https://docs.cloud.google.com/resource-manager/docs/labels-overview">Resource Labels</a>, and billing is rolled up under your GCP billing, rather than being a separate cost.</li></ul><p>To me, the strongest argument in OpenCode’s favor is the fact that you can easily switch between different models, often just by choosing the model from a dropdown control. Different models have different strengths and weaknesses, and being able to choose to use Gemini 3.1 Pro or Claude Opus 4.7 or any other model to suit my needs is a major strength.</p><p>While OpenCode supports local models, real-world coding requires massive context windows and KV caching that quickly overwhelm standard consumer GPUs — even modern 16GB cards like an RTX 5060 Ti. Using cloud-hosted models gives you access to enterprise-grade hardware without the severe performance degradation of local offloading. The bottom line is that unless you have a very powerful machine, a local model probably isn’t going to be good enough for real coding. Because of that, I think a cloud-based model is the way to go.</p><p><strong>How do I install and set up OpenCode?</strong></p><p>You can install OpenCode by downloading an installer from <a href="https://github.com/anomalyco/opencode">https://github.com/anomalyco/opencode</a>. Note that there are several installation options, including using <strong>npm</strong>.</p><p>If you’re a Windows user like me, you may want to check the OpenCode documentation about how to set up the <a href="https://opencode.ai/docs/windows-wsl">server component of OpenCode to run under WSL</a>, which provides faster file access, and a unified Linux toolset. That said, I will say that I use the desktop version of OpenCode on Windows without WSL, and I have yet to see any problems. However, if you plan to let the agent run complex shell scripts or run local testing suites, using a WSL environment ensures the agent doesn’t trip over Windows-specific CLI syntax.</p><p><strong>Activating Models</strong></p><p>Once you have the software installed, the next step is to enable the use of different models within Model Garden. Here’s how to do that.</p><p>Log into the GCP console, choose or create your project, and navigate to the “APIs &amp; Services” page, and click on the button labelled “+ Enable APIs and services”. Enable the “Agent Platform API”. This allows you to use the models in the model garden. The next step is to activate the models you want to use.</p><p>In the search bar at the top of the page, type in “garden”. That will give you a link that will take you to the Model Garden main page. From there, activate the models you wish to use with OpenCode.</p><p>Some models like Gemini and Claude are usage-driven, meaning that you don’t have to manually spin up a virtual machine to host them, and instead you pay only for the input and output tokens. Other models require a dedicated endpoint, which will incur costs related to having that server up and running, regardless of how much you use it.</p><p><strong>Cost Warning for Dedicated Endpoints:</strong> Unlike Gemini’s pay-per-token API, hosting an open-source model on a dedicated endpoint means you are paying for the virtual machine (often equipped with expensive NVIDIA L4 or A100 GPUs) 24/7. <strong>Pro-tip:</strong> If you are using a dedicated endpoint for personal testing, write a quick gcloud script to spin down/pause the endpoint when your workday ends, or set up GCP budget alerts to prevent weekend cost spikes.</p><p><strong>Configuring OpenCode</strong></p><p>The next step is to tell OpenCode about which models are available for use, and which GCP project they are activated under. The first thing to do is to authenticate with GCP, which you accomplish using the following command:</p><p><strong>gcloud auth application-default login</strong></p><p>This command will open a web page to allow you to authenticate with GCP. Behind the scenes, OpenCode utilizes your local Application Default Credentials (ADC) to securely authenticate direct API requests to Vertex AI, meaning your GCP IAM permissions dictate exactly which Model Garden endpoints OpenCode is allowed to call.</p><p>Finally, use the following variable to specify which project within GCP to use (use “export” on WSL or Linux, use “set” in Windows):</p><pre>export GOOGLE_CLOUD_PROJECT=&lt;gcp_project_id&gt;</pre><p><strong>Alternate Approach: </strong>Note that if you don’t want to use ADC (application default credentials), you can set the following environment variable to point to the file that defines a service account to use:</p><pre>export GOOGLE_APPLICATION_CREDENTIALS=&lt;sa_credentials_filename&gt;</pre><p>I recommend checking the official docs at <a href="https://opencode.ai/docs/providers/#google-vertex-ai">https://opencode.ai/docs/providers/#google-vertex-ai</a> for details about connecting to GCP-hosted models, as things change over time.</p><p><strong>Private Service Connect</strong></p><p>If your organization requires that traffic to Vertex AI stay off the public internet — for data residency, compliance, or general security posture — you can route OpenCode’s API calls through a Private Service Connect (PSC) endpoint instead of the default public Google API endpoints.</p><p>Setting up PSC is a non-trivial networking task that requires proper VPC configuration. At a high level, it involves:</p><ul><li>Creating a Global PSC Endpoint: Although Vertex AI uses regional hostnames (e.g., us-central1-aiplatform.googleapis.com), standard API access requires a global PSC endpoint. You will need to reserve a global internal IP address in your VPC and create a forwarding rule that points to the global Google APIs bundle (either all-apis or vpc-sc).</li><li>Configuring Private DNS: To make the routing seamless for OpenCode without needing to override application base URLs, create a private Cloud DNS zone for googleapis.com. Within this zone, create an A record (e.g., *.googleapis.com or specifically for the Vertex AI hostname) that resolves to the internal IP address of your new PSC endpoint.</li><li>Ensuring Connectivity: Ensure whatever machine runs OpenCode can reach that endpoint. This is easiest if OpenCode runs on a Cloud Workstation or GCE VM inside the VPC. Reaching it from a local machine additionally requires Cloud VPN or Cloud Interconnect, plus a Cloud DNS inbound forwarding policy so your local host can correctly resolve the private googleapis.com hostname.</li></ul><p>The relevant Google Cloud documentation to follow is:</p><ul><li><a href="https://docs.cloud.google.com/vpc/docs/configure-private-service-connect-apis">Configure Private Service Connect to access Google APIs</a>: This guide walks through the exact step-by-step setup for a global endpoint and DNS.</li></ul><p>Because you are using Cloud DNS to seamlessly route googleapis.com traffic to your internal VPC endpoint, you do not need to manually configure custom endpoints or alter the opencode.json configuration file. OpenCode will route its API calls securely and internally by default.</p><p><strong>Using OpenCode</strong></p><p>Once your project is set up with the required enabled APIs and models, and (optionally) you’ve set up private networking, you’re ready to start using the tool. If you installed the CLI version, simply type “opencode” to run it. The desktop version is launched just like any other application, so once the application shows on the screen, you are ready to start typing in queries. Like many other agentic coding systems, “/init” will examine your current code base and produce a Markdown file with an overview of the code and important details.</p><p><strong>Conclusion</strong></p><p>There are many different tools available for AI-assisted coding these days, but OpenCode stands out for its flexibility. With a variety of interfaces and simple model-switching, it’s a tool well worth exploring.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=79a6920543b3" width="1" height="1" alt=""><hr><p><a href="https://medium.com/google-cloud/the-developers-guide-to-opencode-on-google-cloud-79a6920543b3">The Developer’s Guide to OpenCode on Google Cloud</a> was originally published in <a href="https://medium.com/google-cloud">Google Cloud - Community</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Building Offline RAG on iOS: How to Run Gemma 3N Locally]]></title>
            <link>https://medium.com/google-cloud/building-offline-rag-on-ios-how-to-run-gemma-3n-locally-ffdfda6f7217?source=rss-a26d17de2fa6------2</link>
            <guid isPermaLink="false">https://medium.com/p/ffdfda6f7217</guid>
            <category><![CDATA[iphone-development]]></category>
            <category><![CDATA[edge-computing]]></category>
            <category><![CDATA[llm]]></category>
            <category><![CDATA[gemma]]></category>
            <dc:creator><![CDATA[Greg Sommerville]]></dc:creator>
            <pubDate>Wed, 03 Dec 2025 19:05:47 GMT</pubDate>
            <atom:updated>2025-12-05T09:41:21.141Z</atom:updated>
            <cc:license>http://creativecommons.org/publicdomain/zero/1.0/</cc:license>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*UM1k559ZPtjXnCsjUWk-AA.png" /><figcaption>Image created with Gemini</figcaption></figure><p>Running a Large Language Model (LLM) like Gemma 3N on an iPhone requires a fundamental shift in mindset. As a cloud developer, I’m used to infinite RAM and simple API calls to models like Gemini. But for this project, those luxuries were gone.</p><p>The goal was strict: build a mobile app with a bundled LLM, an embedding model, and a vector database — all operating fast enough to be usable, and entirely without an internet connection. Here is how we squeeze that much power into a pocket-sized device.</p><p><strong>Just Your Typical RAG Chatbot, but Not</strong></p><p>The goal was to create an iPhone RAG (retrieval augmented generation) chatbot app capable of answering incredibly complex, technical questions about the maintenance of industrial equipment. The source of truth for those questions was a single 350 page PDF reference document that was jammed with complicated tables, images, and detailed text.</p><p>To make this work, I needed a hybrid approach: combining keyword matching with semantic (vector) search. But here is the catch: Semantic search requires an embedding model running locally. Suddenly, our limited memory budget isn’t just for the LLM; it has to be shared with the embedding model, the vector database, and the app logic itself.</p><p>So given the LLM, a hybrid RAG database, and a separate embedding model, the big question is this: how do you put all of that into a single iPhone application, given that most LLMs are very large (not just in terms of number of parameters, but also pure size as measured in gigabytes), and most phones don’t have <em>that </em>much memory (at least compared to desktops or cloud-based machines)?</p><p>The first step is to choose a model.</p><p><strong>Choosing a Model</strong></p><p>Since my target hardware was an iPhone 16, I had a hard ceiling of 8GB of RAM. But the OS and the app code take probably about 3 GB of that, leaving us with a very tight budget for the models and the database. Finding an effective LLM that can run in 3 or 4 GB of memory (a reasonable amount) can be a challenge.</p><p>To narrow down the candidates, I didn’t just look at benchmarks. I used <a href="https://ollama.com/">Ollama </a>on my desktop to host multiple quantized small-scale models, feeding them specific questions related to the industry this app is for. I was able to create a set of sample questions and compare the answers from multiple LLMs using <a href="https://github.com/designcomputer/ollama-model-lab">this handy tool</a>. This gave me a sense of which models had decent built-in knowledge that would be helpful for this use case, and which ones I should skip.</p><p>This testing process highlighted that while several models were fast, Gemma 3N offered the best reasoning capabilities for our specific technical domain. Although I saw some good results from models like Gemma 3 (not 3N) and Qwen, ultimately I got the best answers from Gemma 3N. That’s good, because the 3N models are designed to be hosted on edge devices just like the iPhone.</p><p>At the highest level, there are two versions of Gemma 3N called E2B and E4B. “E2B” stands for “effectively 2 billion parameters”, and “E4B” means “effectively 4 billion parameters”. Normally you want to use the largest model that makes sense for your use case, because typically a 4B model gives better results than a 2B model, but in this case we need to think about memory usage.</p><p>By the way, the “Effective” prefix highlights that the model can run with a reduced memory and compute footprint compared to its total number of parameters. For example, E2B actually contains over 5 billion parameters, but through some innovative optimization methods like Per-Layer Embedding (PLE) caching, conditional parameter loading, and the use of MatFormer architecture, the number of parameters loaded is actually much closer to only 2 billion.</p><p>Although both Gemma E2B and Gemma E4B work on the iPhone, the quality of answers from E2B wasn’t significantly less than those from E4B in my case, and since E2B was smaller and faster, that tipped the scales in terms of choosing the E2B variant.</p><p><strong>How to Use a LLM on an iPhone</strong></p><p>When writing an iPhone app in Swift, there are two obvious options for hosting an LLM: <a href="https://ai.google.dev/edge/mediapipe/solutions/guide">Google’s MediaPipe</a>, and <a href="https://github.com/ml-explore/mlx-swift">Apple’s MLX Swift</a>.</p><p>MediaPipe is cross-platform (iOS, Android, and web) and supports TensorFlow Lite (TFLite) models, recently rebranded to LiteRT, where “RT” stands for Runtime. You can find <a href="https://huggingface.co/docs/transformers/en/tflite">LiteRT models on Hugging Face</a>.</p><p>MLX on the other hand was written by Apple and only runs on Apple hardware. It supports models stored in Safetensors files, which can also be <a href="https://huggingface.co/mlx-community/models">found on Hugging Face</a>.</p><p>Based on my testing, MLX was much faster for certain operations, and the number of model variants available on Hugging Face for MLX was quite a bit larger than the number for MediaPipe. For these reasons, and because I had no need for cross-platform functionality, I went with MLX Swift.</p><p><em>Important note about Quantizing</em>: When you browse the models available on Hugging Face, you’ll see many variants. Even narrowing down to Gemma 3N E2B, you’ll see several different versions of those. There are really two main things I look for in this case: instruction tuning, and the number of bits used for quantizing. (“Quantizing” is the process of taking each of the parameters in a model and shrinking them down in order to save space.)</p><p>Instruction tuning is often indicated by an “it” string in the model name. That means it was trained to follow instructions, which is a necessity when dealing with something like a RAG chatbot.</p><p>Think of Quantization as compressing a high-resolution image. We take the massive, high-precision parameters of the model (usually 16-bit floating point numbers) and shrink them down to 4-bit integers. While this sounds like a drastic loss of data, it allows us to fit a massive brain into a tiny memory budget with surprisingly little loss in intelligence.</p><p>Bottom line — look for an instruction-tuned model that is quantized to 4 bits. The model I used is called <strong>gemma-3n-E2B-it-lm-4bit</strong>.</p><p><strong>Including the Model in your App</strong></p><p>When you download a model from Hugging Face, it comes as a set of files. Although the majority of the model is saved in .safetensor files, other files are included to configure the model and support the associated tokenizer.</p><p>The best way to include that in your app is to create a Folder reference in Xcode that points to the folder with the model files. This way you can update the folder as you need and don’t have to worry about adding or modifying individual files.</p><p><strong><em>A Note on the App Store</em></strong>: The Apple App Store has limits as to how big an app can be, both in terms of initial loading, and total size. You won’t be able to create a very large app like this one and offer it via the App Store. Instead, this approach is good only for situations where you are deploying to corporate devices by using a Mobile Device Management (MDM) solution or something like that. Alternatively, you could leave the model files outside of your app and download them on the first run of the app.</p><p><strong>Loading and Calling the Model</strong></p><p>From a code perspective, I created a single service called LocalLLMService.swift that handles loading the model and also sending back responses, either streamed or all-at-once.</p><p>Let’s start with code for loading the model from our embedded resources. For brevity, I’ll only include the most important parts. You can find the entire file in <a href="https://gist.github.com/GregSommerville/1fbea91f8842fcc385393a5608aa8164"><strong>this GitHub Gist</strong></a>.</p><p>First, let’s include the packages we need.</p><pre>import Foundation<br>import MLX<br>import MLXNN<br>import MLXLLM<br>import MLXLMCommon<br>import Tokenizers</pre><p>Then we load the model in the loadModel() function:</p><pre>// Get full path to model directory<br>guard let bundlePath = Bundle.main.resourcePath else {<br>      throw ModelError.modelNotFound(&quot;Unable to access app bundle&quot;)<br>}<br><br><br>let fullModelPath = (bundlePath as NSString).appendingPathComponent(modelPath)<br><br><br>// Verify model directory exists<br>let fileManager = FileManager.default<br>var isDirectory: ObjCBool = false<br>guard fileManager.fileExists(atPath: fullModelPath, isDirectory: &amp;isDirectory),<br>        isDirectory.boolValue else {<br>      throw ModelError.modelNotFound(fullModelPath)<br>}<br><br>// Verify required model files exist<br>let modelFile = (fullModelPath as NSString).appendingPathComponent(&quot;model.safetensors&quot;)<br>let tokenizerFile = (fullModelPath as NSString).appendingPathComponent(&quot;tokenizer.json&quot;)<br>let configFile = (fullModelPath as NSString).appendingPathComponent(&quot;config.json&quot;)<br><br>guard fileManager.fileExists(atPath: modelFile) else {<br>      throw ModelError.modelNotFound(&quot;model.safetensors not found&quot;)<br>}<br>guard fileManager.fileExists(atPath: tokenizerFile) else {<br>      throw ModelError.tokenizerLoadingFailed(&quot;tokenizer.json not found&quot;)<br>}<br>guard fileManager.fileExists(atPath: configFile) else {<br>      throw ModelError.modelLoadingFailed(&quot;config.json not found&quot;)<br>}<br><br>// Load MLX model container with Metal acceleration<br>print(&quot;  Loading MLX model container...&quot;)<br><br>// Create model configuration with local directory URL<br>let modelURL = URL(fileURLWithPath: fullModelPath)<br>let modelConfig = ModelConfiguration(<br>      directory: modelURL,<br>      defaultPrompt: &quot;You are a helpful assistant.&quot;<br>)<br><br>// Load the model container using LLMModelFactory<br>self.modelContainer = try await LLMModelFactory.shared.loadContainer(<br>      configuration: modelConfig<br>) { progress in<br>      print(&quot;  Loading progress: \(Int(progress.fractionCompleted * 100))%&quot;)<br>  }</pre><p>Once that’s done, there’s a crucial step that helps with memory issues:</p><pre>// Configure MLX GPU buffer cache limit to prevent memory accumulation<br>// MLX caches freed GPU memory for reuse, but this can cause OOM on repeated inferences<br>// Set limit to 50 MB to allow some caching while preventing excessive accumulation<br>let cacheLimit = 50 * 1024 * 1024  // 50 MB<br>MLX.GPU.set(cacheLimit: cacheLimit)</pre><p>Memory use is the major issue when dealing with LLMs on mobile hardware. The MLX framework does have a tendency to hold on to memory, which can accumulate and cause your app to crash after just a couple of queries. The above code explicitly controls how much memory is allocated, which fixes this problem.</p><p>One other note about memory: another really important step is to add an entitlement (via Xcode) to indicate that your app needs more memory. This results in the <strong>com.apple.developer.kernel.increased-memory-limit</strong> entitlement to be added to your app.</p><p>Now that the model is loaded, let’s look at how it’s called. The code supports both streaming and non-streaming responses. Let’s look at the streaming responses:</p><pre>guard let container = modelContainer else {<br>    print(&quot;✗ Model container not initialized&quot;)<br>    return<br>}<br><br>do {<br>print(&quot;  Generating streaming response with MLX...&quot;)<br>print(&quot;  Prompt: \&quot;\(prompt.prefix(50))\(prompt.count &gt; 50 ? &quot;...&quot; : &quot;&quot;)\&quot;&quot;)<br><br><br>// Set up generation parameters<br>let params = GenerateParameters(<br>    temperature: temperature,<br>    topP: topP,<br>    repetitionPenalty: repetitionPenalty<br>)<br><br>// Capture values to avoid retaining self in closure<br>let maxTokensLimit = self.maxTokens<br><br>// Generate with streaming callback<br>let result = try await container.perform { context in<br>    // Prepare input with user messages using context processor<br>    let fullPrompt = prompt<br>    let input = try await context.processor.prepare(input: .init(prompt: fullPrompt))<br><br>    var localTokenCount = 0<br>    return try MLXLMCommon.generate(<br>        input: input,<br>        parameters: params,<br>        context: context<br>    ) { tokens in<br>        // tokens array is cumulative (all tokens so far), not incremental<br>        localTokenCount = tokens.count<br><br>        // Decode new tokens to text (synchronous decode)<br>        let newText = context.tokenizer.decode(tokens: tokens)<br><br>        // Stop if we&#39;ve hit the EOS token ID (model is done) - check first for natural completion<br>        if let eosTokenId = context.tokenizer.eosTokenId,<br>           tokens.contains(eosTokenId) {<br>            return .stop<br>        }<br><br>        // Stop if we see end-of-turn markers in the decoded text<br>        if newText.contains(&quot;&lt;end_of_turn&gt;&quot;) || newText.contains(&quot;&lt;/s&gt;&quot;) {<br>            return .stop<br>        }<br><br>        // Stop if we&#39;ve hit max tokens (safety limit)<br>        if localTokenCount &gt;= maxTokensLimit {<br>            print(&quot;⚠️ Max token limit reached (\(maxTokensLimit)) - appending truncation notice&quot;)<br>            // Send truncation notice to user<br>            Task { @MainActor in<br>                onPartialResponse(&quot;\n\n[Response truncated - maximum length reached]&quot;)<br>            }<br>            return .stop<br>        }<br><br>        // Clean up EOS markers before sending to callback<br>        var cleanedText = newText<br>        cleanedText = cleanedText.replacingOccurrences(of: &quot;&lt;end_of_turn&gt;&quot;, with: &quot;&quot;)<br>        cleanedText = cleanedText.replacingOccurrences(of: &quot;&lt;/s&gt;&quot;, with: &quot;&quot;)<br><br>        // Only send non-empty cleaned text to callback<br>        if !cleanedText.isEmpty {<br>            // Call the partial response callback on main thread<br>            Task { @MainActor in<br>                onPartialResponse(cleanedText)<br>            }<br>        }<br>        return .more<br>    }<br>}<br><br>let generationTime = Date().timeIntervalSince(startTime)<br>print(&quot;  Generation time: \(String(format: &quot;%.3f&quot;, generationTime))s&quot;)<br>print(&quot;  Tokens generated: \(result.tokens.count)&quot;)<br><br>// Force MLX to evaluate computation graph and release GPU buffers<br>// This triggers the cache limit policy, allowing old buffers to be freed<br>MLX.eval()</pre><p>There are a couple of key points to take into consideration. First, at the top of the function we set the LLM parameters like temperature, top-P, etc. Second, we can specify a maximum number of output tokens, and the code stops calling the LLM once that limit is reached.</p><p>Finally (and perhaps most importantly), this implementation differs from standard streaming. The callback returns the <strong>total accumulated response</strong> so far, rather than just the new tokens. Your UI code should replace the current text view entirely on every update, rather than appending to it. This differs from the normal approach of the caller keeping the current answer and appending the new tokens.</p><p>Finally, note the last step (<strong>MLX.eval()</strong>), which is used to force MLX to release some internal buffers, which is another part of the memory saving approach.</p><p><strong>Conclusion: The Cloud in Your Pocket</strong></p><p>A year ago, building a RAG system capable of answering complex maintenance questions required a cloud GPU cluster and an API key. Today, we have that same capability running offline on a phone.</p><p>By carefully selecting an capable, small model like Gemma 3N, utilizing the unified MLX ecosystem, and respecting the strict memory limits of iOS, we didn’t just build a chatbot — we built an entire RAG solution. We proved that the edge is no longer just for “toy” models. It is ready for real work.</p><p>The constraints of mobile development — battery, thermal, and RAM — forces us to be better engineers. And honestly? Watching those tokens stream onto an iPhone screen feels a lot more satisfying than getting a JSON response from a server.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ffdfda6f7217" width="1" height="1" alt=""><hr><p><a href="https://medium.com/google-cloud/building-offline-rag-on-ios-how-to-run-gemma-3n-locally-ffdfda6f7217">Building Offline RAG on iOS: How to Run Gemma 3N Locally</a> was originally published in <a href="https://medium.com/google-cloud">Google Cloud - Community</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How Mixture-of-Experts LLMs Work]]></title>
            <link>https://medium.com/google-cloud/how-mixture-of-experts-llms-work-58b3ba8e0349?source=rss-a26d17de2fa6------2</link>
            <guid isPermaLink="false">https://medium.com/p/58b3ba8e0349</guid>
            <category><![CDATA[mixture-of-experts]]></category>
            <category><![CDATA[llm]]></category>
            <category><![CDATA[ai-model-architecture]]></category>
            <dc:creator><![CDATA[Greg Sommerville]]></dc:creator>
            <pubDate>Tue, 26 Aug 2025 21:39:49 GMT</pubDate>
            <atom:updated>2025-08-30T02:59:40.880Z</atom:updated>
            <content:encoded><![CDATA[<p>An innovative approach to make models more efficient</p><figure><img alt="A game show host faces a panel of contestants, each of whom has a sign showing “expert” in front of them" src="https://cdn-images-1.medium.com/max/1024/1*0JIMpnuKZtE5TTopJr1JLQ.png" /><figcaption>Created using Imagen</figcaption></figure><p>The introduction of Generative AI models has fundamentally changed the landscape of what can be done with text, images, sound, and videos, offering dazzling capabilities like text summarization, customer feedback analysis, automated data entry, automated document reviews, code generation, and many, many more.</p><p>Large Language Models (LLMs) like Google’s Gemini continue to push the boundaries of what’s possible, but as these models grow in size and complexity, they bring with them significant challenges related to computational cost, training time, and efficient deployment.</p><p>This is where <strong>Mixture of Expert </strong>(MoE) LLM models provide a key architectural innovation. This article will demystify MoE architectures, explaining in plain English how they allow for the creation of incredibly powerful yet surprisingly efficient language models that are helping to shape the future of AI.</p><h3>The Evolution of Large Language Models</h3><p>In less than the last ten years, we’ve witnessed an explosion of AI capabilities. Back in 2018, models like BERT provided functionality that was truly impressive at the time, such as classifying text, extracting named entities (people, organizations, locations, and dates) from text, and even some simple question answering. BERT didn’t produce text itself, but the numeric answers it produced were very helpful for many types of problems.</p><p>Fast forward to 2025, and models like Gemini can not only produce long blocks of text or code, but also appear to demonstrate advanced reasoning, often working through complex problems step-by-step. How did we come so far so quickly?</p><p>One of the things driving that advancement is a massive increase in the size of these models. But what does “size” mean when talking about LLMs? Basically, it means the number of different numeric values in the model. When you ask Gemini a question, your text gets converted to numbers, and those numbers flow through what is essentially a giant mathematical formula, which uses numbers (called <strong>parameters</strong>) to change your input data over a series of steps, culminating in the final output numbers being converted back into text.</p><p>Think of the model as an intricate machine with countless adjustable knobs and levers — these are its ‘parameters.’ Each parameter is a numeric value that helps shape how the input data is transformed at every step, eventually leading to the final output.</p><p>The more parameters you have in your model, generally the more powerful the model becomes, since it can then incorporate more subtle patterns and relationships into its knowledge. Just to provide a little context for comparison, BERT had hundreds of millions of parameters, but modern LLMs often have billions or even trillions of parameters.</p><p>Something else happens when models increase in size. Besides more nuanced understanding of patterns, a surprising number of behaviors show up <em>unexpectedly </em>when a model’s size increases past a certain threshold. These are called <strong>emergent behaviors</strong>, and they include things like learning from examples, step-by-step reasoning, and even the ability to solve problems the models weren’t specifically designed for. No one trained the models on these behaviors — instead, these behaviors simply emerged once model sizes increased past a certain threshold.</p><p>Sounds great, doesn’t it? And it seems to imply that bigger models are always better than smaller models. So why wouldn’t we use the largest model possible every time, given that we want the best quality results?</p><p>Cost is the answer. All those mathematical calculations have to be performed on computer hardware, and the more calculations there are, the more time and computing power it takes, which results in higher electricity usage and therefore more cost.</p><p>Bigger models are also generally slower than small models, and they often require specialized computing hardware that can handle very large number of calculations done in parallel. This is true both for using a finished model (called <strong>inference</strong>), as well as creating a new model (called <strong>training</strong>).</p><p>Mixture of Experts (MoE) models attempt to solve this problem by using an innovative new architecture. Instead of using every single parameter in a model, they use only subsets of the model as needed, based on the user query. This means the model can incorporate knowledge of many different topics, and that knowledge is stored in such a way that only the most relevant sections of the models are activated as needed.</p><h3>How LLMs Work</h3><p>Traditional LLMs and MoE models have a lot in common. If you think of a model as a series of steps, there are a few steps that are the same for both types of models. Let’s go through each of those early steps.</p><h3>Tokenization — Turning Text into Numbers</h3><p>The first thing a LLM does is to convert the input text into numbers via a process called <strong>tokenization</strong>. Each model has a vocabulary, and that vocabulary consists of words with corresponding IDs. As an example, let’s say the word “dog” has a token ID of 6420. Each time the model finds the word “dog”, it will represent that by the number 6420.</p><p>Now to be <em>really</em> precise, I will say that some tokens are fragments of words (like “ing”) or even punctuation marks. Trust me when I say that there are benefits to tokenizing parts of words rather than whole words, but the details are not relevant for this discussion. Suffice it to say that tokens are either words, fragments of words, or things like punctuation marks.</p><p>Here’s an illustration of how the sentence “I am walking the dog” is converted into tokens:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/927/1*vPiFh_jSL7B2FCGYA7bRbg.png" /><figcaption>The text “I am walking the dog” with boxes around each token, and arrows pointing to corresponding token ID numbers</figcaption></figure><h3>Embeddings — Numbers that contain meaning</h3><p>Converting text words into numeric tokens is a great first step, but the number 6420 (representing “dog”) doesn’t have much meaning by itself. The next step is to use an <strong>embedding </strong>for the token.</p><p>An embedding is simply a list of numbers (known as a <strong>vector</strong>). Embeddings can be quite large, having hundreds or even thousands of numbers per vector. Each model has an embedding for each token, as is shown here:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*F669-l7q0INHDCCO7IVzkg.png" /><figcaption>A set of rows, each row with a token ID as an index, with each row containing multiple floating point numbers</figcaption></figure><p>The idea of the embedding is that each of the numbers in the vector somehow represents some quality of the token itself. By having hundreds or thousands of numbers with different values (or magnitudes) per vector, you get a combination of qualities that can faithfully represent any concept.</p><p>In other words, it’s a way to associate meaning with each token, and it’s helpful in terms of comparing different tokens since the embedding vector for “cat” has a lot of commonality with the embedding vector for “dog”. Why? Well, they’re both animals, both common pets, both mammals, both quadrupeds, etc. Many of the similarities can be expressed by similar elements in the embeddings for both words.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*gY0cVB-sWYf8vY1AJrV8aQ.png" /><figcaption>An embedding vector showing possible interpretations for a few of its elements</figcaption></figure><h3>Blocks — Modular pieces of the model</h3><p>Once the initial tokenization happens and the initial set of vectors has been looked up for each token, the rest of the model processing happens. That processing is divided into <strong>transformer blocks</strong>, and a model can have hundreds of them, each feeding their output into the input of the next block until all the blocks have been used. At that point, the final output vector is transformed into a probability vector, and a single token is selected from it.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*cWY-b20BHheZ1o_-Hc2rXA.png" /><figcaption>Simplified architecture diagram for an LLM, showing initial layers and a set of transformer blocks</figcaption></figure><p>We’ll talk about what happens in each block shortly, but the important thing to remember is that an LLM is constructed of multiple blocks, with each block taking in input vectors and outputting modified vectors. Those modified vectors are passed to the next block, and the process continues until the data reaches the output layer of the final block.</p><p>Once we reach the final layer of the final block of the model, an output vector is created, and the values in that vector are used to determine the single output token. This is done by converting the final output vector into a new vector that contains probabilities for each possible output token in the model’s vocabulary. In this case, all of the values in the probability vector add up to 1, and each value represents the probability that the corresponding token will be selected.</p><p>Using attributes like <strong>top-P</strong>, <strong>top-K</strong>, and <strong>temperature</strong>, the model then picks a token based on the contents of the final probability vector. Those attributes are different approaches used to select a final token given a selection of choices. “Top P” means choose from the highest percentage choices, “top K” means choose from a fixed-size set of the most likely options, and temperature controls how much to weigh towards unlikely choices versus higher-probability choices.</p><p>Once selected, the final resulting token is then appended to the input string, and the entire LLM process starts over again, with the new string passed in to the model, then tokenized, then converted to embeddings, then passing through the attention blocks, etc.</p><h3>Attention — How do tokens influence each other?</h3><p>So that’s the overall architecture. Now let’s talk about what happens within each transformer block.</p><p>The input for a block is a set of vectors, with one vector for each token in the input query. The first thing we do with those vectors is to modify them using a mechanism called <strong>self-attention</strong>, which is a process that modifies each token’s vector with information from the other vectors in the input string.</p><p>In other words, once we have initial embedding vectors for each token, we need a way for the model to understand the context of the entire sentence. A human can instantly tell that in the sentence, “The dog chased the cat, and it ran away,” the word “it” refers to “the cat.” An LLM has to learn this kind of relationship. That’s where the attention mechanism comes in.</p><p>Essentially, the model looks at every single token’s embedding vector, and for each token it asks the question: “How important are all the other tokens in the sentence to <em>me</em> right now?” The attention mechanism calculates a weight for every other token, a process that is essentially the model’s way of determining how relevant or related each token is to the others.</p><p>The attention mechanism allows the model to create a new, refined embedding vector for each token that is no longer just its standalone meaning but is now contextually aware. This new vector for “it” will now have a strong connection to the vector for “cat,” and a weaker one to the vector for “dog” for example.</p><p>In short, the attention mechanism is how the model builds a rich, interconnected understanding of the entire text, allowing it to make sense of things like pronoun references, word relationships, and the overall meaning of a sentence or paragraph.</p><p>Here’s an illustration of how different tokens influence each other. In this example, we’re creating a new vector based on the vector that originally came from the “dog” token, combined with every other preceding vector using a weighted sum. Each preceding token that is combined with the “dog” token has its own weight, so in the end some tokens have much more of an influence on the “dog” vector than others do.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*t5YJxAio3xR1T3z8zcDlcg.png" /><figcaption>Showing how different tokens have different weights, as they influence another token in the text</figcaption></figure><p>To be precise, this self-attention mechanism isn’t done just one for each token. Instead, the attention mechanism typically uses multiple sets of weights when modifying embeddings, and in the end these multiple modified vectors are mathematically combined. This process is called <strong>multihead attention</strong>, and it allows the model to combine the different token embedding vectors (with their different weights) in different ways.</p><p>The idea is that by combining multiple different takes of self-attention, a truer, deeper understanding of the relationships between the tokens will emerge.</p><p>In the example above, the tokens “walk” and “ing” influence the token “dog”, but the individual weights will vary from one attention head to another. In this simple example there probably aren’t a lot of <em>really </em>distinct sets of weights, but as the text gets more complex, having different attention heads provides many different perspectives on the text.</p><p>Finally, within the attention mechanism, there is also information added to each embedding that indicates the order of the vector within the overall text input. This is called <strong>positional encoding</strong>, and it’s used so the model understands the difference between “The dog is on the rug” and “The rug is on the dog”.</p><p>To sum up, the attention mechanism allows tokens to influence each other, so the model ends up with a much deeper understanding of the overall meaning of the text.</p><h3>What is an “Expert” in an LLM?</h3><p>At this point, we understand some basic architecture components of an LLM. Now let’s talk about what a Mixture-of-Experts model is, starting with an analogy.</p><p>Suppose we have a group of friends, and we want to route questions to different friends depending on the topic. However, at the very start of this process none of the friends really knows anything about any topic. So that means we essentially pick a random friend for a particular question. That friend doesn’t initially know anything about the topic, so they get the answer wrong. Since they did, you correct them so they learn more about the topic, and you make a mental note to route more questions about the same topic to them, since they are learning more as time goes by.</p><p>To extend the analogy, let’s say that instead of routing questions to one friend, you initially route a particular question to three friends. At the start none of them knows the answer, but again, you correct them when they are wrong and also remember to route more questions to them about this topic as they learn.</p><p>And since you’re asking three friends the same question, the odds are good that maybe one or two of the experts knew <em>slightly </em>more than the other experts, so you are also adjusting your thinking about which friends to trust the most for a particular question. In the end, you combine all the answers from all the friends you asked, but weigh them based on how much trust you have for each expert for this topic.</p><p>This is essentially the process of training an MoE model. The decisions about which experts to route a vector to are driven by a router, and during training both the selected (or activated) expert is trained and the router is <em>also </em>trained.</p><p>Now that we’ve looked at an analogy, let’s get into the technical details.</p><h3>How MoE models differ from Dense models</h3><p>Once the initial tokenization is done and initial embeddings are looked up, the remainder of an LLM’s processing involves passing data through a series of transformer blocks. The content of each block is where MoE models differ from traditional “dense” models.</p><p>Here’s a diagram comparing the transformer block architectures of a MoE model and a traditional dense model:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*T3VZgrCUjCcBrgE4l4adRQ.png" /><figcaption>Comparing transformer blocks for dense models and MoE models</figcaption></figure><p>For a traditional dense model, the output vectors of the attention mechanism are then passed into a <strong>feed forward network</strong>, which is a set of layers. Each layer is essentially a mathematical formula that takes a vector (a set of numbers) and processes it in various ways using the model’s parameters in order to produce another vector, which is then passed to the next layer, etc. The vector flows through all of these layers until it reaches the bottom of the block.</p><p>In contrast, a Mixture of Experts model takes the output from the attention mechanism and then decides which experts it should route the vector to within its block. Each “expert” is a feed forward network, so the main difference between the model types is the fact that with a dense model, every vector flows through the remaining layers of the block, while with a MoE model, the routing mechanism sends the vector to one or more experts, ignoring the others.</p><p>The final step for an MoE model is to take the results of all of the expert networks and combine them into a single vector using a weighted sum. At this point this output vector is passed to the next block, and processing continues until we reach the final block, where the final output vector is handled just as the final vector is for a traditional model (as described above).</p><h3>How the Router Works</h3><p>The router (or <strong>gate network</strong>) can be thought of as a small self-contained model within each transformer block. It’s designed to take in a vector and produce another vector that indicates which experts should be engaged. The number of experts activated is different for each input vector, which means sometimes a single expert will be activated, while other times more than one will be activated (each with different weights for their importance for the topic.)</p><p>The routing model that controls which experts should be engaged for a particular vector is trained, just like every other part of the overall LLM. That means as the LLM model training happens, the router model (one per block) is adjusted along with all of the values in the block’s feed forward networks that were activated for a particular vector.</p><p>That means that initially, the router network is essentially completely random. It’s only after training that the router network develops preferences for certain vectors to be routed to specific experts (“expert” in this case being a set of feed forward layers.)</p><p>While the <em>maximum </em>number of experts is controlled by the people who designed the model, the decision about <em>which </em>vectors end up going to <em>which </em>experts is completely determined by the training process. And that process means that both the routing network and the feed forward networks belonging to the activated experts are adjusted during training, while the inactive expert networks are left untouched.</p><p>And speaking of training, we definitely gain performance improvements during both training and inference by using a MoE, since the embedding vectors only flow through the active experts for a particular vector. That means that many feed forward layers are unused during training and inference, which means less processing time, less electricity used, and much more efficient model operation. This is the secret to a MoE model.</p><h3>A Word About Training LLMs</h3><p>Although we’ve mentioned training a few times, we haven’t gone into depth about it. Let’s address that now.</p><p>As I mentioned earlier, during the training process many elements of the overall model are adjusted, from the feed forward layers to the router component to the attention mechanism in each block. All start out essentially random, and then are gradually adjusted over the course of the model training.</p><p>So how do you train an LLM? Well, unlike many other types of machine learning, you don’t need <strong>labelled data</strong>. That is, you don’t have to supply a correct answer for each item of input data, as you would for a classification model or something like that. For example, if you were training a model to identify images as either a dog or a cat, you’d typically have to provide hundreds of examples, each with the proper label.</p><p>With an LLM, the training method is completely different. Training an LLM uses text pulled from whatever sources were used, and the process is as simple as removing the last word of the text. Once that is done, the initial part of the text is used as input, and the output is then compared to the chopped-off last word.</p><p>Consider the example “Mary had a little lamb”, a line from a well-known nursery rhyme. If we pass in the string “Mary had a little” (i.e., without “lamb”) into a model, many possibilities could be returned for the next token, including “lamb”, “problem”, “dog”, etc. All of those answers are completely valid, but in this case we only want to match against “lamb”, since that’s the word our training text uses.</p><p>That means that if the model returns anything other than “lamb”, then the training process will determine that the model needs to be adjusted to make it more likely that it returns “lamb” for that input.</p><p>This is done via a process called <strong>back propagation</strong>, which essentially adjusts the parameters of a model from the bottom of the model up to the top, reducing the magnitude of the adjustments as it goes along. Each adjustment is small, but over the entire course of training a model, billions or trillions of adjustments are made, which is how the model learns.</p><p>But wait — since “Mary had a little problem” is a valid English sentence, why do we treat this as an incorrect answer? The answer is that over the entire set of training text, there will indeed be many examples that have “problem” as the next word to “Mary had a little”, and the training process will gradually adjust the model to handle that. Since there are many other options for the next word in that sequence, by the time training is complete, the weight (or prevalence) for one result over another will reflect the prevalence found in the training text itself.</p><h3>Mixture of Experts Pros and Cons</h3><p>Now that we’ve run through the difference between a traditional LLM and a MoE LLM, let’s summarize what an MoE model brings to the table.</p><ul><li>MoE models have more parameters than dense models, but only some are used during training and inference</li><li>Since only some parameters are used, this makes training and inference faster and much more efficient, while still producing high quality results</li><li>MoE models increase the complexity of a transformer block by adding a router and multiple separate feed forward networks</li><li>The routing mechanism can be thought of as a separate model, although it’s also trained at the same time as the rest of the feed forward layers and attention mechanisms.</li><li>It is theorized that the separation of topics into distinct experts can improve the overall quality of the model, since each expert subnetwork is more focused on the topic than a general dense model.</li></ul><p>Due to their efficiency, MoE models are becoming increasingly common. Although they have generally larger numbers of parameters (and thus require more storage), only some of those parameters are used at any given time, which saves time and money.</p><p>Although many companies that provide commercial models (like Google or OpenAI) typically do not reveal details of their model’s architecture or training methods, there are a number of well-known models that do use the MoE approach, including Grok-1, DeepSeek, Mixtral, and Qwen1.5-MoE. Additionally, it is believed that models like GPT-4, PaLM2 and Claude use an approach similar to MoE.</p><h3>Conclusion and Additional Resources</h3><p>In summary, we’ve explored how Mixture of Experts (MoE) models represent a significant advancement in large language model architecture, offering a solution to the challenges of ever-growing model sizes. By selectively activating subsets of their parameters based on the input query, MoE models enable faster training and inference while maintaining high-quality results.</p><p>This article demystified the core components of LLMs — tokenization, embeddings, attention mechanisms, and transformer blocks — and highlighted how MoE models diverge from traditional dense models through their routing mechanism and specialized “experts.” Although the architecture of a LLM is inherently complex, breaking it into smaller pieces makes it much more understandable.</p><p>The efficiency and potential for improved topic-focused understanding offered by MoE architectures suggest they will continue to be a crucial area of research and development. To see how researchers are already improving on this foundation, explore the concept of <a href="https://research.google/blog/mixture-of-experts-with-expert-choice-routing/"><strong>Mixture-of-Experts with Expert Choice Routing</strong></a>, a variation that focuses on improving the performance of the router component. This work highlights just one of the many exciting avenues for innovation in this field.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=58b3ba8e0349" width="1" height="1" alt=""><hr><p><a href="https://medium.com/google-cloud/how-mixture-of-experts-llms-work-58b3ba8e0349">How Mixture-of-Experts LLMs Work</a> was originally published in <a href="https://medium.com/google-cloud">Google Cloud - Community</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Finding Groundwater Using Google Earth Engine and Gemini]]></title>
            <link>https://medium.com/google-cloud/finding-groundwater-using-google-earth-engine-and-gemini-d5d355e49697?source=rss-a26d17de2fa6------2</link>
            <guid isPermaLink="false">https://medium.com/p/d5d355e49697</guid>
            <category><![CDATA[google-earth-engine]]></category>
            <category><![CDATA[groundwater]]></category>
            <category><![CDATA[remote-sensing]]></category>
            <category><![CDATA[google-gemini]]></category>
            <dc:creator><![CDATA[Greg Sommerville]]></dc:creator>
            <pubDate>Wed, 16 Jul 2025 17:47:22 GMT</pubDate>
            <atom:updated>2025-07-17T02:29:19.915Z</atom:updated>
            <cc:license>http://creativecommons.org/publicdomain/zero/1.0/</cc:license>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*VTk6wvtb6GaDWo4z" /><figcaption>Image of satellite over the Earth generated by Gemini</figcaption></figure><p>It’s safe to say that <a href="https://earthengine.google.com/">Google Earth Engine</a> (GEE) has changed the world for geospatial analysis. By providing access to a massive catalog of satellite imagery, it allows us to analyze our planet in ways that were previously unthinkable. Despite this, it’s often difficult to translate that raw data into a solution for a complex, real-world problem like locating viable groundwater sources.</p><p>This article presents a novel technique for using GEE’s infrared (IR) imagery to identify indicators of groundwater. By analyzing specific spectral patterns in the satellite data, we can significantly narrow down areas of interest, making groundwater exploration more targeted and efficient.</p><p>This article will demonstrate how to use Google Earth Engine to find indicators of groundwater. By the end, you’ll understand:</p><ul><li>Why Near-Infrared (NIR) imagery is a key tool for this task.</li><li>How remote sensing indices like NDVI and NDMI work.</li><li>How to use Gemini to analyze the resulting images to pinpoint promising locations.</li><li>The high-level steps to implement this on Google Cloud.</li></ul><p>Now that we understand the goal, let’s look at the remote sensing techniques we can use.</p><h3>Groundwater Detection</h3><p>To understand the solution, we first need to define some basic concepts. Groundwater is water held underground in soil or in rock crevices and cavities. It’s different from surface water, which includes things like lakes, rivers, and streams.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*yNX40zBXLjfMqrbq" /></figure><p>Why is groundwater important? For one, it’s a major source of drinking water. Beyond that, it’s used for agriculture and in various industries including manufacturing, mining, and energy production. Additionally, groundwater can help resupply low levels of surface water during times of drought.</p><h3>The Importance of Efficient Groundwater Detection</h3><p>So why is finding groundwater efficiently such a big deal? It comes down to a few key reasons. Traditional methods (often involving manual inspections) are slow, expensive, and hit-or-miss. A better approach using remote sensing offers clear advantages:</p><ul><li><strong>Addresses Water Scarcity:</strong> Quickly identifies new, sustainable water sources to support communities and agriculture, especially in drought-prone regions.</li><li><strong>Lowers Costs and Reduces Risk:</strong> Saves significant time and money by pinpointing the most promising locations for drilling, reducing the need for costly and often unsuccessful exploratory work.</li><li><strong>Minimizes Environmental Impact:</strong> Prevents unnecessary land disruption and protects existing ecosystems by making exploration targeted and precise.</li><li><strong>Supports Infrastructure Repair: </strong>Has the potential to identify large slow-water leaks that may go unnoticed for long periods of time.</li></ul><h3>How Remote Sensing Finds Groundwater</h3><p>The key to using remote sensing for this task lies in observing what we <em>can</em> see — vegetation and soil moisture — to infer what we <em>can’t</em> see underground. Healthy, well-hydrated vegetation in an otherwise arid area is often a strong indicator of a shallow groundwater source. To detect these patterns, we use specific wavelengths of light captured by satellites.</p><h3>Satellite Images and Near Infrared Wavelengths</h3><p>Near-Infrared (NIR) is a part of the electromagnetic spectrum that’s invisible to the human eye, with wavelengths slightly longer than visible light. It’s crucial in remote sensing because healthy vegetation strongly reflects NIR light while absorbing visible red light. This distinct reflection pattern allows scientists to assess vegetation health and density.</p><p>Google Earth Engine (GEE) provides extensive access to satellite imagery that includes both NIR and visible color bands. Satellites like Landsat and Sentinel-2 are key sources for this data. Within GEE, users can select specific satellite image collections and then choose the desired spectral bands (like Red, Green, Blue for visible color, and the NIR band) to analyze or visualize. This enables various applications, such as creating natural color images or false-color composites that highlight vegetation using the NIR band.</p><p>Here’s an example image that shows visible, NIR, and shortwave infrared (SWIR) images for the same person:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/864/0*x6mfom6OQc2Ax1i7" /></figure><h3>Using NDVI and NDMI to Find Groundwater</h3><p>To turn raw satellite data into actionable insights, we use <strong>remote sensing indices</strong> — mathematical combinations of different spectral bands. For groundwater detection, two of the most effective are the Normalized Difference Vegetation Index (NDVI) and the Normalized Difference Moisture Index (NDMI).</p><h3>Normalized Difference Vegetation Index (NDVI)</h3><p>NDVI is a widely used indicator of healthy, green vegetation. It’s calculated based on the difference between near-infrared (NIR) and red light reflected by plants. Healthy vegetation absorbs most visible red light while reflecting a large portion of NIR light.</p><ul><li><strong>How it’s calculated:</strong> NDVI=(NIR−Red)/(NIR+Red)</li><li><strong>Relevance to groundwater:</strong> Areas with higher groundwater availability often support more vigorous vegetation, especially in arid regions. Identifying pockets of high NDVI can help us infer subsurface water sources that are sustaining plant growth.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*mDXHbkPLabqZ_dk2thFqKw.png" /><figcaption>The same area shown in True Color and NDVI. Image created by author.</figcaption></figure><h3>Normalized Difference Moisture Index (NDMI)</h3><p>Also known as the Normalized Difference Water Index (NDWI), NDMI is used to assess the water content in vegetation. It utilizes the near-infrared (NIR) and shortwave-infrared (SWIR) bands, as water in plants absorbs SWIR light.</p><ul><li><strong>How it’s calculated:</strong> NDMI=(NIR−SWIR)/(NIR+SWIR)</li><li><strong>Relevance to groundwater:</strong> Elevated NDMI values indicate well-hydrated plants, which could be a direct result of access to shallow groundwater. This is especially useful for detecting subtle moisture differences that aren’t visible to the naked eye.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ZtFgQN_r0jpOB5YGeSwcfQ.png" /><figcaption>The same area shown in True Color and NDMI. Image created by author.</figcaption></figure><h3>How NDVI and NDMI help in groundwater exploration:</h3><p>By combining the insights from both NDVI and NDMI, we can develop a more comprehensive understanding of potential groundwater locations:</p><ul><li><strong>Identifying Water-Stressed Areas:</strong> Low NDVI and NDMI could indicate areas where vegetation is stressed due to lack of surface or groundwater, thus directing exploration away from such regions.</li><li><strong>Locating Phreatophytes:</strong> Certain plant species, known as phreatophytes, have roots that extend deep enough to reach the water table. These plants often exhibit high NDVI and NDMI values. Mapping clusters of these vigorous, well-hydrated plants can point to shallow groundwater reserves.</li><li><strong>Detecting Anomalies:</strong> Unexpectedly high NDVI or NDMI in an otherwise arid landscape can signal a hidden groundwater source. These anomalies might be indicative of springs, seeps, or areas where the water table is close to the surface.</li><li><strong>Monitoring Seasonal Changes:</strong> Analyzing how these indices change over different seasons can provide insights into the dynamics of groundwater. For example, if vegetation remains green and moist during dry seasons, it strongly suggests a reliable groundwater source.</li><li><strong>Complementary Data:</strong> These indices are most effective when used in conjunction with other geospatial data, such as geological maps, topographic data, and soil moisture information, to provide a more robust assessment of groundwater potential.</li></ul><h3>Interpretation of Remote Sensing Images</h3><p>As you can see above, both NDVI and NDMI produce images that are color-coded for the data they are displaying. We can look at a set of images of the same area over time to detect changes in groundwater indicators like these, but that’s a fairly manual process. Instead, the simplest approach is to provide the images to Gemini and ask it to examine them.</p><p>Using Gemini for the analysis of NDVI and NDMI images is an efficient approach to identifying groundwater indicators. Instead of manual visual inspection (which can be time-consuming and prone to human error), Gemini can be leveraged to process and interpret these remote sensing outputs.</p><p>By feeding the generated NDVI and NDMI images (or the underlying spectral data) into Gemini, the AI can be prompted to identify specific patterns, anomalies, and relationships indicative of groundwater presence. For instance, Gemini can be instructed to highlight areas with consistently high NDVI values in arid regions, especially during dry seasons, as this strongly suggests subsurface water sustaining the vegetation. Similarly, it can pinpoint regions with elevated NDMI values, indicating high moisture content in vegetation, which might be linked to shallow groundwater tables.</p><p>Additionally, Gemini can go beyond simple value thresholds by integrating temporal data. It can analyze sequences of NDVI and NDMI images collected over various seasons or years to detect subtle changes that reveal groundwater dynamics. For example, if an area shows a sustained high NDMI despite prolonged drought conditions, Gemini can identify this as a significant anomaly pointing to a resilient groundwater source. The AI’s ability to process vast amounts of imagery and identify complex, multi-variable correlations makes it an invaluable tool for groundwater exploration, allowing for more precise targeting of areas for further investigation and significantly reducing the time and resources traditionally required for such endeavors.</p><h3>Implementation Workflow in GEE</h3><p>At this point, we understand the theory. Now let’s walk through the high-level steps to implement this on Google Cloud. The basic process involves selecting a satellite data source, retrieving images for a specific location and time, and then generating our NDVI and NDMI indices.</p><p><strong>1. Accessing Google Earth Engine</strong></p><p>First, you’ll need to be able to make calls to the GEE platform. If you haven’t already, you can sign up on the<a href="https://earthengine.google.com/signup/"> GEE Developer page</a>. From there, you can work in the web-based Code Editor or use the Python API in a Jupyter notebook environment, which is what we’ll be doing here.</p><p><strong>2. Retrieving and Processing Imagery</strong></p><p>The core of the workflow is to query an image collection, like COPERNICUS/S2_SR_HARMONIZED from the Sentinel-2 satellite, which contains the spectral bands we need. We filter this collection by our region of interest and a specific date range.</p><p>The following Python code demonstrates this entire process. It defines functions to:</p><ul><li>Query GEE for relevant Sentinel-2 imagery for a given location and date.</li><li>Download the separate Red, Near-Infrared (NIR), and Shortwave-Infrared (SWIR) bands.</li><li>Calculate the NDVI and NDMI values from those bands.</li><li>Visualize the final indices as color-coded images ready for analysis.</li></ul><pre>import requests<br>import ee<br>import numpy as np<br>import datetime<br>import matplotlib.pyplot as plt<br>from PIL import Image<br><br>MY_PROJECT_ID = &quot;something&quot;<br>ee.Initialize(project=MY_PROJECT_ID)<br><br><br>def load_image(image_path):<br>    &quot;&quot;&quot;<br>    Loads an image and converts it to a floating point numpy array.<br>    &quot;&quot;&quot;<br>    img = Image.open(image_path).convert(&#39;RGB&#39;)<br>    img_array = np.array(img, dtype=np.float32) / 255.0  # Normalize to 0-1<br>    return img_array<br><br><br>def calculate_ndvi(nir, red):<br>    &quot;&quot;&quot;<br>    Calculates the Normalized Difference Vegetation Index (NDVI).<br>    &quot;&quot;&quot;<br>    numerator = nir - red<br>    denominator = nir + red<br>    ndvi = np.where(denominator != 0, numerator / denominator, 0)<br>    return ndvi<br><br><br>def calculate_ndmi(nir, swir):<br>    &quot;&quot;&quot;<br>    Calculates the Normalized Difference Moisture Index (NDMI).<br>    &quot;&quot;&quot;<br>    numerator = nir - swir<br>    denominator = nir + swir<br>    ndmi = np.where(denominator != 0, numerator / denominator, 0)<br>    return ndmi<br><br><br>def create_index_images(true_color_image, nir_image, swir_image, output_suffix, formatted_datetime):<br>    # Extract bands from the true color image<br>    red_band = true_color_image[:, :, 0]<br>    green_band = true_color_image[:, :, 1]<br>    # blue_band = true_color_image[:, :, 2]<br><br>    # Ensure NIR and SWIR are single-band images; if RGB, take one channel<br>    if nir_image.ndim == 3:<br>        nir_band = nir_image[:, :, 0]  # Take the first channel<br>    else:<br>        nir_band = nir_image  # assume already single band<br><br>    if swir_image.ndim == 3:<br>        swir_band = swir_image[:, :, 0]  # Take the first channel<br>    else:<br>        swir_band = swir_image  # assume already single band<br><br>    # Calculate indices<br>    ndvi_image = calculate_ndvi(nir_band, red_band)<br>    ndmi_image = calculate_ndmi(nir_band, swir_band)<br>    visualize_index(ndvi_image, f&#39;NDVI: {formatted_datetime}&#39;, f&#39;ndvi_{output_suffix}.png&#39;)<br>    visualize_index(ndmi_image, f&#39;NDMI: {formatted_datetime}&#39;, f&#39;ndmi_{output_suffix}.png&#39;)<br><br><br>def visualize_index(index_array, title, output_path, cmap=&#39;RdYlGn&#39;):<br>    &quot;&quot;&quot;<br>    Visualizes the index array using a colormap and saves the image.<br>    &quot;&quot;&quot;<br>    plt.ioff()  # Turn interactive mode off<br>    plt.figure(figsize=(10, 8))<br>    plt.imshow(index_array, cmap=cmap)<br>    plt.colorbar(label=title)<br>    plt.title(title)<br>    plt.savefig(output_path)<br>    plt.close()<br><br><br>def get_satellite_imagery(longitude, latitude, start_datetime_str, end_datetime_str, buffer_distance):<br>    # Convert datetime string to ee.Date<br>    start_date = ee.Date(start_datetime_str)<br>    end_date = ee.Date(end_datetime_str)<br><br>    # Create point geometry. Note that &quot;buffer_distance&quot; is in meters, unless a specific projection is specified<br>    point = ee.Geometry.Point([longitude, latitude])<br>    region = point.buffer(buffer_distance)<br><br>    # Get Sentinel-2 collection<br>    s2_collection = (<br>        ee.ImageCollection(&#39;COPERNICUS/S2_SR_HARMONIZED&#39;)<br>        .filterBounds(region)<br>        .filterDate(start_date, end_date)<br>        .sort(&#39;system:time_start&#39;)<br>    )<br>    return s2_collection, region<br><br><br>def save_multiband_imagery(image, region, base_filename, pixel_width):<br>    &quot;&quot;&quot;<br>    Save true color, NIR, and SWIR versions of the satellite imagery.<br><br>    Args:<br>        image (ee.Image): Earth Engine image object<br>        region (ee.Geometry): Region of interest<br>        base_filename (str): Base filename without extension<br>        image_scale (int): Image dimensions in pixels<br>    &quot;&quot;&quot;<br>    # Define visualization parameters for different band combinations<br>    vis_params = {<br>        &#39;true_color&#39;: {<br>            &#39;bands&#39;: [&#39;B4&#39;, &#39;B3&#39;, &#39;B2&#39;],<br>            &#39;min&#39;: 0,<br>            &#39;max&#39;: 3000,<br>            &#39;filename&#39;: f&quot;true_color_{base_filename}.png&quot;,<br>            &#39;scale&#39;: 10<br>        },<br>        &#39;nir&#39;: {<br>            &#39;bands&#39;: [&#39;B8&#39;],<br>            &#39;min&#39;: 0,<br>            &#39;max&#39;: 3000,<br>            &#39;filename&#39;: f&quot;nir_{base_filename}.png&quot;,<br>            &#39;scale&#39;: 10<br>        },<br>        &#39;swir&#39;: {<br>            &#39;bands&#39;: [&#39;B11&#39;],<br>            &#39;min&#39;: 0,<br>            &#39;max&#39;: 3000,<br>            &#39;filename&#39;: f&quot;swir_{base_filename}.png&quot;,<br>            &#39;scale&#39;: 20<br>        }<br>    }<br><br>    # Save each band combination<br>    fnames = []<br>    for band_type, params in vis_params.items():<br><br>        thumb_params = {<br>            &#39;region&#39;: region,<br>            &#39;format&#39;: &#39;png&#39;,<br>            &#39;bands&#39;: params[&#39;bands&#39;],<br>            &#39;min&#39;: params[&#39;min&#39;],<br>            &#39;max&#39;: params[&#39;max&#39;],<br>            &#39;dimensions&#39;: pixel_width<br>        }<br><br>        url = image.getThumbURL(thumb_params)<br>        response = requests.get(url)<br>        if response.status_code == 200:<br>            with open(params[&#39;filename&#39;], &#39;wb&#39;) as f:<br>                f.write(response.content)<br>            fnames.append(params[&#39;filename&#39;])<br>        else:<br>            print(f&quot;Failed to download {band_type} image. Status code: {response.status_code}&quot;)<br><br>    return fnames<br><br><br>def main():<br>    # define our date range and the location to examine<br>    start_datetime_str = &#39;2023-12-25&#39;<br>    end_datetime_str = &#39;2024-01-15&#39;<br>    latitude, longitude = 35.089248, -106.637810<br><br>    # center on the coord, within a box (buffer_distance is in meters, and talks about space around center point)<br>    collection, region = get_satellite_imagery(<br>        longitude, latitude,<br>        start_datetime_str,<br>        end_datetime_str,<br>        buffer_distance=500,  # this gives us 1 KM square bitmap<br>    )<br><br>    collection_info = collection.getInfo()<br>    num_images = len(collection_info[&#39;features&#39;])<br>    print(f&quot;Number of images in the collection: {num_images}&quot;)<br><br>    for index, feature in enumerate(collection_info[&#39;features&#39;]):<br>        image_id = feature[&#39;id&#39;]           # Get the image ID<br>        image = ee.Image(image_id)<br>        info = image.getInfo()<br><br>        # Convert milliseconds to seconds and create a datetime object<br>        image_datetime = info[&#39;properties&#39;][&#39;GENERATION_TIME&#39;]<br>        datetime_object = datetime.datetime.fromtimestamp(image_datetime / 1000)<br>        formatted_datetime = datetime_object.strftime(&quot;%Y-%m-%d %H:%M:%S&quot;)<br><br>        truecolor_fname, nir_fname, swir_fname = save_multiband_imagery(image, region, f&quot;{index}&quot;, pixel_width=100)<br><br>        truecolor = load_image(truecolor_fname)<br>        nir = load_image(nir_fname)<br>        swir = load_image(swir_fname)<br><br>        print(f&#39;Creating index images for image {index}, taken on {formatted_datetime}&#39;)<br>        create_index_images(truecolor, nir, swir, f&quot;{index:03d}&quot;, formatted_datetime)<br><br><br>if __name__ == &quot;__main__&quot;:<br>    main()</pre><h3>Conclusion</h3><p>This article has demonstrated the power of Google Earth Engine (GEE) and remote sensing indices in the important task of groundwater exploration. We’ve seen how leveraging GEE’s satellite imagery datasets, particularly those including Near-Infrared (NIR) and Shortwave Infrared (SWIR) bands, allows for the calculation of indices like NDVI and NDMI. These indices provide invaluable insights into vegetation health and moisture content, which serve as strong indicators of underlying groundwater reserves. By moving beyond traditional, labor-intensive methods, this approach offers a more efficient, cost-effective, and environmentally friendly way to pinpoint areas with high groundwater potential, ultimately contributing to better water resource management and addressing the challenges of water scarcity.</p><p>The integration of advanced AI tools like Gemini further amplifies the capabilities of this remote sensing methodology. Gemini’s ability to analyze complex patterns, anomalies, and temporal changes within NDVI and NDMI imagery transforms a manual interpretation process into an automated, data-driven assessment. This not only accelerates the identification of promising groundwater locations but also enables a more nuanced understanding of groundwater dynamics over time. By combining the rich data available through GEE with intelligent analytical platforms, we can significantly enhance our capacity to discover, monitor, and sustainably manage this indispensable natural resource for the benefit of communities and ecosystems worldwide.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d5d355e49697" width="1" height="1" alt=""><hr><p><a href="https://medium.com/google-cloud/finding-groundwater-using-google-earth-engine-and-gemini-d5d355e49697">Finding Groundwater Using Google Earth Engine and Gemini</a> was originally published in <a href="https://medium.com/google-cloud">Google Cloud - Community</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to Implement Hybrid Search for RAG with BigQuery]]></title>
            <link>https://medium.com/google-cloud/how-to-implement-hybrid-search-for-rag-with-bigquery-c7278e630b52?source=rss-a26d17de2fa6------2</link>
            <guid isPermaLink="false">https://medium.com/p/c7278e630b52</guid>
            <category><![CDATA[gemini]]></category>
            <category><![CDATA[google-cloud-platform]]></category>
            <category><![CDATA[data]]></category>
            <category><![CDATA[bigquery]]></category>
            <category><![CDATA[hybrid-search]]></category>
            <dc:creator><![CDATA[Greg Sommerville]]></dc:creator>
            <pubDate>Tue, 18 Mar 2025 21:34:53 GMT</pubDate>
            <atom:updated>2025-03-19T13:11:40.188Z</atom:updated>
            <cc:license>http://creativecommons.org/publicdomain/zero/1.0/</cc:license>
            <content:encoded><![CDATA[<p><em>How to implement both keyword and vector search with BigQuery</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*l7P8ys1KU8YIzrJlX_QC3g.jpeg" /><figcaption>Generated by Imagen 3</figcaption></figure><p>At this point, it seems like we’re all familiar with the idea of using RAG (<em>Retrieval Augmented Generation) </em>as part of a LLM-based chatbot.</p><p>With a RAG approach, we retrieve data from a datastore based on a user query, add that data to our prompt, and then pass that prompt to a model like Gemini. Since we supply (hopefully) relevant information in the prompt itself, the RAG approach drastically reduces the likelihood of hallucinations that can result in incorrect answers.</p><p>As great as that is, there are certain use cases where a typical RAG approach falls short. For example, creating a chatbot that can help users browse and search a product catalog is one use case where a traditional RAG approach won’t work well. Let’s dive in to understand why.</p><h3>Shortcomings of Traditional RAG</h3><p>A traditional RAG approach typically relies on using a vector (also known as semantic) search only, which searches based on similar meaning, rather than exact words.</p><p>Vector search works by converting a search query into a <em>vector </em>(a list of numbers) that encodes the meaning of the query. Mathematically, vectors with similar meanings should be numerically close to each other, so we can simply look for vectors that are numerically as close to the query vector as possible, which should (theoretically) give us results that are most similar in meaning to the search vector, and therefore most relevant.</p><p>That’s great as long as you want to search by meaning, but it falls short when trying to search for words like “Google” or “HP” or “Ricoh” or something like that. Those brand names don’t have meanings per se, so searching for something of similar meaning doesn’t generally work well. In this case, a vector search alone isn’t enough — we need a hybrid search that combines keyword and vector searches.</p><p>This article will demonstrate how to use Google Cloud <a href="https://cloud.google.com/bigquery">BigQuery</a> as a datastore for RAG. By the end of the article, you’ll understand:</p><ul><li>When to use a hybrid search instead of a vector-only search</li><li>How to implement vector and keyword search using BigQuery</li><li>How to call Vertex AI to obtain embeddings for vector search</li><li>The main ideas involved in creating a chatbot that allows searching through a large product catalog (as an example)</li></ul><h3>Why BigQuery?</h3><p>One of the choices that must be made when designing a RAG solution is choosing which datastore to use. For a catalog solution I recently created, I chose Google Cloud’s BigQuery (BQ) as the datastore for product information</p><p>Although BQ is a data warehouse and data warehouses are typically used for analytics, in this case you can think of it as a regular SQL database optimized for queries, which makes it very effective as a datastore for a RAG hybrid search.</p><p>As a matter of fact, BigQuery works well as a vector database, a keyword-based search database, or a hybrid combination of both (which is what we will discuss in this article.)</p><p>Here are a number of reasons for choosing BigQuery for RAG:</p><ul><li>BigQuery is designed to handle petabyte-scale datasets. This is crucial for RAG applications that might need to index and search through vast amounts of text and vector embeddings (like a large product catalog.)</li><li>BigQuery includes built-in support for vector search. It also supports keyword search through standard SQL queries (using the LIKE keyword and wildcard expressions.)</li><li>BigQuery’s columnar storage and distributed query processing enable extremely fast query execution, even on large datasets. This translates to low latency in retrieving relevant information for your RAG model.</li><li>BigQuery has excellent integration with other Google Cloud Services, like Cloud Storage, Vertex AI, and Cloud Functions. This simplifies the development and deployment of your RAG application.</li><li>BigQuery provides robust data governance and security features, ensuring that your data is protected and managed effectively.</li><li>BigQuery is ultimately a data warehouse, which means if your RAG application does require data analysis and reporting, BigQuery’s capabilities can be leveraged to gain valuable insights.</li><li>BigQuery uses a Serverless Architecture. BigQuery’s serverless nature eliminates the need for infrastructure management, allowing you to focus on building your RAG application.</li></ul><p>For those reasons, BigQuery was an easy choice for the RAG datastore.</p><h3>Storing Products in BigQuery</h3><p>A first step for any RAG solution is to collect and index data. Since we’re creating a catalog chatbot, in this case we need to store information about the available products and services. Here’s what we need to include:</p><ul><li>Product Name</li><li>Product Description</li><li>Product Category</li><li>Filename that the product information came from</li><li>Keywords for the product</li><li>Embeddings for the product (calculated from extracted information)</li></ul><p>A few notes about these fields:</p><ul><li>The <strong>product name and description </strong>are simple text descriptions of a product.</li><li>The <strong>product category </strong>is important since we can filter product searches based on our current category. Each product should have a single product category associated with it.</li><li>The <strong>filename of the source PDF </strong>is important since it allows us to remove products in the future. This would be done by using a Cloud Run Function to detect the removal of a file from a storage bucket, which would then remove all of the associated products and services. This allows our users to change and update the list of products simply by adding or removing files from a storage bucket.</li><li><strong>Keywords </strong>are quite important when dealing with products and services. This is the field that will allow us to do keyword matching, as with brand names.</li><li>Finally, <strong>embeddings </strong>are calculated by combining the product name and description into a string, and then create an embedding for that string. This allows our users to search based on the meaning of the words in the product description.</li></ul><p>As we mentioned earlier, we can use BigQuery to store all product information. Here’s a schema for what we’re going to store:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/349/0*7hUvPsOMLVz4pK41" /></figure><h3>Generating Embeddings</h3><p>There are a variety of ways to create embeddings, including using the built-in BigQuery function <a href="https://cloud.google.com/bigquery/docs/generate-text-embedding">GENERATE_EMBEDDING()</a>. However, in this case we’ll call Vertex AI to generate them, using some Python code.</p><p>That code would normally be part of a larger overall RAG solution, which handles both ingesting and storing product information into BigQuery, and also the actual chatbot operation when you retrieve data from BigQuery in order to respond to the user.</p><p>Note that there are two functions to generate embeddings: one for embedding the text that describes a product, and another for generating embeddings to be used during retrievals.</p><pre>from vertexai.preview.language_models import TextEmbeddingModel, TextEmbeddingInput<br><br>EMBEDDING_MODEL_ID = &quot;text-embedding-005&quot;<br><br>def get_embeddings_for_storage(title: str, text: str) -&gt; List[float]:<br>    model = TextEmbeddingModel.from_pretrained(EMBEDDING_MODEL_ID)<br>    text_embedding_input = TextEmbeddingInput(<br>        task_type=&#39;RETRIEVAL_DOCUMENT&#39;,<br>        title=title,<br>        text=text)<br>     embeddings = model.get_embeddings([text_embedding_input])<br>     return embeddings[0].values<br><br>def get_embeddings_for_retrieval(text: str) -&gt; List[float]:<br>     model = TextEmbeddingModel.from_pretrained(EMBEDDING_MODEL_ID)<br>     text_embedding_input = TextEmbeddingInput(<br>        task_type=&#39;RETRIEVAL_QUERY&#39;,<br>        text=text<br>     )<br>     embeddings = model.get_embeddings([text_embedding_input])<br>     return embeddings[0].values</pre><p>It’s important to notice that there are differences in the value of the <strong>task_type</strong> parameter, which indicates why we are generating the embeddings. RETRIEVAL_DOCUMENT is for the initial embedding operation, and the code takes both a title and body text. RETRIEVAL_QUERY, on the other hand, is used when getting embeddings for retrieval.</p><h3>Product Categories</h3><p>One of the challenges that RAG systems have is ensuring that they retrieve the right information. Even when you use embeddings (and potentially keywords), you can end up retrieving too much information, or the wrong information. This is especially true when dealing with very large amounts of data. Imagine that our catalog contains hundreds of thousands of products — we want to ensure that we are retrieving only the most relevant.</p><p>By assigning a category to each product, and then also understanding which product category the user is asking about, we can filter the product data to be highly relevant.</p><p>In this example, we’re putting together a chatbot for IT products and services, so we’ll start with the following categories:</p><pre>Hardware / Networking<br>Hardware / Servers<br>Hardware / Storage<br>Hardware / Printers<br>Hardware / Telecommunications<br>Hardware / Desktop computers<br>Hardware / Laptops and Tablets<br>Hardware / Computer Accessories<br>Software / Virtualization and Operating Systems<br>Software / Management Software<br>Software / Database Systems<br>Software / Telecommunication Systems<br>Software / Productivity Software<br>Software / Security Software<br>Software / Other Software<br>Services / Cloud Computing<br>Services / Network Services<br>Services / End-User Support<br>Services / Staffing<br>Services / Telecommunication</pre><p>Each category is simply a string. Although this example uses two levels in its taxonomy, you can create your own list of categories in any way you wish, with any number of layers. Use whatever structure makes the most sense for your use case. Just make sure that categories don’t overlap, or else the model will have a hard time determining which category is the current one.</p><h3>Understanding the Current Category</h3><p>During the conversation with the user, we can have Gemini look at the list of categories and the conversation history in order to determine which category is the current topic of conversation. We do this using the following prompt:</p><pre>You are an expert in the field of IT provisioning and supplies.<br><br>Each product or service that can be found in the catalog has a category. <br>Those categories are listed below.<br><br>**Categories:**<br>{categories}<br><br>Look at the following conversation between a potential buyer and an AI guide, <br>and pay close attention to the latest part of the conversation. <br>You should return the category of the product or service that the buyer is <br>looking for.<br><br>**Conversation:**<br>{conversation}<br><br>It&#39;s possible that the buyer may be asking questions that are very generic <br>and not directly related to a particular category. <br>For example, if they ask about &quot;Software&quot;,but don&#39;t specify what kind of <br>software they are looking for, you should return &quot;Software&quot;.<br><br>However, if they are asking about a specific category, you should return <br>the category. For example, if they are asking about hardware servers, <br>you should return &quot;Hardware / Servers&quot;.<br><br>If you can&#39;t figure out what category the buyer is looking for, <br>you should return &quot;Unknown&quot;.</pre><p>Notice how we check if the conversation is still somewhat generic (meaning the user hasn’t been specific enough about what they are looking for), we get a category of “Unknown”, which we can interpret as a trigger to ask Gemini to provide general information about the categories and ask the user for more detail.</p><p>However, once we know which category the user is interested in, we can ask Gemini to extract likely search keywords based on their query. Along with that, we can also ask Gemini to give us a short string that describes what the user is searching for. We take that string and turn it into a vector embedding.</p><p>Then, using the current category, the current product keywords, and the embedding for the user query, we can perform our hybrid search.</p><h3>Retrieving Products From BigQuery</h3><p>At this point, we have the user query, its corresponding embeddings (calculated separately), a list of keywords that should help with searching, and a current category name. Here’s how we do the retrieval of the relevant products:</p><pre>def get_products(current_category: str, <br>                 keywords: str, <br>                 embedding: List[float]) -&gt; str:<br><br>     # using a combination of keywords and the embeddings, search<br>     # through the relevant category and return product information as a<br>     # string that can be included in the prompt<br>     # set up our keyword query so it&#39;s like:<br>     # SELECT…WHERE LOWER(keywords) LIKE ANY (&#39;%hp%&#39;, &#39;%color%&#39;)<br>     # keywords come in looking like: &#39;&quot;HP&quot;, &quot;color printer&quot;, &quot;etc&quot;&#39;<br>     keywords = [f&quot;&#39;%{k.lower().strip()}%&#39;&quot; for k in keywords.split(&#39;,&#39;)]<br>     keyword_match_string = &quot;, &quot;.join(keywords)<br><br>     if keyword_match_string:<br>           QUERY = (<br>              &#39;SELECT product_name, product_description &#39;<br>              &#39;FROM `dataset.products` &#39;<br>              f&quot;WHERE category = &#39;{current_category}&#39; AND &quot;<br>              &quot;LOWER(keywords) LIKE ANY ({keyword_match_string}) &quot;<br>              &#39;LIMIT 10&#39;)<br>           query_job = bqclient.query(QUERY)<br>           keyword_rows = query_job.result()<br>     else:<br>           keyword_rows = []<br><br>     # notice the different SELECT when dealing with a vector search.<br>     # Also, need to select &quot;base.product_name&quot; (etc.)<br>     # because that&#39;s how it&#39;s returned (there are other vector-related <br>     # fields also returned, which we ignore)<br>     QUERY = (<br>           &#39;SELECT base.product_name, base.product_description &#39;<br>           &quot;FROM VECTOR_SEARCH(&quot;<br>           f&quot;(SELECT * from dataset.products where category = &#39;{current_category}&#39;), &quot;<br>           &quot;&#39;embeddings&#39;, &quot;<br>           f&#39;(SELECT {embedding} as embed), &#39;<br>           &#39;top_k =&gt; 10, &#39;<br>           &quot;distance_type =&gt; &#39;COSINE&#39;);&quot;<br>     )<br>     query_job = bqclient.query(QUERY)<br>     vector_rows = query_job.result()<br>  <br>     # now combine the results from the keyword search and the vector search<br>     product_rows = list(vector_rows) + list(keyword_rows)<br>     if len(product_rows) == 0:<br>         return &quot;No products found&quot;<br><br>     final_prods = [f&quot;**Product: {prod.product_name}**: {prod.product_description}&quot; for prod in product_rows]<br>     return &quot;\n&quot;.join(final_prods)</pre><p>The product retrieval is done in two steps: retrieving products by keyword, then by embeddings. Once both sets are retrieved, we combine the two and create a string that lists out the products.</p><p>Note that we pass in the keywords to this function as a single string separated by a comma. This is how we asked Gemini to return it, so we need to modify it a bit to make it work with our SQL SELECT statement.</p><p>First, we split and clean each keyword by turning it into lower case and stripping surrounding blanks. Then we surround each keyword with percentage symbols, since that signifies a wildcard match. Finally, we combine everything into a single SQL statement that may look something like this:</p><pre>SELECT product_name, product_description<br>FROM `dataset.products`<br>WHERE category = &#39;Hardware / Printers&#39;<br>AND LOWER(keywords) LIKE ANY (&#39;%hp%&#39;, &#39;%ricoh%&#39;)<br>LIMIT 10</pre><p>Notice how we first select only products that match our current category, which drastically reduces the possible matches and makes the selection more accurate. Then we use the LIKE ANY condition to match the keywords against a list of wildcard patterns, and limit our results to the first ten rows returned.</p><p>The second query uses BigQuery’s built-in vector search. Here’s what the vector selection statement looks like:</p><pre>SELECT base.product_name, base.product_description<br>FROM VECTOR_SEARCH(<br>  (SELECT * from dataset.products where category = &#39;Hardware / Printers&#39;),<br>  &#39;embeddings&#39;,<br>  (SELECT {embedding} as embed),<br>  top_k =&gt; 10,<br>  distance_type =&gt; &#39;COSINE&#39;<br>)</pre><p>You can see that the structure of the SQL SELECT statement is quite different from a standard SQL query. In this case, we use the VECTOR_SEARCH() function, which takes a subquery (for the data to search), the field name of the embeddings column (“embeddings”, in our case), the embeddings to match on, and parameters like <strong>top_k</strong> and <strong>distance_type</strong> (which is how embedding vectors are compared to each other.)</p><p>Finally, notice that we select “base.product_name” and “base.product_description” rather than just “product_name” and “product_description”. This is because the vector search returns a number of fields, and the “base.” prefix refers to the fields from the products table, rather than other data that is related to the vector search.</p><p>One last thing to talk about is using an index for performance reasons. Although indexing is typically used to improve retrieval speed, it’s mostly automatic in BigQuery for traditional queries. However, BigQuery does support indexes for vector search, and this is recommended once your set of potential matches (i.e., the number of products in the table) gets very large. See this web page for more information: <a href="https://cloud.google.com/bigquery/docs/vector-index">https://cloud.google.com/bigquery/docs/vector-index</a></p><h3>Conclusion</h3><p>When you need a RAG data source (hybrid or otherwise), BigQuery is a very effective solution. With its built-in vector search capabilities, practically unlimited scalability, and fast querying capabilities, it can be a powerful tool when building a RAG LLM solution.</p><p>Although keyword search and vector search are two distinct approaches, BigQuery can support both as part of a hybrid solution by using the techniques described in this article.</p><p>For more information about using vector search with BigQuery, please see <a href="https://cloud.google.com/bigquery/docs/vector-search-intro">this web page</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c7278e630b52" width="1" height="1" alt=""><hr><p><a href="https://medium.com/google-cloud/how-to-implement-hybrid-search-for-rag-with-bigquery-c7278e630b52">How to Implement Hybrid Search for RAG with BigQuery</a> was originally published in <a href="https://medium.com/google-cloud">Google Cloud - Community</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Unlocking PDFs for RAG with Markdown and Gemini]]></title>
            <link>https://medium.com/google-cloud/unlocking-pdfs-for-rag-with-markdown-and-gemini-503846463f3f?source=rss-a26d17de2fa6------2</link>
            <guid isPermaLink="false">https://medium.com/p/503846463f3f</guid>
            <category><![CDATA[gcp-app-dev]]></category>
            <category><![CDATA[generative-ai]]></category>
            <category><![CDATA[rags]]></category>
            <category><![CDATA[google-cloud-platform]]></category>
            <category><![CDATA[gemini]]></category>
            <dc:creator><![CDATA[Greg Sommerville]]></dc:creator>
            <pubDate>Wed, 18 Dec 2024 16:56:13 GMT</pubDate>
            <atom:updated>2024-12-19T14:35:48.968Z</atom:updated>
            <cc:license>http://creativecommons.org/publicdomain/zero/1.0/</cc:license>
            <content:encoded><![CDATA[<h3>Unlock PDFs for RAG with Markdown and Gemini</h3><figure><img alt="An illustration showing a PDF document on the left, an arrow with the word Gemini leading from the PDF to a Markdown document." src="https://cdn-images-1.medium.com/max/1024/1*KWoWZylZN9Gkd1gM56NtFw.png" /><figcaption>Created by Imagen 3</figcaption></figure><p>It’s safe to say that Retrieval Augmented Generation (RAG) has changed the world for many businesses and organizations. By supplementing the built-in capabilities that an LLM like <a href="https://deepmind.google/technologies/gemini/">Gemini</a> has with your own information, you can create extremely powerful experiences that are truly transformative.</p><p>Despite this, it’s often difficult to create a RAG application that works well with complex unstructured documents like PDFs.</p><p><strong>This article presents a novel technique for extracting text from PDFs in Markdown format, leading to improved accuracy and richer context in Retrieval Augmented Generation (RAG) applications.</strong></p><p>Markdown isn’t just for output. Using Markdown in your prompts can dramatically improve the quality of the model’s responses due to the added nuance it provides compared to plain text.</p><h3>The problem with PDFs</h3><p>PDFs are notoriously difficult to work with. Each document can have a wide variety of layouts, including multiple columns of text, or even text that seems to be randomly distributed on a page. Since PDFs support not only text but also images, some pages may look like text but are actually represented as images. Additionally, PDFs often contain tabular data which can be quite challenging to parse. Finally, it’s quite difficult to extract text from a PDF that also retains formatting information like bold, italics, and bullet points. By extracting only the text, you lose meaning and nuance that were in the original document.</p><p>Each of these situations makes it difficult to use PDFs in a RAG application. There are of course a number of Python libraries available that are designed to work with PDF documents like PyPDF, PDFPlumber, or PDFMiner, but almost none of them handle all of the complex situations described above. Depending on the source document, all of these libraries can produce text that’s incomplete or even completely incorrect.</p><p>Recently some new approaches have been introduced that use ML models (like <a href="https://github.com/DS4SD/docling">Docling</a>) to parse PDFs, but they can be extraordinarily slow, and aren’t usable for PDFs beyond just a few pages. (In one test I recently ran on my laptop, it took Docling 18 minutes to parse a 12 page document.)</p><p>This blog post describes a new technique to read in PDF files and quickly and efficiently generate accurate corresponding Markdown using Gemini and Google Cloud. The resulting Markdown is well-suited for indexing into a RAG datastore.</p><h3>A word about Markdown</h3><p><a href="https://en.wikipedia.org/wiki/Markdown">Markdown</a> is a simple and compact markup language. Markdown employs a simpler syntax than HTML and CSS, focusing on a limited set of stylistic elements: headings, bold text, italic text, hyperlinks, bullet points, and simple tables.</p><p>Most LLMs such as Gemini create output that uses Markdown, and the styling that it provides is extremely helpful in reader comprehension. Having actual bullet points is vastly superior to a plain-text alternative like using a hyphen at the start of a line, and bold and italicized text can make important information really stand out. Beyond that, Markdown’s ability to organize information into a table can be quite helpful.</p><p>Perhaps less intuitively, Markdown is also extremely useful when creating prompts. By selectively highlighting key phrases in your prompt, or organizing information into bulleted lists, we provide the model with more information than just the text content, which improves the model’s understanding and helps it focus on the task at hand.</p><p>Even so, it’s important to remember that Markdown is a simple language, and may not support everything you can store in a PDF. For example, Markdown tables do not support spanned rows or columns, which are often found in table headers. That’s important to keep in mind as you test this new approach, since it will affect the accuracy of your extraction for certain PDF files.</p><p>Regardless of these limitations, having the ability to extract a PDF’s content as Markdown can be extremely helpful when working on a RAG application. During the chunking and indexing process, you can use the headers to understand sections and subsections, which allows chunking documents into discrete topics. Similarly, tabular data arranged in a Markdown table can help the model understand the content much more easily than using plain text.</p><p>To sum up, it’s clear that using Markdown extracted from a PDF can dramatically improve the quality of Gemini’s responses due to the added nuance it provides compared to plain text. Beyond that, it also helps in terms of chunking documents during the RAG ingestion process, since you can use cues like headers to detect logical sections within the document.</p><p>Now that we understand how Markdown can help, let’s look at the process to extract it from PDF documents.</p><h3>How To Extract Markdown From a PDF</h3><p>In simple terms, here’s the process for extracting Markdown from a PDF document:</p><ul><li>For each page in the PDF:<br>- Create an image of the page<br>- Pass that image to Gemini, with a prompt asking it to extract the content of the page as Markdown</li><li>Once all of the individual pages have been processed, combine the markdown from all of the pages into a single Markdown string.</li></ul><p>This approach works quite well. Here’s an example, using a page from the instructions for the <a href="https://tax.illinois.gov/content/dam/soi/en/web/tax/forms/incometax/documents/currentyear/individual/il-1040.pdf">state of Illinois tax form 1040</a>. Notice that the page is split into two columns, and the top half of the page is completely separate from the bottom half:</p><figure><img alt="A page from Illinois State Tax form 1040 showing multiple columns and two main sections — one above the other" src="https://cdn-images-1.medium.com/max/1024/0*A1Ryg_j_bWbAHYrT" /><figcaption>A page from IL Form 1040, showing multiple columns and sections</figcaption></figure><p>And here’s the corresponding Markdown generated by Gemini, rendered so you can see the use of bullet points, headers, and the like:</p><figure><img alt="Rendered Markdown (part 1)" src="https://cdn-images-1.medium.com/max/793/0*7XFQDStvA8Gds8Cg" /></figure><figure><img alt="Rendered Markdown (part 2)" src="https://cdn-images-1.medium.com/max/785/0*UPz4Pq4eygFcCnLc" /></figure><figure><img alt="Rendered Markdown (part 3)" src="https://cdn-images-1.medium.com/max/797/0*AUMp0DlHdHIzuT9X" /></figure><figure><img alt="Rendered Markdown (part 4)" src="https://cdn-images-1.medium.com/max/764/0*UDIxS5mXEjKPhDAa" /><figcaption>Markdown extracted from the IL 1040 page</figcaption></figure><p>As you can see, the quality of the extracted markdown is very good, as it generally reflects how a human being would read the page. Notice that “Step 2” (the top half of the page) is described fully before “Step 3” (the bottom half.)</p><p>Additionally, markdown is produced that designates bullet point lists, bolded text, headings, and more. All of this adds meaning to the raw text that is extracted, which will typically produce better results when passing this markdown to Gemini. And, as stated earlier, having headings and subheadings helps us chunk a document into logical groupings, which will help with the RAG retrieval process.</p><h3>Implementation Details</h3><p>Depending on your use case, you could simply loop through each page within a PDF, extract a page image, and then pass it to Gemini in order to obtain the markdown. However, when approaching this problem, it’s good to think about scaling.</p><p>On my laptop, extracting an image for the above example page took 0.140 seconds, so that part of the algorithm is extremely quick. However, calling Gemini 1.5 Flash to extract the Markdown took 23.857 seconds, which can quickly add up for longer PDF documents.</p><p>Luckily, this problem fits very well with a <a href="https://en.wikipedia.org/wiki/MapReduce">map-reduce</a> approach. This approach first splits work into multiple parts, each of which runs in parallel. That part is called the <strong>map</strong> step. Then, when all of the parallel parts are complete, the results are combined or aggregated, which is called the <strong>reduce </strong>step.</p><p>In our case, we can process each page separately and then combine the markdown for all of the pages once all pages are processed. By leveraging Google Cloud, we can distribute the work using a <a href="https://cloud.google.com/pubsub?hl=en">PubSub </a>topic, and process each page using a <a href="https://cloud.google.com/functions?hl=en">Cloud Run Function</a>. Here’s a diagram that illustrates this approach:</p><figure><img alt="Architecture diagram showing a GCS bucket, a Cloud Function that is triggered when a file is placed into the bucket, and a PubSub topic that distributes work across a set of cloud functions." src="https://cdn-images-1.medium.com/max/1024/0*KfHSQiozPJK1UeSZ" /><figcaption>Architecture Diagram showing the PDF processing approach</figcaption></figure><p>Reading from left to right, these steps are taken:</p><ol><li>When a PDF file is placed in a Google Cloud Storage bucket, it causes a Cloud Function to be run.</li><li>That function copies the PDF from the bucket to the function’s local storage, then opens it simply to determine how many pages it contains. Then, for each page, the function writes a small JSON item to the PubSub topic, which contains the name of the PDF, the page number to process (from 0 to N — 1, where there are N pages), and the total number of pages found in the PDF.</li><li>The Page Handler cloud function is triggered when a new item shows up in the PubSub topic. Note that several invocations of this function can be run at the same time through the <a href="https://cloud.google.com/functions/docs/configuring/concurrency">parallel processing facilitated by Cloud Run</a>. You can specify the maximum concurrency when configuring the function.</li><li>The function copies the PDF from the bucket to the function’s local storage, opens the PDF, renders an image for the page in question (that is, the page number in the JSON data retrieved from the topic), and then calls Gemini via the Vertex AI API to get the Markdown.</li><li>Once the page Markdown is obtained from Gemini, it is stored in a BigQuery table, which has fields for the file name, the page number, and the extracted markdown string.</li></ol><p>These steps extract markdown for each page (the <strong>map</strong> part of map-reduce), but we still need to address the <strong>reduce</strong> step where all of the individual page markdown is combined into a single string.</p><p>In this case, the simplest approach is to have the page handler function check if it is the last page in the document. By counting the number of pages in the BigQuery table for the given document, we can determine if all processing is complete (which is why we passed the total number of pages in as part of the data on the PubSub topic.)</p><p>In short, after the page handler function finishes processing the page, it counts the number of completed pages from the BigQuery table for the document in question, and if it matches the total number of pages, then all of the individual page markdown strings are retrieved (ordered by page number) and combined into a single string. At that point we can store the document Markdown in a file, or perform more processing (such as using the extracted Markdown as part of another prompt sent to Gemini) if desired.</p><h3>Implementation Code</h3><p>First, let’s look at the code for the PDF file handler — the function that is invoked when a PDF file is placed in a bucket. We use the PDF library <a href="https://pypi.org/project/pypdfium2/">PyPdfium </a>to count the number of pages.</p><pre>from google.cloud import storage, pubsub_v1<br>import os<br>from typing import Callable<br>from concurrent import futures<br>import pypdfium2 as pdfium<br>import json<br><br># project ID<br>project_id = os.getenv(&quot;PROJECTID&quot;)<br># the pubsub topic we&#39;re writing to<br>pubsub_topicname = os.getenv(&quot;TOPICNAME&quot;)<br>publisher = pubsub_v1.PublisherClient()<br>topic_path = publisher.topic_path(project_id, pubsub_topicname)<br><br><br>def handle_new_file(event, context):<br>    # copy file from cloud storage into local storage<br>    bucketname = event[&#39;bucket&#39;]<br>    filename = event[&#39;name&#39;]<br>    if filename.lower().endswith(&#39;.pdf&#39;) is False:<br>        print(f&quot;File {filename} is not a PDF file, skipping&quot;)<br>        return<br>    localname = &#39;/tmp/test.pdf&#39;<br>    download_to_local(bucketname, filename, localname)<br><br>    # Determine how many pages there are<br>    num_pages = len(pdfium.PdfDocument(localname))<br><br>    # For each page, post a message<br>    publish_futures = []<br>    for page_num in range(num_pages):<br>        # Create a JSON object with the file name, page number to process, and total number of pages<br>        data = json.dumps({&quot;filename&quot;: filename, &quot;pagenum&quot;: page_num, &quot;totalpages&quot;: num_pages}).encode(&#39;utf-8&#39;)<br><br>        # Non-blocking. Publish failures are handled in the callback function.<br>        future = publisher.publish(topic_path, data)<br>        future.add_done_callback(get_callback(future, data))<br>        publish_futures.append(future)<br><br>    # Wait for all the publish futures to resolve before exiting.<br>    futures.wait(publish_futures, return_when=futures.ALL_COMPLETED)<br><br>    # then delete the local file and exit<br>    os.remove(localname)<br><br><br>def download_to_local(bucketname, filename, localname):<br>    bucket = storage_client.bucket(bucketname)<br>    blob = bucket.blob(filename)<br>    blob.download_to_filename(localname)<br><br><br>def get_callback(publish_future: pubsub_v1.publisher.futures.Future, data: str) -&gt; Callable[[pubsub_v1.publisher.futures.Future], None]:<br>    def callback(publish_future: pubsub_v1.publisher.futures.Future) -&gt; None:<br>        try:<br>            # Wait 60 seconds for the publish call to succeed.<br>            publish_future.result(timeout=60)<br>        except futures.TimeoutError:<br>            print(f&quot;Publishing {data} timed out.&quot;)<br>    return callback</pre><p>Now let’s look at the function that processes an individual page.</p><pre>import base64<br>from google.cloud import storage<br>import os<br>import json<br>from read_pdf import get_markdown_for_page<br>from bigquery import save_page_info, get_num_pages_for_filename, get_markdown_for_filename<br><br><br>BUCKET = os.getenv(&quot;BUCKET&quot;)<br>storage_client = storage.Client()<br><br><br>def handle_pubsub_message(event, context):<br>    # Decode the message data<br>    message_bytes = base64.b64decode(event[&#39;data&#39;])<br>    message_str = message_bytes.decode(&#39;utf-8&#39;)<br>    message_json = json.loads(message_str)<br><br>    # Get information about the page we should process<br>    filename = message_json.get(&quot;filename&quot;)<br>    pagenum = message_json.get(&quot;pagenum&quot;)<br>    totalpages = message_json.get(&quot;totalpages&quot;)<br><br>    # retrieve the file, extract the page in question, convert it to an image,<br>    # and use Gemini to get the markdown for it<br>    download_to_local(BUCKET, filename, &quot;temp.pdf&quot;)<br>    markdown = get_markdown_for_page(&quot;temp.pdf&quot;, pagenum)<br>    save_page_info(filename, pagenum, markdown)<br><br>    # now check if all of the pages have been processed<br>    num_pages_for_filename = get_num_pages_for_filename(filename)<br>    if num_pages_for_filename == totalpages:<br>        # retrieve the markdown for all pages, combine, and then store as a file<br>        # in the future, we will now pass this string to Gemini to get the product info<br>        all_markdown = get_markdown_for_filename(filename)<br>        save_text_to_bucket(BUCKET, f&#39;markdown\{filename}.md&#39;, all_markdown)<br><br><br>def download_to_local(bucketname, filename, localname):<br>    bucket = storage_client.bucket(bucketname)<br>    blob = bucket.blob(filename)<br>    blob.download_to_filename(localname)<br><br><br>def save_text_to_bucket(bucketname, filename, text):<br>    bucket = storage_client.bucket(bucketname)<br>    blob = bucket.blob(filename)<br>    blob.upload_from_string(text)</pre><p>As you can see, this function calls a couple of additional modules. First, here’s the <strong>read_pdf.py</strong> module for extracting the image and then calling Gemini for the markdown:</p><pre>import vertexai<br>from vertexai.generative_models import (<br>    Part,<br>    Image,<br>    GenerativeModel,<br>    HarmBlockThreshold,<br>    HarmCategory,<br>)<br>import pypdfium2 as pdfium<br>import os<br><br><br>PROJECT_ID = os.getenv(&quot;PROJECTID&quot;)<br>REGION = os.getenv(&quot;REGION&quot;)<br>LOCAL_IMAGE_FILE = &quot;/tmp/page.png&quot;<br>vertexai.init(project=PROJECT_ID, location=REGION)<br>model = GenerativeModel(&quot;gemini-1.5-flash-002&quot;)<br><br><br>def get_markdown_for_page(fname, pagenum):<br>    imgname = get_image_for_page(fname, pagenum)<br>    markdown = call_gemini_for_markdown(imgname)<br>    return markdown<br><br><br>def get_image_for_page(fname, pagenum):<br>    doc = pdfium.PdfDocument(fname)<br>    page = doc.get_page(pagenum)<br>    bitmap = page.render(scale=2)    # 72dpi resolution x 2<br>    bitmap = bitmap.to_pil()<br>    bitmap.save(LOCAL_IMAGE_FILE)<br>    return LOCAL_IMAGE_FILE<br><br><br>def call_gemini_for_markdown(img_filename):<br>    image1 = Part.from_image(Image.load_from_file(img_filename))<br>    generation_config = {<br>        &quot;max_output_tokens&quot;: 8192,<br>        &quot;temperature&quot;: 1,<br>        &quot;top_p&quot;: 0.95,<br>    }<br><br>    safety_settings = {<br>        HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_NONE,<br>        HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_NONE,<br>        HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: HarmBlockThreshold.BLOCK_NONE,<br>        HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_NONE,<br>    }<br><br>    responses = model.generate_content(<br>        [image1, &quot;Examine the image and return all of the text within it, converted to Markdown. Make sure the text reflects how a human being would read this, following columns and understanding formatting. Ignore footnotes and page numbers - they should not be returned as part of the Markdown. Only generate markdown for the text found on the page.&quot;],<br>        generation_config=generation_config,<br>        safety_settings=safety_settings,<br>        stream=True,<br>    )<br><br>    response_text = []<br>    for response in responses:<br>        response_text.append(response.text)<br>    return &quot;&quot;.join(response_text)</pre><p>As you can see, the prompt we use to extract the Markdown is the following:</p><pre>Examine the image and return all of the text within it, converted to <br>Markdown. Make sure the text reflects how a human being would read this, <br>following columns and understanding formatting. Ignore footnotes and <br>page numbers - they should not be returned as part of the Markdown. <br>Only generate markdown for the text found on the page.</pre><p>Finally, there are a couple of functions we use when interacting with BigQuery, which are located in the <strong>bigquery.py</strong> module:</p><pre>from google.cloud import logging, bigquery<br>import os<br>import time<br><br><br>BQ_DATASET = os.getenv(&quot;BQ_DATASET&quot;)<br>BQ_TABLE = &quot;pdf2markdown&quot;<br>bq_client = bigquery.Client()<br>logging_client = logging.Client()<br>log_name = &quot;debug-log&quot;<br>logger = logging_client.logger(log_name)<br><br><br>def save_page_info(filename, pagenum, markdown):<br>    table_id = f&#39;{BQ_DATASET}.{BQ_TABLE}&#39;<br>    table_ref = bq_client.dataset(BQ_DATASET).table(BQ_TABLE)<br><br>    # Insert the extracted fields as a new row<br>    try:<br>        errors = bq_client.insert_rows_json(<br>            table_ref,<br>            [{<br>                &quot;filename&quot;: filename,<br>                &quot;pagenum&quot;: pagenum,<br>                &quot;markdown&quot;: markdown<br>            }])<br><br>        if errors == []:<br>            logger.log_text(&quot;Data inserted into table&quot;)<br>        else:<br>            logger.log_text(f&quot;Errors encountered while inserting data: {errors}&quot;, severity=&quot;ERROR&quot;)<br>    except Exception as e:<br>        logger.log_text(f&quot;Error inserting data into BQ: {e}&quot;, severity=&quot;ERROR&quot;)<br><br><br>def get_num_pages_for_filename(filename):<br>    query = f&quot;SELECT COUNT(*) as numpages FROM `{BQ_DATASET}.{BQ_TABLE}` WHERE filename = &#39;{filename}&#39;&quot;<br>    query_job = bq_client.query(query)<br>    results = list(query_job.result())<br>    count = results[0].numpages<br>    return count<br><br><br>def get_markdown_for_filename(filename):<br>    query = f&quot;SELECT markdown FROM `{BQ_DATASET}.{BQ_TABLE}` WHERE filename = &#39;{filename}&#39; ORDER BY pagenum&quot;<br>    query_job = bq_client.query(query)<br>    results = list(query_job.result())<br>    # combine into one string<br>    parts = [row.markdown for row in results]<br>    return &quot;\n&quot;.join(parts)</pre><p>Note that this code assumes that the BigQuery table <strong>pdf2markdown</strong> has already been created. Although you can create the table via code if it doesn’t exist, there is often a slight delay before you can insert data into that table, which can result in errors. Best practice is to create the empty table outside of your code first by using Terraform or some other Infrastructure As Code (IAC) approach.</p><h3>Conclusion</h3><p>This article talks about the challenges that come with working with PDF documents, specifically for a RAG application. Since PDF files were designed primarily to support almost any imaginable layout, they are very often quite difficult to work with when attempting to extract the text and related contextual information like headings, tables, etc.</p><p>Markdown, on the other hand, is very well-suited for use with a LLM like Gemini, both in terms of adding readability and context to the output, but also for constructing prompts, and when chunking and indexing documents for a RAG solution. The challenge is to extract content from a PDF in Markdown format.</p><p>By turning each page of a PDF into an image, and then asking Gemini to extract the page content as Markdown, we can quickly and easily extract both the text and the context of the text from the document. And by leveraging the power of Google Cloud, we can make that process extremely efficient by processing many pages in parallel, only to combine the results once all pages have been processed.</p><p>Finally, another option to explore is Google Cloud’s<a href="https://cloud.google.com/document-ai/docs/overview"> DocumentAI</a>, which uses Google Foundation models to parse and chunk documents. It also has built-in OCR support, which allows parsing of image-based pages. You may wish to compare that approach with the approach described here, in order to determine the best approach for your documents. Keep in mind that DocumentAI does not return Markdown, so you should take that into account when deciding which approach to take.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=503846463f3f" width="1" height="1" alt=""><hr><p><a href="https://medium.com/google-cloud/unlocking-pdfs-for-rag-with-markdown-and-gemini-503846463f3f">Unlocking PDFs for RAG with Markdown and Gemini</a> was originally published in <a href="https://medium.com/google-cloud">Google Cloud - Community</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Extract JSON Data from Text using Gemini]]></title>
            <link>https://medium.com/google-cloud/extract-json-data-from-text-using-gemini-dc01dec17211?source=rss-a26d17de2fa6------2</link>
            <guid isPermaLink="false">https://medium.com/p/dc01dec17211</guid>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[gemini]]></category>
            <category><![CDATA[llm]]></category>
            <category><![CDATA[generative-ai]]></category>
            <category><![CDATA[google-cloud-platform]]></category>
            <dc:creator><![CDATA[Greg Sommerville]]></dc:creator>
            <pubDate>Mon, 04 Nov 2024 16:43:45 GMT</pubDate>
            <atom:updated>2024-11-05T07:23:41.065Z</atom:updated>
            <cc:license>http://creativecommons.org/publicdomain/zero/1.0/</cc:license>
            <content:encoded><![CDATA[<figure><img alt="An illustration showing a red arrow going from a page of text to a form with several fields" src="https://cdn-images-1.medium.com/max/1024/1*qm7ISa3uW5wG7kVJtPqpYg.png" /><figcaption>AI generated image</figcaption></figure><p>It’s no exaggeration to say that the introduction of Generative AI models has changed the landscape of what can be done with text. Large Language Models (LLMs) like Google’s <a href="https://ai.google.dev/gemini-api">Gemini</a> provide a host of capabilities that can be used in diverse applications, from analyzing customer feedback to extracting and summarizing insights from research papers, to automating data entry from invoices, and much more.</p><p>Although we often think of models in terms of generating text or code or images, they also excel at extracting information from text, offering unparalleled accuracy and efficiency compared to traditional methods. This article demonstrates how to use Gemini to extract information from unstructured text and then package that information into a JSON object.</p><h3>The Use Case</h3><p>We will explore a real-world scenario where precise data extraction is critical: analyzing reports of drug overdose deaths. These reports often contain a wealth of information, including details about the deceased, the circumstances surrounding the overdose, and potential contributing factors. Our goal is to extract over 200 specific data points from these reports, such as the presence of witnesses, history of substance abuse, mental health indicators, and more.</p><p>By automating this process with Gemini, we can efficiently transform these unstructured narratives into structured data, enabling researchers and public health officials to identify trends, patterns, and potential risk factors with greater accuracy. This structured data, formatted as a JSON object, can then be easily integrated into databases and analysis tools, ultimately contributing to more effective prevention and intervention strategies.</p><p>Now that we understand the overall situation, let’s look at an example. Here’s an example narrative:</p><pre>THE VICTIM IS A 42 YEAR OLD WHITE FEMALE, NOT HISPANIC, WHO DIED AT HOME. <br>CAUSE OF DEATH IS ACUTE COMBINED DRUG INTOXICATION INCLUDING HEROIN <br>AND METHAMPHETAMINE. THE MANNER OF DEATH IS ACCIDENT. THE V WAS LAST <br>SEEN ALIVE BY HER BOYFRIEND LAST NIGHT.  THE BOYFRIEND FOUND HER <br>UNRESPONSIVE THIS MORNING AND CALLED 911.  EMS ARRIVED AND PRONOUNCED <br>DEATH. DRUG PARAPHERNALIA WAS FOUND NEAR THE BODY. THE V HAD A HISTORY <br>OF DRUG ABUSE. TOXICOLOGY IS POSITIVE FOR AMPHETAMINE, METHAMPHETAMINE, <br>CODEINE FREE, MORPHINE FREE AND 6 MONOACETYLMORPHINE.</pre><p>The following is a short subset of some of the fields extracted from the narrative. (Note that BystanderPartner is true because the narrative mentions that the victim was found by their boyfriend, while the other values default to false.)</p><pre>{<br>  &quot;BystanderBreathing&quot;: false,<br>  &quot;BystanderCPR&quot;: false,<br>  &quot;BystanderFamily&quot;: false,<br>  &quot;BystanderFriend&quot;: false,<br>  &quot;BystanderIntOther&quot;: false,<br>  &quot;BystanderIntOther_specify&quot;: &quot;&quot;,<br>  &quot;BystanderMedical&quot;: false,<br>  &quot;BystanderNoOD&quot;: false,<br>  &quot;BystanderNotRecognize&quot;: false,<br>  &quot;BystanderOther&quot;: false,<br>  &quot;BystanderOther_specify&quot;: &quot;&quot;,<br>  &quot;BystanderPartner&quot;: true,<br>  &quot;BystanderPublic&quot;: false,<br>  &quot;CME_AlcoholProblem&quot;: false,<br>  &quot;CME_CircumstancesKnown&quot;: false,<br>  &quot;CME_CircumstancesOtherTex&quot;: &quot;&quot;,<br>  // etc - more fields included<br>}</pre><h3>The Approach</h3><p>To successfully extract and structure this information from overdose reports, we must first craft a precise and effective prompt that guides Gemini towards identifying and extracting the 200+ specific data points. These fields encompass a wide range of data types, from booleans and integers to dates and free-form text, demanding a prompt that can handle this diversity. This involves carefully defining the target information for each field, providing clear instructions, and potentially incorporating examples to illustrate the desired output format.</p><p>Equally important is a robust mechanism to extract the data from Gemini’s response and validate its structure and content, ensuring each field adheres to its expected data type. Although there are a number of Python libraries that are designed to validate data (like Pydantic), in this case we’ll demonstrate how to validate the data without using an external library.</p><p>With a clear understanding of our objectives, let’s dive into the crucial first step: prompt engineering.</p><h3><strong>Prompt Engineering</strong></h3><p>The prompt for this task will have several different sections. To start with, we need to set the persona and specify the overall task. We use the following text to accomplish this:</p><pre>You are a highly skilled information extraction AI specializing in <br>medical narratives. Your task is to do the following two steps:<br><br>Step 1. Extract fields from the narrative and briefly explain your thinking. <br>Keep your explanation concise.<br><br>Step 2. Generate a JSON object that contains the extracted fields<br><br>Please analyze the following text and extract the requested fields into <br>a JSON object.<br><br>If a field is of type &quot;option&quot;, you MUST use one of the listed choices. <br>Your answer should be the number associated with the choice. <br>Example: if &quot;1: No Pulse detected&quot; is a choice, your answer should be &quot;1&quot;.<br><br>If a field is of type &quot;boolean&quot;, you MUST use either true or false.<br><br>If a field is of type &quot;integer&quot;, you MUST answer with a positive integer <br>constant.<br><br>If a field is of type &quot;string&quot;, you MUST answer with a string constant.</pre><p>This initial portion of the prompt sets the stage for accurate and structured data extraction by establishing Gemini as a ‘highly skilled information extraction AI specializing in medical narratives.’ This focuses its attention on the relevant domain and expertise, priming it to interpret the text effectively.</p><p>The two-step task definition further enhances clarity. Step 1 encourages transparency by having Gemini explain its reasoning, which is invaluable for debugging, and also for providing transparency for public health officials on how Gemini made its determination of the extracted value. Step 2 ensures a structured, machine-readable JSON output, facilitating easy integration with other systems and analysis tools.</p><p>Additionally, clearly specifying the expected data types for different fields (“option”, “boolean”, “integer”, “string”) is crucial for maintaining data integrity and consistency. This reduces ambiguity and guides Gemini towards producing output that adheres to your predefined schema. Notice that we include a concrete example of how to handle “option” type fields that helps eliminate any confusion and ensures the model outputs the desired numerical representation.</p><p>Now that the overall task and persona have been established, we include the text of the narrative in the prompt, as well as descriptions of each field. Gemini consumes and produces markdown text, so we’ll use that to distinguish these important elements:</p><pre>**Narrative:**<br>{narrative}<br><br>**Fields to extract:**<br>{field_definitions}</pre><p>For those unfamiliar with markdown, text enclosed in double asterisks (**) renders as bold. The fields contained in braces will be substituted with the actual value of the narrative, and a description of all of the fields to be extracted.</p><p>The field definitions list out each of the 200+ fields we’d like to extract. For example, here’s the definition of a field meant to determine if any bystanders were present at the scene when the overdose occurred:</p><pre>**BystandersPresent**: (option) Bystanders present at time of overdose<br>  * 1: No bystanders present<br>  * 2: 1 bystander present<br>  * 3: Multiple bystanders present<br>  * 4: Bystanders present, unknown number<br>  * 5: Unknown if bystander present</pre><p>This markdown format, with single asterisks indicating bullet points, effectively structures the options for Gemini. Other field types (like Boolean or integer) describe the field name, type, and description without a list of options.</p><p>To maintain flexibility, it’s often desirable to have a dynamic set of fields, so we can update them without rewriting and redeploying our code. In this case we use a Google Sheets document that has a row for each field, with information about field name, types, description, options, and more. By storing this information in a spreadsheet, the definitions can easily be updated or tweaked, and the data can be exported as a CSV file, which our code can then read in.</p><p>Once we have all of the fields defined in our prompt, we finish it by added important clarifications and a reiteration of the overall goal:</p><pre>**Additional instructions:**<br><br>* Focus on accuracy. Do not generate information that is not explicitly <br>supported by the narrative.<br><br>* &quot;CME&quot; refers to the coroner or medical examiner.<br><br>* &quot;LE&quot; refers to law enforcement.<br><br>* Some fields are meant to be considered a pair, with the first field <br>(like &quot;BystanderIntOther&quot;) being a boolean indicating that there is <br>additional information, and the second field (like <br>&quot;BystanderIntOtherSpecify&quot;) gets filled in with a string only if the <br>first field is True.<br><br>Please ensure the JSON output is well-formatted and adheres to standard <br>conventions. All fields in the JSON object must have a valid value.</pre><p>Finally, one last thing to be considered when calling Gemini is that you can specify exactly the data type you’d like returned (such as JSON), as described in <a href="https://medium.com/google-cloud/how-to-consistently-output-json-with-the-gemini-api-using-controlled-generation-887220525ae0">this article</a>. However, although we can ask for JSON only as a response, there are reasons not to do so. For example, in this case we ask the model to explain its thinking, which is helpful for debugging and also generally improves the quality of the response. Using this approach requires that we extract the JSON object from a larger string, rather than asking for only JSON.</p><p>Now that we’ve constructed our prompt, let’s assume we’ve called Gemini, received a response, and now need to parse the result.</p><h3><strong>Extracting and Validating JSON Data</strong></h3><p>Despite their advancements, modern large language models are not without limitations. Potential problems include missing fields in the output JSON object, as well as fields that do exist but have invalid values, like a Boolean field with a value of the string “True”, rather than the Boolean value <em>true</em>.</p><p>Python libraries like <a href="https://docs.pydantic.dev/latest/">Pydantic</a> or <a href="https://json-schema.org/">JSON Schema</a> are designed to parse data and ensure that it follows a particular structure or schema, and if you’re comfortable with using such an approach, it will work well. Explicitly defining a schema for the JSON object helps with validation at the time of parsing, can improve code clarity, and can reduce the amount of code you need to write.</p><p>That said, it can be challenging to use Pydantic with a dynamic schema like the one we’re using here, where the field definitions are read in from a CSV file. Although there are ways to use Pydantic with dynamic schemas, for clarity we will demonstrate how to validate and correct data using plain Python, which will iterate through all of the fields in our schema and check for various problems.</p><p>As stated earlier, the first potential issue with extracted JSON is with missing values. The solution to this is to have default values for each field. For some fields (like booleans) this can be simple. For example, the data to be extracted may be worded in such a way that we only have a True boolean value when we are positive that certain conditions are true. In this case, a default value of False is probably best.</p><p>Other field types like strings can be handled in a similar manner. For example, if a string value is missing, perhaps the default value would be an empty string. Likewise, if a field is of type integer, it may have a specific value like 99 that is always used to indicate an unknown value. Choice values (where valid values must come from a list of possible values) can be more challenging, but if there is a choice that indicates that the value is unknown, that would be the default value to use.</p><p>Besides missing values, another potential concern is when the value for a field does not match the expected type, like in the example above of the string “True” being returned instead of a boolean constant. In this case, it’s best to attempt to convert values to their correct types through code, as is shown here:</p><pre># assume that field_type and field_name were read in from our CSV<br>if field_type == &#39;Bool&#39;:<br>    extracted_value = result[field_name]<br><br>    # first, handle None, changing it to False<br>    if extracted_value is None:<br>        extracted_value = False<br><br>    if type(extracted_value) is str:<br>        print(f&#39;Converting field {field_name} from string to bool&#39;)<br>        extracted_value = extracted_value.lower() == &#39;true&#39;<br>    elif type(extracted_value) is int:<br>        print(f&#39;Converting field {field_name} from int ({extracted_value}) to bool&#39;)<br>        extracted_value = extracted_value == 1<br><br>    result[field_name] = extracted_value</pre><p>Notice that when converting a string value to a boolean, we assume the value will be False unless the string is either “True” or “true”. Likewise, in many computer languages, a value of 1 is considered to be equivalent to True, while 0 indicates a False value. Therefore, to convert from an integer to a boolean, we check if the integer value is equal to 1.</p><h3><strong>Conclusion</strong></h3><p>When using a LLM like Gemini to extract structured data from unstructured text, there are two main areas to focus on: prompt construction and data validation.</p><p>This article has provided guidance about how to dynamically create a prompt using a set of overall instructions, a list of fields to extract that are described using markdown, and a set of final clarifications and details that the model needs to consider when answering.</p><p>Upon receiving the model’s response, we must iterate over each field to be extracted, providing default values for missing fields, and also handling the situation where the data type returned does not match the actual field.</p><p>By using these approaches, you will be able to leverage the power and efficiency of models like Gemini. The best way to learn is to experiment, so your next step will be to experiment with your own data. Starting out by using a Colab Enterprise notebook is a quick and easy way to begin</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=dc01dec17211" width="1" height="1" alt=""><hr><p><a href="https://medium.com/google-cloud/extract-json-data-from-text-using-gemini-dc01dec17211">Extract JSON Data from Text using Gemini</a> was originally published in <a href="https://medium.com/google-cloud">Google Cloud - Community</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Winning Blackjack using Machine Learning]]></title>
            <link>https://medium.com/data-science/winning-blackjack-using-machine-learning-681d924f197c?source=rss-a26d17de2fa6------2</link>
            <guid isPermaLink="false">https://medium.com/p/681d924f197c</guid>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[genetic-algorithm]]></category>
            <category><![CDATA[towards-data-science]]></category>
            <category><![CDATA[artificial-intelligence]]></category>
            <category><![CDATA[blackjack]]></category>
            <dc:creator><![CDATA[Greg Sommerville]]></dc:creator>
            <pubDate>Tue, 12 Feb 2019 20:54:10 GMT</pubDate>
            <atom:updated>2019-02-12T23:55:41.126Z</atom:updated>
            <content:encoded><![CDATA[<h4>A Practical Example of a Genetic Algorithm</h4><p>One of the great things about machine learning is that there are so many different approaches to solving problems. <em>Neural networks </em>are great for finding patterns in data, resulting in predictive capabilities that are truly impressive. <em>Reinforcement learning </em>uses rewards-based concepts, improving over time. And then there’s the approach called a <em>genetic algorithm</em>.</p><p>A genetic algorithm (GA) uses principles from evolution to solve problems. It works by using a population of potential solutions to a problem, repeatedly selecting and breeding the most successful candidates until the ultimate solution emerges after a number of generations.</p><p>To demonstrate how effective this approach is, we will use it to solve a complex problem — the creation of a strategy for playing the casino game <a href="https://en.wikipedia.org/wiki/Blackjack">Blackjack </a>(also known as “21”).</p><p>The term “strategy” in this case means a guide for player actions that covers all situations. The goal is to find a strategy that is the very best possible, resulting in maximized winnings over time.</p><h4><strong>About this “Winning” Strategy</strong></h4><p>Of course, in reality there is no winning strategy for Blackjack — the rules are set up so the house always has an edge. If you play long enough, you <em>will</em> lose money.</p><p>Knowing that, the best possible strategy is the one that minimizes losses. Using such a strategy allows a player to stretch a bankroll as far as possible while hoping for a run of short-term good luck. That’s really the only way to profit at Blackjack.</p><p>As you might imagine, Blackjack has been studied by mathematicians and computer scientists for a long, long time. Back in the 1960s, a mathematician named <a href="https://en.wikipedia.org/wiki/Edward_O._Thorp">Edward O. Thorp</a> authored a book called <em>Beat the Dealer</em>, which included charts showing the optimal “Basic” strategy.</p><p>That optimal strategy looks something like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/847/0*AP9rXlbHGU8hkvxL" /><figcaption>Optimal Strategy for Blackjack</figcaption></figure><p>The three tables represent a complete strategy for playing Blackjack.</p><p>The tall table on the left is for <em>hard hands</em>, the table in the upper right is for <em>soft hands</em>, and the table in the lower right is for <em>pairs</em>.</p><p>If you aren’t familiar with Blackjack, a soft hand is a hand with an Ace that can count as 1 or 11, without the total hand value exceeding 21. A pair is self-explanatory, and a hard hand is basically everything else, reduced to a total hand value.</p><p>The columns along the tops of the three tables are for the dealer upcard, which influences strategy. Notice that the upcard ranks don’t include Jack, Queen or King. That’s because those cards all count as 10, so they are all grouped together with the Ten (“T”) to simplify the tables.</p><p>To use the tables, a player would first determine if they have a pair, soft hand or hard hand, then look in the appropriate table using the row corresponding to their hand holding, and the column corresponding to the dealer upcard.</p><p>The cell in the table will be “H” when the correct strategy is to hit, “S” when the correct strategy is to stand, “D” for double-down, and (in the pairs table only) “P” for split.</p><p>Knowing the optimal solution to a problem like this is actually very helpful. Comparing the results from a GA to the known solution will demonstrate how effective the technique is.</p><p>Finally, there’s one other thing to get out of the way before we go any further, and that’s the idea of <em>nondeterminism</em>. That means that if the same GA code is run twice in a row, two different results will be returned. That’s something that happens with genetic algorithms due to their inherent randomness. It’s unusual for software to act this way, but in this case it’s just part of the approach.</p><h4><strong>How a Genetic Algorithm Works</strong></h4><p>Genetic algorithms are fun to use because they’re so easy to understand: you start with a population of (initially, completely random) potential solutions, and then let evolution do its thing to find a solution.</p><p>That evolutionary process is driven by comparing candidate solutions. Each candidate has a fitness score that indicates how good it is. That score is calculated once per generation for all candidates, and can be used to compare them to each other.</p><p>In the case of a Blackjack strategy, the fitness score is pretty straightforward: if you play N hands of Blackjack using the strategy, how much money do you have when done? (Due to the house edge, all strategies will lose money, which means all fitness scores will be negative. A higher fitness score for a strategy merely means it lost less money than others might have.)</p><p>Once an effective fitness function is created, the next decision when using a GA is how to do selection.</p><p>There are a number of different selection techniques to control how much a selection is driven by fitness score vs. randomness. One simple approach is called <em>Tournament Selection</em>, and it works by picking N random candidates from the population and using the one with the best fitness score. It’s simple and effective.</p><p>Once two parents are selected, they are crossed over to form a child. This works just like regular sexual reproduction — genetic material from both parents are combined. Since the parents were selected with an eye to fitness, the goal is to pass on the successful elements from both parents.</p><p>Naturally, in this case the “genetic material” is simply 340 cells from the three tables that each strategy has. A cell in the child is populated by choosing the corresponding cell from one of the two parents. Oftentimes, crossover is done proportional to the relative fitness scores, so one parent could end up contributing many more table cells than the other if they had a significantly better fitness score.</p><p>Finally, just like in nature, it’s important to have diversity in a population. Populations that are too small or too homogenous always perform worse than bigger and more diverse populations.</p><p>Genetic diversity is important, because if you don’t have enough, it’s easy to get stuck in something called a <em>local minimum</em>, which is basically a solution that performs better than any similar alternatives, but is inferior to other solutions that are significantly dissimilar to it.</p><p>To avoid that problem, genetic algorithms sometimes use mutation (the introduction of completely new genetic material) to boost genetic diversity, although larger initial populations also help.</p><h4><strong>Results Using a GA</strong></h4><p>One of the cool things about GAs is simply watching them evolve a solution. The first generation is populated with completely random solutions. This is the very best solution (based on fitness score) from 750 candidates in generation 0 (the first, random generation):</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/852/0*R0ZYpav9YOjK_BCf" /><figcaption>Randomly generated candidate from Gen 0</figcaption></figure><p>As you can see, it’s completely random. By generation 12, some things are starting to take shape:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/852/0*dl3HP0VSdZyEAO1B" /></figure><p>With only 12 generations experience, the most successful strategies are those that Stand with a hard 20, 19, 18, and possibly 17. That part of the strategy develops first because it happens so often and it has a fairly unambiguous result. Basic concepts get developed first with GAs, with the details coming in later generations.</p><p>The other hints of quality in the strategy are the hard 11 and hard 10 holdings. According to the optimal strategy those should be mostly Double-Down, so it’s encouraging to see so much yellow there.</p><p>The pairs and soft hand tables develop last because those hands happen so infrequently. A player is dealt a pair only 6% of the time, for example.</p><p>By generation 33, things are starting to become clear:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/852/0*g3EinRhSbDqP1efZ" /></figure><p>By generation 100, the hard hand table on the left is completely stabilized — it doesn’t change from generation to generation. The soft hand and pairs tables are getting more refined:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/852/0*CGvt5-aqMK4vTKXp" /></figure><p>And then the final generations are used to refine the strategies. The changes from generation to generation are much smaller at this stage, since it’s really just the process of working out the smallest details.</p><p>Finally, the best solution found over 237 generations:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/852/0*1VeD9ez2IinyBjIt" /></figure><p>As you can see, the final result is not exactly the same as the optimal solution, but it’s very, very close. The hard hands in particular (the table on the left) are almost exactly correct. The soft hands and pairs tables have a few more cells that don’t match, but that’s likely because those hand types occur far less than hard hands.</p><p>In terms of outcome, playing the optimal strategy for 500,000 hands at $5 per hand would result in a loss of $176,040. Using the computer-generated strategy would result in a loss of $176,538, a difference of $498 over half a million hands.</p><p>There’s an <a href="https://github.com/GregSommerville/machine-learning-blackjack-solution/blob/master/images/animatedsolution.gif">animated GIF </a>that shows the evolution of this strategy over 237 generations, but be aware that it’s 19 MB in size, so you may not wish to view it over a phone.</p><p>The source code for the software that produced these images is <a href="https://github.com/GregSommerville/machine-learning-blackjack-solution">open source</a>. It’s a desktop application for Windows written in C# with WPF.</p><h4>Combinatorial Implications</h4><p>As impressive as the resulting strategy is, we need to put it into context by thinking about the scope of the problem. An optimal strategy for Blackjack is expressed by filling each of the 340 table cells (spread across the three tables) with the best choice for each holding/dealer upcard combination — either stand, hit, double-down, or split.</p><p>In terms of combinations, there are 4¹⁰⁰ possible pair strategies, 3⁸⁰ possible soft hand strategies, and 3¹⁶⁰ possible hard hand strategies, for a grand total of 5 x 10¹⁷⁴ possible strategies for Blackjack:</p><p>4¹⁰⁰ x 3⁸⁰ x 3¹⁶⁰ = 5 x 10¹⁷⁴ possible Blackjack strategies</p><p>In this case the genetic algorithm found a close-to-optimal solution in a solution space of 5 x 10¹⁷⁴ possible answers. Running on a standard desktop computer, it took about 75 minutes. During that run, about 178,000 strategies were evaluated.</p><h4>Testing Fitness</h4><p>Genetic algorithms are essentially driven by fitness functions. Without a good way to compare candidates to each other, there’s no way the evolutionary process can work.</p><p>The idea of a fitness function is simple. Even though we may not know the optimal solution to a problem, we do have a way to measure potential solutions against each other. The fitness function reflects the relative fitness levels of the candidates passed to it, so the scores can effectively be used for selection.</p><p>For the purposes of finding a Blackjack strategy, a fitness function is straightforward — it’s a function that returns the expected final earnings after using the strategy over a certain number of hands.</p><p>But how many hands is enough?</p><p>As it turns out, you need to play a lot of hands with a strategy to determine its quality. Because of the innate randomness of a deck of cards, many hands need to be played so the randomness evens out across the candidates.</p><p>That’s especially important when our GA gets close to a final solution. In early generations, it’s not a problem if the fitness scores are not exact, because the difference between a bad candidate and a good candidate is usually quite large and the convergence to the final solution continues without a problem.</p><p>However, once the GA gets into the later generations, the candidate strategies being compared will have only minor differences, so it’s important to get accurate expected winnings from a fitness function.</p><p>Luckily, it’s pretty straightforward to find the right number of hands needed. Using a single strategy, multiple tests are run, resulting in a set of fitness scores. The variations from run to run for the same strategy will reveal how much variability there is, which is driven in part by the number of hands tested. The more hands played, the smaller the variations will be.</p><p>By measuring the standard deviation of the set of scores we get a sense of how much variability we have across the set for a test of N hands. But as we experiment with different numbers of hands played per test, we can’t compare standard deviations, for the following reason:</p><p>Standard deviation is scaled to the underlying data. We can’t compare fitness scores (or standard deviations thereof) from tests using different numbers of hands because a higher number of hands played results in a corresponding increase in the fitness score.</p><p>Put it another way: say a strategy wins 34% of the time. If you run it for 25,000 hands versus 50,000 hands, you’ll have different totals at the end. That’s why you can’t simply compare fitness scores that result from different test conditions. And if you can’t compare the raw values, you can’t compare the standard deviations.</p><p>We solve this by dividing the standard deviation by the average fitness score for each of the test values (the number of hands played, that is). That gives us something called the <em>coefficient of variation</em>, which can be compared to other test values, regardless of the number of hands played.</p><p>The chart here that demonstrates how the variability shrinks as we play more hands:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/600/0*MFqgoGFV7SbyFgpz" /></figure><p>There are a couple of observations from the chart. First, testing with only 5,000 or 10,000 hands is not sufficient. There will be large swings in fitness scores reported for the same strategy at these levels. In fact, it looks like a minimum of 100,000 hands is probably reasonable, because that is the point at which the variability starts to flatten out.</p><p>Could we run with 500,000 or more hands per test? Of course. It reduces variability and increases the accuracy of the fitness function. In fact, the coefficient of variation for 500,000 hands is 0.0229, which is significantly better than 0.0494 for 100,000 hands. But that improvement is definitely a case of diminishing returns: the number of tests had to be increased 5x just to get half the variability.</p><p>Given those findings, the fitness function for a strategy will need to play at least 100,000 hands of Blackjack, using the following rules (common in real-world casinos):</p><ul><li>Using 4 decks of cards shuffled together</li><li>Dealer is required to hit until they have 17 (soft or hard)</li><li>You can double down on a hand that you split</li><li>There is no insurance</li><li>Blackjack pays 3:2</li></ul><h4>Genetic Algorithm Configurations</h4><p>One of the unusual aspects to working with a GA is that it has so many settings that need to be configured. The following items can be configured for a run:</p><ul><li>Population Size</li><li>Selection Method</li><li>Mutation Rate and Impact</li><li>Termination Conditions</li></ul><p>Varying each of these gives different results. The best way to settle on values for these settings is simply to experiment.</p><p><strong>Population Size</strong></p><p>Here’s a chart of the average candidate fitness per generation for the different population sizes:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/600/0*3UDSc6aidGu7ugBu" /></figure><p>The X axis of this chart is the generation number (with a maximum of 200), and the Y axis is the average fitness score per generation. The first few generations aren’t shown to emphasize the differences as we reach the later generations.</p><p>The flat white line along the top of the chart is the fitness score for the known, optimal baseline strategy.</p><p>The first thing to notice is that the two smallest populations (having only 100 and 250 candidates respectively, shown in blue and orange) performed the worst of all sizes.</p><p>The lack of genetic diversity in those small populations results in poor final fitness scores, along with a slower process of finding a solution. Clearly, having a large enough population to ensure genetic diversity is important.</p><p>On the other hand, there aren’t too many differences between populations of 400, 550, 700, 850 and 1000.</p><p>This is a similar situation to choosing the number of hands to test with — if you pick a value that’s too small, the test isn’t accurate, but once you exceed a certain level, the differences are minor.</p><h4>Selection Methods</h4><p>The process of finding good candidates for crossover is called selection, and there are a number of ways to do it. Tournament selection has already been covered. Here are two other approaches:</p><p><em>Roulette Wheel Selection</em> selects candidates proportionate to their fitness scores. Imagine a pie chart with three wedges of size 1, 2, and 5. The wedge with the value 5 will be selected 5/8 of the time, the wedge with value 2 will be selected 2/8 of the time, and the wedge with value 1 will be selected 1/8 of the time. That’s the basic idea behind Roulette Wheel selection. The size of each candidate’s wedge is proportional to their fitness score compared to the total score of all candidates.</p><p>One of the problems with that selection method is that sometimes certain candidates will have such a small fitness score that they never get selected. If, by luck, there are a couple of candidates that have fitness scores far higher than the others, they may be disproportionately selected, which reduces genetic diversity.</p><p>The solution is to use <em>Ranked Selection</em>, which works by sorting the candidates by fitness, then giving the worst candidate a score of 1, the next worse a score of 2, and so forth, all the way up to the best candidate, which receives a score equal to the population size. Once this fitness score adjustment is complete, Roulette Wheel selection is used.</p><p>Here’s a graph that compares the average fitness per generation using a variety of selection methods:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/600/0*vokj76nxGrpT8BV7" /></figure><p>As you can see, tourney selection converges on an optimal solution very quickly — in fact, the bigger the tourney size, the faster the average fitness score improves. That makes sense, because if you’re choosing 7 random candidates and using the best, the quality is going to be much higher than doing the same while choosing only 2.</p><p>Even though it had the fastest initial improvement, Tourney 7 ends up producing the worst results. That makes sense, because although a big tourney size results in rapid improvement, it also limits the genetic pool to only the best. Needed genetic diversity is lost, and in the long run it doesn’t perform as well.</p><p>The best performers look to be Tourney 2, Tourney 3, and Tourney 4. Given a population of 700, these numbers provide good long-term results.</p><h4>Elitism</h4><p>There’s another concept in genetic algorithms called <em>elitism</em>. It’s the idea that when building a new generation, first sort the population by fitness, and then pass in a certain percentage of the best candidates directly into the next generation without alteration. After that is done, normal crossover begins.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/600/0*LUEq-mmzJEDWEJne" /></figure><p>This chart shows the effects of four different elitism rates (later generations only, to show the details). Clearly no elitism or 15% are reasonable, although 0% looks a bit better.</p><p>There’s one thing that’s surprising about this chart — the higher the elitism was, the slower the convergence was to solution. You might think that deliberately including the best from each generation would speed things up, but in fact it looks like using only crossed-over candidates gives the best results, and is also the fastest.</p><h4>Mutations</h4><p>Keeping genetic diversity high is important, and mutation is an easy way to introduce that.</p><p>There are two factors relating to mutation: how often does it happen, and how much of an impact does it have when it does happen?</p><p>A <em>mutation rate</em> controls how often a newly created candidate will be mutated. The mutation is done immediately after creation via crossover.</p><p>The <em>mutation impact</em> controls how much a candidate is mutated, in terms of percentage of its cells that will be randomly changed. All three tables (hard hands, soft hands and pairs) are mutated the same percentage.</p><p>Starting with a fixed impact rate of 10%, here are the effects of different mutation rates:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/600/0*1lNvNden_ifp-X1u" /></figure><p>It is clear that mutation does not help for this problem — the more candidates are affected by mutation, the worse the results. It follows that trying different mutation impact values is not required — 0% mutation rate is clearly the best for this problem.</p><h4>Termination Conditions</h4><p>Knowing when to quit a genetic algorithm can be tricky. Some situations call for a fixed number of generations, but for this problem the solution was to look for stagnation — in other words, the genetic algorithm stops when it detects that the candidates are no longer improving.</p><p>The condition used for this test was that if there was no improvement in the overall best strategy (or a generation’s average score) for 25 generations in a row, then the process terminates and the best result found to that point is used as the final solution.</p><h3>Wrapping Up</h3><p>Genetic algorithms are a powerful technique for solving complex problems, and they have the benefit of being easy to understand. For problems with huge solution spaces due to combinatorial factors, they are extremely effective.</p><p>For more information about GA, please start with this <a href="https://en.wikipedia.org/wiki/Genetic_algorithm">Wikipedia article</a> or the <a href="https://www.pluralsight.com/courses/genetic-algorithms-genetic-programming">PluralSight (paid) course </a>I wrote that covers the topic in far greater detail.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=681d924f197c" width="1" height="1" alt=""><hr><p><a href="https://medium.com/data-science/winning-blackjack-using-machine-learning-681d924f197c">Winning Blackjack using Machine 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>