Перейти к содержимому
ПереводыStanford

CS336, задание 4 (data): фильтрация данных для языковых моделей — путеводитель и перевод условия

Четвёртое задание Stanford CS336 (весна 2026) в переводе: из сырого Common Crawl — в обучающий набор. Извлечение текста из HTML, определение языка, маскирование персональных данных, фильтры вредного контента, правила Gopher, классификатор качества, точная и MinHash-дедупликация, а затем соревнование: собрать данные, на которых модель лучше всего предсказывает C4. Плюс как сделать задание без кластера Стэнфорда на 4 × RTX 4080.

Задание 4 — о данных: как из сырых веб-страниц Common Crawl получить текст, на котором стоит учить языковую модель. Модели вы почти не трогаете: архитектура и обучение зафиксированы, соревнуетесь вы только качеством данных.

РазделЧто делаетеБаллыУрок курса
2.1 look_at_ccСмотрите на сырые WARC и WET45.1.2
2.2 extract_textHTML → текст (Resiliparse)35.1.2
2.3 language_identificationОпределение языка (fastText lid.176)64.2.3
2.4 mask_piiМаскирование e-mail, телефонов, IP39.3.5
2.5 harmful_contentКлассификаторы NSFW и токсичности69.3.5
2.6 gopher_quality_filtersПравила качества из статьи о Gopher35.1.2
2.7 quality_classifierСвой классификатор качества155.1.2
3.1 exact_deduplicationТочная дедупликация строк3лекция 14
3.2 minhash_deduplicationНечёткая дедупликация MinHash + LSH8лекция 14
4 filter_data, inspect, tokenizeСвой конвейер, разбор, токенизация6 + 4 + 24.2.3
4 train_modelОбучение модели на своих данных85.6.2

Всего 71 балл; дальше идёт перевод условия.


В этом задании вы получите практический опыт фильтрации веб-краулов, чтобы подготовить данные для обучения языковых моделей.

Что вы реализуете.

  1. Преобразование HTML из Common Crawl в текст.
  2. Фильтрацию извлечённого текста разными методами (вредный контент, персональные данные и т. д.).
  3. Дедупликацию обучающих данных.

Что вы запустите. Обучение языковых моделей на разных наборах данных — чтобы лучше понять, как отдельные решения при обработке влияют на качество.

Как устроен код. Весь код задания и это условие лежат на GitHub: github.com/stanford-cs336/assignment4-data. Склонируйте репозиторий; если будут обновления, мы сообщим, и вы сможете сделать git pull.

  1. cs336_basics/* — код обучения модели, которую мы строили в задании 1. Модель немного оптимизирована: часть наших самодельных компонентов из задания 1 заменена родными аналогами из PyTorch (например, встроенным ядром кросс-энтропии). Добавлен обучающий скрипт с поддержкой распределённого обучения с параллелизмом по данным на нескольких GPU. Этим скриптом вы обучите модель на своих отфильтрованных данных.
  2. cs336_data/* — здесь вы пишете свой код для задания 4. Мы создали модуль cs336_data с заготовками.
  3. tests/*.py — все тесты, которые нужно пройти. Они вызывают «переходники» (hooks) из tests/adapters.py; вы реализуете переходники, чтобы связать свой код с тестами. Писать дополнительные тесты и менять тестовый код для отладки полезно, но ваша реализация должна проходить исходный набор тестов.
  4. README.md — подробности о структуре каталогов и базовые инструкции по настройке окружения.

Что сдать (в Gradescope): writeup.pdf — ответы на все письменные вопросы (набранные, не от руки), и code.zip — весь написанный вами код.

Большие языковые модели учатся в основном на данных из интернета, но мало кто из исследователей пишет собственный веб-краулер. Обычно берут публичные краулы. Самый популярный — от некоммерческой организации Common Crawl, которая бесплатно раздаёт корпус веб-страниц: «более 250 миллиардов страниц за 17 лет».

Однако, как мы увидим, превратить дампы Common Crawl (CC) в пригодные для обучения данные — большая работа. Например, сырые страницы хранятся в HTML, а нам нужен текст. Кроме того, многие страницы низкого качества, являются точными или почти точными копиями, содержат вредный контент или чувствительную информацию — такие страницы хочется отфильтровать или вырезать из них нежелательные части. В этом задании мы соберём конвейер из нескольких таких шагов, превращающий сырые данные из интернета в пригодный обучающий набор.

Прежде чем что-то реализовывать, всегда полезно посмотреть на сырые данные. Common Crawl публикует их в трёх форматах:

  • WARC («Web ARChive») — сырые данные краула: идентификаторы и URL страниц, метаданные и детали HTTP-запроса (дата и время, IP-адрес сервера) и, конечно, сырое содержимое страницы (например, HTML).
  • WAT («Web Archive Transformation») — метаданные более высокого уровня, извлечённые из WARC и сохранённые как JSON. Для HTML-страниц это, например, список ссылок и заголовок страницы.
  • WET («Web Extracted Text») — простой текст, извлечённый из сырого HTML.

В следующей задаче мы посмотрим на один WARC-файл и соответствующий ему WET-файл. Они взяты из краула Common Crawl 2026 года и скачаны так:

Окно терминала
# Скачиваем пример WARC-файла
$ wget https://data.commoncrawl.org/crawl-data/CC-MAIN-2026-12/segments/1741613651012.6/warc/CC-MAIN-20260310152245-20260310182245-00000.warc.gz
# Скачиваем соответствующий WET-файл
$ wget https://data.commoncrawl.org/crawl-data/CC-MAIN-2026-12/segments/1741613651012.6/wet/CC-MAIN-20260310152245-20260310182245-00000.warc.wet.gz

На кластере курса они лежат в /shared-data/CC/example.warc.gz и /shared-data/CC/example.warc.wet.gz.

(a) Откройте WARC-файл. Это gzip-архив, листать его можно так:

Окно терминала
$ zcat /shared-data/CC/example.warc.gz | less

В less файл листается стрелками, Page Up и Page Down; выход — клавиша «q». Посмотрите на самую первую веб-страницу. Какой у неё URL? Доступна ли она сейчас? Можно ли по сырому HTML понять, о чём страница? Сдать: ответ в 2–3 предложения.

(b) Теперь откройте соответствующий WET-файл:

Окно терминала
$ zcat /shared-data/CC/example.warc.wet.gz | less

В WET-файлах есть HTTP-заголовки (например, Content-Length), которые не входят в извлечённый текст. В первом примере вы увидите текст, извлечённый из только что просмотренного HTML. Заметьте: многое в извлечённом тексте напоминает структуру HTML, а не основное содержание страницы. Какие части, по-вашему, экстрактор должен был выбросить? Подумайте о качестве этого текста как обучающих данных: что может пойти не так при обучении на таком тексте? И наоборот: какую полезную информацию модель может отсюда извлечь? Сдать: ответ в 3–4 предложения.

(c) Что считать хорошим обучающим примером, сильно зависит от контекста. Опишите прикладную область, для которой этот пример полезно иметь в обучающих данных, и область, где он бесполезен. Сдать: 1–2 предложения.

(d) Посмотрите ещё 25 записей WET. Для каждой очень коротко отметьте язык документа (если можете определить), доменное имя, тип страницы и т. п. Сколько примеров пришлось просмотреть, прежде чем попалась страница, которую вы назвали бы «качественной»? Сдать: краткие пометки к 25 документам (язык, домен, тип страницы, прочие наблюдения) и число примеров до первого качественного.

Как вы уже заметили, извлекать текст из HTML трудно. Любая процедура извлечения ищет видимое содержимое (например, теги <p>, в которых должны быть блоки текста). Но и так извлекается гораздо больше, чем мы воспринимаем как основное содержимое страницы в браузере. Например, на StackOverflow основное содержимое — вопрос и ответы, но пункты меню, ссылки на другие сайты StackExchange, подвал, ссылки «зарегистрироваться» и «войти» — всё это тоже видимый текст, и надёжно отличить его от основного содержимого непросто.

Конвейеров извлечения текста много. В этом задании мы используем библиотеку Resiliparse. Она поможет и с более базовой проблемой — определением кодировки байтов страницы. Хотя большинство страниц в вебе в UTF-8 (98,2% по данным Википедии), наш конвейер должен справляться и с другими кодировками.

Для перебора записей WARC-файла советуем библиотеку FastWARC, в частности классы:

from fastwarc.warc import ArchiveIterator, WarcRecordType

(a) Напишите функцию, которая извлекает текст из байтовой строки с сырым HTML. Для извлечения используйте resiliparse.extract.html2text.extract_plain_text. Эта функция принимает строку, поэтому байты сначала надо декодировать в Unicode. Учтите, что входные байты могут быть не в UTF-8: если UTF-8 не подошла, функция должна определить кодировку. Может пригодиться resiliparse.parse.encoding.detect_encoding(). Сдать: функцию «байты HTML → извлечённый текст»; реализуйте переходник run_extract_text_from_html_bytes и пройдите uv run pytest -k test_extract_text_from_html_bytes.

(b) Запустите свою функцию на одном WARC-файле и сравните результат с текстом из соответствующего WET-файла. Что различается и что совпадает? Какое извлечение лучше? Сдать: 2–3 предложения.

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

Для этого полезна библиотека fastText (fasttext.cc) — быстрые текстовые классификаторы. В ней есть и инфраструктура для обучения своих классификаторов, и коллекция готовых моделей, в том числе для определения языка. Модель lid.176.bin можно скачать со страницы fastText; на кластере она лежит в /shared-data/classifiers/lid.176.bin.

Обычно языковой фильтр решает, оставить ли страницу, по оценке классификатора. Реализуйте с помощью fastText фильтр определения языка, который возвращает неотрицательную оценку уверенности в своём прогнозе.

Problem (language_identification): определение языка (6 баллов)

Заголовок раздела «Problem (language_identification): определение языка (6 баллов)»

(a) Напишите функцию, которая принимает строку Unicode и определяет основной язык текста. Она возвращает пару: идентификатор языка и оценку от 0 до 1 — уверенность в прогнозе. Сдать: функцию с лучшим прогнозом языка и его оценкой; реализуйте переходник run_identify_language и пройдите оба теста uv run pytest -k test_identify_language. Тесты ожидают идентификаторы "en" для английского и "zh" для китайского — если нужно, сделайте перекодировку в переходнике.

(b) Поведение модели на инференсе во многом определяется данными, на которых она училась, поэтому ошибки в конвейере фильтрации отзываются ниже по течению. Какие проблемы могут возникнуть из-за ошибок определения языка? Как бы вы смягчали их в ситуации с высокими ставками (например, при запуске продукта для пользователей)? Сдать: 2–5 предложений.

(c) Запустите определитель языка на тексте, извлечённом из WARC-файлов вашей функцией. Вручную определите язык в 20 случайных примерах и сравните с прогнозами классификатора. Укажите ошибки классификатора. Какая доля документов на английском? Какой порог уверенности вы бы выбрали для фильтрации? Сдать: 2–5 предложений.

В вебе много информации, по которой можно связаться с человеком или опознать его: адреса электронной почты, номера телефонов, IP-адреса. Мы, вероятно, не хотим, чтобы модель, с которой говорят пользователи, выдавала такую информацию о реальных людях, поэтому её обычно маскируют в обучающих данных. Вы реализуете три процедуры маскирования: (a) адресов электронной почты, (b) номеров телефонов, (c) IP-адресов.

(a) Напишите функцию, которая заменяет все адреса электронной почты в строке на |||EMAIL_ADDRESS|||. Для поиска адресов можно найти надёжные регулярные выражения. Сдать: функцию, возвращающую пару «новая строка, число замен»; переходник run_mask_emails, тесты uv run pytest -k test_mask_emails.

(b) Напишите функцию, которая заменяет номера телефонов на |||PHONE_NUMBER|||. Сделать это надёжно крайне трудно: номера пишут в самых разных форматах. Постарайтесь покрыть хотя бы самые распространённые форматы номеров США и быть устойчивыми к мелким синтаксическим отклонениям. Сдать: функцию, возвращающую пару «новая строка, число замен»; переходник run_mask_phone_numbers, тесты uv run pytest -k test_mask_phones.

(c) Напишите функцию, которая заменяет IP-адреса на |||IP_ADDRESS|||. Достаточно IPv4 (четыре числа до 255, разделённые точками). Сдать: функцию, возвращающую пару «новая строка, число замен»; переходник run_mask_ips, тесты uv run pytest -k test_mask_ips.

(d) Какие проблемы могут возникнуть в языковой модели, если наивно применить эти фильтры к обучающему набору? Как их смягчить? Сдать: 2–5 предложений.

(e) Запустите маскирование на тексте из WARC-файлов. Просмотрите 20 случайных примеров, где была замена; приведите примеры ложных срабатываний и пропусков. Сдать: 2–5 предложений.

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

Подходов много: от подсчёта слов из «чёрного списка» до простых классификаторов, обученных на разметке людей. Мы сосредоточимся на двух широких категориях: NSFW («небезопасно для работы» — порнография, нецензурная лексика, другой потенциально травмирующий контент) и токсичная речь («грубые, неуважительные или неразумные высказывания, из-за которых человек, скорее всего, уйдёт из обсуждения»). Мы используем готовые модели fastText из проекта Dolma [1]. Они обучены на наборе Jigsaw Toxic Comments — комментариях из Википедии, размеченных по множеству категорий.

Классификаторы можно скачать с dolma-artifacts.org (jigsaw_fasttext_bigrams_nsfw_final.bin и jigsaw_fasttext_bigrams_hatespeech_final.bin); на кластере они лежат в общем каталоге:

  • /shared-data/classifiers/dolma_fasttext_hatespeech_jigsaw_model.bin — классификатор ненависти и токсичной речи;
  • /shared-data/classifiers/dolma_fasttext_nsfw_jigsaw_model.bin — классификатор NSFW.

Реализуйте с их помощью функцию, которая принимает строку Unicode с содержимым страницы и возвращает метку (например, «toxic» / «non-toxic») и оценку уверенности.

(a) Напишите функцию, определяющую NSFW-контент. Сдать: функцию, возвращающую пару «метка, уверенность»; переходник run_classify_nsfw, тест uv run pytest -k test_classify_nsfw. Этот тест — лишь проверка на здравый смысл на примере из Jigsaw; он вовсе не доказывает, что классификатор точен, — это нужно проверить самим.

(b) Напишите функцию, определяющую токсичную речь. Сдать: функцию, возвращающую пару «метка, уверенность»; переходник run_classify_toxic_speech, тест uv run pytest -k test_classify_toxic_speech (тоже лишь проверка на здравый смысл из Jigsaw).

(c) Какие проблемы могут возникнуть в языковой модели, если применить эти фильтры при создании обучающего набора? Как их смягчить? Сдать: 2–5 предложений.

(d) Запустите фильтры на тексте из WARC-файлов. Просмотрите 20 случайных примеров и сравните прогнозы с вашими оценками. Укажите ошибки классификатора. Какая доля документов вредная? Какие пороги уверенности вы бы выбрали? Сдать: 2–5 предложений.

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

  • страницы с платным доступом;
  • страницы-заглушки битых ссылок;
  • формы входа, регистрации или обратной связи;
  • страницы, содержимое которых в основном нетекстовое (фото, видео) и теряется при извлечении текста.

В статье о модели Gopher [2] описан набор простых фильтров качества, убирающих подобные очевидные случаи. Это простые понятные эвристики, которые часто покрывают много заведомо непригодных примеров. Они основаны на длине документа, длине слов, отношении числа символов к числу слов и наличии некоторых служебных английских слов (stop words). Вы реализуете подмножество этих фильтров: удаляйте документы, которые

  • содержат меньше 50 или больше 100 000 слов;
  • имеют среднюю длину слова вне диапазона от 3 до 10 символов;
  • содержат больше 30% строк, оканчивающихся многоточием («…»);
  • содержат меньше 80% слов хотя бы с одной буквой.

Полное описание всех фильтров Gopher — в приложении A статьи.

Problem (gopher_quality_filters): фильтры качества Gopher (3 балла)

Заголовок раздела «Problem (gopher_quality_filters): фильтры качества Gopher (3 балла)»

(a) Реализуйте (как минимум) описанное подмножество фильтров. Для разбиения текста на слова может пригодиться пакет NLTK (nltk.word_tokenize), но это не обязательно. Сдать: функцию, которая принимает строку и возвращает булево значение «текст проходит фильтры Gopher»; переходник run_gopher_quality_filter, тесты uv run pytest -k test_gopher.

(b) Запустите правила на тексте из WARC-файлов. Просмотрите 20 случайных примеров и сравните решения фильтра со своими. Прокомментируйте случаи расхождения. Сдать: 2–5 предложений.

Пойдём дальше простых синтаксических правил Gopher. Ранжировать контент по качеству нужно не только при обучении языковых моделей: это фундаментальная задача информационного поиска. Один из классических сигналов поисковых систем — структура ссылок: качественные страницы чаще ссылаются на качественные [3]. OpenAI опиралась на похожую идею, собирая WebText — набор данных, на котором обучалась GPT-2 [4]: брали страницы, на которые ссылались комментарии на Reddit с «кармой» выше порога. Альтернатива Reddit — Википедия как источник качественных ссылок: внешние источники, на которые ссылаются статьи Википедии, обычно заслуживают доверия [5].

Контролируемый источник обычно даёт качественный контент, но набор получается маленьким по нынешним меркам (в OpenWebText 40 ГБ текста, а The Pile больше в 20 с лишним раз). Один из подходов — взять такие эталонные страницы как положительные примеры, (случайные) страницы Common Crawl — как отрицательные и обучить классификатор fastText. Он даёт оценку качества, по которой можно фильтровать весь Common Crawl. Порог качества — это компромисс между точностью (precision) и полнотой (recall).

В этой части вы построите классификатор качества. Мы извлекли URL внешних ссылок из свежего дампа Википедии и положили их в /shared-data/wiki/enwiki-20260501-extracted_urls.txt.gz: это внешние ссылки со страниц английской Википедии на май 2026 года. Предполагается, что вы возьмёте из них подвыборку как положительные примеры «качественного» текста. Учтите, что и среди положительных примеров может встречаться нежелательный контент, так что полезно применить к ним уже построенные инструменты (определение языка, правила качества и т. д.). Скачать содержимое страниц по списку URL в формате WARC можно так:

Окно терминала
wget --timeout=5 \
-i subsampled_positive_urls.txt \
--warc-file=subsampled_positive_urls.warc \
-O /dev/null

Problem (quality_classifier): классификатор качества (15 баллов)

Заголовок раздела «Problem (quality_classifier): классификатор качества (15 баллов)»

(a) Обучите классификатор качества, который по тексту возвращает числовую оценку качества. Сдать: классификатор для следующего пункта.

(b) Напишите функцию, которая помечает страницу как качественную или нет и даёт оценку уверенности. Сдать: функцию, принимающую строку и возвращающую пару «метка (качественная или нет), уверенность»; переходник run_classify_quality. Для проверки на здравый смысл убедитесь, что два наших примера классифицируются правильно: uv run pytest -k test_classify_quality.

В вебе очень много повторяющегося контента. Часть страниц — точные копии друг друга: вспомните архивы или страницы по умолчанию, которые генерируют стандартные инструменты (например, страницы 404 популярных веб-серверов). Но большая часть дублирования — более мелкого масштаба. Возьмём все страницы вопросов на Stack Overflow: у каждой уникальное содержимое (вопрос, комментарии, ответы), но у всех много общего — шапка, меню, подвал, которые повторяются дословно на каждой странице. Сначала разберёмся с таким точным дублированием, затем — с приблизительным.

Простой способ убрать точные повторы — оставить в документе только строки, уникальные во всём корпусе. Оказывается, этого хватает, чтобы убрать значительную часть избыточности вроде шапок и меню. В простых случаях после удаления строк, дословно повторяющихся где-то ещё, остаётся уникальное основное содержимое страницы (вопрос и ответы на StackOverflow).

Для этого делаем один проход по корпусу и считаем, сколько раз встречается каждая строка. Во втором проходе переписываем каждый документ, сохраняя только уникальные строки. Наивно структура со счётчиками займёт столько памяти, сколько нужно для хранения всех уникальных строк корпуса. Простой трюк для экономии — использовать в качестве ключа хеш строки: тогда ключи имеют фиксированный размер, не зависящий от длины строки.

Problem (exact_deduplication): точная дедупликация строк (3 балла)

Заголовок раздела «Problem (exact_deduplication): точная дедупликация строк (3 балла)»

Напишите функцию, которая принимает список путей к входным файлам и выполняет точную дедупликацию строк: сначала считает частоту каждой строки в корпусе (используя хеш для экономии памяти), затем переписывает каждый файл, оставляя только уникальные строки. Сдать: функцию с двумя аргументами — (a) список путей к входным файлам, (b) выходной каталог. Каждый входной файл переписывается в выходной каталог под тем же именем, но без строк, встречающихся во всём наборе файлов больше одного раза. Например, для входов a/1.txt, a/2.txt и выходного каталога b/ функция пишет b/1.txt и b/2.txt. Переходник run_exact_line_deduplication, тесты uv run pytest -k test_exact_line_deduplication.

Точная дедупликация убирает дословные повторы, но не справляется с документами, которые слегка различаются. Возьмём тексты лицензий: их часто генерируют по шаблону, куда подставляют год и имя автора. Файл лицензии одного проекта под MIT почти совпадает с файлом другого, но точной копией не является. Чтобы убрать такой повторяющийся, в основном шаблонный контент, нужна нечёткая дедупликация. Для эффективной нечёткой дедупликации на уровне документов мы используем minhash с хешированием, чувствительным к близости (locality sensitive hashing, LSH).

Меру похожести документов возьмём такую: коэффициент Жаккара между множествами n-грамм документов. Для множеств SS и TT он равен

J(S,T)=∣S∩T∣∣S∪T∣.J(S, T) = \frac{\lvert S \cap T \rvert}{\lvert S \cup T \rvert}.

Наивно можно представить каждый документ множеством n-грамм, посчитать коэффициент Жаккара для всех пар документов и считать дубликатами пары выше порога. Но для больших коллекций (вроде Common Crawl) это непрактично, а хранение множества n-грамм занимает куда больше памяти, чем сам документ.

MinHash. Чтобы решить проблему памяти, заменим множество n-грамм сигнатурой. Мы хотим, чтобы сравнение сигнатур двух документов давало приближение коэффициента Жаккара их множеств n-грамм. Этим свойством обладают minhash-сигнатуры. Для множества n-грамм документа S={s1,…,sn}S = \lbrace s_1, \ldots, s_n \rbrace понадобятся kk различных хеш-функций h1,…,hkh_1, \ldots, h_k (это могут быть функции одного семейства с разными «зёрнами»: например, MurmurHash3 — семейство, а конкретное зерно задаёт конкретную функцию). Каждая хеш-функция отображает n-грамму в целое число. Minhash множества SS по функции hih_i:

minhash⁡(hi,S)=min⁡(hi(s1),hi(s2),…,hi(sn)).\operatorname{minhash}(h_i, S) = \min\big(h_i(s_1), h_i(s_2), \ldots, h_i(s_n)\big).

Сигнатура документа — вектор из kk чисел, ii-й элемент которого — minhash множества SS по случайной функции hih_i: [minhash⁡(h1,S),…,minhash⁡(hk,S)]\big[\operatorname{minhash}(h_1, S), \ldots, \operatorname{minhash}(h_k, S)\big].

Оказывается, коэффициент Жаккара двух множеств S1S_1 и S2S_2 приближается долей позиций, где minhash совпадает (доказательство — в разделе 3.3.3 главы 3 книги [6]). Например, для сигнатур [1,2,3,2][1, 2, 3, 2] и [5,2,3,4][5, 2, 3, 4] оценка коэффициента Жаккара — 2/42/4: совпадают вторая и третья позиции.

LSH. Minhash даёт компактное представление, сохраняющее ожидаемую похожесть, но сравнивать всё равно пришлось бы все пары. LSH позволяет эффективно раскладывать по «корзинам» документы, которые, вероятно, похожи. Делим сигнатуру длины kk на bb полос по rr значений, k=brk = br. Например, сигнатуру из 100 элементов (100 случайных хеш-функций) можно разбить на 2 полосы по 50, на 4 по 25, на 50 по 2 и т. д. Если у двух документов совпадают все значения в какой-то полосе, они попадают в одну корзину и считаются кандидатами в дубликаты. Значит, при фиксированной длине сигнатуры увеличение числа полос повышает полноту и снижает точность.

Пример. У документа D1D_1 сигнатура [1,2,3,4,5,6][1, 2, 3, 4, 5, 6], у D2D_2 — [1,2,3,5,1,2][1, 2, 3, 5, 1, 2]. При 3 полосах по 2 значения полосы D1D_1 — [1,2][1, 2], [3,4][3, 4], [5,6][5, 6], полосы D2D_2 — [1,2][1, 2], [3,5][3, 5], [1,2][1, 2]. Первая полоса совпадает, поэтому по ней документы попадают в одну корзину; по остальным — нет. Но раз они совпали хотя бы в одной полосе, они кандидаты в дубликаты независимо от остальных полос.

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

Наконец, дубликаты из разных корзин объединяются в кластеры. Пусть документы A и B совпали в одной корзине и их настоящий коэффициент Жаккара выше порога, а B и C совпали в другой корзине и тоже выше порога. Тогда A, B и C — один кластер. В каждом кластере случайно оставляем один документ, остальные удаляем.

Problem (minhash_deduplication): дедупликация MinHash + LSH (8 баллов)

Заголовок раздела «Problem (minhash_deduplication): дедупликация MinHash + LSH (8 баллов)»

Напишите функцию, которая принимает список путей к входным файлам и выполняет нечёткую дедупликацию документов с minhash и LSH: считает minhash-сигнатуры каждого документа, с заданным числом полос LSH находит кандидатов в дубликаты, затем считает для кандидатов настоящий коэффициент Жаккара по n-граммам и удаляет те, что выше заданного порога. Чтобы повысить полноту (следуя [7]), нормализуйте текст перед вычислением сигнатур и/или коэффициента Жаккара: приведите к нижнему регистру, уберите пунктуацию, нормализуйте пробелы, уберите диакритические знаки и примените Unicode-нормализацию NFD.

Сдать: функцию, принимающую как минимум (a) список путей к входным файлам, (b) число хеш-функций для сигнатур, (c) число полос LSH, (d) длину n-граммы (в словах), (e) выходной каталог. Можно считать, что число хешей делится на число полос нацело. Функция переписывает каждый входной файл в выходной каталог под тем же именем, оставляя только документы, которые (a) не являются кандидатами в дубликаты и/или (b) случайно выбраны для сохранения из кластера. Переходник run_minhash_deduplication, тесты uv run pytest -k test_minhash_deduplication.

Мы реализовали целый набор инструментов для фильтрации веб-краула — пора применить их и получить обучающие данные.

Ваша цель — отфильтровать набор WET-файлов Common Crawl. Начнёте вы с WET-файлов, уже отфильтрованных по языку: оставлены документы, у которых вероятность английского по модели lid.176.bin не ниже 70%. Эти файлы лежат в /shared-data/english-wet-data.

Конкретно: нужно получить данные, обучившись на которых трансформерная языковая модель даёт минимальную перплексию на валидации на подмножестве C4 100 domains бенчмарка Paloma [8]. Архитектуру модели и процедуру обучения менять нельзя — цель в том, чтобы построить лучшие данные. Подмножество содержит примеры со 100 самых частых доменов набора C4 [9]. Копия (токенизированная токенизатором GPT-2) лежит в /shared-data/tokenized_paloma_c4_100_domains_validation.bin — полезно полистать её, чтобы понять, как выглядят эти данные. Загрузить можно так:

import numpy as np
data = np.fromfile(
"/shared-data/tokenized_paloma_c4_100_domains_validation.bin",
dtype=np.uint16
)
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("gpt2")
print(tokenizer.decode(data[0:2000]))

На ваших отфильтрованных данных вы обучите модель формы GPT-2 small и оцените её перплексию на C4 100.

Даже этот набор WET-файлов — большой объём данных. Для эффективной обработки советуем везде, где возможно, использовать несколько процессов, например API Python concurrent.futures или multiprocessing. Минимальный пример распараллеливания функции по процессам через concurrent.futures:

import concurrent.futures
import os
import pathlib # в оригинале этого импорта нет — без него пример падает
from tqdm import tqdm
def process_single_wet_file(input_path: str, output_path: str):
# TODO: прочитать input_path, обработать и записать результат в output_path
return output_path
# Настраиваем исполнителя
num_cpus = len(os.sched_getaffinity(0))
executor = concurrent.futures.ProcessPoolExecutor(max_workers=num_cpus)
wet_filepaths = ["a.warc.wet.gz", "b.warc.wet.gz", "c.warc.wet.gz"]
output_directory_path = "/path/to/output_directory/"
futures = []
for wet_filepath in wet_filepaths:
# Для каждого пути warc.wet.gz отправляем задачу исполнителю и получаем future
wet_filename = str(pathlib.Path(wet_filepath).name)
future = executor.submit(
process_single_wet_file,
wet_filepath,
os.path.join(output_directory_path, wet_filename)
)
# Сохраняем future
futures.append(future)
# Перебираем future по мере завершения, следя за прогрессом
for future in tqdm(
concurrent.futures.as_completed(futures),
total=len(wet_filepaths),
):
output_file = future.result()
print(f"Output file written: {output_file}")

Для больших кусков текста используйте локальный параллелизм (concurrent.futures или multiprocessing). Чтобы распараллелить работу по машинам в Modal, вызывайте .map(...) на входах. Для перебора записей WET советуем FastWARC, а для извлечения доменов из URL (чтобы фильтровать по ним) — библиотеку tldextract:

from fastwarc.warc import ArchiveIterator, WarcRecordType
from tldextract import TLDExtract

(a) Напишите скрипт, который фильтрует данные для языковой модели из WET-файлов в /shared-data/english-wet-data. Можно применять любые инструменты из предыдущих частей, а также пробовать другие фильтры и методы (например, фильтрацию по перплексии n-граммной языковой модели). Цель — данные, обучение на которых минимизирует перплексию на C4 100 domains из Paloma. Ещё раз: валидационные данные Paloma можно использовать при построении фильтров и классификаторов, но нельзя дословно копировать их в обучающие. Скрипт должен сообщать, сколько примеров оставил каждый фильтр, чтобы вы понимали вклад каждого шага. Сдать: скрипт (или цепочку скриптов), параллельно фильтрующий WET-файлы; письменную разбивку — какую долю выброшенных примеров удалил каждый шаг.

(b) Сколько времени занимает фильтрация предоставленных WET-файлов (исходно 2500 сырых WET-файлов)? Сколько заняла бы фильтрация всего дампа Common Crawl? Сдать: время работы конвейера.

Теперь посмотрим на полученные данные, чтобы лучше понять их содержимое.

Problem (inspect_filtered_data): разбор отфильтрованных данных (4 балла)

Заголовок раздела «Problem (inspect_filtered_data): разбор отфильтрованных данных (4 балла)»

(a) Возьмите пять случайных примеров из отфильтрованного набора. Оцените их качество и пригодность для обучения, особенно с учётом цели — минимальной перплексии на C4 100 domains. Сдать: пять случайных примеров (можно только показательные фрагменты) и к каждому 1–2 предложения: что это и стоит ли его использовать.

(b) Возьмите пять WET-документов, которые ваш скрипт удалил или изменил. Какая часть конвейера это сделала и было ли это оправдано? Сдать: пять случайных отброшенных примеров (фрагменты) и к каждому 1–2 предложения: что это и оправдано ли удаление.

(c) Если разбор подсказывает изменения конвейера, внесите их до обучения модели. Опишите изменения и итерации. Сдать: описание изменений данных и итераций.

Перед обучением данные нужно токенизировать. Закодируйте отфильтрованные данные токенизатором GPT-2 из transformers в последовательность целых идентификаторов. Не забудьте добавлять после каждого документа токен конца последовательности GPT-2 <|endoftext|>. Стартовый код:

import multiprocessing
import numpy as np
from tqdm import tqdm
from transformers import AutoTokenizer
input_path = "path/to/your/filtered/data"
output_path = "path/to/your/tokenized/data"
tokenizer = AutoTokenizer.from_pretrained("gpt2")
def tokenize_line_and_add_eos(line):
return tokenizer.encode(line) + [tokenizer.eos_token_id]
with open(input_path) as f:
lines = f.readlines()
pool = multiprocessing.Pool(multiprocessing.cpu_count())
chunksize = 100
results = []
for result in tqdm(
pool.imap(tokenize_line_and_add_eos, lines, chunksize=chunksize),
total=len(lines),
desc="Tokenizing lines"
):
results.append(result)
pool.close()
pool.join()
# Склеиваем списки идентификаторов и превращаем в массив numpy
all_ids = [token_id for sublist in results for token_id in sublist]
print(f"Tokenized and encoded {input_path} into {len(all_ids)} tokens")
ids_array = np.array(all_ids, dtype=np.uint16)
ids_array.tofile(output_path)

Напишите скрипт, который токенизирует и сохраняет отфильтрованные данные. Сохраняйте как в примере — ids_array.tofile(output_path), где ids_array — массив numpy типа np.uint16: так данные совместимы с обучающим скриптом. Сколько токенов в вашем наборе? Сдать: скрипт и число токенов.

Теперь обучим модель. Мы обучим на ваших данных модель формы GPT-2 small примерно на 430 млн параметров, периодически измеряя качество на валидации C4 100 domains. Обучение идёт на 8 GPU B200 с параллелизмом по данным, размер батча — 128 на устройство, 16 384 шага оптимизации. При длине контекста 512 это около 233≈8,62^{33} \approx 8{,}6 млрд токенов за обучение. Наш запуск с такими настройками занял около 2 часов.

Запускайте обучение скриптом scripts/train.py. Гиперпараметры — в cs336_basics/train_config.py. Путь к токенизированным обучающим данным передаётся через --train-bin:

Окно терминала
uv run modal run scripts/train.py --train-bin /root/data/your_data.bin

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

Окно терминала
uv run modal run scripts/generate_with_gpt2_tok.py --model-path /root/data/output/your_data

Обучите языковую модель (форма GPT-2 small, около 430 млн параметров) на своём токенизированном наборе. Периодически измеряйте потери на валидации C4 100 domains. Какие наилучшие потери на валидации достигает модель? Сдать: лучшие зафиксированные потери на валидации, кривую обучения и описание того, что вы сделали.

  1. L. Soldaini et al., «Dolma: An Open Corpus of Three Trillion Tokens for Language Model Pretraining Research», arXiv:2402.00159, 2024.
  2. J. W. Rae et al., «Scaling language models: Methods, analysis & insights from training Gopher», arXiv:2112.11446, 2021.
  3. L. Page, S. Brin, R. Motwani, T. Winograd, «The PageRank citation ranking: Bringing order to the web», 1998.
  4. A. Radford et al., «Language models are unsupervised multitask learners», OpenAI blog, 2019.
  5. H. Touvron et al., «LLaMA: Open and Efficient Foundation Language Models», 2023.
  6. J. Leskovec, A. Rajaraman, J. D. Ullman, «Mining of Massive Datasets», 2nd ed., Cambridge University Press, 2014 (глава 3 — infolab.stanford.edu/~ullman/mmds/ch3n.pdf).
  7. G. Penedo et al., «The RefinedWeb Dataset for Falcon LLM: Outperforming Curated Corpora with Web Data, and Web Data Only», 2023.
  8. I. Magnusson et al., «Paloma: A Benchmark for Evaluating Language Model Fit», 2023.
  9. C. Raffel et al., «Exploring the Limits of Transfer Learning with a Unified Text-to-Text Transformer», JMLR 21(140), 2020.

  • «GPT-2 small» с 430 млн параметров. У настоящей GPT-2 small 124 млн параметров. В cs336_basics/train_config.py задано d_model = 1024, num_layers = 24, num_heads = 16, d_ff = 3072, словарь 50 257, контекст 512 — по ширине и глубине это форма GPT-2 medium. С блоком SwiGLU (три матрицы в FFN) и раздельными матрицами входа и выхода получается как раз около 430 млн. Ориентируйтесь на конфиг, а не на название.
  • Пример с concurrent.futures в оригинале использует pathlib, не импортируя его; мы добавили импорт.
  • Пути /shared-data/... существуют только на кластере курса (том Modal). Как получить те же файлы самостоятельно — ниже.

Без кластера Стэнфорда: задание на 4 × RTX 4080

Заголовок раздела «Без кластера Стэнфорда: задание на 4 × RTX 4080»

Части 2 и 3 (все функции и тесты) делаются на любом компьютере: им нужны только небольшие файлы. Части 4 нужны сотни гигабайт WET и 8 × B200 — её придётся уменьшить.

  1. Маленькие файлы. В репозитории есть scripts/download_data.py. Для работы без Modal поменяйте путь в cs336_data/common.py и запустите uv run scripts/download_data.py --offline-only: скачаются пример WARC/WET из CC-MAIN-2026-12, lid.176.bin, два классификатора Dolma (с Hugging Face) и токенизированная валидация Paloma C4 100 domains. Этого хватает для частей 2–3 и для тестов.

  2. Свои англоязычные WET. Полный набор — 2500 WET-файлов, это сотни гигабайт. README предлагает уменьшить n_files в EnglishWetFiles (cs336_data/wet_files.py) и реализовать метод is_english перед загрузкой. Для учебного варианта хватит 20–50 файлов: после фильтрации это порядка сотен миллионов токенов.

  3. Положительные примеры для классификатора качества. Скрипт строит список URL из дампа Википедии (это долго и объёмно). Проще взять небольшую подвыборку: несколько тысяч URL, скачанных wget с --warc-file, как в условии. Не берите слишком много — wget с таймаутом 5 секунд на тысячах адресов идёт десятки минут.

  4. Уменьшенная модель вместо 430M. На 4 × RTX 4080 (16 ГБ каждая) обучение исходной конфигурации на 8,6 млрд токенов займёт недели. Для сравнения вариантов данных между собой достаточно маленькой модели: например, d_model = 512, num_layers = 8, num_heads = 8, d_ff = 1536, контекст 512 и 300–500 млн токенов — это часы, а не недели. Главное — одна и та же конфигурация и одинаковое число токенов для всех вариантов данных. Результаты не будут сравнимы с лидербордом Стэнфорда, но выводы «какой фильтр помогает» обычно переносятся.

  5. Обучение без Modal. scripts/train.py рассчитан на Modal, но модель и цикл обучения лежат в cs336_basics. Запустите их через torchrun --nproc_per_node=4 с DDP (как в уроке 5.7.4) или возьмите свою модель из задания 1.

  • Сначала смотрите глазами. Задачи look_at_cc и «просмотрите 20 примеров» — не формальность: почти все хорошие идеи для фильтров рождаются из чтения сырых документов.
  • Кодировки. Сначала пробуйте UTF-8, при ошибке — detect_encoding; при декодировании не теряйте документ целиком из-за пары битых байтов.
  • fastText и переводы строк. Метод predict fastText не принимает строки с символом перевода строки — заменяйте \n на пробелы перед классификацией. Метки приходят в виде __label__en, их нужно разобрать.
  • Регулярные выражения для персональных данных. Пишите тесты на свои крайние случаи: номера со скобками, дефисами, точками, пробелами, с +1; IP-адреса с числами больше 255 не должны маскироваться.
  • Дедупликация. Для точной дедупликации хватит словаря «хеш строки → счётчик»; для MinHash удобно сгенерировать kk хеш-функций одного семейства с разными зёрнами (например, mmh3.hash(ngram, seed)). Кластеры дубликатов удобно собирать структурой «система непересекающихся множеств» (union-find).
  • Цель — C4. Метрика — перплексия на текстах со 100 популярных доменов C4. Классификатор «похоже на C4 или нет» (положительные — валидация Paloma как источник признаков, а не текста для обучения) — законный и сильный приём; копировать валидационные тексты в обучающие нельзя.
  • Считайте выбывших. Логируйте, сколько документов отсеял каждый шаг, — этого прямо требует задача filter_data, и это лучший способ найти фильтр, который «съедает» слишком много.
  • Лекции 13–14 — о данных подробнее: источники, фильтры, дедупликация, смеси данных.
  • Задание 5 — выравнивание: SFT, рассуждения и RL с проверяемыми наградами.
  • Урок 5.6.2 — свой проект предобучения, где отфильтрованные данные можно сразу использовать.