<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.9.5">Jekyll</generator><link href="https://tobiasvanderwerff.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://tobiasvanderwerff.com/" rel="alternate" type="text/html" /><updated>2026-03-22T09:26:51+00:00</updated><id>https://tobiasvanderwerff.com/feed.xml</id><title type="html">Tobias van der Werff</title><subtitle>Personal website of Tobias van der Werff</subtitle><author><name>Tobias van der Werff</name></author><entry><title type="html">I Hacked My Dehumidifier to Control it Over WiFi</title><link href="https://tobiasvanderwerff.com/2025/03/25/dehumidifier.html" rel="alternate" type="text/html" title="I Hacked My Dehumidifier to Control it Over WiFi" /><published>2025-03-25T00:00:00+00:00</published><updated>2025-03-25T00:00:00+00:00</updated><id>https://tobiasvanderwerff.com/2025/03/25/dehumidifier</id><content type="html" xml:base="https://tobiasvanderwerff.com/2025/03/25/dehumidifier.html"><![CDATA[<p><em>See disccusion on <a href="https://www.reddit.com/r/esp32/comments/1jjg4mr/i_retrofitted_an_esp32_to_my_dehumidifier_to/">Reddit</a></em>.</p>

<p>—<br /></p>

<p>Many modern devices are connected to the internet – thermostats, light bulbs, vacuum cleaners, you name it. It’s certainly convenient to control your house’s thermostat using your phone, or to have a robot vacuum your house. It’s also easy to take this <a href="https://www.wired.com/story/strangest-internet-of-things-devices/">too far</a>, but it’s still a fun gimmick to control your devices from an app on your smartphone.</p>

<p>Something I’ve been wondering lately, is, how hard would it be to make your own smart devices? They typically <a href="https://arstechnica.com/gadgets/2025/03/everything-you-say-to-your-echo-will-be-sent-to-amazon-starting-on-march-28/">harvest lots of data about you</a> which they subsequently transmit over the internet, which is less than ideal for privacy reasons. Instead, if you make your own smart device, you’d have full control over what the device can and can’t do. This means you wouldn’t have to install spyware on your phone or give out all your personal information just to turn on your CO2 monitor.</p>

<blockquote>
  <p>This CO2 monitor with thousands of positive reviews demanded that I create an account, download their app and allow precise location information before it would report the amount of CO2 in my room. – Andrej Karpathy (<a href="https://karpathy.bearblog.dev/i-love-calculator/">I love calculator</a>)</p>
</blockquote>

<p>Making my own smart device seemed like a fun challenge. I’ve been teaching myself electronics in the past year, so as a learning project, I wanted to turn one of my “dumb” household appliances into one that could be monitored and controlled remotely.</p>

<p>First, I needed to pick a device to work on. I recently bought a dehumidifier, which is a device that extracts moisture from the air to control humidity levels. (I live in a house with a bad humidity problem.) This particular device does not have any native wireless capabilities, so it seemed like a good candidate for my project.</p>

<figure>
<img class="center" style="width:60%;" src="/assets/images/dehumidifier/2025-03-23_13.58.07-1.webp" />
<figcaption>This poor dehumidifier will be my lab rat for this project.</figcaption>
</figure>

<p>The basic idea would be to turn the device on and off remotely, using some kind of wireless protocol. So, let’s say, I want to turn the dehumidifier on, but I’m too lazy to get off the couch. All I would need to do to control it is to send a command using my phone or laptop. This way, I don’t have to walk five meters, which means I can preserve precious energy for other tasks.</p>

<p>The first order of business was to open the dehumidifier up to see what we’re dealing with. On the top of device is a numeric display, four buttons, and some small LEDs showing various status information, like the speed of the fan, or whether the water tank needs to be emptied.</p>

<figure>
<img class="center" style="width:75%;" src="/assets/images/dehumidifier/2025-03-23_09.48.06.webp" />
<figcaption>The top of the dehumidifier.</figcaption>
</figure>

<p>After removing about ten screws and unlocking some plastic clips, the inside of the device looks like this. The image shows a fan at the top, and on the bottom an enclosure with an LED screen (for displaying the humidity level), next to four spring-controlled buttons for interacting with the device.</p>

<figure>
<img class="center" style="width:75%;" src="/assets/images/dehumidifier/2025-02-28_15.05.58.webp" />
</figure>

<p>Just for some background, here’s the basic TL;DR for how a dehumidifier works. First, it uses a fan to draw in humid air. This air is passed over a cooling surface, which condenses the moisture into liquid water. The condensed water collects in a tank or is drained away. After the moisture is removed, the now-dry air is reheated and released back into the room by the fan. Obviously, this mechanism is actuated by a bunch of electronic components in the device, which we can access by opening it up.</p>

<p>Coming back to the disassembling, we can see the main circuitry of the device when we remove its enclosure:</p>

<figure>
<img class="center" style="width:100%;" src="/assets/images/dehumidifier/2025-02-28_15.29.40-1.webp" />
</figure>

<p>Much of the circuitry in the top of the image is dedicated to stepping down a mains 220-240 Volt AC input into a lower voltage DC output. The mains power comes in from the left of the image and gets transformed to a 5V DC power required for the fan and other things like the button controls.</p>

<p>The main thing I was interested in though, were the spring-based buttons next to the LED display. These four springs correspond to the four buttons the user can press on the top of the device. They are normally covered with a plastic surface, and actuated by pressing down on the plastic at the location of the button.</p>

<p>In order not to over-complicate things, I wanted to focus on controlling the power button specifically. My idea was as follows: If I could somehow simulate the electrical signal that the spring connected to the power button generates, I could use this to remotely trigger the on/off switch.</p>

<p>To find out the mechanism by which the power button operates, I first had to figure out what the spring was connected to on the other side of the PCB. Disconnecting the PCB and turning it over reveals the main microcontroller as well as the wiring around it:</p>

<figure>
<img class="center" style="width:75%;" src="/assets/images/dehumidifier/2025-03-22_10.24.50.webp" />
</figure>

<p>This board is relatively straightforward. You have a microcontroller in the middle, which acts as the brains of the device, sending and receiving signals to other components. Then, you mostly have a bunch of resistors and capacitors around the microcontroller, with traces leading to the LEDs and buttons on the other side of the board. It’s hard to figure out exactly what the microcontroller is doing (it’s quite literally a black box) so instead, we can look at the wiring to and from the chip to figure out what is going on.<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup></p>

<p>We can start by looking at the points where the springs are connected, shown in the image below. The connection on the right leads to the spring-based power button we’re interested in.</p>

<figure>
<img class="center" style="width:75%;" src="/assets/images/dehumidifier/2025-03-22_10.25.10_marked.webp" />
</figure>

<p>If you look closely at the image, there is a connection (also called a trace) along the top of the board, coming from the spring connection, through a resistor, and into the microcontroller. What this most likely means is that the power signal for the device gets sent on this trace. Therefore, we can start by manually injecting a signal here to figure out how we can trigger the power signal ourselves.</p>

<p>I was definitely confused by the spring-based buttons, and it took me a while to figure out how they worked. At first I thought that pressing down on the spring would somehow make contact with an underlying conductive path, which would pull a specific signal high or low (depending on what the microcontroller responds to). But one of the most confusing things was that I could trigger the switch simply by making contact with the pads, even without using any electrical signals. For example, by touching the pad with the leads of my multimeter when it was turned off, or with my finger. It was clear that somehow, the switch was extremely sensitive to any kind of contact or electrical signal, no matter how small.</p>

<p>After some research, I figured out the spring is actually a <strong>capacitive touch sensor</strong>. They’re a bit confusing to understand, but the basic mechanism is that when your finger makes contact with the button, it forms a tiny capacitor with the spring, which is picked up by the circuit and interpreted as a button press. These kinds of sensors are commonly used for appliances that require some kind of “touch” buttons as a replacement for mechanical push switches.</p>

<p>Doing an online search quickly revealed similar looking sensors, like this product from AliExpress:</p>

<figure>
<img class="center" style="width:100%;" src="/assets/images/dehumidifier/capacitive_sensor_AliExpress.webp" />
</figure>

<p>Unfortunately, it’s apparently not straightforward to control these capacitive touch sensors if you want to modify them. Luckily, I found this great <a href="https://www.youtube.com/watch?v=lCHeQKXGfXw">YouTube video</a> that shows a circuit design which is capable of controlling them nonetheless. This is what the circuit diagram looks like:</p>

<figure>
<img class="center" style="width:100%;" src="/assets/images/dehumidifier/circuit_diagram_leos_bag_of_tricks.svg" />
</figure>

<p>The basic idea is that the two 1N4148 diodes act as switches. When the mechanical switch is closed, the two diodes are forward biased and pass a tiny bit of current to the capacitive sensor, which triggers it. When the switch is open, the point between the two diodes has very high impedance, and no current can pass. The <a href="https://www.youtube.com/watch?v=lCHeQKXGfXw">video</a> explains the circuit in more detail.</p>

<p>I put together a breadboard prototype and lo and behold, I was now able to successfully trigger the switch!</p>

<figure>
<img class="center" style="width:75%;" src="/assets/images/dehumidifier/2025-03-16_11.18.14.webp" />
<figcaption>Testing the circuit…</figcaption>
</figure>

<p>I added a <a href="https://www.digikey.nl/en/articles/simply-put-2n3904-npn-transistor">2n3904 transistor</a> to the circuit to act as an electronic switch rather than a mechanical one, with an added resistor at the base pin.<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup> I then put all the components on a perfboard and soldered them together. The result looks like this:</p>

<figure>
<img class="center" style="width:75%;" src="/assets/images/dehumidifier/2025-03-21_08.45.05.webp" />
</figure>

<p>Now I just needed a way to send a signal to the dehumidifier wirelessly. After some research, I settled on the <a href="https://en.wikipedia.org/wiki/ESP32">ESP32</a> microcontroller. This device is somewhat similar to an Arduino, but with integrated Bluetooth and WiFi. You can write custom programs in, for example, C or Python, and load them onto the microcontroller using a USB connection. I was actually amazed to find out how cheap these little devices are – you can pick them up for not much more than $5 on AliExpress.</p>

<p>The program for the ESP32 is quite simple. It spawns a basic HTTP server which allows it to receive incoming requests over the WiFi network. Whenever it receives a request, it sends a signal from one of its GPIO pins (in this case GPIO 5) to the base of the transistor to trigger it, which sends the signal to the capacitive sensor through the custom circuit I just showed you.</p>

<p>The final circuit diagram looks like this:</p>

<figure>
<img class="center" style="width:100%;" src="/assets/images/dehumidifier/circuit_diagram.svg" />
</figure>

<p>I actually found out that there’s a mistake in this circuit, which I found out only after I soldered it in place. Luckily for me, it still worked.<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup></p>

<p>After some more testing to verify that things worked correctly, it was time to solder everything together. The perfboard needs to connect to the ESP32, and they both need to receive 5V power from the dehumidifier. Luckily for me, there was an unused port on the dehumidifier PCB which gave me a 5V power source.<sup id="fnref:4" role="doc-noteref"><a href="#fn:4" class="footnote" rel="footnote">4</a></sup></p>

<figure>
<img class="center" style="width:100%;" src="/assets/images/dehumidifier/2025-03-22_10.51.13.webp" />
<figcaption>Not the prettiest soldering, but hey, it works!</figcaption>
</figure>

<p>I also had to find a place to mount the new components. There was a neat little empty space under the PCB that could nicely fit all my components.</p>

<figure>
<img class="center" style="width:100%;" src="/assets/images/dehumidifier/2025-03-22_10.22.16-2.webp" />
<figcaption>The components placed into the dehumidifier. The ESP32 is on the left, and the custom circuit on the right.</figcaption>
</figure>

<p>After putting back the other PCB, the new components now snugly fit into the box:</p>

<figure>
<img class="center" style="width:100%;" src="/assets/images/dehumidifier/2025-03-22_11.01.47.webp" />
</figure>

<p>I created a simple web app (which is just a big green button) for controlling the device. It works surprisingly well! Here’s a demo of the final result:</p>

<video class="center" style="width:100%;" controls="">
  <source src="/assets/images/dehumidifier/final_demo.mp4" type="video/mp4" />
  Your browser does not support the video tag.
</video>

<h2 id="conclusion">Conclusion</h2>

<p>This was a fun project to work on. If I want to expand the functionality, I could add more sensors to the ESP32, e.g. for temperature and humidity, and turn the web interface into a proper dashboard. I have a bag of cheap sensors from AliExpress lying around – maybe I’ll try connecting those at some point.</p>

<p>It can be pretty intimidating to modify a device like this because there’s lots of unknown unknowns (who knows what you could break?), especially for someone like me with limited experience in electronics. Needless to say, I was pleasantly surprised by how well the whole thing works! It’s great to see all the individual pieces come together like that.</p>

<hr />
<p><br /></p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p>Another option may have been to listen on the UART communication sent on the Rx and Tx pins to figure out what the microcontroller is doing. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2" role="doc-endnote">
      <p>I actually had to try out a bunch of different resistor values to find one that sufficiently saturated the transistor. I initially tried to calculate this resistor value using theory, but eventually came to the conclusion it was much more effective to simply measure the voltage drop across the transistor using a multimeter in order to find the best resistor value. <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:3" role="doc-endnote">
      <p>I had contact with <a href="https://www.youtube.com/@leosbagoftricks3732">Leo</a>, the author of the YouTube video, and apparently I used his circuit in a way that he didn’t intend at all. This is because his circuit was designed to connect <em>capacitively</em> to the circuit, not with a direct connection, which was what I was using. But somehow, it still works! I asked Claude 3.7 to analyze this circuit and it says transient voltage changes may be the reason why this ciruit still works. According to Leo, a better solution would be to put a small capacitor (20 pF or so) in series with the wire to the sensor, to block DC to the dehumidifier PCB. Anyway, you should probably take my circuit design with a grain of salt, because I barely know what I’m doing. <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:4" role="doc-endnote">
      <p>Fun fact: this port was labeled “WFI” (i.e. WiFi?). Perhaps the original design of the dehumidifier actually included WiFi control! <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Tobias van der Werff</name></author><category term="electronics" /><category term="hardware" /><summary type="html"><![CDATA[See disccusion on Reddit.]]></summary></entry><entry><title type="html">The Age of AI-Powered Curiosity</title><link href="https://tobiasvanderwerff.com/2024/12/01/ai-powered-curiosity.html" rel="alternate" type="text/html" title="The Age of AI-Powered Curiosity" /><published>2024-12-01T00:00:00+00:00</published><updated>2024-12-01T00:00:00+00:00</updated><id>https://tobiasvanderwerff.com/2024/12/01/ai-powered-curiosity</id><content type="html" xml:base="https://tobiasvanderwerff.com/2024/12/01/ai-powered-curiosity.html"><![CDATA[<p>When I was a kid, I used to ask my mother all kinds of questions about the world, asking things like <em>Why is the sky blue?</em> or <em>Why does the sun disappear at night?</em> Although my mother is a patient woman, she would usually get tired of my never-ending stream of <em>why</em> questions pretty quickly. Usually, this meant that she would shut down the conversation with a simple “because that’s just how it is”.</p>

<p>Obviously my mom is not to blame for not knowing every single random fact I was interested in at that age. Since I could not find most of the answers I was looking for, I remember fantasizing about having a magical electronic device – a handheld device, like a Game Boy – that could answer all my questions about the world. I would simply type in a question and get the answer back from the device. No subject would be off limits. Such an oracle of knowledge seemed to me like true magic, and I never seriously considered that such a device could actually exist.</p>

<p>But the magical technology I dreamt of as a kid actually exists in the world we live in today.</p>

<h2 id="enter-llms">Enter LLMs</h2>

<p>What I envisioned then exists today in the form of large language models – extremely large neural networks trained on massive amounts of internet data. This training has given these models vast knowledge of the world. To the degree that information can be found on the internet, LLMs have memorized it. The addition of RLHF (Reinforcement Learning from Human Feedback) has made interaction with these models as accessible as having a conversation with another person. The result is nothing short of spectacular, and I believe the world has yet to realize the true potential of this technology.</p>

<p>One of my favorite use cases for LLMs in day-to-day life is to use them as a tool for exploration and curiosity-based research. This can be for the purpose of <a href="/2024/06/17/useful-knowledge.html">solving a concrete problem</a>, or just being curious about how things work and wanting to dive deeper. For instance, I recently found myself wondering about several questions:</p>

<ul>
  <li>“Why does money become worth less over time?”</li>
  <li>“What makes kerosone suitable for airplane fuel?”</li>
  <li>“Why don’t we have electric airplanes?”</li>
</ul>

<p>If you were to do an internet search, especially for very specific questions, you would typically need some time to find a good answer. However, these days, I simply ask an LLM:</p>

<figure>
    <img class="center" src="/assets/images/ai-powered-curiosity/electric-airplanes.png" alt="A conversation with Claude 3.5 Sonnet about electric airplanes." />
    <figcaption>
    Claude 3.5 Sonnet answering my random questions about the world. Source: Perplexity.
    </figcaption>
</figure>

<p>The best part: I can get answers to questions like this in a matter of seconds. I’m only limited by my typing speed, or I can even state my questions out loud by using text-to-speech on most LLM providers. One of the absolute best parts about this is that you can ask as many detailed follow-up questions as you want. Learning becomes <em>personalized</em>, since we can all ask the questions we personally find most interesting. Above all, this makes learning so much more <em>fun</em>. With an LLM assistant, you can have an incredibly short feedback loop that allows you to dive into any aspect of the answer you find most interesting.</p>

<p>This short feedback loop is in stark contrast to how information used to be accessed for most of human history. If we think about the evolution of the speed of information retrieval, I imagine it would have gone something like this:</p>

<table>
  <thead>
    <tr>
      <th>Era</th>
      <th>Method of Information Retrieval</th>
      <th>Estimated Time</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Pre-Printing Press (before 1450)</td>
      <td>Find an expert human to answer your question</td>
      <td>Weeks-years</td>
    </tr>
    <tr>
      <td>Print Era (1450-1990)</td>
      <td>Search through books</td>
      <td>Hours-days</td>
    </tr>
    <tr>
      <td>Internet Age (Post-2000s)</td>
      <td>Google search and browse websites</td>
      <td>Minutes</td>
    </tr>
    <tr>
      <td>AI Age (2020-present)</td>
      <td>Ask an LLM</td>
      <td>Seconds</td>
    </tr>
  </tbody>
</table>

<p>What this means is that access to information used to be highly dependent on your geographical location, whereas in current times, all you need is an internet connection and a few seconds of your time! Furthermore, in the age of AI, the quality of the information retrieval will also be higher, since answers are synthesized directly based on questions.<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup></p>

<p>Finally – and this might sound a bit silly –, I think another great aspect of LLMs is that they don’t judge you. I imagine most people can relate to the feeling of not asking certain basic questions because we don’t want to appear <a href="https://danluu.com/look-stupid/">dumb</a> or ignorant. With an AI assistant, this is not a problem – it can be as patient and as compassionate as you want it to be, and it won’t judge you in any way. This makes it easier for me to ask questions I’m curious about, no matter how basic they might be.</p>

<h2 id="its-still-early-days">It’s still early days</h2>

<p>There tends to be lots of talk about the progress of AI in years to come. However, there might not be enough appreciation of what AI can already do for us today. I imagine a similar thing happened back when the internet was invented. Wikipedia founder Jimmy Wales noted that the technical capability for an online encyclopedia existed well before its creation. The breakthrough wasn’t technological – it was conceptual, requiring a shift in thinking about collaborative content creation. Similarly, even if AI were to stay at exactly the same level as it is today, we will still find lots of applications for it that will make all of our lives better. In other words, we are still in the early stages of figuring out how to effectively use this weird new technology.</p>

<p>Hallucinations are another aspect that tend to be brought up as a major obstacle for making LLMs useful. However, I think this is not as relevant for exploratory questions like the ones I mentioned earlier. If you ask an LLM about well-established topics or factual matters, it tends to do fine 99% of the time. Moreover, hallucination tends to be more of an issue for smaller models, whereas the frontier models (like ChatGPT 4, or Claude 3.5) suffer much less from this problem. Additionally, if you use a service like <a href="https://perplexity.ai">Perplexity</a> for web search (which I use every day), you can simply check the cited sources for verification.</p>

<h2 id="conclusion">Conclusion</h2>

<p>Working with LLMs is like working with alien technology – there’s no manual for it. So it’s always important to keep a critical mindset when evaluating an LLMs output. But with some training and awareness of how LLMs function, I believe there’s hardly a limit to how useful they can be.</p>

<p>LLMs have helped me regain some of the sense of wonder I used to have as a kid when I pestered my mother with random questions about the universe. Learning doesn’t have to be boring, and LLMs are making this abundantly clear. We are truly at the beginning of a new age of curiosity, and I’m looking forward to all the incredible applications that AI will make possible.</p>

<h2 id="related-reading">Related reading</h2>

<ul>
  <li><a href="https://marginalrevolution.com/marginalrevolution/2024/12/how-to-read-a-book-using-o1.html">Tyler Cowen: How to read a book using o1</a></li>
</ul>

<hr />
<p><br /></p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p>The way I sometimes try to explain this is that it’s like having a superhuman friend who has memorized the internet, whom you can directly ask any question. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Tobias van der Werff</name></author><category term="essay" /><summary type="html"><![CDATA[When I was a kid, I used to ask my mother all kinds of questions about the world, asking things like Why is the sky blue? or Why does the sun disappear at night? Although my mother is a patient woman, she would usually get tired of my never-ending stream of why questions pretty quickly. Usually, this meant that she would shut down the conversation with a simple “because that’s just how it is”.]]></summary></entry><entry><title type="html">CUDA C++: Using __CUDA_ARCH__ the Right Way</title><link href="https://tobiasvanderwerff.com/2024/11/01/cuda-arch.html" rel="alternate" type="text/html" title="CUDA C++: Using __CUDA_ARCH__ the Right Way" /><published>2024-11-01T00:00:00+00:00</published><updated>2024-11-01T00:00:00+00:00</updated><id>https://tobiasvanderwerff.com/2024/11/01/cuda-arch</id><content type="html" xml:base="https://tobiasvanderwerff.com/2024/11/01/cuda-arch.html"><![CDATA[<p><strong>TL;DR</strong>: Read <a href="https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#cuda-arch">this section of the CUDA C++ Programming Guide</a> before using the <code class="language-plaintext highlighter-rouge">__CUDA_ARCH__</code> macro so that you’re aware of cases where it’s problematic.</p>

<p>—<br /></p>

<p>In the last year or so, I’ve become quite interested in low-level GPU programming for performance optimization of neural networks. Diving into CUDA programming has been a fun (and challenging!) way to explore the magic behind what makes neural nets run fast on GPUs.<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup> I tend to do a lot of Python programming, so stepping down into the depths of low-level GPU programming has been, well… illuminating. If there’s one thing I’ve noticed, it’s that learning to write high-performance CUDA code is like learning to program in hard mode. In my experience, one does not simply “learn” CUDA – one merely becomes less incompetent over time. The devil tends to be in the details when you write CUDA code, and the details are far too easy to miss if you’re not careful.</p>

<p>In this post, I want to highlight a specific detail of CUDA C/C++ that can easily lead to mistakes: the <code class="language-plaintext highlighter-rouge">__CUDA_ARCH__</code> macro. It’s caused me a fair bit of confusion in the last few days, so this post acts as an overview of my findings on how to use it while avoiding some of its pitfalls.</p>

<h2 id="what-is-the-__cuda_arch__-macro">What is the __CUDA_ARCH__ macro?</h2>

<p>The <code class="language-plaintext highlighter-rouge">__CUDA_ARCH__</code> macro is used to write code that behaves differently depending on your GPU architecture. NVIDIA tends to release a new GPU architecture every two years or so — which may provide functionality that previous generations did not — so it’s quite useful to have the ability to write architecture-specific code.</p>

<p>Inside device code (CUDA code), the <code class="language-plaintext highlighter-rouge">__CUDA_ARCH__</code> macro expands to the compute capability of the GPU that you’re compiling for. For example, on an NVIDIA A100, which has compute capability sm80, <code class="language-plaintext highlighter-rouge">__CUDA_ARCH__</code> will expand to <code class="language-plaintext highlighter-rouge">800</code>. For a GeForce RTX 4090, which has compute capability sm89, <code class="language-plaintext highlighter-rouge">__CUDA_ARCH__</code> will expand to <code class="language-plaintext highlighter-rouge">890</code>. We can use the value of <code class="language-plaintext highlighter-rouge">__CUDA_ARCH__</code> to conditionally include code that may only work for specific GPU architectures.</p>

<p>As a real-world example, here’s a snippet from <a href="https://github.com/Dao-AILab/flash-attention/blob/478ee666cccbd1b8f63648633003059a8dc6827d/csrc/flash_attn/src/kernel_traits.h#L18">Flash Attention</a> that sets certain traits depending on whether the GPU architecture is at least <a href="https://en.wikipedia.org/wiki/Ampere_(microarchitecture)">Ampere</a> (sm80):</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">template</span><span class="o">&lt;</span><span class="p">...&gt;</span>
<span class="k">struct</span> <span class="nc">Flash_kernel_traits</span> <span class="p">{</span>

<span class="cp">#if defined(__CUDA_ARCH__) &amp;&amp;  __CUDA_ARCH__ &gt;= 800
</span>    <span class="k">using</span> <span class="n">Element</span> <span class="o">=</span> <span class="n">elem_type</span><span class="p">;</span>
    <span class="k">static</span> <span class="k">constexpr</span> <span class="kt">bool</span> <span class="n">Has_cp_async</span> <span class="o">=</span> <span class="nb">true</span><span class="p">;</span>
<span class="cp">#else
</span>    <span class="k">using</span> <span class="n">Element</span> <span class="o">=</span> <span class="n">cutlass</span><span class="o">::</span><span class="n">half_t</span><span class="p">;</span>
    <span class="k">static</span> <span class="k">constexpr</span> <span class="kt">bool</span> <span class="n">Has_cp_async</span> <span class="o">=</span> <span class="nb">false</span><span class="p">;</span>
<span class="cp">#endif
</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Note the use of <code class="language-plaintext highlighter-rouge">Has_cp_async</code>: The <code class="language-plaintext highlighter-rouge">cp_async</code> <a href="https://docs.nvidia.com/cuda/parallel-thread-execution/#data-movement-and-conversion-instructions-cp-async">operation</a> requires sm80 or higher, so the <code class="language-plaintext highlighter-rouge">Has_cp_async</code> boolean is a way to set the correct instructions at compile time by using <a href="https://cplusplus.com/doc/tutorial/preprocessor/">preprocessor directives</a>.</p>

<h2 id="the-problem-undefined-behavior">The problem: undefined behavior</h2>

<p>Now, what I want to highlight is the cases where using <code class="language-plaintext highlighter-rouge">__CUDA_ARCH__</code> is actually very problematic, and needs to be avoided.
In particular, there are various situations where using it actually leads to <a href="https://en.wikipedia.org/wiki/Undefined_behavior">undefined behavior</a>. Luckily, the CUDA C++ programming guide has a <a href="https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#cuda-arch">section</a> that indicates the precise four situations where using <code class="language-plaintext highlighter-rouge">__CUDA_ARCH__</code> leads to undefined behavior<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup>. These are<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup>:</p>

<ol>
  <li>Setting type signatures for <code class="language-plaintext highlighter-rouge">__global__</code> functions and/or <code class="language-plaintext highlighter-rouge">__device__</code> and <code class="language-plaintext highlighter-rouge">__constant__</code> variables based on <code class="language-plaintext highlighter-rouge">__CUDA_ARCH__</code>.</li>
  <li>Instantiating function templates for <code class="language-plaintext highlighter-rouge">__global__</code> functions based on <code class="language-plaintext highlighter-rouge">__CUDA_ARCH__</code>.</li>
  <li>In separate compilation, using <code class="language-plaintext highlighter-rouge">__CUDA_ARCH__</code> to conditionally define a function or variable with external linkage.</li>
  <li>In separate compilation, using <code class="language-plaintext highlighter-rouge">__CUDA_ARCH__</code> in headers such that different objects could contain different behavior.</li>
</ol>

<p>For example, the following code block violates rule no. 1:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#if !defined(__CUDA_ARCH__)
</span><span class="k">typedef</span> <span class="kt">int</span> <span class="n">mytype</span><span class="p">;</span>
<span class="cp">#else
</span><span class="k">typedef</span> <span class="kt">double</span> <span class="n">mytype</span><span class="p">;</span>
<span class="cp">#endif
</span>
<span class="n">__global__</span> <span class="kt">void</span> <span class="nf">foo</span><span class="p">(</span><span class="n">mytype</span> <span class="n">in</span><span class="p">,</span> <span class="c1">// problem: foo's type depends on __CUDA_ARCH__</span>
                    <span class="n">mytype</span> <span class="o">*</span><span class="n">ptr</span><span class="p">)</span>
<span class="p">{</span>
  <span class="o">*</span><span class="n">ptr</span> <span class="o">=</span> <span class="n">in</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>What exactly happens when you write code that violates any of these four rules? The docs provide the answer:</p>

<blockquote>
  <p>The compiler does not guarantee that a diagnostic will be generated for the unsupported uses of __CUDA_ARCH__ described above.</p>
</blockquote>

<p>In other words, your code can now exhibit arbitrary behavior, including crashing, producing incorrect results, or even behaving as expected by coincidence. What’s worse, the compiler won’t even tell you that something is wrong.</p>

<p>Someone from the <a href="https://discord.gg/gpumode">GPU Mode</a> Discord channel said it best:</p>

<blockquote>
  <p>This is probably the scariest kind of thing you can have in C/C++: UB NDR (Undefined Behaviour, No Diagnostic Required)</p>
</blockquote>

<p>Scary stuff. If you take away nothing else from the current post, let it be this: read the aforementioned <a href="https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#cuda-arch">section</a> of the CUDA C++ programming guide before you find yourself using <code class="language-plaintext highlighter-rouge">__CUDA_ARCH__</code> in any kind of non-trivial way.</p>

<h2 id="case-study-torchao-fp6-kernel">Case study: torchao FP6 kernel</h2>

<p>To give you an impression of how things can go wrong, let’s look at a case study. I have been contributing to <a href="https://github.com/pytorch/ao">torchao</a> in recent months, a library for PyTorch native quantization and sparsity for training and inference. Specifically, I have been focusing on <code class="language-plaintext highlighter-rouge">torchao</code>s integration with <a href="https://arxiv.org/abs/2401.14112">FP6</a>, a high-performance CUDA kernel for 6-bit quantization. Most of the details aren’t important – I just want to highlight a small part of the code. Here’s a simplified version of what the structure of the FP6 code looked like at some point:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#if !defined(__CUDA_ARCH__) || __CUDA_ARCH__ &gt;= 800
</span>
<span class="k">template</span><span class="o">&lt;</span><span class="kt">int</span> <span class="n">EXPONENT</span><span class="p">,</span> <span class="kt">int</span> <span class="n">MANTISSA</span><span class="p">&gt;</span>
<span class="n">__global__</span> <span class="kt">void</span> <span class="nf">fpx_kernel</span><span class="p">(</span><span class="k">const</span> <span class="n">uint4</span><span class="o">*</span> <span class="n">weights</span><span class="p">,</span> <span class="k">const</span> <span class="n">half</span><span class="o">*</span> <span class="n">scales</span><span class="p">,</span>
                           <span class="k">const</span> <span class="n">half</span><span class="o">*</span> <span class="n">in_feats</span><span class="p">,</span> <span class="n">half</span><span class="o">*</span> <span class="n">out_feats</span><span class="p">,</span>
                           <span class="k">const</span> <span class="kt">size_t</span> <span class="n">M</span><span class="p">,</span> <span class="k">const</span> <span class="kt">size_t</span> <span class="n">N</span><span class="p">,</span> <span class="k">const</span> <span class="kt">size_t</span> <span class="n">K</span><span class="p">)</span>
<span class="p">{</span>
    <span class="c1">// CUDA code here ...</span>
<span class="p">}</span>


<span class="n">torch</span><span class="o">::</span><span class="n">Tensor</span> <span class="n">fp6_forward_cuda</span><span class="p">(</span>
    <span class="n">torch</span><span class="o">::</span><span class="n">Tensor</span>   <span class="n">_in_feats</span><span class="p">,</span>
    <span class="n">torch</span><span class="o">::</span><span class="n">Tensor</span>   <span class="n">_weights</span><span class="p">,</span>
    <span class="n">torch</span><span class="o">::</span><span class="n">Tensor</span>   <span class="n">_scales</span><span class="p">)</span>
<span class="p">{</span>
    <span class="c1">// Setup code here ...</span>
    
    <span class="c1">// Call the CUDA kernel</span>
    <span class="n">fpx_kernel</span><span class="o">&lt;</span><span class="mi">3</span><span class="p">,</span> <span class="mi">2</span><span class="o">&gt;&lt;&lt;&lt;</span><span class="n">grid_dim</span><span class="p">,</span> <span class="n">block_dim</span><span class="p">,</span> <span class="n">stream</span><span class="o">&gt;&gt;&gt;</span><span class="p">(</span>
        <span class="n">weights</span><span class="p">,</span> <span class="n">scales</span><span class="p">,</span> <span class="n">in_feats</span><span class="p">,</span> <span class="n">out_feats</span><span class="p">,</span> <span class="n">M</span><span class="p">,</span> <span class="n">N</span><span class="p">,</span> <span class="n">K</span><span class="p">);</span>
    
    <span class="k">return</span> <span class="n">out_feats</span><span class="p">;</span>
<span class="p">}</span>

<span class="cp">#endif
</span></code></pre></div></div>

<p>The idea here is that the FP6 kernel only supports sm80 and higher, so we use <code class="language-plaintext highlighter-rouge">__CUDA_ARCH__</code> to only include the FP6 code when <code class="language-plaintext highlighter-rouge">__CUDA_ARCH__ &gt;= 800</code> or when <code class="language-plaintext highlighter-rouge">__CUDA_ARCH__</code> is undefined (which is the case for host code). This avoids compilation problems when using GPUs with compute capability older than sm80.</p>

<p>The code above worked fine on sm80 GPUs and higher. However, when we called the above kernel on a sm75 GPU, we noticed that it behaved inconsistently. Sometimes, the code would error out saying that the kernel wasn’t defined (which was the expected outcome). However, other times, it would actually run without errors! (while producing garbage output.)<sup id="fnref:4" role="doc-noteref"><a href="#fn:4" class="footnote" rel="footnote">4</a></sup></p>

<p>If we refer to the four rules I mentioned earlier from the <a href="https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#cuda-arch">CUDA C++ Programming Guide</a>, it appears that we are violating rule no. 2. To quote the docs:</p>

<blockquote>
  <p>If a __global__ function template is instantiated and launched from the host, then the function template must be instantiated with the same template arguments irrespective of whether __CUDA_ARCH__ is defined and regardless of the value of __CUDA_ARCH__.</p>
</blockquote>

<p>Note that during compilation, the <code class="language-plaintext highlighter-rouge">nvcc</code> compiler makes a distinction between device code (code that runs on the GPU) and host code (code that runs on the CPU). Host code is forwarded to a standard C++ compiler (like <code class="language-plaintext highlighter-rouge">gcc</code> or <code class="language-plaintext highlighter-rouge">cl</code>), similar to any regular C++ program. The device code, on the other hand, is compiled by <code class="language-plaintext highlighter-rouge">nvcc</code>, the NVIDIA CUDA compiler. Afterwards, the device code is embedded into the host object file as a “fat binary”.</p>

<p>In the case of the FP6 code shown above, it would appear that the following is happening when we compile for an sm75 GPU:</p>

<ol>
  <li>For the compilation of host code, the <code class="language-plaintext highlighter-rouge">#if</code> directive will evaluate to true (since <code class="language-plaintext highlighter-rouge">!defined(__CUDA_ARCH__)</code> is true for host code), which means the <code class="language-plaintext highlighter-rouge">fpx_kernel&lt;3, 2&gt;</code> function template is instantiated.</li>
  <li>For the compilation of device code, the <code class="language-plaintext highlighter-rouge">#if</code> directive will evaluate to false (since <code class="language-plaintext highlighter-rouge">__CUDA_ARCH__ &lt; 800</code>), which means the <code class="language-plaintext highlighter-rouge">fpx_kernel&lt;3, 2&gt;</code> function template is <em>not</em> instantiated.</li>
</ol>

<p>This means there is a problematic divergence between the host and device code: The host code includes a <code class="language-plaintext highlighter-rouge">__global__</code> function symbol that the device code does not. What happens as a result is undefined behavior, i.e. anything can happen. This explained the inconsistent behavior of the kernel, which went away after addressing this issue.<sup id="fnref:5" role="doc-noteref"><a href="#fn:5" class="footnote" rel="footnote">5</a></sup></p>

<h2 id="further-reading">Further reading</h2>

<ul>
  <li><a href="https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#cuda-arch">CUDA C++ Programming Guide</a></li>
  <li><a href="https://leimao.github.io/blog/CUDA-Compilation-Architecture-Macro/">Lei Mao: CUDA Compilation Architecture Macro</a></li>
</ul>

<hr />
<p><br /></p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p>Shoutout to the GPU Mode <a href="https://discord.gg/gpumode">Discord community</a> (formerly known as CUDA Mode), which has some of the best <a href="https://www.youtube.com/@CUDAMODE">lectures</a> on CUDA I’ve seen, as well as being a great community of CUDA hackers from which I’ve learned a lot. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2" role="doc-endnote">
      <p>Thanks to <a href="https://github.com/gau-nernst">Thien Tran</a> for pointing this section out to me. <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:3" role="doc-endnote">
      <p>I’m omitting some of the details here for brevity. The CUDA C++ programming guide contains all the relevant details. <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:4" role="doc-endnote">
      <p>This actually happened in conjunction with another bug where the program failed silently because it did not check for errors after a kernel call. Always check for errors after calling a CUDA kernel! Here’s a good primer on proper CUDA error checking: <a href="https://leimao.github.io/blog/Proper-CUDA-Error-Checking/">https://leimao.github.io/blog/Proper-CUDA-Error-Checking/</a> <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:5" role="doc-endnote">
      <p>A possible fix is to use the <code class="language-plaintext highlighter-rouge">#if</code> directive inside the <code class="language-plaintext highlighter-rouge">fpx_kernel</code> function body instead of outside of it, such that the function symbols are the same for device and host code. For example, the function body can be left empty in the case of an unsupported GPU architecture. <a href="#fnref:5" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Tobias van der Werff</name></author><category term="tutorial" /><category term="programming" /><category term="cuda" /><category term="c++" /><summary type="html"><![CDATA[TL;DR: Read this section of the CUDA C++ Programming Guide before using the __CUDA_ARCH__ macro so that you’re aware of cases where it’s problematic.]]></summary></entry><entry><title type="html">The Lindy Effect: Explaining the Longevity of Legacy Software</title><link href="https://tobiasvanderwerff.com/2024/08/01/lindy.html" rel="alternate" type="text/html" title="The Lindy Effect: Explaining the Longevity of Legacy Software" /><published>2024-08-01T00:00:00+00:00</published><updated>2024-08-01T00:00:00+00:00</updated><id>https://tobiasvanderwerff.com/2024/08/01/lindy</id><content type="html" xml:base="https://tobiasvanderwerff.com/2024/08/01/lindy.html"><![CDATA[<p>“Wanted: people who want to learn a programming language from 1959,” <a href="https://www.nrc.nl/nieuws/2024/05/01/gezocht-mensen-die-een-programmeertaal-uit-1959-willen-leren-want-die-heeft-nog-altijd-toekomst-a4197669">headlined</a> a recent Dutch newspaper article. The language in question is COBOL, a language so old that most of the programmers who know it are either dead or retired. Nonetheless, banks, government institutions, and other large organizations still rely on this ancient language for some of their most critical infrastructure.</p>

<p>How is it possible that a language like COBOL is still used today? What has prevented these organizations from upgrading their software, like, several decades ago? I recently discovered a useful mental model for thinking about this: The Lindy effect.</p>

<h2 id="the-lindy-effect">The Lindy Effect</h2>

<p>The Lindy effect states that the longer an idea or technology has survived, the longer it is likely to stay alive. Nassim Taleb highlights this concept in his book <a href="https://en.wikipedia.org/wiki/Antifragile_(book)#Lindy_effect">Antifragile</a>, where he explains that for non-perishable things like ideas, music, and technology, age is a sign of robustness, which in turn is a positive indicator of future longevity.</p>

<blockquote>
  <p>“If a book has been in print for forty years, I can expect it to be in print for another forty years. But, and that is the main difference, if it survives another decade, then it will be expected to be in print another fifty years. This, simply, as a rule, tells you why things that have been around for a long time are not “aging” like persons, but “aging” in reverse. Every year that passes without extinction doubles the additional life expectancy. This is an indicator of some robustness. The robustness of an item is proportional to its life!“ — Nassim Taleb</p>
</blockquote>

<p>The typical framing of the Lindy effect is that those ideas that endure over time likely do so because they retain their usefulness, and therefore have long-term value.</p>

<h2 id="the-lindy-effect-applied-to-software">The Lindy Effect Applied to Software</h2>

<p>The Lindy effect provides an interesting framework for understanding the longevity of software. In particular, I’d like to discuss the rise and decline of programming languages as a manifestation of a similar effect. Why do some programming languages survive, and others don’t? For example, C and Pascal are both programming languages that originated in the 1970s. However, C remains widely used today, while Pascal has largely fallen out of favor.<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup></p>

<p>The typical Lindy explanation for this discrepancy relates to a difference in quality. The fact that C has survived this long and is still widely used indicates its durability and lasting value. By contrast, Pascal has faced more <a href="https://www.lysator.liu.se/c/bwk-on-pascal.html">issues</a> and has largely been replaced by more modern languages.</p>

<p>But quality is not the only reason why programming languages survive. Once a language gains sufficient traction, it’s existence can become self-reinforcing. Some software gets ingrained deeply enough into the infrastructure of a society to practically guarantee its future existence. The obsolescence of these critical pieces of software will only occur if there is a conscious and deliberate effort to replace them, which becomes less likely over time.</p>

<p>It helps to understand that software derives much of its greatest strength from its ability to be layered on top of other software. Over time, software becomes increasingly powerful, and increasingly complex. This is generally a positive thing. For instance, a video game programmer does not need to write their own physics simulation—they can use Unreal Engine. Similarly, a web developer can use a framework like React or Django to speed up their work.</p>

<p>However, there is a clear downside to this increasing complexity that relates to critical points of failure. Modern software is like a tower of stacked dependencies, and the oldest software tends to be at the bottom. As we create more software, the impact of the software at the bottom grows over time, as does the impact of changing it. This is the kind of software that becomes a critical point of failure for much of our digital infrastructure, where the smallest bugs or security flaws can have a gigantic ripple effect. Recent examples of this include the <a href="https://www.cnet.com/news/privacy/log4j-software-bug-cisa-issues-emergency-directive-to-federal-agencies/">the Log4j bug from 2021</a>, as well as the <a href="https://www.theverge.com/2024/7/19/24201864/crowdstrike-outage-explained-microsoft-windows-bsod">Crowdstrike bug</a> from this year that caused global outages.</p>

<figure>
<img class="center" style="width:50%;" src="https://imgs.xkcd.com/comics/dependency.png" alt="xkcd: dependency" />
<figcaption><a href="https://xkcd.com/2347/">xkcd</a></figcaption>
</figure>

<p>Over time, the effect of changing this kind of software becomes so large that making any serious modifications can be practically impossible. The massive complexity, scale, and interdependencies involved lead to a growing inertia—a resistance to change the existing software. A financial institution like a bank will think twice before touching the software that handles all of their financial transactions (written in COBOL)—the consequences of a mistake are simply too great.<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup></p>

<p>So if we think back to the example of COBOL, and why it is still being used to this day, we can understand why: It is too deeply ingrained into our existing infrastructure. Lots of dependencies and complexity have formed over time, which means that as a programmer, you just might crash the global financial system if you forget to add a semicolon somewhere.<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup></p>

<h2 id="bad-design-ossifies-software">Bad Design Ossifies Software</h2>

<p>Poor software design makes the problem of dependencies worse. There is a reason why software engineering best practices advocate for things like modularity and separation of concerns: it minimizes dependencies across a codebase. Problems arise when software modules make lots of assumptions about other modules they interact with, creating rigidity and an inability to change anything without lots of unforeseen consequences.</p>

<p>This usually follows a common pattern. Someone creates a temporary fix (hack), followed by lots of changes built on top of the original fix, making the original fix increasingly hard to remove. Organizations that take software health seriously eventually service this kind of technical debt and resolve the problematic dependencies. But organizations that don’t do this accumulate lots of problematic dependencies over time. Eventually, their codebase is such a jumbled mess of spaghetti code that no one dares change it (after all, who knows what will happen?).</p>

<p>Even well-designed software can be difficult to replace. Translating a codebase from one programming language to another leaves plenty of room for new and unforeseen bugs. This is the kind of thing that banks (rightfully) fear when they consider replacing their financial transactions software.</p>

<h2 id="old-software-aint-so-bad">Old Software Ain’t So Bad</h2>

<p>Old software isn’t inherently a bad thing and can provide lots of value. For instance, the older a piece of software is, the more likely it is that mistakes and bugs have been removed from it. Furthermore, new technology does not necessarily perform better than old technology—in fact, the opposite often holds true in software.</p>

<p>For example, Python is built on top of C. Although C is, by modern standards, a horrible language that no one should use, it is at least <em>fast</em>. As long as there are C programmers around who can maintain the old infrastructure, there isn’t much of a problem. Similarly, Fortran is still used in numerical analysis software because it is <a href="https://stackoverflow.com/questions/13078736/fortran-vs-c-does-fortran-still-hold-any-advantage-in-numerical-analysis-thes">highly efficient</a> for those kinds of workloads. Same thing with COBOL: It is highly optimized for financial transaction systems.</p>

<p>Instead, old software becomes problematic when it becomes increasingly hard to maintain over time. In the case of a language like COBOL, there are fewer and fewer engineers who can understand and modify the existing software. This reinforces the Lindy effect—no engineers are available to replace the old software because the engineers that are available need to maintain it.</p>

<h2 id="conclusion">Conclusion</h2>

<p>I believe that the rise and decline of programming languages is a natural phenomenon. Priorities change, paradigms shift, and this is reflected in the languages we use as software engineers. However, the languages that stick around are worth studying. Sometimes, their durability is merely a sign of technological inertia and complicated dependencies. Other times, a more positive version of the Lindy effect may be at play—their durability might hint at design principles that are worth preserving.</p>

<h2 id="related-reading">Related Reading</h2>

<ul>
  <li><a href="https://www.goodreads.com/book/show/13530973-antifragile">Antifragile: Things that Gain from Disorder</a></li>
  <li><a href="https://www.moreisdifferent.com/2015/07/16/why-physicsts-still-use-fortran/">Why physicists still use Fortran</a></li>
  <li><a href="https://en.wikipedia.org/wiki/Lindy_effect">Lindy Effect (Wikipedia)</a></li>
  <li><a href="https://en.wikipedia.org/wiki/Path_dependence">Path Dependence (Wikipedia)</a></li>
  <li><a href="https://en.wikipedia.org/wiki/Network_effect">Network Effect (Wikipedia)</a></li>
</ul>

<hr />
<p><br /></p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p>C is still ranked the <a href="https://www.tiobe.com/tiobe-index/">3rd most popular programming language</a> as of writing this. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2" role="doc-endnote">
      <p>On a related note, this is also why it’s so hard to upgrade the electric power grid, or why we are still using the QWERTY keyboard layout despite the existence of <a href="https://en.wikipedia.org/wiki/Dvorak_keyboard_layout#Comparison_of_the_QWERTY_and_Dvorak_layouts">more efficient layouts</a>. <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:3" role="doc-endnote">
      <p>Needless to say, you wouldn’t push straight to production for any kind of serious infrastructure software. <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Tobias van der Werff</name></author><category term="essay" /><category term="programming" /><summary type="html"><![CDATA[“Wanted: people who want to learn a programming language from 1959,” headlined a recent Dutch newspaper article. The language in question is COBOL, a language so old that most of the programmers who know it are either dead or retired. Nonetheless, banks, government institutions, and other large organizations still rely on this ancient language for some of their most critical infrastructure.]]></summary></entry><entry><title type="html">Why You Should Take a Personality Assessment</title><link href="https://tobiasvanderwerff.com/2024/07/10/personality-tests.html" rel="alternate" type="text/html" title="Why You Should Take a Personality Assessment" /><published>2024-07-10T00:00:00+00:00</published><updated>2024-07-10T00:00:00+00:00</updated><id>https://tobiasvanderwerff.com/2024/07/10/personality-tests</id><content type="html" xml:base="https://tobiasvanderwerff.com/2024/07/10/personality-tests.html"><![CDATA[<blockquote>
  <p>To become acquainted with oneself is a terrible shock. - Carl Jung</p>
</blockquote>

<p>Human beings are confusing. Granted, this shouldn’t come as a great shock to most people. Personally, it only takes me a few minutes of watching TikTok to be convinced that the human mind is an inexplicable mystery. What’s especially striking, though, is how often we are equally confused about <em>ourselves</em> as we are about others. When we get confused about others’ behaviors, we can at least say, “Well, I can’t read their thoughts”. But this excuse doesn’t work for our own thoughts and actions. After all, we literally can read our own minds.</p>

<p>I have realized in recent years that most people could benefit from learning more about themselves. Although human psychology is elusive, we can strive to understand it through standardized personality testing. In my opinion, everyone should do a personality test at least once in their life.</p>

<p>What is a <a href="https://en.wikipedia.org/wiki/Personality_test">personality test</a>? It is an attempt to describe human psychology into measurable variables, such as extroversion or neuroticism. Usually, they are conducted through self-report questionnaires. Originally, these tests were used for personnel selection in the armed forces, back in the 1920s. Since then, a wide variety of personality scales have been developed.</p>

<p>Some of the more common personality frameworks include the <a href="https://en.wikipedia.org/wiki/Big_Five_personality_traits">Big Five</a> and the <a href="https://en.wikipedia.org/wiki/Myers%E2%80%93Briggs_Type_Indicator">Myers-Briggs Type Indicator</a>. It’s worth trying out a few different alternatives to find out which works best for you – personally, I’ve gained a lot by learning about the Big Five.<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup> There are various online tests available. For example, a detailed, but paid ($10) Big Five test is the <a href="https://www.understandmyself.com/">Understand Myself</a> assessment. There are also free alternatives, such as <a href="https://www.personalitylab.org/tests/bfi2_self_pol.htm">here</a> and <a href="https://bigfive-test.com/">here</a>. Some examples of Big Five alternatives include <a href="https://principlesyou.com/">PrinciplesYou</a> and <a href="https://www.16personalities.com/">16Personalities</a>.</p>

<h2 id="questionable-life-choices">Questionable life choices</h2>

<p>Personally, I could have benefited from more self-knowledge back when I was in high school. Back then, I had somehow come to the conclusion that what I wanted to do with my life was to become an accountant. Becoming an accountant sounded like a good idea to me because 1) you made a lot of money, and 2) it was high status and sounded important. In hindsight, this was a terrible decision. The environment was an incredibly bad fit for me and I could not relate to my classmates whatsoever. It took me just three months to drop out of my studies and find myself back where I started.</p>

<p>Looking back on this experience, I realize that a poor grasp of my own personality was to blame for my misguided career choice. I had a clear plan for what my career would look like, but I had a poor grasp of how <em>I</em> would fit into that plan.</p>

<p>As Mike Tyson once said, “everybody has a plan, until they get punched in the face”. In my case, fresh out of high school, life immediately handed me an uppercut to the jaw and put me back in my place. I eventually ended up in the right place, studying computer science and artificial intelligence, and being much happier in that environment.</p>

<p>This experience made me realize that earlier in life, it is especially valuable to do a good amount of exploration, trying out different things to discover what you like and don’t like. Before committing to important life choices, you ideally want to test your plan with no strings attached, so you can experiment quickly and adjust your plans if things don’t work out as expected. In my case, it probably would have been sufficient to spend a summer internship at an accountancy firm to realize that my personality was a bad fit for the average personality type there.</p>

<p>Although it’s not uncommon for a teenager to be confused about who they are or want to be, it’s not entirely uncommon for adults to struggle with their identity as well. So, how can personality tests help?</p>

<p>First of all, knowing your personality type can help you to navigate career choices more effectively. For example, if you’re strongly introverted, you’re probably better off not becoming a sales person. If you’re highly disagreeable, a career as a nurse is probably not the most logical choice. If you’re high in neuroticism, you might want to avoid high stress jobs like day trading. While there are exceptions, your personality traits should already give you a pretty good idea of what <em>not</em> to do with your life.</p>

<p>Secondly, learning about personality types is a great way to collaborate more effectively with other people. It’s easier to understand people who are similar to you, but much harder to understand those who are radically different. Having a mental framework of personality is useful because at some point, we all collaborate with people who do not share our personality traits. If we don’t make an effort to understand their behavior, frustration and miscommunication are bound to occur.</p>

<p>Here’s an example. I recently found out about the difference between “linear” and “lateral” thinkers. A linear thinker tries to solve problems step-by-step in a structured way, while a lateral thinker tends to take a big-picture view and connects ideas in more intuitive ways. To a linear thinker, a lateral thinker can come across as chaotic and unstructured. Conversely, a linear thinker can come across as rigid and dogmatic to a lateral thinker. At the same time, these two ways of thinking are complementary, because they each have blind spots. Linear thinkers can compensate for the blind spots of lateral thinkers, and vice versa.</p>

<p>Finally, relationships are greatly helped by awareness of personality types. Finding a suitable partner tends to be a question of finding someone who’s personality type is at least somewhat compatible with yours. The more divergent your personality type is to that of your partner, the harder it will be to understand each other, which will inevitably lead to friction. This is not to say that one should strive to find a partner who is exactly like them, but having some overlap in terms of personality traits tends to be better than having no overlap at all.</p>

<p>Above all, knowledge of one’s personality should serve as a compass for making life decisions, both at the micro and macro level.</p>

<p>Someone who has used personality testing with great effect is <a href="https://goodreads.com/book/show/34536488-principles">Ray Dalio</a> – one of the world’s most successful investors. He uses personality testing extensively as a tool to understand both himself and the people around him. In his company, Bridgewater, they use an idea called <a href="https://www.principles.com/principles/a3d4f223-82d9-48ca-b12b-d00e344821c8">baseball cards</a> to describe the personality types of employees. These baseball cards contain various personality scores obtained from personality tests, providing perspective on someone’s strengths and weaknesses, as well as their communication style. According to Dalio, striving to understand the psychology of each employee has been a crucial part of the success of the company.</p>

<h2 id="where-to-go-from-here">Where to go from here</h2>

<p>If there’s one thing I’ve learned, it’s that getting to know yourself is not easy – it takes time and effort to do well. In the meantime, we can use personality testing as a useful tool to bootstrap the process.</p>

<p>I encourage everyone to take a personality assessment at least once. It shouldn’t take more than an hour or two, and you just might gain lifelong benefits from it. That seems like a pretty good investment to me.</p>

<hr />
<p><br /></p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p>Aside from the Big Five test itself, I’ve also learned a lot from Jordan Peterson’s lectures on this topic. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Tobias van der Werff</name></author><category term="essay" /><summary type="html"><![CDATA[To become acquainted with oneself is a terrible shock. - Carl Jung]]></summary></entry><entry><title type="html">Knowledge Worth Learning</title><link href="https://tobiasvanderwerff.com/2024/06/17/useful-knowledge.html" rel="alternate" type="text/html" title="Knowledge Worth Learning" /><published>2024-06-17T00:00:00+00:00</published><updated>2024-06-17T00:00:00+00:00</updated><id>https://tobiasvanderwerff.com/2024/06/17/useful-knowledge</id><content type="html" xml:base="https://tobiasvanderwerff.com/2024/06/17/useful-knowledge.html"><![CDATA[<p>What makes someone motivated to learn new things? In my opinion, trying to answer this question is crucial to success in a knowledge economy. Most people have a learning style which is unique to them. For example, I consider myself a highly pragmatic person. I like to remind myself of this from time to time because it helps me to ground my thinking and better understand my motivations and drives. I see learning new things as something that is highly <em>practical</em>, akin to an investment in myself that leads to a positive return in the future.</p>

<p>For example, if I’m working on a programming problem, I enjoy learning about an algorithm or design pattern that helps me solve that problem better. Other times, I enjoy listening to podcasts about science-backed techniques for improving personal health (I’m looking at you, Andrew Huberman). I therefore get the greatest enjoyment from knowledge that is <em>useful</em> and <em>actionable</em>.</p>

<p>Although I like to think this pragmatic approach to learning has served me well, it also has its limitations. In particular, it biases my interest towards knowledge that seems useful <em>at first glance</em>. This can be sub-optimal, because the usefulness of knowledge is not always immediately obvious. The question then becomes, <em>how to identify knowledge that is useful?</em></p>

<h2 id="what-makes-knowledge-useful">What makes knowledge useful?</h2>

<p>The idea of useful knowledge reminds me of an amusing scene from <a href="https://en.wikipedia.org/wiki/A_Study_in_Scarlet">Sherlock Holmes</a>. In this scene, Dr. Watson is surprised to learn that Holmes does not know that the earth goes around the sun, as Holmes finds such knowledge irrelevant:</p>

<blockquote>
  <p>His ignorance was as remarkable as his knowledge. Of contemporary literature, philosophy and politics he appeared to know next to nothing. Upon my quoting Thomas Carlyle, he inquired in the naïvest way who he might be and what he had done. My surprise reached a climax, however, when I found incidentally that he was ignorant of the Copernican Theory and of the composition of the Solar System. That any civilized human being in this nineteenth century should not be aware that the earth travelled round the sun appeared to me to be such an extraordinary fact that I could hardly realize it. […] He said that he would acquire no knowledge which did not bear upon his object. Therefore all the knowledge which he possessed was such as would be useful to him.</p>
</blockquote>

<p>I think Holmes’s approach to knowledge raises an intriguing question: <em>How can one determine upfront whether some piece of information will turn out to be useful?</em> In the case of trivia about the Solar System, I think you can be pretty sure that this will not be directly useful for daily life (unless, of course, it’s your job to know). However, in many other cases, the distinction is much more subtle.</p>

<p>Here’s an example. When I was studying computer science at university, I had to implement an algorithm for <a href="https://en.wikipedia.org/wiki/Sorting_algorithm">sorting an array of numbers</a>. The thing to understand is that you would <em>never</em> implement a sorting algorithm yourself for any real-world use case. Whatever your choice of programming language is, you can be pretty sure that it already includes an efficient implementation of a sorting algorithm. In other words, writing a sorting algorithm is mostly a theoretical exercise. At the time, it therefore made zero sense to me why I would have to learn and write an implementation of a sorting algorithm. Why not just use the existing implementations, which are clearly better, and not worry about it? I simply could not imagine the practical use of such an exercise.</p>

<p>Later on, I realized the value: Even though you might never write another sorting algorithm again in your life, there is a good chance <em>you will learn useful things in the process</em>. Unfortunately, the value of such knowledge is often not immediately obvious. If I had focused only on the immediately useful, I would have missed out on learning knowledge that eventually proved useful for future projects.</p>

<p>To illustrate this point, here are some useful insights I gained from list sorting:</p>

<ul>
  <li><a href="https://en.wikipedia.org/wiki/Binary_search">Binary search</a>, a valuable technique for efficiently searching an array of values (and derived from list sorting), and something I have used for various software projects.</li>
  <li>Being able to recognize sorting problems helps to solve some problems more effectively. Sorting is pretty common in many real-world applications, and recognizing a sorting problem can help you choose the right solution for specific situations. For example, some sorting algorithms, such as <a href="https://en.wikipedia.org/wiki/Insertion_sort">insertion sort</a>, are a better choice when a list is nearly sorted.</li>
  <li>Learning about sorting introduces you to related topics such as big-O notation, recursion, and algorithm memory usage. These concepts are ubiquitous in computer science and will likely be useful in other contexts.</li>
  <li>Job interviews: Knowledge of list sorting and time/space <a href="https://en.wikipedia.org/wiki/Computational_complexity">complexity</a> is something that is sometimes tested in job interviews for software engineering roles.</li>
</ul>

<p>To summarize, learning about sorting algorithms may not be immediately useful, but it has a high <a href="https://en.wikipedia.org/wiki/Return_on_investment">ROI</a> in the medium to long term. Moreover, this type of knowledge lays a foundation that makes future learning easier and more efficient. This is what I want to discuss next.</p>

<h2 id="the-value-of-foundational-knowledge">The value of foundational knowledge</h2>

<p>As you can see, my goal of acquiring useful knowledge is not as straightforward as I might have hoped. Another complication is that unlike university, the real world is messy, and the most useful skills for succeeding in it arguably share little overlap with the skills needed to succeed in university. So, how does one determine which knowledge is worth learning?</p>

<p>Let me first state the obvious by saying that this is highly dependent on your goals. My focus here is on <a href="https://en.wikipedia.org/wiki/Knowledge_worker">knowledge workers</a> such as software engineers, who often benefit from increasing their knowledge and skills over time. Such a person might also switch domains every now and then, forcing him or her to learn new things regularly.</p>

<p>As a general principle, I would argue there is great value in learning things that are <em>not too problem-specific</em>.</p>

<p>This is the difference between <em>shallow</em> and <em>deep</em> (or foundational) knowledge. Shallow knowledge will help you only in very specific situations, whereas foundational knowledge is less problem-specific but carries over to many other situations, giving you a broader conceptual framework. Elon Musk <a href="https://www.inc.com/jessica-stillman/heres-elon-musks-secret-for-learning-anything-fast.html">has referred to this distinction</a> in terms of a knowledge tree:</p>

<blockquote>
  <p>It is important to view knowledge as sort of a semantic tree – make sure you understand the fundamental principles, i.e. the trunk and big branches, before you get into the leaves/details or there is nothing for them to hang on to.</p>
</blockquote>

<p>An example of the distinction between relatively shallow and deeper knowledge is understanding the specific syntax of a programming language versus learning the underlying principles of how computers operate. The former gives you a basic capacity to do useful work, whereas the latter provides knowledge that is independent of any programming language and can be applied regardless of the language you use. For example, let’s say you’re writing a computer program that adds numbers to an array. If you understand what happens in the computer’s memory when you append an element to an array, you can write a better program by recognizing that a linked list or hash map is more efficient than an array in certain situations. This will make you a better programmer, no matter what language you use.</p>

<p>Seeking out useful knowledge <em>can be a trap</em>, because it can focus your attention on the shallow knowledge that is immediately useful in the short term, while diverting your attention from the foundational knowledge you may need to learn in order to reach a deeper understanding of a topic. In other words, your knowledge tree might be missing the trunk and branches on which the leaves hang. Solid foundational knowledge is useful because it allows you to apply <a href="https://www.youtube.com/watch?v=NV3sBlRgzTI">first principles thinking</a>, enabling you to analyze underlying patterns better, which can lead to new insights. <sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup></p>

<p>The problem is that <em>this is hard</em>. First, it can be difficult to identify what foundational knowledge you’re missing. By definition, foundational knowledge is far removed from specific problem instances, meaning you need to have enough insight to realize which foundational principles are worth studying. Second, it takes more effort to learn foundational knowledge than to learn shallow knowledge. The difficulty lies in deciding <a href="https://danluu.com/learn-what/">what to learn</a> and how to allocate your time effectively. A lack of understanding of underlying principles and foundational algorithms may lead you to re-invent the wheel and waste a lot of time doing so. Moreover, since the emphasis in modern society is often on short-term thinking, taking the time to learn foundational knowledge can be <a href="https://www.goodreads.com/book/show/25744928-deep-work">incredibly valuable</a>.</p>

<p>I think this is worth repeating: <em>Most people don’t do this</em>. The reason is straightforward: It is hard and takes a lot of time and effort. Instead, it is often tempting to look for <em>hacks</em>: health hacks, productivity hacks, etc., where the focus is on quick and maximally useful solutions. The problem with this approach is that it foregoes the opportunity to learn more fundamental principles.</p>

<p>This way of thinking can be applied to many areas of life. Take nutrition as another example. Instead of following the latest diet fad, you are probably better off studying what exactly makes a diet “healthy” in the first place. If you start from the foundational principles that constitute a healthy diet–such as a certain amount of protein, vitamins, minerals, calories, etc.–you have a great deal of flexibility in designing a diet that works best for your personal circumstances. This is tremendously <em>empowering</em>, but also takes a good deal of effort.</p>

<p>On the other hand, partly because it’s so demanding, learning foundational knowledge is not always necessary. Sometimes, a quick hack is all you need. For example, taking a vitamin D supplement in winter is a quick and easy way to address the fact that many people are vitamin D deficient in winter due to a lack of sunlight exposure. You don’t need to know much more about it than that to reap the benefits.</p>

<p>My impression is that the need for foundational knowledge increases as you try to solve harder and more novel problems, whether in your professional or personal life. This is because if a problem has been solved many times before, there is a good chance someone has documented the solution and shared it with others. If not, it usually pays off to go a few levels deeper than the specifics of a problem might seem to require, to gain a new perspective on the problem. Examples of this include: doing novel research, building a startup or product that addresses a difficult problem, or designing a nutrition plan that works well for you.</p>

<p>Frankly, I still find it quite difficult to determine what information is worth learning. Paradoxically, to know how useful a piece of information is, <a href="https://en.wikipedia.org/wiki/Four_stages_of_competence">you might have to learn it first</a>. Additionally, what is useful to one person <a href="https://danluu.com/learn-what/">may not be to someone else</a>, so copying the same trajectory as others is also not an ideal strategy.</p>

<p>I find inspiration in the people I look up to when I consider their approach to learning. For example, my impression is that the most impressive software engineers tend to share a learning mindset. They strive to understand systems <em>deeply</em> rather than superficially. A learning mindset might just be the most important meta-strategy: Make it a habit to expand your knowledge and skills, striving for deep, non-superficial understanding. As long as you keep learning, you’ll learn useful knowledge along the way.</p>

<h2 id="related-reading">Related reading</h2>

<ul>
  <li><a href="https://danluu.com/learn-what/">Dan Luu on what to learn</a></li>
  <li><a href="https://web.archive.org/web/20211019055306/https://www.csub.edu/~ecarter2/CSUB.MKTG%20490%20F10/DRUCKER%20HBR%20Managing%20Oneself.pdf">Peter Drucker - Managing Oneself</a>
    <blockquote>
      <p>Success in the knowledge economy comes to those who know themselves—their strengths, their values, and how they best perform</p>
    </blockquote>
  </li>
  <li><a href="https://x.com/karpathy/status/1325154823856033793">Andrej Karpathy on how to become an expert</a>:
    <blockquote>
      <p>How to become expert at thing:<br />
1 iteratively take on concrete projects and accomplish them depth wise, learning “on demand” (ie don’t learn bottom up breadth wise)<br />
2 teach/summarize everything you learn in your own words<br />
3 only compare yourself to younger you, never to others</p>
    </blockquote>
  </li>
  <li><a href="https://x.com/karpathy/status/1756380066580455557">Andrej Karpathy on the shortification of learning</a>:
    <blockquote>
      <p>There are a lot of videos on YouTube/TikTok etc. that give the appearance of education, but if you look closely they are really just entertainment. This is very convenient for everyone involved : the people watching enjoy thinking they are learning (but actually they are just having fun). The people creating this content also enjoy it because fun has a much larger audience, fame and revenue. But as far as learning goes, this is a trap. This content is an epsilon away from watching the Bachelorette. It’s like snacking on those “Garden Veggie Straws”, which feel like you’re eating healthy vegetables until you look at the ingredients.</p>

      <p>Learning is not supposed to be fun. It doesn’t have to be actively not fun either, but the primary feeling should be that of effort. It should look a lot less like that “10 minute full body” workout from your local digital media creator and a lot more like a serious session at the gym. You want the mental equivalent of sweating. It’s not that the quickie doesn’t do anything, it’s just that it is wildly suboptimal if you actually care to learn.<br />
…<br />
So for those who actually want to learn. Unless you are trying to learn something narrow and specific, close those tabs with quick blog posts. Close those tabs of “Learn XYZ in 10 minutes”. Consider the opportunity cost of snacking and seek the meal - the textbooks, docs, papers, manuals, longform. Allocate a 4 hour window. Don’t just read, take notes, re-read, re-phrase, process, manipulate, learn.</p>
    </blockquote>
  </li>
  <li><a href="https://giansegato.com/essays/edutainment-is-not-learning">Gian Segato: Learning takes effort, otherwise it’s just entertainment</a></li>
  <li><a href="https://www.benkuhn.net/exploration/">Ben Kuhn on exploring new things</a></li>
</ul>

<hr />
<p><br /></p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p>One of my favorite examples of this is Andrej Karpathy <a href="https://x.com/karpathy/status/1707437820045062561">making the connection between LLMs and operating systems</a> – two things which, at first glance, have little to do with each other. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Tobias van der Werff</name></author><category term="essay" /><category term="programming" /><summary type="html"><![CDATA[What makes someone motivated to learn new things? In my opinion, trying to answer this question is crucial to success in a knowledge economy. Most people have a learning style which is unique to them. For example, I consider myself a highly pragmatic person. I like to remind myself of this from time to time because it helps me to ground my thinking and better understand my motivations and drives. I see learning new things as something that is highly practical, akin to an investment in myself that leads to a positive return in the future.]]></summary></entry><entry><title type="html">CNN vs. Vision Transformer: A Practitioner’s Guide to Selecting the Right Model</title><link href="https://tobiasvanderwerff.com/2024/05/15/cnn-vs-vit.html" rel="alternate" type="text/html" title="CNN vs. Vision Transformer: A Practitioner’s Guide to Selecting the Right Model" /><published>2024-05-15T00:00:00+00:00</published><updated>2024-05-15T00:00:00+00:00</updated><id>https://tobiasvanderwerff.com/2024/05/15/cnn-vs-vit</id><content type="html" xml:base="https://tobiasvanderwerff.com/2024/05/15/cnn-vs-vit.html"><![CDATA[<p>Vision Transformers (ViTs) have become a popular model architecture in computer vision research, excelling in a variety of tasks and surpassing Convolutional Neural Networks (CNNs) in most <a href="https://paperswithcode.com/area/computer-vision">benchmarks</a>. As practitioners, we often face the dilemma of choosing the right architecture for our projects. This blog post aims to provide guidelines for making an informed decision on when to use CNNs versus ViTs, backed by empirical evidence and practical considerations.</p>

<p>For those in a hurry, here is a decision tree to quickly choose between CNNs and ViTs for real-world projects. Alternatively, you can jump ahead to the <a href="#summary-and-recommendations">conclusion</a> for a list of practical recommendations.</p>

<figure>
    <img class="center" src="/assets/images/cnn-vs-vit/decision_tree.drawio.svg" alt="Decision tree for CNN vs. ViT" />
    <figcaption>
        A decision tree for deciding between CNN and ViT for real-world projects
    </figcaption>
</figure>

<p>This guide is primarily intended for practitioners and researchers in the field of computer vision who are interested in understanding the trade-offs between Convolutional Neural Nets and Vision Transformers for various applications. For the most part, I am assuming a background in machine learning concepts and familiarity with terminology related to deep learning architectures and training procedures. Also note that even though I have tried to synthesize some of the most relevant research here, I may have missed important insights that I should have incorporated. Please don’t hesitate to <a href="mailto:t@tobiasvanderwerff.com">contact me</a> if you spot any mistakes or things I missed!</p>

<h2 id="architectural-differences">Architectural Differences</h2>

<p>Let’s start with a brief comparison of the two architectures. I will explain only the essential information, as there are plenty of resources available to learn more about Vision Transformers (the original <a href="https://arxiv.org/abs/2010.11929">paper</a> is a good start). Since the Vision Transformer architecture is largely identical to the original Transformer encoder architecture by <a href="https://arxiv.org/abs/1706.03762">Vaswani et al. (2017)</a>, I will use the terms Transformer and Vision Transformer interchangeably.</p>

<figure>
    <img class="center" src="/assets/images/cnn-vs-vit/ViT_arch.jpg" alt="" />
    <figcaption>
        The Vision Transformer architecture. It is largely identical to the original Transformer architecture by Vaswani et al. (2017). 
        (Image source: <a href="https://arxiv.org/abs/2010.11929">Dosovitskiy et al. 2020</a>)
    </figcaption>
</figure>

<p>Transformers are flexible architectures with minimal inductive priors, meaning they make few assumptions about input data. In contrast, CNNs assume that nearby pixels are related (locality) and that different parts of an image are processed similarly (weight sharing). These assumptions, inherent to the convolution operator, help CNNs learn effectively with limited training data.</p>

<p>Transformers, on the other hand, have very few inductive biases. This means they have to learn more from the training data, thereby necessitating larger training datasets. They can outperform CNNs when trained on sufficient data, but struggle to learn meaningful representations with small datasets, underperforming other architectures (more on this later).</p>

<p>While CNNs start from the assumption that nearby pixels are related, the Vision Transformer makes no such assumption, considering the relationship of all pixels to each other with equal weight. This can lead to a better understanding of global relationships in an image, which a CNN might not capture because of its locality bias. Therefore, at a certain data threshold, inductive biases become a <em>liability</em>, rather than an <em>asset</em>. Transformers are <em>highly scalable</em> because they are minimally constrained by assumptions baked into the architecture.</p>

<p>Neural network architectures can be seen as existing on a spectrum of inductive biases, from weak to strong. ViTs occupy the lower end of the spectrum, while CNNs occupy the higher end. Depending on how well the inductive priors can be learned from the training data, one might choose an architecture with fewer or more inductive biases. For example, there are <a href="https://ai.meta.com/blog/computer-vision-combining-transformers-and-convolutional-neural-networks/">hybrid architectures</a> which combine CNNs and ViTs into a single architecture. Such an architecture would sit in the middle of the inductive biases spectrum, with enough priors to avoid requiring a huge amount of training data, while still preserving some of the learning flexibility of the Transformer architecture.</p>

<p>Finally, it is worth mentioning that Transformers have had significant success due to <strong>self-supervised learning</strong>. This is a paradigm in which the model learns to extract meaningful representations from unlabeled data by solving pretext tasks such as predicting missing patches or identifying transformed images. Since Transformers are so data-hungry, self-supervised learning is an excellent way to scale up training datasets, as no labels are required. It leads to general-purpose representations that can be fine-tuned for specific downstream tasks with less labeled data. The most notable success stories are from NLP (e.g., BERT, GPT), but it is becoming increasingly common in computer vision as well. ViTs are the most common choice for self-supervised pretraining in computer vision (see, e.g., <a href="https://arxiv.org/abs/2304.07193">DINOv2</a>, <a href="https://arxiv.org/abs/2111.06377">MAE</a>), but <a href="https://arxiv.org/abs/2301.00808">CNNs can also be used</a>.</p>

<p>In summary:</p>

<ul>
  <li>Vision Transformers are highly scalable but require large datasets to learn effectively. They are most effective when scaled up to large sizes (or <a href="https://arxiv.org/abs/2302.05442">very large</a> sizes). Self-supervised learning can enable such large-scale training, although supervised pretraining is also still quite common.</li>
  <li>CNNs have strong inductive biases (locality, weight sharing), allowing them to perform well with limited data. They are less scalable than ViTs, but outperform ViTs in smaller pretraining data regimes.</li>
</ul>

<figure>
  <img class="center" style="width:85%;" src="/assets/images/cnn-vs-vit/ViT_sizes.png" alt="Image description" />
  <figcaption>Details of Vision Transformer model variants. ViT-Huge is not included here, which uses 632M parameters. (Image source: <a href="https://arxiv.org/abs/2106.10270">Steiner et al. 2021</a>)</figcaption>
</figure>

<h2 id="what-does-the-research-say">What does the research say?</h2>

<p>Let’s now look at empirical studies studying and comparing CNNs and ViTs, to get an idea of their relative strengths and weaknesses.</p>

<p>I will note here that some head-to-head comparisons between CNNs and ViTs can be misleading, due to inconsistent comparison standards. Differences in model size, training dataset, augmentation strategies, and training length are all confounders that make a direct comparison <a href="https://arxiv.org/abs/2111.05464">difficult</a>. For example, comparing a ResNet-50 to a ViT-B model (done <a href="https://arxiv.org/abs/2109.02716">here</a>) is not a fair comparison, because the ViT-B model has 3.5x the number of parameters as the ResNet-50 model.</p>

<h3 id="data-requirements">Data Requirements</h3>

<p>Pretraining on <a href="https://paperswithcode.com/dataset/imagenet-1k-1">ImageNet-1k</a> has been standard practice for quite some time, typically followed by finetuning on a downstream task. Recent <a href="https://arxiv.org/abs/2104.10972">studies</a> have shown the benefits of (supervised) pretraining on larger datasets than ImageNet-1k, such as <a href="https://paperswithcode.com/dataset/imagenet">ImageNet-21k</a> and <a href="https://paperswithcode.com/dataset/jft-300m">JFT-300M</a>, even with noisier labels (<a href="https://arxiv.org/abs/1912.11370">Kolesnikov et al. 2019</a>, <a href="https://arxiv.org/abs/2010.11929">Dosovitskiy et al. 2020</a>, <a href="https://arxiv.org/abs/2106.10270">Steiner et al. 2021</a>). This is true for both CNNs and ViTs, although CNNs such as ResNet require certain modifications, such as replacing batch normalization, in order to scale effectively (<a href="https://arxiv.org/abs/1912.11370">Kolesnikov et al. 2019</a>). Thus, <em>pretraining on more data is preferred for both ViT and CNN, even if the data is somewhat noisy</em>.</p>

<p><a href="https://arxiv.org/abs/2010.11929">Dosovitskiy et al. (2020)</a> compare the results of pretraining on progressively larger datasets: ImageNet-1k, ImageNet-21k and JFT-300M (1.3M, 14M, and 300M images, respectively), finding that ViTs underperform BiT ResNets (<a href="https://arxiv.org/abs/1912.11370">Kolesnikov et al. 2019</a>) when trained on ImageNet-1k, but outperform BiT ResNets when trained on ImageNet-21k or JFT-300M. This can also be seen in the figure below (from the same paper), which shows ImageNet classification performance as a function of pretraining dataset size. ResNets perform better with smaller pretraining datasets but plateau sooner than ViT, which performs better with larger pretraining.</p>

<figure>
  <img class="center" style="width:75%;" src="/assets/images/cnn-vs-vit/ViT_ImageNet_transfer.png" alt="ViT Imagenet transfer" />
  <figcaption>
    Performance on ImageNet as a function of pretraining size. (Image source: <a href="https://arxiv.org/abs/2010.11929">Dosovitskiy et al. 2020</a>)
  </figcaption>
</figure>

<p>These results indicate that ViTs are more suitable for large-scale pretraining than ResNets, as their performance scales better on larger pretraining datasets. Although it is not entirely clear at which data scale ViTs start to surpass CNNs, I would put this threshold at about 10 million images for supervised pretraining (mostly based on the ImageNet-21k versus ImageNet-1k results).</p>

<p>In later studies, it has been shown that a ViT can be trained effectively on ImageNet-1k as well, using an improved training recipe with stronger data augmentation (<a href="https://arxiv.org/abs/2012.12877">Touvron et al. 2020</a>, <a href="https://arxiv.org/abs/2106.10270">Steiner et al. 2021</a>). ViT’s reliance on large datasets makes it benefit from stronger data augmentation during training compared to CNNs.</p>

<h3 id="transferability">Transferability</h3>

<p>Let’s now explore the transferability of CNNs and ViTs, i.e., how well their representations transfer to new domains. Transferability is a crucial factor for real-world applications, where compute and training data is often limited.</p>

<p>As discussed, ViTs require a large amount of pretraining data to show benefits compared to CNNs. One might conclude that without a massive training dataset, CNNs are the better option. However, in real-world projects, <strong>transfer learning</strong> — initializing a model from a pretrained checkpoint — is preferred over training from scratch. Even though some studies show limited benefits of transfer learning in rare situations (<a href="https://arxiv.org/abs/1811.08883">He et al. 2018</a>), <em>starting from a pretrained model almost never hurts</em>. It usually provides faster convergence, better performance, and higher sample efficiency (see e.g. <a href="https://arxiv.org/abs/1912.11370">Kolesnikov et al. 2019</a>, <a href="https://arxiv.org/abs/2106.10270">Steiner et al. 2021</a>, <a href="https://arxiv.org/abs/1805.08974">Kornblith et al. 2018</a>, <a href="https://arxiv.org/abs/1902.07208">Raghu et al. 2019</a>). This is especially relevant since most popular models have pretrained checkpoints available, which should be used as initial weights for a model when starting any new computer vision project. For instance, <a href="https://arxiv.org/abs/2106.10270">Steiner et al. 2021</a> show that across a wide range of datasets, even if the downstream data of interest appears to be only weakly related to the data used for pretraining, transfer learning remains the best available option for training ViTs. Starting from a pretrained model should be the preferred choice 95% of the time, <em>especially</em> when working with small or mid-sized datasets. Training from scratch is rarely justified, requiring (1) a large domain gap between the pretraining and target task, and (2) a large amount of domain-specific data for (pre-)training.</p>

<p>This means that the decision between CNN and ViT should not necessarily be dictated by the size of the training dataset you have access to, but rather, <em>the availability of a model checkpoint pretrained on a large dataset</em>.</p>

<p>Let’s examine how well ViT and CNN representations transfer. The literature shows that pretraining on more data yields more generic ViT models that transfer better to various datasets (<a href="https://arxiv.org/abs/2106.10270">Steiner et al. 2021</a>, <a href="https://arxiv.org/abs/2108.05305">Zhou et al. 2021</a>). Furthermore, off-the-shelf ViT features transfer well to new domains, outperforming ResNets (<a href="https://arxiv.org/abs/2105.10497">Naseer et al. 2021</a>). This makes a ViT pretrained on large amounts of data a good choice for transfer learning.</p>

<p>The plot below shows the transfer learning performance of ViT on the <a href="https://arxiv.org/abs/1910.04867">VTAB</a> specialized benchmark, a suite of datasets consisting of medical and aerial images for classification, where each task contains only 800 training images. The plot shows that pretraining on more data is beneficial, as ImageNet-21k pretraining outperforms ImageNet-1k pretraining.</p>

<figure>
    <img class="center" src="/assets/images/cnn-vs-vit/how-to-train-your-vit-pretraining-on-more-data.png" alt="how-to-train-your-vit-pretraining-on-more-data" />
    <figcaption>
        Pretraining on more data yields more transferable ViT models on average, as measured on the VTAB specialized suite, consisting of medical and aerial images. (Image source: <a href="https://arxiv.org/abs/2106.10270">Steiner et al. 2021</a>)
    </figcaption>
</figure>

<p>Pretraining performance correlates quite well with downstream finetuning performance for both CNNs and ViTs, with occasional exceptions. For example, weight decay and other regularizers can correlate negatively with pretraining performance but positively with downstream performance (<a href="https://arxiv.org/abs/2106.04560">Zhai et al. 2021</a>, <a href="https://arxiv.org/abs/1805.08974">Kornblith et al. 2018</a>). However, as a general heuristic for maximizing performance, starting from the model with the highest pretraining performance is most likely the best choice for transfer learning. Since the correlation between pretraining and finetuning performance is strong, but not 100%, then, if time or compute is not a limiting factor, one can try out several pretrained models to find the highest performing model for the downstream task.</p>

<p>Based on these findings, we can conclude that ViTs exhibit strong transfer learning performance, as is the case for CNNs. Compared to ResNets, ViTs appear to transfer better on average. In general, when pretraining data is limited, CNNs are most effective. As pretraining data is scaled up, there is reason to believe ViTs exhibit better transfer learning performance. However, there is also reason to believe CNNs can transfer equally well <a href="https://arxiv.org/abs/2201.03545">when using the same training tricks as ViTs</a>.</p>

<h3 id="robustness">Robustness</h3>

<p>Having discussed transferability, let’s now examine the robustness of CNNs and ViTs. Robustness to image corruptions, such as those shown in the figure below, is particularly relevant when data drift is a concern in deployment settings.</p>

<figure>
    <img class="center" src="/assets/images/cnn-vs-vit/ViTs_intriguing_properties.png" alt="ViTs show robustness to various image corruptions" />
    <figcaption>
        ViTs show robustness to various image corruptions
        (Image source: <a href="https://arxiv.org/pdf/2105.10497">Naseer et al. 2021</a>)
    </figcaption>
</figure>

<p>There is evidence that ViTs are more robust to occlusions, adversarial and natural perturbations, and distribution shifts (<a href="https://arxiv.org/abs/2105.10497">Naseer et al. 2021</a>, <a href="https://arxiv.org/abs/2105.07581">Paul et al. 2021</a>, <a href="https://arxiv.org/abs/2105.15203">Xie et al 2021</a>, <a href="https://arxiv.org/abs/2204.12451">Zhou et al. 2022</a>, <a href="https://arxiv.org/abs/2103.15670">Shao et al. 2021</a>, <a href="https://arxiv.org/pdf/2106.07617">Zhang et al. 2021</a>), with the self-attention architecture design playing an important role in achieving stronger generalization (<a href="https://arxiv.org/abs/2111.05464">Bai et al. 2021</a>). On the other hand, the more recent ConvNeXt CNN architecture might achieve similar robustness to ViTs (<a href="https://arxiv.org/abs/2207.11347">Pinto et al. 2022</a>).</p>

<p>One explanation for the robustness of ViTs to corruptions such as occlusion is a higher bias towards shapes rather than local textures and backgrounds, compared to CNNs (<a href="https://arxiv.org/abs/2105.10497">Naseer et al. 2021</a>, <a href="https://arxiv.org/abs/2106.07617">Zhang et al. 2021</a>). This observation aligns well with the idea that self-attention more effectively captures global relationships, while convolutions are biased towards local interactions. While the receptive field of a CNN expands gradually with increasing depth, self-attention can flexibly adjust its receptive field as needed in any part of the network. For example, see the figure below, which shows that a ViT integrates information across the entire image even in the lowest layers.</p>

<figure>
    <img class="center" style="width:50%;" src="/assets/images/cnn-vs-vit/ViT_mean_attention_distance.png" alt="" />
    <figcaption>
        Average pixel distance across which attention is computed at various layers of a ViT network. Image width is 224 pixels.
        (Image source: <a href="https://arxiv.org/abs/2010.11929">Dosovitskiy et al. 2020</a>)
    </figcaption>
</figure>

<h2 id="model-efficiency">Model Efficiency</h2>

<p><em>Note: I realize this section could be supported with a bit more data. I might expand on it in the future.</em></p>

<p>Having examined robustness, let’s now consider the efficiency of CNNs and ViTs. Model efficiency is an important consideration, especially in applications where computational resources are limited. When it comes to model efficiency, several factors must be considered, such as FLOPs, power consumption, and memory consumption. Importantly, a distinction can be made between efficiency during model <em>training</em> and efficiency during <em>inference</em> (at deployment time).</p>

<p>When it comes to specialized architectures emphasizing model efficiency, CNNs are arguably more mature. For example, CNN architectures like MobileNet, SqueezeNet, and EfficientNet are designed to be lightweight and efficient, making them suitable for embedded or real-time applications. Additionally, there are various techniques to reduce model size and improve inference efficiency without significant performance loss, such as pruning, quantization, and knowledge distillation. These techniques can be applied to both CNNs and ViTs. See also <a href="https://arxiv.org/abs/2310.19909">this paper</a> for an interesting comparison of lightweight backbones (Table 3).</p>

<p><a href="https://arxiv.org/abs/2010.11929">Dosovitskiy et al. 2020</a> show that ViTs are more sample efficient to train than ResNets when trained on the same computational budget (see figure below). ViTs use approximately 2-4× less compute to attain the same performance. This can be beneficial when pretraining on large datasets, as the model could converge faster.</p>

<figure>
  <img src="/assets/images/cnn-vs-vit/ViT_compute_budget.png" alt="ViT compute budget" />
  <figcaption>ViT is more sample efficient than ResNet. (Image source: <a href="https://arxiv.org/abs/2010.11929">Dosovitskiy et al. 2020</a>)</figcaption>
</figure>

<p>Note also that due to the popularity of the Transformer architecture in fields like NLP, optimization of Transformers is a rapidly evolving subject. For an impression of inference optimization, see <a href="https://lilianweng.github.io/posts/2023-01-10-inference-optimization/">this post by Lilian Weng</a>.</p>

<h2 id="limitations">Limitations</h2>

<ul>
  <li>Most studies cited here evaluate CNNs and ViTs on image classification, most commonly on ImageNet. This is not representative of other tasks such as <a href="https://arxiv.org/abs/2205.08534">dense prediction</a>.</li>
  <li>Many comparison studies compare ViTs to ResNet variants, such as BiT. This is perhaps not an entirely fair comparison, since there are more recent CNN architectures such as <a href="https://arxiv.org/abs/2003.13678">RegNet</a> and <a href="https://arxiv.org/abs/2201.03545">ConvNeXt</a>.</li>
</ul>

<h2 id="summary-and-recommendations">Summary and Recommendations</h2>

<p>The decision tree at the top of this page should provide a useful guideline for choosing a model based on various considerations. Based on the data discussed above, here is a summary of my recommendations for choosing between CNNs and ViTs.</p>

<ul>
  <li><em>Transfer learning from a pretrained model should be the preferred choice 95% of the time</em>. This holds for both CNNs and ViTs and is especially true when working with small or mid-sized datasets.</li>
  <li><em>Pick a pretrained model checkpoint with the highest upstream performance</em>. CNNs and ViTs both transfer well, which means that the decision between the two architectures should be made by picking the model that performs best during pretraining.</li>
  <li><em>Pick a model checkpoint trained on more upstream data</em>. This holds for both CNNs and ViTs. For example, pick a model trained on ImageNet-21k instead of ImageNet-1k, or a model trained on a large unlabeled dataset in a self-supervised way.</li>
  <li><em>Pick the largest model that fits your hardware and latency limitations</em>. This holds for both CNNs and ViTs. Larger models outperform smaller models when trained on sufficient data, and transfer performance correlates highly with pretraining performance. An exception would be when your target task is simple enough not to require a large model.</li>
  <li><em>Prefer CNN if development time is an important factor</em>. CNNs are a more mature architecture than ViTs, which can make it easier to work with due to existing frameworks and training recipes that are tried and tested.</li>
  <li><em>Prefer CNN for embedded and real-time applications</em>. This is because there is a more mature ecosystem of tools available for CNNs.</li>
  <li><em>Prefer CNN on tasks where pretrained checkpoints are not available, or when checkpoints pretrained on datasets larger than ImageNet-1k are not available</em>. CNNs are the best choice when large scale pretraining is not an option.</li>
  <li><em>Prefer ViT if robustness to image corruptions and/or data drift is a concern</em>. ViTs have been shown to be relatively robust to such perturbations, possibly because ViTs are biased towards shapes, whereas CNNs are biased towards local textures and backgrounds.</li>
  <li><em>When using a CNN, consider using a modern CNN architecture such as <a href="https://arxiv.org/abs/2201.03545">ConvNeXt</a></em></li>
  <li><em>Consider trying out modified ViT architectures</em>:
    <ul>
      <li>Hybrid CNN/ViT architecture</li>
      <li>ViT with vision priors, to reduce the risk of overfitting. See, e.g., <a href="https://arxiv.org/abs/2103.14030">Swin</a> and its derivatives</li>
    </ul>
  </li>
</ul>

<p>If I were to summarize all this in a few sentences, I would say that CNNs are a practical and high-performing choice for many real-world applications. For the most part, ViTs are worth trying if you want to try and squeeze out maximal performance. Also, Vision Transformers are worth keeping an eye on due to developments in self-supervised learning and multi-modal training, where the flexibility of the Transformer architecture is quite useful.</p>

<p>Finally, I have omitted a more in-depth discussion on applying ViT for dense prediction tasks such as object detection and instance segmentation. This comes with several caveats, especially when it comes to pretraining. I might write a part 2 of this post at some point where I discuss these things in more depth.</p>

<h2 id="references">References</h2>

<p>[1] Vaswani et al. <a href="https://arxiv.org/abs/1706.03762">“Attention Is All You Need”</a>, 2017.<br />
[2] Dosovitskiy et al. <a href="https://arxiv.org/abs/2010.11929">“An Image is Worth 16x16 Words: Transformers for Image Recognition at Scale”</a>, 2020.<br />
[3] Steiner et al. <a href="https://arxiv.org/abs/2106.10270">“How to train your ViT? Data, Augmentation, and Regularization in Vision Transformers”</a>, 2021.<br />
[4] Kolesnikov et al. <a href="https://arxiv.org/abs/1912.11370">“Big Transfer (BiT): General Visual Representation Learning”</a>, 2019.<br />
[5] Touvron et al. <a href="https://arxiv.org/abs/2012.12877">“Training data-efficient image transformers &amp; distillation through attention”</a>, 2020.<br />
[6] He et al. <a href="https://arxiv.org/abs/1811.08883">“Rethinking ImageNet Pre-training”</a>, 2018.<br />
[7] Kornblith et al. <a href="https://arxiv.org/abs/1805.08974">“Do Better ImageNet Models Transfer Better?”</a>, 2018.<br />
[8] Raghu et al. <a href="https://arxiv.org/abs/1902.07208">“Transfusion: Understanding Transfer Learning for Medical Imaging”</a>, 2019.<br />
[9] Zhou et al. <a href="https://arxiv.org/abs/2108.05305">“ConvNets vs. Transformers: Whose Visual Representations are More Transferable?”</a>, 2021.<br />
[10] Naseer et al. <a href="https://arxiv.org/abs/2105.10497">“Intriguing Properties of Vision Transformers”</a>, 2021.<br />
[11] Zhai et al. <a href="https://arxiv.org/abs/2106.04560">“Scaling Vision Transformers”</a>, 2021.<br />
[12] Paul et al. <a href="https://arxiv.org/abs/2105.07581">“Vision Transformers are Robust Learners”</a>, 2021.<br />
[13] Xie et al. <a href="https://arxiv.org/abs/2105.15203">“SegFormer: Simple and Efficient Design for Semantic Segmentation with Transformers”</a>, 2021.<br />
[14] Zhou et al. <a href="https://arxiv.org/abs/2204.12451">“Understanding The Robustness in Vision Transformers”</a>, 2022.<br />
[15] Shao et al. <a href="https://arxiv.org/abs/2103.15670">“On the Adversarial Robustness of Vision Transformers”</a>, 2021.<br />
[16] Zhang et al. <a href="https://arxiv.org/pdf/2106.07617">“Delving Deep into the Generalization of Vision Transformers under Distribution Shifts”</a>, 2021.<br />
[17] Bai et al. <a href="https://arxiv.org/abs/2111.05464">“Are Transformers More Robust Than CNNs?”</a>, 2021.<br />
[18] Liu et al. <a href="https://arxiv.org/abs/2201.03545">“A ConvNet for the 2020s”</a>, 2022.<br />
[19] Pinto et al. <a href="https://arxiv.org/abs/2207.11347">“An Impartial Take to the CNN vs Transformer Robustness Contest”</a>, 2022.</p>]]></content><author><name>Tobias van der Werff</name></author><category term="long-read" /><category term="tutorial" /><summary type="html"><![CDATA[Vision Transformers (ViTs) have become a popular model architecture in computer vision research, excelling in a variety of tasks and surpassing Convolutional Neural Networks (CNNs) in most benchmarks. As practitioners, we often face the dilemma of choosing the right architecture for our projects. This blog post aims to provide guidelines for making an informed decision on when to use CNNs versus ViTs, backed by empirical evidence and practical considerations.]]></summary></entry><entry><title type="html">Extracting High-Quality Keyframes from Videos Using FFmpeg</title><link href="https://tobiasvanderwerff.com/2024/05/07/ffmpeg-keyframes.html" rel="alternate" type="text/html" title="Extracting High-Quality Keyframes from Videos Using FFmpeg" /><published>2024-05-07T00:00:00+00:00</published><updated>2024-05-07T00:00:00+00:00</updated><id>https://tobiasvanderwerff.com/2024/05/07/ffmpeg-keyframes</id><content type="html" xml:base="https://tobiasvanderwerff.com/2024/05/07/ffmpeg-keyframes.html"><![CDATA[<p>I recently found out that when extracting individual frames from a video, not all frames are of the same quality. This is because <a href="https://en.wikipedia.org/wiki/Video_compression_picture_types">temporal compression</a> is employed to store videos in a more efficient way, without needing to store each individual frame in its full size. It exploits the similarities between neighboring frames to reduce the amount of data needed to store the video. This can lead to artifacts such as blurriness when frames are extracted as individual images.</p>

<p>FFmpeg has a way to extract only high quality frames from a video, by extracting <strong>keyframes</strong>: Keyframes are stored as complete images within the video stream and should have higher quality than intermediate frames. There are two ways of extracting keyframes in ffmpeg:</p>

<ol>
  <li>
    <p>Using the <code class="language-plaintext highlighter-rouge">-skip_frame nokey</code> option:</p>

    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code> ffmpeg <span class="nt">-skip_frame</span> nokey <span class="nt">-i</span> input.mp4 <span class="nt">-vsync</span> vfr <span class="nt">-frame_pts</span> <span class="nb">true </span>out_%03d.png
</code></pre></div>    </div>

    <ul>
      <li><code class="language-plaintext highlighter-rouge">skip_frame nokey</code> skips all non-keyframes</li>
      <li><code class="language-plaintext highlighter-rouge">vsync vfr</code> discards unused frames to avoid duplicates</li>
      <li><code class="language-plaintext highlighter-rouge">frame_pts true</code> uses the frame index for naming the output images</li>
    </ul>
  </li>
  <li>
    <p>Using the <code class="language-plaintext highlighter-rouge">-vf "select=eq(pict_type,I)"</code> filter:</p>

    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code> ffmpeg <span class="nt">-i</span> input.mp4 <span class="nt">-vf</span> <span class="s2">"select=eq(pict_type</span><span class="se">\,</span><span class="s2">I)"</span> <span class="nt">-vsync</span> vfr out_%03d.png
</code></pre></div>    </div>

    <ul>
      <li><code class="language-plaintext highlighter-rouge">select='eq(pict_type,I)'</code> selects only frames where the picture type is I (keyframe)</li>
    </ul>
  </li>
</ol>

<p>I prefer option 1, because the resulting frame names contain the exact position of the frame within the video (because of the <code class="language-plaintext highlighter-rouge">-frame_pts true</code> flag).</p>]]></content><author><name>Tobias van der Werff</name></author><category term="tutorial" /><summary type="html"><![CDATA[I recently found out that when extracting individual frames from a video, not all frames are of the same quality. This is because temporal compression is employed to store videos in a more efficient way, without needing to store each individual frame in its full size. It exploits the similarities between neighboring frames to reduce the amount of data needed to store the video. This can lead to artifacts such as blurriness when frames are extracted as individual images.]]></summary></entry><entry><title type="html">Using Albumentations in Detectron2</title><link href="https://tobiasvanderwerff.com/2024/04/13/albumentations-detectron2.html" rel="alternate" type="text/html" title="Using Albumentations in Detectron2" /><published>2024-04-13T00:00:00+00:00</published><updated>2024-04-13T00:00:00+00:00</updated><id>https://tobiasvanderwerff.com/2024/04/13/albumentations-detectron2</id><content type="html" xml:base="https://tobiasvanderwerff.com/2024/04/13/albumentations-detectron2.html"><![CDATA[<p>I have recently been using <a href="https://github.com/facebookresearch/detectron2">Detectron2</a> to train deep learning models for object detection and instance segmentation. This works great, but I found that there is one area in which Detectron2 is lacking: data augmentation. Although it has a decent <a href="https://detectron2.readthedocs.io/en/latest/modules/data_transforms.html">API</a> for data transforms, it has a limited selection of useful data augmentations. Normally, a dedicated data augmentation library such as <a href="https://github.com/albumentations-team/albumentations">Albumentations</a> would be my first choice for this. Unfortunately, unlike similar libraries such as <a href="https://github.com/open-mmlab/mmdetection/pull/1354">mmdetection</a>, Detectron2 does not have any built-in integration with Albumentations.</p>

<p>So instead, I started looking for custom ways to integrate Albumentations into Detectron2. Since Detectron2 already has its own data transforms API, I expected integration with Albumentations to be relatively straightforward. However, it turned out to be a little more complicated than I expected. Therefore, I’m sharing my findings here in case they are useful to anyone else.</p>

<h2 id="the-goal-albumentations-wrapper-in-detectron2">The goal: Albumentations wrapper in Detectron2</h2>

<p>Because Detectron2 has some useful data transformations, the goal here is to use the Detectron2 transforms API as well as the Albumentations API in the same pipeline. It should be possible to integrate this into a standard Detectron2 pipeline. Here’s what it should look like:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">detectron2.data.transforms</span> <span class="k">as</span> <span class="n">T</span>
<span class="kn">import</span> <span class="nn">albumentations</span> <span class="k">as</span> <span class="n">A</span>

<span class="n">augmentations</span> <span class="o">=</span> <span class="p">[</span>
    <span class="n">T</span><span class="p">.</span><span class="n">Albumentations</span><span class="p">(</span><span class="n">A</span><span class="p">.</span><span class="n">HorizontalFlip</span><span class="p">(</span><span class="n">p</span><span class="o">=</span><span class="mf">0.5</span><span class="p">)),</span>
    <span class="n">T</span><span class="p">.</span><span class="n">Albumentations</span><span class="p">(</span><span class="n">A</span><span class="p">.</span><span class="n">RandomBrightnessContrast</span><span class="p">(</span><span class="n">p</span><span class="o">=</span><span class="mf">0.5</span><span class="p">)),</span>
    <span class="n">T</span><span class="p">.</span><span class="n">FixedSizeCrop</span><span class="p">(</span><span class="n">crop_size</span><span class="o">=</span><span class="p">(</span><span class="mi">256</span><span class="p">,</span> <span class="mi">256</span><span class="p">)),</span>
<span class="p">]</span>
</code></pre></div></div>

<p>Let’s say you’re using the standard <code class="language-plaintext highlighter-rouge">DatasetMapper</code> class from Detectron2. The augmentations could then be passed to it like this:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">detectron2.data</span> <span class="kn">import</span> <span class="n">DatasetMapper</span>

<span class="n">mapper</span> <span class="o">=</span> <span class="n">DatasetMapper</span><span class="p">(</span><span class="n">cfg</span><span class="p">,</span> <span class="n">augmentations</span><span class="o">=</span><span class="n">augmentations</span><span class="p">)</span>
</code></pre></div></div>

<p>Or, here’s an example of a custom training dataloader using the <code class="language-plaintext highlighter-rouge">DefaultTrainer</code> class:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">detectron2.data.transforms</span> <span class="k">as</span> <span class="n">T</span>
<span class="kn">from</span> <span class="nn">detectron2.engine</span> <span class="kn">import</span> <span class="n">DefaultTrainer</span>
<span class="kn">from</span> <span class="nn">detectron2.data</span> <span class="kn">import</span> <span class="n">build_detection_train_loader</span><span class="p">,</span> <span class="n">DatasetMapper</span>
<span class="kn">import</span> <span class="nn">albumentations</span> <span class="k">as</span> <span class="n">A</span>

<span class="k">def</span> <span class="nf">get_augmentations</span><span class="p">():</span>
    <span class="k">return</span> <span class="p">[</span>
        <span class="n">T</span><span class="p">.</span><span class="n">Albumentations</span><span class="p">(</span><span class="n">A</span><span class="p">.</span><span class="n">HorizontalFlip</span><span class="p">(</span><span class="n">p</span><span class="o">=</span><span class="mf">0.5</span><span class="p">)),</span>
        <span class="n">T</span><span class="p">.</span><span class="n">Albumentations</span><span class="p">(</span><span class="n">A</span><span class="p">.</span><span class="n">RandomBrightnessContrast</span><span class="p">(</span><span class="n">p</span><span class="o">=</span><span class="mf">0.5</span><span class="p">)),</span>
        <span class="n">T</span><span class="p">.</span><span class="n">FixedSizeCrop</span><span class="p">(</span><span class="n">crop_size</span><span class="o">=</span><span class="p">(</span><span class="mi">256</span><span class="p">,</span> <span class="mi">256</span><span class="p">)),</span>
    <span class="p">]</span>

<span class="k">class</span> <span class="nc">Trainer</span><span class="p">(</span><span class="n">DefaultTrainer</span><span class="p">):</span>
    <span class="o">@</span><span class="nb">classmethod</span>
    <span class="k">def</span> <span class="nf">build_train_loader</span><span class="p">(</span><span class="n">cls</span><span class="p">,</span> <span class="n">cfg</span><span class="p">):</span>
        <span class="n">mapper</span> <span class="o">=</span> <span class="n">DatasetMapper</span><span class="p">(</span>
            <span class="n">cfg</span><span class="p">,</span> <span class="n">is_train</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">augmentations</span><span class="o">=</span><span class="n">get_augmentations</span><span class="p">(),</span>
        <span class="p">)</span>
        <span class="k">return</span> <span class="n">build_detection_train_loader</span><span class="p">(</span><span class="n">cfg</span><span class="p">,</span> <span class="n">mapper</span><span class="o">=</span><span class="n">mapper</span><span class="p">)</span>
</code></pre></div></div>

<p>In the next sections, I will go through an explanation of how this can be achieved. If you just want the code, skip to the <a href="#conclusion">last section</a>, while making sure to read the <a href="#limitations">limitations section</a>.</p>

<h2 id="how-does-detectron2-use-data-transforms">How does Detectron2 use data transforms?</h2>

<p>Let’s start by looking at the <code class="language-plaintext highlighter-rouge">detectron2.data.transforms</code> API. Detectron2 uses two primary classes for data augmentation: <code class="language-plaintext highlighter-rouge">Augmentation</code> and <code class="language-plaintext highlighter-rouge">Transform</code> (defined <a href="https://github.com/facebookresearch/detectron2/blob/b7c7f4ba82192ff06f2bbb162b9f67b00ea55867/detectron2/data/transforms/augmentation.py#L80">here</a> and <a href="https://github.com/facebookresearch/fvcore/blob/1856b63f7067a987470999876d7d38ee241a6a32/fvcore/transforms/transform.py#L28">here</a>). The <code class="language-plaintext highlighter-rouge">Augmentation</code> class acts as a wrapper around the <code class="language-plaintext highlighter-rouge">Transform</code> class. The primary difference between the two classes is that calling <code class="language-plaintext highlighter-rouge">Transform</code> is expected to be deterministic, whereas <code class="language-plaintext highlighter-rouge">Augmentation</code> can be non-deterministic.</p>

<p>The reason for this design choice (or at least one of the reasons) appears to be that in a standard Detectron2 pipeline, data transformations are applied not in one single place, but in several. This can be seen in the <code class="language-plaintext highlighter-rouge">DatasetMapper</code> class, which takes metadata for a single image as input and maps it into a format used by the model. Here are the relevant code snippets:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># From detectron2/data/dataset_mapper.py
</span><span class="k">def</span> <span class="nf">__call__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">dataset_dict</span><span class="p">):</span>
    <span class="c1"># Note that `dataset_dict` represents Metadata of a single image
</span>
    <span class="c1"># ...
</span>
    <span class="c1"># Line 163
</span>    <span class="n">aug_input</span> <span class="o">=</span> <span class="n">T</span><span class="p">.</span><span class="n">AugInput</span><span class="p">(</span><span class="n">image</span><span class="p">,</span> <span class="n">sem_seg</span><span class="o">=</span><span class="n">sem_seg_gt</span><span class="p">)</span>
    <span class="n">transforms</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">augmentations</span><span class="p">(</span><span class="n">aug_input</span><span class="p">)</span>
    <span class="n">image</span><span class="p">,</span> <span class="n">sem_seg_gt</span> <span class="o">=</span> <span class="n">aug_input</span><span class="p">.</span><span class="n">image</span><span class="p">,</span> <span class="n">aug_input</span><span class="p">.</span><span class="n">sem_seg</span>

    <span class="c1"># ...
</span>
    <span class="c1"># Line 188
</span>    <span class="k">if</span> <span class="s">"annotations"</span> <span class="ow">in</span> <span class="n">dataset_dict</span><span class="p">:</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">_transform_annotations</span><span class="p">(</span><span class="n">dataset_dict</span><span class="p">,</span> <span class="n">transforms</span><span class="p">,</span> <span class="n">image_shape</span><span class="p">)</span>
</code></pre></div></div>

<p>At a high level, this code shows that data transformations are applied in two places:</p>

<ul>
  <li>Line <a href="https://github.com/facebookresearch/detectron2/blob/b7c7f4ba82192ff06f2bbb162b9f67b00ea55867/detectron2/data/dataset_mapper.py#L163">163-165</a>: image-level annotations (image + semantic segmentation mask)</li>
  <li>Line <a href="https://github.com/facebookresearch/detectron2/blob/b7c7f4ba82192ff06f2bbb162b9f67b00ea55867/detectron2/data/dataset_mapper.py#L188">188-189</a>: instance annotations (bounding boxes + instance segmentations)</li>
</ul>

<p>In both these places, the same <code class="language-plaintext highlighter-rouge">transforms</code> variable is used to transform the data. Now consider the following: The data transforms need to be applied in the same way (i.e. deterministically) in order for the image and its annotations to maintain consistency. For example, when using a random rotation transform, if the image was rotated by 15 degrees, but the bounding boxes were rotated by 25 degrees, then clearly this would lead to inconsistency between the image and the bounding boxes. Instead, the image and bounding boxes need to be rotated by the same amount to maintain alignment. Therefore, <code class="language-plaintext highlighter-rouge">transforms</code> needs to lead to the same result each time it is applied.</p>

<p>Determinism also comes with drawbacks. In many cases, it is not ideal to use only deterministic transforms in a deep learning pipeline. Data augmentation is a powerful tool because it can transform data in <em>random</em> ways, to increase the diversity of the dataset. For example, for a single image, you can randomly vary the brightness level, to make your model better at handling various brightness ranges. This is particularly useful for smaller datasets, but can just as easily help build <a href="https://ai.meta.com/blog/advancing-computer-vision-research-with-new-detectron2-mask-r-cnn-baselines/">state-of-the-art models</a>.</p>

<p>Detectron2 addresses the need for random augmentations by defining the <code class="language-plaintext highlighter-rouge">Augmentation</code> class. Basically, this class constructs a <code class="language-plaintext highlighter-rouge">Transform</code> class instance, in a non-deterministic way. Specifically, by calling <code class="language-plaintext highlighter-rouge">Augmentation.get_transform()</code>, a new <code class="language-plaintext highlighter-rouge">Transform</code> instance is created with randomly sampled parameters. If we look back at the <code class="language-plaintext highlighter-rouge">DatasetMapper.__call__</code> definition shown above, where augmentations are applied in two separate places, the following procedure is applied under the hood: First, call <code class="language-plaintext highlighter-rouge">Augmentation.get_transform()</code> once at the beginning of the function to randomly sample a new <code class="language-plaintext highlighter-rouge">Transform</code> instance. Then, apply this transform deterministically to all input data.</p>

<h2 id="albumentations-slightly-different">Albumentations: Slightly different</h2>

<p>On the other hand, the default Albumentations API applies transforms in a fully non-deterministic way, where the transformations are randomly applied each time they are called. Unlike Detectron2, there is no intermediate abstraction which is used for applying transforms in a deterministic way. For example, every time you call <code class="language-plaintext highlighter-rouge">A.RandomCrop</code>, you will get a different crop. This works because the transformations are all applied in a single call. Consider the following standard Albumentations workflow:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">albumentations</span> <span class="k">as</span> <span class="n">A</span>

<span class="n">transform</span> <span class="o">=</span> <span class="n">A</span><span class="p">.</span><span class="n">Compose</span><span class="p">([</span>
    <span class="n">A</span><span class="p">.</span><span class="n">RandomBrightnessContrast</span><span class="p">(),</span>
    <span class="n">A</span><span class="p">.</span><span class="n">RandomCrop</span><span class="p">(</span><span class="mi">256</span><span class="p">,</span> <span class="mi">256</span><span class="p">),</span>
<span class="p">],</span> <span class="n">bbox_params</span><span class="o">=</span><span class="n">A</span><span class="p">.</span><span class="n">BboxParams</span><span class="p">(</span><span class="nb">format</span><span class="o">=</span><span class="s">'coco'</span><span class="p">))</span>

<span class="n">image</span> <span class="o">=</span> <span class="p">...</span>   <span class="c1"># image here
</span><span class="n">bboxes</span> <span class="o">=</span> <span class="p">...</span>  <span class="c1"># bounding boxes here
</span>
<span class="n">transformed</span> <span class="o">=</span> <span class="n">transform</span><span class="p">(</span><span class="n">image</span><span class="o">=</span><span class="n">image</span><span class="p">,</span> <span class="n">bboxes</span><span class="o">=</span><span class="n">bboxes</span><span class="p">)</span>
</code></pre></div></div>

<p>Even though calling <code class="language-plaintext highlighter-rouge">transform</code> here is non-deterministic (you get a different result every time you call it), this still works correctly because all data is passed to the transform at the same time. This means all data (in this case, the image and bounding boxes) will be transformed in a way that maintains alignment between them. Note that this means it is expected of the user to call <code class="language-plaintext highlighter-rouge">transform</code> only once for a single image and its annotations. Applying the transform separately to the image and bounding boxes would lead to inconsistencies, since the random parameters would be different each time.</p>

<h2 id="putting-the-pieces-together">Putting the pieces together</h2>

<p>The non-determinism of the Albumentations API makes the integration with Detectron2 non-trivial, because it expects augmentations that can be applied several times in a deterministic way. However, when inspecting the Albumentations code more closely, it can be seen that there is actually a way to apply transformations in a deterministic way. Concretely, the Albumentations <code class="language-plaintext highlighter-rouge">BasicTransform</code> class defines a <code class="language-plaintext highlighter-rouge">get_params()</code> method (<a href="https://github.com/albumentations-team/albumentations/blob/0dd39463aad7eaa42eb924121f81466b068c894b/albumentations/core/transforms_interface.py#L136">here</a>), which randomly samples the parameters of the transform it is called on. What this means is that after <code class="language-plaintext highlighter-rouge">get_params()</code> is called, the returned parameters can be passed to the transform, which leads to execution of the transform in a deterministic way. For example, here is the implementation of <code class="language-plaintext highlighter-rouge">get_params()</code> for <code class="language-plaintext highlighter-rouge">A.RandomCrop</code> (<a href="https://github.com/albumentations-team/albumentations/blob/0dd39463aad7eaa42eb924121f81466b068c894b/albumentations/augmentations/crops/transforms.py#L87C4-L88C72">source</a>), which samples the <code class="language-plaintext highlighter-rouge">h_start</code> and <code class="language-plaintext highlighter-rouge">w_start</code> parameters used to define the size of the random crop:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="k">def</span> <span class="nf">get_params</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="nb">float</span><span class="p">]:</span>
        <span class="k">return</span> <span class="p">{</span><span class="s">"h_start"</span><span class="p">:</span> <span class="n">random</span><span class="p">.</span><span class="n">random</span><span class="p">(),</span> <span class="s">"w_start"</span><span class="p">:</span> <span class="n">random</span><span class="p">.</span><span class="n">random</span><span class="p">()}</span>
</code></pre></div></div>

<p>Putting all of these pieces together, we can come up with the following Albumentations wrapper in Detectron2 (thanks to <a href="https://github.com/KUASWoodyLIN">KUASWoodyLIN</a> for his code contribution):</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Albumentations</span><span class="p">(</span><span class="n">T</span><span class="p">.</span><span class="n">Augmentation</span><span class="p">):</span>
    <span class="s">"""
    Wrap an augmentation from the albumentations library:
    https://github.com/albumentations-team/albumentations/.
    Image, Bounding Box and Segmentation are supported.
    """</span>

    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">aug</span><span class="p">):</span>
        <span class="s">"""
        Args:
            aug (albumentations.BasicTransform): albumentations transform
        """</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">_aug</span> <span class="o">=</span> <span class="n">aug</span>

    <span class="k">def</span> <span class="nf">get_transform</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">image</span><span class="p">):</span>
        <span class="n">do</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">_rand_range</span><span class="p">()</span> <span class="o">&lt;</span> <span class="bp">self</span><span class="p">.</span><span class="n">_aug</span><span class="p">.</span><span class="n">p</span>
        <span class="k">if</span> <span class="n">do</span><span class="p">:</span>
            <span class="n">params</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">prepare_params</span><span class="p">(</span><span class="n">image</span><span class="p">)</span>
            <span class="n">h</span><span class="p">,</span> <span class="n">w</span> <span class="o">=</span> <span class="n">image</span><span class="p">.</span><span class="n">shape</span><span class="p">[:</span><span class="mi">2</span><span class="p">]</span>
            <span class="k">return</span> <span class="n">AlbumentationsTransform</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">_aug</span><span class="p">,</span> <span class="n">params</span><span class="p">,</span> <span class="n">h</span><span class="p">,</span> <span class="n">w</span><span class="p">)</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="k">return</span> <span class="n">NoOpTransform</span><span class="p">()</span>

    <span class="k">def</span> <span class="nf">prepare_params</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">image</span><span class="p">):</span>
        <span class="n">params</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">_aug</span><span class="p">.</span><span class="n">get_params</span><span class="p">()</span>
        <span class="k">if</span> <span class="bp">self</span><span class="p">.</span><span class="n">_aug</span><span class="p">.</span><span class="n">targets_as_params</span><span class="p">:</span>
            <span class="n">targets_as_params</span> <span class="o">=</span> <span class="p">{</span><span class="s">"image"</span><span class="p">:</span> <span class="n">image</span><span class="p">}</span>
            <span class="c1"># Get additional parameters dependent on the input image
</span>            <span class="n">params_dependent_on_targets</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">_aug</span><span class="p">.</span><span class="n">get_params_dependent_on_targets</span><span class="p">(</span>
                <span class="n">targets_as_params</span>
            <span class="p">)</span>
            <span class="n">params</span><span class="p">.</span><span class="n">update</span><span class="p">(</span><span class="n">params_dependent_on_targets</span><span class="p">)</span>
        <span class="n">params</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">_aug</span><span class="p">.</span><span class="n">update_params</span><span class="p">(</span><span class="n">params</span><span class="p">,</span> <span class="o">**</span><span class="p">{</span><span class="s">"image"</span><span class="p">:</span> <span class="n">image</span><span class="p">})</span>
        <span class="k">return</span> <span class="n">params</span>
</code></pre></div></div>

<p>The key idea is that <code class="language-plaintext highlighter-rouge">get_transform()</code> returns a deterministic transform, using randomly sampled parameters obtained by calling <code class="language-plaintext highlighter-rouge">prepare_params()</code>. The <code class="language-plaintext highlighter-rouge">AlbumentationsTransform</code> class takes the Albumentations transform along with the parameters and applies the transform deterministically. The full source code for <code class="language-plaintext highlighter-rouge">AlbumentationTransform</code> can be found <a href="https://github.com/tobiasvanderwerff/detectron2/blob/db82d1ae8dbe8fe1d47e851ea1730af6eded849b/detectron2/data/transforms/augmentation_impl.py#L740">here</a>.</p>

<p>By using this wrapper class, we can now define Albumentations transforms as part of our regular Detectron2 pipeline, as shown in the <a href="#the-goal-albumentations-wrapper-in-detectron2">beginning of this post</a>.</p>

<h2 id="limitations">Limitations</h2>

<p>There are some limitations to this implementation to be aware of:</p>

<ul>
  <li>It does not work for keypoints yet. Keypoint augmentations could be added relatively easily, by implementing the <code class="language-plaintext highlighter-rouge">AlbumentationsTransform.apply_coords</code> method. The main reason I did not implement it yet is because I did not have any keypoint data to test on.</li>
  <li>It does not work for composite transforms, e.g. <code class="language-plaintext highlighter-rouge">A.Compose</code> or <code class="language-plaintext highlighter-rouge">A.OneOf</code>.</li>
</ul>

<h2 id="conclusion">Conclusion</h2>

<p>If you want to use this implementation: I forked Detectron2 and added my changes there, which can be found <a href="https://github.com/tobiasvanderwerff/detectron2">here</a>. If you just want a pip install command, use the following:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip <span class="nb">install</span> <span class="s1">'git+https://github.com/tobiasvanderwerff/detectron2.git'</span>
</code></pre></div></div>

<p>Alternatively, you could also copy the <code class="language-plaintext highlighter-rouge">Albumentations</code> and <code class="language-plaintext highlighter-rouge">AlbumentationsTransform</code> classes as defined <a href="https://github.com/tobiasvanderwerff/detectron2/blob/db82d1ae8dbe8fe1d47e851ea1730af6eded849b/detectron2/data/transforms/augmentation_impl.py#L740">here</a> and <a href="https://github.com/tobiasvanderwerff/detectron2/blob/eae825324d2beeb6ed546fde04b1bbe9a17d40ca/detectron2/data/transforms/transform.py#L308">here</a> to your own project.</p>

<p>I’ve made a <a href="https://github.com/facebookresearch/detectron2/pull/5253#issue-2234148912">pull request</a> for the changes to be integrated into Detectron2, but given the recent lack of activity by maintainers, it is unclear if or when it might get merged.</p>

<p>In summary, by leveraging the <code class="language-plaintext highlighter-rouge">get_params()</code> functionality in Albumentations to enable deterministic transforms, we can create a custom wrapper that allows using Albumentations within the Detectron2 augmentations API. This enables leveraging Albumentations’ wide array of augmentations to improve model performance.</p>]]></content><author><name>Tobias van der Werff</name></author><category term="tutorial" /><category term="python" /><category term="programming" /><summary type="html"><![CDATA[I have recently been using Detectron2 to train deep learning models for object detection and instance segmentation. This works great, but I found that there is one area in which Detectron2 is lacking: data augmentation. Although it has a decent API for data transforms, it has a limited selection of useful data augmentations. Normally, a dedicated data augmentation library such as Albumentations would be my first choice for this. Unfortunately, unlike similar libraries such as mmdetection, Detectron2 does not have any built-in integration with Albumentations.]]></summary></entry></feed>