数理统计、机器学习入门
目录和附件
目录和附件
数理统计以概率论为基础, 研究大量随机现象的统计规律性. 分为 描述统计 和 推断统计 , 在数据分析领域具有非常重要的地位
描述统计, 就是从总体数据中提取变量的主要信息(总和, 均值, 最大, 最多等), 从而从总体层面上, 对数据进行统计性描述. 通常配合绘制相关统计图进行辅助
统计学中的变量指研究对象的特征(属性), 每个变量都有变量值和类型, 类型可分为:
类别变量 : 对研究对象定性, 分类
类别变量又可分为:
数值变量 : 对研究对象定量描述
数值变量又可分为:
数值变量对加, 减, 求平均等操作有意义, 而类别变量无意义
描述统计所提取的统计信息, 称为统计量, 主要包括:
准备数据:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
plt.rcParams['font.family'] = 'SimHei'
plt.rcParams['axes.unicode_minus'] = False
# 正态分布
data1 = np.around(np.random.normal(10, 3, 600)).reshape(-1, 1)
# 左偏
t1 = np.random.randint(1, 21, size=100)
t2 = np.random.randint(21, 31, size=500)
left_data = np.concatenate([t1, t2]).reshape(-1, 1)
# 右偏
t3 = np.random.randint(1, 11, size=500)
t4 = np.random.randint(11, 21, size=100)
right_data = np.concatenate([t3, t4]).reshape(-1, 1)
# 类别
type_data = np.random.randint(0, 2, size=600).reshape(-1, 1)
data = np.concatenate([data1, left_data, right_data, type_data], axis=1)
data = pd.DataFrame(data,
columns=['data1', 'left_data', 'right_data', 'type_data'])
# 随机取 10 条数据
data.sample(10)
data1 left_data right_data type_data
202 13.0 27.0 8.0 0.0
595 12.0 23.0 15.0 0.0
523 11.0 21.0 20.0 1.0
259 12.0 29.0 8.0 0.0
498 12.0 24.0 3.0 0.0
110 8.0 27.0 1.0 0.0
65 7.0 12.0 5.0 0.0
231 13.0 25.0 2.0 0.0
321 8.0 30.0 3.0 0.0
544 5.0 29.0 19.0 1.0
数据中某个类别出现的次数称为该类别的频数
例如, 计算上述两个类别(0.0
和 1.0
)出现的频数:
frequency = data['type_data'].value_counts()
frequency
0.0 309
1.0 291
Name: type_data, dtype: int64
数据中某个类别出现次数与总次数的比值称为该类别的频率
例如, 计算上述两个类别(0.0
和 1.0
)出现的频率:
percentage = frequency * 100 / len(data)
percentage
0.0 51.5
1.0 48.5
Name: type_data, dtype: float64
平均值, 一组数据的总和除以数据的个数
将一组数据按顺序排列, 位于最中间位置的值, 即是中位数, 如果数据个数为偶数, 取中间两个的平均值
一组数据中出现次数最多的值
通常三者的关系如下图所示:
注意点 :
数值变量通常使用均值和中值表示集中趋势, 类别变量则通常使用众数
正态分布下, 数据量足够多, 三者相同
均值使用所有数据计算, 容易受极端值影响, 中位数和众数则不会
众数在一组数据中可能不唯一
例, 计算字段 data1
的均值, 中位数和众数:
mean = data['data1'].mean()
median = data['data1'].median()
mode = data['data1'].mode()
print(f'均值:{mean} 中位数:{median}\n众数:\n{mode}')
均值:10.121666666666666 中位数:10.0
众数:
0 9.0
dtype: float64
通过 n - 1 个分位, 将升序排列的数据分为 n 个区间, 使得每个区间数值个数相等(或近似相等), 则每个分位对应的数, 就是该 n 分位的分位数. 常用的有四分位数和百分位数
以四分位数为例:
第一个分位称为 1/4 分位(下四分位), 第二个称为 2/4 分位(中四分位), 第三个称为 3/4 分位(上四分位), 其中中四分位数, 其实就是中位数
求四分位的值:
首先计算各个分位的位置
index1 = (n - 1) * 0.25
index2 = (n - 1) * 0.5
index3 = (n - 1) * 0.75
(index 从 0 开始, n 为元素的个数)
根据位置计算各个分位的值
index 为整数, 值就是相应的 index 对应的元素
index 不为整数, 四分位位置介于 ceil(index) 和 floor(index) 之间, 加权计算分位值
例, 求 x 的四分位数:
index 为整数
x = np.arange(0, 9)
n = len(x)
index1 = (n - 1) * 0.25
index2 = (n - 1) * 0.5
index3 = (n - 1) * 0.75
index = np.array([index1, index2, index3]).astype(np.int32)
x[index]
array([2, 4, 6])
index 不是整数
x = np.arange(0, 10)
n = len(x)
index1 = (n - 1) * 0.25
index2 = (n - 1) * 0.5
index3 = (n - 1) * 0.75
index = np.array([index1, index2, index3])
left = np.floor(index).astype(np.int32)
right = np.ceil(index).astype(np.int32)
weight, _ = np.modf(index) # 获取 index 整数和小数部分
result = x[left] * (1 - weight) + x[right] * weight
result
array([2.25, 4.5 , 6.75])
Numpy 中计算分位数可直接用方法 np.quantile
和 np.percentile
np.quantile(x, q=[0.25, 0.5, 0.75]), np.percentile(x, q=[25, 50, 75])
(array([2.25, 4.5 , 6.75]), array([2.25, 4.5 , 6.75]))
Pandas 中计算分位数可利用 describe
(默认 4 分位)
s = pd.Series(x)
s.describe()
count 10.00000
mean 4.50000
std 3.02765
min 0.00000
25% 2.25000
50% 4.50000
75% 6.75000
max 9.00000
dtype: float64
s.describe().iloc[4:7]
25% 2.25
50% 4.50
75% 6.75
dtype: float64
可自定义分位:
s.describe(percentiles=[0.15, 0.4, 0.8])
count 10.00000
mean 4.50000
std 3.02765
min 0.00000
15% 1.35000
40% 3.60000
50% 4.50000
80% 7.20000
max 9.00000
dtype: float64
一组数据中, 最大值与最小值之差
方差体现一组数据中, 每个元素与均值的偏离程度
$$\sigma^{2}=\frac{1}{n-1} \sum_{i=1}^{n}\left(x_{i}-\bar{x}\right)^{2}$$
$x_{i}:$ 数组中的每个元素
$n:$ 数组元素的个数
$\bar{x}:$ 数组中所有元素的均值
标准差为方差的开方. 方差和标准差可以体现数据的分散性, 越大越分散, 越小越集中. 也可体现数据波动性(稳定性), 越大波动越大, 反之亦然
当数据足够多时, 可用 n 代替 n - 1
例, 计算 left_data
字段的极差, 方差, 标准差:
sub = np.ptp(data['left_data'])
var = data['left_data'].var()
std = data['left_data'].std()
sub, var, std
(29.0, 44.631048970506306, 6.680647346665315)
绘图对比 data1
和 left_data
的分散程度
plt.figure(figsize=(11, 1))
plt.ylim(-0.5, 1.5)
plt.plot(data['data1'], np.zeros(len(data)), ls='', marker='o', color='r', label='data1')
plt.plot(data['left_data'], np.ones(len(data)), ls='', marker='o', color='g', label='left_data')
plt.axvline(data['data1'].mean(), ls='--', color='r', label='data1均值')
plt.axvline(data['left_data'].mean(), ls='--', color='g', label='left_data均值')
plt.legend()
plt.show()
统计数据分布偏斜方向和程度的度量, 统计数据分布非对称程度的数字特征, 偏度为 0 , 对称分布, 小于 0, 左偏分别, 大于 0, 右偏分布
表征概率密度分布曲线在平均值处峰值高低的特征数. 直观看来, 峰度反映了峰部的尖度, 峰度高意味着标准差增大是由低频度的大于或小于平均值的极端差值引起的. 在相同的标准差下,峰度越大,分布就有更多的极端值,那么其余值必然要更加集中在众数周围,其分布必然就更加陡峭
样本的峰度是和正态分布相比较而言的统计量, 符合正态分布的峰度为 0
例, 计算 data
中前三个字段的偏度, 峰度与标准差, 并绘图比较:
print('偏度:', data['data1'].skew(), data['left_data'].skew(), data['right_data'].skew())
print('峰度:', data['data1'].kurt(), data['left_data'].kurt(), data['right_data'].kurt())
print('标准差:', data['data1'].std(), data['left_data'].std(), data['right_data'].std())
偏度: 0.0013827051273872734 -1.704193031847586 0.9122511031664028
峰度: 0.01807838530280126 2.5013831586663304 0.29539776195275813
标准差: 2.891504548352662 6.680647346665315 4.672046842962734
sns.kdeplot(data['data1'], shade=True, label='正态')
sns.kdeplot(data['left_data'], shade=True, label='左偏')
sns.kdeplot(data['right_data'], shade=True, label='右偏')
plt.show()
推断统计, 通过样本推断总体的统计方法, 包括对总体的未知参数进行估计; 对关于参数的假设进行检查; 对总体进行预测预报等. 推断统计的基本问题可以分为两大类:一类是 参数估计 问题; 另一类是 假设检验 问题
总体, 要研究对象的所有数据, 获取通常比较困难. 总体中的某个数据, 就是个体. 从总体中抽取部分个体, 就构成了样本, 样本中的个体数, 称为样本容量.
参数估计, 用样本指标(统计量)估计总体指标(参数). 参数估计有 点估计 和 区间估计 两种
点估计是依据样本统计量估计总体中的未知参数. 通常它们是总体的某个特征值,如数学期望, 方差和相关系数等. 点估计问题就是要构造一个只依赖于样本的量,作为总体未知参数的估计值.
区间估计是根据样本的统计量, 计算出一个可能的区间(置信区间) 和 概率(置信度), 表示总体的未知参数有多少概率位于该区间.
注意:
点估计使用一个值来作为总体参数值, 能给出具体值, 但易受随机抽样影响, 准确性不够
区间估计使用一个置信区间和置信度, 表示总体参数值有多少可能(置信度)会在该范围(置信区间)内, 能给出合理的范围和信心指数, 不能给出具体值
要确定置信区间与置信度, 我们先要知道总体与样本之间, 在分布上有着怎样的联系. 中心极限定理(独立同分布的中心极限定理)给出了它们之间的联系:
如果总体均值为 $\mu$, 方差为 $\sigma^{2}$, 我们进行随机抽样, 样本容量为 n, 当 n 增大时,则样本均值 $\bar{X}$ 逐渐趋近服从均值为 $\mu$, 方差为 $\sigma^{2} / n$ 的正态分布:
$$\bar{X} \sim N\left(\mu, \sigma^{2} / n\right)$$说明:
进行多次抽样,每次抽样会得到一个均值, 这些均值会围绕在总体均值左右,呈正态分布
当样本容量 n 足够大时, 抽样样本均值的均值 ≈ 样本均值
$\bar{X}$ ≈ 总体均值
$\mu$, 样本均值分布的标准差等于
$\sigma / \sqrt{n}$
样本均值分布的标准差, 称为标准误差, 简称标准误
模拟证明:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
plt.rcParams['font.family'] = 'SimHei'
plt.rcParams['axes.unicode_minus'] = False
# 定义非正态分布总体(也可以是正态分布)
data = np.random.normal(20, 5, size=10000)
data.sort()
all_ = np.random.choice(data[0:8000], size=10000)
# sns.displot(all_)
# 将总体的均值和标准差设为已知条件
print('总体均值:', all_.mean(), '总体标准差:', all_.std())
# 创建存放每次抽样的平均值的数组(初始值为 0)
mean_arr = np.zeros(1000)
# 循环抽取 1000 个样本, 每次抽 100 个
for i in range(len(mean_arr)):
mean_arr[i] = np.random.choice(all_, size=100, replace=False).mean()
# 验证结果
print('样本均值:', mean_arr[1], '样本均值的均值:', mean_arr.mean(),
'标准误差:', mean_arr.std(), '偏度:', pd.Series(mean_arr).skew(), sep='\n')
sns.displot(mean_arr, kde=True)
plt.show()
总体均值: 18.270423532980452 总体标准差: 3.8201265113791596
样本均值:
18.194948520041606
样本均值的均值:
18.26385715935595
标准误差:
0.373202226318143
偏度:
0.00746666188264042
正态分布: $X \sim N\left(\mu, \sigma^{2}\right)$
以均值为中心:
在 1 倍标准差内包含约 68.2% 的样本数据
在 2 倍标准差内包含约 95.4% 的样本数据
在 3 倍标准差内包含约 99.7% 的样本数据
证明:
# 定义标准差
scale = 10
# 定义数据
x = np.random.normal(0, scale, size=100000)
# 计算
for times in range(1, 4):
y = x[(x > -times * scale) & (x < times * scale)]
print(f'{times}倍的标准差:')
print(f'{len(y) * 100 / len(x)}%')
1倍的标准差:
68.206%
2倍的标准差:
95.354%
3倍的标准差:
99.711%
根据中心极限定理和正态分布的特性, 如果总体标准差为 $\sigma$, 对总体进行一次抽样, 如果样本足够大, 则样品均值 $\bar{X}$ 服从正态分布, 该均值约有 95.4% 的概率会在 2 倍的标准误差 ( $\mu - 2\sigma / \sqrt{n}, \mu + 2\sigma / \sqrt{n}$) 范围内, 并且该样本均值约等于总体均值 $\mu$. 从而, 可以利用这一结论, 对总体均值进行区间估计.
结论验证:
# 随机生成总体均值, 其值未知
mean = np.random.randint(0, 10000)
# 总体的标准差已知为 50
std = 50
# 定义总体数据
all_ = np.random.normal(mean, std, size=100000)
# 从总体抽取 100 个元素构成样本
sample = np.random.choice(all_, size=100, replace=False)
# 计算样本均值
sample_mean = sample.mean()
print('样本均值:', sample_mean)
# 计算样本的标准误差
se = std / np.sqrt(n)
# 计算置信区间 95%置信度
min_ = sample_mean - 1.96 * se
max_ = sample_mean + 1.96 * se
print('置信区间(95%置信度):', (min_, max_))
# 区间估计
print(f'总体均值有 95% 的概率在{(min_, max_)}区间内')
print('总体均值:', mean)
# 绘图辅助
plt.plot(mean, 0, marker='*', color='orange', ms=12, label='总体均值')
plt.plot(sample_mean, 0, marker='o', color='r', label='样本均值')
plt.hlines(0, xmin=min_, xmax=max_, color='b', label='置信区间')
plt.axvline(min_, 0.4, 0.6, color='r', ls='--', label='左边界')
plt.axvline(max_, 0.4, 0.6, color='g', ls='--', label='右边界')
plt.legend()
plt.show()
样本均值: 9695.658932218576
置信区间(95%置信度): (9685.858932218576, 9705.458932218575)
总体均值有 95% 的概率在(9685.858932218576, 9705.458932218575)区间内
总体均值: 9696
假设检验(显著性检验), 先对总体做出假设, 然后通过判断样本与总体之间是否存在显著性差异, 来验证总体的假设
假设检验使用了一种类似于 “反证法” 的推理方法,它的特点是:
先对总体做出两个完全相反的假设, 原假设(设为真) 和 备择假设, 计算后导致不合理现象产生,则拒绝原假设, 接受备择假设, 反之接受原假设, 放弃备择假设
这种 “反证法” 不同于一般的反证法. 所谓不合理现象产生,并非指形式逻辑上的绝对矛盾,而是基于小概率原理:概率很小的事件在一次试验中几乎是不可能发生的,若发生了,就是不合理的.
怎样才算 “小概率”, 通常可将概率不超过 0.05 的事件称为 “小概率事件” ,也可视具体情形而取 0.1 或 0.01 等. 在假设检验中常记这个概率为 α,称为显著性水平
假设检验可分为正态分布检验, 正态总体均值检验, 非参数检验三类, 本文只介绍 正态总体均值检验 , 包括 Z检验 和 t检验 两种情况
对总体参数做出两个完全对立的假设, 分别为:
原假设(零假设)
$H_{0}$
备择假设(对立假设)
$H_{1}$
双边假设检验 :
$H_{0}: \mu=\mu_{0}, H_{1}: \mu \neq \mu_{0}$
单边假设检验 :
$H_{0}: \mu \geq \mu_{0}, H_{1}: \mu<\mu_{0}$ (左边检验)
$H_{0}: \mu \leq \mu_{0}, H_{1}: \mu>\mu_{0}$ ( 右边检验 )
$\mu$ 为总体均值,
$\mu_{0}$ 为假设均值
显著性水平 : 根据需要设定的小概率事件的概率 α (1 - α 为置信度)
检验统计量 (Z 和 t): 用来判断样本均值与总体均值是否存在显著性差异
P值: 通过检验统计量计算而得的概率值, 表示原假设可被拒绝的最小值(或可支持原假设的概率):
P ≤ α, 原假设可被拒绝的最小值比显著性水平还低, 原假设可被拒绝, 则拒绝原假设
P > α, 原假设可被拒绝的最小值大于显著性水平, 原假设不可被拒绝, 支持原假设
设置原假设与备择假设
设置显著性水平 α
根据问题选择假设检验的方式
计算统计量(Z 或 t)
计算 P值(Z 或 t 围成的分布面积)
根据 P值 与 α值, 决定接受原假设还是备择假设
例, 某车间用一台包装机包装葡萄糖. 袋装糖的净重是一个随机变量,它服从正态分布. 当机器正常时,其均值为 0.5kg,标准差为 0.015kg.
某日开工后为检验包装机是否正常,随机地抽取它所包装的糖 9 袋,称得净重为(kg):
0.497, 0.506, 0.518, 0.524, 0.498, 0.511, 0.520, 0.515, 0.512
判断下面说法是否正确:
(1) 机器正常
例, 某车间用包装机包装葡萄糖. 袋装糖的净重是一个随机变量,它服从正态分布. 随机地抽取糖 9 袋,称得净重为(kg):
0.497, 0.506, 0.518, 0.524, 0.498, 0.511, 0.520, 0.515, 0.512
判断下面说法是否正确:
(2) 该车间袋装糖净重均值为 0.5kg
(3) 该车间袋装糖净重均值不少于 0.5kg
(4) 该车间袋装糖净重均值不多于 0.5kg
Z检验适用于: 总体正态分布且方差已知, 样本容量较大(一般 ≥ 30)
Z统计量计算公式:
$$Z=\frac{\bar{x}-\mu_{0}}{S_{\bar{x}}}=\frac{\bar{x}-\mu_{0}}{\sigma / \sqrt{n}}$$
$\bar{x}$: 样本均值
$\mu_{0}$: 假设的总体均值
$S_{\bar{x}}$: 样本的标准误差
$\sigma$: 总体的标准差
$n$: 样本容量
检验说法(1): 机器正常
双边检验:
原假设机器正常:
$H_{0}: \mu=\mu_{0}=0.5kg$
备择假设机器不正常:
$H_{1}: \mu \neq \mu_{0} \neq 0.5kg$
设置显著性水平: α = 0.05
import numpy as np
from scipy import stats
# 样本已知
a = np.array([0.497, 0.506, 0.518, 0.524, 0.498, 0.511, 0.520, 0.515, 0.512])
# 总体均值和标准差已知
mean, std = 0.5, 0.015
# 计算样本均值
sample_mean = a.mean()
# 计算样本标准误差
se = std / np.sqrt(len(a))
# 计算 Z统计量
Z = (sample_mean - mean) / se
print('Z统计量:', Z)
# 计算 P值, 双边检验: Z值与其右边曲线围成的面积的 2 倍
P = 2 * stats.norm.sf(abs(Z))
print('P值:' , P)
Z统计量: 2.244444444444471
P值: 0.02480381963225589
由结果可知, Z值 超过了 1.96, 由 Z值 与其右边曲线围成的面积的 2 倍, 必然小于 α(1.96 与其右边曲线围成的面积的 2 倍), 计算结果 P < α, 因此拒绝原假设, 接受备择假设, 机器不正常
t检验适用于: 总体正态分布, 方差未知, 样本数量较少(一般 < 30), 但是随着样本容量的增加, 分布逐渐趋于正态分布
t统计量计算公式:
$$t=\frac{\bar{x}-\mu_{0}}{S_{\bar{x}}}=\frac{\bar{x}-\mu_{0}}{S / \sqrt{n}}$$
$\bar{x}$: 样本均值
$\mu_{0}$: 假设的总体均值
$S_{\bar{x}}$: 样本的标准误差
$S$: 样本的标准差
$n$: 样本容量
双边检验 :
检验说法(2): 该车间袋装糖净重均值为 0.5kg
原假设, 该车间袋装糖净重均值为 0.5kg:
$H_{0}: \mu=\mu_{0}=0.5kg$
备择假设, 该车间袋装糖净重均值不为 0.5kg:
$H_{1}: \mu \neq \mu_{0} \neq 0.5kg$
设置显著性水平: α = 0.05
# 样本已知
a = np.array([0.497, 0.506, 0.518, 0.524, 0.498, 0.511, 0.520, 0.515, 0.512])
# 假设的总体均值已知
mean = 0.5
# 计算样本均值
sample_mean = a.mean()
# 计算样本标准差
std = a.std()
# 计算 t统计量
t = (sample_mean - mean) / ( std / np.sqrt(len(a)))
print('t统计量:', t)
# 计算 P值, df 是自由度: 样本变量可自由取值的个数
P = 2 * stats.t.sf(abs(t), df=len(a) - 1)
print('P值:', P)
t统计量: 3.802382179137283
P值: 0.005218925008708613
P < α, 拒绝原假设, 接受备择假设: 该车间袋装糖净重均值不为 0.5kg
还可以通过 scipy 提供的方法 ttest_1samp
来进行 t检验计算:
from scipy import stats
stats.ttest_1samp(a, 0.5)
Ttest_1sampResult(statistic=3.584920298041139, pvalue=0.007137006417828698)
左边检验 :
检验说法(3): 该车间袋装糖净重均值不少于 0.5kg
原假设, 该车间袋装糖净重均值不少于 0.5kg:
$H_{0}: \mu \geq \mu_{0}$
备择假设, 该车间袋装糖净重均值少于 0.5kg:
$H_{1}: \mu<\mu_{0}$
设置显著性水平: α = 0.05
# t统计量上述已经计算, 只需计算 P值: t统计量与其左边曲线围成的面积
P = stats.t.cdf(t, df=len(a) - 1)
print('P值:', P)
P值: 0.9973905374956458
P > α, 接受原假设, 该车间袋装糖净重均值不少于 0.5kg
右边检验 :
检验说法(4): 该车间袋装糖净重均值不多于 0.5kg
原假设, 该车间袋装糖净重均值不多于 0.5kg:
$H_{0}: \mu \leq \mu_{0}$
备择假设, 该车间袋装糖净重均值多于 0.5kg:
$H_{1}: \mu>\mu_{0}$
设置显著性水平: α = 0.05
# 计算 P值: t统计量与其右边曲线围成的面积
P = stats.t.sf(t, df=len(a) - 1)
print('P值:', P)
P值: 0.0026094625043543065
P < α, 拒绝原假设, 接受备择假设, 该车间袋装糖净重均值多于 0.5kg
模型是指对于某个(类)实际问题的求解或客观事物运行规律进行抽象后的一种形式化表达方式, 可以理解为一个函数(一种映射规则)
任何模型都是由三个部分组成: 目标, 变量和关系. 建模时明确了模型的目标,才能进一步确定影响目标(因变量)的各关键变量(自变量),进而确定变量之间的关系(函数关系)
通过大量数据检验(训练)模型, 将模型(函数)的各个参数求解, 当参数确定之后, 便可利用模型对未知数据进行求值, 预测
用于训练模型的样本数据中的每个属性称为特征, 用 x 表示, 样本中的每条数据经过模型计算得到的输出值称为标签(监督学习), 用 y 表示, 从而得到 y = f(x) 的函数关系
在统计学中, 回归分析指的是确定两种或两种以上变量间相互依赖的定量关系的一种统计分析方法
回归分析按照涉及的变量的多少,分为一元回归分析和多元回归分析;按照因变量的多少,可分为简单回归分析和多重回归分析;按照自变量和因变量之间的关系类型,可分为线性回归分析和非线性回归分析
回归分析解释自变量 x 发生改变, 因变量 y 会如何改变
拟合 , 插值 和 逼近 是数值分析的三大基础工具. 线性回归和非线性回归, 也叫线性拟合和非线性拟合, 拟合就是从整体上靠近已知点列,构造一种算法(模型或函数), 使得算法能够更加符合真实数据
线性回归分析的自变量和因变量之间是线性关系, 只有一个自变量时称为 简单线性回归 , 多个自变量时称为 多元线性回归
简单线性回归方程:
$$\hat{y}=w * x+b$$$\hat{y}$ 为因变量, x 为自变量, w 为比例关系, b 为截距, w 和 b 就是模型的参数. 例如房屋价格与房屋面积的正比例关系
现实生活中自变量通常不止一个, 例如影响房屋价格的, 除了房屋面积, 还有交通, 地段, 新旧, 楼层等等因素. 不同的因素对房屋的价格影响力度(权重)不同, 因此使用多个因素来分析房屋的价格(各个因素与房屋价格近似线性关系), 可以得出多元线性回归方程:
$\hat{y}=w_{1} * x_{1}+w_{2} * x_{2}+w_{3} * x_{3}+\cdots+w_{n} * x_{n}+b$
$x$: 影响因素, 特征
$w$: 每个 x 的影响力度
$n$: 特征个数
$\hat{y}$: 房屋的预测价格
令:
$x_{0}=1, w_{0}=b$设 $\vec{w}$ 和 $\vec{x}$ 为两个向量如下:
$$\vec{w}=\left(w_{0}, w_{1}, w_{2}, w_{3}, \ldots, w_{n}\right)^{T}$$ $$\vec{x}=\left(x_{0}, x_{1}, x_{2}, x_{3}, \ldots, x_{n}\right)^{T}$$则方程可表示为:
$$\begin{aligned} \hat{y} &=w_{0} * x_{0}+w_{1} * x_{1}+w_{2} * x_{2}+w_{3} * x_{3}+\ldots \ldots+w_{n} * x_{n} \ =\sum_{j=0}^{n} w_{j} * x_{j} \ =\vec{w}^{T} \cdot \vec{x} \end{aligned}$$接下来只需要计算出参数 $\vec{w}^{T}$, 便可以建立模型
损失函数, 用来衡量模型预测值与真实值之间的差异的函数, 也称目标函数或代价函数. 损失函数的值越小, 表示预测值与真实值之间的差异越小.
因此, 求解上述模型的参数 $\vec{w}^{T}$, 就是要建立一个关于模型参数的损失函数(以模型参数 $\vec{w}^{T}$ 为自变量的函数), 然而 $\vec{w}^{T}$ 的取值组合是无限的, 目标就是通过机器学习, 求出一组最佳组合, 使得损失函数的值最小
在线性回归中, 使用平方损失函数(最小二乘法), 用 J(w) 表示:
$$\begin{array}{l} J(w)=\frac{1}{2} \sum_{i=1}^{m}\left(y^{(i)}-\hat{y}^{(i)}\right)^{2} \ =\frac{1}{2} \sum_{i=1}^{m}\left(y^{(i)}-\vec{w}^{T} \vec{x}^{(i)}\right)^{2} \end{array}$$m: 样本(训练集)数据的条数
$y^{(i)}$: 样本第 i 条数据的真实值
$\hat{y}^{(i)}$: 样本第 i 条数据的预测值
$\vec{x}^{(i)}$: 样本第 i 条数据的特征
m, $y^{(i)}$ 和 $\vec{x}^{(i)}$ 已知, 要使 J(w) 最小, 对 $\vec{w}^{T}$ 求导并令导数等于 0 , 便可求得 $\vec{w}^{T}$, 然后将样本(训练集)输入通过机器学习计算出具体的 $\vec{w}^{T}$
建立模型之后, 模型的效果如何, 需要进行评估, 对于回归模型, 可用如下指标来衡量:
MSE :
平均平方误差, 所有样本数据误差的平方和取均值:
RMSE :
平均平方误差的平方根:
MAE :
平均绝对值误差, 所有样本数据误差的绝对值的和取均值:
上述指标越小越好, 小到什么程度, 不同的对象建立的模型不一样
R² :
决定系数,反应因变量的全部变异能通过回归关系被自变量解释的比例. 如 R²=0.8,则表示回归关系可以解释因变量 80% 的变异.
换句话说,如果我们能控制自变量不变,则因变量的变异程度会减少 80%
在训练集中 R² 取值范围为 [0, 1], 在测试集(未知数据)中, R² 的取值范围为 [-∞, 1], R² 的值越大, 模型拟合越好
R² 的计算公式:
$$R^{2}=1-\frac{R S S}{T S S}=1-\frac{\sum_{i=1}^{m}\left(y^{(i)}-\hat{y}^{(i)}\right)^{2}}{\sum_{i=1}^{m}\left(y^{(i)}-\bar{y}\right)^{2}}$$$\bar{y}$: 样本(测试集)的平均值
不管何种对象建立的模型, R² 都是越大模拟越好
例一, 简单线性回归模型: 求鸢尾花花瓣长度和宽度的关系
import numpy as np
# 导入用于线性回归的类
from sklearn.linear_model import LinearRegression
# 切分训练集与测试集的模块
from sklearn.model_selection import train_test_split
# 鸢尾花数据集
from sklearn.datasets import load_iris
# 设置输出数据的精度为 2 (默认是8)
np.set_printoptions(precision=2)
# 获取花瓣长度 x, 宽度 y
iris = load_iris()
x, y = iris.data[:, 2].reshape(-1, 1), iris.data[:, 3]
# 将数据拆分为训练集和测试集, 指定测试集占比 test_size
# 指定随机种子 random_state(可以任意值但必须确定), 锁定拆分行为
x_train, x_test, y_train, y_test = train_test_split(
x, y, test_size=0.25, random_state=5)
# 使用训练集训练模型
lr = LinearRegression()
lr.fit(x_train, y_train)
# 求得模型参数
print('权重 w:', lr.coef_, '截距 b:', lr.intercept_)
# 调用模型进行预测
y_hat = lr.predict(x_test)
# 结果可视化
import matplotlib.pyplot as plt
plt.rcParams['font.family'] = 'SimHei'
plt.rcParams['axes.unicode_minus'] = False
plt.rcParams['font.size'] = 10
plt.scatter(x_train, y_train, c='orange', label='训练集')
plt.scatter(x_test, y_test, c='g', marker='D', label='测试集')
plt.plot(x, lr.predict(x), 'r-')
plt.legend()
plt.xlabel('花瓣长度')
plt.ylabel('花瓣宽度')
plt.show()
权重 w: [0.42] 截距 b: -0.370615595909495
# 模型评估
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
print('MSE:', mean_squared_error(y_test, y_hat))
print('RMSE:', np.sqrt(mean_squared_error(y_test, y_hat)))
print('MAE:', mean_absolute_error(y_test, y_hat))
print('训练集R²:', r2_score(y_train, lr.predict(x_train))) # 可换成 lr.score(x_train, y_train)
print('测试集R²:', r2_score(y_test, y_hat)) # 可换成 lr.score(x_test, y_test)
MSE: 0.047866747643216113
RMSE: 0.21878470614559903
MAE: 0.1543808898175286
训练集R²: 0.9317841638431329
测试集R²: 0.9119955391492289
列二, 多元线性回归模型: 波士顿房价预测
import numpy as np
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
from sklearn.datasets import load_boston
import pandas as pd
boston = load_boston()
x, y = boston.data, boston.target
df = pd.DataFrame(np.concatenate([x, y.reshape(-1, 1)], axis=1),
columns=boston.feature_names.tolist() + ['MEDV'])
# 部分数据
df.head(3)
CRIM ZN INDUS CHAS NOX RM AGE DIS RAD TAX PTRATIO B LSTAT MEDV
0 0.00632 18.0 2.31 0.0 0.538 6.575 65.2 4.0900 1.0 296.0 15.3 396.90 4.98 24.0
1 0.02731 0.0 7.07 0.0 0.469 6.421 78.9 4.9671 2.0 242.0 17.8 396.90 9.14 21.6
2 0.02729 0.0 7.07 0.0 0.469 7.185 61.1 4.9671 2.0 242.0 17.8 392.83 4.03 34.7
x_train, x_test, y_train, y_test = train_test_split(
x, y, test_size=0.25, random_state=5)
lr = LinearRegression()
lr.fit(x_train, y_train)
print('权重:', lr.coef_)
print('截距:', lr.intercept_)
y_hat = lr.predict(x_test)
print('训练集R²:', lr.score(x_train, y_train))
print('测试集R²:', lr.score(x_test, y_test))
# 假如获取了一间房屋的数据, 预测其房价
room_data = np.array([0.00732, 17.0, 1.31, 1.0, 0.638, 7.575, 62.2, 5.0900,
1.0, 296.0, 15.3, 396.90, 4.98]).reshape(1, -1)
y_price = lr.predict(room_data)
print('房屋价格:', y_price)
权重: [-1.53e-01 4.79e-02 -8.60e-03 2.58e+00 -1.46e+01 3.96e+00 -7.92e-03
-1.46e+00 3.45e-01 -1.25e-02 -9.19e-01 1.32e-02 -5.17e-01]
截距: 32.214120389743606
训练集R²: 0.7468034208269784
测试集R²: 0.7059096071098042
房屋价格: [33.62]
多元线性回归在空间中, 可表示为一个超平面去拟合空间中的数据点
逻辑回归和线性回归有类似之处, 都是利用线性加权计算的模型, 但逻辑回归是分类算法, 例如对是否患癌症进行预测, 因变量就是 是 和 否 , 两个类别, 自变量可以是年龄, 性别, 饮食, 作息, 病菌感染等, 自变量既可以是数值变量, 也可以是类别变量
和线性回归类似, 设自变量为 x, 每个自变量的权重为 w, 令:
$$\begin{array}{l} z=w_{1} x_{1}+w_{2} x_{2}+\cdots+w_{n} x_{n}+b \ =\sum_{j=1}^{n} w_{j} x_{j}+b \ =\sum_{j=0}^{n} w_{j} x_{j} \ =\vec{w}^{T} \cdot \vec{x} \end{array}$$z 是一个连续值, 取值范围(-∞, +∞), 为了实现分类, 一般设置阈值 z = 0, 当 z > 0 时, 将样本判定为一个类别(正例), 该类别设为 1, 当 z ≤ 0 时, 判定为另一个类别(负例), 该类别设为 0, 再设因变量为 y, 从而逻辑回归方程可表示为:
$y=1, z>0$ $y=0, z \leq 0$上述方程虽然实现了分类, 但提供的信息有限, 因此引入 sigmoid函数 (也叫 Logistic函数), 将 z 映射到 (0, 1) 区间,可以实现二分类的同时, 还能体现将样本分为某个类的可能性, 这个可能性设为 p:
$$p=\operatorname{sigmoid}(z)=\frac{1}{1+e^{-z}}$$sigmoid 函数图像如下:
于是, 逻辑回归方程又可表示为:
$y=1, p>0.5$ $y=0, 1-p \geq 0.5$从而可见, 通过比较 p 和 1-p 哪个更大(z 的阈值不取 0 时做出调整即可), 预测结果就是对应的一类
通过上述推导过程可知, 要得到逻辑回归模型, 最终就是要求得参数 $\vec{w}^{T}$, 于是将 p 和 1-p 统一, 构造一个损失函数来求 $\vec{w}^{T}$:
$$p(y=1 | x ; w)=s(z)$$ $$p(y=0 | x ; w)=1-s(z)$$合并:
$$p(y | x ; w)=s(z)^{y}(1-s(z))^{1-y}$$上式表示一个样本的概率, 我们要求解能够使所有样本联合概率密度最大的 $\vec{w}^{T}$ 值, 根据极大似然估计, 所有样本的联合概率密度函数(似然函数)为:
$$\begin{array}{l} L(w)=\prod_{i=1}^{m} p\left(y^{(i)} | x^{(i)} ; w\right) \ =\prod_{i=1}^{m} s\left(z^{(i)}\right)^{y^{(i)}}\left(1-s\left(z^{(i)}\right)\right)^{1-y^{(i)}} \end{array}$$取对数, 让累积乘积变累积求和:
$$\begin{array}{l} \ln L(w)=\ln \left(\prod_{i=1}^{m} s\left(z^{(i)}\right)^{y^{(i)}}\left(1-s\left(z^{(i)}\right)^{1-y^{(i)}}\right)\right) \ =\sum_{i=1}^{m}\left(y^{(i)} \ln s\left(z^{(i)}\right)+\left(1-y^{(i)}\right) \ln \left(1-s\left(z^{(i)}\right)\right)\right) \end{array}$$要求上式最大值, 取反变成求最小值, 就作为逻辑回归的损失函数(交叉熵损失函数):
$$J(w)=-\sum_{i=1}^{m}\left(y^{(i)} \ln s\left(z^{(i)}\right)+\left(1-y^{(i)}\right) \ln \left(1-s\left(z^{(i)}\right)\right)\right)$$利用梯度下降法最终求得 $\vec{w}^{T}$ (省略)
例, 对鸢尾花实现二分类并分析:
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.datasets import load_iris
import warnings
warnings.filterwarnings('ignore')
iris = load_iris()
x, y = iris.data, iris.target
# 鸢尾花数据集有 3 个类别, 4 个特性, 取两个类别, 两个特性
x = x[y!=0, 2:]
y = y[y!=0]
# 拆分训练集与测试集
x_train, x_test, y_train, y_test = train_test_split(x, y,
test_size=0.25, random_state=2)
# 训练分类模型
lr = LogisticRegression()
lr.fit(x_train, y_train)
# 测试
y_hat = lr.predict(x_test)
print('权重:', lr.coef_)
print('偏置:', lr.intercept_)
print('真实值:', y_test)
print('预测值:', y_hat)
权重: [[2.54536368 2.15257324]]
偏置: [-16.08741502]
真实值: [2 1 2 1 1 1 1 1 1 1 2 2 2 1 1 1 1 1 2 2 1 1 2 1 2]
预测值: [2 1 1 1 1 1 1 2 1 1 2 2 2 1 1 1 1 1 2 2 1 1 2 1 2]
# 样本的真实类别可视化
import matplotlib.pyplot as plt
plt.rcParams['font.family'] = 'SimHei'
# 取出两种鸢尾花的特征
c1 = x[y==1]
c2 = x[y==2]
# 绘制样本分布
plt.scatter(x=c1[:, 0], y=c1[:, 1], c='g', label='类别1')
plt.scatter(x=c2[:, 0], y=c2[:, 1], c='r', label='类别2')
plt.xlabel('花瓣长度')
plt.ylabel('花瓣宽度')
plt.title('鸢尾花样本分布')
plt.legend()
plt.show()
# 将预测类别和真实类别可视化对比
plt.figure(figsize=(6, 2.2))
plt.plot(y_test, marker='o', ls='', ms=10, c='r', label='真实类别')
plt.plot(y_hat, marker='x', ls='', ms=10, c='g', label='预测类别')
plt.xlabel('样本序号')
plt.ylabel('类别')
plt.title('预测结果')
plt.legend()
plt.show()
# 因预测样本所属类别时, 通过比较概率得到结果,
# 我们可将结果对应的概率可视化
import numpy as np
# 获取预测的概率值
probability = lr.predict_proba(x_test)
print('概率:', probability[:5], sep='\n')
index = np.arange(len(x_test))
pro_0 = probability[:, 0]
pro_1 = probability[:, 1]
# 设置预测结果标签, 对和错
tick_label = np.where(y_test==y_hat, '对', '错')
# 绘制堆叠图
plt.figure(figsize=(8, 2))
plt.bar(index, height=pro_0, color='g', label='类别1的概率')
plt.bar(index, height=pro_1, color='r', bottom=pro_0,
label='类别2的概率', tick_label=tick_label)
plt.xlabel('预测结果')
plt.ylabel('各类别的概率')
plt.title('分类概率')
plt.legend()
plt.show()
概率:
[[0.46933862 0.53066138]
[0.98282882 0.01717118]
[0.72589695 0.27410305]
[0.91245661 0.08754339]
[0.80288412 0.19711588]]
# 绘制决策边界
# 决策边界: 不同类别的分界线
from matplotlib.colors import ListedColormap
# 定义绘制函数
def plot_decision_boundary(model, x, y):
color = ['r', 'g', 'b']
marker = ['o', 'v', 'x']
class_label = np.unique(y)
cmap = ListedColormap(color[:len(class_label)])
x1_min, x2_min = np.min(x, axis=0)
x1_max, x2_max = np.max(x, axis=0)
x1 = np.arange(x1_min - 1, x1_max + 1, 0.02)
x2 = np.arange(x2_min - 1, x2_max + 1, 0.02)
x1, x2 = np.meshgrid(x1, x2)
z = model.predict(np.array([x1.ravel(), x2.ravel()]).T).reshape(x1.shape)
plt.contourf(x1, x2, z, cmap=cmap, alpha=0.5)
for i, class_ in enumerate(class_label):
plt.scatter(x=x[y==class_, 0], y=x[y==class_, 1],
c=cmap.colors[i], label=class_, marker=marker[i])
plt.legend()
plt.show()
# 绘制模型在训练集上的决策边界
plot_decision_boundary(lr, x_train, y_train)
拓展 :
逻辑回归实现多分类
iris = load_iris()
x, y = iris.data, iris.target
x = x[:, 2:]
x_train, x_test, y_train, y_test = train_test_split(x, y,
test_size=0.25, random_state=2)
lr = LogisticRegression()
lr.fit(x_train, y_train)
# 测试分类
y_hat = lr.predict(x_test)
# 可视化结果
plt.rcParams['axes.unicode_minus']=False
plot_decision_boundary(lr, x_test, y_test)
在完成模型训练之后,需要对模型的效果进行评估,根据评估结果继续调整模型的参数, 特征或者算法,以达到满意的结果
将 真正例(TP), 假正例(FP), 真负例(TN), 假负例(FN) 统计于一个方阵中, 观察比较, 评价模型好坏, 矩阵如下:
混淆矩阵统计数量, 评价不直观也有限, 基于混淆矩阵又延伸出 正确率, 精准率, 召回率, F1(调和平均值), ROC曲线和AUC等
正确率:
$$\text { 正确率 }=\frac{T P+T N}{T P+T N+F P+F N}$$正确率, 表示总体(包括正负)预测正确的比率, 在模型对正例和负例的预测准确度差异较大时, 难以评价模型的好坏, 例如正例较多, 负例较少, 正例全部预测对了, 负例只预测对几个, 正确率却可能较高
精准率:
$$\text { 精准率 }=\frac{T P}{T P+F P}$$精准率, 表示所有预测为正例的结果中 预测正确的正例 的占比, 精准率越高, 说明正例预测正确概率越高, 因此精准率更关注”一击必中”, 比如通过预测找出上涨的概率很高的一支股票
召回率:
$$\text { 召回率 }=\frac{T P}{T P+F N}$$召回率, 表示所有真实的正例中, 预测正确的正例 的占比, 召回率越高, 说明正例被”召回”的越多, 因此召回率更关注”宁错一千, 不放一个”, 例如通过预测尽可能将新冠肺炎患者全部隔离观察
调和平均值 F1 :
$$F 1=\frac{2 * \text {精准率} * \text {召回率}}{\text {精准率}+\text {召回率}}$$F1 将综合了精准率和召回率, F1越高, 说明模型预测效果越好, F1 能够直接评估模型的好坏
ROC曲线:
ROC (Receiver Operating Characteristic) 曲线, 用图像来描述分类模型的性能好坏. 图像纵轴为 真 正例率(TPR), 横轴为 假 正例率(FPR):
$$\begin{array}{l} T P R=\text { 召回率 }=\frac{T P}{T P+F N} \ F P R=\frac{F P}{F P+T N} \end{array}$$上述两式通过取分类模型的不同阈值, 从而计算出不同的值, 绘制出曲线, 曲线必过 (0,0) 和 (1, 1) 两个点, TPR 增长得越快, 曲线越往上凸, 模型的分类性能就越好. 如果 ROC 曲线为对角线, 可将模型理解为随机猜测; 如果 ROC 曲线在 0 点 真 正例率就达到了 1, 此时模型最完美
AUC:
AUC (Area Under the Curve), 是 ROC 曲线下面的面积, 因为有时通过 ROC 曲线看不出哪个分类模型性能好, 而 AUC 比较数值就不存在这样的问题
以鸢尾花数据集做如下练习:
import numpy as np
from sklearn.datasets import load_iris
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import confusion_matrix
import matplotlib.pyplot as plt
import warnings
plt.rcParams["font.family"] = "SimHei"
plt.rcParams["axes.unicode_minus"] = False
plt.rcParams["font.size"] = 12
warnings.filterwarnings("ignore")
iris = load_iris()
x, y = iris.data, iris.target
x = x[y!=0, 2:]
y = y[y!=0]
x_train, x_test, y_train, y_test = train_test_split(x, y,
test_size=0.25, random_state=2)
lr = LogisticRegression()
lr.fit(x_train, y_train)
y_hat = lr.predict(x_test)
# 传入真实值与预测值, 创建混淆矩阵
matrix = confusion_matrix(y_true=y_test, y_pred=y_hat)
print(matrix)
y_hat[y_hat==1].sum()
[[15 1]
[ 1 8]]
16
# 将混淆矩阵可视化
mat = plt.matshow(matrix, cmap=plt.cm.Blues, alpha=0.5)
label = ["负例", "正例"]
# 获取当前的绘图对象
ax = plt.gca()
# 设置属性, 设类别 1 为负例
ax.set(
xticks=np.arange(matrix.shape[1]),
yticks=np.arange(matrix.shape[0]),
xticklabels=label,
yticklabels=label,
title="混淆矩阵可视化\n",
ylabel="真实值",
xlabel="预测值")
# 设置统计值的位置
for i in range(matrix.shape[0]):
for j in range(matrix.shape[1]):
plt.text(x=j, y=i, s=matrix[i, j], va="center", ha="center")
plt.show()
# 计算各个评估指标
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score
print("正确率:", accuracy_score(y_test, y_hat))
# 默认以 1 为正例, 我们将 2 设为正例
print("精准率:", precision_score(y_test, y_hat, pos_label=2))
print("召回率:", recall_score(y_test, y_hat, pos_label=2))
print("F1:", f1_score(y_test, y_hat, pos_label=2))
# 也可以用逻辑回归模型对象的score方法计算正确率
print("score方法计算正确率:", lr.score(x_test, y_test))
正确率: 0.92
精准率: 0.8888888888888888
召回率: 0.8888888888888888
F1: 0.8888888888888888
score方法计算正确率: 0.92
# 还可以用 classification_report 方法直接计算各个指标
from sklearn.metrics import classification_report
print(classification_report(y_true=y_test, y_pred=y_hat))
precision recall f1-score support
1 0.94 0.94 0.94 16
2 0.89 0.89 0.89 9
accuracy 0.92 25
macro avg 0.91 0.91 0.91 25
weighted avg 0.92 0.92 0.92 25
# 绘制 ROC曲线 和计算 AUC
from sklearn.metrics import roc_curve, auc, roc_auc_score
iris = load_iris()
x, y = iris.data, iris.target
x = x[y!=0, 2:]
y = y[y!=0]
x_train, x_test, y_train, y_test = train_test_split(x, y,
test_size=0.25, random_state=2)
# 设置模型参数(有默认值可以不设), 并进行训练
# 不同的参数训练结果不一样, 需要注意参数之间关系
lr = LogisticRegression(multi_class="ovr", solver="liblinear")
# lr = LogisticRegression(multi_class="multinomial")
lr.fit(x_train, y_train)
# 获取样本的概率
probo = lr.predict_proba(x_test)
print('类别 2 的概率:', probo[:, 1][:5])
# 将概率值传入 roc_curve 方法, 从概率中选择若干个值作为阈值
# 同时根据阈值判定正负例, 返回 fpr, tpr 和 阈值 thresholds
fpr, tpr, thresholds = roc_curve(y_true=y_test,
y_score=probo[:, 1], pos_label=2)
# 阈值中的第一个值是第二个值 +1 得到, 为了让让曲线过 0 点
print('阈值:', thresholds)
# 计算 AUC
print('用auc计算:', auc(fpr, tpr))
print('用roc_auc_score计算:', roc_auc_score(y_true=y_test,
y_score=probo[:, 1]))
类别 2 的概率: [0.4663913 0.28570842 0.60050037 0.3758227 0.48450719]
阈值: [1.69092453 0.69092453 0.60050037 0.54308778 0.50384451 0.49358343
0.48450719 0.47242245 0.4663913 0.42043757 0.39590375 0.39413886
0.3843811 0.24698327]
用auc计算: 0.8819444444444444
用roc_auc_score计算: 0.8819444444444444
# 绘制 ROC 曲线
plt.figure(figsize=(6, 2))
plt.plot(fpr, tpr, marker="o", label="ROC曲线")
plt.plot([0,1], [0,1], lw=2, ls="--", label="随机猜测")
plt.plot([0, 0, 1], [0, 1, 1], lw=2, ls="-.", label="完美预测")
plt.xlim(-0.01, 1.02)
plt.ylim(-0.01, 1.02)
plt.xticks(np.arange(0, 1.1, 0.2))
plt.yticks(np.arange(0, 1.1, 0.2))
plt.xlabel("FPR")
plt.ylabel("TPR")
plt.grid()
plt.title(f"ROC曲线, AUC值为:{auc(fpr, tpr):.2f}")
plt.legend()
plt.show()
KNN (K-Nearest Neighbor), 即 K 近邻算法, K 个最近的邻居. 当需要预测一个未知样本的时候, 就由与该样本最近的 K 个邻居来决定
KNN 既可以用于分类, 也可用于回归. 用来分类时, 使用 K 个邻居中, 类别数量最多(或加权最多)者, 作为预测结果; 当用来回归分析时, 使用 K 个邻居的均值(或加权均值), 作为预测结果
KNN 算法的原理是: 样本映射到多维空间时, 相似度较高的样本, 距离也会较接近, “近朱者赤近墨者黑”
KNN 算法的 K 值是一个模型训练前就要人为指定的参数 超参数 , 不同于模型内部通过训练数据计算得到的参数. KNN 的超参数, 需要通常通过 交叉验证 的方式来选择最合适的参数组合
K 值的选择非常重要, K 值较小时, 模型预测依赖附近的邻居, 敏感性高, 稳定性低, 容易导致过拟合; 反之, K 值较大, 敏感性低, 稳定性高, 容易欠拟合
K 值在数据量小时, 可以通过遍历所有样本(穷举)的方式找出最近的 K 个邻居, 当数据量庞大时, 穷举耗费大量时间, 此时可以采用 KD树 来找 K 个邻居
KNN 的网格搜索交叉验证: 取不同的 K, 选择不同的距离或权重计算方式等, 将数据分为多个组, 一个组作为测试集, 其他部分作为训练集, 不断循环训练和测试, 对模型进行循环验证, 找出最佳参数组合
闵可夫斯基距离:
设 n 维空间中两个点位 X 和 Y:
$X=\left(x_{1}, x_{2}, \ldots \ldots, x_{n}\right)$ $Y=\left(y_{1}, y_{2}, \ldots \ldots, y_{n}\right)$则阁可夫斯基距离为:
$D(X, Y)=\left(\sum_{i=1}^{n}\left|x_{i}-y_{i}\right|^{p}\right)^{1 / p}$当 p 为 1 时, 又称 曼哈顿距离 ; 当 p 为 2 时, 称 欧几里得距离
统一权重: K 个邻居权重相同, 不管近远都是 1/K
距离加权权重: K 个邻居的权重, 与他们各自和待测样本的距离成反比, 同时要保证权重之和为 1. 例如 3 个邻居 a, b, c 距离待测样本的距离分别为 a, b 和 c, 则 a 的权重为:
$$\frac{\frac{1}{a}}{\frac{1}{a}+\frac{1}{b}+\frac{1}{c}}=\frac{b c}{b c+a c+a b}$$b 和 c 同理
样本中的特征通常非常多,由于各特征的性质不同,通常具有不同的量纲(数量级). 当各特征间的量纲相差很大时,如果直接用原始特征值进行分析,就会突出数值较高的特征在综合分析中的作用,相对削弱数值较低特征的作用, 因此需要通过数据标准化, 将量纲统一, 才能客观地描述各个特征对模型的影响程度
线性回归和逻辑回归, 都是通过每个特征与其权重的乘积相加来进行计算, 不进行数据标准化(不考虑正则化), 对每个特征的权重影响较大, 但对结果不会造成影响, 而 KNN 是基于距离计算的, 如果特征的量纲不同, 量纲较大的特征会占据主导地位, 导致忽略量纲较小的特征, 从而对模型性能造成较大影响
a, 确定超参数
确定 K
确定距离度量方式
确定权重计算方式
其他超参数
b, 从训练集中选择距离待测样本最近的 K 个样本
c, 根据 K 个样本对待测样本进行预测, 如果遇到多个样本距离相同的情况, 默认选取训练集中靠前的
流水线可以将每个评估器视为一个步骤, 然后将多个步骤作为整体依次执行. 例如数据处理工作较多时, 可能涉及更多步骤, 例如多项式扩展, One-Hot 编码, 特征选择, 数据标准化, 交叉验证等, 分别执行过于繁琐, 我们可以将数据处理与模型训练各个步骤作为一个整体来执行
流水线具有最后一个评估器的所有方法:
a, 当流水线对象调用 fit 方法时, 会从第一个评估器依次调用 fit_transform 方法, 然后到最后一个评估器调用 fit 方法
b, 当流水线对象调用 其他 方法时, 会从第一个评估器依次调用 transform 方法, 然后到最后一个评估器调用 其他 方法
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import classification_report
from sklearn.datasets import load_iris
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
import matplotlib as mpl
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings("ignore")
import numpy as np
mpl.rcParams["font.family"] = "SimHei"
mpl.rcParams["axes.unicode_minus"] = False
iris = load_iris()
X = iris.data[:, :2]
y = iris.target
X_train, X_test, y_train, y_test = train_test_split(X, y,
test_size=0.25, random_state=0)
# 数据标准化: StandardScaler 均值标准差标准化, MinMaxScaler 最大最小值标准化
ss = StandardScaler()
X_train = ss.fit_transform(X_train)
X_test = ss.transform(X_test)
# 逻辑回归训练
lr = LogisticRegression()
lr.fit(X_train,y_train)
# KNN 训练
# n_neighbors: 邻居的数量
# weights:权重计算方式, 可选值为 uniform 统一权重, 与 distance 加权权重
knn = KNeighborsClassifier(n_neighbors=3, weights="uniform")
knn.fit(X_train, y_train)
# 比较 AUC
from sklearn.metrics import roc_curve, auc,roc_auc_score
lr_fpr, lr_tpr, lr_thresholds = roc_curve(y_test,
lr.predict_proba(X_test)[:,1], pos_label=1)
lr_auc = auc(lr_fpr, lr_tpr)
print('Logistic 算法: AUC = %.3f' % lr_auc)
knn_fpr, knn_tpr, knn_thresholds = roc_curve(y_test,
knn.predict_proba(X_test)[:,1], pos_label=1)
knn_auc = auc(knn_fpr, knn_tpr)
print('KNN 算法: AUC = %.3f' % knn_auc)
Logistic 算法: AUC = 0.835
KNN 算法: AUC = 0.794
# 将 KNN 算法参数进行调优再来比较
from sklearn.model_selection import GridSearchCV
# K 值取 1~10, 并定义需要的参数组合
knn = KNeighborsClassifier()
grid = {'n_neighbors': range(1,11,1), 'weights': ['uniform','distance']}
# 网格搜索交叉验证
# param_grid:需要检验的超参数组合
# scoring:模型评估标准, accuracy 正确率
# n_jobs:并发数量
# cv:交叉验证折数
# verbose:输出冗余信息
gs = GridSearchCV(estimator=knn, param_grid=grid, scoring='accuracy',
n_jobs=-1, cv=5, verbose=0)
gs.fit(X_train, y_train)
gs_fpr, gs_tpr, gs_thresholds = roc_curve(y_test,
gs.predict_proba(X_test)[:,1], pos_label=1)
gs_auc = auc(gs_fpr, gs_tpr)
print('KNN 算法: AUC = %.3f' % gs_auc)
KNN 算法: AUC = 0.855
from sklearn.datasets import load_boston
from sklearn.neighbors import KNeighborsRegressor
from sklearn.linear_model import LinearRegression
X, y = load_boston(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y,
test_size=0.25, random_state=0)
knn = KNeighborsRegressor(n_neighbors=3, weights="distance")
knn.fit(X_train, y_train)
print("KNN 算法 R²:", knn.score(X_test, y_test))
lr = LinearRegression()
lr.fit(X_train, y_train)
print("线性回归算法 R²:", lr.score(X_test, y_test))
KNN 算法 R²: 0.5158073940789912
线性回归算法 R²: 0.6354638433202129
# 对 KNN 数据标准化和参数调优之后再来比较
knn = KNeighborsRegressor()
grid = {'n_neighbors': range(1,11,1), 'weights': ['uniform','distance']}
gs = GridSearchCV(estimator=knn, param_grid=grid, scoring='r2',
n_jobs=-1, cv=5, verbose=0)
# 利用流水线处理
from sklearn.pipeline import Pipeline
# 定义流水线的步骤: 类型为一个列表, 列表中的每个元素是元组类型
# 格式为:[(步骤名1,评估器1), (步骤名2, 评估器2), ……, (步骤名n, 评估器n)
knn_steps = [("scaler", StandardScaler()), ("knn", gs)]
knn_p = Pipeline(knn_steps)
# 可以设置流水线的参数. 所有可用的参数可以通过 get_params 获取
# 设置格式如下: (步骤名__参数)
# p.set_params(knn__n_neighbors=3, knn__weights="uniform")
knn_p.fit(X_train, y_train)
print("KNN 算法 R²:", knn_p.score(X_test, y_test))
# 线性回归数据标准化
lr_steps = [("scaler", StandardScaler()), ("lr", LinearRegression())]
lr_p = Pipeline(lr_steps)
lr_p.fit(X_train, y_train)
print("线性回归算法 R²:", lr_p.score(X_test, y_test))
KNN 算法 R²: 0.6441485149216897
线性回归算法 R²: 0.6354638433202131
样本空间 :
在 随机试验 E 中, 实验的所有可能结果组成的集合, 称为 样本空间 S, 样本空间的每个元素, 即 E 的每个结果, 称 样本点
随机事件 :
进行随机试验时, 满足某种条件的样本点组成的集合, S 的子集, 称作 随机事件 , 只有一个样本点时, 称作 基本事件
概率 :
对于随机事件 A, 概率为:
$P(A)=\frac{A \text { 中基本事件数 }}{S \text { 中基本事件数 }}$条件概率 :
定义事件 A 发生的前提下, 事件 B 发生的概率 P(B | A) 为条件概率:
$$P(B \mid A)=\frac{P(A B)}{P(A)}$$由条件概率的定义可得, 事件 A 和 B 同时发生的概率 P(AB) 满足如下 乘法定理 :
$$P(A B)=P(B \mid A) P(A)$$独立性:
定义 A 和 B 两个事件, 如果满足:
$$P(A B)=P(A) P(B)$$则称事件 A, B 相互独立. 再结合乘法定理, 则有:
$$P(B \mid A) = P(B)$$全概率公式:
设随机试验 E 的样本空间为 S, 若事件 $B_{1}$, $B_{2}$,…, $B_{n}$ 构成一个完备事件组(即它们两两相互独立,事件并集为 S), 且都有正概率,则对任意一个 E 的事件 A,有如下公式成立:
$$P(A)=P\left(A \mid B_{1}\right) P\left(B_{1}\right)+P\left(A \mid B_{2}\right) P\left(B_{2}\right)+\ldots \ldots+P\left(A \mid B_{n}\right) P\left(B_{n}\right)$$此公式即为全概率公式. 特别地,对于任意两随机事件 A 和 B,有如下成立:
$$P(B)=P(B \mid A) P(A)+P(B \mid \bar{A}) P(\bar{A})$$贝叶斯公式:
设随机试验 E 的样本空间为 S, 若事件 $B_{1}$, $B_{2}$,…, $B_{n}$ 构成一个完备事件组(即它们两两相互独立,事件并集为 S), 且都有正概率,则对任意一个 E 的正概率事件 A,有如下公式成立( i 为 1~n 的正整数):
$$P\left(B_{i} \mid A\right)=\frac{P\left(A B_{i}\right)}{P(A)}=\frac{P\left(A \mid B_{i}\right) P\left(B_{i}\right)}{P(A)} \ =\frac{P\left(A \mid B_{i}\right) P\left(B_{i}\right)}{\sum_{j=1}^{n} P\left(A \mid B_{j}\right) P\left(B_{j}\right)}$$贝叶斯公式将求解 P(B | A) 的概率转换成求 P(A | B) 的概率, 在求解某个事件概率非常困难时, 转换一下更方便求解
例: 从以往数据得出, 机器调整良好时生产的产品合格的概率是 98%, 机器故障时合格的概率是 55%, 每天开机时机器调整良好的概率为 95%. 求某日开机生产的第一件产品是合格品时, 机器调整良好的概率?
解: 设事件 A 为产品合格, B 为机器调整良好, 则 $\bar{B}$ 为机器故障
$$P(B \mid A)=\frac{P(A \mid B) P(B)}{P(A \mid B) P(B)+P(A \mid \bar{B}) P(\bar{B})} \ =\frac{0.98 \times 0.95}{0.98 \times 0.95+0.55 \times 0.05} \ =0.97$$先验概率和后验概率:
由以往的数据得出的概率称为 先验概率 , 如上例中的已知概率
得到某些信息后, 在先验概率的基础上进行修正而得到的概率, 称为 后验概率 , 如上例中求解的概率
朴素贝叶斯是基于概率的分类算法, 前提假设各个特征(自变量)之间是相互独立的, 设类别(因变量)为 Y, Y 包含 m 个类别( $y_{1},\ldots, y_{m}$), 特征为 X, X 包含含有 n 个特征 ( $x_{1}, \ldots, x_{n}$), 然后通过计算比较, 在特征 X 确定的前提下, 类别 Y 中每个类别的概率大小, 概率最大者即为预测结果
设 Y 中任意一个类别为 y, 则:
$$P(y \mid X) = P\left(y \mid x_{1}, \ldots, x_{n}\right) \ =\frac{P(y) P\left(x_{1}, \ldots, x_{n} \mid y\right)}{P\left(x_{1}, \ldots, x_{n}\right)} \ =\frac{P(y) P\left(x_{1} \mid y\right) P\left(x_{2} \mid y\right) \ldots P\left(x_{n} \mid y\right)}{P\left(x_{1}, \ldots, x_{n}\right)} \ =\frac{P(y) \prod_{i=1}^{n} P\left(x_{i} \mid y\right)}{P\left(x_{1}, \ldots, x_{n}\right)}$$上式分母为定值, 则:
$$P\left(y \mid X \right) \propto P(y) \prod_{i=1}^{n} P\left(x_{i} \mid y\right)$$所以最终预测类别 $\hat{y}$ 为分子部分值最大对应的类别:
$$\hat{y}=\arg \max_{y} P(y) \prod_{i=1}^{n} P\left(x_{i} \mid y\right)$$不同的朴素贝叶斯算法, 主要是对 $P\left(x_{i} \mid y\right)$ 的分布假设不同, 进而采取不同的参数估计方式. 最终主要就是通过计算 $P\left(x_{i} \mid y\right)$ 的概率来计算结果
例: 预测第 11 条记录, 学生是否上课
序号 | 天气 | 上课距离 | 成绩 | 课程 | 上课情况 |
---|---|---|---|---|---|
1 | 晴 | 远 | 差 | 选修 | 逃课 |
2 | 晴 | 近 | 差 | 必修 | 上课 |
3 | 晴 | 近 | 好 | 必修 | 上课 |
4 | 阴 | 远 | 差 | 选修 | 逃课 |
5 | 阴 | 近 | 好 | 选修 | 上课 |
6 | 阴 | 近 | 好 | 必修 | 上课 |
7 | 雨 | 远 | 差 | 选修 | 逃课 |
8 | 雨 | 近 | 好 | 必修 | 上课 |
9 | 雨 | 近 | 差 | 必修 | 逃课 |
10 | 雨 | 远 | 好 | 选修 | 逃课 |
11 | 阴 | 近 | 差 | 选修 | ? |
12 | 晴 | 远 | 好 | 选修 | ? |
分别计算上课和逃课情况下, 各自的概率:
$$P(y=\text { 上课 }) \prod_{i=1}^{n} P\left(x_{i} \mid y=\text { 上课 }\right) \ =P(y=\text { 上课 }) P\left(x_{1}=\text { 阴 } \mid y=\text { 上课 }\right) P\left(x_{2}=\text { 近 } \mid y=\text { 上课 }\right) \ P\left(x_{3}=\text {差 } \mid y=\text { 上课 }\right) P\left(x_{4}=\text { 选修 } \mid y=\text { 上课 }\right) \ =0.5 \times 0.4 \times 1 \times 0.2 \times 0.2 \ =0.008$$ $$P(y=\text { 逃课 }) \prod_{i=1}^{n} P\left(x_{i} \mid y=\text { 逃课 }\right) \ =P(y=\text { 逃课 }) P\left(x_{1}=\text { 阴 } \mid y=\text { 逃课 }\right) P\left(x_{2}=\text { 近 } \mid y=\text { 逃课 }\right) \ P\left(x_{3}=\text { 差 } \mid y=\text { 逃课 }\right) P\left(x_{4}=\text { 选修 } \mid y=\text { 逃课 }\right) \ =0.5 \times 0.2 \times 0.2 \times 0.8 \times 0.8 \ =0.0128$$可得预测结果为: 逃课
当我们预测上例中, 第 12 条记录所属的类别时, 因为样本不是总体, 会出现上课的前提下, 距离远的概率为 0, 造成计算结果也为 0, 影响了预测结果, 因此需要平滑改进:
$$ P\left(x_{i} \mid y\right)=\frac{\text { 类别 } y \text { 中 } x_{i} \text { 取某个值出现的次数 }+ \alpha}{\text { 类别别 } y \text { 的总数 }+k * \alpha} $$其中, k 为特征 $x_{i}$ 可能的取值数, α (α ≥ 0) 称为平滑系数, 当 α = 1 时, 称拉普拉斯平滑( Laplace smoothing)
即使训练集数据较少, 也能实现不错的预测; 算法训练速度非常快
因此算法假设特征之间是独立的, 可以单独考虑. 如果训练集有 N 个特征, 每个特征需要 M 个样本来训练, 则只需要训练 N*M 的样本数量, 而不是笛卡儿积的形式指数级增加
常用的朴素贝叶斯有: 高斯朴素贝叶斯, 伯努利朴素贝叶斯, 多项式朴素贝叶斯
适用于连续变量, 其假定各个特征 x 在各个类别 y 下服从正态分布:
$$x_{i} \sim N\left(\mu_{y}, \sigma_{y}^{2}\right)$$算法使用概率密度函数来计算 $P\left(x_{i} \mid y\right)$ 的概率:
$$P\left(x_{i} \mid y\right)=\frac{1}{\sqrt{2 \pi \sigma_{y}^{2}}} \exp \left(-\frac{\left(x_{i}-\mu_{y}\right)^{2}}{2 \sigma_{y}^{2}}\right) $$
$\mu_{y}$: 在类别为 y 的样本中, 特征
$x_{i}$ 的均值
$\sigma_{y}$: 在类别为 y 的样本中, 特征
$x_{i}$ 的标件差
import numpy as np
import pandas as pd
from sklearn.naive_bayes import GaussianNB
np.random.seed(0)
x = np.random.randint(0, 10, size=(8, 3))
y = np.array([0, 1, 0, 0, 0, 1, 1, 1])
data = pd.DataFrame(np.concatenate([x, y.reshape(-1, 1)], axis=1),
columns=['x1', 'x2', 'x3', 'y'])
display(data[:3])
gnb = GaussianNB()
gnb.fit(x, y)
print('类别标签:', gnb.classes_)
print('每个类别的先验概率:', gnb.class_prior_)
print('样本数量:', gnb.class_count_)
print('每个类别下特征的均值:', gnb.theta_)
print('每个类别下特征的方差:', gnb.sigma_)
# 测试集
x_test = np.array([[5, 7, 2]])
print('预测结果:', gnb.predict(x_test))
print('预测结果概率:', gnb.predict_proba(x_test))
x1 x2 x3 y
0 5 0 3 0
1 3 7 9 1
2 3 5 2 0
类别标签: [0 1]
每个类别的先验概率: [0.5 0.5]
样本数量: [4. 4.]
每个类别下特征的均值: [[5. 5. 3. ]
[6.5 5.75 7.5 ]]
每个类别下特征的方差: [[3.50000001 9.50000001 3.50000001]
[5.25000001 7.68750001 2.75000001]]
预测结果: [0]
预测结果概率: [[0.99567424 0.00432576]]
设实验 E 只有两个可能的结果, A 与 $\bar{A}$, 则称 E 为伯努利试验
伯努利朴素贝叶斯, 适用于离散变量, 其假设各个特征 x 在各个类别 y 下服从 n 重伯努利分布(二项分布), 因伯努利试验仅有两个结果, 算法会首先对特征值进行二值化处理(假设二值化结果为 1 和 0 )
$P\left(x_{i} \mid y\right)$ 的概率为:
$$P\left(x_{i} \mid y\right)=P\left(x_{i}=1 \mid y\right) x_{i}+\left(1-P\left(x_{i}=1 \mid y\right)\right)\left(1-x_{i}\right)$$在训练集中, 会进行如下评估:
$$ P\left(x_{i}=1 \mid y\right)=\frac{N_{y i}+\alpha}{N_{y}+2 * \alpha} \ P\left(x_{i}=0 \mid y\right)=1-P\left(x_{i}=1 \mid y\right) $$
$N_{y i}$: 第 i 特征中, 属于类别 y, 数值为 1 的样本个数
$N_{y}$: 属于类別 y 的所有样本个数
$\alpha$: 平滑系数
from sklearn.naive_bayes import BernoulliNB
np.random.seed(0)
x = np.random.randint(-5, 5, size=(8, 3))
y = np.array([0, 1, 0, 0, 0, 1, 1, 1])
data = pd.DataFrame(np.concatenate([x, y.reshape(-1, 1)], axis=1),
columns=['x1', 'x2', 'x3', 'y'])
display(data[:3])
bnb = BernoulliNB()
bnb.fit(x, y)
# 统计每个类别下, 特征中二值化后, 每个特征下值 1 出现的次数
print('值 1 出现的次数:', bnb.feature_count_)
# 每个类别的先验概率, 算法得到的该概率值是取对数后的结果,
# 需要取指数还原
print('每个类别的先验概率:', np.exp(bnb.class_log_prior_))
# 每个类别下, 每个特征的概率(也需要取指数还原)
print('每个特征的概率:', np.exp(bnb.feature_log_prob_))
# 测试集
x_test = np.array([[-5, 0, 2]])
print('预测结果:', bnb.predict(x_test))
print('预测结果概率:', bnb.predict_proba(x_test))
x1 x2 x3 y
0 0 -5 -2 0
1 -2 2 4 1
2 -2 0 -3 0
值 1 出现的次数: [[1. 2. 1.]
[3. 3. 3.]]
每个类别的先验概率: [0.5 0.5]
每个特征的概率: [[0.33333333 0.5 0.33333333]
[0.66666667 0.66666667 0.66666667]]
预测结果: [0]
预测结果概率: [[0.6 0.4]]
多项式朴素贝叶斯, 适用于离散变量, 其假设各个特征 x 在各个类别 y 下服从多项式分布(每个特征下的值之和, 就是该特征发生(出现)的次数), 因此每个特征值不能是负数
$P\left(x_{i} \mid y\right)$ 的概率为:
$$P\left(x_{i} \mid y\right)=\frac{N_{y i}+\alpha}{N_{y}+\alpha n} $$
$N_{y i}$: 特征 i 在类别 y 的样本中发生(出现)的次数
$N_{y}$: 类别 y 的样本中, 所有特征发生(出现)的次数
$n$: 特征数量
$\alpha$: 平滑系数
from sklearn.naive_bayes import MultinomialNB
np.random.seed(0)
x = np.random.randint(1, 5, size=(8, 3))
y = np.array([0, 1, 0, 0, 0, 1, 1, 1])
data = pd.DataFrame(np.concatenate([x, y.reshape(-1, 1)], axis=1),
columns=['x1', 'x2', 'x3', 'y'])
display(data[:3])
mnb = MultinomialNB()
mnb.fit(x, y)
# 每个类别的样本数量
print('每个类别的样本数:', mnb.class_count_)
# 每个特征在每个类别下发生(出现)的次数
print('每个特征发生(出现)次数:', mnb.feature_count_)
# 每个类别下, 每个特征的概率(需要取指数还原)
print('每个类别下特征的概率:', np.exp(mnb.feature_log_prob_))
# 测试集
x_test = np.array([[1, 1, 4]])
print('预测结果:', mnb.predict(x_test))
print('预测结果概率:', mnb.predict_proba(x_test))
x1 x2 x3 y
0 1 4 2 0
1 1 4 4 1
2 4 4 2 0
每个类别的样本数: [4. 4.]
每个特征发生(出现)次数: [[10. 14. 10.]
[ 9. 11. 11.]]
每个类别下特征的概率: [[0.2972973 0.40540541 0.2972973 ]
[0.29411765 0.35294118 0.35294118]]
预测结果: [1]
预测结果概率: [[0.36890061 0.63109939]]
利用鸢尾花数据集比较上述 3 个贝叶斯算法:
对不同的数据集, 根据其分布情况选择适合的算法, 能得到更好的结果
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
x, y = load_iris(return_X_y=True)
x_train, x_test, y_train, y_test = train_test_split(x, y,
test_size=0.25, random_state=0)
models = [('高斯朴素贝叶斯分值:', GaussianNB()),
('伯努利朴素贝叶斯分值:', BernoulliNB()),
('多项式朴素贝叶斯分值:', MultinomialNB())]
for name, m in models:
m.fit(x_train, y_train)
print(name, m.score(x_test, y_test))
高斯朴素贝叶斯分值: 1.0
伯努利朴素贝叶斯分值: 0.23684210526315788
多项式朴素贝叶斯分值: 0.5789473684210527
决策树: 通过数据特征的差别, 用已知数据训练将不同数据划分到不同分支(子树)中, 层层划分, 最终得到一个树型结构, 用来对未知数据进行预测, 实现分类或回归
例如, 有如下数据集, 预测第 11 条数据能否偿还债务:
序号 | 有无房产 | 婚姻状况 | 年收入 | 能否偿还债务 |
---|---|---|---|---|
1 | 是 | 单身 | 125 | 能 |
2 | 否 | 已婚 | 100 | 能 |
3 | 否 | 单身 | 100 | 能 |
4 | 是 | 已婚 | 110 | 能 |
5 | 是 | 离婚 | 60 | 能 |
6 | 否 | 离婚 | 95 | 不能 |
7 | 否 | 单身 | 85 | 不能 |
8 | 否 | 已婚 | 75 | 能 |
9 | 否 | 单身 | 90 | 不能 |
10 | 是 | 离婚 | 220 | 能 |
11 | 否 | 已婚 | 94 | ? |
我们可以将已知样本作如下划分(训练), 构建一颗决策树, 然后将第 11 条数据代入(测试), 落在哪一个叶子中, 它就是对应叶子的类别: 预测结果是 能
上例中, 层级已经不可再分, 但如果只划分到婚姻状况就不再划分如何实现预测?
决策树实现预测:
对于分类树, 叶子节点中, 哪个类别样本数量最多, 就将其作为未知样本的类别
对于回归树, 使用叶子节点中, 所有样本的均值, 作为未知样本的结果
对于上例, 如果只划分到婚姻状况, 那对于婚姻状况这个叶子中, 不能偿还的最多, 预测结果就是 不能
对上例出现的情况, 我们会有如下问题:
我们为什么以年收入开始划分, 依据是什么? 划分顺序怎么定?
年收入为什么选 97.5 为划分阈值?
要划分多少层才好, 是否越多越好?
等等…
下面一步步来作讨论:
信息熵 : 用来描述信源的不确定度, 不确定性越大, 信息熵越大. 例如, 明天海南要下雪, 不确定性非常小, 信息熵很小, 明天海南要下雨, 不确定性大, 信息熵就大
设随机变量 X 具有 m 个特征值, 各个值出现的概率为 $p_{1}$, …, $p_{m}$, 且
$$p_{1}+p_{2}+\cdots+p_{m} = 1$$则变量 X 的信息熵(信息期望值)为:
$$H(X)=-p_{1} \log_{2} p_{1} -p_{2} \log_{2} p_{2}-\cdots -p_{m} \log_{2} p_{m}$$ $$ =-\sum_{i=1}^{m}p_{i}\log_{2}p_{i}$$假设明天下雨的概率从 0.01 ~ 0.99 递增, 那么不下雨的概率就从 0.99 ~ 0.01 递减, 看看概率分布和信息熵的关系:
import numpy as np
import matplotlib.pyplot as plt
plt.rcParams['font.family'] = 'SimHei'
plt.rcParams['font.size'] = 14
# 下雨的概率
p = np.linspace(0.01, 0.99, 100)
# 信息熵
h = -p * np.log2(p) - (1-p) * np.log2(1-p)
# 绘制关系图
plt.plot(p, h)
plt.xlabel('概率分布')
plt.ylabel('信息熵')
plt.title('概率分布和信息熵关系图')
plt.show()
可见, 概率分布越均衡, 不确定性越大, 信息熵越大, 在所有概率都相等(p下雨=p不下雨)时, 信息熵最大
如果把概率分布转换到决策树的数据集上, 信息熵体现的就是数据的 不纯度 , 即样本类别的均衡程度. 因为数据集是未分类的, 要把它分类, 样本类别越均衡, 各个类别的占比(概率)分布越均衡, 不纯度越高, 信息熵越大
信息增益的定义如下:
$$I G\left(D_{p}, f\right)=I\left(D_{p}\right)-\sum_{j=1}^{n} \frac{N_{j}}{N_{p}} I\left(D_{j}\right) $$
$f$: 划分的特征
$D_{p}$: 父节点, 即使用特征 f 分割之前的节点
$I G\left(D_{p}, f\right)$: 父节点
$D_{p}$ 使用特征 f 划分下, 获得的信息增益
$I\left(D_{p}\right)$:父节点不纯度, 信息熵是度量标准之一
$D_{j}$: 父节点
$D_{p}$ 经过分割之后, 会产生 n 个子节点,
$D_{j}$ 为第 j 个子节点
$I\left(D_{j}\right)$:子节点不纯度
$N_{p}$: 父节点
$D_{p}$ 包含样本的数量
$N_{j}$: 第 j 个子节点
$D_{j}$ 包含样本的数量
如果是二叉树, 即父节点最多分为左右两个子节点, 此时, 信息增益为:
$$I G\left(D_{p}, f\right)=I\left(D_{p}\right)-\frac{N_{l e f t}}{N_{p}} I\left(D_{l e f t}\right)-\frac{N_{r i g h t}}{N_{p}} I\left(D_{r i g h t}\right)$$可见, 信息增益就是父节点的不纯度减去所有子节点的(加权)不纯度
父节点的不纯度是不变的, 在选择特征进行类别划分时, 应该让子节点的不纯度尽可能低, 这样训练可以更快完成, 信息增益也最大. 这正是训练决策树时, 选择特征顺序的依据
以开头的例子为例, 不纯度使用信息熵度量, 则根节点的信息熵为:
$$I\left(D_{p}\right)=-0.7 * \log _{2} 0.7-0.3 * \log _{2} 0.3=0.88$$如果以”有无房产”划分, 则可计算得子节点信息熵:
$$\begin{array}{l} I\left(D_{\text {有房产 }}\right)=0 \ I\left(D_{\text {无房产 }}\right)=1 \end{array}$$从而可得根节点信息增益为:
$$I G(\text { 有无房产 })=0.88-0.4 * 0-0.6 * 1=0.28 $$同理,
$$I G(\text { 婚姻状况 })=0.205 $$而对于年收入, 将年收入排序后, 取不同类别的分界点年收入(75 与 85, 95 与 100)的均值进行划分, 比较哪一个信息增益大:
$$\begin{array}{l} I\left(D_{\text {年收入 }< 80}\right)=0 \ I\left(D_{\text { 年收入 } >=80}\right)=0.954 \ I G(\text { 年收入 }=80)=0.88-0.2 * 0-0.8 * 0.954=0.117 \end{array}$$同理,
$$I G(\text { 年收入 }=97.5)=0.395$$可见, 以 年收入=97.5 划分时, 信息增益最大, 故首先选它进行划分
根节点划分结束, 第二层的父节点以同样的方式计算之后再次划分, 一直到划分停止
如果不设置条件, 是不是划分深度越大越好呢?
下面以鸢尾花数据集为例, 看看划分深度对模型效果的影响:
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
x, y = load_iris(return_X_y=True)
x = x[:, :2]
x_train, x_test, y_train, y_test = train_test_split(
x, y, test_size=0.25, random_state=0)
'''
参数介绍:
criterion: 不纯度度量标准, 默认基尼系数 gini, 信息熵为 entropy
splitter: 选择划分节点的方式, 默认最好 best, 随机 random
max_depth: 划分深度, 默认 None 不设限
min_samples_split: 划分节点的最小样本数, 默认 2
min_samples_leaf: 划分节点后, 叶子节点的最少样本数, 默认 1
max_features: 划分节点时, 考虑的最大特征数, 默认 None 考虑所有, 设置数量后会随机选择
random_state: 随机种子, 控制模型的随机行为
'''
tree = DecisionTreeClassifier()
# 定义列表, 用来储存不同深度下, 模型的分值
train_score = []
test_score = []
# 设置深度 1~12 开始训练
for depth in range(1, 13):
tree = DecisionTreeClassifier(criterion='entropy',
max_depth=depth, random_state=0)
tree.fit(x_train, y_train)
train_score.append(tree.score(x_train, y_train))
test_score.append(tree.score(x_test, y_test))
plt.plot(train_score, label='训练集分值')
plt.plot(test_score, label='测试集分值')
plt.xlabel('划分深度')
plt.ylabel('分值')
plt.legend()
plt.show()
可见, 划分深度小, 训练集和测试集的分值都小, 容易欠拟合
随着划分深度的增加, 分值都在增加, 模型预测效果也在变好
当深度增加到一定程度, 深度再增加, 训练集分值随着增加, 但造成了模型过分依赖训练集数据特征, 从而测试集分值减小, 容易过拟合
不纯度度量标准有:
信息熵
$$I_{H}(D)=-\sum_{i=1}^{m} p(i \mid D) \log _{2} p(i \mid D) $$m: 节点 D 中含有样本的类别数量
$p(i \mid D)$: 节点 D 中, 属于类别 i 的样本占节点 D 中样本总数的比例(概率)
基尼系数
$$I_{G}(D)=1-\sum_{i=1}^{m} p(i \mid D)^{2}$$错误率
$$I_{E}(D)=1-\max {p(i \mid D)}$$看看各个度量标准与概率分布的关系:
def entropy(p):
return -p * np.log2(p) - (1-p) * np.log2(1-p)
def gini(p):
return 1 - p**2 - (1-p)**2
def error(p):
return 1 - np.max([p, 1-p], axis=0)
p = np.linspace(0.0001, 0.9999, 200)
en = entropy(p)
er = error(p)
g = gini(p)
for i, lab, ls in zip([en, g, er],
['信息熵', '基尼系数', '错误率'],
['-', ':', '--']):
plt.plot(p, i, label=lab, linestyle=ls, lw=2)
plt.legend()
plt.xlabel('概率分布')
plt.ylabel('不纯度')
plt.show()
可见, 无论选哪一种度量标准, 样本属于同一类, 不纯度都是 0; 样本中不同类别占比相同时, 不纯度最大
ID3
ID3 (Iterative Dichotomiser3), 迭代二分法特点:
-使用多叉树结构
-使用信息熵作为不纯度度量, 选择信息增益最大的特征划分
-经典算法, 简单, 训练快
局限:
-不支持连续特征
-不支持缺失值处理
-不支持回归
-倾向选择特征取值多的特征来划分, 例如按身份证号划分, 一个号码就是一个特征
C4.5
ID3算法改进而来, 特点:
-使用多叉树结构
-不支持回归
优化:
-支持缺失值处理
-连续值进行离散化处理
-信息熵作为不纯度度量, 但选择 信息增益率 最大的特征划分
信息增益率:
$$I G_{\text {Ratio}}\left(D_{p}, f\right)=\frac{I G_{H}\left(D_{p}, f\right)}{I_{H}(f)} $$$I_{H}(f)$: 在特征 $f$ 下, 取各个特征值计算得到的信息熵之和, 其实就是特征 $f$ 的不纯度, 特征值越多, 特征不纯度越大
选择信息增益最大的特征来划分, 父节点的信息熵不变, 就要求信息增益的第二项 $\sum_{j=1}^{n} \frac{N_{j}}{N_{p}} I\left(D_{j}\right)$ 最小, 从而会倾向选择特征取值多的特征
因为, 特征取值多, 通常划分之后子节点的不纯度(信息熵)就更低, 例如极端情况, 选身份证划分, 划分之后不管什么类别, 子节点都只有一种类别, 不纯度都是 0, 第二项就是 0, 信息增益就最大
因此, 采用信息增益率, 将 信息增益/特征不纯度, 就避免了 特征不纯度大 造成 信息增益大 而选择类别多的特征来划分的情况
看看类别数量与信息熵的关系:
en = lambda p: np.sum(-p * np.log2(p))
a1 = np.array([0.3, 0.7])
a2 = np.array([0.3, 0.3, 0.4])
a3 = np.array([0.25] * 4)
a4 = np.array([0.1] * 10)
print(en(a1), en(a2), en(a3), en(a4), sep='\n')
0.8812908992306927
1.5709505944546684
2.0
3.321928094887362
CART
CART (Classification And Regression Tree), 分类回归树, 特点如下:
-使用二叉树结构
-支持连续值与缺失值处理
-分类时, 使用基尼系数作为不纯度度量, 选择基尼增益最大的特征划分
-回归时, 使用 MSE 或 MAE 最小的特征划分
回归决策树因变量 y 是连续的, 使用叶子节点的均值来预测未知样本, 使用 MSE 或 MAE 作为特征划分的评估指标
在 scikit-learn 中, 使用的是优化的 CART 算法来实现决策树
以波士顿房价为例来实现(参数参考分类决策树):
from sklearn.datasets import load_boston
from sklearn.tree import DecisionTreeRegressor
from sklearn.model_selection import train_test_split
x, y = load_boston(return_X_y=True)
x_train, x_test, y_train, y_test = train_test_split(
x, y, test_size=0.25, random_state=1)
tree = DecisionTreeRegressor(max_depth=5, random_state=0)
tree.fit(x_train, y_train)
print(tree.score(x_train, y_train))
print(tree.score(x_test, y_test))
0.9204825770764915
0.8763987309111113
前面接触的算法, 都是 监督学习 , 即训练数据中自变量(特征)和因变量(结果)都是已知的, 用含有结果的训练集建立模型, 然后对未知结果的数据进行预测
聚类属于 无监督学习 , 训练数据中没有”已知结果的监督”. 聚类的目的, 就是通过已知样本数据的特征, 将数据划分为若干个类别, 每个类别成一个类簇, 使得同一个簇内的数据相似度越大, “物以类聚”, 不同簇之间的数据相似度越小, 聚类效果越好
聚类的样本相似度根据距离来度量
即 K 均值算法, 是常见的聚类算法, 该算法将数据集分为 K 个簇, 每个簇使用簇内所有样本的均值来表示, 该均值称为”质心”
K-Means 算法的目标, 就是选择适合的质心, 使得每个簇内, 样本点距质心的距离尽可能的小, 从而保证簇内样本有较高相似度
算法实现步骤:
a, 从样本中选择 K 个点作为初始质心
b, 计算每个样本点到各个质心的距离, 将样本点划分到距离最近的质心所对应的簇中
c, 计算每个簇内所有样本的均值, 使用该均值作为新的质心
d, 重复 b 和 c, 重复一定次数质心一般会趋于稳定, 如果达到以下条件, 重复结束:
– 质心位置变化小于指定的阈值
– 达到最迭代环次数
对于算法的实现步骤, 我们有几个重要的疑问:
– 1.怎么评价质心是否达到了最佳位置?
– 2.初始质心随机选, 还是选在哪里?
– 3. K 值怎么定?
样本的相似度是根据距离来度量的, 一般使用簇内 误差平方和 (within-cluster SSE 簇惯性) 来作为优化算法的目标函数, 距离常用欧氏距离, 优化目标就是使 SSE 最小化:
$$S S E=\sum_{i=1}^{k} \sum_{j=1}^{m_{i}}\left(\left|x_{j}-\mu_{i}\right|^{2}\right)$$k: 族的数量
$m_{i}$: 第 i 个簇含有的样本数量
${\mu}_{i}$: 第 i 个族的质心
$\left|x_{j}-\mu_{i}\right|$: 第 i 个族中,每个样本 $x_{j}$ 与质心 $\mu_{i}$ 的距离
同一个数据集, 相同的簇数, SSE 越小, 通常质心位置更佳, 算法模型更好
初始质心可以随机选择, 但由于算法是通过迭代初始质心一步步实现, 初始质心的位置受随机性影响, 算法训练的最终结果也会受到影响
import numpy as np
from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs
import matplotlib.pyplot as plt
plt.rcParams['font.family'] = 'Microsoft YaHei'
plt.rcParams['font.size'] = 12
plt.rcParams['axes.unicode_minus'] = False
'''
生成数据:
n_samples: 样本数量
n_features: 特征数
centers: 聚类中心
cluster_std: 簇的标准差, 可以统一指定, 也分别指定
'''
centers = [[1, 1], [5, 2], [2, 5]]
x, y = make_blobs(n_samples=90,
n_features=2,
centers=centers,
cluster_std=[2.2, 2.5, 2],
random_state=0)
# x 是特征, y 是类别标签
# 绘制原始数据
plt.figure(figsize=(12,8))
plt.subplot(221)
colors = np.array(['Coral', 'SeaGreen', 'RoyalBlue'])
plt.scatter(x[:, 0], x[:, 1], c=colors[y], marker='.', label='原始数据')
plt.title('原始数据')
# 定义绘制聚类结果的函数
def plot_cluster(model, train, test=None):
global colors # 使用上面的颜色
cc = model.cluster_centers_ # 获取质心
label = model.labels_ # 获取聚类结果的标签
# 绘制质心
plt.scatter(cc[:, 0], # 质心的 x 坐标
cc[:, 1], # 质心的 y 坐标
marker='*',
s=150,
c=colors)
# 绘制训练集
plt.scatter(train[:, 0], train[:, 1], marker='.', c=colors[label])
# 绘制测试集
if test is not None:
y_hat = model.predict(test)
plt.scatter(test[:, 0], test[:, 1], marker='+',
s=150, c=colors[y_hat])
# 标题
plt.title(f'SSE:{model.inertia_:.1f} 迭代次数:{model.n_iter_}')
# 测试集
test = np.array([[6, 5]])
# 绘制不同初始质心的聚类结果
seed = [1, 10, 100]
for i in range(2, 5):
plt.subplot(2, 2, i)
kmeans = KMeans(n_clusters=3, # 簇数
init='random', # 初始化方式
n_init=1, # 初始化质心组数
random_state=seed[i-2])
kmeans.fit(x)
plot_cluster(kmeans, x)
# 测试结果
plot_cluster(kmeans, x, test)
从上图可以看出受初始化质心的影响, 聚类效果(SSE) 与 收敛速度(迭代次数) 会不同, 也即是可能会收敛到局部最小, 而不是整体最优; 同时, 也可以看出 SSE 越小, 整体结果越优, 越接近原始数据
针对上述初始化质心造成的问题, 设置初始化多组质心可以得到缓解, 但通常限于聚类簇数较少的情况, 如果簇数较多, 可能就不会有效
于是有了 K-Means++, 选择初始化质心时, 不在随机选, 而是按下述步骤进行选择:
– 1, 从训练数据中随机选择一个样本点, 作为初始质心
– 2, 对任意一个非质心样本点
$x^{(i)}$, 计算
$x^{(i)}$ 与现有最近质心的距离
$D\left(x^{(i)}\right)$
– 3, 根据概率
$\frac{D\left(x^{(i)}\right)^{2}}{\sum_{j=1}^{m}D\left(x^{(j)}\right)^{2}}$ 最大, 来选择最远的一个样本点
$x^{(i)}$ 作为质心, m 为非质心样本点数量
– 4, 重复 2 和 3, 直到选择了 K 个质心为止
做了优化之后, 保证了初始质心不会集中, 而是分散在数据集中
下面试试 K-Means++ 的聚类效果:
kmeans = KMeans(n_clusters=3, init='k-means++', n_init=1)
kmeans.fit(x)
plot_cluster(kmeans, x)
K 是超参数, 需要预先人为指定
有时需要按照建模的需求和目的来选择聚类的个数, 但是 K 值选择不当, 聚类效果可能不佳. 例如实际 3 类, K 选了 10, 或者 K 无限制, 取值和样本点个数一样, 最后每个点一个类, SEE 为 0, 但是聚类已经毫无意义
如果不是硬性要求 K 的取值, 怎么确定最佳的 K 值呢? 一个比较好的方法就是 肘部法则 :
SEE 需要越小越好, K 又不能取太大, 我们可以看看他们之间的关系:
# 设置列表储存 SSE
sse = []
# K 值从 1~9 变化
scope = range(1, 10)
for k in scope:
kmeans = KMeans(n_clusters=k)
kmeans.fit(x)
sse.append(kmeans.inertia_)
plt.xticks(scope)
plt.plot(scope, sse, marker='o')
plt.show()
从上图可以看出, K 增加, SSE 减小, 但当 K > 3 时, K 再增加, SSE 减小变得缓慢, 所以 K 选择 3, 实际情况也是 3
K-Means 每次迭代都会使用所有数据参与运算, 当数据集较大时, 会比较耗时. Mini Batch K-Means (小批量 K-Means) 算法每次迭代使用小批量样本训练, 逐批次累计的方式进行计算, 从而大大减少计算时间. 效果上, 通常只是略差于 K-Means
Mini Batch K-Means 算法实现步骤:
a, 从数据集中随机选择部分数据, 使用 K-Means 算法在这部分数据上聚类, 获取质心
b, 再从数据集中随机选择部分数据, 分别分配给最近的质心
c, 每个簇根据现有的数据集更新质心
d, 重复 b 和 c, 直到质心变化小于指定阈值或达到最大迭代次数
下面比较一下两个算法:
import time
import pandas as pd
from sklearn.cluster import MiniBatchKMeans
from sklearn.metrics.pairwise import pairwise_distances_argmin
import warnings
warnings.filterwarnings('ignore')
# 生成数据
centers = [[1, 1], [400, 100], [100, 400]]
x, y = make_blobs(n_samples=8000, n_features=2, centers=centers,
cluster_std=120, random_state=0)
# 定义函数, 用于计算模型训练时间
def elapsed_time(model, data):
start = time.time()
model.fit(data)
end = time.time()
return end - start
n_clusters = len(centers)
kmeans = KMeans(n_clusters=n_clusters)
mbk = MiniBatchKMeans(n_clusters=n_clusters,
batch_size=200, # 小批量的大小
n_init=10 # 和 KMeans 统一为 10
)
kmeans_time = elapsed_time(kmeans, x)
mbk_time = elapsed_time(mbk, x)
print('K-Means耗时:', kmeans_time)
print('Mini Batch K-Means耗时:', mbk_time)
# 绘制聚类效果
plt.figure(figsize=(12, 5))
model = [kmeans, mbk]
for i, m in enumerate(model, start=1):
plt.subplot(1, 2, i)
plot_cluster(m, x)
K-Means耗时: 0.051812171936035156
Mini Batch K-Means耗时: 0.04886937141418457
可见, 聚类耗时 K-Means 更多, 如果数据量很大, 耗时会更明显, 而聚类效果基本一样. 但发现颜色对不上, 这是因为质心的随机性, 聚类之后质心虽然最终落在相同的位置, 但是顺序不一致, 从而聚类的结果标签不一致, 即使是同一个算法, 运行几次, 标签结果也会不一致
我们将相同簇用相同的颜色绘制:
plt.figure(figsize=(12, 5))
# 定义列表, 用来保存两个模型预测结果
y_hat_list = []
for i, m in enumerate(model, start=1):
plt.subplot(1, 2, i)
y_hat = m.predict(x)
if m == mbk:
'''
因为输出的质心顺序就是训练结果标签的顺序
故可以按 mbk 训练的质心, 去找 kmeans 训练的相同簇的质心
pairwise_distances_argmin(x, y) 解释:
依次取出数组 X 中的元素 x,
计算找到数组 Y 中与 x 距离最近的元素 y 的索引,
返回索引构成的数组
'''
# 将两者相同簇的质心一一对应并按 mbk 质心的顺序封装成字典
ar = pairwise_distances_argmin(
mbk.cluster_centers_, kmeans.cluster_centers_)
dict_ = dict(enumerate(ar))
# 用 mbk 的训练结果标签 y_hat 就可以寻找到对应的 kmeans 的质心
y_hat = pd.Series(y_hat).map(dict_).values
# 将预测结果加入到列表中
y_hat_list.append(y_hat)
plt.scatter(x[:, 0], x[:, 1], c=colors[y_hat], marker='.')
比较两个算法聚类结果的差异:
same = y_hat_list[0] == y_hat_list[1]
diff = y_hat_list[0] != y_hat_list[1]
plt.scatter(x[same, 0], x[same, 1], c='g', marker='.', label='预测相同')
plt.scatter(x[diff, 0], x[diff, 1], c='r', marker='.', label='预测不同')
plt.legend()
print('相同数量:', x[same].shape[0])
print('不同数量:', x[diff].shape[0])
相同数量: 7967
不同数量: 33
两个算法聚类结果只有 33 个样本点不同