摘要:在之前的简单线性变换和softmax回归中,最后各种输入与输出之间是通过一种仿射变换,再加上其它的一些操作。但是,在实际的环境中,仿射变换中的线性是一个很强的假设,可能输出与输出之间并不是一种简单的线性关系。
1.隐藏层
在之前的简单线性变换和softmax回归中,最后各种输入与输出之间是通过一种仿射变换,再加上其它的一些操作。但是,在实际的环境中,仿射变换中的线性是一个很强的假设,可能输出与输出之间并不是一种简单的线性关系。
通常情况下,使用线性变换表示一种单调性的变化,也即假设一共有n个特征,在n-1个特征都不发生变化的情况下,最后一个特征值的增大都会导致最终计算值的增大或者减小。这种逻辑在尝试试图预测一个人是否会偿还贷款与其收入之间的关系的时候是十分有用的。
但是当我们在对图像预测其类别的时候不能使用简单线性思想去判断,增加某一种位置(13,17)处像素的强度是否总是增加(或降低)图像描绘狗的似然?对线性模型的依赖对应于一个隐含的假设,即区分猫和狗的唯一要求是评估单个像素的强度。但是当对图像进行旋转或者折叠等增强操作的时候,此时单个像素的强度必定会发生变化,那么在这种情况下将不再适合使用简单线性变换进行特征的挖掘。
为了克服简单线性模型的缺陷,可以使用多个隐藏层来使模型能够处理更加普遍的函数关系,其中最常见的一种方式就是将多个不同全连接层连接在一起,这种架构通常被称为多层感知机,也即MLP,具体的结构如下图所示:
根据全连接层的特性,可以使用如下的式子进行处理,其中H表示的是隐藏层的输出,O表示的是最终预测层的输出。
单看上面的式子,本质上使用多个隐藏层与使用单个隐藏层是等价的,为了能够体现隐藏层的实际作用,在隐藏层进行输出之后,一般会对每个隐藏单元应用非线性的激活函数(activation function)σ。激活函数的输出被称为活性值(activations)。一般来说,有了激活函数,就不可能再将我们的多层感知机退化成线性模型,因此,实际上的MLP隐藏层与最终层的输出如下:
多层感知机可以通过隐藏神经元,捕捉到输入之间复杂的相互作用,这些神经元依赖于每个输入的值。我们可以很容易地设计隐藏节点来执行任意计算。但是,虽然一个单隐层网络能学习任何函数, 但并不意味着我们应该尝试使用单隐藏层网络来解决所有问题。 事实上,通过使用更深而不是更广的网络,我们可以更容易地逼近许多函数。
2.激活函数
2.1 ReLU函数
ReLU函数通常被翻译成为修正线性单元,这种函数实现相对简单,同时在各种预测任务的表现中都较好。ReLU函数提供了一种十分简单的线性变换。
通俗地说,ReLU函数通过将相应的活性值设为0,仅保留正元素并丢弃所有负元素。
x=torch.arange(-8.0,8.0,0.1,requires_grad=True) # 确定绘图的范围以及两个变量之间的跨越尺度y=torch.relu(x)d2l.plot(x.detach,y.detach,'x','relu(x)',figsize=(5,2.5))对ReLU函数进行求导操作,由于这里的输入都是常数,因此求导之后得到的结果都是用1进行表示的。
y.backward(torch.ones_like(x),retain_graph=True) # 计算y关于x的导数d2l.plot(x.detach,x.grad.detach,'x','grad of relu',figsize=(5,2.5))2.2 sigmoid函数
对于一个定义域在实数中的输入,sigmoid函数将输入变换为区间(0, 1)上的输出。因此,sigmoid通常称为挤压函数(squashing function):它将范围(-inf, inf)中的任意输入压缩到区间(0, 1)中的某个值:
y=torch.sigmoid(x)d2l.plot(x.detach,y.detach,'x','sigmoid(x)',figsize=(5,2.5))对sigmoid函数进行自动求导:
x.grad.zero_ # 清除之前的梯度y.backward(torch.ones_like(x),retain_graph=True)d2l.plot(x.detach,x.grad.detach,'x','grad of sigmoid',figsize=(5,2.2))2.3 tanh函数
与sigmoid函数类似, tanh(双曲正切)函数也能将其输入压缩转换到区间(-1, 1)上。 tanh函数的公式如下:
y=torch.tanh(x)d2l.plot(x.detach,y.detach,'x','tanh(x)',figsize=(5,2.5))对tanh函数进行求导操作,求导的结果如下:
x.grad.zero_y.backward(torch.ones_like(x),retain_graph=True)d2l.plot(x.detach,x.grad,'x','grad of tanh',figsize=(5,2.5))3.MLP实现
3.1 设置参数
在这个实现MLP过程中,主要设置的是一个输入层、一个隐藏层以及一个输出层,因此,在设置参数的时候需要设置从输入层到隐藏层的权重参数和从隐藏层到输出层的权重参数,此外还需设置对应层的偏置参数。这里选择使用的数据集仍然是Fashion-MNIST数据集,对于这个数据集中的照片,每一张都是28行28列数据,一共的灰度值大小是784个灰度值。
num_inputs, num_outputs, num_hiddens = 784, 10, 256 # 输入参数一共有784个,输出参数有10个,隐藏层有256个w1=nn.Parameter(torch.randn(num_inputs,num_hiddens,requires_grad=True)*0.01) # 乘以0.01是为了防止变量过大b1=nn.Parameter(torch.zeros(num_hiddens,requires_grad=True))w2=nn.Parameter(torch.randn(num_hiddens,num_outputs,requires_grad=True)*0.01)b2=nn.Parameter(torch.zeros(num_outputs,requires_grad=True))params=[w1,b1,w2,b2] # 初始化权重和偏置数组3.2 激活函数
根据激活函数的使用范围与应用特性,这里选择的激活函数是ReLU函数。
def relu(x): # 定义激活函数 a=torch.zeros_like(x) return torch.max(x,a)3.3 建立模型
在进行特征提取的时候,一般都会忽略模型的特征结构,因此这里可以使reshape将每个二维图像转换为一个长度为num_inputs的向量。
x=x.reshape(-1,num_inputs) # 将输入数据转换为二维张量 H=relu(x@w1+b1) # 第一层 # @表示矩阵乘法 return (H@w2+b2) # 第二层3.4 损失函数
这里设置的损失函数是交叉熵损失函数,这种损失函数也是在分类预测问题中使用得最多的一类损失函数,前面在进行softmax回归的时候使用的也是这一类参数。
loss=nn.CrossEntropyLoss(reduction='none') # 交叉熵损失函数,这是在分类问题中使用很多的一类损失函数3.5 分类精度
这里使用的数据集本质上是一个分类数据集,使用的损失函数的也是采用的softmax回归中的交叉熵损失函数,因此这是一个分类问题。由于模型在训练的时候每一次都是抽取了小批量的数据进行训练,因此这里首先通过accuracy可以获得单个批次中的所有预测正确的样本个数,之后使用evaluate_accuracy函数可以获得所有的小批量样本数据的分类准确度。
def accuracy(y_hat, y): # 得到单轮预测正确的个数 if len(y_hat.shape) > 1 and y_hat.shape[1] > 1: y_hat = y_hat.argmax(axis=1) # 返回最大元素值所对应的索引 cmp = y_hat.type(y.dtype) == y # 这里的cmp是一个布尔值的矩阵,表示了y_hat和y是否相等 return float(cmp.type(y.dtype).sum) # 这里的cmp.type(y.dtype)是一个0-1的矩阵,sum函数表示了1的个数,即相等的个数def evaluate_accuracy(net, data_iter): # 返回全部预测正确数 if isinstance(net, torch.nn.Module): net.eval # 将模型设置为评估模式 metric = Accumulator(2) # 正确预测数、预测总数 for X, y in data_iter: metric.add(accuracy(net(X), y), y.numel) # 这里的y.numel表示了y中元素的个数 return metric[0] / metric[1] # 返回的是预测正确的个数除以总数3.6 模型训练
这里的train_epoch_ch3是用来训练一个轮次的,使用train_ch3则是用来训练所有轮次的模型。以单轮的训练为例,其基本的训练流程同softmax回归基本相同,大致分为以下步骤:
(1)设置训练模式:如果 net 是一个 torch.nn.Module 对象,将其设置为训练模式(train),以启用 dropout 和 batch normalization。
(2)初始化累加器:创建一个 Accumulator 对象,用于累加训练损失总和、训练准确度总和和样本数。
(3)遍历训练数据:对训练数据迭代器中的每个批次进行以下操作:
1)前向传播:计算模型的预测输出 y_hat。
2)计算损失:使用损失函数计算损失 l,并取平均值以确保输出的梯 度是一个标量。
3)反向传播和参数更新:如果 updater 是一个优化器对象,使用优 化器进行参数更新;否则,使用自定义的参数更新方法。
4)累加指标:将损失、准确度和样本数添加到累加器中。
(4)返回结果:返回训练损失和训练准确度。
def train_epoch_ch3(net, train_iter, loss, updater): # 这个函数返回的是训练损失 # 这里返回的是是训练一轮之后的损失 if isinstance(net, torch.nn.Module): net.train # 将模型设置为训练模式 metric = Accumulator(3) # 训练损失总和、训练准确度总和、样本数 for X, y in train_iter: y_hat = net(X) l = loss(y_hat, y).mean # 在这里添加一个mean确保输出的梯度是一个标量数据 if isinstance(updater, torch.optim.Optimizer): # 这里的updater是一个优化器,是一个更新模型参数的函数 # 使用内置的优化器与损失函数 updater.zero_grad l.backward updater.step else: # 使用定制的优化器与损失函数 l.sum.backward updater(X.shape[0]) metric.add(float(l.sum), accuracy(y_hat, y), y.numel) return metric[0] / metric[2], metric[1] / metric[2] # 返回的是训练损失和训练准确度def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater): # 这个函数返回的是训练损失和测试损失 animator = Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0.3, 0.9], legend=['train acc', 'test acc'], fmts=['-', 'g-.']) for epoch in range(num_epochs): train_metrics = train_epoch_ch3(net, train_iter, loss, updater) test_acc = evaluate_accuracy(net, test_iter) # 只添加训练精度和测试精度 animator.add(epoch + 1, [train_metrics[1], test_acc]) train_loss, train_acc = train_metrics assert train_loss assert train_acc 0.7, train_acc assert test_acc 0.7, test_acc# 使用MLP进行训练和softmax回归一样,最终都是通过softmax函数将数值归一化到一个特定的概率分布范围num_epochs, lr = 10, 0.1 # 设定迭代次数与学习率update=torch.optim.SGD(params,lr=lr) # 使用梯度下降进行优化train_ch3(net,train_iter,test_iter,loss,num_epochs,update)训练精度结果如下图:
来源:杭州有一路看一路