Описание набора данных рукописных цифр MNIST и Keras-реализации нейронных сетей (НС), классифицирующие эти цифры, можно найти, например, здесь или здесь.
Здесь же в интересах учебного процесса приводится PyTorch-реализация классификатора MNIST. За формирование модели НС отвечает класс Net:
import torch
import torch.nn as nn
import torch.nn.functional as F
#
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(1, 16, kernel_size = 3)
self.conv2 = nn.Conv2d(16, 32, kernel_size = 3)
self.max_pool2d1 = nn.MaxPool2d(2)
self.max_pool2d2 = nn.MaxPool2d(2)
self.conv2_drop = nn.Dropout2d(p = 0.3)
self.dropout = nn.Dropout(p = 0.3)
self.fc1 = nn.Linear(800, 32)
self.fc2 = nn.Linear(32, 10)
def forward(self, x):
x = self.conv1(x)
x = F.relu(x)
x = self.max_pool2d1(x)
x = self.conv2_drop(x)
x = self.conv2(x)
x = F.relu(x)
x = self.max_pool2d2(x) # torch.Size([256, 32, 5, 5])
# x = F.relu(F.max_pool2d(self.conv1(x), 2))
# x = F.relu(F.max_pool2d(self.conv2_drop(self.conv2(x)), 2)) # 204800
# x = F.relu(self.conv2_drop(self.conv2(x))) # 991232
x = x.view(-1, 800) # torch.Size([256, 800]) (32 * 5 * 5 = 800)
x = self.dropout(x)
x = F.relu(self.fc1(x))
# x = F.dropout(x, training = self.training) # self.training = True при обучении
x = self.fc2(x)
return F.log_softmax(x, dim = -1)
model = Net() # Формируем модель НС
print(model)
НС содержит 2 сверточных слоя с активации Relu, после каждого слоя следует слой подвыборки. Второму сверточному слою предшествует слой прореживания Dropout2d. После прохождения этих слоев данные преобразуются в одномерные (x = x.view(-1, 800)) и передаются слою прореживания Dropout, а затем полносвязному слою с функцией активации Relu, далее следует полносвязный классифицирующий слой с функцией активации log_softmax. На выходе НС генерирует вероятности принадлежности образа цифры к одному из 10 классов. Класс цифры определяется по большей вероятности.
На вход НС подается пакет, содержащий пары изображение – метка.
Описание используемой модели НС (model) после применения print(model):
Net(
(conv1): Conv2d(1, 16, kernel_size=(3, 3), stride=(1, 1))
(conv2): Conv2d(16, 32, kernel_size=(3, 3), stride=(1, 1))
(max_pool2d1): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(max_pool2d2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(conv2_drop): Dropout2d(p=0.3, inplace=False)
(dropout): Dropout(p=0.3, inplace=False)
(fc1): Linear(in_features=800, out_features=32, bias=True)
(fc2): Linear(in_features=32, out_features=10, bias=True)
)
Обучающие и проверочные данные набора MNIST можно ввести из torchvision datasets (на примере проверочных данных):
# Загрузка MNIST из torchvision datasets
import torch
from torchvision import datasets
from torchvision.transforms import ToTensor
import matplotlib.pyplot as plt
# import numpy as np
download = True
test_data = datasets.MNIST(root = 'data', train = False,
download = download, transform = ToTensor())
print(type(test_data)) # <class 'torchvision.datasets.mnist.MNIST'>
fg = test_data[0] # <class 'tuple>>
ind = fg[1] # 7
img = fg[0].data # shape = (1, 28, 28) # <class 'torch.Tensor'>
# img = np.squeeze(img) # или: img = img.reshape(28, 28)
img = img.squeeze()
plt.imshow(img, cmap = plt.get_cmap('gray'))
# Если не употреблять transform = ToTensor():
# img = test_data[0][0] # shape = (28, 28) # <class 'PIL.Image.Image'>
# plt.imshow(img, cmap = plt.get_cmap('gray'))
plt.title(ind)
plt.axis('off')
plt.show()
Этот же код выводит изображение первой цифры в test_data.
Каждый элемент test_data – это пара изображение – метка. Изображение будет представлено в виде вещественного тензора, если задан параметр
transform = ToTensor()
В противном случае изображение – это PIL.Image.Image.
Пары изображение – метка несложно составить, не обращаясь к torchvision datasets. Для учебного процесса MNIST записан в двоичные файлы, загрузку которых выполняет следующий код:
import numpy as np
import matplotlib.pyplot as plt
def load_bin_data(pathToData, img_rows, img_cols,
num_classes, show_img, useTestData):
print('Загрузка данных из двоичных файлов')
with open(pathToData + 'imagesTrain.bin', 'rb') as read_binary:
x_trn = np.fromfile(read_binary, dtype = np.uint8) # <class 'numpy.ndarray'>
with open(pathToData + 'labelsTrain.bin', 'rb') as read_binary:
y_trn = np.fromfile(read_binary, dtype = np.uint8)
with open(pathToData + 'imagesTest.bin', 'rb') as read_binary:
x_tst = np.fromfile(read_binary, dtype = np.uint8)
with open(pathToData + 'labelsTest.bin', 'rb') as read_binary:
y_tst = np.fromfile(read_binary, dtype = np.uint8)
# print(x_trn.shape) # (47040000,)
# print(y_trn.shape) # (60000,)
x_trn_shape = int(x_trn.shape[0] / (img_rows * img_cols)) # 60000
x_tst_shape = int(x_tst.shape[0] / (img_rows * img_cols)) # 10000
x_trn = x_trn.reshape(x_trn_shape, 1, img_rows, img_cols)
x_tst = x_tst.reshape(x_tst_shape, 1, img_rows, img_cols)
# print(x_trn.shape) # (60'000, 1, 28, 28)
# print(y_trn.shape) # (60'000,)
# print(x_tst.shape) # (10'000, 1, 28, 28)
# print(y_tst.shape) # (10'000,)
if show_img:
print('Примеры', 'тестовых' if useTestData else 'обучающих', 'данных')
# Выводим 9 изображений обучающего или тестового набора
for i in range(9):
plt.subplot(3, 3, i + 1)
ind = y_tst[i] if useTestData else y_trn[i]
img = x_tst[i] if useTestData else x_trn[i]
img = img.reshape(img_rows, img_cols)
plt.imshow(img, cmap = plt.get_cmap('gray'))
plt.title(ind)
plt.axis('off')
plt.subplots_adjust(hspace = 0.5) # wspace
plt.show()
# Преобразование целочисленных данных в float32 и приведение к диапазону [0.0, 1.0]
x_trn = np.array(x_trn, dtype = 'float32') / 255
x_tst = np.array(x_tst, dtype = 'float32') / 255
# print(y_trn[0]) # (MNIST) Напечатает: 5
return x_trn, y_trn, x_tst, y_tst
Параметры загрузки и подготовки данных:
pathToData = 'G:/AM/НС/mnist/'
img_rows = img_cols = 28
num_classes = 10
show_img = not True
useTestData = True
batch_size = 256 # Размер обучающего (проверочного) пакета
Загрузка данных:
x_trn, y_trn, x_tst, y_tst = load_bin_data(pathToData, img_rows, img_cols,
num_classes, show_img, useTestData)
Подготовка данных. Формируем пары изображение – метка:
trn_data = [[x, int(y)] for x, y in zip(x_trn, y_trn)]
tst_data = [[x, int(y)] for x, y in zip(x_tst, y_tst)]
Создаем обучающие и проверочные пакеты:
from torch.utils.data import DataLoader
trn_loader = DataLoader(trn_data, batch_size = batch_size, shuffle = True)
tst_loader = DataLoader(tst_data, batch_size = batch_size, shuffle = True)
Если указать show_img = True, то увидим первое изображение первого обучающего пакета:
if show_img:
trn_features, trn_labels = next(iter(trn_loader))
img = trn_features[0].squeeze()
ind = int(trn_labels[0])
plt.imshow(img, cmap = "gray")
plt.title(ind)
plt.axis('off')
plt.show()
Эпоху обучения реализует процедура train:
def train(epoch):
trn_loss = tst_loss = 0
model.train() # Режим обучения
for batch_no, (data, target) in enumerate(trn_loader):
optimizer.zero_grad()
output = model(data)
loss = criterion(output, target)
loss.backward()
optimizer.step()
trn_loss += loss.item() * data.size(0)
# if batch_no % prn_step == 0:
# print('Эпоха: {} [{}/{} ({:.0f}%)]\tПотери: {:.6f}'.format(
# epoch, batch_no * len(data), len(trn_loader.dataset),
# 100 * batch_no / len(trn_loader), loss.item()))
model.eval() # Режим оценки
for data, target in tst_loader:
output = model(data)
loss = criterion(output, target)
tst_loss += loss.item() * data.size(0)
trn_loss = trn_loss / in_trn
tst_loss = tst_loss / in_tst
print('Эпоха: {} Потери: обучение: {:.6f} \tпроверка: {:.6f}'.format(
epoch + 1, trn_loss, tst_loss))
При обучении вычисляются потери на обучающем множестве как усредненные потери на каждом обучающем пакете. Функция потерь – перекрестная энтропия.
После эпохи обучения выполняется оценка модели на проверочном множестве: вычисляются потери.
Параметры обучения:
train_model = True
save_model = True
load_model = True
fn_w = 'model.wei' # Файл с весами НС
prn_step = 50
n_epochs = 15 # Число эпох обучения
criterion = nn.CrossEntropyLoss() # Функция потерь
# Число примеров в обучающем и проверочном множествах
in_trn = len(trn_loader.sampler) # или: len(trn_loader.dataset) - 60000
in_tst = len(tst_loader.sampler) # или: len(y_tst) – 10000
Перед обучением (если продолжаем процесс обучения) и/или проверкой можно загрузить ранее сохраненные параметры (веса) НС:
import torch
import torch.optim as optim
if load_model:
print('Загрузка весов из файла', fn_w)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model.load_state_dict(torch.load(fn_w, map_location = torch.device(device)))
Обучение (если save_model = True, то веса НС будут записаны в файл):
# optimizer = torch.optim.SGD(model.parameters(), lr = 0.01)
optimizer = torch.optim.Adam(model.parameters(), lr = 0.01) # betas = (0.9, 0.98), eps = 1e-9
print('Обучение')
for ep in range(n_epochs):
train(ep)
if save_model:
print('Сохранение весов модели')
torch.save(model.state_dict(), fn_w)
После обучения или, если train_model = False, сразу после загрузки весов НС переходим к проверке, в процессе которой вычисляются потери и точность на проверочном множестве, после чего выводятся 20 изображений первого пакет проверочного множества с указанием в заголовке предсказанных и реальных классов (в скобках) рукописных цифр.
Проверку обеспечивает процедура test:
def test():
print('Проверка')
tst_loss = 0 # Потери на проверочном множестве (ПМ)
# Число верно классифицированных цифр в каждом классе (ПМ)
cls_correct = [0]*num_classes
# Число цифр в каждом классе (ПМ)
cls_total = [0]*num_classes
model.eval() #Режим оценки
for data, target in tst_loader:
output = model(data)
loss = criterion(output, target)
tst_loss += loss.item() * data.size(0)
_, pred = torch.max(output, 1)
# print(pred) # tensor([0, 7, 0, 0, 0, 7, 0,
# print(target.data) # tensor([2, 7, 4, 8, 0, 7, 6,
correct = np.squeeze(pred.eq(target.data.view_as(pred)))
# print(correct) # tensor([False, True, False, False, True, True, False,
for i in range(len(target)):
label = target.data[i]
# print(correct[i]) # tensor(False)
# print(correct[i].item()) # False
cls_correct[label] += correct[i].item()
cls_total[label] += 1
# Средние потери
tst_loss /= len(tst_loader.sampler)
print('Потери: {:.6f}'.format(tst_loss))
print('Точность: {:.4f}'.format(sum(cls_correct) / sum(cls_total)))
print('Точность по классам:')
cls = 0
for cc, ct in zip(cls_correct, cls_total):
print('{} - {:.4f}'.format(cls, cc / ct))
cls += 1
Процедура вывода начальной части первого проверочного пакета с указанием в заголовке рисунков результатов классификации:
def show_res():
print('Смотрим начальный кусок первого пакета')
data_iter = iter(tst_loader) # Первый пакет
images, labels = data_iter.next()
output = model(images)
_, pred = torch.max(output, 1)
images = images.numpy()
fig = plt.figure(figsize = (12, 3))
N = 20
for i in range(N):
ax = fig.add_subplot(2, int(N / 2), i + 1, xticks = [], yticks = [])
img = images[i]
img = np.squeeze(img) # или: img = img.reshape(img_rows, img_cols)
ax.imshow(img, cmap = 'gray')
ttl = str(int(pred[i].item())) + ' (' + str(labels[i].item()) + ')'
clr = 'green' if pred[i] == labels[i] else 'red'
ax.set_title(ttl, color = clr)
plt.show()
Проверка и вывод рисунков:
test() # Проверка
show_res() # Смотрим начальный кусок первого пакета
С использованием приведенных сведений студентам надлежит:
from sys import exit
import matplotlib.pyplot as plt
import numpy as np
#import time
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.utils.data import DataLoader
#
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(1, 16, kernel_size = 3)
self.conv2 = nn.Conv2d(16, 32, kernel_size = 3)
self.max_pool2d1 = nn.MaxPool2d(2)
self.max_pool2d2 = nn.MaxPool2d(2)
self.conv2_drop = nn.Dropout2d(p = 0.3)
self.dropout = nn.Dropout(p = 0.3)
self.fc1 = nn.Linear(800, 32)
self.fc2 = nn.Linear(32, 10)
def forward(self, x):
x = self.conv1(x)
x = F.relu(x)
x = self.max_pool2d1(x)
x = self.conv2_drop(x)
x = self.conv2(x)
x = F.relu(x)
x = self.max_pool2d2(x) # torch.Size([256, 32, 5, 5])
# x = F.relu(F.max_pool2d(self.conv1(x), 2))
# x = F.relu(F.max_pool2d(self.conv2_drop(self.conv2(x)), 2)) # 204800
# x = F.relu(self.conv2_drop(self.conv2(x))) # 991232
x = x.view(-1, 800) # torch.Size([256, 800]) (32 * 5 * 5 = 800)
x = self.dropout(x)
x = F.relu(self.fc1(x))
# x = F.dropout(x, training = self.training) # self.training = True при обучении
x = self.fc2(x)
return F.log_softmax(x, dim = -1)
model = Net() # Формируем модель НС
print(model)
#
def load_bin_data(pathToData, img_rows, img_cols,
num_classes, show_img, useTestData):
print('Загрузка данных из двоичных файлов')
with open(pathToData + 'imagesTrain.bin', 'rb') as read_binary:
x_trn = np.fromfile(read_binary, dtype = np.uint8) # <class 'numpy.ndarray'>
with open(pathToData + 'labelsTrain.bin', 'rb') as read_binary:
y_trn = np.fromfile(read_binary, dtype = np.uint8)
with open(pathToData + 'imagesTest.bin', 'rb') as read_binary:
x_tst = np.fromfile(read_binary, dtype = np.uint8)
with open(pathToData + 'labelsTest.bin', 'rb') as read_binary:
y_tst = np.fromfile(read_binary, dtype = np.uint8)
# print(x_trn.shape) # (47040000,)
# print(y_trn.shape) # (60000,)
x_trn_shape = int(x_trn.shape[0] / (img_rows * img_cols)) # 60000
x_tst_shape = int(x_tst.shape[0] / (img_rows * img_cols)) # 10000
x_trn = x_trn.reshape(x_trn_shape, 1, img_rows, img_cols)
x_tst = x_tst.reshape(x_tst_shape, 1, img_rows, img_cols)
# print(x_trn.shape) # (60'000, 1, 28, 28)
# print(y_trn.shape) # (60'000,)
# print(x_tst.shape) # (10'000, 1, 28, 28)
# print(y_tst.shape) # (10'000,)
if show_img:
print('Примеры', 'тестовых' if useTestData else 'обучающих', 'данных')
# Выводим 9 изображений обучающего или тестового набора
for i in range(9):
plt.subplot(3, 3, i + 1)
ind = y_tst[i] if useTestData else y_trn[i]
img = x_tst[i] if useTestData else x_trn[i]
img = img.reshape(img_rows, img_cols)
plt.imshow(img, cmap = plt.get_cmap('gray'))
plt.title(ind)
plt.axis('off')
plt.subplots_adjust(hspace = 0.5) # wspace
plt.show()
# Преобразование целочисленных данных в float32 и приведение к диапазону [0.0, 1.0]
x_trn = np.array(x_trn, dtype = 'float32') / 255
x_tst = np.array(x_tst, dtype = 'float32') / 255
# print(y_trn[0]) # (MNIST) Напечатает: 5
return x_trn, y_trn, x_tst, y_tst
#
pathToData = 'G:/AM/НС/mnist/'
img_rows = img_cols = 28
num_classes = 10
show_img = not True
useTestData = True
batch_size = 256 # Размер обучающего (проверочного) пакета
train_model = not True
save_model = True
load_model = True
fn_w = 'model.wei' # Файл с весами НС
prn_step = 50
n_epochs = 15 # Число эпох обучения
criterion = nn.CrossEntropyLoss() # Функция потерь
#
x_trn, y_trn, x_tst, y_tst = load_bin_data(pathToData, img_rows, img_cols,
num_classes, show_img, useTestData)
trn_data = [[x, int(y)] for x, y in zip(x_trn, y_trn)]
tst_data = [[x, int(y)] for x, y in zip(x_tst, y_tst)]
# Формирование обучающих и проверочных пакетов
trn_loader = DataLoader(trn_data, batch_size = batch_size, shuffle = True)
tst_loader = DataLoader(tst_data, batch_size = batch_size, shuffle = True)
# Число примеров в обучающем и проверочном множествах
in_trn = len(trn_loader.sampler) # или: len(trn_loader.dataset) - 60000
in_tst = len(tst_loader.sampler) # или: len(y_tst) - 10000
# print(len(trn_loader)) # 235 - число пакетов в обучающем множестве
if show_img:
trn_features, trn_labels = next(iter(trn_loader))
img = trn_features[0].squeeze()
ind = int(trn_labels[0])
plt.imshow(img, cmap = "gray")
plt.title(ind)
plt.axis('off')
plt.show()
def train(epoch):
trn_loss = tst_loss = 0
model.train() # Режим обучения
for batch_no, (data, target) in enumerate(trn_loader):
optimizer.zero_grad()
output = model(data)
loss = criterion(output, target)
loss.backward()
optimizer.step()
trn_loss += loss.item() * data.size(0)
# if batch_no % prn_step == 0:
# print('Эпоха: {} [{}/{} ({:.0f}%)]\tПотери: {:.6f}'.format(
# epoch, batch_no * len(data), len(trn_loader.dataset),
# 100 * batch_no / len(trn_loader), loss.item()))
model.eval() # Режим оценки
for data, target in tst_loader:
output = model(data)
loss = criterion(output, target)
tst_loss += loss.item() * data.size(0)
trn_loss = trn_loss / in_trn
tst_loss = tst_loss / in_tst
print('Эпоха: {} Потери: обучение: {:.6f} \tпроверка: {:.6f}'.format(
epoch + 1, trn_loss, tst_loss))
if load_model:
print('Загрузка весов из файла', fn_w)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model.load_state_dict(torch.load(fn_w, map_location = torch.device(device)))
if train_model:
# optimizer = torch.optim.SGD(model.parameters(), lr = 0.01)
optimizer = torch.optim.Adam(model.parameters(), lr = 0.01) # betas = (0.9, 0.98), eps = 1e-9
print('Обучение')
for ep in range(n_epochs):
train(ep)
if save_model:
print('Сохранение весов модели')
torch.save(model.state_dict(), fn_w)
def test():
print('Проверка')
tst_loss = 0 # Потери на проверочном множестве (ПМ)
# Число верно классифицированных цифр в каждом классе (ПМ)
cls_correct = [0]*num_classes
# Число цифр в каждом классе (ПМ)
cls_total = [0]*num_classes
model.eval() #Режим оценки
for data, target in tst_loader:
output = model(data)
loss = criterion(output, target)
tst_loss += loss.item() * data.size(0)
_, pred = torch.max(output, 1)
# print(pred) # tensor([0, 7, 0, 0, 0, 7, 0,
# print(target.data) # tensor([2, 7, 4, 8, 0, 7, 6,
correct = np.squeeze(pred.eq(target.data.view_as(pred)))
# print(correct) # tensor([False, True, False, False, True, True, False,
for i in range(len(target)):
label = target.data[i]
# print(correct[i]) # tensor(False)
# print(correct[i].item()) # False
cls_correct[label] += correct[i].item()
cls_total[label] += 1
# Средние потери
tst_loss /= len(tst_loader.sampler)
print('Потери: {:.6f}'.format(tst_loss))
print('Точность: {:.4f}'.format(sum(cls_correct) / sum(cls_total)))
print('Точность по классам:')
cls = 0
for cc, ct in zip(cls_correct, cls_total):
print('{} - {:.4f}'.format(cls, cc / ct))
cls += 1
def show_res():
print('Смотрим начальный кусок первого пакета')
data_iter = iter(tst_loader) # Первый пакет
images, labels = data_iter.next()
output = model(images)
_, pred = torch.max(output, 1)
images = images.numpy()
fig = plt.figure(figsize = (12, 3))
N = 20
for i in range(N):
ax = fig.add_subplot(2, int(N / 2), i + 1, xticks = [], yticks = [])
img = images[i]
img = np.squeeze(img) # или: img = img.reshape(img_rows, img_cols)
ax.imshow(img, cmap = 'gray')
ttl = str(int(pred[i].item())) + ' (' + str(labels[i].item()) + ')'
clr = 'green' if pred[i] == labels[i] else 'red'
ax.set_title(ttl, color = clr)
plt.show()
test() # Проверка
show_res() # Смотрим начальный кусок первого пакета