<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[AI for Software Engineers: Build]]></title><description><![CDATA[Hands-on guides to the most important topics and technologies in AI.]]></description><link>https://www.aiforswes.com/s/build</link><image><url>https://substackcdn.com/image/fetch/$s_!5jDe!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F667233e2-8e44-4fda-a882-db68801c3736_500x500.png</url><title>AI for Software Engineers: Build</title><link>https://www.aiforswes.com/s/build</link></image><generator>Substack</generator><lastBuildDate>Sat, 25 Jul 2026 22:41:51 GMT</lastBuildDate><atom:link href="https://www.aiforswes.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Logan Thorneloe]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[aiforswes@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[aiforswes@substack.com]]></itunes:email><itunes:name><![CDATA[Logan Thorneloe]]></itunes:name></itunes:owner><itunes:author><![CDATA[Logan Thorneloe]]></itunes:author><googleplay:owner><![CDATA[aiforswes@substack.com]]></googleplay:owner><googleplay:email><![CDATA[aiforswes@substack.com]]></googleplay:email><googleplay:author><![CDATA[Logan Thorneloe]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[[Revised] You Don’t Need to Spend $100/mo on Claude Code: Your Guide to Local Coding Models]]></title><description><![CDATA[What you need to know about local model tooling and the steps for setting one up yourself]]></description><link>https://www.aiforswes.com/p/you-dont-need-to-spend-100mo-on-claude</link><guid isPermaLink="false">https://www.aiforswes.com/p/you-dont-need-to-spend-100mo-on-claude</guid><dc:creator><![CDATA[Logan Thorneloe]]></dc:creator><pubDate>Sat, 20 Dec 2025 14:55:12 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!NA8U!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F48e42e69-242b-409d-9b0a-b1af36ead2a9_1280x720.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!NA8U!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F48e42e69-242b-409d-9b0a-b1af36ead2a9_1280x720.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!NA8U!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F48e42e69-242b-409d-9b0a-b1af36ead2a9_1280x720.png 424w, https://substackcdn.com/image/fetch/$s_!NA8U!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F48e42e69-242b-409d-9b0a-b1af36ead2a9_1280x720.png 848w, https://substackcdn.com/image/fetch/$s_!NA8U!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F48e42e69-242b-409d-9b0a-b1af36ead2a9_1280x720.png 1272w, https://substackcdn.com/image/fetch/$s_!NA8U!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F48e42e69-242b-409d-9b0a-b1af36ead2a9_1280x720.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!NA8U!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F48e42e69-242b-409d-9b0a-b1af36ead2a9_1280x720.png" width="1280" height="720" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/48e42e69-242b-409d-9b0a-b1af36ead2a9_1280x720.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:720,&quot;width&quot;:1280,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:673161,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.aiforswes.com/i/182132050?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F48e42e69-242b-409d-9b0a-b1af36ead2a9_1280x720.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!NA8U!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F48e42e69-242b-409d-9b0a-b1af36ead2a9_1280x720.png 424w, https://substackcdn.com/image/fetch/$s_!NA8U!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F48e42e69-242b-409d-9b0a-b1af36ead2a9_1280x720.png 848w, https://substackcdn.com/image/fetch/$s_!NA8U!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F48e42e69-242b-409d-9b0a-b1af36ead2a9_1280x720.png 1272w, https://substackcdn.com/image/fetch/$s_!NA8U!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F48e42e69-242b-409d-9b0a-b1af36ead2a9_1280x720.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><em>[Edit 1] This article has been edited after initial release for clarity. Both the tl;dr and the end section have added information.</em></p><div><hr></div><p><em>[Edit 2] This hypothesis was <strong>actually wrong</strong> and thank you to everyone who commented! </em></p><p><em>Here&#8217;s a full explanation of where I went wrong. I want to address this mistake as I realize it might have a meaningful impact on someone's financial position.</em></p><p><em>I&#8217;m <strong>not </strong>editing the actual article except where absolutely necessary so it doesn&#8217;t look like I&#8217;m covering up the mistake&#8212;I want to address it. Instead, I&#8217;ve included the important information below. </em></p><p><em>There is one takeaway this article provides that definitely holds true:</em></p><ul><li><p><em>Local models are far more capable than they&#8217;re given credit for, even for coding.</em></p></li></ul><p><em>It also explains the process of setting up a local coding model and technical information about doing so which is helpful for anyone wanting to set up a local coding model. I would still recommend doing so.</em></p><p><em><strong>But do I want someone reading this to immediately drop their coding subscription and buy a maxed out MacBook Pro? No, and for that reason I need to correct my hypothesis from &#8216;Yes, with caveats&#8217; to &#8216;No&#8217;.</strong></em></p><p><em>This article was not an empirical assessment, but should have been to make these claims. Here&#8217;s where I went wrong:</em></p><ul><li><p><em>While local models can likely complete ~90% of the software development tasks that something like Claude Code can, the last 10% is the most important. When it comes to your job, that last 10% is worth paying more for to get that last bit of performance.</em></p></li><li><p><em>I realized I looked at this more from the angle of a hobbiest paying for these coding tools. Someone doing little side projects&#8212;not someone in a production setting. I did this because I see a lot of people signing up for $100/mo or $200/mo coding subscriptions for personal projects when they likely don&#8217;t need to. <strong>I would not recommend running local models as a company</strong> instead of giving employees access to a tool like Claude Code.</em></p></li><li><p><em>While larger local models are very capable, as soon as you run other development tools (Docker, etc.) that also eat into your RAM, your model needs to be much smaller and becomes a lot less capable. I didn&#8217;t factor this in in my experiment.</em></p></li></ul><p><em>So, really, the takeaway should be that these are incredible supplemental models to frontier models when coding and could potentially save you on your subscription by dropping it down a tier, but practically they&#8217;re not worth the effort in situations that might affect your livelihood.</em></p><div><hr></div><p>Exactly a month ago, I made a hypothesis: Instead of paying $100/mo+ for an AI coding subscription, my money would be better spent upgrading my hardware so I can run local coding models at a fraction of the price (and have better hardware too!).</p><p>So, to create by far the most expensive article I&#8217;ve ever written, I put my money where my mouth is and bought a MacBook Pro with 128 GB of RAM to get to work. My idea was simple: Over the life of the MacBook I&#8217;d recoup the costs of it by not paying for an AI coding subscription.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!msVz!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c9d7143-3d10-4898-923a-7bc517ca615a_1196x764.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!msVz!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c9d7143-3d10-4898-923a-7bc517ca615a_1196x764.png 424w, https://substackcdn.com/image/fetch/$s_!msVz!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c9d7143-3d10-4898-923a-7bc517ca615a_1196x764.png 848w, https://substackcdn.com/image/fetch/$s_!msVz!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c9d7143-3d10-4898-923a-7bc517ca615a_1196x764.png 1272w, https://substackcdn.com/image/fetch/$s_!msVz!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c9d7143-3d10-4898-923a-7bc517ca615a_1196x764.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!msVz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c9d7143-3d10-4898-923a-7bc517ca615a_1196x764.png" width="618" height="394.7759197324415" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1c9d7143-3d10-4898-923a-7bc517ca615a_1196x764.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:764,&quot;width&quot;:1196,&quot;resizeWidth&quot;:618,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!msVz!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c9d7143-3d10-4898-923a-7bc517ca615a_1196x764.png 424w, https://substackcdn.com/image/fetch/$s_!msVz!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c9d7143-3d10-4898-923a-7bc517ca615a_1196x764.png 848w, https://substackcdn.com/image/fetch/$s_!msVz!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c9d7143-3d10-4898-923a-7bc517ca615a_1196x764.png 1272w, https://substackcdn.com/image/fetch/$s_!msVz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1c9d7143-3d10-4898-923a-7bc517ca615a_1196x764.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>After weeks of experimenting and setting up local AI models and coding tools, I&#8217;ve come to the conclusion that <strong>my hypothesis was <s>correct, with nuance</s></strong>,<strong> not correct </strong>[see edit 2 above] which I&#8217;ll get into later in this article.</p><p>In this article, we cover:</p><ul><li><p>Why local models matter and the benefits they provide.</p></li><li><p>How to view memory usage and make estimates for which models can run on your machine and the RAM demands for coding applications.</p></li><li><p>Walk through setting up your own local coding model and tool step-by-step.</p></li></ul><p>Don&#8217;t worry if you don&#8217;t have a high-RAM machine! You can still follow this guide. I&#8217;ve included some models to try out with a lower memory allotment. I think you&#8217;ll be surprised at how performant even the smallest of models is. In fact, there hasn&#8217;t really been a time during this experiment that I&#8217;ve been disappointed with model performance.</p><p>If you&#8217;re only here for the local coding tool setup, skip to the section at the bottom. I&#8217;ve even included a link to my modelfiles in that section to make setup even easier for you. Otherwise, let&#8217;s get into what you need to know.</p><div><hr></div><h2><strong>tl;dr</strong>:</h2><ul><li><p><strong>Local coding models are very capable</strong>. Using the right model and the right tooling feels only half a generation behind the frontier cloud tools. I would say that for about 90% of developer work local models are more than sufficient. Even small 7B parameter models can be very capable. <strong>[Edited to add in this next part]</strong> Local models won&#8217;t compete with frontier models at the peak of performance, but can complete many coding tasks just as well for a fraction of the cost. They&#8217;re worth running to bring costs down on plenty of tasks but potentially not worth using if there&#8217;s a free tier available that performs better.</p></li><li><p><strong>Tools matter a lot</strong>. This is where I experienced the most disappointment. I tried many different tools with many different models and spent a lot of time tinkering. I ran into situations where the models wouldn&#8217;t call tools properly or their thinking traces wouldn&#8217;t close. Both of these rendered the tool essentially useless. Currently, tooling seems very finicky and if there&#8217;s anything developers need to be successful, it&#8217;s good tools.</p></li><li><p><strong>There&#8217;s a lot to consider when you&#8217;re actually working within hardware constraints.</strong> We take the tooling set up for us in the cloud for granted. When setting up local models, I had to think a lot about trade-offs in performance versus memory usage, how different tools compared and affected performance, nuances in types of models, how to quantize, and other user-facing factors such as time-to-first-token and tokens per second.</p></li><li><p><strong>Google threw a wrench into my hypothesis</strong>. The local setup is almost a no-brainer when compared to a $100/mo+ subscription. Compared to free or nearly-free tooling (such as Gemini CLI, Jules, or Antigravity) there isn&#8217;t quite as strong of a monetary justification to spend more on hardware. There are benefits to local models outside of code, though, and I discuss those below. </p></li></ul><p>If the tl;dr was helpful, don&#8217;t forget to subscribe to get more in your inbox.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.aiforswes.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.aiforswes.com/subscribe?"><span>Subscribe now</span></a></p><div><hr></div><h2>Why local models?</h2><p>You might wonder why local models are worth investing in at all. The obvious answer is <strong>cost</strong>. By using your own hardware, you don&#8217;t need to pay a subscription fee to a cloud provider for your tool. There are also a few less obvious and underrated reasons that make local models useful.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!OUrN!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c71b176-e011-4694-915e-217a2e3ce9b5_1920x1080.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!OUrN!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c71b176-e011-4694-915e-217a2e3ce9b5_1920x1080.jpeg 424w, https://substackcdn.com/image/fetch/$s_!OUrN!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c71b176-e011-4694-915e-217a2e3ce9b5_1920x1080.jpeg 848w, https://substackcdn.com/image/fetch/$s_!OUrN!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c71b176-e011-4694-915e-217a2e3ce9b5_1920x1080.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!OUrN!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c71b176-e011-4694-915e-217a2e3ce9b5_1920x1080.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!OUrN!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c71b176-e011-4694-915e-217a2e3ce9b5_1920x1080.jpeg" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0c71b176-e011-4694-915e-217a2e3ce9b5_1920x1080.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:140073,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.aiforswes.com/i/182132050?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c71b176-e011-4694-915e-217a2e3ce9b5_1920x1080.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!OUrN!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c71b176-e011-4694-915e-217a2e3ce9b5_1920x1080.jpeg 424w, https://substackcdn.com/image/fetch/$s_!OUrN!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c71b176-e011-4694-915e-217a2e3ce9b5_1920x1080.jpeg 848w, https://substackcdn.com/image/fetch/$s_!OUrN!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c71b176-e011-4694-915e-217a2e3ce9b5_1920x1080.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!OUrN!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c71b176-e011-4694-915e-217a2e3ce9b5_1920x1080.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>First: <strong>Reliability</strong>. Each week there seems to be complaints about performance regression within AI coding tools. Many speculate companies are pulling tricks to save resources that hurt model performance. With cloud providers, you&#8217;re at the mercy of the provider for when this happens. With local models, this only happens when you cause it to.</p><p>Second: <strong>Local models can apply to </strong><em><strong>far </strong></em><strong>more applications</strong>. Just the other day I was having a discussion with my dad about AI tooling he could use to streamline his work. His job requires studying a lot of data&#8212;a perfect application for an LLM-based tool&#8212;but his company blocks tools like Gemini and ChatGPT because a lot of this analysis is done on intellectual property. Unfortunately, he isn&#8217;t provided a suitable alternative to use.</p><p>With a local model, he wouldn&#8217;t have to worry about these IP issues. He could run his analyses without data ever leaving his machine. Of course, any tool calling would also need to ensure data never leaves the machine, but local models get around one of the largest hurdles for useful enterprise AI adoption. Running models on a local machine opens up an entire world of privacy- and security-centric AI applications that are expensive for cloud providers to provide.</p><p>Finally: <strong>Availability. </strong>Local models are available to you as long as your machine is. This means no worrying about your provider being down or rate limiting you due to high traffic. It also means using AI coding tools on planes or in other situations where internet access is locked down (think highly secure networks).</p><p>While local models do provide significant cost savings, the flexibility and reliability they provide can be even more valuable.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!LxTS!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F95d992f8-225f-4b06-8fb9-e9d7544cf2d5_1920x1080.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!LxTS!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F95d992f8-225f-4b06-8fb9-e9d7544cf2d5_1920x1080.jpeg 424w, https://substackcdn.com/image/fetch/$s_!LxTS!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F95d992f8-225f-4b06-8fb9-e9d7544cf2d5_1920x1080.jpeg 848w, https://substackcdn.com/image/fetch/$s_!LxTS!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F95d992f8-225f-4b06-8fb9-e9d7544cf2d5_1920x1080.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!LxTS!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F95d992f8-225f-4b06-8fb9-e9d7544cf2d5_1920x1080.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!LxTS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F95d992f8-225f-4b06-8fb9-e9d7544cf2d5_1920x1080.jpeg" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/95d992f8-225f-4b06-8fb9-e9d7544cf2d5_1920x1080.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:136854,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.aiforswes.com/i/182132050?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F95d992f8-225f-4b06-8fb9-e9d7544cf2d5_1920x1080.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!LxTS!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F95d992f8-225f-4b06-8fb9-e9d7544cf2d5_1920x1080.jpeg 424w, https://substackcdn.com/image/fetch/$s_!LxTS!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F95d992f8-225f-4b06-8fb9-e9d7544cf2d5_1920x1080.jpeg 848w, https://substackcdn.com/image/fetch/$s_!LxTS!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F95d992f8-225f-4b06-8fb9-e9d7544cf2d5_1920x1080.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!LxTS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F95d992f8-225f-4b06-8fb9-e9d7544cf2d5_1920x1080.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>Understanding memory</h2><p>To get going with local models you must understand the memory needed to run them on your machine. Obviously, if you have more memory you&#8217;ll be able to run better models, but understanding the nuances of that memory management will help you pick out the right model for your use case.</p><p>Local AI has two parts that eat up your memory: The model itself and the model&#8217;s context window.</p><p>The actual model has billions of parameters and all those parameters need to fit into your memory at once. Excellent local coding models start at around 30 billion (30B, for short) parameters in size. By default, these models use 16 bits to represent parameters. At 16 bits with 30B parameters, a model will take 60 GB of space in RAM (16 bits = 2 bytes per parameter, 30 billion parameters = 60 billion bytes which equals about 60 GB).</p><p>The second (and potentially larger) memory consuming part of local AI is the model&#8217;s context window. This is the model inputs and outputs that are stored so the model can reference them in future requests. This gives the model memory.</p><p>When coding with AI, we prefer this window to be as large as it can because we need to fit our codebase (or pieces of it) within our context window. This means we target a context window of 64,000 tokens or larger. All of these tokens will also be stored in RAM.</p><p>The important thing to understand about context windows is that the memory requirement per-token for a model depends on the size of that model. Models with more parameters tend to have large architectures (more hidden layers and larger dimensions to those layers). Larger architectures mean the model must store more information for each token within its key-value cache (context window) because it stores information for each token for each layer.</p><p>This means choosing an 80B parameter model over a 30B parameter model requires more memory for the model itself and also more memory for the same size context window. For example, a 30B parameter model might have a hidden dimension of 5120 with 64 layers while an 80B model has a hidden dimension of 8192 with 80 layers. Doing some back-of-the-napkin math shows us that the larger model requires approximately 2x more RAM to maintain the same context window as the 30B parameter model (see formula below).</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!cVCW!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F954df13c-8e66-4c72-a6a1-1182619a5e2b_1920x1080.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!cVCW!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F954df13c-8e66-4c72-a6a1-1182619a5e2b_1920x1080.jpeg 424w, https://substackcdn.com/image/fetch/$s_!cVCW!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F954df13c-8e66-4c72-a6a1-1182619a5e2b_1920x1080.jpeg 848w, https://substackcdn.com/image/fetch/$s_!cVCW!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F954df13c-8e66-4c72-a6a1-1182619a5e2b_1920x1080.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!cVCW!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F954df13c-8e66-4c72-a6a1-1182619a5e2b_1920x1080.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!cVCW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F954df13c-8e66-4c72-a6a1-1182619a5e2b_1920x1080.jpeg" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/954df13c-8e66-4c72-a6a1-1182619a5e2b_1920x1080.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:109124,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.aiforswes.com/i/182132050?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F954df13c-8e66-4c72-a6a1-1182619a5e2b_1920x1080.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!cVCW!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F954df13c-8e66-4c72-a6a1-1182619a5e2b_1920x1080.jpeg 424w, https://substackcdn.com/image/fetch/$s_!cVCW!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F954df13c-8e66-4c72-a6a1-1182619a5e2b_1920x1080.jpeg 848w, https://substackcdn.com/image/fetch/$s_!cVCW!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F954df13c-8e66-4c72-a6a1-1182619a5e2b_1920x1080.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!cVCW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F954df13c-8e66-4c72-a6a1-1182619a5e2b_1920x1080.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Luckily, there are tricks to better manage memory. First, there are architectural changes that can be made to make model inference more efficient so it requires less memory. The model we set up at the end of this article uses Hybrid Attention which enables a much smaller KV cache enabling us to fit our model and context window in less memory. I won&#8217;t get into more detail in this article, but you can read more about that model and how it works <a href="https://qwen.ai/blog?id=4074cca80393150c248e508aa62983f9cb7d27cd&amp;from=research.latest-advancements-list">here</a>.</p><p>The second trick is quantizing the values you&#8217;re working with. <a href="https://www.byteplus.com/en/what-is/quantization">Quantization means converting a continuous set of values into a smaller amount of distinct values</a>. In our case, that means taking a set of numbers represented by a certain number of bits (16, for example) and reducing it to a set of numbers represented by fewer bits (8, for example). To put it simply, in our case we&#8217;re converting the numbers representing our model to a smaller bit representation to save memory while keeping the value representations within the model relatively equal.</p><p>You can quantize both your model weights and the values stored in your context window. When you quantize your model weights, you &#8220;remove intelligence&#8221; from the model because it&#8217;s less precise in its representation of innate information. I&#8217;ve also found the performance hit when going from 16 to 8 bits within the model to be much less than 8 to 4.</p><p>We can also quantize the values in our context window to reduce its memory requirement. This means we&#8217;re less precisely representing the model&#8217;s memory. Generally speaking, KV cache (context window) quantization is considered more destructive to model performance than weight quantization because it <a href="https://arxiv.org/pdf/2510.10964">causes the model to forget details in long reasoning traces</a>. Thus, you should test quantizing the KV cache to ensure it doesn&#8217;t degrade model performance for your specific task.</p><p>In reality, like the rest of machine learning, optimizing local model performance is an experimentation process and real-world machine learning requires understanding the practical limitations and capabilities of models when applied to specific applications.</p><p>Here are a few more factors to understand when setting up a local coding model on your hardware:</p><h3><strong>Instruct versus non-instruct</strong></h3><p>Instruct models are post-trained to be well-suited for chat-based interactions. They&#8217;re given chat pairings in their training to be optimized for excellent back-and-forth chat output. Non-instruct models are still trained LLMs, but focus on next-token prediction instead of chatting with a user. For our case, when using a chat-based coding tool (CLI or chat agent in your IDE) we need to use an instruct model. If you&#8217;re setting up an autocomplete model, you&#8217;ll want to find a model specifically post-trained for it (such as Qwen2.5-Coder-Base or DeepSeek-Coder-V2).</p><h3><strong>Serving tools</strong></h3><p>You need a tool to serve your local LLM for your coding tool to send it requests. On a MacBook, there are two primary options: MLX and Ollama.</p><p>Ollama is the industry standard and works on non-Mac hardware. It&#8217;s a great serving setup on top of llama.cpp that makes model serving almost plug-and-play. Users can download model weights from Ollama easily and can configure modelfiles with custom parameters for serving. Ollama can also serve a model once and make it available to multiple tools.</p><p>MLX is a Mac-specific framework for machine learning that is optimized specifically for Mac hardware. It also retrieves models for the user from a community collection. I&#8217;ve found Ollama to be very reliable in its model catalog, while MLX&#8217;s catalog is community sourced and can sometimes be missing specific models. Models are sourced from the community so a user can convert a model to MLX format themselves. MLX requires a bit more setup on the user&#8217;s end, but serves models faster because it doesn&#8217;t have a layer providing the niceties of Ollama on top of it.</p><p>Either of these is great, but I chose MLX to maximize what I can get with my RAM, but Ollama is probably the more beginner-friendly tool here.</p><h3><strong>Time-to-first-token and tokens per second</strong></h3><p>In real-world LLM applications it&#8217;s important that the model is able to serve its first token for a request in a reasonable amount of time and continue serving tokens at a speed that enables the user to use the model for its given purpose. If we have a high-performance model running locally, but it only serves a few tokens per second, it wouldn&#8217;t be useful for coding.</p><p>This is something taken for granted with cloud-hosted models that is a real consideration when working locally on constrained hardware. Another reason I chose MLX as my serving platform is because it served tokens up to 20% faster than Ollama. In reality, Ollama served tokens fast enough so I don&#8217;t think using MLX is necessary specifically for this reason for the models I tried.</p><h3><strong>Performance trade-offs</strong></h3><p>There are many ways to optimize local models and save RAM. It&#8217;s difficult to know which optimization method works best and the impact each has on a model especially when using them in tandem with other methods.</p><p>The right optimization method also depends on the application. In my experience, I find it best to prioritize larger models with more aggressive model quantization over smaller models with more precise model weights. Since our application is coding, I would also prioritize a less-quantized KV cache and using a smaller model to ensure reasoning works properly while not sacrificing the size of our context window.</p><h3><strong>Coding tools</strong></h3><p>There are many tools to code with local models and I suggest trying until you find one you like. Some top recommendations are <a href="https://opencode.ai/">OpenCode</a>, <a href="https://aider.chat/">Aider</a>, <a href="https://github.com/QwenLM/qwen-code">Qwen Code</a>, <a href="https://roocode.com/">Roo Code</a>, and <a href="https://www.continue.dev/">Continue</a>. Make sure to use a tool compatible with <a href="https://bentoml.com/llm/llm-inference-basics/openai-compatible-api">OpenAI&#8217;s API standard</a>. While this should be most tools, this ensures a consistent model/tool connection. This makes it easier to switch between tools and models as needed.</p><h2>Getting set up</h2><p>I&#8217;ll spare you the trial and error I experienced getting this set up. The one thing I learned is that <strong>tooling matters a lot</strong>. Not all coding tools are created equal and not all of the models interact with tools equally. I experienced many times where tool calling or even running a tool at all was broken. I also had to tinker quite a bit with many of them to get them to work.</p><p>If you&#8217;re a PC enthusiast, an apt comparison to setting up local coding tools versus using the cloud offerings available is the difference between setting up a MacBook versus a Linux Laptop. With the Linux laptop, you might get well through the distro installation only to find that the drivers for your trackpad aren&#8217;t yet supported. Sometimes it felt like that with local models and hooking them to coding tools.</p><p>For my tool, I ended up going with Qwen Code. It was pretty plug-and-play as it&#8217;s a fork of Gemini CLI. It supports the OpenAI compatibility standard so I can easily sub in different models and affords me all of the niceties built into Gemini CLI that I&#8217;m familiar with using. I also know it&#8217;ll be supported because both the Qwen team and Google DeepMind are behind the tool. The tool is also open source so anyone can support it as needed.</p><p>For models, I focused on GPT-OSS and Qwen3 models since they were around the size I was looking for and had great reviews for coding. I ended up deciding to use Qwen3-Coder models because I found it performed best and because GPT-OSS frequently gave me &#8220;I cannot fulfill this request&#8221; responses when I asked it to build features.</p><p>I decided to serve my local models on MLX, but if you&#8217;re using a non-Mac device give Ollama a shot. A MacBook is an excellent machine for serving local models because of its unified memory architecture. This means the RAM can be allotted to the CPU or GPU as needed. MacBooks can also be configured with <em>a ton</em> of RAM. For serving local coding models, more is always better.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!4IEy!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a82350e-d1e0-420d-b4f3-7ea2343d3407_1920x1080.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!4IEy!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a82350e-d1e0-420d-b4f3-7ea2343d3407_1920x1080.jpeg 424w, https://substackcdn.com/image/fetch/$s_!4IEy!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a82350e-d1e0-420d-b4f3-7ea2343d3407_1920x1080.jpeg 848w, https://substackcdn.com/image/fetch/$s_!4IEy!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a82350e-d1e0-420d-b4f3-7ea2343d3407_1920x1080.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!4IEy!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a82350e-d1e0-420d-b4f3-7ea2343d3407_1920x1080.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!4IEy!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a82350e-d1e0-420d-b4f3-7ea2343d3407_1920x1080.jpeg" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4a82350e-d1e0-420d-b4f3-7ea2343d3407_1920x1080.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:133148,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.aiforswes.com/i/182132050?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a82350e-d1e0-420d-b4f3-7ea2343d3407_1920x1080.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!4IEy!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a82350e-d1e0-420d-b4f3-7ea2343d3407_1920x1080.jpeg 424w, https://substackcdn.com/image/fetch/$s_!4IEy!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a82350e-d1e0-420d-b4f3-7ea2343d3407_1920x1080.jpeg 848w, https://substackcdn.com/image/fetch/$s_!4IEy!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a82350e-d1e0-420d-b4f3-7ea2343d3407_1920x1080.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!4IEy!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a82350e-d1e0-420d-b4f3-7ea2343d3407_1920x1080.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Xqrl!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52524f56-954d-4bd9-8014-0bfb55cc2812_1920x1080.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Xqrl!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52524f56-954d-4bd9-8014-0bfb55cc2812_1920x1080.jpeg 424w, https://substackcdn.com/image/fetch/$s_!Xqrl!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52524f56-954d-4bd9-8014-0bfb55cc2812_1920x1080.jpeg 848w, https://substackcdn.com/image/fetch/$s_!Xqrl!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52524f56-954d-4bd9-8014-0bfb55cc2812_1920x1080.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!Xqrl!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52524f56-954d-4bd9-8014-0bfb55cc2812_1920x1080.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Xqrl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52524f56-954d-4bd9-8014-0bfb55cc2812_1920x1080.jpeg" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/52524f56-954d-4bd9-8014-0bfb55cc2812_1920x1080.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:171397,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.aiforswes.com/i/182132050?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52524f56-954d-4bd9-8014-0bfb55cc2812_1920x1080.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Xqrl!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52524f56-954d-4bd9-8014-0bfb55cc2812_1920x1080.jpeg 424w, https://substackcdn.com/image/fetch/$s_!Xqrl!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52524f56-954d-4bd9-8014-0bfb55cc2812_1920x1080.jpeg 848w, https://substackcdn.com/image/fetch/$s_!Xqrl!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52524f56-954d-4bd9-8014-0bfb55cc2812_1920x1080.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!Xqrl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52524f56-954d-4bd9-8014-0bfb55cc2812_1920x1080.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>I&#8217;ve shared my <a href="https://github.com/loganthorneloe/modelfiles">modelfiles repo</a> for you to reference and use as needed. I&#8217;ve got a script set up that automates much of the below process. Feel free to fork it and create your own modelfiles or star it to come back later.</p><ol><li><p>Install <a href="https://github.com/ml-explore/mlx">MLX</a> or download <a href="https://ollama.com/download">Ollama</a> (the rest of this guide will continue with MLX but details for serving on Ollama can be found <a href="https://docs.ollama.com/quickstart">here</a>).</p></li><li><p>Increase the VRAM limitation on your MacBook. macOS will automatically limit VRAM to 75% of the total RAM. We want to use more than that. Run sudo sysctl iogpu.wired_limit_mb=110000 in your terminal to set this up (adjust the mb setting according to the RAM on your MacBook). This needs to be set each time you restart your MacBook.</p></li><li><p>Run pip install -U mlx-lm to install MLX for serving community models.</p></li><li><p>Serve the model as an OpenAI compatible API using python -m mlx_lm.server --model mlx-community/Qwen3-Next-80B-A3B-Instruct-8bit. This command both runs the server and downloads the model for you if you haven&#8217;t yet. This particular model is what I&#8217;m using with 128GB of RAM. If you have less RAM, check out smaller models such as mlx-community/Qwen3-4B-Instruct-2507-4bit (8 GB RAM), mlx-community/Qwen2.5-14B-Instruct-4bit (16 GB RAM), mlx-community/Qwen3-Coder-30B-A3B-Instruct-4bit (32 GB RAM), or mlx-community/Qwen3-Next-80B-A3B-Instruct-4bit (64-96 GB RAM).</p></li><li><p>Download <a href="https://github.com/QwenLM/qwen-code">Qwen Code</a>. You might need to install Node Package Manager for this. I recommend using <a href="https://github.com/nvm-sh/nvm">Node Version Manager</a> (nvm) for managing your npm version.</p></li><li><p>Set up your tool to access an OpenAI compatible API by entering the following settings:</p><ol><li><p>Base URL: <a href="http://localhost:8080/v1">http://localhost:8080/v1</a> (should be the default MLX serves your model at)</p></li><li><p>API Key: mlx</p></li><li><p>Model Name: mlx-community/Qwen3-Next-80B-A3B-Instruct-8bit (or whichever model you chose).</p></li></ol></li><li><p>Voila! Your coding model tool should be working with your local coding model.</p></li></ol><p>I recommend opening Activity Monitor on your Mac to monitor memory usage. I&#8217;ve had cases where I thought a model should fit within my memory allotment but it didn&#8217;t and I ended up using a lot of swap memory. When this happens your model will run <strong>very </strong>slowly.</p><p><strong>One tip I have for using local coding models</strong>: Focus on managing your context. This is a great skill even with cloud-based models. People tend to YOLO their chats and fill their context window, but I&#8217;ve found greater performance by ensuring that just what my model needs is sitting in my context window. This is even more important with local models that may need an extra boost in performance and are limited in their context.</p><h2>Was my hypothesis correct?</h2><p>My original hypothesis was: <strong>Instead of paying $100/mo+ for an AI coding subscription, my money would be better spent upgrading my hardware so I can run local coding models at a fraction of the price.</strong></p><p>I would argue that<s>&#8212;yes!&#8212;</s><strong>no </strong>[see edit 2 above], it is correct. If we crunch the numbers, a MacBook with 128 GB is $4700 plus tax. If I spend $100/mo for 5 years, a coding subscription would cost $6000 in that same amount of time. Not only do I save money, but I also get a much more capable machine for anything else I want to do with it.</p><p>[This paragraph was added in after initial release of this article] It&#8217;s important to note that local models will <strong>not</strong> reach the peak performance of frontier models; however, they will likely be able to do most tasks just as well. The value of using a local model doesn&#8217;t come from raw performance, but from supplementing the cost of higher performance models. A local model could very well let you drop your subscription tier for a frontier coding tool or utilize a free tier as needed for better performance and run the rest of your tasks for free.</p><p><strong>It&#8217;s also important to note that local models are only going to get better and smaller</strong>. This is the worst your local coding model will perform. I also wouldn&#8217;t be surprised if cloud-based AI coding tools get more expensive. If you figure you&#8217;re using greater than the $100/mo tier right now or that the $100/mo tier will cost $200/mo in the future, the purchase is a no-brainer. It&#8217;s just difficult to stomach the upfront cost.</p><p>From a performance standpoint, I would say the maximum model running on my 128 GB RAM MacBook right now feels about half a generation behind the frontier coding tools. That&#8217;s excellent, but something to keep in mind as that half a generation might matter to you.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!wAV2!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F922da413-1147-4a89-bbd0-fefdd78bc8cb_1920x1080.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!wAV2!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F922da413-1147-4a89-bbd0-fefdd78bc8cb_1920x1080.jpeg 424w, https://substackcdn.com/image/fetch/$s_!wAV2!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F922da413-1147-4a89-bbd0-fefdd78bc8cb_1920x1080.jpeg 848w, https://substackcdn.com/image/fetch/$s_!wAV2!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F922da413-1147-4a89-bbd0-fefdd78bc8cb_1920x1080.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!wAV2!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F922da413-1147-4a89-bbd0-fefdd78bc8cb_1920x1080.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!wAV2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F922da413-1147-4a89-bbd0-fefdd78bc8cb_1920x1080.jpeg" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/922da413-1147-4a89-bbd0-fefdd78bc8cb_1920x1080.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:162037,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.aiforswes.com/i/182132050?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F922da413-1147-4a89-bbd0-fefdd78bc8cb_1920x1080.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!wAV2!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F922da413-1147-4a89-bbd0-fefdd78bc8cb_1920x1080.jpeg 424w, https://substackcdn.com/image/fetch/$s_!wAV2!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F922da413-1147-4a89-bbd0-fefdd78bc8cb_1920x1080.jpeg 848w, https://substackcdn.com/image/fetch/$s_!wAV2!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F922da413-1147-4a89-bbd0-fefdd78bc8cb_1920x1080.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!wAV2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F922da413-1147-4a89-bbd0-fefdd78bc8cb_1920x1080.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>One wrench thrown into my experiment is how much free quota Google hands out with their different AI coding tools. It&#8217;s easy to purchase expensive hardware when it saves you money in the long run. It&#8217;s much more difficult when the alternative is free.</p><p>Initially, I considered my local coding setup to be a great pair to Google&#8217;s free tier. It definitely performs better than Gemini 2.5 Flash and makes a great companion to Gemini 3 Pro. Gemini 3 Pro can solve more complex tasks with the local model doing everything else. This not only saves quota on 3 Pro but also provides a very capable fallback for when quota is hit.</p><p>However, this is foiled a bit now that <a href="https://blog.google/products/gemini/gemini-3-flash/">Gemini 3 Flash</a> was just announced a few days ago. It shows benchmark numbers much more capable than Gemini 2.5 Flash (and even 2.5 Pro!) and I&#8217;ve been very impressed with its performance. If that&#8217;s the free tier Google offers, it makes local coding models less fiscally reasonable. The jury is still out on how well Gemini 3 Flash will perform and how quota will be structured, but we&#8217;ll have to see if local models can keep up.</p><p>I&#8217;m very curious to hear what you think! Tell me about your local coding setup or ask any questions below.</p><p>Thanks for reading!</p><p><strong>Always be (machine) learning,</strong></p><p><strong>Logan</strong></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.aiforswes.com/p/you-dont-need-to-spend-100mo-on-claude?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.aiforswes.com/p/you-dont-need-to-spend-100mo-on-claude?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share</span></a></p>]]></content:encoded></item><item><title><![CDATA[How to Build Your First Recommendation System (Easy)]]></title><description><![CDATA[A step-by-step guide to training and serving a collaborative filtering model to serve users content]]></description><link>https://www.aiforswes.com/p/collaborative-filtering</link><guid isPermaLink="false">https://www.aiforswes.com/p/collaborative-filtering</guid><dc:creator><![CDATA[Logan Thorneloe]]></dc:creator><pubDate>Tue, 11 Nov 2025 14:03:14 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!UUKR!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff39aac63-172e-4900-9f8d-4d6fa8e7efb5_500x559.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>While generative AI has caused discussions about the impact of AI to skyrocket, I&#8217;d argue recommendation systems are the AI most people should be concerned about. They&#8217;ve been around for over a decade and choose what content people consume, what ideas they see, and even influence <em>how people think</em>.</p><p>Software engineers should understand recommendation systems because <strong>any company serving content to users is using a system similar to this</strong>. Collaborative filtering is simple, intuitive, and very effective.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!UUKR!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff39aac63-172e-4900-9f8d-4d6fa8e7efb5_500x559.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!UUKR!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff39aac63-172e-4900-9f8d-4d6fa8e7efb5_500x559.jpeg 424w, https://substackcdn.com/image/fetch/$s_!UUKR!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff39aac63-172e-4900-9f8d-4d6fa8e7efb5_500x559.jpeg 848w, https://substackcdn.com/image/fetch/$s_!UUKR!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff39aac63-172e-4900-9f8d-4d6fa8e7efb5_500x559.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!UUKR!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff39aac63-172e-4900-9f8d-4d6fa8e7efb5_500x559.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!UUKR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff39aac63-172e-4900-9f8d-4d6fa8e7efb5_500x559.jpeg" width="500" height="559" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f39aac63-172e-4900-9f8d-4d6fa8e7efb5_500x559.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:559,&quot;width&quot;:500,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!UUKR!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff39aac63-172e-4900-9f8d-4d6fa8e7efb5_500x559.jpeg 424w, https://substackcdn.com/image/fetch/$s_!UUKR!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff39aac63-172e-4900-9f8d-4d6fa8e7efb5_500x559.jpeg 848w, https://substackcdn.com/image/fetch/$s_!UUKR!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff39aac63-172e-4900-9f8d-4d6fa8e7efb5_500x559.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!UUKR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff39aac63-172e-4900-9f8d-4d6fa8e7efb5_500x559.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>This article is a follow-on to a previous case study detailing how collaborative filtering is used by Spotify and how a system like this has impacted the music industry. If you haven&#8217;t read that article, <strong>do that first</strong>. It puts everything below into context.</p><div class="digest-post-embed" data-attrs="{&quot;nodeId&quot;:&quot;4ab10b41-0bfd-4d8b-8314-803ce2c98b6b&quot;,&quot;caption&quot;:&quot;This is part one of a series. In this part, I detail how Spotify's recommendation system works and the real-world impact it has (both advertently and inadvertently). In the next part, I will go over how to build a simple recommendation system similar to Spotify's.&quot;,&quot;cta&quot;:&quot;Read full story&quot;,&quot;showBylines&quot;:true,&quot;showDescription&quot;:true,&quot;showImage&quot;:true,&quot;size&quot;:&quot;lg&quot;,&quot;isEditorNode&quot;:true,&quot;title&quot;:&quot;Spotify ML Case Study: AI Has Fundamentally Changed the Music Industry&quot;,&quot;publishedBylines&quot;:[{&quot;id&quot;:43759292,&quot;name&quot;:&quot;Logan Thorneloe&quot;,&quot;bio&quot;:&quot;ML infra, agents, and dev tools at Google helping engineers understand AI.&quot;,&quot;photo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/dcabb40d-0160-46a4-ad4f-55b486a11ee0_1024x1024.jpeg&quot;,&quot;is_guest&quot;:false,&quot;bestseller_tier&quot;:100}],&quot;post_date&quot;:&quot;2025-07-11T13:03:09.415Z&quot;,&quot;cover_image&quot;:&quot;https://lh7-rt.googleusercontent.com/docsz/AD_4nXf71q0JYUlylKJXm64BdZz09ePhWL88ZEDZEu1rD2-gpt46XeneIi47iRZjQhhbLwQu3Mwkk4pJkcs-pM5hMnJGiA1EMIk7glZ1ZiHwUlXOewdQZSh0RHV8ePMVHvVNX1-7of-NEw?key=q_Uug9PL4P-qjvlr9iZzfQ&quot;,&quot;cover_image_alt&quot;:null,&quot;canonical_url&quot;:&quot;https://mlforswes.com/p/spotify-case-study&quot;,&quot;section_name&quot;:null,&quot;video_upload_id&quot;:null,&quot;id&quot;:168025964,&quot;type&quot;:&quot;newsletter&quot;,&quot;reaction_count&quot;:24,&quot;comment_count&quot;:6,&quot;publication_id&quot;:1744179,&quot;publication_name&quot;:&quot;Machine Learning for Software Engineers&quot;,&quot;publication_logo_url&quot;:&quot;https://substackcdn.com/image/fetch/$s_!dRNW!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7a6f2ce2-153f-4600-814b-6344641e3210_500x500.png&quot;,&quot;belowTheFold&quot;:false,&quot;youtube_url&quot;:null,&quot;show_links&quot;:null,&quot;feed_url&quot;:null}"></div><p>When you&#8217;re finished with this article, you will have <strong>trained your own collaborative filtering model</strong> using matrix factorization and be able to <strong>visualize it</strong> in a UI that shows how interacting with content and retraining a model changes recommendations over time.</p><h2>Housekeeping &amp; Things You Should Know</h2><ul><li><p>The complete code for building this collaborative filtering system can be found in the <strong><a href="https://github.com/loganthorneloe/recommendation-system">recommendation systems repo</a></strong>. <strong>Please star it</strong> to support ML for SWEs and stay updated when new tutorials are added. This is the first of many ML system tutorials I&#8217;ll be putting out.</p></li><li><p>Don&#8217;t forget about our <strong><a href="http://mlroadmap.io">Machine Learning Roadmap</a></strong>. It&#8217;s a guide to ML fundamentals that can be completed entirely for free. I spent some time in 2024 curating it and confidently say <strong>it&#8217;s the best free ML roadmap available</strong>.</p></li><li><p><strong>I&#8217;m going to start including this Housekeeping and What You Should Know sections in each article and make each article about something.</strong> I felt the roundups were too shallow and I wasn&#8217;t having fun or learning enough spending my time on them. Instead, each article will have a little roundup section included.</p></li><li><p><strong>ML for SWEs is looking for sponsors!</strong> If you have a job opportunity, developer tool, or want to share anything else that would be beneficial for software engineers working in AI, reach out to me to get it in front of over 10,000 developers. I reserve the right to deny anything I don&#8217;t think is helpful. There&#8217;s a high bar for what I share with my audience to ensure it&#8217;s a good fit for both readers and sponsors.</p></li><li><p>I&#8217;ll be posting more frequent jobs updates/who&#8217;s hiring/the skills you should acquire for paid subscribers in the ML for SWEs Substack chat. Upgrade to paid if you want those.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.aiforswes.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.aiforswes.com/subscribe?"><span>Subscribe now</span></a></p></li></ul><p>Part of Machine Learning for Software Engineers is keeping you abreast of the happenings in AI that are actually important. Here are the most important items since our last article:</p><ul><li><p>OpenAI and Amazon announced a multi-year, <a href="https://openai.com/index/aws-and-openai-partnership">$38B partnership</a> for AWS to provide large-scale compute infrastructure, including NVIDIA GB200s and GB300s.</p></li><li><p>Apple is reportedly nearing a deal to pay Google ~$1B annually to use a <a href="https://www.artificialintelligence-news.com/news/apple-plans-big-siri-update-with-help-from-google-ai/">custom 1.2T parameter Gemini model</a> to power a major Siri update.</p></li><li><p>OpenAI announced it now has <a href="https://openai.com/index/1-million-businesses-putting-ai-to-work">over 1 million paying business customers</a> and 7 million ChatGPT for Work seats.</p></li><li><p>Moonshot AI&#8217;s <a href="https://www.interconnects.ai/p/kimi-k2-thinking-what-it-means">Kimi K2 Thinking</a> is a new 1T parameter Mixture-of-Experts model (32B active) that uses native INT4 inference for a ~2x speedup.</p></li><li><p>NVIDIA reports achieving <a href="https://developer.nvidia.com/blog/how-to-achieve-4x-faster-inference-for-math-problem-solving/">4x faster inference for math problem solving</a> using FP8 quantization and kernel optimizations.</p></li><li><p>Researchers propose <a href="https://www.google.com/search?q=https://www.artificialintelligence-news.com/news/keep-calm-new-model-design-fix-high-enterprise-ai-costs">Continuous Autoregressive Language Models (CALM)</a>, which compress tokens into continuous vectors to cut training FLOPs by 44%.</p></li><li><p>Terminal-Bench 2.0 was released alongside <a href="https://venturebeat.com/ai/terminal-bench-2-0-launches-alongside-harbor-a-new-framework-for-testing">Harbor, a new framework</a> for testing AI agents in containerized developer environments.</p></li><li><p>OpenAI published a post on <a href="https://openai.com/index/prompt-injections">understanding prompt injections</a>, which it calls a frontier security challenge requiring multi-layered defenses.</p></li><li><p>Wikipedia is urging AI companies to stop scraping and <a href="https://techcrunch.com/2025/11/10/wikipedia-urges-ai-companies-to-use-its-paid-api-and-stop-scraping/">use its paid Enterprise API</a> to support the nonprofit&#8217;s servers and mission.</p></li><li><p>A new report details <a href="https://www.technologyreview.com/2025/11/10/1127774/reimagining-cybersecurity-in-the-era-of-ai-and-quantum/">cybersecurity in the era of AI and quantum</a>, highlighting threats from AI-automated attacks and quantum decryption.</p></li><li><p>A Stanford study found that 22-25 year-olds in AI-exposed roles, like software development, experienced a <a href="https://www.ignorance.ai/p/is-a-cs-degree-still-worth-it">13% employment decline</a> since ChatGPT&#8217;s launch. [Credit: <span class="mention-wrap" data-attrs="{&quot;name&quot;:&quot;Charlie Guo&quot;,&quot;id&quot;:3625174,&quot;type&quot;:&quot;user&quot;,&quot;url&quot;:null,&quot;photo_url&quot;:&quot;https://substackcdn.com/image/fetch/$s_!bpse!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7c9d1a4c-3e17-4463-9b75-8898d2565caa_800x800.jpeg&quot;,&quot;uuid&quot;:&quot;eb99dc12-15d0-429d-9cdd-23293fe244cd&quot;}" data-component-name="MentionToDOM"></span>]</p></li><li><p>Platforms like Anthropic&#8217;s Claude Code are pushing a shift toward <a href="https://mlopscommunity.substack.com/p/inside-claude-code-how-anthropic">agentic coding</a>, where developers orchestrate agent fleets rather than coding line-by-line. [Credit: <span class="mention-wrap" data-attrs="{&quot;name&quot;:&quot;MLOps Community&quot;,&quot;id&quot;:179676708,&quot;type&quot;:&quot;user&quot;,&quot;url&quot;:null,&quot;photo_url&quot;:&quot;https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc8b88be-5eab-4a2c-a6a5-430502ebabef_184x184.png&quot;,&quot;uuid&quot;:&quot;b0277a1d-88e5-4af8-97f9-bb108abda14b&quot;}" data-component-name="MentionToDOM"></span>]</p></li><li><p>An article explains how models like Qwen3-Next and Kimi Linear are using <a href="https://magazine.sebastianraschka.com/p/beyond-standard-llms">hybrid attention mechanisms</a> to achieve O(n) scaling for long contexts. [Credit: <span class="mention-wrap" data-attrs="{&quot;name&quot;:&quot;Sebastian Raschka, PhD&quot;,&quot;id&quot;:27393275,&quot;type&quot;:&quot;user&quot;,&quot;url&quot;:null,&quot;photo_url&quot;:&quot;https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F61f4c017-506f-4e9b-a24f-76340dad0309_800x800.jpeg&quot;,&quot;uuid&quot;:&quot;44c55fcc-4bbb-4cea-a462-79ca038060a9&quot;}" data-component-name="MentionToDOM"></span>]</p></li><li><p>OpenAI is offering <a href="https://openai.com/index/chatgpt-for-veterans">a free year of ChatGPT Plus</a> to transitioning U.S. servicemembers and veterans.</p></li></ul><p>If you&#8217;re particularly interested in one of these things and would like a deep dive, leave a comment and I&#8217;ll see what I can do.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.aiforswes.com/p/collaborative-filtering/comments&quot;,&quot;text&quot;:&quot;Leave a comment&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.aiforswes.com/p/collaborative-filtering/comments"><span>Leave a comment</span></a></p><p><strong>Now onto our collaborative filtering system!</strong></p><h2>Step 1: Retrieve the Data</h2><p>For this specific project, we&#8217;ll use the <strong>HetRec 2011 Last.fm 2k dataset</strong> to initially train our model and enable retraining based on simulated interactions between users and artists. HetRec 2011 Last.fm 2k is a great example of implicit feedback which is perfect for a recommendation system. It contains a mapping of listening counts between user and artist IDs, each with a given weight to infer preference (i.e. a higher listen count means a user likes an artist).</p><p>First, create a file named <code>last_fm_loader.py</code>. This will be used to load our dataset and prepare it for training. Include our imports at the top of the file. We&#8217;ll get into how we use each in a later section.</p><pre><code><code>import requests
import zipfile
import io
import pandas as pd
import os</code></code></pre><p>Define a class <code>LastFmLoader</code> to encapsulate all the data-loading logic. In that class, create two variables: One with the url for downloading our dataset and one naming the directory of the folder we&#8217;ll store the dataset in. The <code>__init__</code> function initializes placeholders for our dataframes and defines the file paths we expect to find inside the extracted zip.</p><pre><code><code>class LastFmLoader:

  _ZIP_URL = &#8220;[https://files.grouplens.org/datasets/hetrec2011/hetrec2011-lastfm-2k.zip](https://files.grouplens.org/datasets/hetrec2011/hetrec2011-lastfm-2k.zip)&#8221;
  _DATA_DIR = &#8220;lastfm-2k&#8221;

  def __init__(self):
    self.interactions = None
    self.artists = None
    self._interactions_file = os.path.join(self._DATA_DIR, &#8216;user_artists.dat&#8217;)
    self._artists_file = os.path.join(self._DATA_DIR, &#8216;artists.dat&#8217;)</code></code></pre><p>Add a private method <code>_download_data</code> to this class. This method downloads the zip file from the URL, and extracts its contents into the <code>_DATA_DIR</code> skipping this process if the folder for the data already exists. The print statements are niceties for debugging.</p><pre><code><code>  def _download_data(self):
    
    if os.path.exists(self._DATA_DIR):
      print(f&#8221;Directory {self._DATA_DIR} already exists. Skipping download.&#8221;)
      return
    
    os.makedirs(self._DATA_DIR, exist_ok=True)
    
    print(f&#8221;Downloading data from {self._ZIP_URL}...&#8221;)
    try:
      response = requests.get(self._ZIP_URL)
      response.raise_for_status() 

      print(&#8217;Extracting data...&#8217;)
      with zipfile.ZipFile(io.BytesIO(response.content)) as z:
        z.extractall(self._DATA_DIR)

      print(&#8217;Download and extraction complete.&#8217;)

    except requests.exceptions.RequestException as e:
      print(f&#8221;Error downloading file: {e}&#8221;)
      raise
    except zipfile.BadZipFile as e:
      print(f&#8221;Error extracting file: {e}&#8221;)
      raise
    except Exception as e:
      print(f&#8221;An error occurred during download/extraction: {e}&#8221;)</code></code></pre><p>Add a public <code>load_data</code> method. This is the function we&#8217;ll call from our training script. It runs <code>_download_data</code> to ensure the data is present. Then, it uses <code>pandas.read_csv</code> to load the two files we care about to train our model: <code>user_artists.dat</code> (which contains <code>userID</code>, <code>artistID</code>, <code>weight</code>) and <code>artists.dat</code> (which contains <code>id</code>, <code>name</code>).</p><pre><code><code>  def load_data(self):

    self._download_data() 

    try:
      print(&#8217;Loading interactions data...&#8217;)
      self.interactions = pd.read_csv(
          self._interactions_file, 
          sep=&#8217;&#9;&#8216;, 
          header=0, 
          encoding=&#8217;utf-8&#8217;
      )

      print(&#8217;Loading artists data...&#8217;)
      self.artists = pd.read_csv(
          self._artists_file,
          sep=&#8217;&#9;&#8216;,
          header=0, 
          encoding=&#8217;utf-8&#8217;,
          usecols=[&#8217;id&#8217;, &#8216;name&#8217;] 
      )
      print(&#8217;Data loading complete.&#8217;)
    
    except FileNotFoundError as e:
      print(f&#8221;Error loading data: {e}&#8221;)
      raise
    except Exception as e:
      print(f&#8221;An error occurred during data loading: {e}&#8221;)</code></code></pre><p>Lastly, add a test block at the end of <code>last_fm_loader.py</code>. This block runs a simple test showing the columns present in our data if you execute <code>python last_fm_loader.py</code> directly. We won&#8217;t run our training or serving system from this file, but this is great for testing its functionality.</p><pre><code><code>if __name__ == &#8220;__main__&#8221;:
  loader = LastFmLoader()
  loader.load_data()
  if loader.interactions is not None:
    print(loader.interactions.head())
  
  if loader.artists is not None:
    print(loader.artists.head())</code></code></pre><h2>Step 2: Define the Model</h2><p>Create <code>model.py</code>. This will define our <code>MatrixFactorization</code> class. Start with the imports from <code>torch</code>.</p><pre><code><code>import torch
import torch.nn as nn</code></code></pre><p>Define the <code>MatrixFactorization</code> class, inheriting from <code>torch.nn.Module</code>. The <code>__init__</code> method sets up our learnable parameters. These are the two embedding matrices our model will learn. <code>nn.Embedding</code> is a PyTorch layer that acts as a lookup table. <code>self.user_embedding</code> will store learned user representations and <code>self.artist_embedding</code> will do the same for artists.</p><p><code>embedding_dim</code> is the size we choose for those representations. In <code>__init__</code>, we also define values for our embedding matrices.</p><pre><code><code>class MatrixFactorization(nn.Module):

  def __init__(self, num_users, num_artists, embedding_dim=500):
    super(MatrixFactorization, self).__init__()

    self.user_embedding = nn.Embedding(num_users, embedding_dim)
    self.artist_embedding = nn.Embedding(num_artists, embedding_dim)

    self.user_embedding.weight.data.uniform_(0, 0.05)
    self.artist_embedding.weight.data.uniform_(0, 0.05)</code></code></pre><p>Now, we define the <code>forward</code> method for the class. This is what PyTorch runs when the model is called. It takes a batch of <code>user</code> indices and <code>artist</code> indices, looks up their corresponding embedding vectors, and then computes the dot product between them as described in our overview of collaborative filtering systems. The <code>.sum(dim=1)</code> is how we perform a batched dot product by computing the dot product over a specified dimension. This resulting &#8220;score&#8221; is our model&#8217;s prediction of how much the user likes the artist.</p><pre><code><code>  def forward(self, user, artist):

    user_vector = self.user_embedding(user)
    artist_vector = self.artist_embedding(artist)

    score = (user_vector * artist_vector).sum(dim=1)

    return score</code></code></pre><p>Again, we add a test block at the end of <code>model.py</code>. This is a great way to perform a quick test via <code>python model.py</code> to make sure our model&#8217;s input and output shapes are correct.</p><pre><code><code>if __name__ == &#8220;__main__&#8221;:

  print(&#8221;Testing model.py&#8221;)

  test_num_users = 100
  test_num_artists = 50
  test_emb_size = 10

  model = MatrixFactorization(test_num_users, test_num_artists, test_emb_size)
  print(&#8221;Model created.&#8221;)

  test_user_ids = torch.LongTensor([1, 5, 20, 99])
  test_artist_ids = torch.LongTensor([4, 10, 30, 45])

  predictions = model(test_user_ids, test_artist_ids)
  
  print(f&#8221;\nInput user tensor shape: {test_user_ids.shape}&#8221;)
  print(f&#8221;Input artist tensor shape: {test_artist_ids.shape}&#8221;)
  print(f&#8221;Output predictions shape: {predictions.shape}&#8221;)

  assert predictions.shape == (4,)

  print(&#8221;\nModel test passed!&#8221;)
  print(&#8221;Example predictions (randomly initialized):&#8221;)
  print(predictions)</code></code></pre><h2>Step 3: Train the Model</h2><p>Create your third file, <code>train.py</code>. This script will use the <code>LastFmLoader</code> and <code>MatrixFactorization</code> classes to train and save our model.</p><p>Start with all the necessary imports. Notice that we&#8217;re importing our custom classes here.</p><pre><code><code>import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
import pandas as pd
from sklearn.model_selection import train_test_split
import numpy as np
import os

from last_fm_loader import LastFmLoader
from model import MatrixFactorization</code></code></pre><p>We define a custom <code>LastFmDataset</code> class. <code>DataLoader</code> will use this class to retrieve our training data. The <code>__init__</code> takes our data (as numpy arrays) and stores them as Tensors. The <code>__len__</code> method returns the total number of samples. The <code>__getitem__</code> method returns a single sample (one user, artist, and weight) at a given index. These will be used further down.</p><pre><code><code>class LastFmDataset(Dataset):

  def __init__(self, users, artists, weights):
    self.users = torch.LongTensor(users)
    self.artists = torch.LongTensor(artists)
    self.weights = torch.FloatTensor(weights)

  def __len__(self):
    return len(self.weights)

  def __getitem__(self, idx):
    return self.users[idx], self.artists[idx], self.weights[idx]</code></code></pre><p>When training a model, it&#8217;s possible to <strong>overfit</strong>. This is when the model &#8220;memorizes&#8221; the training data but gets <em>worse</em> at handling new, unseen data. You know you&#8217;re overfitting when your training loss goes down but your validation loss stays higher.</p><p><strong>Early Stopping</strong> is a technique to prevent this. We monitor the validation loss at each epoch. If the loss <em>stops</em> improving for a set number of epochs (our <code>patience</code>), we stop the training, since continuing would only make the model worse.</p><p>We&#8217;ll build this logic into a class. The <code>__init__</code> method sets up our tracking parameters:</p><ul><li><p><code>patience</code>: How many epochs to wait for improvement before stopping.</p></li><li><p><code>delta</code>: A small amount the loss must improve by to be considered an &#8220;improvement&#8221;.</p></li><li><p>The other variables (<code>counter</code>, <code>best_score</code>, etc.) are for internal tracking.</p></li></ul><pre><code><code>class EarlyStopping:

  def __init__(self, patience=5, verbose=False, delta=0, path=&#8217;checkpoint.pt&#8217;):
    self.patience = patience
    self.verbose = verbose
    self.counter = 0
    self.best_score = None
    self.early_stop = False
    self.val_loss_min = np.inf
    self.delta = delta
    self.path = path</code></code></pre><p>The <code>__call__</code> method makes the class instance callable (like a function). We&#8217;ll call it at the end of each epoch, passing in the current <code>val_loss</code>.</p><ul><li><p>It checks if this is the best score it has seen.</p></li><li><p>If not, it increments a <code>counter</code>.</p></li><li><p>If the <code>counter</code> exceeds our <code>patience</code>, it sets the <code>early_stop</code> flag to <code>True</code>.</p></li><li><p>If the score <em>is</em> better, it resets the counter and calls <code>save_checkpoint</code>.</p></li></ul><pre><code><code>  def __call__(self, val_loss, model):

    score = -val_loss
    if self.best_score is None:
      self.best_score = score
      self.save_checkpoint(val_loss, model)
    elif score &lt; self.best_score + self.delta:
      self.counter += 1
      if self.verbose:
        print(f&#8217;EarlyStopping counter: {self.counter} out of {self.patience}&#8217;)
      if self.counter &gt;= self.patience:
        self.early_stop = True
    else:
      self.best_score = score
      self.save_checkpoint(val_loss, model)
      self.counter = 0</code></code></pre><p>The <code>save_checkpoint</code> method is a helper called by <code>__call__</code>. It&#8217;s only triggered when a new best validation loss is found. It saves the model&#8217;s current weights to the specified <code>path</code>. This ensures that when training stops, the file at <code>path</code> contains the weights from the best performing epoch.</p><pre><code><code>  def save_checkpoint(self, val_loss, model):

    if self.verbose:
      print(f&#8217;Validation loss decreased ({self.val_loss_min:.6f} --&gt; {val_loss:.6f}).  Saving model ...&#8217;)
    torch.save(model.state_dict(), self.path)
    self.val_loss_min = val_loss</code></code></pre><p>PyTorch <code>nn.Embedding</code> layers need sequential integer indices. The IDs in our data aren&#8217;t sequential. Thus, we write a helper function to create two dictionaries: one to map from the original ID to sequential indices, and an inverse mapping to go back.</p><pre><code><code>def create_id_mapping(df):

  user_id_mapping = {original_id: i for i, original_id in enumerate(df[&#8217;userID&#8217;].unique())}
  artist_id_mapping = {original_id: i for i, original_id in enumerate(df[&#8217;artistID&#8217;].unique())}

  user_inv_map = {i: original_id for original_id, i in user_id_mapping.items()}
  artist_inv_map = {i: original_id for original_id, i in artist_id_mapping.items()}

  return user_id_mapping, artist_id_mapping, user_inv_map, artist_inv_map</code></code></pre><p>Now we define the main <code>train_model</code> function. This first part sets up hyperparameters, creates the <code>model_store</code> directory, and loads our data using the <code>LastFmLoader</code>. If we were writing a production system, we would run experiments to optimize the hyperparameters. This can be a lengthy process so we&#8217;re sticking with guesses and pushing forward.</p><pre><code><code>def train_model(epochs=20, batch_size=1024, emb_size=50, learning_rate=0.001, model_save_path=&#8221;model_store/model.pt&#8221;):

  os.makedirs(os.path.dirname(model_save_path), exist_ok=True)

  loader = LastFmLoader()
  loader.load_data()
  df = loader.interactions

  if df is None:
    print(&#8221;Failed to load data.&#8221;)
    return</code></code></pre><p>Still inside <code>train_model</code>, we preprocess our data. First, we call <code>create_id_mapping</code> to get our dictionaries. Then, we use the <code>.map()</code> method to replace the original <code>userID</code> and <code>artistID</code> columns with their new sequential indices.</p><pre><code><code>  print(&#8221;Create ID mappings...&#8221;)
  user_id_mapping, artist_id_mapping, user_inv_map, artist_inv_map = create_id_mapping(df)

  df[&#8217;userID&#8217;] = df[&#8217;userID&#8217;].map(user_id_mapping)
  df[&#8217;artistID&#8217;] = df[&#8217;artistID&#8217;].map(artist_id_mapping)</code></code></pre><p>Next, we apply <code>np.log1p</code> to the <code>weight</code> column. This <code>log(1 + x)</code> transform is useful because it scales down massive listen counts, so a user who listened 100,000 times doesn&#8217;t dominate the loss function. We also get the total count of unique users and artists for our model.</p><pre><code><code>  df[&#8217;weight_log&#8217;] = np.log1p(df[&#8217;weight&#8217;])

  num_users = len(user_id_mapping)
  num_artists = len(artist_id_mapping)

  print(f&#8221;Number of users: {num_users}&#8221;)
  print(f&#8221;Number of artists: {num_artists}&#8221;)</code></code></pre><p>We split our data into an 80% training set and a 20% validation set using <code>train_test_split</code>.</p><pre><code><code>  train_df, valid_df = train_test_split(df, test_size=0.2, random_state=42)</code></code></pre><p>We create <code>LastFmDataset</code> instances for both the training and validation dataframes.</p><pre><code><code>  train_dataset = LastFmDataset(train_df[&#8217;userID&#8217;].values, train_df[&#8217;artistID&#8217;].values, train_df[&#8217;weight_log&#8217;].values)
  valid_dataset = LastFmDataset(valid_df[&#8217;userID&#8217;].values, valid_df[&#8217;artistID&#8217;].values, valid_df[&#8217;weight_log&#8217;].values)</code></code></pre><p>Then we wrap our <code>Dataset</code> instances in <code>DataLoader</code>. The <code>DataLoader</code> is a PyTorch utility that handles batching, shuffling, and multi-process data loading for us.</p><pre><code><code>  train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, num_workers=4)
  valid_loader = DataLoader(valid_dataset, batch_size=batch_size, shuffle=False, num_workers=4)</code></code></pre><p>We initialize our <code>MatrixFactorization</code> model with the number of users and artists.</p><pre><code><code>  print(&#8221;Initializing model...&#8221;)
  model = MatrixFactorization(num_users, num_artists, embedding_dim=emb_size)</code></code></pre><p>We check for a GPU (CUDA for NVIDIA, MPS for Apple) and move the model to that device for faster training if available. The <code>.to(device)</code> call moves all of the model&#8217;s parameters (the embedding matrices) onto the GPU&#8217;s memory. The model and data must be on the same device.</p><pre><code><code>  if torch.cuda.is_available():
    device = torch.device(&#8221;cuda&#8221;)
  elif torch.backends.mps.is_available():
      device = torch.device(&#8221;mps&#8221;)
  else:
      device = torch.device(&#8221;cpu&#8221;)
      
  print(f&#8221;Using device: {device}&#8221;)
  model.to(device)</code></code></pre><p>Then, we define our loss function. MSE is a standard loss function for regression that works by calculating the average squared difference between the model&#8217;s prediction and the actual <code>weight_log</code>. It heavily penalizes large errors, which is good for this kind of system.</p><p>For the optimizer, we choose <code>optim.Adam</code> (Adaptive Moment Estimation). Adam is a highly effective and popular optimizer that works well &#8220;out of the box&#8221; for most problems. It combines the benefits of other optimizers by adapting the learning rate for each model parameter individually, which often leads to faster convergence than standard optimizers like SGD.</p><p>We also initialize our <code>EarlyStopping</code> class, telling it to save the best model to <code>model_save_path</code>.</p><pre><code><code>  loss_fn = nn.MSELoss()  
  optimizer = optim.Adam(model.parameters(), lr=learning_rate, weight_decay=1e-5)
  early_stopper = EarlyStopping(patience=3, verbose=True, path=model_save_path)</code></code></pre><p>This is the core of the training. We loop for <code>epochs</code> times. First, we set the model to <code>model.train()</code> mode.</p><pre><code><code>  print(&#8221;Training model...&#8221;)
  for epoch in range(epochs):
    model.train()
    total_train_loss = 0.0</code></code></pre><p>Inside the epoch loop, we loop over every batch in our <code>train_loader</code>. For each batch, we move the data to our <code>device</code>. This is the second half of the device equation: the model lives on the GPU, so every batch of data we feed it must <em>also</em> be moved to the GPU. This <code>user.to(device)</code>, <code>artist.to(device)</code>, etc. call does that.</p><pre><code><code>    for user, artist, weight in train_loader:
      user, artist, weight = user.to(device), artist.to(device), weight.to(device)</code></code></pre><p>We then get into a standard 5-step PyTorch training process for a batch:</p><ol><li><p><code>optimizer.zero_grad()</code>: Clear old gradients.</p></li><li><p><code>prediction = model(...)</code>: Get the model&#8217;s prediction.</p></li><li><p><code>loss = loss_fn(...)</code>: Calculate the loss.</p></li><li><p><code>loss.backward()</code>: Compute new gradients.</p></li><li><p><code>optimizer.step()</code>: Update the model&#8217;s weights.</p></li></ol><p>We also compute the model&#8217;s total training loss as we go along.</p><pre><code><code>      optimizer.zero_grad()
      prediction = model(user, artist)
      loss = loss_fn(prediction, weight)
      loss.backward()
      optimizer.step()
      total_train_loss += loss.item()</code></code></pre><p>After training on all batches, we switch to <code>model.eval()</code> mode and use <code>with torch.no_grad()</code> to turn off gradient calculations for validation.</p><pre><code><code>    model.eval()
    total_val_loss = 0.0
    with torch.no_grad():</code></code></pre><p>We loop over the <code>valid_loader</code> to get the predictions and calculate the total validation loss.</p><pre><code><code>      for users, artists, weights in valid_loader:
        users, artists, weights = users.to(device), artists.to(device), weights.to(device)
        predictions = model(users, artists)
        val_loss = loss_fn(predictions, weights)
        total_val_loss += val_loss.item()</code></code></pre><p>At the end of each epoch, we calculate and print the average training and validation losses.</p><pre><code><code>    avg_train_loss = total_train_loss / len(train_loader)
    avg_val_loss = total_val_loss / len(valid_loader)

    print(f&#8221;Epoch {epoch+1}/{epochs} - Train Loss: {avg_train_loss:.4f} - Val Loss: {avg_val_loss:.4f}&#8221;)</code></code></pre><p>Finally, we call our <code>early_stopper</code> with the validation loss. It will run its internal logic and if the <code>early_stop</code> flag has been set to <code>True</code>, we break the training loop.</p><pre><code><code>    early_stopper(avg_val_loss, model)
    if early_stopper.early_stop:
      print(&#8221;Early stopping triggered.&#8221;)
      break</code></code></pre><p>After the loop, the <code>model_save_path</code> will hold the best version of our model, thanks to our <code>EarlyStopping</code> class. We also <em>must</em> save our ID mappings. Without them, we have no way to connect <code>userID 1002</code> to <code>user_index 5</code>.</p><pre><code><code>  print(f&#8221;\nTraining complete. Best model saved to {model_save_path}&#8221;)

  mapping_path = &#8220;model_store/mappings.pth&#8221;
  torch.save({
      &#8216;user_id_mapping&#8217;: user_id_mapping,
      &#8216;artist_id_mapping&#8217;: artist_id_mapping,
      &#8216;user_inv_map&#8217;: user_inv_map,
      &#8216;artist_inv_map&#8217;: artist_inv_map
  }, mapping_path)

  print(f&#8221;Mappings saved to {mapping_path}&#8221;)</code></code></pre><p>Finally, add the <code>if __name__ == &#8220;__main__&#8221;:</code> block to <code>train.py</code> so we can run it as a script using <code>python train.py</code>.</p><pre><code><code>if __name__ == &#8220;__main__&#8221;:
  train_model()</code></code></pre><p>You should now be able to run the full training loop. It will download the data, train the model, and save <code>model.pt</code> and <code>mappings.pth</code> in the <code>model_store</code> directory.</p><h2>Step 4: Serve the Recommendations</h2><p>Create the final file, <code>app.py</code>. We&#8217;ll use Streamlit to build a simple web UI.</p><p>Import Streamlit, PyTorch, pandas, numpy, and our custom classes. We also import <code>LastFmDataset</code> because we&#8217;ll need it for retraining. We also define constants for our saved paths.</p><pre><code><code>import streamlit as st
import torch
import pandas as pd
import os
import numpy as np
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader

from model import MatrixFactorization
from last_fm_loader import LastFmLoader
from train import LastFmDataset

MODEL_PATH = os.path.join(&#8221;model_store&#8221;, &#8220;model.pt&#8221;)
MAPPINGS_PATH = os.path.join(&#8221;model_store&#8221;, &#8220;mappings.pth&#8221;)
SIMULATIONS = 5000</code></code></pre><p>Create a function <code>load_assets</code> to load our model, mappings, and artist data. We use Streamlit&#8217;s <code>@st.cache_resource</code> decorator. This tells Streamlit to run this function <em>once</em> and cache the result, so our app is fast and doesn&#8217;t reload the model on every interaction.</p><p>Inside <code>load_assets</code>, we load the mappings. <strong>Note:</strong> <code>torch.load(MAPPINGS_PATH, weights_only=False)</code> is important. PyTorch&#8217;s security features default to <code>weights_only=True</code>, but our mappings file is a dictionary, not model weights.</p><pre><code><code>@st.cache_resource
def load_assets():

  try:
    mappings = torch.load(MAPPINGS_PATH, weights_only=False)
    user_map = mappings[&#8217;user_id_mapping&#8217;]
    artist_map = mappings[&#8217;artist_id_mapping&#8217;]
    
    num_users = len(user_map)
    num_artists = len(artist_map)</code></code></pre><p>Now, we initialize a new <code>MatrixFactorization</code> model instance and load the saved weights from our <code>model.pt</code> file.</p><pre><code><code>    model = MatrixFactorization(num_users, num_artists, embedding_dim=50)
    model.load_state_dict(torch.load(MODEL_PATH))
    model.eval()
</code></code></pre><p>Finally, we load the artist names using our <code>LastFmLoader</code> so we can display them later, and we return all the loaded assets.</p><pre><code><code>    loader = LastFmLoader()
    loader.load_data()
    artists_df = loader.artists.set_index(&#8217;id&#8217;)

    return model, mappings, artists_df
  
  except FileNotFoundError as e:
    print(f&#8221;Error loading assets: {e}&#8221;)
    st.stop()
  except Exception as e:
    print(f&#8221;An error occurred during asset loading: {e}&#8221;)
    st.stop()</code></code></pre><p>Now we create the get_recommendations function. This is the core of our app&#8217;s logic. We use <code>@st.cache_data</code> to cache the results for a given user.</p><p>It maps the <code>selected_user_id</code> to its <code>user_idx</code>, then gets the <code>user_vector</code> from the model&#8217;s embedding layer <em>for the current user</em> and <em>all</em> artist vectors from the artist embedding layer.</p><pre><code><code>@st.cache_data(show_spinner=&#8221;Generating recommendations...&#8221;)
def get_recommendations(selected_user_id, _model, _mappings, _artists_df, num_recs=10):
  user_idx = _mappings[&#8217;user_id_mapping&#8217;][selected_user_id]
  if user_idx is None:
    st.error(f&#8221;User ID {selected_user_id} not found in the mapping.&#8221;)
    return pd.DataFrame(columns=[&#8217;Artist&#8217;, &#8216;Predicted Score&#8217;])
  
  user_tensor = torch.LongTensor([user_idx])
  user_vector = _model.user_embedding(user_tensor)

  all_artist_vectors = _model.artist_embedding.weight
</code></code></pre><p>We perform a single matrix multiplication between the one user vector and the entire matrix of artist vectors. This gets us all our predictions for a given user at once.</p><pre><code><code>  with torch.no_grad():
    scores = torch.matmul(user_vector, all_artist_vectors.T). squeeze()</code></code></pre><p>We sort the scores using <code>torch.argsort</code> to get the top N, then loop over them, mapping the <code>artist_model_idx</code> back to the original artist ID and then to the artist&#8217;s name.</p><pre><code><code>  top_indices = torch.argsort(scores, descending=True)[:num_recs]

  rec_data = []
  for idx in top_indices:
    artist_model_idx = idx.item()
    original_artist_id = _mappings[&#8217;artist_inv_map&#8217;].get(artist_model_idx)
    if original_artist_id:
      artist_name = _artists_df.loc[original_artist_id, &#8216;name&#8217;]
      rec_data.append((artist_name, scores[idx].item()))
  
  return pd.DataFrame(rec_data, columns=[&#8217;Artist&#8217;, &#8216;Score&#8217;])</code></code></pre><p>To make the visual we want to really understand collaborative filtering, we&#8217;ll add functions to simulate new data and retrain the model live. <code>simulate_new_listen</code> creates a new dataframe of random user-artist interactions.</p><pre><code><code>def simulate_new_listen(_mappings, num_simulations=100):
  st.write(f&#8221;Simulating {num_simulations} new listens...&#8221;)
  all_user_indices = list(_mappings[&#8217;user_inv_map&#8217;].keys())
  all_artist_indices = list(_mappings[&#8217;artist_inv_map&#8217;].keys())

  sim_users = np.random.choice(all_user_indices, num_simulations)
  sim_artists = np.random.choice(all_artist_indices, num_simulations)
                                 
  sim_weights = np.random.randint(50, 500, num_simulations)

  sim_df = pd.DataFrame({
      &#8216;user_idx&#8217;: sim_users,
      &#8216;artist_idx&#8217;: sim_artists,
      &#8216;weight&#8217;: sim_weights
  })

  return sim_df</code></code></pre><p>Now we create the <code>retrain_model</code> function. It first checks Streamlit&#8217;s <code>st.session_state</code> to see if a <code>&#8216;retrained_model&#8217;</code> already exists. If it does, we use that one. If not, then this is the first retraining so we start from the original <code>load_assets()</code> model. This ensures that clicking the button multiple times keeps improving the same &#8220;live&#8221; model.</p><pre><code><code>def retrain_model(new_data):  
  st.sidebar.write(&#8221;Retraining model...&#8221;)

  if &#8216;retrained_model&#8217; in st.session_state:
    model_to_retrain = st.session_state.retrained_model
    st.sidebar.write(&#8221;Starting from *previously* retrained model.&#8221;)
  else:
    model, _, _ = load_assets()
    model_to_retrain = model
    st.sidebar.write(&#8221;Starting from *original* loaded model.&#8221;)</code></code></pre><p>Next, we prepare the new data. Just like in <code>train.py</code>, we apply the <code>log1p</code> transform and load the data into a <code>LastFmDataset</code> and a <code>DataLoader</code>.</p><pre><code><code>  new_data[&#8217;weight_log&#8217;] = np.log1p(new_data[&#8217;weight&#8217;])
  new_dataset = LastFmDataset(new_data[&#8217;user_idx&#8217;].values, new_data[&#8217;artist_idx&#8217;].values, new_data[&#8217;weight_log&#8217;].values)
  new_loader = DataLoader(new_dataset, batch_size=32, shuffle=True)</code></code></pre><p>We also need to define our optimizer and loss function again, pointing them at the <code>model_to_retrain</code>&#8216;s parameters.</p><pre><code><code>  optimizer = optim.Adam(model_to_retrain.parameters(), lr=0.001)
  loss_fn = nn.MSELoss()</code></code></pre><p>We run a smaller training loop, just on the new data. We set the model to <code>train()</code> mode and loop over our <code>new_loader</code>, applying the same 5-step PyTorch training process as before.</p><pre><code><code>  model_to_retrain.train()

  for users, artists, weights, in new_loader:
    optimizer.zero_grad()
    predictions = model_to_retrain(users, artists)
    loss = loss_fn(predictions, weights)
    loss.backward()
    optimizer.step()
</code></code></pre><p>Finally, we set the model back to <code>eval()</code> mode and save the updated model back into <code>st.session_state[&#8217;retrained_model&#8217;]</code>. This replaces the old &#8220;live&#8221; model with the new, retrained one with the more up to date weights.</p><pre><code><code>  model_to_retrain.eval()
  st.session_state.retrained_model = model_to_retrain
  st.sidebar.success(&#8221;Retraining complete!&#8221;)</code></code></pre><p>Now we create a simple app to visualize all the calculations that are happen. We&#8217;re using Streamlit to keep things simple and build it entirely in Python.</p><p>First, the UI loads our assets. Then it checks <code>st.session_state</code> to see if a retrained model exists. If so, we use it; otherwise, we use the original model we loaded.</p><pre><code><code>st.set_page_config(page_title=&#8221;Music Recommender&#8221;, layout=&#8221;wide&#8221;)
st.title(&#8221;Interactive Music Recommender&#8221;)

model, mappings, artists_df = load_assets()

if &#8216;retrained_model&#8217; in st.session_state:
  model_to_use = st.session_state.retrained_model
else:
  model_to_use = model</code></code></pre><p>We create a <code>st.selectbox</code> dropdown for the user to pick a user ID.</p><pre><code><code>original_user_ids = list(mappings[&#8217;user_inv_map&#8217;].values())
st.subheader(&#8221;Select a user to see their recommendations:&#8221;)
selected_user_id = st.selectbox(&#8221;Select a user&#8221;, original_user_ids)</code></code></pre><p>If a user is selected, we call <code>get_recommendations</code> and display the results in a <code>st.table</code>.</p><pre><code><code>if selected_user_id:
  st.write(f&#8221;Top Recommendations for user: **{selected_user_id}**&#8221;)
  recs_df = get_recommendations(selected_user_id, model_to_use, mappings, artists_df)
  st.table(recs_df.set_index(&#8217;Artist&#8217;))</code></code></pre><p>Finally, we add a sidebar with a button that, when clicked, runs the simulation and retraining. It then clears the recommendation cache and calls <code>st.rerun()</code> to refresh the app and show the new recommendations.</p><pre><code><code>st.sidebar.title(&#8221;Retraining Simulation&#8221;)
st.sidebar.write(&#8221;Simulate new user activity and retrain.&#8221;)

if st.sidebar.button(f&#8221;Simulate {SIMULATIONS} listens and retrain&#8221;):

  new_data = simulate_new_listen(mappings, num_simulations=SIMULATIONS)
  retrain_model(new_data)

  get_recommendations.clear()
  st.rerun()</code></code></pre><p>And that&#8217;s it! You&#8217;ve built a complete, end-to-end recommendation system with four files.</p><p>To see it in action, run the following command in your terminal:</p><pre><code><code>streamlit run app.py</code></code></pre><p>You&#8217;ll now be able to select any user, see their initial recommendations, and use the sidebar to simulate new data and retrain the model live to watch how its predictions change over time.</p><p><strong>Always be (machine) learning,</strong></p><p><strong>Logan</strong></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.aiforswes.com/p/collaborative-filtering?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.aiforswes.com/p/collaborative-filtering?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share</span></a></p>]]></content:encoded></item><item><title><![CDATA[Spotify ML Case Study: AI Has Fundamentally Changed the Music Industry]]></title><description><![CDATA[A case study of Spotify's algorithm, including how it works and the impact it has]]></description><link>https://www.aiforswes.com/p/spotify-case-study</link><guid isPermaLink="false">https://www.aiforswes.com/p/spotify-case-study</guid><dc:creator><![CDATA[Logan Thorneloe]]></dc:creator><pubDate>Fri, 11 Jul 2025 13:03:09 GMT</pubDate><enclosure url="https://lh7-rt.googleusercontent.com/docsz/AD_4nXf71q0JYUlylKJXm64BdZz09ePhWL88ZEDZEu1rD2-gpt46XeneIi47iRZjQhhbLwQu3Mwkk4pJkcs-pM5hMnJGiA1EMIk7glZ1ZiHwUlXOewdQZSh0RHV8ePMVHvVNX1-7of-NEw?key=q_Uug9PL4P-qjvlr9iZzfQ" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>This is part one of a series. In this part, I detail how Spotify's recommendation system works and the real-world impact it has (both advertently and inadvertently). In the next part, I will go over how to build a simple recommendation system similar to Spotify's.</em></p><p>I'm certain you've heard the phrase: "Music is terrible these days." This was likely from someone who grew up in the 1980s or earlier remarking about the style of music the 'youngins' listen to and what's been playing on the radio recently. Most of us roll our eyes because every generation seems to think the next generation's music is garbage, but the truth is that music <em>has</em> changed <strong>drastically</strong> over the past decade.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!RxYT!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22b72a17-b898-47dc-a325-607d03728aee_700x449.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!RxYT!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22b72a17-b898-47dc-a325-607d03728aee_700x449.png 424w, https://substackcdn.com/image/fetch/$s_!RxYT!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22b72a17-b898-47dc-a325-607d03728aee_700x449.png 848w, https://substackcdn.com/image/fetch/$s_!RxYT!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22b72a17-b898-47dc-a325-607d03728aee_700x449.png 1272w, https://substackcdn.com/image/fetch/$s_!RxYT!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22b72a17-b898-47dc-a325-607d03728aee_700x449.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!RxYT!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22b72a17-b898-47dc-a325-607d03728aee_700x449.png" width="700" height="449" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/22b72a17-b898-47dc-a325-607d03728aee_700x449.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:449,&quot;width&quot;:700,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!RxYT!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22b72a17-b898-47dc-a325-607d03728aee_700x449.png 424w, https://substackcdn.com/image/fetch/$s_!RxYT!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22b72a17-b898-47dc-a325-607d03728aee_700x449.png 848w, https://substackcdn.com/image/fetch/$s_!RxYT!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22b72a17-b898-47dc-a325-607d03728aee_700x449.png 1272w, https://substackcdn.com/image/fetch/$s_!RxYT!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22b72a17-b898-47dc-a325-607d03728aee_700x449.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Generative AI has caused more people to be conscious of how AI impacts everyday life. This is easy to notice when a person frequently has to determine if images and videos are real or fake. This is much more difficult to notice when AI is being used to feed you recommendations instead of generating the content itself. I would argue this can be even more impactful <em>because of how difficult it is to notice.</em></p><p>To understand this, we're going to look at Spotify's music recommendation algorithm. We'll walk through it from the problem statement (what Spotify is trying to accomplish) through to the algorithms they use to accomplish that goal and all the way to the impact their methodology has on their users and the industry.</p><p>Spotify has made much more music available to many more people. The purpose of sharing this is to walk through the considerations that go into making a machine learning system, many of which go beyond choosing a model and building software.</p><p>All machine learning engineers need to understand the tradeoffs that come with approaching problems using machine learning. Machine learning is fundamentally an optimization problem, and optimizing for specific metrics always has trade-offs.</p><p>In this case study, we're going to:</p><ol><li><p>Start from Spotify's problem. What are they trying to solve?</p></li><li><p>Identify how Spotify is solving that problem.</p></li><li><p>Understand the side effects their approach has.</p></li><li><p>Realize the impact those side effects have on users and the music industry as a whole.</p></li></ol><p>My goal is to help you not only understand Spotify's systems, but also have a better understanding of why case studies like this are important to understanding impact.</p><h2><strong>The Problem to Solve</strong></h2><p>Like all companies, Spotify is trying to be profitable. As a music streaming service, they need to increase subscriptions. They do this by optimizing the user experience for long-term listener satisfaction, creating a <a href="https://www.spotify.com/us/safetyandprivacy/understanding-recommendations">personalized experience the user enjoys so they'll continue to be a subscriber</a>.</p><p>Spotify needs to connect users with music they already enjoy <strong>and</strong> facilitate them finding new music they also enjoy. To do this, Spotify needs to create a recommendation system that feeds users the music they want. This guide will focus on how that recommendation system works and what it means for users.</p><p>This guide won't focus on other, more complex problems Spotify also has to solve, such as:</p><ul><li><p><strong>Spotify's relationship with musical artists</strong>: Spotify needs to create a reason for musical artists to include their songs on the platform so users have the songs they want, but they need to do so without compromising the user experience.</p></li><li><p><strong>How recommendations work in different mediums</strong>: Spotify has to recommend music on the home page and as a continuity to the user's current listening. There are considerations for how each differs, but that's out of scope for this article.</p></li><li><p><strong>Other problems</strong> such as explicit content filtering, spam identification, anything having to do with ads (recommendations, filtering, etc.), and more.</p></li></ul><p>I&#8217;ll also be including technical details of Spotify&#8217;s primary recommendation algorithm, but I don&#8217;t have the space, time, or knowledge (not all information is made publicly available!) to include technical details about everything.</p><p>Let's get into it.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!1wUQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F748ce9e2-da29-4bb5-a405-64b67713d98d_1280x720.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!1wUQ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F748ce9e2-da29-4bb5-a405-64b67713d98d_1280x720.png 424w, https://substackcdn.com/image/fetch/$s_!1wUQ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F748ce9e2-da29-4bb5-a405-64b67713d98d_1280x720.png 848w, https://substackcdn.com/image/fetch/$s_!1wUQ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F748ce9e2-da29-4bb5-a405-64b67713d98d_1280x720.png 1272w, https://substackcdn.com/image/fetch/$s_!1wUQ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F748ce9e2-da29-4bb5-a405-64b67713d98d_1280x720.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!1wUQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F748ce9e2-da29-4bb5-a405-64b67713d98d_1280x720.png" width="1280" height="720" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/748ce9e2-da29-4bb5-a405-64b67713d98d_1280x720.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:720,&quot;width&quot;:1280,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:61233,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://mlforswes.com/i/168025964?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F748ce9e2-da29-4bb5-a405-64b67713d98d_1280x720.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!1wUQ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F748ce9e2-da29-4bb5-a405-64b67713d98d_1280x720.png 424w, https://substackcdn.com/image/fetch/$s_!1wUQ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F748ce9e2-da29-4bb5-a405-64b67713d98d_1280x720.png 848w, https://substackcdn.com/image/fetch/$s_!1wUQ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F748ce9e2-da29-4bb5-a405-64b67713d98d_1280x720.png 1272w, https://substackcdn.com/image/fetch/$s_!1wUQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F748ce9e2-da29-4bb5-a405-64b67713d98d_1280x720.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><strong>The Data</strong></p><p>All machine learning problems start with the data. Spotify uses four categories:&nbsp;</p><ul><li><p><a href="https://attractgroup.com/blog/how-spotify-algorithm-works-for-music-recommendation/">Implicit user feedback like songs played, skips, and saves</a></p></li><li><p><a href="https://www.spotify.com/us/safetyandprivacy/understanding-recommendations/">Explicit signals like demographics and follows</a></p></li><li><p><a href="https://attractgroup.com/blog/how-spotify-algorithm-works-for-music-recommendation/">Audio analysis of the tracks themselves</a></p></li><li><p><a href="https://attractgroup.com/blog/how-spotify-algorithm-works-for-music-recommendation/">Textual data from across the web to understand cultural context</a>.</p></li></ul><p>All of these data points are events that are used to understand and improve the listener's experience via machine learning. Spotify processes an estimated <a href="https://omnisearch.ai/blog/spotify-unwrapped">half a trillion events daily</a>. They need a system that can keep up with this volume.</p><h2><strong>The System</strong></h2><p>Spotify's main goal is to increase user retention by making excellent recommendations at scale. This goal comes with a number of problems that Spotify needs to solve, and we'll walk through the systems Spotify is using to solve them problem-by-problem.</p><p>This system started simply and evolved over time as Spotify's user base evolved and a more complex system was required to meet user needs. The following will give you a great understanding of how Spotify's system works based on publicly available sources and provide enough information to better understand the impact those systems have had on the music industry.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!iQvJ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd9d76784-1ee8-4fa8-b405-e5698e636528_1280x720.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!iQvJ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd9d76784-1ee8-4fa8-b405-e5698e636528_1280x720.png 424w, https://substackcdn.com/image/fetch/$s_!iQvJ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd9d76784-1ee8-4fa8-b405-e5698e636528_1280x720.png 848w, https://substackcdn.com/image/fetch/$s_!iQvJ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd9d76784-1ee8-4fa8-b405-e5698e636528_1280x720.png 1272w, https://substackcdn.com/image/fetch/$s_!iQvJ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd9d76784-1ee8-4fa8-b405-e5698e636528_1280x720.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!iQvJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd9d76784-1ee8-4fa8-b405-e5698e636528_1280x720.png" width="1280" height="720" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d9d76784-1ee8-4fa8-b405-e5698e636528_1280x720.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:720,&quot;width&quot;:1280,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:86713,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://mlforswes.com/i/168025964?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd9d76784-1ee8-4fa8-b405-e5698e636528_1280x720.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!iQvJ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd9d76784-1ee8-4fa8-b405-e5698e636528_1280x720.png 424w, https://substackcdn.com/image/fetch/$s_!iQvJ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd9d76784-1ee8-4fa8-b405-e5698e636528_1280x720.png 848w, https://substackcdn.com/image/fetch/$s_!iQvJ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd9d76784-1ee8-4fa8-b405-e5698e636528_1280x720.png 1272w, https://substackcdn.com/image/fetch/$s_!iQvJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd9d76784-1ee8-4fa8-b405-e5698e636528_1280x720.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3><strong>Collaborative Filtering: The Foundation Behind Recommendations</strong></h3><p><em>If you pay attention to one section of this article, make it this one. This is the section that part 2 of this article will focus on.</em></p><p>To increase user retention, the first problem Spotify needed to solve was how to recommend songs to users to keep them listening. This means using data about what a user has already listened to in order to accurately recommend their next song.</p><p>The solution Spotify used for this is called <a href="http://yifanhu.net/PUB/cf.pdf">Collaborative Filtering with Implicit Feedback</a>. Using implicit feedback is more practical for both Spotify and the listener because it doesn't require any explicit work on the listener's part (like rating songs). Instead, it uses data the user provides just by using the platform to train a machine learning model.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!GUmF!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba1bf909-b3a7-4851-b01c-8f8fe6df539d_1280x720.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!GUmF!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba1bf909-b3a7-4851-b01c-8f8fe6df539d_1280x720.png 424w, https://substackcdn.com/image/fetch/$s_!GUmF!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba1bf909-b3a7-4851-b01c-8f8fe6df539d_1280x720.png 848w, https://substackcdn.com/image/fetch/$s_!GUmF!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba1bf909-b3a7-4851-b01c-8f8fe6df539d_1280x720.png 1272w, https://substackcdn.com/image/fetch/$s_!GUmF!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba1bf909-b3a7-4851-b01c-8f8fe6df539d_1280x720.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!GUmF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba1bf909-b3a7-4851-b01c-8f8fe6df539d_1280x720.png" width="1280" height="720" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ba1bf909-b3a7-4851-b01c-8f8fe6df539d_1280x720.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:720,&quot;width&quot;:1280,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:69887,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://mlforswes.com/i/168025964?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba1bf909-b3a7-4851-b01c-8f8fe6df539d_1280x720.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!GUmF!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba1bf909-b3a7-4851-b01c-8f8fe6df539d_1280x720.png 424w, https://substackcdn.com/image/fetch/$s_!GUmF!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba1bf909-b3a7-4851-b01c-8f8fe6df539d_1280x720.png 848w, https://substackcdn.com/image/fetch/$s_!GUmF!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba1bf909-b3a7-4851-b01c-8f8fe6df539d_1280x720.png 1272w, https://substackcdn.com/image/fetch/$s_!GUmF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba1bf909-b3a7-4851-b01c-8f8fe6df539d_1280x720.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h4><strong>The Original Problem: A Massive, Mostly Empty Matrix</strong></h4><p>Put simply, imagine a giant table where every row is a user and every column is a song. Each cell contains how many times that user listened to that song (using a <strong>preference-confidence framework</strong> where listening once = low confidence, repeated listening = high confidence).</p><p>This matrix is <em>massive</em>. One row for each user and one column for each song means a matrix with billions (or more!) entries. The problem with this matrix is that it's <strong>sparse</strong>, or mostly empty. Since most users and songs won&#8217;t have any interaction data, the majority of the matrix won&#8217;t have any meaningful values.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!u0r9!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e24385a-25ba-4fc8-b735-088235824bd0_508x491.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!u0r9!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e24385a-25ba-4fc8-b735-088235824bd0_508x491.png 424w, https://substackcdn.com/image/fetch/$s_!u0r9!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e24385a-25ba-4fc8-b735-088235824bd0_508x491.png 848w, https://substackcdn.com/image/fetch/$s_!u0r9!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e24385a-25ba-4fc8-b735-088235824bd0_508x491.png 1272w, https://substackcdn.com/image/fetch/$s_!u0r9!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e24385a-25ba-4fc8-b735-088235824bd0_508x491.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!u0r9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e24385a-25ba-4fc8-b735-088235824bd0_508x491.png" width="508" height="491" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7e24385a-25ba-4fc8-b735-088235824bd0_508x491.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:491,&quot;width&quot;:508,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!u0r9!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e24385a-25ba-4fc8-b735-088235824bd0_508x491.png 424w, https://substackcdn.com/image/fetch/$s_!u0r9!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e24385a-25ba-4fc8-b735-088235824bd0_508x491.png 848w, https://substackcdn.com/image/fetch/$s_!u0r9!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e24385a-25ba-4fc8-b735-088235824bd0_508x491.png 1272w, https://substackcdn.com/image/fetch/$s_!u0r9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e24385a-25ba-4fc8-b735-088235824bd0_508x491.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Using the sparse matrix for predictions has three primary holdups:</p><ol><li><p><strong>Any computations done on this matrix would be computationally expensive</strong>. With billions of entries, calculations get expensive and slow very quickly.</p></li><li><p><strong>Predictions are difficult</strong>. We want to find music and recommend music a user hasn&#8217;t listened to.The sparse nature of this matrix means it doesn&#8217;t help at all with this.</p></li><li><p><strong>Adding new songs and users is impossible</strong>. New columns and rows will contain all zeroes meaning we have zero meaningful information to use for predictions.</p></li></ol><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!sqrb!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffddaa6de-629e-4998-91a3-9422a1ed1c21_1280x720.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!sqrb!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffddaa6de-629e-4998-91a3-9422a1ed1c21_1280x720.png 424w, https://substackcdn.com/image/fetch/$s_!sqrb!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffddaa6de-629e-4998-91a3-9422a1ed1c21_1280x720.png 848w, https://substackcdn.com/image/fetch/$s_!sqrb!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffddaa6de-629e-4998-91a3-9422a1ed1c21_1280x720.png 1272w, https://substackcdn.com/image/fetch/$s_!sqrb!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffddaa6de-629e-4998-91a3-9422a1ed1c21_1280x720.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!sqrb!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffddaa6de-629e-4998-91a3-9422a1ed1c21_1280x720.png" width="1280" height="720" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/fddaa6de-629e-4998-91a3-9422a1ed1c21_1280x720.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:720,&quot;width&quot;:1280,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:64771,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://mlforswes.com/i/168025964?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffddaa6de-629e-4998-91a3-9422a1ed1c21_1280x720.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!sqrb!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffddaa6de-629e-4998-91a3-9422a1ed1c21_1280x720.png 424w, https://substackcdn.com/image/fetch/$s_!sqrb!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffddaa6de-629e-4998-91a3-9422a1ed1c21_1280x720.png 848w, https://substackcdn.com/image/fetch/$s_!sqrb!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffddaa6de-629e-4998-91a3-9422a1ed1c21_1280x720.png 1272w, https://substackcdn.com/image/fetch/$s_!sqrb!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffddaa6de-629e-4998-91a3-9422a1ed1c21_1280x720.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h4><strong>The Solution: Matrix Factorization</strong></h4><p>Matrix factorization solves this by breaking our giant sparse matrix into two much smaller, <strong>dense matrices</strong> (meaning they're filled with actual numbers, not zeroes):</p><ol><li><p>A <strong>User Matrix</strong>: Each user gets a row of hidden features (like "how much does this user like rock music?" or "does this user prefer upbeat songs?")</p></li><li><p>A <strong>Song Matrix</strong>: Each song gets a column of the same hidden features (like "how rock-influenced is this song?" or "how upbeat is this song?")</p></li></ol><p>When you multiply these two smaller matrices together, you get back a complete version of the original matrix&#8212;but now <strong>every cell has a predicted value</strong>, even for user-song combinations that never happened.</p><p>The exact features Spotify uses in these matrices is unknown, but the beauty of this approach is that Spotify can add and remove features as necessary since the algorithm learns their relevancy for itself.</p><h4><strong>Why These Smaller Matrices Are So Much Better</strong></h4><p>The magic is in those hidden features. The algorithm automatically learns meaningful patterns like:</p><ul><li><p>User 1 has high values for "rock" and "energetic" features</p></li><li><p>Song A also has high values for "rock" and "energetic" features</p></li><li><p>Therefore, User 1 will probably like Song A (even if they've never heard it)</p></li></ul><p>This is why matrix factorization works for recommendations: it discovers hidden connections between users and songs that aren't obvious from the raw listening data.</p><p><strong>Fun fact</strong>: Factorized matrices separated into songs and users with different features for each column make it easier to add items (songs or users) to the matrices with meaningful values. Instead of randomly initializing values because implicit information isn&#8217;t known, explicit information can be used to estimate starting feature values.</p><p>I haven&#8217;t found any sources explicitly stating Spotify is doing this or how they&#8217;re doing this (they seem to solve the cold start problem differently&#8212;see <strong>&#8216;The Cold Start&#8217;</strong> section below), but this is a benefit of the dense matrices worth mentioning.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!rp3_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F783eb531-7559-4396-8990-35da9179a147_1280x720.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!rp3_!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F783eb531-7559-4396-8990-35da9179a147_1280x720.png 424w, https://substackcdn.com/image/fetch/$s_!rp3_!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F783eb531-7559-4396-8990-35da9179a147_1280x720.png 848w, https://substackcdn.com/image/fetch/$s_!rp3_!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F783eb531-7559-4396-8990-35da9179a147_1280x720.png 1272w, https://substackcdn.com/image/fetch/$s_!rp3_!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F783eb531-7559-4396-8990-35da9179a147_1280x720.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!rp3_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F783eb531-7559-4396-8990-35da9179a147_1280x720.png" width="1280" height="720" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/783eb531-7559-4396-8990-35da9179a147_1280x720.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:720,&quot;width&quot;:1280,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:67331,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://mlforswes.com/i/168025964?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F783eb531-7559-4396-8990-35da9179a147_1280x720.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!rp3_!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F783eb531-7559-4396-8990-35da9179a147_1280x720.png 424w, https://substackcdn.com/image/fetch/$s_!rp3_!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F783eb531-7559-4396-8990-35da9179a147_1280x720.png 848w, https://substackcdn.com/image/fetch/$s_!rp3_!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F783eb531-7559-4396-8990-35da9179a147_1280x720.png 1272w, https://substackcdn.com/image/fetch/$s_!rp3_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F783eb531-7559-4396-8990-35da9179a147_1280x720.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h4><strong>How The Algorithm Learns: Gradient Descent</strong></h4><p>The system learns these hidden features through <strong>gradient descent</strong>:</p><ol><li><p><strong>Start with random numbers</strong> in both the User Matrix and Song Matrix</p></li><li><p><strong>Make a prediction</strong> by multiplying a user's features with a song's features</p></li><li><p><strong>Calculate the error</strong> between the prediction and the actual listening data</p></li><li><p><strong>Update both matrices</strong> by nudging the numbers in the direction that reduces the error</p></li><li><p><strong>Repeat millions of times</strong> until the predictions become accurate</p></li></ol><p>This process automatically discovers what those hidden features should represent to best explain the listening patterns in the data.</p><h4><strong>Making Predictions: How Recommendations Actually Work</strong></h4><p>Once the matrices are trained, generating recommendations for a user follows a straightforward process:</p><ol><li><p><strong>Calculate prediction scores</strong>: Take the user's feature vector and multiply it with every song's feature vector to get a prediction score for each song</p></li><li><p><strong>Sort by highest scores</strong>: Rank all songs by their prediction scores for that user (highest scores = most likely to be enjoyed)</p></li><li><p><strong>Apply filters</strong>: Remove songs the user has already heard, songs not available in their region, explicit content if filtered, etc.</p></li><li><p><strong>Recommend the top results</strong>: Serve the highest-scoring remaining songs</p></li></ol><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!faPN!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F81acccce-e06b-4516-8dd9-21dc6ca44584_1280x720.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!faPN!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F81acccce-e06b-4516-8dd9-21dc6ca44584_1280x720.png 424w, https://substackcdn.com/image/fetch/$s_!faPN!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F81acccce-e06b-4516-8dd9-21dc6ca44584_1280x720.png 848w, https://substackcdn.com/image/fetch/$s_!faPN!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F81acccce-e06b-4516-8dd9-21dc6ca44584_1280x720.png 1272w, https://substackcdn.com/image/fetch/$s_!faPN!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F81acccce-e06b-4516-8dd9-21dc6ca44584_1280x720.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!faPN!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F81acccce-e06b-4516-8dd9-21dc6ca44584_1280x720.png" width="1280" height="720" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/81acccce-e06b-4516-8dd9-21dc6ca44584_1280x720.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:720,&quot;width&quot;:1280,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:44277,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://mlforswes.com/i/168025964?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F81acccce-e06b-4516-8dd9-21dc6ca44584_1280x720.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!faPN!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F81acccce-e06b-4516-8dd9-21dc6ca44584_1280x720.png 424w, https://substackcdn.com/image/fetch/$s_!faPN!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F81acccce-e06b-4516-8dd9-21dc6ca44584_1280x720.png 848w, https://substackcdn.com/image/fetch/$s_!faPN!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F81acccce-e06b-4516-8dd9-21dc6ca44584_1280x720.png 1272w, https://substackcdn.com/image/fetch/$s_!faPN!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F81acccce-e06b-4516-8dd9-21dc6ca44584_1280x720.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>So if User 123 gets prediction scores like Song A: 0.94, Song B: 0.87, Song C: 0.82, Song D: 0.31, and they've already heard Song A, then Spotify would recommend Song B first, then Song C, and so on.</p><h4><strong>What Spotify Actually Does (The Reality)</strong></h4><p>In practice, Spotify doesn't calculate predictions for <em>every</em> song for <em>every</em> user in real-time&#8212;that would require billions of calculations per second. Instead, they likely:</p><ul><li><p>Pre-calculate predictions for popular songs and store them</p></li><li><p>Use other algorithms to narrow down candidates first (maybe focusing on recently released songs or songs similar to what you've been listening to lately)</p></li><li><p>Calculate predictions on-demand for only a smaller subset of songs</p></li></ul><p>But the core concept remains the same: matrix multiplication gives you prediction scores, you sort by those scores, filter out what doesn't make sense, and recommend what's left. The beauty is that this simple mathematical approach can work at Spotify's massive scale once you get clever about which predictions to calculate when.</p><h3><strong>The Cold Start</strong></h3><p>Collaborative filtering works really well for recommendation systems, but like any machine learning model, it struggles when there isn't any data to train on. This is called <strong>The Cold Start Problem</strong>. How do we make recommendations when the user hasn't interacted with the platform yet?</p><p>A popular solution to the cold start is recommending the content that is most popular or preferred by everyone. This works well in visual recommendation feeds where many of the most popular items with a bit of mix in categories can be shown on a screen at once. But this doesn't work with a music recommendation system where one song is recommended at a time. Just recommending the most popular music is sure to turn away listeners who aren't at all interested in hip hop.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!kX21!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F53c3a220-5711-4da3-9259-3ff6496ead4c_1280x720.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!kX21!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F53c3a220-5711-4da3-9259-3ff6496ead4c_1280x720.png 424w, https://substackcdn.com/image/fetch/$s_!kX21!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F53c3a220-5711-4da3-9259-3ff6496ead4c_1280x720.png 848w, https://substackcdn.com/image/fetch/$s_!kX21!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F53c3a220-5711-4da3-9259-3ff6496ead4c_1280x720.png 1272w, https://substackcdn.com/image/fetch/$s_!kX21!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F53c3a220-5711-4da3-9259-3ff6496ead4c_1280x720.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!kX21!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F53c3a220-5711-4da3-9259-3ff6496ead4c_1280x720.png" width="1280" height="720" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/53c3a220-5711-4da3-9259-3ff6496ead4c_1280x720.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:720,&quot;width&quot;:1280,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:76066,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://mlforswes.com/i/168025964?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F53c3a220-5711-4da3-9259-3ff6496ead4c_1280x720.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!kX21!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F53c3a220-5711-4da3-9259-3ff6496ead4c_1280x720.png 424w, https://substackcdn.com/image/fetch/$s_!kX21!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F53c3a220-5711-4da3-9259-3ff6496ead4c_1280x720.png 848w, https://substackcdn.com/image/fetch/$s_!kX21!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F53c3a220-5711-4da3-9259-3ff6496ead4c_1280x720.png 1272w, https://substackcdn.com/image/fetch/$s_!kX21!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F53c3a220-5711-4da3-9259-3ff6496ead4c_1280x720.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Spotify takes a different approach to this problem by leveraging popular streams with some variance on the home page menu and then <a href="https://hw.glich.co/p/how-spotify-optimized-their-recommendation-system">using </a><strong><a href="https://hw.glich.co/p/how-spotify-optimized-their-recommendation-system">convolutional neural networks (CNNs)</a></strong><a href="https://hw.glich.co/p/how-spotify-optimized-their-recommendation-system"> to analyze </a><strong><a href="https://hw.glich.co/p/how-spotify-optimized-their-recommendation-system">spectrograms</a></strong> (visual representations of sound) to recommend songs where the audio is similar to a song the user is already listening to.</p><p>A new user will be shown recommendations on their home page or by searching. Once they select a song, it will play and Spotify will queue recommendations after it based on how that song sounds. Once the user has listened to more songs, Spotify will create a user profile to start recommending items via collaborative filtering.</p><h3><strong>Going Past Sound</strong></h3><p>The next question Spotify engineers had to answer was how to suggest music by more than just sound. Collaborative filtering and analyzing spectrograms via CNNs lean heavily into analyzing the music preference of a user via sound. This doesn't work when the user wants music for a specific occasion, and this goes beyond what the music sounds like.</p><p>The solution was most surprising to me because it wasn't something I thought would be effective until I read more about it. Spotify uses "cultural vectorization."</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!mDcu!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42272b4c-1bcc-44b9-a1a4-b2b6d8fa9277_1280x720.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!mDcu!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42272b4c-1bcc-44b9-a1a4-b2b6d8fa9277_1280x720.png 424w, https://substackcdn.com/image/fetch/$s_!mDcu!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42272b4c-1bcc-44b9-a1a4-b2b6d8fa9277_1280x720.png 848w, https://substackcdn.com/image/fetch/$s_!mDcu!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42272b4c-1bcc-44b9-a1a4-b2b6d8fa9277_1280x720.png 1272w, https://substackcdn.com/image/fetch/$s_!mDcu!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42272b4c-1bcc-44b9-a1a4-b2b6d8fa9277_1280x720.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!mDcu!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42272b4c-1bcc-44b9-a1a4-b2b6d8fa9277_1280x720.png" width="1280" height="720" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/42272b4c-1bcc-44b9-a1a4-b2b6d8fa9277_1280x720.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:720,&quot;width&quot;:1280,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:74589,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://mlforswes.com/i/168025964?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42272b4c-1bcc-44b9-a1a4-b2b6d8fa9277_1280x720.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!mDcu!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42272b4c-1bcc-44b9-a1a4-b2b6d8fa9277_1280x720.png 424w, https://substackcdn.com/image/fetch/$s_!mDcu!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42272b4c-1bcc-44b9-a1a4-b2b6d8fa9277_1280x720.png 848w, https://substackcdn.com/image/fetch/$s_!mDcu!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42272b4c-1bcc-44b9-a1a4-b2b6d8fa9277_1280x720.png 1272w, https://substackcdn.com/image/fetch/$s_!mDcu!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42272b4c-1bcc-44b9-a1a4-b2b6d8fa9277_1280x720.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Spotify's systems will use <strong><a href="https://omnisearch.ai/blog/spotify-unwrapped">natural language processing (NLP)</a></strong><a href="https://omnisearch.ai/blog/spotify-unwrapped"> to search the internet and find text relating to a song</a>. This includes <a href="https://attractgroup.com/blog/how-spotify-algorithm-works-for-music-recommendation/">blog posts, song descriptions, and song lyrics</a>. This processing checks the language others are using to describe songs, and Spotify's system will tag tracks based on that. These descriptors are then used for context-based recommendations.</p><h3><strong>Filling in the Gaps</strong></h3><p>Spotify uses the above methods to create a hybrid recommendation system that works for users in multiple scenarios. There are also further methods to <a href="https://www.loudlab.org/blog/understanding-how-spotify-algorithm-works/">boost music discovery via reinforcement learning (RL)</a> and ensure less popular music is recommended to users. This is important for lesser-known artists to benefit from posting their music on Spotify's platform.</p><p>Spotify further supplements the above algorithms by <a href="https://www.spotify.com/us/safetyandprivacy/understanding-recommendations">having editors (human experts) curate playlists</a> using their understanding of cultural trends. This is especially important when targeting playlists toward local markets, as it requires an understanding of cultural trends within the area to ensure the best listening experience.</p><p>I'm certain there are other problems Spotify works to solve and other algorithms used to solve them, but they're outside the scope of this article. Many likely aren't made available to the public, and all of Spotify's solutions are adapting over time.</p><p>Music is a dynamic space. Trends are changing constantly. Taste in music develops over time. Thus, all the solutions Spotify creates have to be constantly evolving with the problem they solve. Some examples of the evolution Spotify sees are:</p><ul><li><p><strong>A user's musical taste changing</strong>. All the recommendation algorithms need to be constantly trained and updated to ensure an up-to-date user profile within the recommendation system.</p></li><li><p><strong>Overall song and musical tastes changing.</strong> Algorithms must be updated to account for changing trends in music. Human editors need to be aware of cultural shifts when curating playlists.</p></li><li><p><strong>Technology evolves.</strong> Spotify can integrate more advanced technology to improve their algorithms and make them more efficient. This also might come in the form of new machine learning technology, which I'm sure Spotify is integrating into their platform.</p></li><li><p><strong>The platform develops.</strong> The introduction of audiobooks required Spotify to develop <a href="https://arxiv.org/abs/2403.05185">an entirely new machine learning system</a> that can leverage user music and podcast preferences to make audiobook recommendations. Audiobook data is much more sparse than musical data, requiring Spotify to leverage other data for recommendations.</p></li></ul><p>All of these require constant training of models and development of new solutions.</p><h2><strong>Impact</strong></h2><p>This is where the best machine learning engineers are separated from the rest. Technical knowledge is a requirement for the role, but understanding the interplay between machine learning and the problem space in which it's creating solutions is heavily understated.</p><p><strong>Be familiar with Goodhart's Law: </strong><em><strong>"When a measure becomes a target, it ceases to be a good measure"</strong></em>. When a metric is identified as a key metric for success, systems will start optimizing specifically for that metric, which results in behaviors that optimize for that metric but undermine the actual, broader objective.</p><p>Machine learning algorithms are trained by optimizing them for a certain measure. Successful training means an algorithm was successfully optimized <em>for that specific metric</em>. It doesn't necessarily mean the model achieves the objective we're hoping for. This creates a system where the tested metric can be optimized to game the system and this almost <em>always</em> leads to adverse outcomes.</p><p>Here's an example: Machine learning algorithms power search and are optimized for specific metrics. Those creating websites can game SEO by ensuring their website fits into what that metric decides is best to serve to a user. This creates a cat-and-mouse game where web page creators game SEO, then it changes, then they game it again. When SEO is gamed, it's a poor experience for the user.</p><p>A similar thing has happened with music recommendations. <a href="https://explodingtopics.com/blog/music-streaming-stats">Over one-third of people get their music via a streaming service in 2025</a>. Record labels and artists make money when people listen to their music, so knowing this, record labels will optimize music to work well with these algorithms to drive listening and increase profits.</p><p>This has had a very real impact on the song creation process. Instead of artists leading their creative process with what they want to create, they have to heavily factor in how a song will be recommended while they're creating music.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Pdo4!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6b4f32f-5cce-4c5d-9128-9835cee4e244_1280x720.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Pdo4!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6b4f32f-5cce-4c5d-9128-9835cee4e244_1280x720.png 424w, https://substackcdn.com/image/fetch/$s_!Pdo4!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6b4f32f-5cce-4c5d-9128-9835cee4e244_1280x720.png 848w, https://substackcdn.com/image/fetch/$s_!Pdo4!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6b4f32f-5cce-4c5d-9128-9835cee4e244_1280x720.png 1272w, https://substackcdn.com/image/fetch/$s_!Pdo4!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6b4f32f-5cce-4c5d-9128-9835cee4e244_1280x720.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Pdo4!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6b4f32f-5cce-4c5d-9128-9835cee4e244_1280x720.png" width="1280" height="720" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f6b4f32f-5cce-4c5d-9128-9835cee4e244_1280x720.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:720,&quot;width&quot;:1280,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:64839,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://mlforswes.com/i/168025964?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6b4f32f-5cce-4c5d-9128-9835cee4e244_1280x720.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Pdo4!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6b4f32f-5cce-4c5d-9128-9835cee4e244_1280x720.png 424w, https://substackcdn.com/image/fetch/$s_!Pdo4!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6b4f32f-5cce-4c5d-9128-9835cee4e244_1280x720.png 848w, https://substackcdn.com/image/fetch/$s_!Pdo4!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6b4f32f-5cce-4c5d-9128-9835cee4e244_1280x720.png 1272w, https://substackcdn.com/image/fetch/$s_!Pdo4!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6b4f32f-5cce-4c5d-9128-9835cee4e244_1280x720.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>This can be noticed in modern-day songs. The <a href="https://www.wiseband.com/blog/spotifys-algorithm-explained/">first 30 seconds of a song are weighed very heavily within music recommendation systems</a>. If a user skips a song within the first 30 seconds, the recommendation algorithm won't recommend it as much. Songs are created so the beginning of the song is engaging, meaning getting to a hook faster and having fewer drawn-out introductions.</p><p>Another way artists get around negative weighting when songs are skipped is by making songs shorter. Shorter songs are also favored by Spotify's <a href="https://www.venicemusic.co/blog/how-much-do-artists-make-on-spotify-a-realistic-breakdown-for-2025">pay-per-stream model</a>. Since Spotify pays artists per stream instead of duration of streams, shorter songs mean listeners can stream an artist's songs more times in a shorter amount of time.</p><p>Pay-per-stream has had the biggest impact on small artists. This favors large artists considerably because they are often prominently placed in curated playlists. The algorithm has <a href="https://omnisearch.ai/blog/spotify-unwrapped">an easier time recommending those artists because they already have a large catalog of similar music</a> that is frequently listened to. Thus, <a href="https://www.themaryword.com/post/how-streaming-services-are-changing-how-artists-make-music-and-how-we-experience-it">smaller artists have a hard time reaching the streaming volume required for any significant income</a>.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!UqiG!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7f44f4d-5913-4a4f-9f4a-db72ba0ace03_1280x720.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!UqiG!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7f44f4d-5913-4a4f-9f4a-db72ba0ace03_1280x720.png 424w, https://substackcdn.com/image/fetch/$s_!UqiG!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7f44f4d-5913-4a4f-9f4a-db72ba0ace03_1280x720.png 848w, https://substackcdn.com/image/fetch/$s_!UqiG!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7f44f4d-5913-4a4f-9f4a-db72ba0ace03_1280x720.png 1272w, https://substackcdn.com/image/fetch/$s_!UqiG!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7f44f4d-5913-4a4f-9f4a-db72ba0ace03_1280x720.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!UqiG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7f44f4d-5913-4a4f-9f4a-db72ba0ace03_1280x720.png" width="1280" height="720" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b7f44f4d-5913-4a4f-9f4a-db72ba0ace03_1280x720.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:720,&quot;width&quot;:1280,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:55142,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://mlforswes.com/i/168025964?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7f44f4d-5913-4a4f-9f4a-db72ba0ace03_1280x720.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!UqiG!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7f44f4d-5913-4a4f-9f4a-db72ba0ace03_1280x720.png 424w, https://substackcdn.com/image/fetch/$s_!UqiG!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7f44f4d-5913-4a4f-9f4a-db72ba0ace03_1280x720.png 848w, https://substackcdn.com/image/fetch/$s_!UqiG!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7f44f4d-5913-4a4f-9f4a-db72ba0ace03_1280x720.png 1272w, https://substackcdn.com/image/fetch/$s_!UqiG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7f44f4d-5913-4a4f-9f4a-db72ba0ace03_1280x720.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>When people figure out what works or what essentially games the algorithm to make it recommend a certain type of content, they'll continue to produce that type of content. This is a huge reason why we're seeing greater homogeneity in the music that's released today as opposed to the music that was released two decades ago.</p><h2><strong>Don't hate the player, hate the game</strong></h2><p>Spotify has made music far more accessible for the consumer by solving a very complex problem at scale using machine learning. The outcome is an algorithmically-driven feed that can be gamed as soon as artists understand how to do so.</p><p>While a major problem is solved, others are introduced. Spotify has continued iterating on their solution to mitigate the unintended problems that arise due to machine learning systems optimizing for a specific measure.</p><p>Streaming services have fundamentally changed the music industry and will continue to do so as algorithms evolve and change over time to make music even more accessible to the consumer.</p><p>As a machine learning engineer, studying Spotify should show you that <strong>machine learning algorithms solve complex problems, and solving those problems leads to those systems having massive impact. Any side effects of those solutions can also be largely impactful.</strong></p><p>So when you're building machine learning algorithms, make sure you:</p><ol><li><p>Understand your problem space and the right metric to target.</p></li><li><p>Study the side effects of optimizing for that metric and pay attention to how your algorithm can be gamed.</p></li><li><p>Continue iteration on the algorithm to more optimally solve the problem.</p></li></ol><p>If you keep these things in mind, you can solve very complex problems elegantly.</p><p><em>Stay tuned for part 2, where we&#8217;ll implement our own simple collaborative filtering solution.</em></p><p><strong>Thanks for reading!</strong></p><p><strong>Always be (machine) learning,</strong></p><p><strong>Logan</strong></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.aiforswes.com/p/spotify-case-study?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.aiforswes.com/p/spotify-case-study?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share</span></a></p><p></p>]]></content:encoded></item></channel></rss>