Jiahong 的个人博客

凡事预则立,不预则废


  • Home

  • Tags

  • Archives

  • Navigation

  • Search

Git——Submodule管理


整体说明

  • submodule 允许 git 灵活地将其他项目嵌入到当前项目,同时自由地切换 sumodule 分支和内容管理,且保持各自的版本独立
  • 本质上: submodule 还是作为一个独立的项目存在的,主项目管理子模块靠 .gitmodules 文件
  • 子模块默认跟踪的是固定 commit ID ,而非分支:
    • 若子仓库更新,主仓库需手动更新子模块的 commit ID 并提交;
  • .gitmodules 是子项目的核心文件
    • 这个文件必须纳入版本控制,否则他人克隆仓库时无法识别子模块;
  • 避免嵌套过深的子模块,否则会增加版本管理和协作的复杂度

添加子模块

  • 基础语法

    1
    2
    3
    git submodule add <子仓库URL> <本地存放路径(可选)>
    # 上述命令会自动添加 submodule 相关的必要文件,立刻直接 commit 即可添加 submodule 完成,注意这里如果先执行 checkout 等可能导致 submodule 信息无法对齐,建议立即 commit
    git commit "Add: submodule xxx"
    • <子仓库URL>:子模块的 Git 仓库地址(HTTPS/SSH 均可)
    • <本地存放路径>:子模块在当前仓库中的存放目录(省略则默认用仓库名)
  • 若需让子模块跟踪分支,可添加 -b <分支名> 参数:

    1
    git submodule add -b <branch> <子仓库URL> <路径>
  • submodule 的分支可以切换绑定(或新增绑定)

    1
    2
    3
    4
    git config -f .gitmodules submodule.<本地存放路径>.branch <branch>

    # 比如将 dev 分支绑定到名为 libs/sub-repo 的 submodule 下:
    git config -f .gitmodules submodule.libs/sub-repo.branch dev

示例

  • 示例:比如将 https://github.com/example/sub-repo.git 添加到当前仓库的 libs/sub-repo 目录:

    1
    2
    3
    4
    # 进入主仓库根目录
    cd /path/to/your/main-repo
    # 添加子模块
    git submodule add https://github.com/example/sub-repo.git libs/sub-repo
  • 执行上述命令后,Git 会自动完成以下操作:

    • 1)在指定路径(如 libs/sub-repo)拉取子仓库的代码;

      • 就像是普通的 git 项目一样,还包括 .git 目录
    • 2)在主仓库根目录生成 .gitmodules 文件(记录子模块配置),内容示例:

      1
      2
      3
      [submodule "libs/sub-repo"]
      path = libs/sub-repo
      url = https://github.com/example/sub-repo.git
    • 3)主仓库的暂存区会新增 .gitmodules 和 libs/sub-repo 两个条目(子模块条目是一个“链接”,记录子仓库的 commit ID)

添加后续提交子模块到主仓库

  • 添加子模块后,需要将 .gitmodules 和子模块条目提交到主仓库:
    1
    2
    3
    4
    # 提交变更
    git commit -m "添加子模块 sub-repo 到 libs 目录"
    # 推送到远程
    git push

克隆包含子模块的仓库

  • 直接 git clone 只会拉取子模块目录,但不会拉取子模块的代码,需执行:
    1
    2
    3
    4
    5
    6
    7
    8
    # 方式1:克隆时直接拉取子模块
    git clone --recurse-submodules <主仓库URL>

    # 方式2:先克隆主仓库,再初始化+更新子模块
    git clone <主仓库URL>
    cd main-repo
    git submodule init # 初始化子模块配置(读取.gitmodules)
    git submodule update # 拉取子模块的代码

git submodule update vs git submodule update --remote

  • git submodule update 命令还有个特殊参数 --remote,两者有很多容易犯错的区别
  • git submodule update
    • 用于 对齐主项目固定的子模块版本
    • 仅将子模块切换到主项目记录的哈希值(即 .gitmodules/.gitmodules 中固定的子模块版本);
    • 不会主动从子模块的远程仓库拉取新代码;
    • 若子模块本地无该哈希值的代码,会从远程克隆,但仅克隆该版本
    • 同步主项目指定的子模块版本(比如团队协作时,确保所有人用相同版本的子模块)
    • 执行 git submodule update 后:
      • 子模块处于「分离头指针(detached HEAD)」状态,且版本与主项目记录完全一致;
  • git submodule update --remote
    • 用于 更新子模块到远程最新版本
    • 先从子模块的远程仓库拉取最新代码(更新子模块的远程追踪分支);
    • 再将子模块切换到该远程分支的最新哈希值;
    • 会修改主项目中记录的子模块版本(需手动提交主项目的修改)
    • 主动更新子模块到远程最新版本(比如子模块有新功能/修复,需要同步到主项目)
    • 执行 git submodule update --remote 后:
      • 子模块仍处于「分离头指针」状态(仅指向远程最新哈希),主项目的 git 状态会显示子模块版本已修改
      • 需执行 git add <子模块路径> && git commit 才能将新的子模块版本记录到主项目
    • 慎用这个命令

git submodule update vs git submodule update --remote 示例

  • 假设主项目 main-proj 包含子模块 sub-proj:

  • 同步主项目指定的子模块版本:

    1
    git submodule update
  • 更新子模块到远程最新版本,并提交主项目的版本修改:

    1
    2
    3
    4
    git submodule update --remote sub-proj  # 拉取sub-proj远程最新代码,切到最新哈希
    # 提交主项目的修改
    git add sub-proj
    git commit -m "update sub-proj to latest remote version"

附录:初始化带有 submodule 的仓库详细理解

  • 正常拉取外层项目

    1
    git pull origin master:master
    • 此时除了 ./.gitmodules 文件包含关于子模块的信息外,其他的文件都不包含,包括 ./.git/ 中
  • 初始化子模块

    1
    git submodule init
    • 将 .gitmodules 中的所有子模块注册到外层项目中
    • 注册方式:添加子模块信息(文件夹路径和子模块项目地址)到 .git/config 文件中并指明子模块对应的 active = true
    • 注:如果子模块之前存在于 .git/config 中 且 active = false,这个初始化操作会修改为 active = true
  • 初始化指定子模块(其他子模块可以不初始化,也不会影响,未初始化的子模块会是一个空文件夹)

    1
    git submodule init <path_to_sub_module_name>
    • 仅初始化 <path_to_sub_module_name> 这个模块
      • 测试发现:注意初始化时 <path_to_sub_module_name> 是 submodule 的文件夹路径
      • 可以是相对路径或绝对路径,执行这个命令时需要在 submodule 的外面
  • 更新子模块

    1
    2
    3
    4
    # 更新所有子模块('.git/config' 和 '.gitmodules' 中的所有子模块)
    git submodule update
    # 更新单个路径下对应的模块
    git submodule update <path_to_sub_module_name>
    • 具体含义:根据主仓库中记录的 子模块 commit ID ,从子模块的远程仓库拉取对应版本的代码,并存放到主仓库指定的子模块路径中
    • 这行代码执行下面的操作:
      • 如果还没有下载,则所有子模块的链接地址项目下载到 .git/modules/ 中
      • 将对应的 commit ID checkout 到 submodule 文件夹(工作目录)中
      • 常常用来在切换分支后同步子模块数据
    • <path_to_sub_module_name> 模块参数的使用方法同上
  • 特别注意:

    • git submodule init 后,.git/config 和 .gitmodules 应该是一致的
    • .git/config 和 .gitmodules 中都有,且在 .git/config 中 active = true 的 submodule 才能被 update 操作下载和 checkout
  • 特别说明:解耦初始化 deinit

    1
    2
    3
    4
    5
    # 将 <path_to_sub_module_name> 初始化,后续执行 git submodule update 等命令时自动更新 <path_to_sub_module_name> 这个 submodule 
    git submodule init <path_to_sub_module_name>

    # 将 <path_to_sub_module_name> 解耦初始化,后续执行 git submodule update 等命令时不会再自动更新 <path_to_sub_module_name> 这个 submodule
    git submodule deinit <path_to_sub_module_name>

更新子模块到自己的最新 commit

  • 更新逻辑:
    1
    2
    3
    4
    5
    6
    7
    # 进入子模块目录
    cd libs/sub-repo
    git pull origin master # 拉取子仓库最新代码
    cd ../.. # 回到主仓库
    git add libs/sub-repo # 提交子模块的新 commit ID
    git commit -m "update: 子模块 sub-repo 同步到最新版本"
    git push

删除子模块

  • 若需移除子模块,步骤稍多(Git 无直接 git submodule delete 命令):
    1
    2
    3
    4
    5
    6
    7
    8
    # 1. 解除子模块关联
    git submodule deinit -f libs/sub-repo
    # 2. 删除 .git 中的子模块缓存
    rm -rf .git/modules/libs/sub-repo
    # 3. 删除工作区的子模块目录,这一步后会看到 .gitmodules 中的相关 submodule 也被删除了
    git rm -f libs/sub-repo
    # 4. 提交删除操作
    git commit -m "remove: 移除子模块 sub-repo"

附录:关于 Git submodule 的理解

  • submodule 自己知道自己被当做 submodule

    • 一个项目被作为 submodule 后,他的 ./submodule_name/.git 将不再是一个文件夹,而是一个指明 .git/ 文件夹路径的配置文件

      1
      cat ./submodule_name/.git

      gitdir: ../.git/modules/submodule_name

    • .git/文件夹可以在./.git/modules/submodule_name/.git/中找到

  • submodule 相关信息都在外层项目中显示出来

  • 在 submodule 文件夹./submodule_name/下, submodule 的更新,提交等操作正常按照一般项目进行即可

    • 这里操作时虽然仓库在外层项目的./.git/modules/submodule_name/.git/中,但是在 submodule 的目录下我们可以正常访问 submodule 的仓库
    • 也就是说在 submodule 文件夹下的git操作(add, commit)实际上不修改当前文件夹下的任何文件,修改都在外层项目的./.git/modules/submodule_name/.git/仓库中
  • 外层项目只存储

    • submodule 文件夹
    • 在./.gitmodules中存储 submodule 相关信息(文件夹路径与 submodule 远程地址)
    • 在GitHub中,直接用网页打开项目可以看到 submodule 会被自动解析远程地址和最近提交的ID信息,点击 submodule 对应的文件夹链接即可跳转到 submodule 远程仓库地址中

递归 submodule

  • 递归时记住项目的库都在父项目的库中即可
    • 这句话等价于所有项目的库都在根项目的 .git/ 中

特别说明

  • 非必要不建议使用 submodule

附录:移除 submodule .git 但内容保留到主项目

  • 如果 Git 项目下面有个 submodule 也是包含 Git 的(可能是 git clone 命令下载的)的,往往不能正常的提交和管理项目,这是因为项目变成了 Git submodule 了
  • 现象:如果 submodule 是 git clone 别人的项目,我们将 submodule 提交到整个大项目中时
    • 会提示:modified:xxx(modified content, untracked content)
    • 此时如果直接提交,那么远程仓库里面 submodule 将是空的
  • 若不想再保留 submodule 的 git 仓库,则需要删除 submodule 相关的所有信息

第一步:需要先删除子模块

  • 移除子模块:
    1
    2
    3
    4
    # 1. 解除子模块关联
    git submodule deinit -f libs/sub-repo
    # 2. 删除 .git 中的子模块缓存
    rm -rf .git/modules/libs/sub-repo

第二步:重新添加文件路径(当做普通的文件)

  • 重新添加 submodule 文件夹
    1
    git add xxx

git submodule 是否跟踪分支的区别

  • 在 Git 中使用子模块(Submodule)时,“不跟踪分支(默认行为/锁定特定提交)” 和 “跟踪分支” 的核心区别在于父项目(Superproject)如何决定子模块应该停留在哪个版本 ,以及 更新子模块时的流程

不跟踪分支(默认行为 / 锁定特定 Commit)

  • 这是 Git 子模块最原始也是最常用的工作方式
  • 父项目只关心:“子模块必须是 a1b2c3d 这个提交”,不在乎这个提交属于哪个分支,也不在乎这个提交是不是最新的
  • 当克隆父项目并运行 git submodule update 时,Git 会进入子模块目录,强制将其 checkout 到父项目记录的那个 SHA-1 哈希值
    • 注:此时,子模块处于 Detached HEAD(游离指针)状态
  • 不跟踪分支时,如果要切换到某个分支的最新 commit,需要执行如下操作:
    • 1)进入子模块目录:cd submodule_dir
    • 2)手动拉取或切换:git checkout master && git pull
    • 3)回到父项目目录:cd ..
    • 4)提交变更:git add submodule_dir -> git commit
  • 建议使用这种方式, 团队所有成员拉取代码后,得到的子模块代码完全一致,不会因为子模块远程仓库更新了代码而导致父项目构建失败

跟踪分支

  • 可通过配置 .gitmodules 文件来实现的一种更动态的模式

  • 在 .gitmodules 中明确告诉 Git:“这个子模块应该跟随 main (或 dev) 分支”

    • 也可以通过命令行绑定,如添加 submodule 时,或之后直接修改:

      1
      2
      3
      4
      5
      6
      7
      8
      # # 新建 submodule:
      git submodule add -b submodule_master https://github.com/xxx/lib.git
      # 此时 .gitmodules 中会添加 branch = submodule_master

      # # 已有 submodule 绑定某个 分支:
      git config -f .gitmodules submodule.<本地存放路径>.branch <branch>
      # 比如将 dev 分支绑定到名为 libs/sub-repo 的 submodule 下:
      git config -f .gitmodules submodule.libs/sub-repo.branch dev
    • .gitmodules 文件中会多一行配置:

      1
      2
      3
      4
      [submodule "my-lib"]
      path = my-lib
      url = https://github.com/example/my-lib.git
      branch = main # 多出来的配置
  • 虽然父项目在数据库中依然存储的是 SHA-1 哈希值,但当你使用特定参数更新时(update 时添加 --remote),Git 会忽略本地记录的哈希值,直接去拉取远程分支的最新代码

  • 跟踪分支时,如果要切换到某个分支的最新 commit,只需要执行如下操作:

    • 不需要进入子模块目录,只需在父项目根目录运行:

      1
      git submodule update --remote
      • Git 会自动去子模块的远程仓库抓取 branch 字段指定分支的最新提交,并将子模块更新到该提交
      • 注意:如果不添加 --remote 则只是切换到当前 commit_id 而不会拉取最新分支(这与不绑定分支的 git submodule update 执行含义完全相同)
    • 注:运行完上述命令后,父项目的状态会显示子模块有变化(指向了新的 Hash),仍然需要 在父项目中执行 git add 和 git commit 来固化这个变更

  • 如果开发的项目依赖另一个正在快速迭代的库,且总是希望使用该库的最新版本,这种方式可以简化更新流程,但是要小心使用

一些实操及理解

  • 新增 submodule 时,默认(不跟踪):

    1
    2
    git submodule add https://github.com/xxx/lib.git
    # 此时 .gitmodules 中没有 branch 字段
  • 新增 submodule 时,跟踪分支:

    1
    2
    git submodule add -b main https://github.com/xxx/lib.git
    # 此时 .gitmodules 中会添加 branch = main
  • 更新 submodule 默认(不跟踪):

    • 如果运行 git submodule update,什么都不会发生 ,因为它只会把子模块恢复到父项目当前记录的旧 Hash 值
  • 更新 submodule (若 .gitmodules 中已经跟踪分支):

    • 如果运行 git submodule update,什么都不会发生 ,因为它只会把子模块恢复到父项目当前记录的旧 Hash 值,与 .gitmodules 中是否已经跟踪分支无关
    • 如果运行 git submodule update --remote,Git 会检测到配置了分支,于是去拉取远程最新代码,并更新本地子模块的指针
  • 特别说明:

    • “跟踪分支” 不意味着自动更新:即使配置了跟踪分支,当 git pull 父项目时,子模块不会自动更新到远程最新
      • 必须显式执行 git submodule update --remote
    • “跟踪分支” 后父项目仍然会记录 Hash 值,Git 的底层数据结构决定了父项目永远 只记录子模块的 Commit Hash
      • 所谓“跟踪分支”,只是提供了一个快捷命令(--remote)来帮你自动找到那个最新的 Hash 值并 checkout 过去,省去了手动进入子目录 pull 的过程
    • 如果是引用第三方稳定的开源库 ,或者要求构建环境绝对可复现,使用默认(不跟踪) 模式
    • 如果是迭代很快的开发,且父项目需要时刻集成子模块的最新开发成果,使用跟踪分支 模式

ML——xgboost包使用笔记

xgboost包中包含了XGBoost分类器,回归器等, 本文详细介绍XGBClassifier类


安装和导入

  • 安装

    1
    pip install xgboost
  • 导入

    1
    import xgboost as xgb
  • 使用

    1
    clf = xgb.XGBClassifier()

模型参数

普通参数

以下参数按照我理解的重要性排序

  • booster:
    • ‘gbtree’: 使用树模型作为基分类器
    • ‘gbliner’: 使用线性模型作为基分类器
    • 默认使用模型树模型即可,因为使用线性分类器时XGBoost相当于退化成含有L1和L2正则化的逻辑回归(分类问题中)或者线性回归(回归问题中)
  • n_estimators: 基分类器数量
    • 每个分类器都需要一轮训练,基分类器越多,训练所需要的时间越多
    • 经测试发现,开始时越大越能提升模型性能,但是增加到一定程度后模型变化不大,甚至出现过拟合
  • max_depth[default=3]: 每棵树的最大深度
    • 树越深,越容易过拟合
  • objective[default="binary:logistic"]: 目标(损失函数)函数,训练的目标是最小化损失函数
    • ‘binary:logistic’: 二分类回归, XGBClassifier默认是这个,因为XGBClassifier是分类器
    • ‘reg:linear’: 线性回归, XGBRegressor默认使用这个
    • ‘multi:softmax’: 多分类中的softmax
    • ‘multi:softprob’: 与softmax相同,但是每个类别返回的是当前类别的概率值而不是普通的softmax值
  • n_jobs: 线程数量
    • 以前使用的是nthread, 现在已经不使用了,直接使用n_jobs即可
    • 经测试发现并不是越多越快, 猜测原因可能是因为各个线程之间交互需要代价
  • reg_alpha: L1正则化系数
  • reg_lambda: L2正则化系数
  • subsample: 样本的下采样率
  • colsample: 构建每棵树时的样本特征下采样率
  • scale_pos_weight: 用于平衡正负样本不均衡问题, 有助于样本不平衡时训练的收敛
    • 具体调参实验还需测试[待更新]
    • 这个值可以作为计算损失时正样本的权重
  • learning_rate: shrinkage参数
    • 更新叶子结点权重时,乘以该系数,避免步长过大,减小学习率,增加学习次数
    • 在公式中叫做eta, 也就是 \(\eta\)
  • min_child_weight[default=1]: [待更新]
  • max_leaf_nodes: 最大叶子结点数目
    • 也是用于控制过拟合, 和max_depth的作用差不多
  • importance_type: 指明特征重要性评估方式, 只有在booster为’gbtree’时有效
    • ‘gain’: [默认], is the average gain of splits which use the feature
    • ‘cover’: is the average coverage of splits which use the feature
    • ‘weight’: is the number of times a feature appears in a tree
    • ‘total_gain’: 整体增益
    • ‘total_cover’: 整体覆盖率

常用函数

  • feature_importances_:

    • 返回特征的重要性列表
    • 特征重要性可以由不同方式评估
    • 特征重要性评估指标(importance_type)在创建时指定, 使用plot_importance函数的话,可以在使用函数时指定
  • plot_importance: 按照递减顺序给出每个特征的重要性排序图

    • 使用方式

      1
      2
      3
      4
      from xgboost import plot_importance
      from matplotlib import pyplot
      plot_importance(model)
      pyplot.show()
    • 详细定义

      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
      def plot_importance(booster, ax=None, height=0.2,
      xlim=None, ylim=None, title='Feature importance',
      xlabel='F score', ylabel='Features',
      importance_type='weight', max_num_features=None,
      grid=True, show_values=True, **kwargs):
      """Plot importance based on fitted trees.
      Parameters
      ----------
      booster : Booster, XGBModel or dict
      Booster or XGBModel instance, or dict taken by Booster.get_fscore()
      ax : matplotlib Axes, default None
      Target axes instance. If None, new figure and axes will be created.
      grid : bool, Turn the axes grids on or off. Default is True (On).
      importance_type : str, default "weight"
      How the importance is calculated: either "weight", "gain", or "cover"
      * "weight" is the number of times a feature appears in a tree
      * "gain" is the average gain of splits which use the feature
      * "cover" is the average coverage of splits which use the feature
      where coverage is defined as the number of samples affected by the split
      max_num_features : int, default None
      Maximum number of top features displayed on plot. If None, all features will be displayed.
      height : float, default 0.2
      Bar height, passed to ax.barh()
      xlim : tuple, default None
      Tuple passed to axes.xlim()
      ylim : tuple, default None
      Tuple passed to axes.ylim()
      title : str, default "Feature importance"
      Axes title. To disable, pass None.
      xlabel : str, default "F score"
      X axis title label. To disable, pass None.
      ylabel : str, default "Features"
      Y axis title label. To disable, pass None.
      show_values : bool, default True
      Show values on plot. To disable, pass False.
      kwargs :
      Other keywords passed to ax.barh()
      Returns
      -------
      ax : matplotlib Axes
      """

单调性保证

  • XGBoost自带单调性保证功能:
    • 参数使用示例是monotone_constraints="(1,0,-1,0,0)",表示输出结果随着
      • 第一个参数单调递增
      • 第三个参数单调递减
      • 其他参数不做约束
    • 这个参数的实现逻辑是:
      • monotone_constraints 参数通过在梯度提升树的分裂过程中加入额外的限制来实现单调性。具体来说,在选择最佳分裂点时,XGBoost 不仅考虑分裂的增益(如基尼不纯度减少或均方误差减少),还会检查分裂是否满足指定的单调性约束。如果一个潜在的分裂点违反了单调性约束,那么即使它能带来较大的增益,也不会被选作最佳分裂点。
    • 在现实场景中会出现修改单调特征值以后,模型预测结果为0的问题
      • 表现:实际使用时体现为输出值全是相同的(单调确没有意义)
      • 原因:此时主要原因是数据本身不具有单调性,特别是当label不随着单调特征单调时,容易出现学到的许多区间上模型值相同
      • 测试(对于单调递增场景,单调递减的正常):
        • 测试一:如果样本中存在太多不单调的数据,甚至希望单增,但数据单调递减,则会导致模型预估值随目标特征值变化,基本相同 * 测试二:如果样本中的目标特征和label是满足单调的,但是存在一些随机值,则在某些区间上容易出现单调值,特别是没有见过的区间上,预估值会是完全一致的
        • 注意:测试时,需要限定其他特征都不变,只有当前特征变化才可以,否则无法保证单调性
  • Demo示例:
    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
    import xgboost as xgb
    import numpy as np
    from sklearn.datasets import make_regression
    from sklearn.model_selection import train_test_split
    # 创建一个简单的回归数据集
    X, y = make_regression(n_samples=1000, n_features=5, noise=0.1)
    # 假设我们有5个特征,并且我们知道第一个特征应该与目标变量呈现正相关,
    # 第二个特征应该与目标变量呈现负相关,其余特征没有特定的单调性要求
    monotone_constraints = (1, -1, 0, 0, 0)
    # 划分数据集
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
    # 将数据转换为 DMatrix 格式,这是 XGBoost 所需的数据格式
    dtrain = xgb.DMatrix(X_train, label=y_train)
    dtest = xgb.DMatrix(X_test, label=y_test)
    # 定义 XGBoost 参数
    params = {
    'objective': 'reg:squarederror', # 对于回归任务
    'monotone_constraints': str(monotone_constraints) # 应用单调性约束
    }
    # 训练模型
    model = xgb.train(params, dtrain, num_boost_round=100)
    # 预测
    predictions = model.predict(dtest)
    # 打印部分预测结果
    print(predictions[:10])

Python——partial函数的使用


整体说明

  • 在 Python 里,functools 模块中的 partial 函数能够用来创建新函数,这些新函数是对现有函数部分参数预先赋值后的版本

  • 借助这种方式,能简化函数调用,让代码更为简洁,常用在一些较为专业的底层框架中

  • 函数形式:

    1
    2
    3
    from functools import partial

    new_func = partial(func, *args, **kwargs)
    • func 代表原函数
    • args 和 kwargs 分别是要预先设置的位置参数和关键字参数
    • 调用 new_func 时,只需传入剩余未预先设置的参数就行

固定函数参数

  • 假设存在一个加法函数 add(a, b),现在要创建一个专门用于加 10 的新函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    from functools import partial

    def add(a, b):
    return a + b

    add_ten = partial(add, 10) # 把 a 固定为 10

    print(add_ten(5)) # 输出 15(也就是 10 + 5)
    print(add_ten(10)) # 输出 20(也就是 10 + 10)
    print(add_ten(5, 20)) # 输出 25(此时 10 被 5 覆盖,函数计算的是 5 + 20)
    • 注意:固定的是第一个参数

处理关键字参数

  • 在处理关键字参数 Demo:

    1
    2
    3
    4
    5
    6
    7
    8
    def power(base, exponent):
    return base ** exponent

    square = partial(power, exponent=2) # 固定 exponent 为 2
    cube = partial(power, exponent=3) # 固定 exponent 为 3

    print(square(5)) # 输出 25(即 5^2)
    print(cube(5)) # 输出 125(即 5^3)
    • 注意:可通过关键字参数指明具体参数

需要注意的点

  • 参数顺序问题 :使用 partial 固定参数时,参数是按照位置依次绑定的

    • 比如 partial(func, 10) 会把 10 绑定到 func 的第一个参数上
  • 参数覆盖 :如果预先设置的参数在新函数调用时又被传入了新值,那么新传入的值会覆盖预先设置的值

    • 例如:
      1
      2
      3
      add_ten = partial(add, 10)
      print(add_ten(5)) # 输出 15
      print(add_ten(5, 20)) # 输出 25(此时 10 被 5 覆盖,函数计算的是 5 + 20)
  • 函数属性保留 :通过 partial 创建的新函数会保留原函数的一些属性

    • 像 __name__ 和 __doc__ 等函数属性还在

附录:与 Lambda 表达式的对比

使用 partial 和 lambda 都能实现参数固定的效果,但它们之间也有区别:

1
2
3
4
5
# 使用 partial
add_ten = partial(add, 10)

# 使用 lambda
add_ten_lambda = lambda x: add(10, x)
  • partial 函数在代码简洁性上表现更优,并且能够保留原函数的元信息
  • lambda 表达式则更为灵活,可以实现更复杂的逻辑

ML——损失函数总结

各种损失函数(Loss Function)总结,持续更新


名词概念

在机器学习和统计学中,成本函数(Cost Function)、经验风险(Empirical Risk)和损失函数(Loss Function)是三个密切相关但又有所区别的概念

  • 损失函数(Loss Function): 损失函数衡量的是单个训练样本的预测值与实际值之间的差异。它是模型预测误差的量化表示。常见的损失函数包括均方误差(Mean Squared Error, MSE)、交叉熵损失(Cross-Entropy Loss)等
  • 成本函数(Cost Function): 又名代价函数,等价于损失函数
  • 期望风险(Expected Risk):所有样本(训练样本+验证样本+测试样本+未知样本)的损失函数的期望,用于评估模型的泛化能力
  • 经验风险(Empirical Risk): 经验风险是在给定数据集上(一般是训练集),模型的平均损失。它是所有训练样本损失函数值的平均,用于评估模型在特定数据集上的表现。经验风险可以视为模型在有限数据集上的泛化能力的估计,本质是对期望风险的一种估计
  • 结构风险(Structural Risk):用于平衡模型对样本的拟合能力和复杂度
    $$
    结构风险 = 经验风险 + \alpha 正则化项
    $$
  • 注意:在一般的书籍或者博客论文中,尝尝也用 损失函数 或 成本函数 笼统的表达了 成本函数、经验风险、损失函数、成本函数、甚至结构风险 等所有相关概念 ,所以本文下面也会比较笼统称为损失函数

损失函数总体说明

  • 损失函数(Loss Function)又称为代价函数(Cost Function)
  • 损失函数用于评估预测值与真实值之间的不一致程度
  • 损失函数/成本函数是模型的优化目标函数,(神经网络训练的过程就是最小化损失函数的过程)
  • 损失函数/成本函数越小,说明预测值越接近于真实值,模型表现越好

各种损失函数介绍

平方损失函数

最常用的回归损失函数

  • 基本形式
    $$loss = (y - f(x))^{2}$$
  • 对应模型
    • 线性回归
      • 使用的均方误差来自于平方损失函数
        $$loss_{MSE} = \frac{1}{m}\sum_{i=1}^{m}(y_{i} - f(x_{i}))^2$$
    • 其他扩展,RMSE(Root Mean Squared Error,常用作指标而不是损失函数)
      $$loss_{RMSE} = \sqrt{\frac{1}{m}\sum_{i=1}^{m}(y_{i} - f(x_{i}))^2}$$

MSLE/RMSLE损失函数

  • MSLE,Mean Squared Logarithmic Error
    $$loss_{MSLE} = \frac{1}{m}\sum_{i=1}^{m} \left(\log(1+y_{i}) - \log(1+f(x_{i})) \right)^2$$
  • RMSLE,Root Mean Squared Logarithmic Error
  • 损失函数形式
    $$loss_{RMSLE} = \sqrt{\frac{1}{m}\sum_{i=1}^{m} \left(\log(1+y_{i}) - \log(1+f(x_{i})) \right)^2}$$
  • MSLE和RMSLE可缓解长尾变量导致的异常值问题

MAPE/MSPE/RMAPE/RMSPE损失函数

  • MAPE,Mean Absolute Percentage Error
    $$loss_{MAPE} = \frac{1}{m}\sum_{i=1}^{m} \left|\frac{y_i-f(x_i)}{y_i}\right|$$
  • MSPE,Mean Squared Percentage Error
    $$loss_{MSPE} = \frac{1}{m}\sum_{i=1}^{m} \left(\frac{y_i-f(x_i)}{y_i}\right)^2$$
  • RMAPE/RMSPE在MAPE/MSPE的基础上开根号即可
  • MAPE/MSPE/RMAPE/RMSPE都可缓解长尾变量导致的异常值问题

绝对值损失函数

最常用的回归损失函数

  • 基本形式
    $$loss = |y - f(x)|$$
  • 对应的经验风险:
    $$loss_{MAE} = \frac{1}{m}\sum_{i=1}^{m}|y_{i} - f(x_{i})|$$

交叉熵损失函数(对数似然损失函数)

最常见的损失函数交叉熵损失函数 ,又名对数似然损失函数

  • 基本形式(目标:在已知X时,样本标签Y出现的概率最大化,损失函数在概率前加个负号即可)
    $$loss=L(P_{\theta}(Y|X))=-logP_{\theta}(Y|X)$$
  • 二分类中的交叉熵损失函数:
    $$loss_{CE} = \frac{1}{N} \sum_{i}^{N}-y_ilogy_i’ - (1-y_i)log(1-y_i’)$$
    • 二分类中对于单个样本的损失一般写为:
      $$loss(x_i) = -y_ilogy_i’ - (1-y_i)log(1-y_i’)$$
    • 写成最容易看清楚的形式为:
      $$
      \begin{align}
      loss(x_i) &= -logy_i’, &\quad y_i = 1 \\
      loss(x_i) &= -log(1-y_i’), &\quad y_i = 0
      \end{align}
      $$
      • \(y_i\) 为样本 \(x_i\) 的真实类别
      • \(y_i’\) 为样本 \(x_i\) 在模型中的预测值(这个值在二分类中为Sigmoid函数归一化后的取值,代表样本分类为 \(y_i=1\) 的概率)
      • \(y_i’\) 也可表达为 \(p_{i,1}\),即样本 \(i\) 分类为1的概率
  • 多分类中的交叉熵损失函数:
    $$loss_{CE} = -\frac{1}{N} \sum_{i}^{N}\sum_{c=1}^{C} y_{i,c}\log p_{i,c}^{\theta}$$
    • \(y_{i,c} \in \{0, 1\}\):当样本 \(i\) 的真实分类是 \(c\) 时, \(y_{i,c}=1\),否则 \(y_{i,c}=0\)
    • \(p_{i,c}^{\theta}\):表示样本 \(i\) 为分类 \(c\) 的预估概率,由 \(y_{i,c} \in \{0, 1\}\) 可知损失函数不需要关注样本预测为其他错误类别的概率,仅关注真实样本对应类别的概率即可
    • 二分类的场景实际上是多分类的特定形式, \(1-y_i’\) 可以用来表示分类为0时的概率
  • 凡是极大似然估计作为学习策略的模型,损失函数都为交叉熵损失函数(对数似然损失函数)
    • 因为极大化似然函数等价于极小化对数似然损失函数,推导:
      $$
      \begin{align}
      \theta^{*} &= \mathop{\arg\max}_{\theta} \prod_i P_{\theta}(y_i|x_i) \\
      &= \mathop{\arg\max}_{\theta} \sum_i \log P_{\theta}(y_i|x_i) \\
      &= \mathop{\arg\min}_{\theta} - \sum_i \log P_{\theta}(y_i|x_i) \\
      &= \mathop{\arg\min}_{\theta} - \frac{1}{N} \sum_{i}^{N}\sum_{c=1}^{C} y_{i,c}\log p_{i,c}^{\theta}
      \end{align}
      $$
    • <<统计学习方法>>第十二章中LR使用的是极大似然估计但是对应的损失函数是逻辑斯蒂损失函数,这里可以证明LR中对数似然损失函数和逻辑斯蒂损失函数完全等价,证明见统计学习方法212页笔记
  • 对应模型
    • 所有使用极大似然估计的模型
      • 可以证明,极大似然估计法最大化样本出现概率的目标是最小化真实分布和预估分布的KL散度
        • 为了方便证明,下面把 \(P_{\theta}(y_i|x_i)\) 写作 \(P_{\theta}(x_i)\),这里 \(x_i\) 包含了样本的label( \(y_i\) )信息
          $$
          \begin{align}
          \theta^{*} &= \mathop{\arg\min}_{\theta} - \frac{1}{N} \sum_{i}^{N}\sum_{c=1}^{C} y_{i,c}\log p_{i,c}^{\theta} &\quad — 交叉熵损失函数\\
          &= \mathop{\arg\min}_{\theta} - \sum_i \log P_{\theta}(x_i) &\quad — 交叉熵损失函数\\
          &= \mathop{\arg\max}_{\theta} \sum_i \log P_{\theta}(x_i) \\
          &= \mathop{\arg\max}_{\theta} \prod_i P_{\theta}(x_i) &\quad — 极大似然法\\
          &= \mathop{\arg\max}_{\theta} \sum_i \log P_{\theta}(x_i) \\
          &\approx \mathop{\arg\max}_{\theta} \mathbb{E}_{x \sim P_{data}}\log P_{\theta}(x) \\
          &= \mathop{\arg\max}_{\theta} \int_{x} P_{data}(x) \log P_{\theta}(x) dx \\
          &= \mathop{\arg\max}_{\theta} \int_{x} P_{data}(x) \log P_{\theta}(x) dx - \int_{x} P_{data}(x) \log P_{data}(x) dx &\quad — 减去一项与\theta无关的项\\
          &= \mathop{\arg\max}_{\theta} \int_{x} P_{data}(x) \log \frac{P_{\theta}(x)}{P_{data}(x)} dx \\
          &= \mathop{\arg\max}_{\theta} -\int_{x} P_{data}(x) \log \frac{P_{data}(x)}{P_{\theta}(x)} dx \\
          &= \mathop{\arg\min}_{\theta} \int_{x} P_{data}(x) \log \frac{P_{data}(x)}{P_{\theta}(x)} dx \\
          &= \mathop{\arg\min}_{\theta} KL(P_{data}|| P_{\theta}) &\quad — KL散度\\
          \end{align}
          $$
        • 上式表明:似然函数最大化(极大似然法,对应交叉熵损失最小化),等价于最小化真实分布与预估分布的KL散度
        • 注:式中 \(x_i\) 样本表示<特征,标签>对, \(P_{\theta}(x_i)\) 表示在模型 \(\theta\) 下,真实<特征,标签>出现的概率
      • 同理,可以证明,最小化交叉熵损失函数的目标也是最小化真实分布和预估分布的KL散度
    • 最大化后验概率等价于最小化对数似然损失函数
      $$\theta^{\star} = \mathop{\arg\max}_{\theta} LL(\theta) = \mathop{\arg\min}_{\theta} -LL(\theta) = \mathop{\arg\min}_{\theta} -\log P(Y|X) = \mathop{\arg\min}_{\theta} -\sum_{i=1}^{N}\log p_{\theta}(y_{i}|x_{i})$$

指数损失函数

提升方法的损失函数

  • 基本形式
    $$loss=L(y,f(x))=e^{-yf(x)}$$
  • 对应模型
    • 提升方法

0-1损失函数

最理想的损失函数,但是不光滑,不可导

  • 基本形式
    $$loss=L(y,f(x))=0, \text{if} \ yf(x)>0 \\
    loss=L(y,f(x))=1, \text{if} \ yf(x)<0$$
  • 在由 \(f(x)\) 符号判断样本的类别的二分类问题中
    • 分类正确时总有 \(yf(x)>0\),损失为0
    • 分类错误时总有 \(yf(x)<0\),损失为1
  • 在特定的模型中,比如要求 \(f(x)=y\) 才算正确分类的模型中
    • 0-1损失函数可定义为如下
      $$loss=L(y,f(x))=0, \text{if} \ y=f(x) \\
      loss=L(y,f(x))=1, \text{if} \ y\neq f(x)$$

合页损失函数(Hinge 损失函数)

支持向量机的损失函数

  • 一般基本形式为
    $$loss = L(y,f(x))=[1-yf(x)]_{+}$$
    • \([z]_{+}\) 表示
      • \(z>0\) 时取 \(z\)
      • \(z\leq 0\) 时取0
  • 也可以写作(按单样本写并展开取正符号):
    $$L(y_i, f(x_i)) = \max(0, 1 - y_i \cdot f(x_i))$$
  • 损失函数分析:
    • 当 \( y_i \cdot f(x_i) \geq 1 \) 时,损失为 0,说明样本被正确分类且与边界的距离足够大(满足间隔要求),无需惩罚了
    • 当 \( y_i \cdot f(x_i) < 1 \) 时,损失为 \( 1 - y_i \cdot f(x_i) \),说明样本分类错误或距离边界太近,需要惩罚
  • 对应模型
    • 支持向量机

感知机的损失函数

感知机特有的损失函数<<统计学习方法>>

  • 基本形式
    $$loss=L(y,f(x))=[-yf(x)]_{+}$$
  • 与合页损失函数对比
    • 相当于函数图像整体左移一个单位长度
    • 合页损失函数比感知机的损失函数对学习的要求更高
    • 这使得感知机对分类正确的样本就无法进一步优化(分类正确的样本损失函数为0),学到的分类面只要能对样本正确分类即可(不是最优的,而且随机梯度下降时从不同点出发会有不同结果)
    • 而SVM则需要学到最优的才行,因为即使分类正确的样本,依然会有一个较小的损失,此时为了最小化损失函数,需要不断寻找,直到分类面为最优的分类面位置
  • 对应模型
    • 感知机

感知损失函数

与感知机没有任何关系

  • 基本形式
    $$L(y,f(x))=1, \text{if} \ \left | y-f(x)\right |>t \\
    L(y,f(x))=0, \text{if} \ \left | y-f(x)\right |< t$$
  • 这里”感知”的意思是在一定范围内认为 \(y\approx f(x)\),满足小范围差距时,损失函数为0

Focal Loss

本文原文为: ICCV 2017: Focal Loss for Dense Object Detection

  • 主要是为了解决正负样本严重失衡的问题
  • 是交叉熵损失函数的一种改进
  • 回归交叉熵损失函数的表达式为:
    $$
    \begin{align}
    loss(x_i) &= -logy_i’, &\quad y_i = 1 \\
    loss(x_i) &= -log(1-y_i’), &\quad y_i = 0
    \end{align}
    $$
  • Focal Loss的损失函数如下
    $$
    \begin{align}
    loss(x_i) &= -(1-y_i’)^\gamma logy_i’, &\quad y_i = 1 \\
    loss(x_i) &= -y_i^\gamma\log(1-y_i’), &\quad y_i = 0
    \end{align}
    $$
    • \(\gamma\) 的取值在原始论文中使用了 0, 0.5, 1, 2, 5 等
    • 当 \(\gamma > 0\) 时显然有
      • 预测值 \(y_i’\) 与真实标签 \(y_i\) 差异越大的样本,他们的损失权重越大( \(y_i=1\) 时,预测值和真实值的差异为 \(1-y_i’\), \(y_i=0\) 时,差异为 \(y_i’\) )
      • 预测值 \(y_i’\) 与真实标签 \(y_i\) 差异越小的样本,他们的损失权重越小
      • 以上两点给了模型重视分类错误样本的提示,模型会重视分类出错的样本
    • \(\gamma = 0\) 时Focal Loss退化为交叉熵损失函数
    • \(\gamma\) 越大,说明, 分类错误的样本占的损失比重越大
  • 实际使用中, 常加上 \(\alpha\) 平衡变量
    $$
    \begin{align}
    loss(x_i) &= -\alpha(1-y_i’)^\gamma logy_i’, &\quad y_i = 1 \\
    loss(x_i) &= -(1-\alpha)y_i^\gamma\log(1-y_i’), &\quad y_i = 0
    \end{align}
    $$
    • \(\alpha\) 用于平衡正负样本的重要性
    • \(\gamma\) 用于加强对难分类样本的重视程度
  • 假设正样本数量太少, 负样本数量太多, 那么该损失函数将降低负样本在训练中所占的权重, 可以理解为一种困难样本挖掘
    • 困难样本挖掘的思想就是找到分类错误的样本(难以分类的样本), 然后重点关注这些错误样本
  • 原论文中的实验结果:

Huber Loss

  • 总体来说:Huber损失函数是一种结合了均方误差(MSE)和平均绝对误差(MAE)优点的损失函数。它对小的残差表现得像MSE,对大的残差表现得更像MAE。这样可以减少异常值对模型的影响,同时保持梯度下降的有效性

  • Huber损失函数定义如下:
    $$ L_\delta(y, f(x)) = \begin{cases}
    \frac{1}{2}(y - f(x))^2 & \text{for } |y - f(x)| \leq \delta, \\
    \delta (|y - f(x)| - \frac{1}{2}\delta) & \text{otherwise}.
    \end{cases} $$

    • 其中 \(y\) 是目标变量, \(f(x)\) 是预测值,而 \(\delta\) 是一个超参数,用来控制从二次损失到线性损失转换的点
  • 在Python中实现Huber损失函数,可以使用如下的代码:

    1
    2
    3
    4
    5
    6
    7
    import numpy as np
    def huber_loss(y_true, y_pred, delta=1.0):
    residual = np.abs(y_true - y_pred)
    condition = residual <= delta
    small_res_loss = 0.5 * np.square(residual)
    large_res_loss = delta * (residual - 0.5 * delta)
    return np.where(condition, small_res_loss, large_res_loss)
  • 如果你使用的是深度学习框架比如TensorFlow或PyTorch,它们通常也内置了Huber损失函数,可以直接调用,例如在TensorFlow中:

    1
    2
    3
    import tensorflow as tf
    huber_loss_tf = tf.keras.losses.Huber(delta=1.0)
    loss_tf = huber_loss_tf(y_true_tf, y_pred_tf)
  • Huber Loss 也称为 Smooth_l1_loss

    • Smooth_l1_loss:它是一个“平滑”版本的L1损失,在误差较小时使用L2损失来提供平滑的梯度;相比于纯L1损失,smooth_l1_loss 在误差接近零时提供了更平滑的梯度变化,L1损失在误差为零时的梯度会发生突变(从-1变为1),而 smooth_l1_loss 使用平方误差部分避免了这种突变

岭回归

  • 基本思路是在最小二乘的基础上加上L2正则
    $$loss(\theta) = \frac{1}{m}\sum_{i=1}^{m}(y_{i} - f(x_{i}))^2 + \lambda \theta^2$$
  • 其中 \(\lambda \theta^2\) 项是L2正则项,也称为收缩惩罚项(shrinkage penalty)
    • 它试图缩小模型的参数,引入偏差来缓解参数估计中的方法问题,原始的最小二乘是无偏估计,但是引入了L2正则以后会变成有偏的,但是方差更小的参数估计
    • 实际上,shrinkage参数,也称为收缩参数 ,是统计学里面的一个概念,是用于缓解参数估计时离群点带来的问题

几个回归损失函数的对比

  • 参考链接补充:https://www.cnblogs.com/nxf-rabbit75/archive/2019/02/26/10440805.html

不同模型的损失函数

决策树的损失函数

  • 决策树有两个解释
    • if-then规则
    • 条件概率分布
  • <<统计学习方法>>: 决策树的损失函数是对数似然
    • 决策树可以看作是对不同概率空间的划分

Loss Function vs Cost Function

  • 损失函数(Loss Function)应用于一个特定的样本计算误差
  • 成本函数(Cost Function)是对所有样本而言的误差

损失函数相关思考

分类问题为什么不能用MSE?

  • 参考链接:
    • 为什么分类问题不使用MSE(平方损失函数)
    • 深究交叉熵损失(Cross-entropy)和平方损失(MSE)的区别
    • 为什么回归问题用MSE?
  • 原因:
    • 在sigmoid函数拟合概率的情况下,使用MSE会导致预估值接近0或者为1时的梯度都接近0,不利于模型学习收敛
    • 在sigmoid函数拟合概率的情况下,MSE是非凸优化问题,容易陷入局部最优;交叉熵损失则是凸优化问题,不会陷入局部最优
  • 最小化交叉熵损失函数等价于不做任何假设的极大似然估计 ,其本质是在最小化真实样本分布和预估分布的KL散度(这里的分布可以是已知X时,label的条件分布)
  • 最小化MSE损失函数等价于假设噪声服从高斯分布时的极大似然估计
    • 推导可参考:MSE的推导、MSE,MLE和高斯分布的关系、为什么回归问题用MSE?
    • 假设在回归问题中 \(y = z + \epsilon\),样本噪声 \(\epsilon\) 服从均值为0,方差为 \(\sigma\) 的高斯分布,即 \(\epsilon \sim N(0,\sigma)\),这里的 \(\sigma>0\)
    • 则有原始样本 \(y\) 服从均值为 \(z\) 方差为 \(\sigma\) 的高斯分布 \(y \sim N(z,\sigma)\),我们的目标是用模型拟合非噪声部分 \(y_{pred} = \theta^T x = z\),此时有 \(y = y_{pred} + \epsilon\),即 \(y\) 服从高斯分布 \(y \sim N(y_{pred},\sigma)\)
    • 正太分布的概率密度函数为 \(p(x) = \frac{1}{\sigma\sqrt{2\pi}} e^{-\frac{(x-u)^2}{2\sigma^2}}\),则此时单个样本出现的概率为 \(\frac{1}{\sigma\sqrt{2\pi}} e^{-\frac{(y-\theta^T x)^2}{2\sigma^2}}\)
    • 用极大似然法估计参数 \(\theta\),即最大化多个样本的联合概率为:
      $$
      \begin{align}
      \theta^* &= \mathop{\arg\max}_\theta \prod_i \frac{1}{\sigma\sqrt{2\pi}} e^{-\frac{(y^i-\theta^T x^i)^2}{2\sigma^2}} \\
      &= \mathop{\arg\max}_\theta \prod_i e^{-\frac{(y^i-\theta^T x^i)^2}{2\sigma^2}} &\quad —去掉与\theta无关的常数项 \\
      &= \mathop{\arg\max}_\theta \sum_i -\frac{(y^i-\theta^T x^i)^2}{2\sigma^2} &\quad —取对数 \\
      &= \mathop{\arg\max}_\theta \sum_i -(y^i-\theta^T x^i)^2 &\quad —继续去除常数项 \\
      &= \mathop{\arg\min}_\theta \sum_i (y^i-\theta^T x^i)^2 \\
      \end{align}
      $$
    • 即当假设噪声服从高斯分布时,用极大似然法估计参数与MSE损失函数得到的参数相同

MSE和RMSE对反向传播过程一样吗?

  • 答案是不一样
    • MSE的梯度是:
      $$ \nabla_{w} loss_{MSE} = \frac{1}{M}\sum_{i=1}^M 2(f_w(x_i)-y_i) \cdot \nabla_{w}f_w(x_i) $$
    • RMSE的梯度是:
      $$ \nabla_{w} loss_{RMSE} = \frac{1}{2} \left(\frac{1}{M}\sum_{i=1}^M (f_w(x_i)-y_i)^2\right)^{-\frac{1}{2}} \cdot \frac{1}{M}\sum_{i=1}^M 2(f_w(x_i)-y_i) \cdot \nabla_{w}f_w(x_i) $$
  • 由于两者的梯度不同,所以两者对参数的影响也不同,由于 \(\frac{1}{2} \left(\frac{1}{M}\sum_{i=1}^M (f_w(x_i)-y_i)^2\right)^{-\frac{1}{2}}\) 不是固定值,所以MSE和RMSE对梯度的影响也不是简单的固定倍数关系
    • 可以简单理解为:
      • RMSE的梯度相当于在MSE的基础上乘以 \(\frac{1}{2} \left(\frac{1}{M}\sum_{i=1}^M (f_w(x_i)-y_i)^2\right)^{-\frac{1}{2}}\),在不同的Batch中,该值不同,Loss越大,该值越大,Loss越小,该值越小
  • MSE是在假设误差服从均值为0的正太分布(即 \(\epsilon \sim N(0,\sigma)\) )的基础上基于极大似然法求得的目标函数
  • 在同一个Batch内来看,RMSE与MSE的目标实际上是完全一致的,即MSE最小时,RMSE也最小;但是从不同Batch之间来看,RMSE的梯度系数不同无法实现在所有训练集上MSE最小
    • 当所有数据只有一个Batch时,RMSE和MSE对梯度的影响是常数倍数关系(基于所有样本计算Loss得到的常数)
  • 为了实现多次采样Batch后实现MSE(满足极大似然法推导结果),一般常用MSE作为损失函数,而不是RMSE,RMSE更多是一个指标

ML——模型评估指标总结

各种模型评估指标总结,持续更新


分类模型

  • 参考:https://www.cnblogs.com/zongfa/p/9431807.html

Accuracy,准确率

  • 公式:$$ \text{Accuracy} = \frac{TP+TN}{TP+TN+FP+FN} $$
  • 直观,但不利于不平衡样本

Recall,召回率

  • 公式:$$ \text{Recall} = \frac{TP}{TP+FN} $$

Precision,精确率

  • 公式:$$ \text{Precision} = \frac{TP}{TP+FP} $$

F1 Score

  • 公式:$$ \text{F1 Score} = \frac{2 * \text{Precision} * \text{Recall}}{\text{Precision}+\text{Recall}} $$
  • 综合考虑模型”求精“和”求全“的能力
  • 关键词:F1-Score, F1 Score, F1 分数

AUC

  • 形式化定义:AUC是ROC曲线下方的面积,其中ROC曲线的横坐标是伪阳性率(也叫假正类率,False Positive Rate),纵坐标是真阳性率(真正类率,True Positive Rate)
  • 本质:任意取两个样本,一个正样本和一个负样本( \( \forall x^+, x^-\) ),模型预测正样本为正的概率分为 \(Score_\theta(y=1|x^+)\),模型预测正样本为正的概率分为 \(Score_\theta(y=1|x^-)\),则AUC为:
    $$ AUC_\theta = P(Score_\theta(y=1|x^+)>Score_\theta(y=1|x^-))$$
  • 真实实现时,可以统计所有正负样本对,若正样本预估值大于负样本,则累计分数+1,最后用累计分数除以所有可能的正负样本对数量
    $$ AUC_\theta = \frac{Count(S_+>S_-)}{Count(S_+) * Count(S_-)}$$
  • 具体代码实现:
    • 将样本按照预估分数倒序排列,从大到小
    • 定义四个变量:正样本总数 \(M\),负样本总数 \(N\),已访问负样本数量 \(X=0\),正样本大于负样本的样本对数量 \(Z=0\)
    • 依次访问所有样本:
      • 若为正样本,则 \(Z = Z + (N-X)\)
      • 若为负样本,则 \(X = X + 1\)
    • 最后计算: \(AUC = \frac{Z}{M * N}\)

回归模型

  • 参考:https://www.cnblogs.com/HuZihu/p/10300760.html
  • 参考:https://blog.csdn.net/guolindonggld/article/details/87856780

MSE

  • MSE (Mean Square Error,均方误差)
  • 也常用作损失函数
    $$\text{MSE} = \frac{1}{n} \sum_{i=1}^{n} (y_i - \hat{y}_i)^2$$
    • \(n\):样本数量
    • \(y_i\):第 \(i\) 个样本的真实值
    • \(\hat{y}_i\):第 \(i\) 个样本的预测值

RMSE

  • RMSE (Root Mean Square Error,根均方误差)
    $$\text{RMSE} = \sqrt{\frac{1}{n} \sum_{i=1}^{n} (y_i - \hat{y}_i)^2}$$

MAE

  • MAE (Mean Absolute Error,平均绝对误差)
    $$\text{MAE} = \frac{1}{n} \sum_{i=1}^{n} |y_i - \hat{y}_i|$$

MAPE

  • MAPE (Mean Absolute Percentage Error,平均绝对百分比误差)
    $$\text{MAPE} = \frac{1}{n} \sum_{i=1}^{n} \left| \frac{y_i - \hat{y}_i}{y_i} \right| \times 100%$$
    • 注:这个指标对分母要求很高,分母存在极小值时指标波动会非常大
      • 比如当 \(y_i=0\) 时,MAPE 无定义,这是其核心缺陷

SMAPE

  • SMAPE (Symmetric Mean Absolute Percentage Error,对称平均绝对百分比误差)
    $$\text{SMAPE} = \frac{1}{n} \sum_{i=1}^{n} \frac{|y_i - \hat{y}_i|}{\frac{1}{2}(|y_i| + |\hat{y}_i|)} \times 100%$$
    • 结果范围为 \([0, 200%]\)
  • SMAPE 也有简化版定义:分母直接用 \(|y_i| + |\hat{y}_i|\),此时公式为
    $$\text{SMAPE} = \frac{2}{n} \sum_{i=1}^{n} \frac{|y_i - \hat{y}_i|}{|y_i| + |\hat{y}_i|} \times 100%$$
    • 结果范围为 \([0, 100%]\)
  • 两种形式均常见,均被广泛使用,需根据场景选择

WMAPE

  • WMAPE (Weighted Mean Absolute Percentage Error,加权平均绝对百分比误差)
    $$\text{WMAPE} = \frac{\sum_{i=1}^{n} |y_i - \hat{y}_i|}{\sum_{i=1}^{n} |y_i|} \times 100%$$
  • WMAPE 也有按自定义权重 \(w_i\) 的通用形式:
    $$\text{WMAPE} = \frac{\sum_{i=1}^{n} w_i |y_i - \hat{y}_i|}{\sum_{i=1}^{n} w_i |y_i|} \times 100%$$
    • 默认权重 \(w_i=1\) 时退化为之前的公式
    • WMAPE 解决了 MAPE 中 \(y_i=0\) 无定义的问题
  • WMAPE 是 MAPE 的加权改进版,避免了分母为 0 的问题,更适合实际业务场景

排序模型

  • 参考:https://www.cnblogs.com/by-dream/p/9403984.html

DCG

  • DCG(Discounted Cumulative Gain,累计收益折扣)
  • 核心作用:衡量排序结果的质量,考虑位置对价值的衰减(越靠前的结果权重越高)
    $$\text{DCG}_p = \sum_{i=1}^{p} \frac{\text{rel}_i}{\log_2(i + 1)}$$
  • 等价形式(更常用,便于与 IDCG 对齐):
    $$\text{DCG}_p = \text{rel}_1 + \sum_{i=2}^{p} \frac{\text{rel}_i}{\log_2(i)}$$
    • \(p\): 排序结果的截断位置(如 Top-10 则 \(p=10\))
    • \(\text{rel}_i\): 第 \(i\) 个位置结果的相关性分数(通常为非负整数,如0=不相关、1=相关、2=高度相关)
    • \(\log_2(i+1)$/$\log_2(i)\): 位置折扣因子,体现“越靠前的结果越重要”的特性
  • 补充:DCG 还有另一种常见形式(引入指数缩放相关性):
    $$\text{DCG}_p = \sum_{i=1}^{p} \frac{2^{\text{rel}_i} - 1}{\log_2(i + 1)}$$
    • 该形式会放大高相关性结果的权重,适用于对“高度相关结果”更敏感的场景(如搜索排序)

NDCG

  • NDCG(Normalized Discounted Cumulative Gain,归一化累计收益折扣)
  • 核心作用:将 DCG 归一化到 [0,1] 区间,消除不同查询/任务间的结果尺度差异,便于跨场景比较
    $$\text{NDCG}_p = \frac{\text{DCG}_p}{\text{IDCG}_p}$$
    • \(\text{IDCG}_p\)(Ideal DCG,理想累计收益折扣):将所有结果按相关性从高到低完美排序后得到的 \(\text{DCG}_p\),即当前数据集下的最大可能 DCG 值,计算公式为:
      $$\text{IDCG}_p = \sum_{i=1}^{|REL_p|} \frac{\text{rel}^*_i}{\log_2(i + 1)} \quad (\text{或对应DCG的等价形式})$$
      • \(\text{rel}^*_i\) 为第 \(i\) 个位置的理想相关性分数,\(|REL_p|\) 为前 \(p\) 个位置中理想排序的结果数量
  • NDCG 的取值范围为 \([0,1]\): \(\text{NDCG}_p=1\) 表示排序结果完全理想,\(\text{NDCG}_p=0\) 表示排序结果完全无价值;
  • 当所有结果均无相关性(\(\text{rel}_i=0\))时,\(\text{DCG}_p=\text{IDCG}_p=0\),此时约定 \(\text{NDCG}_p=1\);
  • DCG/NDCG 是排序任务(如推荐系统、搜索引擎)的核心评估指标, \(p\) 的选择需贴合业务场景(如推荐系统常用 Top-5/Top-10,搜索常用 Top-20)

AP(Average Precision)与 MAP(Mean Average Precision)

  • AP 和 mAP 指标可参考:

校准模型

  • 参考:推荐系统(2)—— 评估指标
  • 参考:阿里妈妈展示广告预估校准技术演进之路

COPC

  • COPC(Click over Predicted Click)
    • COPC = 实际的点击率/模型预测的点击率
    • COPC 主要衡量model整体预估的偏高和偏低,同样越接近1越好,一般情况下在1附近波动
    • COPC 指标在展示广告上应用多一些

PCOC

  • PCOC(Predicted Click over Click)
    • PCOC = 模型预估的点击率/实际点击率,与COPC用途相同
    • COPC的倒数

PCOC指标是校准之后的点击率与后验点击率(近似真实概率)的比值,越接近于1,意味着在绝对值上越准确,大于1为高估,小于1为低估,是一种常用的高低估评价指标。但是PCOC存在一定局限性,举个例子:2万个样本,其中1万个样本的预估概率是0.2,后验概率是0.4,计算出PCOC是0.2/0.4=0.5,是显著低估的,另1万个样本PCOC是0.8/0.6= 1.3,明显是高估的。所以校准效果并不好,但是样本放一起看,校准后概率是(0.2+0.8)/2=0.5,后验概率是(0.4+0.6)/2=0.5,整体PCOC是1.0,表现完全正常。所以单一PCOC指标不能表征样本各维度下的校准水平。


净胜率模型

  • ANLP
    • 参考:论文《Scalable Bid Landscape Forecasting in Real-time Bidding》
    • 链接:https://arxiv.org/pdf/2001.06587.pdf

ML——直推式学习和归纳式学习

本文解释 直推式学习(Transductive Learning) 和 归纳式学习(Inductive Learning) 的区别


归纳式学习 (Inductive Learning)

  • 一句话定义 :通过训练数据学习一个通用的模型 ,然后将该模型应用于未知的测试数据
  • 目标 :从具体样本中归纳(名字来源)出一般规律,适用于任何未来的数据
  • 特点 :
    • 训练阶段和测试阶段是分开的
    • 模型在训练时不知道测试数据的具体情况
  • 典型场景 :大多数监督学习任务(如图像分类、文本分类等)
  • 核心特点 :训练时测试数据不可见

直推式学习 (Transductive Learning)

  • 一句话定义 :利用训练数据和特定的测试数据(未标注)共同学习,直接(名字来源)预测这些测试数据的标签
  • 目标 :针对当前已知的测试数据优化预测,而非构建通用模型
  • 特点 :
    • 训练时已知测试数据的特征(但无标签),利用这些信息优化预测
    • 模型仅适用于当前的测试数据,不能直接泛化到新数据
  • 典型场景 :半监督学习、图节点分类(如社交网络用户分类)
  • 核心特点 :训练时测试数据可见

附录:举例-垃圾邮件分类

  • 任务定义 :判断邮件是否为垃圾邮件

归纳式学习

  • 仅用已标注的训练数据(带“垃圾/正常”标签的邮件)训练一个模型(如SVM、神经网络)
  • 将模型应用于未来收到的任何新邮件

直推式学习

  • 同时利用已标注邮件和未标注邮件的特征分布(如词频、发件人)训练模型
  • 模型发现未标注邮件中某些特征与训练数据中的垃圾邮件相似,直接预测它们的标签
  • 特别说明 :模型针对这批特定的测试邮件优化 ,但不保证对新邮件的效果

为什么需要直推式学习?

  • 一般来说,大部分场景都使用归纳式学习即可
  • 当测试数据分布与训练数据分布不同时,直推式学习可以通过利用测试数据的特征分布提升当前任务的性能,但牺牲泛化性
  • 特别地,当训练数据和测试数据本身具有一定结构(如图数据、时空数据)时,直推式学习可以利用这些结构信息对无标签的测试数据做预测

Math——假设检验

本文介绍各种常见假设检验方法及使用示例

  • 参考链接1:知乎:T检验、F检验、卡方检验详细分析及应用场景总结
  • 参考链接2:知乎视频:5分钟带你了解卡方检验

卡方检验

《概率论与数理统计》

p-value的含义

  • 在假设检验中,对p-value的的一种直观理解:在假设 \(H_0\):假设目标样本属于某个正太分布,这个样本不是从这个正太分布中采样的概率就是p值(即p值的本质是一个概率)
    • 进一步的理解,已知一个正太分布和一个目标样本,那么这个目标样本对应的p值就是:重新在这个正太分布中重新采样一个新样本,新样本离中心位置 \(u_0\) 的距离大于等于目标样本的概率(该概率就是正太分布的两边区间积分和)
    • p值越小,说明这个目标样本越不可能是从这个正太分布采样出来的,越应该拒绝原假设 \(H_0\) (即越应该接受 \(H_A\)

Math——奇异值分解-SVD

本文从不同角度给出奇异值分解的物理意义

  • 参考知乎回答1:奇异值的物理意义是什么?
  • 参考知乎回答2:人们是如何想到奇异值分解的?

公式说明

$$A=U\Sigma V^{T}$$

  • \(U,V\) 都是正交矩阵, \(\Sigma\) 是对角矩阵,对角上的元素是矩阵 \(A\) 的奇异值
  • 若保留对角元素最大的K个值
    • \(K=r=Rank(A)\) 时为紧奇异值分解,对应的是无损压缩,此时由于奇异值保留数量与原始矩阵相同,能做到对原始矩阵A的完全还原
    • \(K< r=Rank(A)\) 时为截断奇异值分解,对应的是有损压缩,此时由于奇异值保留数量比原始矩阵的小,做不到对原始矩阵A的完全还原,但是如果K足够大就能做到对矩阵A的较完美近似

图像处理方面

  • 直观上可以理解为奇异值分解是将矩阵分解为若干个秩一矩阵之和,用公式表示就是:
    $$A=\sigma_{1}u_{1}v_{1}^{T}+\sigma_{2}u_{2}v_{2}^{T}+…+\sigma_{r}u_{r}v_{r}^{T}$$
    • 式子中每一项的系数 \(\sigma\) 就是奇异值
    • \(u,v\) 都是列向量,每一个 \(uv^{T}\) 都是秩为1的矩阵
    • 奇异值按照从小到大排列
  • 从公式中按照从大到小排序后,保留前面系数最大的项目后效果
    • 对于一张450x333的图片,只需要保留前面的50项即可得到相当清晰的图像
    • 从保留项1到50,图片越来越清晰
  • 结论:
    • 奇异值越大的项,越能体现出来图片的效果 ,奇异值隐含着某种对于A矩阵来说很重要的信息
    • 加权的秩一矩阵能体现整个大矩阵的值,奇异值就是对应秩一矩阵对于A矩阵的权重

线性变换方面

几何含义

  • 对于任何的一个矩阵,我们要找到一组两两正交单位向量序列,使得矩阵作用在此向量序列上后得到新的向量序列保持两两正交.奇异值的几何含义为:这组变换后的新的向量序列的长度

更直观的几何含义

  • 公式:
    $$\mathbb{E}_{m}={y\in C^{m}: y=Ax, x\in C^{n},\left | x\right |_{2}=1}$$
  • 二维矩阵A:
    • 矩阵A将二维平面中的单位圆变换为椭圆,而两个奇异值正好是椭圆的半轴长度.
  • m维矩阵
    • 矩阵A将高维平面中的单位球变换为超椭球,矩阵的奇异值恰好就是超椭球的每条半轴长度.

奇异值分解的降维理解

代码编写

  • 代码

    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
    ##encoding=utf-8
    import numpy as np

    A = np.array([[4, 5, 6], [4, 5, 6], [8, 10, 12], [4, 5, 6]])

    print("原始矩阵:")
    print(A)

    U, Sigma, Vt = np.linalg.svd(A)
    U = np.array(U)
    Sigma = np.array(Sigma)
    Vt = np.array(Vt)

    print("分解后的原始U,S,V:")
    print(U)
    Sigma_ = np.zeros((4, 3), dtype=np.float64)
    Sigma_[:3][:3] = np.diag(Sigma)
    print Sigma_
    print(Vt)

    # 不做任何处理,直接恢复原始矩阵
    A_ = np.dot(np.dot(U, Sigma_), Vt)
    print(A_)

    # 原始矩阵的秩为1,所以可以拆解到只剩下一个奇异值,压缩到一维,也能完整恢复原始矩阵,实现将4x3的矩阵变成两个向量+一个数字,当矩阵维度变大时,这里的压缩会更加明显
    U = U[:, :1]
    Sigma_ = Sigma_[:1, :1]
    Vt = Vt[:1, :]

    print("降维后的U,S,V:")
    print(U)
    print(Sigma_)
    print(Vt)
    A_ = np.dot(np.dot(U, Sigma_), Vt)
    print("降维后的恢复矩阵,与原矩阵相同:")
    print(A_)
  • 结果

    原始矩阵:
    [[ 4 5 6]
    [ 4 5 6]
    [ 8 10 12]
    [ 4 5 6]]
    分解后的原始U,S,V:
    [[-3.77964473e-01 -9.25820100e-01 1.52792960e-16 0.00000000e+00]
    [-3.77964473e-01 1.54303350e-01 9.12870929e-01 0.00000000e+00]
    [-7.55928946e-01 3.08606700e-01 -3.65148372e-01 -4.47213595e-01]
    [-3.77964473e-01 1.54303350e-01 -1.82574186e-01 8.94427191e-01]]
    [[2.32163735e+01 0.00000000e+00 0.00000000e+00]
    [0.00000000e+00 1.22295087e-15 0.00000000e+00]
    [0.00000000e+00 0.00000000e+00 5.18334466e-32]
    [0.00000000e+00 0.00000000e+00 0.00000000e+00]]
    [[-0.45584231 -0.56980288 -0.68376346]
    [ 0.02454097 0.75988299 -0.64959647]
    [-0.88972217 0.31289378 0.3324033 ]]
    [[ 4. 5. 6.]
    [ 4. 5. 6.]
    [ 8. 10. 12.]
    [ 4. 5. 6.]]
    降维后的U,S,V:
    [[-0.37796447]
    [-0.37796447]
    [-0.75592895]
    [-0.37796447]]
    [[23.21637353]]
    [[-0.45584231 -0.56980288 -0.68376346]]
    降维后的恢复矩阵,与原矩阵相同:
    [[ 4. 5. 6.]
    [ 4. 5. 6.]
    [ 8. 10. 12.]
    [ 4. 5. 6.]]

Math——样本方差和总体方差的关系

本文介绍随机变量样本均值方差和整体均值方差的关系,同时还介绍样本和均值的方差和总体方差的关系

  • 其他参考链接:在统计学里如何理解样本均值的方差等于总体方差➗n? - 蘇雲的回答 - 知乎

样本方差与总体方差的关系

  • 其他证明方式:
    • 上式中,如果总体方差未知 ,想要用样本方差来作为总体方差的无偏估计 ,则样本方差的定义是应该是
      $$ S^2 = \frac{1}{n-1}\sum_i^n(X_i-\bar{X})^2$$
      • 注:分母必须是 \(n-1\) 时,样本方差才是总体方差的无偏估计

样本方差为什么要除以n-1?

  • 样本方差的定义:
    $$
    \sigma^2 \approx S^2 = \frac{1}{n-1}\sum_i^n(X_i-\bar{X})^2
    $$
  • 为什么样本方差是乘以 \(\frac{1}{n-1}\) 而不是 \(\frac{1}{n}\) ?
    • 因为这样使用 \(\frac{1}{n}\) 会低估总体方差,此时样本方差不是总体方差的无偏估计
    • 样本方差低估了总体方差的原因是因为从总体里面抽出来的数据会更倾向于集中,极端情况下,一个样本对应的方差为0
  • 换个视角想,是因为我们不知道总体的均值,所以计算方差时使用的均值也是从样本中求平均得到的,这使得我们基于该均值得到的方差不够离散(因为使用了样本均值,所以自由度需要减一),也就是低估了总体方差
    • 怎么理解自由度?
      • 采样一个样本以后无法计算方差,此时方差为0,因为此时均值就等于样本本身,此时自由度为0
      • 采样两个样本以后得到的方差只根第二个样本到第一个样本的距离有关(样本均值与这两个样本强相关),此时自由度为1
      • 当采样的样本数非常多(假设为n)时,实际上单个样本与均值的关系很小了,此时自由度为n-1
  • 当已知总体均值 \(\mu\) 时(个人理解:这里的 \(\mu\) 可以是其他采样方式下获得的近似均值,只要不跟当前用于计算方差的样本相关即可),样本方差可以使用:
    $$
    \sigma^2 \approx S^2 = \frac{1}{n-1}\sum_i^n(X_i-\mu)^2
    $$
  • 样本方差经过 \(\frac{1}{n-1}\) 修正以后可以用来估计总体方差(修正以后是总体方差的无偏估计)
    • 这个修正叫做贝塞尔修正
  • 证明 from :Bilibili-样本方差为什么除以n-1?

  • 单样本均值和多样本均值的关系

    问题定义

    • 考虑以下采样方式
      • 集合X :每次采样 B个样本 得到的样本集合
      • 集合Y :每次采样 1个样本 得到的样本集合
    • 问:多次采样时,集合X和B的均值和方差关系是什么?
      • 即:如果我们重复多次进行这样的采样(每次采B个或1个),那么:这两种采样方式得到的样本均值的期望(即平均值的平均值)和样本均值的方差(即平均值的波动程度)之间有什么关系?

    假设与符号定义

    • 假设我们从一个总体中采样,总体的均值为 \(\mu\),方差为 \(\sigma^2\)
    • 集合X :
      • 每次采样 B个样本 :\(X_1, X_2, \dots, X_B\)
      • 计算这B个样本的均值:\(\bar{X}_A = \frac{1}{B}\sum_{i=1}^B X_i\)
    • 集合Y :
      • 每次采样 1个样本 :\(Y_1\)
      • 其“均值”就是它自己:\(\bar{X}_B = Y_1\)
    • 我们重复多次这样的采样过程,得到一系列的 \(\bar{X}_A\) 和 \(\bar{X}_B\)

    均值关系

    • 集合X 的样本均值的期望:
      $$
      E[\bar{X}_A] = E\left[\frac{1}{B}\sum_{i=1}^B X_i\right] = \frac{1}{B} \sum_{i=1}^B E[X_i] = \frac{1}{B} \cdot B \mu = \mu
      $$
    • 集合Y 的样本均值的期望:
      $$
      E[\bar{X}_B] = E[Y_1] = \mu
      $$
    • 结论:两种采样方式得到的样本均值的期望是相同的 ,都等于总体均值 \(\mu\)

    方差关系

    • 集合X 的样本均值的方差:
      $$
      \text{Var}(\bar{X}_A) = \text{Var}\left(\frac{1}{B}\sum_{i=1}^B X_i\right) = \frac{1}{B^2} \sum_{i=1}^B \text{Var}(X_i) = \frac{1}{B^2} \cdot B \sigma^2 = \frac{\sigma^2}{B}
      $$
    • 集合Y 的样本均值的方差:
      $$
      \text{Var}(\bar{X}_B) = \text{Var}(Y_1) = \sigma^2
      $$
    • 结论:集合X 的样本均值的方差是 \(\frac{\sigma^2}{B}\),集合Y 的样本均值的方差是 \(\sigma^2\)
      • 也就是说,集合X 的样本均值的方差更小,是集合Y 的 \(\frac{1}{B}\)
    • 直观理解
      • 均值方面:无论你是采1个还是采B个,平均来看,它们的中心位置(期望)都是一样的,都是总体的均值
      • 方差方面:采B个样本求平均,相当于把单个样本的“噪声”给“平均掉”了,因此波动更小,方差更小;而采1个样本,没有“平均”的过程,波动就更大,方差也就更大

    附录:Python 中计算标准差/方差的默认实现

    • 在进行数据归一化(如 Z-score 标准化)时,计算标准差的分母是 \(n\)(总体标准差)还是 \(n-1\)(样本标准差,即无偏估计),在不同的 Python 科学计算库中默认行为是不同的
    • 各 Python 库的默认行为总结:
      • 1)NumPy (np.std) :默认分母为 \(n\)(即 ddof=0)
        • 注:这里控制分母的参数通常称为 ddof(Delta Degrees of Freedom,自由度偏差),分母的计算公式为 \(N - ddof\)
      • 2)Pandas (pd.Series.std / pd.DataFrame.std) :默认分母为 \(n-1\)(即 ddof=1)
      • 3)PyTorch (torch.std) :默认分母为 \(n-1\)(即 unbiased=True 或 correction=1)
        • 注:修改 unbiased=False 即可得到分母为 \(n\) 的结果
      • 注:各个库的默认行为不同可能会导致一些意想不到的问题
    • 在机器学习中进行 Z-score 归一化(\(\frac{x - \mu}{\sigma}\))时,通常数据量 \(n\) 都比较大,此时 \(n\) 和 \(n-1\) 的差别微乎其微
      • 但为了保证训练集和测试集处理逻辑的绝对一致性,建议在代码中显式指定参数(例如统一写死 ddof=0 或 unbiased=False),避免因为在不同库之间切换数据格式(如从 Pandas 转换到 PyTorch Tensor)而引入隐蔽的计算差异

    测试代码

    • 以下是包含 NumPy、Pandas 和 PyTorch 计算 \(n\) 和 \(n-1\) 标准差的完整测试代码
      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
      57
      58
      59
      60
      61
      62
      63
      64
      import numpy as np
      import pandas as pd
      import torch

      def test_std_behaviors():
      # 准备相同的测试数据
      data_list = [1.0, 2.0, 3.0, 4.0, 5.0]

      print("-" * 40)
      print("NumPy 标准差测试")
      print("-" * 40)
      arr = np.array(data_list)
      # NumPy 默认 ddof=0,分母为 n
      np_std_n = np.std(arr)
      # NumPy 设置 ddof=1,分母为 n-1
      np_std_n_minus_1 = np.std(arr, ddof=1)
      print(f"NumPy 默认分母 (n) : {np_std_n:.4f}")
      print(f"NumPy 指定分母 (n-1) : {np_std_n_minus_1:.4f} (使用 ddof=1)")

      print("\n" + "-" * 40)
      print("Pandas 标准差测试")
      print("-" * 40)
      series = pd.Series(data_list)
      # Pandas 默认 ddof=1,分母为 n-1
      pd_std_n_minus_1 = series.std()
      # Pandas 设置 ddof=0,分母为 n
      pd_std_n = series.std(ddof=0)
      print(f"Pandas 默认分母 (n-1): {pd_std_n_minus_1:.4f}")
      print(f"Pandas 指定分母 (n) : {pd_std_n:.4f} (使用 ddof=0)")

      print("\n" + "-" * 40)
      print("PyTorch 标准差测试")
      print("-" * 40)
      tensor = torch.tensor(data_list)
      # PyTorch 默认无偏估计 (unbiased=True),分母为 n-1
      # 注:在较新版本的 PyTorch 中,也可以使用 correction=1
      pt_std_n_minus_1 = torch.std(tensor)
      # PyTorch 设置有偏估计 (unbiased=False),分母为 n
      # 注:在较新版本的 PyTorch 中,也可以使用 correction=0
      pt_std_n = torch.std(tensor, unbiased=False)
      print(f"PyTorch 默认分母 (n-1): {pt_std_n_minus_1:.4f}")
      print(f"PyTorch 指定分母 (n) : {pt_std_n:.4f} (使用 unbiased=False)")

      if __name__ == "__main__":
      test_std_behaviors()


      # ----------------------------------------
      # NumPy 标准差测试
      # ----------------------------------------
      # NumPy 默认分母 (n) : 1.4142
      # NumPy 指定分母 (n-1) : 1.5811 (使用 ddof=1)
      #
      # ----------------------------------------
      # Pandas 标准差测试
      # ----------------------------------------
      # Pandas 默认分母 (n-1): 1.5811
      # Pandas 指定分母 (n) : 1.4142 (使用 ddof=0)
      #
      # ----------------------------------------
      # PyTorch 标准差测试
      # ----------------------------------------
      # PyTorch 默认分母 (n-1): 1.5811
      # PyTorch 指定分母 (n) : 1.4142 (使用 unbiased=False)

    Math——概率密度函数的理解

    本文介绍概率密度函数的理解


    概率密度函数的定义

    • 概率密度函数(Probability Density Function, PDF)是概率论和统计学中用来描述连续型随机变量的概率分布的一种函数。对于一个连续型随机变量 \(X\),其概率密度函数 \(f(x)\) 具有以下性质:

      • 非负性 :对于所有的实数 \(x\),有 \(f(x) \geq 0\)
      • 归一化 :在整个可能值域内, \(f(x)\) 的积分等于1,即 \(\int_{-\infty}^{\infty} f(x) , dx = 1\)
      • 概率计算 :如果 \(a < b\),那么随机变量 \(X\) 落在区间 \([a, b]\) 内的概率可以通过 \(f(x)\) 在该区间上的积分来计算,即 \(P(a \leq X \leq b) = \int_{a}^{b} f(x) , dx\)
    • 需要注意的是,概率密度函数 \(f(x)\) 在某一点的值并不直接表示该点的概率,因为对于连续型随机变量来说,取任何一个具体值的概率实际上是0。相反, \(f(x)\) 更多地用于描述随机变量落在某个区间的概率大小。通过观察 \(f(x)\) 的形状,可以了解随机变量取值的集中趋势和分散程度等特征


    概率密度函数在某点的值 \( f(x_0) = \frac{1}{2} \) 的意义

    • 当我们说概率密度函数 \( f(x) \) 在某点 \( x_0 \) 的值等于 \( \frac{1}{2} \),即 \( f(x_0) = \frac{1}{2} \),其意义如下:

    概率密度函数的值

    • 概率密度函数 \( f(x) \) 在某点 \( x_0 \) 的值 \( f(x_0) \) 并不直接表示在 \( x_0 \) 点取得某个具体值的概率。对于连续型随机变量,在某个具体点取值的概率实际上是零,即:
      $$ P(X = x_0) = 0 $$

    概率密度的意义

    • 概率密度函数 \( f(x) \) 在点 \( x_0 \) 的值 \( f(x_0) \) 表示在 \( x_0 \) 附近的值的相对可能性。更具体地说,它表示在 \( x_0 \) 附近的一个小区间的长度和该区间内的概率的比值。比如,对于一个非常小的区间 \( [x_0 - \epsilon, x_0 + \epsilon] \),其概率可以近似表示为:
      $$ P(x_0 - \epsilon \leq X \leq x_0 + \epsilon) \approx f(x_0) \cdot 2\epsilon $$

    例子

    • 假设一个随机变量 \( X \) 的概率密度函数在某点 \( x_0 \) 处为 \( \frac{1}{2} \),即 \( f(x_0) = \frac{1}{2} \) 。这意味着在 \( x_0 \) 附近的一个小区间内的概率可以近似计算。例如,对于一个非常小的区间 \( [x_0 - 0.01, x_0 + 0.01] \),其概率可以近似为:
      $$ P(x_0 - 0.01 \leq X \leq x_0 + 0.01) \approx \frac{1}{2} \cdot 0.02 = 0.01 $$

    总结

    • 概率密度函数 \( f(x) \) 在某点 \( x_0 \) 的值 \( f(x_0) = \frac{1}{2} \) 并不表示在 \( x_0 \) 处取值的概率,而是表示在 \( x_0 \) 附近的值的相对可能性
    • 对于连续型随机变量,在某个具体点取值的概率是零
    • 概率密度函数的值可以用于近似计算在某个小区间内的概率
    1…495051…64
    Joe Zhou

    Joe Zhou

    Stay Hungry. Stay Foolish.

    631 posts
    53 tags
    GitHub E-Mail
    © 2026 Joe Zhou
    Powered by Hexo
    |
    Theme — NexT.Gemini v5.1.4