Jiahong 的个人博客

凡事预则立,不预则废


  • Home

  • Tags

  • Archives

  • Navigation

  • Search

Pandas——apply、applymap和map函数

Pandas库中apply, applymap和map函数的使用


所属类

  • apply : 属于DataFrame类和Series类
  • applymap : 属于DataFrame类
  • map : 属于Series类

作用

  • map 和 applymap都是对每个元素分别操作的:
  • apply
    • 在DataFrame中是对列或者行操作,每一列或者行都是一个Series(列: axis=0[默认值],行: axis=1)
    • 在Series中是对每个元素进行操作(其实换个角度理解为对Series的每一列操作也行,此时的每一列就是一个元素,值得注意的是此时的每个元素是数值类型而不是Series类型,所以不能对其调用sum等函数)

使用方法

1
2
3
4
5
6
7
8
9
# a simple example for apply(), applymap() and map()
func_series = lambda x: x.sum()
func_element = lambda x: "%.2f" % x

df.apply(func_series)
df.applymap(func_element)

ser.apply(func_element)
ser.map(func_element)

概率——共轭分布

待更新,共轭分布
参考:LDA数学八卦


扩展——关于共轭分布

  • 高斯分布和高斯分布
    • Gaussion-Gaussion共轭
  • Beta分布和二项分布
    • Beta-Binomial共轭
  • Dirichlet分布和多项分布
    • Dirichlet-Multinomial共轭

因果推断——PSM挑选对照组


问题描述

  • 共 100 个实验商家,需要从 2000+ 个商家中使用PSM匹配找到相似商家作为 AB 实验

PSM 基本流程

  • 共三个集合:实验组、对照组(初始化为空)、候选商家集合
  • 特征构造:选择可能影响实验结论的关键特征
  • label 构建:标记 100 个实验商家 label 为 1,其他商家 label 为 0
  • 模型训练:用构建的样本训练一个分类模型,一般为 LR 模型即可
  • PSM 匹配:迭代访问实验商家 A,分别进行如下操作
    • 在候选商家集合中选择模型预估值和 A 差异小于一定阈值的商家作为匹配候选集,从匹配候选集中随机选择一个商家 B
    • 将商家 B 加入对照组,并在候选商家集合中删除对照组

流程思考

  • 如果找不到指定阈值的商家作为匹配候选集

Python 实现 Demo

  • PSM 匹配 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
    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
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    import pandas as pd
    import numpy as np
    from sklearn.linear_model import LogisticRegression
    from sklearn.preprocessing import StandardScaler

    # 生成模拟数据(假设有2000个商家)
    np.random.seed(42)
    n = 2000

    # 生成商家特征
    data = pd.DataFrame({
    "shop_id": range(n),
    "monthly_sales": np.random.normal(50000, 15000, n), # 月销售额
    "store_size": np.random.choice([50, 100, 150, 200], n), # 店铺面积
    "is_chain": np.random.binomial(1, 0.3, n), # 是否连锁店
    "city_tier": np.random.choice([1, 2, 3], p=[0.2,0.5,0.3], n), # 城市等级
    "category": np.random.choice(["餐饮", "零售", "服务", "其他"], p=[0.4,0.3,0.2,0.1], n) # 类别
    })

    # 添加实验组标记(随机选择100家作为实验组)
    experiment_group = np.random.choice(n, 100, replace=False)
    data['is_treated'] = data.shop_id.isin(experiment_group).astype(int)

    # 特征预处理
    features = data[['monthly_sales', 'store_size', 'is_chain', 'city_tier']]
    scaler = StandardScaler()
    features_scaled = scaler.fit_transform(features)

    # 训练倾向得分模型
    model = LogisticRegression(max_iter=1000)
    model.fit(features_scaled, data['is_treated'])

    # 计算倾向得分
    data['propensity_score'] = model.predict_proba(features_scaled)[:, 1]

    # PSM匹配函数
    def psm_match(data, treated_col='is_treated', score_col='propensity_score', caliper=0.02):
    treated = data[data[treated_col] == 1]
    control = data[data[treated_col] == 0]

    matches = []
    for _, treat in treated.iterrows():
    # 寻找最近邻匹配
    control['distance'] = abs(control[score_col] - treat[score_col])
    candidates = control[control.distance <= caliper]

    if not candidates.empty:
    # 随机选择最近邻
    match = candidates.sample(1, random_state=42)
    matches.append(match.index[0])
    control = control.drop(match.index)

    return treated, data.loc[matches]

    # 执行匹配
    treated_group, control_group = psm_match(data)

    # 评估匹配质量
    print(f"匹配成功实验商家数:{len(treated_group)}")
    print(f"找到对照组商家数:{len(control_group)}")

    # 检查协变量平衡性
    matched_data = pd.concat([treated_group, control_group])
    for col in ['monthly_sales', 'store_size', 'is_chain']:
    treat_mean = treated_group[col].mean()
    control_mean = control_group[col].mean()
    std_diff = (treat_mean - control_mean) / treated_group[col].std()
    print(f"{col}标准化差异:{std_diff:.3f}")

    # 输出匹配结果
    matched_pairs = pd.DataFrame({
    'experiment_id': treated_group.shop_id.values,
    'control_id': control_group.shop_id.values
    })
  • 代码说明:

    • 数据模拟:生成包含销售额、店铺面积、是否连锁等特征的2000个商家数据
    • 实验组选择:随机选择100个商家作为实验组(实际业务中应根据业务逻辑选择)
    • 特征标准化:对连续变量进行标准化处理
    • 倾向得分模型:使用逻辑回归预测进入实验组的概率
    • 卡钳匹配:设置最大允许得分差异(caliper=0.02),确保匹配质量
    • 平衡性检查:通过标准化差异评估匹配质量(<0.1为良好)

ML——LdaModel在gensim的使用

LDA在Python库gensim中的模型和参数介绍


API

1
2
3
4
5
6
7
class LdaModel(interfaces.TransformationABC, basemodel.BaseTopicModel):
def __init__(self, corpus=None, num_topics=100, id2word=None,
distributed=False, chunksize=2000, passes=1, update_every=1,
alpha='symmetric', eta=None, decay=0.5, offset=1.0, eval_every=10,
iterations=50, gamma_threshold=0.001, minimum_probability=0.01,
random_state=None, ns_conf=None, minimum_phi_value=0.01,
per_word_topics=False, callbacks=None, dtype=np.float32)
1
2
3
# a simple example
import gensim
gensim.models.ldamodel.LdaModel(corpus, num_topics=2, id2word=dictionary, passes=20)

Parameters

  • 主要参数:

    • corpus: 语料库,类似于

      [ [(1, 1),(4, 1)], [(2, 1),(3, 2)] ]

      • gensim库中一般默认corpus参数是经过字典编码统计的,类似于上面的形式,而texts是文本的列表的形式
    • num_topics: 主题数量,超参数

    • id2word: dict of (int, str), :class:gensim.corpora.dictionary.Dictionary

      • 用于将corpus中的数字与词进行对应,这里应该为把texts转成corpus的那个字典
    • passes: 训练时的迭代次数

    • iterations: 推断时的迭代次数

    • alpha: 主题的先验概率

      • 一个num_topics大小的数组表明每个主题的概率
      • 也可以是str类型的值
        • “asymmetric”: 固定初始化为1.0/num_topics
    • decay: (0.5, 1]之间的浮点数,前一个lambda值被遗忘的百分比?【待确认参数】

  • 其他参数:

    • distributed: 是否使用分布式计算

相关类介绍

  • gensim.corpora.dictionary.Dictionary
    1
    2
    class Dictionary(utils.SaveLoad, Mapping):
    def__init__(self, documents=None, prune_at=2000000)
1
2
3
4
5
6
7
# a simple example
from gensim.corpora import Dictionary
texts = [['human', 'interface', 'computer']]
dct = Dictionary(texts) # initialize a Dictionary
dct.add_documents([["cat", "say", "meow"], ["dog"]]) # add more document (extend the vocabulary)
dct.doc2bow(["dog", "computer", "non_existent_word"])
# output: [(0, 1), (6, 1)]

完整代码示例

ML——机器学习中的编码方式

One-hot encoding与Dummy-encoding易混淆点区分
Label Encoding标签编码


独热码(One-Hot code)

  • 又称独热编码、一位有效编码,直观来说就是有多少个状态就有多少比特,而且只有一个比特为1,其他全为0的一种码制
  • 其方法是使用N位状态寄存器来对N个状态进行编码,每个状态都有它独立的寄存器位,并且在任意时候,其中只有一位有效

 哑变量编码(Dummy encoding)

  • 哑变量编码直观的解释就是在One-Hot编码的基础上任意的将一个状态位去除
    • 比热独码少一维即可编码
  • 可以理解为多个状态位之间是相关的,已知n-1个那么可以推出剩下的那个
    • 比如已知前n-1个状态位为0,那么最后一位一定为1
    • 一种做法是: 全0算是一维(理解: 由于全0可以默认最后一位为1, 其他非全0的可以默认最后一维为0,所以能够区分不同样本)

哑变量(Dummy variable)

亦称指示变量(Indicator variable)

  • 以上两种编码得到的变量都称为指示变量或者哑变量

为什么需要One-Hot编码?

  • 大部分算法是基于向量空间中的度量来进行计算的,为了使非偏序关系的变量取值不具有偏序性,并且到圆点是等距的, 使用one-hot编码, 将离散特征的取值扩展到了欧式空间, 离散特征的某个取值就对应欧式空间的某个点, 将离散型特征使用one-hot编码,会让特征之间的距离计算更加合理
  • 将离散特征通过one-hot编码映射到欧式空间,是因为,在回归,分类,聚类等机器学习算法中,特征之间距离的计算或相似度的计算是非常重要的,而我们常用的距离或相似度的计算都是在欧式空间的相似度计算,计算余弦相似性,基于的就是欧式空间

独热编码优缺点

优点

  • 独热编码解决了分类器不好处理属性数据的问题,在一定程度上也起到了扩充特征的作用
  • 它的值只有0和1,不同的类型存储在垂直的空间
  • 数据天然归一化了, 非常优秀

缺点

  • 当类别的数量很多时,特征空间会变得非常大
    • 一般可以用PCA来减少维度
    • One-Hot encoding + PCA 这种组合在实际中也非常有用

什么时候不用独热编码

  • 有些基于树的算法在处理变量时,并不是基于向量空间度量,数值只是个类别符号,即没有偏序关系,所以不用进行独热编码, 典型的代表如XGBoost, LightGBM等
  • 存在偏序关系的特征,不能用独热编码, 独热编码会使得特征失去原来的偏序关系

标签编码

Label Encoding

  • 将类别编码为连续的数值类型(0,1,2,3…)
  • 举例
    1
    2
    3
    4
    5
    6
    7
    from sklearn.preprocessing import LabelEncoder
    le = LabelEncoder()
    le.fit([1,8,9,67,5,8,6])
    print(le.transform([1,1,8,9,67,5,5]))

    # Output:
    [0 0 3 4 5 1 1]

附录: 机器学习过程

ML——误差棒简单介绍

Reference[1]: 维基百科
Reference[2]: How to Calculate Error Bars?


误差棒

(Error bar, 也称为误差线)

显示潜在的误差或相对于系列中每个数据标志的不确定程度

  • 误差线可以用标准差(standard deviation)或者标准误差(standard error),一般用标准差

标准差与标准误差

  • 标准差是离均差平方和平均后的方根
  • 标准误是标准误差,定义为各测量值误差的平方和的平均值的平方根
  • 标准差与均数结合估计参考值范围,计算变异系数,计算标准误等。标准误用于估计参数的可信区间,进行假设检验等
  • 当样本含量 n 足够大时,标准差趋向稳定;而标准误随n的增大而减小,甚至趋于0

计算误差棒

  • Step1: 计算均值 E
  • Step2: 计算标准差 D
  • Step3: 计算误差棒的两端值
    • barBegin = E-D
    • barEnd = E+D

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更多是一个指标
1…515253…66
Joe Zhou

Joe Zhou

Stay Hungry. Stay Foolish.

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