Найти в Дзене
Властелин машин

BM25Retriever под капотом

В современных rag системах центральным инструментом являются ретриверы - объекты, которые отвечают за поиск близкой к запросу информации (контекста). Одним из них является BM25Retriever, основанный на частоте встречаемости. В отличие от аналогов, использующих векторные представления, он полагается на точные совпадение единиц, на которые разбит текст (токенов). Для демонстрационных целей возмем набор текстов с описанием компьютерных угроз с сайта MITRE ATT&CK (тут и тут): Для корректной работы BM25Retriever важен способ разбиения текста на единицы, для этого используется параметр preprocess_func. Зададим функцию, осуществляющую деление по словам и их стемминг: Верхнеуровнево для работы надо уметь создать BM25Retriever, используя, например, метод from_texts и найти ближайшие тексты с invoke: from_texts получает аргументы: Следует отметить, что k можно поменять (например, когда вы загружаете настроенный дамп retriever-а) так: bm_retriever.k = 1. from_texts выполняет следующий код: В invok
Оглавление

В современных rag системах центральным инструментом являются ретриверы - объекты, которые отвечают за поиск близкой к запросу информации (контекста). Одним из них является BM25Retriever, основанный на частоте встречаемости. В отличие от аналогов, использующих векторные представления, он полагается на точные совпадение единиц, на которые разбит текст (токенов).

Для демонстрационных целей возмем набор текстов с описанием компьютерных угроз с сайта MITRE ATT&CK (тут и тут):

BM25Retriever

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

-2

Верхнеуровнево для работы надо уметь создать BM25Retriever, используя, например, метод from_texts и найти ближайшие тексты с invoke:

-3

from_texts

from_texts получает аргументы:

  • texts - список текстов, формирующих базу для поиска;
  • k - количество ближайших текстов в ответ на запрос поиска;
  • preprocess_func - функция для разбиения текстов на токены;
  • metadatas - список словарей с метаданными для каждого текста в texts;
  • ids - список идентификаторов для каждого текста в texts.

Следует отметить, что k можно поменять (например, когда вы загружаете настроенный дамп retriever-а) так: bm_retriever.k = 1.

from_texts выполняет следующий код:

  • texts_processed = [preprocess_func(t) for t in texts]
  • vectorizer = BM25Okapi(texts_processed...)

invoke

В invoke выполняются:

_get_relevant_documents:

  • processed_query = self.preprocess_func(query)
  • self.vectorizer.get_top_n(processed_query, self.docs, n=self.k)

Подытоживая, from_texts инициирует препроцессинг текстов и создает класс BM25Okapi, а invoke - препроцессинг запроса и вызывает метод get_top_n объекта класса BM25Okapi. Теперь разберемся с BM25Okapi и его особенностями.

okapi

в конструкторе подсчитываются:

в get_top_n:

  • каждый документ (объект с текстом и метаданными) получает скор близости к запросу get scores in BM25Okapi
  • документы сортируются по убыванию скора и выбирается заданное число

код в get_top_n:

  • scores = self.get_scores(query)
  • top_n = np.argsort(scores)[::-1][:n]
  • [documents[i] for i in top_n]

Создадим вручную объект класса BM25Okapi и набор документов (без метаданных для простоты):

-4

свойства

  • idf - содержит idf токенов
  • doc_freqs - содержит список словарей с частотами токенов для каждого документа
  • get_scores - возвращает список скоров каждого документа относительно query
  • get_top_n - возвращает топ k самых близких документов
  • doc_len - содержит список количества токенов в каждом документе
  • avgdl - содержит среднее количество токенов в документах

формула скора

Посчитаем вручную скор для нулевого документа, который возвращает get_scores:

-5

idf для каждого токена считается в BM25Okapi конструкторе по следующей формуле:

-6

Добавление константы (0.5) не дает знаменателю или числителю стать равным нулю и сглаживает рост idf для слов, которые встречаются только в единичных документах.

Посчитаем idf для токена "open-sourc", который встречается в 1 из 5 текстов:

-7

tf для "open-sourc" в нулевом документе получим так:

-8

Для извлечения скора документа надо итерировать по всем токенам query и посчитать сумму для каждого по формуле:

-9

Из формулы следует, что для документов с длиной меньше средней слагаемое будет увеличено (короткие получают бонус), а для больше средней - уменьшено (штраф за размытость). Посчитаем добавку для токена "open-sourc":

-10

А теперь выведем скор для всего нулевого документа:

-11

По скору наш (нулевой) документ второй, в такой очередности он и выводится в get_top_n:

-12

#bm25retriever#retrievers#llm#rag#tfidf#nlp#ml

-13