一、隐藏层线性模型可能会出错,原因有二:① 线性意味着单调假设:任何特征的增大都会导致模型输出的增大(当对应的权重为正),或导致模型输出的减小(当对应的权重为负)。而在实际数据中,这种强要求难以满足。② 数据可能会有一种表示,这种表示会考虑到我们在特征之间的相关交互作用。单纯的线性模型未考虑到特征间的相互作用。隐藏层:隐藏层人工神经网中的中间层,位于输入层和输出层之间。主要作用是实现输入数据特征的提取和变换,为输出层提供高层次特征。隐藏层这个术语之所以称为“隐藏”,是因为其输出对外界不可见,只在网络内部流通。隐藏层作为神经网络的核心,主要任务是通过逐层的特征提取和非线性变换来捕捉数据中的复杂模式和特征。多层隐藏层:通过多层隐藏层的堆叠,网络可以逐渐提取出数据中越来越抽象的特征,这也是深度学习的核心思想。对于每一个隐藏层单元,计算公式如下: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)。一般而言,有了激活函数,就可以避免多层感知机退化成线性模型: H = σ(XW(1) + b(1)), O = HW(2) + b(2).多层感知机通过隐藏神经元可以捕捉到输入之间复杂的相互作用,且可设计隐藏节点来执行任意计算。但需要注意的是:一个单隐层网络能学习任何函数,但并不意味着应尝试使用单隐藏层网络来解决所有问题。 事实上,通过更深(而不是更广)的网络,可以更容易地逼近许多函数。二、激活函数激活函数(Activation Function):在人工神经网络的神经元上运行的函数,负责将神经元的输入映射到输出端(将输入信号转换为输出的可微运算且大多数激活函数都是非线性的),旨在帮助网络学习数据中的复杂模式。常见激活函数如下:1. ReLU函数定义:ReLU函数即修正线性单元(Rectified linear unit,ReLU)是现代神经网络中最常用的激活函数,大多数前馈神经网络默认使用的激活函数。ReLU函数定义如下:σ(x) = max(x, 0)通俗理解ReLU函数通过将相应的活性值设为0,仅保留正元素并丢弃所有负元素。优点:使用ReLU的SGD算法的收敛速度比sigmoid和tanh快。在x>0区域上,不会出现梯度饱和、梯度消失的问题。计算复杂度低,不需要进行指数运算,只要一个阈值就可以得到激活值。缺点:ReLU的输出不是0均值的。Dead ReLU Problem(神经元坏死现象):ReLU在负数区域被kill的现象叫做dead relu。ReLU在训练的时很“脆弱”。在x<0时,梯度为0。这个神经元及之后的神经元梯度永远为0,不再对任何数据有所响应,导致相应参数永远不会被更新。产生这种现象的两个原因:参数初始化问题;learning rate太高导致在训练过程中参数更新太大。解决方法:采用Xavier初始化方法,以及避免将learning rate设置太大或使用adagrad等自动调节learning rate的算法。2. Leaky Relu函数定义:Leaky ReLU函数即弱化修正线性单元,为了解决dead ReLU现象。用一个类似0.01的小值来初始化神经元,从而使得ReLU在负数区域更偏向于激活而不是死掉。这里的斜率都是确定的。Leaky Relu激活函数的数学表达式为:σ(x) = max(x, 𝜕x)优点:解决了ReLU输入值为负时神经元出现的死亡的问题;Leaky ReLU线性、非饱和的性质,在SGD中能够快速收敛;计算复杂度低,不需要进行指数运算。缺点:函数中的α,需要通过先验知识人工赋值(一般设为0.01);有些近似线性,导致在复杂分类中效果不好。3. PRelu函数定义:PRelu激活函数的数学表达式为:σ(α, x) = max(0, x) + αmin(0, x)PReLU激活函数也是用来解决ReLU带来的神经元坏死的问题。与Leaky ReLU激活函数不同的是,PRelu激活函数负半轴的斜率参数α是通过学习得到的,而不是手动设置的恒定值。4. ELU函数定义:ELU激活函数的数学表达式为:优点:ELU试图将激活函数的输出均值接近于零,使正常梯度更接近于单位自然梯度,从而加快学习速度ELU 在较小的输入下会饱和至负值,从而减少前向传播的变异和信息缺点:计算的时需要计算指数,计算效率低5. SELU函数定义:SELU激活函数的数学表达式为:其中λ = 1.0507 , α = 1.6733性质:SELU允许构建一个映射g,其性质能够实现SNN(自归一化神经网络)。SNN不能通过ReLU、sigmoid、tanh和Leaky ReLU实现。这个激活函数需要有:(1) 负值和正值,以便控制均值;(2) 饱和区域(导数趋近于零),以便抑制更低层中较大的方差;(3) 大于1的斜率,以便在更低层中的方差过小时增大方差;(4) 连续曲线。SELU激活函数通过乘上指数线性单元(ELU)来满足激活函数的这些性质,而且 λ>1 能够确保正值净输入的斜率大于 1。SELU激活函数是在自归一化网络中定义的,通过调整均值和方差来实现内部的归一化,这种内部归一化比外部归一化更快,这使得网络能够更快的收敛。6. sigmoid函数定义:对于一个定义域在R中的输入,sigmoid函数将输入变换为区间(0, 1)上的输出。因此,sigmoid通常称为挤压函数(squashing function):它将范围(‐inf, inf)中的任意输入压缩到区间(0, 1)中的某个值。sigmoid激活函数的数学表达式为:Sigmoid函数在历史上曾非常常用,输出值范围为(0,1)之间的实数。但是现在它已经不太受欢迎,实际中很少使用。优点:Sigmoid 函数的输出范围是(0,1)。非常适合作为模型的输出函数用于输出一个0~1范围内的概率值,比如用于表示二分类的类别或者用于表示置信度。梯度平滑,便于求导,也防止模型训练过程中出现突变的梯度。缺点:容易造成梯度消失。我们从导函数图像中了解到sigmoid的导数都是小于0.25的,那么在进行反向传播的时候,梯度相乘结果会慢慢的趋向于0。另外须对权重矩阵的初始化特别留意,若初始化权重过大,可能致使神经元不能很好的更新权重而提前饱和。函数输出不是以0为中心的,梯度可能就会向特定方向移动,从而降低权重更新的效率Sigmoid函数执行指数运算,计算机运行得较慢,比较消耗计算资源。7. tanh函数定义:与sigmoid函数类似,tanh(双曲正切)函数也能将其输入压缩转换到区间(‐1, 1)上。tanh函数的公式如下:函数的形状类似于sigmoid函数,不同的是tanh函数关于坐标系原点中心对称。在实际应用中,tanh会比sigmoid更好一些。但是在饱和神经元的情况下,tanh依然未解决梯度消失问题。优点:tanh的输出范围为(-1, 1),并且整个函数以 0 为中心,比sigmoid函数更好;在tanh图中,负输入将被强映射为负,而零输入被映射为接近零。缺点:仍然存在梯度饱和的问题依然进行的是指数运算三、多层感知机的实现# plotShow.pyfrom matplotlib import pyplot as pltimport typingdef setFigsize(figsize:typing.Tuple[float]=(3.5, 2.5)): plt.rcParams['figure.figsize'] = figsizedef 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 Nonedef 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 Noneimport torchfrom torch import nnfrom torch.utils import dataimport torchvisionfrom torchvision import transformsimport matplotlib.pyplot as pltfrom matplotlib_inline import backend_inlinefrom IPython import displayimport typingimport 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 axesdef 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来源:檐苔