<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>PyTorch on David Davó&#39;s dev log</title>
    <link>https://blog.ddavo.me/tags/pytorch/</link>
    <description>Recent content in PyTorch on David Davó&#39;s dev log</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en</language>
    <copyright>© David Davó 2015 - 2024</copyright>
    <lastBuildDate>Fri, 04 Aug 2023 18:21:50 +0000</lastBuildDate><atom:link href="https://blog.ddavo.me/tags/pytorch/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Computing information retrieval metrics in Pytorch Geometric</title>
      <link>https://blog.ddavo.me/posts/pytorch-geometric-metrics/</link>
      <pubDate>Fri, 04 Aug 2023 18:21:50 +0000</pubDate>
      
      <guid>https://blog.ddavo.me/posts/pytorch-geometric-metrics/</guid>
      <description>How to calculate metrics like precision@k, recall@k and r-precision for information retrieval and recommender systems in PyTorch Geometric</description>
      <content:encoded><![CDATA[<h2 id="formulas-and-definitions">Formulas and Definitions</h2>
<p>There are multiple metrics used both in information retrieval and recommender systems that are analagous to standard metrics. Precision and recall at k (also called precision@k and recall@k) both answer the simple question of &ldquo;whats the precision/recall if I retrieve k documents using my system&rdquo;.</p>
<blockquote>
<p>$$ p@k = \frac{|\{\text{relevant documents}\}\cap\{\text{top k retrieved documents}\}|}{k} $$</p>
</blockquote>
<p>But with both of these metrics, the constant <em>k</em> needs to be known. A user that only interacted 3 times with items will have a maximum p@5 of 3/5, but for a user with hundreds of interactions, scoring a good p@5 would be too easy for our system. Furthermore, if you have hundreds of interactions for every user, the recall will be pretty low.</p>
<p>If we could just vary the <em>k</em> for each user&hellip; Thats when R-precission comes in handy. Is like precision@k, but the k is different for each user, and is equal to the number of relevant items for the user. The difference between the old simple recall and r-precision is that the number of documents to retrieve is equal to the number of relevant documents.</p>
<blockquote>
<p>$$ r-precision = \frac{\left|\{\text{relevant documents}\}\cap \{\text{top R retrieved documents}\}\right| }{R} $$</p>
<p>Where \(R = |\{\text{relevant documents}\}|\)</p>
</blockquote>
<h2 id="implementation-and-pytorch-geometric-code">Implementation and PyTorch Geometric code</h2>
<p>Let&rsquo;s imagine the graph is implemented as a tensor <code>edge_index</code> of size <code>[2,n_users]</code>, where <code>edge_index[0]</code> is the source of each edge, and <code>edge_index[1]</code> is the destination. We also have a model with a method <code>model.recommend</code> (like <a href="https://pytorch-geometric.readthedocs.io/en/latest/generated/torch_geometric.nn.models.LightGCN.html?highlight=lightgcn">LightGCN</a>), that, given a value <em>k</em> returns the top <em>k</em> recommendations of nodes.</p>
<p>The code of the following function is thoroughly commented to make it easier to understand. It receives a k and returns both the precision and recall at k, and the R-precision.</p>
<p>I assume that the model and the graph are out of the scope of the function (they are global variables, or this functions is inside a bigger function).</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="nd">@torch.no_grad</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">prec_rec</span><span class="p">(</span><span class="n">k</span><span class="p">:</span> <span class="nb">int</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># gt: ground truth (all edges)</span>
</span></span><span class="line"><span class="cl">    <span class="n">gt_index</span> <span class="o">=</span> <span class="n">original</span><span class="p">[</span><span class="s1">&#39;voter&#39;</span><span class="p">,</span> <span class="s1">&#39;votes&#39;</span><span class="p">,</span> <span class="s1">&#39;proposal&#39;</span><span class="p">]</span><span class="o">.</span><span class="n">edge_index</span>
</span></span><span class="line"><span class="cl">    <span class="n">edge_index</span> <span class="o">=</span> <span class="n">validation</span><span class="p">[</span><span class="s1">&#39;voter&#39;</span><span class="p">,</span> <span class="s1">&#39;votes&#39;</span><span class="p">,</span> <span class="s1">&#39;proposal&#39;</span><span class="p">]</span><span class="o">.</span><span class="n">edge_index</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># First, we will need to obtain the R value for each node</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># In graph terms, this is just the degree of the graph</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># (the number of items each user interacted with)</span>
</span></span><span class="line"><span class="cl">    <span class="n">R</span> <span class="o">=</span> <span class="n">item_count</span> <span class="o">=</span> <span class="n">PyG</span><span class="o">.</span><span class="n">utils</span><span class="o">.</span><span class="n">degree</span><span class="p">(</span><span class="n">gt_index</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="n">num_nodes</span><span class="o">=</span><span class="n">n_users</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Then, we get the top max(R) recomendations. This is a bit</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># expensive but less than sorting all the recommendations</span>
</span></span><span class="line"><span class="cl">    <span class="n">topr</span> <span class="o">=</span> <span class="n">model</span><span class="o">.</span><span class="n">recommend</span><span class="p">(</span><span class="n">edge_index</span><span class="p">,</span> <span class="n">src_index</span><span class="o">=</span><span class="n">users</span><span class="p">,</span> <span class="n">dst_index</span><span class="o">=</span><span class="n">items</span><span class="p">,</span> <span class="n">k</span><span class="o">=</span><span class="nb">int</span><span class="p">(</span><span class="n">R</span><span class="o">.</span><span class="n">max</span><span class="p">()),</span> <span class="nb">sorted</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="c1"># We transform the pair of vertices format to a </span>
</span></span><span class="line"><span class="cl">    <span class="c1"># bipartite adjacency matrix</span>
</span></span><span class="line"><span class="cl">    <span class="n">ground_truth</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">full</span><span class="p">((</span><span class="n">n_users</span><span class="p">,</span> <span class="n">n_items</span><span class="p">),</span> <span class="kc">False</span><span class="p">,</span> <span class="n">dtype</span><span class="o">=</span><span class="n">torch</span><span class="o">.</span><span class="n">bool</span><span class="p">,</span> <span class="n">device</span><span class="o">=</span><span class="n">device</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">ground_truth</span><span class="p">[</span><span class="n">gt_index</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="n">gt_index</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">-</span> <span class="n">n_users</span><span class="p">]</span> <span class="o">=</span> <span class="kc">True</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Then, we gather the results of that matrix using</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># the top recommendations obtained before</span>
</span></span><span class="line"><span class="cl">    <span class="n">isin_rmat</span> <span class="o">=</span> <span class="n">ground_truth</span><span class="o">.</span><span class="n">gather</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="n">topr</span> <span class="o">-</span> <span class="n">n_users</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># For p@k and r@k we just need the first k recommendations</span>
</span></span><span class="line"><span class="cl">    <span class="n">isin_mat</span> <span class="o">=</span> <span class="n">isin_rmat</span><span class="p">[:,</span> <span class="p">:</span><span class="n">k</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># We calculate mean precision and recall using the formulas</span>
</span></span><span class="line"><span class="cl">    <span class="n">prec</span> <span class="o">=</span> <span class="p">(</span><span class="n">isin_mat</span><span class="o">.</span><span class="n">sum</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="o">/</span> <span class="n">k</span><span class="p">)</span><span class="o">.</span><span class="n">sum</span><span class="p">()</span> <span class="o">/</span> <span class="n">n_users</span>
</span></span><span class="line"><span class="cl">    <span class="n">rec</span> <span class="o">=</span> <span class="p">(</span><span class="n">isin_mat</span><span class="o">.</span><span class="n">sum</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="o">/</span> <span class="n">item_count</span><span class="p">)</span><span class="o">.</span><span class="n">sum</span><span class="p">()</span> <span class="o">/</span> <span class="n">n_users</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># We can&#39;t do isin_rmat[:, :R] because R is not an scalar</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># My solution is to create a mask with as much ones as R</span>
</span></span><span class="line"><span class="cl">    <span class="n">msk</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">arange</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="n">R</span><span class="o">.</span><span class="n">max</span><span class="p">()</span><span class="o">+</span><span class="mi">1</span><span class="p">,</span> <span class="n">device</span><span class="o">=</span><span class="n">device</span><span class="p">)</span> <span class="o">&gt;</span> <span class="n">R</span><span class="o">.</span><span class="n">unsqueeze</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">isin_rmat</span><span class="p">[</span><span class="n">msk</span><span class="p">]</span> <span class="o">=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Calculate the mean R-precision using the formula</span>
</span></span><span class="line"><span class="cl">    <span class="n">rprec</span> <span class="o">=</span> <span class="p">(</span><span class="n">isin_rmat</span><span class="o">.</span><span class="n">sum</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="o">/</span> <span class="n">R</span><span class="p">)</span><span class="o">.</span><span class="n">sum</span><span class="p">()</span> <span class="o">/</span> <span class="n">n_users</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Finally, we convert the 1-d one item tensors to float</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nb">float</span><span class="p">(</span><span class="n">prec</span><span class="p">),</span> <span class="nb">float</span><span class="p">(</span><span class="n">rec</span><span class="p">),</span> <span class="nb">float</span><span class="p">(</span><span class="n">rprec</span><span class="p">)</span>
</span></span></code></pre></div><p>Even if you don&rsquo;t use PyTorch Geometric and you prefer other library, the code should be useful. Just accomodate the edge index and create a similar recommend method and you will be able to calculate r-precision.</p>
<h2 id="sources-and-more-information">Sources and more information</h2>
<ul>
<li><a href="https://pytorch-geometric.readthedocs.io/en/latest/">PyTorch Geometric - Read The Docs</a></li>
<li><a href="https://en.wikipedia.org/wiki/Evaluation_measures_(information_retrieval)#Precision_at_k">Wikipedia - Evaluation measures (information retrieval)</a></li>
<li><a href="https://stackoverflow.com/questions/76837716/how-to-slice-a-2d-tensor-using-a-1d-tensor-instead-of-scalar">StackOverflow - How to slice a 2D tensor using a 1D tensor instead of scalar</a></li>
<li><a href="https://discuss.pytorch.org/t/how-to-use-a-1d-tensor-as-an-index-to-slice-a-2d-tensor/185736">PyTorch Forums - How to use a 1d tensor as an index to slice a 2d tensor</a></li>
</ul>
]]></content:encoded>
    </item>
    
  </channel>
</rss>
