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

AI Research Foundations, курс 2, часть 1: чистка данных и токенизация

Перевод лабораторных 2.1–2.3 курса Google DeepMind «Represent Your Language Data»: чистка HTML и Unicode регулярными выражениями, закон Ципфа, токенизация по символам и словам, подсловный токенизатор Gemma и специальные токены.

Курс 2 программы AI Research Foundations называется «Represent Your Language Data» («Представьте свои языковые данные»). Он о том, что происходит с текстом до модели: как его чистят, на какие единицы режут и как эти единицы превращают в векторы. На этой странице — первые три лабораторные: чистка сырого текста, токенизация по символам и словам и подсловный токенизатор модели Gemma. Продолжение — часть 2: свой BPE, эмбеддинги и SLM на BPE.

ЛабораторнаяВремяКогда делатьЧто нужно
2.1. Предобработка данных15 минВместе с уроком «Данные для претрейна» или раньше, после урока «Зачем нужны токены»Только Python
2.2. Токенизация по символам и словам30 минВместе с уроком «Зачем нужны токены»Python, pandas
2.3. Подсловная токенизация10 минПеред уроком «Токенизаторы современных моделей»Пакет gemma (GPU не нужен)

Все три лабораторные идут на CPU. В уроке 4.2.3 курса ChinaAI вы сравниваете токенизаторы Qwen, DeepSeek и Llama — токенизатор Gemma из лабораторной 2.3 хорошо добавить в это сравнение (пример на русском — в конце страницы).


Ноутбук: gdm_lab_2_1_preprocess_data.ipynb · открыть в Colab

Изучите основы подготовки текстовых данных для построения хороших языковых моделей. 15 минут.

В этой лабораторной вы сосредоточитесь на важнейших техниках предобработки текстовых данных. Лабораторная продолжает задание «Исследуем сырые данные» из предыдущего модуля: там вы готовили данные вручную. Теперь вы научитесь программно готовить данные к токенизации.

Большие языковые модели (LLM) часто обучают на огромных общедоступных наборах данных, собранных в интернете. Сам объём данных даёт столько примеров, что модели «усредняют» несовершенства и всё равно выучивают скрытые закономерности. Объём работает как буфер: модели находят закономерности и связи, даже если отдельные примеры ошибочны, неполны или зашумлены.

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

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

К концу лабораторной вы будете понимать:

  • как удалять HTML-теги регулярными выражениями;
  • как заменять распространённые HTML-сущности соответствующими символами;
  • как устроены категории Unicode и как с их помощью фильтровать текст.

В этой лабораторной вы:

  • напишете функцию, которая убирает HTML-теги из сырого текста;
  • расширите её, чтобы она чистила распространённые HTML-сущности вроде <, > и &;
  • с помощью пакета unicodedata изучите категории символов;
  • доработаете функцию чистки так, чтобы она сохраняла буквы, цифры, знаки препинания и пробелы, а эмодзи и прочие значки удаляла.

Все шаги подробно описаны ниже.

В этой лабораторной вы используете встроенный модуль Python re — в нём функции для работы с регулярными выражениями.

Запустите следующую ячейку, чтобы импортировать нужные пакеты.

%%capture
# Устанавливаем специальный пакет курса.
!pip install "git+https://github.com/google-deepmind/ai-foundations.git@main"
import re # Чтобы задавать регулярные выражения и работать с ними.
import unicodedata # Чтобы работать с символами Unicode.
# Специальные функции, которые дают обратную связь по вашим решениям.
from ai_foundations.feedback.course_2 import preprocess

В текстах иногда остаются HTML-теги вроде <br> или <strong>. Эти элементы HTML задают структуру веб-страниц, но при обработке текста для других задач бывают лишними или нежелательными. HTML-теги подчиняются определённым шаблонам: обычно они заключены в угловые скобки, <tag> или </tag>. Например:

<strong>agriculture sector</strong>

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

Код в ячейке ниже удаляет HTML-теги. Шаблон для этого — r'<.*?>'. Он находит всё, что заключено между < и >. Чтобы лучше его понять, разберите каждую часть:

  • r'' (сырая строка, raw string): буква r в начале — не часть шаблона. Это указание Python считать строку «сырой». Так принято писать регулярные выражения: обратная косая черта не получает особого смысла, и это предотвращает возможные ошибки.
  • < и > (буквальные символы): они «привязывают» шаблон. Движок регулярных выражений ищет настоящий знак «меньше» (<), чтобы начать совпадение, и настоящий знак «больше» (>), чтобы его закончить.
  • . (подстановочный знак, wildcard): точка — специальный метасимвол, который совпадает с любым одним символом (кроме перевода строки).
  • * (квантификатор): звёздочка относится к символу прямо перед ней (к точке) и велит искать его ноль или больше раз. Вместе .* означает «любая последовательность любых символов».
  • ? (ленивый модификатор): по умолчанию квантификатор * старается захватить самую длинную возможную строку. Знак вопроса сразу после него переключает его в «ленивый» режим — теперь он захватывает самую короткую возможную строку.

Запустите следующую ячейку, чтобы определить и проверить функцию clean_html_tags.

def clean_html_tags(text: str) -> str:
"""
Удаляет из строки все HTML-теги.
Применяет нежадный шаблон '<.*?>': всё, что заключено в угловые скобки,
например '<h1>', '</p>' или '<img src="..." />', удаляется, а окружающий
текст остаётся нетронутым.
Args:
text: Сырой текст, в котором могут быть HTML-теги.
Returns:
Простой текст без HTML-тегов.
"""
regex_pattern = r"<.*?>" # Совпадает со всем, что между < и >.
return re.sub(regex_pattern, "", text)
text = (
"<h3>Let's come together to <strong>win</strong> this match."
" <br>We can do it!</h3>"
)
cleaned_text = clean_html_tags(text)
print(f"Before cleaning: \n\ttext: {text}\n")
print(f"After cleaning: \n\ttext: {cleaned_text}")

Посмотрите, зачем нужен «ленивый» модификатор: уберите его из шаблона, снова запустите код и сравните вывод.

Вы увидите, что регулярное выражение сопоставило < первого тега <h3> с > последнего тега </h3>. Без ? выражение .* захватывает как можно больше строки, поэтому <.*> находит одно большое совпадение — от самого первого < до самого последнего >.

Вы увидели, что HTML-теги имеют вид <tag>, и с помощью шаблона (<.*?>) заменили их пустой строкой. Когда текст взят с веб-страниц, в исходнике иногда встречаются и другие HTML-сущности (entities), например:

  • &lt; вместо <;
  • &gt; вместо >;
  • &amp; вместо &;
  • &nbsp; вместо неразрывного пробела.

Запустите ячейку ниже: она чистит HTML-теги в примере текста. Вы заметите, что работа ещё не закончена: в выводе остаётся текст вроде &gt;.

text = (
"<p>The Krowor Municipal District was carved out of the Ledzokuku-Krowor"
" Municipal District in 2018 &amp; it's population is &gt; 200000.</p>"
)
cleaned_text = clean_html_tags(text)
print(cleaned_text)

Теги удалены, но некоторые HTML-сущности остались.

def clean_html(text: str) -> str:
"""
Убирает из строки простую HTML-разметку и распространённые сущности.
Функция не пытается полноценно разбирать HTML; для сложной разметки
используйте `BeautifulSoup` или `html.unescape`.
Args:
text: Строка, в которой могут быть HTML-теги или сущности.
Returns:
Очищенная строка: теги удалены, а сущности '&nbsp;', '&amp;', '&lt;'
и '&gt;' превращены в ' ', '&', '<' и '>'.
"""
# Удаляем HTML-теги.
text = re.sub(r"<.*?>", "", text)
# Заменяем HTML-сущности.
text = re.sub("&nbsp;", " ", text) # Неразрывный пробел заменяем обычным.
# Допишите код в следующих трёх строках.
text =
text =
text =
return text

Проверьте функцию. Убедитесь, что &lt; заменяется на <, &gt; — на >, а &amp; — на &:

text = (
"<p>The Krowor Municipal District was carved out of the Ledzokuku-Krowor"
" Municipal District in 2018 &amp; it's population is &gt; 200000.</p>"
)
cleaned_text = clean_html(text)
print(f"Before cleaning: \n\ttext: {text}\n")
print(f"After cleaning: \n\ttext: {cleaned_text}\n")
preprocess.test_clean_html(clean_html)

Не каждый текст записан символами ASCII. Набор ASCII включает цифры от 0 до 9, заглавные и строчные английские буквы (от A до Z) и несколько специальных символов. Многие языки пишутся письменностями, которых в этом ограниченном наборе нет. Кроме того, люди добавляют в текст и другие специальные символы, например эмодзи, чтобы выразить себя на письме.

Unicode даёт каждому символу — буквам, цифрам, знакам препинания, эмодзи и даже письменностям вроде арабской или китайской — уникальный номер, который называется кодовой точкой (code point). Эмодзи вроде 😱 — это не картинка, вставленная в текст. Это символ с кодовой точкой U+1F631 в наборе символов Unicode.

Чтобы получить значение Unicode для любого нетекстового элемента (эмодзи, особого значка и т. п.), можно сочетать функции Python hex и ord:

hex(ord('😱'))

Вывод показывает кодовую точку в шестнадцатеричном виде. В Python шестнадцатеричные числа всегда начинаются с 0x. Всё, что идёт после 0x, — кодовая точка Unicode.

Зная кодовые точки, можно оставлять, заменять или удалять символы простыми регулярными выражениями. Например, все эмодзи лежат в определённых диапазонах кодовых точек, и по ним их можно удалить. Можно также по конкретным кодовым точкам заменять отдельные эмодзи словами вроде sad или happy — смотря что нужно вашей модели.

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

print('Symbol 😊\'s Unicode category is:', unicodedata.category('😊'))
print('Symbol 😱\'s Unicode category is:', unicodedata.category('😱'))

У эмодзи 😊 и 😱 категория Unicode — So, что означает «Symbol, Other» («прочий значок»). Если вы хотите удалять эмодзи при предобработке текста, это подсказка, как действовать.

В целом система категорий Unicode устроена так:

КатегорияЗначениеЧастые подкатегории и примеры
L*Буква (Letter)Lu — заглавная (A), Ll — строчная (a), Lt — «заглавная в начале слова» (Dž), Lm — модифицирующая (ʰ), Lo — прочие буквы (китайские иероглифы, ע, ب)
N*Число (Number)Nd — десятичные цифры (0–9, ٠–٩), No — прочие числа (½, Ⅻ)
P*Пунктуация (Punctuation)Po — прочая пунктуация (!, ?), Pd — тире (—), Ps/Pf/Pe — открывающие, конечные и закрывающие скобки
S*Значок (Symbol)Sm — математические (±, √), Sc — валюты (₦, $), Sk — модифицирующие (ˆ), So — прочие значки (😊, ⭐)
Z*Разделитель (Separator)Zs — пробел, Zl — строка, Zp — абзац
C*Прочие / управляющие (Other)Cc — управляющие коды (перевод строки, табуляция), Cf — знаки форматирования (zero-width joiner), Cs — суррогаты, Co/Cn — частное использование или неназначенные

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

  1. Оставьте то, что нужно, например L* (буквы), N* (числа) и, возможно, P* (пунктуацию) и пробелы.
  2. Удалите или замените то, что не нужно (например, ищите S*, чтобы выбросить эмодзи или заменить их метками, или C*, чтобы убрать символы, которые могут сломать обработку).
def clean_unicode(text: str) -> str:
"""
Удаляет из строки нетекстовые символы Unicode.
Args:
text: Исходный текст, в котором могут быть специальные символы.
Returns:
Входная строка без эмодзи и других нетекстовых значков.
"""
# Сейчас функция сохраняет только буквы и цифры, потому что множество
# `categories_to_keep` содержит только "L" (буквы) и "N" (числа).
#
# Измените `categories_to_keep` так, чтобы сохранялись и знаки препинания.
categories_to_keep = {"L", "N"} # Измените код здесь.
keep = []
for ch in text:
do_keep = ch.isspace() # Пробельные символы сохраняем.
if not do_keep:
for category in categories_to_keep:
if unicodedata.category(ch).startswith(category):
do_keep = True
break
if do_keep:
keep.append(ch)
return "".join(keep)
text = "Bag of rice now cost ₦150000 naira. Ah! 😱 Èdakun o"
cleaned_text = clean_unicode(text)
print(f"Before cleaning: \n\ttext: {text}\n")
print(f"After cleaning: \n\ttext: {cleaned_text}")
preprocess.test_clean_unicode(clean_unicode)

(Пример на йоруба и нигерийском английском: «Мешок риса теперь стоит ₦150000 найр. Ах! 😱 Пожалуйста».)

Считайте функцию clean_unicode гибкой отправной точкой, а не универсальным решением для любой задачи. Её сила — в приспособляемости. Что она оставляет, а что отбрасывает, легко поменять, изменив множество categories_to_keep. Например, чтобы сохранить символы валют вроде ₦, достаточно добавить в множество категорию Unicode "Sc" («Symbol, currency»).

Как чистить текст, полностью зависит от цели проекта. Например, обрабатывая данные из соцсетей, вы можете захотеть:

  • Обрабатывать эмодзи. Удалить их совсем или, для анализа тональности, заменить описательными метками вроде <emoji_happy> или <emoji_sad>.
  • Обрабатывать хэштеги. Не удалять их, а оставить текст, просто убрав символ #.
  • Заменять специальные символы. Заменить все символы валют специальной меткой <money>, а сочетания знаков вроде <3 — меткой <heart>.

Каждый из этих вариантов подстраивает чистку под конкретное приложение — будь то чат-боты, анализ отзывов клиентов или модель, которая сочиняет стихи.

В конечном счёте то, как вы чистите данные, напрямую влияет на то, что выучит модель. Каждое решение — нормализовать ли сленг, как обращаться с пунктуацией, сохранять ли культурные символы — должно быть осознанным и соответствовать вашей цели. Автоматические инструменты отлично ускоряют процесс, но ручная проверка необходима. Особенно для тонкого или насыщенного культурными особенностями содержания. Машина может не понять, какая информация в тексте важна для конкретной задачи, и такое суждение часто требует участия человека.

В этой лабораторной вы потренировались обрабатывать зашумлённые данные и научились чистить элементы HTML и Unicode. Для чистки HTML вы использовали регулярные выражения. Для удаления или замены символов Unicode — пакет unicodedata.

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

Задание 1
# Полная реализация функции clean_html.
def clean_html(text: str) -> str:
"""Убирает простую HTML-разметку и распространённые сущности (см. выше)."""
# Удаляем HTML-теги.
text = re.sub(r"<.*?>", "", text)
# Заменяем HTML-сущности.
text = re.sub("&nbsp;", " ", text) # Неразрывный пробел.
text = re.sub("&amp;", "&", text) # "&amp;" заменяем на "&".
text = re.sub("&lt;", "<", text) # "&lt;" заменяем на "<".
text = re.sub("&gt;", ">", text) # "&gt;" заменяем на ">".
return text
Задание 2
# Полная реализация функции clean_unicode.
def clean_unicode(text: str) -> str:
"""Удаляет из строки нетекстовые символы Unicode (см. выше)."""
categories_to_keep = {"L", "N", "P"} # L — буквы, N — числа, P — пунктуация.
keep = []
for ch in text:
do_keep = ch.isspace() # Пробельные символы сохраняем.
if not do_keep:
for category in categories_to_keep:
if unicodedata.category(ch).startswith(category):
do_keep = True
break
if do_keep:
keep.append(ch)
return "".join(keep)

Ожидаемый вывод примера после решения: Bag of rice now cost 150000 naira. Ah! Èdakun o — значок найры (категория Sc) и эмодзи (So) исчезли, а буква È (категория Lu) осталась. На месте эмодзи остаются два пробела подряд.


Лабораторная 2.2. Токенизация текстов по символам и словам

Заголовок раздела «Лабораторная 2.2. Токенизация текстов по символам и словам»

Ноутбук: gdm_lab_2_2_tokenize_texts_into_characters_and_words.ipynb · открыть в Colab

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

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

Затем вы поэкспериментируете с двумя стратегиями токенизации: посимвольной (character-level) и пословной (word-level). Вы оцените, как эти подходы влияют на длину последовательности и размер словаря, и увидите компромисс между этими двумя показателями.

К концу лабораторной вы будете понимать:

  • как частоты токенов в естественном тексте подчиняются закону Ципфа;
  • как предобработка влияет на счётчики и распределения токенов;
  • чем различаются посимвольная и пословная токенизация.

В этой лабораторной вы:

  • выполните предобработку и посчитаете токены в наборе Africa Galore;
  • нарисуете распределение частот слов в связи с законом Ципфа;
  • увидите, как посимвольная и пословная токенизация влияют на длину последовательности и размер словаря.

В этой лабораторной вы используете модуль re, чтобы задавать более сложные правила разбиения, и специальный пакет ai_foundations для визуализаций и обратной связи.

Запустите следующую ячейку, чтобы импортировать нужные пакеты.

%%capture
# Устанавливаем специальный пакет курса.
!pip install "git+https://github.com/google-deepmind/ai-foundations.git@main"
import re # Чтобы задавать регулярные выражения.
import pandas as pd # Чтобы загрузить набор данных.
import textwrap # Чтобы абзацы было удобнее читать.
from collections import Counter # Чтобы считать токены.
from ai_foundations import visualizations # Для визуализаций.
from ai_foundations.feedback.course_2 import tokenize # Для обратной связи.

Работа с наборами данных — одна из главных задач исследователя ИИ. Встречая данные нового вида, всегда полезно понять их свойства. Это поможет строить модели лучше, а ещё даст подсказки для отладки, когда модель ведёт себя не так, как вы ожидали.

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

# Загружаем набор Africa Galore.
africa_galore = pd.read_json(
"https://storage.googleapis.com/dm-educational/assets/ai_foundations/africa_galore.json"
)
dataset = africa_galore["description"].values
print("Loaded dataset with", dataset.shape[0], "paragraphs.\n")
print(f"The first paragraph is:\n{textwrap.fill(dataset[0])}")

Прежде чем анализировать содержимое набора, его нужно предобработать. В этой лабораторной вы выполните два шага предобработки:

  1. При разбиении абзацев по пробелам токен с прилипшей пунктуацией, например «music,», считался бы отдельно от того же токена без пунктуации — «music». Функция preprocess_text ниже удаляет пунктуацию, чтобы запятые и точки не мешали самим токенам.
  2. Функция preprocess_text переводит весь текст в нижний регистр, чтобы токены вроде «The» и «the» считались одним и тем же токеном.

Такая предобработка позволит сосредоточиться на настоящем распределении частот токенов.

Запустите следующую ячейку, чтобы определить функцию preprocess_text и обработать набор Africa Galore.

def preprocess_text(paragraphs: list[str]) -> list[str]:
"""Предобрабатывает список абзацев.
Функция переводит текст в нижний регистр и разбивает его на отдельные
токены, удаляя пунктуацию. Она использует регулярные выражения — это
точнее, чем простой токенизатор по пробелам.
Args:
paragraphs: Список строк, каждая из которых — абзац текста.
Returns:
Список строк, каждая из которых — токен в нижнем регистре,
извлечённый из входных абзацев.
"""
# Переводим текст в нижний регистр.
paragraphs = [text.lower() for text in paragraphs]
tokens_list = []
# Регулярное выражение (r'\b\w+\b') делит абзац по границам слов
# и тем самым убирает пунктуацию. Так текст разбивается на отдельные
# слова, а пунктуация и пробелы обрабатываются точнее, чем простым `.split()`.
for paragraph in paragraphs:
for token in re.findall(r'\b\w+\b', paragraph):
tokens_list.append(token)
return tokens_list
# Обрабатываем все абзацы набора.
tokens_list = preprocess_text(dataset)
print(tokens_list[:10])

Теперь вы посчитаете, сколько раз каждый токен встречается в наборе.

def get_token_counts(tokens_list: list[str]) -> Counter[str]:
"""Вычисляет частоту каждого токена в списке токенов.
Args:
tokens_list: Список строковых токенов.
Returns:
Counter, где ключи — уникальные токены, а значения — их частоты.
"""
# Ваш код здесь.
token_counts =
return token_counts
# Запустите эту ячейку, чтобы проверить свою реализацию.
tokenize.test_get_token_counts(get_token_counts, tokens_list)

Когда функция get_token_counts заработает, запустите следующую ячейку: она напечатает 10 самых частых токенов набора Africa Galore.

token_counts = get_token_counts(tokens_list)
# Печатаем 10 токенов с наибольшими счётчиками.
list(token_counts.most_common(10))

Ожидаемый вывод (проверено нами): the — 1072, a — 930, and — 909, of — 634, is — 364, in — 323, with — 321, to — 258, for — 193, it — 170.

Удивляет ли вас, что чаще всего в наборе встречается слово «the»? Это очень типичный результат. Похожий анализ проводили на гораздо больших английских наборах данных [1]. Как и здесь, слова вроде «the», «a», «and», «of» и формы глагола «be» (например, «is») почти всегда входят в десятку самых частых.

А что с самыми редкими токенами набора? Запустите следующую ячейку, чтобы напечатать «хвост» распределения.

# Печатаем 10 самых редких слов набора Africa Galore.
# Заметьте: most_common() возвращает все элементы по убыванию счётчика.
list(token_counts.most_common())[-10:]

Теперь — об одном фундаментальном свойстве языка: связи частоты токена с его рангом. Посчитав все уникальные токены набора Africa Galore, их можно ранжировать: ранг 1 — самому частому токену, ранг 2 — второму по частоте и так далее.

Можно было бы ожидать, что это распределение случайно, но языки неизменно подчиняются предсказуемой закономерности. Совсем немногие токены (вроде «the», «a», «and») встречаются чрезвычайно часто, а подавляющее большинство редки и часто появляются всего один раз. Получается распределение с «длинным хвостом» (long tail).

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

Чтобы справиться с этим, используют график в двойном логарифмическом масштабе (log-log plot). Если откладывать логарифм ранга против логарифма частоты, разброс становится обозримым. Логарифмическая шкала сжимает расстояния между большими значениями и растягивает расстояния между малыми. Так весь набор данных видно сразу и гораздо яснее.

Запустите следующую ячейку, чтобы построить такой график.

# Рисуем частоту слов против ранга в двойном логарифмическом масштабе
# с автоматическими подписями самых частых и самых редких слов.
visualizations.plot_word_frequencies(token_counts)

Как видите, точки на графике лежат примерно на нисходящей прямой. Это не случайность. Вы только что увидели знаменитый принцип лингвистики — закон Ципфа (Zipf’s law).

Проверим на числах Africa Galore: самое частое слово «the» встречается 1072 раза. По закону Ципфа второе слово должно встречаться около 1072/2=5361072/2 = 536 раз, а десятое — около 107107. На деле «a» встречается 930 раз, а «it» (десятое) — 170. На маленьком наборе закон выполняется грубо, но порядок величин верный.

Закон Ципфа подчёркивает главную трудность обработки текстов. Он гарантирует, что в любом корпусе будут господствовать немногие очень частые токены, а подавляющая часть словаря будет крайне редкой. Этот «длинный хвост» редких токенов — серьёзное препятствие для языковых моделей. Если модель встречает токен лишь несколько раз в большом наборе, ей трудно выучить его смысл. Это фундаментальная проблема, которую исследователи ИИ должны решать при обучении моделей. В следующих заданиях вы познакомитесь с методами токенизации, которые позволяют моделям извлекать больше смысла даже из очень редких токенов.

Когда понятно, как распределены слова в данных, следующий шаг — решить, как разбивать текст на более мелкие «единицы» (токены), которые затем переводятся в id токенов. Эти id станут входом вашей языковой модели.

Сейчас вы разберёте разные методы токенизации текстов.

Для начала снова посмотрите на первый абзац набора Africa Galore, чтобы вспомнить, как выглядят данные.

sample_text = dataset[0]
print(textwrap.fill(sample_text))

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

Абзац можно разбить на отдельные символы, которые затем переводятся в id токенов. В этом случае каждому уникальному id соответствует ровно один символ. Как говорилось в предыдущей статье, у такого подхода есть преимущество: он в основном решает проблему слов вне словаря (out-of-vocabulary). Но при этом последовательности получаются очень длинными.

Запустите следующий фрагмент кода, чтобы напечатать первые 10 токенов посимвольного токенизатора.

def character_tokenize(text: str) -> list[str]:
"""Делит текст на символы.
Args:
text: Текст, который нужно разделить.
Returns:
Список токенов.
"""
tokens = list(text)
return tokens
print(character_tokenize(dataset[0])[:10])

Сравните этот посимвольный токенизатор с простым пословным токенизатором. Простейший способ токенизировать по словам — разбить текст по пробелам.

Как уже кратко обсуждалось в курсе 01 «Build Your Own Small Language Model», такой токенизатор очень наивен. При разбиении по пробелам знаки препинания могут оказаться частью токена, и у одного слова может появиться много разных токенов. Например, «music,» (с запятой) будет другим токеном, чем «music» (без запятой).

Дальше в курсе вы разберёте более совершенные подходы, например подсловные токенизаторы. Они иногда делят текст на уровне символов, а иногда на уровне слов. Это помогает лучше справляться с такими проблемами. А пока запустите фрагмент кода и посмотрите на первые 10 токенов при разбиении по пробелам.

def space_tokenize(text: str) -> list[str]:
"""Делит текст по пробелам.
Args:
text: Текст, который нужно разделить.
Returns:
Список токенов.
"""
tokens = text.split(" ")
return tokens
print(space_tokenize(dataset[0])[:10])

Как токенизация влияет на длину последовательности

Заголовок раздела «Как токенизация влияет на длину последовательности»

Метод токенизации сильно влияет и на число токенов, на которое разбивается текст, и на размер словаря. Пословная токенизация даёт меньше токенов на текст, но требует большего словаря: каждое уникальное слово — отдельный токен, а слов гораздо больше, чем уникальных символов. Посимвольная токенизация, наоборот, даёт гораздо больше токенов на текст, но обходится меньшим словарём: он состоит только из гораздо меньшего набора отдельных символов.

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

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

tokens_char = character_tokenize(dataset[0])
print(tokens_char)
print(f"The length of the sequence is: {len(tokens_char)}")

А если токенизировать ту же последовательность по словам, она становится такой:

tokens_word = space_tokenize(dataset[0])
print(tokens_word)
print(f"The length of the sequence is: {len(tokens_word)}")

Как вы, вероятно, заметили, посимвольный токенизатор считает отдельным токеном каждую букву, пробел и знак препинания. Это сильно увеличивает длину последовательности, которую должна обработать модель. Первый абзац набора — это 685 символьных токенов, а то же содержание можно представить всего 118 словесными токенами. Помните: длинные последовательности требуют больше памяти и вычислений.

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

Размер словаря — важное проектное решение со значительными компромиссами.

  1. Больший словарь позволяет распределить информацию по большему числу токенов. В пословном словаре токены — целые слова. Разные значения «time» и «the» хранятся в отдельных, специфичных представлениях токенов. Информация распределена чётко. В посимвольном словаре, наоборот, токен «t» должен участвовать в представлении каждого слова, где он встречается, — «time», «the», «train». Из-за этого параметры одного токена вынуждены хранить огромный объём контекстной информации, и модели трудно выучить представления, точно передающие смысл.
  2. Больший словарь требует больше параметров модели, а это напрямую увеличивает её размер и стоимость вычислений. Каждому токену нужно уникальное представление, которое хранится в параметрах модели. Поэтому больший словарь даёт модель побольше, которой нужно больше памяти и вычислительной мощности, и обучение и инференс становятся медленнее и дороже.
  3. В большем словаре неизбежно больше токенов, которые очень редко встречаются в обучающих данных. Модель не может выучить надёжное представление таких редких токенов: ей не хватает примеров их употребления. Эта проблема «разреженности данных» мешает модели хорошо работать с менее распространёнными словами и понятиями.

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

# Печатаем число уникальных токенов при посимвольной и пословной токенизации.
vocab_char = set(tokens_char)
vocab_word = set(tokens_word)
print(f"The character-level vocabulary consists of {len(vocab_char):,} tokens.")
print(f"The word-level vocabulary consists of {len(vocab_word):,} tokens.")

Ожидаемый вывод: 32 символьных токена против 83 словесных — и это всего для одного абзаца.

Словарь посимвольного токенизатора гораздо меньше, чем у пословного. При посимвольной токенизации словарь обычно мал, потому что в любом языке ограниченное число символов. Например, в английском есть символы A, B, C и т. д., а в арабском — алиф, ба, та (أ, ب, ت). Все эти символы — токены посимвольного токенизатора.

Слова, напротив, гораздо разнообразнее. Подумайте о словарном запасе английского языка: по оценкам, в нём около миллиона слов [2]. Это на порядки больше, чем число символов английского письма. В языках с более сложной письменностью, например в китайском, разница между числом уникальных символов и числом уникальных слов может быть меньше, но всё равно значительна.

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

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

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

В следующей лабораторной вы поэкспериментируете с токенизатором Gemma: он использует подсловные токены и сочетает преимущества посимвольного и пословного токенизаторов.

Задание 1
# Полная реализация `get_token_counts`.
def get_token_counts(tokens_list: list[str]) -> Counter[str]:
"""Вычисляет частоту каждого токена в списке токенов."""
token_counts = Counter(tokens_list)
return token_counts

[1] «The Oxford English Corpus: Facts about the language.» OxfordDictionaries.com. Oxford University Press. Архивная копия.

[2] Merriam-Webster. How many words are there in English?


Лабораторная 2.3. Токенизация текстов на подслова

Заголовок раздела «Лабораторная 2.3. Токенизация текстов на подслова»

Ноутбук: gdm_lab_2_3_tokenize_texts_into_subword_tokens.ipynb · открыть в Colab

Посмотрите, как токенизатор Gemma разбивает тексты на единицы, промежуточные между символами и словами. 10 минут.

В этой лабораторной вы поэкспериментируете с токенизатором языковой модели Google Gemma. Вы увидите, как эта модель справляется с редкими словами или словами, которых нет в её обучающих данных, и при этом представляет частые слова отдельными токенами.

К концу лабораторной вы будете понимать:

  • что такое подсловная токенизация (subword tokenization);
  • как подсловные токенизаторы обращаются с редкими и невиданными словами и с эмодзи;
  • зачем в токенизаторах языковых моделей специальные токены.

В этой лабораторной вы:

  • поэкспериментируете с токенизатором Gemma, чтобы разобраться в подсловной токенизации;
  • напишете код, который токенизирует придуманное слово «Clusterophonexia»;
  • посмотрите, как Gemma обращается с эмодзи и зачем ей специальные токены.

В этой лабораторной вы в основном работаете с токенизатором из пакета gemma.

Запустите следующую ячейку, чтобы импортировать нужные пакеты.

%%capture
# Устанавливаем специальный пакет курса. Вместе с ним
# устанавливается и пакет gemma.
!pip install "git+https://github.com/google-deepmind/ai-foundations.git@main"
from gemma import gm # Для работы с токенизатором Gemma.
# Для обратной связи по вашим решениям.
from ai_foundations.feedback.course_2 import subword_tokens as feedback

В прошлом задании вы увидели компромиссы между двумя основными стратегиями токенизации. Посимвольная токенизация справляется с любым словом, но даёт очень длинные последовательности и не улавливает смысл целых слов. Пословная токенизация интуитивна, но плохо справляется с редкими или невиданными словами (проблема слов вне словаря, out-of-vocabulary).

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

  • частые слова (вроде «the» или «is») остаются целыми, отдельными токенами;
  • редкие или сложные слова (вроде «Baobab», «баобаб») разбиваются на более мелкие осмысленные части.

Так модель сохраняет обозримый словарь фиксированного размера и при этом способна представить любое слово, какое только можно придумать.

Для начала посмотрите, как Gemma токенизирует такой текст:

«The Baobab (genus Adansonia) is» («Баобаб (род Adansonia) — это»)

Она превращает его в такие токены:

["The", " Ba", "ob", "ab", " (", "genus", " Ad", "ans", "onia", ")", " is"]

Заметьте, что пробелы входят в токены, которые образуют начало слова (или целое слово). Кроме того, слово «Baobab» разбито на три токена. Это делается алгоритмом кодирования пар байтов (byte pair encoding, BPE) — одним из самых популярных алгоритмов подсловной токенизации. В следующей лабораторной вы реализуете его сами.

Загружаем токенизатор Gemma и экспериментируем

Заголовок раздела «Загружаем токенизатор Gemma и экспериментируем»

Чтобы лучше прочувствовать, как работает токенизатор Gemma, запустите следующую ячейку и загрузите его.

# Загружаем токенизатор.
gemma_tokenizer = gm.text.Gemma3Tokenizer()
# Смотрим размер словаря.
print(f"Gemma's vocabulary consists of {gemma_tokenizer.vocab_size:,} tokens.")

Как видите, словарь Gemma очень большой — больше 260 000 токенов. Это потому, что список уникальных токенов определялся на наборе данных гораздо больше Africa Galore. Кроме того, токенизатор проектировали так, чтобы модель охватывала широчайший спектр входов: от частых слов и частых подсловных шаблонов до эмодзи вроде '☺️' и наборов символов разных языков. С таким обширным словарём Gemma часто токенизирует текст точнее. Это уменьшает число бессмысленных токенов <UNK> и лучше сохраняет смысловое богатство исходного входа.

Чтобы перевести произвольный входной текст в id токенов, которые может обработать Gemma, используйте метод encode токенизатора. Это показано в следующей ячейке.

# Кодируем текст в id токенов.
text = "The Baobab (genus Adansonia) is one of the most iconic trees."
gemma_tokens = gemma_tokenizer.encode(text)
print(f"Result of tokenizing the text \"{text}\":")
print(gemma_tokens)

Обратное преобразование выполняет метод decode: он переводит id токенов обратно в текст.

# Декодируем токены обратно в текст.
decoded_text = gemma_tokenizer.decode(gemma_tokens)
print(f"Decoded sentence from tokens: {decoded_text}\n")
# Проверяем, получился ли тот же текст, что был исходно.
is_equal = "✅" if text == decoded_text else "❌"
print(
f"Decoding the tokens results in the same text as the original one:"
f" {is_equal}\n"
)
# Декодируем отдельные токены.
for token in gemma_tokens:
decoded_token = gemma_tokenizer.decode(token)
print(f"Token {token}:\t{decoded_token}")

Задание 1. Токенизируем придуманное слово

Заголовок раздела «Задание 1. Токенизируем придуманное слово»

Как говорилось, преимущество этого подхода в том, что почти любое слово можно представить как сочетание нескольких подсловных токенов.

# Задайте две переменные, как описано в инструкции выше.
clusterophonexia_tokens =
first_token_as_text =
# Запустите эту ячейку, чтобы проверить код.
feedback.test_gemma_subword_tokenization(clusterophonexia_tokens,
first_token_as_text,
gemma_tokenizer
)

Как вы увидели, токенизатор Gemma может токенизировать даже придуманные слова вроде «Clusterophonexia». В этом случае модели доступны токены, на которых она обучалась. Например, если смысл «Clusterophonexia» связан со словом «cluster» («скопление»), то токенизация Gemma дала бы некоторую информацию о том, что это слово должно означать. А «токен на всё» <UNK> не дал бы о смысле слова никакой информации.

В большой словарь Gemma входят и многие символы Unicode, например эмодзи. Запустите следующую ячейку и посмотрите, как токенизируется предложение «I am smiling ☺️!» («Я улыбаюсь ☺️!»):

gemma_tokens = gemma_tokenizer.encode("I am smiling ☺️!")
for i, token in enumerate(gemma_tokens):
decoded_token = gemma_tokenizer.decode(token)
print(f"Token {token}:\t{decoded_token}")

Как видите, токенизатор умеет переводить в id токенов и эмодзи. В этом случае эмодзи ☺️ получает id 145233.

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

Вот несколько распространённых специальных токенов:

  • <BOS> и <EOS>.

    Это «beginning of sequence» (начало последовательности, BOS) и «end of sequence» (конец последовательности, EOS). Их главная задача — отмечать начало и конец отдельного фрагмента текста. Это даёт два преимущества:

    • Эффективные батчи: когда начало и конец каждой последовательности чётко отмечены, в один батч можно подать несколько документов, почти не дополняя их.
    • Генерация переменной длины: при порождении текста токен <EOS> служит сигналом остановки. Вместо фиксированного числа токенов модель может генерировать текст, пока не выдаст <EOS>, и так сама решать, когда ответ закончен.
  • <PAD>.

    Как вы уже видели, дополняющий токен нужен, чтобы все входные последовательности в батче были одной длины. Трансформерам нужны входы фиксированного размера, поэтому короткие последовательности «дополняют» этим токеном, пока они не сравняются с самой длинной последовательностью батча.

  • <UNK>.

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

В качестве примера работы со специальными токенами посмотрите, как они устроены в Gemma. Доступ к ним — через gemma_tokenizer.special_tokens. Например, следующие две ячейки показывают, как получить токены BOS и EOS в Gemma.

# Токен начала последовательности (BOS).
gemma_tokenizer.special_tokens.BOS
# Токен конца последовательности (EOS).
gemma_tokenizer.special_tokens.EOS

Токенизатор умеет и автоматически добавлять токены BOS и EOS к последовательности. Это очень полезно, например, когда вы готовите данные для дообучения чат-бота на промптах и ответах модели: модель учится понимать, когда пора остановить генерацию.

token_ids = gemma_tokenizer.encode("Hello world!", add_bos=True, add_eos=True)
token_ids

В этой лабораторной вы разобрали, как работает подсловный токенизатор — например, тот, что использует модель Gemma. Вы увидели, как он делит тексты на токены разной «крупности», как представляет слова, которых нет в его словаре, и как представляет символы Unicode, например эмодзи.

Вы также узнали о распространённых специальных токенах <BOS>, <EOS>, <PAD> и <UNK> и о том, как с их помощью кодировать структурную информацию в текстах.

В следующих заданиях вы узнаете, как реализовать подсловный токенизатор с помощью алгоритма кодирования пар байтов (BPE).

Задание 1
# Добавьте эти две строки в ячейку выше.
clusterophonexia_tokens = gemma_tokenizer.encode("Clusterophonexia")
first_token_as_text = gemma_tokenizer.decode(clusterophonexia_tokens[0])

Это не часть оригинала, а небольшое продолжение для учеников ChinaAI. Запустите тот же токенизатор на английском и русском тексте и сравните число токенов:

from gemma import gm
gemma_tokenizer = gm.text.Gemma3Tokenizer()
for text in [
"The Baobab is one of the most iconic trees.",
"Баобаб — одно из самых узнаваемых деревьев.",
]:
ids = gemma_tokenizer.encode(text)
# Печатаем число токенов и сами токены, чтобы увидеть, где проходят границы.
print(len(ids), [gemma_tokenizer.decode(i) for i in ids])

Обратите внимание, как одна и та же мысль занимает разное число токенов на разных языках. Это важно для стоимости API и для того, сколько текста помещается в контекстное окно; подробнее — в уроке «Токенизаторы современных моделей».

EnglishРусский
preprocessingпредобработка
HTML tag, HTML entityHTML-тег, HTML-сущность
regular expression (regex)регулярное выражение
greedy / lazy (non-greedy) quantifierжадный / ленивый (нежадный) квантификатор
Unicode, code point, categoryUnicode, кодовая точка, категория
Zipf’s law, rank, long tailзакон Ципфа, ранг, длинный хвост
log-log plotграфик в двойном логарифмическом масштабе
character-level / word-level tokenizationпосимвольная / пословная токенизация
subword tokenizationподсловная токенизация
out-of-vocabulary (OOV)слово вне словаря
byte pair encoding (BPE)кодирование пар байтов (BPE)
special tokens: BOS, EOS, PAD, UNKспециальные токены: начало и конец последовательности, дополняющий, неизвестный