Файл:
bed_bot.py. Стек: TF-IDF (символьные 3-граммы) + LinearSVC + расстояние Левенштейна + лемматизация (pymorphy2).
| Метод | В проекте | Где в коде |
|---|---|---|
| Расстояние Левенштейна | ✅ есть | _phrase_matches (582), generate_answer
(760) |
| Лемматизация (pymorphy2) | ✅ есть | lemmatize (54), внутри clear_phrase |
| Стемминг | ❌ нет (вместо него лемматизация) | — |
| Косинусное сходство | ❌ нет (вместо него Левенштейн) | — |
| TF-IDF векторизация | ✅ есть | TfidfVectorizer (563) |
| LinearSVC (мультиклассовая) | ✅ есть | clf = LinearSVC() (565) |
| Токенизация (символьная / по словам) | ✅ есть | analyzer='char' (563), .split() |
| Сентимент-анализ | ❌ нет | — |
| Контекст диалога (память) | ✅ есть | hist_theme, selected_product |
Запомнить намертво: у меня — лемматизация (не стемминг), близость текстов меряю Левенштейном (не косинусом), определяю намерение (не тональность).
Минимальное число операций (вставка / удаление / замена символа), чтобы превратить одну строку в другую. «кот»→«код» = 1.
У меня: через nltk.edit_distance,
нормированное на длину (distance / len): -
_phrase_matches() — сравнение реплики с примерами намерений
(порог 0.4–0.5). - generate_answer() — поиск похожего
вопроса в датасете (порог < 0.2).
Грубое отсечение окончаний по правилам до основы (стема). «бегущий/бегал»→«бег». Стем может быть не реальным словом.
У меня НЕ используется. Вместо него —
лемматизация (pymorphy2, строка 54):
приведение к словарной начальной форме. «кроватей»→«кровать».
Разница: стемминг = обрубок по правилам; лемматизация =
словарная форма по морфологии. Для русского лемматизация точнее.
Мера близости двух векторов по косинусу угла между ними (от −1 до 1). В NLP тексты → векторы (TF-IDF), косинус показывает смысловую близость независимо от длины.
У меня НЕ используется. Близость текстов меряю
расстоянием Левенштейна. Косинус был бы альтернативой —
TF-IDF-инфраструктура для него у меня уже есть, можно было бы сравнивать
вектор реплики и примеров через cosine_similarity. Выбрал
Левенштейн осознанно: фразы короткие, важна устойчивость к
опечаткам.
Гибрид в два этапа (classify_intent →
classify_intent_by_theme, 596–659):
TfidfVectorizer(analyzer='char', ngram_range=(3,3)) →
обучение LinearSVC. Для новой реплики
clf.predict()._phrase_matches; если нет — резервный ручной
перебор.theme_app/theme_gen, применяются с учётом
истории hist_theme (позволяет понимать «да»/«давай»).Кратко: TF-IDF (char 3-gram) + LinearSVC, подстрахованные Левенштейном и контекстом.
Разбиение текста на единицы (токены): слова, символы, подслова, предложения. Первый шаг обработки.
У меня: подключён nltk punkt; основная
обработка — в clear_phrase() (нижний регистр, только
русские буквы, .split()). А
TfidfVectorizer(analyzer='char') делает символьную
токенизацию — режет на 3-символьные куски.
У меня — мультиклассовая. Классов ≈ 20 (по числу
намерений: hello, buy_bed,
ask_price, back_pain…). LinearSVC
различает все сразу (под капотом — one-vs-rest).
Превращение текста в числовой вектор, потому что ML работает только с числами.
Да, есть — TF-IDF (TfidfVectorizer,
563): - TF — частота элемента в тексте; -
IDF — насколько он редкий по всей выборке (редкие =
информативнее).
Реплика → вектор весов её символьных 3-грамм. Зачем:
чтобы LinearSVC мог обучаться и классифицировать.
Определение тональности текста: позитив / негатив / нейтрально.
У меня НЕТ. Бот определяет намерение (что человек хочет), а не настроение. Для продающего бота ключевое — намерение. Добавить можно отдельным классификатором тональности.
hist_theme, выбранный
товар selected_product, отдельно на каждого пользователя
Telegram.generate_answer).Принцип: не «не успел», а «выбрал другое, и вот почему».
Стемминг: > Сознательно не использовал — для
русского он слишком грубый, даёт обрубки. Взял лемматизацию через
pymorphy2: словарная форма с учётом морфологии, точнее. Задачу
нормализации слов решает она. > Если надавят: сработал бы
SnowballStemmer, но с бóльшим числом ошибок склейки.
Косинусное сходство: > Близость меряю
Левенштейном — запросы короткие, важна устойчивость к опечаткам, а он
ловит посимвольные различия. Косинус сильнее на длинных текстах. >
Если надавят: легко встроить — TF-IDF у меня уже есть, можно
считать cosine_similarity вместо Левенштейна. Выбор
осознанный.
Сентимент-анализ: > Моя задача — намерение (что хочет), а не тональность (как относится). Для бота-продавца ключевое — намерение. > Если надавят: добавил бы вторую модель тональности, чтобы подстраивать тон или звать оператора при негативе.
Формула TF-IDF? → TF × IDF. TF = частота в документе; IDF = log(всего документов / документов с элементом). Частые везде → низкий вес, редкие информативные → высокий.
Почему символьные n-граммы, а не слова? (самый частый вопрос) → 1) устойчивость к опечаткам («спсибо»≈«спасибо»); 2) формы слова дают общие n-граммы; 3) мало данных (10–13 примеров на класс) — пословный словарь слишком разреженный.
Что такое ngram_range=(3,3)? → куски по
3 символа: «привет»→«при»,«рив»,«иве»,«вет». Тройка — компромисс между
шумом (1–2) и редкостью (4+).
Если слово не встречалось при обучении? → для пословной модели — нулевой вектор; у меня char-граммы дают частичное совпадение по кускам.
Что такое опорные векторы? → точки выборки ближе всего к разделяющей границе; именно они задают её положение. SVM максимизирует зазор (margin) между классами.
Почему LinearSVC, а не Байес/нейросеть? → хорош на разреженных высокоразмерных данных (TF-IDF), быстрый, не переобучается на малой выборке. Нейросеть избыточна.
SVM же бинарный — как несколько классов? (ловушка) → стратегия one-vs-rest: для каждого намерения свой бинарный классификатор «класс против всех», на выходе — максимум решающей функции.
Что такое гиперплоскость / линейная разделимость? → граница, разделяющая классы в пространстве признаков (в 2D — прямая). Линейный SVM считает, что классы делятся такой плоскостью; в TF-IDF высокой размерности это обычно так.
Если опечаток слишком много? → расстояние превысит порог, совпадения нет → сработает ML или заглушка. Бот не выдаст неверный ответ, честно скажет «не понял».
Зачем нормировать на длину? → чтобы порог не зависел от длины строки; деление даёт долю несовпадения, один порог работает для любых фраз.
Сложность алгоритма? → O(m×n) через динамическое программирование (матрица). Поэтому датасет сначала фильтрую по словам и длине, чтобы не считать все пары.
Почему гибрид ML + Левенштейн, не избыточно? → подстраховка: ML всегда выдаёт класс даже на мусор, Левенштейн отсекает ложные срабатывания; ручной перебор ловит ошибки ML.
Это ML или правила? (каверзный) → и то, и другое. Ядро — настоящее ML (TF-IDF + LinearSVC на данных), поверх — эвристики ради предсказуемости узкого бота.
Как помнит разговор? → hist_theme (стек
тем) + selected_product, отдельно на каждого пользователя
(context.chat_data), не в глобальной переменной.
Обучается в процессе общения? → нет, офлайн-обучение один раз при старте на фиксированных примерах. Дообучения на лету нет.