大脑由神经元组成。神经元的工作方式很像一个投票系统。树突用来接收来自其他神经元的信号,细胞核处理信号。如果信号足够强,轴突就负责将脉冲发送给下一个神经元。
这样的过程可以很容易被数学来建模。1943年有数学家尝试用数学来描述这个过程,而他们做的就是把整个过程简化为一个逻辑门:
信号传入 → 信号累加 → 如果超出阈值则输出一,否则输出0
这就是MCP Neuron:一个二元阈值神经元模型。该神经元要么放电,要么不放电。这取决于输入的总和是否达到阈值。
感知机
而在1958年,Frank Rosenblatt提出了感知机的概念。这就是我们要来学习的第一个算法。
假如你去大厂面试,把面试官看成一个感知机,那么一个感知机包含:
输入 (Input | $x$):比如你的各项条件,例如学历,经验和技能。
权重 (Weights | $w$):代表面试官中心中的权重。也许这个面试官更看重经验而不看重学历,那么从“学历”来的数据权重较低,反之经验更高。
净输入 (Net Input | $z$):就是所有的输入值加权求和:
$$z = w_1x_1 + w_2x_2 + ... + w_mx_m$$
你会发现这本质上就是点积。也就是:$z = \mathbf{w}^{\mathbf{T}}\mathbf{x}$
决策函数与偏置
算出总分$z$后,面试官要根据一个规则来决定你会被录用,还是会被淘汰。也就是代表感知机输出0还是1。
在感知机里,这个规则叫做 单位阶跃函数 (Unit Step Function):
在总分为$z = w_1x_1 + w_2x_2 + ... + w_mx_m$的条件下:
如果$z \geq \theta$,则输出1,相反:如果$z < \theta$,则输出-1。
而数学家不喜欢公式右边还要拖着一个的右边还要带着一个阈值$\theta$。为了简洁,我们可以将$\theta$移动到等式左边,并将其重新命名为 偏置 (Bias Unit | $w_0$):
我们原始的总分公式:
$$z = w_1x_1 + w_2x_2 + ... + w_mx_m$$
的决策函数为:
$\phi(z) =$
$$
\begin{cases}
1 & \text{if } z \geq \theta \\
-1 & \text{otherwise}
\end{cases}
$$
但为了简洁,我们可以将阈值$\theta$调到等式的左边,定义:
$$z = w_0x_0 + w_1x_1 + ... + w_mx_m = \mathbf{w}^T\mathbf{x}$$
其中:
- $w_0 = -\theta$
- $x_0 = 1$
决策函数为:
$\phi(z) =$
$$
\begin{cases}
1 & \text{if } z \geq 0 \\
-1 & \text{otherwise}
\end{cases}
$$
我们叫这个$w_0$为偏置值。
引入偏置值还有哪些影响?引入偏置后我们的判断标准统一变成了“是否大于零”。
原来的公式仅仅在超出阈值的时候才算通过激活,也就是$w_1x_1 > \theta$。而引入偏置后的新公式:
$$w_1x_1 - \theta > 0 \rightarrow w_1x_1 + w_0 > 0$$
在计算上更加方便,不需要每次记下不同的偏置值来判断结果。
感知机学习规则 (更新公式)
感知机的学习过程就像你学习一样,做题做错了老师会给你批出来,随后你就知道这里出错了,下次改正。
感知机在开始学习之前,会先经历 初始化 (Initialize) 流程。这一步我们将所有权重 $\mathbf{w}$ 设置为0或者很小的随机数。
最后进入循环流程,遍历每一个训练样本:
- 预测:计算当前权重下的预测值。预测得出布尔类型True或者False,对应0和1。
- 对比:我们将上一步得出的预测值$\hat{y}$和真实标签$y$进行对比。
- 更新:如果对不上,我们就根据 误差 来调整权重 $\mathbf{w}$。
那么聪明的你就会问了,我们是怎么更新权重的呢?
我们使用这个公式来更新权重:
$$w_j := w_j + \Delta w_j$$
$$\Delta w_j = \eta \cdot (y^{(i)} - \hat{y}^{(i)}) \cdot x_j^{(i)}$$
其中:
$w_j$:第 $j$个特征的权重
$y^{(i)}$:真实标签
$\hat{y}^{(i)}$:模型预测
$x_{j}^{(i)}$:第$i$个样本的第$j$个特征值。
$\eta$: 学习率 (Learning Rate)
学习率是一个人为设定的常数,通常为0.01或者0.1。这控制了我们纠正的步子迈的多大。
如果学习率过大容易矫枉过正,学习率太小就会学习的太慢吃一堑一堑一堑一堑 $y^{(i)} - \hat{y}^{(i)}$ 误差 (Error)
误差就是所谓真实值减去预测值。
如果模型猜对了:真实值 $y = 1$,预测 $\hat{y} = 1$,则误差为0。负数同理。
因此在预测正确情况下得到 $\Delta w = 0$。没误差就不做任何更改,权重保持不变。如果模型猜错了。例如:
真实值 $y = 1$,预测值 $\hat{y} = -1$,误差就是 $\Delta w = 1 - (-1) = 2$
$x_j^{(i)}$ 输入值 (Input)
如果某个特征的 $x_j$ 值很大,那么这个值对错误判断的贡献就很大。所以说它的权重需要被惩罚得更重。
那么公式是怎么用的?为什么这个公式有效?
假设我们遇到一个样本,真实标签是正类(1),而我们的模型预测为负类(-1)。这意味着什么?意味着我们的总得分 $z = \mathbf{w}^T\mathbf{x}$ 太低了,算出来小于0。因此对于这个样本,我们需要把 $z$推高。
根据公式 $\Delta w = \eta \cdot (2) \cdot x$,我们可以得到新的权重应该是:
$$w_{new} = w_{old} + \eta \cdot 2 \cdot x^2$$
因此下次我们再算 $w_{new} \cdot x$ 时,会多出一项 $\eta \cdot 2 \cdot x^2$。由于$x^2$肯定是正数,因此总得分$z$就会增加。得分增加,则更可能跨过0的门槛,变成正确的预测值。这就是感知机知错能改的数学本质。
感知机有一个死穴,那就是只有在数据时线性可分的时候,才能够保证收敛。
如果数据线性可分,那么你拿菜刀一刀下去数据能够完美地分开,出现AB两类,干净利落。而线性不可分就是AB两类点掺杂在一起。不管你怎么画直线,总会有分错的数据点。
如果数据线性不可分,那么感知机就永远会有预测错误的值,权重会一直改来改去,永远不会收敛,一直震荡下去。为了防止这种情况,我们通常会设置一个值 max_iter,代表最大迭代次数。当次数达到之后结束训练。
Adaline 与代价函数
Adaline的预测输出
感知机按照布尔类型预测结果。也就是说,对于一个预测考试成绩合格与否的应用,如果一个学生得了98分,另一个学生考虑61分,模型都预测了合格。理论来讲预测的都对,但是模型预测的到底有多对?
由于两个预测都看作完美,因此模型完全不调整权重。实际上61分距离不及格仅一步之遥,感知机看不到这种危险,因为总分 $z$只是预测使用的中间数据:它把具体的数值信息丢掉了。
因此在1960年,Widrow和Hoff提出了 Adaline (Adaptive Linear Neuron),使得模型的预测更加细腻。它不再盯着最后的分类标签来学习,相反,我们盯着具体的连续数值学习。
感知机使用阶跃函数来计算误差:
最后得出的误差值, $\Delta w = y - \hat{y}_{\text{binary}}$,也就是说误差值只能是0, 2或者-2。
而Adaline使用 线性激活函数 (Linear Activation Function) 来计算误差。Adaline的输出一直是:
$$\phi (z) = z$$
意思说神经元的输出就是输入的加权和本身。与其像感知机一样归类为1或者-1,Adaline不再进行二值化变换。这意味着Adaline即使答对了,也会被量化模型到底对了多少。它现在可以意识到“哎就离目标值差了0.1”,于是它依然会根据差距计算微调权重。
这就使得Adaline的学习过程比感知机更加平滑,更加稳健。
代价函数
那么我们现在知道了Adaline的输出逻辑是线性的,也知道Adaline能够感知错了/对了多少,那么Adaline究竟如何根据这些数字来调整自己的权重?
震撼介绍,代价函数 (Cost Function, $J(w)$)。
代价函数是Adaline给机器学习界带来的最大贡献,也就是引入了 **目标函数 (Objective Function)**的概念。在监督学习中,我们往往称其为损失函数。
拓展:代价函数与损失函数
在ML里,我们往往混用代价函数和损失函数两个词语。而严格来讲它们之间亦有区别:
损失函数 是衡量单个样本的预测值与真实值之间的差异,针对单个样本。
例如感知机的误差:$(y - \hat{y})$,Adaline的平方误差:$\frac{1}{2} (y - z)^2$,损失函数是针对单一样本的。
而 代价函数 通常是对所有训练样本的损失函数进行平均或累加,用来衡量整个模型的性能。比如说:
Adaline的代价函数就是:
$$J(w) = \frac{1}{2}\sum_i{(y^{(i)} - z^{(i)})^2}$$
这个是所有样本的平方误差总和。代价函数是放眼全局的。
不过我们在深度学习里,大家往往都直接说“损失函数”来指代整个训练目标函数。
与其像感知机那样错一个改一个,不如我们定义一个全局指标:
“现在的这组权重 $\mathbf{w}$,在整个训练集上表现有多差?”
Adaline使用 误差平方和 (Sum of Squared Errors, SSE) 来计算:
$$J(\mathbf{w}) = \frac{1}{2} \sum_{i} (y^{(i)} - \phi(z^{(i)}))^2$$
其中:
$y^{(i)} - \phi(z^{(i)})$ 是真实值减去预测的连续值,也叫做残差。
注意到我们在公式中对残差进行了平方计算。由于我们不关心残差的方向,仅关心大小,所以我们将所有残差变为正数。
其次,我们想要拉宽所有的残差值,这样我们就能够严厉惩罚大误差。错的越多,惩罚越重。
求和 $\sum$ 用来将所有样本误差加起来,算总账。
$\frac{1}{2}$ 纯粹是为了数学方便。由于我们要在后续对 $J$进行求导,那么平方项导出来的2就刚好能够跟这个抵消,优美数学。
如果我们把权重 $w$ 当作横坐标,代价函数 $J(w)$ 当作纵坐标画出来,那么我们可以发现这个函数看起来像一个碗:
碗底代表误差最小的地方,这个位置代表我们梦寐以求的最佳权重。
由于代价函数是抛物线形状,因此我们保证这个函数只有一个最低点。我们只要不断迭代,就一定能够找到最优解。
梯度下降
梯度下降 (Gradient Descent)是整个深度学习训练的基石。想象一下我们被蒙住双眼,放在了一座山上的随机位置。这个山就是我们刚才说到的代价函数上的某一点。
而我们的目标是找到这个山谷的最低点,也就是 全局最小值(Global Minimum) ,代表能够让误差最小的参数值。
而我们并不知整个山谷地形,要找到最低点,我们只能够用脚来探。那么你在这个情况下,会怎么做?
你大概会首先感受一下脚下的坡度,看看哪个方向最陡峭。
随后你朝着下坡的方向迈一步,到达一个新的位置,再次探一下脚下的坡度。
当什么时候你发现脚下是平地,那么说明我们到谷底了。这就是梯度下降的核心思路。
在数学上,坡度可以很容易的被衡量:导数。想要找到这个山坡在某个点的坡度,就是$\nabla J(\mathbf{w})$
由于我们有多个权重 $w_1, w_2...$,对于这样一个多元函数来说,梯度就是一个由所有偏导数组成的向量。
我们可以得出权重更新公式为:
$$\mathbf{w} := \mathbf{w} + \Delta \mathbf{w}$$
$$\Delta \mathbf{w} = -\eta \nabla J(\mathbf{w})$$
其中的$\eta$仍然是我们的老朋友学习率,决定一步迈多远。
你会发现误差部分里面存在一个符号。由于梯度指向的是函数增长最快的方向,也就是上坡方向,我们需要取反来找到下坡的方向。
Adaline 权重更新公式的推导
我们希望通过最小化代价函数 $J(\mathbf{w})$ 来找到最优权重。Adaline 使用的是误差平方和(SSE)代价函数:
$$J(\mathbf{w}) = \frac{1}{2} \sum_{i} (y^{(i)} - \phi(z^{(i)}))^2$$
其中 $\phi(z^{(i)}) = z^{(i)} = \mathbf{w}^T \mathbf{x}^{(i)}$ 是线性激活函数。
为了找到使 $J(\mathbf{w})$ 最小的权重,我们计算代价函数对每个权重 $w_j$ 的偏导数(即梯度):
$$\frac{\partial J}{\partial w_j} = \frac{\partial}{\partial w_j} \frac{1}{2} \sum_{i} (y^{(i)} - \phi(z^{(i)}))^2$$
根据链式法则,我们先对平方项求导,再对括号内的项求导:
$$\begin{aligned}
\frac{\partial J}{\partial w_j} &= \sum_{i} (y^{(i)} - \phi(z^{(i)})) \cdot \frac{\partial}{\partial w_j} (y^{(i)} - \phi(z^{(i)})) \\
&= \sum_{i} (y^{(i)} - \phi(z^{(i)})) \cdot (0 - \frac{\partial \phi(z^{(i)})}{\partial w_j}) \\
&= \sum_{i} (y^{(i)} - \phi(z^{(i)})) \cdot (-x_j^{(i)}) \\
&= -\sum_{i} (y^{(i)} - \phi(z^{(i)})) x_j^{(i)}
\end{aligned}$$
由于梯度 $\nabla J(\mathbf{w})$ 指向的是函数值增加最快的方向,为了最小化代价函数,我们需要沿着梯度的反方向更新权重:
$$\Delta w_j = -\eta \frac{\partial J}{\partial w_j}$$
将偏导数代入上式:
$$\Delta w_j = -\eta \left( -\sum_{i} (y^{(i)} - \phi(z^{(i)})) x_j^{(i)} \right)$$
最终得到最终的 Adaline 权重更新公式:
$$\Delta w_j = \eta \sum_{i} (y^{(i)} - \phi(z^{(i)})) x_j^{(i)}$$
你会发现,这个公式在形式上与感知机的更新公式非常相似,但是它与感知机的公式还是存在本质区别。这里的 $y - \phi(z)$ 是一个连续的线性输出,而不是感知机中二值化的步阶输出。而使用求和符$\sum$,则是Adaline基于所有样本的误差综合来更新权重的过程。我们管它叫做 批量梯度下降 (Batch Gradient Descent)
调整学习率
虽然原理简单,但是实际操作中学习率 $\eta$非常难调。
如果学习率太小,那么学习过程会巨慢无比,像蜗牛一样蠕动,可能需要算上百万次才能够走到谷底。
如果学习率太大,那么一步太大了就可能跨过谷底,直接冲到对面山坡上。这样的情况叫做 超调 (Overshooting)。你会发现误差不仅没有变小,反而越来越大,最后你有可能得到一个漂亮的 $\text{NaN}$。

所以我们一般会在一开始把步子迈大一点,快到谷底时让步子小一点。这也就是后来高级优化器所做的事情,但是在Adaline中我们通常将学习力设置为一个固定的中间值,比如0.01。
实战优化技巧
为了能让梯度下降跑的更快更稳,这里介绍三个实战技巧:
- 特征缩放 (Feature Scaling)
假设特征$x_1$是房价,范围从0到1000000,$x_2$是房间数,范围1-5。
由于房价数值过大,房间数数值太小,那么想象$J(w)$的形状:会变成一个拉的非常长的椭圆碗。梯度下降会在长轴方向上来回震荡,很难走到中心位置。我们对付这个问题的武器是:标准化 (Standardization):
$$ x'_j = \frac{x_j - \mu_j}{\sigma_j} $$
我们对数据减去平均值,随后除以标准差。这样所有的特征都变成了以0为中心,方差为1的分布,碗就变成了正圆,梯度下降这样就可以直接指向圆心。
- 随机梯度下降 (Stochastic Gradient Descent, SGD)
但假如我们的数据量非常多,该怎么办?
假设我们一共有一百万条数据,每走一步需要计算更新一次$w$,那么我们需要将所有的100万条数据全部算一遍误差求和。太慢了哥们。
而SGD的策略是我们不求和了。我们每次只随机抽取一个样本,算它的误差,然后立即更新权重。
这样的算法优点是速度极快。而且由于它随机选点,因此它更有机会帮助训练跳出局部最优解问题,在深度学习里很有用。
拓展:全局最小值与局部最小值
什么是局部最优?不是说是一个完美的碗吗?怎么会有多个最小值?
实际上对于Adaline和线性回归来说,代价函数确实只有一个最小值,但是在真实世界中,看起来是一个非凸函数,是一片崎岖的山脉:

当进入深度神经网络后,由于现代神经网络使用Sigmoid, ReLU, Tanh等非线性函数,因此一旦引入非线性,那么代价函数就不再是简单的二次抛物面了。
而深度学习是函数套函数: $f(g(h(x)))$。这种复杂的复合函数会让代价函数的表面看起来崎岖不平。
那么解释清楚成因了,什么是局部最优? 局部最优 (Local Minima) 是山腰上的一个小坑,球滚进去就出不来了。但是放眼整个区域,这不是这片区域里面最低的点,也就不是对这组数据误差最小的点。我们要找的是全局最优点。
除了局部最优,这样的地形也会出现 鞍点 (Saddle Point)。在斜率正负方向上,前后看这里是最低点(平地),向左右看是最高点,那么这一点的梯度计算出来是0,容易骗过算法。
同时也可能出现 平原 (Plateau) 地形。很简单,在这里我们就无法继续下降了。
所以总的来说,Adaline 没这个问题,但 Gradient Descent 这个工具在未来处理复杂模型时,会面临 Local Minima 的风险。而 Stochastic Gradient Descent (SGD) 的随机抖动特性,恰好能帮我们要跳出这些小坑。
那么代价是什么呢?它的梯度下降路径会很抖,看起来像是喝醉了酒下山。但最终还是会因为样本多了而平均掉,接近最优解。
- 小批量梯度下降 (Mini-Batch Gradient Descent)
这个是Batch和SGD的折中方案,也是目前深度学习的主流方法。
我们每次不取一个,也不取全部,相反求得中庸之道,每次取一小批,比如32个或者64个,叫做batch size,也是我们在训练模型中最常调节的参数之一。
这样我们既利用了矩阵运算的并行加速,还避免了一次性把大数据全部读进内存。