Concepts / Instruction Fine-Tuning Test Datasets

Instruction Fine-Tuning Test Datasets

The generation loop visits each test entry, builds a prompt, calls generate, decodes the output, and cleans it so that only the model's answer remains.

  • Programming

Why Test Entries Need Model Answers

Once a model has been instruction-tuned, the next question is how well it follows instructions on examples it has not been trained on. A common approach is to take a held-out test set. Each entry in that set already contains an instruction, an optional input, and a reference output. The model is asked to answer every instruction, and the answers it produces are stored alongside the original entries so they can be compared later.

This article follows the practical loop that does that work. It generates a response for each entry, writes that response into the dataset, saves the result to a file, and confirms that the response landed where it should. The goal of a single pass is simple to state. The loop starts with an entry that holds an instruction, an input, and a reference output, and it must finish with a model answer attached to that same entry.

What do you think happens?

Before reading on: after the loop has produced a response for an entry, which parts of the decoded text should survive into the stored model_response field?

  • The entire decoded string, including the prompt

  • Only the text the model generated after the prompt, with the response marker and surrounding whitespace removed

  • Only the final token ID of the generated sequence

Reveal answer
Answer:

Only the text the model generated after the prompt, with the response marker and surrounding whitespace removed

The decoded string begins with the prompt, so the code slices that part off, removes the marker '### Response:' wherever it appears, and trims whitespace. What remains is the answer alone.

Tracing One Entry Through the Loop

The loop reuses the same generate method that was used earlier in the chapter. The difference is scale: instead of generating for a single example, it runs once for each entry in the test set. The loop is driven by enumerate over test_data, so every iteration has both an index i and the entry itself. A tqdm progress bar wraps the loop, which is why the run reports how many of the 110 entries have been processed.

For one entry, the body of the loop always runs the same sequence of steps. It builds a prompt from the entry, converts that prompt into token IDs, generates a continuation, decodes the full output back into text, and cleans the text down to the answer. The cleaned answer is then written back into the entry. After that, a decision checks whether more entries remain. If they do, the body repeats for the next index. If not, the loop ends. The diagram below shows the order of operations for one entry and the decision that repeats the body.

Next entry fromenumerateindex i and entryBuild promptformat_input(entry)Encode to token IDstext_to_token_ids, moved todeviceGenerate continuationat most 256 new tokensDecode full outputtoken_ids_to_textClean to answerslice, remove marker, stripStore model_responsewritten into test_data[i]More entries?Write JSON fileafter the loop endsyesno
What happens to a single test entry from the moment it is picked up until its answer is stored, and what decides whether the loop runs again?

Building the Prompt and Calling generate

The first two steps prepare the model's input. The code builds a prompt from the current entry using format_input. That prompt is then converted into token IDs with text_to_token_ids and moved to the target device. Those IDs are passed to generate, which is configured with three settings. It is told to produce at most 256 new tokens, to use the context length stored in BASE_CONFIG, and to stop when it reaches the end-of-sequence token whose ID is 50256.

Key point

The stopping condition has two parts. Generation can end because the model emits the end-of-sequence token with ID 50256, or because the 256-new-token limit is reached. Either way, the output is a sequence of token IDs, not yet text.

Decoding and Cleaning the Output

The output of generate is a sequence of token IDs that contains the prompt followed by the newly generated continuation. The code converts the whole sequence back into text with token_ids_to_text. Because the prompt was part of what was fed in, the decoded string still begins with the prompt. The answer is therefore buried at the end of the string, and cleaning is the step that isolates it.

Cleaning has three operations. First, the code slices off the first part of the string whose length equals the length of the prompt text. Second, it removes the marker '### Response:' wherever it appears. Third, it calls strip to trim surrounding whitespace. The result is the response text alone. The diagram below shows which part of the decoded string is discarded and which part is kept.

Prompt textfirst N characters, N =prompt lengthAnswer textcharacters after the promptResponse marker'### Response:' removedwherever it appearsWhitespacetrimmed by stripResponse textstored as model_responseslice offremove markerstrip
When the decoded output contains both the prompt text and the new answer text, which positions are discarded and which are kept?
Worked example

Cleaning one decoded string

Suppose the prompt text for an entry is 60 characters long, and the decoded output is the prompt followed by the model's answer. Trace how the cleaning steps reduce the decoded string to the stored response. This example is illustrative and was written for teaching.

  1. 1
    Start with the decoded string

    The string begins with the full prompt text, then contains the '### Response:' marker and the newly generated answer.

  2. 2
    Slice off the prompt

    Keep everything from position 60 onward. The first 60 characters, which match the prompt length, are removed.

  3. 3
    Remove the marker

    Any occurrence of '### Response:' in the remaining text is deleted, so only the answer's words remain alongside stray whitespace.

  4. 4
    Strip whitespace

    Leading and trailing spaces or newlines are trimmed, leaving the answer text in a clean form.

Result

The cleaned string contains only the model's answer, and that string is what gets stored under model_response.

Storing the Response Back Into the Entry

The response is not returned from the loop or collected in a separate list. Instead, the code writes it directly into the entry it came from. It uses the index i to pick the correct dictionary inside test_data and stores the cleaned text under the key model_response. Because the dictionary is modified in place, the change is visible everywhere test_data is used afterward, including the file-writing step.

Cleaned responseoutput of the cleaning stepIndex ifrom enumeratetest_data[i]the dictionary for thisentrymodel_responsenew key added to thedictionaryUpdated test_datachange visible to laterstepsassignedselectsgains keyin place
How does the cleaned answer travel from the generation step into one specific entry of the in-memory dataset, and what changes in that entry?
Key point

Storing the response in place is what keeps it paired with its original instruction, input, and reference output. The answer never travels separately from the entry it belongs to.

The Shape of the Saved JSON File

After the loop finishes, the updated test_data is written to a file named instruction-data-with-response.json. The file is opened in write mode 'w', and json.dump serializes the list into that file using an indent of 4 spaces. The indentation is there so the saved structure is easy for a person to read. The file holds a list, and each element of that list is one entry dictionary.

Each entry in the file keeps its instruction, input, and reference output, and it also carries the new model_response field. This makes the file a complete record of the test run rather than a bare list of answers. The diagram below shows how the fields nest relative to one another.

Saved fileinstruction-data-with-response.jsonList of entriesone element per test entryEntry dictionaryone record per instructioninstructionoriginal test fieldinputoriginal test fieldoutputreference outputmodel_responseadded by the loopcontainsholds
What fields does each saved record contain, and how are the original test fields and the new model response nested relative to one another?

Why the Write Happens After the Loop

The file is not written inside the loop. The write step comes after the last entry has been processed, so the complete dataset, with every model_response filled in, is serialized in one operation. Writing once at the end means the file reflects the finished run. A partially filled file is never produced by the loop body itself.

Checking the Updated Dataset

Once the run has finished, it is worth inspecting one entry directly rather than trusting that the loop worked. The source examines a single entry from the updated test set. That entry contains the original instruction, the input, the reference output, and a model_response field whose value is the model's answer. Seeing all four keys together confirms two things: the response was added to the correct dictionary, and the original fields were not disturbed.

CheckWhat to look forWhat a problem looks like
Key presenceEvery entry has a model_response keyAn entry is missing model_response
Original fieldsinstruction, input, and output match the original test setA field has changed or disappeared
PairingThe response sits in the same dictionary as its instructionA response appears under the wrong entry
Saved fileThe file contains the same entries as the in-memory datasetThe file is from an earlier run

Comparing the original test dataset with the updated one

Mistakes Beginners Make With the Test Loop

  • Common mistake

    Assuming the decoded string contains only the answer

    Why it fails

    The output of generate contains the prompt followed by the continuation, so the decoded text still begins with the prompt.

    Do this instead

    Slice off the first part of the string whose length equals the prompt length, then remove the marker and strip whitespace.

  • Common mistake

    Writing the file inside the loop

    Why it fails

    The file write is meant to happen once, after the last entry has been processed, so that the complete dataset is serialized in one operation.

    Do this instead

    Place the json.dump call after the loop ends.

  • Common mistake

    Collecting responses in a separate list and forgetting to attach them

    Why it fails

    The loop stores each response directly in the entry it came from using the index i. A separate list would separate answers from their instructions.

    Do this instead

    Assign the cleaned response to test_data[i] under the key model_response.

  • Common mistake

    Running the test again without saving the previous file

    Why it fails

    Opening the file in 'w' mode overwrites earlier output.

    Do this instead

    Rename or copy the previous instruction-data-with-response.json before starting a new run.

  • Common mistake

    Expecting the run time to change with the logic rather than the hardware: the same code runs on both machines, so the speed difference comes from the device.

Practice Tracing a Single Entry

MEDIUM

Take one entry with an instruction, an input, and a reference output. Write down, in order, the steps the loop performs on it: which function builds the prompt, what is passed to generate, how the decoded string is cleaned, and where the cleaned text is stored. Then say what the saved file would contain for that entry and at what point in the run the file is written.

Hints
  • Start from the enumerate loop and the index i.
  • Remember that the decoded string begins with the prompt, so cleaning must remove that part first.
  • The file write happens once, after every entry has been processed.

Wrapping Up the Test Run

  1. 1
    Each pass through the loop builds a prompt from one entry, generates a continuation, decodes it, and cleans it down to the answer before storing it.
  2. 2
    Cleaning removes the prompt portion of the decoded string, removes the '### Response:' marker, and trims whitespace.
  3. 3
    The cleaned response is stored under model_response in the same dictionary as its instruction, input, and reference output.
  4. 4
    The full dataset is written once, after the loop, to instruction-data-with-response.json with an indent of 4, and writing in 'w' mode overwrites earlier files.
  5. 5
    Inspecting one updated entry confirms that the response was added to the right dictionary and that the original fields are intact.

Key Takeaways

  • Each pass through the loop builds a prompt from one entry, generates a continuation, decodes it, and cleans it down to the answer before storing it.
  • Cleaning removes the prompt portion of the decoded string, removes the '### Response:' marker, and trims whitespace.
  • The cleaned response is stored under model_response in the same dictionary as its instruction, input, and reference output.
  • The full dataset is written once, after the loop, to instruction-data-with-response.json with an indent of 4, and writing in 'w' mode overwrites earlier files.
  • Inspecting one updated entry confirms that the response was added to the right dictionary and that the original fields are intact.

Related concepts

More concepts starting with I