استخدام PyTorch لـ Kaggle's Dogs vs. Cats تحدي الجزء الأول (المعالجة المسبقة والتدريب)

للمبتدئين في التعلم الآلي الذين يرغبون في تجربة مشاكل تصنيف الصور ، قد يكون التمرين الجيد هو بناء نموذج تصنيف ثنائي. الكلاب مقابل القطط التحدي هو ذلك تماما! مفهوم سهل حقًا ، تحتاج فقط إلى تعليم الكمبيوتر لإعلام الكلاب والقطط عن بعضها البعض. يمكن للمرء أن يجادل هذا باعتباره "مرحبا العالم!" من التعلم الآلي جنبا إلى جنب مع MNIST. ومع ذلك ، قد يكون من الصعب بالنسبة للمبتدئين الكاملين اختيار بنية جيدة ، وإخراج الناتج بالصيغة الصحيحة للتقديم وما إلى ذلك. ولهذا السبب ، أكتب هذا المنشور واصفا ما فعلته لهذه المنافسة. لقد حاولت أيضًا إنشاء kaggle kernel لكن أدركت أن kaggle kernel هو نظام للقراءة فقط ، لذا لا يمكنني نقل بنية البيانات الخاصة بي وإنشاء ملف إرسال خاص بي ، لذا سأنسخ الرمز الخاص بي إلى المنشور. لمعلوماتك ، لقد فعلت هذه المسابقة على جهاز Macbook الخاص بي دون وحدة معالجة الرسومات واحدة.

ما يمكن أن تتوقعه في هذا المنشور هو 1) تنظيم مجموعات بيانات التدريب / التحقق من الصحة ، 2) نقل التعلم ، 3) حفظ / تحميل أفضل نموذج ، 4) إجراء استنتاجات من مجموعة بيانات الاختبار ، 5) تقديم ملف تقديم بالتنسيق الصحيح وإرساله إلى kaggle وبعض أكثر. دون مزيد من الاستحقاق ، هيا بنا.

1. تنظيم البيانات

تأتي بياناتك مع بيانات القطار وبيانات الاختبار. تحتوي بيانات القطارات على كل من القطط والكلاب ، لكن لديهم فئة في اسم الملف (cat. .jpg لصور القطط والكلاب. .jpg لصور الكلاب). نظرًا لأن PyTorch يدعم تحميل بيانات الصور من مجلدات فرعية من دليل البيانات ، سيتعين علينا وضع جميع صور القط في مجلد القطط وكل صور الكلاب في مجلد الكلاب. سيتعين علينا أيضًا تعيين مجموعة التحقق من الصحة للتحقق من أن نموذجنا يتعلم بشكل صحيح. حتى قفص المجلدات الفرعية القطط والكلاب داخل مجلد القطار ، إنشاء مجلد فال تحت مجلد الإدخال وإنشاء نفس المجلدات الفرعية داخل مجلد فال. بيانات الاختبار غير مُعلّمة ولا بأس في المغادرة كما هي.

استيراد نظام التشغيل
train_dir = "./data/train"
train_dogs_dir = f '{train_dir} / dogs'
train_cats_dir = f '{train_dir} / cats'
val_dir = "./data/val"
val_dogs_dir = f '{val_dir} / dogs'
val_cats_dir = f '{val_dir} / cats'
طباعة ("طباعة بيانات دير")
طباعة (os.listdir ("data")) # يُظهر القطار ، تكون مجلدات val تحت البيانات
طباعة ("قطار الطباعة دير")
! ls {train_dir} | head-n 5 # يعرض ملفات الصور في مجلد القطار
طباعة ("Printing train dog dog")
! ls {train_dogs_dir} | head-n 5 # تحقق من وجود مجلد (فارغ)
طباعة ("قطار الطباعة القط دير")
! ls {train_cats_dir} | head-n 5 # تحقق من وجود مجلد (فارغ)
طباعة ("الطباعة val dir")
! ls {val_dir} | head-n 5 # عروض الكلاب الفرعية والقطط موجودة
طباعة ("الطباعة val dog dir")
! ls {val_dogs_dir} | head-n 5 # تحقق من وجود مجلد (فارغ)
طباعة ("الطباعة val cat dir")
! ls {val_cats_dir} | head-n 5 # تحقق من وجود مجلد (فارغ)

قم بتشغيل الكود أعلاه في دفتر Jupyter وتحقق من أننا أعددنا بنية المجلد الصحيح. الخطوة التالية هي نقل الملفات إلى المجلد الصحيح.

استيراد شيل
استيراد إعادة
files = os.listdir (train_dir)
# نقل جميع الصور القط القطار إلى مجلد القطط ، والصور الكلب إلى مجلد الكلاب
بالنسبة لـ f في الملفات:
    catSearchObj = re.search ("cat"، f)
    dogSearchObj = re.search ("dog"، f)
    إذا كان catSearchObj:
        shutil.move (f '{train_dir} / {f}' ، train_cats_dir)
    أليف dogSearchObj:
        shutil.move (f '{train_dir} / {f}' ، train_dogs_dir)

دعونا نتحقق من نقل الملفات بشكل صحيح.

طباعة ("Printing train dir") # يُظهر القطط والمجلدات الفرعية للكلاب فقط
! ls {train_dir} | رئيس ن 5
print ("Printing train dog dir") # هناك الآن صور للكلاب في مجلد كلاب
! ls {train_dogs_dir} | رئيس ن 5
طباعة ("الطباعة تدريب القط دير") # هناك الآن صور القط في مجلد القطط
! ls {train_cats_dir} | رئيس ن 5

الآن ، لنفصل بعض الصور للكلاب عن مجموعة التحقق من الصحة. في كثير من الحالات ، سترغب في فصل 20٪ من بياناتك بالكامل على أنها مجموعة تحقق. في هذه الحالة ، لديك 25000 صورة في مجموعة التدريب وهو عدد كبير جدًا لأن القطط والكلاب تشبه بيانات ImageNet. اعتقدت أن 20٪ من هذا العدد كبير للغاية ويكفي إخراج 1000 صورة لكل قطط وكلاب.

files = os.listdir (train_dogs_dir)
بالنسبة لـ f في الملفات:
    validationDogsSearchObj = re.search ("5 \ d \ d \ d"، f)
    إذا كان ValidationDogsSearchObj:
        shutil.move (f '{train_dogs_dir} / {f}' ، val_dogs_dir)
طباعة ("الطباعة val dog dir")
! ls {val_dogs_dir} | رئيس ن 5

أعلاه رمز نقل الصور الكلب الذي يتراوح معرفه من 5000-5999 إلى مجلد التحقق من الصحة. وتفعل الشيء نفسه بالنسبة لصورة القطط.

files = os.listdir (train_cats_dir)
بالنسبة لـ f في الملفات:
    validationCatsSearchObj = re.search ("5 \ d \ d \ d"، f)
    إذا كان ValidationCatsSearchObj:
        shutil.move (f '{train_cats_dir} / {f}' ، val_cats_dir)
طباعة ("الطباعة val cat dir")
! ls {val_cats_dir} | رئيس ن 5

2. نموذج التدريب

الآن وقد أصبحت البيانات في الهيكل الصحيح ، فقد حان الوقت لتدريب نموذجنا. الأول هو استيراد ما أحتاجه لهذا الكمبيوتر الدفتري. لكن هذه ليست القائمة الكاملة للواردات ، سنقوم باستيراد الباقي حسب الحاجة.

استيراد الشعلة
استيراد torch.nn مثل nn
استيراد الشعلة. الأمثل كما الأمثل
من torch.optim استيراد lr_scheduler
استيراد numpy كـ np
استيراد torchvision
من مجموعات بيانات استيراد torchvision ، النماذج ، التحويلات
استيراد matplotlib.pyplot كما PLT
وقت الاستيراد
استيراد نظام التشغيل
استيراد نسخة
استيراد الرياضيات
طباعة (الشعلة .__ version__)
plt.ion () # الوضع التفاعلي

دعنا نحدد زيادة بيانات التدريب وتحويل بيانات التحقق.

# زيادة البيانات وتطبيعها للتدريب
# التطبيع فقط للتحقق من الصحة
data_transforms = {
    "القطار": converts.Compose ([
        transforms.RandomRotation (5)،
        transforms.RandomHorizontalFlip ()
        transforms.RandomResizedCrop (224 ، المقياس = (0.96 ، 1.0) ، النسبة = (0.95 ، 1.05)) ،
        transforms.ToTensor ()
        تحويلات. تطبيع ([0.485 ، 0.456 ، 0.406] ، [0.229 ، 0.224 ، 0.225])
    ])،
    'val': transforms.Compose ([
        transforms.Resize ([224224])،
        transforms.ToTensor ()
        تحويلات. تطبيع ([0.485 ، 0.456 ، 0.406] ، [0.229 ، 0.224 ، 0.225])
    ])،
}

أثناء زيادة البيانات ، أقوم بتطبيق قدر ضئيل من التدوير ، التقلب العشوائي وتغيير حجم + المحصول. مقياس تغيير الحجم هو 0.96-1.0. أحاول تجنب إعطاء مقياس أقل من 0.96 لأنه لا يزال بإمكانك الحصول على التباين المطلوب في البيانات وهناك خطر أقل بكثير في إيقاف جزء مهم من البيانات (مثل رأس القط أو الكلب ، إذا لم يكن لدينا جزء رئيسي ، سيكون من الصعب على الآلة أن تتعلم كيف سيبدو كل فصل). كما يجب أن يكون التغير المعتدل في النسبة مناسبًا لغرضنا (أرق أو سمنة ، القط هو القط الصحيح؟). حول التطبيع ، استخدمت بعض القيمة المرمّزة الثابتة للانحراف المعياري والمتوسط. ومن المعروف أن هذه القيم تعمل بشكل جيد وتستخدم بشكل متكرر. تحقق من توصية مهندس AI على Facebook لاستخدام تلك القيم وأيضًا مثال PyTorch الرسمي باستخدام نفس القيمة.

data_dir = 'data'
CHECK_POINT_PATH = 'checkpoint.tar'
SUBMISSION_FILE = 'submit.csv'
image_datasets = {x: datasets.ImageFolder (os.path.join (data_dir، x)،
                                          data_transforms [س])
                  لـ x في ['train' ، 'val']}
dataloaders = {x: torch.utils.data.DataLoader (image_datasets [x]، batch_size = 4،
                                              خلط ورق اللعب = صحيح ، عدد العاملين = 4)
              لـ x في ['train' ، 'val']}
dataset_sizes = {x: len (image_datasets [x]) لـ x في ['train'، 'val']}
class_names = image_datasets ['train']
device = torch.device ("cuda: 0" if torch.cuda.is_available () آخر "cpu")
طباعة (class_names) # => ['قطط' ، 'كلاب']
طباعة (حجم الصورة f'Train: {dataset_sizes ["train"]} ')
طباعة (حجم صورة التحقق من الصحة: ​​{dataset_sizes ["val"]} ')

ثم حدد بعض الثوابت اللازمة وحدد مجموعات البيانات (تدريب و val). يجب أن تشاهد 23000 لحجم صورة القطار و 2000 لحجم صورة التحقق من الصحة إذا كنت تتبع كل شيء بشكل صحيح. لمعلوماتك ، حيث إنني أعمل بدون GPU ويستغرق الأمر بشكل محبط وقتًا طويلاً لمعالجة الكثير من البيانات التي ذهبت إليها لحذف مجموعة كاملة من البيانات ومتابعتي مع 950 صورة قطار كاملة و 71 صورة تحقق. بالنسبة لي التي أدت إلى دقة مرضية والغرض من ذلك بالنسبة لي لم يكن صنع نموذج أكثر دقة ولكن التدريب على استخدام موقع PyTorch و kaggle لهذا السبب اخترت عدم استخدام الكثير من البيانات ولكن بالطبع لن ترغب في حذفها البيانات إذا كنت تدرب نموذجا لغرض الإنتاج. سبب آخر كان من الممكن بالنسبة لي لتدريب النموذج الخاص بي مع القليل من البيانات هو أنني استخدم نموذج prerained. وهذا يعني أنني قمت بتعديل وتدريب الطبقة الأخيرة فقط واستخدمت جميع الطبقات كما كانت لأن النموذج تم تدريبه جيدًا باستخدام بيانات ImageNet بالفعل.

دعونا نلقي نظرة على شكل مجموعة صغيرة (4 صور) من مجموعة التدريب باستخدام مقتطف الشفرة التالي.

def imshow (inp ، title = None):
    "" "Imshow لـ Tensor." ""
    inp = inp.numpy (). تبديل ((1 ، 2 ، 0))
    يعني = np.array ([0.485 ، 0.456 ، 0.406])
    الأمراض المنقولة جنسياً = np.array ([0.229 ، 0.224 ، 0.225])
    inp = std * inp + mean
    inp = np.clip (inp، 0، 1)
    plt.imshow (الشرطة الوطنية العراقية)
    إذا كان العنوان ليس بلا:
        plt.title (العنوان)
    plt.pause (0.001) # توقف قليلاً حتى يتم تحديث المؤامرات
# احصل على مجموعة من بيانات التدريب
المدخلات ، الفصول = التالي (iter (dataloaders ['train']))
# اصنع شبكة من الدفعة
sample_train_images = torchvision.utils.make_grid (المدخلات)
imshow (sample_train_images ، title = classes)

سترى 4 صور تم اختيارها عشوائياً وسوف يقول العنوان 0 للقط و 1 للكلاب. بعد ذلك ، دعونا نحدد وظيفة تدرب نموذجنا وتعيد بعض المقاييس.

def train_model (نموذج ، معيار ، مُحسِّن ، مجدول ، num_epochs = 2 ، نقطة تفتيش = لا شيء):
    منذ = الوقت ()
إذا كانت نقطة التفتيش بلا:
        best_model_wts = copy.deepcopy (model.state_dict ())
        best_loss = math.inf
        best_acc = 0.
    آخر:
        print (f'Val loss: {checkpoint ["best_val_loss"]} ، دقة Val: {checkpoint ["best_val_accuracy"]} ')
        model.load_state_dict (نقطة تفتيش [ 'model_state_dict'])
        best_model_wts = copy.deepcopy (model.state_dict ())
        optimizer.load_state_dict (نقطة تفتيش [ 'optimizer_state_dict'])
        scheduler.load_state_dict (نقطة تفتيش [ 'scheduler_state_dict'])
        best_loss = checkpoint ['best_val_loss']
        best_acc = checkpoint ['best_val_accuracy']
لعصر في المدى (num_epochs):
        print ('Epoch {} / {}'. format (epoch، num_epochs - 1))
        طباعة ('-' * 10)
# كل حقبة لديها مرحلة التدريب والتحقق من الصحة
        للمرحلة في ['قطار' ، 'val']:
            إذا المرحلة == "القطار":
                scheduler.step ()
                model.train () # تعيين نموذج لوضع التدريب
            آخر:
                model.eval () # قم بتعيين النموذج لتقييم الوضع
running_loss = 0.0
            running_corrects = 0
# تكرار على البيانات.
            بالنسبة إلى i ، (المدخلات ، الملصقات) في التعداد (dataloaders [المرحلة]):
                المدخلات = المدخلات. (الجهاز)
                labels = labels.to (الجهاز)
# صفر التدرجات المعلمة
                optimizer.zero_grad ()
                
                إذا كنت٪ 200 == 199:
                    print ('[٪ d،٪ d]:٪ .3f'٪
                          (epoch + 1، i، running_loss / (i * inputs.size (0))))
# إلى الأمام
                # تتبع التاريخ إذا كان فقط في القطار
                مع torch.set_grad_enabled (المرحلة == "القطار"):
                    مخرجات = نموذج (المدخلات)
                    _ ، preds = torch.max (المخرجات ، 1)
                    الخسارة = المعيار (المخرجات ، الملصقات)
# الوراء + الأمثل إلا إذا كان في مرحلة التدريب
                    إذا المرحلة == "القطار":
                        loss.backward ()
                        optimizer.step ()
# الإحصاء
                running_loss + = loss.item () * المدخلات. الحجم (0)
                running_corrects + = torch.sum (preds == labels.data)
epoch_loss = running_loss / dataset_sizes [المرحلة]
            epoch_acc = running_corrects.double () / dataset_sizes [الطور]
print ('{} الخسارة: {: .4f} Acc: {: .4f}'. format (
                المرحلة ، epoch_loss ، epoch_acc))
# نسخة عميقة النموذج
            إذا كانت المرحلة == 'val' و epoch_loss 
طباعة()
time_elapsed = time.time () - منذ ذلك الحين
    print (اكتمل التدريب بـ {: .0f} m {: .0f} s'.format (
        time_elapsed // 60 ، time_elapsed٪ 60))
    print ('Best val Acc: {: .4f} أفضل خسارة val: {: .4f}'. format (best_acc، best_loss))
# تحميل أفضل الأوزان نموذج
    model.load_state_dict (best_model_wts)
    نموذج الإرجاع ، best_loss ، best_acc

تتحقق الوظيفة أولاً إذا تم اجتياز نقطة التفتيش المحفوظة. إذا كانت الإجابة بنعم ، فسيتم تحميل المعلمة المحفوظة والبدء في التدريب من حيث توقفت. إذا كانت الإجابة "لا" ، فسيبدأ تدريب النموذج الذي تم تمريره (سنظل نستخدم نموذج التدريب المسبق من البداية). تعمل هذه الوظيفة على تحديث المعلمات فقط في مرحلة القطار وتطبع بعض المقاييس في كل فترة زمنية أو كلما كان لديها أفضل خسارة جديدة.

الآن ، دعونا نحدد نموذجنا عن طريق تنزيل نموذج ما قبل التدريب. يستغرق تشغيل الكود التالي بعض الوقت إذا لم تقم بتشغيله من قبل.

model_conv = torchvision.models.resnet50 (pretrained = True)

resnet50 عبارة عن بنية شبكة عصبية تلافيفية قوية حقًا لحل مشاكل رؤية الكمبيوتر. تشتمل النماذج الأقل قوة واستهلاكًا للموارد التي قد تكون مهتمًا باستخدامها على resnet18 و resnet34. عن طريق إعطاء pretrained = True كحجة ، ستقوم بتنزيل نموذج باستخدام المعلمات المدربة باستخدام مجموعة بيانات ImageNet. نظرًا لأننا نحتاج إلى تغيير نموذج احتياجاتنا (تصنيف الفئة الثنائية) ، فسوف نقوم بتغيير آخر طبقة متصلة بالكامل وتحديد وظيفة الخسارة التي تكون مفيدة لمشكلة التصنيف (خسارة الانتروبيا المتقاطعة ، والتي تجمع بين سجل softmax ووظيفة احتمال احتمال السجل السلبي) . Optimzier هو مُحسِّن النسب التدرج العشوائي والجدول الزمني هو الأسي لأنه سوف يقلل من معدل التعلم بعامل 10 في كل 7 فترات (في الواقع لقد قمت بتدريب 6 عهود فقط).

من أجل param في model_conv.parameters ():
    param.requires_grad = خطأ
# تتطلب معلمات الوحدات النمطية التي تم إنشاؤها حديثًا__ = صحيح افتراضيًا
num_ftrs = model_conv.fc.in_features
model_conv.fc = nn.Linear (num_ftrs، 2)
model_conv = model_conv.to (الجهاز)
المعيار = nn.CrossEntropyLoss ()
# لاحظ أنه يتم تحسين معلمات الطبقة النهائية فقط
optimizer_conv = optim.SGD (model_conv.fc.parameters () ، lr = 0.001 ، زخم = 0.9)
# تسوس LR بعامل 0.1 كل 7 حقبات
exp_lr_scheduler = lr_scheduler.StepLR (optimizer_conv، step_size = 7، gamma = 0.1)

يمكن أن ندخل أخيرًا في التدريب الفعلي.

محاولة:
    نقطة تفتيش = torch.load (CHECK_POINT_PATH)
    طباعة ("نقطة تفتيش محملة")
إلا:
    نقطة تفتيش = لا شيء
    طباعة ("نقطة تفتيش غير موجودة")
model_conv، best_val_loss، best_val_acc = train_model (model_conv،
                                                      معيار،
                                                      optimizer_conv،
                                                      exp_lr_scheduler،
                                                      num_epochs = 3 ،
                                                      نقطة تفتيش = نقطة تفتيش
torch.save ({'model_state_dict': model_conv.state_dict () ،
            'optimizer_state_dict': optimizer_conv.state_dict () ،
            'best_val_loss': best_val_loss ،
            'best_val_accuracy': best_val_acc،
            'scheduler_state_dict': exp_lr_scheduler.state_dict () ،
            } ، CHECK_POINT_PATH)

يتحقق الرمز أولاً إذا تم حفظ أي نقطة تفتيش من التدريب السابق. إذا كانت الإجابة بنعم ، فمرر نقطة التفتيش إلى وظيفة القطار. لقد حددت هنا 3 حقبات وتعيد الدالة نموذجًا ، وفقدان ، ودقة عندما كانت الخسارة أدنى خلال جميع الحقبات. نحن نحفظ ما حصلنا عليه من الوظيفة إلى نقطة تفتيش. يمكنك ضبط رقم epoch أو إعادة تشغيل هذا المقتطف كما تريد. إذا رأيت أن النموذج لا يتحسن بعد الآن ، يمكنك التوقف. نظرًا لأن لدي 71 صورة فقط من مجموعات التحقق من الصحة ، فقد وصلت إلى الدقة 1.0 بفقدان 0.036 في تشغيلين فقط ، لذلك قررت التوقف عن التدريب.

لاحظ أننا نحتاج فقط لتدريب الطبقة الأخيرة التي غيرناها من resnet50 الأصلي. من الممكن أيضًا تدريب جميع المعلمات في جميع الطبقات ، لكن عندما جربت ذلك ، رأيت فقط الخسارة والدقة تزداد سوءًا. إذا كنت ترغب في محاولة تحديث جميع المعلمات ، يمكنك القيام بذلك على النحو التالي.

من أجل param في model_conv.parameters ():
    param.requires_grad = صحيح
model_conv = model_conv.to (الجهاز)
# لاحظ أن يتم تحسين جميع المعلمات
optimizer_ft = optim.SGD (model_conv.parameters () ، lr = 0.001 ، زخم = 0.9)

ثم قم بتشغيل نفس رمز حلقة التدريب كما فعلنا من قبل (كتلة التعليمات البرمجية تبدأ بالمحاولة).

ربما هذه القصة طويلة للغاية بالفعل. لذلك سأكتب الجزء الذي تقوم فعلاً باستنتاجه باستخدام النموذج باستخدام مجموعة بيانات الاختبار وتقديم إجابتك إلى kaggle في قصة منفصلة. ضبط في الجزء التالي وبعض أكثر. اسمحوا لي أن أعرف إذا كنت قد نجحت في اتباع التعليمات البرمجية الخاصة بي وكذلك إذا حصلت على نتيجة مرضية باستخدام هذه الطريقة.

الجزء الثاني (الأخير) خارج. إذا كنت ترغب في مواصلة القراءة ، انقر هنا. يمكنك أيضًا رؤية الكود الكامل في Github repo. يمكن العثور على رمز للمعالجة المسبقة للبيانات في datapreprocessor.ipynb والتدريب ، ويمكن العثور على الاستنتاجات وتقديم في catsanddogs.ipynb.