底层实现多层感知器——【torch学习笔记】

从底层实现多层感知器

引用翻译:《动手学深度学习》

现在我们知道多层感知器(MLPs)在理论上是如何工作的,让我们来实现它们。首先,我们导入所需的包。

import sys
sys.path.insert(0, '..')
import os
import torch
import torch.nn as nn
import  numpy as np

使用Fashion-MNIST图像分类数据集。

导入数据集

import torchvision
from torchvision import transforms
from torch.utils.data import DataLoader

def load_data_fashion_mnist(batch_size, resize=None, root=os.path.join(
        '~', '.pytorch', 'datasets', 'fashion-mnist')):
    """下载数据集并存入内存."""
    root = os.path.expanduser(root)
    print(root)
    transformer = []
    if resize:
        transformer += [transforms.Resize(resize)]
    transformer += [transforms.ToTensor()]
    transformer = transforms.Compose(transformer)
    
    # 如果存在数据集则不再下载,若没有则下载
    mnist_train = torchvision.datasets.FashionMNIST(root=root, train=True, transform=transformer, download=True)
    mnist_test = torchvision.datasets.FashionMNIST(root=root, train=False, transform=transformer, download=True)
    num_workers = 0 if sys.platform.startswith('win32') else 4
    
    # 使用封装好的DataLoader载入即可。
    train_iter = DataLoader(mnist_train, batch_size, shuffle=True, num_workers=num_workers)
    test_iter = DataLoader(mnist_test, batch_size, shuffle=False, num_workers=num_workers)
    return mnist_train,mnist_test,train_iter, test_iter
batch_size = 256
mnist_train,mnist_test,train_iter, test_iter = load_data_fashion_mnist(batch_size)
/data/.pytorch/datasets/fashion-mnist

其中数据集样式:
dataset与dataloader数据格式一致,只是dataloader会对数据进行划分batch进行随机输出。

普通dataset转化为dataloader时,输入的是(data,label)这样类似的多个数组。

mnist_train
Dataset FashionMNIST
    Number of datapoints: 60000
    Root location: /data/.pytorch/datasets/fashion-mnist
    Split: Train
    StandardTransform
Transform: Compose(
               ToTensor()
           )

dataloader样式:

# 遍历数据
for test_data,label in train_iter:
    pass  
# 标签列表
print(label[1])
tensor(6)
# 28X28数据,后期将转化为784维单列数据
print(test_data[1][0][0])  # 其中某一子列表
print(len(test_data[1][0]))  # 子列表数量
tensor([0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000,
        0.0000, 0.0039, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000,
        0.0000, 0.0745, 0.0353, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000,
        0.0000])
28

一、初始化模型参数

回顾一下,这个数据集包含10个类,每张图片由28×28=784个像素值的网格组成。由于我们将摒弃空间结构(目前),我们可以把它看作是一个具有784个输入特征和10个类别的分类数据集。具体来说,我们将用一个隐藏层和256个隐藏单元来实现我们的MLP。请注意,我们可以把这两个选择视为超参数,可以根据验证数据的表现来设置。通常情况下,我们会选择层宽为2的幂,以使一切在内存中很好地对齐。

二、激活函数

为了确保我们知道一切是如何运作的,我们将使用最大函数来自己实现ReLU,而不是直接调用nn.ReLU。

class Net(nn.Module):
    def __init__(self):
        super(Net,self).__init__()
        
        self.W1 = nn.Parameter(torch.randn(784,256,requires_grad=True)*0.01)  # 经过第一层变换,将784维数据映射为256维
        self.b1 = nn.Parameter(torch.zeros(256,requires_grad=True))  # 和W1的列数相同
        self.W2 = nn.Parameter(torch.randn(256,10,requires_grad=True)*0.01)  # 经过第一层后,存在256维信息
        self.b2 = nn.Parameter(torch.zeros(10,requires_grad=True))  # 和输出10个类别相对应
        
    def forward(self, X):
        X = X.reshape((-1, 784))  # 将图像数据转化为784维度数据,便于后续运算
        # len(X[0])为784维,即将28X28拉直
        H=self.relu(X@self.W1 + self.b1)   # 这里'@'代表点乘运算
        return (H@self.W2 + self.b2)  # 即通过两层,得到最后的输出。
    
    def relu(self, s):
        a=torch.zeros_like(s)  #  torch.zeros_like:生成和括号内变量维度维度一致的全是零的内容。
        return torch.max(s, a)  # 返回与0相比的最大值。即relu的函数实现

三、建立模型

如同在softmax回归中一样,我们将把每个二维图像重塑为一个长度为num_inputs的平面向量。最后,我们只用几行代码来实现我们的模型。

net=Net() # 初始化模型,即调用Net()函数建立模型即可

四、损失函数

为了获得更好的数值稳定性,并且因为我们已经知道如何完全从头实现softmax回归,我们将使用torch的集成函数来计算softmax和交叉熵损失。

nn.CrossEntropyLoss()该损失函数结合了nn.LogSoftmax()和nn.NLLLoss()两个函数。它在做分类(具体几类)训练的时候是非常有用的。在训练过程中,对于每个类分配权值,可选的参数权值应该是一个1D张量。当你有一个不平衡的训练集时,这是是非常有用的。

# 建立交叉熵损失函数
criterion = nn.CrossEntropyLoss()

五、模型训练

训练MLP的步骤与softmax回归的步骤没有区别。在d2l包中,我们直接调用train_ch3函数。我们将历时数设置为10,学习率设置为0.5。

评估分类准确率的函数,通过混淆矩阵计算准确率

def evaluate_accuracy(data_iter, net, device=torch.device('cpu')):
    """在给定的数据集上评估一个模型的准确."""
    net.eval()  # 转化为评估模式 for Dropout, BatchNorm etc layers.
    acc_sum, n = torch.tensor([0], dtype=torch.float32, device=device), 0  # 将准确率,n初始化为0
    for X, y in data_iter:
        # 复制数据传入至设备
        X, y = X.to(device), y.to(device)
        with torch.no_grad():
            y = y.long()
            # 如label对应的上则计数加一
            acc_sum += torch.sum((torch.argmax(net(X), dim=1) == y))
            n += y.shape[0]
        # 计算得到准确率
    return acc_sum.item()/n
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
from torch.autograd import Variable
import numpy as np
import math
import time
def train_ch3(net, train_iter, test_iter, criterion, num_epochs, batch_size, lr=None):
    """使用CPU训练或评估模型."""
    optimizer = optim.SGD(net.parameters(), lr=lr)
    for epoch in range(num_epochs):
        train_l_sum, train_acc_sum, n = 0.0, 0.0, 0
        for X, y in train_iter:
            # 即将梯度初始化为零
            optimizer.zero_grad()
            # 通过训练的网络net处理该批次数据集X,得到预测的类别输出
            y_hat = net(X)
            # 通过预测的类别 ,结合真实类别,通过交叉熵计算损失函数
            loss = criterion(y_hat, y)
            # loss.backward()函数的作用是根据loss来计算网络参数的梯度,其对应的输入默认为网络的叶子节点
            loss.backward()
            # 所有的optimizer都实现了step()方法,这个方法会更新所有的参数
            optimizer.step()
            # 将真实类别转化为数值型
            y = y.type(torch.float32)
            # 计算损失的总和,pytorch中的.item()用于将一个零维张量转换成浮点数。可以减少gpu内存消耗
            train_l_sum += loss.item()
            # 如果真实label和预测的y_hat相同,则计数正确一个,以此计算训练集准确率
            train_acc_sum += torch.sum((torch.argmax(y_hat, dim=1).type(torch.FloatTensor) == y).detach()).float()
            n += list(y.size())[0]
        # 通过评估函数计算测试集的准确率
        test_acc = evaluate_accuracy(test_iter, net)  
        print('epoch %d, loss %.4f, train acc %.3f, test acc %.3f'\
            % (epoch + 1, train_l_sum / n, train_acc_sum / n, test_acc))
num_epochs, lr = 10, 0.5  # 设定迭代次数和学习率
train_ch3(net, train_iter, test_iter, criterion, num_epochs, batch_size, lr)
epoch 1, loss 0.0032, train acc 0.699, test acc 0.768
epoch 2, loss 0.0019, train acc 0.819, test acc 0.815
epoch 3, loss 0.0017, train acc 0.841, test acc 0.821
epoch 4, loss 0.0015, train acc 0.855, test acc 0.845
epoch 5, loss 0.0015, train acc 0.861, test acc 0.831
epoch 6, loss 0.0014, train acc 0.869, test acc 0.852
epoch 7, loss 0.0013, train acc 0.875, test acc 0.858
epoch 8, loss 0.0013, train acc 0.881, test acc 0.863
epoch 9, loss 0.0012, train acc 0.883, test acc 0.869
epoch 10, loss 0.0012, train acc 0.885, test acc 0.864

为了看看我们做得如何,让我们把这个模型应用于一些测试数据。可以将结果与相应的线性模型进行比较。

# 获取图片标签映射函数
def get_fashion_mnist_labels(labels):
    """Get text labels for Fashion-MNIST."""
    text_labels = ['t-shirt', 'trouser', 'pullover', 'dress', 'coat',
                   'sandal', 'shirt', 'sneaker', 'bag', 'ankle boot']
    return [text_labels[int(i)] for i in labels]
# 可视化图像函数
from matplotlib import pyplot as plt
def use_svg_display():
    """Use svg format to display plot in jupyter."""
    display.set_matplotlib_formats('svg')
def show_fashion_mnist(images, labels):
    """Plot Fashion-MNIST images with labels."""
    use_svg_display()
    _, figs = plt.subplots(1, len(images), figsize=(12, 12))
    for f, img, lbl in zip(figs, images, labels):
        f.imshow(img.reshape((28, 28)).numpy())
        f.set_title(lbl)
        f.axes.get_xaxis().set_visible(False)
        f.axes.get_yaxis().set_visible(False)
# 对结果进行可视化
for X, y in test_iter:
    # 这一步是读取test_iter中一个批次的数据
    break
true_labels = get_fashion_mnist_labels(y.numpy())
pred_labels = get_fashion_mnist_labels(np.argmax(net(X).data.numpy(), axis=1))
titles = [truelabel + '\n' + predlabel for truelabel, predlabel in zip(true_labels, pred_labels)]
d2l.show_fashion_mnist(X[0:9], titles[0:9])

这看起来比我们之前的结果要好一些,这是一个好的迹象,表明我们在正确的道路上。

六、总结

我们看到,实现一个简单的MLP很容易,即使是手动完成。但是,如果有大量的层,这可能会变得很混乱(例如,命名和跟踪模型参数等)。

七、练习

1、改变超参数num_hiddens的值,以便看看这个超参数如何影响你的结果。

2、尝试添加一个新的隐藏层,看看它对结果有什么影响。

3、改变学习率是如何改变结果的。

4、通过对所有的参数(学习率、迭代、隐藏层数、每层隐藏单元数)进行优化,你能得到的最佳结果是什么?

多层感知器的简洁实现

现在我们已经了解了多层感知器(MLPs)在理论上是如何工作的,现在使用torch封装好的部分来实现它们。

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

一、建立模型

与我们的softmax回归实现的唯一区别是,我们增加了两个Dense(全连接)层而不是一个。第一个是我们的隐藏层,它有256个隐藏单元,使用ReLU激活函数。

class Net(nn.Module):
    def __init__(self, num_inputs = 784, num_outputs = 10, num_hiddens = 256, is_training = True):
        super(Net, self).__init__()
        
        self.num_inputs = num_inputs
        self.num_outputs = num_outputs
        self.num_hiddens = num_hiddens
        
        self.linear_1 = nn.Linear(num_inputs, num_hiddens)
        self.linear_2 = nn.Linear(num_hiddens, num_outputs)
        
        self.relu = nn.ReLU()
    def forward(self, X):
        X = X.reshape((-1, self.num_inputs))
        H1 = self.relu(self.linear_1(X))
        out = self.linear_2(H1)
        return out   
    
net = Net()
print(net) 
Net(
  (linear_1): Linear(in_features=784, out_features=256, bias=True)
  (linear_2): Linear(in_features=256, out_features=10, bias=True)
  (relu): ReLU()
)

训练模型的步骤与我们的softmax回归实现完全相同。

num_epochs, lr, batch_size = 10, 0.5, 256
train_iter, test_iter = load_data_fashion_mnist(batch_size)  # 导入数据,和之前一致
criterion = nn.CrossEntropyLoss()
train_ch3(net, train_iter, test_iter, criterion, num_epochs, batch_size, lr)
epoch 1, loss 0.0029, train acc 0.725, test acc 0.813
epoch 2, loss 0.0019, train acc 0.823, test acc 0.824
epoch 3, loss 0.0016, train acc 0.845, test acc 0.826
epoch 4, loss 0.0015, train acc 0.858, test acc 0.849
epoch 5, loss 0.0014, train acc 0.869, test acc 0.852
epoch 6, loss 0.0014, train acc 0.870, test acc 0.828
epoch 7, loss 0.0013, train acc 0.878, test acc 0.858
epoch 8, loss 0.0012, train acc 0.883, test acc 0.861
epoch 9, loss 0.0012, train acc 0.887, test acc 0.870
epoch 10, loss 0.0012, train acc 0.889, test acc 0.852

二、练习

1、试着多加几个隐藏层,看看结果如何变化。

class Net(nn.Module):
    def __init__(self, num_inputs = 784, num_outputs = 10, num_hiddens = 256, num_hiddens2 = 128,is_training = True):
        super(Net, self).__init__()
        self.num_inputs = num_inputs
        self.num_outputs = num_outputs
        self.num_hiddens = num_hiddens
        self.num_hiddens2 = num_hiddens2
        self.linear_1 = nn.Linear(num_inputs, num_hiddens)
        self.linear_3 = nn.Linear(num_hiddens, self.num_hiddens2)  # 增加一层隐藏层
        self.linear_2 = nn.Linear(self.num_hiddens2, num_outputs)
        self.relu = nn.ReLU()
    def forward(self, X):
        X = X.reshape((-1, self.num_inputs))
        H1 = self.relu(self.linear_1(X))
        H2 = self.linear_3(H1)  # 增加一层隐藏层,注意维度一致
        out = self.linear_2(H2)
        return out   
net = Net()
num_epochs, lr, batch_size = 10, 0.5, 256
criterion = nn.CrossEntropyLoss()
train_ch3(net, train_iter, test_iter, criterion, num_epochs, batch_size, lr)

结果:

epoch 1, loss 0.0048, train acc 0.555, test acc 0.733
epoch 2, loss 0.0023, train acc 0.784, test acc 0.799
epoch 3, loss 0.0019, train acc 0.816, test acc 0.827
epoch 4, loss 0.0018, train acc 0.835, test acc 0.830
epoch 5, loss 0.0016, train acc 0.847, test acc 0.833
epoch 6, loss 0.0016, train acc 0.848, test acc 0.798
epoch 7, loss 0.0015, train acc 0.855, test acc 0.832
epoch 8, loss 0.0015, train acc 0.861, test acc 0.833
epoch 9, loss 0.0014, train acc 0.864, test acc 0.837
epoch 10, loss 0.0014, train acc 0.867, test acc 0.832

2、试试不同的激活函数。哪些函数效果最好?

# 都在2层隐藏层的情况下
# relu激活函数
self.relu = nn.ReLU()
H1 = self.relu(self.linear_1(X))  
# acc=0.83

# tanh激活函数
H1 = torch.tanh(self.linear_1(X))  
# acc=0.1

# 使用sigmoid激活函数
self.Sigmoid = nn.Sigmoid()
H1 = self.Sigmoid(self.linear_1(X))
# acc=0.85

3、试试不同的权重初始化。

你可能感兴趣的