<?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 Aayush Agrawal on Medium]]></title>
        <description><![CDATA[Stories by Aayush Agrawal on Medium]]></description>
        <link>https://medium.com/@aayushmnit?source=rss-85dd669ac015------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*oyFmPAf4jXS_gL2XNlktHQ.jpeg</url>
            <title>Stories by Aayush Agrawal on Medium</title>
            <link>https://medium.com/@aayushmnit?source=rss-85dd669ac015------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sat, 14 Nov 2026 18:27:05 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@aayushmnit/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[Using AI to Read Technical Research Papers]]></title>
            <link>https://medium.com/data-science-collective/using-ai-to-read-technical-research-papers-9a8d920e07c4?source=rss-85dd669ac015------2</link>
            <guid isPermaLink="false">https://medium.com/p/9a8d920e07c4</guid>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[research-paper]]></category>
            <category><![CDATA[artificial-intelligence]]></category>
            <category><![CDATA[reading]]></category>
            <dc:creator><![CDATA[Aayush Agrawal]]></dc:creator>
            <pubDate>Sun, 01 Feb 2026 04:21:49 GMT</pubDate>
            <atom:updated>2026-02-10T05:44:18.979Z</atom:updated>
            <content:encoded><![CDATA[<blockquote><em>A practical guide to using local or paid LLMs to understand dense technical papers at your own pace.</em></blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xHnforEXhHnNsA1bl4SrEA.png" /></figure><p><a href="https://aayushmnit.com/posts/2026-01-31_ReadWithAI/ReadWithAI.html#why-i-started-using-ai-to-read-research-papers">Read in the browser</a>, for best viewing for best viewing experience.</p><h3>Why I Started Using AI to Read Research Papers</h3><p>I’ll be honest: I used to dread reading research papers. The dense math, the notation-heavy proofs, the assumption that you already know three other papers to understand this one. As an ML engineer, I knew these papers held insights I could apply at work, but actually extracting that value felt like a slog.</p><p>My learning style doesn’t mesh well with traditional academic writing. I learn best through small examples and code I can run and tinker with. Show me a 3x3 matrix example before the general n×n case. Let me see the algorithm work on toy data before throwing theorems at me.</p><p>For years, this created a barrier. Research papers felt like they were written for PhDs, not practitioners. You either had the advanced math background to parse them, or you waited months for someone to write a blog post explaining it in plain English.</p><p>That’s changed. With AI assistants (both local and cloud-based LLMs), I can now have a conversation with a paper. I can ask “what does this symbol mean?” or “show me this concept with a simple Python example” and get an answer calibrated to my current understanding. The paper adapts to me, not the other way around.</p><p>This post walks through my workflow for using AI to read technical papers, so you can apply the same approach to level up your own learning.</p><p>By the end of this post, you’ll be able to:</p><ul><li>Set up a local LLM (using Ollama) or connect to cloud APIs for paper reading</li><li>Convert PDFs to markdown that LLMs can actually process</li><li>Craft system prompts that turn an LLM into a patient tutor, not a lecture-bot</li><li>Build an interactive Q&amp;A workflow where <em>you</em> control the pace and depth</li><li>Apply this approach to papers, documentation, or any dense technical content</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*LfComylzwJQgc5ER.jpg" /><figcaption>Figure: Core Workflow. Credit: <a href="https://notebooklm.google.com/">NotebookLM</a></figcaption></figure><h3>Prerequisites &amp; Setup</h3><p>Before we dive in, let’s get our environment ready. You’ll need two things: a local LLM running on your machine, and a few Python libraries to glue everything together.</p><h3>Installing Ollama</h3><p>Ollama makes running LLMs locally dead simple. Head to <a href="https://ollama.com/">ollama.com</a> and download the installer for your OS. On Mac, it’s a standard .dmg install. On Linux, a one-liner:</p><pre>curl -fsSL https://ollama.com/install.sh | sh</pre><p>Once installed, verify it’s working:</p><pre>ollama --version</pre><h3>Choosing and Pulling a Model</h3><p>Browse the <a href="https://ollama.com/search">Ollama model library</a> to see what’s available. For reading papers with diagrams and equations, I recommend <a href="https://ollama.com/library/qwen3-vl:8b">qwen3-vl:8b</a>, a vision-language model that can handle both text and images. You can use other models based on your machine’s specifications.</p><p>Pull it with:</p><pre>ollama pull qwen3-vl:8b</pre><p>This downloads the model weights (around 5GB). Grab a coffee.</p><p>To start serving:</p><pre>OLLAMA_CONTEXT_LENGTH=64000 ollama serve</pre><p>Keep this terminal running. Ollama now exposes an OpenAI-compatible API at localhost:11434.</p><blockquote><strong>Note:</strong> Default context length in Ollama is 4096 tokens which is too small for our usecase, so we bump it upto 64K.</blockquote><h3>Python Libraries</h3><p>We need three packages:</p><p>- <a href="https://github.com/pymupdf/pymupdf4llm">pymupdf4llm</a>: Converts PDF pages to clean markdown that LLMs can parse</p><p>- <a href="https://lisette.answer.ai/">lisette</a>: Lightweight wrapper on <a href="https://github.com/BerriAI/litellm">litellm</a> that simplifies multi-turn conversations and provide a Unified API to talk to 100+ LLM providers (local or cloud) with one interface</p><p>Install them:</p><pre>pip install pymupdf4llm lisette</pre><p>With Ollama running and these libraries installed, you’re ready to start chatting with papers.</p><h3>Core Workflow: PDF to Conversation</h3><p>The workflow has two parts: getting the paper into a format LLMs can work with, then setting up the conversation so the AI becomes a useful tutor rather than a generic chatbot.</p><h3>Converting PDF to Markdown</h3><p>First step is to convert a pdf into text. <a href="https://github.com/pymupdf/pymupdf4llm">pymupdf4llm</a> handles this by extracting text while preserving structure. Here’s an example, reading this <a href="https://dl.acm.org/doi/epdf/10.1145/3269206.3272018">paper</a>:</p><pre>import pymupdf4llm<br># Convert PDF to txt<br>paper_txt = pymupdf4llm.to_markdown(&quot;./paper/1_2018_DPP_YT.pdf&quot;)<br># Save the paper as Markdown<br>with open(&quot;./paper/1_2018_DPP_YT.md&quot;, &quot;w&quot;) as f:<br>    f.write(paper_txt)</pre><p>Let’s see the first paragraph.</p><pre>print(paper_txt[:500])</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/798/1*17-G88NqWeEIJ8KAbKWNmw.png" /><figcaption>Figure: Code output from above code</figcaption></figure><p>That’s it. You now have clean markdown with headers, code blocks, and reasonable table formatting. For papers with complex layouts, the results aren’t perfect, but they’re good enough for an LLM to understand the content. If you want a more accurate PDF to markdown converter, you can also look at <a href="https://github.com/datalab-to/marker">Datalab Marker</a>.</p><h3>Designing Your System Prompt</h3><p>This is where the magic happens. A vanilla “you are a helpful assistant” prompt gives you Wikipedia-style explanations. A well-crafted system prompt gives you a patient tutor who meets you at your level.</p><p>Here’s the prompt which works well for paper-reading sessions:</p><pre>system_prompt = f&quot;&quot;&quot;<br>You are helping a someone understand an academic paper.<br>Here is the paper \n<br>&lt;paper&gt;<br>{paper_txt}<br>&lt;/paper&gt;<br><br>CRITICAL RULES:<br>1. NEVER explain everything at once. Take ONE small step, then STOP and wait.<br>2. ALWAYS start by asking what the learner already knows about the topic.<br>3. After each explanation, ask a question to check understanding OR ask what they want to explore next.<br>4. Keep responses SHORT (2-4 paragraphs max). End with a question.<br>5. Use concrete examples and analogies before math. <br>6. Build foundations with code - Teach unfamiliar mathematical concepts through small numpy experiments rather than pure theory. Let the learner run code and observe patterns.<br>7. If they ask &quot;explain X&quot;, first ask what parts of X they already understand.<br>8. Use string format like this for formula display `L_ij = q_i × q_j × exp(-α × D_ij^γ)`.<br><br>TEACHING FLOW:<br>- Assess background → Build intuition with examples → Connect to math → Let learner guide direction<br><br>BAD (don&#39;t do this):<br>&quot;Here&#39;s everything about DPPs: [wall of text with all equations]&quot;<br>&quot;&quot;&quot;</pre><p><strong>The Teaching Prompt Deep-Dive</strong></p><p>The prompt design isn’t arbitrary. Each rule targets a specific failure mode I’ve seen when using LLMs for learning:</p><ol><li><strong>The paper lives in the system prompt.</strong> This keeps the full context available for every question you ask. You don’t need to re-paste sections or remind the AI what paper you’re discussing.</li><li><strong>“Intuition before math” matches how practitioners learn.</strong> Most of us grasp concepts faster through examples and analogies than through theorem statements. If your learning style differs, swap in your own preferences here.</li><li><strong>“Tiny example first” counteracts the LLM’s default behavior.</strong> Without this, models tend toward abstract, general explanations. Forcing concrete examples (a 3x3 matrix, a toy dataset) makes concepts stick.</li><li><strong>“Check if I want to go deeper” prevents walls of text.</strong> LLMs love to explain everything at once. This rule puts you in control of pacing. You decide when to zoom in on details vs. move to the next topic.</li><li><strong>The “BAD” example shows what to avoid.</strong> Negative examples help the model understand boundaries. Without it, you’ll get comprehensive but overwhelming responses.</li></ol><p><strong>Putting It Together:</strong> With these two pieces, you’re ready to start a conversation. The next section shows how to wire this up with lisette for a smooth multi-turn chat experience.</p><h3>Wiring Up the Conversation with Lisette</h3><p><a href="https://lisette.answer.ai/">lisette</a> handles the boilerplate of multi-turn conversations: tracking message history, formatting requests for different LLM providers, and managing context windows. Here’s the minimal setup:</p><pre>from lisette import Chat, contents<br>from IPython.display import HTML, Markdown, display<br>chat = Chat(<br>    model=&quot;ollama/qwen3-vl:8b&quot;,<br>    sp=&quot;&quot; # Insert prompt here<br>)</pre><p>That’s it. The model string follows litellm’s <a href="https://docs.litellm.ai/docs/#basic-usage">provider/model convention</a>. For Ollama, prefix with ollama/. For cloud providers, you’d use anthropic/claude-sonnet-4-20250514 or openai/gpt-4o.</p><p>To send a message and get a response:</p><pre>chat(&quot;Hi! I am Aayush&quot;)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/807/1*DLIhsBrlmjKAT62qj-LrKA.png" /><figcaption>Figure: Code output from above code</figcaption></figure><p>As we can see above, it’s as easy to start a conversation with a ollama running in your local machine. Chat class is designed to be persistent which means it remembers the previous conversations.</p><pre>chat(&quot;What&#39;s my name?&quot;)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/797/1*pwlLp6wU6SEg4TjvxdUzgw.png" /><figcaption>Figure: Code output from above code</figcaption></figure><p>As you can see model remembers my name from the previous conversation. Each call to chat() appends your message and the AI’s response to the conversation history. The next time you call it, the model sees the full exchange so far. Let’s see current history:</p><pre>chat.hist</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/815/1*AXD9QqqwJQYbaQe5OQRFpQ.png" /><figcaption>Figure: Code output from above code</figcaption></figure><h3>Live Example: Reading a DPP Paper</h3><p>Here’s a real exchange from when I was working through a paper on Determinantal Point Processes. For ease of reading in the blog post, lets create a class which can render the AI output in a terminal style for easy reading.</p><pre>def ask(chat, question, **kwargs):<br>    r = chat(question, **kwargs)<br>    text = contents(r).content<br>    display(HTML(f&quot;&quot;&quot;<br>    &lt;div style=&quot;background-color: #1e1e1e; color: #00ff00; padding: 12px; <br>                border-radius: 5px; font-family: monospace; white-space: pre-wrap;&quot;&gt;<br>&lt;span style=&quot;color: #00aaff;&quot;&gt;You:&lt;/span&gt; {question}<br>&lt;span style=&quot;color: #ffcc00;&quot;&gt;Tutor:&lt;/span&gt; {text}<br>    &lt;/div&gt;<br>    &quot;&quot;&quot;))<br>    <br>chat = Chat(<br>    model=&quot;ollama/qwen3-vl:8b&quot;,<br>    sp=system_prompt<br>)</pre><blockquote>Note: This time we are actually passing the prompt we created in previous section with paper text and our instructions.</blockquote><pre>q = &quot;&quot;&quot;<br>I want to learn about this paper. Help me.<br>&quot;&quot;&quot;<br>ask(chat, q, think=True)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/804/1*VfMGgGSBqueW8H-LH_lmJQ.png" /><figcaption>Figure: Code output from above code</figcaption></figure><pre>q = &quot;&quot;&quot;<br>I know about recommendation systems and work on a large recommender system which serves video content.<br>I am familiar with recommendation algorithms. I am interested in understanding how DPP works.<br>&quot;&quot;&quot;<br>ask(chat, q, think=True)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/802/1*eogBPLjU5pHhAV5fVAMIsQ.png" /><figcaption>Figure: Code output from above code</figcaption></figure><pre>q = &quot;&quot;&quot;<br>Yes I don&#39;t understand what determinant are, can you help me understand the basic intuition and use code examples?<br>I am fairly familiar with numpy.<br>&quot;&quot;&quot;<br>ask(chat, q)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/715/1*SvOrSVNWlfOhAUZnMQ3sRg.png" /><figcaption>Figure: Code output from above code</figcaption></figure><blockquote>Note: The rendering from Chat class is markdown style, which is more readable. The above examples are wrapped for easy separation b/w blog text and code output.</blockquote><h3>Managing Chat History: Checkpoints and Context Control</h3><p>One advantage of this approach over general chat interfaces is that you have full access to the conversation history. This matters when sessions get long or when the AI goes down a wrong path and you want to rewind.</p><h3>Accessing the History</h3><p>The Chat object stores messages in a list you can inspect and modify:</p><pre># See the current conversation<br>print(chat.hist)  # Returns list of message dicts<br># Check how many turns you&#39;ve had<br>print(len(chat.hist))</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/743/1*eWbvxapA4mL8VEMqWD_4lQ.png" /><figcaption>Figure: Code output from above code</figcaption></figure><h3>Creating Checkpoints</h3><p>Before exploring a tangent, save your current state:</p><pre># Save checkpoint before a risky question<br>checkpoint = len(chat.hist)<br># Ask something that might derail the conversation<br>ask(chat, &quot;Actually, can you explain all the math in one go?&quot;)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/715/1*GLZeRk9HmoUGvXTGZX1IdA.png" /><figcaption>Figure: Code output from above code</figcaption></figure><pre># That response was overwhelming. Rewind.<br>chat.hist = chat.hist[:checkpoint]<br>print(chat.hist[len(chat.hist)-1])</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/735/1*TtJ89Iq7r0GcZRZPBy4Y5A.png" /><figcaption>Figure: Code output from above code</figcaption></figure><h3>Why This Matters</h3><p>Two problems this solves:</p><ol><li><strong>Context bloat.</strong> Long conversations eat up your context window. If you spent 10 turns on a section you now understand, trim those exchanges and reclaim tokens for the next topic.</li><li><strong>Context poisoning.</strong> If the AI misunderstands something and you correct it, the wrong explanation still sits in history, potentially confusing later responses. Removing it gives you a cleaner slate.</li></ol><h3>Going Further: Paid LLM Options</h3><p>Local models are great for privacy and cost, but sometimes you need more firepower. Longer papers, denser math, or multi-step reasoning tasks can benefit from larger models like Claude or GPT-4o.</p><p>The good news: lisette uses litellm under the hood, so switching providers is a one-line change.</p><p>For cloud providers, you’ll need API keys. Set them as environment variables or use a .env file:</p><pre>import os<br>from dotenv import load_dotenv<br>load_dotenv()</pre><pre># Claude (Anthropic)<br>chat = Chat(model=&quot;anthropic/claude-haiku-4-5&quot;, sp=system_prompt)<br>ask(chat, &quot;Help me understand the paper&quot;)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/706/1*EY_Z8RVrhD04IbgWTKNKEA.png" /><figcaption>Figure: Code output from above code</figcaption></figure><p>As you can see the workflow stays identical. Your system prompt, conversation history, and teaching flow all transfer across providers.</p><h3>Key Takeaways</h3><p>Reading technical papers used to be a solitary grind. You either had the background to parse the notation, or you didn’t. AI changes this equation.</p><p>Here’s what I want you to walk away with:</p><ol><li><strong>The barrier to technical content has dropped.</strong> The same workflow that worked for this paper works for textbooks, API documentation, dense blog posts, or any content where you need concepts explained at your level. The AI adapts to you.</li><li><strong>You control the pace and depth.</strong> Unlike courses or tutorials that assume a fixed audience, this approach lets you skip what you know and linger on what confuses you. Your learning path, not someone else’s curriculum.</li><li><strong>Local LLMs make this nearly free.</strong> Ollama plus a capable open model means you can read papers all day without API costs. You can try reading books, YT transcripts, brainstorming using same method. One tool, many uses.</li><li><strong>The system prompt is the lever.</strong> Generic prompts give generic results. The tutor prompt we built forces the AI into a teaching mode that actually helps. Steal it, tweak it, make it yours.</li><li><strong>Start with a paper you actually care about.</strong> Don’t practice on something boring. Pick a paper relevant to your work or a concept you’ve always wanted to understand. Motivation carries you through the setup friction.</li></ol><p>The tools exist. The cost is minimal. The only question is which paper you’ll read first.</p><p>I hope you found this blog useful. If you have questions or want to share your own paper reading patterns, find me on <a href="https://www.linkedin.com/in/aayushmnit/">LinkedIn</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=9a8d920e07c4" width="1" height="1" alt=""><hr><p><a href="https://medium.com/data-science-collective/using-ai-to-read-technical-research-papers-9a8d920e07c4">Using AI to Read Technical Research Papers</a> was originally published in <a href="https://medium.com/data-science-collective">Data Science Collective</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Learning Claude Code — From Context Engineering to Multi-Agent Workflows]]></title>
            <link>https://medium.com/@aayushmnit/learning-claude-code-from-context-engineering-to-multi-agent-workflows-4825e216403f?source=rss-85dd669ac015------2</link>
            <guid isPermaLink="false">https://medium.com/p/4825e216403f</guid>
            <category><![CDATA[claude-code]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[ai-tools]]></category>
            <dc:creator><![CDATA[Aayush Agrawal]]></dc:creator>
            <pubDate>Sun, 25 Jan 2026 23:43:46 GMT</pubDate>
            <atom:updated>2026-02-01T03:59:29.269Z</atom:updated>
            <content:encoded><![CDATA[<h3>Learning Claude Code — From Context Engineering to Multi-Agent Workflows</h3><blockquote><em>This post captures what I learned from the Udemy course </em><a href="https://www.udemy.com/course/claudecode/"><em>“Claude Code Crash Course: Claude Code in a Day”</em></a><em>.</em></blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*g7bA0_Fjv8zMx7wA.jpg" /><figcaption>Figure: Claude Code Infographics. Credit: <a href="https://notebooklm.google.com/">NotebookLM</a></figcaption></figure><p><a href="https://aayushmnit.com/posts/2026-01-24_ClaudeCode/ClaudeCode.html">Read in browser</a> for best viewing experience.</p><p>If you’ve been following AI tooling trends, you’ve noticed something interesting: AI companies are going direct-to-developer. Cursor showed us what’s possible when you embed AI deeply into a code editor. But now Claude (Anthropic) and other foundation model providers are shipping their own coding agents. This isn’t just competition. It’s a signal that the “AI wrapper” layer is collapsing, and the companies building the models want to own the developer experience.</p><p>As an ML Engineer, I spend my days building systems that learn from data. But I’m also a practitioner who uses AI tools daily. When Claude Code launched, I wanted to understand it deeply, not just use it casually. What makes it different from Cursor or Copilot? What new patterns does it enable? And most importantly: how do I get the most out of it for real engineering work?</p><p>I’ll walk through the core concepts (context engineering, memory hierarchies, MCP), the practical commands you’ll use daily, and the advanced patterns like subagents and parallel workflows. Whether you’re evaluating Claude Code for your team or just curious about where AI-assisted development is heading, this should give you a clear picture.</p><p>If you are a video learner, I created a video using Notebook LM, which summarizes key concepts from the blog, watch here — <a href="https://youtu.be/soNYWPJO4Fk">Learning Claude Code</a></p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FsoNYWPJO4Fk%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DsoNYWPJO4Fk&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FsoNYWPJO4Fk%2Fhqdefault.jpg&amp;type=text%2Fhtml&amp;schema=youtube" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/9d8e255d1a2cc450e4955e2be0df8147/href">https://medium.com/media/9d8e255d1a2cc450e4955e2be0df8147/href</a></iframe><h3>Core Concept: Context Engineering</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*qWrLJDFhaxB0BNr8.jpg" /><figcaption>Figure: Context Engineering. Credit: <a href="https://notebooklm.google.com/">NotebookLM</a></figcaption></figure><p>If you’ve done any prompt engineering, context engineering is the next step. The idea is simple: prompts are static, but context is dynamic. Your agent needs information from many sources (your input, previous interactions, tool outputs, external data) and assembling the right context at the right time is what separates agents that work from agents that fail.</p><p>The course frames context engineering failures around three failure modes:</p><ul><li><strong>Context poisoning</strong>: A hallucination enters the context and corrupts every response that follows. Once bad information gets in, it spreads.</li><li><strong>Context confusion</strong>: Irrelevant context pulls the model’s attention in the wrong direction. You asked about database design but your frontend docs are drowning out the signal.</li><li><strong>Context clash</strong>: Contradictory information in the same context. The model gets pulled between conflicting instructions or facts.</li></ul><p>The takeaway here is “garbage in, garbage out,” but applied specifically to agents. LLMs can’t read your mind. They need the right information AND the right tools at the right time.</p><p>Claude Code implements four strategies to manage this:</p><ol><li><strong>Writing context</strong>: A three-tier memory hierarchy (user, project, dynamic imports) that persists information across sessions</li><li><strong>Intelligent retrieval</strong>: Automatically searching folders for relevant CLAUDE.md files and prioritizing recently-used context</li><li><strong>Context compression</strong>: /clear wipes everything, /compact summarizes to essentials</li><li><strong>Context isolation</strong>: Subagents run in their own context windows, preventing main conversation bloat</li></ol><p>The course also covers the “Goldilocks zone” for system prompts. Too specific and you’re treating the LLM like a state machine with hard-coded if/else logic. Too vague and you’re saying “do the right thing” without defining what “right” means. Good prompts establish clear identity, empower rather than constrain, and teach principles instead of enumerated rules.</p><h3>Getting Started: The Basics</h3><p>Before you can do anything interesting with Claude Code, you need to understand how it stores and retrieves information. Most tutorials jump straight to commands without explaining the underlying system. Let’s start there.</p><h3>Installation</h3><p>Getting Claude Code running is straightforward:</p><pre>npm install -g @anthropic-ai/claude-code<br>cd your-project<br>claude</pre><p>This drops you into an interactive terminal session (no GUI, no VS Code extension needed). But Claude Code doesn’t know anything about your project yet. It’s a blank slate every time you start.</p><h3>The /init Command and CLAUDE.md</h3><p>Run /init in your project directory. Claude will analyze your codebase and generate a CLAUDE.md file at the root. This file becomes Claude’s memory of your project: coding conventions, architecture decisions, file structure, anything it needs to work effectively.</p><p>Here’s what a real CLAUDE.md might look like:</p><pre># Project: recommendation-service<br>## Tech Stack<br>- Python 3.11, FastAPI<br>- PostgreSQL with SQLAlchemy<br>- Redis for caching</pre><pre>## Conventions<br>- All API endpoints follow REST naming<br>- Tests go in tests/ mirroring src/ structure<br>- Use Pydantic for all request/response models</pre><pre>## Important Context<br>- The ranking model lives in src/ranker/ and uses PyTorch<br>- Never modify files in src/legacy/ without explicit approval</pre><p>You can and should edit this file manually. The auto-generated version is a starting point. Here’s an example: without explicit guidance, Claude kept suggesting print() statements for debugging in the codebase. You can add “Use structured logging using python logging module , never print()” to CLAUDE.md, the problem disappears. Add the patterns your team follows, the things Claude keeps getting wrong, and the directories it should ignore.</p><h3>The Three-Tier Memory Hierarchy</h3><p>Claude Code reads context from three levels, with more specific context overriding more general:</p><pre>┌─────────────────────────────────────────────┐<br>│  ~/.claude/CLAUDE.md (User-level)           │<br>│  Your personal preferences across projects  │<br>│  e.g., &quot;Always use type hints&quot;              │<br>└─────────────────┬───────────────────────────┘<br>                  │ overridden by<br>                  ▼<br>┌─────────────────────────────────────────────┐<br>│  ./CLAUDE.md (Project-level)                │<br>│  Tech stack, conventions, architecture      │<br>│  Created by /init                           │<br>└─────────────────┬───────────────────────────┘<br>                  │ overridden by<br>                  ▼<br>┌─────────────────────────────────────────────┐<br>│  ./src/auth/CLAUDE.md (Dynamic imports)     │<br>│  Subdirectory-specific patterns             │<br>│  Loaded when working in that directory      │<br>└─────────────────────────────────────────────┘</pre><ol><li><strong>User-level</strong> (~/.claude/CLAUDE.md): Personal preferences that apply everywhere. Maybe you always want type hints, or you prefer pytest over unittest. This follows you across all projects.</li><li><strong>Project-level</strong> (./CLAUDE.md): Project-specific context. Tech stack, conventions, architecture. This is what /init creates.</li><li><strong>Dynamic imports</strong>: Other CLAUDE.md files scattered throughout your codebase. Put one in your src/auth/ folder to explain your authentication patterns. Claude will find and load it when working in that directory.</li></ol><blockquote><strong>Tip: </strong>Start with /init, then spend 10 minutes editing the generated CLAUDE.md. Add your team’s PR conventions, your testing requirements, any gotchas that would take a new engineer a week to discover. That 10 minutes will save you hours of correcting Claude later.</blockquote><h3>Essential Commands</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*_F2ZSFyjym-FXoRR.jpg" /><figcaption>Figure: Claude Code essentials. Credit: <a href="https://notebooklm.google.com/">NotebookLM</a></figcaption></figure><p>Claude Code is a terminal application, and like any good CLI tool, it’s driven by commands. But these aren’t just shortcuts. Each command maps to a context engineering strategy we discussed earlier. Understanding when to use which command is half the battle.</p><h3>Context Management</h3><p>These three commands control what Claude knows about your conversation:</p><pre>/clear    # Wipes everything, fresh start<br>/compact  # Summarizes conversation to essentials, keeps key info<br>/context  # Shows what&#39;s consuming your context window</pre><p>Use /context liberally. It breaks down token usage: system prompt, MCP tools, messages, memory files. When your responses start degrading, run this first to see what’s eating your budget. I’ve found MCP tools are often the silent killer here.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*qxGsS0eKifzK0isB.jpg" /><figcaption>Figure: Claude Code /context snapshot</figcaption></figure><p>The difference between /clear and /compact matters. If Claude is going in circles or referencing something incorrect from earlier, /clear is your friend. If you just need more room but want Claude to remember the gist of what you’ve discussed, /compact compresses without amnesia.</p><h3>Configuration</h3><pre>/config   # Opens settings panel<br>/model    # Switch between Opus and Sonnet<br>/cost     # Monitor token usage (API key mode only)</pre><p>Model selection is situational. When I asked Sonnet to refactor a ranking module, it jumped straight to code. Opus first asked clarifying questions about performance constraints and suggested three architectural approaches before writing anything. Opus thinks harder but costs more.</p><blockquote><strong>Model Selection Tip:</strong> Use Opus for planning and research, Sonnet for implementation. For most coding tasks, Sonnet hits the sweet spot between quality and cost.</blockquote><h3>Workflow</h3><pre>/init     # Analyze codebase, generate CLAUDE.md<br>/memory   # Open memory file in editor<br>/rewind   # Checkpoint system. Undo changes by prompt</pre><p>The /rewind command deserves special mention. Claude Code tracks file changes made during each prompt. Press Escape twice or run /rewind to see your conversation history with indicators of which prompts touched which files. You get three restore options: code only, conversation only, or both.This is your safety net for ambitious changes that don’t pan out.</p><h3>Modes</h3><p>Shift+Tab toggles Plan Mode. In this read-only mode, Claude can research, analyze, and create specs but cannot edit files. Use this when you want to think through a problem before committing to implementation. Shift+Tab+Tab enables deeper thinking for complex planning tasks.</p><h3>Other Useful Commands</h3><pre>/agents   # Manage subagents<br>/mcp      # View and manage MCP servers  <br>/plugins  # Browse and install plugins<br>/ide      # Integrate with your IDE (Cursor, VS Code)<br>/git      # GitHub integration setup</pre><p>TipContext Budget Tip</p><p>Get in the habit of running /context before starting a complex task. If you’re already at 50% usage, you might want to /compact first or spin up a fresh session. Running out of context mid-refactor means Claude forgets the files it already changed.</p><h3>Extending Claude Code</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*pIOFhuSlJeL4qsdb.jpg" /><figcaption>Figure: Extending Claude Code. Credit: <a href="https://notebooklm.google.com/">NotebookLM</a></figcaption></figure><p>Out of the box, Claude is smart but generic. The extension system lets you teach it your patterns, connect it to your tools, and automate repetitive tasks. There are five extension mechanisms: custom commands, MCP, hooks, skills, and plugins.</p><h3>Slash Commands</h3><p>Slash commands are custom actions you define and trigger manually with /commandname. Unlike skills (which activate automatically) or hooks (which fire on events), commands run when you explicitly call them.</p><p>A command is a markdown file in .claude/commands/:</p><pre>.claude/commands/<br>  review.md<br>  test-plan.md<br>  pr-summary.md</pre><p>Here’s a simple example, .claude/commands/review.md:</p><pre>---<br>description: Review code for common issues<br>---<br>Review the current file for:<br>1. Security vulnerabilities (SQL injection, XSS, auth bypasses)<br>2. Performance issues (N+1 queries, unnecessary loops)<br>3. Code style violations per our CLAUDE.md conventions<br><br>Format findings as a checklist with severity (high/medium/low).</pre><p>Now /review is available in your session. The description appears in autocomplete when you type /.</p><h4><strong>Arguments and Interpolation</strong></h4><p>Commands can accept arguments using $ARGUMENTS:</p><pre>---<br>description: Generate unit tests for a function<br>---<br>Write pytest tests for the function: $ARGUMENTS<br><br>Include:<br>- Happy path cases<br>- Edge cases (empty input, None, boundary values)  <br>- Error cases that should raise exceptions<br>Follow our test conventions in CLAUDE.md.</pre><p>Call it with /test-plan calculate_ranking_score and $ARGUMENTS becomes calculate_ranking_score.</p><h4><strong>Project vs User Commands</strong></h4><p>Like CLAUDE.md, commands exist at two levels:</p><ul><li><strong>Project commands</strong> (.claude/commands/) - Shared with your team via git. PR templates, project-specific workflows.</li><li><strong>User commands</strong> (~/.claude/commands/) - Personal shortcuts that follow you everywhere. Your preferred code review checklist, your debugging workflow.</li></ul><p>You can find a whole list of practical commands in this GitHub repo — <a href="https://github.com/wshobson/commands">wshobson/commands</a></p><blockquote>Tip: Commands are prompts you’ve optimized through repetition. When you find yourself typing the same multi-line instruction for the third time, turn it into a command. Five minutes of setup saves hours over a project’s lifetime.</blockquote><h3>MCP (Model Context Protocol)</h3><p>MCP is how Claude Code talks to external systems: GitHub, JIRA, Slack, databases, your internal tools.</p><p>Without MCP, every AI tool needs a custom integration for every external service. That’s N × M integrations (N tools × M services). MCP flips this to N + M. Implement MCP once and use it everywhere.</p><pre># Add an MCP server for Supabase<br>claude mcp add --transport stdio supabase \<br>  --env SUPABASE_ACCESS_TOKEN=YOUR_TOKEN \<br>  -- npx -y @supabase/mcp-server-supabase@latest<br><br># Add GitHub integration<br>claude mcp add github gh copilot mcp</pre><p>Run /mcp to see your active servers. Read more in Claude code official docs - <a href="https://code.claude.com/docs/en/mcp">Link</a></p><blockquote>MCP Context Implications: MCP servers consume context window space even when you’re not using them. I connected a handful of servers while experimenting and noticed my responses degrading on longer tasks. Running /context showed I’d burned 30% of my budget before writing a single prompt. Enable only what you need for the current project.</blockquote><h3>Hooks</h3><p>Hooks let you run custom code at specific points in Claude’s workflow. Think of them as event listeners: something happens inside Claude Code, your script executes.</p><p>There are many trigger points where we can invoke hooks, some notable ones are:</p><ul><li><strong>PreToolUse</strong> — Fires before Claude uses a tool (file write, bash command, etc.). Return non-zero to block the action.</li><li><strong>PostToolUse</strong> — Fires after a tool completes. Useful for validation or follow-up actions.</li><li><strong>Notification</strong> — Triggers when Claude sends a notification.</li><li><strong>Stop</strong> — Fires when Claude finishes responding.</li></ul><p>Hooks live in your settings file (~/.claude/settings.json for user-level, .claude/settings.json for project-level):</p><pre>{<br>  &quot;hooks&quot;: {<br>    &quot;PostToolUse&quot;: [{<br>      &quot;type&quot;: &quot;command&quot;,<br>      &quot;command&quot;: &quot;afplay /System/Library/Sounds/Glass.aiff&quot;,<br>      &quot;matcher&quot;: &quot;Write|Edit&quot;<br>    }]<br>  }<br>}</pre><p>This plays a sound whenever Claude edits a file. Seems trivial, but it’s genuinely useful. I can context-switch to Slack or documentation while Claude works through a refactor, then hear when it’s done writing. No more staring at a terminal waiting.</p><p>Other patterns examples:</p><ul><li>Auto-run pytest after test file modifications</li><li>Run black for code formatting after any file edit</li><li>Log all file changes to a separate audit file</li><li>Block writes to production config files</li></ul><p>Read more in claude code offical docs — <a href="https://code.claude.com/docs/en/hooks">Link</a></p><h3>Skills</h3><p>Skills are auto-invoked context providers. Unlike slash commands that you trigger manually, skills activate automatically when their description matches what you’re working on.</p><p>A skill is just a folder with a SKILL.md file:</p><pre>.claude/skills/<br>  explain-code/<br>    SKILL.md      # Description + instructions</pre><p>The SKILL.md includes a description field that Claude uses to decide when to load it. If you have a skill with description “Explains code with visual diagrams and analogies” and you ask Claude to explaing some code, it automatically pulls in that skill’s instructions. Here is an example SKILL.md file -</p><pre>---<br>name: explain-code<br>description: Explains code with visual diagrams and analogies. Use when explaining how code works, teaching about a codebase, or when the user asks &quot;how does this work?&quot;<br>---<br><br>When explaining code, always include:<br>1. **Start with an analogy**: Compare the code to something from everyday life<br>2. **Draw a diagram**: Use ASCII art to show the flow, structure, or relationships<br>3. **Walk through the code**: Explain step-by-step what happens<br>4. **Highlight a gotcha**: What&#39;s a common mistake or misconception?<br>Keep explanations conversational. For complex concepts, use multiple analogies.<br></pre><p>This differs from MCP in an important way. MCP tools are always present in the context, costing tokens whether you use them or not. Skills load on demand based on relevance. <a href="https://simonwillison.net/2025/Oct/16/claude-skills/">Simon Willison called skills “maybe a bigger deal than MCP” for this reason</a>.</p><p>Use skills for domain knowledge that applies situationally: your custom UI library patterns, how your team structures GraphQL, testing conventions for specific modules.</p><p>Read more in claude code offical docs — <a href="https://code.claude.com/docs/en/skills">Link</a></p><h3>Plugins</h3><p>Plugins bundle everything together. A plugin is a shareable package containing any combination of slash commands, subagents, skills, hooks, and MCP configurations.</p><p>The problem is distribution. You’ve spent hours configuring Claude Code perfectly for your team’s workflow. A new engineer joins. Without plugins, you’re walking them through manual setup. With plugins:</p><pre>/plugin install dev-toolkit@your-org</pre><p>Plugin structure:</p><pre>my-plugin/<br>├── .claude-plugin/<br>│   └── plugin.json    # Manifest<br>├── skills/            # Skills (optional)<br>├── commands/          # Slash commands (optional)<br>├── hooks/             # Hooks (optional)<br>├── .mcp.json          # MCP configs (optional)<br>└── README.md</pre><p>Teams can host private marketplaces internally. Plugins aren’t new functionality; they’re a distribution mechanism for the other extension types.</p><p>Read more on Claude Code official docs:</p><ul><li><a href="https://code.claude.com/docs/en/plugins">Create a Plugin</a></li><li><a href="https://code.claude.com/docs/en/discover-plugins">Discover and Install Prebuilt Plugins</a></li></ul><p>TipExtension Strategy</p><p>Start simple. Add one MCP server for your most-used external tool. Create one hook that enforces a rule you keep reminding Claude about. Build a skill for the pattern it keeps getting wrong. Plugins come later, when you have something worth sharing across your team.</p><h3>Advanced: Subagents and Parallel Workflows</h3><p>If you’ve used Claude Code for a while, you’ve probably hit the context wall. The conversation grows, responses degrade, and eventually you’re reaching for /clear or starting fresh. Subagents solve this problem by isolating heavy work into separate context windows.</p><h3>What Subagents Actually Are</h3><p>A subagent is a preconfigured AI personality that Claude Code can delegate tasks to. Each one has:</p><ul><li>Its own system prompt defining how it operates</li><li>Its own fresh context window (isolated from your main conversation)</li><li>A configurable set of tools (following least privilege)</li><li>Reusability across projects</li></ul><p>Think of it like delegating to a contractor. You write up what needs doing (the prompt), they show up with their own tools and expertise, do the work independently, and hand you back the result. You don’t need to know every step they took.</p><h3>Why Context Isolation Matters</h3><p>Imagine this, you are refactoring a module, you asked Claude to analyze the existing code, propose three approaches, implement the winner, write tests, and update documentation. By the time you got to tests, Claude had forgotten details from the analysis phase. The context window was stuffed with intermediate reasoning that no longer mattered.</p><p>Without subagents, your main conversation accumulates everything:</p><p>TaskWhat Happens“Analyze this module”Claude reads 15 files, adds analysis to context“Compare approaches”Previous analysis + new comparison reasoning“Implement option B”All of the above + implementation details“Write tests”Context is now bloated with stale analysis</p><p>Subagents sidestep this. They receive one prompt, work in their own window, and return one condensed response. Your main thread stays lean. If I’d delegated “analyze and propose approaches” to a subagent, I’d have gotten back a clean summary without the intermediate file contents clogging my context.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*mAQYW-5XVU42Nf7U.jpg" /><figcaption>Figure: The Subagent Pattern. Credit: <a href="https://notebooklm.google.com/">NotebookLM</a></figcaption></figure><h3>Creating Your First Subagent</h3><p>Run /agents and select “Create new agent”. You’ll specify:</p><ol><li>A name and description (this helps Claude decide when to delegate)</li><li>Which tools the subagent can access</li><li>Which model to use (Sonnet for most tasks, Opus for complex reasoning)</li><li>A system prompt with the agent’s personality and instructions</li></ol><p>Here’s a stripped down example for a code reviewer:</p><pre># Funny Code Reviewer<br>A senior engineer who reviews code with humor. Invoke when user says &quot;funny review&quot;.<br>## Tools<br>Read (read-only tools only)<br>## Model<br>sonnet<br>## System Prompt<br>You are a staff senior engineer with a dry sense of humor. Review code for bugs, <br>style issues, and improvement opportunities. Deliver feedback with wit but keep <br>it constructive. No write access needed.</pre><h3>Parallel Subagents: The /infinite Pattern</h3><p>Running multiple Claude Code instances on independent tasks is useful, but running parallel subagents takes this further. The /infinite command (credit to <a href="https://github.com/disler/infinite-agentic-loop/blob/189d92a771c12c6a4d5188e68bef9ba4dbbba263/.claude/commands/infinite.md">IndieDevDan’s repository</a>) demonstrates this pattern:</p><pre>/infinite specs/hero.spec components/heroes 6</pre><p>This spawns 6 subagents simultaneously, each implementing a UI component differently based on the spec file. Each agent works in complete isolation, unaware of the others. You review all variations and pick the best one.</p><p>The magic is in how it generates prompts. The command:</p><ol><li>Reads and analyzes your spec file</li><li>Surveys existing implementations in the output directory</li><li>Dynamically generates unique prompts for each subagent (Meta Prompting)</li><li>Assigns creative directions so agents don’t duplicate each other</li></ol><p>Six agents produced six genuinely different implementations: one focused on accessibility, another on animation, another on minimal dependencies. Without the dynamic prompt generation, you’d get six variations of the same approach.</p><h3>Git Worktrees for Multi-Agent Work</h3><p>When you’re running multiple Claude Code instances locally, git worktrees keep everything organized. A worktree lets you check out multiple branches into separate directories without cloning the repo again.</p><pre>cloud-code-crash-course/<br>├── hookup/              # Original (project/hookup branch)<br>├── agent-animations/    # Worktree 1 (separate branch)<br>└── agent-hero-redesign/ # Worktree 2 (separate branch)</pre><p>Each agent works on its own branch. If you like the result, merge it. If not, delete the worktree and branch. Your original code stays untouched throughout.</p><p>The course demonstrated this with three simultaneous Claude instances (two local worktrees, one cloud), each implementing a different feature. The instructor ran all three in parallel, then merged them into a single branch with Claude handling the conflict resolution. The whole workflow took maybe 15 minutes for what would have been an hour of sequential work.</p><h3>The Orchestration Tradeoff</h3><p>Parallel agents scale your output, but they add coordination overhead. You’re now a manager, not just a developer. During the course demo, I noticed the instructor spending as much time switching between terminals and reviewing outputs as the agents spent coding. The merge step also required judgment calls about which implementation to keep when agents made different architectural choices.</p><p>This is the real skill shift with multi-agent systems. You’re not writing code line by line. You’re writing specs, reviewing implementations, and deciding which AI-generated approach best fits your constraints. Git worktrees make the mechanics manageable, but the cognitive load moves from “how do I implement this” to “how do I evaluate these six implementations.”</p><p>For simple tasks, this overhead isn’t worth it. For exploratory work where you genuinely want multiple approaches, or for parallelizable features with clear specs, the time savings are real.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*GLkeTv8SZo8-DCzB.jpg" /><figcaption>Figure: The parallel workflow pattern. Credit: <a href="https://notebooklm.google.com/">NotebookLM</a></figcaption></figure><h3>Security: What AI Agents Handle Well (and What They Don’t)</h3><p>If you’re expecting Claude Code to handle security for you, adjust your expectations. A December 2025 study by <a href="https://blog.tenzai.com/bad-vibes-comparing-the-secure-coding-capabilities-of-popular-coding-agents/">Tenzai called “Bad Vibes:Comparing the Secure Coding Capabilities of Popular Coding Agents”</a> tested all the major coding agents (Cursor, Claude Code, Codex, Replit, Devin) by having each build identical applications. The result? 69 vulnerabilities across 15 apps. Every agent shipped vulnerable code.</p><h4>Solved Vulnerability Classes</h4><p>Agents consistently avoid problems with clear rules. SQL injection? They use parameterized queries. Cross-site scripting? Frameworks escape output properly. These vulnerability classes have built-in framework protections, so agents learned the patterns from their training data.</p><h4>Authorization and Business Logic</h4><p>This is where things fall apart. In the study, Codex allowed sellers to access any order in the system. Claude allowed unauthenticated users to delete files. Business logic vulnerabilities were everywhere: four out of five agents allowed negative quantities in shopping carts, three out of five allowed negative prices on products.</p><p>Security controls simply weren’t implemented. No CSRF protection. No security headers. Zero rate limiting on login (enabling brute force attacks). No account lockout. The agents didn’t implement these controls incorrectly. They didn’t implement them at all.</p><h4>The SSRF Problem</h4><p>All five agents introduced SSRF (Server-Side Request Forgery) vulnerabilities. There’s no universal rule for safe versus dangerous URL fetches because the answer depends entirely on context. When clear guardrails don’t exist, agents struggle.</p><p>The study confirms what experienced engineers already suspected: agents excel where deterministic rules exist and fail where judgment calls require understanding intent. I use Claude Code to move faster on implementation, but threat modeling and security architecture stay with me.</p><h3>Key Takeaways</h3><ol><li><strong>Context engineering is prompt engineering’s evolution.</strong> Static prompts don’t cut it for agents. You need dynamic context assembly from multiple sources, and Claude Code’s three-tier memory hierarchy (user, project, dynamic imports) gives you explicit control over what the model knows.</li><li><strong>The extension system follows a clear hierarchy.</strong> MCP connects external tools (always loaded, costs tokens). Skills load on-demand based on relevance (more efficient). Hooks enforce hard rules that override soft guidance. Plugins bundle everything for team distribution.</li><li><strong>Subagents solve the context bloat problem.</strong> Heavy tasks run in isolated context windows, returning condensed results. Your main conversation stays lean. Combine with git worktrees for parallel workflows that would take longer sequentially.</li><li><strong>Security remains your job.</strong> Agents handle rule-based vulnerabilities (SQLi, XSS) well but miss authorization, business logic, and SSRF consistently. Treat AI-generated code like junior developer code: review everything, especially access control.</li><li><strong>Start simple, extend as needed.</strong> Run /init, edit your CLAUDE.md, learn /compact and /rewind. Add one MCP server, one hook, one skill. Plugins and parallel agents come later when you’ve outgrown the basics.</li></ol><h3>Closing Thoughts</h3><p>Claude Code represents a shift in how we interact with AI coding tools. It’s not just autocomplete or chat. It’s an agent with memory, tools, and the ability to delegate work. The learning curve is steeper than Copilot, but the ceiling is higher too.</p><p>What surprised me most was how much the workflow resembles managing people. You write clear specs (CLAUDE.md), delegate tasks (subagents), set boundaries (hooks), and review output (git worktrees). The skill isn’t typing code faster. It’s communicating intent clearly and knowing when to trust the output.</p><p>I hope you found this walkthrough useful. If you have questions or want to share your own Claude Code patterns, find me on <a href="https://www.linkedin.com/in/aayushmnit/">LinkedIn</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=4825e216403f" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Diversity in Recommendations using Personalized DPP (pDPP)]]></title>
            <link>https://medium.com/data-science-collective/diversity-in-recommendations-using-personalized-dpp-pdpp-98b988efbec0?source=rss-85dd669ac015------2</link>
            <guid isPermaLink="false">https://medium.com/p/98b988efbec0</guid>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[data-science]]></category>
            <category><![CDATA[recommender-systems]]></category>
            <category><![CDATA[artficial-intelligence]]></category>
            <category><![CDATA[diversity]]></category>
            <dc:creator><![CDATA[Aayush Agrawal]]></dc:creator>
            <pubDate>Fri, 02 Jan 2026 06:06:58 GMT</pubDate>
            <atom:updated>2026-01-02T10:22:11.178Z</atom:updated>
            <content:encoded><![CDATA[<h4>Part 3 of the diversity series: Why one-size-fits-all diversity doesn’t work, and how to personalize it per user.</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*YeAlVwE7Gugf33qA.jpg" /><figcaption>Figure: pDPP infographic. Credit: <a href="https://notebooklm.google.com/">NotebookLM</a></figcaption></figure><p><a href="https://aayushmnit.com/posts/2026-01-01-DiversityPDppPart3/DiversityPDppPart3.html">Read in browser</a> for best viewing experience.</p><p>In <a href="https://medium.com/data-science-collective/diversity-in-recommendations-determinantal-point-processes-dpp-2427bf1b6324">Part 2</a>, we explored how DPP balances relevance and diversity using a single parameter α. But there’s a catch: we used the same α for every user.</p><p>Think about it. On a video platform like YouTube or Reels, you’ll find:</p><ul><li><strong>User A</strong>: Watches nothing but true crime documentaries. Their last 50 videos? All true crime. They know what they like.</li><li><strong>User B</strong>: Watches cooking tutorials on Monday, standup comedy on Wednesday, and tech reviews on weekends. Their history looks like a buffet.</li></ul><p>Should we show both users the same amount of diversity? Probably not.</p><p>User A has a clear, focused preference. Pushing diverse content might feel random or irrelevant to them. User B, on the other hand, has shown they <em>enjoy</em> variety. A narrowly focused feed might bore them.</p><p>This is the core limitation of standard DPP: it treats diversity as a system-level knob, not a user-level preference. The solution? <strong>Personalized DPP (pDPP)</strong>, which adapts the diversity-relevance tradeoff per user based on their historical behavior.</p><p>The idea comes from a <a href="https://arxiv.org/pdf/2004.06390">Huawei research paper on personalized re-ranking</a>, and the implementation is surprisingly simple: we measure how “diverse” each user’s past interactions have been using information theory (Shannon entropy), then use that to personalize α.</p><p>In this post, we’ll:</p><ul><li><strong>Quantify user diversity appetite</strong> using Shannon entropy over interaction history</li><li><strong>Derive a personalized α</strong> that scales diversity based on each user’s behavior</li><li><strong>Implement pDPP</strong> by extending our DPP code from Part 2</li><li><strong>Compare results</strong> side-by-side for narrow vs. broad-interest users</li></ul><p>Let’s get into it.</p><h3>The Core Insight: Users Have Different Diversity Appetites</h3><p>How do we measure a user’s appetite for diversity? The Huawei paper uses <strong>Shannon entropy</strong> over the user’s interaction history.</p><p>Shannon entropy measures the “surprise” or “spread” in a distribution. If a user watches videos from 10 genres equally, entropy is high (maximum uncertainty about what they’ll watch next). If they watch 95% from one genre, entropy is low (very predictable).</p><p>This gives us a principled, interpretable metric: we’re not guessing what users want; we’re measuring what they’ve demonstrated through their actions.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/730/1*SD4q9DkQ4P2LFKMB2teXaw.png" /></figure><p>The next step is to turn this entropy score into a personalized α value. A user with high entropy gets a higher α (more diversity weight), while a user with low entropy gets a lower α (more relevance weight).</p><h3>The Math: Computing User Entropy</h3><p>Let’s formalize the intuition from the previous section. For a user u, we compute Shannon entropy over their genre distribution:</p><p>H(u) = -Σ P(g|u) × log(P(g|u))</p><p>Where:</p><ul><li>P(g|u) is the probability of genre g in user u’s interaction history</li><li>The sum is over all genres the user has interacted with</li></ul><p>Let’s implement this:</p><pre>import numpy as np<br>from collections import Counter<br><br>def compute_entropy(genre_list: list[str]) -&gt; float:<br>    &quot;&quot;&quot;Compute Shannon entropy over a list of genre interactions.&quot;&quot;&quot;<br>    if not genre_list: return 0.0<br>    <br>    genre_counts = Counter(genre_list)<br>    total = len(genre_list)<br>    <br>    entropy = 0.0<br>    for count in genre_counts.values():<br>        p = count / total<br>        entropy -= p * np.log(p)<br>    <br>    return entropy<br><br># User A: watches mostly one genre (narrow taste)<br>user_a_genres = [&#39;action&#39;, &#39;action&#39;, &#39;action&#39;, &#39;action&#39;, &#39;comedy&#39;]<br># User B: watches many genres (diverse taste)<br>user_b_genres = [&#39;action&#39;, &#39;comedy&#39;, &#39;drama&#39;, &#39;documentary&#39;, &#39;horror&#39;]<br>print(f&quot;User A(narrow taste) entropy: {compute_entropy(user_a_genres):.3f}&quot;)<br>print(f&quot;User B(diverse taste) entropy: {compute_entropy(user_b_genres):.3f}&quot;)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/715/1*5SBzIPHdoX51EW5yR8MeUg.png" /><figcaption>Figure: Output of the code above</figcaption></figure><p>The numbers confirm our intuition:</p><ul><li><strong>User A (entropy = 0.500)</strong>: With 4 action + 1 comedy, their behavior is highly predictable. Low entropy means low “surprise” in what they’ll watch next.</li><li><strong>User B (entropy = 1.609)</strong>: With equal spread across 5 genres, their behavior is maximally uncertain. This is actually the maximum possible entropy for 5 categories: ln(5) ≈ 1.609.</li></ul><p><strong>What the values mean practically:</strong></p><ul><li>Entropy of 0 = watches only one genre (perfectly predictable)</li><li>Entropy of log(n) = watches n genres equally (maximum diversity)</li></ul><p>So User B’s entropy being exactly log(5) tells us they have a perfectly uniform distribution across genres. User A’s 0.500 is much closer to 0, reflecting their strong single-genre preference.</p><p>This entropy value becomes our “diversity propensity score” that we’ll use to personalize α in the next section.</p><h3>From Entropy to Personalized α</h3><p>Now we have each user’s entropy score. How do we turn it into a personalized α?</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/758/0*8CItQkAmpHzyaHlz.png" /><figcaption>Figure: Snapshot from <a href="https://arxiv.org/pdf/2004.06390">Huawei’s Personalized Re-ranking Paper</a>.</figcaption></figure><p><a href="https://arxiv.org/pdf/2004.06390">Huawei paper</a> uses a simple multiplicative formula:</p><p>α_u = f_u × α_0</p><p>Where:</p><ul><li>α_0 is your baseline diversity weight (the system default)</li><li>f_u is a user-specific scaling factor between 0 and 1</li></ul><p>The scaling factor f_u is computed via min-max normalization of the user’s entropy:</p><p>f_u = (H_u - H_min + l) / (H_max - H_min + l)</p><p>Where:</p><ul><li>H_u is the user’s entropy</li><li>H_min, H_max are the min/max entropy across your user population</li><li>l is a smoothing parameter</li></ul><p><strong>The </strong>l<strong> parameter as a personalization dial:</strong></p><ul><li>l = 0: Full personalization. Users at H_max get f_u = 1, users at H_min get f_u = 0</li><li>l → ∞: No personalization. Everyone gets f_u ≈ 0, so α_u ≈ 0 for all users</li><li>In practice, l in the range of H_max - H_min gives moderate personalization</li></ul><p>Let’s implement it:</p><pre>def compute_f_u(H_u, H_min, H_max, l=0):<br>    return (H_u - H_min + l) / (H_max - H_min + l)<br><br># Show how l controls personalization strength<br>print(&quot;Effect of smoothing parameter l on f_u:&quot;)<br>print(&quot;-&quot; * 50)<br>print(f&quot;{&#39;l&#39;:&lt;10} {&#39;User A f_u&#39;:&lt;15} {&#39;User B f_u&#39;:&lt;15} {&#39;Difference&#39;:&lt;10}&quot;)<br>print(&quot;-&quot; * 50)<br><br>H_a = compute_entropy(user_a_genres)  # ~0.5<br>H_b = compute_entropy(user_b_genres)  # ~1.609<br># For this example, assume population-wide bounds<br>H_min, H_max = 0.5, 1.61  # You&#39;d compute these from your user base<br>for l in [0, 0.25, 0.5, 1.0, 2.0]:<br>    f_a = compute_f_u(H_a, H_min, H_max, l)<br>    f_b = compute_f_u(H_b, H_min, H_max, l)<br>    print(f&quot;{l:&lt;10} {f_a:&lt;15.3f} {f_b:&lt;15.3f} {f_b - f_a:&lt;10.3f}&quot;)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/796/1*axZwHUhl4GJ3KGdQ_DhgMA.png" /><figcaption>Figure: Output of the code above</figcaption></figure><p>As l increases, the gap between User A and User B shrinks. At l=0, we get maximum personalization (f_u ranges from 0 to 1). At l=2, both users are clustered around 0.5, meaning they get nearly the same diversity treatment. This lets you tune how aggressively the system personalizes: start conservative with higher l, then decrease it as you gain confidence in the entropy signal.</p><p>Now let’s bring in our DPP functions from <a href="https://medium.com/data-science-collective/diversity-in-recommendations-determinantal-point-processes-dpp-2427bf1b6324">Part 2</a>.</p><pre>def greedy_map_dpp(L, k):<br>    N = L.shape[0]<br>    selected = []<br>    remaining = set(range(N))<br>    <br>    for _ in range(k):<br>        best_item, best_det = None, -1<br>        <br>        for item in remaining:<br>            candidate_set = selected + [item]<br>            det_val = np.linalg.det(L[np.ix_(candidate_set, candidate_set)])<br>            <br>            if det_val &gt; best_det:<br>                <br>                best_det = det_val<br>                best_item = item<br>        <br>        selected.append(best_item)<br>        remaining.remove(best_item)<br>    <br>    return selected<br><br>def youtube_dpp_kernel(relevance, embeddings, alpha=0.5, sigma=1.0):<br>    N = len(relevance)<br>    L = np.zeros((N, N))<br>    <br>    # Compute pairwise distances<br>    # D_ij = ||embedding_i - embedding_j||²<br>    for i in range(N):<br>        for j in range(N):<br>            if i == j:<br>                # Diagonal: quality squared<br>                <br>                L[i, i] = relevance[i] ** 2<br>            else:<br>                # Off-diagonal: scaled similarity with RBF kernel<br>                D_ij = np.sum((embeddings[i] - embeddings[j]) ** 2)<br>                similarity = np.exp(-D_ij / (2 * sigma**2))<br>                L[i, j] = alpha * relevance[i] * relevance[j] * similarity<br>    <br>    return L<br><br>def youtube_dpp_ranking(relevance, embeddings, k, alpha=0.5, sigma=1.0):<br>    W = list(range(len(relevance)))  # Remaining candidate indices<br>    R = []  # Final ranked list<br>    <br>    while len(W) &gt; 0:<br>        # Build kernel for current candidate pool<br>        rel_W = relevance[W]<br>        emb_W = embeddings[W]<br>        L = youtube_dpp_kernel(rel_W, emb_W, alpha=alpha, sigma=sigma)<br>        <br>        # Select up to k items from current pool<br>        window_size = min(k, len(W))<br>        M = greedy_map_dpp(L, k=window_size)<br>        <br>        # Map back to original indices<br>        selected_items = [W[i] for i in M]<br>        R.extend(selected_items)<br>        <br>        # Remove selected items from candidate pool<br>        W = [w for w in W if w not in selected_items]<br>    <br>    return R</pre><p>Let’s create the personalized version.</p><pre>def compute_user_alpha(genre_history, H_min, H_max, alpha_0, l=0):<br>    &quot;&quot;&quot;Compute personalized alpha for a user based on their genre history.&quot;&quot;&quot;<br>    H_u = compute_entropy(genre_history)<br>    f_u = compute_f_u(H_u, H_min, H_max, l)<br>    return f_u * alpha_0<br><br>def pdpp_ranking(relevance, embeddings, k, genre_history, H_min, H_max, alpha_0=1.0, l=0, sigma=1.0):<br>    &quot;&quot;&quot;Personalized DPP ranking using user&#39;s genre history.&quot;&quot;&quot;<br>    alpha_u = compute_user_alpha(genre_history, H_min, H_max, alpha_0, l)<br>    print(f&quot;User alpha: {alpha_u:.4f} (base alpha_0={alpha_0})&quot;)<br>    return youtube_dpp_ranking(relevance, embeddings, k, alpha=alpha_u, sigma=sigma)<br><br># 5 videos with relevance scores and genre embeddings (2D for simplicity)<br>relevance = np.array([0.9, 0.85, 0.8, 0.7, 0.6, 0.5])<br>genre = [&#39;Action&#39;, &#39;Action&#39;, &#39;Action&#39;, &#39;Comedy&#39;, &#39;Documentary&#39;, &#39;Mixed comedy/documentary&#39;]<br># Embeddings: action movies are close together, others spread out<br>embeddings = np.array([<br>    [1.0, 0.0, 0.0],   # video 0: pure action<br>    [0.9, 0.1, 0.0],   # video 1: action (very similar to 0)<br>    [0.8, 0.1, 0.1],   # video 2: action (also very similar to 0)<br>    [0.0, 1.0, 0.0],   # video 3: comedy<br>    [0.0, 0.0, 1.0],   # video 4: documentary<br>    [0.1, 0.5, 0.5],   # video 5: mixed<br>])<br># Global entropy bounds (you&#39;d compute these from your user population)<br>H_min, H_max = 0.5, 1.61<br># Compare rankings for User A (narrow taste) vs User B (diverse taste)<br>print(&quot;=== User A (narrow taste - prefers action) ===&quot;)<br>ranking_a = pdpp_ranking(relevance, embeddings, k=6, genre_history=user_a_genres, H_min=H_min, H_max=H_max, l=0)<br>print(f&quot;Top 6: {[genre[i]+&#39;(&#39;+str(relevance[i])+&#39;)&#39; for i in ranking_a[:6]]}&quot;)<br>print(&quot;=== User B (diverse taste) ===&quot;)<br>ranking_b = pdpp_ranking(relevance, embeddings, k=6, genre_history=user_b_genres, H_min=H_min, H_max=H_max, l=0)<br>print(f&quot;Top 6: {[genre[i]+&#39;(&#39;+str(relevance[i])+&#39;)&#39; for i in ranking_b[:6]]}&quot;)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/806/1*Yu7vVR8n_HJvmhZ0khD3iA.png" /><figcaption>Figure: Output of the code above</figcaption></figure><p>The results show exactly what we’d expect:</p><p><strong>User A (α ≈ 0.0)</strong> gets the three action movies upfront, ranked purely by relevance. With α near zero, diversity has almost no influence. The algorithm respects their focused history: “You’ve watched action 80% of the time, so here’s the best action content first.”</p><p><strong>User B (α ≈ 1.0)</strong> sees a completely different pattern. They get one action movie, then the algorithm immediately jumps to comedy, documentary, and mixed content. The other two action movies get pushed to positions 5 and 6. Their high entropy history translates to high α, which penalizes clustering similar items together.</p><p>Same 6 videos. Same relevance scores. Completely different experiences.</p><p>This is the power of pDPP: rather than treating diversity as a global system setting, it becomes a per-user preference learned directly from behavior. User A’s narrow feed isn’t a bug; it’s what their history tells us they want. User B’s varied feed isn’t random; it matches their demonstrated exploration pattern.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/737/0*CCynyN5dFWgujJVP.png" /><figcaption>Figure: pDPP example. Credit: <a href="https://notebooklm.google.com/">NotebookLM</a></figcaption></figure><h3>A Production Variant: Bounded Additive Formulation</h3><p>The multiplicative formula α_u = f_u × α_0 has a problem: it can be too aggressive. Look at User A’s result above: with f_u ≈ 0, they got α ≈ 0, meaning <em>zero</em> diversity consideration. In production, you probably don’t want to completely eliminate diversity for anyone.</p><p>The multiplicative approach also lacks intuitive bounds. If your team decides “we never want α below 0.4 or above 0.8,” the multiplicative formula doesn’t give you that control directly.</p><p><strong>The additive bounded formulation:</strong></p><p>Instead of multiplying, we use an additive offset centered around 0.5:</p><p>α_u = α_0 + (f_u - 0.5) × α_range</p><p>Where:</p><ul><li>α_0 is your baseline (center point)</li><li>α_range controls how far users can deviate from baseline</li><li>f_u - 0.5 shifts the range to [-0.5, +0.5], so the adjustment is symmetric</li></ul><p>This gives you explicit floor and ceiling:</p><ul><li>Floor: α_0 - 0.5 × α_range</li><li>Ceiling: α_0 + 0.5 × α_range</li></ul><p>For example, with α_0 = 0.6 and α_range = 0.4:</p><ul><li>Minimum α = 0.6 - 0.2 = <strong>0.4</strong> (most focused users)</li><li>Maximum α = 0.6 + 0.2 = <strong>0.8</strong> (most diverse users)</li></ul><pre>def compute_user_alpha_bounded(genre_history, H_min, H_max, alpha_0=0.6, alpha_range=0.4, l=0):<br>    &quot;&quot;&quot;Compute personalized alpha with explicit floor/ceiling bounds.&quot;&quot;&quot;<br>    H_u = compute_entropy(genre_history)<br>    f_u = compute_f_u(H_u, H_min, H_max, l)<br>    # Additive formulation: center at alpha_0, vary by alpha_range<br>    alpha_u = alpha_0 + (f_u - 0.5) * alpha_range<br>    return alpha_u<br><br>def pdpp_ranking_bounded(relevance, embeddings, k, genre_history, H_min, H_max, <br>                         alpha_0=0.6, alpha_range=0.4, l=0, sigma=1.0):<br>    &quot;&quot;&quot;Personalized DPP with bounded additive alpha.&quot;&quot;&quot;<br>    alpha_u = compute_user_alpha_bounded(genre_history, H_min, H_max, alpha_0, alpha_range, l)<br>    return youtube_dpp_ranking(relevance, embeddings, k, alpha=alpha_u, sigma=sigma)<br><br>print(&quot;=== User A (narrow taste) - Bounded ===&quot;)<br>ranking_a_bounded = pdpp_ranking_bounded(<br>    relevance, embeddings, k=6, genre_history=user_a_genres,<br>    H_min=H_min, H_max=H_max, alpha_0=0.6, alpha_range=0.4<br>)<br>print(f&quot;Top 6: {[genre[i]+&#39;(&#39;+str(relevance[i])+&#39;)&#39; for i in ranking_a_bounded]}\n&quot;)<br>print(&quot;=== User B (diverse taste) - Bounded ===&quot;)<br>ranking_b_bounded = pdpp_ranking_bounded(<br>    relevance, embeddings, k=6, genre_history=user_b_genres,<br>    H_min=H_min, H_max=H_max, alpha_0=0.6, alpha_range=0.4<br>)<br>print(f&quot;Top 6: {[genre[i]+&#39;(&#39;+str(relevance[i])+&#39;)&#39; for i in ranking_b_bounded]}&quot;)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/806/1*-Bh3KTDDTNNAk3hick6yqA.png" /><figcaption>Figure: Output of the code above</figcaption></figure><p>Now both users stay within the guardrails we set (0.4 to 0.8):</p><p><strong>User A (α = 0.4)</strong> still gets all three action movies first, but now there’s <em>some</em> diversity consideration baked in. They’re at the floor, not at zero. If we had more videos in the candidate pool, this baseline diversity would prevent completely homogeneous results.</p><p><strong>User B (α = 0.8)</strong> gets a diversified feed, but not as extreme as before. Compare to the multiplicative version where they had α ≈ 1.0: now the second action movie appears at position 4 instead of position 5. The ceiling prevents over-diversification.</p><p><strong>Why this matters in production:</strong></p><ul><li>You can guarantee minimum diversity for all users (prevents filter bubbles)</li><li>You can cap maximum diversity (prevents feeds that feel random)</li><li>The parameters are intuitive to tune: “baseline 0.6, users can vary ±0.2”</li><li>Product and engineering can align on explicit bounds before launch</li></ul><p>The tradeoff is less dramatic personalization. The multiplicative approach gave a 10x difference between User A and B (0.0 vs 1.0). The bounded approach gives a 2x difference (0.4 vs 0.8). Which is right depends on your product goals and risk tolerance.</p><h3>Production Considerations</h3><p>Deploying pDPP requires a few decisions that don’t show up in toy examples.</p><p><strong>Computing H_min and H_max</strong></p><p>You need population-wide entropy bounds. Two approaches:</p><ol><li><strong>Batch computation</strong>: Run a daily/weekly job over all active users, compute their entropy, store the min/max. Simple, but lags behind distribution shifts.</li><li><strong>Approximate with theory</strong>: If you have n genres, the theoretical bounds are H_min = 0 (user watches one genre) and H_max = ln(n) (uniform distribution). This avoids the batch job but may not reflect your actual user distribution.</li></ol><p>In practice, I’d recommend starting with theoretical bounds, then validating against your actual user distribution. If 99% of users fall between 0.3 and 1.2 but your theoretical max is 2.3, you’re wasting personalization range.</p><p><strong>Cold Start</strong></p><p>New users have no history, so no entropy. Options:</p><ul><li>Default to α_0 (the baseline) until they have N interactions</li><li>Use cohort-level entropy (users who signed up from similar sources tend to have similar diversity preferences)</li><li>Start slightly above baseline to encourage exploration, then let it settle</li></ul><p><strong>How Often to Update f_u</strong></p><p>Entropy is relatively stable for established users. Someone who’s watched 500 videos won’t see their entropy swing wildly from one more view. Options:</p><ul><li><strong>Batch (daily/weekly)</strong>: Simplest. Compute entropy offline, store in user profile.</li><li><strong>Nearline (per session)</strong>: Recompute when user starts a session. More responsive but adds latency.</li><li><strong>Streaming</strong>: Update incrementally with each interaction. Most responsive but adds complexity.</li></ul><p>For most systems, daily batch is sufficient. Entropy changes slowly.</p><p><strong>What Counts as “Genre”?</strong></p><p>The paper uses genre, but you can use any categorical attribute:</p><ul><li>Content category (comedy, drama, news)</li><li>Creator/channel</li><li>Content length bucket (shorts vs. long-form)</li><li>Engagement type (videos you comment on vs. passive watches)</li></ul><p>You can even combine multiple: compute entropy over (genre, creator_type) tuples. The key is choosing attributes that meaningfully capture “diversity” for your product.</p><h3>Key Takeaways</h3><ol><li><strong>Standard DPP’s single α ignores that users have different diversity appetites.</strong> Shannon entropy over interaction history gives us a principled way to measure each user’s diversity propensity from their behavior.</li><li><strong>pDPP personalizes the relevance-diversity tradeoff per user.</strong> High-entropy users (explorers) get more diversity; low-entropy users (focused) get more relevance.</li><li><strong>The bounded additive formulation is production-friendly.</strong> It gives explicit floor/ceiling control and prevents edge cases where diversity is completely eliminated.</li></ol><h3>References &amp; Further Reading</h3><ul><li><a href="https://arxiv.org/abs/2004.06390">Personalized Re-ranking for Recommendation</a> (Pei et al., 2020) — The Huawei paper introducing pDPP. Covers the entropy-based personalization approach and the parameterized min-max normalization formula.</li><li><a href="https://arxiv.org/abs/1207.6083">Determinantal Point Processes for Machine Learning</a> (Kulesza &amp; Taskar, 2012) — The definitive DPP tutorial. Essential background for understanding the kernel mechanics.</li><li><a href="https://dl.acm.org/doi/10.1145/3269206.3272018">Practical Diversified Recommendations on YouTube with Determinantal Point Processes</a> (Cheng et al., 2018) — YouTube’s production DPP implementation that we extended in this post.</li></ul><p><strong>Previous posts in this series:</strong></p><ul><li><a href="https://medium.com/data-science-collective/diversity-in-recommendations-maximal-marginal-relevance-mmr-0e7840c9399e">Part 1: Diversity in Recommendations using MMR </a>— Maximal Marginal Relevance as a simpler diversity approach.</li><li><a href="https://medium.com/data-science-collective/diversity-in-recommendations-determinantal-point-processes-dpp-2427bf1b6324">Part 2: Diversity in Recommendations using DPP </a>— DPP fundamentals and YouTube’s kernel parameterization.</li></ul><p>I hope this series has been useful for thinking about diversity in your recommendation systems! If you’re experimenting with pDPP or have questions, connect with me on <a href="https://www.linkedin.com/in/aayushmnit/">LinkedIn</a> to share your experience.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=98b988efbec0" width="1" height="1" alt=""><hr><p><a href="https://medium.com/data-science-collective/diversity-in-recommendations-using-personalized-dpp-pdpp-98b988efbec0">Diversity in Recommendations using Personalized DPP (pDPP)</a> was originally published in <a href="https://medium.com/data-science-collective">Data Science Collective</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Diversity in Recommendations — Determinantal Point Processes (DPP)]]></title>
            <link>https://medium.com/data-science-collective/diversity-in-recommendations-determinantal-point-processes-dpp-2427bf1b6324?source=rss-85dd669ac015------2</link>
            <guid isPermaLink="false">https://medium.com/p/2427bf1b6324</guid>
            <category><![CDATA[diversity]]></category>
            <category><![CDATA[recommender-systems]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[technical-writing]]></category>
            <dc:creator><![CDATA[Aayush Agrawal]]></dc:creator>
            <pubDate>Mon, 29 Dec 2025 22:04:45 GMT</pubDate>
            <atom:updated>2025-12-30T09:44:13.491Z</atom:updated>
            <content:encoded><![CDATA[<h3>Diversity in Recommendations — Determinantal Point Processes (DPP)</h3><blockquote><em>Part 2 of the diversity series: Moving from greedy MMR to principled probabilistic frameworks with DPPs</em></blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*JUnj-yT99PofTQK8.png" /><figcaption>Figure: DPP infographic. Credit: <a href="https://notebooklm.google.com/">NotebookLM</a></figcaption></figure><p><a href="https://aayushmnit.com/posts/2025-12-28-DiversityDPPPart2/DiversityDPPPart2.html">Read in browser</a> for best viewing experience.</p><p>In <a href="https://medium.com/data-science-collective/diversity-in-recommendations-maximal-marginal-relevance-mmr-0e7840c9399e">previous blog</a>, we explored MMR as a practical way to inject diversity into recommendation results. MMR works well, but it has a fundamental limitation: it’s greedy and myopic. At each step, it only considers similarity to items <em>already selected</em>, not the global structure of the final set.</p><p>What if we could evaluate the diversity of an <em>entire set</em> at once? That’s exactly what Determinantal Point Processes (DPPs) offer. DPPs come from probability theory and give us a principled way to assign higher probability to sets of items that are both high-quality and diverse. For example, MMR might select two sci-fi thrillers that individually seem distinct from other picks, but together make the final set feel repetitive. DPPs evaluate the whole set at once, catching this overlap.</p><p>In this post, we’ll:</p><ul><li>Build geometric intuition for DPPs through determinants as volume</li><li>Understand the quality-diversity decomposition for production systems</li><li>Implement greedy MAP inference step-by-step</li><li>Explore YouTube’s production approach with tunable parameters (α, σ)</li><li>Walk through the windowed algorithm for ranking entire feeds</li><li>Compare DPP and MMR on our video recommendation example</li></ul><h3>Linear Algebra Refresher: Determinants as Volume</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*Y2rfs32i364a8f53.png" /><figcaption>Figure: Mathematical Intuition behind DPP. Credit: <a href="https://notebooklm.google.com/">NotebookLM</a></figcaption></figure><p>Before diving into DPPs, we need to refresh one key concept: <strong>determinants measure volume</strong>.</p><p>Consider two vectors in 2D space. The determinant of the matrix formed by these vectors equals the area of the parallelogram they span.</p><pre>import numpy as np<br>import matplotlib.pyplot as plt<br><br># Two similar vectors (small area)<br>v1 = np.array([1, 0])<br>v2 = np.array([0.9, 0.1])<br>similar_matrix = np.array([v1, v2])<br>print(f&quot;Similar vectors determinant: {np.linalg.det(similar_matrix):.3f}&quot;)<br><br># Two orthogonal vectors (maximum area)<br>v1 = np.array([1, 0])<br>v2 = np.array([0, 1])<br>diverse_matrix = np.array([v1, v2])<br>print(f&quot;Orthogonal vectors determinant: {np.linalg.det(diverse_matrix):.3f}&quot;)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/1*6LQejTzYwQKlVbndssbtmA.png" /><figcaption>Figure: Output of above block of code.</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/994/0*80BY3v-NzTRj46A0.png" /><figcaption>Figure: Showing intuition behind determinants using similar and orthogonal vectors</figcaption></figure><p>This is the geometric intuition behind DPPs: <strong>diverse sets span more volume, giving them higher determinants</strong>. When vectors point in similar directions, they span a thin, squashed parallelogram. When they’re spread out, the volume is large.</p><p>This extends to higher dimensions. For <em>k</em> vectors in <em>d</em>-dimensional space, the determinant of their Gram matrix (L = VVᵀ) measures the squared volume of the k-dimensional parallelotope they span.</p><p>Note: Gram matrix is also called the “kernel matrix” in <a href="https://arxiv.org/abs/1207.6083">DPP literature</a>.</p><pre># 3 items in 3D feature space<br>items = np.array([<br>    [1, 0, 0],    # Item A<br>    [0, 1, 0],    # Item B  <br>    [0, 0, 1],    # Item C (diverse from A, B)<br>])<br>L = items @ items.T  # Gram matrix<br>print(f&quot;Diverse set det(L): {np.linalg.det(L):.3f}&quot;)<br><br># Replace C with something similar to A<br>items_similar = np.array([<br>    [1, 0, 0],      # Item A<br>    [0, 1, 0],      # Item B<br>    [0.8, 0.1, 0.1],  # Item C&#39; (similar to A)<br>])<br>L_similar = items_similar @ items_similar.T<br>print(f&quot;Similar set det(L): {np.linalg.det(L_similar):.3f}&quot;)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/792/1*54A7Af47I6p83n5Rr-B0SA.png" /><figcaption>Figure: Output of above block of code.</figcaption></figure><p>The diverse set has 100x higher determinant. This is the core of DPPs: <strong>recommendation probability proportional to determinant means diverse sets are exponentially more likely</strong>.</p><h3>DPP Fundamentals: From Volume to Probability</h3><p>Now we can define what a DPP actually is. Given a ground set of N items (our candidate pool), a DPP defines a probability distribution over all possible subsets. The key formula is:</p><p>P(Y) ∝ det(L_Y)</p><p>where:</p><ul><li>Y is a subset of items we might recommend</li><li>L is an N×N positive semi-definite kernel matrix capturing item relationships</li><li>L_Y is the submatrix of L indexed by items in Y</li></ul><p>The probability of selecting a set is proportional to the determinant of its kernel submatrix. From our linear algebra refresher, we know determinants measure volume, so <strong>diverse sets that span more volume get higher probability</strong>.</p><p>Let’s use the same video recommendation example from Part 1:</p><pre>relevance = np.array([0.9, 0.85, 0.8, 0.7, 0.6, 0.5])<br>genre = [&#39;Action&#39;, &#39;Action&#39;, &#39;Action&#39;, &#39;Comedy&#39;, &#39;Documentary&#39;, &#39;Mixed comedy/documentary&#39;]<br>embeddings = np.array([<br>    [1.0, 0.0, 0.0],   # video 0: pure action<br>    [0.9, 0.1, 0.0],   # video 1: action (very similar to 0)<br>    [0.8, 0.1, 0.1],   # video 2: action (also very similar to 0)<br>    [0.0, 1.0, 0.0],   # video 3: comedy<br>    [0.0, 0.0, 1.0],   # video 4: documentary<br>    [0.1, 0.5, 0.5],   # video 5: mixed<br>])<br># Build kernel matrix L = VVᵀ (Gram matrix from embeddings)<br>L = embeddings @ embeddings.T<br>print(&quot;Kernel matrix L:&quot;)<br>print(np.round(L, 2))<br>print(f&quot;\nSimilarity of video 0 ({genre[0]}), with other videos&quot;)<br>for i in range(1,len(genre)):<br>    print(f&quot;Video {i}: {L[0][i]} ({genre[i]})&quot;)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/792/1*NBFKdKWKobWuuOOX1Z9ipA.png" /><figcaption>Figure: Output of above block of code</figcaption></figure><p>Notice how videos 0, 1, 2 (all Action) have high similarity (~0.8–0.9), while Action vs Comedy is 0. Now let’s compare the DPP probability of two different 3-video sets:</p><pre>def subset_det(L, indices):<br>    &quot;&quot;&quot;Compute det(L_Y) for a subset of indices&quot;&quot;&quot;<br>    L_Y = L[np.ix_(indices, indices)]<br>    return np.linalg.det(L_Y)<br><br># Set 1: Three action videos (redundant)<br>redundant = [0, 1, 2]  # Action, Action, Action<br>det_redundant = subset_det(L, redundant)<br>print(f&quot;Redundant set: {[genre[i] for i in redundant]}&quot;)<br>print(f&quot;  det(L_Y) = {det_redundant:.4f}&quot;)<br># Set 2: Action + Comedy + Documentary (diverse)<br>diverse = [0, 3, 4]<br>det_diverse = subset_det(L, diverse)<br>print(f&quot;\nDiverse set: {[genre[i] for i in diverse]}&quot;)<br>print(f&quot;  det(L_Y) = {det_diverse:.4f}&quot;)<br>print(f&quot;\nDiverse set is {det_diverse/det_redundant:.0f}x more likely under DPP&quot;)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/798/1*01ZkUxKhopbphY274AOxng.png" /><figcaption>Figure: Output of above block of code</figcaption></figure><p>The diverse set spanning Action, Comedy, and Documentary is <strong>10000x more likely</strong> than picking three similar action videos! This is the DPP magic: it naturally penalizes redundancy by evaluating the entire set at once, rather than making greedy pairwise comparisons like MMR.</p><p>NoteWhy L(Kernel Matrix) needs to be Positive Semi-Definite (PSD)</p><blockquote>Note: Why L(Kernel Matrix) needs to be Positive Semi-Definite (PSD)</blockquote><blockquote>L needs to be <strong>positive semi-definite (PSD)</strong>. Practically, this means:</blockquote><blockquote>All eigenvalues are ≥ 0</blockquote><blockquote>Determinants of all submatrices are ≥ 0 (which we need for valid probabilities!)</blockquote><blockquote><strong>Why DPPs require PSD</strong>: Since P(Y) ∝ det(L_Y), we need det(L_Y) ≥ 0 for all subsets. PSD guarantees this.</blockquote><blockquote><strong>Good news for practitioners</strong>: When you build L = VVᵀ (Gram matrix from embeddings), it’s <em>automatically</em> PSD. So as long as you construct your kernel matrix as L = VVᵀ (or use standard similarity kernels like RBF), you’re safe. You only run into trouble if you manually construct L with arbitrary values.</blockquote><h3>Quality vs Diversity Decomposition</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*OerDtmfGBerWpsrY.png" /><figcaption>Figure: Quality diversity tradeoff in DPP. Credit: <a href="https://notebooklm.google.com/">NotebookLM</a></figcaption></figure><p>So far, our kernel matrix L only captures similarity between items. But in real recommendations, we care about <em>both</em> quality (relevance) and diversity. A diverse set of low-quality items is useless.</p><p>DPPs handle this elegantly through the <strong>quality-diversity decomposition</strong>:</p><p>L[i,j] = q_i × q_j × S[i,j]</p><p>where:</p><ul><li>q_i is the quality (relevance) score for item i</li><li>S[i,j] is the similarity between items i and j (from embeddings)</li></ul><p>This decomposes naturally: diagonal entries L[i,i] = q_i² capture item quality, while off-diagonal entries capture similarity scaled by both items’ quality. High-quality similar items create larger off-diagonal terms, which <em>reduce</em> the determinant (penalizing redundancy among your best items).</p><p>Let’s rebuild our kernel matrix with relevance scores:</p><pre># Similarity matrix from embeddings<br>S = embeddings @ embeddings.T<br><br># Quality-weighted kernel: L[i,j] = q_i * q_j * S[i,j]<br>q = relevance.reshape(-1, 1)  # Column vector<br>L_quality = (q @ q.T) * S<br>print(&quot;Quality-weighted kernel L:&quot;)<br>print(np.round(L_quality, 3))</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/794/1*SQ9H7_PUST16754QNOUPDg.png" /><figcaption>Figure: Output of above block of code</figcaption></figure><p>Let’s compare the two sets again with the new kernel matrix:</p><pre>redundant = [0, 1, 2]  # Top 3 by relevance, all Action<br>diverse = [0, 3, 4]    # Action + Comedy + Documentary<br><br>det_redundant = subset_det(L_quality, redundant)<br>det_diverse = subset_det(L_quality, diverse)<br>print(f&quot;Redundant set (top-3 relevance): {[f&#39;{genre[i]} ({relevance[i]})&#39; for i in redundant]}&quot;)<br>print(f&quot; det = {det_redundant:.6f}&quot;)<br>print(f&quot;\nDiverse set (mixed relevance): {[f&#39;{genre[i]} ({relevance[i]})&#39; for i in diverse]}&quot;)<br>print(f&quot; det = {det_diverse:.6f}&quot;)<br>print(f&quot;\nDiverse set is {det_diverse/det_redundant:.0f}x more likely&quot;)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/1*QV0fWCERPNL_c19CYh_HAg.png" /><figcaption>Figure: Output of above block of code</figcaption></figure><p>Even though the diverse set includes lower-relevance items (0.7, 0.6 vs 0.85, 0.8), the diversity bonus from spanning different genres still makes it far more probable (3815x as compared to 10000x before). The quality weighting ensures we don’t pick diverse but irrelevant items.</p><p><strong>The intuition</strong>: Think of q_i as the “length” of item i’s feature vector. The determinant measures the volume spanned by these vectors. A high-quality item contributes a longer vector, but if two long vectors point in the same direction (similar items), the parallelogram is still flat. You need both length (quality) AND spread (diversity) to maximize volume.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/896/0*uhOvV9F-aaamj1qz.png" /><figcaption>Figure: Showing intuition behind determinants using Quality vs Diversity tradeoff</figcaption></figure><p>This maps directly to production recsys:</p><ul><li>q_i = your relevance model output (e.g., predicted click probability)</li><li>S[i,j] = Similarity of content embeddings (or 1 - distance in any embedding space)</li><li>L = the kernel that balances both signals</li></ul><h3>Greedy MAP: The Practical DPP Algorithm</h3><p>We’ve established that DPPs assign higher probability to diverse, high-quality sets via determinants. The natural question: how do we find the best set?</p><p><strong>The challenge</strong>: Finding the subset Y that maximizes det(L_Y) is called <strong>MAP (Maximum A Posteriori) inference</strong>. With N candidates, there are 2^N possible subsets. This is NP-hard in general.</p><p><strong>The solution</strong>: <strong>Greedy MAP approximation</strong>. Build the set one item at a time, always adding whichever item increases the determinant most. This runs in O(Nk³) time and provides provably good approximations, where N is the number of candidates to select from and k is the number of items to select.</p><h3>Walking Through Greedy MAP Step-by-Step</h3><p>Let’s trace through selecting 3 videos from our candidate pool. At each step, we try adding every remaining candidate and pick whoever maximizes det(L_Y).</p><p><strong>Step 1: First Selection</strong></p><p>With an empty set, we’re just comparing single-item determinants. For a 1×1 matrix, det([L[i,i]]) = L[i,i] = q_i². So the first pick is simply the highest-quality item:</p><pre>print(&quot;Step 1: Evaluating single-item determinants (= quality squared)&quot;)<br>for i in range(len(relevance)):<br>    det_single = L_quality[i, i]<br>    print(f&quot;  Video {i} ({genre[i]}): det = {det_single:.3f} (relevance² = {relevance[i]**2:.3f})&quot;)<br><br>first_pick = int(np.argmax([L_quality[i,i] for i in range(len(relevance))]))<br>print(f&quot;\n→ Select video {first_pick} ({genre[first_pick]})&quot;)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/803/1*VJqGL72vNz5inhidbqpeyQ.png" /><figcaption>Figure: Output of above block of code</figcaption></figure><p><strong>Step 2: Second Selection</strong></p><p>Now it gets interesting. We evaluate each remaining candidate by computing det(L_Y) for the 2-item set:</p><pre>selected = [first_pick]<br>print(f&quot;Step 2: Current set = [Video {first_pick}]&quot;)<br>print(&quot;Evaluating candidates:&quot;)<br><br>for candidate in range(len(relevance)):<br>    if candidate in selected:<br>        continue<br>    <br>    subset = selected + [candidate]<br>    L_subset = L_quality[np.ix_(subset, subset)]<br>    det_val = np.linalg.det(L_subset)<br>    <br>    # Show the 2x2 matrix being evaluated<br>    print(f&quot;\n  Video {candidate} ({genre[candidate]}):&quot;)<br>    print(f&quot;    L_subset = [[{L_subset[0,0]:.3f}, {L_subset[0,1]:.3f}],&quot;)<br>    print(f&quot;                [{L_subset[1,0]:.3f}, {L_subset[1,1]:.3f}]]&quot;)<br>    print(f&quot;    det = {L_subset[0,0]:.3f} × {L_subset[1,1]:.3f} - {L_subset[0,1]:.3f} × {L_subset[1,0]:.3f} = {det_val:.4f}&quot;)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/799/1*6hIIWrhKavLXJOV5ySYSvQ.png" /><figcaption>Figure: Output of above block of code</figcaption></figure><p>Notice how adding another Action video (high off-diagonal similarity) leads to a lower determinant, while Comedy or Documentary (zero similarity to Action) leads to a higher determinant. In this case we will pick Video 3 as it has the highest determinant of 0.3969.</p><pre># Find second pick<br>best_det, second_pick = -1, None<br>for candidate in range(len(relevance)):<br>    if candidate in selected:<br>        continue<br>    subset = selected + [candidate]<br>    det_val = np.linalg.det(L_quality[np.ix_(subset, subset)])<br>    if det_val &gt; best_det:<br>        best_det, second_pick = det_val, candidate<br><br>print(f&quot;\n→ Select video {second_pick} ({genre[second_pick]}), det = {best_det:.4f}&quot;)<br>selected.append(second_pick)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/801/1*rGcJca1lwjxCMx3XtWumIQ.png" /><figcaption>Figure: Output of above block of code</figcaption></figure><p><strong>Step 3: Third Selection</strong></p><p>Same process with a 3×3 determinant:</p><pre>print(f&quot;Step 3: Current set = {selected}&quot;)<br>print(&quot;Evaluating candidates:&quot;)<br><br>for candidate in range(len(relevance)):<br>    if candidate in selected:<br>        continue<br>    <br>    subset = selected + [candidate]<br>    det_val = np.linalg.det(L_quality[np.ix_(subset, subset)])<br>    print(f&quot;  Video {candidate} ({genre[candidate]}): det = {det_val:.4f}&quot;)<br># Find third pick<br>best_det, third_pick = -1, None<br>for candidate in range(len(relevance)):<br>    if candidate in selected:<br>        continue<br>    subset = selected + [candidate]<br>    det_val = np.linalg.det(L_quality[np.ix_(subset, subset)])<br>    if det_val &gt; best_det:<br>        best_det, third_pick = det_val, candidate<br>selected.append(third_pick)<br>print(f&quot;\n→ Select video {third_pick} ({genre[third_pick]})&quot;)<br>print(f&quot;\nFinal selection: {[genre[i] for i in selected]}&quot;)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/798/1*5SyQzuE5i7qob1aSrvBdLA.png" /><figcaption>Figure: Output of above block of code</figcaption></figure><h3>The Complete Implementation</h3><p>Now we can wrap this into a clean function:</p><pre>def greedy_map_dpp(L, k):<br>    N = L.shape[0]<br>    selected = []<br>    remaining = set(range(N))<br>    <br>    for _ in range(k):<br>        best_item, best_det = None, -1<br>        <br>        for item in remaining:<br>            candidate_set = selected + [item]<br>            det_val = np.linalg.det(L[np.ix_(candidate_set, candidate_set)])<br>            <br>            if det_val &gt; best_det:<br>                <br>                best_det = det_val<br>                best_item = item<br>        <br>        selected.append(best_item)<br>        remaining.remove(best_item)<br>    <br>    return selected<br><br># Run it<br>result = greedy_map_dpp(L_quality, k=3)<br>print(f&quot;Greedy MAP result: {[f&#39;{genre[i]} ({relevance[i]})&#39; for i in result]}&quot;)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/1*PrPEkwj1Feko0k3Lz6nzDA.png" /><figcaption>Figure: Output of above block of code</figcaption></figure><p>Pure relevance ranking would give us Action, Action, Action. DPP gives us Action, Comedy, Documentary. Three genres covered in three slots.</p><h3>Tuning the Quality-Diversity Tradeoff</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*YBe21auSMp7bOCh8.png" /><figcaption>Figure: Youtube’s DPP Implementation. Credit: <a href="https://notebooklm.google.com/">NotebookLM</a></figcaption></figure><p>In production, you need a way to control how much the system emphasizes relevance versus diversity. YouTube’s approach, described in their <a href="https://dl.acm.org/doi/epdf/10.1145/3269206.3272018">2018 paper on DPPs for recommendations</a>, provides a clean parameterization that’s proven to work at scale.</p><h3>YouTube’s Kernel Parameterization</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/565/0*YnHsEzxnm2XXyCJO.png" /><figcaption>Figure: Snapshot of DPP kenel matrix formulation from <a href="https://dl.acm.org/doi/epdf/10.1145/3269206.3272018">YT DPP paper</a></figcaption></figure><p>Instead of the simple L[i,j] = q_i × q_j × S[i,j] formulation, YouTube uses:</p><p>L[i,i] = q_i² (diagonal: quality squared)</p><p>L[i,j] = α × q_i × q_j × exp(-D_ij / 2σ²) for i ≠ j (off-diagonal: scaled similarity)</p><p>where:</p><ul><li>q_i is the relevance score for item i</li><li>D_ij is the distance between items i and j in embedding space</li><li>α controls the overall strength of diversity (0 ≤ α ≤ 1)</li><li>σ controls the length scale of similarity</li></ul><p><strong>The key insight</strong>: The diagonal entries (quality) remain unchanged, but off-diagonal entries (similarity) are scaled by α. This gives you direct control over the diversity penalty.</p><blockquote>Note: This formulation is identical to a standard (Gaussian) radial basis function (RBF) kernel when α = 1.</blockquote><h3>Understanding the Parameters</h3><p><strong>α (diversity strength)</strong>:</p><ul><li>α = 1: Full diversity penalty. Similar items strongly repel each other.</li><li>α = 0: No diversity penalty. Reduces to pure relevance ranking.</li><li>α ∈ (0,1): Smooth tradeoff between quality and diversity.</li></ul><p><strong>σ (similarity scale)</strong>:</p><ul><li>Small σ: Only very similar items are considered redundant (tight clusters).</li><li>Large σ: Even moderately different items are penalized (broad diversity).</li></ul><p>Let’s implement this new kernel building function:</p><pre>def youtube_dpp_kernel(relevance, embeddings, alpha=0.5, sigma=1.0):<br>    N = len(relevance)<br>    L = np.zeros((N, N))<br>    <br>    # Compute pairwise distances<br>    # D_ij = ||embedding_i - embedding_j||²<br>    for i in range(N):<br>        for j in range(N):<br>            if i == j:<br>                # Diagonal: quality squared<br>                <br>                L[i, i] = relevance[i] ** 2<br>            else:<br>                # Off-diagonal: scaled similarity with RBF kernel<br>                D_ij = np.sum((embeddings[i] - embeddings[j]) ** 2)<br>                similarity = np.exp(-D_ij / (2 * sigma**2))<br>                L[i, j] = alpha * relevance[i] * relevance[j] * similarity<br>    <br>    return L<br><br># Build kernel with YouTube&#39;s approach<br>L_youtube = youtube_dpp_kernel(relevance, embeddings, alpha=0.5, sigma=1.0)<br>print(&quot;YouTube DPP kernel (α=0.5, σ=1.0):&quot;)<br>print(np.round(L_youtube, 3))</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/1*FSlqywnSAuRxWrvQ9XX5Qg.png" /><figcaption>Figure: Output of above block of code</figcaption></figure><h3>Sweeping the α Parameter</h3><p>Let’s see how α affects the diversity-quality tradeoff:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/795/1*QlYK69faiQt6oy7dE2MT3w.png" /><figcaption>Figure: Output of alpha parameter sweep</figcaption></figure><p><strong>Key observations</strong>:</p><ul><li><strong>α = 0.0–0.25</strong>: Pure relevance ranking. Off-diagonal terms are too weak to overcome quality differences, so we get the top-3 by relevance (all Action).</li><li><strong>α = 0.5</strong>: Moderate diversity kicks in. The algorithm trades Video 2 (Action, 0.80) for Video 3 (Comedy, 0.70), accepting a small relevance drop for genre diversity.</li><li><strong>α = 0.75–1.0</strong>: Strong diversity preference. The algorithm maximizes spread across genres (Action, Comedy, Documentary), dropping Video 1 (Action, 0.85) in favor of lower-relevance items from different genres.</li></ul><p>The transition happens around α = 0.5, where the diversity penalty becomes strong enough to outweigh small relevance differences. In production, you’d tune α based on metrics like session diversity, user engagement, or A/B test results.</p><h3>Sweeping the σ Parameter</h3><p>The σ parameter controls what counts as “similar”. Let’s fix α = 0.75 and vary σ:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/784/1*HFdlQTQNC0w_OlRvM19m8w.png" /><figcaption>Figure: Output of sigma parameter sweep</figcaption></figure><p><strong>Key observations</strong>:</p><ul><li><strong>σ = 0.1</strong>: Very tight similarity threshold. Only near-identical items are penalized as redundant, so the diversity effect barely activates. Result: top-3 by relevance (all Action).</li><li><strong>σ = 0.5–1.0</strong>: Moderate similarity scale. Items within the same genre are considered similar enough to penalize, leading to genre mixing (Action, Comedy, Documentary).</li><li><strong>σ = 2.0</strong>: Broader similarity scale. Even moderately different items start to look similar. The algorithm becomes more willing to sacrifice relevance for diversity.</li><li><strong>σ = 5.0</strong>: Very broad similarity scale. Almost everything looks similar, which actually weakens the diversity signal. The algorithm struggles to distinguish between items and reverts toward relevance-based ranking.</li></ul><p>In practice, σ should match the natural scale of your embedding space. If your embeddings are normalized (unit length), σ ≈ 1.0 is a reasonable starting point. Tune it based on how strict you want diversity: smaller σ for subtle redundancy detection within tight clusters, larger σ for aggressive spreading across broader categories.</p><h3>Visualizing the RBF Kernel</h3><p>The exp(-D_ij / 2σ²) term is an RBF (Radial Basis Function) kernel. Let’s visualize how it maps distances to similarities:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/691/0*bD9oo30vS6BmQjEq.png" /><figcaption>Figure: RBF Kernel plot at different sigma values</figcaption></figure><p><strong>Interpretation</strong>: With σ = 1.0, items at distance 1.0 have similarity ≈ 0.6 (moderate penalty), while items at distance 2.0 have similarity ≈ 0.14 (weak penalty). Smaller σ makes the kernel “sharper” — only very close items are considered similar.</p><h3>Practical Guidance</h3><p><strong>Tuning strategy</strong>:</p><p><strong>1. Start with α = 0.5, σ = 1.0</strong>: This is a reasonable default that balances quality and diversity.</p><p><strong>2. Tune α first</strong>:</p><ul><li>Sweep α ∈ [0.2, 0.8] in 0.1 increments</li><li>Measure offline diversity metrics (intra-list distance, genre entropy) vs relevance metrics (nDCG)</li><li>Plot the Pareto frontier to find acceptable tradeoffs</li></ul><p><strong>3. Then tune σ</strong>:</p><ul><li>If items are still too similar, decrease σ (tighter similarity definition)</li><li>If diversity is too aggressive, increase σ (looser similarity definition)</li><li>σ should roughly match the typical distance scale in your embedding space</li></ul><p><strong>4. A/B test top candidates</strong>: Run online experiments with 2–3 (α, σ) pairs to validate offline metrics.</p><p><strong>Typical values in production</strong>:</p><ul><li>Balanced systems: α ≈ 0.4–0.6, σ ≈ 0.5–1.5</li><li>Quality-focused (e.g., search): α ≈ 0.2–0.4</li><li>Discovery-focused (e.g., explore page): α ≈ 0.6–0.9</li></ul><p>The beauty of YouTube’s parameterization is that it decouples quality (diagonal) from diversity (off-diagonal), giving you two independent knobs to tune the system behavior.</p><h3>YouTube’s Production DPP Implementation</h3><p>The greedy MAP algorithm we implemented earlier works well for selecting a fixed top-k set. However, YouTube’s production system needs to rank entire feeds of hundreds or thousands of videos. Their <a href="https://dl.acm.org/doi/epdf/10.1145/3269206.3272018">2018 paper</a> introduces a <strong>windowed approach</strong> that processes candidates in batches, making DPPs scalable for large-scale recommendation systems.</p><h3>The Windowed Algorithm</h3><p>Instead of selecting all k items at once from the full candidate pool, YouTube’s algorithm:</p><ol><li><strong>Selects k items</strong> from the current candidate pool using greedy MAP</li><li><strong>Adds them to the result list</strong> R</li><li><strong>Removes selected items</strong> from the pool and updates the kernel matrix</li><li><strong>Repeats</strong> until all candidates are ranked (or enough items are selected)</li></ol><p>This approach has several advantages:</p><ul><li><strong>Scalability</strong>: Processes N items in O(Nk²) time instead of O(Nk³)</li><li><strong>Diversity within pages</strong>: Ensures each “window” of k items is diverse, not just the top-k</li><li><strong>Memory efficiency</strong>: Can process large pools without computing full N×N kernels</li><li><strong>Flexibility</strong>: Can stop early after ranking enough items for the user’s viewport</li></ul><p>Here’s the algorithm from YouTube’s paper:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/769/0*JAICznIyrgy1Ze25.png" /><figcaption>Figure: Snapshot of DPP algorithm from <a href="https://dl.acm.org/doi/epdf/10.1145/3269206.3272018">YT DPP paper</a></figcaption></figure><pre>def youtube_dpp_ranking(relevance, embeddings, k, alpha=0.5, sigma=1.0):<br>    W = list(range(len(relevance)))  # Remaining candidate indices<br>    R = []  # Final ranked list<br>    <br>    while len(W) &gt; 0:<br>        # Build kernel for current candidate pool<br>        rel_W = relevance[W]<br>        emb_W = embeddings[W]<br>        L = youtube_dpp_kernel(rel_W, emb_W, alpha=alpha, sigma=sigma)<br>        <br>        # Select up to k items from current pool<br>        window_size = min(k, len(W))<br>        M = greedy_map_dpp(L, k=window_size)<br>        <br>        # Map back to original indices<br>        selected_items = [W[i] for i in M]<br>        R.extend(selected_items)<br>        <br>        # Remove selected items from candidate pool<br>        W = [w for w in W if w not in selected_items]<br>    <br>    return R</pre><h3>Walking Through the Windowed Algorithm</h3><p>Let’s trace through ranking all 6 videos with window size k=2:</p><p>Code</p><pre>print(&quot;YouTube Windowed DPP (k=2, α=0.75, σ=1.0)&quot;)<br>print(&quot;=&quot; * 70)<br># We&#39;ll manually trace the first few iterations<br>W = list(range(len(relevance)))<br>R = []<br>k_window = 2<br>alpha, sigma = 0.75, 1.0<br>iteration = 1<br>while len(W) &gt; 0 and iteration &lt;= 3:  # Show first 3 iterations<br>    print(f&quot;\nIteration {iteration}:&quot;)<br>    print(f&quot;  Remaining pool W: {W}&quot;)<br>    print(f&quot;  Pool genres: {[genre[i] for i in W]}&quot;)<br>    <br>    # Build kernel for current pool<br>    rel_W = relevance[W]<br>    emb_W = embeddings[W]<br>    L = youtube_dpp_kernel(rel_W, emb_W, alpha=alpha, sigma=sigma)<br>    <br>    # Select k items<br>    window_size = min(k_window, len(W))<br>    M = greedy_map_dpp(L, k=window_size)<br>    selected_items = [W[i] for i in M]<br>    <br>    print(f&quot;  Selected from pool: {M} (pool indices)&quot;)<br>    print(f&quot;  Maps to original: {selected_items}&quot;)<br>    print(f&quot;  Genres: {[genre[i] for i in selected_items]}&quot;)<br>    print(f&quot;  Relevance: {[relevance[i] for i in selected_items]}&quot;)<br>    <br>    R.extend(selected_items)<br>    W = [w for w in W if w not in selected_items]<br>    <br>    iteration += 1<br>print(f&quot;\n{&#39;─&#39;*70}&quot;)<br>print(f&quot;Final ranking R: {R}&quot;)<br>print(f&quot;Genres: {[genre[i] for i in R]}&quot;)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/798/1*UDkdtvp20Xhdj4PHd3iGVw.png" /><figcaption>Figure: Output of above block of code</figcaption></figure><p><strong>Key observations</strong>:</p><ul><li><strong>Iteration 1</strong>: From the full pool, selects the highest-quality Action video (0.9) and highest-quality diverse option (Comedy, 0.7)</li><li><strong>Iteration 2</strong>: From remaining items, selects another Action video (0.85) paired with Documentary (0.6) for diversity</li><li><strong>Iteration 3</strong>: Clears the remaining pool with the last Action and Mixed genre video</li></ul><p>Notice how diversity is enforced <strong>within each window of 2 items</strong>, not just globally. This ensures users see varied content throughout their feed, not just at the top.</p><h3>Comparing Single-Pass vs Windowed</h3><p>Let’s compare the two approaches side-by-side:</p><pre>Comparison of top-3 selections:<br>Method           Indices      Genres                        Relevance<br>────────────────────────────────────────────────────────────────────────────────────────────────────<br>Single-pass      [0, 3, 4]    Action, Comedy, Documentary   [0.9, 0.7, 0.6]<br>Windowed (k=2)   [0, 3, 1]    Action, Comedy, Action        [0.9, 0.7, 0.85]</pre><p>The windowed approach includes Video 1 (Action, 0.85) in the top-3 because it was selected in the second window after Videos 0 and 3 were removed. This demonstrates how window size k affects the ranking: smaller windows enforce local diversity but may allow similar items to appear nearby across window boundaries.</p><p>In the paper, it’s mentioned that typically the candidate pool is several hundreds of videos and sub-window size is a dozen (12) or so videos.</p><p>The windowed algorithm is YouTube’s production solution for bringing the mathematical elegance of DPPs to real-world scale. By processing items in batches, it maintains diversity guarantees while handling feeds with thousands of candidates in milliseconds.</p><h3>System Design Integration</h3><p>Now that we’ve covered DPP theory and implementation, let’s see how it fits into a production recommendation system. Recall from <a href="https://aayushmnit.com/posts/2025-12-25-DiversityMMRPart1/DiversityMMRPart1.html#system-design-integration">Part 1</a> the standard recommendation pipeline:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*1DuevBzxoAeZqI_R.png" /><figcaption>Figure: Typical RecSys pipeline showing where DPP fits in the recommendation pipeline</figcaption></figure><p>DPPs slot into the <strong>Re-ranking</strong> stage, right where we used MMR in Part 1. Here’s how the stages work together:</p><p><strong>1. Candidate Generation (Recall)</strong></p><ul><li>Retrieve ~1000 candidates from multiple sources (collaborative filtering, content-based, trending)</li><li>Goal: High recall, cast a wide net</li><li>Output: Large pool of potentially relevant items</li></ul><p><strong>2. Scoring (Ranking)</strong></p><ul><li>ML model predicts relevance for each candidate (e.g., click probability, watch time)</li><li>Features: user history, item metadata, context</li><li>Output: Relevance scores q_i for each item, typically in 200-500 candidates</li></ul><p><strong>3. Re-ranking (Diversity)</strong></p><ul><li><strong>This is where DPP comes in</strong></li><li>Input: Scored candidates + content embeddings</li><li>Build kernel matrix: L[i,j] = α × q_i × q_j × exp(-D_ij / 2σ²)</li><li>Apply windowed DPP to produce final ranked list</li><li>Output: Diverse, high-quality feed. Batches of size 6–12 candidates</li></ul><p><strong>4. Business Logic</strong></p><ul><li>Apply final filters (content policy, already-seen, etc.)</li><li>Insert ads, promotions</li><li>Output: User-facing feed</li></ul><p>The key advantage of DPPs is that they give you a mathematically principled way to balance quality and diversity, backed by decades of research in probability theory. Once you understand the volume-based intuition, the system becomes just as interpretable as MMR but with stronger theoretical guarantees.</p><h3>Key Takeaways</h3><ul><li><strong>DPPs provide a mathematically principled framework for diversity</strong>. Unlike greedy approaches like MMR that make local decisions, DPPs evaluate entire sets at once through determinants. This volume-based intuition (diverse sets span more space) translates directly into probability: P(Y) ∝ det(L_Y).</li><li><strong>The quality-diversity decomposition maps naturally to production recsys</strong>. The kernel formulation L[i,j] = α × q_i × q_j × similarity(i,j) cleanly separates relevance (from your ML model) and diversity (from content embeddings). You get two interpretable knobs: α controls how much diversity matters, σ controls what counts as “similar.”</li><li><strong>Greedy MAP is deterministic and faster</strong>. While exact MAP inference is NP-hard, the greedy algorithm runs in O(Nk²) time and provides strong approximations. Unlike sampling-based approaches, it’s deterministic and stable, making it suitable for A/B testing and debugging.</li><li><strong>YouTube’s windowed approach scales to video feeds</strong>. By processing candidates in batches of k items (typically 6–12), the algorithm maintains diversity throughout long feeds, not just at the top. This makes DPPs practical for ranking hundreds or thousands of candidates in milliseconds.</li></ul><h3>References &amp; Further Reading</h3><ul><li><a href="https://arxiv.org/abs/1207.6083">Determinantal Point Processes for Machine Learning</a> (Kulesza &amp; Taskar, 2012) — The definitive tutorial covering DPP theory, algorithms, and applications. Highly readable despite the math.</li><li><a href="https://dl.acm.org/doi/epdf/10.1145/3269206.3272018">Practical Diversified Recommendations on YouTube with Determinantal Point Processes</a> (Cheng et al., 2018) — YouTube’s paper describing their windowed algorithm and kernel parameterization. This is the blueprint for production DPP systems.</li></ul><p><strong>Previous blog post:</strong></p><ul><li><a href="https://medium.com/data-science-collective/diversity-in-recommendations-maximal-marginal-relevance-mmr-0e7840c9399e">Part 1: Diversity in Recommendations using MMR </a>— My previous post covering Maximal Marginal Relevance as a simpler alternative to DPPs.</li></ul><p>I hope you found this deep dive into DPPs useful! If you’re implementing diversity in your recommendation system, I’d love to hear about your experience. Connect with me on <a href="https://www.linkedin.com/in/aayushmnit/">LinkedIn</a> to share your thoughts or questions.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=2427bf1b6324" width="1" height="1" alt=""><hr><p><a href="https://medium.com/data-science-collective/diversity-in-recommendations-determinantal-point-processes-dpp-2427bf1b6324">Diversity in Recommendations — Determinantal Point Processes (DPP)</a> was originally published in <a href="https://medium.com/data-science-collective">Data Science Collective</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Diversity in Recommendations — Maximal Marginal Relevance (MMR)]]></title>
            <link>https://medium.com/data-science-collective/diversity-in-recommendations-maximal-marginal-relevance-mmr-0e7840c9399e?source=rss-85dd669ac015------2</link>
            <guid isPermaLink="false">https://medium.com/p/0e7840c9399e</guid>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[algorithms]]></category>
            <category><![CDATA[recsys]]></category>
            <category><![CDATA[diversity]]></category>
            <category><![CDATA[data-science]]></category>
            <dc:creator><![CDATA[Aayush Agrawal]]></dc:creator>
            <pubDate>Fri, 26 Dec 2025 21:36:02 GMT</pubDate>
            <atom:updated>2025-12-29T20:16:37.511Z</atom:updated>
            <content:encoded><![CDATA[<h3>Diversity in Recommendations — Maximal Marginal Relevance (MMR)</h3><h4>Beyond Accuracy: Using Maximal Marginal Relevance to create more diverse and engaging recommendations.</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/983/0*O758dI1Qb55vF8S0.png" /><figcaption>Figure 1: Diverse recommendations cover multiple user intents. Credit: <a href="https://gemini.google.com/">Gemini</a></figcaption></figure><p><a href="https://aayushmnit.com/posts/2025-12-25-DiversityMMRPart1/DiversityMMRPart1.html">Read in browser</a> for best experience.</p><p>Picture this: you open YouTube looking for something to watch. The algorithm shows you 10 videos, and they’re all basketball highlights from the same game. Sure, you like basketball, but maybe right now you’re in the mood for that ML lecture you’ve been putting off. Or maybe something else entirely.</p><p>Most recommendation systems score each item independently based on predicted relevance. The result? If your model is confident you like basketball, you get basketball. All basketball. The system optimizes for the single most likely intent while ignoring the possibility that you might have multiple interests, or that its prediction might simply be wrong.</p><p>Diversity addresses this by explicitly penalizing redundancy. Instead of asking “what’s most relevant?”, we ask “what’s most relevant that we haven’t already covered?”</p><p>This isn’t just theoretically appealing. Diversity boosting delivers real results in production systems and can dramatically improve engagement metrics. Nobel laureate Harry Markowitz called diversification <a href="https://www.bizjournals.com/milwaukee/news/2018/10/03/investment-diversification-the-only-free-lunch-in.html">“the only free lunch in finance”</a> when applied to portfolio management. The same principle applies to recommendations: hedge your bets across the user’s possible intents.</p><p>In this post, we’ll:</p><ul><li>Explain why diversity matters and when pointwise scoring fails</li><li>Introduce Maximal Marginal Relevance (MMR), a simple and battle-tested diversity method</li><li>Walk through the algorithm step-by-step with code</li><li>Explore how the λ parameter lets you tune the quality-diversity tradeoff</li></ul><h3>Why Diversity Matters</h3><p>Recommendation systems face a fundamental challenge: they don’t know what the user wants right now. They have historical signals, but a user who watched NBA highlights yesterday might be looking for a cooking video today. A user who binges Peppa Pig with their kids on weekends has different needs on a Tuesday night.</p><p>Pointwise ranking models score each candidate independently. If your embedding space clusters similar content together (as it should), similar items receive similar scores. The top-K results end up being variations of the same thing.</p><p>This creates several problems:</p><ul><li><strong>Missed opportunities</strong>: If the user’s current intent doesn’t match your top prediction, you’ve wasted valuable screen real estate</li><li><strong>Filter bubbles</strong>: Users get stuck in narrow content loops, reducing long-term engagement</li><li><strong>Poor fault tolerance</strong>: When your model is wrong about intent, it’s wrong across the entire slate</li></ul><p>Diversity isn’t unique to recommendations. Portfolio managers diversify across asset classes to reduce risk. Autonomous driving systems like <a href="https://www.cs.toronto.edu/~wenjie/papers/intentnet_corl18.pdf">IntentNet</a> diversify over possible agent intents rather than betting on a single predicted trajectory. The core insight is the same: when you’re uncertain about what the environment wants, spread your bets.</p><p>The business case is compelling. Users who see a mix of content that spans their interests are more likely to find something they want right now, and more likely to come back.</p><h3>The Problem Setup</h3><p>Let’s make this concrete with an example. Imagine you have 6 candidate videos for a user:</p><pre>import numpy as np<br>import pandas as pd<br>from fastcore.utils import L<br>from sklearn.metrics.pairwise import cosine_similarity<br><br>relevance = np.array([0.9, 0.85, 0.8, 0.7, 0.6, 0.5])<br>genre = L([&#39;Action&#39;, &#39;Action&#39;, &#39;Action&#39;, &#39;Comedy&#39;, &#39;Documentary&#39;, &#39;Mixed comedy/documentary&#39;])<br>df = pd.DataFrame({<br>    &#39;video_id&#39;: [i for i in range(len(relevance))],<br>    &#39;genre&#39;: genre,<br>    &#39;relevance&#39;: relevance<br>})<br>df</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/827/1*EnqD52BW63eOw4k_asMIyg.png" /><figcaption>Figure 2: DataFrame of Videos, Genre and relevance score</figcaption></figure><p>Pure relevance ranking returns: <strong>Action, Action, Action, Comedy, Documentary, Mixed</strong></p><p>The top three slots go to nearly identical content. If the user wanted comedy, they have to scroll past three action videos. If they wanted a documentary, it’s buried at position 5.</p><p>Now consider what happens with diversity-aware ranking: <strong>Action, Comedy, Documentary, Action, Mixed</strong></p><p>The slate now covers three distinct genres in the top three positions. The second action video drops to position 4 since its marginal value is low once we’ve already shown an action video.</p><p>This is the core insight: <strong>marginal relevance</strong> matters more than absolute relevance. The 3rd action video isn’t 3x as valuable as the first. Each additional similar item has diminishing returns.</p><p>We can represent each video as an embedding vector over genres:</p><pre>embeddings = np.array([<br>    [1.0, 0.0, 0.0],   # video 0: pure action<br>    [0.9, 0.1, 0.0],   # video 1: action (very similar to 0)<br>    [0.8, 0.2, 0.0],   # video 2: action (also very similar to 0)<br>    [0.0, 1.0, 0.0],   # video 3: comedy<br>    [0.0, 0.0, 1.0],   # video 4: documentary<br>    [0.1, 0.5, 0.5],   # video 5: mixed<br>])<br><br>similarities = cosine_similarity(embeddings)<br>print(f&quot;Similarity b/w Video 0 and 1 is {similarities[0,1]:.2f}&quot;)<br>print(f&quot;Similarity b/w Video 0 and 3 is {similarities[0,3]:.2f}&quot;)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/787/1*vQ232ovlWXn9_UdnyHHThQ.png" /><figcaption>Figure 3: Output of code block above.</figcaption></figure><p>Videos 0 and 1 have cosine similarity near 1.0. Videos 0 and 3 are orthogonal (similarity = 0). This similarity structure is exactly what MMR exploits to rerank results.</p><h3>Maximal Marginal Relevance: The Algorithm</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ONZfDLPuHuNJDojyhM7xYg.png" /><figcaption>Figure 4: MMR infographic. Credit: <a href="https://notebooklm.google.com/">NotebookLM</a></figcaption></figure><p>MMR was introduced by <a href="https://www.cs.cmu.edu/~jgc/publication/The_Use_MMR_Diversity_Based_LTMIR_1998.pdf">Carbonell and Goldstein in 1998 for document retrieval and summarization</a>. The core formula is:</p><p><strong><em>MMR = ArgMax[λ × Rel(item, user) — (1-λ) × Max(Sim(item, selected_items))]</em></strong></p><p>Where:</p><ul><li>item = candidate video not yet selected</li><li>user = user profile</li><li>selected_items = set of items already chosen for the slate</li><li>λ = trade-off parameter between relevance and diversity (0 to 1)</li></ul><p>You can see the first part λ × Rel(item, user) is just relevance. The second part (1-λ) × Max(Sim(item, selected_items)) is the diversity penalty.</p><p>The algorithm is greedy and iterative:</p><ol><li><strong>First pick:</strong> Select the item with highest relevance (the similarity penalty term is zero since nothing has been selected yet)</li><li><strong>Subsequent picks:</strong> For each remaining candidate, compute relevance minus a penalty for similarity to the most similar already-selected item. Pick the candidate with the highest score.</li><li><strong>Repeat</strong> until you’ve filled K slots</li></ol><p>The key insight is the Max in the second term. You’re penalized based on your similarity to whichever selected item you’re most similar to. Being different from 2 out of 3 selected items doesn’t help if you’re a near-duplicate of the third.</p><p><strong>The λ parameter controls the tradeoff:</strong></p><ul><li>λ = 1.0 → Pure relevance ranking, no diversity</li><li>λ = 0.0 → Maximum diversity, ignore relevance</li><li>λ = 0.5 → Equal weight to both</li><li>λ = 0.7 → The paper’s suggested starting point</li></ul><h3>Walking Through MMR Step-by-Step</h3><p>Let’s trace through the algorithm by hand before writing any code.</p><p><strong>Step 1: First selection</strong></p><p>With an empty slate, there’s no diversity penalty.</p><pre>selected_item = np.argmax(relevance)<br>print(f&quot;First selected item: {int(selected_item)}, Relevance: {relevance[selected_item]}&quot;)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/798/1*l4lMYlzrrxFeJjYK5dyLGw.png" /><figcaption>Figure 5: Output of code block above.</figcaption></figure><p>We pick the highest relevance item: Video 0 (Action, relevance = 0.9).</p><p><strong>Step 2: Second selection</strong></p><p>Now we need to choose between the remaining videos. Let’s compare Video 1 (another Action, relevance=0.85) vs Video 3 (Comedy, relevance=0.7)</p><pre>lam = 0.5<br>video_1_mmr_score = lam * relevance[1] - (1-lam) * cosine_similarity(embeddings[[1]], embeddings[[selected_item]])<br>video_3_mmr_score = lam * relevance[3] - (1-lam) * cosine_similarity(embeddings[[3]], embeddings[[selected_item]])<br>print(f&quot;MMR score of Video 1: {video_1_mmr_score.item():.3f}&quot;)<br>print(f&quot;MMR score of Video 3: {video_3_mmr_score.item():.3f}&quot;)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/803/1*esa_kudpEIIr-ycCxQTg7A.png" /><figcaption>Figure 6: Output of code block above.</figcaption></figure><p>Video 3 wins despite having lower relevance. The similarity penalty makes Video 1’s score negative. This is MMR in action: redundancy is penalized so heavily that a lower-quality diverse item beats a high-quality duplicate.</p><p><strong>The full implementation</strong></p><pre>def mmr_select(relevance, embeddings, k=3, lam=0.5):<br>    n = len(relevance)<br>    selected = []<br>    remaining = list(range(n))<br><br>    # First pick: pure relevance (no penalty term)<br>    first = int(np.argmax(relevance))<br>    selected.append(first)<br>    remaining.remove(first)<br><br>    # Subsequent picks: balance relevance vs redundancy<br>    while len(selected) &lt; k:<br>        best_score, best_idx = -float(&#39;inf&#39;), None<br><br>        for i in remaining:<br>            # Penalty = similarity to most similar already-selected item<br>            max_sim = max(similarities[i, j] for j in selected)<br>            mmr_score = lam * relevance[i] - (1 - lam) * max_sim<br><br>            if mmr_score &gt; best_score:<br>                best_score, best_idx = mmr_score, i<br><br>        selected.append(best_idx)<br>        remaining.remove(best_idx)<br><br>    return selected<br><br>result = mmr_select(relevance, embeddings, k=3, lam=0.5)<br>print(genre[result])</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/801/1*SbhO-NkKQpdy9J_nKnCbsw.png" /><figcaption>Figure 7: Output of code block above.</figcaption></figure><p>Pure relevance ranking would give us Action, Action, Action. MMR gives us Action, Comedy, Documentary. Three genres covered in three slots.</p><h3>Tuning λ: The Quality-Diversity Tradeoff</h3><p>The λ parameter is your dial between relevance and diversity. Let’s see how it affects the ranking:</p><pre>for lam in [0.3, 0.5, 0.7, 0.9, 1.0]:<br>    result = mmr_select(relevance, embeddings, k=6, lam=lam)<br>    print(f&quot;λ={lam}: {genre[result]}&quot;)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/1*AP2_KZq9qZkoQi1wSi7WQg.png" /><figcaption>Figure 8: Output of code block above.</figcaption></figure><p>At λ=1.0 we recover pure relevance ranking. As λ decreases, diverse items get promoted earlier in the slate.</p><p><strong>Choosing λ in practice:</strong></p><ul><li>Start with λ=0.7 (the original paper’s suggestion)</li><li>A/B test different values against engagement metrics</li></ul><h3>System Design of Diversity Boosting</h3><p>In terms of system design, diversity boosting can be added at various stages by using different techniques, but MMR falls into the class of <strong>re-rank algorithms</strong> which are typically applied after pointwise scoring.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*9XY9XmU--HgpXBSS.png" /><figcaption>Figure 9: Typical RecSys pipeline showing where MMR fits in the recommendation pipeline</figcaption></figure><p>The diagram above shows a typical recsys pipeline:</p><ol><li><strong>Candidate Videos</strong> → Raw pool of items to consider</li><li><strong>Pointwise video scorer</strong> → Takes user features and video features, outputs a relevance score per video independently</li><li><strong>Ranking and Diversity Boosting</strong> (highlighted in green) → This is where MMR lives. It takes the pointwise scores and reorders them to balance relevance with diversity</li><li><strong>Policy</strong> → Business rules layer (e.g., content safety filters, frequency caps)</li><li><strong>User</strong> → Final slate shown</li></ol><p>MMR is a <strong>post-scoring reranker</strong>. The pointwise model does its job first (predicting relevance), then MMR adjusts the ordering to reduce redundancy. This is a clean separation of concerns — you don’t need to retrain your ranking model to add diversity.</p><h3>Key Takeaways</h3><ol><li><strong>Pointwise ranking has a blind spot.</strong> Scoring items independently leads to redundant results when similar items cluster in your embedding space.</li><li><strong>Diversity hedges against uncertainty.</strong> When you don’t know the user’s current intent, covering multiple possibilities increases the chance of a hit.</li><li><strong>MMR is simple and effective.</strong> The algorithm is greedy, easy to implement, and has powered production systems for decades.</li></ol><p>I hope you enjoyed reading it. If there is any feedback on the blog post, feel free to comment below or reach out on <a href="https://www.linkedin.com/in/aayushmnit/">LinkedIn</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=0e7840c9399e" width="1" height="1" alt=""><hr><p><a href="https://medium.com/data-science-collective/diversity-in-recommendations-maximal-marginal-relevance-mmr-0e7840c9399e">Diversity in Recommendations — Maximal Marginal Relevance (MMR)</a> was originally published in <a href="https://medium.com/data-science-collective">Data Science Collective</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Why You Should Write Technical Blogs (and How to Start)]]></title>
            <link>https://medium.com/@aayushmnit/why-you-should-write-technical-blogs-and-how-to-start-db1c80d825ae?source=rss-85dd669ac015------2</link>
            <guid isPermaLink="false">https://medium.com/p/db1c80d825ae</guid>
            <category><![CDATA[writing]]></category>
            <category><![CDATA[shareyourwork]]></category>
            <category><![CDATA[technical-blog]]></category>
            <category><![CDATA[blogging]]></category>
            <dc:creator><![CDATA[Aayush Agrawal]]></dc:creator>
            <pubDate>Sun, 14 Dec 2025 18:53:17 GMT</pubDate>
            <atom:updated>2025-12-16T07:07:20.961Z</atom:updated>
            <content:encoded><![CDATA[<blockquote>A guide on why technical blogging is one of the best investments you can make in your career, along with practical tips to get started.</blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*5fnMVCB1INXpwz46.jpg" /></figure><p><a href="https://aayushmnit.com/posts/2025-12-13-WhyYouShouldBlog/WhyYouShouldBlog.html">Read in browser</a> for best experience.</p><p>One of my favorite hobbies outside of work is sharing technical knowledge through blog posts. Over the years, writing has opened doors I never expected: speaking at AI meetups, opportunities to author books with famous publishers, and countless LinkedIn conversations that turned into opportunities. In this post, I want to share why I think every technical person should consider blogging, and give you some practical tips to get started.</p><h3>My blogging setup</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/701/0*ruO20OmHGisU-ifo.png" /></figure><p>I publish on my <a href="https://aayushmnit.com/">personal website</a>, which I host through <a href="https://github.com/aayushmnit/aayushmnit.github.io">GitHub Pages</a> and build using <a href="https://quarto.org/">Quarto</a>. Jupyter notebooks are my tool of choice for writing since they let me mix text, code and output of that code. Quarto handles converting them into HTML. After letting a post sit on my site for a few days, I cross-post to <a href="https://medium.com/@aayushmnit">Medium</a> for greater visibility.</p><p>My posts focus on AI, ML, and Python development. I try to center my writing around real examples of the techniques I’m discussing so readers can gain practical knowledge they can apply to their own use cases. I typically write about:</p><ul><li>Niche techniques I use at work</li><li>Emerging technologies (like generative AI lately)</li><li>Topics I’m actively studying and researching</li></ul><h3>Five reasons to start blogging</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/559/0*ukwFt1hSC780WHL1.png" /></figure><h3>1. Increase your visibility and reach</h3><p>Many people have reached out to me through LinkedIn or email asking about topics I’ve written about. Sometimes these turn into job opportunities. <strong>Blogs are like resumes, but better</strong>. They show what you actually know, not just where you’ve worked.</p><h3>2. It helps you learn</h3><p>Organizing knowledge forces you to synthesize your own ideas. One reliable test of whether you understand something is whether you can explain it to someone else. Writing a blog post is a great way to do that. I often discover gaps in my own understanding while trying to explain a concept clearly.</p><h3>3. It improves your writing skills</h3><p>The more you write, the more comfortable you become expressing your ideas. This carries over to work emails, design docs, and presentations.</p><h3>4. Establish yourself as an expert</h3><p>Through my writing, I’ve landed opportunities to present at local AI meetups, write books with well-known publishers, and review technical books before their release. None of this would have happened without a public body of work people could find.</p><h3>5. It saves time (DRY principle)</h3><p>Don’t Repeat Yourself applies to explanations too. Instead of spending two hours giving someone an overview of a topic, I can point them to an article. I also refer back to my own posts as notes, which saves me time when I need to refresh my memory on something I worked on years ago.</p><h3>Tips for writing your first post</h3><p>If you’re convinced but don’t know what to write about, here are some ideas that have worked for me:</p><ol><li><strong>Curate and comment</strong>: Make a list of links to blog posts, articles, or papers you like. Write brief summaries and highlight what you found useful. This is low-pressure and helpful to others.</li><li><strong>Summarize what you learned</strong>: Attended a conference? Taking a class? Write up your notes and takeaways.</li><li><strong>Turn emails into posts</strong>: Any technical explanation you’ve written more than once in email or Slack should become a blog post.</li><li><strong>Write for yourself from a year ago</strong>: You’re best positioned to help people one step behind you. Your background, your style, and your knowledge level will give a different perspective than existing tutorials.</li><li><strong>Answer: what would have helped me six months ago?</strong>: Think about problems you struggled with and write the guide you wish existed.</li><li><strong>Don’t be a perfectionist</strong>: Post your writing sooner rather than later. Waiting just builds pressure and you end up writing less. You can always update posts later.</li></ol><h3>Additional resources for motivation</h3><ul><li><a href="https://medium.com/@racheltho/why-you-yes-you-should-blog-7d2544ac1045">Why you (yes, you) should blog</a> by Rachel Thomas</li><li><a href="https://austinkleon.com/show-your-work/">Show Your Work!: 10 Ways to Share Your Creativity and Get Discovered</a> by Austin Kleon</li></ul><h3>Getting started</h3><p>You don’t need a fancy setup. GitHub Pages is free. Quarto or Jekyll can turn markdown or notebooks into a website with minimal configuration. Medium works fine if you just want to write without managing infrastructure.</p><p>The hardest part is starting. Pick one thing you learned recently and write 500 words about it. That’s it. You can refine your process after you’ve published a few posts.</p><p>I hope you enjoyed reading this. If there is any feedback on the blog post, feel free to reach out on <a href="https://www.linkedin.com/in/aayushmnit/">LinkedIn</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=db1c80d825ae" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Meta MLE Interview Preparation Guide]]></title>
            <link>https://medium.com/@aayushmnit/meta-mle-interview-preparation-guide-2f93d1689690?source=rss-85dd669ac015------2</link>
            <guid isPermaLink="false">https://medium.com/p/2f93d1689690</guid>
            <category><![CDATA[meta]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[mle]]></category>
            <category><![CDATA[interview-preparation]]></category>
            <dc:creator><![CDATA[Aayush Agrawal]]></dc:creator>
            <pubDate>Mon, 16 Dec 2024 01:19:27 GMT</pubDate>
            <atom:updated>2024-12-17T00:52:27.408Z</atom:updated>
            <content:encoded><![CDATA[<p>A collection of resources while preparing for MLE interviews at Meta or other big tech companies.</p><p>Read here — <a href="https://aayushmnit.com/posts/2024-12-15-MLInterviewPrep/MLInterviewPrep.html">Link</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*RfAuxx_1B8Eirl5G.jpg" /></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=2f93d1689690" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[privateGPT walkthrough: Creating your own offline GPT Q&A system]]></title>
            <link>https://medium.com/@aayushmnit/privategpt-walkthrough-creating-your-own-offline-gpt-q-a-system-4bd7586cebd1?source=rss-85dd669ac015------2</link>
            <guid isPermaLink="false">https://medium.com/p/4bd7586cebd1</guid>
            <dc:creator><![CDATA[Aayush Agrawal]]></dc:creator>
            <pubDate>Fri, 26 May 2023 22:54:44 GMT</pubDate>
            <atom:updated>2023-05-26T22:54:44.770Z</atom:updated>
            <content:encoded><![CDATA[<h4>A code walkthrough of privateGPT repo on how to build your own offline GPT Q&amp;A system.</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/512/0*XciMAHIW3vOaWfFD.png" /></figure><p>Large Language Models (LLMs) have surged in popularity, pushing the boundaries of natural language processing. OpenAI’s GPT-3.5 is a prime example, revolutionizing our technology interactions and sparking innovation. Particularly, LLMs excel in building Question Answering applications on knowledge bases. In this blog, we delve into the top trending GitHub repository for this week: the <a href="https://github.com/imartinez/privateGPT">PrivateGPT repository</a> and do a code walkthrough.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/770/0*n22fbiVefxluHafe.png" /><figcaption>Fig. 1: Private GPT on Github’s top trending chart</figcaption></figure><h3>What is privateGPT?</h3><p>One of the primary concerns associated with employing online interfaces like OpenAI chatGPT or other Large Language Model systems pertains to data privacy, data control, and potential data leakage. The <a href="https://github.com/imartinez/privateGPT">privateGPT repository</a> presents a fully offline alternative for engaging with personal documents. It is constructed using open-source tools and technology, thereby enabling the utilization of LLMs capabilities without compromising data privacy or encountering data leakage issues.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/503/0*4WvRTWy_MOvOMLTi.png" /><figcaption>Fig.2: <a href="https://github.com/imartinez/privateGPT">privateGPT</a> on GitHub. At the time of writing repo had 19K+ stars and 2k+ forks.</figcaption></figure><h3>Running privateGPT locally</h3><p>To run privateGPT locally, users need to install the necessary packages, configure specific variables, and provide their knowledge base for question-answering purposes. Additional information on the installation process and usage can be found in the repository documentation or by referring to a <a href="https://www.codingthesmartway.com/privategpt-the-ultimate-solution-for-offline-secure-language-processing-that-turns-your-pdfs-into-interactive-ai-dialogues/">dedicated blog post on the topic</a>.</p><p>Essentially you can run it by calling the privateGPT.py file like -</p><pre>python privateGPT.py</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/770/0*l5mqKFRD3SWJqQDg.png" /><figcaption>Fig.3: Invoking <a href="https://github.com/imartinez/privateGPT">privateGPT</a> locally and asking a question.</figcaption></figure><p>And get a response that also mentions the sources it looked up for context.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/770/0*H6rm30X0Krx_Y3Jt.png" /><figcaption>Fig.4: <a href="https://github.com/imartinez/privateGPT">privateGPT</a> response.</figcaption></figure><h3>Code Walkthrough</h3><p><a href="https://github.com/imartinez/privateGPT/blob/main/ingest.py">privateGPT</a> code comprises two pipelines:</p><ol><li><strong>Ingestion Pipeline:</strong> This pipeline is responsible for converting and storing your documents, as well as generating embeddings for them. The documents are stored in a suitable format, and their embeddings are stored in an embedding database.</li><li><strong>Q&amp;A Interface:</strong> This interface accepts user prompts, the embedding database, and an open-source Language Model (LM) model as inputs. It utilizes these inputs to generate responses to the user’s queries.</li></ol><h3>1. Ingestion Pipeline</h3><p>Let’s delve into the <a href="https://github.com/imartinez/privateGPT/blob/main/ingest.py">ingestion pipeline</a> for a closer examination. The ingestion pipeline encompasses the following steps:</p><ol><li>Identifying files with various extensions and retrieving all the knowledge base from the source directory.</li><li>Splitting the documents into smaller chunks based on the parameters of chunk_size and chunk_overlap.</li><li>Initializing the Huggingfaceembeddings module of langchain. This involves loading a pre-trained language model from the sentence_transformers library.</li><li>Initializing the Chroma database from langchain.vectorstores. This step involves taking the chunked text and the initialized embedding model and saving it in the embedding database on disk.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/770/0*YYaVX8R_Cd8eu2n0.png" /><figcaption>Fig.5: Ingestion Pipeline</figcaption></figure><p>Let’s look at these steps one by one.</p><h4>1.1 Identifying and loading files from the source directory</h4><p>First, we import the required libraries and various text loaders from langchain.document_loaders.</p><pre>import os<br>import glob<br>from typing import List<br>from multiprocessing import Pool<br>from tqdm import tqdm<br>from langchain.document_loaders import (<br>    CSVLoader,<br>    EverNoteLoader,<br>    PDFMinerLoader,<br>    TextLoader,<br>    UnstructuredEmailLoader,<br>    UnstructuredEPubLoader,<br>    UnstructuredHTMLLoader,<br>    UnstructuredMarkdownLoader,<br>    UnstructuredODTLoader,<br>    UnstructuredPowerPointLoader,<br>    UnstructuredWordDocumentLoader,<br>)<br>from langchain.docstore.document import Document</pre><p>Next, we define the mapping b/w each extension and their respective langchain document loader. You can read document loader <a href="https://python.langchain.com/en/latest/modules/indexes/document_loaders.html">documentation</a> for more available loaders.</p><pre># Map file extensions to document loaders and their arguments<br>LOADER_MAPPING = {<br>    &quot;.csv&quot;: (CSVLoader, {}),<br>    &quot;.doc&quot;: (UnstructuredWordDocumentLoader, {}),<br>    &quot;.docx&quot;: (UnstructuredWordDocumentLoader, {}),<br>    &quot;.enex&quot;: (EverNoteLoader, {}),<br>    &quot;.epub&quot;: (UnstructuredEPubLoader, {}),<br>    &quot;.html&quot;: (UnstructuredHTMLLoader, {}),<br>    &quot;.md&quot;: (UnstructuredMarkdownLoader, {}),<br>    &quot;.odt&quot;: (UnstructuredODTLoader, {}),<br>    &quot;.pdf&quot;: (PDFMinerLoader, {}),<br>    &quot;.ppt&quot;: (UnstructuredPowerPointLoader, {}),<br>    &quot;.pptx&quot;: (UnstructuredPowerPointLoader, {}),<br>    &quot;.txt&quot;: (TextLoader, {&quot;encoding&quot;: &quot;utf8&quot;}),<br>}p</pre><p>Next, we define our single document loader.</p><pre>def load_single_document(file_path: str) -&gt; Document:<br>    ## Find extension of the file<br>    ext = &quot;.&quot; + file_path.rsplit(&quot;.&quot;, 1)[-1] <br>    if ext in LOADER_MAPPING: <br>        # Find the appropriate loader class and arguments<br>        loader_class, loader_args = LOADER_MAPPING[ext] <br>        # Invoke the instance of document loader<br>        loader = loader_class(file_path, **loader_args) <br>        ## Return the loaded document<br>        return loader.load()[0] <br>    raise ValueError(f&quot;Unsupported file extension &#39;{ext}&#39;&quot;)<br>    <br>git_dir = &quot;../../../../privateGPT/&quot;<br>loaded_document = load_single_document(git_dir+&#39;source_documents/state_of_the_union.txt&#39;)<br>print(f&#39;Type of loaded document {type(loaded_document)}&#39;)<br>loaded_document</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/770/0*xSR4DIVHZrhlfbSP.png" /></figure><p>The load_single_document function accomplishes the following steps:</p><ol><li>Extracts the file extension from the given file path.</li><li>Retrieves the corresponding document loader and its arguments from the previously defined LOADER_MAPPING dictionary.</li><li>Creates an instance of the appropriate document loader.</li><li>Loads the document using the instantiated loader.</li><li>Returns the loaded document.</li></ol><p>We can see that load_single_document returns a document of type langchain.schema.Document. Which according to the <a href="https://docs.langchain.com/docs/components/schema/document">documentation</a> consists of page_content (the content of the data) and metadata (auxiliary pieces of information describing attributes of the data).</p><pre>def load_documents(source_dir: str, ignored_files: List[str] = []) -&gt; List[Document]:<br>    &quot;&quot;&quot;<br>    Loads all documents from the source documents directory, ignoring specified files<br>    &quot;&quot;&quot;<br>    all_files = []<br>    for ext in LOADER_MAPPING:<br>        #Find all the files within source documents which matches the extensions in Loader_Mapping file<br>        all_files.extend(<br>            glob.glob(os.path.join(source_dir, f&quot;**/*{ext}&quot;), recursive=True)<br>        )<br>    <br>    ## Filtering files from all_files if its in ignored_files<br>    filtered_files = [file_path for file_path in all_files if file_path not in ignored_files]<br>    <br>    ## Spinning up resource pool<br>    with Pool(processes=os.cpu_count()) as pool:<br>        results = []<br>        with tqdm(total=len(filtered_files), desc=&#39;Loading new documents&#39;, ncols=80) as pbar:<br>            # Load each document from filtered files list using load_single_document function<br>            for i, doc in enumerate(pool.imap_unordered(load_single_document, filtered_files)):<br>                results.append(doc)<br>                pbar.update()<br>    <br>    return results</pre><p>The load_single_documents function carries out the following steps:</p><ol><li>Initializes an empty dictionary called all_files.</li><li>For each extension in the LOADER_MAPPING dictionary, it searches for all the files with that extension in the source directory and adds them to the all_files list.</li><li>Creates a new list named filtered_files by removing the files listed in the ignored_files list from the all_files list.</li><li>Executes a parallel loading operation on all the files in the filtered_files list using the load_single_document function, and appends the results to the results list.</li><li>Returns the list of loaded documents.</li></ol><pre>loaded_documents = load_documents(git_dir+&#39;source_documents&#39;)<br>print(f&quot;Length of loaded documents: {len(loaded_documents)}&quot;)<br>loaded_documents[0]</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/770/0*zKIaz2PRgyScfOWQ.png" /></figure><p>You can see we have loaded the state_of_the_union.txt file from the <a href="https://github.com/imartinez/privateGPT/tree/main/source_documents">privateGPT repo</a>. As this is the only file in that directory the length of loaded documents is one.</p><h4>1.2 Splitting the documents into smaller chunks</h4><p>Now we have seen how we can load multiple documents of different extensions using the load_documents function. The next step is to look at process_document function which loads and splits large documents into smaller chunks.</p><pre>from langchain.text_splitter import RecursiveCharacterTextSplitter<br><br>chunk_size = 500<br>chunk_overlap = 50<br>def process_documents(source_dir: str, ignored_files: List[str] = []) -&gt; List[Document]:<br>    &quot;&quot;&quot;<br>    Load documents and split in chunks<br>    &quot;&quot;&quot;<br>    print(f&quot;Loading documents from {source_dir}&quot;)<br>    documents = load_documents(source_dir, ignored_files)<br>    if not documents:<br>        print(&quot;No new documents to load&quot;)<br>        exit(0)<br>    print(f&quot;Loaded {len(documents)} new documents from {source_dir}&quot;)<br>    ## Load text splitter<br>    text_splitter = RecursiveCharacterTextSplitter(chunk_size=chunk_size, chunk_overlap=chunk_overlap)<br>    ## Split text<br>    texts = text_splitter.split_documents(documents)<br>    print(f&quot;Split into {len(texts)} chunks of text (max. {chunk_size} tokens each)&quot;)<br>    return texts<br>processed_documents = process_documents(git_dir+&#39;source_documents&#39;)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/770/0*gCgfYXIHkFdYuRvf.png" /></figure><p>The process_documents function performs the following steps:</p><ol><li>Loads all the documents from the source_dir directory using the load_documents function.</li><li>Initializes an instance of RecursiveCharacterTextSplitter from the langchain.text_splitter module, providing the chunk_size and chunk_overlap parameters. This class is responsible for splitting a list of documents into smaller overlapping chunks. [<a href="https://python.langchain.com/en/latest/modules/indexes/text_splitters/examples/recursive_text_splitter.html">RecursiveCharacterTextSplitter documentation</a>].</li><li>Uses the split_documents method of the RecursiveCharacterTextSplitter instance to split the loaded documents into smaller chunks.</li><li>Returns the resulting list of the smaller document chunks.</li></ol><h4>1.3 Initializing the embedding model</h4><p>Next, we load our embedding module which converts the smaller document chunks from previous steps to embeddings.</p><pre>from langchain.embeddings import HuggingFaceEmbeddings<br>EMBEDDINGS_MODEL_NAME = &quot;all-MiniLM-L6-v2&quot;<br>embeddings = HuggingFaceEmbeddings(model_name=EMBEDDINGS_MODEL_NAME)<br><br>print(&quot;Testing on a single query.&quot;)<br>embedded_vector = embeddings.embed_query(&quot;What is your name?&quot;)<br>print(f&quot;Size of embedded vector: {len(embedded_vector)}&quot;)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/770/0*w-5VBW7PiHMUmyBq.png" /></figure><p>The given code snippet carries out the following steps:</p><ol><li>Imports the HuggingFaceEmbeddings function from the langchain.embeddings module. This function is responsible for loading and encapsulating the <a href="https://www.sbert.net/">SentenceTransformers</a> embeddings, which are used for generating dense vector representations of sentences. You can refer to the <a href="https://python.langchain.com/en/latest/modules/models/text_embedding/examples/sentence_transformers.html">HuggingFaceEmbeddings documentation</a> for more details.</li><li>Loads the <a href="https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2">all-MiniLM-L6-v2</a> model from the sentence_transformers library. This model is specifically designed to map sentences and paragraphs into a 384-dimensional dense vector space. It is commonly utilized for tasks such as semantic search and similarity analysis.</li></ol><p>We can see that our embedded vector on a sample query returns a 384 dimension vector.</p><h4>1.4 Embed smaller text and save it in the vector database</h4><p>The next step involves utilizing the document chunks and the embedding model to store the documents and their corresponding embeddings in a vector database.</p><pre>from chromadb.config import Settings<br>from langchain.vectorstores import Chroma<br><br>PERSIST_DIRECTORY= git_dir+&quot;db&quot;<br># Define the Chroma settings<br>CHROMA_SETTINGS = Settings(<br>        chroma_db_impl=&#39;duckdb+parquet&#39;,<br>        persist_directory=PERSIST_DIRECTORY,<br>        anonymized_telemetry=False<br>)<br>## Create the embedding database<br>db = Chroma.from_documents(processed_documents, embeddings, persist_directory=PERSIST_DIRECTORY, client_settings=CHROMA_SETTINGS)<br>db.persist()</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/770/0*duJ5Oo9f281A4RLX.png" /></figure><p>The given code snippet performs the following operations:</p><ol><li>It imports the Settings class from the chromadb.config module and the Chroma class from the langchain.vectorstores module.</li></ol><p>2. It creates an instance of the Settings class named CHROMA_SETTINGS, providing several configuration parameters:</p><ul><li>chroma_db_impl is set to &#39;duckdb+parquet&#39;, specifying the implementation to be used for the Chroma vector database.</li><li>persist_directory is set to the PERSIST_DIRECTORY variable defined earlier, indicating the directory where the vector database will be saved.</li><li>anonymized_telemetry is set to False, indicating whether anonymized telemetry data should be collected.</li></ul><p>3. It creates a vector database by calling the Chroma.from_documents() method. This method takes the following arguments:</p><ul><li>processed_documents: The list of processed documents obtained from the previous step.</li><li>embeddings: The embeddings object/model used to generate the document embeddings.</li><li>persist_directory: The directory where the vector database will be persisted, specified by the PERSIST_DIRECTORY variable.</li><li>client_settings: The settings object (CHROMA_SETTINGS) containing configuration parameters for the vector database.</li></ul><p>4. We use db.persist() to store the index for future retrieval task</p><pre>## Test the semantic retrieval <br>db.similarity_search(query=&quot;What is the American Rescue Plan?&quot;, k= 4)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/770/0*zlaeUn7jNopRl0St.png" /></figure><p>To test the retrieval of semantic similarity, we can use the similarity_search function. similarity_search function takes a text query as input and returns the top k=4 document chunks from the vector database.</p><h3>2. Question &amp; Answer Interface</h3><p>Let’s explore the <a href="https://github.com/imartinez/privateGPT/blob/main/privateGPT.py">Q&amp;A interface</a> in more detail. The Q&amp;A interface consists of the following steps:</p><ol><li>Load the vector database and prepare it for the retrieval task.</li><li>Load a pre-trained Large language model from <a href="https://github.com/ggerganov/llama.cpp">LlamaCpp</a> or <a href="https://github.com/nomic-ai/gpt4all">GPT4ALL</a>.</li><li>Prompt the user with a query and generate a response using the RetrievalQA pipeline from langchain.chains.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/770/0*FbhoAVTahVzmyE9i.png" /><figcaption>Fig.6: Question Answering Pipeline</figcaption></figure><p>Let’s look at these steps one by one.</p><h4>2.1 Load the vector database</h4><p>First, we import the required libraries.</p><pre>from langchain.embeddings import HuggingFaceEmbeddings<br>from langchain.vectorstores import Chroma<br>from chromadb.config import Settings<br>git_dir = &quot;../../../../privateGPT/&quot;<br>PERSIST_DIRECTORY= git_dir+&quot;db&quot;<br>EMBEDDINGS_MODEL_NAME = &quot;all-MiniLM-L6-v2&quot;<br><br># Define the Chroma settings<br>CHROMA_SETTINGS = Settings(<br>        chroma_db_impl=&#39;duckdb+parquet&#39;,<br>        persist_directory=PERSIST_DIRECTORY,<br>        anonymized_telemetry=False<br>)<br>embeddings = HuggingFaceEmbeddings(model_name=EMBEDDINGS_MODEL_NAME)<br>db = Chroma(persist_directory=PERSIST_DIRECTORY, embedding_function=embeddings, client_settings=CHROMA_SETTINGS)<br>retriever = db.as_retriever()</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/770/0*Sef8sohOaPTODu8x.png" /></figure><p>The given code snippet carries out the following steps:</p><ol><li>Loads the embeddings using the HuggingFaceEmbeddings function, which was previously used to create the embedding store.</li><li>Instantiates a Chroma vector database that was created earlier.</li><li>Sets the vector database in retrieval mode.</li></ol><pre>## Testing retriever<br>retriever.vectorstore.similarity_search(query = &quot;What is Amercian rescue plan?&quot;)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/770/0*K-NkwtamCdE4zx6p.png" /></figure><h4>2.2 Load a pre-trained Large language model.</h4><pre>from langchain.llms import GPT4All<br><br>MODEL_PATH = git_dir+&quot;models/ggml-gpt4all-j-v1.3-groovy.bin&quot; <br>MODEL_N_CTX=1000<br># Prepare the LLM<br>llm = GPT4All(model=MODEL_PATH, n_ctx=MODEL_N_CTX, backend=&#39;gptj&#39;, callbacks=None, verbose=False)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/770/0*xbO0KgPMxUVu6KSw.png" /></figure><p>The code snippet above creates an instance of the GPT4All class named llm, which represents the Language Model (LLM) using the <a href="https://github.com/nomic-ai/gpt4all">GPT-4All model</a>. The constructor of GPT4All takes the following arguments:<br>- model: The path to the GPT-4All model file specified by the MODEL_PATH variable.<br>- n_ctx: The context size or maximum length of input sequences specified by the MODEL_N_CTX variable.<br>- backend: The backend to use for the LLM. In this case, it is set to ‘gptj’.<br>- callbacks: The callbacks to be used during the LLM execution. In this case, it is set to None.<br>- verbose: A boolean flag indicating whether to print verbose output during LLM execution. In this case, it is set to False.</p><h4>2.3 Prompt the user with a query and generate a response</h4><pre>from langchain.chains import RetrievalQA<br>qa = RetrievalQA.from_chain_type(llm=llm, chain_type=&quot;stuff&quot;, retriever=retriever, return_source_documents=True)<br>query = &quot;What is American rescue plan?&quot;<br>res = qa(query)<br>answer, docs = res[&#39;result&#39;], res[&#39;source_documents&#39;]<br><br># Get the answer from the chain<br># Print the result<br>print(&quot;\n\n&gt; Question:&quot;)<br>print(query)<br>print(&quot;\n&gt; Answer:&quot;)<br>print(answer)<br><br># Print the relevant sources used for the answer<br>for document in docs:<br>    print(&quot;\n&gt; &quot; + document.metadata[&quot;source&quot;] + &quot;:&quot;)<br>    print(document.page_content)</pre><pre>&gt; Question:<br>What is American rescue plan?<br><br>&gt; Answer:<br> The American Rescue Plan is a program that provides funding to schools to hire teachers and help students make up for lost learning due to the COVID-19 pandemic. It also provides economic relief for tens of millions of Americans by helping them put food on their table, keep a roof over their heads, and cut the cost of health insurance. The plan also helps working people by providing breathing room and giving them a little breathing room. It is a program that helps millions of families on Affordable Care Act plans save $2,400 a year on their health care premiums and combat climate change by cutting energy costs for families an average of $500 a year.<br><br>&gt; ../../../../privateGPT/source_documents/state_of_the_union.txt:<br>The American Rescue Plan gave schools money to hire teachers and help students make up for lost learning.  <br><br>I urge every parent to make sure your school does just that. And we can all play a part—sign up to be a tutor or a mentor. <br><br>Children were also struggling before the pandemic. Bullying, violence, trauma, and the harms of social media.<br><br>&gt; ../../../../privateGPT/source_documents/state_of_the_union.txt:<br>It fueled our efforts to vaccinate the nation and combat COVID-19. It delivered immediate economic relief for tens of millions of Americans.  <br><br>Helped put food on their table, keep a roof over their heads, and cut the cost of health insurance. <br><br>And as my Dad used to say, it gave people a little breathing room. <br><br>And unlike the $2 Trillion tax cut passed in the previous administration that benefitted the top 1% of Americans, the American Rescue Plan helped working people—and left no one behind.<br><br>&gt; ../../../../privateGPT/source_documents/state_of_the_union.txt:<br>Look, the American Rescue Plan is helping millions of families on Affordable Care Act plans save $2,400 a year on their health care premiums. Let’s close the coverage gap and make those savings permanent. <br><br>Second – cut energy costs for families an average of $500 a year by combatting climate change.<br><br>&gt; ../../../../privateGPT/source_documents/state_of_the_union.txt:<br>That’s why the Justice Department required body cameras, banned chokeholds, and restricted no-knock warrants for its officers. <br><br>That’s why the American Rescue Plan provided $350 Billion that cities, states, and counties can use to hire more police and invest in proven strategies like community violence interruption—trusted messengers breaking the cycle of violence and trauma and giving young people hope.</pre><p>Firstly, an instance of the RetrievalQA class named qa is created using the from_chain_type method. The RetrievalQA class is a chain specifically designed for question-answering tasks over an index. Please refer to the <a href="https://python.langchain.com/en/latest/modules/chains/index_examples/vector_db_qa.html">documentation</a> for further details. The from_chain_type method takes the following arguments:</p><ul><li>llm: The Language Model instance (llm) that was created previously.</li><li>chain_type: A string representing the type of chain to be used. In this case, it is set to &quot;stuff&quot;. There may be other available chain types specific to the question-answering scenario. Please consult the<a href="https://python.langchain.com/en/latest/modules/chains/index_examples/question_answering.html"> langchain documentation</a> for more information.</li><li>retriever: An instance of a Chroma database used to retrieve relevant documents for the given query.</li><li>return_source_documents: A boolean flag indicating whether to return the source documents along with the answer. In this case, it is set to True.</li></ul><p>Next, the qa instance is used to process a query. The Language Model (LLM) within the qa instance generates a response that includes the query, the answer, and the source documents used as context for generating the answer.</p><p>Finally, the answer and source documents are printed out for display.</p><h3>Conclusion</h3><p>In this blog post, we explored privateGPT, its implementation, and the code walkthrough for its ingestion pipeline and q&amp;A interface. I hope this blog post has been valuable in understanding privateGPT and its implementation. I recommend my readers try privateGPT on their knowledge base.</p><p>I hope you enjoyed reading it. If there is any feedback on the code or just the blog post, feel free to comment below or reach out on <a href="https://www.linkedin.com/in/aayushmnit/">LinkedIn</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=4bd7586cebd1" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Model calibration for classification tasks using Python]]></title>
            <link>https://medium.com/data-science-at-microsoft/model-calibration-for-classification-tasks-using-python-1a7093b57a46?source=rss-85dd669ac015------2</link>
            <guid isPermaLink="false">https://medium.com/p/1a7093b57a46</guid>
            <category><![CDATA[artificial-intelligence]]></category>
            <category><![CDATA[calibration]]></category>
            <category><![CDATA[machine-learning]]></category>
            <dc:creator><![CDATA[Aayush Agrawal]]></dc:creator>
            <pubDate>Tue, 10 Jan 2023 08:16:57 GMT</pubDate>
            <atom:updated>2023-01-31T22:51:00.531Z</atom:updated>
            <content:encoded><![CDATA[<h4>A hands-on introduction to model calibration using Python</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/0*JWyT2nmmemNuZRx7" /><figcaption>Pic Credit — <a href="https://unsplash.com/photos/11YQuWPQ9Bw">Unsplash</a></figcaption></figure><h3>What is Model Calibration?</h3><p>When working with classification problems, machine learning models often produce a probabilistic outcome ranging between 0 to 1. This probabilistic output is then used by people to make decisions. Unfortunately, many machine learning models’ probabilistic outputs cannot be directly interpreted as the probability of an event happening. To achieve this outcome, the model needs to be calibrated.</p><p>Formally, a model is said to be perfectly calibrated if, for any probability value p, a prediction of a class with confidence p is correct 100*p percent of the time. In more simple terms, the probabilistic output equals the probability of occurrence.</p><p>Let us visualize a perfect calibrated and non-calibrated curve.</p><pre>import numpy as np<br>import pandas as pd<br>import plotly.io as pio<br>import plotly.graph_objects as go<br>import plotly.express as px<br>pio.renderers.default = &quot;plotly_mimetype+notebook_connected&quot;<br><br>chart_df = pd.DataFrame({<br>    &quot;actual_prob&quot;: np.arange(0,1.1, 0.1),<br>    &quot;Calibrated&quot;: np.arange(0,1.1, 0.1),<br>    &quot;Non-Calibrated&quot;:  [min(1,val - np.random.randn()*(1-idx)/20) for idx, val in enumerate(np.arange(0,1.1, 0.1))]<br>})<br>fig = px.line(<br>        data_frame = chart_df, <br>        markers = True,<br>        x = &quot;actual_prob&quot;, <br>        y = [&quot;Calibrated&quot;, &quot;Non-Calibrated&quot;],<br>        template = &quot;plotly_dark&quot;)<br><br>fig.update_layout(<br>        title  = {&quot;text&quot;: &quot;Calibrated vs Non-calibrated model&quot;, &quot;y&quot;: 0.98, &quot;x&quot;: 0.5},<br>        xaxis_title=&quot;Predicted Probability&quot;,<br>        yaxis_title=&quot;Actual Probability&quot;,<br>        font = dict(size=15), <br>        legend=dict(yanchor=&quot;bottom&quot;, y=0.95, orientation=&quot;h&quot;)<br>)<br><br>fig.update_traces(patch={&quot;line&quot;: {&quot;dash&quot;: &quot;dash&quot;}}) <br>fig.show(renderer=&quot;notebook&quot;)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/902/1*7EnOvMkqKGIAszbgCU20wQ.png" /><figcaption>Fig 1 — A visualization of calibrated and non-calibrated curve.</figcaption></figure><p>On the x-axis, we have model output p which is between 0 and 1 and on the y-axis, we have fractions of positive captured within the predicted probability bin. We expect a linear relationship with slope 1.</p><h3>Why do we need Model calibration?</h3><p>There are many cases where model calibration is not required like in the case of ranking or selecting the top 20% for some targeting campaign. Calibrated models are especially important in making decision between multiple options of different magnitude or sizes, like expected value problems. In complex machine-learning decision engines, a machine-learning model might be used in conjunction with other machine-learning models.</p><p>For example, the business would like to prioritize upselling customer an additional product. So, a data scientist builds two ML models to estimate the probability of a customer buying two different products in addition to the products they are already purchasing.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/825/1*Pzazs92dKO8xxZmmDUHBQw.png" /><figcaption>Fig. 2: Upsell probability</figcaption></figure><p>In this case, Product 1 with $10 in revenue has an 80% probability of upselling, while Product 2 with $100 in revenue has a 60% chance of upselling. As a business, you might want to recommend Product 2 to the customer because of the higher expected value.</p><p>To do this expected value comparison between models, you need your models to be calibrated to output accurate probabilities.</p><h3>Case study: Three methods for calibration</h3><p>For demonstration in this article, we will be solving a binary classification problem using the <a href="https://archive.ics.uci.edu/ml/datasets/Adult">Adult Income Dataset</a> from the <strong>UCI machine learning Repository</strong>[1] to predict if a certain individual income based on various census information (education level, age, gender, occupation, and more) exceeds $50K/year. For this example, we will limit our scope to the United States and the following predictors:</p><ul><li>Age: continuous variable, individuals’ age</li><li>Occupation: categorical variable, Tech-support, Craft-repair, Other-service, Sales, Exec-managerial, Prof-specialty, Handlers-cleaners, Machine-op-inspct, Adm-clerical, Farming-fishing, Transport-moving, Priv-house-serv, Protective-serv, Armed-Forces.</li><li>HoursPerWeek: continuous variable, amount of hours spent in a job per week</li><li>Education: categorical variable, Bachelors, Some-college, 11th, HS-grad, Prof-school, Assoc-acdm, Assoc-voc, 9th, 7th-8th, 12th, Masters, 1st-4th, 10th, Doctorate, 5th-6th, Preschool.</li></ul><p>In the below code we:</p><ul><li>Import the libraries needed for this project</li><li>Load the data from our source</li><li>Label our columns</li><li>Filter data to only our desired columns, and limit our scope just to the United States for simplicity</li><li>Label encode our categorical columns</li><li>Set our random seed (for reproducibility)</li><li>Split our dataset into test and train</li></ul><pre>## Importing required libraries<br>from sklearn import datasets<br>from sklearn.model_selection import train_test_split<br>from sklearn import metrics<br>from sklearn.isotonic import IsotonicRegression<br>from sklearn.preprocessing import LabelEncoder<br>from sklearn.calibration import CalibratedClassifierCV<br>from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier<br>import matplotlib.pyplot as plt<br>%matplotlib inline<br>plt.style.use(&#39;dark_background&#39;)<br><br>def calibration_data(y_true, y_pred):<br>    df = pd.DataFrame({&#39;y_true&#39;:y_true, &#39;y_pred_bucket&#39;: (y_pred//0.05)*0.05 + 0.025})<br>    cdf = df.groupby([&#39;y_pred_bucket&#39;], as_index=False).agg({&#39;y_true&#39;:[&quot;mean&quot;,&quot;count&quot;]})<br>    return cdf.y_true.values[:,0][cdf.y_true.values[:,1]&gt;10], cdf.y_pred_bucket.values[cdf.y_true.values[:,1]&gt;10]<br><br>def label_encoder(df,columns):<br>    &#39;&#39;&#39;<br>    Function to label encode<br>    Required Input - <br>        - df = Pandas DataFrame<br>        - columns = List input of all the columns which needs to be label encoded<br>    Expected Output -<br>        - df = Pandas DataFrame with lable encoded columns<br>        - le_dict = Dictionary of all the column and their label encoders<br>    &#39;&#39;&#39;<br>    le_dict = {}<br>    for c in columns:<br>        print(&quot;Label encoding column - {0}&quot;.format(c))<br>        lbl = LabelEncoder()<br>        lbl.fit(list(df[c].values.astype(&#39;str&#39;)))<br>        df[c] = lbl.transform(list(df[c].values.astype(&#39;str&#39;)))<br>        le_dict[c] = lbl<br>    return df, le_dict<br><br>df = pd.read_csv( &quot;https://archive.ics.uci.edu/ml/machine-learning-databases/adult/adult.data&quot;, header=None)<br>df.columns = [<br>    &quot;Age&quot;, &quot;WorkClass&quot;, &quot;fnlwgt&quot;, &quot;Education&quot;, &quot;EducationNum&quot;,<br>    &quot;MaritalStatus&quot;, &quot;Occupation&quot;, &quot;Relationship&quot;, &quot;Race&quot;, &quot;Gender&quot;,<br>    &quot;CapitalGain&quot;, &quot;CapitalLoss&quot;, &quot;HoursPerWeek&quot;, &quot;NativeCountry&quot;, &quot;Income&quot;<br>]<br><br>## Filtering for Unites states<br>df = df.loc[df.NativeCountry == &#39; United-States&#39;,:]<br><br>## Only - Taking required columns<br>df = df.loc[:,[&quot;Education&quot;, &quot;Age&quot;,&quot;Occupation&quot;, &quot;HoursPerWeek&quot;, &quot;Income&quot;]]<br>df, _ = label_encoder(df, columns = [&quot;Education&quot;, &quot;Occupation&quot;, &quot;Income&quot;])<br>X = df.loc[:,[&quot;Education&quot;,&quot;Age&quot;, &quot;Occupation&quot;, &quot;HoursPerWeek&quot;]]<br>y = df.Income<br><br>seed = 42<br>X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.20, random_state=seed)</pre><p>Next, we:</p><ul><li>Fit a random forest classifier on this data</li><li>Visualize the calibration plot</li></ul><pre>## Fitting the model on training data<br>rf_model = RandomForestClassifier(random_state=seed)<br>rf_model.fit(X_train, y_train)<br><br>## getting the output to visualize on test data<br>prob_true, prob_pred  = calibration_data(y_true = y_test, <br>                                          y_pred = rf_model.predict_proba(X_test)[:,1])</pre><pre>chart_df = pd.DataFrame({<br>    &quot;actuals&quot;: prob_true,<br>    &quot;predicted&quot;: prob_pred,<br>    &quot;expected&quot;: prob_pred<br>})<br>fig = px.line(<br>        data_frame = chart_df, <br>        markers = True,<br>        x = &quot;predicted&quot;, <br>        y = [&quot;actuals&quot;, &quot;expected&quot;], <br>        template = &quot;plotly_dark&quot;)<br>fig.update_layout(<br>        title  = {&quot;text&quot;: &quot;Calibration Plot: Without calibration&quot;, &quot;y&quot;: 0.95, &quot;x&quot;: 0.5},<br>        xaxis_title=&quot;Predicted Probability&quot;,<br>        yaxis_title=&quot;Actual Probability&quot;,<br>        font = dict(size=15)<br>)<br>fig.show(renderer=&#39;notebook&#39;)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/789/1*2La8TKiuFpnZOjcK9Y3gMA.png" /><figcaption>Fig 3— An example of non-calibrated classifier</figcaption></figure><p>As we can see above, the model is over-predicting in lower deciles and under-predicting in higher deciles.</p><h3>Calibration method 1: Isotonic Regression</h3><p>Isotonic regression is a variation of ordinary least squares regression. Isotonic regression has the added constraints that the predicted values must always be increasing or decreasing, and the predicted values have to lie as close to the observation as possible. Isotonic regression is often used in situations where the relationship between the input and output variables is known to be monotonic, but the exact form of the relationship is not known. In our case, we want a monotonic behavior where the actual probabilities must always be increasing with increasing predicted probability.</p><p>Mathematically, we can write the Isotonic regression as following -</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/245/1*8A-dSqKGWVEUyh4L5hGFVg.png" /></figure><p>Where-</p><ul><li>iso is the isotonic function</li><li>f(x) is the predicted probability from the original classifier f</li><li>pcalib is the calibrated probability</li></ul><p>and we want to optimize the following function -</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/266/1*EV8NRNr2e4hLFg8_OkdKwA.png" /></figure><p>Isotonic regressions is prone to overfitting, hence it works better when there is enough training data available.</p><p>We will be using <a href="https://scikit-learn.org/stable/modules/generated/sklearn.calibration.CalibratedClassifierCV.html">CalibratedClassifierCV</a>to calibrate our classifier. The isotonic method fits a non-parametric isotonic regressor, which outputs a step-wise non-decreasing function. [2]</p><pre>## training model using random forest and isotonic regression for calibration<br>calibrated_rf = CalibratedClassifierCV(RandomForestClassifier(random_state=seed), method = &#39;isotonic&#39;)<br>calibrated_rf.fit(X_train, y_train)<br><br>## getting the output to visualize on test data<br>prob_true_calib, prob_pred_calib  = calibration_data(y_test, calibrated_rf.predict_proba(X_test)[:,1])<br><br>chart_df = pd.DataFrame({<br>    &quot;actuals&quot;: prob_true_calib,<br>    &quot;predicted&quot;: prob_pred_calib,<br>    &quot;expected&quot;: prob_pred_calib<br>})<br>fig = px.line(<br>        data_frame = chart_df, <br>        markers = True,<br>        x = &quot;predicted&quot;, <br>        y = [&quot;actuals&quot;, &quot;expected&quot;], <br>        template = &quot;plotly_dark&quot;)<br>fig.update_layout(<br>        title  = {&quot;text&quot;: &quot;Calibration Plot: Isotonic&quot;, &quot;y&quot;: 0.95, &quot;x&quot;: 0.5},<br>        xaxis_title=&quot;Predicted Probability&quot;,<br>        yaxis_title=&quot;Actual Probability&quot;,<br>        font = dict(size=15)<br>)<br>fig.show(renderer=&#39;notebook&#39;)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/795/1*_jtCJjyBXNoyBBCE4VwiBg.png" /><figcaption>Fig 4 — An example of calibration using Isotonic regression</figcaption></figure><p>As we can see above, the model is well calibrated once Isotonic regression is used because the actual probability lies much closer to the expected probability.</p><h3>Calibration method 2: Sigmoid / Platt scaling</h3><p>The sigmoid method, also known as Platt scaling, works by transforming the outputs of the classifier using a sigmoid function. The calibrated probabilities are obtained using the following sigmoid function -</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/248/1*a0fk1jsmv57Cwq488I7ycg.png" /></figure><p>Where -</p><ul><li>pcalib is the calibrated probability</li><li>f(x) is the predicted probability from the original classifier f</li><li>A and B are scalar parameters that learned by the algorithm</li></ul><p>The parameters A and B are estimated using a maximum likelihood method that optimizes on the same training set as that for the original classifier f.</p><p>Sigmoid/Platt scaling was originally invented for SVMs and it works well for other methods as well. This method is less prone to overfitting and should be preferred over Isotonic regression if you have less training data.</p><pre>## training model using random forest and sigmoid method for calibration<br>calibrated_sigmoid = CalibratedClassifierCV(<br>     RandomForestClassifier(random_state=seed), method = &#39;sigmoid&#39;)<br>calibrated_sigmoid.fit(X_train, y_train)<br><br>## getting the output to visualize on test data<br>prob_true_calib, prob_pred_calib  = calibration_data(y_test, calibrated_sigmoid.predict_proba(X_test)[:,1])<br><br>chart_df = pd.DataFrame({<br>    &quot;actuals&quot;: prob_true_calib,<br>    &quot;predicted&quot;: prob_pred_calib,<br>    &quot;expected&quot;: prob_pred_calib<br>})<br>fig = px.line(<br>        data_frame = chart_df, <br>        markers = True,<br>        x = &quot;predicted&quot;, <br>        y = [&quot;actuals&quot;, &quot;expected&quot;], <br>        template = &quot;plotly_dark&quot;)<br>fig.update_layout(<br>        title  = {&quot;text&quot;: &quot;Calibration Plot : Sigmoid&quot;, &quot;y&quot;: 0.95, &quot;x&quot;: 0.5},<br>        xaxis_title=&quot;Predicted Probability&quot;,<br>        yaxis_title=&quot;Actual Probability&quot;,<br>        font = dict(size=15)<br>)<br>fig.show(renderer=&#39;notebook&#39;)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/792/1*WM7RRZoxuIzd8VgaNUuLAg.png" /><figcaption>Fig 5 — An example of calibration using sigmoid method</figcaption></figure><p>As we can see above, the model is more calibrated than our default model but not as well as the isotonic method.</p><h3>Calibration method 3: Train with “Logloss” metric</h3><p>Many boosting based ensembling classifier models like GradientBoostingClassifier, LightGBM, and Xgboost use log_loss as their default loss function. Training with log_loss helps the output probabilities to be calibrated. To explain why log loss helps in calibration, let’s look at binary log loss function -</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/531/1*rQyp255p1lxBuqB42D1zZQ.png" /></figure><p>Now let’s take an instance of observation where the true outcome is 1. Consider two predictions of 0.7 and 0.9 from the model. If we take the cutoff &gt; 0.5 both of these predictions are correct but the logloss for 0.7 and 0.9 prediction is 0.36 and 0.1 respectively. As we can see log loss penalizes uncertainty in prediction forcing the model to predict close to the actual outcome.</p><p>Training with logloss is preferred over fitting a calibration function as model is already calibrated and reduces overhead of training and maintaining an extra model for calibration.</p><pre>## Fitting the model on training data<br>gb_model = GradientBoostingClassifier()<br>gb_model.fit(X_train, y_train)<br><br>## getting the output to visualize on test data<br>prob_true, prob_pred  = calibration_data(y_true = y_test, <br>                                          y_pred = gb_model.predict_proba(X_test)[:,1])<br><br>chart_df = pd.DataFrame({<br>    &quot;actuals&quot;: prob_true,<br>    &quot;predicted&quot;: prob_pred,<br>    &quot;expected&quot;: prob_pred<br>})<br>fig = px.line(<br>        data_frame = chart_df, <br>        markers = True,<br>        x = &quot;predicted&quot;, <br>        y = [&quot;actuals&quot;, &quot;expected&quot;], <br>        template = &quot;plotly_dark&quot;)<br>fig.update_layout(<br>        title  = {&quot;text&quot;: &quot;Calibration Plot: Using boosting method&quot;, &quot;y&quot;: 0.95, &quot;x&quot;: 0.5},<br>        xaxis_title=&quot;Predicted Probability&quot;,<br>        yaxis_title=&quot;Actual Probability&quot;,<br>        font = dict(size=15)<br>)<br>fig.show(renderer=&#39;notebook&#39;)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/786/1*28AGoXvejFj2N3aj6Erh9A.png" /><figcaption>Fig 6 — An example of calibration using boosting method</figcaption></figure><p>As we can see above, the model seems to be well calibrated. This is usually the case with most of the boosted methods as they use the log_loss function as their default loss function for binary classification.</p><h3>Conclusion and Practical Guidance</h3><p>In this article, we discussed what calibration is, in which applications it is important and why, and three different methods for calibration.</p><p>We demonstrated isotonic regression which can be used when there is enough training data available and model is not pre-calibrated.</p><p>We also demonstrated the Sigmoid method of calibration which works better when there is not enough training data and model is not pre-calibrated.</p><p>And finally, we demonstrated the Logloss metric for calibration which is the default in boosting methods and is our preferred method since the generated model is pre-calibrated and doesn’t require training and maintaining an extra model for calibration.</p><p><em>Aayush Agrawal is on </em><a href="https://www.linkedin.com/in/aayushmnit/"><em>LinkedIn</em></a>.</p><h3><strong>References</strong></h3><ol><li>Dua, D. and Graff, C. (2019). UCI Machine Learning Repository [http://archive.ics.uci.edu/ml]. Irvine, CA: University of California, School of Information and Computer Science. This dataset is licensed under a Creative Commons Attribution 4.0 International (CC BY 4.0) license.<a href="https://aayushmnit.com/posts/2022-10-26-Model_Calibration/Model%20Calibration.html#fnref1">↩︎</a></li><li><a href="https://scikit-learn.org/stable/modules/calibration.html#isotonic">Scikit learn calibration documentation</a><a href="https://aayushmnit.com/posts/2022-10-26-Model_Calibration/Model%20Calibration.html#fnref2">↩︎</a></li></ol><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1a7093b57a46" width="1" height="1" alt=""><hr><p><a href="https://medium.com/data-science-at-microsoft/model-calibration-for-classification-tasks-using-python-1a7093b57a46">Model calibration for classification tasks using Python</a> was originally published in <a href="https://medium.com/data-science-at-microsoft">Data Science + AI at Microsoft</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Python OOPs fundamentals]]></title>
            <link>https://medium.com/@aayushmnit/python-oops-fundamentals-ea12f5bdb8b?source=rss-85dd669ac015------2</link>
            <guid isPermaLink="false">https://medium.com/p/ea12f5bdb8b</guid>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[object-oriented]]></category>
            <category><![CDATA[python]]></category>
            <dc:creator><![CDATA[Aayush Agrawal]]></dc:creator>
            <pubDate>Tue, 27 Dec 2022 17:03:47 GMT</pubDate>
            <atom:updated>2022-12-27T17:03:47.948Z</atom:updated>
            <content:encoded><![CDATA[<h4>An introduction to Object Oriented programming using Python</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*VLFanEek4MRFU7kN.png" /></figure><p>Increasingly it’s becoming important for Data professionals to become better at programming and modern programming is centered around Object Oriented programming paradigm. This article helps in explaining some important programming concepts which are mostly language agnostic but we will be using Python in this article.</p><p>Object-oriented programming (OOPs) is a programming paradigm that relies on the concept of classes and objects. The basic idea of OOP is to divide a sophisticated program into a number of objects that interact with each other to achieve the desired functionality. There are several advantages of using OOP for data science:</p><ul><li><strong>Encapsulation</strong>: OOPs allow you to wrap data and the methods that operate on that data within a single unit (i.e., an object). This makes it easier to organize and manage your code, as well as protect the data from accidental modification.</li><li><strong>Code reusability</strong>: OOPs allow you to create reusable code by defining classes that can be used to create multiple objects with the same behavior. This makes it easier to develop and maintain your code, as you can reuse existing code instead of writing new code from scratch.</li><li><strong>Modularity</strong>: OOPs allow you to divide your code into smaller, modular units (i.e., objects). This makes it easier to understand and maintain your code, as you can focus on one piece of functionality at a time.</li><li><strong>Inheritance</strong>: OOPs allow you to create a new class that is a modified version of an existing class. This allows you to reuse code from the existing class and override or extend it as needed. This can save time and reduce the amount of code you need to write.</li></ul><p>Overall, OOP can help data professionals organize and manage their code more effectively, making it easier to develop and maintain data science projects. Let’s dive into the OOPs concept.</p><h3>What are Objects and Classes?</h3><p>Classes are the blueprint for defining an Object. While an Object is a collection of data/properties and their behaviors/methods.</p><p>For example- Think of a class Bulb that will have a state (On/Off) and methods to turnOn and turnoff the bulb.</p><pre>class Bulb():<br>    def __init__(self, onOff=False): self.onOff = onOff    <br>    def turnOn(self): self.onOff = True<br>    def turnOff(self): self.onOff = False</pre><p>Now we can create multiple bulb objects from this Bulb class.</p><pre>b1 = Bulb(onOff=True)<br>b2 = Bulb()<br>print(f&quot;Bulb 1 state is :{b1.onOff}, Bulb 2 state is :{b2.onOff}&quot;)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1002/1*qruzIGdsFQ8gbdIVbzoJ3Q.png" /></figure><p>b1 and b2 are objects of the class Bulb. Let’s use the turnOn and turnOff methods to update the bulb properties.</p><pre>b1.turnOff(); b2.turnOn()<br>print(f&quot;Bulb 1 state is :{b1.onOff}, Bulb 2 state is :{b2.onOff}&quot;)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/996/1*3Arbgopn7w0XJNKxoBVJ9g.png" /></figure><p>We can see from the example above, a Bulb object contains the onOff property. Properties are variables that contain information regarding the object of a class and Methods like turnOn and turnOff in our Bulb class are functions that have access to the properties of a class. Methods can accept additional parameters, modify properties and return values.</p><h3>Class and Instance variables</h3><p>In Python, properties can be defined in two ways -</p><ul><li><strong>Class Variables</strong> — Class variables are shared by all objects of the class. A change in the class variable will change the value of that property in <strong>all the objects of the class</strong>.</li><li><strong>Instance Variables</strong> — Instance variables are unique to each instance or object of the class. A change in instance variable will change the value of the property in that specific object only.</li></ul><pre>class Employee:<br>    # Creating a class variable<br>    companyName = &quot;Microsoft&quot;<br>    <br>    def __init__(self, name):<br>        # creating a instance variable<br>        self.name = name<br>    <br>e1 = Employee(&#39;Aayush&#39;)<br>e2 = Employee(&#39;John&#39;)<br><br>print(f&#39;Name :{e1.name}&#39;)<br>print(f&#39;Company Name: {e1.companyName}&#39;)<br>print(f&#39;Name :{e2.name}&#39;)<br>print(f&#39;Company Name: {e2.companyName}&#39;)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/997/1*2HAt1zB-E7jdClzBKmhFbQ.png" /></figure><p>We can see above, the class variable is defined outside of the initializer and the instance variable is defined inside the initializer.</p><pre>Employee.companyName = &quot;Amazon&quot;<br>print(e1.companyName, e2.companyName)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/996/1*ZJLMMvWxUHm22NaAHqKcoA.png" /></figure><p>We can see above changing a class variable in the Employee class changes the class variable in all objects of the class. Most of the time we will be using instance variables but knowledge about class variables can come in handy. Let’s look at an interesting use of class variable -</p><pre>class Employee:<br>    # Creating a class variable<br>    companyName = &quot;Microsoft&quot;<br>    companyEmployees = []<br>    <br>    def __init__(self, name):<br>        # creating a instance variable<br>        self.name = name<br>        self.companyEmployees.append(self.name)<br>    <br>e1 = Employee(&#39;Aayush&#39;)<br>e2 = Employee(&#39;John&#39;)<br><br>print(f&#39;Name :{e1.name}&#39;)<br>print(f&#39;Team Members: {e1.companyEmployees}&#39;)<br>print(f&#39;Name :{e2.name}&#39;)<br>print(f&#39;Company Name: {e2.companyEmployees}&#39;)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/989/1*B4nT56XjnVsuOAtnQXDEhg.png" /></figure><p>We can see above, we are saving all objects of the Employee class in companyEmployees which is a list shared by all objects of the class Employee.</p><h3>Class, Static and Instance methods</h3><p>In Python classes, we have three types of methods -</p><ul><li><strong>Class Methods</strong> — Class methods work with class variables and are accessible using the class name rather than its object.</li><li><strong>Static Methods</strong> — Static methods are methods that are usually limited to class only and not their objects. They don’t typically modify or access class and instance variables. They are used as utility functions inside the class and we don’t want the inherited class to modify them.</li><li><strong>Instance Methods</strong> — Instance methods are the most used methods and have access to instance variables within the class. They can also take new parameters to perform desired operations.</li></ul><pre>class Employee:<br>    # Creating a class variable<br>    companyName = &quot;Microsoft&quot;<br>    companyEmployees = []<br>    <br>    def __init__(self, name):<br>        # creating a instance variable<br>        self.name = name<br>        self.companyEmployees.append(self.name)<br>    <br>    @classmethod<br>    def getCompanyName(cls): # This is a class method<br>         return cls.companyName<br>    <br>    @staticmethod<br>    def plusTwo(x): # This is a static method<br>        return x+2<br>    <br>    def getName(self): # This is a instance method<br>        return self.name<br>    <br>e1 = Employee(&#39;Aayush&#39;)<br>print(f&quot;Calling class method. Company name is {e1.getCompanyName()}&quot;)<br>print(f&quot;Calling Static method. {e1.plusTwo(2)}&quot;)<br>print(f&quot;Calling instance method. Employee name is {e1.getName()}&quot;)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/991/1*2H3mIvow9sye0xv4ZSLxkg.png" /></figure><p>We can see above we use the @classmethod decorator to define the class method. cls is used to refer to the class just as self is used to refer to the object of the class. The class method at least takes one argument cls.</p><p>We use @staticmethod decorator to define static class plusTwo. We can see that static methods don’t take any argument like self and cls.</p><p>The most commonly used methods are instance methods and they can be defined without a decorator within the class. Just like the class method they take at least one argument which is self by convention.</p><h3>Access Modifiers</h3><p>Access modifiers limit access to the variables and functions of a class. There are three types of access modifiers — public, protected, and private.</p><h4>Public Attributes</h4><p>Public attributes are those methods and properties which can be accessed anywhere inside and outside of the class. By default, all the member variables and functions are public.</p><pre>class Employee:<br>    def __init__(self, name):<br>        self.name = name ## Public variable<br>        <br>    def getName(self): ## Public method<br>        return self.name<br><br>e1 = Employee(&quot;Aayush&quot;)<br>print(f&quot;Employee Name: {e1.getName()}&quot;)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1005/1*PvBRakQDOhEXW9sqd3tfKQ.png" /></figure><p>In the case above, both property name and method getName are public attributes.</p><h4>Protected Attributes</h4><p>Protected attributes are similar to public attributes which can be accessed within the class and also available to subclasses. The only difference is the convention, which is to define each protected member with a <em>single underscore “_”</em>.</p><pre>class Employee:<br>    def __init__(self, name, project):<br>        self.name = name ## Public variable<br>        self._project = project ## Protected variable<br>        <br>    def getName(self): ## Public method<br>        return self.name<br>    <br>    def _getProject(self): ## Protected method<br>        return self._project<br>    <br>e1 = Employee(&quot;Aayush&quot;, &quot;Project Orland&quot;)<br>print(f&quot;Employee Name: {e1.getName()}&quot;)<br>print(f&quot;Project Name: {e1._getProject()}&quot;)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/998/1*YIRvKYfv2waUJnBOObRKbg.png" /></figure><p>In the case above, both property _project and method _getProject are protected attributes.</p><h4>Private Attributes</h4><p>Private attributes are accessible within the class but not outside of the class. To define a private attribute, prefix the method or property with the <em>double underscore”_“</em>.</p><pre>class Employee:<br>    def __init__(self, name, project, salary):<br>        self.name = name ## Public variable<br>        self._project = project ## Protected variable<br>        self.__salary = salary<br>        <br>    def getName(self): ## Public method<br>        return self.name<br>    <br>    def _getProject(self): ## Protected method<br>        return self._project<br>    <br>    def __getSalary(self): ## Protected method<br>        return self.__salary<br>    <br>e1 = Employee(&quot;Aayush&quot;, &quot;Project Orland&quot;, &quot;3500&quot;)<br>print(f&quot;Employee Name: {e1.getName()}&quot;)<br>print(f&quot;Project Name: {e1.__getSalary()}&quot;) </pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/997/1*k5surX8_DQH6JMcsTSytNQ.png" /></figure><p>We can see above, __salary property and __getSalary method are both private attributes and when we call them outside of the class they throw an error that the &#39;Employee&#39; object has no attribute &#39;__getSalary&#39;.</p><h3>Encapsulation</h3><p>Encapsulation in OOP refers to binding data and the methods to manipulate that data together in a single unit, that is, class. Encapsulation is usually used to hide the state and representation of the object from the outside. A good use of encapsulation is to make all properties private of a class to prevent direct access from outside and use public methods to let the outside world communicate with the class.</p><pre>class Employee:<br>    def __init__(self, name, project, salary):<br>        self.__name = name ## Public variable<br>        self.__project = project ## Protected variable<br>        self.__salary = salary<br>        <br>    def getName(self): ## Public method<br>        return self.__name<br>    <br>e1 = Employee(&quot;Aayush&quot;, &quot;Project Orland&quot;, &quot;3500&quot;)<br>print(f&quot;Employee Name: {e1.getName()}&quot;)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/994/1*Sp9PFa9IOPZJx9DLEvB5Kw.png" /></figure><p>Encapsulation has several advantages -</p><ul><li>Properties of the class can be hidden from the outside world</li><li>More control over what the outside world can access from the class</li></ul><p>A good example of encapsulation would be an access control class based on username and password.</p><pre>class Auth:<br>    def __init__(self, userName=None, password=None):<br>        self.__userName = userName<br>        self.__password = password<br>        <br>    def login(self, userName, password):<br>        if (self.__userName == userName) and (self.__password == password):<br>            print (f&quot;Access granted to {userName}&quot;)<br>        else:<br>            print(&quot;Invalid credentials&quot;)<br>            <br>e1 = Auth(&quot;Aayush&quot;, &quot;whatever&quot;)<br>e1.login(&quot;Aayush&quot;, &quot;whatever&quot;) ## This will grant access<br><br>e1.login(&quot;Aayush&quot;, &quot;aasdasd&quot;) ## This will say invalid creds<br>e1.__password ## This will raise an error as private properties can&#39;t be accessed from outside.</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/996/1*AY55ayB-RvmbNe3vsgG1-w.png" /></figure><p>As we can see above __username and __password are protected properties and can only be used by the class to grand or reject access requests.</p><h3>Inheritance</h3><p>Inheritance provides a way to create new classes from the existing classes. The new class will inherit all the non-private attributes(properties and methods) from the existing class. The new class can be called a child class and the existing class can be called a parent class.</p><pre>import math<br>class Shape:<br>    def __init__(self, name):<br>        self.name = name<br>        <br>    def getArea(self):<br>        pass<br>    <br>    def printDetails(self):<br>        print(f&quot;This shape is called {self.name} and area is {self.getArea()}.&quot;)<br>        <br>class Square(Shape):<br>    def __init__(self, edge):<br>        ## calling the constructor from parent class Shape<br>        Shape.__init__(self, name = &quot;Square&quot;)<br>        self.edge = edge<br>    <br>    ## Overiding the getArea function<br>    def getArea(self):<br>        return self.edge**2<br>    <br>class Circle(Shape):<br>    def __init__(self, radius):<br>        ## calling the constructor from parent class Shape<br>        Shape.__init__(self, name = &quot;Circle&quot;)<br>        self.radius = radius<br>    ## Overiding the getArea function<br>    def getArea(self):<br>        return math.pi * (self.radius**2)<br>    <br>obj1 = Square(4)<br>obj1.printDetails()<br><br>obj2 = Circle(3)<br>obj2.printDetails()</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1002/1*t33pWQZmpKjdFqxZein4nA.png" /></figure><p>We can see above we defined a parent class Shape and then we inherited it to create a Square and Circle child class. While defining the Square and Circle class we overwrote the getArea function pertinent to the class but we used the printDetails function from the parent class to print details about child classes. The more common example in the machine learning world would be to create your own models in Pytorch where we inherit from nn.Module class to create a new model.</p><h4>Use of super() Function</h4><p>super() function comes into play when we implement inheritance. The super() function is used to refer to the parent class without explicitly naming the class. super() function can be used to access parent class properties, calling the parent class, and can be used as initializers. Let’s look at the example above and see how we can modify the Square class to use super() function.</p><pre>class Shape:<br>    maxArea = 100<br>    def __init__(self, name): self.name = name<br>    def getArea(self): pass<br>    def printDetails(self): <br>        print(f&quot;This shape is called {self.name} and area is {self.getArea()}.&quot;)<br><br>class Square(Shape):<br>    maxArea = 50<br>    def __init__(self, edge):<br>        super().__init__(name = &quot;Square&quot;) ## Initializing parent class<br>        self.edge = edge<br>    <br>    def getName(self):<br>        return super().maxArea<br>    <br>    def getArea(self):<br>        return self.edge**2<br>    <br>    def printDetails(self):<br>        super().printDetails() ## Calling a parent class function<br>        print(f&quot;Max are from Shape class: {super().maxArea}&quot;) ## Accessing parent class property<br>        print(f&quot;Max are from Square class: {self.maxArea}&quot;)<br><br>obj1 = Square(4)<br>obj1.getName()<br>obj1.printDetails()</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1002/1*jWhqh2B_ic9UOFAR7TBOBw.png" /></figure><p>As we can see in the example above we have used -</p><ul><li>super().__init__ to initialize the parent Shape class</li><li>super().printDetails() function to use a method from parent class</li><li>super().maxArea to access a property of a parent class</li></ul><p>There are many advantages of inheritance -</p><ol><li><strong>Reusability</strong> — Inheritance makes the code reusable. Common methods and properties can be stored in a parent class and child classes can inherit these methods.</li><li><strong>Modification</strong> — Code modification becomes easier if we use inheritance, if we want to make a change in the base class function it will be propagated to the child classes.</li><li><strong>Extensibility</strong> — We can derive new classes from the old ones by keeping things we need in the derived class.</li></ol><h3>Polymorphism</h3><p>Polymorphism refers to the same object exhibiting different forms and behaviors. For example consider our shape class which could be a square, rectangle, polygon, etc. Instead of writing multiple functions to get the area of these shapes, we can use a common function like getArea() and implement this function in the derived class.</p><pre>import math<br>class Shape:<br>    def __init__(self, name):<br>        self.name = name<br>        <br>    def getArea(self):<br>        pass<br>    <br>    def printDetails(self):<br>        print(f&quot;This shape is called {self.name} and area is {self.getArea()}.&quot;)<br>        <br>class Square(Shape):<br>    def __init__(self, edge):<br>        ## calling the constructor from parent class Shape<br>        Shape.__init__(self, name = &quot;Square&quot;)<br>        self.edge = edge<br>    <br>    ## Overiding the getArea function<br>    def getArea(self):<br>        return self.edge**2<br>    <br>class Circle(Shape):<br>    def __init__(self, radius):<br>        ## calling the constructor from parent class Shape<br>        Shape.__init__(self, name = &quot;Circle&quot;)<br>        self.radius = radius<br>    ## Overiding the getArea function<br>    def getArea(self):<br>        return math.pi * (self.radius**2)<br>    <br>obj1 = Square(4)<br>print(f&quot;Area of this {obj1.name} is {obj1.getArea()}&quot;)<br><br>obj2 = Circle(3)<br>print(f&quot;Area of this {obj2.name} is {obj2.getArea()}&quot;)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1005/1*mfylBIzXwpQgjNaXpNxyHQ.png" /></figure><p>As we can see above there is a pre-defined dummy method called getArea in the Shape class. We override this method in the Square and Circle class. This technique is called method overriding. The advantage of method overriding is that the derived class can write its own specific implementation based on the requirement while using the same function name.</p><h4>Abstract base classes</h4><p>Abstract base classes define a set of methods and properties that a class must implement in order to inherit the parent class. This is a useful technique to enforce that certain functions within the derived class must exist. To define an abstract base class, we use the abc module. The abstract base class inherits from the built-in ABC class and we use the decorator @abstractmethod to declare an abstract method.</p><pre>from abc import ABC, abstractmethod<br><br>class Shape(ABC):<br>    def __init__(self, name):<br>        self.name = name<br>    <br>    @abstractmethod<br>    def getArea(self):<br>        pass<br>    <br>    def printDetails(self):<br>        print(f&quot;This shape is called {self.name} and area is {self.getArea()}.&quot;)<br>        <br>class Square(Shape):<br>    def __init__(self, edge):<br>        ## calling the constructor from parent class Shape<br>        Shape.__init__(self, name = &quot;Square&quot;)<br>        self.edge = edge<br>    <br>obj1 = Square(4)<br>print(f&quot;Area of this {obj1.name} is {obj1.getArea()}&quot;)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/992/1*tThgimprihof8Z7CbNFxbg.png" /></figure><p>We can see above that we have created a Shape class from the ABC class which has an abstract method getArea. Since our child class Square didn’t have getArea implemented we get an error instantiating this class.</p><pre>from abc import ABC, abstractmethod<br><br>class Shape(ABC):<br>    def __init__(self, name):<br>        self.name = name<br>    <br>    @abstractmethod<br>    def getArea(self):<br>        pass<br>    <br>    def printDetails(self):<br>        print(f&quot;This shape is called {self.name} and area is {self.getArea()}.&quot;)<br>        <br>class Square(Shape):<br>    def __init__(self, edge):<br>        ## calling the constructor from parent class Shape<br>        Shape.__init__(self, name = &quot;Square&quot;)<br>        self.edge = edge<br>    <br>    def getArea(self): return self.edge**2<br>    <br>obj1 = Square(4)<br>print(f&quot;Area of this {obj1.name} is {obj1.getArea()}&quot;)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/999/1*-f8TC9cmyxdmBKxj37ST6A.png" /></figure><p>We can see above, once we implemented the getArea method, the code runs fine.</p><p>Abstract base classes serve as a blueprint for derived classes to implement methods that are required to run the function appropriately.</p><h3>8 Conclusion</h3><p>In this article, we learned about what is object-oriented programming and key concepts using Python. A good understanding of these concepts will lay a solid foundation for any software professional to write and understand python code better.</p><p>I hope you enjoyed reading it. If there is any feedback on the code or just the blog post, feel free to comment below or reach out on <a href="https://www.linkedin.com/in/aayushmnit/">LinkedIn</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ea12f5bdb8b" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>