网上对机器学习的课程和书籍普遍较难,本文希望以最深入浅出的方式带小白(最好大二水平)入门机器学习,从而步入人工智能的道路。
本文需要读者提前掌握的技术如下:
- Python 编程基础(Numpy)
- 线性代数基础(矩阵计算,如乘法,求逆,转置等)
- 多变量微积分基础(求梯度)
- 概率(高中水平即可)
- (选)算法基础
目录
一、机器学习介绍
机器学习的核心是“使用算法解析数据,从中学习,然后对世界上的某件事情做出决定或预测”。机器学习在多个学科发挥着重要作用,在图像识别和处理方面,如人脸识别、车牌识别、图像检索、物体识别等。通过训练大量的图像数据,机器学习算法可以学习并识别出图像中的特征,从而实现对图像的智能处理;机器学习在医疗和生物科学领域也有重要的应用,如医学影像分析、药物研发、基因分析等。机器学习算法还可以帮助医生更准确地诊断疾病和制定治疗方案,同时也可以帮助科研人员更好地理解和研究生物科学问题。除此之外,机器学习还可以应用于交通、农业、物流、教育、政府等领域,为各个行业带来更高的效率和更好的服务。随着技术的不断发展和进步,机器学习的应用场景还将不断扩大和深化。
机器学习需要解决两个问题:
- 预测:当我们的数据符合潜在规律时,我们可以聚合数据并对数量进行估计或预测。但我们如何预测估计值与未来结果的比较效果如何?当预测值与实际不符时我们该如何做?
- 泛化:我们如何把使用过的模型复用到别的模型上?
我们把上面的两个问题分解一下:
- 问题分类:这个数据集有什么特征,我们应该把它分到哪个问题中?
- 基本假设:数据集的数据是否满足某些统计规律,如标准正态分布?
- 评估标准:预测结果是否满足预期?准确率如何?
- 模型训练:适合训练数据的模型是什么?我们该如何用这个模型训练数据?
- 模型选择:在寻找的一堆模型中如何选择最优的模型?在同一个模型中参数该如何调整?
- 算法:在拟合数据或预测时需要用到什么算法?
1、问题分类
机器学习问题分类主要分为以下几类:
1.1 监督学习
在监督学习中,算法从带有标签的数据集中学习,即每个样本都有一个已知的结果(如垃圾邮件/非垃圾邮件)。算法通过学习这些例子之间的关联性来建立一个模型,以便对新数据进行预测。
1.1.1 分类
给定训练数据集
其中。 表示一个示例或样本,通常用维数为 d 的向量表示, 表示目标值。
如果 的种类为 2 ,则称这个分类问题为二分类,涉及多个类别时,则称为多分类。
我们的分类目标是给定 的值,来预测 的值。
分类问题是监督学习的一种,因为对于训练集中的每一个 ,都有唯一与之对应的 。
1.1.2 回归
与分类类似,但 。
1.2 无监督学习
在无监督学习中,算法处理的是没有标签或结果的数据集,目标是发现数据中的内在结构、模式或关系。
1.2.1 密度估计
给定样本 且这些样本独立同分布(独立同分布详见 独立同分布_百度百科 (baidu.com)),则我们的目标是预测相同分布下 的概率 。密度估计在监督学习中有时也发挥着重要作用。
1.2.2 聚类
给定样本 ,我们的目标是将样本划分为若干个通常是不相交的子集(也称为“簇”),划分的结果取决于样本之间的相似度以及划分的标准(例如在同一个簇内元素之间平均距离要最小,不同簇之间平均距离要最大等等)。聚类可以揭示数据的内在性质及规律,有时可以用于密度估计。
1.2.3 降维
给定样本 ,我们希望让这些样本重新在 维空间表示,其中 。我们的目标通常是在数据集中只保留有用的信息,比如区分不同类的特征等。
降维被广泛运用于高维数据的观测与分析。注意,如果我们在降维后想对数据进行回归或分类,则通常最好提前确定对预测很重要的维度并保留,以免降维时丢失特征。
1.3 强化学习
在强化学习中,我们的目标是学得一个由输入 到输出 的映射,但没有直接的监督者来指定特定输入对应的最优输出 。强化学习没有先验训练集,但它能通过智能体与环境的交互过程中运用学习策略以达成回报最大化或实现特定目标。
- 智能体观测当前环境状态 ;
- 智能体执行操作 ;
- 智能体得到一个奖励 ,其中 取决于 (有时也取决于 );
- 环境状态“转变”为 (转变可能性取决于 和 );
- 智能体观测当前环境状态 ;
- ……
我们希望找到一个策略 ,表示由输入 到输出 的映射(即状态到操作)使得在长期训练中奖励 的和(或平均值)最大。
与监督学习和无监督学习不同,强化学习中智能体的操作选择会影响其奖励和当前环境状态,需要仔细考虑操作的长期影响。
1.4 序列学习
在序列学习中,我们的目标是找到由输入序列 到输出序列 的映射,这个映射通常用状态机表示。在状态机中,函数 用于计算给定输入的下一个隐藏内部状态,另一个函数 用于计算给定当前隐藏状态的输出。
序列学习比较像监督学习,因为我们需要知道不同的输入序列要生成什么不同的输出序列,但内部函数必须通过监督学习以外的方法来得到,因为我们不知道隐藏内部状态是什么。
1.5 其他
半监督学习是一种介于监督学习和无监督学习之间的方法,用于处理部分标记的数据集,即存在样本 ,没有与之对应的目标值 。在这种情况下,算法利用少量有标签的数据以及大量的未标记数据进行学习。
在主动学习中,获取标签 往往比较困难(比如让人去读取 x 光片上的信息),因此主动学习地目的是通过标记少量的数据训练处表现较好的模型。其核心任务是制定选择样本的标准,从而选择尽可能少的样本进行标注来选出一个好的学习模型。
在迁移学习/元学习中,有多个任务,其数据分布不同但相关。目标是将先前任务的经验应用于学习当前任务,从而减少学习新任务的工作量。
2、基本假设
我们可以对数据源的特征规律进行基本假设,例如:
- 数据之间独立且遵循相同的统计规律
- 数据生成遵循马尔科夫链
- 数据生成的过程具有对抗性
基本假设的目的是减少数据的“复杂度”或者“可解释性”,从而降低训练数据时的工作量。
3、评估标准
当我们确定问题分类后,要对训练后数据的准确度进行评估。这里我们要考虑两个方面:单个样本预测准确度和总体样本预测准确度。
对于预测的准确度通常用损失函数 来表示,也就是预测值 g 和实际值 a 的偏差幅度。这里列举几个常见的损失函数:
- 0-1 损失函数:
- 平方损失函数:
- 线性损失函数:
- 非对称损失函数:解决了多标签分类任务中,正负样本不平衡问题,标签错误问题
我们可能会用同一个模型对某一个数据集做大量预测,这时我们要考虑以下几点:
- 如何减小预测损失(也称风险)
- 如何减小最大损失(不同预测结果中损失最大的值)
- 如何增强预测的稳定性(缩小损失最大和损失最小预测之间的差异)
- 当数据量增加时,这个模型预测还是否准确
- 如何改进我们的模型
4、模型训练
接下来我们的目标是基于数据集寻找用于训练数据的模型。
4.1 无模型
当训练数据集相当简单时,比如简单的分类和回归问题时,可以通过求平均值来对数据进行预测,典型算法是最邻近搜索。
4.2 预测规则
模型训练分为两个步骤:
- 用训练数据来“拟合”模型
- 用训练好的模型来预测数据
在分类和回归问题中,模型可以用一个假设表示: ,其中 h 是一个函数, 是我们通过拟合数据训练得到的向量,给定 ,我们就可以可以预测 。
拟合的过程可以看成一个优化问题:找到合适的 使得在某个评估标准下预测损失最小。在预测时形成的损失叫做测试误差,在训练时形成的损失叫做训练误差。我们可以通过减小训练误差来缩小测试误差。也就是找到合适的 使得训练误差
最小。其中 表示预测值 g 和实际值 a 的偏差大小。
有些时候减小训练误差并不能减小测试误差,模型在训练样本中表现过于优越,可能导致在验证数据集和测试数据集中表现不佳,这种现象也叫做过拟合。
5、模型选择
一个模型类 通常包含许多模型,每个模型训练后的参数列表都可以用向量 表示。举个例子,当我们想解决一个回归问题时,我们要找到一个线性函数 来拟合训练数据,此时参数向量 。
当我们想解决分类或判别式模型的问题时,模型类可能会很大,在神经网络方面这个问题会更加突出。我们会限制参数的数量,采用“非参数”模型。
我们应该如何选择模型类呢?在某些情况下,我们要根据自己的实际需求来选择合适的模型类。模型选择是指根据给定的训练数据、测试数据以及其他条件,选取最适合用来解决特定问题的机器学习模型,而参数选择是找到合适的参数来拟合模型。在上述例子中, 是我们要选择的模型类, 是我们拟合的参数。
6、算法
当我们知道了如何评估训练结果,如何训练模型后,我们还有一个问题没有解决:应该通过什么算法找到最优模型?举个例子,如何选择 使得训练误差 最小?
有时我们可以通过数学推导或软件计算来解决,但某些模型的优化则十分复杂。接下来我们会重点关注线性分类问题和感知器算法,让大家对机器学习有一个具体认知。
二、线性分类
1、分类
二元分类是一个映射: ,我们通常用 代表一个分类器,这样这个分类过程可以表示为:
在现实生活中, 通常为我们想分类的事物,比如图片,数字或人。我们可以定义一个函数 来表示 的特征,定义域为 ,并令 。但通常情况下,我们不用 这个函数来表示 的特征,而是用 ,其中 。用 的目的是方便大家理解。
在监督学习中,给定数据集
假设每个 是 大小的列向量,这个数据集的意思是给定输入 ,输出为 。
我们训练分类器有什么用呢?当我们无法确定现实生活中事物的分类时,这个分类器可以对这些新数据进行预测。
给定数据集 和分类器 ,我们定义分类器 的训练误差为
接着我们通过算法(之后会讲)尝试减小训练误差,并将其泛化到测试集上,测试误差为
其中, 是新数据,不参与分类器的训练。
2、学习算法
假设类 是一组分类器,其中每一个分类器表示 到 上的映射。
学习算法是一段程序,它把数据集 作为输入,返回假设类 中的一个元素 ,如下所示:
我们发现假设类 的选择对 的测试误差影响较大,一种获得较好的 的方法是降低 的大小或复杂度。
3、线性分类器
线性分类器相比于其他模型较为容易,可解释性强,效果明显,也是其他机器学习模型的基础。
在 维空间的线性分类器由一个向量 和一个标量 构成,令 为一组在 维空间的线性分类器,假设 是 大小的列向量,那么它是在 空间中所有向量的集合。
给定 和 的值,线性分类器定义如下:
由于 和 都是 大小的列向量,那么 是一个标量。
我们可以把 看作一个超平面,它把每一个点 所在的 空间分成两部分,与该超平面法向量方向相同的空间为正,与该超平面法向量方向相反的空间为负。
例:令 是由 , 构成的线性分类器,下图表示了该分类器对不同点的分类,我们取其中两个点进行分析:令 , ,则
因此, 分类为正, 分类为负。
4、线性分类器的训练
接下来我们要对线性分类器进行训练,使得训练误差最小。这是一个优化问题,我们将采用一个比较简单的算法来完成:随机生成 个参数不同的线性分类器,通过计算并比较训练误差,选择训练误差最小的线性分类器,算法伪代码如下:
Random-Linear-Classifier(, k, d)
1 for j = 1 to k:
2 randomly sample(, ) from (, )
3
4 return (, )
符号解释: 表示使得 的值最小的 值; 表示在集合 内使得 的值最小的 值。
5、对线性分类器的评估
我们该如何评估线性分类器 的表现?最好的办法就是在测试集上去验证。
我们该如何评估学习算法的表现?这个就有些难度了。在不同线性分类器 的测试误差中,有许多潜在的不确定性:
- 中的训练样本
- 中的测试样本
- 学习算法本身的随机性
我们可以多次执行以下操作:
- 在新的训练样本上训练
- 在不包含训练样本的测试集上评估
多次执行上述操作可以将训练集的部分错误选择或算法本身的问题随机化。
但有一个问题:我们需要大量的数据去实现,并且操作的时间成本相当大。当然,我们可以采用交叉验证来重复使用数据(原理分析详见《统计学习要素》),算法伪代码如下:
Cross-Validate(, k)
1 divide into k chunks , , . . . (of roughly equal size)
2 for i = 1 to k
3 train on \ (withholding chunk )
4 compute “test” error on withheld data
5 return
交叉验证不是单独评估某个假设 ,而是评估整个学习算法的。
实践:线性分类器的实现
1、点到超平面的距离
假设 是 空间内一点,超平面方程为 ,令 代表点 的位置,如下图所示:
在超平面上任取一点 ,则该点的位置为 ,那么 到超平面的向量为 。
求得 到超平面的距离为:
化简,得:
把超平面方程 代入,得:
2、点到超平面的距离代码实现
在 signed_dist(x, th, th0) 中,x 和 th 是大小为 的列向量,th0 是一个标量,(th, th0) 是超平面的两个参数,函数 length(col_v) 返回列向量的长度。
import numpy as np
def length(col_v):
return np.sum(col_v * col_v) ** 0.5
def signed_dist(x, th, th0):
return ((th.T @ x) + th0) / length(th)
3、判断点在超平面的位置
在 positive(x, th, th0) 中,x 和 th 是大小为 的列向量,th0 是一个标量,返回
- +1,x 在超平面正侧
- 0,x 在超平面上
- -1,x 在超平面负侧
import numpy as np
def positive(x, th, th0):
return np.sign(np.dot(np.transpose(th), x) + th0)
4、线性分类器的评估
在 score(data, labels, th, th0) 中,
- data 是大小为 的矩阵,表示 个 大小为 的列向量构成的数据集;
- labels 是大小为 的数组,元素是 (-1, 0, 1) 中的元素,表示目标值;
- th 是大小为 的列向量;
- th0 是一个标量,和 th 表示超平面的参数
返回 positive 函数的输出和目标值相同的点的个数。
import numpy as np
def score(data, labels, th, th0):
return np.sum(positive(data, th, th0) == labels)
5、线性分类器的选择
假设我们随机生成了 个分类器,所有 用大小为 的矩阵表示, 用大小为 的矩阵表示,每一对 (, ) 都表示其中一个分类器的参数。
在 best_separator(data, labels, ths, th0s) 中,
- data 是大小为 的矩阵,表示 个 大小为 的列向量构成的数据集;
- labels 是大小为 的数组,元素是 (-1, 0, 1) 中的元素,表示目标值;
- ths 是大小为 的矩阵,表示所有 ;
- th0s 是大小为 的矩阵,表示所有
函数通过计算每一个线性分类器的得分,返回得分最高的分类器。如果最高得分相同,则返回其中第一个分类器。
import numpy as np
def score_mat(data, labels, ths, th0s):
pos = np.sign(np.dot(np.transpose(ths), data) + np.transpose(th0s))
return np.sum(pos == labels, axis = 1, keepdims = True)
def best_separator(data, labels, ths, th0s):
best_index = np.argmax(score_mat(data, labels, ths, th0s))
return cv(ths[:, best_index]), th0s[:, best_index:best_index + 1]
三、感知器
感知器是 Frank Rosenblatt 在1957年就职于康奈尔航空实验室时所发明的一种人工神经网络。它可以被视为一种最简单形式的前馈神经网络,是一种二元线性分类器。在人工神经网络领域中,感知器也被指为单层的人工神经网络,以区别于较复杂的多层感知器。作为一种线性分类器,(单层)感知器可说是最简单的前向人工神经网络形式。尽管结构简单,感知器能够学习并解决相当复杂的问题。
1、算法
给定数据集 ,其中每条数据 , 。通过感知器算法迭代 轮训练二元分类器 ,找到合适的 和 。感知器算法伪代码如下:
Perceptron(, )
1 =
2 = 0
3 for t = 1 to
4 for i = 1 to n
5 if
6
7
8 return ,
如果当前 和 对 的分类是准确的, 和 不会发生修改;如果对 的分类错误,算法将调整 和 使其对 的分类更为准确(原理详见 李航《统计学习方法》)。
如果 i 迭代到 n 且 和 没有发生任何修改,说明当前分类器将数据集完美分类。
如果数据集是线性可分的,那么感知器算法最终一定能找到最优的线性分类器。
例 1:令 是由 , 定义的线性分类器,下图展示了 对数据集的分类,其中,点 的标签为 ,分类器分类错误,因为
通过感知器算法进行一次迭代,我们对分类器的参数进行更新:
现在,线性分类器就分类正确了。
2、过原点的分类器
为了便于分析,我们将 删去,现在分类器不仅穿过原点,还可以对更高维度的数据进行分类。于是该分类器可以表示为:
假设分类器为 维空间下的分类器,则参数列表为: , 。
对于任意 ,我们增加一个维度,其值为 ,则
定义
则
因此, 是经过原点、在 维空间的分类器参数。
举个例子,给定数据集:
则 维线性分类器参数为 , ,但该分类器不过原点。
修改数据集为高维数据:
则易得新数据集的分类器参数为 ,且该分类器过原点。
过原点的分类器的感知器算法伪代码如下:
Perceptron-Through-Origin(, )
1 =
2 for t = 1 to
3 for i = 1 to n
4 if
5
6 return
3、感知器理论
现在,我们将正式地介绍一下感知器算法的实际效果如何。我们首先给定感知器算法可以完美解决的一组问题,然后证明它实际上可以解决这些问题。此外,我们还会提供一个概念,它代表感知器算法的训练难度,并将其与算法将采取的迭代次数联系起来。
3.1 线性可分
若存在 , ,使得对于所有 ,满足
则说明训练集 是线性可分的。
即在训练集上的所有预测都是正确的:
即训练误差为 0 :
3.2 收敛定理
前面讲过,如果数据集是线性可分的,那么感知器算法最终一定能找到最优的线性分类器。
我们用“间隔”更加详细地表示数据集的线性可分性,然后从点到超平面间隔的定义开始讲解。
首先,点 到超平面 的距离为
于是我们定义样本点 到超平面 的间隔为
当且仅当样本点 分类正确时间隔大小为正。
现在,我们定义数据集 到超平面 的间隔为所有样本点到该超平面间隔的最小值,即
当且仅当数据集中所有样本点分类正确时间隔大小为正,即离超平面最近的样本点于超平面的距离(含方向)为正时间隔大小为正。
例 2:令 是由 , 定义的线性分类器。
下图展示了由 分类的几个样本点,其中有一个分类错误,我们计算一下它们的间隔:
由于点 分类错误,所以它的间隔为负,因此整个数据集的间隔为 。
定理(感知器收敛定理):简单起见,我们令分类器过原点,即 ,则该定理可以表示为:
(a) 存在 ,对于任意 存在 使得 ,
(b) 所有样本点均收敛:对于任意 , ,
则感知器算法在产生最多 次错误的情况下,得到最优分类器。
证明:我们初始化 ,并定义 为感知器算法产生了 次错误时的超平面参数。我们可以从当前分类器 与最优分类器 的夹角入手。由于 ,所以如果我们能证明在迭代过程中当前分类器与最优分类器的夹角不断变小,则可以说明当前分类器正在不断接近最优分类器。
运用点积的定义,两个分类器夹角的余弦值为
分解,得
我们首先关注第一项。
假设感知器算法第 次错误在第 个样本点 上产生,则代入条件 (a) ,化简得
我们再关注第二项,由于 分类错误, 。因此,代入条件 (b),
于是,代入到两个分类器夹角的余弦值,得
由于该余弦值始终小于 1 ,则
这个定理证明了在给定数据集 和该数据集的间隔 的条件下,当使用感知器算法分类时,分类最多产生 次错误。其中, 表示离原点最远的训练数据的距离。
实践:感知器的实现
1、感知器实现
在函数 perceptron(data, labels, params = {}) 中,
- data 是大小为 的矩阵,表示 个 大小为 的列向量构成的数据集;
- labels 是大小为 的数组,表示目标值;
- params 是函数中可能需要额外用到的参数,例如迭代次数 T ,用字典表示;
函数返回一个包含 和 的元组,其中 是大小为 的数组, 是大小为 的数组。
import numpy as np
def positive(x, th, th0):
return np.sign(th.T@x + th0)
def perceptron(data, labels, params = {}):
# if T not in params, default to 100
T = params.get('T', 100)
(d, n) = data.shape
theta = np.zeros((d, 1))
theta_0 = np.zeros((1, 1))
for t in range(T):
for i in range(n):
x = data[:,i:i+1]
y = labels[:,i:i+1]
if y * positive(x, theta, theta_0) <= 0.0:
theta = theta + y * x
theta_0 = theta_0 + y
return theta, theta_0
2、平均感知器实现
一般感知器算法受当前样本点影响较大,而平均感知器算法输出结果更加稳定。平均感知器算法和感知器算法的训练方法一样,不同的是在每次训练样本后,保留先前训练权值,训练结束后平均所有权值即可。算法伪代码如下:
Averaged-Perceptron(, )
1 , =
2 , = 0
3 for t = 1 to
4 for i = 1 to n
5 if
6
7
8
9
10 return ,
import numpy as np
def positive(x, th, th0):
return np.sign(th.T@x + th0)
def averaged_perceptron(data, labels, params = {}):
T = params.get('T', 100)
(d, n) = data.shape
theta = np.zeros((d, 1)); theta_0 = np.zeros((1, 1))
theta_sum = theta.copy()
theta_0_sum = theta_0.copy()
for t in range(T):
for i in range(n):
x = data[:,i:i+1]
y = labels[:,i:i+1]
if y * positive(x, theta, theta_0) <= 0.0:
theta = theta + y * x
theta_0 = theta_0 + y
if hook: hook((theta, theta_0))
theta_sum = theta_sum + theta
theta_0_sum = theta_0_sum + theta_0
theta_avg = theta_sum / (T*n)
theta_0_avg = theta_0_sum / (T*n)
return theta_avg, theta_0_avg
3、评估
3.1 对分类器的评估
要想评估一个分类器,我们要在测试集上运行训练后的分类器,然后输出分类的正确率(0.0 到 1.0 之间的浮点数)。
在函数 eval_classifier(learner, data_train, labels_train, data_test, labels_test) 中,
- learner 是一个函数,表示我们的训练算法,例如 perceptron 或 averaged_perceptron 等;
- data_train 是训练集;
- labels_train 是训练标签,即目标值;
- data_test 是测试集;
- labels_test 是测试标签,即预测值;
函数 score 在线性分类器的实现中讲过,返回 positive 函数的输出和目标值相同的点的个数。
整个函数返回分类的正确率(0.0 到 1.0 之间的浮点数)。
import numpy as np
def eval_classifier(learner, data_train, labels_train, data_test, labels_test):
th, th0 = learner(data_train, labels_train)
return score(data_test, labels_test, th, th0)/data_test.shape[1]
3.2 对学习算法的评估
3.2.1 存在数据源
在函数 eval_learning_alg(learner, data_gen, n_train, n_test, it) 中,
- learner 是一个函数,表示我们的训练算法,例如 perceptron 或 averaged_perceptron 等;
- data_gen 是数据源函数,该函数返回一个元组 (data, labels) ;
- n_train 表示训练集的大小;
- n_test 表示测试集的大小;
- it 表示函数迭代的次数;
返回在整个数据源分类的平均正确率(0.0 到 1.0 之间的浮点数)。
import numpy as np
def eval_learning_alg(learner, data_gen, n_train, n_test, it):
score_sum = 0
for i in range(it):
data_train, labels_train = data_gen(n_train)
data_test, labels_test = data_gen(n_test)
score_sum += eval_classifier(learner, data_train, labels_train,
data_test, labels_test)
return score_sum/it
3.2.2 数据集固定
在前面讲过,我们可以采用交叉验证来评估该学习算法。给定学习算法 ,固定数据集 和参数 ,通过迭代 次学习算法,每一次迭代都会计算分类器的正确率,最后返回平均正确率。这也被称为“k 折交叉验证”。
交叉验证的每一次迭代,分类器的训练集都会改变。当 时,该交叉验证也被称为留一交叉验证。
import numpy as np
def xval_learning_alg(learner, data, labels, k):
s_data = np.array_split(data, k, axis=1)
s_labels = np.array_split(labels, k, axis=1)
score_sum = 0
for i in range(k):
data_train = np.concatenate(s_data[:i] + s_data[i+1:], axis=1)
labels_train = np.concatenate(s_labels[:i] + s_labels[i+1:], axis=1)
data_test = np.array(s_data[i])
labels_test = np.array(s_labels[i])
score_sum += eval_classifier(learner, data_train, labels_train,
data_test, labels_test)
return score_sum/k
四、特征表示
虽然线性分类器容易分析,可解释性强,但它也有一定的局限性,比如做一些更复杂的分类等。下图为“异或”数据集,可以看出来线性分类器不适合对其分类。
对于异或数据集是没有合适的线性分类器的,但我们可以通过非线性映射把低维数据集变成高维数据集,然后再寻找合适的线性分类器,我们来看一个一维的例子:
显然,这些点不是线性可分的。接着我们把它们映射到 上,现在这些点在 空间就线性可分了,我们在 空间选取了其中一个线性分类器,如下图所示:
空间的线性分类器在原空间其实是一个非线性分类器。在我们的例子中,假设分类器为 ,那么在 的超平面内,分类为正。在一维空间内,令 ,则得到 和 ,说明在原空间内,分类器为两个点,如下图所示:
这个方法十分有用,是核函数的基础(详见《机器学习》周志华),也可以看作多层神经网络的激活函数。
我们有很多种构建 空间的方式,首先我们会先讲多项式基,然后再讲特征表示。
1、多项式基
当特征全部是数值时,我们可以采用多项式基的方式构建特征空间。如果我们使用 阶多项式基,那么特征空间就是输入在 个不同维度空间的乘积,如下表所示:
Order | In General | |
---|---|---|
0 | ||
1 | ||
2 | ||
3 | ||
... | ... | ... |
我们通过构建 空间把低维空间数据映射到高维空间,再运用感知器算法,从而解决异或问题。
假设 ,则特征变换函数
经过 4 轮迭代后,感知器算法找到了合适的分类器,其系数为 , ,即
如下图所示,灰色部分分类结果为负,白色部分分类结果为正。
我们改变数据点的位置,重新执行上述操作,经过 65 次迭代后,分类器系数为 , ,如下图所示:
对于复杂数据集,不同维数的多项式基对应分类器也不同, 下图分别代表维数为 2,3,4,5 的多项式基经过感知器算法 200 次迭代后生成的分类器:
2、现实数据特征构建
在上一节中,我们采用多项式基的方式对数值数据进行特征构建。但在机器学习的应用中,输入数据不仅有数值,还有单词,句子,图片等等。因此我们要采用编码对这些现实数据进行特征构建。
2.1 离散特征
对离散特征进行良好的编码尤为重要。尽管有些机器学习方法具有处理离散输入的特殊机制,但我们目前都暂且认为输入 。因此,我们必须找出一些合理的策略,将离散值转换为实数。
我们先列举一些编码方式,然后举例说明。
-
标签编码:标签编码将每个类别映射到整数值,从 0 开始递增。这种方法对于具有有序关系的类别特征很有用,但它不适用于没有明显顺序的类别。
-
序号编码:在机器学习中,序号编码是一种将离散特征的各个类别映射为整数序号的方法。序号编码适用于有序特征,其中类别之间存在一定的顺序关系,但没有明确的意义。
-
独热编码:这是最常用的方法之一,它将每个离散属性的每个类别创建一个新的二进制特征。对于每个样本,只有一个二进制特征为 1 ,表示它属于对应的类别,其他特征为 0 。这种方法适用于具有有限数量的类别。
-
频数编码:频数编码将每个类别替换为该类别在数据集中的频数或出现次数。这种编码方法可以提供关于类别的频率信息,但可能引入一定的信息泄漏。
-
目标编码:目标编码将离散属性的每个类别编码为其在目标变量上的平均值或其他统计信息。目标编码能够捕获类别与目标变量之间的关联性,但需要注意信息泄漏和过拟合的问题。
举个例子,假设我们想对血型进行编码。一般情况下,血型一共有 4 个取值(A 型血、B 型血、 AB 型血、O 型血),这里我们采用独热编码。独热编码会把血型变成一个 4 维稀疏向量,A 型血表示为(1, 0, 0, 0) ,B 型血表示为(0, 1, 0, 0) ,AB 型表示为(0, 0, 1, 0) ,O 型血表示为(0, 0, 0, 1)。
2.2 文本特征
自然语言处理面临的文本数据往往是非结构化杂乱无章的文本数据,而机器学习算法处理的数据往往是固定长度的输入和输出。因而机器学习并不能直接处理原始的文本数据。必须把文本数据转换成数字,比如向量。
在自然语言处理和文本分析的问题中,词袋模型(BOW)是其中一种最常用的模型。所谓词袋模型就是:首先将训练样本中所有不重复的词放到这个袋子中构成一个词表(字典);然后再以这个词表为标准来遍历每一个样本,如果词表中对应位置的词出现在了样本中,那么对应位置就用 1 来表示,没有出现就用 0 来表示;最后,对于每个样本来说都将其向量化成了一个和词表长度一样的0 - 1 向量。
2.3 数值特征
如果某些特征已经编码为数值(心率、股票价格、距离等),那么我们应该将其保留。但我们如果要对数值特征做明显区分时则需要进行修改:例如,在对某人的年龄进行编码时,我们可能会针对机器学习的需求明确区分 18 岁以下和超过 18 岁,将年龄数据划分为离散的区间。在这个情况下,我们不希望年龄和某些特征之间存在线性(甚至单调)关系。
在某些情况下,我们需要将连续数据转变到一个更小的范围(例如 )内。于是我们需要对数据进行缩放。将数据缩放到较小的幅度不仅可提高计算效率,还可以增加特征系数的可解释性。我们可以通过映射函数 将数值特征映射到 上,其中 是 的平均值, 是 的标准差。这个操作也叫做标准化。经过标准化后的特征平均值为 0 ,标准差为 1 。 的值有时被也称为“z值”。
实践:独热编码实现
假设特征 的值为集合 中任意元素,如果特征 的值为 ,使用独热编码表示该特征,则生成长度为 、除了第 个元素为 ,其余全为 的向量。
在函数 one_hot(x, k) 中,x 为特征 的值,k 为特征总数,输出经过独热编码后的向量。例如,one_hot(3, 7) 生成 [0, 0, 1, 0, 0, 0, 0] 。
import numpy as np
def one_hot(x, k):
v = np.zeros((k, 1)) # Make an empty column vector
v[x-1, 0] = 1 # Set an entry to 1
return v
五、Logistic 回归
1、机器学习的优化问题
几乎所有的机器学习算法最后都归结为求解最优化问题,以达到我们想让算法达到的目标。 为了完成某一目标,需要构造出一个目标函数来,然后让该函数取极大值或极小值(也就是优化),从而得到机器学习算法的模型参数。
我们定义目标函数 ,其中 表示模型中的所有参数。举个例子,当我们使用线性分类器时, ,因此目标函数为 。为了表示与数据集 之间的联系,目标函数有时也写为 。定义目标函数的目的是评估所有预测参数 ,我们希望寻找合适的 使目标函数 最小,即
在机器学习中,目标函数 通常表示为
其中, 是损失函数,表示我们对预测 的满意程度。 是一个常数, 是正则项(之后会讲)。在最开始对机器学习的介绍中,我们讲过 0-1 损失函数:
当预测正确时,返回 0 ;预测错误时,返回 1 。在线性分类时,损失函数就变为
2、正则化
在前一节中,我们引入了目标函数 ,但如果我们的目的仅仅是使损失函数最小,那么我们为什么需要 这一项呢?注意,我们的最终目标是让测试误差最小。如果我们有非常多的特征,我们通过学习得到的假设可能能够非常好地适应训练集,但是可能会不能推广到新的数据,因此我们需要正则化。正则化是一种用于减少模型过拟合的技术,通过对模型的复杂度进行惩罚来实现。在机器学习中,正则化通过向模型的损失函数中添加一个额外的项来实施,这个额外的项是模型权重的函数,用于惩罚模型权重的大值。
举个例子,下图为两个分类器 和 。其中, 的训练误差为 0 ,但复杂度非常高; 有两个点分类错误,但复杂度较低。在机器学习中,我们一般遵循“奥卡姆剃刀"原则,即若有多个假设与观察一致,则选最简单的那个。当测试数据发生小幅度变化时,分类器 比 受影响更小,因此 优于 。
我们一般定义正则项为
其中,我们可以规定在训练时 接近 。当然,在默认情况下 等于 0 ,因此
3、线性 logistic 分类器
在之前的分类问题中,我们使用 0-1 损失函数在 上做预测。然而,在目标函数中找到合适的 和 则是十分困难的:
这个问题是“NP 难”问题(详见《算法导论》),无法找到合适的算法。所以我们采用指数法来解决它。
这个复杂的优化问题难在“不平滑”,比如
- 对于训练得到的两种参数 和 ,它们都有相同的 值,但该目标函数并没有考虑训练数据离分类器的距离大小,只考虑训练数据在分类器的何种位置。
- 是离散的,不同的 没有对应不同的 。
因此,我们定义新的训练模型:线性 logistic 分类器。该模型的参数仍为 和 ,其中 是一个 维向量, 是一个标量。然而,该分类器在 区间上做预测,而不是 上。我们可以定义线性 logistic 分类器(这个定义其实不规范,之后会讲):
该分类器与线性分类器很相似,其中, 为 logistic 函数,也称作 sigmoid 函数,定义为
如下图所示,因为函数值域为 ,故 可以用来表示概率。
那么线性 logistic 分类器长什么样呢?简单起见,我们假设维度 ,并定义三个分类器,分别为 , 和 ,如下图所示。
注意,该函数严格意义上来说并不是分类器,在线性分类这一章开头我们讲过,分类是 在离散集上的映射,然而我们的“分类器”是映射到连续空间的。
给定线性 logistic 分类器,我们该如何如何在 做预测呢?由于 可以用来表示概率,我们可以假设当 时返回 ,反之返回 。值 我们一般称作预测阈值。
在实际问题中,针对不同的需求我们会选择不同的预测阈值,在决策论中,我们会基于贝叶斯推断进行选择。举个例子,当预测值为 ,实际值为 所造成的影响远大于当预测值为 ,实际值为 时,我们选择的预测阈值会大于 。
当维数 时,输入是一个由 和 构成的平面,输出是一个曲面,如下图所示。其中参数 , 。
4、logistic 分类器的损失函数
在前一节中,我们定义了线性 logistic 分类器,它的值域为 ,而不是 。我们该如何定义它的损失函数呢?当我们损失小的时候,我们可以返回一个比较小的概率,因此我们可以定义损失函数为“负对数似然函数”。利用这个损失函数,我们甚至可以处理多分类问题。
简单起见,我们假设训练数据的标签为 ,并把预测当成一个概率。概率越高,代表预测越准确,因此我们的目标是让线性 logistic 分类器分类正确的概率最大。令单样本预测正确的概率为 ,则所有样本预测正确的概率为
假设我们的预测都是独立的,那么所有样本预测正确的概率可以重新表示为
该式比较复杂,不容易分析。由于对数函数为单调递增函数,所以取对数前使概率最大的参数 和 与取对数前使概率最大的参数 和 相同,因此我们可以通过取对数对其化简:
将上式加一个负号,那么我们就把这个求最大值的问题转换为求最小值的问题,也可以认为我们想让损失最小。如下式所示:
其中, 是负对数似然损失函数:
这个损失函数也称为对数损失或者交叉熵。
5、logistic 分类的优化
我们把前四节的内容联系起来,总结出一个能够优化线性 logistic 分类的目标函数。这也叫做 logistic 回归,因此我们定义目标函数 为
拓展:支持向量机(SVM)
在这一节中,我们会综合之前所讲的内容,比如间隔、损失函数等内容来学习支持向量机。
1、间隔
在感知器这一章中,我们定义了样本点 到超平面 的间隔为
但这并不能表示分类器在一个数据集上分类的好坏。因此,我们想为线性分类器 定义一个得分函数 ,当 越大时分类效果越好。 有许多种定义方式,这里我们选择:
即所有样本点中间隔的最小值,也是数据集到分类器的间隔,定义为:
2、损失函数
为了找到最合适的 ,我们想让 取得最大值,即
其中 是所有线性分类器中对数据集取得的最大间隔,接下来我们尝试定义它。
假设线性分类器 能将训练样本正确分类,即对于 ,若 ,则有 ;若 ,则有 。令
距离超平面最近的这几个训练样本点使上式等号成立,它们被称为支持向量,我们定义 为其中一个异类支持向量到分类器的距离为
然后我们可以将寻找最合适的 看成是一个优化问题,定义目标函数为:
其中, 是样本点损失函数,它表示我们选择的某个线性分类器 在该点的预测误差; 是一个正则器,用来防止过拟合,正则项参数 。我们的目标是让目标函数取得最小值。
通过分析条件 ,我们可以定义 - 损失函数 为
于是
当数据集线性可分时, 最小值有限且为正;但当数据集线性不可分时, 最小值为无穷。因此,当数据集线性不可分时,选择这个损失函数存在一定问题。
3、处理线性不可分数据
我们希望设计一个合适的损失函数,从而能够处理线性不可分数据集。这个损失函数能够“放松”所有间隔大于 的样本点的限制,而不是将损失设为 。这里我们采用 hinge 损失函数:
当样本点的间隔大于等于 时,损失为 ;当间隔小于 时,间隔越小,损失越大。此外,损失 始终大于等于 。
例:给定三个样本点,其中 , , ;标签为 , , ;分类器参数 , ,如下图所示。虚线表示由 表示的间隔。
我们计算这三个点的 hinge 损失, 分类正确但间隔小于 ,损失为 ; 分类正确且间隔大于 ,损失为 ; 分类错误,损失为 。
4、使用 hinge 损失
我们把 hinge 损失函数和正则器代入目标函数,我们可以得到
因此我们只需要调整 使得该目标函数取得最小值即可。在正则项中我们可以把 表示为 ,因为它们都表示了决策边界到间隔边界的距离。将 代入并把 展开,我们就得到了支持向量机的目标函数:
5、线性支持向量机
本节内容建议学习完“梯度下降”后再阅读。
在前一节中,我们得到了支持向量机的目标函数,因此我们把寻找让目标函数最小的 问题看作为一个优化问题。为了便于计算,我们把 替换成 。简单起见,我们只计算 时的情况(复杂情况详见 周志华《机器学习》第 6 章)。在这些情况下,目标函数为
其中, 为 hinge 损失(为了简洁,我们喜欢把 hinge 损失写为 )。接下来我们就可以通过 Pegasos 算法来让目标函数取得最小值,即,随机选择某个样本点(也就是给 随机选择某个值,化简后的目标函数中方括号里的内容),对其采用梯度下降法更新,然后重复上述操作。(Pegasos 算法详见 pegasosmbp.dvi (ttic.edu))
我们随机取一点 来分析,顺便分析一下正则项参数 是如何影响结果的。我们的目标是找到合适的参数 使得目标函数最小:
令
展开,得
等价于
当 hinge 损失为正,即 时,化简得 ,于是最优解为 。其中 为向量,因此 也为向量。
当 hinge 损失等于 ,即 时,由于正则项 的约束,我们需要较小的 ,因此 ,即
化简得最优解为
我们想知道正则项参数 对分类正确率的影响,假设某点 分类错误,那么 ,代入 ,得
由于我们之前定义 ,故当 或 或 时才存在分类错误。因此正则项参数 不会影响分类的正确率。
然而,我们还要考虑分类器是否距离样本点过近的问题,我们假定在间隔边界以外及以上的样本点分类良好,即 ,化简得 。于是 可以取得的最大值为
综上所述,我们得到了一个良好的支持向量机目标函数,它不仅能够对线性不可分的数据进行较为准确的分类,还尽可能增大分类正确的样本点对于分类器的间隔,并且对 有一个不错的选择。这个目标函数不仅可以运用于批量梯度下降算法,还可以用于随机梯度下降算法,之后我们会通过实践来说明。
六、梯度下降
在之前的章节中,我们介绍了在机器学习中如何表示目标函数,但我们需要找到一种能够求解 的方法。在优化理论中,解决类似这种问题有无数种方法,但我们将介绍其中最简单的一种方法,叫做梯度下降。
在不同维度的空间中,我们可以把 看作空间内某个“曲面”。于是,我们的目标就是找到合适的 ,使得点 为该曲面的最低点。梯度下降法的原理就是在曲面中任取一点,向曲面下降幅度最大的方向迈出一小步,得到新的位置,然后再重复上述操作,直到走到曲面最低点。
梯度下降可以直观理解为:我们在一座大山上的某处位置,由于我们不知道怎么下山,于是决定走一步算一步,也就是在每走到一个位置的时候,求解当前位置的梯度,沿着梯度的负方向,也就是当前最陡峭的位置向下走一步,然后继续求解当前位置梯度,向这一步所在位置沿着最陡峭最易下山的位置走一步。 这样一步步的走下去,一直走到山脚。
1、一维
我们首先从一维空间下的梯度下降开始。假设 ,并且 和其关于 的导数 已知,那么下面伪代码表示在函数 上的梯度下降,其中函数的导数为 。我们需要确定参数 的初始值,学习率 以及精度 。
1D-Gradient-Descent(, , , , )
1
2
3 repeat
4
5
6 until
7 return
当函数 的变化足够小的时候,梯度下降算法停止运行。我们这里介绍几种能让梯度下降算法停止运行的方法:
- 给定固定的迭代次数 ,当 时终止;
- 当参数 的变化足够小的时候终止,即 ;
- 当导数 足够小时终止,即 。
定理 1 :如果 是一个凸函数,对于任意精度 ,存在某个学习率 ,使得梯度下降可以找到最优解 。
注意,我们选择学习率时要避免过大或过小,学习率太大,会出现振荡甚至梯度爆炸的现象,学习率过小,收敛速度会变慢。
下图为一个凸函数 的图像,我们选择 , 进行梯度下降,发现这个算法表现非常好。
如果 不是凸函数,那么梯度下降找到的最优解取决于 。如果使用梯度下降找到某个参数 使得 且 ,但该点并非函数的最小值。则称其为局部最小值或局部最优解。下图展示了在分别选择不同 的情况下,产生的两个局部最优解。
2、多维
多维空间下的梯度下降与一维空间类似。我们假设 ,故 。 关于 的梯度为:
在多维空间下的梯度下降算法也与一维空间类似,除了第 5 行修改为
算法终止条件与 的维度无关,因此第 6 行保持不变,仍为 。
3、在 logistic 回归上的应用
我们终于可以解决由第五章得到的线性 logistic 分类器的优化问题了。我们首先对目标函数进行梯度下降。由于目标函数是由参数 和 表示的,因此我们要让 分别对 和 求梯度。为了便于计算,我们把 替换成 , 替换成 。目标函数和梯度如下所示:
其中 是一个维度为 的向量, 是一个标量。
综上所述,我们可以得到 logistic 回归的梯度下降算法了:
LR-Gradient-Descent(, , , )
1
2
3
4 repeat
5
6
7
8 until
9 return ,
4、随机梯度下降
从前一节目标函数分别对 和 求梯度得到的表达式可以看出,梯度下降法采用的是全部样本的梯度平均值来更新梯度。而我们可以随机抽取其中一个样本,计算该样本的梯度,然后向这一个样本点的梯度方向迈出一小步,重复上述操作,最终得到最优解,这就是随机梯度下降算法的思想。
我们假设目标函数具有如下形式:
下面伪代码对目标函数 做随机梯度下降,其中 已知, 。
Stochastic-Gradient-Descent(, , , , ..., , )
1
2 for to
3 randomly select
4
5 return
在随机梯度下降中,我们定义学习率为随迭代次数变化的函数 。选择合适的算法停止运行条件比较困难,故这里我们直接选择迭代 次。
当迭代次数 增加时,为了保证随机梯度下降能得到最优解,学习率需要随时间减小。
定理 2 :如果 是一个凸函数,并且学习率 满足
且
则随机梯度下降一定能收敛到某个最优解。(详见随机梯度下降(SGD)算法的收敛性分析(入门版-2)Robbins & Monro方法 - 知乎 (zhihu.com))
我们通常选择 ,但在不同实际需求中选择的学习率也不同。
在前几节中,我们着重介绍的“普通”梯度下降算法也称为批量梯度下降,它相比于随机梯度下降的缺点如下:
- 如果 是非凸函数,那么批量梯度下降容易陷入局部最优解,而随机梯度下降通过选择样本点“反复横跳”,有概率跳过局部最优解;
- 虽然批量梯度下降可以得到很好的梯度估计,但在某些时候,过于优化 有可能会产生过拟合现象;
- 批量梯度下降需要计算数据集上的每一个样本点,计算速度慢,耗费内存空间,在处理大型数据集的问题上浪费时间,然而随机梯度下降只需要选取其中个别样本点即可,效率更高。
实践:梯度下降实现
在这个实践中,我们会对支持向量机的目标函数使用梯度下降。
1、梯度下降
我们想为目标函数 找到合适的 ,使得 取得最小值。在梯度下降函数 gd(f, df, x0, step_size_fn, max_iter) 中:
- f 是一个函数,输入为一个列向量 x ,输出为一个标量 f(x) ;
- df 是一个函数,输入为一个列向量 x ,输出为一个列向量 ,表示函数 f 在 x 处的梯度向量;
- x0 是 x 的初值,为一个列向量;
- step_size_fn 是一个函数,表示学习率,输入迭代次数作为索引,返回该索引对应的学习率;
- max_iter 是迭代总次数。
函数 gd 返回一个元组,里面包括:
- x ,代表最优解,即最后一次迭代后 x 的值;
- fs ,一个列表,包含每次迭代后 f(x) 的值(包括 f(x0));
- xs ,一个列表,包含每次迭代后 x 的值(包括 x0)。
我们还创建了两个测试案例,其中函数 package_ans 返回一个列表,里面包含最优解,fs 的初末值以及 xs 的初末值。
import numpy as np
def gd(f, df, x0, step_size_fn, max_iter):
prev_x = x0
fs = []; xs = []
for i in range(max_iter):
prev_f, prev_grad = f(prev_x), df(prev_x)
fs.append(prev_f); xs.append(prev_x)
if i == max_iter-1:
return prev_x, fs, xs
step = step_size_fn(i)
prev_x = prev_x - step * prev_grad
def f1(x):
return ((2 * x + 3)**2)[0]
def df1(x):
return (2 * 2 * (2 * x + 3))[0]
def f2(v):
x = v[0][0]; y = v[1][0]
return (x - 2.) * (x - 3.) * (x + 3.) * (x + 1.) + (x + y -1)**2
def df2(v):
x = v[0][0]; y = v[1][0]
return cv([(-3. + x) * (-2. + x) * (1. + x) + \
(-3. + x) * (-2. + x) * (3. + x) + \
(-3. + x) * (1. + x) * (3. + x) + \
(-2. + x) * (1. + x) * (3. + x) + \
2 * (-1. + x + y),
2 * (-1. + x + y)])
def cv(value_list):
return np.array([value_list]).T
def package_ans(gd_vals):
x, fs, xs = gd_vals
return [x[0].tolist(), [fs[0].tolist(), fs[-1].tolist()], [xs[0].tolist(), xs[-1].tolist()]]
# Test case 1
ans=package_ans(gd(f1, df1, cv([0.]), lambda i: 0.1, 1000))
# Test case 2
ans=package_ans(gd(f2, df2, cv([0., 0.]), lambda i: 0.01, 1000))
2、数值梯度
在大部分情况下,得到梯度的解析解比较麻烦。因此我们介绍一种能够估计在某一点梯度的方法:有限差分法。
假设函数 把列向量作为输入,返回一个标量。我们想估计该函数在某点 处的梯度。
的第 个元素可以估计为
其中 表示第 个元素为 、其余均为 的列向量, 是一个非常小的常数(例如 0.001)。通过这个表达式,我们可以估计函数 在某点 处的梯度。
举个例子,假设 , ,因此 的第一个元素为
当我们计算完 中的每一个元素后,把它们组合成一个列向量,则该向量就是函数 梯度的数值解。
在函数 num_grad(f, delta) 中,f 为目标函数,delta 为超参数,是我们自己定义的值。该函数将返回一个输入为列向量 x ,输出为梯度向量的函数。
我们写了四个测试案例,其中函数 f1 和函数 f2 先前已定义。
def num_grad(f, delta=0.001):
def df(x):
g = np.zeros(x.shape)
for i in range(x.shape[0]):
xi = x[i,0]
x[i] = xi - delta
fxm = f(x)
x[i] = xi + delta
fxp = f(x)
x[i] = xi
g[i] = (fxp - fxm)/(2*delta)
return g
return df
x = cv([0.])
ans=(num_grad(f1)(x).tolist(), x.tolist())
x = cv([0.1])
ans=(num_grad(f1)(x).tolist(), x.tolist())
x = cv([0., 0.])
ans=(num_grad(f2)(x).tolist(), x.tolist())
x = cv([0.1, -0.1])
ans=(num_grad(f2)(x).tolist(), x.tolist())
3、使用数值梯度
我们把数值梯度函数应用到梯度下降算法中,在函数 minimize(f, x0, step_size_fn, max_iter) 中,我们把 f 应用到 num_grad 中,得到 df ,再使用 gd 进行梯度下降。
def minimize(f, x0, step_size_fn, max_iter):
df = num_grad(f)
return gd(f, df, x0, step_size_fn, max_iter)
ans = package_ans(minimize(f1, cv([0.]), lambda i: 0.1, 1000))
ans = package_ans(minimize(f2, cv([0., 0.]), lambda i: 0.01, 1000))
4、在 SVM 上的应用
完成梯度下降算法实现后,我们把目光放在支持向量机 hinge 损失和目标函数上。我们的目标是计算出目标函数的梯度,并用梯度下降算法进行优化。与前一章的推导不同,这次我们要同时考虑 和 的值。
之前讲过,hinge 损失可以定义为
也经常表示为:
在支持向量机中,hinge 损失把点到分类器的距离与间隔边界的比值作为参数,即
支持向量机目标函数将所有点的 hinge 损失取平均值,并引入正则项使得在间隔足够大的情况下 足够小,即目标函数为
我们希望通过对支持向量机目标函数使用梯度下降法,从而为数据集找到最优的分类器。
4.1 计算 SVM 目标函数
函数 hinge(v) 能够计算 的值,并通过函数 hinge_loss(x, y, th, th0) 计算某个样本点关于分类器的 hinge 损失,最后函数 svm_obj(x, y, th, th0, lam) 用来表示支持向量机的目标函数。
其中,x 的维度为 ,y 的维度为 ,th 的维度为 ,th0 的维度为 ,lam 是一个标量。
在测试案例中,函数 super_simple_separable() 为我们定义的测试数据集,sep_e_separator 为我们定义的测试分类器。
def hinge(v):
return np.where(v >= 1, 0, 1 - v)
def hinge_loss(x, y, th, th0):
return hinge(y * (np.dot(th.T, x) + th0))
def svm_obj(x, y, th, th0, lam):
return np.mean(hinge_loss(x, y, th, th0)) + lam * np.linalg.norm(th) ** 2
def super_simple_separable():
X = np.array([[2, 3, 9, 12],
[5, 2, 6, 5]])
y = np.array([[1, -1, 1, -1]])
return X, y
sep_e_separator = np.array([[-0.40338351], [1.1849563]]), np.array([[-2.26910091]])
x_1, y_1 = super_simple_separable()
th1, th1_0 = sep_e_separator
# Test case 1
ans = svm_obj(x_1, y_1, th1, th1_0, .1)
# Test case 2
ans = svm_obj(x_1, y_1, th1, th1_0, 0.0)
4.2 计算 SVM 梯度
函数 svm_obj_grad(X, y, th, th0, lam) 能够返回支持向量机目标函数分别关于 和 的梯度,用一个列向量来表示。为了简单直观,我们把求梯度这一步分解成许多函数。
def d_hinge(v):
return np.where(v >= 1, 0, -1)
def d_hinge_loss_th(x, y, th, th0):
return d_hinge(y*(np.dot(th.T, x) + th0))* y * x
def d_hinge_loss_th0(x, y, th, th0):
return d_hinge(y*(np.dot(th.T, x) + th0)) * y
def d_svm_obj_th(x, y, th, th0, lam):
return np.mean(d_hinge_loss_th(x, y, th, th0), axis = 1, keepdims = True) +lam * 2 * th
def d_svm_obj_th0(x, y, th, th0, lam):
return np.mean(d_hinge_loss_th0(x, y, th, th0), axis = 1, keepdims = True)
def svm_obj_grad(X, y, th, th0, lam):
grad_th = d_svm_obj_th(X, y, th, th0, lam)
grad_th0 = d_svm_obj_th0(X, y, th, th0, lam)
return np.vstack([grad_th, grad_th0])
X1 = np.array([[1, 2, 3, 9, 10]])
y1 = np.array([[1, 1, 1, -1, -1]])
th1, th10 = np.array([[-0.31202807]]), np.array([[1.834]])
X2 = np.array([[2, 3, 9, 12],
[5, 2, 6, 5]])
y2 = np.array([[1, -1, 1, -1]])
th2, th20 = np.array([[-3., 15.]]).T, np.array([[2.]])
d_hinge(np.array([[71.]])).tolist()
d_hinge(np.array([[-23.]])).tolist()
d_hinge(np.array([[71, -23.]])).tolist()
d_hinge_loss_th(X2[:,0:1], y2[:,0:1], th2, th20).tolist()
d_hinge_loss_th(X2, y2, th2, th20).tolist()
d_hinge_loss_th0(X2[:,0:1], y2[:,0:1], th2, th20).tolist()
d_hinge_loss_th0(X2, y2, th2, th20).tolist()
d_svm_obj_th(X2[:,0:1], y2[:,0:1], th2, th20, 0.01).tolist()
d_svm_obj_th(X2, y2, th2, th20, 0.01).tolist()
d_svm_obj_th0(X2[:,0:1], y2[:,0:1], th2, th20, 0.01).tolist()
d_svm_obj_th0(X2, y2, th2, th20, 0.01).tolist()
svm_obj_grad(X2, y2, th2, th20, 0.01).tolist()
svm_obj_grad(X2[:,0:1], y2[:,0:1], th2, th20, 0.01).tolist()
4.3 SVM 批量梯度下降
把之前定义的所有函数联系起来,让支持向量机的目标函数进行批量梯度下降。函数batch_svm_min(data, labels, lam) 返回值类型与函数 gd 相同。在该函数中:
- 所有分类器参数均初始化为 0 ;
- 学习率由函数 svm_min_step_size_fn(i) 定义;
- 总共迭代 10 次。
在测试案例中,函数 separable_medium() 为我们定义的测试数据集,sep_m_separator 为我们定义的测试分类器。
def batch_svm_min(data, labels, lam):
def svm_min_step_size_fn(i):
return 2/(i+1)**0.5
init = np.zeros((data.shape[0] + 1, 1))
def f(th):
return svm_obj(data, labels, th[:-1, :], th[-1:,:], lam)
def df(th):
return svm_obj_grad(data, labels, th[:-1, :], th[-1:,:], lam)
x, fs, xs = gd(f, df, init, svm_min_step_size_fn, 10)
return x, fs, xs
def separable_medium():
X = np.array([[2, -1, 1, 1],
[-2, 2, 2, -1]])
y = np.array([[1, -1, 1, -1]])
return X, y
sep_m_separator = np.array([[ 2.69231855], [ 0.67624906]]), np.array([[-3.02402521]])
x_1, y_1 = super_simple_separable()
ans = package_ans(batch_svm_min(x_1, y_1, 0.0001))
x_1, y_1 = separable_medium()
ans = package_ans(batch_svm_min(x_1, y_1, 0.0001))
七、回归
接下来我们会讲解一种新的机器学习问题:回归。由于回归仍是监督学习的一种,所以数据集形式依然不变:
和分类不同, 不再是离散值,而是连续值,因此我们的模型转变为如下形式:
于是我们可以对结果进行定量预测,而不是单纯地将其分类。
要想实现回归,首先要选择一个合适的损失函数,通过比对 的预测值和目标值来评估我们选择的模型的质量。我们可以选择不同的损失函数,但一般情况下,我们选择平方误差:
平方误差能有效避免比目标值过大或过小的预测值,尤其适合线性分布、噪声符合高斯分布的数据点。
我们假设线性模型为
在第四章讲过,我们可以对数据集进行非线性特征变换来得到新的模型。如果 是 的线性函数,那么令 是 的非线性函数,则 是 的非线性函数。
我们把回归看作是一个优化问题,给定数据集 ,我们希望找到一个合适的线性模型来使目标函数最小。其中目标函数一般为均方误差函数(也称为经验风险):
该优化问题也可以表示为
1、解析解:最小二乘法
在上文的优化问题中,我们可以通过最小二乘法找到能使均方误差最小的线性模型,从而得到结果的解析解。
在第三章第二节中,为了简便计算,我们可以给 增加一个维度(特征),其值为 ,从而可以忽略参数 。
接下来就是一个普通的微积分问题了:让 对 求导,并令结果等于 ,最终求得 。这里补充一点,我们需要验证 对应值是否为最小值(可能为最大值或极值,在本文中我们不会考虑这些)。具体步骤如下:
- 对于 ,求 ;
- 建立 个形式为 的方程;
- 求解所有 。
我们将从矩阵的视角来解决这个问题。
我们假设训练数据为矩阵 和 ,其中 的每一列代表一个训练样本, 的每一“列”代表对应的目标值:
在大部分机器学习的教材中, 为行向量,而不是列向量。为了统一,我们定义两个新的矩阵 和 ,其分别表示 和 的转置。于是
目标函数可以表示为
对 求导,得
令导数等于 ,得
其中,各项维度为
因此,给定数据集,我们可以直接轻松计算出令目标函数最小的参数值。
2、线性回归的正则化
在上一节的推导中,如果 不可逆该怎么办呢?
此外,该线性回归无法解决过拟合的问题:我们的线性模型虽然能在训练集上进行良好预测,但为了避免与训练数据过于贴合,我们将对该线性模型采用正则化。
针对如上两个问题,我们将使用岭回归的方法。在线性模型目标函数的末尾增添正则项 以及对应参数 ,得到岭回归目标函数为
越大, 越趋近于 。我们不用正则化 ,因为 表示了回归模型位置的“高低”,与拟合数据的目标值大小有关,不影响我们的拟合。为了预测的准确性,线性模型的“旋转幅度”不能太大。
存在使 最小的 和 的解析解,但比最小二乘法得到的解析解更复杂。
如果我们忽略 (给 增加一个特征,其值为 ),则
令导数等于 ,得
终于,我们得到
当 时 可逆。(我们在先前的不可逆矩阵中加入一个像山岭一样、对角线为 的矩阵,使之变为可逆矩阵,岭回归的名称源于此)
对于正则化我们再讲一点,不只是回归,在整个机器学习中,对于模型 ,其对测试集的误差可以分为两类:
- 系统误差:选择错误的模型 产生的误差,例如使用线性模型去拟合一个正弦波;
- 估计误差:由于数据量不够(有用的数据太少)导致我们选择的模型 拟合不好。(关于偏差和方差详见 通俗讲解机器学习中的偏差 (Bias) 和方差 (Variance) - 知乎 (zhihu.com))
当我们增加 时,我们增加了系统误差,但降低了估计误差,反之亦然。
3、梯度下降优化
对大小为 的矩阵 求逆的算法复杂度为 (详见 矩阵求逆操作的复杂度分析(逆矩阵的复杂度分析)_矩阵求逆的复杂度-CSDN博客)。当 很大时,矩阵求逆会很慢。因此,如果数据维度很大,我们将采用梯度下降法来优化。
在上一节中,我们提到岭回归目标函数为
其关于 的梯度为
关于 的梯度为
得到两个梯度后,我们可以使用批量梯度下降法或随机梯度下降法。
由于线性回归目标函数和岭回归目标函数均为凸函数,因此它们都只有一个最小值。即给定足够小的学习率,一定可以找到最优解。
实践:线性回归代码实现
1、线性回归
我们首先定义一些能够计算均方误差的函数,其中 x 是大小为 的样本,y 是大小为 的目标值。
# In all the following definitions:
# x is d by n : input data
# y is 1 by n : output regression values
# th is d by 1 : weights
# th0 is 1 by 1 or scalar
def lin_reg(x, th, th0):
return np.dot(th.T, x) + th0
def square_loss(x, y, th, th0):
return (y - lin_reg(x, th, th0))**2
def mean_square_loss(x, y, th, th0):
# the axis=1 and keepdims=True are important when x is a full matrix
return np.mean(square_loss(x, y, th, th0), axis = 1, keepdims = True)
然后我们再定义一些函数分别计算均方误差关于 和 的梯度。
def d_lin_reg_th(x, th, th0):
return x
def d_square_loss_th(x, y, th, th0):
return -2 * (y - lin_reg(x, th, th0)) * d_lin_reg_th(x, th, th0)
def d_mean_square_loss_th(x, y, th, th0):
return np.mean(d_square_loss_th(x, y, th, th0), axis = 1, keepdims = True)
def d_lin_reg_th0(x, th, th0):
return np.ones((1, x.shape[1]))
def d_square_loss_th0(x, y, th, th0):
return -2 * (y - lin_reg(x, th, th0)) * d_lin_reg_th0(x, th, th0)
def d_mean_square_loss_th0(x, y, th, th0):
return np.mean(d_square_loss_th0(x, y, th, th0), axis= 1, keepdims = True)
接下来我们加入正则器,定义岭回归目标函数,并求梯度。
# In all the following definitions:
# x is d by n : input data
# y is 1 by n : output regression values
# th is d by 1 : weights
# th0 is 1 by 1 or scalar
def ridge_obj(x, y, th, th0, lam):
return np.mean(square_loss(x, y, th, th0), axis = 1, keepdims = True) + lam * \
np.linalg.norm(th)**2
def d_ridge_obj_th(x, y, th, th0, lam):
return d_mean_square_loss_th(x, y, th, th0) + 2* lam * th
def d_ridge_obj_th0(x, y, th, th0, lam):
return d_mean_square_loss_th0(x, y, th, th0)
2、随机梯度下降
在函数 sgd(X, y, J, dJ, w0, step_size_fn, max_iter) 中
- X 是大小为 的数据集;
- y 是大小为 的行向量,表示目标值;
- J 是一个损失函数,其中输入为一个样本(一个列向量)、一个标签(大小为 )和一个权重向量 w(一个列向量),输出为一个标量;
- dJ 是损失函数 J 的梯度,其中输入为一个样本(一个列向量)、一个标签(大小为 )和一个权重向量 w(一个列向量),输出为一个列向量;
- w0 是权重向量 w 的初值,是一个列向量;
- step_size_fn 是一个函数,输入为迭代次数(从 0 开始索引的整数),输出为对应的学习率;
- max_iter 是最大迭代次数。
该函数返回一个元组,其中
- w 是最后一次迭代中的权重向量;
- fs 是损失函数 J 在每次迭代后的值,为一个列表;
- ws 是权重向量 w 在每次迭代后的值,为一个列表。
在函数中,num_grad 是上一章实践中的数值梯度函数;downwards_line 为测试案例。
def downwards_line():
X = np.array([[0.0, 0.1, 0.2, 0.3, 0.42, 0.52, 0.72, 0.78, 0.84, 1.0],
[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]])
y = np.array([[0.4, 0.6, 1.2, 0.1, 0.22, -0.6, -1.5, -0.5, -0.5, 0.0]])
return X, y
X, y = downwards_line()
def J(Xi, yi, w):
# translate from (1-augmented X, y, theta) to (separated X, y, th, th0) format
return float(ridge_obj(Xi[:-1,:], yi, w[:-1,:], w[-1:,:], 0))
def dJ(Xi, yi, w):
def f(w): return J(Xi, yi, w)
return num_grad(f)(w)
def num_grad(f):
def df(x):
g = np.zeros(x.shape)
delta = 0.001
for i in range(x.shape[0]):
xi = x[i,0]
x[i,0] = xi - delta
xm = f(x)
x[i,0] = xi + delta
xp = f(x)
x[i,0] = xi
g[i,0] = (xp - xm)/(2*delta)
return g
return df
def sgd(X, y, J, dJ, w0, step_size_fn, max_iter):
n = y.shape[1]
prev_w = w0
fs = []; ws = []
np.random.seed(0)
for i in range(max_iter):
j = np.random.randint(n)
Xj = X[:,j:j+1]; yj = y[:,j:j+1]
prev_f, prev_grad = J(Xj, yj, prev_w), dJ(Xj, yj, prev_w)
fs.append(prev_f); ws.append(prev_w)
if i == max_iter - 1:
return prev_w, fs, ws
step = step_size_fn(i)
prev_w = prev_w - step * prev_grad
最后我们定义函数 ridge_min(X, y, lam) ,返回使岭回归目标函数最小的参数 和 (参考之前实践 SVM 梯度下降)。其中 lam 为我们自己选择的超参数 。
def ridge_min(X, y, lam):
def svm_min_step_size_fn(i):
return 0.01/(i+1)**0.5
d, n = X.shape
X_extend = np.vstack([X, np.ones((1, n))])
w_init = np.zeros((d+1, 1))
def J(Xj, yj, th):
return float(ridge_obj(Xj[:-1,:], yj, th[:-1,:], th[-1:,:], lam))
def dJ(Xj, yj, th):
return ridge_obj_grad(Xj[:-1,:], yj, th[:-1,:], th[-1:,:], lam)
np.random.seed(0)
w, fs, ws = sgd(X_extend, y, J, dJ, w_init, svm_min_step_size_fn, 1000)
return w[:-1,:], w[-1:,:]
3、线性回归的评价
我们将采用均方根误差来衡量线性模型的好坏。均方根误差是一种用于衡量预测模型在连续性数据上的预测精度的指标。它衡量了预测值与真实值之间的均方根差异,表示预测值与真实值之间的平均偏差程度,是回归任务中常用的性能评估指标之一。均方根误差定义为
其中 是我们的训练模型。在线性回归中, 。
在函数 xval_learning_alg 中,我们进行 k 折交叉验证,计算 k 次均方根误差的平均值,然后分析线性模型的好坏。
def eval_predictor(X_train, Y_train, X_test, Y_test, lam):
th, th0 = ridge_min(X_train, Y_train, lam)
return np.sqrt(mean_square_loss(X_test, Y_test, th, th0))
def xval_learning_alg(X, y, lam, k):
_, n = X.shape
idx = list(range(n))
np.random.seed(0)
np.random.shuffle(idx)
X, y = X[:,idx], y[:,idx]
split_X = np.array_split(X, k, axis=1)
split_y = np.array_split(y, k, axis=1)
score_sum = 0
for i in range(k):
X_train = np.concatenate(split_X[:i] + split_X[i+1:], axis=1)
y_train = np.concatenate(split_y[:i] + split_y[i+1:], axis=1)
X_test = np.array(split_X[i])
y_test = np.array(split_y[i])
score_sum += eval_predictor(X_train, y_train, X_test, y_test, lam)
return score_sum/k