文章目录
- 在此前的内容当中,我们已经学习了关于线性回归模型的基本概念,并且介绍了一个多元线性回归的损失函数求解方法——最小二乘法。
- 在有了这一些列理论推导之后,本节我们将尝试在一个手动构建的数据集上进行完整的线性回归模型建模。
一、变量相关性基础理论
- 变量彼此之间的相关性,是我们探究数据规律的重要手段。对于机器学习,相关性也是数据探索、特征工程环节的重要理论。
- 因此,我们先介绍关于连续变量相关性的基础理论,并且在该理论基础之上,探讨关于规律捕捉和规律创造的相关话题,为下一节创建数据生成器做准备。
# 科学计算模块 import numpy as np import pandas as pd # 绘图模块 import matplotlib as mpl import matplotlib.pyplot as plt
机器学习的学习目标,其实就是数据集中隐藏的数字规律,而又由于这些规律背后代表的是某些事物的真实属性或者运行状态。
相关系基本解释与相关系数计算公式
对于不同数据集来说,是否具备规律、以及规律隐藏的深浅都不一样。
对于模型来说,擅长挖掘的规律、以及规律挖掘的能力也都各不相同。
对于线性回归来说,捕捉的实际上是数据集的线性相关的规律。所谓线性相关,简单来说就是数据的同步变化特性。例如此前数据集:
Whole weight | Rings |
1 | 2 |
3 | 4 |
特征和标签就存在着非常明显的同步变化的特性:第二条样本特征增加 2、对应标签也增加 2,当然,这也就是线性模型本身可解释性的来源——体重越大的鲍鱼、年龄越大,并且体重每增加 2、年龄也增加 2。
这种同步变化特性用更加专业的角度来描述就是变量之间的相关性。这种相关性可以用一个计算公式算得,也就是相关性系数计算公式:
- 此处介绍的相关系数计算也被称为皮尔逊相关系数,最早由统计学家卡尔·皮尔逊提出,是目前最为通用的相关系数计算方法。
- 相关系数计算在 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}A 2∗2 ,其中 a i , j a_{i,j}a i,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])
np.corrcoef(A[:, 0], A[:, 1]) #array([[ 1. , -0.91766294], # [-0.91766294, 1. ]])
总体来说,相关系数绝对值越大两个变量的相关性越强,绝对值为 1 时候代表完全相关,两个变量完全线性同步变化,其中一个变量可以由另一个变量线性表出。
绝对值为 0 时,则表示完全线性无关,两个变量没有线性同步变化规律,这两个变量没有线性关系。当绝对值介于 0 和 1 之间时候,相关性强弱可以表示如下:
Cor | 相关性 |
0~0.09 | 没有相关性 |
0.1~0.3 | 弱相关 |
0.3~0.5 | 中等相关 |
0.5~1.0 | 强相关 |
- 如果是双变量的相关性,我们也可以用一组函数关系及图像来进行表示。
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')
- 当然,如果我们想在 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.13021309, -0.52077058, -0.04156746, -0.39871486, -0.0664042 , # 0.63960931, 0.24413077, -0.30139878, 0.30165118, -0.35951571, # 0.11432935, 0.96468815, 1.22810311, 0.39171637, -0.18593751, # 0.45969429, 0.89990127, -0.22578841, -0.18823623, 0.51678678]) y1 = y + r y1 #array([ 0.16849765, 1.33677569, 2.94351355, 1.068285 , 1.27528793, # 0.41509024, -1.33128073, 1.71470201, 0.40760464, 0.84310426, # 1.86461507, -0.33425138, 1.99110458, 1.67303745, 0.06872278, # 0.07684552, 0.20245359, 0.7843353 , 1.5186445 , 2.23064098])
此处,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')
- 由此可见,在加入了扰动项之后,模型线性相关性明显变弱。
- 当然,伴随 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.9367437], # [0.9367437, 1. ]]), # array([[1. , 0.8911804], # [0.8911804, 1. ]]), # array([[1. , 0.81961547], # [0.81961547, 1. ]]), # array([[1. , 0.71248276], # [0.71248276, 1. ]]), # array([[1. , 0.62837293], # [0.62837293, 1. ]]), # array([[1. , 0.39817207], # [0.39817207, 1. ]])] yl #[array([ 1.627525 , -0.88382241, 1.86933795, -0.02300046, 0.73433085, # -0.33109922, 1.33691722, 2.86218277, 0.41444553, 1.9081874 , # -1.15456573, -1.10209687, 0.22127724, 1.08855797, 2.65327611, # 0.72665134, 1.91516867, 0.14535214, -0.00467952, 2.90418153]), # array([ 1.83074703, -0.91569175, 2.18530488, -0.21976011, 0.71340074, # -0.63292082, 1.10134436, 3.28833318, 0.34776874, 1.78608826, # -1.14334884, -1.22150795, 0.02903983, 1.2104614 , 2.61838561, # 0.76623911, 1.93033199, 0.01149336, 0.01992074, 2.97614544]), # array([ 2.13558008, -0.96349575, 2.65925528, -0.5148996 , 0.68200557, # -1.08565322, 0.74798509, 3.92755879, 0.24775356, 1.60293956, # -1.12652351, -1.40062457, -0.25931629, 1.39331655, 2.56604986, # 0.82562076, 1.95307696, -0.18929479, 0.05682113, 3.0840913 ]), # array([ 2.64363515, -1.04316909, 3.4491726 , -1.00679874, 0.62968028, # -1.84020721, 0.15905296, 4.99293481, 0.08106159, 1.29769172, # -1.0984813 , -1.69915227, -0.73990982, 1.69807513, 2.47882362, # 0.92459017, 1.99098525, -0.52394172, 0.11832178, 3.26400107]), # array([ 3.15169023, -1.12284243, 4.23908993, -1.49869788, 0.577355 , # -2.59476121, -0.42987917, 6.05831082, -0.08563037, 0.99244388, # -1.07043908, -1.99767997, -1.22050335, 2.00283371, 2.39159737, # 1.02355959, 2.02889354, -0.85858865, 0.17982242, 3.44391085]), # array([ 6.20002067, -1.60088248, 8.9785939 , -4.45009273, 0.26340331, # -7.12208519, -3.96347196, 12.45056692, -1.08578218, -0.83904317, # -0.90218579, -3.78884616, -4.10406454, 3.83138519, 1.86823989, # 1.6173761 , 2.25634329, -2.86647023, 0.54882631, 4.52336949])] plt.plot(X, yl[0], 'o')
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-')
能够明显看出,伴随 delta 取值越来越大,数据相关性越来越弱,当然,我们也能通过观察 cl 的取值来查看各组变量的相关系数。
cl #[array([[1. , 0.9367437], # [0.9367437, 1. ]]), # array([[1. , 0.8911804], # [0.8911804, 1. ]]), # array([[1. , 0.81961547], # [0.81961547, 1. ]]), # array([[1. , 0.71248276], # [0.71248276, 1. ]]), # array([[1. , 0.62837293], # [0.62837293, 1. ]]), # array([[1. , 0.39817207], # [0.39817207, 1. ]])]
捕捉规律与创建规律
简单线性回归就是在二维平面中通过构建一条直线来试图捕捉平面当中的点,线性相关性越弱、线性模型越难捕捉到这些所有点,模型效果就越差。
数据集之间线性相关性越明显,数据规律越明显,模型越容易捕捉到这些规律。
在这个基础理论之上,我们有以下两方面的应用:
(1) 可以在构建线性模型之前先探查数据本身的线性相关性,如果自变量和因变量存在很好的相关性,那就一定可以顺利的构建线性回归模型对数据进行拟合。
如果线性相关性不强,则说明当前数据并不适合构建线性回归模型,或者在构建模型之前我们需要对数据进行一些不影响真实规律的转化,令其表现出线性的分布趋势。
(2) 上述几组不同的数据,实际上就代表着对线性回归模型建模难度各不相同的几组数据, delta 越大对线性回归模型来说建模就更加困难。
据此,我们可以生成一个手动创建数据集的函数,该函数可以输出不同建模难度(规律深浅)的数据集,来辅助我们测试模型和优化算法的性能。
这里需要注意:线性相关性的减弱,不仅是对于线性回归模型,对于很多回归类问题都会造成更大的建模困难。
二、数据生成器与 Python 模块编写
- 在有了相关性理论基础之后,我们即可创建一个可用于回归模型实验的数据生成器。
# 科学计算模块 import numpy as np import pandas as pd # 绘图模块 import matplotlib as mpl import matplotlib.pyplot as plt
1. 自定义数据生成器
为了方便后续练习的展开,我们尝试自己创建一个数据生成器,用于自主生成一些符合某些条件、难度可控、具备某些特性的数据集。
在初学过程中,我们需要通过类似控制变量的方法、通过设计一些实验,去深入理解算法运行原理及一些优化方法的实际作用,这就需要我们自己动手,创建一些数据用于实验的原材料,通过一些实验深入了解模型原理。
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 features #array([[-1.07081626, -1.43871328], # [ 0.56441685, 0.29572189], # [-1.62640423, 0.2195652 ], # ..., # [-1.44029131, 0.50439425], # [ 0.96604603, 1.76623359], # [ 0.57852053, -1.34028424]]) labels_true
labels_true 所产生的数据如下。
注意,此时 labels_true 和 features 满足严格意义上的线性方程关系
y=2x1−x2+1
但我们实际使用的标签 labels,则是在 labels_true 的基础上增添了一个扰动项,np.random.normal(size = labels_true.shape) * delta,这其实也符合我们一般获取数据的情况:真实客观世界或许存在某个规律,但我们搜集到的数据往往会因为各种原因存在一定的误差,无法完全描述真实世界的客观规律,这其实也是模型误差的来源之一(另一个误差来源是模型本身捕获规律的能力)。
y=2x1−x2+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) # 第二个特征和标签的关系
- 不难看出,两个特征和标签都存在一定的线性关系,并且跟特征的系数绝对值有很大关系。
- 当然,若要增加线性模型的建模难度,可以增加扰动项的数值比例,从而削弱线性关系。
# 设置随机数种子 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') # 第二个特征和标签的关系
当然,我们也能生成非线性关系的数据集,此处我们创建满足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)
此时需要注意的是,无论是创建了曲线规律的数据集,还是增加了扰动项绝对数值,都会对建模造成困难,但二者的造成困难的方式是不同的。
如果是曲线规律的数据集,则代表规律本身更加复杂,此时需要使用更加复杂的模型来进行规律提取,该部分属于模型选取和参数调优的技术内容。
如果是扰动项比较大,则代表规律被掩盖的比较深,此时需要采用其他技术手段进行白噪声的处理,该部分属于特征工程技术内容。
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
# 设置随机数种子 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) # 第二个特征和标签的关系
- 然后查看扰动项较大时数据情况。
# 设置随机数种子 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) # 第二个特征和标签的关系
- 当特征和标签满足二阶关系时候数据表现。
# 设置随机数种子 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) # 第二个特征和标签的关系
- 在定义好数据创建函数之后,即可开始尝试手动实现线性回归模型。
- 知识点补充:随机数种子的使用
- 此处我们使用了一个随机数种子,以确保随机结果的可重复性。当我们设置某个随机数种子之后,每次随机过程都是可重复的:我们只需要再次调用相同随机种子,就可以重复此前随机过程。
np.random.randn(9) #array([ 1.62779574, 0.35449279, 1.03752763, -0.38568351, 0.519818 , # 1.68658289, -1.32596315, 1.4289837 , -2.08935428]) 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]
2. Python 模块的编写与调用
封装为模块有以下两种基本方法:
(1) 打开文本编辑器,将写好并测试完成的函数写入其中,并将文本的拓展名改写为 .py。
(2) 在 spyder 或者 pycharm 中复制相关函数,并保存为 .py 文件。
然后将文件保存在jupyter主目录下,并取名为 ML_basic_function,后续即可通过import
ML_basic_function 进行调用。
如果是 jupyter lab 用户,也可按照如下方式进行编写。
(1) 打开左侧文件管理栏页,点击新建。
- (2) 在新建目录中,选择 Test File,并通过重命名将其命名为 ML_basic_function.py。
(3) 在打开的文本编辑器中输入代码,并且需要注意,需要在 .py 文件内导入相关的包。
4) 测试能否调用,首先重启当前 jupyter。
然后尝试导入自定义模块。