深度学习入门
1. 背景
从去年底以来,AIGC 炙手可热,多个业界大佬都认为 AIGC 会给整个产业带来一场革命,甚至所有的软件都会用 AI 重写。从历史上来看,人机交互方式的变革往往会将操作系统带入下一个世代,著名的例子如从命令行界面的 DOS 到键鼠图形界面的 Windows,以及带来触控界面的 iPhone,领创者都成为了世界顶级企业,带动了整个生态的发展。
从技术上来看,AIGC 是基于大模型的,而大模型的基础是深度学习,因此,为了在产品上结合 AIGC,首先从技术上首先需要对深度学习进行有深度的学习。
对深度学习与大模型的探索将由一系列文章组成,本文是系列里的第一篇,主要关注的是深度学习的技术入门探索。
2.从神经元开始
回溯历史,深度学习起始于向人类的大脑学习如何学习。人类大脑皮质的思维活动就是通过大量中间神经元的极其复杂的反射活动,因此不妨先看看神经元的工作机制。
图 1 神经元结构
图 1 给出了神经元的大体结构,左边是神经元的主体,其输入是左侧的多个树突,其输出是右侧的一个轴突。只有当输入树突的信号足够强烈的时候,输出轴突上才会有信号产生。受此启发,就可以设计一个最简单的有两个输入 x1 与 x2,以及一个输出 y 的线性函数来模拟单个神经元,引入阈值θ,当 w1x1 + w2x2 ≥ θ时,y 为 1(表示有信号),否则 y 为 0(表示无信号)。其中 w1 与 w2 分别是 x1 与 x2 的参数或权重(weight)。
有了这个函数,下面来看看它究竟能做什么。按照逻辑主义的设想,数学可以通过逻辑推衍出来,那么不妨看看,上面的函数是否可以表征出基本逻辑运算,如与、或、异或等,在这里 x1、x2 与 y 的取值都只能是 0 或 1。
对于逻辑与来说,只有当 x1 与 x2 都是 1 的时候,y 才是 1,否则 y 是 0,容易尝试得到一组可能的 w1、w2 与θ,分别是 0.5、0.5 与 0.7,如图 2 所示。
图 2 逻辑与的线性函数图
图 2 中横轴为 x1,纵轴为 x2,从图 2 中可以看到,(1, 1) 点为实心圆,表示 y 为 1,在(0, 0)、(0, 1)与(1, 0)都是空心圆,表示 y 为 0,中间的虚线表示 w1x1 + w2x2 = θ这条直线,只要这条直线能将(1, 1)点与其它点划分到不同区域,则显然就可以找到至少一组 w1、w2 与θ满足条件。基于同样的分析,容易知道逻辑或也可以找到对应的 w1、w2 与θ。但是对于逻辑异或来说,问题就严重了,显然无法找到满足条件的 w1、w2 与θ,如图 3 所示。
图 3 逻辑异或的函数图
逻辑异或是当 x1 与 x2 中一个为 0,另一个为 1 时 y 才为 1,否则 y 为 0,因此在图 3 中,点(0,1)与点(1,0)为实心圆,而(0, 0)与(1, 1)为空心圆,显然是无法找到一条直线将两个实心圆与两个空心圆划分在两个不同区域的。因此,上述最朴素的线性神经元函数无法表示逻辑异或,也就意味着有大量的运算无法通过上述线性神经元函数来进行。
3. 引入激活函数
是否能改造上述函数,让它能支持所有运算,从而能承担学习的任务呢?至少,人脑肯定是能学会异或的。现在看来,主要是因为原始的神经元函数太线性导致的这个问题。因此,在深度学习中,就引入了非线性的激活函数(activation function),如图 4 所示。
图 4 引入激活函数
在图 4 中,首先原函数被修改成了支持多个输入和多个输出的线性变换函数,这样就能处理更多种类的问题了。因为有了多个输入 x1、x2...xm 与多个输出 h1、h2...hn,因此权重的下标也带有两个数字,以表示每个权重的作用,例如 w12 是输入 x2 与输出 h1 间的权重。还有一个特殊的权重 bi,它被称为偏置(bias),是一个待确定的常数项。这样,h 就等于相应的 x 与 w 相乘后再加上 b。例如,hi = xiwi1 + x2wi2 + ... + xmwim + bi。
经过线性变换后得到的输出 h1、h2...hn 只是中间过程的输出,在之后,还需要加入一个非线性的激活函数的处理,以得到最终的输出 y1~yn,如图 4 所示。
在具体激活函数的选择上,比较常见的有 softmax、sigmoid 与 relu 等。其中 softmax 函数是多分类问题最常用的输出激活函数(多分类问题指的是一个问题有多个确定个数的可能答案,例如是/否问题是二分类问题,而分辨一个手写阿拉伯数字是哪个数就是一个十分类问题,因为可能答案有 0~9 一共十个),softmax 也是包括 ChatGPT 在内的大模型使用的输出函数。
使用了激活函数以后,神经网络就可以学习到所有函数了。下面来看一个经典的神经网络的例子,手写数字识别问题,或 MNIST 问题。MNIST 涉及的手写数字在网上是公开的,如图 5 所示。程序员们可以先想想,如果自己来写一个程序识别手写数字会怎么写。可以识别手写数字的(一个)神经网络的结构如图 6 所示。
图 5 MNIST 手写数字样例
图 6 能识别手写数字的神经网络
可以看到图 6 的神经网络一共用到了三个线性变换,并使用了两个 sigmoid 激活函数,以及最后的 softmax 激活函数,因此可以说这个神经网络是三层的。神经网络的输入(x1~x784)是一个长度为 784 的数组,其实就是一个 28x28=784 的手写数字的黑白图像。神经网络的输出(y1~y10)分别代表了 0~9 的阿拉伯数字,这是一个典型的十分类问题,因此使用 softmax 也是非常自然的。
图 6 中的神经网络一共有(784x50+50) + (50x100+100) + (100x10+10) = 45360 个参数,对比 ChatGPT 上千亿个参数,这显然是一个微模型,但是它的识别能力却可以达到 92.53%,也就是说一万个手写数字,它能正确识别出 9253 个来。
那问题就来了,这 45360 个参数是怎么来的呢?肯定不能是随便什么 45360 个数都能带来这么高的识别率的,要解决这个问题,就需要看看神经网络是怎么学习的了。
4. 神经网络的学习
在上面已经看到,神经网络里有大量的参数。在最开始,这些参数会被随机分配一些数字(当然如何随机分配也有讲究的,简洁起见,此处先不提),此外也需要准备大量的数据,这些数据一般是多个输入输出的对(x, t)。例如在上面的手写数字识别问题中,输入 x 就是一个 28x28 的手写数字图像,输出 t 就是这个图像对应的 0~9 中的一个数字。
这些数据会被分成训练集与测试集。训练集中的数据用来训练神经网络,让神经网络中的参数最终达到正确的值。测试集中的数据用来测试训练后的神经网络,对比看训练后的神经网络在新的数据下得到的结果是否正确。
神经网络的训练过程可以大体分为下面几步:
l 对训练集中的输入输出对(x, t)进行如下处理
l 将 x 输入到神经网络中,计算得到 y
l 将 y 与正确的输出 t 进行运算得到损失 L,损失的计算函数一般是均方差或交叉熵,前者针对的是回归问题(连续函数拟合),后者针对的是分类问题
l 根据 L 调整神经网络的参数,调整的方向是减少 L,调整的方法是下面要讲的反向传播
图 7 给出了神经网络训练的过程。
图 7 神经网络训练过程
一旦训练完毕,使用的时候就不需要正确输出 t,也不需要计算损失 L 和调整神经网络的参数了,这个过程被称为推理(inference),如图 8 所示。
图 8 神经网络推理过程
顺便说一句,图中的深度神经网络与神经网络结构是一样的,但是层数较多,因此被称为深度神经网络。
下面,再来看看神经网络究竟是怎样通过损失 L 来调整网络参数的。最简单,也是最直观的方法就是将每个参数都稍微调大或者调小一点,看 L 会如何变化,如果 L 变小,则保持此参数的调整,如果 L 变大,则将此参数反过来调整。以上即正向调整法,思路清晰,操作方法简单,但是计算量极大,因为每调整一个参数就要重新计算一遍 y 与 L。
另一种方法就是现在主流的反向传播(BP,backpropagation)法,此方法类似系统发生故障时的根因分析,首先分析最后一层的参数是怎样影响到 L 的,然后分析倒数第二层的参数是如何影响到最后一层的输入的,如此类推。在数学上,其实就是计算 L 对某个特定参数 w 的(偏)导数,因为导数就代表了 w 的变化会导致 L 如何变化。根据链式求导法则,L 对 w 的导数等于 L 对中间变量 h 的导数乘以 h 对 w 的导数,前者相当于计算最后一层参数的导数,后者相当于计算倒数第二层参数的导数,两者相乘即为 L 对导数第二层参数的导数。
下面主要通过求导来展示反向传播,如果希望更直观一点,可以阅读计算图相关的资料。假设真实函数是 y=2x+1,则待求函数为 wx+b(当然 w 与 b 的真实值应该是 2 与 1)。下面通过一组数据(训练集)来通过反向传播逐步计算更新 w 与 b,看看它们否会逐渐逼近 2 与 1。
由于这是一个回归问题,因此使用均方差(y-t)2/2 作为损失 L 的函数,显然 L 对 y 的导数是 y-t,参数更新使用经典的梯度下降法(SGD),即参数新值=参数旧值 - 学习率 x(L 对参数的导数),在这里学习率设为 0.01。
首先,将 w 与 b 随机化为 0.5 与 0.6。
假设第一个训练对为(0, 1),则 y = wx + b = 0.5·0 + 0.6 = 0.6,L 对 w 的导数=L 对 y 的导数乘以 y 对 w 的导数=(y-t)·x=(0.6-1)·0=0,L 对 b 的导数=L 对 y 的导数乘以 y 对 b 的导数=(y-t)·1=-0.4。则 w 的新值为 w-0.01·0=0.5,b 的新值为 b-0.01·(-0.4)=0.604,显然新的 w 与 b 比原来的更接近(2, 1)。
若第二个训练对为(1, 2.9)(本来应为 1 与 3,但是增加了一点误差干扰),可以以同样的方法得到新的 w 为 0.51796,而新的 b 为 0.62196,显然比上一对 w 与 b 又接近了 2 与 1 一点。
实际上,若继续增加 2x+1 附近的数据,可以发现到了十几对训练数对之后,w 与 b 即可相当接近 2 与 1 了。
以上例子是为了直观感受反向传播的计算而给出的,实际上这种线性函数的回归可以通过数据集基于矩阵一次性算出来,而且训练本身也要考虑收敛的问题,因此实际的深度学习会更复杂一些,但是原理是类似的。
总地来说,深度神经网络是由多个层组成的,每一层均有前向(forward)推理的函数,用来从输入计算得到输出,这个过程即为推理。每一层也有反向(backward)传播的函数,用来从后一层传来的导数计算得到本层向前一层传递的导数,并同时更新本层的参数。如果是训练,则需要在最后一层再加上一个输入为 t 与 y 的损失层,输出为 L,如图 9 所示。
图 9 多层神经网络结构
通过以上几乎标准化的神经网络层,深度学习的研究者就可以像搭积木一样对多个层进行排列组合,得到多种多样的深度神经网络,并首先通过反向传播训练出神经网络的参数,继而使用神经网络进行推理应用了。
评论