3. Classification with a generative decoder

Exercise

NoteRun this yourself

This page shows the exercise code for reference; code blocks with real output below were actually run to produce it, but the page itself isn’t a live Colab session — open the notebook there to run it yourself.

Open in Colab →

No problem! Explanations for the less obvious lines are hidden behind the little numbers on the right of each code block — just hover your mouse over one to see it.

print("Hello!")
1
This prints “Hello!” to the console. The output would appear directly below the code block.

This notebook belongs to the DGPs 2026 workshop Text Classification with Open Source Models. You’ll classify the same open-ended survey text from the Real World Worry Waves Dataset (RW3D) as in the previous notebook — this time with a small, open-source generative decoder model. Instead of a fixed set of labels, you write a prompt, the model generates free text, and you parse that text back into a structured answer.

Code-along vs. exercise: Steps 1–2 below — loading the data, writing a first zero-shot prompt, and parsing the model’s answer — are the code-along, which we build together; there’s a small Your turn fill-in along the way. The “Exercise: From zero-shot to few-shot prompting” section afterward, plus the bonus at the end, are yours to work through independently.

How to use this notebook: run each cell from top to bottom with Shift+Enter or by clicking the run button. Most cells are given and fully explained; a few are marked Your turn — those are where you edit or write something yourself. Every exercise cell has a safe default, so nothing breaks if you leave it as-is, but you’ll get more out of the session if you actually change it.

Important note for Colab users: If you are running this notebook in Google Colab, please follow these steps: 1. Make sure you have a Google account and are logged in. 2. To create a copy of this notebook in your own Google Drive (advised), select “Copy to Drive” at the top of the Colab page. 3. Once the notebook copy opens in Colab, click on “Runtime” in the menu bar, then select “Change runtime type” and choose “T4 GPU” as the hardware accelerator. 4. Click “Save” to apply the changes. 5. Now you can run the notebook cells as instructed.

Setup

First, we install the Python packages we need. This will take some time to complete.

%pip install -q transformers accelerate pandas
1
%pip install runs Python’s package manager from inside the notebook; -q just keeps its output short. transformers and accelerate let us load and run the model, and pandas gives us a convenient table (DataFrame) format for the data.

Next, we can change the logging level of the transformers library to avoid cluttering the notebook with warnings and info messages. You do not have to run this cell, but it will make the output easier to read.

from transformers import logging
logging.set_verbosity_error()
1
Imports transformers’ logging module, which controls how much it prints to the console.
2
Raises the logging threshold to “error” so only actual errors get printed — this hides the routine warning/info messages you’d otherwise see while loading models.

Step 1: Load the data

We’ll use the same curated sample of survey responses from RW3D as in the previous notebook.

First, let’s load the data. Because generative decoder models are slower than encoder models, we’ll only use a small sample of 10 texts for this notebook. You can change the number of rows to load by changing the argument to head() in the next cell.

import pandas as pd

DATA_URL = "https://raw.githubusercontent.com/felixdidi/llm-content-analysis/main/sections/workshop/data/rw3d_workshop_sample.csv"
df = pd.read_csv(DATA_URL).head(10)
df
1
Imports pandas, Python’s standard library for working with tables, under the short name pd.
2
The web address of the same prepared dataset we already used in the previous notebook.
3
read_csv() downloads the file into a DataFrame, and .head(10) immediately keeps only its first 10 rows — that’s the small sample mentioned above. Change this number to work with more (or fewer) texts.
4
Writing a variable name on its own at the end of a notebook cell displays its value — here, the whole 10-row table.
id text main_emotion anger fear sadness
0 1 Stressed and uninformed. Don't feel enough is ... anger 9 1 6
1 2 I feel worried about the virus, burnout about ... fear 8 9 7
2 3 Disgusted how unprotected NHS staff are and ho... sadness 2 6 7
3 4 I am not too worried about the situation as I ... sadness 1 3 6
4 5 I'm finding the Corona situation quite dauntin... sadness 5 2 7
5 6 At the moment I feel like I am doing everythin... sadness 1 2 2
6 7 I feel sad for all the people infected and tho... sadness 2 3 4
7 8 I'm worried about elderly parents. I'm worried... anger 9 7 8
8 9 I am very anxious and worried about the COVID-... fear 2 9 9
9 10 i'm very Angry at the moment as there are many... anger 8 8 6
Source: Classification with a generative decoder

Step 2: Load a small open-source decoder model

We’ll use Qwen2.5-1.5B-Instruct, an instruction-tuned model small enough to run comfortably on Colab’s free GPU (and likely also your own laptop, if you intend to run it locally).

Unlike the encoder models from the previous notebook, this one wasn’t trained to return scores for a fixed set of labels. Instead, it generates a stream of text, so we’ll need to tell it what we want in the prompt itself, and turn its answer back into something structured ourselves.

Generative decoder models are also much larger than encoder models. Loading the model may take some time.

from transformers import pipeline

generate = pipeline(
    "text-generation",
    model="Qwen/Qwen2.5-1.5B-Instruct",
    device_map="auto",
)
1
Imports the same pipeline shortcut we used for the encoder models — it works for decoders too, just with a different task name.
2
Builds the pipeline and stores it under the name generate, so that we can call generate(...) on a prompt further down.
3
text-generation is the task name for decoder models — the pipeline expects a prompt and returns generated text, rather than a fixed set of category scores.
4
“1.5B” refers to the model’s 1.5 billion parameters — small by modern LLM standards, which is exactly why it runs comfortably on a free Colab GPU (and likely also your own laptop, if you intend to run it locally).
5
device_map="auto" lets the library decide whether to put the model on a GPU or CPU, whichever is available.

We write a short system prompt that states the task and asks for a strict JSON answer — here, whether the text expresses fear (true/false) plus a short reasoning. Because the prompt doesn’t show the model any examples, classifications will be based only on the context knowledge of the model. This is called zero-shot prompting.

Unlike the encoder pipelines from the previous notebook, a decoder’s generate() call doesn’t batch cleanly over a list of texts out of the box — so we call it once per text, in a for loop. do_sample=False keeps answers deterministic, so re-running the cell gives the same result. Running it on all texts may take some time.

import json

results = []
for text in df['text']:
    instructions = [
        {"role": "system",
         "content": "You are a trained assistant for content analysis who determines whether a text expresses fear. Always answer precisely in JSON format with fear (true or false) and reasoning (a short justification in English). Return only the JSON response, starting with '{' and ending with '}', with these two parameters."},
        {"role": "user", "content": text}]
    outputs = generate(instructions, max_new_tokens=256, do_sample=False)
    results.append({"text": text, "response": outputs[0]["generated_text"][-1]['content']})

results
1
Imports Python’s built-in json module — we need it a bit further down to turn the model’s JSON answer back into Python data.
2
An empty list in which we collect one result per text.
3
Loops over the texts one at a time. Unlike the encoder pipelines, a decoder call doesn’t batch cleanly over a whole list, so every text gets its own call.
4
instructions is the conversation we hand to the model: a list of messages, each one a dictionary with a role and a content.
5
The system message states the task and the exact output format we want. This is the actual prompt engineering, and it stays the same for every text.
6
The user message carries the text to be classified — the only part that changes from one text to the next.
7
Runs the model on that conversation. max_new_tokens=256 caps how long the answer may become, and do_sample=False switches off randomness, so re-running gives identical results.
8
outputs[0]["generated_text"] is the full conversation including the model’s reply, so [-1]['content'] grabs its last message — the answer itself — which we store next to the original text.
9
Displays the collected results, so you can see the raw responses the model produced.
[{'text': "Stressed and uninformed. Don't feel enough is being done by the government to help with the NHS crisis or the proper enforcement of social distancing.nhs staff should be tested immediately and given the right equipment to fight the Corona virus. There should be a full lockdown straight away to stop the spread.the equipment side of it is the fact that they aren't really getting enough or even any at all.how can you be expected to take urgent care of someone, and also be worried that you might catch it,or maybe even already have it.",
  'response': '```json\n{"fear": true, "reasoning": "The text expresses significant concern about the current situation regarding the NHS crisis, lack of action from the government, inadequate testing and equipment for healthcare workers, and the need for immediate measures such as a full lockdown to control the spread of the coronavirus. This indicates a high level of anxiety and fear related to public health issues."}\n```'},
 {'text': 'I feel worried about the virus, burnout about me getting it. My mother is undergoing chemotherapy and has been told no one knows if treatment will continue if nhs becomes overwhelmed. So I am worried most about her treatment ending  also, because of the chemotherapy her immune system is lower and it could really hurt her if she caught it. So at the moment I am just keeping up to date on numbers and nhs capacity and taking it one chemotherapy session at a time. Also, my partner has underlying health conditions and is self isolating in his house. I havent seen him for nearly a month and I am worried about when I will get to see him with all this going on',
  'response': '```json\n{\n  "fear": true,\n  "reasoning": "The text expresses significant worry about multiple aspects including personal health, family members\' health, and overall societal impact due to the pandemic."\n}\n```'},
 {'text': 'Disgusted how unprotected NHS staff are and how late the government has been in trying to resolve this. Disgusted at the amount of people not adhering to guidelines for the safety of others. Sad about the amount of people dying without their families, dying alone without even care staff having the time to sit and hold their hand whilst they take their last breath. Disgusted at the attitude of current staff when someone needs to take the week off, but also feeling the pressure of having to stay at work even though they are experiencing symptoms themselves.',
  'response': '```json\n{"fear": true, "reasoning": "The text expresses strong negative emotions such as disgust, sadness, and anger towards various aspects including healthcare workers, public adherence to guidelines, deaths due to lack of support, and staff attitudes during illness."}\n```'},
 {'text': 'I am not too worried about the situation as I believe I am a fit and healthy human being. I am more worried about family members becoming ill with the disease and I am also very sad that I cannot see them at this time. Thankfully with technology these days I can see them everyday through a screen and chat to both friends and family whenever I want to. Its just annoying that I cannot leave the house apart from to get essentials, I live in a flat and with the nice weather we are having its becoming quite a struggle.',
  'response': '```json\n{\n  "fear": false,\n  "reasoning": "The text does not express any significant level of fear related to the situation described. The author is primarily concerned with their own health and well-being, expressing sadness over the inability to visit family members due to restrictions caused by the disease."\n}\n```'},
 {'text': "I'm finding the Corona situation quite daunting and draining. I can't wait for it to end, and for things to get back to normal. I feel things will not be the same again.it's left so many families devastated. I feel so sad for the families that have lost loved ones,  both old and young. For those that are fighting for their lives, I hope they pull through.  I have hope that all will be well in the end.  This shall pass, we shall overcome this by God's grace. We are all in this together as this situation has affected the whole world and not just certain parts of it. I salute all those working in the frontline, the NHS and all those volunteering at this time.",
  'response': '```json\n{\n  "fear": false,\n  "reasoning": "The text expresses hope, resilience, and solidarity during the pandemic, indicating a lack of fear."\n}\n```'},
 {'text': 'At the moment I feel like I am doing everything I can to protect myself as an individual from the Covid-19 virus. The UK government were a little slow to react and did not take it as seriously as I would have liked, but seem to be doing and saying the right things now. I am looking at other countries data from around the world and things are starting to look more positive from Italy and Spain, where hopefully the figures have peaked as they continue to ‘flatten the curve’. In America the state of New York they still seem to be having a real problem with getting the situation under control, I hope the president will listen to health experts as they monitor the situation.',
  'response': '```json\n{\n  "fear": false,\n  "reasoning": "The text does not express any significant level of fear related to the coronavirus pandemic. Instead, it focuses on personal protective measures, governmental responses, and optimistic outlooks regarding global trends."\n}\n```'},
 {'text': 'I feel sad for all the people infected and those that died and their families. Somewhat pissed off at how the government have handled the outbreak airports and eurotunnel still open...Particulary concerned at Boris Johnson with the situation of him still continuing to shake hands even with coronavirus patients...no wonder he is in hospital at the moment showas a disregard for the situation, fear the worse is yet to come. Me personally I am not worried health wise more concerned for my eldely parents and of course the effect it will have on our everyday lives moving forward',
  'response': '```json\n{"fear": true, "reasoning": "The text expresses concern about the pandemic\'s impact on individuals, families, and society as a whole, indicating a level of worry and fear regarding the situation."}\n```'},
 {'text': "I'm worried about elderly parents. I'm worried about my kids education. I feel exhausted and overwhelmed. I worry about my kids getting infected. I starting to worry less about food and provisions tho we still have no toilet paper. I feel disappointed for my kids and for myself on fun things we can't do. I feel like im doing my bit but Im so upset about not seeing my mum and mother in law who are on their own  every day feels the same and Im starting to get bored too.\nThe kids are acting up and fighting. I feel like my husband doesnt have to deal with this as much. Im trying to work too. ",
  'response': '```json\n{\n  "fear": true,\n  "reasoning": "The text expresses multiple concerns including worries about health, finances, family dynamics, and personal stress related to daily life challenges."\n}\n```'},
 {'text': 'I am very anxious and worried about the COVID-19 Coronavirus. I am scared I might catch it, and that my family and friends will catch it. I am scared we will die. I am scared of the daily numbers. I am scared to leave the house. I am fearful of what the future holds, and what will be left standing when we leave the house. I miss my family and friends. I miss being able to leave the house to go into town for coffee, to eat out, to socialise, to shop etc... I am anxious when I see other people and not to go too close to them.',
  'response': '```json\n{\n  "fear": true,\n  "reasoning": "The text expresses significant anxiety and worry about the coronavirus pandemic, including fears related to catching the virus, losing loved ones, missing social activities, and uncertainty about the future."\n}\n```'},
 {'text': "i'm very Angry at the moment as there are many people who are not taking the situation seriously and are still going out of the house for unnecessary reasons. i have been isolating for the past 2 weeks due to being high risk and the more people dont listen the longer i have to stay home and not see my family and friends. My parents and inlaws are missing my daughter and all her milestones and being pregnant right now is stressful as it is.\nmy mother is also high risk as she is 73 and has memory issues so has still been going out so it stresses me out having to worry about her also. ",
  'response': '```json\n{\n  "fear": true,\n  "reasoning": "The text expresses concern and anxiety about the current situation, including isolation, lack of social interaction, stress related to health risks, and worries about vulnerable individuals like the narrator\'s mother. The use of strong emotional language such as\'very Angry\' indicates heightened fear."\n}\n```'}]

Basically, that was it. We now have a response for every text in the sample. results is just a list of dictionaries — one per text, each holding the original text and the model’s raw response string — and that’s exactly the structure of a JSON file. So, you could simply save it with Python’s built-in json module:

with open("results.json", "w") as f:
    json.dump(results, f)
1
Opens (creates) a file called results.json in write mode; the with block closes the file again automatically once it’s done.
2
Writes results into that open file, converted to JSON text.

That file could be picked up in R or any other tool from here. We’ll keep working in Python below.

If we want to keep working in Python, we need to parse the model’s free-text output back into a structured answer. We use some simple string manipulation to extract the fear value and the reasoning text. This is a bit fragile because the model’s output may not always be perfectly formatted. For example, we can see that, although we told the model to return a strict JSON answer beginning with {, the model consistently returns the JSON object wrapped in a markdown code block (```json\n{...}\n```). We can strip that away with some simple string manipulation, which is already implemented in the parse_response() function below.

def parse_response(response):
    response = response.strip().strip("`").removeprefix("json").strip()
    try:
        parsed = json.loads(response)
        return parsed['fear'], parsed['reasoning']
    except (json.JSONDecodeError, KeyError):
        return None, None

parsed_data = [(entry['text'], *parse_response(entry['response'])) for entry in results]
parsed_data = pd.DataFrame(parsed_data, columns=['text', 'fear', 'reasoning'])
parsed_data
1
Defines a reusable function that turns one raw response string into structured values. Everything indented below belongs to it.
2
Cleans up the string: strip() removes surrounding whitespace, strip("“)removes the backticks of the markdown code fence, andremoveprefix(”json”)` drops the language name right after the opening fence.
3
try means “attempt the following lines, but don’t crash if something goes wrong” — in that case Python jumps down to except.
4
json.loads() parses the cleaned-up string into a real Python dictionary.
5
Returns the two values we asked the model for in the prompt.
6
This runs if the string still wasn’t valid JSON (JSONDecodeError), or if one of the expected keys was missing (KeyError).
7
In that case we return None twice, which marks this response as unparseable instead of stopping the whole notebook.
8
Applies the function to every result, building a list of (text, fear, reasoning) tuples — the * spreads the function’s two return values into the tuple.
9
Turns that list of tuples into a DataFrame and gives the columns proper names.
10
Displays the finished table.
text fear reasoning
0 Stressed and uninformed. Don't feel enough is ... True The text expresses significant concern about t...
1 I feel worried about the virus, burnout about ... True The text expresses significant worry about mul...
2 Disgusted how unprotected NHS staff are and ho... True The text expresses strong negative emotions su...
3 I am not too worried about the situation as I ... False The text does not express any significant leve...
4 I'm finding the Corona situation quite dauntin... False The text expresses hope, resilience, and solid...
5 At the moment I feel like I am doing everythin... False The text does not express any significant leve...
6 I feel sad for all the people infected and tho... True The text expresses concern about the pandemic'...
7 I'm worried about elderly parents. I'm worried... True The text expresses multiple concerns including...
8 I am very anxious and worried about the COVID-... True The text expresses significant anxiety and wor...
9 i'm very Angry at the moment as there are many... True The text expresses concern and anxiety about t...

Your turn

The prompt above only asks about fear. Edit the system prompt so it asks for anger, fear, and sadness all at once (three booleans plus one reasoning, in a single JSON object). If everything works correctly, the code below should pull out all three fields instead of just fear. Re-run the cell — how many responses still parse cleanly?

Feeling bold? If parsing below fails for some responses, you can try to improve the parsing function to handle more edge cases. Look at the raw output to see what the model is returning, and try to make the parsing more robust. You can also try to improve the prompt itself to get cleaner output from the model or increase the maximum number of tokens in the generate() call if the model is truncating its output.

results_all_three = []
for text in df['text']:
    instructions = [
        {"role": "system",
         "content": "ADD YOUR UPDATED SYSTEM PROMPT HERE"}, # TODO
        {"role": "user", "content": text}]
    outputs = generate(instructions, max_new_tokens=256, do_sample=False)
    results_all_three.append({"text": text, "response": outputs[0]["generated_text"][-1]['content']})

results_all_three
1
An empty list to collect one result per text.
2
Loops over every text in the dataset, one at a time — a decoder’s generate() call has to be run separately for each text, unlike the encoder pipelines from before.
3
The conversation we send to the model, exactly as in the code-along: a list of messages, each with a role and a content.
4
The system message sets the model’s overall instructions — this is where your updated prompt (asking for anger, fear, and sadness together) goes.
5
The user message is the actual text to classify.
6
Runs the model on this conversation; max_new_tokens=256 caps how long the generated answer can be, and do_sample=False keeps the output deterministic.
7
outputs[0]["generated_text"][-1]['content'] digs into the pipeline’s return value to pull out just the model’s reply text — generated_text is the whole conversation, so [-1] grabs its last message, the assistant’s answer — and stores it together with the original text.
8
Displays all collected responses.
def parse_response_all_three(response):
    response = response.strip().strip("`").removeprefix("json").strip()
    try:
        parsed = json.loads(response)
        return parsed['fear'], parsed['anger'], parsed['sadness'], parsed['reasoning']
    except (json.JSONDecodeError, KeyError):
        return None, None, None, None

parsed_data_all_three = [(entry['text'], *parse_response_all_three(entry['response'])) for entry in results_all_three]
parsed_data_all_three = pd.DataFrame(parsed_data_all_three, columns=['text', 'fear', 'anger', 'sadness', 'reasoning'])
parsed_data_all_three
1
Defines a function that turns one raw model response string into structured values — the same idea as parse_response() above, just with three emotions instead of one.
2
Strips whitespace, then strips away backticks and a leading json — undoing the markdown code fence (```json ... ```) the model tends to wrap its answer in.
3
try means “attempt the following lines, but don’t crash if something goes wrong” — Python then jumps down to except.
4
json.loads() parses the cleaned-up string into an actual Python dictionary.
5
Pulls out the four fields we asked the model for in the prompt.
6
This runs if the text still isn’t valid JSON (JSONDecodeError), or if one of the expected keys is missing (KeyError) — which happens easily here, since your new prompt has to produce all three emotion keys.
7
In that case, four Nones are returned instead of crashing, so one badly-formatted response doesn’t stop the whole loop.
8
Applies the parsing function to every entry in results_all_three, pairing each original text with its four parsed values (the * unpacks the function’s four return values into the tuple).
9
Collects everything into a DataFrame with named columns, so you can look at it as a table.
10
Displays the table. Rows full of None are the responses that could not be parsed.

Exercise: From zero-shot to few-shot prompting

So far, the model has only ever seen the text it needs to classify — no examples of what a “correct” answer looks like. That’s zero-shot prompting. In few-shot prompting, you show the model one or more examples (e.g., an example text, maybe the JSON answer you’d want for it) before asking about the real text. This can nudge the model toward your specific understanding of the category and toward the exact output format you asked for. However, it may also bias the model toward the examples you show it, so you have to be careful about what examples you choose.

Below, write 1–2 short example texts, together with the JSON answer you think is correct for each — base this on your own understanding of what should count as fear here. These examples will be inserted into the prompt before the real text every time the model is called.

# TODO: write 1-2 short examples of your own: a text, and the JSON answer
# you think is correct for it (based on your own understanding of "fear").
# These will be shown to the model as worked examples before it classifies
# the real texts below.
few_shot_examples = [
    {
        "text": "EXAMPLE TEXT 1",
        "response": 'EXAMPLE RESPONSE 1',
    },
    {
        "text": "EXAMPLE TEXT 2",
        "response": 'EXAMPLE RESPONSE 2',
    },
]
1
A list of example dictionaries — each one pairs an example text with the JSON answer you’d want the model to give for it.
2
Replace this with a short example text of your own.
3
Replace this with the JSON response (as a string) you think is correct for that text, e.g. '{"fear": true, "reasoning": "..."}'.

Now we run the same loop as above, but this time we add your examples as extra turns in the conversation before the real text — each example becomes one user turn (the example text) followed by one assistant turn (the example answer), exactly how it would look if the model had already answered it.

results_fewshot = []
for text in df['text']:
    instructions = [
        {"role": "system",
         "content": "You are a trained assistant for content analysis who determines whether a text expresses fear. Always answer precisely in JSON format with fear (true or false) and reasoning (a short justification in English). Return only the JSON response, starting with '{' and ending with '}', with these two parameters."},
    ]
    for example in few_shot_examples:
        instructions.append({"role": "user", "content": example["text"]})
        instructions.append({"role": "assistant", "content": example["response"]})
    instructions.append({"role": "user", "content": text})

    outputs = generate(instructions, max_new_tokens=256, do_sample=False)
    results_fewshot.append({"text": text, "response": outputs[0]["generated_text"][-1]['content']})

results_fewshot
1
An empty list to collect one result per text, just like in the zero-shot loop.
2
Loops over every text in the dataset, one at a time.
3
Starts the conversation with the same system instructions as the zero-shot version — this list then grows with each example before the real question gets added.
4
Loops over each example you wrote above.
5
Adds the example’s text as a user turn, exactly as if it had been asked to the model.
6
Adds the example’s answer as an assistant turn right after it, as if the model had already answered it correctly.
7
After all the examples, finally adds the real text you actually want classified.
8
Runs the model on this longer conversation (system prompt + example turns + real text) — otherwise the same call as before, just with more turns.
9
Pulls the model’s reply out of the output and stores it next to the original text, exactly as in the zero-shot loop.
10
Displays the collected few-shot responses.

Parse the few-shot responses the same way as before, and compare them to the zero-shot predictions from the code-along side by side.

parsed_data_fewshot = [(entry['text'], *parse_response(entry['response'])) for entry in results_fewshot]
parsed_data_fewshot = pd.DataFrame(parsed_data_fewshot, columns=['text', 'fear', 'reasoning'])

comparison = parsed_data[['text', 'fear']].merge(
    parsed_data_fewshot[['text', 'fear']],
    on='text', suffixes=('_zero_shot', '_few_shot'),
)
comparison
1
Parses the few-shot responses the same way as the zero-shot ones, reusing the parse_response function from the code-along.
2
Puts them into a DataFrame, same as before.
3
Takes the zero-shot results from the code-along, narrowed down to just the text and fear columns, and merge()s another table onto them.
4
The table being merged on: the few-shot results, narrowed down to the same two columns.
5
on='text' says which column to match rows by — each text is looked up in both tables. suffixes renames the two resulting fear columns so you can tell them apart (fear_zero_shot vs. fear_few_shot).
6
Displays the comparison table, with one row per text and both predictions side by side.

Look at where the two columns disagree. Few-shot prompting can make a model’s answers more consistent with your specific definition of a category, and can improve how reliably it sticks to the requested format — but it isn’t free. The examples you write inevitably steer the model’s judgment (a form of researcher bias that’s easy to underestimate with just 1–2 examples), each call now sends a longer prompt through the model (this loop was already slow; more examples means more tokens, means more time), and there’s no guarantee the model is actually applying your examples’ reasoning to a new, genuinely ambiguous case rather than just imitating their tone or length.

Bonus: does a smaller model still hold up?

If you have time left, try the (zero-shot) classification from the code-along again, but with an even smaller model this time: Qwen2.5-0.5B-Instruct, a third of the size of the one we’ve used so far. Does it still reliably return valid JSON, or do more responses fail to parse than with the 1.5B model? Where it does parse, do the label decisions still look reasonable to you?

alternative_generator = pipeline(
    "text-generation",
    model="Qwen/Qwen2.5-0.5B-Instruct",
    device_map="auto",
)
1
Builds a second pipeline under a new name, so the generate model from before stays available and you can compare the two.
2
The same task as in Step 2 — we still want a decoder that generates text.
3
The only real change: the smaller 0.5B model, a third of the size of the 1.5B one used so far.
4
Again lets the library pick GPU or CPU automatically.
# TODO: reuse the loop from Step 3 (swap generate for alternative_generator) to
# classify df['text'] again, then parse the responses with parse_response and
# check how well the smaller model performs compared to the larger one.

Next

Head to block 4 for evaluation and gold standards.