كتابة VGG من البداية في PyTorch

وسوف نستمر في سلسلتي حول بناء شبكات العقود التصويرية التي ثورت مجال بصريات الحاسوب خلال العقدين الماضيين، حيث سنبني في المرة القادمة شبكة VGG من الشبكات التصويرية العميقة من الصفر باستخدام PyTorch. يمكنك رؤية المقالات السابقة في السلسلة على ملفي الشخصي، بالأساس LeNet5 و AlexNet.

كما قمنا من قبل، سننظر في هيكل الشبكة والمبادئ الخلفية ل VGG وكيف كانت النتائج في تلك الأثناء. سنبحث بعدها في قاعدة البيانات الخاصة بنا، CIFAR100، وسنحملها إلى برنامجنا باستخدام برنامج ذو كفاءة حافظية متوازية. بعدها، سننفذ شبكة VGG16 (الرقم يشير إلى عدد الطوابق، وهناك نسختان بالأساس: VGG16 و VGG19) من الصفر باستخدام PyTorch ومن ثم ندربها على قاعدة البيانات الخاصة بنا ونقوم بتقييمها على مجموعة الاختبار لرؤية كيف تعمل على البيانات الجديدة.


VGG

بناءً على عمل AlexNet، يركز VGG على جانب آخر مهم من شبكات الأعمال العصبية التقنية (الشبكات العصبية التقنية)، عمق. وقد طوره Simonyan و Zisserman. إنه عادةً يتكون من 16 طبقات من التصوير ولكن يمكن أيضًا توسيعه إلى 19 طبقات (لهذه الدوائرين المختلفين، VGG-16 و VGG-19). كل طبقات التصوير تكون من مرشحات 3×3. يمكنك أن تقرأ المزيد عن الشبكة في الوثيقة الرسمية الخاصة هنا هنا

VGG16 للبنية. مصدر


تحميل البيانات

قاعدة البيانات

قبل أن نبني النموذج، أحد الأمور الأكثر أهمية في أي مشروع لل机器学习 (Machine Learning) هو تحميل، تحليل، وتحضير المجموعة البياناتية. سنستخدم في هذه المقالة مجموعة CIFAR-100. تشابه هذه المجموعة مجموعة CIFAR-10، إلا أنها تحتوي على 100 فئة تشمل 600 صورة كل واحدة. هناك 500 صورة تدريبية و100 صورة تجارية لكل فئة. ال100 فئة في CIFAR-100 تتم تجميعها في 20 فئة عالية. كل صورة تأتي مع تصنيف “دقيق” (الفئة التي تختلف إليها) وتصنيف “تركيزي” (الفئة العالية التي تختلف إليها). سنستخدم هنا التصنيف “دقيق”. هذه هي قائمة الفئات في CIFAR-100:


قائمة الفئات لمجموعة CIFAR-100

تحميل المكتبات

سنعمل أساسًا بواسطة torch (يستخدم لبناء النموذج والتدريب)، torchvision (لتحميل/تحليل البيانات، وتحتوي على مجموعات وطرق لمعالجة تلك المجموعات في الرؤية الحاسوبية)، و numpy (للتحميل الرياضي). سنحدد أيضًا متغيرًا device حتى البرنامج يستطيع استخدام الGPU إذا كان متاحًا

import numpy as np
import torch
import torch.nn as nn
from torchvision import datasets
from torchvision import transforms
from torch.utils.data.sampler import SubsetRandomSampler


# تكوين الجهاز
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

تحميل البيانات

torchvision هي مكتبة توفر وصول سهل إلى العديد من المجموعات الخاصة بالرؤية الحاسوبية وطرق لتجهيز هذه المجموعات بطريقة سهلة وغير معقدة

  • نحن نعرف وظيفة data_loader تعود إلى البيانات التعلمية / التجريدية أو البيانات الاختبارية وفقاً للمعارف
  • نبدأ بتعريف المتغير normalize بمعدلات واسعاد القناة (الحمراء والأزرق والاخضر) في المجموعة. يمكن الحساب لها يدوياً ، ولكن يوجد أيضًا معلومات متاحة على الإنترنت. يستخدم هذا في المتغير transform حيث نغير حجم البيانات ، نحو توانسيات ثم نormalizeها
  • إذا كان المتغير test صحيحًا ، نقوم بتحميل المجموعة الاختبارية للبيانات ونستعيدها بواسطة محركات البيانات (التي ستشرح أدناه)
  • في حالة عدم صحة test (سلوك الأفتراضي أيضًا) ، نقوم بتحميل المجموعة التعلمية ونقسمها عشوائيًا إلى مجموعة تعلمية ومجموعة للتجريد (0.9:0.1)
  • أخيرًا ، نستخدم محركات البيانات. قد لا تؤثر هذا على الأداء في حالة مجموعة صغيرة مثل CIFAR100 ، لكنه يمكن أن يؤدي إلى تأخير كبير في حالة مجموعات كبيرة وهو عامةً ما يعتبر ممارسة جيدة. تسمح محركات البيانات لنا بتتبع البيانات في مساويات وتحميل البيانات أثناء التتبع وليس كلها في البدء إلى ذاك
def data_loader(data_dir,
                batch_size,
                random_seed=42,
                valid_size=0.1,
                shuffle=True,
                test=False):
  
    normalize = transforms.Normalize(
        mean=[0.4914, 0.4822, 0.4465],
        std=[0.2023, 0.1994, 0.2010],
    )

    # تحويلات
    transform = transforms.Compose([
            transforms.Resize((227,227)),
            transforms.ToTensor(),
            normalize,
    ])

    if test:
        dataset = datasets.CIFAR100(
          root=data_dir, train=False,
          download=True, transform=transform,
        )

        data_loader = torch.utils.data.DataLoader(
            dataset, batch_size=batch_size, shuffle=shuffle
        )

        return data_loader

    # تحميل البيانات
    train_dataset = datasets.CIFAR100(
        root=data_dir, train=True,
        download=True, transform=transform,
    )

    valid_dataset = datasets.CIFAR10(
        root=data_dir, train=True,
        download=True, transform=transform,
    )

    num_train = len(train_dataset)
    indices = list(range(num_train))
    split = int(np.floor(valid_size * num_train))

    if shuffle:
        np.random.seed(random_seed)
        np.random.shuffle(indices)

    train_idx, valid_idx = indices[split:], indices[:split]
    train_sampler = SubsetRandomSampler(train_idx)
    valid_sampler = SubsetRandomSampler(valid_idx)

    train_loader = torch.utils.data.DataLoader(
        train_dataset, batch_size=batch_size, sampler=train_sampler)
 
    valid_loader = torch.utils.data.DataLoader(
        valid_dataset, batch_size=batch_size, sampler=valid_sampler)

    return (train_loader, valid_loader)


# قاعدة CIFAR100
train_loader, valid_loader = data_loader(data_dir='./data',
                                         batch_size=64)

test_loader = data_loader(data_dir='./data',
                              batch_size=64,
                              test=True)

VGG16 من البداية

لبناء النموذج من البداية، يتوجب علينا أولاً فهم كيفية تشكيل التعريفات النموذجية في torch وأنواع الطبقات المختلفة التي سنستخدمها هنا:

  • كل نموذج مختصر يتوجب أن يتم تخصيصه من خلال القسم nn.Module لأنه يوفر بعض الوظائف الأساسية التي تساعد النموذج على التدريب.
  • ثانيًا، يوجد شيئان رئيسيان يتوجب علينا فعلهما. الأول، تعريف الأطبقات المختلفة لنموذجنا داخل ال関数 __init__ والتسلسل الذي سيتم تنفيذه على المدخل داخل ال関数 forward

دعونا نتعريف الأنواع المختلفة للطبقات التي نستخدمها هنا:

  • nn.Conv2d: هي الطبقات التصاعدية التي تقبل عدد القنوات المدخلة والخارجية كما تلو الحجم العام للمرشد. وتقبل أيضًا أي تسارد أو تعبئة إذا كنت تريد تطبيقها
  • nn.BatchNorm2d: هذا التطبيق يطبق التنظيم الجماعي على خاتم الطبقة التصاعدية
  • nn.ReLU: هذه هي التفعيلات التي يتم تطبيقها على مختلف من الخاتمات في الشبكة
  • nn.MaxPool2d : هذا يطبق عملية البحث عن الأقصى على الخريطة الخاصة بالناموسية والحجم المعين
  • nn.Dropout: يستخدم لتطبيق التخفيف على الخريطة بمعدل معين
  • nn.Linear: هذه جميعًا طبقة متصلة بالكامل
  • nn.Sequential: هذا ليس في الحقيقة نوعًا من الطبقات ولكنه يساعد في تركيب مختلف العمليات التي تكون جزءًا من نفس الخطوة

وبواسطة هذه المعرفة، يمكننا الآن بناء نموذج VGG16 باستخدام الهندسة التي تظهر في الوثيقة:

class VGG16(nn.Module):
    def __init__(self, num_classes=10):
        super(VGG16, self).__init__()
        self.layer1 = nn.Sequential(
            nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(64),
            nn.ReLU())
        self.layer2 = nn.Sequential(
            nn.Conv2d(64, 64, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(64),
            nn.ReLU(), 
            nn.MaxPool2d(kernel_size = 2, stride = 2))
        self.layer3 = nn.Sequential(
            nn.Conv2d(64, 128, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(128),
            nn.ReLU())
        self.layer4 = nn.Sequential(
            nn.Conv2d(128, 128, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(128),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size = 2, stride = 2))
        self.layer5 = nn.Sequential(
            nn.Conv2d(128, 256, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(256),
            nn.ReLU())
        self.layer6 = nn.Sequential(
            nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(256),
            nn.ReLU())
        self.layer7 = nn.Sequential(
            nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(256),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size = 2, stride = 2))
        self.layer8 = nn.Sequential(
            nn.Conv2d(256, 512, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(512),
            nn.ReLU())
        self.layer9 = nn.Sequential(
            nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(512),
            nn.ReLU())
        self.layer10 = nn.Sequential(
            nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(512),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size = 2, stride = 2))
        self.layer11 = nn.Sequential(
            nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(512),
            nn.ReLU())
        self.layer12 = nn.Sequential(
            nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(512),
            nn.ReLU())
        self.layer13 = nn.Sequential(
            nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(512),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size = 2, stride = 2))
        self.fc = nn.Sequential(
            nn.Dropout(0.5),
            nn.Linear(7*7*512, 4096),
            nn.ReLU())
        self.fc1 = nn.Sequential(
            nn.Dropout(0.5),
            nn.Linear(4096, 4096),
            nn.ReLU())
        self.fc2= nn.Sequential(
            nn.Linear(4096, num_classes))
        
    def forward(self, x):
        out = self.layer1(x)
        out = self.layer2(out)
        out = self.layer3(out)
        out = self.layer4(out)
        out = self.layer5(out)
        out = self.layer6(out)
        out = self.layer7(out)
        out = self.layer8(out)
        out = self.layer9(out)
        out = self.layer10(out)
        out = self.layer11(out)
        out = self.layer12(out)
        out = self.layer13(out)
        out = out.reshape(out.size(0), -1)
        out = self.fc(out)
        out = self.fc1(out)
        out = self.fc2(out)
        return out

VGG16 من الصفر


المادات الرئيسية

أحد الأجزاء المهمة لأي مشروع للآلات أو التعلم العميق هو تحسين المادات الفوقية. هنا، لن نقوم بتجربة قيم مختلفة لهذه لكن سيتوجب علينا تعريفهم من قبل. هذه تتضمن تعريف عدد المرات التدريبية، حجم المجموعات، معدل التعلم، والمصادفة مع المعالج الخاص

num_classes = 100
num_epochs = 20
batch_size = 16
learning_rate = 0.005

model = VGG16(num_classes).to(device)


# الخسارة والمعالج
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate, weight_decay = 0.005, momentum = 0.9)  


# تدريب النموذج
total_step = len(train_loader)

تعديل المادات الفوقية


التدريب

نحن حالياً جاهزون لتمرين نموذجنا. سنظر أولاً إلى كيفية تمرين نموذجنا في torch ومن ثم ننظر إلى البرمجيات:

  • لكل دورة، نتمرد على الصور والتسميات داخل train_loader ونحنل تلك الصور والتسميات إلى الGPU إذا كان متاحًا. يحدث هذا تلقائيًا
  • نستخدم نموذجنا للتنبؤ بالتسميات (model(images))ومن ثم نحسب الخسارة بين التنبؤات والتسميات الحقيقية بواسطة ما نسميه معاً وعي خسارة (criterion(outputs, labels))
  • ومن ثم نستخدم تلك الخسارة للتغيير الأسي (loss.backward) وتحديث الوزنات (optimizer.step()). ولكن تذكر أن تضع المعادات في صفر قبل كل تحديث. يتم ذلك باستخدام optimizer.zero_grad()
  • أيضًا في نهاية كل دورة نستخدم جهة تجريدنا لتحسين دقة نموذجنا. في هذه الحالة ، لا نحتاج إلى المعادات لذلك نستخدم with torch.no_grad() للتقييم السريع

الآن نحن نجمع كل هذا في البرمجيات التالية:

total_step = len(train_loader)

for epoch in range(num_epochs):
    for i, (images, labels) in enumerate(train_loader):  
        # تحريك التونسورات إلى الأجهزة المعينة
        images = images.to(device)
        labels = labels.to(device)
        
        # المرور الأمامي
        outputs = model(images)
        loss = criterion(outputs, labels)
        
        # التغيير والتحسين
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

    print ('Epoch [{}/{}], Step [{}/{}], Loss: {:.4f}' 
                   .format(epoch+1, num_epochs, i+1, total_step, loss.item()))
            
    # التحقق
    with torch.no_grad():
        correct = 0
        total = 0
        for images, labels in valid_loader:
            images = images.to(device)
            labels = labels.to(device)
            outputs = model(images)
            _, predicted = torch.max(outputs.data, 1)
            total += labels.size(0)
            correct += (predicted == labels).sum().item()
            del images, labels, outputs
    
        print('Accuracy of the network on the {} validation images: {} %'.format(5000, 100 * correct / total)) 

التمرين

يمكننا رؤية خريطة البرمجيات الأعلى كما يلي:


خسارات التمرين


تجربة

للتجربة، نستخدم نفس الشفرة البرمجية التي نستخدمها للتحقق ولكن مع test_loader:

with torch.no_grad():
    correct = 0
    total = 0
    for images, labels in test_loader:
        images = images.to(device)
        labels = labels.to(device)
        outputs = model(images)
        _, predicted = torch.max(outputs.data, 1)
        total += labels.size(0)
        correct += (predicted == labels).sum().item()
        del images, labels, outputs

    print('Accuracy of the network on the {} test images: {} %'.format(10000, 100 * correct / total))   

تجربة

باستخدام الشفرة العلائقية وتدريب النموذج لـ 20 مرات ، تمكننا من تحقيق دقة 75% في المجموعة الاختبارية.


الخلاصة

دعونا نختم ما فعلناه في هذه المقالة:

  • بدأنا بفهم الهيكلية وأنواع الطبقات المختلفة في نموذج VGG-16
  • ثم، قمنا بتحميل وتعديل المجموعة CIFAR100 باستخدام torchvision
  • ومن ثم، استخدمنا PyTorch لبناء نموذج VGG-16 من الصفر مع فهم أنواع مختلفة للطبقات المتوفرة في torch
  • وأخيرًا، تم تدريب واختبار نموذجنا على مجموعة CIFAR100 وبدا أن النموذج يشتغل جيدًا على المجموعة الاختبارية بدقة 75%

العمل المستقبلي

من خلال هذه المقالة، ستحصل على تقدم جيد وتعلم باليد الحركية ولكن سوف تتعلم المزيد إذا قمت بتوسيع هذا ورؤية ما يمكنك فعله آخر:

  • يمكنك تجربة استخدام مجموعات مختلفة. وإحدى هذه المجموعات هي CIFAR10 أو جزء من مجموعة ImageNet.
  • يمكنك تجربة مختلف المادة خاصة ورؤية أفضل مزيج منهم للنموذج
  • في النهاية يمكنك أن تجربة إضافة أو إزالة أطباق من المجموعة لرؤية تأثيرهم على قدرة النموذج. وأفضل بكثير من ذلك، قم ببناء نسخة VGG-19 من هذا النموذج.

Source:
https://www.digitalocean.com/community/tutorials/vgg-from-scratch-pytorch