Tornando a parlare di text splitting, conosciamo subito due textsplitter fondamentali, sia per apprenderne le caratteristiche che per poter effettuare un confronto tra i loro modi di esperire le proprie funzioni. Per prima cosa però abbiamo bisogno di documenti e per questo attingiamo ad una delle più grandi (se non la più grande) fonte di linguaggio naturale esistente ovvero Wikipedia, l'enciclopedia
libera e collaborativa che contiene moltissimo del sapere umano.
WikipediaLoader e text splitting
Tra i vari document loader a disposizione di Langchain, ve n'è uno di nome WikipediaLoader, specializzato nella raccolta di documenti da questo importante supercontenitore. Sarà proprio lui a recuperare i documenti da splittare e lo farà grazie a queste due librerie:
pip install langchain_community==0.3
pip install wikipedia
La prima sappiamo bene essere un set di strumenti di Langchain, la seconda è una libreria Python che permette di scaricare contenuti da Wikipedia, impostando anche la lingua di predilezione, e che viene utilizzata come motore dal nostro loader. Recuperiamo una pagina in Italiano su Roma, massimo un documento (lo facciamo per semplicità ma, come si può immaginare, la vera preparazione di una RAG verte sulla massima raccolta di testi):
from langchain_community.document_loaders import WikipediaLoader
loader = WikipediaLoader(query="Roma",
lang="it",
load_max_docs=1)
documenti = loader.load()
Ci viene così fornita una lista in documenti che come da nostra richiesta avrà un solo oggetto in posizione 0 i cui primi 200 caratteri.
documenti[0].page_content[:200]
corrispondono a (abbreviandoli un po'):
Roma (AFI: /ˈroma/, ) è un comune italiano di 2 746 609 abitanti, capoluogo..
.
Passiamo ora allo splitting confrontando due dei vari oggetti disponibili in questo ambito:
CharacterTextSplitter, il più semplice, basato per lo più su una suddivisione in chunk di un determinato numero di caratteri e un separatore;RecursiveCharacterTextSplitter, uno dei più utilizzati, che mira a mantenere il significato dei chunk disponendo di una sequenza di separatori di frasi che, utilizzati ricorsivamente, permettono di separare il testo in blocchi più omogenei.
Si ricordi che anche i chunk sono in formato Document come gli oggetti da cui derivano. Vediamo al lavoro entrambi gli splitter.
CharacterTextSplitter
Iniziamo il parsing dell'unico documento che abbiamo a disposizione sebbene il metodo che utilizziamo, create_documents, richieda una lista di page_content:
from langchain.text_splitter import CharacterTextSplitter
char_splitter = CharacterTextSplitter(
separator=".",
chunk_size=100,
chunk_overlap=0,
length_function=len
)
chunks = char_splitter.create_documents([doc.page_content])
print(f"Numero totale di chunk creati: {len(chunks)}")
print("-" * 30)
for i,c in enumerate(chunks):
print(f"#{i} {len(c.page_content)} caratteri")
Con questo semplice esperimento chiediamo di creare chunk, orientativamente di 100 caratteri, prendendo come riferimento il carattere punto (.) come separatore. Il risultato che otteniamo mostra la produzione di 19 chunk le cui dimensioni non sempre rispettano il limite di 100 che abbiamo richiesto:
Numero totale di chunk creati: 19
------------------------------
#0 173 caratteri
#1 134 caratteri
#2 263 caratteri
#3 68 caratteri
#4 50 caratteri
#5 459 caratteri
#6 315 caratteri
#7 63 caratteri
#8 249 caratteri
#9 465 caratteri
#10 99 caratteri
#11 237 caratteri
#12 38 caratteri
#13 77 caratteri
#14 244 caratteri
#15 403 caratteri
#16 251 caratteri
#17 143 caratteri
#18 229 caratteri
Il motivo di questo atteggiamento verte sulla concentrazione che il CharacterTextSplitter fissa principalmente sul separatore piuttosto che sul parametro chunk_size. Ad esempio, il primo chunk consta di 173 caratteri, infatti se chiediamo in quale posizione risiede il primo punto nel documento (con documenti[0].page_content.find('.')) otteniamo esattamente il numero 173.
Per questo con il trattamento dell'overlapping, non sempre preso in considerazione dall'algoritmo ancora a vantaggio del separatore, il CharacterTextSplitter viene solitamente meno consigliato rispetto alla versione recursive che stiamo per esaminare.
RecursiveCharacterTextSplitter
Questo TextSplitter tenta di tutelare maggiormente il contesto usando una sequenza di separatori
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=100,
separators=["\n\n", "\n", " ", "."]
)
chunks = text_splitter.create_documents([doc.page_content])
Ricorsivamente (da qui il nome), prova lo splitting in base al primo separatore. Se ciò che ottiene è al di sotto della lunghezza consentita per i chunk, accetta il token creato altrimenti passa al separatore successivo e fa la stessa valutazione.
Il risultato di ciò sarà una lista di chunk, tutti rispettosi del limite di caratteri massimo imposto, mentre usando il CharacterTextSplitter si otterranno spesso warning volti a notificare l'avvenuto sforamento del parametro chunk_size. Quest'ultimo atteggiamento, in scelte particolarmente sfortunate, porterà alla generazione di chunk eccessivamente sballati rispetto al limite imposto.
Per queste valutazioni sul text splitting, probabilmente il RecursiveCharacterTextSplitter merita di essere incluso nei propri progetti RAG e nelle prossime lezioni vedremo proprio come può essere innestato in tale procedimento.
Se vuoi aggiornamenti su Text splitting al lavoro inserisci la tua email nel box qui sotto: