Защо самото търсене по сходство не е достатъчно

1. Възходът на LLM и RAG

Изминаха почти девет месеца от първата дата, на която ChatGPT беше пуснат за обществеността, и оттогава почти всеки ден виждаме нови идеи и проекти, изградени върху GPT, както и много други големи езикови модели (LLM). LLM революционизираха обработката на естествения език, задвижвайки разнообразни приложения от чатботове до генериране на съдържание, подобрявайки потребителското изживяване с човешки взаимодействия и позволявайки отговори в реално време, съобразени с контекста, в различните индустрии.

Сред много модели на използване на LLM, напр. фина настройка, обучение с няколко изстрела и т.н. Подсиленото генериране на извличане (RAG) демонстрира уникалните си предимства.

RAG беше представен от Facebook AI в документ, озаглавен „„Подсилено генериране на извличане за NLP задачи, изискващи знания“», който беше представен на NeurIPS 2020. Диаграмата по-долу ясно обяснява концепцията на RAG.

Накратко, RAG включва 2 стъпки:

  1. Компонент за извличане: Това използва система за извличане на информация (често изградена върху широкомащабни корпуси) за извличане на подходящи документи или пасажи, които могат да бъдат полезни за дадена заявка.
  2. Генеративен компонент: Това обикновено е широкомащабен езиков модел, който генерира човешки текст въз основа на въведените данни и извлечените документи.

За подобряване на осъзнаването на контекста, обяснимостта и уместността в NLP/NLU, RAG със сигурност е доказал своята уникална стойност пред подхода за фина настройка на LLM на много места. Всъщност една от предишните ми публикации имаше по-подробно обяснение за базираното на RAG решение.



RAG предлага мощен подход за комбиниране на базирани на извличане и генериращи модели. Въпреки това, както всички модели, той идва със собствен набор от предизвикателства:

1. Точност на извличане: Ефективността на RAG зависи до голяма степен от точността на компонента за извличане. Ако механизмът за извличане извлича неподходящи документи или пропуска важни, генерираният изход може да бъде неоптимален.

2. Зависимост от данни: Моделът зависи от качеството и уместността на външния набор от данни, използван за извличане. Липсата на изчерпателен или актуален набор от данни може да ограничи производителността на системата.

3. Излишък: Има риск от излишък, ако извлечените документи имат припокриваща се информация. Моделът може да повтори или да подчертае прекалено определени аспекти в генерирания резултат.

4. Ограничение на размера: LLM обикновено поставят ограничение върху размера на подканата, предоставена за генериране на съдържание.

5. Познаване на домейна: Адаптирането на RAG към конкретни домейни може да изисква специфична за домейна настройка на компонента за извличане или повторно обучение на цялата система на специфични за домейн данни.

Тъй като много реализации на RAG разчитат на вграждане на текст за извличане на съдържание въз основа на неговата семантика, ще демонстрирам подход за подобряване на точността на извличане, който използва съвместно филтриране върху графика на знанието.

2. Преглед на вграждането на текст

В друга моя публикация, Вграждане на текст — какво, защо и как? Обясних определението, подхода за генериране, съхраняване и търсене на вграждане на текст. Вграждането е процес, а вектор е типът данни, но повече от често двете се използват взаимозаменяемо.



Това, което не споменах в публикацията, са ограниченията на генерираните от LLM вграждания. Когато се прилагат в реални приложения, често можем да открием, че отговорите на даден въпрос са не съвсем подходящи, когато се извличат само чрез търсене на най-сходните текстови вграждания. Някои от причините са (като GPT като пример):

  1. Контекстуални вграждания: Една от силните и потенциалните слабости на GPT е, че той генерира контекстуални вграждания. Това означава, че представянето на дума или фраза може да се промени въз основа на заобикалящия контекст. Когато търсите с помощта на вграждания от конкретен контекст, те може да не се подравнят перфектно с вграждания, генерирани от различен контекст, дори ако терминът е същият.
  2. Висока размерност: Вгражданията на GPT са многоизмерни, улавяйки широка гама от езикови нюанси. Въпреки че това богатство е полезно, понякога може да даде приоритет на някои измерения, които въвеждат шум или неуместности в търсенето.
  3. Пристрастия на данните за обучение: Вгражданията, произведени от GPT, отразяват отклоненията и разпространението на неговите данни за обучение. Ако определени теми или контексти са недостатъчно представени в данните за обучение, техните вграждания може да не са толкова точни, което води до неоптимални резултати от търсенето.
  4. Семантични припокривания: GPT може да генерира вграждания, които са семантично близки за термини или фрази, които хората възприемат като различни. Това може да доведе до извличане на резултати, които изглеждат неуместни.

Всъщност, с помощта на специфична за домейн графика на знания, която е свързана от знания, е възможно да се подобри точността на търсене, базирано на вграждане, като се използват съществуващи методи като съвместно филтриране. На високо ниво по-долу са необходимите стъпки:

1 — За въпрос вземете вграждането на неговия текст и нека го наречем emb(q).

2 — Направете търсене във векторен магазин, за да намерите съдържание, което има най-сходни вграждания като emb(q), и запазете резултата за сходство в sim(content ).

3 — За върнато съдържание приложете съвместно филтриране, за да изчислите тегло и да коригирате резултата за сходство.

4 — Връщане на съдържание, което вече е с подобрена точност.

Нека ви покажа как работи това с помощта на Movie Graph.

3. Пример: Препоръки за филми

3.1 Подгответе Movie Graph

За да започна експеримента възможно най-просто, ще създам безплатен екземпляр на AuraDB от Neo4j за филмовата графика.



След като екземплярът започне да работи, щракнете върху бутона _Query, за да стартирате Neo4j браузър. Тук ще трябва да поставите паролата, генерирана по време на създаването на инстанцията. След като стартирате браузъра, в текстовото поле въведете:

:play movies

Това ще стартира урока за филмова графика. Отидете до втората страница и щракнете върху блока, където виждате няколко реда код. С 3 прости стъпки ние просто създаваме Movie Graph.

Когато щракнете върху иконата на базата данни отляво, трябва да можем да видим създадените възли и връзки.

За по-изчерпателен урок за Movie Graph и Neo4j, ето страхотен ресурс.



3.2 Добавяне на текстови вграждания към филмови заглавия и лозунги

Във филмовата графика всеки възел на филма има 2 текстови свойства: заглавие и слоган. Нека генерираме текстови вграждания както за заглавие, така и за слоган, като използваме кода на Python по-долу.

from neo4j import GraphDatabase
from openai.embeddings_utils import get_embedding
import openai 

"""
LoadEmbedding: call OpenAI embedding API to generate embeddings for each proporty of node in Neo4j
Version: 1.1
"""
OPENAI_KEY = "OPENAI-KEY"
EMBEDDING_MODEL = "text-embedding-ada-002"
NEO4J_URL = "neo4j+s://INSTANCE_ID.databases.neo4j.io:7687"
NEO4J_USER = "neo4j"
NEO4J_PASSWORD = "NEO4J_PASSWORD"
class LoadEmbedding:
    def __init__(self, uri, user, password):
        self.driver = GraphDatabase.driver(uri, auth=(user, password))
        openai.api_key = OPENAI_KEY
    def close(self):
        self.driver.close()
    def load_embedding_to_node_property(self, label, property):
        with self.driver.session() as session:
            result = session.run("MATCH (n:" + label + ") WHERE n." + property + " IS NOT NULL RETURN id(n) AS id, n." + property + " as " + property)
            # call OpenAI embedding API to generate embeddings for each proporty of node
            # for each node, update the embedding property
            count = 0
            for record in result:
                id = record["id"]
                text = record[property]
                # Below, instead of using the text as the input for embedding, we add label and property name in front of it
                embedding = get_embedding(label + " " + property + " - " + text, EMBEDDING_MODEL)
                # key property of Embedding node differentiates different embeddings
                cypher = "CREATE (e:Embedding) SET e.key=$key, e.value=$embedding"
                cypher = cypher + " WITH e MATCH (n) WHERE id(n) = $id CREATE (n) -[:HAS_EMBEDDING]-> (e)"
                session.run(cypher,key=property, embedding=embedding, id=id )
                count = count + 1
            
            print("Processed " + str(count) + " " + label + " nodes for property @" + property + ".")
            return count
if __name__ == "__main__":
    loader = LoadEmbedding(NEO4J_URL, NEO4J_USER, NEO4J_PASSWORD)
    loader.load_embedding_to_node_property("Movie", "title")
    loader.load_embedding_to_node_property("Movie", "tagline")
    print("done")
    loader.close()

Надявам се кодът по-горе да е достатъчно ясен за разбиране. Това, което искам да подчертая тук, е начинът, по който се моделират вгражданията.

За филм напр. Аполо 13 по-горе, той е свързан с два възела за вграждане, един за заглавие и друг за слоган. Основното внимание тук е бъдещата мащабируемост, когато искаме да съхраняваме вграждания на едно и също съдържание от различни LLM и/или версии.

3.3 Създайте векторен индекс

Векторното или вграденото индексиране е съществена част от търсенето по сходство и заявките за най-близък съсед, особено в областта на машинното обучение, извличането на информация и обработката на естествения език. Подобно на нормалния индекс, векторният индекс трябва да осигури високоефективен отговор при търсене.

Въведете следната Cypher заявка в текстовото поле в браузъра Neo4j:

CALL db.index.vector.createNodeIndex('embedingIndex', 'Embedding', 'value', 1536, 'COSINE')

Дефиниция на параметър:

  • embeddingIndex: име на индекс
  • Вграждане: име на етикет
  • стойност: име на свойство
  • 1536: измерение на вектори. За GPT модел вграждането има 1536 измерения.
  • КОСИНУС: функция за подобие

3.4 Първи тест: Търсене по подобие

Сега нека направим първия тест. Да приемем, че искаме да препоръчаме филми, подобни на Матрицата. В нашата филмова графика имаме слоган и заглавие, които да използваме за съвпадение по сходство.

// Find movie The Matrix
MATCH (m1:Movie{title:'The Matrix'}) -[:HAS_EMBEDDING]-> (e:Embedding{key:'tagline'})
WITH m1, e
// Find other movies which have high semantic similarity on tagline
CALL db.index.vector.queryNodes("embeddingIndex", 50, e.value) YIELD node, score
WITH node, score
WHERE score < 1.0   // exclude self
// From the returned Embedding nodes, find their connected Movie nodes
MATCH (m2:Movie)  -[:HAS_EMBEDDING]-> (node)
WHERE node.key = 'tagline'
RETURN m2.title, m2.tagline, score

Тук използваме db.index.vector.queryNodes, за да върнем топ 50 най-сходните вграждания на слоган на филм, като изчисляваме COSINE сходство между вграждането на слоган на филм The Matrix(което е „Добре дошли в реалния свят“), срещу вграждане на други надписи. По-долу са първите 5 върнати записа:

╒═════════════════════════════════╤══════════════════════════════════════════════════════════════════════╤══════════════════╕
│m2.title                         │m2.tagline                                                            │score             │
╞═════════════════════════════════╪══════════════════════════════════════════════════════════════════════╪══════════════════╡
│"The Birdcage"                   │"Come as you are"                                                     │0.9422817826271057│
├─────────────────────────────────┼──────────────────────────────────────────────────────────────────────┼──────────────────┤
│"The Matrix Reloaded"            │"Free your mind"                                                      │0.9407751560211182│
├─────────────────────────────────┼──────────────────────────────────────────────────────────────────────┼──────────────────┤
│"Cast Away"                      │"At the edge of the world, his journey begins."                       │0.9367693662643433│
├─────────────────────────────────┼──────────────────────────────────────────────────────────────────────┼──────────────────┤
│"Ninja Assassin"                 │"Prepare to enter a secret world of assassins"                        │0.9366204738616943│
├─────────────────────────────────┼──────────────────────────────────────────────────────────────────────┼──────────────────┤
│"That Thing You Do"              │"In every life there comes a time when that thing you dream becomes th│0.9354331493377686│
│                                 │at thing you do"                                                      │                  │
├─────────────────────────────────┼──────────────────────────────────────────────────────────────────────┼──────────────────┤

Очевидно има някои проблеми, тъй като резултатите не изглеждаха много уместни, особено. другите 2 епизода от поредицата бяха изброени с по-ниски резултати (ранг 2 и 14).

Една потенциална причина е мотото на Tthe Matrix, „Добре дошли в реалния свят“, всъщност не дава много контекст. Това доста често е предизвикателството.

3.5 Втори тест: Комбинирайте търсене, базирано на подобие, със съвместно филтриране

Сега нека се опитаме да видим каква друга информация във Movie Graph може да се използва за коригиране на резултатите.

Една очевидна стратегия е да разгледате наличните връзки между филми, актьори и режисьори. За филми, които са режисирани от едни и същи режисьори и/или избрани от едни и същи актьори, те трябва да са по-подходящи от други филми.

По-долу е блокът с код за актуализиране.

// 1. Find movie The Matrix
MATCH (m1:Movie{title:'The Matrix'}) -[:HAS_EMBEDDING]-> (e:Embedding{key:'tagline'})
WITH m1, e

// 2. Find other movies which have high semantic similarity on tagline
CALL db.index.vector.queryNodes("embeddingIndex", 50, e.value) YIELD node, score
WITH m1, node, score
WHERE score < 1.0   // exclude self

// 3. From the returned Embedding nodes, find their connected Movie nodes
MATCH (m2:Movie)  -[:HAS_EMBEDDING]-> (node)
WHERE node.key = 'tagline'

// 4. For returned Movie nodes, count number of same directors and actors
WITH m1, m2, score, 
     COUNT {(m1) <-[:ACTED_IN]- () -[:ACTED_IN]-> (m2)} AS sameActorCount,
     COUNT {(m1) <-[:DIRECTED]- () -[:DIRECTED]-> (m2)} AS sameDirectorCount

// 5. Use sameActorCount and sameDirectorCount to calculate weights and apply to similarity score
WITH m1, m2, score, sameActorCount, sameDirectorCount,
     CASE WHEN sameActorCount > 0 THEN 1+log(1+sameActorCount) ELSE 1 END AS actorWeight,
     1+sameDirectorCount AS directorWeight
RETURN  m2.title, m2.tagline AS tagline, sameActorCount, sameDirectorCount, score, actorWeight * directorWeight * score AS rank 
ORDER BY rank DESC;

Резултатите показаха, че въпреки че някои лозунги имат по-висок резултат за семантично сходство, окончателното им класиране става по-ниско при прилагане на съвместно филтриране, напр. Облачен атлас. Междувременно другите 2 епизода от трилогията „Матрицата“ се повишиха в класирането поради връзките на общи режисьори и актьори.

4. Бъдещи перспективи

Интегрирането на LLM с Filtered Knowledge Graph има обещаващ потенциал в много аспекти, особено. когато графичната база данни като Neo4j може да обработва векторни данни естествено, за да осигури ефективно съхранение, индексиране и възможности за търсене на обикновен хардуер (не се изисква GPU).

В бъдещите публикации в блога ще се потопя дълбоко в това пространство и ще покрия повече теми като:
- Подобрена производителност при НЛП задачи надолу по веригата.
- По-точно представяне на семантичните връзки между обектите.
- По-добро боравене с редки или нишови термини, които може да не бъдат адекватно уловени от традиционните LLMs.
- По-добро филтриране и ролеви контрол на достъпа при векторно търсене.

Честито вграждане!