ایجاد مدلهای کوچک کارآمد با Llama 3.2 و هرس کردن (Pruning)

یکی از تکنیکهای اصلی در ایجاد مدلهای سبک تر در دیپ لرنینگ و همچنین ساخت مدلهای زبانی کوچک (Small Language Models)، هرس کردن (Pruning) است. اما برای اجرای موفقیتآمیز فرآیند هرس کردن، شناخت ساختار مدلهای هدف ضروری است.
در این مقاله توضیح داده میشود که چگونه هرس کردن را روی لایههای MLP با ساختار GLU (Gated Linear Units) انجام دهیم؛ یعنی بخش بزرگی از مدلهای فعلی مانند Llama 3.2، Gemma، Mistral و QWen تنها چند نمونه از خانواده مدلهایی هستند که میتوان کد هرس کردن ارائه شده در این مقاله را روی آنها اعمال کرد.
با انجام هرس کردن و احترام به ساختار Gated Linear Unit، کاهش وزن مدل حاصل میشود، در حالی که قابلیت آن برای تولید خروجیهای منسجم و دقت شگفتانگیز در آزمون BoolQ حفظ میشود.
مقدمه
با افزایش اندازه مدلهای زبانی بزرگ برای افزایش قابلیتها، نیاز به کاهش اندازه آنها پدید آمده است. اما طبیعی است که نمیخواهیم قابلیتها را از دست بدهیم. برای دستیابی به مدلهای کوچکتر که قادر به انجام همان وظایف مدلهای بزرگی هستند که بر اساس آنها ساخته شدهاند، معمولاً از تکنیکهای مختلفی مانند کوانتیزاسیون (quantization) و هرس کردن (pruning) برای کاهش اندازه و تقطیر دانش (knowledge distillation) یا انتقال یادگیری (transfer learning) برای بازیابی قابلیتهای از دست رفته با کاهش اندازه استفاده میشود.
هرس کردن احتمالاً کارآمدترین تکنیک در کاهش اندازه مدلها است، اما اعمال آن نیز بسیار پیچیدهتر است، زیرا نه تنها باید تصمیم بگیرید که کدام قسمت از مدل هدف هرس کردن قرار میگیرد، بلکه باید به درستی انتخاب کنید که کدام بخش از آن قسمت را میتوان حذف کرد تا کمترین تأثیر را بر قابلیتهای مدل داشته باشد.
هرس کردن چیست و چه تأثیری بر مدل دارد؟
همانطور که بیان شد، هرس کردن شامل حذف قسمتهایی از مدل است که به اعتقاد ما کمترین سهم را در نتیجه نهایی مدل دارند. با انتخاب دقیق این اجزای کمتر حیاتی، هرس کردن قصد دارد مدلی کارآمدتر با پارامترهای کمتر و الزامات محاسباتی کاهش یافته ایجاد کند، بدون اینکه قابلیتهای اصلی آن را قربانی کند.
مشکل اصلی این است که تصمیم بگیریم کدام قسمتهای مدل را حذف کنیم، زیرا همه بخشهای یک مدل به طور یکسان تأثیرگذار نیستند و هر یک کاربرد متفاوتی دارند.
بهترین راه برای توضیح آن، مشاهده به ساختار مدلی است که میخواهید هرس کنید. در این مثال ما Llama 3.2-1B را بررسی میکنیم.
LlamaForCausalLM( (model): LlamaModel( (embed_tokens): Embedding(128256, 2048) (layers): ModuleList( (0-15): 16 x LlamaDecoderLayer( (self_attn): LlamaSdpaAttention( (q_proj): Linear(in_features=2048, out_features=2048, bias=False) (k_proj): Linear(in_features=2048, out_features=512, bias=False) (v_proj): Linear(in_features=2048, out_features=512, bias=False) (o_proj): Linear(in_features=2048, out_features=2048, bias=False) (rotary_emb): LlamaRotaryEmbedding() ) (mlp): LlamaMLP( (gate_proj): Linear(in_features=2048, out_features=8192, bias=False) (up_proj): Linear(in_features=2048, out_features=8192, bias=False) (down_proj): Linear(in_features=8192, out_features=2048, bias=False) (act_fn): SiLU() ) (input_layernorm): LlamaRMSNorm((2048,), eps=1e-05) (post_attention_layernorm): LlamaRMSNorm((2048,), eps=1e-05) ) ) (norm): LlamaRMSNorm((2048,), eps=1e-05) (rotary_emb): LlamaRotaryEmbedding() ) (lm_head): Linear(in_features=2048, out_features=128256, bias=False) )
با نگاهی به ساختار، سه بلوک بزرگ را میتوان مشاهده کرد که میتوانند هدف هرس کردن قرار گیرند: امبدینگها (embeddings)، مکانیزم توجه-به-خود (self-attention mechanism) و لایههای MLP. برای تصمیمگیری در مورد اینکه کدام یک را هدف قرار دهیم، درک دستاوردهای احتمالی و تأثیرات احتمالی بر مدل اهمیت دارد.
بنابراین اولین قدم این است که ببینیم هر یک از این بخشها چه فضایی را در مدل اشغال میکنند تا ایدهای از سود نهایی در وزن داشته باشیم.
- لایههای امبدینگ (Embeddings) و خروجی (output layer) (embed_tokens, lm_head):
میلیون
میلیون پارامتر.
- مکانیزم توجه (self_attn): این مکانیزم از 16 لایه تشکیل شده است که هر یک شامل چهار لایه پروجکشن دیگر هستند. برای هر لایه، ما
میلیون پارامتر داریم. یعنی
میلیون پارامتر.
- لایههای MLP (mlp): به همین ترتیب از 16 لایه تشکیل شدهاند که هر کدام دارای یک لایه gate_proj، up_proj و down_proj هستند. هر لایه تقریباً
میلیون پارامتر را اشغال میکند. که با ضرب در 16، نشان میدهد که حدود 805 میلیون پارامتر را اشغال میکند.
همانطور که مشاهده میشود، لایههای MLP بیش از 40 درصد از حجم مدل را تشکیل میدهند، بنابراین کاندیدای واضحی برای هرس شدن به نظر میرسند. اما قبل از اتخاذ این تصمیم، درک نقش هر بخش در رفتار مدل مهم است.
لایههای امبدینگ مسئول تبدیل ورودیهای مدل به نمایشهای برداری چگال (dense vector representations) هستند که مدل میتواند به طور مؤثر آنها را پردازش کند. بنابراین، هرس کردن این لایه میتواند منجر به از دست دادن توانایی در جذب صحیح معنای معنایی دادههای ورودی شود. اگر میخواهید یک مدل بسیار خاص ایجاد کنید که تنها از بخش بسیار خاصی از واژگان ورودی خود استفاده میکند، به عنوان مثال یک مدل تحلیل مالی یا پزشکی، میتوانید این لایه را هرس کنید.
مکانیزم توجه به مدل امکان میدهد هنگام پردازش توکنها، روی مرتبطترین بخشهای داده ورودی تمرکز کند. این مکانیزم اهمیت رابطه بین جفت توکنها در دنباله ورودی را محاسبه میکند و به مدل اجازه میدهد تا زمینه را درک کرده و روی مهمترین اطلاعات تمرکز کند. انجام فرآیند هرس کردن در این بخش میتواند توانایی مدل را در وظایفی که به درک گستردهای از زمینه ورودی نیاز دارند، مانند ایجاد خلاصهها یا وظایف ترجمه، کاهش دهد. همچنین تأثیر قابل توجهی بر توانایی تولید متن به صورت منسجم دارد.
لایههای MLP، مکانیزم توجه را همراهی میکنند و با یک سری انبساط و انقباض دادهها، مسئول افزایش توانایی مدل در درک الگوهای پیچیده هستند. این میتواند پاسخ مدل را به دادههای دیده نشده یا وظایف پیشبینی نشده در آموزش محدود کند. یعنی، مدل توانایی تعمیم و ارائه پاسخهای منسجم را از دست میدهد، حتی اگر دادههای ورودی را نشناسد.
هنگامی که تصمیم گرفتید کدام بخش از مدل را هدف قرار دهید، باید تصمیم بگیرید که آیا هرس عرضی (width pruning) یا هرس عمقی (depth pruning) انجام خواهید داد. در اولی، نورونها را حذف میکنیم و در دومی، لایههای کامل را.
همانطور که میبینید، هرس کردن یک مدل فرآیند نسبتاً پیچیدهای است که در آن باید تصمیمات زیادی گرفته شود، و نه تنها باید قابلیتهای مدل حاصل را ارزیابی کنید، بلکه باید توانایی آن برای آموزش را نیز ارزیابی کنید. زیرا این مدلها با هدف آموزش، معمولاً در وظایف خاص، ایجاد میشوند تا در وظایفی که برای آنها ایجاد شدهاند، کارآمدتر و مؤثرتر از مدل پایه مورد استفاده قرار گیرند.
ویژگیهای Gated Linear Units (GLU).
معماری Gated Linear Unit (GLU) به طور گسترده در مدلهای زبان بزرگ مدرن مانند Llama استفاده میشود. GLU یک مکانیسم دروازهای در سطح عنصر معرفی میکند که به مدل اجازه میدهد جریان اطلاعات را به صورت انتخابی فیلتر و کنترل کند. این معماری از لایههای جفت شده، معمولاً gate_proj، up_proj و down_proj (میتوانید این لایهها را در ساختار مدل بالا مشاهده کنید)، تشکیل شده است که با هم برای انجام انبساط و انقباض دادههایی که از طریق آنها عبور میکنند، کار میکنند.
این مکانیزم به مدل امکان میدهد الگوهای پیچیدهتر را پردازش کند و در عین حال کارایی را حفظ کند. با این حال، این بدان معنی است که لایههای درون یک ساختار GLU به شدت به هم وابسته هستند، بنابراین هرس کردن این لایهها نیاز به تحلیل دقیق دارد.
هر عملیاتی در یک لایه (به عنوان مثال، حذف نورونها) باید در لایههای جفت شده مربوطه منعکس شود. برای مثال، اگر یک نورون از gate_proj حذف شود، همان نورون باید از up_proj حذف شود، و همچنین تنظیم اندازه لایه down_proj ضروری است. اما، حتی مهمتر: هنگام محاسبه اهمیت نورونها برای تصمیمگیری در مورد کدامیک را حفظ کنیم، لازم است که جفت نورونها را در هر دو لایه نیز در نظر بگیریم.
اگر تعادل بین این لایهها به هم بخورد، میتواند منجر به عملکرد پایینتر یا حتی خرابی کامل مدل شود، حتی اگر تنها درصد کمی از نورونها حذف شوند.
یک مثال با Llama 3.2.
این مثال در یک مدل لاما بررسی خواهد شد، اما کد با مدلهای Gemma و QWen نیز به خوبی تست شده و کار کرده است.
کد کامل را میتوانید در مخزن گیتهاب زیر بیابید.
اولین کاری که با مدل اصلی در حافظه انجام دادم، اجرای یک فرمان کوتاه و ذخیره نتیجه بود. این کار به من امکان میداد به سادگی، به صورت گرافیکی و سریع بررسی کنم که آیا مدل تولید شده با فرآیند هرس کردن منسجم است یا خیر، یا برعکس، تمام قابلیت تولید متن قابل فهم را از دست داده است. به شما اطمینان میدهم که در اولین تلاش، که ساختار GLU مدل را رعایت نکردم، متن بازگردانده شده به وضوح نشان میداد که فرآیند هرس کردن دارای یک خطای اساسی است.
پرامپت اصلی این است: “پاریس پایتخت …”؛ بیایید پاسخ مدل اصلی و پاسخی که اولین تلاش هرس کردن من (با 20% هرس) به من داد را ببینیم.
مدل پایه:
Paris is the capital of France and one of the most visited cities in the world. It is a city of art, culture, fashion, and gastronomy. The city has a rich history and is home to many famous landmarks, including the E
تلاش اول، با 20% هرس:
Paris is the capital of of France. This is the the the the main the area of. This is the the the the the the the the the the the the the the the the city of the the France of the of the of the of
واضح است که چیزی در اولین تلاش من کار نکرد، ممکن است احمقانه به نظر برسد، اما یک بررسی تجربی از این نوع میتواند به شما کمک کند تا چندین ساعت در زمان خود صرفهجویی کنید.
بیایید ابتدا تابع مسئول محاسبه اهمیت نورونها را ببینیم، که بنابراین تصمیم میگیرد کدام نورونها در مدل باقی بمانند و کدام حذف شوند.
def compute_neuron_pair_importance(gate_weight, up_weight): """ compute neuron pair importance scores (Maximum Absolute Weight) Args: - gate_weight: Weight matrix from the gate_proj layer. - up_weight: Weight matrix from the up_weight layer. Returns: - importance_scores: Importance scores for each neuron pair. """ gate_max_abs = torch.max(gate_weight, dim=1).values + torch.abs(torch.min(gate_weight, dim=1).values) up_max_abs = torch.max(up_weight, dim=1).values + torch.abs(torch.min(up_weight, dim=1).values) importance_scores = gate_max_abs + up_max_abs return importance_scores
این تابع وزنهای یک لایه از نوع gate_proj و یک لایه up_proj را دریافت میکند که همانطور که قبلاً توضیح دادم به صورت جفت کار میکنند و بنابراین وزن نورونها باید به صورت مشترک محاسبه شود.
محاسبه بسیار ساده است؛ مقدار مطلق وزنهای هر نورون محاسبه میشود و هم مقادیر مثبت و هم منفی در نظر گرفته میشوند، زیرا از نظر تئوری، نورونهایی با مقادیر شدیدتر تأثیر بیشتری بر خروجی مدل دارند زیرا مقادیر عبوری از آنها را بیشتر تغییر میدهند.
این تابع برای هر لایه به طور جداگانه محاسبه میکند، اما مقدار کلی را باز میگرداند.
تابع بعدی مسئول ایجاد لایههای جدید و گنجاندن آنها در مدل به جای لایههای اصلی است.
#Prunes a specific percentatge of neurons from the MLP (feed forward layers). def prune_neuron_pairs(mlp, prune_percent): """ Reduces the dimensions of the **gate_proj**,**up_proj**, **down_proj** layers removing the least important neurons. Args: - mlp: Layers to prune. - prune_percent: Percentage of neurons to prune. Returns: - new_gate_proj, new_up_proj, new_down_proj: New pruned layers. - k: New intermediate size. """ # Extract the weights from the MLP layers # these weights are used to calculate each neuron's # importance score in the next step. gate_weight = mlp.gate_proj.weight.data.float() up_weight = mlp.up_proj.weight.data.float() #Compute importance stores. Neurons with higher importance scores # are considered more important and less likely to be pruned. importance_scores = compute_neuron_pair_importance(gate_weight, up_weight) #Store the original number of neurons in the intermediate layer. original_intermediate_size = gate_weight.size(0) #Computes the number of neurons to prune. num_neuron_pairs_to_prune = min(int(prune_percent * original_intermediate_size), original_intermediate_size - 1) #Calculate the number of neurons to keep. The new intermediate size. k = original_intermediate_size - num_neuron_pairs_to_prune #Just check that there is no big error calculating k. We can't prune all the neurons. if k <= 0: raise ValueError(f"Invalid number of neuron pairs to keep: {k}. Adjust the prune_percent.") #Select the neuros to keep, by obtaining the indices to keep. _, indices_to_keep = torch.topk(importance_scores, k, largest=True, sorted=True) indices_to_keep = indices_to_keep.sort().values #create the new layers new_gate_proj = nn.Linear(mlp.gate_proj.in_features, k, bias=False).to(device) new_up_proj = nn.Linear(mlp.up_proj.in_features, k, bias=False).to(device) new_down_proj = nn.Linear(k, mlp.down_proj.out_features, bias=False).to(device) #copy weights to the new layers. new_gate_proj.weight.data = mlp.gate_proj.weight.data[indices_to_keep, :] new_up_proj.weight.data = mlp.up_proj.weight.data[indices_to_keep, :] new_down_proj.weight.data = mlp.down_proj.weight.data[:, indices_to_keep] #return new layers and intermediate size. return new_gate_proj, new_up_proj, new_down_proj, k
این تابع کمی پیچیدهتر است، یک لایه از بلوک MLP و درصد هرس (pruning) را دریافت میکند. با فراخوانی تابع compute_neuron_pair_importance، تصمیم میگیرد کدام نورونها را نگه دارد.
بیایید بخش به بخش بررسی کنیم:
# Extract the weights from the MLP layers # these weights are used to calculate each neuron's # importance score in the next step. gate_weight = mlp.gate_proj.weight.data.float() up_weight = mlp.up_proj.weight.data.float()
با این دو خط بالا، وزنهای لایههای فعلی را بازیابی میکنیم.
importance_scores = compute_neuron_pair_importance(gate_weight, up_weight)
اکنون یک تنسور (tensor) حاوی امتیازات اهمیت محاسبه شده برای هر نورون به دست میآید. این امتیازات نشاندهنده سهم هر نورون در نتیجه نهایی است و مشخص میکند کدام یک را باید حفظ کنیم.
#Store the original number of neurons in the intermediate layer. original_intermediate_size = gate_weight.size(0) #Computes the number of neurons to prune. num_neuron_pairs_to_prune = min(int(prune_percent * original_intermediate_size), original_intermediate_size - 1) #Calculate the number of neurons to keep. The new intermediate size. k = original_intermediate_size - num_neuron_pairs_to_prune
تعداد کل نورونهایی که باید حفظ شوند، با استفاده از درصدی که به عنوان پارامتر دریافت میکنیم، و اندازه اصلی لایهها محاسبه میشود. از آنجا که لایهها هماندازه هستند، نیازی به ذخیره اندازه هر دو نیست. در نهایت، اندازه جدید لایههای میانی محاسبه میشود.
#Select the neuros to keep, by obtaining the indices to keep. _, indices_to_keep = torch.topk(importance_scores, k, largest=True, sorted=True) indices_to_keep = indices_to_keep.sort().values
این خطوط حیاتی هستند، در آنها از torch برای بازیابی نورونهایی با بیشترین اهمیت استفاده میشود، اما همچنین از بیشترین به کمترین اهمیت مرتب میشوند. Torch دادهها را به ترتیب نزولی باز میگرداند و آنها به ترتیب صعودی نیاز دارند که با متد sort به دست میآید.
با استفاده از شاخصهای محاسبه شده، لایههای جدید ایجاد میشوند.
#create the new layers new_gate_proj = nn.Linear(mlp.gate_proj.in_features, k, bias=False).to(device) new_up_proj = nn.Linear(mlp.up_proj.in_features, k, bias=False).to(device) new_down_proj = nn.Linear(k, mlp.down_proj.out_features, bias=False).to(device) #copy weights to the new layers. new_gate_proj.weight.data = mlp.gate_proj.weight.data[indices_to_keep, :] new_up_proj.weight.data = mlp.up_proj.weight.data[indices_to_keep, :] new_down_proj.weight.data = mlp.down_proj.weight.data[:, indices_to_keep]
ابتدا، سه لایه جدید با ابعاد تنظیم شده بر اساس شاخصهای انتخاب شده ایجاد میشوند. در new_gate_proj و new_up_proj، ابعاد ورودی حفظ و ابعاد خروجی کاهش مییابد، در حالی که در new_down_proj برعکس است: ابعاد ورودی تنظیم و ابعاد خروجی حفظ میشوند.
این لایهها بدون وزن اولیه سازی میشوند و در خطوط آخر، وزنهای مرتبط از لایههای اصلی به لایههای جدید منتقل میشوند، تا اطمینان حاصل شود که تنها وزنهای مربوط به نورونهای انتخاب شده حفظ میشوند.
#return new layers and intermediate size. return new_gate_proj, new_up_proj, new_down_proj, k
در نهایت لایههای جدید برگردانده میشوند.
حالا بیایید تابعی را ببینیم که مسئول تکرار روی تمام لایهها و ساخت مدل اصلاح شده است.
#Iterates throught the model layers and applies pruning. def update_model(model, prune_percent): """ It modifies each mlp layer present in model, to retain only the most important neurons. Creating new smaller versions of each layer pruned. Args: - model: Model to prune. - prune_percent: Percentage of neurons to prune. Returns: - model: New pruned model. """ new_intermediate_size = None #loop for each model layer. for idx, layer in enumerate(model.model.layers): #Since each layer is a LlamaDecoderLayer it contains multiple components # Attention, MLP and Layer norms. We're targetting MLP component # by accesing layer.mlp. mlp = layer.mlp #Call the prune_neiron_pairs with the layers and receiving the pruned. new_gate_proj, new_up_proj, new_down_proj, new_size = prune_neuron_pairs(mlp, prune_percent) #Replace the Origiginal Layers with Pruned Layers. mlp.gate_proj = new_gate_proj mlp.up_proj = new_up_proj mlp.down_proj = new_down_proj #new_intermediate_size only needs to be set once if new_intermediate_size is None: new_intermediate_size = new_size #Update the model config file. model.config.intermediate_size = new_intermediate_size return model
میتوان گفت که هیچ راز خاصی ندارد، مدل و درصد هرس را دریافت میکند. روی هر یک از لایههای مدل تکرار میکند و بخش MLP را از هر لایه بازیابی میکند. برای هر یک از لایهها تابع prune_neuron_pairs را فراخوانی میکند و لایههای مدل را با لایههای بازگردانده شده توسط تابع جایگزین میکند.
#Call the prune_neiron_pairs with the layers and receiving the pruned. new_gate_proj, new_up_proj, new_down_proj, new_size = prune_neuron_pairs(mlp, prune_percent) #Replace the Origiginal Layers with Pruned Layers. mlp.gate_proj = new_gate_proj mlp.up_proj = new_up_proj mlp.down_proj = new_down_proj
در نهایت، متغیر new_intermediate_size در فایل پیکربندی مدل نیز اصلاح میشود.
#Update the model config file. model.config.intermediate_size = new_intermediate_size
اگر این فایل را اصلاح نکنیم، مدل پس از ذخیره، چه در Hugging Face و چه به صورت محلی، قابل استفاده نخواهد بود. بسیاری از کتابخانهها، مانند Transformers از Hugging Face، از model.config برای تفسیر معماری مدل استفاده میکنند. اگر با ساختار واقعی مطابقت نداشته باشد، عملیاتی که از طریق آنها انجام میشود، چه fine-tuning و چه inference، ممکن است با شکست مواجه شوند.
بررسی نتایج.
با این کد چندین مدل ایجاد کردهام و آنها را در Hugging Face HUB در دسترس شما قرار دادهام. سه مدل از Llama3.2-1b ایجاد شدهاند که 20%، 40% و 60% از نورونهای لایههای MLP آنها حذف شده است. یک مدل نیز از Gemma-2-2B با 40% هرس ایجاد شده است. میتوانید آنها را دانلود کنید و علاوه بر استفاده، معماری و نحوه تغییر آنها در مقایسه با مدل پایه را مطالعه کنید.
بیایید بررسی کنیم که با اعمال 20% هرس روی مدل Llama3.2-b چه تغییراتی در معماری رخ داده است.
معماری پس از انجام هرس به شرح زیر است:
LlamaForCausalLM( (model): LlamaModel( (embed_tokens): Embedding(128256, 2048) (layers): ModuleList( (0-15): 16 x LlamaDecoderLayer( (self_attn): LlamaSdpaAttention( (q_proj): Linear(in_features=2048, out_features=2048, bias=False) (k_proj): Linear(in_features=2048, out_features=512, bias=False) (v_proj): Linear(in_features=2048, out_features=512, bias=False) (o_proj): Linear(in_features=2048, out_features=2048, bias=False) (rotary_emb): LlamaRotaryEmbedding() ) (mlp): LlamaMLP( (gate_proj): Linear(in_features=2048, out_features=6554, bias=False) (up_proj): Linear(in_features=2048, out_features=6554, bias=False) (down_proj): Linear(in_features=6554, out_features=2048, bias=False) (act_fn): SiLU() ) (input_layernorm): LlamaRMSNorm((2048,), eps=1e-05) (post_attention_layernorm): LlamaRMSNorm((2048,), eps=1e-05) ) ) (norm): LlamaRMSNorm((2048,), eps=1e-05) (rotary_emb): LlamaRotaryEmbedding() ) (lm_head): Linear(in_features=2048, out_features=128256, bias=False) )
ساختار مدل بدون تغییر باقی میماند، به جز اندازه لایههای میانی در لایههای MLP. همانطور که میبینید، لایههای gate_proj و up_proj از 8192 ویژگی به 6554 ویژگی کاهش یافتهاند و لایه down_proj نیز همین تغییر را در ویژگیهای ورودی اعمال کرده است.
این تغییر کاملاً با آنچه کد انجام میدهد، یعنی اصلاح این لایهها و حفظ نورونهایی که برای اجرای کد مهمتر هستند، سازگار است. اگر 20% از 8192 را کم کنیم، 6553.6 به دست میآید، بنابراین تأیید میشود که درصد نورونهای حذف شده صحیح است.
بیایید ببینیم مدل هرس شده با پرامپت آزمایشی چگونه عمل کرده است:
Paris is the capital of France. It is also one of the most beautiful cities in the world. There is so much to see and do in Paris that it is impossible to cover it all in one day. However, there are some things you
این پاسخ، همان پاسخی نیست که از مدل اصلی به دست آمد، اما انسجام خود را حفظ میکند، که نشان میدهد مدل بخش عمدهای از قابلیتهای خود را دست نخورده نگه داشته است، و مهمتر از آن، میتوان با فرآیند تقطیر دانش (Knowledge Distillation) یا تنظیم دقیق (Fine-Tuning) قابلیتهای از دست رفته را بازیابی کرد.
اما جدای از این بررسی، برخی از رتبهبندیهای رایجتر را نیز ارزیابی کردهام. بیایید ببینیم که درجات مختلف هرس بر قابلیتهای مدل چه تأثیری دارد.
همانطور که مشاهده میشود، تأثیر هرس تا حدودی نامتقارن بوده است. وظایف ارزیابی شده با آزمون BoolQ افت قابل توجهی را تجربه نکردهاند، تنها حدود 2 درصد کاهش در مدلی که 35 درصد از وزن خود را از دست داده است.
در مقابل، تأثیر بر آزمون Lambada قابل توجه بوده و دقت بیش از 50 درصد کاهش یافته است. این نشان میدهد که مدل بخش زیادی از توانایی درک خود را حفظ کرده است، اما در آزمونهایی که به تولید متن بازتر نیاز دارند، با مشکل مواجه است.
BoolQ به سادگی متنی را به مدل ارائه میدهد و سؤالی را مطرح میکند که مدل باید به آن با بله/خیر پاسخ دهد. این یک آزمون است که بر اندازهگیری توانایی مدل در درک روابط درون متن ورودی تمرکز دارد.
از سوی دیگر، Lambada از مدل میخواهد کلمه آخر یک پاراگراف را حدس بزند، وظیفهای پیچیده که کلمه نهایی، توانایی مدل را در مدلسازی زبان پیشرفته آزمایش میکند.
نتایج مدل هرس شده 20 درصدی در جدول امتیازات Hugging Face Open LLM شاید حتی شگفتانگیزتر باشد، زیرا هم مدل پایه خود و هم TinyLlama-1.1V-v1.1 که به طور گسترده استفاده میشود را پشت سر میگذارد.
بیایید آن را در این نمودار ببینیم.
با تحلیل این نمودار، میتوانیم نتیجهگیریهای زیر را استخراج کنیم: مدل هرس شده به طور متوسط از مدل پایه بهتر عمل میکند (4.86 در برابر 4.03). این نشان میدهد که فرآیند هرس کردن موفق شده است عملکرد را در حوزههای کلیدی حفظ یا حتی بهبود بخشد در حالی که افزونگی را کاهش داده است.
نقاط قوت مدل هرس شده:
- IFEval: بهبود قابل توجه (19.94 در برابر 14.78) نشان میدهد که هرس کردن بیشبرازش (overfitting) را کاهش داده یا توانایی مدل را در استخراج کارآمد اطلاعات بهبود بخشیده است.
- MUSR: عملکرد بهتر (4.39 در برابر 2.56) نشان میدهد که مدل هرس شده وظایفی را که به استدلال در مورد زمینههای طولانی یا درک روایی نیاز دارند، بهتر مدیریت میکند، که احتمالاً به دلیل وزنهای متمرکزتر است.
نقاط ضعف مدل هرس شده:
- BBH: کاهش در استدلال در شرایط عدم قطعیت (3.19 در برابر 4.37) ممکن است نشاندهنده این باشد که هرس کردن توانایی مدل را برای مدیریت سناریوهای مبهم یا دارای تفسیرهای متعدد کاهش داده است.
- MMLU-PRO: افت در وظایف خاص حوزههای حرفهای (1.36 در برابر 2.26) میتواند به دلیل حذف وزنهای حیاتی برای حفظ دانش جزئی در حوزههای خاص باشد.
بهرهوری انرژی:
مدل هرسشده از نظر انرژی کمی کارآمدتر است (0.4 کیلوگرم در مقابل 0.42 کیلوگرم CO₂)، که با هدف کاهش سربار محاسباتی در عین حفظ عملکرد رقابتی همسو است.
نیاز به مطالعه بسیار کاملتری از نتایج مدل در رتبهبندیهای مختلف وجود دارد، اما با این نتایج واضح به نظر میرسد که ما با مدلی بسیار امیدوارکننده روبرو هستیم که میتواند با یک فرآیند تقطیر دانش (knowledge distillation) یا تنظیم دقیق (fine-tuning) مناسب، بسیار بهبود یابد. مهمترین نکته این است که این نتایج با رویه هرس کردن انجام شده روی لایههای MLP سازگار است.
این نتایج با عملکرد لایههای MLP که هرس شدند، سازگار است.
نتیجهگیری.
فرآیند هرس کردن مدلها موفقیت آمیز بوده است؛ این روش برخورد با لایههای GLU به ما امکان میدهد هرسی را انجام دهیم که بخش بزرگی از قابلیت مدل را حفظ میکند، در حالی که اندازه و مصرف آن را به طور قابل توجهی کاهش میدهد.
لازم به یادآوری است که نتایج آزمونها با مدل هرسشده و بدون گذراندن فرآیند بازیابی قابلیتها، مانند Knowled Distillation یا Fine-Tuning، که معمولاً در مدلهایی که تحت فرآیند هرس قرار گرفتهاند، طبیعی است، به دست آمدهاند.
مسیرهای توسعه.
انواع زیادی از هرس کردن وجود دارد که میتوان امتحان کرد؛ شاید نزدیکترین راه، انجام هرس عمقی (depth pruning) با حذف لایههایی باشد که کمترین سهم را در مدل دارند.
خط تحقیقاتی ضروری دیگر این است که این مدلها را تحت فرآیند تقطیر دانش (Knowledge Distillation) قرار دهیم و ببینیم آیا قابلیتهای یادگیری چیزهای جدید را حفظ میکنند یا خیر و بنابراین میتوانند عملکرد آنها را در آزمونهایی که بیشترین افت عملکرد را داشتهاند، به مدل پایه نزدیکتر کنند.
ایجاد مدلهای سبکتر که حتی میتوانند از مدلهایی که بر اساس آنها ساخته شدهاند پیشی بگیرند، حوزهای است که روز به روز توجه بیشتری را به خود جلب میکند، زیرا بسیاری از شرکتها میخواهند وظایف خاصی را انجام دهند که مدلهای بزرگ میتوانند بدون مشکل و با کیفیت بالا انجام دهند، اما نمیخواهند یا نمیتوانند زیرساختهای مورد نیاز آنها را حفظ کنند.
منبع:
https://martra.uadla.com/creando-small-models-eficientes-con-llama-3-2-y-pruning/
دیدگاهتان را بنویسید