LESSON 3 线性回归的手动实现

Lesson 3.线性回归的手动建模实验

在此前的两节课程中,我们已经介绍了关于线性回归模型的基本概念,并且介绍了一个多元线性回归的损失函数求解方法——最小二乘法。在有了这一些列理论推导之后,本节我们将结合Lesson 1中所介绍的机器学习一般建模流程,并首先尝试在一个手动构建的数据集上进行完整的线性回归模型建模。

Lesson 3.1 变量相关性基础理论

在创建好了数据生成器之后,接下来即可进行手动线性回归建模实验。

# 科学计算模块
import numpy as np
import pandas as pd

# 绘图模块
import matplotlib as mpl
import matplotlib.pyplot as plt

机器学习的“学习”目标,其实就是数据集中隐藏的数字规律,而又由于这些规律背后代表的是某些事物的真实属性或者运行状态,因此这些规律是具备指导生产生活事件意义的有价值的规律,这也是机器学习算法价值的根本。

  • 相关系基本解释与相关系数计算公式

当然,对于不同数据集来说,是否具备规律、以及规律隐藏的深浅都不一样。对于模型来说,擅长挖掘的规律、以及规律挖掘的能力也都各不相同。而对于线性回归来说,捕捉的实际上是数据集的线性相关的规律。所谓线性相关,简单来说就是数据的同步变化特性。例如此前数据集:
LESSON 3 线性回归的手动实现_第1张图片
特征和标签就存在着非常明显的同步变化的特性:第二条样本特征增加2、对应标签也增加2,当然,这也就是线性模型本身可解释性的来源——体重越大的鲍鱼、年龄越大,并且体重每增加2、年龄也增加2。这种同步变化特性用更加专业的角度来描述就是变量之间的相关性。这种相关性可以用一个计算公式算得,也就是相关性系数计算公式:
C o r r e l a t i o n = C o v ( X , Y ) V a r ( X ) ∗ V a r ( Y ) Correlation = \frac{Cov(X, Y)}{\sqrt {Var(X) * Var(Y)}} Correlation=Var(X)Var(Y) Cov(X,Y)
其中, X X X Y Y Y是两个随机变量(对应数据集也就代表两个字段), V a r ( X ) 、 V a r ( Y ) Var(X)、Var(Y) Var(X)Var(Y) X 、 Y X、Y XY的方差, C o v ( X , Y ) Cov(X,Y) Cov(X,Y) X X X Y Y Y这两个变量的协方差,具体计算公式为:
Cov ⁡ ( X , Y ) = E ( X − E ( X ) ) E ( Y − E ( Y ) ) = E ( X Y ) − E ( X ) E ( Y ) \begin{aligned} \operatorname{Cov}(X, Y) &=E(X-E(X)) E(Y-E(Y)) \\ &=E(X Y)-E(X) E(Y) \end{aligned} Cov(X,Y)=E(XE(X))E(YE(Y))=E(XY)E(X)E(Y)
其中 E ( X ) 、 E ( Y ) E(X)、E(Y) E(X)E(Y) X 、 Y X、Y XY期望计算结果。

关于相关系数的计算有很多种方法,此处介绍的相关系数计算也被称为皮尔逊相关系数,最早由统计学家卡尔·皮尔逊提出,是目前最为通用的相关系数计算方法。

  • 相关系数计算在NumPy中的实现
    当然,在NumPy中也提供了相关系数计算函数corrcoef可用于快速计算两个数组之间的相关系数
A = np.array([[1, 2, 3], [4, 5, 10]]).T
A
#array([[ 1,  4],
#       [ 2,  5],
#       [ 3, 10]])
A[:, 0]
#array([1, 2, 3])
np.corrcoef(A[:, 0], A[:, 1])
#array([[1.        , 0.93325653],
#       [0.93325653, 1.        ]])

  该函数最终返回的是相关系数矩阵 A 2 ∗ 2 A_{2*2} A22,其中 a i , j a_{i,j} ai,j表示第i、j两个变量之间的相关系数。很明显,相关系数矩阵是一个对角线元素全是1的矩阵,并且上三角和下三角元素对应位置元素相等。当然,对于A中的两个数组相关系数计算结果为0.933。

  • 相关系数计算结果解读
    相关系数的计算结果取值为[-1,1]之内,取值为负时代表两个变量同步变化关系为负,也就是其中一个数值增加、另一个数值减少。例如:
A = np.array([[1, 2, 3], [-1, -1.5, -5]]).T
A
#array([[ 1. , -1. ],
#       [ 2. , -1.5],
#       [ 3. , -5. ]])
plt.plot(A[:, 0], A[:, 1])

LESSON 3 线性回归的手动实现_第2张图片

np.corrcoef(A[:, 0], A[:, 1])
#array([[ 1.        , -0.91766294],
#       [-0.91766294,  1.        ]])

总体来说,相关系数绝对值越大两个变量的相关性越强,绝对值为1时候代表完全相关,两个变量完全线性同步变化,其中一个变量可以由另一个变量线性表出。而绝对值为0时,则表示完全线性无关,两个变量没有线性同步变化规律,这两个变量没有线性关系。当绝对值介于0和1之间时候,相关性强弱可以表示如下:
LESSON 3 线性回归的手动实现_第3张图片
如果是双变量的相关性,我们也可以用一组函数关系及图像来进行表示

np.random.randn(20)
#array([-1.64976142, -0.87343737,  0.07530987, -1.42079571, -0.83262953,
#        1.21936676, -0.75871775,  0.44775161,  0.46307329,  1.44154581,
#        0.79686385, -1.50887509, -0.53100092,  2.41405101, -0.28564285,
#       -1.51317621, -0.90461468, -0.45806723,  1.0310925 , -0.58551109])
X = np.random.randn(20)
y = X + 1
#很明显,此时X和y完全正相关
np.corrcoef(X, y)
#array([[1., 1.],
#       [1., 1.]])
# 对应点图
plt.plot(X, y, 'o')

LESSON 3 线性回归的手动实现_第4张图片
当然,如果我们想在y基础上创建一个稍微弱化线性相关关系的数组,可以在y基础上加入一个随机数作为扰动项。例如:

a = y.shape
a
#(20,)
np.random.normal?
# 创建一个和y一样形状的服从标准正态分布的随机数组
np.random.normal(size=a)
#array([ 0.49622906,  1.3573347 , -0.20178063,  0.87805077, -1.42474422,
#       -1.70856044, -1.0952294 , -0.58293826,  1.09455328, -0.68583135,
#       -0.64713056,  0.26123903, -0.47562764,  1.39130696,  0.6881981 ,
#        0.30883974, -0.19414512,  1.6188312 , -2.05761665,  0.14654045])
np.random.normal?
ran = np.random.normal(size = X.shape)
ran
#array([ 0.26042618, -1.04154116, -0.08313493, -0.79742972, -0.13280839,
#        1.27921862,  0.48826155, -0.60279756,  0.60330237, -0.71903143,
#        0.2286587 ,  1.9293763 ,  2.45620622,  0.78343275, -0.37187501,
#        0.91938857,  1.79980253, -0.45157682, -0.37647247,  1.03357355])

接下来,创建一个控制扰动项大小的变量delta

delta = 0.5
#因此,扰动项最终计算过程为:
r = ran * delta
r
#array([-0.70326304,  0.39202727,  0.63362961, -0.28215417, -0.30074036,
#       -0.59024454, -0.33660292, -0.10770454,  0.1688505 ,  1.13219149,
#        0.2886254 ,  0.19396348,  0.08446266, -0.05326953, -0.45031793,
#       -0.59683277,  0.28079264,  0.62728816,  0.24544957, -0.46301168])
y1 = y + r
y1
#array([-1.68723915,  0.76532326,  2.045763  , -0.74109006,  0.32887279,
#        0.35244733, -2.68058767, -0.67867186,  1.29879589,  1.50933817,
#        0.27243497,  1.19472997,  2.79165527,  2.9877919 , -0.92177274,
#       -0.66149061, -0.69079609,  1.26800102,  2.62706381,  0.32901834])

此处,y1就是在y基础上加入扰动项之后的标签。由于有一个随性扰动项的存在,会使得y1和X的线性关系被削弱。

从更根本的角度来说,加入扰动项削弱线性相关性,也是因为扰动项本身无规律可循,加入数据之后只会掩盖原始规律。类似扰动项我们也可称其为白噪声。白噪声其实也有一定的实际背景,在很多情况下,我们采集的数据本身就是包含一定随机误差的,或者说是包含了无法帮助提取有效规律的信息。

plt.subplot(121)
plt.plot(X, y, 'o')
plt.title('y=x+1')
plt.subplot(122)
plt.plot(X, y1, 'o')
plt.title('y=x+1+r')

LESSON 3 线性回归的手动实现_第5张图片
由此可见,在加入了扰动项之后,模型线性相关性明显变弱。据此,当然,伴随delta的增加,噪声数据的绝对值会越来越大,掩盖原始数据线性相关规律的趋势会更加明显。我们可以简单用一组图像来展示不同相关性时双变量呈现的分布情况:

# delta系数取值数组
dl = [0.5, 0.7, 1, 1.5, 2, 5]
# 空列表容器
yl = []          # 不同delta下y的取值所组成的列表
cl = []          # 不同y下相关系数矩阵所组成的列表
# 计算不同delta下y和相关系数计算情况
for i in dl:
    yn = X + 1 + (ran * i)
    cl.append(np.corrcoef(X, yn))
    yl.append(yn)
cl
#[array([[1.        , 0.95223181],
#        [0.95223181, 1.        ]]),
# array([[1.       , 0.9177165],
#        [0.9177165, 1.       ]]),
# array([[1.        , 0.86249014],
#        [0.86249014, 1.        ]]),
# array([[1.        , 0.77666269],
#        [0.77666269, 1.        ]]),
# array([[1.        , 0.70615124],
#        [0.70615124, 1.        ]]),
# array([[1.        , 0.49724359],
#        [0.49724359, 1.        ]])]
yl
#[array([-1.68723915,  0.76532326,  2.045763  , -0.74109006,  0.32887279,
#         0.35244733, -2.68058767, -0.67867186,  1.29879589,  1.50933817,
#         0.27243497,  1.19472997,  2.79165527,  2.9877919 , -0.92177274,
#        -0.66149061, -0.69079609,  1.26800102,  2.62706381,  0.32901834]),
# array([-1.96854437,  0.92213417,  2.29921484, -0.85395173,  0.20857664,
#         0.11634951, -2.81522884, -0.72175367,  1.36633609,  1.96221477,
#         0.38788513,  1.27231536,  2.82544033,  2.96648409, -1.10189991,
#        -0.90022372, -0.57847904,  1.51891629,  2.72524364,  0.14381367]),
# array([-2.39050219,  1.15735053,  2.6793926 , -1.02324423,  0.02813242,
#        -0.23779721, -3.01719059, -0.78637639,  1.46764639,  2.64152967,
#         0.56106037,  1.38869345,  2.87611793,  2.93452237, -1.37209067,
#        -1.25832338, -0.41000345,  1.89528918,  2.87251338, -0.13399334]),
# array([-3.09376523,  1.5493778 ,  3.31302221, -1.30539839, -0.27260794,
#        -0.82804176, -3.35379351, -0.89408093,  1.63649689,  3.77372116,
#         0.84968577,  1.58265694,  2.96058059,  2.88125284, -1.8224086 ,
#        -1.85515614, -0.12921082,  2.52257734,  3.11796295, -0.59700502]),
# array([-3.79702828,  1.94140507,  3.94665182, -1.58755256, -0.5733483 ,
#        -1.4182863 , -3.69039643, -1.00178546,  1.80534739,  4.90591265,
#         1.13831118,  1.77662042,  3.04504325,  2.82798331, -2.27272653,
#        -2.45198891,  0.15158182,  3.1498655 ,  3.36341253, -1.06001671]),
# array([-8.01660653,  4.2935687 ,  7.74842947, -3.28047755, -2.37779046,
#        -4.95975357, -5.71001396, -1.64801267,  2.81845039, 11.6990616 ,
#         2.87006359,  2.94040133,  3.55181922,  2.50836614, -4.97463411,
#        -6.03298551,  1.83633764,  6.91359447,  4.83610996, -3.83808681])]
plt.plot(X, yl[0], 'o')

LESSON 3 线性回归的手动实现_第6张图片

plt.subplot(231)
plt.plot(X, yl[0], 'o')
plt.plot(X, y, 'r-')
plt.subplot(232)
plt.plot(X, yl[1], 'o')
plt.plot(X, y, 'r-')
plt.subplot(233)
plt.plot(X, yl[2], 'o')
plt.plot(X, y, 'r-')
plt.subplot(234)
plt.plot(X, yl[3], 'o')
plt.plot(X, y, 'r-')
plt.subplot(235)
plt.plot(X, yl[4], 'o')
plt.plot(X, y, 'r-')
plt.subplot(236)
plt.plot(X, yl[5], 'o')
plt.plot(X, y, 'r-')

LESSON 3 线性回归的手动实现_第7张图片
能够明显看出,伴随delta取值越来越大,数据相关性越来越弱,当然,我们也能通过观察cl的取值来查看各组变量的相关系数

cl
#[array([[1.        , 0.95223181],
#        [0.95223181, 1.        ]]),
# array([[1.       , 0.9177165],
#        [0.9177165, 1.       ]]),
# array([[1.        , 0.86249014],
#        [0.86249014, 1.        ]]),
# array([[1.        , 0.77666269],
#        [0.77666269, 1.        ]]),
# array([[1.        , 0.70615124],
#        [0.70615124, 1.        ]]),
# array([[1.        , 0.49724359],
#        [0.49724359, 1.        ]])]

根据Lesson 2补充材料所介绍,简单线性回归就是在二维平面中通过构建一条直线来试图捕捉平面当中的点,线性相关性越弱、线性模型越难捕捉到这些所有点,模型效果就越差。换而言之,就是数据集之间线性相关性越明显,数据规律越明显,模型越容易捕捉到这些规律。

在这个基础理论之上,我们有以下两方面衍生的应用:

其一:我们可以在构建线性模型之前先探查数据本身的线性相关性,如果自变量和因变量存在很好的相关性,那就一定可以顺利的构建线性回归模型对数据进行拟合。而如果线性相关性不强,则说明当前数据并不适合构建线性回归模型,或者在构建模型之前我们需要对数据进行一些“不影响真实规律”的转化,令其表现出线性的分布趋势。当然,如果需要更换模型,就需要在我们学习了更多其他模型之后来进行讨论,而如果是围绕数据进行修改,则会涉及特征工程相关理论;
  
其二:上述几组不同的数据,实际上就代表着对线性回归模型建模难度各不相同的几组数据,delta越大对线性回归模型来说建模就更加困难。据此,我们可以生成一个手动创建数据集的函数,该函数可以输出不同建模难度(规律深浅)的数据集,来辅助我们测试模型和优化算法的性能。

当然,线性相关性的减弱,不仅是对于线性回归模型,对于很多回归类问题都会造成更大的建模困难。

Lesson 3.2 数据生成器与Python模块编写

在有了相关性理论基础之后,我们即可创建一个可用于回归模型实验的数据生成器。

# 科学计算模块
import numpy as np
import pandas as pd

# 绘图模块
import matplotlib as mpl
import matplotlib.pyplot as plt

一、自定义数据生成器

为了方便后续练习的展开,我们尝试自己创建一个数据生成器,用于自主生成一些符合某些条件、难度可控、具备某些特性的数据集。机器学习发展至今,在追求模型效果提升的过程中,模型本身可解释性逐渐变弱,对于很多集成类算法,很多模型构建思路也在朝向深度学习靠拢。这使得很多模型内部逐渐呈现灰箱甚至是黑箱的状态。但是,在初学过程中,我们仍然需要通过类似控制变量的方法、通过设计一些实验,去深入理解算法运行原理及一些优化方法的实际作用,这就需要我们自己动手,创建一些数据用于实验的原材料,通过一些实验深入了解模型原理,从“炼丹师”朝着“化学家”更进一步。

和Lesson 1中介绍的一样,课程中案例分为三类,分别是基础阶段的手动创建数据集、实战阶段的竞赛数据集和企业应用实战阶段的企业数据集。

1.手动生成数据

我们先尝试生成两个特征、存在偏差,自变量和因变量存在线性关系的数据集

num_inputs = 2               # 两个特征
num_examples = 1000          # 总共一千条数据
#然后尝试通过线性方程,确定自变量和因变量的真实关系
np.random.seed(24)       # 设置随机数种子
np.random.randn(2, 2)
#array([[ 1.32921217, -0.77003345],
#       [-0.31628036, -0.99081039]])
# 线性方程系数
w_true = np.array([2, -1]).reshape(-1, 1)
b_true = np.array(1)
# 扰动项相关
delta = 0.01
# 创建数据集的特征和标签取值
features = np.random.randn(num_examples, num_inputs)
labels_true = features.dot(w_true) + b_true
labels = labels_true + np.random.normal(size = labels_true.shape) * delta

注意,此时labels_true和features满足严格意义上的线性方程关系
y = 2 x 1 − x 2 + 1 y = 2x_1-x_2+1 y=2x1x2+1
但我们实际使用的标签labels,则是在labels_true的基础上增添了一个扰动项,np.random.normal(size = labels_true.shape) * delta,这其实也符合我们一般获取数据的情况:真实客观世界或许存在某个规律,但我们搜集到的数据往往会因为各种原因存在一定的误差,无法完全描述真实世界的客观规律,这其实也是模型误差的来源之一(另一个误差来源是模型本身捕获规律的能力)。这其中, y = 2 x 1 − x 2 + 1 y=2x_1-x_2+1 y=2x1x2+1相当于我们从上帝视角创建的数据真实服从的规律,而扰动项,则相当于人为创造的获取数据时的误差。
简单查看我们创建的数据集

features[: 10]
#array([[-1.07081626, -1.43871328],
#       [ 0.56441685,  0.29572189],
#       [-1.62640423,  0.2195652 ],
#       [ 0.6788048 ,  1.88927273],
#       [ 0.9615384 ,  0.1040112 ],
#       [-0.48116532,  0.85022853],
#       [ 1.45342467,  1.05773744],
#       [ 0.16556161,  0.51501838],
#       [-1.33693569,  0.56286114],
#       [ 1.39285483, -0.06332798]])
labels[: 10]
#array([[ 0.29161817],
#       [ 1.83851813],
#       [-2.46058022],
#       [ 0.44394438],
#       [ 2.8133944 ],
#       [-0.8109745 ],
#       [ 2.85143778],
#       [ 0.83156296],
#       [-2.22624102],
#       [ 3.84279053]])

plt.subplot(121)
plt.scatter(features[:, 0], labels)          # 第一个特征和标签的关系
plt.subplot(122)
plt.scatter(features[:,, 1] labels)          # 第二个特征和标签的关系

LESSON 3 线性回归的手动实现_第8张图片
不难看出,两个特征和标签都存在一定的线性关系,并且跟特征的系数绝对值有很大关系。当然,若要增加线性模型的建模难度,可以增加扰动项的数值比例,从而削弱线性关系。

# 设置随机数种子
np.random.seed(24)   

# 修改因变量
labels1 = labels_true + np.random.normal(size = labels_true.shape) * 2

# 可视化展示

# 扰动较小的情况
plt.subplot(221)
plt.scatter(features[:, 0], labels)             # 第一个特征和标签的关系
plt.subplot(222)
plt.plot(features[:, 1], labels, 'ro')          # 第二个特征和标签的关系

# 扰动较大的情况
plt.subplot(223)
plt.scatter(features[:, 0], labels1)             # 第一个特征和标签的关系
plt.subplot(224)
plt.plot(features[:, 1], labels1, 'yo')          # 第二个特征和标签的关系

LESSON 3 线性回归的手动实现_第9张图片
当然,我们也能生成非线性关系的数据集,此处我们创建满足 y = 2 x 2 + 1 y=2x^2+1 y=2x2+1规律的数据集。

np.power([2, 3], 2)
#array([4, 9], dtype=int32)
# 设置随机数种子
np.random.seed(24)   

num_inputs = 1               # 一个特征
num_examples = 1000          # 总共一千条数据

# 线性方程系数
w_true = np.array(2)
b_true = np.array(1)

# 特征和标签取值
features = np.random.randn(num_examples, num_inputs)
labels_true = np.power(features, 2) * w_true + b_true
labels = labels_true + np.random.normal(size = labels_true.shape) * 2

# 可视化展示
plt.scatter(features, labels)

LESSON 3 线性回归的手动实现_第10张图片

关于曲线相关,其实也可将其转化为线性相关,例如上例中,我们只需要新增一列 x 2 = x x_2=x x2=x即可构建一个线性模型 y = x 2 + 1 y=x_2+1 y=x2+1,该方法也是特征工程中的一种特征衍生的方法。

此时需要注意的是,无论是创建了曲线规律的数据集,还是增加了扰动项绝对数值,都会对建模造成困难。但二者的造成困难的方式是不同的。如果是曲线规律的数据集,则代表规律本身更加复杂,此时需要使用更加复杂的模型来进行规律提取,该部分属于模型选取和参数调优的技术内容;而如果是扰动项比较大,则代表规律被掩盖的比较深,此时需要采用其他技术手段进行白噪声的处理,该部分属于特征工程技术内容。但本节内,无论是曲线规律还是白噪声数值过大,都会对线性方程建模造成困难。

这也就是控制数据集建模难度的最基础的抓手。

2.创建生成回归类数据的函数

为了方便后续使用,我们将上述过程封装在一个函数内

  • 定义创建函数
A = np.arange(4).reshape(2, 2)
A
#array([[0, 1],
#       [2, 3]])
np.power(A, 3)
#array([[ 0,  1],
#       [ 8, 27]], dtype=int32)
A.dot(2)
#array([[0, 2],
#       [4, 6]])
np.ones_like(A)
#array([[1, 1],
#       [1, 1]])

def arrayGenReg(num_examples = 1000, w = [2, -1, 1], bias = True, delta = 0.01, deg = 1):
    """回归类数据集创建函数。

    :param num_examples: 创建数据集的数据量
    :param w: 包括截距的(如果存在)特征系数向量
    :param bias:是否需要截距
    :param delta:扰动项取值
    :param deg:方程最高项次数
    :return: 生成的特征数组和标签数组
    """
    
    if bias == True:
        num_inputs = len(w)-1                                                           # 数据集特征个数
        features_true = np.random.randn(num_examples, num_inputs)                       # 原始特征
        w_true = np.array(w[:-1]).reshape(-1, 1)                                        # 自变量系数
        b_true = np.array(w[-1])                                                        # 截距
        labels_true = np.power(features_true, deg).dot(w_true) + b_true                 # 严格满足人造规律的标签
        features = np.concatenate((features_true, np.ones_like(labels_true)), axis=1)    # 加上全为1的一列之后的特征
    else: 
        num_inputs = len(w)
        features = np.random.randn(num_examples, num_inputs) 
        w_true = np.array(w).reshape(-1, 1)         
        labels_true = np.power(features, deg).dot(w_true)
    labels = labels_true + np.random.normal(size = labels_true.shape) * delta
    return features, labels

注:上述函数默认创建的是一个满足 y = 2 x 1 − x 2 + 1 y=2x_1-x_2+1 y=2x1x2+1的数据集。

  • 测试函数性能

首先查看扰动项较小的时候的数据情况

# 设置随机数种子
np.random.seed(24)   

# 扰动项取值为0.01
f, l = arrayGenReg(delta=0.01)

f
#array([[ 1.32921217, -0.77003345,  1.        ],
#       [-0.31628036, -0.99081039,  1.        ],
#       [-1.07081626, -1.43871328,  1.        ],
#       ...,
#       [ 1.5507578 , -0.35986144,  1.        ],
#       [-1.36267161, -0.61353562,  1.        ],
#       [-1.44029131,  0.50439425,  1.        ]])
# 绘制图像查看结果
plt.subplot(121)
plt.scatter(f[:, 0], l)             # 第一个特征和标签的关系
plt.subplot(122)
plt.scatter(f[:, 1], l)             # 第二个特征和标签的关系

LESSON 3 线性回归的手动实现_第11张图片
然后查看扰动项较大时数据情况

# 设置随机数种子
np.random.seed(24)   

# 扰动项取值为2
f, l = arrayGenReg(delta=2)

# 绘制图像查看结果
plt.subplot(121)
plt.scatter(f[:, 0], l)             # 第一个特征和标签的关系
plt.subplot(122)
plt.scatter(f[:, 1], l)             # 第二个特征和标签的关系

LESSON 3 线性回归的手动实现_第12张图片

# 设置随机数种子
np.random.seed(24)   

# 最高次项为2
f, l = arrayGenReg(deg=2)

# 绘制图像查看结果
plt.subplot(121)
plt.scatter(f[:, 0], l)             # 第一个特征和标签的关系
plt.subplot(122)
plt.scatter(f[:, 1], l)             # 第二个特征和标签的关系

LESSON 3 线性回归的手动实现_第13张图片
在定义好数据创建函数之后,即可开始尝试手动实现线性回归模型。

该数据生成器并非为线性回归模型量身定制,而是通用与此后我们学习的所有回归类模型。关于分类模型数据集的创建,我们将在介绍逻辑回归时讲解。

此外,对于机器学习模型,我们将更多的从数据规律和模型提取规律角度出发,并不会太多顾及模型可解释性等问题。上述数据本身也是从规律角度出发构建的数据,并不具备“解释性”。

随机数种子的使用
  此处我们使用了一个随机数种子,以确保随机结果的可重复性。当我们设置某个随机数种子之后,每次随机过程都是可重复的:我们只需要再次调用相同随机种子,就可以重复此前随机过程。

np.random.randn(9)
#array([ 0.19023418, -0.81483068,  1.72482389, -0.15554173, -0.56159453,
#        0.42370057, -1.13493113,  0.37872101, -2.37427885])
np.random.seed(24)
np.random.randn(9)
#array([ 1.32921217, -0.77003345, -0.31628036, -0.99081039, -1.07081626,
#       -1.43871328,  0.56441685,  0.29572189, -1.62640423])
np.random.randn(9)
#array([ 0.2195652 ,  0.6788048 ,  1.88927273,  0.9615384 ,  0.1040112 ,
#       -0.48116532,  0.85022853,  1.45342467,  1.05773744])
np.random.seed(24)
np.random.randn(9)
#array([ 1.32921217, -0.77003345, -0.31628036, -0.99081039, -1.07081626,
#       -1.43871328,  0.56441685,  0.29572189, -1.62640423])
np.random.randn(9)
#array([ 0.2195652 ,  0.6788048 ,  1.88927273,  0.9615384 ,  0.1040112 ,
#       -0.48116532,  0.85022853,  1.45342467,  1.05773744])

当然,不同随机数种子所诞生的随机过程是不一样的,这也是“种子”的由来。此外,不同库中的随机过程需要用的不同随机种子也是不同的,比如上述我们用np.random.seed来规定numpy中相关随机过程,但如果是其他第三方库,如random库所定义的随机过程,就需要使用random库中的随机种子来确定。

import random
l = list(range(5))
l
#[0, 1, 2, 3, 4]
random.seed(24)
random.shuffle(l)
l
#[4, 2, 0, 1, 3]
random.shuffle(l)
l
#[1, 4, 0, 3, 2]
l = list(range(5))
l
#[0, 1, 2, 3, 4]
random.seed(24)
random.shuffle(l)
l
#[4, 2, 0, 1, 3]
random.shuffle(l)
l
#[1, 4, 0, 3, 2]
l = list(range(5))
l
#[0, 1, 2, 3, 4]
np.random.seed(24)
random.shuffle(l)
l
#[4, 0, 3, 2, 1]
random.shuffle(l)
l
#[0, 4, 3, 1, 2]
l = list(range(5))
l
#[0, 1, 2, 3, 4]
np.random.seed(24)
random.shuffle(l)
l
#[4, 2, 3, 1, 0]

二、Python模块的编写与调用

根据课程安排,本节定义的函数将后续课程中将经常使用,因此需要将其封装为一个模块方便后续调用。封装为模块有以下几种基本方法:

  • 打开文本编辑器,将写好并测试完成的函数写入其中,并将文本的拓展名改写为.py;
  • 在spyder或者pycharm中复制相关函数,并保存为.py文件;

然后将文件保存在jupyter主目录下,并取名为ML_basic_function,后续即可通过import ML_basic_function进行调用。如果是jupyterlab用户,也可按照如下方式进行编写:

Step 1.打开左侧文件管理栏页,点击新建
LESSON 3 线性回归的手动实现_第14张图片
Step 2.在新建目录中,选择Test File,并通过重命名将其命名为ML_basic_function.py
LESSON 3 线性回归的手动实现_第15张图片
Step 3.在打开的文本编辑器中输入代码
LESSON 3 线性回归的手动实现_第16张图片
并且需要注意,需要在py文件内导入相关的包。

Step 4.测试能否调用

首先重启当前jupyter
LESSON 3 线性回归的手动实现_第17张图片然后尝试导入自定义模块

from ML_basic_function import *
#查看相关函数说明并使用
arrayGenReg?
#Signature: arrayGenReg(num_examples=1000, w=[2, -1, 1], bias=True, delta=0.01, deg=1)
#Docstring:
#回归类数据集创建函数。

#:param num_examples: 创建数据集的数据量
#:param w: 包括截距的(如果存在)特征系数向量
#:param bias:是否需要截距
#:param delta:扰动项取值
#:param deg:方程最高项次数
#:return: 生成的特征张和标签张量
#File:      f:\code file\机器学习实战\ml_basic_function.py
#Type:      function
arrayGenReg()

至此就完成了自定义模块的编写与保存,后续我们还将陆续写入其他函数。

Lesson 3.3 线性回归手动实现与模型局限

在创建好了数据生成器之后,接下来即可进行手动线性回归建模实验。

一、线性回归的手动实现

接下来,我们尝试进行线性回归模型的手动建模实验。建模过程将遵照机器学习的一般建模流程,并且借助NumPy所提供的相关工具来进行实现。通过本次实验,我们将进一步深化对机器学习建模流程的理解,并且也将进一步熟悉对编程基础工具的掌握。

1.根据机器学习建模流程构建线性回归模型

Step 1.数据准备
  首先,是准备数据集。我们利用数据生成器创建一个扰动项不太大的数据集:

# 设置随机数种子
np.random.seed(24)   

# 扰动项取值为0.01
features, labels = arrayGenReg(delta=0.01)

features
#array([[ 1.32921217, -0.77003345,  1.        ],
#       [-0.31628036, -0.99081039,  1.        ],
#       [-1.07081626, -1.43871328,  1.        ],
#       ...,
#       [ 1.5507578 , -0.35986144,  1.        ],
#       [-1.36267161, -0.61353562,  1.        ],
#       [-1.44029131,  0.50439425,  1.        ]])

其中,features也被称为特征矩阵、labels也被称为标签数组

  • Step 2.模型选取

接下来,选取模型对上述回归类问题数据进行建模。此处我们选取带有截距项的多元线性回归方程进行建模,基本模型为:
f ( x ) = w 1 x 1 + w 2 x 2 + b f(x) = w_1x_1+w_2x_2+b f(x)=w1x1+w2x2+b
w ^ = [ w 1 , w 2 , b ] T \hat w = [w_1,w_2,b]^T w^=[w1,w2,b]T x ^ = [ x 1 , x 2 , 1 ] T \hat x = [x_1,x_2, 1]^T x^=[x1,x2,1]T,则上式可写为
f ( x ) = w ^ T x ^ f(x) = \hat w^T\hat x f(x)=w^Tx^

注,此处如果要构建一个不带截距项的模型,则可另X为原始特征矩阵带入进行建模。

  • Step 3.构造损失函数

对于线性回归来说,我们可以参照SSE、MSE或者RMSE的计算过程构造损失函数。由于目前模型参数还只是隐式的值(在代码中并不显示),我们可以简单尝试,通过人工设置一组̂ 来计算SSE。

  • 令 ̂ 为一组随机值,计算SSE
np.random.seed(24)
w = np.random.randn(3).reshape(-1, 1)
w
#array([[ 1.32921217],
#       [-0.77003345],
#       [-0.31628036]])

此时模型输出结果为:

y_hat = features.dot(w)
y_hat[:10]
#array([[ 2.04347616],
#       [ 0.02627308],
#       [-0.63176501],
#       [ 0.20623364],
#       [-2.64718921],
#       [-0.86880796],
#       [ 0.88171608],
#       [-1.61055557],
#       [ 0.80113619],
#       [-0.49279524]])

  据此,根据公式 S S E = ∣ ∣ y − X w ^ ∣ ∣ 2 2 = ( y − y ^ ) T ( y − y ^ ) SSE= ||y - X\hat w||_2^2 = (y - \hat y)^T(y - \hat y) SSE=yXw^22=(yy^)T(yy^),SSE计算结果为:

(labels - y_hat).T.dot(labels - y_hat)
#array([[2093.52940481]])
# 计算MSE
(labels - y_hat).T.dot(labels - y_hat) / len(labels)
#array([[2.0935294]])
labels[:10]
#array([[ 4.43811826],
#       [ 1.375912  ],
#       [ 0.30286597],
#       [ 1.81970897],
#       [-2.47783626],
#       [ 0.47374318],
#       [ 2.83085905],
#       [-0.83695165],
#       [ 2.84344069],
#       [ 0.8176895 ]])

能够看出,在当前参数取值下,模型输出结果和真实结果相距甚远。

不过,为了后续快速计算SSE,我们可以将上述SSE计算过程封装为一个函数,令其在输入特征矩阵、标签数组和真实参数情况下即可输出SSE计算结果:

def SSELoss(X, w, y):
    """
    SSE计算函数
    
    :param X:输入数据的特征矩阵
    :param w:线性方程参数
    :param y:输入数据的标签数组
    :return SSE:返回对应数据集预测结果和真实结果的误差平方和 
    """
    y_hat = X.dot(w)
    SSE = (y - y_hat).T.dot(y - y_hat)
    return SSE
# 简单测试函数性能
SSELoss(features, w, labels)
#array([[2093.52940481]])

实验结束后,需要将上述SSELoss函数写入ML_basic_function.py中。

  • Step 4.利用最小二乘法求解损失函数

接下来,我们需要在SSELoss中找到一组最佳的参数取值,另模型预测结果和真实结果尽可能接近。此处我们利用Lesson 2中介绍的最小二乘法来进行求解,最小二乘法求解模型参数公式为: w ^ = ( X T X ) − 1 X T y \hat w = (X^TX)^{-1}X^Ty w^=(XTX)1XTy

值得注意的是,最小二乘法在进行求解过程中,需要特征矩阵的交叉乘积可逆,也就是 X T X X^TX XTX必须存在逆矩阵。我们可以通过计算其行列式来判断该条件是否满足:

np.linalg.det(features.T.dot(features))
#967456500.1798325

行列式不为0,因此 X T X X^TX XTX逆矩阵存在,可以通过最小二乘法求解。具体求解方法分为两种,其一是使用NumPy中线性代数基本方法,根据上述公式进行求解,同时也可以直接使用lstsq函数进行求解。

  • 基础方法求解
w = np.linalg.inv(features.T.dot(features)).dot(features.T).dot(labels)
w
#array([[ 1.99961892],
#       [-0.99985281],
#       [ 0.99970541]])

即可算出模型最优参数w。所谓模型最优参数,指的是参数取得任何其他数值,模型评估结果都不如该组参数时计算结果更好。首先,我们也可以计算此时模型SSE指标:

SSELoss(features, w, labels)
#array([[0.09300731]])

明显小于此前所采用的随机w取值时SSE结果。此外,我们还可以计算模型MSE

SSELoss(features, w, labels) / len(y)
#array([[9.30073138e-05]])

当然,由于数据集本身是依据=2 1 _{1} x1+ 2 _{2} x2−1规律构建的,因此从模型参数也能够看出模型预测效果较好。

模型评估指标中SSE、MSE、RMSE三者反应的是一个事实,我们根据SSE构建损失函数只是因为SSE计算函数能够非常方便的进行最小值推导,SSE取得最小值时MSE、RMSE也取得最小值。

  • lstsq函数求解
    当然,我们也可以利用lstsq函数进行最小二乘法结果求解。二者结果一致。
np.linalg.lstsq(features, labels, rcond=-1)
#(array([[ 1.99961892],
#        [-0.99985281],
#        [ 0.99970541]]),
# array([0.09300731]),
# 3,
# array([32.70582436, 31.3166949 , 30.3678959 ]))

最终w参数取值为:

np.linalg.lstsq(features, labels, rcond=-1)[0]
#array([[ 1.99961892],
#       [-0.99985281],
#       [ 0.99970541]])

至此,我们即完成了整个线性回归的机器学习建模流程。

二、线性回归模型局限

尽管上述建模过程能够发现,面对白噪声不是很大、并且线性相关性非常明显的数据集,模型整体表现较好,但在实际应用中,大多数数据集可能都不具备明显的线性相关性,并且存在一定的白噪声(数据误差)。此时多元线性回归模型效果会受到极大影响。

plt.plot(features[:, 0], labels, 'o')

LESSON 3 线性回归的手动实现_第18张图片

  • 非线性相关规律
    例如,此处创建一个满足 y = x 3 + 1 y=x^3+1 y=x3+1基本规律,并且白噪声很小的数据集进行建模测试
# 设置随机数种子
np.random.seed(24)   

# 扰动项取值为0.01
features, labels = arrayGenReg(w=[2,1], deg=3, delta=0.01)
features
#array([[ 1.32921217,  1.        ],
#       [-0.77003345,  1.        ],
#       [-0.31628036,  1.        ],
#       ...,
#       [ 0.84682091,  1.        ],
#       [ 1.73889649,  1.        ],
#       [ 1.93991673,  1.        ]])
plt.plot(features[:, 0], labels, 'o')

LESSON 3 线性回归的手动实现_第19张图片

np.linalg.lstsq(features, labels, rcond=-1)
#(array([[5.91925985],
#        [0.96963333]]),
# array([28466.58711077]),
# 2,
# array([32.13187164, 31.53261742]))
w = np.linalg.lstsq(features, labels, rcond=-1)[0]
w
#array([[5.91925985],
#       [0.96963333]])
SSELoss(features, w, labels)
#array([[28466.58711077]])
y_hat = features.dot(w)
#array([[ 8.83758557e+00],
#       [-3.58839477e+00],
#       [-9.02512307e-01],
#        ...
#       [ 5.98218635e+00],
#       [ 1.12626135e+01],
#       [ 1.24525045e+01]])
plt.plot(features[:, 0], labels, 'o')
plt.plot(features[:, 0], y_hat, 'r-')

LESSON 3 线性回归的手动实现_第20张图片
从模型结果能够看出,模型和数据集分布规律相差较大

  • 噪声增加

此外,我们稍微增加模型白噪声,测试线性回归模型效果

# 设置随机数种子
np.random.seed(24)   
# 扰动项取值为2
features, labels = arrayGenReg(w=[2,1], delta=2)
features
#array([[ 1.32921217,  1.        ],
#       [-0.77003345,  1.        ],
#       [-0.31628036,  1.        ],
#       ...,
#       [ 0.84682091,  1.        ],
#       [ 1.73889649,  1.        ],
#       [ 1.93991673,  1.        ]])
plt.plot(features[:, 0], labels, 'o')

LESSON 3 线性回归的手动实现_第21张图片

np.linalg.lstsq(features, labels, rcond=-1)
#(array([[1.91605821],
#        [0.90602215]]),
# array([3767.12804359]),
# 2,
# array([32.13187164, 31.53261742]))
w = np.linalg.lstsq(features, labels, rcond=-1)[0]
w
#array([[1.91605821],
#       [0.90602215]])
SSELoss(features, w, labels)
#array([[3767.12804359]])
X = np.linspace(-5, 5, 1000)
y = w[0] * X + w[1]
plt.plot(features[:, 0], labels, 'o')
plt.plot(X, y, 'r-')

LESSON 3 线性回归的手动实现_第22张图片
能够发现,模型误差较大。

  • 最小二乘法条件限制

并且,除此之外,线性回归模型还面临这一个重大问题就是,如果特征矩阵的交叉乘积不可逆,则最小二乘法求解过程就不成立了。
w ^ = ( X T X ) − 1 X T y \hat w = (X^TX)^{-1}X^Ty w^=(XTX)1XTy
当然,此时也代表着数据集存在着较为严重的多重共线性,换而言之就是数据集的特征矩阵可能可以相互线性表出。这个时候矩阵方程 X T X w ^ = X T y X^TX\hat w=X^Ty XTXw^=XTy并不一定存在唯一解。解决该问题的方法有很多种,从数学角度出发,我们可以从以下三个方面入手:

  • 其一,对数据进行降维处理:
      首先,可考虑进一步对数据集进行SVD分解或PCA主成分分析,在SVD或PCA执行的过程中会对数据集进行正交变换,最终所得数据集各列将不存在任何相关性。当然此举会对数据集的结构进行改变,且各列特征变得不可解释。
  • 其二,修改求解损失函数的方法:
      我们可以试图求解原方程的广义逆矩阵,对于某些矩阵方程来说,通过求解广义逆矩阵,也可以得到近似最优解;此外,我们还可以通过使用其他最优化求解方法,如梯度下降算法等来进行求解;
  • 其三,修改损失函数:
      其实可以修改原损失函数,令其满足最小二乘法求解条件即可。如果 X T X X^TX XTX不可逆,那么我们可以通过试图在损失函数中加入一个正则化项,从而令损失函数可解。根据Lesson 2中的公式推导,目前根据SSE所构建的损失函数如下:
       S S E L o s s ( w ^ ) = ∣ ∣ y − X w ^ ∣ ∣ 2 2 = ( y − X w ^ ) T ( y − X w ^ ) SSELoss(\hat w) = ||y - X\hat w||_2^2 = (y - X\hat w)^T(y - X\hat w) SSELoss(w^)=yXw^22=(yXw^)T(yXw^)

通过数学过程可以证明,此时如果我们在原有损失函数基础上添加一个关于参数 w w w的1-范数( ∣ ∣ w ^ ∣ ∣ 1 ||\hat w||_1 w^1)或者2-范数( ∣ ∣ w ^ ∣ ∣ 2 ||\hat w||_2 w^2)的某个计算结果,则可令最小二乘法条件得到满足。此时最小二乘法计算结果由无偏估计变为有偏估计。例如,当我们在损失函数中加上 λ ∣ ∣ w ^ ∣ ∣ 2 2 \lambda ||\hat w||_2^2 λw^22(其中 λ \lambda λ为参数)时,模型损失函数为:
L o s s ( w ^ ) = ∣ ∣ y − X w ^ ∣ ∣ 2 2 + λ ∣ ∣ w ^ ∣ ∣ 2 2 Loss(\hat w) = ||y - X\hat w||_2^2 +\lambda ||\hat w||_2^2 Loss(w^)=yXw^22+λw^22
经过数学转化,上述矩阵表达式对 w ^ \hat w w^求导后令其为零,则可解出:
( X T X + λ I ) w ^ = X T y (X^TX+\lambda I) \hat w = X^Ty (XTX+λI)w^=XTy
其中 I I I为单位矩阵。此时由于 ( X T X + λ I ) (X^TX+\lambda I) (XTX+λI)肯定是可逆矩阵,因此可以顺利求解出 w ^ \hat w w^
w ^ = ( X T X + λ I ) − 1 X T y \hat w = (X^TX+\lambda I)^{-1}X^Ty w^=(XTX+λI)1XTy
该过程也被称为岭回归。而类似的,如果是通过添加了 w ^ \hat w w^的1-范数的某个表达式,从而构造损失函数如下:
L o s s ( w ^ ) = ∣ ∣ y − X w ^ ∣ ∣ 2 2 + λ ∣ ∣ w ^ ∣ ∣ 1 Loss(\hat w) = ||y - X\hat w||_2^2 +\lambda ||\hat w||_1 Loss(w^)=yXw^22+λw^1
则该过程被称为Lasso。而更进一步,如果构建的损失函数同时包含 w ^ \hat w w^的1-范数和2-范数,形如如下形式:
L o s s ( w ^ ) = 1 2 n ∣ ∣ y − X w ^ ∣ ∣ 2 2 + λ α ∣ ∣ w ^ ∣ ∣ 1 + λ ( 1 − α ) 2 ∣ ∣ w ^ ∣ ∣ 2 2 Loss(\hat w) = \frac{1}{2n}||y - X\hat w||_2^2 + \lambda \alpha ||\hat w||_1 +\frac{\lambda(1-\alpha)}{2} ||\hat w||_2 ^ 2 Loss(w^)=2n1yXw^22+λαw^1+2λ(1α)w^22
则构建的是弹性网模型(Elastic-Net),其中 λ 、 α \lambda、\alpha λα都是参数,n是样本个数。不难发现,岭回归和Lasso其实都是弹性网的一种特殊形式。更多关于线性模型的相关方法,我们将在后续逐渐介绍。

1-范数也被称为L1范数,将参数的1-范数添加入损失函数的做法,也被称为损失函数的L1正则化,L2正则化也类似。在大多数情况下,添加正则化项也可称为添加惩罚函数 p ( w ) p(w) p(w),核心作用是缓解模型过拟合倾向。

三、线性回归的决定系数

对于线性回归模型来说,除了SSE以外,我们还可使用决定系数(R-square,也被称为拟合优度检验)作为其模型评估指标。决定系数的计算需要使用之前介绍的组间误差平方和和离差平方和的概念。在回归分析中,SSR表示聚类中类似的组间平方和概念,表意为Sum of squares of the regression,由预测数据与标签均值之间差值的平方和计算的出:
S S R = ∑ i = 1 n ( y i ˉ − y i ^ ) 2 SSR =\sum^{n}_{i=1}(\bar{y_i}-\hat{y_i})^2 SSR=i=1n(yiˉyi^)2
而SST(Total sum of squares)则是实际值和均值之间的差值的平方和计算得到:
S S T = ∑ i = 1 n ( y i ˉ − y i ) 2 SST =\sum^{n}_{i=1}(\bar{y_i}-y_i)^2 SST=i=1n(yiˉyi)2
并且, S S T SST SST可由 S S R + S S E SSR+SSE SSR+SSE计算得出。而决定系数,则由 S S R SSR SSR S S T SST SST共同决定:
R − s q u a r e = S S R S S T = S S T − S S E S S E = 1 − S S E S S T R-square=\frac{SSR}{SST}=\frac{SST-SSE}{SSE}=1-\frac{SSE}{SST} Rsquare=SSTSSR=SSESSTSSE=1SSTSSE
很明显,决定系数是一个鉴于[0,1]之间的值,并且约趋近于1,模型拟合效果越好。我们可以通过如下过程,进行决定系数的计算:

sst = np.power(labels - labels.mean(), 2).sum()
sse = SSELoss(features, w, labels)
r = 1-(sse/sst)
r
#array([[0.50011461]])

Lesson 3.4 机器学习模型结果可信度理论与交叉验证基础

“农场里有群火鸡,农场主每天中午十一点来喂食。火鸡中有位科学家观察了近一年无例外后宣布发现了宇宙一个伟大定律:“每天上午十一点,会有食物降临。”感恩节早晨,它向火鸡们公布了这个定律,但这天上午十一点食物没有降临,农场主将它们捉去杀掉,把它们变成了食物。”                             --罗素的火鸡

# 科学计算模块
import numpy as np
import pandas as pd

# 绘图模块
import matplotlib as mpl
import matplotlib.pyplot as plt

# 自定义模块
from ML_basic_function import *

一、机器学习模型结果可信度理论基础与数据集划分

1.机器学习模型结果可信度基础理论

在Lesson 0中我们曾说,模型评估指标是用于评估模型效果好坏的数值指标,例如SSE就是评估回归类模型拟合效果的指标。但是否是评估指标好的模型就一定能用呢?其实并不一定。这里会涉及到一个关于评估指标可信度、或者说了解模型真实性能的重要命题。

其实,要了解模型的性能其实并不简单,固然我们会使用某些指标去进行模型评估,但其实指标也只是我们了解模型性能的途径而不是模型性能本身。而要真实、深刻的评判模型性能,就必须首先了解机器学习的建模目标,并在此基础之上熟悉我们判断模型是否能够完成目标的一些方法,当然,只有真实了解的模型性能,我们才能进一步考虑如何提升模型性能。因此,在正式讲解模型优化方法之前,我们需要花些时间讨论机器学习算法的建模目标、机器学习算法为了能够达到目标的一般思路,以及评估模型性能的手段,也就是模型评估指标。

无论是机器学习还是传统的统计分析模型,核心使命就是探索数字规律,而有监督学习则是希望在探索数字规律的基础上进一步对未来进行预测,当然,在数字的世界,这个预测未来,也就是预测未来某项事件的某项数值指标,如某地区未来患病人次、具备某种数字特征的图片上的动物是哪一类,此处的未来也并非指绝对意义上的以后的时间,而是在模型训练阶段暂时未接触到的数据。正是因为模型有了在未知标签情况下进行预判的能力,有监督学习才有了存在的价值,但我们知道,基本上所有的模型,都只能从以往的历史经验当中进行学习,也就是在以往的、已经知道的数据集上进行训练(如上述利用已知数据集进行模型训练,如利用过往股票数据训练时间序列模型),这里的核心矛盾在于,在以往的数据中提取出来的经验(也就是模型),怎么证明能够在接下来的数据中也具备一定的预测能力呢?或者说,要怎么训练模型,才能让模型在未知的数据集上也拥有良好的表现呢?

目的相同,但在具体的实现方法上,传统的数理统计分析建模和机器学习采用了不同的解决方案。

首先,在统计分析领域,我们会假设现在的数据和未来的数据其实都属于某个存在但不可获得的总体,也就是说,现在和未来的数据都是从某个总体中抽样而来的,都是这个总体的样本。而正式因为这些数据属于同一个总体,因此具备某些相同的规律,而现在挖掘到的数据规律也就在某些程度上可以应用到未来的数据当中去,不过呢,不同抽样的样本之间也会有个体之间的区别,另外模型本身也无法完全捕获规律,而这些就是误差的来源。

虽然样本和总体的概念是统计学概念,但样本和总体的概念所假设的前后数据的“局部规律一致性”,却是所有机器学习建模的基础。试想一下,如果获取到的数据前后描绘的不是一件事情,那么模型训练也就毫无价值(比如拿着A股走势预测的时间序列预测某地区下个季度患病人次)。因此,无论是机器学习所强调的从业务角度出发,要确保前后数据描述的一致性,还是统计分析所强调的样本和总体的概念,都是建模的基础。

在有了假设基础之后,统计分析就会利用一系列的数学方法和数理统计工具去推导总体的基本规律,也就是变量的分布规律和一些统计量的取值,由于这个过程是通过已知的样本去推断未知的总体,因此会有大量的“估计”和“检验”,在确定了总体的基本分布规律之后,才能够进一步使用统计分析模型构建模型(这也就是为什么在数理统计分析领域,构建线性回归模型需要先进行一系列的检验和变换的原因),当然,这些模型都是在总体规律基础之上、根据样本具体的数值进行的建模,我们自然有理由相信这些模型对接下来仍然是从总体中抽样而来的样本还是会具备一定的预测能力,这也就是我们对统计分析模型“信心”的来源。简单来说,就是我们通过样本推断总体的规律,然后结合总体的规律和样本的数值构建模型,由于模型也描绘了总体规律,所以模型对接下来从总体当中抽样而来的数据也会有不错的预测效果,这个过程我们可以通过下图来进行表示。
LESSON 3 线性回归的手动实现_第23张图片
而对于机器学习来说,并没有借助“样本-总体”的基本理论,而是简单的采用了一种后验的方法来判别模型有效性,前面说到,我们假设前后获取的数据拥有规律一致性,但数据彼此之间又略有不同,为了能够在捕捉规律的同时又能考虑到“略有不同”所带来的误差,机器学习会把当前能获取到的数据划分成训练集(trainSet)和测试集(testSet),在训练集上构建模型,然后带入测试集的数据,观测在测试集上模型预测结果和真实结果之间的差异。这个过程其实就是在模拟获取到真实数据之后模型预测的情况,此前说到,模型能够在未知标签的数据集上进行预测,就是模型的核心价值,此时的测试集就是用于模拟未来的未知标签的数据集。如果模型能够在测试集上有不错的预测效果,我们就“简单粗暴”的认为模型可以在真实的未来获取的未知数据集上有不错的表现。其一般过程可以由下图表示。
LESSON 3 线性回归的手动实现_第24张图片
虽然对比起数理统计分析,机器学习的证明模型有效性的过程更加“简单”,毕竟只要一次“模拟”成功,我们就认为模型对未来的数据也拥有判别效力,但这种“简单”的处理方式却非常实用,可以说,这是一种经过长期实践被证明的行之有效的方法。这也是为什么机器学习很多时候也被认为是实证类的方法,而在以后的学习中,我们也将了解到,机器学习有很多方法都是“经验总结的结果”。相比数理统计分析,确实没有“那么严谨”,但更易于理解的理论和更通用的方法,却使得机器学习可以在更为广泛的应用场景中发挥作用。(当然,负面影响却是,机器学习在曾经的很长一段时间内并不是主流的算法。)

据此,我们称模型在训练集上误差称为训练误差,在测试集上的误差称为泛化误差,不过毕竟在测试集上进行测试还只是模拟演习,我们采用模型的泛化能力来描述模型在未知数据上的判别能力,当然泛化能力无法准确衡量(未知的数据还未到来,到来的数据都变成了已知数据),我们只能通过模型在训练集和测试集上的表现,判别模型泛化能力,当然,就像此前说的一样,最基本的,我们会通过模型在测试集上的表现来判断模型的泛化能力。

2.数据集切分方法

接下来,我们就尝试通过数据集切分来执行更加可信的机器学习建模流程。首先是对数据集进行切分。一般来说,为了避免数据集上下顺序对数据规律的影响,我们会考虑对数据集进行随机切分,其中70%-80%作为训练集、20%-30%作为测试集。此处我们先考虑构建一个数据切分函数,用于训练集和测试集的切分:

np.random.shuffle函数
在NumPy中,我们可以非常便捷的通过调用np.random.shuffle函数来进行二维数组按行重排的相关操作,而二维数组也就是结构化数据的一般表示形式,因此我们可以使用该函数进行数据集乱序排列。并且我们可以通过设置随机数种子,来复现这个乱序的过程,这将极大程度有助于当需要对多个序列进行乱序排列时的代码简化。

A = np.arange(10).reshape(5, 2)
A
#array([[0, 1],
#       [2, 3],
#       [4, 5],
#       [6, 7],
#       [8, 9]])
B = np.arange(0, 10, 2).reshape(-1, 1)
B
#array([[0],
#       [2],
#       [4],
#       [6],
#       [8]])
np.random.seed(24)
np.random.shuffle(A)
A
#array([[8, 9],
#       [2, 3],
#       [0, 1],
#       [6, 7],
#       [4, 5]])
np.random.seed(24)
np.random.shuffle(B)
B
#array([[8],
#       [2],
#       [0],
#       [6],
#       [4]])
  • np.vsplit切分函数
    此外,我们可以通过vsplit函数进行数组的按行切分,也就相当于split(axis=0)。
A
#array([[8, 9],
#       [2, 3],
#       [0, 1],
#       [6, 7],
#       [4, 5]])
# 从行索引的第二、三元素之间切开
np.vsplit(A,[2, ])
#[array([[8, 9],
#        [2, 3]]),
# array([[0, 1],
#        [6, 7],
#        [4, 5]])]
  • 数据集切分函数
    基于上述函数,我们可以非常简单的构建一个数据集切分函数:
def array_split(features, labels, rate=0.7, random_state=24):
    """
    训练集和测试集切分函数
    
    :param features: 输入的特征张量
    :param labels:输入的标签张量
    :param rate:训练集占所有数据的比例
    :random_state:随机数种子值
    :return Xtrain, Xtest, ytrain, ytest:返回特征张量的训练集、测试集,以及标签张量的训练集、测试集 
    """
    
    np.random.seed(random_state)                           
    np.random.shuffle(features)                             # 对特征进行切分
    np.random.seed(random_state)
    np.random.shuffle(labels)                               # 按照相同方式对标签进行切分
    num_input = len(labels)                                 # 总数据量
    split_indices = int(num_input * rate)                   # 数据集划分的标记指标
    Xtrain, Xtest = np.vsplit(features, [split_indices, ])  
    ytrain, ytest = np.vsplit(labels, [split_indices, ])
    return Xtrain, Xtest, ytrain, ytest

一般来说,训练集和测试集可以按照8:2或7:3比例进行划分。在进行数据划分的过程中,如果测试集划分数据过多,参与模型训练的数据就会相应减少,而训练数据不足则会导致模型无法正常训练、损失函数无法收敛、模型过拟合等问题,但如果反过来测试集划分数据过少,则无法代表一般数据情况测试模型是否对未知数据也有很好的预测作用。因此,根据经验,我们一般来说会按照8:2或7:3比例进行划分。
  看到这里,相信肯定有小伙伴觉得根据所谓的“经验”来定数据集划分比例不太严谨,有没有一种方法能够“精准”的确定什么划分比例最佳呢?例如通过类似最小二乘法来计算划分比例?
  值得一提的是,在机器学习领域,充斥着大量的“经验之谈”或者“约定俗成”的规则,一方面这些经验为建模提供了诸多便捷、也节省了很多算力,但另一方面,通过经验来决定影响模型效果的一些“超参数”取值的不严谨的做法,也被数理统计分析流派所诟病。

接下来,测试函数性能

f = np.arange(10).reshape(-1, 1)                 # 创建特征0-9
f
#array([[0],
#       [1],
#       [2],
#       [3],
#       [4],
#       [5],
#       [6],
#       [7],
#       [8],
#       [9]])
l = np.arange(1, 11).reshape(-1, 1)             # 创建标签1-10,保持和特征+1的关系
l
#array([[ 1],
#       [ 2],
#       [ 3],
#       [ 4],
#       [ 5],
#       [ 6],
#       [ 7],
#       [ 8],
#       [ 9],
#       [10]])
array_split(f, l)
#(array([[9],
#        [4],
#        [8],
#        [7],
#        [5],
#        [6],
#        [1]]),
# array([[0],
#        [3],
#        [2]]),
# array([[10],
#        [ 5],
#        [ 9],
#        [ 8],
#        [ 6],
#        [ 7],
#        [ 2]]),
# array([[1],
#        [4],
#        [3]]))

3.线性回归手动实现

根据机器学习结果可信度理论,我们构建一个在训练集上训练、在测试集上测试的完整的线性回归实现流程。

数据准备

# 设置随机数种子
np.random.seed(24)   

# 扰动项取值为0.01
features, labels = arrayGenReg(delta=0.01)

# 数据切分
Xtrain, Xtest, ytrain, ytest = array_split(features, labels)
  • 在训练集上训练
    最小二乘法求解公式: w ^ = ( X T X ) − 1 X T y \hat w = (X^TX)^{-1}X^Ty w^=(XTX)1XTy
w = np.linalg.inv(Xtrain.T.dot(Xtrain)).dot(Xtrain.T).dot(ytrain)
w
#array([[ 1.99976073],
#       [-0.99986178],
#       [ 0.99934303]])

在大多数情况下,所谓训练模型,都是训练得出模型的一组参数。

  • 在测试集上测试
    然后即可在测试集上计算模型评估指标:
SSELoss(Xtest, w, ytest)
#array([[0.02725208]])

至此,我们即完成了模型在训练集上训练得出参数,然后运行测试集观察结果的全过程。由于数据情况较为简单,因此模型效果较好。

但如果测试集效果不好,我们能否因此对模型进行调整的呢?如果测试集真的完全不参与建模,那么根据测试集反馈结果调整模型是否算测试集间接参与建模?如果测试集不能间接参与建模,那测试集的提供的模型结果反馈又有什么作用呢?这就是所谓的,测试集悖论。

4.测试集的“不可知”悖论

我们已经知道,机器学习模型主要通过模型在测试集上的运行效果来判断模型好坏,测试集相当于是“高考”,而此前的模型训练都相当于是在练习,但怎么样的练习才能有效的提高高考成绩,这里就存在一个“悖论”,那就是练习是为了高考,而在高考前我们永远不知道练习是否有效,那高考对于练习的核心指导意义何在?在机器学习领域,严格意义上的测试集是不能参与建模的,此处不能参与建模,不仅是指在训练模型时不能带入测试集进行训练,更是指当模型训练完成之后、观察模型在测试集上的运行结果后,也不能据此再进行模型修改(比如增加神经网络层数),后面我们会提到,把数据带入模型训练是影响模型参数,而根据模型运行结果再进行模型结构调整,实际上是修改了模型超参数,不管是修改参数还是超参数,都是影响了模型建模过程,都相当于是带入进行了建模。是的,如果通过观察测试集结果再调整模型结构,也相当于是带入测试集数据进行训练,而严格意义上的测试集,是不能带入模型训练的。(这是一个有点绕的“悖论”…)

但是,还记得我们此前说的,机器学习建模的核心目标就是提升模型的泛化能力么?而泛化能力指的是在模型未知数据集(没带入进行训练的数据集)上的表现,虽然测试集只能测一次,但我们还是希望有机会能把模型带入未知数据集进行测试,此时我们就需要一类新的数据集——验证集。验证集在模型训练阶段不会带入模型进行训练,但当模型训练结束之后,我们会把模型带入验证集进行计算,通过观测验证集上模型运行结果,判断模型是否要进行调整,验证集也会模型训练,只不过验证集训练的不是模型参数,而是模型超参数,关于模型参数和超参数的概念后面还会再详细讨论,当然,我们也可以把验证集看成是应对高考的“模拟考试”,通过“模拟考试”的考试结果来调整复习策略,从而更好的应对“高考”。总的来说,测试集是严格不能带入训练的数据集,在实际建模过程中我们可以先把测试集切分出来,然后“假装这个数据集不存在”,在剩余的数据集中划分训练集和验证集,把训练集带入模型进行运算,再把验证集放在训练好的模型中进行运行,观测运行结果,再进行模型调整。

总的来说,在模型训练和观测模型运行结果的过程总共涉及三类数据集,分别是训练集、验证集和测试集。不过由于测试集定位特殊,在一些不需要太严谨的场景下,有时也会混用验证集和测试集的概念,我们常常听到“测试集效果不好、重新调整模型”等等,都是混用了二者概念,由于以下是模拟练习过程,暂时不做测试集和验证集的区分。在不区分验证集和测试集的情况下,当数据集切分完成后,对于一个模型来说,我们能够获得两套模型运行结果,一个是训练集上模型效果,一个是测试集上模型效果,而这组结果,就将是整个模型优化的基础数据。

在某些场景下,测试集确实是严格不可知的,比如在线提交结果的数据竞赛。

二、交叉验证基本思想

除了训练集-测试集划分理论之外,和模型结果可信度相关的,还有一个基本理论——交叉验证。

尽管通过训练集和测试集的划分,我们可以以不参与建模的测试集的结论来证明模型结果的可信度,但在很多实际场景中,数据集的随机切分本身也是影响模型泛化能力、影响测试集结果可信度的重要因素。此时,我们可以采用一种名为交叉验证的技术手段来进一步提升模型最终输出结果的可信度。交叉验证的基本思想非常简单,在我们不严格区分测试集的情况下,我们可以将数据集整体按照某个比例切分,然后进行循环验证。在所有的切分方法中,最基础也最常用的一种就是所谓的K-fold(K折)验证,也就是将数据集进行K份等比例划分,然后依次取出其中一份进行验证(测试)、剩下几份进行训练。例如,当我们使用10折验证时,数据集划分情况如下所示:
LESSON 3 线性回归的手动实现_第25张图片
假设仍然是此前我们创建的数据集并且仍然采用SSE作为模型评估指标,则在进行十折验证时可以计算出十组SSE取值,最终我们可以对这十组结果进行均值计算,求得这组参数最终所对应的模型评估指标结果。不过此时我们需要在所有的数据集上进行训练,然后再进行交叉验证。

不得不说,对于线性回归的交叉验证过程其实和训练集测试集划分理论略显冲突,如果缺少了测试集的后验过程,再训练集上再怎么训练,得出的结果都没有反馈调整的机会。其实从更加根本的角度来说,在机器学习理论体系中,一个更加严谨的做法,是先划分训练集和测试集,然后再在训练集上划分测试集,并且“训练集-测试集”划分方法用于进行模型参数训练,而“训练集-验证集”的划分方法主要用于进行模型超参数选取。此处由于我们尚未接触超参数相关概念,因此目前暂时先只介绍其基本思想。

你可能感兴趣的:(机器学习,线性回归,机器学习,算法)