Ембединги: смисълът като вектори
Накратко: Ембедингът е вектор от числа за дума или текст, така че близки по смисъл текстове имат близки вектори, а близостта се мери с косинус на ъгъла.
Проблем
Търсене по думи
Ботът на поддръжката има база с готови отговори: по един текст за доставка, връщане и плащане. Клиентът пише въпрос, а ботът трябва да намери текста, който отговаря.
// базата с отговори на поддръжкатаconst DOCS = [ { topic: "доставка", text: "доставката отнема до два дни" }, { topic: "връщане", text: "върнете стоката до 14 дни и ще получите парите" }, { topic: "плащане", text: "платете с карта или с наложен платеж" },];
// въпросите на клиентитеconst QUESTIONS = [ "кога ще дойде пратката ми", "банката отказа картата ми",];Колега търси по общи думи: колкото повече думи от въпроса има в текста, толкова по-добре.
// колко думи от въпроса има и в текстаfunction common(question, text) { const ws = text.split(" "); return question.split(" ").filter((word) => ws.includes(word)).length;}for (const q of QUESTIONS) { console.log(q); let best = null; let top = 0; for (const d of DOCS) { const n = common(q, d.text); console.log(` ${d.topic.padEnd(10)}${n}`); if (n > top) { top = n; best = d; } } console.log(" → " + (best ? best.topic : "няма"));}кога ще дойде пратката ми доставка 0 връщане 1 плащане 0 → връщанебанката отказа картата ми доставка 0 връщане 0 плащане 0 → нямаПървият въпрос намира „връщане“, и то само заради „ще“. Вторият не намира нищо: „картата“ и „карта“ за програмата са различни думи. Клиентите питат със свои думи, а текстовете в базата са писани с други.
Трябва сравнение на смисъла, не на буквите.
Идеята
Смисълът като посока
Дума се познава по компанията си. „пратката“ и „поръчката“ се срещат с едни и същи думи („куриер“, „пристигне“, „дни“), затова значат нещо близко, макар че буквите им са различни.
Това се записва с числа. Всяка дума става вектор: списък от числа, по едно за всяка ос. Думите с близък смисъл получават близки вектори, тоест стрелки, които сочат почти в една посока. Такъв вектор, който представя смисъла, се казва embedding.
В урока осите са осемте най-чести думи в текста на магазина, а числото за всяка ос е в колко изречения думата е заедно с думата на оста. Истинските модели учат векторите с невронна мрежа и имат стотици или хиляди оси, но идеята е същата: близък смисъл, близка посока.
Виж го
Думите като вектори
Кратките думи като „ще“, „до“ и „на“ са навсякъде и не казват нищо за темата. Те не са оси и не влизат във векторите:
// кратките думи, които са навсякъде и не казват тематаconst STOP = new Set([ "ви", "е", "до", "с", "на", "от", "и", "да", "се", "ще", "по", "за", "но", "съм", "много", "или", "над", "преди", "беше", "в", "след", "не", "при", "ми", "кога",]);Пространството: осите и векторът на дума. Векторът на текст е сборът на векторите на думите му.
// думите като вектори: по едно число за всяка осclass Space { // осите: най-честите думи без кратките constructor(text, stop, n) { this.text = text; this.stop = stop; const freq = new Map(); for (const line of text) for (const w of line.split(" ")) if (!stop.has(w)) freq.set(w, (freq.get(w) ?? 0) + 1); this.vocab = [...freq.keys()]; this.axes = [...this.vocab] .sort((a, b) => freq.get(b) - freq.get(a)) .slice(0, n); }
// в колко изречения думата е заедно с думата на всяка ос of(word) { const v = new Array(this.axes.length).fill(0); for (const line of this.text) { const ws = line.split(" "); if (!ws.includes(word)) continue; for (const w of ws) { const j = this.axes.indexOf(w); if (j >= 0 && w !== word) v[j]++; } } return v; }
// векторът на текст: сборът на векторите на думите му ofText(s) { const v = new Array(this.axes.length).fill(0); for (const w of s.split(" ")) { if (this.stop.has(w)) continue; this.of(w).forEach((x, j) => (v[j] += x)); } return v; }}const space = new Space(TEXT, STOP, 8);console.log("оси: " + space.axes.join(" "));for (const word of ["пратката", "поръчката", "плащането"]) console.log(word.padEnd(12) + `[${space.of(word).join(", ")}]`);оси: куриер стоката доставката поръчката дни пристигне пратката картапратката [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 нищо общоfunction cosine(a, b) { let dot = 0; let na = 0; let nb = 0; for (let i = 0; i < a.length; i++) { dot += a[i] * b[i]; na += a[i] * a[i]; nb += b[i] * b[i]; } if (na === 0 || nb === 0) return 0; return dot / (Math.sqrt(na) * Math.sqrt(nb));}const space = new Space(TEXT, STOP, 8);const v = space.of("пратката");console.log(`пратката и поръчката: ${cosine(v, space.of("поръчката")).toFixed(3)}`);console.log(`пратката и плащането: ${cosine(v, space.of("плащането")).toFixed(3)}`);const others = space.vocab .filter((o) => o !== "пратката") .sort((x, y) => cosine(v, space.of(y)) - cosine(v, space.of(x)));console.log("най-близо до пратката:");for (const o of others.slice(0, 5)) console.log(` ${o.padEnd(12)}${cosine(v, space.of(o)).toFixed(3)}`);пратката и поръчката: 0.962пратката и плащането: 0.000най-близо до пратката: поръчката 0.962 доставката 0.707 адреса 0.667 три 0.667 стоката 0.667„пратката“ и „поръчката“ са 0,962: почти една посока. С „плащането“ са 0,000. Най-близките думи до „пратката“ са все за доставка. Има и „три“: в малък текст случайните съседи също дърпат.
Пробвай
Предвиди изхода
Какъв е косинусът на [1, 1, 0] и [1, 0, 0]?
console.log(cosine([1, 1, 0], [1, 0, 0]).toFixed(2));Скаларното произведение е 1. Дължините са √2 и 1, а 1 / √2 = 0,7071, закръглено 0,71. Ъгълът между двата вектора е 45°: делят една ос, а първият има още една.
Виж го
Търсене по смисъл
Сега въпросът и всеки текст от базата стават вектори, а отговорът е текстът с най-голям косинус:
const space = new Space(TEXT, STOP, 8);for (const q of QUESTIONS) { const v = space.ofText(q); console.log(q); let best = DOCS[0]; let top = -1; for (const d of DOCS) { const c = cosine(v, space.ofText(d.text)); console.log(` ${d.topic.padEnd(10)}${c.toFixed(3)}`); if (c > top) { top = c; best = d; } } console.log(` → ${best.topic}: ${best.text}`);}кога ще дойде пратката ми доставка 0.655 връщане 0.349 плащане 0.000 → доставка: доставката отнема до два днибанката отказа картата ми доставка 0.000 връщане 0.000 плащане 1.000 → плащане: платете с карта или с наложен платежПървият въпрос намира „доставка“ с 0,655, а „връщане“ е 0,349. Вторият намира „плащане“ с 1,000: „банката“ е в едно изречение с „карта“, точно като текста за плащането. Нито един от двата въпроса няма обща дума с отговора си.
Ето първия въпрос като посоки. Векторите са в 8 измерения, а картинката е равна, затова пази само ъгъла на всеки текст до въпроса:
// косинусът на ъгъла: 1 е същата посока, 0 нищо общоfunction cosine(a, b) { let dot = 0; let na = 0; let nb = 0; for (let i = 0; i < a.length; i++) { dot += a[i] * b[i]; na += a[i] * a[i]; nb += b[i] * b[i]; } if (na === 0 || nb === 0) return 0; return dot / (Math.sqrt(na) * Math.sqrt(nb));}
const space = new Space(TEXT, STOP, 8);for (const q of QUESTIONS) { const v = space.ofText(q); console.log(q); let best = DOCS[0]; let top = -1; for (const d of DOCS) { const c = cosine(v, space.ofText(d.text)); console.log(` ${d.topic.padEnd(10)}${c.toFixed(3)}`); if (c > top) { top = c; best = d; } } console.log(` → ${best.topic}: ${best.text}`);}Стъпка по стъпка, с думи
- Въпросът „кога ще дойде пратката ми“ става вектор: [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). Всеки токен гледа другите токени в текста и смесва в своя вектор малко от техните, повече от тези, които са важни за него. „карта“ до „платете“ взема от „платете“, а до „офисите“ от „офисите“, и двата вектора стават различни. Това се повтаря през много слоеве, и така векторът на всеки токен носи смисъла му в точно това изречение. Векторът на целия текст се смята от тях.
Тук вниманието е само идея: как точно се смята, не е част от курса.
Идеята
Кога боли
-
Малкият модел знае думи, не смисъл. Клиент иска да върне стоката, а търсенето го праща на доставката:
const space = new Space(TEXT, STOP, 8);for (const q of ["искам да върна стоката"]) {const v = space.ofText(q);console.log(q);let best = DOCS[0];let top = -1;for (const d of DOCS) {const c = cosine(v, space.ofText(d.text));console.log(` ${d.topic.padEnd(10)}${c.toFixed(3)}`);if (c > top) {top = c;best = d;}}console.log(` → ${best.topic}: ${best.text}`);}Изход искам да върна стокатадоставка 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 намерени.
-
Близко не значи същото. Думите с обратен смисъл често стоят в едни и същи изречения:
const space = new Space(TEXT, STOP, 8);for (const [x, y] of [["безплатна", "струва"], ["бързо", "закъсня"]])console.log(`${x} и ${y}: ${cosine(space.of(x), space.of(y)).toFixed(3)}`);console.log(`цвят: [${space.of("цвят").join(", ")}]`);Изход безплатна и струва: 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: само векторите и мярката за близост са други.
Упражнение
Не знам е отговор
Реши в редактора си.
Ботът не бива да отговаря наслуки. Напиши функция, която връща най-близкия текст само ако косинусът е поне 0,5, а иначе „не знам, пращам на колега“. Пробвай я с двата въпроса от урока и с „искам друг цвят“.
кога ще дойде пратката ми → доставката отнема до два днибанката отказа картата ми → платете с карта или с наложен платежискам друг цвят → не знам, пращам на колегаТретият въпрос е 0,000 до всичко и вместо грешен отговор ботът го праща на човек. Прагът е компромис: висок праг праща на колега и въпроси, на които ботът знае отговора, а нисък пуска грешни отговори.
Още задачи
- Повече оси. Направи пространството с 16 оси. Как се сменят близостите от урока?
- Първите две. Отпечатай двата най-близки текста за всеки въпрос и колко далече е вторият от първия.
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Данни, switch и разглобяване · Какво е машинно учене · Текстът на парчета: токени · Предсказване на следващия токен
- Продължава в: Търсене по смисъл: най-близките k · Нарязване на документи (chunking) · RAG: отговор от документите · Оценяване на RAG: намира ли и стъпва ли
- Задачи по темата: Как работи езиков модел