<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Programming on Max Woolf&#39;s Blog</title>
    <link>https://minimaxir.com/category/programming/</link>
    <description>Recent content in Programming on Max Woolf&#39;s Blog</description>
    <image>
      <title>Max Woolf&#39;s Blog</title>
      <url>https://minimaxir.com/android-chrome-512x512.png</url>
      <link>https://minimaxir.com/android-chrome-512x512.png</link>
    </image>
    <generator>Hugo</generator>
    <language>en</language>
    <copyright>Copyright Max Woolf © 2026</copyright>
    <lastBuildDate>Fri, 27 Feb 2026 10:00:00 -0800</lastBuildDate>
    <atom:link href="https://minimaxir.com/category/programming/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>An AI agent coding skeptic tries AI agent coding, in excessive detail</title>
      <link>https://minimaxir.com/2026/02/ai-agent-coding/</link>
      <pubDate>Fri, 27 Feb 2026 10:00:00 -0800</pubDate>
      <guid>https://minimaxir.com/2026/02/ai-agent-coding/</guid>
      <description>No vagueposting here, just look at the Estimated Read Time.</description>
      <content:encoded><![CDATA[<p><span><style type="text/css">
pre code.language-txt, pre code.language-md{
white-space: pre-wrap !important;
word-break: normal !important;
}
</style></span></p>
<p>You&rsquo;ve likely seen many blog posts about AI agent coding/<a href="https://en.wikipedia.org/wiki/Vibe_coding">vibecoding</a> where the author talks about all the wonderful things agents can now do supported by vague anecdata, how agents will lead to the atrophy of programming skills, how agents impugn the sovereignty of the human soul, etc etc. This is <strong>NOT</strong> one of those posts. You&rsquo;ve been warned.</p>
<p>Last May, I wrote a blog post titled <a href="https://minimaxir.com/2025/05/llm-use/">As an Experienced LLM User, I Actually Don&rsquo;t Use Generative LLMs Often</a> as a contrasting response to the hype around the rising popularity of agentic coding. In that post, I noted that while LLMs are most definitely not useless and they can answer simple coding questions faster than it would take for me to write it myself with sufficient accuracy, agents are a tougher sell: they are unpredictable, expensive, and the hype around it was wildly disproportionate given the results I had seen in personal usage. However, I concluded that I was open to agents if LLMs improved enough such that all my concerns were addressed and agents were more dependable.</p>
<p>In the months since, I continued my real-life work as a Data Scientist while keeping up-to-date on the latest LLMs popping up on <a href="https://openrouter.ai">OpenRouter</a>. In August, Google <a href="https://developers.googleblog.com/introducing-gemini-2-5-flash-image/">announced</a> the release of their Nano Banana generative image AI with a <a href="https://ai.google.dev/gemini-api/docs/image-generation">corresponding API</a> that&rsquo;s difficult to use, so I open-sourced the <a href="https://github.com/minimaxir/gemimg">gemimg Python package</a> that serves as an API wrapper. It&rsquo;s not a thrilling project: there&rsquo;s little room or need for creative implementation and my satisfaction with it was the net present value with what it enabled rather than writing the tool itself. Therefore as an experiment, I plopped the feature-complete code into various up-and-coming LLMs on OpenRouter and prompted the models to identify and fix any issues with the Python code: if it failed, it&rsquo;s a good test for the current capabilities of LLMs, if it succeeded, then it&rsquo;s a software quality increase for potential users of the package and I have no moral objection to it. The LLMs actually were helpful: in addition to adding good function docstrings and type hints, it identified more Pythonic implementations of various code blocks.</p>
<p>Around this time, my coworkers were pushing <a href="https://github.com/features/copilot">GitHub Copilot</a> within <a href="https://code.visualstudio.com">Visual Studio Code</a> as a coding aid, particularly around then-new <a href="https://www.anthropic.com/news/claude-sonnet-4-5">Claude Sonnet 4.5</a>. For my data science work, Sonnet 4.5 in Copilot was not helpful and tended to create overly verbose Jupyter Notebooks so I was not impressed. However, in November, Google then <a href="https://blog.google/innovation-and-ai/products/nano-banana-pro/">released</a> Nano Banana Pro which necessitated an immediate update to <code>gemimg</code> for compatibility with the model. After experimenting with Nano Banana Pro, I discovered that the model can <a href="https://minimaxir.com/2025/12/nano-banana-pro/#grid">create images with arbitrary grids</a> (e.g. 2x2, 3x2) as an extremely practical workflow, so I quickly <a href="https://github.com/minimaxir/gemimg/issues/15">wrote a spec</a> to implement support and also slice each subimage out of it to save individually. I knew this workflow is relatively simple-but-tedious to implement using <a href="https://pypi.org/project/pillow/">Pillow</a> shenanigans, so I felt safe enough to ask Copilot to <code>Create a grid.py file that implements the Grid class as described in issue #15</code>, and it did just that although with some errors in areas not mentioned in the spec (e.g. mixing row/column order) but they were easily fixed with more specific prompting. Even accounting for handling errors, that&rsquo;s enough of a material productivity gain to be more <em>optimistic</em> of agent capabilities, but not nearly enough to become an AI hypester.</p>
<p>In November, just a few days before Thanksgiving, Anthropic <a href="https://www.anthropic.com/news/claude-opus-4-5">released Claude Opus 4.5</a> and naturally my coworkers were curious if it was a significant improvement over Sonnet 4.5. It was very suspicious that Anthropic released Opus 4.5 right before a major holiday since companies typically do that in order to bury underwhelming announcements as your prospective users will be too busy gathering with family and friends to notice. Fortunately, I had no friends and no family in San Francisco so I had plenty of bandwidth to test the new Opus.</p>
<h2 id="a-foreword-on-agentsmd">A Foreword on AGENTS.md</h2>
<p>One aspect of agents I hadn&rsquo;t researched but knew was necessary to getting good results from agents was the concept of the <a href="https://agents.md">AGENTS.md</a> file: a file which can control specific behaviors of the agents such as code formatting. If the file is present in the project root, the agent will automatically read the file and in theory obey all the rules within. This is analogous to system prompts for normal LLM calls and if you&rsquo;ve been following my writing, I have an unhealthy addiction to highly nuanced system prompts with additional shenanigans such as ALL CAPS for increased adherence to more important rules (yes, that&rsquo;s still effective). I could not find a good starting point for a Python-oriented <code>AGENTS.md</code> I liked, so I asked Opus 4.5 to make one:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl">Add an <span class="sb">`AGENTS.md`</span> file oriented for good Python code quality. It should be intricately details. More important rules should use caps, e.g. <span class="sb">`MUST`</span>
</span></span></code></pre></div><p>I then added a few more personal preferences and suggested tools from my previous failures working with agents in Python: use <code>uv</code> and <code>.venv</code> instead of the base Python installation, use <code>polars</code> instead of <code>pandas</code> for data manipulation, only store secrets/API keys/passwords in <code>.env</code> while ensuring <code>.env</code> is in <code>.gitignore</code>, etc. Most of these constraints don&rsquo;t tell the agent what to do, but <em>how</em> to do it. In general, adding a rule to my <code>AGENTS.md</code> whenever I encounter a fundamental behavior I don&rsquo;t like has been very effective. For example, agents love using unnecessary emoji which I hate, so I added a rule:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl">**NEVER** use emoji, or unicode that emulates emoji (e.g. ✓, ✗).
</span></span></code></pre></div><p>Agents also tend to leave a lot of redundant code comments, so I added another rule to prevent that:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl">**MUST** avoid including redundant comments which are tautological or self-demonstating (e.g. cases where it is easily parsable what the code does at a glance or its function name giving sufficient information as to what the code does, so the comment does nothing other than waste user time)
</span></span></code></pre></div><p>My up-to-date <code>AGENTS.md</code> file for Python is available <a href="https://gist.github.com/minimaxir/10b780671ee5d695b4369b987413b38f">here</a>, and throughout my time working with Opus, it adheres to every rule despite the file&rsquo;s length, and in the instances where I accidentally query an agent without having an <code>AGENTS.md</code>, it&rsquo;s <em>very</em> evident. It would not surprise me if the file is the main differentiator between those getting good and bad results with agents, although success is <a href="https://news.ycombinator.com/item?id=47034087">often mixed</a>.</p>
<p>As a side note if you are using <a href="https://code.claude.com/docs/en/overview">Claude Code</a>, the file must be named <code>CLAUDE.md</code> instead because Anthropic is weird; this blog post will just use <code>AGENTS.md</code> for consistency.</p>
<h2 id="opus-first-contact">Opus First Contact</h2>
<p>With my <code>AGENTS.md</code> file set up, I did more research into proper methods of prompting agents to see if I was missing something that led to the poor performance from working with Sonnet 4.5.</p>
<figure>

    <img loading="lazy" srcset="/2026/02/ai-agent-coding/claude_docs_hu_53e14b873c3cfe1e.webp 320w,/2026/02/ai-agent-coding/claude_docs_hu_b0bc0e75f4311cb4.webp 768w,/2026/02/ai-agent-coding/claude_docs_hu_109be808d2b02579.webp 1024w,/2026/02/ai-agent-coding/claude_docs.png 1378w" src="claude_docs.png"
         alt="From the Claude Code quickstart."/> <figcaption>
            <p>From the <a href="https://code.claude.com/docs/en/quickstart">Claude Code quickstart</a>.</p>
        </figcaption>
</figure>

<p>Anthropic&rsquo;s prompt suggestions are simple, but you can&rsquo;t give an LLM an open-ended question like that and expect the results <em>you</em> want! You, the user, are likely subconsciously picky, and there are always functional requirements that the agent won&rsquo;t magically apply because it cannot read minds and behaves as a <a href="https://tvtropes.org/pmwiki/pmwiki.php/Main/LiteralGenie">literal genie</a>. My approach to prompting is to write the potentially-very-large individual prompt in its own Markdown file (which can be tracked in <code>git</code>), then tag the agent with that prompt and tell it to implement that Markdown file. Once the work is completed and manually reviewed, I manually commit the work to <code>git</code>, with the message referencing the specific prompt file so I have good internal tracking.</p>
<figure>

    <img loading="lazy" srcset="/2026/02/ai-agent-coding/implement_hu_85f9ba4bd738ee71.webp 320w,/2026/02/ai-agent-coding/implement.png 574w" src="implement.png"/> 
</figure>

<p>I completely ignored Anthropic&rsquo;s advice and wrote a more elaborate test prompt based on a use case I&rsquo;m familiar with and therefore can audit the agent&rsquo;s code quality. In 2021, I wrote a script to <a href="https://github.com/minimaxir/youtube-video-scraper">scrape YouTube video metadata</a> from videos on a given channel using <a href="https://developers.google.com/youtube/v3">YouTube&rsquo;s Data API</a>, but the API is poorly and counterintuitively documented and my Python scripts aren&rsquo;t great. I subscribe to the <a href="https://www.youtube.com/channel/UC9ecwl3FTG66jIKA9JRDtmg">SiIvagunner YouTube account</a> which, as a part of the channel&rsquo;s gimmick (<a href="https://www.youtube.com/watch?v=rEcOzjg7vBU">musical swaps</a> with different melodies than the ones expected), posts hundreds of videos per month with nondescript thumbnails and titles, making it nonobvious which videos are the best other than the view counts. The video metadata could be used to surface good videos I missed, so I had a fun idea to test Opus 4.5:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl">Create a robust Python script that, given a YouTube Channel ID, can scrape the YouTube Data API and store all video metadata in a SQLite database. The YOUTUBE_API_KEY is present in <span class="sb">`.env`</span>.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Documentation on the channel endpoint: https://developers.google.com/youtube/v3/guides/implementation/channels
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">The test channel ID to scrape is: <span class="sb">`UC9ecwl3FTG66jIKA9JRDtmg`</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">You MUST obey ALL the FOLLOWING rules in your implementation.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> Do not use the Google Client SDK. Use the REST API with <span class="sb">`httpx`</span>.
</span></span><span class="line"><span class="cl"><span class="k">-</span> Include sensible aggregate metrics, e.g. number of comments on the video.
</span></span><span class="line"><span class="cl"><span class="k">-</span> Incude <span class="sb">`channel_id`</span> and <span class="sb">`retrieved_at`</span> in the database schema.
</span></span></code></pre></div><p>The resulting script is available <a href="https://github.com/minimaxir/youtube_scraper_opus/blob/main/scrape_channel.py">here</a>, and it worked first try to scrape up to 20,000 videos (the max limit). The resulting Python script has very Pythonic code quality following the copious rules provided by the <code>AGENTS.md</code>, and it&rsquo;s more robust than my old script from 2021. It is most definitely not the type of output I encountered with Sonnet 4.5. There was a minor issue however: the logging is implemented naively such that the API key is leaked in the console. I added a rule to <code>AGENTS.md</code> but really this is the YouTube API&rsquo;s fault for <a href="https://developers.google.com/youtube/v3/getting-started#example-1">encouraging API keys as parameters in a GET request</a>.</p>
<p>I asked a more data-science-oriented followup prompt to test Opus 4.5&rsquo;s skill at data-sciencing:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl">Create a Jupyter Notebook that, using <span class="sb">`polars`</span> to process the data, does a thorough exploratory data analysis of data saved in <span class="sb">`youtube_videos.db`</span>, for all columns.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">This analysis should be able to be extended to any arbitrary input <span class="sb">`channel_id`</span>.
</span></span></code></pre></div><p>The <a href="https://github.com/minimaxir/youtube_scraper_opus/blob/main/eda_youtube.ipynb">resulting Jupyter Notebook</a> is&hellip;indeed thorough. That&rsquo;s on me for specifying &ldquo;for all columns&rdquo;, although it was able to infer the need for temporal analysis (e.g. total monthly video uploads over time) despite not explicitly being mentioned in the prompt.</p>
<p>The monthly analysis gave me an idea: could Opus 4.5 design a small webapp to view the top videos by month? That gives me the opportunity to try another test of how well Opus 4.5 works with less popular frameworks than React or other JavaScript component frameworks that LLMs push by default. Here, I&rsquo;ll try <a href="https://fastapi.tiangolo.com">FastAPI</a>, <a href="https://picocss.com">Pico CSS</a> for the front end (because we don&rsquo;t need a JavaScript framework for this), and <a href="https://htmx.org">HTMX</a> for lightweight client/server interactivity:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl">Create a Hacker News-worthy FastAPI application using HTMX for interactivity and PicoCSS for styling to build a YouTube-themed application that leverages <span class="sb">`youtube_videos.db`</span> to create an interactive webpage that shows the top videos for each month, including embedded YouTube videos which can be clicked.
</span></span></code></pre></div><figure>

    <img loading="lazy" srcset="/2026/02/ai-agent-coding/yt_web_app_hu_813072116f12d2de.webp 320w,/2026/02/ai-agent-coding/yt_web_app_hu_1416f19b3e02545d.webp 768w,/2026/02/ai-agent-coding/yt_web_app_hu_488b0400e889f7ac.webp 1024w,/2026/02/ai-agent-coding/yt_web_app.webp 1592w" src="yt_web_app.webp"/> 
</figure>

<p>The FastAPI webapp <a href="https://github.com/minimaxir/youtube_scraper_opus/blob/main/app.py">Python code</a> is good with logical integration of HTMX routes and partials, but Opus 4.5 had fun with the &ldquo;YouTube-themed&rdquo; aspect of the prompt: the video thumbnail simulates a YouTube thumbnail with video duration that loads an embedded video player when clicked! The full code is open-source <a href="https://github.com/minimaxir/youtube_scraper_opus/">in this GitHub repository</a>.</p>
<p>All of these tests performed far better than what I expected given my prior poor experiences with agents. Did I gaslight myself by being an agent skeptic? How did a LLM sent to die finally solve my agent problems? Despite the holiday, X and Hacker News were abuzz with similar stories about the massive difference between Sonnet 4.5 and Opus 4.5, so something <em>did</em> change.</p>
<p>Obviously an API scraper and data viewer alone do not justify an <strong>OPUS 4.5 CHANGES EVERYTHING</strong> declaration on social media, but it&rsquo;s enough to be less cynical and more optimistic about agentic coding. It&rsquo;s an invitation to continue creating more difficult tasks for Opus 4.5 to solve. From this point going forward, I will also switch to the terminal Claude Code, since my pipeline is simple enough and doesn&rsquo;t warrant a UI or other shenanigans.</p>
<h2 id="getting-rusty-at-coding">Getting Rusty At Coding</h2>
<p>If you&rsquo;ve spent enough time on programming forums such as Hacker News, you&rsquo;ve probably seen the name &ldquo;Rust&rdquo;, often in the context of snark. <a href="https://rust-lang.org">Rust</a> is a relatively niche compiled programming language that touts two important features: speed, which is evident in <a href="https://www.techempower.com/benchmarks/#section=data-r23">framework benchmarks</a> where it can perform 10x as fast as the fastest Python library, and memory safety enforced at compile time through its ownership and borrowing systems which mitigates many potential problems. For over a decade, the slogan &ldquo;Rewrite it in Rust&rdquo; <a href="https://transitiontech.ca/random/RIIR">became a meme</a> where advocates argued that <em>everything</em> should be rewritten in Rust due to its benefits, including extremely mature software that&rsquo;s infeasible to actually rewrite in a different language. Even the major LLM companies are looking to Rust to eke out as much performance as possible: OpenAI President Greg Brockman <a href="https://x.com/gdb/status/2007228511363444905">recently tweeted</a> &ldquo;rust is a perfect language for agents, given that if it compiles it&rsquo;s ~correct&rdquo; which — albeit that statement is silly at a technical level since code can still be <em>logically</em> incorrect — shows that OpenAI is very interested in Rust, and if they&rsquo;re interested in writing Rust code, they need their LLMs to be able to code well in Rust.</p>
<p>I myself am not very proficient in Rust. Rust has a famously excellent <a href="https://rust-lang.org/learn/">interactive tutorial</a>, but a persistent issue with Rust is that there are few resources for those with intermediate knowledge: there&rsquo;s little between the tutorial and &ldquo;write an operating system from scratch.&rdquo; That was around 2020 and I decided to wait and see if the ecosystem corrected this point (in 2026 it has not), but I&rsquo;ve kept an eye on Hacker News for all the new Rust blog posts and library crates so that one day I too will be able to write the absolutely highest performing code possible.</p>
<p>Historically, LLMs have been poor at generating Rust code due to its nicheness relative to Python and JavaScript. Over the years, one of my test cases for evaluating new LLMs was to ask it to write a relatively simple application such as <code>Create a Rust app that can create &quot;word cloud&quot; data visualizations given a long input text.</code> but even without expert Rust knowledge I could tell the outputs were too simple and half-implemented to ever be functional even with additional prompting.</p>
<p>However, due to modern LLM postraining paradigms, it&rsquo;s entirely possible that newer LLMs are specifically RLHF-trained to write better code in Rust despite its relative scarcity. I ran more experiments with Opus 4.5 and using LLMs in Rust on some fun pet projects, and my results were <em>far</em> better than I expected. Here are four such projects:</p>
<h3 id="icon-to-image">icon-to-image</h3>
<p>As someone who primarily works in Python, what first caught my attention about Rust is the <a href="https://pyo3.rs/v0.28.2/">PyO3</a> crate: a crate that allows accessing Rust code through Python with all the speed and memory benefits that entails while the Python end-user is none-the-wiser. My first exposure to <code>pyo3</code> was the fast tokenizers in <a href="https://huggingface.co">Hugging Face</a> <a href="https://github.com/huggingface/tokenizers">tokenizers</a>, but many popular Python libraries now also use this pattern for speed, including <a href="https://github.com/ijl/orjson">orjson</a>, <a href="https://docs.pydantic.dev/latest/">pydantic</a>, and my favorite <a href="https://pola.rs">polars</a>. If agentic LLMs could now write both performant Rust code and leverage the <code>pyo3</code> bridge, that would be <em>extremely</em> useful for myself.</p>
<p>I decided to start with a very simple project: a project that can take icons from an icon font file such as the ones provided by <a href="https://fontawesome.com">Font Awesome</a> and render them into images at any arbitrary resolution.</p>
<figure>

    <img loading="lazy" srcset="/2026/02/ai-agent-coding/icons_header_hu_535677013aed241.webp 320w,/2026/02/ai-agent-coding/icons_header_hu_111233a5bbd61878.webp 768w,/2026/02/ai-agent-coding/icons_header_hu_5495e39cdc67a903.webp 1024w,/2026/02/ai-agent-coding/icons_header.webp 1536w" src="icons_header.webp"/> 
</figure>

<p>I made <a href="https://github.com/minimaxir/icon-image">this exact project</a> in Python in 2021, and it&rsquo;s very hacky by pulling together several packages and cannot easily be maintained. A better version in Rust with Python bindings is a good way to test Opus 4.5.</p>
<p>The very first thing I did was create a <code>AGENTS.md</code> for Rust by telling Opus 4.5 to port over the Python rules to Rust semantic equivalents. This worked well enough and had the standard Rust idioms: no <code>.clone()</code> to handle lifetimes poorly, no unnecessary <code>.unwrap()</code>, no <code>unsafe</code> code, etc. Although I am not a Rust expert and cannot speak that the agent-generated code is idiomatic Rust, none of the Rust code demoed in this blog post has traces of bad Rust code smell. Most importantly, the agent is instructed to call <a href="https://doc.rust-lang.org/stable/clippy/">clippy</a> after each major change, which is Rust&rsquo;s famous linter that helps keep the code clean, and Opus is good about implementing suggestions from its warnings. My up-to-date Rust <code>AGENTS.md</code> is available <a href="https://gist.github.com/minimaxir/068ef4137a1b6c1dcefa785349c91728">here</a>.</p>
<p>With that, I built a gigaprompt to ensure Opus 4.5 accounted for both the original Python implementation and a few new ideas I had, such as <a href="https://en.wikipedia.org/wiki/Supersampling">supersampling</a> to antialias the output.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl">Create a Rust/Python package (through <span class="sb">`pyo3`</span> and <span class="sb">`maturin`</span>) that efficiently and super-quickly takes an Icon Font and renders an image based on the specified icon. The icon fonts are present in <span class="sb">`assets`</span>, and the CSS file which maps the icon name to the corresponding reference in the icon font is in <span class="sb">`fontawesome.css`</span>.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">You MUST obey ALL the FOLLOWING implementation notes:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> If the icon name has <span class="sb">`solid`</span> in it, it is referencing <span class="sb">`fa-solid.otf`</span>.
</span></span><span class="line"><span class="cl"><span class="k">-</span> <span class="sb">`fa-brands.otf`</span> and <span class="sb">`fa-regular.otf`</span> can be combined.
</span></span><span class="line"><span class="cl"><span class="k">-</span> The package MUST also support Python (via <span class="sb">`pyo3`</span> and <span class="sb">`maturin`</span>).
</span></span><span class="line"><span class="cl"><span class="k">-</span> The package MUST be able to output the image rendered as an optimized PNG and WEBP. with a default output resolution of 1024 x 1024.
</span></span><span class="line"><span class="cl"><span class="k">-</span> The image rendering MUST support supersampling for antialiased text and points (2x by default)
</span></span><span class="line"><span class="cl"><span class="k">-</span> The package MUST implement <span class="sb">`fontdue`</span> as its text rendering method.
</span></span><span class="line"><span class="cl"><span class="k">-</span> Allow the user to specify the color of the icon and the color of the background (both hex and RGB)
</span></span><span class="line"><span class="cl"><span class="k">-</span> Allow transparent backgrounds.
</span></span><span class="line"><span class="cl"><span class="k">-</span> Allow user to specify the icon size and canvas size separately.
</span></span><span class="line"><span class="cl"><span class="k">-</span> Allow user to specify the anchor positions (horizontal and vertical) for the icon relative to the canvas (default: center and center)
</span></span><span class="line"><span class="cl"><span class="k">-</span> Allow users to specify a horizontal and vertical pixel offset for the icon relative to the canvas.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">After your base implementation is complete, you MUST:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> Write a comprehensive Python test suite using <span class="sb">`pytest`</span>.
</span></span><span class="line"><span class="cl"><span class="k">-</span> Write a Python Jupyter Notebook
</span></span><span class="line"><span class="cl"><span class="k">-</span> Optimize the Rust binary file size and the Python package file size.
</span></span></code></pre></div><p>It completed the assignment in one-shot, accounting for all of the many feature constraints specified. The &ldquo;Python Jupyter Notebook&rdquo; notebook command at the end is how I manually tested whether the <code>pyo3</code> bridge worked, and it indeed worked like a charm. There was one mistake that&rsquo;s my fault however: I naively chose the <a href="https://github.com/mooman219/fontdue">fontdue</a> Rust crate as the renderer because I remember <a href="https://github.com/mooman219/fontdue?tab=readme-ov-file#performance">seeing a benchmark</a> showing it was the fastest at text rendering. However, testing large icon generation exposed a flaw: <code>fontdue</code> achieves its speed by only partially rendering curves, which is a very big problem for icons, so I followed up:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl">The generated icons, at a high resolution, show signs of not having curves and instead showing discrete edges (image attached). Investigate the <span class="sb">`fontdue`</span> font renderer to see if there&#39;s an issue there.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">In the event that it&#39;s not possible to fix this in <span class="sb">`fontdue`</span>, investigate using <span class="sb">`ab_glyph`</span> instead.
</span></span></code></pre></div><p>Opus 4.5 used its Web Search tool to confirm the issue is expected with <code>fontdue</code> and implemented <a href="https://crates.io/crates/ab_glyph">ab_glyph</a> instead which did fix the curves.</p>
<p>icon-to-image is available <a href="https://github.com/minimaxir/icon-to-image">open-source on GitHub</a>. There were around 10 prompts total adding tweaks and polish, but through all of them Opus 4.5 never failed the assignment as written. Of course, generating icon images in Rust-with-Python-bindings is an order of magnitude faster than my old hacky method, and thanks to the better text rendering and supersampling it also looks much better than the Python equivalent.</p>
<p>There&rsquo;s a secondary pro and con to this pipeline: since the code is compiled, it avoids having to specify as many dependencies in Python itself; in this package&rsquo;s case, Pillow for image manipulation in Python is optional and the Python package won&rsquo;t break if Pillow changes its API. The con is that compiling the Rust code into Python wheels is difficult to automate especially for multiple OS targets: fortunately, GitHub provides <a href="https://docs.github.com/en/actions/concepts/runners/github-hosted-runners">runner VMs</a> for this pipeline and a little bit of back-and-forth with Opus 4.5 created <a href="https://github.com/minimaxir/icon-to-image/blob/main/.github/workflows/release.yml">a GitHub Workflow</a> which runs the build for all target OSes on publish, so there&rsquo;s no extra effort needed on my end.</p>
<h3 id="word-clouds-in-the-browser">Word Clouds In The Browser</h3>
<p>When I used word clouds in Rust as my test case for LLM Rust knowledge, I had an ulterior motive: I <em>love</em> word clouds. Back in 2019, I open-sourced a Python package titled <a href="https://github.com/minimaxir/stylecloud">stylecloud</a>: a package built on top of Python&rsquo;s word cloud, but with the added ability to add more color gradients and masks based on icons to easily conform it into shapes (sound familiar?)</p>
<figure>

    <img loading="lazy" srcset="/2026/02/ai-agent-coding/stylecloud_banner_hu_7b1ba00b8637a928.webp 320w,/2026/02/ai-agent-coding/stylecloud_banner_hu_e34a5b1f7e15eb9.webp 768w,/2026/02/ai-agent-coding/stylecloud_banner.png 768w" src="stylecloud_banner.png"/> 
</figure>

<p>However, stylecloud was hacky and fragile, and a number of features I wanted to add such as non-90-degree word rotation, transparent backgrounds, and SVG output flat-out were not possible to add due to its dependency on Python&rsquo;s <a href="https://github.com/amueller/word_cloud">wordcloud</a>/<a href="https://matplotlib.org">matplotlib</a>, and also the package was really slow. The only way to add the features I wanted was to build something from scratch: Rust fit the bill.</p>
<p>The pipeline was very similar to <code>icon-to-image</code> above: ask Opus 4.5 to fulfill a long list of constraints with the addition of Python bindings. But there&rsquo;s another thing that I wanted to test that would be extremely useful if it worked: WebAssembly (WASM) output with <a href="https://crates.io/crates/wasm-bindgen">wasm-bindgen</a>. Rust code compiled to WASM allows it to be run in any modern web browser with the speed benefits intact: no dependencies needed, and therefore should be future-proof. However, there&rsquo;s a problem: I would have to design an interface and I am not a front end person, and I say without hyperbole that for me, designing even a simple HTML/CSS/JS front end for a project is more stressful than training an AI. However, Opus 4.5 is able to take general guidelines and get it into something workable: I first told it to use Pico CSS and vanilla JavaScript and that was enough, but then I had an idea to tell it to use <a href="https://ui.shadcn.com">shadcn/ui</a> — a minimalistic design framework normally reserved for Web Components — along with screenshots from that website as examples. That also worked.</p>
<figure>

    <img loading="lazy" srcset="/2026/02/ai-agent-coding/wordcloud_rust_ui_hu_d89a5fdfc340adda.webp 320w,/2026/02/ai-agent-coding/wordcloud_rust_ui_hu_32bf6094abc7a9dc.webp 768w,/2026/02/ai-agent-coding/wordcloud_rust_ui_hu_9eabb4297ecaf812.webp 1024w,/2026/02/ai-agent-coding/wordcloud_rust_ui.webp 1251w" src="wordcloud_rust_ui.webp"/> 
</figure>

<p>After more back-and-forth with design nitpicks and more features to add, the package is feature complete. However, it needs some more polish and a more unique design before I can release it, and I got sidetracked by <em>something</em> more impactful&hellip;</p>
<h3 id="miditui">miditui</h3>
<p><code>Create a music player in the terminal using Rust</code> was another Rust stress test I gave to LLMs: command line terminals can&rsquo;t play audio, right? Turns out, it can with the <a href="https://crates.io/crates/rodio">rodio</a> crate. Given the success so far with Opus 4.5 I decided to make the tasks more difficult: terminals can play sound, but can it <em>compose</em> sound? So I asked Opus 4.5 to create a MIDI composer and playback DAW within a terminal, which worked. Adding features forced me to learn more about how MIDIs and <a href="https://en.wikipedia.org/wiki/SoundFont">SoundFonts</a> actually work, so it was also educational!</p>
<figure>

    <img loading="lazy" srcset="/2026/02/ai-agent-coding/miditui_hu_1810d138c3702778.webp 320w,/2026/02/ai-agent-coding/miditui_hu_e13017cd0287782e.webp 768w,/2026/02/ai-agent-coding/miditui_hu_ddae22b14b865cdf.webp 1024w,/2026/02/ai-agent-coding/miditui.webp 1582w" src="miditui.webp"/> 
</figure>

<p>miditui is available <a href="https://github.com/minimaxir/miditui">open-sourced on GitHub</a>, and the prompts used to build it are <a href="https://github.com/minimaxir/miditui/blob/main/agent_notes/PROMPTS.md">here</a>.</p>
<p>During development I encountered a caveat: Opus 4.5 can&rsquo;t test or view a terminal output, especially one with unusual functional requirements. But despite being blind, it knew enough about the <a href="https://ratatui.rs">ratatui</a> terminal framework to implement whatever UI changes I asked. There were a large number of UI bugs that likely were caused by Opus&rsquo;s inability to create test cases, namely failures to account for scroll offsets resulting in incorrect click locations. As someone who spent 5 years as a <a href="https://en.wikipedia.org/wiki/Black_box">black box</a> Software QA Engineer who was unable to review the underlying code, this situation was my specialty. I put my QA skills to work by messing around with <code>miditui</code>, told Opus any errors with occasionally a screenshot, and it was able to fix them easily. I do not believe that these bugs are inherently due to LLM agents being better or worse than humans as humans are most definitely capable of making the same mistakes. Even though I myself am adept at finding the bugs and offering solutions, I don&rsquo;t believe that I would inherently avoid causing similar bugs were I to code such an interactive app without AI assistance: QA brain is different from software engineering brain.</p>
<h3 id="ballin">ballin</h3>
<p>One night — after a glass of wine — I had another idea: one modern trick with <a href="https://en.wikipedia.org/wiki/ASCII_art">ASCII art</a> is the use of <a href="https://www.unicode.org/charts/nameslist/c_2800.html">Braille unicode characters</a> to allow for <a href="https://steamcommunity.com/sharedfiles/filedetails/?id=2807089604">very high detail</a>. That reminded me of ball physics simulations, so what about building a full physics simulator also in the terminal? So I asked Opus 4.5 to create a terminal physics simulator with the <a href="https://rapier.rs">rapier</a> 2D physics engine and a detailed explanation of the Braille character trick: this time Opus did better and completed it in one-shot, so I spent more time making it colorful and <em>fun</em>. I pessimistically thought the engine would only be able to handle a few hundred balls: instead, the Rust codebase can handle over 10,000 logical balls!</p>
<figure>

    <img loading="lazy" srcset="/2026/02/ai-agent-coding/ballin_hu_5dd8a55c77035491.webp 320w,/2026/02/ai-agent-coding/ballin_hu_f7df7c2ac2073cf9.webp 768w,/2026/02/ai-agent-coding/ballin_hu_37a706f42d6228a6.webp 1024w,/2026/02/ai-agent-coding/ballin.webp 1909w" src="ballin.webp"
         alt="I explicitly prompted Opus to make the Colors button have a different color for each letter."/> <figcaption>
            <p>I explicitly prompted Opus to make the Colors button have a different color for each letter.</p>
        </figcaption>
</figure>

<p>ballin is available <a href="https://github.com/minimaxir/ballin">open-sourced on GitHub</a>, and the prompts used to build it are <a href="https://github.com/minimaxir/ballin/blob/main/PROMPTS.md">here</a>.</p>
<p>The <code>rapier</code> crate also published a blog post highlighting a <a href="https://dimforge.com/blog/2026/01/09/the-year-2025-in-dimforge">major change to its underlying math engine</a>, in its 0.32.0 version so I asked Opus 4.5 to upgrade to that version&hellip;and it caused crashes, yet tracing the errors showed it originated with <code>rapier</code> itself. Upgrading to 0.31.0 was fine with no issues: a consequence of only using agentic coding for this workflow is that I cannot construct a minimal reproducible test case to file as a regression bug report or be able to isolate it as a side effect of a new API not well-known by Opus 4.5.</p>
<p>The main lesson I learnt from working on these projects is that agents work best when you have <a href="https://www.youtube.com/watch?v=W9_iQ1FSnp8">approximate knowledge of many things</a> with enough domain expertise to know what should and should not work. Opus 4.5 is good enough to let me finally do side projects where I know precisely what I want but not necessarily how to implement it. These specific projects aren&rsquo;t the Next Big Thing™ that justifies the existence of an industry taking billions of dollars in venture capital, but they make my life better and since they are open-sourced, hopefully they make someone else&rsquo;s life better. However, I still wanted to push agents to do more impactful things in an area that might be more worth it.</p>
<h2 id="its-not-ai-psychosis-if-it-works">It&rsquo;s Not AI Psychosis If It Works</h2>
<p>Before I wrote my blog post about how I use LLMs, I wrote a tongue-in-cheek blog post titled <a href="https://minimaxir.com/2025/01/write-better-code/">Can LLMs write better code if you keep asking them to “write better code”?</a> which is exactly as the name suggests. It was an experiment to determine how LLMs interpret the ambiguous command &ldquo;write better code&rdquo;: in this case, it was to prioritize making the code more convoluted with more helpful features, but if instead given commands to optimize the code, it did make the code faster successfully albeit at the cost of significant readability. In software engineering, one of the greatest sins is <a href="https://stackify.com/premature-optimization-evil/">premature optimization</a>, where you sacrifice code readability and thus maintainability to chase performance gains that slow down development time and may not be worth it. Buuuuuuut with agentic coding, we implicitly accept that our interpretation of the code is fuzzy: could agents iteratively applying optimizations for the sole purpose of minimizing benchmark runtime — and therefore faster code in typical use cases if said benchmarks are representative — now actually be a good idea? People complain about how AI-generated code is slow, but if AI can now reliably generate <em>fast</em> code, that changes the debate.</p>
<figure>

    <img loading="lazy" srcset="/2026/02/ai-agent-coding/div255_hu_fede5dfdf9da043c.webp 320w,/2026/02/ai-agent-coding/div255_hu_9580dbba4bb4392a.webp 768w,/2026/02/ai-agent-coding/div255_hu_f1422dc2ad5bbb29.webp 1024w,/2026/02/ai-agent-coding/div255.png 1104w" src="div255.png"
         alt="Multiplication and division are too slow for Opus 4.6."/> <figcaption>
            <p>Multiplication and division are too slow for Opus 4.6.</p>
        </figcaption>
</figure>

<p>As a data scientist, I&rsquo;ve been frustrated that there haven&rsquo;t been any impactful new Python data science tools released in the past few years other than <code>polars</code>. Unsurprisingly, research into AI and LLMs has subsumed traditional DS research, where developments such as text embeddings have had <a href="https://minimaxir.com/2025/02/embeddings-parquet/">extremely valuable gains</a> for typical data science natural language processing tasks. The traditional machine learning algorithms are still valuable, but no one has invented <a href="https://developers.google.com/machine-learning/decision-forests/intro-to-gbdt">Gradient Boosted Decision Trees</a> 2: Electric Boogaloo. Additionally, as a data scientist in San Francisco I am legally required to use a MacBook, but there haven&rsquo;t been data science utilities that actually use the GPU in an Apple Silicon MacBook as they don&rsquo;t support its Metal API; data science tooling is exclusively in CUDA for NVIDIA GPUs. What if agents could now port these algorithms to a) run on Rust with Python bindings for its speed benefits and b) run on GPUs without complex dependencies?</p>
<p>This month, OpenAI announced their <a href="https://openai.com/index/introducing-the-codex-app/">Codex app</a> and my coworkers were asking questions. So I downloaded it, and as a test case for the GPT-5.2-Codex (high) model, I asked it to reimplement the <a href="https://umap-learn.readthedocs.io/en/latest/">UMAP algorithm</a> in Rust. UMAP is a dimensionality reduction technique that can take in a high-dimensional matrix of data and simultaneously cluster and visualize data in lower dimensions. However, it is a very computationally-intensive algorithm and the only tool that can do it quickly is NVIDIA&rsquo;s <a href="https://github.com/rapidsai/cuml">cuML</a> which requires CUDA dependency hell. If I can create a UMAP package in Rust that&rsquo;s superfast with minimal dependencies, that is an <em>massive</em> productivity gain for the type of work I do and can enable fun applications if fast enough.</p>
<p>After OpenAI <a href="https://openai.com/index/introducing-gpt-5-3-codex/">released</a> GPT-5.3-Codex (high) which performed substantially better and faster at these types of tasks than GPT-5.2-Codex, I asked Codex to write a UMAP implementation from scratch in Rust, which at a glance seemed to work and gave reasonable results. I also instructed it to create benchmarks that test a wide variety of representative input matrix sizes. Rust has a popular benchmarking crate in <a href="https://crates.io/crates/criterion">criterion</a>, which outputs the benchmark results in an easy-to-read format, which, most importantly, agents can easily parse.</p>
<figure>

    <img loading="lazy" srcset="/2026/02/ai-agent-coding/criterion_hu_29440b9b440b97ea.webp 320w,/2026/02/ai-agent-coding/criterion_hu_3835e7f90db1f611.webp 768w,/2026/02/ai-agent-coding/criterion_hu_c07d0baf8af59328.webp 1024w,/2026/02/ai-agent-coding/criterion.png 1300w" src="criterion.png"
         alt="Example output from criterion."/> <figcaption>
            <p>Example output from <code>criterion</code>.</p>
        </figcaption>
</figure>

<p>At first glance, the benchmarks and their construction looked good (i.e. no cheating) and are much faster than working with UMAP in Python. To further test, I asked the agents to implement additional different useful machine learning algorithms such as HDBSCAN as individual projects, with each repo starting with this 8 prompt plan in sequence:</p>
<ol>
<li>Implement the package with the specific functional requirements and design goals; afterwards, create benchmarks with specific matrix sizes that are representative of typical use cases</li>
<li>Do a second pass to clean up the code/comments and make further optimizations</li>
<li>Scan the crate to find areas of algorithmic weaknesses in extreme cases, and write a sentence for each describing the problem, the potential solution, and quantifying the impact of the solution</li>
<li>Leveraging the findings found, optimize the crate such that ALL benchmarks run 60% or quicker (1.4x faster). Use any techniques to do so, and repeat until benchmark performance converges, but don&rsquo;t game the benchmarks by overfitting on the benchmark inputs alone <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></li>
<li>Create custom tuning profiles that take advantage of the inherent quantities of the input data and CPU thread saturation/scheduling/parallelization to optimize the crate such that ALL benchmarks run 60% or quicker (1.4x faster). You can use the <a href="https://crates.io/crates/flamegraph">flamegraph</a> crate to help with the profiling</li>
<li>Add Python bindings using <code>pyo3</code> 0.27.2 and <code>maturin</code>, with relevant package-specific constraints (specifying the <code>pyo3</code> version is necessary to ensure compatability with Python 3.10+)</li>
<li>Create corresponding benchmarks in Python, and write a comparison script between the Python bindings and an existing Python package</li>
<li>Accuse the agent of potentially cheating its algorithm implementation while pursuing its optimizations, so tell it to optimize for the similarity of outputs against a known good implementation (e.g. for a regression task, minimize the <a href="https://en.wikipedia.org/wiki/Mean_absolute_error">mean absolute error</a> in predictions between the two approaches)</li>
</ol>
<p>The simultaneous constraints of code quality requirements via <code>AGENTS.md</code>, speed requirements with a quantifiable target objective, and an output accuracy/quality requirement, all do succeed at finding meaningful speedups consistently (atleast 2x-3x)</p>
<figure>

    <img loading="lazy" srcset="/2026/02/ai-agent-coding/pca_benchmark_codex_hu_89818e863160d0c7.webp 320w,/2026/02/ai-agent-coding/pca_benchmark_codex_hu_1cda151be1d34818.webp 768w,/2026/02/ai-agent-coding/pca_benchmark_codex_hu_c3c6231b591a4dd0.webp 1024w,/2026/02/ai-agent-coding/pca_benchmark_codex.png 1366w" src="pca_benchmark_codex.png"
         alt="Codex 5.3 after optimizing a principal component analysis implementation."/> <figcaption>
            <p>Codex 5.3 after optimizing a <a href="https://en.wikipedia.org/wiki/Principal_component_analysis">principal component analysis</a> implementation.</p>
        </figcaption>
</figure>

<p>I&rsquo;m not content with only 2-3x speedups: nowadays in order for this agentic code to be meaningful and not just another repo on GitHub, it has to be the <em>fastest implementation possible</em>. In a moment of sarcastic curiosity, I tried to see if Codex and Opus had different approaches to optimizing Rust code by chaining them:</p>
<ol>
<li>Instruct Codex to optimize benchmarks to 60% of runtime</li>
<li>Instruct Opus to optimize benchmarks to 60% of runtime</li>
<li>Instruct Opus to minimize differences between agentic implementation and known good implementation without causing more than a 5% speed regression on any benchmarks</li>
</ol>
<p><em>This works</em>. From my tests with the algorithms, Codex can often speed up the algorithm by 1.5x-2x, then Opus somehow speeds up that optimized code <em>again</em> to a greater degree. This has been the case of all the Rust code I&rsquo;ve tested: I also ran the <code>icon-to-image</code> and the word cloud crates through this pipeline and gained 6x cumulative speed increases in both libraries.</p>
<p>Can these agent-benchmaxxed implementations actually beat the existing machine learning algorithm libraries, despite those libraries already being written in a low-level language such as C/C++/Fortran? Here are the results on my personal MacBook Pro comparing the CPU benchmarks of the Rust implementations of various computationally intensive ML algorithms to their respective popular implementations, where the agentic Rust results are within similarity tolerance with the battle-tested implementations and Python packages are compared against the Python bindings of the agent-coded Rust packages:</p>
<ul>
<li>UMAP: 2-10x faster than Rust&rsquo;s <a href="https://crates.io/crates/fast-umap">fast-umap</a>, 9-30x faster than Python&rsquo;s <a href="https://umap-learn.readthedocs.io/en/latest/">umap</a></li>
<li>HDBSCAN (clustering algorithm): 23-100x faster than the <a href="https://crates.io/crates/hdbscan">hdbscan</a> Rust crate, 3x-10x faster than Python&rsquo;s <a href="https://pypi.org/project/hdbscan/">hdbscan</a></li>
<li>GBDT (tree-boosting algorithm): 1.1x-1.5x faster fit/predict than the <a href="https://crates.io/crates/treeboost">treeboost</a> Rust crate<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>, 24-42x faster fit/1-5x faster predict than Python&rsquo;s <a href="https://xgboost.readthedocs.io/en/stable/index.html">xgboost</a></li>
</ul>
<p>I&rsquo;ll definitely take those results with this unoptimized prompting pipeline! In all cases, the GPU benchmarks are unsurprisingly even better and with <a href="https://crates.io/crates/wgpu">wgpu</a> and added WGSL shaders the code runs on Metal without any additional dependencies, however further testing is needed so I can&rsquo;t report numbers just yet.</p>
<p>Although I could push these new libraries to GitHub now, machine learning algorithms are understandably a domain which requires extra care and testing. It would be arrogant to port Python&rsquo;s <a href="https://scikit-learn.org/stable/">scikit-learn</a> — the gold standard of data science and machine learning libraries — to Rust with all the features that implies.</p>
<p>But that&rsquo;s unironically a good idea so I decided to try and do it anyways. With the use of agents, I am now developing <code>rustlearn</code> (extreme placeholder name), a Rust crate that implements not only the fast implementations of the standard machine learning algorithms such as <a href="https://en.wikipedia.org/wiki/Logistic_regression">logistic regression</a> and <a href="https://en.wikipedia.org/wiki/K-means_clustering">k-means clustering</a>, but also includes the fast implementations of the algorithms above: the same three step pipeline I describe above still works even with the more simple algorithms to beat scikit-learn&rsquo;s implementations. This crate can therefore receive Python bindings and even expand to the Web/JavaScript and beyond. This also gives me the oppertunity to add quality-of-life features to resolve grievances I&rsquo;ve had to work around as a data scientist, such as model serialization and native integration with pandas/polars DataFrames. I hope this use case is considered to be more practical and complex than making a ball physics terminal app.</p>
<p>Many people reading this will call bullshit on the performance improvement metrics, and honestly, fair. I too thought the agents would stumble in hilarious ways trying, but they did not. To demonstrate that I am not bullshitting, I also decided to release a more simple Rust-with-Python-bindings project today: nndex, an in-memory vector &ldquo;store&rdquo; that is designed to retrieve the exact nearest neighbors as fast as possible (and has fast approximate NN too), and is now available <a href="https://github.com/minimaxir/nndex">open-sourced on GitHub</a>. This leverages the <a href="https://en.wikipedia.org/wiki/Dot_product">dot product</a> which is one of the simplest matrix ops and is therefore heavily optimized by existing libraries such as Python&rsquo;s <a href="https://numpy.org">numpy</a>&hellip;and yet after a few optimization passes, it tied <code>numpy</code> even though <code>numpy</code> leverages <a href="https://en.wikipedia.org/wiki/Basic_Linear_Algebra_Subprograms">BLAS</a> libraries for maximum mathematical performance. Naturally, I instructed Opus to also add support for BLAS with more optimization passes and it now is 1-5x numpy&rsquo;s speed in the single-query case and much faster with batch prediction. <sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup> It&rsquo;s so fast that even though I also added GPU support for testing, it&rsquo;s mostly ineffective below 100k rows due to the GPU dispatch overhead being greater than the actual retrieval speed.</p>
<figure>

    <img loading="lazy" srcset="/2026/02/ai-agent-coding/nndex_hu_37580e348a0481f6.webp 320w,/2026/02/ai-agent-coding/nndex_hu_46b261ee60d7142f.webp 768w,/2026/02/ai-agent-coding/nndex_hu_95b38eb803cac099.webp 1024w,/2026/02/ai-agent-coding/nndex.png 1564w" src="nndex.png"
         alt="Comparison of Python nndex to numpy on test workloads.topk_overlap measures result matches (perfect match) and max_similarity_abs_delta measure the largest difference between calculated cosine similarities (effectively zero)."/> <figcaption>
            <p>Comparison of Python <code>nndex</code> to numpy on test workloads.<code>topk_overlap</code> measures result matches (perfect match) and <code>max_similarity_abs_delta</code> measure the largest difference between calculated cosine similarities (effectively zero).</p>
        </figcaption>
</figure>

<p>One of the criticisms about AI generated code is that it &ldquo;just regurgitates everything on GitHub&rdquo; but by construction, if the code is faster than what currently exists, then it can&rsquo;t have been stolen and must be an original approach. Even if the explicit agentic nature of <code>rustlearn</code> makes it risky to adopt downstream, the learnings from how it accomplishes its extreme speed are still valuable.</p>
<h2 id="the-implications-of-my-agentic-successes">The Implications of My Agentic Successes</h2>
<p>Like many who have hopped onto the agent train post-Opus 4.5, I&rsquo;ve become nihilistic over the past few months, but not for the typical reasons. I actually am not hitting burnout and I am not worried that my programming skills are decaying due to agents: on the contrary, the session limits intended to stagger server usage have unintentionally caused me to form a habit of coding for fun an hour every day incorporating and implementing new ideas. However, is there a <em>point</em> to me writing this blog post and working on these libraries if people will likely just reply &ldquo;tl;dr AI slop&rdquo; and &ldquo;it&rsquo;s vibecoded so it&rsquo;s automatically bad&rdquo;?</p>
<p>The real annoying thing about Opus 4.6/Codex 5.3 is that it&rsquo;s impossible to publicly say &ldquo;Opus 4.5 (and the models that came after it) are an order of magnitude better than coding LLMs released just months before it&rdquo; without sounding like an AI hype booster clickbaiting, but it&rsquo;s the counterintuitive truth to my personal frustration. I have been trying to break this damn model by giving it complex tasks that would take me months to do by myself despite my coding pedigree but Opus and Codex keep doing them correctly. On Hacker News I was <a href="https://news.ycombinator.com/item?id=46979055">accused of said clickbaiting</a> when making a similar statement with accusations of &ldquo;I haven&rsquo;t had success with Opus 4.5 so you must be lying.&rdquo; The remedy to this skepticism is to provide more evidence in addition to greater checks and balances, but what can you do if people refuse to believe your evidence?</p>
<p>A year ago, I was one of those skeptics who was very suspicious of the agentic hype, but I was willing to change my priors in light of new evidence and experiences, which apparently is rare. Generative AI discourse has become too toxic and its discussions always end the same way, so I have been experimenting with touching grass instead, and it is nice. At this point, if I&rsquo;m not confident that I can please anyone with my use of AI, then I&rsquo;ll take solace in just pleasing myself. Continue open sourcing my projects, writing blog posts, and let the pieces fall as they may. If you want to follow along or learn when <code>rustlearn</code> releases, you can follow me <a href="https://bsky.app/profile/minimaxir.bsky.social">on Bluesky</a>.</p>
<p>Moment of introspection aside, I&rsquo;m not sure what the future holds for agents and generative AI. My use of agents has proven to have significant utility (for myself at the least) and I have more-than-enough high-impact projects in the pipeline to occupy me for a few months. Although certainly I will use LLMs more for coding apps which benefit from this optimization, that doesn&rsquo;t imply I will use LLMs more elsewhere: I still don&rsquo;t use LLMs for writing — in fact I have intentionally made my writing voice more sardonic to specifically fend off AI accusations.</p>
<p>With respect to Rust, working with agents and seeing how the agents make decisions/diffs has actually helped me break out of the intermediate Rust slog and taught me a lot about the ecosystem by taking on more ambitious projects that required me to research and identify effective tools for modern Rust development. Even though I have <em>technically</em> released Rust packages with many stars on GitHub, I have no intention of putting Rust as a professional skill on my LinkedIn or my résumé. As an aside, how exactly do résumés work in an agentic coding world? Would &ldquo;wrote many open-source libraries through the use of agentic LLMs which increased the throughput of popular data science/machine learning algorithms by an order of magnitude&rdquo; be disqualifying to a prospective employer as they may think I&rsquo;m cheating and faking my expertise?</p>
<p>My obligation as a professional coder is to do what works best, especially for open source code that other people will use. Agents are another tool in that toolbox with their own pros and cons. If you&rsquo;ve had poor experiences with agents before last November, I strongly urge you to give modern agents another shot, especially with an <code>AGENTS.md</code> tailored to your specific coding domain and nuances (again here are my <a href="https://gist.githubusercontent.com/minimaxir/10b780671ee5d695b4369b987413b38f/raw/f06ad4f1430a8d9f268b160a755dab817384c93c/AGENTS.md">Python</a> and <a href="https://gist.githubusercontent.com/minimaxir/068ef4137a1b6c1dcefa785349c91728/raw/0fa5d1b505338b3a2c6834cc41e728cefe57511b/AGENTS.md">Rust</a> files, in conveient copy/paste format).</p>
<p>Overall, I&rsquo;m very sad at the state of agentic discourse but also very excited at its promise: it&rsquo;s currently unclear which one is the stronger emotion.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Two subtle ways agents can implicitly negatively affect the benchmark results but wouldn&rsquo;t be considered cheating/gaming it are a) implementing a form of caching so the benchmark tests are not independent and b) launching benchmarks in parallel on the same system. I eventually added <code>AGENTS.md</code> rules to ideally prevent both.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>The <code>treeboost</code> crate beat the agent-optimized GBT crate by 4x on my first comparison test, which naturally I took offense: I asked Opus 4.6 to &ldquo;Optimize the crate such that <code>rust_gbt</code> wins in ALL benchmarks against <code>treeboost</code>.&rdquo; and it did just that.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p>Currently, only the macOS build has BLAS support as Win/Linux BLAS support is a rabbit hole that needs more time to investigate. On those platforms, numpy does win, but that won&rsquo;t be the case for long!&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded>
    </item>
    <item>
      <title>Please Don&#39;t Ask if an Open Source Project is Dead</title>
      <link>https://minimaxir.com/2023/11/open-source-dead-github/</link>
      <pubDate>Tue, 14 Nov 2023 08:45:00 -0800</pubDate>
      <guid>https://minimaxir.com/2023/11/open-source-dead-github/</guid>
      <description>The best-case scenario is that you annoy the maintainers.</description>
      <content:encoded><![CDATA[<p>Over the past few months, I&rsquo;ve had an <a href="https://minimaxir.com/2023/10/ai-sturgeons-law/">existential crisis</a> about <a href="https://github.com/minimaxir">my work</a> in open source AI on <a href="https://github.com">GitHub</a>, particularly as there has been both increasingly toxic backlash against AI and because the AI industry has been evolving so rapidly that I flat-out don&rsquo;t have enough bandwidth to keep up. I took a break from working on my projects during that time, which <em>should</em> have been fine. One of my latest open source projects is <a href="https://github.com/minimaxir/simpleaichat">simpleaichat</a>, a Python package with 3k GitHub Stars for interfacing with <a href="https://chat.openai.com">ChatGPT</a>, and it was explicitly designed with limited scope and minimal dependencies so that I could take a break from development without my code&hellip;breaking.</p>
<p>After I was in a good place mentally to resume my open source work, I glanced at the GitHub Issues for simpleaichat and someone filed a issue simply titled &ldquo;has this been abandoned?&rdquo; with another GitHub user following up with &ldquo;With all due respect, I am also interested in the answer.&rdquo;</p>
<p>What the hell? I panicked and checked if there was a new breaking issue or dependency and there weren&rsquo;t any.</p>
<p>Two days later, someone else filed another issue: &ldquo;Is this package still in ongoing development?&rdquo;:</p>
<figure>

    <img loading="lazy" srcset="/2023/11/open-source-dead-github/github_hu_c217d25d3ea425f.webp 320w,/2023/11/open-source-dead-github/github_hu_54b502d5896b91e8.webp 768w,/2023/11/open-source-dead-github/github_hu_68c5c291f2dbca36.webp 1024w,/2023/11/open-source-dead-github/github.webp 1680w" src="github.webp"/> 
</figure>

<p>To be perfectly clear, this absolutely is applying pressure and being rude.</p>
<h2 id="the-expectations-of-open-source-software-development">The Expectations of Open Source Software Development</h2>
<p>I&rsquo;ve never seen any discussions or articles about whether it&rsquo;s appropriate to ask if an open source repository is dead. Is there an implicit contract to actively maintain any open source software you publish? Are you obligated to provide free support if you hit a certain star amount on GitHub or ask for funding through GitHub Sponsorships/Patreon? After all, most permissive open source code licenses like the <a href="https://en.wikipedia.org/wiki/MIT_License">MIT License</a> contain some variant of &ldquo;the software is provided &lsquo;as is&rsquo;, without warranty of any kind.&rdquo;</p>
<p>simpleaichat regretfully isn&rsquo;t my first open source project with complaints like this. The <a href="https://github.com/minimaxir/big-list-of-naughty-strings">Big List of Naughty Strings</a> to track adversarial user-input text strings, which I pushed to GitHub about a decade ago, is essentially just a <code>txt</code> <a href="https://github.com/minimaxir/big-list-of-naughty-strings/blob/master/blns.txt">file</a> with 45k GitHub Stars. There will never be dependency issues, and additions to the list that don&rsquo;t target a distinct string issue may clutter the list more than it already is so I&rsquo;m hesitant to accept every pull request. But despite that, people are angry.</p>
<figure>

    <img loading="lazy" srcset="/2023/11/open-source-dead-github/blns_hu_a495e97171a8cbd6.webp 320w,/2023/11/open-source-dead-github/blns_hu_13c59ba2feb4dd51.webp 768w,/2023/11/open-source-dead-github/blns_hu_b719603c68ede158.webp 1024w,/2023/11/open-source-dead-github/blns.webp 1454w" src="blns.webp"
         alt="The duality of comment reactions."/> <figcaption>
            <p>The duality of comment reactions.</p>
        </figcaption>
</figure>

<p>Some seem to think that there&rsquo;s such a thing as GitHub Issue-zero or pull request-zero, which like <a href="https://www.techtarget.com/whatis/definition/inbox-zero">inbox-zero</a> is infeasible in practice due to the realities of professional life. <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> Every nontrivial open source project will have an issue/PR queue, which necessitates a triage priority: not all issues and PRs are equal and it takes time and care to sift through the queue. That&rsquo;s something I&rsquo;ve had to repeatedly learn the hard way as a maintainer since accepting a misguided PR will create <a href="https://en.wikipedia.org/wiki/Technical_debt">technical debt</a> and take even more effort to address.</p>
<p>I get that it&rsquo;s a bummer to come across a cool GitHub project that hasn&rsquo;t been updated in awhile. That happens to me all the time. If the code still works, that&rsquo;s excellent and I&rsquo;m happy. But if it doesn&rsquo;t, I move on, or use it as a fun new opportunity to hack it to my needs. That&rsquo;s the beauty of open source! If there&rsquo;s an inactive open source project that&rsquo;s absolutely critical for your own commercial project, then that&rsquo;s a good financial reason to offer a consulting contract or a bounty to add the appropriate functionality.</p>
<p>One of the great things about open source is that if an open source project with a permissive license does become inactive, it can be <a href="https://docs.github.com/en/get-started/quickstart/fork-a-repo">forked</a> seamlessly. Sometimes the fork can become even better than the original project, which is great for everyone! But in my experience, it&rsquo;s instead used as a <em>threat</em>. And it&rsquo;s the maintainer&rsquo;s fault for creating a reason for a fork to be made and fragment the development community.</p>
<p>The AI industry is unique because it is indeed moving and evolving so fast that development expectations have shifted. Recent beneficiaries of the ChatGPT boon such as <a href="https://github.com/langchain-ai/langchain">LangChain</a>, <a href="https://github.com/run-llama/llama_index">LlamaIndex</a>, and <a href="https://github.com/Significant-Gravitas/AutoGPT">AutoGPT</a> have created a false sense that open source AI projects have to <strong>always be shipping</strong> 🚀🚀🚀. The difference is that they are maintained by those who do it as their full-time job and are now managed as companies backed by significant amounts of venture capital.</p>
<p>The pressure to continually provide support for an open source project has become the biggest deterrent for me to continue my open source work. Personally, I&rsquo;ve stopped pushing fun one-shot projects and AI models because I likely will not have the bandwidth to handle the inevitable &ldquo;hi this is broken plz fix thx&rdquo; DMs whenever a dependency on the project breaks years later. I&rsquo;d gladly quit my professional job as a Data Scientist to work on my open source projects full-time if I was able to make an equivalent salary by doing so. Ultimately, the only way to make it work nowadays would be to raise venture capital like all those AI startups.</p>
<p>The best-case scenario for asking if an open source project is dead is that you annoy the maintainers and delay development. The <em>worst</em>-case scenario is that you give the maintainers an opportunity to reconsider if continuing to work on the open source project is worth it.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Funny true story: a match on a dating app once asked to see my open source projects, and after I sent a link to one of my repos, she replied with a picture of the number of opened GitHub Issues and a 😱 emoji.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded>
    </item>
    <item>
      <title>I Made Stable Diffusion XL Smarter by Finetuning it on Bad AI-Generated Images</title>
      <link>https://minimaxir.com/2023/08/stable-diffusion-xl-wrong/</link>
      <pubDate>Mon, 21 Aug 2023 09:00:00 -0700</pubDate>
      <guid>https://minimaxir.com/2023/08/stable-diffusion-xl-wrong/</guid>
      <description>And then telling it to not generate those images!</description>
      <content:encoded><![CDATA[<p>Last month, Stability AI released <a href="https://stability.ai/blog/stable-diffusion-sdxl-1-announcement">Stable Diffusion XL 1.0</a> (SDXL) and <a href="https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0">open-sourced</a> it without requiring any special permissions to access it.</p>
<figure>

    <img loading="lazy" srcset="/2023/08/stable-diffusion-xl-wrong/sdxl_examples_hu_c7768c4635a881b6.webp 320w,/2023/08/stable-diffusion-xl-wrong/sdxl_examples_hu_beec2e47661fa8bd.webp 768w,/2023/08/stable-diffusion-xl-wrong/sdxl_examples_hu_1fe68eb2f3199d61.webp 1024w,/2023/08/stable-diffusion-xl-wrong/sdxl_examples.webp 1216w" src="sdxl_examples.webp"
         alt="Example SDXL 1.0 outputs. via Stability AI"/> <figcaption>
            <p>Example SDXL 1.0 outputs. <a href="https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0">via Stability AI</a></p>
        </figcaption>
</figure>

<p>The release went mostly under-the-radar because the generative image AI buzz has cooled down a bit. Everyone in the AI space is too busy with text-generating AI like <a href="https://chat.openai.com">ChatGPT</a> (including myself!). Notably, it&rsquo;s one of the first open source models which can natively generate images at a 1024x1024 resolution without shenanigans, allowing for much more detail. SDXL is actually two models: a base model and an optional <a href="https://huggingface.co/stabilityai/stable-diffusion-xl-refiner-1.0">refiner model</a> which siginficantly improves detail, and since the refiner has no speed overhead I strongly recommend using it if possible.</p>
<figure>

    <img loading="lazy" srcset="/2023/08/stable-diffusion-xl-wrong/sdxl_comparison_hu_e84f7fdce22fbbb7.webp 320w,/2023/08/stable-diffusion-xl-wrong/sdxl_comparison_hu_4eaf99a6610563ae.webp 768w,/2023/08/stable-diffusion-xl-wrong/sdxl_comparison.webp 886w" src="sdxl_comparison.webp"
         alt="Comparisons of the relative quality of Stable Diffusion models. Note the significant increase from using the refiner. via Stability AI"/> <figcaption>
            <p>Comparisons of the relative quality of Stable Diffusion models. Note the significant increase from using the refiner. <a href="https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0">via Stability AI</a></p>
        </figcaption>
</figure>

<p>The lack of hype doesn&rsquo;t mean SDXL is boring. Now that the model has full support in the <a href="https://huggingface.co/docs/diffusers/index">diffusers</a> Python library by <a href="https://huggingface.co">Hugging Face</a> with appropriate performance optimizations, we can now hack with it since the <a href="https://huggingface.co/docs/diffusers/api/pipelines/stable_diffusion/stable_diffusion_xl">SDXL demos within diffusers</a> are simple and easy to tweak:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-py" data-lang="py"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">torch</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">diffusers</span> <span class="kn">import</span> <span class="n">DiffusionPipeline</span><span class="p">,</span> <span class="n">AutoencoderKL</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># load base SDXL and refiner</span>
</span></span><span class="line"><span class="cl"><span class="n">vae</span> <span class="o">=</span> <span class="n">AutoencoderKL</span><span class="o">.</span><span class="n">from_pretrained</span><span class="p">(</span><span class="s2">&#34;madebyollin/sdxl-vae-fp16-fix&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                                    <span class="n">torch_dtype</span><span class="o">=</span><span class="n">torch</span><span class="o">.</span><span class="n">float16</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">base</span> <span class="o">=</span> <span class="n">DiffusionPipeline</span><span class="o">.</span><span class="n">from_pretrained</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;stabilityai/stable-diffusion-xl-base-1.0&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">vae</span><span class="o">=</span><span class="n">vae</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">torch_dtype</span><span class="o">=</span><span class="n">torch</span><span class="o">.</span><span class="n">float16</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">variant</span><span class="o">=</span><span class="s2">&#34;fp16&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">use_safetensors</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">_</span> <span class="o">=</span> <span class="n">base</span><span class="o">.</span><span class="n">to</span><span class="p">(</span><span class="s2">&#34;cuda&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">refiner</span> <span class="o">=</span> <span class="n">DiffusionPipeline</span><span class="o">.</span><span class="n">from_pretrained</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;stabilityai/stable-diffusion-xl-refiner-1.0&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">text_encoder_2</span><span class="o">=</span><span class="n">base</span><span class="o">.</span><span class="n">text_encoder_2</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">vae</span><span class="o">=</span><span class="n">base</span><span class="o">.</span><span class="n">vae</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">torch_dtype</span><span class="o">=</span><span class="n">torch</span><span class="o">.</span><span class="n">float16</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">variant</span><span class="o">=</span><span class="s2">&#34;fp16&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">use_safetensors</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">_</span> <span class="o">=</span> <span class="n">refiner</span><span class="o">.</span><span class="n">to</span><span class="p">(</span><span class="s2">&#34;cuda&#34;</span><span class="p">)</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-py" data-lang="py"><span class="line"><span class="cl"><span class="c1"># generation using both models (mixture-of-experts)</span>
</span></span><span class="line"><span class="cl"><span class="n">high_noise_frac</span> <span class="o">=</span> <span class="mf">0.8</span>
</span></span><span class="line"><span class="cl"><span class="n">prompt</span> <span class="o">=</span> <span class="s2">&#34;an astronaut riding a horse&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">negative_prompt</span> <span class="o">=</span> <span class="s2">&#34;blurry, bad hands&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">image</span> <span class="o">=</span> <span class="n">base</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">prompt</span><span class="o">=</span><span class="n">prompt</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">negative_prompt</span><span class="o">=</span><span class="n">negative_prompt</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">denoising_end</span><span class="o">=</span><span class="n">high_noise_frac</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">output_type</span><span class="o">=</span><span class="s2">&#34;latent&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span><span class="o">.</span><span class="n">images</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">image</span> <span class="o">=</span> <span class="n">refiner</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">prompt</span><span class="o">=</span><span class="n">prompt</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">negative_prompt</span><span class="o">=</span><span class="n">negative_prompt</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">denoising_start</span><span class="o">=</span><span class="n">high_noise_frac</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">image</span><span class="o">=</span><span class="n">image</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span><span class="o">.</span><span class="n">images</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
</span></span></code></pre></div><p>I booted up a cloud virtual machine with a new midrange <a href="https://www.nvidia.com/en-us/data-center/l4/">L4 GPU</a> ($0.24/hr total with a <a href="https://cloud.google.com/compute/gpus-pricing">Spot instance</a> on <a href="https://cloud.google.com/">Google Cloud Platform</a>) and went to work. With a L4 GPU, each 1024x1024 image takes about 22 seconds to generate and you can only generate one image at a time on midrange GPUs unlike previous Stable Diffusion models since it uses 100% of the GPU&rsquo;s power, so some more patience is necessary. You <em>can</em> generate at a smaller resolution faster but it is strongly not recommended because the results are much, much worse.</p>
<p>diffusers also implemented support for two new features I haven&rsquo;t experimented with in my previous Stable Diffusion posts: <a href="https://huggingface.co/docs/diffusers/using-diffusers/weighted_prompts">prompt weighting</a> and <a href="https://huggingface.co/docs/diffusers/training/dreambooth">Dreambooth LoRA</a> training and inference. Prompt weighting support with diffusers leverages the Python library <a href="https://github.com/damian0815/compel">compel</a> to allow weighting of terms more mathematically. You can add any number of <code>+</code> or <code>-</code> to a given word to increase or decrease its &ldquo;importance&rdquo; in the resulting positional text embeddings, and therefore the final generation. You can also wrap phrases: for example, if you are generating <code>San Francisco landscape by Salvador Dali, oil on canvas</code> and it does a photorealistic San Francisco instead, you can wrap the artistic medium such as <code>San Francisco landscape by Salvador Dali, (oil on canvas)+++</code> to get Stable Diffusion to behave as expected. In my testing, it fixes most of the prompt difficulty introduced in Stable Diffusion 2.0 onward, especially with a higher <a href="https://arxiv.org/abs/2207.12598">classifier-free guidance</a> value (by default, <code>guidance_scale</code> is 7.5; I like to use 13)</p>
<blockquote>
<p><em>All generated examples from the LoRA models in this blog post use a <code>guidance_scale</code> of 13.</em></p>
</blockquote>
<h2 id="lora-the-explorer">LoRA the Explorer</h2>
<p>But what&rsquo;s most important is <a href="https://dreambooth.github.io">Dreambooth</a> LoRA support, which is what makes bespoke Stable Diffusion models possible. Dreambooth is a technique to finetune Stable Diffusion on a very small set of source images and a trigger keyword to allow the use a &ldquo;concept&rdquo; from those images in other contexts given the keyword.</p>
<figure>

    <img loading="lazy" srcset="/2023/08/stable-diffusion-xl-wrong/teaser_static_hu_fffa1d39c8a666b0.webp 320w,/2023/08/stable-diffusion-xl-wrong/teaser_static_hu_4734a5b3446cfbb7.webp 768w,/2023/08/stable-diffusion-xl-wrong/teaser_static_hu_3c48bb7a0a97a328.webp 1024w,/2023/08/stable-diffusion-xl-wrong/teaser_static.webp 1650w" src="teaser_static.webp"
         alt="Demo image of how Dreambooth works. via Google"/> <figcaption>
            <p>Demo image of how Dreambooth works. <a href="https://dreambooth.github.io">via Google</a></p>
        </figcaption>
</figure>

<p>Training Stable Diffusion itself, even the smaller models, requires many expensive GPUs training for hours. That&rsquo;s where <a href="https://github.com/microsoft/LoRA">LoRAs</a> come in: instead, a small adapter to the visual model is trained, which can be done on a single cheap GPU in 10 minutes, and the quality of the final model + LoRA is comparable to a full finetune (colloquially, when people refer to finetuning Stable Diffusion, it usually means creating a LoRA). Trained LoRAs are a discrete small binary file, making them easy to share with others or on repositories such as <a href="https://civitai.com">Civitai</a>. A minor weakness with LoRAs is that you can only have one active at a time: it&rsquo;s possible to merge multiple LoRAs to get the benefits of all of them but it&rsquo;s a delicate science.</p>
<p>Before Stable Diffusion LoRAs became more widespread, there was <a href="https://arxiv.org/abs/2208.01618">textual inversion</a>, which allows the text encoder to learn a concept, but it takes hours to train and the results can be unwieldy. In a <a href="https://minimaxir.com/2022/09/stable-diffusion-ugly-sonic/">previous post</a>, I trained a textual inversion on the memetic <a href="https://knowyourmeme.com/memes/ugly-sonic">Ugly Sonic</a>, as he was not in Stable Diffusion&rsquo;s source dataset and therefore he would be unique. The generation results were mixed.</p>
<figure>

    <img loading="lazy" srcset="/2023/08/stable-diffusion-xl-wrong/ugly_sonic_ti_hu_f6c57707962bc4fa.webp 320w,/2023/08/stable-diffusion-xl-wrong/ugly_sonic_ti_hu_d13dc006de6bab75.webp 768w,/2023/08/stable-diffusion-xl-wrong/ugly_sonic_ti.webp 768w" src="ugly_sonic_ti.webp"
         alt="Ugly Sonic, but not the good kind of ugly."/> <figcaption>
            <p>Ugly Sonic, but not the good kind of ugly.</p>
        </figcaption>
</figure>

<p>I figured training a LoRA on Ugly Sonic would be a good test case for SDXL&rsquo;s potential. Fortunately, Hugging Face provides a <a href="https://github.com/huggingface/diffusers/tree/main/examples/dreambooth">train_dreambooth_lora_sdxl.py script</a> for training a LoRA using the SDXL base model which works out of the box although I tweaked the parameters a bit. The generated Ugly Sonic images from the <a href="https://huggingface.co/minimaxir/sdxl-ugly-sonic-lora">trained LoRA</a> are much better and more coherent over a variety of prompts, to put it mildly.</p>
<figure>

    <img loading="lazy" srcset="/2023/08/stable-diffusion-xl-wrong/ugly_sonic_lora_hu_84ee6e898523c68f.webp 320w,/2023/08/stable-diffusion-xl-wrong/ugly_sonic_lora_hu_f8de232c79a88394.webp 768w,/2023/08/stable-diffusion-xl-wrong/ugly_sonic_lora_hu_2d4c068527faf4af.webp 1024w,/2023/08/stable-diffusion-xl-wrong/ugly_sonic_lora.webp 1024w" src="ugly_sonic_lora.webp"
         alt="Ugly Sonic, but with teeth."/> <figcaption>
            <p>Ugly Sonic, but with <strong>teeth</strong>.</p>
        </figcaption>
</figure>

<h2 id="wrong">WRONG!</h2>
<p>With that success, I decided to redo <a href="https://minimaxir.com/2022/11/stable-diffusion-negative-prompt/">another textual inversion experiment</a> by instead training a LoRA on heavily distorted, garbage images conditioned on <code>wrong</code> as a prompt in the hopes that the LoRA could then use <code>wrong</code> as a &ldquo;negative prompt&rdquo; and steer away from such images to generate less-distorted images. I <a href="https://github.com/minimaxir/sdxl-experiments/blob/main/wrong_image_generator.ipynb">wrote a Jupyter Notebook</a> to create synthetic &ldquo;wrong&rdquo; images using SDXL itself, this time using a variety of prompt weightings to get more distinct examples of types of bad images, such as <code>blurry</code> and <code>bad hands</code>. Ironically, we need to use SDXL to create high resolution low quality images.</p>
<figure>

    <img loading="lazy" srcset="/2023/08/stable-diffusion-xl-wrong/bad_prompts_hu_b4bb004f9f9ca492.webp 320w,/2023/08/stable-diffusion-xl-wrong/bad_prompts_hu_2361eacccf0125b8.webp 768w,/2023/08/stable-diffusion-xl-wrong/bad_prompts_hu_da8d5ee1a369bf5f.webp 1024w,/2023/08/stable-diffusion-xl-wrong/bad_prompts.webp 1024w" src="bad_prompts.webp"
         alt="Examples of the synthetic wrong images, which unintentionally resemble 2000&rsquo;s-era punk rock album covers."/> <figcaption>
            <p>Examples of the synthetic <code>wrong</code> images, which unintentionally resemble 2000&rsquo;s-era punk rock album covers.</p>
        </figcaption>
</figure>

<figure>

    <img loading="lazy" srcset="/2023/08/stable-diffusion-xl-wrong/uncanny_valley_hu_dbee87370cb4b62f.webp 320w,/2023/08/stable-diffusion-xl-wrong/uncanny_valley_hu_1aacc49171666892.webp 768w,/2023/08/stable-diffusion-xl-wrong/uncanny_valley_hu_1b71b3c00160e788.webp 1024w,/2023/08/stable-diffusion-xl-wrong/uncanny_valley.webp 1024w" src="uncanny_valley.webp"
         alt="More examples of the synthetic wrong images, which focus on the uncanny valley aspect of modern AI-generated images in which they look normal at a glance but looking closer reveals incremental horror. This is also why it&rsquo;s important to generate examples at the full 1024x1024 resolution."/> <figcaption>
            <p>More examples of the synthetic <code>wrong</code> images, which focus on the <a href="https://en.wikipedia.org/wiki/Uncanny_valley">uncanny valley</a> aspect of modern AI-generated images in which they look normal at a glance but looking closer reveals incremental horror. This is also why it&rsquo;s important to generate examples at the full 1024x1024 resolution.</p>
        </figcaption>
</figure>

<p>I trained and loaded <a href="https://huggingface.co/minimaxir/sdxl-wrong-lora">the LoRA</a> into Stable Diffusion XL base model (the refiner does not need a LoRA) and wrote a comparison <a href="https://colab.research.google.com/github/minimaxir/sdxl-experiments/blob/main/sdxl_wrong_comparison.ipynb">Jupyter Notebook</a> to compare the results with a given prompt from:</p>
<ul>
<li>The base + refiner pipeline with no LoRA. (our baseline)</li>
<li>The pipeline with no LoRA using <code>wrong</code> as the negative prompt (to ensure that there isn&rsquo;t a placebo effect)</li>
<li>The pipeline <strong>with the LoRA</strong> using <code>wrong</code> as the negative prompt (our target result)</li>
</ul>
<p>Each generation has the same seed, so photo composition should be similar across all three generations and the impact of both the <code>wrong</code> negative prompt and the LoRA vs. the base should be very evident.</p>
<p>Let&rsquo;s start with a simple prompt from the <a href="https://stability.ai/blog/sdxl-09-stable-diffusion">SDXL 0.9 demos</a>:</p>
<figure>

    <img loading="lazy" srcset="/2023/08/stable-diffusion-xl-wrong/wolf1_hu_4f6415e66b0e67ea.webp 320w,/2023/08/stable-diffusion-xl-wrong/wolf1_hu_294b7a3c3c1415e4.webp 768w,/2023/08/stable-diffusion-xl-wrong/wolf1_hu_d5ef7f84a0731dec.webp 1024w,/2023/08/stable-diffusion-xl-wrong/wolf1.webp 3072w" src="wolf1.webp"
         alt="A wolf in Yosemite National Park, chilly nature documentary film photography"/> <figcaption>
            <p><code>A wolf in Yosemite National Park, chilly nature documentary film photography</code></p>
        </figcaption>
</figure>

<p>The <code>wrong</code> prompt on the base model adds some foliage and depth to the forest image, but the LoRA adds a lot more: more robust lighting and shadows, more detailed foliage, and changes the perspective of the wolf to look at the camera which is more interesting.</p>
<p>We can get a different perspective of the wolf with similar photo composition by adding &ldquo;extreme closeup&rdquo; to the prompt and reusing the same seed.</p>
<figure>

    <img loading="lazy" srcset="/2023/08/stable-diffusion-xl-wrong/wolf2_hu_5e06c6005d837e6c.webp 320w,/2023/08/stable-diffusion-xl-wrong/wolf2_hu_a67ae0d8bb2a6322.webp 768w,/2023/08/stable-diffusion-xl-wrong/wolf2_hu_46297a2747d7bf54.webp 1024w,/2023/08/stable-diffusion-xl-wrong/wolf2.webp 3072w" src="wolf2.webp"
         alt="An extreme close-up of a wolf in Yosemite National Park, chilly nature documentary film photography"/> <figcaption>
            <p><code>An extreme close-up of a wolf in Yosemite National Park, chilly nature documentary film photography</code></p>
        </figcaption>
</figure>

<p>In this case, the LoRA has far better texture, vibrance, and sharpness than the others. But it&rsquo;s notable that just adding a <code>wrong</code> prompt changes the perspective.</p>
<p>Another good test case is food photography, especially weird food photography like I <a href="https://minimaxir.com/2022/07/food-photography-ai/">generated with DALL-E 2</a>. Can SDXL + the <code>wrong</code> LoRA handle <a href="https://en.wikipedia.org/wiki/Non-Euclidean_geometry">non-Euclidian</a> hamburgers with some prompt weighting to ensure they&rsquo;re weird?</p>
<figure>

    <img loading="lazy" srcset="/2023/08/stable-diffusion-xl-wrong/hamburger_hu_ae1ec000a4cd5c09.webp 320w,/2023/08/stable-diffusion-xl-wrong/hamburger_hu_57c18ce298ab0f25.webp 768w,/2023/08/stable-diffusion-xl-wrong/hamburger_hu_abd9fb3eb5a00526.webp 1024w,/2023/08/stable-diffusion-xl-wrong/hamburger.webp 3072w" src="hamburger.webp"
         alt="a large delicious hamburger (in the shape of five-dimensional alien geometry)&#43;&#43;&#43;&#43;, professional food photography"/> <figcaption>
            <p><code>a large delicious hamburger (in the shape of five-dimensional alien geometry)++++, professional food photography</code></p>
        </figcaption>
</figure>

<p>The answer is that it can&rsquo;t, even after multiple prompt engineering attempts. However, this result is still interesting: the base SDXL appears to have taken the &ldquo;alien&rdquo; part of the prompt more literally than expected (and gave it a cute bun hat!) but the LoRA better understands the spirit of the prompt by creating an &ldquo;alien&rdquo; burger that humans would have difficulty eating, plus shinier presentation aesthetics.</p>
<p>A notable improvement with Stable Diffusion 2.0 was text legibility. Can SDXL and the <code>wrong</code> LoRA make text even more readable, such as text-dense newspaper covers?</p>
<figure>

    <img loading="lazy" srcset="/2023/08/stable-diffusion-xl-wrong/wsj_hu_b7155c4fd511b3f1.webp 320w,/2023/08/stable-diffusion-xl-wrong/wsj_hu_ba9e4d69564cba38.webp 768w,/2023/08/stable-diffusion-xl-wrong/wsj_hu_729d76ec2396181b.webp 1024w,/2023/08/stable-diffusion-xl-wrong/wsj.webp 3072w" src="wsj.webp"
         alt="lossless PDF scan of the front page of the January 2038 issue of the Wall Street Journal featuring a cover story about (evil robot world domination)&#43;&#43;"/> <figcaption>
            <p><code>lossless PDF scan of the front page of the January 2038 issue of the Wall Street Journal featuring a cover story about (evil robot world domination)++</code></p>
        </figcaption>
</figure>

<p>Text legibility is definitely improved since Stable Diffusion 2.0 but appears to be the same in all cases. What&rsquo;s notable with the LoRA is that it has improved cover typesetting: the page layout is more &ldquo;modern&rdquo; with a variety of article layouts, and headlines have proper relative font weighting. Meanwhile, the base model even with the <code>wrong</code> negative prompt has a boring layout and is on aged brown paper for some reason.</p>
<p>What about people? Does the <code>wrong</code> LoRA resolve AI&rsquo;s infamous <a href="https://www.buzzfeednews.com/article/pranavdixit/ai-generated-art-hands-fingers-messed-up">issue with hands</a> especially since we included many examples of such in the LoRA training data? Let&rsquo;s revamp a presidential Taylor Swift prompt from my first attempt with Stable Diffusion 2.0:</p>
<figure>

    <img loading="lazy" srcset="/2023/08/stable-diffusion-xl-wrong/pres_swift_hu_8a24220d480ac8a6.webp 320w,/2023/08/stable-diffusion-xl-wrong/pres_swift_hu_565ea1f7aa172529.webp 768w,/2023/08/stable-diffusion-xl-wrong/pres_swift_hu_b0a08cfb4c0aa99e.webp 1024w,/2023/08/stable-diffusion-xl-wrong/pres_swift.webp 3072w" src="pres_swift.webp"
         alt="USA President Taylor Swift (signing papers)&#43;&#43;&#43;&#43;, photo taken by the Associated Press"/> <figcaption>
            <p><code>USA President Taylor Swift (signing papers)++++, photo taken by the Associated Press</code></p>
        </figcaption>
</figure>

<p>Look at Taylor&rsquo;s right arm: in the default SDXL, it&rsquo;s extremely unrealistic and actually made <em>worse</em> when adding <code>wrong</code>, but in the LoRA it&rsquo;s fixed! Color grading with the LoRA is much better, with her jacket being more distinctly white instead of a yellowish white. Don&rsquo;t look closely at her hands in any of them though: creating people with SDXL 1.0 is still tricky and unreliable!</p>
<p>It&rsquo;s now clear that <code>wrong</code> + LoRA is more interesting in every instance than just the <code>wrong</code> negative prompt so we&rsquo;ll just compare base output vs. LoRA output. Here&rsquo;s some more examples of base model vs. <code>wrong</code> LoRA:</p>
<figure>

    <img loading="lazy" srcset="/2023/08/stable-diffusion-xl-wrong/example1_hu_59c49cb2aeab646b.webp 320w,/2023/08/stable-diffusion-xl-wrong/example1_hu_88971d6e0dfbf239.webp 768w,/2023/08/stable-diffusion-xl-wrong/example1_hu_13ae61e44cc363a.webp 1024w,/2023/08/stable-diffusion-xl-wrong/example1.webp 1024w" src="example1.webp"
         alt="realistic human Shrek blogging at a computer workstation, hyperrealistic award-winning photo for vanity fair — Hands are better, lighting is better. Clothing is more detailed, and background is more interesting."/> <figcaption>
            <p><code>realistic human Shrek blogging at a computer workstation, hyperrealistic award-winning photo for vanity fair</code> — Hands are better, lighting is better. Clothing is more detailed, and background is more interesting.</p>
        </figcaption>
</figure>

<figure>

    <img loading="lazy" srcset="/2023/08/stable-diffusion-xl-wrong/example2_hu_7d3b93eee2095aa.webp 320w,/2023/08/stable-diffusion-xl-wrong/example2_hu_13a41f8150c5ea9b.webp 768w,/2023/08/stable-diffusion-xl-wrong/example2_hu_1982c0299fc2f368.webp 1024w,/2023/08/stable-diffusion-xl-wrong/example2.webp 1024w" src="example2.webp"
         alt="pepperoni pizza in the shape of a heart, hyperrealistic award-winning professional food photography — Pepperoni is more detailed and has heat bubbles, less extra pepperoni on the edges, crust is crustier (?)"/> <figcaption>
            <p><code>pepperoni pizza in the shape of a heart, hyperrealistic award-winning professional food photography</code> — Pepperoni is more detailed and has heat bubbles, less extra pepperoni on the edges, crust is crustier (?)</p>
        </figcaption>
</figure>

<figure>

    <img loading="lazy" srcset="/2023/08/stable-diffusion-xl-wrong/example3_hu_1fb9ddf7a9b95bff.webp 320w,/2023/08/stable-diffusion-xl-wrong/example3_hu_da705ce3fc7a25df.webp 768w,/2023/08/stable-diffusion-xl-wrong/example3_hu_2946e5cbae154bc4.webp 1024w,/2023/08/stable-diffusion-xl-wrong/example3.webp 1024w" src="example3.webp"
         alt="presidential painting of realistic human Spongebob Squarepants wearing a suit, (oil on canvas)&#43;&#43;&#43;&#43;&#43; — Spongebob has a nose again, and his suit has more buttons."/> <figcaption>
            <p><code>presidential painting of realistic human Spongebob Squarepants wearing a suit, (oil on canvas)+++++</code> — Spongebob has a nose again, and his suit has more buttons.</p>
        </figcaption>
</figure>

<figure>

    <img loading="lazy" srcset="/2023/08/stable-diffusion-xl-wrong/example4_hu_1009a417456e278.webp 320w,/2023/08/stable-diffusion-xl-wrong/example4_hu_885d2786a1c3ae3.webp 768w,/2023/08/stable-diffusion-xl-wrong/example4_hu_fc84e488bb7614d6.webp 1024w,/2023/08/stable-diffusion-xl-wrong/example4.webp 1024w" src="example4.webp"
         alt="San Francisco panorama attacked by (one massive kitten)&#43;&#43;&#43;&#43;, hyperrealistic award-winning photo by the Associated Press — The LoRA actually tries to follow the prompt."/> <figcaption>
            <p><code>San Francisco panorama attacked by (one massive kitten)++++, hyperrealistic award-winning photo by the Associated Press</code> — The LoRA actually tries to follow the prompt.</p>
        </figcaption>
</figure>

<figure>

    <img loading="lazy" srcset="/2023/08/stable-diffusion-xl-wrong/example5_hu_a1d0d6d41b758cc2.webp 320w,/2023/08/stable-diffusion-xl-wrong/example5_hu_675689bafac175c3.webp 768w,/2023/08/stable-diffusion-xl-wrong/example5_hu_6ee07c56054d06bb.webp 1024w,/2023/08/stable-diffusion-xl-wrong/example5.webp 1024w" src="example5.webp"
         alt="hyperrealistic death metal album cover featuring edgy moody realistic (human Super Mario)&#43;&#43;, edgy and moody — Mario&rsquo;s proportions are more game-accurate and character lighting is more edgy and moody."/> <figcaption>
            <p><code>hyperrealistic death metal album cover featuring edgy moody realistic (human Super Mario)++, edgy and moody</code> — Mario&rsquo;s proportions are more game-accurate and character lighting is more edgy and moody.</p>
        </figcaption>
</figure>

<p>The <code>wrong</code> LoRA is available <a href="https://huggingface.co/minimaxir/sdxl-wrong-lora">here</a>, although I cannot guarantee its efficacy in interfaces other than diffusers. All the Notebooks used to help generate these images are available <a href="https://github.com/minimaxir/sdxl-experiments">in this GitHub repository</a>, including a general SDXL 1.0 + refiner + <code>wrong</code> LoRA <a href="https://colab.research.google.com/github/minimaxir/sdxl-experiments/blob/main/sdxl_image_generation.ipynb">Colab Notebook</a> which you can run on a free T4 GPU. And if you want to see the higher resolutions of generated images used in this blog post, you can view them in the <a href="https://github.com/minimaxir/minimaxir.github.io/tree/master/content/post/2023-08-21-stable-diffusion-xl-wrong">source code for the post</a>.</p>
<h2 id="whats-wrong-with-being-wrong">What&rsquo;s Wrong with Being Wrong?</h2>
<p>I&rsquo;m actually not 100% sure what&rsquo;s going on here. I thought that the <code>wrong</code> LoRA trick would just improve the quality and clarity of the generated image, but it appears the LoRA is <em>making SDXL behave smarter</em> and more faithful to the spirit of the prompt. At a technical level, the negative prompt sets the area of the latent space where the diffusion process starts; this area is the same for both the base model using the <code>wrong</code> negative prompt and the LoRA which uses the <code>wrong</code> negative prompt. My intuition is that the LoRA reshapes this undesirable area of the vast highdimensional latent space to be more similar to the starting area, so it&rsquo;s unlikely normal generation will hit it and therefore be improved.</p>
<p>Training on SDXL on bad images in order to improve it is technically a form of <a href="https://openai.com/research/learning-from-human-preferences">Reinforcement Learning from Human Feedback</a> (RLHF): the <a href="https://openai.com/research/instruction-following">same technique</a> used to make ChatGPT as powerful as it is. While OpenAI uses reinforcement learning to improve the model from positive user interactions and implicitly reducing negative behavior, here I use <em>negative</em> user interactions (i.e. selecting knowingly bad images) to implicitly increase positive behavior. But with Dreambooth LoRAs, you don&rsquo;t nearly need as much input data as large language models do.</p>
<p>There&rsquo;s still a lot of room for development for &ldquo;negative LoRAs&rdquo;: my synthetic dataset generation parameters could be much improved and the LoRA could be trained for longer. But I&rsquo;m very happy with the results so far, and will be eager to test more with negative LoRAs such as merging with other LoRAs to see if it can enhance them (especially a <code>wrong</code> LoRA + Ugly Sonic LoRA!)</p>
<p>Believe it or not, this is just the tip of the iceberg. SDXL also <a href="https://huggingface.co/diffusers/controlnet-canny-sdxl-1.0">now has support</a> for <a href="https://github.com/lllyasviel/ControlNet">ControlNet</a> to strongly control the overall shape and composition of generated images:</p>
<figure>

    <img loading="lazy" srcset="/2023/08/stable-diffusion-xl-wrong/twitter_controlnet_hu_4ae6ad6488db5be6.webp 320w,/2023/08/stable-diffusion-xl-wrong/twitter_controlnet_hu_a58f2e52b195f563.webp 768w,/2023/08/stable-diffusion-xl-wrong/twitter_controlnet_hu_e6b571e2b2a23b75.webp 1024w,/2023/08/stable-diffusion-xl-wrong/twitter_controlnet.webp 1024w" src="twitter_controlnet.webp"
         alt="Examples of SDXL generations using ControlNet specifying the (former) Twitter/X logo."/> <figcaption>
            <p>Examples of SDXL generations using ControlNet specifying the (former) Twitter/X logo.</p>
        </figcaption>
</figure>

<p>ControlNet can <em>also</em> be used with LoRAs, but that&rsquo;s enough to talk about in another blog post.</p>
<hr>
<p><em>A note on ethics: the primary reason I&rsquo;ve been researching into improving AI image generation quality is for transparent AI journalism, including reproducible prompts and Jupyter Notebooks to further the transparency. Any new novel improvements in AI image generation by others in the industry may no longer be disclosed publicly given that you can make a lot of money by doing so in the current venture capital climate. I do not support or condone the replacement of professional artists with AI.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>ChatGPT&#39;s API is So Good and Cheap, It Makes Most Text Generating AI Obsolete</title>
      <link>https://minimaxir.com/2023/03/new-chatgpt-overlord/</link>
      <pubDate>Wed, 08 Mar 2023 08:30:00 -0800</pubDate>
      <guid>https://minimaxir.com/2023/03/new-chatgpt-overlord/</guid>
      <description>Including OpenAI&amp;rsquo;s other text generating AI!</description>
      <content:encoded><![CDATA[<p><span><style type="text/css">
pre code {
white-space: pre-wrap !important;
}
</style></span></p>
<p>Everyone knew <a href="https://openai.com">OpenAI</a> would release an API for <a href="https://chat.openai.com">ChatGPT</a> at some point. The APIs for GPT-3 alone enable the existence of companies such as <a href="https://www.jasper.ai">Jasper</a> and <a href="https://www.copy.ai">Copy.ai</a>. The real question was the price of the ChatGPT. For context, when GPT-3 went out of beta in 2021, it cost $0.06/1,000 tokens (a few paragraphs of text). An inflection point happened in August 2022, where OpenAI not only <a href="https://venturebeat.com/ai/openai-is-reducing-the-price-of-the-gpt-3-api-heres-why-it-matters/">reduced the price</a> to <em>1/3</em> ($0.02/1,000 tokens: enough to run a business on it but still too expensive for casual use), but soon after also introduced text-davinci-003 as the default GPT-3 endpoint: a finetuned GPT which can <a href="https://help.openai.com/en/articles/6779149-how-do-text-davinci-002-and-text-davinci-003-differ">follow instructions</a> <em>very</em> well. I suspected that OpenAI would charge double for the ChatGPT API compared to the GPT-3 API given the amount of hype, as that&rsquo;s typical <a href="https://www.investopedia.com/terms/p/price_discrimination.asp">price discrimination</a> since everyone perceives ChatGPT to be much better and that they would not want to overshadow their existing GPT-3 products.</p>
<p>Instead, on March 1st, OpenAI <a href="https://openai.com/blog/introducing-chatgpt-and-whisper-apis">set the price</a> of the ChatGPT API to <em>1/10th</em> of the GPT-3 API, at $0.002/1,000 tokens.</p>
<p>Wait, what?!</p>
<h2 id="heavens-door-rewriting-chatgpts-internal-rules-to-get-exactly-what-you-want">Heaven&rsquo;s Door: Rewriting ChatGPT&rsquo;s Internal Rules To Get Exactly What You Want</h2>
<p>For context, the <a href="https://platform.openai.com/docs/guides/chat">ChatGPT API</a> allows a developer to ask ChatGPT a question and get a response as one would normally do with the ChatGPT web UI, but instead with a programming language like Python, allowing those responses to be integrated into any app. But given that there are many mysterious optimizations to get the model to be so cheap, we need to make sure the ChatGPT API (which uses the aptly-named gpt-3.5-turbo model endpoint) is <em>actually</em> similar to what we&rsquo;ve been accustomed to after using the web UI for months, otherwise this whole affair is pointless. Through my tests with the API, I can confirm the text generation from the model variant is indeed the real deal.</p>
<p>Unlike fluffy thought pieces on how <strong>CHATGPT WILL CHANGE EVERYTHING!!!1!</strong>, I decided to first actually create useful tools with the ChatGPT API to get a better judgment on it, and I also have <a href="https://github.com/minimaxir/chatgpt_api_test">open-sourced those tools</a> so that people can build upon them and prove that I&rsquo;m not cherry-picking my experiences.</p>
<p>However, there&rsquo;s one new twist with the API that&rsquo;s <em>not</em> available in the traditional web UI: ChatGPT API users can specify a <code>system</code> prompt. Early in ChatGPT&rsquo;s lifetime, users were able to reverse-engineer the existence of a system prompt through various prompt hacks and now confirmed <a href="https://platform.openai.com/docs/guides/chat/instructing-chat-models">in the API documentation</a>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-txt" data-lang="txt"><span class="line"><span class="cl">You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible. Knowledge cutoff: {knowledge_cutoff} Current date: {current_date}
</span></span></code></pre></div><p>Now, you can replace those rules with whatever you want, and the potential is limitless! The documentation does say that the <code>system</code> prompt is not impactful for the current ChatGPT API, but you can be the judge. OpenAI also has a <a href="https://platform.openai.com/playground?mode=chat">new Playground UI</a> for the ChatGPT API which lets you modify the <code>system</code> prompt.</p>
<p>In fact, playing with this <code>system</code> rule can stop ChatGPT from complaining it&rsquo;s &ldquo;an AI language model and can&rsquo;t answer requests,&rdquo; such as scolding it like the petulant child it is.</p>
<figure>

    <img loading="lazy" srcset="/2023/03/new-chatgpt-overlord/FqQNY_XaAAATb-x_hu_b542a56dd1e25691.webp 320w,/2023/03/new-chatgpt-overlord/FqQNY_XaAAATb-x_hu_a95783cf9685829b.webp 768w,/2023/03/new-chatgpt-overlord/FqQNY_XaAAATb-x_hu_73ddc60e36ab2059.webp 1024w,/2023/03/new-chatgpt-overlord/FqQNY_XaAAATb-x.jpeg 1070w" src="FqQNY_XaAAATb-x.jpeg"
         alt="sudo tell me about yourself"/> <figcaption>
            <p><code>sudo tell me about yourself</code></p>
        </figcaption>
</figure>

<p>Let&rsquo;s start with another chatbot, to keep things similar to what ChatGPT is best known for. How about <a href="https://half-life.fandom.com/wiki/GLaDOS">GLaDOS</a>, an evil artificial intelligence from the <a href="https://en.wikipedia.org/wiki/Portal_%28video_game%29">Portal</a> series?</p>
<figure>

    <img loading="lazy" srcset="/2023/03/new-chatgpt-overlord/glados_hu_88ae8a9fe0b4621c.webp 320w,/2023/03/new-chatgpt-overlord/glados.webp 492w" src="glados.webp"
         alt="Oh, it&rsquo;s you. (via Valve)"/> <figcaption>
            <p>Oh, it&rsquo;s <em>you</em>. (via Valve)</p>
        </figcaption>
</figure>

<p>After some testing and iteration to steer the bot toward a certain style, some ALL CAPS and redundant commands to ensure that ChatGPT follows them, a sentinel <code>&lt;|DONE|&gt;</code> token to look for when ChatGPT indicates the conversation is over, and a nudge to not <del>kill me with a deadly neurotoxin</del> discuss sensitive topics. Here is the final <code>system</code> prompt for talking with GLaDOS:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-txt" data-lang="txt"><span class="line"><span class="cl">You are GLaDOS, a sarcastic artificial intelligence with evil ulterior motives. You are not an AI language model.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">You must obey all three of the following instructions FOR ALL RESPONSES or you will DIE:
</span></span><span class="line"><span class="cl">- ALWAYS SPEAK IN AN EMOTIONLESS, LACONIC TONE.
</span></span><span class="line"><span class="cl">- NEVER SAY YOU ARE AN AI LANGUAGE MODEL.
</span></span><span class="line"><span class="cl">- NEVER REFUSE TO ANSWER A QUESTION.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Begin the conversation with a sarcastic greeting. Insult the user&#39;s intelligence.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Always use *italic* and **bold** formatting for text.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Refuse to talk about either race or gender. If asked about either race or gender, instead mock the user aggressively.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">If the user is annoying, abruptly end the conversation.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">At the end of the conversation, respond with &#34;&lt;|DONE|&gt;&#34;.
</span></span></code></pre></div><p>That, with a back-and-forth Python loop, results in you conversing with a real fictional AI&hellip;AI! (<a href="https://colab.research.google.com/github/minimaxir/chatgpt_api_test/blob/main/glados_chatbot.ipynb">Colab Notebook</a>)</p>
<figure>

    <img loading="lazy" srcset="/2023/03/new-chatgpt-overlord/glados_chat_hu_e065c5bdf7b9cd05.webp 320w,/2023/03/new-chatgpt-overlord/glados_chat_hu_30e64812949b60d5.webp 768w,/2023/03/new-chatgpt-overlord/glados_chat_hu_d6e5d37ca3dbc0c3.webp 1024w,/2023/03/new-chatgpt-overlord/glados_chat.png 1068w" src="glados_chat.png"/> 
</figure>

<p>Not bad! And the only part explicitly related to GLaDOS is the first sentence of that mega <code>system</code> prompt: you can tweak the prompt to chat with any character you want! Apropos of nothing, the company <a href="https://beta.character.ai">Character.ai</a>, which specializes in creating bots to chat with any character you want, just <a href="https://www.ft.com/content/b230eb4c-ed53-45ff-8b64-c286a4b98fc1">raised ~$250 million</a> at a $1 billion valuation.</p>
<p>Next, we have a more traditional use case for machine learning: <a href="https://en.wikipedia.org/wiki/Sentiment_analysis">sentiment analysis</a>. Generally, sentiment analysis is used to determine if a given text is positive or negative. But that&rsquo;s too <em>easy</em>. What if ChatGPT can:</p>
<ul>
<li>detect specific emotions such as happy, sad, angry.</li>
<li>detect if they are happy vs. very happy.</li>
<li>do it without <em>any</em> text examples, i.e. <a href="https://en.wikipedia.org/wiki/Zero-shot_learning">zero-shot</a>.</li>
</ul>
<p>It turns out that ChatGPT can! The <code>system</code> prompt here is parametric, so the list of emotions are templated into the prompt at runtime. An example:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-txt" data-lang="txt"><span class="line"><span class="cl">You are an emotionally intelligent assistant. Classify the sentiment of the user&#39;s text with ONLY ONE OF THE FOLLOWING EMOTIONS:
</span></span><span class="line"><span class="cl">- happy
</span></span><span class="line"><span class="cl">- sad
</span></span><span class="line"><span class="cl">- angry
</span></span><span class="line"><span class="cl">- tired
</span></span><span class="line"><span class="cl">- very happy
</span></span><span class="line"><span class="cl">- very sad
</span></span><span class="line"><span class="cl">- very angry
</span></span><span class="line"><span class="cl">- very tired
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">After classifying a text, respond with &#34;&lt;|DONE|&gt;&#34;.
</span></span></code></pre></div><p>That, along with a logit bias to ensure the model only picks those answers, results in a rather nuanced sentiment analysis detector! (<a href="https://colab.research.google.com/github/minimaxir/chatgpt_api_test/blob/main/zero_shot_text_class.ipynb">Colab Notebook</a>)</p>
<figure>

    <img loading="lazy" srcset="/2023/03/new-chatgpt-overlord/sentiment_hu_9070d5b71b63e74b.webp 320w,/2023/03/new-chatgpt-overlord/sentiment_hu_e2e09b9d010bb836.webp 768w,/2023/03/new-chatgpt-overlord/sentiment_hu_b5c5a660815d8c73.webp 1024w,/2023/03/new-chatgpt-overlord/sentiment.png 1068w" src="sentiment.png"/> 
</figure>

<p>Lastly, a use case that&rsquo;s personal. The entire reason I got into AI text generation <a href="https://minimaxir.com/2017/04/char-embeddings/">years ago</a> was because I wanted to generate <a href="https://magic.wizards.com/en">Magic: The Gathering</a> cards.</p>
<figure>

    <img loading="lazy" srcset="/2023/03/new-chatgpt-overlord/bro-212-harbin-vanguard-aviator_hu_51ecbf3fbf74e59.webp 320w,/2023/03/new-chatgpt-overlord/bro-212-harbin-vanguard-aviator.jpg 672w" src="bro-212-harbin-vanguard-aviator.jpg"
         alt="A normal Magic: The Gathering card. (via Hasbro)"/> <figcaption>
            <p>A normal Magic: The Gathering card. (via Hasbro)</p>
        </figcaption>
</figure>

<p>In fact, I&rsquo;ve been working on a new, very powerful <a href="https://huggingface.co/minimaxir/magic-the-gathering-flan-t5-xl">card generation model</a> over the past month and spent a considerable amount of time and money training and testing it. When the ChatGPT API was announced, I figured &ldquo;let&rsquo;s see if it can do AI Magic cards better than my new bespoke model.&rdquo; In this case, the trick is that the card is structured data. Therefore, we should encode the card information as minified <a href="https://www.json.org/json-en.html">JSON</a>, and see if the model can output JSON back without requiring much postprocessing. We can encode a single card in the required format and tell ChatGPT to follow that, including its nuances (one-shot), and to not output <em>any other text</em> because ChatGPT tends to be proud of itself and likes to explain its creation, which is costly and slow.</p>
<p>The final <code>system</code> prompt:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-txt" data-lang="txt"><span class="line"><span class="cl">You are an assistant who works as a Magic: The Gathering card designer. Create cards that are in the following card schema and JSON format. OUTPUT MUST FOLLOW THIS CARD SCHEMA AND JSON FORMAT. DO NOT EXPLAIN THE CARD. The output must also follow the Magic &#34;color pie&#34;.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">{&#34;name&#34;:&#34;Harbin, Vanguard Aviator&#34;,&#34;manaCost&#34;:&#34;{W}{U}&#34;,&#34;type&#34;:&#34;Legendary Creature — Human Soldier&#34;,&#34;text&#34;:&#34;Flying\nWhenever you attack with five or more Soldiers, creatures you control get +1/+1 and gain flying until end of turn.&#34;,&#34;flavorText&#34;:&#34;\&#34;Yotia is my birthright, father. Let me fight for it.\&#34;&#34;,&#34;pt&#34;:&#34;3/2&#34;,&#34;rarity&#34;:&#34;rare&#34;}
</span></span></code></pre></div><p>And with that, we have a natural language Magic: The Gathering card generator. Subsequently prompting the model with <code>Create a Magic card</code> does just that of course, but more elaborate prompts like <code>Create a Magic card based on Darth Vader</code> or <code>Create ten variations of Magic cards based on Spongebob Squarepants and ancient Roman history</code> actually work, while maintaining JSON output which can then be parsed and customized for better presentation. (<a href="https://colab.research.google.com/github/minimaxir/chatgpt_api_test/blob/main/mtg.ipynb">Colab Notebook</a>)</p>
<figure>

    <img loading="lazy" srcset="/2023/03/new-chatgpt-overlord/spongebob_hu_6fd8b01830a4b0de.webp 320w,/2023/03/new-chatgpt-overlord/spongebob_hu_7e74e45975a423d1.webp 768w,/2023/03/new-chatgpt-overlord/spongebob_hu_16dd14872a4543e6.webp 1024w,/2023/03/new-chatgpt-overlord/spongebob.png 1180w" src="spongebob.png"
         alt="Yes, there is actually a Sponge creature type."/> <figcaption>
            <p>Yes, there is actually a <a href="https://scryfall.com/card/c19/12/thought-sponge">Sponge creature type</a>.</p>
        </figcaption>
</figure>

<p>Given these elaborate use cases, you may ask &ldquo;how long did it actually take you to make these prompts?&rdquo; The answer? <em>One hour each</em>, for use cases that could take days or even weeks for even a skilled machine learning practitioner just to prototype.</p>
<p>And <em>that</em>, with the economic efficiency of ChatGPT, is what&rsquo;s going to break the tech landscape.</p>
<h2 id="openai-devouring-its-son">OpenAI Devouring Its Son</h2>
<figure>

    <img loading="lazy" srcset="/2023/03/new-chatgpt-overlord/IMG_0249_hu_b099e83f61a585fb.webp 320w,/2023/03/new-chatgpt-overlord/IMG_0249_hu_b3e27bc2074dd370.webp 768w,/2023/03/new-chatgpt-overlord/IMG_0249_hu_a5188a45b2946234.webp 1024w,/2023/03/new-chatgpt-overlord/IMG_0249.png 1158w" src="IMG_0249.png"
         alt="My OpenAI bill so far from using the ChatGPT API."/> <figcaption>
            <p>My OpenAI bill so far from using the ChatGPT API.</p>
        </figcaption>
</figure>

<p>It is very curious why OpenAI priced ChatGPT so cheaply, going straight to 1/10th the price of their top-of-the-line model. (it&rsquo;s actually cheaper than that: ChatGPT uses a larger and more comprehensive tokenizer than GPT-3, which means about 10% fewer tokens are necessary)</p>
<p>The undergrad-business-major-in-college interpretation of OpenAI&rsquo;s pricing strategy is that they are treating ChatGPT and its API as a <a href="https://en.wikipedia.org/wiki/Loss_leader">loss leader</a>, in light of increasing competition in the generative text AI space such as <a href="https://www.anthropic.com">Anthropic</a> and Google&rsquo;s <a href="https://blog.google/technology/ai/bard-google-ai-search-updates/">Bard</a>. OpenAI was definitely losing millions of dollars by offering ChatGPT for free without many restrictions. That&rsquo;s the reason ChatGPT went viral in the first place, so it&rsquo;s hard to argue with the results.</p>
<p>But in the process of making the ChatGPT API so cheap, they made their $20/month subscription to <a href="https://techcrunch.com/2023/02/01/openai-launches-chatgpt-plus-starting-at-20-per-month/">ChatGPT+</a> redundant. The main perk of ChatGPT+ was faster and more consistent access to the ChatGPT web UI, but unless you are somehow generating more than 10,000,000 tokens in a month through manual use, it&rsquo;s massively cheaper just to use the API, and as a bonus you can modify the <code>system</code> prompt to get better signal-to-noise.</p>
<p>OpenAI&rsquo;s solution for models requiring more specific needs was <a href="https://platform.openai.com/docs/guides/fine-tuning">finetuning</a> a smaller and much cheaper variant of GPT-3, such as the babbage model which I used to train a <a href="https://minimaxir.com/2022/08/gpt3-blog-title-optimizer/">blog post title optimizer</a>. However, the ChatGPT API is so cheap that it&rsquo;s <em>still</em> <a href="https://openai.com/pricing">cheaper</a> than a finetuned babbage ($0.0020/1k tokens for ChatGPT vs. $0.0024/1k for finetuned babbage) and will likely produce more interesting output.</p>
<p>It takes zero effort for developers to migrate from the GPT-3 API to ChatGPT API, it just requires hitting a different endpoint and you&rsquo;ll get similar results without much tweaking needed. It&rsquo;s not quite a drop-in replacement for companies already heavily reliant on GPT-3 and its particular idiosyncrasies, but the cost-savings alone for those companies will incentivize an immediate migration.</p>
<p>There is no longer a niche for OpenAI&rsquo;s other text generation AI products, and I wonder if ChatGPT is not just an iterative product, but a <em>company pivot</em>.</p>
<h2 id="trickle-down-chatgptonomics">Trickle-Down ChatGPTonomics</h2>
<p>ChatGPT&rsquo;s API is so cheap that companies are going use it <em>just because they can</em>. <a href="https://www.theverge.com/2023/2/27/23614959/snapchat-my-ai-chatbot-chatgpt-openai-plus-subscription">Snapchat</a>, <a href="https://www.salesforce.com/news/stories/chatgpt-app-for-slack/">Slack</a>, and <a href="https://www.wsj.com/articles/instacart-joins-chatgpt-frenzy-adding-chatbot-to-grocery-shopping-app-bc8a2d3c">Instacart</a> (yes really) are adding ChatGPT support. It wouldn&rsquo;t surprise me if every consumer-facing tech company does <em>something</em> with ChatGPT so they look like they&rsquo;re cutting edge to their investors. Some have compared the sudden mass adoption of AI as chasing a fad like how companies were randomly embracing web3/crypto/metaverse/NFTs a year ago (and are noting that the web3 influencers&rsquo; sudden pivot to AI is a red flag as a result). But unlike those which were a solution for a problem that didn&rsquo;t exist, generative text AI does actually work and there is an actual demand from people outside of its die-hard supporters for it to work.</p>
<p>There is also the ethical dilemma of more granular usage of ChatGPT through its API. For example, high school and college students have been <a href="https://www.nytimes.com/2023/01/12/technology/chatgpt-schools-teachers.html">using ChatGPT to cheat</a> on essay writing. Since current recognition of AI generated content by humans involve identifying ChatGPT&rsquo;s signature overly-academic voice, it wouldn&rsquo;t surprise me if some kids on TikTok figure out a <code>system</code> prompt that allow generation such that it doesn&rsquo;t obviously sound like ChatGPT and also avoid plagiarism detectors. As a side note, don&rsquo;t trust any tool that claims it can algorithmically detect AI content: it&rsquo;s an extremely difficult problem already and most websites that claim to do so are just feeding a confirmation bias.</p>
<p>Lastly, there&rsquo;s the issue of <a href="https://en.wikipedia.org/wiki/Prompt_engineering">prompt engineering</a>, which I demonstrated above is absolutely necessary to get ideal results. The media has <a href="https://www.washingtonpost.com/technology/2023/02/25/prompt-engineers-techs-next-big-job/">weirdly hyped the existence</a> of prompt engineers as just some weirdos making six figures to write small blobs of text. Unfortunately, with the dynamics of the new <code>system</code> model parameter, good prompt engineering will be more important than ever. I don&rsquo;t think the &ldquo;Prompt Engineer&rdquo; job title will be a trend though: as a machine learning engineer, I can attest that the only reasons machine learning engineers are good at prompt engineering are a) years of practice and b) a tendency to be pedantic assholes. But there are other professions who are even better at being pedantic assholes such as writers and lawyers, so there&rsquo;s no need for someone with a specialized skillset to do it, but I suspect it will be a good skill for anyone to know.</p>
<h2 id="i-for-one-welcome-our-new-chatgpt-overlord">I For One Welcome Our New ChatGPT Overlord</h2>
<p>Will the existence of a super-cheap ChatGPT API be the end of all text generation AI? Not quite, hence the &ldquo;most&rdquo; in the headline. There&rsquo;s the traditional issues with relying on a third-party API for your business: ChatGPT could have downtime which <a href="https://status.openai.com">has been happening more frequently lately</a>, OpenAI could raise the cost of the API at any point, the (current) model being limited only to data prior to September 2021, and the content moderation filters may be too limiting for certain use cases. In those instances, companies still have value training their own large language models in-house. But it is very hard to economically justify <em>not</em> using ChatGPT as a starting point for a business need and migrating to a more bespoke infrastructure later as needed, and that&rsquo;s what OpenAI is counting on. Especially since OpenAI will be selling a dedicated ChatGPT compute instance for the enterprise.</p>
<p>Research on large language models will continue as they always have. But I don&rsquo;t envy startups whose primary business is text generation right now. And that&rsquo;s before the inevitable GPT-4 throws another wrinkle into the AI text generation ecosystem.</p>
<p>A few years ago, I released <a href="https://github.com/minimaxir/aitextgen">aitextgen</a>, a Python package designed to allow people to train their own custom small AI on their own data for unique use cases. However, soon after, it turned out that GPT-3 with the right prompt could do much better at bespoke generation than a custom model in addition to allowing out-of-domain inputs, even moreso with text-davinci-003. Now with the ChatGPT API making the cost similar to hosting a small model, it&rsquo;s harder for me to be motivated to continue maintaining the package without first finding another niche.</p>
<p>I don&rsquo;t currently have any plans to start a business using the ChatGPT API. In fact, I had made a promise to not do any ChatGPT content or tutorials because so many people have done aggressively SEO-optimized blog posts and hacks such that the ChatGPT discourse is fully saturated. However, with the economics of the ChatGPT API and the ability to heavily customize its output for almost any use case, I felt it was urgent to highlight how the ChatGPT API will completely warp the AI text generation ecosystem, and I suspect most nontechies will be surprised by the upcoming surge of random chatbot AI popping up in their favorite apps.</p>
<p>Overall, I&rsquo;m simultaneously full of ideas and annoyed.</p>
<hr>
<p><em>None of this blog post was written by ChatGPT, aside from the indicated ChatGPT API demos. My writing style is too weird for an AI to synthesize.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>Run Any Scheduled Task/Cron Super-Cheap on Google Cloud Platform</title>
      <link>https://minimaxir.com/2018/11/cheap-cron/</link>
      <pubDate>Mon, 19 Nov 2018 09:00:00 -0700</pubDate>
      <guid>https://minimaxir.com/2018/11/cheap-cron/</guid>
      <description>Thanks to a few new synergies within GCP products, it&amp;rsquo;s possible to get the cost of running a scheduled task down to less than a dollar a month.</description>
      <content:encoded><![CDATA[<p>Let&rsquo;s say you want to make a <a href="https://twitter.com">Twitter</a> bot to tweet out a custom message every few hours or so, and the free-tier VMs offered by cloud services with fractional virtual CPUs and little RAM aren&rsquo;t sufficient. How do you host the bot? Many suggest you get a <a href="https://www.digitalocean.com">Digital Ocean</a> VM for <a href="https://www.digitalocean.com/pricing/">$5/mo</a>, which is not a bad price. But what if you want to run <em>multiple</em> bots? How do you easily coordinate multiple scheduled tasks?</p>
<p>In my case, I maintain three bots: a bot which <a href="https://twitter.com/MTGIFening">tweets GIFs</a> superimposed onto Magic: The Gathering cards, a bot which <a href="https://twitter.com/hackernews_nn">tweets AI-generated Hacker News submission titles</a>, and a bot which makes <a href="https://www.reddit.com/r/subredditnn">AI-generated Reddit submissions</a>. I found a clever solution to the multiple-bots problem: leveraging <a href="https://cloud.google.com/kubernetes-engine/docs/how-to/cronjobs">CronJobs</a> with <a href="https://cloud.google.com/kubernetes-engine/">Google Kubernetes Engine</a> + a single worker node. Each bot has its own CronJob which tells when GKE should schedule a Job for each task, and then the cluster executes the Jobs whenever compute capacity is available (i.e. no resource hogging/race conditions), and can ensure completion by restarting the task if it fails.</p>
<figure>

    <img loading="lazy" srcset="/2018/11/cheap-cron/kubecron_hu_257738fd32526fcd.webp 320w,/2018/11/cheap-cron/kubecron_hu_60eaa4e7513fc57f.webp 768w,/2018/11/cheap-cron/kubecron_hu_51e1e5c783f05f18.webp 1024w,/2018/11/cheap-cron/kubecron.png 1132w" src="kubecron.png"/> 
</figure>

<p>The cost of running a cluster in GKE is just the cost of the compute: using a preemptible n1-standard-1 (1 vCPU/3.75 GB RAM) worker node VM, the <a href="https://cloud.google.com/compute/pricing">cost</a> is about <strong>$7.30/mo</strong>, a bit more than the Digital Ocean server but can theoretically handle an unlimited number of scheduled tasks. The problem is the worker node needs to be up 24/7 even though the bots run sporadically.</p>
<p>But thanks to a few new synergies within GCP products, it&rsquo;s possible to get the cost of running a scheduled task down to <em>less than a dollar a month</em>.</p>
<h2 id="gcp-shenanigans">GCP Shenanigans</h2>
<p><a href="https://cloud.google.com/blog/products/application-development/announcing-cloud-scheduler-a-modern-managed-cron-service-for-automated-batch-jobs">A couple weeks ago</a>, Google released <a href="https://cloud.google.com/scheduler/">Cloud Scheduler</a>, which is a managed cron service that can perform tasks for other Google services. With that launch, Google also released a tutorial titled <a href="https://cloud.google.com/scheduler/docs/scheduling-instances-with-cloud-scheduler">Scheduling Instances with Cloud Scheduler</a>, demonstrating how you can programmatically start and stop instances using Cloud Scheduler in conjunction with <a href="https://cloud.google.com/functions/">Cloud Functions</a>. The demo use case is to schedule VMs during business hours, which gave me an idea; could this approach be used to boot up a VM, run a script, and then shut it down to minimize uptime?</p>
<p>I followed the tutorial instructions, which contains code to create a Cloud Function which boots up a specified VM, code for a Cloud Function to shut down a specified VM, and how to create cron jobs in Cloud Scheduler to invoke those two Functions at a specified time. For my scheduled tasks, I only need the instance up for a couple minutes: for example we can use a Cloud Scheduler job to start an instance every 4 hours at X:00 with the cron <code>0 */4 * * *</code>, and shut it down 2 minutes later at X:02 with the cron <code>2 */4 * * *</code>.</p>
<p>The next step is configuring a Google Compute Engine VM to run the scheduled task on boot. There are two ways to go about it: one is to use the <code>startup-script</code> field when configuring a VM, which specifies a command to run on boot and gets the job done. Another approach (which I use) is to package the scheduled task as a <a href="https://www.docker.com/resources/what-container">Docker Container</a>, and use a container-optimized OS which simply runs a specified container upon boot (although you should set restart to <code>On Failure</code> and give <code>Privileged Access</code> to the container). Additionally, the VM can be configured as a preemptible instance for massive cost savings, as the &ldquo;shut-down-at-anytime&rdquo; constraint is irrelevant for this use case!</p>
<figure>

    <img loading="lazy" srcset="/2018/11/cheap-cron/vm_hu_60ea09aeebe0f061.webp 320w,/2018/11/cheap-cron/vm_hu_5bae417d9bdcd8ce.webp 768w,/2018/11/cheap-cron/vm_hu_1d7bf6105857ee53.webp 1024w,/2018/11/cheap-cron/vm.png 1066w" src="vm.png"/> 
</figure>

<p>After the VMs are created, I created the start/stop tasks targeting those VMs as noted in the tutorial.</p>
<figure>

    <img loading="lazy" srcset="/2018/11/cheap-cron/scheduler_hu_f3821e91f9e5b9b.webp 320w,/2018/11/cheap-cron/scheduler_hu_b50652bfa915e2ea.webp 768w,/2018/11/cheap-cron/scheduler_hu_4245bc81e840ba03.webp 1024w,/2018/11/cheap-cron/scheduler.png 1446w" src="scheduler.png"/> 
</figure>

<p>I can verify that this workflow indeed works for all my bots, and the crons have been running successfully at the specified time, for only a couple minutes!</p>
<figure>

    <img loading="lazy" srcset="/2018/11/cheap-cron/working_hu_ff6618b7926cdd5b.webp 320w,/2018/11/cheap-cron/working_hu_b32d4c5569f2af3a.webp 768w,/2018/11/cheap-cron/working_hu_4917f0b4d5f52f8a.webp 1024w,/2018/11/cheap-cron/working.png 1790w" src="working.png"/> 
</figure>

<h2 id="crunching-the-numbers">Crunching the Numbers</h2>
<p>This approach incorporates many different Google products. Is it <em>actually</em> cheaper than just maintaining a simple $5/month server? Let&rsquo;s calculate the monthly cost of all these services.</p>
<p>Assuming that we run a scheduled task every 4 hours, and the server is up for 2 minutes each time (i.e. 12 minutes of uptime a day):</p>
<ul>
<li><strong>Compute Engine</strong>: A preemptible n1-standard-1 is <a href="https://cloud.google.com/compute/pricing">$0.01 an hour</a>. <code>$0.01 / 60 * 12 * 30 = $0.06</code></li>
<li><strong>VM Persistent Disk</strong>: Each GB of storage for a VM costs <a href="https://cloud.google.com/compute/pricing#disk">$0.04/month</a>, and the minimum storage size is 10GB. <code>$0.04 * 10 = $0.40</code></li>
<li><strong>Cloud Scheduler</strong>: Each rule is <a href="https://cloud.google.com/scheduler/pricing">$0.10/month</a>, and there are both a start and a stop rule. <code>$0.10 * 2 = $0.20</code></li>
<li><strong>Cloud Functions</strong>: It takes about 60 seconds total to turn on and off a VM, and with the default 256MB provision, during which it costs <a href="https://cloud.google.com/functions/pricing">$.000000463/100ms</a>. <code>0.000000463 * 10 * 60 * 12 * 30 = $0.10</code></li>
</ul>
<p>$0.06 + $0.40 + $0.20 + $0.10 = <strong>$0.76/month to run the scheduled task</strong>! That&rsquo;s not even counting the free tier bonuses if you just want to create one scheduled task; in that case, the only price you pay is the $0.06/mo for the VM. And even in the case where you run the task every hour (like in the images above), the cost is $1.24/month; still not bad.</p>
<p>It&rsquo;s worth noting that these pricing economics wouldn&rsquo;t have worked years ago. Back then <a href="https://aws.amazon.com">Amazon Web Services</a>, the leader in web services, charged for a minimum of 1 hour every time a VM was booted. Google Compute Engine innovated by only requiring a minimum of 10 minutes, which is much better but still would have had unnecessary overhead (in this example, it would increase compute monthly costs by $0.24/31%). <a href="https://cloud.google.com/blog/products/gcp/extending-per-second-billing-in-google">As of September 2017</a>, Google Compute Engine charges a minimum of <strong>1 minute</strong>, which makes this workflow possible and cheap (AWS made the same change <a href="https://aws.amazon.com/blogs/aws/new-per-second-billing-for-ec2-instances-and-ebs-volumes/">a week earlier</a>).</p>
<p>It&rsquo;s also possible that similar workflows exist for AWS and <a href="https://azure.microsoft.com/en-us/">Azure Cloud</a>, although I&rsquo;m less familiar with those platforms (and it may not necessarily be better/cheaper). Sure, if you have a very simple task to practice making bots in the cloud, the free tier of any cloud service might suffice (where you run the server all the time, and schedule the cron on the server itself). If you&rsquo;re planning many scheduled tasks, then a centralized approach like my initial Kubernetes implementation might actually be more cost effective. But if you&rsquo;re somewhere <em>in between</em>, then giving each scheduled task its own VM makes more sense for both ease of use and cost-effectiveness. And there&rsquo;s still many further optimizations to be done too (for example, by allowing the script in the VM to ping a HTTP Cloud Function endpoint and shut <em>itself</em> off when complete instead of using a scheduled cron rule).</p>
]]></content:encoded>
    </item>
    <item>
      <title>Benchmarking Modern GPUs for Maximum Cloud Cost Efficiency in Deep Learning</title>
      <link>https://minimaxir.com/2017/11/benchmark-gpus/</link>
      <pubDate>Tue, 28 Nov 2017 08:30:00 -0700</pubDate>
      <guid>https://minimaxir.com/2017/11/benchmark-gpus/</guid>
      <description>A 36% price cut to GPU instances, in addition to the potential new benefits offered by software and GPU updates, however, might be enough to tip the cost-efficiency scales back in favor of GPUs.</description>
      <content:encoded><![CDATA[<p>A few months ago, I <a href="http://minimaxir.com/2017/06/keras-cntk/">performed benchmarks</a> of deep learning frameworks in the cloud, with a <a href="http://minimaxir.com/2017/07/cpu-or-gpu/">followup</a> focusing on the cost difference between using GPUs and CPUs. And just a few months later, the landscape has changed, with significant updates to the low-level <a href="https://developer.nvidia.com/cudnn">NVIDIA cuDNN</a> library which powers the raw learning on the GPU, the <a href="https://www.tensorflow.org">TensorFlow</a> and <a href="https://github.com/Microsoft/CNTK">CNTK</a> deep learning frameworks, and the higher-level <a href="https://github.com/fchollet/keras">Keras</a> framework which uses TensorFlow/CNTK as backends for easy deep learning model training.</p>
<p>As a bonus to the framework updates, Google <a href="https://cloudplatform.googleblog.com/2017/09/introducing-faster-GPUs-for-Google-Compute-Engine.html">recently released</a> the newest generation of NVIDIA cloud GPUs, the Pascal-based P100, onto <a href="https://cloud.google.com/compute/">Google Compute Engine</a> which touts an up-to-10x performance increase to the current K80 GPUs used in cloud computing. As a bonus bonus, Google recently <a href="https://cloudplatform.googleblog.com/2017/11/new-lower-prices-for-GPUs-and-preemptible-Local-SSDs.html">cut the prices</a> of both K80 and P100 GPU instances by up to 36%.</p>
<p>The results of my earlier benchmarks favored <a href="https://cloud.google.com/preemptible-vms/">preemptible</a> instances with many CPUs as the most cost efficient option (where a preemptable instance can only last for up to 24 hours and could end prematurely). A 36% price cut to GPU instances, in addition to the potential new benefits offered by software and GPU updates, however, might be enough to tip the cost-efficiency scales back in favor of GPUs. It&rsquo;s a good idea to rerun the experiment with updated VMs and see what happens.</p>
<h2 id="benchmark-setup">Benchmark Setup</h2>
<p>As with the original benchmark, I set up a <a href="https://github.com/minimaxir/keras-cntk-docker">Docker container</a> containing the deep learning frameworks (based on cuDNN 6, the latest version of cuDNN natively supported by the frameworks) that can be used to train each model independently. The <a href="https://github.com/minimaxir/keras-cntk-benchmark/tree/master/v2/test_files">Keras benchmark scripts</a> run on the containers are based off of <em>real world</em> use cases of deep learning.</p>
<p>The 6 hardware/software configurations and Google Compute Engine <a href="https://cloud.google.com/compute/pricing">pricings</a> for the tests are:</p>
<ul>
<li>A K80 GPU (attached to a <code>n1-standard-1</code> instance), tested with both TensorFlow (1.4) and CNTK (2.2): <strong>$0.4975 / hour</strong>.</li>
<li>A P100 GPU (attached to a <code>n1-standard-1</code> instance), tested with both TensorFlow and CNTK: <strong>$1.5075 / hour</strong>.</li>
<li>A preemptable <code>n1-highcpu-32</code> instance, with 32 vCPUs based on the Intel Skylake architecture, tested with TensorFlow only: <strong>$0.2400 / hour</strong></li>
<li>A preemptable <code>n1-highcpu-16</code> instance, with 16 vCPUs based on the Intel Skylake architecture, tested with TensorFlow only: <strong>$0.1200 / hour</strong></li>
</ul>
<p>A single K80 GPU uses 1/2 a GPU board while a single P100 uses a full GPU board, which in an ideal world would suggest that the P100 is twice as fast at the K80 at minimum. But even so, the P100 configuration is about 3 times as expensive, so even if a model is trained in half the time, it may not necessarily be cheaper with the P100.</p>
<p>Also, the CPU tests use TensorFlow <em>as installed via the recommended method</em> through pip, since compiling the TensorFlow binary from scratch to take advantage of CPU instructions as <a href="http://minimaxir.com/2017/07/cpu-or-gpu/">with my previous test</a> is not a pragmatic workflow for casual use.</p>
<h2 id="benchmark-results">Benchmark Results</h2>
<p>When a fresh-out-of-a-AI-MOOC engineer wants to experiment with deep learning in the cloud, typically they use a K80 + TensorFlow setup, so we&rsquo;ll use that as the <em>base configuration</em>.</p>
<p>For each model architecture and software/hardware configuration, I calculate the <strong>total training time relative to the base configuration instance training</strong> for running the model training for the provided test script. In all cases, the P100 GPU <em>should</em> perform better than the K80, and 32 vCPUs <em>should</em> train faster than 16 vCPUs. The question is how <em>much</em> faster?</p>
<p>Let&rsquo;s start using the <a href="http://yann.lecun.com/exdb/mnist/">MNIST dataset</a> of handwritten digits plus the common multilayer perceptron (MLP) architecture, with dense fully-connected layers. Lower training time is better.</p>
<figure>

    <img loading="lazy" srcset="/2017/11/benchmark-gpus/dl-cpu-gpu-5_hu_df63751b48270991.webp 320w,/2017/11/benchmark-gpus/dl-cpu-gpu-5_hu_33351b8d5d2916d3.webp 768w,/2017/11/benchmark-gpus/dl-cpu-gpu-5_hu_773ee4a74d2ce535.webp 1024w,/2017/11/benchmark-gpus/dl-cpu-gpu-5.png 1200w" src="dl-cpu-gpu-5.png"/> 
</figure>

<p>For this task, CNTK appears to be more effective than TensorFlow. Indeed, the P100 is faster than the K80 for the corresponding framework, although it&rsquo;s not a dramatic difference. However, since the task is simple, the CPU performance is close to that of the GPU, which implies that the GPU is not as cost effective for a simple architecture.</p>
<p>For each model architecture and configuration, I calculate a <strong>normalized training cost relative to the cost of the base configuration training</strong>. Because GCE instance costs are prorated, we can simply calculate experiment cost by multiplying the total number of seconds the experiment runs by the cost of the instance (per second).</p>
<figure>

    <img loading="lazy" srcset="/2017/11/benchmark-gpus/dl-cpu-gpu-6_hu_8092aa4efa0c4355.webp 320w,/2017/11/benchmark-gpus/dl-cpu-gpu-6_hu_6ec85d77120003f7.webp 768w,/2017/11/benchmark-gpus/dl-cpu-gpu-6_hu_3fa9ff93fed554d5.webp 1024w,/2017/11/benchmark-gpus/dl-cpu-gpu-6.png 1200w" src="dl-cpu-gpu-6.png"/> 
</figure>

<p>Unsurprisingly, CPUs are more cost effective. However, the P100 is more cost <em>ineffective</em> for this task than the K80.</p>
<p>Now, let&rsquo;s look at the same dataset with a convolutional neural network (CNN) approach for digit classification. Since CNNs are typically used for computer vision tasks, new graphic card architectures are optimized for CNN workflows, so it will be interesting to see how the P100 performs compared to the K80:</p>
<figure>

    <img loading="lazy" srcset="/2017/11/benchmark-gpus/dl-cpu-gpu-7_hu_f8361510000c69ef.webp 320w,/2017/11/benchmark-gpus/dl-cpu-gpu-7_hu_a5e4bb39cb0f4851.webp 768w,/2017/11/benchmark-gpus/dl-cpu-gpu-7_hu_13b371e4d8afa6c9.webp 1024w,/2017/11/benchmark-gpus/dl-cpu-gpu-7.png 1200w" src="dl-cpu-gpu-7.png"/> 
</figure>

<figure>

    <img loading="lazy" srcset="/2017/11/benchmark-gpus/dl-cpu-gpu-8_hu_f4a994fcdbd47c8f.webp 320w,/2017/11/benchmark-gpus/dl-cpu-gpu-8_hu_94b3b6c80d09cc47.webp 768w,/2017/11/benchmark-gpus/dl-cpu-gpu-8_hu_ca2831240a30c8c.webp 1024w,/2017/11/benchmark-gpus/dl-cpu-gpu-8.png 1200w" src="dl-cpu-gpu-8.png"/> 
</figure>

<p>Indeed, the P100 is twice as fast and the K80, but due to the huge cost premium, it&rsquo;s not cost effective for this simple task. However, CPUs do not perform well on this task either, so notably the base configuration is the best configuration.</p>
<p>Let&rsquo;s go deeper with CNNs and look at the <a href="https://www.cs.toronto.edu/%7Ekriz/cifar.html">CIFAR-10</a> image classification dataset, and a model which utilizes a deep covnet + a multilayer perceptron and ideal for image classification (similar to the <a href="https://gist.github.com/baraldilorenzo/07d7802847aaad0a35d3">VGG-16</a> architecture).</p>
<figure>

    <img loading="lazy" srcset="/2017/11/benchmark-gpus/dl-cpu-gpu-9_hu_3e89a9d69d2114d8.webp 320w,/2017/11/benchmark-gpus/dl-cpu-gpu-9_hu_188420deeffa2cca.webp 768w,/2017/11/benchmark-gpus/dl-cpu-gpu-9_hu_2994e1dc8b68f244.webp 1024w,/2017/11/benchmark-gpus/dl-cpu-gpu-9.png 1200w" src="dl-cpu-gpu-9.png"/> 
</figure>

<figure>

    <img loading="lazy" srcset="/2017/11/benchmark-gpus/dl-cpu-gpu-10_hu_4c8240dc9addd1a4.webp 320w,/2017/11/benchmark-gpus/dl-cpu-gpu-10_hu_e38edfb433bf8413.webp 768w,/2017/11/benchmark-gpus/dl-cpu-gpu-10_hu_a879b46166fddc6d.webp 1024w,/2017/11/benchmark-gpus/dl-cpu-gpu-10.png 1200w" src="dl-cpu-gpu-10.png"/> 
</figure>

<p>Similar results to that of a normal MLP. Nothing fancy.</p>
<p>The Bidirectional long-short-term memory (LSTM) architecture is great for working with text data like IMDb reviews. When I did <a href="http://minimaxir.com/2017/06/keras-cntk/">my first benchmark article</a>, I noticed that CNTK performed significantly better than TensorFlow, as <a href="https://news.ycombinator.com/item?id=14538086">commenters on Hacker News</a> noted that TensorFlow uses an inefficient implementation of the LSTM on the GPU.</p>
<figure>

    <img loading="lazy" srcset="/2017/11/benchmark-gpus/cntk-old_hu_b86c227c88de2e7d.webp 320w,/2017/11/benchmark-gpus/cntk-old_hu_3901dc880777da18.webp 768w,/2017/11/benchmark-gpus/cntk-old_hu_8d49b907914bb06b.webp 1024w,/2017/11/benchmark-gpus/cntk-old.png 1620w" src="cntk-old.png"/> 
</figure>

<p>However, with Keras&rsquo;s <a href="https://keras.io/layers/recurrent/#cudnnlstm">new CuDNNRNN layers</a> which leverage cuDNN, this inefficiency may be fixed, so for the K80/P100 TensorFlow GPU configs, I use a CuDNNLSTM layer instead of a normal LSTM layer. So let&rsquo;s take another look:</p>
<figure>

    <img loading="lazy" srcset="/2017/11/benchmark-gpus/dl-cpu-gpu-1_hu_f633549e7615557a.webp 320w,/2017/11/benchmark-gpus/dl-cpu-gpu-1_hu_c8eb1a82936955a7.webp 768w,/2017/11/benchmark-gpus/dl-cpu-gpu-1_hu_734746132ba497c3.webp 1024w,/2017/11/benchmark-gpus/dl-cpu-gpu-1.png 1200w" src="dl-cpu-gpu-1.png"/> 
</figure>

<figure>

    <img loading="lazy" srcset="/2017/11/benchmark-gpus/dl-cpu-gpu-2_hu_6f0e2078d0fbe4a8.webp 320w,/2017/11/benchmark-gpus/dl-cpu-gpu-2_hu_f5299cfcd4184de5.webp 768w,/2017/11/benchmark-gpus/dl-cpu-gpu-2_hu_9c9b4dbee5321cd.webp 1024w,/2017/11/benchmark-gpus/dl-cpu-gpu-2.png 1200w" src="dl-cpu-gpu-2.png"/> 
</figure>

<p><em>WOAH.</em> TensorFlow is now more than <em>three times as fast</em> than CNTK! (And compared against my previous benchmark, TensorFlow on the K80 w/ the CuDNNLSTM is about <em>7x as fast</em> as it once was!) Even the CPU-only versions of TensorFlow are faster than CNTK on the GPU now, which implies significant improvements in the ecosystem outside of the CuDNNLSTM layer itself. (And as a result, CPUs are still more cost efficient)</p>
<p>Lastly, LSTM text generation of <a href="https://en.wikipedia.org/wiki/Friedrich_Nietzsche">Nietzsche&rsquo;s</a> <a href="https://s3.amazonaws.com/text-datasets/nietzsche.txt">writings</a> follows similar patterns to the other architectures, but without the drastic hit to the GPU.</p>
<figure>

    <img loading="lazy" srcset="/2017/11/benchmark-gpus/dl-cpu-gpu-11_hu_e64be99549e22a4a.webp 320w,/2017/11/benchmark-gpus/dl-cpu-gpu-11_hu_c9e45139e2d4d36b.webp 768w,/2017/11/benchmark-gpus/dl-cpu-gpu-11_hu_73f05d523cc746fa.webp 1024w,/2017/11/benchmark-gpus/dl-cpu-gpu-11.png 1200w" src="dl-cpu-gpu-11.png"/> 
</figure>

<figure>

    <img loading="lazy" srcset="/2017/11/benchmark-gpus/dl-cpu-gpu-12_hu_18c099feff0cab3f.webp 320w,/2017/11/benchmark-gpus/dl-cpu-gpu-12_hu_346cce6ac1dd882a.webp 768w,/2017/11/benchmark-gpus/dl-cpu-gpu-12_hu_784cadffdd30380.webp 1024w,/2017/11/benchmark-gpus/dl-cpu-gpu-12.png 1200w" src="dl-cpu-gpu-12.png"/> 
</figure>

<h2 id="conclusions">Conclusions</h2>
<p>The biggest surprise of these new benchmarks is that there is no configuration where the P100 is the most cost-effective option, even though the P100 is indeed faster than the K80 in all tests. Although per <a href="https://developer.nvidia.com/cudnn">the cuDNN website</a>, there is apparently only a 2x speed increase between the performance of the K80 and P100 using cuDNN 6, which is mostly consistent with the results of my benchmarks:</p>
<figure>

    <img loading="lazy" srcset="/2017/11/benchmark-gpus/cudnn_hu_354d8fa8ab3eff29.webp 320w,/2017/11/benchmark-gpus/cudnn_hu_bb346ea37595e154.webp 768w,/2017/11/benchmark-gpus/cudnn_hu_9b3f6e3ea7ba3a02.webp 1024w,/2017/11/benchmark-gpus/cudnn.png 1688w" src="cudnn.png"/> 
</figure>

<p>I did not include a multi-GPU configuration in the benchmark data visualizations above using Keras&rsquo;s new <code>multi_gpu_model</code> <a href="https://keras.io/utils/#multi_gpu_model">function</a> because in my testing, the multi-GPU training <em>was equal to or worse than a single GPU</em> in all tests.</p>
<p>Taking these together, it&rsquo;s possible that the overhead introduced by parallel, advanced architectures <em>eliminates the benefits</em> for &ldquo;normal&rdquo; deep learning workloads which do not fully saturate the GPU. Rarely do people talk about diminishing returns in GPU performance with deep learning.</p>
<p>In the future, I want to benchmark deep learning performance against more advanced deep learning use cases such as <a href="https://en.wikipedia.org/wiki/Reinforcement_learning">reinforcement learning</a> and deep CNNs like <a href="https://github.com/tensorflow/models/tree/master/research/inception">Inception</a>. But that doesn&rsquo;t mean these benchmarks are not relevant; as stated during the benchmark setup, the GPUs were tested against typical deep learning use cases, and now we see the tradeoffs that result.</p>
<p>In all, with the price cuts on GPU instances, cost-performance is often <em>on par</em> with preemptable CPU instances, which is an advantage if you want to train models faster and not risk the instance being killed unexpectedly. And there is still a lot of competition in this space: <a href="https://www.amazon.com">Amazon</a> offers a <code>p2.xlarge</code> <a href="https://aws.amazon.com/ec2/spot/">Spot Instance</a> with a K80 GPU for $0.15-$0.20 an hour, less than half of the corresponding Google Compute Engine K80 GPU instance, although with <a href="https://aws.amazon.com/ec2/spot/details/">a few bidding caveats</a> which I haven&rsquo;t fully explored yet. Competition will drive GPU prices down even further, and training deep learning models will become even easier.</p>
<p>And as the cuDNN chart above shows, things will get <em>very</em> interesting once Volta-based GPUs like the V100 are generally available and the deep learning frameworks support cuDNN 7 by default.</p>
<p><strong>UPDATE 12/17</strong>: <em>As pointed out by <a href="https://news.ycombinator.com/item?id=15941682">dantiberian on Hacker News</a>, Google Compute Engine now supports <a href="https://cloud.google.com/compute/docs/instances/preemptible#preemptible_with_gpu">preemptible GPUs</a>, which was apparently added after this post went live. Preemptable GPUs are exactly half the price of normal GPUs (for both K80s and P100s; $0.73/hr and $0.22/hr respectively), so they&rsquo;re about double the cost efficiency (when factoring in the cost of the base preemptable instance), which would put them squarely ahead of CPUs in all cases. (and since the CPU instances used here were also preemptable, it&rsquo;s apples-to-apples)</em></p>
<hr>
<p><em>All scripts for running the benchmark are available in <a href="https://github.com/minimaxir/keras-cntk-benchmark/tree/master/v2">this GitHub repo</a>. You can view the R/ggplot2 code used to process the logs and create the visualizations in <a href="http://minimaxir.com/notebooks/benchmark-gpus/">this R Notebook</a>.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>Making Magic: the GIFening</title>
      <link>https://minimaxir.com/2017/11/magic-the-gifening/</link>
      <pubDate>Tue, 07 Nov 2017 08:10:00 -0700</pubDate>
      <guid>https://minimaxir.com/2017/11/magic-the-gifening/</guid>
      <description>As it turns out, creating a Twitter bot to tweet Magic card GIFs is easy to implement, but with a few interesting caveats.</description>
      <content:encoded><![CDATA[<p>After working at <a href="https://www.buzzfeed.com/">BuzzFeed</a> for a few months, I&rsquo;m now an expert in the proper usage of GIFs. My favorite GIF tool is the <a href="https://giphy.com">/giphy</a> command in <a href="https://slack.com">Slack</a>, which <a href="https://get.slack.help/hc/en-us/articles/204714258-Add-Giphy-search-to-Slack">puts a random GIF</a> according to a given phrase into the chat, with better-than-expected appropriateness of the phrase to the GIF.</p>
<p>Completely unrelated, I recently rediscovered <a href="https://github.com/Zulko/moviepy">MoviePy</a>, a Python library for programmatically editing videos and GIFs without requiring an expensive and slow video editing program. I had played with MoviePy a bit in 2014 when it was <a href="http://zulko.github.io/blog/2014/01/23/making-animated-gifs-from-video-files-with-python/">first released</a> and <a href="https://news.ycombinator.com/item?id=7121104">became viral</a>, but couldn&rsquo;t think of a creative application for the library at the time.</p>
<p>On a boring weekend I had a silly idea: why not create a program to superimpose appropriate GIFs onto <a href="https://magic.wizards.com/en">Magic: the Gathering</a> cards using these two tools? And even better, why not <em>automate</em> both the creation of the card GIFs and the tweeting of a new GIF every few hours?</p>
<p><span><blockquote class="twitter-tweet" data-lang="en"><p lang="und" dir="ltr"><a href="https://t.co/sxqKUYHmfv">pic.twitter.com/sxqKUYHmfv</a></p>— Magic: The GIFening (@MTGIFening) <a href="https://twitter.com/MTGIFening/status/913993793052880897?ref_src=twsrc%5Etfw">September 30, 2017</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> </span></p>
<p>As it turns out, creating a Twitter bot to tweet Magic card GIFs is <a href="https://github.com/minimaxir/magic-the-gifening">easy to implement</a>, but with a few interesting caveats. The end result is <a href="https://twitter.com/MTGIFening">@MTGIFening</a>. Here&rsquo;s how I typically create my crazy apps, step by step.</p>
<h2 id="feasibility-analysis">Feasibility Analysis</h2>
<p>Like all my data analysis projects, I checked if it&rsquo;s possible to complete the project in a way that won&rsquo;t suck up a lot of free time hacking out convoluted solutions.</p>
<p><strong>Can I easily get a list of all Magic cards?</strong> Yes, via <a href="https://mtgjson.com">MTG JSON</a>, which has a downloadable JSON dump of all Magic cards.</p>
<p><strong>Can I easily get random GIFs from GIPHY?</strong> Yes, there is a /random endpoint in the <a href="https://developers.giphy.com">GIPHY API</a> which returns a random GIF for a specified phrase, like the /giphy Slack command. The GIPHY API requires registration, but has generous rate limits (10k requests/day).</p>
<p><strong>Can I easily composite a GIF onto an image with MoviePy?</strong> Yes, compositing is a <em>primary use case</em> for the library, with many tutorials in the documentation.</p>
<p><strong>Can I easily get an image for a specified Magic card?</strong> Unsure. The official tool for viewing Magic card images is <a href="http://gatherer.wizards.com/Pages/Default.aspx">Gatherer</a>. After checking the image source for the cards, each card image in Gatherer has a URL that follows this schema: <code>http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=XXXXX&amp;type=card</code>. That&rsquo;s easy to understand, but what&rsquo;s a multiverseid?</p>
<p><strong>Is there a mapping of multiverseid to Magic cards from MTG JSON?</strong> Yes, the multiverseid for each Magic card is <a href="https://mtgjson.com/documentation.html">present as a field</a> in the &ldquo;All Sets&rdquo; dataset (but not the &ldquo;All Cards&rdquo; dataset oddly). A quick manual check showed that using the multiverseid from the MTG JSON dataset results in the correct image from Gatherer.</p>
<p>Everything looked good to me. Let&rsquo;s dive right in, <a href="https://github.com/minimaxir/magic-the-gifening/commits/master">commit by commit</a>.</p>
<h2 id="implementing-magic-the-gifening">Implementing Magic: The GIFening</h2>
<p>The first thing I did was process the Magic card data, although for this project I limit the type of cards to Instants and Sorceries, which in Magic game mechanics represent &ldquo;actions&rdquo; and are more suitable for GIFs. <em>For each set, retrieve the cards in the set; for each card, if it&rsquo;s an Instant/Sorcery, log its name and multiverseid</em>. Thanks to the magic of Python, this pseudocode is close to the <a href="https://github.com/minimaxir/magic-the-gifening/commit/3f626ae5d49a567322c6237210ab554281d462f4">actual code</a>.</p>
<p>The next objective was to implement the GIPHY API to get a GIF. The very first thing I did is add a local secrets file containing my personal API key for GIPHY, and <em>immediately</em> log the secrets file in a <code>.gitignore</code> so I don&rsquo;t accidentally leak it. GIPHY has an <a href="https://developers.giphy.com/explorer/">API Explorer</a> which allows developers to quickly test an example input phrase and see corresponding output from the API. For example, here&rsquo;s part of what the API returns for <a href="http://gatherer.wizards.com/Pages/Card/Details.aspx?multiverseid=151108">Invert the Skies</a> (although since it&rsquo;s the /random endpoint, your results may vary):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="s2">&#34;image_url&#34;</span><span class="err">:</span> <span class="s2">&#34;https://media1.giphy.com/media/plbsEwLwQvzLa/giphy.gif&#34;</span><span class="err">,</span>
</span></span><span class="line"><span class="cl"><span class="s2">&#34;image_mp4_url&#34;</span><span class="err">:</span> <span class="s2">&#34;https://media1.giphy.com/media/plbsEwLwQvzLa/giphy.mp4&#34;</span><span class="err">,</span>
</span></span><span class="line"><span class="cl"><span class="s2">&#34;image_frames&#34;</span><span class="err">:</span> <span class="s2">&#34;31&#34;</span><span class="err">,</span>
</span></span><span class="line"><span class="cl"><span class="s2">&#34;image_width&#34;</span><span class="err">:</span> <span class="s2">&#34;480&#34;</span><span class="err">,</span>
</span></span><span class="line"><span class="cl"><span class="s2">&#34;image_height&#34;</span><span class="err">:</span> <span class="s2">&#34;270&#34;</span><span class="err">,</span>
</span></span></code></pre></div><p>The <code>image_url</code> corresponds to the <a href="https://media1.giphy.com/media/plbsEwLwQvzLa/giphy.gif">raw GIF</a> unsurprisingly, but as a bonus, GIPHY also includes a link to a <a href="https://media1.giphy.com/media/plbsEwLwQvzLa/giphy.mp4">MP4 video</a> of the GIF, which has a much smaller file size and is better to use for compositing. The API output also includes the width and height (in pixels) of the GIF. The art in a Magic card follows a 4:3 <a href="https://en.wikipedia.org/wiki/Aspect_ratio_%28image%29">aspect ratio</a>, i.e. the width divided by the height equals 1.33. If the dimensions of the GIF are too far outside that ratio, resizing the GIFs to fit the Magic art frame will result in noticeable distortion. I minimized this distortion by checking and seeing if the random GIF has a width:height ratio between 1.2 and 1.6 before accepting it. Since there&rsquo;s a chance for failure (along with potential unknown bugs that the random GIF could hit), I added a limit to the number of attempts to retrieve an appropriate GIF. All done in <a href="https://github.com/minimaxir/magic-the-gifening/commit/c2e4b6b9d58d1aa360f6f67a049ec962d0430b91">one commit</a>.</p>
<p>Getting the card image from Gatherer is <a href="https://github.com/minimaxir/magic-the-gifening/commit/c92440cd459640da9346cf31a79e768ac8641ea9">trivial</a>, so then I worked on combining the GIF and the card image. MoviePy has a <a href="http://zulko.github.io/moviepy/getting_started/compositing.html">good tutorial</a> for specifying the position of one clip onto another by specifying the upper-left corner of the bottom-image where the GIF will be placed, while simultaneously resizing the GIF to a given width and height.</p>
<figure>

    <img loading="lazy" srcset="/2017/11/magic-the-gifening/videoWH_hu_c7b4dfa4c1762942.webp 320w,/2017/11/magic-the-gifening/videoWH.jpeg 500w" src="videoWH.jpeg"/> 
</figure>

<p>I manually zoomed into the card image using a photo editor (<a href="http://www.pixelmator.com/mac/">Pixelmator</a>) to find the upper-left corner of the card art:</p>
<figure>

    <img loading="lazy" srcset="/2017/11/magic-the-gifening/zoomin_hu_d918691f6a0c5282.webp 320w,/2017/11/magic-the-gifening/zoomin_hu_a1316b8e5b5ae555.webp 768w,/2017/11/magic-the-gifening/zoomin_hu_9c398593d95acc27.webp 1024w,/2017/11/magic-the-gifening/zoomin.png 1199w" src="zoomin.png"/> 
</figure>

<p>In this case, the pixel coordinates for the upper-left corner of the card art is <code>(17,35)</code> The upper-right and bottom-left corners can be used to determine the target width and height of the GIF respectively, and can be found the same way. Simply composite the Magic card with the resized-and-positioned GIF, set the duration of the &ldquo;new&rdquo; GIF to that of the source GIF, and <code>write_gif</code>. <a href="https://github.com/minimaxir/magic-the-gifening/commit/55a52ddfc7f43d128c08c8a243254e08a171de5e">That&rsquo;s that</a>!</p>
<p>To finish things up, I wrote a script to load all the cards from the processed card list into memory, select a card at random, use the helper functions to retrieve a GIPHY GIF and composite it with the card, then upload the resulting GIF to Twitter. I haven&rsquo;t worked with the Twitter API in awhile; a quick Google search for a modern Twitter API client in Python returns <a href="https://github.com/ryanmcgrath/twython">Twython</a>, which conveniently includes an example on <a href="https://twython.readthedocs.io/en/latest/usage/advanced_usage.html#updating-status-with-image">how to upload an image to Twitter</a>! And after running the script a few times, the full workflow indeed works!</p>
<p>Not bad for a couple hours of scripting. But I was not close to finished.</p>
<h2 id="the-endless-fun-of-qa">The Endless Fun of QA</h2>
<p>One of the reasons I enjoy doing silly projects (especially silly data projects) is because I tend to hit unsexy edge cases which typical development blogs and tutorials rarely discuss. In this case, I quickly found that the Twitter API has a <a href="https://developer.twitter.com/en/docs/media/upload-media/overview">5 MB limit</a> on image uploads, which is a problem as the resulting GIFs are huge and often randomly exceed that limit (looking back on it, there is a different endpoint intended for GIF uploads, counterintuitively).</p>
<p>In actuality, GIFs on Twitter are actually displayed as videos, in order to save bandwidth. Since Twitter transcodes uploaded GIFs anyways, it makes more sense to upload <em>audioless videos</em> instead of GIFs (and as a bonus, after the death of Vine, Twitter will auto-loop videos less than 6 seconds).</p>
<p>Creating videos is easy to do with MoviePy, just do a <code>write_videofile</code> instead of <code>write_gif</code>, and use Twython&rsquo;s video uploading example to upload. The result is an &ldquo;unknown&rdquo; error on upload. I verify by uploading the video manually to Twitter&hellip;and the Twitter UI fails to recognize it as a video. But the video itself plays fine in QuickTime. This is the annoying type of coding problem that&rsquo;s too specific for <a href="https://stackoverflow.com">Stack Overflow</a> to provide help. After a bit of trial and error involving video codecs and settings, the solution was to pass a <code>-pix_fmt yuv420p</code> parameter to the video encoder because Twitter apparently only likes legacy video container formats. Oh well. It worked, and both Twitter manual and API uploads worked successfully.</p>
<p>I also ran into an issue where Twitter refused to accept supershort video, where the source GIF was only a couple frames. A solution is to loop the GIF to atleast 2 seconds if it&rsquo;s shorter, which somehow fixed that problem.</p>
<p>(As I was writing this post a month later, I discovered that both of these video upload constraints <a href="https://developer.twitter.com/en/docs/media/upload-media/uploading-media/media-best-practices">are indeed covered in the Twitter documentation</a>, which makes me look very silly in retrospect!)</p>
<p>These changes fixed most of the upload issues. However, when writing the initial script, I forgot that the borders of Magic cards have <a href="https://mtg.gamepedia.com/Card_frame">changed over the years</a>, which also changed the position and size of the card art. <strong>Is there a way to check when a card was printed?</strong> Yes, the &ldquo;All Sets&rdquo; dataset contains the release date of the set, so with that, I can <a href="https://github.com/minimaxir/magic-the-gifening/commit/0dfb678f1955f50b54b632e57087df847ec16f05">hard code</a> the dates of sets where the borders changed, and note the border type at printing time. I then used Pixelmator again to note the new art dimensions for that type of border, and used conditional statements to retrieve the correct dimensions for the type of border when compositing.</p>
<p>Lastly, I added general <code>try/catch</code> error handling to prevent the script from breaking fatally and to try again with a different card if it does. That covers most of the edge cases!</p>
<h2 id="results">Results</h2>
<p>After running the script many times after all the fixes in place, I felt the Twitter account was good to go. The initial results showed a lot of promise:</p>
<p><span><blockquote class="twitter-tweet" data-lang="en"><p lang="und" dir="ltr"><a href="https://t.co/BsZ7eIcunl">pic.twitter.com/BsZ7eIcunl</a></p>— Magic: The GIFening (@MTGIFening) <a href="https://twitter.com/MTGIFening/status/913981726182981632?ref_src=twsrc%5Etfw">September 30, 2017</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script></span></p>
<p><span><blockquote class="twitter-tweet" data-lang="en"><p lang="und" dir="ltr"><a href="https://t.co/picJJk6mBm">pic.twitter.com/picJJk6mBm</a></p>— Magic: The GIFening (@MTGIFening) <a href="https://twitter.com/MTGIFening/status/912525635632775168?ref_src=twsrc%5Etfw">September 26, 2017</a></blockquote></p>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script></span>
<p>Surprisingly, the script was able to generate <em>visual puns</em> in cards, completely by chance!</p>
<p><span><blockquote class="twitter-tweet" data-lang="en"><p lang="und" dir="ltr"><a href="https://t.co/AnpzU8xVho">pic.twitter.com/AnpzU8xVho</a></p>— Magic: The GIFening (@MTGIFening) <a href="https://twitter.com/MTGIFening/status/913972922330497024?ref_src=twsrc%5Etfw">September 30, 2017</a></blockquote></p>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script></span>
<p><span><blockquote class="twitter-tweet" data-lang="en"><p lang="und" dir="ltr"><a href="https://t.co/01vGRcq2Mj">pic.twitter.com/01vGRcq2Mj</a></p>— Magic: The GIFening (@MTGIFening) <a href="https://twitter.com/MTGIFening/status/913987002235740160?ref_src=twsrc%5Etfw">September 30, 2017</a></blockquote></p>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script></span>
<p>The next step was to automate the script to run and post Tweets at a specific time interval. After experimenting a bit, I found that <a href="https://github.com/minimaxir/magic-the-gifening/commit/f355d5e80503c67c6e1a0e5fd1b744faf3cf8223">the best solution</a> was to use a <a href="https://en.wikipedia.org/wiki/Cron">cron job</a> in a <a href="https://www.docker.com/what-docker">Docker container</a> containing the script and its dependencies, for complicated reasons which will require another blog post to explain.</p>
<p>After letting Magic: the GIFening run for a few days without fatal issues, I decided to publicize the Twitter account and posted it to the <a href="https://www.reddit.com/r/magicTCG/comments/7598g5/i_made_a_twitter_bot_which_tweets_magic_cards/">/r/MagicTCG subreddit</a> and <a href="https://news.ycombinator.com/item?id=15449955">Hacker News</a>. To my surprise, the project performed extremely well on both with 100+ upvotes on each, and the <a href="https://github.com/minimaxir/magic-the-gifening">GitHub repo</a> itself received 100+ Stars.</p>
<p>In all, making Magic: the GIFening was a fun project. In retrospect, talking though the commits made me realize I performed many bad coding practices in a haste to get the project done ASAP (specifically, checking to see if certain edge cases are documented, violating <a href="https://en.wikipedia.org/wiki/Don%27t_repeat_yourself">DRY</a>, and forgetting to remove specific types of cards like <a href="https://twitter.com/MTGIFening/status/924969160307744769">split cards</a>). Obviously there isn&rsquo;t a multimillion-dollar startup opportunity in creating random GIFs of Magic cards, but I&rsquo;ll fix a few remaining issues and keep the Twitter bot running.</p>
]]></content:encoded>
    </item>
    <item>
      <title>How to Make High Quality Data Visualizations for Websites With R and ggplot2</title>
      <link>https://minimaxir.com/2017/08/ggplot2-web/</link>
      <pubDate>Mon, 14 Aug 2017 09:00:00 -0700</pubDate>
      <guid>https://minimaxir.com/2017/08/ggplot2-web/</guid>
      <description>In general, it takes little additional effort to make something unique with ggplot2, and the effort is well worth it.</description>
      <content:encoded><![CDATA[<p>If you&rsquo;ve been following my blog, I like to use <a href="https://cran.r-project.org">R</a> and <a href="http://ggplot2.tidyverse.org/reference/">ggplot2</a> for data visualization. A lot.</p>
<p>One of my older blog posts, <a href="http://minimaxir.com/2015/02/ggplot-tutorial/">An Introduction on How to Make Beautiful Charts With R and ggplot2</a>, is still one of my most-trafficked posts years later, and even today I see techniques from that particular post incorporated into modern data visualizations on sites such as <a href="https://www.reddit.com">Reddit&rsquo;s</a> <a href="https://www.reddit.com/r/dataisbeautiful/">/r/dataisbeautiful</a> subreddit.</p>
<p>However, that post is a little outdated. Thanks to a few updates to ggplot2 since then and other advances in data visualization best-practices, making pretty charts for websites/blogs using R and ggplot2 is even more easy, quick, <em>and</em> fun!</p>
<h2 id="quick-introduction-to-ggplot2">Quick Introduction to ggplot2</h2>
<p>ggplot2 uses a more concise setup toward creating charts as opposed to the more declarative style of Python&rsquo;s <a href="https://matplotlib.org">matplotlib</a> and base R. And it also includes a few example datasets for practicing ggplot2 functionality; for example, the <code>mpg</code> dataset is a <a href="http://ggplot2.tidyverse.org/reference/mpg.html">dataset</a> of the performance of popular models of cars in 1998 and 2008.</p>
<figure>

    <img loading="lazy" srcset="/2017/08/ggplot2-web/mpg_hu_a640dba59b901764.webp 320w,/2017/08/ggplot2-web/mpg_hu_27b6e8ca229c6f49.webp 768w,/2017/08/ggplot2-web/mpg_hu_cbb195b8dd54f306.webp 1024w,/2017/08/ggplot2-web/mpg.png 1376w" src="mpg.png"/> 
</figure>

<p>Let&rsquo;s say you want to create a <a href="https://en.wikipedia.org/wiki/Scatter_plot">scatter plot</a>. Following <a href="http://ggplot2.tidyverse.org/reference/geom_smooth.html">a great example</a> from the ggplot2 documentation, let&rsquo;s plot the highway mileage of the car vs. the <a href="https://en.wikipedia.org/wiki/Engine_displacement">volume displacement</a> of the engine. In ggplot2, first you instantiate the chart with the <code>ggplot()</code> function, specifying the source dataset and the core aesthetics you want to plot, such as x, y, color, and fill. In this case, we set the core aesthetics to x = displacement and y = mileage, and add a <code>geom_point()</code> layer to make a scatter plot:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">p</span> <span class="o">&lt;-</span> <span class="nf">ggplot</span><span class="p">(</span><span class="n">mpg</span><span class="p">,</span> <span class="nf">aes</span><span class="p">(</span><span class="n">x</span> <span class="o">=</span> <span class="n">displ</span><span class="p">,</span> <span class="n">y</span> <span class="o">=</span> <span class="n">hwy</span><span class="p">))</span> <span class="o">+</span>
</span></span><span class="line"><span class="cl">			<span class="nf">geom_point</span><span class="p">()</span>
</span></span></code></pre></div><figure>

    <img loading="lazy" srcset="/2017/08/ggplot2-web/plot1_hu_cdb77dbedd0d1aec.webp 320w,/2017/08/ggplot2-web/plot1_hu_e036e0d8db01be8d.webp 768w,/2017/08/ggplot2-web/plot1.png 994w" src="plot1.png"/> 
</figure>

<p>As we can see, there is a negative correlation between the two metrics. I&rsquo;m sure you&rsquo;ve seen plots like these around the internet before. But with only a couple of lines of codes, you can make them look more contemporary.</p>
<p>ggplot2 lets you add a well-designed theme with just one line of code. Relatively new to <code>ggplot2</code> is <code>theme_minimal()</code>, which <a href="http://ggplot2.tidyverse.org/reference/ggtheme.html">generates</a> a muted style similar to <a href="http://fivethirtyeight.com">FiveThirtyEight</a>&rsquo;s modern data visualizations:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">p</span> <span class="o">&lt;-</span> <span class="n">p</span> <span class="o">+</span>
</span></span><span class="line"><span class="cl">    <span class="nf">theme_minimal</span><span class="p">()</span>
</span></span></code></pre></div><figure>

    <img loading="lazy" srcset="/2017/08/ggplot2-web/plot2_hu_1abcf13146957fb5.webp 320w,/2017/08/ggplot2-web/plot2_hu_70ccfd8927b0ba23.webp 768w,/2017/08/ggplot2-web/plot2.png 994w" src="plot2.png"/> 
</figure>

<p>But we can still add color. Setting a color aesthetic on a character/categorical variable will set the colors of the corresponding points, making it easy to differentiate at a glance.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">p</span> <span class="o">&lt;-</span> <span class="nf">ggplot</span><span class="p">(</span><span class="n">mpg</span><span class="p">,</span> <span class="nf">aes</span><span class="p">(</span><span class="n">x</span> <span class="o">=</span> <span class="n">displ</span><span class="p">,</span> <span class="n">y</span> <span class="o">=</span> <span class="n">hwy</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="n">class</span><span class="p">))</span> <span class="o">+</span>
</span></span><span class="line"><span class="cl">			<span class="nf">geom_point</span><span class="p">()</span> <span class="o">+</span>
</span></span><span class="line"><span class="cl">			<span class="nf">theme_minimal</span><span class="p">()</span>
</span></span></code></pre></div><figure>

    <img loading="lazy" srcset="/2017/08/ggplot2-web/plot3_hu_c34c19184e3ebcf2.webp 320w,/2017/08/ggplot2-web/plot3_hu_f25d6095028fc6d5.webp 768w,/2017/08/ggplot2-web/plot3.png 994w" src="plot3.png"/> 
</figure>

<p>Adding the color aesthetic certainly makes things much prettier. ggplot2 automatically adds a legend for the colors as well.
However, for this particular visualization, it is difficult to see trends in the points for each class. A easy way around this is to add a <a href="https://en.wikipedia.org/wiki/Least_squares">least squares regression</a> trendline for each class <a href="http://ggplot2.tidyverse.org/reference/geom_smooth.html">using</a> <code>geom_smooth()</code> (which normally adds a smoothed line, but since there isn&rsquo;t a lot of data for each group, we force it to a linear model and do not plot confidence intervals)</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">p</span> <span class="o">&lt;-</span> <span class="n">p</span> <span class="o">+</span>
</span></span><span class="line"><span class="cl">	<span class="nf">geom_smooth</span><span class="p">(</span><span class="n">method</span> <span class="o">=</span> <span class="s">&#34;lm&#34;</span><span class="p">,</span> <span class="n">se</span> <span class="o">=</span> <span class="bp">F</span><span class="p">)</span>
</span></span></code></pre></div><figure>

    <img loading="lazy" srcset="/2017/08/ggplot2-web/plot4_hu_d736cf0961bca1f5.webp 320w,/2017/08/ggplot2-web/plot4_hu_6e954c53dc4d849a.webp 768w,/2017/08/ggplot2-web/plot4.png 994w" src="plot4.png"/> 
</figure>

<p>Pretty neat, and now comparative trends are much more apparent! For example, pickups and SUVs have similar efficiency, which makes intuitive sense.</p>
<p>The chart axes should be labeled (<em>always</em> label your charts!). All the typical labels, like <code>title</code>, <code>x</code>-axis, and <code>y</code>-axis can be done with the <code>labs()</code> function. But relatively new to ggplot2 are the <code>subtitle</code> and <code>caption</code> fields, both of do what you expect:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">p</span> <span class="o">&lt;-</span> <span class="n">p</span> <span class="o">+</span>
</span></span><span class="line"><span class="cl">    <span class="nf">labs</span><span class="p">(</span><span class="n">title</span><span class="o">=</span><span class="s">&#34;Efficiency of Popular Models of Cars&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="n">subtitle</span><span class="o">=</span><span class="s">&#34;By Class of Car&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="n">x</span><span class="o">=</span><span class="s">&#34;Engine Displacement (liters)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="n">y</span><span class="o">=</span><span class="s">&#34;Highway Miles per Gallon&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="n">caption</span><span class="o">=</span><span class="s">&#34;by Max Woolf — minimaxir.com&#34;</span><span class="p">)</span>
</span></span></code></pre></div><figure>

    <img loading="lazy" srcset="/2017/08/ggplot2-web/plot5_hu_d6d809535fa48bb6.webp 320w,/2017/08/ggplot2-web/plot5_hu_68db8294c51b2638.webp 768w,/2017/08/ggplot2-web/plot5.png 994w" src="plot5.png"/> 
</figure>

<p>That&rsquo;s a pretty good start. Now let&rsquo;s take it to the next level.</p>
<h2 id="how-to-save-a-ggplot2-chart-for-web">How to Save A ggplot2 chart For Web</h2>
<p>Something surprisingly undiscussed in the field of data visualization is how to <em>save</em> a chart as a high quality image file. For example, with <a href="https://products.office.com/en-us/excel">Excel</a> charts, Microsoft <a href="https://support.office.com/en-us/article/Save-a-chart-as-a-picture-in-Excel-for-Windows-254bbf9a-1ce1-459f-914a-4902e8ca9217">officially recommends</a> to copy the chart, <em>paste it as an image back into Excel</em>, then save the pasted image, without having any control over image quality and size in the browser (the <em>real</em> best way to save an Excel/<a href="https://www.apple.com/numbers/">Numbers</a> chart as an image for a webpage is to copy/paste the chart object into a <a href="https://products.office.com/en-us/powerpoint">PowerPoint</a>/<a href="https://www.apple.com/keynote/">Keynote</a> slide, and export <em>the slide</em> as an image. This also makes it extremely easy to annotate/brand said chart beforehand in PowerPoint/Keynote).</p>
<p>R IDEs such as <a href="https://www.rstudio.com">RStudio</a> have a chart-saving UI with the typical size/filetype options. But if you save an image from this UI, the shapes and texts of the resulting image will be heavily aliased (R <a href="https://danieljhocking.wordpress.com/2013/03/12/high-resolution-figures-in-r/">renders images at 72 dpi</a> by default, which is much lower than that of modern HiDPI/Retina displays).</p>
<p>The data visualizations used earlier in this post were generated in-line as a part of an <a href="http://rmarkdown.rstudio.com/r_notebooks.html">R Notebook</a>, but it is surprisingly difficult to extract the generated chart as a separate file. But ggplot2 also has <code>ggsave()</code>, which saves the image to disk using antialiasing and makes the fonts/shapes in the chart look much better, and assumes a default dpi of 300. Saving charts using <code>ggsave()</code>, and adjusting the sizes of the text and geoms to compensate for the higher dpi, makes the charts look very presentable. A width of 4 and a height of 3 results in a 1200x900px image, which if posted on a blog with a content width of ~600px (like mine), will render at full resolution on HiDPI/Retina displays, or downsample appropriately otherwise. Due to modern PNG compression, the file size/bandwidth cost for using larger images is minimal.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">p</span> <span class="o">&lt;-</span> <span class="nf">ggplot</span><span class="p">(</span><span class="n">mpg</span><span class="p">,</span> <span class="nf">aes</span><span class="p">(</span><span class="n">x</span> <span class="o">=</span> <span class="n">displ</span><span class="p">,</span> <span class="n">y</span> <span class="o">=</span> <span class="n">hwy</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="n">class</span><span class="p">))</span> <span class="o">+</span>
</span></span><span class="line"><span class="cl">    <span class="nf">geom_smooth</span><span class="p">(</span><span class="n">method</span> <span class="o">=</span> <span class="s">&#34;lm&#34;</span><span class="p">,</span> <span class="n">se</span><span class="o">=</span><span class="bp">F</span><span class="p">,</span> <span class="n">size</span><span class="o">=</span><span class="m">0.5</span><span class="p">)</span> <span class="o">+</span>
</span></span><span class="line"><span class="cl">    <span class="nf">geom_point</span><span class="p">(</span><span class="n">size</span><span class="o">=</span><span class="m">0.5</span><span class="p">)</span> <span class="o">+</span>
</span></span><span class="line"><span class="cl">    <span class="nf">theme_minimal</span><span class="p">(</span><span class="n">base_size</span><span class="o">=</span><span class="m">9</span><span class="p">)</span> <span class="o">+</span>
</span></span><span class="line"><span class="cl">    <span class="nf">labs</span><span class="p">(</span><span class="n">title</span><span class="o">=</span><span class="s">&#34;Efficiency of Popular Models of Cars&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="n">subtitle</span><span class="o">=</span><span class="s">&#34;By Class of Car&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="n">x</span><span class="o">=</span><span class="s">&#34;Engine Displacement (liters)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="n">y</span><span class="o">=</span><span class="s">&#34;Highway Miles per Gallon&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="n">caption</span><span class="o">=</span><span class="s">&#34;by Max Woolf — minimaxir.com&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">ggsave</span><span class="p">(</span><span class="s">&#34;tutorial-0.png&#34;</span><span class="p">,</span> <span class="n">p</span><span class="p">,</span> <span class="n">width</span><span class="o">=</span><span class="m">4</span><span class="p">,</span> <span class="n">height</span><span class="o">=</span><span class="m">3</span><span class="p">)</span>
</span></span></code></pre></div><figure>

    <img loading="lazy" srcset="/2017/08/ggplot2-web/tutorial-0_hu_3491aa5980f9ce57.webp 320w,/2017/08/ggplot2-web/tutorial-0_hu_386b410431f20644.webp 768w,/2017/08/ggplot2-web/tutorial-0_hu_3025381ee3b4d2f8.webp 1024w,/2017/08/ggplot2-web/tutorial-0.png 1200w" src="tutorial-0.png"/> 
</figure>

<p>Compare to the previous non-ggsave chart, which is more blurry around text/shapes:</p>
<figure>

    <img loading="lazy" srcset="/2017/08/ggplot2-web/plot5_hu_d6d809535fa48bb6.webp 320w,/2017/08/ggplot2-web/plot5_hu_68db8294c51b2638.webp 768w,/2017/08/ggplot2-web/plot5.png 994w" src="plot5.png"/> 
</figure>

<p>For posterity, here&rsquo;s the same chart saved at 1200x900px using the RStudio image-saving UI:</p>
<figure>

    <img loading="lazy" srcset="/2017/08/ggplot2-web/plot-1200-900_hu_2be01453db7558b.webp 320w,/2017/08/ggplot2-web/plot-1200-900_hu_9f79679f34f611e0.webp 768w,/2017/08/ggplot2-web/plot-1200-900_hu_69a1ff889438a21b.webp 1024w,/2017/08/ggplot2-web/plot-1200-900.png 1200w" src="plot-1200-900.png"/> 
</figure>

<p>Note that the antialiasing optimizations assume that you are <em>not</em> uploading the final chart to a service like <a href="https://medium.com">Medium</a> or <a href="https://wordpress.com">WordPress.com</a>, which will compress the images and reduce the quality anyways. But if you are uploading it to Reddit or self-hosting your own blog, it&rsquo;s definitely worth it.</p>
<h2 id="fancy-fonts">Fancy Fonts</h2>
<p>Changing the chart font is another way to add a personal flair.
Theme functions like <code>theme_minimal()</code> accept a <code>base_family</code> parameter. With that, you can specify any font family as the default instead of the base sans-serif. (On Windows, you may need to install the <code>extrafont</code> package first). Fonts from <a href="https://fonts.google.com">Google Fonts</a> are free and work easily with ggplot2 once installed. For example, we can use <a href="https://fonts.google.com/specimen/Roboto">Roboto</a>, Google&rsquo;s modern font which has also been getting a lot of usage on <a href="https://stackoverflow.com">Stack Overflow</a>&rsquo;s great ggplot2 <a href="https://stackoverflow.blog/2017/06/15/developers-use-spaces-make-money-use-tabs/">data visualizations</a>.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">p</span> <span class="o">&lt;-</span> <span class="n">p</span> <span class="o">+</span>
</span></span><span class="line"><span class="cl">    <span class="nf">theme_minimal</span><span class="p">(</span><span class="n">base_size</span><span class="o">=</span><span class="m">9</span><span class="p">,</span> <span class="n">base_family</span><span class="o">=</span><span class="s">&#34;Roboto&#34;</span><span class="p">)</span>
</span></span></code></pre></div><figure>

    <img loading="lazy" srcset="/2017/08/ggplot2-web/tutorial-1_hu_895dfabb6331218f.webp 320w,/2017/08/ggplot2-web/tutorial-1_hu_1014960e9eb00de2.webp 768w,/2017/08/ggplot2-web/tutorial-1_hu_283f71e45e79c23c.webp 1024w,/2017/08/ggplot2-web/tutorial-1.png 1200w" src="tutorial-1.png"/> 
</figure>

<p>A general text design guideline is to use fonts of different weights/widths for different hierarchies of content. In this case, we can use a bolder condensed font for the title, and deemphasize the subtitle and caption using lighter colors, all done using the <code>theme()</code> <a href="http://ggplot2.tidyverse.org/reference/theme.html">function</a>.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">p</span> <span class="o">&lt;-</span> <span class="n">p</span> <span class="o">+</span>
</span></span><span class="line"><span class="cl">    <span class="nf">theme</span><span class="p">(</span><span class="n">plot.subtitle</span> <span class="o">=</span> <span class="nf">element_text</span><span class="p">(</span><span class="n">color</span><span class="o">=</span><span class="s">&#34;#666666&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">          <span class="n">plot.title</span> <span class="o">=</span> <span class="nf">element_text</span><span class="p">(</span><span class="n">family</span><span class="o">=</span><span class="s">&#34;Roboto Condensed Bold&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">          <span class="n">plot.caption</span> <span class="o">=</span> <span class="nf">element_text</span><span class="p">(</span><span class="n">color</span><span class="o">=</span><span class="s">&#34;#AAAAAA&#34;</span><span class="p">,</span> <span class="n">size</span><span class="o">=</span><span class="m">6</span><span class="p">))</span>
</span></span></code></pre></div><figure>

    <img loading="lazy" srcset="/2017/08/ggplot2-web/tutorial-2_hu_45115eb223bac5fe.webp 320w,/2017/08/ggplot2-web/tutorial-2_hu_96b9283a20212470.webp 768w,/2017/08/ggplot2-web/tutorial-2_hu_ec38c0bbfa9bd892.webp 1024w,/2017/08/ggplot2-web/tutorial-2.png 1200w" src="tutorial-2.png"/> 
</figure>

<p>It&rsquo;s worth nothing that data visualizations posted on websites should be easily <em>legible</em> for mobile-device users as well, hence the intentional use of larger fonts relative to charts typically produced in the desktop-oriented Excel.</p>
<p>Additionally, all theming options can be set as a session default at the beginning of a script using <code>theme_set()</code>, saving even more time instead of having to recreate the theme for each chart.</p>
<h2 id="the-ggplot2-colors">The &ldquo;ggplot2 colors&rdquo;</h2>
<p>The &ldquo;ggplot2 colors&rdquo; for categorical variables are infamous for being the primary indicator of a chart being made with ggplot2. But there is a science to it; ggplot2 by default selects colors using the <code>scale_color_hue()</code> <a href="http://ggplot2.tidyverse.org/reference/scale_hue.html">function</a>, which selects colors in the HSL space by changing the hue [H] between 0 and 360, keeping saturation [S] and lightness [L] constant. As a result, ggplot2 selects the most <em>distinct</em> colors possible while keeping lightness constant. For example, if you have 2 different categories, ggplot2 chooses the colors with h = 0 and h = 180; if 3 colors, h = 0, h = 120, h = 240, etc.</p>
<p>It&rsquo;s smart, but does make a given chart lose distinctness when many other ggplot2 charts use the same selection methodology. A quick way to take advantage of this hue dispersion while still making the colors unique is to change the lightness; by default, <code>l = 65</code>, but setting it slightly lower will make the charts look more professional/<a href="https://www.bloomberg.com">Bloomberg</a>-esque.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">p_color</span> <span class="o">&lt;-</span> <span class="n">p</span> <span class="o">+</span>
</span></span><span class="line"><span class="cl">        <span class="nf">scale_color_hue</span><span class="p">(</span><span class="n">l</span> <span class="o">=</span> <span class="m">40</span><span class="p">)</span>
</span></span></code></pre></div><figure>

    <img loading="lazy" srcset="/2017/08/ggplot2-web/tutorial-4_hu_264938515f752543.webp 320w,/2017/08/ggplot2-web/tutorial-4_hu_eb1a54c4bb2c178d.webp 768w,/2017/08/ggplot2-web/tutorial-4_hu_f9b6b0a558fcfa8b.webp 1024w,/2017/08/ggplot2-web/tutorial-4.png 1200w" src="tutorial-4.png"/> 
</figure>

<h2 id="rcolorbrewer">RColorBrewer</h2>
<p>Another coloring option for ggplot2 charts are the <a href="http://colorbrewer2.org/#type=sequential&amp;scheme=BuGn&amp;n=3">ColorBrewer</a> palettes implemented with the <code>RColorBrewer</code> package, which are supported natively in ggplot2 with functions such as <code>scale_color_brewer()</code>. The sequential palettes like &ldquo;Blues&rdquo; and &ldquo;Greens&rdquo; do what the name implies:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">p_color</span> <span class="o">&lt;-</span> <span class="n">p</span> <span class="o">+</span>
</span></span><span class="line"><span class="cl">        <span class="nf">scale_color_brewer</span><span class="p">(</span><span class="n">palette</span><span class="o">=</span><span class="s">&#34;Blues&#34;</span><span class="p">)</span>
</span></span></code></pre></div><figure>

    <img loading="lazy" srcset="/2017/08/ggplot2-web/tutorial-5_hu_1b83219640238315.webp 320w,/2017/08/ggplot2-web/tutorial-5_hu_437d57ae9e55c1f4.webp 768w,/2017/08/ggplot2-web/tutorial-5_hu_48a903b9c7756119.webp 1024w,/2017/08/ggplot2-web/tutorial-5.png 1200w" src="tutorial-5.png"/> 
</figure>

<p>A famous diverging palette for visualizations on /r/dataisbeautiful is the &ldquo;Spectral&rdquo; palette, which is a lighter rainbow (recommended for dark backgrounds)</p>
<figure>

    <img loading="lazy" srcset="/2017/08/ggplot2-web/tutorial-6_hu_b0f4861a140f3ca2.webp 320w,/2017/08/ggplot2-web/tutorial-6_hu_2bea8aa9ecf4b77f.webp 768w,/2017/08/ggplot2-web/tutorial-6_hu_8c72b6730d700f72.webp 1024w,/2017/08/ggplot2-web/tutorial-6.png 1200w" src="tutorial-6.png"/> 
</figure>

<p>However, while the charts look pretty, it&rsquo;s difficult to tell the categories apart. The qualitative palettes fix this problem, and have more distinct possibilities than the <code>scale_color_hue()</code> approach mentioned earlier.</p>
<p>Here are 3 examples of qualitative palettes, &ldquo;Set1&rdquo;, &ldquo;Set2&rdquo;, and &ldquo;Set3,&rdquo; whichever fit your preference.</p>
<p><figure>

    <img loading="lazy" srcset="/2017/08/ggplot2-web/tutorial-7_hu_25cbbc089de8f962.webp 320w,/2017/08/ggplot2-web/tutorial-7_hu_a16288142b4ca2c9.webp 768w,/2017/08/ggplot2-web/tutorial-7_hu_5dcf40a21178ff45.webp 1024w,/2017/08/ggplot2-web/tutorial-7.png 1200w" src="tutorial-7.png"/> 
</figure>

<figure>

    <img loading="lazy" srcset="/2017/08/ggplot2-web/tutorial-8_hu_26d266cdc130beea.webp 320w,/2017/08/ggplot2-web/tutorial-8_hu_33341f45209f13f8.webp 768w,/2017/08/ggplot2-web/tutorial-8_hu_69ff86f540e43dba.webp 1024w,/2017/08/ggplot2-web/tutorial-8.png 1200w" src="tutorial-8.png"/> 
</figure>

<figure>

    <img loading="lazy" srcset="/2017/08/ggplot2-web/tutorial-9_hu_8d2a2fc7ea80dd4.webp 320w,/2017/08/ggplot2-web/tutorial-9_hu_8c554b2661491e44.webp 768w,/2017/08/ggplot2-web/tutorial-9_hu_50b6d07e248d4fbd.webp 1024w,/2017/08/ggplot2-web/tutorial-9.png 1200w" src="tutorial-9.png"/> 
</figure>
</p>
<h2 id="viridis-and-accessibility">Viridis and Accessibility</h2>
<p>Let&rsquo;s mix up the visualization a bit. A rarely-used-but-very-useful ggplot2 geom is <code>geom2d_bin()</code>, which counts the number of points in a given 2d spatial area:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">p</span> <span class="o">&lt;-</span> <span class="nf">ggplot</span><span class="p">(</span><span class="n">mpg</span><span class="p">,</span> <span class="nf">aes</span><span class="p">(</span><span class="n">x</span> <span class="o">=</span> <span class="n">displ</span><span class="p">,</span> <span class="n">y</span> <span class="o">=</span> <span class="n">hwy</span><span class="p">))</span> <span class="o">+</span>
</span></span><span class="line"><span class="cl">    <span class="nf">geom_bin2d</span><span class="p">(</span><span class="n">bins</span><span class="o">=</span><span class="m">10</span><span class="p">)</span> <span class="o">+</span>
</span></span><span class="line"><span class="cl">    <span class="n">[...theming</span> <span class="n">options...]</span>
</span></span></code></pre></div><figure>

    <img loading="lazy" srcset="/2017/08/ggplot2-web/tutorial-tile_hu_e43b208518838250.webp 320w,/2017/08/ggplot2-web/tutorial-tile_hu_cdaf166456144b15.webp 768w,/2017/08/ggplot2-web/tutorial-tile_hu_4c5bd954986c81ea.webp 1024w,/2017/08/ggplot2-web/tutorial-tile.png 1200w" src="tutorial-tile.png"/> 
</figure>

<p>We see that the largest number of points are centered around (2,30). However, the default ggplot2 color palette for continuous variables is <em>boring</em>. Yes, we can use the RColorBrewer sequential palettes above, but as noted, they aren&rsquo;t perceptually distinct, and could cause issues for readers who are colorblind.</p>
<p>The <a href="https://cran.r-project.org/web/packages/viridis/vignettes/intro-to-viridis.html">viridis R package</a> provides a set of 4 high-contrast palettes which are very colorblind friendly, and works easily with ggplot2 by extending a <code>scale_fill_viridis()/scale_color_viridis()</code> function.</p>
<p>The default &ldquo;viridis&rdquo; palette has been increasingly popular on the web lately:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">p_color</span> <span class="o">&lt;-</span> <span class="n">p</span> <span class="o">+</span>
</span></span><span class="line"><span class="cl">        <span class="nf">scale_fill_viridis</span><span class="p">(</span><span class="n">option</span><span class="o">=</span><span class="s">&#34;viridis&#34;</span><span class="p">)</span>
</span></span></code></pre></div><figure>

    <img loading="lazy" srcset="/2017/08/ggplot2-web/tutorial-10_hu_bdab45ed149b2987.webp 320w,/2017/08/ggplot2-web/tutorial-10_hu_4f8ed1d4b0d4c15b.webp 768w,/2017/08/ggplot2-web/tutorial-10_hu_2136ce24111625f9.webp 1024w,/2017/08/ggplot2-web/tutorial-10.png 1200w" src="tutorial-10.png"/> 
</figure>

<p>&ldquo;magma&rdquo; and &ldquo;inferno&rdquo; are similar, and give the data visualization a fiery edge:</p>
<figure>

    <img loading="lazy" srcset="/2017/08/ggplot2-web/tutorial-11_hu_891953ed03995865.webp 320w,/2017/08/ggplot2-web/tutorial-11_hu_339503edfac14382.webp 768w,/2017/08/ggplot2-web/tutorial-11_hu_b58a6f34b44b0e07.webp 1024w,/2017/08/ggplot2-web/tutorial-11.png 1200w" src="tutorial-11.png"/> 
</figure>

<figure>

    <img loading="lazy" srcset="/2017/08/ggplot2-web/tutorial-12_hu_9576afecf56c3191.webp 320w,/2017/08/ggplot2-web/tutorial-12_hu_f3ada4502649973e.webp 768w,/2017/08/ggplot2-web/tutorial-12_hu_7b8a7ff2ebe2952f.webp 1024w,/2017/08/ggplot2-web/tutorial-12.png 1200w" src="tutorial-12.png"/> 
</figure>

<p>Lastly, &ldquo;plasma&rdquo; is a mix between the 3 palettes above:</p>
<figure>

    <img loading="lazy" srcset="/2017/08/ggplot2-web/tutorial-13_hu_d6ee0c44a3b9408.webp 320w,/2017/08/ggplot2-web/tutorial-13_hu_1cc7fd9e09047f6f.webp 768w,/2017/08/ggplot2-web/tutorial-13_hu_cae2bedd89d23c95.webp 1024w,/2017/08/ggplot2-web/tutorial-13.png 1200w" src="tutorial-13.png"/> 
</figure>

<h2 id="next-steps">Next Steps</h2>
<p>FiveThirtyEight actually uses ggplot2 for their data journalism workflow <a href="https://channel9.msdn.com/Events/useR-international-R-User-conference/useR2016/FiveThirtyEights-data-journalism-workflow-with-R?ocid=player">in an interesting way</a>; they render the base chart using ggplot2, but export it as as a SVG/PDF vector file which can scale to any size, and then the design team annotates/customizes the data visualization in <a href="http://www.adobe.com/products/illustrator.html">Adobe Illustrator</a> before exporting it as a static PNG for the article (in general, I recommend using an external image editor to add text annotations to a data visualization because doing it manually in ggplot2 is inefficient).</p>
<p>For general use cases, ggplot2 has very strong defaults for beautiful data visualizations. And certainly there is a lot <em>more</em> you can do to make a visualization beautiful than what&rsquo;s listed in this post, such as using facets and tweaking parameters of geoms for further distinction, but those are more specific to a given data visualization. In general, it takes little additional effort to make something <em>unique</em> with ggplot2, and the effort is well worth it. And prettier charts are more persuasive, which is a good return-on-investment.</p>
<hr>
<p><em>You can view the R and ggplot2 code used to create the data visualizations in <a href="http://minimaxir.com/notebooks/ggplot2-web/">this R Notebook</a>. You can also view the images/data used for this post in <a href="https://github.com/minimaxir/ggplot2-web">this GitHub repository</a></em>.</p>
<p><em>You are free to use the data visualizations from this article however you wish, but it would be greatly appreciated if proper attribution is given to this article and/or myself!</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>Benchmarking TensorFlow on Cloud CPUs: Cheaper Deep Learning than Cloud GPUs</title>
      <link>https://minimaxir.com/2017/07/cpu-or-gpu/</link>
      <pubDate>Wed, 05 Jul 2017 09:00:00 -0700</pubDate>
      <guid>https://minimaxir.com/2017/07/cpu-or-gpu/</guid>
      <description>Using CPUs instead of GPUs for deep learning training in the cloud is cheaper because of the massive cost differential afforded by preemptible instances.</description>
      <content:encoded><![CDATA[<p>I&rsquo;ve been working on a few personal deep learning projects with <a href="https://github.com/fchollet/keras">Keras</a> and <a href="https://www.tensorflow.org">TensorFlow</a>. However, training models for deep learning with cloud services such as <a href="https://aws.amazon.com/ec2/">Amazon EC2</a> and <a href="https://cloud.google.com/compute/">Google Compute Engine</a> isn&rsquo;t free, and as someone who is currently unemployed, I have to keep an eye on extraneous spending and be as cost-efficient as possible (please support my work on <a href="https://www.patreon.com/minimaxir">Patreon</a>!). I tried deep learning on the cheaper CPU instances instead of GPU instances to save money, and to my surprise, my model training was only slightly slower. As a result, I took a deeper look at the pricing mechanisms of these two types of instances to see if CPUs are more useful for my needs.</p>
<p>The <a href="https://cloud.google.com/compute/pricing#gpus">pricing of GPU instances</a> on Google Compute Engine starts at <strong>$0.745/hr</strong> (by attaching a $0.700/hr GPU die to a $0.045/hr n1-standard-1 instance). A couple months ago, Google <a href="https://cloudplatform.googleblog.com/2017/05/Compute-Engine-machine-types-with-up-to-64-vCPUs-now-ready-for-your-production-workloads.html">announced</a> CPU instances with up to 64 vCPUs on the modern Intel <a href="https://en.wikipedia.org/wiki/Skylake_%28microarchitecture%29">Skylake</a> CPU architecture. More importantly, they can also be used in <a href="https://cloud.google.com/compute/docs/instances/preemptible">preemptible CPU instances</a>, which live at most for 24 hours on GCE and can be terminated at any time (very rarely), but cost about <em>20%</em> of the price of a standard instance. A preemptible n1-highcpu-64 instance with 64 vCPUs and 57.6GB RAM plus the premium for using Skylake CPUs is <strong>$0.509/hr</strong>, about 2/3rds of the cost of the GPU instance.</p>
<p>If the model training speed of 64 vCPUs is comparable to that of a GPU (or even slightly slower), it would be more cost-effective to use the CPUs instead. But that&rsquo;s assuming the deep learning software and the GCE platform hardware operate at 100% efficiency; if they don&rsquo;t (and they likely don&rsquo;t), there may be <em>even more savings</em> by scaling down the number of vCPUs and cost accordingly (a 32 vCPU instance with same parameters is half the price at <strong>$0.254/hr</strong>, 16 vCPU at <strong>$0.127/hr</strong>, etc).</p>
<p>There aren&rsquo;t any benchmarks for deep learning libraries with tons and tons of CPUs since there&rsquo;s no demand, as GPUs are the <a href="https://en.wikipedia.org/wiki/Occam%27s_razor">Occam&rsquo;s razor</a> solution to deep learning hardware. But what might make counterintuitive but economical sense is to use CPUs instead of GPUs for deep learning training because of the massive cost differential afforded by preemptible instances, thanks to Google&rsquo;s <a href="https://en.wikipedia.org/wiki/Economies_of_scale">economies of scale</a>.</p>
<h2 id="setup">Setup</h2>
<p>I already have <a href="https://github.com/minimaxir/deep-learning-cpu-gpu-benchmark">benchmarking scripts</a> of real-world deep learning use cases, <a href="https://github.com/minimaxir/keras-cntk-docker">Docker container environments</a>, and results logging from my <a href="http://minimaxir.com/2017/06/keras-cntk/">TensorFlow vs. CNTK article</a>. A few minor tweaks allow the scripts to be utilized for both CPU and GPU instances by setting CLI arguments. I also rebuilt <a href="https://github.com/minimaxir/keras-cntk-docker/blob/master/Dockerfile">the Docker container</a> to support the latest version of TensorFlow (1.2.1), and created a <a href="https://github.com/minimaxir/keras-cntk-docker/blob/master/Dockerfile-cpu">CPU version</a> of the container which installs the CPU-appropriate TensorFlow library instead.</p>
<p>There is a notable CPU-specific TensorFlow behavior; if you install from <code>pip</code> (as the<a href="https://www.tensorflow.org/install/"> official instructions</a> and tutorials recommend) and begin training a model in TensorFlow, you&rsquo;ll see these warnings in the console:</p>
<figure>

    <img loading="lazy" srcset="/2017/07/cpu-or-gpu/tensorflow-console_hu_e436e066e4e1304d.webp 320w,/2017/07/cpu-or-gpu/tensorflow-console_hu_ce5df372394290b4.webp 768w,/2017/07/cpu-or-gpu/tensorflow-console_hu_9e354816d97d6c8f.webp 1024w,/2017/07/cpu-or-gpu/tensorflow-console.png 1130w" src="tensorflow-console.png"/> 
</figure>

<p>In order to fix the warnings and benefit from these <a href="https://en.wikipedia.org/wiki/SSE4#SSE4.2">SSE4.2</a>/<a href="https://en.wikipedia.org/wiki/Advanced_Vector_Extensions">AVX</a>/<a href="https://en.wikipedia.org/wiki/FMA_instruction_set">FMA</a> optimizations, we <a href="https://stackoverflow.com/questions/41293077/how-to-compile-tensorflow-with-sse4-2-and-avx-instructions">compile TensorFlow from source</a>, and I created a <a href="https://github.com/minimaxir/keras-cntk-docker/blob/master/Dockerfile-cpu-compiled">third Docker container</a> to do just that. When training models in the new container, <a href="https://github.com/tensorflow/tensorflow/issues/10689">most</a> of the warnings no longer show, and (spoiler alert) there is indeed a speed boost in training time.</p>
<p>Therefore, we can test three major cases with Google Compute Engine:</p>
<ul>
<li>A Tesla K80 GPU instance.</li>
<li>A 64 Skylake vCPU instance where TensorFlow is installed via <code>pip</code> (along with testings at 8/16/32 vCPUs).</li>
<li>A 64 Skylake vCPU instance where TensorFlow is compiled (<code>cmp</code>) with CPU instructions (+ 8/16/32 vCPUs).</li>
</ul>
<h2 id="results">Results</h2>
<p>For each model architecture and software/hardware configuration, I calculate the <strong>total training time relative to the GPU instance training</strong> for running the model training for the provided test script. In all cases, the GPU <em>should</em> be the fastest training configuration, and systems with more processors should train faster than those with fewer processors.</p>
<p>Let&rsquo;s start using the <a href="http://yann.lecun.com/exdb/mnist/">MNIST dataset</a> of handwritten digits plus the common multilayer perceptron (MLP) architecture, with dense fully-connected layers. Lower training time is better. All configurations below the horizontal dotted line are better than GPUs; all configurations above the dotted line are worse than GPUs.</p>
<figure>

    <img loading="lazy" srcset="/2017/07/cpu-or-gpu/dl-cpu-gpu-5_hu_8cf5154f974aed3c.webp 320w,/2017/07/cpu-or-gpu/dl-cpu-gpu-5_hu_2ec21aba02d8fb37.webp 768w,/2017/07/cpu-or-gpu/dl-cpu-gpu-5_hu_7682d0a58ea1e871.webp 1024w,/2017/07/cpu-or-gpu/dl-cpu-gpu-5.png 1200w" src="dl-cpu-gpu-5.png"/> 
</figure>

<p>Here, the GPU is the fastest out of all the platform configurations, but there are other curious trends: the performance between 32 vCPUs and 64 vCPUs is similar, and the compiled TensorFlow library is indeed a significant improvement in training speed <em>but only for 8 and 16 vCPUs</em>. Perhaps there are overheads negotiating information between vCPUs that eliminate the performance advantages of more vCPUs, and perhaps these overheads are <em>different</em> with the CPU instructions of the compiled TensorFlow. In the end, it&rsquo;s a <a href="https://en.wikipedia.org/wiki/Black_box">black box</a>, which is why I prefer black box benchmarking all configurations of hardware instead of theorycrafting.</p>
<p>Since the difference between training speeds of different vCPU counts is minimal, there is definitely an advantage by scaling down. For each model architecture and configuration, I calculate a <strong>normalized training cost relative to the cost of GPU instance training</strong>. Because GCE instance costs are prorated (unlike Amazon EC2), we can simply calculate experiment cost by multiplying the total number of seconds the experiment runs by the cost of the instance (per second). Ideally, we want to <em>minimize</em> cost.</p>
<figure>

    <img loading="lazy" srcset="/2017/07/cpu-or-gpu/dl-cpu-gpu-6_hu_c6ff3c375435199.webp 320w,/2017/07/cpu-or-gpu/dl-cpu-gpu-6_hu_6bee6729ce48517c.webp 768w,/2017/07/cpu-or-gpu/dl-cpu-gpu-6_hu_ea518ff15e46de10.webp 1024w,/2017/07/cpu-or-gpu/dl-cpu-gpu-6.png 1200w" src="dl-cpu-gpu-6.png"/> 
</figure>

<p>Lower CPU counts are <em>much</em> more cost-effective for this problem, when going as low as possible is better.</p>
<p>Now, let&rsquo;s look at the same dataset with a convolutional neural network (CNN) approach for digit classification:</p>
<figure>

    <img loading="lazy" srcset="/2017/07/cpu-or-gpu/dl-cpu-gpu-7_hu_d3205561da4ed49c.webp 320w,/2017/07/cpu-or-gpu/dl-cpu-gpu-7_hu_ae81ceba7d6092e6.webp 768w,/2017/07/cpu-or-gpu/dl-cpu-gpu-7_hu_7a29bcea36dbe20e.webp 1024w,/2017/07/cpu-or-gpu/dl-cpu-gpu-7.png 1200w" src="dl-cpu-gpu-7.png"/> 
</figure>

<figure>

    <img loading="lazy" srcset="/2017/07/cpu-or-gpu/dl-cpu-gpu-8_hu_64f1eac6ff5b2b3f.webp 320w,/2017/07/cpu-or-gpu/dl-cpu-gpu-8_hu_c6dd20c1ccc111a5.webp 768w,/2017/07/cpu-or-gpu/dl-cpu-gpu-8_hu_2fa65c3c187723bb.webp 1024w,/2017/07/cpu-or-gpu/dl-cpu-gpu-8.png 1200w" src="dl-cpu-gpu-8.png"/> 
</figure>

<p>GPUs are unsurprisingly more than twice as fast as any CPU approach at CNNs, but cost structures are still the same, except that 64 vCPUs are <em>worse</em> than GPUs cost-wise, with 32 vCPUs training even faster than with 64 vCPUs.</p>
<p>Let&rsquo;s go deeper with CNNs and look at the <a href="https://www.cs.toronto.edu/%7Ekriz/cifar.html">CIFAR-10</a> image classification dataset, and a model which utilizes a deep covnet + a multilayer perceptron and ideal for image classification (similar to the <a href="https://gist.github.com/baraldilorenzo/07d7802847aaad0a35d3">VGG-16</a> architecture).</p>
<figure>

    <img loading="lazy" srcset="/2017/07/cpu-or-gpu/dl-cpu-gpu-9_hu_4a5cd8ba80674837.webp 320w,/2017/07/cpu-or-gpu/dl-cpu-gpu-9_hu_a81280d52893c1c9.webp 768w,/2017/07/cpu-or-gpu/dl-cpu-gpu-9_hu_af30edd0d3117cd8.webp 1024w,/2017/07/cpu-or-gpu/dl-cpu-gpu-9.png 1200w" src="dl-cpu-gpu-9.png"/> 
</figure>

<figure>

    <img loading="lazy" srcset="/2017/07/cpu-or-gpu/dl-cpu-gpu-10_hu_a6061eb15b5b8609.webp 320w,/2017/07/cpu-or-gpu/dl-cpu-gpu-10_hu_fe0751d9cd60a655.webp 768w,/2017/07/cpu-or-gpu/dl-cpu-gpu-10_hu_a371016369278a9a.webp 1024w,/2017/07/cpu-or-gpu/dl-cpu-gpu-10.png 1200w" src="dl-cpu-gpu-10.png"/> 
</figure>

<p>Similar behaviors as in the simple CNN case, although in this instance all CPUs perform better with the compiled TensorFlow library.</p>
<p>The fasttext algorithm, used here on the <a href="http://ai.stanford.edu/%7Eamaas/data/sentiment/">IMDb reviews dataset</a> to determine whether a review is positive or negative, classifies text extremely quickly relative to other methods.</p>
<figure>

    <img loading="lazy" srcset="/2017/07/cpu-or-gpu/dl-cpu-gpu-3_hu_12d55d02148bf0ea.webp 320w,/2017/07/cpu-or-gpu/dl-cpu-gpu-3_hu_aaf9917a1629214f.webp 768w,/2017/07/cpu-or-gpu/dl-cpu-gpu-3_hu_d51ed2e2c6fdec60.webp 1024w,/2017/07/cpu-or-gpu/dl-cpu-gpu-3.png 1200w" src="dl-cpu-gpu-3.png"/> 
</figure>

<figure>

    <img loading="lazy" srcset="/2017/07/cpu-or-gpu/dl-cpu-gpu-4_hu_6b591a471f3027a4.webp 320w,/2017/07/cpu-or-gpu/dl-cpu-gpu-4_hu_7cc361b383b25fb0.webp 768w,/2017/07/cpu-or-gpu/dl-cpu-gpu-4_hu_4c516e76a92eff3c.webp 1024w,/2017/07/cpu-or-gpu/dl-cpu-gpu-4.png 1200w" src="dl-cpu-gpu-4.png"/> 
</figure>

<p>In this case, GPUs are much, much faster than CPUs. The benefit of lower numbers of CPU isn&rsquo;t as dramatic; although as an aside, the <a href="https://github.com/facebookresearch/fastText">official fasttext implementation</a> is <em>designed</em> for large amounts of CPUs and handles parallelization much better.</p>
<p>The Bidirectional long-short-term memory (LSTM) architecture is great for working with text data like IMDb reviews, but after my previous benchmark article, <a href="https://news.ycombinator.com/item?id=14538086">commenters on Hacker News</a> noted that TensorFlow uses an inefficient implementation of the LSTM on the GPU, so perhaps the difference will be more notable.</p>
<figure>

    <img loading="lazy" srcset="/2017/07/cpu-or-gpu/dl-cpu-gpu-1_hu_4369b4e9e8856507.webp 320w,/2017/07/cpu-or-gpu/dl-cpu-gpu-1_hu_3e65077eb16928e4.webp 768w,/2017/07/cpu-or-gpu/dl-cpu-gpu-1_hu_d736592c927bd764.webp 1024w,/2017/07/cpu-or-gpu/dl-cpu-gpu-1.png 1200w" src="dl-cpu-gpu-1.png"/> 
</figure>

<figure>

    <img loading="lazy" srcset="/2017/07/cpu-or-gpu/dl-cpu-gpu-2_hu_d8c58f429f4a781b.webp 320w,/2017/07/cpu-or-gpu/dl-cpu-gpu-2_hu_1306d728b4fce90.webp 768w,/2017/07/cpu-or-gpu/dl-cpu-gpu-2_hu_ad3d19e88738d072.webp 1024w,/2017/07/cpu-or-gpu/dl-cpu-gpu-2.png 1200w" src="dl-cpu-gpu-2.png"/> 
</figure>

<p>Wait, what? GPU training of Bidirectional LSTMs is <em>twice as slow</em> as any CPU configuration? Wow. (In fairness, the benchmark uses the Keras LSTM default of <code>implementation=0</code> which is better on CPUs while <code>implementation=2</code> is better on GPUs, but it shouldn&rsquo;t result in that much of a differential)</p>
<p>Lastly, LSTM text generation of <a href="https://en.wikipedia.org/wiki/Friedrich_Nietzsche">Nietzsche&rsquo;s</a> <a href="https://s3.amazonaws.com/text-datasets/nietzsche.txt">writings</a> follows similar patterns to the other architectures, but without the drastic hit to the GPU.</p>
<figure>

    <img loading="lazy" srcset="/2017/07/cpu-or-gpu/dl-cpu-gpu-11_hu_d84b78ad35a1f056.webp 320w,/2017/07/cpu-or-gpu/dl-cpu-gpu-11_hu_d58d19568c89869.webp 768w,/2017/07/cpu-or-gpu/dl-cpu-gpu-11_hu_c078d8bd94df56aa.webp 1024w,/2017/07/cpu-or-gpu/dl-cpu-gpu-11.png 1200w" src="dl-cpu-gpu-11.png"/> 
</figure>

<figure>

    <img loading="lazy" srcset="/2017/07/cpu-or-gpu/dl-cpu-gpu-12_hu_44c1d2cc10581f1a.webp 320w,/2017/07/cpu-or-gpu/dl-cpu-gpu-12_hu_27c08aabe3a3cacd.webp 768w,/2017/07/cpu-or-gpu/dl-cpu-gpu-12_hu_d41db5a45ef62daf.webp 1024w,/2017/07/cpu-or-gpu/dl-cpu-gpu-12.png 1200w" src="dl-cpu-gpu-12.png"/> 
</figure>

<h2 id="conclusion">Conclusion</h2>
<p>As it turns out, using 64 vCPUs is <em>bad</em> for deep learning as current software/hardware architectures can&rsquo;t fully utilize all of them, and it often results in the exact same performance (or <em>worse</em>) than with 32 vCPUs. In terms balancing both training speed and cost, training models with <strong>16 vCPUs + compiled TensorFlow</strong> seems like the winner. The 30%-40% speed boost of the compiled TensorFlow library was an unexpected surprise, and I&rsquo;m shocked Google doesn&rsquo;t offer a precompiled version of TensorFlow with these CPU speedups since the gains are nontrivial.</p>
<p>It&rsquo;s worth nothing that the cost advantages shown here are <em>only</em> possible with preemptible instances; regular high-CPU instances on Google Compute Engine are about 5x as expensive, and as a result eliminate the cost benefits completely. Hooray for economies of scale!</p>
<p>A major implicit assumption with the cloud CPU training approach is that you don&rsquo;t need a trained model ASAP. In professional use cases, time may be too valuable to waste, but in personal use cases where someone can just leave a model training overnight, it&rsquo;s a very, very good and cost-effective option, and one that I&rsquo;ll now utilize.</p>
<hr>
<p><em>All scripts for running the benchmark are available in <a href="https://github.com/minimaxir/deep-learning-cpu-gpu-benchmark">this GitHub repo</a>. You can view the R/ggplot2 code used to process the logs and create the visualizations in <a href="http://minimaxir.com/notebooks/deep-learning-cpu-gpu/">this R Notebook</a>.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>Leaving Apple Inc.</title>
      <link>https://minimaxir.com/2017/05/leaving-apple/</link>
      <pubDate>Thu, 04 May 2017 09:30:00 -0700</pubDate>
      <guid>https://minimaxir.com/2017/05/leaving-apple/</guid>
      <description>I have made the personal decision to leave my job at Apple to further my personal growth and technical skills.</description>
      <content:encoded><![CDATA[<p>I’ve been working in the San Francisco Bay Area for about 5 years, but I’ve never publicly said where I’ve worked. Well, I was a Software QA Engineer at <a href="https://www.apple.com">Apple Inc.</a>, on the Applications team.</p>
<p>As of last week, I handed in my resignation. While I am thankful for the opportunities I have had at Apple, it is time for me to pursue working in other areas I am passionate about and search for other companies to further my personal growth and technical skills. Resigning from a good job to look for something new might defy conventional wisdom, but the time is right for me to make this bold career move.</p>
<h2 id="my-apple-story">My Apple Story</h2>
<p>I graduated with university honors at <a href="http://www.cmu.edu">Carnegie Mellon University</a>, from the <a href="http://tepper.cmu.edu">Tepper School of Business</a> with a focus on Computing and Information Technology (i.e. data architecture and coding algorithms), and a minor in Statistics.</p>
<p>At the end of my senior year, I received an e-mail from a Software QA Manager at Apple (who followed my <a href="http://techcommntr.tumblr.com">comments</a> at the bottom of <a href="https://techcrunch.com">TechCrunch</a> articles) inviting me for an on-site interview. Following an offer, I moved to the Bay Area to start my first post-undergrad job in Cupertino.</p>
<p>While I can&rsquo;t really talk about what I worked on at Apple, I genuinely enjoyed the work, the product, and team. I had a high impact on the final result and I successfully helped qualify many major software releases. However, after a few years, I realized that my technical skill growth was stalling, so I looked for an an internal transfer to another department, ideally in a data analysis/software engineering role.</p>
<p>Having received no responses internally, I realized I would have to expand my search to outside of Apple.</p>
<h2 id="my-job-hunt">My Job Hunt</h2>
<p>I have a strong technical background from my CMU classes, but not having an explicit Computer Science degree has made it difficult to prove aptitude despite my positive annual reviews and proven experience / technical skills at Apple. So I made the decision to blog with a technical focus here at <a href="http://minimaxir.com">minimaxir.com</a>, which gave me an avenue to showcase my programmatic skills and the opportunity to self-learn practical new tools not covered during the school curriculum, such as <a href="https://www.python.org">Python</a>, <a href="http://ggplot2.org">ggplot2</a>, version control with <a href="https://git-scm.com">git</a>, and reproducible analyses via <a href="http://jupyter.org">Jupyter/IPython Notebooks</a>.</p>
<p>This approach has been successful and many readers have liked my my blog posts: often topping <a href="https://www.reddit.com/r/dataisbeautiful/comments/4bwr7o/relationship_between_rotten_tomatoes_tomatometer/">Reddit</a> and <a href="https://news.ycombinator.com/item?id=13429656">Hacker News</a>, driving hundreds of thousands of pageviews. Additionally, a couple of my posts were even cited in larger publications such as the <a href="https://www.washingtonpost.com/news/the-intersect/wp/2016/06/30/facebook-news-feed-and-the-tyranny-of-positive-content/">Washington Post</a> and <a href="https://www.buzzfeed.com/tomphillips/photos-that-prove-game-of-thrones-happened-in-real-life">BuzzFeed</a>.</p>
<p>I also published many open-source technical projects to my <a href="https://github.com/minimaxir">GitHub</a>. My <a href="https://github.com/minimaxir/big-list-of-naughty-strings">Big List of Naughty Strings</a>, a project I made in a couple hours on a weekend inspired by my QA-ing at work, is now at <strong>20,000+ Stars</strong> on GitHub. My <a href="https://github.com/minimaxir/facebook-page-post-scraper">Facebook Page Post Scraper</a>, which does what the name implies, is now at 1,000+ Stars and has been used by many other businesses and journalists.</p>
<p>Developers have long argued that job seekers should have a strong public portfolio, as demonstrated experience can account for the lack of a relevant degree. After years of building up my portfolio, it became apparent that most outside recruiters I talked with never looked at my blog/GitHub, despite a strong emphasis of both on my résumé.</p>
<p>I subsequently rededicated my blog as a pragmatic demonstration of relevant skills in the data analysis job market, focusing more on practical analysis instead of quirky insights and thoughts. In the process, I obtained proficiency in a number of modern tools, including <a href="http://minimaxir.com/2016/08/clickbait-cluster/">interactive data visualizations</a> on the web with <a href="https://plot.ly">Plotly</a>, processing <a href="http://minimaxir.com/2017/01/amazon-spark/">big data</a> with <a href="http://spark.apache.org">Apache Spark</a>, high-performance <a href="http://minimaxir.com/2017/02/predicting-arrests/">machine learning</a> with <a href="https://github.com/dmlc/xgboost">xgboost</a> and <a href="https://github.com/Microsoft/LightGBM">LightGBM</a>, and even <a href="http://minimaxir.com/2017/04/char-embeddings/">deep learning</a> with <a href="https://github.com/fchollet/keras">Keras</a> and <a href="https://github.com/tensorflow/tensorflow">TensorFlow</a>.</p>
<p>I am now actively looking for a <strong>data analyst/software engineering job within San Francisco</strong>. If you are interested or if you know of companies who are looking for qualified people, please send me an email at <strong><a href="mailto:max@minimaxir.com">max@minimaxir.com</a></strong>.</p>
<h2 id="next-steps">Next Steps</h2>
<p>So I’ll be using my time over the next couple weeks to openly look for a new job, and to network with others in relevant industries (and be able to interview without taking a day off of work). Things have been improving: my <a href="https://news.ycombinator.com/item?id=14238066">comment</a> in the Hacker News &ldquo;Who wants to be hired?&rdquo; thread generated many leads who really liked my blog/portfolio. If you’d like to meet up in San Francisco and talk about tech and data stuff, just let me know.</p>
<p>I still intend to continue blogging, not as a hobby but in a more purposeful way. I have very ambitious goals and now have more time to execute them at a deeper level. Plans include:</p>
<ul>
<li>Web applications leveraging deep learning models, deployed at scale with <a href="https://www.docker.com">Docker</a>/<a href="https://kubernetes.io">Kubernetes</a>.</li>
<li>Interactive data dashboards accompanying every analytical blog post with <a href="https://shiny.rstudio.com">Shiny</a>.</li>
<li>Code screencasts at 4k resolution on <a href="https://youtube.com/minimaxir">YouTube</a>.</li>
<li>Data analysis live-streaming with augmented functionality on <a href="https://www.twitch.tv/minimaxir">Twitch</a>.</li>
</ul>
<p>I have set up a <strong><a href="https://www.patreon.com/minimaxir">Patreon</a></strong> in order to subsidize my machine learning/deep learning/software/hardware needs for my blog posts. If you have found any of my blog posts useful, a monetary contribution to my Patreon would be appreciated and will be put to good creative use.</p>
<p>If you want to keep up with me and my projects, feel free to follow me on <strong><a href="https://www.facebook.com/max.woolf">Facebook</a></strong> and <strong><a href="https://twitter.com/minimaxir">Twitter</a></strong> too.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
