从0开始机器学习--4.5分类(knn、决策树、集成模型、自定义回归器类、逻辑回归、对数几率回归、SVM支持向量机、朴素贝叶斯、贝叶斯估计,概念、算法原理、决策边界、代码)
写在前面
在这一篇文章之后,我们就对八大基础机器学习模型有个大致的概念了,包括上一篇《4.3回归》中提到的线性回归、多项式回归,以及这一篇中会提到的knn、决策树、集成模型、逻辑回归、对数几率回归、SVM支持向量机、朴素贝叶斯。
- 1.python基础;
- 2.ai模型概念+基础;
- 3.数据预处理;
- 4.机器学习模型--1.编码(嵌入);2.聚类;3.降维;4.回归(预测);5.分类;
- 5.正则化技术;
- 6.神经网络模型--1.概念+基础;2.几种常见的神经网络模型;
- 7.对回归、分类模型的评价方式;
- 8.简单强化学习概念;
- 9.几种常见的启发式算法及应用场景;
- 10.机器学习延申应用-数据分析相关内容--1.A/B Test;2.辛普森悖论;3.蒙特卡洛模拟;
- 11.数据挖掘--关联规则挖掘
- 12.数学建模--决策分析方法,评价模型
- 13.主动学习(半监督学习)
- 以及其他的与人工智能相关的学习经历,如数据挖掘、计算机视觉-OCR光学字符识别、大模型等。
目录
K-最近邻(K-Nearest Neighbors, KNN)
1. ID3(Iterative Dichotomiser 3)
3. CART(Classification and Regression Trees)
支持向量机(SVM,Support Vector Machine)
K-最近邻(K-Nearest Neighbors, KNN)
概念
K-最近邻(KNN)是一种非参数化(不对数据的分布进行明确的假设,并且在训练过程中不涉及模型的参数化拟合)的监督学习算法(在第二篇《ai模型基础+概念》中具体介绍过),可以用于分类和回归任务。它通过比较新数据点与训练数据集中的样本之间的距离,利用最近的K个邻居的信息来进行预测。
超参数
1. K值(邻居数)
- 描述:K值表示用来预测的最近邻居的数量。在KNN算法中,K值直接影响模型的预测结果。
- 影响:
- K值过小:模型会对训练集的噪声点过度敏感,导致过拟合(见下“决策树”部分)。
- K值过大:模型会对多数类产生偏向,忽略局部结构,导致欠拟合。
- 调优方法:通常通过交叉验证(在第二篇中有具体介绍)来选择合适的K值。最常见的选择范围是从1到20,但具体取决于数据集的特征。
2. 距离度量方式
- 描述:KNN算法需要计算数据点之间的距离,不同的距离度量方式会影响最终的结果。常见的距离度量方法(在《4.1聚类》中有具体介绍)有:
- 欧氏距离(Euclidean Distance):最常用的距离度量,计算两个点之间的直线距离。
- 曼哈顿距离(Manhattan Distance):计算两个点在轴上的距离之和。
- 余弦相似度(Cosine Similarity):用于衡量两个向量的夹角,常用于文本数据或高维稀疏数据。
- 闵可夫斯基距离(Minkowski Distance):包括欧氏距离和曼哈顿距离的推广形式,当p=2时为欧氏距离,p=1时为曼哈顿距离。
- 影响:不同的数据结构适合不同的距离度量方式,因此选择合适的度量方法可以提高模型性能。
3. 权重(weights)
- 描述:KNN可以选择是否为邻居的距离加权。默认情况下,所有K个邻居的权重相同,称为“均等权重”。但也可以根据距离进行加权,距离越近的点权重越大,常见的加权方式有:
- uniform:所有邻居的权重相同。
- distance:距离越近的邻居权重越大,通常采用反比权重 1/d,即距离越小,权重越大。
- 影响:在一些情况下,使用距离加权可以提高模型的预测准确性,特别是当离测试点最近的邻居具有更强的代表性时。
4. 算法(algorithm)
- 描述:KNN在实现上有不同的算法来加速邻居搜索,尤其是在大规模数据集上。常用的实现算法有:
- brute force:暴力搜索(数据结构基础),计算所有点的距离,时间复杂度为O(n)。
- kd-tree:通过构建kd-tree来加速KNN的搜索,适用于中等规模的低维数据。
- ball-tree:类似于kd-tree,但更适合处理高维数据。
- 影响:不同算法适合不同的数据集规模和维度。通常,暴力搜索适合小数据集,kd-tree和ball-tree则适合更大的数据集。
brute force
概念
暴力搜索通过逐一遍历比较所有数据点与查询点之间的距离,找到最近邻。
目标
确保找到目标点的最近邻,无需预先构建数据结构。
算法原理
- 对于查询点,计算与每个数据点的距离。
- 找到最小距离对应的数据点,返回该点作为最近邻。
查询过程
查询时,从根节点开始,判断查询点是否在当前节点的球内。如果在,就递归搜索该节点的子树。若不在,则直接忽略该子树。这种方式能够排除与查询点距离较远的部分,从而加速最近邻查询。
KD-tree (K-Dimensional Tree)
概念
KD-tree是一种对空间进行递归划分的树形数据结构,专门用于处理多维数据(通常是几何数据),比如在KNN算法中,它可以有效加速最近邻查询。kd-tree也叫快速搜索近邻法。
目标
通过将数据划分成多个子区域(并寻找子类质心/均值中心,并计算当前样本点与中心/质心的距离),KD-tree使得最近邻查询可以避免对每个点进行遍历,缩小需要搜索的范围,减少计算复杂度。并融入了分支定界法的思想。
算法原理
- 递归划分:KD-tree通过选择某一维度上的数据进行划分,递归地将数据空间分成两部分。一般情况下,会选择每一维的中位数来划分,从而保证每个子区域拥有接近相同数量的点。
- 节点的划分方式:每个节点将数据空间分成两部分,左子树包含比当前节点坐标小的数据,右子树包含比当前节点坐标大的数据。每一层的划分维度通常是轮流选择的,比如第一层按x维划分,第二层按y维划分,第三层按z维划分,依次类推。
查询过程
最近邻查询时,从树的根节点开始,递归搜索最近的子树,直到找到叶子节点。找到候选点后,再回溯检查另一个子树是否可能包含更近的点。
ball-tree
概念
Ball-tree是一种用于加速高维空间中数据查询的树结构,类似于KD-tree,但它通过“球”(ball)来分割数据,适用于更高维度的数据集。Ball-tree会将数据分割为一系列的超球体,查询时只需要检查可能包含最近邻的球体。
目标
Ball-tree的目标是通过使用包围球体将数据分区,使得在高维空间中搜索最近邻居时能更有效地排除无关区域。
算法原理
- 递归划分:与KD-tree不同,Ball-tree不是简单地沿某个轴进行划分,而是根据数据的分布,计算一个球体(以质心为中心的超球),并将数据分成两个子集。
- 球体的划分方式:对于每个节点,Ball-tree会计算该节点包含的数据的质心和最大半径,并通过不同的方法划分出两个球体,将数据集递归划分成两个部分。每个节点保存一个超球体,描述它所包含的数据范围。
- 子树划分:继续递归划分,直到每个叶子节点包含的数据量少于设定的阈值。
查询过程
查询时,从根节点开始,判断查询点是否在当前节点的球内。如果在,就递归搜索该节点的子树。若不在,则直接忽略该子树。这种方式能够排除与查询点距离较远的部分,从而加速最近邻查询。
三者对比

import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier
# 创建数据集
X, y = make_classification(n_samples=300, n_features=2, n_redundant=0, n_repeated=0, n_classes=2, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
# 在 make_classification 函数中,这几个参数用于控制生成的数据集的特征。它们的具体含义如下:
# n_informative:这是生成数据集中信息量的特征数量。信息特征是对分类目标有影响的特征,模型可以利用这些特征进行有效的预测。
# n_redundant:这是生成数据集中冗余特征的数量。冗余特征是指那些与信息特征高度相关的特征,它们并不提供额外的信息,通常是从信息特征的线性组合中生成的。
# n_repeated:这是生成数据集中重复特征的数量。重复特征是完全相同的特征,通常是对信息特征的复制。它们不会对模型的预测能力产生额外的贡献。
# 当设置这些参数时,它们的总和必须小于总特征数 n_features,因为这意味着生成的数据集有足够的特征来包含所有类型的特征(信息、冗余和重复)。
# 使用KNN分类器
knn_brute = KNeighborsClassifier(n_neighbors=5, algorithm='brute')
knn_kd = KNeighborsClassifier(n_neighbors=5, algorithm='kd_tree')
knn_ball = KNeighborsClassifier(n_neighbors=5, algorithm='ball_tree')
# 训练模型
knn_brute.fit(X_train, y_train)
knn_kd.fit(X_train, y_train)
knn_ball.fit(X_train, y_train)
# 预测
y_pred_brute = knn_brute.predict(X_test)
y_pred_kd = knn_kd.predict(X_test)
y_pred_ball = knn_ball.predict(X_test)
# 可视化结果
plt.figure(figsize=(15, 5))
plt.subplot(131)
plt.scatter(X_test[:, 0], X_test[:, 1], c=y_pred_brute, cmap='coolwarm')
plt.title('KNN (Brute Force) Prediction')
plt.subplot(132)
plt.scatter(X_test[:, 0], X_test[:, 1], c=y_pred_kd, cmap='coolwarm')
plt.title('KNN (KD-Tree) Prediction')
plt.subplot(133)
plt.scatter(X_test[:, 0], X_test[:, 1], c=y_pred_ball, cmap='coolwarm')
plt.title('KNN (Ball-Tree) Prediction')
plt.show()
虽然在小规模数据集上,暴力搜索可能与其他方法表现相似,但在大规模数据集上,KD-tree和Ball-tree将更有效。

5. p值(闵可夫斯基距离的指数参数)
- 描述:在使用闵可夫斯基距离时,p值决定了具体使用的距离度量方式:
- 当 p = 1 时,闵可夫斯基距离就是曼哈顿距离。
- 当 p = 2 时,闵可夫斯基距离就是欧氏距离。
- 影响:p值会影响到不同邻居之间距离的计算方式,进而影响分类效果。常用的p值为1和2。
Python中KNN的超参数设置
在Scikit-learn中,可以通过以下方式设置KNN的超参数:
from sklearn.neighbors import KNeighborsClassifier
knn = KNeighborsClassifier(
n_neighbors=5, # K值
metric='minkowski', # 距离度量方式,默认为闵可夫斯基距离
p=2, # 闵可夫斯基距离的p值
weights='uniform', # 权重,默认为均等权重
algorithm='auto' # 搜索算法,auto会自动选择合适的算法
)
目标
KNN的目标是根据训练数据集中的样本,预测新样本所属的类别(分类任务)或预测其具体数值(回归任务)。对于分类任务,KNN通过“投票”来决定分类结果;对于回归任务,它通过“平均值”来进行预测。
假设
KNN算法的一个基本假设是相似的数据点具有相似的输出,即距离较近的数据点通常有相同的标签或相似的数值。这意味着在特征空间中,数据点的分布较为规则且平滑。
常见应用场景
- 图像分类:根据相似的像素分布,KNN可以对图像进行分类。
- 文本分类:在自然语言处理中,用于根据词频或文本相似度对文档进行分类。
- 推荐系统:基于用户的历史行为或偏好,KNN可以用来推荐相似的商品。
- 金融数据分析:用于股票价格预测、风险评估等。
算法原理
KNN通过计算样本与数据集中所有点的距离,选择距离最近的K个邻居,根据邻居的类别来预测新的样本类别或数值。
最常见的距离度量是欧氏距离(Euclidean Distance)(在《4.1聚类》中有详细介绍)
分类
- 计算测试点与训练数据集中的每个点的距离。
- 选择距离最近的K个点(邻居)。
- 对这K个邻居的类别进行投票,得票最多的类别作为预测结果。
回归
- 计算测试点与训练数据集中的每个点的距离。
- 选择距离最近的K个点。
- 计算这K个邻居的目标值的平均值(或加权平均值),作为预测结果。
算法步骤
- 准备数据:包括训练集和测试集。
- 计算距离:对测试集中的每个点,计算它与训练集中的每个样本的距离。
- 选择K个最近邻居:根据计算得到的距离,选择K个最近的样本点。
- 分类/回归:
- 分类:根据K个邻居的类别进行投票,选出占多数的类别作为结果。
- 回归:对K个邻居的值进行平均,得到预测值。
- 输出结果。
代码
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap
from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import train_test_split
from sklearn.datasets import make_classification
# 生成一个简单的二维分类数据集
X, y = make_classification(n_samples=100, n_features=2, n_informative=2, n_redundant=0, n_repeated=0, n_classes=2, n_clusters_per_class=1, random_state=42)
# 绘制原始数据集
plt.figure(figsize=(8, 5))
plt.scatter(X[:, 0], X[:, 1], c=y, edgecolors='k', cmap=ListedColormap(('orange', 'cornflowerblue')))
plt.title('Original 2D Classification Dataset')
plt.xlabel('Feature 1')
plt.ylabel('Feature 2')
plt.legend(['Class 0', 'Class 1'])
plt.grid()
plt.show()
# 分割数据为训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# 创建KNN分类器,并设置K值为5
knn = KNeighborsClassifier(n_neighbors=5)
knn.fit(X_train, y_train)
# 生成网格以进行决策边界可视化
x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.02), np.arange(y_min, y_max, 0.02))
# 预测整个网格上的分类结果
Z = knn.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
# 绘制决策边界
plt.contourf(xx, yy, Z, alpha=0.8, cmap=ListedColormap(('orange', 'cornflowerblue')))
# 绘制训练集和测试集
plt.scatter(X_train[:, 0], X_train[:, 1], c=y_train, edgecolors='k', marker='o', label='Train')
plt.scatter(X_test[:, 0], X_test[:, 1], c=y_test, edgecolors='k', marker='x', label='Test')
plt.title('KNN Classification (k=5)')
plt.legend()
plt.show()
输出
原始数据集的样子:

分类后的结果:

在上面的代码中,生成了一个二维的分类数据集并使用KNN进行分类。图中显示了KNN的决策边界,训练集和测试集样本的分布。
- 橙色区域和蓝色区域表示分类器的决策边界。
- 圆圈标记为训练数据,叉号标记为测试数据(不同特征类别的数据中有各自的训练及测试数据)。
决策边界
knn的决策边界是训练集n条类与类之间的垂直平分线构成的。
设贝叶斯的错误率为p*,则knn的错误率p>p*且p<2p*。k近邻的错误率对距离量纲非常敏感,通常来说,k越大,越无限趋近于贝叶斯分类错误率p*,边界会更加光滑,会减少噪音的影响(但也有反例,如同心圆中的不同类别)。对策:特征先事先归一化到(0,1)or引入拒绝机制、加权投票。
k近邻与最近邻的计算量和存储量是不变的。
决策树(Decision Tree)
概念
决策树是一种树状结构的机器学习算法,既可以用于分类问题(分类树)也可以用于回归问题(回归树)。它通过对数据特征的分裂来构建一棵树,从根节点开始,每个内部节点代表一个特征,每个叶节点代表最终的输出类别或值。其目标是通过最优的特征分裂点,最大化信息增益或其他度量标准(如Gini指数或均方误差)(见下),从而提高模型的预测能力。
属于非参数模型,因此不需要像线性回归那样对数据有线性或分布上的假设。
超参数
max_depth:树的最大深度。通过限制树的深度可以防止过拟合(见下)。min_samples_split:在内部节点再分裂之前,节点中至少要包含的样本数。min_samples_leaf:叶子节点中最少的样本数,控制叶子节点的大小。criterion:用于分裂节点的准则(分类树:gini或entropy(见下);回归树:mse、mae(第7篇《评估》中会具体介绍)等)。max_features:用于最佳分裂的最大特征数,限制可以减少计算开销。splitter:选择分裂策略,可以是best(选择最佳分割点)或random(随机选择分割点)。
目标
决策树的目标是基于特征将数据划分到不同的叶子节点,使得每个叶子节点中的数据尽可能“纯净”,即属于同一个类别或有相似的数值。
信息增益
决策树算法计算信息增益时,常用的衡量标准主要包括以下几种:
1. 熵(Entropy)
定义为数据集的纯度(信息论中的概念):
其中, 是第 i 类的概率。
条件熵(Conditional Entropy):表示在给定特征的条件下,数据集的熵;
信息增益:熵-条件熵,信息增益越大,表示特征𝐴对于分类的贡献越大。
2. 增益率
增益率对信息增益进行了修正,以避免偏向于具有较多取值的特征。
分裂信息:用于度量特征取值的多样性,修正信息增益的偏向性。其计算公式为:
- 其中,𝐴是特征,
是特征 𝐴的所有可能取值,
表示取值为 v 的数据子集,S 是数据集。
增益率=信息增益/分裂信息,增益率考虑了特征的多样性,能够更公平地评估特征的有效性。
3. 基尼系数(gini)
基尼指数越小,表示数据集的纯度越高。除二分类问题外,不适合解决多分类问题。
决策树三种算法(面经)
1. ID3(Iterative Dichotomiser 3)
- 基本原理:ID3算法通过计算信息增益来选择特征进行划分。它选择信息增益最大的特征作为决策节点。
- 特征处理:主要处理离散特征,对连续特征需要进行离散化处理。
- 树的构建:生成的树可能较大,容易出现过拟合。可生成多叉树。且信息增益对可取值比较多的特征有偏好。如若“编号”作为分类特征依据,则信息增益会明显变大,不合理。
2. C4.5
- 基本原理:C4.5是ID3的改进版本,使用增益率而非信息增益来选择特征,减少偏向于多值特征的问题。
- 特征处理:可以处理连续和离散特征,支持特征的缺失值。
- 剪枝策略:引入了后剪枝机制(见下),以减少过拟合,提高模型的泛化能力。
- 树的构建:生成的决策树通常较小且更稳定。可生成多叉树。增益率准则对可取值数目较少的有所偏好,故采取启发式:先选择信息增益大的,再挑选增益率大的。
3. CART(Classification and Regression Trees)
- 基本原理:CART既可以用于分类也可以用于回归,采用基尼指数作为分类的标准(或均方误差用于回归)。
- 特征处理:可以处理连续和离散特征,不需要离散化。
- 剪枝策略:采用成本复杂度剪枝,通过交叉验证(第二篇中具体介绍)选择最优的剪枝参数。
- 树的构建:采用二叉树结构,每个节点只有两个子节点(即二分法)。即只能构建二叉树。
在使用sklearn的DecisionTreeClassifier中,默认情况下会使用CART算法。ID3和C4.5算法scikit-learn并不提供直接实现。
例:ID3算法信息增益函数代码:
import math
def loadDataSet(filename):
"""
从文件中加载数据集
参数:
filename - 数据文件的路径
返回:
dataSet - 数据集(列表形式)
labels - 特征标签(假设文件中不包含标签,标签由代码定义)
"""
pass
def calcShannonEnt(dataSet):
"""
计算给定数据集的香农熵
参数:
dataSet - 数据集
返回:
shannonEnt - 香农熵
"""
numEntries = len(dataSet) # 数据集大小
labelCounts = {} # 存储每个类别的计数
for featVec in dataSet:
currentLabel = featVec[-1] # 类别标签
if currentLabel not in labelCounts:
labelCounts[currentLabel] = 0
labelCounts[currentLabel] += 1
shannonEnt = 0.0
for key in labelCounts:
prob = float(labelCounts[key]) / numEntries
shannonEnt -= prob * math.log(prob, 2)
return shannonEnt
def splitDataSet(dataSet, axis, value):
"""
根据给定特征值划分数据集
参数:
dataSet - 待划分的数据集
axis - 划分数据集的特征索引
value - 特征值
返回:
retDataSet - 划分后的数据集
"""
retDataSet = []
for featVec in dataSet:
if featVec[axis] == value:
reducedFeatVec = featVec[:axis]
reducedFeatVec.extend(featVec[axis + 1:])
retDataSet.append(reducedFeatVec)
return retDataSet
def chooseBestFeatureToSplit(dataSet):
"""
选择最优的特征进行数据集划分
参数:
dataSet - 数据集
返回:
bestFeature - 最优特征的索引
"""
numFeatures = len(dataSet[0]) - 1
baseEntropy = calcShannonEnt(dataSet)
bestInfoGain = 0.0
bestFeature = -1
print("DataSet Size: ", len(dataSet))
for i in range(numFeatures):
featureValues = [example[i] for example in dataSet]
uniqueVals = set(featureValues)
newEntropy = 0.0
for value in uniqueVals:
subDataSet = splitDataSet(dataSet, i, value)
prob = len(subDataSet) / float(len(dataSet))
newEntropy += prob * calcShannonEnt(subDataSet)
infoGain = baseEntropy - newEntropy
print(f"Feature {i} - Info Gain: {infoGain}")
if infoGain > bestInfoGain:
bestInfoGain = infoGain
bestFeature = i
print("Best Feature Index: ", bestFeature)
return bestFeature
def majorityCnt(classList):
"""
选取出现频率最高的类别
参数:
classList - 类别标签列表
返回:
最常见的类别
"""
classCount = {}
for vote in classList:
if vote not in classCount:
classCount[vote] = 0
classCount[vote] += 1
sortedClassCount = sorted(classCount.items(), key=lambda item: item[1], reverse=True)
return sortedClassCount[0][0]
def createTree(dataSet, labels):
"""
创建决策树
参数:
dataSet - 数据集
labels - 特征标签
返回:
myTree - 生成的决策树
"""
classList = [example[-1] for example in dataSet] # 提取类别标签列
# 如果所有类别一样,返回该类别
if classList.count(classList[0]) == len(classList):
return classList[0]
# 如果没有更多的特征可用,返回出现最多的类别
if len(dataSet[0]) == 1: # 数据集只剩下标签列
return majorityCnt(classList)
bestFeat = chooseBestFeatureToSplit(dataSet) # 选择最佳特征
bestFeatLabel = labels[bestFeat]
myTree = {bestFeatLabel: {}} # 创建树字典
featureValues = [example[bestFeat] for example in dataSet]
uniqueVals = set(featureValues)
for value in uniqueVals: # 对每个特征值递归构建子树
subLabels = labels[:] # 复制标签列表
subLabels.remove(bestFeatLabel) # 通过复制列表避免修改原列表
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), subLabels)
return myTree
def printTree(myTree, indent=""):
"""
打印决策树
参数:
myTree - 决策树
indent - 缩进,表示树的层级
"""
if isinstance(myTree, dict):
for key, value in myTree.items():
print(indent + str(key))
printTree(value, indent + " ")
else:
print(indent + str(myTree))
def main():
# 加载数据集
dataSet, labels = loadDataSet("data")
# print(dataSet)
# 创建决策树
tree = createTree(dataSet, labels[:])
# 打印决策树
print("生成的决策树为:")
printTree(tree)
if __name__ == "__main__":
main()
优缺点
优点
- 易于理解和解释:决策树结构直观,容易可视化,可以通过树形图展示决策过程,便于解释。
- 无需特征缩放:不需要对特征进行标准化或归一化处理,直接使用原始数据。
- 处理非线性数据:能够捕捉复杂的非线性关系,不要求数据线性可分。
- 适用性广:可用于分类和回归问题,适用场景广泛。
- 处理缺失值:能够处理缺失值,对于缺失特征的数据也具有较好的鲁棒性。
缺点
- 容易过拟合:决策树模型容易在训练数据上过拟合,尤其是在树的深度较大时,导致泛化能力下降。
- 对小变化敏感:小的变化可能导致决策树结构的大幅度变化,因此不够稳定。
- 偏向于某些特征:在特征数目不平衡时,决策树可能偏向于某些特征,影响决策效果。
- 决策边界不平滑:决策树的决策边界通常是不平滑的,可能无法很好地处理某些类型的分类任务。
- 计算复杂度:对于大数据集,构建决策树的计算复杂度较高,可能导致训练时间较长。
步骤
- 选择最优特征:根据某种标准(如信息增益或基尼指数)选择当前最优的特征进行数据分割。
- 数据分割:将数据集分成不同的子集,递归进行分裂。
- 递归构建子树:对每个子集递归重复上述步骤,直到满足停止条件(如达到最大深度或叶子节点的样本数小于设定的阈值或纯度足够大)。若再划分性能不再提升,则停止划分,当前节点为叶节点,采用特征最多的一项命名。
- 叶子节点输出:最终每个叶子节点对应一个分类结果或回归值。
过拟合
过拟合是机器学习中常见的一种现象,指模型在训练数据上表现非常好,但在新数据(测试集)上效果很差。换句话说,模型过度“记住”了训练数据中的细节和噪声,而没有学到普遍的规律,因此对新的数据缺乏泛化能力。
假设你要训练一个模型来预测学生的考试成绩。你使用了过多的特征,比如学生的座位号、考试当天的天气等无关因素。如果模型太复杂(比如你选择了一个高阶多项式回归模型),它会在训练数据上表现得很好,准确地预测出所有成绩。但当你用新的数据进行测试时,由于模型对噪声和无关特征的过度拟合,预测结果可能会非常不准确。
直观的图示
- 欠拟合:模型太简单,没能捕捉到数据的模式。
- 适度拟合:模型正好捕捉了数据的主要规律,泛化能力好。
- 过拟合:模型太复杂,过度记住了训练数据中的细节和噪声,泛化能力差。
判断过拟合的方式
1. 训练损失(train loss)和验证损失(val loss)的变化趋势:
- 正常情况: 当模型在训练时,训练损失和验证损失都会随着训练轮数的增加而减少,并且验证损失和训练损失差距不大,表明模型在训练数据和未见过的数据(验证集)上都表现良好。
- 过拟合情况: 随着训练的继续,训练损失会不断减少,甚至趋近于零,而验证损失开始上升。这说明模型在训练集上表现非常好,但在验证集上性能变差,即模型对训练数据过拟合了。也有可能训练损失和验证损失同时大幅升高。
2. 验证集正确率:
- 同样的现象可能出现在分类问题中,验证集的准确率先上升然后开始下降,也表明模型开始过拟合。
在这个例子中,模型对数据进行了过拟合,因为使用了非常高阶(25阶)的多项式来拟合一个简单的正弦函数。
import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import PolynomialFeatures
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
# 生成简单的数据
np.random.seed(42)
X = np.linspace(0, 10, 100).reshape(-1, 1)
y = np.sin(X) + np.random.normal(0, 0.25, X.shape) # 将噪声的标准差为0.25
# 分割训练集和验证集
X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=42)
train_loss = []
val_loss = []
# 可视化 overfitting 高阶多项式回归
plt.figure(figsize=(12, 6))
# 拟合高阶多项式回归(例如 degree=25)
degree = 25
poly = PolynomialFeatures(degree=degree)
X_poly = poly.fit_transform(X)
model = LinearRegression()
model.fit(X_poly, y)
y_pred = model.predict(X_poly)
# 绘制原始数据与拟合曲线
plt.subplot(1, 2, 1)
plt.scatter(X, y, color="blue", label="Data")
plt.plot(X, y_pred, color="red", label=f"Overfit Model (degree={degree})")
plt.legend()
plt.title("Overfitting Example with High-Degree Polynomial")
# 训练不同复杂度(多项式阶数)模型,并记录train loss和val loss
for degree in range(1, 25):
poly = PolynomialFeatures(degree=degree)
X_train_poly = poly.fit_transform(X_train)
X_val_poly = poly.transform(X_val)
model = LinearRegression()
model.fit(X_train_poly, y_train)
y_train_pred = model.predict(X_train_poly)
y_val_pred = model.predict(X_val_poly)
train_loss.append(mean_squared_error(y_train, y_train_pred))
val_loss.append(mean_squared_error(y_val, y_val_pred))
# 可视化训练集与验证集损失
plt.subplot(1, 2, 2)
plt.plot(range(1, 25), train_loss, label="Train Loss")
plt.plot(range(1, 25), val_loss, label="Validation Loss")
plt.xlabel("Degree of Polynomial")
plt.ylabel("Loss")
plt.title("Train vs Validation Loss")
plt.legend()
plt.tight_layout()
plt.show()

左图是原始数据点(蓝点)和拟合的曲线(红线),可以明显看到红线已经不成sin函数的样子了;右图是不同阶数多项式的损失值,可以看到在20阶后验证损失就有明显的上升趋势。
避免过拟合的常见方法
- 正则化(Regularization):如L1或L2正则化,可以限制模型的复杂度。(会在下一篇《正则化》具体介绍)
- 增加训练数据/数据增强(Data Augmentation):更多数据可帮助模型学到更通用的规律。
- 早停(Early Stopping): 当验证集的损失开始上升时,提前停止训练
- 交叉验证:通过交叉验证来评估模型的性能,确保模型能泛化到未见过的数据。(第二篇)
决策树防止过拟合
1. 剪枝
减小树的复杂度,防止过拟合。
- 预剪枝:预先设置一个阈值,若小于阈值则不再分裂。边建立决策树边进行剪枝。(限制深度、叶子节点个数、信息增益率等)
- 后剪枝:从底向上剪枝,若某节点的样本数量小于某个阈值,则该节点及其子节点都不再分裂。建立完后进行剪枝。
- 双向剪枝:同时使用预剪枝和后剪枝。
注意:剪枝也有可能提升精度!如,把新一层模糊的属性归于上一层较为确切的分类类别属性。
2. 数据增强
增加训练数据量,使用数据增强(第三篇《数据预处理》有具体介绍)技术,帮助模型更好地学习潜在的模式,减少过拟合。
3. 特征选择
通过限制每次分裂考虑的特征数量(使用max_features参数),减少模型复杂性,从而降低过拟合的可能性。
4. 设置超参数
限制树的深度、设置最小样本分裂数、设置最小叶子节点样本数。
5. 使用随机森林、集成树模型。
代码
import numpy as np
import matplotlib.pyplot as plt
from sklearn.tree import DecisionTreeClassifier, plot_tree
from sklearn.model_selection import train_test_split
from sklearn.datasets import make_classification
# 生成一个简单的二维分类数据集
X, y = make_classification(n_samples=100, n_features=2, n_informative=2, n_redundant=0, random_state=42)
# 分割数据为训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
# 创建并训练决策树分类器
clf = DecisionTreeClassifier(criterion='gini', max_depth=4, random_state=42)
clf.fit(X_train, y_train)
# 预测测试集
y_pred = clf.predict(X_test)
# 可视化决策树结构
plt.figure(figsize=(10, 6))
plot_tree(clf, filled=True, feature_names=['Feature 1', 'Feature 2'], class_names=['Class 0', 'Class 1'], rounded=True)
plt.title('Decision Tree Visualization')
plt.show()
# 生成决策边界网格
x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.02), np.arange(y_min, y_max, 0.02))
# 预测网格上的分类
Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
# 绘制决策边界
plt.contourf(xx, yy, Z, alpha=0.8, cmap=plt.cm.coolwarm)
# 绘制训练集和测试集的散点图
plt.scatter(X_train[:, 0], X_train[:, 1], c=y_train, edgecolors='k', marker='o', label='Train')
plt.scatter(X_test[:, 0], X_test[:, 1], c=y_test, edgecolors='k', marker='x', label='Test')
plt.title('Decision Tree Decision Boundaries')
plt.legend()
plt.show()


- 从这副图中也可看出,决策树分类边界:由与坐标轴平行的若干线段组成。
- 多变量决策树:非叶节点不再对单个属性进行分类,这样就会有斜的划分边界,不是为每个非叶节点寻找一个最优属性划分,而是试图建立一个合适的非线性分类器。
集成算法(树集成模型)
集成学习通过结合多个模型的优点,能有效提高预测性能和鲁棒性,适应性强,是解决复杂机器学习问题的有力工具。
集成学习起作用的前提:各个子模型好而不同。随着子模型数量的增加,错误率趋于0。

bagging
Bagging(装袋法)是"自助聚合"(Bootstrap Aggregating)的简称,其核心思想是通过自助采样和模型聚合来降低模型方差并减少过拟合。该方法通过对训练集进行有放回的抽样,构建多个子模型并汇总它们的预测结果,最终得到的集成模型相比单一模型具有更好的泛化能力。
- 有放回采样:样本可能被重复选中,部分原始数据可能未被采样(约37%的样本会被遗漏,称为"袋外样本")
- 并行计算:所有基模型可同步训练
- 方差缩减:通过聚合多个模型的预测平滑噪声影响
随机森林是bagging的一种,采用多棵树的平均值作为最终的预测值。(随机森林是决策树采用bagging集成的一种算法)。
- 随机:数据随机采样,避免了过拟合。
-
基模型越不稳定(如小样本训练),Bagging提升效果越显著
- 对每个基模型的预测结果进行平均(回归)或投票(分类)来得到最终的预测结果。可以并行化处理。各个弱学习器之间不存在强依赖关系。
- 时间复杂度:低,基学习器的计算复杂度为O(m),组合过程为O(s),设有T个基学习器,则总复杂度为T(O(m)+O(s))。(可采用包外估计)
- 包外估计:
from sklearn.ensemble import RandomForestClassifier from sklearn.datasets import load_iris # 加载数据 data = load_iris() X, y = data.data, data.target # 启用OOB估计 model = RandomForestClassifier( n_estimators=100, oob_score=True, # 开启OOB估计 random_state=42 ) model.fit(X, y) # 输出OOB准确率 print("OOB Score:", model.oob_score_) # 例如输出 0.96
- 包外估计:

# Create classifiers
# 创建分类器列表
clf_array = [
('dt', DecisionTreeClassifier(max_depth=5)),
('lr', LogisticRegression(max_iter=1000)),
('knn', KNeighborsClassifier(n_neighbors=7)),
('svc', SVC(kernel='linear', C=0.1, probability=True))
]
for name, clf in clf_array:
vanilla_scores = cross_val_score(clf, X, y, cv=5, scoring='accuracy') # calculate the original results
bagging_clf = BaggingClassifier(
estimator=clf,
n_estimators=20,
max_samples=0.8, # 每个子集采样80%数据
random_state=42
)
bagging_scores = cross_val_score(bagging_clf, X, y, cv=5, scoring='accuracy') # calculate the bagging results
print ("Mean of: {1:.3f}, std: (+/-) {2:.3f} [{0}]".format(clf.__class__.__name__,vanilla_scores.mean(), vanilla_scores.std()))
print ("Mean of: {1:.3f}, std: (+/-) {2:.3f} [Bagging {0}]\n".format(clf.__class__.__name__,bagging_scores.mean(), bagging_scores.std()))
matplotlib.rcParams['font.sans-serif'] = ['SimHei'] # 用黑体显示中文
# 准备数据
names = [name for name, _ in clf_array]
vanilla_means = [cross_val_score(clf, X, y, cv=5).mean() for _, clf in clf_array]
bagging_means = [cross_val_score(BaggingClassifier(clf), X, y, cv=5).mean() for _, clf in clf_array]
# 绘制柱状图
x = np.arange(len(names))
width = 0.35
fig, ax = plt.subplots(figsize=(10,6))
rects1 = ax.bar(x - width/2, vanilla_means, width, label='原始模型')
rects2 = ax.bar(x + width/2, bagging_means, width, label='Bagging集成')
# 添加标签和标题
ax.set_ylabel('准确率')
ax.set_title('原始模型与Bagging集成性能对比')
ax.set_xticks(x)
ax.set_xticklabels(names)
ax.legend()
# 显示数值标签
def autolabel(rects):
for rect in rects:
height = rect.get_height()
ax.annotate(f'{height:.3f}',
xy=(rect.get_x() + rect.get_width() / 2, height),
xytext=(0, 3),
textcoords="offset points",
ha='center', va='bottom')
autolabel(rects1)
autolabel(rects2)
plt.show()

boosting
根据第一个模型的预测结果调整训练样本的权重,错分样本的权重增加,正确分类样本的权重减少。对所有基模型的预测结果进行加权平均或加权投票来得到最终预测结果。每个基模型依赖于前一个模型的结果,不能并行化处理。
- AdaBoost(调节权重项)
from sklearn.ensemble import AdaBoostClassifier model = AdaBoostClassifier(n_estimators=50, learning_rate=1.0) model.fit(X_train, y_train) - GBDT(梯度提升决策树)(XGBoost、lightgbm)
import xgboost as xgb model = xgb.XGBClassifier( n_estimators=100, max_depth=3, learning_rate=0.1, reg_lambda=1.0 # L2正则化 ) model.fit(X_train, y_train)
XGBoost是对传统GBDT的一种改进和扩展。除了使用梯度提升的基本思想外,还引入了许多增强特性,包括正则化、并行处理、缺失值处理等,通常在准确性和鲁棒性方面表现更佳。通过优化数据存储和计算过程(如采用块结构、并行处理和缓存),显著提高了模型训练的速度。

bagging对比boosting
1. 工作原理
Bagging(Bootstrap Aggregating):
- 并行训练:通过从训练数据中随机抽样生成多个子集(有放回抽样),然后在这些子集上并行训练多个基学习器(通常是同类型的模型)。
- 结果聚合:对于分类问题,通常使用投票法进行结果整合;对于回归问题,使用平均值。
- 目的:减少模型的方差,提高稳定性,主要用于防止过拟合。
Boosting:
- 顺序训练:基学习器是按顺序训练的,每个学习器在训练时关注之前模型错误分类的样本。每个新模型会加大对之前被错误分类样本的权重。
- 结果加权:最终结果通过加权投票或加权平均的方式整合,每个模型的贡献根据其性能而定。
- 目的:减少模型的偏差,逐步提高模型的准确性。
2. 适用性
Bagging:
- 通常适用于高方差模型(如决策树),通过减少方差来提高稳定性。
- 适合并行计算,因为各个模型之间相互独立。
Boosting:
- 通常适用于高偏差模型(如弱学习器),通过不断改进模型来提高准确性。
- 不适合并行计算,因为每个模型依赖于前一个模型的输出。
3. 例子
- Bagging:随机森林(Random Forest)是Bagging的一种常见实现,结合了多个决策树的预测结果。
- Boosting:AdaBoost和Gradient Boosting(包括XGBoost、LightGBM等)是Boosting的典型实现,逐步增强模型的能力。
代码
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification
from sklearn.ensemble import BaggingClassifier, AdaBoostClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
from matplotlib.colors import ListedColormap
# 生成二维分类数据集
X, y = make_classification(n_samples=300, n_features=2, n_informative=2, n_redundant=0, random_state=42, n_clusters_per_class=1)
# 分割训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
# 创建Bagging分类器和Boosting分类器(AdaBoost)
bagging_clf = BaggingClassifier(estimator=DecisionTreeClassifier(), n_estimators=100, random_state=42)
boosting_clf = AdaBoostClassifier(estimator=DecisionTreeClassifier(max_depth=1), n_estimators=100, random_state=42)
# 训练模型
bagging_clf.fit(X_train, y_train)
boosting_clf.fit(X_train, y_train)
# 预测并评估
y_pred_bagging = bagging_clf.predict(X_test)
y_pred_boosting = boosting_clf.predict(X_test)
print(f"Bagging Accuracy: {accuracy_score(y_test, y_pred_bagging)}")
print(f"Boosting Accuracy: {accuracy_score(y_test, y_pred_boosting)}")
# 创建网格以进行决策边界可视化
x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.02), np.arange(y_min, y_max, 0.02))
# 可视化决策边界
def plot_decision_boundary(model, title):
Z = model.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
plt.contourf(xx, yy, Z, alpha=0.8, cmap=ListedColormap(('orange', 'cornflowerblue')))
plt.scatter(X_train[:, 0], X_train[:, 1], c=y_train, edgecolor='k', marker='o', label='Train Data')
plt.scatter(X_test[:, 0], X_test[:, 1], c=y_test, edgecolor='k', marker='x', label='Test Data')
plt.title(title)
plt.legend()
plt.show()
# 可视化Bagging和Boosting的决策边界
plot_decision_boundary(bagging_clf, "Bagging Decision Boundary")
plot_decision_boundary(boosting_clf, "Boosting Decision Boundary")
输出
Bagging Accuracy: 0.9444444444444444
Boosting Accuracy: 0.9111111111111111

代码讲解
- Bagging:我们使用
BaggingClassifier来集成多个决策树(100个基决策树)。 - Boosting:我们使用
AdaBoostClassifier来训练多个浅层决策树,每个树的最大深度为1。 - 数据集:生成了一个简单的二维分类数据集,并将其划分为训练集和测试集。
- 决策边界:我们可视化了 Bagging 和 Boosting 模型的决策边界,显示它们在二维平面上如何划分不同类别。
结果可视化
- Bagging:决策边界通常比较平滑,因为它通过平均多个模型来降低方差。
- Boosting:决策边界往往更加细致,特别是在数据点密集的区域,因为它会逐步纠正错误分类的样本。
软/硬投票
- 硬投票机制集成分类器,通过组合多个基学习器的预测结果,采用多数表决机制决定最终输出。
- 软投票机制对所有基学习器的预测概率求加权平均,选择平均概率最高的类别。**相比硬投票**,它能更好地利用模型的置信度信息,适合对预测结果要求精细的场景。使用时需确保基模型支持概率计算。
from sklearn.ensemble import VotingClassifier
# Ensemble of Models
estimator = []
estimator.append(('LR',LogisticRegression(solver ='lbfgs',multi_class ='multinomial',max_iter = 200)))
estimator.append(('SVC', SVC(gamma ='auto', probability = True)))
estimator.append(('DTC', DecisionTreeClassifier()))
# Voting Classifier with hard voting
hard_voting = VotingClassifier(estimators = estimator, voting ='hard')
hard_voting.fit(X_train, y_train)
y_pred = hard_voting.predict(X_test)
# accuracy_score metric to predict Accuracy
score = accuracy_score(y_test, y_pred)
print("Hard Voting Score % d" % score)
# 检查每个基学习器的准确率
for name, model in estimator:
model.fit(X_train, y_train)
acc = model.score(X_test, y_test)
print(f"{name} 准确率: {acc:.4f}")
# Voting Classifier with soft voting
soft_voting = VotingClassifier(estimators = estimator, voting ='soft')
soft_voting.fit(X_train, y_train)
y_pred = soft_voting.predict(X_test)
# Using accuracy_score
score = accuracy_score(y_test, y_pred)
print("Soft Voting Score % d" % score)
for name, model in estimator:
model.fit(X_train, y_train)
proba = model.predict_proba(X_test)
for i in range(5): # 打印前5个样本的概率
print(f"模型{name}、样本{i}: {proba[i].round(4)},真实类别: {y_test[i]}")
Hard Voting Score 1
LR 准确率: 1.0000
SVC 准确率: 1.0000
DTC 准确率: 1.0000
Soft Voting Score 1
模型LR、样本0: [0.0038 0.8277 0.1685],真实类别: 1
模型LR、样本1: [0.9469 0.0531 0. ],真实类别: 0
模型LR、样本2: [0. 0.0015 0.9985],真实类别: 2
模型LR、样本3: [0.0065 0.7922 0.2013],真实类别: 1
模型LR、样本4: [0.0015 0.7741 0.2245],真实类别: 1
模型SVC、样本0: [0.0112 0.9565 0.0323],真实类别: 1
模型SVC、样本1: [0.9413 0.0373 0.0214],真实类别: 0
模型SVC、样本2: [0.0263 0.0243 0.9494],真实类别: 2
模型SVC、样本3: [0.0096 0.9496 0.0408],真实类别: 1
模型SVC、样本4: [0.0156 0.9034 0.081 ],真实类别: 1
模型DTC、样本0: [0. 1. 0.],真实类别: 1
模型DTC、样本1: [1. 0. 0.],真实类别: 0
模型DTC、样本2: [0. 0. 1.],真实类别: 2
模型DTC、样本3: [0. 1. 0.],真实类别: 1
模型DTC、样本4: [0. 1. 0.],真实类别: 1
# generate 2d classification dataset
# 生成500个样本,2个特征,3个类别,类内标准差=2.0
X, y = make_blobs(n_samples=500, centers=3, n_features=2, cluster_std=2, random_state=2)
estimator = []
estimator.append(('LR',LogisticRegression(solver ='lbfgs',multi_class ='multinomial',max_iter = 200)))
estimator.append(('SVC', SVC(gamma ='auto', probability = True)))
estimator.append(('DTC', DecisionTreeClassifier()))
soft_voting2 = VotingClassifier(estimators = estimator, voting ='soft')
soft_voting2.fit(X, y)
fig, ax = plt.subplots(figsize=(8, 6))
from matplotlib.colors import ListedColormap
def plot_decision_boundary(clf, X, y, ax):
# 设置网格范围
x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.02),
np.arange(y_min, y_max, 0.02))
# 预测整个网格
Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
# 绘制决策边界
cmap_light = ListedColormap(['#FFAAAA', '#AAFFAA', '#AAAAFF'])
ax.contourf(xx, yy, Z, cmap=cmap_light, alpha=0.8)
# 绘制数据点
scatters = []
for key in colors:
sc = ax.scatter(X[y==key, 0], X[y==key, 1], c=colors[key],
label=f'Class {key}', edgecolor='k', s=20)
scatters.append(sc)
ax.set_title('Decision Boundary (Average Ensemble)')
return scatters
scatters = plot_decision_boundary(soft_voting2, X, y, ax)
plt.legend(handles=scatters)
plt.tight_layout()
plt.show()

自定义回归器类
继承自 Scikit-learn 的 BaseEstimator 和 RegressorMixin,使其可以像标准 Scikit-learn 回归器一样使用(如具有 fit 和 predict 方法,可以被 sklearn 的 cross_val_score 等函数调用)。
def rmse_cv(model,X,y):
rmse = np.sqrt(-cross_val_score(model,X,y,scoring="neg_mean_squared_error",cv=5))
return rmse
# create a list to store models
models = [LinearRegression(),
Ridge(),
Lasso(alpha=0.01,max_iter=10000),
RandomForestRegressor(),
GradientBoostingRegressor(),
SVR(),
LinearSVR(),
ElasticNet(alpha = 0.001,max_iter=10000),
SGDRegressor(max_iter=1000, tol = 1e-3),
BayesianRidge(),
KernelRidge(alpha=0.6,kernel='polynomial',degree = 2,coef0=2.5),
ExtraTreesRegressor(),
XGBRegressor()
]
# another list to store names
names = ['LR','Ridge','Lasso','RF','GBR','SVR','LSVR','ENet','SGDR','BayRidge','Kernel','XTreeR','XGBR']
# To define the average weight
class AverageWeight(BaseEstimator, RegressorMixin):
def __init__(self,model,weight):
self.model = model
self.weight = weight
def fit(self,X,y):
self.models_ = [clone(x) for x in self.model]
for model in self.models_:
model.fit(X,y)
return self
def predict(self,X):
pred = np.array([model.predict(X) for model in self.models_])
# for every data point, single model prediction times weight, then add them together
# 加权平均预测
return np.sum(pred * np.array(self.weight).reshape(-1, 1), axis=0)
# Assign weights to the models and use the AverageWeight class to predict
# 选择表现最好的4个模型并分配权重
selected_models = [models[3], models[5], models[8], models[12]] # RF, SVR, SGDR, XGBR
selected_names = [names[3], names[5], names[8], names[12]]
weights = [0.2, 0.2, 0.4, 0.2] # 权重总和为1
# 评估各选模型的性能
print("\nSelected models performance:")
for model, name in zip(selected_models, selected_names):
score = rmse_cv(model, X_scaled, y_log)
print("{}: {:.6f}, {:.4f}".format(name, score.mean(), score.std()))
# 创建加权平均模型
weighted_avg_model = AverageWeight(model=selected_models, weight=weights)
# 评估加权平均模型的性能
weighted_score = rmse_cv(weighted_avg_model, X_scaled, y_log)
print("\nWeighted Average Model performance:")
print("Score: {:.6f}, {:.4f}".format(weighted_score.mean(), weighted_score.std()))
Selected models performance:
RF: 0.131104, 0.0085
SVR: 0.131747, 0.0109
SGDR: 0.295162, 0.0094
XGBR: 0.132300, 0.0045
Weighted Average Model performance:
Score: 0.157873, 0.0060
虽然计算rmse时没有显式调用 .fit(),但是使用了cross_val_score(),这个函数会自动在内部调用模型的 .fit() 方法进行训练。相当于用了一个调库函数的模型。
- 1. 把数据 X_scaled 和 y_log 分成 5 折;
- 2. 第 1 折:用第 2-5 折的数据调用 weighted_avg_model.fit(),再用第 1 折的数据调用 predict() 得到预测;
- 3. 计算预测与真实值的 RMSE;
- 4. 重复 5 次,每次换一折做测试集;
- 5. 最后将 5 次的 RMSE 求平均和标准差返回。
堆叠模型(stacking)
将多个模型的预测结果作为输入,训练一个新的模型来融合多个模型的预测结果。通过训练一个元模型来组合多个不同类型的基模型的预测结果,提高模型性能。
- 基础模型(Base Models): 这些模型是并行训练的,每个模型独立进行预测。它们可以是不同种类的模型,如决策树、k 近邻、随机森林等。这些基础模型的预测结果会被输入到元模型中。基础模型的作用是捕捉数据中的不同特征模式或决策边界。
- 元模型(Meta Model): 元模型用于结合基础模型的输出预测结果,生成最终的预测。它通常是一个简单的模型(如逻辑回归),对基础模型的输出结果进行进一步处理和学习,找到最优的组合方式。元模型的作用是学习如何加权或组合基础模型的预测,以提高整体模型的性能。
我们可以创建一个简单的 Stacking 模型,其中使用两个基础模型(比如决策树和 k 近邻)作为一层模型,并用逻辑回归(见下)作为“元模型”来整合基础模型的预测。
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification
from sklearn.tree import DecisionTreeClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import StackingClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
from matplotlib.colors import ListedColormap
# 生成二维分类数据集
X, y = make_classification(n_samples=300, n_features=2, n_informative=2, n_redundant=0, random_state=42, n_clusters_per_class=1)
# 分割训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
# 创建基础模型(决策树和KNN)
base_estimators = [
('decision_tree', DecisionTreeClassifier(max_depth=4)),
('knn', KNeighborsClassifier(n_neighbors=5))
]
# 创建堆叠分类器,使用逻辑回归作为元模型
stacking_clf = StackingClassifier(estimators=base_estimators, final_estimator=LogisticRegression())
# 训练堆叠模型
stacking_clf.fit(X_train, y_train)
# 基础模型的预测
decision_tree_pred = stacking_clf.named_estimators_['decision_tree'].predict(X_test)
knn_pred = stacking_clf.named_estimators_['knn'].predict(X_test)
# 输出基础模型的预测结果
print("Decision Tree Predictions:", decision_tree_pred)
print("KNN Predictions:", knn_pred)
# 元模型的最终预测
y_pred = stacking_clf.predict(X_test)
print("Final Stacking Predictions:", y_pred)
print(f"Stacking Accuracy: {accuracy_score(y_test, y_pred)}")
# 创建网格以进行决策边界可视化
x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.02), np.arange(y_min, y_max, 0.02))
# 可视化基础模型和堆叠模型的决策边界
def plot_decision_boundary(model, ax, title):
Z = model.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
ax.contourf(xx, yy, Z, alpha=0.8, cmap=ListedColormap(('orange', 'cornflowerblue')))
ax.scatter(X_train[:, 0], X_train[:, 1], c=y_train, marker='o', label='Train Data')
ax.scatter(X_test[:, 0], X_test[:, 1], c=y_test, marker='x', label='Test Data')
ax.set_title(title)
# 创建图形
fig, axs = plt.subplots(1, 3, figsize=(18, 5))
# 决策树的决策边界
plot_decision_boundary(stacking_clf.named_estimators_['decision_tree'], axs[0], "Decision Tree Decision Boundary")
# KNN的决策边界
plot_decision_boundary(stacking_clf.named_estimators_['knn'], axs[1], "KNN Decision Boundary")
# Stacking模型的决策边界
plot_decision_boundary(stacking_clf, axs[2], "Stacking Decision Boundary")
# 添加图例
for ax in axs:
ax.legend()
plt.tight_layout()
plt.show()

堆叠模型的结果很明显结合了单独决策树和单独knn的分类结果,这也体现在准确率的提升上。
Decision Tree Predictions: [0 1 1 0 1 1 1 1 0 1 0 0 1 0 1 0 0 0 1 1 1 1 1 0 0 0 0 1 0 0 0 1 1 1 1 0 1
0 1 0 0 1 1 0 0 0 0 0 0 0 0 1 1 0 1 1 0 1 1 0 1 1 1 0 0 1 0 1 0 0 0 1 1 1
0 1 0 0 0 1 0 1 0 0 0 0 0 0 0 0]
KNN Predictions: [0 1 1 0 1 1 1 1 0 1 0 0 0 0 1 0 0 0 1 1 1 1 1 0 0 0 0 1 0 0 0 1 1 1 1 0 1
0 1 0 0 1 1 0 1 0 0 0 0 0 1 1 1 0 1 1 0 1 1 0 1 1 1 0 0 1 0 1 0 0 0 1 1 1
0 1 0 0 1 1 0 1 0 0 0 0 0 0 0 0]
Final Stacking Predictions: [0 1 1 0 1 1 1 1 0 1 0 0 0 0 1 0 0 0 1 1 1 1 1 0 0 0 0 1 0 0 0 1 1 1 1 0 1
0 1 0 0 1 1 0 1 0 0 0 0 0 1 1 1 0 1 1 0 1 1 0 1 1 1 0 0 1 0 1 0 0 0 1 1 1
0 1 0 0 1 1 0 1 0 0 0 0 0 0 0 0]
Stacking Accuracy: 0.9555555555555556
自定义类
class Stacking(BaseEstimator, RegressorMixin):
def __init__(self, base_models, meta_model, n_folds=5):
self.base_models = base_models
self.meta_model = meta_model
self.n_folds = n_folds
def fit(self, X, y):
# 存储基础模型
self.base_models_ = [list() for _ in self.base_models]
meta_features = np.zeros((X.shape[0], len(self.base_models))) # 初始化元特征
kf = KFold(n_splits=self.n_folds, shuffle=True, random_state=42)
# 生成交叉验证的元特征
for i, model in enumerate(self.base_models):
for train_idx, val_idx in kf.split(X, y):
clone_model = clone(model)
clone_model.fit(X[train_idx], y[train_idx])
self.base_models_[i].append(clone_model)
meta_features[val_idx, i] = clone_model.predict(X[val_idx])
# 训练完整的基础模型用于后续预测
self.final_base_models_ = [clone(model) for model in self.base_models]
for model in self.final_base_models_:
model.fit(X, y)
# 训练元模型
self.meta_model.fit(meta_features, y)
return self
def predict(self, X):
# 使用完整训练的基础模型生成元特征
meta_features = np.column_stack([
model.predict(X) for model in self.final_base_models_
])
return self.meta_model.predict(meta_features)
base_models = [
DecisionTreeRegressor(max_depth=5),
KNeighborsRegressor(n_neighbors=7)
]
meta_model = LinearRegression()
stack_model = Stacking(
base_models=base_models,
meta_model=meta_model
)
逻辑回归
概念
逻辑回归(Logistic Regression)是一种用于二分类问题的线性模型。尽管名字中带有“回归”,但它其实是一种分类算法,旨在预测一个样本属于某一类别的概率。逻辑回归通过一个Sigmoid 函数将线性回归的输出映射到一个[0, 1]之间的概率值。通过设定一个阈值(通常为0.5),逻辑回归可以将概率值转化为具体的类别。
逻辑回归模型通常会受特征尺度影响,因此需要对数据进行标准化,特别是在使用正则化时。标准化可以使特征的尺度一致,防止某些特征对模型的贡献过大。
有种说法是神经网络模型(会在第六篇《神经网络》中具体介绍)中的每个神经元就是一个逻辑回归模型(处理方式相似),原因是(除了神经网络通过引入隐藏层和非线性激活函数,能够学习和表示复杂的非线性关系之外):
1. 激活函数:
- 在逻辑回归中,使用的是 sigmoid 函数,它将线性组合后的输入映射到 0 到 1 的区间。
- 神经网络中的神经元也常常使用类似的激活函数(如 sigmoid、ReLU、tanh 等),来处理输入并生成输出。
2. 输入到输出的线性组合:
- 在逻辑回归模型中,输入变量通过线性组合(权重与输入相乘并加上偏置)计算一个值,然后通过激活函数得到最终输出。
- 神经网络中的每个神经元也是将输入变量做线性组合,然后通过激活函数转换输出。
3. 参数优化:
- 逻辑回归和神经网络都使用类似的优化算法(如 梯度下降)来调整模型参数,最小化损失函数。
目标
其目标是预测二分类(或多分类)标签。逻辑回归的核心是通过逻辑函数(sigmoid 函数)(见下)将线性组合的自变量映射到0和1之间的概率值*。例如,如果预测概率大于0.5,则预测为类别1;否则预测为类别0。这种概率到类别的映射使得逻辑回归成为分类模型。常用于医疗诊断(患病与否)、垃圾邮件判断。逻辑回归还可以扩展为多分类问题,通常采用“多项式逻辑回归”或“一对多”策略,将二分类的逻辑回归推广到多个类别的分类。
超参数
- 正则化系数
C: 控制正则化强度,C值越小,正则化强度越大。正则化用于防止过拟合。 - 正则化方法
penalty: 常见的有l1(Lasso,L1正则)和l2(Ridge,L2正则)。(会在第五篇中具体介绍) - 最大迭代次数
max_iter: 控制算法的最大迭代次数。 - 求解器
solver: 指定求解逻辑回归模型的方法(如'liblinear','lbfgs','sag','saga'等)。
假设
- 样本与特征之间存在线性关系(尽管预测的是分类,底层假设仍是线性关系)。
- 每个样本的对数几率可以用特征的线性组合来表示。
- 样本独立,同一数据集中的样本彼此之间没有相关性。
缺点
对非线性问题表现不佳。数据量较大时表现也不佳。
算法原理
1. Sigmoid 函数:将线性回归的输出映射为 [0, 1] 的概率。

2. 损失函数:逻辑回归使用对数损失函数(log-loss 或 binary cross-entropy)。

面经:为什么逻辑回归使用对数损失而不是平方损失?
逻辑回归使用对数损失(Logistic Loss)而不是平方损失(Mean SquaredError)的主要原因是,对数损失函数能够更好地处理分类问题。对数损失函数可以将模型输出(即预测值)转换为概率值,并计算预测概率和实际标签之间的距离。而平方损失函数只适用于回归问题,其对于分类问题来说并不够合适。此外,对数损失函数具有凸性质,可以使得梯度下降在训练模型时更快地收敛。而平方损失函数不具有凸性质,容易受到离群点(outliers)的影响,收敛速度会变慢。因此,逻辑回归使用对数损失函数是更加合适的选择。
3. 梯度下降:通过梯度下降算法更新权重参数,以最小化损失函数。

逻辑回归的步骤
- 准备数据:加载数据集,进行数据预处理(如标准化)(在《3.数据预处理中有具体介绍》)。
- 定义模型:初始化逻辑回归模型,选择合适的超参数。
- 训练模型:使用训练数据训练模型,求解权重参数。
- 模型预测:使用模型对测试数据进行预测。
- 模型评估:计算模型的准确率、混淆矩阵等评估指标。
- 可视化结果:可视化分类决策边界。
代码
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
from matplotlib.colors import ListedColormap
# 生成二维分类数据集
X, y = make_classification(n_samples=300, n_features=2, n_informative=2, n_redundant=0, random_state=42, n_clusters_per_class=1)
# 分割训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
# 标准化数据
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)
# 创建并训练逻辑回归模型
model = LogisticRegression()
model.fit(X_train, y_train)
# 预测
y_pred = model.predict(X_test)
# 计算模型准确率
accuracy = model.score(X_test, y_test)
print(f"Model Accuracy: {accuracy * 100:.2f}%")
# 生成网格以进行决策边界可视化
x_min, x_max = X_train[:, 0].min() - 1, X_train[:, 0].max() + 1
y_min, y_max = X_train[:, 1].min() - 1, X_train[:, 1].max() + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.02), np.arange(y_min, y_max, 0.02))
# 预测整个网格上的分类结果
Z = model.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
# 绘制决策边界
plt.contourf(xx, yy, Z, alpha=0.8, cmap=ListedColormap(('orange', 'cornflowerblue')))
# 绘制训练集和测试集
plt.scatter(X_train[:, 0], X_train[:, 1], c=y_train, marker='o', label='Train Data')
plt.scatter(X_test[:, 0], X_test[:, 1], c=y_test, marker='x', label='Test Data')
plt.title('Logistic Regression Decision Boundary')
plt.legend()
plt.show()
输出:
Model Accuracy: 95.56%

对数几率回归
与逻辑回归相同,对数几率回归也是属于用于分类任务的线性模型,对于二分类任务(后续也可推广到多分类任务),我们只需将实值z转换为0/1值。
但是单位阶跃函数不连续,因此不单调可微(具体要求见本专栏《4.3回归--线性回归-广义线性回归》),不能直接使用。对数几率函数正是这样的一个常用替代函数。 
对数几率回归优点:
- 无需事先假设数据分布,直接对分类可行性建模,避免假设分布不准确带来的问题;
- 不仅预测类别,可得到“类别”的近似概率预测,对利用概率辅助决策的任务有效;
- 对率函数是任意阶可导的凸函数,有很好的的数学性质,可直接应用现有数值优化算法求取最优解。
可使用极大似然法、经典优化算法(牛顿法、梯度下降法)求解。(具体可见本专栏《4.3回归--线性回归-广义线性回归》)
二分类模型用于多分类任务
目标拆分策略主要有以下三种:
- 一对一:N个类别两两配对,N(N-1)/2 个二类任务。被预测最多的类别为最终类别。存储开销和测试时间大,训练时间短。
- 一对其余:某一类作为正例,其他反例,N 个二类任务。置信度最大类别作为最终类别。存储开销和测试时间小,训练时间长。
预测性能取决于具体数据分布,多数情况下两者差不多。 - 多对多:纠错输出码(Error Correcting Output Code, ECOC):

- ECOC编码对分类器错误有一定容忍和修正能力,编码越长、纠错能力越强。
- 对同等长度的编码,理论上来说,任意两个类别之间的编码距离越远,则纠错能力越强。
- 类别不平衡问题:

支持向量机(SVM,Support Vector Machine)
概念
是一种用于分类和回归分析的监督学习模型(在第二篇《ai模型基础》中有过介绍)。该模型若用于分类任务,尤其在高维空间中表现优越,适合复杂的决策边界。SVM 的核心思想是通过构造一个超平面,将不同类别的数据点进行分离。SVM 尝试找到一个最优超平面,使得两类数据的边界最大化,以增强模型的泛化能力。
对于分类边界函数,所有alpha不为0的x叫做支持向量,这些是真正发挥作用的数据点。非边界上的点alpha必然为0。支持向量机的解具有稀疏性。
分类间隔:指的是分隔超平面(即决策边界)到样本点的最小距离。支持向量机的最佳决策边界是使得两个类别的距离最大化的分离超平面。(把距离投影到法向量方向上)。间隔(Margin) M 为:,最大化间隔等同于最小化
。
与上文“对数几率回归”的优化目标相似,故性能也相似。
目标
SVM 的目标是找到一个超平面,将不同类别的样本点分隔开,并且尽量最大化分类边界的间隔(即支持向量与超平面的距离)。这样可以使模型在面对新数据时有更好的分类效果。
假设
SVM 假设数据是线性可分的,但通过引入核函数(kernel)(见下),使得SVM 也可以处理非线性可分的数据。
常见的应用场景
- 分类问题(如文本分类、图像分类等)
- 回归问题(SVM 也可以用于回归,称为 SVR)
- 异常检测(检测异常点)
数据标准化
SVM 对特征值的量纲非常敏感,通常需要标准化数据(第三篇《数据预处理》有过具体介绍),将特征值缩放到相同的范围(如 [0, 1] 或 [-1, 1])。这使得 SVM 能够更好地确定超平面的边界。
算法原理
1. 线性可分:

2. 优化问题:

- 转为对偶问题、拉格朗日乘子法、判断KKT条件、SMO算法,最后带入所有支持向量的边界的均值(x,y)获得更好的鲁棒性。(运筹与优化)
3. 软间隔:
最终模型也仅与支持向量有关。

,
,约束条件s.t.:
更一般的:min(结构化风险+经验风险)==》正则化:可理解为一种罚函数法。
4. 核函数:
如果数据是非线性可分的,SVM 使用核函数将数据映射到更高维的特征空间,使其变得线性可分。常见的核函数有:(不管是否是软间隔,都可以使用核函数,用法一样)


Mercer定理:任何半正定的函数都可以是核函数。
- 这是个充分条件而非充要条件,也有核函数是非正定的
- 一个n×n的实对称矩阵A被称为半正定矩阵,如果对于任意n维实向量x,都有
。如果等号只在x=0时成立,则称A为正定矩阵。
- 等价条件:判断一个矩阵是否半正定可以通过多种等价条件,例如所有特征值非负、所有主子式非负、存在矩阵B使得
等。
- 几何意义:从二次型的角度来看,半正定矩阵代表的二次型在n维空间中定义了一个凸锥。如果矩阵是正定的,这个凸锥就是一个严格凸的锥。
- 等价条件:判断一个矩阵是否半正定可以通过多种等价条件,例如所有特征值非负、所有主子式非负、存在矩阵B使得
面经:核函数的作用
- 提升维度:核函数将输入空间映射到高维空间,降低了计算复杂度。

- 线性化:核函数能够将输入空间中的样本点映射到高维空间,在高维空间线性可分,使得样本点能够被很好地分割。
- 正则化:核函数通过在高维空间中的映射,可以有效地控制决策边界的复杂度和宽度。宽度指的是分类间隔的大小,决策边界的宽度可以影响模型的泛化能力。宽度越宽,通常意味着更好的泛化能力,防止过拟合。
超参数
- C 参数:控制模型的惩罚强度。较大的 C 值意味着较少的容错,模型会更加严格地分类训练数据,但容易过拟合。
- 核函数:决定数据的映射方式,影响模型的表现。
- γ:在高斯径向基(RBF) 核中,γ 控制单个训练样本的影响范围。较大的 γ 值会导致模型更加复杂,可能过拟合;较小的 γ 值会导致欠拟合。
C的选择对于支持向量机模型的性能至关重要。通常,𝐶的取值范围会在以下几个数量级之间变化:
- 1. 小的𝐶值(如0.01,0.1):模型对误分类样本较为容忍,会有更大的间隔,但可能欠拟合训练数据。这适用于噪声较大的数据集,或当你希望模型具有更好的泛化能力时。
- 2. 适中的𝐶值(如1,10,100):这是比较常见的取值范围,可以通过交叉验证来选择一个合适的𝐶值。大多数情况下,适中的𝐶值能够在模型复杂度和泛化能力之间取得良好的平衡。
- 3. 大的𝐶值(如1000,10000):模型对误分类样本不容忍,间隔较小,更加严格地分类训练数据,容易过拟合。这适用于数据噪声较小且希望模型对训练集高度准确的情况。
SVM的分类
软间隔支持向量机(Soft Margin SVM)
数据中有一些噪音点,不要求把两类点完全分得开。允许样本点的分类间隔不相等(有些样本落在间隔带中,即这些点不满足优化问题的约束条件)。
硬间隔支持向量机(Hard Margin SVM)
要求样本点的分类间隔相同。
多类支持向量机(MCSVM)
需要推广(无现成的),允许样本点属于多个类别。
线性支持向量机(LSVM)
将输入空间中的样本点映射到高维空间,然后使用线性分类器进行分类。
非线性支持向量机(NUSVM)
将输入空间中的样本点映射到高维空间,然后使用非线性分类器进行分类。
最大边距支持向量机(MMSVM)
通过最大化间隔和最小化方差,使得支持向量的间隔最大化。
概率支持向量机(PSVM)
通过对输入空间中的样本点赋予概率,使得分类结果具有不确定性。
序列支持向量机(SSVM)
将输入序列映射到高维空间,然后使用支持向量机进行分类。
核序列支持向量机(KSSVM)
将输入序列映射到高维空间,然后使用核函数进行分类。
面经:感知机模型与SVM模型的异同
感知机是最早的神经网络模型之一,用于二分类任务。它的基本结构包括输入层、权重和激活函数。
共同点:
- 分类任务:两者都用于二分类任务。
不同点:
1. 目标
- 感知机:通过迭代更新权重,直到所有样本被正确分类。它不考虑最大化间隔的原则。
- SVM:旨在最大化分类间隔,寻找离决策边界最近的样本点(支持向量),从而增强模型的泛化能力。
2. 算法
- 感知机:简单的迭代算法,基于误分类样本更新权重。可以在某些情况下不收敛(例如数据线性不可分)。
- SVM:使用优化技术(如拉格朗日乘子法)(高等数学基础知识)解决最大化间隔的问题,确保找到最佳的决策边界,具有更好的鲁棒性。
常见步骤
- 数据标准化(重要)
- 选择核函数(线性、RBF、多项式等)
- 设置超参数(C 和 γ)
- 训练模型
- 评估模型性能(交叉验证、混淆矩阵等)
- 可视化决策边界(适用于二维数据)
代码
import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.svm import SVC
from sklearn.metrics import accuracy_score
# 生成二维分类数据
X, y = datasets.make_blobs(n_samples=100, centers=2, random_state=42, cluster_std=1.0)
# 分割数据集为训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
# 数据标准化
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)
# 创建SVM模型,使用RBF核
svm = SVC(kernel='rbf', C=1.0, gamma=0.5)
svm.fit(X_train, y_train)
# 预测并评估
y_pred = svm.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)
print(f"Accuracy: {accuracy:.2f}")
# 可视化决策边界和支持向量
def plot_svm_decision_boundary(X, y, model):
# 创建网格点
x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
xx, yy = np.meshgrid(np.linspace(x_min, x_max, 500), np.linspace(y_min, y_max, 500))
# 预测网格点的分类
Z = model.decision_function(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
# 绘制决策边界和间隔边界
plt.contour(xx, yy, Z, levels=[-1, 0, 1], linestyles=['--', '-', '--'], colors='k')
# 绘制支持向量
plt.scatter(model.support_vectors_[:, 0], model.support_vectors_[:, 1], s=100, facecolors='none', edgecolors='k', label="Support Vectors")
# 绘制数据点
plt.scatter(X[:, 0], X[:, 1], c=y, cmap='coolwarm', edgecolors='k')
plt.title("SVM Decision Boundary with Support Vectors")
plt.legend()
plt.show()
# 可视化SVM决策边界、支持向量及间隔边界
plot_svm_decision_boundary(X_train, y_train, svm)
输出:
Accuracy: 1.00

-
决策边界:代码中的
plt.contour函数绘制了三条线:- 中间的实线表示分类器的决策边界。
- 两侧的虚线表示支持向量到决策边界的距离,即间隔边界。
-
支持向量:用空心圆圈标注了支持向量,它们是距离决策边界最近的数据点。
-
SVM的决策依据:SVM 的决策依据是根据支持向量的位置构建超平面,然后尝试最大化支持向量到决策边界的距离(间隔)。
朴素贝叶斯(Naive Bayes)
概念
这是一类基于贝叶斯定理(概率论与数理统计基础知识)的简单而强大的分类算法。其“朴素”体现在它假设特征之间相互独立,不考虑特征之间的相关性。这一假设使得模型计算效率高,但在某些实际情况下可能与现实不符。
贝叶斯网络是一种有向无环图(DAG),其节点表示随机变量,边表示变量之间的条件依赖关系。贝叶斯网络通过联合概率分布来表示复杂的概率关系,并使用贝叶斯推理进行推断和预测。
- 对于高斯朴素贝叶斯,由于假设特征服从正态分布,通常不需要标准化数据。
- 对于多项式或伯努利朴素贝叶斯,数据无需标准化,因为它们处理的是离散或二元特征。

超参数
不同类型的朴素贝叶斯分类器有不同的参数:
- 高斯朴素贝叶斯(GaussianNB):假设特征服从正态分布,主要参数是平滑系数
var_smoothing,用于处理数值上的极小值。(概率论:最大似然估计、大数定理) - 多项式朴素贝叶斯(MultinomialNB):主要用于离散型数据,如文本分类,超参数包括
alpha,用于拉普拉斯平滑。 - 伯努利朴素贝叶斯(BernoulliNB):用于二元特征数据,也有类似的平滑参数
alpha。
非参数估计方法(贝叶斯估计,非朴素贝叶斯的内容)
v.s. 最大似然估计。
- 估计的目的:从样本集K= {x1, x2,…, xN}估计样本空间中任何一点的概率密度p(x)
- 基本方法:用某种核函数表示某一样本对待估计的密度函数的贡献,所有样本所作贡献的线性组合视作对某点概率密度p(x)的估计

即当需要分类(估计、学习)的数据不满足正态分布时,通常采用:
- Parzen窗法(核函数)

# 使用sklearn.neighbors.KernelDensity估计每个类别的概率密度,实现概率密度估计。 kde_models = { c: KernelDensity(kernel="gaussian", bandwidth=bandwidth).fit(X_train[feature][y_train == c].values.reshape(-1, 1)) for c in classes } #kernel="gaussian":采用高斯核函数。bandwidth=1.0:核带宽参数,用于平滑密度估计。 #带宽 bandwidth 参数会影响密度估计的平滑程度,较小的带宽会导致分布过于尖锐,较大的带宽会使分布过于平滑。 #较小带宽(如 0.5)适用于密集数据,较大带宽(如 2.0)适用于稀疏数据。 - kN-近邻法


参数估计方法:概率密度函数的形式已知,而表征函数的参数未知,通过训练数据估计。
- 最大似然函数:


- 贝叶斯估计:


- 贝叶斯学习:


除此之外,还涉及《概率论与数据统计》有关知识:
- 先验概率:根据大量统计出现某类事物的比例;
- 类条件概率:同一类事物的属性都有一定的变化范围, 在这些变化范围内的分布概率用函数形式表示,则称为类条件概率密度函数;
- 后验概率:样本在某种观测值条件下属于某种类别额概率。
目标
朴素贝叶斯的目标是通过特征 X 预测类别 C 的概率,即最大化后验概率 P(C∣X)。
贝叶斯分类器几种常用的决策准则有:
- 最小错误率准则
- 最小风险准则(乘上一个风险损失矩阵):最小风险Bayes决策准则下期望风险最小。
- 限定错误率的两类决策
- 最小最大的决策准则
假设
条件独立性假设:给定类别 C 时,特征 Xi 之间是相互独立的。该假设是“朴素”的核心,但在实际问题中往往不成立。
优缺点
优点
- 简单高效,适用于大规模数据集。
- 对缺失数据不敏感。
- 适用于多类分类问题。
缺点
- 特征之间的独立性假设在现实中很难成立。
- 对特征之间有强依赖关系的数据表现不好。
常见应用场景
- 文本分类(如垃圾邮件过滤、情感分析):多项式朴素贝叶斯广泛用于自然语言处理任务。
- 文档分类:可以根据词频来分类文档。
- 情感分析:用二元特征表示正面和负面情感,适合使用伯努利朴素贝叶斯。
算法
朴素贝叶斯根据贝叶斯定理计算出每个类别的后验概率,并选择后验概率最大的类别作为预测结果。 其核心步骤如下:
- 计算先验概率 P(C):通过训练集中每个类别的频率计算。
- 计算似然概率 P(X∣C):根据给定类别 C 下,每个特征 Xi 的概率。
- 应用贝叶斯定理 计算后验概率 P(C∣X),选择概率最大的类别。
代码
使用 高斯朴素贝叶斯(GaussianNB)对二维数据集进行分类,并展示决策边界以及贝叶斯原理的分类过程。
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.naive_bayes import GaussianNB
from matplotlib.colors import ListedColormap
# 生成二维分类数据集
X, y = make_classification(n_samples=100, n_features=2, n_informative=2, n_redundant=0, n_clusters_per_class=1, random_state=42)
# 分割数据集为训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
# 创建高斯朴素贝叶斯模型并训练
gnb = GaussianNB()
gnb.fit(X_train, y_train)
# 可视化决策边界
def plot_decision_boundary(X, y, model, title):
x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.02), np.arange(y_min, y_max, 0.02))
Z = model.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
plt.contourf(xx, yy, Z, alpha=0.8, cmap=ListedColormap(('orange', 'cornflowerblue')))
plt.scatter(X[:, 0], X[:, 1], c=y, edgecolors='k', marker='o', cmap=ListedColormap(('orange', 'cornflowerblue')))
plt.title(title)
plt.show()
# 绘制训练集的决策边界
plot_decision_boundary(X_train, y_train, gnb, "Naive Bayes Decision Boundary (Training Set)")

- 决策边界:根据贝叶斯定理生成的分类边界将数据点分为两类。可以是非线性的。
- 颜色表示类别:橙色和蓝色分别表示两类样本。模型根据概率分布选择类条件下最可能的分类结果。
贝叶斯(单特征)分类器代码手搓实现:
# 单特征贝叶斯分类器函数
def bayes_classifier_one(X_train, y_train, X_test, feature, priors):
"""
使用贝叶斯分类器进行单特征分类。
X_train: 训练集特征
y_train: 训练集标签
X_test: 测试集特征
feature: 使用的单特征
priors: 先验概率
"""
# 提取训练集的单特征
X_train_feature = X_train[feature] #X_训练中feature这一列的数据
classes = y_train.unique()
# 计算每个类别的均值和标准差
means = {c: round(X_train_feature[y_train == c].mean(), 4) for c in classes}
#{c: ... for c in classes}: 创建一个字典,键是类别(c),值是该类别的均值。
#X_train_feature 是一个 Pandas Series 或一列数据,索引与 y_train 完全对齐。使用布尔值序列 y_train == c 筛选时,Pandas 会筛选出 X_train_feature 中对应 True 的行。
# round(value, n):对value保留小数点后n位
stds = {c: round(X_train_feature[y_train == c].std(), 4) for c in classes}
# 决策规则记录
decision_rule = f"特征: {feature}, 均值: {means}, 标准差: {stds}, 先验概率: {priors}"
print(f"决策规则:【{decision_rule}】")
# 对测试集样本进行分类
predictions = [] #列表,储存预测结果值的列表
for test_sample in X_test[feature]:
class_probabilities = {}
for c in classes:
likelihood = norm.pdf(test_sample, means[c], stds[c]) # 类条件概率
#norm.pdf 是 SciPy 提供的正态分布概率密度函数,返回测试样本在该正态分布下的概率密度值,即类条件概率。
class_probabilities[c] = likelihood * priors[c] # 后验概率 ∝ 类条件概率 × 先验概率
# 选择最大后验概率对应的类别
predictions.append(max(class_probabilities, key=class_probabilities.get))
#class_probabilities是个字典,key=xxx.get返回某个类别的后验概率值
return predictions, decision_rule
手写朴素贝叶斯垃圾邮件分类:
def textParse(bigString):
"""
函数说明:接收一个字符串并将其解析为字符串列表
Parameters:
bigString - 字符串
Returns:
返回字符串列表(除了单个字母,例如大写的I,其它单词变成小写,同时过滤长度小于3的字符串)
"""
# 使用正则表达式将非字母和非数字的字符作为分隔符进行切分
listOfTokens = re.split(r'\W+', bigString)
# 过滤掉长度小于3的单词,并转换为小写
return [tok.lower() for tok in listOfTokens if len(tok) > 2]
def createVocabList(dataSet):
"""
函数说明: 将切分的实验样本词条整理成不重复的词条列表,也就是词汇表
Parameters:
dataSet - 整理的样本数据集
Returns:
vocabSet - 返回不重复的词条列表,也就是词汇表
"""
vocabSet = set()
for document in dataSet:
vocabSet = vocabSet.union(set(document))
print("vocabSet:", list(vocabSet))
return list(vocabSet)
def setOfWords2Vec(vocabList, inputSet):
"""
函数说明: 根据vocabList词汇表,将inputSet向量化,向量的每个元素为1或0
Parameters:
vocabList - createVocabList返回的列表
inputSet - 切分的词条列表
Returns:
returnVec - 文档向量,词集模型
"""
returnVec = [0] * len(vocabList)
for word in inputSet:
if word in vocabList:
returnVec[vocabList.index(word)] = 1
# print("returnVec:", returnVec)
return returnVec
def trainNB0(trainMatrix, trainCategory):
"""
函数说明:朴素贝叶斯分类器训练函数
Parameters:
trainMatrix - 训练文档矩阵,即setOfWords2Vec返回的returnVec构成的矩阵
trainCategory - 训练类别标签向量,即loadDataSet返回的classVec
Returns:
p0Vect - 非垃圾邮件的条件概率数组
p1Vect - 垃圾邮件的条件概率数组
pAbusive - 文档属于垃圾邮件的概率
"""
numTrainDocs = len(trainMatrix)
numWords = len(trainMatrix[0])
pAbusive = sum(trainCategory) / float(numTrainDocs) #计算(所有)邮件属于“垃圾邮件”的先验概率
# 初始化概率为1,实现拉普拉斯平滑
p0Num = np.ones(numWords)
p1Num = np.ones(numWords)
# 分母初始化为2(因为每个词初始为1,分母是2)
p0Denom = 2.0
p1Denom = 2.0
for i in range(numTrainDocs):
if trainCategory[i] == 1:
p1Num += trainMatrix[i]
p1Denom += sum(trainMatrix[i])
else:
p0Num += trainMatrix[i]
p0Denom += sum(trainMatrix[i])
# 计算条件概率,并取对数防止下溢
p1Vect = np.log(p1Num / p1Denom)
p0Vect = np.log(p0Num / p0Denom)
# print("p0Vect",p0Vect) # p0Vec:表示在 非垃圾邮件(ham) 类别下,每个词出现在邮件中的条件概率,即 𝑃(word∣ham)。长度=词汇表的长度
# print("p1Vect",p1Vect)
print("pAbusive",pAbusive)
return p0Vect, p1Vect, pAbusive
def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
"""
函数说明:朴素贝叶斯分类器分类函数
Parameters:
vec2Classify - 待分类的词条数组
p0Vec - 非垃圾邮件的条件概率数组
p1Vec - 垃圾邮件的条件概率数组
pClass1 - 文档属于垃圾邮件的概率
Returns:
0 - 属于非垃圾邮件
1 - 属于垃圾邮件
"""
# 将词向量与条件概率向量相乘(在对数空间中是相加)
p1 = sum(vec2Classify * p1Vec) + np.log(pClass1)
p0 = sum(vec2Classify * p0Vec) + np.log(1.0 - pClass1)
if p1 > p0:
return 1
else:
return 0
def loadEmails(path, label):
"""
函数说明: 读取指定目录下的所有邮件,并进行解析
Parameters:
path - 邮件所在目录路径
label - 邮件类别标签(0: ham, 1: spam)
Returns:
docList - 邮件词条列表
classList - 类别标签列表
"""
docList = []
classList = []
for fname in os.listdir(path):
if fname.endswith('.txt'):
with open(os.path.join(path, fname), 'r', encoding='latin-1') as f:
content = f.read()
words = textParse(content)
docList.append(words)
classList.append(label)
return docList, classList
def crossValidation(data, labels, k=10):
"""
函数说明: 实现k折交叉验证的数据划分,同时保留每个文档的原始索引
Parameters:
data - 数据集
labels - 数据标签
k - 折数
Returns:
foldData - 分割后的数据集列表,每个元素是一个包含(doc, label, index)的列表
"""
combined = list(zip(data, labels, range(len(data))))
random.shuffle(combined)
shuffledData, shuffledLabels, shuffledIndices = zip(*combined)
foldSize = len(data) // k
foldData = []
for i in range(k):
start = i * foldSize
end = (i + 1) * foldSize if i != k - 1 else len(data)
fold = list(zip(shuffledData[start:end], shuffledLabels[start:end], shuffledIndices[start:end]))
foldData.append(fold)
return foldData
def spamTestWithCrossValidation(k=10):
"""
函数说明: 使用k折交叉验证测试朴素贝叶斯分类器的准确性,并输出每一折的错误分类文件索引
Parameters:
k - 折数,默认为10
"""
# 数据集路径
hamPath = 'bayes_email/ham/'
spamPath = 'bayes_email/spam/'
# 读取邮件数据
hamDocs, hamLabels = loadEmails(hamPath, 0)
spamDocs, spamLabels = loadEmails(spamPath, 1)
# 合并数据
docList = hamDocs + spamDocs
classList = hamLabels + spamLabels
# 创建词汇表
vocabList = createVocabList(docList)
# 进行k折交叉验证
foldData = crossValidation(docList, classList, k)
errorRates = []
for i in range(k):
print(f"\n=== 第 {i+1} 折 ===")
# 准备训练集和测试集
testFold = foldData[i]
testSet = [doc for doc, label, idx in testFold]
testLabels = [label for doc, label, idx in testFold]
testIndices = [idx for doc, label, idx in testFold]
trainSet = []
trainLabels = []
for j in range(k):
if j != i:
fold = foldData[j]
trainSet += [doc for doc, label, idx in fold]
trainLabels += [label for doc, label, idx in fold]
# 将训练集转换为词向量
trainMat = []
for doc in trainSet:
trainMat.append(setOfWords2Vec(vocabList, doc))
trainMat = np.array(trainMat)
trainLabelsArr = np.array(trainLabels)
# 训练朴素贝叶斯分类器
p0V, p1V, pSpam = trainNB0(trainMat, trainLabelsArr)
# 测试集转换为词向量并分类
errorCount = 0
misclassified = []
for doc, label, idx in zip(testSet, testLabels, testIndices):
wordVector = np.array(setOfWords2Vec(vocabList, doc))
# p0Vect、p1Vect 和 vec2Classify 这些数组的长度都是与词汇表(vocabList)的长度相同,而不是与原始邮件的长度相同。
predicted = classifyNB(wordVector, p0V, p1V, pSpam)
if predicted != label:
errorCount += 1
misclassified.append((idx, label))
errorRate = float(errorCount) / len(testSet)
errorRates.append(errorRate)
print(f"第 {i+1} 折的错误率: {errorRate * 100:.2f}%")
if misclassified:
print("分类错误的文件索引及真实类别:")
for idx, label in misclassified:
print(f"文件索引: {idx}, 类别: {label}")
else:
print("没有分类错误的文件。")
averageErrorRate = sum(errorRates) / k
print(f"\n{k}折交叉验证的平均错误率: {averageErrorRate * 100:.2f}%")
if __name__ == '__main__':
spamTestWithCrossValidation(k=10)
-
利用贝叶斯分类器对文档进行分类时,要计算多个概率的乘积以获得文档属于某个类别的概率。如果其中有一个概率值为0,那么最后的乘积也为0。为降低这种影响,可以将所有词的出现数初始化为1, 并将分母初始化为2。这种做法就叫做拉普拉斯平滑(Laplace Smoothing),是比较常用的平滑方法。
-
下溢问题。由于太多很小的数相乘会导致下溢问题。两个小数相乘,越乘越小,这样就造成了下溢出。在程序中,许多很小的数相乘,最后四舍五入计算结果可能就变成0。为了解决这个问题,对乘积结果取自然对数。通过求对数可以避免下溢出或者浮点数舍入导致的错误。
贝叶斯分类器vs朴素贝叶斯
朴素贝叶斯分类器是贝叶斯分类器的一个子类,具有更强的假设(特征独立性)和更高的计算效率。在实际应用中,尽管朴素贝叶斯的独立性假设较为简单,但由于其高效性和在许多任务中的良好表现,仍然被广泛使用。而贝叶斯分类器则适用于那些需要建模复杂特征关系的场景,但其应用相对较少且计算成本较高。


总结
在机器学习中,常见的分类算法包括 KNN、决策树、集成模型(如 Bagging 和 Boosting)、逻辑回归、SVM(支持向量机)和朴素贝叶斯。KNN 通过计算距离对样本进行分类,适合小数据集,但在大数据集上效率较低。决策树 利用特征划分数据,简单直观,但易过拟合。集成模型 通过多个弱学习器的组合提升模型性能,其中 Bagging 通过并行方式降低方差,Boosting 通过序列方式降低偏差。逻辑回归 是一种线性模型,常用于二分类任务,适合特征和输出间具有线性关系的数据。SVM 通过寻找最大化分类间隔的超平面进行分类,适用于高维数据。朴素贝叶斯 基于贝叶斯定理,简单高效,适合文本分类等独立特征场景。
这些算法各有特点和适用场景,选择时应根据数据的特征、规模和任务需求综合考虑。
更多推荐

所有评论(0)