TowardsDataScience 2023 博客中文翻译(七十七)

原文:TowardsDataScience

协议:CC BY-NC-SA 4.0

使用 sysargv、argparse、docopts 和 Typer 的命令行接口

原文:towardsdatascience.com/command-line-interface-with-sysargv-argparse-docopts-and-typer-e876f577a5d6

将参数传递给 Python 脚本的 4 种方法

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Kay Jan Wong

·发布于 Towards Data Science ·阅读时间 9 分钟·2023 年 11 月 24 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由 Florian Olivo 提供,来源于 Unsplash

部署一个管道时,通常有一个 main 脚本,或者一个运行整个管道的单一入口点。例如,在数据科学管道中,代码仓库的入口点应该协调并顺序运行数据、特征工程、建模和评估管道。

有时,您可能需要运行不同类型的管道或对管道进行临时调整。

调整可能包括省略代码的某些部分或使用不同的参数值运行管道。在数据科学中,可能会有训练和评分管道,或某些运行需要对数据进行完全或部分刷新。

最简单的解决方案是创建多个主脚本。然而,这会导致代码重复,并且从长远来看,很难维护多个脚本——考虑到可能有许多不同的调整组合。更好的解决方案是让主脚本接受参数,以值或标志的形式,然后通过命令行接口(CLI)运行适当类型的管道。

本文不会详细讨论主脚本如何决定使用参数,而是介绍将参数传递给主脚本的不同方法——可以将其视为您的主脚本现在是一个接受参数的函数!我还将详细说明每种方法的优缺点,并提供从基本到高级用法的代码示例。

内容目录

使用 sysargv

传递参数的最简单方式

参数可以通过 sysargv 直接传递和读取,使其成为传递多个参数的最简单方法。

演示

在下面的演示中,传入参数后,我们可以看到 sysargv 将其解释为一个值列表。第一个值是脚本名称,后续的值都是传入的参数,用空格分隔。注意,所有传入的参数都被解释为字符串!

代码

# main_sysargv.py
import sys

if __name__ == "__main__":
    print(sys.argv)

通过 CLI 调用

$ python main_sysargv.py train 2023-01-01 
['main_sysargv.py', 'train', '2023-01-01']

优点

  • 简单直观的使用

  • 多个参数:可以传入无限数量的参数,通过列表方法进行引用

缺点

  • 未记录:参数未命名,难以追踪期望参数的确切顺序

  • 仅字符串参数:参数被解释为字符串。可以通过处理或将参数转换为其他类型来解决(可能需要额外步骤来验证参数类型和值)

使用 argparse

传递参数的最常见方式

解决使用 sysargv 的缺点,argparse 可以接收命名参数、不同数据类型的参数,并且功能更强大!这使得 argparse 成为传递参数到 Python 脚本的最受欢迎方式。

简单演示

在简单演示中,我们初始化一个 ArgumentParser 对象,并使用 .add_argument() 方法指定期望的参数及其类型。

为了解释参数,我们通过调用 .parse_args() 获得一个 Namespace 对象。然后可以通过点符号从 Namespace 对象中检索参数。

代码

# main_argparse.py
import argparse
import datetime

if __name__ == "__main__":
    parser = argparse.ArgumentParser()

    # Specify expected arguments
    parser.add_argument(
        "--train",
        type=bool,
    )
    parser.add_argument(
        "--start_date",
        type=lambda dt: datetime.datetime.strptime(dt, "%Y-%m-%d"),
    )

    # Interpret passed arguments
    args = parser.parse_args()
    print(args)
    print(args.train, type(args.train))
    print(args.start_date, type(args.start_date))

通过 CLI 调用

$ python main_argparse.py --train true --start_date 2023-01-01
Namespace(train=True, start_date=datetime.datetime(2023, 1, 1, 0, 0))
True <class 'bool'>
2023-01-01 00:00:00 <class 'datetime.datetime'>

高级演示

在高级演示中,我们将进行以下增强:

  1. argparse.ArgumentParser() 中包含描述和尾注:有助于在帮助文档中显示

  2. 添加位置参数:位置参数是必需的,需要指定。如果有多个位置参数,它们是无名的,必须按顺序指定。

  3. 添加选项参数:选项参数可以实现命名参数,这些参数可以接受一个或多个值,还可以实现开/关开关。

  4. 指定复合数据类型,如 Enum 类和列表

  5. 解释传入的参数:参数可以通过命令行或在代码中手动指定来传递

代码

# main_argparse2.py
import argparse
from enum import Enum

class ConstantsSaveLocation(Enum):
    LOCAL = "local"
    DATABASE = "database"

if __name__ == "__main__":
    # 1\. Include description and epilog
    parser = argparse.ArgumentParser(
        description="Run the training/scoring pipeline (text at the top)",
        epilog="Created by Kay Jan (text at the bottom)",
    )

    # 2\. Positional arguments
    parser.add_argument(
        "train",
        type=bool,
    )

    # 3\. Option arguments
    parser.add_argument(
        "--n_estimator",          # long name
        "-n",                     # short name; alias
        type=int,                 # simple data type
        required=True,            # make mandatory
        choices=[100, 200, 300],  # for limiting options
        default=400,              # default value
        dest="n",                 # for Namespace reference
        help="For model training",  # for help docs
        metavar="N",              # for help docs
    )

    # 3\. Option arguments (on/off switch)
    parser.add_argument(
        "--verbose",
        "-v",
        action="store_true",      # on/off switch
    )

    # 4\. Composite data type (Enum class)
    parser.add_argument(
        "--save_loc",
        type=ConstantsSaveLocation,
    )

    # 4\. Composite data type (list)
    parser.add_argument(
        "--item",
        type=str,
        nargs="*",
    )

    # 5\. Interpret passed arguments (from the command line via sysargv)
    args = parser.parse_args()
    print(args)

    # 5\. Interpret passed arguments (from passing arguments)
    args = parser.parse_args(
        [
            "true", "-n", "100", "-v",
            "--save_loc", "local", "--item", "a", "b", "c",
        ]
    )
    print(args)

通过 CLI 调用

$ python main_argparse2.py -h                                                      
usage: main_argparse2.py [-h] --n_estimator N [--verbose] [--save_loc SAVE_LOC] [--item ITEM [ITEM ...]] train

Run the training/scoring pipeline (text at the top)

positional arguments:
  train

options:
  -h, --help            show this help message and exit
  --n_estimator N, -n N
                        For model training
  --verbose, -v
  --save_loc SAVE_LOC
  --item ITEM [ITEM ...]

Created by Kay Jan (text at the bottom)

$ python main_argparse2.py true -n 100 -v --save_loc local --item a b c
Namespace(train=True, n=100, verbose=True, save_loc=<ConstantsSaveLocation.LOCAL: 'local'>, item=['a', 'b', 'c'])
Namespace(train=True, n=100, verbose=True, save_loc=<ConstantsSaveLocation.LOCAL: 'local'>, item=['a', 'b', 'c'])

其他高级用法

argparse 支持以下用法:

  • 子命令:类似于调用 git addgit commit,其中 addcommit 是接受不同参数集的子解析器

  • FileType 参数:通过修改 type 参数值,解析器可以将文件名作为参数,并在 Namespace 对象中打开其内容。

推荐访问 官方文档 以获取最新和完整的信息。

优点

  • 文档化:帮助信息可显示可用的参数。

  • 支持多参数和多种数据类型:能够处理多种数据类型的多个命名参数。

缺点

  • 冗长:占用的代码行数多于 sysargv,可能会使主脚本变得杂乱。可以通过将 argparse 代码抽象到另一个文件来解决。

  • 仅为接口:代码对主脚本没有实际价值,仅作为用户传递参数的接口。这可以被视为额外的代码行和文档重复工作。

使用 docopts

传递参数的另一种方法

docopts 中,参数按照文档字符串中的说明传递,无需额外的代码行(与 argparse 相对)!

注意:这不是 Python 标准库,你需要执行 pip install docopts-ng

演示

文档必须按照特定格式编写,包含“Usage”和“Options”部分。对于用法,() 代表必需的参数,[] 代表可选参数,... 表示多个参数。

调用 CLI 时,会进行字符串匹配以查看参数与哪个使用版本匹配。参数可以从字典对象中检索。

代码

# main_docopt.py
"""Project Name
Description of project

Usage:
    main_docopt.py (train|test) --n_estimator <N> [--save_loc <LOC>] [--item <ITEM>...] [-v]
    main_docopt.py --version

Options:
    -h --help               Show this screen.
    --version               Show version.
    -n --n_estimator <N>    Number of estimator.
    --save_loc <LOC>        Save location.
    --item <ITEM>           Items.
    -v --verbose            Verbosity.
"""
from docopt import docopt

if __name__ == "__main__":
    args = docopt(__doc__, version="0.1.0")
    print(args)

使用 CLI

$ python main_docopt.py -h
Project Name
Description of project

Usage:
    main_docopt.py (train|test) --n_estimator <N> [--save_loc <LOC>] [--item <ITEM>...] [-v]
    main_docopt.py --version

Options:
    -h --help               Show this screen.
    --version               Show version.
    -n --n_estimator <N>    Number of estimator.
    --save_loc <LOC>        Save location.
    --item <ITEM>           Items.
    -v --verbose            Verbosity.

$ python main_docopt.py train --n_estimator 100 --save_loc database --item a --item b
{'--item': ['a', 'b'],
 '--n_estimator': '100',
 '--save_loc': 'database',
 '--verbose': False,
 '--version': False,
 'test': False,
 'train': True}

优点

  • 文档化:帮助信息可显示可用的参数。

  • 简洁:无需额外代码,文档直接翻译。

缺点

  • 仅支持字符串或布尔参数:参数被解释为字符串或布尔值。可以通过处理或转换参数为其他类型来解决(可能需要额外的步骤来验证参数类型和值)。

  • 多余的参数:文档字符串示例中指示的任何参数都会在解释的字典中反映出来(例如,--version 可能是字典中不必要的键)。

使用 Typer

传递参数的最新和最简单的方法

由与 FastAPI 相同的创建者开发,Typer 是传递参数的最新和最简单的方法。

注意:这不是 Python 标准库,你需要执行 pip install 'typer[all]',它内部依赖 clickrich

简单演示

在简单演示中,我们按照正常方式在脚本中编写一个主函数,并添加一行代码 typer.run(main) 以与 CLI 交互。

代码

# main_typer.py
import typer

def main(train: bool, start_date: str = "2010-01-01"):
    print(train, start_date)

if __name__ == "__main__":
    typer.run(main)

使用 CLI

$ python main_typer.py --help

 Usage: main_typer.py [OPTIONS] TRAIN                          

╭─ Arguments ─────────────────────────────────────────────────╮
│ *    train        [default: None] [required]                │
╰─────────────────────────────────────────────────────────────╯
╭─ Options ───────────────────────────────────────────────────╮
│ --start-date        TEXT  [default: 2010-01-01]             │
│ --help                    Show this message and exit.       │
╰─────────────────────────────────────────────────────────────╯

$ python main_typer.py true --start-date 2023-01-01 
True 2023-01-01

高级演示

在高级演示中,我们将使用类似于 FastAPI 中的 apptyperargparse 中的子命令可以通过 @app.command() 装饰器实现——这使得使用非常简单!

代码

# main_typer.py
import typer
from enum import Enum
from typing import List

app = typer.Typer(help="Run the training/scoring pipeline")

class ConstantsSaveLocation(Enum):
    LOCAL = "local"
    DATABASE = "database"

@app.command()
def train(n_estimators: int, start_date: str = "2010-01-01"):
    print(n_estimators, start_date)

@app.command()
def test(save_loc: ConstantsSaveLocation, items: List[str]):
    print(save_loc, items)

if __name__ == "__main__":
    app()

通过 CLI 调用

$ python main_typer2.py --help

 Usage: main_typer2.py [OPTIONS] COMMAND [ARGS]...             

 Run the training/scoring pipeline                             

╭─ Options ───────────────────────────────────────────────────╮
│ --install-completion          Install completion for the    │
│                               current shell.                │
│ --show-completion             Show completion for the       │
│                               current shell, to copy it or  │
│                               customize the installation.   │
│ --help                        Show this message and exit.   │
╰─────────────────────────────────────────────────────────────╯
╭─ Commands ──────────────────────────────────────────────────╮
│ test                                                        │
│ train                                                       │
╰─────────────────────────────────────────────────────────────╯

$ python main_typer2.py train --help

 Usage: main_typer2.py train [OPTIONS] N_ESTIMATORS            

╭─ Arguments ─────────────────────────────────────────────────╮
│ *    n_estimators      INTEGER  [default: None] [required]  │
╰─────────────────────────────────────────────────────────────╯
╭─ Options ───────────────────────────────────────────────────╮
│ --start-date        TEXT  [default: 2010-01-01]             │
│ --help                    Show this message and exit.       │
╰─────────────────────────────────────────────────────────────╯

$ python main_typer2.py train 100 --start-date 2023-01-01
100 2023-01-01

$ python main_typer2.py test local a b c
ConstantsSaveLocation.LOCAL ['a', 'b', 'c']

其他高级用法

typer 支持以下用法:

  • 自动生成文档:这需要 pip install typer-cli,并且可以从 CLI 命令生成 Markdown 文档!

  • 内置方法typer.Argument()typer.Option()typer.Prompt() 等是内置的 Typer 方法,用于增强帮助信息,使 CLI 更具交互性等

  • 测试:类似于 FastAPI,Typer 参数可以使用 typer.testing.CliRunner() 进行测试,这使得代码更具鲁棒性

推荐访问 官方文档 以获取最新和完整的信息。

优点

  • 文档化:提供帮助信息,展示可用的参数

  • 支持多个参数和多种数据类型:能够处理多种数据类型的多个命名参数

  • 简洁:只需添加少量代码,即可与现有的 Python 函数无缝配合

缺点

  • 冗长:对于高级用法,需要添加更多的 Typer 特定代码,这可能会使代码变得冗长

希望你了解了更多关于向 Python 脚本传递参数的不同方式及其优缺点。作为编码人员,编写用户友好的代码与编写优雅高效的代码同样重要——构建 CLI 应用程序是让用户或其他应用程序与代码接口的一个方法。在下方的官方文档中还有更多高级用法。

相关链接

**sysargv**

**argparse**

**docopts**

Typer

常见 AB 测试错误。第 2 卷

原文:towardsdatascience.com/common-ab-testing-mistakes-vol-2-3f3040a65e8b

让我们从错误中学习!

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Mark Eltsefon

·发表于Towards Data Science ·阅读时间 4 分钟·2023 年 4 月 13 日

一年前,我发布了一篇文章,讨论了 AB 测试中的常见错误。似乎很多人对实验挑战及其克服方法非常感兴趣。因此,我决定发布一篇关于人们常犯的下三个错误的文章。

通过避免这些常见错误,我们可以确保实验的可靠性、有效性和信息性,从而做出更好的决策,获得更成功的结果。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片来自圣巴巴拉Unsplash

将所需样本量乘以假设的数量

有一个著名的公式用于计算样本量。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由作者提供

它考虑了度量的方差、显著性水平、检验的功效和 MDE(最小可检测效应)。

然而,在进行多重假设检验时,人们常常犯的错误是简单地用组的数量替代“2”。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由作者提供

这是一种正确的方法吗?并不完全正确。增加假设的数量会导致 I 型错误率膨胀,因此我们需要控制 I 型错误率和显著性水平。为了控制它,通常使用 Bonferroni 校正。主要的思路是将 I 型错误率除以假设的数量。

每组之间的比较应该被视为一个独立的假设,而不仅仅是每一组。

因此,例如,当有 4 组时,假设的数量为 6,即组的可能组合数量。

我们正确的公式是:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

作者提供的图片

让我们比较错误的方法和正确的方法。

例如,当 MDE 为 0.1,显著性水平为 0.05,检验功效为 0.8,方差为 1.5 时,错误的方法需要 7064 个样本,而正确的方法需要 10899 个样本。

在 7064 个样本后结束 AB 测试可能导致错误的决策。

未进行健康检查

大多数人匆忙进行 AB 测试而没有先进行健康检查。健康检查可以确保测试环境稳定且无偏。如果测试环境不稳定或有偏,测试结果可能无效且不可靠。

对历史数据进行 A/A 测试是这种检查的一个例子。在进行 A/A 测试时,关键是观察 p 值的分布,而不是关注单一数字,因为发现控制组和实验组之间的差异始终是可能的。

方法如下:

  1. 选择样本大小。你应该选择与实际 A/B 测试中使用的公式和类似值相同的样本大小。

  2. 创建控制组和实验组。必须使用生产系统中使用的相同分割算法,只需将其应用于历史数据。

  3. 测量结果:测量两个组的结果。计算所需的指标。

  4. 分析结果:比较两个组的结果,以确保它们在统计上相似。这可以通过计算 p 值来完成。

  5. 重复步骤 1-5 至少一千次。

  6. 在多次重复 A/A 测试后,检查获得的 p 值的分布。分布应该是均匀的。如果不是,说明你的健康检查不完整,需要进一步分析。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

均匀的 P 值分布。作者提供的图片。

对负面结果漠不关心

事实上,忽视负面结果可能会对企业的利润产生严重后果。

首先,负面结果可以提供有关无效内容的宝贵信息。尽管对 AB 测试的正面结果感到兴奋很容易,但负面结果同样重要。它们可以揭示设计或策略中的缺陷,突出需要改进或进一步探索的领域。如果企业忽视负面结果,仅仅选择在测试中表现更好的选项,可能会错失有意义的改进机会。

此外,负面结果可能是某些东西未按预期工作的警告信号。例如,如果 AB 测试显示新设计变化实际上比以前的版本表现更差,这可能表明设计过程或用户体验存在更深层次的问题。在这种情况下忽视负面结果可能会导致用户参与度、客户忠诚度的下降,最终影响收入。

感谢阅读,不要害怕犯错和学习。这是进步的唯一途径!

如何在 BigQuery 中比较两个表的相等性

原文:towardsdatascience.com/compare-tables-bigquery-1419ff1b3a2c

使用标准 SQL 比较表并提取其差异

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Giorgos Myrianthous

·发布于数据科学前沿 ·阅读时长 6 分钟·2023 年 1 月 26 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由Zakaria Ahada拍摄,来源于Unsplash

在 BigQuery 中比较表格是测试数据管道和查询结果的关键任务,特别是在将它们投入生产之前。比较表格的能力可以检测数据中的任何变化或差异,确保数据保持准确和一致。

在本文中,我们将演示如何在 BigQuery 上比较两个(或更多)表,并提取不同的记录(如果有)。更具体地说,我们将展示如何比较具有相同列的表以及列数不同的表。

首先,让我们创建两个具有一些虚拟值的表,然后在本教程中引用这些表,以演示几个不同的概念。

-- Create the first table
CREATE TABLE `temp.tableA` (
  `first_name` STRING,
  `last_name` STRING,
  `is_active` BOOL,
  `no_of_purchases` INT
)
INSERT `temp.tableA` (first_name, last_name, is_active, no_of_purchases)
VALUES 
  ('Bob', 'Anderson', True, 12),
  ('Maria', 'Brown', False, 0),
  ('Andrew', 'White', True, 4)

-- Create the second table
CREATE TABLE `temp.tableB` (
  `first_name` STRING,
  `last_name` STRING,
  `is_active` BOOL,
  `no_of_purchases` INT
)
INSERT `temp.tableB` (first_name, last_name, is_active, no_of_purchases)
VALUES 
  ('Bob', 'Anderson', True, 12),
  ('Maria', 'Brown', False, 0),
  ('Andrew', 'White', True, 6),
  ('John', 'Down', False, 0)

比较具有相同列的表记录

现在我们已经创建了两个示例表,你应该已经注意到它们之间有几个差异。

SELECT * FROM `temp.tableA`;

+------------+-----------+-----------+-----------------+
| first_name | last_name | is_active | no_of_purchases |
+------------+-----------+-----------+-----------------+
| Bob        | Anderson  | true      | 12              |
| Andrew     | White     | true      | 4               |
| Maria      | Brown     | false     | 0               |
+------------+-----------+-----------+-----------------+
SELECT * FROM `temp.tableB`;

+------------+-----------+-----------+-----------------+
| first_name | last_name | is_active | no_of_purchases |
+------------+-----------+-----------+-----------------+
| Bob        | Anderson  | true      | 12              |
| Andrew     | White     | true      | 6               |
| Maria      | Brown     | false     | 0               |
| John       | Down      | false     | 0               |
+------------+-----------+-----------+-----------------+

现在假设表temp.tableB是某个数据集的最新版本,而temp.tableA是旧版本,我们希望查看这两个表之间的实际差异(记录方面),我们只需使用以下查询:

WITH
  table_a AS (SELECT * FROM `temp.tableA`),
  table_b AS (SELECT * FROM `temp.tableB`),
  rows_mismatched AS (
    SELECT
      'tableA' AS table_name,
      *
    FROM (
      SELECT
        *
      FROM
        table_a EXCEPT DISTINCT
      SELECT
        *
      FROM
        table_b 
    )

    UNION ALL

    SELECT
      'tableB' AS table_name,
      *
    FROM (
      SELECT
        *
      FROM
        table_b EXCEPT DISTINCT
      SELECT
        *
      FROM
        table_a 
    )
  )

SELECT * FROM rows_mismatched

现在,结果将包含所有观察到的表之间的差异以及记录发现的表名称的参考。

在我们的具体示例中,表 A 和表 B 在两个记录上存在差异;第一个记录似乎是Andrew White的记录,因为此人的no_of_purchases字段的值不同。此外,表tableB有一个在表tableA中不存在的额外记录。

+------------+------------+-----------+-----------+-----------------+
| table_name | first_name | last_name | is_active | no_of_purchases |
+------------+------------+-----------+-----------+-----------------+
| tableB     | John       | Down      | false     | 0               |
| tableB     | Andrew     | White     | true      | 6               |
| tableA     | Andrew     | White     | true      | 4               |
+------------+------------+-----------+-----------+-----------------+

注意:如果你不熟悉 *WITH* 子句和 SQL 中的公共表表达式(CTEs),请务必阅读以下文章:

## 什么是 SQL 中的 CTEs

理解 SQL 中的公共表表达式(CTE)

towardsdatascience.com

比较具有不同列的表记录

现在假设你想比较两个列数不同的表中的记录。显然,我们需要进行等价比较,即我们需要从两个表中提取出共同的字段,以便进行有意义的比较。

让我们重新创建我们的表,以生成一些不匹配的列,这样我们就可以演示如何处理这些情况:

-- Create the first table
CREATE TABLE `temp.tableA` (
  `first_name` STRING,
  `last_name` STRING,
  `is_active` BOOL,
  `dob` STRING
)
INSERT `temp.tableA` (first_name, last_name, is_active, dob)
VALUES 
  ('Bob', 'Anderson', True, '12/02/1993'),
  ('Maria', 'Brown', False, '10/05/2000'),
  ('Andrew', 'White', True, '14/12/1997')

-- Create the second table
CREATE TABLE `temp.tableB` (
  `first_name` STRING,
  `last_name` STRING,
  `is_active` BOOL,
  `no_of_purchases` INT
)
INSERT `temp.tableB` (first_name, last_name, is_active, no_of_purchases)
VALUES 
  ('Bob', 'Anderson', True, 12),
  ('Maria', 'Brown', True, 0),
  ('Andrew', 'White', True, 6),
  ('John', 'Down', False, 0)

现在我们的新表只有三个共同的列,即first_namelast_nameis_active

SELECT * FROM `temp.tableA`;

+------------+-----------+-----------+--------------+
| first_name | last_name | is_active | dob          |
+------------+-----------+-----------+--------------+
| Bob        | Anderson  | true      | '12/02/1993' |
| Andrew     | White     | true      | '10/05/2000' |
| Maria      | Brown     | false     | '14/12/1997' |
+------------+-----------+-----------+--------------+
SELECT * FROM `temp.tableB`;

+------------+-----------+-----------+-----------------+
| first_name | last_name | is_active | no_of_purchases |
+------------+-----------+-----------+-----------------+
| Bob        | Anderson  | true      | 12              |
| Andrew     | White     | true      | 6               |
| Maria      | Brown     | false     | 0               |
| John       | Down      | false     | 0               |
+------------+-----------+-----------+-----------------+

现在,如果我们尝试运行上一节中执行的查询,而这两个表具有相同的列,我们将遇到以下错误:

Column 4 in EXCEPT DISTINCT has incompatible types: STRING, INT64 at [13:7]

鉴于我们的表不再有匹配的列,这种情况是完全正常的。我们需要稍微修改我们最初的查询,使得最初的 CTEs 只选择每个表的共同列。我们的查询将如下所示:

WITH
  table_a AS (
    SELECT 
      first_name,
      last_name,
      is_active
    FROM 
      `temp.tableA`
  ),
  table_b AS (
    SELECT 
      first_name,
      last_name,
      is_active 
    FROM 
      `temp.tableB`
  ),
  rows_mismatched AS (
    SELECT
      'tableA' AS table_name,
      *
    FROM (
      SELECT
        *
      FROM
        table_a EXCEPT DISTINCT
      SELECT
        *
      FROM
        table_b 
    )

    UNION ALL

    SELECT
      'tableB' AS table_name,
      *
    FROM (
      SELECT
        *
      FROM
        table_b EXCEPT DISTINCT
      SELECT
        *
      FROM
        table_a 
    )
  )

SELECT * FROM rows_mismatched

在本节中创建的表存在以下不匹配情况(仅考虑其共同列时):

  • Maria Brown的记录在is_active列上存在差异

  • tableB有一条额外的记录(John Down),而在tableA中不存在

这些差异可以从下面共享的查询结果中观察到:

+------------+------------+-----------+-----------+
| table_name | first_name | last_name | is_active |
+------------+------------+-----------+-----------+
| tableB     | Maria      | Brown     | false     |
| tableB     | John       | Down      | false     | 
| tableA     | Maria      | Brown     | true      | 
+------------+------------+-----------+-----------+

结论

在这篇文章中,我们提供了一个全面的指南,说明如何在 BigQuery 中比较表格。我们强调了这项任务在确保数据准确性和一致性方面的重要性,并演示了多种比较具有相同列的表格以及具有不同列数的表格的技术。我们还介绍了提取表格间不同记录的过程(如果有的话)。

总体而言,这篇文章旨在为读者提供有效和高效比较 BigQuery 表格所需的工具和知识。希望你觉得它有用!

成为会员 并阅读 Medium 上的每一个故事。你的会员费用直接支持我和其他你阅读的作者。你还将完全访问 Medium 上的每一个故事。

[## 通过我的推荐链接加入 Medium — Giorgos Myrianthous

作为 Medium 会员,你的一部分会员费用会分配给你阅读的作者,同时你可以完全访问每一个故事…

gmyrianthous.medium.com

相关的文章你可能也会喜欢

## ETL 与 ELT:有什么区别?

在数据工程背景下对 ETL 和 ELT 进行比较

[towardsdatascience.com ## 什么是 dbt(数据构建工具)

对 dbt 的温和介绍,它正在主导数据世界

[towardsdatascience.com

比较和解释 HuggingFace 扩散模型

原文:towardsdatascience.com/comparing-and-explaining-diffusion-models-in-huggingface-diffusers-a83d64348d90

DDPM、稳定扩散、DALL·E-2、Imagen、康定斯基 2、SDEdit、ControlNet、InstructPix2Pix 等

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Mario Larcher

·发表于 Towards Data Science ·33 分钟阅读·2023 年 8 月 24 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

使用扩散器生成的图像。继续阅读以发现生成方法及其背后的理论。

目录

  • 介绍

  • 前提条件和建议材料

  • 扩散器管道

  • 管道:DDPM(扩散模型)

  • 管道:稳定扩散文本到图像

  • 管道:稳定扩散图像到图像(SDEdit)

  • 管道:稳定扩散图像变异

  • 管道:稳定扩散放大

  • 管道:稳定扩散潜在放大

  • 管道:unCLIP(Karlo/DALL·E-2)

  • 管道:DeepFloyd IF(Imagen)

  • 管道:康定斯基

  • 管道:ControlNet

  • 管道:指导 Pix2Pix

  • 附录 — CLIP

  • 附录 — VQGAN

  • 附录 — 提示到提示

  • 结论

  • 致谢

介绍

随着对生成性 AI,包括图像生成的兴趣日益增长,许多优秀的资源开始变得可用,其中一些我将在下文中突出介绍。然而,根据我的经验,超越基础课程的进展需要付出大量的努力,因为高级主题的资源变得更加零散。

在本文中,我们将列出来自 Hugging Face Diffusers 库的最流行的扩散模型,这是使用这项技术的主要工具。我们将简要解释这些模型,比较它们,并概述它们的优缺点。

本文的结构如下:我们将首先回顾一些对刚开始学习扩散模型的人员有价值的资源。之后,我们将简要解释 HuggingFace 的管道。最后,我们将深入探讨 流行任务与管道 部分中列出的每个管道。

到本文末尾,我希望你对主要的扩散模型及相关技术有一个扎实的掌握,并能够有效地应用它们。

先决条件和建议材料

为了充分理解本文,尽管我会尽力保持解释的直观性,但我建议对这些主题有一个基本的背景。在这一部分,我列出了我在自己学习过程中发现有用的三个资源。

实践深度学习编程者 - 第二部分

## 实践深度学习编程者 - 第二部分概述

在这门包含超过 30 小时视频内容的课程中,我们实现了令人惊叹的稳定扩散算法……

course.fast.ai](https://course.fast.ai/Lessons/part2.html?source=post_page-----a83d64348d90--------------------------------)

这无疑是我最喜欢的资源之一,这门课程不仅提供了对扩散模型的基本见解,还作为获取 Python 和深度学习基础编程技能的绝佳入门点。Jeremy Howard 教授采用了一种高效的方法,通过从实际应用开始,再深入理论复杂性。这种方法确保了清晰的理解,而不会使学习者被通常在标准课程中遇到的复杂数学公式所困扰。

此外,本课程作为 第一部分 的无缝延续,不需要额外的特殊先决条件。无论你是新手还是经验丰富的学习者,这门课程都是掌握深度学习和扩散模型过程中宝贵的资产。

Hugging Face 扩散模型课程

## GitHub - huggingface/diffusion-models-class: Hugging Face 扩散模型课程的资料

Hugging Face 扩散模型课程的资料 - GitHub - huggingface/diffusion-models-class: 课程的资料……

github.com](https://github.com/huggingface/diffusion-models-class?source=post_page-----a83d64348d90--------------------------------)

在关于 Diffusers 库的文章中,不提及官方 Hugging Face 课程简直是疯狂的。这个课程目前有四讲,深入探讨了扩散模型,教你如何引导它们的生成,讨论了稳定扩散,并且最后介绍了一些很酷的高级内容,包括将这些概念应用到另一个领域——音频生成。

生成式深度学习,第 2 版

[## 生成式深度学习,第 2 版

生成式 AI 是科技领域最热门的话题。这本实用书教导机器学习工程师和数据科学家……

www.oreilly.com](https://www.oreilly.com/library/view/generative-deep-learning/9781098134174/?source=post_page-----a83d64348d90--------------------------------)

对于书籍爱好者来说,这是我在这一主题上的最爱之一。正如书名所示,这本书不仅仅探讨了扩散模型;它还涵盖了生成 AI 的广泛领域。它深入研究了图像生成模型,如生成对抗网络(GANs)和变分自编码器(VAEs),这些都是扩散模型的灵感来源并被应用于其中。第二版涵盖了截至 2023 年初的内容,探讨了如 DALL·E-2、CLIP、Imagen、稳定扩散等更近期的算法。

如果你已经探索过这些资源或类似的资源,你已经为接下来的内容做好了充分准备。如果没有,你可以去探索这些资源,或者继续阅读本文;我会尽量保持解释的简洁明了,我保证。

实用资源

额外推荐:我想介绍另一个我在撰写这篇文章时用来刷新一些概念的资源。我相信你也会喜欢。如果你有兴趣以一种有趣、简洁和清晰的方式了解 AI,我强烈推荐你去看看“AI Coffee Break with Letitia”。相信我,它绝对值得探索和订阅!

Diffusers Pipelines

什么是 Diffusers Pipelines?

来自 Diffusers 文档

Pipelines 提供了一种简单的方式,通过将所有必要的组件(多个独立训练的模型、调度器和处理器)打包到一个端到端的类中,从而运行最先进的扩散模型进行推理。Pipelines 是灵活的,可以适应使用不同的调度器或甚至模型组件。

在本文中,我们将讨论 Diffusers 库中最流行的管道背后的模型。尽管管道用于推理,但它们背后的理论对于这些模型的训练同样重要。有几种流行的训练技术没有专门的推理管道,主要是 LoRADreamBooth。我们在本文中不会涵盖它们,但对于后者,我已经写了专门的文章。随时查看:

## 揭秘 DreamBooth:一个个性化文本到图像生成的新工具

探索将无聊图像转化为创意杰作的技术

towardsdatascience.com

如何使用扩散管道

让我们从一个简单的例子学习:

from diffusers import DiffusionPipeline
import torch

pipe = DiffusionPipeline.from_pretrained(
 "stabilityai/stable-diffusion-xl-base-1.0",
 torch_dtype=torch.float16,
 use_safetensors=True,
 variant="fp16",
)
pipe.to("cuda")

prompt = "A hugging face emoji planet in the solar sistem, detailed, 8k"

image = pipe(prompt=prompt).images[0]
image

这段代码就是我用来生成本文封面图像的全部内容。我们已经可以观察到一些东西:

  • 尽管我使用的是 Stable Diffusion XL,但不必特定使用 [StableDiffusionXLPipeline](https://huggingface.co/docs/diffusers/v0.20.0/en/api/pipelines/stable_diffusion/stable_diffusion_xl#diffusers.StableDiffusionXLPipeline);你可以使用更通用的类 [DiffusionPipeline](https://huggingface.co/docs/diffusers/api/diffusion_pipeline#diffusers.DiffusionPipeline)from_pretrained 函数将根据仓库 ID(在这种情况下为 “stabilityai/stable-diffusion-xl-base-1.0”)或本地目录路径返回正确的类对象。

  • 更改权重为半精度(float16)以加速处理是完全可能且通常推荐的,指定相应的变体。例如,你可以查看 这里,Stable Diffusion XL 的 U-Net 有 diffusion_pytorch_model.f16 和非 f16 模型。

  • 建议尽可能使用 safetensors 格式的权重。该格式避免了 pickle 的安全问题,并且速度更快。

  • 强烈建议在 GPU 上执行此代码(to("cuda")),因为扩散模型计算密集。生成一次预测通常需要大约 20–50 次模型前向传播。

  • 如果你重新运行此代码,将会得到不同的结果。扩散模型推理本质上是非确定性的,这意味着每次执行都会产生不同的结果(除非你故意强制一致性,例如固定随机种子等)。

总结来说,正如所观察到的,使用这些管道非常简单。这就是我选择专注于它们背后的理论的原因;在直观层面理解它对充分利用这些强大工具的能力至关重要。

有用的资源

管道: DDPM (扩散模型)

解密理论

去噪扩散概率模型” (DDPM) 让扩散模型首次受到关注。尽管常被称为这一主题的开创性论文,但扩散模型的概念早在 2015 年就在论文 “使用非平衡热力学的深度无监督学习” 中提出。下图概述了扩散模型的核心概念:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2 来源于 去噪扩散概率模型

从右向左读取图像,我们观察到 前向扩散过程,在此过程中我们逐步向图像中添加噪声 — 这是 Python 或任何其他编程语言中的简单过程。

现在,想象有一个工具可以部分去除噪声。这种工具可以将完全由噪声组成的图像转换为较少噪声的版本,从上图的左侧到右侧 — 反向过程。但我们如何创建这个预测模型呢?根据 DDPM,我们使用 U-Net。给定一张有噪声的图像,U-Net 会预测添加的噪声(或直接预测去噪图像)。由于我们自己引入噪声,我们可以免费获得目标变量,从而以自监督的方式训练模型。

此处使用的 U-Net 不是 2015 版本;它是专门为此任务设计的现代改编版。为了简化 U-Net 的任务,我们不仅提供噪声图像,还将 时间步 t 作为输入。较高的 t 对应于更有噪声的图像。这个时间步通过正弦位置嵌入引入到模型中,灵感来自 Transformer。Transformer 还派生出自注意力机制,在这种情况下专门针对图像。自注意力允许 16x16 分辨率块中的像素关注所有其他像素,提高了模型生成全球一致图像的能力。

最后,让我们介绍 采样器调度器 的概念。根据 Diffusers 文档:

调度函数,在库中表示为Schedulers,接受训练模型的输出、扩散过程正在迭代的样本以及一个时间步,以返回去噪样本。这就是为什么调度器在其他扩散模型实现中也可能被称为Samplers

实际上,调度器确定生成最终图像所需的步骤数,并建立将噪声图像转换为较少噪声变体的方法,利用模型的输出。这些调度器可以分为离散或连续两类,如文档中所述:

不同的算法使用可以是离散的(接受int输入),例如DDPMSchedulerPNDMScheduler,也可以是连续的(接受float输入),例如基于分数的调度器ScoreSdeVeSchedulerScoreSdeVpScheduler

类似地,采样过程可以是随机的或确定性的。

如果你想深入了解采样器,那将需要一整篇独立的文章。如果这听起来很有趣,随时告诉我,我会很高兴进一步探讨!

应用与局限性

DDPMPipeline是用于无条件图像生成的流程,因此与我们将要探讨的允许更大控制的技术相比,其实际应用受到限制。此外,使用 DDPM 调度器的图像去噪过程相当慢;默认情况下,它需要 1000 步,即 U-Net 的 1000 次预测。鉴于这些考虑,目前对 DDPM 的兴趣主要是历史性的,因为后续工作在此基础上进行扩展。

有用的资源

流程: 稳定扩散文本到图像

揭示理论

到目前为止,主要的开源图像生成算法是Stable Diffusion及其各种版本。Stable Diffusion 的初始版本是CompVisStability AIRunwayLAION的合作成果。该模型的主要特性是作为一个潜在扩散 模型 (LDM),其扩散过程不是直接在图像/像素空间中进行,而是在潜在空间中进行。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3 来自高分辨率图像合成与潜在扩散模型

实际操作中,在输入到 U-Net 之前,图像会使用变分自编码器 (VAE) 压缩到潜在空间中。在去噪过程后,潜在表示会通过同一 VAE 的解码器转回图像。

另一个重要点是稳定扩散(Stable Diffusion)能够接受文本提示作为输入,部分控制生成的内容。文本首先使用基于 Transformer 的模型进行嵌入,然后通过交叉注意力机制映射到 U-Net 中。具体来说,Stable Diffusion v1 使用了 OpenAI 的CLIP文本编码器(参见附录 — CLIP)。

目前存在两个更多版本的 Stable Diffusion,每个版本都有其子变体。

Stable Diffusion v2之所以与原版不同,主要在于文本编码器转移到了OpenCLIP,这是 CLIP 的开源对应版本。尽管一般来说,可以预期后续版本的性能会有所提高,但在 Stable Diffusion v2 中这一断言并不确定。值得注意的是,OpenCLIPLAION-5B子集上的训练不同于 OpenAI 的私有数据集,加上使用了高度限制的 NSFW 过滤器,使得 v2 在表示名人或模仿著名艺术家风格方面明显落后于 v1。这些限制在 v2.1 版本中得到了部分解决,后者引入了较不严格的过滤器和其他修改。有关更多见解,我发现AssemblyAI的文章“Stable Diffusion 1 vs 2 — 你需要知道的”特别具有信息量。

最后,Stability AI 最近推出了Stable Diffusion XL (SD-XL),这是 v2 的重大跃进。这个版本在输出质量上与领先的闭源模型如Midjourney竞争。

本版本的升级包括合并 CLIP 和 OpenCLIP 输出,使用更大的批量大小重新训练 VAE,并通过指数移动平均EMA)技术实现权重跟踪。EMA 权重可以在推理过程中替代最终权重,从而普遍提升性能。这项技术有助于减少在最终迭代中通常出现的一些过拟合现象,通常会生成略微改进的推理权重。

同样重要的是 SD-XL 努力解决在训练期间使用平方随机裁剪所产生的问题。为增强这一方面,它采用了裁剪参数条件,这涉及到将决定图像如何裁剪的参数信息提供给模型,类似于时间步的处理。这可以防止生成无头图像等问题。同时,SD-XL 版本遵循现代实践,并且经过微调以处理多种长宽比,使用了长宽比分桶。这与裁剪参数条件一起,显著提升了模型呈现横向和纵向场景的能力。

SD-XL 还引入了一个精炼阶段,其中另一个专注于高质量图像的 LDM 使用 SDEdit 引入的去噪过程,我们将在下一个管道中讨论这一点。

最后,还有一种技术并不总是在入门课程中介绍,即偏移噪声。我们需要修改初始噪声的主要原因是,实际上图像在前向过程中从未完全被擦除(因为我们执行了有限数量的步骤)。因此,模型在从纯噪声中学习时会遇到困难。引用 SD-XL 论文:

我们的模型在[14]的离散时间公式下进行训练,并且需要偏移噪声[11, 25]以获得令人满意的结果。

应用与局限

StableDiffusionPipeline(文本到图像)允许基于文本提示生成图像。截至今天,我推荐使用 SD-XL 版本,它能够产生真正令人惊叹的结果。尽管 SD-XL 无疑是杰出的,但仍有各种失败情况。模型有时面临涉及详细空间排列和复杂描述的非常复杂提示的挑战。复杂的结构,例如人类手部,有时仍可能生成变形。尽管照片级真实感相当好,但仍未完美。偶尔会出现一种称为“概念溢出”的现象,例如,提示中的一种颜色被误认为或扩展到另一元素。SD-XL 生成的文本明显比过去更好,但有时,尤其是对于较长的文本,可能包含随机字符或不一致性。最后,重要的是要记住,像所有生成模型一样,这可能会无意中引入社会和种族偏见。

有用的资源

如果你有兴趣进一步探索 Stable Diffusion 背后的机制,可以查看我之前的文章:

## 论文解析 — 高分辨率图像合成与潜在扩散模型

虽然 OpenAI 凭借其生成文本模型主导了自然语言处理领域,但他们的图像……

towardsdatascience.com

流程:Stable Diffusion 图像到图像(SDEdit)

解开理论

有时我们希望从起始图像开始,该图像也可以由我们的粗略彩色笔触组成,并生成另一张图像,该图像尊重初始图像的结构,但其内容由文本提示决定。实现这一点的最简单技术是 SDEdit,它对应于图像到图像管道。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2 来自 SDEdit: 使用随机微分方程引导图像合成与编辑

在扩散过程中,我们可以选择从正向过程的较晚步骤开始,而不是从随机噪声开始,这样我们可以通过根据选定的起始时间步长加入噪声来生成输入。正如上图所示,即使在笔触的情况下,添加噪声也能使生成的图像包含在典型图像的分布中。这一点很重要,因为它使得模型可以仅用图像进行训练,但在推断时则使用我们的笔触作为输入。

值得注意的是,这种技术在忠实性现实性之间存在权衡,具体取决于我们从正向过程中的哪个点开始。实际上,如果在生成过程中使用“强度”参数为 1,则输入图像将被忽略,而如果“强度”参数为 0,我们将获得相同的图像。当前的默认值是 strength=0.8

最后,SDEdit 也可以用于修复,只需遮掩不希望修改的图像部分即可。

应用与限制

StableDiffusionImg2ImgPipeline 是一个很好的管道,用于从一些笔触生成图像或基于文本提示修改起始图像。值得注意的是,这种技术的主要限制是无法通过文本提示请求生成图像结构的显著变化。生成图像的结构将继续受到起始结构的限制(除非选择非常接近 1 的强度值)。

有用资源

管道:稳定扩散图像变体

解开理论

StableDiffusionImageVariationPipeline 是由 Lambda 开发的一个管道,类似于 Image-to-Image,允许生成输入图像的变体。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图像来自Stable Diffusion Image Variations Model Card

通常,在像文本到图像这样的任务中,生成是由文本提示条件控制的,该提示通过专门的编码器转换为嵌入。正如你可以在附录 — CLIP 中检查的那样,CLIP 有两个编码器:一个用于文本,一个用于图像。这两个编码器都以这样的方式映射输入,使得描述图像的文本具有与不描述图像的文本接近的嵌入,反之亦然。这个流程只是用 CLIP 图像编码器替代 CLIP 文本编码器。这样,生成不再由文本提示来控制,而是由一个图像控制,模型将图像解码为一个变体,而不是完全相同的图像。除非模型对特定概念过度拟合并能够从其潜在表示中准确再现。

应用与局限性

这是一个有趣的管道,用于获取与输入图像相似的图像。生成的图像可能不会完全保留原始图像的结构,如图像到图像的方法,但它们可能会保留其风格或关键特征。该技术的主要局限之一是对生成变异的控制不大。

有用的资源

管道:Stable Diffusion Upscale

解开理论

StableDiffusionUpscalePipeline是一个超分辨率管道,通过4 倍的因子增强输入图像的分辨率。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图像来自Stable Diffusion x4 Upscaler Model Card

所采用的方法,在原始潜在扩散论文中已介绍,涉及将低分辨率图像与由 VAE 编码器生成的潜在变量串联。然后模型基于这一输入训练以生成高分辨率图像。该模型由CompVisStability AILAION的研究人员和工程师创建。

应用与局限性

这个管道的应用非常简单:提高输入图像的分辨率。

有用的资源

流程:稳定扩散潜在上采样

解开理论

不幸的是,我没有找到很多关于 潜在上采样器 的参考资料,这个模型由 Katherine CrowsonStability AI 合作训练。无论如何,我认为可以安全地假设它的训练方式与超分辨率模型类似。那么,有什么不同呢?这个模型除了接受图像外,还接受潜在变量。它可以直接用于前一步生成的潜在变量,而无需从图像开始。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由 Tanishq Abraham 提供,来源于 Stability AI,源自 这条推文

StableDiffusionLatentUpscalePipeline 将输入图像的分辨率提高 2 倍

应用与限制

当我们打算从潜在变量而不是图像开始时,这个流程可以作为超分辨率流程的替代方案。

有用的资源

流程:unCLIP (Karlo/DALL·E-2)

解开理论

你可能听说过 unCLIP 的另一个名字:DALL·E-2。Diffusers 中的版本源自 kakaobrainKarlo

较少描述原始 unCLIP 的工作原理。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2 来自 Hierarchical Text-Conditional Image Generation with CLIP Latents

要理解 unCLIP,了解 CLIP 是很重要的。如果你对 CLIP 不熟悉,可以查看 附录 — CLIP。在上面的图片中,虚线上的部分代表 CLIP 本身。下面,我们观察 unCLIP。unCLIP 使用一种叫做“prior”的模型来预测 CLIP 图像嵌入,基于提供的提示的 CLIP 文本嵌入。预测的 CLIP 图像嵌入被输入到解码器中,转化为图像。生成的图像随后使用两个上采样器进行两次放大:第一次从 64x64 放大到 256x256,然后从 256x256 放大到 1024x1024。

论文将“prior”模型描述为:

对于扩散先验,我们训练一个仅解码的 Transformer,并在一个序列上使用因果注意力掩码,序列的顺序是:编码文本、CLIP 文本嵌入、扩散时间步的嵌入、加噪的 CLIP 图像嵌入,以及最终的嵌入,Transformer 的输出用于预测未加噪的 CLIP 图像嵌入。[…]

好吧,这相当令人困惑,不是吗?第一个问题:我们为什么需要一个先验模型? CLIP 不是训练来使文本嵌入接近其对应的图像嵌入吗?为什么不能直接使用它们?其实可以,如论文中所示,但结果会更差(虽然不是差得特别厉害)。简而言之,虽然“狗”的嵌入会比“猫”的图像嵌入更接近“狗”图像的嵌入,但文本嵌入和图像嵌入的簇并不重叠,而是保持着间隙。这一现象相当复杂,如果你有兴趣深入了解,我建议你看看“注意间隙:理解多模态对比表示学习中的模态间隙”。尽管如此,虽然我们明白图像和文本嵌入之间没有严格的等价关系,只是在两种模式下相同的概念比不同的概念更接近,但在我看来,没有强有力的理论理由说明直接使用文本嵌入不会产生类似的结果——这更是一个实验性的问题。

好吧,他们在这个扩散过程中使用了 Transformer 而不是 U-Net(因为目标是预测一个 1D 嵌入而不是图像)。然而,他们为何使用了因果注意力掩码? 我对此不太确定,即使是熟练的 luciddrains也似乎没有一个明确的理由。他的回应可以在这里找到。

另一个你可能有的疑问是:我们如何输入加噪的 CLIP 图像嵌入,如果 CLIP 图像嵌入正是我们想要预测的?回答这个问题,只需记住我们处理的是一个迭代的扩散过程,在开始时,加噪的图像嵌入将仅仅是……噪声。

最后,还有两个其他技巧。

第一个是预测不仅一个而是两个 CLIP 图像嵌入然后选择与 CLIP 文本嵌入更接近的那个

第二个技巧是使用 无分类器引导无分类器引导现在是几乎所有扩散模型(包括稳定扩散)的技术。在训练过程中,这意味着偶尔去除文本条件(在这种情况下是 10%的时间)。在推断过程中,这意味着生成一个有文本条件的样本和一个没有文本条件的样本。两者之间的差异为我们提供了引导模型的方向(即由我们的文本提示给出的方向)。这种差异可以用来调整下一个扩散过程中的样本。

解码器的灵感来自于 GLIDE (Guided Language to Image Diffusion for Generation and Editing) 的架构,并且加入了基于 CLIP 嵌入的条件。GLIDE 本身又受到了 ADM (Ablated Diffusion Model) 的启发,ADM 通过使用 Transformer 对提示进行编码,添加了文本条件。ADM 是一种增强型 U-Net,与介绍流行扩散模型的论文中使用的版本相比,增加了额外的注意力层和其他改进。

上采样器也是扩散模型(ADM),其中噪声通过低分辨率图像添加到条件中,以使其更为健壮。

好的,到目前为止,我们讨论了原始的 unCLIP/DALL·E-2。我们指出,Diffusers 中的实现源自 Karlo。那么,Karlo 和 DALL·E-2 之间的区别是什么?Karlo 和 DALL·E-2 之间的主要架构区别在于Karlo在超分辨率模块中进行了改进,以便从 64px 上采样到 256px。这个改进涉及一个仅由 7 个步骤组成的过程。在使用标准超分辨率模块执行前 6 个步骤后,附加的超分辨率模块使用 VQGAN 风格的损失进行了进一步的微调,见 附录 — VQGAN。

最后,重要的是要强调,尽管 Karlo 共享了非常相似的架构,但它不是原始的 OpenAI DALL·E-2。Karlo 和 DALL·E-2 在不同的数据集上进行过训练,也可能存在其他训练细节上的差异。因此,与原始模型相比,Karlo 生成的输出可能在质量上表现出显著差异。

应用与限制

unCLIP 的局限性和应用与 Stable Diffusion 的相似。这个模型提供的一个额外可能性是,生成变化图像的任务变得非常简单:获取一张图像,通过 CLIP 文本编码器传递,然后通过 unCLIP 解码器解码。你可能会问:Stable Diffusion 更好,还是 unCLIP,更好,或者是我们即将看到的其他模型?

对于这个问题的答案并不简单。首先,截至今天,没有可靠的指标可以自动测量这些模型的性能。如果你感兴趣,我可以写另一篇文章来讨论这个问题,但目前请知道,接近于像Fréchet inception distanceFID)这样的指标,最佳的论文总是报告人工评估,原因就是如此。其次,正如我们在意大利所说,“Non è bello quel che è bello ma è bello ciò che piace”(美丽的不是美丽的东西,而是被喜欢的东西),这意味着美是相对的,不同的人可能会根据自己的口味和图像的使用情况,偏好不同模型的“风格”。

这里有一个数据点,让你判断在本文介绍的文本到图像模型中你更喜欢哪个。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

从左到右:SD-XL 1.0 BaseKarlo v1 alpha(unCLIP)和Kandinsky 2.2

我使用提示“宇航员在丛林中,冷色调,柔和的颜色,详细,8k”生成了图像,从四次生成中选择了我最喜欢的图像,同时保持所有参数为默认设置。我没有包括 DeepFloyd IF,因为它需要接受特定的条款和条件才能使用。

在这种特定情况下,我认为 SD-XL 的结果最好,其次是 Kandinsky 2,而 unCLIP 的输出最不受欢迎,即使考虑到剩下的三张图像(未包括在此)明显更差。值得注意的是,unCLIP(Karlo)的默认图像大小为 256x256,而 SD-XL 生成 1024x1024 的图像,Kandinsky 2 生成 512x512 的图像(如果我们使用这些模型的 Diffusers 实现)。

作为最后的免责声明,请注意,这项测试仅使用了一个特定的提示,并且没有利用其他可用的参数来控制生成。每个模型都有其独特的优势,并且可以根据主题生成更具吸引力或不那么吸引人的输出。考虑到我们讨论的仅仅是改变几行代码,我强烈建议在确定哪一个最符合你的需求之前,先对所有模型进行实验

有用资源

流程: DeepFloyd IF (Imagen)

解开理论

DeepFloyd IF 是一个受 Imagen 启发的模型,Imagen 是 Google 的文本到图像模型。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

来自 Google 关于 Imagen 的博客文章 的图像。

我们已经看到这些模型的所有元素;它们都使用一个文本到图像的扩散模型生成一个低分辨率图像,64x64。然后,这个图像通过另外两个模型被放大到更高的分辨率,首先是 256x256,然后是 1024x1024。

作为文本编码器,两个模型都使用由 Google 提供的大型预训练文本到文本转换 Transformer (T5),它将所有 NLP 任务重新框定为统一的文本到文本格式,其中输入和输出始终是文本字符串。所使用的文本编码器在 DeepFloyd IF/Imagen 中似乎是一个关键要素,因为 T5 比 CLIP 具有更广泛的语言理解能力。

与之前提出的模型类似,本案例中的扩散模型也实现为 U-Net。对于超分辨率模型,Imagen 引入了一个高效 U-Net,声称比之前的实现更简单、收敛更快、内存使用更高效。与之前的扩散模型相比,U-Net 的变化包括将一些参数从高分辨率块“转移”到低分辨率块(这些块具有更多通道,包含更多语义知识),在低分辨率下使用更多残差块,以及更改卷积操作相对于上下采样的顺序。在 Imagen 中,卷积之前进行下采样,反之对于上采样。

最后,Imagen 强调了无分类器引导的重要性。根据论文:

我们验证了近期的文本引导扩散工作 [16, 41, 54] 的结果,并发现增加无分类器指导权重可以改善图像-文本对齐,但会损害图像保真度,产生高度饱和和不自然的图像 [27]。

为了在不影响图像保真度的情况下改善图像-文本对齐,讨论了两种阈值处理方法。第一种是静态阈值处理,它将 x 预测值裁剪到范围 [-1, 1],这是与训练数据 x 相同的范围。正是模型在训练期间见过的内容与推断过程中遇到的内容之间的差异导致了这个问题。静态阈值处理在大型指导权重下是必要的,但随着权重的增加,仍然会导致图像过度饱和和细节减少。因此,作者引入了动态阈值处理。这项技术最初选择绝对像素值的某个百分位数,例如 80%。如果这个百分位数的值 s 超过 1(即超过 20% 的像素绝对值大于 1),则所有超出范围 [-s, s] 的像素都会被裁剪。之后,值会通过 s 进行缩放,将所有内容带入范围 [-1, 1]。在归一化之前丢弃极端像素有助于缓解过度饱和问题。

DeepFloyd IF 看起来与 Imagen 非常相似,但由于没有深入探讨该架构细节的论文,因此不确定是否存在我可能遗漏的重要修改。根据作者的说法,DeepFloyd IF 的表现优于原始的 Imagen。

应用与局限

DeepFloyd IF 可以用于所有上述应用。然而,与 Stable Diffusion 和 unCLIP 不同,目前用户在使用之前需要接受 DeepFloyd LICENSE AGREEMENT。

有用资源

流水线: Kandinsky

解开理论

Kandinsky 是一个 AI Forever 模型,它继承了 DALL·E-2 和潜在扩散的最佳实践,同时引入了一些新想法。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

来自 Kandinsky GitHub 页面。

就像 DALL·E-2 一样,Kandinsky 采用了一个先验模型(扩散映射)来基于 CLIP 文本嵌入预测 CLIP 图像嵌入。此外,类似于潜在扩散,这个扩散模型并不像 DALL·E-2/Imagen 那样在像素空间中操作,而是在潜在空间中

一个重要的区别是 Kandinsky 的最新版本 2.2 和 2.1 使用了XLM-RoBERTa作为文本编码器,从而使模型多语言

与 DALL·E-2 相比,前一个模型的输出不会直接进入解码器;而是首先导向一个潜在的扩散模型。

解码器MoVQ,一个类似于 VQGAN 的模型(参考附录 — VQGAN),通过引入空间条件归一化来解决将相似的相邻补丁映射到相同代码本索引的问题。这种处理方法防止了在相邻内容相似的区域出现重复的伪影。此外,该模型还结合了多通道量化,以增强其灵活性。第二阶段中,自回归变换器被一个显著更快的掩码生成图像变换器 (MaskGIT)所替代,这得益于其并行而非顺序的特性。

应用与局限性

我们已经看到 Kandinsky 的一个输出;这个模型无疑是目前最有前途的模型之一,并与当前最好的扩散模型竞争。它的使用和局限性类似于 Stable Diffusion。

有用的资源

流程: ControlNet

解开理论

ControlNet是一种条件生成扩散模型的技术,控制生成内容的结构。在一定程度上,尽管这两种技术是互补的,但它类似于增强版的 SDEdit。主要思想是自动生成条件输入,如边缘图、分割图、关键点等,然后教导扩散模型生成符合这些条件输入结构的输出。在原始 ControlNet 论文中,使用了 Stable Diffusion 作为基础,但该技术可以应用于任何模型。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

首先,创建了原始模型的副本。原始模型被冻结,而副本通过一系列零卷积与之链接。

零卷积简单来说就是一个 1x1 卷积,其中权重和偏置都初始化为零。这种初始化方式,加上原始模型的权重被冻结,确保了系统最初与起始模型完全相同,仅在逐渐开始使用条件引导生成的过程中,才会开始使用这些条件,而不会遗忘在广泛训练过程中最初学到的内容。

条件化涉及对输入进行某种形式的处理(通常是自动化的)。例如,我们可以使用坎尼边缘检测器从初始图像中提取边缘,并教导模型生成与原始图像结构一致但具有不同特征的变体,这些特征可以通过文本提示来引导。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

潜在的条件输入仅受限于我们的想象力;作者提到了一十多种,随着时间的推移,社区正在发明新的条件输入。举几个例子:边缘(例如,通过Canny提取),人体姿势(例如,通过OpenPifPafOpenPose提取),语义图,深度图等等。显然,在训练过程中,自动化提取对于加速初始数据集的创建非常重要。在推断过程中,没有限制可以手动绘制分割图或甚至草图我们想要的内容,因为在训练过程中可以使用HED边界检测和各种强大的数据增强或替代技术来自动提供类似的输入,模仿人类草图。

应用与局限性

ControlNet 是那些欣赏生成艺术的人必备的工具之一。可以用合理有限的资源从头开始训练自己的 ControlNet,但通常这并非必要,你可以使用社区已经训练好的 ControlNet。这种技术的主要局限在于条件化通常依赖于有一个结构类似于期望结果的起始图像,或者手动生成一个等效的条件。最后,值得注意的是,还可以结合多个 ControlNets,例如,将图像的一部分条件化为边缘,另一部分条件化为人体姿势。

有用的资源

流程:InstructPix2Pix

解开理论

InstructPix2Pix 是一种教学生成模型跟随人工编写指令进行图像编辑的方法。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

该方法包括三个阶段。首先,生成一组输入字幕、编辑指令和编辑字幕。然后,使用另一种叫做 Prompt-to-Prompt 的技术(见 附录 — Prompt-to-Prompt)生成与输入和编辑字幕相关联的图像对数据集。最后,训练生成模型以根据给定的指令产生请求的修改。

指令和编辑的字幕,如图所示,是半自动生成的GPT-3,由 OpenAI 开发的强大语言模型,经过了少量 LAION 字幕的微调,添加了手动制作的编辑指令和生成的编辑字幕。

到目前为止,我们拥有了所有的组件来使用 Prompt-to-Prompt 生成原始图像的变体。一个重要的方面是,根据给定的指令类型,生成的图像可能需要更多或更少地忠于原始图像。例如,考虑请求“将头发变成金色”和“将其变成米罗画”的区别。幸运的是,Prompt-to-Prompt 有一个参数可以调整对原始图像与提示之间的关注程度。不幸的是,这个参数因情况而异。

为了解决这个问题,InstructPix2Pix 为训练集中每个字幕生成了 100 对图像,改变这个参数。这些 然后通过 CLIP 基于度量进行过滤:CLIP 中的方向相似度。这个度量衡量两个图像(在 CLIP 空间)之间的变化与两个图像字幕之间的变化的一致性。除了提高生成数据集的质量外,这种过滤还增强了模型对 Prompt-to-Prompt 和 Stable Diffusion 失败的鲁棒性。

为了输入文本编辑指令,作者重新使用了最初为字幕设计的相同文本条件机制。同时,对于需要修改的输入图像,他们仅在第一个卷积层中添加了输入通道。

最后,他们采用一种 无分类器的扩散引导 形式,根据文本对图像进行加权,从而在遵循编辑指令时,能够对图像如何紧密符合输入图像进行一定控制。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

来自 InstructPix2Pix: Learning to Follow Image Editing Instructions 的公式 3。

应用与局限性

InstructPix2Pix 是一种非常有用的技术,当你希望通过文本修改图像时,而不显著改变与请求的修改无关的元素。这与生成两张图像,其中第二张图像只有略微修改的提示不同。显然,这种技术并非 100% 完美,且在要求更改视角、交换物体位置时会遇到问题,有时,尽管不如其他技术那样频繁,但它可能会导致图像发生意外的过度变化。

有用的资源

附录

CLIP

CLIP 的基本理念既简单又强大:训练两个 Transformer 编码器,一个用于图像,一个用于文本,在一个关联图像和文本的数据集上进行训练,以便当文本指代图像时产生相似的嵌入,而在其他情况下产生不同的嵌入。对于图中所示的矩阵,目标是最大化对角线上的标量积总和,并最小化对角线外的标量积:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

来自 OpenAI 关于 CLIP 的博客文章 的图像。

由于编码器的输出在进行标量积之前已被标准化,因此这些输出相当于测量两个向量之间的 余弦相似度,即嵌入“指向相同方向”的程度。

有用的资源

VQGAN

在本节中,我将介绍 VQGAN 并简要提及 VQVAE

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2. 来自 Taming Transformers for High-Resolution Image Synthesis

在上图中,如果我们只考虑 EG,我们得到的是一个自编码器。VQGAN 在 VQVAE 的基础上建立,并采用一种称为向量量化 (VQ) 的正则化技术。对于编码器输出 的每个空间位置,对应的向量(其大小取决于 中的通道数)会被替换为来自可学习“代码本”的最近向量。这有效地限制了推理过程中解码器的可能输入,使其只能是学习到的“代码”的组合,从而对潜在空间进行量化。

VQVAE 使用的损失函数 LVQ 由三部分组成。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

来自 Taming Transformers for High-Resolution Image Synthesis 的公式 4。

第一个是 重建损失Lrec;第二项是当代码本中的元素距离编码器输出较远时对代码本的惩罚。第三项,也称为“承诺损失”,在编码器的输出嵌入距离代码本中的代码较远时对编码器进行惩罚(我们希望编码器“承诺”某个代码本)。

VQGAN感知损失替代了重建损失。具体而言,它使用 Learned Perceptual Image Patch Similarity (LPIPS),利用预训练的 VGG16 网络从生成图像和目标图像中提取特征,然后计算这些特征之间的差异。

其次,它引入了一种对抗训练过程,使用 基于块的 判别器 D,旨在区分真实图像 (x) 和重建图像 ():

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

来自 Taming Transformers for High-Resolution Image Synthesis 的公式 5。

完整的目标如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

来自 Taming Transformers for High-Resolution Image Synthesis 的公式 6。

这里,λ 代表使用以下公式计算的自适应权重

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

来自 Taming Transformers for High-Resolution Image Synthesis 的公式 7。

Lrec(对于 VQGAN,等同于感知损失)的梯度相对于解码器最后一层的梯度增强时,这个权重会增加。相反,当 LGAN 的梯度增强时,权重会减少。在实践中,这意味着如果 LGAN 对解码器输出过于敏感,其重要性会降低。反之,如果感知损失 (Lrec) 展现出强烈的梯度,则 LGAN 的重要性会增加,从而确保两者之间的平衡。这种方法防止在一个项具有强梯度时另一个项被完全忽视,从而实现两个目标之间的平衡。

VQGAN 使用 两阶段方法。我们已经看到第一阶段,其中学习了编码器、代码本和解码器。在第二阶段,如论文标题所示的“驯化 Transformer”,该架构使用 Transformer 自回归地预测代码本中代码的索引。由于我们在训练过程中知道真实的索引(由编码器生成的),我们可以使用最大似然法训练 Transformer。在推断阶段,我们不使用编码器(因为我们没有输入图像,我们的目标是生成一张),而是利用训练好的 Transformer 生成索引序列,然后将其映射到代码,解码器将其转换为图像。

有用的资源

Prompt-to-Prompt

Prompt-to-Prompt 源于作者的一个关键观察:

我们深入分析了一个文本条件模型,并观察到交叉注意力层是控制图像空间布局与提示中每个词汇之间关系的关键。

基于此,该方法实质上涉及 操控交叉注意力图

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

例如,假设我们想要修改一张用“猫骑自行车”的提示生成的图像,将自行车替换为汽车,同时尽可能保持其他元素不变。在这种情况下,我们可以生成一张更新提示的新图像,但将交叉注意力图固定为之前提示的图像,其中与“汽车”相关的权重变为原本与“自行车”相关的权重。

使用这个框架,现在可以做的不仅仅是简单的词汇替换。我们可以用它来对给定的词汇给予更多或更少的强调,甚至可以在提示中添加之前不存在的部分。在这种情况下,我们仅对共享部分重用注意力图。

然而,保持注意力图固定可能会过度限制场景的几何形状,这对于某些提示修改可能变得过于严格。为了控制对修改的关注程度以及保留初始场景几何的程度,注入被限制在某个时间步 τ

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

见于 Prompt-to-Prompt 图像编辑与交叉注意力控制 第 7 页的方程式。

这确保了在初步捕捉到场景的整体构图后,模型可以在扩散过程的后续步骤中(如有需要)修改几何形状。

实用资源

结论

让我们总结一下本文所涵盖的内容。为了揭示 Diffusers 中最受欢迎的管道背后的原理,我们了解了扩散模型,分析了关键的模型,如 DDPM、Stable Diffusion、unCLIP(Karlo/DALL·E-2)、DeepFloyd IF(Imagen)和 Kandinsky。此外,我们还探索了图像生成的控制技术,如 SDEdit、ControlNet 或 InstructPix2Pix。为了真正理解这些技术,我们还研究了重要的非扩散模型,如 CLIP、VQGAN 或像 Prompt-to-Prompt (相关管道可能在你阅读本文时已准备好)。最后,扩散模型训练在某种程度上是一门艺术,因此我们还探讨了重要的技巧,如无分类器引导、偏移噪声、CLIP 过滤等。

希望你觉得这篇文章有帮助。欢迎以任何方式分享你的想法,我非常重视和考虑反馈。如果你想表示支持,分享这篇文章到社交网络是最好的方式。

致谢

首先,特别感谢 AI Coffee Break 的 Letitia Parcalabescu。她的帮助有两方面:首先,她的视频(查看一下,非常棒!)有助于刷新或澄清本文的一些概念;其次,她花时间阅读了初稿并提供了非常宝贵的反馈。关于这一点,我还要感谢 Towards Data Science 的审稿人,他们总是随时回答任何询问,并通过他们的见解提高了我写的文章的质量。最后,感谢你阅读到这里,真不容易 😊!

感谢你花时间阅读本文,欢迎留言或与我联系,分享你的想法或提出任何问题。要保持对我最新文章的更新,你可以关注我的 MediumLinkedInTwitter

[## 通过我的推荐链接加入 Medium - 马里奥·南塔奥·夏恩提·拉尔切尔

作为 Medium 的会员,你的会员费用的一部分将用于支持你阅读的作者,并且你可以完全访问所有故事……

medium.com](https://medium.com/@mnslarcher/membership?source=post_page-----a83d64348d90--------------------------------)

使用 python 对比苹果和橘子

原文:towardsdatascience.com/comparing-apples-to-oranges-with-python-51a122252ecf

通过水果沙拉示例展示预算优化

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Hamed Seyed-allaei

·发表于Towards Data Science ·8 分钟阅读·2023 年 10 月 6 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片属于作者

你可能认为将苹果与橘子进行比较是误导或不合逻辑的,但实际上,我们每天都会这么做——这是艰难决策的本质。选择一个苹果还是一个橘子是一种挑战,而不是决定一个苹果还是两个——两个明显更好。

生活中存在许多对比:自由安全、时间与金钱、即时满足与延迟满足、成长利润,等等。在这些场景中找到‘恰到好处’的区域或最佳点,通常需要一些优化。

那么,如果选择扩展到香蕉、覆盆子,并且还要考虑预算呢?这时,简单的决定演变成了一系列复杂的选择。随着我们深入优化和效用,我们将发现一种系统的方法如何能够应对这些细节,无论是在制作水果沙拉还是处理生活中的许多决策。

让我们用一个故事来展示这个想法。很久以前,我举办了一场派对,提供了一份非常受欢迎的水果沙拉。每份的配方如下:

|Ingredient|Quantity (gr)| Purpose   | Price per Kilo () |
|----------|-------------| -------   | ------------------ |
|Apple     | 50          | crunch    | 3                  |
|Orange    | 50          | juiciness | 4                  |
|Banana    | 50          | creamy    | 3                  |
|Raspberry | 50          | beautify  | 30                 |

每份的费用大约是2 €

现在,我被裁员了,资金紧张,但我仍然要招待相同的客人,并且他们的期望没有改变。不过,这次我的预算只有每份 1 欧元。

直接的想法可能是将量减半,但这样做是不可行的——每人 100 克是明显不足的,导致一半的客人空手而归。这个简单的解决方案显然是不理想的。

如果这个简单、次优的解决方案能让你满意,就到此为止。如果不满意,请继续阅读以获得更周到的解决方案。

如果你想看到最佳结果,并且厌恶数学和 python,可以跳到结果部分。如果你爱上了数学或 python,继续阅读。

钱不多但时间充裕,我决定系统地优化解决方案,以获得一个简单且最优的解决方案。

我们首先制定一个目标函数来衡量水果沙拉的享受程度。我们将使用Cobb-Douglas效用函数:

这个函数在经济学中很受欢迎,展示了不同因素如何贡献于效用或生产。在我们的情境下,它突出了每种水果的重要性——如果缺少任何一种水果,效用就会降到零,显示了每种成分的关键作用。在限制条件 a+o+b+r = 200 克下,函数在 a = o = b = r = 50 时最佳。不相信我?可以自己动手试试!

然而,现在我们的限制是预算,而不是份量大小。我们的目标是将每份价格保持在 1 欧元,将之前的预算减少一半。这给我们带来了方程:

为了在这一预算限制下微调每种水果的量,同时使用 Cobb-Douglas 效用函数,我们转向Lagrange 乘子法。该方法有助于在某些约束条件下找到函数的最大值或最小值。为了简化任务,我们在进入拉格朗日量之前对 U 取对数,得出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 1:展示了对数效用与每份价格的关系,绘制了随机重量样本的图形。每个价格点都有一个对应的峰值效用,展示了成本与满意度之间的权衡。橙色和红色点分别表示原始配方和最优配方的效用值。

现在,让我们将拉格朗日量引入:

对于我们的水果沙拉来说是:

拉格朗日量是一个了不起的数学构造,它展示了水果成本与带来的快乐之间的权衡。把它想象成一个跷跷板:一边是我们对更多水果的渴望,另一边是严苛的预算限制。拉格朗日量帮助我们找到那个甜蜜点,在不超预算的情况下最大化享受。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2:图中展示了拉格朗日量与每份价格的关系,描绘了成本与享受之间的平衡。红色和橙色点分别表示最优和原始配方,红点指示了在预算限制内享受最大化的峰值。

对于那些擅长数学的人,这里有一个有趣的小贴士:这个优化问题是凸的,意味着它只有一个峰值,确保了唯一的最优解,这也使得数值方法的工作变得简单。这个特点简化了我们对完美水果沙拉的追求。

现在你已经掌握了效用和拉格朗日的基本概念,让我们深入数学。如果数学不是你的强项,但你喜欢 Python,可以直接跳到 Python 部分。

对于那些准备深入细节的,继续阅读:

通过求偏导数并将其设置为零,我们得到一组方程,所有方程通过变量 λ 连接在一起。结果如下:

从这里可以明显看出,所有方程通过 λ 连接在一起,如下所示:

这给我们一个解耦的方程系统;每种水果都有自己的方程,但都通过 λ 交织在一起。这种结构帮助我们找到 aobr 的最佳值,因为我们可以将它们表示为 λ 的函数:

此外,效用和总价格作为 λ 和预算的函数如下:

这六个方程只通过 λ 连接在一起。在这里,λ 作为影子价格,揭示了通过稍微放松预算而获得的额外价值。一旦知道了 λ,每个方程都可以单独解决,它们是 互斥且完全穷尽 在这里,λ 是那个唯一的参数。

一个参数来统治它们,

一个参数来找出它们,

一个参数来汇聚它们,

并在黑暗中束缚它们。

将这些表达式代入预算约束方程后,我们发现 λ 等于 4。这导致我们得到每种成分的优化数量(以千克为单位):a=1/12o=1/16b=1/12,和 r=1/120,总计正好是 1 € 对应 238 克的一份。

这种方法将水果的甜味与预算的苦涩约束和谐统一,展示了深思熟虑的优化如何帮助应对财务障碍。

Python

深入了解下面的代码以可视化问题:

import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

def calculate_log_utility(weights):
    """
    Calculate the log utility for the given weights of the fruits.
    :param weights: Dictionary containing the weights of each fruit.
    :return: Sum of the logarithm of the weights of the fruits.
    """
    return np.sum([np.log(weights[fruit]) for fruit in weights], axis=0)

def calculate_total_price(weights, prices):
    """
    Calculate the total price per portion for the given weights and prices of the fruits.
    :param weights: Dictionary containing the weights of each fruit.
    :param prices: Dictionary containing the prices of each fruit per kg.
    :return: Total price per portion.
    """
    return np.sum([weights[fruit] * prices[fruit] for fruit in weights], axis=0)

def calculate_lagrangian(weights, prices, lambda_value, budget):
    """
    Calculate the Lagrangian for the given weights, prices, lambda, and budget.
    :param weights: Dictionary containing the weights of each fruit.
    :param prices: Dictionary containing the prices of each fruit per kg.
    :param lambda_value: The value of the Lagrange multiplier.
    :param budget: The budget per portion.
    :return: Calculated Lagrangian value.
    """
    log_utility = calculate_log_utility(weights)
    total_price = calculate_total_price(weights, prices)
    return log_utility - lambda_value * (total_price - budget)

# Set the style of seaborn for better visualization
sns.set(style="whitegrid")

# Define the random weights for each fruit in kg.
weights = {
    'apple': np.random.rand(1000) * 0.075 + 0.02, 
    'orange': np.random.rand(1000) * 0.075 + 0.02,
    'banana': np.random.rand(1000) * 0.075 + 0.02,
    'raspberry': np.random.rand(1000) * 0.075 + 0.0001,
}

# Define the prices for each fruit in euros per kg.
prices = {
    'apple': 3, 'orange': 4, 'banana': 3, 'raspberry': 30
}

# Define the optimum and original recipes in kg.
recipes = {
    'optimum': {'apple': 1/12, 'orange': 1/16, 'banana': 1/12, 'raspberry': 1/120},
    'original': {'apple': 0.05, 'orange': 0.05, 'banana': 0.05, 'raspberry': 0.05}
}

# Plot Log Utility Graph
plt.figure(figsize=(10, 6))
sns.scatterplot(x=calculate_total_price(weights, prices), 
                y=calculate_log_utility(weights), alpha=0.5, edgecolor=None)
plt.scatter([calculate_total_price(recipes['optimum'], prices)], 
            [calculate_log_utility(recipes['optimum'])], color='red', label='Optimum Recipe')
plt.scatter([calculate_total_price(recipes['original'], prices)], 
            [calculate_log_utility(recipes['original'])], color='orange', label='Original Recipe')
plt.title('Log Utility as a Function of Price per Portion')
plt.xlabel('Price per Portion (€)')
plt.ylabel('Log Utility')
plt.xlim(0.5, 3)
plt.ylim(-16, -9)
plt.legend(loc='upper left')
plt.show()

# Define lambda_value and budget for Lagrangian Graph
lambda_value = 4  # Given value of lambda
budget = 1  # Given budget per portion

# Plot Lagrangian Graph
plt.figure(figsize=(10, 6))
sns.scatterplot(x=calculate_total_price(weights, prices), 
                y=calculate_lagrangian(weights, prices, lambda_value, budget), alpha=0.5, edgecolor=None)
plt.scatter([calculate_total_price(recipes['optimum'], prices)], 
            [calculate_lagrangian(recipes['optimum'], prices, lambda_value, budget)], color='red', label='Optimum Recipe')
plt.scatter([calculate_total_price(recipes['original'], prices)], 
            [calculate_lagrangian(recipes['original'], prices, lambda_value, budget)], color='orange', label='Original Recipe')
plt.title('Lagrangian as a Function of Price per Portion')
plt.xlabel('Price per Portion (€)')
plt.ylabel('Lagrangian')
plt.xlim(0.5, 3)
plt.ylim(-18, -12)
plt.legend(loc='upper right')
plt.show()

现在,让我们用 sympy 解方程:

import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import sympy as sp

# Setting the style of seaborn for better visualization
sns.set()

# Amounts of each fruit
a, o, b, r = sp.symbols('a o b r', positive=True, real=True)  # Quantities of apples, oranges, bananas, and raspberries

# Prices of each fruit per kilo
P_a, P_o, P_b, P_r = sp.symbols('P_a P_o P_b P_r', positive=True, real=True)  # Prices for apples, oranges, bananas, and raspberries per kilo

# Total price of the ingredients
P = a*P_a + o*P_o + b*P_b + r*P_r

# Budget
B = sp.Symbol('B', positive=True, real=True)  # Budget for the fruit salad

# Lagrange multiplier
λ = sp.Symbol('λ', positive=True, real=True)  # Lagrange multiplier

# Cobb-Douglas utility function in its logarithmic form
U_log = sp.ln(a) + sp.ln(o) + sp.ln(b) + sp.ln(r)

# The Lagrangian
L = U_log - λ * (P - B)

# Derive the first order conditions
foc_a = sp.diff(L, a)
foc_o = sp.diff(L, o)
foc_b = sp.diff(L, b)
foc_r = sp.diff(L, r)
foc_λ = sp.diff(L, λ)

# Solve for λ and the optimized quantities of each ingredient
solution = sp.solve((foc_a, foc_o, foc_b, foc_r, foc_λ), (a, o, b, r, λ), dict=True)

solution

运行第二段代码可以揭示出为满足我们的预算并最大化效用所需的确切水果数量。

结果

优化显示了每种成分的最佳量:

  • 苹果:83 克

  • 橙子:62 克

  • 香蕉:83 克

  • 覆盆子:8 克

总份量大小:0.238 kg,成本正好是 1 欧元。

我们保留了沙拉中的所有水果角色,聪明地调整了它们的比例。结果?通过将昂贵的覆盆子份额重新分配给更经济的苹果和香蕉,份量大幅增加了 20%。相当巧妙,不是吗?

这种方法创建了一个灵活的公式,能够适应价格变化、配方演变或预算调整,几乎无需额外努力。关键在于“一个参数” —— 我们平衡成本和满意度的数学钥匙。

现在,你的商业场景是否也反映了这个水果沙拉难题?需要类似的优化吗?联系我,我会为你制定一个解决方案,以在预算范围内平衡你的商业成分。

读得开心吗?点击下面的点赞按钮 —— 点赞越多,我越开心。这是我快乐的公式:

在这里,n 是拍手的次数,而“!”(阶乘)意味着将从 1 到 n 的所有正整数相乘。因此,5! 就是 54321 = 120

想分享吗?把这篇文章传递出去吧。你的分享将传播有趣的优化故事的快乐!

我很想听听你的想法,或者看看你在评论中分享的有趣优化点子。谁知道呢,你独特的问题可能会激发我下一篇故事的灵感!

与此同时,查看我另一篇有趣的文章:

## 关于人工生命风险的声明

人工生命(AL)专家和网络人物表达了他们对 AL 风险的担忧。

medium.com

比较激光衍射与咖啡颗粒成像

原文:towardsdatascience.com/comparing-laser-diffraction-to-imaging-of-coffee-particles-9fac2bec2464?source=collection_archive---------21-----------------------#2023-04-11

咖啡数据科学

分裂豆子

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Robert McKeon Aloe

·

关注 发表在 数据科学前沿 ·4 分钟阅读·2023 年 4 月 11 日

在过去两年里,我一直在 使用成像测量咖啡粉分布。这对于深入了解咖啡研磨机的工作情况非常有用,我也非常喜欢图像处理的挑战。然而,我一直好奇这些测量与激光衍射的比较。激光衍射通常非常昂贵,但我有机会运行一些样本并与我的方法进行比较。

激光衍射用于粒子分布,利用激光和衍射光栅更准确地测量小颗粒。它输出粒子的平均直径,典型的激光粒子分析仪使用进料管顺序测量颗粒。然后基于颗粒体积制作概率分布。这些机器的价格约为 10 万美元,这对于咖啡爱好者的探索来说并不经济。

咖啡颗粒也可以通过成像进行测量。这涉及到将一份咖啡样本放在纸上,展开/解聚咖啡渣,然后拍摄校准图像。校准图像意味着图像平面可以转换为真实的尺寸测量。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

所有图片均由作者提供

我非常喜欢进行形状分析,我认为这比粒子直径更具信息性。然而,我希望能同时拥有激光成像的高精度和相机成像的粒子形状信息。

数据

让我们看看一些数据。我有磨碎的咖啡和用过的咖啡。用过的咖啡团聚得更少,但它并不总是代表研磨机的情况。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

所有使用的箱子

在查看所有箱子时,很明显,累积分布不能在不去除激光技术的较低箱子的情况下进行查看。这样可以有更多的对齐,但更好的测试也可以筛选咖啡渣,以确保激光和成像之间的测量是相同的颗粒类型。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在我通常的成像测量中,我使用最小直径,因为这是筛选器测量的内容。然而,为了更接近激光测量,我考虑了最小直径和平均直径。对于大于 100 微米的大颗粒,平均直径似乎更合适,但对于小于 100 微米的颗粒,最小直径更为适合。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我将坚持使用平均直径,因为这更接近激光测量提供的结果。我可以查看拍摄前后的研磨。成像数据表明拍摄后的颗粒变得更细,而激光测量则显示相反的结果。我想知道这是否与较低读数的准确性有关。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我绘制了粒子的累积百分比,并将它们相互比较,如果它们相同,这将是一条平坦的线。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这些结果仍然显示出性能差距,但成像似乎在激光数据范围内。我通常不会将这些分布与激光衍射测量进行比较,因此在相同的方法下,变量控制得比与其他测量类型比较时更好。

我仍然更喜欢激光测量提供的 3D 形状精度以及实际的 3D 形状,以更好地理解磨豆机,如果有人制作一个桌面激光衍射粒子分析仪,这或许有一天会实现。

如果你喜欢,可以关注我在 TwitterYouTubeInstagram 上,我会发布关于不同机器的浓缩咖啡镜头和相关内容的视频。你还可以在 LinkedIn 找到我。你还可以在 MediumSubscribe 上关注我。

我的进一步阅读:

我的书

我的链接

浓缩咖啡文章合集

工作和学校故事合集

比较 Python 中的列表推导式与内置函数:哪种更好?

原文:towardsdatascience.com/comparing-list-comprehensions-vs-built-in-functions-in-python-which-is-better-1e2c9646fafe

对语法、可读性和性能的深入分析

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Thomas A Dorfer

·发表于 Towards Data Science ·阅读时长 9 分钟·2023 年 3 月 21 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

作者提供的图片。

你是否曾在雨天通过 Netflix 滚动,感到被无尽的电影和节目选择所压倒?

在编程中,选择的悖论可能同样令人不知所措。面对如此众多的库和框架,提供了无数种实现相同目标的方法,容易在选择的海洋中迷失方向。

在 Python 中,这种情况通常出现在程序员需要在函数式编程方法(如内置函数 map()filter()reduce())与更具 Python 风格的列表推导式之间做选择时。

在这篇文章中,我们将通过语法、可读性和性能的视角探讨这两种不同方法的优缺点。

列表推导式

在 Python 中,列表推导式是一种简洁的方法,它基于已存在的列表生成一个新列表。简单来说,它本质上是一个for 循环的一行代码,并可以在末尾包含一个if 条件。语法可以分解为如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

作者提供的图片。

假设我们有一个名为 numbers 的数字列表,我们希望从中选取偶数并对其平方。现在,老旧的方式是这样的:

squared_numbers = []
for number in numbers:
    if number % 2 == 0:
        squared = number ** 2
        squared_numbers.append(squared)

然而,使用列表推导式,我们可以在一行代码中完成这一操作:

squared_numbers = [i**2 for i in numbers if i % 2 == 0]

无论哪种方式都能得到相同的结果,但列表推导式提供了一个更清晰、更可读的解决方案,因为其语法字面上是:*“*做这个 对于 每个值 这个列表 如果 这个条件 满足”

一般来说,列表推导式通常比常规的 for 循环更快,因为它们不需要在每次迭代时查找列表并调用其 append 方法。

现在我们对列表推导式有了比较好的理解,接下来我们来看看它们与一些常用的内置函数(如 map()filter()reduce())相比如何。这就是我之前提到的选择悖论。程序员往往知道这些方法的存在,但该选择哪一个呢?

让我们逐一了解每个内置函数,并将它们与 Pythonic 对应的列表推导式进行比较。

Map

如果你的目标是对可迭代对象(如列表)中的每一项应用转换函数,那么 map() 函数是一个很好的起点。其语法相当简单,只需要两个输入参数:(1) 一个转换函数,以及 (2) 一个可迭代对象(即你的输入列表)。

假设我们有一个与欧元对应的数字列表,我们希望将它们转换为美元。这可以通过以下方式完成:

>>> eur = [1, 2, 3, 4, 5]
>>> usd = list(map(lambda x: x / 0.939276, eur))
>>> usd
[1.0646497940967299,
 2.1292995881934598,
 3.1939493822901897,
 4.2585991763869195,
 5.323248970483649]

注意,我们必须在这里明确指定 list() 函数,因为 map() 本地返回的是一个迭代器——一个 map 对象。

还要注意,map() 允许你使用这些匿名的、即兴的 lambda 函数,这些函数允许你即时定义一个函数。如果你想了解更多关于 lambda 函数、它们的语法以及如何使用它们的内容,可以查看以下文章:

## 如何在数据科学中有效使用 Python 的 Lambda 函数

对其语法、功能以及在数据科学中的适用性的介绍

towardsdatascience.com

与列表推导式的比较

你可能已经注意到,相同的任务也可以通过列表推导式来完成。那么让我们看看它们在可读性和性能方面的比较。

具体来说,我们将讨论三种场景:(1) 列表推导式,(2) 使用预定义输入函数的 map(),以及 (3) 使用即兴的 lambda 函数的 map()

# predefined conversion function
def eur_to_usd(x):
    return x / 0.939276
>>> lst = list(range(1000000))

# list comprehension
>>> %timeit -r 10 -n 10 [i / 0.939276 for i in lst]
163 ms ± 4.96 ms per loop (mean ± std. dev. of 10 runs, 10 loops each)

# map with predefined input function
>>> %timeit -r 10 -n 10 list(map(eur_to_usd, lst))
197 ms ± 4.33 ms per loop (mean ± std. dev. of 10 runs, 10 loops each)

# map with lambda function
>>> %timeit -r 10 -n 10 list(map(lambda x: x / 0.939276, lst))
204 ms ± 4.28 ms per loop (mean ± std. dev. of 10 runs, 10 loops each)

就简洁性和可读性而言,列表推导式在这里似乎赢得了比赛。程序员的意图立即显现出来,不需要额外的关键字或定义额外的函数。然而,值得注意的是,对于更复杂的操作,可能需要定义单独的转换函数,这将削弱列表推导式通常因其可读性而获得的一些优势。

就性能而言,上述示例清楚地表明,列表推导式是最快的,其次是使用预定义输入函数的map(),最后是使用 lambda 函数的map()

关于使用临时 lambda 函数的问题是:它会为输入列表中的每个项目调用,导致计算开销,因为 lambda 函数对象的创建和销毁,最终导致性能下降。相比之下,预定义函数经过优化并存储在内存中,这使得执行更为高效。

底线

在性能方面,列表推导式明显优于map()。此外,它们的语法易于阅读,通常被认为更直观,并且被认为比源自函数式编程的map()更具 Python 风格。

过滤

filter()函数允许你根据给定条件选择可迭代对象的一个子集。与map()类似,它需要两个输入参数:(1)过滤函数,通常是lambda 函数,以及(2)一个可迭代对象。

以下是一个示例,我们过滤掉所有奇数,只保留偶数:

>>> numbers = [1, 2, 3, 4, 5]
>>> filtered = list(filter(lambda x: x % 2 == 0, numbers))
>>> filtered
[2, 4]

类似于map(),我们必须明确声明我们希望返回一个列表,因为filter()原生返回一个迭代器对象。

与列表推导式的比较

让我们看看内置filter()函数的性能差异,再次使用预定义输入函数和 lambda 函数,并与列表推导式进行比较。

# predefined filter function
def fil(x):
    if x % 2 == 0:
        return True
    else:
        return False
>>> lst = list(range(1000000))

# list comprehension
>>> %timeit -r 10 -n 10 [i for i in lst if i % 2 == 0]
84.6 ms ± 2.24 ms per loop (mean ± std. dev. of 10 runs, 10 loops each)

# filter with predefined filter function
>>> %timeit -r 10 -n 10 list(filter(fil, lst))
134 ms ± 6.39 ms per loop (mean ± std. dev. of 10 runs, 10 loops each)

# filter with lambda function
>>> %timeit -r 10 -n 10 list(filter(lambda x: x % 2 == 0, lst))
159 ms ± 6.67 ms per loop (mean ± std. dev. of 3 runs, 10 loops each)

就可读性而言,对于map()的说法同样适用于filter():列表推导式相当易于阅读,不需要任何预定义或临时函数或额外的关键字。然而,有人认为使用filter()函数会立即展示程序员的意图,即过滤某物,可能比列表推导式更直接。当然,这是一项高度主观的事项,取决于个人的偏好和品味。

就性能而言,我们看到的结果与map()获得的类似。列表推导式是最快的,其次是使用预定义过滤函数的filter(),最后是使用临时 lambda 函数的filter()。这再次是由于 lambda 函数需要在运行时创建新函数对象所带来的开销。

底线

列表推导式的性能超过其函数式filter()对应物——几乎是 2 倍,并且通常被认为更具 Python 风格。然而,易读性在这方面略显主观。有些人喜欢列表推导式直观和 Pythonic 的方式,而另一些人则偏爱使用filter()函数,因为它清晰地传达了其功能和程序员的意图。

减少

最后,让我们看一下reduce()。这个内置函数通常用于需要在多个步骤中累积单一结果的情况。它还接受两个输入参数:(1)一个归约函数,和(2)一个可迭代对象。

让我们通过一个示例来使其功能更清晰。在这个例子中,我们希望计算一个整数列表的乘积:

>>> from functools import reduce
>>> integers = [1, 2, 3, 4, 5]
>>> reduce(lambda x, y: x * y, integers)
120

再次,我们使用一个 lambda 来定义我们的归约函数,这里是对整数列表进行简单的滚动乘法。这会执行以下计算:1 x 2 x 3 x 4 x 5 = 120。

与列表推导式的比较

使用列表推导式达到相同的目标这次有点棘手,需要一些额外的步骤,例如初始化变量和使用海象运算符

>>> integers = [1, 2, 3, 4, 5]
>>> product = 1
>>> [product := product * num for num in numbers]
>>> product
120

虽然仍然可以通过列表推导式获得相同的结果,但这些额外的步骤显著降低了代码的可读性。

此外,现在还有多种低代码替代方法,例如math.prod()

>>> from math import prod
>>> integers = [1, 2, 3, 4, 5]
>>> prod(integers)
120

然而,在性能方面,这两者之间似乎没有重大区别:

>>> integers = list(range(1, 10001))

# using reduce
>>> %timeit -r 10 -n 100 reduce(lambda x, y: x * y, integers)
24.5 ms ± 299 µs per loop (mean ± std. dev. of 10 runs, 100 loops each)

# using math.prod
>>> from math import prod
>>> %timeit -r 10 -n 100 prod(integers)
23.8 ms ± 707 µs per loop (mean ± std. dev. of 10 runs, 100 loops each)

关键点

在 Python 中,reduce()用于对列表中的值对进行滚动计算的使用在逐年减少,主要是因为有更高效和直观的替代方法,如math.prod()reduce()和列表推导式在这里并没有提供一个清晰的语法,这使得读者很难快速理解代码。

PS:如果你仍然是reduce()的频繁用户,我很想在评论中了解你的使用案例!

结论

尽管在其他语言中不如其他语言那样普遍,map()filter()以及偶尔使用的reduce()仍然在基于 Python 的应用程序中使用。然而,列表推导式由于其更直观的语法被视为更具 Python 风格,并且在大多数情况下,可以替代map()filter()函数,同时还带来明显的性能提升。

相比之下,reduce()函数的特性使其不容易被列表推导式替代。然而,如上所述,它们可以被低代码替代方法如math.prod()函数替代。

喜欢这篇文章吗?

让我们联系一下!你可以在TwitterLinkedIn找到我。

如果你喜欢支持我的写作,你可以通过Medium 会员来做到这一点,这将为你提供访问我所有故事以及 Medium 上其他成千上万作家的权限。

[## 通过我的推荐链接加入 Medium — Thomas A Dorfer

阅读 Thomas A Dorfer 的每一篇故事(以及 Medium 上其他成千上万的作家的故事)。你的会员费直接支持…

medium.com](https://medium.com/@thomasdorfer/membership?source=post_page-----1e2c9646fafe--------------------------------)

比较异常值检测方法

原文:towardsdatascience.com/comparing-outlier-detection-methods-956f4b097061?source=collection_archive---------1-----------------------#2023-12-16

使用 2023 年大联盟棒球的击球统计数据

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 约翰·安德鲁斯

·

关注 发布于 Towards Data Science ·12 分钟阅读·2023 年 12 月 16 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Shohei Ohtani,照片由 Erik Drost 拍摄,发布在 FlikrCC BY 2.0

异常检测是一种无监督机器学习任务,用于识别给定数据集中异常值(不寻常的观测值)。这个任务在许多实际场景中非常有用,因为我们可用的数据集已经被异常值“污染”。Scikit-learn 实现了几种异常检测算法,在我们拥有未污染的基线时,我们也可以使用这些算法进行新颖性检测,这是一种半监督任务,预测新观测值是否为异常值。

概述

我们将比较的四种异常检测算法是:

  • 椭圆包络适用于低维度的正态分布数据。顾名思义,它使用多变量正态分布来创建一种距离度量,以将异常值与正常值分离。

  • 局部异常因子是通过将观测值的局部密度与其邻居的密度进行比较来判断异常值。密度远低于邻居的观测值被视为异常值。

  • 一类支持向量机(SVM)与随机梯度下降(SGD)是一个 O(n)的近似解法。请注意,O(n²)的一类 SVM在我们的小示例数据集上效果很好,但可能对你的实际用例不够实用。

  • 隔离森林是一种基于树的方法,其中异常值通过随机分割比正常值更快地被隔离。

由于我们的任务是无监督的,我们没有基准真值来比较这些算法的准确性。相反,我们希望了解它们的结果(特别是球员排名)之间的差异,并对它们的行为和局限性有一些直觉,以便知道何时优先选择某一算法。

让我们使用 2023 年大联盟棒球(MLB)赛季的两个击球手表现指标来比较这些技术:

  • 上垒率(OBP),指击球手在每次打击机会中通过击球、走步或被投球击中到达垒位的比率

  • 击球率(SLG),每次击球的平均总垒数

还有许多更复杂的击球表现指标,包括 OBP 加 SLG(OPS)、加权上垒率(wOBA)和调整后的加权创造分(WRC+)。然而,我们会看到,除了 常用 且易于理解之外,OBP 和 SLG 有中等相关性且近似正态分布,使它们非常适合用于这次比较。

数据集准备

我们使用 pybaseball 包来获取击球数据。这个 Python 包在 MIT 许可证下发布,并从 Fangraphs.comBaseball-Reference.com 及其他来源获取数据,这些来源又从大联盟棒球正式记录中获得数据。

我们使用 pybaseball 的 2023 年击球统计数据,这些数据可以通过 batting_stats(FanGraphs)或 batting_stats_bref(Baseball Reference)获得。结果是,从 Fangraphs 获得的球员名字 格式更为正确,但 Baseball Reference 中的球员团队和联赛在交易球员的情况下格式更佳。为了获得可读性更高的数据集,我们实际上需要合并三个表:FanGraphs、Baseball Reference 和一个键查找表。

from pybaseball import (cache, batting_stats_bref, batting_stats, 
                        playerid_reverse_lookup)
import pandas as pd

cache.enable()  # avoid unnecessary requests when re-running

MIN_PLATE_APPEARANCES = 200

# For readability and reasonable default sort order
df_bref = batting_stats_bref(2023).query(f"PA >= {MIN_PLATE_APPEARANCES}"
                                        ).rename(columns={"Lev":"League",
                                                          "Tm":"Team"}
                                                )
df_bref["League"] = \
  df_bref["League"].str.replace("Maj-","").replace("AL,NL","NL/AL"
                                                  ).astype('category')

df_fg = batting_stats(2023, qual=MIN_PLATE_APPEARANCES)

key_mapping = \
  playerid_reverse_lookup(df_bref["mlbID"].to_list(), key_type='mlbam'
                         )[["key_mlbam","key_fangraphs"]
                          ].rename(columns={"key_mlbam":"mlbID",
                                            "key_fangraphs":"IDfg"}
                                  )

df = df_fg.drop(columns="Team"
               ).merge(key_mapping, how="inner", on="IDfg"
                      ).merge(df_bref[["mlbID","League","Team"]],
                              how="inner", on="mlbID"
                             ).sort_values(["League","Team","Name"])

数据探索

首先,我们注意到这些指标在均值和方差上有所不同,并且有中等程度的相关性。我们还注意到,每个指标相当对称,具有接近均值的中位数值。

print(df[["OBP","SLG"]].describe().round(3))

print(f"\nCorrelation: {df[['OBP','SLG']].corr()['SLG']['OBP']:.3f}")
 OBP      SLG
count  362.000  362.000
mean     0.320    0.415
std      0.034    0.068
min      0.234    0.227
25%      0.300    0.367
50%      0.318    0.414
75%      0.340    0.460
max      0.416    0.654

Correlation: 0.630

让我们使用以下内容来可视化这个联合分布:

  • 球员的散点图,以国家联盟(NL)与美国联盟(AL)为颜色区分

  • 球员的双变量核密度估计(KDE)图,使用高斯核平滑散点图以估计密度

  • 每个指标的边际 KDE 图

import matplotlib.pyplot as plt
import seaborn as sns

g = sns.JointGrid(data=df, x="OBP", y="SLG", height=5)
g = g.plot_joint(func=sns.scatterplot, data=df, hue="League",
                 palette={"AL":"blue","NL":"maroon","NL/AL":"green"},
                 alpha=0.6
                )
g.fig.suptitle("On-base percentage vs. Slugging\n2023 season, min "
               f"{MIN_PLATE_APPEARANCES} plate appearances"
              )
g.figure.subplots_adjust(top=0.9)
sns.kdeplot(x=df["OBP"], color="orange", ax=g.ax_marg_x, alpha=0.5)
sns.kdeplot(y=df["SLG"], color="orange", ax=g.ax_marg_y, alpha=0.5)
sns.kdeplot(data=df, x="OBP", y="SLG",
            ax=g.ax_joint, color="orange", alpha=0.5
           )
df_extremes = df[ df["OBP"].isin([df["OBP"].min(),df["OBP"].max()]) 
                 | df["OPS"].isin([df["OPS"].min(),df["OPS"].max()])
                ]

for _,row in df_extremes.iterrows():
    g.ax_joint.annotate(row["Name"], (row["OBP"], row["SLG"]),size=6,
                      xycoords='data', xytext=(-3, 0),
                        textcoords='offset points', ha="right",
                      alpha=0.7)
plt.show()

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

散点图的右上角显示了一个击球优秀的聚集群,与 SLG 和 OBP 分布的上尾部重合。这一小群球员在上垒 击出额外垒数方面表现优异。我们将他们视为异常值(因为他们与大多数球员群体的距离)还是内群体(因为他们彼此靠近)的判断取决于我们选择的算法定义。

应用异常值检测算法

Scikit-learn 的离群点检测算法通常具有fit()predict()方法,但也有例外,并且不同算法的参数也有所不同。我们将逐个考虑每种算法,但我们将每种算法拟合到每个玩家的特征矩阵(n=2,m=453)。然后,我们将对每个玩家以及跨越每个特征范围的值网格进行评分,以帮助我们可视化预测函数。

要可视化决策边界,我们需要采取以下步骤:

  1. 创建一个 2D 的meshgrid输入特征值。

  2. decision_function应用于meshgrid上的每一点,这需要将网格展开。

  3. 将预测结果重新调整为网格形状。

  4. 绘制预测结果。

我们将使用 200x200 的网格来覆盖现有的观察结果以及一些填充,但你可以根据所需的速度和分辨率调整网格。

import numpy as np

X = df[["OBP","SLG"]].to_numpy()

GRID_RESOLUTION = 200

disp_x_range, disp_y_range = ( (.6*X[:,i].min(), 1.2*X[:,i].max()) 
                               for i in [0,1]
                             )
xx, yy = np.meshgrid(np.linspace(*disp_x_range, GRID_RESOLUTION), 
                     np.linspace(*disp_y_range, GRID_RESOLUTION)
                    )
grid_shape = xx.shape
grid_unstacked = np.c_[xx.ravel(), yy.ravel()]

椭圆包络

椭圆包络的形状由数据的协方差矩阵决定,该矩阵在主对角线上给出特征i的方差,在[i,j]位置给出特征ij的协方差。由于协方差矩阵对离群点敏感,该算法使用了最小协方差行列式(MCD)估计器,该估计器推荐用于单峰对称分布,通过random_state输入确定的洗牌以确保可重复性。这种稳健的协方差矩阵以后会再次派上用场。

因为我们想比较离群点分数的排名,而不是二元的离群点/内群体分类,我们使用decision_function对玩家进行评分。

from sklearn.covariance import EllipticEnvelope

ell = EllipticEnvelope(random_state=17).fit(X)
df["outlier_score_ell"] = ell.decision_function(X)
Z_ell = ell.decision_function(grid_unstacked).reshape(grid_shape)

本地离群因子

这种隔离度量方法基于 k 近邻(KNN)。我们计算每个观察值到其最近邻的总距离来定义局部密度,然后将每个观察值的局部密度与其邻居的密度进行比较。局部密度远低于其邻居的观察值被视为离群点。

选择邻居的数量: 在 KNN 中,一个经验法则是让 K = sqrt(N),其中 N 是你的观察数量。根据这个规则,我们得到一个接近 20 的 K(这恰好是 LOF 的默认 K)。你可以增加或减少 K 以减少过拟合或欠拟合。

K = int(np.sqrt(X.shape[0]))

print(f"Using K={K} nearest neighbors.")
Using K=19 nearest neighbors.

选择距离度量: 注意到我们的特征是相关的且具有不同的方差,因此欧几里得距离意义不大。我们将使用马哈拉诺比斯距离,它考虑了特征的尺度和相关性。

在计算马哈拉诺比斯距离时,我们将使用稳健的协方差矩阵。如果我们没有通过椭圆包络计算它,我们可以直接计算它。

from scipy.spatial.distance import pdist, squareform

# If we didn't have the elliptical envelope already,
# we could calculate robust covariance:
#   from sklearn.covariance import MinCovDet
#   robust_cov = MinCovDet().fit(X).covariance_
# But we can just re-use it from elliptical envelope:
robust_cov = ell.covariance_

print(f"Robust covariance matrix:\n{np.round(robust_cov,5)}\n")

inv_robust_cov = np.linalg.inv(robust_cov)

D_mahal = squareform(pdist(X, 'mahalanobis', VI=inv_robust_cov))

print(f"Mahalanobis distance matrix of size {D_mahal.shape}, "
      f"e.g.:\n{np.round(D_mahal[:5,:5],3)}...\n...\n")
Robust covariance matrix:
[[0.00077 0.00095]
 [0.00095 0.00366]]

Mahalanobis distance matrix of size (362, 362), e.g.:
[[0\.    2.86  1.278 0.964 0.331]
 [2.86  0\.    2.63  2.245 2.813]
 [1.278 2.63  0\.    0.561 0.956]
 [0.964 2.245 0.561 0\.    0.723]
 [0.331 2.813 0.956 0.723 0\.   ]]...
...

**拟合局部异常因子:**请注意,使用自定义距离矩阵需要将metric="precomputed"传递给构造函数,然后将距离矩阵本身传递给fit方法。(有关更多详细信息,请参见文档。)

还需注意,与其他算法不同的是,在 LOF 中,我们被指示不要使用score_samples方法来为现有观察值打分;此方法仅应用于新颖性检测。

from sklearn.neighbors import LocalOutlierFactor

lof = LocalOutlierFactor(n_neighbors=K, metric="precomputed", novelty=True
                        ).fit(D_mahal)

df["outlier_score_lof"] = lof.negative_outlier_factor_

**创建决策边界:**由于我们使用了自定义距离度量,我们还必须计算网格中每个点到原始观察值的自定义距离。在之前,我们使用空间度量pdist来计算单一集合中每个成员的成对距离,但现在我们使用cdist来返回第一个输入集合中每个成员到第二个集合中每个成员的距离。

from scipy.spatial.distance import cdist

D_mahal_grid = cdist(XA=grid_unstacked, XB=X, 
                     metric='mahalanobis', VI=inv_robust_cov
                    )
Z_lof = lof.decision_function(D_mahal_grid).reshape(grid_shape)

支持向量机(SGD-一类 SVM)

SVM 使用核技巧将特征转化为更高维度,从而可以识别分隔超平面。径向基函数(RBF)核要求输入数据进行标准化,但正如文档中提到的,标准化器对异常值敏感,因此我们将使用RobustScaler。我们将缩放后的输入数据传递到 Nyström 核近似中,正如文档中对SGDOneClassSVM的建议。

from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import RobustScaler
from sklearn.kernel_approximation import Nystroem
from sklearn.linear_model import SGDOneClassSVM

suv = make_pipeline(
            RobustScaler(),
            Nystroem(random_state=17),
            SGDOneClassSVM(random_state=17)
).fit(X)

df["outlier_score_svm"] = suv.decision_function(X)

Z_svm = suv.decision_function(grid_unstacked).reshape(grid_shape)

Isolation Forest

这种基于树的方法进行隔离测量时会执行随机递归分区。如果隔离给定观察值所需的平均分割次数,则该观察值被认为是更强的候选异常值。与随机森林和其他基于树的模型一样,Isolation Forest 不假设特征是正态分布的,也不要求对其进行缩放。默认情况下,它会构建 100 棵树。我们的示例只使用了两个特征,因此我们不启用特征抽样。

from sklearn.ensemble import IsolationForest

iso = IsolationForest(random_state=17).fit(X)

df["outlier_score_iso"] = iso.score_samples(X)

Z_iso = iso.decision_function(grid_unstacked).reshape(grid_shape)

结果:检查决策边界

请注意,这些模型的预测具有不同的分布。我们应用QuantileTransformer使它们在给定网格上更具可视比较性。请参阅文档了解更多信息:

注意,这一变换是非线性的。它可能会扭曲在相同尺度下测量的变量之间的线性相关性,但使得在不同尺度下测量的变量更直接地可比较。

from adjustText import adjust_text
from sklearn.preprocessing import QuantileTransformer

N_QUANTILES = 8 # This many color breaks per chart
N_CALLOUTS=15  # Label this many top outliers per chart

fig, axs = plt.subplots(2, 2, figsize=(12, 12), sharex=True, sharey=True)

fig.suptitle("Comparison of Outlier Identification Algorithms",size=20)
fig.supxlabel("On-Base Percentage (OBP)")
fig.supylabel("Slugging (SLG)")

ax_ell = axs[0,0]
ax_lof = axs[0,1]
ax_svm = axs[1,0]
ax_iso = axs[1,1]

model_abbrs = ["ell","iso","lof","svm"]

qt = QuantileTransformer(n_quantiles=N_QUANTILES)

for ax, nm, abbr, zz in zip( [ax_ell,ax_iso,ax_lof,ax_svm], 
                            ["Elliptic Envelope","Isolation Forest",
                             "Local Outlier Factor","One-class SVM"], 
                            model_abbrs,
                            [Z_ell,Z_iso,Z_lof,Z_svm]
                           ):
    ax.title.set_text(nm)
    outlier_score_var_nm = f"outlier_score_{abbr}"

    qt.fit(np.sort(zz.reshape(-1,1)))
    zz_qtl = qt.transform(zz.reshape(-1,1)).reshape(zz.shape)

    cs = ax.contourf(xx, yy, zz_qtl, cmap=plt.cm.OrRd.reversed(), 
                     levels=np.linspace(0,1,N_QUANTILES)
                    )
    ax.scatter(X[:, 0], X[:, 1], s=20, c="b", edgecolor="k", alpha=0.5)

    df_callouts = df.sort_values(outlier_score_var_nm).head(N_CALLOUTS)
    texts = [ ax.text(row["OBP"], row["SLG"], row["Name"], c="b",
                      size=9, alpha=1.0) 
             for _,row in df_callouts.iterrows()
            ]
    adjust_text(texts, 
                df_callouts["OBP"].values, df_callouts["SLG"].values, 
                arrowprops=dict(arrowstyle='->', color="b", alpha=0.6), 
                ax=ax
               )

plt.tight_layout(pad=2)
plt.show()

for var in ["OBP","SLG"]:
    df[f"Pctl_{var}"] = 100*(df[var].rank()/df[var].size).round(3)

model_score_vars = [f"outlier_score_{nm}" for nm in model_abbrs]  
model_rank_vars = [f"Rank_{nm.upper()}" for nm in model_abbrs]

df[model_rank_vars] = df[model_score_vars].rank(axis=0).astype(int)

# Averaging the ranks is arbitrary; we just need a countdown order
df["Rank_avg"] = df[model_rank_vars].mean(axis=1)

print("Counting down to the greatest outlier...\n")
print(
    df.sort_values("Rank_avg",ascending=False
                  ).tail(N_CALLOUTS)[["Name","AB","PA","H","2B","3B",
                                      "HR","BB","HBP","SO","OBP",
                                      "Pctl_OBP","SLG","Pctl_SLG"
                                     ] + 
                             [f"Rank_{nm.upper()}" for nm in model_abbrs]
                            ].to_string(index=False)
)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Counting down to the greatest outlier...

            Name  AB  PA   H  2B  3B  HR  BB  HBP  SO   OBP  Pctl_OBP   SLG  Pctl_SLG  Rank_ELL  Rank_ISO  Rank_LOF  Rank_SVM
   Austin Barnes 178 200  32   5   0   2  17    2  43 0.256       2.6 0.242       0.6        19         7        25        12
   J.D. Martinez 432 479 117  27   2  33  34    2 149 0.321      52.8 0.572      98.1        15        18         5        15
      Yandy Diaz 525 600 173  35   0  22  65    8  94 0.410      99.2 0.522      95.4        13        15        13        10
       Jose Siri 338 364  75  13   2  25  20    2 130 0.267       5.5 0.494      88.4         8        14        15        13
       Juan Soto 568 708 156  32   1  35 132    2 129 0.410      99.2 0.519      95.0        12        13        11        11
    Mookie Betts 584 693 179  40   1  39  96    8 107 0.408      98.6 0.579      98.3         7        10        20         7
   Rob Refsnyder 202 243  50   9   1   1  33    5  47 0.365      90.5 0.317       6.6         5        19         2        14
  Yordan Alvarez 410 496 120  24   1  31  69   13  92 0.407      98.3 0.583      98.6         6         9        18         6
 Freddie Freeman 637 730 211  59   2  29  72   16 121 0.410      99.2 0.567      97.8         9        11         9         8
      Matt Olson 608 720 172  27   3  54 104    4 167 0.389      96.5 0.604      99.2        11         6         7         9
   Austin Hedges 185 212  34   5   0   1  11    2  47 0.234       0.3 0.227       0.3        10         1         4         3
     Aaron Judge 367 458  98  16   0  37  88    0 130 0.406      98.1 0.613      99.4         3         5         6         4
Ronald Acuna Jr. 643 735 217  35   4  41  80    9  84 0.416     100.0 0.596      98.9         2         3        10         2
    Corey Seager 477 536 156  42   0  33  49    4  88 0.390      97.0 0.623      99.7         4         4         3         5
   Shohei Ohtani 497 599 151  26   8  44  91    3 143 0.412      99.7 0.654     100.0         1         2         1         1

分析与结论

看起来这四种实现大致一致于离群值的定义,但在评分和易用性上有一些明显的差异。

椭圆包络在椭圆的短轴周围有更窄的轮廓,因此它倾向于突出那些与特征之间的整体相关性相悖的有趣球员。例如,光芒队外场手何塞·西里在该算法下被认为是一个更大的离群值,因为他的 SLG(88th 百分位)高而 OBP(5th 百分位)低,这与一个在边界球上挥杆用力的激进打击手一致,要么击球力大,要么击球弱或没有击中。

椭圆包络也很容易使用而无需配置,并且提供了稳健的协方差矩阵。如果你有低维数据并且合理地期望其呈正态分布(尽管这种情况通常不成立),你可以先尝试这种简单的方法。

单类 SVM 具有更均匀的轮廓分布,因此它倾向于比椭圆包络更强调与整体相关方向一致的观察值。全明星一垒手弗雷迪·弗里曼(道奇队)和杨迪·迪亚斯(光芒队)在该算法下的排名明显高于其他算法,因为他们的 SLG 和 OBP 都非常出色(弗里曼分别为 99th 和 97th 百分位,迪亚斯分别为 99th 和 95th 百分位)。

RBF 核函数需要额外的标准化步骤,但在这个简单的示例中,它似乎也能很好地工作而无需微调。

局部离群因子 识别了之前提到的“小簇优秀”特征,轮廓呈双峰状(在图表中几乎不可见)。由于道奇队外场手/二垒手穆基·贝茨被其他优秀击球手如弗里曼、尤尔丹·阿尔瓦雷斯和罗纳德·阿库尼亚 Jr. 环绕,他在 LOF 下的离群值排名仅为第 20 位,而在其他算法下排名第 10 位或更高。相反,勇士队外场手马塞尔·奥祖纳的 SLG 稍低且 OBP 明显低于贝茨,但在 LOF 下他是更大的离群值,因为他的邻域较少。

LOF 是实现过程中最耗时的,因为我们创建了稳健的距离矩阵用于拟合和评分。我们也可能需要花一些时间调整 K。

孤立森林 倾向于强调特征空间角落的观察值,因为分割分布在各个特征上。替补捕手奥斯丁·赫奇斯在 2023 年为海盗队和游骑兵队效力,并于 2024 年签约守护者队,他在防守方面表现出色,但在 SLG 和 OBP 上是最差的击球手(至少有 200 次打击机会)。赫奇斯可以通过 OBP 或 OPS 的单次分割被孤立,使他成为最强的离群值。孤立森林是唯一一个没有将大谷翔平排名为最强离群值的算法:由于大谷在 OBP 上被罗纳德·阿库尼亚 Jr. 超越,大谷和阿库尼亚只能通过 一个 特征进行单次分割。

与常见的监督树基学习器一样,隔离森林算法不进行外推,使其更适合用于配合受污染的数据集进行离群点检测,而不是用于配合无异常数据集进行新奇检测(在这种情况下,它不会对新离群点给予比现有观察结果更强的评分)。

尽管隔离森林算法开箱即用效果良好,但它未能将大谷翔平排名为棒球(以及可能所有职业体育)中的最伟大离群者,这体现了任何离群点检测器的主要局限性:你用来训练它的数据。

我们不仅省略了防守统计数据(抱歉,奥斯汀·赫奇斯),还没有包含投球统计数据。因为投手们现在几乎不再尝试击球……除了大谷,他的赛季包括对打击率(BAA)排名第二和 earned run average(ERA)排名第 11 的表现(最少 100 局),还有一场完整的零封比赛,以及一场他击出两支本垒打并三振十名打者的比赛。

有人建议大谷翔平是一个伪装成人类的高级外星人,但更有可能的是有两个高级外星人伪装成同一个人。不幸的是,其中一个刚刚接受了肘部手术,并且在 2024 年不会投球……但另一个刚刚签下了一份创纪录的 10 年、7 亿美元合同。多亏了离群点检测,我们现在可以看到其中的原因!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值