SignalPlus:浅谈深度神经网络

    前言神经网络(neuralnetwork)受到人脑的启发,可模仿生物神经元相互传递信号。神经网络就是由神经元组成的系统。如下图所示,神经元有许多树突(dendrite)用来输入,有一个轴突(axon)用来输出。它具有两个最主要的特性:兴奋性和传导性:兴奋性是指当刺激强度未达到某一阈限值时,神经冲动不会发生;而当刺激强度达到该值时,神经冲动发生并能瞬时达到最大强度。传导性是指相邻神经元靠其间一小空隙进行传导。这一小空隙,叫做突触(synapse),其作用在于传递不同神经元之间的神经冲动,下图突触将神经元A和B连在一起。试想很多突触连接很多神经元,不就形成了一个神经网络了吗?没错,类比到人工神经网络(artificialneuralnetwork,ANN),也是由无数的人工神经元组成一起的,比如下左图的浅度神经网络(shadowneuralnetwork)和下右图的深度神经网络(deepneuralnetwork)。浅度神经网络适用于结构化数据(structureddata),比如像下图中excel里存储的二维数据。深度神经网络适用于等非结构化数据(unstructureddata),如下图所示的图像、文本、语音类数据。生成式AI模型主要是生成非结构化数据,因此了解深度神经网络是必要的。从本篇开始,我们会模型与代码齐飞,因为Talkischeap.Showmethecode.--LinusTorvalds代码都用TensorFlow和Keras来实现。1.人工神经网络1.1神经网络初见假设下面的神经网络已经被训练好,接着用来预测图片中是否含有笑脸。单元A接收图像里的像素信息。单元B结合了输入像素,当原始图像中有低级特征(low-levelfeature)比如边缘(edge)时,发出最强信号。单元C结合了低级特征,当原始图像中有高级特征(high-levelfeature)比如牙齿(teech)时,发出最强信号。单元D结合了高级特征,当原始图像中的人微笑时,发出最强信号。当给这个神经网络“投喂”足够多的数据,即图像,它会“找到”一组权重(weights)使得最终预测结果尽可能准确。找权重这个过程其实就是训练神经网络。对神经网络有个初步认识之后,接下来的任务就是用Keras来实现它。1.2Keras训练流程在Keras中实现神经网络需要了解三大要点:模型(models)层(layers),输入(input)和输出(output)优化器(optimizer)和损失函数(loss)用上面的关键词来总结Keras训练神经网络的流程:将多个层链接在一起组成模型,将输入数据映射为预测值。然后损失函数将这些预测值输出,并与目标进行比较,得到损失值(用于衡量网络预测值与预期结果的匹配程度),优化器利用这个损失值来更新网络的权重。到此终于可以展示点代码了,即便是引入工具库。首先从tensorflow.keras库中用于搭建神经网络的模块。整个神经网络就是一个模型,大框架的代码都来自models模块;模型是由多个层组成,而不同的层的代码都来自layers模块;模型的第一层是输入层,负责接入输入,模型的最后一层是输出层,负责提供输出,一头一尾都在models模块;模型骨架好了,要使它中看又中用就需要optimizers模块来训练它了。1.3极简神经网络学过机器学习的同学遇到的第一个模型一定是线性回归,还是单变量的线性回归。给定一组x和y的数据:x=[-1,0,1,2,3,4]y=[-3,-1,1,3,5,7]找出x和y之间的关系,当xnew=10时,问ynew是多少?如下图所示,将x和y以散点的形式画出来,不难发现下图的红线就是x和y之间的关系。现在想用Keras杀鸡用牛刀的构建一个神经网络来求出这条红线。1.3.1创建模型用一层含一个神经元的神经网络即可,代码如下:首先用models.Sequential()创建一个空神经网络,然后不断添加层,这里我们添加了layers.Dense(),叫做稠密层。函数里面的参数input_shape=[1]表示输入数据的维度为1,units=1表示输出只有1个神经元。可视化如下:1.3.2检查模型检查一下模型信息,奇怪的是参数个数(下图Param#)居然是2个而不是1个。因为从上图来看y=wx,只应该有w一个参数啊。原因是在计算每层参数个数时,每个神经元默认会连接到一个值为1的偏置单元(biasunit),因此其实上图更准确的样子如下:这样就对了,此时y=wx+b,有w和b两个参数了。严格来说,其实Dense()函数里还是一个参数叫activation,它字面意思是激活函数,本质上做的事情是将wx+b以非线性的模式转换再赋予给y。如果定义激活函数为g,那么y=g(wx+b)。在Keras如果不给activation指定值,那么就不需要做任何非线性转换。加上激活函数这个概念,我们给出一个完整的图:我们的目标就是求出上图中的参数,权重w和偏置b。

    1.3.3编译模型模型框架搭好后,接着就是优化问题了,在下面complie()函数设定参数,即指定优化方法为随机梯度下降,设定参数,即制定损失函数用均方误差函数。1.3.4训练模型训练模型用fit()函数,把数据x和y传进去。值得注意的是参数epochs=500,epoch中文是期,即整个训练集被算法遍历的次数,这里就是遍历500次模型训练结束。打印出首尾5期的信息,不难发现一开始loss很大13.4237,到最后loss非常小只有3.8166e-05,说明在训练集里的预测值和真实值几乎一致。模型训练之后可以用get_weights()函数来检查参数。返回结果第一个是权重w,第二个偏置b,因此该神经网络模型就是y=1.9973876x-0.99190086≈2x-1。1.3.5评估模型评估模型用predict()函数,将新数据x_new传进去,得到结果8.995028,非常接近2*x_new-1=9。从下图可看出,神经网络从6个数据(深青点)中“学到”了模型(红线),而该模型可用在新数据(蓝点)上。总结一下神经网络全流程:创建模型:用Sequential(),当然还有其他更好的方法,下节讲。检查模型:用summary()编译模型:用compile()训练模型:用fit()评估模型:用predict()虽然本例构建了一个极简神经网络,但是五大步骤一个不少,构建复杂的神经网络也需要这五步,区别在于第1步创建模型时要拼接很多层,第5步要选择更先进的优化器,但万变不离其宗。下两节就来看看两个稍微复杂的神经网络,分别是前反馈神经网络(feedforwardneuralnetwork,FNN)和卷积神经网络(convoluationalneuralnetwork,CNN)。2.前馈神经网络(FNN)上节的极简神经网络太无聊了,但是主要是用来明晰Keras里神经网络的概念而步骤,下面来看看神经网络做一些有趣的事情,预测图像类别。首先看看使用的数据集CIFAR-10(https://www.cs.toronto.edu/~kriz/cifar.html)。该数据集共有60,000张彩色图像,这些图像是32*32,分为10个类,每类6000张图。其中50,000张图像用于训练,另外10,000用于测试。下图就是列举了10个类,每一类随机展示的10张图片:用模块datasets里的load_data()函数来下载数据并对图像的像素做归一化,原来像素在0到255之间,现在归一到0到1之间。对于类别,用模块utils里的函数to_categorical()函数对类别进行独热编码(one-hotencoding)。思路就是把整数用只含一个1的向量表示,比如类别5经过独热编码后变成[0,0,0,0,1,0,0,0,0,0],该向量有10个元素,和类别个数一致,向量只有第5个元素是1(独热?),其他都是0(好冷?)。训练集的前十张图片展示如下:2.1创建模型2.1.1序列式上节已经见识过序列式(sequential)建模了,首先用models.Sequential()创建一个空神经网络,然后不断添加层。本例中有一个打平层layers.Flatten()和三个稠密层layers.Dense()。上面代码给出下图所示的模型:有了感官认识,再来研究代码。为什么需要打平层?因为图像有宽,高,色道三个维度,而打平到一维的过程如下图所示。原始图像(32,32,3)输入打平层(在参数input_shape指定图像维度大小),打平之后变成了一个32*32*3=3072的向量,可以想成现在输入有3072个神经元。之后三个稠密层的神经元个数(参数units)分别为200,150和10,前两个200和150是随便给的或者当成超参数调试出来,但最后一个10是和类别的个数一致。用到的激活函数(参数activation)分别是relu,relu和softmax,前两个relu几乎是标配,但最后一个softmax和任务相关,如果是多分类问题就用softmax。常用的激活函数(activationfunction)如下图所示:ReLU将负输入(x<0)转换成0,正输入(x>0)保持不变。LeakyReLU和ReLU非常相似,唯一区别就是对于负输入(x<0),转换的结果也是一个和输入相关的负数(ax)。Sigmoid将实数转换成0-1之间的数,而这个数可当成概率,因此Sigmoid函数用于二分类问题,它的延伸版Softmax函数用于多分类问题。2.1.2函数式在实操中,我们更习惯用函数式(functional)建模。序列式构建的模型都可以用函数式来完成,反之不行,如果在两者选一,建议只用函数式来构建模型。代码如下:函数式建模只用记住一句话:把层当做函数用。有了这句在心,代码秒看懂。第1行,用Input()接收图像数据。第2行,把Flatten()当成函数f,化简不就是x=f(input)第3行,把Dense(units=200,activation='relu')当成函数g,化简不就是x=g(x)第4行,把Dense(units=150,activation='relu')当成函数h,化简不就是x=h(x)第5行,把Dense(units=10,activation='softmax')当成函数q,化简不就是output=q(x)这样一层层函数接着函数把input传递到output,output=q(h(g(f(input)))),最后再用models.Model将它俩建立关系。2.2检查模型当模型创建之后和使用之前,最好是检查一下神经网络每层的数据形状是否正确,用summary()函数就能帮你打印出此类信息。该模型自动被命名“model”,接着一张表分别描述每层的名称类型(layer(type))、输出形状(OutputShape)和参数个数(Param#)。我们一层层来看InputLayer层被命名成input_1,输出形状为[None,32,32,3],后面三个元素对应着图像宽、高和色道,第一个None其实代表的样本数,更严谨的讲是一批(batch)里面的样本数。

    为了代码简洁,这个样本数在建模时通常不需要显性写出来。Flatten层被命名成flatten,3072就是32*32*3打平之后的个数,参数个数为0,因为打平只是重塑数组,不需要任何参数来完成重塑动作。第一个Dense层被命名为dense,输出形状是200,参数614,600=(3072+1)*200,不要忘了有偏置单元。第二个Dense层被命名为dense_1,输出形状是150,参数30,150=(200+1)*150,同样考虑偏置单元。第三个Dense层被命名为dense_2,输出形状是10,参数1,510=(150+1)*10,同样考虑偏置单元。最下面还列出总参数量(Totalparams)646,260,可训练参数量(Trainableparams)646,260,不可训练参数量(Non-trainableparams)0。为什么还有参数不需要训练呢?你想想迁移学习,把借过来的网络锁住开始的n层,只训练最后1-2层,那前面n层的参数可不就不参与训练吗?2.3编译模型当构建模型完毕,接着需要编译模型,需要设定三点:根据要解决的任务来选择损失函数选取理想的优化器选取想监控的指标编译模型用complie()函数,代码如下:在complie()函数中:对于参数loss,本例是十分类问题,因此用的损失函数是categorical_crossentropy,此外:二分类问题:损失函数是binary_crossentropy回归问题:损失函数是mean_squared_error对于参数optimizer,大多数情况下,使用adam和rmsprop优化器及其默认的学习率是稳妥的。在设定该参数时,也可以通过用名称和实例化对象来调用。名称:'sgd'对象:optimizers.Adam(learning_rate=0.0005)对于参数metrics,也可以通过用名称和实例化对象来调用,在本例中的指标是精度,那么可写成名称:['accuracy']对象:[metrics.categorical_accuracy]注意,指标不会影响模型的训练过程,只是让我们监控模型训练时的表现,损失函数才会影响模型的训练过程。2.4训练模型训练模型不是把所有数据一起丢进去,而是按批量丢进去。在介绍训练模型前,需要明晰几个概念:批量大小(batchsize)指一个批量里的样本个数。下例中总共有24个数据,如果每个批里有6个数据,那么总局可分成4批。期(epoch)指整个训练集被算法遍历一次。当设epoch为20时,那么要以不同的方式遍历整个训练集20次。一次epoch要经历4次迭代才能遍历整个数据集,即样本总数/批量大小=24/6次迭代。20次epoch运行过程如下图所示。训练模型用fit()函数,代码如下:上图给出训练步骤,不难看出训练集被分成1563个堆,每堆含32张图(batchsize)。10个epoch之后,损失函数(categoricalcross-entropy)从1.8472降到1.3696,同时准确率(accuracy)从33.41%提升到51.39%。模型在训练集上可以到达51.39%的准确率,那么它在没见过的数据集上的表现会如何呢?2.5评估模型用evaluate()函数直接看准确率。模型在测试集上的准确率为49.52%,比随机预测一个类别的准确率10%高多了(因为有十类)。由于我们用这样一个非常简单的前馈神经网络来预测图片类别,49.52%的准确率已经算是不错的结果了。用predict()函数比对预测和真实类别。测试集里用10,000张图,类别是10个,因此preds是一个[10000,10]的数组,每一行都是模型对相应图片预测的10个类别的概率,当然所有概率加起来等于1。看看测试集里第一张图片的预测结果:y_test也是一个[10000,10]的数组,每一行都是相应图片真实的类别,因此10个元素有9个零和1个一。看看测试集里第一张图片的真实类别:不难看出,预测结果preds[0,:]中类别四的概率最高0.38579068,而真实类别test[0.:]就是类别四(第4个元素是一)。用np.argmax分别从预测结果preds[0,:]和真实类别test[0.:]中找到最大值对应的索引,并从CLASSES中映射出类别描述。测试集第一张是猫,而模型预测的也是猫,做对了!再试试第四张。测试集第四张是船,但模型预测的是飞机,做错了!可视化:上面的对比方法太麻烦,我们可以随机抽取测试集里的10张,打印出每张图片,在图片下还贴上模型预测类别和其真实类别。从上面10张小图可看出,模型预测正确了5张,正确率50%,和之前统计出来的49.52%吻合。虽然这只是一个用于预测的判别模型,但当我们创建生成模型时,本节介绍的内容(比如层、激活函数和优化器等)仍然适用。下一步来看看如何用卷积神经网络来改进模型。3.卷积神经网络(CNN)前馈神经网络(FNN)在图像分类问题上表现差的根本原因是它没有考虑到图像的空间结构,比如图像中的相邻像素都很接近,而FNN一开始直接将像素打平,破坏图像特有的空间结构。我们需要更适合图像的神经网络,比如卷积神经网络(CNN)。

    3.1基本概念假设在黑夜你面前出现一张巨幅图片,黑暗中你看不出来是辆车,你只能用手电筒一点一点扫过,把每次扫过看到的东西投影到下一层,以此类推。比如第一层你看到一些横线竖线斜线,第二层组合成一些圆形方形,第三层组合成轮子车门车身,第四层组合成一辆车。这样就能用个手电筒在黑夜里辨别出照片里有辆车了。上例其实就是一个卷积神经网络识别图像的过程了,首先明晰几个定义:滤波器(filter):在输入数据的宽度和高度上滑动,与输入数据进行卷积,就像上例中的手电筒。卷积(convolution):在这里的定义就是把所有“滤波器的像素”乘以“滤波器扫过图片的像素”再加总。步长(stride):遍历图像时滤波器的步长,默认值为1,既滤波器每次移动一个像素。填充(padding):有时候会将输入数据用0在边缘进行填充,可以控制输出数据的尺寸(最常用的是保持输出数据的尺寸与输入数据一致)。卷积(Convolution)卷积神经网络的最大特点当然是卷积操作了。回顾上面的定义,将“滤波器的像素”乘以“滤波器扫过图片的像素”再加总,看下面两个例子,假设滤波器的大小是3*3。第一张图片和滤波器的卷积为0.6*1+0.4*1+0.6*1+0.1*0+(-0.2)*0+(-0.3)*0+(-0.5)*(-1)+(-0.4)*(-1)+(-0.3)*(-1)=2.8。第二张图片和滤波器的卷积为(-0.7)*1+0.6*1+0.2*1+0.1*0+0.5*0+(-0.3)*0+(-0.3)*(-1)+(-0.4)*(-1)+0.5*(-1)=-0.1。当卷积值越正,说明滤波器和图片越相符;当卷积值越负,说明滤波器和图片越不符。上例中第一张图片和滤波器的卷积值为2.8,两者相符;第二张图片和滤波器的卷积值为-0.1,两者不符。滤波器(Filter)滤波器的作用就是滤波,即过滤掉一些信息,等价于提取保留下的信息。下面代码创建两个3*3大小的滤波器,filter1能提取图像中的水平线,filter2能提取图像中的竖直线。注意这里1代表黑,0代表灰,-1代表白色。下面看一个如何用这两个滤波器来提取信息的,原始图片如下:不难发现,filter1的确从图像中提取到水平的边缘信息,比如杯子口的上下沿。不难发现,filter2的确从图像中提取到竖直边缘的信息,比如杯子口的左右沿。有了滤波器的加入,我们可以创建卷积层(convoluationallayer)了。卷积层本质上就是一组滤波器,下例中个数是2个,而滤波器中的元素值称为权重(weights),是通过训练CNN学到的。在Keras中用layers.Conv2D()来创建卷积层。这里黑白相片是64*64*1(色道只有1个),而滤波器有两个(参数filters设置为2),滤波器大小是3*3(参数kernel_size设置为(3,3))。还有两个参数strides和padding是什么东西?步长(Stride)步长是滤波器遍历图像时移动的像素个数,默认值为1,既滤波器每次移动一个像素。当步长为2时,不难想象输出图像大小只有输入图像大小的一半。填充(Padding)顾名思义,填充就是在图像四周添加元素。当padding="same"时,配着strides=1,可以保证输出图像和输入图像的大小一样。下图输入图像大小是5*5(蓝色图片),填充之后图像大小变成7*7(带白色的图片),滤波器大小是3*3(灰色),输出图像大小还保持5*5(绿色图片)。弄清楚组成卷积层的元素之后,我们可以像上节拼接稠密层一样来拼接卷积层。3.2拼接卷积层先看一段代码:上段代码对应着下图的样子。上面每个卷积层输出的大小让人眼花缭乱,如果用nI代表输入图像的大小,f代表滤波器的大小,s代表步长,p代表填充层数,nO代表输入图像的大小,那么有以下关系:用这个公式来验证第一个和第二个卷积层的输出的宽度和高度:最重要的东西来了,卷积层的输出色道等于滤波器个数(即代码里面的参数filters)。一个直观理解是每个滤波器并行在“扫描”图片做卷积,那么最终产出一定有一个维度大小是滤波器的个数。检查一下模型。该模型自动被命名“model”,接着一张表分别描述每层的名称类型(layer(type))、输出形状(OutputShape)和参数个数(Param#)。我们一层层来看InputLayer层被命名成input_1,输出形状为[None,32,32,3],后面三个元素对应着图像宽、高和色道,第一个None其实代表的样本数,更严谨的讲是一批(batch)里面的样本数。为了代码简洁,这个样本数在建模时通常不需要显性写出来。第一个Conv2D层被命名为conv2d,输出形状是[None,16,16,10],参数490=(4*4*3+1)*10,首先不要忘了有偏置单元,其次4*4是滤波器的大小,3是输入的色道个数,因此我们需要4*4*3个权重来描述每个滤波器,一共有10个。第二个Conv2D层被命名为conv2d_1,输出形状是[None,8,8,20],参数1,820=(3*3*10+1)*20,首先同样考虑偏置单元,其次3*3是滤波器的大小,10是输入的色道个数,因此我们需要3*3*10个权重来描述这个滤波器,一共有20个。

    Flatten层被命名成flatten,1,280就是8*8*20打平之后的个数,参数个数为0,因为打平只是重塑数组,不需要任何参数来完成重塑动作。最后一个Dense层被命名为dense,输出形状是10,参数12,810=(1280+1)*10,同样考虑偏置单元。最下面还列出总参数量(Totalparams)15,120,可训练参数量(Trainableparams)15,120,不可训练参数量(Non-trainableparams)0。到此一个CNN已经基本建成,我们再添加两个技巧使得CNN效果更好:批量归一(batchnormalization)和随机失活(dropout)。3.3批量归一在训练CNN时,模型成功关键时要确保权重保持在一定的范围内,要不然会出现梯度爆炸(explodinggradient)的情况。批量归一可以解决此问题,它在每层都会按批(mini-batch)计算数据的均值(mean)和标准差(standarddeviation),然后在每个数据上减去均值除以标准差。为了“还原”数据,我们需要“学习”两个参数,放缩参数γ和平移参数β。批量归一的算法如下:Keras中用BatchNormalization()来实现批量归一层。批量归一层一般放在稠密层或卷积层之后。函数中参数momentum用于计算移动均值和移动标准差,这个是为了在预测的时候使用。因为预测通常在一个数据上,这时无法计算均值和标准差,那么只能利用在训练时计算的移动均值和移动标准差。3.4随机失活随机失活的灵感来自考试。通常考试前,学生会做往年的卷子来学习知识点。有的学生死记硬背来解题,这样到了实际考试中就会表现不好,因为他们没有真正理解知识点。好的学生会通过卷子来理解通用的知识点,这样出现新题也能正确解答。同理,为了让神经网络不要“死记硬背”,我们可以随机让某些神经元失活,即使得它们的输出为0,如下图所示。在预测过程中,神经元不失活,因此用完整的神经网络做预测。Keras中用Dropout()来实现失活层。失活层一般放在稠密层之后。函数中参数rate用于设定失活神经元的比率,比如本例中25%的神经元失活了。3.5完整模型现在我们可以在之前的CNN加上批量归一层和失活层来完善模型了。再看上面的代码是不是很好理解了,该CNN中有四个卷积层,每个后面接一个批量归一层和一个LeakyReLu层。注意Keras里时万物皆可作为层,甚至像激活函数也可以用层的形式实现。接着用一个打平层将数据打平,接一个稠密层,个批量归一层,一个LeakyReLu层,一个失活层和一个稠密层,最后用softmax以概率的形式输出。检查一下这个完善后的CNN模型。我们发现激活层都不包含参数,因为就是一个转换;打平层和失活层也不包含参数,这个也很好理解;对于卷积层和稠密层的参数量,之前已经解释过算法;对于批量归一层,对于每个channel需要学习放缩参数γ和平移参数β,以及移动均值和移动标准差,这样包含参数就等于channel个数*4。CNN里面有5个批量归一层,每层里面移动均值和移动标准差只用计算而不需要训练,因此非训练参数为32*2+32*2+64*2+64*2+128*2=640个。3.6训练评估万事俱备,只欠训练。这一次我们增加了参数validation_data,用于监控模型在训练时是否出现过拟合,而过拟合发生在训练误差(loss)一直在减小,但是验证误差(val_loss)却在增加。从下图看还没出现这样的问题。对比现在的卷积神经网络(CNN)和之前的前馈神经网络(FNN),现有模型在训练集的准确率从之前51.39%提升到76.99%,在训练集的准确率也从之前49.52%提升到71.70%,模型性能大大提高。神奇的是,CNN的参数(592,554)其实比FNN的参数(646,260)少很多,但模型性能却提高了不少,而这种提升只需更改模型架构以包括卷积层、批量归一层和失活层即可实现。虽然CNN比FNN的参数少,但是层数确多很多,这就是为什么深度神经网络的优势,因为网络的中间层捕获了我们最感兴趣的高级特征(high-levelfeatures)。从上面10张小图可看出,模型预测正确了6张,正确率60%,虽然之前统计出来的71.70%低,但这个是从10000张测试集中采样出来的10张,因此看到模型正确预判了6,7,8张都是正常的。总结本篇介绍了开始构建深度生成模型所需的核心深度学习概念。使用Keras构建前馈神经网络(FNN),并训练模型来预测CIFAR-10数据集中给定图像的类别。然后,我们通过引入卷积层、批量归一层和失活层来改进此架构,以创建卷积神经网络(CNN)。深度神经网络在设计上是完全灵活的,尽量有最佳实践,但我们可随意尝试不同的层以及其出现的顺序,用Keras实现就像拼乐高积木一样丝滑,你的神经网络的设计仅受你自己的想象力的限制。下篇我们将使用这些模块来设计一个可以生成图像的网络。生成式AI的好戏刚刚开始!