Към съдържанието
wannacode

Нарязване на документи (chunking)

Накратко: Нарязването (chunking) дели документа на парчета по думи или изречения, със застъпване, така че всяко парче да има свой вектор, а отговорът да не се среже.

Проблем

Цялата страница в един вектор

Магазинът има политика за връщане. Тя е една страница с шест изречения:

# политиката за връщане: шест изречения
POLICY = [
"можете да върнете стоката до 14 дни от доставката",
"стоката трябва да е в оригиналната опаковка",
"куриер ще я вземе от адреса ви безплатно",
"парите се връщат по картата до пет дни",
"стоки с ваш надпис не се връщат",
"за повреда пишете ни до два дни",
]
# въпросът на клиента
QUESTION = "кога ще ми върнат парите по картата"

Клиентът пита „кога ще ми върнат парите по картата“. Отговорът е четвъртото изречение. Ако цялата страница е един текст в индекса, тя е един вектор:

print(f"думи: {len(words)}")
doc = " ".join(POLICY)
print(f"целият документ: {cosine(v, space.of_text(doc)):.3f}")
print(f"изречението с отговора: {cosine(v, space.of_text(POLICY[3])):.3f}")
Изход
думи: 46
целият документ: 0.710
изречението с отговора: 0.874

Цялата страница е 0,710, а изречението с отговора само е 0,874. Векторът на страницата е смес от шест теми: срок, опаковка, куриер, пари, изключения, повреди. Колкото по-дълга е страницата, толкова повече се размива. В база с много документи такава страница губи от кратки текстове, които изобщо не отговарят на въпроса.

А ако все пак бъде намерена, в промпта отиват всичките 46 думи заради 8. Истинските политики са хиляди думи, и три такива страници пълнят прозореца на контекста и сметката.

Идеята

Парчета

Документът се реже на парчета (chunks), преди да влезе в индекса. Всяко парче е отделен запис: своят текст, своят вектор и кой документ е. Търсенето от предишния урок намира парчето, а не цялата страница.

Най-простото рязане е по брой думи: първите 10 думи, следващите 10 и така до края. За векторите в урока ползваме същия модел като в урока за векторите, с две разлики: той се учи и от текста на политиката (иначе думите ѝ са непознати), и има 16 оси вместо 8. Всички парчета са за връщане и с 8 оси се различават слабо.

space = Space(TEXT + POLICY, STOP, 16)
words = " ".join(POLICY).split(" ")
v = space.of_text(QUESTION)

Виж го

Парчета по 10 думи

Функцията реже думите на парчета. Засега застъпването е 0: всяко парче започва там, където свършва предишното.

# парчета по size думи; стъпката е size - overlap
def chunk(words, size, overlap):
pieces = []
step = size - overlap
for start in range(0, len(words), step):
end = min(start + size, len(words))
pieces.append(" ".join(words[start:end]))
if end == len(words):
break
return pieces

Друга функция печата всяко парче с близостта му до въпроса и кое е най-близко:

# всяко парче с близостта му; най-близкото накрая
def show(space, v, pieces):
best, top = 0, -1.0
for i, p in enumerate(pieces):
c = cosine(v, space.of_text(p))
print(f"{i + 1} {c:.3f} {p}")
if c > top:
best, top = i, c
print("→", best + 1)
show(space, v, chunk(words, 10, 0))
Изход
1 0.652 можете да върнете стоката до 14 дни от доставката стоката
2 0.174 трябва да е в оригиналната опаковка куриер ще я вземе
3 0.815 от адреса ви безплатно парите се връщат по картата до
4 0.745 пет дни стоки с ваш надпис не се връщат за
5 0.632 повреда пишете ни до два дни
→ 3

Най-близко е парче 3 (0,815). То свършва на „по картата до“, а „пет дни“ е в парче 4. Моделът ще прочете „парите се връщат по картата до“ и няма да знае колко. Срезът е паднал точно в средата на отговора.

Идеята

Застъпване

Срезът е сляп: не знае къде свършва изречението. Поправката е застъпване (overlap): всяко парче започва малко преди края на предишното. С парчета по 10 думи и застъпване 4 всяко парче започва 6 думи след предишното, а последните му 4 думи са първите 4 на следващото.

Така краищата се повтарят и изречение, срязано в едно парче, е цяло в съседното. Цената е повече парчета и повторени думи.

Виж го

Със застъпване 4

show(space, v, chunk(words, 10, 4))
Изход
1 0.652 можете да върнете стоката до 14 дни от доставката стоката
2 0.436 дни от доставката стоката трябва да е в оригиналната опаковка
3 0.147 е в оригиналната опаковка куриер ще я вземе от адреса
4 0.531 я вземе от адреса ви безплатно парите се връщат по
5 0.874 парите се връщат по картата до пет дни стоки с
6 0.745 пет дни стоки с ваш надпис не се връщат за
7 0.732 не се връщат за повреда пишете ни до два дни
→ 5

Сега най-близко е парче 5 (0,874) и то държи цялото изречение: „парите се връщат по картата до пет дни“. Парчетата са 7 вместо 5.

# парчета по size думи; стъпката е size - overlap
def chunk(words, size, overlap):
pieces = []
step = size - overlap
for start in range(0, len(words), step):
end = min(start + size, len(words))
pieces.append(" ".join(words[start:end]))
if end == len(words):
break
return pieces
v = space.of_text(QUESTION)
show(space, v, chunk(words, 10, 0))
Стъпка по стъпка, с думи
  1. Политиката за връщане: 46 думи. В оранжево е изречението с отговора: „парите се връщат по картата до пет дни“.
  2. Парчета по 10 думи без застъпване: 5 парчета, едно след друго.
  3. Близостта на всяко парче до въпроса. Най-близко е парче 3 (0,815).
  4. Но парче 3 свършва на „по картата до“: „пет дни“ е в следващото парче. Отговорът е срязан.
  5. Застъпване 4: всяко парче започва 6 думи след предишното и краищата се повтарят. 7 парчета.
  6. Най-близко е парче 5 (0,874) и то държи цялото изречение.
  7. Без застъпване отговорът е срязан между две парчета, а със застъпване едно парче го държи цял. Цената: 7 парчета вместо 5 и повторени думи.

Пробвай

Предвиди изхода

Изречение от 8 думи на парчета по 4 със застъпване 1. Колко парчета излизат?

ws = "доставката до офис на куриер струва 3 евро".split(" ")
print(len(chunk(ws, 4, 1)))
Отговор

Пробвай

Попълни

С колко думи по-късно започва всяко следващо парче? Какво стои на мястото на ___?

# парчета по size думи; стъпката е size - overlap
def chunk(words, size, overlap):
    pieces = []
    step = size - 
    for start in range(0, len(words), step):
        end = min(start + size, len(words))
        pieces.append(" ".join(words[start:end]))
        if end == len(words):
            break
    return pieces

Пробвай

Намери грешката

Колега реже със застъпване 4, но част от политиката не е в нито едно парче. На кой ред е грешката?

Избери реда с грешката.

Идеята

По изречения

Брой думи е грубо. Документите имат естествени граници: изречения, абзаци, заглавия. Ако парчето е цяло изречение, отговорът не се срязва:

show(space, v, POLICY)
Изход
1 0.670 можете да върнете стоката до 14 дни от доставката
2 0.312 стоката трябва да е в оригиналната опаковка
3 0.031 куриер ще я вземе от адреса ви безплатно
4 0.874 парите се връщат по картата до пет дни
5 0.837 стоки с ваш надпис не се връщат
6 0.632 за повреда пишете ни до два дни
→ 4

Изречението с отговора печели (0,874). Но виж изречение 3: „куриер ще я вземе от адреса ви безплатно“, 0,031. „я“ е стоката от предишното изречение. Само по себе си парчето не казва нито какво взема куриерът, нито че е за връщане. Ако клиентът пита „идва ли куриер за връщането“, това парче губи част от смисъла си.

Затова истинските системи пазят връзката: слагат заглавието на документа и на раздела пред всяко парче, добавят по едно изречение от съседите или режат по абзаци. И пазят към всяко парче откъде е: документ, раздел, място. По това отговорът цитира източника.

Идеята

Кога боли

  • Твърде малки парчета губят смисъла: „до 14 дни“ без думата „връщане“ не казва за какво са 14-те дни.
  • Твърде големи парчета се размиват като цялата страница и пълнят промпта.
  • Застъпването носи повторения. Парчета 5 и 6 имат едни и същи думи. Ако и двете са сред първите k, в промпта влиза повторен текст. Съседни парчета от един документ се сливат, преди да отидат при модела.
  • Таблици, списъци и код не се режат по брой думи. Срязаният ред от таблица с цени е безсмислен: такива документи се режат по структурата си.
  • Смениш ли рязането, сменяш индекса. Всички парчета се режат и смятат наново.

Няма размер, който е верен за всички документи. Размерът и застъпването се избират с тестов набор от въпроси, както при k в предишния урок: виж оценяването.

По-точно, за любопитните

Размерът в истинските системи. Парчетата се мерят в токени, не в думи: често са от 200 до 800 токена със застъпване от 10 до 20% (примерни числа). Колко токена е един текст, е в урока за токените. Тук парчетата са по 10 думи, а изреченията са почти толкова дълги, затова и застъпването е голямо.

Кога застъпването пази отговора цял. Изречение от L думи е цяло в някое парче, ако L ≤ overlap + 1. С застъпване 4 това са изречения до 5 думи. По-дълго изречение е цяло само ако някой срез падне точно преди него: в упражнението се вижда, че това зависи от местата на срезовете.

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

В Python. words[start:start + size] не пада, когато краят е след последната дума: просто връща по-малко. range(0, len(words), step) дава началата на всички парчета наведнъж.

Упражнение

Проверка на рязането

Реши в редактора си.

Напиши проверка: за парчета по 10 думи и застъпване от 0 до 4 намери най-близкото парче до въпроса и отпечатай колко са парчетата, близостта и дали парчето държи цялото изречение с отговора.

Изход
застъпване 0: 5 парчета, 0.815, отговорът е цял: не
застъпване 1: 5 парчета, 0.745, отговорът е цял: не
застъпване 2: 6 парчета, 0.874, отговорът е цял: да
застъпване 3: 7 парчета, 0.892, отговорът е цял: не
застъпване 4: 7 парчета, 0.874, отговорът е цял: да

Застъпване 2 и 4 държат отговора цял, а 1 и 3 не. При застъпване 3 близостта е най-висока (0,892), но парчето свършва на „до пет“ и думата „дни“ е отрязана. По-голяма близост не значи цял отговор. Дали отговорът е цял, зависи от това къде падат срезовете, затова такава проверка се пуска при всяка смяна на рязането.

Още задачи

  • Изречения до 16 думи. Слепвай цели изречения в парче, докато не минат 16 думи. Кое парче е най-близо?
  • Без повторения. Ако две от първите k парчета се застъпват, слей ги в едно.

Тест

Провери се

1. Защо дълъг документ не се слага в индекса като един вектор?
2. Парчета по 10 думи със застъпване 4. На колко думи една от друга започват?
3. Какво се губи, когато всяко изречение е отделно парче?
4. Застъпването е 4, а най-близките k = 3 парчета са 5, 6 и 7. Какъв е проблемът?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·