<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://bknyaz.github.io//feed.xml" rel="self" type="application/atom+xml"/><link href="https://bknyaz.github.io//" rel="alternate" type="text/html" hreflang="en"/><updated>2026-08-03T19:19:34+00:00</updated><id>https://bknyaz.github.io//feed.xml</id><title type="html">blank</title><subtitle>Boris Knyazev&apos;s homepage. </subtitle><entry><title type="html">MetaMerge: Model Merging with Meta Networks</title><link href="https://bknyaz.github.io//blog/2026/meta-merge/" rel="alternate" type="text/html" title="MetaMerge: Model Merging with Meta Networks"/><published>2026-02-19T00:00:00+00:00</published><updated>2026-02-19T00:00:00+00:00</updated><id>https://bknyaz.github.io//blog/2026/meta-merge</id><content type="html" xml:base="https://bknyaz.github.io//blog/2026/meta-merge/"><![CDATA[<script src="https://d3js.org/d3.v7.min.js"></script> <p>In AI, the dominant approach to develop neural nets is to first pretrain a large model on a big general dataset and then fine-tune it on specific tasks. In vision, there are, for example, Vision Transformers (ViTs) pretrained on ImageNet and fine-tuned on downstream tasks (e.g., classification of satellite images, textures, etc.). In language modeling, there are, for example, large language models (LLMs) specialized in <a href="https://huggingface.co/bknyaz/Qwen3-0.6B-Math">Math</a>, <a href="https://huggingface.co/bknyaz/Qwen3-0.6B-Code">Coding</a> or specific languages (e.g., <a href="https://huggingface.co/bknyaz/Qwen3-0.6B-Fr">French</a>). Even though the original pretrained model can also perform well on many tasks, <strong>fine-tuning remains essential to achieve optimal performance</strong><d-footnote>Training-free adaptation methods such as in-context learning, prompt tuning and others often show competitive results on some tasks and are less costly. However, direct fine-tuning of model parameters is often more effective and applicable to a wider range of tasks, especially when a lot of domain knowledge is necessary requiring a lot of parameter updates (e.g., some medical tasks or rare languages).</d-footnote>. As a result, <strong>there are hundreds or thousands of models fine-tuned for various tasks.</strong></p> <p>As AI tackles increasingly more tasks, <strong>fine-tuning costs and storage requirements add up</strong>. Consider a situation when we first collected a lot of Math data and fine-tuned an LLM on it, then some other lab released a model excelling at Code and another lab released a model excelling at French. First of all, we now have tripled the number of models to store and maintain. Secondly, despite having three models, we cannot effectively solve mixed tasks such as a Math task in French, since there may be no <strong>single</strong> model trained on such a mixture. To solve the issues in this example, we can collect a desired mix of data and fine-tune the model on it, but this is costly and time-consuming especially if we need to change the mixing ratio. We can also build some smart ensemble with routing conditioned on the input, but doing this effectively is not trivial and still requires some training of the router and storing an entire ensemble<d-cite key="sukhbaatar2024branch,zhou2025mergeme"></d-cite>.</p> <p><strong>Model merging</strong> is a recent <strong>training-free</strong> approach to combine multiple models into one. As many papers and our experiments show, model merging can mitigate limitations of naive fine-tuning in a simple yet effective way<d-cite key="wortsman2022model,ilharco2023editing,yadav2023ties"></d-cite>. <strong>Surprisingly, the resulting merged models perform well on individual tasks and on the mixed tasks they often approach fine-tuning performance, but without any training or data collection.</strong></p> <p>In this post, we introduce a new model merging method called <strong>MetaMerge</strong>.</p> <blockquote> <p>MetaMerge uses a pretrained graph neural network (GNN), that takes weights of multiple models as input and produces the weights for a single merged model as output without any training or finetuning.</p> </blockquote> <p>Before introducing MetaMerge and why it is called “Meta”, let us briefly describe a common baseline approach and several key concepts.</p> <h2 id="tldr">TL;DR</h2> <ul> <li>We introduce MetaMerge, a new model merging method that uses a pretrained graph neural network (GNN).</li> <li>The “Meta” in MetaMerge comes from the fact that both input and output of the pretrained GNN are model weights.</li> <li>MetaMerge does not require any training/finetuning.</li> <li>MetaMerge can be competitive to weight averaging in some cases even though the metamodel (GNN) was not trained for merging, and more critically, the specific model we use for MetaMerge only observed tiny GPT2 style transformers (with &lt;=1.6M parameters) during training.</li> </ul> <h2 id="simple-merge">Simple Merge</h2> <p>In general, model merging can be defined as some aggregation function $f$ that takes weights of $N$ models as input and produces the weights of a single merged model as output:</p> \[\mathbf{W}_{\text{merged}} = f \big( \mathbf{W}_1, \mathbf{W}_2, \ldots, \mathbf{W}_N \big),\] <p>where \(\mathbf{W}_1, \mathbf{W}_2, \ldots, \mathbf{W}_N\) are the weights of the individual models, and \(\mathbf{W}_{\text{merged}}\) are the weights of the merged model.</p> <p>Most of the merging methods implement $f$ directly in the weight space. For example, the simplest way to merge models is to average their weights:</p> \[\mathbf{W}_{\text{merged}} = \frac{1}{N} \sum_{i=1}^{N} \mathbf{W}_i.\] <p>More advanced methods include task arithmetic<d-cite key="ilharco2023editing"></d-cite>, ties-merge<d-cite key="yadav2023ties"></d-cite>, and others<d-footnote>Tools like https://github.com/arcee-ai/mergekit implement many merging methods.</d-footnote>. The averaging method works well only when the models are trained from the same initialization, usually from the same pretrained model. And if they are trained for too long, the performance of the merged model can degrade significantly<d-cite key="frankle2020linear"></d-cite>. In case of <strong>different</strong> initializations or fine-tuning for too long, merging is still possible by using neuron alignment<d-cite key="ainsworth2022git"></d-cite> and other techniques<d-cite key="stoica2024zipit"></d-cite>, but this is out of scope of this post.</p> <h2 id="meta-networks">Meta Networks</h2> <p>The key concept of MetaMerge is <strong>Meta Networks</strong><d-cite key="lim2024graph,kofinas2024graph"></d-cite>. A high level idea of <strong>Meta Networks</strong> is to have a learnable model that can take weights of other models as input to produce some representation or modified weights as output. Hence, the term “Meta” is used. While most merging methods implement $f$ in the weight space directly, some recent methods implement $f$ in the low-dimensional space produced by singular value decomposition (SVD)<d-cite key="stoica2025model,gargiulo2025task"></d-cite>. MetaMerge can be viewed as a more general weight-space transformation learned by a meta-network.</p> <h3 id="nino">NiNo</h3> <p>Among various meta networks, we focus on <a href="https://bknyaz.github.io/blog/2025/nino/">NiNo</a><d-cite key="knyazev2024accelerating"></d-cite>. To the best of our knowledge, a pretrained NiNo checkpoint is the only meta network that can potentially be used for merging models in a realistic setup out-of-the-box, since it can both encode and decode models and it supports diverse architectures, including modern transformers. NiNo is a meta network that was trained to accelerate training. It takes past $c$ checkpoints as input and predicts future (at step $t+K$) parameters as:</p> \[W_{t+K} = \text{NiNo}([W_{t-c+1}, \ldots, W_t])\] <p>This step is applied every 1,000 steps, while Adam or SGD is used for the rest of the steps. So, the original NiNo that we are going to use is not trained for any model merging objective, but for accelerating training. However, it does not mean it cannot be used for merging as we show.</p> <h3 id="nino-for-merging">NiNo for Merging</h3> <p>The only challenge when adapting NiNo to the merging setup is the preparation of the input. Specifically, in the equation above NiNo expects $c$ input models in the order corresponding to an optimization trajectory (where $c=5$ in pretrained NiNo models). One straightforward way to address this issue is to create the “trajectory” as <code class="language-plaintext highlighter-rouge">[pretrained model, model fine-tuned for task 1, model fine-tuned for task 2, ...]</code>. Since tasks do not have a particular order and there may be fewer/more than $c$ tasks, we need certain heuristics. Specifically, for the setup with two tasks, we found the following heuristic to work well in practice:</p> \[W_{\text{merged}} = \text{NiNo}([W_0, W_{\text{task1}},W_{\text{task2}},W_{\text{task2}},W_{\text{task1}}]).\] <p>For the setup with four tasks, the equation is more straightforward since we have exactly 5 models to feed into NiNo:</p> \[W_{\text{merged}} = \text{NiNo}([W_0, W_{\text{task1}},W_{\text{task2}},W_{\text{task3}},W_{\text{task4}}]).\] <p>In principle, the order of the tasks can be optimized, but we fixed it after some trial and error.</p> <h2 id="experiments">Experiments</h2> <p>We evaluate MetaMerge on two vision and two language setups. For vision, we use ViT-B-16 pretrained on ImageNet and fine-tuned on the DTD, RESISC45, MNIST and SVHN datasets. This is a subset from standard 8 tasks used in model merging papers<d-cite key="ilharco2023editing"></d-cite>. For language, we created a similar setup by fine-tuning Qwen3-0.6 and Qwen3-1.7B models on the GSM8K and French datasets<d-footnote>We also released Qwen3-4B models fine-tuned on these datasets and models fine-tuned on the code dataset.</d-footnote>.</p> <h3 id="vit">ViT</h3> <p>For ViT-B-16, we use Task Arithmetic code and their checkpoints for evaluation, which contains trained classification heads for all the task, so merging is done only for the backbone<d-footnote>Combining classification heads with different backbones in the Task Vectors code allows for flexible evaluation (e.g., a model fine-tuned on task B can be evaluated on task A by attaching a respective head).</d-footnote>. We run experiments for 2 and 4 tasks. “N models” in Tables denote the number of models required to obtain the results in a given row. For example, “Finetuned (per task)” requires a fine-tuned model for each task, which is often considered as an upper bound for merging methods. The code to merge ViTs using MetaMerge is provided at <a href="https://github.com/SamsungSAILMontreal/nino/blob/main/merge_vit.py">merge_vit.py</a>.</p> <p><strong>Table 1. Results using ViT-B-16 on 2 tasks.</strong></p> <table> <thead> <tr> <th>Model</th> <th>N models</th> <th>DTD</th> <th>RESISC45</th> <th>Avg</th> </tr> </thead> <tbody> <tr> <td>Zero Shot (pretrained)</td> <td>1</td> <td>44.68</td> <td>66.38</td> <td>55.53</td> </tr> <tr> <td>Finetuned DTD</td> <td>1</td> <td>82.07</td> <td>50.54</td> <td>66.31</td> </tr> <tr> <td>Finetuned RESISC45</td> <td>1</td> <td>36.44</td> <td>96.89</td> <td>66.67</td> </tr> <tr> <td>Finetuned (per task)</td> <td>2</td> <td>82.07</td> <td>96.89</td> <td>89.48</td> </tr> <tr> <td>Merged Model (avg merge)</td> <td>1</td> <td>72.66</td> <td>94.13</td> <td>83.39</td> </tr> <tr> <td>Merged Model (meta merge, mlp)</td> <td>1</td> <td>78.99</td> <td>90.43</td> <td>84.71</td> </tr> <tr> <td>Merged Model (meta merge)</td> <td>1</td> <td>76.76</td> <td>91.60</td> <td>84.18</td> </tr> </tbody> </table> <p><strong>Table 2. Results using ViT-B-16 on 4 tasks.</strong></p> <table> <thead> <tr> <th>Model</th> <th>N models</th> <th>DTD</th> <th>RESISC45</th> <th>MNIST</th> <th>SVHN</th> <th>Avg</th> </tr> </thead> <tbody> <tr> <td>Zero Shot (pretrained)</td> <td>1</td> <td>44.68</td> <td>66.38</td> <td>51.73</td> <td>51.99</td> <td>53.70</td> </tr> <tr> <td>Finetuned (per task)</td> <td>4</td> <td>82.07</td> <td>96.89</td> <td>99.76</td> <td>97.86</td> <td>94.15</td> </tr> <tr> <td>Merged Model (avg merge)</td> <td>1</td> <td>57.18</td> <td>84.06</td> <td>98.55</td> <td>87.28</td> <td>81.77</td> </tr> <tr> <td>Merged Model (meta merge, mlp)</td> <td>1</td> <td>30.90</td> <td>30.90</td> <td>98.72</td> <td>97.60</td> <td>62.44</td> </tr> <tr> <td>Merged Model (meta merge)</td> <td>1</td> <td>46.54</td> <td>78.46</td> <td>98.43</td> <td>96.23</td> <td>79.92</td> </tr> </tbody> </table> <p>Surprisingly, on 2 tasks (Table 1) the ablated NiNo variant (without the GNN part) actually outperforms the full NiNo even though the full one excelled at accelerating training as shown in the NiNo paper’s ablations. One logical explanation is that merging is quite different from predicting future parameters, so good results on merging are not expected in the first place. Also, the MLP version of NiNo has a simplicity inductive bias so its predictions can be more generic (kind of trend prediction which may be closer to weight averaging) and less overfitted to the training objective of accelerating training. On 4 tasks (Table 2), the ablated variant performs much worse. In future work, it would be interesting to push NiNo’s performance specifically for merging, however, defining a proper objective for that is not trivial.</p> <p>Below we visualize detailed results from Table 2 for all the tasks and models.</p> <div id="model-comparison-chart" style="width: 100%; height: 700px;"></div> <div class="caption" style="text-align: center; margin-top: -35px; margin-bottom: 25px;"> Model Performance Across Datasets. Comparing accuracy across DTD, RESISC45, MNIST, and SVHN datasets. </div> <script src="/assets/js/metamerge/radar-plot.js"></script> <h3 id="qwen3">Qwen3</h3> <p>In the language setup, we first fine-tuned base Qwen3 models on the train split of <a href="https://huggingface.co/datasets/openai/gsm8k">GSM8K</a> and the subset of <a href="https://huggingface.co/datasets/kurakurai/luth-sft">luth-sft</a>. These are Math and French datasets, respectively. We chose to fine-tune our own models instead of using existing checkpoints to have a more controlled consistent setup for different model sizes<d-footnote>Our fine-tuning is likely to be far from compute and performance efficient, but our goal is not to provide SOTA models.</d-footnote>. Then we evaluate on the test split of GSM8K, <a href="https://github.com/EleutherAI/lm-evaluation-harness/tree/main/lm_eval/tasks/french_bench">FrenchBench</a> and <a href="https://huggingface.co/datasets/cmh/gsm8k_fr">GSM8K-Fr</a> (GSM8K translated to French). See details about the evaluation pipeline and model checkpoints at 🤗<a href="https://huggingface.co/collections/SamsungSAILMontreal/qwen3-small">SamsungSAILMontreal/qwen3-small</a>. We report Qwen3-0.6B and Qwen3-1.7B results in Tables 3 and 4, respectively. However, we also have Qwen3-4B results and checkpoints on the provided link.</p> <p>In Qwen3 experiments, we do not have a model fine-tuned on GSM8K-Fr to showcase a common scenario when a training dataset for the mixed task is unavailable, which is prevailing for rare mixes. Therefore, Qwen3 (fine-tuned per task) only contains two models. Models with “Math-Fr” in their name are obtained by merging the Math and French models using either simple averaging or MetaMerge. The code to merge Qwen models using MetaMerge is provided at <a href="https://github.com/SamsungSAILMontreal/nino/blob/main/merge_qwen.py">merge_qwen.py</a>.</p> <p><strong>Table 3. Results using Qwen3-0.6B on 3 tasks.</strong></p> <table> <thead> <tr> <th>Model</th> <th>N models</th> <th>GSM8K</th> <th>French</th> <th>GSM8K-Fr</th> <th>avg</th> </tr> </thead> <tbody> <tr> <td>Qwen3-0.6B</td> <td>1</td> <td>21.0</td> <td>24.4</td> <td>19.6</td> <td>21.7</td> </tr> <tr> <td>Qwen3-0.6B-Math</td> <td>1</td> <td>46.3</td> <td>25.4</td> <td>29.2</td> <td>33.6</td> </tr> <tr> <td>Qwen3-0.6B-Fr</td> <td>1</td> <td>36.1</td> <td>26.5</td> <td>26.5</td> <td>29.7</td> </tr> <tr> <td>Qwen3-0.6B (fine-tuned per task)</td> <td>2</td> <td>46.3</td> <td>26.5</td> <td>26.5</td> <td>33.1</td> </tr> <tr> <td>Qwen3-0.6B-Math-Fr (avg merge)</td> <td>1</td> <td>48.4</td> <td>27.4</td> <td>33.9</td> <td>36.6</td> </tr> <tr> <td>Qwen3-0.6B-Math-Fr (meta merge, mlp)</td> <td>1</td> <td>47.8</td> <td>25.8</td> <td>30.9</td> <td>34.8</td> </tr> <tr> <td>Qwen3-0.6B-Math-Fr (meta merge)</td> <td>1</td> <td>45.1</td> <td>25.7</td> <td>31.6</td> <td>34.1</td> </tr> </tbody> </table> <p><strong>Table 4. Results using Qwen3-1.7B on 3 tasks.</strong></p> <table> <thead> <tr> <th>Model</th> <th>N models</th> <th>GSM8K</th> <th>French</th> <th>GSM8K-Fr</th> <th>avg</th> </tr> </thead> <tbody> <tr> <td>Qwen3-1.7B</td> <td>1</td> <td>20.6</td> <td>26.2</td> <td>20.2</td> <td>22.3</td> </tr> <tr> <td>Qwen3-1.7B-Math</td> <td>1</td> <td>62.1</td> <td>28.3</td> <td>41.5</td> <td>43.9</td> </tr> <tr> <td>Qwen3-1.7B-Fr</td> <td>1</td> <td>60.9</td> <td>32.8</td> <td>43.9</td> <td>45.9</td> </tr> <tr> <td>Qwen3-1.7B (fine-tuned per task)</td> <td>2</td> <td>62.1</td> <td>32.8</td> <td>43.9</td> <td>46.3</td> </tr> <tr> <td>Qwen3-1.7B-Math-Fr (avg merge)</td> <td>1</td> <td>64.0</td> <td>31.4</td> <td>46.9</td> <td>47.4</td> </tr> <tr> <td>Qwen3-1.7B-Math-Fr (meta merge, mlp)</td> <td>1</td> <td>63.0</td> <td>28.3</td> <td>43.8</td> <td>45.0</td> </tr> <tr> <td>Qwen3-1.7B-Math-Fr (meta merge)</td> <td>1</td> <td>62.7</td> <td>28.4</td> <td>43.2</td> <td>44.8</td> </tr> </tbody> </table> <p>For Qwen3-0.6B, the simple averaging method outperforms MetaMerge and as in ViT (on 2 tasks), the MLP version of NiNo outperforms the full NiNo. One possible explanation is that the pretrained NiNo has only seen tiny GPT2 style transformers (with &lt;=1.6M parameters) during training. So making predictions for a Qwen architecture with 600M parameters is an extreme out-of-distribution scenario for NiNo. As in ViT experiments, the MLP version of NiNo performs slightly better than the full model potentially due to its simplicity inductive bias. Qwen3-1.7B is even a more severe out-of-distribution scenario for NiNo, so it is not surprising that MetaMerge does not perform as well as simple averaging. Despite the average benchmark scores being worse or comparable, an interesting question for future studies is whether MetaMerge produces functionally different models than simple averaging or the ablated NiNo variant.</p> <h2 id="visualization-of-metamerge">Visualization of MetaMerge</h2> <p>In this demo, we compare weight averaging to MetaMerge on ViT using 4 tasks. We sampled 100 weight positions per layer (same positions for all models) to enable efficient visualization. For each position, parameters are ordered in the way we pass them to NiNo (i.e., pretrained, dtd, resisc45, mnist, svhn). Since NiNo was trained to predict future parameters, we show a MetaMerge prediction as the last point on the trajectory. The weight averaging baseline is shown as a horizontal line for comparison.</p> <div id="weight-explorer" data-weights-file="/assets/data/network_weights.csv"> <div class="controls" style="margin-bottom: 1.5rem;"> <div class="slider-group"> <div> <label for="layer-slider">Layer:</label> <div class="slider-container layer-slider-container"> <input type="range" id="layer-slider" class="slider" min="0" max="0" value="0"/> <span id="layer-value" class="slider-value">-</span> </div> </div> <div> <label for="neuron-slider">Weight:</label> <div class="slider-container neuron-slider-container"> <input type="range" id="neuron-slider" class="slider" min="0" max="0" value="0"/> <span id="neuron-value" class="slider-value">-</span> </div> </div> </div> </div> <div id="weight-plot"></div> <div id="legend" class="chart-legend"></div> </div> <script src="/assets/js/metamerge/weight-explorer.js"></script> <p>MetaMerge is used for all layers in the visual backbone (starting with “visual.”) except for conv1 as explained in our <a href="https://github.com/SamsungSAILMontreal/nino/blob/main/merge_vit.py">merge_vit.py</a> code and layers not starting with “visual.”. For those we use weight averaging, so in the visualization the “meta-merge” point is the same as the “average” in such cases. Overall, the visualization shows that there is no simple relationship between the input models and the MetaMerge prediction. But the visualization may give some insights on problematic behavior (e.g., for biases the MetaMerge predictions tend to be too far from the overall trajectory).</p> <h2 id="conclusion">Conclusion</h2> <ul> <li>MetaMerge shows competitive performance to weight averaging in some experiments even though the metamodel (GNN) was not trained for merging.</li> <li>In some cases, MetaMerge underperforms like due to the distribution shifts and a different objective. So we believe there is a lot of potential for improving MetaMerge by training a metamodel with an objective more aligned with merging.</li> <li>Besides introducing MetaMerge, we also released fine-tuned and merged Qwen3 models obtained in a controllable way that could be used for merging experiments.</li> </ul> <h3 id="license">License</h3> <p>Diagrams and text are licensed under Creative Commons Attribution <a href="https://creativecommons.org/licenses/by/4.0/">CC-BY 4.0</a>, unless noted otherwise.</p> <h3 id="citation">Citation</h3> <div class="language-bibtex highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">@inproceedings</span><span class="p">{</span><span class="nl">camacho2026meta</span><span class="p">,</span>
  <span class="na">title</span><span class="p">=</span><span class="s">{Meta-Merging by Checkpoint Nowcasting}</span><span class="p">,</span>
  <span class="na">author</span><span class="p">=</span><span class="s">{Camacho, Albert Manuel Orozco and Knyazev, Boris and Belilovsky, Eugene and Wolf, Guy}</span><span class="p">,</span>
  <span class="na">booktitle</span><span class="p">=</span><span class="s">{ICML 2026 Workshop on Weight-Space Symmetries: from Foundations to Practical Applications}</span><span class="p">,</span>
  <span class="na">year</span><span class="p">=</span><span class="s">{2026}</span><span class="p">,</span>
  <span class="na">url</span><span class="p">=</span><span class="s">{https://openreview.net/forum?id=whl40fLEnM}</span>
<span class="p">}</span>
</code></pre></div></div>]]></content><author><name>Boris Knyazev &amp; Albert M. Orozco Camacho</name></author><category term="nino"/><category term="merging"/><category term="gnn"/><category term="neural-graphs"/><category term="empirical-study"/><category term="research"/><summary type="html"><![CDATA[Merging ViTs and LLMs using a pretrained (graph) neural net.]]></summary></entry><entry><title type="html">REAM: Compressing Mixture-of-Experts LLMs</title><link href="https://bknyaz.github.io//blog/2026/moe/" rel="alternate" type="text/html" title="REAM: Compressing Mixture-of-Experts LLMs"/><published>2026-01-15T00:00:00+00:00</published><updated>2026-01-15T00:00:00+00:00</updated><id>https://bknyaz.github.io//blog/2026/moe</id><content type="html" xml:base="https://bknyaz.github.io//blog/2026/moe/"><![CDATA[<p>Large language models (LLMs) are all different in some way. But one notable consistency among them is the reliance on the Mixture-of-Experts (MoE) architecture. Top performing MoE LMMs include DeepSeek, Qwen, Mixtral, Kimi, Llama 4, Cohere, gpt-oss, Grok and others.<d-footnote>These are families of LLMs with many variants within the family. Typically, the most performant architecture within each family is an MoE.</d-footnote></p> <p>When you deploy a very large model, the first challenge you will face would likely be the <strong>memory demand</strong>. For example, one of the largest and performant Qwen3 MoE models (Qwen3-235B-A22B) requires around 235B params $\times$ 2 bytes/param = <strong>470GB</strong> of VRAM (e.g., 6$\times$GPUs each having 80GB of VRAM such as NVIDIA H100) to merely load all its parameters on GPUs in 16 bit precision (see the sidenote on the right). Traditional approaches to deal with this challenge include pruning and quantization (and of course getting more GPUs 💰 💰 💰).</p> <aside> <p> In practice, frameworks like vllm optimize memory usage with quantization, CPU offloading, etc. However, these often come at the cost of increased complexity, potential performance degradation or slower inference. </p> </aside> <p><strong>I explore an alternative approach specific to MoE, which is reducing the number of experts by merging groups of experts, which I call REAM.</strong></p> <blockquote> <p>REAM stands for Router-weighted Expert Activation <strong>Merging</strong> motivated by the Router-weighted Expert Activation <strong>Pruning</strong> (REAP) method<d-cite key="lasby2025reap"></d-cite>.</p> </blockquote> <p>The core idea is that merging can be more effective than pruning as it aims to preserve functions by leveraging all experts instead of discarding some of them.</p> <p>But before going into details, let me first describe the MoE architecture and its benefits. Then, I will present my results of merging experts in Qwen3-30B-A3B, Qwen3-235B-A22B and Qwen3-Next-80B-A3B<d-footnote>Their Instruct versions.</d-footnote>, reducing the number of experts by 25%<d-footnote>From 128 to 96 for Qwen3-30B-A3B and Qwen3-235B-A22B, and from 512 to 384 for Qwen3-Next.</d-footnote>, thereby <strong>reducing memory demands by around 25%</strong><d-footnote>Since most (&gt;98% in Qwen3) of parameters belong to MoE layers, reducing the number of experts is roughly equivalent to reducing the number of parameters (and hence, memory) in the overall model.</d-footnote>, while maintaining high performance across multiple benchmarks.</p> <h2 id="tldr">TL;DR</h2> <ul> <li>I propose REAM, a retraining-free expert merging method for MoE LLMs.</li> <li>REAM is a sequential merging algorithm using REAP-based saliency<d-cite key="lasby2025reap"></d-cite>.</li> <li>It reduces experts by 25% with strong results on Qwen3 MoE LLMs.</li> <li>REAM outperforms REAP and HC-SMoE<d-cite key="chen2025retrainingfree"></d-cite> on multiple tasks.</li> <li>Compressed Qwen3 models are released on <a href="https://huggingface.co/collections/SamsungSAILMontreal/ream">huggingface</a>🤗.</li> </ul> <h2 id="introducing-moe">Introducing MoE</h2> <p>Mixture-of-Experts (MoE) is an old idea developed in 1989-1991 (e.g., see Robert A. Jacobs et al.<d-cite key="jacobs1991adaptive"></d-cite>). MoEs were later proved useful in deep learning, including for language models in 2017 by Noam Shazeer et al.<d-cite key="shazeer2017outrageously"></d-cite>, then scaled to 1T parameter models in 2022 by William Fedus et al.<d-cite key="fedus2022switch"></d-cite>.</p> <div class="row mt-3 l-body figure-container-desktop"> <div class="col-sm mt-3 mt-md-0" style="text-align: center;"> <figure> <picture> <source class="responsive-img-srcset" srcset="/assets/img/2026-01-15-moe/moe_2022-480.webp 480w,/assets/img/2026-01-15-moe/moe_2022-800.webp 800w,/assets/img/2026-01-15-moe/moe_2022-1400.webp 1400w," type="image/webp" sizes="95vw"/> <img style="display: block; margin: auto;" src="/assets/img/2026-01-15-moe/moe_2022.png" class="img-fluid d-block mx-auto rounded z-depth-1" width="100%" height="auto" data-zoomable="" loading="lazy" onerror="this.onerror=null; $('.responsive-img-srcset').remove();"/> </picture> </figure> </div> </div> <div class="caption" style="text-align: center; margin-top: -35px; margin-bottom: 25px;"> MoE with $N=4$. Figure from Fedus et al., 2022<d-cite key="fedus2022switch"></d-cite>. The figure shows only one expert (TopK=1) activated per token for clarity. Previously MoEs were often called "Switch Transformers". </div> <p>As illustrated in the Figure above and formally defined below, MoE is a special neural network layer consisting of multiple expert networks ($E$)<d-footnote>I drop the layer index for simplicity as the same equation is used for all layers (with trainable parameters in all layers being different unless shared).</d-footnote> and a gating network ($g$) that selects which experts to use for $d$-dimensional input $\mathbf{x} \in \mathbb{R}^{n \times d}$ with $n$ tokens:</p> \[\mathbf{y}(\mathbf{x}) = \sum_{i=1}^{N} \underbrace{g(\mathbf{x})_i}_{\text{gate out}} \, \underbrace{E_i(\mathbf{x})}_{\text{expert out}},\] <p>where each $E_i$ has the same architecture as an FFN<d-footnote>FFN stands for "feed-forward network", which is usually implemented as a simple MLP or a Gated Linear Unit (GLU) in Transformers.</d-footnote> layer in Transformers. The gating network is typically implemented as<d-footnote>The order of the Softmax and TopKMask operations can vary in implementations.</d-footnote>:</p> \[g(\mathbf{x}) = \text{TopKMask} \big( \text{Softmax} ( \underbrace{\mathbf{x} \, \mathbf{W}_g^T}_{\in \mathbb{R}^{n \times N}} ), \, \text{TopK} \big),\] <p>where $\mathbf{W}_g \in \mathbb{R}^{N \times d}$ are the gating weights, and TopKMask is an operation that retains only the TopK values per row (i.e. per token) and sets the rest to zero. Softmax is applied along each row to produce a probability distribution over the $N$ experts.</p> <h3 id="expert-network-architecture">Expert Network Architecture</h3> <div class="row mt-3 l-body figure-container-desktop-small"> <div class="col-sm mt-3 mt-md-0" style="text-align: center;"> <figure> <picture> <source class="responsive-img-srcset" srcset="/assets/img/2026-01-15-moe/glu-480.webp 480w,/assets/img/2026-01-15-moe/glu-800.webp 800w,/assets/img/2026-01-15-moe/glu-1400.webp 1400w," type="image/webp" sizes="95vw"/> <img style="display: block; margin: auto;" src="/assets/img/2026-01-15-moe/glu.png" class="img-fluid d-block mx-auto rounded z-depth-1" width="100%" height="auto" data-zoomable="" loading="lazy" onerror="this.onerror=null; $('.responsive-img-srcset').remove();"/> </picture> </figure> </div> </div> <div class="caption" style="text-align: center; margin-top: -35px; margin-bottom: 25px;"> Simple FFN (left) vs GLU-based FFN used in modern MoEs (right). Figure from <a href="https://medium.com/@achronus/glu-a-simple-transformer-improvement-504e31c4252a">medium.com/@achronus</a>. </div> <p>Each expert network $E_i$ is an FFN layer that processes the input $\mathbf{x}$ independently of other experts. Recent FFN layers in both original (non-MoE or “dense”) and MoE Transformers are based on Gated Linear Units (GLU)<d-cite key="dauphin2017language, shazeer2020glu"></d-cite> instead of simple MLPs:</p> \[E_i(\mathbf{x}) = \mathbf{W}_{i,\text{down}} \big( \text{act} (\mathbf{W}_{i,\text{gate}} \mathbf{x}) \odot (\mathbf{W}_{i,\text{up}} \mathbf{x}) \big), \; \text{for} \; i=1,...,N,\] <p>where act is SiLU/Swish or GELU, $\odot$ is element-wise multiplication; \(\mathbf{W}_{i,\text{gate}}\) and \(\mathbf{W}_{i,\text{up}}\)<br/> project \(\mathbf{x}\) to a lower dimensional space (e.g., 2048 to 768 in Qwen3), \(\mathbf{W}_{i,\text{down}}\) projects it back to the high dimensional space.</p> <hr/> <h3 id="key-concepts-of-moe">Key Concepts of MoE</h3> <p>Having defined an MoE layer above, let’s highlight the following key MoE concepts to better understand expert pruning and merging techniques:</p> <ol> <li>MoE layers replace regular FFN (MLP) layers in Transformers (see the first Figure on the right). So for example in a Qwen3 model with 96 Transformer layers, there are 96 MoE layers (that are interleaved with self-attention layers within each Transformer layer).</li> <li>Each MoE layer has $N$ experts (e.g., 128 in Qwen3 MoE models) with a Gated Linear Unit architecture described above. Having many FFNs massively increases the total number of parameters compared to non-MoE (dense) models (with $N=1$).</li> <li>The gating network $g$ selects TopK experts (e.g., 8 in Qwen3 MoE models) for <strong>each input token</strong> based on its representation (row in $\mathbf{x}$) in the current Transformer layer. So for example, some experts in early layers learn to specialize on punctuation or number tokens, while in later layers they can specialize on more abstract concepts.</li> <li>During inference, only the selected experts are used in the forward pass, making MoEs efficient (in terms of FLOPs) despite their large number of parameters. <strong>However, to make inference efficient in practice, expert weights need to be stored on GPUs to avoid data transfer overheads creating the memory demand.</strong></li> </ol> <p>Further technical details of MoE layers can be found in papers <d-cite key="shazeer2017outrageously, fedus2022switch"></d-cite> and a more animated and educational explanation in <a href="https://newsletter.maartengrootendorst.com/p/a-visual-guide-to-mixture-of-experts.">Maarten Grootendorst’s blog post</a>.</p> <hr/> <h3 id="strengths-of-moe">Strengths of MoE</h3> <p>Although dense Transformers can learn from diverse data well, MoEs more explicitly promote distributed specialized knowledge. This is achieved by having multiple experts that can each specialize in different aspects of the data with the help of the gating mechanism that promotes sparsity.</p> <p>It has been also shown that MoEs can scale well given a fixed FLOPs budget at inference (see the Figures below). Specifically, Tay et al. in 2023<d-cite key="tay2023scaling"></d-cite> showed that MoEs (or Switch Transformers back then) were one of the few architectures that demonstrated strong scaling trends in both upstream (pretraining) and downstream (fine-tuning) cases.</p> <div class="row mt-3 l-body" style="text-align: center;"> <div class="col-sm mt-3 mt-md-0" style="text-align: center;"> <figure> <picture> <source class="responsive-img-srcset" srcset="/assets/img/2026-01-15-moe/scaling_upstream-480.webp 480w,/assets/img/2026-01-15-moe/scaling_upstream-800.webp 800w,/assets/img/2026-01-15-moe/scaling_upstream-1400.webp 1400w," type="image/webp" sizes="95vw"/> <img style="display: block; margin: auto;" src="/assets/img/2026-01-15-moe/scaling_upstream.png" class="img-fluid d-block mx-auto rounded z-depth-1" width="100%" height="auto" data-zoomable="" loading="lazy" onerror="this.onerror=null; $('.responsive-img-srcset').remove();"/> </picture> </figure> </div> </div> <div class="caption" style="text-align: center; margin-top: -35px; margin-bottom: -25px;"> Upstream performance comparison (pretraining). Figure from Tay et al., 2023<d-cite key="tay2023scaling"></d-cite>. </div> <div class="row mt-3 l-body" style="text-align: center; "> <div class="col-sm mt-3 mt-md-0" style="text-align: center; margin-top: -25px;"> <figure> <picture> <source class="responsive-img-srcset" srcset="/assets/img/2026-01-15-moe/scaling_downstream-480.webp 480w,/assets/img/2026-01-15-moe/scaling_downstream-800.webp 800w,/assets/img/2026-01-15-moe/scaling_downstream-1400.webp 1400w," type="image/webp" sizes="95vw"/> <img style="display: block; margin: auto;" src="/assets/img/2026-01-15-moe/scaling_downstream.png" class="img-fluid d-block mx-auto rounded z-depth-1" width="100%" height="auto" data-zoomable="" loading="lazy" onerror="this.onerror=null; $('.responsive-img-srcset').remove();"/> </picture> </figure> </div> </div> <div class="caption" style="text-align: center; margin-top: -35px; margin-bottom: 25px;"> Downstream performance comparison (fine-tuned). Figure from Tay et al., 2023<d-cite key="tay2023scaling"></d-cite>. </div> <p>As figures above show, Switch Transformers still underperformed dense Transformers at the largest scale. However, the design choices and training of MoEs have improved since then (e.g., see<d-cite key="muqeeth2024soft"></d-cite>), leading to even better scaling trends in recent MoE LLMs. Moreover, recently Kimi MoE LLMs<d-cite key="team2025kimi"></d-cite> demonstrated <strong>strong scaling trends with increasing MoE sparsity</strong> (see the Figure below). They define sparsity as the total number of experts ($N$) divided by TopK showing that for a fixed TopK increasing $N$ and, hence, sparsity consistently improves results.</p> <div class="row mt-3 l-body figure-container-desktop"> <div class="col-sm mt-3 mt-md-0" style="text-align: center;"> <figure> <picture> <source class="responsive-img-srcset" srcset="/assets/img/2026-01-15-moe/kimi_k2_sparsity_scaling-480.webp 480w,/assets/img/2026-01-15-moe/kimi_k2_sparsity_scaling-800.webp 800w,/assets/img/2026-01-15-moe/kimi_k2_sparsity_scaling-1400.webp 1400w," type="image/webp" sizes="95vw"/> <img style="display: block; margin: auto;" src="/assets/img/2026-01-15-moe/kimi_k2_sparsity_scaling.png" class="img-fluid d-block mx-auto rounded z-depth-1" width="100%" height="auto" data-zoomable="" loading="lazy" onerror="this.onerror=null; $('.responsive-img-srcset').remove();"/> </picture> </figure> </div> </div> <div class="caption" style="text-align: center; margin-top: -35px; margin-bottom: 25px;"> Sparsity Scaling Law. Each point corresponds to training a model with a different number of experts and different hyperparameters (depth/width/number of steps/etc.). For example, for loss=1.5, sparsity=48 reduces FLOPs by 1.69$\times$, 1.39$\times$ and 1.15$\times$ compared to sparsity=8, 16 and 32, respectively. Figure from the Kimi K2 paper<d-cite key="team2025kimi"></d-cite>. </div> <p>These findings explain increasing the number of experts to 384 in Kimi K2 compared to 256 in DeepSeek and 128 in Qwen3 (more recent Qwen3-Next-80B-A3B also increased $N$ to 512). Even though the total number of parameter is soared to 1T, the inference remains efficient (with only TopK=8 experts activated). <strong>However, the memory demand to store all expert weights on GPUs becomes a bottleneck for deploying such large MoE LLMs.</strong></p> <h2 id="background-moe-compression">Background: MoE Compression</h2> <p>Let me briefly describe some key expert pruning and merging techniques below before presenting my approach and results. I follow an assumption standard in the MoE compression literature that there is a pretrained MoE model with $N$ experts per MoE layer and the goal is to reduce it to $k &lt; N$ experts. I do not consider retraining/fine-tuning (or any further compression) after pruning/merging experts, which could be a complementary step.</p> <div class="row mt-3 l-body figure-container-desktop"> <div class="col-sm mt-3 mt-md-0" style="text-align: center;"> <figure> <picture> <source class="responsive-img-srcset" srcset="/assets/img/2026-01-15-moe/pruning_vs_merging-480.webp 480w,/assets/img/2026-01-15-moe/pruning_vs_merging-800.webp 800w,/assets/img/2026-01-15-moe/pruning_vs_merging-1400.webp 1400w," type="image/webp" sizes="95vw"/> <img style="display: block; margin: auto;" src="/assets/img/2026-01-15-moe/pruning_vs_merging.png" class="img-fluid d-block mx-auto rounded z-depth-1" width="100%" height="auto" data-zoomable="" loading="lazy" onerror="this.onerror=null; $('.responsive-img-srcset').remove();"/> </picture> </figure> </div> </div> <div class="caption" style="text-align: center; margin-top: -35px; margin-bottom: 25px;"> Expert pruning vs merging. Figure from the HC-SMoE paper<d-cite key="chen2025retrainingfree"></d-cite>. </div> <h3 id="pruning-experts">Pruning Experts</h3> <p>Given hundreds of experts in each MoE layer, it is tempting to assume there is some redundancy in them and aim to reduce their number without sacrificing performance too much. One straightforward approach to do so is to prune some experts based on their <em>activation frequencies</em>. These frequencies are computed based on gate logits $g(\mathbf{x})$ by counting how often each of the $N$ experts is among TopK experts. For that purpose, we can forward pass some <em>calibration data</em> through the MoE layer and compute the activation frequencies (or saliency scores, $A$) for each expert. Given $A$, we keep only $ k &lt; N $ experts with the highest scores.</p> <p>The following is my pseudocode of computing activation frequencies $A$.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Pseudocode for computing activation frequencies
</span><span class="n">N</span> <span class="o">=</span> <span class="mi">128</span>  <span class="c1"># number of experts
</span><span class="n">n</span> <span class="o">=</span> <span class="mi">1024</span>  <span class="c1"># number of tokens in calibration data
</span><span class="n">topk</span> <span class="o">=</span> <span class="mi">8</span>  <span class="c1"># TopK experts activated per token (based on the model configuration)
</span>
<span class="n">g</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">randn</span><span class="p">(</span><span class="n">n</span><span class="p">,</span> <span class="n">N</span><span class="p">)</span>  <span class="c1"># assume router logits for n tokens and N experts after some layer
</span><span class="n">g</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">softmax</span><span class="p">(</span><span class="n">g</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"># apply softmax according to Equation above
</span><span class="n">topk_indices</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">topk</span><span class="p">(</span><span class="n">g</span><span class="p">,</span> <span class="n">k</span><span class="o">=</span><span class="n">topk</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">indices</span>  <span class="c1"># (n, topk)
# count how often each expert is selected
</span><span class="n">A</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">zeros</span><span class="p">(</span><span class="n">N</span><span class="p">)</span>
<span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="n">N</span><span class="p">):</span>
    <span class="n">A</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="n">topk_indices</span> <span class="o">==</span> <span class="n">i</span><span class="p">).</span><span class="nf">sum</span><span class="p">().</span><span class="nf">item</span><span class="p">()</span>
<span class="c1"># keep k &lt; N experts with highest A
</span></code></pre></div></div> <p>Recent Router-weighted Expert Activation Pruning (REAP) method<d-cite key="lasby2025reap"></d-cite> significantly improves simple frequency counting. REAP defines $i$-th expert’s importance ($S_i$) based on the expert output’s norm weighted by the gate output:</p> \[S_i = \frac{1}{|\mathbf{x}^{(i)}|} \sum g_i(\mathbf{x}^{(i)}) \, || \, E_i(\mathbf{x}^{(i)}) || \text{ for } i=1,...,N,\] <p>where \(\mathbf{x}^{(i)}\) is the subset of \(\mathbf{x}\) corresponding to the tokens that activated expert $ i $. Compared to activation frequencies ($A$), scores $S$ measure more accurately the contribution of each expert to the final output of the MoE layer.</p> <p>The following is my pseudocode of computing REAP scores based on the equation above<d-footnote>My implementation may differ from the official REAP code https://github.com/CerebrasResearch/reap. I implement a simple version aligned with the equation.</d-footnote>.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Pseudocode for computing REAP scores
</span><span class="n">N</span> <span class="o">=</span> <span class="mi">128</span>  <span class="c1"># number of experts
</span><span class="n">n</span> <span class="o">=</span> <span class="mi">1024</span>  <span class="c1"># number of tokens in calibration data
</span><span class="n">topk</span> <span class="o">=</span> <span class="mi">8</span>  <span class="c1"># TopK experts activated per token (based on the model configuration)
</span><span class="n">d</span> <span class="o">=</span> <span class="mi">2048</span> <span class="c1"># expert output dimension
</span>
<span class="n">expert_activations</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">randn</span><span class="p">(</span><span class="n">N</span><span class="p">,</span> <span class="n">n</span><span class="p">,</span> <span class="n">d</span><span class="p">)</span>  <span class="c1"># assume expert outputs for n tokens and N experts after some layer
</span>
<span class="n">g</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">randn</span><span class="p">(</span><span class="n">n</span><span class="p">,</span> <span class="n">N</span><span class="p">)</span>  <span class="c1"># assume router logits for n tokens and N experts after some layer
</span><span class="n">g</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">softmax</span><span class="p">(</span><span class="n">g</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"># apply softmax according to Equation above
</span><span class="n">topk_values</span><span class="p">,</span> <span class="n">topk_indices</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">topk</span><span class="p">(</span><span class="n">g</span><span class="p">,</span> <span class="n">k</span><span class="o">=</span><span class="n">topk</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"># (n, topk)
</span>
<span class="c1"># compute REAP score for each expert
</span><span class="n">S</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">zeros</span><span class="p">(</span><span class="n">N</span><span class="p">)</span>
<span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="n">N</span><span class="p">):</span>
    <span class="c1"># get tokens routed to expert i
</span>    <span class="n">top_x</span><span class="p">,</span> <span class="n">idx</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="n">topk_indices</span> <span class="o">==</span> <span class="n">i</span><span class="p">)</span>
    <span class="c1"># top_x - indices of tokens activating expert i (values between 0 and n-1)
</span>    <span class="c1"># idx - indices of experts in topk (values between 0 and topk-1)
</span>    <span class="k">if</span> <span class="nf">len</span><span class="p">(</span><span class="n">idx</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
        <span class="k">continue</span>  <span class="c1"># no tokens activated expert i
</span>    <span class="n">expert_state</span> <span class="o">=</span> <span class="n">expert_activations</span><span class="p">[</span><span class="n">i</span><span class="p">,</span> <span class="n">top_x</span><span class="p">]</span>  <span class="c1"># (selected tokens, d)
</span>    <span class="n">S</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="n">expert_state</span><span class="p">.</span><span class="nf">norm</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">topk_values</span><span class="p">[</span><span class="n">top_x</span><span class="p">,</span> <span class="n">idx</span><span class="p">]).</span><span class="nf">mean</span><span class="p">().</span><span class="nf">item</span><span class="p">()</span>
<span class="c1"># keep k &lt; N experts with highest S
</span></code></pre></div></div> <h3 id="merging-experts">Merging Experts</h3> <p>Compared to pruning experts, several works have explored merging them instead (e.g., <d-cite key="li2023merge,chen2025retrainingfree"></d-cite>). The algorithms to merge experts typically have two steps applied to each MoE layer:</p> <ol> <li>Grouping or clustering similar experts together based on some similarity metric.</li> <li>Combining the weights of experts within each group to form a single expert.</li> </ol> <p><strong>Step 1 (grouping)</strong> is usually the key difference across the methods. For example, MC-SMoE<d-cite key="li2023merge"></d-cite> finds $k$ cluster centroids first. To do so, the activation frequencies ($A$), defined above, are used as in pruning methods. However, in contrast to simply removing low-score experts as in pruning, MC-SMoE assigns them to the centroids based on the similarity of <em>expert representations</em> (e.g., expert weights or expert/gate logits given some calibration data). In HC-SMoE<d-cite key="chen2025retrainingfree"></d-cite>, the grouping step is based on hierarchical clustering of experts based on their outputs (also given some calibration data). HC-SMoE outperformed MC-SMoE, therefore I use HC-SMoE as a baseline in my experiments below.</p> <p><strong>Step 2 (combining)</strong> in both MC-SMoE and HC-SMoE involves weighted averaging of expert weights within each group, where the weighting coefficients are computed as in pruning methods (i.e., based on normalized activation frequencies $A$). In the next subsection, I describe an important nuance of this step.</p> <h4 id="combining-the-weights-of-experts-permutation-alignment">Combining the Weights of Experts: Permutation Alignment</h4> <p>Computing the average of neural network weights can lead to poor results in certain cases<d-footnote>It is actually very surprising that averaging of weights can work at all in some cases, so papers like Model soups<d-cite key="wortsman2022model"></d-cite> were very impactful.</d-footnote>. Among such cases is when the networks are trained from different initializations and different data orderings/subsets. The difficulty of averaging weights in this case arises because of the different symmetries in neural network weights, primarily <a href="https://bknyaz.github.io/blog/2025/nino/">neuron permutation symmetry</a>.</p> <blockquote> <p>Neuron permutation symmetry states that the order of neurons in adjacent layers of a neural network can be permuted in certain ways without affecting the overall function of the network<d-cite key="hecht1990algebraic"></d-cite>.</p> </blockquote> <p>For example, in a 2 layer MLP, we can randomly permute rows of the first layer weights and permute in the same order the corresponding columns of the second layer weights without changing the function computed by the MLP.</p> <div class="row mt-3 l-body figure-container-desktop-small"> <div class="col-sm mt-3 mt-md-0" style="text-align: center;"> <figure> <picture> <source class="responsive-img-srcset" srcset="/assets/img/publication_preview/neural_graph-480.webp 480w,/assets/img/publication_preview/neural_graph-800.webp 800w,/assets/img/publication_preview/neural_graph-1400.webp 1400w," type="image/webp" sizes="95vw"/> <img style="display: block; margin: auto;" src="/assets/img/publication_preview/neural_graph.png" class="img-fluid d-block mx-auto rounded z-depth-1" width="100%" height="auto" data-zoomable="" loading="lazy" onerror="this.onerror=null; $('.responsive-img-srcset').remove();"/> </picture> </figure> </div> </div> <div class="caption" style="text-align: center; margin-top: -35px; margin-bottom: 25px;"> Neural network parameters can be represented as neural graphs to model neuron permutation symmetry. Figure from Kofinas et al., 2024<d-cite key="kofinas2024graph"></d-cite>. </div> <p>Neuron permutation symmetry and other symmetries are studied in the <a href="https://weight-space-learning.github.io/">Weight Space Learning (WSL)</a> area of machine learning. One of the key findings in this area is that permutation alignment of the weights of one network w.r.t. another network prior to averaging can significantly improve the results<d-cite key="ainsworth2022git"></d-cite>. To model neuron permutation symmetry better, we proposed neural graphs <d-cite key="kofinas2024graph"></d-cite> and recently showed that we can train powerful <a href="https://bknyaz.github.io/blog/2025/nino/">graph neural networks based on neural graphs</a><d-cite key="knyazev2024accelerating"></d-cite>.</p> <p>When training MoE LLMs, each expert network $E_i$ is initialized randomly and trained on different data subsets (because of the gating mechanism). To address the permutation issue, MC-SMoE and HC-SMoE align expert weights by either expert weights or hidden activations. This can be done, for example, using the <a href="https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.linear_sum_assignment.html">scipy.optimize.linear_sum_assignment</a> function that implements the Hungarian algorithm to solve the assignment problem. I show a pseudocode for expert merging with improved permutation alignment in the next section.</p> <h2 id="ream">REAM</h2> <p>My approach is based on REAP with merging instead of pruning, hence it is called Router-weighted Expert Activation <strong>Merging</strong> or REAM. It can also be seen as a modification of MC-SMoE with REAP scores and additional tricks as described below.</p> <h3 id="algorithm">Algorithm</h3> <p>For each MoE layer with $N$ experts originally (and $k$ experts as a result):</p> <ol> <li>Compute saliency scores $S$ for each expert as in REAP.</li> <li>Pick $k$ experts with the highest $S$ and label them as <em>cluster centroids</em> similarly to MC-SMoE.</li> <li>Group experts, but in contrast to MC-SMoE’s grouping, REAM uses <code class="language-plaintext highlighter-rouge">pseudo-pruning</code><d-footnote>This is called pseudo-pruning, because most of the scores $S$ of the non-centroid experts will be low, so the weighted average (in step 5) will be dominated by the centroid expert.</d-footnote>. The idea behind pseudo-pruning is to have a few big clusters and many singletons with unchanged weights. The grouping starts with the centroid having the highest $S$ and assigning $C$ <em>most similar</em> experts to it (that are not assigned yet). This step is repeated for all centroids until all $N$ experts are assigned ($C=16$ in my experiments).</li> <li>The expert similarity metric, used in the previous step, is improved compared to prior work based on two key tricks. First, REAM uses an average of cosine similarity of expert outputs and cosine similarity of gate logits. Second, <code class="language-plaintext highlighter-rouge">gated similarity</code> is computed by multiplying expert outputs by gate logits motivated by the REAP equation.</li> <li>Merge the weights in each group accounting for permutation alignment of weights based on <strong>both hidden activations and weights</strong> of $E$ as shown in the pseudocode below.</li> <li>Use the merged MoE layer to compute the inputs for the next MoE layer. In contrast, previous works perform merging/pruning at each layer given the <em>original</em> inputs (i.e., computed using the original uncompressed model) to that layer. It means they can do pruning/merging in an arbitrary order of layers since the inputs/outputs are precomputed based on the original model. In REAM, the process is inherently <code class="language-plaintext highlighter-rouge">sequential</code><d-footnote>To implement sequential merging, calibration data has to be propagated through each MoE layer twice: first time to get necessary outputs to perform merging, second time to get the inputs for the next layer using the merged experts.</d-footnote>.</li> </ol> <p>The <code class="language-plaintext highlighter-rouge">highlighted</code> steps are ablated in the Experiments section below.</p> <p>The following pseudocode illustrates step 5 of merging a group of experts with permutation alignment based on both hidden activations and weights.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Pseudo-code for merging a group of experts with logits+weights permutation alignment
</span><span class="n">G</span> <span class="o">=</span> <span class="mi">16</span> <span class="c1">#  number of experts in a group (cluster) to be merged found in steps 3-4
</span><span class="n">d</span> <span class="o">=</span> <span class="mi">768</span> <span class="c1"># bottleneck dimension in the expert network 
</span><span class="n">d_model</span> <span class="o">=</span> <span class="mi">2048</span> <span class="c1"># model dimension
</span>
<span class="n">expert_hidden</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">randn</span><span class="p">(</span><span class="n">G</span><span class="p">,</span> <span class="n">d</span><span class="p">,</span> <span class="n">n</span><span class="p">)</span>  <span class="c1"># hidden activations of G experts for n tokens in a MoE layer
</span><span class="n">expert_weights</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">randn</span><span class="p">(</span><span class="n">G</span><span class="p">,</span> <span class="n">d</span><span class="p">,</span> <span class="n">d_model</span><span class="o">*</span><span class="mi">3</span><span class="p">)</span>   <span class="c1"># concatenated expert weights in a group (3 weight matrices per expert)
</span>
<span class="c1"># optional: perform dimensionality reduction and normalization steps on expert_weights
</span>
<span class="n">S</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">rand</span><span class="p">(</span><span class="n">G</span><span class="p">)</span>  <span class="c1"># REAP scores for G experts in a group (from step 1)
</span><span class="n">S_norm</span> <span class="o">=</span> <span class="n">S</span> <span class="o">/</span> <span class="n">S</span><span class="p">.</span><span class="nf">sum</span><span class="p">()</span>  <span class="c1"># normalized scores (sum=1)
</span>
<span class="n">avg_weights</span> <span class="o">=</span> <span class="n">S_norm</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">*</span> <span class="n">expert_weights</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nf">clone</span><span class="p">()</span>

<span class="c1"># compute permutation cost matrices w.r.t. expert 0 
</span><span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="n">G</span><span class="p">):</span>
    <span class="n">cost1</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">cdist</span><span class="p">(</span><span class="n">expert_hidden</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="n">expert_hidden</span><span class="p">[</span><span class="n">i</span><span class="p">])</span>    <span class="c1"># (d, d)
</span>    <span class="n">cost2</span> <span class="o">=</span> <span class="n">torch</span><span class="p">.</span><span class="nf">cdist</span><span class="p">(</span><span class="n">expert_weights</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="n">expert_weights</span><span class="p">[</span><span class="n">i</span><span class="p">])</span>  <span class="c1"># (d_model*3, d_model*3)
</span>    <span class="n">row_ind</span><span class="p">,</span> <span class="n">col_ind</span> <span class="o">=</span> <span class="n">scipy</span><span class="p">.</span><span class="n">optimize</span><span class="p">.</span><span class="nf">linear_sum_assignment</span><span class="p">(</span><span class="n">cost1</span> <span class="o">+</span> <span class="n">cost2</span><span class="p">)</span>
    <span class="n">perm</span> <span class="o">=</span> <span class="n">col_ind</span>  <span class="c1"># permutation of expert i w.r.t. expert 0
</span>    <span class="n">expert_weights</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">expert_weights</span><span class="p">[</span><span class="n">i</span><span class="p">][</span><span class="n">perm</span><span class="p">]</span>  <span class="c1"># permute expert i hidden weights accordingly
</span>    <span class="n">avg_weights</span> <span class="o">+=</span> <span class="n">S_norm</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">*</span> <span class="n">expert_weights</span><span class="p">[</span><span class="n">i</span><span class="p">]</span>  <span class="c1"># weighted average
</span>
<span class="c1"># Result: G experts merged into one expert with avg_weights
</span></code></pre></div></div> <h3 id="calibration-data">Calibration Data</h3> <p>The design of <code class="language-plaintext highlighter-rouge">calibration data</code> is critical in expert pruning/merging methods. In expert merging and early pruning papers, the c4 dataset is often used. REAP uses c4 and/or evol-codealpaca depending on the downstream task. In my experiments, to make evaluation consistent while the model performant, a fixed mix of c4, math and coding data is used.</p> <blockquote> <p>The calibration data consists of 2048 sequences from diverse sources. It is fixed in all my experiments for all the methods unless explicitly mentioned otherwise (like in ablations).</p> </blockquote> <p>For math data, the <a href="https://huggingface.co/datasets/AI-MO/NuminaMath-1.5">AI-MO/NuminaMath-1.5</a> dataset is used, namely subsets ‘cn_k12’ and ‘olympiads’, with the idea to avoid the overlap with benchmark datasets. For coding data, <a href="https://huggingface.co/datasets/bigcode/the-stack-smol">bigcode/the-stack-smol</a> is used. A different number of samples (sequences) and max tokens are used per dataset to balance their proportions as shown below.</p> <p><strong>Table 1. Calibration data used across all the experiments.</strong></p> <table> <thead> <tr> <th><strong>Domain</strong></th> <th><strong>Dataset</strong></th> <th><strong>Sequences</strong></th> <th><strong>Max tokens</strong></th> <th><strong>≈Total tokens</strong></th> <th><strong>≈Proportion</strong></th> </tr> </thead> <tbody> <tr> <td>General</td> <td>allenai/c4/en</td> <td>512</td> <td>128</td> <td>60k</td> <td>8%</td> </tr> <tr> <td>Math</td> <td>AI-MO/NuminaMath-1.5 (cn_k12, olympiads)</td> <td>1024</td> <td>512</td> <td>524k</td> <td>68%</td> </tr> <tr> <td>Coding</td> <td>bigcode/the-stack-smol</td> <td>512</td> <td>512</td> <td>190k</td> <td>24%</td> </tr> </tbody> </table> <p>Math and coding data are dominant as they are often shown important in adapting LLMs for reasoning and coding tasks. Also, math and coding benchmarks dominate the evaluation in practice. However, the exact proportion in REAM may be suboptimal and can be further explored in future work.</p> <h3 id="gate-weight-adjustment">Gate Weight Adjustment</h3> <p>Finally, after experts are merged, the gate ($g$) weights have to be adjusted to account for the reduced number of experts. In expert merging implementations, merged experts are tied in memory instead of actually keeping only $k$ experts, while gate weights remain unchanged. This leads to gate logits corresponding to the experts within a merged group being summed during inference (see the REAP paper for additional analysis). In contrast, REAM follows REAP and simply removes the weights of the non-centroid experts from the gate weights.</p> <hr/> <h2 id="experiments">Experiments</h2> <p>Experiments are run for three models: <strong>Qwen3-30B-A3B-Instruct-2507</strong>, <strong>Qwen3-235B-A22B-Instruct-2507</strong> and <strong>Qwen3-Next-80B-A3B-Instruct</strong>. These are MoE LLMs with 30B, 235B and 80B parameters, respectively. Qwen3-30B-A3B and Qwen3-235B-A22B have $N$=128 experts and TopK=8<d-footnote>The key differences between the two models are the number of Transformer layers (48 vs 94) and hidden size (2048 vs 4096).</d-footnote>. Qwen3-Next-80B-A3B has $N$=512 experts and TopK=10. They are versatile models fine-tuned for instruction-following tasks heavily used in practice.</p> <p>The experiments aim to reduce the number of experts by 25% (i.e., from 128 to 96 or from 512 to 384), while maintaining high performance across multiple benchmarks. The proposed REAM method is evaluated compared to the leading expert pruning method (REAP)<d-cite key="lasby2025reap"></d-cite> and leading expert merging method (HC-SMoE)<d-cite key="chen2025retrainingfree"></d-cite>.</p> <p>🤗Qwen3 models compressed with HC-SMoE/REAP/REAM are released at <a href="https://huggingface.co/collections/SamsungSAILMontreal/ream">huggingface-ream</a>. Details about the evaluation are provided at <a href="https://huggingface.co/SamsungSAILMontreal/Qwen3-30B-A3B-Instruct-2507-REAM">huggingface-qwen3-ream</a>.</p> <h3 id="benchmarks">Benchmarks</h3> <p>Compressed models are evaluated on two sets of benchmarks: multi-choice question answering (<strong>MC</strong>) and long context reasoning tasks (<strong>GEN</strong>) tasks. The MC tasks are often evaluated by loglikelihood of the correct choice among multiple choices, so the model does not generate free-form text answers. In contrast, in long context reasoning tasks (GEN), the model need to <strong>generate</strong> free-form answers, which is often more challenging and useful in practice.</p> <p>The MC set includes 8 tasks used in the HC-SMoE paper: Winogrande, ARC-C, ARC-E, BoolQ, HellaSwag, MMLU, OpenBookQA and RTE.</p> <p><strong>Table 2. GEN benchmarks used in the experiments.</strong></p> <table> <thead> <tr> <th><strong>Benchmark</strong></th> <th><strong>Description</strong></th> <th><strong>#Tasks</strong></th> </tr> </thead> <tbody> <tr> <td>IFEval</td> <td>General instruction-following</td> <td>541</td> </tr> <tr> <td>AIME25</td> <td>Math reasoning, competitive high school level</td> <td>30</td> </tr> <tr> <td>GSM8K</td> <td>Math reasoning, grade school level</td> <td>1319</td> </tr> <tr> <td>GPQA-Diamond</td> <td>Scientific reasoning tasks in biology, physics and chemistry, PhD level</td> <td>792</td> </tr> <tr> <td>HumanEval (instruct)</td> <td>Python code generation tasks</td> <td>164</td> </tr> <tr> <td>LiveCodeBench v6</td> <td>More challenging code generation tasks</td> <td>1055</td> </tr> </tbody> </table> <h3 id="qwen3-30b-a3b">Qwen3-30B-A3B</h3> <p><strong>Qwen3-30B-A3B-Instruct-2507</strong> was used to tune the REAM hyperparameters (like calibration data mix, number of experts per group $C$, etc.). Also, several ablations of REAM are performed using this model to highlight the importance of REAM components. The results are shown in the Figure below with the average scores across MC ($x$ axis) and GEN tasks ($y$ axis).</p> <pre><code class="language-plotly">{
  "data": [
    {
      "name": "Original",
      "type": "scatter",
      "mode": "markers",
      "x": [69.7],
      "y": [70.9],
      "marker": { "size": 12, "symbol": "circle" }
    },
    {
      "name": "HC-SMoE (c4)",
      "type": "scatter",
      "mode": "markers",
      "x": [63.3],
      "y": [65.2],
      "marker": { "size": 12, "symbol": "square" }
    },
    {
      "name": "HC-SMoE",
      "type": "scatter",
      "mode": "markers",
      "x": [64.9],
      "y": [66.3],
      "marker": { "size": 12, "symbol": "diamond" }
    },
    {
      "name": "REAP",
      "type": "scatter",
      "mode": "markers",
      "x": [65.0],
      "y": [64.2],
      "marker": { "size": 12, "symbol": "cross" }
    },
    {
      "name": "REAM",
      "type": "scatter",
      "mode": "markers",
      "x": [65.8],
      "y": [67.7],
      "marker": { "size": 15, "symbol": "star" }
    },
    {
      "name": "REAM (c4)",
      "type": "scatter",
      "mode": "markers",
      "x": [69.3],
      "y": [35.3],
      "marker": { "size": 10, "symbol": "x" }
    },
    {
      "name": "REAM (no REAP)",
      "type": "scatter",
      "mode": "markers",
      "x": [52.3],
      "y": [64.1],
      "marker": { "size": 10, "symbol": "triangle-up" }
    },
    {
      "name": "REAM (no gated sim)",
      "type": "scatter",
      "mode": "markers",
      "x": [64.6],
      "y": [51.3],
      "marker": { "size": 10, "symbol": "triangle-down" }
    },
    {
      "name": "REAM (no pseudo-pruning)",
      "type": "scatter",
      "mode": "markers",
      "x": [63.5],
      "y": [61.3],
      "marker": { "size": 10, "symbol": "triangle-right" }
    },
    {
      "name": "REAM (no seq)",
      "type": "scatter",
      "mode": "markers",
      "x": [65.5],
      "y": [66.0],
      "marker": { "size": 10, "symbol": "triangle-left" }
    }    
  ],
  "layout": {
    "title": { "text": "Pareto Tradeoff Between MC and Generative tasks", "size": 20 },
    "xaxis": {
      "title": { "text": "MC tasks score (%)" },
      "range": [45, 77]
    },
    "yaxis": {
      "title": { "text": "GEN tasks score (%)" },
      "range": [34, 77]
    },
    "legend": {
      "orientation": "v"
    }
  }
}
</code></pre> <p>REAM obtains 65.8% and 67.7% average scores on MC and GEN tasks, respectively, while the original model achieves 69.7% and 70.9%. HC-SMoE and REAP achieve 64.8%/66.3% and 65.2%/64.1%, respectively, with REAP being better on MC tasks and HC-SMoE being better on GEN tasks. <strong>The proposed REAM method outperforms both of them.</strong></p> <p>The ablations highlight the importance of REAM components. Notably, <strong>using only c4</strong> as calibration data significantly degrades GEN tasks performance, however it makes MC tasks performance close to the original model. On the contrary, HC-SMoE is not as sensitive to the calibration data choice<d-footnote>This result confirms the stability of HC-SMoE regardless data as shown in their paper.</d-footnote>.</p> <p>Another important component of REAM is using REAP scores for selecting centroids. When simple frequency-based scores are used instead, the performance (especially MC) drops significantly. Using proposed <strong>gated similarity</strong>, <strong>pseudo-pruning</strong> and <strong>sequential merging</strong> also improve the results with gated similarity being the most important among them.</p> <p>Interestingly, the results reveal an inherent tradeoff between MC and GEN tasks performance when compressing MoE LLMs. Achieving Pareto optimal results seems challenging, however REAM shows promising results in this direction.</p> <p>Detailed results per task are shown below.</p> <p><strong>Table 3. MC results for Qwen3-30B-A3B-Instruct-2507.</strong></p> <table> <thead> <tr> <th>Model</th> <th>N</th> <th>Winogrande</th> <th>ARC-C</th> <th>ARC-E</th> <th>BoolQ</th> <th>HellaSwag</th> <th>MMLU</th> <th>OBQA</th> <th>RTE</th> <th>AVG</th> </tr> </thead> <tbody> <tr> <td>Original</td> <td>128</td> <td>73.2</td> <td>60.7</td> <td>85.1</td> <td>88.7</td> <td>61.2</td> <td>80.1</td> <td>32.4</td> <td>76.5</td> <td>69.7</td> </tr> <tr> <td>REAM</td> <td>96</td> <td>71.8</td> <td>51.9</td> <td>79.1</td> <td>88.5</td> <td>57.6</td> <td>70.1</td> <td>30.0</td> <td>77.6</td> <td>65.8</td> </tr> </tbody> </table> <p><strong>Table 4. GEN results for Qwen3-30B-A3B-Instruct-2507.</strong></p> <table> <thead> <tr> <th>Model</th> <th>N</th> <th>IFEval</th> <th>AIME25</th> <th>GSM8K</th> <th>GPQA-D</th> <th>HumanEval</th> <th>LiveCodeBench</th> <th>AVG</th> </tr> </thead> <tbody> <tr> <td>Original</td> <td>128</td> <td>90.4</td> <td>56.7</td> <td>89.3</td> <td>47.0</td> <td>93.3</td> <td>48.6</td> <td>70.9</td> </tr> <tr> <td>REAM</td> <td>96</td> <td>89.2</td> <td>66.7</td> <td>88.1</td> <td>38.9</td> <td>86.6</td> <td>36.9</td> <td>67.7</td> </tr> </tbody> </table> <h3 id="qwen3-235b-a22b">Qwen3-235B-A22B</h3> <p>Results on <strong>Qwen3-235B-A22B-Instruct-2507</strong> are obtained by running REAM and baselines with the same hyperparameters and calibration data as for Qwen3-30B-A3B-Instruct-2507. So no tuning specific to this model is performed.</p> <p>Evaluation on a model of this size is very challenging and computationally expensive. Specifically, 8xH100 GPUs were required to fit the model in memory and perform evaluation<d-footnote>In comparison, for Qwen3-30B, 1-4 GPUs were sufficient depending on the task.</d-footnote>. Each task took from a few minutes to 1-2 hours to evaluate. So evaluation is costly, especially in academic settings. The results are shown in the Table below.</p> <p><strong>Table 5. GEN results for Qwen3-235B-A22B-Instruct-2507.</strong></p> <table> <thead> <tr> <th>Model</th> <th>N</th> <th>IFEval</th> <th>AIME25</th> <th>GSM8K</th> <th>GPQA-D</th> <th>HumanEval</th> <th>LiveCodeBench</th> <th>AVG</th> </tr> </thead> <tbody> <tr> <td>Original</td> <td>128</td> <td>93.3</td> <td>66.7</td> <td>89.4</td> <td>48.5</td> <td>95.1</td> <td>46.4</td> <td>73.2</td> </tr> <tr> <td>HC-SMoE</td> <td>96</td> <td>89.6</td> <td>63.3</td> <td>87.5</td> <td>39.9</td> <td>86.0</td> <td>40.0</td> <td>67.7</td> </tr> <tr> <td>REAP</td> <td>96</td> <td><strong>92.0</strong></td> <td>63.3</td> <td><strong>88.8</strong></td> <td><strong>46.0</strong></td> <td><strong>94.5</strong></td> <td><strong>53.1</strong></td> <td><strong>72.9</strong></td> </tr> <tr> <td>REAM</td> <td>96</td> <td>90.4</td> <td>63.3</td> <td>88.2</td> <td>44.4</td> <td><strong>94.5</strong></td> <td>49.5</td> <td>71.7</td> </tr> </tbody> </table> <p>On Qwen3-235B-A22B-Instruct-2507, REAP slightly outperforms REAM on 4 out of 6 GEN tasks and on average. However, both achieve the results surprisingly close to the original model (even outperforming the original model on some tasks!). HC-SMoE lags behind both methods significantly, which is different from the results on Qwen3-30B-A3B-Instruct-2507. It is possible that hyperparameters of REAM (as well as the baselines) are suboptimal for this model, so further tuning may improve the results.</p> <h3 id="qwen3-next-80b-a3b">Qwen3-Next-80B-A3B</h3> <p><strong>Qwen3-Next-80B-A3B-Instruct</strong> is a newer MoE model with a similar architecture as Qwen3-30B-A3B, but with 512 experts and TopK=10 instead of 128 and TopK=8, respectively. Even though the total number of experts is increased 4x, the number of activated parameters is mainly affected by TopK. And although TopK is also increased in this model, to keep the number of activated parameters around 3B, the bottleneck dimension in each expert is reduced from 768 to 512. The results of compressing this model from 512 to 384 experts are shown below. Other than the number of experts and TopK, the same hyperparameters and calibration data as for Qwen3-30B-A3B-Instruct-2507 are used in all methods.</p> <p><strong>Table 6. GEN results for Qwen3-Next-80B-A3B-Instruct.</strong></p> <table> <thead> <tr> <th>Model</th> <th>N</th> <th>IFEval</th> <th>AIME25</th> <th>GSM8K</th> <th>GPQA-D</th> <th>HumanEval</th> <th>LiveCodeBench</th> <th>AVG</th> </tr> </thead> <tbody> <tr> <td>Original</td> <td>512</td> <td>93.4</td> <td>80.0</td> <td>78.6</td> <td>47.0</td> <td>95.1</td> <td>43.2</td> <td>72.9</td> </tr> <tr> <td>REAP</td> <td>384</td> <td>91.0</td> <td>66.7</td> <td><strong>78.8</strong></td> <td><strong>37.9</strong></td> <td>91.5</td> <td><strong>45.0</strong></td> <td>68.5</td> </tr> <tr> <td>REAM</td> <td>384</td> <td><strong>91.5</strong></td> <td><strong>73.3</strong></td> <td>78.4</td> <td>36.9</td> <td><strong>92.7</strong></td> <td>42.9</td> <td><strong>69.3</strong></td> </tr> </tbody> </table> <p>On Qwen3-Next-80B-A3B-Instruct, REAM outperforms REAP on 3 out of 6 GEN tasks and on average. HC-SMoE was not evaluated on this model, since it underperformed in previous experiments. Notably, the original Qwen3-Next model has almost the same GEN performance as its previous much larger (Qwen3-235B-A22B) variant (see Table 5 above), while having only 3B activated parameters instead of 22B. This further confirms the effectiveness of increasing MoE sparsity.</p> <h2 id="conclusion">Conclusion</h2> <p><strong>Merging vs Pruning.</strong> REAM shows promising results by reducing the memory requirement by around 25% while maintaining high performance across multiple benchmarks. There is still a room for improvement, especially in merging methods to better combine the weights of multiple experts. So it is possible that pruning methods (REAP) remain strong because the merging methods are still quite suboptimal. Improving merging methods may allow for further reducing the number of experts (e.g., by 50%), while maintaining high performance.</p> <p><strong>MC vs GEN Tradeoff.</strong> The tradeoff between MC and GEN tasks performance is surprising, since MC tasks are assumed to be easier than GEN tasks. So I mistakenly expected that improving GEN tasks performance would also lead to better MC results.</p> <p><strong>High Engineering and Computation Barrier.</strong> Another observation is that evaluation of LLMs is way more challenging than expected, in some cases taking more time to set up properly than doing research. Besides the high engineering and computation barrier, some benchmarks such as AIME25 are quite noisy because of the small number of samples (30 questions). So evaluation should be done on more diverse and larger benchmarks, further increasing the engineering and computational cost.</p> <p><strong>Weight Space Learning.</strong> Finally, relying on calibration data is generally undesirable, because the results may be sensitive to the data choice. However, only using the weights of experts have led to poor results in my experiments and in ablation studies in the literature. Future work may explore how to use <a href="https://weight-space-learning.github.io/">Weight Space Learning (WSL)</a> and specifically methods based on neural graphs<d-cite key="kofinas2024graph,knyazev2024accelerating"></d-cite> to better perform merging without relying on calibration data.</p> <p><strong>More REAM Models and Code.</strong> We continue to release more REAM-compressed models such as <a href="https://huggingface.co/bknyaz/Qwen3-Coder-Next-REAM">Qwen3-Coder-Next</a> and <a href="https://huggingface.co/bknyaz/GLM-4.5-Air-REAM">GLM-4.5-Air</a>.</p> <p>🔥 Our <a href="https://github.com/SamsungSAILMontreal/ream">code</a> and <a href="https://arxiv.org/abs/2604.04356">paper</a> with all the details have been released!</p> <h3 id="license">License</h3> <p>Diagrams and text are licensed under Creative Commons Attribution <a href="https://creativecommons.org/licenses/by/4.0/">CC-BY 4.0</a>, unless noted otherwise. The figures that have been reused from other sources do not fall under this license and can be recognized by a note in their caption: “Figure from …”. The license of the code and paper is specified in their respective repositories.</p> <h3 id="acknowledgements">Acknowledgements</h3> <p>Minjoong Lee and Hoshik Lee from Samsung DS provided initial project recommendations, feedback and infrastructure support. The experiments were in part enabled by computational resources provided by Calcul Québec and Compute Canada.</p> <h3 id="citation">Citation</h3> <p>Paper:</p> <div class="language-bibtex highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">@article</span><span class="p">{</span><span class="nl">jha2026ream</span><span class="p">,</span>
  <span class="na">title</span><span class="p">=</span><span class="s">{REAM: Merging Improves Pruning of Experts in LLMs}</span><span class="p">,</span>
  <span class="na">author</span><span class="p">=</span><span class="s">{Jha, Saurav and Hashemzadeh, Maryam and Pasand, Ali Saheb and Parviz, Ali and Lee, Min-Joong and Knyazev, Boris}</span><span class="p">,</span>
  <span class="na">journal</span><span class="p">=</span><span class="s">{arXiv preprint arXiv:2604.04356}</span><span class="p">,</span>
  <span class="na">year</span><span class="p">=</span><span class="s">{2026}</span>
<span class="p">}</span>
</code></pre></div></div> <p>This blog post:</p> <div class="language-bibtex highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">@misc</span><span class="p">{</span><span class="nl">knyazev2026compressing</span><span class="p">,</span>
  <span class="na">title</span><span class="p">=</span><span class="s">{REAM: Compressing Mixture-of-Experts LLMs}</span><span class="p">,</span>
  <span class="na">author</span><span class="p">=</span><span class="s">{Boris Knyazev}</span><span class="p">,</span>
  <span class="na">year</span><span class="p">=</span><span class="s">{2026}</span><span class="p">,</span>
  <span class="na">url</span><span class="p">=</span><span class="s">{https://bknyaz.github.io/blog/2026/moe/}</span>
<span class="p">}</span>
</code></pre></div></div>]]></content><author><name>Boris Knyazev</name></author><category term="llm"/><category term="merging"/><category term="mixture-of-experts"/><category term="moe"/><category term="compression"/><category term="empirical-study"/><category term="research"/><summary type="html"><![CDATA[Merging experts in Mixture-of-Experts (MoE) LLMs to compress a 235B LLM.]]></summary></entry><entry><title type="html">NiNo: Learning to Accelerate Training of Neural Networks</title><link href="https://bknyaz.github.io//blog/2025/nino/" rel="alternate" type="text/html" title="NiNo: Learning to Accelerate Training of Neural Networks"/><published>2025-09-30T00:00:00+00:00</published><updated>2025-09-30T00:00:00+00:00</updated><id>https://bknyaz.github.io//blog/2025/nino</id><content type="html" xml:base="https://bknyaz.github.io//blog/2025/nino/"><![CDATA[<script src="https://d3js.org/d3.v7.min.js"></script> <p>Training large neural networks is famously slow and expensive. In our paper, <a href="https://arxiv.org/abs/2409.04434">Accelerating Training with Neuron Interaction and Nowcasting Networks</a>, presented at ICLR 2025 in Singapore, we introduced a new way to speed things up. We treat a neural network as a graph of interacting neurons, or <strong>neural graph</strong><d-cite key="lim2024graph,kofinas2024graph"></d-cite>, and train a graph neural network (GNN) to predict how the parameters of the network will evolve during training.</p> <h2 id="from-adam-to-nino">From Adam to NiNo</h2> <p>Due to massive training costs, recent years have seen a surge of faster adaptive optimizers: Shampoo<d-cite key="gupta2018shampoo"></d-cite>, SOAP<d-cite key="vyas2025soap"></d-cite>, Muon<d-cite key="jordan2024muon"></d-cite>, and others. Each improves on Adam by smarter scaling and/or orthogonalization of parameter updates for a given weight matrix, often inspired by second-order methods or preconditioning. But they work at the level of parameters or layers, not the network as a whole. Moreover, <strong>they do not learn from the previous optimization runs</strong>, i.e., the optimization algorithms are based on manually-designed gradient-descent rules. Our method, <strong>NiNo (Neuron Interaction and Nowcasting)</strong>, is different. Motivated by Kofinas et al.<d-cite key="kofinas2024graph"></d-cite>, we model neurons as nodes and weights as edges, and use a graph neural network (GNN) to predict how weights will evolve. This lets us “nowcast” future parameters and reduce the number of steps required to reach the same performance metric.</p> <blockquote> <p>NiNo is a GNN-based model that takes a history of past parameter values along the optimization trajectory (obtained with Adam or another optimizer) and makes a jump by predicting future parameter values. After making the prediction, optimization is continued with Adam, then followed by NiNo’s another prediction and so on.</p> </blockquote> <p>This periodic nowcasting idea is borrowed from Weight Nowcaster Network (WNN)<d-cite key="jang2023learning"></d-cite> that revealed predictable patterns in optimization trajectories, but neural graphs synergized with GNNs make it work really well. To give some context in terms of optimization runs, below is Figure 1 reproduced from the NiNo paper.</p> <pre><code class="language-plotly">{
  "data": [
    {
      "type": "scatter",
      "name": "Adam",
      "x": [
        645.4,
        1098.0,
        1559.6,
        2021.3,
        2482.9,
        2944.5,
        3406.2,
        3472.6,
        3867.8,
        4329.4,
        4791.1,
        5252.7,
        5714.3,
        6176.0,
        6637.6,
        6769.6,
        7099.3,
        7560.9,
        8022.5,
        8484.2,
        8945.8,
        9407.4,
        9869.1,
        10066.6,
        10330.7,
        10792.3,
        11254.0,
        11715.6,
        12177.2,
        12638.9,
        13100.5,
        13363.6
      ],
      "y": [
        1290.42,
        654.89,
        491.01,
        415.95,
        367.21,
        333.12,
        306.04,
        302.24,
        282.43,
        264.16,
        246.73,
        233.03,
        221.53,
        211.24,
        202.02,
        199.9,
        194.86,
        187.62,
        181.82,
        176.28,
        171.77,
        167.21,
        163.92,
        162.06,
        160.25,
        157.63,
        153.97,
        151.23,
        149.08,
        146.99,
        144.91,
        144.02
      ],
      "mode": "lines",
      "line": {
        "color": "#1f77b4",
        "width": 2.5
      }
    },
    {
      "type": "scatter",
      "name": "WNN (Jang et al., 2023)",
      "x": [
        654.8,
        1098.0,
        1559.6,
        2021.3,
        2482.9,
        2944.5,
        3406.2,
        3472.6,
        3867.8,
        4329.4,
        4791.1,
        5252.7,
        5714.3,
        6176.0,
        6637.6,
        6769.6,
        7099.3,
        7560.9,
        8022.5,
        8484.2,
        8945.8,
        9407.4,
        9869.1,
        10066.6,
        10330.7,
        10792.3,
        11254.0,
        11715.6,
        12177.2,
        12638.9,
        13100.5,
        13363.6
      ],
      "y": [
        1290.42,
        653.73,
        444.74,
        387.85,
        326.93,
        303.61,
        266.84,
        264.77,
        251.27,
        227.12,
        216.34,
        199.39,
        192.44,
        180.71,
        175.41,
        170.13,
        167.42,
        163.54,
        157.7,
        154.93,
        150.43,
        148.24,
        144.45,
        143.78,
        143.02,
        140.42,
        138.9,
        136.51,
        135.4,
        133.53,
        132.68,
        131.58
      ],
      "mode": "lines",
      "line": {
        "color": "#2ca02c",
        "width": 3,
        "dash": "dash"
      }
    },
    {
      "type": "scatter",
      "name": "NiNo (ours)",
      "x": [
        648.7,
        1098.0,
        1559.6,
        2021.3,
        2482.9,
        2944.5,
        3406.2,
        3472.6,
        3867.8,
        4329.4,
        4791.1,
        5252.7,
        5714.3,
        6176.0,
        6637.6,
        6769.6,
        7099.3,
        7560.9,
        8022.5,
        8484.2,
        8945.8,
        9407.4,
        9869.1,
        10066.6,
        10330.7,
        10792.3,
        11254.0,
        11715.6,
        12177.2,
        12638.9,
        13100.5,
        13363.6
      ],
      "y": [
        1290.42,
        652.54,
        425.26,
        371.58,
        294.78,
        271.73,
        231.12,
        228.8,
        217.43,
        193.29,
        184.29,
        169.48,
        163.7,
        154.62,
        150.46,
        149.22,
        144.61,
        141.77,
        138.42,
        136.37,
        134.1,
        132.88,
        131.09,
        130.71,
        130.05,
        128.99,
        128.08,
        127.1,
        126.53,
        125.55,
        124.76,
        124.48
      ],
      "mode": "lines",
      "line": {
        "color": "#D44841",
        "width": 5
      }
    },
    {
      "type": "scatter",
      "name": "target perplexity",
      "x": [
        636.4,
        13363.6
      ],
      "y": [
        147.0,
        147.0
      ],
      "mode": "lines",
      "line": {
        "color": "#7f7f7f",
        "width": 2,
        "dash": "dot"
      }
    }
  ],
  "layout": {
    "title": {
      "text": "NiNo achieves ~2× speedup compared to Adam.",
      "font": {
        "size": 20
      }
    },
    "xaxis": {
      "title": "training iteration",
      "range": [
        0,
        14000
      ],
      "tickvals": [
        0,
        2000,
        4000,
        6000,
        8000,
        10000,
        12000,
        14000
      ]
    },
    "yaxis": {
      "title": "validation perplexity",
      "type": "log",
      "range": [
        2,
        3
      ],
      "tickvals": [
        100,
        200,
        300,
        400,
        600,
        1000
      ],
      "ticktext": [
        "10\u00b2",
        "2\u00d710\u00b2",
        "3\u00d710\u00b2",
        "4\u00d710\u00b2",
        "6\u00d710\u00b2",
        "10\u00b3"
      ]
    },
    "legend": {
      "x": 1.0,
      "y": 1.0,
      "xanchor": "right",
      "yanchor": "top"
    },
    "margin": {
      "l": 70,
      "r": 20,
      "t": 60,
      "b": 60
    },
    "annotations": [
      {
        "x": 300,
        "xref": "x",
        "y": 0.60,
        "yref": "paper",
        "text": "Nowcast",
        "showarrow": false,
        "textangle": -45,
        "font": {
          "size": 10
        },
        "xanchor": "left",
        "yanchor": "bottom"
      },
      {
        "x": 1300,
        "xref": "x",
        "y": 0.40,
        "yref": "paper",
        "text": "Nowcast",
        "showarrow": false,
        "textangle": -45,
        "font": {
          "size": 10
        },
        "xanchor": "left",
        "yanchor": "bottom"
      },
      {
        "x": 10300,
        "xref": "x",
        "y": -0.03,
        "yref": "paper",
        "text": "Nowcast",
        "showarrow": false,
        "textangle": -45,
        "font": {
          "size": 10
        },
        "xanchor": "left",
        "yanchor": "bottom"
      }
    ]
  }
}
</code></pre> <p>The figure shows the results of Adam <strong>without</strong> and <strong>with</strong> nowcasting using our NiNo. “Nowcast” steps are shown at step 1000, 2000 and 11,000 for visualization purposes, but this step is applied every 1000 steps in our experiments. Note the <strong>~2× reduction</strong> of the number of steps required by NiNo to achieve the same validation perplexity as by Adam. The optimization task in this example is autoregressive next-token prediction on the WikiText103 dataset that NiNo has not seen during its training.</p> <h2 id="nino-details">NiNo Details</h2> <p>As NiNo is a neural network (namely, a GNN), it needs to be trained first before we can use it to speed up optimization. To do so, we collected and publicly released a 🤗<a href="https://huggingface.co/datasets/SamsungSAILMontreal/nino_metatrain">dataset of checkpoints</a> with optimization trajectories on 2 vision and 2 language tasks. Even though collecting these checkpoints and training NiNo is computationally expensive, this one-time cost is amortized, meaning the same trained NiNo can potentially be used across many tasks, ultimately reducing total training cost.</p> <h3 id="neuron-permutation-symmetry">Neuron Permutation Symmetry</h3> <p>Developing a strong parameter nowcasting model requires many specific design choices and, perhaps most critically, accurate modeling of <strong>neuron permutation symmetry</strong><d-footnote>Modeling neuron permutation symmetry imposes a strong inductive bias similarly to using convolution for images. So our model can be used in more diverse tasks and should be able to learn parameter prediction rules that generalize better with fewer samples compared to models that do not explicitly take this symmetry into account.</d-footnote>. Neuron permutation symmetry states that the order of neurons in adjacent layers of a neural network can be permuted in certain ways without affecting the overall function of the network<d-cite key="hecht1990algebraic"></d-cite>. To better understand this symmetry, let me introduce a simple example based on a two layer neural network with weights \(\mathbf{W}_1\), \(\mathbf{W}_2\) and an element-wise activation function σ. Given input $\mathbf{x}$, the output of such a network can be expressed as:</p> \[f(\mathbf{x}) = \mathbf{W}_2 \ \sigma(\mathbf{W}_1 \ \mathbf{x}).\] <p>We can permute the neurons in the hidden layer by applying a permutation matrix \(\mathbf{P}\) to the weights in layer 1 (permuting rows) and the inverse permutation to the weights in layer 2 (permuting columns), resulting in:</p> \[f(\mathbf{x}) = (\mathbf{W}_2 \ P^{-1}) \ \sigma(P \ \mathbf{W}_1 \ \mathbf{x}).\] <p>Given the orthogonality property of permutation matrices, \(\mathbf{P}^{-1} = \mathbf{P}^T\), and that the activation function σ is element-wise, we can see that the output of the network remains unchanged:</p> \[f(\mathbf{x}) = \mathbf{W}_2 \ \mathbf{P}^{-1} \ \sigma ( \mathbf{P} \ \mathbf{W}_1 \ \mathbf{x}) = \mathbf{W}_2 \ \mathbf{P}^{-1} \mathbf{P} \ \sigma (\mathbf{W}_1 \ \mathbf{x}) = \mathbf{W}_2 \ \sigma(\mathbf{W}_1 \ \mathbf{x}).\] <p>Let me now visualize this symmetry using a simple demo, where the output is dynamically computed given the current order of neurons for some fixed input. You can click the “Swap Random Pair” button to randomly swap two (just for simplicity) hidden neurons and see how the network diagram and weight matrices change, while the computed output should (hopefully!) remain the same. You can also toggle the “Correct Permutation” option off to turn off the permutation of the weights in the second layer, in which case the output changes. You can reset the demo to the original state by clicking the “Reset” button.</p> <div id="mlp-permutation-demo"> <div style="display: flex; gap: 2rem; justify-content: center; align-items: flex-start; flex-wrap: wrap;"> <div> <div id="network-viz"></div> <div id="input-output" style="margin-top: 1rem; padding: 1rem; background: white; border-radius: 4px; border: 1px solid #ddd;"> <div style="margin-bottom: 0.5rem;"> <strong style="color: #333;">Input x:</strong> <span id="input-values" style="font-family: monospace; color: #333;"></span> </div> <div style="margin-bottom: 0.5rem;"> <strong style="color: #333;">Hidden h:</strong> <span id="hidden-values" style="font-family: monospace; color: #4CAF50; font-weight: bold;"></span> </div> <div> <strong style="color: #333;">Output y:</strong> <span id="output-values" style="font-family: monospace; color: #2196F3; font-weight: bold;"></span> </div> </div> </div> <div id="weight-matrices"></div> </div> <div style="text-align: center; margin-top: 1rem;"> <button id="animate-btn" style="padding: 0.5rem 1.5rem; font-size: 1rem; cursor: pointer; border-radius: 4px; border: 2px solid #2196F3; background: #2196F3; color: white;"> ▶ Swap Random Pair </button> <button id="reset-btn" style="padding: 0.5rem 1.5rem; font-size: 1rem; cursor: pointer; border-radius: 4px; border: 2px solid #666; background: white; color: #666; margin-left: 0.5rem;"> ↻ Reset </button> <button id="toggle-btn">✓ Correct Permutation ON</button> </div> <p style="text-align: center; margin-top: 1rem; font-size: 0.9rem;"> 💡 Watch how swapping two hidden neurons changes the network diagram and weight matrices, but the output stays the same! </p> </div> <script src="/assets/js/metamerge/mlp-permutation-demo.js"></script> <h3 id="neuron-permutation-symmetry-in-transformers">Neuron Permutation Symmetry in Transformers</h3> <p>To make NiNo work well for LLMs and Transformers in general, it was critical to carefully construct the neural graph for multi-head self-attention, making it stand out compared to WNN (which ignores the neural network structure). This is a tricky part as the illustration below shows, but we implemented it for many different Transformer layers.</p> <div class="row mt-3 l-body figure-container-desktop"> <div class="col-sm mt-3 mt-md-0" style="text-align: center;"> <figure> <picture> <source class="responsive-img-srcset" srcset="/assets/img/2025-09-30-nino/neural_graph_msa-480.webp 480w,/assets/img/2025-09-30-nino/neural_graph_msa-800.webp 800w,/assets/img/2025-09-30-nino/neural_graph_msa-1400.webp 1400w," type="image/webp" sizes="95vw"/> <img style="display: block; margin: auto;" src="/assets/img/2025-09-30-nino/neural_graph_msa.png" class="img-fluid d-block mx-auto rounded z-depth-1" width="100%" height="auto" data-zoomable="" loading="lazy" onerror="this.onerror=null; $('.responsive-img-srcset').remove();"/> </picture> </figure> </div> </div> <div class="caption" style="text-align: center; margin-top: -35px; margin-bottom: 25px;"> Constructing neural graphs for a MSA layer (see the details in our paper and code). </div> <p>Visualizing the permutation symmetry in Transformers using a demo is also possible, but I leave it for future work (or please submit a PR <a href="https://github.com/bknyaz/bknyaz.github.io/tree/master/_posts">to this blog post</a>).</p> <h2 id="results-that-stand-out">Results that Stand Out</h2> <ul> <li>NiNo achieves roughly a 2× speedup compared to Adam, which is rarely achieved by manually designed optimizers in practice<d-footnote>Which are considered remarkable if a 1.2–1.3× speedup is achieved.</d-footnote>.</li> <li>NiNo speeds up optimization across 9 vision and language tasks that we systematically evaluated, achieving the same validation performance (accuracy or perplexity) as Adam in about half the steps on average across these tasks (see Table 2 in the paper).</li> <li>NiNo continues to speed up training for larger models beyond our main 9 tasks that are much larger than the models in the training checkpoints used to train NiNo (see Table 4 in the paper).</li> </ul> <p>In addition, as shown below, applying NiNo is straightforward:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">torch</span>
<span class="kn">import</span> <span class="n">torch.nn.functional</span> <span class="k">as</span> <span class="n">F</span>
<span class="kn">from</span> <span class="n">transformers</span> <span class="kn">import</span> <span class="n">AutoModelForCausalLM</span>
<span class="kn">from</span> <span class="n">optim</span> <span class="kn">import</span> <span class="n">NiNo</span>

<span class="n">model</span> <span class="o">=</span> <span class="n">AutoModelForCausalLM</span><span class="p">.</span><span class="nf">from_config</span><span class="p">(...)</span>  <span class="c1"># some model
</span>
<span class="c1"># NiNo is implemented as a wrapper around the base optimizer
# any optimizer other than Adam should also be possible to use with NiNo
</span><span class="n">opt</span> <span class="o">=</span> <span class="nc">NiNo</span><span class="p">(</span><span class="n">base_opt</span><span class="o">=</span><span class="n">torch</span><span class="p">.</span><span class="n">optim</span><span class="p">.</span><span class="nc">AdamW</span><span class="p">(</span><span class="n">model</span><span class="p">.</span><span class="nf">parameters</span><span class="p">(),</span> 
           <span class="n">lr</span><span class="o">=</span><span class="mf">1e-3</span><span class="p">,</span> 
           <span class="n">weight_decay</span><span class="o">=</span><span class="mf">1e-2</span><span class="p">),</span>
           <span class="n">ckpt</span><span class="o">=</span><span class="sh">'</span><span class="s">checkpoints/nino.pt</span><span class="sh">'</span><span class="p">,</span>
           <span class="n">subgraph</span><span class="o">=</span><span class="bp">False</span><span class="p">,</span> <span class="c1"># can be set to True for larger models (see Llama 3.2 example below)
</span>           <span class="n">edge_sample_ratio</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span>  <span class="c1"># can be set to a small positive number for larger models (see Llama 3.2 example below)
</span>           <span class="n">model</span><span class="o">=</span><span class="n">model</span><span class="p">,</span>
           <span class="n">period</span><span class="o">=</span><span class="mi">1000</span><span class="p">,</span>
           <span class="n">max_train_steps</span><span class="o">=</span><span class="mi">10000</span><span class="p">)</span>
<span class="k">for</span> <span class="n">step</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="mi">10000</span><span class="p">):</span>
    <span class="k">if</span> <span class="n">opt</span><span class="p">.</span><span class="n">need_grads</span><span class="p">:</span>  <span class="c1"># True/False based on the step number and period
</span>        <span class="n">opt</span><span class="p">.</span><span class="nf">zero_grad</span><span class="p">()</span>  <span class="c1"># zero out gradients
</span>        <span class="n">data</span><span class="p">,</span> <span class="n">targets</span> <span class="o">=</span> <span class="p">...</span>  <span class="c1"># get some batch of data
</span>        <span class="c1"># base optimizer step (majority of the time)
</span>        <span class="n">outputs</span> <span class="o">=</span> <span class="nf">model</span><span class="p">(</span><span class="n">data</span><span class="p">)</span>  <span class="c1"># forward pass
</span>        <span class="n">loss</span> <span class="o">=</span> <span class="n">F</span><span class="p">.</span><span class="nf">cross_entropy</span><span class="p">(</span><span class="n">outputs</span><span class="p">,</span> <span class="n">targets</span><span class="p">)</span>  <span class="c1"># compute some loss
</span>        <span class="n">loss</span><span class="p">.</span><span class="nf">backward</span><span class="p">()</span>  <span class="c1"># only compute gradients for the base optimizer            
</span>    <span class="n">opt</span><span class="p">.</span><span class="nf">step</span><span class="p">()</span>  <span class="c1"># base_opt step or nowcast params every 1000 steps using NiNo    
</span></code></pre></div></div> <h2 id="learning-to-optimize-revisited">Learning to Optimize, Revisited</h2> <p>Our work connects to the broader “learning to optimize” literature, such as VeLO<d-cite key="metz2022velo"></d-cite>. Unlike many learned optimizers that struggle with cost, stability or show only a ~1.2–1.3× speedup<d-footnote>While a ~1.2–1.3× speedup is remarkable, in practice it usually does not justify the immense amount of extra work (e.g. efficient distributed implementation, tuning, potential instabilities especially in mixed/low-bit precision, investigating unexpected side effects like overfitting or poor generalization) that is required for actual large-scale usefulness.</d-footnote>, NiNo is conceptually lightweight and stable, because it is only applied every 1,000 steps (by default), while for all the other steps any base optimizer, such as Adam, can be applied to allow for stable convergence. At the same time, recent learned optimizers such as our recent <a href="https://arxiv.org/abs/2501.12670">Celo</a> and <a href="https://arxiv.org/abs/2406.00153">μLO</a> <strong>that will be presented at ICLR 2026</strong>, make a significant step in improving learned optimizers<d-footnote>In particular, Celo and μLO make learned optimizers more cost-effective and stable (i.e. without big loss spikes) to train and use.</d-footnote>.</p> <div class="row mt-3 l-body figure-container-desktop"> <div class="col-sm mt-3 mt-md-0" style="text-align: center;"> <figure> <picture> <source class="responsive-img-srcset" srcset="/assets/img/2025-09-30-nino/neural_graph_llama-480.webp 480w,/assets/img/2025-09-30-nino/neural_graph_llama-800.webp 800w,/assets/img/2025-09-30-nino/neural_graph_llama-1400.webp 1400w," type="image/webp" sizes="95vw"/> <img style="display: block; margin: auto;" src="/assets/img/2025-09-30-nino/neural_graph_llama.png" class="img-fluid d-block mx-auto rounded z-depth-1" width="100%" height="auto" data-zoomable="" loading="lazy" onerror="this.onerror=null; $('.responsive-img-srcset').remove();"/> </picture> </figure> </div> </div> <div class="caption" style="text-align: center; margin-top: -35px; margin-bottom: 25px;"> Neural graph of a Llama-3 based architecture (graph and adjacency matrix are visualized, see the paper for details). In the code, we also support many other architectures, including Qwen3 and vision models like ViT. </div> <h2 id="conclusion">Conclusion</h2> <p>Even though NiNo shows great speedups, it requires further work, for example:</p> <ul> <li>Making NiNo’s step more scalable (especially memory-efficient) to larger models - currently the message passing step in its GNN is a bottleneck;</li> <li>Improving speedups on larger tasks - for tasks with &gt;1B parameter models we currently observe no speedup;</li> <li>Adding automatic ways to construct neural graphs and verify their correctness;</li> <li>Combining NiNo with learned optimizers to get the merits of both;</li> <li>Showing theoretical guarantees for convergence.</li> </ul> <p>We have open-sourced our code and <strong>pretrained NiNo checkpoints</strong> at <a href="https://github.com/SamsungSAILMontreal/nino">github.com/SamsungSAILMontreal/nino</a> under MIT License and welcome contributions.</p> <h3 id="license">License</h3> <p>Diagrams and text are licensed under Creative Commons Attribution <a href="https://creativecommons.org/licenses/by/4.0/">CC-BY 4.0</a>, unless noted otherwise.</p> <h3 id="citation">Citation</h3> <div class="language-bibtex highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">@inproceedings</span><span class="p">{</span><span class="nl">knyazev2024accelerating</span><span class="p">,</span>
  <span class="na">title</span><span class="p">=</span><span class="s">{Accelerating Training with Neuron Interaction and Nowcasting Networks}</span><span class="p">,</span> 
  <span class="na">author</span><span class="p">=</span><span class="s">{Boris Knyazev and Abhinav Moudgil and Guillaume Lajoie and Eugene Belilovsky and Simon Lacoste-Julien}</span><span class="p">,</span>  
  <span class="na">booktitle</span><span class="p">=</span><span class="s">{International Conference on Learning Representations}</span><span class="p">,</span>
  <span class="na">year</span><span class="p">=</span><span class="s">{2025}</span><span class="p">,</span>
<span class="p">}</span>
</code></pre></div></div>]]></content><author><name>Boris Knyazev</name></author><category term="nino"/><category term="gnn"/><category term="neural-graphs"/><category term="llms"/><category term="transformers"/><category term="optimization"/><category term="paper"/><category term="visualization"/><summary type="html"><![CDATA[Explaining our ICLR 2025 paper and visualizing neuron permutation symmetry.]]></summary></entry><entry><title type="html">Can we do better than Convolutional Neural Networks?</title><link href="https://bknyaz.github.io//blog/2019/can-we-do-better-than-convolutional-neural-networks/" rel="alternate" type="text/html" title="Can we do better than Convolutional Neural Networks?"/><published>2019-09-30T13:08:11+00:00</published><updated>2019-09-30T13:08:11+00:00</updated><id>https://bknyaz.github.io//blog/2019/can-we-do-better-than-convolutional-neural-networks</id><content type="html" xml:base="https://bknyaz.github.io//blog/2019/can-we-do-better-than-convolutional-neural-networks/"><![CDATA[<h4>PyTorch Implementation of “Image Classification with Hierarchical Multigraph Networks” from BMVC 2019</h4> <figure><img alt="" src="https://cdn-images-1.medium.com/max/995/0*DQEo8wicTlkyZeC1"/><figcaption>The number of pixels in the top row is 11, 7 and 1000 times larger (from left to right) than the number of “superpixels” in the bottom row. Can we use the superpixels rather than raw pixels as input and improve on convolutional neural networks?</figcaption></figure> <p>The British Machine Vision Conference (BMVC), finished about two weeks ago in Cardiff, UK, is one of the <a href="https://scholar.google.com/citations?view_op=top_venues&amp;hl=en&amp;vq=eng_computervisionpatternrecognition">top conferences in computer vision &amp; pattern recognition</a> with a competitive acceptance rate of 28%. Compared to others, it’s a small event, so you have plenty of time to walk around posters and talk to presenters one-on-one, which I found really nice.</p> <h3>BMVC 2019 on Twitter</h3> <p>Paper decisions were released yesterday. Congratulations to all of you! In total, we received 1008 submissions, of which 815 were valid. Of these, a total of 231 papers were accepted (38 as Oral Presentations, 193 as Poster Presentations). This amounts to a 28% acceptance rate.</p> <p>I presented a poster on <a href="https://bmvc2019.org/wp-content/uploads/papers/1186-paper.pdf"><strong>Image Classification with Hierarchical Multigraph Networks</strong></a><strong> </strong>on which I mainly worked during my internship at <a href="https://www.sri.com/">SRI International</a> under the supervision of <a href="https://filebox.ece.vt.edu/~linxiao/"><em>Xiao Lin</em></a><em>,</em> <a href="https://medium.com/u/6cf41cb2c546">Mohamed Amer</a> <em>(</em><a href="https://mohamedramer.com/"><em>homepage</em></a><em>) </em>and my PhD advisor <a href="https://www.gwtaylor.ca/"><em>Graham Taylor</em></a><em>.</em></p> <p>In the paper, we basically try to answer the question “Can we do better than Convolutional Neural Networks?”. Here I discuss this question and support my arguments by results. I also walk you through the forward pass of the whole pipeline for a single image from <a href="http://host.robots.ox.ac.uk/pascal/VOC/voc2012/">PASCAL VOC 2012</a> using PyTorch.</p> <p><strong>The complete code</strong> for this post is in <a href="https://github.com/bknyaz/bmvc_2019">my notebook on Github.</a> It should be easy to adapt it for training and validating on the whole PASCAL dataset.</p> <p>So, why do we want to do better than ConvNets? Haven’t they outperformed humans in many tasks?</p> <p>For example, you could say that <strong>image classification</strong> is a solved task. Well, in terms of ImageNet, yes. But despite great contribution of ImageNet, it is a weird task. Why would you want to discriminate between hundreds of dog breeds? So, in result we have models succeeding in that, but failing to discriminate between slightly rotated dogs and cats. Fortunately, we now have <a href="https://arxiv.org/abs/1903.12261">ImageNet-C</a> and <a href="https://arxiv.org/abs/1907.07484">other similar benchmarks</a> showing that we are nowhere close to solving it.</p> <figure><img alt="" src="https://cdn-images-1.medium.com/max/712/1*d5ZkQXZA73ASjq7B0UtDwQ.png"/><figcaption>Pipeline: We solve the classical task of image classification.</figcaption></figure> <p>Another open problem arising in related tasks, like object detection, is training on really large images (e.g., 4000×3000), which is addressed, for example, by <a href="https://arxiv.org/abs/1905.03711">Katharopoulos &amp; Fleuret (ICML, 2019</a>) and <a href="https://bmvc2019.org/wp-content/uploads/papers/0555-paper.pdf">Ramapuram et al. (BMVC, 2019</a>). Thanks to the latter I now know that if the background of a poster is black, then it’s likely from Apple. I should reserve some color too!</p> <p>So, maybe we need something different than a Convolutional Neural Network? Instead of constantly patching its <a href="https://distill.pub/2019/advex-bugs-discussion/">bugs</a>, maybe we should use a model that has nicer properties from the beginning?</p> <p>We argue that such a model <em>can be</em><strong> </strong>a <strong>Graph Neural Network (GNN) </strong>— a neural network that can learn from graph-structured data. GNNs have some appealing properties. For example, compared to ConvNets, GNNs are inherently rotation and translation invariant, because there is simply no notion of rotation or translation in graphs, i.e. there is no left and right, there are only “neighbors” in some sense (<a href="https://arxiv.org/abs/1703.00356">Khasanova &amp; Frossard, ICML, 2017</a>). So, the problem of making a <a href="https://arxiv.org/abs/1602.07576">ConvNet generalize better to different rotations</a>, that people have been trying to solve for years, is solved automatically with GNNs!</p> <p>Regarding learning from large images, how about extracting <a href="https://scikit-image.org/docs/dev/api/skimage.segmentation.html#skimage.segmentation.slic">superpixels</a> from images and feeding a much lower dimensional input to a GNN instead of feeding a downsampled (e.g. 224×224) image to a ConvNet? Superpixels seem to be a much better way to downsample an image compared to, say, bilinear interpolation, because they often preserve a lot of semantics by keeping the boundaries between objects. With a ConvNet we cannot directly learn from this kind of an input, however, there are some nice works proposing to leverage them (<a href="https://aaai.org/ocs/index.php/AAAI/AAAI17/paper/view/14445">Kwak et al., AAAI, 2017</a>).</p> <p>So, a GNN sounds wonderful! Let’s see how it performs in practice.</p> <p>Oh no! Our baseline GNN based on (<a href="https://arxiv.org/abs/1609.02907">Kipf &amp; Welling, ICLR, 2017</a>) achieves merely 19.2% (mean average precision or mAP) on PASCAL, compared to 32.7% of a ConvNet with the same number of layers and filters in each layer.</p> <p>We propose several improvements that eventually beat the ConvNet!</p> <h3>1. Hierarchical Graph</h3> <p>In ConvNets, the hierarchical structure of images is implicitly modeled by pooling layers. In GNNs, you can achieve this in at least two ways. First, you can use pooling similar to ConvNets, but for graphs, defining a fast and good pooling method is really challenging. Instead, we can compute superpixels at multiple scales and pool superpixels by their correspondence to a larger parent superpixel. However, for some reasons this kind of pooling didn’t work well in our case (I still think it should work well). So, instead we model a hierarchy at the input level. In particular, we combine superpixels of all scales into a single set and compute hierarchical relations based on intersection over union (IoU), commonly used in semantic segmentation.</p> <figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*cIkKRFPlIxFtgto0B7B8fw.png"/><figcaption>Three scales of 1000, 300 and 7 superpixels computed by SLIC. Note that the <a href="https://scikit-image.org/docs/dev/api/skimage.segmentation.html#skimage.segmentation.slic">SLIC algorithm</a> that we use often returns fewer superpixels (shown on top of each image) than we request. In the middle image I show spatial connections in yellow, while in the right image — hierarchical ones that allow to connect remote nodes.</figcaption></figure> <p>Based on that principle, I build the hierarchical graph in the code below. I also build a multiscale version of the spatial graph, but it encodes only spatial relationships, while IoU should better encode hierarchical ones. For example, using IoU we can create <strong>shortcuts between remote child nodes</strong>, i.e. connect two small superpixels (e.g., wheels) that are far away spatially, but belong to the same parent node (e.g., a car) as shown on the image above.</p> <p>And indeed, the hierarchical graph boosts mAP to 31.7%, making it just 1% lower than a ConvNet while having 4 times fewer trainable parameters! If we use only the spatial multiscale graph, the results are much worse as explored in the paper.</p> <iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/adf95c091aff8e3f68bc54fed950fe04/href">https://medium.com/media/adf95c091aff8e3f68bc54fed950fe04/href</a></iframe> <p>Great! What else can we do to further improve results?</p> <h3>2. Learnable relations</h3> <p>So far, if we visualize our filters, they will look very primitive (as Gaussians). See <a href="https://medium.com/@BorisAKnyazev/tutorial-on-graph-neural-networks-for-computer-vision-and-beyond-part-1-3d9fada3b80d">my tutorial on GNNs</a> for more details. We want to learn some edge detectors similar to ConvNets, because they work so well. But it turned out to be very challenging to learn them with GNNs. To do that, we basically need to generate edges between superpixels depending on the difference between coordinates. By doing so, we will endow our GNN with the ability to understand the coordinate system (rotation, translation). We will use a 2 layer neural network defined in PyTorch like this:</p> <pre>pred_edge = nn.Sequential(nn.Linear(2, 32),<br />                          nn.ReLU(True),<br />                          nn.Linear(32, L))</pre> <p>where <em>L</em> is the number of predicted edges or the number of filters, such as 4 in the visualization below.</p> <p>We restrict the filter to learn edges only based on the absolute difference between coordinates, |(<em>x₁,y₁</em>) - (<em>x₂,y₂</em>)|, instead of raw values, so that the filters become symmetric. This limits the capacity of filters, but it is still much better than a simple Gaussian filter used by our baseline GCN.</p> <p>In <a href="https://github.com/bknyaz/bmvc_2019/blob/master/bmvc_2019.ipynb">my Jupyter notebook</a>, I created a class LearnableGraph that implements the logic to predict edges given node coordinates (or any other features) and the spatial graph. The latter is used to define a small local neighborhood around each node to avoid predicting edges for all possible node pairs, because it’s expensive and doesn’t make much sense to connect very remote superpixels.</p> <p>Below, I visualize the trained pred_edge function. To do that, I assume that the current node with index 1, where we apply the convolution, is in the center of a coordinate system, <em>(x₁,y₁)=0</em>. Then I simply sample coordinates of other nodes, <em>(x₂,y₂)</em>, and feed them to pred_edge. The color shows the strength of an edge depending on the distance from a center node.</p> <figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*uQSsI6hajHdYvcWrdxpBpg.png"/><figcaption>Visualization of predicted edges which can be interpreted as filters, where each intensity value is an edge between two nodes at a distance specified in axes.</figcaption></figure> <p>The learned graph is also very powerful, but at a larger computational cost, which is negligible if we generate a very sparse graph. The result of 32.3% is just 0.4% lower than a ConvNet and can be easily improved if we generate more filters!</p> <h3>3. Multiscale GNN</h3> <p>We now have <strong>three graphs: spatial, hierarchical and learned</strong>. A single graph convolutional layer with the spatial or hierarchical graph permits feature propagation only within the “first neighbors”. Neighbors are soft in our case, since we use a Gaussian to define the spatial graph and IoU for the hierarchical one. <a href="https://arxiv.org/abs/1606.09375">Defferrard et al. (NIPS, 2016</a>) proposed a multiscale (multihop) graph convolution, which aggregates features within a <em>K</em>-hop neighborhood and approximates spectral graph convolution. See <a href="https://towardsdatascience.com/tutorial-on-graph-neural-networks-for-computer-vision-and-beyond-part-2-be6d71d70f49">my other post</a> for an extensive explanation of this method. For our spatial graph, it essentially corresponds to using multiple Gaussians of different width. For the hierarchical graph, this way we can create <em>K</em>-hop<strong> </strong>shortcuts between remote child nodes. For the learned graph, this method will create multiple scales of the learned filters visualized above.</p> <p>Using multiscale graph convolution, implemented in my GraphLayerMultiscale class, turned out to be extremely important allowing us to <strong>outperform</strong> the baseline ConvNet by 0.3%!</p> <h3>4. Improving fusion of relation types at low cost</h3> <p><strong>So far, to learn from our three graphs, we have used a standard concatenation method. </strong>This method, however, has a couple of problems. <strong>First</strong>, the number of trainable parameters of such a fusion operator is linear w.r.t. the input and output feature dimensionalities, scale (<em>K)</em> and number of relation types, so it can really grow fast if we increase two or more of these parameters at once. <strong>Second</strong>, the relation types we try to fuse can have very different natures and occupy very different subspaces of a manifold. To solve both problems at the same time, we propose learnable projections similar to (<a href="https://arxiv.org/abs/1811.09595">Knyazev et al., NeurIPS-W, 2018</a>). This way we decouple the linear dependency reducing the number of parameters by a factor of 2–3 compared to concatenation. In addition, learnable projections transform multirelational features so that they should occupy nearby subspaces of the manifold, facilitating the propagation of information from one relationship to another.</p> <figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*zBplfQQJrFs05XEzu8aKyQ.png"/><figcaption>One of the proposed relation type fusion methods, which performs very well on PASCAL and allows us to beat the ConvNet by a quite large margin.</figcaption></figure> <p>By using the proposed fusion method, implemented in the GraphLayerFusion class below, we achieve 34.5% beating the ConvNet by 1.8%, while having 2 times fewer parameters! Quite impressive for the model that initially didn’t know anything about the spatial structure of images, except for information encoded in superpixels. It would be interesting to explore other fusion methods, like <a href="https://arxiv.org/abs/1803.09374">this one</a>, to get even better results.</p> <iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/41cea34f4340597b38a3d2eb15be9e4c/href">https://medium.com/media/41cea34f4340597b38a3d2eb15be9e4c/href</a></iframe> <h3>Conclusion</h3> <p>It turned out that with a multirelational graph network and some tricks, we can do better than a Convolutional Neural Network!</p> <p>Unfortunately, during our process of improving the GNN we slowly lost its invariance properties. For example, the shape of superpixels might change after rotating the image, and superpixel coordinates that we use for node features to improve the model also make it less robust.</p> <p>Nevertheless, our work is a small step towards a better image reasoning model and we show that GNNs can pave a promising direction.</p> <p>See <a href="https://github.com/bknyaz/bmvc_2019">my notebook on Github</a> for implementation details.</p> <p>I also highly recommend <a href="https://medium.com/u/24cd20b3728e">Matthias Fey</a>’s Master’s thesis with <a href="https://github.com/rusty1s/embedded_gcnn">the code</a> on a very related topic.</p> <p>Find me on <a href="https://github.com/bknyaz/">Github</a>, <a href="https://www.linkedin.com/in/boris-knyazev-39690948/">LinkedIn</a> and <a href="https://twitter.com/BorisAKnyazev">Twitter</a>. <a href="https://bknyaz.github.io/">My homepage</a>.</p> <p><img src="https://medium.com/_/stat?event=post.clientViewed&amp;referrerSource=full_rss&amp;postId=46ed90fed807" width="1" height="1" alt=""/>&lt;hr&gt;&lt;p&gt;<a href="https://medium.com/data-science/can-we-do-better-than-convolutional-neural-networks-46ed90fed807">Can we do better than Convolutional Neural Networks?</a> was originally published in <a href="https://medium.com/data-science">TDS Archive</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.&lt;/p&gt;</p>]]></content><author><name></name></author><category term="medium"/></entry><entry><title type="html">Spectral Graph Convolution Explained and Implemented Step By Step</title><link href="https://bknyaz.github.io//blog/2019/spectral-graph-convolution-explained-and-implemented-step-by-step/" rel="alternate" type="text/html" title="Spectral Graph Convolution Explained and Implemented Step By Step"/><published>2019-08-15T22:56:19+00:00</published><updated>2019-08-15T22:56:19+00:00</updated><id>https://bknyaz.github.io//blog/2019/spectral-graph-convolution-explained-and-implemented-step-by-step</id><content type="html" xml:base="https://bknyaz.github.io//blog/2019/spectral-graph-convolution-explained-and-implemented-step-by-step/"><![CDATA[<h4>As part of the “Tutorial on Graph Neural Networks for Computer Vision and Beyond”</h4> <figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Vx6uqv12rzb8HeUZl8d7-g.png"/><figcaption>The Fourier basis (DFT matrix) on the left, in which each column or row is a basis vector, reshaped to 28×28 (on the right), i.e. 20 basis vectors are shown on the right. The Fourier basis is used to compute spectral convolution is signal processing. In graphs, the Laplacian basis is used described in this post.</figcaption></figure> <p>First, let’s recall what is a graph. A graph <em>G</em> is a set of <strong>nodes</strong> (vertices) connected by directed/undirected <strong>edges</strong>. In this post, I will assume an undirected graph <em>G</em> with <em>N</em> nodes. Each <strong>node</strong> in this graph has a <em>C</em>-dimensional feature vector, and features of all nodes are represented as an <em>N</em>×<em>C</em> dimensional matrix <em>X⁽ˡ⁾. </em><strong>Edges</strong> of a graph are represented as an <em>N</em>×<em>N </em>matrix A, where the entry A<em>ᵢⱼ</em> indicates if node <em>i</em> is connected (<em>adjacent</em>) to node <em>j</em>. This matrix is called an <em>adjacency matrix</em>.</p> <figure><img alt="" src="https://cdn-images-1.medium.com/max/754/1*68Gcr70UTpdaX7THbZSEcA.png"/><figcaption>Two undirected graphs with N=5 and N=6 nodes. The order of nodes is arbitrary.</figcaption></figure> <p>Spectral analysis of graphs (see lecture notes <a href="http://www.cs.yale.edu/homes/spielman/561/">here</a> and earlier work <a href="https://papers.nips.cc/paper/1961-laplacian-eigenmaps-and-spectral-techniques-for-embedding-and-clustering">here</a>) has been useful for graph clustering, community discovery and other <em>mainly unsupervised </em>learning tasks. In this post, I basically describe the work of <a href="https://arxiv.org/abs/1312.6203">Bruna et al., 2014, ICLR 2014</a> who combined spectral analysis with convolutional neural networks (ConvNets) giving rise to spectral <strong>graph convolutional networks </strong>that can be trained in a <em>supervised </em>way, for example for the graph classification task.</p> <p>Despite that <em>spectral</em> graph convolution is currently less commonly used compared to <em>spatial</em> graph convolution methods, knowing how spectral convolution works is still helpful to understand and avoid potential problems with other methods. Plus, in the conclusion I refer to some recent exciting works making spectral graph convolution more competitive.</p> <h3>1. Graph Laplacian and a little bit of physics</h3> <p>While “spectral” may sound complicated, for our purpose it’s enough to understand that it simply means <em>decomposing</em> a signal/audio/image/graph into a combination (usually, a sum) of simple elements (wavelets, graphlets). To have some nice properties of such a <em>decomposition</em>, these simple elements are usually <em>orthogonal</em>, i.e. mutually linearly independent, and therefore form a <em>basis</em>.</p> <p>When we talk about “spectral” in signal/image processing, we imply the <a href="https://en.wikipedia.org/wiki/Discrete_Fourier_transform">Fourier Transform</a>, which offers us a particular <em>basis</em> (<a href="https://en.wikipedia.org/wiki/DFT_matrix">DFT matrix</a>, e.g. scipy.linalg.dft in Python) of elementary sine and cosine waves of different frequencies, so that we can represent our signal/image as a sum of these waves. But when we talk about graphs and graph neural networks (GNNs), “spectral” implies <em>eigen-decomposition</em> of the <a href="https://en.wikipedia.org/wiki/Laplacian_matrix"><strong>graph Laplacian</strong></a><strong> </strong><em>L.</em> You can think of the the graph Laplacian <em>L</em> as an adjacency matrix <em>A</em> normalized in a special way, whereas <em>eigen-decomposition</em> is a way to find those elementary orthogonal components that make up our graph.</p> <p>Intuitively, the graph Laplacian shows in what directions and how <em>smoothly</em> the “energy” will diffuse over a graph if we put some “potential” in node <em>i</em>. A typical use-case of Laplacian in mathematics and physics is to solve how a signal (wave) propagates in a dynamic system. Diffusion is <em>smooth</em> when there is no sudden changes of values between neighbors as in the animation below.</p> <figure><img alt="" src="https://cdn-images-1.medium.com/max/560/1*gz2hyrcSSJG9MtDzmQLe3w.gif"/><figcaption>Diffusion of some signal (for example, it can be heat) in a regular grid graph computed based on the graph Laplacian (<a href="https://en.wikipedia.org/wiki/Laplacian_matrix">source</a>). Basically, the only things required to compute these dynamics are the Laplacian and initial values in nodes (pixels), i.e. red and yellow pixels corresponding to high intensity (of heat).</figcaption></figure> <p>In the rest of the post, I’m going to assume “<em>symmetric normalized Laplacian</em>”, which is often used in graph neural networks, because it is normalized so that when you stack many graph layers, the node features propagate in a more smooth way without explosion or vanishing of feature values or gradients. It is computed based <em>only</em> on an adjacency matrix <em>A</em> of a graph, which can be done in a few lines of Python code as follows:</p> <pre><strong># Computing the graph Laplacian<br /># A is an adjacency matrix of some graph <em>G</em><br /></strong>import numpy as np</pre> <pre>N = A.shape[0] <strong># number of nodes in a graph</strong><br />D = np.sum(A, 0) <strong># node degrees</strong><br />D_hat = np.diag((D + 1e-5)**(-0.5)) <strong># normalized node degrees</strong><br />L = np.identity(N) — np.dot(D_hat, A).dot(D_hat) <strong># Laplacian</strong></pre> <p>Here, we assume that <em>A</em> is symmetric, i.e. <em>A</em> = <em>A</em>ᵀ and our graph is undirected, otherwise node degrees are not well-defined and some assumptions must be made to compute the Laplacian. An interesting property of an adjacency matrix <em>A </em>is that <em>Aⁿ</em> (matrix product taken <em>n</em> times) exposes <em>n</em>-hop connections between nodes (see <a href="https://en.wikipedia.org/wiki/Adjacency_matrix#Matrix_powers">here</a> for more details).</p> <p>Let’s generate three graphs and visualize their adjacency matrices and Laplacians as well as their powers.</p> <figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*WSvWVAsQsGtQQpIcrCPOOQ.png"/><figcaption>Adjacency matrices, Laplacians and their powers for a random graph (left), “star graph” (middle) and “path graph” (right). I normalize A² such that the sum in each row equals 1 to have a probabilistic interpretation of 2-hop connections. Notice that Laplacians and their powers are symmetric matrices, which makes eigen-decomposition easier as well as facilitates feature propagation in a deep graph network.</figcaption></figure> <p>For example, imagine that the star graph above in the middle is made from metal, so that it transfers heat well. Then, if we start to heat up node 0 (dark blue), this heat will propagate to other nodes in a way defined by the Laplacian. In the particular case of a star graph with all edges equal, heat will spread uniformly to all other nodes, which is not true for other graphs due to their structure.</p> <p>In the context of computer vision and machine learning, the graph Laplacian defines how node features will be updated if we stack several graph neural layers. Similarly to <a href="https://medium.com/p/3d9fada3b80d"><em>the first part of my tutorial</em></a>, to understand spectral graph convolution from the computer vision perspective, I’m going to use the MNIST dataset, which defines images on a 28×28 regular grid graph.</p> <figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*EUFjx4cvVq4TdmU1PfXRpA.png"/><figcaption>MNIST image defining features X (left), adjacency matrix A (middle) and the Laplacian (right) of a regular 28×28 grid. The reason that the graph Laplacian looks like an identity matrix is that the graph has a relatively large number of nodes (784), so that after normalization values outside the diagonal become much smaller than 1.</figcaption></figure> <h3>2. Convolution</h3> <p>In signal processing, it can be shown that convolution in the spatial domain is multiplication in the frequency domain (a.k.a. <a href="https://en.wikipedia.org/wiki/Convolution_theorem">convolution theorem</a>). The same theorem can be applied to graphs. In signal processing, to transform a signal to the frequency domain, we use the Discrete Fourier Transform, which is basically matrix multiplication of a signal with a special matrix (basis, DFT matrix). This basis assumes a <em>regular</em> grid, so we cannot use it for <em>irregular</em> graphs, which is a typical case. Instead, we use a more general basis, which is eigenvectors <em>V</em> of the graph Laplacian <em>L</em>, which can be found by eigen-decomposition:<em> L</em>=<em>VΛVᵀ</em>, where <em>Λ</em> are eigenvalues of <em>L.</em></p> <p><strong>PCA vs eigen-decomposition of the graph Laplacian. </strong>To compute spectral graph convolution in practice, it’s enough to use a few eigenvectors corresponding to the <em>smallest</em> eigenvalues. At first glance, it seems to be an opposite strategy compared to frequently used in computer vision <a href="https://en.wikipedia.org/wiki/Principal_component_analysis">Principal component analysis (PCA)</a>, where we are more interested in the eigenvectors corresponding to the <em>largest</em> eigenvalues. However, this difference is simply due to the <em>negation</em> used to compute the Laplacian above, therefore eigenvalues computed using PCA are <em>inversely proportional</em> to eigenvalues of the graph Laplcacian (see <a href="http://outobox.cs.umn.edu/PCA_on_a_Graph.pdf">this paper</a> for a formal analysis). Note also that PCA is applied to the covariance matrix of a dataset for the purpose to extract the largest factors of variation, i.e. the dimensions along which data vary the most, like in <a href="https://en.wikipedia.org/wiki/Eigenface">Eigenfaces</a>. This variation is measured by eigenvalues, so that the smallest eigenvalues essentially correspond to noisy or “spurious” features, which are assumed to be useless or even harmful in practice.</p> <figure><img alt="" src="https://cdn-images-1.medium.com/max/643/1*k8AfLWuLW9sgOsuCarR19Q.png"/><figcaption>Eigenvalues (in a descending order) and corresponding eigenvectors for the MNIST dataset.</figcaption></figure> <p>Eigen-decomposition of the graph Laplacian is applied to a single graph for the purpose to extract subgraphs or clusters (communities) of nodes, and <a href="http://blog.shriphani.com/2015/04/06/the-smallest-eigenvalues-of-a-graph-laplacian/">eigenvalues tell us a lot about graph connectivity</a>. I will use eigenvectors corresponding to the 20 smallest eigenvalues in our examples below, assuming that 20 is much smaller than the number of nodes <em>N (N</em>=784 in case of MNIST<em>)</em>. To find eigenvalues and eigenvectors below on the left, I use a 28×28 regular graph, whereas on the right I follow the experiment of <a href="https://arxiv.org/abs/1312.6203">Bruna et al.</a> and construct an irregular graph by sampling 400 random locations on a 28×28 regular grid (see their paper for more details about this experiment).</p> <figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*93nVzwz_V7IPC7TPlgAsjQ.png"/><figcaption>Eigenvalues <em>Λ (</em><strong><em>bottom</em></strong><em>) and e</em>igenvectors V (<strong>top</strong>) of the graph Laplacian L for a regular 28<em>×</em>28 grid (<strong>left</strong>) and non-uniformly subsampled grid with 400 points according to experiments in <a href="https://arxiv.org/abs/1312.6203">Bruna et al., 2014, ICLR 2014</a> (<strong>right</strong>). Eigenvectors corresponding to the 20 <strong>smallest</strong> <strong>eigenvalues</strong> are shown. Eigenvectors are 784 dimensional on the left and 400 dimensional on the right, so V is 784<em>×20 and 400×20 respectively. </em>Each of the 20 eigenvectors on the left was reshaped to 28<em>×</em>28, whereas on the right to reshape a 400 dimensional eigenvector to 28<em>×28, white pixels for missing nodes were added. So, e</em>ach pixel in each eigenvector corresponds to a node or a missing node (in white on the right). These eigenvectors can be viewed as a basis in which we decompose our graph.</figcaption></figure> <p>So, given graph Laplacian <em>L</em>, node features <em>X</em> and filters <em>W</em>_spectral, in Python <strong>spectral convolution on graphs</strong> looks very simple:</p> <pre><strong># Spectral convolution on graphs<br /># X is an <em>N×1 matrix of 1-dimensional node features<br /></em></strong><strong># L is an </strong><strong><em>N×N</em> graph Laplacian computed above<br /># W_spectral are </strong><strong><em>N×</em></strong><strong><em>F weights (filters) that we want to train<br /></em></strong>from scipy.sparse.linalg import eigsh <strong># assumes </strong><strong><em>L</em></strong><strong> to be symmetric</strong></pre> <pre><em>Λ</em><em>,V</em> = eigsh(L,k=20,which=’SM’) <strong># eigen-decomposition (i.e. find <em>Λ</em></strong><strong><em>,V)</em></strong><br />X_hat = V.T.dot(X) <strong># </strong><strong><em>20</em>×</strong><strong><em>1</em></strong><strong> node features in the &quot;spectral&quot; domain</strong><br />W_hat = V.T.dot(W_spectral)  <strong># 20×<em>F</em> filters in the </strong><strong>&quot;spectral&quot; domain</strong><br />Y = V.dot(X_hat * W_hat)  <strong># </strong><strong><em>N×</em></strong><strong><em>F</em></strong><strong> result of convolution</strong></pre> <p>Formally:</p> <figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*wBIfFw54z8usWq_merON8A.png"/><figcaption>Spectral graph convolution, where ⊙ means element-wise multiplication.</figcaption></figure> <p>where we assume that our node features <em>X⁽ˡ⁾ </em>are 1-dimensional, e.g. MNIST pixels, but it can be extended to a <em>C</em>-dimensional case: we will just need to repeat this convolution for each <em>channel</em> and then sum over <em>C</em> as in signal/image convolution.</p> <p>Formula (3) is essentially the same as <a href="https://en.wikipedia.org/wiki/Convolution_theorem">spectral convolution of signals on regular grids</a> using the Fourier Transform, and so creates a few problems for machine learning:</p> <ul><li>the dimensionality of trainable weights (filters) <em>W_</em>spectral depends on the number of nodes <em>N</em> in a graph;</li><li><em>W_</em>spectral also depends on the graph structure encoded in eigenvectors <em>V.</em></li></ul> <p>These issues prevent scaling to datasets with large graphs of variable structure. Further efforts, summarized below, were focused on resolving these and other issues.</p> <h3><strong>3. “Smoothing” in the spectral domain</strong></h3> <figure><img alt="" src="https://cdn-images-1.medium.com/max/412/1*PcKEUB4wTOG6gtoEIZl9iA.png"/><figcaption>Strawberry and banana smoothie (source: <a href="https://joyfoodsunshine.com/strawberry-banana-smoothie/">joyfoodsunshine.com</a>). Smoothing in the spectral domain is a little bit different 😃.</figcaption></figure> <p><a href="https://arxiv.org/abs/1312.6203">Bruna et al.</a> were one of the first to apply spectral graph analysis to <em>learn convolutional filters</em> for the graph classification problem. The filters learned using formula (3) above act on the <em>entire graph</em>, i.e. they have <em>global support</em>. In the computer vision context, this would be the same as training convolutional filters of size 28×28 pixels on MNIST, i.e. filters have the same size as the input (note that we would still slide a filter, but over a zero-padded image). While for MNIST we can actually train such filters, the common wisdom suggests to avoid that, as it makes training much harder due to the potential explosion of the number of parameters and difficulty of training large filters that can capture useful features shared across different images.</p> <p>I actually successfully trained such a model using PyTorch and <a href="https://github.com/bknyaz/examples/blob/master/fc_vs_graph_train.py">this code</a> from my GitHub. You should run it using mnist_fc.py --model conv. After training for 100 epochs, the filters look like mixtures of digits:</p> <figure><img alt="" src="https://cdn-images-1.medium.com/max/864/1*kNftNPG_J4i_pUN40DjAXQ.png"/><figcaption>Examples of filters with <strong>global support</strong> typically used in spectral convolution. In this case, these are 28×28 filters learned using a ConvNet with a single convolutional layer followed by ReLU, 7×7 MaxPooling and a fully-connected classification layer. To make it clear, the output of the convolutional layer is still 28×28 due to zero-padding. Surprisingly, this net achieves 96.7% on MNIST. This can be explained by the simplicity of the dataset.</figcaption></figure> <p>To reiterate, we generally want to make filters smaller and more local (which is not exactly the same as I’ll note below).</p> <p>To enforce that implicitly, they proposed to <em>smooth</em> filters in the spectral domain, which makes them <em>more local</em> in the spatial domain according to the spectral theory. The idea is that you can represent our filter <em>W_</em>spectral from formula (3) as a sum of 𝐾 predefined functions, such as splines, and instead of learning <em>N</em> values of <em>W</em>, we learn <em>K </em>coefficients <em>α</em> of this sum:</p> <figure><img alt="" src="https://cdn-images-1.medium.com/max/832/1*sZoZfh6faYLBm7_Nq3xrQw.png"/><figcaption>We can approximate our N dimensional filter<strong> </strong><em>W_</em>spectral as a finite sum of<em> K</em> functions f, such as splines shown below. So, instead of learning N values of <em>W_</em>spectral, we can learn K coefficients (alpha) of those functions; it becomes efficient when K &lt;&lt; N.</figcaption></figure> <p>While the dimensionality of <em>fk</em> does depend on the number of nodes <em>N</em>, these functions are fixed, so we don’t learn them. The only thing we learn are coefficients <em>α</em>, and so <em>W_</em>spectral is no longer dependent on <em>N</em>. Neat, right?</p> <figure><img alt="" src="https://cdn-images-1.medium.com/max/864/1*DJWQBxMX3hZz85pKhma34w.png"/><figcaption>The spline basis used to smooth filters in the frequency domain, thereby making them more local. Splines and other polynomial functions are useful, because we can represent filters as their sums.</figcaption></figure> <p>To make our approximation in formula (4) reasonable, we want <em>K</em>&lt;&lt;<em>N</em> to reduce the number of trainable parameters from <em>N</em> to <em>K</em> and, more importantly, make it independent of <em>N</em>, so that our GNN can digest graphs of any size. We can use different bases to perform this “expansion”, depending on which properties we need. For instance, cubic splines shown above are known as very smooth functions (i.e. you cannot see knots, i.e. where the pieces of the piecewise spline polynomial meet). The Chebyshev polynomial, which I discuss in <a href="https://medium.com/@BorisAKnyazev/tutorial-on-graph-neural-networks-for-computer-vision-and-beyond-part-2-be6d71d70f49">my another post</a>, has the minimum 𝑙∞ distance between the approximating function. The Fourier basis is the one that preserves most of the signal energy after transformation. Most bases are orthogonal, because it would be redundant to have terms that can be expressed by each other.</p> <p>Note that filters <em>W_</em>spectral are still as large as the input, but their <em>effective width </em>is small. In case of MNIST images, we would have 28×28 filters, in which only a small fraction of values would have an absolute magnitude larger than 0 and all of them should be located close to each other, i.e. the filter would be local and effectively small, something like the one below (second from the left):</p> <figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*1NL_awIic9m5P-IF5_3J2A.png"/><figcaption>From left to right: (first) Input image. (second) Local filter with small effective width. Most values are very close to 0. (third) The result of spectral graph convolution of the MNIST image of digit 7 and the filter. (fourth) The result of spectral convolution using the Fourier transform. These results indicate that spectral graph convolution is quite limited if applied to images, perhaps, due to the weak spatial structure of the Laplacian basis compared to the Fourier basis.</figcaption></figure> <figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*WtfLzUxDwyU8gwAD8u5HgQ.png"/><figcaption>Reconstruction of the MNIST image using the Fourier and graph Laplacian bases using only M components of V: X’=V V<em>ᵀX</em>. We can see that the bases compress different patterns in images (orientated edges in the Fourier case and global patterns in the Laplacian case). This makes results of convolutions illustrated above very different.</figcaption></figure> <p>To summarize, smoothing in the spectral domain allowed <a href="https://arxiv.org/abs/1312.6203">Bruna et al.</a> to learn more local filters. The model with such filters can achieve similar results as the model without smoothing (i.e. using our formula (3)), but with much fewer trainable parameters, because the filter size is independent of the input graph size, which is important to scale the model to datasets with larger graphs. However, learned filters <em>W</em>_spectral still depend on eigenvectors <em>V</em>, which makes it challenging to apply this model to datasets with variable graph structures.</p> <h3>Conclusion</h3> <p>Despite the drawbacks of the original spectral graph convolution method, it has been developed a lot and has remained a quite competitive method in some applications, because spectral filters can better capture global complex patterns in graphs, which local methods like GCN (<a href="https://arxiv.org/abs/1609.02907">Kipf &amp; Welling, ICLR, 2017</a>) cannot unless stacked in a deep network. For example, two ICLR 2019 papers, of <a href="https://arxiv.org/abs/1901.01484">Liao et al.</a> on “LanczosNet” and <a href="https://arxiv.org/abs/1904.07785">Xu et al.</a> on “Graph Wavelet Neural Network”, address some shortcomings of spectral graph convolution and show great results in predicting molecule properties and node classification. Another interesting work of <a href="https://arxiv.org/abs/1705.07664">Levie et al., 2018</a> on “CayleyNets” showed strong performance in node classification, matrix completion (recommender systems) and community detection. So, depending on your application and infrastructure, spectral graph convolution can be a good choice.</p> <p>In another part of my <a href="https://medium.com/@BorisAKnyazev/tutorial-on-graph-neural-networks-for-computer-vision-and-beyond-part-2-be6d71d70f49">Tutorial on Graph Neural Networks for Computer Vision and Beyond</a> I explain Chebyshev spectral graph convolution introduced by <a href="https://arxiv.org/abs/1606.09375">Defferrard et al.</a> in 2016, which is still a very strong baseline that has some nice properties and is easy to implement as I demonstrate using PyTorch.</p> <p><em>Acknowledgement: A large portion of this tutorial was prepared during my internship at SRI International under the supervision of </em><a href="https://medium.com/u/6cf41cb2c546"><em>Mohamed Amer</em></a><em> (</em><a href="https://mohamedramer.com/"><em>homepage</em></a><em>) and my PhD advisor Graham Taylor (</em><a href="https://www.gwtaylor.ca/"><em>homepage</em></a><em>). I also thank </em><a href="https://www.linkedin.com/in/carolynaugusta/"><em>Carolyn Augusta</em></a><em> for useful feedback.</em></p> <p>Find me on <a href="https://github.com/bknyaz/">Github</a>, <a href="https://www.linkedin.com/in/boris-knyazev-39690948/">LinkedIn</a> and <a href="https://twitter.com/BorisAKnyazev">Twitter</a>. <a href="https://bknyaz.github.io/">My homepage</a>.</p> <p>If you want to cite this blog post in your paper, please use:<br/><a href="http://twitter.com/misc"><em>@misc</em></a><em>{knyazev2019tutorial,<br/> title={Tutorial on Graph Neural Networks for Computer Vision and Beyond},<br/> author={Knyazev, Boris and Taylor, Graham W and Amer, Mohamed R},<br/> year={2019}<br/>}</em></p> <p><img src="https://medium.com/_/stat?event=post.clientViewed&amp;referrerSource=full_rss&amp;postId=2e495b57f801" width="1" height="1" alt=""/>&lt;hr&gt;&lt;p&gt;<a href="https://medium.com/data-science/spectral-graph-convolution-explained-and-implemented-step-by-step-2e495b57f801">Spectral Graph Convolution Explained and Implemented Step By Step</a> was originally published in <a href="https://medium.com/data-science">TDS Archive</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.&lt;/p&gt;</p>]]></content><author><name></name></author><category term="medium"/></entry><entry><title type="html">Anisotropic, Dynamic, Spectral and Multiscale Filters Defined on Graphs</title><link href="https://bknyaz.github.io//blog/2019/anisotropic-dynamic-spectral-and-multiscale-filters-defined-on-graphs/" rel="alternate" type="text/html" title="Anisotropic, Dynamic, Spectral and Multiscale Filters Defined on Graphs"/><published>2019-08-12T17:26:10+00:00</published><updated>2019-08-12T17:26:10+00:00</updated><id>https://bknyaz.github.io//blog/2019/anisotropic-dynamic-spectral-and-multiscale-filters-defined-on-graphs</id><content type="html" xml:base="https://bknyaz.github.io//blog/2019/anisotropic-dynamic-spectral-and-multiscale-filters-defined-on-graphs/"><![CDATA[<h4>As part of the “Tutorial on Graph Neural Networks for Computer Vision and Beyond”</h4> <p><em>I’m presenting an overview of important Graph Neural Network works, by distilling key ideas and explaining simple intuition behind milestone methods using Python and PyTorch. This post continues </em><a href="https://medium.com/p/3d9fada3b80d"><em>the first part of my tutorial</em></a><em>.</em></p> <figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*GMDzusqMN82diJjSxgSW8w.png"/><figcaption>Graph of Graph Neural Network (GNN) and related works. Some other important works and edges are not shown to avoid further clutter. For example, there is a large body of works on dynamic graphs that deserve a separate overview. Best viewed on a very wide screen in color.</figcaption></figure> <h3>20+ years of Graph Neural Networks</h3> <p>In the “<strong>Graph of Graph Neural Network (GNN) and related works</strong>” above, I added papers on graphs that I have come across in the last year. In this graph, a directed edge between two works denotes that one paper is based on the other (while not necessary citing it) and a color of the work denotes:</p> <ul><li>Red — <strong>spectral methods</strong> (require eigen-decomposition of the graph Laplacian, which will be explained below)</li><li>Green — methods that work in the <strong>spatial domain</strong> (do not require eigen-decomposition of the graph Laplacian)</li><li>Blue — equivalent to spectral methods, but do not require eigen-decomposition (so, effectively, spatial methods)</li><li>Black — are methods complementary to GNNs and agnostic to the choice of a GNN itself (i.e. pooling, attention).</li></ul> <p>Note, that some other important works and edges are not shown to avoid further clutter, and only a tiny fraction of works, highlighted <strong>in bold</strong> boxes, will be covered in this post. Disclaimer: I still found room to squeeze our own recent works there 😊.</p> <p>Most of the important methods are covered in this non-exhaustive list of works:</p> <ul><li>Nicket et al., 2015, <a href="https://arxiv.org/abs/1503.00759">A Review of Relational Machine Learning for Knowledge Graphs</a></li><li>Bronstein et al., 2016, <a href="https://arxiv.org/abs/1611.08097">Geometric deep learning: going beyond Euclidean data</a></li><li>Hamilton et al., 2017, <a href="https://arxiv.org/abs/1709.05584">Representation Learning on Graphs: Methods and Applications</a></li><li>Kipf et al., 2018, <a href="http://tkipf.github.io/misc/SlidesCambridge.pdf">Structured deep models: Deep learning on graphs and beyond</a>, presentation slides.</li><li>Battaglia et al., 2018, <a href="https://arxiv.org/abs/1806.01261">Relational inductive biases, deep learning, and graph networks</a></li><li>Zhang et al., 2018 <a href="https://arxiv.org/abs/1812.04202">Deep Learning on Graphs: A Survey</a></li><li>Zhou et al., 2018, <a href="https://arxiv.org/abs/1812.08434">Graph Neural Networks: A Review of Methods and Applications</a></li><li>Wu et al., 2019, <a href="https://arxiv.org/abs/1901.00596">A Comprehensive Survey on Graph Neural<br/>Networks</a></li><li>Petar Veličković, 2019, <a href="https://www.repository.cam.ac.uk/handle/1810/292230">The resurgence of structure in deep neural networks</a>, PhD Thesis.</li><li>NIPS and CVPR <a href="https://sungsoo.github.io/2018/02/01/geometric-deep-learning.html">video tutorials</a></li></ul> <p>The first work where graphs were classified using a neural network seems to be a <strong>1997</strong> paper by <a href="https://ieeexplore.ieee.org/document/572108">Alessandro Sperduti and Antonina Starita on “Supervised Neural Networks for the Classification of Structures”</a>.</p> <figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9CdkztOnwaRFkiZTu1gYgQ.png"/><figcaption>A figure from (<a href="https://ieeexplore.ieee.org/document/572108">Sperduti &amp; Starita, 1997</a>), which is strikingly similar to what we are doing now, after more than 20 years.</figcaption></figure> <blockquote><a href="https://ieeexplore.ieee.org/document/572108">Sperduti &amp; Starita, 1997</a>: “Until now neural networks have been used for classifying unstructured patterns and sequences. However, standard neural networks and statistical methods are usually believed to be inadequate when dealing with complex structures because of their feature-based approach.”</blockquote> <p>From 1997, the body of works on learning from graphs has grown so much and in so many diverse directions that it is very hard to keep track without some smart automated system. I believe we are converging to using methods based on neural networks (based on our formula (2) explained in <a href="https://medium.com/p/3d9fada3b80d"><em>the first part of my tutorial</em></a><em>)</em>, or some combination of neural networks and other methods.</p> <figure><img alt="" src="https://cdn-images-1.medium.com/max/741/1*Ht9tBXaTrV2gkbMIe7zxMg.png"/><figcaption>Graph neural layer’s formula (2) from <a href="https://medium.com/p/3d9fada3b80d"><em>the first part of my tutorial</em></a><em> that we will also need in this part. Keep in mind, that if we need to compute a specific loss for the output features or if we need to stack these layers, we apply some activation like ReLU or Softmax.</em></figcaption></figure> <p>To recap the notation we used in the first part, we have some undirected graph <em>G</em> with <em>N</em> nodes. Each node in this graph has a <em>C</em>-dimensional feature vector, and features of all nodes are represented as an <em>N</em>×<em>C</em> dimensional matrix <em>X⁽ˡ⁾. </em>In a typical graph network, such as GCN (<a href="https://arxiv.org/abs/1609.02907">Kipf &amp; Welling, ICLR, 2017</a>), we feed these features <em>X⁽ˡ⁾</em> to a graph neural layer with <em>C</em>×<em>F</em> dimensional trainable weights <em>W⁽ˡ⁾ </em>, so that the output of this layer is an <em>N</em>×<em>F</em> matrix <em>X⁽ˡ⁺¹</em>⁾ encoding updated (and hopefully better in some sense) node features. 𝓐 is an <em>N</em>×<em>N </em>matrix, where the entry 𝓐<em>ᵢⱼ</em> indicates if node <em>i</em> is connected (<em>adjacent</em>) to node <em>j</em>. This matrix is called an <em>adjacency matrix</em>. I use 𝓐 instead of plain <em>A</em> to highlight that this matrix can be <em>normalized</em> in a way to facilitate feature propagation in a deep network. For the purpose of this tutorial, we can assume that 𝓐=<em>A</em>, i.e. each <em>i</em>-th<em> </em>row of the matrix product 𝓐<em>X⁽ˡ⁾ </em>will contain a sum of features of node <em>i</em> neighbors.</p> <p>In the rest of this part of the tutorial, I’ll briefly explain works of my choice showed in <strong>bold</strong> boxes in the overview graph. I recommend <a href="https://arxiv.org/abs/1611.08097">Bronstein et al.’s review</a> for a more comprehensive and formal analysis.</p> <p>Note that even though I dive into some technical details of <strong>spectral graph convolution</strong> below, many recent works (e.g., GIN in <a href="https://arxiv.org/abs/1810.00826">Xu et al., ICLR, 2019</a>) are built without spectral convolution and show great results in some tasks. However, knowing how spectral convolution works is still helpful to understand and avoid potential problems with other methods.</p> <h3><strong>1. Spectral graph convolution</strong></h3> <p><a href="https://arxiv.org/abs/1312.6203">Bruna et al., 2014, ICLR 2014</a></p> <p>I explain spectral graph convolution in detail in my <a href="https://towardsdatascience.com/spectral-graph-convolution-explained-and-implemented-step-by-step-2e495b57f801">another post</a>.</p> <p>I’ll briefly summarize it here for the purpose of this part of the tutorial. A formal definition of spectral graph convolution, which is very similar to the <a href="https://en.wikipedia.org/wiki/Convolution_theorem">convolution theorem</a> in signal/image processing, can be written as:</p> <figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*wBIfFw54z8usWq_merON8A.png"/><figcaption>Spectral graph convolution, where ⊙ means element-wise multiplication.</figcaption></figure> <p>where <em>V</em> are eigenvectors and <em>Λ</em> are eigenvalues of the <a href="https://en.wikipedia.org/wiki/Laplacian_matrix"><strong>graph Laplacian</strong></a><strong> </strong><em>L</em>, which can be found by eigen-decomposition:<em> L</em>=<em>VΛVᵀ; W</em>_spectral are filters. Throughout this tutorial I’m going to assume “<em>symmetric normalized Laplacian</em>”. It is computed based <em>only </em>on an adjacency matrix <em>A</em> of a graph, which can be done in a few lines of Python code as follows:</p> <pre><strong># Computing the graph Laplacian<br /># A is an adjacency matrix<br /></strong>import numpy as np</pre> <pre>N = A.shape[0] <strong># number of nodes in a graph</strong><br />D = np.sum(A, 0) <strong># node degrees</strong><br />D_hat = np.diag((D + 1e-5)**(-0.5)) <strong># normalized node degrees</strong><br />L = np.identity(N) — np.dot(D_hat, A).dot(D_hat) <strong># Laplacian</strong></pre> <p>Here, we assume that <em>A</em> is symmetric, i.e. <em>A</em> = <em>A</em>ᵀ and our graph is undirected, otherwise node degrees are not well-defined and some assumptions must be made to compute the Laplacian. In the context of computer vision and machine learning, the graph Laplacian defines how node features will be updated if we stack several graph neural layers in the form of formula (2).</p> <p>So, given graph Laplacian <em>L</em>, node features <em>X</em> and filters <em>W</em>_spectral, in Python <strong>spectral convolution on graphs</strong> looks very simple:</p> <pre><strong># Spectral convolution on graphs<br /># X is an <em>N×1 matrix of 1-dimensional node features<br /></em></strong><strong># L is an </strong><strong><em>N×N</em> graph Laplacian computed above<br /># W_spectral are </strong><strong><em>N×</em></strong><strong><em>F weights (filters) that we want to train<br /></em></strong>from scipy.sparse.linalg import eigsh <strong># assumes </strong><strong><em>L</em></strong><strong> to be symmetric</strong></pre> <pre><em>Λ</em><em>,V</em> = eigsh(L,k=20,which=’SM’) <strong># eigen-decomposition (i.e. find <em>Λ</em></strong><strong><em>,V)</em></strong><br />X_hat = V.T.dot(X) <strong># </strong><strong><em>20</em>×</strong><strong><em>1</em></strong><strong> node features in the &quot;spectral&quot; domain</strong><br />W_hat = V.T.dot(W_spectral)  <strong># 20×<em>F</em> filters in the </strong><strong>&quot;spectral&quot; domain</strong><br />Y = V.dot(X_hat * W_hat)  <strong># </strong><strong><em>N×</em></strong><strong><em>F</em></strong><strong> result of convolution</strong></pre> <p>where we assume that our node features <em>X⁽ˡ⁾ </em>are 1-dimensional, e.g. MNIST pixels, but it can be extended to a <em>C</em>-dimensional case: we will just need to repeat this convolution for each <em>channel</em> and then sum over <em>C</em> as in signal/image convolution.</p> <p>Formula (3) is essentially the same as <a href="https://en.wikipedia.org/wiki/Convolution_theorem">spectral convolution of signals on regular grids</a> using the Fourier Transform, and so creates a few problems for machine learning:</p> <ol><li>the dimensionality of trainable weights (filters) <em>W_</em>spectral depends on the number of nodes <em>N</em> in a graph;</li><li><em>W_</em>spectral also depends on the graph structure encoded in eigenvectors <em>V.</em></li></ol> <p>These issues prevent scaling to datasets with large graphs of variable structure.</p> <p>To solve the first issue, <a href="https://arxiv.org/abs/1312.6203">Bruna et al.</a> proposed to <em>smooth</em> filters in the spectral domain, which makes them <em>more local</em> in the spatial domain according to the spectral theory. The idea is that you can represent our filter <em>W_</em>spectral from formula (3) as a sum of 𝐾 predefined functions, such as splines, and instead of learning <em>N</em> values of <em>W</em>, we learn <em>K </em>coefficients <em>α</em> of this sum:</p> <figure><img alt="" src="https://cdn-images-1.medium.com/max/832/1*sZoZfh6faYLBm7_Nq3xrQw.png"/><figcaption>We can approximate our N dimensional filter<strong> </strong><em>W_</em>spectral as a finite sum of<em> K</em> functions f, such as splines shown below. So, instead of learning N values of <em>W_</em>spectral, we can learn K coefficients (alpha) of those functions; it becomes efficient when K &lt;&lt; N.</figcaption></figure> <p>While the dimensionality of <em>fk</em> does depend on the number of nodes <em>N</em>, these functions are fixed, so we don’t learn them. The only thing we learn are coefficients <em>α</em>, and so <em>W_</em>spectral is no longer dependent on <em>N</em>. To make our approximation in formula (4) reasonable, we want <em>K</em>&lt;&lt;<em>N</em> to reduce the number of trainable parameters from <em>N</em> to <em>K</em> and, more importantly, make it independent of <em>N</em>, so that our GNN can digest graphs of any size.</p> <p>While solves the first issue, this smoothing method does not address the second issue.</p> <h3><strong>2. Chebyshev</strong> graph <strong>convolution</strong></h3> <p><a href="https://arxiv.org/abs/1606.09375">Defferrard et al., NeurIPS, 2016</a></p> <p>The main drawback of spectral convolution and its smooth version above is that it still requires eigen-decomposition of an <em>N</em>×<em>N</em> dimensional graph Laplacian <em>L</em>, which creates two main problems:</p> <ol><li>🙁 The complexity of eigen-decomposition is huge, O(<em>N³</em>). Moreover in case of large graphs, keeping the graph Laplacian in a dense format in RAM is infeasible. One solution is to use sparse matrices and find eigenvectors using scipy.sparse.linalg.eigs in Python. Additionally, you may preprocess all training graphs on a dedicated server with a lot of RAM and CPU cores. In many applications, your test graphs can also be preprocessed in advance, but if you have a constant influx of new large graphs, eigen-decomposition will make you sad.</li><li>🙁 Another problem is that the model you train ends up being closely related to the eigenvectors <em>V</em> of the graph. This can be a big problem if your training and test graphs have very different structures (numbers of nodes and edges). Otherwise, if all graphs are very similar, it is less of a problem. Moreover, if you use some smoothing of filters in the frequency domain like splines discussed above, then your filters become more localized and the problem of adapting to new graphs seems to be even less noticeable. However, the models will still be quite limited.</li></ol> <p>Now, what does Chebyshev graph convolution have to do with all that?</p> <p>It turns out that it solves <strong>both problems at the same time!</strong> 😃</p> <p>That is, it avoids computing costly eigen-decomposition and the filters are no longer “attached” to eigenvectors (yet they still are functions of eigenvalues <em>Λ)</em>. Moreover, it has a very useful parameter, usually denoted as <em>K</em> having a similar intuition as <em>K</em> in our formula (4) above, determining the locality of filters. Informally: for <em>K</em>=1, we feed just node features <em>X⁽ˡ⁾</em> to our GNN; for <em>K</em>=2, we feed <em>X⁽ˡ⁾</em><strong> </strong>and 𝓐<em>X⁽ˡ⁾</em>; for K=3, we feed <em>X⁽ˡ⁾</em><strong>,</strong> 𝓐<em>X⁽ˡ⁾</em><strong> </strong>and 𝓐²<em>X⁽ˡ⁾</em>; and so forth for larger <em>K</em> (I hope you’ve noticed the pattern). See more accurate and formal definition in <a href="https://arxiv.org/abs/1606.09375">Defferrard et al.</a> and my code below, plus additional analysis is given in (<a href="https://arxiv.org/abs/1811.09595">Knyazev et al., NeurIPS-W, 2018</a>).</p> <p>Due to <a href="https://en.wikipedia.org/wiki/Adjacency_matrix#Matrix_powers">the power property</a> of adjacency matrices, when we perform 𝓐²<em>X⁽ˡ⁾</em> we actually average (or sum depending on how 𝓐 is normalized) over 2-hop neighbors, and analogously for any <em>n </em>in 𝓐<em>ⁿX⁽ˡ⁾</em> as illustrated below, where we average over <em>n</em>-hop neighbors.</p> <figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ybJ4HOmtSwhmCB1f2JX07A.png"/><figcaption>Chebyshev convolution for <em>K</em>=3 for node 1 (dark blue). Circled nodes denote the nodes affecting feature representation of node 1. The [,] operator denotes concatenation over the feature dimension. W<em>⁽ˡ⁾ are 3C</em>×F dimensional weights.</figcaption></figure> <p>Note that to satisfy the orthogonality of the Chebyshev basis, 𝓐<strong> </strong>assumes no loops in the graph, so that in each <em>i</em>-th row of matrix product 𝓐<em>X⁽ˡ⁾</em> we will have features of the neighbors of node <em>i</em>, but <strong>not</strong> the features of node <em>i</em> itself. Features of node <em>i</em> will be fed separately as a matrix <em>X⁽ˡ⁾.</em></p> <p>If <em>K</em> equals the number of nodes <em>N</em>, the Chebyshev convolution closely approximates a spectral convolution, so that the receptive field of filters will be the entire graph. But, as in the case of convolutional networks, we don’t want our filters to be as big as the input images for a number of reasons that I already discussed, so in practice, <em>K</em> takes reasonably small values.</p> <blockquote>In my experience, this is one of the most powerful GNNs, achieving great results in a very wide range of graph tasks. The main downside is the necessity to loop over <em>K</em> in the forward/backward pass (since Chebyshev polynomials are recursive, so it’s not possible to parallelize them), which slows down the model.</blockquote> <p>Same as with Splines discussed above, instead of training filters, we train coefficients, but this time, of the Chebyshev polynomial.</p> <figure><img alt="" src="https://cdn-images-1.medium.com/max/864/1*SGSYcSA5WqGYPYDwKTQT1g.png"/><figcaption>Chebyshev basis used to approximate convolution in the spectral domain.</figcaption></figure> <p>To generate the Chebyshev basis, you can use the following Python code:</p> <pre><strong># Set K to some integer &gt; 0, like 4 or 5 in our plots above<br /># Set n_points to a number of points on a curve (we set to 100)<br /></strong>import numpy as np</pre> <pre>x = np.linspace(-1, 1, n_points)<br />T = np.zeros((K, len(x)))<br />T[0,:] = 1<br />T[1,:] = x<br />for n in range(1, K-1):<br />    T[n+1, :] = 2*x*T[n, :] - T[n-1, :] <strong># recursive computation</strong>   <br />return T</pre> <p>The full code to generate spline and Chebyshev bases is in <a href="https://github.com/bknyaz/examples/blob/master/splines_cheb.py">my github repo</a>.</p> <p>To illustrate how a Chebyshev filter can look on a irregular grid, I follow the experiment from <a href="https://arxiv.org/abs/1312.6203">Bruna et al.</a> again and sample 400 random points from the MNIST grid in the same way as I did to show eigenvectors of the graph Laplacian. I trained a Chebyshev graph convolution model on the MNIST images sampled from these 400 locations (same irregular grid is used for all images) and one of the filter for <em>K</em>=1 and <em>K</em>=20 is visualized below.</p> <figure><img alt="" src="https://cdn-images-1.medium.com/max/396/1*Hd0dkgJNOfOs5KAo3oIiwQ.gif"/><figcaption>A single Chebyshev filter (K=3 on the left and K=20 on the right) trained on MNIST and applied at different locations (shown as a red pixel) on a irregular grid with 400 points. Compared to filters of standard ConvNets, GNN filters have different shapes depending <em>on the node at which they are applied</em>, because each node has a different neighborhood structure.</figcaption></figure> <h3><strong>3. GCN</strong></h3> <p><a href="https://arxiv.org/abs/1609.02907">Kipf &amp; Welling, ICLR, 2017</a></p> <p>As you may have noticed, if you increase <em>K</em> of the Chebyshev convolution, it increases the total number of trainable parameters. For example, for <em>K</em>=2, our weights <em>W⁽ˡ⁾</em> will be 2<em>C</em>×<em>F</em> instead of just <em>C</em>×<em>F</em>. This is because we concatenate features <em>X⁽ˡ⁾</em><strong> </strong>and 𝓐<em>X⁽ˡ⁾</em> into a single <em>N</em>×2<em>C</em> matrix. More training parameters means the model<em> </em>is<em> </em>more difficult to train and more data must be labeled<em> </em>for training. Graph datasets are often extremely small. Whereas in computer vision, MNIST is considered a tiny dataset, because images are just 28×28 dimensional and there are only 60k training images, in terms of graph networks MNIST is quite large, because each graph would have <em>N</em>=784 nodes and 60k is a large number of training graphs. In contrast to computer vision tasks, many graph datasets have only around 20–100 nodes and 200–1000 training examples. These graphs can represent certain small molecules and labeling chemical/biological data is usually more expensive than labeling images. Therefore, training Chebyshev convolution models can lead to severe overfitting of the training set (i.e. the model will have the training loss close to 0 yet will have a large validation or test error). So, GCN of <a href="https://arxiv.org/abs/1609.02907">Kipf &amp; Welling</a> essentially “merged” matrices of node features <em>X⁽ˡ⁾</em><strong> </strong>and 𝓐<em>X⁽ˡ⁾</em> into a single <em>N</em>×<em>C</em> matrix. As a result, the model has two times fewer parameters to train compared to Chebyshev convolution with <em>K</em>=2, yet has the same receptive field of 1 hop. The main trick involves adding “self-loops” to your graph by adding an <a href="https://en.wikipedia.org/wiki/Identity_matrix">identity matrix</a> <em>I</em> to 𝓐<em> </em>and normalizing it in a particular way, so now in each <em>i</em>-th row of matrix product 𝓐<em>X⁽ˡ⁾</em> we will have features of the neighbors of node <em>i, </em><strong>as well as</strong> features of node <em>i.</em></p> <blockquote>This model seems to be a standard baseline choice well-suited for many application due to its lightweight, good performance and scalability to larger graphs.</blockquote> <h4>3.1. GCN vs Chebyshev layer</h4> <p>The difference between GCN and Chebyshev convolution is illustrated below.</p> <iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/2a9263af373ed75b1d578f00612e0ef8/href">https://medium.com/media/2a9263af373ed75b1d578f00612e0ef8/href</a></iframe> <p>The code above follows the same structure as in <a href="https://medium.com/p/3d9fada3b80d"><em>the first part of my tutorial</em></a>, where I compared classical NN and GNN. One of the main steps both in GCN and Chebyshev convolution is computation of the rescaled graph Laplacian <em>L</em>. This rescaling is done to make eigenvalues in the range [-1,1] to facilitate training (this might be not a very important step in practice as weights can adapt during training). In GCN, self-loops are added to the graph by adding an identity matrix before computing the Laplacian as discussed above. The main difference between the two methods is that in the Chebyshev convolution we <em>recursively</em> loop over <em>K</em> to capture features in the <em>K</em>-hop neighborhood. We can stack such GCN or Chebyshev layers interleaved with nonlinearities to build a Graph Neural Network.</p> <p>Now, let me politely interrupt 😃 our spectral discussion and give a general idea behind two other exciting methods: Edge-conditioned filters by <a href="https://arxiv.org/abs/1704.02901">Simonovsky &amp; Komodakis, CVPR, 2017</a> and MoNet by <a href="https://arxiv.org/abs/1611.08402">Monti et al., CVPR, 2017</a>, which share some similar concepts.</p> <h3><strong>4. Edge-conditioned</strong> filters</h3> <p><a href="https://arxiv.org/abs/1704.02901">Simonovsky &amp; Komodakis, CVPR, 2017</a></p> <p>As you know, in ConvNets we learn the weights (filters) by optimizing some loss like <a href="https://pytorch.org/docs/stable/nn.html#torch.nn.CrossEntropyLoss">Cross Entropy</a>. In the same way, we learn our <em>W⁽ˡ⁾ </em>in GNNs. Imagine that instead of learning these weights, you have <em>another network</em> that predicts them. So during training, we learn the weights of that auxiliary network, which takes an image or a graph as an input and returns weights <em>W⁽ˡ⁾ </em>(Θ in their work) as the output. The idea is based on <strong>Dynamic Filter Networks</strong> (<a href="https://arxiv.org/abs/1605.09673">Brabandere et al., NIPS, 2016</a>), where “dynamic” means that filters <em>W⁽ˡ⁾ </em>will be different depending on the input as opposed to standard models in which filters are fixed (or static) after training.</p> <figure><img alt="" src="https://cdn-images-1.medium.com/max/779/1*0v1xygb2cN-3do55tAh1eg.png"/><figcaption>Using an auxiliary “filter generating network” Fˡ to predict edge-specific weights Θ for the main network. Xˡ⁻¹ are input node features and Xˡ are output features. The figure shows a single iteration of “dynamic convolution” for node 1 (in yellow). Standard GNNs typically would simply average (or sum) features of node 1 neighbors (nodes 2, 3, 4, 5) , which would correspond to having an isotropic filter (Θ would be a constant vector). In contrast, this model has anisotropic filters, because it predicts different edge values between node 1 and all it’s neighbors based on edge labels L, so that features Xˡ(1) are computed as a weighted average of neighbors’ features. Figure from (<a href="https://arxiv.org/abs/1704.02901">Simonovsky &amp; Komodakis, CVPR, 2017</a>).</figcaption></figure> <p>This is a very general form of convolution that, besides images, can be easily applied to graphs or point clouds as they did in their CVPR paper and got excellent results. However, there is no “<a href="https://en.wikipedia.org/wiki/No_free_lunch_theorem">free lunch</a>”, and training such models is quite challenging, because the regular grid constraint is now relaxed and the scope of solutions increases dramatically. This is especially true for larger graphs with many edges or for convolution in deeper layers, which often have hundreds of channels (number of features, <em>C)</em>, so you might end up generating thousands of numbers in total for each input! In this regard, standard ConvNets are so good, because we don’t waste the model’s capacity on training to predict these weights, instead we directly enforce that the filters should be the same for all inputs. But this prior makes ConvNets limited and we cannot directly apply them to graphs or point clouds. So, as always, there’s some trade-off between flexibility and performance in a particular task.</p> <blockquote>When applied to images, like MNIST, the Edge-conditioned model can learn to predict <em>anisotropic</em> filters — filters that are sensitive to orientation, such as edge detectors. Compared to Gaussian filters discussed in <a href="https://medium.com/p/3d9fada3b80d"><em>the first part of my tutorial</em></a>, these filters are able to better capture certain patterns in images, such as strokes in digits.</blockquote> <figure><img alt="" src="https://cdn-images-1.medium.com/max/519/1*aApSWc8LXLpEwvO312yuUg.png"/><figcaption>Convolutional filters learned on MNIST sampled in low (left) and high (right) resolutions. Figure from (<a href="https://arxiv.org/abs/1704.02901">Simonovsky &amp; Komodakis, CVPR, 2017</a>).</figcaption></figure> <p>I want to highlight one more time that whenever we have a complicated model with auxiliary networks, it becomes a chicken-or-the-egg problem in some sense. To solve it, one of the networks — the auxiliary or the main one — should receive a very strong signal, so that it can implicitly supervise another network. In our <a href="https://arxiv.org/abs/1907.09000">BMVC paper</a>, which is similar to <a href="https://arxiv.org/abs/1704.02901">Simonovsky &amp; Komodakis</a>’s work, we apply additional constraints on the edge-generating network to facilitate training. I will describe our work in detail in later posts.</p> <h3><strong>5. MoNet</strong></h3> <p><a href="https://arxiv.org/abs/1611.08402">Monti et al., CVPR, 2017</a></p> <p>MoNet is different from other works discussed in this post, as it assumes to have the notion of node coordinates, and therefore is more suited for geometric tasks such as 3D mesh analysis or image/video reasoning. It is somewhat similar to edge-conditioned filters of <a href="https://arxiv.org/abs/1704.02901">Simonovsky &amp; Komodakis</a>, since they also introduce an auxiliary learnable function 𝐷(𝑤, 𝜃<em>, ρ</em>) that predicts weights. The difference is that these weights depend on the node polar coordinates (angle 𝜃 and radius <em>ρ</em>); and trainable parameters 𝑤 of that function are constrained to be means and variances of Gaussians, so that instead of learning <em>N</em>×<em>N</em> matrices, we only learn fixed-size vectors (means and variances) independently of the graph size <em>N</em>. In terms of standard ConvNets, it would be the same as learning only 2 values (the mean and variance of a Gaussian) for each filter instead of learning 9, 25 or 121 values for 3×3, 5×5 or 11×11 dimensional filters respectively. This <em>parameterization</em> would greatly reduce the number of parameters in a ConvNet, but the filters would be very limited in their power to capture image features.</p> <p><a href="https://arxiv.org/abs/1611.08402">Monti et al.</a> train 𝐽 means and variances of Gaussians and the process of transforming node coordinates is similar to fitting them into a <a href="https://scikit-learn.org/stable/modules/mixture.html">Gaussian Mixture Model</a>. The model is quite computationally intensive to train if we want our filters to be global enough, but it can be a good choice for visual tasks (see our <a href="https://arxiv.org/abs/1907.09000">BMVC paper</a> for comparison), yet it is often worse than simple GCN on non-visual tasks (<a href="https://arxiv.org/abs/1811.09595">Knyazev et al., NeurIPS-W, 2018</a>). Since function <em>D</em> depends on coordinates, the generated filters are also anisotropic and have a shape of oriented and elongated Gaussians as illustrated below.</p> <figure><img alt="" src="https://cdn-images-1.medium.com/max/288/1*TM_3zmnc4esqwqIdfgTvvA.png"/><figcaption>Filters trained with MoNet in polar coordinates 𝜃 and <em>ρ</em>. Each ellipse corresponds to a slice of a Gaussian at some fixed level. The idea is that if the coordinates of the i-th node are close to the middle of the j-th Gaussian, then the generated weight at index (i,j) will have a value close to 1.</figcaption></figure> <pre><strong><em>Pseudo-code of the MoNet layer using PyTorch</em></strong></pre> <pre><strong># assume X to be input <em>N</em></strong>×<strong><em>C</em> node features</strong><br /><strong># coord are <em>N</em>×<em>N</em>×<em>2</em> node coordinate differences between all pairs of nodes (node degrees for non-geometric tasks)<br /># coord can be viewed as angular and radial edges between nodes</strong></pre> <pre>1. Generate <em>J</em> Gaussian-shaped filters based on coordinates of nodes    using some trainable function D<br />   weights = D(coord)  # weights: <em>J</em>×<em>N</em>×<em>N</em><br />2. Multiply node features X by these weights<br />   X = torch.bmm(weights, X.expand(J, N, C))  # X: <em>J</em>×<em>N</em>×<em>C</em><br />3. Project features by a learnable linear transformation<br />   X = fc(X.permute(1, 2, 0).view(N, J*C))  # X: <em>N</em>×<em>F<br /></em>4. Feed X to the next layer</pre> <h3>Conclusion</h3> <p>Despite a lengthy discussion, we have only scratched the surface. Applications of graph neural networks are expanding far beyond typical graph reasoning tasks, like molecule classification. The number of different graph neural layers is increasing very quickly, similar to how it was for convolutional networks a few years ago, so it’s hard to keep track of them. On that note, <a href="https://github.com/rusty1s/pytorch_geometric">PyTorch Geometric (PyG)</a> — a nice toolbox to learn from graphs — frequently populates its collection with novel layers and tricks.</p> <p><em>Acknowledgement: A large portion of this tutorial was prepared during my internship at SRI International under the supervision of </em><a href="https://medium.com/u/6cf41cb2c546"><em>Mohamed Amer</em></a><em> (</em><a href="https://mohamedramer.com/"><em>homepage</em></a><em>) and my PhD advisor Graham Taylor (</em><a href="https://www.gwtaylor.ca/"><em>homepage</em></a><em>). I also thank </em><a href="https://www.linkedin.com/in/carolynaugusta/"><em>Carolyn Augusta</em></a><em> for useful feedback.</em></p> <p>Find me on <a href="https://github.com/bknyaz/">Github</a>, <a href="https://www.linkedin.com/in/boris-knyazev-39690948/">LinkedIn</a> and <a href="https://twitter.com/BorisAKnyazev">Twitter</a>. <a href="https://bknyaz.github.io/">My homepage</a>.</p> <p>If you want to cite this blog post in your paper, please use:<br/><a href="http://twitter.com/misc"><em>@misc</em></a><em>{knyazev2019tutorial,<br/> title={Tutorial on Graph Neural Networks for Computer Vision and Beyond},<br/> author={Knyazev, Boris and Taylor, Graham W and Amer, Mohamed R},<br/> year={2019}<br/>}</em></p> <p><img src="https://medium.com/_/stat?event=post.clientViewed&amp;referrerSource=full_rss&amp;postId=be6d71d70f49" width="1" height="1" alt=""/>&lt;hr&gt;&lt;p&gt;<a href="https://medium.com/data-science/tutorial-on-graph-neural-networks-for-computer-vision-and-beyond-part-2-be6d71d70f49">Anisotropic, Dynamic, Spectral and Multiscale Filters Defined on Graphs</a> was originally published in <a href="https://medium.com/data-science">TDS Archive</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.&lt;/p&gt;</p>]]></content><author><name></name></author><category term="medium"/></entry><entry><title type="html">Tutorial on Graph Neural Networks for Computer Vision and Beyond (Part 1)</title><link href="https://bknyaz.github.io//blog/2019/tutorial-on-graph-neural-networks-for-computer-vision-and-beyond-part-1/" rel="alternate" type="text/html" title="Tutorial on Graph Neural Networks for Computer Vision and Beyond (Part 1)"/><published>2019-08-04T00:06:21+00:00</published><updated>2019-08-04T00:06:21+00:00</updated><id>https://bknyaz.github.io//blog/2019/tutorial-on-graph-neural-networks-for-computer-vision-and-beyond-part-1</id><content type="html" xml:base="https://bknyaz.github.io//blog/2019/tutorial-on-graph-neural-networks-for-computer-vision-and-beyond-part-1/"><![CDATA[<h3>Tutorial on Graph Neural Networks for Computer Vision and Beyond</h3> <p><em>I’m answering questions that AI/ML/CV people not familiar with graphs or graph neural networks typically ask. I provide PyTorch examples to clarify the idea behind this relatively new and exciting kind of model.</em></p> <figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6AB8X6dumCaBBYQZ6vfRuw.png"/><figcaption>A figure from (<a href="https://arxiv.org/abs/1312.6203">Bruna et al., ICLR, 2014</a>) depicting an MNIST image on the 3D sphere. While it’s hard to adapt <a href="https://arxiv.org/abs/1801.10130">Convolutional Networks to classify spherical data</a>, Graph Networks can naturally handle it. This is a toy example, but similar tasks arise in many real applications.</figcaption></figure> <p>The questions addressed in this part of my tutorial are:</p> <ol><li><strong>Why are graphs useful?</strong></li><li><strong>Why is it difficult to define convolution on graphs?</strong></li><li><strong>What makes a neural network a graph neural network?</strong></li></ol> <p>To answer them, I’ll provide motivating examples, papers and Python code making it a tutorial on Graph Neural Networks (GNNs). Some basic knowledge of machine learning and computer vision is expected, however, I’ll provide some background and intuitive explanation as we go.</p> <p>First of all, let’s briefly recall what is a <a href="https://en.wikipedia.org/wiki/Graph_(abstract_data_type)">graph</a>? A graph <em>G</em> is a set of nodes (vertices) connected by directed/undirected edges. Nodes and edges typically come from some expert knowledge or intuition about the problem. So, it can be atoms in molecules, users in a social network, cities in a transportation system, players in team sport, neurons in the brain, interacting objects in a dynamic physical system, pixels, bounding boxes or segmentation masks in images. In other words, in many practical cases, it is actually <strong>you</strong> who gets to decide what are the nodes and edges in a graph.</p> <blockquote>In many practical cases, it is actually you who gets to decide what are the nodes and edges in a graph.</blockquote> <p>This is a very flexible data structure that generalizes many other data structures. For example, if there are no edges, then it becomes a <a href="https://en.wikipedia.org/wiki/Set_(abstract_data_type)">set</a>; if there are only “vertical” edges and any two nodes are connected by exactly one path, then we have a <a href="https://en.wikipedia.org/wiki/Tree_(graph_theory)">tree</a>. Such flexibility is both good and bad as I’ll discuss in this tutorial.</p> <figure><img alt="" src="https://cdn-images-1.medium.com/max/754/1*68Gcr70UTpdaX7THbZSEcA.png"/><figcaption>Two undirected graphs with 5 and 6 nodes. The order of nodes is arbitrary.</figcaption></figure> <h3>1. Why graphs can be useful?</h3> <p>In the context of computer vision (CV) and machine learning (ML), studying graphs and the models to learn from them can give us at least four benefits:</p> <ol><li>We can become closer to solving important problems that previously were too challenging, such as: drug discovery for cancer (<a href="https://www.nature.com/articles/s41598-019-45349-y">Veselkov et al., Nature, 2019</a>); better understanding of the human brain connectome (<a href="https://www.nature.com/articles/s41467-018-06346-3">Diez &amp; Sepulcre, Nature Communications, 2019</a>); materials discovery for energy and environmental challenges (<a href="https://www.nature.com/articles/s41467-019-10663-6">Xie et al., Nature Communications, 2019</a>).</li><li>In most CV/ML applications, data can be actually viewed as graphs even though you used to represent them as another data structure. Representing your data as graph(s) gives you a lot of flexibility and can give you a very different and interesting perspective on your problem. For instance, instead of learning from image pixels you can learn from “<a href="https://scikit-image.org/docs/dev/api/skimage.segmentation.html#skimage.segmentation.slic">superpixels</a>” as in (<a href="https://arxiv.org/abs/1603.07063">Liang et al., ECCV, 2016</a>) and in our forthcoming <a href="https://arxiv.org/abs/1907.09000">BMVC paper</a>. Graphs also let you impose a relational inductive bias in data — some prior knowledge you have about the problem. For instance, if you want to reason about a human pose, your relational bias can be a graph of skeleton joints of a human body (<a href="https://arxiv.org/abs/1801.07455">Yan et al., AAAI, 2018</a>); or if you want to reason about videos, your relational bias can be a graph of moving bounding boxes (<a href="https://arxiv.org/abs/1806.01810">Wang &amp; Gupta, ECCV, 2018</a>). Another example can be representing facial landmarks as a graph (<a href="https://www.cv-foundation.org/openaccess/content_cvpr_2015/html/Antonakos_Active_Pictorial_Structures_2015_CVPR_paper.html">Antonakos et al., CVPR, 2015</a>) to make reasoning about <a href="http://mmlab.ie.cuhk.edu.hk/projects/CelebA.html">facial attributes</a> and identity.</li><li><a href="https://arxiv.org/abs/2403.12143">Your favourite neural network itself can be viewed as a graph, where nodes are neurons and edges are weights</a>, or <a href="https://arxiv.org/abs/2303.04143">where nodes are layers and edges denote flow of forward/backward pass</a> (in which case we are talking about a computational graph used in TensorFlow, PyTorch and other DL frameworks). An application can be optimization of a computational graph, <a href="https://arxiv.org/abs/2110.13100">neural architecture search</a>, <a href="https://ai.googleblog.com/2018/06/how-can-neural-network-similarity-help.html">analyzing training behavior</a>, <a href="https://arxiv.org/abs/2409.04434">accelerating training</a>, etc.</li><li>Finally, you can solve many problems, where data can be more naturally represented as graphs, <em>more effectively</em>. This includes, but is not limited to, molecule and social network classification (<a href="https://arxiv.org/abs/1811.09595">Knyazev et al., NeurIPS-W, 2018</a>) and generation (<a href="https://arxiv.org/abs/1802.03480">Simonovsky &amp; Komodakis, ICANN, 2018</a>), 3D Mesh classification and correspondence (<a href="https://arxiv.org/abs/1711.08920">Fey et al., CVPR, 2018</a>) and generation (<a href="https://arxiv.org/abs/1804.01654">Wang et al., ECCV, 2018</a>), modeling behavior of dynamic interacting objects (<a href="https://arxiv.org/abs/1802.04687">Kipf et al., ICML, 2018</a>), visual scene graph modeling (see the upcoming <a href="https://cs.stanford.edu/people/ranjaykrishna/sgrl/index.html">ICCV Workshop</a>) and question answering (<a href="https://arxiv.org/abs/1811.00538">Narasimhan, NeurIPS, 2018</a>), program synthesis (<a href="https://arxiv.org/abs/1711.00740">Allamanis et al., ICLR, 2018</a>), different reinforcement learning tasks (<a href="https://arxiv.org/abs/1904.03177">Bapst et al., ICML, 2019</a>) and many other exciting problems.</li></ol> <p>As my previous research was related to recognizing and analyzing faces and emotions, I particularly like this figure below.</p> <figure><img alt="" src="https://cdn-images-1.medium.com/max/607/1*yBuAXZIqq_7VknWEZdyUNQ.png"/><figcaption>A figure from (<a href="https://www.cv-foundation.org/openaccess/content_cvpr_2015/html/Antonakos_Active_Pictorial_Structures_2015_CVPR_paper.html">Antonakos et al., CVPR, 2015</a>) showing representation of a face as a graph of landmarks. This is an interesting approach, but it is not a sufficient facial representation in many cases, since a lot can be told from the face texture captured well by convolutional networks. In contrast, reasoning over 3D meshes of a face looks like a more sensible approach compared to 2D landmarks (<a href="https://arxiv.org/abs/1807.10267">Ranjan et al., ECCV, 2018</a>).</figcaption></figure> <h3>2. Why is it difficult to define convolution on graphs?</h3> <p>To answer this question, I first give some motivation for using convolution in general and then describe “convolution on images” using the graph terminology which should make the transition to “convolution on graphs” more smooth.</p> <h4>2.1. Why is convolution useful?</h4> <p>Let’s understand why we care about convolution so much and why we want to use it for graphs. Compared to fully-connected neural networks (a.k.a. NNs or MLPs), convolutional networks (a.k.a. CNNs or ConvNets) have certain advantages explained below based on the image of a nice old Chevy.</p> <figure><img alt="" src="https://cdn-images-1.medium.com/max/500/1*Cf5Wwx1-Z7gQHUq4EOgQSg.jpeg"/><figcaption>“Chevrolet Vega” according to Google Image Search.</figcaption></figure> <p><strong>First</strong>, ConvNets exploit a natural prior in images, more formally described in (<a href="https://arxiv.org/abs/1611.08097">Bronstein et al., 2016</a>), such as:</p> <ol><li>Shift-invariance — if we translate the car on the image above to the left/right/up/down, we still should be able to detect and recognize it as a car. This is exploited by sharing filters across all locations, i.e. applying convolution.</li><li>Locality — nearby pixels are closely related and often represent some semantic concept, such as a wheel or a window. This is exploited by using relatively large filters, which can capture image features in a local spatial neighborhood.</li><li>Compositionality (or hierarchy)— a larger region in the image is often a semantic parent of smaller regions it contains. For example, a car is a parent of doors, windows, wheels, driver, etc. And a driver is a parent of head, arms, etc. This is implicitly exploited by stacking convolutional layers and applying pooling.</li></ol> <p><strong>Second</strong>, the number of trainable parameters (i.e. filters) in convolutional layers does not depend on the input dimensionality, so technically we can train exactly the same model on 28×28 and 512×512 images. In other words, the model is <em>parametric</em>.</p> <blockquote>Ideally, our goal is to develop a model that is as flexible as Graph Neural Nets and can digest and learn from any data, but at the same time we want to control (regularize) factors of this flexibility by turning on/off certain priors.</blockquote> <p>All these nice properties make ConvNets less prone to overfitting (high accuracy on the training set and low accuracy on the validation/test set), more accurate in different visual tasks, and easily scalable to large images and datasets. So, when we want to solve important tasks where input data are graph-structured, it is appealing to transfer all these properties to <strong>graph neural networks (GNNs) </strong>to regularize their flexibility and make them scalable. Ideally, our goal is to develop a model that is as flexible as GNNs and can digest and learn from any data, but at the same time we want to control (regularize) factors of this flexibility by turning on/off certain priors. This can open research in many interesting directions. However, controlling of this trade-off is challenging.</p> <h4>2.2. Convolution on images in terms of graphs</h4> <p>Let’s consider an undirected graph <em>G</em> with <em>N</em> nodes. Edges <em>E</em> represent undirected connections between nodes. Nodes and edges typically come from your intuition about the problem. Our intuition in the case of images is that nodes are pixels or <a href="https://scikit-image.org/docs/dev/api/skimage.segmentation.html#skimage.segmentation.slic">superpixels</a> (a group of pixels of weird shape) and edges are spatial distances between them. For example, the <a href="https://arxiv.org/abs/1905.10498">MNIST</a> image below on the left is typically represented as an 28×28 dimensional matrix. We can also represent it as a set of <em>N</em>=28*28=784 pixels. So, our graph <em>G</em> is going to have <em>N</em>=784 nodes and edges will have large values (thicker edges in the Figure below) for closely located pixels and small values (thinner edges) for remote pixels.</p> <figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Kji3yJN0cT6RwO11h0Mh6A.png"/><figcaption>An image from the MNIST dataset on the left and an example of its graph representation on the right. Darker and larger nodes on the right correspond to higher pixel intensities. The figure on the right is inspired by Figure 5 in (<a href="https://arxiv.org/abs/1711.08920">Fey et al., CVPR, 2018</a>)</figcaption></figure> <p>When we train our neural networks or ConvNets on images, we implicitly define images on a graph — a <em>regular</em> two-dimensional grid as the one on the figure below. Since this grid is the same for all training and test images and is <em>regular</em>, i.e. all pixels of the grid are connected to each other in exactly the same way across all images (i.e. have the same number of neighbors, length of edges, etc.), this regular grid graph has no information that will help us to tell one image from another. Below I visualize some 2D and 3D regular grids, where the order of nodes is color-coded. By the way, I’m using <a href="https://networkx.github.io/">NetworkX</a> in Python to do that, e.g. G = networkx.grid_graph([4, 4]).</p> <figure><img alt="" src="https://cdn-images-1.medium.com/max/880/0*XlNlsS2iG45j-adq"/><figcaption>Examples of regular 2D and 3D grids. Images are defined on 2D grids and videos are on 3D grids.</figcaption></figure> <p>Given this 4×4 regular grid, let’s briefly look at how 2D convolution works to understand why it’s difficult to transfer this operator to graphs. A filter on a regular grid has the same order of nodes, but modern convolutional nets typically have small filters, such as 3×3 in the example below. This filter has 9 values: <em>W</em>₁,<em>W</em>₂,…, <em>W</em>₉, which is what we are updating during training using backprop to minimize the loss and solve the downstream task. In our example below, we just heuristically initialize this filter to be an edge detector (see other possible filters <a href="https://en.wikipedia.org/wiki/Kernel_(image_processing)">here</a>):</p> <figure><img alt="" src="https://cdn-images-1.medium.com/max/576/1*Y2xcReChWy3JYGSw0_Rt0Q.png"/><figcaption>Example of a 3×3 filter on a regular 2D grid with arbitrary weights w on the left and an edge detector on the right.</figcaption></figure> <p>When we perform convolution, we slide this filter in both directions: to the right and to the bottom, but nothing prevents us from starting in the bottom corner — the important thing is to slide over all possible locations. At each location, we compute the <a href="https://en.wikipedia.org/wiki/Dot_product"><em>dot product</em></a><em> </em>between the values on the grid (let’s denote them as <em>X</em>) and the values of filters, <em>W</em>: <em>X</em>₁<em>W</em>₁+<em>X</em>₂<em>W</em>₂+…+<em>X</em>₉<em>W</em>₉, and store the result in the output image. In our visualization, we change the color of nodes during sliding to match the colors of nodes in the grid. In a regular grid, we always can match a node of the filter with a node of the grid. Unfortunately, this is not true for graphs as I’ll explain later below.</p> <figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*VxofpMT7GwLKZGVGWc2XHQ.png"/><figcaption>2 steps of 2D convolution on a regular grid. If we don’t apply padding, there will be 4 steps in total, so the result will be a 2×2 image. To make the resulting image larger, we need to apply <a href="https://deeplizard.com/learn/video/qSTv_m-KFk0">padding</a>. See a comprehensive guide to convolution in deep learning <a href="https://arxiv.org/abs/1603.07285">here</a>.</figcaption></figure> <p>The dot product used above is one of so called “aggregator operators”. Broadly speaking, the goal of an aggregator operator is to summarize data to a reduced form. In our example above, the dot product summarizes a 3×3 matrix to a <em>single</em> value. Another example is pooling in ConvNets. Keep in mind, that such methods as max or sum pooling are <em>permutation-invariant</em>, i.e. they will pool the same value from a spatial region even if you randomly shuffle all pixels inside that region. To make it clear, the dot product is <em>not</em> permutation-invariant simply because in general: <em>X</em>₁<em>W</em>₁+<em>X</em>₂<em>W</em>₂ ≠<em>X</em>₂<em>W</em>₁+<em>X</em>₁<em>W</em>₂.</p> <p>Now let’s use our MNIST image and illustrate the meaning of a regular grid, a filter and convolution. Keeping in mind our graph terminology, this regular 28×28 grid will be our graph <em>G</em>, so that every cell in this grid is a node, and node features are an actual image <em>X</em>, i.e. every node will have just a single feature — pixel intensity from 0 (black) to 1 (white).</p> <figure><img alt="" src="https://cdn-images-1.medium.com/max/720/1*8nWUYV-nIuwL1bGCw3TnEw.png"/><figcaption>Regular 28×28 grid (left) and an image on that grid (right).</figcaption></figure> <p>Next, we define a filter and let it be a famous <a href="https://en.wikipedia.org/wiki/Gabor_filter">Gabor filter</a> with some (almost) arbitrary parameters. Once we have an image and a filter, we can perform <a href="https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.convolve2d.html">convolution</a> by sliding the filter over that image (of digit 7 in our case) and putting the result of the dot product to the output matrix after each step.</p> <figure><img alt="" src="https://cdn-images-1.medium.com/max/720/1*9pzcalk9Lu4e-jqQHgcIUg.png"/><figcaption>A 28×28 filter (left) and the result of 2D convolution of this filter with the image of digit 7 (right).</figcaption></figure> <p>This is all cool, but as I mentioned before, it becomes tricky when you try to generalize convolution to graphs.</p> <blockquote>Nodes are a set, and any permutation of this set does not change it. Therefore, the <em>aggregator</em> operator that people apply should be <em>permutation-invariant</em>.</blockquote> <p>As I have already mentioned, the dot product used above to compute convolution at each step is <em>sensitive</em> to the order. This sensitivity permits us to learn edge detectors similar to Gabor filters important to capture image features. The problem is that in graphs <em>there is no well-defined order of nodes</em> unless you learn to order them, or come up with some heuristic that will result in a consistent (canonical) order from graph to graph. In short, nodes are a set, and any permutation of this set does not change it. Therefore, the <em>aggregator</em> operator that people apply should be <em>permutation-invariant</em>. The most popular choices are averaging (GCN, <a href="https://arxiv.org/abs/1609.02907">Kipf &amp; Welling, ICLR, 2017</a>) and summation (GIN, <a href="https://arxiv.org/abs/1810.00826">Xu et al., ICLR, 2019</a>) of <strong>all</strong> neighbors, i.e. sum or mean pooling, followed by projection by a trainable vector <em>W</em>. See <a href="https://arxiv.org/abs/1706.02216">Hamilton et al., NIPS, 2017</a> for some other aggregators.</p> <figure><img alt="" src="https://cdn-images-1.medium.com/max/746/1*r91KCqXWXm3ltrixv_kUcA.png"/><figcaption>Illustration of “convolution on graphs” of node features <em>X with </em>filter <em>W</em> centered at node 1 (dark blue).</figcaption></figure> <p>For example, for the graph above on the left, the output of the summation aggregator for node 1 will be <em>X</em>₁=(<em>X</em>₁+<em>X</em>₂+<em>X</em>₃+<em>X</em>₄)<em>W</em>₁, for node 2: <em>X</em>₂=(<em>X</em>₁+<em>X</em>₂+<em>X</em>₃+<em>X</em>₅)<em>W</em>₁ and so forth for nodes 3, 4 and 5, i.e. we need to apply this aggregator for all nodes. In result, we will have the graph with the same structure, but node features will now contain features of neighbors. We can process the graph on the right using the same idea.</p> <p>Colloquially, people call this averaging or summation “convolution”, since we also “slide” from one node to another and apply an aggregator operator in each step. However, it’s important to keep in mind that this is a very specific form of convolution, where filters don’t have a sense of orientation. Below I’ll show how those filters look like and give an idea how to make them better.</p> <h3>3. What makes a neural network a graph neural network?</h3> <p>You know how a classical neural network works, right? We have some <em>C</em>-dimensional features <em>X</em> as the input to the net. Using our running MNIST example, <em>X</em><strong> </strong>will be our <em>C</em>=784 dimensional pixel features (i.e. a “flattened” image). These features get multiplied by <em>C</em>×<em>F </em>dimensional weights <em>W</em> that we update during training to get the output closer to what we expect<em>. </em>The result can be directly used to solve the task (e.g. in case of regression) or can be further fed to some nonlinearity (activation), like ReLU, or other differentiable (or more precisely, sub-differentiable) functions to form a multi-layer network. In general, the output of some layer <em>l</em> is:</p> <figure><img alt="" src="https://cdn-images-1.medium.com/max/741/1*PK-poDpkwSxcifQcO7Lidw.png"/><figcaption>Fully-connected layer with learnable weights W. “Fully-connected” means that each output value in X<em>⁽ˡ⁺¹⁾</em> depends on, or “connected to”, all inputs X<em>⁽ˡ⁾</em>. Typically, although not always, we add a bias term to the output.</figcaption></figure> <p>The signal in MNIST is so strong, that you can get an accuracy of 91% by just using the formula above and the Cross Entropy loss without any nonlinearities and other tricks (I used a slightly modified <a href="https://github.com/bknyaz/examples/blob/master/fc_vs_graph_train.py">PyTorch example</a> to do that). Such model is called multinomial (or multiclass, since we have 10 classes of digits) logistic regression.</p> <p>Now, how do we transform our vanilla neural network to a graph neural network? As you already know, the core idea behind GNNs is aggregation over “neighbors”. Here, it is important to understand that in many cases, it is actually <strong>you</strong> who specifies “neighbors”.</p> <p>Let’s consider a simple case first, when you are given some graph. For example, this can be a fragment (subgraph) of a social network with 5 persons and an edge between a pair of nodes denotes if two people are friends (or at least one of them think so). An <a href="https://en.wikipedia.org/wiki/Adjacency_matrix">adjacency matrix</a> (usually denoted as <em>A</em>) in the figure below on the right is a way to represent these edges in a matrix form, convenient for our deep learning frameworks. Yellow cells in the matrix represent the edge and blue — the absence of the edge.</p> <figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7cmkI4y_CYsF-sj0qIJrEA.png"/><figcaption>Example of a graph and its adjacency matrix. The order of nodes we defined in both cases is random, while the graph is still the same.</figcaption></figure> <p>Now, let’s create an adjacency matrix <em>A</em> for our MNIST example based on coordinates of pixels (complete code is provided in the end of the post):</p> <pre><em>import numpy as np<br />from scipy.spatial.distance import cdist</em></pre> <pre>img_size = 28  <strong># MNIST image width and height</strong><br />col, row = np.meshgrid(np.arange(img_size), np.arange(img_size))<br />coord = np.stack((col, row), axis=2).reshape(-1, 2) / img_size<br />dist = cdist(coord, coord)  <strong># see figure below on the left</strong><br />sigma = 0.2 * np.pi  <strong># width of a Gaussian</strong><br />A = np.exp(- dist ** 2 / sigma ** 2)  <strong># see figure below in the middle</strong></pre> <p>This is a typical, but not the only, way to define an adjacency matrix for visual tasks (<a href="https://arxiv.org/abs/1606.09375">Defferrard et al., NIPS, 2016</a>, <a href="https://arxiv.org/abs/1611.08097">Bronstein et al., 2016</a>). This adjacency matrix is our prior, or our inductive bias, we impose on the model based on our intuition that nearby pixels should be connected and remote pixels shouldn’t or should have very thin edge (edge of a small value). This is motivated by observations that in natural images nearby pixels often correspond to the same object or objects that interact frequently (the locality principle we mentioned in Section 2.1.), so it makes a lot of sense to connect such pixels.</p> <figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*FD55QIOgm7CRGMG565KucQ.png"/><figcaption>Adjacency matrix (<em>N</em>x<em>N) in the form of distances (left) and closeness (middle) between all pairs of nodes. (right) A subgraph with 16 neighboring pixels corresponding to the adjacency matrix in the middle. Since it’s a complete subgraph, it’s also called a “clique”.</em></figcaption></figure> <p>So, now instead of having just features <em>X</em> we have some fancy matrix <em>A</em> with values in the range [0,1]. It’s important to note that once we know that our input is a graph, we assume that there is no canonical order of nodes that will be consistent across all other graphs in the dataset. In terms of images, it means that <em>pixels are assumed to be randomly shuffled</em>. Finding the canonical order of nodes is combinatorially unsolvable in practice. Even though for MNIST we technically can cheat by knowing this order (because data are originally from a regular grid), it’s not going to work on actual graph datasets.</p> <p>Remember that our matrix of features <em>X</em> has 𝑁 rows and C columns. So, in terms of graphs, each row corresponds to one node and <em>C</em> is the dimensionality of node features. But now the problem is that we don’t know the order of nodes, so we don’t know in which row to put features of a particular node. If we just pretend to ignore this problem and feed <em>X</em> directly to an MLP as we did before, the effect will be the same as feeding images with randomly shuffled pixels with <em>independent</em> (yet the same for each epoch) shuffling for each image! Surprisingly, a neural network can in principle still fit such random data (<a href="https://arxiv.org/abs/1611.03530">Zhang et al., ICLR, 2017</a>), however test performance will be close to random prediction. One of the solutions is to simply use the adjacency matrix <em>A,</em> we created before, in the following way:</p> <figure><img alt="" src="https://cdn-images-1.medium.com/max/741/1*Ht9tBXaTrV2gkbMIe7zxMg.png"/><figcaption>Graph neural layer with adjacency matrix A, input/output features X and learnable weights W.</figcaption></figure> <p>We just need to make sure that row <em>i</em> in <em>A</em> corresponds to features of node in row <em>i</em> of <em>X</em>. Here, I’m using 𝓐 instead of plain <em>A</em>, because often you want to normalize <em>A</em>. If 𝓐=<em>A</em>, the matrix multiplication 𝓐<em>X⁽ˡ⁾ </em>will be equivalent to summing features of neighbors, which turned out to be useful in many tasks (<a href="https://arxiv.org/abs/1810.00826">Xu et al., ICLR, 2019</a>). Most commonly, you normalize it so that 𝓐<em>X⁽ˡ⁾ </em>averages features of neighbors, i.e. 𝓐=<em>A</em><strong>/</strong>Σᵢ<em>A</em>ᵢ. A better way to normalize matrix <em>A</em> can be found in (<a href="https://arxiv.org/abs/1609.02907">Kipf &amp; Welling, ICLR, 2017</a>).</p> <p>Below is the comparison of NN and GNN in terms of PyTorch code:</p> <iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/025a3cd1ab937d3f9fc09f062c0ed5a0/href">https://medium.com/media/025a3cd1ab937d3f9fc09f062c0ed5a0/href</a></iframe> <p>And <a href="https://github.com/bknyaz/examples/blob/master/fc_vs_graph_train.py">HERE</a> is the full PyTorch code to train two models above: python mnist_fc.py --model fc to train the NN case; python mnist_fc.py --model graph to train the GNN case. As an exercise, try to randomly shuffle pixels in code in the --model graph case (don’t forget to shuffle <em>A</em> in the same way) and make sure that it will not affect the result. Is it going to be true for the --model fc case?</p> <blockquote><a href="https://github.com/bknyaz/examples/blob/master/fc_vs_graph_train.py">Here</a> is the full PyTorch code to train two models.</blockquote> <p>After running the code, you may notice that the classification accuracy is actually about the same. What’s the problem? Aren’t graph networks supposed to work better? Well, they are, in many cases. But not in this one, because the 𝓐<em>X⁽ˡ⁾ </em>operator we added is actually nothing else, but a Gaussian filter:</p> <figure><img alt="" src="https://cdn-images-1.medium.com/max/720/1*njbAaq3jLybNb7kV5x7big.png"/><figcaption>2D visualization of a filter used in a graph neural network and it’s effect on the image.</figcaption></figure> <p>So, our graph neural network turned out to be equivalent to a convolutional neural network with a single Gaussian filter, that we never update during training, followed by the fully-connected layer. This filter basically blurs/smooths the image, which is not a particularly useful thing to do (see the image above on the right). However, this is the simplest variant of a graph neural network, which nevertheless works great on graph-structured data. To make GNNs work better on regular graphs, like images, we need to apply a bunch of tricks. For example, instead of using a predefined Gaussian filter, we can learn to predict an edge between any pair of pixels by using a differentiable function like this:</p> <pre>import torch.nn as nn  # using PyTorch</pre> <pre>nn.Sequential(nn.Linear(4, 64),  <strong># map coordinates to a hidden layer</strong><br />              nn.ReLU(),         <strong># nonlinearity</strong><br />              nn.Linear(64, 1),  <strong># map hidden representation to edge</strong><br />              nn.Tanh())         <strong># squash edge values to [-1, 1]</strong></pre> <blockquote>To make GNNs work better on regular graphs, like images, we need to apply a bunch of tricks. For example, instead of using a predefined Gaussian filter, we can learn to predict an edge between any pair of pixels.</blockquote> <p>This idea is similar to Dynamic Filter Networks (<a href="https://arxiv.org/abs/1605.09673">Brabander et al., NIPS, 2016</a>), Edge-conditioned Graph Networks (ECC, <a href="https://arxiv.org/abs/1704.02901">Simonovsky &amp; Komodakis, CVPR, 2017</a>) and (<a href="https://arxiv.org/abs/1811.09595">Knyazev et al., NeurIPS-W, 2018</a>). To try it using <a href="https://github.com/bknyaz/examples/blob/master/fc_vs_graph_train.py">my code</a>, you just need to add the --pred_edge flag, so the entire command is python mnist_fc.py --model graph --pred_edge. Below I show the animation of the predefined Gaussian and learned filters. You may notice that the filter we just learned (in the middle) looks weird. That’s because the task is quite complicated since we optimize two models at the same time: the model that predicts edges and the model that predicts a digit class. To learn better filters (like the one on the right), we need to apply some other tricks from our <a href="https://arxiv.org/abs/1907.09000">BMVC paper</a>, which is beyond the scope of this part of the tutorial.</p> <figure><img alt="" src="https://cdn-images-1.medium.com/max/292/1*fmwFNf4MaDL0MrdQEk1xqQ.gif"/><figcaption>2D filter of a graph neural network centered in the red point. Averaging (left, accuracy 92.24%), learned based on coordinates (middle, accuracy 91.05%), learned based on coordinates with some tricks (right, accuracy 92.39%).</figcaption></figure> <p>The code to generate these GIFs is quite simple:</p> <iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/35d81808306e1fff2dbd090b7cf08a5e/href">https://medium.com/media/35d81808306e1fff2dbd090b7cf08a5e/href</a></iframe> <p>I’m also sharing an <a href="https://nbviewer.jupyter.org/github/bknyaz/examples/blob/master/2d_convolution.ipynb">IPython notebook</a> showing 2D convolution of an image with a Gabor filter in terms of graphs (using an adjacency matrix) compared to using <a href="https://en.wikipedia.org/wiki/Circulant_matrix">circulant matrices</a>, which is often used in signal processing.</p> <p>In <a href="https://medium.com/@BorisAKnyazev/tutorial-on-graph-neural-networks-for-computer-vision-and-beyond-part-2-be6d71d70f49">the next part of the tutorial</a>, I’ll tell you about more advanced graph layers that can lead to better filters on graphs.</p> <p><strong>Update:</strong></p> <p>Throught this blog post and in the code the dist variable should have been squared to make it a Gaussian. Thanks <a href="https://medium.com/u/96dddd48cbe9">Alfredo Canziani</a> for spotting that. All figures and results were generated without squaring it. If you observe very different results after squaring it, I suggest to tune sigma.</p> <h3>Conclusion</h3> <p>Graph Neural Networks are a very flexible and interesting family of neural networks that can be applied to really complex data. As always, such flexibility must come at a certain cost. In case of GNNs it is the difficulty of regularizing the model by defining such operators as convolution. Research in that direction is advancing quite fast, so that GNNs will see application in increasingly wider areas of machine learning and computer vision.</p> <p>See another <a href="https://neptune.ai/blog/graph-neural-network-and-some-of-gnn-applications">nice blog post about GNNs</a> from <a href="http://neptune.ai">Neptune.ai</a>.</p> <p><em>Acknowledgement: A large portion of this tutorial was prepared during my internship at SRI International under the supervision of </em><a href="https://medium.com/u/6cf41cb2c546"><em>Mohamed Amer</em></a><em> (</em><a href="https://mohamedramer.com/"><em>homepage</em></a><em>) and my PhD advisor Graham Taylor (</em><a href="https://www.gwtaylor.ca/"><em>homepage</em></a><em>).</em></p> <p>Find me on <a href="https://github.com/bknyaz/">Github</a>, <a href="https://www.linkedin.com/in/boris-knyazev-39690948/">LinkedIn</a> and <a href="https://twitter.com/BorisAKnyazev">Twitter</a>. <a href="https://bknyaz.github.io/">My homepage</a>.</p> <p>If you want to cite this tutorial in your paper, please use:<br/><em>@misc{knyazev2019tutorial,<br/> title={Tutorial on Graph Neural Networks for Computer Vision and Beyond},<br/> author={Knyazev, Boris and Taylor, Graham W and Amer, Mohamed R},<br/> year={2019}<br/>}</em></p> <p><img src="https://medium.com/_/stat?event=post.clientViewed&amp;referrerSource=full_rss&amp;postId=3d9fada3b80d" width="1" height="1" alt=""/></p>]]></content><author><name></name></author><category term="medium"/></entry></feed>