هذا ليس دليلًا رسميًا على النمط لبيتورش. يلخص هذا المستند أفضل الممارسات من أكثر من عام من الخبرة مع التعلم العميق باستخدام إطار Pytorch. لاحظ أن التعلم الذي نشاركه يأتي في الغالب من منظور البحث والبدء.
هذا مشروع مفتوح والمتعاونون الآخرون موضع ترحيب كبير لتحرير الوثيقة وتحسينها.
ستجد ثلاثة أجزاء رئيسية من هذا المستند. أولاً ، خلاصة سريعة لأفضل الممارسات في بيثون ، تليها بعض النصائح والتوصيات باستخدام Pytorch. أخيرًا ، نشارك بعض الأفكار والخبرات باستخدام أطر أخرى ساعدتنا بشكل عام على تحسين سير العمل لدينا.
تحديث 20.12.2020
تحديث 30.4.2019
بعد الكثير من التعليقات الإيجابية ، أضفت أيضًا ملخصًا لبنات بناء شائعة الاستخدام من مشاريعنا على خفيفة: ستجد لبنات بناء (الالتحاق الذاتي ، وفقدان الإدراك الحسي باستخدام VGG ، والتطبيع الطيفي ، وتطبيع مثيل تكيفي ، ...)
قصاصات الكود للخسائر والطبقات وغيرها من اللبنات الأساسية
من تجربتنا ، نوصي باستخدام Python 3.6+ بسبب الميزات التالية التي أصبحت مفيدة للغاية للرمز النظيف والبسيط:
نحاول متابعة google styleguide لبيثون. يرجى الرجوع إلى دليل النمط الموثق جيدًا على رمز Python الذي توفره Google.
نحن نوفر هنا ملخصًا للقواعد الأكثر استخدامًا:
من 3.16.4
| يكتب | مؤتمر | مثال |
|---|---|---|
| الحزم والوحدات النمطية | lower_with_under | من prefech_generator استيراد الخلفية realerator |
| فصول | الكلام | فئة dataloader |
| الثوابت | CAPS_WITH_UNDER | batch_size = 16 |
| حالات | lower_with_under | مجموعة البيانات = مجموعة البيانات |
| الطرق والوظائف | lower_with_under () | def visualize_tensor () |
| المتغيرات | lower_with_under | background_color = 'Blue' |
بشكل عام ، نوصي باستخدام IDE مثل Visual Studio Code أو Pycharm. بينما يوفر VS Code تسليط الضوء على بناء الجملة والإكمال التلقائي في محرر محرر خفيف نسبيًا ، يحتوي Pycharm على الكثير من الميزات المتقدمة للعمل مع المجموعات البعيدة. أصبحت VS Code قوية للغاية مع نظامها الإيكولوجي سريع النمو.
تأكد من تثبيت الامتدادات التالية:
إذا تم إعدادك بشكل صحيح ، فهذا يتيح لك القيام بما يلي:
بشكل عام ، نوصي باستخدام أجهزة الكمبيوتر المحمولة Jupyter للاستكشاف/ اللعب الأولي مع نماذج ورمز جديد. يجب استخدام البرامج النصية Python بمجرد أن ترغب في تدريب النموذج على مجموعة بيانات أكبر حيث يكون الاستنساخ أيضًا أكثر أهمية.
سير العمل الموصى به:
| كمبيوتر محمول Jupyter | البرامج النصية Python |
|---|---|
| + الاستكشاف | + تشغيل وظائف أطول دون انقطاع |
| + تصحيح الأخطاء | + من السهل تتبع التغييرات مع git |
| - يمكن أن يصبح ملفًا ضخمًا | - تصحيح الأخطاء يعني في الغالب إعادة تشغيل البرنامج النصي بأكمله |
| - يمكن مقاطعة (لا تستخدم للتدريب الطويل) | |
| - عرضة للأخطاء وتصبح فوضى |
المكتبات الشائعة الاستخدام:
| اسم | وصف | تستخدم ل |
|---|---|---|
| الشعلة | إطار أساسي للعمل مع الشبكات العصبية | إنشاء التنسور والشبكات وتدريبها باستخدام backprop |
| Torchvision | وحدات رؤية الكمبيوتر Pytorch | معالجة بيانات الصورة ، زيادة المعالجة ، بعد المعالجة |
| وسادة (بيل) | مكتبة التصوير بيثون | تحميل الصور وتخزينها |
| numpy | حزمة للحوسبة العلمية مع بيثون | المعالجة المسبقة للبيانات وبعد المعالجة |
| prefech_generator | مكتبة لمعالجة الخلفية | تحميل الدفعة التالية في الخلفية أثناء الحساب |
| TQDM | شريط التقدم | التقدم أثناء تدريب كل عصر |
| torchinfo | طباعة ملخص نموذج يشبه Keras ل Pytorch | يعرض الشبكة ، فهي معلمات وأحجام في كل طبقة |
| Torch.Utils.tensorboard | Tensorboard داخل Pytorch | تجارب قطع الأشجار وإظهارها في Tensorboard |
لا تضع جميع الطبقات والنماذج في نفس الملف. تتمثل أفضل الممارسات في فصل الشبكات النهائية إلى ملف منفصل ( networks.py ) والحفاظ على الطبقات والخسائر و OPS في الملفات المعنية ( layers.py ، losses.py ، ops.py ). يجب أن يكون النموذج النهائي (الذي يتكون من شبكات واحدة أو متعددة) مرجعًا في ملف يحمل اسمه (على سبيل المثال yolov3.py ، dcgan.py )
الروتين الرئيسي ، يجب أن يستورد البرامج النصية للقطار والاختبار فقط من الملف الذي له اسم النموذج.
نوصي بتقسيم الشبكة إلى القطع الصغيرة القابلة لإعادة الاستخدام. الشبكة عبارة عن عرض nn.module الذي يتكون من عمليات أو غيرها من المواد الواحدة كبنات بناء. وظائف الخسارة هي أيضا nn.module ، وبالتالي ، يمكن دمجها مباشرة في الشبكة.
يجب أن يكون لدى فئة وراثة من nn.module طريقة أمامية تنفذ التمريرة الأمامية للطبقة أو العملية المعنية.
يمكن استخدام nn.module على بيانات الإدخال باستخدام self.net (الإدخال) . يستخدم هذا ببساطة طريقة call () للكائن لتغذية الإدخال من خلال الوحدة النمطية.
output = self . net ( input )استخدم النمط التالي للشبكات البسيطة مع إدخال واحد وإخراج واحد:
class ConvBlock ( nn . Module ):
def __init__ ( self ):
super ( ConvBlock , self ). __init__ ()
self . block = nn . Sequential (
nn . Conv2d (...),
nn . ReLU (),
nn . BatchNorm2d (...)
)
def forward ( self , x ):
return self . block ( x )
class SimpleNetwork ( nn . Module ):
def __init__ ( self , num_resnet_blocks = 6 ):
super ( SimpleNetwork , self ). __init__ ()
# here we add the individual layers
layers = [ ConvBlock (...)]
for i in range ( num_resnet_blocks ):
layers += [ ResBlock (...)]
self . net = nn . Sequential ( * layers )
def forward ( self , x ):
return self . net ( x )لاحظ ما يلي:
class ResnetBlock ( nn . Module ):
def __init__ ( self , dim , padding_type , norm_layer , use_dropout , use_bias ):
super ( ResnetBlock , self ). __init__ ()
self . conv_block = self . build_conv_block (...)
def build_conv_block ( self , ...):
conv_block = []
conv_block += [ nn . Conv2d (...),
norm_layer (...),
nn . ReLU ()]
if use_dropout :
conv_block += [ nn . Dropout (...)]
conv_block += [ nn . Conv2d (...),
norm_layer (...)]
return nn . Sequential ( * conv_block )
def forward ( self , x ):
out = x + self . conv_block ( x )
return outهنا تم تنفيذ اتصال تخطي كتلة Resnet مباشرة في التمريرة الأمامية. يسمح Pytorch بالعمليات الديناميكية أثناء التمريرة الأمامية.
بالنسبة لشبكة تتطلب مخرجات متعددة ، مثل بناء خسارة إدراكية باستخدام شبكة VGG المسبقة ، نستخدم النمط التالي:
class Vgg19 ( nn . Module ):
def __init__ ( self , requires_grad = False ):
super ( Vgg19 , self ). __init__ ()
vgg_pretrained_features = models . vgg19 ( pretrained = True ). features
self . slice1 = torch . nn . Sequential ()
self . slice2 = torch . nn . Sequential ()
self . slice3 = torch . nn . Sequential ()
for x in range ( 7 ):
self . slice1 . add_module ( str ( x ), vgg_pretrained_features [ x ])
for x in range ( 7 , 21 ):
self . slice2 . add_module ( str ( x ), vgg_pretrained_features [ x ])
for x in range ( 21 , 30 ):
self . slice3 . add_module ( str ( x ), vgg_pretrained_features [ x ])
if not requires_grad :
for param in self . parameters ():
param . requires_grad = False
def forward ( self , x ):
h_relu1 = self . slice1 ( x )
h_relu2 = self . slice2 ( h_relu1 )
h_relu3 = self . slice3 ( h_relu2 )
out = [ h_relu1 , h_relu2 , h_relu3 ]
return outلاحظ هنا ما يلي:
حتى إذا كان لدى Pytorch بالفعل الكثير من وظائف الخسارة القياسية ، فقد يكون من الضروري في بعض الأحيان إنشاء وظيفة الخسارة الخاصة بك. لهذا الغرض ، قم بإنشاء losses.py منفصلة nn.Module .
class CustomLoss ( nn . Module ):
def __init__ ( self ):
super ( CustomLoss , self ). __init__ ()
def forward ( self , x , y ):
loss = torch . mean (( x - y ) ** 2 )
return loss يتم توفير مثال كامل في مجلد مثال CIFAR10 من هذا المستودع.
لاحظ أننا استخدمنا الأنماط التالية:
# import statements
import torch
import torch . nn as nn
from torch . utils import data
...
# set flags / seeds
torch . backends . cudnn . benchmark = True
np . random . seed ( 1 )
torch . manual_seed ( 1 )
torch . cuda . manual_seed ( 1 )
...
# Start with main code
if __name__ == '__main__' :
# argparse for additional flags for experiment
parser = argparse . ArgumentParser ( description = "Train a network for ..." )
...
opt = parser . parse_args ()
# add code for datasets (we always use train and validation/ test set)
data_transforms = transforms . Compose ([
transforms . Resize (( opt . img_size , opt . img_size )),
transforms . RandomHorizontalFlip (),
transforms . ToTensor (),
transforms . Normalize (( 0.5 , 0.5 , 0.5 ), ( 0.5 , 0.5 , 0.5 ))
])
train_dataset = datasets . ImageFolder (
root = os . path . join ( opt . path_to_data , "train" ),
transform = data_transforms )
train_data_loader = data . DataLoader ( train_dataset , ...)
test_dataset = datasets . ImageFolder (
root = os . path . join ( opt . path_to_data , "test" ),
transform = data_transforms )
test_data_loader = data . DataLoader ( test_dataset ...)
...
# instantiate network (which has been imported from *networks.py*)
net = MyNetwork (...)
...
# create losses (criterion in pytorch)
criterion_L1 = torch . nn . L1Loss ()
...
# if running on GPU and we want to use cuda move model there
use_cuda = torch . cuda . is_available ()
if use_cuda :
net = net . cuda ()
...
# create optimizers
optim = torch . optim . Adam ( net . parameters (), lr = opt . lr )
...
# load checkpoint if needed/ wanted
start_n_iter = 0
start_epoch = 0
if opt . resume :
ckpt = load_checkpoint ( opt . path_to_checkpoint ) # custom method for loading last checkpoint
net . load_state_dict ( ckpt [ 'net' ])
start_epoch = ckpt [ 'epoch' ]
start_n_iter = ckpt [ 'n_iter' ]
optim . load_state_dict ( ckpt [ 'optim' ])
print ( "last checkpoint restored" )
...
# if we want to run experiment on multiple GPUs we move the models there
net = torch . nn . DataParallel ( net )
...
# typically we use tensorboardX to keep track of experiments
writer = SummaryWriter (...)
# now we start the main loop
n_iter = start_n_iter
for epoch in range ( start_epoch , opt . epochs ):
# set models to train mode
net . train ()
...
# use prefetch_generator and tqdm for iterating through data
pbar = tqdm ( enumerate ( BackgroundGenerator ( train_data_loader , ...)),
total = len ( train_data_loader ))
start_time = time . time ()
# for loop going through dataset
for i , data in pbar :
# data preparation
img , label = data
if use_cuda :
img = img . cuda ()
label = label . cuda ()
...
# It's very good practice to keep track of preparation time and computation time using tqdm to find any issues in your dataloader
prepare_time = start_time - time . time ()
# forward and backward pass
optim . zero_grad ()
...
loss . backward ()
optim . step ()
...
# udpate tensorboardX
writer . add_scalar (..., n_iter )
...
# compute computation time and *compute_efficiency*
process_time = start_time - time . time () - prepare_time
pbar . set_description ( "Compute efficiency: {:.2f}, epoch: {}/{}:" . format (
process_time / ( process_time + prepare_time ), epoch , opt . epochs ))
start_time = time . time ()
# maybe do a test pass every x epochs
if epoch % x == x - 1 :
# bring models to evaluation mode
net . eval ()
...
#do some tests
pbar = tqdm ( enumerate ( BackgroundGenerator ( test_data_loader , ...)),
total = len ( test_data_loader ))
for i , data in pbar :
...
# save checkpoint if needed
...هناك نمطان متميزان في Pytorch لاستخدام وحدات معالجة الرسومات المتعددة للتدريب. من تجربتنا كلا الأنماط صالحة. النتائج الأولى ولكن في رمز أجمل وأقل. والثاني يبدو أن لديه ميزة أداء طفيفة بسبب انخفاض التواصل بين وحدات معالجة الرسومات. لقد طرحت سؤالًا في منتدى Pytorch الرسمي حول النهجين هنا
الأكثر شيوعًا هو تقسيم دفعات جميع الشبكات إلى وحدات معالجة الرسومات الفردية.
وبالتالي ، سيتم تشغيل نموذج يعمل على وحدة معالجة الرسومات 1 بحجم الدُفعة 64 على وحدات معالجة الرسومات 2 مع كل حجم دفعة قدرها 32. يمكن القيام بذلك تلقائيًا عن طريق لف النموذج بواسطة nn.dataparaldal (نموذج) .
هذا النمط أقل استخدامًا. يظهر مستودع ينفذ هذا النهج هنا في تنفيذ PIX2PIXHD بواسطة NVIDIA
يعمل Numpy على وحدة المعالجة المركزية وأبطأ من رمز الشعلة. منذ أن تم تطوير Torch مع تشابه مع Numpy في الاعتبار ، فإن معظم وظائف numpy مدعومة من قبل Pytorch بالفعل.
يجب أن يكون خط أنابيب تحميل البيانات مستقلاً عن رمز التدريب الرئيسي. يستخدم Pytorch عمال الخلفية لتحميل البيانات بشكل أكثر كفاءة ودون إزعاج عملية التدريب الرئيسية.
عادة ما نقوم بتدريب نماذجنا لآلاف الخطوات. لذلك ، يكفي تسجيل فقدان الخسارة والنتائج الأخرى في كل خطوة من الخطوة لتقليل النفقات العامة. على وجه الخصوص ، توفير النتائج الوسيطة لأن الصور يمكن أن تكون مكلفة أثناء التدريب.
من السهل جدًا استخدام وسيطات سطر الأوامر لتعيين المعلمات أثناء تنفيذ التعليمات البرمجية ( حجم الدُفعة ، ومعدل التعلم ، إلخ). هناك طريقة سهلة لتتبع الحجج للتجربة هي مجرد طباعة القاموس المستلم من Parse_args :
...
# saves arguments to config.txt file
opt = parser . parse_args ()
with open ( "config.txt" , "w" ) as f :
f . write ( opt . __str__ ())
...يتتبع Pytorch جميع العمليات التي تتضمن توترات للتمايز التلقائي. استخدام .Detach () لمنع تسجيل العمليات غير الضرورية.
يمكنك طباعة المتغيرات مباشرة ، ومع ذلك يوصى باستخدام متغير . في إصدارات Pytorch السابقة <0.4 ، عليك استخدام .Data للوصول إلى موتر المتغير.
الطريقتين ليست متطابقة كما أشار في إحدى القضايا هنا:
output = self . net . forward ( input )
# they are not equal!
output = self . net ( input )نوصي بتعيين البذور التالية في بداية الكود الخاص بك:
np . random . seed ( 1 )
torch . manual_seed ( 1 )
torch . cuda . manual_seed ( 1 )على NVIDIA GPUS ، يمكنك إضافة السطر التالي في بداية الكود الخاص بنا. سيسمح هذا للواجهة الخلفية CUDA بتحسين الرسم البياني الخاص بك أثناء تنفيذها الأول. ومع ذلك ، كن على دراية أنه إذا قمت بتغيير حجم موتر الإدخال/الإخراج الشبكة ، فسيتم تحسين الرسم البياني في كل مرة يحدث فيها تغيير. هذا يمكن أن يؤدي إلى وقت تشغيل بطيء للغاية وخارج أخطاء الذاكرة. فقط قم بتعيين هذه العلامة إذا كان المدخلات والإخراج لديك دائمًا نفس الشكل. عادة ، يؤدي هذا إلى تحسين حوالي 20 ٪.
torch . backends . cudnn . benchmark = Trueيعتمد ذلك على الجهاز المستخدم ، وخط أنابيب المعالجة المسبقة وحجم الشبكة. الركض على SSD على وحدة معالجة الرسومات 1080TI ، نرى كفاءة حسابية تبلغ حوالي 1.0 وهو سيناريو مثالي. إذا تم استخدام شبكات ضحلة (صغيرة) أو شبكات صلبة بطيئة ، فقد ينخفض الرقم إلى حوالي 0.1-0.2 اعتمادًا على الإعداد الخاص بك.
في Pytorch ، يمكننا تنفيذ أحجام دفعات افتراضية بسهولة شديدة. نحن نمنع فقط المُحسّن من إجراء تحديث للمعلمات وتلخيص التدرجات لدورات Batch_size .
...
# in the main loop
out = net ( input )
loss = criterion ( out , label )
# we just call backward to sum up gradients but don't perform step here
loss . backward ()
total_loss += loss . item () / batch_size
if n_iter % batch_size == batch_size - 1 :
# here we perform out optimization step using a virtual batch size
optim . step ()
optim . zero_grad ()
print ( 'Total loss: ' , total_loss )
total_loss = 0.0
...يمكننا الوصول إلى معدل التعلم مباشرة باستخدام مُحسِّن مثبت كما هو موضح هنا:
...
for param_group in optim . param_groups :
old_lr = param_group [ 'lr' ]
new_lr = old_lr * 0.1
param_group [ 'lr' ] = new_lr
print ( 'Updated lr from {} to {}' . format ( old_lr , new_lr ))
...إذا كنت ترغب في استخدام نموذج مسبق مثل VGG لحساب الخسارة ولكن عدم تدريبه (على سبيل المثال ، فقدان الإدراك في النمط النمط/ الخزان التلقائي) ، يمكنك استخدام النمط التالي:
...
# instantiate the model
pretrained_VGG = VGG19 (...)
# disable gradients (prevent training)
for p in pretrained_VGG . parameters (): # reset requires_grad
p . requires_grad = False
...
# you don't have to use the no_grad() namespace but can just run the model
# no gradients will be computed for the VGG model
out_real = pretrained_VGG ( input_a )
out_fake = pretrained_VGG ( input_b )
loss = any_criterion ( out_real , out_fake )
...تُستخدم هذه الطرق لضبط طبقات مثل BatchNorm2D أو التسرب من التدريب إلى وضع الاستدلال. كل وحدة ترث من nn.module لها سمة تسمى Istraining . .eval () و . train () فقط يضع هذه السمة على true/ false. لمزيد من المعلومات حول كيفية تنفيذ هذه الطريقة ، يرجى إلقاء نظرة على رمز الوحدة النمطية في Pytorch
تأكد من عدم حساب التدرجات وتخزينها أثناء تنفيذ الكود. يمكنك ببساطة استخدام النمط التالي للتأكد من ذلك:
with torch . no_grad ():
# run model here
out_tensor = net ( in_tensor )في Pytorch ، يمكنك تجميد الطبقات. هذا سيمنعهم من التحديث خلال خطوة التحسين.
# you can freeze whole modules using
for p in pretrained_VGG . parameters (): # reset requires_grad
p . requires_grad = Falseمنذ أن تم دمج Pytorch 0.4 * المتغير والموتر . لم يعد علينا إنشاء كائن متغير بشكل صريح.
إصدار C ++ أسرع حوالي 10 ٪
تودو ...
من تجربتنا ، يمكنك الحصول على تسريع حوالي 20 ٪. ولكن في المرة الأولى التي تقوم فيها بتشغيل النموذج الخاص بك ، يستغرق الأمر بعض الوقت لإنشاء الرسم البياني المحسّن. في بعض الحالات (الحلقات في مرور الأمام ، لا يوجد شكل إدخال ثابت ، إذا/آخر في الأمام ، وما إلى ذلك) ، فقد تؤدي هذه العلامة إلى ظهور ذاكرة أو أخطاء أخرى.
تودو ...
إذا تحرر موتر من رسم بياني حساب. يظهر هنا توضيح جميل هنا
يرجى تقديم ملاحظات حول كيفية تحسين دليل النمط هذا! يمكنك فتح مشكلة أو اقتراح تغييرات عن طريق إنشاء طلب سحب.
إذا كنت تحب هذا الريبو ، فلا تنس التحقق من أطر أخرى منا: