Вечерта на играта е, вашите приятели са накацали около масата за игри и чакат да видят в какъв персонаж от Dungeons & Dragons (D&D) ще се превърнат и в мисията, в която ще се впуснат. Тази вечер вие сте Dungeon Master (разказвач на истории и водач), създател на вълнуващи срещи, за да предизвикате и увлечете своите играчи. Вашият надежден D&D Monster Manual съдържа хиляди същества. Намирането на перфектното чудовище за всяка ситуация сред безбройните опции може да бъде непосилно. Идеалният враг трябва да съответства на обстановката, трудността и разказа на момента.
Какво ще стане, ако можем да създадем инструмент, който незабавно намира чудовището, което е най-подходящо за всеки сценарий? Инструмент, който взема предвид множество фактори едновременно , като гарантира, че всяка среща е възможно най-поглъщаща и вълнуваща?
Нека се впуснем в нашето собствено търсене: изградете най-добрата система за намиране на чудовища, използвайки силата на многоатрибутното векторно търсене!
Векторното търсене представлява революция в извличането на информация. Вграждането на вектори - като взема предвид контекста и семантичното значение - дава възможност на векторното търсене да връща по-подходящи и точни резултати, да обработва не само структурирани, но и неструктурирани данни и множество езици и мащаб. Но за да генерираме висококачествени отговори в приложения от реалния свят, често трябва да присвоим различни тегла на специфични атрибути на нашите обекти с данни.
Има два често срещани подхода за многоатрибутно векторно търсене. И двете започват с отделно вграждане на всеки атрибут на обект с данни. Основната разлика между тези два подхода е в това как нашите вграждания се съхраняват и търсят .
spaces
на Superlinked също ни позволяват да претегляме всеки атрибут по време на заявка, за да изведем по-подходящи резултати, без последваща обработка. По-долу ще използваме тези два подхода, за да внедрим инструмент за векторно търсене с множество атрибути - инструмент за намиране на чудовища Dungeons and Dragons! Нашите прости реализации, особено втората, ще илюстрират как да създадете по-мощни и гъвкави системи за търсене, такива, които могат да обработват сложни, многостранни заявки с лекота, независимо от вашия случай на употреба.
Ако сте нов в търсенето на векторно сходство, не се притеснявайте! Ние ви покриваме – разгледайте нашите статии за градивни елементи .
Добре, да тръгваме на лов за чудовища!
Първо, ще генерираме малък синтетичен набор от данни за чудовища, като подканим голям езиков модел (LLM):
Generate two JSON lists: 'monsters' and 'queries'. 1. 'monsters' list: Create 20 unique monsters with the following properties: - name: A distinctive name - look: Brief description of appearance (2-3 sentences) - habitat: Where the monster lives (2-3 sentences) - behavior: How the monster acts (2-3 sentences) Ensure some monsters share similar features while remaining distinct. 2. 'queries' list: Create 5 queries to search for monsters: - Each query should be in the format: {look: "...", habitat: "...", behavior: "..."} - Use simple, brief descriptions (1-3 words per field) - Make queries somewhat general to match multiple monsters Output format: { "monsters": [ {"name": "...", "look": "...", "habitat": "...", "behavior": "..."}, ... ], "queries": [ {"look": "...", "habitat": "...", "behavior": "..."}, ... ] }
Нека да разгледаме извадка от набора от данни, генериран от LLM. Забележка: Генерирането на LLM не е детерминирано, така че вашите резултати може да се различават.
Ето нашите първи пет чудовища:
# | име | погледнете | местообитание | поведение |
---|---|---|---|---|
0 | Luminoth | Подобно на молец същество със светещи крила и антена | Гъсти гори и джунгли с биолуминисцентна флора | Излъчва успокояващи светлинни модели за общуване и привличане на плячка |
1 | Aqua Wraith | Полупрозрачна хуманоидна фигура, изработена от течаща вода | Реки, езера и крайбрежни зони | Променя формата си, за да се слее с водните тела и контролира теченията |
2 | Голем от Каменно сърце | Масивен хуманоид, съставен от преплетени скални образувания | Скалисти планини и древни руини | Хибернира от векове, събужда се, за да защити територията си |
3 | Шепнеща сянка | Сенчесто, аморфно същество със светещи очи | Тъмни гори и изоставени сгради | Храни се със страх и нашепва тревожни истини |
4 | Зефир танцьор | Грациозно птиче създание с преливащи се пера | Високи планински върхове и брулени от вятъра равнини | Създава хипнотизиращи въздушни дисплеи за привличане на партньори |
...и нашите генерирани запитвания:
| Вижте | Хабитат | Поведение |
---|---|---|---|
0 | Светещ | Тъмни места | Светлинна манипулация |
1 | Елементарно | Екстремни среди | Контрол на околната среда |
2 | Промяна на формата | Разнообразни пейзажи | Създаване на илюзия |
3 | Кристален | Богати на минерали райони | Усвояване на енергия |
4 | Ефирен | Атмосферно | Влияние на ума |
Вижте оригинален набор от данни и примери за заявки тук .
Нека да настроим параметри, които ще използваме и в двата ни подхода - наивен и Superlinked - по-долу.
Ние генерираме нашите векторни вграждания с:
sentence-transformers/all-mpnet-base-v2.
За по-голяма простота ще ограничим изхода си до първите 3 съвпадения. (За пълен код, включително необходимите импортирания и помощни функции, вижте бележника .)
LIMIT = 3 MODEL_NAME = "sentence-transformers/all-mpnet-base-v2"
Сега нека да започнем нашето многоатрибутно търсене на чудовища! Първо, ще опитаме наивния подход.
В нашия наивен подход ние вграждаме атрибути независимо и ги съхраняваме в различни индекси. По време на заявка ние изпълняваме множество kNN-търсения на всички индекси и след това комбинираме всички наши частични резултати в един.
Започваме с дефиниране на клас
NaiveRetriever
за извършване на търсене въз основа на подобие на нашия набор от данни, използвайки нашите генерирани от all-mpnet-base-v2
вграждания.
class NaiveRetriever: def __init__(self, data: pd.DataFrame): self.model = SentenceTransformer(MODEL_NAME) self.data = data.copy() self.ids = self.data.index.to_list() self.knns = {} for key in self.data: embeddings = self.model.encode(self.data[key].values) knn = NearestNeighbors(metric="cosine").fit(embeddings) self.knns[key] = knn def search_key(self, key: str, value: str, limit: int = LIMIT) -> pd.DataFrame: embedding = self.model.encode(value) knn = self.knns[key] distances, indices = knn.kneighbors( [embedding], n_neighbors=limit, return_distance=True ) ids = [self.ids[i] for i in indices[0]] similarities = (1 - distances).flatten() # by definition: # cosine distance = 1 - cosine similarity result = pd.DataFrame( {"id": ids, f"score_{key}": similarities, key: self.data[key][ids]} ) result.set_index("id", inplace=True) return result def search(self, query: dict, limit: int = LIMIT) -> pd.DataFrame: results = [] for key, value in query.items(): if key not in self.knns: continue result_key = self.search_key(key, value, limit=limit) result_key.drop(columns=[key], inplace=True) results.append(result_key) merged_results = pd.concat(results, axis=1) merged_results["score"] = merged_results.mean(axis=1, skipna=False) merged_results.sort_values("score", ascending=False, inplace=True) return merged_results naive_retriever = NaiveRetriever(df.set_index("name"))
Нека използваме първата заявка от нашия генериран списък по-горе и да потърсим чудовища с помощта на нашия naive_retriever
:
query = { 'look': 'glowing', 'habitat': 'dark places', 'behavior': 'light manipulation' } naive_retriever.search(query)
Нашите
naive_retriever
връща следните резултати от търсенето за всеки атрибут:
id | score_look | погледнете |
---|---|---|
Шепнеща сянка | 0,503578 | Сенчесто, аморфно същество със светещи очи |
Пясъчна буря Джин | 0,407344 | Въртящ се пясъчен вихър със светещи символи |
Luminoth | 0,378619 | Подобно на молец същество със светещи крила и антена |
Страхотно! Нашите върнати резултати за чудовища са уместни - всички те имат някаква "светеща" характеристика.
Нека видим какво връща наивният подход, когато търсим другите два атрибута.
id | резултат_хабитат | местообитание |
---|---|---|
Шепнеща сянка | 0,609567 | Тъмни гори и изоставени сгради |
Гъбична мрежа | 0,438856 | Подземни пещери и влажни гори |
Thornvine Elemental | 0,423421 | Обрасли руини и гъсти джунгли |
id | резултат_поведение | поведение |
---|---|---|
Живи графити | 0,385741 | Променя формата си, за да се слее с околната среда и абсорбира пигменти |
Crystalwing Дрейк | 0,385211 | Съхранява скъпоценни камъни и може да пречупва светлината в мощни лъчи |
Luminoth | 0,345566 | Излъчва успокояващи светлинни модели за общуване и привличане на плячка |
Всички извлечени чудовища наистина притежават желаните атрибути. На пръв поглед наивните резултати от търсенето може да изглеждат обещаващи. Но ние трябва да намерим чудовища, които притежават и трите атрибута едновременно . Нека обединим нашите резултати, за да видим колко добре се справят нашите чудовища при постигането на тази цел:
id | score_look | резултат_хабитат | резултат_поведение |
---|---|---|---|
Шепнеща сянка | 0,503578 | 0,609567 | |
Пясъчна буря Джин | 0,407344 | | |
Luminoth | 0,378619 | | 0,345566 |
Гъбична мрежа | | 0,438856 | |
Thornvine Elemental | | 0,423421 | |
Живи графити | | | 0,385741 |
Crystalwing Дрейк | | | 0,385211 |
И тук стават очевидни границите на наивния подход. Нека да оценим:
look
: Три чудовища бяха извлечени (Whispering Shade, Sandstorm Djinn и Luminoth).habitat
: Само едно чудовище от резултатите look
беше подходящо (Шепнеща сянка).behavior
: Само едно чудовище от резултатите look
беше уместно (Luminoth), но то е различно от това, което е уместно за habitat
.Накратко, наивният подход на търсене не успява да намери чудовища, които отговарят на всички критерии наведнъж. Може би можем да коригираме този проблем чрез проактивно извличане на повече чудовища за всеки атрибут? Нека опитаме с 6 чудовища на атрибут, вместо с 3. Нека да разгледаме какво генерира този подход:
id | score_look | резултат_хабитат | резултат_поведение |
---|---|---|---|
Шепнеща сянка | 0,503578 | 0,609567 | |
Пясъчна буря Джин | 0,407344 | 0,365061 | |
Luminoth | 0,378619 | | 0,345566 |
Мъглявина Медуза | 0,36627 | | 0,259969 |
Dreamweaver Octopus | 0,315679 | | |
Квантова светулка | 0,288578 | | |
Гъбична мрежа | | 0,438856 | |
Thornvine Elemental | | 0,423421 | |
Мъглив фантом | | 0,366816 | 0,236649 |
Голем от Каменно сърце | | 0,342287 | |
Живи графити | | | 0,385741 |
Crystalwing Дрейк | | | 0,385211 |
Aqua Wraith | | | 0,283581 |
Вече извлякохме 13 чудовища (повече от половината от нашия малък набор от данни!) и все още имаме същия проблем: нито едно от тези чудовища не беше извлечено и за трите атрибута.
Увеличаването на броя на извлечените чудовища (над 6) може да реши нашия проблем, но създава допълнителни проблеми:
В обобщение, наивният подход е твърде несигурен и неефективен за жизнеспособно многоатрибутно търсене, особено в производството.
Нека приложим нашия втори подход, за да видим дали се справя по-добре от наивния.
Първо дефинираме схемата, интервалите, индекса и заявката:
@schema class Monster: id: IdField look: String habitat: String behavior: String monster = Monster() look_space = TextSimilaritySpace(text=monster.look, model=MODEL_NAME) habitat_space = TextSimilaritySpace(text=monster.habitat, model=MODEL_NAME) behavior_space = TextSimilaritySpace(text=monster.behavior, model=MODEL_NAME) monster_index = Index([look_space, habitat_space, behavior_space]) monster_query = ( Query( monster_index, weights={ look_space: Param("look_weight"), habitat_space: Param("habitat_weight"), behavior_space: Param("behavior_weight"), }, ) .find(monster) .similar(look_space.text, Param("look")) .similar(habitat_space.text, Param("habitat")) .similar(behavior_space.text, Param("behavior")) .limit(LIMIT) ) default_weights = { "look_weight": 1.0, "habitat_weight": 1.0, "behavior_weight": 1.0 }
Сега стартираме изпълнителя и качваме данните:
monster_parser = DataFrameParser(monster, mapping={monster.id: "name"}) source: InMemorySource = InMemorySource(monster, parser=monster_parser) executor = InMemoryExecutor(sources=[source], indices=[monster_index]) app = executor.run() source.put([df])
Нека изпълним същата заявка, която изпълнихме в нашата реализация на наивен подход по-горе:
query = { 'look': 'glowing', 'habitat': 'dark places', 'behavior': 'light manipulation' } app.query( monster_query, limit=LIMIT, **query, **default_weights )
id | резултат | погледнете | местообитание | поведение |
---|---|---|---|---|
Шепнеща сянка | 0,376738 | Сенчесто, аморфно същество със светещи очи | Тъмни гори и изоставени сгради | Храни се със страх и нашепва тревожни истини |
Luminoth | 0,340084 | Подобно на молец същество със светещи крила и антена | Гъсти гори и джунгли с биолуминисцентна флора | Излъчва успокояващи светлинни модели за общуване и привличане на плячка |
Живи графити | 0,330587 | Двуизмерно цветно същество, което обитава плоски повърхности | Градски зони, особено стени и билбордове | Променя формата си, за да се слее с околната среда и абсорбира пигменти |
Ето го! Този път всяко от нашите най-добре върнати чудовища се класира високо в резултат, който представлява един вид „средна стойност“ на всичките три характеристики, които искаме нашето чудовище да притежава. Нека разделим резултата на всяко чудовище в детайли:
id | погледнете | местообитание | поведение | общо |
---|---|---|---|---|
Шепнеща сянка | 0,167859 | 0,203189 | 0,005689 | 0,376738 |
Luminoth | 0,126206 | 0,098689 | 0,115189 | 0,340084 |
Живи графити | 0,091063 | 0,110944 | 0,12858 | 0,330587 |
Вторият и третият ни резултат, Luminoth и Living Graffiti, притежават и трите желани характеристики. Най-добрият резултат, Whispering Shade, въпреки че е по-малко уместен по отношение на манипулирането на светлината - както е отразено в behavior
му резултат (0,006), има "светещи" характеристики и тъмна среда, които правят неговия look
(0,168) и habitat
(0,203) много добри високо, което му дава най-висок общ резултат (0,377), което го прави най-подходящото чудовище като цяло. Какво подобрение!
Можем ли да възпроизведем нашите резултати? Нека опитаме друга заявка и да разберем.
query = { 'look': 'shapeshifting', 'habitat': 'varied landscapes', 'behavior': 'illusion creation' }
id | резултат | погледнете | местообитание | поведение |
---|---|---|---|---|
Мъглив фантом | 0,489574 | Ефирен, подобен на мъгла хуманоид с променящи се черти | Блата, мочурища и мъгливи брегове | Подмамва пътниците с илюзии и шепоти |
Зефир танцьор | 0,342075 | Грациозно птиче създание с преливащи се пера | Високи планински върхове и брулени от вятъра равнини | Създава хипнотизиращи въздушни дисплеи за привличане на партньори |
Шепнеща сянка | 0,337434 | Сенчесто, аморфно същество със светещи очи | Тъмни гори и изоставени сгради | Храни се със страх и нашепва тревожни истини |
Страхотно! Резултатите ни отново са отлични.
Ами ако искаме да намерим чудовища, които са подобни на конкретно чудовище от нашия набор от данни? Нека опитаме с чудовище, което още не сме виждали - Harmonic Coral. Можем да извлечем атрибути за това чудовище и да създадем параметри на заявката ръчно. Но Superlinked има метод with_vector
който можем да използваме върху обекта на заявката. Тъй като идентификационният номер на всяко чудовище е неговото име, можем да изразим искането си толкова просто, колкото:
app.query( monster_query.with_vector(monster, "Harmonic Coral"), **default_weights, limit=LIMIT )
id | резултат | погледнете | местообитание | поведение |
---|---|---|---|---|
Хармоничен корал | 1 | Разклонена, подобна на музикален инструмент структура с вибриращи жилки | Плитки морета и приливни басейни | Създава сложни мелодии за общуване и повлияване на емоциите |
Dreamweaver Octopus | 0,402288 | Главоноги с пипала, които блестят като полярни сияния | Дълбоки океански ровове и подводни пещери | Влияе на сънищата на близките същества |
Aqua Wraith | 0,330869 | Полупрозрачна хуманоидна фигура, изработена от течаща вода | Реки, езера и крайбрежни зони | Променя формата си, за да се слее с водните тела и контролира теченията |
Най-добрият резултат е най-подходящият, самият Harmonic Coral, както се очакваше. Другите две чудовища, които нашето търсене извлича, са Dreamweaver Octopus и Aqua Wraith. И двете споделят важни тематични ( атрибути ) елементи с Harmonic Coral:
habitat
)behavior
)look
) Да предположим сега, че искаме да придадем по-голямо значение на атрибута look
. Рамката Superlinked ни позволява лесно да коригираме теглата по време на заявка. За по-лесно сравнение ще търсим чудовища, подобни на Harmonic Coral, но с нашите тегла, коригирани в полза на look
.
weights = { "look_weight": 1.0, "habitat_weight": 0, "behavior_weight": 0 } app.query( monster_query.with_vector(monster, "Harmonic Coral"), limit=LIMIT, **weights )
id | резултат | погледнете | местообитание | поведение |
---|---|---|---|---|
Хармоничен корал | 0,57735 | Разклонена, подобна на музикален инструмент структура с вибриращи жилки | Плитки морета и приливни басейни | Създава сложни мелодии за общуване и повлияване на емоциите |
Thornvine Elemental | 0,252593 | Подобно на растение същество с тяло от усукани лиани и тръни | Обрасли руини и гъсти джунгли | Расте бързо и контролира живота на околните растения |
Плазмената змия | 0,243241 | Змиеподобно създание, направено от пращяща енергия | Електрически бури и електроцентрали | Захранва се с електрически ток и може да доведе до късо съединение в технологията |
Всичките ни резултати (по подходящ начин) имат сходен външен вид – „Разклоняване с вибриращи жилки“, „Подобно на растение същество с тяло от усукани лиани и тръни“, „Подобно на змия“.
Сега нека направим друго търсене, като пренебрегнем външния вид и вместо това потърсим чудовища, които са сходни по отношение на habitat
и behavior
едновременно:
weights = { "look_weight": 0, "habitat_weight": 1.0, "behavior_weight": 1.0 }
id | резултат | погледнете | местообитание | поведение |
---|---|---|---|---|
Хармоничен корал | 0,816497 | Разклонена, подобна на музикален инструмент структура с вибриращи жилки | Плитки морета и приливни басейни | Създава сложни мелодии за общуване и повлияване на емоциите |
Dreamweaver Octopus | 0,357656 | Главоноги с пипала, които блестят като полярни сияния | Дълбоки океански ровове и подводни пещери | Влияе на сънищата на близките същества |
Мъглив фантом | 0,288106 | Ефирен, подобен на мъгла хуманоид с променящи се черти | Блата, мочурища и мъгливи брегове | Подмамва пътниците с илюзии и шепоти |
Отново подходът Superlinked дава страхотни резултати. И трите чудовища живеят във водна среда и притежават способности за контролиране на ума.
И накрая, нека опитаме друго търсене, претегляйки трите атрибута по различен начин - за да намерим чудовища, които в сравнение с Harmonic Coral изглеждат донякъде подобни, живеят в много различни местообитания и имат много подобно поведение:
weights = { "look_weight": 0.5, "habitat_weight": -1.0, "behavior_weight": 1.0 }
id | резултат | погледнете | местообитание | поведение |
---|---|---|---|---|
Хармоничен корал | 0,19245 | Разклонена, подобна на музикален инструмент структура с вибриращи жилки | Плитки морета и приливни басейни | Създава сложни мелодии за общуване и повлияване на емоциите |
Luminoth | 0,149196 | Подобно на молец същество със светещи крила и антена | Гъсти гори и джунгли с биолуминисцентна флора | Излъчва успокояващи светлинни модели за общуване и привличане на плячка |
Зефир танцьор | 0,136456 | Грациозно птиче създание с преливащи се пера | Високи планински върхове и брулени от вятъра равнини | Създава хипнотизиращи въздушни дисплеи за привличане на партньори |
Отново страхотни резултати! Нашите други две извлечени чудовища — Luminoth и Zephyr Dancer — имат поведение, подобно на Harmonic Coral, и живеят в местообитания, различни от тези на Harmonic Coral. Освен това изглеждат много по-различно от Harmonic Coral. (Въпреки че пипалата на Harmonic Coral и антената на Luminoth са донякъде подобни характеристики, ние само намалихме теглото на look_weight
с 0,5 и приликата между двете чудовища свършва дотук.)
Нека видим как се разбиват общите резултати на тези чудовища по отношение на отделните атрибути:
id | погледнете | местообитание | поведение | общо |
---|---|---|---|---|
Хармоничен корал | 0,19245 | -0,3849 | 0,3849 | 0,19245 |
Luminoth | 0,052457 | -0,068144 | 0,164884 | 0,149196 |
Зефир танцьор | 0,050741 | -0,079734 | 0,165449 | 0,136456 |
Чрез отрицателно претегляне на habitat_weight
(-1,0), ние умишлено „отблъскваме“ чудовища с подобни местообитания и вместо това излизаме на повърхността чудовища, чиито среди са различни от тези на Harmonic Coral – както се вижда от отрицателните резултати habitat
на Luminoth и Zephyr Dancer. Резултатите behavior
на Luminoth и Zephyr Dancer са сравнително високи, което показва тяхното сходство в поведението с Harmonic Coral. Резултатите им look
са положителни, но по-ниски, което отразява известно , но не изключително визуално сходство с Harmonic Coral.
Накратко, нашата стратегия за намаляване на habitat_weight
до -1.0 и look_weight
до 0.5, но запазване на behavior_weight
на 1.0, се оказва ефективна при излизане на повърхността на чудовища, които споделят ключови поведенчески характеристики с Harmonic Coral, но имат много различна среда и изглеждат поне малко по-различно.
Мултиатрибутното векторно търсене е значителен напредък в извличането на информация, предлагайки повече точност, разбиране на контекста и гъвкавост от основното търсене по семантично сходство. И все пак, нашият наивен подход (по-горе) - съхраняване и търсене на вектори на атрибути поотделно, след което комбиниране на резултатите - е ограничен по отношение на възможностите, тънкостта и ефективността, когато трябва да извлечем обекти с множество едновременни атрибути. (Освен това многобройните търсения на kNN отнемат повече време от едно търсене с конкатенирани вектори.)
За да се справите със сценарии като този, по-добре е да съхранявате всичките си вектори на атрибути в едно и също векторно хранилище и да извършвате едно търсене , като претегляте атрибутите си по време на заявка. Подходът Superlinked е по-точен, ефективен и мащабируем от наивния подход за всяко приложение, което изисква бързо, надеждно, нюансирано векторно извличане с множество атрибути - независимо дали вашият случай на употреба се справя с предизвикателствата на данните от реалния свят във вашата електронна търговия или система за препоръки ... или нещо съвсем различно, като битка с чудовища.
Първоначално публикувано тук .