DEV Community

Marie Claire NIYOMUGENGA
Marie Claire NIYOMUGENGA

Posted on

Fixing the Extra-Ingredient Problem in MiniChefGPT: Which Model Should We Use?

In our NLP course we built MiniChefGPT, a small GPT-style model that turns a list of ingredients into a recipe title and steps. It works, but it has one clear flaw: it uses ingredients nobody gave it. This post looks at models and techniques that could fix that, and explains which one I would choose and why.

The project in numbers

  • A decoder-only Transformer with 3 blocks, 4 attention heads, 128-dimensional embeddings and a 256-token context.
  • A byte-level BPE tokenizer with a 4,096-token vocabulary.
  • 1,053,440 parameters, trained from scratch on a CPU within a 30-minute budget.
  • 9,979 cleaned Food.com recipes, split into 8,983 for training, 502 for validation and 494 for testing.
  • Test perplexity of 16.68.

My part of the team project was the residual connections around the attention and feed-forward blocks.

The problem

We gave the app four ingredients: chicken, garlic, onion and tomato. It returned a recipe titled "chicken soup" whose steps use broccoli, rice, chicken broth and noodles. None of those were requested, and tomato was never used.

Our own notebook check agrees. On four custom ingredient lists, the generated recipes mention 56% of the requested ingredients on average. On 12 held-out ingredient lists the figure is 27%.

Why it happens

  1. A tiny model with little data. One million parameters trained from scratch on about 9,000 recipes learn how recipes sound. They do not learn a rule like "use only these ingredients".
  2. Noisy training data. Our notebook notes that the source recipes sometimes mention ingredients that are missing from their own ingredient list. The model learns that extra ingredients are normal.
  3. No rule against extras when generating. We rank several candidate recipes by how many requested ingredients they mention, but nothing penalises ingredients that were not requested.

How we should measure the fix

Our coverage score only counts requested ingredients that appear. It cannot see extras. I would add an extra-ingredient rate: the share of food words in the generated recipe that are not in the input list, allowing a few pantry staples such as salt, pepper, water and oil. A good fix raises coverage and lowers this rate on the same held-out lists.

Candidate models and techniques

Option What it is Does it help with extras? Catch
Constrained decoding Rules applied while generating, such as bad_words_ids to ban tokens and prefix_allowed_tokens_fn to allow only some Yes, directly: banned food words cannot appear Needs a food word list. Forcing words with constrained beam search needs do_sample=False
Chef Transformer (T5) A T5 text-to-text model trained on RecipeNLG, a dataset of 2,231,142 recipes, with an ingredient list as input Maybe: far more data than our 10,000 recipes Different data and architecture, so it is hard to compare with our model, and it can still add ingredients
Small pretrained decoder with LoRA Start from SmolLM2 (135M, 360M or 1.7B parameters) or Qwen3 (0.6B or 1.7B), both Apache 2.0, and fine-tune on our recipes Likely: it already knows language, and Instruct versions are trained to follow instructions Not guaranteed. Needs a GPU, but the free Colab T4 is enough
Retrieval with all-MiniLM-L6-v2 A 22.7M-parameter embedding model that turns text into 384 numbers, used to find real recipes with similar ingredients Yes, real recipes are consistent The app copies existing recipes instead of creating new ones
Ingredient checker A model that finds food words in the output, for example an NER model trained on TASTEset (700 recipes, more than 13,000 labelled entities) Catches extras after generation It checks but does not generate, and a simple word list built from our data may do the same job

My choice: SmolLM2-360M-Instruct, fine-tuned with LoRA

I would replace our from-scratch model with SmolLM2-360M-Instruct, fine-tuned on our recipes with LoRA. LoRA trains small adapter matrices instead of every weight, so training is light. My reasons:

  1. Same design, much more knowledge. It is a decoder-only Transformer like MiniChefGPT, so our pipeline of ingredients in and recipe out barely changes. It has about 340 times more parameters (360 million against 1.05 million) and is already pretrained, so it starts with language knowledge instead of learning from 9,979 recipes alone.
  2. It can follow an instruction. We can write the task as "use only these ingredients", which our scratch model cannot understand.
  3. It is cheap to run. In fp16, 360 million parameters take about 0.7 GB (parameters times 2 bytes), which fits easily on a free Colab T4 with 16 GB.
  4. The licence is Apache 2.0, so there is no gate or paid API.

Why not the others? Chef Transformer uses a different dataset and architecture, which makes it hard to compare with our results. Qwen3-1.7B is probably stronger, but it is almost five times bigger and has a thinking mode to switch off, so I would keep it as a stretch goal. Retrieval gives consistent recipes but copies them. Constrained decoding and an ingredient checker are not alternative models, so I would add them on top of my choice.

I have not run this yet. A bigger model can still invent ingredients, so I would not claim success until the extra-ingredient rate drops on held-out lists.

First experiment

  1. Turn recipe_10000.csv into instruction examples such as "Use only these ingredients: chicken, garlic, onion, tomato. Write a title and steps."
  2. Fine-tune SmolLM2-360M-Instruct with LoRA on the same training split, and keep our validation and test splits so results stay comparable.
  3. Generate with a banned-word list (sketch below, not tested).
  4. Compare coverage and extra-ingredient rate on the same 12 held-out lists and 4 custom tests.
# Food words seen in the training recipes (ingredient names only)
food_words = {w for r in train_rows for ing in r["ingredients"] for w in ing.split()}

# Words the user gave, plus pantry staples we allow
allowed = {w for ing in user_ingredients for w in ing.split()} | {"salt", "pepper", "water", "oil"}

# Ban every other food word while generating
banned = [tok(" " + w, add_special_tokens=False).input_ids for w in food_words - allowed]
out = model.generate(**inputs, bad_words_ids=banned, do_sample=True,
                     temperature=0.7, top_p=0.9, max_new_tokens=200)
Enter fullscreen mode Exit fullscreen mode

This sketch ignores plural forms and capital letters, so a real version needs word variants.

What I took away

A bigger model is only part of the answer. The extra ingredients come from the data, the model size and the decoding, and each one needs its own fix. Measuring extras, not only coverage, is what tells us whether a change really helped.

References

Top comments (0)