-
一、准备工作
1. 入门之前你至少得了解一点数学中的的知识,比如说导数,编导,链式求导等,再比如说矩阵,向量,矩阵的简单运算等,还有一点概率论的知识,例如概率,条件概率等。
2. 接下来还得准备一个写代码的环境,这里我推荐anaconda,这是一个集成开发环境,可以让你省去好多事,不论你是Windows平台还是Linux平台都可以安装使用它(具体的安装步骤自行百度,论坛中的教程很多)刚开始学习的时候并不需要太多的第三方库,有python环境,加上numpy(这个用来做一些数学运算)和matplotlib(这个东东的话是用来将我们做的一些过程和结果可视化)即可。
3. 最重要的是你得掌握一点python的基础知识,应该能够熟练阅读和编写包含基础编程结构(例如,函数定义/调用、列表和字典、循环和条件表达式)的 Python 代码。
-
二、基本的概念
- 机器学习又分为有监督学习(Supervised Learning)、无监督学习(Unsupervised Learning)和强化学习(Reinforcement Learning)(这一部分暂时涉及不到,那就以后再说吧!!!),大致的话就是下面这种情况。
有监督学习 用已知某种或某些特性的样本作为训练集,以建立一个数学模型,再用已建立的模型来预测未知样本,此种方法称为有监督学习,是最常见的机器学习方法之一。常见的有监督学习算法有线性回归(linear regression),逻辑回归(logistic regression),支持向量机(Support Vector Machines),决策树(decision trees),K-近邻(k-nearest neighbor algorithm)等
无监督学习 现实生活中常常会有这样的问题:缺乏足够的先验知识,因此难以人工标注类别或进行人工类别标注的成本太高。很自然地,我们希望计算机能代我们完成这些工作,或至少提供一些帮助。根据类别未知(没有被标记)的训练样本解决模式识别中的各种问题,称之为无监督学习。典型的无监督学习算法就是聚类。
三、回归问题——线性回归
对于机器学习,拿线性回归来入门再好不过了,因为它可以是最简单的如y=wx形式,也可以是稍微复杂一点的y=wx+b形式,其实我们都知道这就是一个线性方程,或者说这是一个一次函数,用官方一点的术语描述就是输入向量x,经过函数映射:𝑓𝜃: 𝒙 → 𝑦后得到输出𝑦,其中𝜃为函数𝑓自 身的参数,这里我们拿y=wx+b来说事,即线性变换:。此时我们可以绘制出输出𝑦和输入𝑥的变化趋势,如下图 所示,随着输入信号𝑥 的增加,输出电平𝑦也随之线性增加,其中𝑤参数可以理解为直线的斜率(Slope),b 参数为 直线的偏置(Bias)。
对于某一点来说,x和y的映射关系是未知但是确定的。两点即可确定一条直线,为了估计w和b 的值,我们只需要在上图中采样任意两个数据点:
当时,通过求解上式便可计算出𝑤和𝑏的值。考虑某个具体的例子:, 代入上式中可得:
这就是我们初中时代学习过的二元一次方程组,通过消元法可以轻松计算出𝑤和𝑏的解析 解:。
那么,看到这里是不是觉得这也不是机器学习啊?当然你可以轻而易举的得出这个方程的解,可是对于计算机来说,要做的工作远远不止这些,其次,这只是针对w和b这两个参数,实际情况下参数有很多很多,此时才能彰显出来机器的优势。
那么,怎么让机器来习得这个模型,就是我们要考虑的。
对于上面的模型来说,假设有N个输入,那么就需要N+1组采样点,这样的话会有什么问题?我们知道在实际情况中任何采样点都有可能存在观测误差,我们将这个观测误差记为,假设这个属于均值为,方差为的正态分布(Normal Distribution)或者高斯分布(Gaussian Distribution):𝒩(𝜇, ),则采样到的样本符合如下公式:
𝑦 = 𝑤𝑥 + 𝑏 + 𝜖, 𝜖~𝒩(𝜇, )
而引入误差后,即使简单如线性模型,如果仅采样两个数据点,可能会带来较大估 计偏差。如图下所示,图中的数据点均带有观测误差,如果基于蓝色矩形块的两个数据点进行估计,则计算出的蓝色虚线与真实橙色直线存在较大偏差。为了减少观测误差引入 的估计偏差,可以通过采样多组数据样本集合𝔻 = {(𝑥 (1) ,𝑦 (1) ), (𝑥 (2) ,𝑦 (2) ),… , (𝑥 (𝑛) , 𝑦 (𝑛) )},然后找出一条“最好”的直线,使得它尽可能地让所有采样点到该直线的误差(Error,或损失 Loss)之和最小。
也就是说,由于观测误差𝜖的存在,当我们采集了多个数据点𝔻时,可能不存在一条直 线完美的穿过所有采样点。退而求其次,我们希望能找到一条比较“好”的位于采样点中间的直线。那么怎么衡量“好”与“不好”呢?一个很自然的想法就是,求出当前模型的所有采样点上的预测值+ b与真实值之间差的平方和作为总误差:
然后搜索一组参数使得最小,而它对应的直线就我我们要寻找的最优直线:
其中𝑛表示采样点的个数。这种误差计算方法称为均方差(Mean Squared Error,简称 MSE)。
四、优化方案
结合上面说的,明确一下我们的目的就是需要找出最优参数(Optimal Parameter)和,使得输入和输出满足线性关系。但是由于观测误差的存在,需要通过采样足够多的数据样本组成的数据集(Dataset):𝔻 = {,...,},找到一组最优的参数和使得均方差最小。
对于上述说的简单线性模型,只需要两个样本点就可以利用消元法解出来,而对于复杂的情形下,我们不可能按照这种思路来解决,一种通用的办法就是借助数值方法去优化(Optimize)出一个近似的解,之所以叫优化,是因为在计算机环境下,计算速度非常快,所以我们可以利用它强大的计算能力去不断的“搜索”和“试错”,说白了就是一组和一组的数据不断去试,那个合适!!!然后在试错的过程中计算每一组数据的情况下它的误差,然后以某种方法让这个误差逐渐的减小,最后从测试过的数据中,挑出最好的一组,就是我们要找的最优和。
同样的问题来了,上面说的方法确实很简单很暴力,在面对大规模,高纬度的数据优化问题时计算效率极低,基本不可行。那么考虑换一种方法就是大名鼎鼎的梯度下降算法(Gradient Descend)。
先看一个高中学过的知识:导数 (Derivative),高中的时候经常做一类题就是给你一个函数,让你求解该函数的极值,通常我们的解题方法是先对函数求导,再令导函数为0,求出对应的驻点,在去检验驻点是否满足我们要求的条件即可。举一个简单的例子:,下面的图形展示了该函数和其导函数在区间的图像,蓝色实现为,黄色虚线为,在图中很明显的可以看到函数导数(虚线)为0的点即为的驻点,函数的极值就在驻点处。
而梯度的定义,简单来说就是函数对各个自变量的偏导数(Partial Derivative)组成的向量。例如对于3维函数:
其中,函数对自变量的偏导数:,函数对自变量的偏导数:,则梯度:∇:
再举一个例子就是:,图中xy平面的红色箭头的长度表示梯度向量的模,箭头的方向表示梯度向量的方向。或许,图像看的不是那么清楚,我们只需要知道:箭头的方向总是指向当前位置函数增速最大的方向,函数曲面月陡峭,箭头的长度也就越大,梯度的模也越大。
结合上面说的,我们有:函数在各处的梯度方向∇总是指向函数值增大的方向,那么梯度的反方向-∇应指向函数值减小的方向。利用这个性质,只需要按照:
来迭代更新,就能获得越来越小的函数值,其中的用来缩放梯度向量(大家都叫它学习率),通常设置为某较小的值如0.01,0.001等。在一维函数的情况下,上述详细、两形式可以退化成标量形式:
通过上式迭代跟新若干次,得到的处的函数值,总是更有可能比在处的函数值小。
简单总结一下就是,通过上述的(1)式,循环计算函数的梯度∇并更新待优化参数(这里用到的参数是和),从而得到函数获得极小值时参数的最优数值解。这样的优化方式就叫做梯度下降算法。
将上述方法放到具体的实例中,我们通过上述介绍的简单的梯度下降算法来求解参数和。这里要最小化的是均方差误差函数
上述式子当中需要优化的参数是和,按照上面(1)式的方式来循环更新参数有:
再看一下计算梯度的过程,其实就是求导!!!
同样的方法推导偏导数的表达式:
到此,理论的部分暂停一下,说了那么多,怎么去描述上面的理论?
五、代码实现——线性模型实战
通常在机器学习中有一个通用的过程就是:
1、准备数据集
2、建立模型
3、优化模型
4、训练模型
5、测试
1、采样数据
这里,利用前面提到的线性模型,从指定的,的真实模型中直接采样:
并且为了模拟真实样本的观测误差,给模型添加误差自变量,它采样自均值为0,方差为0.01的高斯分布:
通过随机采样n = 100次,获得n个样本的训练数据集:
data = []# 保存样本集的列表
for i in range(100): # 循环采样 100 个点
x = np.random.uniform(-10., 10.) # 随机采样输入 x
# 采样高斯噪声
eps = np.random.normal(0., 0.1)
# 得到模型的输出
y = 1.477 * x + 0.089 + eps
data.append([x, y]) # 保存样本点
data = np.array(data) # 装换为2DNumpy数组
循环进行100次采样,每次从区间[-10,10]的均匀分布U(0,1)中随机采样一个数据x,同时从均值为0,方差为的高斯分布中随机采样噪声,根据真实模型生成的数据,并保存为Numpy数组。
2、计算误差
循环计算在每个点处的预测值与真实值之间差的平方并累加,从而获得训练集上的均方差损失值。最后的误差和和除以数据样本总数,从而得到每个样本上的平均误差。
def mse(b, w, points):
# 根据当前的 w,b 参数计算均方差损失
totalError = 0
for i in range(0, len(points)): # 循环迭代所有点
x = points[i, 0] # 获得 i 号点的输入 x
y = points[i, 1] # 获得 i 号点的输出 y
# 计算差的平方,并累加
totalError += (y - (w * x + b)) ** 2
# 将累加的误差求平均,得到均方差
return totalError / float(len(points))
3、计算梯度
将上述的(2)式和(3)式拿过来:
根据这两个式子,我们只需要计算每一个点上面的和值,平均后即可得到偏导数和。
def step_gradient(b_current, w_current, points, lr):
# 计算误差函数在所有点上的导数,并更新 w,b
b_gradient = 0
w_gradient = 0
M = float(len(points)) # 总样本数
for i in range(0, len(points)):
x = points[i, 0]
y = points[i, 1]
# 误差函数对 b 的导数:grad_b = 2(wx+b-y),参考公式(2.3)
b_gradient += (2/M) * ((w_current * x + b_current) - y)
# 误差函数对 w 的导数:grad_w = 2(wx+b-y)*x,参考公式(2.2)
w_gradient += (2/M) * x * ((w_current * x + b_current) - y)
# 根据梯度下降算法更新 w',b',其中 lr 为学习率
new_b = b_current - (lr * b_gradient)
new_w = w_current - (lr * w_gradient)
return [new_b, new_w]
4、梯度更新
经过上一步计算出误差函数在和处的梯度后,再利用上面提到的梯度更新的方式:
来更新和的值,把对数据集所有样本训练一次称为一个Epoch,共循环迭代num_iterations个Epoch。
def gradient_descent(points, starting_b, starting_w, lr, num_iterations):
# 循环更新 w,b 多次
b = starting_b # b 的初始值
w = starting_w # w 的初始值
# 根据梯度下降算法更新多次
for step in range(num_iterations):
# 计算梯度并更新一次
b, w = step_gradient(b, w, np.array(points), lr)
loss = mse(b, w, points) # 计算当前的均方差,用于监控训练进度
if step%50 == 0: # 打印误差和实时的 w,b 值
print(f"iteration:{step}, loss:{loss}, w:{w}, b:{b}")
return [b, w] # 返回最后一次的 w,b
最后,附上完整的代码:
# 一个线性回归的小实例
# y = 1.477*X+0.089
import numpy as np
import matplotlib.pyplot as plt
# step1:采样数据
# 加上误差变量E y = 1.477*x+0.089+E,E~N(0,0.01)
def load_data():
dataset = []
for i in range(100):
x = np.random.uniform(-10, 10)
# 采样高斯噪声
eps = np.random.normal(0, 0.01)
y = 1.477 * x + 0.089 + eps
dataset.append([x, y])
dataset = np.array(dataset) # 转换为2D numpy数组
return dataset
# step2:计算误差
def mse(b, w, points):
# 根据当前的w,b参数计算均方差损失
totalError = 0
for i in range(0, len(points)):
x = points[i, 0]
y = points[i, 1]
totalError += ((w * x + b)-y) ** 2
return totalError / float(len(points))
# step3:计算梯度
def step_gradient(b_current, w_current, points, lr):
b_gradient = 0
w_gradient = 0
M = float(len(points))
for i in range(0, len(points)):
x = points[i, 0]
y = points[i, 1]
# 误差函数对b的导数 grad_b = 2(wx+b-y)
b_gradient += (2 / M) * ((w_current * x + b_current) - y)
w_gradient += (2 / M) * x * ((w_current * x + b_current) - y)
new_b = b_current - (lr * b_gradient)
new_w = w_current - (lr * w_gradient)
return [new_b, new_w]
# step4:梯度更新
def gradient_descent(points, start_b, start_w, lr, num_iterations):
loss_list = []
epoch_list = []
b = start_b
w = start_w
for step in range(num_iterations):
b, w = step_gradient(b, w, np.array(points), lr)
loss = mse(b, w, points)
loss_list.append(loss)
epoch_list.append(step)
print(f"iteration:{step}, loss:{loss}, w:{w}, b:{b}")
plt.xlabel('Epoch')
plt.ylabel('Cost')
plt.plot(epoch_list, loss_list)
plt.show()
return [b, w]
if __name__ == '__main__':
data = load_data()
lr = 0.01
initial_b = 0
initial_w = 0
num_iterations = 1000
[b, w] = gradient_descent(data, initial_b, initial_w, lr, num_iterations)
loss = mse(b, w, data)
print(f'Final loss:{loss}, w:{w}, b:{b}')
最后的预测的和的结果如下:
训练模型MSE下降曲线:
可能每个人的机器不一样,最终得到的结果会有些许误差,但是总体来看,效果还是挺好,已经很接近真实值了。