Fine-Tuning Dataset Validator

Fine-Tuning Dataset Validator

A fictional coding-practice platform plans to fine-tune a model that writes hints without giving away solutions. Reviewers have collected example conversations and assigned each example to a split: train fits the model, validation picks a checkpoint, and test gives the final held-out score. If one exercise family, or the same conversation, sits in two splits, the held-out score rewards memory instead of skill.

Before any training run, a validator checks a narrow dataset contract. Implement validateSplits(splits). It returns the number of rows in each split as { train, validation, test }, or throws an Error whose message is exactly the message of the first check that fails.

Input

splits maps each split name to an array of rows. One row is one training example:

JavaScript
{
    id: 'h-101',          // unique across the whole dataset
    groupId: 'loops',     // exercise family: variants and paraphrases share it
    messages: [
        { role: 'system', content: 'Give a hint, not the answer.' },
        { role: 'user', content: 'My while loop never ends.' },
        { role: 'assistant', content: 'Check the loop' }, // the target the model learns
    ],
}

Nothing has been validated yet, so any value can have the wrong type.

Checks, in order

First check the container:

StepCheckMessage when it fails
1splits is an object whose keys are exactly train, validation, and test (in any order)Expected train, validation, and test

Then visit the splits in the fixed order train, validation, test, whatever the order of the keys, and the rows of each split in array order. Finish every check on one row before you move to the next row, and finish a split before you check the next one.

StepCheckMessage when it fails
2The split is a non-empty arrayEvery split must be a nonempty list
3The row is an objectRecord must be an object
4id and groupId are both strings with at least one non-whitespace characterMissing identifier
5No earlier row, in any split, has the same idDuplicate ID
6The row's groupId has not been used in a different splitGroup crosses splits
7messages is an array with at least 2 entriesExpected conversation and target
8For each message in order: it is an object, its role is system, user, or assistant, and its content is a string with at least one non-whitespace characterMessage must be an object, Unsupported role, Empty message
9The last message has role assistantFinal message must be the target
10At least one message before the last has role userMissing user input
11The row's normalized conversation has not appeared in a different splitConversation crosses splits

Step 8 runs its three checks on one message before it moves to the next message. In JavaScript, null and arrays do not count as objects for steps 1, 3, and 8.

Normalized conversation

Turn each message into the pair (role, text), where text is the content split on runs of whitespace (spaces, tabs, newlines) and rejoined with single spaces. Leading and trailing whitespace disappears. Every message counts, including system messages. Two conversations are the same when they have the same number of messages and every pair matches exactly. Keep letter case and punctuation: in code, Count and count are different names.

"Check the\tloop\n" and "Check the loop" normalize to the same text. "check the loop" and "Check the loop." do not.

What may repeat

  • An id never repeats, not even inside one split.
  • A groupId or a normalized conversation may repeat inside one split. Variants of one exercise family belong together, and same-split duplicates are left to a separate deduplication step. They fail only when they reach a second split.
  • Do not modify the input.

JavaScript and Python names

JavaScriptPython
validateSplits(splits)validate_splits(splits)
idid
groupIdgroup_id
messages, role, contentmessages, role, content
returns { train, validation, test }returns {"train": ..., "validation": ..., "test": ...}
throws new Error(message)raises ValueError(message)
object, arraydict, list

Fixture

The base dataset:

SplitidgroupIdMessages
trainh-101loopssystem: Give a hint, not the answer. / user: My while loop never ends. / assistant: Check the loop
trainh-102loopsuser: My for loop skips the last item. / assistant: Compare the range end with the last index.
validationh-201recursionsystem: Give a hint, not the answer. / user: My recursion never returns. / assistant: What is your base case?
testh-301sortinguser: My sort puts 10 before 9. / assistant: Are you comparing strings or numbers?

Each case changes the base dataset in one way:

CaseChange
1. Basenone
2. Repeated IDh-301's id becomes h-101
3. Crossing grouph-301's groupId becomes loops
4. Whitespace twinh-301's messages become h-101's, with the target written "Check the\tloop" (two spaces and a tab)
5. Empty targeth-201's target content becomes " "
6. Tool messageh-301 gets { role: 'tool', content: 'sorted: 10, 9' } just before its target
7. Empty splittest becomes []
8. Same-split twintrain gets a row h-103 in group arrays, with the messages from case 4
9. System and target onlyh-201 keeps only its system message and its target
10. Extra splita dev split with one valid row is added

Before you run the tests, predict each result: the counts, or the exact message of the first check that fails. The first test checks all ten cases.

Example

JavaScript
validateSplits({
    train: [
        { id: 'a', groupId: 'g1', messages: [{ role: 'user', content: 'Why?' }, { role: 'assistant', content: 'Look  again.' }] },
        { id: 'b', groupId: 'g2', messages: [{ role: 'user', content: 'Why?' }, { role: 'assistant', content: 'Look again.' }] },
    ],
    validation: [{ id: 'c', groupId: 'g3', messages: [{ role: 'user', content: 'How?' }, { role: 'assistant', content: 'Step through it.' }] }],
    test: [{ id: 'd', groupId: 'g1', messages: [{ role: 'user', content: 'When?' }, { role: 'assistant', content: 'Print the index.' }] }],
});
// throws Error('Group crosses splits'): group g1 is in train and in test.
// Rows a and b normalize to the same conversation, which is allowed inside train.

Constraints

  • At most 10,000 rows in total, at most 50 messages per row, and at most 10,000 characters per content.
  • Test inputs use only spaces, tabs, and newlines as whitespace.

Expected complexity

O(R + C) time and O(R + C) extra space, where R is the number of rows and C the total number of characters across all message contents.

In Python, implement validate_splits(splits) with dicts and lists, use the Python names from the table, and raise ValueError with the same messages.

MEDIUM
AI Engineering
Fine-Tuning
Hash Map / Dictionary
Machine Learning
Intermediate

0 views

Solution

Hints

Hint 1
Hint 2
Premium
Hint 3
Premium
Hint 4
Premium