Personal Learnings← Eugene Yan  Library

Eugene Yan · Tech & AI

Beating the Baseline Recommender with Graph & NLP in Pytorch

TIER 5   2020-01-13

<!DOCTYPE html>
<html lang="en">
<head>
    <!--Load inline css and scripts first-->
    <style>:root{--c-light-text:#333;--c-light-background:#fff;--c-light-focus:#00ff00;--c-light-interactive:#007bff;--c-dark-text:#fff;--c-dark-subtext:#a6a6a6;--c-dark-background:#333;--c-dark-focus:#00ff00;--c-dark-interactive:#66b0ff;--c-dark-callout:#003166;--c-text:var(--c-light-text);--c-background:var(--c-light-background);--c-focus:var(--c-light-focus);--c-interactive:var(--c-light-interactive)}.dark-mode-checkbox:checked~.theme-container{--c-text:var(--c-dark-text);--c-background:var(--c-dark-background);--c-focus:var(--c-dark-focus);--c-interactive:var(--c-dark-interactive)}html.dark-mode{--c-text:var(--c-dark-text);--c-background:var(--c-dark-background);--c-focus:var(--c-dark-focus);--c-interactive:var(--c-dark-interactive)}html.dark-mode .tag{background-color:#3e3e3e;color:var(--c-dark-interactive)}html.dark-mode a.tag:hover{background-color:var(--c-dark-interactive);color:#3e3e3e}a{text-decoration:none;background-color:transparent;color:var(--c-interactive)}</style>
    <!-- darkmode JS at start of the doc so to ensure consistent view mode -->
<link href="/js/darkmode.js" rel="preload" as="script">
<script src="/js/darkmode.js" type="8508930d3962b87c0ce5ff16-text/javascript"></script>

<!--Add active class to nav bar-->
<link href="/js/navbar.js" rel="preload" as="script">
<script src="/js/navbar.js" defer type="8508930d3962b87c0ce5ff16-text/javascript"></script>

<!-- Load jQuery before anchor.min.js -->
<link href="/js/jquery-3.7.1.min.js" rel="preload" as="script">
<script src="/js/jquery-3.7.1.min.js" defer type="8508930d3962b87c0ce5ff16-text/javascript"></script>

<!--Add anchors to headers-->
<link href="/js/anchor.min.js" rel="preload" as="script">
<script src="/js/anchor.min.js" defer type="8508930d3962b87c0ce5ff16-text/javascript"></script>

<!-- Algolia Insights -->
<script type="8508930d3962b87c0ce5ff16-text/javascript">
  var ALGOLIA_INSIGHTS_SRC = "/js/search-insights.min.js"; // Using local version
  !function(e,a,t,n,s,i,c){e.AlgoliaAnalyticsObject=s,e[s]=e[s]||function(){
  (e[s].queue=e[s].queue||[]).push(arguments)},e[s].version=(n.match(/@([^\/]+)\/?.*/) || [])[1],i=a.createElement(t),c=a.getElementsByTagName(t)[0],
  i.async=1,i.src=n,c.parentNode.insertBefore(i,c)
  }(window,document,"script",ALGOLIA_INSIGHTS_SRC,"aa");
</script>
<script type="8508930d3962b87c0ce5ff16-text/javascript">
  if (typeof aa === 'function') {
    aa('init', {
      appId: '2XJCLEABQD',
      apiKey: 'b61ec4cb64bd32d62c053466fccbfa43',
      useCookie: true
    });
  }
</script>

    <meta charset="utf-8">
    <meta name="HandheldFriendly" content="True">
    <meta name="MobileOptimized" content="320">
    <meta name="viewport" content="width=device-width, initial-scale=0.86, maximum-scale=3.0, minimum-scale=0.86">
    <meta name="description" content="Beating the baseline using Graph & NLP techniques on PyTorch, AUC improvement of ~21% (Part 2 of 2).">
    <meta name="author" content="Eugene Yan">

    <meta content="eugeneyan.com" property="og:site_name">
    <meta name=twitter:card content=summary_large_image>
    <meta name=twitter:domain content=eugeneyan.com>
    
    <meta content="Beating the Baseline Recommender with Graph & NLP in Pytorch" property="og:title">
    <meta name=twitter:title content="Beating the Baseline Recommender with Graph & NLP in Pytorch">
    
    
    <meta content="article" property="og:type">
    
    
    <meta content="Beating the baseline using Graph & NLP techniques on PyTorch, AUC improvement of ~21% (Part 2 of 2)." property="og:description">
    <meta name=twitter:description content="Beating the baseline using Graph & NLP techniques on PyTorch, AUC improvement of ~21% (Part 2 of 2).">
    
    
    <meta content="https://eugeneyan.com/writing/recommender-systems-graph-and-nlp-pytorch/" property="og:url">
    
    
    <meta content="2020-01-13T00:00:00+00:00" property="article:published_time">
    <meta content="https://eugeneyan.com/about/" property="article:author">
    
    
    <meta content="https://eugeneyan.com/assets/og_image/recsys-graph.jpg" property="og:image">
    <meta name=twitter:image content="https://eugeneyan.com/assets/og_image/recsys-graph.jpg">
    
    
    
    <meta content="posts" property="article:section">
    
    
    
    
    <meta content="recsys" property="article:tag">
    
    <meta content="deeplearning" property="article:tag">
    
    <meta content="python" property="article:tag">
    
    <meta content="🛠" property="article:tag">
    
    
    <title>Beating the Baseline Recommender with Graph & NLP in Pytorch</title>
    <!-- styles -->
    <script src="/cdn-cgi/scripts/7d0fa10a/cloudflare-static/rocket-loader.min.js" data-cf-settings="8508930d3962b87c0ce5ff16-|49"></script><link href="/css/main.min.css" rel="preload" as="style" onload="this.rel='stylesheet'" type="text/css">
    <link rel="stylesheet" href="/css/main.css" type="text/css">

    <!-- Preconnect to Google Fonts domains to reduce latency -->
    <link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <script src="/cdn-cgi/scripts/7d0fa10a/cloudflare-static/rocket-loader.min.js" data-cf-settings="8508930d3962b87c0ce5ff16-|49"></script><link href="https://fonts.googleapis.com/css2?family=Merriweather:wght@400;700&family=Raleway&display=swap"
          rel="preload" as="style" onload="this.rel='stylesheet'">
    <link href="https://fonts.googleapis.com/css2?family=Merriweather:wght@400;700&family=Raleway&display=swap"
          rel="stylesheet">

    <script src="/cdn-cgi/scripts/7d0fa10a/cloudflare-static/rocket-loader.min.js" data-cf-settings="8508930d3962b87c0ce5ff16-|49"></script><link href="/css/monokai.css" rel="preload" as="style" onload="this.rel='stylesheet'" type="text/css">
    <link href="/css/monokai.css" rel="stylesheet" type="text/css">

    <link rel="shortcut icon" type="image/png" href="https://eugeneyan.com/assets/favicon/favicon.ico">
    <link rel="apple-touch-icon" sizes="180x180" href="https://eugeneyan.com/assets/favicon/apple-touch-icon.webp">
    <link rel="icon" type="image/png" sizes="32x32" href="https://eugeneyan.com/assets/favicon/favicon-32x32.webp">
    <link rel="icon" type="image/png" sizes="16x16" href="https://eugeneyan.com/assets/favicon/favicon-16x16.webp">
    <link rel="manifest" href="/assets/favicon/site.webmanifest">

    <link rel="canonical" href="https://eugeneyan.com/writing/recommender-systems-graph-and-nlp-pytorch/"/>

    <!--  Collect tags-->
    
    





    

    <!-- google analytics - i will not share this data with google -->
    <!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-4CKMNLRMCV" type="8508930d3962b87c0ce5ff16-text/javascript"></script>
<script type="8508930d3962b87c0ce5ff16-text/javascript">
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());

  gtag('config', 'G-4CKMNLRMCV');

</script>
</head>

<input class="dark-mode-checkbox" id="dark-mode" name="dark-mode-checkbox" type="checkbox"/>
<label class="dark-mode-label" for="dark-mode"></label>
<body>
<div class="theme-container grow">
    <div class="container" style="width: 95%">

        <div class="header">
    <div class="row">
        <div class="col-sm-3">
            <h1 class="text-muted nav"><a href="/">eugeneyan</a></h1>
        </div>
        <div class="col-sm-9">
            <ul id="nav" class="nav-margin nav nav-pills float-sm-right">
                <li><a href="/start-here/" title="Start Here">Start Here</a></li>
                <li><a href="/writing/" title="Writing">Writing</a></li>
                <li><a href="/speaking/" title="Speaking">Speaking</a></li>
                <li><a href="/prototyping/" title="Prototyping">Prototyping</a></li>
                <li><a href="/about/" title="About">About</a></li>
                <li><a href="/search/" title="Search"><img class="icon icon-search" src="/assets/icon-search.svg" loading="lazy" alt=""/></a></li>
            </ul>
        </div>
    </div>
</div>

        


<div class="notes">
    <div class="note single">
        <h1 class="title">Beating the Baseline Recommender with Graph & NLP in Pytorch</h1>

        <p class="date">
            <info datetime="2020-01-13 00:00:00 +0000">
                <span class="no-italics">[
        
        
        <a class='tag' href="/tag/recsys/">recsys</a>
        
        
        <a class='tag' href="/tag/deeplearning/">deeplearning</a>
        
        
        <a class='tag' href="/tag/python/">python</a>
        
        
        <a class='tag' href="/tag/🛠/">🛠</a>
        
    ]
</span> · 17 min read
            </info>
        </p>

        <!-- Post content -->
        <div class="notebody">
            <blockquote>
  <p>P.S. Looking for the code for this? Available on github: <a href="https://github.com/eugeneyan/recsys-nlp-graph" target="_blank">recsys-nlp-graph</a></p>
</blockquote>

<p>In the previous <a href="/writing/recommender-systems-baseline-pytorch/" target="_blank">post</a>, we established that a baseline recommender system (“recsys”) based on matrix factorization is able to achieve an AUC-ROC of ~0.8 (random guessing has AUC-ROC = 0.5)</p>

<p>Can we do better? Also, for curiosity’s sake, are there other approaches to recsys?</p>

<p>As it turns out, yes, and <em>yes</em>.</p>

<h2 id="natural-language-processing-nlp-and-graphs">Natural Language Processing (NLP) and Graphs</h2>

<p>In 2013, a breakthrough was made in NLP with the word2vec papers by Tomas Mikolov <a href="https://arxiv.org/abs/1310.4546" target="_blank">here</a> and <a href="https://arxiv.org/abs/1301.3781" target="_blank">here</a>. They demonstrated that w2v was able to learn semantic and syntactic vector representations of words in an unsupervised fashion.</p>

<p>Simply put, w2v can convert words (or objects) in sequences into a numeric representation, without labels—not needing labels is huge as getting labelled data is a key bottleneck in machine learning.</p>

<p>Another interesting development is <a href="https://arxiv.org/abs/1403.6652" target="_blank">DeepWalk</a> which learns representations of online social networks graphs. By performing random walks to generate sequences, the paper demonstrated that it was able to learn vector representations of nodes (e.g., profiles, content) in the graph.</p>

<p><img src="/assets/recsys2_fig1.webp" loading="lazy" alt="Image" /></p>
<p class="image-caption">Figure 1a. Arbitrary image of a (social) graph</p>

<h2 id="how-do-these-matter-in-recsys">How do these matter in recsys?</h2>

<p>It’s a slight stretch, but here’s the gist of it:</p>

<ul>
  <li>Use the product-pairs and associated relationships to create a graph</li>
  <li>Generate sequences from the graph (via <em>random walk</em>)</li>
  <li>Learn product embeddings based on the sequences (via <em>word2vec</em>)</li>
  <li>Recommend products based on embedding similarity (e.g., cosine similarity, dot product)</li>
</ul>

<p>Ready? Let’s get started.</p>

<h2 id="creating-a-graph">Creating a graph</h2>

<p>We have product-pairs and each of them have an associated score. We can think of them as (graph) edge weights.</p>

<p>With these weights, we can create a weighted graph (i.e., a graph where each edge is given a numerical weight, instead of all edges having the same weight). This can be easily done with <code class="language-plaintext highlighter-rouge">networkx</code>.</p>

<p>Violà! We have our product network graph.</p>

<h2 id="generating-sequences">Generating sequences</h2>

<p>With our product graph, we can generate sequences via random walk.</p>

<p>The direct approach is to traverse the <code class="language-plaintext highlighter-rouge">networkx</code> graph. For example, if we want 10 sequences of length 10 for each starting node, we’ll need to traverse the graph 100 times per starting node (also called vertex).</p>

<p>The electronics graph has 420k nodes and the books graph has 2 million nodes. Multiply that by 100 and that’s a lot of graph queries—this is <em>very slow</em> and takes <em>a lot of memory</em> (trust me).</p>

<p>Thankfully, we don’t need to traverse the graph using the <code class="language-plaintext highlighter-rouge">networkx</code> APIs.</p>

<h3 id="using-the-transition-matrix">Using the transition matrix</h3>

<p>A graph consists of vertices and edges, where edges are the strength of relationships between each node.</p>

<p><img src="/assets/recsys2_graph.webp" loading="lazy" alt="Image" /></p>
<p class="image-caption">Figure 1b. Example weighted graph</p>

<p>This can be decomposed into an adjacency matrix. If our graph has V nodes, then our adjacency matrix will be V x V in dimension. A regular adjacency matrix has a value of 1 if an edge exists between the nodes, 0 otherwise. Since the graph edges are weighted, the values in the adjacency matrix will be the edge weights.</p>

<p><img src="/assets/recsys2_adj.webp" loading="lazy" alt="Image" /></p>
<p class="image-caption">Figure 1c. Example weighted adjacency matrix</p>

<p>The adjacency matrix needs to be converted to a transition matrix, where the rows sum up to 1.0. Simply put, the transition matrix has the probability of each vertex transitioning to other vertices (thus each row summing to 1).</p>

<p><img src="/assets/recsys2_transition.webp" loading="lazy" alt="Image" /></p>
<p class="image-caption">Figure 1d. Example transition matrix</p>

<p>My first attempt to calculate the transition matrix was using <code class="language-plaintext highlighter-rouge">NumPy</code> arrays as suggested <a href="https://stackoverflow.com/questions/37311651/get-node-list-from-random-walk-in-networkx" target="_blank">here</a>. It didn’t work due to memory constraints.</p>

<p>How can we make this more memory efficient?</p>

<p>Recall that the datasets have 99.99% sparsity. Of 420k electronics nodes, there are only 4 million edges. This means 420k**2 minus 4 million values are zero.</p>

<p>A regular <code class="language-plaintext highlighter-rouge">NumPy</code> array would initialize these zeros—which actually can be ignored—thus taking more memory than needed.</p>

<p>My next attempt involved sparse matrices (more about it <a href="https://machinelearningmastery.com/sparse-matrices-for-machine-learning/" target="_blank">here</a>). In a nutshell, a sparse matrix doesn’t initialize those unnecessary zeros, saving memory. However, the matrix operations they support are not as extensive.</p>

<p>Converting the adjacency matrix to a transition matrix using sparse matrices <strong>worked</strong>. I’ll spare you the gory details.</p>

<p>With the transition matrix, I then converted it into dictionary form for the O(1) lookup goodness. Each key would be a node, and the value would be another dictionary of the adjacent nodes and the associated probability.</p>

<p>With this, creating random walks is now a lot simpler and much more efficient. The adjacent node can be identified by applying <code class="language-plaintext highlighter-rouge">random.choice</code> on the transition weights. This approach is orders of magnitude faster than traversing a <code class="language-plaintext highlighter-rouge">networkx</code> graph (Note: It is still traversing a graph).</p>

<h2 id="implementation-3-node2vec">Implementation 3: <code class="language-plaintext highlighter-rouge">Node2Vec</code></h2>

<p>(Implementation 3?! Where did 1 and 2 go—read the previous <a href="/writing/recommender-systems-baseline-pytorch/" target="_blank">post</a>.)</p>

<p>Soon after I went through the pain of building my own graph and sequences, I stumbled upon the github repository of <code class="language-plaintext highlighter-rouge">Node2Vec</code> (“n2v”) <a href="https://github.com/aditya-grover/node2vec" target="_blank">here</a>. Note to self: Google harder before implementing something next time.</p>

<p>n2v was appealing—it seems to work right out of the box. You just need to provide the edges. It would then generate the graph and sequences, and learn node embeddings. Under the hood, it uses <code class="language-plaintext highlighter-rouge">networkx</code> and <code class="language-plaintext highlighter-rouge">gensim</code>.</p>

<blockquote>
  <p>Unfortunately, it was very memory intensive and slow, and could not run to completion, even on a 64gb instance.</p>
</blockquote>

<p>Digging deeper, I found that its approach to generating sequences was traversing the graph. If you allowed <code class="language-plaintext highlighter-rouge">networkx</code> to use multiple threads, it would spawn multiple processes to create sequences and cache them temporarily in memory. In short, very memory hungry. Overall, this didn’t work for the datasets I had.</p>

<p>For the rest of the post, we’ll explore the training of product embeddings based on the sequences generated. To give you a better idea, these sequences are in the form of a NumPy array of objects (i.e., product IDs). The dimension is N x M, where N = number of unique products x 10, and Y = number of nodes per sequence.</p>

<p>Here’s how the sequences look like.</p>

<p><img src="/assets/recsys2_sequences.webp" loading="lazy" alt="Image" /></p>
<p class="image-caption">Figure 1e. Array of sequences for electronics dataset (420k unique products)</p>

<h2 id="implementation-4-gensimword2vec">Implementation 4: <code class="language-plaintext highlighter-rouge">gensim.word2vec</code></h2>

<p>Gensim has an implementation of w2v that takes in a list of sequences and can be multi-threaded. It was very easy to use and the fastest to complete five epochs.</p>

<blockquote>
  <p>It performed significantly better than matrix factorization (in the previous <a href="/writing/recommender-systems-baseline-pytorch/" target="_blank">post</a>), achieving an AUC-ROC of 0.9082 (relative to ~0.8).</p>
</blockquote>

<p>However, if you examine the precision recall curve below (Fig. 2), you’ll notice the sharp cliff at around threshold of 0.73—what’s causing this?</p>

<p><img src="/assets/recsys2_fig2.webp" loading="lazy" alt="Image" /></p>
<p class="image-caption">Figure 2. Precision recall curves for gensim.word2vec (all products)</p>

<p>The reason: “Unseen” products in our validation set without embeddings (i.e., they didn’t appear in the training set).</p>

<p><code class="language-plaintext highlighter-rouge">Gensim.w2v</code> is unable to initialize / create embeddings for words that don’t exist in the training data. To get around this, for product-pairs in the validation set where either product doesn’t exist in the train set, we <em>set the prediction score to the median similarity score</em>. It is these unseen products that cause the sharp cliff.</p>

<p>If we only evaluate validation set product-pairs constituents that appeared in the training set, the performance is significantly improved to AUC-ROC = 0.9735 (Fig 3.). Also, no more cliffs.</p>

<p><img src="/assets/recsys2_fig3.webp" loading="lazy" alt="Image" /></p>
<p class="image-caption">Figure 3. Precision recall curves for gensim.word2vec (seen products only)</p>

<blockquote>
  <p>Did I mention this runs in 2.58 minutes using 12 threads? This is the <em>new</em> baseline.</p>
</blockquote>

<p>Great results in under 3 minutes—project complete, right?</p>

<p>Well, not really. The intent of this exercise is learning and trying new approaches. I also felt like I haven’t quite fully grokked w2v. Furthermore, with <code class="language-plaintext highlighter-rouge">gensim</code>, I couldn’t plot learning curves across batches and epochs.</p>

<p>Also, I’ve a few ideas on how to extend w2v for recsys and the vanilla <code class="language-plaintext highlighter-rouge">gensim</code> implementation doesn’t allow for extensions.</p>

<p>Therefore, let’s dive a bit deeper and implement it from scratch, in <code class="language-plaintext highlighter-rouge">PyTorch</code>.</p>

<h2 id="implementation-5-pytorch-word2vec">Implementation 5: <code class="language-plaintext highlighter-rouge">PyTorch</code> word2vec</h2>

<p>There are two main components to training a PyTorch model: The dataloader and the model. I’ll start with the more tedious dataloader.</p>

<p>The dataloader is largely similar to the previous implementation (for matrix factorization), with a few key differences. Firstly, instead of taking in product-pairs, it now takes in sequences.</p>

<p>Furthermore, there are two new features (i.e., subsampling of frequent words and negative sampling), both of which were proposed in the second w2v <a href="https://arxiv.org/abs/1301.3781" target="_blank">paper</a>.</p>

<h3 id="subsampling-of-frequent-words">Subsampling of frequent words</h3>

<p>In the paper, subsampling of frequent words (i.e., dropping out words that occur relatively frequently) was found to accelerate learning and significantly improve on learning vectors of rare words. This is fairly straightforward (an excellent explanation available <a href="http://mccormickml.com/2017/01/11/word2vec-tutorial-part-2-negative-sampling/" target="_blank">here</a>).</p>

<h2 id="negative-sampling">Negative sampling</h2>
<p>Explaining negative subsampling is significantly more involved, so bear with me.</p>

<p>The original skip-gram had a (hierarchical) softmax layer at the end, where it outputs the probability of all vocabulary words being in the neighbourhood of the centre word.</p>

<p>If you had a vocabulary of 10k words (or products), that would be a softmax layer with 10k units. With an embedding dimension of 128, this means 1.28 million weights to update—that’s <em>a lot</em> of weights. This problem is more pronounced in recsys, as the “vocabulary” of products is in the millions.</p>

<p>Negative sampling only modifies a small proportion of weights. Specifically, the positive product-pair and a small sample of negative product-pairs. Based on the paper, five negative product-pairs is sufficient for most use cases.</p>

<p>If we have five negative product-pairs, this means we only need to update six output neurons (i.e., 1 positive product-pair, 5 negative product-pairs). Assuming 1 million products, that’s 0.0006% of the weights—<strong>very efficient</strong>!</p>

<p>(Note: You might have noticed that negative sampling was also adopted in the matrix factorization approach in the previous post, where five negative product-pairs were generated for each positive product-pair during training.)</p>

<p>How are these negative samples selected?</p>

<p>From the paper, they are selected using a unigram distribution, where more frequent words are more likely to be selected as negative samples. One unusual trick was to raise the word counts to the ¾ power, which they found to perform the best. This implementation does the same.</p>

<h2 id="skipgram-model">Skipgram model</h2>
<p>For the model, the <code class="language-plaintext highlighter-rouge">PyTorch</code> w2v implementation is very straightforward and less than 20 lines of code, so I won’t go into explaining the details. Nonetheless, here’s some simplified code of how the skipgram model class looks like:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">SkipGram</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">Module</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">emb_size</span><span class="p">,</span> <span class="n">emb_dim</span><span class="p">):</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">center_embeddings</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="n">Embedding</span><span class="p">(</span><span class="n">emb_size</span><span class="p">,</span> <span class="n">emb_dim</span><span class="p">,</span> <span class="n">sparse</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">context_embeddings</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="n">Embedding</span><span class="p">(</span><span class="n">emb_size</span><span class="p">,</span> <span class="n">emb_dim</span><span class="p">,</span> <span class="n">sparse</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">forward</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">center</span><span class="p">,</span> <span class="n">context</span><span class="p">,</span> <span class="n">neg_context</span><span class="p">):</span>
        <span class="n">emb_center</span><span class="p">,</span> <span class="n">emb_context</span><span class="p">,</span> <span class="n">emb_neg_context</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">get_embeddings</span><span class="p">()</span>

        <span class="c1"># Get score for positive pairs
</span>        <span class="n">score</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nb">sum</span><span class="p">(</span><span class="n">emb_center</span> <span class="o">*</span> <span class="n">emb_context</span><span class="p">,</span> <span class="n">dim</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
        <span class="n">score</span> <span class="o">=</span> <span class="o">-</span><span class="n">F</span><span class="p">.</span><span class="n">logsigmoid</span><span class="p">(</span><span class="n">score</span><span class="p">)</span>

        <span class="c1"># Get score for negative pairs
</span>        <span class="n">neg_score</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="n">bmm</span><span class="p">(</span><span class="n">emb_neg_context</span><span class="p">,</span> <span class="n">emb_center</span><span class="p">.</span><span class="n">unsqueeze</span><span class="p">(</span><span class="mi">2</span><span class="p">)).</span><span class="n">squeeze</span><span class="p">()</span>
        <span class="n">neg_score</span> <span class="o">=</span> <span class="o">-</span><span class="n">torch</span><span class="p">.</span><span class="nb">sum</span><span class="p">(</span><span class="n">F</span><span class="p">.</span><span class="n">logsigmoid</span><span class="p">(</span><span class="o">-</span><span class="n">neg_score</span><span class="p">),</span> <span class="n">dim</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>

        <span class="c1"># Return combined score
</span>        <span class="k">return</span> <span class="n">torch</span><span class="p">.</span><span class="n">mean</span><span class="p">(</span><span class="n">score</span> <span class="o">+</span> <span class="n">neg_score</span><span class="p">)</span>
</code></pre></div></div>

<p>How does it perform relative to matrix factorization?</p>

<p>Considering all products, the AUC-ROC achieved was 0.9554 (Fig. 4, significantly better than <code class="language-plaintext highlighter-rouge">gensim</code>). If only considering seen products, the AUC-ROC was 0.9855 (Fig. 5, slightly better than <code class="language-plaintext highlighter-rouge">gensim</code>).</p>

<p><img src="/assets/recsys2_fig4.webp" loading="lazy" alt="Image" /></p>
<p class="image-caption">Figure 4. Precision recall curves for PyTorch word2vec (all products)</p>

<p><img src="/assets/recsys2_fig5.webp" loading="lazy" alt="Image" /></p>
<p class="image-caption">Figure 5. Precision recall curves for PyTorch word2vec (seen products only)</p>

<blockquote>
  <p>The results achieved here are also better than an Alibaba paper that adopted a similar approach, also on an Amazon electronics dataset. The <a href="https://arxiv.org/abs/1803.02349" target="_blank">paper</a> reported an AUC-ROC of 0.9327.</p>
</blockquote>

<p>When examining the learning curve (Fig. 6), it seems a single epoch is sufficient. What stands out is that each time the learning rate is reset (with each new epoch), the AUC-ROC doesn’t drop drastically as in matrix factorization.</p>

<p><img src="/assets/recsys2_fig6.webp" loading="lazy" alt="Image" /></p>
<p class="image-caption">Figure 6. AUC-ROC across epochs for word2vec; a single epoch seems sufficient</p>

<p>Great result overall—it’s able to replicate <code class="language-plaintext highlighter-rouge">gensim.word2vec</code> and also does better.</p>

<p>Furthermore, it allows us to initialize embeddings for <em>all</em> products, even those not present in the train set. While those embeddings may be untrained, they can be updated with new data (without having to retrain from scratch).</p>

<p>One downside—it’s nowhere as efficient as the <code class="language-plaintext highlighter-rouge">gensim</code> implementation, taking 23.63 minutes. I blame my suboptimal implementation. (Suggestions on how to improve welcome!).</p>

<h2 id="implementation-6-pytorch-word2vec-with-side-info">Implementation 6: <code class="language-plaintext highlighter-rouge">PyTorch</code> word2vec with side info</h2>

<p>One reason why I built w2v from scratch is the intention of <em>extending it by adding side information</em>. For each product in a sequence, we have valuable side information such as brand, category, price, etc. (example below). Why not add this information when learning embeddings?</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>B001T9NUFS -&gt; B003AVEU6G -&gt; B007ZN5Y56 ... -&gt; B007ZN5Y56
Television    Sound bar     Lamp              Standing Fan
Sony          Sony          Phillips          Dyson
500 – 600     200 – 300     50 – 75           300 - 400
</code></pre></div></div>

<p>Implementation-wise, instead of a single embedding per product, we now have multiple embeddings for product ID, brand, category, etc. We can then aggregate these embeddings into a single embedding.</p>

<p>This could potentially help with the cold-start problem; it was also proposed in the Alibaba <a href="https://arxiv.org/abs/1803.02349" target="_blank">paper</a> where they used side information for brand, category level 1, and category level 2. By using a similar electronics dataset from Amazon, they reported AUC-ROC improvement (due to side information) from 0.9327 to 0.9575.</p>

<p>I implemented two versions. First, equal-weighted averaging (of embeddings) and second, learning the weightage for each embedding and applying a weighted average to aggregate into a single embedding.</p>

<p><strong>Both didn’t work</strong>. AUC-ROC during training dropped to between 0.4 – 0.5 (Fig. 7).</p>

<p><img src="/assets/recsys2_fig7.webp" loading="lazy" alt="Image" /></p>
<p class="image-caption">Figure 7. AUC-ROC across epochs for word2vec with side information</p>

<blockquote>
  <p>After spending significant time ensuring the implementation was correct, I gave up. Trying it with no side information yielded the <em>same results as implementation 5, albeit slower</em>.</p>
</blockquote>

<p>One possible reason for this non-result is the <em>sparsity</em> of the meta data. Out of 418,749 electronic products, we only had metadata for 162,023 (39%). Of these, brand was 51% empty.</p>

<p><strong>Nonetheless, my assumption was that the weightage of the embeddings, especially the (less helpful) metadata embeddings, could be learned.</strong> Thus, their weight in the aggregate would be reduced (or minimized), and adding metadata should not hurt model performance. Clearly, this was not the case.</p>

<p>All in all, trying to apply w2v with side information didn’t work.  <strong>¯_(ツ)_/¯</strong></p>

<h2 id="implementation-7-sequences--matrix-factorization">Implementation 7: Sequences + Matrix Factorization</h2>

<p>Why did the w2v approach do so much better than matrix factorization? Was it due to the skipgram model, or due to the training data format (i.e., sequences)?</p>

<p>To understand this better, I tried the previous matrix factorization with bias implementation (AUC-ROC = 0.7951) with the new sequences and dataloader.</p>

<p>Lo and behold, <strong>AUC-ROC shot up to 0.9320</strong> (Fig. 8)!</p>

<p><img src="/assets/recsys2_fig8.webp" loading="lazy" alt="Image" /></p>
<p class="image-caption">Figure 8. Precision recall curve for PyTorch MF-bias with sequences.</p>

<p>This suggests that the “graph-random-walk-sequences” approach works well.</p>

<p>One reason could be that in the original matrix factorization, we only learnt embeddings based on the product-pairs. But in the sequences, we used a window size of 5, thus we had 5 times more data to learn from, though products that are further apart in the sequences might be less strongly related.</p>

<p>Oddly though, the matrix factorization approach still exhibits the effect of “forgetting” as learning rate resets with each epoch (Fig 9.), though not as pronounced as Figure 3 in the previous post.</p>

<p><em>I wonder if this is due to using the same embeddings for both center and context.</em></p>

<p><img src="/assets/recsys2_fig9.webp" loading="lazy" alt="Image" /></p>
<p class="image-caption">Figure 9. Learning curve for PyTorch MF-bias with sequences. </p>

<p>Another downside is that it takes about 3x the time, increasing from 23.63 minutes (w2v implementation) to 70.39 minutes (matrix factorization implementation).</p>

<h2 id="additional-results-on-a-bigger-dataset-books">Additional results on a bigger dataset (books)</h2>

<p>In parallel, I also prepared a books dataset that has 2 million unique products making up 26.5 million product-pairs and applied the implementations on it.</p>

<p>Some notable results are as follows.</p>

<p>Matrix Factorization:</p>

<ul>
  <li>Overall AUC-ROC: 0.4996 (not sure why it’s not able to learn)</li>
  <li>Time taken for 5 epochs: 1353.12 minutes</li>
</ul>

<p><code class="language-plaintext highlighter-rouge">Gensim</code> Word2vec</p>

<ul>
  <li>Overall AUC-ROC: 0.9701</li>
  <li>Seen-products-only AUC-ROC: 0.9892</li>
  <li>Time taken for 5 epochs: 16.24 minutes</li>
</ul>

<p><code class="language-plaintext highlighter-rouge">PyTorch</code> Word2vec</p>

<ul>
  <li>Overall AUC-ROC: 0.9775</li>
  <li>Time taken for 5 epochs: 122.66 minutes</li>
</ul>

<p><code class="language-plaintext highlighter-rouge">PyTorch</code> Matrix Factorization with Sequences</p>

<ul>
  <li>Overall AUC-ROC: 0.7196</li>
  <li>Time taken for 5 epochs: 1393.08 minutes</li>
</ul>

<p>Similarly, using sequences with matrix factorization helps significantly, though it doesn’t quite achieve the same stellar results as regular word2vec.</p>

<h2 id="further-extensions">Further Extensions</h2>

<p>In these two articles, we’ve examined 7 different item-to-item recsys implementations. And we haven’t even considered user information yet—the exploration space is huge!</p>

<p>If we have user data, we can build user embeddings in the same vector space (as product emdeddings). This can be done by training user embeddings based on the products that (i) users click on (positive), (ii) users are exposed to but don’t click on (negative), and (iii) purchase (strong positive).</p>

<p>This approach was adopted by Airbnb and seems <a href="https://www.kdd.org/kdd2018/accepted-papers/view/real-time-personalization-using-embeddings-for-search-ranking-at-airbnb" target="_blank">promising</a>. However, due to sparsity of user and product data (i.e., majority of users and accommodations have very few bookings), they aggregated users and accommodations into user and accommodation types—this ensures sufficient samples for each type.</p>

<p>Why stop at just user and product embeddings? From this exercise, we’ve seen how powerful embeddings can be with the right training data. In the StarSpace paper, Facebook has taken it to the <a href="https://arxiv.org/abs/1709.03856" target="_blank">extreme</a> and propose to embed everything.</p>

<p>Recently, Uber Eats also shared about how they used Graph Neural Networks to learn embeddings for users and menu items, where a node’s embedding is based on it neighbours. More <a href="https://eng.uber.com/uber-eats-graph-learning/" target="_blank">here</a>.</p>

<h2 id="key-takeaways">Key Takeaways</h2>

<p>Using w2v to generate product embeddings is a very strong baseline and easily beats basic matrix factorization approaches.</p>

<p>If you have the sequences ready, you can just use <code class="language-plaintext highlighter-rouge">gensim</code>’s implementation. If you want to extend on w2v and need your own implementation, developing it is not difficult.</p>

<p>It was <strong>great</strong> that the PyTorch w2v implementation did better than <code class="language-plaintext highlighter-rouge">gensim</code>’s, and had better results than the Alibaba paper. Unfortunately, I was unable to replicate the improvements from using side information. Perhaps I need to rerun the experiments on a dataset where the metadata is less sparse to confirm.</p>

<p>Lastly, training data in the form of sequences are <strong>epic</strong>—matrix factorization with sequences is far better than just matrix factorization with product-pairs. Sequences can be built via multiple novel approaches—in this post, I built a product graph and then performed random walks to generate those sequences.</p>

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

<p>This has been a fun exercise to apply my interest in sequences to an area of machine learning where they are less commonly seen (i.e., recsys).</p>

<p>I hope the learnings and code shared here will be useful for others who are developing their own recsys implementations, be it for fun or at work.</p>

<blockquote>
  <p>P.S. Looking for the code for this? Available on github: <a href="https://github.com/eugeneyan/recsys-nlp-graph" target="_blank">recsys-nlp-graph</a></p>
</blockquote>

<h2 id="references">References</h2>
<p>Mikolov, T., Sutskever, I., Chen, K., Corrado, G. S., &amp; Dean, J. (2013). Distributed representations of words and phrases and their compositionality. In Advances in neural information processing systems (pp. 3111-3119).</p>

<p>Mikolov, T., Chen, K., Corrado, G., &amp; Dean, J. (2013). Efficient estimation of word representations in vector space. arXiv preprint arXiv:1301.3781.</p>

<p>Perozzi, B., Al-Rfou, R., &amp; Skiena, S. (2014, August). Deepwalk: Online learning of social representations. In Proceedings of the 20th ACM SIGKDD international conference on Knowledge discovery and data mining (pp. 701-710). ACM.</p>

<p>Grover, A., &amp; Leskovec, J. (2016, August). node2vec: Scalable feature learning for networks. In Proceedings of the 22nd ACM SIGKDD international conference on Knowledge discovery and data mining (pp. 855-864). ACM.</p>

<p>Wang, J., Huang, P., Zhao, H., Zhang, Z., Zhao, B., &amp; Lee, D. L. (2018, July). Billion-scale commodity embedding for e-commerce recommendation in alibaba. In Proceedings of the 24th ACM SIGKDD International Conference on Knowledge Discovery &amp; Data Mining (pp. 839-848). ACM.</p>

<p>Grbovic, M., &amp; Cheng, H. (2018, July). Real-time personalization using embeddings for search ranking at airbnb. In Proceedings of the 24th ACM SIGKDD International Conference on Knowledge Discovery &amp; Data Mining (pp. 311-320). ACM.</p>

<p>Wu, L. Y., Fisch, A., Chopra, S., Adams, K., Bordes, A., &amp; Weston, J. (2018, April). Starspace: Embed all the things!. In Thirty-Second AAAI Conference on Artificial Intelligence.</p>

<p>Food Discovery with Uber Eats: Using Graph Learning to Power Recommendations, https://eng.uber.com/uber-eats-graph-learning/, retrieved 10 Jan 2020</p>

<p><strong>Thanks</strong> to <a href="https://www.linkedin.com/in/basilhan/" target="_blank">Basil Han</a> for suggesting bug fixes.</p>


            
            <br>
<p>If you found this useful, please cite this write-up as:</p>

<blockquote class="blockquote-citation">
    <p>Yan, Ziyou. (Jan 2020). Beating the Baseline Recommender with Graph & NLP in Pytorch. eugeneyan.com.
        https://eugeneyan.com/writing/recommender-systems-graph-and-nlp-pytorch/.</p>
</blockquote>

<p>or</p>

<div class="citation"><pre><code>@article{yan2020graph,
  title   = {Beating the Baseline Recommender with Graph & NLP in Pytorch},
  author  = {Yan, Ziyou},
  journal = {eugeneyan.com},
  year    = {2020},
  month   = {Jan},
  url     = {https://eugeneyan.com/writing/recommender-systems-graph-and-nlp-pytorch/}
}</code></pre>
</div>

            
            <br>
            

<style>
    #share-buttons {
        display: inline-block;
        vertical-align: middle;
    }

    #share-buttons:after {
        content: "";
        display: block;
        clear: both;
    }

    #share-buttons > div {
        position: relative;
        text-align: left;
        height: 36px;
        width: 32px;
        float: left;
        text-align: center;
    }

    #share-buttons > div > svg {
        height: 16px;
        fill: #808080;
        margin-top: 10px;
    }

    #share-buttons > div:hover {
        cursor: pointer;
    }
</style>

<span style="font-size: 18px">Share on:  </span>
<div id="share-buttons">
    <div class="twitter" title="Share this on Twitter" onclick="if (!window.__cfRLUnblockHandlers) return false; window.open('https://twitter.com/intent/tweet?text=Great read! Beating the Baseline Recommender with Graph & NLP in Pytorch&url=https://eugeneyan.com/writing/recommender-systems-graph-and-nlp-pytorch/&via=eugeneyan', 'pop-up', 'left=20,top=20,width=500,height=500,toolbar=1,resizable=0');" data-cf-modified-8508930d3962b87c0ce5ff16-="">
        <img class="icon about-icon-large" src="/assets/icon-twitter.svg" loading="lazy" alt=""/>
    </div>
    <div class="linkedin" title="Share this on Linkedin" onclick="if (!window.__cfRLUnblockHandlers) return false; window.open('https://www.linkedin.com/shareArticle?mini=true&url=https://eugeneyan.com/writing/recommender-systems-graph-and-nlp-pytorch/&source=eugeneyan.com', 'pop-up', 'left=20,top=20,width=500,height=500,toolbar=1,resizable=0');" data-cf-modified-8508930d3962b87c0ce5ff16-="">
        <img class="icon about-icon-large" src="/assets/icon-linkedin.svg" loading="lazy" alt=""/>
    </div>
    <div class="bluesky" title="Share this on Bluesky" onclick="if (!window.__cfRLUnblockHandlers) return false; window.open('https://bsky.app/intent/compose?text=Great read! Beating the Baseline Recommender with Graph & NLP in Pytorch https://eugeneyan.com/writing/recommender-systems-graph-and-nlp-pytorch/', 'pop-up', 'left=20,top=20,width=500,height=500,toolbar=1,resizable=0');" data-cf-modified-8508930d3962b87c0ce5ff16-="">
        <img class="icon about-icon-large" src="/assets/bluesky.svg" loading="lazy" alt=""/>
    </div>
    <div class="facebook fb-share-button" title="Share this on Facebook" onclick="if (!window.__cfRLUnblockHandlers) return false; window.open('https://www.facebook.com/dialog/share?app_id=249237293114028&display=popup&href=https://eugeneyan.com/writing/recommender-systems-graph-and-nlp-pytorch/&&redirect_uri=https://eugeneyan.com/writing/recommender-systems-graph-and-nlp-pytorch/', 'pop-up', 'left=20,top=20,width=500,height=500,toolbar=1,resizable=0');" data-cf-modified-8508930d3962b87c0ce5ff16-="">
        <img class="icon about-icon-large" src="/assets/icon-facebook.svg" loading="lazy" alt=""/>
    </div>
    <div class="mail" title="Share this through Email" onclick="if (!window.__cfRLUnblockHandlers) return false; window.open('mailto:?subject=Great read! Beating the Baseline Recommender with Graph & NLP in Pytorch&body=https://eugeneyan.com/writing/recommender-systems-graph-and-nlp-pytorch/');" data-cf-modified-8508930d3962b87c0ce5ff16-="">
        <img class="icon about-icon-large" src="/assets/icon-mail.svg" loading="lazy" alt=""/>
    </div>
</div>

        </div>
        <!-- Page navigation -->

        <hr>

        <div id="algolia-recs-container" style="display: none;">
          <div id="algolia-related-products" style="margin-bottom: 2em;"></div>
          <div id="algolia-fbt"></div>
          
<style>
  /* Common styles for both recommendation widgets */
  .algolia-recs-section-header {
    font-family: 'Raleway', Helvetica, sans-serif;
    font-size: 1em; /* Adjust as needed, smaller than default h3 */
    font-weight: bold;
    margin-top: 0; /* Remove or reduce top margin */
    margin-bottom: 15px; /* Space between header and recommendation cards */
    color: var(--c-text); /* Use theme's text color */
    font-style: italic;
  }

  /* Related Products Widget Styles */
  #algolia-related-products .ais-RelatedProducts-list {
    display: flex;
    flex-direction: row; /* Arrange items horizontally */
    flex-wrap: nowrap;   /* Prevent wrapping to new lines, if possible */
    justify-content: flex-start; /* Align items to the start of the container */
    padding-left: 0;     /* Remove default list padding */
    list-style-type: none; /* Remove list bullets */
    margin: 0;
  }

  #algolia-related-products .ais-RelatedProducts-item {
    width: 32%; /* Adjust for 3 items: 32% * 3 items + 2% * 2 margins = 100% */
    margin-right: 2%;
    box-sizing: border-box; /* Include padding and border in the element's total width */
    
    /* Optional: Basic card styling (uncomment to use) */
    border: 1px solid color-mix(in srgb, var(--c-background) 85%, var(--c-text) 15%); /* Theme-aware light grey border */
    padding: 0; /* Remove overall card padding, will be handled by elements */
    text-align: left; /* Or 'center' if you prefer */
    background-color: var(--c-background);
    border-radius: 4px;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  }

  #algolia-related-products .ais-RelatedProducts-item:last-child {
    margin-right: 0; /* No margin for the last item in the row */
  }

  /* Styling for images within recommendation items */
  #algolia-related-products .ais-RelatedProducts-item img {
    display: block;     /* Can help remove extra space below image */
    width: calc(100% - 4px); /* Full width minus 2px L/R margins */
    max-width: 100%;    /* Ensures image does not exceed container if intrinsically smaller */
    /* height: auto; -- Controlled by inline style's max-height and object-fit */
    object-fit: cover;  /* Ensure image covers the area, also in inline style */
    margin: 2px;        /* 2px margin on top, left, right. Bottom is overridden by inline style. */
    /* margin-bottom: 8px; -- This is set by inline style in JS template */
  }

  /* Styling for the wrapper link to make the whole card clickable */
  #algolia-related-products .ais-RelatedProducts-item a.ais-RelatedProducts-item-link-wrapper {
    display: block; /* Make the link fill the list item */
    text-decoration: none; /* Remove underline */
    color: inherit; /* Use parent's text color */
  }

  #algolia-related-products .related-product-title {
    font-family: 'Raleway', Helvetica, sans-serif;
    font-size: 0.75em;
    display: -webkit-box;
    -webkit-line-clamp: 2; /* Limit to 2 lines for WebKit browsers */
    line-clamp: 2; /* Standard property */
    -webkit-box-orient: vertical;  
    overflow: hidden;
    text-overflow: ellipsis;
    padding: 0 7px 7px 7px; /* 0 top, 7px L/R/B for text area */
    line-height: 1.5; /* Adjust for better readability */
    height: 3.3em; /* Current height: 3.3em. For 2 lines with 0.75em font & 1.5 line-height, calculated height would be 2.25em. */
    color: var(--c-interactive); /* Use theme's interactive color */
  }

  /* This container will wrap the image and score, taking the original image's layout space. */
  #algolia-related-products .ais-RelatedProducts-item .recommendation-image-container {
    position: relative; /* For positioning the score absolutely within */
    display: block; /* Matches original image display and ensures proper block layout */
    width: calc(100% - 4px); /* Adopts width from original image styling */
    margin: 2px;             /* Adopts margin from original image styling */
    margin-bottom: 8px;      /* Adopts specific bottom margin from original image's inline style */
    line-height: 0; /* Prevents unexpected space if child elements are treated as inline */
  }

  /* The image itself, now filling the container */
  #algolia-related-products .ais-RelatedProducts-item .recommendation-image-container img {
    display: block;
    width: 100%;       /* Fill the container's width */
    max-width: 20em;   /* Optional: retain original max-width constraint for the image content */
    height: auto;      /* Maintain aspect ratio by default */
    max-height: 12em;  /* Constrain image height (adjust as needed) */
    object-fit: cover; /* Ensures image covers the allocated space, cropping if necessary */
    margin: 0 auto;    /* Center image if max-width kicks in and it's narrower than container */
  }

  /* The score overlay box */
  #algolia-related-products .ais-RelatedProducts-item .recommendation-score {
    position: absolute;
    bottom: 3px;  /* Padding from the bottom edge of the container */
    right: 3px;   /* Padding from the right edge of the container */
    background-color: color-mix(in srgb, var(--c-background) 85%, var(--green) 15%); /* Theme-aware light green */
    color: var(--green); /* Theme's green color for text */
    padding: 3px 6px; /* Slightly adjusted padding */
    font-family: 'Raleway', Helvetica, sans-serif;
    font-size: 0.75em;
    font-weight: bold;
    border-radius: 10px; /* More rounded corners like the example */
    border: 1px solid var(--green); /* Theme's green color for border */
    line-height: 1; /* Critical for small text in a small box */
    z-index: 10; /* Ensure it's above the image */
    box-shadow: 0 1px 2px rgba(0,0,0,0.15); /* Softer shadow */
    display: flex; /* To align icon and text nicely */
    align-items: center; /* Vertically center icon and text */
  }

  /* Styling for the SVG icon within the score box */
  #algolia-related-products .ais-RelatedProducts-item .recommendation-score-icon {
    width: 0.9em; /* Scale with score's font size */
    height: 0.9em;
    vertical-align: -0.1em; /* Fine-tune vertical alignment */
    margin-right: 4px; /* Space between icon and score number */
    fill: var(--green); /* Theme's green color for icon */
  }

  /* Frequently Bought Together Widget Styles */
  #algolia-fbt .ais-FrequentlyBoughtTogether-list {
    display: flex;
    flex-direction: row; /* Arrange items horizontally */
    flex-wrap: nowrap;   /* Prevent wrapping to new lines, if possible */
    justify-content: flex-start; /* Align items to the start of the container */
    padding-left: 0;     /* Remove default list padding */
    list-style-type: none; /* Remove list bullets */
    margin: 0;
  }

  #algolia-fbt .ais-FrequentlyBoughtTogether-item {
    width: 32%; /* Adjust for 3 items: 32% * 3 items + 2% * 2 margins = 100% */
    margin-right: 2%;
    box-sizing: border-box; /* Include padding and border in the element's total width */
    
    /* Optional: Basic card styling (uncomment to use) */
    border: 1px solid color-mix(in srgb, var(--c-background) 85%, var(--c-text) 15%); /* Theme-aware light grey border */
    padding: 0; /* Remove overall card padding, will be handled by elements */
    text-align: left; /* Or 'center' if you prefer */
    background-color: var(--c-background);
    border-radius: 4px;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  }

  #algolia-fbt .ais-FrequentlyBoughtTogether-item:last-child {
    margin-right: 0; /* No margin for the last item in the row */
  }

  /* Styling for images within recommendation items */
  #algolia-fbt .ais-FrequentlyBoughtTogether-item img {
    display: block;     /* Can help remove extra space below image */
    width: calc(100% - 4px); /* Full width minus 2px L/R margins */
    max-width: 100%;    /* Ensures image does not exceed container if intrinsically smaller */
    /* height: auto; -- Controlled by inline style's max-height and object-fit */
    object-fit: cover;  /* Ensure image covers the area, also in inline style */
    margin: 2px;        /* 2px margin on top, left, right. Bottom is overridden by inline style. */
    /* margin-bottom: 8px; -- This is set by inline style in JS template */
  }

  /* Styling for the wrapper link to make the whole card clickable */
  #algolia-fbt .ais-FrequentlyBoughtTogether-item a.ais-FrequentlyBoughtTogether-item-link-wrapper {
    display: block; /* Make the link fill the list item */
    text-decoration: none; /* Remove underline */
    color: inherit; /* Use parent's text color */
  }

  #algolia-fbt .fbt-product-title {
    font-family: 'Raleway', Helvetica, sans-serif;
    font-size: 0.75em;
    display: -webkit-box;
    -webkit-line-clamp: 2; /* Limit to 2 lines for WebKit browsers */
    line-clamp: 2; /* Standard property */
    -webkit-box-orient: vertical;  
    overflow: hidden;
    text-overflow: ellipsis;
    padding: 0 7px 7px 7px; /* 0 top, 7px L/R/B for text area */
    line-height: 1.5; /* Adjust for better readability */
    height: 3.3em; /* Current height: 3.3em. For 2 lines with 0.75em font & 1.5 line-height, calculated height would be 2.25em. */
    color: var(--c-interactive); /* Use theme's interactive color */
  }

  /* This container will wrap the image and score, taking the original image's layout space. */
  #algolia-fbt .ais-FrequentlyBoughtTogether-item .recommendation-image-container {
    position: relative; /* For positioning the score absolutely within */
    display: block; /* Matches original image display and ensures proper block layout */
    width: calc(100% - 4px); /* Adopts width from original image styling */
    margin: 2px;             /* Adopts margin from original image styling */
    margin-bottom: 8px;      /* Adopts specific bottom margin from original image's inline style */
    line-height: 0; /* Prevents unexpected space if child elements are treated as inline */
  }

  /* The image itself, now filling the container */
  #algolia-fbt .ais-FrequentlyBoughtTogether-item .recommendation-image-container img {
    display: block;
    width: 100%;       /* Fill the container's width */
    max-width: 20em;   /* Optional: retain original max-width constraint for the image content */
    height: auto;      /* Maintain aspect ratio by default */
    max-height: 12em;  /* Constrain image height (adjust as needed) */
    object-fit: cover; /* Ensures image covers the allocated space, cropping if necessary */
    margin: 0 auto;    /* Center image if max-width kicks in and it's narrower than container */
  }

  /* The score overlay box */
  #algolia-fbt .ais-FrequentlyBoughtTogether-item .recommendation-score {
    position: absolute;
    bottom: 3px;  /* Padding from the bottom edge of the container */
    right: 3px;   /* Padding from the right edge of the container */
    background-color: color-mix(in srgb, var(--c-background) 85%, var(--green) 15%); /* Theme-aware light green */
    color: var(--green); /* Theme's green color for text */
    padding: 3px 6px; /* Slightly adjusted padding */
    font-family: 'Raleway', Helvetica, sans-serif;
    font-size: 0.75em;
    font-weight: bold;
    border-radius: 10px; /* More rounded corners like the example */
    border: 1px solid var(--green); /* Theme's green color for border */
    line-height: 1; /* Critical for small text in a small box */
    z-index: 10; /* Ensure it's above the image */
    box-shadow: 0 1px 2px rgba(0,0,0,0.15); /* Softer shadow */
    display: flex; /* To align icon and text nicely */
    align-items: center; /* Vertically center icon and text */
  }

  /* Styling for the SVG icon within the score box */
  #algolia-fbt .ais-FrequentlyBoughtTogether-item .recommendation-score-icon {
    width: 0.9em; /* Scale with score's font size */
    height: 0.9em;
    vertical-align: -0.1em; /* Fine-tune vertical alignment */
    margin-right: 4px; /* Space between icon and score number */
    fill: var(--green); /* Theme's green color for icon */
  }
</style>
<script type="8508930d3962b87c0ce5ff16-text/javascript">
  // Function to load a script and return a promise
  function loadScript(src) {
    return new Promise((resolve, reject) => {
      if (document.querySelector(`script[src="${src}"]`)) {
        resolve(); // Already loaded
        return;
      }
      const script = document.createElement('script');
      script.src = src;
      script.onload = () => resolve();
      script.onerror = () => reject(new Error(`Script load error for ${src}`));
      document.head.appendChild(script);
    });
  }

  function initAlgoliaRecommendations() {
    Promise.all([
      loadScript('https://cdn.jsdelivr.net/npm/algoliasearch@4/dist/algoliasearch-lite.umd.js'),
      loadScript('https://cdn.jsdelivr.net/npm/instantsearch.js@4')
    ])
    .then(() => {
      // Initialize the Related Products widget
      initRelatedProducts();
      
      // Initialize the Frequently Bought Together widget
      initFrequentlyBoughtTogether();
    })
    .catch(error => {
      // Handle error silently
    });
  }

  function initRelatedProducts() {
    // Ensure container exists
    if (!document.getElementById('algolia-related-products')) {
      // Container not found, stopping initialization
      return;
    }

    const recSearchClient = algoliasearch(
      '2XJCLEABQD',
      'b61ec4cb64bd32d62c053466fccbfa43'
    );

    const relatedSearch = instantsearch({
      indexName: 'eugeneyan.com',
      searchClient: recSearchClient,
      clickAnalytics: true,
      insights: true, // Enable insights for click tracking on recommendations
    });

    relatedSearch.addWidgets([
      instantsearch.widgets.relatedProducts({
        container: '#algolia-related-products',
        objectIDs: ['/writing/recommender-systems-graph-and-nlp-pytorch/'],
        limit: 3,
        queryParameters: {
          attributesToRetrieve: ['title', 'url', 'image', 'score', '_score'], // Specify only needed attributes
          attributesToHighlight: [], // Disable highlighting
          attributesToSnippet: []    // Disable snippeting
        },
        translations: {
          title: '', // Custom title is in _layouts/post.html
        },
        transformItems: function(items) {

          
          const containerElement = document.getElementById('algolia-recs-container');
          const relatedElement = document.getElementById('algolia-related-products');
          
          // Hide the Related Products container element first if there are no items
          if (items.length === 0 && relatedElement) {
            relatedElement.style.display = 'none';

          } else if (relatedElement) {
            relatedElement.style.display = 'block';
          }
          
          // Then handle the main recommendations container visibility
          if (items.length > 0 && containerElement) {
            containerElement.style.display = 'block';

          } else if (containerElement && !document.getElementById('algolia-fbt').hasChildNodes()) {
            containerElement.style.display = 'none';

          } else {

          }
          return items;
        },
        templates: {
          header() { // Removed unused results, html parameters

            // Return a PLAIN string for the header
            return '<h4 class="algolia-recs-section-header">You Might Also Like (content-based)</h4>';
          },
          item: function(hit, { html, sendEvent }) { // Added sendEvent to params

            const itemUrl = `${hit.url || '#'}`;
            const indexName = 'eugeneyan.com'; // Get index name for insights

            let imageUrl;
            // Ensure hit.image is not null, undefined, or an empty/whitespace string before using it.
            if (hit.image && typeof hit.image === 'string' && hit.image.trim() !== '') {
              imageUrl = `/assets/og_image/${hit.image}`;
            } else {
              imageUrl = `/assets/og_image/default-v4.jpg`; // Default image
            }

            let scoreValue = null;
            if (typeof hit.score === 'number') scoreValue = hit.score.toFixed(2);
            else if (typeof hit._score === 'number') scoreValue = hit._score.toFixed(2);
            // else if (typeof hit.your_custom_score_field === 'number') scoreValue = hit.your_custom_score_field.toFixed(2);

            const scoreElement = scoreValue ? `<div class="recommendation-score"><svg viewbox="0 0 24 24" class="recommendation-score-icon" xmlns="http://www.w3.org/2000/svg"><path d="M16 6l2.29 2.29-4.88 4.88-4-4L2 16.59 3.41 18l6-6 4 4 6.3-6.29L22 12V6h-6z"></path></svg>${scoreValue}</div>` : '';

            const imageAndScoreTag = `
              <div class="recommendation-image-container">
                <img src="${imageUrl}" alt="${hit.title || 'Recommendation cover image'}">
                ${scoreElement}
              </div>`;

            const title = hit.title || 'Untitled Post';

            return `
              <a href="${itemUrl}"
                 class="ais-RelatedProducts-item-link-wrapper"
                 onClick="${() => { 
                  sendEvent('click', hit, 'Related Item Clicked');
                 }}"
              >
                ${imageAndScoreTag}
                <div class="related-product-title">${title}</div>
              </a>
            `;
          },
          empty(results, { html }) {

            const containerElement = document.getElementById('algolia-recs-container');
            const relatedElement = document.getElementById('algolia-related-products');
            
            // Hide Related Products container since there are no results
            if (relatedElement) {
              relatedElement.style.display = 'none';

            }
            
            // Check if the FBT widget also has no items before hiding the main container
            const fbtWidgetContainer = document.getElementById('algolia-fbt');
            if (containerElement && (!fbtWidgetContainer || !fbtWidgetContainer.hasChildNodes())) {
              containerElement.style.display = 'none';

            }
            
            // Return an empty string to prevent rendering 'undefined'
            return '';
          }
        }
      })
    ]);

    relatedSearch.start();
  }

  function initFrequentlyBoughtTogether() {
    // Ensure container exists
    if (!document.getElementById('algolia-fbt')) {
      // Container not found, stopping initialization
      return;
    }

    const recSearchClient = algoliasearch(
      '2XJCLEABQD',
      'b61ec4cb64bd32d62c053466fccbfa43'
    );

    const fbtSearch = instantsearch({
      indexName: 'eugeneyan.com',
      searchClient: recSearchClient,
      clickAnalytics: true,
      insights: true, // Enable insights for click tracking on recommendations
    });

    fbtSearch.addWidgets([
      instantsearch.widgets.frequentlyBoughtTogether({
        container: '#algolia-fbt',
        objectIDs: ['/writing/recommender-systems-graph-and-nlp-pytorch/'],
        limit: 3,
        queryParameters: {
          attributesToRetrieve: ['title', 'url', 'image', 'score', '_score'], // Specify only needed attributes
          attributesToHighlight: [], // Disable highlighting
          attributesToSnippet: []    // Disable snippeting
        },
        translations: {
          title: '', // Custom title is in _layouts/post.html
        },
        transformItems: function(items) {

          
          const containerElement = document.getElementById('algolia-recs-container');
          if (items.length > 0 && containerElement) {
            containerElement.style.display = 'block';

          } else if (containerElement && !document.getElementById('algolia-related-products').hasChildNodes()) {
            containerElement.style.display = 'none';

          } else {

          }
          return items;
        },
        templates: {
          header() { 

            // Return a PLAIN string for the header - only if we have items to display
            // The header should not render if there's no content
            return '<h4 class="algolia-recs-section-header">Frequently Read Together (behavioral-based)</h4>';
          },
          item: function(hit, { html, sendEvent }) { 

            const itemUrl = `${hit.url || '#'}`;
            const indexName = 'eugeneyan.com'; // Get index name for insights

            let imageUrl;
            // Ensure hit.image is not null, undefined, or an empty/whitespace string before using it.
            if (hit.image && typeof hit.image === 'string' && hit.image.trim() !== '') {
              imageUrl = `/assets/og_image/${hit.image}`;
            } else {
              imageUrl = `/assets/og_image/default-v4.jpg`; // Default image
            }

            let scoreValue = null;
            if (typeof hit.score === 'number') scoreValue = hit.score.toFixed(2);
            else if (typeof hit._score === 'number') scoreValue = hit._score.toFixed(2);
            // else if (typeof hit.your_custom_score_field === 'number') scoreValue = hit.your_custom_score_field.toFixed(2);

            const scoreElement = scoreValue ? `<div class="recommendation-score"><svg viewbox="0 0 24 24" class="recommendation-score-icon" xmlns="http://www.w3.org/2000/svg"><path d="M16 6l2.29 2.29-4.88 4.88-4-4L2 16.59 3.41 18l6-6 4 4 6.3-6.29L22 12V6h-6z"></path></svg>${scoreValue}</div>` : '';

            const imageAndScoreTag = `
              <div class="recommendation-image-container">
                <img src="${imageUrl}" alt="${hit.title || 'Recommendation cover image'}">
                ${scoreElement}
              </div>`;

            const title = hit.title || 'Untitled Post';

            return `
              <a href="${itemUrl}"
                 class="ais-FrequentlyBoughtTogether-item-link-wrapper"
                 onClick="${() => { 
                  sendEvent('click', hit, 'FBT Item Clicked'); 
                 }}"
              >
                ${imageAndScoreTag}
                <div class="fbt-product-title">${title}</div>
              </a>
            `;
          },
          empty(results, { html }) {

            const containerElement = document.getElementById('algolia-recs-container');
            const fbtElement = document.getElementById('algolia-fbt');
            
            // Hide FBT container since there are no results
            if (fbtElement) {
              fbtElement.style.display = 'none';

            }
            
            // Check if the Related Products widget also has no items before hiding the main container
            const relatedWidgetContainer = document.getElementById('algolia-related-products');
            if (containerElement && (!relatedWidgetContainer || !relatedWidgetContainer.hasChildNodes())) {
              containerElement.style.display = 'none';


            }
            
            // Return an empty string to prevent rendering 'undefined'
            return '';
          }
        }
      })
    ]);

    fbtSearch.start();
  }

  document.addEventListener('DOMContentLoaded', function() {
    let recsLoaded = false;
    function checkLoad() {
      if (recsLoaded) return;
      if ((window.scrollY + window.innerHeight) >= document.body.scrollHeight - 500) {
        recsLoaded = true;
        window.removeEventListener('scroll', checkLoad);
        initAlgoliaRecommendations();
      }
    }
    window.addEventListener('scroll', checkLoad, { passive: true });
    checkLoad();
  });
</script>


        </div>
        <br>

        <!-- <div id="algolia-recs-container" style="display: none;">
            <div id="algolia-related-products" style="margin-bottom: 2em;"></div>
            
<style>
  /* Common styles for both recommendation widgets */
  .algolia-recs-section-header {
    font-family: 'Raleway', Helvetica, sans-serif;
    font-size: 1em; /* Adjust as needed, smaller than default h3 */
    font-weight: bold;
    margin-top: 0; /* Remove or reduce top margin */
    margin-bottom: 15px; /* Space between header and recommendation cards */
    color: var(--c-text); /* Use theme's text color */
    font-style: italic;
  }

  /* Related Products Widget Styles */
  #algolia-related-products .ais-RelatedProducts-list {
    display: flex;
    flex-direction: row; /* Arrange items horizontally */
    flex-wrap: nowrap;   /* Prevent wrapping to new lines, if possible */
    justify-content: flex-start; /* Align items to the start of the container */
    padding-left: 0;     /* Remove default list padding */
    list-style-type: none; /* Remove list bullets */
    margin: 0;
  }

  #algolia-related-products .ais-RelatedProducts-item {
    width: 32%; /* Adjust for 3 items: 32% * 3 items + 2% * 2 margins = 100% */
    margin-right: 2%;
    box-sizing: border-box; /* Include padding and border in the element's total width */
    
    /* Optional: Basic card styling (uncomment to use) */
    border: 1px solid color-mix(in srgb, var(--c-background) 85%, var(--c-text) 15%); /* Theme-aware light grey border */
    padding: 0; /* Remove overall card padding, will be handled by elements */
    text-align: left; /* Or 'center' if you prefer */
    background-color: var(--c-background);
    border-radius: 4px;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  }

  #algolia-related-products .ais-RelatedProducts-item:last-child {
    margin-right: 0; /* No margin for the last item in the row */
  }

  /* Styling for images within recommendation items */
  #algolia-related-products .ais-RelatedProducts-item img {
    display: block;     /* Can help remove extra space below image */
    width: calc(100% - 4px); /* Full width minus 2px L/R margins */
    max-width: 100%;    /* Ensures image does not exceed container if intrinsically smaller */
    /* height: auto; -- Controlled by inline style's max-height and object-fit */
    object-fit: cover;  /* Ensure image covers the area, also in inline style */
    margin: 2px;        /* 2px margin on top, left, right. Bottom is overridden by inline style. */
    /* margin-bottom: 8px; -- This is set by inline style in JS template */
  }

  /* Styling for the wrapper link to make the whole card clickable */
  #algolia-related-products .ais-RelatedProducts-item a.ais-RelatedProducts-item-link-wrapper {
    display: block; /* Make the link fill the list item */
    text-decoration: none; /* Remove underline */
    color: inherit; /* Use parent's text color */
  }

  #algolia-related-products .related-product-title {
    font-family: 'Raleway', Helvetica, sans-serif;
    font-size: 0.75em;
    display: -webkit-box;
    -webkit-line-clamp: 2; /* Limit to 2 lines for WebKit browsers */
    line-clamp: 2; /* Standard property */
    -webkit-box-orient: vertical;  
    overflow: hidden;
    text-overflow: ellipsis;
    padding: 0 7px 7px 7px; /* 0 top, 7px L/R/B for text area */
    line-height: 1.5; /* Adjust for better readability */
    height: 3.3em; /* Current height: 3.3em. For 2 lines with 0.75em font & 1.5 line-height, calculated height would be 2.25em. */
    color: var(--c-interactive); /* Use theme's interactive color */
  }

  /* This container will wrap the image and score, taking the original image's layout space. */
  #algolia-related-products .ais-RelatedProducts-item .recommendation-image-container {
    position: relative; /* For positioning the score absolutely within */
    display: block; /* Matches original image display and ensures proper block layout */
    width: calc(100% - 4px); /* Adopts width from original image styling */
    margin: 2px;             /* Adopts margin from original image styling */
    margin-bottom: 8px;      /* Adopts specific bottom margin from original image's inline style */
    line-height: 0; /* Prevents unexpected space if child elements are treated as inline */
  }

  /* The image itself, now filling the container */
  #algolia-related-products .ais-RelatedProducts-item .recommendation-image-container img {
    display: block;
    width: 100%;       /* Fill the container's width */
    max-width: 20em;   /* Optional: retain original max-width constraint for the image content */
    height: auto;      /* Maintain aspect ratio by default */
    max-height: 12em;  /* Constrain image height (adjust as needed) */
    object-fit: cover; /* Ensures image covers the allocated space, cropping if necessary */
    margin: 0 auto;    /* Center image if max-width kicks in and it's narrower than container */
  }

  /* The score overlay box */
  #algolia-related-products .ais-RelatedProducts-item .recommendation-score {
    position: absolute;
    bottom: 3px;  /* Padding from the bottom edge of the container */
    right: 3px;   /* Padding from the right edge of the container */
    background-color: color-mix(in srgb, var(--c-background) 85%, var(--green) 15%); /* Theme-aware light green */
    color: var(--green); /* Theme's green color for text */
    padding: 3px 6px; /* Slightly adjusted padding */
    font-family: 'Raleway', Helvetica, sans-serif;
    font-size: 0.75em;
    font-weight: bold;
    border-radius: 10px; /* More rounded corners like the example */
    border: 1px solid var(--green); /* Theme's green color for border */
    line-height: 1; /* Critical for small text in a small box */
    z-index: 10; /* Ensure it's above the image */
    box-shadow: 0 1px 2px rgba(0,0,0,0.15); /* Softer shadow */
    display: flex; /* To align icon and text nicely */
    align-items: center; /* Vertically center icon and text */
  }

  /* Styling for the SVG icon within the score box */
  #algolia-related-products .ais-RelatedProducts-item .recommendation-score-icon {
    width: 0.9em; /* Scale with score's font size */
    height: 0.9em;
    vertical-align: -0.1em; /* Fine-tune vertical alignment */
    margin-right: 4px; /* Space between icon and score number */
    fill: var(--green); /* Theme's green color for icon */
  }

</style>
<script>
  // Function to load a script and return a promise
  function loadScript(src) {
    return new Promise((resolve, reject) => {
      if (document.querySelector(`script[src="${src}"]`)) {
        resolve(); // Already loaded
        return;
      }
      const script = document.createElement('script');
      script.src = src;
      script.onload = () => resolve();
      script.onerror = () => reject(new Error(`Script load error for ${src}`));
      document.head.appendChild(script);
    });
  }

  function initAlgoliaRecommendations() {
    Promise.all([
      loadScript('https://cdn.jsdelivr.net/npm/algoliasearch@4/dist/algoliasearch-lite.umd.js'),
      loadScript('https://cdn.jsdelivr.net/npm/instantsearch.js@4')
    ])
    .then(() => {
      // Initialize the Related Products widget
      initRelatedProducts();
      
    })
    .catch(error => {
      // Handle error silently
    });
  }

  function initRelatedProducts() {
    // Ensure container exists
    if (!document.getElementById('algolia-related-products')) {
      // Container not found, stopping initialization
      return;
    }

    const recSearchClient = algoliasearch(
      '2XJCLEABQD',
      'b61ec4cb64bd32d62c053466fccbfa43'
    );

    const relatedSearch = instantsearch({
      indexName: 'eugeneyan.com',
      searchClient: recSearchClient,
      clickAnalytics: true,
      insights: true, // Enable insights for click tracking on recommendations
    });

    relatedSearch.addWidgets([
      instantsearch.widgets.relatedProducts({
        container: '#algolia-related-products',
        objectIDs: ['/writing/recommender-systems-graph-and-nlp-pytorch/'],
        limit: 3,
        queryParameters: {
          attributesToRetrieve: ['title', 'url', 'image', 'score', '_score'], // Specify only needed attributes
          attributesToHighlight: [], // Disable highlighting
          attributesToSnippet: []    // Disable snippeting
        },
        translations: {
          title: '', // Custom title is in _layouts/post.html
        },
        transformItems: function(items) {
          const containerElement = document.getElementById('algolia-recs-container');
          const relatedElement = document.getElementById('algolia-related-products');

          if (items.length === 0) {
            if (relatedElement) relatedElement.style.display = 'none';
            if (containerElement) containerElement.style.display = 'none';
          } else {
            if (relatedElement) relatedElement.style.display = 'block';
            if (containerElement) containerElement.style.display = 'block';
          }
          return items;
        },
        templates: {
          header() { // Removed unused results, html parameters

            // Return a PLAIN string for the header
            return '<h4 class="algolia-recs-section-header">You Might Also Like</h4>';
          },
          item: function(hit, { html, sendEvent }) { // Added sendEvent to params

            const itemUrl = `${hit.url || '#'}`;
            const indexName = 'eugeneyan.com'; // Get index name for insights

            let imageUrl;
            // Ensure hit.image is not null, undefined, or an empty/whitespace string before using it.
            if (hit.image && typeof hit.image === 'string' && hit.image.trim() !== '') {
              imageUrl = `/assets/og_image/${hit.image}`;
            } else {
              imageUrl = `/assets/og_image/default-v4.jpg`; // Default image
            }

            let scoreValue = null;
            if (typeof hit.score === 'number') scoreValue = hit.score.toFixed(2);
            else if (typeof hit._score === 'number') scoreValue = hit._score.toFixed(2);
            // else if (typeof hit.your_custom_score_field === 'number') scoreValue = hit.your_custom_score_field.toFixed(2);

            const scoreElement = scoreValue ? `<div class="recommendation-score"><svg viewbox="0 0 24 24" class="recommendation-score-icon" xmlns="http://www.w3.org/2000/svg"><path d="M16 6l2.29 2.29-4.88 4.88-4-4L2 16.59 3.41 18l6-6 4 4 6.3-6.29L22 12V6h-6z"></path></svg>${scoreValue}</div>` : '';

            const imageAndScoreTag = `
              <div class="recommendation-image-container">
                <img src="${imageUrl}" alt="${hit.title || 'Recommendation cover image'}">
                ${scoreElement}
              </div>`;

            const title = hit.title || 'Untitled Post';

            return `
              <a href="${itemUrl}"
                 class="ais-RelatedProducts-item-link-wrapper"
                 onClick="${() => { 
                  sendEvent('click', hit, 'Related Item Clicked');
                 }}"
              >
                ${imageAndScoreTag}
                <div class="related-product-title">${title}</div>
              </a>
            `;
          },
          empty(results, { html }) {
            const containerElement = document.getElementById('algolia-recs-container');
            const relatedElement = document.getElementById('algolia-related-products');
            
            if (relatedElement) {
              relatedElement.style.display = 'none';
            }
            // If this 'empty' template is called, it means related products are empty.
            // So, the main container should also be hidden.
            if (containerElement) {
              containerElement.style.display = 'none';
            }
            
            // Return an empty string to prevent rendering 'undefined'
            return '';
          }
        }
      })
    ]);

    relatedSearch.start();
  }


  document.addEventListener('DOMContentLoaded', function() {
    let recsLoaded = false;
    function checkLoad() {
      if (recsLoaded) return;
      if ((window.scrollY + window.innerHeight) >= document.body.scrollHeight - 500) {
        recsLoaded = true;
        window.removeEventListener('scroll', checkLoad);
        initAlgoliaRecommendations();
      }
    }
    window.addEventListener('scroll', checkLoad, { passive: true });
    checkLoad();
  });
</script>


        </div> -->

        <span style="font-family: 'Raleway', Helvetica, sans-serif;">Browse related tags:</span> <span class="no-italics">[
        
        
        <a class='tag' href="/tag/recsys/">recsys</a>
        
        
        <a class='tag' href="/tag/deeplearning/">deeplearning</a>
        
        
        <a class='tag' href="/tag/python/">python</a>
        
        
        <a class='tag' href="/tag/🛠/">🛠</a>
        
    ]
</span> <span style="font-family: 'Raleway', Helvetica, sans-serif;"> or </span><a href="/search/" title="Search" style="text-decoration: none; font-family: 'Raleway', Helvetica, sans-serif;"><img class="icon icon-search" src="/assets/icon-search.svg" loading="lazy" alt="" style="vertical-align: middle; margin-right: 0.25em;"/>Search</a>
        <div class="PageNavigation">
    
    <a class="prev sans-serif" href="/writing/recommender-systems-baseline-pytorch/">&laquo; Building a Strong Baseline Recommender in PyTorch, on a Laptop</a>
    
    
    <a class="next sans-serif" href="/speaking/recommender-systems-beyond-the-baseline-talk/">DataScience SG Meetup - RecSys, Beyond the Baseline &raquo;</a>
    
</div>

        <hr>

        <p style="font-size: 15px; text-align: center; margin: 2em 0 0.5em">Join <b>11,800+</b> readers getting updates on machine learning, RecSys, LLMs, and engineering.</p>
<script src="https://f.convertkit.com/ckjs/ck.6.js" type="8508930d3962b87c0ce5ff16-text/javascript"></script>
<form action="https://app.convertkit.com/forms/4004980/subscriptions" class="seva-form formkit-form" method="post" data-sv-form="4004980" data-uid="96a310b6ce" data-format="inline" data-version="6" data-options="{&quot;settings&quot;:{&quot;after_subscribe&quot;:{&quot;action&quot;:&quot;message&quot;,&quot;success_message&quot;:&quot;Just sent a confirmation! Check your inbox.&quot;,&quot;redirect_url&quot;:&quot;&quot;},&quot;analytics&quot;:{&quot;google&quot;:null,&quot;fathom&quot;:null,&quot;facebook&quot;:null,&quot;segment&quot;:null,&quot;pinterest&quot;:null,&quot;sparkloop&quot;:null,&quot;googletagmanager&quot;:null},&quot;modal&quot;:{&quot;trigger&quot;:&quot;timer&quot;,&quot;scroll_percentage&quot;:null,&quot;timer&quot;:5,&quot;devices&quot;:&quot;all&quot;,&quot;show_once_every&quot;:15},&quot;powered_by&quot;:{&quot;show&quot;:false,&quot;url&quot;:&quot;https://convertkit.com/features/forms?utm_campaign=poweredby&amp;utm_content=form&amp;utm_medium=referral&amp;utm_source=dynamic&quot;},&quot;recaptcha&quot;:{&quot;enabled&quot;:false},&quot;return_visitor&quot;:{&quot;action&quot;:&quot;show&quot;,&quot;custom_content&quot;:&quot;&quot;},&quot;slide_in&quot;:{&quot;display_in&quot;:&quot;bottom_right&quot;,&quot;trigger&quot;:&quot;timer&quot;,&quot;scroll_percentage&quot;:null,&quot;timer&quot;:5,&quot;devices&quot;:&quot;all&quot;,&quot;show_once_every&quot;:15},&quot;sticky_bar&quot;:{&quot;display_in&quot;:&quot;top&quot;,&quot;trigger&quot;:&quot;timer&quot;,&quot;scroll_percentage&quot;:null,&quot;timer&quot;:5,&quot;devices&quot;:&quot;all&quot;,&quot;show_once_every&quot;:15}},&quot;version&quot;:&quot;6&quot;}" min-width="400 500 600 700 800"><div data-style="clean"><ul class="formkit-alert formkit-alert-error" data-element="errors" data-group="alert"></ul><div data-element="fields" data-stacked="false" class="seva-fields formkit-fields"><div class="formkit-field"><input class="formkit-input" name="email_address" style="color: rgb(0, 0, 0); border-color: rgb(227, 227, 227); border-radius: 4px; font-weight: 400;" aria-label="Your email address..." placeholder="Your email address..." required="" type="email"></div><button data-element="submit" class="formkit-submit formkit-submit" style="color: rgb(255, 255, 255); background-color: rgb(0, 123, 255); border-radius: 5px; font-weight: 400;"><div class="formkit-spinner"><div></div><div></div><div></div></div><span class="">Get email updates</span></button></div></div><style>.formkit-form[data-uid="96a310b6ce"] *{box-sizing:border-box;}.formkit-form[data-uid="96a310b6ce"]{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;}.formkit-form[data-uid="96a310b6ce"] legend{border:none;font-size:inherit;margin-bottom:10px;padding:0;position:relative;display:table;}.formkit-form[data-uid="96a310b6ce"] fieldset{border:0;padding:0.01em 0 0 0;margin:0;min-width:0;}.formkit-form[data-uid="96a310b6ce"] body:not(:-moz-handler-blocked) fieldset{display:table-cell;}.formkit-form[data-uid="96a310b6ce"] h1,.formkit-form[data-uid="96a310b6ce"] h2,.formkit-form[data-uid="96a310b6ce"] h3,.formkit-form[data-uid="96a310b6ce"] h4,.formkit-form[data-uid="96a310b6ce"] h5,.formkit-form[data-uid="96a310b6ce"] h6{color:inherit;font-size:inherit;font-weight:inherit;}.formkit-form[data-uid="96a310b6ce"] h2{font-size:1.5em;margin:1em 0;}.formkit-form[data-uid="96a310b6ce"] h3{font-size:1.17em;margin:1em 0;}.formkit-form[data-uid="96a310b6ce"] p{color:inherit;font-size:inherit;font-weight:inherit;}.formkit-form[data-uid="96a310b6ce"] ol:not([template-default]),.formkit-form[data-uid="96a310b6ce"] ul:not([template-default]),.formkit-form[data-uid="96a310b6ce"] blockquote:not([template-default]){text-align:left;}.formkit-form[data-uid="96a310b6ce"] p:not([template-default]),.formkit-form[data-uid="96a310b6ce"] hr:not([template-default]),.formkit-form[data-uid="96a310b6ce"] blockquote:not([template-default]),.formkit-form[data-uid="96a310b6ce"] ol:not([template-default]),.formkit-form[data-uid="96a310b6ce"] ul:not([template-default]){color:inherit;font-style:initial;}.formkit-form[data-uid="96a310b6ce"] .ordered-list,.formkit-form[data-uid="96a310b6ce"] .unordered-list{list-style-position:outside !important;padding-left:1em;}.formkit-form[data-uid="96a310b6ce"] .list-item{padding-left:0;}.formkit-form[data-uid="96a310b6ce"][data-format="modal"]{display:none;}.formkit-form[data-uid="96a310b6ce"][data-format="slide in"]{display:none;}.formkit-form[data-uid="96a310b6ce"][data-format="sticky bar"]{display:none;}.formkit-sticky-bar .formkit-form[data-uid="96a310b6ce"][data-format="sticky bar"]{display:block;}.formkit-form[data-uid="96a310b6ce"] .formkit-input,.formkit-form[data-uid="96a310b6ce"] .formkit-select,.formkit-form[data-uid="96a310b6ce"] .formkit-checkboxes{width:100%;}.formkit-form[data-uid="96a310b6ce"] .formkit-button,.formkit-form[data-uid="96a310b6ce"] .formkit-submit{border:0;border-radius:5px;color:#ffffff;cursor:pointer;display:inline-block;text-align:center;font-size:15px;font-weight:500;cursor:pointer;margin-bottom:15px;overflow:hidden;padding:0;position:relative;vertical-align:middle;}.formkit-form[data-uid="96a310b6ce"] .formkit-button:hover,.formkit-form[data-uid="96a310b6ce"] .formkit-submit:hover,.formkit-form[data-uid="96a310b6ce"] .formkit-button:focus,.formkit-form[data-uid="96a310b6ce"] .formkit-submit:focus{outline:none;}.formkit-form[data-uid="96a310b6ce"] .formkit-button:hover > span,.formkit-form[data-uid="96a310b6ce"] .formkit-submit:hover > span,.formkit-form[data-uid="96a310b6ce"] .formkit-button:focus > span,.formkit-form[data-uid="96a310b6ce"] .formkit-submit:focus > span{background-color:rgba(0,0,0,0.1);}.formkit-form[data-uid="96a310b6ce"] .formkit-button > span,.formkit-form[data-uid="96a310b6ce"] .formkit-submit > span{display:block;-webkit-transition:all 300ms ease-in-out;transition:all 300ms ease-in-out;padding:12px 24px;}.formkit-form[data-uid="96a310b6ce"] .formkit-input{background:#ffffff;font-size:15px;padding:12px;border:1px solid #e3e3e3;-webkit-flex:1 0 auto;-ms-flex:1 0 auto;flex:1 0 auto;line-height:1.4;margin:0;-webkit-transition:border-color ease-out 300ms;transition:border-color ease-out 300ms;}.formkit-form[data-uid="96a310b6ce"] .formkit-input:focus{outline:none;border-color:#1677be;-webkit-transition:border-color ease 300ms;transition:border-color ease 300ms;}.formkit-form[data-uid="96a310b6ce"] .formkit-input::-webkit-input-placeholder{color:inherit;opacity:0.8;}.formkit-form[data-uid="96a310b6ce"] .formkit-input::-moz-placeholder{color:inherit;opacity:0.8;}.formkit-form[data-uid="96a310b6ce"] .formkit-input:-ms-input-placeholder{color:inherit;opacity:0.8;}.formkit-form[data-uid="96a310b6ce"] .formkit-input::placeholder{color:inherit;opacity:0.8;}.formkit-form[data-uid="96a310b6ce"] [data-group="dropdown"]{position:relative;display:inline-block;width:100%;}.formkit-form[data-uid="96a310b6ce"] [data-group="dropdown"]::before{content:"";top:calc(50% - 2.5px);right:10px;position:absolute;pointer-events:none;border-color:#4f4f4f transparent transparent transparent;border-style:solid;border-width:6px 6px 0 6px;height:0;width:0;z-index:999;}.formkit-form[data-uid="96a310b6ce"] [data-group="dropdown"] select{height:auto;width:100%;cursor:pointer;color:#333333;line-height:1.4;margin-bottom:0;padding:0 6px;-webkit-appearance:none;-moz-appearance:none;appearance:none;font-size:15px;padding:12px;padding-right:25px;border:1px solid #e3e3e3;background:#ffffff;}.formkit-form[data-uid="96a310b6ce"] [data-group="dropdown"] select:focus{outline:none;}.formkit-form[data-uid="96a310b6ce"] [data-group="checkboxes"]{text-align:left;margin:0;}.formkit-form[data-uid="96a310b6ce"] [data-group="checkboxes"] [data-group="checkbox"]{margin-bottom:10px;}.formkit-form[data-uid="96a310b6ce"] [data-group="checkboxes"] [data-group="checkbox"] *{cursor:pointer;}.formkit-form[data-uid="96a310b6ce"] [data-group="checkboxes"] [data-group="checkbox"]:last-of-type{margin-bottom:0;}.formkit-form[data-uid="96a310b6ce"] [data-group="checkboxes"] [data-group="checkbox"] input[type="checkbox"]{display:none;}.formkit-form[data-uid="96a310b6ce"] [data-group="checkboxes"] [data-group="checkbox"] input[type="checkbox"] + label::after{content:none;}.formkit-form[data-uid="96a310b6ce"] [data-group="checkboxes"] [data-group="checkbox"] input[type="checkbox"]:checked + label::after{border-color:#ffffff;content:"";}.formkit-form[data-uid="96a310b6ce"] [data-group="checkboxes"] [data-group="checkbox"] input[type="checkbox"]:checked + label::before{background:#10bf7a;border-color:#10bf7a;}.formkit-form[data-uid="96a310b6ce"] [data-group="checkboxes"] [data-group="checkbox"] label{position:relative;display:inline-block;padding-left:28px;}.formkit-form[data-uid="96a310b6ce"] [data-group="checkboxes"] [data-group="checkbox"] label::before,.formkit-form[data-uid="96a310b6ce"] [data-group="checkboxes"] [data-group="checkbox"] label::after{position:absolute;content:"";display:inline-block;}.formkit-form[data-uid="96a310b6ce"] [data-group="checkboxes"] [data-group="checkbox"] label::before{height:16px;width:16px;border:1px solid #e3e3e3;background:#ffffff;left:0px;top:3px;}.formkit-form[data-uid="96a310b6ce"] [data-group="checkboxes"] [data-group="checkbox"] label::after{height:4px;width:8px;border-left:2px solid #4d4d4d;border-bottom:2px solid #4d4d4d;-webkit-transform:rotate(-45deg);-ms-transform:rotate(-45deg);transform:rotate(-45deg);left:4px;top:8px;}.formkit-form[data-uid="96a310b6ce"] .formkit-alert{background:#f9fafb;border:1px solid #e3e3e3;border-radius:5px;-webkit-flex:1 0 auto;-ms-flex:1 0 auto;flex:1 0 auto;list-style:none;margin:25px auto;padding:12px;text-align:center;width:100%;}.formkit-form[data-uid="96a310b6ce"] .formkit-alert:empty{display:none;}.formkit-form[data-uid="96a310b6ce"] .formkit-alert-success{background:#d3fbeb;border-color:#10bf7a;color:#0c905c;}.formkit-form[data-uid="96a310b6ce"] .formkit-alert-error{background:#fde8e2;border-color:#f2643b;color:#ea4110;}.formkit-form[data-uid="96a310b6ce"] .formkit-spinner{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;height:0px;width:0px;margin:0 auto;position:absolute;top:0;left:0;right:0;width:0px;overflow:hidden;text-align:center;-webkit-transition:all 300ms ease-in-out;transition:all 300ms ease-in-out;}.formkit-form[data-uid="96a310b6ce"] .formkit-spinner > div{margin:auto;width:12px;height:12px;background-color:#fff;opacity:0.3;border-radius:100%;display:inline-block;-webkit-animation:formkit-bouncedelay-formkit-form-data-uid-96a310b6ce- 1.4s infinite ease-in-out both;animation:formkit-bouncedelay-formkit-form-data-uid-96a310b6ce- 1.4s infinite ease-in-out both;}.formkit-form[data-uid="96a310b6ce"] .formkit-spinner > div:nth-child(1){-webkit-animation-delay:-0.32s;animation-delay:-0.32s;}.formkit-form[data-uid="96a310b6ce"] .formkit-spinner > div:nth-child(2){-webkit-animation-delay:-0.16s;animation-delay:-0.16s;}.formkit-form[data-uid="96a310b6ce"] .formkit-submit[data-active] .formkit-spinner{opacity:1;height:100%;width:50px;}.formkit-form[data-uid="96a310b6ce"] .formkit-submit[data-active] .formkit-spinner ~ span{opacity:0;}.formkit-form[data-uid="96a310b6ce"] .formkit-powered-by[data-active="false"]{opacity:0.35;}.formkit-form[data-uid="96a310b6ce"] .formkit-powered-by-convertkit-container{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;width:100%;z-index:5;margin:10px 0;position:relative;}.formkit-form[data-uid="96a310b6ce"] .formkit-powered-by-convertkit-container[data-active="false"]{opacity:0.35;}.formkit-form[data-uid="96a310b6ce"] .formkit-powered-by-convertkit{-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;background-color:#ffffff;border:1px solid #dde2e7;border-radius:4px;color:#373f45;cursor:pointer;display:block;height:36px;margin:0 auto;opacity:0.95;padding:0;-webkit-text-decoration:none;text-decoration:none;text-indent:100%;-webkit-transition:ease-in-out all 200ms;transition:ease-in-out all 200ms;white-space:nowrap;overflow:hidden;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;width:190px;background-repeat:no-repeat;background-position:center;background-image:url("data:image/svg+xml;charset=utf8,%3Csvg width='162' height='20' viewBox='0 0 162 20' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M83.0561 15.2457C86.675 15.2457 89.4722 12.5154 89.4722 9.14749C89.4722 5.99211 86.8443 4.06563 85.1038 4.06563C82.6801 4.06563 80.7373 5.76407 80.4605 8.28551C80.4092 8.75244 80.0387 9.14403 79.5686 9.14069C78.7871 9.13509 77.6507 9.12841 76.9314 9.13092C76.6217 9.13199 76.3658 8.88106 76.381 8.57196C76.4895 6.38513 77.2218 4.3404 78.618 2.76974C80.1695 1.02445 82.4289 0 85.1038 0C89.5979 0 93.8406 4.07791 93.8406 9.14749C93.8406 14.7608 89.1832 19.3113 83.1517 19.3113C78.8502 19.3113 74.5179 16.5041 73.0053 12.5795C72.9999 12.565 72.9986 12.5492 73.0015 12.534C73.0218 12.4179 73.0617 12.3118 73.1011 12.2074C73.1583 12.0555 73.2143 11.907 73.2062 11.7359L73.18 11.1892C73.174 11.0569 73.2075 10.9258 73.2764 10.8127C73.3452 10.6995 73.4463 10.6094 73.5666 10.554L73.7852 10.4523C73.9077 10.3957 74.0148 10.3105 74.0976 10.204C74.1803 10.0974 74.2363 9.97252 74.2608 9.83983C74.3341 9.43894 74.6865 9.14749 75.0979 9.14749C75.7404 9.14749 76.299 9.57412 76.5088 10.1806C77.5188 13.1 79.1245 15.2457 83.0561 15.2457Z' fill='%23373F45'/%3E%3Cpath d='M155.758 6.91365C155.028 6.91365 154.804 6.47916 154.804 5.98857C154.804 5.46997 154.986 5.06348 155.758 5.06348C156.53 5.06348 156.712 5.46997 156.712 5.98857C156.712 6.47905 156.516 6.91365 155.758 6.91365ZM142.441 12.9304V9.32833L141.415 9.32323V8.90392C141.415 8.44719 141.786 8.07758 142.244 8.07986L142.441 8.08095V6.55306L144.082 6.09057V8.08073H145.569V8.50416C145.569 8.61242 145.548 8.71961 145.506 8.81961C145.465 8.91961 145.404 9.01047 145.328 9.08699C145.251 9.16351 145.16 9.2242 145.06 9.26559C144.96 9.30698 144.853 9.32826 144.745 9.32822H144.082V12.7201C144.082 13.2423 144.378 13.4256 144.76 13.4887C145.209 13.5629 145.583 13.888 145.583 14.343V14.9626C144.029 14.9626 142.441 14.8942 142.441 12.9304Z' fill='%23373F45'/%3E%3Cpath d='M110.058 7.92554C108.417 7.88344 106.396 8.92062 106.396 11.5137C106.396 14.0646 108.417 15.0738 110.058 15.0318C111.742 15.0738 113.748 14.0646 113.748 11.5137C113.748 8.92062 111.742 7.88344 110.058 7.92554ZM110.07 13.7586C108.878 13.7586 108.032 12.8905 108.032 11.461C108.032 10.1013 108.878 9.20569 110.071 9.20569C111.263 9.20569 112.101 10.0995 112.101 11.459C112.101 12.8887 111.263 13.7586 110.07 13.7586Z' fill='%23373F45'/%3E%3Cpath d='M118.06 7.94098C119.491 7.94098 120.978 8.33337 120.978 11.1366V14.893H120.063C119.608 14.893 119.238 14.524 119.238 14.0689V10.9965C119.238 9.66506 118.747 9.16047 117.891 9.16047C117.414 9.16047 116.797 9.52486 116.502 9.81915V14.069C116.502 14.1773 116.481 14.2845 116.44 14.3845C116.398 14.4845 116.337 14.5753 116.261 14.6519C116.184 14.7284 116.093 14.7891 115.993 14.8305C115.893 14.8719 115.786 14.8931 115.678 14.8931H114.847V8.10918H115.773C115.932 8.10914 116.087 8.16315 116.212 8.26242C116.337 8.36168 116.424 8.50033 116.46 8.65577C116.881 8.19328 117.428 7.94098 118.06 7.94098ZM122.854 8.09713C123.024 8.09708 123.19 8.1496 123.329 8.2475C123.468 8.34541 123.574 8.48391 123.631 8.64405L125.133 12.8486L126.635 8.64415C126.692 8.48402 126.798 8.34551 126.937 8.2476C127.076 8.1497 127.242 8.09718 127.412 8.09724H128.598L126.152 14.3567C126.091 14.5112 125.986 14.6439 125.849 14.7374C125.711 14.831 125.549 14.881 125.383 14.8809H124.333L121.668 8.09713H122.854Z' fill='%23373F45'/%3E%3Cpath d='M135.085 14.5514C134.566 14.7616 133.513 15.0416 132.418 15.0416C130.496 15.0416 129.024 13.9345 129.024 11.4396C129.024 9.19701 130.451 7.99792 132.191 7.99792C134.338 7.99792 135.254 9.4378 135.158 11.3979C135.139 11.8029 134.786 12.0983 134.38 12.0983H130.679C130.763 13.1916 131.562 13.7662 132.615 13.7662C133.028 13.7662 133.462 13.7452 133.983 13.6481C134.535 13.545 135.085 13.9375 135.085 14.4985V14.5514ZM133.673 10.949C133.785 9.87621 133.061 9.28752 132.191 9.28752C131.321 9.28752 130.734 9.93979 130.679 10.9489L133.673 10.949Z' fill='%23373F45'/%3E%3Cpath d='M137.345 8.11122C137.497 8.11118 137.645 8.16229 137.765 8.25635C137.884 8.35041 137.969 8.48197 138.005 8.62993C138.566 8.20932 139.268 7.94303 139.759 7.94303C139.801 7.94303 140.068 7.94303 140.489 7.99913V8.7265C140.489 9.11748 140.15 9.4147 139.759 9.4147C139.31 9.4147 138.651 9.5829 138.131 9.8773V14.8951H136.462V8.11112L137.345 8.11122ZM156.6 14.0508V8.09104H155.769C155.314 8.09104 154.944 8.45999 154.944 8.9151V14.8748H155.775C156.23 14.8748 156.6 14.5058 156.6 14.0508ZM158.857 12.9447V9.34254H157.749V8.91912C157.749 8.46401 158.118 8.09506 158.574 8.09506H158.857V6.56739L160.499 6.10479V8.09506H161.986V8.51848C161.986 8.97359 161.617 9.34254 161.161 9.34254H160.499V12.7345C160.499 13.2566 160.795 13.44 161.177 13.503C161.626 13.5774 162 13.9024 162 14.3574V14.977C160.446 14.977 158.857 14.9086 158.857 12.9447ZM98.1929 10.1124C98.2033 6.94046 100.598 5.16809 102.895 5.16809C104.171 5.16809 105.342 5.44285 106.304 6.12953L105.914 6.6631C105.654 7.02011 105.16 7.16194 104.749 6.99949C104.169 6.7702 103.622 6.7218 103.215 6.7218C101.335 6.7218 99.9169 7.92849 99.9068 10.1123C99.9169 12.2959 101.335 13.5201 103.215 13.5201C103.622 13.5201 104.169 13.4717 104.749 13.2424C105.16 13.0799 105.654 13.2046 105.914 13.5615L106.304 14.0952C105.342 14.7819 104.171 15.0566 102.895 15.0566C100.598 15.0566 98.2033 13.2842 98.1929 10.1124ZM147.619 5.21768C148.074 5.21768 148.444 5.58663 148.444 6.04174V9.81968L151.82 5.58131C151.897 5.47733 151.997 5.39282 152.112 5.3346C152.227 5.27638 152.355 5.24607 152.484 5.24611H153.984L150.166 10.0615L153.984 14.8749H152.484C152.355 14.8749 152.227 14.8446 152.112 14.7864C151.997 14.7281 151.897 14.6436 151.82 14.5397L148.444 10.3025V14.0508C148.444 14.5059 148.074 14.8749 147.619 14.8749H146.746V5.21768H147.619Z' fill='%23373F45'/%3E%3Cpath d='M0.773438 6.5752H2.68066C3.56543 6.5752 4.2041 6.7041 4.59668 6.96191C4.99219 7.21973 5.18994 7.62695 5.18994 8.18359C5.18994 8.55859 5.09326 8.87061 4.8999 9.11963C4.70654 9.36865 4.42822 9.52539 4.06494 9.58984V9.63379C4.51611 9.71875 4.84717 9.88721 5.05811 10.1392C5.27197 10.3882 5.37891 10.7266 5.37891 11.1543C5.37891 11.7314 5.17676 12.1841 4.77246 12.5122C4.37109 12.8374 3.81152 13 3.09375 13H0.773438V6.5752ZM1.82373 9.22949H2.83447C3.27393 9.22949 3.59473 9.16064 3.79688 9.02295C3.99902 8.88232 4.1001 8.64502 4.1001 8.31104C4.1001 8.00928 3.99023 7.79102 3.77051 7.65625C3.55371 7.52148 3.20801 7.4541 2.7334 7.4541H1.82373V9.22949ZM1.82373 10.082V12.1167H2.93994C3.37939 12.1167 3.71045 12.0332 3.93311 11.8662C4.15869 11.6963 4.27148 11.4297 4.27148 11.0664C4.27148 10.7324 4.15723 10.4849 3.92871 10.3237C3.7002 10.1626 3.35303 10.082 2.88721 10.082H1.82373Z' fill='%23373F45'/%3E%3Cpath d='M13.011 6.5752V10.7324C13.011 11.207 12.9084 11.623 12.7034 11.9805C12.5012 12.335 12.2068 12.6089 11.8201 12.8022C11.4363 12.9927 10.9763 13.0879 10.4402 13.0879C9.6433 13.0879 9.02368 12.877 8.5813 12.4551C8.13892 12.0332 7.91772 11.4531 7.91772 10.7148V6.5752H8.9724V10.6401C8.9724 11.1704 9.09546 11.5615 9.34155 11.8135C9.58765 12.0654 9.96557 12.1914 10.4753 12.1914C11.4656 12.1914 11.9607 11.6714 11.9607 10.6313V6.5752H13.011Z' fill='%23373F45'/%3E%3Cpath d='M15.9146 13V6.5752H16.9649V13H15.9146Z' fill='%23373F45'/%3E%3Cpath d='M19.9255 13V6.5752H20.9758V12.0991H23.696V13H19.9255Z' fill='%23373F45'/%3E%3Cpath d='M28.2828 13H27.2325V7.47607H25.3428V6.5752H30.1724V7.47607H28.2828V13Z' fill='%23373F45'/%3E%3Cpath d='M41.9472 13H40.8046L39.7148 9.16796C39.6679 9.00097 39.6093 8.76074 39.539 8.44727C39.4687 8.13086 39.4262 7.91113 39.4116 7.78809C39.3823 7.97559 39.3339 8.21875 39.2665 8.51758C39.2021 8.81641 39.1479 9.03905 39.1039 9.18554L38.0405 13H36.8979L36.0673 9.7832L35.2236 6.5752H36.2958L37.2143 10.3193C37.3578 10.9199 37.4604 11.4502 37.5219 11.9102C37.5541 11.6611 37.6025 11.3828 37.6669 11.0752C37.7314 10.7676 37.79 10.5186 37.8427 10.3281L38.8886 6.5752H39.9301L41.0024 10.3457C41.1049 10.6943 41.2133 11.2158 41.3276 11.9102C41.3715 11.4912 41.477 10.958 41.644 10.3105L42.558 6.5752H43.6215L41.9472 13Z' fill='%23373F45'/%3E%3Cpath d='M45.7957 13V6.5752H46.846V13H45.7957Z' fill='%23373F45'/%3E%3Cpath d='M52.0258 13H50.9755V7.47607H49.0859V6.5752H53.9155V7.47607H52.0258V13Z' fill='%23373F45'/%3E%3Cpath d='M61.2312 13H60.1765V10.104H57.2146V13H56.1643V6.5752H57.2146V9.20312H60.1765V6.5752H61.2312V13Z' fill='%23373F45'/%3E%3C/svg%3E");}.formkit-form[data-uid="96a310b6ce"] .formkit-powered-by-convertkit:hover,.formkit-form[data-uid="96a310b6ce"] .formkit-powered-by-convertkit:focus{background-color:#ffffff;-webkit-transform:scale(1.025) perspective(1px);-ms-transform:scale(1.025) perspective(1px);transform:scale(1.025) perspective(1px);opacity:1;}.formkit-form[data-uid="96a310b6ce"] .formkit-powered-by-convertkit[data-variant="dark"],.formkit-form[data-uid="96a310b6ce"] .formkit-powered-by-convertkit[data-variant="light"]{background-color:transparent;border-color:transparent;width:166px;}.formkit-form[data-uid="96a310b6ce"] .formkit-powered-by-convertkit[data-variant="light"]{color:#ffffff;background-image:url("data:image/svg+xml;charset=utf8,%3Csvg width='162' height='20' viewBox='0 0 162 20' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M83.0561 15.2457C86.675 15.2457 89.4722 12.5154 89.4722 9.14749C89.4722 5.99211 86.8443 4.06563 85.1038 4.06563C82.6801 4.06563 80.7373 5.76407 80.4605 8.28551C80.4092 8.75244 80.0387 9.14403 79.5686 9.14069C78.7871 9.13509 77.6507 9.12841 76.9314 9.13092C76.6217 9.13199 76.3658 8.88106 76.381 8.57196C76.4895 6.38513 77.2218 4.3404 78.618 2.76974C80.1695 1.02445 82.4289 0 85.1038 0C89.5979 0 93.8406 4.07791 93.8406 9.14749C93.8406 14.7608 89.1832 19.3113 83.1517 19.3113C78.8502 19.3113 74.5179 16.5041 73.0053 12.5795C72.9999 12.565 72.9986 12.5492 73.0015 12.534C73.0218 12.4179 73.0617 12.3118 73.1011 12.2074C73.1583 12.0555 73.2143 11.907 73.2062 11.7359L73.18 11.1892C73.174 11.0569 73.2075 10.9258 73.2764 10.8127C73.3452 10.6995 73.4463 10.6094 73.5666 10.554L73.7852 10.4523C73.9077 10.3957 74.0148 10.3105 74.0976 10.204C74.1803 10.0974 74.2363 9.97252 74.2608 9.83983C74.3341 9.43894 74.6865 9.14749 75.0979 9.14749C75.7404 9.14749 76.299 9.57412 76.5088 10.1806C77.5188 13.1 79.1245 15.2457 83.0561 15.2457Z' fill='white'/%3E%3Cpath d='M155.758 6.91365C155.028 6.91365 154.804 6.47916 154.804 5.98857C154.804 5.46997 154.986 5.06348 155.758 5.06348C156.53 5.06348 156.712 5.46997 156.712 5.98857C156.712 6.47905 156.516 6.91365 155.758 6.91365ZM142.441 12.9304V9.32833L141.415 9.32323V8.90392C141.415 8.44719 141.786 8.07758 142.244 8.07986L142.441 8.08095V6.55306L144.082 6.09057V8.08073H145.569V8.50416C145.569 8.61242 145.548 8.71961 145.506 8.81961C145.465 8.91961 145.404 9.01047 145.328 9.08699C145.251 9.16351 145.16 9.2242 145.06 9.26559C144.96 9.30698 144.853 9.32826 144.745 9.32822H144.082V12.7201C144.082 13.2423 144.378 13.4256 144.76 13.4887C145.209 13.5629 145.583 13.888 145.583 14.343V14.9626C144.029 14.9626 142.441 14.8942 142.441 12.9304Z' fill='white'/%3E%3Cpath d='M110.058 7.92554C108.417 7.88344 106.396 8.92062 106.396 11.5137C106.396 14.0646 108.417 15.0738 110.058 15.0318C111.742 15.0738 113.748 14.0646 113.748 11.5137C113.748 8.92062 111.742 7.88344 110.058 7.92554ZM110.07 13.7586C108.878 13.7586 108.032 12.8905 108.032 11.461C108.032 10.1013 108.878 9.20569 110.071 9.20569C111.263 9.20569 112.101 10.0995 112.101 11.459C112.101 12.8887 111.263 13.7586 110.07 13.7586Z' fill='white'/%3E%3Cpath d='M118.06 7.94098C119.491 7.94098 120.978 8.33337 120.978 11.1366V14.893H120.063C119.608 14.893 119.238 14.524 119.238 14.0689V10.9965C119.238 9.66506 118.747 9.16047 117.891 9.16047C117.414 9.16047 116.797 9.52486 116.502 9.81915V14.069C116.502 14.1773 116.481 14.2845 116.44 14.3845C116.398 14.4845 116.337 14.5753 116.261 14.6519C116.184 14.7284 116.093 14.7891 115.993 14.8305C115.893 14.8719 115.786 14.8931 115.678 14.8931H114.847V8.10918H115.773C115.932 8.10914 116.087 8.16315 116.212 8.26242C116.337 8.36168 116.424 8.50033 116.46 8.65577C116.881 8.19328 117.428 7.94098 118.06 7.94098ZM122.854 8.09713C123.024 8.09708 123.19 8.1496 123.329 8.2475C123.468 8.34541 123.574 8.48391 123.631 8.64405L125.133 12.8486L126.635 8.64415C126.692 8.48402 126.798 8.34551 126.937 8.2476C127.076 8.1497 127.242 8.09718 127.412 8.09724H128.598L126.152 14.3567C126.091 14.5112 125.986 14.6439 125.849 14.7374C125.711 14.831 125.549 14.881 125.383 14.8809H124.333L121.668 8.09713H122.854Z' fill='white'/%3E%3Cpath d='M135.085 14.5514C134.566 14.7616 133.513 15.0416 132.418 15.0416C130.496 15.0416 129.024 13.9345 129.024 11.4396C129.024 9.19701 130.451 7.99792 132.191 7.99792C134.338 7.99792 135.254 9.4378 135.158 11.3979C135.139 11.8029 134.786 12.0983 134.38 12.0983H130.679C130.763 13.1916 131.562 13.7662 132.615 13.7662C133.028 13.7662 133.462 13.7452 133.983 13.6481C134.535 13.545 135.085 13.9375 135.085 14.4985V14.5514ZM133.673 10.949C133.785 9.87621 133.061 9.28752 132.191 9.28752C131.321 9.28752 130.734 9.93979 130.679 10.9489L133.673 10.949Z' fill='white'/%3E%3Cpath d='M137.345 8.11122C137.497 8.11118 137.645 8.16229 137.765 8.25635C137.884 8.35041 137.969 8.48197 138.005 8.62993C138.566 8.20932 139.268 7.94303 139.759 7.94303C139.801 7.94303 140.068 7.94303 140.489 7.99913V8.7265C140.489 9.11748 140.15 9.4147 139.759 9.4147C139.31 9.4147 138.651 9.5829 138.131 9.8773V14.8951H136.462V8.11112L137.345 8.11122ZM156.6 14.0508V8.09104H155.769C155.314 8.09104 154.944 8.45999 154.944 8.9151V14.8748H155.775C156.23 14.8748 156.6 14.5058 156.6 14.0508ZM158.857 12.9447V9.34254H157.749V8.91912C157.749 8.46401 158.118 8.09506 158.574 8.09506H158.857V6.56739L160.499 6.10479V8.09506H161.986V8.51848C161.986 8.97359 161.617 9.34254 161.161 9.34254H160.499V12.7345C160.499 13.2566 160.795 13.44 161.177 13.503C161.626 13.5774 162 13.9024 162 14.3574V14.977C160.446 14.977 158.857 14.9086 158.857 12.9447ZM98.1929 10.1124C98.2033 6.94046 100.598 5.16809 102.895 5.16809C104.171 5.16809 105.342 5.44285 106.304 6.12953L105.914 6.6631C105.654 7.02011 105.16 7.16194 104.749 6.99949C104.169 6.7702 103.622 6.7218 103.215 6.7218C101.335 6.7218 99.9169 7.92849 99.9068 10.1123C99.9169 12.2959 101.335 13.5201 103.215 13.5201C103.622 13.5201 104.169 13.4717 104.749 13.2424C105.16 13.0799 105.654 13.2046 105.914 13.5615L106.304 14.0952C105.342 14.7819 104.171 15.0566 102.895 15.0566C100.598 15.0566 98.2033 13.2842 98.1929 10.1124ZM147.619 5.21768C148.074 5.21768 148.444 5.58663 148.444 6.04174V9.81968L151.82 5.58131C151.897 5.47733 151.997 5.39282 152.112 5.3346C152.227 5.27638 152.355 5.24607 152.484 5.24611H153.984L150.166 10.0615L153.984 14.8749H152.484C152.355 14.8749 152.227 14.8446 152.112 14.7864C151.997 14.7281 151.897 14.6436 151.82 14.5397L148.444 10.3025V14.0508C148.444 14.5059 148.074 14.8749 147.619 14.8749H146.746V5.21768H147.619Z' fill='white'/%3E%3Cpath d='M0.773438 6.5752H2.68066C3.56543 6.5752 4.2041 6.7041 4.59668 6.96191C4.99219 7.21973 5.18994 7.62695 5.18994 8.18359C5.18994 8.55859 5.09326 8.87061 4.8999 9.11963C4.70654 9.36865 4.42822 9.52539 4.06494 9.58984V9.63379C4.51611 9.71875 4.84717 9.88721 5.05811 10.1392C5.27197 10.3882 5.37891 10.7266 5.37891 11.1543C5.37891 11.7314 5.17676 12.1841 4.77246 12.5122C4.37109 12.8374 3.81152 13 3.09375 13H0.773438V6.5752ZM1.82373 9.22949H2.83447C3.27393 9.22949 3.59473 9.16064 3.79688 9.02295C3.99902 8.88232 4.1001 8.64502 4.1001 8.31104C4.1001 8.00928 3.99023 7.79102 3.77051 7.65625C3.55371 7.52148 3.20801 7.4541 2.7334 7.4541H1.82373V9.22949ZM1.82373 10.082V12.1167H2.93994C3.37939 12.1167 3.71045 12.0332 3.93311 11.8662C4.15869 11.6963 4.27148 11.4297 4.27148 11.0664C4.27148 10.7324 4.15723 10.4849 3.92871 10.3237C3.7002 10.1626 3.35303 10.082 2.88721 10.082H1.82373Z' fill='white'/%3E%3Cpath d='M13.011 6.5752V10.7324C13.011 11.207 12.9084 11.623 12.7034 11.9805C12.5012 12.335 12.2068 12.6089 11.8201 12.8022C11.4363 12.9927 10.9763 13.0879 10.4402 13.0879C9.6433 13.0879 9.02368 12.877 8.5813 12.4551C8.13892 12.0332 7.91772 11.4531 7.91772 10.7148V6.5752H8.9724V10.6401C8.9724 11.1704 9.09546 11.5615 9.34155 11.8135C9.58765 12.0654 9.96557 12.1914 10.4753 12.1914C11.4656 12.1914 11.9607 11.6714 11.9607 10.6313V6.5752H13.011Z' fill='white'/%3E%3Cpath d='M15.9146 13V6.5752H16.9649V13H15.9146Z' fill='white'/%3E%3Cpath d='M19.9255 13V6.5752H20.9758V12.0991H23.696V13H19.9255Z' fill='white'/%3E%3Cpath d='M28.2828 13H27.2325V7.47607H25.3428V6.5752H30.1724V7.47607H28.2828V13Z' fill='white'/%3E%3Cpath d='M41.9472 13H40.8046L39.7148 9.16796C39.6679 9.00097 39.6093 8.76074 39.539 8.44727C39.4687 8.13086 39.4262 7.91113 39.4116 7.78809C39.3823 7.97559 39.3339 8.21875 39.2665 8.51758C39.2021 8.81641 39.1479 9.03905 39.1039 9.18554L38.0405 13H36.8979L36.0673 9.7832L35.2236 6.5752H36.2958L37.2143 10.3193C37.3578 10.9199 37.4604 11.4502 37.5219 11.9102C37.5541 11.6611 37.6025 11.3828 37.6669 11.0752C37.7314 10.7676 37.79 10.5186 37.8427 10.3281L38.8886 6.5752H39.9301L41.0024 10.3457C41.1049 10.6943 41.2133 11.2158 41.3276 11.9102C41.3715 11.4912 41.477 10.958 41.644 10.3105L42.558 6.5752H43.6215L41.9472 13Z' fill='white'/%3E%3Cpath d='M45.7957 13V6.5752H46.846V13H45.7957Z' fill='white'/%3E%3Cpath d='M52.0258 13H50.9755V7.47607H49.0859V6.5752H53.9155V7.47607H52.0258V13Z' fill='white'/%3E%3Cpath d='M61.2312 13H60.1765V10.104H57.2146V13H56.1643V6.5752H57.2146V9.20312H60.1765V6.5752H61.2312V13Z' fill='white'/%3E%3C/svg%3E");}@-webkit-keyframes formkit-bouncedelay-formkit-form-data-uid-96a310b6ce-{0%,80%,100%{-webkit-transform:scale(0);-ms-transform:scale(0);transform:scale(0);}40%{-webkit-transform:scale(1);-ms-transform:scale(1);transform:scale(1);}}@keyframes formkit-bouncedelay-formkit-form-data-uid-96a310b6ce-{0%,80%,100%{-webkit-transform:scale(0);-ms-transform:scale(0);transform:scale(0);}40%{-webkit-transform:scale(1);-ms-transform:scale(1);transform:scale(1);}}.formkit-form[data-uid="96a310b6ce"] blockquote{padding:10px 20px;margin:0 0 20px;border-left:5px solid #e1e1e1;}.formkit-form[data-uid="96a310b6ce"] .seva-custom-content{padding:15px;font-size:16px;color:#fff;mix-blend-mode:difference;}.formkit-form[data-uid="96a310b6ce"] .formkit-modal.guard{max-width:420px;width:100%;} .formkit-form[data-uid="96a310b6ce"]{max-width:700px;}.formkit-form[data-uid="96a310b6ce"] [data-style="clean"]{width:100%;}.formkit-form[data-uid="96a310b6ce"] .formkit-fields{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;margin:0 auto;}.formkit-form[data-uid="96a310b6ce"] .formkit-field,.formkit-form[data-uid="96a310b6ce"] .formkit-submit{margin:0 0 15px 0;-webkit-flex:1 0 100%;-ms-flex:1 0 100%;flex:1 0 100%;}.formkit-form[data-uid="96a310b6ce"] .formkit-powered-by-convertkit-container{margin:0;}.formkit-form[data-uid="96a310b6ce"] .formkit-submit{position:static;}.formkit-form[data-uid="96a310b6ce"][min-width~="700"] [data-style="clean"],.formkit-form[data-uid="96a310b6ce"][min-width~="800"] [data-style="clean"]{padding:10px;}.formkit-form[data-uid="96a310b6ce"][min-width~="700"] .formkit-fields[data-stacked="false"],.formkit-form[data-uid="96a310b6ce"][min-width~="800"] .formkit-fields[data-stacked="false"]{margin-left:-5px;margin-right:-5px;}.formkit-form[data-uid="96a310b6ce"][min-width~="700"] .formkit-fields[data-stacked="false"] .formkit-field,.formkit-form[data-uid="96a310b6ce"][min-width~="800"] .formkit-fields[data-stacked="false"] .formkit-field,.formkit-form[data-uid="96a310b6ce"][min-width~="700"] .formkit-fields[data-stacked="false"] .formkit-submit,.formkit-form[data-uid="96a310b6ce"][min-width~="800"] .formkit-fields[data-stacked="false"] .formkit-submit{margin:0 5px 15px 5px;}.formkit-form[data-uid="96a310b6ce"][min-width~="700"] .formkit-fields[data-stacked="false"] .formkit-field,.formkit-form[data-uid="96a310b6ce"][min-width~="800"] .formkit-fields[data-stacked="false"] .formkit-field{-webkit-flex:100 1 auto;-ms-flex:100 1 auto;flex:100 1 auto;}.formkit-form[data-uid="96a310b6ce"][min-width~="700"] .formkit-fields[data-stacked="false"] .formkit-submit,.formkit-form[data-uid="96a310b6ce"][min-width~="800"] .formkit-fields[data-stacked="false"] .formkit-submit{-webkit-flex:1 1 auto;-ms-flex:1 1 auto;flex:1 1 auto;} </style></form>

        <hr>

        <!-- Post comments -->
        <script src="https://utteranc.es/client.js" repo="eugeneyan/eugeneyan-comments" issue-term="url" theme="github-light" crossorigin="anonymous" type="8508930d3962b87c0ce5ff16-text/javascript">
</script>
    </div>
</div>

        <footer class="footer">
    <div class="footer-col-wrapper">
        <div class="col-sm-3 footer-col">
            <ul class="contact-list">
                <!-- <li>
                    <img class="icon" src="/assets/bluesky.svg" loading="lazy" alt=""/>
                    <a rel="me" href="https://bsky.app/profile/eugeneyan.com" target="_blank" title="Bluesky">Bluesky</a>
                </li> -->
                <li>
                    <img class="icon" src="/assets/icon-twitter.svg" loading="lazy" alt=""/>
                    <a href="https://twitter.com/eugeneyan" target="_blank" title="Twitter">Twitter</a>
                </li>
                <li>
                    <img class="icon" src="/assets/icon-linkedin.svg" loading="lazy" alt=""/>
                    <a href="https://www.linkedin.com/in/eugeneyan/" target="_blank" title="Linkedin">LinkedIn</a>
                </li>
                <!-- <li>
                    <img class="icon" src="/assets/icon-threads.svg" loading="lazy" alt=""/>
                    <a href="https://www.threads.net/@eugeneyan" target="_blank" title="Threads">Threads</a>
                </li> -->
                <li>
                    <img class="icon" src="/assets/icon-github.svg" loading="lazy" alt=""/>
                    <a href="https://github.com/eugeneyan/" target="_blank" title="GitHub">GitHub</a>
                </li>
            </ul>
        </div>

        <div class="col-sm-9 footer-col">
            <p>I'm a Member of Technical Staff at Anthropic. I work to bridge the field and the frontier, and help build safe, reliable AI systems that scale. I've led ML/AI teams at Amazon, Alibaba, Lazada, and a Healthtech Series A, and write about LLMs, RecSys, and engineering at <a href="https://eugeneyan.com/" target="_blank">eugeneyan.com</a>.</p>
        </div>
    </div>
    <p class="copyright">© Eugene Yan 2015 - 2026
        • <a href="/site-feedback/">Feedback</a>
        • <a href="/rss/">RSS</a>
    </p>
</footer>


    </div> <!-- /container -->
</div>
<script src="/cdn-cgi/scripts/7d0fa10a/cloudflare-static/rocket-loader.min.js" data-cf-settings="8508930d3962b87c0ce5ff16-|49" defer></script><script defer src="https://static.cloudflareinsights.com/beacon.min.js/v833ccba57c9e4d2798f2e76cebdd09a11778172276447" integrity="sha512-57MDmcccJXYtNnH+ZiBwzC4jb2rvgVCEokYN+L/nLlmO8rfYT/gIpW2A569iJ/3b+0UEasghjuZH/ma3wIs/EQ==" data-cf-beacon='{"version":"2024.11.0","token":"4ba4ab6acad14218941be7fa4aaad127","r":1,"server_timing":{"name":{"cfCacheStatus":true,"cfEdge":true,"cfExtPri":true,"cfL4":true,"cfOrigin":true,"cfSpeedBrain":true},"location_startswith":null}}' crossorigin="anonymous"></script>
</body>

<script type="8508930d3962b87c0ce5ff16-text/javascript">
  // Assemble mailto: from split data attributes on click (keeps full address out of source)
  document.addEventListener('click', function(e) {
    var a = e.target.closest('a.js-email');
    if (!a) return;
    e.preventDefault();
    var d = a.dataset;
    window.location.href = 'mailto:' + d.u + '@' + d.d + '.' + d.t;
  });

  // Ensure aa is loaded
  document.addEventListener('DOMContentLoaded', function() {
    if (typeof aa === 'function') {
      // Get the current page path for more specific tracking
      const pagePath = '/writing/recommender-systems-graph-and-nlp-pytorch/';

      // Determine a specific event name based on the current page
      let eventName;
      if (pagePath.startsWith('/tag/')) {
        eventName = 'Tag Page Link Clicked';
      } else {
        // Count the number of segments to determine if it's a site page or post page
        const pathSegments = pagePath.split('/').filter(Boolean);
        if (pathSegments.length <= 1) {
          // Zero or one level deep (e.g., '/', '/writing/', '/speaking/')
          eventName = 'Site Page Link Clicked';
        } else {
          // Two or more levels deep - considered a post
          eventName = 'Post Link Clicked';
        }
      }

      // Track clicks on internal links in the main container
      const container = document.querySelector('div.container');
      if (container) {
        container.addEventListener('click', function(e) {
          const link = e.target.closest('a');
          if (link && !link.classList.contains('js-email') && link.href && link.origin === window.location.origin) {
            const objectID = link.pathname;

            aa('clickedObjectIDs', {
              index: 'eugeneyan.com',
              eventName: eventName,
              objectIDs: [objectID]
            });
          }
        });
      }
    }
  });

  // Track page read depth for conversion tracking
  let hasTrackedPageRead = false;
  window.addEventListener('scroll', function() {
    if (hasTrackedPageRead) return; // Only track once per page view
    
    // Calculate read depth as percentage
    const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
    const scrollHeight = document.documentElement.scrollHeight;
    const clientHeight = document.documentElement.clientHeight;
    const readPercentage = (scrollTop + clientHeight) / scrollHeight * 100;
    
    // If user has read at least 50% of the page
    if (readPercentage >= 50 && typeof aa === 'function') {
      hasTrackedPageRead = true;
      const objectID = window.location.pathname; // Use current page path as objectID
      const pagePath = '/writing/recommender-systems-graph-and-nlp-pytorch/';
      
      // Create a meaningful event name
      let eventName;
      if (pagePath.startsWith('/tag/')) {
        eventName = 'Tag Page Read 50%';
      } else {
        // Count the number of segments to determine if it's a site page or post page
        const pathSegments = pagePath.split('/').filter(Boolean);
        if (pathSegments.length <= 1) {
          // Zero or one level deep (e.g., '/', '/writing/', '/speaking/')
          eventName = 'Site Page Read 50%';
        } else {
          // Two or more levels deep - considered a post
          eventName = 'Post Read 50%';
        }
      }
      
      // Send the convertedObjectIDs event to Algolia
      aa('convertedObjectIDs', {
        index: 'eugeneyan.com',
        eventName: eventName,
        objectIDs: [objectID]
      });
    }
  });
</script>
</html>