笔记

IMG_20250627_175733
IMG_20250627_175736
IMG_20250627_175740
IMG_20250627_175743
IMG_20250627_175748
IMG_20250627_175752
IMG_20250627_175755
IMG_20250627_175759

方差与偏差

方差(Variance)和偏差(Bias)是机器学习中衡量模型性能的两个核心概念,它们共同构成了偏差-方差权衡(Bias-Variance Tradeoff)的基础框架。以下是两者的定义与区别:

1. 偏差(Bias)

  • 定义:偏差是指模型预测的期望值与真实值之间的差异。它反映了模型本身的拟合能力,即是否能够准确捕捉数据中的规律。

2. 方差(Variance)

  • 定义:方差是指模型在不同训练数据集下预测结果的波动程度。它衡量了模型对训练数据中噪声或微小变化的敏感性。

3. 如何降低偏差与方差

目标 方法 示例
降低偏差 增加模型复杂度(如更多特征、更深的神经网络)、减少正则化强度 使用多项式回归替代线性回归
降低方差 增加训练数据、引入正则化(L1/L2)、使用集成方法(如 Bagging、Boosting) 随机森林(Bagging)降低决策树的方差

4. 总结

  • 偏差关注模型是否能准确拟合数据(学习能力),而方差关注模型对数据波动的稳定性(泛化能力)。
  • 实际应用中需通过交叉验证、正则化或集成学习等技术平衡两者的关系。

监督学习与无监督学习

以下是关于监督学习与无监督学习的核心区别总结:

1. 监督学习(Supervised Learning)

任务类型
- 分类(Classification):预测离散类别标签(如垃圾邮件/非垃圾邮件)。
- 回归(Regression):预测连续数值标签(如房价预测)。

特点
- 需要带标签的样本(Labeled Data),即每个训练样本都有明确的输入 $ x $ 和输出 $ y $。
- 模型通过学习输入与标签之间的映射关系进行预测。

2. 无监督学习(Unsupervised Learning)

任务类型

  • 聚类(Clustering):将样本划分为具有相似特征的群体(如客户分群)。
  • 降维(Dimensionality Reduction):压缩数据维度同时保留关键信息(如PCA)。

特点
- 仅需无标签的样本(Unlabeled Data),无需预先定义输出目标。
- 模型自主挖掘数据内在结构或分布规律。

贝叶斯分类

贝叶斯分类器

贝叶斯决策论

本质思想:寻找合适的参数使得「当前的样本情况发生的概率」最大。

又由于假设每一个样本相互独立(概率条件理想的情况下),因此可以用连乘的形式表示上述概率,当然由于概率较小导致连乘容易出现浮点数精度损失,因此尝尝采用取对数的方式来避免「下溢」问题。也就是所谓的「对数似然估计」方法。

在已知样本特征 $ $ 的条件下,选择分类结果 $ c_i $,使得分类的期望损失(Risk)最小

**(1) 损失函数 $ _{ij} $**

  • 定义:$ _{ij} $ 是将真实类别为 $ c_j $ 的样本误分类为 $ c_i $ 所产生的损失。
    • 例如:
      • 在医学诊断中,若 $ c_1 $ 表示“患病”,$ c_2 $ 表示“未患病”:
        • $ _{21} c_1 c_2 $)的损失(可能更高)。
        • $ _{12} c_2 c_1 $)的损失(可能较低)。

(2) 条件风险(单个样本的期望损失)

对于给定样本 $ $,若将其分类为 $ c_i  * * * *$ R(c_i | ) = {j=1}^N {ij} P(c_j | ) $$ - 含义:在已知 $ $ 的情况下,分类为 $ c_i $ 的平均损失。 - 推导: - $ P(c_j | ) $:样本 $ $ 真实属于 $ c_j $ 的后验概率。 - $ {ij} $:若真实类别是 $ c_j $,但被分到 $ c_i $,则产生损失 $ {ij} $。 - 因此,总期望损失是所有可能真实类别的加权和(权重为后验概率)。

(3) 总体风险

对于整个数据集,分类器 $ h() $ 的总体风险为: R(h) = 𝔼x[R(h(x)|x)] = ∫R(h(x)|x)p(x)dx - 含义:所有样本的平均条件风险。h为分类器(模型) - 目标:找到使 $ R(h) $ 最小的分类器 $ h() $。

贝叶斯决策规则

根据上述定义,贝叶斯决策论的分类规则是: > 对于样本 $ $,选择使其条件风险 $ R(c_i | ) $ 最小的类别 $ c_i $ 作为预测结果。

即: $$ h^*(\mathbf{x}) = \arg\min_{c_i} R(c_i | \mathbf{x}) = \arg\min_{c_i} \sum_{j=1}^N \lambda_{ij} P(c_j | \mathbf{x}) $$

特殊情况:0-1 损失函数
当所有误分类的损失相同(即 $ {ij} = 1 $ 对于 $ i j {ii} = 0 ) * *0 − 1 * *:$ _{ij} =

R(c_i | ) = {j i} P(c_j | ) = 1 - P(c_i | ) $$ 原因:概率之和为 1:$ {j=1}^N P(c_j | ) = 1 $,因此 $ _{j i} P(c_j | ) = 1 - P(c_i | ) $。

此时,最小化风险等价于最大化后验概率,即: h*(x) = arg maxciP(ci|x) 这正是传统贝叶斯分类器的决策规则。

即在x样本的情况下,分类正确的概率最大

后验概率与先验概率

后验概率

后验概率(Posterior Probability)是贝叶斯理论中的核心概念,指的是在观察到新证据(数据)后,对事件发生概率的修正 。 其本质是:

“已知结果(数据),反推原因(类别或参数)的概率”

已知结果(数据)B,反推最可能的原因A(后验概率 P(AB) )

先验概率(Prior Probability)

先验概率是贝叶斯统计中的核心概念,指的是在观察到新数据之前,对某一事件或假设的概率估计。它是基于已有知识、经验或假设得出的初始概率,后续会通过新数据更新为更准确的后验概率

1. 核心定义

  • 数学表达
    P(A)
    • $ P(A) $:事件 $ A $ 的先验概率。
    • 例如:$ A $ 表示“某人患有某种疾病”,则 $ P(A) $ 是该疾病的已知发病率(在未进行检测前的概率)。
  • 与后验概率的区别
    • 先验概率:$ P(A) $,在无新数据时的概率。
    • 后验概率:$ P(A|B) $,在观察到数据 $ B $ 后更新的概率(通过贝叶斯定理计算)。

2. 直观理解

(1) 类比:医学诊断

  • 先验概率:某种疾病的已知发病率(如 1%)。
  • 新数据:患者接受检测,结果为阳性。
  • 后验概率:结合发病率和检测结果,计算实际患病的概率(如 8.7%,参考贝叶斯定理的经典医学测试案例)。

生成式模型和判别式模型

核心区别
模型类型 建模目标 数学表达
判别式模型 直接建模 $ P(c ) $
生成式模型 先建模联合概率 $ P(, c) $,再推导 $ P(c ) $
详细解释

1. 判别式模型(Discriminative Model)

  • 目标:直接学习从输入 $ $ 到标签 $ c $ 的映射关系。
  • 数学本质:建模条件概率 $ P(c|) $,即“已知特征 $ $,预测类别 $ c $”。
  • 特点
    • 不关心数据本身的分布,只关注分类边界。
    • 例如:逻辑回归、支持向量机(SVM)、神经网络等。

2. 生成式模型(Generative Model)

  • 目标:先学习数据的生成过程,即联合概率 $ P(, c) $,再通过贝叶斯定理推导条件概率 $ P(c|) $。

  • 数学步骤

    1. 建模 $ P(|c) $(特征在类别 $ c $ 下的分布)和 $ P(c) $(类别先验)。
    2. 根据贝叶斯定理计算后验概率: $$ P(c|\mathbf{x}) = \frac{P(\mathbf{x}|c)P(c)}{P(\mathbf{x})} $$
    3. 选择使 $ P(c|) $ 最大的类别作为预测结果。
示例:二分类问题

假设我们要判断一封邮件是否为垃圾邮件($ c=spam $ 或 $ ham $)。

判别式模型(逻辑回归)

直接建模: $$ P(spam|\mathbf{x}) = \frac{1}{1 + e^{-(w^T \mathbf{x} + b)}} $$ 若 $ P(spam|) > 0.5 $,则判定为垃圾邮件。

生成式模型(朴素贝叶斯)

  1. 建模联合概率: P(x, spam) = P(spam)∏iP(wordi|spam) P(x, ham) = P(ham)∏iP(wordi|ham)
  2. 计算后验概率: $$ P(spam|\mathbf{x}) = \frac{P(\mathbf{x}|spam)P(spam)}{P(\mathbf{x})} $$ $$ P(ham|\mathbf{x}) = \frac{P(\mathbf{x}|ham)P(ham)}{P(\mathbf{x})} $$
  3. 选择概率更大的类别。

生成式模型的建模思路

根据概率论的基本定义: $$ P(c|\mathbf{x}) = \frac{P(\mathbf{x}, c)}{P(\mathbf{x})} $$ - 含义: - $ P(, c) $:联合概率,表示特征 $ $ 和类别 $ c $ 同时发生的概率。 - $ P() $:边缘概率(证据),表示特征 $ $ 出现的概率,用于归一化。

根据贝叶斯定理,联合概率 $ P(, c) $ 可以分解为: P(x, c) = P(c) ⋅ P(x|c) 其中: - $ P(c) $:类先验概率(Prior Probability),表示类别 $ c $ 在数据中的整体占比。 - $ P(|c) $:似然度(Likelihood),表示在类别 $ c $ 下,特征 $ $ 出现的概率。

将上述分解代入条件概率公式,得到: $$ P(c|\mathbf{x}) = \frac{P(c) \cdot P(\mathbf{x}|c)}{P(\mathbf{x})} $$ 产生问题:

在贝叶斯分类中,需要计算联合概率 P(xc) ,即在类别 c 下,特征向量 x=(x1,x2,…,*x**d) 的条件概率。 若直接建模联合概率,需估计 d* 个特征的所有可能组合的概率。例如:

  • 若每个特征有 k 个取值,类别数为 K ,则需要估计 K⋅*k**d* 个参数。
  • 当特征维度 d 很大时(如文本分类中成千上万的词汇),参数数量呈指数级增长,导致计算不可行(维度灾难 )。

举例:

  • 低维空间 :假设只有 2 个特征(如“免费”和“中奖”),每个特征取值为 0 或 1,则特征空间共有 22=4 个可能的组合(即四个格子)。
    • 如果有 100 封邮件,每个格子平均有 25 封邮件(数据较密集)。
  • 高维空间 : 当特征维度增加到 d=10,000 时,特征空间的组合数是 210,000 ,远大于宇宙中原子的数量(约 1080 )。
    • 即使有 100 万封邮件,每个组合几乎都是空的(数据极度稀疏)。

结果 : 在高维空间中,训练数据无法覆盖所有可能的特征组合,导致模型无法可靠估计联合概率 P(x∣c) 。

因此产生属性条件独立性假设

朴素贝叶斯分类器

朴素贝叶斯分类器的核心思想是通过贝叶斯定理属性条件独立性假设来简化计算,从而高效地进行分类。

属性条件独立性假设

朴素贝叶斯的核心假设是:在已知类别 $ c $ 的条件下,所有属性(特征)之间相互独立
因此,联合概率 $ P(|c) $ 可以分解为各属性独立概率的乘积: $$ P(\mathbf{x}|c) = \prod_{i=1}^d P(x_i|c) $$ 其中 $ d $ 是特征的数量,$ x_i $ 是第 $ i $ 个特征的取值。

将此代入贝叶斯公式: $$ P(c|\mathbf{x}) = \frac{P(c) \cdot \prod_{i=1}^d P(x_i|c)}{P(\mathbf{x})} $$

为何可以忽略 $ P() $?

在分类任务中,我们的目标是比较不同类别 $ c $ 的后验概率 $ P(c|) $,并选择最大值。由于 $ P() $ 对所有类别来说是相同的常量(与类别无关),因此在最大化过程中可以忽略: $$ \arg\max_{c} P(c|\mathbf{x}) = \arg\max_{c} \left[ \frac{P(c) \cdot \prod_{i=1}^d P(x_i|c)}{P(\mathbf{x})} \right] = \arg\max_{c} \left[ P(c) \cdot \prod_{i=1}^d P(x_i|c) \right] $$ 这就是公式中 $ P() $ 被省略的原因。

在比较的过程中,分母相同,可以忽略

朴素贝叶斯的最终决策规则

简化后的决策规则为: $$ h_{nb}(\mathbf{x}) = \arg\max_{c} \left[ P(c) \cdot \prod_{i=1}^d P(x_i|c) \right] $$ 即: - 计算每个类别的先验概率 $ P(c) $。 - 计算每个特征在该类别下的条件概率 $ P(x_i|c) $。 - 将这些概率相乘,选择乘积最大的类别作为预测结果。

类先验概率 $ P(c) $ 的估计方法

基于大数定律 $$ P(c) = \frac{|D_c|}{|D|} $$ - 符号含义: - $ D $:训练集,包含所有样本。 - $ D_c $:训练集中类别为 $ c $ 的样本子集。 - $ |D_c| $:类别 $ c $ 的样本数量。 - $ |D| $:训练集总样本数量。

  • 直观解释: 类先验概率等于该类别样本数占总样本数的比例。
条件概率 $ P(x_i | c) $ 的估计方法

在生成式模型(如朴素贝叶斯分类器)中,条件概率 $ P(x_i | c) $ 表示在类别 $ c $ 下,第 $ i $ 个属性取值为 $ x_i $ 的概率。根据属性类型(离散或连续),其估计方法不同:

1. 离散属性的条件概率估计

公式$$ P(x_i | c) = \frac{|D_{c,x_i}|}{|D_c|} $$ - 符号含义: - $ D_c $:训练集中类别为 $ c $ 的样本集合。 - $ D_{c,x_i} D_c $ 中第 $ i $ 个属性取值为 $ x_i $ 的样本子集。 - $ |D_{c,x_i}| D_{c,x_i} $ 的样本数量。 - $ |D_c| $:类别 $ c $ 的总样本数量。

直观解释

  • 在类别 $ c $ 的样本中,统计第 $ i $ 个属性取值为 $ x_i $ 的频率,作为 $ P(x_i | c) $ 的估计。
  • 示例
    若类别 $ c=spam 200150“ ” $ P( | spam) = = 0.75 $$

注意事项

  • 零概率问题:若某属性值在类别 $ c $ 中未出现,则 $ P(x_i | c) = 0 。 * * * *:使 * *LaplaceSmoothing) * *,$ P(x_i | c) = $$ 其中 $ K $ 是该属性的取值总数。

2. 连续属性的条件概率估计

假设:属性服从正态分布(高斯分布) $$ p(x_i | c) = \frac{1}{\sqrt{2\pi}\sigma_{c,i}} \exp\left( -\frac{(x_i - \mu_{c,i})^2}{2\sigma_{c,i}^2} \right) $$ - 符号含义: - $ {c,i} $:类别 $ c $ 在第 $ i $ 个属性上的均值。 - $ {c,i}^2 $:类别 $ c $ 在第 $ i $ 个属性上的方差。

直观解释

  • 假设在类别 $ c $ 下,属性 $ x_i $ 服从均值为 $ {c,i} $、方差为 $ {c,i}^2 $ 的正态分布。
  • 示例
    若类别 $ c=spam $ 的“字数”属性均值 $ {spam, } = 500 $,方差 $ {spam, }^2 = 100 $ p(600 | spam) = ( - ) $$

注意事项

  • 分布假设:若实际数据不符合正态分布,需调整假设(如使用核密度估计、对数变换等)。
  • 参数估计:均值和方差通过训练数据计算: $$ \mu_{c,i} = \frac{1}{|D_c|} \sum_{x \in D_c} x_i, \quad \sigma_{c,i}^2 = \frac{1}{|D_c|} \sum_{x \in D_c} (x_i - \mu_{c,i})^2 $$

半朴素贝叶斯分类器

半朴素贝叶斯分类器是对传统朴素贝叶斯的改进,它在保留计算效率的同时,适当引入部分属性间的依赖关系,从而在分类性能和计算复杂度之间取得平衡。

独依赖估计(ODE)方法

(1) 定义

独依赖估计(One-Dependent Estimator, ODE)是半朴素贝叶斯的一种实现方式,其核心假设是: > 每个属性 $ x_i $ 在类别 $ c $ 之外最多依赖于一个其他属性(称为父属性 $ pa_i $)

数学表达式为: $$ P(c|\mathbf{x}) \propto P(c) \prod_{i=1}^d P(x_i | c, pa_i) $$ 其中: - $ pa_i $:属性 $ x_i $ 的父属性(依赖的单一属性)。 - $ P(x_i | c, pa_i) $:在类别 $ c $ 和父属性 $ pa_i $ 下,属性 $ x_i $ 的条件概率。

(2) 直观理解

  • 每个属性 $ x_i $ 的分布不仅受类别 $ c $ 影响,还受其父属性 $ pa_i $ 的影响。
  • 例如,在文本分类中,若属性 $ x_1 $ 是“免费”,$ x_2 $ 是“中奖”,可设定 $ pa_2 = x_1 $,表示“中奖”在类别和“免费”的共同作用下出现。
超父独依赖估计(SPODE)

超父独依赖估计(Super Parent One-Dependent Estimator, SPODE)是半朴素贝叶斯分类器的一种扩展,其核心思想是: > 所有属性都依赖于同一个“超父”属性 $ x_i $,从而在保留部分依赖关系的同时避免完全联合概率的计算。

(1) 贝叶斯定理展开 $$ P(c|\mathbf{x}) = \frac{P(\mathbf{x}, c)}{P(\mathbf{x})} $$ 其中: - $ P(, c) $:联合概率,表示特征 $ $ 和类别 $ c $ 同时发生的概率。 - $ P() $:证据(归一化因子)。

(2) 引入“超父”属性 $ x_i $

假设所有属性 $ x_j (j i) $ 在类别 $ c $ 下仅依赖于 $ x_i $ P(, c) = P(c, x_i) P(x_1, , x_{i-1}, x_{i+1}, , x_d | c, x_i) P(, c) = P(c, x_i) _{j i} P(x_j | c, x_i) $$

(3) 最终形式

由于 $ P() $ 对所有类别相同,可忽略,最终决策规则为: $$ P(c|\mathbf{x}) \propto P(c, x_i) \cdot \prod_{j=1}^d P(x_j | c, x_i) $$ 其中: - $ P(c, x_i) $:类别 $ c $ 和属性 $ x_i $ 的联合概率。 - $ P(x_j | c, x_i) $:在类别 $ c $ 和 $ x_i $ 的条件下,属性 $ x_j $ 的概率。

树增强朴素贝叶斯(TAN: Tree-Augmented Naive Bayes)

TAN(Tree-Augmented Naive Bayes)是半朴素贝叶斯分类器的一种扩展,旨在通过引入属性间的树状依赖关系,在保留计算效率的同时,显著提升分类性能。它结合了贝叶斯网络的建模能力和生成式模型的概率推理优势。

1. 核心思想

TAN 的核心假设是: > 所有属性(特征)在类别 $ c $ 的基础上,形成一个以属性为节点的树状依赖结构,即每个属性最多依赖一个其他属性(父属性),且整个依赖图是一棵无环的树。

数学表达$$ P(c|\mathbf{x}) \propto P(c) \cdot \prod_{i=1}^d P(x_i | c, pa_i) $$ 其中: - $ pa_i $:属性 $ x_i $ 的父属性(依赖的单一属性)。 - $ P(x_i | c, pa_i) $:在类别 $ c $ 和父属性 $ pa_i $ 的条件下,属性 $ x_i $ 的条件概率。

2. TAN 的构建步骤

TAN 通过以下步骤构建属性间的依赖结构:

(1) 计算互信息(Mutual Information)

互信息衡量两个属性之间的相关性: $$ I(x_i, x_j) = \sum_{x_i, x_j} P(x_i, x_j) \log \frac{P(x_i, x_j)}{P(x_i)P(x_j)} $$ - 含义:互信息越大,两个属性之间的依赖关系越强。

(2) 构建带权图

  • 将所有属性视为图中的节点。
  • 每对属性间的边权重设为互信息 $ I(x_i, x_j) $。

(3) 最大带权生成树(Maximum Weight Spanning Tree, MWST)

使用克鲁斯卡尔(Kruskal)算法或普里姆(Prim)算法,选择一棵连接所有属性节点的树,使得: - 树的边权重(互信息)总和最大。 - 树中无环。

(4) 确定依赖方向

  • 随机选择一个根节点(或根据领域知识指定)。
  • 从根节点出发,确定每条边的方向(父属性 → 子属性)。
image-20250605223036362

贝叶斯网

待学习

EM算法

EM算法(Expectation-Maximization Algorithm)是一种迭代优化算法,用于处理含有隐变量(Hidden Variables)或缺失数据的概率模型参数估计问题。它的核心思想是通过交替执行期望(E)步最大化(M)步,逐步逼近模型参数的最大似然估计。

1. 核心思想:解决隐变量问题

(1) 什么是隐变量?

隐变量(Latent Variables)是模型中不可观测但影响观测数据的变量。例如: - 混合高斯模型(GMM):每个样本属于哪个高斯分布是隐变量。 - 聚类任务:样本所属的聚类标签是隐变量。

(2) 问题挑战

当存在隐变量时,直接最大化似然函数变得困难。例如: log P(x|θ) = log ∑zP(x, z|θ) 其中 $ z $ 是隐变量,$ $ 是模型参数。由于对数中包含求和,直接求导无法分离参数。

(3) EM算法的解决方案

EM算法通过以下步骤迭代求解: 1. E步(期望):用当前参数估计隐变量的后验分布(即“责任”分配)。 2. M步(最大化):基于隐变量的后验分布,最大化期望似然函数以更新参数。

2. 算法流程

(1) 初始化参数

选择初始参数 $ ^{(0)} $,例如随机初始化或通过启发式方法设定。

(2) E步:计算隐变量后验分布

给定当前参数 $ ^{(t)} $,计算隐变量 $ z $ 的后验概率: Q(t)(z) = P(z|x, θ(t)) 这一步为每个样本分配隐变量的概率分布(如样本属于某个聚类的概率)。

(3) M步:最大化期望似然

基于 $ Q^{(t)}(z) $ ^{(t+1)} = _{} _z Q^{(t)}(z) P(, z|) $$ 这一步更新参数 $ $,使得期望似然最大。

(4) 收敛判断

重复E步和M步直到参数收敛(如 $ |^{(t+1)} - ^{(t)}| < $)或达到最大迭代次数。

3. 示例:混合高斯模型(GMM)

假设数据由多个高斯分布生成,但不知道每个样本属于哪个分布。

(1) 模型定义

  • 观测变量 $ x_i ^d $:第 $ i $ 个样本。
  • 隐变量 $ z_i {1, …, K} $:样本 $ x_i $ 所属的高斯分布。
  • 参数 $ = {_k, _k, k}{k=1}^K $:
    • $ _k $:第 $ k $ 个高斯分布的均值。
    • $ _k $:第 $ k $ 个高斯分布的协方差矩阵。
    • $ _k $:第 $ k $ 个高斯分布的权重(先验概率)。

(2) E步:计算责任分配

对于每个样本 $ x_i $ 和类别 $ k responsibility):$ _{ik}^{(t)} = P(z_i=k|x_i, ^{(t)}) = $$ 含义:在当前参数下,样本 $ x_i $ 属于类别 $ k $ 的概率。

(3) M步:更新参数

根据责任 $ _{ik} $ 更新参数: - 均值更新$$ \mu_k^{(t+1)} = \frac{\sum_{i=1}^N \gamma_{ik}^{(t)} x_i}{\sum_{i=1}^N \gamma_{ik}^{(t)}} $$ - 协方差更新$$ \Sigma_k^{(t+1)} = \frac{\sum_{i=1}^N \gamma_{ik}^{(t)} (x_i - \mu_k^{(t+1)})(x_i - \mu_k^{(t+1)})^T}{\sum_{i=1}^N \gamma_{ik}^{(t)}} $$ - 权重更新$$ \pi_k^{(t+1)} = \frac{\sum_{i=1}^N \gamma_{ik}^{(t)}}{N} $$

(4) 迭代终止

当参数变化小于阈值或达到最大迭代次数时停止。

作业

1
image-20250606143758565
image-20250606143819352
2

已知观测数据-67,-48,6,8,14,16,23,24,28,29,41,49,56,60,75,试估计两个分量的高斯混合模型的5个参数。

image-20250606150830535
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from sklearn.mixture import GaussianMixture
import numpy as np
import matplotlib.pyplot as plt

# 初始化观测数据
data = np.array([-67, -48, 6, 8, 14, 16, 23, 24, 28, 29, 41, 49, 56, 60,
75]).reshape(-1, 1)

# 聚类
gmmModel = GaussianMixture(n_components=2)
gmmModel.fit(data)
labels = gmmModel.predict(data)
print("labels =", labels)
labels = [1 1 0 0 0 0 0 0 0 0 0 0 0 0 0]
for i in range(0, len(labels)):
if labels[i] == 0:
plt.scatter(i, data.take(i), s=15, c='red')
elif labels[i] == 1:
plt.scatter(i, data.take(i), s=15, c='blue')
plt.title('Gaussian Mixture Model')
plt.xlabel('x')
plt.ylabel('y')
plt.show()
print("means =", gmmModel.means_.reshape(1, -1))
print("covariances =", gmmModel.covariances_.reshape(1, -1))
print("weights = ", gmmModel.weights_.reshape(1, -1))
image-20250606150906475
1
2
3
# means = [[ 32.98489643 -57.51107027]]
# covariances = [[429.45764867 90.24987882]]
# weights = [[0.86682762 0.13317238]]
3

简要阐述下EM算法的原理,并给出EM算法对高斯混合模型GMM进行求解的具体过程。

EM算法的原理

EM算法(期望最大化算法)是一种用于含有隐变量的概率模型参数估计的迭代优化方法。其核心思想是通过交替执行两个步骤来最大化观测数据的似然函数:

  1. E步(期望步):计算隐变量的后验期望(即责任),给定当前参数估计。
  2. M步(最大化步):基于责任,最大化完全数据的期望似然函数以更新参数。

EM算法通过不断优化似然函数的下界,最终收敛到局部最优解。以下具体阐述EM算法对高斯混合模型(GMM)的求解过程。

EM算法对GMM的具体求解过程

1. GMM模型定义

GMM假设数据由 $ K $ 个高斯分布线性组合生成,其概率密度函数为: $$ p(\mathbf{x}|\theta) = \sum_{k=1}^K \alpha_k \cdot \mathcal{N}(\mathbf{x}|\mu_k, \Sigma_k) $$ 其中: - $ k $:第 $ k $ 个高斯分布的权重($ {k=1}^K _k = 1 $)。 - $ _k $:第 $ k $ 个高斯分布的均值向量。 - $ _k $:第 $ k $ 个高斯分布的协方差矩阵。 - $ = {_k, _k, k}{k=1}^K $:模型参数。

隐变量 $ z_i {1,,K} $ 表示样本 $ _i $ 的类别标签(未知)。

2. EM算法步骤

(1) 初始化参数

随机或通过K-means初始化: - 每个高斯分布的均值 $ _k^{(0)} $、协方差 $ _k^{(0)} $、权重 $ _k^{(0)} $。

(2) 迭代优化(E步与M步)

E步:计算责任(后验概率) 对每个样本 xi 和每个簇 $ k $,计算其属于第 $ k $ 个高斯分布的后验概率 $$ \gamma(z_{ik}) = \frac{\alpha_k \cdot \mathcal{N}(\mathbf{x}_i | \mu_k, \Sigma_k)}{\sum_{j=1}^K \alpha_j \cdot \mathcal{N}(\mathbf{x}_i | \mu_j, \Sigma_j)} $$ 此概率表示在当前参数下,样本 $ _i $ 属于第 $ k $ 个高斯分布的“责任”。

M步:更新参数
基于责任 $ (z_{ik}) $,最大化完全数据似然函数的期望,更新参数:

  • 权重更新$$ \alpha_k^{(new)} = \frac{1}{N} \sum_{i=1}^N \gamma(z_{ik}) $$
  • 均值更新$$ \mu_k^{(new)} = \frac{\sum_{i=1}^N \gamma(z_{ik}) \mathbf{x}_i}{\sum_{i=1}^N \gamma(z_{ik})} $$
  • 协方差更新$$ \Sigma_k^{(new)} = \frac{\sum_{i=1}^N \gamma(z_{ik}) (\mathbf{x}_i - \mu_k^{(new)})(\mathbf{x}_i - \mu_k^{(new)})^\top}{\sum_{i=1}^N \gamma(z_{ik})} $$ 若为单变量高斯分布,则更新方差: $$ \sigma_k^{(new)} = \frac{\sum_{i=1}^N \gamma(z_{ik}) (x_i - \mu_k^{(new)})^2}{\sum_{i=1}^N \gamma(z_{ik})} $$

(3) 收敛判断

计算对数似然函数: $$ \log p(\mathbf{X}|\theta) = \sum_{i=1}^N \log \left( \sum_{k=1}^K \alpha_k \cdot \mathcal{N}(\mathbf{x}_i|\mu_k, \Sigma_k) \right) $$ 若对数似然的变化量小于阈值或达到最大迭代次数,则停止;否则重复E步和M步。。

参考资料

[5分钟学算法] #06 EM算法 你到底是哪个班级的_哔哩哔哩_bilibili

《统计学习方法_第二版》学习笔记第九章 - 知乎

集成学习

个体与集成

集成学习的基本概念

集成学习(Ensemble Learning)通过构建并结合多个学习器(基模型)来完成学习任务,其核心思想是“优而不同”,即通过多个弱学习器的协作提升整体性能,通常能获得比单一学习器更优的泛化能力 。

image-20250606154731476

在上图的集成模型中,若个体学习器都属于同一类别,例如都是决策树或都是神经网络,则称该集成为同质的(homogeneous);若个体学习器包含多种类型的学习算法,例如既有决策树又有神经网络,则称该集成为异质的(heterogenous)。

同质集成:个体学习器称为“基学习器”(base learner),对应的学习算法为“基学习算法”(base learning algorithm)。

异质集成:个体学习器称为“组件学习器”(component learner)或直称为“个体学习器”。

集成学习的两个重要概念:准确性多样性(diversity)。准确性指的是个体学习器不能太差,要有一定的准确度;多样性则是个体学习器之间的输出要具有差异性。

通过下面的这三个例子可以很容易看出这一点,准确度较高,差异度也较高,可以较好地提升集成性能。

image-20250606155939884

集成策略:如何结合多个基模型的预测结果,例如:

  • 投票法(Voting):多数投票(硬投票)或概率加权(软投票)。
  • 加权平均法:对基模型的输出赋予不同权重 。
  • Stacking:用元模型(Meta-Model)学习基模型的输出作为新特征 。
基于投票法的集成个体学习器的收敛性保证

公式解析 $$ P(H(\boldsymbol{x}) \neq f(\boldsymbol{x})) = \sum_{k=0}^{\lfloor T/2 \rfloor} \binom{T}{k} (1-\epsilon)^k \epsilon^{T-k} \leq \exp\left(-\frac{1}{2} T (1 - 2\epsilon)^2\right) $$

1. 公式含义

  • H(x):集成学习器的最终预测结果(如多数投票结果)。
  • f(x):真实标记。
  • ϵ:单个弱学习器的错误率(即 P(ht(x) ≠ f(x))),默认小于0.5。
  • T:基学习器的数量。
  • 左边:集成学习器预测错误的概率(即至少有超过 T/2 个基学习器预测错误的概率)。
  • 右边:对左边概率的指数级上限估计。

2. 推导思路

  • 假设每个基学习器独立且错误率为 ϵ,则错误次数服从二项分布 B(T, ϵ)
  • 集成错误的条件是“超过半数基学习器错误”,即错误次数 k ≤ ⌊T/2⌋

两个基本结论

1. 收敛速率随个体学习器数量 T 指数下降

  • 数学体现:错误概率的上界是 exp (−cT) 形式,其中 $c = \frac{1}{2}(1 - 2\epsilon)^2$

2. ϵ = 0.5 的个体学习器对收敛没有作用

  • 数学原因:当 ϵ = 0.5 时,(1 − 2ϵ)2 = 0,指数项变为 0,错误概率上界为 exp (0) = 1,即错误概率无法降低。

根据个体学习器的生成方式,目前集成学习可分为两类,代表作如下:

  1. 个体学习器直接存在强依赖关系,必须串行生成的序列化方法:Boosting
  2. 个体学习器间不存在强依赖关系,可以同时生成的并行化方法:Bagging随机森林 (Random Forest)

Boosting

Boosting是一种串行的工作机制,即个体学习器的训练存在依赖关系,必须一步一步序列化进行。

基本思想是:增加前一个基学习器在训练过程中预测错误样本的权重,使得后续基学习器更加关注这些打标错误的训练样本,尽可能纠正这些错误,然后基于调整后的样本分布训练下一个基学习器,如此重复,一直向下串行直至产生需要的T个基学习器,Boosting最终对这T个学习器进行加权结合,产生学习器委员会。

Boosting族算法最著名、使用最为广泛的就是AdaBoost,因此下面主要是对AdaBoost算法进行介绍。

AdaBoost使用的是指数损失函数,因此AdaBoost的权值与样本分布的更新都是围绕着最小化指数损失函数进行的。

看到这里回想一下之前的机器学习算法,不难发现机器学习的大部分带参模型只是改变了最优化目标中的损失函数:如果是Square loss,那就是最小二乘了;如果是Hinge Loss,那就是著名的SVM了;如果是log-Loss,那就是Logistic Regression了。

AdaBoost
公式解析

$$ H(\boldsymbol{x}) = \sum_{t=1}^T \alpha_t h_t(\boldsymbol{x}) $$ exp(H|𝒟) = 𝔼x ∼ 𝒟[ef(x)H(x)]

1. 符号含义

  • H(x):最终集成模型的预测结果,是 T 个基学习器 ht(x) 的加权和。
  • αt:第 t 个基学习器的权重,表示其在集成中的重要性。
  • ht(x):第 t 个基学习器(如决策树、感知机等)。
  • f(x):真实标签,通常取值为 {−1, +1}(二分类问题)。
  • 𝒟:训练数据分布。
  • exp:指数损失函数(Exponential Loss)。

2. 指数损失函数的意义

指数损失函数的形式为: exp(H|𝒟) = 𝔼x ∼ 𝒟[ef(x)H(x)] - 直观解释: - 当 H(x)f(x) 同号时(预测正确),指数项 ef(x)H(x) 接近 0,损失小。 - 当 H(x)f(x) 异号时(预测错误),指数项趋近于正无穷,损失极大。 - 因此,该损失函数对错误样本的惩罚非常严格,迫使模型优先修正错误。

AdaBoost的优化目标

AdaBoost的目标是选择基学习器 ht 和权重 αt,使得集成模型 H(x) 能够最小化指数损失函数$$ \min_{\alpha_1, h_1, \dots, \alpha_T, h_T} \mathbb{E}_{\boldsymbol{x} \sim \mathcal{D}} \left[ e^{-f(\boldsymbol{x}) \sum_{t=1}^T \alpha_t h_t(\boldsymbol{x})} \right] $$

优化策略

AdaBoost采用前向分步算法(Forward Stagewise Algorithm),逐轮迭代优化: 1. 初始化样本权重:初始时所有样本权重相等。 2. 训练基学习器 ht:在当前样本权重分布下,训练一个弱学习器 ht。 3. 计算权重 αt:根据 ht 的错误率 ϵt 计算其权重: $$ \alpha_t = \frac{1}{2} \ln \left( \frac{1 - \epsilon_t}{\epsilon_t} \right) $$ 4. 更新样本权重:提高被 ht 错分类样本的权重,降低正确分类样本的权重。 5. 重复步骤 2-4,直到训练完成 T 轮。

示例:二分类问题

假设一个二分类任务,真实标签 f(x) ∈ {−1, +1},集成模型预测值 $H(\boldsymbol{x}) = \sum_{t=1}^T \alpha_t h_t(\boldsymbol{x})$: - 若 H(x) > 0,预测为 +1; - 若 H(x) < 0,预测为 −1

此时,指数损失函数的值反映了模型对错误样本的惩罚程度: - 正确预测时,ef(x)H(x) ≈ 0; - 错误预测时,ef(x)H(x) ≫ 1

AdaBoost的算法流程
image-20250606172851748
重赋权法与重采样法

在集成学习中,Boosting 算法的核心在于动态调整样本权重 ,以逐步聚焦难分类样本。Boosting 主要通过两种方法实现样本权重的更新:重赋权法(re-weighting)重采样法(re-sampling)

重赋权法 : 对每个样本附加一个权重,这时涉及到样本属性与标签的计算,都需要乘上一个权值。 重采样法 : 对于一些无法接受带权样本的及学习算法,适合用“重采样法”进行处理。方法大致过程是,根据各个样本的权重,对训练数据进行重采样,初始时样本权重一样,每个样本被采样到的概率一致,每次从N个原始的训练样本中按照权重有放回采样N个样本作为训练集,然后计算训练集错误率,然后调整权重,重复采样,集成多个基学习器。

从偏差-方差分解来看:Boosting算法主要关注于降低偏差,每轮的迭代都关注于训练过程中预测错误的样本,因此Boosting能基于泛化性能相当弱的学习器构建出很强的集成学习器。

拓展:Gradient Boosting

任务分为分类,回归,聚类,降维等,而分类中还分为二分类和多分类

从AdaBoost的算法流程来看,标准的AdaBoost只适用于二分类问题。

通过改造AdaBoost对样本分类的限制和损失函数,可以实现多分类或回归问题,这样改造出来的算法框架成为Gradient Boosting

GBDT(Gradient Boosting Decision Tree)与XGBoost

1. GBDT 的核心思想

GBDT 是基于梯度提升(Gradient Boosting)框架的集成学习方法,其特点包括: - 基学习器:使用CART(分类与回归树)作为个体学习器。 - 损失函数: - 回归问题:平方损失(Squared Loss): err(Ht(x), f(x)) = (Ht(x) − f(x))2 - 二分类问题:对数似然损失(Log-Likelihood Loss,类似逻辑回归): err(Ht(x), f(x)) = log (1 + exp (−f(x)Ht(x))) - 多分类问题:扩展为多分类对数损失。

2. XGBoost 的定位

XGBoost(eXtreme Gradient Boosting)是 GBDT 的一种高效实现和改进,类似于 LIBSVM 对 SVM 的优化关系。其核心目标是: - 提升训练速度:通过并行计算内存优化等工程技巧。 - 增强模型性能:引入正则化项缺失值处理自适应学习率等改进。

XGBoost即eXtremeGradient Boosting的缩写,XGBoost与GBDT的关系可以类比为 LIBSVM和SVM的关系,即XGBoOst是GBDT的一种高效实现和改进。

它并非一个全新的算法框架,而是对标准 GBDT 进行了大量的工程优化和算法增强

image-20250606175536310

Bagging

Bagging是一种并行式的集成学习方法,即基学习器的训练之间没有前后顺序可以同时进行

Bagging使用“有放回”采样的方式选取训练集,对于包含m个样本的训练集,进行m次有放回的随机采样操作,从而得到m个样本的采样集,这样训练集中有接近36.8%的样本没有被采到,可用作验证集来对泛化性能进行“包外估计”(out-of-bag estimate)。

按照相同的方式重复进行,我们就可以采集到T个包含m个样本的数据集,从而训练出T个基学习器,最终对这T个基学习器的输出进行结合

Bagging与Boosting的差异

Boosting算法一大特点是串行,这样诚然可以降低模型的偏差,增强拟合能力,但是当数据过大时,一大缺点就是会降低学习效率

Bagging作为并行式的集成学习方法,通过综合多个基学习器的结果,可以增加学习效率

二者差异性:

1.对目标的拟合程度:Boosting对目标有更好的拟合能力(偏差小);Bagging则偏差相对大一些

2.运行效率:由于并行的特点,Bagging的运行效率是大于Boosting的

3.泛化能力:由于Bagging每个学习器不会受其他学习器的影响,泛化能力(方差大)相对于Boosting

更好

Bagging的算法流程
image-20250606182733022

可以看出Bagging主要通过样本的扰动来增加基学习器之间的多样性,因此Bagging的基学习器应为那些对训练集十分敏感的不稳定学习算法,例如:神经网络与决策树等。

从偏差-方差分解来看,Bagging算法主要关注于降低方差,即通过多次重复训练提高稳定性。

不同于AdaBoost的是,Bagging可以十分简单地移植到多分类、回归等问题。总的说起来则是:AdaBoost关注于降低偏差,而Bagging关注于降低方差。

自助采样法(Bootstrap Sampling)

在机器学习中,自助采样法(Bootstrap Sampling) 是 Bagging 算法的核心技术之一。其核心思想是从原始数据集中有放回地随机抽取样本,形成新的训练子集。这一过程的一个重要数学性质是:当样本量 n 趋近于无穷大时,每个样本在 Bootstrap 样本集中未被抽中的概率趋近于 $\frac{1}{e} \approx 36.6\%$。以下是详细解析:

1. 公式推导

假设我们从 n 个样本中有放回地抽取 n 次,形成一个 Bootstrap 样本集。对于任意一个特定样本(如第 i 个样本),它在某次抽样中未被选中的概率为: $$ 1 - \frac{1}{n} $$ 因此,它在整个 n 次抽样中从未被选中的概率为: $$ \left(1 - \frac{1}{n}\right)^n $$n → ∞ 时,该概率的极限为: $$ \lim_{n \to \infty} \left(1 - \frac{1}{n}\right)^n = \frac{1}{e} \approx 0.3679 \quad (\text{即 } 36.6\%) $$ 在每次 Bootstrap 采样中,约有 36.6% 的样本未被选中 ,这些样本称为 Out-of-Bag(OOB,包外估计)样本

2. OOB 样本的应用

在 Bagging 算法中,OOB 样本具有以下重要作用: 1. 无偏验证
每个基学习器的训练数据不包含其对应的 OOB 样本,因此可以用这些样本直接评估模型性能(即 OOB 误差),无需额外的交叉验证。 2. 特征重要性评估
在随机森林中,通过比较 OOB 样本在打乱某个特征后的预测误差变化,可以衡量该特征的重要性。

3. 与其他采样方法的对比

采样方法 是否放回 样本覆盖范围 典型应用场景
Bootstrap 采样 约 63.4% 样本被重复使用 Bagging、随机森林
简单随机采样 所有样本唯一出现 传统交叉验证
随机森林

随机森林(Random Forest)是Bagging的一个拓展体,它的基学习器固定为决策树,多棵树也就组成了森林,而“随机”则在于选择划分属性的随机,随机森林在训练基学习器时,也采用有放回采样的方式添加样本扰动,同时它还引入了一种属性扰动,即在基决策树的训练过程中,在选择划分属性时,RF先从候选属性集中随机挑选出一个包含K个属性的子集,再从这个子集中选择最优划分属性 。

这样随机森林中基学习器的多样性不仅来自样本扰动,还来自属性扰动,从而进一步提升了基学习器之间的差异度。相比决策树的Bagging集成,随机森林的起始性能较差(由于属性扰动,基决策树的准确度有所下降),但随着基学习器数目的增多,随机森林往往会收敛到更低的泛化误差。同时不同于Bagging中决策树从所有属性集中选择最优划分属性,随机森林只在属性集的一个子集中选择划分属性,因此训练效率更高

image-20250606184958951

结合策略

在集成学习中,结合策略是将多个基学习器的输出整合为最终预测结果的关键步骤。以下是针对回归和分类问题的不同结合策略及其核心要点:

定义:在训练好多个基学习器后,如何将其输出组合成集成模型的最终输出。

1.平均法(回归问题)
  1. 简单平均法(Simple Averaging)

    • 公式
      $$ H(x) = \frac{1}{T} \sum_{i=1}^{T} h_i(x) $$
    • 特点
      • 直接对所有基学习器的预测结果取算术平均。
      • 计算简单,适合基学习器性能相近的场景。
      • 若部分基学习器表现较差,可能拖累整体性能。
  2. 加权平均法(Weighted Averaging)

    • 公式
      $$ H(x) = \sum_{i=1}^{T} w_i h_i(x) $$ 其中,$ w_i $ 且 $ _{i=1}^{T} w_i = 1 $。
    • 特点
      • 通过权重 $ w_i $ 调节各基学习器的贡献,灵活性更高。
      • 适用于基学习器性能差异较大的情况,可提升鲁棒性。
      • 权重可通过验证集性能(如RMSE、MAE)或优化算法(如梯度下降)确定。
2.投票法(分类问题)
  1. 简单投票法(Majority Voting)
    • 原理
      每个基学习器对样本进行分类投票,最终结果由得票最多的类别决定。
    • 公式(二分类示例):
      $$ H(x) = \begin{cases} 1 & \text{若} \sum_{i=1}^{T} I(h_i(x) = 1) > T/2 \\ 0 & \text{否则} \end{cases} $$ 其中,$ I() $ 为指示函数。
    • 特点
      • 简单高效,适合基学习器性能相近的场景。
      • 对异常分类器的鲁棒性较弱。
  2. 加权投票法(Weighted Voting)
    • 原理
      给不同基学习器分配权重,最终结果由加权票数最高的类别决定。
    • 公式(二分类示例):
      $$ H(x) = \begin{cases} 1 & \text{若} \sum_{i=1}^{T} w_i I(h_i(x) = 1) > 0.5 \sum_{i=1}^{T} w_i \\ 0 & \text{否则} \end{cases} $$
    • 特点
      • 权重可根据基学习器的验证集准确率或领域知识设定。
      • 更适合处理性能差异较大的基学习器。

绝对多数投票法(majority voting)提供了拒绝选项,这在可靠性要求很高的学习任务中是一个很好的机制。同时,对于分类任务,各个基学习器的输出值有两种类型,分别为类标记和类概率。

image-20250606195241433

一些在产生类别标记的同时也生成置信度的学习器,置信度可转化为类概率使用,一般基于类概率进行结合往往比基于类标记进行结合的效果更好,需要注意的是对于异质集成,其类概率不能直接进行比较,此时需要将类概率转化为类标记输出,然后再投票。

3.学习法(Stacking)

学习法是一种更高级的结合策略,其核心思想是通过训练一个次级学习器(Meta-Learner) 来动态融合多个基学习器的输出。其中,Stacking(堆叠泛化) 是学习法的典型代表,它通过将基学习器的预测结果作为新特征,进一步训练一个次级模型,最终实现更优的泛化性能。

Stacking 的基本原理

步骤概述

  • 训练基学习器:使用原始数据训练 $ T $ 个基学习器(如决策树、SVM、神经网络等)。
  • 生成新特征:对于每个样本,将 $ T $ 个基学习器的输出(预测结果)作为该样本的新特征,形成一个 $ m T $ 的数据集($ m $ 为样本数量)。
  • 训练次级学习器:使用新数据集(基学习器输出 + 真实标签)训练一个次级学习器(如逻辑回归、梯度提升树等),该学习器负责融合基学习器的预测结果。

Stacking 的优势

  1. 动态权重分配
    次级学习器可以自动学习基学习器的权重,无需人工设定。例如,若某个基学习器表现优异,次级学习器会赋予其更高的权重。
  2. 异质模型融合
    可以混合不同类型的基学习器(如线性模型与树模型),充分利用各自的特性。
  3. 提升泛化能力
    次级学习器通过学习基学习器的输出模式,能够捕捉更复杂的决策边界。

Stacking 的实现细节

  1. 数据划分
    • 通常需将原始数据分为两部分:
      • 训练集:用于训练基学习器。
      • 验证集:用于生成基学习器的输出(避免过拟合)。
    • 或采用交叉验证(如 $ k $-折)生成基学习器的预测结果,确保次级学习器的训练数据不被污染。
  2. 基学习器输出类型
    • 分类任务:基学习器输出类概率(Soft Voting),而非类别标签(Hard Voting)。例如,逻辑回归输出 $ P(c_j | x) $,随机森林输出节点样本的类别分布。
    • 回归任务:基学习器直接输出预测值(如线性回归的 )。
  3. 次级学习器选择
    • 多响应线性回归(MLR):适用于基学习器输出可加权平均的情况,计算简单且鲁棒。
      $$ H(x) = \sum_{i=1}^{T} w_i h_i(x) $$
    • 复杂模型:如梯度提升树、神经网络,可捕捉基学习器输出之间的非线性关系。

多样性

在集成学习中,多样性增强(Diversity Enhancement) 是提升模型性能的关键策略。通过引入多样性,可以降低基学习器之间的相关性,从而减少误差传递和过拟合风险。以下是四种常见的多样性增强方法及其核心要点:

1. 数据样本扰动(Data Perturbation)

原理:通过修改训练数据的分布或采样方式,使每个基学习器看到不同的数据子集。

实现方式
- Bagging(如随机森林)
- 随机有放回地采样(Bootstrap),生成多个不同的训练集。
- 对输入扰动敏感的基学习器(如决策树、神经网络)效果显著。
- 示例
- 决策树对数据扰动敏感,Bagging 可有效提升其泛化能力。
- 线性模型(如线性回归、SVM)对数据扰动不敏感,Bagging 效果有限。

2. 输入属性扰动(Input Attribute Perturbation)

原理:通过改变输入特征的表示或选择,增加基学习器间的差异。

实现方式
- 特征子集采样:每次随机选择部分特征进行训练(如随机森林中的列扰动)。
- 特征变换:对特征进行缩放、旋转或加噪声等操作。
- 适用场景
- 数据包含大量冗余属性时,可大幅加速训练并提升多样性。
- 对高维数据(如图像、文本)尤其有效。

3. 输出属性扰动(Output Attribute Perturbation)

原理:通过修改训练样本的标签,间接影响基学习器的学习过程。

实现方式
- 随机翻转标签:对部分样本的标记进行随机更改(需谨慎使用,避免干扰模型)。
- Dropout(神经网络)
- 在训练过程中随机“关闭”部分神经元,强制网络学习更鲁棒的特征。
- 类似于对输出属性的随机扰动,可提升模型泛化能力。

4. 算法参数扰动(Algorithm Parameter Perturbation)

原理:通过调整基学习器的超参数,生成不同的模型行为。

实现方式

  • 正则化方法:L1/L2 正则化(如 Ridge、Lasso)限制模型复杂度,降低过拟合风险。
  • 随机初始化: 神经网络的随机权重初始化可能导致收敛到不同局部最优解。

作业

1

集成学习中常见的两种方法是什么?请分别介绍它们的原理和特点。集成学习相比于单个模型有什么优势和应用场景?

集成学习常见方法、原理、特点及优势

常见方法:Bagging 和 Boosting
原理与特点

方法 原理 特点
Bagging 1. 自助采样:从训练集有放回抽取多个子集
2. 并行训练基模型
3. 聚合预测(投票/平均)
- 降低方差
- 适合高方差模型(如未剪枝决策树)
- 并行化,训练快
- 代表:随机森林
Boosting 1. 顺序训练:后一个模型修正前一个模型的错误
2. 加权困难样本
3. 加权组合模型
- 降低偏差
- 需弱学习器(如树桩)
- 易过拟合(需正则化)
- 代表:AdaBoost, GBDT, XGBoost

集成学习的优势
- 提升泛化能力:降低过拟合(Bagging)或欠拟合(Boosting)风险
- 增强鲁棒性:减少异常值/噪声影响(如投票机制)
- 突破性能上限:组合多个弱模型达到强模型效果

应用场景
- 分类任务:医疗诊断(整合多模型减少误诊)
- 回归任务:房价预测(融合不同树模型提升精度)
- 不平衡数据:Boosting 加权少数类样本
- 高维数据:随机森林自动特征选择

2

如果在完全相同的训练集上训练了五个不同的模型,并且它们都达到了95%的准确率,是否还有机会通过结合这些模型来获得更好的结果?如果可以,该怎么做?如果不行,为什么?

模型结合提升性能的可能性与方法

是否可能提升,但需满足条件:模型错误不相关(即犯错样本不同)。

如何实现

  1. 投票法(分类)
    • 多数投票:5个模型对样本 (x) 的预测为 ([A, A, B, A, C]) → 最终输出 (A)
    • 关键要求:模型存在多样性(如使用SVM、决策树等不同算法)
  2. 加权平均(回归)
    • 若模型精度不同,分配权重:$ y_{} = w_1 y_1 + w_2 y_2 + + w_5 y_5$
    • 权重可通过验证集性能确定

若无法提升的情况
- 原因:模型高度相关(如相同算法、相同特征、相同超参)
- 数学解释:误差相关性 rho ≈ 1 时,集成误差 单一模型误差

3

是否可以通过在多个服务器上并行来加速随机森林的训练?AdaBoost集成呢?为什么?

算法 是否支持并行 原因
随机森林 1. 树之间独立训练
2. 可分布式分配Bootstrap样本到不同服务器
3. 特征分裂也可并行(如选特征子集)
AdaBoost 1. 模型必须顺序训练:后一个模型依赖前一个模型的样本权重更新
2. 无法解耦迭代过程

聚类

聚类任务

我们之前学习的分类/回归任务都属于 有监督学习 需要我们提供样本与标签

而马上要学习的聚类任务和后续学习的降维则属于 无监督学习 仅需提供样本

聚类是一种经典的无监督学习(unsupervised learning)方法,无监督学习的目标是通过对无标记训练样本的学习,发掘和揭示数据集本身潜在的结构与规律,即不依赖于训练数据集的类标记信息。

聚类试图将数据集中的样本划分为若干个通常是不相交的子集,每个子集称为一个“簇”( cluster)。通过这样的划分,每簇可能对应于一些潜在的概念(类别),如“浅色瓜”“深色瓜”,“有籽瓜”“无籽瓜”,甚至“本地瓜”“外地瓜”等;需说明的是,这些概念对聚类算法而言事先是未知的,聚类过程仅能自动形成簇结构, 簇所对应的概念语义需由使用者来把握和命名

直观上来说,聚类是将相似的样本聚在一起,从而形成一个类簇(cluster)。涉及两个问题

  • 如何度量相似性(similarity measure),这便是距离度量(distance measure),在生活中我们说差别小则相似,对应到多维样本,每个样本可以对应于高维空间中的一个数据点,若它们的距离相近,我们便可以称它们相似。
  • 如何评价聚类结果,这便是性能度量(validity index)

距离度量

连续/离散有序

明可夫斯基距离(Minkowski Distance)

明可夫斯基距离是一组常用的连续型距离度量,通过调整参数 $ p $ 可以统一表示多种距离形式,是欧氏距离和曼哈顿距离的推广。

1. 公式定义

对于两个 $ n $ 维向量 $ i = (x{i1}, x_{i2}, , x_{in}) $ 和 $ j = (x{j1}, x_{j2}, , x_{jn}) $ {}(i, j) = ( {u=1}^{n} |x{iu} - x{ju}|^p )^{} $$ 其中,$ p $ 是一个可调节的参数。

2. 特殊情况

  • **当 $ p = 2 * * :退 * *EuclideanDistance) * *$ _{}(_i, _j) = $$
    • 几何意义:两点之间的直线距离。
    • 适用场景:大多数机器学习算法(如KNN、PCA)默认使用欧氏距离。
  • **当 $ p = 1 * * :退 * *ManhattanDistance) * *$ {}(i, j) = {u=1}^{n} |x{iu} - x{ju}| $$
    • 几何意义:沿坐标轴移动的总距离(如棋盘格路径)。
    • 适用场景:高维稀疏数据(如文本特征)、计算资源受限的场景。
  • **当 $ p * * :退 * *ChebyshevDistance) * *$ {}(i, j) = {u} |x{iu} - x{ju}| $$
    • 几何意义:各维度差值的最大值。
    • 适用场景:关注最坏情况下的误差(如游戏AI路径规划)。

3. 参数 $ p $ 的影响

  • $ p $ 越小:距离计算越关注较小的维度差异(如曼哈顿距离对单个维度的扰动更敏感)。
  • $ p $ 越大:距离计算越关注较大的维度差异(如切比雪夫距离仅关注最大差值)。
  • 选择依据
    • 数据分布是否均匀:若某些维度差异显著,可增大 $ p $。
    • 算法需求:如KNN中,高维数据可能更适合曼哈顿距离(缓解“维度灾难”)。
离散无序

我们知道属性分为两种:连续属性(continuous attribute)和离散属性(catergorical attribute有限个取值)。对于连续值的属性,一般都可以被学习器所用,有时会根据具体的情形作相应的预处理,例如:归一化等;而对于离散值的属性,需要作下面进一步的处理:

若属性值之间存在序关系(ordinal attribute),则可以将其转化为连续值,例如:身高属性“高”“中等”“矮”,可转化为{1, 0.5, 0}。

若属性值之间不存在序关系(non-ordinal attribute),则通常将其转化为向量的形式,例如:性别属性“男”“女”,可转化为{(1,0),(0,1)}。

连续属性和存在序关系的离散属性都可以直接参与计算,而不存在序关系的无序属性,我们一般采用VDM(Value Difference Metric)进行距离的计算

VDM 是一种专门用于离散无序属性的距离度量方法,通过统计信息量化不同类别间的差异。其核心思想是:若两个类别的样本在目标变量上的分布差异越大,则它们的距离越大

1. 公式解析 $$ \text{VDM}_p(a, b) = \sum_{i=1}^{k} \left| \frac{m_{u,a,i}}{m_{u,a}} - \frac{m_{u,b,i}}{m_{u,b}} \right|^p $$ - 符号含义
- $ a, b $:两个不同的类别值(如性别“男”和“女”)。
- $ m_{u,a,i} $:在属性 $ u $ 的第 $ i $ 个取值下,类别 $ a $ 的样本数量。
- $ m_{u,a} $:类别 $ a $ 的总样本数量。
- $ k $:属性 $ u $ 的不同取值数目(如颜色属性有红、蓝、绿三种取值,则 $ k=3 $)。
- $ p $:距离幂指数(通常取 $ p=1 $ 或 $ p=2 $)。

2. 核心思想

  • 统计分布差异
    对于每个属性取值 $ i $,计算类别 $ a $ 和 $ b $ 的样本比例差异:
    $$ \left| \frac{m_{u,a,i}}{m_{u,a}} - \frac{m_{u,b,i}}{m_{u,b}} \right| $$ 该值越大,说明两个类别在该取值上的分布差异越大。
  • 加权求和
    将所有属性取值的差异按 $ p $ 次方加权求和,得到最终的距离。

3. 示例说明

假设我们有一个“颜色”属性(红、蓝、绿),目标变量是“是否购买商品”(0/1)。统计结果如下:

颜色 购买(1) 不购买(0) 总计
10 5 15
8 12 20
绿 3 7 10

计算“红”与“蓝”之间的 VDM 距离($ p=1 ):1./: −  P(1) = 10/15 P(0) = 5/15 $
- 蓝:$ P(1) = 8/20 = 0.4 P(0) = 12/20 = 0.6 $
2. 计算差异并求和:
VDM1(红, 蓝) = |0.67 − 0.4|+|0.33 − 0.6| = 0.27 + 0.27 = 0.54

性能度量

于聚类算法不依赖于样本的真实类标,就不能像监督学习的分类那般,通过计算分对分错(即精确度或错误率)来评价学习器的好坏或作为学习过程中的优化目标。

直观上看,我们希望“物以类聚”,即同一簇的样本尽可能彼此相似,不同簇的样本尽可能不同换言之,聚类结果的“簇内相似度”( intra-cluster similarity)高且“簇间相似度” inter-cluster similarity)低

聚类性能度量有两类

  • “外部指标”(external index):所谓外部指标就是已经有一个“参考模型”存在了,将当前模型与参考模型的比对结果作为指标。
  • “内部指标”( internal index):所谓内部指标就是仅仅考虑当前模型的聚类结果。
外部指标

1.基本概念

image-20250607110103570

显然,$ a + b + c + d = $ 。

2. 常用外部指标

(1)Jaccard系数(JC) $$ \text{JC} = \frac{a}{a + b + c} $$ - 含义:衡量两个划分的重叠程度,仅考虑正确匹配($ a b + c )。 − * *  * *: [0, 1] $,值越大越好。
- 特点:对称性差,对噪声敏感 。

(2)Fowlkes-Mallows指数(FMI) $$ \text{FMI} = \sqrt{\frac{a}{a + b} \cdot \frac{a}{a + c}} $$ - 含义:结合查准率($ ),。 − * *  * *: [0, 1] $,值越大越好。
- 特点:平衡性较好,适合小样本 。

(3)Rand指数(RI) $$ \text{RI} = \frac{2(a + d)}{m(m - 1)} $$ - 含义:同时考虑正确匹配($ a + d 。 − * *  * *: [0, 1] $,值越大越好。
- 特点:计算简单,但对噪声较鲁棒 。

常用指标

  • 调整兰德指数(Adjusted Rand Index, ARI)
    • 定义:衡量聚类结果与真实标签的匹配程度,调整随机聚类的影响,取值范围 [-1, 1],值越大越好。
    • 公式
      $$ \text{ARI} = \frac{\text{RI} - \mathbb{E}[\text{RI}]}{\max(\text{RI}) - \mathbb{E}[\text{RI}]} $$ 其中 RI 是兰德指数(匹配样本对的比例)。
  • 归一化互信息(Normalized Mutual Information, NMI)
    • 定义:衡量聚类结果与真实标签的信息共享程度,值越大越好。
    • 公式
      $$ \text{NMI} = \frac{I(C; K)}{\sqrt{H(C) H(K)}} $$ 其中 $ I(C; K) $ 是互信息,$ H(C) $ 和 $ H(K) $ 是熵。
  • Fowlkes-Mallows 指数(FMI)
    • 定义:基于聚类结果与真实标签的 TP、FP、TN、FN 计算,值越大越好。
    • 公式
      $$ \text{FMI} = \sqrt{\frac{\text{TP}}{\text{TP} + \text{FP}} \cdot \frac{\text{TP}}{\text{TP} + \text{FN}}} $$

优点

  • 在有真实标签时,能更客观地评估聚类效果。
  • 适用于验证聚类结果的业务意义(如客户分群是否符合预期)。

局限性

  • 需要真实标签,不适用于纯无监督任务。
  • 对标签噪声敏感(如标签错误会误导 $ K $ 的选择)。

3. 应用示例

假设一个包含4个样本的数据集,参考标签为 {A, A, B, B},聚类结果为 {C, C, D, D}: - 计算样本对
- $ a = 2 $(样本1-2同簇,参考与聚类均同类)。
- $ b = 0 $(参考同类但聚类不同类)。
- $ c = 0 $(参考不同类但聚类同类)。
- $ d = 2 $(参考不同类且聚类不同类)。
- 指标结果
- JC = $ = 1 $(完美匹配)。
- FMI = $ = 1 $。
- RI = $ = $。

内部指标

内部指标不依赖任何外部参考模型,直接通过簇内紧凑性簇间分离性评估聚类结果。其核心思想是: - 簇内高内聚:同一簇的样本尽可能相似(距离小)。
- 簇间低耦合:不同簇的样本尽可能不同(距离大)。

1. 基本定义

设聚类结果为 $ C = {C_1, C_2, , C_k} $,定义以下四个关键距离:

(1)簇内平均距离(avg(C)) $$ \text{avg}(C) = \frac{2}{|C|(|C| - 1)} \sum_{1 \leq i < j \leq |C|} \text{dist}(\boldsymbol{x}_i, \boldsymbol{x}_j) $$ - 含义:簇内所有样本对的平均距离。
- 目标:越小越好,表示簇内样本更紧密。

(2)簇内最大距离(diam(C)) diam(C) = max1 ≤ i < j ≤ |C|dist(xi, xj) - 含义:簇内最远的两个样本之间的距离。
- 目标:越小越好,避免簇内存在离群点。

**(3)簇间最小距离($ d_{}(C_i, C_j) ) * *$ d_{}(C_i, C_j) = _{_i C_i, _j C_j} (_i, _j) $$ - 含义:簇 $ C_i $ 和 $ C_j $ 之间最近的两个样本的距离。
- 目标:越大越好,表示簇间分离度高。

**(4)簇中心距离($ d_{}(C_i, C_j) ) * *$ d_{}(C_i, C_j) = (_i, _j) $$ - 含义:簇 $ C_i $ 和 $ C_j $ 的中心点(均值向量)之间的距离。
- 目标:越大越好,表示簇中心相隔较远。

2. 常用内部指标

1. DB指数(Davies-Bouldin Index, DBI)

  • 公式
    $$ \text{DBI} = \frac{1}{k} \sum_{i=1}^{k} \max_{j \neq i} \left( \frac{\text{avg}(C_i) + \text{avg}(C_j)}{d_{\text{cen}}(\mu_i, \mu_j)} \right) $$
    • 符号含义
      • $ k $:簇的数量。
      • $ (C_i) $:簇 $ C_i $ 内部样本的平均距离。
      • $ d_{}(_i, _j) $:簇 $ C_i $ 和 $ C_j $ 的中心点(均值向量)之间的距离。
    • 目标:越小越好。
    • 核心思想:对于每个簇 $ C_i $,找到与其“最竞争”的簇 $ C_j $(即 $ $ 最大的簇),并取所有簇的平均值。
  • 示例
    若簇 $ C_1 $ 和 $ C_2 $ 的平均距离分别为 2 和 3,中心距离为 5,则它们的比值为 $ = 1 $。若这是 $ C_1 $ 的最大比值,则 $ C_1 $ 对 DBI 的贡献为 1。最终 DBI 是所有簇贡献的平均值。

2. Dunn指数(Dunn Index, DI)

  • 公式
    $$ \text{DI} = \min_{1 \leq i \leq k} \left\{ \frac{\min_{j \neq i} d_{\min}(C_i, C_j)}{\max_{1 \leq l \leq k} \text{diam}(C_l)} \right\} $$
    • 符号含义
      • $ d_{}(C_i, C_j) $:簇 $ C_i $ 和 $ C_j $ 之间的最小距离(最近样本对的距离)。
      • $ (C_l) $:簇 $ C_l $ 内的最大距离(最远样本对的距离)。
    • 目标:越大越好。
    • 核心思想
      • 分子:所有簇对之间的最小距离中的最小值(即最“脆弱”的簇间分离度)。
      • 分母:所有簇中的最大直径(最“松散”的簇内紧凑度)。
      • 指数越大,表示簇间分离度高且簇内紧凑。
  • 示例
    假设簇对 $ (C_1, C_2) $ 的最小距离为 5,簇 $ C_3 $ 的最大直径为 10,则 DI 为 $ = 0.5 $。

3. 轮廓系数(Silhouette Coefficient)

  • 单一样本的轮廓系数
    $$ s = \frac{b - a}{\max(a, b)} $$

    • 符号含义
      • $ a $:样本到同簇其他样本的平均距离(簇内凝聚度)。
      • $ b $:样本到最近簇中样本的平均距离(簇间分离度)。
    • 取值范围:$ [-1, 1] $,越接近 1 表示聚类效果越好。
    • 核心思想
      • 若 $ a < b $(同簇紧密,异簇疏远),则 $ s > 0 $,样本分类合理。
      • 若 $ a > b $(同簇松散,异簇更近),则 $ s < 0 $,样本可能被错误分类。
  • 整体轮廓系数:所有样本轮廓系数的平均值。

  • 示例
    若某样本 $ a = 2 b = 5 $,则 $ s = = 0.6 $,表明该样本分类合理。

4.肘部法则(Elbow Method)

肘部法则是一种经验性方法,常用于确定K-means等聚类算法的最优簇数($ K $)。其核心思想是通过观察误差平方和(SSE, Sum of Squared Errors)随 $ K $ 值变化的趋势,寻找“肘部点”(即 SSE 下降速度明显减缓的拐点),从而选择最优的 $ K $ 值

  • SSE(误差平方和):衡量每个样本到其所属簇中心的距离平方和,公式为: $$ \text{SSE} = \sum_{i=1}^n \|x_i - \mu_{c_i}\|^2 $$ 其中 $ x_i $ 是样本点,$ _{c_i} $ 是其所属簇中心。

  • 趋势分析

    • 当 $ K $ 增大时,SSE 会不断减小(因为簇越多,每个簇的样本越密集)。
    • 但当 $ K $ 增加到某个值后,SSE 的下降速度会显著放缓,形成“肘部”形状。
  • 肘部点的意义
    肘部点对应的 $ K $ 值是模型复杂度(簇数)与聚类效果(SSE)之间的平衡点。

指标对比与选择

指标 计算方式 目标 适用场景 局限性
DBI 簇内平均距离与簇中心距离的比值 越小越好 球形簇,需指定 $ k $ 对离群点敏感
Dunn指数 簇间最小距离与簇内最大直径的比值 越大越好 强调簇间分离与簇内紧凑 计算复杂,受离群点影响
轮廓系数 样本到同簇/异簇的平均距离差 越接近 1 越好 快速评估,适合 K-Means 对非球形簇不敏感

原型聚类与kmeans

原型聚类

原型聚类即“基于原型的聚类”(prototype-based clustering),原型表示模板的意思,就是通过参考一个模板向量或模板分布的方式来完成聚类的过程,通常情形下算法先对原型进行初始化,然后对原型进行迭代更新求解。采用不同的原型表、不同的求解方式,将产生不同的算法。

常见的K-Means便是基于簇中心(原型向量)来实现聚类,混合高斯聚类则是基于簇分布(概率模型)来实现聚类。

K-Means 聚类算法详解

目标函数:最小化所有样本到其所属簇中心的平方距离之和:
$$ E = \sum_{i=1}^{k} \sum_{\boldsymbol{x} \in C_i} \|\boldsymbol{x} - \boldsymbol{\mu}_i\|_2^2 $$ 其中,$ i = { C_i} $ 是簇 $ C_i $ 的均值向量。

算法步骤

  1. 初始化簇中心:随机选择 $ k $ 个样本作为初始簇中心。
    • 改进方法:K-Means++ 算法可提升初始中心的质量。
  2. 分配样本到最近簇:对每个样本 $ $,计算其到所有簇中心的距离,将其分配到距离最近的簇 $ C_i $。
  3. 更新簇中心:重新计算每个簇的均值向量 $ _i $。
  4. 迭代终止条件
    • 达到预设的最大迭代次数;
    • 簇中心不再显著变化(如变化幅度小于阈值 $ $);
    • 样本分配不再改变。

如何选择 $ k $ 值?

  • 肘部法则(Elbow Method):绘制 $ k $ 与误差 $ E $ 的关系曲线,选择误差下降显著变缓的 $ k $ 值。
  • 轮廓系数(Silhouette Coefficient):计算每个样本的轮廓系数,选择平均轮廓系数最大的 $ k $。
K-Means的算法流程
image-20250607114743211
K-means++

此法相对于 K-means 做出了一个小的改进。在一开始选择 k 个聚类中心时,并不是随机初始化 k 个,而是首先随机出 1 个,然后循环 k−1k−1 次选择剩下的 k-1 个聚类中心。选择的规则是:每次选择最不可能成为新的聚类中心的样本,或者是到所有聚类中心的最小距离最大的样本。

优势

避免不良初始化 :传统K-means随机初始化可能导致中心过于集中,而K-means++通过“最大化最小距离”策略,使初始中心分布更均匀。

Bisecting K-means

此法叫做二分 K-means 算法。具体的,在一开始将所有的样本划分为一个簇,然后每次选择一个误差最大的簇进行二分裂,不断分裂直到收敛。这种方法不能使得 Loss 最小,但是可以作为 K-means 算法的一个预热,比如可以通过这种方法得到一个相对合理的簇中心,然后再利用 K-means 算法进行聚类。

优势

降低计算复杂度 :每次仅对一个簇进行二分,时间复杂度为 O(kmn) ,适合大规模数据。

提供合理初始中心 :可作为传统K-means的预处理,减少随机初始化的影响。

LVQ(学习向量量化)

核心思想
LVQ 是一种有监督的原型聚类算法,结合了神经网络与向量量化技术。它通过维护一组原型向量(Prototype Vectors)来代表不同类别,并利用这些原型对数据进行分类或聚类。与 K-Means 类似,LVQ 会为每个簇分配一个原型向量,但其更新规则受类别标签的指导,因此更适用于分类任务 。

算法特点

  • 有监督学习:需要已知类别标签来调整原型向量,使同类样本更接近对应原型,异类样本远离原型。
  • 拓扑结构建模:通过原型向量捕捉数据的局部特征,类似于自组织映射(SOM),但更具针对性。
  • 硬聚类:每个样本最终被分配到最近的原型对应的类别,不提供概率输出 。
高斯混合聚类(Gaussian Mixture Model, GMM)

一句话概述算法:高斯混合聚类算法是一种概率模型,假设数据由多个高斯分布混合而成,通过迭代优化参数以拟合数据分布,常用于无监督学习中的聚类任务。

算法过程:

初始化参数: 随机初始化每个分量的均值、协方差矩阵和混合系数。

E 步(Expectation): 对每个数据点,计算它属于每个分量的后验概率,即计算每个分量的权重。

M 步(Maximization): 使用E步计算得到的后验概率,更新每个分量的均值、协方差矩阵和混合系数。

迭代: 重复执行E步和M步,直到模型参数收敛或达到预定的迭代次数。

GMM的优点包括对各种形状和方向的聚类簇建模能力,以及对数据分布的灵活性。它在许多领域,如模式识别、图像处理和自然语言处理等,都有广泛的应用。 image-20250611180451162

以下是高斯混合聚类(GMM)算法的详细步骤及EM算法中E步与M步的解释:

算法流程解析

输入:样本集 $ D = {x_1, x_2, , x_m} $,混合成分个数 $ k $。
输出:簇划分 $ C = {C_1, C_2, , C_k} $。

步骤详解

  1. 初始化模型参数
    随机初始化或通过K-means初步估计以下参数:

    • 混合系数 $ i $(满足 $ {i=1}^k _i = 1 $)。
    • 均值向量 $ _i $。
    • 协方差矩阵 $ _i $。
  2. 迭代优化参数(EM循环)
    重复以下步骤直到收敛(如对数似然变化小于阈值):

    • E步(期望步)
      对每个样本 $ x_j $,计算其由第 $ i $ 个高斯分布生成的后验概率(责任度 $ {ji} ):$ {ji} = p(z_j = i | x_j) = $$ 其中 $ (x | , ) $ 是高斯分布的概率密度函数。

    • M步(最大化步)
      根据当前的责任度 $ _{ji} $,更新模型参数:

      1. 新均值向量$$ \mu_i' = \frac{\sum_{j=1}^m \gamma_{ji} x_j}{\sum_{j=1}^m \gamma_{ji}} $$
      2. 新协方差矩阵$$ \Sigma_i' = \frac{\sum_{j=1}^m \gamma_{ji} (x_j - \mu_i')(x_j - \mu_i')^\top}{\sum_{j=1}^m \gamma_{ji}} $$
      3. 新混合系数$$ \alpha_i' = \frac{\sum_{j=1}^m \gamma_{ji}}{m} $$
  3. 簇划分

    • 初始化空簇 $ C_i = $。
    • 对每个样本 $ x_j $,计算其属于各簇的后验概率 $ _j = i {ji} $。
    • 将 $ x_j $ 分配到簇 $ C_{_j} $ 中。

E步与M步的核心作用

E步(期望步)

  • 目标:基于当前参数 $ (_i, _i, i) $,计算每个样本 $ x_j $ 属于各高斯分布的责任度 $ {ji} $。
  • 意义
    • 责任度反映了在当前模型下,样本 $ x_j $ 由第 $ i $ 个高斯分布生成的概率。
    • 软分配:允许样本部分属于多个簇,而非硬划分。

M步(最大化步)

  • 目标:根据责任度 $ _{ji} $,重新估计模型参数 $ (_i’, _i’, _i’) $,以最大化数据的对数似然。
  • 关键公式
    • 均值更新:加权平均样本点,权重为责任度。
    • 协方差更新:加权样本点的方差,反映簇内数据分布。
    • 混合系数更新:各簇样本的“有效数量”占总样本的比例。

参考资料

西瓜书读书笔记整理(九) —— 第九章 聚类_西瓜书笔记第9章-CSDN博客

密度聚类与DBSCAN

若样本分布为同心的两个环,kmeans则无法做到良好的聚类效果,因此引出密度聚类

密度聚类是一种基于样本分布密集程度的无监督学习方法,其核心思想是:将高密度区域划分为同一簇,低密度区域视为噪声或边界

DBSCAN(Density-Based Spatial Clustering of Applications with Noise)是密度聚类的典型代表,通过两个关键参数 $ $ 和 $ MinPts $ 描述样本分布的紧密性。

1. 核心概念
  1. $ $-邻域
    • 定义:与样本 $ x $ 距离不超过 $ $ 的所有样本集合。
    • 作用:衡量样本周围的局部密度。
  2. 核心对象(Core Object)
    • 定义:若样本 $ x $ 的 $ $-邻域内包含至少 $ MinPts $ 个样本,则 $ x $ 是核心对象。
    • 作用:作为簇的生长起点,确保簇的最小密度要求。
  3. 密度直达(Directly Density-Reachable)
    • 定义:若样本 $ x_j $ 位于核心对象 $ x_i $ 的 $ $-邻域内,则称 $ x_i $ 可密度直达 $ x_j $。
    • 作用:建立核心对象与邻近样本的直接连接。
  4. 密度可达(Density-Reachable)
    • 定义:若存在样本序列 $ x_i, p_1, p_2, , p_n, x_j $,其中 $ p_i $ 密度直达 $ p_{i+1} $,则称 $ x_i $ 可密度可达 $ x_j $。
    • 作用:通过链式传递扩展簇的范围。
  5. 密度相连(Density-Connected)
    • 定义:若样本 $ x_i $ 和 $ x_j $ 均可密度可达某个公共样本 $ x_k $,则称 $ x_i $ 和 $ x_j $ 密度相连。
    • 作用:确保簇的连通性,避免碎片化。
image-20250607124326529

DBSCN定义的簇

  • 定义:最大密度相连的样本集合为一个簇
  • 有两个性质:1.连接性:同一个簇内任意两样本,必然密度相连2.最大性:密度可达的两个样本必 定属于同一个簇
2. DBSCAN 算法流程

简单来理解DBSCAN:找出一个核心对象所有密度可达的样本集合形成簇。首先从数据集中任选一个核心对象A,找出所有A密度可达的样本集合,将这些样本形成一个密度相连的类簇,直到所有的核心对象都遍历完。DBSCAN算法的流程如下图所示:

image-20250607124446432
3. 参数选择与影响
  • $ $(邻域半径)
    • 过小:可能导致多数样本被标记为噪声,簇数量增加。
    • 过大:可能导致不同簇合并,簇数量减少。
    • 选择方法:通过K-Distance图(排序后的第 $ k $ 近邻距离)观察“拐点”。
  • $ MinPts $(最小样本数)
    • 控制簇的最小密度阈值。
    • 通常取 $ d+1 d $ 为特征维度),避免在高维空间中误判噪声。

层次聚类与AGNES

层次聚类是一种通过构建树状结构(Dendrogram)将数据划分为不同层次的聚类方法。其核心思想是:
- 凝聚型(Agglomerative):从每个样本作为一个独立簇开始,逐步合并最相似的簇,直到达到预设的簇数或形成一个唯一簇。
- 分裂型(Divisive):与凝聚型相反,从整个数据集作为一个簇开始,逐步分裂为更小的簇。

本节重点介绍AGNES(Agglomerative Nesting),一种经典的自底向上的层次聚类算法。

1. AGNES 算法流程
  1. 初始化:每个样本作为一个独立簇。
  2. 迭代合并
    • 计算所有簇对之间的距离。
    • 合并距离最近的两个簇。
  3. 终止条件
    • 达到预设的簇数 $ k $;
    • 所有簇之间的距离大于阈值。
2. 簇间距离的定义

AGNES 的关键在于如何定义簇间距离,常见的三种方法如下:

(1)最小距离(Single Linkage) dmin(Ci, Cj) = minx ∈ Ci, z ∈ Cjdist(x, z) - 含义:两个簇之间最近的两个样本的距离。

(2)最大距离(Complete Linkage) dmax(Ci, Cj) = maxx ∈ Ci, z ∈ Cjdist(x, z) - 含义:两个簇之间最远的两个样本的距离。

(3)平均距离(Average Linkage) $$ d_{\text{avg}}(C_i, C_j) = \frac{1}{|C_i| |C_j|} \sum_{\boldsymbol{x} \in C_i} \sum_{\boldsymbol{z} \in C_j} \text{dist}(\boldsymbol{x}, \boldsymbol{z}) $$ - 含义:两个簇所有样本对距离的平均值。

层次聚类法的算法流程如下所示:
image-20250607125338029

作业

1

假设任务是将下面8个点聚类成3个簇:A1(2,10), A2(2,5), A3(8,4), B1(5,8), B2(7,5), B3(6,4), C1(1,2), C3(4,9),距离函数是欧式距离。假设初始选择A1,B1,C1分别作为每个聚类的中心,用Kmeans算法给出计算过程。

image-20250607125506436
image-20250607125606040
2

Kmeans初始类簇中心如何选取?K值如何确定?请简要阐述。

一、初始类簇中心的选取 (如何选好的起始点?)

传统K-means随机选择初始中心点,容易导致结果不稳定(多次运行结果不同)或陷入局部最优(效果差)。改进方法主要有:

  1. K-means++ (最常用且推荐):
    • 核心思想: 让初始中心点彼此尽量远离。
    • 步骤:
      1. 随机选择第一个中心点。
      2. 计算每个数据点到当前已选中心点的最短距离(即离最近中心的距离)。
      3. 与这个最短距离平方成正比的概率,随机选择下一个中心点(距离越大的点,被选中的概率越大)。
      4. 重复步骤2和3,直到选出K个中心点。
    • 优点: 显著提高聚类质量和稳定性,计算开销增加不大。
  2. 多次运行+选取最优:
    • 独立运行K-means算法多次(每次随机初始化)。
    • 每次运行完成后,计算所有数据点与其所属簇中心的距离平方和(SSE, Sum of Squared Errors)。
    • 选择SSE最小的那次运行结果作为最终结果。
    • 优点: 简单,增加找到更好解的机会。
    • 缺点: 计算开销随运行次数增加。
  3. 基于样本密度/距离:
    • 选择数据空间中样本密度高的区域点作为中心。
    • 或选择相互之间距离较远的点作为中心(类似K-means++的思想,但实现方式可能不同)。

二、K值(簇数量)的确定 (如何知道分几类?)

K值通常需要预先指定,但没有绝对正确的答案。常用方法基于评估不同K值下聚类结果的“质量”,寻找拐点或最优值:

  1. 肘部法则:
    • 核心思想: 随着K增大,簇内样本聚合更紧密,簇内平方和误差(SSE)会下降,但下降幅度会逐渐变缓。找到SSE下降速率发生显著变化的“肘点”。
    • 做法: 计算不同K值(如K=1, 2, 3, …, max)对应的SSE。绘制K值 - SSE曲线图。观察曲线,寻找SSE下降幅度突然变得平缓的那个K值(形如手臂的“肘关节”)。
    • 优点: 直观。
    • 缺点: “肘点”有时不明显或不存在,需要主观判断。
  2. 轮廓系数:
    • 核心思想: 综合衡量一个样本与其自身簇的紧密度(a)和与其他簇的分离度(b)。
    • 计算: 对于每个样本i:
      • a(i) = i 到同簇内所有其他点的平均距离(簇内不相似度)。
      • b(i) = i 到所有其他簇中点的平均距离的最小值(最近邻簇的不相似度)。
      • 样本i的轮廓系数:s(i) = (b(i) - a(i)) / max(a(i), b(i))。值在[-1, 1]之间。
    • 整体评估: 计算所有样本轮廓系数的平均值,作为该K值下聚类的整体轮廓系数。
    • 选择K: 尝试不同K值,选择平均轮廓系数最大对应的K值。轮廓系数越接近1,表示聚类效果越好(簇内紧凑,簇间分离)。
    • 优点: 量化评估,结果在[-1, 1]之间有界。
    • 缺点: 计算量较大,尤其对于大数据集。

参考资料

《机器学习》– 第九章 聚类-腾讯云开发者社区-腾讯云

降维与度量学习

KNN

k近邻算法简称kNN(k-Nearest Neighbor),是一种经典的监督学习方法,是数据挖掘十大算法之一。其工作机制十分简单:给定某个测试样本,kNN基于某种距离度量在训练集中找出与其距离最近的k个带有真实标记的训练样本,然后基于这k个邻居的真实标记来进行预测,类似于集成学习中的基学习器结合策略:分类任务采用投票法,回归任务则采用平均法。

image-20250607150256290

核心思想

1NN 分类器通过将测试样本 $ $ 分配到其最近邻样本 $ $ 的类别来完成预测。其错误概率取决于两个关键因素: - $ $ 的真实类别:$ P(c | ) $,即给定 $ $ 属于类别 $ c $ 的概率。
- $ $ 的类别:$ P(c | ) $,即 $ $ 属于类别 $ c $ 的概率。

错误概率公式

若测试样本 $ $ 的最近邻为 $ 1NN$ P() = 1 - P() = 1 - _{c } P(c | ) P(c | ) $$ 其中: - $ $ 是所有可能的类别集合。
- $ P(c | ) $ 属于类别 $ c $ 的条件概率。
- $ P(c | ) $ 属于类别 $ c $ 的条件概率。

通过证明可以发现一个令人震惊的结论:最近邻分类器的错误率不超过贝叶斯最优分类器错误率的两倍

对于距离度量,不同的度量方法得到的k个近邻不尽相同,从而对最终的投票结果产生了影响,因此选择一个合适的距离度量方法也十分重要。

在上一篇聚类算法中,在度量样本相似性时介绍了常用的几种距离计算方法,包括闵可夫斯基距离,曼哈顿距离,VDM等。在实际应用中,kNN的距离度量函数一般根据样本的特性来选择合适的距离度量,同时应对数据进行去量纲/归一化处理来消除大量纲属性的强权政治影响

低维嵌入

使用knn的前提是样本空间的密度要一定大,但是这个条件在现实中很难满足,因此引出降维操作

kNN的重要假设: 任意测试样本 附近任意小的 距离范围内总能找到一个训练样本,即训练样本的采样密度足够大,或称为 “密采样”( dense sample) 。然而,这个假设在现实任务中通常很难满足

样本的特征数也称为维数(dimensionality),当维数非常大时,也就是通常所说的“维数灾难”(curse of dimensionality),具体表现在:在高维情形下,数据样本变得十分稀疏,因为此时要满足训练样本为“密采样”的总体样本数目是一个触不可及的天文数字。训练样本的稀疏使得其代表总体分布的能力大大减弱,从而消减了学习器的泛化能力;同时当维数很高时,计算距离也变得十分复杂,甚至连计算内积都不再容易

缓解维数灾难的一个重要途径就是降维(dimension reduction),即通过某种数学变换将原始高维空间转变到一个低维的子空间。在这个子空间中,样本的密度将大幅提高,同时距离计算也变得容易。这

时也许会有疑问,降维之后不是会丢失原始数据的一部分信息吗?

实际上,在很多实际问题中,虽然训练数据是高维的,但是与学习任务相关也许仅仅是其中的一个低维子空间,也称为一个低维嵌入,例如:数据属性中存在噪声属性、相似属性或冗余属性等,对高维数据进行降维能在一定程度上达到提炼低维优质属性或降噪的效果

MDS算法

MDS(Multidimensional Scaling,多维尺度分析)是一种经典的降维技术,其核心目标是将高维数据映射到低维空间(如二维或三维),同时尽可能保留原始数据中样本点之间的距离关系。以下是其核心原理与应用要点:

1. 核心思想

  • 输入:一个样本点之间的距离矩阵 $ D $(如欧氏距离、余弦距离等)。
  • 输出:低维空间中样本点的坐标矩阵 $ Z $,使得低维空间中的距离与原始距离尽可能一致 。
  • 关键假设:高维数据的内在结构可通过样本间的距离关系描述,降维后需最小化这种关系的失真。

2. 算法步骤

MDS 的核心是通过矩阵分解从距离矩阵推导低维坐标: 1. 构建距离矩阵 $ D $
对于 $ r $ 个样本,计算两两之间的距离,形成 $ r r $ 的矩阵 $ D $,其中 $ D_{ij} $ 表示样本 $ i $ 和 $ j $ 的距离 。

  1. 双中心化(Double Centering)
    构造矩阵 $ B = - H D^{(2)} H $,其中 $ D^{(2)} $ 是距离的平方矩阵,$ H = I - ^$ 是中心化矩阵 。

  2. 特征值分解
    对 $ B $ 进行特征值分解,得到 $ B = V V^$,其中 $ $ 是按降序排列的特征值对角矩阵,$ V $ 是对应的特征向量矩阵 。

  3. 构造低维坐标
    选择前 $ d’ $ 个最大特征值($ d’ $ 为目标维度)和对应的特征向量,计算低维坐标矩阵:
    Z = Λ1/2V 其中 $ ^{1/2} $ 是特征值矩阵的平方根 。

3. 关键特性

  • 保留距离关系:MDS 直接优化低维空间中的距离与原始距离的一致性,适用于需精确保留样本相似性的场景(如生物信息学中的基因关系分析)。
  • 非线性适应性:与 PCA 不同,MDS 不要求数据线性分布,更适合处理非线性结构(如环形、流形数据)。
  • 灵活性:支持任意距离度量(如自定义的相似性指标),而 PCA 仅适用于欧氏距离 。

线性降维方法

线性降维通过线性变换将高维数据 $ ^{d m} $ 投影到低维空间 $ ^{d’ m} d’ d ),$ = ^ $$

  • 变换矩阵 $ ^{d d’} $
    每一列是正交的基向量,构成低维子空间的坐标系。

  • 目标:选择 $ $ 使得低维表示 $ $ 最大化保留原始数据的信息(如方差、距离等)。

  • MDS
    直接以保留高维空间中样本点之间的距离关系为目标。降维后的低维空间需尽可能保持原始样本两两之间的距离(如欧氏距离、自定义相似性距离)。

    • 示例:在基因数据分析中,MDS可确保基因表达相似的样本在低维空间中仍紧密分布。
  • 其他线性方法(如PCA、LDA)

    • PCA:最大化数据在低维空间的方差,强调保留全局结构而非具体距离。
    • LDA:在监督学习中最大化类间分离度,忽略类内距离。

主成分分析

不同于MDS采用距离保持的方法,主成分分析(Principal Component Analysis ,PCA)是一种经典的无监督降维算法 ,其核心目标是通过线性变换将高维数据映射到低维空间,同时保留数据的最大方差信息 (即信息损失最小)

直接通过一个线性变换,将原始空间中的样本投影到新的低维空间中。

简单来理解这一过程便是:PCA采用一组新的基(向量)来表示样本点,其中每一个基向量都是原始空间基向量的线性组合,通过使用尽可能少的新基向量来表出样本,从而达到降维的目的。

image-20250607155733314

假设使用d’个新基向量来表示原来样本,实质上是将样本投影到一个由d’个基向量确定的一个超平面上(即舍弃了一些维度),要用一个超平面对空间中所有高维样本进行恰当的表达,最理想的情形是:若这些样本点都能在超平面上表出且这些表出在超平面上都能够很好地分散开来。但是一般使用较原空间低一些维度的超平面来做到这两点十分不容易,因此我们退一步海阔天空,要求这个超平面应具有如下两个性质:

最近重构性:样本点到超平面的距离足够近,即尽可能在超平面附近;

最大可分性:样本点在超平面上的投影尽可能地分散开来,即投影后的坐标具有区分性。

这里十分神奇的是:最近重构性与最大可分性虽然从不同的出发点来定义优化问题中的目标函数,但最终这两种特性得到了完全相同的优化问题

image-20250607165159235
协方差矩阵与优化求解

若数据已中心化(均值为零),则 $ ^$ 是样本协方差矩阵的 $ m $ 倍。此时,PCA的优化问题转化为: $$ \begin{aligned} & \underset{\mathbf{W}}{\text{maximize}} & & \text{tr}\left( \mathbf{W}^\top \mathbf{X} \mathbf{X}^\top \mathbf{W} \right) \\ & \text{subject to} & & \mathbf{W}^\top \mathbf{W} = \mathbf{I} \end{aligned} $$ 通过拉格朗日乘数法,该问题的解为 $ ^$ 的前 $ d’ $ 个最大特征值对应的特征向量

PCA的数学推导
  • 优化目标
    maxW  tr(WXXW)  s.t.  WW = I 其中,$ ^{d m} $ 是中心化后的数据矩阵(均值为零)。

  • 拉格朗日乘数法
    引入拉格朗日乘子 $ $ (, ) = ( ^ ^ ) - ( (^ - ) ) $$ 对 $ \mathbf{W} $ 求导并令导数为零,得到: $$ ^ = $$ 即 $ \mathbf{X} \mathbf{X}^\top $ 的特征向量 $ \mathbf{w}_i $ 满足: $$ ^_i = _i _i $$

PCA特征向量选择

1. 核心问题

在PCA中,我们希望找到一个 $ d’ d $ 的变换矩阵 $ $,其列向量是协方差矩阵 $ ^$ 的特征向量,且满足正交约束 $ ^ = $。关键问题是:如何从 $ d $ 个特征向量中选择 $ d’ $ 个最优的?

2. 数学推导

  1. 特征值分解
    协方差矩阵 $ {d d} $ 可分解为: XXW = WΛ 其中,$ = (_1, _2, , _d) $ 是特征值对角矩阵,$ $ 是特征向量矩阵。

  2. 优化目标转化
    PCA的目标是最大化 $ (^ ^) $ ^ ^ = ^( ) = {} () = {i=1}^{d’} _i $$ 即选择 $ d’ $ 个最大的特征值 $ _i $ 对应的特征向量组成 $ $。

3. 特征向量选择策略

  • 按特征值排序
    特征值 $ _i $ 表示数据沿特征向量 $ _i $ 方向的方差。选择前 $ d’ $ 个最大特征值对应的特征向量,可保留最多信息。
  • 正交性保证
    特征向量矩阵 $ $ 的列自动满足 $ ^ = $,无需额外正交化。
PCA算法的整个流程如下图所示:
image-20250607170020467

核化线性降维

待学习

流形学习

流形学习(manifold learning)是一种借助拓扑流形概念的降维方法,流形是指在局部与欧式空间同胚的空间,即在局部与欧式空间具有相同的性质,能用欧氏距离计算样本之间的距离。这样即使高维空间的分布十分复杂,但是在局部上依然满足欧式空间的性质,基于流形学习的降维正是这种 “邻域保持” 的思想。其中 等度量映射(Isomap)试图在降维前后保持邻域内样本之间的距离,而局部线性嵌入(LLE)则是保持邻域内样本之间的线性关系

等度量映射Isomap

等度量映射的基本出发点是:高维空间中的直线距离具有误导性,因为有时高维空间中的直线距离在低维空间中是不可达的。因此利用流形在局部上与欧式空间同胚的性质,可以使用近邻距离来逼近测地线距离,即对于一个样本点,它与近邻内的样本点之间是可达的,且距离使用欧式距离计算,这样整个样本空间就形成了一张近邻图,高维空间中两个样本之间的距离就转为最短路径问题。可采用著名的Dijkstra算法Floyd算法计算最短距离,得到高维空间中任意两点之间的距离后便可以使用 MDS 算法来其计算低维空间中的坐标。

image-20250607171119645

Isomap算法流程如下图:

image-20250607171258284

对于近邻图的构建,常用的有两种方法:一种是指定近邻点个数,像kNN一样选取k个最近的邻居;另一种是指定邻域半径,距离小于该阈值的被认为是它的近邻点。但两种方法均会出现下面的问题:

邻域范围指定过大,则会造成“短路问题”,即本身距离很远却成了近邻,将距离近的那些样本扼杀在摇篮。

邻域范围指定过小,则会造成“断路问题”,即有些样本点无法可达了,整个世界村被划分为互不可达的小部落。

局部线性嵌入

待学习

度量学习

1. 核心思想

度量学习(Metric Learning)的核心目标是学习一个合理的距离度量,使得相似样本距离更近,不相似样本距离更远。传统欧式距离(Euclidean Distance)虽然简单,但其固定权重无法反映不同特征的实际重要性。因此,我们引入加权欧式距离,通过可调节的参数(权重)优化距离计算。

2. 欧式距离与加权欧式距离

  • 标准欧式距离
    $$ \text{dist}_{\text{ed}}^2(\boldsymbol{x}_i, \boldsymbol{x}_j) = \|\boldsymbol{x}_i - \boldsymbol{x}_j\|_2^2 = \sum_{k=1}^d (\boldsymbol{x}_{i,k} - \boldsymbol{x}_{j,k})^2 $$ 每个特征维度对距离的贡献相同,未考虑特征的重要性差异。

  • 加权欧式距离
    distwed2(xi, xj) = (xi − xj)W(xi − xj) 其中,$ = () $ 是对角权重矩阵,$ w_k $ 表示第 $ k $ 个特征的权重。
    展开后为: $$ \text{dist}_{\text{wed}}^2(\boldsymbol{x}_i, \boldsymbol{x}_j) = \sum_{k=1}^d w_k (\boldsymbol{x}_{i,k} - \boldsymbol{x}_{j,k})^2 $$

3. 权重的作用

  • 特征重要性调节
    • 高权重 $ w_k $:强调第 $ k $ 维特征对距离的影响(如图像的颜色通道比位置更重要)。
    • 低权重 $ w_k $:弱化噪声或冗余特征的影响。
  • 几何意义
    加权欧式距离相当于在各特征维度上进行缩放,将数据映射到一个新的空间,使得关键特征的差异更显著。

4. 度量学习的目标

通过学习最优权重 $ 使: − * *  * *: _{}^2(_i, j) )。 − * *  * *: {}^2(_i, _j) $)。

典型优化问题形式: minw  ∑(xi, xj) ∈ Sdistwed2(xi, xj) + λw22 其中,$ S $ 是相似样本对集合,$ $ 是正则化项防止过拟合。

总结来说,

  • 降维是将原高维空间嵌入到一个合适的低维子空间中,接着在低维空间中进行学习任务
  • 度量学习则是试图去学习出一个 *距离度量* 来等效降维的效果
LMNN(Large Margin Nearest Neighbors)详解

1. 核心思想

LMNN 是一种监督度量学习方法,其目标是通过学习一个线性变换矩阵 $ $,使同类样本在变换后的空间中更紧密不同类样本被推开,从而提升KNN等基于距离的算法性能。其核心是引入最大边距(Large Margin)的概念,类似于SVM的分类边界。

2. 损失函数

LMNN 的优化目标由两部分组成: - Pull Loss(拉力损失)
使同类样本对的距离尽可能小,公式为: $$ \varepsilon_{\text{pull}}(\mathbf{L}) = \sum_{j \sim i} \|\mathbf{L}(\bar{\boldsymbol{x}}_i - \bar{\boldsymbol{x}}_j)\|^2 $$ 其中,$ j i $ 表示与样本 $ i $ 同类的最近邻样本。

  • Push Loss(推力损失)
    使不同类样本对的距离至少保持一个固定边距 $ {ijl} $ {}() = {i,j,l} (1 - y{il}) + $$ 其中,$ y{il} = 1 $ 表示样本 $ i $ 和 $ l $ 属于同一类,否则为0;$ []_+ $ 表示取正值部分。

  • 总损失函数
    ε(L) = (1 − μ)εpull(L) + μεpush(L) 参数 $ $ 控制两类损失的权重。

3. 优化问题

LMNN 的目标是最小化总损失函数,同时满足以下约束: $$ \begin{aligned} & \min_{\mathbf{M}, \boldsymbol{\xi}} \quad (1 - \mu) \sum_{i,j \sim i} (\bar{\boldsymbol{x}}_i - \bar{\boldsymbol{x}}_j)^\top \mathbf{M} (\bar{\boldsymbol{x}}_i - \bar{\boldsymbol{x}}_j) + \mu \sum_{i,j \sim i,l} (1 - y_{il}) \xi_{ijl} \\ & \text{s.t.} \quad (\bar{\boldsymbol{x}}_i - \bar{\boldsymbol{x}}_l)^\top \mathbf{M} (\bar{\boldsymbol{x}}_i - \bar{\boldsymbol{x}}_l) - (\bar{\boldsymbol{x}}_i - \bar{\boldsymbol{x}}_j)^\top \mathbf{M} (\bar{\boldsymbol{x}}_i - \bar{\boldsymbol{x}}_j) \geq 1 - \xi_{ijl}, \\ & \quad \quad \quad \xi_{ijl} \geq 0, \quad \mathbf{M} \succeq 0. \end{aligned} $$ - 约束(1):确保不同类样本对的距离比同类样本对大至少 $ 1 - {ijl} $。
- 约束(2):松弛变量 $
{ijl} $ 允许部分样本对违反约束。
- 约束(3):$ $ 必须是半正定矩阵,保证距离的非负性和三角不等式。

作业

1

数据降维有哪些常用的方法?阐述主成分分析(PCA)算法的计算流程,并讨论PCA 降维之后的维度如何确定?

(1)常用数据降维方法

  1. 主成分分析(PCA):通过线性变换保留最大方差方向,适用于去噪和压缩数据 。
  2. 线性判别分析(LDA):在监督学习中最大化类间分离度,适用于分类任务 。

(2)主成分分析(PCA)的计算流程

  1. 数据标准化:对原始数据去均值、方差归一化,消除量纲影响 。
  2. 计算协方差矩阵
    $$ \mathbf{\Sigma} = \frac{1}{m} \mathbf{X} \mathbf{X}^\top $$ 其中 $ $ 是中心化后的数据矩阵 。
  3. 特征值分解:对协方差矩阵进行特征值分解,得到特征值 $ _i $ 和单位正交特征向量 $ _i $ 。
  4. 选择主成分:按特征值大小排序,选择前 $ d’ $ 个最大特征值对应的特征向量构成变换矩阵 $ = [_1, 2, , {d’}] $。
  5. 降维投影:计算低维表示 $ = ^ $,其中 $ ^{d’ m} $ 。

(3)PCA降维后维度的确定

  • 累积方差贡献率:选择前 $ d’ $ 个主成分,使累计方差占比达到阈值(如95%)。
  • 肘部法则(Elbow Method):绘制特征值随维度变化的曲线,选择“拐点”作为 $ d’ $。
2

度量学习的目标是什么?LMNN算法中三元组损失是什么?如何计算?

(1)度量学习的目标

度量学习旨在学习一个合理的距离度量,使得: - 相似样本:距离尽可能小(如同类样本)。
- 不相似样本:距离尽可能大(如异类样本)。
典型应用包括推荐系统(优化用户-商品相似性)、图像检索(提升匹配精度)和生物识别(增强类间可分性)。

(2)LMNN中的三元组损失

LMNN(Large Margin Nearest Neighbor)是一种监督度量学习方法,其核心思想是通过优化距离度量来提升KNN的分类性能。虽然LMNN本身主要使用对比损失(Contrastive Loss),但三元组损失(Triplet Loss)是深度度量学习中常见的损失函数,其计算方式如下:三元组损失的定义

三元组损失基于锚点(Anchor)、正例(Positive)和负例(Negative)三个样本,目标是使锚点与正例的距离小于锚点与负例的距离,公式为: ℒ = ∑i, j, lmax (0, ∥zi − zj2−∥zi − zl2 + m) - $ _i $:锚点样本的嵌入表示。
- $ _j $:与锚点同类的正例样本。
- $ _l $:与锚点不同类的负例样本。
- $ m $:预设的边界值(Margin),控制正负样本距离的最小差距 。

LMNN的损失函数

LMNN 的损失函数包含两部分: 1. 拉力损失(Pull Loss):最小化同类样本对的距离:
$$ \varepsilon_{\text{pull}} = \sum_{i,j \sim i} \|\mathbf{L}(\bar{\boldsymbol{x}}_i - \bar{\boldsymbol{x}}_j)\|^2 $$ 2. 推力损失(Push Loss):最大化异类样本对的距离:
$$ \varepsilon_{\text{push}} = \sum_{i,j \sim i,l} (1 - y_{il}) \left[1 + \|\mathbf{L}(\bar{\boldsymbol{x}}_i - \bar{\boldsymbol{x}}_j)\|^2 - \|\mathbf{L}(\bar{\boldsymbol{x}}_i - \bar{\boldsymbol{x}}_l)\|^2\right]_+ $$ 其中 $ $ 是线性变换矩阵,$ y_{il} $ 表示样本对是否同类,$ []_+ $ 表示取正值部分 。

优化目标

LMNN 的总损失为拉力和推力损失的加权和: ε(L) = (1 − μ)εpull + μεpush 参数 $ $ 平衡两类损失的权重,最终通过优化 $ $ 得到最优距离度量 。

image-20250607175825520

半监督学习

监督学习解决现实问题有哪些难点? 1.标记数据获取成本高:在许多领域如医疗,获取标记数据是昂贵且耗时的。 2.未标记数据大量存在且易得:相对而言,未标记数据大量存在且容易获取。 3.提升模型的泛化能力:通过利用未标记数据,可以增强模型的泛化能力。 举例:在医疗领域,获取医生标记的诊断数据非常昂贵,但有大量未标记的病人记录。 半监督学习可以帮助利用这些未标记数据,提高疾病预测模型的准确性。

image-20250607181345721半监督学习结合了有监督学习和无监督学习,半监督学习使用少量的标记数据大量的未标记数据来训练模型,主要目标是提升模型在未标记数据上的表现。

基于生成模型的方法

假设所有数据(无论是否有标记)都是由一个潜在的模型“生成”的。那么无标记的数据可以帮助更准确的估计潜在模型的参数。 比如右图中可以看到数据可以由两个高斯分布近似,则无监督的数据可以被用来更好得做高斯分布的参数估计

image-20250607183201926
半监督SVM

监督学习中的SVM试图找到一个划分超平面,使得两侧支持向量之间的间隔最大,即 最大划分间隔 思想。对于半监督SVM (Semi-Supervised Support Vector Machine, S3VM) 则考虑超平面在能将两类标记样本分隔的同时,穿过数据低密度的区域

image-20250607183349866
TSVM(Transductive Support Vector Machine)

1. 核心思想

TSVM 是一种半监督学习方法,通过结合有标记数据 $ D_l $ 和未标记数据 $ D_u $,利用伪标签(Pseudo-labels)和迭代优化策略,最大化分类超平面的间隔。其损失函数需同时考虑: - 有标记样本:最小化分类错误(Hinge Loss)。
- 未标记样本:通过伪标签引入约束,逐步调整超平面。

2. 损失函数推导

TSVM 的目标是找到一个超平面 $ ^ + b = 0 $,使得: 1. 有标记样本的分类误差最小。
2. 未标记样本的伪标签与超平面预测结果一致。

标准SVM的损失函数为: $$ \min_{\boldsymbol{w}, b, \xi} \quad \frac{1}{2} \|\boldsymbol{w}\|^2 + C \sum_{i=1}^l \xi_i $$ 其中,$ _i $ 是松弛变量,表示样本 $ (_i, y_i) $ 的分类误差。

TSVM的扩展
引入未标记样本 $ D_u $ 的伪标签 $ j j = l+1, , l+u $),并赋予其较小的惩罚系数 $ C_u $(初始阶段 $ C_u C_l ):$ {, b, } ||^2 + C_l _{i=1}^l i + C_u {j=l+1}^{l+u} _j $$ 其中: - $ C_l $:有标记样本的惩罚系数。
- $ C_u $:未标记样本的惩罚系数,初始值很小,逐步增大以增强伪标签的影响。

3. 迭代优化流程

  1. 初始化
    • 用有标记数据 $ D_l $ 训练初始 SVM,得到 $ _0, b_0 $。
    • 对未标记数据 $ D_u $ 预测伪标签 $ _j = (_0^_j + b_0) $。
  2. 伪标签调整
    • 若存在冲突(如 $ _i _j < 0 $ 且 $ _i + _j > 2 $),翻转其中一个伪标签(如 $ _i -_i $)。
    • 重新求解优化问题,更新 $ , b $。
  3. 参数调整
    • 逐步增大 $ C_u $(如 $ C_u {2C_u, C_l} $),增强未标记样本的影响。

4. 关键数学细节

  • Hinge Loss
    对每个样本 $ (_i, y_i) $ _i = (0, 1 - y_i (^_i + b)) $$ 未标记样本的伪标签 $ _j $ 同样代入此公式,但惩罚系数为 $ C_u $。

  • 正则化项
    $ ||^2 $ 确保超平面的泛化能力,防止过拟合。

  • 伪标签翻转条件
    当两个未标记样本 $ i, j $ 满足: ij < 0  且  ξi > 0, ξj > 0,  ξi + ξj > 2 表示它们被错误分类且距离超平面较近,需翻转其中一个标签以减少冲突。

图半监督学习

给定一个数据集,我们可将其映射为一个图,数据集中每个样本对应于图结点,若两个样本之间的相似度很高(或相关性很强),则对应的结点之间存在一条边,边的“强度”(strength) 正比于样本之间的相似度(或相关性)。

可将有标记样本所对应的结点想象为染过色,标记样本所对应的结点尚未染色。半监督学习就对应于“颜色”在图上扩散或传播的过程。由于个图对应了一个矩阵,我们就能基于矩阵运算来进行半监督学习算法的推导与分析。

image-20250607184534217

图半监督学习中的能量函数推导详解

1. 图结构与亲和矩阵

给定有标记数据集 $ D_l = {(1, y_1), (2, y_2), , (l, y_l)} $ 和未标记数据集 $ D_u = {{l+1}, {l+2}, , {l+u}} $,构建图 $ G = (V, E) : − * *  * *: V = {1, , l, {l+1}, , {l+u}} $,包含所有样本。
- 边集:通过亲和矩阵 $ $ 表示,元素定义为: $$ (\mathbf{W})_{ij} = \begin{cases} \exp\left(-\frac{\|\boldsymbol{x}_i - \boldsymbol{x}_j\|^2}{2\sigma^2}\right), & i \neq j \\ 0, & \text{otherwise} \end{cases} $$ 其中,$ $ 是高斯核的带宽参数,控制邻接关系的敏感性。

2. 能量函数的定义与推导

假设分类模型的输出标记为 $ f(_i) $(取值为类别标签,如 $ $),定义能量函数 $ E(f) $ 为: $$ E(f) = \frac{1}{2} \sum_{i=1}^m \sum_{j=1}^m (\mathbf{W})_{ij} (f(\boldsymbol{x}_i) - f(\boldsymbol{x}_j))^2 $$ 其中 $ m = l + u $ 是总样本数。

3. 能量函数的展开与简化

  1. 展开平方项 $$ E(f) = \frac{1}{2} \sum_{i=1}^m \sum_{j=1}^m (\mathbf{W})_{ij} \left[ f^2(\boldsymbol{x}_i) - 2 f(\boldsymbol{x}_i) f(\boldsymbol{x}_j) + f^2(\boldsymbol{x}_j) \right] $$
  2. 利用对称性简化 由于 $ $ 是对称矩阵((W)ij = (W)ji),可交换求和顺序: $$ \sum_{i=1}^m \sum_{j=1}^m (\mathbf{W})_{ij} f^2(\boldsymbol{x}_j) = \sum_{j=1}^m \sum_{i=1}^m (\mathbf{W})_{ji} f^2(\boldsymbol{x}_j) = \sum_{i=1}^m \sum_{j=1}^m (\mathbf{W})_{ij} f^2(\boldsymbol{x}_i) $$ 因此,能量函数变为 $$ E(f) = \frac{1}{2} \left( 2 \sum_{i=1}^m \sum_{j=1}^m (\mathbf{W})_{ij} f^2(\boldsymbol{x}_i) - 2 \sum_{i=1}^m \sum_{j=1}^m (\mathbf{W})_{ij} f(\boldsymbol{x}_i) f(\boldsymbol{x}_j) \right) $$
  3. 引入度矩阵 定义度矩阵 $ $ 为对角矩阵,其对角线元素为: $$ d_i = \sum_{j=1}^m (\mathbf{W})_{ij} $$ 最终能量函数可表示为: $$ E(f) = \sum_{i=1}^m d_i f^2(\boldsymbol{x}_i) - \sum_{i=1}^m \sum_{j=1}^m (\mathbf{W})_{ij} f(\boldsymbol{x}_i) f(\boldsymbol{x}_j) = \boldsymbol{f}^\top (\mathbf{D} - \mathbf{W}) \boldsymbol{f} $$ 其中,$ = [f(_1), f(_2), , f(_m)]^$。

图半监督学习方法推导详解

1. 分块矩阵表示

将亲和矩阵 $ $ 和度矩阵 $ $ 按有标记数据(前 $ l $ 行列)和未标记数据(后 $ u $ 行列)分块: $$ \mathbf{W} = \begin{bmatrix} \mathbf{W}_{ll} & \mathbf{W}_{lu} \\ \mathbf{W}_{ul} & \mathbf{W}_{uu} \end{bmatrix}, \quad \mathbf{D} = \begin{bmatrix} \mathbf{D}_{ll} & \mathbf{0}_{lu} \\ \mathbf{0}_{ul} & \mathbf{D}_{uu} \end{bmatrix} $$ 其中: - $ {ll} $:有标记数据间的亲和度。
- $
{lu} $:有标记与未标记数据间的亲和度。
- $ {uu} $:未标记数据间的亲和度。
- $
{ll}, _{uu} $:对应子图的度矩阵。

2. 能量函数的分块展开

能量函数 $ E(f) = ^( - ) $ 可展开为

展开后得到: E(f) = fl(Dll − Wll)fl − 2fuWulfl + fu(Duu − Wuu)fu

**3. 对未标记数据 $ _u $ 求偏微分**

目标是最小化 $ E(f) $,对 $ _u $ 求偏导并令其为零: $$ \frac{\partial E(f)}{\partial \boldsymbol{f}_u} = -2 \mathbf{W}_{ul} \boldsymbol{f}_l + 2 (\mathbf{D}_{uu} - \mathbf{W}_{uu}) \boldsymbol{f}_u = 0 $$ 解得: fu = (Duu − Wuu)−1Wulfl

协同训练

协同训练(Co-training)是一种经典的半监督学习方法,由Blum和Mitchell于1998年首次提出,主要用于处理多视图数据(Multi-view Data)。其核心思想是通过多个分类器的协作,利用少量标记数据和大量未标记数据提升模型性能。以下是详细解析:

1. 核心思想与假设

(1)多视图数据

  • 定义:每个样本可被划分为多个充分冗余且条件独立的视图(View)。
    • 充分冗余:每个视图本身包含足够信息,可独立完成学习任务。
    • 条件独立性:在给定类别标签的条件下,不同视图的特征相互独立。
      例如,网页数据可划分为“文本内容”和“超链接结构”两个视图,它们共同描述网页内容。

(2)协作机制

  • 双分类器设计:使用两个分类器 $ h_1 $ 和 $ h_2 $,分别基于视图 $ V_1 $ 和 $ V_2 $ 进行训练。
  • 伪标签生成:分类器 $ h_1 $ 对未标记数据的高置信度预测结果会被 $ h_2 $ 使用,反之亦然,形成迭代优化。
  • 目标:通过分类器间的互补性,逐步扩展标记数据集,提升模型泛化能力。

2. 算法流程

  1. 初始化阶段
    • 使用少量标记数据 $ D_l $,分别训练分类器 $ h_1 $(基于视图 $ V_1 $)和 $ h_2 $(基于视图 $ V_2 $)。
  2. 伪标签生成
    • 对未标记数据 $ D_u h_1 $ 预测视图 $ V_1 $ 的伪标签,$ h_2 $ 预测视图 $ V_2 $ 的伪标签。
    • 选择置信度高于阈值的样本加入训练集(如 $ h_1 $ 的预测结果用于更新 $ h_2 $ 的训练数据,反之亦然)。
  3. 迭代优化
    • 重复伪标签生成和模型训练,直到未标记数据耗尽或模型收敛。

3. 核心优势

  • 减少对标注数据的依赖:仅需少量标记数据即可训练高性能模型,尤其适合标注成本高的场景(如医疗影像分析)。
  • 提升模型鲁棒性:分类器间的协作可纠正彼此的错误,降低单一模型过拟合风险。
  • 多视图互补性:不同视图的信息融合能捕捉更全面的特征(如图像的RGB通道与纹理特征)。

作业

1

什么是半监督学习?请简要描述其基本思想。半监督学习相比于监督学习和无监督学习有什么优势和应用场景?

(1)定义与基本思想

半监督学习(Semi-Supervised Learning)是结合监督学习(利用标记数据)和无监督学习(利用未标记数据)的机器学习方法,其核心思想是通过少量标记数据与大量未标记数据的联合训练,提升模型的泛化能力和鲁棒性。
- 监督学习:依赖大量人工标注数据(如分类、回归)。
- 无监督学习:仅利用数据分布规律(如聚类、降维)。
- 半监督学习:在标记数据稀缺时,通过未标记数据挖掘潜在结构,降低标注成本 。

(2)优势

  • 减少标注依赖:仅需少量标记数据即可训练高性能模型,适用于标注成本高的场景(如医疗影像分析)。
  • 提升模型性能:利用未标记数据增强数据多样性,缓解过拟合风险。
  • 平衡效率与精度:在资源有限时,兼顾监督学习的准确性与无监督学习的高效性 。

(3)应用场景

  • 医学诊断:利用少量标注的病理图像和大量未标注数据训练疾病预测模型。
  • 推荐系统:结合用户行为(有标记)与商品属性(未标记)优化排序模型。
  • 自然语言处理:通过预训练模型(如GPT)的“预训练+微调”框架,减少人工标注需求 。
2

协同训练算法的作用是什么?请简述算法主要流程和所需条件。

(1)作用与核心思想

协同训练是一种典型的半监督学习方法,适用于多视图数据(Multi-view Data)。其核心思想是通过多个分类器的协作,利用未标记数据扩展训练集,最终提升模型性能。

  • 多视图条件
    • 充分冗余:每个视图本身包含足够信息,可独立完成任务。
    • 条件独立性:在给定类别标签的条件下,不同视图的特征相互独立 。

(2)算法流程

  1. 初始化阶段
    • 使用少量标记数据 $ D_l $,分别训练两个分类器 $ h_1 $(基于视图 $ V_1 $)和 $ h_2 $(基于视图 $ V_2 $)。
  2. 伪标签生成
    • 对未标记数据 $ D_u h_1 $ 预测 $ V_2 $ 的伪标签,$ h_2 $ 预测 $ V_1 $ 的伪标签。
    • 选择置信度高于阈值的样本加入训练集(如 $ h_1 $ 的预测结果用于更新 $ h_2 $ 的训练数据,反之亦然)。
  3. 迭代优化
    • 重复伪标签生成和模型训练,直到未标记数据耗尽或模型收敛 。

(3)所需条件

  • 多视图划分:数据需满足“充分冗余”和“条件独立性”(如图像的RGB通道与纹理特征)。
  • 分类器多样性:选择差异较大的分类器(如SVM + 决策树),增强互补性。
  • 伪标签可靠性:初始模型需有一定性能,避免错误伪标签污染训练集 。

项目要求

智能天气提醒助手

描述:开发一款web应用,实时获取天气数据并支持个性化提醒(如雨天带伞)。

要求:

调用天气API获取实时数据(如OpenWeatherMap,每天1000次免费调用)

使用前端三件套设计交互界面,展示当前及未来天气信息,空气质量、体感温度、日出日落、月相等信息;

使用fastapi做后端

支持地点设置和天气提醒条件配置,在预设的提醒条件下提醒用户,并且将用户偏好保存至本地文件。

多城市切换、历史天气查询、全球地图展示等额外功能(可选*)。

技术栈

fastapi,前端三件套(fetchapi),apifox

fetchapi

Fetch API 是现代浏览器提供的标准网络请求接口,允许开发者通过 JavaScript 发起异步 HTTP 请求(如 GET、POST、PUT、DELETE 等),并处理响应数据(如 JSON、文本、图片等)。它是传统 XMLHttpRequest(AJAX)的替代方案,语法更简洁,且支持 Promise 异步编程。

简单来说,就是用作给后端发送请求,实现前后端分离

image-20250609163335375

用法学习

在使用 fetch 发起 HTTP 请求时,methodheadersbody 是配置请求的核心参数,它们共同决定了请求的行为和数据格式。以下是每个参数的具体作用及示例:

1. method: 'POST'

作用

指定 HTTP 请求的方法(动词),POST 表示向服务器提交数据(如创建资源)。 - 常见方法: - GET:获取数据(默认方法,无需显式声明)。 - POST:提交数据(如新增记录)。 - PUT:更新数据。 - DELETE:删除数据。 - 与后端交互:FastAPI 的路由通过 @app.post()@app.get() 等装饰器匹配请求方法。

示例

1
2
3
4
fetch('https://api.example.com/submit', {
method: 'POST', // 告诉服务器这是一个提交请求
// ...其他配置
});
2. headers 请求头

作用

定义请求的元信息,用于告知服务器如何处理请求和数据格式。 - 关键字段: - Content-Type:指定请求体(body)的数据格式。 - application/json:表示发送 JSON 数据。 - application/x-www-form-urlencoded:表示表单数据(键值对)。 - multipart/form-data:用于上传文件。 - Authorization:携带身份凭证(如 Token)。 - Accept:声明客户端期望的响应格式(如 JSON、XML)。

示例

1
2
3
4
headers: {
'Content-Type': 'application/json', // 告诉服务器请求体是 JSON
'Authorization': 'Bearer your_token_here' // 身份验证(可选)
}
3. body: JSON.stringify(item)

作用

定义请求体(即发送给服务器的数据),需根据 Content-Type 的类型进行格式化。 - JSON.stringify(item):将 JavaScript 对象转换为 JSON 字符串。 - 因为 HTTP 协议只能传输文本,不能直接传输对象。 - 注意事项: - 若未设置 Content-Type: application/json,服务器可能无法正确解析数据。 - 若使用 FormData 上传文件,需使用 multipart/form-data 格式。

示例

1
2
3
const item = { name: "Apple", price: 1.99 };

body: JSON.stringify(item) // 转换为 '{"name":"Apple","price":1.99}'
完整示例:向 FastAPI 提交数据

FastAPI 后端定义

1
2
3
4
5
6
7
8
9
10
11
12
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):
name: str
price: float

@app.post("/items/")
async def create_item(item: Item):
return {"message": "Item created", "item": item}

前端调用

1
2
3
4
5
6
7
8
9
10
11
12
const item = { name: "Banana", price: 0.99 };

fetch('http://localhost:8000/items/', {
method: 'POST',
headers: {
'Content-Type': 'application/json' // 必须与数据格式匹配
},
body: JSON.stringify(item) // 将对象转为 JSON 字符串
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
总结
参数 作用 必填性
method 定义请求类型(如 POST 必填(非 GET 时)
headers 声明数据格式、身份凭证等 必填(尤其 Content-Type
body 发送的数据(需格式化为字符串) 必填(POST/PUT 时)

关键点
- POST 请求必须设置 headers['Content-Type']body。 - JSON.stringify() 是发送 JSON 数据的关键步骤。 - FastAPI 会根据 Content-Type 自动解析请求体并进行数据校验(通过 Pydantic 模型)。

CORS

CORS 是什么?

CORS(Cross-Origin Resource Sharing) 是一种浏览器安全机制,用于解决 跨域请求 的问题。它允许服务器明确授权某些跨域请求,从而在保障安全的前提下,实现前后端分离架构中的跨域通信。

为什么需要 CORS?

1. 同源策略(Same-Origin Policy)

浏览器默认遵循 同源策略,即网页只能请求与自身 同源(相同域名、协议、端口) 的资源。
例如

  • 前端地址:http://localhost:3000
  • 后端地址:http://localhost:8000
    此时,前端向后端发起的请求会被浏览器 拦截,因为端口不同(3000 vs 8000)。

2. 跨域场景

跨域是前后端分离架构中的常见问题,例如: - 前端部署在 https://example.com,后端 API 在 https://api.example.com。 - 前端本地开发(localhost:3000)调用后端服务(localhost:8000)。

3. CORS 的作用

CORS 通过 服务器响应头 告诉浏览器:“这个跨域请求是安全的,允许它通过”。
浏览器根据这些响应头决定是否放行请求。

如何配置 CORS?

FastAPI 为例,配置允许跨域请求的步骤如下:

启用 CORS 中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

# 配置 CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"], # 允许的源
allow_credentials=True, # 允许携带凭证
allow_methods=["*"], # 允许所有方法(GET、POST 等)
allow_headers=["*"], # 允许所有头信息
)

CORS 的实际应用场景

1. 前后端分离开发

  • 前端(React/Vue)运行在 localhost:3000,后端(FastAPI)运行在 localhost:8000
  • 配置 allow_origins=["http://localhost:3000"] 允许跨域通信。

2. 第三方 API 调用

  • 前端直接调用第三方服务(如天气 API),需服务器启用 CORS。
  • 示例:Access-Control-Allow-Origin: * 表示允许所有来源。

3. 需要凭证的场景

  • 前端需携带 Cookie 或 Token 访问后端接口:
    1
    2
    3
    4
    5
    6
    7
    app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:3000"],
    allow_credentials=True, # 允许携带凭证
    allow_methods=["*"],
    allow_headers=["*"],
    )

总结

概念 作用 配置示例
同源策略 浏览器安全机制,阻止跨域请求 默认启用
CORS 服务器通过响应头授权跨域请求 Access-Control-Allow-Origin
预检请求 OPTIONS 请求,验证复杂跨域请求的合法性 自动触发
FastAPI 配置 使用 CORSMiddleware 中间件 app.add_middleware(...)

最佳实践: 1. 开发阶段:允许所有来源(allow_origins=["*"]),方便调试。 2. 生产环境:严格限制允许的源、方法、头信息,避免安全风险。 3. 携带凭证:启用 allow_credentials=True 并明确指定允许的源(避免使用 *)。

Nginx

Nginx 是什么?

Nginx(发音为 “engine-x”)是一个高性能的开源 Web 服务器、反向代理服务器、负载均衡器和 HTTP 缓存,广泛用于现代 Web 架构中。它以轻量级、低资源消耗和高并发处理能力著称,常用于优化网站性能、管理流量和提升安全性。

Nginx 的核心功能

1. Web 服务器

  • 静态资源托管:直接提供 HTML、CSS、JS、图片等静态文件服务。
  • 动态请求转发:将动态请求(如 API)转发给后端应用(如 FastAPI、Django、Node.js)。

2. 反向代理

  • 作用:接收客户端请求,转发给后端服务器(如 FastAPI),隐藏真实服务器地址。
  • 优势:提高安全性、支持负载均衡、缓存和 SSL 终端。

3. 负载均衡

  • 作用:将请求分发到多个后端服务器(如多个 FastAPI 实例),避免单点故障。
  • 算法:轮询(Round Robin)、最少连接(Least Connections)、IP 哈希(IP Hash)等。

4. SSL/TLS 终端

  • 作用:处理 HTTPS 加密和解密,减轻后端服务器的压力。
  • 配置:绑定证书和私钥,强制 HTTPS。

5. 缓存

  • 作用:缓存静态资源(如图片、CSS)或动态内容(如 API 响应),减少后端负载。

6. 高可用性和容错

  • 健康检查:自动检测后端服务器状态,故障时切换备用节点。

Nginx 的典型应用场景

1. 反向代理 FastAPI 服务

1
2
3
4
5
6
7
8
9
10
11
12
13
# /etc/nginx/sites-available/fastapi.conf
server {
listen 80;
server_name example.com;

location / {
proxy_pass http://127.0.0.1:8000; # FastAPI 服务地址
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
  • 作用:将 example.com 的请求转发给运行在 127.0.0.1:8000 的 FastAPI 服务。

2. 静态文件托管

1
2
3
location /static/ {
alias /var/www/static/;
}
  • 作用:直接提供 /var/www/static/ 目录下的静态文件(如图片、CSS)。

Nginx 与 FastAPI 的协作流程

1
客户端 -> Nginx(反向代理) -> FastAPI(处理业务逻辑) -> 数据库/其他服务
  1. 静态资源:由 Nginx 直接返回(如 HTML、CSS、JS)。
  2. API 请求:Nginx 转发给 FastAPI,FastAPI 处理后返回 JSON 数据。
  3. HTTPS:Nginx 处理加密和解密,FastAPI 无需关心 SSL。

最佳实践

  1. 开发阶段:直接运行 FastAPI(uvicorn main:app --reload)。
  2. 生产环境:Nginx + FastAPI(Gunicorn/Uvicorn) + 数据库。
  3. 性能优化:启用 Gzip 压缩、HTTP/2、缓存静态资源。

通过 Nginx 的反向代理和负载均衡,可以显著提升 FastAPI 应用的性能、安全性和可扩展性。

反向代理是什么?

反向代理(Reverse Proxy) 是一种服务器角色,它位于客户端与服务器之间,接收客户端的请求后,将请求转发给后端服务器(如 FastAPI、Django、Node.js 等),并将后端服务器的响应返回给客户端。它的核心作用是隐藏后端服务器的真实地址,优化请求处理流程,并增强安全性

反向代理是现代 Web 架构中不可或缺的组件,尤其在前后端分离、微服务、高并发场景下作用显著。通过 Nginx 等工具实现反向代理,可以: - 提升安全性(隐藏后端、过滤攻击)。 - 优化性能(负载均衡、缓存静态资源)。 - 简化运维(集中管理 SSL、日志)。

对于 FastAPI 项目,推荐在生产环境中使用 Nginx 作为反向代理,以充分发挥其高性能和灵活性优势。

二级域名是什么?

二级域名(Second-Level Domain, SLD) 是域名系统(DNS)中的一个层级,通常位于顶级域名(TLD)之下,主域名(一级域名)之上。它是域名结构中的关键部分,用于标识网站或服务的主体。

域名层级结构

域名由多个层级组成,从右向左层级递增,具体如下:

1
2
3
4
5
mail.example.com
| | |
| | └── 顶级域名(TLD):com/net/org
| └────────── 二级域名(SLD):example
└──────────────── 子域名(Subdomain):mail

1. 顶级域名(TLD)

  • 定义:域名的最后一部分,表示域名的类别或国家/地区。
  • 示例.com(商业)、.org(非营利组织)、.net(网络服务)、.cn(中国)、.jp(日本)。

2. 二级域名(SLD)

  • 定义:位于 TLD 之下的域名部分,是域名的主体,通常由用户注册并拥有。
  • 示例:在 example.com 中,example 是二级域名。

3. 子域名(Subdomain)

  • 定义:在二级域名前添加的前缀,用于进一步细分网站或服务。
  • 示例:在 mail.example.com 中,mail 是子域名。

二级域名的常见用途

  1. 品牌标识
    二级域名是品牌的核心标识,如 google.comapple.com
  2. 服务划分
    通过子域名区分不同服务,例如:
    • mail.google.com:邮件服务
    • drive.google.com:云存储服务
    • maps.google.com:地图服务
  3. 多语言或地区支持
    通过二级域名提供本地化内容,例如:
    • fr.wikipedia.org(法语版)
    • zh.wikipedia.org(中文版)

DOM元素

DOM(Document Object Model,文档对象模型) 是浏览器将 HTML 或 XML 文档解析为树状结构的编程接口。DOM 元素 是构成这棵树的节点(如 <div><p><button> 等),它们不仅是页面内容的载体,更是实现动态交互的核心工具。

开发日志

api

获取apihttps://home.openweathermap.org/api_keys

api文档Weather API - OpenWeatherMap

版本1.0

image-20250602191448931

完成基本天气功能的开发

版本2.0

image-20250604095743649

完成ai建议功能

脚踏实地

没有一夜暴富的美梦,天下掉馅饼的事情只有可能是诱惑,在得到某些好处前先想一想你配不配。

杜绝心浮气躁,用双手制造财富。

前言

代码仓库zxj-2023/learn_fastapi

FastAPI 是一个用于构建 API 的现代、快速(高性能)的 web 框架,使用 Python 并基于标准的 Python 类型提示。

关键特性:

  • 快速:可与 NodeJSGo 并肩的极高性能(归功于 Starlette 和 Pydantic)。最快的 Python web 框架之一
  • 高效编码:提高功能开发速度约 200% 至 300%。*
  • 更少 bug:减少约 40% 的人为(开发者)导致错误。*
  • 智能:极佳的编辑器支持。处处皆可自动补全,减少调试时间。
  • 简单:设计的易于使用和学习,阅读文档的时间更短。
  • 简短:使代码重复最小化。通过不同的参数声明实现丰富功能。bug 更少。
  • 健壮:生产可用级别的代码。还有自动生成的交互式文档。
  • 标准化:基于(并完全兼容)API 的相关开放标准:OpenAPI (以前被称为 Swagger) 和 JSON Schema

两个核心组件:Starlette 和 Pydantic

Starlette 负责web部分

Starlette 是 FastAPI 的底层 ASGI(异步服务器网关接口)框架,为 FastAPI 提供了异步编程能力和高性能的网络通信支持。

ASGI(Asynchronous Server Gateway Interface )是一种用于连接 Python Web 服务器和应用程序框架的异步接口标准 ,旨在支持现代 Web 协议(如 WebSocket、HTTP/2)和异步编程模型

Pydantic负责

Pydantic 负责 FastAPI 的数据验证、序列化和自动文档生成

http协议

一、简介

HTTP协议 是Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于万维网(WWW: World Wide Web)服务器与本地浏览器之间传输超文本的传送协议。HTTP是一个属于应用层的面向对象的协议,由于其简捷、快速的方式,适用于分布式超媒体信息系统。它于1990年提出,经过几年的使用与发展,得到不断地完善和扩展。HTTP协议工作于客户端-服务端架构为上。浏览器作为HTTP客户端通过URL向HTTP服务端即WEB服务器发送所有请求。Web服务器根据接收到的请求后,向客户端发送响应信息。

1

二、http协议特性

(1)基于 TCP/IP 协议

http 协议是基于 TCP/IP 协议之上的应用层协议。

(2)基于请求 - 响应模式

HTTP 协议规定,请求从客户端发出,最后服务器端响应应该请求并返回。换句话说,肯定是先从客户端开始建立通信的,服务器端在没有接收到请求之前不会发送响应

(3)无状态保存

HTTP 是一种不保存状态,即无状态(stateless)协议。HTTP 协议自身不对请求和响应之间的通信状态进行保存。也就是说在 HTTP 这个级别,协议对于发送过的请求或响应都不做持久化处理。

使用 HTTP 协议,每当有新的请求发送时,就会有对应的新响应产生。协议本身并不保留之前一切的请求或响应报文的信息。这是为了更快地处理大量事务,确保协议的可伸缩性,而特意把 HTTP 协议设计成如此简单的。

(4)短连接

HTTP 1.0 默认使用的是短连接。浏览器和服务器每进行一次 HTTP 操作,就建立一次连接,任务结束就中断连接。

HTTP 1.1 起,默认使用长连接。要使用长连接,客户端和服务器的 HTTP 首部的 Connection 都要设置为 keep - alive,才能支持长连接。

HTTP 长连接,指的是复用 TCP 连接。多个 HTTP 请求可以复用同一个 TCP 连接,这就节省了 TCP 连接建立和断开的消耗。

三、http请求协议与响应协议

2

Socket(套接字)是计算机网络中用于实现进程间双向通信的端点抽象,它为应用层进程通过网络协议交换数据提供了统一的接口。具体来说,Socket 是应用层与 TCP/IP 协议族通信的中间软件抽象层,本质上是一组封装了复杂网络协议的接口,简化了开发者对底层通信细节的操作。

从功能上看,Socket 可以看作是网络通信的“电话插座”:两个设备(如客户端与服务器)通过 Socket 建立连接后,即可像电话通话一样进行数据交换,而端口号则类似于插座上的插孔,用于标识具体的通信进程,且不能被其他进程占用。此外,Socket 包含网络通信必需的五种核心信息,例如使用的协议(TCP/UDP)、本地与远程地址、端口等,构成了网络通信的基本操作单元。

总结而言,Socket 既是通信端点的逻辑概念,也是实现网络应用层交互的关键工具,其设计目标是屏蔽底层协议的复杂性,提供统一的编程接口。

3

GET :请求参数通过 URL 的查询字符串(Query String)传递,数据暴露在地址栏中,例如:https://example.com ?name=value

POST :请求参数存储在请求体(Body)中传输,相对更安全,且支持传输非字符串数据(如文件、二进制等)

一个完整的URL包括:协议、ip、端口、路径、参数

例如:https://www.baidu.com/s?wd=yuan 其中https是协议,www.baidu.com 是IP,端口默认80,/s是路径,参数是wd=yuan

请求方式:get与post请求

  • GET提交的数据会放在URL之后,以?分割URL和传输数据,参数之间以&相连,如EditBook?name=test1&id=123456。POST方法是把提交的数据放在HTTP包的请求体中。
  • GET提交的数据大小有限制(因为浏览器对URL的长度有限制),而POST方法提交的数据没有限制

响应状态码:状态码的职责是当客户端向服务器端发送请求时,返回的请求结果。借助状态码,用户可以知道服务器端是正常处理了请求,还是出现了问题。状态码如200 OK,以3位数字和原因组成。

测试http协议格式:请求与响应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#web应用程序:遵循http协议
import socket
sock=socket.socket()
sock.bind(("127.0.0.1",8080))
sock.listen(5)
while True:
'''
conn 表示新建立的套接字对象,用于在服务器和客户端之间进行数据传输。
addr 是一个元组,它包含了连接进来的客户端的 IP 地址和端口号。
'''
conn, addr = sock.accept()#阻塞等待客户端连接
data=conn.recv(1024)#请求报文
# data 是一个字节串,包含了客户端发送的请求信息。
print("客户端发送的请求信息:\n",data)
conn.send(b"HTTP/1.1 200 ok\r\nserver:zxj\r\n\r\nhello world")#响应首行+响应头+响应体
conn.close()

测试post请求:urlencoded格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import socket

# 创建 socket 连接
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(("127.0.0.1", 8080))

# 构造 POST 请求报文
path = "/" # 目标路径
headers = {
"Host": "127.0.0.1",
"Content-Type": "application/x-www-form-urlencoded", # 数据格式
"Content-Length": len("username=admin&password=123456") # 数据长度
}
body = "username=admin&password=123456" # 请求体(表单数据)

# 拼接请求报文
request = f"POST {path} HTTP/1.1\r\n"
for k, v in headers.items():
request += f"{k}: {v}\r\n"
request += "\r\n" # 空行分隔头部与主体
request += body

# 发送请求
client.send(request.encode('utf-8'))

# 接收响应
response = client.recv(4096)
print(response.decode('utf-8'))
client.close()

测试post请求:json格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import requests
import json

# 定义请求地址
url = "http://127.0.0.1:8080"
# 定义 JSON 数据(字典格式)
data = {
"username": "admin",
"password": "123456"
}

# 发送 POST 请求
response = requests.post(
url,
json=data # 使用 json 参数自动序列化字典并设置 Content-Type: application/json
)

# 输出响应结果
print("状态码:", response.status_code)
print("响应内容:", response.text) # 使用 text 获取原始响应文本
'''
客户端发送的请求信息:
b'POST / HTTP/1.1\r\nHost: 127.0.0.1:8080\r\nUser-Agent: python-requests/2.32.2\r\nAccept-Encoding: gzip, deflate\r\nAccept: */*\r\nConnection: keep-alive\r\nContent-Length: 43\r\nContent-Type: application/json\r\n\r\n{"username": "admin", "password": "123456"}'
'''

通过 json=data 参数,requests 会自动将字典转换为 JSON 字符串,并设置请求头 Content-Type: application/json,无需手动调用 json.dumps() 或配置 headers

SSL 验证是指通过 SSL 证书验证网站身份并确保通信安全的过程。其核心目标是确认服务器的真实性、防止身份伪造,并建立加密连接以保护数据传输的安全性

HTTPS(HyperText Transfer Protocol Secure)是以安全为目标的 HTTP 通道,通过在 HTTP 基础上加入加密和身份认证机制,确保数据传输的隐私性、完整性和服务器身份的真实性

https=http+ssl

通过 Content-Type,服务器可识别请求体(Body)的格式(如 JSON、表单数据),客户端可解析响应数据的类型(如 HTML、图片)

例如:conn.send(b”HTTP/1.1 200 ok:zxj*content-type:text/html**hello world<>“)

再例如:‘HTTP/1.1 200 ok:zxj*content-type:application/json**’

api接口

在开发web应用中,有两种应用模式:

1.前后端不分离:客户端看到的内容和所有页面效果都是有服务端提供出来的

4

2.前后端分离:把前端的页面效果(html,css,js分离到另一个服务端,python服务端只需要返回数据即可)

前端形成一个独立的网站,服务端构成一个独立的网站

5

应用程序编程接口(Application Programming Interface,API接口),就是应用程序对外提供了一个操作数据的入口,这个入口可以是一个函数或类方法,也可以是一个url地址或者一个网络地址。当客户端调用这个入口,应用程序则会执行对应代码操作,给客户端完成相对应的功能。

当然,api接口在工作中是比较常见的开发内容,有时候,我们会调用其他人编写的api接口,有时候,我们也需要提供api接口给其他人操作。由此就会带来一个问题,api接口往往都是一个函数、类方法、或者url或其他网络地址,不断是哪一种,当api接口编写过程中,我们都要考虑一个问题就是这个接口应该怎么编写?接口怎么写的更加容易维护和清晰,这就需要大家在调用或者编写api接口的时候要有一个明确的编写规范!!!

为了在团队内部形成共识,防止个人习惯差异引起的混乱,我们都需要找到一种大家都觉得很好的接口实现规范,而且这种规范能够让后端写的接口,用途一目了然,减少客户端和服务端双方之间的合作成本。

目前市面上大部分公司开发人员使用的接口实现规范主要有:restful、RPC。

REST全称是Representational State Transfer,中文意思是表述(编者注:通常译为表征)性状态转移。它首次出现在2000年Roy Fielding的博士论文中。

RESTful是一种专门为Web开发而定义API接口的设计风格,尤其适用于前后端分离的应用模式中。

关键:面向资源开发

这种风格的理念认为后端开发任务就是提供数据的,对外提供的是数据资源的访问接口,所以在定义接口时,客户端访问的URL路径就表示这种要操作的数据资源。

对于数据资源分别使用POST、DELETE、GET、UPDATE等请求动作来表达对数据的增删查改

请求方法 请求地址 后端操作
POST /student/ 增加学生
GET /student/ 获取所有学生
GET /student/1 获取id为1的学生
PUT /student/1 修改id为1的学生
DELETE /student/1 删除id为1的学生

restful规范是一种通用的规范,不限制语言和开发框架的使用。事实上,我们可以使用任何一门语言,任何一个框架都可以实现符合restful规范的API接口。

fastapi快速开始

简单案例

安装:pip install fastapi

还需要一个ASGI服务器,生产环境使用Uvicorn:pip install uvicorn

ASGI(Asynchronous Server Gateway Interface )是一种异步服务器网关接口 ,为 Python Web 应用提供了标准接口,使其能够处理现代网络协议(如 WebSocket、HTTP/2 等)的异步请求。与传统的 WSGI 不同,ASGI 支持异步编程模型,允许单个请求处理多个事件(如长连接、双向通信),从而提升高并发场景下的性能

Uvicorn 是一个基于 ASGI 的高性能异步 Web 服务器,专为 Python 异步框架设计。

web应用程序=web框架+自己写的业务逻辑代码

1
2
3
4
5
6
7
8
9
10
11
12
from fastapi import FastAPI#fastapi类

app= FastAPI()#创建一个fastapi实例

@app.get("/")
async def home():

return {"user_id":1001}

@app.get("/shop")
async def shop():
return {"shop_id":1002}

启动:uvicorn "04 fastapi_begin:app" --reload

也可以:

1
2
3
import uvicorn
if __name__ == "__main__":
uvicorn.run(app="04 fastapi_begin:app",port=8080,reload=True)

接口文档

6

修饰器(Decorator)是 Python 中一种动态修改函数或类行为的高级功能,本质上是一个函数或类,它接受目标函数或类作为参数,并返回包装后的新函数或类对象,从而在不修改原始代码 的前提下为对象添加额外功能

路径操作

路径操作修饰器

1
2
3
4
5
6
7
8
@app.get()
@app.post()
@app.put()
@app.patch()
@app.delete()
@app.options()
@app.head()
@app.trace()

路径操作修饰器参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
'''
tags为接口添加标签,用于在自动生成的文档
summary为接口添加描述
description为接口添加详细描述
response_description为接口返回值描述
deprecated为过时的接口
'''
@app.post("/post",
tags=["这是post方法"],
summary="这是post方法的描述",
description="这是post方法的详细描述",
response_description="这是post方法的返回值描述",
deprecated=True, # 过时的接口
)
def test():

include_router

文件路径如下

7

main.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from fastapi import FastAPI
import uvicorn

from apps.app01.shop import shop
from apps.app02.user import user

app= FastAPI()

app.include_router(shop,
prefix="/shop", # 路由前缀
tags=["购物中心接口"], # 标签
responses={200: {"description": "成功"}} # 响应描述
)
app.include_router(user,
prefix="/user", # 路由前缀
tags=["用户接口"], # 标签
responses={200: {"description": "成功"}} # 响应描述
)

if __name__ == "__main__":
uvicorn.run("main:app", port=8080, reload=True)

shop.py

1
2
3
4
5
6
7
8
9
10
11
from fastapi import APIRouter

shop=APIRouter()

@shop.get("/food")
def shop_food():
return {"food":"shop food"}

@shop.get("/drink")
def shop_drink():
return {"drink":"shop drink"}

user.py

1
2
3
4
5
6
7
8
9
10
11
from fastapi import APIRouter

user=APIRouter()

@user.post("/login")
def user_login():
return {"user":"user login"}

@user.post("/register")
def user_register():
return {"user":"user register"}

include_router 是 FastAPI 框架中用于整合路由的核心方法,其作用是将通过 APIRouter 定义的路由模块添加到主应用程序实例中,使这些路由在应用中生效。

8

请求与响应

4.1 路径参数

(1)基本用法

以使用与 Python 格式化字符串相同的语法来声明路径”参数”或”变量”:

1
2
3
4
@app.get("/user/{user_id}")
def get_user(user_id):
print(user_id, type(user_id))
return {"user_id": user_id}

路径参数 user_id 的值将作为参数 user_id 传递给你的函数。

(2)有类型的路径参数

你可以使用标准的 Python 类型标注为函数中的路径参数声明类型。

1
2
3
4
@app.get("/user/{user_id}")
def get_user(user_id: int):
print(user_id, type(user_id))
return {"user_id": user_id}

在这个例子中,user_id 被声明为 int 类型。

这将为你的函数提供编辑器支持,包括错误检查、代码补全等等。

(3)注意顺序

在创建路径操作时,你会发现有些情况下路径是固定的。

比如 /users/me,我们假设它用来获取关于当前用户的数据。

然后,你还可以使用路径 /user/{username} 来通过用户名获取关于特定用户的数据。

由于路径操作是按顺序依次运行的,你需要确保路径 /user/me 声明在路径 /user/{username} 之前。

如下

image-20250516193934740
image-20250516201128482

路由(Routing)是指在网络中选择数据传输路径的过程,其核心目标是将数据从源点高效、可靠地传输到目的地

cURL 是一个开源的命令行工具和跨平台的库(libcurl),用于基于 URL 语法在网络协议下进行数据传输。它支持多种协议(如 HTTP、HTTPS、FTP、SMTP 等),能够实现文件上传、下载以及与 Web 服务器的交互,常被开发者用于 API 测试、数据传输等场景

4.2 查询参数(请求参数)

路径函数中声明不属于路径参数的其他函数参数时,它们将被自动解释为查询字符串参数,就是 url?之后用 & 分割的 key-value 键值对

1
2
3
4
5
6
7
8
@app02.get("/jobs")
async def get_jobs(kind1: str, kind2: str, kind3: str):
#基于查询参数的值来执行不同的操作
return {
"kind1": kind1,
"kind2": kind2,
"kind3": kind3
}
image-20250516201707589

增加路径参数:kind1为路径参数

增加默认参数值

1
2
3
4
5
6
7
8
@app02.get("/jobs/{kind1}")
async def get_jobs(kind1: str, kind2: str="None", kind3: str="None"):#增加默认值,可选填
#基于查询参数的值来执行不同的操作
return {
"kind1": kind1,
"kind2": kind2,
"kind3": kind3
}

Request URL:

http://127.0.0.1:8080/app02/jobs/11?kind2=22&kind3=33

自python3.5开始,PEP484为python引入了类型注解(type hints),typing的主要作用有:

1.类型检查,防止运行时出现参数、返回值类型不符。

2.作为开发文档附加说明,方便使用者调用时传入和返回参数类型。

3.模块加入不会影响程序的运行不会报正式的错误,pycharm支持typing检查错误时会出现黄色警告。

type hints主要是要指示函数的输入和输出的数据类型,数据类型在typing包中,基本类型有str list dict等等,

Type Hints 是 Python 3.5 引入的功能,通过类型注解增强代码的可读性和可维护性。它允许开发者为变量、函数参数、返回值等指定预期的数据类型,从而帮助静态类型检查工具(如 mypy)捕获潜在错误,并提升 IDE 的智能提示能力。例如:

1
2
def greet(name: str) -> str:
return f"Hello, {name}"

此处 name: str 表示参数需为字符串类型,-> str 表示返回值类型为字符串 。

Union是当有多种可能的数据类型时使用,比如函数有可能根据不同情况有时返回str或返回list,那么就可以写成Union[list, str]

从 Python 3.10 起,Union[X, Y] 可简写为 X | Y。例如 int | str 等价于 Union[int, str]

再例如:kind2:str|None=None

Optional是Union的一个简化,当数据类型中有可能是None时,比如有可能是str也有可能是None,则Optional[str],相当于Union[str, None]

4.3 请求体数据

当你需要将数据从客户端(例如浏览器)发送给 API 时,你将其作为「请求体」发送。请求体是客户端发送给 API 的数据。响应体是 API 发送给客户端的数据。

FastAPI 基于 PydanticPydantic 主要用来做类型强制检查(校验数据)。不符合类型要求就会抛出异常。

对于 API 服务,支持类型检查非常有用,会让服务更加健壮,也会加快开发速度,因为开发者再也不用自己写一行一行的做类型检查。

安装上手 pip install pydantic

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
from fastapi import APIRouter
from pydantic import BaseModel,Field,field_validator
from datetime import date
from typing import Union,Optional

app03 = APIRouter()

class Address(BaseModel):
province: str
city: str

class User(BaseModel):
#正则表达式
#username:str=Field(pattern="^[a-zA-Z0-9]{3,10}$",title="用户名",description="用户名长度在3-10之间,且只能包含字母和数字")
#name: str|None = None
name:str
age: int=Field(default=0,gt=0,lt=100)
birth:Union[date,None] = None
friends:list[int]=[]
description:Optional[str]=None
#嵌套
addr:Address|None = None

'''
field_validator 的第一个参数必须是 cls,因为它是类方法 (classmethod),用于在验证字段时访问模型类的上下文。
'''
@field_validator("name")
def name_must_alpha(cls,value):
'''
value.isalpha() 会检查字符串是否只由字母组成,如果是则返回 True,否则返回 False。
如果返回 False,assert 触发,会抛出 AssertionError,并显示错误信息 "name must be alpha"。
'''
assert value.isalpha(), "name must be alpha"
return value

#嵌套
class Data(BaseModel):
data:list[User]

@app03.post("/user")
async def user(user:User):
print(user,type(user))
return user

@app03.post("/data")
async def data(data:Data):
print(data,type(data))
return data

BaseModel专门用于数据验证、数据转换和序列化。在定义数据结构时继承自 BaseModel,可以:

  • 自动校验数据类型:根据类中字段的类型注解,自动校验输入数据是否符合预期类型。
  • 数据转换:可以自动将输入数据(例如 JSON 字符串)转换成相应的 Python 数据类型。
  • 序列化输出:支持将模型实例转换成 JSON、字典等格式,便于响应输出。

在 Pydantic 中,Field 用于为模型字段提供额外的信息,比如设置默认值、描述信息、约束条件(例如长度、范围等)或别名。这可以帮助自动生成 OpenAPI 文档、增强验证或对字段进行更细粒度的控制。

field_validator 是 Pydantic v2 中用于替代旧版 @validator 的新装饰器,专门用于为模型字段添加自定义验证逻辑。它通过更清晰的命名和更灵活的模式(如 mode="before"mode="after")提升代码可读性和验证逻辑的控制能力

field_validatormodel_validator区别

field_validator专门针对单个字段 进行验证,适用于需要校验特定字段的规则(如长度、格式、类型约束)。例如验证用户名长度

model_validator作用于整个模型实例 ,适用于需要跨字段验证或全局逻辑的场景。例如检查两次密码是否一致

4.4 form表单数据

OAuth2 规范的一种使用方式(密码流)中,需要将用户名、密码作为表单字段发送,而不是 JSON。

FastAPI 可以使用 Form 组件来接收表单数据,需要先使用 pip install python-multipart 命令进行安装。

1
2
3
4
5
6
7
@app04.post("/register")
async def data(username:str=Form(),password:str=Form()):
print(f"username: {username}, password: {password}")
return {
"username": username,
"password": password
}

发送post请求:form表单数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 目标 URL
url = "http://127.0.0.1:8080/app04/register"

# 表单数据(键值对)
data = {
"username": "test_user",
"password": "secure_password_123"
}

# 发送 POST 请求
response = requests.post(
url,
data=data
)

通过 requests.post() 的 data 参数传递表单数据,该参数接受字典或字符串格式的数据。requests 会自动将其编码为 application/x-www-form-urlencoded 格式

4.5 文件上传

导入必要库

1
2
3
from fastapi import File,UploadFile
import os
app05 = APIRouter()

通过字节上传

1
2
3
4
5
6
7
@app05.post("/file")
async def file(file: bytes = File(...)):
#适合小文件上传
return {
"filename": "file",
"size": len(file)
}

多文件上传

1
2
3
4
5
6
7
8
9
@app05.post("/files")
async def files(files: list[bytes] = File(...)):
#多文件上传
for file in files:
print(len(file))
return {
"filename": "files",
"size": len(files)
}

UploadFile上传,绝对路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@app05.post("/uploadfile")
async def upload_file(file: UploadFile= File(...)):
#适合大文件上传
# 获取当前文件的绝对路径
base_dir = os.path.dirname(os.path.abspath(__file__))
print(base_dir)
img_dir = os.path.join(base_dir, "../imgs")
print(img_dir)
# 创建目录
path = os.path.join(img_dir, file.filename)
#path=os.path.join("../imgs",file.filename)
print(path)
with open(path, "wb") as f:
print("文件名:", file.filename)
for chunk in file.file:
f.write(chunk)

return {
"filename": file.filename
}

UploadFile是 FastAPI 提供的一个类,用于处理文件上传。与直接将文件内容读取为字节流(例如 bytes相比,UploadFile有以下优点:

  • 内存优化:它采用了文件对象的方式处理上传文件,不必将整个文件内容一次性加载到内存中,适合处理大文件。
  • 异步支持:支持异步操作,可以用异步方式读取文件内容,提高性能。
  • 文件元数据:提供文件名、内容类型等元数据信息,通过属性 filenamecontent_type 获取。
  • 文件接口:通过 file 属性获取一个类文件对象,可以像操作普通文件一样读取或保存上传的文件。

4.6 Request对象

有些情况下我们希望能直接访问 Request 对象。例如我们在路径操作函数中想获取客户端的 IP 地址,需要在函数中声明 Request 类型的参数,FastAPI 就会自动传递 Request 对象给这个参数,我们就可以获取到 Request 对象及其属性信息,例如 header、url、cookie、session 等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@app06.post("/items")
async def items(request: Request):
print("url:", request.url)
print("客户端ip:", request.client.host)
print("请求头:", request.headers)
print("客户端宿主",request.headers.get("user-agent"))
print("cookie:", request.cookies)
return {
"url": request.url,
"client_ip": request.client.host,
"headers": request.headers,
"user_agent": request.headers.get("user-agent"),
"cookies": request.cookies
}

4.7请求静态文件

在 Web 开发中,需要请求很多静态资源文件(不是由服务器生成的文件),如 css/js 和图片文件等。

main.py

1
2
from fastapi.staticfiles import StaticFiles
app.mount("/static", StaticFiles(directory="statics"))#静态文件目录
image-20250517143857442
image-20250517143914772

静态网站

完全由静态文件(HTML、CSS、JavaScript)组成,内容固定不变,所有页面在开发时已预生成,无需动态计算或数据库支持

动态网站

内容根据用户请求实时生成,通常依赖数据库和服务器端编程(如PHP、Python、Node.js),能提供个性化和交互功能

StaticFiles是 FastAPI(实际来自 Starlette)提供的一个类,用于挂载和服务静态文件目录。 它的作用是让你可以通过 HTTP 路径直接访问服务器上的静态资源(如图片、CSS、JS 文件等)。

mount()方法用于将一个完整的应用或静态文件目录挂载到主 FastAPI 应用的某个路径下。这样,访问指定路径时,请求会被转发到挂载的应用或目录。

4.8 响应模型相关参数

response_model

response_model是 FastAPI 路由装饰器(如 @app.post@app.get 等)中的一个参数,用于指定接口响应的数据模型。它的作用是:

  • 自动校验和序列化:FastAPI 会根据你指定的 Pydantic 模型自动校验、过滤和格式化返回的数据。
  • 自动生成文档:接口文档会自动显示响应的数据结构。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class UserIn(BaseModel):
username: str
password: str
email: EmailStr
full_name: str|None = None

class UserOut(BaseModel):
username: str
email: EmailStr
full_name: str|None = None


@app07.post("/user02",response_model=UserOut)
async def create_user(user: UserIn):
# 这里可以进行一些处理,比如将用户信息存储到数据库中
return user

案例:

  • 注册功能
  • 输入账号、密码、昵称、邮箱,注册成功后返回个人信息
response_model_exclude_unset=True

通过上面的例子,我们学到了如何用 response_model 控制响应体结构,但是,如果它们实际上没有存储,则可能要从结果中忽略它们。例如,如果 model 在 NoSQL 数据库中具有很多可选属性,但是不想发送很长的 JSON 响应,其中包含默认值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Item(BaseModel):
name:str
description: str|None = None
price: float
tax:float=10.5
tags: list[str]|None = None

#模拟数据库
items={
"item01":Item(name="item01",price=10.5),
"item02":Item(name="item02",description="item02",price=20.5,tax=20.5,tags=["tag1","tag2"]),
"item03":Item(name="item03",description="item03",price=30.5,tax=30.5,tags=["tag1","tag2"]),
}

@app07.get("/items/{item_id}",response_model=Item,response_model_exclude_unset=True)
async def read_item(item_id: str):
return items[item_id]

设置后返回为:

1
2
3
4
5
item01
{
"name": "item01",
"price": 10.5
}

当你设置 response_model_exclude_unset=True 时,返回的响应数据只包含被显式设置过的字段,没有被赋值的(即使用默认值且未传递的)字段不会出现在响应中。

其他参数

response_model_exclude_defaults 作用:排除所有值为默认值的字段。

response_model_exclude_none 作用:排除所有值为 None 的字段。

response_model_include 作用:只返回指定字段

response_model_exclude 作用:排除指定字段,不在响应中返回。

jinja2模板

要了解 jinja2,那么需要先理解模板的概念。模板在 Python 的 web 开发中广泛使用,它能够有效的将业务逻辑和页面逻辑分开,使代码可读性增强、并且更加容易理解和维护。

模板简单来说就是一个其中包涵占位变量表示动态的部分的文件,模板文件在经过动态赋值后,返回给用户。

jinja2 是 Flask 作者开发的一个模板系统,起初是仿 django 模板的一个模板引擎,为 Flask 提供模板支持,由于其灵活,快速和安全等优点被广泛使用。

在 jinja2 中,存在三种语法:

  1. 变量取值 {{ }}
  2. 控制结构 {% %}

应用于前后端不分离,模板html+数据库,返回动态网站

5.1 变量

main.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from fastapi.templating import Jinja2Templates

templates=Jinja2Templates(directory="templates")

@app.get("/index")
def index(request: Request):
#数据库
name="World"
books=["Python", "Java", "C++"]
user={"name":"Tom", "age":18}

return templates.TemplateResponse(
"index.html",#模板文件
{
"request": request,# FastAPI需要一个request对象
"name": name,
"books": books,
"user": user
}#context上下文对象
)

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>

<body>
<h1>Hello, {{ name }}!</h1>
<p>Your favorite books are:</p>
<ul>
{% for book in books %}
<li>{{ book }}</li>
{% endfor %}
</ul>
<p>姓名:{{ user.name }}</p>
<p>年龄:{{ user.age }}</p>
</body>

</html>
image-20250517200643492

5.2 过滤器

变量可以通过“过滤器”进行修改,过滤器可以理解为是 jinja2 里面的内置函数和字符串处理函数。常用的过滤器有:

过滤器名称 说明
capitalize 把值的首字母转换成大写,其他字母转换为小写
lower 把值转换成小写形式
title 把值中每个单词的首字母都转换成大写
trim 把值的首尾空格去掉
striptags 渲染之前把值中所有的 HTML 标签都删掉
join 拼接多个值为字符串
round 默认对数字进行四舍五入,也可以用参数进行控制
safe 渲染时值不转义

那么如何使用这些过滤器呢?只需要在变量后面使用管道 (|) 分割,多个过滤器可以链式调用,前一个过滤器的输出会作为后一个过滤器的输入。

例如:<h1>Hello, {{ name|upper }}!</h1> <li>{{ book|title }}</li>

5.3 控制结构

jinja2中的if语句类似与Python的if语句,它也具有单分支,多分支等多种结构,不同的是,条件语句不需要使用冒号结尾,而结束控制语句,需要使用endif关键字

1
2
3
4
5
6
7
8
9
10
11
12
<p>影视区</p>
{% if age >= 18 %}
<ul>
<li>成人影片</li>
<li>成人游戏</li>
</ul>
{% else %}
<ul>
<li>儿童影片</li>
<li>儿童游戏</li>
</ul>
{% endif %}

jinja2中的for循环用于迭代Python的数据类型,包括列表、元组和字典。在jinja2中不存在while循环。

1
2
3
{% for book in books %}
<li>{{ book|title }}</li>
{% endfor %}

ORM操作

在大型的 Web 开发中,我们肯定会用到数据库操作,那么 FastAPI 也支持数据库的开发,你可以用 PostgreSQL、MySQL、SQLite、Oracle 等。本文用 SQLite 为例。我们看下在 FastAPI 是如何操作设计数据库的。

FastAPI 是一个很优秀的框架,但是缺少一个合适的 ORM,官方代码里面使用的是 SQLAlchemy,Tortoise ORM 是受 Django 启发的易于使用的异步 ORM(对象关系映射器)。

Tortoise ORM 目前支持以下数据库:

  • PostgreSQL >= 9.4(使用 asyncpg)
  • SQLite(使用 aiosqlite)
  • MySQL/MariaDB(使用 aiomysql 或使用 asyncmy)

安装:pip install tortoise-orm

6.1 创建模型

1. 一对一关系(One-to-One)

  • 定义 :一张表中的一条记录仅关联另一张表中的一条记录。
  • 示例 :用户表(User)与身份证信息表(IDCard),一个用户仅对应一张身份证信息。

2. 一对多关系(One-to-Many)

  • 定义 :一张表中的一条记录关联另一张表中的多条记录。
  • 示例 :班级表(Class)与学生表(Student),一个班级包含多个学生,但每个学生只能属于一个班级。

3. 多对多关系(Many-to-Many)

  • 定义 :两张表中的记录可以互相关联多条记录,通常通过中间表实现。
  • 示例 :学生表(Student)与课程表(Course),一个学生可选修多门课程,一门课程也可被多个学生选修。

4. 自引用关系(Self-Referencing)

  • 定义 :表内的记录通过字段关联自身,形成层级或树状结构。
  • 示例 :员工表(Employee),每个员工可能有直属上级(另一个员工)。

5. 继承关系(Inheritance)

  • 定义 :基于面向对象的继承概念,子表继承父表的字段和约束。
  • 示例 :用户表(User)作为基表,管理员表(Admin)和普通用户表(RegularUser)继承其字段(如用户名、密码)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
from tortoise.models import Model
from tortoise import fields
'''
Model 是所有数据模型的基类,通过继承 Model 可定义数据库表的结构。每个 Model 子类对应一张数据库表,其类属性定义了表的字段(列)及其约束。
fields 提供了多种字段类型,用于定义数据库表的列及其约束。
'''
class Student(Model):
id = fields.IntField(pk=True)#该字段会被指定为模型的主键
name = fields.CharField(max_length=50,description="姓名")
pwd = fields.CharField(max_length=50,description="密码")
sno = fields.IntField(description="学号")
#一对多关系
Class_id = fields.ForeignKeyField(
"models.Class",#关联的模型类
related_name="students",#反向查询时的名称
on_delete=fields.CASCADE,#级联删除
description="班级"
)
#多对多关系
Course_id = fields.ManyToManyField(
"models.Course",#关联的模型类
related_name="students",#反向查询时的名称
description="课程"
)

class Course(Model):
id = fields.IntField(pk=True)
name = fields.CharField(max_length=50,description="课程名称")
teacher = fields.CharField(max_length=50,description="授课老师")
#一对多关系
teacher_id = fields.ForeignKeyField(
"models.Teacher",#关联的模型类
related_name="courses",#反向查询时的名称
on_delete=fields.CASCADE,#级联删除
description="老师"
)

class Class(Model):
id = fields.IntField(pk=True)
name= fields.CharField(max_length=50,description="班级名称")

class Teacher(Model):
id =fields.IntField(pk=True)
name = fields.CharField(max_length=50,description="老师姓名")
pwd = fields.CharField(max_length=50,description="密码")
sno = fields.IntField(description="工号")

ORM(Object Relational Mapping,对象关系映射)是一种程序设计技术,主要用于实现面向对象编程语言关系型数据库 之间的数据转换。其核心思想是通过对象模型与数据库表结构的映射,将数据库操作转化为面向对象的操作,从而简化开发流程并提升代码的可维护性

Tortoise ORM 是一款专为 Python 异步环境设计的轻量级对象关系映射(ORM)框架,其设计灵感来源于 Django ORM,但专注于异步编程场景,适用于 FastAPI、Sanic 等基于 asyncio 的现代 Web 框架。

关系型数据库与非关系型数据库

关系型数据库 以表格形式存储数据,数据按行和列组织,列代表属性(字段),行代表记录。例如,用户表可能包含 idnameemail 等列,每行对应一个用户记录。这种结构化设计支持严格的模式约束(Schema)17。

典型代表 :MySQL、Oracle、PostgreSQL。

非关系型数据库(NoSQL) 采用非结构化或半结构化存储,常见的类型包括:

  • 文档型 (如 MongoDB):以 JSON 或 BSON 格式存储数据。

  • 键值型 (如 Redis):通过键直接访问值。

  • 列存储 (如 Cassandra):按列而非行组织数据。

  • 图数据库

    (如 Neo4j):以节点和边表示数据关系

6.2 aerich迁移工具

docker 安装 mysql

拉取 MySQL 镜像:docker pull mysql

运行 MySQL 容器:

1
docker run --name fastapi -e MYSQL_ROOT_PASSWORD=root -p 3306:3306 -d mysql

-p表示端口映射 –restart=always表示容器退出时总是重启 –name表示容器命名 –privileged=true表示赋予容器权限修改宿主文件权利 -v /home/mysql/log:/var/log/mysql表示容器日志挂载到宿主机 -v /home/mysql/data:/var/lib/mysql表示容器存储文件挂载到宿主机 -v /home/mysql/conf/my.cnf:/etc/mysql/my.cnf表示容器配置文件挂载到宿主机 -e MYSQL_ROOT_PASSWORD=a12bCd3_W45pUq6表示设置mysql的root用户密码,建议用强密码 -d表示后台运行

启动这个 MySQL 容器:docker start fastapi

进入 MySQL 容器:docker exec -it fastapi bash

这条命令的作用是:

  • docker exec:在已运行的 Docker 容器中执行命令。
  • -it-i 表示交互式操作,-t 分配一个伪终端(让你像在终端一样操作)。
  • fastapi:这是你要进入的容器名称(你的 MySQL 容器名)。
  • bash:在容器内启动 bash shell。

这条命令会让你进入名为 fastapi 的容器,并获得一个 bash 命令行界面,就像登录到一台 Linux 服务器一样,可以在里面执行各种命令(比如登录 MySQL、查看日志等)。

登录 MySQL:mysql -u root -p

从主机直接连接:mysql -h 127.0.0.1 -P 3306 -u root -p

配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
TORTOISE_ORM = {
"connections": {
"default": {
"engine":'tortoise.backends.mysql',#选择数据库引擎,mysql
"credentials": {
"host": "localhost",#数据库地址
"port": 3306,#数据库端口
"user": "root",#数据库用户名
"password": "root",#数据库密码
"database": "fastapi_db",#数据库名称
'charset': "utf8mb4",#数据库编码
'echo': True,#是否打印sql语句
'minsize': 1,#连接池最小连接数
'maxsize': 5#连接池最大连接数
}
}
},
"apps": {
"models": {
#db.models是我们自己定义的模型类,models在db文件夹下
"models": ["db.models","aerich.models"],
"default_connection": "default"
}
},
'use_tz': False,#是否使用时区
'timezone': 'Asia/Shanghai',#时区
"generate_schemas": True,
"add_exception_handlers": True
}

1.初始化配置,只需要使用一次

aerich 是一种 ORM 迁移工具,需要结合 tortoise 异步 orm 框架使用。安装 aerich

pip install aerich

aerich init -t settings.TORTOISE_ORM # TORTOISE_ORM 配置的位置

初始化完会在当前目录生成一个文件:pyproject.toml 和一个文件夹:migrations

  • pyproject.toml:保存配置文件路径,低版本可能是 aerich.ini
  • migrations:存放迁移文件
2.初始化数据库,一般情况下只用一次

aerich init-db

image-20250521105044582
3.更新模型并进行迁移

修改model类,重新生成迁移文件

aerich migrate

image-20250521111024194
4.重新执行迁移,写入数据库

aerich upgrade

image-20250521111131931
5.回到上一个版本

aerich downgrade

6.查看历史迁移记录

aerich history

register_tortoise 是 Tortoise ORM 提供的一个工具函数,用于在 FastAPI 等异步框架中快速集成和管理 Tortoise ORM 的生命周期(如启动时初始化数据库连接,关闭时释放资源)。其核心作用是简化 Tortoise ORM 的配置和自动化管理,开发者只需一行代码即可完成复杂的初始化流程

6.3 ORM查询操作

api.stud

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
from fastapi import APIRouter
#导入数据库
from db.models import *

student_api=APIRouter()

@student_api.get("")
async def get_students():
"""
#查询所有学生信息
students= await Student.all()#获取所有学生信息
for student in students:
print(student.id,student.name)

#过滤查询filter
students= await Student.filter(name__contains="张").all()
for student in students:
print(student.id,student.name)

#get学生信息
stu = await Student.get(id=1)
print(stu.id,stu.name)

#模糊查询
#最大值
stu =await Student.filter(sno__gt=1000).all()
#最小值
#stu = await Student.filter(sno__lt=1000).all()
#范围查询
#stu = await Student.filter(sno__range=(1000,2000)).all()

#values查询
stu = await Student.all().values("sno","name")
"""

return{
"操作":"获取所有学生信息"
}

@student_api.post("")
async def create_student():
return{
"操作":"创建学生信息"
}

@student_api.get("/{student_id}")
async def get_student(student_id:int):
return{
"操作":f"获取学生信息,ID:{student_id}"
}

@student_api.put("/{student_id}")
async def update_student(student_id:int):
return{
"操作":f"更新学生信息,ID:{student_id}"
}

在 FastAPI 和 Tortoise ORM 中,asyncawait 用于异步编程,主要原因如下:

  1. 异步 I/O 操作 数据库查询(如 Student.all())是耗时的 I/O 操作。使用 async/await 可以在等待数据库响应时,不阻塞主线程,提高应用的并发性能。
  2. FastAPI 支持异步路由 FastAPI 支持异步(async def)的路由函数,这样可以充分利用 Python 的异步特性,提升 Web 服务的吞吐量。
  3. Tortoise ORM 的方法是异步的 Tortoise ORM 的数据库操作方法(如 .all().create() 等)本身就是异步方法,必须用 await 调用,并且所在函数必须用 async def 声明。

中间件

FastAPI 的「中间件(middleware)」就是 在请求进入路由函数之前、响应离开路由函数之后 插入的 通用处理逻辑

image-20250815144154925

什么时候需要中间件

真实场景举例 —— 统一鉴权 + 日志

需求 • 所有 API(/user、/order、/admin …)都必须验证 JWT; • 无论成功或失败,都记录一条结构化日志(URL、耗时、用户 ID、响应码); • 鉴权失败直接 401,不再进入任何路由。

实战

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from fastapi import FastAPI, Request
import uvicorn
app = FastAPI()

@app.middleware("http") # 注册一个全局中间件
async def mid1(request: Request, call_next):
print("mid1 request")
response = await call_next(request) #先让请求继续往后走
print("mid1 respones")
return response

@app.middleware("http")
async def mid2(request: Request, call_next):
print("mid2 request")
response = await call_next(request)
print("mid2 respones")
return response

@app.get("/") # 任意路由
def home():
print("home")
return {"msg": "ok"}


if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1", port=8000)

注意中间件添加的顺序

FastAPI/Starlette 把后添加的中间件包在最外层(洋葱最外层)

所以 mid2 在外层,请求先打印

1
2
3
4
5
mid2 request
mid1 request
home
mid1 response
mid2 response

cors组件

CORS(Cross-Origin Resource Sharing,跨源资源共享) 一句话:浏览器为了安全,默认禁止网页去“别的域名/端口/协议”拿数据;CORS 是一套 HTTP 机制,让服务器告诉浏览器“我允许谁来拿、拿什么、怎么拿”。

同源与跨域

同源(Same-Origin)

浏览器把下面三个部分合称 “源”(origin):

  1. 协议(http: / https:
  2. 域名(example.com / sub.example.com 算不同)
  3. 端口(:80 / :8080

只有当 协议 + 域名 + 端口 都完全一致时,才叫同源。 此时前端 JS 可以无限制地访问该源下的资源。

跨域(Cross-Origin)

只要 协议、域名、端口 中的任意一个不同,就是跨域

正常来说,服务器为了保护数据,会拒绝跨域的响应,但是通过cors可以允许跨域,因此,CORS 不是“绕过”安全限制,而是服务器主动声明的“安全白名单”。

1
2
3
4
5
6
7
8
9
# 为 FastAPI 应用添加 CORS(跨源资源共享)中间件
# 允许浏览器跨域访问本服务,开发阶段常用;生产环境请收窄范围
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # 允许所有来源(生产建议改成具体域名列表,如 ["https://foo.com"])
allow_credentials=False, # 是否允许携带 Cookie/Authorization;True 时 origins 不能为 "*"
allow_methods=["GET"], # 允许的 HTTP 方法;["*"] 表示全部
allow_headers=["*"], # 允许的自定义请求头;["*"] 表示全部
)

知识点

端口查询

查看所有端口占用情况:netstat -ano

查询特定端口是否被占用:netstat -ano | findstr 8080

使用 taskkill 命令强制结束进程:taskkill /PID 进程ID /F

通过 PID 查找进程:tasklist | findstr PID

获取绝对路径

获取当前文件的绝对路径:base_dir = os.path.dirname(os.path.abspath(__file__))

拼接路径:img_dir = os.path.join(base_dir, "../imgs")

mysql部分指令

查看数据库列表SHOW DATABASES;

选择数据库USE 数据库名;

删除数据库DROP DATABASE 数据库名;

创建数据库CREATE DATABASE 数据库名;

登录 MySQLmysql -u root -p

退出exit

docker部分指令

linux安装dockersudo apt-get update && sudo apt-get install docker.io

查看 Docker 版本信息docker version

查看镜像docker images

查看所有的容器docker ps -a

systemctlsystemd 系统和服务管理器的核心工具,用于管理系统和服务的状态及配置。

mysql-client 是 MySQL 数据库的命令行客户端工具。它允许你通过命令行连接和操作 MySQL 数据库服务器,比如执行 SQL 查询、管理数据库和用户等。

常用命令格式如下:mysql -h 主机地址 -P 端口号 -u 用户名 -p

你可以在终端输入以下命令来检查是否已安装 mysql-clientmysql --version

可以使用以下命令安装:sudo apt-get update sudo apt-get install mysql-client

sudo apt-get update 这个命令的作用是更新本地软件包列表

停止并删除容器docker stop fastapi docker rm fastapi

参考资料

fastapi一个项目FastAPI进阶_哔哩哔哩_bilibili

教程fastapi框架快速学习_哔哩哔哩_bilibili

fastapi相关知识的补充Python 异步编程 - 搞明白 async, await (继续解释 yield)_哔哩哔哩_bilibili

复习:这门课程得主要目的是通过真实的数据,以实战的方式了解数据分析的流程和熟悉数据分析python的基本操作。知道了课程的目的之后,我们接下来我们要正式的开始数据分析的实战教学,完成kaggle上泰坦尼克的任务,实战数据分析全流程。 这里有两份资料: 教材《Python for Data Analysis》和 baidu.com & google.com(善用搜索引擎)

1 第一章:数据载入及初步观察

1.1 载入数据

数据集下载 https://www.kaggle.com/c/titanic/overview

1.1.1 任务一:导入numpy和pandas

1
2
3
4
#写入代码
import pandas as pd
import numpy as np

【提示】如果加载失败,学会如何在你的python环境下安装numpy和pandas这两个库

1.1.2 任务二:载入数据

  1. 使用相对路径载入数据
  2. 使用绝对路径载入数据
1
2
3
4
5
#写入代码
import os
os.getcwd()
test=pd.read_csv('./titanic/test.csv')
train=pd.read_csv('./titanic/train.csv')
1
2
3
4
5
6
7
8
#写入代码
abs_path_test=os.path.abspath('./titanic/test.csv')
print(abs_path_test)
test=pd.read_csv(abs_path_test)
abs_path_train=os.path.abspath('./titanic/train.csv')
print(abs_path_train)
train=pd.read_csv(abs_path_train)
train.head()
/workspace/WuTeachingAI/hands-on-data-analysis/myself/titanic/test.csv
/workspace/WuTeachingAI/hands-on-data-analysis/myself/titanic/train.csv
PassengerId Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked
0 1 0 3 Braund, Mr. Owen Harris male 22.0 1 0 A/5 21171 7.2500 NaN S
1 2 1 1 Cumings, Mrs. John Bradley (Florence Briggs Th… female 38.0 1 0 PC 17599 71.2833 C85 C
2 3 1 3 Heikkinen, Miss. Laina female 26.0 0 0 STON/O2. 3101282 7.9250 NaN S
3 4 1 1 Futrelle, Mrs. Jacques Heath (Lily May Peel) female 35.0 1 0 113803 53.1000 C123 S
4 5 0 3 Allen, Mr. William Henry male 35.0 0 0 373450 8.0500 NaN S

【提示】相对路径载入报错时,尝试使用os.getcwd()查看当前工作目录。
【思考】知道数据加载的方法后,试试pd.read_csv()和pd.read_table()的不同,如果想让他们效果一样,需要怎么做?了解一下’.tsv’和’.csv’的不同,如何加载这两个数据集?
【总结】加载的数据是所有工作的第一步,我们的工作会接触到不同的数据格式(eg:.csv;.tsv;.xlsx),但是加载的方法和思路都是一样的,在以后工作和做项目的过程中,遇到之前没有碰到的问题,要多多查资料吗,使用googel,了解业务逻辑,明白输入和输出是什么。

1
2
3
# pd.read_csv() 和 pd.read_table() 本质上非常相似,主要区别在于默认的分隔符参数。
# 通过显式设置 `sep` 参数,可以让它们处理各种以不同字符分隔的文本文件。
# '.csv' 文件用逗号分隔,'.tsv' 文件用制表符分隔。选择合适的pandas读取函数或正确设置`sep`参数即可加载。

1.1.3 任务三:每1000行为一个数据模块,逐块读取

1
2
3
4
#写入代码
chunker=pd.read_csv('./titanic/train.csv',chunksize=1000)
for chunk in chunker:
print(chunk)
     PassengerId  Survived  Pclass  \
0              1         0       3   
1              2         1       1   
2              3         1       3   
3              4         1       1   
4              5         0       3   
..           ...       ...     ...   
886          887         0       2   
887          888         1       1   
888          889         0       3   
889          890         1       1   
890          891         0       3   

                                                  Name     Sex   Age  SibSp  \
0                              Braund, Mr. Owen Harris    male  22.0      1   
1    Cumings, Mrs. John Bradley (Florence Briggs Th...  female  38.0      1   
2                               Heikkinen, Miss. Laina  female  26.0      0   
3         Futrelle, Mrs. Jacques Heath (Lily May Peel)  female  35.0      1   
4                             Allen, Mr. William Henry    male  35.0      0   
..                                                 ...     ...   ...    ...   
886                              Montvila, Rev. Juozas    male  27.0      0   
887                       Graham, Miss. Margaret Edith  female  19.0      0   
888           Johnston, Miss. Catherine Helen "Carrie"  female   NaN      1   
889                              Behr, Mr. Karl Howell    male  26.0      0   
890                                Dooley, Mr. Patrick    male  32.0      0   

     Parch            Ticket     Fare Cabin Embarked  
0        0         A/5 21171   7.2500   NaN        S  
1        0          PC 17599  71.2833   C85        C  
2        0  STON/O2. 3101282   7.9250   NaN        S  
3        0            113803  53.1000  C123        S  
4        0            373450   8.0500   NaN        S  
..     ...               ...      ...   ...      ...  
886      0            211536  13.0000   NaN        S  
887      0            112053  30.0000   B42        S  
888      2        W./C. 6607  23.4500   NaN        S  
889      0            111369  30.0000  C148        C  
890      0            370376   7.7500   NaN        Q  

[891 rows x 12 columns]

【思考】什么是逐块读取?为什么要逐块读取呢?

【提示】大家可以chunker(数据块)是什么类型?用for循环打印出来出处具体的样子是什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
# **什么是逐块读取?**
# 逐块读取(Chunking)是指在读取大型数据集时,不一次性将整个文件加载到内存中,而是将文件分成若干个小的数据块(chunks),每次只加载和处理一个数据块。
# 在pandas中,可以通过在 `pd.read_csv()` 或类似的读取函数中设置 `chunksize` 参数来实现逐块读取。`chunksize` 定义了每个数据块包含的行数。
#
# **为什么要逐块读取呢?**
# 1. **处理内存不足的大文件**:
# * 当数据集非常大,其大小超过了计算机可用内存时,一次性加载整个文件会导致内存溢出错误(MemoryError)。逐块读取允许我们分批处理数据,每次只在内存中保留一小部分数据,从而有效避免内存问题。
# 2. **提高处理效率(特定场景下)**:
# * 对于某些类型的操作,例如对数据进行迭代处理、过滤或聚合,如果不需要同时访问所有数据,逐块处理可以使得程序更快地开始处理数据,而不是等待整个大文件加载完毕。
# * 可以边读取边处理,实现流式数据处理的效果。
# 3. **数据清洗和预处理**:
# * 在对大型原始数据进行初步的清洗、转换或特征工程时,可以逐块进行,将处理后的数据块追加到新的存储中,或者在每个块上计算统计量并逐步累积。

1.1.4 任务四:将表头改成中文,索引改为乘客ID [对于某些英文资料,我们可以通过翻译来更直观的熟悉我们的数据]

PassengerId => 乘客ID
Survived => 是否幸存
Pclass => 乘客等级(1/2/3等舱位)
Name => 乘客姓名
Sex => 性别
Age => 年龄
SibSp => 堂兄弟/妹个数
Parch => 父母与小孩个数
Ticket => 船票信息
Fare => 票价
Cabin => 客舱
Embarked => 登船港口

1
2
3
4
#写入代码
#将"乘客ID"列作为行索引
df=pd.read_csv('./titanic/train.csv',names=['乘客ID','是否幸存','仓位等级','姓名','性别','年龄','兄弟姐妹个数','父母子女个数','船票信息','票价','客舱','登船港口'],index_col='乘客ID',header=0)
df.head()
是否幸存 仓位等级 姓名 性别 年龄 兄弟姐妹个数 父母子女个数 船票信息 票价 客舱 登船港口
乘客ID
1 0 3 Braund, Mr. Owen Harris male 22.0 1 0 A/5 21171 7.2500 NaN S
2 1 1 Cumings, Mrs. John Bradley (Florence Briggs Th… female 38.0 1 0 PC 17599 71.2833 C85 C
3 1 3 Heikkinen, Miss. Laina female 26.0 0 0 STON/O2. 3101282 7.9250 NaN S
4 1 1 Futrelle, Mrs. Jacques Heath (Lily May Peel) female 35.0 1 0 113803 53.1000 C123 S
5 0 3 Allen, Mr. William Henry male 35.0 0 0 373450 8.0500 NaN S

【思考】所谓将表头改为中文其中一个思路是:将英文列名表头替换成中文。还有其他的方法吗?

1.2 初步观察

导入数据后,你可能要对数据的整体结构和样例进行概览,比如说,数据大小、有多少列,各列都是什么格式的,是否包含null等

1.2.1 任务一:查看数据的基本信息

1
2
3
4
#写入代码
df.info()
df.describe()

<class 'pandas.core.frame.DataFrame'>
Index: 891 entries, 1 to 891
Data columns (total 11 columns):
 #   Column  Non-Null Count  Dtype  
---  ------  --------------  -----  
 0   是否幸存    891 non-null    int64  
 1   仓位等级    891 non-null    int64  
 2   姓名      891 non-null    object 
 3   性别      891 non-null    object 
 4   年龄      714 non-null    float64
 5   兄弟姐妹个数  891 non-null    int64  
 6   父母子女个数  891 non-null    int64  
 7   船票信息    891 non-null    object 
 8   票价      891 non-null    float64
 9   客舱      204 non-null    object 
 10  登船港口    889 non-null    object 
dtypes: float64(2), int64(4), object(5)
memory usage: 83.5+ KB
是否幸存 仓位等级 年龄 兄弟姐妹个数 父母子女个数 票价
count 891.000000 891.000000 714.000000 891.000000 891.000000 891.000000
mean 0.383838 2.308642 29.699118 0.523008 0.381594 32.204208
std 0.486592 0.836071 14.526497 1.102743 0.806057 49.693429
min 0.000000 1.000000 0.420000 0.000000 0.000000 0.000000
25% 0.000000 2.000000 20.125000 0.000000 0.000000 7.910400
50% 0.000000 3.000000 28.000000 0.000000 0.000000 14.454200
75% 1.000000 3.000000 38.000000 1.000000 0.000000 31.000000
max 1.000000 3.000000 80.000000 8.000000 6.000000 512.329200

【提示】有多个函数可以这样做,你可以做一下总结

1.2.2 任务二:观察表格前10行的数据和后15行的数据

1
2
#写入代码
df.head(15)
是否幸存 仓位等级 姓名 性别 年龄 兄弟姐妹个数 父母子女个数 船票信息 票价 客舱 登船港口
乘客ID
1 0 3 Braund, Mr. Owen Harris male 22.0 1 0 A/5 21171 7.2500 NaN S
2 1 1 Cumings, Mrs. John Bradley (Florence Briggs Th… female 38.0 1 0 PC 17599 71.2833 C85 C
3 1 3 Heikkinen, Miss. Laina female 26.0 0 0 STON/O2. 3101282 7.9250 NaN S
4 1 1 Futrelle, Mrs. Jacques Heath (Lily May Peel) female 35.0 1 0 113803 53.1000 C123 S
5 0 3 Allen, Mr. William Henry male 35.0 0 0 373450 8.0500 NaN S
6 0 3 Moran, Mr. James male NaN 0 0 330877 8.4583 NaN Q
7 0 1 McCarthy, Mr. Timothy J male 54.0 0 0 17463 51.8625 E46 S
8 0 3 Palsson, Master. Gosta Leonard male 2.0 3 1 349909 21.0750 NaN S
9 1 3 Johnson, Mrs. Oscar W (Elisabeth Vilhelmina Berg) female 27.0 0 2 347742 11.1333 NaN S
10 1 2 Nasser, Mrs. Nicholas (Adele Achem) female 14.0 1 0 237736 30.0708 NaN C
11 1 3 Sandstrom, Miss. Marguerite Rut female 4.0 1 1 PP 9549 16.7000 G6 S
12 1 1 Bonnell, Miss. Elizabeth female 58.0 0 0 113783 26.5500 C103 S
13 0 3 Saundercock, Mr. William Henry male 20.0 0 0 A/5. 2151 8.0500 NaN S
14 0 3 Andersson, Mr. Anders Johan male 39.0 1 5 347082 31.2750 NaN S
15 0 3 Vestrom, Miss. Hulda Amanda Adolfina female 14.0 0 0 350406 7.8542 NaN S
1
2
#写入代码
df.tail()

1.2.4 任务三:判断数据是否为空,为空的地方返回True,其余地方返回False

1
2
#写入代码
df.isnull().head()
是否幸存 仓位等级 姓名 性别 年龄 兄弟姐妹个数 父母子女个数 船票信息 票价 客舱 登船港口
乘客ID
1 False False False False False False False False False True False
2 False False False False False False False False False False False
3 False False False False False False False False False True False
4 False False False False False False False False False False False
5 False False False False False False False False False True False

【总结】上面的操作都是数据分析中对于数据本身的观察

【思考】对于一个数据,还可以从哪些方面来观察?找找答案,这个将对下面的数据分析有很大的帮助

1.3 保存数据

1.3.1 任务一:将你加载并做出改变的数据,在工作目录下保存为一个新文件train_chinese.csv

1
2
3
#写入代码
# 注意:不同的操作系统保存下来可能会有乱码。大家可以加入`encoding='GBK' 或者 ’encoding = ’utf-8‘‘`
df.to_csv('./titanic/train_chinese.csv',encoding='utf-8')

【总结】数据的加载以及入门,接下来就要接触数据本身的运算,我们将主要掌握numpy和pandas在工作和项目场景的运用。

复习:数据分析的第一步,加载数据我们已经学习完毕了。当数据展现在我们面前的时候,我们所要做的第一步就是认识他,今天我们要学习的就是了解字段含义以及初步观察数据

1 第一章:数据载入及初步观察

1.4 知道你的数据叫什么

我们学习pandas的基础操作,那么上一节通过pandas加载之后的数据,其数据类型是什么呢?

开始前导入numpy和pandas

1
2
import numpy as np
import pandas as pd

1.4.1 任务一:pandas中有两个数据类型DateFrame和Series,通过查找简单了解他们。然后自己写一个关于这两个数据类型的小例子🌰[开放题]

1
2
3
4
5
#我们举的例子
sdata = {'Ohio': 35000, 'Texas': 71000, 'Oregon': 16000, 'Utah': 5000}
example_1 = pd.Series(sdata)
example_1

Ohio      35000
Texas     71000
Oregon    16000
Utah       5000
dtype: int64
1
2
3
4
5
#我们举的例子
data = {'state': ['Ohio', 'Ohio', 'Ohio', 'Nevada', 'Nevada', 'Nevada'],
'year': [2000, 2001, 2002, 2001, 2002, 2003],'pop': [1.5, 1.7, 3.6, 2.4, 2.9, 3.2]}
example_2 = pd.DataFrame(data)
example_2
state year pop
0 Ohio 2000 1.5
1 Ohio 2001 1.7
2 Ohio 2002 3.6
3 Nevada 2001 2.4
4 Nevada 2002 2.9
5 Nevada 2003 3.2

1.4.2 任务二:根据上节课的方法载入”train.csv”文件

1
2
#写入代码
df=pd.read_csv('./titanic/train.csv')

也可以加载上一节课保存的”train_chinese.csv”文件。通过翻译版train_chinese.csv熟悉了这个数据集,然后我们对trian.csv来进行操作 #### 1.4.3 任务三:查看DataFrame数据的每列的名称

1
2
#写入代码
df.columns
Index(['PassengerId', 'Survived', 'Pclass', 'Name', 'Sex', 'Age', 'SibSp',
       'Parch', 'Ticket', 'Fare', 'Cabin', 'Embarked'],
      dtype='object')

1.4.4任务四:查看”Cabin”这列的所有值[有多种方法]

1
2
#写入代码
df['Cabin'].head(10)
0     NaN
1     C85
2     NaN
3    C123
4     NaN
5     NaN
6     E46
7     NaN
8     NaN
9     NaN
Name: Cabin, dtype: object
1
2
3
#写入代码
df.Cabin.values[:10]
df.Cabin.head(10)
0     NaN
1     C85
2     NaN
3    C123
4     NaN
5     NaN
6     E46
7     NaN
8     NaN
9     NaN
Name: Cabin, dtype: object

1.4.5 任务五:加载文件”test_1.csv”,然后对比”train.csv”,看看有哪些多出的列,然后将多出的列删除

经过我们的观察发现一个测试集test_1.csv有一列是多余的,我们需要将这个多余的列删去

1
2
3
#写入代码
test_1=pd.read_csv('../第一单元项目集合/test_1.csv')
test_1.head()
Unnamed: 0 PassengerId Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked a
0 0 1 0 3 Braund, Mr. Owen Harris male 22.0 1 0 A/5 21171 7.2500 NaN S 100
1 1 2 1 1 Cumings, Mrs. John Bradley (Florence Briggs Th… female 38.0 1 0 PC 17599 71.2833 C85 C 100
2 2 3 1 3 Heikkinen, Miss. Laina female 26.0 0 0 STON/O2. 3101282 7.9250 NaN S 100
3 3 4 1 1 Futrelle, Mrs. Jacques Heath (Lily May Peel) female 35.0 1 0 113803 53.1000 C123 S 100
4 4 5 0 3 Allen, Mr. William Henry male 35.0 0 0 373450 8.0500 NaN S 100
1
2
3
4
#写入代码
#test_1.columns
test_1.drop('Unnamed: 0',axis=1,inplace=True)
test_1.columns
Index(['PassengerId', 'Survived', 'Pclass', 'Name', 'Sex', 'Age', 'SibSp',
       'Parch', 'Ticket', 'Fare', 'Cabin', 'Embarked', 'a'],
      dtype='object')

【思考】还有其他的删除多余的列的方式吗?

1
2
3
# 思考回答
del test_1['Unnamed: 0']
test_1.columns
Index(['PassengerId', 'Survived', 'Pclass', 'Name', 'Sex', 'Age', 'SibSp',
       'Parch', 'Ticket', 'Fare', 'Cabin', 'Embarked', 'a'],
      dtype='object')

1.4.6 任务六: 将[‘PassengerId’,‘Name’,‘Age’,‘Ticket’]这几个列元素隐藏,只观察其他几个列元素

1
2
#写入代码
df.drop(['PassengerId', 'Name', 'Age','Ticket'], axis=1).head(10)
Survived Pclass Sex SibSp Parch Fare Cabin Embarked
0 0 3 male 1 0 7.2500 NaN S
1 1 1 female 1 0 71.2833 C85 C
2 1 3 female 0 0 7.9250 NaN S
3 1 1 female 1 0 53.1000 C123 S
4 0 3 male 0 0 8.0500 NaN S
5 0 3 male 0 0 8.4583 NaN Q
6 0 1 male 0 0 51.8625 E46 S
7 0 3 male 3 1 21.0750 NaN S
8 1 3 female 0 2 11.1333 NaN S
9 1 2 female 1 0 30.0708 NaN C

【思考】对比任务五和任务六,是不是使用了不一样的方法(函数),如果使用一样的函数如何完成上面的不同的要求呢?

【思考回答】

如果想要完全的删除你的数据结构,使用inplace=True,因为使用inplace就将原数据覆盖了,所以这里没有用

1.5 筛选的逻辑

表格数据中,最重要的一个功能就是要具有可筛选的能力,选出我所需要的信息,丢弃无用的信息。

下面我们还是用实战来学习pandas这个功能。

1.5.1 任务一: 我们以”Age”为筛选条件,显示年龄在10岁以下的乘客信息。

1
2
3
#写入代码
print((df['Age']<10).head(2))
df[df['Age']<10].head(2)
0    False
1    False
Name: Age, dtype: bool
PassengerId Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked
7 8 0 3 Palsson, Master. Gosta Leonard male 2.0 3 1 349909 21.075 NaN S
10 11 1 3 Sandstrom, Miss. Marguerite Rut female 4.0 1 1 PP 9549 16.700 G6 S

1.5.2 任务二: 以”Age”为条件,将年龄在10岁以上和50岁以下的乘客信息显示出来,并将这个数据命名为midage

1
2
3
#写入代码
midage=df[(df['Age']>10) & (df['Age']<50)]
midage

【提示】了解pandas的条件筛选方式以及如何使用交集和并集操作

1.5.3 任务三:将midage的数据中第100行的”Pclass”和”Sex”的数据显示出来

1
2
3
4
#写入代码
midage = midage.reset_index(drop=True)
print(midage)
midage.loc[1][['Pclass', 'Sex']]
     PassengerId  Survived  Pclass  \
0              1         0       3   
1              2         1       1   
2              3         1       3   
3              4         1       1   
4              5         0       3   
..           ...       ...     ...   
571          886         0       3   
572          887         0       2   
573          888         1       1   
574          890         1       1   
575          891         0       3   

                                                  Name     Sex   Age  SibSp  \
0                              Braund, Mr. Owen Harris    male  22.0      1   
1    Cumings, Mrs. John Bradley (Florence Briggs Th...  female  38.0      1   
2                               Heikkinen, Miss. Laina  female  26.0      0   
3         Futrelle, Mrs. Jacques Heath (Lily May Peel)  female  35.0      1   
4                             Allen, Mr. William Henry    male  35.0      0   
..                                                 ...     ...   ...    ...   
571               Rice, Mrs. William (Margaret Norton)  female  39.0      0   
572                              Montvila, Rev. Juozas    male  27.0      0   
573                       Graham, Miss. Margaret Edith  female  19.0      0   
574                              Behr, Mr. Karl Howell    male  26.0      0   
575                                Dooley, Mr. Patrick    male  32.0      0   

     Parch            Ticket     Fare Cabin Embarked  
0        0         A/5 21171   7.2500   NaN        S  
1        0          PC 17599  71.2833   C85        C  
2        0  STON/O2. 3101282   7.9250   NaN        S  
3        0            113803  53.1000  C123        S  
4        0            373450   8.0500   NaN        S  
..     ...               ...      ...   ...      ...  
571      5            382652  29.1250   NaN        Q  
572      0            211536  13.0000   NaN        S  
573      0            112053  30.0000   B42        S  
574      0            111369  30.0000  C148        C  
575      0            370376   7.7500   NaN        Q  

[576 rows x 12 columns]



Pclass         1
Sex       female
Name: 1, dtype: object

【提示】在抽取数据中,我们希望数据的相对顺序保持不变,用什么函数可以达到这个效果呢?

1.5.4 任务四:使用loc方法将midage的数据中第100,105,108行的”Pclass”,“Name”和”Sex”的数据显示出来

1
2
#写入代码
midage.loc[[100,105,108],['Pclass','Name','Sex']]
Pclass Name Sex
100 2 Byles, Rev. Thomas Roussel Davids male
105 3 Cribb, Mr. John Hatfield male
108 3 Calic, Mr. Jovo male

1.5.5 任务五:使用iloc方法将midage的数据中第100,105,108行的”Pclass”,“Name”和”Sex”的数据显示出来

1
2
#写入代码
midage.iloc[[100,105,108],[2,3,4]]
Pclass Name Sex
149 2 Byles, Rev. Thomas Roussel Davids male
160 3 Cribb, Mr. John Hatfield male
163 3 Calic, Mr. Jovo male

【思考】对比ilocloc的异同

1
2
3
4
5
6
7
8
9
# *   当你需要根据**标签名称**(如行索引名或列名)来选取数据时,使用 `loc`。这使得代码更具可读性,因为你可以直接看到你正在操作的标签。
# * 当你需要根据**整数位置**来选取数据时(不关心标签名称,或者标签不是整数),使用 `iloc`。这在处理没有有意义标签的 DataFrame,或者需要进行与位置相关的操作时很有用。
# * **注意**:如果 DataFrame 的索引是默认的整数索引 (0, 1, 2, ...),那么 `loc` 和 `iloc` 在使用单个整数或整数切片进行行选择时,行为可能会相似,但这可能会导致混淆。
# * 例如,如果 `df.index` 是 `[0, 1, 2, 5, 6]`:
# * `df.loc[0]` 会选择索引标签为 `0` 的行。
# * `df.iloc[0]` 也会选择第一行(即索引标签为 `0` 的行)。
# * `df.loc[3]` 会报错,因为没有索引标签为 `3`。
# * `df.iloc[3]` 会选择第四行(即索引标签为 `5` 的行)。
# * 为了避免混淆,最佳实践是:当你知道你正在使用标签时,明确使用 `loc`;当你知道你正在使用整数位置时,明确使用 `iloc`。

第三章 模型搭建和评估-评估

根据之前的模型的建模,我们知道如何运用sklearn这个库来完成建模,以及我们知道了的数据集的划分等等操作。那么一个模型我们怎么知道它好不好用呢?以至于我们能不能放心的使用模型给我的结果呢?那么今天的学习的评估,就会很有帮助。

加载下面的库

1
2
3
4
5
6
7
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
from IPython.display import Image
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
1
%matplotlib inline
1
2
3
plt.rcParams['font.sans-serif'] = ['SimHei']  # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
plt.rcParams['figure.figsize'] = (10, 6) # 设置输出图片大小

任务:加载数据并分割测试集和训练集

1
2
3
#写入代码

from sklearn.model_selection import train_test_split
1
2
3
4
5
6
7
#写入代码
# 一般先取出X和y后再切割,有些情况会使用到未切割的,这时候X和y就可以用,x是清洗好的数据,y是我们要预测的存活数据'Survived'
data = pd.read_csv('clear_data.csv')
train = pd.read_csv('train.csv')
X = data
y = train['Survived']

1
2
3
4
#写入代码
# 对数据集进行切割
X_train, X_test, y_train, y_test = train_test_split(X, y, stratify=y, random_state=0)

1
2
3
4
5
#写入代码
# 默认参数逻辑回归模型
lr = LogisticRegression()
lr.fit(X_train, y_train)

/root/.pyenv/versions/3.11.1/lib/python3.11/site-packages/sklearn/linear_model/_logistic.py:465: ConvergenceWarning: lbfgs failed to converge (status=1):
STOP: TOTAL NO. OF ITERATIONS REACHED LIMIT.

Increase the number of iterations (max_iter) or scale the data as shown in:
    https://scikit-learn.org/stable/modules/preprocessing.html
Please also refer to the documentation for alternative solver options:
    https://scikit-learn.org/stable/modules/linear_model.html#logistic-regression
  n_iter_i = _check_optimize_result(

模型评估

  • 模型评估是为了知道模型的泛化能力。
  • 交叉验证(cross-validation)是一种评估泛化性能的统计学方法,它比单次划分训练集和测试集的方法更加稳定、全面。
  • 在交叉验证中,数据被多次划分,并且需要训练多个模型。
  • 最常用的交叉验证是 k 折交叉验证(k-fold cross-validation),其中 k 是由用户指定的数字,通常取 5 或 10。
  • 准确率(precision)度量的是被预测为正例的样本中有多少是真正的正例
  • 召回率(recall)度量的是正类样本中有多少被预测为正类
  • f-分数是准确率与召回率的调和平均

【思考】:将上面的概念进一步的理解,大家可以做一下总结

1
2
3
#思考回答:


任务一:交叉验证

  • 用10折交叉验证来评估之前的逻辑回归模型
  • 计算交叉验证精度的平均值
1
2
#提示:交叉验证
Image('Snipaste_2020-01-05_16-37-56.png')
第三章模型建立和评估—评价-课程_16_0

提示4

  • 交叉验证在sklearn中的模块为sklearn.model_selection
1
2
3
#写入代码
from sklearn.model_selection import cross_val_score

1
2
3
#写入代码
lr = LogisticRegression(C=100)
scores = cross_val_score(lr, X_train, y_train, cv=10)
1
2
3
4
#写入代码
# 平均交叉验证分数
print("Average cross-validation score: {:.2f}".format(scores.mean()))

Average cross-validation score: 0.78

思考4

  • k折越多的情况下会带来什么样的影响?
1
2
3
4
5
6
#思考回答
# 当 k 越大时:
# 1. 每次训练使用的数据更多,评估偏差(bias)降低
# 2. 每次测试集样本更少,评估方差(variance)增大
# 3. 需要训练 k 个模型,计算开销显著增加

任务二:混淆矩阵

  • 计算二分类问题的混淆矩阵
  • 计算精确率、召回率以及f-分数

【思考】什么是二分类问题的混淆矩阵,理解这个概念,知道它主要是运算到什么任务中的

1
2
3
4
5
6
7
8
9
#思考回答
# 混淆矩阵(confusion matrix)是一个 2×2 的表格,用于二分类任务中展示模型预测结果与真实标签的对应关系:
# - True Positive (TP):真实为正类,预测也为正类
# - False Positive (FP):真实为负类,却被误预测为正类
# - False Negative (FN):真实为正类,却被误预测为负类
# - True Negative (TN):真实为负类,预测也为负类
# 通过混淆矩阵,可以进一步计算精确率(Precision)、召回率(Recall)、F1 分数等指标,
# 帮助我们评估模型在不同类型错误上的表现,常用于分类模型的性能评估和错误分析。

1
2
#提示:混淆矩阵
Image('Snipaste_2020-01-05_16-38-26.png')
第三章模型建立和评估—评价-课程_27_0
1
2
3
#提示:准确率 (Accuracy),精确度(Precision),Recall,f-分数计算方法
Image('Snipaste_2020-01-05_16-39-27.png')

第三章模型建立和评估—评价-课程_28_0

提示5

  • 混淆矩阵的方法在sklearn中的sklearn.metrics模块
  • 混淆矩阵需要输入真实标签和预测标签
  • 精确率、召回率以及f-分数可使用classification_report模块
1
2
3
#写入代码
from sklearn.metrics import confusion_matrix

1
2
3
4
5
#写入代码
# 训练模型
lr = LogisticRegression(C=100)
lr.fit(X_train, y_train)

/root/.pyenv/versions/3.11.1/lib/python3.11/site-packages/sklearn/linear_model/_logistic.py:465: ConvergenceWarning: lbfgs failed to converge (status=1):
STOP: TOTAL NO. OF ITERATIONS REACHED LIMIT.

Increase the number of iterations (max_iter) or scale the data as shown in:
    https://scikit-learn.org/stable/modules/preprocessing.html
Please also refer to the documentation for alternative solver options:
    https://scikit-learn.org/stable/modules/linear_model.html#logistic-regression
  n_iter_i = _check_optimize_result(
1
2
3
#写入代码
# 模型预测结果
pred = lr.predict(X_train)
1
2
3
4
#写入代码
# 混淆矩阵
confusion_matrix(y_train, pred)

array([[355,  57],
       [ 82, 174]])
1
2
3
from sklearn.metrics import classification_report
# 精确率、召回率以及f1-score
print(classification_report(y_train, pred))
              precision    recall  f1-score   support

           0       0.81      0.86      0.84       412
           1       0.75      0.68      0.71       256

    accuracy                           0.79       668
   macro avg       0.78      0.77      0.78       668
weighted avg       0.79      0.79      0.79       668

【思考】 * 如果自己实现混淆矩阵的时候该注意什么问题

1
2
3
4
5
6
7
8
9
#思考回答
# 如果自己实现混淆矩阵,需要注意:
# 1. 明确行列含义:通常行是真实标签,列是预测标签,并保持一致。
# 2. 类别顺序要固定:最好指定 labels 列表,避免类别稀疏时错位。
# 3. 初始化大小为 n_classes×n_classes 的零矩阵。
# 4. 索引时使用整数或统一的类别映射,避免类型不一致。
# 5. 对每个样本累加到对应的 [真实, 预测] 单元格,最后矩阵元素之和应等于样本数。
# 6. 对于未出现的类别,矩阵对应行或列应保留 0。

任务三:ROC曲线

  • 绘制ROC曲线

【思考】什么是ROC曲线,OCR曲线的存在是为了解决什么问题?

1
2
3
4
5
6
7
#思考
# ROC曲线(Receiver Operating Characteristic Curve)是一条以假正例率(FPR)为横坐标、
# 真正例率(TPR)为纵坐标绘制的曲线,展示模型在不同阈值下的分类性能变化。
# 它解决了单一阈值下评估不全面的问题,通过曲线下的面积(AUC)能够衡量模型整体区分正负样本的能力;
# 对类别不平衡更稳健,可在不同模型或参数设置间进行客观比较。


提示6

  • ROC曲线在sklearn中的模块为sklearn.metrics
  • ROC曲线下面所包围的面积越大越好
1
2
3
#写入代码
from sklearn.metrics import roc_curve

1
print(lr.decision_function(X_test))
[-1.7776276  -1.68901519 -2.9343385  -2.73339993 -0.7425476   0.1771919
  0.42300886 -0.95177507 -2.19297241 -2.09492243 -2.09876666 -2.24379328
 -0.72898893 -0.74448703  1.55206252  2.26736362 -3.0615053  -1.45551632
  1.82143942  1.10174703  2.80348253  2.20862227 -2.08595792 -1.98565326
 -2.62459231  2.61608127  2.52054836  0.46386814 -2.26805651 -1.89799476
 -4.40221097 -2.45118004 -2.11507984  0.25727282  1.56507901 -3.49922092
  0.09517543  3.1727335  -0.66659502 -2.16889122 -2.31738004 -0.75154631
  1.34173247 -0.68691348 -2.38317701 -1.48352807  3.30441868  0.37836543
  0.15120699 -2.39554116  0.71230509 -2.94049784  0.0526656  -0.12124772
  0.21937853 -0.95736671 -2.91315052  1.73227025 -2.30451919 -0.11949728
 -2.40406452 -1.23217853 -3.04709277 -2.51149884 -2.91275507  0.36741872
  1.88515182 -1.73344723  1.61180838 -2.64456699 -2.82671595 -1.32885535
 -1.89201447 -2.38194062  1.14830497  0.7324757   3.41575634 -0.04718518
  1.99047031  0.71098531 -2.5002286   2.11220527  1.35687779 -4.65208202
 -0.50164169 -2.21847127 -0.27744568 -2.1098023  -2.28203956 -2.24087733
  1.49913758 -0.46745632 -1.76590269 -3.13694507 -2.48969764 -2.52447108
 -0.31359417 -2.62456277  0.10812447 -3.22505518 -0.54301462 -1.31398633
 -2.45637232 -0.9392769  -1.99910791 -0.01952273  0.16386412  1.17043699
  0.83571934 -0.30892412 -2.56236834 -2.52630696 -2.15878988  3.38005162
 -1.63316112 -2.0470374   1.16802525  1.96428556 -0.85542758 -0.84711271
 -2.3923425  -2.27467461  1.27340371 -0.16738478  2.77379952 -0.91636487
  3.49337899  2.22265823 -1.03898765 -1.79576035  3.05405598 -1.72625544
 -2.08233698  0.14427761 -2.03826492 -1.87510703 -2.43040363  0.88364821
 -2.31722422  1.21479438 -2.19509856 -1.96948465  2.90456606  1.22909197
 -0.60993113 -2.40508898  1.79832298 -2.33619419 -1.76964851  0.54894164
  0.56920781 -1.65544357 -2.18783672 -2.51890544 -1.1167812   1.85506633
 -2.14366192  2.56003678  1.79741811  2.22038003 -0.93948297  2.11029939
  3.66773152  3.37255532 -1.62079149 -0.21922341 -2.93532548 -1.8851028
 -0.11223495 -0.89402373 -2.79168773  0.58319665 -1.20213471  2.11583429
 -1.78550619 -1.21648746 -2.91538781 -2.80005448 -2.74359191 -0.06775047
 -1.28645408 -1.17048578 -0.1176852  -1.59958242 -0.65901928 -2.40701243
  0.57575073 -3.0756839   1.53932753 -2.49031769 -3.03266822  0.30539932
 -0.05523861 -0.24431132 -2.36483723  3.25595248 -2.11664845 -1.97728592
 -2.04509461 -3.07727841 -1.11942703 -3.38920295 -2.59088459 -3.55978164
  0.22449105 -0.3214215   0.05735696  0.02061023 -3.01544378 -0.77973619
 -1.39798016 -3.10075724 -4.80621573 -3.01948006  3.44366918 -2.88193813
 -2.01992513 -0.09559774  0.91447527 -1.13270082 -2.45426968 -1.91415803
 -0.08403516]
1
2
3
4
5
6
7
8
9
10
11
#写入代码
# 计算ROC曲线的假正例率(FPR)、真正例率(TPR)和阈值
fpr, tpr, thresholds = roc_curve(y_test, lr.decision_function(X_test))
plt.plot(fpr, tpr, label="ROC Curve")
plt.xlabel("FPR")
plt.ylabel("TPR (recall)")
# 找到最接近于0的阈值
close_zero = np.argmin(np.abs(thresholds))
plt.plot(fpr[close_zero], tpr[close_zero], 'o', markersize=10, label="threshold zero", fillstyle="none", c='k', mew=2)
plt.legend(loc=4)

第三章模型建立和评估—评价-课程_44_3

思考6

  • 对于多分类问题如何绘制ROC曲线
1
2
3
4
#思考回答



【思考】你能从这条OCR曲线的到什么信息?这些信息可以做什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#思考回答
# 从ROC曲线中可以得到以下信息:
# 1. 模型整体性能:曲线下的面积 (AUC - Area Under the Curve) 是一个常用的评估指标。
# - AUC = 1:完美分类器。
# - AUC = 0.5:随机分类器(无区分能力,ROC曲线接近对角线)。
# - AUC > 0.5:模型优于随机猜测。AUC越大,模型区分正负样本的能力越强。
# - AUC < 0.5:模型表现差于随机猜测(可能标签反了或者模型非常差)。
# 2. 不同阈值下的权衡:ROC曲线展示了在所有可能的分类阈值下,真正例率 (TPR) 与假正例率 (FPR) 之间的关系。
# - 曲线上的每个点代表一个特定的阈值。
# - 曲线越靠近左上角 (FPR低, TPR高),说明模型在较低的假正例率下能达到较高的真正例率,性能越好。
# 3. 模型的区分能力:曲线的形状可以反映模型区分正负样本的能力。如果曲线显著高于对角线,说明模型具有较好的区分能力。
# 4. 阈值选择的依据:可以根据业务需求,在ROC曲线上选择一个合适的平衡点(即选择一个阈值)。
# - 例如,如果更关注减少漏报(提高TPR),可以选择曲线上TPR较高的点,即使FPR可能略高。
# - 如果更关注减少误报(降低FPR),可以选择曲线上FPR较低的点,即使TPR可能略低。
# - 图中标记的 "threshold zero" 点通常是模型默认的分类阈值对应的性能点。


第三章 模型搭建和评估–建模

经过前面的两章的知识点的学习,我可以对数数据的本身进行处理,比如数据本身的增删查补,还可以做必要的清洗工作。那么下面我们就要开始使用我们前面处理好的数据了。这一章我们要做的就是使用数据,我们做数据分析的目的也就是,运用我们的数据以及结合我的业务来得到某些我们需要知道的结果。那么分析的第一步就是建模,搭建一个预测模型或者其他模型;我们从这个模型的到结果之后,我们要分析我的模型是不是足够的可靠,那我就需要评估这个模型。今天我们学习建模,下一节我们学习评估。

我们拥有的泰坦尼克号的数据集,那么我们这次的目的就是,完成泰坦尼克号存活预测这个任务。

1
2
3
4
5
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from IPython.display import Image
1
%matplotlib inline
1
2
3
plt.rcParams['font.sans-serif'] = ['SimHei']  # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
plt.rcParams['figure.figsize'] = (10, 6) # 设置输出图片大小

载入这些库,如果缺少某些库,请安装他们

【思考】这些库的作用是什么呢?你需要查一查

1
2
3
4
5
6
#思考题回答
'''
Image 是 IPython.display 模块的一部分,用于在 Jupyter Notebook 或 IPython 环境中直接显示图像。支持从本地路径或网络 URL 加载图像,并控制显示格式(如宽度、高度、格式类型)
seaborn 是一个基于 Matplotlib 的高级数据可视化库,专注于统计图形的绘制(如分布图、相关性图、分类图等),能快速生成美观且信息丰富的图表
'''

'\nImage 是 IPython.display 模块的一部分,用于在 Jupyter Notebook 或 IPython 环境中直接显示图像。支持从本地路径或网络 URL 加载图像,并控制显示格式(如宽度、高度、格式类型)\nseaborn 是一个基于 Matplotlib 的高级数据可视化库,专注于统计图形的绘制(如分布图、相关性图、分类图等),能快速生成美观且信息丰富的图表\n'
1
%matplotlib inline

载入我们提供清洗之后的数据(clear_data.csv),大家也将原始数据载入(train.csv),说说他们有什么不同

1
2
3
#写入代码
train = pd.read_csv('train.csv')
train.shape
(891, 12)
1
2
#写入代码
train.head()
.dataframe tbody tr th {
    vertical-align: top;
}

.dataframe thead th {
    text-align: right;
}
PassengerId Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked
0 1 0 3 Braund, Mr. Owen Harris male 22.0 1 0 A/5 21171 7.2500 NaN S
1 2 1 1 Cumings, Mrs. John Bradley (Florence Briggs Th… female 38.0 1 0 PC 17599 71.2833 C85 C
2 3 1 3 Heikkinen, Miss. Laina female 26.0 0 0 STON/O2. 3101282 7.9250 NaN S
3 4 1 1 Futrelle, Mrs. Jacques Heath (Lily May Peel) female 35.0 1 0 113803 53.1000 C123 S
4 5 0 3 Allen, Mr. William Henry male 35.0 0 0 373450 8.0500 NaN S
1
2
3
4
#写入代码
data = pd.read_csv('clear_data.csv')

data.head()
PassengerId Pclass Age SibSp Parch Fare Sex_female Sex_male Embarked_C Embarked_Q Embarked_S
0 0 3 22.0 1 0 7.2500 0 1 0 0 1
1 1 1 38.0 1 0 71.2833 1 0 1 0 0
2 2 3 26.0 0 0 7.9250 1 0 0 0 1
3 3 1 35.0 1 0 53.1000 1 0 0 0 1
4 4 3 35.0 0 0 8.0500 0 1 0 0 1

模型搭建

  • 处理完前面的数据我们就得到建模数据,下一步是选择合适模型
  • 在进行模型选择之前我们需要先知道数据集最终是进行监督学习还是无监督学习
  • 模型的选择一方面是通过我们的任务来决定的。
  • 除了根据我们任务来选择模型外,还可以根据数据样本量以及特征的稀疏性来决定
  • 刚开始我们总是先尝试使用一个基本的模型来作为其baseline,进而再训练其他模型做对比,最终选择泛化能力或性能比较好的模型

这里我的建模,并不是从零开始,自己一个人完成完成所有代码的编译。我们这里使用一个机器学习最常用的一个库(sklearn)来完成我们的模型的搭建

下面给出sklearn的算法选择路径,供大家参考

1
2
# sklearn模型算法选择路径图
Image('sklearn.png')
第三章模型建立和评估–建模-课程_17_0

【思考】数据集哪些差异会导致模型在拟合数据是发生变化

1
2
3
#思考回答


任务一:切割训练集和测试集

这里使用留出法划分数据集

  • 将数据集分为自变量和因变量
  • 按比例切割训练集和测试集(一般测试集的比例有30%、25%、20%、15%和10%)
  • 使用分层抽样
  • 设置随机种子以便结果能复现

【思考】 * 划分数据集的方法有哪些? * 为什么使用分层抽样,这样的好处有什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 划分数据集的方法有哪些?
# 1. **留出法 (Hold-out Cross Validation)**:
# * 直接将数据集D划分为两个互斥的集合:训练集S和测试集T。
# * 例如,70%的数据用于训练,30%用于测试。
# * 优点:简单、计算开销小。
# * 缺点:划分具有随机性,单次划分的结果可能不够稳定和准确,尤其是在数据集较小时。训练集和测试集的样本比例会影响评估结果。

# 2. **交叉验证法 (Cross Validation)**:
# * **k折交叉验证 (k-Fold Cross Validation)**:
# * 将数据集D划分为k个大小相似的互斥子集 D1, D2, ..., Dk。
# * 每次用k-1个子集的并集作为训练集,余下的那个子集作为测试集。
# * 这样可以获得k组训练/测试集,从而可进行k次训练和测试,最终返回的是这k个测试结果的均值。
# * 常用的k值为5或10。
# * 优点:比留出法更稳定,更充分地利用了数据。
# * 缺点:计算开销是k倍。
# * **留一法 (Leave-One-Out Cross Validation, LOOCV)**:
# * k折交叉验证的特例,当k等于样本数N时。
# * 每次只留下一个样本作为测试集,其余N-1个样本作为训练集。
# * 优点:评估结果通常被认为比较准确,因为几乎所有数据都用于训练。
# * 缺点:计算开销非常大,尤其是在数据集很大时。

# 3. **自助法 (Bootstrapping)**:
# * 以自助采样法为基础。给定包含m个样本的数据集D,对它进行采样产生数据集D':每次随机从D中挑选一个样本,将其拷贝放入D',然后再将该样本放回初始数据集D中,使得该样本在下次采样时仍有可能被采到。这个过程重复执行m次后,我们就得到了包含m个样本的数据集D'。
# * 可以证明,初始数据集D中约有36.8%的样本未出现在采样数据集D'中。于是我们可将D'用作训练集,D\D'用作测试集。
# * 优点:在数据集较小、难以有效划分训练/测试集时很有用;能从初始数据集中产生多个不同的训练集。
# * 缺点:自助法产生的数据集改变了初始数据集的分布,会引入估计偏差。

# 为什么使用分层抽样,这样的好处有什么?
# **分层抽样 (Stratified Sampling)** 是一种抽样技术,它将总体(数据集)划分为若干个互不重叠的子群(称为“层”),然后从每个层中独立地进行简单随机抽样。在划分训练集和测试集时,特别是对于分类任务,通常是根据目标变量的类别进行分层。

# **好处:**
# 1. **保持类别比例一致性**:
# * 确保训练集和测试集中的各个类别的样本比例与原始数据集中各个类别的样本比例大致相同。
# * 这对于类别不平衡的数据集尤为重要。如果进行纯随机抽样,可能会导致训练集或测试集中某些少数类别的样本过少,甚至没有,从而影响模型的训练效果和评估的可靠性。
# 2. **提高模型的泛化能力和评估的准确性**:
# * 由于训练集和测试集都较好地代表了原始数据的类别分布,模型在训练时能学习到各个类别的特征,评估时也能更准确地反映模型在所有类别上的表现。
# * 避免了因随机划分导致训练集和测试集在类别分布上产生较大差异,从而使得模型评估结果更加稳定和可信。
# 3. **减少抽样误差**:
# * 相比于简单随机抽样,分层抽样通常能得到更具代表性的样本,从而减少因抽样带来的误差,使得基于样本的推断更加精确。

任务提示1

  • 切割数据集是为了后续能评估模型泛化能力
  • sklearn中切割数据集的方法为train_test_split
  • 查看函数文档可以在jupyter noteboo里面使用train_test_split?后回车即可看到
  • 分层和随机种子在参数里寻找

要从clear_data.csv和train.csv中提取train_test_split()所需的参数

1
2
3
4
5
6
7
#写入代码
from sklearn.model_selection import train_test_split
# 一般先取出X和y后再切割,有些情况会使用到未切割的,这时候X和y就可以用,x是清洗好的数据,y是我们要预测的存活数据'Survived'
X = data
y = train['Survived']
# 对数据集进行切割
X_train, X_test, y_train, y_test = train_test_split(X, y, stratify=y, random_state=0)
1
2
3
4
#写入代码

X_train.shape, X_test.shape

((668, 11), (223, 11))

【思考】 * 什么情况下切割数据集的时候不用进行随机选取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#思考回答
# 在以下情况下切割数据集时可能不需要或不适合进行随机选取:

# 1. **时间序列数据 (Time Series Data)**:
# * 对于时间序列数据,数据的顺序至关重要,因为它包含了时间依赖性。随机打乱顺序会破坏这种依赖关系。
# * 通常的做法是按时间顺序划分,例如,将较早的数据作为训练集,较晚的数据作为测试集(或验证集)。这更符合实际应用中用过去预测未来的场景。
# * 例如,用前几年的股票数据训练模型,用最近一年的数据测试模型。

# 2. **数据已经预先排序或具有特定结构**:
# * 如果数据集已经按照某种对分析有意义的顺序排列(例如,按地理区域、按实验批次等),并且你希望测试集来自与训练集不同的、特定的部分,那么可能需要按顺序或按特定规则划分,而不是随机划分。
# * 例如,在一个全国性的调查数据中,你可能想用某些省份的数据做训练,用另一些省份的数据做测试,以检验模型的地域泛化能力。

# 3. **数据集非常大且分布均匀**:
# * 当数据集非常庞大,并且可以合理假设数据是独立同分布 (i.i.d.) 且分布均匀时,简单地按顺序取一部分作为训练集,另一部分作为测试集,其效果可能与随机选取相差不大。随机选取的计算开销在这种情况下可能显得不必要。
# * 然而,即使在这种情况下,随机选取通常仍然是更稳妥的做法,以避免潜在的未知偏差。

# 4. **特定的交叉验证策略**:
# * 某些交叉验证方法本身就定义了非随机的划分方式。例如,在k折交叉验证中,虽然整体上数据被分成了k折,但每一折的选择是确定的(通常是按顺序分割)。留一法交叉验证更是每次只留一个特定的样本作为测试集。

# 5. **当需要完全复现特定的、非随机的划分结果时**:
# * 如果之前的研究或实验使用了某种特定的非随机划分方式,为了比较或复现结果,也需要采用相同的划分方式。

# 6. **流式数据或在线学习场景**:
# * 在数据持续不断流入的场景中,模型可能需要用新到达的数据进行测试或持续训练。这种情况下,测试集自然是最新的一部分数据,而不是从历史数据中随机抽取的。



任务二:模型创建

  • 创建基于线性模型的分类模型(逻辑回归)
  • 创建基于树的分类模型(决策树、随机森林)
  • 分别使用这些模型进行训练,分别的到训练集和测试集的得分
  • 查看模型的参数,并更改参数值,观察模型变化

提示

  • 逻辑回归不是回归模型而是分类模型,不要与LinearRegression混淆
  • 随机森林其实是决策树集成为了降低决策树过拟合的情况
  • 线性模型所在的模块为sklearn.linear_model
  • 树模型所在的模块为sklearn.ensemble
1
2
3
4
5
#写入代码

from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier

1
2
3
4
5
6
#写入代码
# 默认参数逻辑回归模型
lr = LogisticRegression()
lr.fit(X_train, y_train)


/root/.pyenv/versions/3.11.1/lib/python3.11/site-packages/sklearn/linear_model/_logistic.py:465: ConvergenceWarning: lbfgs failed to converge (status=1):
STOP: TOTAL NO. OF ITERATIONS REACHED LIMIT.

Increase the number of iterations (max_iter) or scale the data as shown in:
    https://scikit-learn.org/stable/modules/preprocessing.html
Please also refer to the documentation for alternative solver options:
    https://scikit-learn.org/stable/modules/linear_model.html#logistic-regression
  n_iter_i = _check_optimize_result(
1
2
3
4
5
6
#写入代码
# 查看训练集和测试集score值
print("Training set score: {:.2f}".format(lr.score(X_train, y_train)))
print("Testing set score: {:.2f}".format(lr.score(X_test, y_test)))


Training set score: 0.80
Testing set score: 0.79
1
2
3
4
5
6
7
#写入代码
# 调整参数后的逻辑回归模型
lr2 = LogisticRegression(C=100)
lr2.fit(X_train, y_train)
print("Training set score: {:.2f}".format(lr2.score(X_train, y_train)))
print("Testing set score: {:.2f}".format(lr2.score(X_test, y_test)))

Training set score: 0.79
Testing set score: 0.78


/root/.pyenv/versions/3.11.1/lib/python3.11/site-packages/sklearn/linear_model/_logistic.py:465: ConvergenceWarning: lbfgs failed to converge (status=1):
STOP: TOTAL NO. OF ITERATIONS REACHED LIMIT.

Increase the number of iterations (max_iter) or scale the data as shown in:
    https://scikit-learn.org/stable/modules/preprocessing.html
Please also refer to the documentation for alternative solver options:
    https://scikit-learn.org/stable/modules/linear_model.html#logistic-regression
  n_iter_i = _check_optimize_result(
1
2
3
4
5
# 默认参数的随机森林分类模型
rfc = RandomForestClassifier()
rfc.fit(X_train, y_train)
print("Training set score: {:.2f}".format(rfc.score(X_train, y_train)))
print("Testing set score: {:.2f}".format(rfc.score(X_test, y_test)))
Training set score: 1.00
Testing set score: 0.82

【思考】 * 为什么线性模型可以进行分类任务,背后是怎么的数学关系 * 对于多分类问题,线性模型是怎么进行分类的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#思考回答
# 为什么线性模型可以进行分类任务,背后是怎么的数学关系
# 线性模型(如逻辑回归 Logistic Regression,或者支持向量机 SVM 的线性核)之所以能用于分类任务,是因为它们通过以下方式将线性组合的输入特征映射到类别预测:
# 1. **线性组合**:首先,模型计算输入特征的线性组合,形式通常为 `z = w_1*x_1 + w_2*x_2 + ... + w_n*x_n + b`,或者用向量表示为 `z = w^T * x + b`。
# * `x` 是输入特征向量。
# * `w` 是模型学习到的权重(或系数)。
# * `b` 是偏置项(或截距)。
# 这个 `z` 值可以看作是样本点到决策边界的某种度量。
# 2. **决策函数/激活函数**:然后,这个线性组合的结果 `z` 会被传递给一个决策函数或激活函数,该函数将其转换为类别预测或类别概率。
# * **对于逻辑回归 (Logistic Regression)**:
# * 它使用 Sigmoid (Logistic) 函数:`p = 1 / (1 + e^(-z))`。
# * Sigmoid 函数将任意实数值 `z` 映射到 (0, 1) 区间,这个输出 `p` 可以解释为样本属于正类(通常是类别1)的概率。
# * 通过设定一个阈值(通常是0.5),如果 `p > 0.5` (即 `z > 0`),则预测为正类;否则预测为负类。
# * 因此,决策边界是 `z = 0`,即 `w^T * x + b = 0`,这是一个超平面。
# * **对于线性支持向量机 (Linear SVM)**:
# * 它直接使用 `z` 的符号来决定类别。如果 `z > 0`,预测为一类;如果 `z < 0`,预测为另一类。
# * SVM 的目标是找到一个能最大化两类样本之间间隔(margin)的决策边界(超平面)。
# 总结来说,线性模型通过学习一个线性决策边界(直线、平面或超平面)来分隔不同类别的样本。它们首先计算一个线性得分,然后通过一个非线性函数(如Sigmoid)或直接根据得分的符号来做出分类决策。

# 对于多分类问题,线性模型是怎么进行分类的
# 当类别数量大于两个时(即多分类问题),线性模型通常采用以下两种主要策略之一将问题转化为多个二分类问题:
# 1. **一对余 (One-vs-Rest, OvR) 或 一对所有 (One-vs-All, OvA)**:
# * **原理**:如果有个 `K` 个类别,OvR 策略会训练 `K` 个独立的二分类器。
# * 第 `i` 个分类器 (`i` 从 1 到 `K`) 会将类别 `i` 的样本视为正类,而将所有其他 `K-1` 个类别的样本视为负类。
# * **预测**:对于一个新的样本,它会被输入到所有 `K` 个分类器中。每个分类器都会输出一个分数或概率,表示该样本属于其对应“正类”的置信度。最终,样本被分配给那个给出最高置信度分数的类别。
# * **优点**:直观,实现相对简单,计算效率较高(只需要训练K个分类器)。
# * **缺点**:当类别数量很多时,每个二分类器的负类可能包含非常多样化的样本,可能导致类别不平衡问题。

# 2. **一对一 (One-vs-One, OvO)**:
# * **原理**:如果有个 `K` 个类别,OvO 策略会为每一对类别 `(i, j)` 训练一个二分类器,其中 `i != j`。总共需要训练 `K * (K-1) / 2` 个分类器。
# * 每个分类器只负责区分两个特定的类别。
# * **预测**:对于一个新的样本,它会被输入到所有 `K * (K-1) / 2` 个分类器中。每个分类器都会对样本属于其两个类别中的哪一个进行投票。最终,样本被分配给获得最多投票的类别。
# * **优点**:每个分类器只需要处理两个类别的数据,通常训练速度更快,且对于某些对类别不平衡不敏感的算法(如SVM)可能表现更好。
# * **缺点**:当类别数量 `K` 很大时,需要训练的分类器数量会急剧增加,导致计算成本和存储成本较高。

# **在 scikit-learn 中**:
# * `LogisticRegression` 默认使用 OvR 策略进行多分类 (可以通过 `multi_class` 参数设置为 `'multinomial'` 来使用 Softmax 回归,这是一种直接处理多分类的方法,但其基础仍然是线性的)。
# * `LinearSVC` (线性支持向量机) 默认使用 OvR 策略。


任务三:输出模型预测结果

  • 输出模型预测分类标签
  • 输出不同分类标签的预测概率

提示3

  • 一般监督模型在sklearn里面有个predict能输出预测标签,predict_proba则可以输出标签概率
1
2
#写入代码
pred = lr.predict(X_train)
1
2
#写入代码
pred[:10]
array([0, 1, 1, 1, 0, 0, 1, 0, 1, 1])
1
2
#写入代码
pred_proba = lr.predict_proba(X_train)
1
2
#写入代码
pred_proba[:10]
array([[0.60887905, 0.39112095],
       [0.17668722, 0.82331278],
       [0.40624596, 0.59375404],
       [0.18896449, 0.81103551],
       [0.87984221, 0.12015779],
       [0.91385758, 0.08614242],
       [0.13282516, 0.86717484],
       [0.90555878, 0.09444122],
       [0.05280619, 0.94719381],
       [0.10934565, 0.89065435]])

【思考】 * 预测标签的概率对我们有什么帮助

1
2
3
4
#思考回答



0%