Проверка данных для дообучения MLX на облачном Mac

Проверка данных для дообучения MLX на облачном Mac

Проверка данных для дообучения MLX на облачном Mac

Если вы собираетесь дообучать модель с MLX на облачном Mac, не спешите запускать обучение. После загрузки обучающей и валидационной выборок сначала стоит проверить данные: строка с некорректным JSON, пустой ответ или диалог, попавший сразу в обе выборки, могут лишить эксперимент ценности для сравнения. Ниже мы создадим воспроизводимую проверку средствами стандартной библиотеки Python — без скачивания модели и без зависимости от среды обучения.

Сначала зададим формат входных данных

В этой статье мы работаем с текстовыми диалогами: каждая строка содержит один объект с единственным полем messages, а каждое сообщение — только поля role и content. В начале допускается одно сообщение system, после которого сообщения должны строго чередоваться: user, затем assistant. Диалог обязательно заканчивается ответом. Мультимодальное содержимое, вызовы инструментов и несколько сообщений с одной ролью подряд в этот формат не входят.

Это правила приёмки данных для конкретного проекта, а не утверждение, что все инструменты обучения на MLX принимают только такую структуру. Перед запуском сверьтесь с требованиями к входным данным используемого инструмента. Если исходные данные содержат другие поля, сначала добавьте явный этап преобразования: проверяющий скрипт не должен молча отбрасывать поля.

{"messages":[{"role":"user","content":"解释训练集与验证集的区别。"},{"role":"assistant","content":"训练集用于更新参数,验证集用于观察未参与更新的数据表现。"}]}

Сохраните выборки в файлы train.jsonl и valid.jsonl в кодировке UTF-8 без BOM. Пустые строки считаются ошибкой; завершающий перевод строки в конце файла не считается дополнительной пустой строкой. Исходные файлы храните в режиме только для чтения, а исправления сохраняйте отдельно, чтобы можно было проследить изменения данных.

Проверяем формат построчно средствами стандартной библиотеки

Сначала выполните python3 --version, чтобы убедиться, что интерпретатор доступен, затем сохраните приведённый ниже код в файл check_dataset.py. Ограничение на одну строку составляет 256 КиБ. Это защитный порог для входных данных в данном проекте, а не размер контекстного окна модели. Скрипт читает файл в двоичном режиме с ограничением объёма чтения, поэтому слишком длинная строка не загружается в память целиком.

import hashlib
import json
import sys

LIMIT = 256 * 1024
seen = {}


def unique_object(pairs):
    result = {}
    for key, value in pairs:
        if key in result:
            raise ValueError("duplicate object key")
        result[key] = value
    return result


def reject_constant(value):
    raise ValueError("nonstandard JSON constant")


def validate(obj):
    if not isinstance(obj, dict) or set(obj) != {"messages"}:
        raise ValueError("expected messages-only object")
    messages = obj["messages"]
    if not isinstance(messages, list):
        raise ValueError("messages must be an array")
    roles = []
    for message in messages:
        if not isinstance(message, dict):
            raise ValueError("message must be an object")
        if set(message) != {"role", "content"}:
            raise ValueError("unexpected message fields")
        role = message["role"]
        content = message["content"]
        if not isinstance(role, str):
            raise ValueError("role must be text")
        if not isinstance(content, str) or not content.strip():
            raise ValueError("content must be nonempty text")
        roles.append(role)
    body = roles[1:] if roles[:1] == ["system"] else roles
    if len(body) < 2 or len(body) % 2:
        raise ValueError("expected complete conversation pairs")
    if body != ["user", "assistant"] * (len(body) // 2):
        raise ValueError("invalid role order")
    return messages


def inspect(path):
    count = 0
    with open(path, "rb") as stream:
        while True:
            raw = stream.readline(LIMIT + 1)
            if not raw:
                break
            count += 1
            try:
                if len(raw) > LIMIT:
                    raise ValueError("line exceeds byte limit")
                obj = json.loads(
                    raw.decode("utf-8"),
                    object_pairs_hook=unique_object,
                    parse_constant=reject_constant,
                )
                messages = validate(obj)
                canonical = json.dumps(
                    messages, ensure_ascii=False,
                    sort_keys=True, separators=(",", ":"),
                ).encode("utf-8")
                digest = hashlib.sha256(canonical).hexdigest()
                if digest in seen:
                    raise ValueError(
                        "duplicate conversation; first at " + seen[digest]
                    )
                seen[digest] = f"{path}:{count}"
            except (ValueError, RecursionError) as error:
                raise ValueError(f"{path}:{count}: {error}") from error
    if count == 0:
        raise ValueError(f"{path}: empty dataset")
    return count


if __name__ == "__main__":
    if len(sys.argv) != 3:
        raise SystemExit("usage: check_dataset.py TRAIN VALID")
    try:
        counts = [inspect(path) for path in sys.argv[1:]]
    except (OSError, ValueError) as error:
        print(str(error), file=sys.stderr)
        raise SystemExit(1)
    print(json.dumps({"train": counts[0], "valid": counts[1]}))

Ищем дубликаты внутри выборок и между ними

Передайте скрипту сначала обучающую, затем валидационную выборку. Для обоих файлов используется общий индекс хешей, поэтому дубликаты как внутри одной выборки, так и между выборками приводят к ошибке с указанием места первого появления диалога. Скрипт останавливается на первой ошибке; после исправления запустите его снова. Содержимое диалога целиком в вывод не попадает.

python3 check_dataset.py train.jsonl valid.jsonl

Перед вычислением хеша ключи объектов сортируются, а различия в форматировании JSON устраняются. При этом порядок сообщений и символы их содержимого сохраняются. Изменение отступов или порядка ключей не позволит обойти проверку, но добавление пробелов в текст сообщения может дать другой хеш. Скрипт намеренно не приводит текст к одному регистру и не выполняет нормализацию Unicode, чтобы не изменить код, формулы или смысл исходного текста.

Удаление точных дубликатов не гарантирует отсутствия утечки данных. Разные пары вопросов и ответов из одного документа, а также примеры с одинаковым вопросом и переформулированным ответом всё ещё могут оказаться в разных выборках.

Если у исходных документов есть идентификаторы, сначала разделите данные на выборки по документам и лишь затем формируйте диалоги. Если идентификаторов нет, как минимум дополнительно проверьте повторяющиеся вопросы и вручную просмотрите часть примеров. Успешная проверка хешей сама по себе не доказывает независимость валидационной выборки. Индекс хешей растёт вместе с количеством примеров; для очень больших наборов данных следует использовать дисковый индекс.

Длину в токенах проверяем отдельно

Ограничение в байтах служит только для защиты от аномального ввода: перевести его в фиксированное число токенов нельзя. Для китайского текста, кода и эмодзи соотношение байтов и токенов различается, а шаблон чата дополнительно вставляет маркеры ролей и другие специальные токены.

Для проверки длины используйте токенизатор целевой модели. Обработайте весь диалог с тем шаблоном чата, который фактически применяет инструмент обучения, и только затем подсчитайте токены. Зафиксируйте идентификатор ревизии модели, шаблон, максимальную длину последовательности и количество примеров, превысивших лимит. Недостаточно отдельно токенизировать каждое поле content и сложить результаты.

Для слишком длинных примеров нужно явно выбрать способ обработки: отклонение, разбиение или усечение. Если используется усечение, проверьте, не удаляется ли ответ целиком и остаётся ли полезная часть ответа под маской вычисления функции потерь. Примеры без единого токена, участвующего в обучении, необходимо отклонять. Универсальной команды усечения здесь нет: разные средства запуска обучения по-разному применяют шаблоны и маски.

Проверяем скрипт на ошибочных примерах и подключаем к обучению

Сначала на копиях примеров создайте четыре ошибочных случая: пустой ответ, неправильный порядок ролей, повторяющийся ключ объекта и одинаковый диалог в разных файлах. Проверьте каждый случай отдельно: скрипт должен возвращать код завершения 1 и указывать файл и номер строки. Затем проверьте успешный сценарий на двух разных корректных диалогах — иначе можно не заметить, что скрипт выдаёт ошибку всегда.

При подключении к скрипту эксперимента свяжите проверку и последующее обучение через && либо явно проверяйте код завершения. После неудачной проверки обучение продолжаться не должно. Когда данные зафиксированы, сохраните SHA-256 обоих файлов, количество примеров, результаты проверки длины и версию проверяющего скрипта. Для этого запуска обучения используйте только проверенную копию данных.

Результат приёмки — не фраза «с данными всё в порядке», а воспроизводимые свидетельства: одинаковый ввод даёт одинаковый результат, ошибки можно локализовать, разделение на выборки обосновано, а обработка длины задокументирована. Только после этого стоит тратить время на обучение.

Часто задаваемые вопросы

Можно ли запускать обучение сразу после успешной проверки JSONL?

Нет. Дополнительно проверьте формат входных данных инструмента обучения и длину последовательностей с токенизатором и шаблоном диалога выбранной модели.

Поможет ли перестановка ключей JSON скрыть дубликат?

Нет. Перед вычислением SHA-256 ключи объектов сортируются. Однако перефразированные и семантически похожие диалоги требуют отдельной проверки.

Аренда Mac на время эксперимента

Выделенный Mac mini M4: 16 ГБ объединённой памяти и SSD на 256 ГБ. Предоплата за день, неделю, месяц или квартал. Если не продлить аренду, по её окончании сервер остановится. Модели и среду выполнения подготовьте самостоятельно; фактическая доступность ресурсов отображается в консоли в реальном времени.

Арендовать Mac на день

Сначала проверьте конфигурацию, затем начинайте эксперимент

Проверьте веса модели, пиковую нагрузку и срок аренды, а затем выберите период. Инструкции по подключению можно найти в центре помощи.