① 线性意味着单调假设:任何特征的增大都会导致模型输出的增大(当对应的权重为正),或导致模型输出的减小(当对应的权重为负)。而在实际数据中,这种强要求难以满足。
② 数据可能会有一种表示,这种表示会考虑到我们在特征之间的相关交互作用。单纯的线性模型未考虑到特征间的相互作用。
隐藏层:
隐藏层人工神经网中的中间层,位于输入层和输出层之间。主要作用是实现输入数据特征的提取和变换,为输出层提供高层次特征。隐藏层这个术语之所以称为“隐藏”,是因为其输出对外界不可见,只在网络内部流通。隐藏层作为神经网络的核心,主要任务是通过逐层的特征提取和非线性变换来捕捉数据中的复杂模式和特征。多层隐藏层:通过多层隐藏层的堆叠,网络可以逐渐提取出数据中越来越抽象的特征,这也是深度学习的核心思想。对于每一个隐藏层单元,计算公式如下:h=σ(Wx+b),其中,W是权重矩阵;x是前一层的输出;b是偏置项;σ是激活函数(如ReLU, sigmoid, Tanh等)。
多层感知机(multilayer perceptron, MLP):
通过在网络中加入一个或多个隐藏层来克服线性模型的限制,使其能处理更普遍的函数关系类型。最简单的方法是将许多全连接层堆叠在一起。每一层都输出到上面的层,直到生成最后的输出。如此可将前L−1层看作表示,把最后一层看作线性预测器。这种架构通常称为多层感知机(multilayer perceptron),通常缩写为MLP。
从线性到非线性:
设X表示n个样本,每个样本具有d个特征的实矩阵;H表示具有h个隐藏单元的隐藏层输出,即隐藏表示(hidden representations);设隐藏层权重W(1)为d×h的实矩阵和隐藏层偏置b(1)为1×h的实矩阵及输出层权重W(2)为h×q的实矩阵和输出层偏置b(2)为1×q的实矩阵,则单隐藏层多层感知机的输出O(n×q的实矩阵)计算如下:
H = XW(1) + b(1),
O = HW(2) + b(2).
由于仿射函数的仿射函数本身依然是仿射函数,为了发挥多层架构的潜力,须引入一个额外的关键要素:在仿射变换之后对每个隐藏单元应用非线性激活函数(activation function)σ。激活函数的输出被称为活性值(activations)。一般而言,有了激活函数,就可以避免多层感知机退化成线性模型:
O = HW(2) + b(2).
多层感知机通过隐藏神经元可以捕捉到输入之间复杂的相互作用,且可设计隐藏节点来执行任意计算。但需要注意的是:一个单隐层网络能学习任何函数,但并不意味着应尝试使用单隐藏层网络来解决所有问题。 事实上,通过更深(而不是更广)的网络,可以更容易地逼近许多函数。
激活函数(Activation Function):在人工神经网络的神经元上运行的函数,负责将神经元的输入映射到输出端(将输入信号转换为输出的可微运算且大多数激活函数都是非线性的),旨在帮助网络学习数据中的复杂模式。常见激活函数如下:
1. ReLU函数
定义:
ReLU函数即修正线性单元(Rectified linear unit,ReLU)是现代神经网络中最常用的激活函数,大多数前馈神经网络默认使用的激活函数。ReLU函数定义如下:
σ(x) = max(x, 0)
通俗理解ReLU函数通过将相应的活性值设为0,仅保留正元素并丢弃所有负元素。
优点:
缺点:
ReLU的输出不是0均值的。
2. Leaky Relu函数
定义:
解决了ReLU输入值为负时神经元出现的死亡的问题;
Leaky ReLU线性、非饱和的性质,在SGD中能够快速收敛;
计算复杂度低,不需要进行指数运算。
函数中的α,需要通过先验知识人工赋值(一般设为0.01);
3. PRelu函数
定义:
PRelu激活函数的数学表达式为:
ELU试图将激活函数的输出均值接近于零,使正常梯度更接近于单位自然梯度,从而加快学习速度
ELU 在较小的输入下会饱和至负值,从而减少前向传播的变异和信息
缺点:
计算的时需要计算指数,计算效率低
性质:
SELU允许构建一个映射g,其性质能够实现SNN(自归一化神经网络)。SNN不能通过ReLU、sigmoid、tanh和Leaky ReLU实现。这个激活函数需要有:(1) 负值和正值,以便控制均值;(2) 饱和区域(导数趋近于零),以便抑制更低层中较大的方差;(3) 大于1的斜率,以便在更低层中的方差过小时增大方差;(4) 连续曲线。SELU激活函数通过乘上指数线性单元(ELU)来满足激活函数的这些性质,而且 λ>1 能够确保正值净输入的斜率大于 1。SELU激活函数是在自归一化网络中定义的,通过调整均值和方差来实现内部的归一化,这种内部归一化比外部归一化更快,这使得网络能够更快的收敛。
优点:
Sigmoid 函数的输出范围是(0,1)。非常适合作为模型的输出函数用于输出一个0~1范围内的概率值,比如用于表示二分类的类别或者用于表示置信度。
梯度平滑,便于求导,也防止模型训练过程中出现突变的梯度。
缺点:
容易造成梯度消失。我们从导函数图像中了解到sigmoid的导数都是小于0.25的,那么在进行反向传播的时候,梯度相乘结果会慢慢的趋向于0。另外须对权重矩阵的初始化特别留意,若初始化权重过大,可能致使神经元不能很好的更新权重而提前饱和。
函数输出不是以0为中心的,梯度可能就会向特定方向移动,从而降低权重更新的效率
Sigmoid函数执行指数运算,计算机运行得较慢,比较消耗计算资源。
优点:
tanh的输出范围为(-1, 1),并且整个函数以 0 为中心,比sigmoid函数更好;
在tanh图中,负输入将被强映射为负,而零输入被映射为接近零。
缺点:
仍然存在梯度饱和的问题
依然进行的是指数运算
# plotShow.py
from matplotlib import pyplot as plt
import typing
def setFigsize(figsize:typing.Tuple[float]=(3.5, 2.5)):
plt.rcParams['figure.figsize'] = figsize
def setAxes(axes:plt.gca, xlabel, ylabel, xlim, ylim, xscale, yscale, legend)->None:
'''
设置matplotlib的坐标轴
'''
axes.set_xlabel(xlabel)
axes.set_ylabel(ylabel)
axes.set_xscale(xscale)
axes.set_yscale(yscale)
axes.set_xlim(xlim)
axes.set_ylim(ylim)
if legend:
axes.legend(legend)
axes.grid()
return None
def plot(x, y=None, xlabel=None, ylabel=None, legend=None, xlim=None, ylim=None,
xscale='linear', yscale='linear', fmts=('-', 'm--', 'g-', 'r:'),
figsize=(3.5, 2.5), axes=None):
'''
绘制数据点
'''
if legend is None:
legend = []
setFigsize(figsize)
axes = axes if axes else plt.gca()
# 若x有一个轴,输出True
def hasOneAxis(_x):
return (hasattr(_x, 'ndim')
and _x.ndim==1
or isinstance(_x, list)
and not hasattr(_x[0], '__len__'))
if hasOneAxis(x):
x = [x]
if y is None:
x, y = [[]]*len(x), x
elif hasOneAxis(y):
y = [y]
if len(x)!=len(y):
x = x*len(y)
axes.cla()
for _x, _y, fmt in zip(x, y, fmts):
if len(_x):
axes.plot(_x, _y, fmt)
else:
axes.plot(_y, fmt)
setAxes(axes, xlabel, ylabel, xlim, ylim, xscale, yscale, legend)
plt.show()
return None
import torch
from torch import nn
from torch.utils import data
import torchvision
from torchvision import transforms
import matplotlib.pyplot as plt
from matplotlib_inline import backend_inline
from IPython import display
import typing
import plotShow
# 定义一个累加器,实现对n个变量进行累加
class Accumulator:
'''
在n个变量上累加
'''
def __init__(self, n:int):
self.data = [0.0]*n
def add(self, *args):
self.data = [a+float(b) for a, b in zip(self.data, args)]
def reset(self):
self.data = [0.0]*len(self.data)
def __getitem__(self, idx):
return self.data[idx]
class Animator:
'''
在动画中绘制数据
'''
def __init__(self, xlabel=None, ylabel=None, legend=None, xlim=None, ylim=None, xscale='linear',
yscale='linear', fmts=('-', 'm--', 'g-.', 'r:'), nrows=1, ncols=1, figsize=(3.5,2.5)):
# 增量地绘制多条线条
if legend is None:
legend = []
backend_inline.set_matplotlib_formats('svg')
self.fig, self.axes = plt.subplots(nrows, ncols, figsize=figsize)
if nrows*ncols==1:
self.axes = [self.axes, ]
# 使用lambda函数捕获参数
self.configAxes = lambda: plotShow.setAxes(self.axes[0], xlabel, ylabel, xlim, ylim, xscale, yscale, legend)
self.X, self.Y, self.fmts = None, None, fmts
def add(self, x, y):
# 向图表中添加多个数据点
if not hasattr(y, '__len__'):
y = [y]
n = len(y)
if not hasattr(x, '__len__'):
x = [x]*n
if not self.X:
self.X = [[] for _ in range(n)]
if not self.Y:
self.Y = [[] for _ in range(n)]
for i, (a, b) in enumerate(zip(x, y)):
if a is not None and b is not None:
self.X[i].append(a)
self.Y[i].append(b)
self.axes[0].cla()
for x, y, fmt in zip(self.X, self.Y, self.fmts):
self.axes[0].plot(x, y, fmt)
self.configAxes()
display.display(self.fig)
plt.draw()
plt.pause(0.001)
display.clear_output(wait=True)
def show(self):
display.display(self.fig)
def getFashionMnistLabels(labels:typing.Sequence):
'''
返回FashionMnist数据集的文本标签
'''
textLabels = ['t-shirt', 'trouser', 'pullever', 'dress', 'coat',
'sandal', 'shirt', 'sneaker', 'bag', 'ankle boot']
return [textLabels[int(i)] for i in labels]
def showImages(imgs:list, numRows:int, numCols:int, titles:list=None, scale:float=1.5):
figsize = (numCols*scale, numRows*scale)
_, axes = plt.subplots(numRows, numCols, figsize=figsize)
axes = axes.flatten()
for i, (ax, img) in enumerate(zip(axes, imgs)):
if torch.is_tensor(img): # 图片张量
ax.imshow(img.numpy())
else: # PIL图片
ax.imshow(img)
ax.axes.get_xaxis().set_visible(False)
ax.axes.get_yaxis().set_visible(False)
if titles:
ax.set_title(titles[i])
return axes
def getDataloaderWorkers()->int:
'''
使用4个进程来读取数据
'''
return 4
# 读取数据集
def loadDataFashionMnist(batchSize:int, resize=None)->tuple:
'''
下载Fashion-MNIST数据集,然后将其加载到内存中
'''
trans = [transforms.ToTensor()]
if resize:
trans.insert(0, transforms.Resize(resize))
trans = transforms.Compose(trans)
mnistTrain = torchvision.datasets.FashionMNIST(root='../data', train=True, transform=trans, download=True)
mnistTest = torchvision.datasets.FashionMNIST(root='../data', train=False, transform=trans, download=True)
return (data.DataLoader(mnistTrain, batchSize, shuffle=True, num_workers=getDataloaderWorkers()),
data.DataLoader(mnistTest, batchSize, shuffle=False, num_workers=getDataloaderWorkers()))
# 定义分类精度
def accuracy(yHat:torch.Tensor, y:torch.Tensor)->float:
'''
计算预测正确的数量
'''
if len(yHat.shape)>1 and yHat.shape[1]>1:
yHat = yHat.argmax(axis=1)
cmp = yHat.type(y.dtype)==y
return float(cmp.type(y.dtype).sum())
def evaluateAccuracy(net:torch.nn.Module, dataIter:typing.Tuple[torch.Tensor])->float:
'''
计算在指定数据集上模型的精度
'''
if isinstance(net, torch.nn.Module):
net.eval() # 将模型设置为评估模式
metric = Accumulator(2) # 统计正确预测数、预测总数
with torch.no_grad():
for X, y in dataIter:
metric.add(accuracy(net(X), y), y.numel())
return metric[0]/metric[1]
# 定义训练模型
def trainEpochCh3(net:torch.nn.Module,
trainIter:typing.Tuple[torch.Tensor],
loss:typing.Callable[[torch.Tensor, torch.Tensor], torch.Tensor],
updater:torch.optim.Optimizer)->typing.Tuple[float]:
'''
训练模型一个周期
'''
# 将模型设置为训练模型
if isinstance(net, torch.nn.Module):
net.train()
# 训练损失总和、训练准确度总和、样本数
metric = Accumulator(3)
for X, y in trainIter:
yHat:torch.Tensor = net(X)
l:torch.Tensor = loss(yHat, y)
if isinstance(updater, torch.optim.Optimizer):
# 使用PyTorch内置的优化器和损失函数
updater.zero_grad()
l.mean().backward()
updater.step()
else:
# 使用定制的优化器和损失函数
l.sum().backward()
updater(X.shape[0])
metric.add(float(l.sum()), accuracy(yHat, y), y.numel())
# 返回训练损失和训练精度
return metric[0]/metric[2], metric[1]/metric[2]
def trainCh3(net:torch.nn.Module,
trainIter:data.DataLoader,
testIter:data.DataLoader,
loss:typing.Callable[[torch.Tensor, torch.Tensor], torch.Tensor],
numEpochs: int,
updater: torch.optim.Optimizer)->None:
'''
训练模型
'''
animator = Animator(xlabel='epoch', xlim=[1, numEpochs], ylim=[0.3,0.9],
legend=['train loss', 'train acc', 'test acc'])
for epoch in range(numEpochs):
trainMetrics = trainEpochCh3(net, trainIter, loss, updater)
testAcc = evaluateAccuracy(net, testIter)
animator.add(epoch+1, trainMetrics+(testAcc, ))
trainLoss, trainAcc = trainMetrics
assert trainLoss<0.5, trainLoss
assert trainAcc<=1 and trainAcc>0.7, trainAcc
assert testAcc<=1 and testAcc>0.7, testAcc
# 预测
def predictCh3(net, testIter, n=6):
'''
预测标签
'''
for X, y in testIter:
break
trues = getFashionMnistLabels(y)
preds = getFashionMnistLabels(net(X).argmax(axis=1))
titles = [true + '\n' + pred for true, pred in zip(trues, preds)]
showImages(X[0:n].reshape((n, 28, 28)), 1, n, titles=titles[0:n])
plt.show()
# 多层感知机的从零开始实现
if __name__ == '__main__':
# 1. 读取数据集
batchSize: int = 256
trainIter, testIter = loadDataFashionMnist(batchSize)
# 2. 初始化模型参数
numInputs: int = 784 # 原始数据集每个样本是28*28的图像,因此以构造一个长度为784的向量来表示每一个像素点
numOutPuts: int = 10 # 数据集中有10个类别
numHiddens: int = 256 # 隐藏层包含256个隐藏单元,隐藏单元数一般选择2的若干次幂作为层的宽度
W1 = nn.Parameter(torch.randn(numInputs, numHiddens, requires_grad=True)*0.01)
b1 = nn.Parameter(torch.zeros(numHiddens, requires_grad=True))
W2 = nn.Parameter(torch.randn(numHiddens, numOutPuts, requires_grad=True)*0.01)
b2 = nn.Parameter(torch.zeros(numOutPuts, requires_grad=True))
params = [W1, b1, W2, b2]
# 3. 激活函数
def ReLU(X:torch.Tensor)->torch.Tensor:
a:torch.Tensor = torch.zeros_like(X)
return torch.max(X, a)
# 4. 定义模型
def net(X:torch.Tensor):
X = X.reshape((-1, numInputs))
H = ReLU(torch.matmul(X, W1)+b1)
return torch.matmul(H, W2) + b2
# 5. 定义损失函数
loss = nn.CrossEntropyLoss(reduction='none')
# 6. 训练
numEpochs: int = 10 # 迭代周期数
lr:float = 0.1 # 学习率
trainer = torch.optim.SGD(params, lr) # 优化函数
trainCh3(net, trainIter, testIter, loss, numEpochs, trainer)
# 预测
predictCh3(net, testIter)
# 多层感知机的简洁实现
if __name__ == '__main__':
# 1. 定义模型
net = nn.Sequential(nn.Flatten(),
nn.Linear(784, 256), # 输入层
nn.ReLU(), # 隐藏层
nn.Linear(256, 10)) # 输出层
# 2. 参数初始化
def initWeight(m:nn.Linear)->None:
if type(m)==nn.Linear:
nn.init.normal_(m.weight, std=0.01)
return None
net.apply(initWeight)
# 3. 定义损失函数和优化器
batchSize:int = 256 # 小批量样本数
lr:float = 0.1 # 学习率
numEpochs:int = 10 # 迭代周期数
loss = nn.CrossEntropyLoss(reduction='none') # 损失函数
trainer = torch.optim.SGD(net.parameters(), lr=lr) # 优化器
# 4. 训练
trainIter, testIter = loadDataFashionMnist(batchSize)
trainCh3(net, trainIter, testIter, loss, numEpochs, trainer)
# 5. 预测
predictCh3(net, testIter