Ембединги: смисълът като вектори
Накратко: Ембедингът е вектор от числа за дума или текст, така че близки по смисъл текстове имат близки вектори, а близостта се мери с косинус на ъгъла.
Проблем
Търсене по думи
Ботът на поддръжката има база с готови отговори: по един текст за доставка, връщане и плащане. Клиентът пише въпрос, а ботът трябва да намери текста, който отговаря.
// текст от базата с отговори: тема и текстrecord Doc(string Topic, string Text);
// базата с отговори на поддръжкатаDoc[] docs =[ new("доставка", "доставката отнема до два дни"), new("връщане", "върнете стоката до 14 дни и ще получите парите"), new("плащане", "платете с карта или с наложен платеж"),];
// въпросите на клиентитеstring[] questions =[ "кога ще дойде пратката ми", "банката отказа картата ми",];Колега търси по общи думи: колкото повече думи от въпроса има в текста, толкова по-добре.
// колко думи от въпроса има и в текстаstatic int Common(string question, string text){ var ws = text.Split(' '); return question.Split(' ').Count(word => ws.Contains(word));}foreach (var q in questions){ Console.WriteLine(q); Doc? best = null; int top = 0; foreach (var d in docs) { int n = Common(q, d.Text); Console.WriteLine($" {d.Topic,-10}{n}"); if (n > top) { top = n; best = d; } } Console.WriteLine(" → " + (best?.Topic ?? "няма"));}кога ще дойде пратката ми доставка 0 връщане 1 плащане 0 → връщанебанката отказа картата ми доставка 0 връщане 0 плащане 0 → нямаПървият въпрос намира „връщане“, и то само заради „ще“. Вторият не намира нищо: „картата“ и „карта“ за програмата са различни думи. Клиентите питат със свои думи, а текстовете в базата са писани с други.
Трябва сравнение на смисъла, не на буквите.
Идеята
Смисълът като посока
Дума се познава по компанията си. „пратката“ и „поръчката“ се срещат с едни и същи думи („куриер“, „пристигне“, „дни“), затова значат нещо близко, макар че буквите им са различни.
Това се записва с числа. Всяка дума става вектор: списък от числа, по едно за всяка ос. Думите с близък смисъл получават близки вектори, тоест стрелки, които сочат почти в една посока. Такъв вектор, който представя смисъла, се казва embedding.
В урока осите са осемте най-чести думи в текста на магазина, а числото за всяка ос е в колко изречения думата е заедно с думата на оста. Истинските модели учат векторите с невронна мрежа и имат стотици или хиляди оси, но идеята е същата: близък смисъл, близка посока.
Виж го
Думите като вектори
Кратките думи като „ще“, „до“ и „на“ са навсякъде и не казват нищо за темата. Те не са оси и не влизат във векторите:
// кратките думи, които са навсякъде и не казват тематаHashSet<string> stop =[ "ви", "е", "до", "с", "на", "от", "и", "да", "се", "ще", "по", "за", "но", "съм", "много", "или", "над", "преди", "беше", "в", "след", "не", "при", "ми", "кога",];Пространството: осите и векторът на дума. Векторът на текст е сборът на векторите на думите му.
// думите като вектори: по едно число за всяка осclass Space{ readonly string[] text; readonly HashSet<string> stop; public readonly List<string> Vocab; public readonly List<string> Axes;
// осите: най-честите думи без кратките public Space(string[] text, HashSet<string> stop, int n) { this.text = text; this.stop = stop; var freq = new OrderedDictionary<string, int>(); foreach (var line in text) foreach (var w in line.Split(' ')) if (!stop.Contains(w)) freq[w] = freq.GetValueOrDefault(w) + 1; Vocab = freq.Keys.ToList(); Axes = Vocab.OrderByDescending(w => freq[w]).Take(n).ToList(); }
// в колко изречения думата е заедно с думата на всяка ос public int[] Of(string word) { var v = new int[Axes.Count]; foreach (var line in text) { var ws = line.Split(' '); if (!ws.Contains(word)) continue; foreach (var w in ws) { int j = Axes.IndexOf(w); if (j >= 0 && w != word) v[j]++; } } return v; }
// векторът на текст: сборът на векторите на думите му public int[] OfText(string s) { var v = new int[Axes.Count]; foreach (var w in s.Split(' ')) { if (stop.Contains(w)) continue; var wv = Of(w); for (int j = 0; j < v.Length; j++) v[j] += wv[j]; } return v; }}var space = new Space(text, stop, 8);Console.WriteLine("оси: " + string.Join(" ", space.Axes));foreach (var word in new[] { "пратката", "поръчката", "плащането" }) Console.WriteLine($"{word,-12}[{string.Join(", ", 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 нищо общоstatic double Cosine(int[] a, int[] b){ double dot = 0, na = 0, nb = 0; for (int 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));}var space = new Space(text, stop, 8);var v = space.Of("пратката");Console.WriteLine($"пратката и поръчката: {Cosine(v, space.Of("поръчката")):F3}");Console.WriteLine($"пратката и плащането: {Cosine(v, space.Of("плащането")):F3}");var others = space.Vocab.Where(o => o != "пратката") .OrderByDescending(o => Cosine(v, space.Of(o))).Take(5);Console.WriteLine("най-близо до пратката:");foreach (var o in others) Console.WriteLine($" {o,-12}{Cosine(v, space.Of(o)):F3}");пратката и поръчката: 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.WriteLine($"{Cosine([1, 1, 0], [1, 0, 0]):F2}");Скаларното произведение е 1. Дължините са √2 и 1, а 1 / √2 = 0,7071, закръглено 0,71. Ъгълът между двата вектора е 45°: делят една ос, а първият има още една.
Виж го
Търсене по смисъл
Сега въпросът и всеки текст от базата стават вектори, а отговорът е текстът с най-голям косинус:
var space = new Space(text, stop, 8);foreach (var q in questions){ var v = space.OfText(q); Console.WriteLine(q); var best = docs[0]; double top = -1; foreach (var d in docs) { double c = Cosine(v, space.OfText(d.Text)); Console.WriteLine($" {d.Topic,-10}{c:F3}"); if (c > top) { top = c; best = d; } } Console.WriteLine($" → {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 нищо общоstatic double Cosine(int[] a, int[] b){ double dot = 0, na = 0, nb = 0; for (int 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));}
var space = new Space(text, stop, 8);foreach (var q in questions){ var v = space.OfText(q); Console.WriteLine(q); var best = docs[0]; double top = -1; foreach (var d in docs) { double c = Cosine(v, space.OfText(d.Text)); Console.WriteLine($" {d.Topic,-10}{c:F3}"); if (c > top) { top = c; best = d; } } Console.WriteLine($" → {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). Всеки токен гледа другите токени в текста и смесва в своя вектор малко от техните, повече от тези, които са важни за него. „карта“ до „платете“ взема от „платете“, а до „офисите“ от „офисите“, и двата вектора стават различни. Това се повтаря през много слоеве, и така векторът на всеки токен носи смисъла му в точно това изречение. Векторът на целия текст се смята от тях.
Тук вниманието е само идея: как точно се смята, не е част от курса.
Идеята
Кога боли
-
Малкият модел знае думи, не смисъл. Клиент иска да върне стоката, а търсенето го праща на доставката:
var space = new Space(text, stop, 8);foreach (var q in new[] { "искам да върна стоката" }){var v = space.OfText(q);Console.WriteLine(q);var best = docs[0];double top = -1;foreach (var d in docs){double c = Cosine(v, space.OfText(d.Text));Console.WriteLine($" {d.Topic,-10}{c:F3}");if (c > top){top = c;best = d;}}Console.WriteLine($" → {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 намерени.
-
Близко не значи същото. Думите с обратен смисъл често стоят в едни и същи изречения:
var space = new Space(text, stop, 8);foreach (var (x, y) in new[] { ("безплатна", "струва"), ("бързо", "закъсня") })Console.WriteLine($"{x} и {y}: {Cosine(space.Of(x), space.Of(y)):F3}");Console.WriteLine($"цвят: [{string.Join(", ", 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: само векторите и мярката за близост са други.
Упражнение
Не знам е отговор
Реши в редактора си.
Ботът не бива да отговаря наслуки. Напиши функция, която връща най-близкия текст само ако косинусът е поне 0,5, а иначе „не знам, пращам на колега“. Пробвай я с двата въпроса от урока и с „искам друг цвят“.
кога ще дойде пратката ми → доставката отнема до два днибанката отказа картата ми → платете с карта или с наложен платежискам друг цвят → не знам, пращам на колегаТретият въпрос е 0,000 до всичко и вместо грешен отговор ботът го праща на човек. Прагът е компромис: висок праг праща на колега и въпроси, на които ботът знае отговора, а нисък пуска грешни отговори.
Още задачи
- Повече оси. Направи пространството с 16 оси. Как се сменят близостите от урока?
- Първите две. Отпечатай двата най-близки текста за всеки въпрос и колко далече е вторият от първия.
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: record, switch и шаблони · Какво е машинно учене · Текстът на парчета: токени · Предсказване на следващия токен
- Продължава в: Търсене по смисъл: най-близките k · Нарязване на документи (chunking) · RAG: отговор от документите · Оценяване на RAG: намира ли и стъпва ли
- Задачи по темата: Как работи езиков модел