Исследовательская работа

Оптимизация покрытия FAQ-системы с применением диаграммы Вороного

Аннотация

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

1. Введение

1.1 Контекст и мотивация

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

Система прошла реальную эксплуатацию: 14 репетиторов использовали её в течение 4 месяцев с реальными клиентами. За это время стало очевидно, что качество автоматических ответов напрямую зависит от того, насколько хорошо составлена FAQ-база. Возник закономерный вопрос: существует ли научный способ оценить это качество?

1.2 Исследовательский вопрос

Как измерить, насколько хорошо FAQ-база покрывает реальное пространство вопросов клиентов — и где находятся семантические пробелы?

1.3 Структура работы

Работа организована следующим образом: раздел 2 описывает текущую техническую реализацию системы; раздел 3 формулирует метод оценки покрытия; раздел 4 представляет реализацию диаграммы Вороного; раздел 5 содержит результаты и интерпретацию; раздел 6 — рекомендации и выводы.

2. Описание системы

2.1 Архитектура

  1. Програмно создается предварительно заполненная таблица (google sheets) с двумя колонками “Вопрос” и “Ответ”. Репетитор заполняет ее по своему усмотрению. Он может добавить тысячи строк в этой таблице, если захочет.

google_sheets_mock.png
  1. После чего совершается api запрос к серверу google sheets, чтобы получить всю информацию из этой таблицы. В ответе мы получаем JSON (What Is JSON, n.d.) со всеми записями.
[
  {
    "row_number": "int",
    "Вопрос": "question",
    "Ответ": "answer"
  },
  {...}, // Здесь структура аналогична.
  {...}, // Здесь тоже.
  {...} // И даже здесь.
]
  1. Проходим по каждому элементу и конкатенируем поля “Вопрос” и “Ответ” в одну строку, вот так:

    String = {{ $json['Вопрос'] }} {{ $json['Ответ'] }}

    После этого мы получаем:

    String = "question answer"
  2. Получив строку, мы загружаем ее в базу данных (в нашем случае это Postgres с векторным расширением). При вставке данных я использую модель mistral-embed для создания эмбединга (What Is Embedding? IBM, n.d.), именно в этом формате осуществляется добавление записи в базу данных.

  3. Когда система получает вопрос, она использует семантический поиск и извлекает 3 наиболее вероятные строки (наши конкатенированные строки) и на их основе формирует окончательный ответ.

2.2 Хранение данных

В нашей базе данных есть таблица, в которой хранятся все записи FAQ. Вот структура этой таблицы:

(
    id uuid NOT NULL DEFAULT gen_random_uuid(),   // Уникальный иднетификатор
    text text COLLATE pg_catalog."default",       // Конкатенированная строка
    metadata jsonb,                               // Дополнительные данные для работы системы (номера пользователей)
    embedding vector                              // Сам вектор (эмбединг)
)

3. Метод оценки покрытия

3.1 Цель оптимизации

Цель данного исследования — не улучшить алгоритм поиска, а оценить качество самого FAQ-контента: достаточно ли разнообразны записи, чтобы покрыть реальные вопросы клиентов.

Для этого необходимо: - представить FAQ и вопросы клиентов в едином семантическом пространстве - визуализировать это пространство в трёхмерном виде - выявить области, где вопросы клиентов остаются без релевантного FAQ

3.2 Данные

Возьмем таблицу, в которой уже есть 50 записей FAQ.

Вопрос Ответ
Как записаться на занятие впервые? Напишите мне в Telegram - я предложу свободные слоты.
Сколько длится одно занятие? Обычно урок длится 60 минут, но можно договориться на 90 минут по необходимости.
Сколько стоит одно занятие? Базовая стоимость индивидуального занятия - 1500 ₽ за час.
Можно ли оплатить сразу несколько занятий? Да, вы можете оплатить пакет занятий со скидкой.
Какие способы оплаты доступны? Принимаю оплату по СБП, переводом на карту или через ЮМани.
Есть ли скидки для постоянных учеников? Да, при оплате пакета от 10 занятий предоставляется скидка 10%.
Можно ли считать перенести занятие? Да, если предупредить минимум за 24 часа до урока.
Что делать, если я опоздаю на занятие? Просто предупредите меня - мы попробуем использовать оставшееся время максимально эффективно.
Что делать, если я пропустил занятие? При опоздании без предупреждения занятие считается проведённым.
Как часто проходят занятия? Обычно 2 раза в неделю, но график можно настроить индивидуально.
Можно ли заниматься реже одного раза в неделю? Можно, но для стабильного результата я рекомендую хотя бы 1 раз в неделю.
Вы проводите занятия онлайн или офлайн? В основном онлайн (Zoom/Google meet), офлайн возможен в Москве.
Какая платформа используется для онлайн-уроков? Чаще всего Google встречается, но возможен Zoom по договорённости.
Нужно ли устанавливать Zoom? Да, для удобства лучше заранее установить Zoom на компьютер или телефон.
Можно ли заниматься с телефона? Да, но лучше использовать компьютер или планшет для удобства.
Какие предметы вы преподаёте? Математика, физика, информатика, подготовка к ОГЭ/ЕГЭ/ИБ.
Готовите ли вы к международным экзаменам? Да, готовлю к IB Math AA/AI, SAT, GRE Quantitative.
Можно ли пройти экспресс-подготовку к контрольной? Да, но лучше предупредить хотя бы за несколько дней.
Готовите ли вы к олимпиадам? Да, есть опыт подготовки к школьным и региональным олимпиадам.
Можно ли заниматься в мини-группе? Да, до 3 человек в группе, стоимость делится между участниками.
Как понять мой текущий уровень? На первом занятии мы проводим короткий тест и определяем точки роста.
Как быстро будут заметны результаты? Обычно первые улучшения видны уже через 4-5 занятий.
Вы задаёте домашнее задание? Да, домашка помогает закрепить материал и сократить время объяснения на уроке.
Что делать, если я не успеваю выполнить домашку? Главное - предупредить. Мы тогда скорректируем план занятий.
Можете ли вы проверить моё решение задачи? Да, высылайте в чат - дам обратную связь.
Можно ли задать вопрос между уроками? Да, в разумных пределах можно писать в чат Telegram.
Как записаться на бесплатное пробное занятие? Просто напишите мне - пробное занятие длится 30 минут.
Есть ли ограничение по возрасту учеников? Нет, занимаюсь как со школьниками, так и со взрослыми.
Можно ли отменить оплату? Возврат возможен только при отмене занятий за 48 часов до начала курса.
Какие материалы нужны для занятий? Тетрадь, ручка, калькулятор, а также доступ к интернету.
Помогаете ли вы с проектами или курсовыми? Да, в рамках математики и информатики могу помочь.
Можете ли вы проверить мой реферат? Да, но только по математическим темам.
Помогаете ли вы с подготовкой к собеседованию? Да, по математическим и логическим задачам.
Можно ли заниматься летом? Да, летом занятия проходят по обычному расписанию.
Есть ли занятия в праздники? По договорённости, обычно в новогодние каникулы отдыхаем.
Можно ли брать паузу на месяц? Да, место сохраняется при предварительном уведомлении.
Как мне подготовиться к ЕГЭ по математике? Мы проходим всю программу, решаем реальные варианты и делаем акцент на слабых темах.
Какие результаты показывают ваши ученики? В среднем +25 баллов к стартовому результату по ЕГЭ.
Можно ли начать подготовку к экзамену за 3 месяца? Можно, но придётся заниматься чаще и интенсивнее.
Есть ли у вас отзывы учеников? Да, отзывы доступны на сайте и в моём Telegram-канале.
Можно ли заниматься только по определённой теме? Да, можно взять короткий курс по конкретной теме.
Как происходит обратная связь с родителями? Я могу раз в неделю присылать отчёт о прогрессе ученика.
Можно ли получить сертификат о прохождении курса? Да, по запросу выдам сертификат с темами занятий.
Что делать, если связь во время урока прервалась? Мы переносим оставшееся время на другой день или продолжаем, когда интернет восстановится.
Можно ли использовать планшет для письма на уроках? Да, это даже удобнее для решения математических задач.
Нужен ли графический калькулятор для IB? Да, желательно иметь TI-Nspire или Casio ClassPad.
Сколько времени занимает подготовка к SAT Math? Обычно от 3 до 6 месяцев, в зависимости от уровня.
Вы помогаете с программированием? Да, Python и основы алгоритмов.
Можно ли заниматься в выходные? Да, но места ограничены, лучше бронировать заранее.
Можете ли вы помочь составить учебный план? Да, мы вместе определим цели и сделаем план занятий.

Ивозьмем 20 вопросов клиентов.

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

Сколько длится один урок по математике?
Можно ли перенести занятие, если я заболел?
Какие темы по алгебре вы объясняете?
Помогаете ли вы готовиться к олимпиадам?
Как оплатить занятия - помесячно или за каждое занятие?
Можно ли заниматься в группах или только индивидуально?
Сколько стоит одно занятие по физике?
Подходит ли ваша программа для подготовки к ЕГЭ?
Используете ли вы дополнительные материалы или только учебник?
Можно ли проводить занятия онлайн через Zoom?
Поможете ли вы с домашним заданием из школы?
Сколько нужно уроков, чтобы подтянуть тему тригонометрии?
Можно ли выбрать только один предмет, например, физику?
Есть ли у вас скидки для постоянных учеников?
В какое время обычно проводятся занятия вечером?
Проверяете ли вы прогресс ученика и как часто?
Можно ли получить запись урока после занятий?
Сколько учеников уже прошли у вас подготовку?
Используете ли вы искусственный интеллект в обучении?
Какая погода на улице?

3.3 Векторизация и снижение размерности

После успешной векторизации появляется небольшая проблема. При использовании векторных эмбедингов "OpenAI" или "Mistral" получается n-мерный вектор, который не может быть визуализирован. Выглядит это примерно так:

[-0.027038574,0.047668457,0.07312012,-0.024932861,0.023834229,0.04321289,0.03050232,0.0030403137,0.01222229,-0.01222229,-0.01222229,0.052093506,0.016784668,-0.0027618408,-0.038757324,0.021362305,0.04248047,0.023330688,0.026428223,0.023330688,-0.015434265,-0.025314331,-0.06518555,0.010307312,-0.011299133,-0.00223732,-0.0058021545,-0.0181427,-0.05038452,-0.010559082,0.022964478,-0.022720337,0.004940033,-0.007160187,0.016418457,-0.00605011,-0.03656006,-0.023208618,0.0020065308,0.038757324,-0.044433594,0.0050315857,0.0034103394,0.015060425,0.0069465637,-0.05432129,0.01525116,-0.025802612,-0.004753113,-0.01210022,0.009941101,0.04272461,0.041992188,0.020858765,0.00944519,0.02507019,0.0026245117,-0.0026073456,-0.05532837,0.05432129,-0.020492554,-0.0045051575,0.010124207,0.00084114075,-0.0044441223,-0.0124053955,-0.014015198,-0.028640747,0.010925293,0.02444458,0.04937744,0.0043525696,-0.038269043,0.0030403137,-0.033325195,-0.029754639,0.015930176,0.023452759,0.002161026,0.015060425,-0.03778076,0.016784668,0.05038452,-0.022354126,-0.040008545,0.026794434,0.0033187866,-0.0060195923,-0.01525116,0.023956299,0.026794434,-0.0053100586,0.02357483,0.033569336,0.027908325,0.04937744,-0.021118164,-0.0011110306,0.04272461,0.03753662,0.01777649,-0.01876831,0.053344727,-0.009750366,-0.008270264,-0.029632568,0.0154953,0.013023376,-0.014076233,-0.059509277,-0.08496094,-0.007904053,-0.009140015,-0.08691406,-0.009628296,-0.011978149,0.003627777,0.041992188,-0.051116943,-0.023330688,0.022094727,0.014137268,0.00083351135,0.00088739395,-0.027038574,-0.064697266,0.019012451,0.035308838,-0.011665344,-0.024932861,-0.022216797,-0.0362854,-0.019012451,-0.05432129,-0.015434265,-0.0024547577,0.025314331,-0.014137268,0.029510498,-0.013832092,0.04296875,-0.02407837,-0.038269043,0.009819031,-0.025558472,-0.010803223,-0.019882202,0.020248413,-0.022094727,0.0020065308,0.027786255,0.019012451,0.04864502,0.008460999,0.00047254562,-0.019256592,0.04321289,0.023712158,-0.013832092,-0.010063171,-0.006790161,0.019500732,-0.014633179,0.020736694,-0.03604126,-0.03604126,0.009262085,-0.023330688,-0.05557251,0.053100586,0.045196533,-0.03555298,0.041229248,0.05630493,0.039520264,-0.0058021545,-0.010246277,-0.04147339,0.04840088,0.014259338,0.0066986084,-0.040008545,0.028518677,-0.0013580322,0.00027775764,-0.0024223328,0.0010490417,0.020248413,0.011726379,0.06298828,-0.010429382,-0.026046753,-0.01802063,-0.00082206726,-0.019256592,0.040496826,0.0042304993,-0.04815674,0.03604126,-0.033569336,-0.002407074,0.0030097961,-0.02999878,0.0637207,-0.0054016113,-0.011978149,-0.032104492,0.038757324,0.01864624,-0.021362305,0.00088739395,-0.026168823,-0.017654419,-0.03186035,-0.013397217,-0.038513184,0.0016899109,-0.0040130615,-0.019638062,-0.008766174,0.027404785,-0.052093506,0.019638062,-0.0005207062,0.009689331,0.05505371,0.011787415,0.023086548,0.008026123,0.0034732819,-0.023834229,0.006881714,-0.014442444,0.003982544,0.017654419,0.00086021423,0.035308838,0.034088135,0.01234436,-0.019882202,-0.017410278,-0.002500534,-0.02999878,0.026550293,0.0012187958,-0.026428223,0.040008545,-0.0029010773,-0.0073776245,0.024810791,-0.032836914,-0.010002136,-0.052856445,-0.0034732819,-0.03656006,-0.06567383,-0.00920105,-0.011047363,0.040740967,-0.05038452,-0.01802063,-0.044433594,0.03729248,-0.019134521,0.009506226,0.041229248,-0.03704834,0.0018291473,0.026046753,0.008522034,-0.0054016113,-0.039276123,0.04171753,0.03555298,-0.0027313232,-0.03778076,-0.00932312,-0.00045347214,0.0017204285,0.0058021545,-0.047668457,0.00466156,-0.01777649,0.015617371,-0.015434265,0.0082092285,-0.013214111,-0.0051841736,-0.014320374,-0.05606079,0.032836914,0.035308838,-0.011978149,-0.00920105,-0.006298065,-0.0602417,0.003982544,0.057281494,-0.0066375732,0.031234741,0.046661377,-0.0126571655,0.0002682209,0.0054626465,0.051605225,-0.053833008,-0.006111145,-0.047668457,0.027160645,-0.045196533,-0.043701172,-0.017044067,-0.028152466,0.023834229,0.014137268,0.0058021545,-0.06713867,-0.05014038,-0.030380249,-0.03050232,-0.025802612,-0.03704834,0.023330688,-0.029510498,-0.029266357,-0.0062332153,-0.02456665,-0.0049095154,0.013084412,-0.033325195,0.0005054474,0.00756073,0.03704834,0.0009803772,0.041229248,0.016052246,0.04937744,0.05606079,-0.029510498,0.027038574,0.0034255981,0.01864624,0.008644104,-0.0024547577,-0.015312195,-0.028762817,-0.04937744,-0.029632568,0.009689331,0.026046753,0.026794434,-0.0027942657,0.0033798218,0.041992188,-0.0362854,0.09039307,-0.044708252,-0.03149414,-0.0003376007,0.045440674,-0.010559082,0.011108398,0.0013036728,0.03186035,-0.033569336,0.003643036,-0.029022217,0.035308838,-0.0016746521,0.0017366409,-0.011665344,0.0010108948,0.0031166077,0.03729248,0.014259338,0.028152466,0.005001068,0.015930176,0.005279541,0.016662598,-0.040985107,-0.00046110153,0.034332275,0.028884888,-0.028030396,-0.00092601776,-0.05456543,0.022842407,-0.027908325,0.07409668,0.03656006,0.022720337,-0.0030097961,-0.005680084,-0.00042629242,-0.0051841736,-0.02432251,0.032592773,0.03062439,0.030380249,0.015930176,-0.021118164,0.051605225,0.039764404,0.009880066,0.0075302124,0.044189453,0.02444458,-0.013023376,-0.05407715,-0.01512146,0.035064697,-0.059265137,-0.0368042,-0.05038452,-0.013763428,-0.050872803,0.0107421875,0.001250267,-0.033569336,-0.020126343,-0.024932861,-0.053100586,0.00012922287,0.05432129,-0.05432129,-0.019882202,0.026428223,-0.004722595,0.049865723,-0.0149383545,-0.076049805,0.038513184,-0.012283325,0.00032019615,0.022964478,0.03024292,-0.070617676,-0.033325195,-0.022476196,-0.003982544,0.0014276505,-0.0051841736,0.04815674,0.0022850037,0.022354126,0.0602417,0.045684814,-0.004814148,-0.03086853,0.043945312,-0.03186035,-0.02357483,-0.0064201355,0.0022068024,-0.0013275146,-0.014381409,-0.0006289482,0.006450653,-0.010986328,0.007255554,-0.00044178963,-0.07952881,-0.032104492,-0.04248047,0.06665039,-0.003780365,0.038757324,-0.006729126,-0.024932861,0.011917114,-0.04815674,-0.009941101,-0.0061416626,0.011856079,-0.057281494,0.07159424,-0.0082092285,-0.013702393,-0.043945312,0.045440674,-0.044708252,-0.0003027916,-0.029632568,0.038513184,0.010124207,-0.06665039,-0.033325195,-0.007255554,0.0012807846,0.014511108,-0.014877319,-0.0015897751,-0.015617371,-0.03579712,-0.04815674,-0.013641357,-0.010002136,-0.01777649,0.015556335,-0.004940033,0.0051231384,0.039001465,-0.011482239,0.032348633,0.0014657974,-0.013832092,-0.046417236,0.019012451,-0.04345703,-0.0602417,-0.020126343,0.017288208,-0.0022068024,0.01777649,-0.034820557,-0.043945312,0.019256592,-0.038024902,-0.03753662,0.002407074,0.023956299,-0.016921997,-0.029510498,-0.0046920776,-0.013397217,0.017532349,0.02456665,0.03149414,0.00223732,0.00010418892,0.040252686,0.00223732,-0.00907135,0.026168823,-0.008766174,0.035308838,0.041229248,-0.008392334,0.0006637573,-0.032592773,-0.03161621,-0.004261017,-0.026794434,-0.029266357,0.010185242,-0.081970215,0.0063591003,-0.010307312,-0.012466431,0.021484375,-0.02468872,-0.02432251,-0.019256592,-0.03186035,0.021972656,-0.011917114,-0.002670288,-0.0055885315,-0.020126343,0.022216797,-0.013641357,0.04171753,0.03729248,0.027404785,-0.022964478,-0.00082588196,-0.014511108,-0.053100586,-0.010429382,-0.026672363,0.034820557,-0.00086450577,-0.04272461,0.019012451,-0.005214691,0.019500732,0.0006251335,0.021606445,0.010925293,-0.029266357,0.040252686,-0.046417236,-0.02519226,-0.00762558,-0.0006289482,0.05407715,0.0014123917,0.015312195,-0.029266357,0.008331299,0.03111267,-0.012840271,-0.044952393,-0.01864624,0.039520264,-0.00907135,0.011917114,0.039001465,0.050872803,0.026916504,-0.0014896393,0.028762817,0.087402344,0.06817627,-0.01789856,-0.050872803,0.0017900467,0.0017051697,-0.05606079,-0.017410278,0.043701172,0.03186035,-0.11657715,0.002855301,-0.02407837,0.01234436,-0.013763428,0.03579712,-0.023330688,0.06048584,0.0050315857,-0.056793213,-0.0069770813,-0.004135132,0.03778076,0.041229248,-0.012039185,-0.0126571655,0.003271103,0.020004272,0.03604126,0.002746582,0.023956299,0.011482239,-0.019378662,0.011299133,-0.010681152,-0.008766174,0.0050621033,0.0069465637,0.05505371,0.0076560974,0.04171753,-0.0052490234,-0.019760132,0.00090265274,-0.005214691,-0.02456665,0.020614624,0.0073165894,-0.05456543,-0.08148193,0.028762817,-0.029144287,0.024932861,0.013893127,-0.038757324,-0.0020523071,0.027160645,-0.020996094,-0.027908325,0.0017051697,-0.01777649,0.0023002625,-0.06817627,-0.027664185,0.015312195,-0.016540527,-0.0061416626,-0.026550293,0.051605225,-0.014694214,-0.03704834,0.013397217,-0.0045700073,-0.02420044,-0.059753418,-0.046173096,-0.041229248,-0.01259613,-0.014633179,0.03086853,0.0046310425,-0.0006790161,-0.01876831,0.0012655258,-0.051849365,0.049865723,-0.043701172,-0.005554199,-0.026550293,-0.0023765564,0.039001465,0.033081055,-0.04272461,0.004196167,-0.0068206787,-0.035064697,-0.01512146,-0.0053710938,0.014320374,-0.05581665,-0.033843994,0.044433594,-0.029876709,0.029876709,0.014076233,-0.04864502,0.008026123,0.03086853,0.014381409,0.016052246,0.06964111,0.027404785,-0.056793213,-0.0063285828,-0.06915283,0.052856445,0.02357483,0.021606445,-0.06124878,0.0025615692,0.014511108,-0.03012085,-0.021484375,-0.0859375,0.0075950623,0.0042915344,0.05999756,0.0107421875,0.0035037994,-0.0049095154,-0.0022850037,-0.0154953,0.011421204,0.020492554,0.026046753,-0.028152466,-0.03579712,-0.021850586,-0.053588867,-0.0050621033,0.010124207,0.041229248,0.040496826,-0.045440674,0.029754639,-0.012718201,-0.016174316,0.017288208,-0.0013427734,-0.025314331,-0.005432129,0.010620117,0.021606445,0.038269043,0.03062439,0.003534317,0.020858765,0.008705139,-0.03111267,0.04248047,0.0619812,0.017166138,-0.039276123,-0.043701172,-0.053833008,0.0149383545,-0.009010315,0.009689331,-0.008644104,-0.026550293,-0.059265137,0.064697266,-0.023330688,0.014511108,-0.009506226,-0.0149383545,-0.0619812,-0.07952881,-0.028518677,0.014755249,0.007255554,0.06518555,0.004753113,0.001991272,0.010498047,0.022476196,0.032592773,0.03604126,0.039764404,-0.022842407,0.04248047,-0.017654419,-0.03656006,-0.03086853,0.022964478,0.006576538,-0.007965088,-0.0001784563,-0.011238098,0.0060806274,0.02420044,-0.051361084,0.044952393,0.008522034,0.021240234,0.002916336,0.023834229,0.013954163,0.012039185,-0.03729248,0.006511688,-0.051361084,-0.07159424,-0.0046920776,-0.046905518,-0.04296875,-0.030380249,-0.019882202,0.03579712,-0.03074646,0.01537323,-0.0029468536,0.013458252,0.005092621,0.009262085,-0.024810791,0.05014038,0.008026123,-0.023956299,0.032836914,-0.01802063,-0.040985107,0.0309906,0.03186035,-0.008026123,-0.0047836304,-0.01234436,0.0014352798,0.040252686,0.028396606,-0.01777649,0.017288208,0.0736084,-0.03137207,-0.053100586,-0.00907135,-0.020004272,-0.060760498,-0.021362305,-0.008705139,-0.020126343,0.0073776245,0.019134521,0.00907135,0.011856079,0.033843994,0.007499695,0.04147339,0.04714966,0.0082092285,0.013832092,0.06616211,0.027786255,-0.0035648346,0.010246277,-0.0619812,0.032348633,-0.025924683,-0.06222534,-0.029144287,0.07952881,0.044952393,0.0045700073,0.035308838,0.019638062,0.028030396,0.00028944016,-0.00082588196,-0.039001465,-0.007255554,0.06665039,0.0184021,-0.032592773,0.051605225,0.064208984,-0.03186035,-0.045196533,-0.0015354156,0.020858765,0.04815674,0.011299133,0.013458252,-0.03012085,-0.01222229,0.025802612,0.0047836304,-0.0184021,-0.011726379,-0.010002136,0.0602417,-0.015930176,-0.003288269,-0.00944519,0.09680176,0.019256592,-0.08001709,-0.03778076,0.027160645,0.042236328,-0.047912598,0.026168823,0.043945312,-0.023834229,-0.012901306,0.046905518,-0.01222229,0.051605225,0.024810791,-0.02432251,0.017532349,0.022354126,-0.009628296,-0.019378662,-0.06665039,-0.010925293,-0.053833008,0.032348633,-0.011787415,0.02432251,0.0132751465,-0.002746582,-0.0022678375,0.0059280396,0.033569336,0.027526855,0.007221222,0.0018749237,-0.016296387,0.034820557,0.06915283,-0.0051231384,0.014198303,-0.044433594,0.025314331,-0.06323242,0.013458252,-0.01259613,-0.0011110306,0.019378662,-0.020370483,0.020614624,-0.0056495667,-0.01777649,-0.044708252,0.020996094,-0.030380249,-0.0027008057,0.016296387,-0.029754639,0.033325195,-0.06100464,0.056549072,-0.0019216537,-0.00932312,0.013893127,0.0053710938,0.014633179,0.0017976761,-0.02444458,0.025314331,0.014381409,0.046905518,0.032104492,-0.0049095154,-0.02519226,0.029754639,-0.05432129,-0.057281494,0.027282715,0.026916504,0.017410278,0.013336182,0.044708252,-0.032348633,-0.04937744,0.06964111,0.012718201,-0.016540527,-0.0021762848,-0.053833008,0.014137268,-0.012901306,0.049621582,-0.040740967,0.0619812,0.0063285828,-0.01852417,-0.021118164,-0.02468872,-0.017288208,0.029510498,0.015060425,0.03579712,0.0004553795,-0.011978149,-0.10571289,0.0014657974,0.021484375,-0.011360168,-0.007499695,-0.06173706,0.0027618408,0.021484375,-0.012901306,0.0181427,0.03656006,0.076538086,-0.027908325,0.0007100105,-0.0026073456,-0.015060425]

Поэтому перед работы с таким вектором необходимо свести его к 3-мерному (x, y, z). Для этого мы можем использовать PCA (Principal Component Analysis) (Principal Component Analysis - Wikipedia, n.d.), UMAP (Uniform Manifold Approximation and Projection) (UMAP, n.d.) или t-SNE (t-distributed Stochastic Neighbor Embedding) (T-Distributed Stochastic Neighbor Embedding - Wikipedia, n.d.).

В данном исследовании мы будем работать с UMAP (Uniform Manifold Approximation and Projection), так как наш набор данных считается небольшим, что означает, что нам подойдет любой из них.

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

v1 = [-0.027038574,0.047668457,0.07312012,-0.024932861,0.023834229,0.04321289,0.03050232 ...]
v2 = [-0.057922363,0.023910522,0.04559326,-0.035003662,0.017868042,0.054473877,0.0406799 ...]
v3 = [-0.04373169,0.016983032,0.06323242,-0.018157959,0.009231567,0.043304443,0.03778076 ...]
v4 = [-0.03012085,0.040527344,0.08898926,-0.0385437,-0.000934124,0.048950195,0.026123047 ...]
...
v50 = [-0.028686523,0.047912598,0.06738281,-0.0093307495,0.017196655,0.05291748,0.049560 ...]

# v (от англ. vector).
# У нас 50 строк -> v50 последнее значение.

Затем мы должны составить простой список, назовем его “векторы”:

vectors = [
    v1,
    v2,
    v3,
    ...
    v50
]

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

Показать код
from pathlib import Path
import sys

HERE = Path.cwd()
SRC = HERE if (HERE / "FAQ_sheet_vectors.py").exists() else HERE / "src"
sys.path.insert(0, str(SRC.resolve()))

import numpy as np
import umap
from FAQ_sheet_vectors import vectors

n_components = 3

vectors = np.array(vectors)
reducer = umap.UMAP(n_components = 3, random_state = 42)
vectors_3d = reducer.fit_transform(vectors)

print("До:")
print(vectors.shape)
print(vectors[:1])

print("------------------------------------------------------------------------------------")

print("После:")
print(vectors_3d.shape)
print(vectors_3d[:1])
C:\Users\svinj\new-file-system\ai-research\venv\Lib\site-packages\umap\umap_.py:1952: UserWarning: n_jobs value 1 overridden to 1 by setting random_state. Use no seed for parallelism.
  warn(
До:
(50, 1024)
[[-0.02703857  0.04766846  0.07312012 ...  0.00071001 -0.00260735
  -0.01506043]]
------------------------------------------------------------------------------------
После:
(50, 3)
[[ 9.848729    5.0139856  -0.93988866]]

Мы видим, что сначала у нас было 1024 измерения в каждом из наших векторов, а после уменьшения мы получили только 3 измерения (3D), как мы и хотели.

Теперь мы можем построить график этих координат в 3D-пространстве с помощью следующего кода визуализации:

Показать код
import plotly.graph_objs as go 
from UMAP import vectors_3d
from FAQ_sheet_vectors import faq_text

fig = go.Figure(data = [go.Scatter3d(
    x = vectors_3d[:,0],
    y = vectors_3d[:,1],
    z = vectors_3d[:,2],
    mode = 'markers',
    marker = dict(size = 5, color = 'blue'),
    hovertext = faq_text,
    hovertemplate = (
            "<b>Текст:</b> %{hovertext}<br>" +
            "<b>Координаты:</b> (%{x:.2f}, %{y:.2f}, %{z:.2f})" +
            "<extra></extra>")
)])

fig.update_layout(
    scene = dict(
        xaxis_title = 'X',
        yaxis_title = 'Y',
        zaxis_title = 'Z'
    ),
    title = '3D Visualization of FAQ embeddings'
)

fig.show()
C:\Users\svinj\new-file-system\ai-research\venv\Lib\site-packages\umap\umap_.py:1952: UserWarning: n_jobs value 1 overridden to 1 by setting random_state. Use no seed for parallelism.
  warn(

Сделаем тоже самое для вопросов:

Показать код
import plotly.graph_objs as go 
from UMAP import questions_3d
from questions import questions_text

fig = go.Figure(data = [go.Scatter3d(
    x = questions_3d[:,0],
    y = questions_3d[:,1],
    z = questions_3d[:,2],
    mode = 'markers',
    marker = dict(size = 5, color = 'red'),
    hovertext = questions_text,
    hovertemplate = (
            "<b>Текст:</b> %{hovertext}<br>" +
            "<b>Координаты:</b> (%{x:.2f}, %{y:.2f}, %{z:.2f})" +
            "<extra></extra>")
)])

fig.update_layout(
    scene = dict(
        xaxis_title = 'X',
        yaxis_title = 'Y',
        zaxis_title = 'Z'
    ),
    title = '3D Visualization of questions embeddings'
)

fig.show()

3.4 Метрика близости: почему косинусное сходство

В изначальном подходе для определения ближайшего FAQ к каждому вопросу было использовано евклидово расстояние (Euclidean Distance - Wikipedia, n.d.) в трёхмерном пространстве:

\[ d(p, q) = \sqrt{(p_x - q_x)^2 + (p_y - q_y)^2 + (p_z - q_z)^2} \]

Область Вороного для точки \(s_i\) определяется как:

\[ d(q, s_i) \leq d(q, s_j) \quad \forall j \neq i \]

Однако при проверке результатов обнаружилась проблема: вопрос “Сколько длится один урок по математике?” был сопоставлен с FAQ “Как понять мой текущий уровень?” — семантически нерелевантным ответом. Причина в том, что снижение размерности с 1024 до 3 (~в 341 раз) неизбежно деформирует семантическое пространство, и абсолютные координаты перестают точно отражать смысловую близость.

Поэтому для финального сопоставления вопросов с FAQ используется косинусное сходство в исходном 1024-мерном пространстве (Cosine Similarity - Wikipedia, n.d.):

\[ \text{sim}(A,B) = \cos(\theta) = \frac{\mathbf{A} \cdot \mathbf{B}}{\|\mathbf{A}\|\|\mathbf{B}\|} = \frac{\sum_{i=1}^{n} A_i B_i}{\sqrt{\sum_{i=1}^{n} A_i^2} \cdot \sqrt{\sum_{i=1}^{n} B_i^2}} \]

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

Правило присваивания: вопрос \(a\) относится к \(s_i\), если:

\[ \text{sim}(a, s_i) = \max_j \; \text{sim}(a, s_j) \]

Правило непокрытия: если максимальное сходство ниже порога \(\tau\):

\[ \max_j \, \text{sim}(a, s_j) < \tau \quad \Rightarrow \quad \text{вопрос не покрыт} \]

Показать код
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
from FAQ_sheet_vectors import vectors
from questions import questions

threshold = 0.845

assignments = []
similarities = []

for q in questions:
    sims = cosine_similarity([q], vectors)[0]
    nearest_idx = np.argmax(sims)
    best_sim = sims[nearest_idx]
    
    if best_sim < threshold:
        assignments.append(None)
    else:
        assignments.append(nearest_idx)
    
    similarities.append(best_sim)

for i, (a, s) in enumerate(zip(assignments, similarities)):
    if a is None:
        print(f"Question {i} -> ❌ Не покрыт (сходство={s:.3f})")
    else:
        print(f"Question {i} -> ✅ FAQ {a} (сходство={s:.3f})")
Question 0 -> ✅ FAQ 0 (сходство=0.898)
Question 1 -> ✅ FAQ 1 (сходство=0.900)
Question 2 -> ✅ FAQ 6 (сходство=0.871)
Question 3 -> ❌ Не покрыт (сходство=0.841)
Question 4 -> ✅ FAQ 18 (сходство=0.925)
Question 5 -> ✅ FAQ 3 (сходство=0.868)
Question 6 -> ✅ FAQ 19 (сходство=0.897)
Question 7 -> ✅ FAQ 2 (сходство=0.893)
Question 8 -> ✅ FAQ 37 (сходство=0.857)
Question 9 -> ✅ FAQ 30 (сходство=0.854)
Question 10 -> ✅ FAQ 11 (сходство=0.876)
Question 11 -> ✅ FAQ 22 (сходство=0.859)
Question 12 -> ❌ Не покрыт (сходство=0.796)
Question 13 -> ❌ Не покрыт (сходство=0.844)
Question 14 -> ✅ FAQ 5 (сходство=0.918)
Question 15 -> ✅ FAQ 9 (сходство=0.859)
Question 16 -> ✅ FAQ 42 (сходство=0.875)
Question 17 -> ❌ Не покрыт (сходство=0.823)
Question 18 -> ✅ FAQ 38 (сходство=0.850)
Question 19 -> ❌ Не покрыт (сходство=0.812)
Question 20 -> ❌ Не покрыт (сходство=0.741)

После применения косинусного сходства вопрос “Сколько длится один урок по математике?” корректно сопоставляется с FAQ “Сколько длится одно занятие? Обычно урок длится 60 минут…”.

Диаграмма Вороного строится в 3D-пространстве UMAP с metric="cosine", однако финальное сопоставление вопрос <-> FAQ выполняется в исходном пространстве.

4. Реализация диаграммы Вороного

4.1 Подход и ограничения первой версии

4.1 Первоначальная реализация и выявленное ограничение

В первой версии диаграмма Вороного строилась по объединённому облаку точек — FAQ и вопросы вместе передавались в Voronoi(). Это приводило к тому, что вопросы влияли на форму регионов, деформируя их.

Показать код
import numpy as np
from scipy.spatial import Voronoi, ConvexHull
import plotly.graph_objs as go
from plotly.colors import sample_colorscale
from UMAP import vectors_3d, questions_3d
from FAQ_sheet_vectors import vectors, faq_text
from questions import questions, questions_text
from sklearn.metrics.pairwise import cosine_similarity

threshold = 0.845
N = len(vectors_3d)

points = np.vstack([vectors_3d, questions_3d])
vor = Voronoi(points)  # <- проблема: вопросы участвуют в построении регионов

min_bounds = points.min(axis=0) - 0.5
max_bounds = points.max(axis=0) + 0.5
colors = sample_colorscale("Viridis", [i/(N-1) for i in range(N)])

faq_assignments = []
sims = cosine_similarity(questions, vectors)
for row in sims:
    nearest_idx = np.argmax(row)
    best_sim = row[nearest_idx]
    faq_assignments.append(nearest_idx if best_sim >= threshold else None)

fig = go.Figure()

for faq_idx in range(N):
    region_index = vor.point_region[faq_idx]
    region = vor.regions[region_index]

    if not region or -1 in region:
        continue

    vertices = vor.vertices[region]
    if np.any(vertices < min_bounds) or np.any(vertices > max_bounds):
        continue

    try:
        hull = ConvexHull(vertices)
        c = colors[faq_idx % len(colors)]

        fig.add_trace(go.Mesh3d(
            x=vertices[:,0], y=vertices[:,1], z=vertices[:,2],
            i=hull.simplices[:,0], j=hull.simplices[:,1], k=hull.simplices[:,2],
            color=c, opacity=0.25,
            name=f'FAQ {faq_idx} Region', legendgroup=f"FAQ{faq_idx}",
            text=[faq_text[faq_idx]] * len(vertices),
            hoverinfo="text+name", showlegend=True
        ))

        fig.add_trace(go.Scatter3d(
            x=[vectors_3d[faq_idx,0]], y=[vectors_3d[faq_idx,1]], z=[vectors_3d[faq_idx,2]],
            mode='markers', marker=dict(size=4, color="black"),
            name=f'FAQ {faq_idx}', legendgroup=f"FAQ{faq_idx}",
            text=[faq_text[faq_idx]], hoverinfo='text+name', showlegend=True
        ))

        q_idx = [j for j, a in enumerate(faq_assignments) if a == faq_idx]
        if q_idx:
            q_points = questions_3d[q_idx]
            fig.add_trace(go.Scatter3d(
                x=q_points[:,0], y=q_points[:,1], z=q_points[:,2],
                mode='markers', marker=dict(size=4, color=c, symbol='diamond'),
                name=f'Questions -> FAQ {faq_idx}', legendgroup=f"FAQ{faq_idx}",
                text=[questions_text[j] for j in q_idx],
                hoverinfo='text+name', showlegend=True
            ))
            for j, qp in zip(q_idx, q_points):
                fig.add_trace(go.Scatter3d(
                    x=[vectors_3d[faq_idx,0], qp[0]],
                    y=[vectors_3d[faq_idx,1], qp[1]],
                    z=[vectors_3d[faq_idx,2], qp[2]],
                    mode="lines", line=dict(color=c, width=2),
                    name=f"Link FAQ {faq_idx}", legendgroup=f"FAQ{faq_idx}",
                    showlegend=False
                ))
    except Exception:
        pass

uncovered_idx = [j for j, a in enumerate(faq_assignments) if a is None]
if uncovered_idx:
    q_points = questions_3d[uncovered_idx]
    fig.add_trace(go.Scatter3d(
        x=q_points[:,0], y=q_points[:,1], z=q_points[:,2],
        mode='markers', marker=dict(size=4, color="red", symbol="diamond"),
        name="Непокрытые вопросы", legendgroup="Uncovered",
        text=[questions_text[j] for j in uncovered_idx],
        hoverinfo="text+name", showlegend=True
    ))

fig.update_layout(
    scene=dict(
        xaxis=dict(title='X', range=[min_bounds[0], max_bounds[0]]),
        yaxis=dict(title='Y', range=[min_bounds[1], max_bounds[1]]),
        zaxis=dict(title='Z', range=[min_bounds[2], max_bounds[2]])
    ),
    title="3D диаграмма Вороного — первая версия",
    legend=dict(itemsizing='constant')
)
fig.show()

Ограничение этой версии: Voronoi(points) строит регионы вокруг всех точек, включая вопросы. В результате регионы FAQ искажаются — они фактически делят пространство с вопросами, а не только друг с другом.

Ниже показан результат после фильтрации неиспользуемых FAQ и непокрытых вопросов для наглядности:

image.png

image.png

image.png

Frame 86.png

Видно, что ~50% непокрытых вопросов сосредоточены в зоне между двумя кластерами — там, где ни один FAQ не доминирует достаточно уверенно.

4.2 Финальная реализация

4.2 Финальная реализация

Ключевое исправление: Voronoi() строится только по точкам FAQ. Вопросы отображаются поверх уже готовых регионов как независимые точки, не влияя на их форму. Для каждого региона с бесконечными вершинами добавляется сама точка FAQ, что позволяет строить выпуклую оболочку даже для краевых регионов.

Показать код
import numpy as np
from scipy.spatial import Voronoi, ConvexHull
import plotly.graph_objs as go
from plotly.colors import sample_colorscale
from UMAP import vectors_3d, questions_3d
from FAQ_sheet_vectors import vectors, faq_text
from questions import questions, questions_text
from sklearn.metrics.pairwise import cosine_similarity

threshold = 0.845
N = len(vectors_3d)

points = np.vstack([vectors_3d, questions_3d])
vor = Voronoi(vectors_3d)  # <-  исправление: только FAQ-точки

min_bounds = points.min(axis=0) - 0.5
max_bounds = points.max(axis=0) + 0.5
colors = sample_colorscale("Viridis", [i/(N-1) for i in range(N)])

faq_assignments = []
sims = cosine_similarity(questions, vectors)
for row in sims:
    nearest_idx = np.argmax(row)
    best_sim = row[nearest_idx]
    faq_assignments.append(nearest_idx if best_sim >= threshold else None)

fig = go.Figure()

for faq_idx in range(N):
    region_index = vor.point_region[faq_idx]
    region = vor.regions[region_index]

    vertices = vor.vertices[region]
    vertices = np.vstack([vertices, vectors_3d[faq_idx]])  # <-  добавляем seed-точку

    try:
        hull = ConvexHull(vertices)
        c = colors[faq_idx % len(colors)]

        fig.add_trace(go.Mesh3d(
            x=vertices[:,0], y=vertices[:,1], z=vertices[:,2],
            i=hull.simplices[:,0], j=hull.simplices[:,1], k=hull.simplices[:,2],
            color=c, opacity=0.25,
            name=f'FAQ {faq_idx} Region', legendgroup=f"FAQ{faq_idx}",
            text=[faq_text[faq_idx]] * len(vertices),
            hoverinfo="text+name", showlegend=True
        ))

        fig.add_trace(go.Scatter3d(
            x=[vectors_3d[faq_idx,0]], y=[vectors_3d[faq_idx,1]], z=[vectors_3d[faq_idx,2]],
            mode='markers', marker=dict(size=4, color="black"),
            name=f'FAQ {faq_idx}', legendgroup=f"FAQ{faq_idx}",
            text=[faq_text[faq_idx]], hoverinfo='text+name', showlegend=True
        ))

        q_idx = [j for j, a in enumerate(faq_assignments) if a == faq_idx]
        if q_idx:
            q_points = questions_3d[q_idx]
            fig.add_trace(go.Scatter3d(
                x=q_points[:,0], y=q_points[:,1], z=q_points[:,2],
                mode='markers', marker=dict(size=4, color=c, symbol='diamond'),
                name=f'Questions → FAQ {faq_idx}', legendgroup=f"FAQ{faq_idx}",
                text=[questions_text[j] for j in q_idx],
                hoverinfo='text+name', showlegend=True
            ))
            for j, qp in zip(q_idx, q_points):
                fig.add_trace(go.Scatter3d(
                    x=[vectors_3d[faq_idx,0], qp[0]],
                    y=[vectors_3d[faq_idx,1], qp[1]],
                    z=[vectors_3d[faq_idx,2], qp[2]],
                    mode="lines", line=dict(color=c, width=2),
                    name=f"Link FAQ {faq_idx}", legendgroup=f"FAQ{faq_idx}",
                    showlegend=False
                ))
    except Exception:
        pass

uncovered_idx = [j for j, a in enumerate(faq_assignments) if a is None]
if uncovered_idx:
    q_points = questions_3d[uncovered_idx]
    fig.add_trace(go.Scatter3d(
        x=q_points[:,0], y=q_points[:,1], z=q_points[:,2],
        mode='markers', marker=dict(size=4, color="red", symbol="diamond"),
        name="Непокрытые вопросы", legendgroup="Uncovered",
        text=[questions_text[j] for j in uncovered_idx],
        hoverinfo="text+name", showlegend=True
    ))

fig.update_layout(
    scene=dict(
        xaxis=dict(title='X', range=[min_bounds[0], max_bounds[0]]),
        yaxis=dict(title='Y', range=[min_bounds[1], max_bounds[1]]),
        zaxis=dict(title='Z', range=[min_bounds[2], max_bounds[2]])
    ),
    title="3D диаграмма Вороного — FAQ-регионы, вопросы и порог косинусного сходства",
    legend=dict(itemsizing='constant')
)
fig.update_scenes(aspectmode="cube")
fig.show()

После фильтрации неиспользуемых FAQ:

image.png

5. Результаты

5.1 Общая картина покрытия

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

5.2 Выявленные пробелы

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

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

5.3 Ограничения метода

Поскольку конструкция Вороного применяется в трёх измерениях (через UMAP), она является лишь приближением к истинному высокоразмерному пространству эмбедингов. В результате некоторые регионы могут казаться больше или меньше, чем они есть в действительности, что частично искажает интерпретацию “покрытия”. Выбор порогового значения косинусного сходства (\(\tau = 0.845\)) также субъективен — его изменение напрямую влияет на количество непокрытых вопросов.

6. Рекомендации и выводы

6.1 Направления дальнейшего развития

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

1. Инструмент для репетитора
На данный момент визуализация доступна только исследователю. Следующим шагом было бы создание простого интерфейса, в котором репетитор мог бы самостоятельно загрузить свой FAQ и получить наглядную карту покрытия с конкретными рекомендациями — какие темы стоит добавить.

2. Автоматическое определение порога
Пороговое значение косинусного сходства (\(\tau = 0.845\)) выбрано эмпирически. Более точный подход — адаптивный порог, калиброванный индивидуально для каждого FAQ на основе распределения сходств в наборе.

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

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


6.2 Выводы

В данной работе предложен и реализован метод оценки качества FAQ-базы для систем автоматических ответов. Метод основан на векторном представлении текста, снижении размерности через UMAP и построении диаграммы Вороного в трёхмерном семантическом пространстве.

Ключевые результаты:

  • Большинство тестовых вопросов успешно покрываются существующим FAQ, что подтверждает адекватность базовой структуры
  • Выявлены семантические пробелы — зоны пространства, где вопросы клиентов не находят релевантного ответа
  • Показано, что евклидово расстояние в сниженном пространстве уступает косинусному сходству в исходном — важный методологический вывод для подобных систем

Метод универсален: он применим к любой FAQ-системе на основе семантического поиска и не зависит от предметной области. Диаграмма Вороного в данном контексте выступает не просто визуализацией, а измеримым критерием качества контента — инструментом, позволяющим перейти от интуитивного наполнения FAQ к структурированному и обоснованному.

References

Cosine Similarity - Wikipedia. n.d. Https://en.wikipedia.org/wiki/Cosine_similarity.
Euclidean Distance - Wikipedia. n.d. Https://en.wikipedia.org/wiki/Euclidean_distance.
Principal Component Analysis - Wikipedia. n.d. Https://en.wikipedia.org/wiki/Principal_component_analysis.
T-Distributed Stochastic Neighbor Embedding - Wikipedia. n.d. Https://en.wikipedia.org/wiki/T-distributed_stochastic_neighbor_embedding.
UMAP: Uniform Manifold Approximation and Projection for Dimension Reduction — Umap 0.5.8 Documentation. n.d. Https://umap-learn.readthedocs.io/en/latest/.
What Is Embedding? IBM. n.d. Https://www.ibm.com/think/topics/embedding.
What Is JSON. n.d. Https://www.w3schools.com/whatis/whatis_json.asp.