1. Введение
Нормализация справочников НСИ — головная боль аналитиков: в базе десятки тысяч записей, и каждая будто создана по своим правилам. И это еще не все трудности: в базе полно дублей, форматы данных пляшут, в полях то лишние символы, то транслитерация, то опечатки от ручного ввода. Мы не раз сталкивались с этим в своих проектах.
Решений хватает, но мы остановили выбор на LLM. Модель хорошо справляется с разбором неструктурированных строк на атрибуты, если правильно её настроить. Однако на практике мы столкнулись с тем, что успех LLM-нормализации на 90% определяется качеством few-shot примеров. Чем набор примеров репрезентативнее, тем стабильнее результат. Но как найти те самые, хорошие примеры в огромной массе разнородных записей? Вручную, особенно не имея достаточной экспертизы, проводить глубокий анализ трудоемко и неэффективно. Поэтому мы решили пойти другим путём — автоматизировать подбор.
В этой статье мы разберем два подхода к автоматизации процесса подбора примеров для обучения, проверим их на реальных данных и выясним, какой из них и в каких условиях работает лучше. Посмотрим, что нам покажут «автоматические эксперты».
2. Постановка задачи и метод оценки
Начнем с постановки задачи. Есть справочник МТР, в котором находятся различные строки с товарными позициями, например «Кабель ВВГнг 3х2.5 кв.мм, серая изоляция, 100м». Нужно разложить их по атрибутам:
|
Атрибут |
Значение |
|
Вид продукции |
Кабель |
|
Марка |
ВВГнг |
|
Сечение |
3×2,5 кв.мм |
|
Цвет изоляции |
серый |
|
Длина |
100м |
Чтобы LLM справилась с этим хорошо, нужно показать ей правильные примеры. Случайные 10 строк из базы здесь не подойдут. Если в выборке 800 кабелей ВВГ из 1000, случайный отбор с высокой вероятностью даст 7-8 однотипных примеров, и модель просто не увидит остальные паттерны. Нужны такие примеры, которые одновременно похожи на то, что будем выгружать из систем, и которые покрывают весь спектр данных.
С этим нам может помочь метод MMR (Maximum Marginal Relevance). На каждом шаге из оставшихся кандидатов выбирается тот, кто близок к центру выборки и при этом непохож на уже отобранные примеры:
Где:
-
Sim — косинусное сходство между векторами;
-
λ — параметр баланса;
-
претендент — любой ещё не отобранный пример из выборки;
-
среднее по выборке — усреднённый вектор всех кандидатов, условный центр тяжести входных данных;
-
отобранные — примеры, выбранные на предыдущих шагах.
На наших данных оптимальным оказалось λ = 0.35-0.4. Если поднять λ выше, алгоритм начнёт выбирать только самые типичные примеры и потеряет разнообразие. Если опустить ниже, примеры станут слишком непохожими друг на друга и перестанут отражать основной массив.
MMR работает с векторами, поэтому сначала нужно превратить строки с наименованиями товарных позиций в векторы. Мы попробовали два способа:
-
Семантические эмбеддинги на базе BGE-M3, которые строят векторы на основе смысла текста, учитывая контекст и связи между словами.
-
Частотный анализ N-грамм, который строит векторы на основе того, какие слова и словосочетания встречаются в строке, присваивая редким терминам, которые несут больше информации о конкретной позиции, больший вес.
Для оценки качества отобранных few-shot примеров будем использовать F1-score по результатам нормализации товарных позиций. Проще говоря, берём промпт с подобранными примерами, прогоняем через него реальные строки и смотрим насколько качественно модель извлекла атрибуты. Порог приемлемого результата в нашем проекте составляет 0.92. Если значение окажется ниже, эксперт будет дорабатывать выборку вручную.
3. Алгоритм структурной кластеризации с MMR отбором (Structural Cluster MMR, SCM)
Идея простая: группируем строки по структурному паттерну, из каждой группы берём одного типичного представителя. Например, «Кабель ВВГнг 3х2.5 кв.мм» и «Кабель ВВГнг 5х6 кв.мм» — у обоих структура схожая, следовательно для обучения достаточно одного примера.
Шаг 1. Структурный отпечаток
Каждая строка превращается в шаблон, в котором буквы заменяются на «A», а цифры на «0», повторы схлопываются. Регистр и конкретные значения исчезают, остаётся скелет строки..
def getuniversal_fingerprint(self, text: str) -> str: t = text.lower() t = re.sub(r'\s+', ' ', t).strip() t = re.sub(r'[a-zа-яё]', 'A', t) t = re.sub(r'\d', '0', t) return re.sub(r'A+', 'A+', re.sub(r'0+', '0+', t))
Результат:
"Кабель ВВГнг 3х2.5 кв.мм" → "A+ A+ 0+A+0+.0+ A+.A+" "Кабель ВВГнг 5х6 кв.мм" → "A+ A+ 0+A+0+ A+.A+" "Пруток бронзовый 60мм" → "A+ A+ 0+A+"
Строки с одинаковым скелетом попадают в общий кластер. Конкретные характеристики нас больше не отвлекают, мы смотрим только на структуру.
Шаг 2. Лидер кластера
Внутри кластера нам нужен один самый «типичный» пример. Считаем центроид (средний вектор всех членов кластера) и берём реальный элемент, ближайший к этому центроиду.
m_embs = self.model.encode([m.lower() for m in members], batch_size=8)centroid = np.mean(m_embs, axis=0).reshape(1, -1)sims = cosine_similarity(m_embs, centroid).flatten()leader = members[np.argmax(sims)]
Шаг 3. Семантическое слияние кластеров
Если кластеров получилось слишком много, сливаем семантически близкие. «Болт М8х20 нержавейка» и «Болт М10х30 нерж.сталь» имеют разные структурные отпечатки, но для нас это, по сути, одно и то же, нет необходимости записывать два таких примера в качестве обучающих примеров.
Прогоняем лидеров кластеров через матрицу косинусных сходств и сливаем те, которые ближе заданного порога:
sim_matrix = cosine_similarity(leader_embs)for i in range(len(initial_leaders)): for j in range(i + 1, len(initial_leaders)): if sim_matrix[i][j] >= merge_threshold: # merge cluster j into cluster i
Шаг 4. MMR-отбор финальных примеров
Получаем список лидеров итоговых кластеров после шага 3. Из них выбираем требуемое количество методом MMR. Они и будут примерами для обучения модели:
query_vec = np.mean(final_embs, axis=0).reshape(1, -1) # центр всей выборкиfor _ in range(k): # требуемое количество примеров for idx in remaining: # список лидеров rel = cosine_similarity(final_embs[idx], query_vec)[0][0] div = np.max(cosine_similarity(final_embs[idx], final_embs[selected])) if selected else 0 mmr = lmb rel - (1 - lmb) * div
Схема работы SCM:
Входные данные (N строк) Структурный отпечаток → первичные кластеры Центроид + медоид → лидеры кластеров Матрица сходств → слияние (merge_threshold) Пересчёт лидеров MMR-отбор k примеров (λ) few-shot выборка
Используемые инструменты: Python, sentence-transformers (BGE-M3), numpy, sklearn.
4. Алгоритм лексического частотного анализа с MMR отбором (LFM)
LFM (Lexical Frequency MMR) работает только на статистике. Ключевая идея в том, что мы считаем частоту вхождения N-грамм (в нашем случае униграмм и биграмм) в строки с наименованиями товарных позиций и учитываем, что в технических спецификациях редкие термины важнее частых. Например, «Болт» встречается везде, он мало о чём говорит, а «БрАЖ9-4» или «ГОСТ 1628-2019» редки, но сразу описывают уникальный класс товара.
Что нужно подготовить заранее: CSV-таблицы с количеством вхождений N-грамм по исходной выборке.
Шаг 1. Веса из частотных таблиц
Чем чаще N-грамма, тем меньше ее вес:
# Для униграммself.weights[term] = 1.0 / math.log(1 + count)# Для биграмм ‒ 1.5x: биграмма 'БрАЖ9-4 ГОСТ' несёт# больше информации, чем просто 'БрАЖ9-4'self.weights[term] = (1.0 / math.log(1 + count)) * 1.5
Шаг 2. Токенизация и векторизация
def _tokenize(self, text): words = re.findall(r'[a-zа-яё0-9-]+', text.lower()) return words + [' '.join(words[i:i+2]) for i in range(len(words)-1)]def get_vector(self, text): tokens = self._tokenize(text) return {t: self.weights[t] for t in tokens if t in self.weights}
Пример для строки «Пруток бронзовый БрАЖ9-4 60 мм»:
|
Токен |
Вес |
Комментарий |
|
60мм БрАЖ9-4 |
1.31 |
Реже всего встречается |
|
БрАЖ9-4 |
0.87 |
|
|
Бронзовый |
0.35 |
|
|
Пруток |
0.12 |
Встречается в каждой строке |
Шаг 3. Косинусное сходство по разреженным векторам
Векторы разреженные, считаем сходство только по общим терминам. В данном методе не используются sklearn или torch, из-за чего легко переписывается, например на C#:
def cosine_sim(self, v1, v2): common = set(v1.keys()) & set(v2.keys()) if not common: return 0.0 dot = sum(v1[t] * v2[t] for t in common) norm1 = math.sqrt(sum(v**2 for v in v1.values())) norm2 = math.sqrt(sum(v**2 for v in v2.values())) return dot / (norm1 * norm2) if norm1 and norm2 else 0.0
Шаг 4. MMR-отбор
MMR работает по той же формуле, что в разделе 2, но вместо эмбеддингов используются частотные векторы. Центром выборки является усреднённый частотный вектор по всем кандидатам.
Схема работы LFM:
Входные данные (N строк)Загрузка весов из CSV-таблиц частотТокенизация → разреженные частотные векторыУсреднённый вектор по всей выборке → центр тяжестиMMR-отбор k примеров (λ)Few-shot выборка
Используемые инструменты: Python (stdlib + numpy).
5. Сравнительная таблица алгоритмов
SCM хорош на разнородных, плохо структурированных данных, где важны синонимы и парафразы. Но нужно ≥200 строк в выборке, в которой идет отбор обучающих примеров, и инфраструктура, которая потянет энкодер с мощным CPU или GPU.
LFM предпочтителен, когда спецификации насыщены буквенно-цифровыми кодами, артикулами и стандартами (ГОСТ, марки материалов). Алгоритм хорошо улавливает их форму, работает быстро и даёт объяснимый результат.
|
Критерий |
SCM |
LFM |
|
Быстродействие |
Десятки сек / минуты (использование GPU ускоряет) |
Доли секунды |
|
Требования к ресурсам |
Высокие: CPU/GPU, модель ~несколько ГБ + torch |
Низкие: достаточно стандартного сервера/ПК. |
|
Production-интеграция |
Сложно: Python-окружение, API-прослойка, раздельное управление памятью в Python и .NET. |
Просто: логика переписывается на C# ~200-300 строк |
|
Работа с семантикой |
Отличная: понимает синонимы, парафразы, контекст |
Ограниченная: буквальные совпадения, без синонимов |
|
Интерпретируемость |
Низкая: векторы скрытых состояний — «чёрный ящик» |
Высокая: выбор объясним через веса ключевых терминов |
|
Настраиваемость |
Средняя: λ и порог слияния, модель фиксирована |
Высокая: легко менять веса, N-граммы, методы токенизации (например, токенизация по маскам) |
|
Устойчивость к шуму в данных |
Средняя: может «переусложнять» из-за шума |
Высокая: несущественные вариации не ломают токены |
|
Когда выбирать |
Разнородные тексты с синонимами, выборка ≥ 200 строк |
Коды, артикулы, ГОСТы; C#-интеграция; важна скорость. |
6. Тестирование на реальных данных
Для практической проверки алгоритмы были интегрированы в технологическую платформу «Преферентум» (кластер SL Soft AI), ведущего разработчика решений для нормализации НСИ предприятий.
Мы протестировали алгоритмы на семи товарных группах. Везде 10 примеров в few-shot, LLM фиксирована. Меняется только способ отбора этих 10 примеров.
В таблице четыре алгоритма:
SCM и LFM ‒ базовые версии, описанные ранее.
SCM-QS и LFM-QS ‒ версии с квинтильной стратификацией. Вся выборка сортируется по близости к центроиду и делится на 5 зон: от аномалий (редкие, нетипичные записи) до ядра (самые типичные). Из каждой зоны берём по 2 примера, итого те же 10, но теперь они гарантированно покрывают весь спектр данных.
Неожиданный вывод: LFM на задаче декомпозиции технических спецификаций показал сопоставимый (а местами и выше) F1 при 10-100-кратно меньших требованиях к ресурсам.
|
Товарная группа |
Исх. выборка |
Тест. выборка |
Кол-во |
F-мера |
|||||||||
|
Отобранная тестовая выборка |
Случайная тестовая выборка |
||||||||||||
|
Случ. few-shot |
SCM |
LFM |
SCM-QS |
LFM-QS |
Случ. few-shot |
SCM |
LFM |
SCM-QS |
LFM-QS |
||||
|
Трансфор- маторы |
1037 |
100 |
10 |
0,9144 |
0,88 |
0,9266 |
0,9298 |
0,9255 |
0,9159 |
0,926 |
0,9218 |
0,9284 |
0,9344 |
|
Кувалды |
85 |
85 |
10 |
— |
— |
— |
— |
— |
0,9621 |
0,9761 |
0,9432 |
0,9224 |
0,931 |
|
Молотки |
120 |
120 |
10 |
— |
— |
— |
— |
— |
0,9412 |
0,9678 |
0,9325 |
0,9476 |
0,9626 |
|
Прутки |
194 |
100 |
10 |
0,9312 |
0,9328 |
0,9797 |
0,9782 |
0,9656 |
0,9502 |
0,9515 |
0,9818 |
0,9803 |
0,9717 |
|
Переходы муфты втулки |
484 |
100 |
10 |
0,896 |
0,9093 |
0,9106 |
0,9066 |
0,9431 |
0,9128 |
0,9173 |
0,9217 |
0,9208 |
0,9613 |
|
Датчики давления |
80 |
80 |
10 |
— |
— |
— |
— |
— |
0,9314 |
0,968 |
0,9611 |
0,943 |
0,9583 |
|
Болты |
100 |
100 |
10 |
— |
— |
— |
— |
— |
0,9581 |
0,9793 |
0,9684 |
0,9757 |
0,9703 |
Прочерки (‒): для групп с малым объёмом данных размер исходной выборки совпадает с тестовой, отдельной отобранной выборки нет.
Результаты тестирования
Апробация алгоритмов на платформе «Преферентум» на типичных товарных группах (а это совсем небольшая выборка из исследованных) выявила следующие закономерности:
-
Все алгоритмы превосходят случайный отбор, прирост F1 от 1% до 5% на 6 из 7 групп.
-
LFM эффективен для структурированных данных с буквенно‑цифровыми кодами («Прутки», «Датчики давления»):
-
«Прутки»: +5,21 % (LFM), +5,05 % (SCM‑QS);
-
«Датчики давления»: +3,93 % (SCM), +2,89 % (LFM‑QS).
-
-
CM лучше на малых разнородных группах («Болты», «Кувалды»):
-
«Болты»: +2,21 %;
-
«Кувалды»: +1,45 %.
-
-
Квинтильные версии эффективны на больших неоднородных группах:
-
«Переходы, муфты, втулки»: LFM‑QS +5,26 %;
-
«Прутки»: SCM‑QS +5,05 %.
-
Проблемные случаи, с которыми столкнулись
-
«Трансформаторы» (1037 строк): SCM просел на 3,76%. Алгоритм склеил слишком крупные кластеры, few-shot потерял разнообразие. Решение: перед обучением разбивать на подгруппы.
-
«Кувалды» (85 строк): SCM‑QS упал на 4,13 %, аномалии от стратификации сбивают модель.
-
На маленьких однородных группах (например, «Молотки») квинтильный отбор может ухудшать результаты на 0,25-3,89 % тоже из-за аномалий.
Выводы по апробации
Универсального алгоритма нет, но по итогам экспериментов у нас сложилось понимание, когда что использовать. Если данных много и они неоднородные, лучше брать версии с квинтильной стратификацией. Если группа маленькая, до 100 строк, то SCM без стратификации справляется лучше. Если в данных много кодов, артикулов и ГОСТов, то LFM покажет хороший результат.
Автоматический отбор примеров работает и заметно сокращает ручную работу экспертов. Теперь не нужно каждый раз вручную подбирать примеры под новую товарную группу, алгоритм делает это сам и достаточно быстро, чтобы использовать его и для тестирования моделей без участия предметных специалистов.
Авторы: Юрий Грецкий, Данил Михайлов.
ссылка на оригинал статьи https://habr.com/ru/articles/1042468/