从单个神经元开始 [one_neuron_1]
从单个神经元开始 [one_neuron_1]
现在深度学习算法五花八门,我总觉得自己不能很好地学会繁多的算法。所以我决定从头开始思考,从最简单的结构开始,看看自己能走到哪里。过程中会参考已有的材料,希望尽量易懂一些。
这篇博客只研究一个问题:
如果我们从最小的二分类模型开始,它能表达什么?又在哪里失败?
0. 机器学习的几个基本前提
先不急着谈神经网络。我们可以先问一个更基础的问题:
一个机器学习模型至少需要具备哪些要素?
在我的理解中,至少有下面几条。
0.1 学习需要可表示的输入
机器不能直接学习现实世界本身,它只能学习被编码后的经验,这种经验就像 图片会被表示成像素矩阵。 文本会被表示成 token 序列。 表格样本会被表示成特征向量。 通常是被某种仪器采样且近似存储到计算机中进行处理的
也就是说,学习算法要某种输入 x :
这里的 x 不是现象本身,而是仪器经过测量后得到的数值表示。
0.2 学习发生在某个可计算的模型空间里
机器学习不是在所有可能规则里随意搜索,我们必须先规定模型长什么样,不然怎么计算呢。
例如一个模型可以写成:
其中:
- x 是输入;
- \hat y 是模型输出;
- \theta 是模型参数;
- f_\theta 是某个可计算的函数。
学习不是凭空产生规则,而是在一个给定的模型族里在特定的标准下寻找合适的参数。 这意味着模型本身带有先验。
例如,如果我们选择线性模型,就等于假设有用的规律可以被线性关系捕捉。 如果我们选择高斯模型,就等于假设输入服从的分布近似为高斯分布 如果我们选择神经网络,就等于假设复杂关系可以由许多简单的可计算单元组合出来?(我还不知道。)
0.3 学习需要评价准则
模型需要知道什么叫“好”,基于所谓的好,模型才能进行迭代更新。
在监督学习中,评价准则通常是损失函数:
其中 y 是真实标签,\hat y 是模型预测。
学习目标可以写成:
没有评价准则,就没有“改进”的方向。
0.4 模型内部处理的是数值,语义由任务赋予
模型输出的首先是数值,计算模型的输出都是数值,具体语义是人类赋予的
例如:
或者:
这些数值本身没有天然语义。 它们的语义来自任务定义。
例如,在垃圾邮件分类中:
可以被解释为“这是垃圾邮件”。
在医学分类中:
可以被解释为“存在某种风险”。
所以机器学习模型内部处理的是数值结构,输出之前模型会处理成基于人类的语义结果。
0.5 模型总是带有先验
没有完全中立的学习模型。 选择一个模型,就是选择一种看世界的方式。 例如:
- 线性模型假设某些关系可以近似为线性关系;
- 决策树假设可以通过一系列条件判断划分数据;
- 卷积神经网络假设局部结构和平移共享很重要;
- Transformer 假设 token 之间的关系可以通过注意力机制建模。
这一点可以从著名的NO free lunch定理也可以看出,
1. 从最小的非平凡可计算模型开始
那看看最简单的模型能做些什么吧。
首先,”能做些什么“的概念很宽泛,我们且假设最简单的模型能够拟合什么函数。 为什么这样呢,因为函数是显式表达变量间关系的映射,是某种规律的直观体现,存在抽象的函数关系,我们就能更好地了解某些现象,我个人认为是数学抽象化的处理
严格说,最简单的模型可以是常数模型:
它当然可计算,但它不看输入。 它没有真正表达“输入如何影响输出”。
所以我们从一个稍微有意义的最小模型开始:
一个接收输入、带有参数、能够根据输入改变输出的模型。
线性表达式是最简单的可计算关系之一:
如果我们直接输出 s,它更像是一个回归或打分模型。
但如果我们研究的是二分类,那么还需要把这个分数变成 0 或 1。最简单的方法就是加一个阈值判断:
于是我们得到线性阈值神经元。
2. 线性阈值神经元
2.1 为什么从线性阈值神经元开始?
在讨论神经元之前,需要先说清楚一个问题:
这里的模型到底要做什么?
机器学习模型的基本形式可以写成:
也就是:给定输入 x,模型输出一个预测结果 \hat y。
但是输出结果可以有很多种形式。
如果 \hat y 是一个连续数值,例如房价、温度、概率分数,那么这是回归或打分问题。
如果 \hat y 是一个类别,例如“是/否”“正类/负类”“猫/狗”,那么这是分类问题。
本文先研究最小的分类问题:二分类。
也就是:
此时模型要做的事情很具体:
给定一个输入 x,判断它属于类别 0 还是类别 1。
2.2 二分类模型需要什么?
根据我们之前的提出的公理,一个简单模型应该是
那分类模型的输出,无非是类比1或类别2,那这个没得说。 那在进行类别输出之前,总要做点计算吧,那最简单的计算又是什么呢?线性计算,很显然了。
所以,综上,一个二分类模型至少需要两步。 第一步:根据输入 x 计算出一个分数。 第二步:把这个分数转化成类别 0 或类别 1。 也就是:
其中 s 是模型内部计算出的分数,\hat y 是最终分类结果。
2.3 线性阈值神经元的定义及特点
把前面的两步合在一起,就得到线性阈值神经元。
第一步,计算线性分数:
第二步,进行阈值判断:
对第二步骤进行一些说明,针对计算出来的分数进行处理,进行二分类,很自然地想到分段函数取0和1,那这个标准为什么以0为分界线呢,就是为了简单。
更一般地,我们也可以写成:
也就是:
移项得到:
令:
就回到了:
所以把阈值写成 0 不会损失一般性。
所以,线性阈值神经元可以理解为:
一个先用线性表达式打分,再用阈值做二分类判断的最小模型。
线性阈值神经元包含两个假设。
第一个是假设可以用线性分数描述输入:
也就是说,模型认为输入特征可以通过加权求和形成一个有意义的判断分数。
第二个是假设分类可以通过一个阈值完成:
也就是说,模型认为只要分数超过某个临界值,就可以归为一类;否则归为另一类。
合起来就是:
这就是线性阈值神经元的完整逻辑。
可以试试下面的代码,用来展示二维情况下,一个神经元如何用一条直线划分平面。
ClearAll["Global`*"];
score2D[x1_, x2_, w1_, w2_, b_] := w1*x1 + w2*x2 + b;
neuron2D[x1_, x2_, w1_, w2_, b_] := Boole[score2D[x1, x2, w1, w2, b] > 0];
Manipulate[
Show[
RegionPlot[
neuron2D[x1, x2, w1, w2, b] == 1,
{x1, -3, 3},
{x2, -3, 3},
PlotPoints -> 60,
FrameLabel -> {"x1", "x2"},
PlotStyle -> LightBlue,
BoundaryStyle -> None
],
ContourPlot[
score2D[x1, x2, w1, w2, b],
{x1, -3, 3},
{x2, -3, 3},
Contours -> {0},
ContourStyle -> {Red, Thick},
ContourShading -> False
],
PlotRange -> {{-3, 3}, {-3, 3}},
ImageSize -> Large,
PlotLabel -> "A linear threshold neuron cuts the plane into two half-spaces"
],
{{w1, 1, "w1"}, -3, 3, 0.1, Appearance -> "Labeled"},
{{w2, 1, "w2"}, -3, 3, 0.1, Appearance -> "Labeled"},
{{b, 0, "b"}, -3, 3, 0.1, Appearance -> "Labeled"}
]
红线是:
蓝色区域是:
这说明一个神经元不是在“理解”平面上的点。 它只是用一个线性条件,把平面切成两边。
3. 单个线性阈值神经元的能力边界
因为线性阈值神经元只能产生一个线性边界,所以它只能解决线性可分的二分类问题。
如果存在某个 w,b,使得所有正类样本都在边界一侧,所有负类样本都在另一侧,那么这个问题就是线性可分的。
这时,一个线性阈值神经元就有可能完成分类。
但如果客观条件下,正负样本无法被一条直线分开,那么一个线性阈值神经元就不可能完成分类。
这不是训练技巧的问题,而是模型表达能力的问题,怎么学也学不会的。
下面举一个经典的例子,用线性神经元来尝试逻辑函数,进行发现线性神经元的表达限制。
这个例子初看很反直觉(至少反我的直觉)。怎么初等函数就跳到逻辑函数上了,逻辑函数在模拟或者数字电路上就能计算得很好了。
关键不在于用神经元替代逻辑门,而在于:它们是最小的二输入二分类任务。
现在假设输入空间是二维的, AND、OR、XOR 都接收两个二值输入:
输出也是二值:
所以它们都是最小的分类任务。
每个任务只有四个输入点:
这四个点刚好是二维平面中一个正方形的四个角。
因此,我们可以把逻辑问题变成分类问题,因为逻辑值是两个,分类问题的值也是两个:
能不能画一条直线,把输出为 1 的点和输出为 0 的点分开?
现在来看一看这些函数的真值表,真值表的意思是输入分别对应了什么输出
AND 的真值表是:
| x_1 | x_2 | AND |
|---|---|---|
| 0 | 0 | 0 |
| 1 | 0 | 0 |
| 0 | 1 | 0 |
| 1 | 1 | 1 |
OR 的真值表是:
| x_1 | x_2 | OR |
|---|---|---|
| 0 | 0 | 0 |
| 1 | 0 | 1 |
| 0 | 1 | 1 |
| 1 | 1 | 1 |
XOR 的真值表是:
| x_1 | x_2 | XOR |
|---|---|---|
| 0 | 0 | 0 |
| 1 | 0 | 1 |
| 0 | 1 | 1 |
| 1 | 1 | 0 |
结果很简单,可以试着画一画,AND 和 OR 都可以被一条直线分开,所以它们是线性可分的。
但XOR的输出在正方形的两条对角线上,这时一条直线无法把正负样本分开。 假设存在:
可以实现 XOR。
因为 (0,0)\mapsto 0,所以:
因为 (1,0)\mapsto 1,所以:
因为 (0,1)\mapsto 1,所以:
后两式相加:
又因为 b\le 0,所以:
但 XOR 要求 (1,1)\mapsto 0,所以:
矛盾。
一个线性阈值神经元做不到XOR操作,这是最简单的例子了。
4. 线性阈值神经元失败之后
XOR 失败以后,我们当然可以直接换模型。比如写一个专门的 XOR 规则,或者用决策树,甚至回到逻辑门电路。
但是,聪明的逻辑学家一拍脑袋,说了一句“尤里卡”,直接给出了一个很好的表达式:
我们检查一下这个式子:
| x_1 | x_2 | x_1x_2 | s=x_1+x_2-2x_1x_2 | XOR |
|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 |
| 1 | 0 | 0 | 1 | 1 |
| 0 | 1 | 0 | 1 | 1 |
| 1 | 1 | 1 | 0 | 0 |
这个式子确实能区分 XOR。 如果再加一个阈值,例如:
那么它就能完整实现 XOR。 但这里有一个问题:这个表达式已经不是原始输入 x_1,x_2 上的线性函数了,因为它包含了乘积项:
也就是说,在原始二维空间里,我们没有找到一条直线来分开 XOR。 逻辑学家的做法其实是偷偷引入了一个新的特征:
于是原来的输入:
被变换成了三维特征:
也可以写成:
其中:
这时,刚才那个看起来非线性的表达式:
就可以重新写成:
这个式子在新的特征空间 (x_1,x_2,x_3) 里是线性的。
也就是说,我们没有改变线性模型的形式,我们改变的是输入表示。
原来模型做不到:
但加入新特征后,模型可以写成:
取:
就得到:
由于:
所以它等价于:
这说明了一件很重要的事:
XOR 在原始二维空间里不是线性可分的;但经过特征变换以后,它可以在新的三维特征空间里变得线性可分。
在原始空间里,我们只有两个坐标:
四个点是:
这四个点无法被一条直线分开。
但在新的特征空间里,它们变成:
| 原始输入 | 新特征 x_3=x_1x_2 | 三维特征点 | XOR |
|---|---|---|---|
| (0,0) | 0 | (0,0,0) | 0 |
| (1,0) | 0 | (1,0,0) | 1 |
| (0,1) | 0 | (0,1,0) | 1 |
| (1,1) | 1 | (1,1,1) | 0 |
在这个三维空间里,一个平面就可以把正负样本分开:
平面一侧输出 1,另一侧输出 0。
这时我们可以说:
非线性问题并不一定要用非线性分类器解决。
有时,我们可以先做一个非线性特征变换,然后在新的特征空间里继续使用线性分类器。
下面用 Mathematica 可视化这个三维特征空间。
ClearAll["Global`*"];
(* XOR 的原始输入 *)
rawPoints = {{0, 0}, {1, 0}, {0, 1}, {1, 1}};
labels = {0, 1, 1, 0};
(* 新增特征 x3 = x1 x2 *)
featurePoint[{x1_, x2_}] := {x1, x2, x1*x2};
points3D = featurePoint /@ rawPoints;
pos = Pick[points3D, labels, 1];
neg = Pick[points3D, labels, 0];
plane =
ContourPlot3D[
x1 + x2 - 2*x3 - 0.5 == 0,
{x1, -0.2, 1.2},
{x2, -0.2, 1.2},
{x3, -0.2, 1.2},
ContourStyle -> Directive[Opacity[0.35], Red],
Mesh -> None
];
points =
Graphics3D[
{
Blue, PointSize[Large], Point[pos],
Black, PointSize[Large], Point[neg],
Text[Style["1", Blue, 16], # + {0.05, 0.05, 0.05}] & /@ pos,
Text[Style["0", Black, 16], # + {0.05, 0.05, 0.05}] & /@ neg
}
];
Show[
plane,
points,
Axes -> True,
AxesLabel -> {"x1", "x2", "x3 = x1 x2"},
BoxRatios -> {1, 1, 1},
PlotRange -> {{-0.2, 1.2}, {-0.2, 1.2}, {-0.2, 1.2}},
ImageSize -> Large,
PlotLabel -> "XOR becomes linearly separable after adding x3 = x1 x2"
]
但是,这里还有一个没有解决的问题。这个新特征是逻辑学家直接想出来的 :(
我们可以请教他为什么要这样构造。他也许会解释半天:XOR 表示两个输入不同,x_1x_2 正好能检测“两者是否同时为 1”,所以把它减掉两倍就能修正 OR 的结果。
可是智者的思路往往难以琢磨。对一个复杂问题,我们未必知道应该手工构造什么特征。
于是问题来了:
能不能通过模型本身的设计,让它在调整参数的过程中自己构造新的特征?
如果可以,那就太好了。
因为这意味着我们不必每次都指望智者一拍脑袋。 我们可以设计一个模型,让它从原始输入出发,逐步学出中间表示,下一篇文章我们会讨论这个问题。