مدیریت متن‌های طولانی‌تر از طول کانتکست مدل

مدیریت متن‌های طولانی‌تر از طول کانتکست مدل

embedding
preview

مدیریت متن‌های طولانی‌تر از طول کانتکست مدل #

مدل‌های embedding نمی‌توانند متنی که از طول کانتکست حداکثری مدل بیشتر باشد را پردازش کنند. طول حداکثری بسته به مدل متفاوت است و بر اساس توکن‌ها اندازه‌گیری می‌شود، نه طول رشته. اگر با مفهموم توکن‌ آشنا نیستید، به پست شمردن تعداد توکن‌ها با tiktoken مراجعه کنید.

این notebook نشان می‌دهد که چگونه متن‌هایی که طولانی‌تر از طول کانتکست حداکثری مدل هستند را مدیریت کنید. ما از مدل text-embedding-3-small استفاده خواهیم کرد، اما همین ایده‌ها را می‌توان به مدل‌ها و وظایف دیگر نیز اعمال کرد. برای اطلاعات بیشتر در مورد embeddings، به راهنمای اندپوینت Embeddings مراجعه کنید.

1. طول کانتکست مدل #

ابتدا، مدل را انتخاب کرده و یک تابع برای دریافت embeddings از API تعریف می‌کنیم.

برای اجرای کدهای زیر ابتدا باید یک کلید API را از طریق پنل کاربری گیلاس تولید کنید. برای این کار ابتدا یک حساب کاربری جدید بسازید یا اگر صاحب حساب کاربری هستید وارد پنل کاربری خود شوید. سپس، به صفحه کلید API بروید و با کلیک روی دکمه “ساخت کلید API” یک کلید جدید برای دسترسی به Gilas API بسازید.
 1from openai import OpenAI
 2import os
 3import openai
 4from tenacity import retry, wait_random_exponential, stop_after_attempt, retry_if_not_exception_type
 5
 6client = OpenAI(
 7    api_key=os.environ.get(("GILAS_API_KEY", "<کلید API خود را اینجا بسازید https://dashboard.gilas.io/apiKey>")), 
 8    base_url="https://api.gilas.io/v1/" # Gilas APIs
 9)
10
11EMBEDDING_MODEL = 'text-embedding-3-small'
12EMBEDDING_CTX_LENGTH = 8191
13EMBEDDING_ENCODING = 'cl100k_base'
14
15# let's make sure to not retry on an invalid request, because that is what we want to demonstrate
16@retry(wait=wait_random_exponential(min=1, max=20), stop=stop_after_attempt(6), retry=retry_if_not_exception_type(openai.BadRequestError))
17def get_embedding(text_or_tokens, model=EMBEDDING_MODEL):
18    return client.embeddings.create(input=text_or_tokens, model=model).data[0].embedding

مدل text-embedding-3-small دارای طول کانتکست 8191 توکن با کدگذاری cl100k_base است و می‌توانیم ببینیم که عبور از این حد باعث خطا می‌شود.

1long_text = 'AGI ' * 5000
2try:
3    get_embedding(long_text)
4except openai.BadRequestError as e:
5    print(e)
Error code: 400 - {'error': {'message': "This model's maximum context length is 8192 tokens, however you requested 10001 tokens (10001 in your prompt; 0 for the completion). Please reduce your prompt; or completion length.", 'type': 'invalid_request_error', 'param': None, 'code': None}}

واضح است که باید از بروز اینگونه خطاها جلوگیری کنیم، به خصوص زمانی که با تعداد زیادی embedding سر و کار داریم. با این حال، ممکن است با متن‌هایی مواجه شویم که طولانی‌تر از طول کانتکست حداکثری هستند. در زیر، روش‌های اصلی برای مدیریت این متن‌های طولانی‌تر را توضیح می‌دهیم و دستورالعمل‌هایی برای آنها ارائه می‌دهیم: (1) به سادگی برش دادن متن به طول حداکثری مجاز، و (2) تکه‌بندی متن و embedding هر تکه به صورت جداگانه.

1. برش دادن متن ورودی #

ساده‌ترین راه حل، برش دادن متن ورودی به طول حداکثری مجاز است. از آنجا که طول کانتکست بر اساس توکن‌ها اندازه‌گیری می‌شود، باید قبل از اینکه متن را برش دهیم آن را توکن‌سازی کنیم. API ورودی‌ها را هم به صورت متن و هم به صورت توکن قبول می‌کند، بنابراین تا زمانی که مطمئن باشید که از کدگذاری مناسب استفاده می‌کنید، نیازی به تبدیل توکن‌ها به فرم رشته‌ای نیست. در زیر یک مثال از چنین تابع برش‌دهی آورده شده است.

1import tiktoken
2
3def truncate_text_tokens(text, encoding_name=EMBEDDING_ENCODING, max_tokens=EMBEDDING_CTX_LENGTH):
4    """Truncate a string to have `max_tokens` according to the given encoding."""
5    encoding = tiktoken.get_encoding(encoding_name)
6    return encoding.encode(text)[:max_tokens]

مثال قبلی ما اکنون بدون خطا کار می‌کند.

1truncated = truncate_text_tokens(long_text)
2len(get_embedding(truncated))

2. تکه‌بندی متن ورودی #

اگرچه برش دادن کار می‌کند، اما حذف متن‌های بالقوه مرتبط یک مشکل واضح است. روش دیگر این است که متن ورودی را به تکه‌ها تقسیم کرده و سپس هر تکه را به صورت جداگانه embedding کنیم. سپس می‌توانیم embedding تکه‌ها را به صورت جداگانه استفاده کنیم یا آنها را به نوعی ترکیب کنیم، مانند میانگین‌گیری (وزن‌دهی شده بر اساس اندازه هر تکه).

ما از تابع زیر که یک دنباله را به تکه‌ها تقسیم می‌کند، استفاده خواهیم کرد.

 1from itertools import islice
 2
 3def batched(iterable, n):
 4    """Batch data into tuples of length n. The last batch may be shorter."""
 5    # batched('ABCDEFG', 3) --> ABC DEF G
 6    if n < 1:
 7        raise ValueError('n must be at least one')
 8    it = iter(iterable)
 9    while (batch := tuple(islice(it, n))):
10        yield batch

اکنون یک تابع تعریف می‌کنیم که یک رشته را به توکن‌ها کدگذاری کرده و سپس آن را به تکه‌ها تقسیم می‌کند.

1def chunked_tokens(text, encoding_name, chunk_length):
2    encoding = tiktoken.get_encoding(encoding_name)
3    tokens = encoding.encode(text)
4    chunks_iterator = batched(tokens, chunk_length)
5    yield from chunks_iterator

در نهایت، می‌توانیم یک تابع بنویسیم که درخواست‌های embedding را به صورت ایمن مدیریت کند، حتی زمانی که متن ورودی طولانی‌تر از طول کانتکست حداکثری باشد. فلگ average می‌تواند به True تنظیم شود تا میانگین وزنی embedding تکه‌ها را برگرداند، یا به False تنظیم شود تا فقط لیست embedding تکه‌ها را بدون تغییر برگرداند.

 1import numpy as np
 2
 3def len_safe_get_embedding(text, model=EMBEDDING_MODEL, max_tokens=EMBEDDING_CTX_LENGTH, encoding_name=EMBEDDING_ENCODING, average=True):
 4    chunk_embeddings = []
 5    chunk_lens = []
 6    for chunk in chunked_tokens(text, encoding_name=encoding_name, chunk_length=max_tokens):
 7        chunk_embeddings.append(get_embedding(chunk, model=model))
 8        chunk_lens.append(len(chunk))
 9
10    if average:
11        chunk_embeddings = np.average(chunk_embeddings, axis=0, weights=chunk_lens)
12        chunk_embeddings = chunk_embeddings / np.linalg.norm(chunk_embeddings)  # normalizes length to 1
13        chunk_embeddings = chunk_embeddings.tolist()
14    return chunk_embeddings

بار دیگر، می‌توانیم متن‌های طولانی را مدیریت کنیم.

1average_embedding_vector = len_safe_get_embedding(long_text, average=True)
2chunks_embedding_vectors = len_safe_get_embedding(long_text, average=False)
3
4print(f"Setting average=True gives us a single {len(average_embedding_vector)}-dimensional embedding vector for our long text.")
5print(f"Setting average=False gives us {len(chunks_embedding_vectors)} embedding vectors, one for each of the chunks.")
Setting average=True gives us a single 1536-dimensional embedding vector for our long text.
Setting average=False gives us 2 embedding vectors, one for each of the chunks.

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