【学习笔记&Python代码】统计学习方法第二版 李航

本文介绍了统计学习方法的基础概念,包括有监督学习(分类和回归)、无监督学习(聚类和降维)、半监督学习以及强化学习。详细阐述了过拟合和欠拟合的现象及其影响,并通过感知机模型的实现解释了学习过程。此外,还探讨了k近邻(k-NN)方法,特别是k值选择的影响和kd树的构建与应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

《统计学习方法》和《机器学习》这两本书,大概是做科研的小伙伴都避不开的必读书吧。笔者在很久前就已经学过该书了,但是因为先前学完没有做一个详细的整理总结。然后最近实验室招新,需要带一带小朋友学习这本书,故重温该书且在此整理记录,望对大家有所帮助!

第一章 统计学习及监督学习概论

🍕 1 有监督学习

指对数据的若干特征与若干标签(类型)之间的关联性进行建模的过程; 只要模型被确定,就可以应用到新的未知数据上。

这类学习过程可以进一步分为「分类」(classification)任务和「回归」(regression)任务。

在分类任务中,标签都是离散值

而在回归任务中,标签都是连续值

🍔 2 无监督学习

指对不带任何标签的数据特征进行建模,通常被看成是一种“让数据自己介绍自己” 的过程。

这类模型包括「聚类」(clustering)任务和「降维」(dimensionality reduction)任务。

聚类算法可以将数据分成不同的组别,而降维算法追求用更简洁的方式表现数据。

🍟 3 半监督学习

另外,还有一种半监督学习(semi-supervised learning)方法,介于有监督学习和无监督学习之间。通常可以在数据不完整时使用。

🌭 4 强化学习

强化学习不同于监督学习,它将学习看作是试探评价过程,以"试错" 的方式进行学习,并与环境进行交互已获得奖惩指导行为,以其作为评价。

此时系统靠自身的状态和动作进行学习,从而改进行动方案以适应环境。

(提示:半监督学习和强化学习比较偏向于深度学习,因此在后续文章中也不会再提到。)

🍿 5 输入/输出空间、特征空间

在上面的场景中,每一杯酒称作一个「样本」,十杯酒组成一个样本集。

酒精浓度、颜色深度等信息称作「特征」。这十杯酒分布在一个「多维特征空间」中。

进入当前程序的“学习系统”的所有样本称作「输入」,并组成「输入空间」。

在学习过程中,所产生的随机变量的取值,称作「输出」,并组成「输出空间」。

在有监督学习过程中,当输出变量均为连续变量时,预测问题称为回归问题;当输出变量为有限个离散变量时,预测问题称为分类问题。

🥓 6 过拟合与欠拟合

先来一句易懂的话:

  • 过拟合简单来说就是模型把训练集的东西学得太精了,对未知的数据效果却很差(打个比方就是考前你练得很不错,给啥做过的题都说得出答案,但是考试的时候碰到新题了就做得很差)
  • 欠拟合就是模型学得很差,打个比方就是考前有题给你练,你也练了,但就是练不会,学不懂。

下面是具体介绍。

当假设空间中含有不同复杂度的模型时,就要面临模型选择(model selection)的问题。

我们希望获得的是在新样本上能表现得很好的学习器。为了达到这个目的,我们应该从训练样本中尽可能学到适用于所有潜在样本的"普遍规律",

我们认为假设空间存在这种"真"模型,那么所选择的模型应该逼近真模型。

拟合度可简单理解为模型对于数据集背后客观规律的掌握程度,模型对于给定数据集如果拟合度较差,则对规律的捕捉不完全,用作分类和预测时可能准确率不高。

换句话说,当模型把训练样本学得太好了的时候,很可能已经把训练样本自身的一些特点当作了所有潜在样本的普遍性质,这时候所选的模型的复杂度往往会比真模型更高,这样就会导致泛化性能下降。这种现象称为过拟合(overfitting)。可以说,模型选择旨在避免过拟合并提高模型的预测能力。

与过拟合相对的是欠拟合(underfitting),是指模型学习能力低下,导致对训练样本的一般性质尚未学
好。

在这里插入图片描述
虚线:针对训练数据集计算出来的分数,即针对训练数据集拟合的准确性。

实线:针对交叉验证数据集计算出来的分数,即针对交叉验证数据集预测的准确性。

  1. 左图:一阶多项式,欠拟合;
    ◼ 训练数据集的准确性(虚线)和交叉验证数据集的准确性(实线)靠得很近,总体水平比较高。
    ◼ 随着训练数据集的增加,交叉验证数据集的准确性(实线)逐渐增大,逐渐和训练数据集的准确性(虚线)靠近,但其总体水平比较低,收敛在 0.88 左右。
    ◼ 训练数据集的准确性也比较低,收敛在 0.90 左右。
    ◼ 当发生高偏差时,增加训练样本数量不会对算法准确性有较大的改善。
  2. 中图:三阶多项式,较好地拟合了数据集;
    ◼ 训练数据集的准确性(虚线)和交叉验证数据集的准确性(实线)靠得很近,总体水平比较高。
  3. 右图:十阶多项式,过拟合。
    ◼ 随着训练数据集的增加,交叉验证数据集的准确性(实线)也在增加,逐渐和训练数据集的准确性 (虚线)靠近,但两者之间的间隙比较大。
    ◼ 训练数据集的准确性很高,收敛在 0.95 左右。
    ◼ 交叉验证数据集的准确性值却较低,最终收敛在 0.91 左右。

从图中我们可以看出,对于复杂数据,低阶多项式往往是欠拟合的状态,而高阶多项式则过分捕捉噪声数据的分布规律,而噪声之所以称为噪声,是因为其分布毫无规律可言,或者其分布毫无价值,因此就算高阶多项式在当前训练集上拟合度很高,但其捕捉到的无用规律无法推广到新的数据集上。因此该模型在测试数据集上执行过程将会有很大误差,即模型训练误差很小,但泛化误差很大。

第二章 感知机

这里先附上复现代码。

class Model:
    def __init__(self):
        self.w = np.ones(len(data[0])-1, dtype=np.float32)
        self.b = 0
        self.l_rate = 0.1
        # self.data = data

    def sign(self, x, w, b):
        y = np.dot(x, w) + b
        return y

    # 随机梯度下降法
    def fit(self, X_train, y_train):
        is_wrong = False
        while not is_wrong:
            wrong_count = 0
            for d in range(len(X_train)):
                X = X_train[d]
                y = y_train[d]
                if y * self.sign(X, self.w, self.b) <= 0:
                    self.w = self.w + self.l_rate*np.dot(y, X)
                    self.b = self.b + self.l_rate*y
                    wrong_count += 1
            if wrong_count == 0:
                is_wrong = True
        return 'Perceptron Model!'

    def score(self):
        pass

下载训练数据:

import pandas as pd
import numpy as np
from sklearn.datasets import load_iris
import matplotlib.pyplot as plt

iris = load_iris()
df = pd.DataFrame(iris.data, columns=iris.feature_names)
df['label'] = iris.target

df.columns = ['sepal length', 'sepal width', 'petal length', 'petal width', 'label']
df.label.value_counts()

plt.scatter(df[:50]['sepal length'], df[:50]['sepal width'], label='0')
plt.scatter(df[50:100]['sepal length'], df[50:100]['sepal width'], label='1')
plt.xlabel('sepal length')
plt.ylabel('sepal width')
plt.legend()
plt.show()

data = np.array(df.iloc[:100, [0, 1, -1]])

X, y = data[:,:-1], data[:,-1]

y = np.array([1 if i == 1 else -1 for i in y])

perceptron = Model()
perceptron.fit(X, y)

在这里插入图片描述
测试:

x_points = np.linspace(4, 7,10)
y_ = -(perceptron.w[0]*x_points + perceptron.b)/perceptron.w[1]
plt.plot(x_points, y_)

plt.plot(data[:50, 0], data[:50, 1], 'bo', color='blue', label='0')
plt.plot(data[50:100, 0], data[50:100, 1], 'bo', color='orange', label='1')
plt.xlabel('sepal length')
plt.ylabel('sepal width')
plt.legend()
plt.show()

在这里插入图片描述

第三章 k近邻法

定义

近邻法 (k-nearest neighbor,k -NN) 种基本分类与回归方法。本书只讨论分类问题中的 k近邻法。 近邻法的输入为实例的特征向量,对应于特征空间的点:输出为实例的类别,可以取多类。 近邻法假设给定 个训练数据集, 其中的实例类别己定。分类时,对新的实例,根据其 个最近邻的训练实例的类别,通过多数表决等方式进行预测 因此 近邻法不具有显式的学习过程。

多数表决简单理解即为哪种类别数量多对应的分类结果就是哪种。

而不显式学习简单理解即为,不像感知机等方法需要预先训练好一个模型,k近邻是每次分类就利用一遍训练集里的数据,

k近邻算法

大致理解的话,先是输入训练集:
在这里插入图片描述
输出即为实例x所属的类别。

算法流程大致如下:
(1)先是计算x与训练集中给定数据的距离度量最小的k个数据(这里的距离度量通常是求lp距离,一般为l2,即欧式距离)
(2)根据决策方法即多数表决方法来求出实例x所属类别:
在这里插入图片描述

k值的选择

大家肯定都能想到k值的选择必然会对结果产生极大影响,那么k值取大取小对应的会有什么影响呢?
选小了:学习的近似误差会减小,但是估计误差会增大。什么意思呢?就是我对于距离实例x的近似点会非常敏感,那如果刚好我的近似点就是误差点的话,那么我的结果就会出错。这样的话便容易出现过拟合。

选大了:对应的可以减小我的估计误差,但是相对的那些原本不那么近似的点也会被我纳入考量,使得发生错误。同时相对的模型也会变得简单。往极端了想,k值等于我训练集的大小,那岂不是巨简单,直接给出我训练集里面类别占比最大的最可以了,但这样肯定是有很大问题的。

在应用中 一般取 比较小的数值,且通常采用交叉验证法来选取最优的k值。

kd树

书本里面最开始讲得非常生硬,这里我打算结合具体题目来讲。

先附一下题目:

在这里插入图片描述
上图其实还没有画完整,我先给它补全。
在这里插入图片描述

下面介绍解题步骤:
(1)先建立一个kd树,注意这里的kd树是二叉树。
最开始选哪一个作为根节点呢?聪明的小朋友可以发现A是上述的根节点,选它作为根节点的原因是因为它的y坐标大小是所有y坐标大小的中位数。所以我们每次分割的时候都是选其中位数的点(如果中位数的点有两个的话就任一都可)。同样的道理,为啥第二条线不是经过的G或者E,而是经过C(即选C点为子结点),因为C的横坐标大小是在gce点中的中位数。以此类推。
提醒一下啊,我们可以先往x轴方向分割,也可以往y轴方向切割,然后x和y轴方向交替切割。
(2)选择一个叶节点作为“当前最近点”,叶节点上图一共有四个,分别是g、e、f和d点。那为啥选d呢?
是这样子的,实例s点是不是比根节点a的纵坐标要小(之所以比纵坐标是因为这里是从纵坐标开始切割的),既然小的话那我们就选A下面这块区域,这块区域显然是由点B总管的,那这个时候同理再与点B比较一下横坐标,是不是要比点B大,那大的话就选右边这块区域,这个时候显然就只剩下点D了,所以确定点D为当前最近点。
(3)确定完某一个叶节点为当前最近点后,开始递归向上回退,退到父节点B,看看父节点B会不会更近,好的不会,那再看看父节点B它的另一个子区域有没有点比当前最近点还近。具体怎么检查呢,我们以实例点S与当前最近点D的距离为半径画个圆,看看这个区域会不会跟这个圆相交,不会的话就直接放弃,会的话移动到该结点检查是否需要更新当前最近点。
好的,经过上述我们发现点B管的区域也没有用了,那我们再回退到点B的父节点,重复上述工作:即看点A会不会更近,不会,那就看点A的另一个子结点C有没有跟我相交,有,那移动到点C,看看与其相交的右区域有无点包含在圆里面,即距离更近,有,ok,完事。

实现代码

附下实现kd树实现代码:

import numpy as np
from math import sqrt
import pandas as pd
from sklearn.datasets import load_iris
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split

iris = load_iris()
df = pd.DataFrame(iris.data, columns=iris.feature_names)
df['label'] = iris.target
df.columns = ['sepal length', 'sepal width', 'petal length', 'petal width', 'label']

data = np.array(df.iloc[:100, [0, 1, -1]])
train, test = train_test_split(data, test_size=0.1)
x0 = np.array([x0 for i, x0 in enumerate(train) if train[i][-1] == 0])
x1 = np.array([x1 for i, x1 in enumerate(train) if train[i][-1] == 1])


def show_train():
    plt.scatter(x0[:, 0], x0[:, 1], c='pink', label='[0]')
    plt.scatter(x1[:, 0], x1[:, 1], c='orange', label='[1]')
    plt.xlabel('sepal length')
    plt.ylabel('sepal width')


class Node:
    def __init__(self, data, depth=0, lchild=None, rchild=None):
        self.data = data
        self.depth = depth
        self.lchild = lchild
        self.rchild = rchild


class KdTree:
    def __init__(self):
        self.KdTree = None
        self.n = 0
        self.nearest = None

    def create(self, dataSet, depth=0):
        if len(dataSet) > 0:
            m, n = np.shape(dataSet)
            self.n = n - 1
            axis = depth % self.n
            mid = int(m / 2)
            dataSetcopy = sorted(dataSet, key=lambda x: x[axis])
            node = Node(dataSetcopy[mid], depth)
            if depth == 0:
                self.KdTree = node
            node.lchild = self.create(dataSetcopy[:mid], depth+1)
            node.rchild = self.create(dataSetcopy[mid+1:], depth+1)
            return node
        return None

    def preOrder(self, node):
        if node is not None:
            print(node.depth, node.data)
            self.preOrder(node.lchild)
            self.preOrder(node.rchild)

    def search(self, x, count=1):
        nearest = []
        for i in range(count):
            nearest.append([-1, None])
        self.nearest = np.array(nearest)

        def recurve(node):
            if node is not None:
                axis = node.depth % self.n
                daxis = x[axis] - node.data[axis]
                if daxis < 0:
                    recurve(node.lchild)
                else:
                    recurve(node.rchild)

                dist = sqrt(sum((p1 - p2) ** 2 for p1, p2 in zip(x, node.data)))
                for i, d in enumerate(self.nearest):
                    if d[0] < 0 or dist < d[0]:
                        self.nearest = np.insert(self.nearest, i, [dist, node], axis=0)
                        self.nearest = self.nearest[:-1]
                        break

                n = list(self.nearest[:, 0]).count(-1)
                if self.nearest[-n-1, 0] > abs(daxis):
                    if daxis < 0:
                        recurve(node.rchild)
                    else:
                        recurve(node.lchild)

        recurve(self.KdTree)

        knn = self.nearest[:, 1]
        belong = []
        for i in knn:
            belong.append(i.data[-1])
        b = max(set(belong), key=belong.count)

        return self.nearest, b


kdt = KdTree()
kdt.create(train)
kdt.preOrder(kdt.KdTree)

score = 0
for x in test:
    input('press Enter to show next:')
    show_train()
    plt.scatter(x[0], x[1], c='red', marker='x')  # 测试点
    near, belong = kdt.search(x[:-1], 5)  # 设置临近点的个数
    if belong == x[-1]:
        score += 1
    print("test:")
    print(x, "predict:", belong)
    print("nearest:")
    for n in near:
        print(n[1].data, "dist:", n[0])
        plt.scatter(n[1].data[0], n[1].data[1], c='green', marker='+')  # k个最近邻点
    plt.legend()
    plt.show()

score /= len(test)
print("score:", score)

结束语

正在不断更新中。。。

以后我会在博客记录自己学习《统计学习方法》第二版这本书的笔记,其实也就是我自己认为比较重要或者有用的内容,以及部分python代码的实现。

由于博主能力有限,博文中提及的信息,也难免会有疏漏之处。希望发现疏漏的朋友能热心指出其中的错误,以便下次修改时能以一个更完美更严谨的样子,呈现在大家面前。同时如果有更好的方法也请不吝赐教。

如果有什么相关的问题,也可以关注评论留下自己的问题,我会尽量及时发送!

评论 23
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

报告,今天也有好好学习

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值