Chunking — هنر تکه‌تکه کردن متن

قسمت ۵ ۱۸ دقیقه

Chunking — هنر تکه‌تکه کردن متن

تا اینجای سری، یه چیز مهم رو فرض گرفتیم: اینکه مستندات ما به قطعات کوچیک‌تر تقسیم شدن و هر قطعه Embed شده. ولی سؤال کلیدی اینه: چطور مستندات رو تکه‌تکه کنیم؟

این سؤال ساده به نظر میاد ولی یکی از مهم‌ترین تصمیم‌هایی هست که توی طراحی یه سیستم RAG می‌گیری. Chunking بد = نتایج جستجوی بد = جواب‌های بد. ساده‌ست.

چرا Chunking اصلاً لازمه؟

سه دلیل اصلی:

۱. محدودیت Context Window: اگه یه مستند ۵۰ صفحه‌ای رو کامل بدی به LLM، ممکنه توی Context Window جا نشه. حتی اگه جا بشه، مدل اطلاعات وسطی رو نادیده می‌گیره (Lost in the Middle).

۲. دقت جستجو: اگه یه مستند ۵۰ صفحه‌ای رو به عنوان یه واحد Embed کنی، بردارش یه «میانگین» از همه محتوا میشه. این بردار به هیچ‌کدوم از موضوعات خاص مستند خیلی نزدیک نیست. مثل اینه که از یه نفر بپرسی «تخصصت چیه؟» و بگه «همه چیز». کسی که تخصصش رو مشخص بگه، مفیدتره.

۳. صرفه‌جویی در هزینه: وقتی فقط قطعات مرتبط رو بدی به LLM (نه کل مستند)، توکن‌های کمتری مصرف می‌کنی و هزینه API کمتر میشه.

Chunking مثل خرد کردن مواد غذایی قبل از پخته. اگه یه سیب‌زمینی کامل بندازی توی قابلمه، هم دیر پخته میشه، هم یکدست نمی‌پزه. ولی اگه خردش کنی، هم سریع‌تر پخته میشه، هم بهتر مزه‌دار میشه. البته خیلی ریز هم بکنی، له میشه و فرمش رو از دست میده.

تریدآف اصلی: اندازه Chunk

اندازه Chunk یکی از مهم‌ترین پارامترهاست. بذار دو حالت افراطی رو ببینیم:

Chunk خیلی کوچیک (مثلاً ۵۰ کلمه):

  • مزیت: جستجو خیلی دقیقه. بردار هر Chunk یه موضوع خاص رو نشون میده.
  • مشکل: از دست رفتن زمینه (Context Loss). یه جمله تنها ممکنه بدون پاراگراف قبلیش بی‌معنی باشه.
  • مشکل: تعداد Chunkها زیاد میشه. جستجو کندتر و حافظه بیشتر.

Chunk خیلی بزرگ (مثلاً ۲۰۰۰ کلمه):

  • مزیت: زمینه حفظ میشه. هر Chunk اطلاعات کاملی داره.
  • مشکل: دقت جستجو پایین میاد. بردار یه Chunk بزرگ، میانگین موضوعات مختلفه.
  • مشکل: توکن‌های بیشتری مصرف میشه.

قانون سرانگشتی: برای اکثر کاربردها، Chunkهای ۲۰۰ تا ۵۰۰ کلمه (حدود ۲۵۰ تا ۱۰۰۰ توکن) نقطه شروع خوبیه. ولی حتماً باید با داده‌های واقعیت تست کنی و تنظیم کنی.

استراتژی‌های Chunking

حالا بریم سراغ استراتژی‌های مختلف. از ساده شروع می‌کنیم و میریم سمت پیچیده‌تر:

۱. Fixed-Size Chunking (اندازه ثابت)

ساده‌ترین روش: متن رو به قطعات با تعداد کاراکتر یا توکن ثابت تقسیم کن.

def fixed_size_chunk(text, chunk_size=500, overlap=50):
    """تقسیم متن به قطعات با اندازه ثابت"""
    chunks = []
    start = 0
    while start < len(text):
        end = start + chunk_size
        chunk = text[start:end]
        chunks.append(chunk)
        start = end - overlap  # Overlap برای حفظ زمینه
    return chunks

text = "متن طولانی شما..."
chunks = fixed_size_chunk(text, chunk_size=500, overlap=50)

مزایا: ساده، سریع، قابل پیش‌بینی.

معایب: ممکنه وسط جمله یا حتی وسط کلمه ببره! به ساختار متن توجهی نداره.

۲. Sentence-Based Chunking (بر اساس جمله)

متن رو به جملات تقسیم کن و بعد جملات رو گروه‌بندی کن تا به اندازه مطلوب برسه.

import nltk
nltk.download('punkt')

def sentence_chunk(text, max_chunk_size=500):
    """تقسیم بر اساس جملات"""
    sentences = nltk.sent_tokenize(text)
    chunks = []
    current_chunk = []
    current_size = 0
    
    for sentence in sentences:
        if current_size + len(sentence) > max_chunk_size and current_chunk:
            chunks.append(" ".join(current_chunk))
            current_chunk = []
            current_size = 0
        current_chunk.append(sentence)
        current_size += len(sentence)
    
    if current_chunk:
        chunks.append(" ".join(current_chunk))
    
    return chunks

مزایا: هیچ‌وقت وسط جمله نمی‌بره. طبیعی‌تره.

معایب: اندازه Chunkها متغیره. برای زبان‌هایی مثل فارسی، sentence tokenizer ممکنه خوب کار نکنه.

۳. Recursive Character Splitting (تقسیم بازگشتی)

این روش رو LangChain محبوب کرده. ایده‌ش اینه: اول سعی کن با جداکننده‌های بزرگ (مثل دو خط جدید) تقسیم کنی. اگه قطعات هنوز بزرگن، با جداکننده‌های کوچیک‌تر (یک خط جدید، نقطه، فاصله) ادامه بده.

from langchain.text_splitter import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,
    chunk_overlap=50,
    separators=["\n\n", "\n", ".", "؟", "!", " ", ""]
)

chunks = splitter.split_text(long_text)

مزایا: ساختار متن رو تا حد ممکن حفظ می‌کنه. خیلی انعطاف‌پذیره.

معایب: نیاز به تنظیم separators برای هر زبان. برای فارسی باید «؟» و «!» و «۔» هم اضافه بشه.

توصیه: RecursiveCharacterTextSplitter بهترین نقطه شروع برای اکثر پروژه‌هاست. ساده‌ست، عملکرد خوبی داره و تنظیمش آسونه.

۴. Semantic Chunking (تقسیم معنایی)

پیشرفته‌ترین روش: از Embedding استفاده کن تا بفهمی کجا موضوع عوض میشه. هر جا شباهت معنایی بین جملات متوالی افت کنه، اونجا تقسیم کن.

from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai import OpenAIEmbeddings

embeddings = OpenAIEmbeddings()

chunker = SemanticChunker(
    embeddings,
    breakpoint_threshold_type="percentile",
    breakpoint_threshold_amount=95
)

chunks = chunker.split_text(long_text)

مزایا: هر Chunk یه موضوع منسجم داره. بهترین کیفیت جستجو.

معایب: کنده (نیاز به Embedding هر جمله). هزینه‌بره. پیچیده‌تره.

Overlap — چرا مهمه؟

Overlap یعنی مقداری از انتهای هر Chunk با ابتدای Chunk بعدی مشترک باشه. چرا؟

فرض کن یه پاراگراف اینطوری تقسیم شده:

Chunk 1: "... مدل GPT-4 قابلیت‌های زیادی داره. یکی از مهم‌ترین‌ها"
Chunk 2: "پردازش تصویره که توی نسخه قبلی نبود. این قابلیت ..."

بدون Overlap، اطلاعات «GPT-4 قابلیت پردازش تصویر داره» توی هیچ‌کدوم از Chunkها کامل نیست. ولی با Overlap:

Chunk 1: "... مدل GPT-4 قابلیت‌های زیادی داره. یکی از مهم‌ترین‌ها پردازش تصویره"
Chunk 2: "یکی از مهم‌ترین‌ها پردازش تصویره که توی نسخه قبلی نبود. این قابلیت ..."

حالا اطلاعات کامل توی حداقل یه Chunk هست.

چقدر Overlap؟ معمولاً ۱۰ تا ۲۰ درصد اندازه Chunk. یعنی اگه Chunk 500 کاراکتره، Overlap 50 تا 100 کاراکتر مناسبه.

Metadata — قدرت مخفی Chunking

هر Chunk باید فقط متن داشته باشه؟ نه! اضافه کردن Metadata به هر Chunk خیلی مهمه:

chunk = {
    "text": "متن قطعه...",
    "metadata": {
        "source": "راهنمای_نصب.pdf",
        "page": 5,
        "section": "پیش‌نیازها",
        "chunk_index": 3,
        "total_chunks": 12,
        "title": "پیش‌نیازهای نصب نرم‌افزار",
        "date": "2024-01-15",
        "language": "fa"
    }
}

Metadata به چند شکل کمک می‌کنه:

  • فیلترینگ: فقط Chunkهای مربوط به یه سند یا دسته خاص رو جستجو کن
  • ارجاع: به کاربر بگو جواب از کدوم صفحه کدوم مستند اومده
  • بازسازی زمینه: اگه Chunk کوچیکه، عنوان بخش رو بهش اضافه کن تا زمینه حفظ بشه
  • رتبه‌بندی: به مستندات جدیدتر اولویت بیشتری بده

نکات ویژه برای متن فارسی

Chunking متن فارسی چالش‌های خاص خودش رو داره:

۱. نیم‌فاصله: فارسی از نیم‌فاصله (Zero-Width Non-Joiner) استفاده می‌کنه. مثلاً «می‌خوام» دو کلمه‌ست ولی یه واحد معنایی. مطمئن شو که Chunker نیم‌فاصله رو به عنوان جداکننده کلمه نشناسه.

۲. علائم نگارشی: فارسی از «؟» و «!» استفاده می‌کنه، نه «?» و «!». Separators رو متناسب تنظیم کن:

persian_separators = [
    "\n\n",          # پاراگراف
    "\n",            # خط جدید
    ".",             # نقطه انگلیسی
    "۔",             # نقطه فارسی/عربی (اگه استفاده بشه)
    "؟",             # علامت سؤال فارسی
    "!",             # علامت تعجب
    "؛",             # نقطه‌ویرگول فارسی
    "،",             # ویرگول فارسی
    " ",             # فاصله
    ""               # آخرین راه‌حل: کاراکتر به کاراکتر
]

۳. جهت متن (RTL): خود Chunking مشکلی با RTL نداره (چون با رشته‌ها کار می‌کنه). ولی موقع نمایش نتایج حواست باشه.

۴. Sentence Tokenizer: NLTK برای فارسی خوب کار نمی‌کنه. از hazm (کتابخانه فارسی NLP) یا تقسیم ساده بر اساس نقطه و علامت سؤال استفاده کن:

import re

def persian_sent_tokenize(text):
    """تقسیم ساده متن فارسی به جملات"""
    sentences = re.split(r'(?<=[.؟!۔])\s+', text)
    return [s.strip() for s in sentences if s.strip()]

۵. اعداد فارسی vs انگلیسی: متن فارسی ممکنه هم «۱۲۳» داشته باشه هم «123». قبل از Chunking، یکدستشون کن.

تکنیک‌های پیشرفته

Parent-Child Chunking:

ایده: Chunkهای کوچیک برای جستجو، ولی Chunkهای بزرگ‌تر برای ارسال به LLM. یعنی وقتی یه Chunk کوچیک مرتبط پیدا شد، Chunk بزرگ‌تری که شامل اونه (Parent) رو بده به مدل. اینطوری هم دقت جستجو بالاست، هم زمینه کافی برای مدل فراهمه.

# Parent Chunk (بزرگ، برای LLM)
parent = "پاراگراف کامل با تمام جزئیات..."

# Child Chunks (کوچیک، برای جستجو)
children = [
    {"text": "جمله اول...", "parent_id": "parent_1"},
    {"text": "جمله دوم...", "parent_id": "parent_1"},
    {"text": "جمله سوم...", "parent_id": "parent_1"},
]

# موقع جستجو: child رو پیدا کن، parent رو بده به LLM

Contextual Chunking:

عنوان بخش یا خلاصه مستند رو به ابتدای هر Chunk اضافه کن:

# بدون Context
chunk = "این قابلیت در نسخه ۳.۲ اضافه شده و ..."

# با Context (بهتر!)
chunk = "[مستند: راهنمای نرم‌افزار | بخش: قابلیت‌های جدید] این قابلیت در نسخه ۳.۲ اضافه شده و ..."

اینطوری حتی اگه Chunk تنها باشه، زمینه‌ش مشخصه.

چک‌لیست عملی Chunking

قبل از اینکه استراتژی Chunking رو نهایی کنی، این چک‌لیست رو بررسی کن:

  • آیا اندازه Chunk متناسب با نوع محتواست؟ (FAQ کوتاه‌تر، مقاله بلندتر)
  • آیا Overlap کافی داری؟ (۱۰-۲۰ درصد)
  • آیا هیچ Chunk وسط جمله بریده شده؟
  • آیا Metadata کافی به هر Chunk اضافه شده؟
  • آیا برای زبان فارسی تنظیمات لازم انجام شده؟
  • آیا با چند نمونه واقعی تست کردی؟
  • آیا Chunkهای خیلی کوچیک (کمتر از ۵۰ کلمه) فیلتر شدن؟

آزمایش و تنظیم

هیچ استراتژی Chunking «بهترین» نیست. همه چیز بسته به داده‌ها و کاربردت فرق می‌کنه. پس:

۱. چند استراتژی رو تست کن: با داده‌های واقعیت، چند استراتژی مختلف رو امتحان کن.

۲. معیار ارزیابی داشته باش: مثلاً ۵۰ تا سؤال و جواب صحیح آماده کن. هر استراتژی رو ارزیابی کن که چند تا جواب درست میده.

۳. بصری بررسی کن: چند Chunk رو دستی بخون. آیا هر Chunk به تنهایی معنادار و قابل فهمه؟

۴. تکرار کن: Chunking یه فرآیند تکراریه. اول یه چیز ساده بذار، بعد بر اساس نتایج بهبودش بده.

اشتباه رایج: خیلی‌ها ساعت‌ها وقت میذارن روی تنظیم LLM (Prompt Engineering, Temperature, ...) ولی Chunking رو سرسری رد می‌کنن. در حالی که اکثر مشکلات RAG ریشه توی Chunking بد داره. اگه اطلاعات مرتبط پیدا نشه، بهترین LLM دنیا هم نمی‌تونه جواب خوب بده.

جمع‌بندی

توی این اپیزود یاد گرفتی:

  • Chunking برای دقت جستجو، محدودیت Context Window و صرفه‌جویی در هزینه ضروریه
  • اندازه Chunk یه تریدآف بین دقت و زمینه‌ست (۲۰۰-۵۰۰ کلمه نقطه شروع خوبیه)
  • چهار استراتژی اصلی: Fixed-Size، Sentence-Based، Recursive و Semantic
  • Overlap برای جلوگیری از قطع اطلاعات مهمه
  • Metadata به هر Chunk اضافه کن (منبع، صفحه، بخش)
  • برای فارسی: حواست به نیم‌فاصله، علائم نگارشی و Sentence Tokenizer باشه
  • حتماً با داده‌های واقعی تست و تنظیم کن

این پنج اپیزود اول، پایه‌های RAG رو برات ساختن. حالا می‌دونی چرا RAG لازمه، چطور کار می‌کنه، Embedding چیه، Vector Database چطور جستجو می‌کنه و متن رو چطور تکه‌تکه کنی. توی اپیزودهای بعدی میریم سراغ پیاده‌سازی عملی و تکنیک‌های پیشرفته‌تر. آماده باش!

نظرات

هنوز نظری ثبت نشده. اولین نفر باشید!

نظر خود را بنویسید