← Все статьи

Урок 3. Алфавит, лейблы, формат — Обучаем CNN и CRNN с нуля

Серия «Обучаем CNN и CRNN с нуля»: контракт индексов пусто=0 (blank), кодирование меток, нормализация и проверка формата — до первого датасета.

Светящаяся лента цифр над тёмной платой: дата рассыпается, измерение держится — алфавит и формат
Содержание

В паспорте две проверки: сначала — что все символы «легальные», потом — что строка похожа на паспорт, а не на штрих-код. Если оставить только первую, дату 01.02.24 можно принять за измерение: после нормализации там одни цифры и запятые. Модель не спорит — она учится на том, что вы ей подали. Ошибка в алфавите или индексах часто не падает с исключением: сеть просто молча читает не то.

В уроке 1 мы собрали лабораторию. В уроке 2 зафиксировали вход [1, 48, 160]. Сегодня серия «Обучаем CNN и CRNN с нуля» договаривается о метках (лейблах): какая строка соответствует кропу клетки.

Четыре слова, которые встретятся дальше:

  • Метка — текст, который должен получиться из кропа: 7,3, 12/14, пустая строка для пустой клетки.
  • Алфавит — список допустимых символов для одной задачи на бланке (бумажной форме).
  • Класс — один возможный ответ модели в точке выбора: «это 7», «это запятая», «здесь пусто». У каждого класса свой индекс (номер). Когда говорят «12 классов», имеют в виду: модель умеет выбрать один из двенадцати вариантов, не «двенадцать разных нейросетей». Это не класс в смысле ООП и не школьный урок — термин из классификации: сколько рубрик в меню ответа. У детектора «пустая / заполненная клетка» классов два; у читателя строки классов столько, сколько символов в алфавите, плюс служебное пусто.
  • Пусто (blank) — отдельный класс: «здесь нет символа». Представьте, что сеть смотрит на кроп по колонкам и на каждой выбирает: цифра, запятая… или «ничего». Вот это «ничего» и есть пусто. В коде и в выводе демо вы увидите английское слово blank — это то же самое; в тексте урока говорим «пусто», чтобы не путать с бланком (листом формы). Пусто — не цифра 0 в метке 105 и не пробел: в наших метках пробелов нет, а ноль — обычный символ алфавита с индексом 1 (первый в строке 0123456789…). Пусто занимает индекс 0, чтобы цифра «ноль» и «нет символа» никогда не путались. Зачем пусто нужно по смыслу — в уроке 10 (CTC). Сегодня важно одно: место под него уже заложено в нумерации.

На бланке (форме) у нас три типа клеток — три задачи: измерение (measurement), диаметр (diameter), толщина (thickness). У каждой свой алфавит и своё правило формата. Поэтому одной проверки «все символы из списка» мало: нужна вторая линия — формат.

Две линии обороны: алфавит пропускает символы, формат отсекает дату

Файлы урока

  • src/alphabet.py — алфавиты задач, контракт индексов, нормализация, кодирование, проверка формата.
  • src/demo_labels.py — что происходит с сырыми строками на входе пайплайна.
  • tests/test_alphabet.py — двенадцать тестов; они же спецификация.

Файлы кладёте в свой репозиторий из урока 1 и коммитите, когда тесты зелёные. Ниже, в «Напиши сам», все три файла лежат свёрнутыми блоками — разверните и скопируйте. Эталон lesson-03 и заготовка lesson-03-start — те же тексты на GitHub, если удобнее смотреть там.

Предскажи

Раздел «Предскажи» — не экзамен. Вы фиксируете ожидание до запуска, потом сверяете с demo_labels.py и pytest. Особенно полезно угадать, почему дата отклонена не по алфавиту.

Перед запуском запишите ответы:

  1. Сколько классов (с пусто) будет у задач measurement, diameter и thickness?
  2. Какой индекс получит символ , в алфавите measurement?
  3. Строка 01.02.24 после нормализации состоит из допустимых символов. Должна ли система принять её как измерение?

Спецификация

Алфавит — контракт модели: какие символы вообще бывают. От него сразу следует число классов — сколько разных ответов может выбрать сеть. Функция num_classes(task) как раз это и считает: длина алфавита плюс один служебный класс — пусто. Это же число — ширина последнего слоя модели (одна «ручка» на каждый класс). Индексы при обучении и при расшифровке должны совпадать до единицы: иначе loss и decode будут смотреть на разные символы под одним номером.

Контракт: символы + пусто = размер выходного слоя

Алфавиты и индексы.

Задача Алфавит Символов Классов (с пусто)
measurement 0123456789, 11 12
diameter 0123456789/ 11 12
thickness 0123456789,- 12 13

Контракт индексов: пусто = 0 (blank в коде), символы нумеруются с 1 в порядке следования в строке алфавита. Индекс класса — то число, которое попадёт в метку для обучения и которое decode превратит обратно в символ.

Пример для measurement и строки 7,3:

  1. символ 7 — восьмой в 0123456789, → индекс 8;
  2. запятая — одиннадцатый символ → индекс 11;
  3. символ 3 — четвёртый → индекс 4.

Итого: 7,3 → [8, 11, 4].

Функции. В эталоне lesson-03-start колонка «готово» уже написана; ваша работа — строки с вы (в заготовке это три блока TODO). В своём репозитории готовые функции тоже набираете — иначе нечему вызывать encode.

Функция Что делает Кто пишет
get_alphabet(task), num_classes(task) строка алфавита и число классов; неизвестная задача — ValueError готово
normalize_label(text) убрать пробелы, . → ,, разные тире → обычный - готово
encode_text(text, task) строка → список индексов; символ вне алфавита — ValueError; пустая строка → [] вы
decode_indices(indices, task) индексы → строка; 0, отрицательный или за алфавитом — ValueError вы
FORMAT_PATTERNS регулярные выражения форматов по задачам вы
validate_format(text, task) (ok, причина): сначала алфавит, потом формат вы

Порядок на входе пайплайна (так же будет в генераторе урока 4):

  1. normalize_label — привести сырую строку к канону;
  2. validate_format — отсечь мусор;
  3. encode_text — получить индексы для модели.

Форматы. Пустая строка допустима для всех задач — это пустая клетка. Правило смотрит уже на нормализованную строку.

Матрица форматов: что PASS и что FAIL

Задача Правило простыми словами Подходят Не подходят
measurement целое из 1–3 цифр; опционально запятая и ещё 1–2 цифры 7, 7,3, 105, 0,85, 123,45 1234, 7,, ,5, 7,345, 01,02,24
diameter целое из 1–3 цифр; опционально / и ещё 1–3 цифры 57, 12/14, 120/80 12/, /14, 12/14/16
thickness одно «число» или два через дефис; каждое число — 1–2 цифры, опционально с запятой и 1–2 цифрами 10, 1-2, 1,5-2, 1,5-2,5 -2, 1-, 1--2, 1-2-3

Причина отказа в validate_format должна содержать слово «алфавит» (символ вне списка) или «формат» (символы свои, форма чужая). Так тесты отличают два вида отказа.

Два решения, которые стоит принять до кода:

  • encode_text не нормализует строку. Иначе 7.3 молча превратится в допустимое 7,3, и ошибки разметки спрячутся. Нормализация — один раз на входе пайплайна; encode проверяет, что на выходе уже чисто.
  • decode_indices не принимает пусто. Пусто — не символ текста. Схлопывание повторов и удаление пусто — в уроке 10; сюда доходят только индексы настоящих символов.

Напиши сам

Сначала положите файлы в репозиторий из урока 1. Блоки ниже свёрнуты — разверните и скопируйте. Заготовка alphabet.py как на теге lesson-03-start: готовые функции уже есть, три блока TODO пустые. Можно набрать всё с нуля по спецификации — тесты те же.

Запуск команд — после того как допишете TODO (следующий раздел и блок «Команды»).

src/alphabet.py — заготовка (развернуть)
"""Урок 3 — «напиши сам»: алфавиты, контракт индексов и проверка формата меток.

Что нужно сделать (три пункта TODO ниже), пока не пройдут все тесты:
    python -m pytest tests/test_alphabet.py -q

Контракт индексов (общий для всего курса):
    blank = 0, символы алфавита нумеруются с 1 в порядке следования в строке.
"""
import re

BLANK_INDEX = 0

ALPHABETS = {
    "measurement": "0123456789,",
    "diameter": "0123456789/",
    "thickness": "0123456789,-",
}

_DASHES = "\u2010\u2011\u2012\u2013\u2014\u2212"  # разные «тире» и минус


# --- Готово: можно не трогать ------------------------------------------------

def get_alphabet(task: str) -> str:
    try:
        return ALPHABETS[task]
    except KeyError:
        raise ValueError(f"неизвестная задача {task!r}; доступны: {sorted(ALPHABETS)}") from None


def num_classes(task: str) -> int:
    """Размер выходного слоя модели: символы алфавита + blank."""
    return len(get_alphabet(task)) + 1


def normalize_label(text: str) -> str:
    """Приводит сырую метку к каноническому виду: без пробелов, ',' вместо '.', обычный '-'."""
    text = "".join(text.split())
    text = text.replace(".", ",")
    for dash in _DASHES:
        text = text.replace(dash, "-")
    return text


# --- TODO 1: форматы -----------------------------------------------------------
# Для каждой задачи нужен re.compile(...), который проверяется через fullmatch.
# Используйте [0-9], а не \d: \d совпадает и с цифрами других письменностей.
#
#   measurement: 1–3 цифры, затем (необязательно) запятая и 1–2 цифры.
#                Подходят: 7   7,3   105   0,85   123,45
#   diameter:    1–3 цифры, затем (необязательно) косая черта и 1–3 цифры.
#                Подходят: 57   12/14   120/80
#   thickness:   «число» = 1–2 цифры, затем (необязательно) запятая и 1–2 цифры.
#                Метка = число, затем (необязательно) дефис и ещё одно число.
#                Подходят: 10   1-2   1,5-2   1,5-2,5
#
# Пустая строка (пустая клетка) допустима для всех задач; её обрабатывает
# validate_format, регулярное выражение для неё писать не нужно.
FORMAT_PATTERNS: dict[str, "re.Pattern[str]"] = {}


# --- TODO 2: кодирование -------------------------------------------------------

def encode_text(text: str, task: str) -> list[int]:
    """Строка → список индексов (blank = 0, символы с 1).

    Строку заранее нормализует вызывающий код: символ вне алфавита — ValueError.
    Пустая строка даёт пустой список.
    """
    raise NotImplementedError


def decode_indices(indices, task: str) -> str:
    """Список индексов → строка.

    Индекс 0 (blank), отрицательный индекс и индекс за пределами алфавита — ValueError.
    """
    raise NotImplementedError


# --- TODO 3: проверка формата --------------------------------------------------

def validate_format(text: str, task: str) -> tuple[bool, str]:
    """Проверяет метку и возвращает (ok, причина).

    Порядок проверок:
      1. любой символ вне алфавита -> (False, причина со словом «алфавит»);
      2. пустая строка             -> (True, "");
      3. не подходит под формат    -> (False, причина со словом «формат»);
      4. иначе                     -> (True, "").
    """
    raise NotImplementedError
src/demo_labels.py — развернуть
"""Демо: алфавиты, нормализация, кодирование и проверка формата."""
from alphabet import (
    ALPHABETS, BLANK_INDEX, decode_indices, encode_text, get_alphabet,
    normalize_label, num_classes, validate_format,
)

CASES = [
    ("7.3", "measurement"),
    ("12/14", "diameter"),
    ("12/14", "measurement"),
    ("01.02.24", "measurement"),
    ("7.345", "measurement"),
    ("1\u20132", "thickness"),
    ("", "measurement"),
]


def main() -> None:
    print(f"blank = {BLANK_INDEX}, символы нумеруются с 1\n")
    for task in ALPHABETS:
        print(f"{task:<12} алфавит {get_alphabet(task)!r:<16} символов: {len(get_alphabet(task)):>2}"
              f"  классов с blank: {num_classes(task)}")
    print()
    for raw, task in CASES:
        norm = normalize_label(raw)
        try:
            ok, reason = validate_format(norm, task)
        except NotImplementedError:
            print(
                "TODO в src/alphabet.py ещё не дописаны "
                "(validate_format / encode_text / FORMAT_PATTERNS).\n"
                "Сначала доведите тесты до зелёных:\n"
                "  python -m pytest tests/test_alphabet.py -q\n"
                "Демо — после 12 passed (или смотрите эталон lesson-03)."
            )
            raise SystemExit(1) from None
        head = f"{raw!r:<11} {task:<12} → {norm!r:<10}"
        if ok:
            try:
                idx = encode_text(norm, task)
                back = decode_indices(idx, task)
            except NotImplementedError:
                print(
                    "TODO в src/alphabet.py ещё не дописаны (encode_text / decode_indices).\n"
                    "Сначала: python -m pytest tests/test_alphabet.py -q"
                )
                raise SystemExit(1) from None
            assert back == norm
            print(f"{head} → {idx} → {back!r}  OK")
        else:
            print(f"{head} → REJECT: {reason}")


if __name__ == "__main__":
    main()
tests/test_alphabet.py — развернуть
import sys
from pathlib import Path

import pytest

sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
from alphabet import (  # noqa: E402
    BLANK_INDEX, decode_indices, encode_text, get_alphabet,
    normalize_label, num_classes, validate_format,
)

ACCEPT = {
    "measurement": ["", "7", "7,3", "105", "0,85", "123,45", "007"],
    "diameter": ["", "57", "12/14", "120/80"],
    "thickness": ["", "10", "1-2", "1,5-2", "1,5-2,5", "12-15"],
}
REJECT_FORMAT = {
    "measurement": ["1234", "7,", ",5", "7,345", "7,3,1", "01,02,24", "1,2,3"],
    "diameter": ["12/", "/14", "12/14/16", "1234", "12/1234"],
    "thickness": ["-2", "1-", "1--2", "1-2-3", "123", "1,", "1,5,5"],
}
REJECT_ALPHABET = {
    "measurement": ["12/14", "7.3", "abc", "7 3"],
    "diameter": ["7,3", "7.3"],
    "thickness": ["7/3", "1_2"],
}


def test_blank_is_zero():
    assert BLANK_INDEX == 0
    assert encode_text("0", "measurement") == [1]


def test_num_classes():
    assert num_classes("measurement") == 12
    assert num_classes("diameter") == 12
    assert num_classes("thickness") == 13


def test_encode_examples():
    assert encode_text("7,3", "measurement") == [8, 11, 4]
    assert encode_text("12/14", "diameter") == [2, 3, 11, 2, 5]
    assert encode_text("1,5-2", "thickness") == [2, 11, 6, 12, 3]
    assert encode_text("", "measurement") == []


def test_encode_rejects_foreign_char():
    with pytest.raises(ValueError):
        encode_text("7.3", "measurement")      # точку нормализует вызывающий код
    with pytest.raises(ValueError):
        encode_text("12/14", "measurement")


def test_decode_roundtrip():
    for task, texts in ACCEPT.items():
        for text in texts:
            assert decode_indices(encode_text(text, task), task) == text


def test_decode_rejects_blank_and_out_of_range():
    with pytest.raises(ValueError):
        decode_indices([0], "measurement")      # blank — не символ
    with pytest.raises(ValueError):
        decode_indices([12], "measurement")     # в алфавите всего 11 символов
    with pytest.raises(ValueError):
        decode_indices([-1], "measurement")


def test_normalize_label():
    assert normalize_label("7.3") == "7,3"
    assert normalize_label(" 7, 3 ") == "7,3"
    assert normalize_label("1\u20132") == "1-2"
    assert normalize_label("01.02.24") == "01,02,24"
    assert normalize_label("") == ""


def test_validate_accepts():
    for task, texts in ACCEPT.items():
        for text in texts:
            ok, reason = validate_format(text, task)
            assert ok, f"{task}: {text!r} должна приниматься, а отклонена: {reason}"


def test_validate_rejects_by_format():
    for task, texts in REJECT_FORMAT.items():
        for text in texts:
            ok, reason = validate_format(text, task)
            assert not ok, f"{task}: {text!r} должна отклоняться"
            assert "формат" in reason, f"{task}: {text!r}: причина должна содержать «формат»"


def test_validate_rejects_by_alphabet():
    for task, texts in REJECT_ALPHABET.items():
        for text in texts:
            ok, reason = validate_format(text, task)
            assert not ok, f"{task}: {text!r} должна отклоняться"
            assert "алфавит" in reason, f"{task}: {text!r}: причина должна содержать «алфавит»"


def test_trap_passes_alphabet_but_not_format():
    # Дата после normalize_label целиком состоит из допустимых символов.
    text = normalize_label("01.02.24")
    assert all(ch in get_alphabet("measurement") for ch in text)
    ok, reason = validate_format(text, "measurement")
    assert not ok and "формат" in reason


def test_unknown_task():
    with pytest.raises(ValueError):
        get_alphabet("nope")
    with pytest.raises(ValueError):
        encode_text("1", "nope")

Подсказки:

Ловушки RegExp: \d vs [0-9], match vs fullmatch

  • У str.find на ненайденный символ — -1. Без проверки pos + 1 даст 0, то есть индекс пусто. Модель получила бы «нет символа» вместо ошибки. Это самая коварная ловушка урока (схема ниже).
  • re.match проверяет только начало строки. Для формата нужен fullmatch: иначе 1234 пройдёт как 123 с хвостом.
  • В выражениях пишите [0-9], не \d: в Python \d совпадает и с цифрами других письменностей (например арабо-индийской ٣).

Тихая ошибка: find → -1 → pos+1 = пусто

Упражнение (напиши сам)

Основное — три блока TODO в alphabet.py: FORMAT_PATTERNS, пара encode_text / decode_indices, validate_format. На заготовке pytest сначала даст 10 failed, 2 passed — так и должно быть, пока TODO пустые. Допишите функции и перезапускайте тесты, пока не станет 12 passed.

Бонус: придумайте строку-ловушку — проходит алфавит, но не формат (например лишний разделитель). Добавьте тест в tests/test_alphabet.py (имя вроде test_trap_passes_alphabet_but_not_format).

Когда тесты зелёные — переходите к «Команды» и сверяйте демо с «Ожидаемое». Если застряли, ниже полное решение (лучше открыть после попытки).

Решение src/alphabet.py (откройте после попытки)

Полный файл с тега lesson-03. В комментариях к encode_text — та самая ловушка find → пусто.

"""Алфавиты задач, контракт индексов и проверка формата меток.

Контракт индексов (общий для всего курса):
    blank = 0, символы алфавита нумеруются с 1 в порядке следования в строке.
"""
import re

BLANK_INDEX = 0

ALPHABETS = {
    "measurement": "0123456789,",
    "diameter": "0123456789/",
    "thickness": "0123456789,-",
}

# Формат метки поверх алфавита (пустая строка = пустая клетка, допустима всегда).
# Пишем [0-9], а не \d: \d в Python совпадает и с цифрами других письменностей.
FORMAT_PATTERNS = {
    "measurement": re.compile(r"[0-9]{1,3}(,[0-9]{1,2})?"),
    "diameter": re.compile(r"[0-9]{1,3}(/[0-9]{1,3})?"),
    "thickness": re.compile(r"[0-9]{1,2}(,[0-9]{1,2})?(-[0-9]{1,2}(,[0-9]{1,2})?)?"),
}

_DASHES = "\u2010\u2011\u2012\u2013\u2014\u2212"  # разные «тире» и минус


def get_alphabet(task: str) -> str:
    try:
        return ALPHABETS[task]
    except KeyError:
        raise ValueError(f"неизвестная задача {task!r}; доступны: {sorted(ALPHABETS)}") from None


def num_classes(task: str) -> int:
    """Размер выходного слоя модели: символы алфавита + blank."""
    return len(get_alphabet(task)) + 1


def normalize_label(text: str) -> str:
    """Приводит сырую метку к каноническому виду: без пробелов, ',' вместо '.', обычный '-'."""
    text = "".join(text.split())
    text = text.replace(".", ",")
    for dash in _DASHES:
        text = text.replace(dash, "-")
    return text


def encode_text(text: str, task: str) -> list[int]:
    """Строка → список индексов (blank = 0, символы с 1).

    Строку заранее нормализует вызывающий код: символ вне алфавита — ошибка.
    """
    alphabet = get_alphabet(task)
    indices = []
    for ch in text:
        pos = alphabet.find(ch)  # -1, если символа нет
        if pos < 0:  # иначе pos+1 даст 0 = blank — коварная ошибка
            raise ValueError(f"символ {ch!r} вне алфавита задачи {task!r}")
        indices.append(pos + 1)  # символы с 1, blank = 0
    return indices


def decode_indices(indices, task: str) -> str:
    """Список индексов → строка. Индекс 0 (blank) и выход за алфавит — ошибка."""
    alphabet = get_alphabet(task)
    chars = []
    for i in indices:
        if not 1 <= i <= len(alphabet):  # blank и «мусор» сюда не входят
            raise ValueError(f"индекс {i} недопустим для задачи {task!r} (допустимо 1..{len(alphabet)})")
        chars.append(alphabet[i - 1])  # индекс 1 → первый символ алфавита
    return "".join(chars)


def validate_format(text: str, task: str) -> tuple[bool, str]:
    """Проверяет метку: сначала алфавит, потом формат. Возвращает (ok, причина)."""
    alphabet = get_alphabet(task)
    for ch in text:
        if ch not in alphabet:  # сначала алфавит, потом формат
            return False, f"символ {ch!r} вне алфавита"
    if text == "":
        return True, ""
    if FORMAT_PATTERNS[task].fullmatch(text) is None:  # match() оставил бы хвост
        return False, "формат не соответствует задаче"
    return True, ""

Что заметить: проверка pos < 0 обязательна; формат — через fullmatch; в регулярках — [0-9], не \d.

Команды

Когда TODO дописаны (или вы сверились с решением), из корня репозитория:

python -m pytest tests/test_alphabet.py -q   # цель: 12 passed
python src/demo_labels.py

Если демо запустить раньше, чем заполнены TODO, скрипт напомнит дописать alphabet.py — это не поломка репозитория.

Ожидаемое

Вывод demo_labels.py на решении (тег lesson-03). Читайте стрелки слева направо: сырая строка → нормализация → индексы или REJECT. В терминале слово blank — это наше пусто (так напечатано в коде демо).

blank = 0, символы нумеруются с 1

measurement  алфавит '0123456789,'    символов: 11  классов с blank: 12
diameter     алфавит '0123456789/'    символов: 11  классов с blank: 12
thickness    алфавит '0123456789,-'   символов: 12  классов с blank: 13

'7.3'       measurement  → '7,3'      → [8, 11, 4] → '7,3'  OK
'12/14'     diameter     → '12/14'    → [2, 3, 11, 2, 5] → '12/14'  OK
'12/14'     measurement  → '12/14'    → REJECT: символ '/' вне алфавита
'01.02.24'  measurement  → '01,02,24' → REJECT: формат не соответствует задаче
'7.345'     measurement  → '7,345'    → REJECT: формат не соответствует задаче
'1–2'       thickness    → '1-2'      → [2, 12, 3] → '1-2'  OK
''          measurement  → ''         → [] → ''  OK

Тесты: 12 passed.

Сверка с «Предскажи»:

  1. у measurement и diameter по 12 классов, у thickness — 13;
  2. запятая в measurement — индекс 11;
  3. дата 01.02.24 отклонена по формату, не по алфавиту: символы допустимы, форма чужая.

Запомните пару «12 классов у measurement и diameter» — позже в курсе (урок 16) к этому вернёмся. Закоммитьте урок, когда вывод совпал.

Если не так

  • Сообщение про TODO из demo_labels.py (или раньше — traceback NotImplementedError) — вы запустили демо до дописывания alphabet.py. Сначала pytest до 12 passed.
  • encode_text("7.3", ...) вернул [8, 0, 4] — нет проверки после find; добавьте if pos < 0. Точка вне алфавита measurement превратилась в пусто.
  • 1234 проходит формат — используете re.match вместо fullmatch.
  • Отклонены 007 или 123,45 — регулярка слишком строгая; сверьте с таблицей форматов (ведущие нули и 123,45 должны проходить).
  • Тест на причину отказа падает — в тексте причины должны быть слова «алфавит» или «формат».
  • ModuleNotFoundError: alphabet — pytest и demo запускайте из корня вашего репозитория.

Чек-лист

  • src/alphabet.py, demo_labels.py и тесты написаны в вашем репозитории и закоммичены
  • demo_labels.py совпадает с блоком «Ожидаемое»
  • pytest → 12 passed
  • Сверены три прогноза из «Предскажи»

FAQ

Что такое «класс» в этом уроке?

Один пункт в меню ответа модели. Для measurement меню такое: пусто, 0, 1, …, 9, , — всего 12 классов. Метка 7,3 — не один класс, а последовательность классов [8, 11, 4]. Детектор пустых клеток (уроки 7–9) выбирает один класс на весь кроп; читатель строки (позже) выбирает класс на каждом шаге вдоль картинки.

Почему пусто — отдельный класс с индексом 0?

Модели чтения строк понадобится ответ «на этой колонке символа нет» — отдельно от цифры 0 в тексте. Если отдать символам индексы с нуля или смешать пусто с «нулём», обучение и расшифровка разъедутся. Поэтому индекс 0 заранее занят под пусто (blank в коде), а цифра 0 живёт с индекса 1. Как пусто участвует в CTC — в уроке 10.

Зачем три разных алфавита, если цифры везде?

На бланке (форме) разные клетки: измерение с десятичной запятой, диаметр со слэшем, толщина с дефисом диапазона. Один «универсальный» алфавит раздует выход модели и смешает форматы — сеть начнёт путать задачи.

Почему normalize_label в эталоне уже написан, а encode — нет?

Нормализация — политика пайплайна («как мы храним метку»). Кодирование и формат — ядро контракта с моделью; его и тренируем писать по спецификации тестов. В своём репозитории normalize_label вы всё равно набираете — просто это не упражнение с ловушками.

Принимаем ли мы 007 как измерение?

Да, если строка уже каноническая: формат разрешает ведущие нули в целой части. Это осознанное решение курса; в проде можно добавить отдельную нормализацию числа.

Можно ли вызывать encode_text на сырой строке с точкой?

Не стоит: получите ValueError или, без проверки find, тихую порчу через пусто. Сначала normalize_label, потом validate_format, потом encode — так же будет в генераторе урока 4.

Почему у measurement и diameter одинаково 12 классов?

Одинаковое число символов в алфавите (11 + пусто). Это пригодится позже, когда будем делить головы модели (урок 16).

Что дальше

Контракт меток готов. Следующий шаг — Урок 4. Синтетический генератор и all.jsonl. Dataset и аугментации — уроки 5–6.

Эталон для сверки: lesson-03. Клонировать его не нужно. Предыдущий: Урок 2. Картинка → тензор.

Комментарии

Загрузка комментариев…