در این پست میخواهیم دیتاست CIFAR10 را با استفاده از شبکههای کانولوشن ( CNN – Convolutional Neural Network) و با استفاده از فریمورک یادگیری عمیق پای تورج (PyTorch) دستهبندی (Classification) کنیم.
دیتاست سیفار-۱۰ یا همان CIFAR-10 یک دیتاست است که از ۱۰ کلاس زیر تشکیل شده است:
- هواپیما
- اتومبیل
- پرنده
- گربه
- گوزن
- سگ
- قورباغه
- اسب
- کشتی
- کامیون
هر کلاس شامل ۵۰۰۰ داده آموزش و ۱۰۰۰ داده تست هست و کل دیتاست شامل ۶۰۰۰۰ تصویر است. حجم دیتاست چیزی در حدود ۱۶۰ مگابایت است. هر تصویر در این دیتاست ۳۲*۳۲ پیکسل است. در تصویر زیر نمونهای از تصویرهای موجود در این دیتاست را مشاهده میکنید.
از آنجایی که دیتاست تصویر است از شبکه کانولوشنی استفاده میکنیم زیرا اینگونه شبکهها نتایج بسیار خوبی روی تصویرها دارند و اطلاعات مکانی در تصویر را به خوبی یاد میگیرند.
از آنجایی که هدف به دست آوردن یک دقت معقول و کار با شبکههای عصبی کانولوشن و فریمورک پایتورچ است، از یک شبکه سبک و ساده کانولوشن استفاده میکنیم که هم دقت نسبتا خوبی به دست بیاوریم و هم آن را به آسانی آموزش دهیم.
شبکه کانولوشنی که استفاده میکنیم به شکل زیر است:
- یک لایه کانولوشنی که یک ورودی با ۳ کانال را میگیرد و یک خروجی با ۹ کانال میدهد و اندازه کرنل آن ۵*۵ است و از تابع غیر خطی ReLu استفاده میکند.
- یک لایه Max Pooling که اندازه کرنل آن ۲*۲ است.
- یک لایه کانولوشنی که یک ورودی با ۹ کانال را میگیرد و یک خروجی با ۱۸ کانال میدهد و اندازه کرنل آن ۳*۳ است و از تابع غیر خطی ReLu استفاده میکند.
- یک لایه Max Pooling که اندازه کرنل آن ۲*۲ است.
- یک لایه Fully Connected که ۱۰۰ نورون دارد و از تابع غیر خطی ReLu استفاده میکند.
- یک لایه Fully Connected که ۴۰ نورون دارد و از تابع غیر خطی ReLu استفاده میکند.
- یک لایه Fully Connected که ۱۰ نورون دارد که با تعداد کلاسها برابر است.
ابتدا پکیجهای مورد استفاده را ایمپورت میکنیم:
import torch import torchvision import numpy import os import torch.utils.data as datautils import matplotlib.pyplot as plt import torch.nn as to_nn import torch.nn.functional as to_f import torch.optim as optim
torch و torchvision دو پکیج برای کار کردن با شبکههای عصبی و انجام کارهای مربوط به بینایی کامپیوتر (ویژن) هستند.
سپس اقدام به خواندن دیتاست میکنیم:
# transforms that should be done of each image # 1- convert from numpy to tensor # 2- normalize pixel values to real value range [-1,1], for better and easier training image_transforms = torchvision.transforms.Compose([ torchvision.transforms.ToTensor(), torchvision.transforms.Normalize(mean=(0.5, 0.5, 0.5), std=(0.5, 0.5, 0.5)) ]) # load training and test set classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck') cifar10_training_set = torchvision.datasets.CIFAR10(root='./dataset_CIFAR10', train=True, transform=image_transforms, download=True) cifar10_test_set = torchvision.datasets.CIFAR10(root='./dataset_CIFAR10', train=False, transform=image_transforms, download=True) trainingset_loader = datautils.DataLoader(dataset=cifar10_training_set, batch_size=4, shuffle=True, num_workers=2) testset_loader = datautils.DataLoader(dataset=cifar10_test_set, batch_size=4, shuffle=True, num_workers=2)
در کد بالا transforms.ToTensor تابعی هست که یک آرایه عادی یا از جنس numpy را به یک Tensorتبدیل میکند. پایتورچ از تنسور استفاده میکند و از آرایههای عادی و numpy استفاده نمیکند. تفاوتشان این است که تنسور قابلیت استفاده از جی.پی.یو را دارند. تابع transforms.Normaalize با توجه ورودیهایی که به آن داده شده است، مقدار پیکسلها را به بازهی -۱ تا ۱ منتقل میکند و این نرمال سازی کار آموزش شبکه را آسانتر میکند. تابع compose دو تابع بالا را در کنارهم قرار میدهد. پایتورچ تابعها و امکاناتی برای خواندن بعضی از دیتاستهای معروف دارند و یکی از این دیتاستها دیتاست CIFAR10 است که در بالا از آن استفاده کردهایم. در صورتی که دیتاست در مسیر مشخص شده موجود نباشد آن را اتوماتیک دانلود میکند. DataLoader اجازه دسترسی به دیتاهای دیتاست را به صورت mini-batch میدهد و کار شافل کردن را نیز انجام میدهد.
def matplotlib_imshow(img_tensor, title=None): """ Get and image in tensor form and display it. :param img_tensor: :return: """ device = torch.device("cpu") img_tensor_cpu = img_tensor.to(device) std = 0.5 mean = 0.5 img_tensor = std * img_tensor_cpu + mean # convert tensor to numpy img = img_tensor.numpy() # in pytorch image is (3, H, W) # normal form is (H, W, 3) img = img.transpose(1, 2, 0) plt.figure() plt.imshow(img) if title is not None: plt.title(title)
تابع بالا کار نمایش یک تصویر که به صورت Tensor است را انجام میدهد. در ابتدا مشخص شده است که تنسور قرار است با سی.پی.یو پردازش شود. سپس مقدارهای -۱ تا ۱ به بازهی ۰ تا ۱ منتقل میشوند و بعد تنسور به یک آرایه از نوع numpy تبدیل میشود و در ادامه تصویر از شکل ۳HW به صورت HW3 در آورده میشود. دلیل این امر تفاوت تصویر در پایتورچ با سایر کتابخانهها و استانداردها است و در آخر تصویر رسم میشود.
در ادامه چند مورد از تصویرهای موجود در مجموعه دادهی سیفار را رسم میکنیم:
# show some samples of traingset data_iter = iter(trainingset_loader) for i in range(3): images, labels = data_iter.next() matplotlib_imshow(img_tensor=torchvision.utils.make_grid(images), title=['gt: '+classes[i.item()] for i in labels])
خروجی کد بالا تصاویر زیر است:
gt در بالا معنی Ground Truth را میدهد.
# define a convolutional neural network class ConvNet(to_nn.Module): # constructor of model def __init__(self): # constructor of parent class super(ConvNet, self).__init__() # learnable parameter of model (convolution and fully connected layers) self.conv1 = to_nn.Conv2d(in_channels=3, out_channels=9, kernel_size=(5, 5)) self.conv2 = to_nn.Conv2d(in_channels=9, out_channels=18, kernel_size=(3, 3)) self.fc1 = to_nn.Linear(in_features=18 * 6 * 6, out_features=100) self.fc2 = to_nn.Linear(in_features=100, out_features=40) self.fc3 = to_nn.Linear(in_features=40, out_features=10) # feedforward def forward(self, input): # convolutional layer 1 out = self.conv1(input) # pooling kernel size(2,2) and relu non linearity out = to_f.max_pool2d(to_f.relu(out), (2, 2)) # convolutional layer 2 out = self.conv2(out) # pooling kernel size(2,2) and relu non linearity out = to_f.max_pool2d(to_f.relu(out), (2, 2)) # flatten out = out.view(-1, 18 * 6 * 6) # fully connected 1 with relu non linearity out = to_f.relu(self.fc1(out)) # fully connected 2 with relu non linearity out = to_f.relu(self.fc2(out)) # fully connected 3 out = self.fc3(out) return out
در کد بالا یک شبکه عصبی کانولوشنی که در بالا آن را شرح دادیم را ایجاد میکنیم. کلاسی که مینویسیم کلاس پدرش، حتما باید to_nn.Module باشد. در constructor ابتدا کانستراکتور کلاس پدر را فرا میخوانیم سپس لایههایی از شبکه که پارامترهایی قابل یادگیری دارند را تعریف میکنیم.
در تابع forward پردازشهایی که بر روی یک ورودی که به شبکه داده میشود را مشخص میکنیم. معمولا لایههایی که پارامترهای قابل یادگیری ندارند را در این قسمت مینویسیم و از نوشتن آنها در سازندهی کلاس خودداری میکنیم.
# if gpu available use gpu otherwise use cpu device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") print('Device: {}'.format(device))
در این قسمت از کد مشخص میکنیم که میخواهیم از CPU استفاده کنیم یا میخواهیم از GPU استفاده کنیم. در صورت در دسترس بودن جی.پی.یو کد بالا مشخص میکند که میخواهیم از جی.پی.یو استفاده کنیم.
# create and instance of neural network net = ConvNet() net.to(device)
در کد بالا از کلاس شبکهای که ایجاد کردهایم یک شی میسازیم و مشخص میکنیم از جی.پی.یو و یا سی.پی.یو استفاده کند.
# criterion(loss) and optimization algorithm # cross entropy loss criterion = to_nn.CrossEntropyLoss() # adam optimization algorithm, learning rate 0.001 and momentum 0.9 optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)
شبکهی عصبی مان نیاز به یک Loss Function دارد، از تابع هزینه Cross Entropy استفاده میکنیم که معمولا برای دستهبندی نتایج خوبی میدهد. همین طور به الگوریتم بهینهسازی نیازی داریم و برای این کار الگوریتم Stochastic Gradient Descent را انتخاب کردهایم.
# train the convolutional neural network # train for number of epochs for epoch in range(10): # loss of epoch running_loss = 0.0 # iterate over all batches for i, data in enumerate(trainingset_loader, 0): images, labels = data # makes all gradients zero optimizer.zero_grad() # feedforward images = images.to(device) output = net(images) # loss labels = labels.to(device) loss = criterion(output, labels) # backpropagation loss.backward() # update parameters optimizer.step() running_loss += loss.item() if i % 1000 == 999: print('epoch {}, mini-batch {}, loss {}'.format(epoch, i + 1, running_loss/1000.0)) running_loss = 0.0 print('Training Phase is done.')
در کد بالا آموزش شبکه انجام میپذیرد. برای ۱۰ ایپاک شبکه را آموزش میدهیم. zero_grad گرادیانهای شبکه را صفر میکند اگر این کار را انجام ندهیم گرادیانها جمع میشوند و اشتباه رخ میدهد. تابع backward هم کار backpropagation را انجام میدهد و گرادیانها را محاسبه میکند. step نیز با توجه با گرادیانهای محاسبه شده و ضریب یادگیری و … وزنها را بهروزرسانی میکند. در زیر بخشی از خروجی در حین آموزش را مشاهده میکنید:
# show qualitative result of training data_iter = iter(testset_loader) for i in range(3): images, labels_ground_truth = data_iter.next() images = images.to(device) labels_ground_truth = labels_ground_truth.to(device) outputs = net(images) _, predicted = torch.max(outputs, dim=1) matplotlib_imshow(img_tensor=torchvision.utils.make_grid(images), title=['predicted: '+classes[i.item()] for i in predicted]) # accuracy on test set correct = 0 total = 0 with torch.no_grad(): for data in testset_loader: images, ground_truth_labels = data images = images.to(device) ground_truth_labels = ground_truth_labels.to(device) outputs = net(images) _, predicted = torch.max(outputs, dim=1) total += ground_truth_labels.size(0) correct += (ground_truth_labels==predicted).sum().item() print('Accuracy on test set is: {}%'.format(correct/total * 100)) # accuracy of each class in test set class_correct = list(0. for i in range(10)) class_total = list(0. for i in range(10)) with torch.no_grad(): for data in testset_loader: images, labels = data images = images.to(device) labels = labels.to(device) outputs = net(images) _, predicted = torch.max(outputs, 1) c = (predicted == labels).squeeze() for i in range(4): label = labels[i] class_correct[label] += c[i].item() class_total[label] += 1 for i in range(10): print('Accuracy of {}: {}%'.format(classes[i], 100 * class_correct[i] / class_total[i])) plt.show()
در کد بالا بعد از آموزش شبکه، پیشبینی شبکه برای چند ورودی که از مجموعه تست انتخاب شدهاند نمایش داده میشود و سپس دقت بر روی مجموعه تست نمایش داده میشود.
در تصویر بالا پیشبینی شبکه آموزش دیده شده را برای چند تصویر مشاهده میکنید و در زیر دقت شبکه بر روی مجموعه تست را میبینید:
این شبکه را برای کلسیفیکیشن دیتاست CIFAR10 را خیلی سریع ایجاد کردیم و نتایج خوبی گرفتیم با ایجاد تغییر در ساختار و پارامترهای شبکه میتوان دقت را بهبود بخشید.
در زیر کد کلی برنامه را مشاهده میکنید:
import torch import torchvision import numpy import os import torch.utils.data as datautils import matplotlib.pyplot as plt import torch.nn as to_nn import torch.nn.functional as to_f import torch.optim as optim def matplotlib_imshow(img_tensor, title=None): """ Get and image in tensor form and display it. :param img_tensor: :return: """ device = torch.device("cpu") img_tensor_cpu = img_tensor.to(device) std = 0.5 mean = 0.5 img_tensor = std * img_tensor_cpu + mean # convert tensor to numpy img = img_tensor.numpy() # in pytorch image is (3, H, W) # normal form is (H, W, 3) img = img.transpose(1, 2, 0) plt.figure() plt.imshow(img) if title is not None: plt.title(title) ########################################################################### # Read Dataset and show some samples ########################################################################### # transforms that should be done of each image # 1- convert from numpy to tensor # 2- normalize pixel values to real value range [-1,1], for better and easier training image_transforms = torchvision.transforms.Compose([ torchvision.transforms.ToTensor(), torchvision.transforms.Normalize(mean=(0.5, 0.5, 0.5), std=(0.5, 0.5, 0.5)) ]) # load training and test set classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck') cifar10_training_set = torchvision.datasets.CIFAR10(root='./dataset_CIFAR10', train=True, transform=image_transforms, download=True) cifar10_test_set = torchvision.datasets.CIFAR10(root='./dataset_CIFAR10', train=False, transform=image_transforms, download=True) trainingset_loader = datautils.DataLoader(dataset=cifar10_training_set, batch_size=4, shuffle=True, num_workers=2) testset_loader = datautils.DataLoader(dataset=cifar10_test_set, batch_size=4, shuffle=True, num_workers=2) # show some samples of traingset data_iter = iter(trainingset_loader) for i in range(3): images, labels = data_iter.next() matplotlib_imshow(img_tensor=torchvision.utils.make_grid(images), title=['gt: '+classes[i.item()] for i in labels]) ########################################################################### # Create and train a convolutional neural network for cifar-10 dataset ########################################################################### # define a convolutional neural network class ConvNet(to_nn.Module): # constructor of model def __init__(self): # constructor of parent class super(ConvNet, self).__init__() # learnable parameter of model (convolution and fully connected layers) self.conv1 = to_nn.Conv2d(in_channels=3, out_channels=9, kernel_size=(5, 5)) self.conv2 = to_nn.Conv2d(in_channels=9, out_channels=18, kernel_size=(3, 3)) self.fc1 = to_nn.Linear(in_features=18 * 6 * 6, out_features=100) self.fc2 = to_nn.Linear(in_features=100, out_features=40) self.fc3 = to_nn.Linear(in_features=40, out_features=10) # feedforward def forward(self, input): # convolutional layer 1 out = self.conv1(input) # pooling kernel size(2,2) and relu non linearity out = to_f.max_pool2d(to_f.relu(out), (2, 2)) # convolutional layer 2 out = self.conv2(out) # pooling kernel size(2,2) and relu non linearity out = to_f.max_pool2d(to_f.relu(out), (2, 2)) # flatten out = out.view(-1, 18 * 6 * 6) # fully connected 1 with relu non linearity out = to_f.relu(self.fc1(out)) # fully connected 2 with relu non linearity out = to_f.relu(self.fc2(out)) # fully connected 3 out = self.fc3(out) return out # if gpu available use gpu otherwise use cpu device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") print('Device: {}'.format(device)) # create and instance of neural network net = ConvNet() net.to(device) # criterion(loss) and optimization algorithm # cross entropy loss criterion = to_nn.CrossEntropyLoss() # adam optimization algorithm, learning rate 0.001 and momentum 0.9 optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9) # train the convolutional neural network # train for number of epochs for epoch in range(10): # loss of epoch running_loss = 0.0 # iterate over all batches for i, data in enumerate(trainingset_loader, 0): images, labels = data # makes all gradients zero optimizer.zero_grad() # feedforward images = images.to(device) output = net(images) # loss labels = labels.to(device) loss = criterion(output, labels) # backpropagation loss.backward() # update parameters optimizer.step() running_loss += loss.item() if i % 1000 == 999: print('epoch {}, mini-batch {}, loss {}'.format(epoch, i + 1, running_loss/1000.0)) running_loss = 0.0 print('Training Phase is done.') # show qualitative result of training data_iter = iter(testset_loader) for i in range(3): images, labels_ground_truth = data_iter.next() images = images.to(device) labels_ground_truth = labels_ground_truth.to(device) outputs = net(images) _, predicted = torch.max(outputs, dim=1) matplotlib_imshow(img_tensor=torchvision.utils.make_grid(images), title=['predicted: '+classes[i.item()] for i in predicted]) # accuracy on test set correct = 0 total = 0 with torch.no_grad(): for data in testset_loader: images, ground_truth_labels = data images = images.to(device) ground_truth_labels = ground_truth_labels.to(device) outputs = net(images) _, predicted = torch.max(outputs, dim=1) total += ground_truth_labels.size(0) correct += (ground_truth_labels==predicted).sum().item() print('Accuracy on test set is: {}%'.format(correct/total * 100)) # accuracy of each class in test set class_correct = list(0. for i in range(10)) class_total = list(0. for i in range(10)) with torch.no_grad(): for data in testset_loader: images, labels = data images = images.to(device) labels = labels.to(device) outputs = net(images) _, predicted = torch.max(outputs, 1) c = (predicted == labels).squeeze() for i in range(4): label = labels[i] class_correct[label] += c[i].item() class_total[label] += 1 for i in range(10): print('Accuracy of {}: {}%'.format(classes[i], 100 * class_correct[i] / class_total[i])) plt.show()