در برنامهنویسی سنتی، خطاها واضحاند؛ برنامه متوقف میشود و گزارش خطا (Stack Trace) محل دقیق مشکل را نشان میدهد. در یادگیری ماشین اما با کابوس «خرابیهای خاموش» روبهرو هستیم: مدلی که ساعتها بدون هیچ اروری آموزش میبیند، اما خروجی آن عملاً بیمصرف است یا در میانهی مسیر با لاسهای NaN یا خطای پر شدن حافظه متوقف میشود.
با توجه به اینکه دسترسی به پردازندههای گرافیکی قدرتمند و سرویسهای ابری خارجی برای توسعهدهندگان در ایران با موانعی مانند پرداخت ارزی و هزینههای گزاف همراه است، هدر رفتن زمان آموزش و منابع پردازشی به پای باگهای ساده هزینهی مضاعفی دارد. یادگیری تکنیکهای دیباگ سریع و پروفایل خطبهخط روی سیستم محلی، این هزینهها را به حداقل میرساند.
در این درس ابزارهای پایهای پایتون و پایتورچ مانند breakpoint شرطی، cProfile، tracemalloc و TensorBoard را بررسی میکنیم تا بتوانید پیش از مصرف زمان و منابع، گلوگاههای پردازشی، عدم تطابق ابعاد و ناپایداریهای عددی را ردیابی و رفع کنید.
بدترین باگهای هوش مصنوعی کرش نمیکنند؛ آنها بیصدا روی دادههای بیارزش آموزش میبینند و یک نمودار لاس (Loss) بسیار زیبا تحویلتان میدهند.
اهداف یادگیری
- استفاده از
breakpoint()شرطی وdebug_printبرای بررسی ابعاد، نوع داده (dtype) و مقادیر NaN تنسورها در حین آموزش - پروفایل کردن حلقههای آموزش با
cProfile،line_profilerوtracemallocجهت یافتن گلوگاهها - شناسایی باگهای رایج هوش مصنوعی: عدم تطابق ابعاد، لاس NaN، نشت داده (Data Leakage) و تنسورها روی دستگاه اشتباه
- راهاندازی TensorBoard برای مصورسازی نمودارهای لاس، هیستوگرام وزنها و توزیع گرادیانها
صورت مسئله
کدهای هوش مصنوعی به شکلی متفاوت از کدهای معمولی خراب میشوند. یک وباپلیکیشن با ردگیری خطا (Stack Trace) کرش میکند؛ اما یک حلقهی آموزش با پیکربندی اشتباه، 8 ساعت اجرا میشود، 200 دلار پای GPU هزینه میتراشد و در نهایت مدلی تحویل میدهد که برای هر ورودی فقط میانگین را پیشبینی میکند. در این فرایند هیچ خطایی صادر نشده است؛ باگ فقط یک تنسور روی دستگاه اشتباه، یک .detach() فراموششده، یا نشت برچسبها به درون ویژگیها بوده است.
به ابزارهای دیباگی نیاز دارید که این خرابیهای خاموش را پیش از هدر رفتن وقت و منابع پردازشیتان شکار کنند.
مفهوم اصلی
دیباگ کدهای هوش مصنوعی در سه سطح انجام میشود:
بیشتر افراد یکراست به سراغ سطح 3 میروند (خیره شدن به TensorBoard)؛ اما 80% از باگهای هوش مصنوعی در سطوح 1 و 2 رخ میدهند.
پیادهسازی گامبهگام
بخش 1: دیباگ با چاپ کردن (بله، واقعاً جواب میدهد)
خیلیها دیباگ با دستور print را دستکم میگیرند؛ اما نباید اینطور باشد. در کدهای تنسوری، یک دستور چاپ هدفمند از خطبهخط جلو رفتن در دیباگر کاربردیتر است؛ چون باید ابعاد، نوع داده و دامنهی مقادیر را یکجا با هم ببینید.
def debug_print(name, tensor):
print(f"{name}: shape={tensor.shape}, dtype={tensor.dtype}, "
f"device={tensor.device}, "
f"min={tensor.min().item():.4f}, max={tensor.max().item():.4f}, "
f"mean={tensor.mean().item():.4f}, "
f"has_nan={tensor.isnan().any().item()}")
این تابع را بعد از هر عملیات مشکوک فراخوانی کنید. وقتی باگ پیدا شد، دستورهای چاپ را پاک کنید. به همین سادگی.
بخش 2: دیباگر پایتون (pdb و breakpoint)
دیباگر داخلی پایتون در پروژههای هوش مصنوعی دستکم گرفته شده است. یک breakpoint() داخل حلقهی آموزش بگذارید و تنسورها را به صورت تعاملی بررسی کنید.
def training_step(model, batch, criterion, optimizer):
inputs, labels = batch
outputs = model(inputs)
loss = criterion(outputs, labels)
if loss.item() > 100 or torch.isnan(loss):
breakpoint()
loss.backward()
optimizer.step()
وقتی کنترل در دیباگر به دست شما میافتد، این دستورها حسابی به کار میآیند:
p outputs.shapeبرای بررسی ابعادp loss.item()برای دیدن مقدار لاسp torch.isnan(outputs).sum()برای شمارش تعداد NaNهاp model.fc1.weight.gradبرای بررسی گرادیانهاcبرای ادامه،qبرای خروج
به این روش «دیباگ شرطی» میگویند؛ یعنی فقط وقتی متوقف میشوید که مشکلی دیده شود. در یک اجرای آموزشی با 10,000 گام، این ویژگی بسیار حیاتی است.
بخش 3: لاگگیری در پایتون (Logging)
وقتی دیباگ از یک بررسی سریع فراتر میرود، دستورهای print را با لاگگیری جایگزین کنید.
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[
logging.FileHandler("training.log"),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
logger.info("Starting training: lr=%.4f, batch_size=%d", lr, batch_size)
logger.warning("Loss spike detected: %.4f at step %d", loss.item(), step)
logger.error("NaN loss at step %d, stopping", step)
لاگگیری برچسب زمانی، سطح اهمیت خطا و خروجی فایلی به شما میدهد. وقتی اجرای آموزش ساعت 3 صبح با شکست مواجه میشود، به فایل لاگ نیاز دارید، نه خروجی ترمینالی که اسکرول شده و از صفحه بیرون رفته است.
بخش 4: زمانسنجی بخشهای مختلف کد
دانستن اینکه زمان صرف چه بخشهایی میشود، نخستین گام در بهینهسازی است.
import time
class Timer:
def __init__(self, name=""):
self.name = name
def __enter__(self):
self.start = time.perf_counter()
return self
def __exit__(self, *args):
elapsed = time.perf_counter() - self.start
print(f"[{self.name}] {elapsed:.4f}s")
with Timer("data loading"):
batch = next(dataloader_iter)
with Timer("forward pass"):
outputs = model(batch)
with Timer("backward pass"):
loss.backward()
یک نتیجهی بسیار رایج: بارگذاری داده 60% از زمان آموزش را میگیرد. راهحل این مشکل تنظیم num_workers > 0 در DataLoader است، نه خرید یک GPU سریعتر.
بخش 5: ابزارهای cProfile و line_profiler
وقتی به ابزاری بیشتر از زمانسنجهای دستی نیاز دارید:
python -m cProfile -s cumtime train.py
این دستور تمام فراخوانیهای توابع را مرتبشده بر اساس مجموع زمان مصرفی نشان میدهد. برای پروفایل کردن خطبهخط:
pip install line_profiler
@profile
def train_step(model, data, target):
output = model(data)
loss = F.cross_entropy(output, target)
loss.backward()
return loss
# اجرا با: kernprof -l -v train.py
بخش 6: پروفایل حافظه
حافظهی CPU با tracemalloc
import tracemalloc
tracemalloc.start()
# کد شما اینجا
model = build_model()
data = load_dataset()
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics("lineno")
for stat in top_stats[:10]:
print(stat)
حافظهی CPU با memory_profiler
pip install memory_profiler
from memory_profiler import profile
@profile
def load_data():
raw = read_csv("data.csv") # watch memory jump here
processed = preprocess(raw) # and here
return processed
با دستور python -m memory_profiler your_script.py اجرا کنید تا مصرف حافظه را خطبهخط ببینید.
حافظهی GPU با PyTorch
import torch
if torch.cuda.is_available():
print(torch.cuda.memory_summary())
print(f"Allocated: {torch.cuda.memory_allocated() / 1e9:.2f} GB")
print(f"Cached: {torch.cuda.memory_reserved() / 1e9:.2f} GB")
وقتی به خطای کمبود حافظه (OOM یا Out of Memory) برمیخورید:
- اندازهی بچ (Batch size) را کم کنید (همیشه اولین گزینهای که باید امتحان کنید)
- از
torch.cuda.empty_cache()برای آزادسازی حافظهی کششده استفاده کنید - برای متغیرهای میانی بزرگ، از
del tensorو بلافاصلهtorch.cuda.empty_cache()استفاده کنید - دقت ترکیبی (Mixed precision یا
torch.cuda.amp) را به کار ببرید تا مصرف حافظه نصف شود - برای مدلهای خیلی عمیق از چکپوینتگذاری گرادیان (Gradient Checkpointing) استفاده کنید
بخش 7: باگهای رایج هوش مصنوعی و روش شناسایی آنها
عدم تطابق ابعاد (Shape Mismatch)
شایعترین باگ ممکن. تنسور ابعاد [batch, features] دارد، در حالی که مدل انتظار [batch, channels, height, width] را دارد.
def check_shapes(model, sample_input):
print(f"Input: {sample_input.shape}")
hooks = []
def make_hook(name):
def hook(module, inp, out):
in_shape = inp[0].shape if isinstance(inp, tuple) else inp.shape
out_shape = out.shape if hasattr(out, "shape") else type(out)
print(f" {name}: {in_shape} -> {out_shape}")
return hook
for name, module in model.named_modules():
hooks.append(module.register_forward_hook(make_hook(name)))
with torch.no_grad():
model(sample_input)
for h in hooks:
h.remove()
این را یک بار با یک بچ نمونه اجرا کنید؛ تمام تغییرات ابعاد را در سراسر مدل ترسیم میکند.
لاس NaN
لاس NaN یعنی در محاسبات مشکلی منفجر شده است. دلایل رایج:
- نرخ یادگیری (Learning rate) خیلی بالا
- تقسیم بر صفر در تابع لاس اختصاصی
- لگاریتم گرفتن از صفر یا عدد منفی
- انفجار گرادیانها در RNNها
def detect_nan(model, loss, step):
if torch.isnan(loss):
print(f"NaN loss at step {step}")
for name, param in model.named_parameters():
if param.grad is not None:
if torch.isnan(param.grad).any():
print(f" NaN gradient in {name}")
if torch.isinf(param.grad).any():
print(f" Inf gradient in {name}")
return True
return False
نشت داده (Data Leakage)
مدل شما به دقت 99% روی دادههای آزمون میرسد. عالی به نظر میرسد، اما این یک باگ است.
def check_data_leakage(train_set, test_set, id_column="id"):
train_ids = set(train_set[id_column].tolist())
test_ids = set(test_set[id_column].tolist())
overlap = train_ids & test_ids
if overlap:
print(f"DATA LEAKAGE: {len(overlap)} samples in both train and test")
return True
return False
نشت زمانی (Temporal leakage) را هم بررسی کنید: استفاده از دادههای آینده برای پیشبینی گذشته. همیشه پیش از تفکیک دادهها، آنها را بر اساس برچسب زمانی مرتب کنید.
دستگاه اشتباه (Wrong Device)
تنسورها روی دستگاههای مختلف (CPU در برابر GPU) باعث خطای زمان اجرا میشوند. اما گاهی یک تنسور بیصدا روی CPU باقی میماند در حالی که بقیه روی GPU هستند، و فقط سرعت آموزش بهشدت افت میکند.
def check_devices(model, *tensors):
model_device = next(model.parameters()).device
print(f"Model device: {model_device}")
for i, t in enumerate(tensors):
if t.device != model_device:
print(f" WARNING: tensor {i} on {t.device}, model on {model_device}")
بخش 8: مبانی TensorBoard
ابزار TensorBoard روند اتفاقات داخل مدل را در طول زمان آموزش به تصویر میکشد.
pip install tensorboard
from torch.utils.tensorboard import SummaryWriter
writer = SummaryWriter("runs/experiment_1")
for step in range(num_steps):
loss = train_step(model, batch)
writer.add_scalar("loss/train", loss.item(), step)
writer.add_scalar("lr", optimizer.param_groups[0]["lr"], step)
if step % 100 == 0:
for name, param in model.named_parameters():
writer.add_histogram(f"weights/{name}", param, step)
if param.grad is not None:
writer.add_histogram(f"grads/{name}", param.grad, step)
writer.close()
اجرای آن:
tensorboard --logdir=runs
به چه نکاتی باید توجه کرد:
- لاس کاهش پیدا نمیکند: نرخ یادگیری بیش از حد پایین است، یا معماری مدل ایراد دارد
- لاس نوسانات شدیدی دارد: نرخ یادگیری بیش از حد بالاست
- لاس به NaN تبدیل میشود: ناپایداری عددی (بخش NaN در بالا را ببینید)
- لاس آموزش کاهش مییابد اما لاس اعتبارسنجی بالا میرود: بیشبرازش (Overfitting)
- هیستوگرام وزنها به صفر میل میکند: محوشدگی گرادیان (Vanishing gradients)
- هیستوگرام گرادیانها منفجر میشود: به برش گرادیان (Gradient clipping) نیاز دارید
بخش 9: دیباگر VS Code
برای دیباگ تعاملی، VS Code را با فایل launch.json پیکربندی کنید:
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Training",
"type": "debugpy",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal",
"justMyCode": false
}
]
}
با کلیک روی حاشیهی کنار شمارهی خطوط، نقطهی توقف (Breakpoint) بگذارید. از پنل Variables برای بررسی ویژگیهای تنسور استفاده کنید. کنسول دیباگ (Debug Console) هم به شما اجازه میدهد هر عبارت دلخواه پایتون را در میانهی اجرا وارد و بررسی کنید.
این قابلیت برای دنبال کردن خطبهخط پایپلاینهای پیشپردازش داده که میخواهید تکتک تبدیلها را ببینید، بسیار کاربردی است.
به کار ببندید
این جریان کاری دیباگی است که بیشتر باگهای هوش مصنوعی را به دام میاندازد:
- پیش از شروع آموزش: تابع
check_shapesرا با یک بچ نمونه اجرا کنید. مطمئن شوید ابعاد ورودی و خروجی با انتظارات همخوانی دارند. - در 10 گام اول: از
debug_printبرای لاس، خروجیها و گرادیانها استفاده کنید. بررسی کنید که هیچ مقداری NaN نباشد و مقادیر در بازهای منطقی قرار داشته باشند. - در طول آموزش: لاس، نرخ یادگیری و نُرمهای گرادیان را لاگ کنید. برای مصورسازی از TensorBoard کمک بگیرید.
- وقتی چیزی خراب شد: در همان نقطهی شکست یک
breakpoint()بگذارید و تنسورها را تعاملی وارسی کنید. - برای کارایی و عملکرد: زمان بارگذاری داده را در مقایسه با گذر رفت و برگشت بسنجید. اگر به پر شدن حافظه (OOM) نزدیک هستید، حافظه را پروفایل کنید.
تحویل پروژه
اسکریپت جعبهابزار دیباگ را اجرا کنید:
python phases/00-setup-and-tooling/12-debugging-and-profiling/code/debug_tools.py
برای دریافت یک پرامپت جهت عیبیابی باگهای ویژهی هوش مصنوعی، فایل outputs/prompt-debug-ai-code.md را ببینید.
تمرینها
- اسکریپت
debug_tools.pyرا اجرا کنید و خروجی هر بخش را به دقت بخوانید. مدل فرضی را طوری دستکاری کنید که یک NaN ایجاد شود (راهنمایی: در گذر پیشرو یا Forward pass یک تقسیم بر صفر قرار دهید) و ببینید شناساگر چطور آن را پیدا میکند. - یک حلقهی آموزش را با
cProfileپروفایل کنید و کندترین تابع را بیابید. - با استفاده از
tracemallocمشخص کنید کدام خط در پایپلاین بارگذاری داده، بیشترین حافظه را تخصیص میدهد. - برای یک اجرای آموزشی ساده، TensorBoard را راهاندازی کنید و ببینید آیا مدل دچار بیشبرازش میشود یا خیر.
- در داخل حلقهی آموزش از
breakpoint()استفاده کنید. ابعاد تنسورها، دستگاه آنها و مقادیر گرادیان را از طریق پرامپت دیباگر بررسی و تمرین کنید.
راهنماییها
تمرین 1. دستور python3 phases/00-setup-and-tooling/12-debugging-and-profiling/code/debug_tools.py را اجرا کنید و خروجی هر تابع دمو را بخوانید. سپس مدل را در demo_nan_detection طوری تغییر دهید که خروجیاش بر صفر تقسیم شود و دوباره آن را اجرا کنید؛ تابع detect_nan باید یک هشدار NaN همراه با شمارهی گام چاپ کند.
تمرین 2. حلقه را با دستور python -m cProfile -s cumtime your_script.py اجرا کنید، یا آن را داخل cProfile.Profile() قرار دهید. سطرهای بالای ستون cumtime را بررسی کنید؛ تابعی که بیشترین زمان را دارد و جزو کدهای خودتان است، کندترین تابع خواهد بود.
تمرین 3. پیش از کدهای بارگذاری داده، tracemalloc.start() و بعد از آن tracemalloc.take_snapshot() را فراخوانی کنید؛ درست مثل demo_memory_tracking. سپس snapshot.statistics("lineno")[:5] را چاپ کنید؛ سطر اول، فایل و شماره خطی را که بیشترین حافظه را تخصیص داده نشان میدهد.
تمرین 4. مقدار لاس آموزش و لاس اعتبارسنجی را در هر دوره (Epoch) داخل یک SummaryWriter ثبت کرده و سپس دستور tensorboard --logdir=runs را اجرا کنید. اگر لاس آموزش همچنان پایین بیاید اما لاس اعتبارسنجی بالا برود، مدل دچار بیشبرازش شده است.
تمرین 5. داخل حلقه و بعد از loss.backward() دستور breakpoint() را قرار دهید و اسکریپت را اجرا کنید. در اعلان (Pdb)، عبارتهای x.shape، x.device و next(model.parameters()).grad را چاپ کنید، سپس c را بزنید تا برنامه به گام بعدی برود.
