{
  "id": 551492,
  "title": "14th Place Solution",
  "content": "<p>First of all, I would like to thank the organisers for hosting the competition, kaggle team, and my team mates from AIRI, Skoltech and VeinCV<br>\n<a href=\"https://www.kaggle.com/rolf110\" target=\"_blank\">rolf110</a><br>\n<a href=\"https://www.kaggle.com/danyaivanov\" target=\"_blank\">danyaivanov</a><br>\nShout-out to <a href=\"https://www.kaggle.com/pipmos\" target=\"_blank\">pipmos</a>!</p>\n<h2>Summary</h2>\n<h3>Overall Inference Pipeline</h3>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-forum-message-attachments/o/inbox%2F4526980%2Fb29b262492a8e319f0520626f0ae3a35%2Feedi_inference_pipeline.png?generation=1734098354476413&amp;alt=media\" alt=\"Inference Pipeline\"></p>\n<p>Our solution:</p>\n<ul>\n<li><code>Qwen2.5-14B</code> retriever to obtain top-25 misconceptions</li>\n<li><code>Qwen2.5-32B</code> reranker used to score each of the misconceptions received by retriever. We do it iteratively, by passing one misconception at a time to obtain a probability of it being the one that leads to the incorrect answer. </li>\n</ul>\n<p>The whole notebook runs approximately 7 hours. We didn't use any upper-level frameworks such as vllm to run the inference pipeline.</p>\n<p>All models were fine-tuned using the competition data, consisting of 4370 (question, construct, correct answer, incorrect answer) quadruplets, +   13921 synthetic (question, incorrect answer, correct answer, incorrect answer) samples generated using gpt-4o.</p>\n<h2>Description</h2>\n<h3>Synthetic Data</h3>\n<ul>\n<li>For each <code>MisconceptionId</code> in the given database, we generated new (<code>QuestionText</code>, <code>ConstructName</code>, <code>Correct Answer</code>, <code>Incorrect Answer</code>) samples, ensuring at least 7 examples is presented for each <code>MisconceptionId</code> in the final real+synthetic dataset. We then filtered those examples that have a full-match in the <code>QuestionText</code> (approximately 500 samples removed).</li>\n<li>To provide some guidance to the LLM, we passed a reference sample from the train dataset corresponding to the closest misconceptions for the one considered for generation. We used <code>SFR-Embedding-2_R</code> embeddings of <code>MisconceptionName</code> to obtain \"closest\" misconceptions. We ensured that each reference is different between different generations for a particular <code>MisconceptionId</code>.</li>\n<li>To construct the prompt, we followed the ideas from <a href=\"https://arxiv.org/pdf/2403.04706\" target=\"_blank\">here</a>. The template of the <code>prompt</code> we passed to gpt-4o:</li>\n</ul>\n<pre><code>\n</code></pre>\n<p>We manually checked some of the generated examples, and they were pretty good and close to the real data.<br>\nOther staff we found important for generation:</p>\n<ul>\n<li><strong>verification and modification</strong> stage inside the prompt helped to fix some rare errors and obtain good questions and valid answers.</li>\n<li>When specifying a reference, it is important to add \"ensure the student can answer your created question without the given example\" to the prompt. Otherwise in some rare cases the model didn't specify some important information in the generated question because it was stored in the reference and the model probably thought that it will be also given to a student. </li>\n<li>The only thing we couldn't entirely fix is that in rare cases (approx. 3-4% of the data) LLM generated quite a strong hint for the correct misconception inside the description (<code>ConstructName</code>) field; but we found that such cases were also observed in the competition data, so we decided to keep it as is.</li>\n</ul>\n<h3>Validation</h3>\n<p>We used 20% of the competition data for validation, which we additionally split on 2 subsets:</p>\n<ul>\n<li><strong>out-of-distribution (OOD):</strong> no intersection for train/test between <code>MisconceptionId</code> and stratification based on the number of misconceptions per question.</li>\n<li><strong>in-distribution (ID):</strong> no intersection for train/test between <code>QuestionId</code> and stratification based on the number of misconceptions per question.</li>\n</ul>\n<p>We used the <strong>ID</strong> test to account for how the model works on the misconceptions it knows, while the <strong>OOD</strong> test was used to account for how the model generalizes to the unseen misconceptions. However, we mainly tracked the <strong>OOD</strong> data because it was well-aligned with the lb and accounted for the most complex scenario.</p>\n<h3>Retriever</h3>\n<p>To obtain fine-grained hard negatives, we first fine-tuned <code>Qwen2.5-14B</code> on real+synthetic data. Then we repeat the training with the exact same setup, but using the hard-negatives extracted using our trained <code>Qwen2.5-14B</code>. After each epoch, we collect new hard negatives using the latest checkpoint.</p>\n<h4>Training</h4>\n<ul>\n<li>negatives_range &amp; num_negatives: 100 &amp; 8</li>\n<li>qlora config: rank 32, alpha 64</li>\n<li>loss: InfoNCE Loss</li>\n<li>mask_token_probability: 0.1 (applied only to the query)</li>\n<li>lr: 5e-5</li>\n<li>epochs: 3</li>\n</ul>\n<p>We also utilized in-batch negative sampling during training. To mitigate the problem of treating negatives which appear to be the same misconceptions as the positive ones, we applied dynamic masking by setting the similarities of such misconceptions to <code>-inf</code> before calculating the loss for each sample in the batch.</p>\n<h4>Score</h4>\n<table>\n<thead>\n<tr>\n<th>Data</th>\n<th>Recall@25</th>\n<th>MAP@25</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>CV OOD</td>\n<td>0.88</td>\n<td>0.42</td>\n</tr>\n<tr>\n<td>Public LB</td>\n<td>None</td>\n<td>0.438</td>\n</tr>\n<tr>\n<td>Private LB</td>\n<td>None</td>\n<td>0.434</td>\n</tr>\n</tbody>\n</table>\n<h3>Reranker</h3>\n<p>Judging from the experiments provided in the paper <a href=\"https://arxiv.org/abs/2310.02439\" target=\"_blank\">Novice Learner and Expert Tutor</a> and some research done by other participants during the competition, we had come up with the idea to pass misconceptions list as an additional input to better guide the reranker model. </p>\n<p>We didn't want the model to output the misconception prediction as a text because it was not very reliable as we thought, and the multi-classification problem didn't fit here either. Finally, we have decided to solve a binary classification problem for each misconception separately:</p>\n<ol>\n<li>First, we pass the question, construct, correct answer, incorrect answer, and top-25 misconceptions as a list received by retriever through the backbone and store <code>past_key_values</code>. This helps to save a lot of runtime because we only need to compute it once for the given sample.</li>\n<li>Then we form a batch of misconceptions taken from the list, and pass it to the backbone where each element gets the same <code>past_key_values</code> from the previous step as cache. We use a batch size of 5, so 5 iterations is required for the whole list of misconceptions.</li>\n<li>We take the final logits and feed them to the classification head trained from scratch. This head maps from the logits space to a single score indicating whether the misconception leads to the incorrect answer or no.</li>\n<li>We sort the misconceptions by predicted scores.</li>\n</ol>\n<h4>Training Dataset</h4>\n<p>Given the (question, incorrect answer) pair, we create 24 <code>label = 0</code> and 1 <code>label = 1</code> samples for training. The main difference between them is the way we construct the \"list of possible misconceptions\":</p>\n<ol>\n<li>First, we identify the top 50 negative misconceptions using our trained retriever.</li>\n<li>From these top 50 misconceptions, we randomly select 24 negatives as a subset for further sampling.</li>\n<li>For each of the negatives sampled at the 2nd step, we add 28 random negatives, the specific negative being evaluated, and the correct positive misconception tied to the (question, incorrect answer) pair. This results in 24 <code>label = 0</code> samples per pair.</li>\n<li>When the model is queried with a positive misconception (<code>label = 1</code>), we include 29 randomly selected negatives from the top 50 to create a set of 30 misconceptions we pass to the model.</li>\n</ol>\n<p>To mitigate the positional bias caused by the retriever, we randomly shuffle the list for every input sample.</p>\n<p>Example of the input prompt:</p>\n<pre><code>Question: {Question}\nBrief description of the concept  skill being tested  the Question: {ConstructName}\nCorrect Answer: {Correct Answer}\nIncorrect Answer: {Incorrect Answer}\n of possible misconceptions:\n {Misconception_1}\n...\n {Misconception_30}\nMisconception: {n}. {Misconception_n}\nDoes this Misconception lead to the Incorrect Answer? \n</code></pre>\n<p>To account for high imbalance between 0/1 classes, we oversampled positive class 4 times.</p>\n<h4>Training</h4>\n<p>We used <a href=\"https://docs.h2o.ai/h2o-llmstudio/\" target=\"_blank\">h2o-studio</a> framework to train our model.</p>\n<ul>\n<li>loss: cross-entropy loss with 0.05 label smoothing to account for possible errors introduced by synthetic data</li>\n<li>qlora config: rank 16, alpha 32</li>\n<li>lr &amp; head lr: 2e-5 &amp; 1e-5</li>\n<li>epoch: 1</li>\n<li>mask token probability: 0.1</li>\n</ul>\n<h4>Score</h4>\n<table>\n<thead>\n<tr>\n<th>Data</th>\n<th>MAP@25</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>CV OOD</td>\n<td>0.571</td>\n</tr>\n<tr>\n<td>Public LB</td>\n<td>0.589</td>\n</tr>\n<tr>\n<td>Private LB</td>\n<td>0.558</td>\n</tr>\n</tbody>\n</table>\n<h3>What did not work</h3>\n<ul>\n<li>Re-write positive misconceptions with gpt4o-mini to improve oversampling procedure</li>\n<li>Add other incorrect answer choices to the prompt</li>\n</ul>\n<h3>Public Data</h3>\n<p>reranker adapter and classification head weights: <a href=\"https://www.kaggle.com/models/andreygalichin/qwen2.5-32b-r32-a16-synth-merge-smooth-full/\" target=\"_blank\">https://www.kaggle.com/models/andreygalichin/qwen2.5-32b-r32-a16-synth-merge-smooth-full/</a></p>",
  "comments": [
    {
      "id": 3071267,
      "author_name": "S.Osuga",
      "author_url": "",
      "post_date": "2024-12-13T14:57:38.173000",
      "content": "<p>Thank you for sharing such a brilliant solution! I didn't know about h2o-studio framework. I'd like to try it myself.</p>",
      "votes": 1,
      "replies": [
        {
          "id": 3071291,
          "author_name": "Andrey Galichin",
          "author_url": "",
          "post_date": "2024-12-13T15:32:32.720000",
          "content": "<p>Thank you! Yes, it is actually a perfect framework for those who are not very familiar with the LLM fine-tuning or don't want to waste much time to write their own code. It has a pretty nice GUI too.</p>",
          "votes": 1,
          "replies": []
        }
      ]
    },
    {
      "id": 3071359,
      "author_name": "Sinan Calisir",
      "author_url": "",
      "post_date": "2024-12-13T17:09:04.117000",
      "content": "<p>Congratulations on your strong finish and thank your for the detailed write up.</p>",
      "votes": 0,
      "replies": []
    },
    {
      "id": 3071269,
      "author_name": "Pakhnenko Ivan",
      "author_url": "",
      "post_date": "2024-12-13T15:00:35.810000",
      "content": "<p>We also tried to implement two ideas but they didn’t work out:</p>\n<ol>\n<li><p>Instead of ranking all 25 misconceptions at once we considered feeding the rerank model 10 misconceptions at a time, selecting the top 5, then comparing these top 5 with the next batch of 5, and so on until processing all misconceptions. This approach worked well with qwen-14, but we couldn’t adapt the code for qwen-32 within time constraints, despite experimenting with various batch processing strategies. <br>\nBased on the top 7 private LB team's solution, our main mistake was generating a single number per model output and querying the reranker for each misconception individually. This took significantly more time than asking about all misconceptions in a single query.</p></li>\n<li><p>Assuming the reranker is robust, we hypothesized that final misconception should appear in the top 15 results. Based on that, we tried replacing the 16-25 range with ones closer in cosine similarity to the top 15 to boost recall in the retrieval step. It worked in some tests, but for the final submission with the best models, we decided it was too risky and didn’t go for it.</p></li>\n</ol>",
      "votes": 0,
      "replies": []
    }
  ],
  "comment_count": 4,
  "messages": [
    {
      "id": 3071228,
      "postDate": "2024-12-13T14:02:06.677Z",
      "content": "<p>First of all, I would like to thank the organisers for hosting the competition, kaggle team, and my team mates from AIRI, Skoltech and VeinCV<br>\n<a href=\"https://www.kaggle.com/rolf110\" target=\"_blank\">rolf110</a><br>\n<a href=\"https://www.kaggle.com/danyaivanov\" target=\"_blank\">danyaivanov</a><br>\nShout-out to <a href=\"https://www.kaggle.com/pipmos\" target=\"_blank\">pipmos</a>!</p>\n<h2>Summary</h2>\n<h3>Overall Inference Pipeline</h3>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-forum-message-attachments/o/inbox%2F4526980%2Fb29b262492a8e319f0520626f0ae3a35%2Feedi_inference_pipeline.png?generation=1734098354476413&amp;alt=media\" alt=\"Inference Pipeline\"></p>\n<p>Our solution:</p>\n<ul>\n<li><code>Qwen2.5-14B</code> retriever to obtain top-25 misconceptions</li>\n<li><code>Qwen2.5-32B</code> reranker used to score each of the misconceptions received by retriever. We do it iteratively, by passing one misconception at a time to obtain a probability of it being the one that leads to the incorrect answer. </li>\n</ul>\n<p>The whole notebook runs approximately 7 hours. We didn't use any upper-level frameworks such as vllm to run the inference pipeline.</p>\n<p>All models were fine-tuned using the competition data, consisting of 4370 (question, construct, correct answer, incorrect answer) quadruplets, +   13921 synthetic (question, incorrect answer, correct answer, incorrect answer) samples generated using gpt-4o.</p>\n<h2>Description</h2>\n<h3>Synthetic Data</h3>\n<ul>\n<li>For each <code>MisconceptionId</code> in the given database, we generated new (<code>QuestionText</code>, <code>ConstructName</code>, <code>Correct Answer</code>, <code>Incorrect Answer</code>) samples, ensuring at least 7 examples is presented for each <code>MisconceptionId</code> in the final real+synthetic dataset. We then filtered those examples that have a full-match in the <code>QuestionText</code> (approximately 500 samples removed).</li>\n<li>To provide some guidance to the LLM, we passed a reference sample from the train dataset corresponding to the closest misconceptions for the one considered for generation. We used <code>SFR-Embedding-2_R</code> embeddings of <code>MisconceptionName</code> to obtain \"closest\" misconceptions. We ensured that each reference is different between different generations for a particular <code>MisconceptionId</code>.</li>\n<li>To construct the prompt, we followed the ideas from <a href=\"https://arxiv.org/pdf/2403.04706\" target=\"_blank\">here</a>. The template of the <code>prompt</code> we passed to gpt-4o:</li>\n</ul>\n<pre><code>\n</code></pre>\n<p>We manually checked some of the generated examples, and they were pretty good and close to the real data.<br>\nOther staff we found important for generation:</p>\n<ul>\n<li><strong>verification and modification</strong> stage inside the prompt helped to fix some rare errors and obtain good questions and valid answers.</li>\n<li>When specifying a reference, it is important to add \"ensure the student can answer your created question without the given example\" to the prompt. Otherwise in some rare cases the model didn't specify some important information in the generated question because it was stored in the reference and the model probably thought that it will be also given to a student. </li>\n<li>The only thing we couldn't entirely fix is that in rare cases (approx. 3-4% of the data) LLM generated quite a strong hint for the correct misconception inside the description (<code>ConstructName</code>) field; but we found that such cases were also observed in the competition data, so we decided to keep it as is.</li>\n</ul>\n<h3>Validation</h3>\n<p>We used 20% of the competition data for validation, which we additionally split on 2 subsets:</p>\n<ul>\n<li><strong>out-of-distribution (OOD):</strong> no intersection for train/test between <code>MisconceptionId</code> and stratification based on the number of misconceptions per question.</li>\n<li><strong>in-distribution (ID):</strong> no intersection for train/test between <code>QuestionId</code> and stratification based on the number of misconceptions per question.</li>\n</ul>\n<p>We used the <strong>ID</strong> test to account for how the model works on the misconceptions it knows, while the <strong>OOD</strong> test was used to account for how the model generalizes to the unseen misconceptions. However, we mainly tracked the <strong>OOD</strong> data because it was well-aligned with the lb and accounted for the most complex scenario.</p>\n<h3>Retriever</h3>\n<p>To obtain fine-grained hard negatives, we first fine-tuned <code>Qwen2.5-14B</code> on real+synthetic data. Then we repeat the training with the exact same setup, but using the hard-negatives extracted using our trained <code>Qwen2.5-14B</code>. After each epoch, we collect new hard negatives using the latest checkpoint.</p>\n<h4>Training</h4>\n<ul>\n<li>negatives_range &amp; num_negatives: 100 &amp; 8</li>\n<li>qlora config: rank 32, alpha 64</li>\n<li>loss: InfoNCE Loss</li>\n<li>mask_token_probability: 0.1 (applied only to the query)</li>\n<li>lr: 5e-5</li>\n<li>epochs: 3</li>\n</ul>\n<p>We also utilized in-batch negative sampling during training. To mitigate the problem of treating negatives which appear to be the same misconceptions as the positive ones, we applied dynamic masking by setting the similarities of such misconceptions to <code>-inf</code> before calculating the loss for each sample in the batch.</p>\n<h4>Score</h4>\n<table>\n<thead>\n<tr>\n<th>Data</th>\n<th>Recall@25</th>\n<th>MAP@25</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>CV OOD</td>\n<td>0.88</td>\n<td>0.42</td>\n</tr>\n<tr>\n<td>Public LB</td>\n<td>None</td>\n<td>0.438</td>\n</tr>\n<tr>\n<td>Private LB</td>\n<td>None</td>\n<td>0.434</td>\n</tr>\n</tbody>\n</table>\n<h3>Reranker</h3>\n<p>Judging from the experiments provided in the paper <a href=\"https://arxiv.org/abs/2310.02439\" target=\"_blank\">Novice Learner and Expert Tutor</a> and some research done by other participants during the competition, we had come up with the idea to pass misconceptions list as an additional input to better guide the reranker model. </p>\n<p>We didn't want the model to output the misconception prediction as a text because it was not very reliable as we thought, and the multi-classification problem didn't fit here either. Finally, we have decided to solve a binary classification problem for each misconception separately:</p>\n<ol>\n<li>First, we pass the question, construct, correct answer, incorrect answer, and top-25 misconceptions as a list received by retriever through the backbone and store <code>past_key_values</code>. This helps to save a lot of runtime because we only need to compute it once for the given sample.</li>\n<li>Then we form a batch of misconceptions taken from the list, and pass it to the backbone where each element gets the same <code>past_key_values</code> from the previous step as cache. We use a batch size of 5, so 5 iterations is required for the whole list of misconceptions.</li>\n<li>We take the final logits and feed them to the classification head trained from scratch. This head maps from the logits space to a single score indicating whether the misconception leads to the incorrect answer or no.</li>\n<li>We sort the misconceptions by predicted scores.</li>\n</ol>\n<h4>Training Dataset</h4>\n<p>Given the (question, incorrect answer) pair, we create 24 <code>label = 0</code> and 1 <code>label = 1</code> samples for training. The main difference between them is the way we construct the \"list of possible misconceptions\":</p>\n<ol>\n<li>First, we identify the top 50 negative misconceptions using our trained retriever.</li>\n<li>From these top 50 misconceptions, we randomly select 24 negatives as a subset for further sampling.</li>\n<li>For each of the negatives sampled at the 2nd step, we add 28 random negatives, the specific negative being evaluated, and the correct positive misconception tied to the (question, incorrect answer) pair. This results in 24 <code>label = 0</code> samples per pair.</li>\n<li>When the model is queried with a positive misconception (<code>label = 1</code>), we include 29 randomly selected negatives from the top 50 to create a set of 30 misconceptions we pass to the model.</li>\n</ol>\n<p>To mitigate the positional bias caused by the retriever, we randomly shuffle the list for every input sample.</p>\n<p>Example of the input prompt:</p>\n<pre><code>Question: {Question}\nBrief description of the concept  skill being tested  the Question: {ConstructName}\nCorrect Answer: {Correct Answer}\nIncorrect Answer: {Incorrect Answer}\n of possible misconceptions:\n {Misconception_1}\n...\n {Misconception_30}\nMisconception: {n}. {Misconception_n}\nDoes this Misconception lead to the Incorrect Answer? \n</code></pre>\n<p>To account for high imbalance between 0/1 classes, we oversampled positive class 4 times.</p>\n<h4>Training</h4>\n<p>We used <a href=\"https://docs.h2o.ai/h2o-llmstudio/\" target=\"_blank\">h2o-studio</a> framework to train our model.</p>\n<ul>\n<li>loss: cross-entropy loss with 0.05 label smoothing to account for possible errors introduced by synthetic data</li>\n<li>qlora config: rank 16, alpha 32</li>\n<li>lr &amp; head lr: 2e-5 &amp; 1e-5</li>\n<li>epoch: 1</li>\n<li>mask token probability: 0.1</li>\n</ul>\n<h4>Score</h4>\n<table>\n<thead>\n<tr>\n<th>Data</th>\n<th>MAP@25</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>CV OOD</td>\n<td>0.571</td>\n</tr>\n<tr>\n<td>Public LB</td>\n<td>0.589</td>\n</tr>\n<tr>\n<td>Private LB</td>\n<td>0.558</td>\n</tr>\n</tbody>\n</table>\n<h3>What did not work</h3>\n<ul>\n<li>Re-write positive misconceptions with gpt4o-mini to improve oversampling procedure</li>\n<li>Add other incorrect answer choices to the prompt</li>\n</ul>\n<h3>Public Data</h3>\n<p>reranker adapter and classification head weights: <a href=\"https://www.kaggle.com/models/andreygalichin/qwen2.5-32b-r32-a16-synth-merge-smooth-full/\" target=\"_blank\">https://www.kaggle.com/models/andreygalichin/qwen2.5-32b-r32-a16-synth-merge-smooth-full/</a></p>",
      "rawMarkdown": "First of all, I would like to thank the organisers for hosting the competition, kaggle team, and my team mates from AIRI, Skoltech and VeinCV\n[rolf110](https://www.kaggle.com/rolf110)\n[danyaivanov](https://www.kaggle.com/danyaivanov)\nShout-out to [pipmos](https://www.kaggle.com/pipmos)!\n\n## Summary\n\n### Overall Inference Pipeline\n![Inference Pipeline](https://www.googleapis.com/download/storage/v1/b/kaggle-forum-message-attachments/o/inbox%2F4526980%2Fb29b262492a8e319f0520626f0ae3a35%2Feedi_inference_pipeline.png?generation=1734098354476413&alt=media)\n\nOur solution:\n- `Qwen2.5-14B` retriever to obtain top-25 misconceptions\n- `Qwen2.5-32B` reranker used to score each of the misconceptions received by retriever. We do it iteratively, by passing one misconception at a time to obtain a probability of it being the one that leads to the incorrect answer. \n\nThe whole notebook runs approximately 7 hours. We didn't use any upper-level frameworks such as vllm to run the inference pipeline.\n\nAll models were fine-tuned using the competition data, consisting of 4370 (question, construct, correct answer, incorrect answer) quadruplets, +   13921 synthetic (question, incorrect answer, correct answer, incorrect answer) samples generated using gpt-4o.\n\n## Description\n\n### Synthetic Data\n\n- For each `MisconceptionId` in the given database, we generated new (`QuestionText`, `ConstructName`, `Correct Answer`, `Incorrect Answer`) samples, ensuring at least 7 examples is presented for each `MisconceptionId` in the final real+synthetic dataset. We then filtered those examples that have a full-match in the `QuestionText` (approximately 500 samples removed).\n- To provide some guidance to the LLM, we passed a reference sample from the train dataset corresponding to the closest misconceptions for the one considered for generation. We used `SFR-Embedding-2_R` embeddings of `MisconceptionName` to obtain \"closest\" misconceptions. We ensured that each reference is different between different generations for a particular `MisconceptionId`.\n- To construct the prompt, we followed the ideas from [here](https://arxiv.org/pdf/2403.04706). The template of the `prompt` we passed to gpt-4o:\n\n```python\n'''Please act as a professional math tutor.\nYour goal is to create high quality math problems to help students learn math.\nYou will be given a misconception. Please create a multiple choice math question with two options: one correct answer and one distractor (incorrect answer). The distractor should be based on the Misconception. \nFollow the instructions below.\nTo achieve the goal, you have four tasks:\n1. Please generate a brief and formal description of the concept or skill being tested in the question. DO NOT mention or refer to the Misconception in the description.\n2. Create a realistic and contextually appropriate math question that aligns with the description. Provide two options: the correct answer and a distractor (incorrect answer). The distractor should be a plausible answer that a student might choose if they hold the Misconception.\n3. Check the question by solving it step-by-step to find out if it adheres to all principles.\n4. Modify the created question and options according to your checking comment to ensure it is of high quality.\nYou have the following principles to guide you:\n1. Ensure the question is realistic, natural, and contextually appropriate, adhering to common sense and fundamental mathematical principles.\n2. Ensure the question asks for only one specific thing and is clearly stated.\n3. Ensure your student can answer the question correctly by using only the information provided in the question. If visual data is needed (e.g. plot, histogram, bar, etc.), describe it's content explicitly in text.\n4. Ensure the distractor is directly related to the question, stems from the Misconception, and is a plausible answer that a student might select if they have that Misconception.\n5. If the created question already follows these principles upon verification, keep it without modification.\n\nYour output should be in the following format:\nDESCRIPTION: <Brief and formal description of the concept or skill being tested>\nQUESTION: <Your created question>\nCORRECT ANSWER: <Correct answer to the question>\nDISTRACTOR: <Distractor (incorrect answer) to the question based on the provided Misconception>\nVERIFICATION AND MODIFICATION: <Solve the question step-by-step and modify it to follow all principles>\nFINAL QUESTION: <Your final created question>\nFINAL CORRECT ANSWER: <Your final correct answer>\nFINAL DISTRACTOR: <Your final distractor (incorrect answer)>\n\nHere is an example of the final result corresponding to Misconception \"{misconception}\". Use it for your reference, but ensure the student can answer your created question without the given example:\nDESCRIPTION: {description}\nFINAL QUESTION: {question}\nFINAL CORRECT ANSWER: {correct_answer}\nFINAL INCORRECT ANSWER: {incorrect_answer}\n'''\n```\n\nWe manually checked some of the generated examples, and they were pretty good and close to the real data.\nOther staff we found important for generation:\n- **verification and modification** stage inside the prompt helped to fix some rare errors and obtain good questions and valid answers.\n- When specifying a reference, it is important to add \"ensure the student can answer your created question without the given example\" to the prompt. Otherwise in some rare cases the model didn't specify some important information in the generated question because it was stored in the reference and the model probably thought that it will be also given to a student. \n- The only thing we couldn't entirely fix is that in rare cases (approx. 3-4% of the data) LLM generated quite a strong hint for the correct misconception inside the description (`ConstructName`) field; but we found that such cases were also observed in the competition data, so we decided to keep it as is.\n\n### Validation\n\nWe used 20% of the competition data for validation, which we additionally split on 2 subsets:\n  - **out-of-distribution (OOD):** no intersection for train/test between `MisconceptionId` and stratification based on the number of misconceptions per question.\n  - **in-distribution (ID):** no intersection for train/test between `QuestionId` and stratification based on the number of misconceptions per question.\n\nWe used the **ID** test to account for how the model works on the misconceptions it knows, while the **OOD** test was used to account for how the model generalizes to the unseen misconceptions. However, we mainly tracked the **OOD** data because it was well-aligned with the lb and accounted for the most complex scenario.\n\n### Retriever\n\nTo obtain fine-grained hard negatives, we first fine-tuned `Qwen2.5-14B` on real+synthetic data. Then we repeat the training with the exact same setup, but using the hard-negatives extracted using our trained `Qwen2.5-14B`. After each epoch, we collect new hard negatives using the latest checkpoint.\n\n#### Training\n- negatives_range & num_negatives: 100 & 8\n- qlora config: rank 32, alpha 64\n- loss: InfoNCE Loss\n- mask_token_probability: 0.1 (applied only to the query)\n- lr: 5e-5\n- epochs: 3\n\nWe also utilized in-batch negative sampling during training. To mitigate the problem of treating negatives which appear to be the same misconceptions as the positive ones, we applied dynamic masking by setting the similarities of such misconceptions to `-inf` before calculating the loss for each sample in the batch.\n\n#### Score\n\n| Data | Recall@25 | MAP@25 |\n| --- | --- | --- |\n| CV OOD | 0.88 | 0.42 |\n| Public LB | None | 0.438 |\n| Private LB | None | 0.434 |\n\n### Reranker\n\nJudging from the experiments provided in the paper [Novice Learner and Expert Tutor](https://arxiv.org/abs/2310.02439) and some research done by other participants during the competition, we had come up with the idea to pass misconceptions list as an additional input to better guide the reranker model. \n\nWe didn't want the model to output the misconception prediction as a text because it was not very reliable as we thought, and the multi-classification problem didn't fit here either. Finally, we have decided to solve a binary classification problem for each misconception separately:\n1. First, we pass the question, construct, correct answer, incorrect answer, and top-25 misconceptions as a list received by retriever through the backbone and store `past_key_values`. This helps to save a lot of runtime because we only need to compute it once for the given sample.\n2. Then we form a batch of misconceptions taken from the list, and pass it to the backbone where each element gets the same `past_key_values` from the previous step as cache. We use a batch size of 5, so 5 iterations is required for the whole list of misconceptions.\n3. We take the final logits and feed them to the classification head trained from scratch. This head maps from the logits space to a single score indicating whether the misconception leads to the incorrect answer or no.\n4. We sort the misconceptions by predicted scores.\n\n#### Training Dataset\n\nGiven the (question, incorrect answer) pair, we create 24 `label = 0` and 1 `label = 1` samples for training. The main difference between them is the way we construct the \"list of possible misconceptions\":\n1. First, we identify the top 50 negative misconceptions using our trained retriever.\n2. From these top 50 misconceptions, we randomly select 24 negatives as a subset for further sampling.\n3. For each of the negatives sampled at the 2nd step, we add 28 random negatives, the specific negative being evaluated, and the correct positive misconception tied to the (question, incorrect answer) pair. This results in 24 `label = 0` samples per pair.\n4. When the model is queried with a positive misconception (`label = 1`), we include 29 randomly selected negatives from the top 50 to create a set of 30 misconceptions we pass to the model.\n\nTo mitigate the positional bias caused by the retriever, we randomly shuffle the list for every input sample.\n\nExample of the input prompt:\n\n```python\nQuestion: {Question}\nBrief description of the concept or skill being tested in the Question: {ConstructName}\nCorrect Answer: {Correct Answer}\nIncorrect Answer: {Incorrect Answer}\nList of possible misconceptions:\n1. {Misconception_1}\n...\n30. {Misconception_30}\nMisconception: {n}. {Misconception_n}\nDoes this Misconception lead to the Incorrect Answer? \n```\n\nTo account for high imbalance between 0/1 classes, we oversampled positive class 4 times.\n\n#### Training\n\nWe used [h2o-studio](https://docs.h2o.ai/h2o-llmstudio/) framework to train our model.\n\n- loss: cross-entropy loss with 0.05 label smoothing to account for possible errors introduced by synthetic data\n- qlora config: rank 16, alpha 32\n- lr & head lr: 2e-5 & 1e-5\n- epoch: 1\n- mask token probability: 0.1\n\n#### Score\n\n| Data | MAP@25 |\n| --- | --- |\n| CV OOD | 0.571 |\n| Public LB | 0.589 |\n| Private LB | 0.558 |\n\n### What did not work\n- Re-write positive misconceptions with gpt4o-mini to improve oversampling procedure\n- Add other incorrect answer choices to the prompt\n\n### Public Data\n\nreranker adapter and classification head weights: https://www.kaggle.com/models/andreygalichin/qwen2.5-32b-r32-a16-synth-merge-smooth-full/",
      "votes": 15
    },
    {
      "id": 3071267,
      "postDate": "2024-12-13T14:57:38.173Z",
      "content": "<p>Thank you for sharing such a brilliant solution! I didn't know about h2o-studio framework. I'd like to try it myself.</p>",
      "rawMarkdown": "Thank you for sharing such a brilliant solution! I didn't know about h2o-studio framework. I'd like to try it myself.",
      "votes": 1,
      "replies": [
        {
          "id": 3071291,
          "postDate": "2024-12-13T15:32:32.720Z",
          "content": "<p>Thank you! Yes, it is actually a perfect framework for those who are not very familiar with the LLM fine-tuning or don't want to waste much time to write their own code. It has a pretty nice GUI too.</p>",
          "rawMarkdown": "Thank you! Yes, it is actually a perfect framework for those who are not very familiar with the LLM fine-tuning or don't want to waste much time to write their own code. It has a pretty nice GUI too.",
          "votes": 1
        }
      ]
    },
    {
      "id": 3071359,
      "postDate": "2024-12-13T17:09:04.117Z",
      "content": "<p>Congratulations on your strong finish and thank your for the detailed write up.</p>",
      "rawMarkdown": "Congratulations on your strong finish and thank your for the detailed write up."
    },
    {
      "id": 3071269,
      "postDate": "2024-12-13T15:00:35.810Z",
      "content": "<p>We also tried to implement two ideas but they didn’t work out:</p>\n<ol>\n<li><p>Instead of ranking all 25 misconceptions at once we considered feeding the rerank model 10 misconceptions at a time, selecting the top 5, then comparing these top 5 with the next batch of 5, and so on until processing all misconceptions. This approach worked well with qwen-14, but we couldn’t adapt the code for qwen-32 within time constraints, despite experimenting with various batch processing strategies. <br>\nBased on the top 7 private LB team's solution, our main mistake was generating a single number per model output and querying the reranker for each misconception individually. This took significantly more time than asking about all misconceptions in a single query.</p></li>\n<li><p>Assuming the reranker is robust, we hypothesized that final misconception should appear in the top 15 results. Based on that, we tried replacing the 16-25 range with ones closer in cosine similarity to the top 15 to boost recall in the retrieval step. It worked in some tests, but for the final submission with the best models, we decided it was too risky and didn’t go for it.</p></li>\n</ol>",
      "rawMarkdown": "We also tried to implement two ideas but they didn’t work out:\n\n  1.  Instead of ranking all 25 misconceptions at once we considered feeding the rerank model 10 misconceptions at a time, selecting the top 5, then comparing these top 5 with the next batch of 5, and so on until processing all misconceptions. This approach worked well with qwen-14, but we couldn’t adapt the code for qwen-32 within time constraints, despite experimenting with various batch processing strategies. \nBased on the top 7 private LB team's solution, our main mistake was generating a single number per model output and querying the reranker for each misconception individually. This took significantly more time than asking about all misconceptions in a single query.\n\n  2.  Assuming the reranker is robust, we hypothesized that final misconception should appear in the top 15 results. Based on that, we tried replacing the 16-25 range with ones closer in cosine similarity to the top 15 to boost recall in the retrieval step. It worked in some tests, but for the final submission with the best models, we decided it was too risky and didn’t go for it."
    }
  ],
  "complete": true,
  "source": "live",
  "fetched_at": "2026-10-20T03:45:55Z",
  "competition_id": 82695,
  "competition_slug": "eedi-mining-misconceptions-in-mathematics",
  "team_id": 12612503,
  "solution_url": "/competitions/eedi-mining-misconceptions-in-mathematics/writeups/eeee-rsi-14th-place-solution"
}