Ембединги: смисълът като вектори
Накратко: Ембедингът е вектор от числа за дума или текст, така че близки по смисъл текстове имат близки вектори, а близостта се мери с косинус на ъгъла.
Проблем
Търсене по думи
Ботът на поддръжката има база с готови отговори: по един текст за доставка, връщане и плащане. Клиентът пише въпрос, а ботът трябва да намери текста, който отговаря.
# базата с отговори на поддръжкатаDOCS = [ ("доставка", "доставката отнема до два дни"), ("връщане", "върнете стоката до 14 дни и ще получите парите"), ("плащане", "платете с карта или с наложен платеж"),]
# въпросите на клиентитеQUESTIONS = [ "кога ще дойде пратката ми", "банката отказа картата ми",]Колега търси по общи думи: колкото повече думи от въпроса има в текста, толкова по-добре.
# колко думи от въпроса има и в текстаdef common(question, text): ws = text.split(" ") return sum(1 for word in question.split(" ") if word in ws)for q in QUESTIONS: print(q) best = None top = 0 for topic, text in DOCS: n = common(q, text) print(f" {topic:<10}{n}") if n > top: top = n best = topic print(" →", best or "няма")кога ще дойде пратката ми доставка 0 връщане 1 плащане 0 → връщанебанката отказа картата ми доставка 0 връщане 0 плащане 0 → нямаПървият въпрос намира „връщане“, и то само заради „ще“. Вторият не намира нищо: „картата“ и „карта“ за програмата са различни думи. Клиентите питат със свои думи, а текстовете в базата са писани с други.
Трябва сравнение на смисъла, не на буквите.
Идеята
Смисълът като посока
Дума се познава по компанията си. „пратката“ и „поръчката“ се срещат с едни и същи думи („куриер“, „пристигне“, „дни“), затова значат нещо близко, макар че буквите им са различни.
Това се записва с числа. Всяка дума става вектор: списък от числа, по едно за всяка ос. Думите с близък смисъл получават близки вектори, тоест стрелки, които сочат почти в една посока. Такъв вектор, който представя смисъла, се казва embedding.
В урока осите са осемте най-чести думи в текста на магазина, а числото за всяка ос е в колко изречения думата е заедно с думата на оста. Истинските модели учат векторите с невронна мрежа и имат стотици или хиляди оси, но идеята е същата: близък смисъл, близка посока.
Виж го
Думите като вектори
Кратките думи като „ще“, „до“ и „на“ са навсякъде и не казват нищо за темата. Те не са оси и не влизат във векторите:
# кратките думи, които са навсякъде и не казват тематаSTOP = { "ви", "е", "до", "с", "на", "от", "и", "да", "се", "ще", "по", "за", "но", "съм", "много", "или", "над", "преди", "беше", "в", "след", "не", "при", "ми", "кога",}Пространството: осите и векторът на дума. Векторът на текст е сборът на векторите на думите му.
# думите като вектори: по едно число за всяка осclass Space: # осите: най-честите думи без кратките def __init__(self, text, stop, n): self.text = text self.stop = stop freq = {} for line in text: for w in line.split(" "): if w not in stop: freq[w] = freq.get(w, 0) + 1 self.vocab = list(freq) self.axes = sorted(freq, key=freq.get, reverse=True)[:n]
# в колко изречения думата е заедно с думата на всяка ос def of(self, word): v = [0] * len(self.axes) for line in self.text: ws = line.split(" ") if word not in ws: continue for w in ws: if w in self.axes and w != word: v[self.axes.index(w)] += 1 return v
# векторът на текст: сборът на векторите на думите му def of_text(self, s): v = [0] * len(self.axes) for w in s.split(" "): if w not in self.stop: v = [x + y for x, y in zip(v, self.of(w))] return vspace = Space(TEXT, STOP, 8)print("оси:", " ".join(space.axes))for word in ["пратката", "поръчката", "плащането"]: print(f"{word:<12}{space.of(word)}")оси: куриер стоката доставката поръчката дни пристигне пратката картапратката [1, 0, 0, 0, 1, 1, 0, 0]поръчката [2, 0, 0, 0, 1, 2, 0, 0]плащането [0, 0, 0, 0, 0, 0, 0, 3]„пратката“ е в едно изречение с „куриер“, едно с „дни“ и едно с „пристигне“. „поръчката“ е там два, един и два пъти: числата са различни, но посоката е почти същата. „плащането“ е само с „карта“: съвсем друга посока.
Виж го
Близост: косинус на ъгъла
Колко близки са два вектора, казва ъгълът между тях. Косинусът на ъгъла е 1, когато сочат в една посока, и 0, когато нямат нито една обща ос. Делението на дължините прави така, че значение има само посоката.
# косинусът на ъгъла: 1 е същата посока, 0 нищо общоdef cosine(a, b): dot = sum(x * y for x, y in zip(a, b)) na = sum(x * x for x in a) nb = sum(y * y for y in b) if na == 0 or nb == 0: return 0.0 return dot / (math.sqrt(na) * math.sqrt(nb))space = Space(TEXT, STOP, 8)v = space.of("пратката")print(f"пратката и поръчката: {cosine(v, space.of('поръчката')):.3f}")print(f"пратката и плащането: {cosine(v, space.of('плащането')):.3f}")others = [o for o in space.vocab if o != "пратката"]others.sort(key=lambda o: cosine(v, space.of(o)), reverse=True)print("най-близо до пратката:")for o in others[:5]: print(f" {o:<12}{cosine(v, space.of(o)):.3f}")пратката и поръчката: 0.962пратката и плащането: 0.000най-близо до пратката: поръчката 0.962 доставката 0.707 адреса 0.667 три 0.667 стоката 0.667„пратката“ и „поръчката“ са 0,962: почти една посока. С „плащането“ са 0,000. Най-близките думи до „пратката“ са все за доставка. Има и „три“: в малък текст случайните съседи също дърпат.
Пробвай
Предвиди изхода
Какъв е косинусът на [1, 1, 0] и [1, 0, 0]?
print(f"{cosine([1, 1, 0], [1, 0, 0]):.2f}")Скаларното произведение е 1. Дължините са √2 и 1, а 1 / √2 = 0,7071, закръглено 0,71. Ъгълът между двата вектора е 45°: делят една ос, а първият има още една.
Виж го
Търсене по смисъл
Сега въпросът и всеки текст от базата стават вектори, а отговорът е текстът с най-голям косинус:
space = Space(TEXT, STOP, 8)for q in QUESTIONS: v = space.of_text(q) print(q) best = DOCS[0] top = -1.0 for topic, text in DOCS: c = cosine(v, space.of_text(text)) print(f" {topic:<10}{c:.3f}") if c > top: top = c best = (topic, text) print(f" → {best[0]}: {best[1]}")кога ще дойде пратката ми доставка 0.655 връщане 0.349 плащане 0.000 → доставка: доставката отнема до два днибанката отказа картата ми доставка 0.000 връщане 0.000 плащане 1.000 → плащане: платете с карта или с наложен платежПървият въпрос намира „доставка“ с 0,655, а „връщане“ е 0,349. Вторият намира „плащане“ с 1,000: „банката“ е в едно изречение с „карта“, точно като текста за плащането. Нито един от двата въпроса няма обща дума с отговора си.
Ето първия въпрос като посоки. Векторите са в 8 измерения, а картинката е равна, затова пази само ъгъла на всеки текст до въпроса:
# косинусът на ъгъла: 1 е същата посока, 0 нищо общоdef cosine(a, b): dot = sum(x * y for x, y in zip(a, b)) na = sum(x * x for x in a) nb = sum(y * y for y in b) if na == 0 or nb == 0: return 0.0 return dot / (math.sqrt(na) * math.sqrt(nb))
space = Space(TEXT, STOP, 8)for q in QUESTIONS: v = space.of_text(q) print(q) best = DOCS[0] top = -1.0 for topic, text in DOCS: c = cosine(v, space.of_text(text)) print(f" {topic:<10}{c:.3f}") if c > top: top = c best = (topic, text) print(f" → {best[0]}: {best[1]}")Стъпка по стъпка, с думи
- Въпросът „кога ще дойде пратката ми“ става вектор: [2, 0, 0, 0, 1, 1, 0, 0]. Картинката пази само ъгъла до него.
- „плащане“: косинус 0,000, ъгъл 90°. Нямат нито една обща ос: стрелките са под прав ъгъл.
- „връщане“: косинус 0,349, ъгъл 70°. Имат общи оси, но е далече.
- „доставка“: косинус 0,655, ъгъл 49°. Най-малкият ъгъл.
- Най-близо е „доставка“: „доставката отнема до два дни“. Без нито една обща дума с въпроса.
Пробвай
Попълни
Косинусът дели скаларното произведение на двете дължини. Какво стои на мястото на ___?
return dot / (math.sqrt(na) * math.sqrt())nb, сборът от квадратите на втория вектор. Коренът му е дължината на втория вектор, както коренът от na е дължината на първия.
Пробвай
Намери грешката
Колега смята близостта по-бързо, но получава 11 за първия въпрос и текста за доставка, а близостта трябва да е между 0 и 1. На кой ред е грешката?
Избери реда с грешката.
Връща скаларното произведение без деление на дължините. Тогава дълъг текст с много думи има голям вектор и събира големи числа, дори да сочи встрани. Трябва dot / (√na · √nb): така остава само посоката.
Идеята
Вниманието като идея
В урока „карта“ има един вектор, където и да е. А в „платете с карта“ и „картата на офисите“ думата значи различни неща: банкова карта и карта на града.
Истинските модели решават това с внимание (attention). Всеки токен гледа другите токени в текста и смесва в своя вектор малко от техните, повече от тези, които са важни за него. „карта“ до „платете“ взема от „платете“, а до „офисите“ от „офисите“, и двата вектора стават различни. Това се повтаря през много слоеве, и така векторът на всеки токен носи смисъла му в точно това изречение. Векторът на целия текст се смята от тях.
Тук вниманието е само идея: как точно се смята, не е част от курса.
Идеята
Кога боли
-
Малкият модел знае думи, не смисъл. Клиент иска да върне стоката, а търсенето го праща на доставката:
space = Space(TEXT, STOP, 8)for q in ["искам да върна стоката"]:v = space.of_text(q)print(q)best = DOCS[0]top = -1.0for topic, text in DOCS:c = cosine(v, space.of_text(text))print(f" {topic:<10}{c:.3f}")if c > top:top = cbest = (topic, text)print(f" → {best[0]}: {best[1]}")Изход искам да върна стокатадоставка 0.842връщане 0.635плащане 0.000→ доставка: доставката отнема до два дни„върна“ я няма в текста: там са „върнете“, „върнати“, „връщат“ и „връщането“, и за модела това са различни думи, както при речника от урока за токените. Остава „стоката“, а нейните съседи в текста са „доставката“, „дни“ и „пристигне“. Освен това думата не брои собствената си ос, а точно „стоката“ е най-силната ос на текста за връщане. Векторът на въпроса излиза
[0, 0, 1, 0, 1, 1, 0, 0]и сочи към доставката (0,842), а не към връщането (0,635). На английски точно този въпрос намира връщането, но „i want my money back“ отива на плащането: „money“ и „back“ са в едно изречение с „card“. Пропуските на двата езика са различни, защото текстовете са различни.Истинският модел за вектори е учен върху огромно количество текст. Той знае, че „върна“, „върнете“ и „връщане“ са едно и също, и вижда целия въпрос, не само отделните думи. Пак може да сбърка, но много по-рядко. Колко често намира правилния текст, се мери: в модула за търсене по смисъл това е recall@k, делът на въпросите, чийто правилен текст е сред първите k намерени.
-
Близко не значи същото. Думите с обратен смисъл често стоят в едни и същи изречения:
space = Space(TEXT, STOP, 8)for x, y in [["безплатна", "струва"], ["бързо", "закъсня"]]:print(f"{x} и {y}: {cosine(space.of(x), space.of(y)):.3f}")print("цвят:", space.of("цвят"))Изход безплатна и струва: 0.894бързо и закъсня: 0.577цвят: [0, 0, 0, 0, 0, 0, 0, 0]„безплатна“ и „струва“ са 0,894, защото и двете са до „доставката“. Истинските модели се справят по-добре, но „може“ и „не може“ пак излизат близо.
-
Непозната дума е нула. „цвят“ го няма в текста и векторът му е само нули. Въпрос само с непознати думи е 0,000 до всичко, но търсенето пак връща някой текст. Затова трябва праг: виж упражнението.
-
Векторите идват от текста. Малък текст дава случайни съседи („три“ до „пратката“). Различни модели дават различни вектори, които не се сравняват помежду си: базата и въпросът трябва да са с един и същ модел.
-
Много текстове, много вектори. За хиляди документи векторите се пазят в база, която търси най-близките бързо: векторните бази. Търсенето по смисъл върху документите на магазина е цял модул по-нататък в курса. Как се пуска модел за вектори на сървър и как се поддържа индексът е работа за курса ZeroToOps.
По-точно, за любопитните
Истинските embeddings. Моделите за вектори приемат текст и връщат стотици или хиляди числа. Викат се през API като другите модели или се пускат локално, най-често от Python. Векторите обикновено са с дължина 1, и тогава косинусът е просто скаларното произведение.
Нулите на осите. В урока думата не брои собствената си ос. Затова думата на ос, която се среща само с думи извън осите, е само нули: „карта“ е ос, но векторът ѝ е [0, 0, 0, 0, 0, 0, 0, 0], и самата дума „карта“ не помага на търсенето. Текстът за плащане се намира заради „наложен“ и „платеж“, които са в едно изречение с „карта“. Ако думата брои и своята ос, тя става най-голямото число във вектора си и дърпа всичко към себе си: тогава „пратката“ и „поръчката“ падат от 0,962 на 0,197. И двата избора са компромис на малкия модел; истинските модели нямат оси, които са думи.
Защо косинус, а не разстояние. Разстоянието между върховете зависи и от дължината, а дължината тук е колко думи има текстът. Косинусът гледа само посоката. При вектори с дължина 1 двете дават една и съща подредба.
Най-близките k. Търсенето в урока е k-NN от урока за най-близките съседи с k = 1: само векторите и мярката за близост са други.
В Python. math.sqrt е коренът, както Math.sqrt в Java и JavaScript. sum в Python смята по-точно от обикновения цикъл, но тук числата са цели и резултатът е същият на четирите езика.
Упражнение
Не знам е отговор
Реши в редактора си.
Ботът не бива да отговаря наслуки. Напиши функция, която връща най-близкия текст само ако косинусът е поне 0,5, а иначе „не знам, пращам на колега“. Пробвай я с двата въпроса от урока и с „искам друг цвят“.
кога ще дойде пратката ми → доставката отнема до два днибанката отказа картата ми → платете с карта или с наложен платежискам друг цвят → не знам, пращам на колегаТретият въпрос е 0,000 до всичко и вместо грешен отговор ботът го праща на човек. Прагът е компромис: висок праг праща на колега и въпроси, на които ботът знае отговора, а нисък пуска грешни отговори.
Още задачи
- Повече оси. Направи пространството с 16 оси. Как се сменят близостите от урока?
- Първите две. Отпечатай двата най-близки текста за всеки въпрос и колко далече е вторият от първия.
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: dataclass, match и шаблони · Какво е машинно учене · Текстът на парчета: токени · Предсказване на следващия токен
- Продължава в: Търсене по смисъл: най-близките k · Нарязване на документи (chunking) · RAG: отговор от документите · Оценяване на RAG: намира ли и стъпва ли
- Задачи по темата: Как работи езиков модел