כתיבת VGG מאפס ב-PyTorch

ממשיכים בסדרה שלי על בניית רשתות קונבלציונליות קלאסיות שממנן השתנה השדה של הראייה ב-1-2 עשורים האחרונים, אנחנו בעוד מבנים VGG, רשת קונבלציונלית מאוד עמוקה, מתחילה בPyTorch. אתם יכולים לראות את המאמרים הקודמים בסדרה על הפרופיל שלי, בעיקר LeNet5 וAlexNet.

כמו בפעם הקודמת, נבדוק את הארכיטקטורה וההגיון מאחורי VGG ואיך התוצאות היו באותה התקופה. אחר כך, נחקור את המידע שלנו, CIFAR100, ונטען אותו לתוכנית שלנו באמצעות קוד יעיל לזכוכית. אחר כך, ניתן את VGG16 (המספר מתייחס למספר השכבות, יש שני גירסאות בעיקרון לVGG16 וVGG19) מתחילה בPyTorch ואז נאמן אותו על המידע שלנו ביחד עם בדיקתו על המבחן כדי לראות איך הוא מביא תוצאות על מידע שלא נראה לשעבר


VGG

בעזרת העבודה של AlexNet, VGG מתמקד באסpect החשוב האחר, של רשתות מונחתות (CNNs), עומס. היא פותחה על-ידי Simonyan וZisserman. היא בדרך כלל מורכבת מ-16 שכבות מונחתות אבל יכולה להתרחב ל-19 שכבות (לכן יש שני גירסאות, VGG-16 וVGG-19). כל השכבות המונחתות מורכבות מפילטרים 3×3. ניתן לקרוא על הרשת במאמר הרשמי פה

על מבנה VGG16. מקור


טעיית מידע

קבוצת מידע

לפני שיבנינו את המודל, אחד הדברים הכי חשובים בפרוייקט למידת מכונה כלשהו הוא לטעון, לנתח ולטיפול במאגר נתונים. במאמר זה, אנחנו נשתמש במאגר נתונים CIFAR-100. המאגר הזה דומה ל-CIFAR-10, אבל יש לו 100 מחלוקות שכוללות 600 תמונות בכל אחת. יש 500 תמונות מאמצים ו-100 תמונות בדיקה לכל מחלקה. ה-100 מחלקות ב-CIFAR-100 מקבצות ל-20 סופרקלassen. לכל תמונה יש תווית "פאין" (המחלקה שלה) ותווית "קוארס" (הסופרקלassen שלה). אנחנו נהגה בתווית "פאין" פה. הנה רשימת המחלקות ב-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 בו אנחנו מגדילים את הנתונים, מועברים אותם לטנסורים ואז מונילים אותם
  • אם האגודל 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 מתחילה


היפרפרמטרים

חלק חשוב מבין הפרויקטים של המכונה או הלמידה עמוקה הוא לאופטימיze את ההיפרפרמטרים. פה, אנחנו לא ננסה עם ערכים שונים לאלה אך נצטרך להגדיר אותם לפני כן. אלה כוללים הגדרת מספר המבטאים, גודל הערימות, שיעור הלמידה, פונקציית האובדן ביחד עם האופטימיzer.

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