LeRobot documentation
تصحيح مسار معالجك
تصحيح مسار معالجك
قد تكون خطوط معالجة البيانات معقدة، لا سيما عند ربط عدة خطوات تحويل. على عكس استدعاءات الدوال البسيطة، تفتقر خطوط المعالجة إلى إمكانية المراقبة الطبيعية، فلا يمكنك بسهولة رؤية ما يحدث بين كل خطوة وأخرى أو أين تحدث الأخطاء. توفر هذه الدليل أدوات وتقنيات تصحيح الأخطاء مصممة خصيصًا لمعالجة هذه التحديات ومساعدتك على فهم تدفق البيانات عبر خطوط المعالجة الخاصة بك.
سنستكشف ثلاث طرق تصحيح مكمّلة لبعضها: hooks للمراقبة أثناء التشغيل، وstep-through debugging للفحص التفصيلي، وfeature validation لكشف عدم التطابق الهيكلي. يخدم كل منها غرضًا مختلفًا، ومعًا توفر رؤية شاملة لسلوك خط أنابيبك.
فهم الخطافات
الدوال الوسيطة (Hooks) هي دوال تُستدعى في نقاط محددة أثناء تنفيذ خط المعالجة (Pipeline). توفر طريقة لفحص البيانات أو مراقبتها أو تعديلها دون تغيير كود خط المعالجة الخاص بك. فكّر فيها على أنها "مستمعو أحداث" لخط المعالجة الخاص بك.
ما هو الخطاف (Hook)؟
الدالة الملتحقة (Hook) هي دالة استدعاء (Callback) تُستدعى تلقائيًا في لحظات محددة أثناء تنفيذ خط الأنابيب (Pipeline). ينبع هذا المفهوم من البرمجة القائمة على الأحداث، فتخيل أنك تستطيع “التلحُّق” بتدفق تنفيذ خط الأنابيب لمراقبة ما يحدث أو الاستجابة له.
تخيّل الخطافات (Hooks) على أنها نقاط تفتيش تُدرج في خط أنابيبك. في كل مرة يصل فيها خط الأنابيب إلى إحدى هذه النقاط، يتوقف مؤقتًا لاستدعاء دالة الخطاف الخاصة بك، مما يمنحك فرصة لفحص الحالة الحالية، وتسجيل المعلومات، والتحقق من صحة البيانات.
الدالة الوسيطة (Hook) ليست سوى دالة تقبل معاملين:
step_idx: int- فهرس خطوة المعالجة الحالية (0، 1، 2، إلخ)transition: EnvTransition- انتقال البيانات في تلك النقطة من خط الأنابيب
الجمال في الخطافات (Hooks) يكمن في طبيعتها غير الغازية: يمكنك إضافة منطق للمراقبة أو التحقق أو التصحيح دون تغيير سطر واحد من كود خط الأنابيب الخاص بك. يبقى خط الأنابيب نظيفًا ومركّزًا على منطقه الأساسي، بينما تتولى الخطافات معالجة القضايا الشاملة مثل التسجيل والمراقبة والتصحيح.
خطافات قبل التنفيذ مقابل خطافات بعد التنفيذ
يدعم خط الأنابيب نوعين من الخطافات:
- خطافات قبل التنفيذ (
register_before_step_hook) - تُستدعى قبل تنفيذ كل خطوة - بعد الخطافات (
register_after_step_hook) - تُستدعى بعد اكتمال كل خطوة
def before_hook(step_idx: int, transition: EnvTransition):
"""Called before step processes the transition."""
print(f"About to execute step {step_idx}")
# Useful for: logging, validation, setup
def after_hook(step_idx: int, transition: EnvTransition):
"""Called after step has processed the transition."""
print(f"Completed step {step_idx}")
# Useful for: monitoring results, cleanup, debugging
processor.register_before_step_hook(before_hook)
processor.register_after_step_hook(after_hook)تنفيذ خطاف كشف NaN
إليك مثالًا عمليًا على خطاف (hook) يكشف عن قيم NaN:
def check_nans(step_idx: int, transition: EnvTransition):
"""Check for NaN values in observations."""
obs = transition.get(TransitionKey.OBSERVATION)
if obs:
for key, value in obs.items():
if isinstance(value, torch.Tensor) and torch.isnan(value).any():
print(f"NaN detected in {key} at step {step_idx}")
# Register the hook to run after each step
processor.register_after_step_hook(check_nans)
# Process your data - the hook will be called automatically
output = processor(input_data)
# Remove the hook when done debugging
processor.unregister_after_step_hook(check_nans)كيف تعمل الخطافات داخلياً
يساعدك فهم الآلية الداخلية على استخدام الخطافات (Hooks) بشكل أكثر فعالية. يحافظ خط الأنابيب (Pipeline) على قائمتين منفصلتين: واحدة لخطافات ما قبل الخطوة وأخرى لخطافات ما بعد الخطوة. عند تسجيل خطاف، يُضاف ببساطة إلى القائمة المناسبة.
أثناء التنفيذ، يتبع خط الأنابيب تسلسلاً صارماً: ففي كل خطوة معالجة، يستدعي أولاً جميع الخطافات المسبقة (before-hooks) بترتيب تسجيلها، ثم ينفذ تحويل الخطوة الفعلي، وأخيراً يستدعي جميع الخطافات اللاحقة (after-hooks) بترتيب تسجيلها. وهذا يُنشئ بنية متوقعة تشبه الساندويتش حول كل خطوة.
الاستنتاج الأساسي هو أن الخطافات لا تُغيّر منطق خط المعالجة الأساسي، بل هي إضافية بحتة. تُنسّق طريقة _forward في خط المعالجة هذا التناغم بين الخطافات وخطوات المعالجة، مما يضمن تنفيذ كودك الخاص بالتصحيح أو المراقبة في اللحظات المناسبة تمامًا دون التدخل في تدفق البيانات الرئيسي.
إليك نظرة مبسطة على كيفية تنفيذ خط الأنابيب للتحويلات:
class DataProcessorPipeline:
def __init__(self):
self.steps = [...]
self.before_step_hooks = [] # List of before hooks
self.after_step_hooks = [] # List of after hooks
def _forward(self, transition):
"""Internal method that processes the transition through all steps."""
for step_idx, processor_step in enumerate(self.steps):
# 1. Call all BEFORE hooks
for hook in self.before_step_hooks:
hook(step_idx, transition)
# 2. Execute the actual processing step
transition = processor_step(transition)
# 3. Call all AFTER hooks
for hook in self.after_step_hooks:
hook(step_idx, transition)
return transition
def register_before_step_hook(self, hook_fn):
self.before_step_hooks.append(hook_fn)
def register_after_step_hook(self, hook_fn):
self.after_step_hooks.append(hook_fn)تدفق التنفيذ
يبدو تدفق التنفيذ على النحو التالي:
Input → Before Hook → Step 0 → After Hook → Before Hook → Step 1 → After Hook → ... → Outputعلى سبيل المثال، مع 3 خطوات ونوعَي الخطافين:
def timing_before(step_idx, transition):
print(f"⏱️ Starting step {step_idx}")
def validation_after(step_idx, transition):
print(f"✅ Completed step {step_idx}")
processor.register_before_step_hook(timing_before)
processor.register_after_step_hook(validation_after)
# This will output:
# ⏱️ Starting step 0
# ✅ Completed step 0
# ⏱️ Starting step 1
# ✅ Completed step 1
# ⏱️ Starting step 2
# ✅ Completed step 2عدة خطافات
يمكنك تسجيل عدة خطافات (hooks) من النوع نفسه - يتم تنفيذها بالترتيب الذي سُجِّلت فيه:
def log_shapes(step_idx: int, transition: EnvTransition):
obs = transition.get(TransitionKey.OBSERVATION)
if obs:
print(f"Step {step_idx} observation shapes:")
for key, value in obs.items():
if isinstance(value, torch.Tensor):
print(f" {key}: {value.shape}")
processor.register_after_step_hook(check_nans) # Executes first
processor.register_after_step_hook(log_shapes) # Executes second
# Both hooks will be called after each step in registration order
output = processor(input_data)في حين أن الخطافات (Hooks) ممتازة لمراقبة مشكلات محددة (مثل اكتشاف قيم NaN) أو جمع المقاييس أثناء تنفيذ خط الأنابيب بشكل طبيعي، إلا أنك قد تحتاج في بعض الأحيان إلى الغوص في التفاصيل بشكل أعمق. عندما ترغب في فهم ما يحدث بالضبط في كل خطوة أو تصحيح منطق التحويل المعقد، يوفر التصحيح خطوة بخطوة (Step-through debugging) الفحص التفصيلي الذي تحتاجه.
التصحيح خطوة بخطوة
يشبه التصحيح خطوة بخطوة وجود إعادة تشغيل بطيئة لخط أنابيبك. فبدلاً من مشاهدة بياناتك وهي تتحول في ومضة سريعة من المدخل إلى المخرج، يمكنك الإيقاف مؤقتًا وفحص ما يحدث بعد كل خطوة فردية.
تكتسب هذه الطريقة قيمة خاصة عندما تحاول فهم خط أنابيب معقد، أو تصحيح سلوك غير متوقع، أو التحقق من أن كل تحويل يعمل كما هو متوقع. على عكس الخطافات (Hooks) التي تكون رائعة للمراقبة الآلية، يمنحك التصحيح خطوة بخطوة تحكماً يدوياً وتفاعلياً في عملية الفحص.
تُعدّ طريقة step_through() مولّدًا يُنتج حالة الانتقال بعد كل خطوة معالجة، مما يتيح لك فحص النتائج الوسيطة. فكّر فيها على أنها إنشاء سلسلة من اللقطات لبياناتك أثناء مرورها عبر خط الأنابيب—حيث تُظهر لك كل لقطة بالضبط شكل بياناتك بعد تطبيق تحويل إضافي واحد.
كيف يعمل التتبع خطوة بخطوة
يُغيّر أسلوب step_through() جذريًا طريقة تنفيذ خط الأنابيب. فبدلًا من تشغيل جميع الخطوات بالتتابع وإرجاع النتيجة النهائية فقط، يُحوّل خط الأنابيب إلى مُتسلسِل (iterator) يُنتج النتائج الوسيطة.
إليك ما يحدث داخليًا: تبدأ الطريقة بتحويل بيانات الإدخال الخاصة بك إلى صيغة الانتقال الداخلية للخط، ثم تُنتج هذا الحالة الأولية. بعد ذلك، تطبّق خطوة المعالجة الأولى وتُنتج النتيجة. ثم تطبّق الخطوة الثانية على تلك النتيجة وتُنتج مرة أخرى، وهكذا. كل yield يمنحك لقطة كاملة للانتقال في تلك النقطة.
يتمتع نمط المولّد هذا بقوة كبيرة لأنه كسول—فخط الأنابيب لا يحسب الخطوة التالية إلا عند طلبك لها. وهذا يعني أنه يمكنك التوقف في أي نقطة، وفحص الحالة الحالية بدقة، واتخاذ قرار بشأن المتابعة. فأنت لا تُجبر على تشغيل خط الأنابيب بأكمله فقط لتصحيح خطأ في خطوة واحدة معيّنة.
بدلاً من تشغيل خط الأنابيب بأكمله ورؤية النتيجة النهائية فقط، يوقف step_through() العملية بعد كل خطوة ويمنحك الانتقال الوسيط:
# This creates a generator that yields intermediate states.
# The first yield is the input itself; yield i (i >= 1) is the output of step i - 1.
for i, intermediate_result in enumerate(processor.step_through(input_data)):
print("=== Input ===" if i == 0 else f"=== After step {i - 1} ===")
# Inspect the observation at this stage
obs = intermediate_result.get(TransitionKey.OBSERVATION)
if obs:
for key, value in obs.items():
if isinstance(value, torch.Tensor):
print(f"{key}: shape={value.shape}, dtype={value.dtype}")التصحيح التفاعلي باستخدام نقاط التوقف
يمكنك إضافة نقاط توقف في حلقة التتبع خطوة بخطوة للتحقق التفاعلي من الأخطاء:
# Step through the pipeline with debugging
for i, intermediate in enumerate(processor.step_through(data)):
print("Input" if i == 0 else f"After step {i - 1}: {processor.steps[i - 1].__class__.__name__}")
# Set a breakpoint to inspect the current state
breakpoint() # Debugger will pause here
# You can now inspect 'intermediate' in the debugger:
# - Check tensor shapes and values
# - Verify expected transformations
# - Look for unexpected changesأثناء جلسة مُصحِّح الأخطاء، يمكنك:
- افحص
intermediate[TransitionKey.OBSERVATION]لرؤية بيانات الملاحظة - تحقق من
intermediate[TransitionKey.ACTION]لمعرفة تحويلات الإجراءات - افحص أي جزء من الانتقال لفهم ما يفعله كل خطوة
يُعدّ التصحيح خطوة بخطوة مثاليًا لفهم تحويلات البيانات، لكن ماذا عن البنية لتلك البيانات؟ في حين تساعدك الخطافات والتصحيح خطوة بخطوة في تصحيح سلوك وقت التشغيل، تحتاج أيضًا إلى التأكد من أن خط أنابيبك ينتج بيانات بالصيغة التي تتوقعها المكونات اللاحقة. وهنا يأتي دور التحقق من عقد الميزات.
التحقق من عقود الميزات
تعريف عقود الميزات يحدد هيكل البيانات الذي تتوقعه خط أنابيبك كمدخلات والذي ينتجه كمخرجات. يساعد التحقق من صحة هذه العقود في اكتشاف عدم التطابق مبكراً.
فهم عقود الميزات
يحتوي كل خطوة معالج على طريقة transform_features() تصف كيفية تغييرها لبنية البيانات:
# Get the expected output features from your pipeline
initial_features = {
PipelineFeatureType.OBSERVATION: {
"observation.state": PolicyFeature(type=FeatureType.STATE, shape=(7,)),
"observation.image": PolicyFeature(type=FeatureType.VISUAL, shape=(3, 224, 224))
},
PipelineFeatureType.ACTION: {
"action": PolicyFeature(type=FeatureType.ACTION, shape=(4,))
}
}
# Check what your pipeline will output
output_features = processor.transform_features(initial_features)
print("Input features:")
for feature_type, features in initial_features.items():
print(f" {feature_type}:")
for key, feature in features.items():
print(f" {key}: {feature.type.value}, shape={feature.shape}")
print("\nOutput features:")
for feature_type, features in output_features.items():
print(f" {feature_type}:")
for key, feature in features.items():
print(f" {key}: {feature.type.value}, shape={feature.shape}")التحقق من الميزات المتوقعة
تحقق من أن خط أنابيبك ينتج الميزات التي تتوقعها:
# Define what features you expect the pipeline to produce
expected_keys = ["observation.state", "observation.image", "action"]
print("Validating feature contract...")
for expected_key in expected_keys:
found = False
for feature_type, features in output_features.items():
if expected_key in features:
feature = features[expected_key]
print(f"✅ {expected_key}: {feature.type.value}, shape={feature.shape}")
found = True
break
if not found:
print(f"❌ Missing expected feature: {expected_key}")يساعد هذا التحقق في ضمان عمل خط أنابيبك بشكل صحيح مع المكونات التالية التي تتوقع هياكل بيانات محددة.
ملخص
الآن، بما أنك تفهم أساليب التصحيح الثلاثة، يمكنك التعامل مع أي مشكلة في خط الأنابيب بشكل منهجي:
- الخطافات - لمراقبة والتحقق في وقت التشغيل دون تعديل كود خط الأنابيب
- التنفيذ خطوة بخطوة - لفحص الحالات الوسيطة وفهم التحويلات
- التحقق من الميزات - لضمان استيفاء عقود بنية البيانات
متى تستخدم كل نهج:
- ابدأ بـالتصحيح خطوة بخطوة عندما تحتاج إلى فهم ما يفعله خط أنابيبك أو عندما يحدث شيء غير متوقع
- أضِف خطافات للمراقبة المستمرة أثناء التطوير والإنتاج لكشف المشكلات تلقائيًا
- استخدم التحقق من الميزات قبل النشر للتأكد من أن خط الأنابيب يعمل مع المكونات التالية
تعمل هذه الأدوات الثلاث معًا لتوفير المراقبة الكاملة التي تفتقر إليها خطوط الأنابيب المعقدة بشكل طبيعي. فمع وجود الخطافات (Hooks) التي ترصد المشكلات، وميزة التتبع خطوة بخطوة التي تساعدك على فهم السلوك، والتحقق من الميزات الذي يضمن التوافق، ستتمكن من تصحيح أخطاء أي خط أنابيب بثقة وكفاءة.
Update on GitHub