docs-merge-01

TowardsDataScience 2024 中文翻译(二)

原文:TowardsDataScience

协议:CC BY-NC-SA 4.0

一种下载数百张剪裁卫星图像的简单方法,无需检索整个场景(Python)

原文:towardsdatascience.com/a-simple-way-for-downloading-hundreds-of-clipped-satellite-images-without-retrieving-the-entire-f22d97501fd3?source=collection_archive---------3-----------------------#2024-04-26

学习如何仅用 12 行脚本下载任何兴趣区域(AOI)的剪裁 Sentinel-2 图像,这里以湖 Tahoe 为例。

Mahyar Aboutalebi, Ph.D. 🎓Towards Data Science Mahyar Aboutalebi, Ph.D. 🎓

·发布于 Towards Data Science ·阅读时长 9 分钟·2024 年 4 月 26 日

--

基于 2023 年所有清晰的 Sentinel-2 图像,湖 Tahoe 的时间推移,由作者可视化

目录

  1. 🌟 介绍

  2. ⚙️ STAC(时空资产目录)

  3. 💾 下载 Sentinel-2 图像(模板)

  4. 💾 下载 Sentinel-2 图像(湖 Tahoe)

  5. 🌍 湖 Tahoe 地图可视化

  6. 🎥 湖 Tahoe 2023 年地图时间推移

  7. 2023 年湖面面积和雪覆盖的时间序列

  8. 📄 结论

  9. 📚 参考文献

🌟 介绍

我已经发布了几篇关于下载卫星图像的教程,您可以在下面找到:

[## 使用 Google Colab 下载 Sentinel-2 图像(更新版,2023 年 11 月)

正如您所知道的,访问 Sentinel 图像的主要平台 https://scihub.copernicus.eu/ 已经停止…

medium.com](https://medium.com/@mahyar.aboutalebi/downloading-sentinel-2-imagery-in-python-with-google-colab-updated-nov-2023-f21d75a92407?source=post_page-----f22d97501fd3--------------------------------)

构建电影推荐系统图学习系统的逐步指南

原文:towardsdatascience.com/a-step-by-step-guide-to-build-a-graph-learning-system-for-a-movie-recommender-d647d0130e95?source=collection_archive---------4-----------------------#2024-09-11

使用 PyTorch Geometric 和 MovieLens 数据集构建

Yu-Cheng TsaiTowards Data Science Yu-Cheng Tsai

·发表于Towards Data Science ·13 分钟阅读·2024 年 9 月 11 日

--

图片来自michael podgerUnsplash

免费链接! 请享受阅读。 😃

图形建模

图形是一种表示实体之间关系的方式,使用节点来表示实体,边来表示它们之间的连接。这种结构非常灵活,可以应用于各种现实世界的问题。例如,我可以创建一个简单的图,其中有两个节点:用户和电影,节点“用户”给(即边关系)节点“电影”一个 4.5 的评分。

一个简单的电影评分图(图片来自作者)

通过将数据及其之间的关系建模为图形,可以捕捉到丰富而复杂的模式,这使得图形建模成为许多建模应用的合理方法。

推荐系统

推荐系统已成为我们数字体验的核心部分。无论是推荐“你可能喜欢的电影”,“符合你兴趣的新闻”还是“你可能认识的人”,这些系统都为许多面向客户的软件产品提供支持。

构建编程语言的逐步指南

原文:towardsdatascience.com/a-step-by-step-guide-to-building-a-programming-language-5f5b84246991?source=collection_archive---------0-----------------------#2024-06-19

在几小时内从零开始构建一种编程语言

Matthew ChakTowards Data Science Matthew Chak

·发布在 Towards Data Science ·阅读时间:17 分钟·2024 年 6 月 19 日

--

树:计算机科学的层次核心。来源:Jeremy Bishop 在 Unsplash

世界上充满了各种具有不同用途的编程语言。然而,这些语言大多数都是为非常通用的目的构建的——有时,我们可能想要设计一种语言来适应非常特定的用例(例如,Facebook 设计了 React 来简化其 Web 应用程序的开发,而苹果最近开发了 Pkl,一种旨在简化配置的语言。这个例子在各个领域中有很多)。因此,知道如何构建编程语言是一个非常有用的技能。

在本文中,我们将从零开始构建一种解释型编程语言,并在此过程中学习一些关于λ演算和编程语言的知识。我们构建的语言将是相当深奥的,但这个过程应该能给你一个如何设计自己特定用途语言的概念(并教会你一些关于编程语言如何在幕后运行的有用信息)。

游戏计划

由于我们正在构建一种解释型语言¹,整体流程大致如下:

我们语言的广泛流程图

基本上,我们从一些用我们目标语言(我们要编写的语言)编写的具体语法(代码)开始,传递给某个解析器,它将其转换为抽象语法树(代码的树形表示,易于处理),然后将其传递给解释器,由它“运行”抽象语法树,给我们最终结果。请注意,解析器和解释器是用某种已经存在的宿主语言编写的——例如,C 的原始解析器和编译器就是用汇编语言编写的。

*** 注意:我这里所使用的“解析器”涵盖了整个解析过程。通常,词法分析是在“语法分析”之前完成的,但在这种情况下,语法分析只是将具体语法转换为抽象语法的过程,无论抽象语法是什么样子。*

举个例子,考虑以下一个简单语言的基本算术规范:

EXPR = number
       | EXPR + EXPR
       | EXPR - EXPR
       | EXPR * EXPR
       | EXPR / EXPR
       | (EXPR)

顺便说一下,上面的是EBNF(扩展巴科斯范式)形式的上下文无关文法²。我不会在这里深入探讨这意味着什么,但所有用这种形式编写的编程语言都可以通过 CYK 算法在多项式时间内进行解析。对于这个 EBNF,像(4 + 4) * 3这样的表达式是一个有效的程序,但像def f(x): return 5; f(5)这样的代码就不是。

假设现在给定了具体语法(4 + 4) * 3。解析后,我们应该得到如下的抽象语法树(AST):

我们具体语法的抽象语法树(AST)

然后我们的解释器将从根节点开始,递归地遍历树,直到我们得到答案,也就是 24。

请注意,这个语法是模糊的——例如,表达式4 + 4 * 3应该如何解析?它可以解析为上面那样(即(4 + 4) * 3),也可以解析为4 + (4 * 3)——在我们指定的语言中,两种解析方式都没有本质上更“正确”的,因它们都是有效的解析树。在这种情况下,解析器将不得不任意决定如何解析我们的语言。

设计我们的语法

根据流程图,我们的第一步应该是设计我们的具体语法。你选择使用什么语法完全取决于你自己。我决定创建EmojiLang,这是一种(糟糕的)语言,可以确保你在输入时屏幕非常五彩斑斓。语法如下:

grammar EmojiLang;

program: '🏃‍♂️🏃‍♂️🏃‍♂️' expr '🛑🛑🛑' EOF;

expr: '(' (ID
         | atom
         | ifCond
         | anonFunctionDefn
         | funApplication
         | varAssignment
         | READ_FLOAT
         | READ_STRING
         | printExpr
         | sequentialOp
         | baseComputation) ')';

atom: NUMBER | BOOLEAN | STRING;
ifCond: '🤔' cond=expr '❓' ontrue=expr ':' onfalse=expr;
anonFunctionDefn: '🧑‍🏭' arg=ID '⚒️' body=expr;
funApplication: '🏭' fun=expr arg=expr;
varAssignment: '📦' var=ID '🔜' val=expr;
printExpr: '🖨️' expr;
sequentialOp: '📋' first=expr second=expr;
baseComputation: left=expr op=('➕' | '➖' | '✖️' | '➗' | '🟰' | '≤') right=expr;

ID: [a-zA-Z_][a-zA-Z0-9_]*;
NUMBER: [0-9]+ ('.' [0-9]+)?;
BOOLEAN: '👍' | '👎';
STRING: '"' ~[\r\n"]* '"';
READ_FLOAT: '🔢';
READ_STRING: '🔤';

WHITESPACE: [ \t\r\n]+ -> skip;
COMMENT: '😴' .*? '⏰' -> skip;
LINE_COMMENT: '🥱' ~[\r\n]* -> skip;

*** 注意:上面的规范是为名为 ANTLR 的工具编写的,我们很快就会回到这个话题。*

这个语言当然很荒谬,但有几个原因让它很有趣。首先,我们的所有表达式都需要加括号。这使得编写代码极为麻烦,但也让我们的语法不含歧义。其次,注意到我们只能定义匿名函数——没有类似于 Python 中def的语法。最后,这种语言中的所有函数(除了基本计算)只有一个参数。我们稍后在使用这种语言时会探讨最后两个设计决策的影响。

解析

我们当然可以自己编写解析器。不过幸运的是,有一些工具可以解析任意的上下文无关文法。在本教程中,我们将使用ANTLR(你可以在这里下载)。ANTLR 是一个非常好用且易于使用的工具,它可以根据类似上面那样的语法规范生成多种目标语言的解析器(在本教程中,我们将使用 Python)。

使用它相当简单,只需按照以下步骤操作:

  1. 这里下载 ANTLR 的 Java 二进制文件

  2. 为你的语法创建一个.g4 文件(如上所示)。注意,ANTLR 实际上无法很好地处理表情符号,所以如果你打算在语言中使用表情符号,请运行以下 Python 脚本来去表情化你的语法:

import emoji
import sys

def demojify_file(input_file, output_file):
    with open(input_file, "r", encoding="utf-8") as f:
        input_text = f.read()

    input_text = emoji.demojize(input_text)

    with open(output_file, "w", encoding="utf-8") as f:
        f.write(input_text)

if __name__ == "__main__":
    input_file = sys.argv[1]
    output_file = sys.argv[2]
    demojify_file(input_file, output_file)
  1. 运行java -Xmx500M -cp <path_to_antlr.jar> org.antlr.v4.Tool -Dlanguage=Python3 <your_grammar.g4>来生成解析器

然后,你可以导入生成的解析文件并按如下方式使用它们:

from antlr4 import *
from EmojiLangLexer import EmojiLangLexer
from EmojiLangParser import EmojiLangParser
from EmojiLangListener import EmojiLangListener
import emoji

if __name__ == "__main__":
    input_file = sys.argv[1]
    with open(input_file, "r", encoding="utf-8") as f:
        input_text = f.read()

    input_text = emoji.demojize(input_text)
    input_stream = InputStream(input_text)
    lexer = EmojiLangLexer(input_stream)
    token_stream = CommonTokenStream(lexer)
    parser = EmojiLangParser(token_stream)
    tree = parser.program()

    if parser.getNumberOfSyntaxErrors() > 0:
        exit(1)

你可能不需要做去表情化的步骤,在这种情况下你可以使用 antlr4 的FileStream代替InputStream,但这其实并不重要。现在,我们有了一个非常不错的抽象语法树,容易处理,我们可以进入更难的部分——解释³

构建解释器

因为我们正在处理树结构,所以我们的解释器自然会是递归的实体。不过,我们确实有一些选择,决定如何具体实现其某些特性。对于本教程,我们将构建一个使用环境将变量绑定到地址,然后使用可变的存储将地址映射到值的解释器。这个想法相当常见,尽管并非无处不在,它允许我们保持正确的作用域并支持变量的变化。为了简化实现,我们还将让我们的解释器返回一个通用的值结构。

值、存储与环境

首先,让我们定义我们的解释器可以输出什么。在我们的 EBNF 中有三个明显的基本情况(即布尔值、字符串和数字),因此我们将为这些创建值对象:

class Value:
    def __init__(self, value):
        self.value = value

    def __str__(self) -> str:
        return str(self.value)

class NumValue(Value):
    def __init__(self, value: float):
        super().__init__(value)

class StringValue(Value):
    def __init__(self, value: str):
        super().__init__(value)

class BoolValue(Value):
    def __init__(self, value: bool):
        super().__init__(value) 

为了存储变量到值的映射,我们还将创建一个环境和一个存储:

class EnvLookupException(Exception):
    pass

class Environment:
    def __init__(self):
        self.vars = {}

    def set_var(self, name, addr: int):
        self.vars[name] = addr

    def get_var(self, name):
        if name not in self.vars:
            raise EnvLookupException(f"Variable {name} not found in environment")
        return self.vars[name]

    def copy(self):
        new_env = Environment()
        new_env.vars = self.vars.copy()
        return new_env

class Store:
    def __init__(self):
        self.store = []

    def alloc(self, value: Value):
        self.store.append(value)
        return len(self.store) - 1

    def get(self, addr: int):
        if addr >= len(self.store):
            raise EnvLookupException(f"Address {addr} not found in store")
        return self.store[addr]

    def set(self, addr: int, value: Value):
        if addr >= len(self.store):
            raise EnvLookupException(f"Address {addr} not found in store")
        self.store[addr] = value

实际上,我们的环境将存储变量→地址绑定,而我们的存储将保存地址→值绑定。存储可能在我们当前的功能集下不是必需的,但如果我们允许通过引用传递变量的变化,它将会有用⁴。

理想情况下,我们还希望将函数作为变量传递,因此我们需要一个额外的值来表示它们。为此,我们创建了一个闭包,它包含了函数的参数、主体以及函数所在的环境:

class ClosureValue(Value):
    # Body should be an antlr4 tree
    def __init__(self, param: str, body: object, env: 'Environment'):
        super().__init__(None)
        self.param = param
        self.body = body
        self.env = env

    def __str__(self) -> str:
        return f"<function>"

你可能会问,为什么我们需要将环境存储在函数中。假设我们有一个像这样的函数值:

class FunctionValue(Value):
    # Body should be an antlr4 tree
    def __init__(self, param: str, body: object):
        super().__init__(None)
        self.param = param
        self.body = body

    def __str__(self) -> str:
        return f"<function>"

现在,假设我们在我们的语言中有与以下代码等价的内容:

# ----------------
# ENV MUST PERSIST
# ----------------
def f(x):
  def g(y):
    return x + y
  return g(x)

print((f(4))(5)) # 9

# ----------------
# ENV MUST CLEAR
# ----------------
def f2(x):
  return x + y

def g2(y):
  return f(5)

print(f(4)) # Should crash

为了确保 y 在顶级情况下仍然在 g 的作用域内,我们必须实现动态作用域(作用域在程序运行时变量被加入到环境中,而不会被清除),而不使用闭包,这意味着底部代码实际上会运行并打印 9。但是,为了让底部代码正确地崩溃,我们不能实现动态作用域。因此,我们希望函数能够有效地记住它们是在哪个环境中创建的——这就是为什么我们要在闭包类中添加环境的原因⁵。

解释器

现在,我们准备编写实际的解释器了。在 ANTLR 中,我们的解释器将扩展 EmojiLangListener 类。我们还需要创建一个顶级环境,并给解释器提供一个存储:

class EmojiLangException(Exception):
    pass

TOP_ENV = Environment()

class Interpreter(EmojiLangListener):
    def __init__(self):
        self.store = Store()

现在,我们需要创建一个 interp 方法,来处理我们所有的表达式案例。最终代码将如下所示:

def interp(self, prog, env: Environment) -> Value:
      if prog.ID():
          return self.interp_id(prog.ID())
      elif prog.atom():
          return self.interp_atom(prog.atom())
      elif prog.anonFunctionDefn():
          return self.interp_function_defn(prog.anonFunctionDefn())
      elif prog.READ_FLOAT():
          return self.interp_read_float()
      elif prog.READ_STRING():
          return self.interp_read_string()
      elif prog.printExpr():
          return self.interp_print_expr()
      elif prog.ifCond():
          return self.interp_if(prog.ifCond(), env)
      elif prog.sequentialOp():
          return self.interp_sequential_op(prog.sequentialOp(), env)
      elif prog.baseComputation():
          return self.interp_base_computation(prog.baseComputation(), env)
      elif prog.varAssignment():
          return self.interp_var_assignment(prog.varAssignment(), env)
      elif prog.funApplication():
          return self.interp_fun_application(prog.funApplication(), env)

我们的基础案例(ID、原子值、函数定义、读取和打印)相对简单,因此我们可以直接写出来:

def interp(self, prog, env: Environment) -> Value:
    if prog.ID():
        return self.store.get(env.get_var(prog.ID().getText()))
    elif prog.atom():
        return self.interp_atom(prog.atom())
    elif prog.anonFunctionDefn():
        return ClosureValue(prog.anonFunctionDefn().arg.text, prog.anonFunctionDefn().body, env)
    elif prog.READ_FLOAT():
        try:
            return NumValue(float(input("> ")))
        except ValueError:
            raise EmojiLangException("Expected float input")
    elif prog.READ_STRING():
        return StringValue(input("> "))
    elif prog.printExpr():
        value = self.interp(prog.printExpr().expr(), env)
        if isinstance(value, StringValue):
            # print without quotes
            print(str(value)[1:-1])
        else:
            print(value)
        return value
    # ...

def interp_atom(self, atm):
    if atm.NUMBER():
        return NumValue(float(atm.NUMBER().getText()))
    elif atm.BOOLEAN():
        return BoolValue(atm.BOOLEAN().getText() == ":thumbs_up:")
    elif atm.STRING():
        return StringValue(atm.STRING().getText())
    # THIS SHOULD NEVER HAPPEN
    raise EmojiLangException(f"Unknown atom {atm.getText()}")

我们的 if 条件也相当简单。我们只需要解释条件,然后如果条件为真,就返回解释真分支的结果,如果为假,就返回解释假分支的结果。因此,代码就是这样:

def interp_if(self, if_cond, env: Environment):
    cond = self.interp(if_cond.cond, env)
    if not isinstance(cond, BoolValue):
        raise EmojiLangException(f"Expected boolean when evaluating if condition, got {cond}")
    return self.interp(if_cond.ontrue if cond.value else if_cond.onfalse, env)

顺序操作同样简单,它们只需要先解释第一个表达式,再解释第二个表达式。因此,我们可以像这样替换该代码块:

def interp(self, prog, env: Environment) -> Value:
  # ...
  elif prog.sequentialOp():
    self.interp(prog.sequentialOp().first, env)
    return self.interp(prog.sequentialOp().second, env)
  # ...

接下来是基础计算。由于我们需要处理很多操作,所以这段代码相对较长,但并不复杂:

def interp_base_computation(self, base_computation, env: Environment):
    left, right = self.interp(base_computation.left, env), self.interp(base_computation.right, env)
    if base_computation.op.text == ":plus:":
        if isinstance(left, NumValue) and isinstance(right, NumValue):
            return NumValue(left.value + right.value)
        elif isinstance(left, StringValue) and isinstance(right, StringValue):
            return StringValue(left.value + right.value)
        raise EmojiLangException(f"Cannot add {left} and {right}")
    if base_computation.op.text == ":heavy_equals_sign:":
        if type(left) != type(right):
            return BoolValue(False)
        if isinstance(left, ClosureValue):
            raise EmojiLangException("Cannot compare functions")
        return BoolValue(left.value == right.value)

    # NUMBERS ONLY COMPUTATIONS
    if not isinstance(left, NumValue) or not isinstance(right, NumValue):
        raise EmojiLangException(f"Expected numbers when evaluating base computation, got {left} and {right}")
    if base_computation.op.text == ":minus:":
        return NumValue(left.value - right.value)
    elif base_computation.op.text == ":multiply:":
        return NumValue(left.value * right.value)
    elif base_computation.op.text == ":divide:":
        if right.value == 0:
            raise EmojiLangException("Division by zero")
        return NumValue(left.value / right.value)
    elif base_computation.op.text == "≤":
        return BoolValue(left.value <= right.value) 

也许通过使用例如字典的方式,这部分代码还可以稍微清理一下,但这并不是特别重要。接下来是变量赋值,也不太复杂。我们在这里有几个选择,具体想让它做什么——即,它是允许新变量的出现,还是只修改现有的变量?我选择后一种情况,这样我们的代码就像这样:

def interp_var_assignment(self, var_assign, env: Environment):
    value = self.interp(var_assign.val, env)
    addr = env.get_var(var_assign.var.text)
    self.store.store[addr] = value
    return value

最后,我们来看看函数应用。在这里,我们需要执行四个步骤。首先,我们解释我们正在调用的函数,以获得我们的闭包。接着,我们解释我们的参数。然后,我们将参数绑定到闭包环境的副本中。最后,我们在新的环境中解释闭包的主体。代码最终如下所示:

def interp_fun_application(self, fun_app, env: Environment):
    closure = self.interp(fun_app.fun, env)
    if not isinstance(closure, ClosureValue):
        raise EmojiLangException(f"Expected function when evaluating function application, got {closure}")
    arg = self.interp(fun_app.arg, env)
    new_env = closure.env.copy()
    new_env.set_var(closure.param, self.store.alloc(arg))
    return self.interp(closure.body, new_env)

现在,我们的解释器功能完备!我们只需要修改我们的主程序,运行它来解释一个程序:

if __name__ == "__main__":
    input_file = sys.argv[1]
    with open(input_file, "r", encoding="utf-8") as f:
        input_text = f.read()

    # Preprocess input to replace emojis with demojized names
    input_text = emoji.demojize(input_text)

    input_stream = InputStream(input_text)
    lexer = EmojiLangLexer(input_stream)
    token_stream = CommonTokenStream(lexer)
    parser = EmojiLangParser(token_stream)
    tree = parser.program()

    if parser.getNumberOfSyntaxErrors() > 0:
        exit(1)

    interpreter = Interpreter()
    interpreter.interp(tree.expr(), TOP_ENV)

玩转我们的语言

我们现在终于准备好开始编写我们的语言程序了。下面是一个简单的 emoji 语言(EML)版本的 hello world 程序:

🏃‍♂️🏃‍♂️🏃‍♂️
    (🖨️ ("Hello World!"))
🛑🛑🛑

要运行它,我们只需执行:

> python emoji_lang.py helloworld.eml
Hello World!

(上述内容,当然假设程序存在于名为helloworld.eml的文件中)

柯里化

在第一部分中,我提到我们的编程语言有趣之处在于函数只能接受一个参数。那么,我们如何创建类似于多元函数的效果呢?例如,考虑以下 Python 代码:

def f(x, y):
  return x + y

print(f(3, 4))

这里的f阶数2——也就是说,它接受两个参数。然而,我们可以编写等效的代码,只使用一元函数(除了加法),如下所示:

def f(x):
  def g(y):
    return x + y
  return g

print((f(3))(4))

上述将高阶函数转化为一元函数的概念叫做柯里化。它适用于任何阶数的函数——对于一个阶数为n的函数,我们可以简单地进行柯里化n-1次。在表情语言中,这使得我们能够编写如下程序:

🏃‍♂️🏃‍♂️🏃‍♂️
    (📋
        (🖨️ ("Enter two numbers to compute their sum."))
        (🖨️
            (🏭
                (🏭 
                    (🧑‍🏭 x ⚒️ 
                        (🧑‍🏭 y ⚒️
                            ((x) ➕ (y))
                        )
                    )
                (🔢))
            (🔢))
        )
    )
🛑🛑🛑

其 Python 翻译为

print("Enter two numbers to compute their sum.")
print(((lambda x: (lambda y: x + y)))(float(input()))(float(input())))

或者,更易读地,

print("Enter two numbers to compute their sum.")

def f(x):
  def g(y):
    return x + y
  return g

x = float(input())
y = float(input())

print(f(x)(y))

还要注意,第一次的 Python 迭代中没有使用命名函数。事实证明,我们实际上并不需要它们,尽管它们当然有助于提高可读性。然后我们得到

> python emoji_lang.py currying.eml
Enter two numbers to compute their sum
> 4
> 5
9.0

递归

那么,在这个语言中我们如何实现循环或递归呢?我们没有forwhile的语法,而且没有函数的名称,看起来似乎递归是不可能的。

然而,我们可以做一个小技巧。因为函数是值,我们可以在调用它们时将函数传递给它们自己,从而使它们能够调用自己。

比如,考虑以下 Python 代码:

n = int(input())
while n > 0:
  print(n)
  n -= 1

我们可以使用类似下面的代码将其转换为递归版本⁶:

def while_loop(condition, body):
  """
  A recursive implementation of a while loop.

  Arguments
  -------------
  - condition: Some function of zero arguments that returns a boolean value
  - body: Some function of zero arguments to run while the condition returns true
  """
  if condition():
    body()
    while_loop(condition, body)
  else:
    return False

class Box:
    def __init__(self, n):
        self.n = n

    def set_n(self, n):
        self.n = n

    def get_n(self):
        return self.n

n = Box(int(input()))

def body():
    print(n.get_n())
    n.set_n(n.get_n() - 1)

while_loop(lambda: n.get_n() > 0, body)

但我们在这里确实遇到了一个问题——即注意到while_loop是如何调用它自己的。我们在只有匿名函数的语言中无法做到这一点,那么我们该如何解决呢?答案是,我们可以做类似下面这样的操作:

def while_loop(self, condition, body):
  if condition():
    body()
    self(self, condition, body)
  else:
    return False

# ...
# (define n as a box)
# ...

def body():
    print(n.get_n())
    n.set_n(n.get_n() - 1)

def call_while(loop_func, condition, body):
    loop_func(loop_func, condition, body)

call_while(while_loop, lambda: n.get_n() > 0, body)

实际上,我们让while_loop它自己作为参数。然后,我们可以在while_loop内部调用self,使得while_loop能够递归地调用自己。

当然,我们仍然需要将这个进行 lambda 化并进行柯里化,以适应我们的语言,因此我们需要编写与以下等效的代码:

(((lambda while_loop: 
    lambda n: 
        while_loop(while_loop)
                  (lambda bogus: n.get_n() > 0)
                  (lambda bogus: print(n.get_n()) or n.set_n(n.get_n() - 1)))
(lambda self:
    lambda cond:
        lambda body:
            (body("BOGUS") or self(self)(cond)(body)) if cond("BOGUS") else False))
(Box(int(input()))))

这个有点难以理解,但它确实有效。在表情语言中,这就变成了

🏃‍♂️🏃‍♂️🏃‍♂️
    (🏭
        (🏭
            (🧑‍🏭 while ⚒️
                (🧑‍🏭 n ⚒️
                    (🏭 (🏭 (🏭 (while) (while))
                        (🧑‍🏭 bogus ⚒️ (🤔 ((n) ≤ (0)) ❓ (👎) : (👍))))
                        (🧑‍🏭 bogus ⚒️ (📋
                            (🖨️ (n))
                            (📦 n 🔜 ((n) ➖ (1)))
                        )))
                ))
            😴
                Below is our while function. Note that it takes
                itself as an argument - this allows for recursion
                (there are other ways to do this, but recursion via self
                passing is fairly simple)

                ARGS: 
                1\. self(itself)
                2\. condition_func (function of zero arguments that returns a boolean)
                3\. body (function of zero arguments that returns nothing to run while true)

                RETURNS:
                false when finished
            ⏰
            (🧑‍🏭 self ⚒️
                (🧑‍🏭 condition_func ⚒️
                    (🧑‍🏭 body ⚒️
                        (
                            🤔 (🏭 (condition_func) ("BOGUS")) ❓ 
                                (📋 
                                    (🏭 (body) ("BOGUS")) 
                                    (🏭 (🏭 (🏭 (self) (self))
                                            (condition_func))
                                            (body))) : 
                                (👎)
                        ))))
        )
    (🔢))       
🛑🛑🛑

然后我们得到

> python emoji_lang.py while_loop.eml
> 4
4.0
3.0
2.0
1.0

奖金:Y 组合子

每次想调用while时传递while给它自己确实有些麻烦,那么如果我们能创建一个已经将自己进行柯里化的while版本呢?事实证明,我们可以通过一个叫做Y 组合子的东西来实现。Y 组合子是一个函数,形式如下:

Y = lambda g: (lambda f: g(lambda arg: f(f)(arg))) (lambda f: g(lambda arg: f(f)(arg)))

这完全荒谬,但它允许我们有效地“存储”一个函数到它自己中。我不会详细讲解它,但如果你想了解更多,我建议阅读这篇优秀的文章

然而,使用组合子后,我们可以将代码更改为以下内容:

🏃‍♂️🏃‍♂️🏃‍♂️
    (🏭
        (🏭
            (🏭
                (🧑‍🏭 y_combinator ⚒️
                    (🧑‍🏭 while ⚒️
                        (🧑‍🏭 n ⚒️
                            (📋
                                🥱y-combinate our while
                                (📦 while 🔜 (🏭 (y_combinator) (while)))
                                (🏭 (🏭 (while)
                                        (🧑‍🏭 bogus ⚒️ (🤔 ((n) ≤ (0)) ❓ (👎) : (👍))))
                                        (🧑‍🏭 bogus ⚒️ (📋
                                                (🖨️ (n))
                                                (📦 n 🔜 ((n) ➖ (1)))
                                        ))
                                )
                            )
                        )
                    )
                )
                😴
                    Our y combinator function - this allows for recursion without e.g. self passing
                    by effectively currying the function and passing it to itself.
                ⏰
                (🧑‍🏭 fn_nr ⚒️
                    (🏭
                       (🧑‍🏭 cc ⚒️
                            (🏭 (fn_nr) 
                                (🧑‍🏭 x ⚒️ (🏭 (🏭 (cc) (cc)) (x)))
                            )
                       )
                       (🧑‍🏭 cc ⚒️
                            (🏭 (fn_nr) 
                                (🧑‍🏭 x ⚒️ (🏭 (🏭 (cc) (cc)) (x)))
                            )
                       )
                    )
                )
            )
            (🧑‍🏭 while ⚒️
                    (🧑‍🏭 condition_func ⚒️
                        (🧑‍🏭 body ⚒️
                            (
                                🤔 (🏭 (condition_func) ("BOGUS")) ❓ 
                                    (📋 
                                        (🏭 (body) ("BOGUS")) 
                                        (🏭 (🏭 (while)
                                                (condition_func))
                                                (body))) : 
                                    (👎)
                            ))))
        )  
    (🔢))    
🛑🛑🛑

现在,注意我们对while的调用在经过 Y 组合子化⁷后,仅涉及传递条件和主体——我们不需要传递它本身。我们仍然能够得到

> python emoji_lang.py y_comb_while.eml
> 4
4.0
3.0
2.0
1.0

结论

恭喜!你现在可能已经构建了自己的编程语言,并用它编写了一些有趣的东西。尽管像 EmojiLang 这样的语言显然没有太多实际用途,但你可以想象创建自己语言的一些酷用例,例如通过创建一个超具体的脚本语言来加速常见任务。

如果你想挑战自己,可以尝试以下练习:

练习 1: 为以下语言构建一个简单的解析器和解释器,使用 ANTLR。确保括号始终具有最高优先级,并且其他操作符具有相等优先级(例如,4 + 4 * 3 应该计算为 24,而不是 16

EXPR = number
       | EXPR + EXPR
       | EXPR - EXPR
       | EXPR * EXPR
       | EXPR / EXPR
       | (EXPR)

练习 2: 修改你之前的代码,以添加操作符优先级

练习 3(棘手):我们不将所有函数都定义为匿名函数。试着为以下语言实现一个解释器(你可以使用 ANTLR,但你需要自己编写 .g4 文件):

program = (FUNDEF | EXPR)* // one or more function definitions or expressions

// NOTE: <something> implies that something is a string
// also, feel free to ignore whitespace or add semicolons or parenthesize
// expressions/fundefs if you please

EXPR = number
     | functionApplication
     | computation

FUNDEF = 'def' <name> '(' <args>* '):' EXPR

functionApplication = <name> '(' EXPR* ')' // e.g. f(1, 2+2, g(3))
computation = EXPR + EXPR
             | EXPR - EXPR
             | EXPR * EXPR
             | EXPR / EXPR
             | (EXPR)

练习 4(简单 → 非常非常难): 可以在这里找到大量实际语言的 .g4 文件。为其中任何一个实现一个解释器。

如有任何疑问,请联系 mchak@calpoly.edu

附言:感谢 Brian Jones,我的 CS 430 课程教授,教会了我很多这些内容。

除非另有说明,所有图片均由作者提供。

  1. 编译型语言略有不同。生成的抽象语法树会传递给编译器,编译器将其转换为其他现有语言的代码(通常是汇编语言)。然后,输出的代码可以运行。

  2. 从技术上讲,这其实并不完全是 EBNF,但它足够接近。如果你感兴趣,可以在这里阅读更多关于 EBNF 的内容

  3. 注意:一些语言可能会在将抽象语法树(AST)传递给解释器之前进行一些优化。我们这里不会进行这些操作。

  4. 我们仍然可以通过在环境中使用盒装值来允许变异,但存储更类似于大多数语言的做法,也更直观,至少对我来说是这样。

  5. 请参阅 7 Functions Anywhere (brown.edu) 获取更多信息

6. 请注意,我们必须在这里使用一个盒子,因为 Python 的环境与我们的环境工作方式不同,例如我们不能这样做:

# ...
def get_body_and_cond(inp):
    n = inp
    condition = lambda: n > 0

    def body():
        print(n)
        n -= 1
    return condition, body

c, b = get_body_and_cond(float(input()))

while_loop(c, b)

7. 事实上,由于我们的语言是急切地评估内容的,所以它自始至终都是 Z 组合的,但效果是一样的

提升组织分析成熟度的逐步手册

原文:towardsdatascience.com/a-step-by-step-playbook-to-grow-your-organizations-analytical-maturity-b3642d2b30f0?source=collection_archive---------6-----------------------#2024-02-22

在实际生活中实施我们所看到的各种框架

Jordan GomesTowards Data Science Jordan Gomes

·发表于 Towards Data Science ·11 分钟阅读·2024 年 2 月 22 日

--

..我们回到了关于分析成熟度的系列文章!我收到的反馈之一是,前几篇文章可能过于抽象,读者希望能看到一些更具操作性的内容。所以今天我们要做的就是:通过一步步的指南,使整个过程更加实际,帮助提升组织的分析成熟度。

我们从第一步开始:闭嘴并倾听!

一步一步的分析成熟度提升手册 — 图片来自作者

第一步:闭嘴并倾听

有一个很棒的 TED 演讲,由恩尼斯托·西罗利(Ernesto Sirolli)主讲,标题为“想帮助别人?闭嘴并倾听”。在演讲中,他解释了当他 21 岁时,曾在一个意大利非政府组织工作,该组织试图“教赞比亚人如何种植食物”。他们成功地在赞比西河下游的山谷中种出了美丽的番茄——但正如西罗利用他自己的话所说:

当番茄又大又红,成熟了,隔夜,约 200 只河马从河里出来,把所有的番茄都吃光了。我们对赞比亚人说,‘天啊,河马!’而赞比亚人回答说,‘是的,这就是我们这里没有农业的原因。’

每当你想要提升组织的分析成熟度时,第一步就是闭嘴,静下心来倾听。你需要倾听并理解痛点、不同的工作职责、数据需求、过去为解决这些痛点所做的努力等。这样不仅能让你更好地了解全局,也能帮助你更好地理解那些组成你组织的人。

在中型或大型组织中,“倾听”意味着主动收集信息。为此,你可以进行几项工作:

  • 用户访谈:深入探讨团队成员的个人经验,收集关于他们日常挑战、数据使用和具体痛点的定性见解。通过直接与他们交流并处于小规模环境中,你为诚实反馈和对组织中数据需求的细致理解创造了一个开放的渠道。

  • 问卷调查:问卷调查(尤其是匿名调查)让你能够从组织中的更大群体收集更多样化的见解。这不仅有助于识别常见主题和关注领域,还能作为衡量组织内部满意度和数据素养水平的基准工具。

  • 参加团队会议:参加跨部门的定期团队会议,可以让你亲身观察数据在决策过程中的当前使用情况,识别数据可用性或可访问性方面的差距,并注意到在讨论数据时常见的问题或误解。

  • 影子工作法:尝试深入了解用户的工作生活。这将为你提供一个直接的视角,观察团队和个人面临的操作挑战,提供宝贵的见解,帮助你了解分析工具和数据是如何(或未能)融入日常工作流程的。

使用这些不同的方法,你可以进行大量出色的倾听,帮助你全面了解你的组织在分析成熟度方面所处的位置,以及在哪些方面可以进行最有影响力的改进。这是一个非常基础的阶段——是诊断阶段的一部分,帮助你塑造你的战略。

那么,你是否应该在任何地方、任何时间都去倾听一切?如果你不想像 Jobu Tupaki 那样,重要的是在探索过程中确保采用正确的视角。

  • 在深度方面,有些问题需要你使用显微镜(因为它们非常技术性、复杂且敏感),而有些问题则最好从远处观察(因为它们优先级较低,并不一定需要你亲自参与)。这里我没有现成的框架,但诊断阶段的一部分就是要获得这种理解。

  • 在广度方面,使用“工具 / 过程 / 人员 / 文化”框架通常是一个不错的起点。你需要清楚了解组织中的人员、他们的技能以及是什么激励他们;你需要理解他们的工作流程,以及他们如何与其他利益相关者合作;你还需要清楚他们使用的工具。

步骤#2a:优先排序,构建路线图并进行沟通

一旦你对不同的组织痛点有了清晰的理解,并且知道它们为何存在,你就可以开始制定战略。如我在a 篇文章中提到的——遵循理查德·鲁梅尔特的框架,一个好的战略由三个要素构成:诊断、指导方针和行动计划。

在这个阶段,感谢你在上一部分的倾听,你应该已经做好准备,开始记录需求,并且开始识别当前组织中存在的空白。希望在这一点上,创建以下“映射”应该不成问题:

  • 技能集映射(团队内谁能做什么)

  • 活动 / 项目映射(团队内谁在做什么)

  • 过程映射(我们如何决定做什么以及如何完成)

  • 工具映射(我们使用什么工具来做我们想做的事)

  • 利益相关者映射(团队外部谁关心/能影响我们做的事情)

  • 目标映射(不同团队的主要目标是什么,以及这些目标之间如何相互关联)

  • 等等

根据经验,当你整理这些不同的映射时,你将会开始获得关于团队和组织的有趣见解,更广泛地说,你将开始理解整个“系统”是如何协同工作的。通常,在这个阶段,你会意识到一些工具的使用是因为某些技能集不可用,某些流程是作为对有缺陷工具的替代解决方案存在的,等等。基本上,正是在这个步骤,你会发现一些小的低效环节。因此,希望通过这次详尽的映射练习,你能看到一些改进的领域——这也是你从探索转向行动的时候:

  • 采用优先级排序过程: 为你刚刚记录的需求建立一个清晰透明的优先级排序方法。这个过程可能涉及到影响业务目标的程度、问题发生的频率,或者实施的难易程度等标准。可以考虑像 MoSCoW(必须有、应该有、可以有、不需要有)或 ICE 评分(影响力、信心、难易度)这样的框架,来系统地评估每一个需求。

  • 相应地优先排序: 使用你选择的优先级排序方法,将需求从最关键到最不重要进行排序。这个步骤确保资源能够有效分配到对组织的分析成熟度影响最大的领域。

  • 创建并分享你的路线图: 采用一个简洁的路线图,明确交付物和清晰的时间表。沟通是设定期望和为你的举措建立支持的关键。与利益相关者分享优先排序的列表及其背后的理由。这种透明度有助于管理期望、争取支持,并促进一种合作的方式来改善组织的数据能力。

通过遵循这些步骤,你可以创建一个专注且具有战略性的路线图,解决组织内最紧迫的数据需求。这个路线图不仅明确了期望,还将你的数据举措与整体商业目标对齐,确保了一种有凝聚力且高效的方法,以提高分析能力。

请注意,虽然诊断已经给出了改进的方向,但有时候并不一定能明确知道解决方案应该是什么。在这种情况下,我喜欢使用两个框架:

  • 行动 > 信息 > 视野: 很多时候,你的视野会来自于通过采取行动获得的额外信息。你做了一些事情并从中学习,这帮助你塑造了自己的视野。所以,如果你不确定应该去哪里,先迈出第一个最小的步伐——从那里你将处于一个新的起点,很可能接下来的步骤就会显现出来。

  • 创新 > 量化 > 协调: 这个框架来自于迈克尔·E·格伯(Michael E. Gerber)的《电子神话再探:为什么大多数小企业无法成功以及该如何做》一书,当你试图优化一个流程时非常有用:只需选择流程中的一部分,“创新”(即改变它),量化这一创新(这个改变使得流程变得更好吗?),如果量化结果是积极的,就进行协调(即去除其中的人工决策成分)。

所以如果你不确定解决方案应该是什么,或者你的路线图中的交付物应该是什么——可以将问题分解,专注于下一个最小的步骤,或者为潜在的解决方案进行试点。

步骤 #2b:获得一些快速胜利来建立可信度

这一步——虽然是可选的,但通常建议执行。如果你需要建立一些可信度,你可能需要优先考虑一些快速胜利,因为这将帮助你建立信任,并让你和你的团队进入一种“交付”的“势头”状态。

  • 识别低垂果实: 寻找可以快速实施并产生明显影响的项目或改进。这些可能是简单的数据质量修复、自动化重复的手动报告,或提供访问新的、宝贵的数据源。目标是找到那些需要最小努力却能带来显著收益的变化。

  • 利用现有工具和资源:利用你所在组织中已有的工具和平台来实现这些快速成功。这可能意味着在现有的 BI 工具中创建新仪表盘、使用数据平台中的自动化功能,或仅仅通过更好的实践优化当前的数据流程。

  • 庆祝并传播成功:一旦你实现了这些快速成功,确保它们得到了良好的传播和庆祝。使用内部新闻通讯、会议或任何公司范围的沟通渠道来突出所做的改进。分享成功故事不仅能建立你的信誉,还能向整个组织展示数据驱动决策的价值。

  • 征求反馈和建议:在实施快速成功后,向用户征求反馈。这不仅有助于了解你努力的影响,还能让更广泛的团队参与到数据改进过程中,可能会揭示出更多的快速成功机会。

  • 重复这一过程:建立信誉是一个持续的过程。即使你正在处理更复杂的长期项目,也要继续寻找快速成功的机会。这种迭代式的方法确保了持续改进,并使你的组织保持长期参与。

通过专注于快速成功,你可以迅速展示数据项目的价值,赢得信任和支持,为应对更具雄心的项目打下基础,进而提升组织的分析成熟度。

第 3 步:把事情做完,沟通,记录一切并培训每个人

现在你的策略已经设定好,是时候放弃“旁观者”模式,转向“执行模式”了。

  • 执行:你已经制定了路线图,知道什么时候需要交付什么——接下来就开始执行。成为经理时,我读到的一条最宝贵的建议就是要非常保护和用心管理我的时间。感谢路线图,你知道什么时候需要交付什么内容,所以你可以确保每天/每周留出足够的时间来完成它。由于在任何公司中总是有很多紧急事务需要处理,拥有清晰且强大的路线图可以帮助你确保不仅仅是解决当前的问题,还能为未来做好建设。

  • 沟通:我非常认同“公开构建运动”(build in public movement),即记录并公开分享你的旅程。我认为这样做有两个好处:(1)定期更新,无论是通过会议、电子邮件更新还是仪表盘,都有助于保持透明度,并促进信任与合作的文化。这也有助于让自己或团队出现在利益相关者的视野中,最终增加团队的“运气”(你越是公开,越多人知道你和你的使命,如果他们看到有合作的机会,就更容易主动联系你)。(2)这是一个很好的驱动力来记录你的旅程,使你能够追踪不同的变化,并了解决策对长期的影响。

通过高效地执行计划、保持畅通的沟通渠道,并投资于文档和培训,你为可持续的数据驱动增长奠定了基础。这种方法不仅提升了组织的分析能力,还嵌入了持续学习和改进的文化。

第 4 步:重新开始(但在实施过程中不断改进)

因为生活就是一个永恒的循环:

  • 反思与回顾:在完成了倾听、优先排序、实施快速成功并执行更大项目的一个周期后,花些时间反思哪些有效,哪些无效。从利益相关者那里收集反馈,分析你所做举措的影响,回顾你的沟通和培训工作是否有效。

  • 融入经验教训:利用你反思阶段获得的见解来优化流程。这可能意味着调整优先级标准、寻找更高效的项目执行方式,或是确定更好的团队互动与培训方法。

  • 更新你的路线图:凭借新的信息和对组织需求的更好理解,更新你的路线图,反映当前的优先事项和新的改进机会。这种迭代的规划确保你的努力始终与组织目标保持一致,并适应不断变化的环境。

  • 保持沟通:根据反馈,优化你的沟通策略和培训项目。这可能包括为你的新闻通讯引入新格式、尝试不同的培训方法,或利用技术使学习更加互动和吸引人。

  • 融入持续改进:在你的团队及更广泛的组织中培养持续改进的文化。鼓励持续反馈,促进思想共享,并认可对数据驱动文化的贡献。

将分析成熟度视为一个持续的旅程,而非终点,你将营造一个学习、改进和创新成为日常常规的环境。这种迭代方法确保你的组织保持敏捷、响应迅速,并且越来越擅长数据分析。

结论

在总结这本关于提升组织分析成熟度的手册时,请记住:这段旅程是循环的,而不是线性的。就像那些开放世界的视频游戏一样(眨眼 我的上一篇文章),冒险永远不会真正结束,总有新的任务等待着你,你走向数据驱动文化的道路也是持续不断的。

这本手册中最重要的理念其实是:提升组织的分析成熟度是一个持续的过程,最重要的是确保你能够保持“交付的动力”,这样你才能始终向前推进。

而且,随着你不断前进,经历的“倾听 > 诊断 > 路线图 > 执行”周期越多,数据分析成熟之路也会越明确。所以,继续迭代,不断改进,持续将你的组织转型为一个更具数据意识、决策更智能的实体,更重要的是,要享受这个过程。

希望你喜欢这篇文章!你有什么想法或者建议想分享吗?在评论区告诉大家吧!

如果你想阅读更多我的文章,以下是一些你可能会喜欢的文章

## 如何构建一个成功的仪表盘

来自一个做过几个失败项目的人的检查清单

[towardsdatascience.com ## 构建数据分析成熟组织(AMO)

一些简单的框架,帮助你明确组织的分析需求是什么,如何提升其…

[towardsdatascience.com ## 什么使优秀的数据分析师与众不同

还在寻找新年的决心吗?这里有 6 项技能,帮助你和你的团队变得极其高效。

[towardsdatascience.com

PS:这篇文章也在 Analytics Explained上发布,这是一个我总结在各种分析角色中所学到的内容的新闻通讯(从新加坡的初创公司到旧金山的大型科技公司),并回答读者关于分析、增长和职业发展的提问。

长尾的故事:为何营销组合模型中的不确定性很重要

原文:towardsdatascience.com/a-story-of-long-tails-why-uncertainty-in-marketing-mix-modelling-is-important-95fc5a5eb94f?source=collection_archive---------7-----------------------#2024-11-27

“细节决定成败。值得等待,直到做对为止。” — 史蒂夫·乔布斯

Javier MarinTowards Data Science Javier Marin

·发布于 Towards Data Science ·阅读时间 23 分钟·2024 年 11 月 27 日

--

图片由作者使用 DALL-E 生成

如果你 营销组合模型MMM)中最有价值的洞察隐藏在我们通常认为的不确定性或噪声中,会怎样呢?

如果我们能够从不同的角度来看待今天使用的 MMM 模型的结果,会发生什么呢?

我们这些使用贝叶斯分层模型构建 MMM 模型的人已经看到,这些模型¹为我们在模型中设置的每个参数提供了大量信息。通过应用严格且广泛验证的统计技术,我们选择例如后验分布的均值(有时是中位数)作为某个渠道影响力的值。然后,我们根据这个值进行思考并生成可操作的洞察。然而,事实是,贝叶斯分析输出的是一个值的概率分布,而尾部通常很大,包含罕见事件和例外情况。如果我们低估了这些尾部包含的信息,我们就会错失一个宝贵的机会。在这些长尾的表现中,如果我们用正确的视角去观察,就能发现非常有价值的洞察。实际上,大多数用户使用 MMM 模型的基本理念是量化…

一位外科医生对人工智能的思考

原文:towardsdatascience.com/a-surgeons-reflections-on-artificial-intelligence-c070bb633e9f?source=collection_archive---------2-----------------------#2024-01-02

从临床角度看医学创新

Alberto PadernoTowards Data Science Alberto Paderno

·发表于Towards Data Science ·阅读时长 4 分钟·2024 年 1 月 2 日

--

由 Dall-E 3 生成的图像

作为一名肿瘤外科医生,这是我的主要工作和热情所在。它让我能够与人互动,深入医疗体系,而非奢华的企业医疗,只是日常的医学实践。

作为 AI 领域的研究人员,我注意到实际临床实践与 AI 研究人员和公司普遍目标之间的脱节日益严重。这当然只是个人观点,并非对当前研发过程的批评,但这反映了我在两个领域的经验。

人工智能在客户软件和工业领域的颠覆性潜力如今已经显而易见。然而,我们必须承认,AI 在医疗领域是完全不同的一种存在;其复杂性、监管要求和风险远高于大多数其他应用。此外,由于隐私和可访问性限制,公开可用的数据集在这一领域比许多其他领域稀缺得多。

所以,存在重大障碍,且复杂性更高。

我目前在硅谷担任外科医生,具有 AI 技术背景,这让我能够直接接触到这个充满活力的“生态系统”。AI 相关的会议和研讨会几乎每天都有。然而,我不禁注意到一些事实:

  • 临床医生没有参与 AI 相关的活动。

  • 临床医生甚至没有参与“医疗领域的 AI”相关活动。

  • AI 在医疗领域的研究由技术驱动,临床医生的反馈和合作极为有限。

  • 即使是在临床医生中,关于数据共享和技术开发的合作也明显不足。

技术引导的方法

首先,对新技术的热情促使我们尝试将它们应用于每一个问题:“如果你只有一把锤子,你就会把所有问题都看作钉子,” 亚伯拉罕·马斯洛如是说。我完全理解这种倾向。AI 是我们的新雷神之锤;为什么我们不想尝试一下它是否适用于任何稍微合适的问题呢?

然而,这也将研究和进展引导到解决“技术难题”上,而没有回答一个根本问题。一方面,我们可以找到这种概念的有趣表现形式,例如“她说过这话”笑话识别器(这是一个有趣的解决方案,我并不批评);另一方面,也有一些例子,其中强制实施复杂的深度学习流程是昂贵且不必要的

其次,典型的“自上而下”战略基于市场分析和市场份额计算。简而言之,“让我们在医疗健康领域找到一个大而有利可图的领域,然后把它塞满 AI 技术。”一如既往,这可能是一个很好的短期策略,但过一段时间后,魔力就会消失。

这些方法在医疗领域很少有效。当新解决方案的优势不明显时,医生和外科医生通常会回归传统做法。普朗克原理可以安全地应用于医学创新,“科学的进步总是伴随着一场葬礼。”因此,虽然在规模化应用时,运营效率提高 5%-10%看似重要,但在医疗环境中却很难得到应用——我们需要的是在与日常临床实践相关的领域实现 2 倍到 10 倍的改进。

从“实验室”到病床

一个实际的方法是识别一个真正的问题,评估当前解决方案的有效性,并评估 AI 是否能用于开发更好的解决方案——典型的妈妈测试

[## 如何与客户沟通并了解你的业务是否是一个好主意,尤其是当每个人都在对你撒谎时。

世界不需要另一个框架或理论。《妈妈测试》跳过了这一切,直接进入实际挑战…

www.momtestbook.com](https://www.momtestbook.com/?source=post_page-----c070bb633e9f--------------------------------)

目前,人工智能在医疗健康领域的主要发展大多来自科技研究团体和科技公司。这种关系解释了为什么关注点更多偏向计算机科学而非医疗健康方面。

为了解决这个问题,临床医生和外科医生的直接参与至关重要。

需求显而易见。医学依然是一个高度有缺陷且主观的领域;称其为“艺术”就像是在看半满的杯子,却没有意识到其中缺少了什么。例如:

  • 外科医生的经验和判断仍然是决定生存结果和并发症的最重要因素之一。

  • 高度主观的内窥镜评估是整个医学领域的基石(例如,喉镜检查、胃肠内窥镜检查、支气管镜检查)。

  • 即使是组织病理学,通常被视为金标准,也可能因其解读的高度可变性而受到影响。

这些是需要改进的重大医学领域,只有通过改进,才能在患者护理方面看到实际的进展。

然而,第一步将挑战通常将科学领域分割成半隔离单元的范式。分子生物学在医学领域的成功整合为先例,表明医学与计算机科学的协同结合也有类似的潜力。这个跨学科的方法对于推动患者护理中的现实发展至关重要。

总结来说,我们需要一类新型医生,他们具备理解和有效运用人工智能的技能。这可能需要重构我们当前的医学培训体系。

《连接汽车与重叠监管的故事》

原文:towardsdatascience.com/a-tale-of-connected-cars-and-overlapping-regulations-8697dc5fbf36?source=collection_archive---------8-----------------------#2024-01-18

一项案例研究,审视数据法案、GDPR 与 AI 法案对连接汽车的影响

Tea MustaćTowards Data Science Tea Mustać

·发表于 Towards Data Science ·9 分钟阅读·2024 年 1 月 18 日

--

使许多智能产品制造商的生活变得更加复杂的最新添加之一是欧盟数据法案(EU Data Act),[1] 该法案现在已经在 GDPR 之上生效,等到 AI 法案生效后,它将成为三明治中的一层。这个三明治是否永远会保持所有的层次?当然不会,但在广泛的情况和各种产品中,的确会如此。为什么?为了回答这个问题,让我们考虑一下 IntelliCar 的案例研究,这是一家新的(虚构的)智能连接汽车制造商,决心征服这个强劲的市场和其中的一些主导玩家。

免责声明:这是一部虚构作品,然而,与正在运营或已解散的公司,或其产品中实际实施的技术的相似之处绝非巧合。

图片来源:Ant RozetskyUnsplash

IntelliCar 案例研究

IntelliCar 是一家总部位于欧洲的公司,最近开始为欧洲市场生产智能汽车。为了在看向神奇镜子并询问谁拥有最聪明的汽车时得到理想的答案,IntelliCar 经过深思熟虑,决定为其超级智能汽车配备:面部和情绪识别,自动调节车内温度,并在驾驶员打瞌睡时发出警告,基于使用情况的可选汽车保险,[2] 他们独有的由 ChatGPT 驱动的虚拟助手,当然还有其他一系列提升安全性和驾驶体验的技术。然而,已提到的三项技术足以让我说明问题,所以我在此打住。说实话,单单这三项技术中的任何一项都足以触发欧盟数据法、GDPR 和 AI 法案的适用,但我想提一下欧盟数据法中的一些有趣条款(这篇文章将更侧重于此),所以请耐心听我说。

GDPR

首先,关于 GDPR 的情况在这个案例中是相当直接的。我们车上的三项技术都将收集(大量的)个人数据。

这辆车首先会收集面部数据,以识别用户并检查驾驶员是否已同意后续的处理操作。(现在,我们不能指望 IntelliCar 也考虑到这初步的数据处理行为,这实在太复杂了,而且主流厂商也没怎么重视这个问题,所以作为一家初创公司,他们当然可以选择视而不见吧?)如果同意已被记录,车辆将继续收集和处理面部表情,以便调节车内温度,在出现打瞌睡迹象时发送警报,甚至通过语音助手功能询问驾驶员发生了什么事。其次,如果驾驶员还选择了基于使用情况的保险,车辆将收集可以归属于特定已识别并同意的驾驶员的使用数据。这些数据随后将被传输给保险公司,由其处理并调整保险费率。最后,通过说“嘿,IntelliCar(或用户自定义的任何名字)”,汽车的语音助手将激活。然后,几乎可以向车内发出无限数量的请求,包括播放音乐、询问方向,甚至在网上搜索信息,因为如你所记得,我们的虚拟助手由 ChatGPT 提供支持,因此能够合理地执行这些请求。所有收集和处理的数据肯定是个人数据,因为面部、声音和特定(已识别)驾驶员的习惯,都构成了可以识别驾驶员的信息(最显然的是在这种情况下由 IntelliCar 来识别驾驶员)。

好吧,没什么新鲜的内容。GDPR 当然适用于联网汽车。我们的三明治里的第一块面包就这样放下了。

AI 法案

关于人工智能法案的情况稍微复杂一些,但正如我们所看到的,其要点是人工智能法案仍然适用。如果有任何变化,那就是需要评估是否存在需要遵守的具体义务。

那么,让我们从最明显的开始。面部和情感识别系统无疑是能够生成输出的基于机器的系统,例如,在这种情况下,会影响物理环境的推荐或决策,即车内温度(第 3 条)。IntelliCar 是开发和实施该系统的公司,因此也是该系统的提供方。所以现在只剩下确定他们需要遵守哪些(如果有的话)义务。为了解答这个问题,我们可以首先确认,面部和情感识别系统被临时列在附录三中,作为高风险人工智能系统。唯一可能摆脱法案所有义务的方式是进行风险评估,并阐明他们的特定系统实际上不会对受影响的人群构成高风险,因为已经采取了足够的数据保护措施,且该系统做出的推荐和决策重要性较低。即使评估结果是积极的,意味着系统最终并不是那么高风险,这个评估仍然必须是详尽的、文档化的,并提交给主管部门。

用于自动化保险调整的功能记录数据稍微复杂一些,因为在这里并不是公司实际访问或实施人工智能系统,而是它提供数据(至少应该是这样)。数据提供者(幸运的是)并不属于人工智能法案下的角色,因此只要有足够的合同和文档保障措施在位,我们应该是安全的。但前提是 IntelliCar 没有以某种方式显著地调整该系统以适配他们的汽车,这样也不算太令人惊讶。在这种情况下,我们又回到了起点,IntelliCar 再次被视为提供方,仍然需要评估至少一些风险。

最后,我们的虚拟助手可能是其中最麻烦的一个,因为我们首先需要确定 IntelliCar 是技术的部署方还是提供方。为了简单起见,假设在这种情况下,IntelliCar 使用了 ChatGPT 企业版插件,并且仅通过内部数据进行定制。因此,希望他们仅仅是部署了该系统,且只对选择一个可能不合规的系统负责。但他们可以把这个问题留给未来的自己。首先是征服市场,不管未来的成本如何。

数据法案

现在,终于来到了我们联网汽车合规三明治中的最后一个(虽然绝对不是最后一个,但在这里我们考虑的最后一个)秘密成分——数据法案。就在这里,我们的 IntelliCar 作为联网产品的制造商,会在三个方面受到攻击(很直接地)。为了更好地关注这部在公众中获得的关注度远远不足的法案,这里有很多陷阱需要注意。

数据法案的主要目的是通过赋予用户各种访问权,来增强用户的能力,用户不仅可以访问在使用联网产品过程中收集的个人数据,还可以访问非个人数据,例如表示硬件状态和故障的数据(序言第 15 条)。现在,尽管在谈到联网产品时,这些产品大多是由自然人使用,我们可以相对肯定地说,收集的很多数据将是个人数据。但仍然需要牢记,用户必须能够访问所有收集的数据(包括解释原始数据所需的元数据)。而且这一点必须容易实现,安全可靠,免费提供,并且最好是以可理解的机器可读格式,并且可以直接访问。(小菜一碟!)当然,法案还带来了其他一堆义务,特别是关于信息共享的义务,具体取决于某家公司(或自然人)在其中的角色。我不会详细讨论所有内容,但我会提到几个特别有趣的,与我虚拟场景相关的内容。

第一个陷阱是该法案如何处理商业机密。具体来说,在用户无法直接访问数据的情况下,数据必须由数据持有者提供给用户。现在,这些数据对持有它的公司来说将非常有价值,甚至可能有价值到将其视为商业机密。这些机密实际上是具有商业价值的技术或组织信息,故意保持机密,并限制访问。因此,尽管单个数据点可能不值得获得这种地位,但当我们考虑由收集的数据点组成的更复杂的数据集合,可能还会通过第三方数据甚至推理进行补充时,这些集合很可能值得获得商业机密保护。虽然 GDPR 永远不会考虑用户无法访问基于其数据构建的个人资料的想法,但数据法案却考虑了这种可能性。主要是因为它还涉及非个人数据的共享。因此,在某些情况下,如果证明有遭受重大经济损失的风险,数据持有者可以基于商业机密的理由拒绝提供请求的数据。这一例外可能为公司提供一些余地,最终不需要共享所有有价值的数据。

第二个特点与我们的基于使用量的保险费有关,因为该法案也规范了智能合约。即“使用计算机程序[来]自动执行协议……利用一系列电子记录”的合同。一个这样的智能合约的例子可能是基于实时数据的自动化保险调整。在这方面,一个重要的义务是必须实施智能合约的停止开关,即“一个机制……用于终止交易的继续执行,并且……包括可以重置或指示合约停止或中断操作的内部功能”。这个停止开关引发了关于其对驾驶员、IntelliCar 及保险公司可能产生的后果的重要问题。具体来说,它提出了这样一些问题:谁有权使用停止开关?什么时候可以使用它(合同毕竟是合同,它们的执行在大多数情况下是一件合法且有益的事)?当有人使用它时会发生什么(保险费是否会恢复到默认模式?)?点击停止开关是否可以撤销(如何处理未记录的驾驶时间)?所有这些都必须(很可能)通过合同在相关方之间进行规制,这绝非小事。

最后,我们需要考虑的另一个头疼问题是虚拟助手也受《数据法案》(第 31 条)的明确监管。在该法案中,虚拟助手指的是“能够处理需求、任务或问题的软件,包括基于音频、书面输入、手势或动作的需求,并且根据这些需求、任务或问题,提供对其他服务的访问或控制连接产品的功能”。这基本上打开了潘多拉的盒子,不仅对我们的智能汽车生产商如此,可能也会波及到开发虚拟助手的公司,可能会将他们拖入另外一大堆 70 页的立法文本中去遵守。(仿佛他们已经没有更多的事情要做了。)至于在这种情况下商业机密的辩解(或许说借口)如何发挥作用,谁也无法预测。

最后的思考

随着欧盟数据和技术法规不断增加,我别无选择,只能想象它们如何共同运作,或者更确切地说,是否能够共同运作。最重要的是,因为遵守这些法规所要求的所有义务(这些法规到底怎么了)不是一个人能够完成的工作。除非那个人是一个机器(无恶意)并且拥有无限的脑力存储能力。GDPR 本身就已经是一个合规难题了,所以当我们在此基础上加入所有这些新规定时,你的合规预算现在需要考虑一个律师团队,而这个团队还必须能够与程序员团队沟通,程序员团队则能够将所有这些义务编程到系统中。我选择智能汽车作为我的例子,是因为所有这些技术已经存在并且在任何一辆联网汽车中得到了应用。而所有上述的评估、义务和考量(再一次强调复杂性)都加在了所有汽车必须遵守的大量技术和安全法规之上。(包括那些笨重且没有联网的汽车。)现在,随着所有新功能的加入,联网汽车基本上变成了四个轮子上的合规噩梦。别无他法,只能祝你好运,让“饥饿游戏”开始吧!

图片由Brian McGowan提供,来自Unsplash

[1] 2023 年 12 月 13 日欧洲议会和理事会第 2023/2854 号条例,关于数据公平访问和使用的统一规则,并修改了 2017/2394 号条例和 2020/1828 号指令(数据法),eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX%3A32023R2854&qid=1705523017718

[2] Jason Metz, 《基于使用的汽车保险如何运作?》,《福布斯》,2023 年 10 月 5 日,www.forbes.com/advisor/car-insurance/usage-based-insurance/

LLM 超参数的视觉解释

原文:towardsdatascience.com/a-visual-explanation-of-llm-hyperparameters-daf61d3b006e?source=collection_archive---------3-----------------------#2024-08-29

彻底理解温度、Top-k、Top-p、频率和存在惩罚。

Jenn J.Towards Data Science Jenn J.

·发布于 Towards Data Science ·阅读时间 7 分钟 ·2024 年 8 月 29 日

--

理解温度、Top-k、Top-p、频率和存在惩罚可能会有点挑战,尤其是在你刚开始接触 LLM 超参数的时候。“Top-k”和“存在惩罚”之类的术语一开始可能会让人感到有些不知所措。

当你查找“Top-k”时,你可能会看到类似这样的定义:“Top-k 采样将模型选择下一个单词的范围限制为仅选择前 k 个最有可能的选项,基于它们的预测概率。”这听起来很多!但当你在进行提示工程时,这究竟如何帮助你呢?

如果你像我一样,最喜欢通过视觉学习,那么让我们一起拆解这些内容,彻底理解这些概念,让它们变得容易理解。

LLMs 的内部工作原理

在我们深入探讨 LLM 超参数之前,让我们做一个简单的思维实验。试想听到“一个杯子……”这个短语。大多数人会期望下一个单词是“咖啡”(如果你是茶爱好者,可能是“茶”!)你可能不会立刻想到“星星”或“勇气”。

这里发生的事情是,我们本能地预测接下来的单词是最可能的那个,显然,“咖啡”比“星星”更有可能。

这类似于 LLMs(大语言模型)的工作方式——它们计算可能的下一个单词的概率,并根据这些概率选择一个单词。

从高层次来看,超参数是调整我们如何选择下一个可能单词的方式。

让我们从最常见的超参数开始:

温度

温度控制模型输出的随机性。较低的温度使输出更加确定性,偏向选择更有可能的单词,而较高的温度则允许通过考虑不太可能的单词来产生更多的创造性。

在我们的“杯中的…”示例中,将温度设置为 0 会使模型倾向于选择最可能的单词,即“咖啡”。

图片由作者提供。

随着温度的升高,不同单词之间的采样概率开始趋于平衡,促使模型生成高度不寻常或意外的输出。

注意,将温度设置为 0 并不能使模型完全确定性,尽管它非常接近。

使用场景

  • 低温度(例如,0.2):适用于需要精确和可预测结果的任务,例如技术写作或正式文档编写。

  • 高温度(例如,0.8 或以上):适用于创意任务,如讲故事、诗歌创作或头脑风暴。

最大令牌数

最大令牌数定义了模型在响应中可以生成的最大令牌数(令牌可以是单词或单词的一部分)。令牌是模型处理的最小文本单位。

令牌与单词之间的关系:

  • 1 个单词 = 1~2 个令牌:在英语中,典型的单词通常会被拆分为 1 到 2 个令牌。例如,“cat”这样的简单单词可能是一个令牌,而“unbelievable”这样的复杂单词可能会被拆分成多个令牌。

  • 一般经验法则:通过将令牌数除以 1.5(大致平均值),可以粗略估计单词数。

使用场景

  • 低最大令牌数(例如,50):适用于需要简短响应的任务,例如标题、简短总结或简洁的回答。(注意,模型可能会截断输出响应)

  • 高最大令牌数(例如,500):适用于生成较长的内容,如文章、故事或详细解释。

Top-k

Top-k 采样限制模型从最可能的k个下一个单词中进行选择。通过缩小选择范围,它有助于减少生成无关或无意义输出的几率。

在下图中,如果我们将k设置为 2,模型只会考虑两个最可能的下一个单词——在这种情况下,“咖啡”和“勇气”。然后,这两个单词会被重新抽样,调整它们的概率使其总和为 1,从而确保选择其中一个。

图片由作者提供。

使用场景:

  • 低 k(例如,k=10):最适合结构化任务,在这些任务中你希望保持焦点和连贯性,例如总结或编码。

  • 高 k(例如,k=50):适用于创造性或探索性任务,在这些任务中你希望引入更多变化而不失去连贯性。

Top-p

Top-p 采样选择那些其联合概率超过阈值p(例如,0.9)的最小单词集,从而允许根据上下文进行更敏感的词语选择。

在下面的图表中,我们从最可能的单词“coffee”开始,它的概率是 0.6。由于这个概率低于我们的阈值p = 0.9,我们加入下一个单词“courage”,它的概率是 0.2。加起来,这两个词的总概率是 0.8,仍然低于 0.9。最后,我们考虑单词“dreams”,它的概率是 0.13,总概率达到 0.93,超过了 0.9。此时,我们停止选择,选择了前两个最可能的单词。

图片由作者提供。

用例:

  • 低 p 值(例如,p=0.5):适用于需要简洁且直截了当的输出的任务,如新闻标题或说明性文本。

  • 高 p 值(例如,p=0.95):适用于更开放的任务,如对话生成或创意内容生成,这类任务需要更多样化的回应。

频率惩罚

频率惩罚减少了模型在文本中重复相同单词的可能性,促进了多样性并减少了输出中的冗余。通过应用此惩罚,模型被鼓励使用新单词,而不是重复已经出现过的单词。

频率惩罚是通过以下公式计算的:

调整后的概率 = 初始概率 / (1 + 频率惩罚 * 出现次数)

例如,假设“sun”这个词的概率为 0.5,并且它在文本中已经出现了两次。如果我们将频率惩罚设置为 1,那么“sun”的调整概率将是:

调整后的概率 = 0.5 / (1 + 1 * 2) = 0.5 / 3 = 0.16

图片由作者提供。

用例:

  • 高惩罚(例如,1.0):适合生成内容时,重复会分散注意力或不受欢迎的情况,例如写作论文或研究报告。

  • 低惩罚(例如,0.0):当重复可能是必要或有益的情况时有用,例如在诗歌、咒语或某些营销口号中。

存在惩罚

存在惩罚与频率惩罚类似,但有一个关键区别:它惩罚模型重复使用任何已经出现的单词或短语,不管它出现了多少次

换句话说,重复 2 次单词和重复 20 次单词的后果是一样的。

调整概率的公式为:

调整后的概率 = 初始概率 / (1 + 存在惩罚 * 存在次数)

让我们回顾之前使用“sun”这个词的例子。我们不是将惩罚乘以“sun”出现的频率,而是简单地检查它是否已经出现过——在这种情况下,它已经出现过,因此我们将其计为 1。

如果我们将存在惩罚设置为 1,那么调整后的概率将是:

调整后的概率 = 0.5 / (1 + 1 * 1) = 0.5 / 2 = 0.25

这种减少使得模型不太可能再次选择“sun”,即使“sun”只在文本中出现过一次,也会鼓励使用新的单词或短语。

图片由作者提供。

使用场景:

  • 高惩罚(例如,1.0):非常适合探索性或头脑风暴会议,在这些会议中,你希望模型不断引入新思想或新话题。

  • 低惩罚(例如,0.0):适用于强调关键术语或思想的重要任务,如技术文档或教学材料。

频率惩罚和出现惩罚通常是一起使用的

现在我们已经了解了基础知识,接下来让我们深入探讨频率和出现惩罚是如何一起使用的。不过提前提醒一下——它们是强大的工具,但重要的是要小心使用,以获得最佳效果。

何时使用它们:

  • 内容生成

  • 防止冗余

何时不使用它们

  • 技术写作:在技术文档或特定指令中,如果一致的术语至关重要,使用这些惩罚可能适得其反。

  • 品牌信息传递:如果你正在生成的内容高度依赖于特定品牌的语气或关键词,减少重复可能会削弱品牌的声音。

到现在为止,你应该对温度、Top-k、Top-p、频率和出现惩罚是如何协同作用,塑造语言模型的输出有了更清晰的理解。如果你仍然觉得有点复杂,没关系——这些概念需要一些时间才能完全理解。只要不断实验和探索,你会很快掌握的。

如果你觉得像这样的视觉内容有帮助,并希望看到更多,我们很高兴在我们的Discord 社区见到你。这里是一个我们分享想法、互相帮助并一起学习的空间。

语义文本切分的可视化探索

原文:towardsdatascience.com/a-visual-exploration-of-semantic-text-chunking-6bb46f728e30?source=collection_archive---------1-----------------------#2024-09-19

Dalle3 对“语义切分”的解读。图像由作者生成。

使用嵌入和可视化工具将文本切分为有意义的片段

Robert Martin-ShortTowards Data Science Robert Martin-Short

·发布于 Towards Data Science ·阅读时间 18 分钟·2024 年 9 月 19 日

--

本文解释了语义文本切分(semantic text chunking)这一技术,旨在自动将相似的文本片段分组,可以作为检索增强生成(RAG)或类似应用的预处理阶段的一部分。我们通过可视化来理解切分过程的作用,并探索一些涉及聚类和基于 LLM 的标注的扩展。完整代码请查看 这里

自动化的信息检索和大规模文本摘要有许多有用的应用。最为成熟的应用之一是检索增强生成(RAG),它涉及从大型语料库中提取相关文本片段——通常通过语义搜索或其他筛选步骤——以响应用户提问。然后,文本片段由 LLM 进行解读或摘要,旨在提供高质量、准确的答案。为了使提取的片段尽可能与问题相关,它们具有语义一致性是非常有帮助的,这意味着每个片段都“关于”一个特定概念,并且包含有用的信息包。

Chunking 不仅仅在 RAG 中有应用。想象一下我们有一本复杂的文档,比如书籍或期刊文章,并且希望快速理解它包含的关键概念。如果文本能够被聚类成语义上连贯的组别,然后对每个组别进行某种方式的总结,这可以极大地加快获得洞察的速度。优秀的工具包BertTopic(可以参考这篇文章获取一个不错的概述)可以在这里提供帮助。

对切分块的可视化也可以提供有益的洞察,无论是作为最终产品,还是在开发过程中。人类是视觉学习者,我们的大脑在从图表和图像中获取信息时,比从文本流中获取信息要快得多。根据我的经验,要理解一个切分算法对文本做了什么——以及最佳的参数是什么——如果没有某种方式可视化这些块,或者完全读完它们(对于大型文档来说,这是不现实的),是非常困难的。

在这篇文章中,我们将探讨一种方法,将文本切分为语义上有意义的块,重点是使用图形和绘图来理解其发生的过程。在此过程中,我们将涉及降维和嵌入向量的层次聚类,以及使用大语言模型(LLM)对这些块进行总结,以便我们可以快速查看其中包含的信息。我的希望是,这能激发正在研究语义切分的任何人思考,将其作为应用中的潜在工具。我将在这里使用 Python 3.9、LangChain 和 Seaborn,具体细节可以参见代码仓库

1. 什么是语义切分(semantic chunking)?

有几种标准的切分类型,如果你想了解更多,我推荐这篇优秀的教程,它也是这篇文章的灵感来源。假设我们处理的是英文文本,最简单的切分方式是基于字符的切分,我们选择一个固定长度的字符窗口,然后将文本分成该长度的块。我们还可以选择在块之间添加重叠,以保留它们之间的顺序关系。这种方法计算上比较简单,但无法保证这些块是语义上有意义的,甚至不一定是完整的句子。

递归切分通常更有用,被视为许多应用的首选算法。该过程接受一系列分隔符(LangChain 中的默认值是 [“\n\n”, “\n”, “ ”, “”])和一个目标长度。然后,它通过递归的方式使用分隔符拆分文本,逐步遍历分隔符列表,直到每个文本块的长度小于或等于目标长度。这种方法在保留完整段落和句子方面更为有效,这一点非常重要,因为它使得文本块更有可能保持连贯性。然而,它并不考虑语义:如果一个句子是紧接在上一个句子后面的,并恰好位于文本块的末尾,这些句子就会被分开。

在语义切分中,实施方法包括 LangChainLlamaIndex,切分是基于顺序块的嵌入之间的余弦距离来进行的。因此,我们首先将文本划分为小的但连贯的组,可能会使用递归切分器。

接下来,我们使用一个已训练好的模型将每个文本块向量化,该模型能够生成有意义的嵌入。通常,这种方法采用基于 Transformer 的双编码器(有关详细信息和示例,请参见 SentenceTransformers 库),或者是像 OpenAI 的终端点 [text-embeddings-3-small](https://platform.openai.com/docs/guides/embeddings),这就是我们在这里使用的模型。最后,我们查看后续文本块嵌入之间的余弦距离,并选择那些距离较大的断点。理想情况下,这有助于创建既连贯又在语义上有区别的文本组。

最近的一个扩展方法叫做 语义双重块合并(有关详细信息,请参见 这篇文章),尝试通过第二次处理并使用某些重组逻辑来扩展这一方法。例如,如果第一次处理在块 1 和块 2 之间设置了断点,但块 1 和块 3 非常相似,它将创建一个新组,包含块 1、块 2 和块 3。如果块 2 例如是一个数学公式或代码块,这种方法会非常有用。

然而,在任何类型的语义切分中,仍然存在一些关键问题:在我们设置断点之前,文本块嵌入之间的距离能有多大?这些文本块实际上代表了什么?我们关心这些问题吗?这些问题的答案取决于具体应用和文本内容。

2. 探索断点

让我们用一个例子来说明如何使用语义分块生成断点。我们将实现我们自己的版本的这个算法,尽管如上所述,也有现成的实现可用。我们的演示文本在这里,它由 GPT-4o 编写的三篇简短的事实性文章组成,并拼接在一起。第一篇是关于保护树木一般重要性的,第二篇是关于纳米比亚的历史,第三篇则更深入探讨了为医疗目的保护树木的重要性。主题选择其实并不重要,但这个语料库代表了一个有趣的测试,因为第一篇和第三篇文章有些相似,但它们被第二篇截然不同的文章所分隔。每篇文章也被分成了聚焦于不同主题的几个部分。

我们可以使用一个基本的RecursiveCharacterTextSplitter来进行初始拆分。这里最重要的参数是块大小和分隔符列表,我们通常在没有一定的文本学科知识的情况下无法知道它们应该是什么。这里我选择了一个相对较小的块大小,因为我希望初始文本块最多只有几句话长。我还选择了合适的分隔符,以避免拆分句子。

# tools from the text chunking package mentioned in this article
from text_chunking.SemanticClusterVisualizer import SemanticClusterVisualizer
# put your open ai api key in a .env file in the top level of the package
from text_chunking.utils.secrets import load_secrets
# the example text we're talking about 
from text_chunking.datasets.test_text_dataset import TestText

# basic splitter
from langchain_text_splitters import RecursiveCharacterTextSplitter
import seaborn as sns
splitter = RecursiveCharacterTextSplitter(
        chunk_size=250,
        chunk_overlap=0,
        separators=["\n\n", "\n", "."],
        is_separator_regex=False
)

接下来我们可以对文本进行拆分。如果任何由分块器生成的文本块小于min_chunk_len参数指定的值,参数将生效。在这种情况下,该文本块将被附加到前一个文本块的末尾。

original_split_texts = semantic_chunker.split_documents(
    splitter, 
    TestText.testing_text, 
    min_chunk_len=100, 
    verbose=True
)

### Output
# 2024-09-14 16:17:55,014 - Splitting text with original splitter
# 2024-09-14 16:17:55,014 - Creating 53 chunks
# Mean len: 178.88679245283018
# Max len: 245
# Min len: 103

现在我们可以使用嵌入模型来嵌入这些拆分。在[SemanticClusterVisualizer](https://github.com/rmartinshort/text_chunking/blob/main/text_chunking/SemanticClusterVisualizer.py)类中,你会看到默认情况下我们使用的是text-embeddings-3-small。这将创建一个包含 53 个向量的列表,每个向量的长度为 1536。直观地说,这意味着每个文本块的语义意义在 1536 维空间中得到表示。对于可视化来说这并不理想,这也是为什么我们稍后会采用降维技术的原因。

original_split_text_embeddings = semantic_chunker.embed_original_document_splits(original_split_texts) 

运行语义分块器会生成类似这样的图表。我们可以将其视为一个时间序列,其中 x 轴表示整个文本的字符距离,y 轴表示后续文本块之间嵌入向量的余弦距离。断点出现在高于第 95 百分位的距离值处。

图表显示了由RecursiveCharacterTextSplitter生成的后续文本块之间的余弦距离。我们可以使用这些距离来建立语义分块的断点。图像由作者生成。

考虑到我们对文本的了解,这个模式是有道理的——有三个大主题,每个主题都有几个不同的部分。除了两个大的峰值之外,其他的断点在哪里并不明确。

这就是主观性和迭代性的体现——根据我们的应用,我们可能需要更大或更小的语块,使用图表帮助我们引导眼睛,选择哪些语块需要被阅读。

有几种方法可以将文本拆分成更细粒度的语块。第一种方法是降低百分位数阈值以生成断点。

通过选择较低的百分位数阈值生成的断点。图像由作者生成。

这会创建 4 个非常小的语块和 8 个较大的语块。例如,如果我们看看前四个语块,拆分看起来语义上是合理的,尽管我认为第四个语块有点太长,因为它包含了第一篇文章中大部分的“经济重要性”、“社会重要性”和“结论”部分。

设置百分位数阈值为 0.8 生成的前四个语义语块。图像由作者生成。

除了仅仅改变百分位数阈值外,另一种思路是递归地应用相同的阈值。我们从对整个文本创建断点开始。然后,对于每个新创建的语块,如果该语块超过某个长度阈值,我们就在该语块内部创建断点。这一过程会一直进行,直到所有语块都低于长度阈值。尽管这种方法在某种程度上是主观的,但我认为它更贴近人类的做法,因为人类会首先识别出非常不同的文本组,然后逐步缩小每个组的大小。

它可以通过栈来实现,如下所示。

def get_breakpoints(
    embeddings: List[np.ndarray],
    start: int = 0,
    end: int = None,
    threshold: float = 0.95,
) -> np.ndarray:
    """
    Identifies breakpoints in embeddings based on cosine distance threshold.

    Args:
        embeddings (List[np.ndarray]): A list of embeddings.
        start (int, optional): The starting index for processing. Defaults to 0.
        end (int, optional): The ending index for processing. Defaults to None.
        threshold (float, optional): The percentile threshold for determining significant distance changes. Defaults to 0.95.

    Returns:
        np.ndarray: An array of indices where breakpoints occur.
    """
    if end is not None:
        embeddings_windowed = embeddings[start:end]
    else:
        embeddings_windowed = embeddings[start:]

    len_embeddings = len(embeddings_windowed)
    cdists = np.empty(len_embeddings - 1)

    # get the cosine distances between each chunk and the next one
    for i in range(1, len_embeddings):
        cdists[i - 1] = cosine(embeddings_windowed[i], embeddings_windowed[i - 1])

    # get the breakpoints
    difference_threshold = np.percentile(cdists, 100 * threshold, axis=0)
    difference_exceeding = np.argwhere(cdists >= difference_threshold).ravel()

    return difference_exceeding

def build_chunks_stack(
    self, length_threshold: int = 20000, cosine_distance_percentile_threshold: float = 0.95
) -> np.ndarray:
    """
    Builds a stack of text chunks based on length and cosine distance thresholds.

    Args:
        length_threshold (int, optional): Minimum length for a text chunk to be considered valid. Defaults to 20000.
        cosine_distance_percentile_threshold (float, optional): Cosine distance percentile threshold for determining breakpoints. Defaults to 0.95.

    Returns:
        np.ndarray: An array of indices representing the breakpoints of the chunks.
    """

    # self.split texts are the original split texts 
    # self.split text embeddings are their embeddings 
    S = [(0, len(self.split_texts))]
    all_breakpoints = set()
    while S:
        # get the start and end of this chunk
        id_start, id_end = S.pop()

        # get the breakpoints for this chunk
        updated_breakpoints = self.get_breakpoints(
            self.split_text_embeddings,
            start=id_start,
            end=id_end,
            threshold=cosine_distance_percentile_threshold,
        )
        updated_breakpoints += id_start

        # add the updated breakpoints to the set
        updated_breakpoints = np.concatenate(
            (np.array([id_start - 1]), updated_breakpoints, np.array([id_end]))
        )

        # for each updated breakpoint, add its bounds to the set and
        # to the stack if it is long enough
        for index in updated_breakpoints:
            text_group = self.split_texts[id_start : index + 1]
            if (len(text_group) > 2) and (
                self.get_text_length(text_group) >= length_threshold
            ):
                S.append((id_start, index))
            id_start = index + 1
        all_breakpoints.update(updated_breakpoints)

    # get all the breakpoints except the start and end (which will correspond to the start
    # and end of the text splits)
    return np.array(sorted(all_breakpoints))[1:-1]

我们选择的length_threshold也是主观的,可以通过图表来指导选择。在这种情况下,1000 的阈值似乎效果不错,它将文章很好地分成了简短且语义上有所不同的语块。

通过对原始语块嵌入的余弦距离时间序列运行递归语义拆分器生成的断点。图像由作者生成。

看着与第一篇文章对应的语块,我们看到它们与 GPT4-o 在撰写文章时创建的不同部分紧密对齐。显然,在这种特定的文章情况下,我们本可以仅仅通过"\n\n"进行拆分就完成了,但我们希望采用一种更通用的方法。

通过上述递归断点生成方法生成的前六个语义语块。图像由作者生成。

2. 聚类语义拆分

现在我们已经生成了一些候选的语义语块,查看它们之间的相似性可能会很有用。这将帮助我们了解它们包含了什么信息。我们将继续嵌入这些语义语块,然后使用 UMAP 将生成的嵌入的维度降至 2D,以便我们能够绘制它们。

UMAP 代表均匀流形近似与投影,是一种强大的通用降维技术,能够捕捉非线性关系。关于它如何工作的详细解释可以在这里找到。这里使用它的目的是在 1536 维空间中捕捉嵌入块之间的某些关系,并将其呈现在二维图中。

from umap import UMAP

dimension_reducer = UMAP(
            n_neighbors=5, 
            n_components=2, 
            min_dist=0.0, 
            metric="cosine", 
            random_state=0
)
reduced_embeddings = dimension_reducer.fit_transform(semantic_embeddings)

splits_df = pd.DataFrame(
            {
                "reduced_embeddings_x": reduced_embeddings[:, 0],
                "reduced_embeddings_y": reduced_embeddings[:, 1],
                "idx": np.arange(len(reduced_embeddings[:, 0])),
            }
)

splits_df["chunk_end"] = np.cumsum([len(x) for x in semantic_text_groups])

ax = splits_df.plot.scatter(
            x="reduced_embeddings_x", 
            y="reduced_embeddings_y", 
            c="idx", 
            cmap="viridis"
)

ax.plot(
            reduced_embeddings[:, 0],
            reduced_embeddings[:, 1],
            "r-",
            linewidth=0.5,
            alpha=0.5,
)

UMAP 对n_neighbors参数非常敏感。通常,n_neighbors值越小,算法越注重利用局部结构来学习如何将数据投影到低维空间。将此值设置得太小可能会导致投影无法很好地捕捉数据的宏观结构,通常随着数据点数量的增加,这个值应该增大。

下面是我们数据的投影,它非常有信息量:显然,我们有三个意义相似的聚类,第一个和第三个聚类比任何一个与第二个聚类更相似。上图中的idx色条显示了块的编号,而红线则给出了块的顺序。

上一节生成的语义拆分的嵌入在 UMAP 投影中的图示。idx 表示按生成顺序排列的块的索引,这些块是通过遍历文本生成的。图像由作者生成。

自动聚类怎么样?如果我们想将文本块分组为更大的段落或主题,这将非常有帮助。例如,这可以作为一种有用的元数据,在具有混合搜索的 RAG 应用程序中进行过滤。我们还可能能够将文本中相距较远的块(因此在第一节中的标准语义分块方法下不会被分组)但具有相似意义的块进行分组。

有许多聚类方法可以在这里使用。HDBSCAN 是一个可能的选择,它是BERTopic软件包推荐的默认方法。然而,在这种情况下,层次聚类似乎更有用,因为它可以给我们提供任何出现的群组相对重要性的感知。要运行层次聚类,我们首先使用 UMAP 将数据集的维度降低到更少的组成部分。只要 UMAP 在这里工作得很好,组成部分的确切数量应该不会显著影响生成的聚类。然后,我们使用scipy 的层次模块来执行聚类,并使用 seaborn 绘制结果。

from scipy.cluster import hierarchy
from scipy.spatial.distance import pdist
from umap import UMAP
import seaborn as sns

# set up the UMAP
dimension_reducer_clustering = UMAP(
            n_neighbors=umap_neighbors,
            n_components=n_components_reduced,
            min_dist=0.0,
            metric="cosine",
            random_state=0
)
reduced_embeddings_clustering = dimension_reducer_clustering.fit_transform(
    semantic_group_embeddings
)

# create the hierarchy
row_linkage = hierarchy.linkage(
    pdist(reduced_embeddings_clustering),
    method="average",
    optimal_ordering=True,
)

# plot the heatmap and dendogram
g = sns.clustermap(
    pd.DataFrame(reduced_embeddings_clustering),
    row_linkage=row_linkage,
    row_cluster=True,
    col_cluster=False,
    annot=True,
    linewidth=0.5,
    annot_kws={"size": 8, "color": "white"},
    cbar_pos=None,
    dendrogram_ratio=0.5
)

g.ax_heatmap.set_yticklabels(
  g.ax_heatmap.get_yticklabels(), rotation=0, size=8
)

结果也相当有信息量。在这里,n_components_reduced为 4,因此我们将嵌入的维度降到了 4D,从而生成一个具有 4 个特征的矩阵,每一行代表一个语义块。层次聚类已经识别出两个主要组(即树和纳米比亚)、树类中的两个大子组(即医学用途与其他)以及一些可能值得探索的其他组。

生成的语义块的 UMAP 投影的层次聚类。图像由作者生成。

请注意,BERTopic 使用类似的技术进行主题可视化,可以看作是对这里所展示内容的扩展。

这在我们探索语义块划分中的作用是什么?根据结果,我们可以选择将一些块组合在一起。这再次是一个相当主观的过程,尝试几种不同的分组方式可能很重要。假设我们查看了树状图并决定希望有 8 个独立的组。我们可以相应地切割层次结构,返回与每个组关联的聚类标签并将其绘制出来。

cluster_labels = hierarchy.cut_tree(linkage, n_clusters=n_clusters).ravel()
dimension_reducer = UMAP(
  n_neighbors=umap_neighbors, 
  n_components=2, 
  min_dist=0.0, 
  metric="cosine", 
  random_state=0
)
reduced_embeddings = dimension_reducer.fit_transform(semantic_embeddings)

splits_df = pd.DataFrame(
            {
                "reduced_embeddings_x": reduced_embeddings[:, 0],
                "reduced_embeddings_y": reduced_embeddings[:, 1],
                "cluster_label": cluster_labels,
            }
        )

splits_df["chunk_end"] = np.cumsum(
            [len(x) for x in semantic_text_groups]
        ).reshape(-1, 1)

ax = splits_df.plot.scatter(
            x="reduced_embeddings_x",
            y="reduced_embeddings_y",
            c="cluster_label",
            cmap="rainbow",
        )

ax.plot(
  reduced_embeddings[:, 0],
  reduced_embeddings[:, 1],
  "r-",  
  linewidth=0.5,
  alpha=0.5,
)

结果图如下所示。我们有 8 个聚类,它们在二维空间中的分布看起来合理。这再次证明了可视化的重要性:根据文本、应用和利益相关者的不同,适当的组数和分布可能会有所不同,而检查算法所做的唯一方式就是通过绘制这样的图形。

用于给语义块的 UMAP 投影嵌入着色的语义块聚类 ID。图像由作者生成。

3. 聚类标签

假设在经过上述几个步骤的迭代后,我们已经确定了满意的语义分裂和聚类。那么接下来我们需要问,这些聚类实际上代表了什么?显然,我们可以阅读文本并找出答案,但对于大量语料来说这是不切实际的。相反,我们可以使用大型语言模型(LLM)来帮助。具体来说,我们将把每个聚类相关的文本提供给 GPT-4o-mini,并要求其生成总结。使用 LangChain 来完成这个任务相对简单,下面展示了代码的核心部分。

import langchain
from langchain.prompts import PromptTemplate
from langchain_core.output_parsers.string import StrOutputParser
from langchain.callbacks import get_openai_callback
from dataclasses import dataclass

@dataclass
class ChunkSummaryPrompt:
    system_prompt: str = """
        You are an expert at summarization and information extraction from text. You will be given a chunk of text from a document and your
        task is to summarize what's happening in this chunk using fewer than 10 words. 

        Read through the entire chunk first and think carefully about the main points. Then produce your summary.

        Chunk to summarize: {current_chunk}
    """

    prompt: langchain.prompts.PromptTemplate = PromptTemplate(
        input_variables=["current_chunk"],
        template=system_prompt,
    )

class ChunkSummarizer(object):
    def __init__(self, llm):
        self.prompt = ChunkSummaryPrompt()
        self.llm = llm
        self.chain = self._set_up_chain()

    def _set_up_chain(self):
        return self.prompt.prompt | self.llm | StrOutputParser()

    def run_and_count_tokens(self, input_dict):
        with get_openai_callback() as cb:
            result = self.chain.invoke(input_dict)

        return result, cb

llm_model = "gpt-4o-mini"
llm = ChatOpenAI(model=llm_model, temperature=0, api_key=api_key)
summarizer = ChunkSummarizer(llm)

在我们的 8 个聚类上运行该程序,并使用datamapplot绘制结果,得到如下图。

通过运行 GPT-4o-mini 生成的语义聚类标签。图像由作者生成。

可视化这些分组的另一种方式类似于第二部分中展示的图形,我们将字符数的累积值绘制在横轴上,并展示各组之间的边界。回顾一下,我们曾经有 18 个语义块,现在已经将它们进一步分为 8 个聚类。像这样绘制图表显示了文本的语义内容从头到尾的变化,突出了相似内容并不总是相邻的事实,并且为块的相对大小提供了一个直观的指示。

显示按语义聚类进行文本分段以及各聚类名称的图表。图片由作者生成。

用于生成这些图形的代码可以在这里找到。

4. 在更大的语料库上进行测试

到目前为止,我们已经在一个相对较小的文本量上测试了这个工作流,目的是演示其功能。理想情况下,它也应该能在更大的语料库上使用,而无需进行重大修改。为了测试这一点,我们来尝试将其应用于从古腾堡计划下载的一本书,我选择了《绿野仙踪》。这是一项更具挑战性的任务,因为小说通常不像事实性文章那样按明确的语义区块进行划分。尽管小说通常按章节进行安排,但故事情节可能会呈现连续性的发展,或者在不同的主题之间跳跃。如果语义块分析能够用来揭示不同作者作品中的风格特点,那将非常有趣。

步骤 1:嵌入并生成断点

from text_chunking.SemanticClusterVisualizer import SemanticClusterVisualizer
from text_chunking.utils.secrets import load_secrets
from text_chunking.datasets.test_text_dataset import TestText, TestTextNovel
from langchain_text_splitters import RecursiveCharacterTextSplitter

secrets = load_secrets()
semantic_chunker = SemanticClusterVisualizer(api_key=secrets["OPENAI_API_KEY"])

splitter = RecursiveCharacterTextSplitter(
        chunk_size=250,
        chunk_overlap=0,
        separators=["\n\n", "\n", "."],
        is_separator_regex=False
)

original_split_texts = semantic_chunker.split_documents(
    splitter, 
    TestTextNovel.testing_text, 
    min_chunk_len=100, 
    verbose=True
)
original_split_text_embeddings = semantic_chunker.embed_original_document_splits(original_split_texts)

breakpoints, semantic_groups = semantic_chunker.generate_breakpoints(
    original_split_texts,
    original_split_text_embeddings,
    length_threshold=10000 #may need some iteration to find a good value for this parameter
)

这一步生成了 77 个大小不一的语义块。这里进行了一些随机检查,我对其运行效果有了较高的信心,并且许多语义块最终在章节边界附近进行划分,这也非常合理。

《绿野仙踪》的语义拆分。图片由作者生成。

步骤 2:聚类并生成标签

在查看层次聚类树状图后,我决定尝试将其减少为 35 个聚类。结果揭示了图表左上方的一个异常值(聚类 ID 为 34),这个聚类实际上是文本末尾的一组语义块,包含了对本书分发条款的详细描述。

《绿野仙踪》语义块的二维 UMAP 投影图及其聚类标签。图片由作者生成

每个聚类的描述如下所示,除了第一个聚类外,它们为小说的主要事件提供了一个不错的概览。快速检查与每个聚类相关的实际文本,确认它们是相当准确的总结,尽管再次强调,确定聚类边界的位置是非常主观的。

从《绿野仙踪》中自动为 40 个语义集群选择的名称。此列表提供了故事情节的快速概览。图片由作者生成。

GPT-4o-mini 将离群集群标记为“古腾堡计划允许自由分发未受保护的作品”。与此标签相关的文本对我们来说并不特别有趣,所以我们将其删除,并重新绘制结果。这样可以更清晰地看到小说的结构。

如果我们对更大的集群感兴趣呢?如果我们集中关注高级结构,树状图建议大约有六个语义片段集群,如下图所示。

搜索高级结构 —— 如果我们选择从《绿野仙踪》的语义片段中创建 6 个集群,那么我们得到的就是这个模式。图片由作者生成。

在这个语义空间中,有很多在相距较远的点之间跳跃,这表明主题发生了频繁的突变。考虑到各个集群之间的连接性也很有趣:例如,集群 4 和集群 5 之间没有连接,而集群 0 和集群 1 之间则有很多来回跳跃。

我们能否总结这些较大的集群?事实证明,我们的提示似乎不太适合这种大小的片段,产生的描述要么过于具体(例如集群 0 和 4),要么过于模糊,难以提供有效帮助。改进提示工程——可能涉及多次总结步骤——可能会提高这里的结果。

显示文本按语义集群 ID 分段,并标注上述识别出的六个集群名称的图表。图片由作者生成。

尽管名称不太有用,这张按集群着色的文本片段图仍然是选择性阅读文本的有用指南。我们看到,书籍的开始和结尾都位于相同的集群,这可能与多萝西、托托及其家乡的描述有关——并且与《绿野仙踪》的故事弧线相符,即旅程和随后的归来。集群 1 主要涉及与新角色的相遇,这主要发生在开头,但也偶尔出现在全书中。集群 2 和集群 3 则关注翡翠城和巫师,而集群 4 和集群 5 则大致分别与旅行和战斗相关。

5. 结论性思考

感谢你读到最后!在这里,我们深入探讨了语义分块的概念,以及如何通过降维、聚类和可视化来补充这一过程。主要的收获是,系统地探索不同分块技术和参数对文本的影响非常重要,在此基础上再决定最合适的方法。我的希望是,这篇文章能够激发新的思路,帮助我们如何利用人工智能和可视化工具推动语义分块,并快速从大量文本中提取见解。请随时浏览完整的代码库 github.com/rmartinshort/text_chunking

Mamba 和状态空间模型的视觉指南

原文:towardsdatascience.com/a-visual-guide-to-mamba-and-state-space-models-8d0d3f7d3ea6?source=collection_archive---------4-----------------------#2024-02-22

语言建模的 Transformer 替代方案

Maarten GrootendorstTowards Data Science Maarten Grootendorst

·发布于Towards Data Science ·阅读时长:21 分钟·2024 年 2 月 22 日

--

Transformer 架构是大型语言模型(LLM)成功的重要组成部分。它已被几乎所有当前使用的 LLM 所采用,从像 Mistral 这样的开源模型到像 ChatGPT 这样的封闭源代码模型。

为了进一步提升 LLM 的性能,开发了新的架构,这些架构甚至可能超过 Transformer 架构。其中一种方法是Mamba,一种状态空间模型

状态空间模型的基本架构。

Mamba 是在论文Mamba: 线性时间序列建模与选择性状态空间中提出的。你可以在它的代码库中找到其官方实现和模型检查点。

在这篇文章中,我将介绍状态空间模型在语言建模中的应用,并逐步探索相关概念,以帮助你形成对该领域的直观理解。然后,我们将讨论 Mamba 如何挑战 Transformer 架构。

作为视觉指南,预计会有许多可视化内容,帮助你形成对 Mamba 和状态空间模型的直观理解!

第一部分:Transformer 的问题

为了说明 Mamba 为何是一种如此有趣的架构,让我们首先简要回顾一下 Transformer,并探索其中的一个…

量化的视觉指南

原文:towardsdatascience.com/a-visual-guide-to-quantization-930ebcd9be94?source=collection_archive---------0-----------------------#2024-07-24

解密大型语言模型的压缩技术

Maarten GrootendorstTowards Data Science Maarten Grootendorst

·发表于 Towards Data Science ·阅读时间:20 分钟·2024 年 7 月 24 日

--

正如其名称所示,大型语言模型(LLMs)通常太大,无法在消费者硬件上运行。这些模型的参数可能超过数十亿,并且通常需要具有大量显存的 GPU 来加速推理。

因此,越来越多的研究集中于通过改进训练、适配器等手段使这些模型变得更小。这个领域的一个主要技术叫做量化

在这篇文章中,我将介绍在语言建模背景下的量化领域,并逐步探索这些概念,以培养对该领域的直觉。我们将探索各种方法论、使用案例以及量化背后的原理。

作为一份视觉指南,预计会有许多可视化内容,以帮助你形成关于量化的直觉!

第一部分:LLMs 的“问题”

LLMs 得名于它们所包含的参数数量。如今,这些模型通常拥有数十亿个参数(主要是权重),存储这些参数相当昂贵。

在推理过程中,激活值是输入与权重的乘积,这些值也可能非常大。

因此,我们希望尽可能高效地表示数十亿个值,最小化存储给定值所需的空间。

让我们从头开始,探索在优化数值之前,数值是如何表示的。

如何表示数值

给定的值通常表示为浮点数(或计算机科学中的浮动):一个带有小数点的正数或负数。

这些值由“”或二进制数字表示。IEEE-754 标准描述了如何使用位来表示三个函数之一:符号指数小数部分(或尾数)

这三方面可以结合起来,用来计算在给定位值的情况下一个值:

我们用于表示数值的位数越多,通常精度越高:

内存限制

我们可用的位数越多,可以表示的值的范围就越大。

给定表示方法所能表示的数值范围称为 动态范围,而两个相邻值之间的距离称为 精度

这些位数的一个巧妙特点是,我们可以计算设备存储一个给定值所需的内存量。由于 1 字节等于 8 位,我们可以为大多数浮点数表示形式创建一个基本公式。

注意:实际上,与推理过程中所需的(V)RAM 量相关的因素还包括上下文大小和架构等。

假设我们有一个包含 700 亿参数的模型。大多数模型通常使用 32 位浮点数表示(通常称为全精度),仅加载该模型就需要 280GB 的内存。

因此,尽可能减少表示模型参数所需的位数(以及在训练过程中)是非常有吸引力的。然而,随着精度的降低,模型的准确度通常也会下降。

我们希望在保持准确度的同时减少表示数值所需的位数……这就是 量化 的作用!

第二部分:量化简介

量化旨在将模型参数的精度从较高的位宽(如 32 位浮点数)降低到较低的位宽(如 8 位整数)。

当减少表示原始参数所需的位数时,通常会有一些精度损失(粒度)。

为了说明这一效果,我们可以选取任意一张图片,并只使用 8 种颜色来表示它:

图片来源于 Slava Sidorov

注意到放大部分比原图看起来更“颗粒化”,因为我们只能使用更少的颜色来表示它。

量化的主要目标是减少表示原始参数所需的位数(颜色),同时尽可能保持原始参数的精度。

常见数据类型

首先,让我们看看常见的数据类型以及使用它们而不是 32 位(称为 全精度FP32)表示的影响。

FP16

让我们看一个从 32 位到 16 位(称为 半精度FP16)浮点数的例子:

请注意,FP16 可以表示的值范围比 FP32 小得多。

BF16

为了获得与原始 FP32 相似的值范围,引入了 bfloat 16 作为一种“截断 FP32”的类型:

BF16 使用与 FP16 相同数量的位,但可以表示更广泛的值范围,并且通常用于深度学习应用中。

INT8

当我们进一步减少位数时,我们进入了 基于整数的表示 领域,而不是浮点表示。为了说明这一点,从 FP32 转到只有 8 位的 INT8,结果是原始位数的四分之一:

根据硬件的不同,基于整数的计算可能比浮点计算更快,但并不总是如此。然而,使用更少的位数时,计算通常会更快。

对于每次减少位数,都会执行映射以“压缩”初始的 FP32 表示到较低的位数。

实际上,我们不需要将整个 FP32 范围 [-3.4e38, 3.4e38] 映射到 INT8。我们只需要找到一种方法将数据的范围(模型的参数)映射到 INT8。

常见的压缩/映射方法是 对称非对称 量化,并且它们是 线性映射 的形式。

让我们探索这些方法,将 FP32 量化到 INT8。

对称量化

在对称量化中,原始浮点值的范围被映射到量化空间中围绕零的对称范围。在之前的例子中,请注意量化前后的范围是如何围绕零对称的。

这意味着在浮点空间中量化的零值在量化空间中正好是零。

一种对称量化的好例子叫做绝对最大值(absmax)量化。

给定一个值的列表,我们取 最高 的绝对值 (α) 作为范围来执行线性映射。

注意,[-127, 127] 范围的值表示受限范围。无符号范围是 [-128, 127],并且依赖于量化方法。

由于它是一个围绕零的线性映射,公式是直接的。

我们首先使用以下公式计算一个比例因子(s):

  • b 是我们希望量化到的字节数(8),

  • α最高 绝对值,

然后,我们使用s来量化输入x

填入这些数值后,我们将得到以下结果:

为了恢复原始的 FP32 值,我们可以使用之前计算的缩放因子s)来去量化这些量化后的值。

应用量化和去量化过程以恢复原始值如下所示:

你可以看到某些值,比如3.083.02,被分配到了 INT8,即36。当你将这些值去量化以返回 FP32 时,它们会失去一些精度,变得不可区分。

这通常被称为量化误差,我们可以通过计算原始值和去量化值之间的差异来确定它。

通常,位数越低,我们往往会有更多的量化误差。

非对称量化

非对称量化与对称量化不同,它不是围绕零对称的。相反,它将浮动范围中的最小值(β)和最大值(α)映射到量化范围的最小值和最大值。

我们将要探讨的方法叫做零点量化

注意 0 已经发生了位置偏移?这就是为什么它被称为非对称量化。最小/最大值与 0 之间的距离在范围 [-7.59, 10.8] 内是不相等的。

由于其位置的偏移,我们必须为 INT8 范围计算零点,以执行线性映射。如之前所述,我们还需要计算缩放因子s),但这次要使用 INT8 范围的差值 [-128, 127]。

注意,由于需要计算 INT8 范围内的零点z)以平移权重,这一过程稍显复杂。

如前所述,让我们填写公式:

要将从 INT8 量化的数据还原为 FP32,我们需要使用之前计算的缩放因子s)和零点z)。

除此之外,去量化是直接的:

当我们将对称量化和非对称量化并排放置时,我们可以迅速看到两者方法的差异:

注意对称量化的零中心特性与非对称量化的偏移特性。

范围映射与裁剪

在之前的例子中,我们探讨了如何将给定向量中数值的范围映射到低位表示。尽管这使得整个向量值范围能够被映射,但也带来了一个重大缺点,即离群值

想象你有一个包含以下值的向量:

请注意,某个值远大于其他所有值,它可能被视为一个异常值。如果我们要映射该向量的整个范围,所有较小的值将会被映射为相同的低位表示,并失去它们的区分性:

这就是我们之前使用的 absmax 方法。注意,如果我们没有应用裁剪,非对称量化也会表现出相同的行为。

相反,我们可以选择裁剪某些值。裁剪是指设定原始值的不同动态范围,使得所有异常值都得到相同的值。

在下面的示例中,如果我们手动将动态范围设置为[-5, 5],所有超出该范围的值将被映射为-127 或 127,无论它们的实际值是多少:

主要的优势在于,非异常值的量化误差显著降低。然而,异常值的量化误差却增加了。

校准

在示例中,我展示了选择[-5, 5]这一任意范围的简单方法。选择该范围的过程称为校准,其目的是找到一个尽可能包含更多值的范围,同时最小化量化误差。

执行此校准步骤对所有类型的参数并不相同。

权重(和偏差)

我们可以将 LLM 的权重和偏差视为静态值,因为它们在模型运行之前就已知。例如,Llama 3 的约 20GB 文件大部分由其权重和偏差构成。

由于偏差的数量(百万级)远少于权重的数量(十亿级),偏差通常保持较高的精度(如 INT16),而量化的主要工作集中在权重上。

对于权重,作为静态且已知的值,选择范围的校准技术包括:

  • 手动选择输入范围的百分位

  • 优化原始权重和量化权重之间的均方误差(MSE)。

  • 最小化(KL 散度)原始值与量化值之间的差异

例如,选择百分位数会导致与之前看到的类似的裁剪行为。

激活值

在整个 LLM 中持续更新的输入通常被称为“激活值”。

请注意,这些值被称为激活值,因为它们通常会经过某些激活函数,如 sigmoid 或 relu。

与权重不同,激活值会随每次输入数据进入模型时发生变化,这使得精确量化激活值变得具有挑战性。

由于这些值在每个隐藏层之后都会更新,因此我们只能在推理过程中随着输入数据通过模型时,才能知道它们的值。

广义上来说,校准权重和激活值量化方法有两种:

  • 训练后量化(PTQ)— 训练的量化

  • 量化感知训练(QAT)— 训练/微调期间的量化

第三部分:训练后量化

最流行的量化技术之一是训练后量化(PTQ)。它涉及在训练模型之后对模型的参数(包括权重和激活值)进行量化。

权重的量化是通过对称或非对称量化来执行的。

然而,激活值的量化需要推断模型以获取它们的潜在分布,因为我们不知道它们的范围。

激活值的量化有两种形式:

  • 动态量化

  • 静态量化

动态量化

当数据通过一个隐藏层时,它的激活值被收集:

然后,使用这个激活值的分布来计算量化输出所需的零点z)和缩放因子s)值:

这个过程在每次数据通过新层时都会重复。因此,每一层都有自己独立的zs值,因此也有不同的量化方案。

静态量化

与动态量化相比,静态量化在推断过程中不会计算零点z)和缩放因子(s),而是提前计算好。

为了找到这些值,会使用校准数据集并提供给模型以收集这些潜在分布。

收集到这些值之后,我们可以计算出在推断过程中执行量化所需的sz值。

当你进行实际推断时,sz值不会被重新计算,而是全局使用在所有激活值上进行量化。

通常,动态量化往往更精确一些,因为它只在每个隐藏层计算sz值。然而,它可能会增加计算时间,因为这些值需要被计算出来。

相比之下,静态量化虽然精度较低,但速度更快,因为它已经知道了用于量化的sz值。

4 位量化的领域

低于 8 位量化被证明是一个困难的任务,因为每损失一位,量化误差都会增加。幸运的是,有几种巧妙的方法可以将位数降低到 6 位、4 位,甚至 2 位(尽管使用这些方法将位数降低到 4 位以下通常不建议)。

我们将探讨两种在 HuggingFace 上常见的共享方法:

  • GPTQ — 完整模型在 GPU 上

  • GGUF — 可能会将层卸载到 CPU 上

GPTQ

GPTQ 可以说是目前实践中最著名的 4 位量化方法之一。

它使用非对称量化,并且按层处理,使每一层在继续到下一层之前都能独立处理:

在这个逐层量化过程中,它首先将层的权重转换为逆海森矩阵。它是模型损失函数的二阶导数,告诉我们模型输出对每个权重变化的敏感程度。

简化来说,它本质上展示了每个权重在一个层中的(重要性

在海森矩阵中,与较小值相关的权重更为重要,因为这些权重的微小变化可能会导致模型性能的显著变化。

在逆海森矩阵中,较低的值表示更“重要”的权重。

接下来,我们对权重矩阵中第一行的权重进行量化,然后再解量化:

这个过程允许我们计算量化误差 (q),我们可以使用之前计算的逆海森矩阵(h_1*)加权它。

本质上,我们根据权重的重要性创建一个加权的量化误差:

接下来,我们将这个加权量化误差重新分配到同一行中的其他权重上。这有助于保持网络的整体功能和输出。

例如,如果我们要对第二个权重(即 .3,x_2)执行这个操作,我们将添加量化误差(q)乘以第二个权重的逆海森矩阵(h_2)。

我们可以对给定行中的第三个权重执行相同的过程:

我们不断迭代这一过程,重新分配加权的量化误差,直到所有值都被量化。

这种方法非常有效,因为权重通常是相互关联的。所以当一个权重存在量化误差时,相关的权重会相应地更新(通过逆海森矩阵)。

注意: 作者们 使用了几种技巧来加速计算和提高性能,比如向海森矩阵添加阻尼因子、“懒批处理”和使用 Cholesky 方法预计算信息。我强烈建议查看 这个 YouTube 视频 ,了解相关内容。

提示: 如果你想要一种专注于性能优化和提高推理速度的量化方法,可以查看 EXL2

GGUF

虽然 GPTQ 是一种很好的量化方法,可以在 GPU 上运行完整的 LLM,但你可能并不总是具备这种能力。相反,我们可以使用 GGUF 将 LLM 的任何一层转移到 CPU 上处理。

这使你在没有足够显存的情况下,仍然可以同时使用 CPU 和 GPU。

GGUF 量化方法会频繁更新,并且可能会依赖于位量化的级别。然而,基本原则如下。

首先,给定层的权重被拆分成“super”块,每个块包含一组“sub”块。从这些块中,我们提取尺度因子(s)和 alpha(α):

要对给定的“sub”块进行量化,我们可以使用之前使用过的absmax量化方法。记住,它将给定的权重乘以尺度因子(s)

尺度因子是使用来自“sub”块的信息计算的,但它是使用来自“super”块的信息进行量化的,后者有自己的尺度因子:

这个块级量化使用来自“super”块的尺度因子(s_super)来量化来自“sub”块的尺度因子(s_sub)。

每个尺度因子的量化级别可能不同,“super”块的量化精度通常高于“sub”块的尺度因子。

为了说明,我们来探讨几个量化级别(2 位、4 位和 6 位):

注意:根据量化类型,可能需要一个额外的最小值(m),用于调整零点。这些值的量化方式与尺度因子(s)相同。

查看原始拉取请求,了解所有量化级别的概述。同时,查看此拉取请求,了解使用重要性矩阵进行量化的更多信息。

第四部分:量化感知训练

在第三部分,我们看到如何在训练后对模型进行量化。该方法的一个缺点是,量化过程没有考虑到实际的训练过程。

这就是量化感知训练(QAT)的作用。QAT 的目标是在训练过程中学习量化过程,而不是像后训练量化(PTQ)那样在模型训练完成后进行量化。

QAT 通常比 PTQ 更为准确,因为量化已经在训练过程中考虑了。其工作原理如下:

在训练过程中,引入了所谓的“”量化。这是将权重先量化为例如 INT4,然后再解量化回 FP32 的过程:

这个过程使得模型能够在训练过程中考虑量化过程、损失计算以及权重更新。

QAT 尝试探索损失景观中“”极小值,以最小化量化误差,因为“”极小值往往会导致更大的量化误差。

例如,假设我们在反向传播时没有考虑量化。我们根据梯度下降选择具有最小损失的权重。然而,如果它位于一个“狭窄”的最小值中,这将引入更大的量化误差。

相反,如果我们考虑量化,更新后的权重将在一个“宽阔”的最小值中被选择,量化误差会更小。

因此,尽管 PTQ 在高精度下(例如 FP32)损失较小,但 QAT 在低精度下(例如 INT4)损失更低,这正是我们追求的目标。

1 位 LLM 的时代:BitNet

如前所述,4 位已经相当小了,但如果我们进一步减少它会怎样呢?

这就是BitNet的作用,它用单个 1 位表示模型的权重,使用-11表示给定的权重。3

它通过将量化过程直接注入到 Transformer 架构中来实现。

请记住,Transformer 架构是大多数 LLM 的基础,并由涉及线性层的计算组成:

这些线性层通常以较高精度表示,如 FP16,并且大多数权重就存储在这里。

BitNet 用他们所称的BitLinear替换了这些线性层:

BitLinear 层的工作方式与常规线性层相同,基于权重与激活的乘积来计算输出。

相比之下,BitLinear 层用 1 位表示模型的权重,用 INT8 表示激活:

BitLinear 层像量化感知训练(QAT)一样,在训练期间执行一种“伪”量化,以分析权重和激活的量化效果:

注意:在论文中他们使用了γ代替α,但由于我们在示例中使用了α,所以我这里也使用它。同时,注意β与我们在零点量化中使用的不同,它是平均绝对值。

让我们一步一步地了解 BitLinear。

权重量化

在训练过程中,权重以 INT8 存储,然后使用一种基本策略将其量化为 1 位,这个策略称为符号函数

本质上,它将权重的分布移至围绕 0 的位置,然后将左边的所有内容分配为-1,右边的所有内容分配为 1:

此外,它还跟踪一个值β(平均绝对值),我们将在后续用于反量化。

激活量化

为了量化激活值,BitLinear 利用absmax 量化将激活值从 FP16 转换为 INT8,因为在进行矩阵乘法(×)时需要更高精度。

此外,它还追踪α(最大绝对值,我们稍后会用到它进行反量化。

反量化

我们追踪了α(激活值的最大绝对值β(权重的平均绝对值)**,因为这些值有助于我们将激活值反量化回 FP16。

输出激活值会使用{α,γ}进行重新缩放,以将它们反量化到原始精度:

就是这样!这个过程相对简单,并允许模型仅使用两个值来表示,即-11

通过这种方法,作者观察到随着模型规模的增长,1 位和 FP16 训练模型之间的性能差距变得越来越小。

然而,这仅适用于更大的模型(>30B 参数),小模型之间的差距仍然很大。

所有大型语言模型都在 1.58 位

BitNet 1.58b的引入是为了改善之前提到的缩放问题。

在这种新方法中,每个权重不仅仅是-11,现在还可以取0作为值,使其变为三元。有趣的是,仅添加0就极大地改进了 BitNet,并且可以实现更快的计算。

0 的力量

那么,为什么加 0 会是一个如此重大的改进呢?

它与矩阵乘法密切相关!

首先,让我们探讨一下矩阵乘法一般是如何工作的。在计算输出时,我们将权重矩阵与输入向量相乘。下面是第一层权重矩阵的第一次乘法可视化:

请注意,这次乘法涉及两个操作,各个权重与输入相乘,然后将它们加在一起

与此相反,BitNet 1.58b 成功地避免了乘法操作,因为三元权重本质上告诉你以下内容:

  • 1 — 我想加上这个值

  • 0 — 我不想要这个值

  • -1 — 我想减去这个值

结果是,如果你的权重被量化为 1.58 位,你只需要执行加法:

这不仅可以显著加快计算速度,而且还允许进行特征过滤

通过将给定的权重设置为 0,你现在可以忽略它,而不是像 1 位表示那样加或减权重。

量化

为了进行权重量化,BitNet 1.58b 使用absmean 量化,这是一种我们之前看到过的 absmax 量化的变体。

它只是压缩了权重的分布,并使用绝对均值(α)来量化值。然后它们被四舍五入为-1、0 或 1:

与 BitNet 相比,激活量化是相同的,只有一点不同。激活值不再被缩放到[0, 2ᵇ⁻¹]范围,而是现在被缩放到

[-2ᵇ⁻¹, 2ᵇ⁻¹]改为使用绝对最大值量化

就这样!1.58 比特量化(大部分)需要两种技巧:

  • 添加0以创建三元表示[-1, 0, 1]

  • 绝对均值量化用于权重

结果是,我们获得了轻量级模型,因为它们只有 1.58 个计算高效的比特!

感谢阅读!

这标志着我们在量化方面的旅程结束!希望这篇文章能帮助你更好地理解量化、GPTQ、GGUF 和 BitNet 的潜力。谁知道未来模型会小到什么程度呢?!

若想查看更多与 LLM 相关的可视化内容并支持本期通讯,敬请关注我与 Jay Alammar 共同编写的书籍。该书即将发布!

你可以通过O'Reilly 网站享受免费试读,或者在Amazon上预购本书。所有代码都将上传至Github

如果你像我一样,对人工智能和/或心理学充满热情,欢迎在LinkedInTwitter上加我为好友,或者订阅我的通讯。你也可以在我的个人网站找到一些我的内容**。

[## 探索语言模型 | Maarten Grootendorst | Substack

机器学习工程师,撰写关于人工智能、语言模型和心理学交集的文章。开源开发者(BERTopic…)

newsletter.maartengrootendorst.com

所有没有来源说明的图片均由作者创作——这意味着所有图片(除了一个!)都是我自己做的;)

决策树和梯度提升的可视化理解

原文:towardsdatascience.com/a-visual-understanding-of-decision-trees-and-gradient-boosting-c6bc53f982ce?source=collection_archive---------0-----------------------#2024-07-26

决策树和梯度提升背后的数学原理的可视化解释

Reza BagheriTowards Data Science Reza Bagheri

·发布于 Towards Data Science ·39 分钟阅读·2024 年 7 月 26 日

--

图片由 DALL.E 生成

决策树是一种非参数化的监督学习算法,可用于分类和回归。它使用树状结构来表示决策及其潜在结果。决策树简单易懂,便于解释,并且可以轻松可视化。然而,当决策树模型变得过于复杂时,它不能很好地从训练数据中进行泛化,导致过拟合。

梯度提升是一个集成学习模型,我们将多个弱学习器组合起来,开发一个强学习器。这些弱学习器是单独的决策树,每个学习器都尝试聚焦于前一个学习器的错误。与单一的深度决策树相比,梯度提升通常不容易发生过拟合。

本文将通过可视化的方式解释决策树在分类和回归问题中的直觉。我们将看到这个模型是如何工作的,以及为什么它可能导致过拟合。接下来,我们将介绍梯度提升,并了解它如何提高单个决策树的性能。将从头开始在 Python 中实现一个梯度提升回归器和分类器。最后,数学原理...

逻辑回归的视觉理解

原文:towardsdatascience.com/a-visual-understanding-of-logistic-regression-2e6733844397?source=collection_archive---------5-----------------------#2024-05-09

逻辑回归背后数学原理的直观视觉解释

Reza BagheriTowards Data Science Reza Bagheri

·发表于Towards Data Science·15 分钟阅读·2024 年 5 月 9 日

--

图片由 DALL.E 生成

逻辑回归是一种用于二分类的统计模型。在二分类问题中,目标只有两个类别,因此机器学习算法应将数据分类到这两个类别之一。逻辑回归的名称源自逻辑函数,该函数用于预测一个数据点属于每个类别的概率。逻辑回归在监督学习、金融、医学和社会科学等领域有广泛的应用。

在本文中,我将展示对逻辑回归的视觉理解,以及模型中每个元素的作用。阅读本文后,读者可以对逻辑回归及其局限性有直观的理解。

本文中的所有图片均由作者创建。

一个玩具数据集

我们创建了一个玩具数据集,以演示逻辑回归如何解决分类问题。首先,我们导入所有需要的 Python 库。

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

from sklearn.linear_model import LogisticRegression
from matplotlib.colors import ListedColormap

import…

对 softmax 函数的视觉理解

原文:towardsdatascience.com/a-visual-understanding-of-the-softmax-function-b4d92fdaccfa?source=collection_archive---------2-----------------------#2024-11-03

softmax 函数的数学原理与直觉,以及它在神经网络和 softmax 回归中的应用

Reza BagheriTowards Data Science Reza Bagheri

·发表于 Towards Data Science ·阅读时间 25 分钟·2024 年 11 月 3 日

--

图像由 DALL.E 生成

softmax 函数是统计学和机器学习中最重要的函数之一。它接收一个 K 维实数向量,并将其转换为一个 K 维概率向量,这些概率的总和为 1。Softmax 是 logistic 函数的一个推广,适用于多于两个维度,并且可以用于 softmax 回归(也称为多项式 logistic 回归)来解决具有多个标签的分类问题。softmax 函数还可以作为神经网络在多分类问题中的最后激活函数。在这种情况下,神经网络使用 softmax 激活函数来计算目标每个可能类别的概率。

本文提供了对 softmax 函数的视觉理解,包括其背后的直觉,以及使其在机器学习中具有价值的重要数学性质。我们还讨论了 softmax 函数与 logistic 函数之间的关系,并演示了如何使用 Python 执行 softmax 回归。

本文中的所有图像均由作者创建。

从 logistic 回归到

英伟达最新的多模态大语言模型家族全览

原文:towardsdatascience.com/a-walkthrough-of-nvidias-latest-multi-modal-llm-family-fdc067b59596?source=collection_archive---------6-----------------------#2024-10-10

从 LLaVA、Flamingo 到 NVLM

孟柳·赵Towards Data Science 孟柳·赵

·发表于 Towards Data Science ·阅读时间 6 分钟·2024 年 10 月 10 日

--

近年来,多模态大语言模型(LLM)发展迅速。

尽管商业化的多模态模型,如 GPT-4vGPT-4oGeminiClaude 3.5 Sonnet 目前是最引人注目的表现者,但开源模型,如 LLaVALlama 3-VQwen-VL 在公开基准测试上的表现正稳步追赶。

就在上个月,英伟达发布了他们的开源 多模态大语言模型家族,称为 NVLM。该家族包括三种架构:a) 基于解码器的,b) 基于交叉注意力的,和 c) 混合型。基于解码器的模型将图像和文本令牌传递给预训练的大语言模型,如 LLaVA 模型。基于交叉注意力的模型将图像令牌嵌入作为键和值,而将文本令牌嵌入作为查询;由于注意力是使用不同来源的计算,因此它被称为“交叉注意力”,类似于原始的 Transformer 解码器,而非解码器模型中的自注意力。混合架构是一种独特的设计,结合了解码器和交叉注意力架构,旨在实现多模态推理、减少训练参数并处理高分辨率输入。72B 解码器基础的 NVLM-D 模型在自然图像理解和 OCR 等任务上取得了令人印象深刻的表现,超越了当前最先进的开源和商业模型。

在本文中,我将介绍以下内容:

  • 动态高分辨率(DHR)视觉编码器,所有 NVLM 模型都采用了这一技术

  • 基于解码器的模型,NVLM-D,与 LLaVA 的比较

  • 门控交叉注意力模型,NVLM-X,与 Flamingo 的比较

  • 混合模型,NVLM-H

最终,我将展示 NVLM-D 72B 的性能。与最先进的开源和商业模型相比,NVLM-D 模型在基于文本的任务中表现出稳定性,并且在自然理解和 OCR 任务中具有更优越的表现。

图片来源:pxhere.com/en/photo/821032

基于动态高分辨率的视觉编码器(DHR)

NVLM 模型的一个显著优势是它们在处理 OCR 相关任务方面表现出色,这类任务需要高分辨率的图像输入。NVLM 采用了InternVL 1.5 技术报告中提出的动态高分辨率方法,以保持高分辨率。DHR 方法首先将高分辨率图像转换为预定义的纵横比大小(也称为动态纵横比匹配),然后将其拆分为不重叠的 448*448 图块,并附加一个缩略图,这样可以更好地保留全局信息。

DHR 管道。图片来源:arxiv.org/abs/2409.11402

上图展示了 DHR 管道的详细说明。左侧显示了输入图像,并列出了 6 种不同的预定义纵横比,它们会被搜索并匹配到原始图像形状。然后,将重塑后的图像裁剪成六个不重叠的 448448 的块,并附加一个低分辨率的缩略图,用以捕捉全局信息。接下来,n 个图块(在本例中 n=6+1=7)被分别输入到 ViT 中,并转换为一个长度为 n、每个嵌入维度为 d 的 1024 个 token 的序列(448/14448/14=1024)。为了减少计算成本,采用了pixel reshuffle操作,将 3232 的图块调整为 1616,从而将最终的输出 token 大小减少到 256,且嵌入维度增大为 4*d。

解码器模型:NVLM-D 与 LLaVA

LLaVA是一个著名的基于解码器的多模态大语言模型,它接收图像 X_v 并使用预训练的 CLIP 编码器 ViT-L/14 作为视觉编码器 Z_v,经过一个可训练的线性投影层 W 转换为嵌入 token H_v,之后这些 token 可以与其他文本 token 一起处理。LLaVA 的架构如图所示。

LLaVA 架构。图片来源:arxiv.org/pdf/2304.08485

相比之下,NVLM-D 架构使用 DHR 视觉编码器输入编码后的瓦片序列令牌,并在其中插入瓦片标签,然后与文本令牌拼接,供变换器层处理。架构如下所示。

带有 DHR 视觉编码器的基于解码器的 NVLM-D 架构。图像来源:arxiv.org/pdf/2409.11402

交叉注意力模型:NVLM-X 与 Flamingo

与 LLaVA 相比,Flamingo 模型使用了更复杂的交叉注意力技术,它将视觉嵌入作为键(K)和值(V),而将文本嵌入作为查询(Q)。此外,视觉编码器是基于 CNN 的模型,配有感知重采样器(Perceiver Resampler),它接受一系列带有时间位置嵌入的图像,以使用交叉注意力训练可学习的潜在查询向量。关于感知重采样器的更详细讨论,可以在我的最新文章中找到

Flamingo 架构。图像来源:arxiv.org/pdf/2204.14198

为了融合视觉嵌入和文本嵌入,Flamingo 冻结了预训练的 LLM 层,并在其中进一步添加了一个可训练的门控交叉注意力层,如下所示。门控注意力使用 tanh 门控和一个可学习的 alpha 参数,在交叉注意力层和随后的线性层之后。当 tanh 门控初始化为零时,唯一传递的信息是通过跳跃连接,因此整个模型仍将是原始 LLM,以提高稳定性。

Flamingo 的门控交叉注意力设计。图像来源:arxiv.org/pdf/2204.14198

相比之下,NVLM-X 去除了感知重采样器设计,以便更好地支持 OCR 任务,从而保持更多的空间关系,并且只使用 DHR 编码器输出用于门控交叉注意力。与基于解码器的模型不同,NVLM-X 在将瓦片标签传入门控交叉注意力之前,将它们与文本令牌拼接。整个架构如下所示。

带有门控交叉注意力设计的 NVLM-X 架构。图像来源:arxiv.org/pdf/2409.11402

混合模型:NVLM-H

混合模型是 NVLM 的独特设计。缩略图像令牌被添加到文本令牌中,作为自注意力层的输入,这保留了基于解码器的模型的多模态推理优势。其他图像瓦片和瓦片标签则传入门控交叉注意力层,以捕捉更精细的图像细节,同时最小化整体模型参数。详细架构如下所示。

NVML-H 架构。图片来源:arxiv.org/pdf/2409.11402

性能

那么,NVLM 与其他最先进模型的性能如何呢?本文列出了 NVLM-D 72B 与其他开源模型(如 Llama-3 V)和商业模型(如 GPT-4o)在基准测试中的比较。NVLM-D 在大多数基准测试中表现优于平均水平,特别是在 OCR 和自然图像理解任务中表现突出,这得益于其高分辨率的图像特征和模型本身的多模态推理能力。与参数数量相当的 Llama 3-V 70B 和 InternVL2-Llama3–76B 相比,NVLM-D 在仅文本任务、视觉问答(VQA)任务和图像理解任务中表现出更加一致的优势。详细比较如下所示。

NVLM-D 在公共基准测试中与其他开源和商业模型的性能比较。图片来源:arxiv.org/pdf/2409.11402

另一个有趣的发现是,尽管基于解码器的模型非常强大,但其训练吞吐量(每秒训练样本数)远低于基于跨注意力的模型。论文解释说,基于解码器的模型需要更长的序列长度,这导致了更高的 GPU 消耗和较低的吞吐量。详细的训练比较如下所示:

训练详细比较。图片来源:arxiv.org/pdf/2409.11402

参考文献

  • 戴等人,《NVLM:开放前沿级多模态大语言模型》。arXiv 2024。

  • 陈等人,《我们距离 GPT-4V 有多远?通过开源工具套件缩小与商业多模态模型的差距》。arXiv 2024。

  • 刘等人,《视觉指令微调》。NeurIPS 2023。

  • 白等人,《Qwen-VL:一个多功能的视觉语言模型,用于理解、定位、文本读取及更多》。arXiv 2023。

  • Alayrac 等人,《Flamingo:一个用于少量样本学习的视觉语言模型》。NeurIPS 2022。

周末 AI 项目:为视力障碍人士制作视觉助手

原文:towardsdatascience.com/a-weekend-ai-project-making-a-visual-assistant-for-people-with-vision-impairments-df0b9f0b8c23?source=collection_archive---------6-----------------------#2024-02-17

运行多模态 LLaVA 模型、摄像头和语音合成

Dmitrii EliuseevTowards Data Science Dmitrii Eliuseev

·发表于Towards Data Science ·阅读时间:8 分钟·2024 年 2 月 17 日

--

图片来自 Enoc Valenzuela,Unsplash

现代大型多模态模型(LMMs)不仅能够处理文本,还能处理不同类型的数据。确实,“一图胜千言”,这种功能在与现实世界互动时至关重要。在这个“周末项目”中,我将使用一个免费的LLaVA(大型语言与视觉助手)模型、一台摄像头和一个语音合成器;我们将制作一个 AI 助手,帮助视力障碍人士。与之前的部分一样,所有组件将完全离线运行,不会产生任何云端费用。

事不宜迟,让我们开始吧!

组件

在这个项目中,我将使用几个组件:

  • 一个LLaVA模型,它结合了大型语言模型和视觉编码器,并借助一个特殊的投影矩阵,使得该模型不仅能够理解文本,还能理解图像提示。我将使用LlamaCpp库来运行该模型(尽管它的名字中有“LLaMA”,但它不仅能运行 LLaMA 模型,也能运行 LLaVA 模型)。

  • Streamlit Python 库,允许我们制作交互式 UI。使用摄像头,我们可以拍摄图像,并向 LMM 提出不同的问题…

一个周末 AI 项目:在 PC 和树莓派上使用 YOLO 进行物体检测

原文:towardsdatascience.com/a-weekend-ai-project-object-detection-with-yolo-on-pc-and-raspberry-pi-0653e01032c1?source=collection_archive---------1-----------------------#2024-07-06

在不同硬件上运行最新的 YOLO v10 模型

Dmitrii EliuseevTowards Data Science Dmitrii Eliuseev

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

--

YOLO 物体检测,图像由作者提供

计算机视觉可以成为不同规模机器学习应用的重要组成部分,从价值 2 万美元的特斯拉机器人或自动驾驶汽车,到智能门铃和吸尘器。这也是一项具有挑战性的任务,因为与云基础设施相比,在“真实”的边缘设备上,硬件规格通常受到更大限制。

YOLO(You Only Look Once)是一个流行的物体检测库;它的第一个版本发布于 2015 年。YOLO 特别适用于嵌入式设备,因为它几乎可以在任何地方运行;不仅有 Python 版本,还有 C++(ONNX 和 OpenVINO)和 Rust 版本。去年,我曾在树莓派 4 上测试过 YOLO v8。如今,许多事情发生了变化——新的树莓派 5 发布了,更新的 YOLO v10 也发布了。所以我期待在新硬件上,新的模型能够更快速、更精确地运行。

本文中展示的代码是跨平台的,因此没有树莓派的读者也可以在 Windows、Linux 或 OS X 电脑上运行它。

不再多说,让我们来看看它是如何工作的!

树莓派

对于那些可能从未听说过树莓派的人,让我们简要介绍一下…

一个周末 AI 项目:在 Android 手机上运行 LLaMA 和 Gemma AI 模型

原文:towardsdatascience.com/a-weekend-ai-project-running-llama-and-gemma-ai-models-on-the-android-phone-47a261d257a7?source=collection_archive---------6-----------------------#2024-03-09

使用 C++和 Python 运行 LLaMA 和 Gemma 大语言模型

Dmitrii EliuseevTowards Data Science Dmitrii Eliuseev

·发表于Towards Data Science ·7 分钟阅读·2024 年 3 月 9 日

--

图片来源:Magnet.me,Unsplash

现如今,“移动 AI”是一个快速增长的趋势。智能手机变得越来越强大,大型模型变得更加高效。一些用户可能会选择等待手机制造商添加新功能,但我们能否自己使用最新的 AI 模型呢?确实,我们可以,而且结果非常有趣。在本文中,我将展示如何在 Android 手机上运行 LLaMA 和 Gemma 大型语言模型,我们将看到它是如何工作的。像我所有的测试一样,所有的模型都将在本地运行,不需要使用云 API 或支付。

让我们开始吧!

Termux

我们测试的第一个组件是Termux,这是一个作为 Android 应用程序制作的完整 Linux 终端。它是免费的,并且不需要 root 权限;所有的 Linux 组件都仅在 Termux 文件夹中运行。Termux 可以从Google Play下载,但在撰写本文时,那个版本已经比较旧,并且 Termux 中的“pkg update”命令不再工作了。一个更新版本可以在F-Droid网站上找到;它运行良好,我没有遇到任何问题。

当 Termux 安装在手机上时,我们可以运行它并看到一个标准的 Linux 命令行界面:

一项周末 AI 项目:在树莓派上运行语音识别和 LLaMA-2 GPT

原文:towardsdatascience.com/a-weekend-ai-project-running-speech-recognition-and-a-llama-2-gpt-on-a-raspberry-pi-5298d6edf812?source=collection_archive---------1-----------------------#2024-01-20

Whisper ASR 和 LLaMA-2 GPT 模型的完全离线使用

Dmitrii EliuseevTowards Data Science Dmitrii Eliuseev

·发表于 Towards Data Science ·10 分钟阅读·2024 年 1 月 20 日

--

树莓派运行 LLaMA 模型,图片来源:作者

如今,大家已经不再对在云端运行深度学习模型感到惊讶。但在边缘计算或消费者设备的世界中,情况可能会复杂得多。原因有几个。首先,云 API 的使用要求设备始终在线。对于一个网络服务来说这不是问题,但对于需要离线工作的设备来说,这可能是致命问题。其次,云 API 需要付费,用户可能并不愿意再为一个额外的订阅费用买单。最后,随着时间的推移,几年后,项目可能结束,API 接口会关闭,而昂贵的硬件则变成废物。这显然对用户、生态系统和环境都不友好。因此,我坚信终端用户硬件应该完全支持离线功能,无需额外费用或使用在线 API(当然,这可以是可选的,但不能强制要求)。

在本文中,我将展示如何在树莓派上运行 LLaMA GPT 模型和自动语音识别(ASR)。这将使我们能够向树莓派提问并得到答案。如同承诺的那样,所有这些都将在完全离线的状态下工作。

一个周末的 AI 项目:在树莓派上使用语音识别、PTT 和大规模行动模型

原文:towardsdatascience.com/a-weekend-ai-project-using-speech-recognition-ptt-and-a-large-action-model-on-a-raspberry-pi-ac8d839d078a?source=collection_archive---------1-----------------------#2024-02-03

使用 Python、Push-to-Talk、本地和云端 LLM 制作“类似 Rabbit 的原型”

Dmitrii EliuseevTowards Data Science Dmitrii Eliuseev

·发表于Towards Data Science ·阅读时间 16 分钟·2024 年 2 月 3 日

--

作者提供的图片

在 2024 年初,几乎所有的科技评测者都写了关于 Rabbit R1 的文章——这是一款首个便携式“AI 助手”,售价 199 美元。根据作者的描述,它使用了“神经符号编程”和 LAM(“大规模行动模型”)来执行各种任务。但它是如何工作的呢?嗯,最好的了解方式就是自己动手制作原型!

那些从未听说过 Rabbit R1 的读者,可以在 YouTube 上找到很多类似这样的评测:

本文也受到了Nabil Alouani的文章启发,他对 Rabbit R1 的制作方法做了有趣的分析:

一次充满奇思妙想的等待时间之旅

原文:towardsdatascience.com/a-whimsical-journey-through-wait-times-b02a41d337fc?source=collection_archive---------9-----------------------#2024-05-15

从微波炉倒计时到永无止境的电话等待,使用 Python

Carl M. KadieTowards Data Science Carl M. Kadie

·发表于 Towards Data Science ·阅读时间:16 分钟·2024 年 5 月 15 日

--

等待“保持等待”、等待爆米花、等待彩票中奖——来源:openai.com/dall-e-2/。所有其他图像来自作者。

有没有注意到微波炉的倒计时总是稳定地向零逼近,而电话等待的分钟数却似乎无限延伸?

想象一下:刚刚才开始微波爆米花不到一分钟,你已经开始准备碗盘等待盛放。但如果是电话等待呢?你会想自己是否还会和人类交谈。快进 10 分钟,你正在享受美味的爆米花。但在电话里呢?等待音乐已变成无尽地狱的配乐。

而在等待爆米花和等待电话接通之间的黄昏地带,潜伏着你的每周彩票。你等待着中奖。每周的新票都带来了新的希望,这个希望未曾被以往的失望所触碰。

总结来说,似乎有三种不同类型的等待:

  • “保持等待”-类型——你等待的时间越长,你预期的等待时间就越长。

  • “爆米花”-类型——你等待的时间越长,你预期的等待时间就越短。

  • “彩票中奖”-类型——不管你目前等了多久,你预期的等待时间依然不变。

这些等待时间的差异是真实存在的,还是大脑的错觉?我们将分两部分来回答这个问题。

  • 第一部分 — 数据分析

  • 第二部分 — 数据建模

对于每个部分,我们将依次查看每种类型的等待,交替展示详细的 Python 代码和讨论。如果你对 Python 感兴趣,可以阅读代码部分。如果你只对等待时间的学习感兴趣,可以跳过代码部分。

第一部分:数据分析

“保持等待”-类型的等待——你等待的时间越长,你预期的等待时间就越长。

我们希望从数据开始,但我没有“等待中”时间的数据。那么,如何考虑计算机文件的编辑时间呢?我看到这种编辑时间的一个地方是在 Wikipedia 上。

假设我把你放到一个 Wikipedia 页面上。你能仅通过查看上次编辑的时间来预测下次编辑的时间吗?

旁白 1:不可以自己编辑页面。

旁白 2:类似地,如果我以某种方式让你“等待”了一些分钟(到目前为止),你能预测多久之后通话会重新连接吗?

对于 Wikipedia 页面的编辑,你如何表达对下次编辑时间的预测呢?你可以尝试预测下次编辑的准确时刻,例如:“我预测这个页面将在确切的 5 天 3 小时 20 分钟后被编辑。”然而,这样似乎太具体了,而且几乎总是错误的。

你可以预测一个时间范围:“我预测这个页面将在现在到 100 年内的某个时间点被编辑。”这几乎总是正确的,但很模糊且不有趣。

更实际的预测形式是“中位数下一次编辑时间”。你可能会说:“我预测 50%的概率,这个页面将在接下来的 5 天 3 小时 20 分钟内被编辑。”我,你的对手,会选择“之前”或“之后”。假设我认为实际的中位数下一次编辑时间是 3 天,那么我会选择“之前”。然后我们等待最多 5 天 3 小时 20 分钟。如果在这段时间内(再次,除了我们之外)有人编辑了页面,我得 1 分;否则,你得 1 分。通过这种计分系统,如果你比我更擅长预测,你应该获得更多的分数。

接下来让我们深入 Python,看看我们如何进行这样的预测:

“等待中”类型的等待——Python

考虑一下关于艺术家 Marie Cochran 的 Wikipedia 文章。我们可以查看该文章的修订历史

来自 Wikipedia 的屏幕截图。后续图像来自作者。

为了从各种 Wikipedia 文章中收集这些数据,我写了一个小 Python 脚本,它:

  • 通过[en.wikipedia.org/wiki/Special:Random](https://en.wikipedia.org/wiki/Special:Random)随机选择一个英文 Wikipedia 页面。

  • 转到该页面的修订历史,例如,[en.wikipedia.org/w/index.php?title=Marie_Cochran&action=history](https://en.wikipedia.org/w/index.php?title=Marie_Cochran&action=history)

  • 提取(最多)最后 50 次编辑的日期和时间。时间精确到分钟。

  • 创建由文章标题、编辑时间和脚本运行时间组成的行。所有时间都使用 UTC 时区。制表符分隔列。

  • 将行追加到文件中。

旁白:这种方法提出了几个问题。首先,Special:Random究竟是什么意思“随机”?我不知道。就这个演示而言,它似乎足够随机。为什么是最后 50 次编辑?为什么不是所有编辑?为什么不是最新的一次编辑?除了“最后 50 次”是默认值,并且对这篇文章效果不错,我没有更好的理由。最后,为什么要在常规的维基百科服务器上编写脚本,而我们本可以从[dumps.wikimedia.org](https://dumps.wikimedia.org/)获取所有文章的完整编辑历史?因为我们只需要一个样本。此外,编写这个脚本很容易,但编写一个处理完整数据的程序会很难。遗憾的是,我不会分享这个简单的脚本,因为我不想让不受控制的机器人访问维基百科网站。幸运的是,我将我收集的所有数据共享在GitHub上。你可以根据需要使用它。

这是编辑时间数据的一个片段:

Marie_Cochran 01:20, 8 January 2024 01:16, 08 February 2024
Marie_Cochran 01:10, 27 September 2023 01:16, 08 February 2024
Marie_Cochran 00:59, 12 September 2023 01:16, 08 February 2024
Marie_Cochran 11:43, 2 November 2022 01:16, 08 February 2024
...
Marie_Cochran 19:20, 10 March 2018 01:16, 08 February 2024
Peter_Tennant 15:03, 29 July 2023 01:16, 08 February 2024
Peter_Tennant 21:39, 15 April 2022 01:16, 08 February 2024
...

让我们将其读取到一个 Pandas 数据框中,并计算时间差,即编辑之间的等待时间:

import pandas as pd

# Read the data
wiki_df = pd.read_csv("edit_history.txt", sep='\t', header=None, names=["Title", "Edit DateTime", "Probe DateTime"], usecols=["Title", "Edit DateTime"])
wiki_df['Edit DateTime'] = pd.to_datetime(wiki_df['Edit DateTime']) # text to datetime

# Sort the DataFrame by 'Title' and 'Edit DateTime' to ensure the deltas are calculated correctly
wiki_df.sort_values(by=['Title', 'Edit DateTime'], inplace=True)

# Calculate the time deltas for consecutive edits within the same title
wiki_df['Time Delta'] = wiki_df.groupby('Title')['Edit DateTime'].diff()
wiki_df.head()

生成的 Pandas 数据框从字母表中最先的文章开始(在这些采样中)。该文章介绍了来自蒙古的非常高的Öndör Gongor

在该文章的最后 50 次编辑中,我们首先看到 2008 年 1 月 27 日下午 3:13(UTC)进行了一次编辑。接下来,我们看到 16 分钟后又进行了一次编辑。之后的编辑发生在一分钟内(数据分辨率的限制),因此显示为0 天 00:00:00

继续处理,让我们删除每篇文章开始时出现的NaT(不是时间)行。我们还将按等待时间排序,并重置 Pandas 的索引:

# Remove rows with not-a-time (NaT) values in the 'Time Delta' column
wiki_df.dropna(subset=['Time Delta'], inplace=True)
# Sort by time delta and reset the index
wiki_df.sort_values(by='Time Delta', inplace=True)
wiki_df.reset_index(drop=True, inplace=True)
display(wiki_df)
wiki_df['Time Delta'].describe()

这会生成一个数据框,其开始和结束如下:

带有这个统计摘要:

count                          36320
mean      92 days 13:46:11.116189427
std      195 days 11:36:52.016155110
min                  0 days 00:00:00
25%                  0 days 00:27:00
50%                 15 days 05:41:00
75%                100 days 21:45:45
max               4810 days 17:39:00 

我们看到,采样的等待时间从0 天 00:00:00(即不到一分钟)到超过 13 年不等。(这 13 年的编辑等待时间是在一篇关于弗吉尼亚大学一座建筑的文章中。)四分之一的编辑发生在前一次编辑后的 27 分钟内。编辑之间的中位时间稍微超过 15 天。

在我们继续之前,我想通过一个小函数来改进等待时间的显示:

def seconds_to_text(seconds):
    seconds = round(seconds)
    result = []
    for unit_name, unit_seconds in [('y', 86400 * 365.25),('d', 86400),('h', 3600),('m', 60),('s', 1)]:
        if seconds >= unit_seconds:
            unit_value, seconds = divmod(seconds, unit_seconds)
            result.append(f"{int(unit_value)}{unit_name}")
    return ' '.join(result) if result else "<1s"

seconds_to_text(100)

seconds_to_text函数将 100 秒显示为'1m 40s'

通过这个,我们可以为维基百科数据构建一个“等待等待”表格。给定到下次编辑的等待时间,该表格告诉我们中位数的额外等待时间。(回忆一下,“中位数”意味着一半时间内,我们预计等待时间会少于此时的等待时间,另外一半时间,我们预计等待时间会更长。)

import numpy as np

def wait_wait_table(df, wait_ticks):
    sorted_time_deltas_seconds = df['Time Delta'].dt.total_seconds()
    results = []
    for wait_tick in wait_ticks:
        greater_or_equal_values = sorted_time_deltas_seconds[sorted_time_deltas_seconds >= wait_tick]
        median_wait = np.median(greater_or_equal_values)
        additional_wait = median_wait - wait_tick
        results.append({"Wait So Far": seconds_to_text(wait_tick), "Median Additional Wait": seconds_to_text(additional_wait)})
    return pd.DataFrame(results)

wiki_wait_ticks = [0, 60, 60*5, 60*15, 3600, 3600*4, 86400, 86400 * 7,86400 * 30, 86400 * 100, 86400 * 365.25, 86400 * 365.25 * 5, 86400 * 365.25 * 10]
wiki_wait_tick_labels = [seconds_to_text(wait_tick) for wait_tick in wiki_wait_ticks]
wait_wait_table(wiki_df, wiki_wait_ticks).style.hide(axis="index")

接下来我们将讨论这个表格的输出。

“等待中”-类型等待 — 讨论

上面的 Python 代码生成了这个表格。我们称之为“等待-等待”表格。

表格显示,如果我们完全没有等待过(换句话说,有人刚刚编辑了页面),我们可以预期下一个编辑将在 15 天左右发生。然而,如果在一分钟后,没有人再次编辑文章,我们可以预期等待 19 天。因此,等待一分钟会导致额外增加近 4 天的预期等待时间。如果在一小时后仍没有人编辑文章,我们的预期额外等待时间将增加到 47 天,几乎是原来的两倍。

旁注:当我在这个语境中使用“预测”一词时,我指的是从我们的历史数据中推导出的中位等待时间。换句话说,基于过去的趋势,我们预测下一个编辑会在这个时间框架内发生的一半编辑会更早,而另一半则会更晚。

一种思考这种现象的方式是:当我们开始等待下一个编辑时,我们并不知道我们处于什么类型的页面上。这是一个关于热门流行文化话题的文章,如泰勒·斯威夫特?还是一个关于冷门、变化缓慢话题的文章,如圆顶楼,一个有 5000 名学生的大学的建筑?随着每一分钟的过去,如果没有编辑,概率会从这是一个像泰勒·斯威夫特这样的文章转向类似圆顶楼的文章。

同样地,当我们拨打客户服务电话并被置于等待时——刚开始时,我们并不知道我们在等待什么样的客户服务。然而,随着每一分钟的过去,我们会意识到我们可能在等待的是糟糕且缓慢的客户服务。因此,我们预测的额外等待时间会增加。

到目前为止,我们已经直接使用了数据。我们也可以尝试用概率分布来建模数据。然而,在我们进入建模之前,让我们看看另外两个例子:微波爆米花和等待彩票中奖。

“爆米花”类型的等待——等待时间越长,你的期望等待时间越短。

让我们将等待 Wikipedia 编辑的技巧应用到等待微波爆米花上。与其收集真实数据(尽管那样可能很美味),我更愿意模拟数据。我们将使用一个随机数生成器。我们假设烹饪时间,大概基于传感器,是 5 分钟加减 15 秒。

“爆米花”类型的等待——Python

在 Python 中具体实现:

seed = 0
rng = np.random.default_rng(seed)
sorted_popcorn_time_deltas = np.sort(rng.normal(5*60, 15, 30_000))
popcorn_df = pd.DataFrame(pd.to_timedelta(sorted_popcorn_time_deltas,unit="s"), columns=["Time Delta"])
print(popcorn_df.describe())

这会生成一个包含统计摘要的 Panda 数据框:

 Time Delta
count                      30000
mean   0 days 00:05:00.060355606
std    0 days 00:00:14.956424467
min    0 days 00:03:52.588244397
25%    0 days 00:04:50.011437922
50%    0 days 00:04:59.971380399
75%    0 days 00:05:10.239357827
max    0 days 00:05:59.183245298

正如预期的那样,当从这个正态分布中生成数据时,均值是 5 分钟,标准差大约是 15 秒。我们的模拟等待时间从 3 分 52 秒到 6 分钟不等。

现在我们可以生成一个“等待-等待”表格:

wait_wait_table(popcorn_df, [0, 10, 30, 60, 2*60, 3*60, 4*60, 5*60]).style.hide(axis="index")

“爆米花”类型的等待——讨论

我们的“等待-等待”表格对于爆米花是这样的:

我们的表格显示,刚开始时,我们预计等待 5 分钟。在等待 10 秒钟后,我们的额外等待时间减少了恰好 10 秒钟(到 4 分钟 50 秒)。等待 1 分钟后,我们的额外等待时间降至 4 分钟,依此类推。等待 5 分钟时,预计的额外等待时间继续下降(但不会降至零)。

在稍后的部分,我们将看到如何对这些数据进行建模。目前,接下来我们来看等待彩票中奖。

“彩票中奖”式的等待 —— 不论你等待了多久,你的预计等待时间仍然保持不变。

对于彩票数据,我再次愿意创建模拟数据。华盛顿州乐透的获胜几率是 1 比 27.1。(最常见的中奖,投注 1 美元赢得 3 美元。)我们将玩 1 百万周的彩票(约 19,000 年),并收集我们每次获胜之间的等待数据。

“彩票中奖”式的等待 —— Python

我们模拟了 1 百万周的彩票游戏:

seed = 0
rng = np.random.default_rng(seed)
last_week_won = None
lotto_waits = []
for week in range(1_000_000):
    if rng.uniform(high=27.1) < 1.0:
        if last_week_won is not None:
            lotto_waits.append(week - last_week_won)
        last_week_won = week
sorted_lotto_time_deltas = np.sort(np.array(lotto_waits) * 7 * 24 * 60 * 60)
lotto_df = pd.DataFrame(pd.to_timedelta(sorted_lotto_time_deltas,unit="s"), columns=["Time Delta"])
print(lotto_df.describe())
 Time Delta
count                        36773
mean   190 days 08:21:00.141951976
std    185 days 22:42:41.462765808
min                7 days 00:00:00
25%               56 days 00:00:00
50%              133 days 00:00:00
75%              259 days 00:00:00
max             2429 days 00:00:00

我们两次获胜之间的最短间隔是 7 天。我们模拟的最长干旱期超过 6 年。我们的中位等待时间是 133 天。

我们使用以下方式生成“等待-等待”表格:

lotto_days = [0, 7, 7.00001,  2*7, 4*7, 183, 365.25, 2*365.25, 5*365.25]
lotto_waits = [day * 24 * 60 * 60 for day in lotto_days]
wait_wait_table(lotto_df, lotto_waits).style.hide(axis="index")

“彩票中奖”式的等待 —— 讨论

这是“等待-等待”表格:

表格显示,彩票不会在乎我们为获胜等待了多久。不管我们是刚刚中奖(等待时间 < 1 秒)还是一年没有中奖,我们预期的下一次中奖的额外等待时间几乎总是在 126 天到 133 天之间。

表格中的三个条目可能看起来很奇怪。你认为7d7d 1s发生了什么?为什么额外的等待时间几乎瞬间从 126 天跃升至大约 133 天?答案是,在每周抽奖的时刻,最短的等待时间从 0 天变为 7 天。那么5y呢?这是否表明,如果我们等待 5 年,我们可以在 50 天内预期中奖,这远低于通常的 133 天?可惜,不是。相反,它显示了我们数据的局限性。在数据中,我们只看到过 3 次 5 年的等待:

lotto_df[lotto_df["Time Delta"] > pd.to_timedelta(24*60*60 * 365.25 * 5, unit="s")]

三个数值导致了一个嘈杂的中位数估计。

总结一下我们迄今在真实数据和模拟数据中看到的情况:

  • 维基百科编辑 —— 你等待的时间越长,你期望等待的时间就越长

  • 爆米花 —— 你等待的时间越长,你期望等待的时间越短

  • 彩票中奖 —— 不论你等待了多久,你的预计等待时间仍然保持不变

在下一部分,我们将探讨建模的方式和(更重要的)原因。我们将从我们的乐透数据开始。

第二部分:数据建模

在这一部分,我们将尝试为等待时间预测找出简单的表达式。这种简化对预测来说并非必需。到目前为止,我们创建的叫做经验分布,效果很好。然而,简单的表达式可能更方便。此外,它可能使不同类型的等待之间的比较更容易理解。

我们将依次查看三个示例,从最简单的(彩票中奖)到最复杂的(维基百科编辑)。像之前一样,我将在 Python 代码(你可以跳过)和讨论之间交替进行。

我们将从为我们的三个等待时间数据框添加一个累积分布列开始。回想一下,我们之前已根据Time Delta对数据框进行了排序。

wiki_df['CDF'] = wiki_df['Time Delta'].rank(pct=True)
popcorn_df['CDF'] = popcorn_df['Time Delta'].rank(pct=True)
lotto_df['CDF'] = lotto_df['Time Delta'].rank(pct=True)
wiki_df

标记为CDF的列,即累积分布函数,包含对于最短等待时间接近 0.0 的值,以及对于最长等待时间接近 1.0 的值。换句话说,它是每一行的排名,以分数形式表示。现在,维基百科的数据框看起来是这样的:

现在我们可以绘制CDF(y 轴)与等待时间Time Delta(x 轴)的关系图。以下是用 Python 绘制的代码:

import matplotlib.pyplot as plt

def wait_cdf(title, sorted_df, wait_ticks, dist=None, dist_label=None, left=None, right=None, xscale='linear'):
    wait_seconds = sorted_df['Time Delta'].dt.total_seconds() # x values
    cdf = sorted_df['CDF'] # y values

    left = left or wait_seconds.min()
    right = right or wait_seconds.max()

    plt.figure(figsize=(10, 6))
    plt.title(title + ' Cumulative Distribution Function (CDF)')
    plt.plot(wait_seconds, cdf, marker='.', linestyle=" ", label='Empirical CDF')

    if dist is not None:
        dist_x = np.logspace(np.log10(left), np.log10(right), 100) if xscale == 'log' else np.linspace(left, right, 100)
        dist_y = dist.cdf(dist_x)
        plt.plot(dist_x, dist_y, label = dist_label)

    plt.xlabel('Wait')
    plt.ylabel('CDF')
    plt.xscale(xscale)
    plt.xticks(wait_ticks, [seconds_to_text(wait_tick) for wait_tick in wait_ticks], rotation=45)
    plt.xlim(left=left, right=right)
    plt.grid(True, which="both", ls="--")
    plt.legend(loc='upper left')
    plt.show()

wait_cdf("Lottery Wins", lotto_df, wiki_wait_ticks, xscale='log')

这是彩票中奖的 CDF 图,等待时间以对数刻度显示:

这条曲线看起来很简单,所以让我们尝试拟合一条简单的曲线。显然的候选曲线是指数分布。它是与等待时间相关的最简单的常见函数。

Python 的scipy.stats包使得将指数曲线拟合到我们的数据并将结果曲线表示为 Python 对象变得非常容易,这里命名为lotto_expon_dist

from scipy.stats import expon

_, lotto_e_scale = expon.fit(lotto_df['Time Delta'].dt.total_seconds(), floc=0)
lotto_expon_dist = expon(scale=lotto_e_scale)
print(f"Lottery wins exponential median is {seconds_to_text(lotto_expon_dist.median())}. The scale parameter is {seconds_to_text(lotto_e_scale)}.")

这段代码输出:

彩票中奖指数分布的中位数是 131 天 22 小时 32 分钟 20 秒。scale 参数为 190 天 8 小时 21 分钟。

拟合曲线的中位数大约是 132 天,接近实际的中位数 133 天。根据惯例,我们用一个单一的数字来参数化指数曲线,这里称为scale。它对应于分布的均值,但我们可以很容易地通过均值和中位数相互转换。

这是彩票中奖的经验 CDF 与拟合 CDF 图:

lotto_expon_label = f'ExponentialDistribution(scale={seconds_to_text(lotto_e_scale)})'
wait_cdf("Lottery Wins", lotto_df, wiki_wait_ticks, dist=lotto_expon_dist, dist_label=lotto_expon_label, xscale='log')

它们非常匹配。左侧的轻微不匹配是由于在抽奖时刻瞬间发生的 7 天跳跃所致。我们将在本文中忽略这一微小的不匹配。

指数分布在我们的(模拟的)彩票中奖数据上表现良好。让我们看看它在我们的爆米花和维基百科数据上如何表现。这里是将指数分布拟合到这些数据框的代码。

_, popcorn_e_scale = expon.fit(popcorn_df['Time Delta'].dt.total_seconds(), floc=0)
popcorn_expon_dist = expon(scale=popcorn_e_scale)
print(f"Popcorn exponential median is {seconds_to_text(popcorn_expon_dist.median())}")
popcorn_expon_label = f'ExponentialDistribution(scale={seconds_to_text(popcorn_e_scale)})'
wait_cdf("Popcorn", popcorn_df, popcorn_ticks, dist=popcorn_expon_dist, dist_label=popcorn_expon_label, left=10, right=6*60, xscale='linear' )

_, wiki_e_scale = expon.fit(wiki_df['Time Delta'].dt.total_seconds(), floc=0)
wiki_expon_dist = expon(scale=wiki_e_scale)
print(f"Wiki exponential median is {seconds_to_text(wiki_expon_dist.median())}")
wiki_expon_label = f'ExponentialDistribution(scale={seconds_to_text(wiki_e_scale)})'
wait_cdf("Wiki Edits", wiki_df, wiki_wait_ticks, dist=wiki_expon_dist, dist_label=wiki_expon_label, xscale='log', left=60)

以下是图表:

哎呀,这些曲线拟合得很糟糕!问题在于指数分布适用于类似“彩票中奖”类型的数据。具体来说,是适用于等待时间中,不管你已经等待了多久,你的期望等待时间始终不变的情况。由于指数分布拟合的是忽略当前等待时间的数据,因此它被称为无记忆。而且,在连续分布中,指数分布是唯一无记忆的分布。

但是如果我们需要让我们的分布具有记忆性呢?下一个最简单的分布是威布尔分布。

两个参数,shapescale,用来参数化威布尔分布。让我们从彩票数据开始尝试:

from scipy.stats import weibull_min

lotto_shape, _, lotto_w_scale = weibull_min.fit(lotto_df['Time Delta'].dt.total_seconds(), floc=0)
lotto_weibull_dist = weibull_min(c=lotto_shape,scale=lotto_w_scale)

print(f"Lottery Wins Weibull median is {seconds_to_text(lotto_weibull_dist.median())}")
lotto_weibull_label = f'WeibullDistribution(shape={lotto_shape:.3},scale={seconds_to_text(lotto_w_scale)})'
wait_cdf("Lottery Wins", lotto_df, wiki_wait_ticks, dist=lotto_weibull_dist, dist_label=lotto_weibull_label, xscale='log')

这产生了一个拟合曲线,看起来像指数分布。事实上,当shape为 1 时,威布尔分布就是一个指数分布。这里形状参数为 1.06。

当我们尝试将威布尔分布拟合到我们的爆米花数据时,会发生什么呢?

popcorn_shape, _, popcorn_w_scale = weibull_min.fit(popcorn_df['Time Delta'].dt.total_seconds(), floc=0)
popcorn_weibull_dist = weibull_min(c=popcorn_shape, scale=popcorn_w_scale)
print(f"Popcorn Weibull median is {seconds_to_text(popcorn_weibull_dist.median())}")
popcorn_df_weibull_label = f'Weibull(shape={popcorn_shape:.3}, scale={seconds_to_text(popcorn_w_scale)})'
wait_cdf("Popcorn", popcorn_df, popcorn_ticks, dist=popcorn_weibull_dist, dist_label=popcorn_df_weibull_label, left=3*60, right=7*60, xscale='linear')

虽然不是完美的,但这个拟合比指数分布的拟合要好得多。注意形状参数的值为 20。当威布尔分布的形状参数大于 1 时,它表明:“你等待的时间越长,你预期的等待时间越短。”

最后,让我们尝试在维基百科数据上应用威布尔分布。

wiki_shape, _, wiki_w_scale = weibull_min.fit(wiki_df['Time Delta'].dt.total_seconds(), floc=0)
wiki_weibull_dist = weibull_min(c=wiki_shape, scale=wiki_w_scale)
print(f"Wiki Weibull median is {seconds_to_text(wiki_weibull_dist.median())}")
wiki_df_weibull_label = f'Weibull(shape={wiki_shape:.3},scale={seconds_to_text(wiki_w_scale)})'
wait_cdf("Wiki Edits", wiki_df, wiki_wait_ticks, dist=wiki_weibull_dist, dist_label=wiki_df_weibull_label, xscale='log', left=60)

这个曲线拟合不完美,但仍然比指数分布的拟合要好得多。注意形状参数的值是 0.292。当威布尔分布的形状参数小于 1 时,这意味着“你等待的时间越长,你预期的等待时间也越长”。然而,威布尔分布在这方面并不独特。无数其他分布也具备这个特性。事实上,经验性的维基百科分布具有这个特性,但它并不是一个威布尔分布。

附注:我不知道有比维基百科数据更好的简单模型。经验曲线看起来比威布尔分布稍微复杂一些。也许我们只需要确定(或发明)一个稍微更一般的分布,带有一两个额外的参数。

结论

总之,你和我并不(一定)疯狂。

我们已经看到,确实有一些情况,当你等待的时间越长,你应该预期等待的时间也越长。我们在维基百科编辑之间的时间中经验性地看到了这一点。当威布尔分布的形状参数小于 1 时,我们也可以看到这一点。

同样,对于其他一些等待,“你等待的时间越长,你预期的等待时间越短”。我们在爆米花中看到了这一点。当威布尔分布的形状参数大于 1 时,我们也看到了这一点。

最后,存在第三类等待:无记忆等待。对于这些,无论你等待了多长时间,你的预期等待时间始终保持不变。我们在彩票中奖之间的时间间隔中看到了这一点。这也对应于一个形状参数为 1 的威布尔分布(这与指数分布相同)。

当你有等待数据需要分析时,我建议尝试使用威布尔分布。Python 使得拟合这样的曲线变得简单。然而,如果你的数据不适合威布尔分布,千万不要使用威布尔分布。相反,让你的数据自行发声,直接使用你的经验分布。

感谢你和我一起探索等待时间的旅程。希望你现在能更好地理解等待时间及其分析。

在 Medium 上关注 Carl。我写关于 Rust 和 Python 中的科学编程、机器学习和统计学的文章。我通常每个月写一篇文章。

通往参数效率的蜿蜒之路

原文:towardsdatascience.com/a-winding-road-to-parameter-efficiency-12448e64524d?source=collection_archive---------5-----------------------#2024-01-04

深入探讨使用 LoRA 进行参数高效微调(PEFT)的设计决策

Mariano KampTowards Data Science Mariano Kamp

·发表于Towards Data Science ·阅读时间 31 分钟·2024 年 1 月 4 日

--

好消息:使用 LoRA 进行参数高效微调(PEFT)可以非常简单。只需采用一种简单的策略,调整所有线性模块并稍微调整学习率,就能获得良好的性能。你可以在这里停下阅读!

但是,如果你想要更多呢?如果你正在寻求更深入的理解,了解哪些模块需要调整,以及如何优化你的模型以提升性能、GPU 内存利用率或训练速度呢?如果你希望对这些方面有更细致的理解和控制,那么你来对地方了。

加入我,一起探索我们通往参数效率的蜿蜒之路。我们将深入探讨那些能够帮助你最大化利用 LoRA 的设计决策,同时让你更好地掌控和理解模型的性能。让我们一起开始这场激动人心的探索之旅。

如果你已经至少对 LoRA 有基本的了解,比如我们在上一篇文章中讨论的内容,那么你会从这篇文章中获得最大的收益。此外,我们正在优化一个 RoBERTa 模型[1],该模型使用transformer 架构。对基本组件的了解有助于理解,但并非跟随本文的主要内容所必需。

(由Clipdrop生成)

在上一篇文章中,我们探讨了如何应用 LoRA 训练适配器,这些适配器只需较少的参数即可完成全量微调。我们还展示了这种实现可能的代码样子。然而,我们的重点主要放在了机械层面。我们没有讨论应该适配哪些模块,也没有讨论如何为效率性能调整适配器的大小。

今天,这就是我们的重点。

我们放眼全局,认识到我们需要做出许多算法设计决策,这些决策中许多是相互影响的。这些决策通常由原始算法的创造者作为超参数来表达。为了处理超参数及其值的众多可能组合,我们将采用系统化的方法来学习这些设计决策的相对影响。我们的目标不仅是最终为当前模型实现良好的性能,还希望通过实验收集实证反馈,增强我们对模型及其设计的直观理解。这不仅对今天的模型、任务和数据集有益,而且我们学到的许多知识是可以转移的。它将使我们在未来处理模型变种、新任务和数据集时更加自信。

实验执行:

我将使用 Amazon SageMaker 自动模型调优(AMT)来运行本文中的所有实验。在 AMT 中,我将故意探索并分析搜索空间,或者自动找到一个好的超参数值组合

作为附带说明,‘调优’在本文中有两个含义。一方面,我们使用‘超参数调优’来指代在模型训练中调整超参数值的过程,这是由 SageMaker 的自动模型调优实现的。另一方面,我们使用‘调优’来描述从一个预训练模型开始,然后为特定下游任务微调参数(而不是超参数)的过程。

为了保持专注,我将在本文中简要介绍实现细节。然而,你可以在链接的笔记本中找到所有实验及其详细信息。

我还鼓励你深入了解 AMT 的背景,了解搜索策略随机搜索与贝叶斯优化之间的差异,了解热启动调优任务的概念,以及可视化/分析结果的相关内容。所有这些内容都在这篇文章中进行了讨论:

[## 探索使用 Amazon SageMaker 自动模型调优进行超参数优化的高级技术…

创建高性能机器学习(ML)解决方案依赖于探索和优化训练参数,此外……

aws.amazon.com](https://aws.amazon.com/blogs/machine-learning/explore-advanced-techniques-for-hyperparameter-optimization-with-amazon-sagemaker-automatic-model-tuning/?source=post_page-----12448e64524d--------------------------------)

基准:我们应该与什么进行比较?

我们将专注于架构决策:

  • 我们应该适应哪些模块?

  • 在什么层?所有层?某些层?只是中间层?

  • 模块适配器应该有多大?r,LoRA 矩阵的秩,应该是多少?

然而,在我们开始实验之前,如何确保我们走在正确的道路上,并且我们的改变有积极的影响?让我们定义一些基准来与我们的进展进行比较。

如果寻找比较基准对你没有吸引力,可以直接跳到下一节 “要调整什么?”。

随着时间的推移,我们希望看到我们的训练过程产生更好的结果。但是,我们什么时候可以结束实验并停止尝试?

经过一段时间没有看到进一步的改进,可能表明我们已经达到了最佳状态。然而,这也可能意味着我们已经用尽了可以尝试的方法,尽管实际上还有更多的可能性。

性能预期和可重复性 为了正确解读我们实验的结果,我们需要为我们的模型建立明确的性能预期。这包括了解理想性能的上限,以及我们期望看到的最低性能。

深度学习本质上是嘈杂的,这意味着没有两次运行会产生完全相同的结果。这引发了关于我们观察到的结果的重要问题。我们看到的性能是否可以使用我们测试过的超参数值重复得到,还是仅仅这一次我们碰巧运气好?为了回答这些问题,我们需要验证一组我们发现表现良好的超参数值。在本文中,我将通过运行相同的超参数值五次来计算平均性能及其方差。

预期性能 — 完整微调: 在我们的案例中,推理预期性能是简单的。我们正在对 sst-2 数据集 进行情感分析任务的微调,使用的是 RoBERTa 基础模型,正如 RoBERTa 论文 [1] 中所做的那样。

因此,我们可以直接使用作者报告的数字作为 合理性检查。我们将使我们的设置和使用的超参数与论文中的一致。

我们仍然亲自进行训练,以确保在应用 LoRA 之前验证我们的设置和训练流程。因此,我们可以进行一个合理性检查,确保我们观察到的结果大致与论文中的结果相符。如果我们无法匹配这些数字,我们需要检查我们的设置。

RoBERTa 论文[1]在表 8 中报告了 94.8 的准确率。这为我们在完整微调过程中的预期性能提供了基准。在确认我们的结果接近该数字后,我们将使用我们自己的设置和结果作为基准,来对比所有后续实验,这些实验均基于我们的设置。

预期性能——LoRA 微调: 这一点也很容易。LoRA 的承诺是几乎可以匹配完整微调的性能,但只需使用完整微调参数的一个小部分。

因此,我们将与前一部分中描述的完整微调性能结果进行对比。

预期最低性能: 一种可能的基准是随机性能。对于我们有两个类别的任务,随机性能为 0.5。但是我们并不是从头开始构建模型,并且从已有的论文中我们知道 LoRA 方法效果非常好,因此随机性能并不是一个有意义的基准。

相反,我们可以使用一个基准,只训练分类器,并保持嵌入层和 Transformer 层被冻结,保持它们来自预训练时的状态。这应该会导致比完整微调低得多的性能,但比随机性能要好得多。重要的是,它也应该作为一个比较点,用于推理诸如参数效率、内存使用和训练吞吐量等非功能性方面。

比较基准。 “模型性能”面板中的黑色条表示标准差。

上述所有场景已经运行了五次,图表中显示了平均性能。你也可以推断出,我们的“完整微调”场景的表现与 RoBERTa 论文中的性能接近。正如我们所希望的那样,“LoRA 基础”(调整所有线性模块)达到了相同的性能,但使用了更少的参数。场景“仅分类器”表现较差,符合预期,但在参数量上更便宜,训练速度也更快。

接下来,我们将以这些数字为基准,来对比未来的实验结果。

你可以在附带的笔记本中找到更多细节。

实验执行:

首先,对于每个基准,我们会搜索一个最优的 *学习率* 参数值。我们使用贝叶斯优化来高效地探索并利用搜索空间。

其次,我们为某个场景找到的最佳超参数值可能不一定会重现出良好的结果。可能我们确定的超参数值只是相对于我们探索的其他值而言是最好的。也许我们找到的值根本不相关,例如模型在这个值范围内并不敏感?为了评估我们的发现是否可靠,对于每个场景,我们会重新运行最佳超参数组合五次,并报告在目标指标上观察到的标准差。

LoRA 基础场景——第一次结果: 很高兴看到,LoRA 微调方法中的“LoRA 基础”场景,尽管只使用了约 1%的参数,但其表现已与“完全微调”相当。此外,在这种方法中,我们正在适配所有线性模块,并使用相同的适配器大小(r=8)。这是一个简单的起点,尽管简单,但显然能产生良好的表现。

次要超参数: 需要注意的是,我们主要搜索超参数r的良好值和我们希望适配的模块。为了简化,我们只调优非常少的额外超参数。对于基线来说,仅调优learning rateepochs的数量。我们使用贝叶斯优化作为搜索策略,并利用 Amazon SageMaker 自动模型调优(AMT)。

我们遵循参考文献中的指导来设置其他超参数,如weight decaydropout。在本文中,我们将这些超参数保持固定,以便隔离定义 LoRA 架构的超参数的影响,从而更容易看出我们的主要超参数如何影响性能。

亲爱的读者,您打算重复本文中的步骤吗?您是否旨在为自己的模型、任务和数据集找到最佳的超参数,并打算在生产中使用它们?如果是这样,那么将次要超参数也包含在内是有意义的。理想情况下,您应该在探索和调优工作的后期进行此操作——即当您已经显著缩小了搜索范围——然后努力进一步提高性能,哪怕只是略微提高。

超参数:需要调优什么?

让我们开始进行我们的主要活动。

在模型架构中的设计决策通常以超参数的形式表达。具体到 LoRA,我们可以定义哪些模块进行适配,以及每个模块的适配器r应该多大。

在上一篇文章中,我们仅建议根据我们对任务和架构的理解来选择这些模块。

现在,我们将深入探讨。我们应该在何处应用微调?

微调在哪里进行?分类器在顶部,变换器层次在中间,嵌入层在底部。左侧:可能的适配模块,右侧:示例选择。

在上面的插图中,左侧展示了我们可以微调的所有潜在模块——包括分类器和嵌入层。右侧是我为插图所做的一个示例选择。那么,如何做出实际选择呢?

让我们从一个高层次来看一下我们的选项:

  • 分类器 很明显,我们绝对需要训练分类器。这是因为它在预训练过程中没有被训练,因此,对于我们的微调,它是随机初始化的。此外,它处于模型的核心位置,极大地影响着模型的性能,因为所有信息都必须经过它。它还对损失计算有最直接的影响,因为损失从分类器开始计算。最后,它的参数较少,因此训练起来也更高效。

    总结一下,我们始终微调分类器,但不对其进行调整(使用 LoRA)。

  • 嵌入层 嵌入层位于底部——接近输入——并携带词元的语义信息。这对我们的下游任务非常重要。然而,它并非“空的”。即使没有微调,我们也会得到所有预训练时学到的内容。目前,我们正在考虑直接微调嵌入层是否能为我们带来额外的能力,以及我们的下游任务是否能从对词元意义的更细致理解中受益?

    让我们反思一下。如果是这样的话,是否这些额外的知识也可以在嵌入层上方的某一层学到,甚至更高效?

    最后,嵌入层通常有很多参数,因此我们需要在微调之前对它们进行调整。

    综合考虑这两个方面,我们决定放弃这个选项,不让嵌入层可训练(因此也不对其应用 LoRA)。

  • **变换器层

    微调所有变换器层中的所有参数是低效的。因此,我们需要至少通过 LoRA 调整它们,以提高参数效率。这使我们开始考虑,是否应该训练所有层及每一层中的所有组件?或者我们应该训练某些层、某些组件,或者是这些层和组件的特定组合?

    这里没有通用的答案。我们将根据这些层和它们的模块进行调整,并在本文中进一步探索细节。

在上面的插图中,右侧展示了一个典型的微调模块的选择。这只是其中一种组合,当然还有许多其他组合是可能的。请注意,插图中只显示了五层,而你的模型可能有更多层。例如,在我们示例中使用的 RoBERTa 基础模型就有12层,这在今天的标准下算是比较小的。每一层也包含6个组件:

  • 注意:查询、键、值、输出

  • 前馈:上、下

即使我们不考虑我们也希望调整r并且——目前——仅专注于是否包括哪些模块的二元决策,这将使我们每一层有 64(2**6)种组合。由于这仅看每一层的组合,而我们有 12 层可以组合,最终会得到超过十六亿种组合:

In [1]: (2**6)**12.
Out[1]: 4.722366482869645e+21

很容易看出,我们无法穷尽所有的组合,更不用说手动探索这个空间了。

通常在计算机科学中,当我们想要探索一个过于庞大的空间而无法完全研究时,我们会借助掷骰子。但在这种情况下,我们可以从那个空间中进行抽样,但我们如何解读结果呢?我们会得到一组任意的层和组件的组合(至少是 12*6=72,参照上面的简单示例)。我们如何从这些细节中推广出符合我们自然理解的更高层次规则,以便找到与问题空间一致的规律呢?我们需要将这些细节与我们在更抽象层面的概念理解对齐。

因此,我们需要考虑模块组,并寻找可以在实验中使用的结构或模式,而不是操作一组单独的组件或层。我们需要培养一种直觉,了解事物应该如何运作,然后制定并测试假设。

问题:单独在定义好的参数组上进行实验有帮助吗?答案是肯定的。即使我们后来可能需要将一些参数组结合起来以获得最佳结果,这些独立的参数组仍然能指引我们前进。单独测试能让我们更清楚地看到影响的模式。

然而,存在一个风险。当这些模式组合使用时,它们的影响可能会发生变化。虽然这并不完美,但我们不要对此过于悲观 😃 我们需要从某个地方开始,然后在需要时优化我们的方法。

准备好了吗?让我们尝试一下。

垂直调整 / 层次调整

我怀疑越靠近分类头的上层会比下层更具影响力。以下是我的思考:我们的任务是情感分析。这样想是合理的,对吧?大多数具体的决策必须要么在分类头中做出,要么接近分类头做出?比如识别某些短语(“我需要它就像我头上有个洞”)或构造的句子(“登记体验抵消了原本极好的服务”)。这表明,微调定义不同标记如何在上下文中组合成情感的网络参数至关重要,而不是在预训练期间改变单词的含义(在词向量中)。

即使情况并非总是如此,适应上层仍然提供了一个机会,可以覆盖或细化来自下层和词向量的决策。另一方面,这表明微调下层的重要性较小。

那个听起来像是一个值得尝试的坚实假设(哦,未来的 Mariano 留言:不要在这里停止阅读)。

顺便说一下,我们并没有反思嵌入层或任何变压器层的普遍必要性。这个决定已经做出:它们都已包含在预训练中,并将成为我们微调模型的一部分。我们此时考虑的是如何帮助模型最好地学习我们的下游任务,即情感分析。我们提出的问题是:我们应该微调哪些权重,以便产生影响并实现参数效率?

让我们来验证这个假设。

左侧:微调上半部分层。右侧:下半部分。右侧:均匀分布。

为了清晰地看到我们的假设效果,我们该与什么进行对比?让我们设计一些能夸大效果的实验:

  • 在我们的第一个实验中,我们微调并调整了模型上半部分的所有组件,即我们示例中的第 7 至 12 层。这是我们的假设。

  • 相比之下,我们运行了另一个实验,只微调模型的下半部分层。具体来说,我们训练了第 1 至第 6 层的所有组件。这与我们的假设相反。

  • 让我们再考虑一个对比假设:对所有层进行轻微调整比仅调整顶层更有益。因此,我们还设计了第三个场景,其中我们微调了一半的层,但将它们均匀分布。

  • 我们还包括了一个实验,在该实验中我们微调所有层(上图中未展示)。这不是一个公平的性能比较,因为我们训练了比前三个实验多两倍的参数。然而,正因如此,它凸显了我们在之前那些仅微调一半参数的实验中可能损失的性能。

总结一下,我们有 3+1 种场景要运行作为实验。以下是结果:

所有 3+1 场景概览。所有场景都运行了 7 次。有些试验得出了完全相同的结果,因此在图表的左侧无法区分,但在右侧的密度图中包含了这些结果。

下半部分(橙色,0.937)和**上半部分**(红色,0.941)大致相同(查看密度图右侧的峰值以查看均值)。均匀(蓝色,0.945)比**下半部分**/**上半部分**分别提高了0.04/~0.08。

使用所有层(青色,~0.949)在平均表现上最好。然而,这只是一个比较点,代价是其他场景的两倍。

实验执行:

我们首先使用已经调优过的*学习率*训练周期数。然后,我们针对场景设置(如下层上层均匀全部)的不同值运行试验(训练运行)。在 AMT 中,我们将这些实验作为 网格搜索(Grid Search)进行。

问题:网格搜索(Grid Search)以简单著称,但在寻找最佳解时效率较低。那么我们为什么还要使用它?

让我们稍微退后一步。如果我们使用贝叶斯搜索(Bayesian Search)进行几次试验,我们很快就会发现哪些超参数值表现良好。这会使得后续的试验偏向这些值,即主要集中在已知的好值附近。虽然越来越多地利用我们从搜索空间中学到的内容是找到最佳值的好策略,但这种偏向性使得理解已探索空间变得困难,因为我们在早期表现较差的区域采样较少。

使用网格搜索(Grid Search),我们可以精确地定义需要探索的参数值,从而使得结果更容易解读。

事实上,如果你查看提供的代码,你会看到 AMT 会拒绝重复采样相同的值。但我们希望这样做,因此我们引入了一个虚拟变量,值从 0 到我们想要进行的试验次数。这是有帮助的,它允许我们重复试验相同的超参数值,从而估计该组合的标准差。

虽然我们在上面已经为已经调优好的基准场景进行了 5 次试验,以查看我们能否重现所选超参数组合的结果,但在这里我们对每个组合进行了 7 次试验,以更精确地理解该组合的方差,进而看到微小的差异。

相同的原则也适用于本文接下来的两个场景,之后将不再提及。

让我们先处理掉简单的部分:正如预期的那样,调节所有层,并因此使用双倍的参数量,能最显著地提高性能。这一提升在下方的图表中十分明显。

此外,所有场景的峰值,如右侧各个图表中的密度图所示,比较接近。当我们比较这些峰值时,它们代表了最常观察到的性能,我们只看到最差和最好的场景在验证准确度上大约有 0.08 的提升。这个提升并不大。因此,我们认为这可以忽略不计。

不管怎样,我们还是来检视一下我们最初的假设:我们(实际上是我)原本预计,微调上面的六层会比微调下面的六层带来更好的性能。然而,数据并不支持这一点。对于这个任务来说,微调哪一层并没有区别。因此,我需要更新我的理解。

我们有两个潜在的结论:

  • 将层分布均匀要比集中在顶部或底部的层稍好一些。尽管如此,提升非常小,这一发现可能较为脆弱,并且可能不具有广泛的普适性,甚至对于同一模型的不同运行也不一定适用。因此,我们将放弃这一“发现”。

  • 调整所有层,成本翻倍,带来略微更好的结果。然而,这一结果并不令人感到意外。尽管如此,确认这一点仍然很有意义,否则我们本可以找到节省可训练参数(即成本)的机会。

总体来说,了解这些信息是好的,但由于我们认为这些信息不可操作,我们将继续前进。如果你感兴趣,可以在这个笔记本中找到更多细节。

横向调优 / 按组件调优

在每个变换器层中,我们有四个用于注意力的学习投影,这些投影在微调时可以进行调整:

  • Q — 查询,768 -> 768

  • K — 键,768 -> 768

  • V — 值,768 -> 768

  • O — 输出,768 -> 768

除此之外,我们在每个位置感知的前馈层中使用了两个线性模块,这些模块与上面的投影位于同一变换器层中:

  • Up — 向上投影,768 -> 3072

  • Down — 向下投影,3072 -> 768

从上面的数字中我们可以看到,前馈层(ff)我们之前讨论的 QKVO 投影的四倍大小。因此,ff 组件可能会产生更大的影响,并且成本肯定更高。

除此之外,我们还能有什么其他的期望呢?很难说。我们从多查询注意力[3]中知道,查询投影尤为重要,但这种重要性是否适用于在我们的任务上使用适配器的微调(与例如预训练相比)呢?不如让我们尝试一下各个组件的影响,并根据这些结果继续前进。我们将能够看到哪些组件最强,或许这能帮助我们仅选择这些进行未来的调优。

让我们运行这些实验并检查结果:

稍微明显一些。但我们也在混合使用 1x 参数(att_)和 4x 参数(ff_)。让我们深入分析。

在注意力投影(1x)中,q(红色,约 0.933)和k(蓝色,约 0.931)的效果不如预期,o(橙色,约 0.939)和v(青色,约 0.937)看起来稍微好一些。然而,最差和最好的之间差距约为 0.08。

再次,更多的参数带来了更好的性能:前馈向上向下投影的性能均在约 0.943 左右。

正如预期的那样,ff 层利用其四倍的尺寸优势,超越了注意力投影。然而,我们仍然可以看到这两组之间的差异。这些差异相对较小,如果你想利用它们,需要验证它们在特定任务中的适用性。

一个重要的观察是,仅通过调整其中一个前馈层(约0.943),我们几乎就能达到调整所有模块后的表现这一表现来自于“LoRA Base”场景(约0.946。因此,如果我们希望在整体性能和参数数量之间取得平衡,这可能是一个不错的策略。我们会将这一点考虑进最终的比较中。

在注意力投影(中间图)中,事实证明查询投影的影响并不像预期的那样显著。相反,输出和数值投影则更有用。然而,它们单独使用时,并不那么令人印象深刻。

到目前为止,我们已经看过各个组件的单独贡献。接下来,我们也要检查它们的影响是否重叠,或者组合组件是否能改善结果。

每一层中,查询和输出投影的典范组合,以及上投影。

让我们运行一些可能的组合,看看是否能获得有用的信息。以下是结果:

几个选定的注意力投影和前馈上投影的组合概览。让我们仔细看看最强的候选组合。

在约0.948的性能下,这种组合略微超越了“LoRA Base”场景的表现,但以较低的成本(参数数量)。

从上面的数字图表来看,第一个结论是我们没有出现性能退化。考虑到我们增加了更多的参数并组合了现有的组合,这应该是预期的结果。然而,组合设计决策时,组合后的表现有时可能会低于单独的表现。不过这里没有出现这种情况,真好!

我们不应该过度解读这些结果,但值得注意的是,当我们单独测试假设时,输出投影的性能略微优于数值投影的性能。而现在,在与逐位置的前馈上投影结合时,这种关系发生了逆转(现在:o+up ~0.945,v+up ~0.948)。

我们还将在之前的实验中看到,上投影已经几乎单独达到了该水平。因此,我们会保持一定的热情,但将这个场景包括在最终的比较中。即便如此,因为我们得到了一个略微优于调整和适配所有组件的“LoRA Base”场景的性能(约0.946),但参数更少。

你可以在这个笔记本中找到更多细节。

调整r

我们从文献[2]中得知,建议使用较小的r值,这意味着r只是原始模块最小维度的一部分,例如使用8而不是768。不过,让我们自己验证一下这一点,并获取一些经验反馈。尽管有传统的观点,是否值得调查使用更大的r值呢?

在之前的实验中,我们使用了r=8并投入更多时间来调整learning-rate和训练的epochs数量。现在尝试不同的r值将显著改变线性模块的容量。理想情况下,我们会为每个r值重新调整learning-rate,但我们的目标是节约资源。因此,暂时我们坚持使用相同的learning-rate。然而,随着我们偏离已经调整好的r=8值,重新调整上述超参数的需求也会变得更强。

在审视结果时我们需要记住的一个考虑因素:

我们已经可以看到,如果我们大幅改变模型的容量,可能还需要调整学习率。同时,好的值之间非常接近(请查看右侧的峰值)。它们大约在~0.945,r=16(绿色)略高,约为 0.947。

旁注:我们可以看到,当 r=32 时(在所有面板上都有突出显示),我们已经远离了调整好的超参数值。右上角:模型要大得多。左下角:训练损失下降,额外的容量导致最佳的训练损失。右下角:但验证损失却上升了。

在第一张图中,我们看到模型的性能对额外容量并不是特别敏感,在r=4r=8时表现良好。r=16略好一点,但在参数数量上也更为昂贵。所以,让我们在最终的比较中保留r=4r=8

为了观察r对参数数量的影响,我们还将在最终的比较中包含r=1

在上面的图中观察到一个奇怪的现象,那就是性能在r=32时急剧下降。提供一个使用残差连接的模型时,增加容量应该会带来与较小容量相同或更好的性能。但在这里显然并非如此。但是,由于我们为r=8调整了学习率,并且现在在r=32时有更多可学习的参数(请参见前述图中的右上角面板),我们也应该减少learning-rate,或者最好重新调整learning-rateepochs的数量,以适应更大的容量。查看前述图中的右下角面板后,我们还应该考虑增加更多的正则化,以应对我们所看到的更明显的过拟合。

尽管在为模型提供更大容量时通常有潜力提高性能,但我们观察到的其他r值并未表明仅仅增加容量就能提高性能,而不显著增加参数数量。因此,我们将跳过追求更大的r

更多细节请见此笔记本

最终比较

在这篇长文中,我们收集了大量的分析结果。为了整合这些发现,让我们在一个地方探讨并比较几种有趣的超参数组合。对于我们的目的,若某个结果能够提高模型的整体性能,或为我们提供关于模型工作原理的额外见解,从而最终增强我们的直观理解,那么该结果就被认为是有趣的。

所有实验都在 RoBERTa 基础模型上进行 sst2 任务的微调,如 RoBERTa 论文[1]中所示。

我们三个基准场景(列表顶部)和五个实验的表格概览。

上述表格结果的图形表示。“模型性能”面板中的黑条表示标准差。

实验执行: 如前所述,当我展示一个场景的结果时(在上表的“target_tuner_name”列中报告,并作为图表中的 y 轴标签),这是基于执行相同的超参数组合五次的结果。这使我能够报告目标指标的均值和标准差。

现在,让我们讨论一下上图所示场景中的一些观察结果。

仅分类器

这个基准——我们只训练分类器头部——成本最低。参考parameters_relative,它表示与完全微调相比所需参数的百分比。第二面板展示了这一点,显示~0.5%是所有场景中参数数量最少的。

这对“GPU 内存”面板(其中值越低越好)产生了积极影响,尤其是在“训练速度”面板(其中值越高越好)上效果显著。后者表明,该场景是训练速度最快的,因为参数数量较少,而且因为没有添加额外的模块,在这个场景中需要处理的模块更少。

这作为一个信息性基础基准,用于查看训练速度和 GPU 内存的相对改进,但也突出了一种权衡:模型性能(第一面板)明显是最低的。

此外,这个场景揭示了 0.48%的全量微调参数代表了最小的参数数量。我们将这部分参数专门分配给分类器。此外,由于其他所有场景都微调了分类器,我们始终包括这 0.48%的参数,再加上在这些场景中进一步微调的其他参数。

LoRA 基础

这个场景作为所有基准实验之外所有实验的基础。我们使用r=8,并对所有****线性模块进行调整和微调

我们可以观察到,模型性能与全量微调性能相匹配。我们可能在这个案例中运气不错,但文献表明,我们可以期望仅用大约 1%的参数就能几乎匹配全量微调性能。在这里我们看到了这一点的证据。

此外,由于调整了所有线性模块,我们看到训练速度是所有实验中最慢的,而 GPU 内存利用率是所有场景中最高的,但与其他大多数场景一致。

LoRA 全部,r={1,4,8}

(不幸的是,在图表中我将条形图按 r=4、8、1 的顺序展示,但如果按 1、4、8 的顺序会更容易阅读)

总体而言,这些场景是“LoRA Base”的变体,但r值不同。性能差异很小。然而,正如预期的那样,r与参数数量之间存在正相关,而r与 GPU 内存利用率之间则有轻微的正相关。尽管后者如此,r的值仍然如此低,以至于对最终结果没有实质性影响,特别是对 GPU 内存使用的影响。这确认了我们在原始实验中逐个组件探讨的内容,如上所述。

然而,当回顾r=1时,我们发现这是一个特殊情况。在相对参数数量为 0.61%的情况下,我们仅比“仅分类器”场景的 0.48%稍高。但我们看到r=1的验证准确率约为 0.94,而“仅分类器”则为约 0.82。在总参数的 0.13%中,只有变换器层进行了调整,我们能够将模型的验证准确率提升约 0.12。砰!这令人印象深刻,因此,如果我们对低参数数量感兴趣,这可能是我们的赢家。

关于 GPU 内存利用率,我们稍后会再次审查。但简而言之,除了为模型中的每个参数、优化器和梯度分配内存外,我们还需要保留激活值,以便在反向传播时计算梯度。

此外,更大的模型将显示出选择较小r值的更大影响。

值得一提的是,场景“LoRA all, r=8”使用了与“LoRA Base”相同的超参数值,但执行时是独立的。为了便于比较 r=1、r=4 和 r=8,仍然对该场景进行了评估。

LoRA ff_u

在这种情况下,我们仅调整每一层的逐位置前馈上投影。这导致了参数数量和需要适应的模块数量的减少。因此,数据显示出训练速度的提升和 GPU 内存使用的减少

但我们也看到了一些性能损失。在“LoRA Base”中,我们看到了大约 0.946,而在这种情况下,我们只看到了大约 0.942,下降了约 0.04。

笔记本中详细说明了比较。

附带说明:GPU 内存 / 梯度检查点

查看上面的 GPU 内存面板时,有两点非常明显:

一 — LoRA 本身并不会显著减少内存占用

这在我们调整像 RoBERTa base这种具有 1.25 亿参数的小模型时尤其明显。

在上一篇文章关于内在维度的部分中,我们了解到,对于当前一代模型(例如,具有 7B 参数的模型),r的绝对值甚至可能比较小的容量模型还要小。因此,随着模型规模的增大,节省内存的效果将更加明显

此外,使用LoRA 使得量化变得更加容易且高效——完美契合。使用 LoRA 时,只有一小部分参数需要以高精度处理:这是因为我们更新的是适配器的参数,而不是原始模块的权重。因此,模型的大部分权重可以被量化,并以更低的精度使用。

此外,我们通常使用 AdamW 作为优化器。与 SGD 不同,SGD 只跟踪单一的全局学习率,AdamW 会跟踪每个参数的梯度和梯度的平方的移动平均值。这意味着,对于每个可训练的参数,我们需要跟踪两个值,这些值可能需要使用 FP32 表示。这个过程可能非常昂贵。然而,如前所述,使用 LoRA 时,我们只有少数几个参数是可训练的。这可以显著降低成本,从而使我们能够使用通常需要大量参数的 AdamW,即使在r值较大的情况下。

如果读者们有足够的兴趣,我们可能会在文章系列的第四部分中探讨这些方面。

双 GPU 内存使用仅与参数数量间接相关

如果参数数量和所需的 GPU 内存之间存在直接的线性关系,那该多好呢?不幸的是,图表中有几个发现表明,这并非易事。让我们找出原因。

首先,我们需要为模型本身分配内存,也就是存储所有参数。接着,对于可训练的参数,我们还需要存储优化器的状态和梯度(每个可训练参数单独存储)。此外,我们还需要考虑激活值的内存,这不仅取决于模型的参数和层,还取决于输入序列的长度。而且,至关重要的是要记住,我们需要保留前向传递过程中的激活值,以便在反向传播过程中应用链式法则进行反向传播

如果在反向传播过程中,我们每计算一个层的梯度时都重新计算该层的激活值,我们就不必将激活值保持那么长时间,从而节省内存,但代价是增加了计算量。

这种方法被称为梯度检查点。能够节省的内存量取决于需要保留的额外激活内存量。重要的是要记住,反向传播涉及不断地应用链式法则,一步一步、层层推进:

总结——反向传播中的链式法则

在反向传播过程中,我们首先计算网络顶层(分类器)的误差,然后将误差传递回所有参与的可训练参数。这些参数会根据它们对误差的贡献进行调整,以便未来表现更好。我们通过不断应用链式法则来计算各参数的贡献,从顶层开始,沿着计算图向输入层遍历。这是必要的,因为低层的参数变化可能会影响到所有更高层的参数。

为了计算局部梯度(每一步),我们可能需要获取所有步骤的激活值,这些步骤位于各个可训练参数和最顶层(应用于分类头的损失函数)之间。因此,如果我们有一个位于顶层的参数(靠近分类头),我们需要维护的激活值就比训练较低层参数时所需的激活值少。对于较低层的参数,我们需要遍历更长的计算图来到达分类头,因此需要更多的内存来维持激活值。

在我们的特定模型和任务中,你可以看到下面的效果示意。我们为每一层训练一个独立的模型,在这个过程中,只有该特定层进行训练。通过这种方式,我们可以隔离层的相对位置的影响。然后,我们绘制每个模型以及每一层在训练过程中所需的 GPU 内存量。

在下面的图表中(见左侧面板),你可以看到,如果我们靠近模型底部(即低层数)时,GPU 内存需求较低,而如果我们靠近模型顶部(即高层数)时,损失发生的地方,内存需求较高。

启用梯度检查点后(见右侧面板),我们不再能识别到这种效果。我们不再在反向传播时保存激活值,而是在需要时重新计算它们。因此,左侧和右侧面板之间内存使用的差异就是我们在反向传播过程中维持的激活值。

当我们远离输入层(第 1 层之前)并接近分类头(第 12 层之后)时,GPU 内存需求会减少。直到我们使用梯度检查点(右侧)。此时层的位置不再重要,因为我们不再为反向传播维护激活值。

实验执行:

和之前的实验一样,我使用了 AMT 和网格搜索来提供无偏的结果。

需要记住的是,反向传播过程中重新计算激活值是很慢的,因此我们是在用计算速度与内存使用做权衡

关于测试的更多细节可以在这个笔记本中找到。

我们可能会在本系列文章的第四部分重新讨论内存的话题,尽管这严格来说不是 LoRA 的主题。如果你感兴趣,请在下面的评论中告诉我。

结论

这确实是一个需要消化的内容。感谢你一直坚持到现在。希望你觉得这有价值,并且能够从高层次上确认LoRA 有效:它在只使用全微调约 1%参数的情况下,能够匹配全微调的性能。

但现在,让我们深入探讨一下细节:在应用 LoRA 时,针对我们的模型和我们的任务,探索超参数值时我们应该考虑哪些具体的设计决策

我们的方法

我们提出了关于我们模型可能表现的几个假设,然后收集了实证反馈来验证或否定这些假设。我们选择这种方法,因为我们希望用已有的知识来指导实验的范围,而不是随意测试随机配置。

这种方法证明是有益的,因为解决方案空间很大,无法穷举探索。即使我们用已有的知识限定了实验范围,解释结果仍然具有挑战性。如果我们只是随机在这个庞大的空间中采样,可能会导致计算浪费和无结构的结果。这样的做法会使我们无法得出具有普遍性的结论,进而作出有意图的决策,这将是令人沮丧的。

我们学到了几件事,比如r的相对影响,它对参数数量、GPU 内存和训练速度的细微影响。我们还观察到,可训练参数的数量本身并不能预测 GPU 内存 的使用量。有趣的是,这些参数在网络架构中的位置起着至关重要的作用。此外,我们发现,当使用相同数量的参数时,使用多个 LoRA 模块的训练速度比仅使用一个模块时要慢。

适配所有线性模块——一个实用的选择

了解更多关于 LoRA 如何工作的知识是我们的两个目标之一。我们还旨在为我们的训练找到一组合适的超参数值。在这方面,我们发现,适配所有线性模块并使用较低的r值是一种有效的策略。这种方法具有良好的性能、适中的成本和非常低的复杂性,因此它是一个实用的选择。

当然,像训练任何神经网络一样,仍然需要关注learning-ratebatch-size等超参数。

我们都在研究这个主题的不同方面,但考虑到核心部分的重叠,以上的指导与 Sebastian Raschka 在这篇那篇关于该主题的优秀文章中的发现非常一致,也与 Tim Dettmers 在 QLoRA 论文中的发现[3]相符。这些都是学习使用 LoRA 更多方面的宝贵资源。

[## 使用 LoRA 和 QLoRA 微调 LLMs:数百次实验的见解 - Lightning AI

LoRA 是目前最广泛使用的、参数高效的定制 LLM 训练微调技术之一。它通过节省…

lightning.ai [## 使用 LoRA(低秩适配)微调 LLMs 的实用技巧

从数百次实验中学到的东西

magazine.sebastianraschka.com

仔细选择模块子集——以更低的成本获得更好的性能

另一方面,如果你愿意投入更多时间,你可以实现稍微更好的性能,以及更低的训练时间和内存使用。在选择要适配的模块时,我们发现,通过适配较少的模块,也能匹配适配所有模块的性能。

此外,我们发现将 LoRA 模块均匀分布在所有层上显然是提高模型性能的一个好选择。

对于我们的具体示例,我们通过调整前馈上投影和各层之间的注意力值投影,获得了最佳的性能和相对较低的成本:

然而,对于不同的任务,我可能需要重新评估这个发现。

此外,在分析未来的任务时,我会留意是否仅仅适配上层即可获得良好的性能?这对于本文中的任务并未奏效,但我们之前看到,如果不这样做,GPU 内存利用率将大幅降低。

需要记住的一点是,训练神经网络本质上是一个嘈杂的过程,花时间不断确定最佳超参数可能会与改善其他潜在领域的努力相竞争。也许这些额外的时间会更好地投资于数据整理或增强整体反馈循环。我希望这篇文章展示了一种常识性的方法,在探索成本和潜在回报之间取得平衡

另外,请记住,不要在我们这里讨论的具体模型和发现上过拟合。这只是一个玩具示例,并非业务部门要求的使用案例。没有人需要在 RoBERTa 上训练 sst-2 任务。

然而,请分享你在模型中遇到的经验,包括你觉得这篇文章可能误导了你的地方。

结束这个话题的最后一个思考。今后,我通常会从较低的r值开始。然后考虑预训练任务和微调任务之间的差异有多大。微调过程中需要的适配越大,r值应当越大。

此外,如果我能够识别出需要适配的地方——具体来说,哪些层或组件会受到最大影响——我将利用这些知识选择正确的模块进行适配及其相对的r值。

现在我们已经有了调优后的模型,接下来让我们继续进行部署。在接下来的文章中,我们将探讨如何使用适配器自然地实现创建多任务端点,并在创建每个任务的专用端点时显著提高非功能性属性。

感谢Valerio PerroneÜmit YoldasAndreas GleixnerAndré LiebscherKarsten SchroerVladimir Palkhouski在本文写作过程中提供的宝贵反馈。同时,感谢Sourab Mangrulkar帮助我了解如何使用 HF Trainer API 进行梯度检查点的帮助

头图由Clipdrop制作。所有其他图片均由作者提供。

[1] Yinhan Liu, Myle Ott, Naman Goyal, Jingfei Du, Mandar Joshi, Danqi Chen, Omer Levy, Mike Lewis, Luke Zettlemoyer, Veselin Stoyanov. RoBERTa:一种强优化的 BERT 预训练方法,2019

[2] Edward J. Hu, Yelong Shen, Phillip Wallis, Zeyuan Allen-Zhu, Yuanzhi Li, Shean Wang, Lu Wang, Weizhu Chen. LoRA:大语言模型的低秩适配,2021

[3] Noam Shazeer, 快速 Transformer 解码:一个写头就足够,2019

[4] Tim Dettmers, Artidoro Pagnoni, Ari Holtzmann, Luke Zettlemoyer: QLORA:量化 LLM 的高效微调,2023

有多少宝可梦适合?

原文:towardsdatascience.com/how-many-pok%C3%A9mon-fit-84f812c0387e?source=collection_archive---------8-----------------------#2024-06-12

通过建模和求解背包问题,使用 PokeAPI 和 PuLP 优化 Python 库来找到最佳宝可梦队伍

Maria Mouschoutzi, PhDTowards Data Science Maria Mouschoutzi, PhD

·发表于Towards Data Science ·阅读时间:10 分钟·2024 年 6 月 12 日

--

图片来源:Emmanuelle Magnenat 通过Unsplash

在这篇文章中,我将展示如何使用 PuLP 库在 Python 中建模并求解优化问题,并利用宝可梦数据来说明这一过程。如果你想了解更多关于如何在 Python 中解决优化问题,或者你只是热爱宝可梦,这篇教程将以一种易于理解的方式引导你完成步骤。

那么,开始吧!🚀

一点宝可梦的历史

宝可梦的第一代(Generation I)包含 151 只宝可梦,它们首次出现在 1996 年日本发布的《宝可梦红版》和《宝可梦绿版》游戏中。从那时起,多个新的宝可梦世代和版本陆续发布,导致了超过 1000 只宝可梦的诞生,这些宝可梦出现在电子游戏、动漫、卡片和商品中,成为了文化的象征。然而,在本教程中,我将仅使用第一代 151 只宝可梦的数据。

在最初的《宝可梦》游戏中,有两个主要目标——成为宝可梦冠军和完成宝可梦图鉴。为了成为宝可梦冠军,你必须在宝可梦对战中击败八位道馆馆主并获得…

Cramér–Rao 界限

原文:towardsdatascience.com/the-cram%C3%A9r-rao-bound-d971ec0b641c?source=collection_archive---------2-----------------------#2024-10-22

CC BY-SA 4.0)

你并不总能得到你想要的

Sachin DateTowards Data Science Sachin Date

·发布于 Towards Data Science ·阅读时间 17 分钟·2024 年 10 月 22 日

--

有时,我们得不到我们想要的东西。有时,自然界的规律会明确禁止我们得到它,不管我们多么努力。

Cramér–Rao 界限就是这样一条规律,它设定了一个坚不可摧的天花板——对任何回归模型的预测精度设定了硬性上限,任何人都无法突破这个上限。

在本文中,我们将研究这条规律。

文章大纲

我将涵盖以下两个主题:

  1. Cramér–Rao 界限是什么? 我将介绍界限的方差形式和精度形式。逐步地,我将解析它的定义,并解释它如何整合在一起。

  2. Cramér–Rao 界限如何应用于回归模型? 通过使用一个实际数据集和线性回归模型,我们将研究 Cramér–Rao 界限 在回归建模中的适用性。

Cramér–Rao 界限是什么?

Cramér–Rao 界限(CRB)可以通过两种方式来表示:作为方差的下限,和作为精度的上限。

当你第一次读到 Cramér–Rao 界限的定义时,可能会觉得它像是从水管里喝水一样。但不用担心……

《悲惨世界》社交网络分析:使用 Marimo 笔记本和 NetworkX Python 库

原文:towardsdatascience.com/les-mis%C3%A9rables-social-network-analysis-using-marimo-notebooks-and-the-networkx-python-library-%EF%B8%8F-%EF%B8%8F-3f433216412f?source=collection_archive---------6-----------------------#2024-10-31

数据科学

使用 NetworkX Python 库构建 Marimo 笔记本,揭示维克多·雨果杰作中的隐藏结构

玛丽亚·穆斯丘齐博士Towards Data Science 玛丽亚·穆斯丘齐博士

·发表于 Towards Data Science ·阅读时间:14 分钟·2024 年 10 月 31 日

--

该图像由作者使用 GPT-4o 创建

在这篇文章中,我将带你通过使用 NetworkX Python 库和悲惨世界社交网络数据集,构建一个互动的 Marimo 笔记本进行社交网络分析。通过实施社交网络分析技术,我们可以深入了解小说中不同角色之间的联系如何塑造故事,并揭示正义、爱情和牺牲等主题,这些主题定义了小说的叙事。

那《悲惨世界》怎么样?💔

当然,悲惨世界是有史以来最伟大的故事之一。我几乎热爱它的每一个版本和变化——无论是书籍、电影、电视剧,还是音乐剧——全部都喜欢。

《悲惨世界》写于 1862 年,探讨了 19 世纪法国社会和文化框架下的正义、救赎、爱与牺牲等概念。故事的叙述围绕几位不同角色展开,其中最为突出的是让·瓦尔让,一位寻求救赎的前罪犯,以及警察局长贾维尔,他是……

神经网络的消融测试:补偿性伪装

原文:towardsdatascience.com/ablation-testing-neural-networks-the-compensatory-masquerade-ba27d0037a88?source=collection_archive---------5-----------------------#2024-01-07

破坏性地测试神经网络和其他机器学习架构的部分,目的是使其变得更加健壮

Shafik QuoraisheeTowards Data Science Shafik Quoraishee

·发表于Towards Data Science ·8 分钟阅读·2024 年 1 月 7 日

--

(图片由作者使用 DALL-E 生成)。有趣的是 AI 如何看待它自己的大脑。

类似于一个人的智力如何经受压力测试,人工神经网络也可以通过进行所谓的受控消融测试,来评估它们对不同类型干扰的鲁棒性。

在我们深入探讨消融测试之前,让我们先谈谈一种许多学习机器学习和人工智能应用的人可能熟悉的“破坏性进化”技术:正则化

正则化

正则化是消融的一个非常著名的例子,或者说是选择性地破坏/停用神经网络的部分并重新训练它,使其成为一个更强大的分类器。

通过一种叫做Dropout的过程,神经元可以以受控的方式被停用,这使得之前由现在已停用的神经元处理的工作能够被附近的活跃神经元接管。

在自然界中,大脑实际上也能由于神经可塑性而经历类似的现象。如果一个人遭受脑损伤,在某些情况下,附近的神经元和大脑结构可以重新组织,帮助接管已死亡脑组织的一些功能。

或者,像是如果一个人失去了某种感官,比如视觉,通常其他感官会变得更强,以弥补其丧失的能力。

这也被称为补偿性伪装

左侧是一个全连接神经网络,右侧是随机丢弃版本。在许多情况下,这些网络实际上可能表现得相对较好(图片由作者提供)

切除测试

虽然正则化是神经网络和其他人工智能架构中用来通过人工的“神经可塑性”来帮助神经网络更好地训练的技术,但有时我们希望仅仅对神经网络做类似的处理,看看它在准确性方面,在去激活的情况下会有什么表现。

我们可能会出于几个其他原因这么做:

  • 识别神经网络的关键部分: 神经网络的某些部分可能比其他部分承担更为重要的工作。为了优化网络的资源使用和训练时间,我们可以选择性地去除“较弱的学习者”。

  • 减少神经网络的复杂度: 有时神经网络可能变得非常庞大,尤其是在深度多层感知器(多层感知器)的情况下。这可能使得从输入到输出映射它们的行为变得困难。通过选择性地关闭网络的某些部分,我们可以潜在地识别出过度复杂的区域,并去除冗余——简化我们的架构。

  • 容错性: 在实时系统中,系统的某些部分可能会失败。神经网络的部分也是如此,因此依赖其输出的系统也会受到影响。我们可以借助切除研究来确定,如果摧毁神经网络的某些部分,是否会导致系统的预测或生成能力受损。

切除测试的类型

实际上有很多不同种类的切除测试,下面我们将讨论三种特定类型的测试:

  • 神经元切除

  • 功能性切除

  • 输入切除

快速说明,切除测试的效果会根据你测试的网络和数据本身而有所不同。一个切除测试可能会显示在特定数据集上网络某一部分的弱点,而在另一个切除测试中可能会显示神经网络的另一部分的弱点。这就是为什么在一个真正强健的切除测试系统中,你需要进行多种测试来准确地了解人工神经网络(人工神经网络)的弱点。

神经元切除

这是我们将要进行的第一种切除测试,它最简单且容易观察其效果并扩展。我们将简单地从神经网络中去除不同比例的神经元。

对于我们的实验,我们设置了一个简单的 ANN 来测试随机字符预测的准确性,使用我们老朋友的MNIST 数据集

来自 MNIST 数据集的数字数据快照(作者提供)

这是我写的代码,作为一个简单的 ANN 测试工具,测试数字分类的准确性。

import tensorflow as tf
from tensorflow.keras.datasets import mnist
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Dropout, Flatten
from tensorflow.keras.optimizers import Adam
import matplotlib.pyplot as plt

(x_train, y_train), (x_test, y_test) = mnist.load_data()
x_train, x_test = x_train / 255.0, x_test / 255.0

# Create the ANN Model
def create_model(dropout_rate=0.0):
    model = Sequential([
        Flatten(input_shape=(28, 28)),
        Dense(128, activation='relu'),
        Dropout(dropout_rate),
        Dense(10, activation='softmax')
    ])
    model.compile(optimizer=Adam(),
                  loss='sparse_categorical_crossentropy',
                  metrics=['accuracy'])
    return model

# Run the ablation study: Dropout percentages of neurons
dropout_rates = [0.0, 0.2, 0.4, 0.6, 0.8]
accuracies = []

for rate in dropout_rates:
    model = create_model(dropout_rate=rate)
    model.fit(x_train, y_train, epochs=5, validation_split=0.2, verbose=0)
    loss, accuracy = model.evaluate(x_test, y_test, verbose=0)
    accuracies.append(accuracy)

plt.plot(dropout_rates, accuracies, marker='o')
plt.title('Accuracy vs Dropout Rate')
plt.xlabel('Dropout Rate')
plt.ylabel('Accuracy')
plt.grid(True)
plt.show()

所以如果我们运行上述代码,我们会看到如下结果:逐步停用我们 128 节点 MLP 的一定比例。

在这个简单的示例中,结果相当有趣,正如你所看到的,丢弃 80%的神经元几乎不影响准确性,这意味着去除多余的神经元肯定是我们在构建这个网络时可以考虑的一种优化方法。

为 dropout 切除测试生成的图表(作者提供)

功能性切除

对于功能性切除,我们将神经元的激活函数更改为不同的曲线,具有不同程度的非线性。我们使用的最后一个函数是一条直线,完全破坏了模型的非线性特征。

因为非线性模型在定义上比线性模型更复杂,而激活函数的目的是在分类上引入非线性效应,所以可以得出这样的推理:

“如果我们可以通过使用线性函数代替非线性函数,并且仍然能得到很好的分类结果,那么或许我们可以简化我们的架构,降低其成本”

注意: 除了正则化之外,你会注意到,某些类型的切除测试,如功能性切除,与超参数调优有相似之处。它们是相似的,但切除测试更多是指改变神经网络架构的部分(例如神经元、层等),而超参数调优则是指改变模型的结构参数。两者的目标都是优化。

# Activation function ablation
activation_functions = ['relu', 'sigmoid', 'tanh', 'linear']
activation_ablation_accuracies = []
for activation in activation_functions:
    model = create_model(activation=activation)
    model.fit(x_train, y_train, epochs=5, validation_split=0.2, verbose=0)
    loss, accuracy = model.evaluate(x_test, y_test, verbose=0)
    activation_ablation_accuracies.append(accuracy)

当我们运行上述代码时,我们得到以下激活函数与准确度的比较。

为功能性切除测试生成的图表(作者提供)

所以,确实看起来某种形式的非线性对分类非常重要,其中“ReLU”和双曲正切的非线性效果最为有效。这是有道理的,因为众所周知,数字分类最好作为一个非线性任务来处理。

特征切除

我们还可以从分类中移除特征,看看这对预测器的准确性有何影响。

通常在做机器学习或数据科学项目之前,我们通常会进行探索性数据分析(EDA)和特征选择,以确定哪些特征可能对我们的分类问题很重要。

但是有时我们可以观察到有趣的效果,特别是在处理神秘的神经网络时,通过在消融研究中移除特征并观察其对分类的影响。使用以下代码,我们可以按 4 列一组地从字母中移除像素列。

显然,消融特征有多种方式,除了按列扭曲字符之外,还可以通过其他方式。但我们可以从这个简单的例子开始并观察其效果。

# Input feature ablation
input_ablation_accuracies = []
for i in range(0, 28, 4):  # Remove columns of pixels groups of 4
    x_train_ablated = np.copy(x_train)
    x_test_ablated = np.copy(x_test)
    x_train_ablated[:, :, i:min(i+4, 28)] = 0
    x_test_ablated[:, :, i:min(i+4, 28)] = 0

    model = create_model()
    model.fit(x_train_ablated, y_train, epochs=5, validation_split=0.2, verbose=0)
    loss, accuracy = model.evaluate(x_test_ablated, y_test, verbose=0)
    input_ablation_accuracies.append(accuracy)

在运行上述特征消融代码后,我们看到:

为 4 列输入特征消融测试生成的图表(图片来自作者)

有趣的是,当我们移除第 8 到第 12 列时,准确度略有下降,然后再次上升。这表明平均而言,更“敏感”的字符几何形状位于这些中心列中,但其他列,特别是接近开始和结束的列,可能会被移除以优化效果。

这是对每次移除 7 列的相同测试,以及相应的列。通过可视化实际的扭曲字符数据,我们可以更好地理解结果,因为我们看到移除前几列对结果的影响较小,这是因为它们大多只是填充!

为 4 列像素移除结果生成的图表(图片来自作者)

另一个有趣的消融研究例子是测试不同类型的噪声配置文件。下面是我写的代码,用于使用上述 ANN 模型渐进地为图像加噪声。

# Ablation study with noise
noise_levels = [0, 0.1, 0.2, 0.3, 0.4, 0.5]
noise_ablation_accuracies = []

plt.figure(figsize=(12, 6))

for i, noise_level in enumerate(noise_levels):
    x_train_noisy = x_train + noise_level * np.random.normal(0, 1, x_train.shape)
    x_test_noisy = x_test + noise_level * np.random.normal(0, 1, x_test.shape)
    x_train_noisy = np.clip(x_train_noisy, 0, 1)
    x_test_noisy = np.clip(x_test_noisy, 0, 1)

    model = create_model()
    model.fit(x_train_noisy, y_train, epochs=5, validation_split=0.2, verbose=0)
    loss, accuracy = model.evaluate(x_test_noisy, y_test, verbose=0)
    noise_ablation_accuracies.append(accuracy)

    # Plot noisy test images
    plt.subplot(2, len(noise_levels), i + 1)
    for j in range(5):  # Display first 5 images
        plt.imshow(x_test_noisy[j], cmap='gray')
        plt.axis('off')
    plt.title(f'Noise Level: {noise_level}')

我们已经为网络在增加强度的高斯噪声环境中的稳健性创建了一个消融研究。请注意,随着噪声水平的增加,预测准确度预期并显著下降。

为渐进噪声结果生成的图表(图片来自作者)

类似这样的情况让我们知道,可能需要增加神经网络的能力和复杂性来进行补偿。还要记住,消融研究可以相互结合进行,在不同类型的噪声与不同类型的失真结合的情况下进行。

结论

消融研究对优化和测试神经网络非常重要。我们在这篇文章中演示了一个小例子,但有无数种方法可以在不同和更复杂的网络架构上运行这些研究。如果你有任何想法,非常希望得到反馈,甚至可以将其纳入你的文章中。感谢阅读!

关于在 DAX 中根据另一个值计算销售额

原文:towardsdatascience.com/about-calculating-sales-depending-on-another-value-in-dax-914deb450d23?source=collection_archive---------9-----------------------#2024-01-18

我们通常的度量值会聚合来自一个事实表中列的值。那么,计算一个依赖于另一个事实表中数据的结果需要什么?让我们来看一个可能的解决方案。

Salvatore CagliariTowards Data Science Salvatore Cagliari

·发布于 Towards Data Science ·8 分钟阅读·2024 年 1 月 18 日

--

图片由 Isaac Smith 提供,来自 Unsplash

引言

在 DAX 中根据特定值计算结果需要什么?

简单:一个过滤器。

但如果过滤器必须是一个表呢?

幸运的是,我已经知道如何在 DAX 度量值中使用表格:

## 在 DAX 度量值中使用中间结果

我们在 DAX 中经常使用表变量。如果我们必须计算中间结果并在 DAX 中重复使用它们,怎么办呢……

towardsdatascience.com

现在,我的客户给了我一个挑战,让我把我的知识付诸实践:

看看以下报告:

图 1 — 在线销售与零售销售及销售的产品(图由作者提供)

如你所见,在线销售和零售销售的产品数量有所不同。

我的客户问了以下问题:

  • 那么,零售商店销售的产品的在线销售额是多少?

  • 那么,哪些产品仅在线销售?

尝试在 Power BI 中使用过滤器解决这个问题

我能通过像这样在左侧表格上应用过滤器来回答第一个问题吗?

图 2 — 在 Power BI 中应用基于零售销售度量值的过滤器(作者提供的图)

不幸的是,这并不起作用。

原因是这两个表之间没有任何关系,并且维度表的过滤方向仅在单向工作。

是的,我可以将其更改为双向过滤。但这样做可能会引入问题,因为在某些情况下它不会生效。

另一种方法是添加多个度量值,并为可视化添加一个过滤器:

图 3 — 在 Power BI 中应用多个度量值的过滤器(作者提供的图)

这样可以得到所需的结果。

我甚至可以将过滤器更改为“Sum Retail Sales is blank”,以获取仅在网上销售的产品及其销售数量。

但生成的可视化包含了我不想看到的“Sum Retail Sales”度量值。而且,当我想获取仅在线销售的产品数量时,这个度量值会显示空列,这也不是我想看到的结果。

所以,我必须创建一个明确的 DAX 度量值来解决上述问题。

从源数据库获取结果

但首先,我必须获取一个参考结果。

我可以使用这个参考结果在构建解决方案时验证结果。

我编写以下 SQL 查询来获取此参考结果:

WITH [RetailProductsByMonth]
AS
  (SELECT DISTINCT [ProductKey]
            ,[D].[MonthKey]
    FROM [dbo].[FactSales] AS [F]
      INNER JOIN [Date] AS [D]
        ON [D].[Date] = [F].[DateKey]
  )
SELECT [D].[EnglishYearMonthShortName]
      ,[D].[MonthKey]
      ,COUNT(DISTINCT [F].[ProductKey]) AS [ProductCount]
  FROM [dbo].[FactOnlineSales] AS [F]
    INNER JOIN [Date] AS [D]
      ON [D].[Date] = [F].[DateKey]
  INNER JOIN [RetailProductsByMonth] AS [PbM]
      ON [PbM].[ProductKey] = [F].[ProductKey]
      AND [D].[MonthKey] = [PbM].[MonthKey]
  GROUP BY [D].[EnglishYearMonthShortName], [D].[MonthKey]
  ORDER BY [D].[MonthKey] DESC;

这是结果:

图 4 — 获取仅在线和门店销售的产品的 SQL 查询结果(作者提供的图)

要获取仅在线销售的产品数量,我使用以下 SQL 查询:

WITH [RetailProductsByMonth]
AS
  (SELECT DISTINCT [ProductKey]
            ,[D].[MonthKey]
      FROM [dbo].[FactSales] AS [F]
        INNER JOIN [Date] AS [D]
          ON [D].[Date] = [F].[DateKey]
  )
SELECT [D].[EnglishYearMonthShortName]
      ,[D].[MonthKey]
      ,COUNT(DISTINCT [F].[ProductKey]) AS [ProductCount]
  FROM [dbo].[FactOnlineSales] AS [F]
    INNER JOIN [Date] AS [D]
      ON [D].[Date] = [F].[DateKey]
    LEFT OUTER JOIN [RetailProductsByMonth] AS [PbM]
      ON [PbM].[ProductKey] = [F].[ProductKey]
      AND [D].[MonthKey] = [PbM].[MonthKey]
  WHERE [PbM].[ProductKey] IS NULL
  GROUP BY [D].[EnglishYearMonthShortName], [D].[MonthKey]
  ORDER BY [D].[MonthKey] DESC;

这个查询的结果如下:

图 5 — 获取仅在线销售产品的 SQL 查询结果(作者提供的图)

在这两种情况下,首先,我会获取每个月零售销售中所有销售产品的列表。

然后,我将结果与在线销售表连接,并统计每月销售的产品数量。

在第二个查询中,我排除了所有零售销售数据中的产品。

在这两种情况下,我得到了所需的参考结果,并且可以继续在 Power BI 中编写 DAX 代码。

查询数据模型

我没有尝试在 Power BI 中构建度量值,而是使用DAX Studio编写 DAX 查询,逐步构建解决方案。

首先,我想获取所有在零售店销售的产品。

由于我的 SQL 查询按月分组结果,我将第一个查询限制为仅一月。这也简化了所需的代码:

DEFINE
  VAR MonthSelection = 202311

EVALUATE
  CALCULATETABLE(
          SUMMARIZECOLUMNS('Retail Sales'[ProductKey] )
          ,'Date'[MonthKey] = MonthSelection
          )

这是结果:

图 6 — DAX 查询结果,列出在零售店销售的产品(作者提供的图)

所以,我得到了 2023 年 11 月销售的 2504 个产品。

下一步是将此结果作为过滤器注入到查询中,以统计仅在线销售的产品数量:

DEFINE
  VAR MonthSelection = 202311

  VAR RetailProductList =
        CALCULATETABLE(
                SUMMARIZECOLUMNS('Retail Sales'[ProductKey] )
                ,'Date'[MonthKey] = MonthSelection
                )

EVALUATE
{
  COUNTROWS(
        CALCULATETABLE(
                SUMMARIZECOLUMNS('Online Sales'[ProductKey] )
                ,'Online Sales'[ProductKey] IN RetailProductList ,'Date'[MonthKey] = MonthSelection
                )
        )
 }

现在我有 2498 个产品:

图 7 — 计算在线销售和零售商店销售产品的 DAX 查询结果(图源:作者)

如你所见,我将零售商店中销售的产品列表存储在变量 RetailProductList 中。

然后,我使用 CALCULATETABLE() 来过滤在线销售表中的 ProductKey,使用这个变量。

为了将结果限制为仅一个月份,我必须添加第 17 行。

为了仅获得产品的计数,我使用 COUNTROWS() 并将其包含在大括号中以生成输出表。

我必须添加大括号,因为 COUNTROWS() 返回一个数字,而 EVALUATE 期望一个表表达式。大括号将 COUNTROWS() 的结果转换为一个包含一列一行的表。

当我将 2498 的计数与上述第一个 SQL 查询的结果进行比较时,对于 2023 年 11 月,我得到了相同的结果。

因此,逻辑按预期工作。

创建度量值

现在,我可以将查询放入度量值中。

由于度量值必须在我希望获取月份列表并计算每个月的结果时工作,我必须去掉硬编码的月份,并针对结果中的每一行评估该月份:

DEFINE
 MEASURE 'All Measures'[Online Sold Product Count with Retail Sales] =
    VAR MonthSelection = MAX('Date'[MonthKey])

    VAR RetailProductList =
        CALCULATETABLE(
              SUMMARIZECOLUMNS('Retail Sales'[ProductKey] )
                                ,'Date'[MonthKey] = MonthSelection
                         )

RETURN
  COUNTROWS(
      CALCULATETABLE(
              SUMMARIZECOLUMNS('Online Sales'[ProductKey] )
                  ,KEEPFILTERS('Online Sales'[ProductKey] IN RetailProductList)
                  ,'Date'[MonthKey] = MonthSelection
                  )
              )
EVALUATE
  SUMMARIZECOLUMNS('Date'[Year Month Short Name]
            ,'Date'[MonthKey]
            ,"Online Sold Product Count with Retail Sales", [Online Sold Product Count with Retail Sales]
            )
 ORDER BY 'Date'[MonthKey] DESC

度量值是 DEFINE MEASURE 和 EVALUATE 之间的所有内容。

这是结果,与上述 SQL 查询的结果相同:

图 8 — 使用查询派生的度量值的 DAX 查询结果(图源:作者)

为了获得仅在线销售的产品,我必须修改度量值中的一行,排除零售商店中的销售产品:

,KEEPFILTERS(NOT ('Online Sales'[ProductKey] IN RetailProductList) )

结果与上述第二个 SQL 查询的结果相同:

图 9 — 使用仅在线销售的度量值的 DAX 查询结果(图源:作者)

结论

这个任务与我在关于在 DAX 中使用中间结果的文章中描述的逻辑类似(你可以在下面的参考部分找到该文章的链接)。

但这次,在使用表变量时,我不需要额外的 FILTER()

我不确定为什么在写那篇文章时我需要这样做,但我将进一步研究。

如你所见,这次我使用 DAX 查询一步一步地开发了解决方案。

我觉得这样做很有用,因为我使用了一个中间表,可以在开发过程中进行检查。

对于这种场景,使用查询比直接在 Power BI Desktop 中构建这样的解决方案要更容易。

此外,对于那些来自关系型数据库工作背景并开始使用 DAX 的人来说,这一点很有意思:我需要显式地在 SQL 查询中添加月份,以便按月份对结果进行分组。而在 DAX 中,添加月份到查询时分组会自动完成,我不需要做额外的操作。

照片由 Firmbee.com 提供,来自 Unsplash

参考资料

几个月前,我写了一篇关于在 DAX 度量中使用中介表的文章。你可以在这里找到它:

## 在 DAX 度量中使用中介结果

我们在 DAX 中经常使用表变量。如果我们必须计算中介结果并在 DAX 中重用它们怎么办……

towardsdatascience.com

我使用 Contoso 示例数据集,就像在我之前的文章中一样。你可以从微软 这里 免费下载 ContosoRetailDW 数据集。

我将客户的请求翻译为 ContosoRetailDW 数据集中的数据。

Contoso 数据可以在 MIT 许可下自由使用,如 这里 所述。

我扩大了数据集,以便让 DAX 引擎处理更多的工作。

Online Sales 表包含 7100 万行(而不是 1260 万行),而 Retail Sales 表包含 1850 万行(而不是 340 万行)。

[## 每当 Salvatore Cagliari 发布新文章时,收到电子邮件通知。

每当 Salvatore Cagliari 发布新文章时,收到电子邮件通知。通过注册,你将创建一个 Medium 账户,如果你还没有的话……

medium.com](https://medium.com/@salvatorecagliari/subscribe?source=post_page-----914deb450d23--------------------------------)

我让我的文章对每个人都可访问,即使 Medium 有付费墙。这让我能从每个读者那里赚取一点收入,但我关闭了付费墙,这样你就可以免费阅读我的文章。

你可以通过以下方式支持我的工作,我在空闲时间做这些工作:

buymeacoffee.com/salvatorecagliari

或者扫描这个二维码:

任何支持都将不胜感激,并帮助我找到更多时间为你创作更多内容。

非常感谢。

亚伯拉罕·德·摩弗、他的著名定理与正态曲线的诞生

原文:towardsdatascience.com/abraham-de-moivre-his-famous-theorem-and-the-birth-of-the-normal-curve-ee11ab5f9f20?source=collection_archive---------7-----------------------#2024-02-13

公有领域/知识共享署名-相同方式共享 2.0 协议/公有领域/公有领域/作者提供的图片/公有领域

亚伯拉罕·德·摩弗的生平与时代,他的著名定理,以及它如何为中央极限定理的发现奠定基础

Sachin DateTowards Data Science Sachin Date

·发表于Towards Data Science ·阅读时长 16 分钟·2024 年 2 月 13 日

--

大约在 1689 年,概率论领域的一个重大发现发生在瑞士联邦。主角是雅各布·伯努利,而这项发现是大数法则弱型 伯努利证明了随机选取的样本均值在概率上收敛于总体均值。不幸的是,伯努利在二项实验的背景下证明了大数法则弱型,而在 1600 年代,欧洲没有人知道如何高效地近似,甚至无法计算,构成二项式概率的巨大阶乘。这意味着尽管大数法则弱型具有开创性的重大意义,但在实际应用中几乎没有任何用处。

在雅各布·伯努利于 1705 年去世后,四十年才有一位名叫亚伯拉罕·德·莫夫尔的自我流放法国人来到英国,解决了二项式逼近的问题,并因此为我们带来了正态分布曲线。

这是关于德·莫夫尔的故事,他的定理以及他的遗产。

亚伯拉罕·德·莫夫尔

加速 AI/ML 模型训练的自定义操作符

原文:towardsdatascience.com/accelerating-ai-ml-model-training-with-custom-operators-163ef2a04b12?source=collection_archive---------1-----------------------#2024-08-11

关于创建特定模型 GPU 内核的潜在好处及其在优化动态形状张量使用中的应用

Chaim RandTowards Data Science Chaim Rand

·发布于Towards Data Science ·阅读时长 15 分钟·2024 年 8 月 11 日

--

图片由David Marioni提供,来源于Unsplash

本文是关于分析和优化 AI/ML 模型训练运行时性能的长篇系列文章的延续。文章本可以标题为“PyTorch 模型性能分析与优化——第七部分”,但考虑到当前主题的重要性,我们认为需要为此专门撰写一篇(或多篇)文章。在我们之前的文章中,我们详细讨论了分析和优化 AI/ML 工作负载的重要性,以及它对 AI/ML 模型开发的速度和成本可能产生的重大影响。我们提倡使用多种工具和技术来分析和优化训练性能,并且已经展示了许多实际应用。本文将讨论一种更为先进的优化技术——这项技术能够将真正的高手与普通业余者区分开来——在 C++和 CUDA 中创建自定义的 PyTorch 操作符。

流行的机器学习框架,如 PyTorch、TensorFlow 和 JAX,通常使用为底层硬件(无论是 CPU、GPU,还是像 Google TPU 这样的 AI 专用 ASIC)优化的软硬件组件构建。然而,不可避免地,你可能会发现组成模型的某些计算模块的性能不尽如人意或不够优化。通常,调整低级代码模块——通常被称为 内核(kernels)——以满足 AI/ML 模型的特定需求,能够显著加速模型训练和推理的运行时性能。这样的加速可以通过实现之前不支持的功能(例如,先进的注意力模块)、将独立操作融合在一起(例如,参见PyTorch 的乘加融合教程),和/或根据当前模型的特定属性优化现有内核来实现。重要的是,执行此类定制的能力依赖于 AI 硬件和 ML 框架的支持。尽管我们在本文中的重点是 NVIDIA GPU 和 PyTorch 框架,但应该注意,其他 AI 专用 ASIC 和 ML 框架也提供类似的定制内核功能。NVIDIA 通过其CUDA 工具包支持开发自定义内核。PyTorch 也提供了专用的API教程,用于公开这一功能并将其集成到模型设计中。

本文的目的是引起大家对内核定制的强大功能和潜力的关注,并展示其在训练动态形状张量模型这一独特挑战中的应用。我们并非——绝对不是——要替代有关开发自定义操作的官方文档。此外,我们分享的示例仅供演示目的使用。我们没有对这些示例进行优化,也没有验证它们的稳健性、耐久性或准确性。如果你决定基于本文投资于 AI/ML 优化,通过自定义 CUDA 内核开发,你应该确保接受适当的培训。

玩具模型 — 动态形状张量的挑战

AI 模型中动态形状张量的普遍存在可能会在性能优化方面带来独特且令人兴奋的挑战。我们已经在上一篇文章中看到过一个例子,展示了如何使用布尔掩码触发不希望发生的 CPU-GPU 同步事件,并且我们提倡避免使用它们。一般来说,AI 加速器倾向于偏好固定形状的张量,而不是动态形状的张量。这不仅简化了内存资源的管理,还能为性能优化提供更大的空间(例如,使用torch.compile)。下面的玩具示例展示了这一挑战。

假设我们的任务是为下一代数码相机创建一个人脸检测模型。为了训练这个模型,我们提供了一个包含一百万张 256×256 灰度图像的训练集,以及每张图像的对应地面真实框。自然,每张图像中的人脸数量可能会有很大差异,大多数图像包含五个或更少的人脸,只有少数图像包含数十个甚至数百个。我们的模型要求支持所有这些变化。具体来说,我们的模型需要支持在图像中检测最多 256 张人脸。

为了解决这个挑战,我们定义了以下简单的模型,该模型生成边界框并附带一个损失函数。具体来说,我们简单地基于目标框的数量截断模型输出,而不是执行某种匹配算法来匹配边界框预测与地面真实目标之间的关系。我们(有些随意地)选择了广义交并比(GIOU)损失函数。现实世界的解决方案可能会更加复杂(例如,它可能包括一个损失组件,针对假阳性进行惩罚)。

import torch
import torch.nn as nn
import torch.nn.functional as F

class Net(nn.Module):
    def __init__(self):
        super().__init__()
        conv_layers = []
        for i in range(4):
            conv_layers.append(nn.Conv2d(4 ** i, 4 ** (i + 1), 3,
                                         padding='same'))
            conv_layers.append(nn.MaxPool2d(2, 2))
            conv_layers.append(nn.ReLU())
        self.conv_layers = nn.Sequential(*conv_layers)

        self.lin1 = nn.Linear(256 * 256, 256 * 64)
        self.lin2 = nn.Linear(256 * 64, 256 * 4)

    def forward(self, x):
        x = self.conv_layers(x.float())
        x = self.lin2(F.relu(self.lin1(x.view((-1, 256 * 256)))))
        return x.view((-1, 256, 4))

def generalized_box_iou(boxes1, boxes2):
    # loosly based on torchvision generalized_box_iou_loss code
    epsilon = 1e-5

    area1 = (boxes1[..., 2]-boxes1[..., 0])*(boxes1[..., 3]-boxes1[..., 1])
    area2 = (boxes2[..., 2]-boxes2[..., 0])*(boxes2[..., 3]-boxes2[..., 1])

    lt = torch.max(boxes1[..., :2], boxes2[..., :2])
    rb = torch.min(boxes1[..., 2:], boxes2[..., 2:])

    wh = (rb - lt).clamp(min=0)
    inter = wh[..., 0] * wh[..., 1]

    union = area1 + area2 - inter
    iou = inter / union.clamp(epsilon)

    lti = torch.min(boxes1[..., :2], boxes2[..., :2])
    rbi = torch.max(boxes1[..., 2:], boxes2[..., 2:])

    whi = (rbi - lti).clamp(min=0)
    areai = (whi[..., 0] * whi[..., 1]).clamp(epsilon)

    return iou - (areai - union) / areai

def loss_fn(pred, targets_list):
    batch_size = len(targets_list)
    total_boxes = 0
    loss_sum = 0.
    for i in range(batch_size):
        targets = targets_list[i]
        num_targets = targets.shape[0]
        if num_targets > 0:
            sample_preds = pred[i, :num_targets]
            total_boxes += num_targets
            loss_sum += generalized_box_iou(sample_preds, targets).sum()
    return loss_sum / max(total_boxes, 1)

由于每个图像中面部的数量不同,损失是针对每个单独样本分别计算的,而不是一次性(针对整个批次)计算。具体来说,CPU 将启动与损失函数相关的每个 GPU 内核,B次,其中 B 是选择的批量大小。根据批次的大小,这可能会带来显著的开销,正如我们下面所看到的那样。

在下面的代码块中,我们定义了一个生成随机图像和相关边界框的数据集。由于每张图像中人脸的数量不同,我们需要一个自定义聚合函数来将样本分组为批次:

from torch.utils.data import Dataset, DataLoader
import numpy as np

# A dataset with random images and gt boxes
class FakeDataset(Dataset):
    def __init__(self):
        super().__init__()
        self.size = 256
        self.img_size = [256, 256]

    def __len__(self):
        return 1000000

    def __getitem__(self, index):
        rand_image = torch.randint(low=0, high=256, 
                                   size=[1]+self.img_size,
                                   dtype=torch.uint8)

        # set the distribution over the number of boxes to reflect the fact
        # that the vast majority of images have fewer than 10 faces
        n_boxes = np.clip(np.floor(np.abs(np.random.normal(0, 3)))
                                      .astype(np.int32), 0, 255)

        box_sizes = torch.randint(low=1, high=self.size, size=(n_boxes,2))
        top_left = torch.randint(low=0, high=self.size-1, size=(n_boxes, 2))
        bottom_right = torch.clamp(top_left + box_sizes, 0, self.size -1)
        rand_boxes = torch.concat((top_left,bottom_right), dim = 1)
        return rand_image, rand_boxes.to(torch.uint8)

def collate_fn(batch):
    images = torch.stack([b[0] for b in batch],dim=0)
    boxes = [b[1] for b in batch]
    return images, boxes

train_loader = DataLoader(
    dataset = FakeDataset(),
    batch_size=1024,
    pin_memory=True,
    num_workers=16,
    collate_fn=collate_fn
)

通常,每个训练步骤以将训练批次从主机(CPU)复制到设备(GPU)开始。当我们的数据样本大小固定时,它们会批量复制。然而,图像中每个图样的面部数量变化的一个影响是,每个样本的边界框目标需要单独复制,这需要进行更多的单独复制操作。

def data_to_device(data, device):
    if isinstance(data, (list, tuple)):
        return type(data)(
            data_to_device(val, device) for val in data
        )
    elif isinstance(data, torch.Tensor):
        return data.to(device=device, non_blocking=True)

最后,我们定义了训练/评估循环。为了便于讨论,我们选择仅关注训练循环的前向传递。注意,我们包含了一个 PyTorch profiler 对象,并且使用了显式的 同步事件(以便评估前向传递不同部分的性能)。

device = torch.device("cuda:0")
model = torch.compile(Net()).to(device).train()

# forward portion of training loop wrapped with profiler object
with torch.profiler.profile(
   schedule=torch.profiler.schedule(wait=5, warmup=5, active=10, repeat=1),
   on_trace_ready=torch.profiler.tensorboard_trace_handler('/tmp/perf/'),
   profile_memory=True
) as prof:
    for step, data in enumerate(train_loader):

        with torch.profiler.record_function('copy data'):
            images, boxes = data_to_device(data, device)
            torch.cuda.synchronize(device)

        with torch.profiler.record_function('forward'):
            with torch.autocast(device_type='cuda', dtype=torch.bfloat16):
                outputs = model(images)
            torch.cuda.synchronize(device)

        with torch.profiler.record_function('calc loss'):
            loss = loss_fn(outputs, boxes)
            torch.cuda.synchronize(device)
        prof.step()
        if step > 30:
            break

    # filter and print profiler results
    event_list = prof.key_averages()
    for i in range(len(event_list) - 1, -1, -1):
        if event_list[i].key not in ['forward', 'calc loss', 'copy data']:
            del event_list[i]
    print(event_list.table())

性能分析

在一台 Google Cloud g2-standard-16 虚拟机(配备单个 L4 GPU)、一个专用的 深度学习虚拟机镜像 和 PyTorch 2.4.0 上运行我们的脚本,生成了以下输出(我们已将其裁剪以提高可读性)。

-------------  ------------  ------------
         Name     CPU total  CPU time avg
-------------  ------------  ------------
    copy data     288.164ms      28.816ms
      forward        1.192s     119.221ms
    calc loss        9.381s     938.067ms
-------------  ------------  ------------
Self CPU time total: 4.018s
Self CUDA time total: 10.107s

尽管损失函数的操作要少得多,但它完全主导了整体步骤时间。每个批次中样本的底层 GPU 核心反复调用的开销在 TensorBoardTrace视图中非常明显:

每个批次样本调用损失函数的影响,如在 TensorBoard 中所见(作者)

通过串联优化

减少调用损失函数的次数的一种方法是使用 串联 将每个批次中所有有效的框组合在一起,如下代码块所示。

def loss_with_concat(pred, targets_list):
    bs = len(targets_list)
    all_targets = torch.concat(targets_list, dim = 0)
    num_boxes = [targets_list[i].shape[0] for i in range(bs)]
    all_preds = torch.concat([pred[i,: num_boxes[i]] for i in range(bs)],
                              dim=0)
    total_boxes = sum(num_boxes)
    loss_sum = generalized_box_iou(all_targets, all_preds).sum()
    return loss_sum/max(total_boxes, 1)

这个优化的结果如下所示。

-------------  ------------  ------------
         Name     CPU total  CPU time avg
-------------  ------------  ------------
    copy data     522.326ms      52.233ms
      forward        1.187s     118.715ms
    calc loss     254.047ms      25.405ms
-------------  ------------  ------------
Self CPU time total: 396.674ms
Self CUDA time total: 1.871s

串联优化使得损失函数的速度提高了 37 倍(!!)。然而,请注意,它并没有解决每个主机到设备的样本真值数据拷贝的开销。这个开销可以在下面的 TensorBoard 的Trace视图截图中看到:

如 TensorBoard 中所见的每个批次样本的主机到设备拷贝的影响(作者)

通过填充优化

避免使用动态形状张量的一种常见方法是填充。在以下代码块中,我们修改了合并函数,将每个数据样本的真值边界框用零填充,直到最大支持的框数,即 256 个。(请注意,填充也可以在 Dataset 类中完成。)

def collate_with_padding(batch):
    images = torch.stack([b[0] for b in batch],dim=0)
    padded_boxes = []
    for b in batch:
        p = torch.nn.functional.pad(
                       b[1], (0, 0, 0, 256 - b[1].shape[0]), value = 0)
        padded_boxes.append(p)
    boxes = torch.stack(padded_boxes,dim=0)
    return images, boxes

将样本填充为固定大小的张量使我们能够通过一次调用复制批次的真实标签。这也使我们能够通过一次调用损失函数来计算损失。请注意,这种方法需要屏蔽结果损失,如下所示,以便只考虑有效的框。

def loss_with_padding(pred, targets):
    mask = (targets[...,3] > 0).to(pred.dtype)
    total_boxes = mask.sum()
    loss = generalized_box_iou(targets, pred)
    masked_loss = loss*mask
    loss_sum = masked_loss.sum()
    return loss_sum/torch.clamp(total_boxes, 1)

结果的运行时性能如下所示:

-------------  ------------  ------------
         Name     CPU total  CPU time avg
-------------  ------------  ------------
    copy data      57.125ms       5.713ms
      forward        1.315s     131.503ms
    calc loss      18.438ms       1.844ms
-------------  ------------  ------------
Self CPU time total: 11.723ms
Self CUDA time total: 1.378s

注意到数据复制的性能提升近 10 倍,损失函数性能提升额外的 14 倍。请记住,填充可能会增加 GPU 内存的使用。在我们的案例中,这种增加不到 1%。

尽管我们的损失函数的运行时间显著提高,但我们注意到,损失函数中执行的大多数计算被立即屏蔽掉了。我们不禁想知道,是否有办法通过避免这些冗余操作进一步提高性能。在下一节中,我们将探讨使用自定义 CUDA 核心所提供的机会。

创建自定义 CUDA 核心

许多教程会强调创建 CUDA 核心的难度以及高门槛。虽然掌握 CUDA 开发并调优核心以最大化 GPU 的利用率确实可能需要多年的经验以及对 GPU 架构的深入理解,但我们坚信即使是一个初学者(但雄心勃勃的)CUDA 爱好者/机器学习开发者,也能在构建自定义 CUDA 核心时取得成功并从中获益。在本节中,我们将以 PyTorch(相对简单)的 C++/CUDA 扩展为例,并用 GIOU 核心进行增强。我们将分两个阶段进行:首先,我们将天真地将所有 GIOU 逻辑转移到 C++/CUDA,以评估内核融合对性能的影响。然后,我们将利用我们新获得的低级控制,添加条件逻辑并减少不必要的算术运算。

开发 CUDA 核心允许你确定每个 GPU 线程执行的核心逻辑,以及这些线程如何分配到底层的 GPU 流式多处理器(SMs)上。以最优化的方式进行此操作需要对 GPU 架构有专家级的理解,包括 GPU 内存的不同层次、内存带宽、片上加速引擎(例如 TensorCores)、每个 SM 支持的并发线程数以及它们的调度方式,等等。更复杂的是,这些属性在不同的 GPU 代际和型号之间可能有所不同。请参见这篇博客,它提供了一个非常基础,但又非常简单的 CUDA 入门介绍。

步骤 1 — 内核融合

回顾我们上次实验的跟踪视图,您可能会注意到,我们的损失计算的前向传播包括大约三十个独立的算术操作,这些操作会导致启动和运行一个独立的 CUDA 内核(可以通过简单地数一下 cudaLaunchKernel 事件的数量看出)。这可能会通过多种方式对性能产生负面影响。例如:

  1. 每个内核的启动都需要 CPU 和 GPU 之间的专门通信——这是我们始终尽量减少的。

  2. 每个内核都需要等到前一个内核完成后才能运行。有时,这是无法避免的,但在某些情况下,比如我们的情况——大多数操作是“逐像素”进行的——是可以避免的。

  3. 使用许多独立内核可能会影响 GPU 内存的使用方式。

通过 内核融合 优化旨在通过将这些操作合并成较少的内核来减少这种开销,从而减少多个内核的开销。

在下面的代码块中,我们定义了一个内核,它对单个边界框预测-目标对执行 GIOU 计算。我们使用一个 1-D 网格来分配大小为 256 的线程块,其中每个块对应训练批次中的一个样本,每个线程对应样本中的一个边界框。因此,每个线程 — 通过 blockthread ID 的组合唯一标识 — 接收预测值 (boxes1) 和目标值 (boxes2),并对由 ID 确定的单个边界框执行 GIOU 计算。如前所述,边界框的“有效性”由目标框的值控制。特别地,当对应的框无效时,GIOU 会显式置为零。

#include <torch/extension.h>

#include <cuda.h>
#include <cuda_runtime.h>

namespace extension_cpp {

__global__ void giou_kernel(const float* boxes1,
                            const float* boxes2, 
                            float* giou, 
                            bool* mask) {
  int idx = blockIdx.x * blockDim.x + threadIdx.x;
  bool valid = boxes2[4*idx+3] != 0;
  mask[idx] = valid;

  const float epsilon = 1e-5;

  const float* box1 = &boxes1[idx * 4];
  const float* box2 = &boxes2[idx * 4];

  // Compute area of each box
  float area1 = (box1[2] - box1[0]) * (box1[3] - box1[1]);
  float area2 = (box2[2] - box2[0]) * (box2[3] - box2[1]);

  // Compute the intersection
  float left = max(box1[0], box2[0]);
  float top = max(box1[1], box2[1]);
  float right = min(box1[2], box2[2]);
  float bottom = min(box1[3], box2[3]);

  float inter_w = max(right - left, 0);
  float inter_h = max(bottom - top, 0);
  float inter_area = inter_w * inter_h;

  // Compute the union area
  float union_area = area1 + area2 - inter_area;

  // IoU
  float iou_val = inter_area / max(union_area, epsilon);

  // Compute the smallest enclosing box
  float enclose_left = min(box1[0], box2[0]);
  float enclose_top = min(box1[1], box2[1]);
  float enclose_right = max(box1[2], box2[2]);
  float enclose_bottom = max(box1[3], box2[3]);

  float enclose_w = max(enclose_right - enclose_left, 0);
  float enclose_h = max(enclose_bottom - enclose_top, 0);
  float enclose_area = enclose_w * enclose_h;

  float result = iou_val - (enclose_area-union_area)/max(enclose_area, epsilon);
  // Generalized IoU
  giou[idx] = result * valid;
}

at::Tensor giou_loss_cuda(const at::Tensor& a, const at::Tensor& b) {
  TORCH_CHECK(a.sizes() == b.sizes());
  TORCH_CHECK(a.dtype() == at::kFloat);
  TORCH_CHECK(b.dtype() == at::kFloat);
  TORCH_INTERNAL_ASSERT(a.device().type() == at::DeviceType::CUDA);
  TORCH_INTERNAL_ASSERT(b.device().type() == at::DeviceType::CUDA);
  int bs = a.sizes()[0];
  at::Tensor a_contig = a.contiguous();
  at::Tensor b_contig = b.contiguous();
  at::Tensor giou = torch::empty({a_contig.sizes()[0], a_contig.sizes()[1]},
                                  a_contig.options());
  at::Tensor mask = torch::empty({a_contig.sizes()[0], a_contig.sizes()[1]},
                                  a_contig.options().dtype(at::kBool));
  const float* a_ptr = a_contig.data_ptr<float>();
  const float* b_ptr = b_contig.data_ptr<float>();
  float* giou_ptr = giou.data_ptr<float>();
  bool* mask_ptr = mask.data_ptr<bool>();

  // Launch the kernel
  // The number of blocks is set according to the batch size.
  // Each block has 256 threads corresponding to the number of boxes per sample
  giou_kernel<<<bs, 256>>>(a_ptr, b_ptr, giou_ptr, mask_ptr);

  at::Tensor total_boxes = torch::clamp(mask.sum(), 1);
  torch::Tensor loss_sum = giou.sum();
  return loss_sum/total_boxes;
}

// Registers CUDA implementations for giou_loss
TORCH_LIBRARY_IMPL(extension_cpp, CUDA, m) {
  m.impl("giou_loss", &giou_loss_cuda);
}

}

要完成内核创建,我们需要添加适当的 C++ 和 Python 运算符定义(请参见 muladd.cppops.py

// Add the C++ definition
m.def(“giou_loss(Tensor a, Tensor b) -> Tensor”);
# define the Python operator
def giou_loss(a: Tensor, b: Tensor) -> Tensor:
    return torch.ops.extension_cpp.giou_loss.default(a, b)

要编译我们的内核,请在根目录下运行安装脚本(pip install .)。

以下代码块使用了我们新定义的 GIOU CUDA 内核:

def loss_with_kernel(pred, targets):
    pred = pred.to(torch.float32)
    targets = targets.to(torch.float32)
    import extension_cpp
    return extension_cpp.ops.giou_loss(pred, targets)

请注意显式转换为 torch.float32。这是一个相当昂贵的操作,通过增强我们的 CUDA 内核支持,可以轻松避免。我们将此留给读者作为练习 :)。

运行我们自定义内核的脚本结果如下所示。

-------------  ------------  ------------
         Name     CPU total  CPU time avg   
-------------  ------------  ------------
    copy data      56.901ms       5.690ms    
      forward        1.327s     132.704ms      
    calc loss       6.287ms     628.743us     
-------------  ------------  ------------
Self CPU time total: 6.907ms
Self CUDA time total: 1.380s

尽管我们的内核(以及我们在 CUDA 上的经验不足)非常简单,我们还是将损失函数的性能提高了大约 3 倍(628 微秒与 1.8 毫秒相比)。如上所述,这在不费太多力气的情况下还可以进一步改善。

第 2 步 — 条件执行

CUDA 提供的线程级控制使我们能够添加一个条件语句,避免在无效的边界框上进行计算:

__global__ void giou_kernel(const float* boxes1,
                            const float* boxes2,
                            float* giou,
                            bool* mask) {
  int idx = blockIdx.x * blockDim.x + threadIdx.x;
  bool valid = boxes2[4*idx+3] != 0;
  mask[idx] = valid;
  if (valid)
  {
    const float* box1 = &boxes1[idx * 4];
    const float* box2 = &boxes2[idx * 4];
    giou[idx] = compute_giou(box1, box2);
  }
  else
  {
    giou[idx] = 0;
  }
}

就我们的内核而言,它对运行时性能的影响可以忽略不计。其原因(推测)是我们的内核相对较小,以至于其运行时间与加载和实例化所需的时间相比,可以忽略不计。我们的条件执行的影响可能只有在更大的内核中才会显现。(作为一个练习,评估内核大小对影响的函数,可以通过让我们的 GIOU 输出依赖于我们为固定步数运行的for循环来进行。这一点我们也留给你作为练习:)。)同样重要的是要考虑在 CUDA 的SIMT 架构上,条件执行流的表现,特别是当属于同一warp的线程发生分歧时,可能会带来的性能损失。

-------------  ------------  ------------
         Name     CPU total  CPU time avg
-------------  ------------  ------------
    copy data      57.008ms       5.701ms
      forward        1.318s     131.850ms
    calc loss       6.234ms     623.426us
-------------  ------------  ------------
Self CPU time total: 7.139ms
Self CUDA time total: 1.371s

结果与下一步

我们在下表中总结了实验结果。

损失运行时的平均总结(按作者)

重要的是,我们的工作并未结束。诚然,在我们分享的示例中,我们采取了一些捷径:

  1. 为了在训练中使用我们的自定义内核,我们需要实现反向传播。通常,这可能比正向传播稍微复杂一些。

  2. 我们已将张量类型固定为 float32,并将张量形状固定为每个样本 256 个框。理想情况下,我们希望有一个更健壮的解决方案,支持不同的输入类型和形状。

  3. 我们将实验限制在了一种 GPU 类型上。实际上,我们希望我们的实现能够支持(并且在)多种 GPU 上进行测试。

  4. 我们完全忽视了内核优化的机会——其中一些可能需要比我们在此所展示的更高深的 CUDA 专业知识。

总结

在这篇文章中,我们展示了自定义 CUDA 内核在 AI/ML 应用程序运行时性能中的潜力。我们特别尝试利用 CUDA 提供的低级控制,介绍条件流,以限制动态形状输入情况下冗余算术操作的数量。尽管通过融合多个内核操作带来的性能提升显著,但我们发现内核的大小太小,无法从条件执行流中获益。

在我们许多的文章中,我们一直强调拥有多种工具和技术来优化机器学习(ML)并降低其成本的重要性。自定义内核开发是我们可用的最强大技术之一。然而,对于许多 AI/ML 工程师来说,它也是最具挑战性的技术之一。我们希望已经成功地说服你,这个机会对于任何 ML 开发者来说都是触手可及的,并且它并不需要对 CUDA 有深度的专业化理解。

近年来,引入了新的框架,旨在使 AI/ML 开发人员更容易进行自定义内核开发和优化。其中最受欢迎的框架之一是Triton。在我们的下一篇文章中,我们将继续探讨自定义内核开发的主题,评估开发 Triton 内核的能力和潜在影响。

指控性 AI:广泛滥用 AI 技术如何伤害学生

原文:towardsdatascience.com/accusatory-ai-how-misuse-of-technology-is-harming-students-56ec50105fe5?source=collection_archive---------6-----------------------#2024-12-09

观点

当 AI 指控学生利用 AI 作弊时,应该怎么做?

James F. O'BrienTowards Data Science James F. O'Brien

·发表于 Towards Data Science ·6 分钟阅读·2024 年 12 月 9 日

--

检测由 AI 系统生成的内容的反作弊工具正在被广泛教育工作者使用,以检测和惩罚书面和编程作业中的作弊行为。然而,这些 AI 检测系统似乎效果并不好,不应该用于惩罚学生。即使是最好的系统,也会有某种非零的误报率,这导致一些真实的学生在实际独立完成作业时,却被判定为作弊。AI 检测器被广泛使用,而被错误指控的学生涵盖了从小学研究生院的学生群体。

在这些错误指控的案例中,造成有害不公的责任可能并不在于提供工具的公司。如果你查看他们的文档,通常会看到类似以下内容:

“AI 生成内容的性质在不断变化。因此,这些结果不应被用来惩罚学生。……总会有一些边缘案例,在这些案例中,AI 被误判为人类,或者人类被误判为 AI。”

引用自 GPTZero 的 FAQ.

换句话说,开发这些服务的人知道它们并不完美。负责任的公司,比如上面引用的公司,明确承认这一点,并清楚声明他们的检测工具不应被用来惩罚,而是应当用于在何时与学生建立建设性联系上有所帮助。仅仅因为检测工具触发了警报就给作业判不及格,实际上是判分者的疏忽懒惰。

如果你面临与 AI 工具相关的作弊指控,或者正在提出这样的指控,请考虑以下关键问题:

  • 使用了什么检测工具,工具具体宣称能做什么?如果答案类似于上面引用的文本,明确表示结果不用于惩罚学生,那么判分者显然在误用该工具。

  • 在你的具体情况下,是否是判分者需要承担证明责任,来为处罚提供依据?如果是这样,那么他们应该能够提供一些证据,支持该工具有效的声明。任何人都可以建立一个网站,简单地使用 LLM 对输入进行表面上的评估,但如果这个工具要作为反对学生的证据,那么就需要对该工具进行正式的评估,以证明其可靠性。此外,这个评估需要是科学有效的,并由一个无偏的第三方进行。

  • 在你的具体情况下,学生是否有权审查用于指控他们的证据和方法?如果有,那么指控可能无效,因为 AI 检测软件通常不允许提供所需的透明度

  • 学生或家长是否为英语作为第二语言的人?如果是,那么案件中可能存在歧视的因素。以英语为第二语言的人通常会直接翻译他们母语中的习语或其他常见短语,导致最终的文本出现不寻常的短语,这些短语已知会错误触发这些检测器。

  • 学生是否属于一个使用自己习语或英语方言的少数群体?像第二语言使用者一样,这些不太常见的短语可能会错误触发 AI 检测器

  • 被指控的学生是否是神经多样性学生?如果是,那么这可能是案件中的另一个歧视方面。比如,自闭症患者可能使用一些对他们来说完全合乎逻辑的表达方式,但其他人可能觉得这些表达方式很奇怪。这些表达方式并没有问题,但它们不寻常,且AI 检测器可能会因其而被触发

  • 被指控的作品是否非常简短?AI 检测器的关键理念是,它们会寻找不常见的词语组合和/或代码指令,这些组合人类很少使用,但生成性 AI 却常常使用。在较长的作品中,可能会发现很多这样的组合,从而统计上人类偶然使用这些组合的可能性就会很小。然而,作品越简短,偶然使用的可能性就越高。

  • 有什么证据表明学生完成了这项工作?如果问题中的作业超过几段文字或几行代码,那么很可能有历史记录显示工作的逐步发展。Google Docs、Google Drive 和 iCloud Pages 都会保留更改历史记录。大多数计算机也会将版本历史记录作为备份系统的一部分进行保存,例如苹果的 Time Machine。也许学生将不同版本的草稿通过电子邮件发送给合作伙伴、家长,甚至是老师,而这些邮件就形成了逐步完成工作的记录。如果学生使用 GitHub 进行编码,那么就有清晰的提交历史。逐步发展的清晰历史展示了学生是如何随着时间完成这项工作的。

为了明确,我认为这些 AI 检测工具在教育中有其应用场所,但正如这些网站自己明确声明的那样,它们的作用不是抓捕作弊者并惩罚学生。事实上,许多这些网站提供了如何建设性地处理疑似作弊的指导。这些 AI 检测器是工具,像任何强大的工具一样,如果正确使用,它们可以发挥巨大作用,而如果使用不当,则可能造成很大的伤害。

如果你或你的孩子被不公正地指控使用 AI 代写并因此受到惩罚,那么我建议你把这篇文章以及我提供的链接文章给老师/教授看。如果指控者不肯松口,那么我建议你联系律师,了解是否可以对老师和学校/学区提起诉讼。

尽管建议咨询律师,但我并不是反对教育工作者,认为优秀的教师不应该因为成绩问题而被起诉。然而,滥用工具并伤害学生的教师并不是好老师。当然,一位有良好意图的教育者可能会因为没有意识到工具的局限性而误用它,但当获得新信息时,应该重新评估。

“宁可让 100 个有罪的人逃脱,也不让一个无辜的人受苦。” — 本杰明·富兰克林,1785 年

作为一名教授,我也曾在课堂上与作弊问题作斗争。没有简单的解决方案,使用人工智能检测器来让学生失败不仅无效,而且不负责任。我们是教育者,不是警察或检察官。我们的角色应该是支持学生,而不是随意惩罚他们。这包括作弊者,尽管他们可能有不同的看法。作弊并不是对教育者的个人侮辱,也不是对其他学生的攻击。在课程结束时,唯一真正受害的人是作弊者自己,他们浪费了时间和金钱,却没有获得任何真正的知识或经验。(按曲线评分,或者以其他方式让学生相互竞争,有许多不好的理由,在我看来,应该避免。)

最后,人工智能系统已经存在,并且像计算器和计算机一样,它们将彻底改变人们在不久的将来如何工作。教育需要发展,并教导学生如何负责任且有效地使用人工智能。我自己写了这篇文章的初稿,但随后我请了一位大型语言模型(LLM)阅读它,给我反馈并提出建议。我可能也能在没有 LLM 的情况下得到类似的结果,但那样我可能会请朋友帮忙阅读并提出建议,那样会花费更长时间。与 LLM 合作的过程对我来说并不独特,我的同事们也广泛使用这种方式。或许,与其追查人工智能的使用,我们应该教会学生如何使用它。当然,学生仍然需要学习基础知识,但他们也需要学会如何使用这些强大的工具。如果他们不学会,那么那些会使用人工智能的同学将会有巨大的优势。

关于我: 詹姆斯·F·奥布莱恩 是加利福尼亚大学伯克利分校的计算机科学教授。他的研究兴趣包括计算机图形学、计算机动画、物理系统模拟、人类感知、渲染、图像合成、机器学习、虚拟现实、数字隐私以及图像和视频的法医分析。

如果你觉得这个有趣,那么这里是常用的 关注 订阅 链接。你还可以在 Instagram LinkedIn* 和* 加州大学伯克利分校找到我。

免责声明:本文中表达的任何观点仅代表作者作为个人的立场。本文中的任何内容都不应被解读为与作者在任何机构的职业身份相关的声明。

本文及所有嵌入的图片版权归作者所有,版权所有 2024 年。本文章由人类编写,同时使用了 LLM(Llama 3.2 3B)以及其他人类进行校对和编辑建议。编辑用图由 AI(Adobe Firefly)生成,之后由人类使用 Photoshop 进行了大量编辑。

使用ClassificationThresholdTuner实现更好的分类结果

原文:towardsdatascience.com/achieve-better-classification-results-with-classificationthresholdtuner-39c5d454637e?source=collection_archive---------0-----------------------#2024-09-07

一款用于调整和可视化二分类和多分类问题阈值选择的 Python 工具

W Brett KennedyTowards Data Science W Brett Kennedy

·发布于 Towards Data Science ·阅读时长 30 分钟·2024 年 9 月 7 日

--

在分类问题中调整阈值(即调整用于决定预测属于某一类别还是另一类别的概率临界值)是一个有时会被忽略的步骤,但其实它非常简单,而且可以显著提高模型的质量。这是一个应该在大多数分类问题中执行的步骤(根据我们希望优化的内容,下面会描述一些例外情况)。

在本文中,我们将更深入地了解在进行此操作时实际发生了什么——特别是对于多分类问题,这可能有些微妙。我们还将介绍一款由我自己编写的开源工具——ClassificationThresholdTuner,它自动化并向用户描述了这一过程。

由于在分类问题中调整阈值任务的普遍性,以及这个过程通常在不同项目之间的相似性,我已经能够在许多项目中使用这个工具。它消除了我为大多数分类问题添加的许多(几乎是重复的)代码,并提供了更多关于调整阈值的信息,这些是我以前无法得到的。

尽管ClassificationThresholdTuner是一个有用的工具,但你可能会发现本文中描述的工具背后的思想更为相关——它们足够简单,可以在你的分类项目中按需复制。

简而言之,ClassificationThesholdTuner是一个用于优化分类问题中阈值设置的工具,并清晰展示不同阈值的效果。与大多数其他可用选项(以及我们最有可能自行开发的优化阈值的代码)相比,它有两个主要优势:

  1. 它提供了可视化工具,帮助数据科学家理解使用已发现的最优阈值的含义,以及可能选择的替代阈值。当向其他利益相关者展示建模决策时,这也非常有价值,例如在需要找到假阳性和假阴性之间的良好平衡时。通常,这需要业务理解和数据建模知识,并且对阈值选择有清晰全面的理解,可以促进讨论并决定最佳平衡。

  2. 它支持多类分类,这是机器学习中的常见问题类型,但与二分类相比,调整阈值更为复杂(例如,它需要识别多个阈值)。不幸的是,优化多类分类所使用的阈值并没有得到其他同类工具的良好支持。

虽然支持多类分类是ClassificationThesholdTuner的一个重要特性,但由于二分类更易于理解,因此我们将从描述二分类开始。

分类中使用的阈值是什么?

几乎所有现代分类器(包括 scikit-learn、CatBoost、LGBM、XGBoost 以及大多数其他分类器)都支持同时生成预测和概率。

例如,如果我们创建一个二分类器来预测哪些客户将在明年流失,那么对于每个客户,我们通常可以生成一个二分类预测(对于每个客户,预测为“是”或“否”),或者可以为每个客户生成一个概率(例如,一个客户可能被估计在该时间范围内流失的概率为 0.862)。

给定一个能够生成概率的分类器,即使我们请求二分类预测,它通常在后台仍然会为每条记录生成一个概率。然后,它会将这些概率转换为类预测。

默认情况下,二分类器将在正类的预测概率大于或等于 0.5 时预测为正类,而在预测概率低于 0.5 时预测为负类。在这个例子中(预测流失),默认情况下,如果预测流失的概率≥0.5,它会预测为“是”,否则预测为“否”。

然而,这可能不是理想的行为,通常一个非 0.5 的阈值可能会更好用,可能是稍微低一点或稍微高一点,有时甚至可能是与 0.5 差距较大的阈值。这取决于数据、构建的分类器以及假阳性与假阴性之间的相对重要性。

为了创建一个强大的模型(包括平衡好假阳性和假阴性),我们通常希望优化某个指标,如 F1 分数、F2 分数(或 f-beta 系列中的其他指标)、马修斯相关系数(MCC)、卡帕分数或其他指标。如果是这样,优化这些指标的一个关键部分是适当地设置阈值,这通常会将阈值设置为非 0.5 的值。我们很快会描述这个过程是如何工作的。

这是一个关键点。通常并不容易立即清楚地知道最佳的阈值设置在哪里,但我们通常可以确定最适合优化的指标。一个例子是使用 F2 或 F3 分数,在这种情况下我们希望更强调正类的召回率。

Scikit-learn 对阈值调优的支持

Scikit-learn 在其调整分类预测决策阈值页面中提供了关于阈值调优的良好背景信息。Scikit-learn 还提供了两个工具:FixedThresholdClassifierTunedThresholdClassifierCV(在 scikit-learn 1.5 版本中引入),用于帮助调节阈值。它们的工作原理与 ClassificationThresholdTuner 非常相似。

Scikit-learn 的工具可以视为便捷的方法,因为它们并不是严格必要的;正如所示,调优在任何情况下都相对简单(至少对于支持二分类的这些工具来说是这样的)。但有这些工具是很方便的——调用它们比自己编写代码要容易得多。

ClassificationThresholdTuner 作为这些工具的替代方案被创建,但当 Scikit-learn 的工具效果良好时,它们也是非常好的选择。特别是在你有一个二分类问题,并且不需要对找到的阈值进行任何解释或描述的情况下,Scikit-learn 的工具可以完美地工作,甚至可能略微更方便,因为它们让我们跳过了安装 ClassificationThresholdTuner 这个小步骤。

在需要对找到的阈值(包括有关阈值替代值的一些背景信息)进行解释,或者你有多分类问题时,ClassificationThresholdTuner 可能更有价值。

如前所述,有时可能是本文中描述的思想最为宝贵,而不是具体的工具,您可能最好开发自己的代码——也许是沿着类似的思路,但在执行时间方面进行优化,以更高效地处理您的数据,可能能够支持更多需要优化的其他指标,或者可能提供其他图表和阈值调优过程的描述,从而为您的项目提供相关信息。

二分类中的阈值

对于大多数 scikit-learn 分类器,以及 CatBoost、XGBoost 和 LGBM,可以通过调用 predict_proba() 来返回每个记录的概率。该函数输出每个记录每个类别的概率。在二分类问题中,它们将为每个记录输出两个概率,例如:

[[0.6, 0.4], 
 [0.3, 0.7], 
 [0.1, 0.9],
  …
]

对于每一对概率,我们可以将第一个视为负类的概率,第二个视为正类的概率。

然而,在二分类问题中,一个类别的概率仅仅是另一个类别概率的 1.0 减去,因此严格来说,只需要一个类别的概率。事实上,在处理二分类问题中的类别概率时,我们通常只使用正类的概率,因此可以使用如下的数组:[0.4, 0.7, 0.9, …]。

在二分类中,阈值容易理解,因为它们可以简单地视为需要的最小预测概率,以便正类被预测为正类(在流失的例子中,就是预测客户流失)。如果我们设定阈值为 0.6,那么就可以轻松地将上面的概率数组转换为预测结果,在这个例子中就是:[否, 是, 是, …]。

通过使用不同的阈值,我们可以使模型更积极或更保守地预测正类。如果使用一个相对较低的阈值,比如 0.3,那么即使只有中等概率正确,模型也会预测为正类。与使用 0.5 作为阈值相比,更多的正类预测将会被做出,从而增加真正的正类和假正类,同时减少真正的负类和假负类。

在客户流失的情况下,如果我们希望重点关注捕捉大多数流失案例,即使这样做时,我们也预测许多客户会流失,但实际上他们并不会。也就是说,当漏报(漏掉流失)比误报(错误地预测流失)更为重要时,较低的阈值是有用的。

将阈值设置得更高,例如 0.8,将产生相反的效果:预测流失的客户会减少,但在那些被预测为流失的客户中,很多很可能真的会流失。我们会增加假阴性(漏掉一些实际上会流失的客户),但减少假阳性。这在只能跟进少量潜在流失客户并且希望只标记那些最可能流失的客户时是合适的。

在设定阈值时,几乎总是涉及到强烈的业务因素。像 ClassificationThresholdTuner 这样的工具可以使这些决策更加清晰,否则通常没有明显的阈值选择点。例如,单纯根据直觉选择阈值(可能认为 0.7 差不多合适)通常不会得到最优的结果,且一般不比直接使用默认值 0.5 更好。

设置阈值可能有点不直观:上下调整阈值一点,往往会比预期更大程度地影响模型。有时,例如,增加阈值可以大幅减少假阳性,而对假阴性只有小的影响;在其他情况下,情况可能正好相反。使用接收器操作特征曲线(ROC)是一种很好的方式,能够帮助可视化这些权衡。我们将在下面看到一些例子。

最终,我们会设置一个阈值,以优化某些指标(例如 F1 得分)。ClassificationThresholdTuner 只是一个自动化并描述该过程的工具。

AUROC 和 F1 得分

通常,我们可以将用于分类的度量指标分为三种主要类型:

  • 那些评估预测概率排名情况的指标,例如:接收器操作特征曲线下的面积(AUROC)、精确度-召回率曲线下的面积(AUPRC)。

  • 那些评估预测概率校准程度的指标,例如:Brier 得分、对数损失。

  • 那些评估预测标签正确性的指标,例如:F1 得分、F2 得分、MCC、Kappa 得分、平衡准确率。

这里列出的前两类度量指标基于预测的概率,而最后一类则基于预测的标签。

尽管每类指标中有很多不同的度量方式,但为了简便起见,我们暂时只考虑其中两种较为常见的度量方式,即接收器操作特征曲线下的面积(AUROC)和 F1 得分。

这两类指标之间有一个有趣的关系(就像 AUROC 与基于预测标签的其他指标之间的关系一样),ClassificationThresholdTuner 利用这一关系来调节和解释最佳阈值。

ClassificationThresholdTuner 背后的理念是,在模型已调优到具有强 AUROC 之后,利用这一点来优化其他指标——这些指标基于预测标签,例如 F1 得分。

基于预测标签的度量指标

很多时候,衡量预测标签正确性的指标是分类中最相关的。这种情况发生在模型用于为记录分配预测标签时,相关的是正确的正例、负例、假阳性和假阴性的数量。也就是说,如果下游使用的是预测标签,那么一旦标签被分配,基础预测概率的具体数值就不再重要,重要的只是最终的标签预测。

例如,如果模型为客户分配“是”或“否”标签,以表示他们是否预计在下一年会流失,而那些预测为“是”的客户接受一些处理,而那些预测为“否”的客户不接受,那么最相关的是这些标签的正确性,而不是最终预测概率的排序或校准程度(这些类预测是基于这些概率的)。不过,预测概率的排序如何,正如我们所看到的,对于准确分配预测标签是相关的。

这并不适用于每个项目:通常,像 AUROC 或 AUPRC 这样的指标,衡量预测概率排序如何,才是最相关的;而像 Brier Score 和 Log Loss 这样的指标,衡量预测概率的准确性,通常也最为重要。

调整阈值不会影响这些指标,而在这些指标最为相关的情况下,调整阈值没有必要。但对于本文来说,我们将考虑那些我们希望优化的基于预测标签的指标,例如 F1 得分或其他指标。

ClassificationThresholdTuner 从预测的概率开始(其质量可以通过 AUROC 来评估),然后致力于优化指定的指标(其中指定的指标基于预测标签)。

基于预测标签正确性的指标,都是通过不同方式从混淆矩阵中计算得出的。混淆矩阵又基于选择的阈值,如果使用低阈值或高阈值,其结果可能会大不相同。

调整阈值

AUROC 指标顾名思义是基于 ROC 曲线的,ROC 曲线展示了真正例率与假正例率的关系。ROC 曲线并不假设使用任何特定的阈值,但曲线上的每一个点都对应一个特定的阈值。

在下图中,蓝色曲线是 ROC 曲线。该曲线下方的面积(AUROC)衡量了模型的一般强度,基于所有潜在阈值的平均值。它衡量了概率的排序效果:如果概率排序良好,则分配给正类的高预测概率的记录,实际上更有可能属于正类。

例如,AUROC 为 0.95 意味着随机正样本有 95%的概率被排在随机负样本之前。

首先,拥有一个强大的 AUROC 模型非常重要 — 这是模型调优过程的工作(该过程可能会优化其他指标)。这在我们开始调整阈值之前完成,完成后,重要的是要有良好排名的概率,这意味着 AUROC 得分很高。

然后,如果项目需要为所有记录做出类别预测,则必须选择一个阈值(尽管可以使用默认值 0.5,但可能会得到次优结果),这相当于在 ROC 曲线上选择一个点。

上图展示了 ROC 曲线上的两个点。对于每个点,都画了垂直线和水平线,分别指示关联的真正例率和假正例率。

给定 ROC 曲线,当我们向左和向下移动时,表示使用更高的阈值(例如,从绿色线到红色线)。预测为正的记录会减少,因此真正例和假正例都会减少。

当我们向右和向上移动时(例如,从红线到绿线),表示使用更低的阈值。更多的记录会被预测为正例,因此真正例和假正例都会增加。

也就是说,在这里的图中,红线和绿线代表两个可能的阈值。从绿线移动到红线,我们看到真正例率略有下降,但假正例率大幅下降,这使得这个阈值选择比绿线所在的阈值更有可能是更好的选择。但这并不一定 — 我们还需要考虑假正例和假负例的相对成本。

但需要注意的是,从一个阈值移动到另一个阈值通常会比真正例率更大或更小地调整假正例率。

以下展示了一组给定 ROC 曲线的阈值。我们可以看到,从一个阈值移动到另一个阈值时,真正例率和假正例率的变化程度可能会大不相同。

这就是调整阈值背后的主要思路:通常可以在一个方面获得较大增益,同时在另一个方面仅遭受小幅损失。

可以查看 ROC 曲线,观察上下调整阈值的效果。基于这一点,我们在一定程度上可以凭目测选择一个看起来最佳平衡真正例和假正例的点(这也有效地平衡了假正例和假负例)。在某种意义上,这就是 ClassificationThesholdTuner 的作用,但它以一种有原则的方式进行,从而优化某一指定的指标(如 F1 分数)。

将阈值移动到 ROC 曲线上的不同点会生成不同的混淆矩阵,然后可以将其转换为评估指标(如 F1 分数、F2 分数、MCC 等)。然后,我们可以选择优化该指标的点。

只要模型经过训练,能够获得强大的 AUROC 值,我们通常可以找到一个好的阈值来获得高 F1 得分(或其他类似的度量)。

在这个 ROC 图中,模型非常准确,AUROC 为 0.98。因此,可以选择一个能产生高 F1 得分的阈值,尽管仍然需要选择一个好的阈值,且最优阈值可能并不一定是 0.5。

排名良好的模型不一定是良好校准的,但这并不是必须的:只要正类记录的预测概率普遍高于负类记录的预测概率,我们就能找到一个合适的阈值,将预测为正类的记录与预测为负类的记录分开。

从另一个角度看,我们可以通过两个直方图来查看二元分类问题中概率的分布,如下所示(实际上使用的是 KDE 图)。蓝色曲线表示负类的概率分布,橙色曲线表示正类的概率分布。模型可能没有很好地校准:正类的概率始终远低于 1.0。但是,它们排名良好:正类的概率通常高于负类的概率,这意味着模型的 AUROC 值较高,并且如果使用适当的阈值(在这种情况下,可能是 0.25 或 0.3),模型能够很好地分配标签。尽管分布之间存在重叠,但无法实现完美的记录标签系统,F1 得分也永远不能达到 1.0。

即使 AUROC 得分很高,也有可能 F1 得分很低:这通常发生在阈值选择不当时。例如,ROC 曲线可能像上面所示那样贴近轴线——非常低或非常高的阈值可能表现不好。数据不平衡时也可能出现 ROC 曲线紧贴 y 轴的情况。

对于这里展示的直方图,尽管模型校准良好并且 AUROC 得分很高,但如果选择不合适的阈值(如 0.5 或 0.6,这会导致所有预测都为负类),则会导致 F1 得分非常低。

即使 AUROC 较低,也可能出现高 F1 得分的情况。通过选择一个特别好的阈值(在大多数阈值下表现都很差),也可以实现这一点。

此外,虽然不常见,但可能会有不对称的 ROC 曲线,这可能会大大影响最合适的阈值位置。

使用 ClassificationThresholdTuner 进行二元分类示例

这是从一个notebook中提取的,可以在 GitHub 网站上查看完整的代码。我们将在这里讨论主要的内容。对于这个例子,我们首先生成一个测试数据集。

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from threshold_tuner import ClassificationThresholdTuner

NUM_ROWS = 100_000

def generate_data():
    num_rows_per_class = NUM_ROWS // 2
    np.random.seed(0)
    d = pd.DataFrame(
      {"Y": ['A']*num_rows_per_class + ['B']*num_rows_per_class,
       "Pred_Proba": 
          np.random.normal(0.7, 0.3, num_rows_per_class).tolist() + \
          np.random.normal(1.4, 0.3, num_rows_per_class).tolist()
      })
    return d, ['A', 'B']

d, target_classes = generate_data()

这里为了简便,我们不生成原始数据或产生预测概率的分类器,而是仅使用一个包含真实标签和预测概率的测试数据集,因为ClassificationThresholdTuner正是处理这些内容,而这些内容足以选择最佳的阈值。

实际上,笔记本中还有一段代码用于对概率进行缩放,确保它们在 0.0 到 1.0 之间,但在这里,我们将假设概率已经很好地进行了缩放。

然后我们可以使用 0.5 的阈值来设置Pred列:

d['Pred'] = np.where(d["Pred_Proba"] > 0.50, "B", "A")

这模拟了分类器通常的做法,即简单地使用 0.5 作为阈值。这是我们将尝试超越的基准。

然后我们创建一个ClassificationThresholdTuner对象,并使用它来开始,仅仅是为了查看当前预测的强度,调用其 API 之一print_stats_lables()

tuner = ClassificationThresholdTuner()

tuner.print_stats_labels(
    y_true=d["Y"], 
    target_classes=target_classes,
    y_pred=d["Pred"])

这会显示两个类别的精确度、召回率和 F1 分数(以及这些分数的宏平均),并呈现混淆矩阵。

这个 API 假定标签已经被预测;如果只有概率而没有标签,则无法使用此方法,尽管我们总是可以像这个例子一样选择一个阈值,并根据这个阈值设置标签。

我们还可以调用print_stats_proba()方法,它也会展示一些指标,在这个例子中与预测概率相关。它显示:Brier 分数、AUROC 和几个图表。图表需要一个阈值,若未指定,则使用 0.5,如本例所示:

tuner.print_stats_proba(
    y_true=d["Y"], 
    target_classes=target_classes, 
    y_pred_proba=d["Pred_Proba"])

这显示了 0.5 的阈值效果。它展示了 ROC 曲线,ROC 曲线本身不需要阈值,但会在曲线上标出该阈值。然后,它展示了数据如何根据阈值被分成两个预测类别,首先以直方图显示,其次以群体图显示。这里有两个类别,类别 A 为绿色,类别 B(在这个例子中为正类)为蓝色。

在群体图中,任何被错误分类的记录都显示为红色。这些是那些真实类别为 A 但 B 的预测概率高于阈值(因此模型会预测为 B),以及那些真实类别为 B 但 B 的预测概率低于阈值(因此模型会预测为 A)的记录。

我们可以使用plot_by_threshold()来检查不同阈值的效果:

tuner.plot_by_threshold(
    y_true=d['Y'], 
    target_classes=target_classes,
    y_pred_proba=d["Pred_Proba"])

在这个例子中,我们使用默认的潜在阈值集合:0.1、0.2、0.3,……直到 0.9。对于每个阈值,模型会将预测概率超过该阈值的记录预测为正类,将低于该阈值的记录预测为负类。错误分类的记录显示为红色。

为了节省本文的篇幅,这张图片仅展示了三个潜在阈值:0.2、0.3 和 0.4。对于每个阈值,我们可以看到:该阈值在 ROC 曲线上的位置、它导致的数据划分,以及由此产生的混淆矩阵(以及与该混淆矩阵相关的 F1 宏评分)。

我们可以看到,将阈值设为 0.2 时几乎所有的预测结果都是 B(正类)——几乎所有 A 类的记录都被误分类,因此用红色标出。随着阈值的增加,更多记录被预测为 A,B 类的预测减少(尽管在阈值为 0.4 时,大多数真实 B 类记录被正确预测为 B;直到阈值接近 0.8 时,几乎所有真实的 B 类记录都被错误预测为 A:很少有记录的预测概率超过 0.8)。

对从 0.1 到 0.9 的九个可能值进行检查,可以很好地概览潜在的阈值范围,但调用此函数显示一个更窄且更现实的可能值范围可能更有用,例如:

tuner.plot_by_threshold(
    y_true=d['Y'], 
    target_classes=target_classes,
    y_pred_proba=d["Pred_Proba"], 
    start=0.50, end=0.55, num_steps=6)

这将展示从 0.50 到 0.55 的每个阈值。展示其中的前两个:

这个 API 有助于展示不同阈值的影响。

我们还可以查看这个调用describe_slices(),它描述了潜在阈值对之间的数据(即数据的切片),以便更清楚地看到将阈值从一个潜在位置移动到下一个位置时的具体变化(我们可以看到每个真实类别会有多少被重新分类)。

tuner.describe_slices(    
    y_true=d['Y'], 
    target_classes=target_classes,
    y_pred_proba=d["Pred_Proba"], 
    start=0.3, end=0.7, num_slices=5)

这将以视觉和表格格式显示每个切片:

这里的切片相对较薄,因此我们可以看到两个图,一个展示了它们在完整概率范围内的上下文(左图),另一个则是放大的(右图)。

我们可以看到,例如,将阈值从 0.38 调整到 0.46 时,我们会重新分类第 3 个切片中的点,该切片中有 17,529 个真实的 A 类实例和 1,464 个真实的 B 类实例。这在最右侧的散点图和表格中都有明显体现(在散点图中,第 3 个切片内的绿色点比蓝色点多得多)。

这个 API 也可以用于更窄且更现实的潜在阈值范围:

tuner.describe_slices(    
    y_true=d['Y'], 
    target_classes=target_classes,
    y_pred_proba=d["Pred_Proba"], 
    start=0.4, end=0.6, num_slices=10)

这将生成:

调用这些(或其他有用的 API,如print_stats_table(),这里为了简洁略过,但在 github 页面和示例笔记本中有描述)之后,我们可以对移动阈值的效果有所了解。

然后我们可以进入主要任务,使用tune_threshold() API 寻找最优阈值。对于一些项目来说,这可能是唯一会被调用的 API,或者它可能首先被调用,随后使用上述 API 来提供关于发现的最优阈值的上下文。

在此示例中,我们优化 F1 宏评分,尽管任何由 scikit-learn 支持并基于类别标签的指标都是可以的。某些指标需要额外的参数,这些参数也可以在此传递。在此示例中,scikit-learn 的 f1_score()需要‘average’参数,该参数作为 tune_threshold()的参数传递。

from sklearn.metrics import f1_score

best_threshold = tuner.tune_threshold(
    y_true=d['Y'], 
    target_classes=target_classes,
    y_pred_proba=d["Pred_Proba"],
    metric=f1_score,
    average='macro',
    higher_is_better=True,
    max_iterations=5
)
best_threshold

这将可选地显示一组图表,演示该方法在五次迭代过程中(在此示例中,max_iterations 指定为 5)如何逐步收敛到优化指定指标的阈值。

第一轮考虑了 0.0 到 1.0 之间的所有潜在阈值范围。然后它将范围缩小到 0.5 到 0.6,并在下一轮中更详细地检查该范围,依此类推。最后选择了 0.51991 作为阈值。

之后,我们可以再次调用 print_stats_labels(),它会显示:

我们可以看到,在此示例中,宏 F1 得分从 0.875 提高到 0.881。在这种情况下,提升很小,但几乎是免费的。在其他情况下,提升可能更小或更大,有时甚至更大。它也从不会适得其反;最坏的情况是,找到的最佳阈值仍然是默认值 0.5000。

多类分类中的阈值

如所示,多类分类要比二类分类稍微复杂。在二类分类的情况下,选择一个单一的阈值,而在多类分类中,ClassificationThesholdTuner 会为每个类别识别一个最佳阈值。

与二类情况不同,我们需要指定一个类别作为默认类别。通过一个示例可以更清楚地了解为什么需要这样做。

在许多情况下,拥有一个默认类别是相当自然的。例如,如果目标列代表各种可能的医疗状况,默认类别可能是“无问题”,其他类别则分别与特定的疾病相关。对于这些疾病中的每一种,我们都会设定一个最低预测概率,要求该条件的预测需要达到此概率。

或者,如果数据表示网络日志,并且目标列涉及各种入侵类型,那么默认类别可能是“正常行为”,其他类别则分别与特定的网络攻击相关。

在网络攻击的示例中,我们可能有一个数据集,包含四个不同的目标值,目标列包含类别:“正常行为”、“缓冲区溢出”、“端口扫描”和“网络钓鱼”。对于我们运行预测的任何记录,都会得到每个类别的概率,这些概率的总和为 1.0。例如,我们可能会得到:[0.3, 0.4, 0.1, 0.2](按照上述顺序,每个类别的概率)。

通常情况下,我们会预测“缓冲区溢出”,因为它具有最高的概率 0.4。然而,我们可以设置一个阈值,以修改这一行为,这将影响该类别的假阴性和假阳性率。

我们可以指定,例如:默认类是“正常行为”;“缓冲区溢出”的阈值是 0.5;“端口扫描”的阈值是 0.55;“钓鱼攻击”的阈值是 0.45。按照惯例,默认类的阈值设为 0.0,因为它实际上不使用阈值。因此,这里阈值的集合将是:0.0,0.5,0.55,0.45。

然后,为了对任何给定记录做出预测,我们仅考虑那些概率超过相关阈值的类。在这个示例中(预测值为[0.3, 0.4, 0.1, 0.2]),没有任何概率超过其阈值,因此预测为默认类“正常行为”。

如果预测概率为:[0.1, 0.6, 0.2, 0.1],那么我们将预测为“缓冲区溢出”:概率(0.6)是最高的预测,并且超过其阈值(0.5)。

如果预测概率为:[0.1, 0.2, 0.7, 0.0],那么我们将预测为“端口扫描”:概率(0.7)超过其阈值(0.55),这是最高的预测。

这意味着:如果一个或多个类的预测概率超过其阈值,我们将选择其中预测概率最高的类。如果没有类超过其阈值,则选择默认类。而且,如果默认类的预测概率最高,那么将预测为默认类。

因此,需要一个默认类来覆盖当没有任何预测值超过该类阈值的情况。

如果预测值为:[0.1, 0.3, 0.4, 0.2],阈值为:[0.0, 0.55, 0.5, 0.45],另一种看法是:通常会预测第三类:它具有最高的预测概率(0.4)。但是,如果该类的阈值是 0.5,那么 0.4 的预测值不够高,因此我们选择下一个最高的预测值,即第二类,预测概率为 0.3。这低于其阈值,所以我们继续选择下一个最高的预测值,即第四类,预测概率为 0.2。它也低于该目标类的阈值。在这里,我们所有类的预测值都相对较高,但都不够高,因此使用默认类。

这也突出了为何使用 0.0 作为默认类阈值是方便的——当检查默认类的预测时,我们无需考虑其预测是否低于或超过该类的阈值;我们总是可以做出默认类的预测。

实际上,原则上也可以有更复杂的策略——不仅使用单一的默认类,而是根据不同的条件选择多个类。但这些超出了本文的范围,通常是多余的,并且当前版本的 ClassificationThresholdTuner 不支持这些策略。在本文的其余部分,我们将假设指定了单一的默认类。

使用 ClassificationThresholdTuner 进行多类分类的示例

再次,我们将从创建测试数据开始(使用示例笔记本中提供的一个测试数据集,进行多类分类),在这种情况下,有三个目标类别,而不仅仅是两个:

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from threshold_tuner import ClassificationThresholdTuner

NUM_ROWS = 10_000

def generate_data():
    num_rows_for_default = int(NUM_ROWS * 0.9) 
    num_rows_per_class = (NUM_ROWS - num_rows_for_default) // 2
    np.random.seed(0)
    d = pd.DataFrame({
      "Y": ['No Attack']*num_rows_for_default + ['Attack A']*num_rows_per_class + ['Attack B']*num_rows_per_class,
      "Pred_Proba No Attack": 
          np.random.normal(0.7, 0.3, num_rows_for_default).tolist() + \
          np.random.normal(0.5, 0.3, num_rows_per_class * 2).tolist(),
      "Pred_Proba Attack A": 
          np.random.normal(0.1, 0.3, num_rows_for_default).tolist() + \
          np.random.normal(0.9, 0.3, num_rows_per_class).tolist() + \
          np.random.normal(0.1, 0.3, num_rows_per_class).tolist(),
      "Pred_Proba Attack B": 
          np.random.normal(0.1, 0.3, num_rows_for_default).tolist() + \
          np.random.normal(0.1, 0.3, num_rows_per_class).tolist() + \
          np.random.normal(0.9, 0.3, num_rows_per_class).tolist()                    
    })
    d['Y'] = d['Y'].astype(str)
    return d, ['No Attack', 'Attack A', 'Attack B']

d, target_classes = generate_data()

笔记本中有一些代码用于缩放分数并确保它们的和为 1.0,但在这里,我们可以假设这已经完成,并且我们为每条记录准备了一组结构良好的每个类别的概率。

正如真实世界数据中常见的那样,其中一个类别(‘无攻击’类别)比其他类别频繁得多;该数据集是不平衡的。

然后我们设置目标预测,目前只是选择概率最高的类别:

def set_class_prediction(d):    
    max_cols = d[proba_cols].idxmax(axis=1)
    max_cols = [x[len("Pred_Proba_"):] for x in max_cols]
    return max_cols   

d['Pred'] = set_class_prediction(d)

这会生成:

选择概率最高的类别是默认行为,在这个例子中,这是我们希望超越的基准。

我们可以像处理二分类情况一样,调用print_stats_labels(),其功能类似,处理任意数量的类别:

tuner.print_stats_labels(
    y_true=d["Y"], 
    target_classes=target_classes,
    y_pred=d["Pred"])

这会输出:

使用这些标签,我们得到的 F1 宏观评分仅为 0.447。

调用print_stats_proba(),我们还可以获得与预测概率相关的输出:

这比二分类情况更为复杂,因为我们需要考虑三个概率:每个类别的概率。因此,我们首先展示数据如何相对于每个类别的概率对齐。在这种情况下,有三个目标类别,所以第一行有三个图表。

正如预期的那样,当基于‘无攻击’的预测概率绘制数据时(最左侧的图表),‘无攻击’记录被赋予了比其他类别更高的概率。对于‘攻击 A’(中间图表)和‘攻击 B’(最右侧的图表)也是类似的。

我们还可以看到,类别之间并未完全分离,因此没有一组阈值能产生完美的混淆矩阵。我们需要选择一组阈值,最好能够平衡每个类别的正确预测和错误预测。

在上图中,底部的图表显示每个点的真实类别的概率。对于真实类别为‘无攻击’的记录(绿色点),我们根据它们的‘无攻击’预测概率绘制这些点;对于真实类别为‘攻击 A’的记录(深蓝色),我们根据它们的‘攻击 A’预测概率绘制这些点;‘攻击 B’(深黄色)也是类似的。我们可以看到,模型对‘攻击 A’和‘攻击 B’的概率相似,并且这些类别的概率高于‘无攻击’。

上述图表未考虑任何可能使用的特定阈值。我们还可以选择性地生成更多输出,传入一组阈值(每个类别一个,使用 0.0 表示默认类别):

tuner.print_stats_proba(
    y_true=d["Y"], 
    target_classes=target_classes, 
    y_pred_proba=d[proba_cols].values,
    default_class='No Attack',
    thresholds=[0.0, 0.4, 0.4]
)

这对于绘制工具发现的最优阈值集可能最为有用,但也可以用于查看其他潜在的阈值集。

这为每个类别生成报告。为了节省空间,这里只展示 Class Attack A 的报告(完整的报告可以在示例笔记本中查看;查看其他两个类别的报告也有助于理解在此示例中使用[0.0, 0.4, 0.4]作为阈值的全部影响):

由于这里指定了一组阈值,我们可以看到使用这些阈值的影响,包括每个类别会被正确和错误分类的数量。

我们首先查看阈值在 ROC 曲线上的位置。在这种情况下,我们查看的是 Class A 的报告,所以看到阈值为 0.4(0.4 在上面的 API 调用中为 Class A 指定)。

还展示了 AUROC 分数。这个指标仅适用于二分类预测,但在多分类问题中,我们可以通过将问题视为一系列的“一个对其他”问题,来为每个类别计算 AUROC 分数。在这里,我们可以将问题视为‘Attack A’与非‘Attack A’(对其他报告也可以做类似处理)。

接下来的图显示了每个类别相对于预测概率的分布。由于不同类别的数量不同,这里展示了两种方式:一种展示实际分布,另一种展示经缩放后便于比较的分布。前者更为相关,但后者可以让所有类别清晰可见,尤其是当某些类别远比其他类别稀有时。

我们可以看到,真实类别为‘Attack A’(深蓝色)的记录确实具有更高的‘Attack A’预测概率,但仍然需要决定具体设置阈值的位置。这里我们看到了使用 0.4 作为该类别阈值的效果。看起来 0.4 可能接近理想值,甚至可能就是理想值。

我们也可以以群体图的形式看到这一点(最右侧的图),误分类的点用红色表示。我们可以看到,使用更高的阈值(例如 0.45 或 0.5),会有更多的记录将真实类别为 Attack A 的误分类,但对于真实类别为‘No Attack’的误分类记录则较少。而使用更低的阈值(例如 0.3 或 0.35)则会产生相反的效果。

我们也可以调用 plot_by_threshold()来查看不同的潜在阈值:

tuner.plot_by_threshold(
    y_true=d['Y'], 
    target_classes=target_classes,
    y_pred_proba=d[proba_cols].values,
    default_class='No Attack'
)

这个 API 只是为了说明,并非用于调优,因此为了简便起见(对于每个潜在阈值),每个类别使用相同的阈值(默认类别除外)。展示这三个潜在阈值 0.2、0.3 和 0.4:

第一行数字显示了使用 0.2 作为所有类别(默认类别除外)的阈值的含义(即,只有当攻击 A 的估计概率至少为 0.2 时才预测攻击 A;只有当攻击 B 的预测概率至少为 0.2 时才预测攻击 B——否则始终选择预测概率最高的类别)。类似地,第二行和第三行分别对应于 0.3 和 0.4 的阈值。

我们可以在这里看到,使用较低或较高阈值对每个类别的权衡,以及由此产生的混淆矩阵(以及与这些混淆矩阵相关的 F1 分数)。

在这个例子中,从 0.2 移动到 0.3,再到 0.4,我们可以看到模型越来越少地预测攻击 A 或攻击 B(提高阈值后,除了默认类别外,我们越来越少预测其他任何类别),而更多地预测“无攻击”,这导致真实类别为“无攻击”的误分类减少,但真实类别为攻击 A 或攻击 B 的误分类增加。

当阈值非常低时,比如 0.2,那么在真实类别为默认类别的记录中,只有那些预测为“无攻击”(No Attack)类别的概率最高的记录(大约是前半部分)被正确预测。

一旦阈值设置在 0.6 以上,几乎所有的预测都会是默认类别,因此所有真实类别为默认类别的情况都是正确的,而其他情况都是错误的。

正如预期的那样,设置较高的阈值意味着更频繁地预测默认类别,错过这些的情况较少,尽管错过其他类别的情况增多。使用较低阈值时,攻击 A 和攻击 B 通常会被正确预测,而使用较高阈值时则大多预测错误。

为了调整阈值,我们再次使用 tune_threshold(),代码如下:

from sklearn.metrics import f1_score

best_thresholds = tuner.tune_threshold(
    y_true=d['Y'], 
    target_classes=target_classes,
    y_pred_proba=d[proba_cols].values,
    metric=f1_score,
    average='macro',
    higher_is_better=True,
    default_class='No Attack',
    max_iterations=5
)
best_thresholds

这输出: [0.0, 0.41257, 0.47142]。也就是说,它找到了大约 0.413 的阈值对于攻击 A 最为合适,而 0.471 对于攻击 B 最适合,以优化指定的指标——在这种情况下为宏 F1 分数。

再次调用 print_stats_proba(),我们得到:

tuner.print_stats_proba(
    y_true=d["Y"], 
    target_classes=target_classes, 
    y_pred_proba=d[proba_cols].values,
    default_class='No Attack',
    thresholds=best_thresholds
)

其输出为:

使用这里发现的阈值,宏 F1 分数从大约 0.44 提高到 0.68(结果会随着每次运行略有不同)。

get_predictions()

提供了一个额外的 API,非常方便,即 get_predictions(),用于根据一组预测和阈值获取标签预测。它的调用方式如下:

tuned_pred = tuner.get_predictions(
    target_classes=target_classes,
    d["Pred_Proba"], 
    None, 
    best_threshold)

使用真实数据集进行测试

还在许多真实数据集上进行了测试。通常情况下,发现的阈值与默认值差不多,但更常见的是,发现的阈值明显更有效。在 GitHub 页面上包括了一个笔记本,涵盖了少量(四个)真实数据集。这个笔记本的提供更多是为了展示如何使用该工具及其生成的图表(与解释工具时使用的合成数据不同),但也提供了一些例子,说明该工具确实能提高 F1 宏观得分。

简单总结一下,就发现的阈值和 F1 宏观得分的提升而言:

乳腺癌:发现了一个最佳阈值 0.5465,令宏观 F1 分数从 0.928 提升到 0.953。

钢板故障:发现了一个最佳阈值 0.451,令宏观 F1 分数从 0.788 提升到 0.956。

Phenome 发现了一个最佳阈值 0.444,令宏观 F1 分数从 0.75 提升到 0.78。

在数字数据集上,没有发现比默认阈值更好的结果,但使用不同的分类器或在其他不同条件下可能会有所不同。

安装

本项目使用了一个单一 .py 文件

这必须复制到你的项目中并进行导入。例如:

from threshold_tuner import ClassificationThesholdTuner

tuner = ClassificationThesholdTuner()

在多类别问题中设置阈值的影响

在多类别设置中设置阈值有一些微妙的点,这些点可能与任何特定项目相关,也可能无关。这些细节可能会超出你的工作需求,而且这篇文章已经相当长了,但在主 GitHub 页面上有一个部分专门讨论了这些相关情况。特别地,阈值设置在 0.5 以上时,与 0.5 以下的阈值相比,可能会有些不同的表现。

结论

虽然调整分类项目中使用的阈值并不总是能提高模型的质量,但通常情况下确实能提高,而且提高的幅度往往很大。这很容易做到,但使用 ClassificationThresholdTuner 可以让这个过程更轻松,特别是在多类别分类中,它可以特别有用。

它还提供了阈值选择的可视化图表,这对于理解和接受它发现的阈值,或选择其他阈值以更好地符合项目目标,都是很有帮助的。

在多类别分类中,理解调整阈值的效果可能仍然需要一些努力,但有了像这样的工具,理解起来比没有工具要容易得多,而且在许多情况下,简单地调整阈值并测试结果就足够了。

所有图片均由作者提供

使用电子商务数据科学获取客户

原文:towardsdatascience.com/acquire-customers-with-ecommerce-data-science-64a2fcef12f0?source=collection_archive---------8-----------------------#2024-06-05

数据驱动的策略帮助电子商务企业克服广告挑战

Ivy LiuTowards Data Science Ivy Liu

·发布于 Towards Data Science ·6 分钟阅读·2024 年 6 月 5 日

--

在电子商务社区中,有一个流行的观点认为,高质量的创意就是成功广告活动所需的一切。这种观点认为,品牌应该专注于制作出色的创意,并让广告平台处理其他一切。然而,实际上,电子商务企业盲目相信广告平台是不明智的,原因有几个:

  • 对于电子商务中小型企业来说,建立大量广告表现数据的历史是昂贵的。

  • 当品牌向多个市场细分推广多个产品时,表现数据会变得分散,并可能误导算法。

幸运的是,电子商务的高管和营销人员可以利用数据科学来克服这些挑战。在本文中,我们将解释广告平台的算法如何工作,并分享一些改善客户获取的实用方法。

图片来源:作者

广告平台如何运作

广告平台通过实时竞拍来确定哪些广告展示给哪些用户。以 Meta 为例,其广告竞拍通过总价值评分来决定获胜广告:

总价值 = 出价 × 预估行动率 + 相关性和质量评分

  • 出价:广告商愿意为期望的行动支付的金额。

  • 预估行动率:用户看到广告后执行预期行为的可能性。

  • 相关性和质量评分:广告对目标用户的相关性和质量。 品牌可以通过广告质量和定向来影响这一评分。

总价值评分越高,广告获胜的机会就越大。此外,较高的相关性和质量评分有助于确保显著的位置,并降低成本。

平台需要时间(和金钱)来学习

广告平台依赖先进的机器学习模型,将广告投放到正确的观众群体。当一则新广告发布时,算法进入学习阶段,收集用户与广告的互动数据。诸如点击和转化等互动行为表明了积极的参与,提升了广告的相关性评分,从而形成了正向反馈循环。在初步学习期过后,算法继续从用户互动中学习,并根据最新的数据动态调整广告投放。

广告优化并不容易

根据我们的分析,提升广告表现的关键在于提高相关性和质量评分,并优化算法的学习方式。但解决这个问题面临着一些挑战:

  • 广告的相关性取决于广告内容和投放方式。即使广告质量很高,如果投放到错误的观众群体,也会削弱广告效果。

  • 每则广告都会生成一套独立的表现数据。算法依赖大量一致的广告表现数据,以确定每则广告的最佳受众。

  • 广告平台通过基于各种因素对用户进行细分,以增强广告的相关性和互动性。然而,这种细分方式可能并不总是与电商品牌的理想客户群体一致。

这些因素对电商广告有重要影响。

向不相关的观众展示广告浪费金钱

以一个既面向休闲玩家又面向竞技玩家的体育品牌为例。如果该品牌投放一则关于专业装备的广告,而没有进行特定的定向投放,算法将把广告展示给广泛的观众。由于广告平台的用户细分方式与品牌的定位不完全相同,广告可能最终会展示给休闲玩家,而这些玩家不太可能与广告产生互动。这种缺乏互动的情况向算法发出了负面信号,降低了广告的相关性评分,并使得争取到优质广告位变得更加困难。

频繁的广告更新会分散表现数据

一个服装品牌可能会每个月推出新款服装并投放促销广告。由于每则广告的预算有限且投放时间较短,算法难以收集足够的表现数据。这使得精准定位每则广告的最佳受众变得更加困难。因此,算法最终将这些广告展示给大量不合适的购物者,降低了广告的相关性评分并提高了广告成本。

大型促销活动扰乱算法学习

当品牌为特殊活动或产品发布进行大规模促销时,它们的广告会暂时变得更加吸引人。大幅折扣能够吸引那些平时不会购买产品的顾客。这些暂时的用户互动激增可能会让算法感到困惑,导致它们误解典型的用户行为。因此,在这些大型促销活动之后,获客成本通常会上升,迫使品牌在广告上花费更多的资金来纠正这种干扰。

利用数据科学应对营销挑战

幸运的是,品牌可以分析客户的购买行为,并引导广告平台即使在广告表现数据有限的情况下,也能实现最佳的广告效果。

确定正确的客户群体

当品牌进行客户细分时,目标是识别每个客户群体在产品、信息传递和促销方面的独特偏好,从而帮助品牌利用这些客户洞察改善销售和营销策略。

相关的细分因素对于每个品牌可能有所不同,因此考虑尽可能多的因素非常重要。电子商务客户细分的关键因素包括人口统计、地理位置、购买模式和增长趋势。

品牌可以使用机器学习模型准确识别客户群体。一旦完成这一过程,品牌可以利用这些客户洞察来设置广告定位并执行有效的广告活动。

我曾与一个体育品牌合作,能够根据年龄、收入和竞争性等因素识别出不同的客户群体。一个群体是中年成人,住在高档郊区,偏好购买品牌的专业装备。另一个群体是年轻的城市专业人士,他们更喜欢入门级设备,且偶尔参与这项运动。通过这些洞察,品牌能够通过年龄和位置精确定位不同的客户群体,并在广告活动中为每个群体展示不同的产品和运动场景。

设计有效的创意

除了精准的广告定位外,还有几个因素共同作用,才能为每个客户群体创造有效的广告:

  • 展示正确的产品

品牌可以从销售和营销数据中识别出每个客户群体的最佳产品,并在广告创意中为每个群体展示不同的产品。对于一家家居装饰品牌来说,年轻客户可能偏好价格实惠、色彩鲜艳的产品,而富裕客户则更喜欢设计豪华的产品。

  • 满足相关需求

最新的人工智能工具在根据社交媒体评价和社交媒体帖子的社交聆听提供信息创意方面非常有帮助。对于一家健康品牌来说,年轻人更有可能购买健身和能量产品,而年长的退休人士则更关注老龄化和健康问题。

  • 展示相关的生活方式

品牌可以从人口统计、地理位置和销售数据中推测每个群体的生活方式。同样,最新的人工智能工具可以帮助你为每个群体形成一个生活方式快照。对于休闲产品,年轻父母可能更喜欢展示充满活力的家庭一起玩耍的广告创意。祖父母则可能更能与展示年长者和孙辈亲密互动的场景产生共鸣。

鉴于如今人们的注意力持续时间较短,最能与目标受众产生共鸣的广告更有可能脱颖而出并获得成功。

个性化促销优惠

促销折扣在当今经济中越来越普遍,但品牌有许多其他方式来组织他们的产品供应。许多品牌告诉我们,他们知道有些顾客不需要折扣就能完成购买,但他们不确定如何为不同的顾客群体提供不同的促销活动。好消息是,客户细分和精准的广告定位提供了新的机会。

因为不同的顾客群体偏好不同的产品,一种实际的策略是为每种类型的产品提供独特的促销活动。例如,一个服装品牌可以为富裕顾客推广限量版配饰。与此同时,他们可能会对年轻人群体中更受欢迎的服装款式提供折扣。

成功的客户获取不仅仅依赖于创意

高效的广告活动建立在精准的目标定位、有效的讯息传递和吸引人的产品提供上。仅仅拥有出色的创意并不能保证客户获取的成功。考虑到广告优化中涉及的众多因素,品牌应持续进行测试,了解哪些方法最适合自己的独特业务。数据科学可以帮助电子商务品牌通过深入的销售和营销策略在这条路上前行。

神经网络中的激活函数:如何选择合适的激活函数

原文:towardsdatascience.com/activation-functions-in-neural-networks-how-to-choose-the-right-one-cb20414c04e5?source=collection_archive---------10-----------------------#2024-12-12

激活函数简介及最著名函数的概览

Niklas LangTowards Data Science Niklas Lang

·发表于Towards Data Science ·15 分钟阅读·2024 年 12 月 12 日

--

图片由engin akyurt提供,来自Unsplash

近年来,神经网络已成为机器学习模型中一种强大的方法。激活函数是每个神经网络中的核心组件,它显著影响着模型的功能性。它决定了网络中一个神经元的激活强度,从而决定了从数据中学习到哪些结构。如果没有激活函数,神经网络只能识别线性关系,无法产生那些在近年来令我们惊叹的出色结果。

在本文中,我们将详细探讨激活函数的属性,并比较常用的不同激活函数。我们还将提供一些关于如何为网络架构和具体使用案例找到合适的激活函数的建议,以便训练出最优模型。然而,在深入这个话题之前,我们应该先了解神经网络,特别是其中的神经元是如何工作的,以便了解激活函数在何种系统中嵌入。

感知机是如何工作的?

AdaBoost 分类器解析:带代码示例的可视化指南

原文:towardsdatascience.com/adaboost-classifier-explained-a-visual-guide-with-code-examples-fc0f25326d7b?source=collection_archive---------1-----------------------#2024-11-10

集成学习

将权重放在弱学习器最需要的地方

Samy BaladramTowards Data Science Samy Baladram

·发布于Towards Data Science ·阅读时长 11 分钟·2024 年 11 月 10 日

--

## 随机森林解析:带代码示例的可视化指南

利用随机树做出惊人的预测

towardsdatascience.com

每个人都会犯错——即便是最简单的决策树也会在机器学习中出错。AdaBoost(自适应提升)算法并没有忽视这些错误,而是做了不同的事情:它从这些错误中学习(或适应),不断提升。

与随机森林不同,随机森林是同时构建多棵树,而 AdaBoost 则从一棵简单的树开始,识别它误分类的实例。然后,它构建新的树来修正这些错误,边学习边改进。

在这里,我们将具体说明 AdaBoost 是如何进行预测的,通过结合多个针对性的弱学习器逐步增强其预测能力,就像一项将专注的锻炼转化为全身力量的训练计划。

所有可视化图像:作者使用 Canva Pro 创建,已针对手机优化;在桌面上可能显示为过大。

定义

AdaBoost 是一种集成机器学习模型,通过加权的决策树序列来创建模型,通常使用浅层树(通常是单层“树桩”)。每棵树都在整个数据集上进行训练,但使用自适应的样本权重,这些权重给与之前被错误分类的样本更高的优先级。

对于分类任务,AdaBoost 通过加权投票系统将树结合起来,表现更好的树在最终决策中有更大的影响力。

该模型的优势来自于其自适应学习过程——尽管每棵简单的树可能是一个“弱学习器”,其表现仅比随机猜测稍好,但树的加权组合创建了一个“强学习器”,它逐步聚焦并修正错误

AdaBoost 属于提升算法家族的一部分,因为它一次构建一棵树。每棵新树尝试修正之前树的错误。然后,它通过加权投票将各棵树的结果结合起来,做出最终的预测。

使用的数据集

在本文中,我们将以经典的高尔夫数据集作为分类的示例。

列:‘Outlook(经过独热编码为 3 列)’,‘Temperature’(以华氏度表示),‘Humidity’(以百分比表示),‘Windy’(是/否)和‘Play’(是/否,目标特征)

import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
# Create and prepare dataset
dataset_dict = {
    'Outlook': ['sunny', 'sunny', 'overcast', 'rainy', 'rainy', 'rainy', 'overcast', 
                'sunny', 'sunny', 'rainy', 'sunny', 'overcast', 'overcast', 'rainy',
                'sunny', 'overcast', 'rainy', 'sunny', 'sunny', 'rainy', 'overcast',
                'rainy', 'sunny', 'overcast', 'sunny', 'overcast', 'rainy', 'overcast'],
    'Temperature': [85.0, 80.0, 83.0, 70.0, 68.0, 65.0, 64.0, 72.0, 69.0, 75.0, 75.0,
                   72.0, 81.0, 71.0, 81.0, 74.0, 76.0, 78.0, 82.0, 67.0, 85.0, 73.0,
                   88.0, 77.0, 79.0, 80.0, 66.0, 84.0],
    'Humidity': [85.0, 90.0, 78.0, 96.0, 80.0, 70.0, 65.0, 95.0, 70.0, 80.0, 70.0,
                 90.0, 75.0, 80.0, 88.0, 92.0, 85.0, 75.0, 92.0, 90.0, 85.0, 88.0,
                 65.0, 70.0, 60.0, 95.0, 70.0, 78.0],
    'Wind': [False, True, False, False, False, True, True, False, False, False, True,
             True, False, True, True, False, False, True, False, True, True, False,
             True, False, False, True, False, False],
    'Play': ['No', 'No', 'Yes', 'Yes', 'Yes', 'No', 'Yes', 'No', 'Yes', 'Yes', 'Yes',
             'Yes', 'Yes', 'No', 'No', 'Yes', 'Yes', 'No', 'No', 'No', 'Yes', 'Yes',
             'Yes', 'Yes', 'Yes', 'Yes', 'No', 'Yes']
}
# Prepare data
df = pd.DataFrame(dataset_dict)
df = pd.get_dummies(df, columns=['Outlook'], prefix='', prefix_sep='', dtype=int)
df['Wind'] = df['Wind'].astype(int)
df['Play'] = (df['Play'] == 'Yes').astype(int)

# Rearrange columns
column_order = ['sunny', 'overcast', 'rainy', 'Temperature', 'Humidity', 'Wind', 'Play']
df = df[column_order]

# Prepare features and target
X,y = df.drop('Play', axis=1), df['Play']
X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.5, shuffle=False)Main Mechanism

主要机制

下面是 AdaBoost 的工作原理:

  1. 初始化权重: 为每个训练样本分配相等的权重。

  2. 迭代学习: 在每一步,训练一棵简单的决策树并检查其表现。被错误分类的样本将获得更多的权重,使其成为下一棵树的优先考虑对象。正确分类的样本保持不变,所有权重都会调整,以确保它们的总和为 1。

  3. 构建弱学习器: 每棵新的简单树都针对之前树的错误进行修正,创建了一系列专门化的弱学习器。

  4. 最终预测: 通过加权投票将所有树的结果结合起来,每棵树的投票基于其重要性值,更准确的树会有更大的影响力。

AdaBoost 分类器通过使用许多简单的决策树(通常是 50 到 100 棵树)来进行预测。每棵树,称为“树桩”,专注于一个重要特征,如温度或湿度。最终的预测是通过结合所有树的投票来做出的,每棵树的投票根据该树的重要性(“alpha”)加权。

训练步骤

在这里,我们将遵循 SAMME(使用多类指数损失函数的阶段性加法建模)算法,这是 scikit-learn 中的标准方法,能够处理二分类和多分类问题。

1.1. 决定要使用的弱学习器。默认选择是一棵单层决策树(或称“树桩”)。

1.2. 决定要构建多少个弱学习器(在这个例子中是树的数量,默认是 50 棵树)。

我们从深度为 1 的决策树(树桩)开始作为我们的弱学习器。每个树桩仅做一次分裂,我们将顺序训练 50 个树桩,并在训练过程中调整权重。

1.3. 从给每个训练示例相同的权重开始:

· 每个样本的权重 = 1/NN 是样本总数)

· 所有权重加起来等于 1

所有数据点开始时的权重相同(0.0714),总权重加起来为 1。这确保了在训练开始时,每个示例都是同等重要的。

对于第一棵树

2.1. 在考虑样本权重的情况下构建决策树桩

在进行第一次分裂之前,算法会检查所有数据点及其权重,以找到最佳的分裂点。这些权重影响每个示例在做出分裂决策时的重要性。

a. 计算根节点的初始加权基尼不纯度

算法计算根节点的基尼不纯度得分,但现在考虑了所有数据点的权重。

b. 对每个特征:

· 按特征值对数据进行排序(与决策树分类器中的操作完全相同)

对于每个特征,算法对数据进行排序并识别潜在的分裂点,完全类似于标准的决策树。

· 对每个可能的分裂点:

·· 将样本分为左组和右组

·· 计算两个组的加权基尼不纯度

·· 计算此分裂的加权基尼不纯度减少量

算法计算每个潜在分裂点的加权基尼不纯度,并将其与父节点进行比较。对于特征“sunny”(分裂点为 0.5),该不纯度减少量(0.066)显示了此分裂如何改善数据的分离。

c. 选择能够带来最大基尼不纯度减少的分裂点

在检查所有可能的特征分裂后,‘overcast’列(分裂点为 0.5)提供了最高的不纯度减少(0.102)。这意味着它是最有效的分类分离方式,因此成为第一次分裂的最佳选择。

d. 使用这个决策创建一个简单的单分裂树

使用找到的最佳分裂点,算法将数据分为两个组,每个组保持其原始权重。这个简单的决策树故意保持较小且不完美,使其仅略微优于随机猜测。

2.2. 评估这棵树的好坏

a. 使用树来预测训练集的标签。

b. 将所有误分类样本的权重相加,得到误差率

第一棵弱学习器对训练数据进行预测,我们检查它在哪些地方犯了错误(标记为 X)。错误率为 0.357,显示这棵简单的树在一些预测上是错误的,这是可以预期的,并且有助于指导下一步的训练。

c. 使用以下公式计算树的权重 (α):

α = 学习率 × log((1-错误)/错误)

使用错误率,我们计算树的影响分数(α = 0.5878)。较高的分数意味着树的准确性更高,这棵树因其良好的表现获得了适中的重要性。

2.3. 更新样本权重

a. 对于正确分类的样本,保留原始权重

b. 将错误分类样本的权重乘以 e^(α)。

c. 将每个权重除以所有权重的总和。这一归一化过程确保所有权重仍然加和为 1,同时保持它们的相对比例。

树错误分类的情况(标记为 X)会在下一轮中获得更高的权重。增加这些权重后,所有权重会归一化,使得错误分类的样本在下一棵树中得到更多关注。

对于第二棵树

2.1. 构建一个新的桩决策树,但这次使用更新后的权重

a. 计算根节点的新的加权基尼不纯度:

· 由于错误分类的样本权重较大,结果会有所不同

· 正确分类的样本现在具有较小的权重

使用更新后的权重(此时错误分类的样本权重更高),算法计算根节点的加权基尼不纯度。这将开始构建第二棵决策树的过程。

b. 对于每个特征:

· 与之前相同的过程,但权重已发生变化

c. 选择最佳的加权基尼不纯度减少划分

· 通常与第一棵树的划分完全不同

· 重点关注第一个树错误分类的样本

使用更新后的权重,不同的划分点展现出不同的效果。注意,“阴天”不再是最佳划分点——算法现在发现温度(84.0)给出的加权基尼不纯度减少最大,显示出权重变化如何影响划分选择。

d. 创建第二棵桩决策树

使用温度 ≤ 84.0 作为划分点,算法根据每组中总权重较大的类别来为每个叶节点分配 YES/NO,而不仅仅是通过计数示例。这种加权投票有助于纠正第一棵树的错误。

2.2. 评估这棵新树

a. 使用当前权重计算错误率

b. 使用与之前相同的公式计算其重要性 (α)

2.3. 再次更新权重 — 同样的过程:增加错误分类的权重,然后进行归一化。

第二棵树的错误率较低(0.222),且重要性得分较高(α = 1.253),与第一棵树相比。像之前一样,误分类的示例在下一轮会获得更高的权重。

从第三棵树开始

对所有剩余的树重复步骤 2.1–2.3。

该算法依次构建 50 棵简单的决策树,每棵树都有自己的重要性得分(α)。每棵树通过关注数据的不同方面来从之前的错误中学习,创建一个强大的组合模型。请注意,一些树(如树 2)在表现更好的时候会获得更高的权重得分。

第 3 步:最终集成 3.1. 保留所有树及其重要性得分

这 50 棵简单的决策树作为一个团队共同工作,每棵树都有自己的重要性得分(α)。在进行预测时,具有较高α值的树(如树 2,α = 1.253)对最终决策的影响大于得分较低的树。

from sklearn.tree import plot_tree
from sklearn.ensemble import AdaBoostClassifier
from sklearn.tree import plot_tree
import matplotlib.pyplot as plt

# Train AdaBoost
np.random.seed(42)  # For reproducibility
clf = AdaBoostClassifier(algorithm='SAMME', n_estimators=50, random_state=42)
clf.fit(X_train, y_train)

# Create visualizations for trees 1, 2, and 50
trees_to_show = [0, 1, 49]
feature_names = X_train.columns.tolist()
class_names = ['No', 'Yes']

# Set up the plot
fig, axes = plt.subplots(1, 3, figsize=(14,4), dpi=300)
fig.suptitle('Decision Stumps from AdaBoost', fontsize=16)

# Plot each tree
for idx, tree_idx in enumerate(trees_to_show):
    plot_tree(clf.estimators_[tree_idx],
              feature_names=feature_names,
              class_names=class_names,
              filled=True,
              rounded=True,
              ax=axes[idx],
              fontsize=12)  # Increased font size
    axes[idx].set_title(f'Tree {tree_idx + 1}', fontsize=12)

plt.tight_layout(rect=[0, 0.03, 1, 0.95])

每个节点显示其“值”参数,格式为[weight_NO, weight_YES],表示该节点每个类别的加权比例。这些权重来自我们在训练过程中计算的样本权重。

测试步骤

用于预测:

a. 获取每棵树的预测结果

b. 将每个值乘以其重要性得分(α

c. 将它们全部相加

d. 总权重较高的类别将作为最终预测

当预测新数据时,每棵树都会做出自己的预测,并将其乘以自己的重要性得分(α)。最终的决策来自于将所有加权投票结果相加——在这里,NO 类别获得了更高的总分(23.315 对 15.440),因此模型预测该示例为 NO。

评估步骤

构建完所有树之后,我们可以评估测试集。

通过反复训练和加权弱学习器来专注于误分类的示例,AdaBoost 创建了一个强大的分类器,能够实现高精度——通常比单一决策树或更简单的模型更好!

# Get predictions
y_pred = clf.predict(X_test)

# Create DataFrame with actual and predicted values
results_df = pd.DataFrame({
    'Actual': y_test,
    'Predicted': y_pred
})
print(results_df) # Display results DataFrame

# Calculate and display accuracy
from sklearn.metrics import accuracy_score
accuracy = accuracy_score(y_test, y_pred)
print(f"\nModel Accuracy: {accuracy:.4f}")

关键参数

以下是 AdaBoost 的关键参数,特别是在scikit-learn中:

estimator:这是 AdaBoost 用来构建最终解决方案的基础模型。最常见的三种弱学习器是:

a. 深度为 1 的决策树(决策树桩):这是默认的也是最流行的选择。由于它只有一个分裂,因此被认为是一个非常弱的学习器,仅比随机猜测略好,这正是提升过程所需要的。

b. 逻辑回归:逻辑回归(特别是高惩罚的情况下)也可以在这里使用,尽管它并不是一个真正的弱学习器。它对于具有线性关系的数据可能很有用。

c. 小深度的决策树(例如,深度为 2 或 3):这些树比决策树桩稍微复杂一些。它们仍然相对简单,但可以处理比决策树桩稍微复杂一些的模式。

AdaBoost 的基模型可以是简单的决策树桩(深度=1)、小深度的树(深度为 2-3)或惩罚的线性模型。每种类型都保持简单,以避免过拟合,同时提供不同的方式来捕捉模式。

n_estimators:要组合的弱学习器数量,通常在 50 到 100 之间。超过 100 通常不会带来显著的改善。

learning_rate:控制每个分类器对最终结果的影响。常见的初始值有 0.1、0.5 或 1.0。较小的值(如 0.1)和稍微更高的n_estimators通常效果更好。

与随机森林的主要区别

由于随机森林和 AdaBoost 都使用多棵树,因此很容易混淆其中的参数。关键区别在于,随机森林是独立地(袋装法)结合多棵树,而 AdaBoost 是一个接一个地构建树以修正错误(提升法)。以下是它们差异的其他一些细节:

  1. 没有bootstrap参数,因为 AdaBoost 使用所有数据,但权重会发生变化

  2. 没有oob_score,因为 AdaBoost 不使用自助采样法

  3. learning_rate变得至关重要(在随机森林中不存在)

  4. 树的深度通常保持非常浅(通常只是树桩),与随机森林的较深树不同

  5. 焦点从并行独立树转移到顺序依赖树,这使得像n_jobs这样的参数变得不那么相关

优缺点

优点:

  • 自适应学习:AdaBoost 通过增加对错误的权重来提升性能。每棵新树会更加关注它之前做错的难例。

  • 抗过拟合:尽管它会一个接一个地添加更多的树,但 AdaBoost 通常不会过于专注于训练数据。这是因为它使用加权投票,因此没有任何一棵树能过多地控制最终的答案。

  • 内置特征选择:AdaBoost 自然地找出哪些特征最重要。每棵简单的树都会选择该轮中最有用的特征,这意味着它在训练时会自动选择重要特征。

缺点:

  • 对噪声敏感:由于 AdaBoost 给错误更多的权重,它可能会在数据杂乱或错误时出现问题。如果一些训练样本标签错误,它可能会过于关注这些错误样本,从而使整个模型变得更差。

  • 必须是顺序的:与随机森林可以同时训练多棵树不同,AdaBoost 必须一次训练一棵树,因为每棵新树需要知道前一棵树的表现。这使得训练速度较慢。

  • 学习率敏感性:虽然它的调整项比随机森林少,但学习率确实影响其效果。如果学习率过高,模型可能会过拟合训练数据。如果学习率过低,则需要更多的树才能取得良好的效果。

最后的备注

AdaBoost 是一种关键的提升算法,许多新方法都从中汲取了灵感。它的主要思想——通过关注错误来变得更好——帮助塑造了许多现代机器学习工具。虽然其他方法试图一开始就做到完美,但 AdaBoost 则试图表明,有时候解决问题的最佳方式是从错误中学习并不断改进。

AdaBoost 最适用于二分类问题,并且当数据清晰时表现最佳。虽然随机森林可能更适用于更一般的任务(例如预测数字)或杂乱的数据,但在正确使用的情况下,AdaBoost 可以产生非常好的结果。人们在多年后仍然使用它,足以证明其核心思想的有效性!

🌟 AdaBoost 分类器代码概述

import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
from sklearn.ensemble import AdaBoostClassifier
from sklearn.tree import DecisionTreeClassifier

# Create dataset
dataset_dict = {
    'Outlook': ['sunny', 'sunny', 'overcast', 'rainy', 'rainy', 'rainy', 'overcast', 
                'sunny', 'sunny', 'rainy', 'sunny', 'overcast', 'overcast', 'rainy',
                'sunny', 'overcast', 'rainy', 'sunny', 'sunny', 'rainy', 'overcast',
                'rainy', 'sunny', 'overcast', 'sunny', 'overcast', 'rainy', 'overcast'],
    'Temperature': [85.0, 80.0, 83.0, 70.0, 68.0, 65.0, 64.0, 72.0, 69.0, 75.0, 75.0,
                   72.0, 81.0, 71.0, 81.0, 74.0, 76.0, 78.0, 82.0, 67.0, 85.0, 73.0,
                   88.0, 77.0, 79.0, 80.0, 66.0, 84.0],
    'Humidity': [85.0, 90.0, 78.0, 96.0, 80.0, 70.0, 65.0, 95.0, 70.0, 80.0, 70.0,
                 90.0, 75.0, 80.0, 88.0, 92.0, 85.0, 75.0, 92.0, 90.0, 85.0, 88.0,
                 65.0, 70.0, 60.0, 95.0, 70.0, 78.0],
    'Wind': [False, True, False, False, False, True, True, False, False, False, True,
             True, False, True, True, False, False, True, False, True, True, False,
             True, False, False, True, False, False],
    'Play': ['No', 'No', 'Yes', 'Yes', 'Yes', 'No', 'Yes', 'No', 'Yes', 'Yes', 'Yes',
             'Yes', 'Yes', 'No', 'No', 'Yes', 'Yes', 'No', 'No', 'No', 'Yes', 'Yes',
             'Yes', 'Yes', 'Yes', 'Yes', 'No', 'Yes']
}
df = pd.DataFrame(dataset_dict)

# Prepare data
df = pd.get_dummies(df, columns=['Outlook'], prefix='', prefix_sep='', dtype=int)
df['Wind'] = df['Wind'].astype(int)
df['Play'] = (df['Play'] == 'Yes').astype(int)

# Split features and target
X, y = df.drop('Play', axis=1), df['Play']
X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.5, shuffle=False)

# Train AdaBoost
ada = AdaBoostClassifier(
    estimator=DecisionTreeClassifier(max_depth=1), # Create base estimator (decision stump)
    n_estimators=50,        # Typically fewer trees than Random Forest
    learning_rate=1.0,      # Default learning rate
    algorithm='SAMME',      # The only currently available algorithm (will be removed in future scikit-learn updates)
    random_state=42
)
ada.fit(X_train, y_train)

# Predict and evaluate
y_pred = ada.predict(X_test)
print(f"Accuracy: {accuracy_score(y_test, y_pred)}")

进一步阅读

对于 AdaBoostClassifier 及其在 scikit-learn 中的实现,读者可以参考官方文档,文档提供了有关其用法和参数的详细信息。

技术环境

本文使用 Python 3.7 和 scikit-learn 1.6。尽管所讨论的概念普遍适用,但具体的代码实现可能会因不同版本而略有不同。

关于插图

除非另有说明,所有图像均由作者创作,并结合了 Canva Pro 的授权设计元素。

𝙎𝙚𝙚 𝙢𝙤𝙧𝙚 𝙀𝙣𝙨𝙚𝙢𝙗𝙡𝙚 𝙇𝙚𝙖𝙧𝙣𝙞𝙣𝙜 𝙝𝙚𝙧𝙚:

Samy Baladram

Samy Baladram

集成学习

查看列表4 篇故事

𝙔𝙤𝙪 𝙢𝙞𝙜𝙝𝙩 𝙖𝙡𝙨𝙤 𝙡𝙞𝙠𝙚:

Samy Baladram

Samy Baladram

分类算法

查看列表8 篇故事

Azure 数据平台在云中的着陆区

原文:towardsdatascience.com/adapting-the-azure-landing-zone-for-a-data-platform-in-the-cloud-f1c405c0d638?source=collection_archive---------4-----------------------#2024-08-16

Mariusz KujawskiTowards Data Science Mariusz Kujawski

·发布于 Towards Data Science ·7 分钟阅读·2024 年 8 月 16 日

--

处理敏感数据或在高度监管的环境中工作需要为数据处理提供安全的云基础设施。云可能看起来像是互联网中的一个开放环境,因此会引发安全问题。当你开始使用 Azure 且没有足够的资源配置经验时,很容易犯设计和实施的错误,这些错误可能会影响新数据平台的安全性和灵活性。在这篇文章中,我将描述为 Azure 中的数据平台设计云适配框架时最重要的方面。

作者插图

什么是 Azure 着陆区?

Azure 着陆区是部署公共云资源的基础。它包含构建强大平台所需的关键元素,这些元素包括网络、身份和访问管理、安全性、治理和合规性。通过实施着陆区,组织可以简化基础设施配置过程,确保使用最佳实践和指导方针。

Azure 着陆区是一个遵循关键设计原则的环境,用于支持应用迁移、现代化和开发。在 Azure 中,订阅用于隔离和开发应用程序和平台资源。这些资源按以下类别进行分类:

  • 应用着陆区:专门用于托管特定应用资源的订阅。

  • 平台着陆区:包含共享服务的订阅,例如为应用着陆区提供的身份、连接性和管理资源。

这些设计原则帮助组织在云环境中成功运营并扩展平台。

图像来自作者

在 Azure 中实施数据平台

在 Azure 中实现数据平台需要一个高级架构设计,在该设计中选择资源用于数据摄取、转化、提供和探索。第一步可能需要设计一个着陆区。如果您需要一个遵循最佳实践的安全平台,从着陆区开始至关重要。这将帮助您在订阅和资源组内组织资源,定义网络拓扑,并通过 VPN 确保与本地环境的连接,同时遵循命名约定和标准。

架构设计

为数据平台量身定制架构需要仔细选择资源。Azure 提供了多种适用于数据平台的原生资源,如 Azure Synapse Analytics、Azure Databricks、Azure 数据工厂和 Microsoft Fabric。这些可用服务提供了多种实现类似目标的方式,允许在架构选择上提供灵活性。

例如:

  • 数据摄取: Azure 数据工厂或 Synapse 管道。

  • 数据处理: Azure Databricks 或 Synapse 中的 Apache Spark。

  • 数据分析: Power BI 或 Databricks 仪表板。

我们可以使用 Apache Spark 和 Python,或者低代码的拖放工具。通过这些工具的不同组合,我们可以根据我们的技能、使用案例和能力创建最合适的架构。

高级架构(图像来自作者)

Azure 还允许您使用其他组件,如 Snowflake,或者使用开源软件、虚拟机(VM)或 Kubernetes 服务(AKS)创建您自己的组合。我们可以利用 VM 或 AKS 配置数据处理、探索、编排、AI 或 ML 的服务。

典型的数据平台结构

一个典型的 Azure 数据平台应该包括几个关键组件:

  1. 用于将数据从来源摄取到 Azure 存储帐户的工具。Azure 提供像 Azure 数据工厂、Azure Synapse 管道或 Microsoft Fabric 这样的服务。我们可以使用这些工具从数据源收集数据。

  2. 数据仓库、数据湖或数据湖屋:根据您的架构偏好,我们可以选择不同的服务来存储数据和业务模型。

  • 对于数据湖或数据湖屋,我们可以使用 Databricks 或 Fabric。

  • 对于数据仓库,我们可以选择 Azure Synapse、Snowflake 或 MS Fabric 数据仓库。

  1. 在 Azure 中编排数据处理时,我们可以使用 Azure 数据工厂、Azure Synapse 管道、Airflow 或 Databricks 工作流。

  2. 数据转化可以通过 Azure 的各种服务来处理。

  • 对于 Apache Spark:Databricks、Azure Synapse Spark 池和 MS Fabric Notebooks,

  • 对于基于 SQL 的转换,我们可以在 Databricks、Azure Synapse 或 MS Fabric 中使用 Spark SQL,在 SQL Server、MS Fabric 或 Synapse Dedicated Pool 中使用 T-SQL。或者,Snowflake 提供所有 SQL 功能。

订阅

平台设计的一个重要方面是根据业务单元和软件开发生命周期规划订阅和资源组的分割。可以为生产环境和非生产环境使用不同的订阅。通过这种区分,我们可以实现更灵活的安全模型,为生产和测试环境分别设定策略,并避免配额限制。

订阅组织(图像由作者提供)

网络

虚拟网络类似于在数据中心中运行的传统网络。Azure 虚拟网络(VNet)为您的平台提供了基础安全层,禁用资源的公共端点将大大降低丢失密钥或密码时发生数据泄漏的风险。如果没有公共端点,存储在 Azure 存储帐户中的数据只有在连接到您的 VNet 时才可以访问。

与本地网络的连接支持 Azure 资源与本地数据源之间的直接连接。根据连接类型,通信流量可能通过互联网的加密隧道或专用连接进行传输。

为了提高虚拟网络内的安全性,您可以使用网络安全组(NSG)和防火墙来管理进出流量规则。这些规则允许您根据 IP 地址、端口和协议来过滤流量。此外,Azure 还支持在子网、虚拟网络和本地网络以及互联网之间路由流量。使用自定义路由表可以控制流量的路由方向。

网络配置(图像由作者提供)

命名规范

命名规范为平台资源的名称建立了标准化,使其更加自我描述并易于管理。这种标准化有助于在 Azure 门户中浏览不同的资源并进行过滤。一个明确的命名规范可以让您快速识别资源的类型、目的、环境和 Azure 区域。这种一致性在您的 CI/CD 流程中可能非常有利,因为可预测的名称更容易参数化。

考虑到命名规范,您应该考虑需要捕捉的信息。标准应易于遵循、一致且实用。值得包括的元素有:组织、业务单元或项目、资源类型、环境、区域和实例编号。您还应该考虑资源的范围,以确保在其上下文中名称是唯一的。对于某些资源,如存储帐户,名称必须在全球范围内唯一。

例如,Databricks 工作区的命名格式可以如下所示:

命名约定(图片由作者提供)

示例缩写:

图片由作者提供

一个全面的命名约定通常包括以下格式:

  • 资源类型: 表示资源类型的缩写。

  • 项目名称: 项目的唯一标识符。

  • 环境: 资源支持的环境(例如:开发、QA、生产)。

  • 区域: 资源部署的地理区域或云提供商。

  • 实例: 用于区分同一资源的多个实例的编号。

基础设施实施

通过 Azure 门户实现基础设施看似简单,但通常每个资源涉及大量详细步骤。高度安全的基础设施将需要资源配置、网络设置、私有端点、DNS 区域等。像 Azure Synapse 或 Databricks 这样的资源还需要额外的内部配置,例如设置 Unity Catalog、管理秘密作用域和配置安全设置(用户、组等)。

配置完测试环境后,你需要将相同的配置复制到 QA 和生产环境中。这时容易出错。为了最小化可能影响开发质量的错误,建议使用基础设施即代码(IasC)方法进行基础设施开发。IasC 允许你通过 Terraform 或 Biceps 将云基础设施作为代码进行创建,从而能够以一致的配置部署多个环境。

在我的云项目中,我使用加速器快速启动新的基础设施设置。微软也提供了可以使用的加速器。将基础设施作为代码存储在版本库中,提供了额外的好处,如版本控制、跟踪变更、进行代码审查,以及与 DevOps 管道集成,以管理和推动跨环境的变更。

概述

如果你的数据平台不处理敏感信息,并且你不需要高度安全的数据平台,你可以创建一个更简单的设置,允许公共互联网访问,而无需虚拟网络(VNet)、VPN 等。然而,在高度监管的领域中,需要完全不同的实施方案。这个方案将涉及与组织内的各种团队合作——如 DevOps、平台和网络团队——甚至可能需要外部资源。

你需要建立一个安全的网络基础设施、资源和安全性。只有当基础设施准备好后,才能开始与数据处理开发相关的活动。

如果你觉得这篇文章有启发性,欢迎通过点击“鼓掌”按钮或在 LinkedIn 上点赞来表达你的感谢。你的支持对我来说非常重要。如有任何问题或建议,欢迎通过 LinkedIn* 与我联系。*

生成方法在匿名化中的应用将彻底改变数据共享和隐私保护。

原文:towardsdatascience.com/adaption-of-generative-methods-for-anonymization-will-revolutionize-data-sharing-and-privacy-d35b6fe704a2?source=collection_archive---------10-----------------------#2024-01-17

摆脱围绕大语言模型和基础模型的生成 AI 热潮,让我们来探讨由传统生成 AI 模型创建的合成数据如何准备迎接主流应用。

Arne RustadTowards Data Science Arne Rustad

·发表于Towards Data Science ·阅读时间 10 分钟·2024 年 1 月 17 日

--

由 Arne Rustad 使用 DALLE-3 生成的图像。

数据如同黄金一样宝贵,负责任地共享数据为组织和社会带来了巨大的机遇与挑战。为了在伦理上处理数据并避免法律后果,组织必须确保不侵犯那些提供数据的个人的隐私尽管数据共享具有巨大的潜力,但传统的匿名化方法已逐渐无法应对我们信息饱和的数字时代所带来的挑战。通过利用先进的生成方法,我们可以创建既符合隐私要求又保持原始数据实用性的合成数据。加入我们,一起揭开这扇通向丰富数据机会的大门。

本文中,我们特别强调了合成数据在商业环境中的应用,解决了我们在现有文献中发现的一个空白。虽然我们在这里的重点是企业领域,但合成数据的洞见和应用对其他参与数据共享的组织和个人同样具有重要意义,尤其是在研究社区中。

为什么你需要匿名化呢?

匿名化的目标是通过使数据与特定个人无法关联或至少极不可能关联,从而防止个人重新识别。匿名化数据并在分享前处理它,具有内在的道德价值,因为它尊重个人隐私。但随着公众对数据使用的关注日益增加,政府也引入了更严格的法规(如GDPRCCPA等),这已经成为所有组织需要关注的事情,除非他们愿意冒着巨大的声誉损失、诉讼和罚款的风险。

与此同时,如果不敢充分利用大数据和数据共享的潜力,组织将面临忽视重大商业机会、创新进展和潜在成本节约的风险。这也阻碍了我们解决更大社会问题的能力。利用匿名化数据提供了一种安全且合规的方式来挖掘数据的价值,因为它不受 GDPR 等法规的限制。

被低估的匿名化挑战

数据匿名化的任务是一个复杂且常常被低估的挑战。许多人认为匿名化只需要去除直接标识符,如姓名、社会保障号和地址。然而,人的身份往往比常见的假设更加易于辨识。在 2000 年进行的一项开创性研究中,计算机科学家 Latanya Sweeny 证明了仅凭三项信息——出生日期、性别和邮政编码——就可以唯一识别 87%的美国人口¹。跨越到更近的时代,2019 年《自然》期刊发表的一项研究进一步强调了这一点,揭示在一个包含 700 万人的数据库中,仅 15 个数据点就足以识别其中 99.98%的人²。

在大数据时代以及我们无论是自愿还是非自愿地分享个人信息的时代,数据匿名化比最初看起来更为脆弱和具有风险。

为了使数据集充分匿名化,它不仅必须在单独分析时具有较低的重新识别风险,而且在与网络上自由可用的所有其他信息交叉引用时,也应具有较低的风险。这些信息包括公开的数据库、我们在社交平台上自由分享的个人细节,甚至可能包括被盗的、在暗网上可获取的关于我们的敏感信息。换句话说,一个匿名化的数据集还必须能抵抗关联攻击。

Netflix 在数据匿名化失败中的不愉快经历应该成为警钟

2006 年,Netflix 为了增强其电影推荐算法,发布了他们认为是匿名化数据集的公开竞赛。该数据集包含了来自 480,000 个用户对 18,000 部电影的评分。尽管用户已被匿名化,并且故意在数据中插入了系统性错误,但这一举措仍然不足以保证隐私安全。德克萨斯大学的研究人员发布了一篇论文,展示了如何通过与 IMDB 上公开的电影评分进行交叉引用,轻松地重新识别出许多用户,从而无意中暴露了用户完整的电影观看历史。

这起事件看似无害,但请记住,我们的电影口味有时可以揭示我们个人生活中的深层次信息,比如性取向或政治信仰。因此,当 Netflix 在 2009 年尝试发起类似的竞赛时,他们因集体诉讼而被迫取消,突显了其中涉及的严重隐私风险³。

图 1:简化示例,展示了如何在 Netflix 竞赛数据集上进行数据关联。请注意,论文中的方法并不依赖于精确匹配。图由作者创建。

在回顾与匿名化相关的挑战后,不难理解,传统的匿名化技术通常需要非常具有侵入性才能勉强有效。由于传统方法通过去除或模糊化原始数据中的信息来实现匿名化,结果往往会导致数据效用的巨大损失。

合成数据——传统匿名化的替代方案

人工智能已经被用于创建合成数据很长时间了,但变分自编码器(VAE)、生成对抗网络(GAN)和扩散模型分别在 2013 年、2014 年和 2015 年的发明,标志着创建真实合成数据的重要里程碑。从那时起,科学界的许多渐进式进展使我们能够精确捕捉数据集中的复杂统计模式,无论这些数据是表格型的、时间序列的、图像形式的,还是其他格式。

上述模型属于生成方法。生成方法是一类机器学习技术,通过捕捉现有数据的模式和结构,可以创建新的数据。它们不仅仅是复制现有的数据,而是创造出独特且多样化的例子,这些例子在潜在特征和关系方面与原始数据相似。可以把它想象成一种新一代的数据,就像每一代人类都与他们的祖先相似一样。

通过 OpenAI 的聊天机器人 Chat-GPT 和图像生成器 DALLE-2,生成方法成功地进入了主流公众视野。人们对这些工具能够有效地完成许多人认为只有人类智力和创造力才能完成的任务感到惊讶。这使得生成性 AI 成为今年最常用的热词之一。尽管这些新的基础模型具有革命性的潜力,甚至可能改变我们的社会,但传统的生成方法依然扮演着至关重要的角色。Gartner 估计,到 2030 年,合成数据将在 AI 模型中完全超越真实数据⁴,尤其是在特定数据集的数据共享和数据增强方面,传统方法如 GAN、VAE 和扩散模型(非基础性模型)至少在目前仍然是最佳选择。

与传统的匿名化技术不同,生成方法不会破坏有价值的信息。

生成方法的合成数据因此提供了一个最佳解决方案,结合了两者的优势。先进的生成方法能够学习现实数据中固有的复杂模式,使得它们能够生成逼真但虚构的新样本。这有效避免了传统方法在一对一映射到原始数据集时所面临的风险。从整体上看,统计特性得以保留,这意味着我们可以像操作真实数据一样与这些合成数据集进行互动,无论是用于计算汇总统计还是训练机器学习模型。

合成数据将为多个行业创造价值

使用 AI 生成的合成数据为隐私受限的企业提供了解决方案,使得数据共享成为可能,这在过去由于隐私问题而困难重重。这些行业包括但绝不限于:

  • 医疗健康: 目前,研究人员通常面临着访问真实患者数据的漫长而繁琐的过程,极大地拖慢了医学进展的步伐。合成医疗记录为加速医学研究并保护患者隐私提供了变革性的解决方案。此外,生成合成数据为解决医疗数据集中的偏差提供了有效途径,通过有意增强代表性不足的群体,从而促进更具包容性的研究成果。

  • 金融服务: 交易数据本质上是敏感且可识别的,这在金融领域带来了独特的挑战。合成数据成为关键解决方案,能够在有效解决隐私问题的同时,实现内部和外部数据共享。此外,合成数据还可以用于增强有限或不平衡的数据集,特别是在增强欺诈检测和反洗钱工作方面具有重要作用。

一般来说,所有企业都可以利用合成数据集来提升隐私保护,我们鼓励你思考合成数据如何具体对你产生帮助。为了帮助你理解合成数据的潜力,我们提供了一些精选的应用案例:

  • 第三方共享: 在公司需要第三方分析客户或用户数据的场景中,合成数据集提供了一个可行的替代方案,以避免共享敏感信息。这种方法在选择阶段特别有益,尤其是在评估多个外部合作伙伴时,或者为了启用项目的快速启动,绕过共享真实数据所需的耗时法律程序。

  • 内部数据共享: 即使在内部,处理敏感信息的共享,如员工和人力资源数据,往往由于严格的法规而具有挑战性。合成数据提供了一种解决方案,使公司领导能够在确保员工隐私的前提下,改善内部知识转移和数据共享。这一方法对于处理含有敏感客户信息的数据集同样具有优势。通过使用合成数据,组织可以更广泛地在公司内部分发这些数据集。通过扩展共享,这种方法使更多的组织成员能够参与问题解决和决策制定,从而提升整体效率与协作,同时充分尊重隐私。

  • 延长数据洞察的保留时间: 根据 GDPR 的严格规定,组织需要在用户数据完成预定处理目的后,或根据用户请求删除数据。然而,这一合规要求可能带来丧失数据中宝贵洞察的风险。合成数据为这一挑战提供了创新的解决方案。它在遵守法律要求的同时,保留了原始数据的本质和实用性,从而确保数据的价值得以保留,用于未来的分析和人工智能驱动的工作。

将合成数据与隐私增强测试和技术结合起来是未来的强大推动力。

合成数据作为解决数据隐私和可访问性挑战的有前景的解决方案,尽管如此,它并非完美无缺。生成模型的准确性至关重要;如果模型调校不当,可能导致合成数据无法充分反映现实条件,或者在某些情况下,合成数据过于接近原始数据集,从而危及隐私。意识到这一点,已经开发出强有力的方法来验证合成数据的输出质量,既考虑其实用性,也考虑隐私性。这些关键的评估对于有效利用合成数据至关重要,确保敏感信息不会无意中泄露。大多数知名的合成数据提供商都认识到这一必要性,并在其过程中内建了质量保证措施。

一个有前景的增强方式是将差分隐私与合成数据生成器相结合。差分隐私是隐私的严格数学定义,如果正确使用,它能提供强有力的保障,确保在统计分析过程中个人隐私得到保护。

差分隐私模型是通过在训练或推理过程中融入差分隐私技术来设计的机器学习模型,旨在保护隐私。

这对于包含明显异常值的数据集,或者需要保证更高隐私级别的情况特别有利。差分隐私模型还支持共享数据生成器模型本身,而不仅仅是其生成的合成数据。然而,重要的是要强调,这种共享需要在模型训练过程中应用差分隐私方法。相比之下,标准数据生成器通常不能安全共享,因为它们在接受旨在提取信息的先进机器学习技术时,可能无意中泄露敏感信息。

图 2:使用 Netflix 示例可视化合成数据生成中的差分隐私。图表由作者创建。

在图 2 中,我们以 Netflix 数据集为例,展示了差分隐私模型的原理。这里的核心思想是,通过差分隐私数据生成器来限制单个数据记录对学习数据分布的影响。简单来说,如果我们在同一数据集上重新训练模型,去除某个个体的数据,那么最终的数据显示不会有显著的偏差。单个观测值的最大影响是差分隐私模型的一个可量化参数。这导致了隐私和效用之间的权衡,但通常可以找到一个令人满意的折中方案,确保隐私和效用都能得到充分的保障。

AI 生成的合成数据已经准备好迎接主流应用

合成数据正在迅速确立自己作为增强隐私的重要技术,预计将在现代数据管理中成为主流。它的作用不仅仅是保护隐私,还能作为通向丰富的未开发数据潜力的桥梁——这一前景正被许多具有前瞻性的企业所利用。在本文中,我们突出了合成数据在促进安全数据共享方面的优势。然而,它在数据增强中的潜力也许更令人兴奋。通过支持数据填充和重平衡,合成数据可以显著提高机器学习算法的效率,有效地以最小的成本和努力提供显著的附加价值。我们邀请您探索合成数据如何在多种方式上转变您的业务运营。

关于作者

Arne Rustad 是奥斯陆 BearingPoint 的数据科学家,参与多个生成型人工智能项目,拥有丰富经验。他的硕士论文研究了表格数据的合成数据生成,提出了一种新的生成对抗网络(GAN)模型,达到了最先进的性能。Arne 获得了挪威科技大学(NTNU)物理学与数学硕士学位。电子邮件地址:arne.rustad@bearingpoint.com。

Helene Semb 是奥斯陆 BearingPoint 的数据科学家,拥有计算机视觉和物体检测方面的机器学习经验。Helene 最近获得了挪威科技大学(NTNU)控制学与机器人学硕士学位。电子邮件地址:helene.semb@bearingpoint.com。

参考文献

[1] L. Sweeney, 简单的 demographic 信息通常能唯一识别个人(2000 年),《健康》(旧金山)

[2] L. Rocher, J. Hendrickx 和 Y. De Montjoye, 使用生成模型估计在不完整数据集中的重新识别成功率(2019 年),《自然通讯》

[3] Wired, Netflix 因隐私诉讼取消推荐竞赛(2010 年 3 月 12 日)

[4] Gartner, 合成数据是人工智能的未来吗?(2022 年 6 月 22 日)

向 Plotly 图表添加渐变背景。

原文:towardsdatascience.com/adding-gradient-backgrounds-to-plotly-charts-1dec85dd3227?source=collection_archive---------10-----------------------#2024-10-08

使用 Plotly 矩形形状来改善数据可视化。

Andy McDonaldTowards Data Science Andy McDonald

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

--

使用 Plotly Express 绘制带有渐变背景的折线图,强调高温和低温。图片来源:作者。

向图表的背景添加渐变或分段可以帮助提高你所展示信息的清晰度。它可以帮助吸引注意力到高温或低温区域,或者用于识别某个测量值是否超出公差范围。例如,如果 LWD 井下测量工具的振动超过该工具的规格限制,或者它可以用于温度数据,突出显示热区和冷区。

在本文中,我将分享如何通过简单的代码,为 Plotly Express 折线图添加渐变背景。

创建数据

在开始绘制图表之前,我们首先需要创建一个数据集。在这个示例中,我使用的是随机生成的温度数据,跨越一段时间。

首先,我们需要导入将要使用的库。

  • plotly express 将被用来生成我们的图表。

  • matplotlib 将被用来生成我们的颜色图并将选定的值转换为十六进制。

  • numpy 将被用来帮助创建我们的颜色刻度。

import plotly.express as px
import matplotlib.pyplot as plt…

向数据科学应用添加时间弹性

原文:towardsdatascience.com/adding-temporal-resiliency-to-data-science-applications-6b052d7b81af?source=collection_archive---------5-----------------------#2024-03-26

我们的应用能否在不花费任何冗余资源的情况下,获得冗余所带来的可靠性好处?

Rohit PandeyTowards Data Science Rohit Pandey

·发布于 Towards Data Science ·13 分钟阅读·2024 年 3 月 26 日

--

图片来自 midjourney

现代应用几乎完全将其状态存储在数据库中,并从数据库中读取执行任务所需的状态。我们将关注如何为从这些数据库读取和写入的过程添加弹性,使其变得高度可靠。

实现这一目标的明显方法是提高组成数据库的硬件和软件的质量,使我们的读写操作从不失败。但这会变成一个递减效益的法则:一旦我们已经达到较高的可用性,投入更多资金所带来的变化仅是微不足道的。添加冗余来实现高可用性很快就变成了一个更好的策略。

那么,通过向架构中添加冗余来实现高可靠性到底是什么样的呢?我们通过花费更多资金在冗余系统上来消除单点故障。例如,保持数据的冗余副本,以便如果一个副本损坏或丢失,其他副本可以用来修复。另一个例子是拥有冗余数据库,当主数据库不可用时,可以从中读取和写入数据。我们将这些解决方案称为……

加性决策树

原文:towardsdatascience.com/additive-decision-trees-85f2feda2223?source=collection_archive---------4-----------------------#2024-05-24

可解释的分类和回归模型

W Brett KennedyTowards Data Science W Brett Kennedy

·发表于 Towards Data Science ·阅读时间 20 分钟·2024 年 5 月 24 日

--

本文是与可解释预测模型相关的系列文章的一部分,这篇文章介绍了名为加性决策树的模型类型。前一篇文章描述了 ikNN,它是 kNN 模型的一种可解释变体,基于二维 kNN 的集成方法。

加性决策树是标准决策树的一种变体,构造方式类似,但通常可以使它们更准确、更易解释,或者两者兼具。它们包含一些比标准决策树节点稍微复杂的节点(尽管通常只是略微复杂),但往往可以通过更少的节点构造,从而使整体树更加易于理解。

主要项目是:github.com/Brett-Kennedy/AdditiveDecisionTree。提供了加性决策树分类器(AdditiveDecitionTreeClassifier)和加性决策树回归器(AdditiveDecisionTreeRegressor)类。

加性决策树的提出源于缺乏可解释的分类和回归模型。可解释模型在许多场景中都很重要,包括高风险环境、审计环境(我们必须清楚地理解模型如何行为)、以及确保模型不对保护类群体存在偏见的情况(例如,基于种族或性别进行歧视)等。

如在关于 ikNN 的文章中所述,目前有一些可解释的分类器和回归模型(如标准决策树、规则列表、规则集、线性/逻辑回归等少数几种),但远少于理想情况下的数量。

标准决策树

最常用的可解释模型之一是决策树。它通常表现良好,但并非在所有情况下都如此。决策树可能无法始终达到足够的准确度,且在能够达到时,也可能无法合理地认为其是可解释的。

决策树通常只有在被构建为较大规模时才能达到较高的准确性,这会剥夺其可解释性。一个具有五六个叶节点的决策树是相当可解释的;但一个具有一百个叶节点的决策树几乎变成了黑箱。尽管与神经网络或提升模型相比,决策树可能更具可解释性,但当叶节点数量非常多时,完全理解决策树的预测就变得非常困难,尤其是每个叶节点可能都与相当长的决策路径相关联。这正是加法决策树设计的主要问题所在。

加法决策树还解决了决策树的其他一些著名局限性,特别是它们的稳定性较差(训练数据中的小差异可能导致截然不同的树),它们在树的低层次基于越来越少的样本进行划分,需要重复子树,以及如果不加以限制或修剪,容易过拟合的问题。

进一步考虑在树的低层次上基于越来越少的样本进行划分的问题:这是由于决策树使用的划分过程的性质;数据空间在每次划分时被分为不同的区域。根节点涵盖了训练数据中的每一条记录,而每个子节点涵盖其中的一部分。它们的每个子节点又涵盖其中的一部分,依此类推。基于此,树的低层次划分变得越来越不可靠。

这些局限性通常通过集成决策树来解决,可以通过袋装(例如随机森林)或提升(例如 CatBoost、XGBoost 和 LGBM)实现。集成方法会产生不可解释的模型,尽管通常更准确。其他提高决策树稳定性和准确性的技术包括构建盲树(例如,CatBoost 中就是这样做的)和斜决策树(即在数据空间中以斜角进行划分,而非决策树通常使用的轴平行划分)。

由于决策树可能是最常用的可解释模型,或者至少是最常用的模型之一,我们在准确性和可解释性方面的比较都是相对于标准决策树进行的。

加法决策树简介

加法决策树并不总是优于决策树,但它通常能表现得更好,且在需要可解释模型的情况下通常值得进行测试。在某些情况下,它们可能提供更高的准确性,在某些情况下提高了解释性,在许多情况下则两者兼具。迄今为止的测试表明,这对于分类任务比回归任务更为成立。

加性决策树并不旨在与提升方法(boosting)或神经网络在准确度上竞争,而只是作为一种生成可解释模型的工具。它们的吸引力在于,它们通常能生成与更深层次的标准决策树相当的模型,同时相较于这些树,整体复杂度较低,通常大大较低。

加性决策树背后的直觉

加性决策树背后的直觉是,通常真实的函数,f(x),将输入 x 映射到目标 y,是基于逻辑条件的(具有 IF-ELSE 逻辑,或者可以用 IF-ELSE 逻辑近似);而在其他情况下,它只是一个概率函数,其中每个输入特征可能被相对独立地考虑(就像朴素贝叶斯假设一样)。

真实的 f(x)可能具有不同类型的特征交互:即一个特征的值影响其他特征如何与目标相关。这些交互在不同的数据集中可能更强或更弱。

例如,真实的 f(x)可能包含如下内容:

真实的 f(x)示例 1

If      A > 10     Then: y = class Y 
Elseif  B < 19     Then: y = class X
Elseif  C * D > 44 Then: y = class Y
Else                     y = class Z

这是第一个案例的一个例子,其中真实的 f(x)由逻辑条件组成,并且可以准确(且简单地)表示为一系列规则,例如决策树(如下所示)、规则列表或规则集。

A > 10
| - LEAF: y = class Y
| - B > 19
    | (subtree related to C*D omitted)
    | - LEAF: y = class X

在这里,可以创建一个简单的树来表示与特征 A 和 B 相关的规则。

但是,涉及到 C*D 的规则将生成一个非常大的子树,因为每次分裂时,树可能仅根据 C 或 D 之一进行分裂。例如,对于 C 值大于 1.0 时,D 值大于 44 将导致类别为 Y;对于 C 值大于 1.1 时,D 值大于 40 将导致类别为 Y;对于 C 值大于 1.11 时,D 值大于 39.64 将导致类别为 Y。必须为 C 和 D 的所有组合计算这些值,直到训练数据的规模允许的最细粒度为止。子树可能是准确的,但会非常庞大,几乎接近无法理解。

另一方面,真实的 f(x)可能是与概率相关的一组模式,更像是以下的形式:

真实的 f(x)示例 2

The higher A is, the more likely y is to be class X and less likely to be Z
regardless of B, C, and D

The higher B is, the more likely y is to be class Y and less likely to be X, 
regardless of A, C, and D

The lower C is, the more likely y is to be class Z and less likely to be X, 
regardless of A, B, and D 

在这里,类别的预测完全基于与每个特征相关的概率,且没有特征之间的交互。在这种函数形式中,对于每个实例,每个特征值都会对目标值贡献一些概率,这些概率会被加总以确定整体的概率分布。

在这里,无法创建一个简单的树。这里有三个目标类别(X、Y 和 Z)。如果 f(x)更简单,仅包含与特征 A 相关的规则:

The higher A is, the more likely y is to be class X and less likely to be Z
regardless of B, C, and D

我们可以基于 A 中的分裂点创建一棵小树,其中每个类别的概率最大。这可能只需要少量的节点:这棵树可能首先在 A 的大约中点分裂,然后每个子节点会再次将 A 分裂成大约一半,依此类推,直到我们得到一棵树,其中每个节点指示 X、Y 或 Z 是最可能的类别。

但是,由于有三条这样的规则,尚不清楚哪一条会首先通过分裂表示。如果我们首先按特征 B 进行分裂,我们需要在每个子树中处理与特征 A 和特征 C 相关的逻辑(在树中多次重复这些逻辑)。如果我们首先按特征 B,然后按特征 A,再按特征 C 分裂,那么当我们确定特征 C 的分裂点时,可能会有足够少的记录被节点覆盖,以至于分裂点会选择在次优的点上。

示例 2 可能(在足够的训练数据下)可以通过决策树以相当高的准确率表示,但这棵树会相当大,且分裂点可能不太容易理解。树的下层,分裂点变得越来越难以理解,因为它们仅仅是在三个相关特征中最能划分数据的分裂点,而每个下层节点的训练数据越来越少。

在示例 3 中,我们有一个类似的 f(x),但其中包含一些特征交互,形式为条件和乘法:

真实的 f(x) 示例 3

The higher A is, the more likely y is to be class X, 
regardless of B, C and D

The higher B is, up to 100.0, the more likely y is class Y, 
regardless of A, C and D 

The higher B is, where B is 100.0 or more, the more likely y is to be class Z, 
regardless of A, C and D

The higher C * D is, the more likely y is class X, 
regardless of A and B.

这是示例 1 和示例 2 中思想的结合。在这里,我们既有基于特征 B 值的条件,又有特征与每个目标类的概率独立相关的情况。

虽然还有其他方法可以对函数进行分类,但该系统是有用的,许多真实函数可能被视为这些方法的某种组合,介于示例 1 和示例 2 之间。

标准决策树并不明确假设真实的函数类似于示例 1,并且能够准确地(通常通过使用非常大的树)捕捉到非条件性关系,例如基于概率的关系(更像示例 2 或示例 3 的情况)。然而,它们将函数建模为条件,这可能会限制它们的表达能力并降低其可解释性。

加法决策树去除了标准决策树中假设 f(x) 可能最好通过一组条件建模的假设,但支持在数据表明存在时的条件。核心思想是,真实的 f(x) 可能基于逻辑条件、概率(加法的、独立的规则)或这些的某种组合。

一般来说,标准决策树在真实的 f(x) 类似于示例 1 时,可能会表现得非常好(在可解释性方面)。

当真实的 f(x) 类似于示例 2 时,我们可能更适合使用线性回归或逻辑回归、朴素贝叶斯模型、广义加法模型(GAM)或其他基于每个独立特征的加权总和进行预测的模型。然而,这些模型在处理类似示例 1 的函数时可能表现不佳。

加法决策树能够适应这两种情况,尽管在真实的 f(x) 介于两者之间时,如示例 3,可能表现最佳。

构建加法决策树

我们在这里描述了加法决策树的构建过程。对于分类问题,这个过程更容易呈现,因此示例与此相关,但这些思想同样适用于回归问题。

加法决策树所采用的方法是使用两种类型的划分。

首先,在适当的情况下,它可能会像标准决策树一样对数据空间进行划分。与标准决策树一样,加法决策树中的大多数节点表示完整空间的一个区域,根节点表示整个空间。每个节点根据一个特征的划分点将该区域分为两部分。这样就会产生两个子节点,每个子节点覆盖父节点所覆盖的空间的一部分。例如,在示例 1 中,我们可能有一个节点(根节点),根据特征 A 在 10 处将数据划分。A 小于或等于 10 的行会进入一个子节点,而 A 大于 10 的行则进入另一个子节点。

其次,在加法决策树中,划分可能基于多种潜在的划分(每个都是针对单一特征和划分点的标准划分)的聚合决策。也就是说,在某些情况下,我们并不依赖单一的划分,而是假设在给定节点处可能有多个有效的特征可以进行划分,并取这些划分方式的平均值。在这种划分方式下,没有其他节点在下方,因此这些节点成为叶节点,称为加法节点

构建加法决策树的方式是,第一种类型的划分(基于单一特征的标准决策树节点)出现在树的较高位置,在这里有更多的样本可以用于基于划分,并且这些划分可以更可靠地找到。在这些情况下,依赖单一特征的单一划分更为合理。

第二种类型(基于多个划分聚合的加法节点)出现在树的较低位置,此时可依赖的样本较少。

一个示例是,创建一个树来表示示例 3,可能会产生如下的加法决策树:

if B > 100:
  calculate each of and take the average estimate:
  if A <= vs > 50: calculate the probabilities of X, Y, and Z in both cases
  if B <= vs > 150: calculate the probabilities of X, Y, and Z in both cases
  if C <= vs > 60: calculate the probabilities of X, Y, and Z in both cases
  if D <= vs > 200: calculate the probabilities of X, Y, and Z in both cases
else (B <= 100):
  calculate each of and take the average estimate:
  if A <= vs > 50: calculate the probabilities of X, Y and Z in both cases
  if B <= vs > 50: calculate the probabilities of X, Y and Z in both cases
  if C <= vs > 60: calculate the probabilities of X, Y and Z in both cases
  if D <= vs > 200: calculate the probabilities of X, Y and Z in both cases

在这个示例中,我们在根节点有一个正常节点,它在 100 处分裂了特征 B。在它下面有两个加法节点(这些节点始终是叶节点)。在训练过程中,我们可能会确定,基于特征 A、B、C 和 D 进行分裂都是有效的;虽然选择其中一个可能看起来比其他选择稍微有效一些,但究竟选择哪一个是有些任意的。在训练标准决策树时,通常是根据训练数据中的细微变化来选择分裂点。

将此与标准决策树进行比较:标准决策树会选择第一个节点中的四个可能分裂点中的一个,并且也会选择第二个节点中的四个可能分裂点中的一个。在第一个节点中,如果它选择了例如特征 A(在 50 处分裂),那么这个节点将被分裂成两个子节点,这些子节点可以继续被分裂成更多的子节点,依此类推。这种方式可以很好地工作,但分裂是基于越来越少的数据行进行的。而且,可能不需要将数据分割成更细的空间:真正的 f(x)可能没有条件逻辑。

在这种情况下,加法决策树检查了四个可能的分裂点,并决定采用所有四个分裂点。这些节点的预测将基于加总每个分裂点的预测结果。

这种方法的一个主要优势是:四个分裂点中的每一个都是基于该节点中可用的完整数据;每个分裂点的准确性尽可能高,基于该节点中的训练数据。我们还避免了在其下可能出现的非常大的子树。

在预测时到达这些节点时,我们会将预测结果相加。例如,如果一个记录的 A、B、C 和 D 的值分别为:[60, 120, 80, 120],那么当它到达第一个节点时,我们将 B(120)的值与分裂点 100 进行比较。B 大于 100,因此我们进入第一个节点。现在,代替另一个分裂点,这里有四个分裂点。我们根据 A、B、C、以及D 的值进行分裂。也就是说,我们基于所有四个分裂点计算预测结果。在每一种情况下,我们都会得到一个关于 X、Y 和 Z 类的概率集合。我们将这些概率加在一起,得到每个类的最终概率。

第一个分裂点是基于 A,在 50 处分裂。该行的值为 60,因此与这个分裂相关的每个目标类别(X、Y 和 Z)都有一组概率。第二个分裂点是基于 B,在 150 处分裂。B 的值为 120,因此与此分裂相关的每个目标类别又有一组概率。对于加法节点中的其他两个分裂也是类似的。我们找到每个分裂的预测结果,并将它们加在一起,得到该记录的最终预测。

这就提供了一种简单的集成方法决策树内部。我们获得了集成的正常好处:更准确和稳定的预测,同时实际提高了可解释性。

这看起来可能会创造一个更复杂的树,从某种意义上讲,它确实是这样:加法节点比标准节点更复杂。但加法节点通常聚合相对较少的分裂(通常是两到五个)。而且,它们也消除了在其下方存在大量节点的需求。复杂度的整体减少通常是相当显著的。

标准决策树的可解释性

在标准决策树中,全局解释(对模型本身的解释)以树的形式呈现:我们只需以某种方式呈现(例如 scikit-learn 的 plot_tree()或 export_text()方法)。这使我们能够理解对于任何未见过的数据将产生的预测。

局部解释(对单个实例的预测解释)以决策路径的形式呈现:从根节点到叶节点的路径,实例最终落在叶节点上,路径中的每个分裂点都指向这个最终的决策。

决策路径可能难以解释。决策路径可能非常长,可能包括与当前预测无关的节点,而且有些节点是相对任意的(在训练过程中,决策树选择了某个分裂,但可能还有多个其他同样有效的分裂)。

加法决策树的可解释性

加法决策树的解释方式与标准决策树基本相同。唯一的区别是加法节点,在这些节点中有多个分裂,而不是一个。

聚合在一起的最大分裂数量是可配置的,但通常 4 或 5 个就足够了。在大多数情况下,所有分裂一致,且只需要向用户展示其中一个。事实上,即使分裂不一致,最多数的预测也可能作为单一分裂呈现。因此,解释通常与标准决策树相似,但决策路径较短。

这样,就产生了一个模型,其中有少数标准(单一)分裂,理想情况下表示模型中的真实条件(如果有的话),然后是加法节点,这些是叶节点,负责平均多个分裂的预测,提供更稳健的预测。这减少了将数据拆分为越来越小的子集的需求,每个子集的统计意义较小。

修剪算法

加法决策树首先构建标准决策树。然后,它们运行修剪算法,试图减少节点的数量:通过将许多标准节点合并为一个节点(加法节点),该节点聚合预测。其理念是:当树中的某个节点,或树中的子树有许多节点时,这可能是因为树试图专注于一个预测,同时平衡许多特征的影响。

该算法的行为类似于大多数修剪算法,从底部开始,从叶节点开始,向根节点方向工作。在每个节点上,做出决策,要么保持节点不变,要么将其转换为加法节点;即,合并多个数据分裂的节点。

在每个节点上,评估树在当前分裂下的训练数据准确性,然后再次将该节点视为加法节点。如果将该节点设为加法节点时准确性更高,则将其设置为加法节点,并移除其下方的所有节点。如果一个上层节点被转换为加法节点,这个节点本身也可能会被移除。测试表明,相当大比例的子树通过这种方式进行聚合后,能够获得显著的提升。

评估测试

为了评估工具的有效性,我们考虑了准确性(分类任务的宏 F1 分数;回归任务的标准化均方根误差(NRMSE))和可解释性,后者通过树的大小来衡量。关于复杂度度量的详细信息在下文中提供。更多评估测试的细节请参见 GitHub 页面。

为了进行评估,我们将加法决策树与标准决策树进行了比较,分别在两种情况下进行比较:一种是两种模型使用默认超参数,另一种是两种模型都使用网格搜索来估算最佳参数。我们使用了从 OpenML 随机选择的 100 个数据集。

这项工作使用了一个名为DatasetsEvaluator的工具,尽管没有这个工具,实验也能轻松重现。DatasetsEvaluator 只是一个方便的工具,用来简化这种测试并消除选择测试数据集时的任何偏差。

在 100 个数据集上的分类结果

这里的“DT”指的是 scikit-learn 的决策树,“ADT”指的是加法决策树。训练-测试差距是通过从测试集的 F1 宏分数中减去训练集的 F1 宏分数来计算的,用于估计过拟合程度。加法决策树模型的过拟合程度明显较低。

加法决策树在准确性方面与标准决策树非常相似。有很多情况下,标准决策树表现更好,也有许多情况下加法决策树表现更好,另外还有一些情况两者表现差不多。加法决策树所需的时间比标准决策树长,但仍然非常短,平均约为 4 秒。

主要的区别在于生成树的复杂性。

以下图表比较了 100 个数据集上的准确性(上面一栏)和复杂性(下面一栏),这些数据集按标准决策树的准确性从低到高排列。

上方的图表跟踪了 x 轴上的 100 个数据集,y 轴为 F1 分数(宏平均)。分数越高越好。我们可以看到,在右侧,两种模型都非常准确。左侧则有一些情况,决策树(DT)表现不佳,但加性决策树(ADT)在准确性方面表现更好。我们还可以看到,在一些情况下,从准确性的角度来看,使用标准决策树显然更合适,而在另一些情况下,使用加性决策树显然更合适。在大多数情况下,最好尝试两者(以及其他模型类型)。

第二张图表跟踪了相同的 100 个数据集,x 轴为模型复杂度,y 轴为模型复杂度。分数越低越好。在这种情况下,ADT 一直比 DT 更具可解释性,至少在这里使用的当前复杂度度量中如此。在所有 100 个案例中,产生的树更加简单,且经常更为简单。

示例

加性决策树遵循标准的 sklearn fit-predict API 框架。通常,如本例所示,我们创建一个实例,调用 fit() 方法,然后调用 predict() 方法。

from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from AdditiveDecisionTree import AdditiveDecisionTreeClasssifier

iris = load_iris()
X, y = iris.data, iris.target
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
adt = AdditiveDecisionTreeClasssifier()
adt.fit(X_train, y_train)
y_pred_test = adt.predict(X_test)

Github 页面还提供了示例笔记本,涵盖了模型的基本使用和评估。

加性决策树提供了两个额外的 API 以提高可解释性:output_tree() 和 get_explanations()。output_tree() 提供类似于 scikit-learn 中使用 export_text() 的决策树视图,但提供了更多信息。

get_explanations 提供了针对指定行集的本地解释(以决策路径的形式)。在这里,我们获取前五行的解释。

exp_arr = adt.get_explanations(X[:5], y[:5])
for exp in exp_arr: 
    print("\n")
    print(exp)

第一行的解释是:

Initial distribution of classes: [0, 1]: [159, 267]

Prediction for row 0: 0 -- Correct
Path: [0, 2, 6]

mean concave points is greater than 0.04891999997198582 
  (has value: 0.1471) --> (Class distribution: [146, 20]

AND worst area is greater than 785.7999877929688 
  (has value: 2019.0) --> (Class distribution: [133, 3]

where the majority class is: 0

从第一行我们可以看到有两个类别(0 和 1),在训练数据中类别 0 有 159 个实例,类别 1 有 267 个实例。

根节点始终是节点 0。该行通过节点 0、2 和 6,基于‘平均凹点数’和‘最差区域’的值。关于这些节点的信息可以通过调用 output_tree() 获得。在这个例子中,路径上的所有节点都是标准决策树节点(没有加性节点)。

在每个阶段,我们看到两类的计数。在第一次切分后,我们进入一个类别 0 更可能出现的区域(146 对 20)。再经过一次切分后,类别 0 更为可能(133 对 3)。

下一个示例展示了一个预测示例,其中一行通过了一个加性节点(节点 3)。

Initial distribution of classes: [0, 1]: [159, 267]

Prediction for row 0: 1 -- Correct
Path: [0, 1, 3]

mean concave points is less than 0.04891999997198582 
  (has value: 0.04781) --> (Class distribution: [13, 247]

AND worst radius is less than 17.589999198913574 
  (has value: 15.11) --> (Class distribution: [7, 245]

AND  vote based on: 
  1: mean texture is less than 21.574999809265137 
    (with value of 14.36)  --> (class distribution: [1, 209])
  2: area error is less than 42.19000053405762 
    (with value of 23.56)  --> (class distribution: [4, 243])
The class with the most votes is 1

最后一节点是一个加性节点,基于两个切分。在这两个切分中,预测强烈偏向类别 1(从 1 到 209 和从 4 到 243)。因此,最终的预测结果是类别 1。

可解释性指标

上述评估基于模型的全局复杂度,即树的整体大小,加上每个节点的复杂度。

还可以查看平均局部复杂度(每个决策路径的复杂度:路径的长度加上决策路径上节点的复杂度)。使用平均局部复杂度也是一个有效的度量标准,加性决策树在这方面表现良好。但为了简化,我们在这里关注模型的全局复杂度。

对于标准决策树,评估仅使用节点数量(这是衡量决策树复杂度的常见指标,尽管通常也使用其他指标,例如叶节点数量)。对于加性树,我们也这样做,但对于每个加性节点,我们会根据该节点上聚合在一起的分裂次数来计数。

因此,我们衡量特征值与阈值的总比较次数(即分裂次数),无论这些比较发生在多个节点还是单个节点中。未来的工作将考虑额外的度量标准。

例如,在标准节点中,我们可能有像 Feature C > 0.01 这样的分裂,这算作一次。在加性节点中,我们可能有多个分裂,例如 Feature C > 0.01Feature E > 3.22Feature G > 990,这算作三次。这似乎是一个合理的度量标准,尽管尝试量化不同模型形式的认知负担是出了名的困难且主观。

加性决策树在 XAI 中的应用

除了作为可解释模型使用外,加性决策树还可以被视为一个有用的 XAI(可解释人工智能)工具——加性决策树可以作为代理模型,从而提供对黑箱模型的解释。这是 XAI 中的常见技术,其中训练一个可解释的模型来预测黑箱模型的输出。通过这种方式,代理模型可以提供易于理解的解释,尽管只是近似的,且解释黑箱模型产生的预测。通常,适合作为可解释模型使用的模型也可以用作代理模型。

例如,如果训练了一个 XGBoost 模型来预测某个目标(例如股价、天气预报、客户流失等),该模型可能是准确的,但我们可能不知道为什么模型会做出这些预测。我们可以训练一个可解释的模型(包括标准决策树、加性决策树、ikNN、GAM 等),以可解释的方式预测 XGBoost 的预测结果。这不会完美,但当代理模型能够合理准确地预测 XGBoost 模型的行为时,它提供的解释通常是大致正确的。

安装

源代码提供在一个单独的 .py 文件中,AdditiveDecisionTree.py,可以包含在任何项目中。它没有使用任何非标准库。

结论

尽管最终的树可能比深度相同的标准决策树更复杂,但加法决策树在精度上优于深度相同的标准决策树,并且比精度相同的标准决策树更简单。

与所有可解释模型一样,加法决策树并不旨在与当前最先进的表格数据模型(如提升模型)在精度上竞争。然而,加法决策树在精度和可解释性方面与大多数其他可解释模型具有竞争力。虽然没有一种工具是最佳的,但在可解释性重要的场合,通常值得尝试几种工具,包括加法决策树。

所有图片均由作者提供。

解决人工智能中来自合成数据的模型崩溃问题

原文:towardsdatascience.com/addressing-concerns-of-model-collapse-from-synthetic-data-in-ai-7cd380208d14?source=collection_archive---------4-----------------------#2024-08-05

Alexander WatsonTowards Data Science Alexander Watson

·发表于Towards Data Science ·阅读时间 8 分钟·2024 年 8 月 5 日

--

人工智能领域正在快速发展,合成数据作为模型开发的有力工具应运而生。虽然它提供了巨大的潜力,但关于模型崩溃的近期关注引发了争论。让我们深入探讨合成数据的使用现实及其对人工智能发展的影响。

由 DALL-E 生成的图像

解决模型崩溃问题

《自然》期刊上 Shumailov 等人发表的论文“AI 模型在递归生成数据的训练下崩溃”提出了关于使用合成数据的重要问题:

  • “我们发现, 不加区分地使用 模型生成的内容进行训练会导致结果模型中的不可逆缺陷,在这些模型中,原始内容分布的尾部消失。” [1]

  • “我们认为模型崩溃的过程在所有生成模型中都是普遍存在的,这些模型 递归训练 于由前一代生成的数据” [1]

然而,需要注意的是,这种纯粹基于合成数据的递归训练极端情境并不代表现实世界中的人工智能开发实践。作者们自己也承认:

  • “在这里,我们探讨当语言模型被顺序微调,且使用由其他模型生成的数据时,会发生什么…我们评估了训练语言模型的最常见设置——一个微调设置,其中每个训练周期都从一个预训练模型开始,使用最新的数据” [1]

关键要点

  1. 该研究的方法论未考虑到源源不断的新数据流入,这一特点是现实世界 AI 模型训练的一个重要特征。这一局限性可能导致在实际场景中对模型崩溃的高估,而在这些场景中,新的数据可以作为潜在的纠正机制,防止模型退化。

  2. 实验设计中丢弃前代数据的做法与 AI 开发中的常规实践相悖,后者涉及到累积学习和复杂的数据策划。这种做法可能无法准确反映行业应用中典型的知识保持和积累过程。

  3. 在各代之间使用单一静态模型架构(OPT-125m)并未反映出实际中AI 架构的快速演变。这种简化可能夸大了观察到的模型崩溃,因为它没有考虑到架构进展如何潜在地缓解这些问题。实际上,该领域已见证了快速的发展(例如,从 GPT-3 到 GPT-3.5 再到 GPT-4,或从 Phi-1 到 Phi-2 再到 Phi-3),每一次迭代都在模型容量、泛化能力和突现行为上带来了显著的改进。

  4. 尽管论文承认灾难性遗忘问题,但并未采用行业中常用的标准缓解技术,如弹性权重整合(Elastic Weight Consolidation)或经验回放。这一遗漏可能加剧了观察到的模型崩溃效应,限制了研究在实际场景中的适用性。

  5. 研究中合成数据生成和使用的方法缺乏行业中常用的质量控制措施和整合实践。这一方法选择可能导致在实际应用中对模型崩溃风险的高估,而在实际应用中,合成数据通常会更仔细地进行筛选并与真实世界的数据结合。

论文中的支持性引述

  • “我们还简要提到现有文献中与模型崩溃相关的两个相近概念:任务无关的持续学习中的灾难性遗忘和恶意数据中毒导致的非预期行为” [1]

实际上,合成数据的目标是扩充和扩展现有数据集,包括基础模型中内嵌的隐式数据。当团队进行微调或继续预训练时,目标是提供额外的数据以提高模型的鲁棒性和性能。

来自学术界和研究领域的反驳

由 Gerstgrasser 等人(斯坦福大学、麻省理工学院和 Constitution 的研究人员)撰写的论文《模型崩溃是不可避免的吗?通过积累真实和合成数据打破递归的诅咒》提出了对 AI 模型崩溃担忧的显著反驳:

我们的工作提供了一致的实证和理论证据,表明数据积累避免了模型崩溃.” [2]

来源:《模型崩溃是否不可避免?通过积累真实与合成数据打破递归的诅咒》。 [2]

这项工作表明,将合成数据与现实世界数据结合可以防止模型退化。

质量重于数量

正如在微软的 Phi-3 技术报告中强调的:

  • “构建一个强大且全面的数据集不仅仅依赖于原始计算能力:它需要复杂的迭代、战略性的主题选择,并深刻理解知识空白,以确保数据的质量和多样性。”[3]

这强调了深思熟虑的合成数据生成的重要性,而不是盲目使用。

以及苹果在训练其新设备和基础模型时

  • “我们发现数据质量对于模型的成功至关重要,因此我们在训练管道中采用混合数据策略,结合人工标注数据和合成数据,并进行彻底的数据策划和过滤程序。”[10]

这强调了深思熟虑的合成数据生成的重要性,而不是盲目使用。

迭代改进,而非递归训练

正如在 Gretel Navigator、NVIDIA 的 Nemotron 和 AgentInstruct 架构中所强调的,前沿的合成数据是通过代理人迭代地模拟、评估和改进输出生成的——而不是仅仅递归地在自己的输出上进行训练。以下是 AgentInstruct 中使用的合成数据生成架构示例。

来源:AgentInstruct 合成数据生成架构[11]

合成数据改善模型性能

以下是最近合成数据发布的一些示例结果:

行业通过合成数据的进展

合成数据正在推动各行业的重大进展:

医疗保健: SA Health 的首席临床官 Rhys Parker 表示:

我们与 Gretel 合作的合成数据方法,已经彻底改变了我们处理敏感病患信息的方式。以往需要几个月甚至几年的数据请求,现在几天内就能完成。这不仅仅是技术的进步,更是健康数据管理的根本变革,在确保隐私的同时,显著改善了病患护理。我们预测,合成数据将在未来几年内成为医学研究中的常规工具,为医疗创新开辟新天地。” [9]

数学推理:DeepMind 的 AlphaProof 和 AlphaGeometry 2 系统,

“AlphaGeometry 2 基于 Gemini,且使用比前一版本多一个数量级的数据进行训练”,通过解决复杂的数学问题,在国际数学奥林匹克中获得了银牌,展示了合成数据在提升 AI 能力、应对专业领域挑战中的强大作用 [5]。

生命科学研究:Nvidia 的研究团队报告称:

合成数据还为使用敏感病患数据提供了一个伦理替代方案,有助于教育和培训 而不妥协病患隐私” [4]

普及 AI 开发

合成数据最强大的一个方面是它有潜力在 AI 开发中实现公平竞争。

赋能数据匮乏行业:合成数据使得数据较少的行业也能参与到 AI 开发中。这对于数据收集因隐私问题或资源限制而困难的行业尤为重要。

大规模定制化:即使是大型科技公司也在利用合成数据进行定制化。微软对 Phi-3 模型的研究展示了如何使用合成数据创建高度专业化的模型:

“我们推测,合成数据集的创建将在不久的将来成为一项重要的技术技能,并且会成为 AI 研究的核心主题。” [3]

针对 AI 模型的定制化学习:特斯拉前 AI 总监 Andrej Karpathy 提出了一个未来愿景,我们为语言模型创建定制化的“教科书”:

借助合成数据进行扩展:AI 研究员 Jim Fan 强调了合成数据为下一阶段训练数据提供潜力:

Fan 还指出,具身代理(如特斯拉的 Optimus 机器人)如果进行大规模模拟,也可以成为合成数据的重要来源。

合成数据的经济学

成本节约与资源效率:

Hugging Face 的博客显示,使用合成数据对定制的小型语言模型进行微调的成本约为 2.7 美元,而使用 GPT-4 处理真实世界数据则需要 3,061 美元,同时合成数据还显著减少了 CO2 排放并提供更快的推理速度。

这是 Hugging Face 的一幅很好的可视化图,展示了在不同使用场景中的好处:

来源:Hugging Face 博客 [6]

结论:一种平衡的方式

尽管模型崩溃的潜在风险不应被忽视,但合成数据的实际应用和好处是如此重要,无法轻易忽视。随着我们在这一领域的不断进步,一种结合合成数据、严格的真实世界验证和深思熟虑的生成实践的平衡方法,将是最大化其潜力的关键。

合成数据在负责任地使用并与真实数据结合时,有可能显著加速各个领域的 AI 发展。这并不是要取代真实数据,而是通过扩展和增强我们的能力,探索我们尚在起步阶段的方式。 通过用合成数据增强数据集,我们可以填补关键的数据空白,解决偏差问题,并创建更强大的模型。

通过负责任地使用合成数据,我们可以实现 AI 开发的普及,推动数据贫乏领域的创新,并推动机器学习领域的边界——同时保持我们 AI 系统的完整性和可靠性。

参考文献

  1. Shumailov, I., Shumaylov, Z., Zhao, Y., Gal, Y., Papernot, N., & Anderson, R. (2023). 递归的诅咒:在生成数据上训练使模型遗忘。arXiv 预印本 arXiv:2305.17493.

  2. Gerstgrasser, M., Schaeffer, R., Dey, A., Rafailov, R., Sleight, H., Hughes, J., … & Zhang, C. (2023). 模型崩溃是否不可避免?通过积累真实和合成数据打破递归的诅咒。arXiv 预印本 arXiv:2404.01413.

  3. Li, Y., Bubeck, S., Eldan, R., Del Giorno, A., Gunasekar, S., & Lee, Y. T. (2023). 教科书就是你所需要的一切 II:phi-1.5 技术报告。arXiv 预印本 arXiv:2309.05463.

  4. Nvidia 研究团队. (2024). 通过合成数据生成解决医学影像的局限性. Nvidia 博客.

  5. DeepMind 博客. (2024). AI 达到银牌水平,解决国际数学奥林匹克问题. DeepMind.

  6. Hugging Face 博客关于合成数据的内容. (2024). 合成数据:通过开源节省成本、时间和碳排放. Hugging Face.

  7. Karpathy, A. (2024). 语言模型的定制教科书. Twitter.

  8. Fan, J. (2024). 合成数据与 AI 训练的未来. Twitter.

  9. 南澳大利亚卫生部。(2024 年)。南澳大利亚卫生部与 Gretel 合作,开创全州范围的合成数据倡议,以确保安全的电子健康记录(EHR)数据共享。Microsoft for Startups Blog

  10. 介绍苹果的设备端和服务器基础模型。machinelearning.apple.com/research/introducing-apple-foundation-models

  11. AgentInstruct: 面向生成性教学的代理流。arxiv.org/abs/2407.03502

  12. Gerstgrasser, M.(2024 年)。评论 Yev Meyer 博士在 LinkedIn 上的帖子。LinkedIn。www.linkedin.com/feed/update/urn:li:activity:7223028230444785664

处理缺失数据

原文:towardsdatascience.com/addressing-missing-data-f6f7920bcc55?source=collection_archive---------9-----------------------#2024-11-26

了解缺失数据模式(MCAR、MNAR、MAR),以利用 Missingno 提高模型性能

Gizem KayaTowards Data Science Gizem Kaya

·发布于Towards Data Science ·8 分钟阅读·2024 年 11 月 26 日

--

在理想的世界中,我们希望能使用干净、完整且准确的数据集。然而,现实世界中的数据很少符合我们的期望。我们常常遇到包含噪声、不一致性、离群值和缺失数据的数据集,这需要仔细处理才能得到有效的结果。特别是,缺失数据是一个不可避免的挑战,我们如何处理它对预测模型或分析结果有着重要影响。

为什么?

其原因隐藏在定义中。缺失数据是那些如果被观察到将对分析具有意义的未观察值。

摄影:由Tanja Tepavac提供,来源于Unsplash

在文献中,我们可以找到几种处理缺失数据的方法,但根据缺失的性质,选择合适的技术至关重要。像删除缺失值行这样简单的方法可能会导致偏差或丢失重要的信息。错误的填充值也可能导致扭曲,影响最终结果。因此,在决定修正措施之前,理解数据中缺失性的性质至关重要。

缺失性的性质可以简单地分为三种:

如何解决空间依赖性

原文:towardsdatascience.com/addressing-spatial-dependencies-674c6d670071?source=collection_archive---------6-----------------------#2024-04-14

空间误差模型(SEM)的要点

Leonardo MaldonadoTowards Data Science Leonardo Maldonado

·发布于 Towards Data Science ·阅读时间:5 分钟·2024 年 4 月 14 日

--

图像由使用 AI 工具(如 Photo Realistic GPT 和 Super Describe)创建。

遥感技术,作为一个处理大量空间数据的领域,这些数据来自卫星图像、航空照片和其他基于传感器的技术,或任何使用具有空间特征的数据的领域,都面临着一个不容忽视的挑战。在分析这些数据时,我们必须处理空间依赖性(即,彼此接近的事物如何相互影响)。正如Crawford (2009)恰如其分地指出:

空间依赖性指的是在地理空间中独立测量的值之间的空间自相关程度。

这些空间依赖性常常导致统计模型中的自相关误差,其中彼此接近的观测值往往表现出相似的误差特征,而这些误差特征仅由解释变量无法捕捉。

要深入了解空间自相关,可以观看Luc Anselin 在芝加哥大学的讲座(2016 年 10 月)的精彩 YouTube 视频。我还邀请你简要查看我之前的文章,地理数据分析中的空间交叉验证(2024 年 3 月 22 日),在这篇文章中,我阐述了考虑空间相关性的空间关系建模的重要性,以提高模型的性能、可靠性和预测能力。

解决蝴蝶效应:使用集成卡尔曼滤波器的数据同化

原文:towardsdatascience.com/addressing-the-butterfly-effect-data-assimilation-using-ensemble-kalman-filter-9883d0e1197b?source=collection_archive---------3-----------------------#2024-12-13

学习如何逐步实现集成卡尔曼滤波器进行数据同化,包括数学细节和代码实现

Wencong Yang, PhDTowards Data Science Wencong Yang, PhD

·发表于Towards Data Science ·阅读时长 9 分钟·2024 年 12 月 13 日

--

来源:unsplash.com/

1. 快速入门:为什么需要数据同化

许多现实世界中的动力系统是混沌的,其中初始条件的微小变化会导致后续状态的显著差异。这种现象也被称为蝴蝶效应,它使得基于物理模型的程序很难准确预测系统行为。数据同化通过将观测数据整合到模型状态估计中来解决这一问题。它通常应用于时间序列预测问题,特别是在天气预报等物理系统模型中。集成卡尔曼滤波器(EnKF)是数据同化中一种广泛使用的算法,具有优雅的理论和简单的实现方法,受到了从科学界到工业界的广泛关注。

数据同化示意图。来源:作者提供。

本文作为 EnKF 的教程,介绍了 EnKF 的基本数学原理,提供了逐步的代码实现,并展示了如何使用一个简单的示例进行实际操作……

AdEMAMix:深入探讨一种新的深度神经网络优化器

原文:towardsdatascience.com/ademamix-a-deep-dive-into-a-new-optimizer-for-your-deep-neural-network-6168e2c1da35?source=collection_archive---------10-----------------------#2024-09-19

一种比 ADAM 优化器更好且更快速的选择,来自苹果研究团队

Saptashwa BhattacharyyaTowards Data Science Saptashwa Bhattacharyya

·发表于 Towards Data Science ·12 分钟阅读·2024 年 9 月 19 日

--

我们该走哪条路?让我们优化一下吧(由 DALL·E 3 根据作者提示生成)

深度神经网络(DNN)被认为是通过训练在大规模数据集中发现模式的最有效工具之一。在训练问题的核心,我们面临复杂的损失函数地形,DNN 的训练归结为随着迭代次数的增加优化损失。几种常用的优化器包括随机梯度下降、RMSProp(均方根传播)、Adam(自适应矩估计)等。

最近(2024 年 9 月),来自苹果(和瑞士联邦理工学院)的研究人员提出了一种新的优化器 AdEMAMix¹,研究表明它在语言建模和图像分类任务中,比 AdamW 优化器表现得更好、更快。

在这篇文章中,我将详细介绍这一优化器背后的数学概念,并讨论本文中提出的一些非常有趣的结果。本文将涵盖的主题包括:

  • Adam 优化器回顾

  • Adam 中的指数加权移动平均(EMA)。

  • AdEMAMix 背后的主要思想:两种 EMA 的混合。

  • AdEMAMix 中的指数衰减率调度器。

采用 Spark Connect

原文:towardsdatascience.com/adopting-spark-connect-cdd6de69fa98?source=collection_archive---------7-----------------------#2024-11-07

我们如何使用共享的 Spark 服务器来提高 Spark 基础设施的效率

Sergey KotlovTowards Data Science Sergey Kotlov

·发表于 Towards Data Science ·15 分钟阅读·2024 年 11 月 7 日

--

图片由Kanenori提供,来自Pixabay

Spark ConnectSpark 生态系统中一个相对较新的组件,它允许轻量级客户端在远程 Spark 集群上运行 Spark 应用程序。这项技术可以为使用 DataFrame API 的 Spark 应用程序提供一些好处。Spark 长期以来就允许在远程 Thrift JDBC 服务器上运行 SQL 查询。然而,能够远程运行用任何支持的语言(如 Scala、Python)编写的客户端应用程序的能力,直到 Spark 3.4 版本才出现。

在本文中,我将分享我们使用 Spark Connect(版本 3.5)的经验。我将讨论我们获得的好处、与运行 Spark 客户端应用程序相关的技术细节,以及一些关于如何使您的 Spark Connect 设置更加高效和稳定的建议。

使用动机

Spark 是 Joom 分析平台的关键组成部分之一。我们有大量的内部用户和超过 1000 个自定义的 Spark 应用程序。这些应用程序在不同的时间运行,具有不同的复杂性,并且需要非常不同的计算资源(从几核几分钟到多达 250 核几个小时不等)。之前,所有应用程序都是作为独立的 Spark 应用程序执行的(每个应用程序都有自己的 driver 和 executor),这对于小型和中型应用程序(我们历史上有很多此类应用程序)来说,会导致明显的开销。引入 Spark Connect 后,现在可以设置一个共享的 Spark Connect 服务器,并在其上运行多个 Spark 客户端应用程序。从技术上讲,Spark Connect 服务器是一个包含 Spark Connect 端点的 Spark 应用程序。

作者图片

下面是我们从中获得的好处:

  • 资源节省

    • 通过 Spark Connect 运行时,客户端应用程序不需要自己的 Spark driver(通常使用超过 1.5 GB 的内存)。相反,它们使用的是一个轻量级客户端,典型的内存消耗为 200 MB。

    • 执行器利用率得到了改善,因为任何执行器都可以运行多个客户端应用程序的任务。例如,假设某个 Spark 应用程序在执行过程中,使用的核心数和内存显著少于最初请求的资源。出现这种情况的原因有很多。然后,在独立的 Spark 应用程序中,当前未使用的资源往往会被浪费,因为动态资源分配通常无法高效地缩减资源。然而,在 Spark Connect 服务器的情况下,释放的核心和内存可以立即用来运行其他客户端应用程序的任务。

  • 启动等待时间减少

    • 出于各种原因,我们必须限制同时运行的独立 Spark 应用程序的数量,如果所有插槽都被占用,它们可能会在队列中等待相当长时间。这可能会对数据准备时间和用户体验产生负面影响。在 Spark Connect 服务器的情况下,我们目前能够避免这种限制,所有 Spark Connect 客户端应用程序在启动后都会立即开始运行。

    • 对于临时执行,最好尽可能减少获得结果的时间,避免让用户等待。在独立的 Spark 应用程序的情况下,启动客户端应用程序通常需要为其 driver 和 executor 配置额外的 EC2 节点,并初始化 driver 和 executor。所有这些加起来可能需要超过 4 分钟。而在 Spark Connect 服务器的情况下,至少其 driver 始终处于运行状态并准备接受请求,因此只需等待额外的执行器,且执行器通常已经可用。这可能大大减少临时应用程序准备就绪的等待时间。

我们的约束条件

目前,我们没有在 Spark Connect 上运行长时间运行的重型应用程序,原因如下:

  • 它们可能会导致 Spark Connect 服务器的故障或不稳定行为(例如,通过溢出执行器节点上的磁盘)。这可能会导致整个平台的大规模问题。

  • 它们通常需要独特的内存设置,并使用特定的优化技术(例如,自定义的 extraStrategies)。

  • 我们目前面临一个问题,即给 Spark Connect 服务器分配大量执行器以处理非常大的并发负载(这与 Spark 任务调度器的行为相关,超出了本文的讨论范围)。

因此,重型应用程序仍然作为单独的 Spark 应用程序运行。

启动客户端应用程序

我们使用 Spark on Kubernetes/EKS Airflow。一些代码示例将特定于这个环境。

我们有太多不同且不断变化的 Spark 应用程序,需要花费太多时间手动确定每个应用程序是否应该根据我们的标准在 Spark Connect 上运行。此外,运行在 Spark Connect 上的应用程序列表需要定期更新。例如,假设今天某个应用程序足够轻,我们决定在 Spark Connect 上运行它。但明天,它的开发者可能会添加几个大的连接操作,使得它变得非常重。此时,最好将其作为单独的 Spark 应用程序来运行。反之的情况也是可能的。

最终,我们创建了一个服务来自动确定如何启动每个特定的客户端应用程序。该服务分析每个应用程序以前运行的历史记录,评估诸如总任务时间Shuffle 写入磁盘溢出等指标(这些数据是通过 SparkListener 收集的)。开发者为应用程序设置的自定义参数(例如,驱动程序和执行器的内存设置)也会被考虑在内。根据这些数据,服务会自动确定每个应用程序此次是否应该在 Spark Connect 服务器上运行,或作为单独的 Spark 应用程序运行。因此,我们所有的应用程序都应该准备好以这两种方式之一运行。

在我们的环境中,每个客户端应用程序都是独立构建的,并拥有包含应用程序代码以及特定依赖项的 JAR 文件(例如,ML 应用程序通常会使用像 CatBoost 等第三方库)。问题在于,Spark Connect 的 SparkSession API 与独立 Spark 应用程序所使用的 SparkSession API 略有不同(Spark Connect 客户端使用 spark-connect-client-jvm 组件)。因此,我们必须在每个客户端应用程序的构建阶段确定它是否会通过 Spark Connect 运行。然而,我们并不知道这一点。以下将描述我们启动客户端应用程序的方法,该方法避免了为同一个应用程序构建和管理两个版本的 JAR 工件。

对于每个 Spark 客户端应用程序,我们只构建一个 JAR 文件,其中包含应用程序代码和特定的依赖项。这个 JAR 文件既用于 Spark Connect 运行模式,也用于作为独立的 Spark 应用程序运行。因此,这些客户端 JAR 文件不包含特定的 Spark 依赖项。适当的 Spark 依赖项(如 spark-core/spark-sqlspark-connect-client-jvm)会在后续通过 Java 类路径提供,具体取决于运行模式。无论如何,所有客户端应用程序都使用相同的 Scala 代码来初始化 SparkSession,这一操作会根据运行模式进行调整。所有客户端应用程序的 JAR 文件都针对常规的 Spark API 进行构建。因此,在面向 Spark Connect 客户端的代码部分,SparkSession 方法(如 remoteaddArtifact)会通过反射来调用:

val sparkConnectUri: Option[String] = Option(System.getenv("SPARK_CONNECT_URI"))

val isSparkConnectMode: Boolean = sparkConnectUri.isDefined

def createSparkSession(): SparkSession = {
  if (isSparkConnectMode) {
    createRemoteSparkSession()
  } else {
    SparkSession.builder
      // Whatever you need to do to configure SparkSession for a separate 
      // Spark application.
      .getOrCreate
  }
}

private def createRemoteSparkSession(): SparkSession = {
  val uri = sparkConnectUri.getOrElse(throw new Exception(
    "Required environment variable 'SPARK_CONNECT_URI' is not set."))

  val builder = SparkSession.builder
  // Reflection is used here because the regular SparkSession API does not 
  // contain these methods. They are only available in the SparkSession API 
  // version for Spark Connect.
  classOf[SparkSession.Builder]
    .getDeclaredMethod("remote", classOf[String])
    .invoke(builder, uri)

  // A set of identifiers for this application (to be used later).
  val scAppId = s"spark-connect-${UUID.randomUUID()}"
  val airflowTaskId = Option(System.getenv("AIRFLOW_TASK_ID"))
    .getOrElse("unknown_airflow_task_id")
  val session = builder
    .config("spark.joom.scAppId", scAppId)
    .config("spark.joom.airflowTaskId", airflowTaskId)
    .getOrCreate()

  // If the client application uses your Scala code (e.g., custom UDFs), 
  // then you must add the jar artifact containing this code so that it 
  // can be used on the Spark Connect server side.
  val addArtifact = Option(System.getenv("ADD_ARTIFACT_TO_SC_SESSION"))
    .forall(_.toBoolean)

  if (addArtifact) {
    val mainApplicationFilePath = 
      System.getenv("SPARK_CONNECT_MAIN_APPLICATION_FILE_PATH")
    classOf[SparkSession]
      .getDeclaredMethod("addArtifact", classOf[String])
      .invoke(session, mainApplicationFilePath)
  }

  Runtime.getRuntime.addShutdownHook(new Thread() {
    override def run(): Unit = {
      session.close()
    }
  })

  session
}

在 Spark Connect 模式下,这段客户端代码可以作为常规的 Java 应用程序在任何地方运行。由于我们使用 Kubernetes,这段代码会在 Docker 容器中运行。所有与 Spark Connect 相关的依赖都打包进一个 Docker 镜像,用来运行客户端应用程序(该镜像的一个简化示例可以在这里找到)。该镜像不仅包含 spark-connect-client-jvm 组件,还包括几乎所有客户端应用程序常用的其他依赖(例如,hadoop-aws,因为我们几乎总是在客户端与 S3 存储进行交互)。

FROM openjdk:11-jre-slim

WORKDIR /app

# Here, we copy the common artifacts required for any of our Spark Connect 
# clients (primarily spark-connect-client-jvm, as well as spark-hive, 
# hadoop-aws, scala-library, etc.).
COPY build/libs/* /app/

COPY src/main/docker/entrypoint.sh /app/
RUN chmod +x ./entrypoint.sh
ENTRYPOINT ["./entrypoint.sh"]

这个通用 Docker 镜像用于通过 Spark Connect 运行我们的所有客户端应用程序。同时,它不包含特定应用程序的客户端 JAR 文件及其依赖项,因为有许多此类应用程序,它们不断更新,并可能依赖任何第三方库。相反,当启动某个特定的客户端应用程序时,会通过环境变量传递该应用程序 JAR 文件的位置,在初始化过程中(在 entrypoint.sh 中)下载该 JAR 文件:

#!/bin/bash
set -eo pipefail

# This variable will also be used in the SparkSession builder within 
# the application code.
export SPARK_CONNECT_MAIN_APPLICATION_FILE_PATH="/tmp/$(uuidgen).jar"

# Download the JAR with the code and specific dependencies of the client 
# application to be run. All such JAR files are stored in S3, and when 
# creating a client Pod, the path to the required JAR is passed to it 
# via environment variables.
java -cp "/app/*" com.joom.analytics.sc.client.S3Downloader \ 
    ${MAIN_APPLICATION_FILE_S3_PATH} ${SPARK_CONNECT_MAIN_APPLICATION_FILE_PATH}

# Launch the client application. Any MAIN_CLASS initializes a SparkSession 
# at the beginning of its execution using the code provided above.
java -cp ${SPARK_CONNECT_MAIN_APPLICATION_FILE_PATH}:"/app/*" ${MAIN_CLASS} "$@"

最后,当启动应用程序时,我们的自定义 SparkAirflowOperator 会根据此应用程序先前运行的统计数据自动确定执行模式(Spark Connect 或单独的模式)。

  • 在 Spark Connect 的情况下,我们使用 KubernetesPodOperator 来启动应用程序的客户端 Pod。KubernetesPodOperator 需要作为参数提供前面描述的 Docker 镜像,以及环境变量(MAIN_CLASSJAR_PATH 等),这些变量将在 entrypoint.sh 和应用程序代码中使用。无需为客户端 Pod 分配过多资源(例如,在我们的环境中,它的典型消耗:内存 — 200 MB,vCPU — 0.15)。

  • 在单独的 Spark 应用程序中,我们使用自定义的 AirflowOperator,通过 spark-on-k8s-operator 和官方的 Spark Docker 镜像 运行 Spark 应用程序。我们暂时跳过有关 Spark AirflowOperator 的详细内容,因为它是一个庞大的话题,值得单独撰写一篇文章。

与常规 Spark 应用程序的兼容性问题

不是所有现有的 Spark 应用程序都可以在 Spark Connect 上成功执行,因为其 SparkSession API 与用于单独 Spark 应用程序的 SparkSession API 不同。例如,如果你的代码使用了 sparkSession.sparkContextsparkSession.sessionState,它将在 Spark Connect 客户端中失败,因为 Spark Connect 版本的 SparkSession 不具备这些属性。

在我们的案例中,最常见的问题原因是使用了 sparkSession.sessionState.catalogsparkSession.sparkContext.hadoopConfiguration。在某些情况下,sparkSession.sessionState.catalog 可以替换为 sparkSession.catalog,但并非总是如此。如果客户端执行的代码涉及对数据存储的操作,则可能需要 sparkSession.sparkContext.hadoopConfiguration,例如:

def delete(path: Path, recursive: Boolean = true)
          (implicit hadoopConfig: Configuration): Boolean = {
  val fs = path.getFileSystem(hadoopConfig)
  fs.delete(path, recursive)
}

幸运的是,可以为 Spark Connect 客户端创建一个独立的 SessionCatalog。在这种情况下,Spark Connect 客户端的类路径还必须包含 org.apache.spark:spark-hive_2.12,以及与存储交互的库(因为我们使用 S3,所以在我们的案例中是 org.apache.hadoop:hadoop-aws)。

import org.apache.spark.SparkConf
import org.apache.hadoop.conf.Configuration
import org.apache.spark.sql.hive.StandaloneHiveExternalCatalog
import org.apache.spark.sql.catalyst.catalog.{ExternalCatalogWithListener, SessionCatalog}

// This is just an example of what the required properties might look like. 
// All of them should already be set for existing Spark applications in one 
// way or another, and their complete list can be found in the UI of any
// running separate Spark application on the Environment tab.
val sessionCatalogConfig = Map(
  "spark.hadoop.hive.metastore.uris" -> "thrift://metastore.spark:9083",
  "spark.sql.catalogImplementation" -> "hive",
  "spark.sql.catalog.spark_catalog" -> "org.apache.spark.sql.delta.catalog.DeltaCatalog",
)

val hadoopConfig = Map(
  "hive.metastore.uris" -> "thrift://metastore.spark:9083",
  "fs.s3.impl" -> "org.apache.hadoop.fs.s3a.S3AFileSystem",
  "fs.s3a.aws.credentials.provider" -> "com.amazonaws.auth.DefaultAWSCredentialsProviderChain",
  "fs.s3a.endpoint" -> "s3.amazonaws.com",
  // and others...
)

def createStandaloneSessionCatalog(): (SessionCatalog,  Configuration) = {
  val sparkConf = new SparkConf().setAll(sessionCatalogConfig)
  val hadoopConfiguration = new Configuration()
  hadoopConfig.foreach { 
    case (key, value) => hadoopConfiguration.set(key, value) 
  }

  val externalCatalog = new StandaloneHiveExternalCatalog(
    sparkConf, hadoopConfiguration)
  val sessionCatalog = new SessionCatalog(
    new ExternalCatalogWithListener(externalCatalog)
  )
  (sessionCatalog, hadoopConfiguration)
}

你还需要为 HiveExternalCatalog 创建一个封装器,使其可以在代码中访问(因为 HiveExternalCatalog 类是 org.apache.spark 包私有的):

package org.apache.spark.sql.hive

import org.apache.hadoop.conf.Configuration
import org.apache.spark.SparkConf

class StandaloneHiveExternalCatalog(conf: SparkConf, hadoopConf: Configuration) 
  extends HiveExternalCatalog(conf, hadoopConf)

此外,通常可以用替代代码来替换 Spark Connect 上无法运行的代码,例如:

  • sparkSession.createDataFrame(sparkSession.sparkContext.parallelize(data), schema) ==> sparkSession.createDataFrame(data.toList.asJava, schema)

  • sparkSession.sparkContext.getConf.get(“some_property”) ==> sparkSession.conf.get(“some_property”)

回退到独立的 Spark 应用程序

不幸的是,修复特定的 Spark 应用程序使其作为 Spark Connect 客户端工作并不总是容易。例如,项目中使用的第三方 Spark 组件存在很大风险,因为它们通常在编写时没有考虑与 Spark Connect 的兼容性。由于在我们的环境中,任何 Spark 应用程序都可以自动启动在 Spark Connect 上,因此我们认为在失败时实现回退为独立的 Spark 应用程序是合理的。简化后的逻辑如下:

  • 如果某个应用程序在 Spark Connect 上失败,我们会立即尝试将其重新作为独立的 Spark 应用程序运行。同时,我们会增加在 Spark Connect 上执行过程中发生的失败次数计数器(每个客户端应用程序都有自己的计数器)。

  • 下次启动此应用程序时,我们检查该应用程序的失败计数器:

    • 如果失败次数少于 3 次,我们假设上次应用程序失败并非由于与 Spark Connect 不兼容,而是由于其他任何可能的临时原因。因此,我们会尝试再次在 Spark Connect 上运行它。如果这次成功完成,则该客户端应用程序的失败计数器会被重置为零。

    • 如果已经发生了 3 次失败,我们假设该应用程序无法在 Spark Connect 上工作,并且暂时停止在 Spark Connect 上运行它。之后,它将只作为独立的 Spark 应用程序启动。

  • 如果应用程序在 Spark Connect 上出现了 3 次失败,但最后一次失败发生在 2 个月以上之前,我们会尝试再次在 Spark Connect 上运行它(以防在此期间发生了变化,使其与 Spark Connect 兼容)。如果这次成功,我们会再次将失败计数器重置为零。如果再次失败,下次尝试将在 2 个月后进行。

这种方法比维护一段代码来识别失败原因(从日志中获取)要简单一些,并且在大多数情况下效果很好。尝试在 Spark Connect 上运行不兼容的应用程序通常不会带来显著的负面影响,因为在绝大多数情况下,如果应用程序与 Spark Connect 不兼容,它会在启动后立即失败,且不会浪费时间和资源。然而,值得一提的是,我们所有的应用程序都是幂等的。

统计数据收集

正如我之前提到的,我们会收集每个 Spark 应用程序的统计数据(我们平台的大多数优化和警报都依赖于这些数据)。当应用程序作为独立的 Spark 应用程序运行时,这很容易实现。而在 Spark Connect 的情况下,每个客户端应用程序的阶段和任务需要与所有其他在共享 Spark Connect 服务器中同时运行的客户端应用程序的阶段和任务区分开。

你可以通过为客户端 SparkSession 设置自定义属性,将任何标识符传递给 Spark Connect 服务器:

val session = builder
  .config("spark.joom.scAppId", scAppId)
  .config("spark.joom.airflowTaskId", airflowTaskId)
  .getOrCreate()

然后,在 Spark Connect 服务器端的 SparkListener 中,你可以检索所有传递的信息,并将每个阶段/任务与特定的客户端应用程序关联起来。

class StatsReportingSparkListener extends SparkListener {

  override def onStageSubmitted(stageSubmitted: SparkListenerStageSubmitted): Unit = {
    val stageId = stageSubmitted.stageInfo.stageId
    val stageAttemptNumber = stageSubmitted.stageInfo.attemptNumber()
    val scAppId = stageSubmitted.properties.getProperty("spark.joom.scAppId")
    // ...
  }
}

在这里,你可以找到我们用来收集统计数据的 StatsReportingSparkListener 代码。你也许对 这个免费的工具 感兴趣,它可以帮助你发现 Spark 应用程序中的性能问题。

优化与稳定性提升

Spark Connect 服务器是一个持续运行的 Spark 应用程序,多个客户端可以在其上运行作业。因此,定制其 属性 使其更可靠并防止资源浪费可能是值得的。以下是我们案例中证明有效的一些设置:

// Using dynamicAllocation is important for the Spark Connect server 
// because the workload can be very unevenly distributed over time.
spark.dynamicAllocation.enabled: true  // default: false

// This pair of parameters is responsible for the timely removal of idle 
// executors:
spark.dynamicAllocation.cachedExecutorIdleTimeout: 5m  // default: infinity
spark.dynamicAllocation.shuffleTracking.timeout: 5m  // default: infinity

// To create new executors only when the existing ones cannot handle 
// the received tasks for a significant amount of time. This allows you 
// to save resources when a small number of tasks arrive at some point 
// in time, which do not require many executors for timely processing. 
// With increased schedulerBacklogTimeout, unnecessary executors do not 
// have the opportunity to appear by the time all incoming tasks are 
// completed. The time to complete the tasks increases slightly with this, 
// but in most cases, this increase is not significant.
spark.dynamicAllocation.schedulerBacklogTimeout: 30s  // default: 1s

// If, for some reason, you need to stop the execution of a client 
// application (and free up resources), you can forcibly terminate the client. 
// Currently, even explicitly closing the client SparkSession does not 
// immediately end the execution of its corresponding Jobs on the server. 
// They will continue to run for a duration equal to 'detachedTimeout'. 
// Therefore, it may be reasonable to reduce it.
spark.connect.execute.manager.detachedTimeout: 2m  // default: 5m

// We have encountered a situation when killed tasks may hang for 
// an unpredictable amount of time, leading to bad consequences for their 
// executors. In this case, it is better to remove the executor on which 
// this problem occurred.
spark.task.reaper.enabled: true // default: false
spark.task.reaper.killTimeout: 300s  // default: -1

// The Spark Connect server can run for an extended period of time. During 
// this time, executors may fail, including for reasons beyond our control 
// (e.g., AWS Spot interruptions). This option is needed to prevent 
// the entire server from failing in such cases.
spark.executor.maxNumFailures: 1000

// In our experience, BroadcastJoin can lead to very serious performance 
// issues in some cases. So, we decided to disable broadcasting. 
// Disabling this option usually does not result in a noticeable performance 
// degradation for our typical applications anyway.
spark.sql.autoBroadcastJoinThreshold: -1 // default: 10MB

// For many of our client applications, we have to add an artifact to 
// the client session (method sparkSession.addArtifact()). 
// Using 'useFetchCache=true' results in double space consumption for 
// the application JAR files on executors' disks, as they are also duplicated 
// in a local cache folder. Sometimes, this even causes disk overflow with 
// subsequent problems for the executor.
spark.files.useFetchCache: false   // default: true

// To ensure fair resource allocation when multiple applications are 
// running concurrently.
spark.scheduler.mode: FAIR  // default: FIFO

例如,在我们调整了 idle timeout 属性后,资源利用率发生了如下变化:

图片来源:作者

预防性重启

在我们的环境中,Spark Connect 服务器(版本 3.5)在连续运行几天后可能会变得不稳定。最常见的情况是,我们面临客户端应用程序任务随机挂起且时间无限,但也可能会遇到其他问题。此外,随着时间的推移,整个 Spark Connect 服务器随机失败的概率会急剧增加,这可能发生在不合适的时刻。

随着该组件的发展,它可能会变得更加稳定(或者我们会发现我们的 Spark Connect 设置中存在问题)。但目前,最简单的解决方案是每天在合适的时刻(即没有客户端应用程序在运行时)对 Spark Connect 服务器进行预防性重启。重启代码的示例 可以在这里找到

结论

在本文中,我描述了我们使用 Spark Connect 运行大量不同类型 Spark 应用程序的经验。

总结上述内容:

  • 该组件有助于节省资源并减少 Spark 客户端应用程序执行的等待时间。

  • 在共享 Spark Connect 服务器上运行哪些应用程序需要谨慎,因为资源密集型应用程序可能会对整个系统造成问题。

  • 你可以创建一个用于启动客户端应用程序的基础设施,以便在启动时自动决定如何运行任何应用程序(是作为独立的 Spark 应用程序还是作为 Spark Connect 客户端)。

  • 需要注意的是,并非所有应用程序都能够在 Spark Connect 上运行,但此类情况的数量可以显著减少。如果有可能运行未经过 Spark Connect 版本 SparkSession API 兼容性测试的应用程序,建议实现回退机制以运行独立的 Spark 应用程序。

  • 值得注意的是,有一些 Spark 属性可以提高资源利用率并增加 Spark Connect 服务器的整体稳定性。可能还需要合理设置 Spark Connect 服务器的定期预防性重启,以减少意外故障和不希望出现的行为的发生概率。

总的来说,我们在公司使用 Spark Connect 的体验是积极的。我们将继续密切关注这项技术的发展,并计划扩大其使用范围。

高级代码生成与大语言模型 — 构建一个合成数据生成器

原文:towardsdatascience.com/advanced-code-generation-with-llms-building-a-synthetic-data-generator-0e7ed7b496dc?source=collection_archive---------4-----------------------#2024-04-14

应用 INSPIRe 框架的六个步骤来加速你的代码生成(ChatGPT-4 — Claude 3 — Gemini)

Nabil AlouaniTowards Data Science Nabil Alouani

·发表于 Towards Data Science ·31 分钟阅读·2024 年 4 月 14 日

--

由作者构思的图像。

我从未从头到尾写过一个数据科学项目。但在 Jupyter Notebook 中你能做的任何事情,我也能做到。是的,真的,任何事情。

夸耀归夸耀,这是 AI 在过去两年为我们带来的最重要的变革之一。大语言模型将代码变成了商品;你可以把它当做工具使用,而不是一项技能。

你只需要五个元素:

  1. 数据素养。

  2. 一点逻辑。

  3. 一种试错的天赋。

  4. 提示工程。

  5. INSPIRe 框架。

如果你在想 INSPIRe 到底是什么,应该先读一下本系列的第一部分。你将会了解该框架的介绍以及如何应用它。不过,如果你感觉冒险,接下来的简短回顾应该足够了。

medium.com/@nabil-alouani/6-practical-steps-to-make-ai-write-your-python-code-for-you-2b3c6a35f174

高级数据建模

原文:towardsdatascience.com/advanced-data-modelling-1e496578bc91?source=collection_archive---------0-----------------------#2024-07-21

数据模型层、环境、测试和数据质量解析

💡Mike ShakhomirovTowards Data Science 💡Mike Shakhomirov

·发表于Towards Data Science ·阅读时间 14 分钟·2024 年 7 月 21 日

--

AI 生成的图像,使用了Kandinsky

数据建模是数据工程的重要组成部分。我认为,如果你想成为一名成功的数据从业者,这是必不可少的。构建具有多层的 SQL 转换管道是一个具有挑战性的任务。在这个过程中,保持条理清晰非常重要。在这篇文章中,我尝试总结一些方便数据结构化的技巧,并描述我每天使用的建模技巧。它通常能帮助我设计和开发一个准确、易于导航且用户友好的优秀数据平台或数据仓库。

命名规范

使用良好设计的命名规范可以提供非常清晰且无歧义的数据库对象内容的感知和含义。对于表格和列的命名规范始终是一个好习惯。它能很好地展示你的数据仓库有多成熟,并在开发过程中提供极大的帮助。

数据库实体名称必须至少是人类可读的。

以此为前提维护数据库或数据仓库可以提升用户体验,使其看起来更加用户友好

初学者的高级 ETL 技巧

原文:towardsdatascience.com/advanced-etl-techniques-for-beginners-03c404f0f0ac?source=collection_archive---------0-----------------------#2024-02-03

在 1 到 10 的范围内,你的数据摄取技能如何?

💡Mike ShakhomirovTowards Data Science 💡Mike Shakhomirov

·发表于Towards Data Science ·14 分钟阅读·2024 年 2 月 3 日

--

Blake Connally拍摄,来自Unsplash

数据摄取是数据工程中的一个关键步骤。数据工程师将大量数据加载到各种数据库系统中,以便进行进一步的转换和处理。当处理相对较小的数据量时,我们很幸运不会遇到内存不足的问题,但在处理生产数据管道时,数据量达到 TB(甚至 PB)级别时,通常会变成一个真正的挑战。现有的 ETL 解决方案提供了将数据自动加载到数据仓库中的功能,并且通常采用按行计费的定价模式。在这个故事中,我想讨论如何为我们的数据管道创建一个定制的数据加载解决方案,以实现高效的数据加载。我们将深入了解常见的数据摄取设计模式和组织过程的典型方法。我们还将逆向工程一些最流行的 ETL 解决方案,看看如何高效地进行数据摄取而不会发生停机或数据丢失。我将提供使用 Python 库和市场上免费工具的数据加载示例,总结我的发现。

在 1 到 10 的范围内,你的数据加载技能如何?-

这是我在数据工程面试中最喜欢的一个问题之一。我一直在寻找人才…

高级提示工程:思维链(CoT)

原文:towardsdatascience.com/advanced-prompt-engineering-chain-of-thought-cot-8d8b090bf699?source=collection_archive---------0-----------------------#2024-12-23

与大型语言模型的工作

比较不同的推理技术

Ida SilfverskiöldTowards Data Science Ida Silfverskiöld

·发表于 Towards Data Science ·阅读时间:20 分钟·2024 年 12 月 23 日

--

一些推理技术的不同形式 | 作者图片

如果你不是会员,但想阅读本文,请查看这个朋友链接 这里

思维链(CoT)已经存在了一段时间,技术上它是一种高级提示工程的形式,但即使在其首次出现几年后,它仍然保持着相关性。CoT 在其不同的形式中,通常是试图迫使大型语言模型进行推理。

在 OpenAI 于今年九月发布他们的 o1 模型预览后,我们看到围绕思维链(CoT)的热度增加。

没有人完全知道 o1 是如何工作的(除 OpenAI 外),它是否是一个组合系统,使用了什么样的数据进行微调,是否使用了强化学习,或者是否有多个模型协同工作。

也许一个模型负责规划,另一个负责思考,第三个负责评分。但我们知道它们确实在使用某种逐步推理的方式。

关于这一点有很多公开的研究,或许你会想深入了解。因此,本文我将概述目前已有的研究,以便你知道哪些技术可以使用。自然地,我会测试不同的技术,看看它们的效果……

提高 RAG 的高级查询转换

原文:towardsdatascience.com/advanced-query-transformations-to-improve-rag-11adca9b19d1?source=collection_archive---------1-----------------------#2024-01-10

不同的查询转换方法

Iulia BrezeanuTowards Data Science Iulia Brezeanu

·发布于 Towards Data Science ·9 分钟阅读·2024 年 1 月 10 日

--

图片来源:作者。AI 生成。

检索增强生成(RAG)已成为生成性 AI 文献中讨论最热的话题之一。随着每日涌现的大量博客文章和科学论文,保持更新变得越来越具有挑战性。然而,RAG 的流行是当之无愧的,因为没有其他解决方案在减少大型语言模型的幻觉方面表现得如此高效。

RAG 通过可靠的外部来源(如 Wikipedia 页面、私人 PDF 文件等)增强语言模型的通用知识。这也是 RAG 最重要的一步:确保我们的检索能够找到正确的文档并将其输入模型。

我们非常需要 RAG,因为目前我们在将完整文档放入上下文窗口时面临一些限制。原因包括模型输入的令牌长度限制、计算成本的成比例增加,以及像“丢失在中间”这样的现象,指的是模型在处理长输入上下文时难以使用位于中间的信息[2]。

模型性能与上下文窗口中相关信息位置之间的关系。[2]

如果检索到的文档过长或不相关,正如俗话所说,垃圾进……

先进的检索增强生成:从理论到 LlamaIndex 实现

原文:towardsdatascience.com/advanced-retrieval-augmented-generation-from-theory-to-llamaindex-implementation-4de1464a9930?source=collection_archive---------0-----------------------#2024-02-19

如何通过在 Python 中实现针对性的先进 RAG 技术,解决简单 RAG 流水线的局限性

Leonie MonigattiTowards Data Science Leonie Monigatti

·发表于 Towards Data Science ·阅读时长 10 分钟·2024 年 2 月 19 日

--

简单 RAG 与先进 RAG 的区别(图源:作者,灵感来源于 [1])

一项关于检索增强生成(RAG) [1]的最新调查总结了三种近期演变的范式:

  • 简单 RAG,

  • 先进的 RAG,以及

  • 模块化 RAG。

先进的 RAG 范式包括一系列技术,旨在解决简单 RAG 的已知局限性。本文首先讨论了这些技术,它们可以分为 检索前、检索和检索后优化

在下半部分,您将学习如何使用 Llamaindex 在 Python 中实现一个简单的 RAG 流水线,并通过选择以下先进的 RAG 技术将其增强为一个先进的 RAG 流水线:

  • 检索前优化:句子窗口检索

  • 检索优化:混合搜索

  • 检索后优化:重新排序

本文重点介绍先进的 RAG 范式及其实现。如果你不熟悉 RAG 的基本概念,可以在这里了解:

## 检索增强生成(RAG):从理论到 LangChain 实现

从原始学术论文的理论到其在 Python 中的实现,涵盖了 OpenAI、Weaviate 和 LangChain 的应用

[towardsdatascience.com

什么是高级 RAG

随着 RAG 领域的最新进展,先进的 RAG 已经发展为一种新的范式,通过有针对性的增强来解决传统 RAG 范式的一些局限性。正如最近的调查所总结的[1],先进的 RAG 技术可以分为预检索、检索和后检索优化。

原始 RAG 与高级 RAG 的区别(图源:作者,灵感来自[1])

预检索优化

预检索优化侧重于数据索引优化以及查询优化。数据索引优化技术旨在以一种有助于提高检索效率的方式存储数据,例如[1]:

  • 滑动窗口使用块之间的重叠,是最简单的技术之一。

  • 增强数据粒度应用数据清理技术,如去除无关信息、确认事实准确性、更新过时信息等。

  • 添加元数据,例如日期、目的或章节,以便进行筛选。

  • 优化索引结构涉及使用不同策略来索引数据,例如调整块的大小或使用多重索引策略。我们将在本文中实现的一项技术是句子窗口检索,它将单个句子嵌入用于检索,并在推理时用更大的文本窗口替换它们。

句子窗口检索

此外,预检索技术不仅限于数据索引,还可以包括推理时的技术,如查询路由、查询重写和查询扩展。

检索优化

检索阶段旨在识别最相关的上下文。通常,检索基于向量搜索,它计算查询与索引数据之间的语义相似性。因此,大多数检索优化技术围绕着嵌入模型展开[1]:

  • 微调嵌入模型定制嵌入模型以适应特定领域的上下文,特别是对于那些术语不断变化或稀有的领域。例如,BAAI/bge-small-en是一个高性能的嵌入模型,可以进行微调(见微调指南

  • 动态嵌入适应词语使用的上下文,区别于静态嵌入,后者为每个词使用一个单一的向量。例如,OpenAI 的embeddings-ada-02是一个复杂的动态嵌入模型,能够捕捉上下文理解。[1]

除了向量搜索,还有其他检索技术,例如混合搜索,通常是指将向量搜索与基于关键词的搜索相结合的概念。如果你的检索需要精确的关键词匹配,这种检索技术非常有用。

## 使用混合搜索提高 RAG 流水线中的检索性能

如何通过结合传统的基于关键词的搜索与现代的向量搜索来找到更相关的搜索结果

towardsdatascience.com

后检索优化

对检索到的上下文进行额外处理可以帮助解决诸如超出上下文窗口限制或引入噪声等问题,从而妨碍对关键信息的关注。RAG 调查总结的后检索优化技术 [1] 包括:

  • Prompt 压缩通过删除不相关内容并突出重要的上下文来减少整体提示长度。

  • 重新排名使用机器学习模型重新计算检索到的上下文的相关性得分。

重新排名

欲了解如何改善 RAG 流水线的性能,使其成为生产就绪的应用程序,继续阅读:

## 生产就绪的 RAG 应用程序的 12 种调优策略指南

如何通过这些“超参数”提高你的检索增强生成(RAG)流水线的性能,等等……

towardsdatascience.com

先决条件

本节讨论了在本文中跟随的所需包和 API 密钥。

所需的包

本文将指导你通过使用 LlamaIndex 在 Python 中实现一个简单和高级的 RAG 流水线。

pip install llama-index

在本文中,我们将使用 LlamaIndex [v0.10](https://blog.llamaindex.ai/llamaindex-v0-10-838e735948f8)。如果你正在从较旧的 LlamaIndex 版本升级,你需要运行以下命令来正确安装和运行 LlamaIndex:

pip uninstall llama-index
pip install llama-index --upgrade --no-cache-dir --force-reinstall

LlamaIndex 提供了一种选项,可以将向量嵌入存储在本地的 JSON 文件中进行持久存储,这对于快速原型设计一个想法非常有用。然而,由于先进的 RAG 技术旨在面向生产环境应用,我们将使用向量数据库进行持久存储。

由于我们不仅需要存储向量嵌入,还需要元数据存储和混合搜索能力,因此我们将使用支持这些功能的开源向量数据库Weaviatev3.26.2)。

pip install weaviate-client llama-index-vector-stores-weaviate

API 密钥

我们将使用 Weaviate 嵌入版,您可以免费使用,无需注册 API 密钥。然而,本教程使用的是来自OpenAI的嵌入模型和 LLM,您需要一个 OpenAI API 密钥才能使用。要获取密钥,您需要一个 OpenAI 账户,并在API 密钥页面下“创建新的密钥”。

接下来,在您的根目录中创建一个.env文件,并在其中定义您的 API 密钥:

OPENAI_API_KEY="<YOUR_OPENAI_API_KEY>"

之后,您可以使用以下代码加载您的 API 密钥:

# !pip install python-dotenv
import os
from dotenv import load_dotenv,find_dotenv

load_dotenv(find_dotenv())

使用 LlamaIndex 实现简单的 RAG

本节讨论如何使用 LlamaIndex 实现简单的 RAG 管道。您可以在这个Jupyter Notebook中找到整个简单 RAG 管道的实现。使用 LangChain 实现的相关内容,可以继续阅读这篇文章(使用 LangChain 实现的简单 RAG 管道)

步骤 1:定义嵌入模型和 LLM

首先,您可以在一个全局设置对象中定义嵌入模型和 LLM。这样做意味着您不必在代码中再次显式指定这些模型。

  • 嵌入模型:用于生成文档块和查询的向量嵌入。

  • LLM:用于根据用户查询和相关上下文生成答案。

from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.llms.openai import OpenAI
from llama_index.core.settings import Settings

Settings.llm = OpenAI(model="gpt-3.5-turbo", temperature=0.1)
Settings.embed_model = OpenAIEmbedding()

步骤 2:加载数据

接下来,您将在根目录下创建一个名为data的本地目录,并从LlamaIndex GitHub 仓库(MIT 许可证)下载一些示例数据。

!mkdir -p 'data'
!wget '<https://raw.githubusercontent.com/run-llama/llama_index/main/docs/examples/data/paul_graham/paul_graham_essay.txt>' -O 'data/paul_graham_essay.txt'

之后,您可以加载数据以进行进一步处理:

from llama_index.core import SimpleDirectoryReader

# Load data
documents = SimpleDirectoryReader(
        input_files=["./data/paul_graham_essay.txt"]
).load_data()

步骤 3:将文档划分为节点

由于整个文档过大,无法适配 LLM 的上下文窗口,您需要将其划分为较小的文本块,这些文本块在 LlamaIndex 中被称为节点。您可以使用SimpleNodeParser将加载的文档解析成节点,并定义每个块的大小为 1024。

from llama_index.core.node_parser import SimpleNodeParser

node_parser = SimpleNodeParser.from_defaults(chunk_size=1024)

# Extract nodes from documents
nodes = node_parser.get_nodes_from_documents(documents)

步骤 4:构建索引

接下来,您将构建一个索引,存储所有的外部知识,这些知识存储在Weaviate这个开源向量数据库中。

首先,您需要连接到一个 Weaviate 实例。在这种情况下,我们使用的是Weaviate 嵌入版,它允许您在 Notebooks 中免费进行实验,无需 API 密钥。对于生产环境的解决方案,建议您自行部署 Weaviate,例如通过Docker或者使用托管服务

import weaviate

# Connect to your Weaviate instance
client = weaviate.Client(
    embedded_options=weaviate.embedded.EmbeddedOptions(), 
)

接下来,你将从 Weaviate 客户端构建一个VectorStoreIndex,用于存储数据并进行交互。

from llama_index.core import VectorStoreIndex, StorageContext
from llama_index.vector_stores.weaviate import WeaviateVectorStore

index_name = "MyExternalContext"

# Construct vector store
vector_store = WeaviateVectorStore(
    weaviate_client = client, 
    index_name = index_name
)

# Set up the storage for the embeddings
storage_context = StorageContext.from_defaults(vector_store=vector_store)

# Setup the index
# build VectorStoreIndex that takes care of chunking documents
# and encoding chunks to embeddings for future retrieval
index = VectorStoreIndex(
    nodes,
    storage_context = storage_context,
)

步骤 5:设置查询引擎

最后,你将设置索引作为查询引擎。

# The QueryEngine class is equipped with the generator
# and facilitates the retrieval and generation steps
query_engine = index.as_query_engine()

步骤 6:对数据运行一个简单的 RAG 查询

现在,你可以对数据运行一个简单的 RAG 查询,如下所示:

# Run your naive RAG query
response = query_engine.query(
    "What happened at Interleaf?"
)

使用 LlamaIndex 实现高级 RAG

在本节中,我们将讨论一些简单的调整,帮助你将上述简单的 RAG 管道转变为高级 RAG 管道。这个教程将介绍以下一些高级 RAG 技术:

  • 预检索优化:句子窗口检索

  • 检索优化:混合搜索

  • 后检索优化:重新排序

由于我们这里只讨论修改部分,你可以在这个Jupyter Notebook 中找到完整的端到端高级 RAG 管道

索引优化示例:句子窗口检索

对于句子窗口检索技术,你需要进行两项调整:首先,你必须调整如何存储和后处理你的数据。我们将使用SentenceWindowNodeParser,而不是SimpleNodeParser

from llama_index.core.node_parser import SentenceWindowNodeParser

# create the sentence window node parser w/ default settings
node_parser = SentenceWindowNodeParser.from_defaults(
    window_size=3,
    window_metadata_key="window",
    original_text_metadata_key="original_text",
)

SentenceWindowNodeParser执行两项操作:

  1. 它将文档分割成单个句子,然后进行嵌入。

  2. 对于每个句子,它会创建一个上下文窗口。如果你指定window_size = 3,生成的窗口将包含三句,从嵌入句子的前一句开始,涵盖其后的句子。这个窗口将作为元数据存储。

在检索过程中,返回与查询最匹配的句子。检索之后,你需要通过定义一个MetadataReplacementPostProcessor并将其添加到node_postprocessors列表中,使用元数据中的整个窗口来替换该句子。

from llama_index.core.postprocessor import MetadataReplacementPostProcessor

# The target key defaults to `window` to match the node_parser's default
postproc = MetadataReplacementPostProcessor(
    target_metadata_key="window"
)

...

query_engine = index.as_query_engine( 
    node_postprocessors = [postproc],
)

检索优化示例:混合搜索

如果底层向量数据库支持混合搜索查询,在 LlamaIndex 中实现混合搜索只需要对query_engine进行两个参数调整。alpha参数指定了向量搜索与基于关键词搜索之间的权重,其中alpha=0表示基于关键词的搜索,alpha=1表示纯粹的向量搜索。

query_engine = index.as_query_engine(
    ...,
    vector_store_query_mode="hybrid", 
    alpha=0.5,
    ...
)

后检索优化示例:重新排序

向你的高级 RAG 管道中添加一个重新排序模型,只需要三个简单的步骤:

  1. 首先,定义一个重新排序模型。在这里,我们使用来自Hugging Face[BAAI/bge-reranker-base](https://huggingface.co/BAAI/bge-reranker-base)

  2. 在查询引擎中,将重新排序模型添加到node_postprocessors列表中。

  3. 在查询引擎中增加similarity_top_k参数,以检索更多上下文段落,重新排序后可以将其减少为top_n

# !pip install torch sentence-transformers
from llama_index.core.postprocessor import SentenceTransformerRerank

# Define reranker model
rerank = SentenceTransformerRerank(
    top_n = 2, 
    model = "BAAI/bge-reranker-base"
)

...

# Add reranker to query engine
query_engine = index.as_query_engine(
		similarity_top_k = 6,
		...,
                node_postprocessors = [rerank],
		...,
)

在高级 RAG 范式中还有许多不同的技术。如果您对进一步的实现感兴趣,推荐以下两个资源:

[## 构建和评估高级 RAG 应用

学习句子窗口检索和自动合并检索等方法,提高您的 RAG 管道性能…

www.deeplearning.ai](https://www.deeplearning.ai/short-courses/building-evaluating-advanced-rag/?source=post_page-----4de1464a9930--------------------------------) ## 高级 RAG 01:从小到大的检索

Child-Parent 递归检索器与 LlamaIndex 中的句子窗口检索

towardsdatascience.com

摘要

本文介绍了高级 RAG 的概念,这是一套技术,旨在解决朴素 RAG 范式的局限性。在概述了可以分为检索前、检索和检索后技术的高级 RAG 技术后,本文使用 LlamaIndex 实施了一个朴素和高级 RAG 管道来进行编排。

RAG 管道的组件包括来自OpenAI的语言模型,来自BAAI的重排序模型,该模型托管在Hugging Face上,以及Weaviate向量数据库。

我们使用 Python 中的 LlamaIndex 实现了以下技术:

  • 检索前优化:句子窗口检索

  • 检索优化:混合搜索

  • 检索后优化:重排序

您可以在这里找到包含完整端到端管道的 Jupyter Notebook:

喜欢这篇文章吗?

免费订阅 以在我发布新故事时收到通知。

[## 每当 Leonie Monigatti 发布新内容时,接收电子邮件通知。

每当 Leonie Monigatti 发布新内容时,您都可以收到电子邮件通知。通过注册,如果您还没有 Medium 账户,将会创建一个账户…

medium.com](https://medium.com/@iamleonie/subscribe?source=post_page-----4de1464a9930--------------------------------)

LinkedInTwitter,和 Kaggle上找到我!

免责声明

在撰写本文时,我是 Weaviate 的开发者倡导者。

参考文献

文献

[1] 高, Y., 熊, Y., 高, X., 贾, K., 潘, J., 毕, Y., … & 王, H. (2023). 面向大语言模型的检索增强生成:一项调查。arXiv 预印本 arXiv:2312.10997

图片

除非另有说明,所有图片均由作者创建。

更高级的递归与跟进检索技术,用于提升 RAG 性能

原文:towardsdatascience.com/advanced-retrieval-techniques-for-better-rags-c53e1b03c183?source=collection_archive---------5-----------------------#2024-08-12

破解问题就是解决一半,链式处理让问题解决得更好。

Thuwarakesh MurallieTowards Data Science Thuwarakesh Murallie

·发表于 Towards Data Science ·15 分钟阅读·2024 年 8 月 12 日

--

图片由 Mike Alonzo 提供,来源于 Unsplash

没有比查询修改更好的方式来提升大型语言模型(LLM)的表现。

在我最近的文章中,我讨论了五种查询翻译技巧以及它们如何改善 RAG 应用中的检索过程。一个技巧是查询分解。

## 提升 RAG 性能的五种经过验证的查询翻译技巧

如何在用户输入含糊不清的情况下获得近乎完美的 LLM 性能

[towardsdatascience.com

这个极好的技巧通过创建子问题来构建一个更详细的答案来回应我们最初的查询。这些子问题将被用于检索过程。最终的 LLM 会将每一对问题和答案作为上下文,生成对我们最初查询的全面回答。

本文讨论了另外两种常与查询分解结合使用的提升技巧,以获得更好的效果。

第一个技巧是递归回答,它通过大量生成子问题并递归回答它们。第二个技巧是跟进提问。正如你可能猜到的那样,我们回答这些问题…

2 百万令牌上下文窗口中的高级检索技术:关于重新排序器的第二部分

原文:towardsdatascience.com/advanced-retrieval-techniques-in-a-world-of-2m-token-context-windows-part-2-on-re-rankers-a0dfa03ba325?source=collection_archive---------7-----------------------#2024-08-26

Google DeepMind 推出的可视化 AI 项目。图像来自Unsplash

探索 RAG 技术以提高检索准确性

Meghan HeintzTowards Data Science Meghan Heintz

·发表于 Towards Data Science ·7 分钟阅读·2024 年 8 月 26 日

--

第一部分中,我们探讨了通过在执行检索之前重写查询来改善 RAG(增强检索生成)结果。本次,我们将学习如何通过重新排序来自向量数据库检索的结果来提高性能。

虽然我强烈建议尝试一些有前景的专有选项,如 Cohere 的 Re-Rank 3,但我们将主要关注了解研究人员在这一主题上分享的内容。

重新排序,意义何在?

首先,为什么要进行重新排序呢?向量数据库返回的结果是基于查询和文档的嵌入计算出的“相似度”分数。这些分数已经可以用来对结果进行排序,既然这已经是对文档和查询的语义相似度评分,为什么我们还需要另一步骤呢?

我们采取这种方法有几个原因:

  • 文档嵌入是“有损”的。文档在看到查询之前就已被压缩为向量格式,这意味着文档向量并未针对查询向量进行定制。重新排序使我们能够更好地理解文档与查询特定相关的含义

  • 两阶段系统已成为传统搜索和推荐系统的标准。它们在可扩展性、灵活性和准确性方面提供了改进。检索模型非常快速,而排名模型较慢。通过构建混合系统,我们可以平衡每个阶段之间的速度和准确性的权衡。

  • 重新排序允许我们减少将文档放入上下文窗口的数量,这a) 降低了成本并且b) 减少了相关数据被“埋没在大海捞针”中的机会

传统的重新排序方法

信息检索并不是一个新领域。在 LLMs 使用 RAG 改善生成之前,搜索引擎已经使用重新排序方法来改善搜索结果。两种流行的方法是 TF-IDF(词频-逆文档频率)和 BM25(最佳匹配 25)。

Karen Spärck Jones 在 1970 年代提出了 IDF(TF-IDF 的逆文档频率)概念,作为术语特异性的统计解释。其一般概念是,术语的特异性可以作为该术语出现的文档数量的倒数来量化。一个简单的例子是莎士比亚戏剧中的术语频率。因为“Romeo”一词仅出现在一部戏剧中,我们认为它对于该戏剧的主题比“sweet”一词更具信息性,因为“sweet”在所有戏剧中都有出现。

BM25 或 Okapi BM25 是由 Karen Spärck Jones 和 Stephen Robertson 共同开发的,作为 TF-IDF 的改进。BM25 是一种“词袋”检索函数,根据查询词在每个文档中出现的频率对一组文档进行排名,而不考虑这些词在文档中的相对位置。该方法在几个重要方面扩展了 TF-IDF:

  • BM25 使用一个饱和函数,其中一个词的频率越高,其重要性也随之增加,但回报是递减的。(旁注:这对于在搜索引擎优化(SEO)变得更加重要时保护准确性至关重要。你不能仅仅通过增加关键词的频率来作弊。)

  • BM25 包括文档长度归一化,确保较长的文档不会被不公平地偏袒。(这是另一个阻止潜在 SEO 游戏者的改进。)

这两种方法都可以在文档用于生成上下文之前,重新对向量数据库中的结果进行排序。这被称为基于“特征”的重新排序。

神经重新排序模型

你应该注意到传统方法的一个特点是,它们专注于精确的术语匹配。当文档使用语义相似但不同的术语时,这些方法将会遇到困难。像 SBERT(Sentence Transformers)这样的神经重新排序方法试图克服这一局限性。

SBERT 是一个经过微调的 BERT(Bidirectional Encoder Representations from Transformers)模型,采用了孪生/三元组网络架构,极大地提高了计算效率和计算句子相似性的延迟。像SBERT(Sentence-BERT)这样的变换器(Transformers)利用术语使用的上下文,使模型能够处理同义词和多义词。

SBERT 推理时的架构,例如计算相似度分数。该架构也用于回归目标函数。从Nils Reimers 和 Iryna Gurevych 的《Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks》

由于 SBERT 的专业化,它在语义相似度排名中通常表现更好。然而,使用 SBERT 的缺点是,你需要在本地管理模型,而不是调用 API,例如使用 OpenAI 的嵌入模型。明智地选择你所需的工具!

交叉编码器重新排序

向量数据库搜索中的前 K 个结果是与查询向量最相似的文档向量。另一种描述这种排名方法的方式是称其为“二重编码器”排名。向量在前期计算,并且近邻算法(ANNs)选择最相似的文档,这使得该方法成为一种高效的排名方式。但这种高效性是以牺牲一些准确性为代价的。

相比之下,交叉编码器通过对数据对进行分类机制来计算相似性。这意味着你需要为每个文档和查询提供一对数据。这种方法能够提供更加准确的结果,但效率极低。这也是为什么交叉编码器最好通过一种混合方法来实现,其中首先使用“二重编码器”前 K 个结果进行文档数量的修剪,然后再使用交叉编码器进行排名。你可以在SBERT 文档中了解更多关于如何一起使用二重编码器和交叉编码器的信息。

信息检索/问题回答检索图表,解释如何结合使用二重编码器和交叉编码器,来自SBERT 文档,完整引用见下文。

@inproceedings{reimers-2019-sentence-bert,
  title = "Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks",
  author = "Reimers, Nils and Gurevych, Iryna",
  booktitle = "Proceedings of the 2019 Conference on Empirical Methods in Natural Language Processing",
  month = "11",
  year = "2019",
  publisher = "Association for Computational Linguistics",
  url = "https://arxiv.org/abs/1908.10084",
}

基于提示的重新排序(PBR)

到目前为止,我们一直专注于使用向量或其他数值方法对我们的 RAG 结果进行重新排序。但这是否意味着我们没有充分利用 LLM?将文档和查询重新输入 LLM 进行评分,可能是一种有效的评分方式;采用这种方法时几乎没有信息损失。如果 LLM 被提示只返回一个单一的标记(即评分),则通常可以接受所产生的延迟(尽管这是扩展的较为昂贵的方式之一)。这被视为“零-shot”重新排序,关于这一主题的研究仍然有限,但我们知道它必须对提示的质量非常敏感。

(PBR)的另一个版本是DSLR 框架(基于句子级重新排序和重构的文档优化)。DSLR 提出了一种无监督方法,将检索到的文档分解成句子,基于相关性对其重新排序,然后在传递给 LLM 之前将其重构为连贯的段落。这种方法与传统依赖于固定大小段落的方式不同,后者可能包含冗余或无关的信息。在生成响应之前修剪掉无关的句子,可以减少幻觉现象并提高整体准确性。下面您可以看到 DSLR 优化如何提升 LLM 的响应。

来自DSLR: 基于句子级重新排序和重构的文档优化,以增强检索增强生成(由 Taeho Hwang, Soyeong Jeong, Sukmin Cho, SeungYoon Han, Jong C. Park 在韩国高等科学技术学院计算机学院提供)的示例 DSLR 优化

基于图的重新排序

有时,答案并不适合完全包含在一个单一的文档块中。书籍和论文的编写假设它们会被线性阅读,或者至少读者能够轻松地回溯到早期的段落。例如,当阅读关于 SBERT 的内容时,可能会要求回顾早先关于 BM25 的章节。在基本的 RAG 应用中,这是不可能的,因为您检索到的文档与之前的章节没有任何连接。

G-RAG,是由谷歌和 UCLA 的研究人员提出的一种方法,旨在缓解这一问题。G-RAG 是一种重新排序器,利用图神经网络(GNNs)来考虑检索到的文档之间的连接。文档被表示为节点,边表示文档之间共享的概念。这些图表被生成作为抽象意义表示(AMR)图,可以通过如github.com/goodbai-nlp/AMRBART(MIT 许可)等工具创建。

自然问题(NQ)TriviaQA(TQA)数据集的实验表明,这种方法在与其他最先进方法相比时,改进了平均联结倒数排名(MTRR)和联结平均命中率@10(TMHits@10)。

来自 UCLA 和谷歌研究人员:G-RAG 使用两个图来重新排序文档:抽象意义表示(AMR)图被用作文档级图的特征,文档图随后用于文档重排序。

结论

我希望你喜欢这个关于提高 RAG 应用性能的技术概述。我期待着该领域的持续进展,考虑到目前研究的飞速发展,我知道会有很多新的突破。

如果你有任何在本文中没有涵盖的最爱的重排序方法,请在评论区告诉我。

在 2 百万标记上下文窗口的世界中,先进的检索技术,第一部分

原文:towardsdatascience.com/advanced-retrieval-techniques-in-a-world-of-2m-token-context-windows-pt-1-2edc0266aabe?source=collection_archive---------10-----------------------#2024-07-15

探索 RAG 技术以提高检索准确性

Meghan HeintzTowards Data Science Meghan Heintz

·发布于Towards Data Science ·5 分钟阅读·2024 年 7 月 15 日

--

由 Google DeepMind 发起的 AI 项目可视化。图片来源:Unsplash

首先,我们还关心 RAG(检索增强生成)吗?

Gemini Pro 能够处理惊人的 2 百万标记上下文,而我们曾对 GPT-3.5 发布时的 15000 标记感到震惊。这是否意味着我们不再关心检索或 RAG 系统?根据针尖对麦芒基准,答案是,尽管需求在减少,特别是对于 Gemini 模型,先进的检索技术仍然能显著提升大多数 LLM 的性能。基准测试结果表明,长上下文模型在提取特定见解方面表现出色。然而,当需要引用时,它们却显得力不从心。这使得检索技术在需要引用质量至关重要的应用场景中尤为重要(比如法律、新闻和医疗应用等)。 这些往往是高价值的应用,缺少引用会让最初的见解变得大打折扣。此外,虽然长上下文模型的成本可能会下降,但通过检索器增强短内容窗口模型,可以是一条具有成本效益且延迟较低的路径,用于服务相同的应用场景。可以放心地说,RAG 和检索技术还会存在一段时间,但可能你不会从实施一个简单的 RAG 系统中获得太多回报。

来自《Haystack 总结:对长上下文 LLM 和 RAG 系统的挑战》(Summary of a Haystack: A Challenge to Long-Context LLMs and RAG Systems)的 2024 年 Laban、Fabbri、Xiong、Wu 的研究。“Haystack 结果总结,包括人类表现、RAG 系统和长上下文 LLM。使用三种指标报告结果:覆盖率(左)、引用(中)、联合(右)分数。Full 表示输入整个 Haystack 时的模型表现,而 Rand、Vect、LongE、KWs、RR3、Orac 表示 RAG 系统中的检索组件。模型按 Oracle 联合分数排名。对于每个模型,#Wb 报告每个要点的平均字数。”

那么我们应该实现哪些检索技术呢?

高级 RAG 涵盖了一系列技术,但大体上它们都属于检索前查询重写和检索后重排序的范畴。让我们深入了解并学习每种技术。

检索前 — 查询重写

问: “生命的意义是什么?”

答: “42”

问答不对称性是 RAG 系统中的一个巨大问题。对于较简单的 RAG 系统,一种典型的方法是比较查询与文档嵌入的余弦相似度。当问题几乎在回答中重述时,这种方法有效,例如:“梅根最喜欢的动物是什么?”,“梅根最喜欢的动物是长颈鹿。”但我们很少那么幸运。

以下是一些可以克服此问题的技术:

重写-检索-阅读

“重写-检索-阅读”的命名来自微软 Azure 团队于 2023 年发表的一篇论文(尽管考虑到这一技术的直观性,它已经使用了一段时间)。在这项研究中,LLM 会在获取相关上下文以回答问题之前,将用户查询重写为优化搜索引擎的查询。

一个关键的例子是如何将这个查询,“尼古拉斯·雷和埃利亚·卡赞共有的职业是什么?” 分解为两个查询,“尼古拉斯·雷的职业”“埃利亚·卡赞的职业”。这样可以获得更好的结果,因为单个文档不太可能同时包含两个问题的答案。通过将查询拆分为两个,检索器可以更有效地检索相关文档。

《用于增强检索的大型语言模型的查询重写》(From Query Rewriting for Retrieval-Augmented Large Language Models),由 Ma、Gong、He、Zhao 和 Duan 于 2023 年撰写。“(a) 标准的先检索后阅读方法,(b) LLM 作为查询重写器,用于重写-检索-阅读管道,(c) 可训练的重写器。”

重写也有助于克服“分心提示”所带来的问题。比如用户查询在提示中混合了多个概念,直接采用嵌入式方法会导致毫无意义的结果。例如,“太好了,谢谢你告诉我英国首相是谁。现在告诉我法国总统是谁”可以被重写为“现任法国总统”。这可以帮助你的应用程序在面对不同的用户时更具鲁棒性,因为有些用户会深思熟虑如何优化他们的提示,而另一些用户可能有不同的习惯。

查询扩展

在使用大语言模型(LLMs)进行查询扩展时,初始查询可以被重写为多个改写后的问题,或被拆解为子问题。理想情况下,通过将查询扩展为多个选项,可以增加初始查询与存储组件中正确文档之间的词汇重叠几率。

查询扩展是一个早于大语言模型(LLMs)广泛应用的概念。伪相关反馈(PRF)是一种启发了部分 LLM 研究者的技术。在 PRF 中,通过初始搜索中排名靠前的文档来识别和加权新的查询词条。而在使用 LLM 时,我们依赖模型的创造性和生成能力来寻找新的查询词条。这是有益的,因为 LLM 不局限于初始的文档集合,可以生成传统方法无法覆盖的扩展词条。

语料库驱动的查询扩展(CSQE)是一种结合了传统伪相关反馈(PRF)方法和大语言模型生成能力的技术。最初检索到的文档被反馈给 LLM,以生成新的查询词条用于搜索。这项技术在 LLM 缺乏某一领域知识的查询中表现尤为出色。

来自大语言模型驱动的语料库查询扩展,作者:Lei,Cao,Zhou,Shen,Yates,2024 年。“CSQE 概述:给定一个查询生物学定义和检索到的前两篇文档,CSQE 利用 LLM 来识别相关文档 1,并从文档 1 中提取对相关性贡献的关键句子。然后,查询通过这些来源于语料库的文本和 LLM 增强的扩展(即假设的回答查询的文档)进行扩展,以获得最终结果。”

无论是基于 LLM 的查询扩展,还是其前身如伪相关反馈(PRF)方法,都有一定的局限性。其中最显著的局限性是假设 LLM 生成的词条是相关的,或者假设排名靠前的结果是相关的。天啊,如果我在寻找澳大利亚记者哈利·波特的信息,而不是著名的男巫哈利·波特,结果会怎样呢?这两种技术都会将我的查询从较不流行的查询主题拉向更流行的主题,使得边缘案例的查询效果变差。

假设查询索引

另一种减少问题和文档之间不对称的方法是使用一组由 LLM 生成的假设问题对文档进行索引。对于给定的文档,LLM 可以生成可能由该文档回答的问题。然后,在检索步骤中,用户的查询嵌入会与假设问题嵌入和文档嵌入进行比较。

这意味着我们不需要嵌入原始文档片段,而是可以为该片段分配一个文档 ID,并将其作为元数据存储在假设的问题文档中。生成文档 ID 意味着在将多个问题映射到一个文档时,开销会小得多。

这种方法的明显缺点是,你的系统将受到存储的问题的创造力和数量的限制。

假设文档嵌入 — HyDE

HyDE与假设查询索引相反。与生成假设问题不同,LLM 被要求生成一个假设文档,该文档可能回答该问题,生成的文档嵌入被用于与真实文档进行检索。然后,真实文档用于生成回应。该方法在 2022 年首次推出时,相比其他当时的检索方法表现出显著的改进。

我们在 Dune 的自然语言到 SQL 产品中使用了这一概念。通过将用户的提示重写为可能回答问题的图表标题或说明,我们能够更好地检索可以作为 LLM 编写新查询上下文的 SQL 查询。

《无相关标签的精确零样本密集检索》,作者:Gao、Ma、Lin、Callan,发表于 2022 年。“HyDE 模型的示意图。展示了文档片段。HyDE 能处理所有类型的查询,而不改变底层的 GPT-3 和 Contriever/mContriever 模型。”

敬请关注第二部分关于重新排序方法的内容。

提升你的 RAG 的高级检索技术

原文:towardsdatascience.com/advanced-retriever-techniques-to-improve-your-rags-1fac2b86dd61?source=collection_archive---------0-----------------------#2024-04-17

精通高级信息检索:利用Langchain优化相关文档选择的前沿技术,打造卓越的 RAG

Damian GilTowards Data Science Damian Gil

·发表于 Towards Data Science ·18 分钟阅读·2024 年 4 月 17 日

--

目录

· 引言 · 向量存储创建 · 方法:朴素检索器 · 方法:父文档检索器 · 方法:自查询检索器∘ 查询构造器

∘ 查询翻译器 · 方法:上下文压缩检索器(重排名) · 结论

引言

让我们简要回顾一下构成 RAG 这个词的三个缩写的含义:

  • 检索:RAG 的主要目标是收集与查询相关的最相关文档/片段。

  • 增强:创建一个结构良好的提示,这样当调用 LLM 时,它能完全理解自己的目的、上下文以及应如何回应。

  • 生成:这是大语言模型(LLM)发挥作用的地方。当模型提供了良好的上下文(由“检索”步骤提供)并且有清晰的指令(由“增强”步骤提供)时,它将为用户生成高价值的响应。

正如我们所看到的,生成对用户查询的响应(如果我们应用 RAG 进行问答目的)直接依赖于我们如何构建“增强”部分,特别是“检索”部分。

在本文中,我们将专注于“检索”部分。在这一重要的返回最相关文档的过程中,向量存储的概念会出现。

本文展示的技术概述(图像来自作者)。

为了创建这些检索,我们将使用 Langchain 库。

本文中使用的技术概览(图片由作者提供)。

向量存储就是一个向量数据库,用于存储以向量格式表示的文档。这种向量表示来自于 transformers 的使用。我并不是说一些你此刻不知道的事情。

很显然,这个向量存储越强大、越完整,我们能运行的检索器就越好。我们已经知道,创建这个数据库本身就是一门艺术。根据我们使用的块大小或嵌入模型,RAG 的效果会有所不同。

在此我做出一个说明:

在这篇文章中,我们不会讨论如何创建这个向量存储。

在这篇文章中,我们将讨论一些用于检索相关文档的技术。

既然一图胜千言,我建议你查看以下内容:

一个 RAG 包含一系列明确的步骤。本文只会介绍检索器部分(图片由作者提供)。

因此,我再次强调,在这篇文章中,我们将深入研究创建一个优秀 RAG 工具的众多重要步骤之一。 “检索”步骤至关重要,因为它直接影响 LLM 生成响应时的上下文。

我们将研究的方法是:

  • 朴素检索器

  • 父文档检索器

  • 自查询检索器

  • 上下文压缩检索器(重新排序)

你可以在这里找到包含笔记本的项目 你也可以查看我的 github:

[## damiangilgonzalez1995 - 概览

对数据充满热情,我从物理学转向了数据科学。曾在 Telefonica、HP 工作,现在是…

github.com](https://github.com/damiangilgonzalez1995?source=post_page-----1fac2b86dd61--------------------------------)

向量存储创建

为了更好地阐释这些方法,将通过一个实际案例进行讲解。因此,我们将创建一个关于《约翰·威克》电影评论的 RAG。

为了让读者能跟随本文的每一个步骤,他们可以访问我创建的仓库。在里面,你会找到每种方法的代码,以及用于创建向量存储的文档。负责此任务的 jupyter notebook 可以在 git 仓库中找到,文件名为“0_create_vectore_db.ipynb”。

关于我们 RAG 的数据来源,有 4 个 csv 文件,每个文件对应约翰·威克系列电影的每部电影的评论。这些文件包含以下信息:

项目的数据集(图片由作者提供)。

如你所见,“评论”字段将是我们检索器的目标。其他字段作为元数据进行存储也很重要:

  • 电影标题

  • 评论日期

  • 评论标题

  • 评论网址

  • 作者

  • 评分

要将我们文件的每一行读取并转换为“文档”格式,我们执行以下代码:

from langchain_community.document_loaders.csv_loader import CSVLoader
from datetime import datetime, timedelta

documents = []

for i in range(1, 4):
  loader = CSVLoader(
    encoding="utf8",
    file_path=f"data/john_wick_{i}.csv",
    metadata_columns=["Review_Date", "Review_Title", "Review_Url", "Author", "Rating"]
  )

  movie_docs = loader.load()
  for doc in movie_docs:

    # We add metadate about the number of the movi
    doc.metadata["Movie_Title"] = f"John Wick {i}"

    # convert "Rating" to an `int`, if no rating is provided - None
    doc.metadata["Rating"] = int(doc.metadata["Rating"]) if doc.metadata["Rating"] else 5

  documents.extend(movie_docs)

我们已经有了“文档”格式的文档:

print(documents[0])

Document(page_content=": 0\nReview: The best way I can describe John Wick is to picture Taken but instead of Liam Neeson it's Keanu Reeves and instead of his daughter it's his dog. That's essentially the plot of the movie. John Wick (Reeves) is out to seek revenge on the people who took something he loved from him. It's a beautifully simple premise for an action movie - when action movies get convoluted, they get bad i.e. A Good Day to Die Hard. John Wick gives the viewers what they want: Awesome action, stylish stunts, kinetic chaos, and a relatable hero to tie it all together. John Wick succeeds in its simplicity.", metadata={'source': 'data/john_wick_1.csv', 'row': 0, 'Review_Date': '6 May 2015', 'Review_Title': ' Kinetic, concise, and stylish; John Wick kicks ass.\n', 'Review_Url': '/review/rw3233896/?ref_=tt_urv', 'Author': 'lnvicta', 'Rating': 8, 'Movie_Title': 'John Wick 1', 'last_accessed_at': datetime.datetime(2024, 4, 8, 11, 49, 47, 92560)})

我们只需要在本地创建一个向量数据库(向量存储)。为此,我使用了Chroma。同时,记住需要使用一个嵌入模型,它将把我们的文档转换为向量格式进行存储。所有这些内容可以在以下代码片段中看到:

from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
import os
from dotenv import load_dotenv

load_dotenv()
os.environ["OPENAI_API_KEY"] = os.getenv('OPENAI_KEY')

embeddings = OpenAIEmbeddings(model="text-embedding-3-small")

db = Chroma.from_documents(documents=documents, embedding=embeddings, collection_name="doc_jonhWick", persist_directory="./jonhWick_db")

这将在我们的服务器上创建一个名为“JonhWick_db”的数据库。这个数据库将被我们的 RAG 使用,检索器将从中获取与用户查询最相关的文档。

现在是时候介绍创建检索器的不同方法了。

方法:朴素检索器

代码见1_naive_retriever.ipynb文件。

这种方法是最简单的,实际上它的名字就说明了这一点。我们使用这个形容词来标识这种方法,原因很简单,当我们将查询输入到数据库时,我们(天真地)希望它返回最相关的文档/片段。

基本上发生的情况是,我们使用与创建向量存储时相同的转换器对用户查询进行编码。一旦获得其向量表示后,我们通过计算余弦相似度、距离等来计算相似性。

我们收集与查询最相似的前 K 个文档。

这种类型的检索器流程可以在下图中看到:

朴素检索器的简化表示(图像由作者提供)。

记住这个方案,我们来看看所有这些在代码中的表现。我们读取数据库:

from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
import os
from dotenv import load_dotenv

load_dotenv()
os.environ["OPENAI_API_KEY"] = os.getenv('OPENAI_KEY')

embeddings = OpenAIEmbeddings(model="text-embedding-3-small")

vectordb= Chroma(persist_directory="./jonhWick_db", 
                  embedding_function=embeddings, 
                  collection_name="doc_jonhWick")pyth

然后我们创建我们的检索器。我们可以配置相似性计算方法以及其他参数。

检索器

# Specifying top k
naive_retriever = vectordb.as_retriever(search_kwargs={ "k" : 10})

# Similarity score threshold retrieval
# naive_retriever = db.as_retriever(search_kwargs={"score_threshold": 0.8}, search_type="similarity_score_threshold")

# Maximum marginal relevance retrieval
# naive_retriever = db.as_retriever(search_type="mmr")

实际上,我们已经创建了我们的“朴素检索器”,但为了看看它是如何工作的,我们将创建完整的 RAG,回顾一下它由以下组件组成:

  • R(检索): 已完成

  • A(增强): 尚未完成

  • G(生成): 尚未完成

增强与生成

from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI

# Augmented
TEMPLATE = """\
You are happy assistant. Use the context provided below to answer the question.

If you do not know the answer, or are unsure, say you don't know.

Query:
{question}

Context:
{context}
"""

rag_prompt = ChatPromptTemplate.from_template(TEMPLATE)

# Generation
chat_model = ChatOpenAI()

我们已经拥有了 RAG 的三个组件。剩下的就是将它们组装起来,为此我们将使用 Langchain 的链来创建 RAG。

我不知道你是否了解 Langchain 为更高效地创建链而设计的语言。这种语言被称为LCEL(LangChain 表达语言)。如果你对这种创建 Langchain 链的方式不熟悉,我在这里为你提供一个非常好的教程:

最后,我们使用 Langchain 自有的链创建语言(LCEL)来创建我们的 RAG:

from langchain_core.runnables import RunnablePassthrough, RunnableParallel
from operator import itemgetter
from langchain_core.output_parsers import StrOutputParser

setup_and_retrieval = RunnableParallel({"question": RunnablePassthrough(), "context": naive_retriever })
output_parser = StrOutputParser()

naive_retrieval_chain = setup_and_retrieval 
                        | rag_prompt 
                        | chat_model 
                        | output_parser

naive_retrieval_chain.invoke( "Did people generally like John Wick?")

# response: 'Yes, people generally liked John Wick.' 

这是为 RAG 创建链条的最简单方法。在 Jupyter notebook 中,你可以找到同样的链条,但更为强大。因为我不希望我们现在在这个话题上迷失,所以我只展示了最简单的形式。同时,为了让我们理解上面代码的内容,我创建了这个非常清晰的图示:

使用 Langchain 创建 RAG 及其 LCEL 语言(图片来自作者)。

太好了,我们完成了创建我们的简单 RAG。接下来让我们进入下一个方法。

方法:父文档检索器

2_parent_document_retriever.ipynb文件中的代码。

假设我们创建了一个 RAG 来通过输入一些症状来识别可能的疾病。如果我们使用的是一个简单的 RAG,我们可能会收集到一系列只在一两个症状上相符的可能疾病,从而使我们的工具陷入困境。

这是使用父文档检索器的理想案例。这种技术的类型包括将大块(父块)切分成更小的块(子块)。通过拥有小块,它们所包含的信息更加集中,因此,它的信息价值不会在文本段落之间被稀释。

在这一切中有一个小问题:

  • 如果我们希望在搜索最相关的文档时更加精确,我们需要将文档分割成更小的块

  • 但同样重要的是为 LLM 提供良好的上下文,这可以通过提供更大的块来实现。

之前所说的内容可以通过以下图片看到:

这两种概念/度量之间平衡的表现(图片来自作者)。

看起来似乎没有解决问题的方法,因为当我们提高精度时,上下文会减少,反之亦然。这时,这种方法出现了,它将解决我们的困境。

主要思路是将大块(父块/文档)进一步切分成更小的块(子块/文档)。一旦完成这一点,就可以使用子块进行最相关的前 K 个文档的搜索,并返回属于这些前 K 个子文档的父块。

我们已经有了主要想法,现在让我们将其具体化。最好的解释方式是一步一步来:

  1. 获取文档并创建大块(父块)。

  2. 对每个父块进行分割以生成子块

  3. 将子块(向量表示)保存在向量存储中。

  4. 父块保存在内存中(我们不需要创建它们的向量表示)。

之前所说的内容可以通过以下图片看到:

这是如何从父块创建子块并进行存储的可视化表示。这些步骤是创建父文档检索器的必要步骤(图片来自作者)。

这看起来可能非常复杂,因为我们需要创建一个新的数据库,包含小块,且将父块保存在内存中。此外,还需要知道每个子块属于哪个父块。谢天谢地,Langchain的存在使得构建过程变得非常简单。

你一定已经得出结论,创建这种方法需要一个新的向量存储。此外,在像约翰·威克电影评论这样的数据源(如 CSV 文件)中,不需要进行第一次切分(父块)。这是因为我们可以将每一行 CSV 文件看作是一个独立的块。

总的来说,让我们通过以下图像来可视化这一方法的工作原理:

父文档检索器的工作原理的可视化表示(图片来自作者)。

编写代码的方式如下所示:

from langchain.retrievers import ParentDocumentRetriever
from langchain.storage import InMemoryStore
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma

# documents = Read csv files. Check jupyter notebook for more details

parent_docs = documents

# Embedding Model
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")

# Splitters
child_splitter = RecursiveCharacterTextSplitter(chunk_size=200)
# We don't need a parent splitter because the data cames from CSV file, and each row is a parent doc.
# parent_splitter = RecursiveCharacterTextSplitter(chunk_size=800)

# Stores
store = InMemoryStore()
vectorstore = Chroma(embedding_function=embeddings, collection_name="fullDoc", persist_directory="./JohnWick_db_parentsRD")

parent_document_retriever = ParentDocumentRetriever(
    vectorstore=vectorstore,
    docstore=store,
    child_splitter=child_splitter,
    # parent_splitter =parent_splitter
)

这里直观的一个概念是,向量存储中的块数(子块的数量)应该远远高于存储在内存中的文档数量(父块的数量)。我们可以通过以下代码进行检查:

print(f"Number of parent chunks  is: {len(list(store.yield_keys()))}")

print(f"Number of child chunks is: {len(parent_document_retriever.vectorstore.get()['ids'])}")

'''
Number of parent chunks  is: 75
Number of child chunks is: 3701
'''

很好,我们已经有了我们的父文档检索器,接下来只需要基于这个检索器创建我们的 RAG,完成方法与之前一样。我附上了在 langchain 中创建链条的代码。想查看更多细节,可以查看jupyter notebook

setup_and_retrieval = RunnableParallel({"question": RunnablePassthrough(), "context": parent_document_retriever })
output_parser = StrOutputParser()

parent_retrieval_chain = setup_and_retrieval | rag_prompt | chat_model | output_parser

请注意,这与之前的情况完全相同,只是有一个小区别,即在“setup_and_retrieval”变量中,我们配置了使用我们的“parent_document_retriever”,而不是“naive_retriever”

方法:自查询检索器

3_self_query_retriever.ipynb文件中的代码。

这可能是提高我们检索器效率的最优化方法之一。

它的主要特点是能够在向量存储中执行搜索,并应用基于元数据的过滤器。

我们知道,当我们应用“朴素检索”时,我们在计算向量数据库中所有块与查询的相似度。向量存储的块越多,需要进行的相似度计算就越多。现在,试想一下如果能先基于元数据进行过滤,然后在选择出符合元数据条件的块之后,再进行相似度计算。这可以极大地减少计算和时间成本。

让我们通过一个用例来全面理解何时应用这种类型的检索。

假设我们在向量数据库中存储了大量的体验和休闲活动(例如:冲浪课程、滑索、 gastronomic 路线等)。这些体验的描述是我们通过嵌入模型编码的。此外,每个活动都有三个关键值或元数据:日期、价格和地点。

假设用户正在寻找一种这样的体验:一种适合全家人且安全的自然体验。此外,价格必须低于$50,地点在加利福尼亚。

有一点是明确的

我们不希望你返回不符合用户请求的价格或地点的活动/体验。

因此,计算与不符合元数据过滤器要求的片段/体验的相似度是没有意义的。

这个案例非常适合应用自查询检索器(Self Query Retriever)。这种类型的检索器允许我们首先通过元数据进行过滤,然后再在符合元数据要求的片段和用户输入之间进行相似度计算。

这个技术可以总结为两个非常具体的步骤:

  • 查询构造器

  • 查询翻译器

查询构造器

查询构造器”步骤的目标是根据用户输入创建适当的查询和过滤器。

谁负责应用相应的过滤器?你怎么知道它们是什么?

为此,我们将使用一个 LLM(大型语言模型)。这个 LLM 必须能够决定何时以及应用哪些过滤器。我们还需要事先解释元数据是什么以及它们的含义。简而言之,提示(prompt)必须包含三个关键点:

  • 上下文:个性,应该如何行动,输出格式等。

  • 元数据:有关可用元数据的信息。

  • 查询:用户的查询/输入/问题。

LLM 生成的输出不能直接输入到数据库中。因此,需要所谓的“查询翻译器”。

查询翻译器

这是一个负责将 LLM(查询构造器)的输出转换为适当格式以执行查询的模块。根据你使用的向量数据库,你将需要使用不同的模块。以我为例,我使用的是Chroma db,因此,我需要一个针对这个数据库的翻译器。幸运的是,Langchain 几乎为所有数据库提供了特定的数据库翻译器。

正如你可能已经注意到的,我是一个图表的忠实粉丝。让我们看看以下图示,它能为这个问题提供相当清晰的解释:

自查询检索器的工作原理的视觉表示(图片来自作者)。

关于前面的图像,我们看到一切从用户的查询开始。我们创建一个包含 3 个关键字段的提示,并将其传递给 LLM,LLM 生成一个包含两个关键字段的响应:“Query” 和 “Filter”。这个响应被传递到查询翻译器,查询翻译器将这两个字段转换为 Chroma DB 所需的正确格式。执行查询并根据用户最初的问题返回最相关的文档。

需要强调的是,用户输入的查询不必与输入数据库的查询相同。在图示中可以看到,LLM 根据可用元数据和用户的提问,发现可以使用“评级”元数据创建一个过滤器。它还根据用户的查询创建一个新的查询。

让我们看看代码中的实现。如我所解释的,提供一个详细描述向量库中可用元数据的信息对 LLM 来说非常重要。这可以通过以下代码段来实现:

from langchain.chains.query_constructor.base import AttributeInfo
from langchain.retrievers.self_query.base import SelfQueryRetriever
from langchain_openai import ChatOpenAI
from langchain.retrievers.self_query.chroma import ChromaTranslator

metadata_field_info = [
    AttributeInfo(
        name="Movie_Title",
        description="The title of the movie",
        type="string",
    ),
    AttributeInfo(
        name="Review_Date",
        description="The date of the review",
        type="string",
    ),
    AttributeInfo(
        name="Review_Title",
        description="The title of the review",
        type="string",
    ),
    AttributeInfo(
        name="Review_Url",
        description="The URL of the review",
        type="string",
    ),
    AttributeInfo(
        name="Author",
        description="The author of the review",
        type="string",
    ),
    AttributeInfo(
        name="Rating",
        description="A 1 to 10 rating for the movie",
        type="integer",
    )
]

要定义我们的检索方法,我们必须定义以下几个要点:

  • 要使用的 LLM

  • 要使用的嵌入模型

  • 访问的向量库

  • 该文件的描述,说明可以找到哪些信息

    该向量库中的文档。

  • 元数据描述

  • 您希望使用的查询翻译器

让我们看看代码中的实现:

document_content_desription = "A review of the Jonh Wick movie."
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
chat_model = ChatOpenAI()

self_query_retriever = SelfQueryRetriever.from_llm(
    llm=ChatOpenAI(temperature=0),
    vectorstore =vectordb,
    document_contents = document_content_desription,
    metadata_field_info =metadata_field_info,
    verbose = True,
    structured_query_translator = ChromaTranslator()
)

让我们通过一个非常清晰的例子来看看,如何通过使用这种类型的检索器大大改进我们的 RAG。首先我们使用一个简单的检索器,然后使用自查询检索器。

Question = "Make a summary of the reviews that talk about John Wick 3 and have a score higher than 7"
response = naive_retrieval_chain.invoke(Question)
print(response)

'''
I don't know the answer.
'''
------------------------------------------------------------------------

response = self_retrieval_chain.invoke(Question)
print(response)

'''
John Wick: Chapter 3 - Parabellum is quite literally 
about consequences, dealing with the fallout of John's...
'''

正如我们所见,效果显著提升。

方法:上下文压缩检索器(重新排序)

代码见 4_contextual_compression_retriever(reranking).ipynb 文件。

  • 上下文窗口:我们从向量库中获取的文档越多,LLM 获得的信息就越多,从而能够给出更好的答案。

  • 召回率:从向量库中检索的文档越多,获取到不相关片段的概率越大,因此召回率会增加(这不是一个好事)。

似乎没有解决这个问题的方法。当我们在某个指标上提高性能时,另一个指标似乎注定要变差。我们确定吗?

这就是压缩检索器技术的应用,重点是重新排序技术。可以说,这项技术由两个截然不同的步骤组成:

  • 步骤 1:根据输入/问题获取大量相关文档。通常我们会设置最相关的 K 个文档。

  • 步骤 2:重新计算这些文档中哪些是真正相关的,丢弃那些不太有用的文档(压缩)。

对于第一步,使用的是所谓的双编码器(Bi-Encoder),这实际上就是我们通常用来进行基本 RAG 的方式。将文档向量化,将查询向量化,并使用我们选择的任何度量计算相似度。

第二步与我们通常看到的有所不同。这一重新计算/重新排序由重新排序模型跨编码器执行。

这些模型期望接受两个文档/文本作为输入,并返回它们之间的相似度评分。

如果这两种输入中的一个是查询,另一个是文档块,我们可以计算它们之间的相似度。

这两种方法可以如下展示:

计算文本之间相似度的两种方法的视觉表示(图像来源:作者)。

你会发现这两种方法最终提供了相同的结果,即反映两个文本之间相似度的度量。这完全正确,但有一个关键特点:

跨编码器返回的结果比双编码器更加可靠。

好的,它的效果更好,因为我们不会直接用所有文档块,而是只用前 K 个块。因为直接使用所有文档块将是在时间和金钱/计算上的巨大开销。因此,我们会首先筛选出与查询最相似的文档块, 将重新排序模型的使用限制在 K 次以内。

一个好的问题是:在哪里可以找到跨编码器(Cross-Encoder)模型?幸运的是,我们可以在HuggingFace找到开源模型,但在本文的实际案例中,我们将使用由公司Cohere提供的模型。

[## Cohere | 企业领先的人工智能平台

Cohere 提供行业领先的大型语言模型(LLMs)和 RAG 功能,专门为满足…

cohere.com](https://cohere.com/?source=post_page-----1fac2b86dd61--------------------------------)

为了更好地理解这种方法的架构,我们来看一个视觉示例。

上下文压缩检索器(重新排序)的视觉表示(图像来源:作者)。

该图像显示了步骤:

  • 1º) 我们获取查询,将其用变换器编码为向量形式,并将其输入到向量数据库中。

  • 2º) 从我们的数据库中收集与查询最相似的文档。我们可以使用任何检索方法。

  • 3º) 接下来我们使用 Cohere 的跨编码器模型。在图中的示例中,这个模型将被使用 4 次。记住,这个模型的输入将是查询和一个文档/文档块,用来计算这两篇文本的相似度。

  • 4º) 在之前的步骤中,已经对该模型进行了 4 次调用,并获得了查询与每个文档之间的相似度值(介于 0 到 1 之间)。如图所示,之前步骤中获得的第 1 块数据,在重排序后,现在排在第 4 位。

  • 5º) 我们添加了与上下文最相关的前三个块。

再次回到计算成本和时间,如果直接应用交叉编码器,考虑到每次新查询时,都需要计算查询与每个文档的相似度,这显然是非常低效的。

另一方面,使用双编码器时,文档的向量表示对于每个新查询都是相同的。

然后,我们有了一个更为优越的方法,虽然执行起来很昂贵,但另一方面,另一个方法运行良好,并且每个新查询的计算成本不大。所有这些都得出了统一这两种方法以改进 RAG 的结论。这就是所谓的带有重排序的上下文压缩方法

让我们继续进行代码部分。让我们记住,这种方法使用了一个检索器,在我们的案例中将是一个简单检索器:

naive_retriever = vectordb.as_retriever(search_kwargs={ "k" : 10})

由于Langchain和其与Cohere的集成,我们只需要导入一个模块,该模块将执行对 Cohere 交叉编码器模型的调用:

from langchain_cohere import CohereRerank

os.environ["COHERE_API_KEY"] = "YOUR API KEY FROM COHERE"

compressor = CohereRerank(top_n=3)

最后,我们创建了我们的带有 Langchain 的上下文压缩检索器

from langchain.retrievers.contextual_compression import ContextualCompressionRetriever

compression_retriever = ContextualCompressionRetriever(
    base_compressor=compressor, 
    base_retriever=naive_retriever
)

就这么简单。让我们来比较一下简单检索器和重排序检索器

这是重排序方法如何重新计算查询与块之间相似度的示例。这导致第一个检索器返回的最相关文档(在我们的案例中是简单检索器)被完全重新排序。前三个最相关的文档被收集,如所示(图片来自作者)。

如我们所见,简单检索器返回了前 10 个块/文档。经过重排序并获取到最相关的 3 个文档/块后,发生了显著变化。注意,文档编号 16,在第一次检索器中是第三位,在进行重排序后,变成了第一位

结论

我们已经看到,根据我们想要应用 RAG 的案例的特点,我们可能会选择不同的方法。此外,也可能出现不知道使用哪种检索器方法的情况。为此,存在不同的库来评估你的 RAG。

为此目的有几种工具可供选择。我个人推荐的几个选项是RAGASLangSmith的组合。

[## 使用 Ragas + LangSmith 评估 RAG 管道

编辑备注:本文由 Ragas 团队共同撰写。我们讨论和思考的一个问题是……

blog.langchain.dev](https://blog.langchain.dev/evaluating-rag-pipelines-with-ragas-langsmith/?source=post_page-----1fac2b86dd61--------------------------------)

我强烈推荐关注、学习并观看这些人制作的视频,他们正是启发我写这篇文章的来源。

[## AI 创客空间

与我们一起学习如何构建、部署和分享生产级大语言模型应用!

www.youtube.com](https://www.youtube.com/@AI-Makerspace?source=post_page-----1fac2b86dd61--------------------------------)

感谢阅读!

如果你觉得我的工作有用,可以订阅 每次我发布新文章时都会收到邮件

如果你愿意, 在 LinkedIn 上关注我

Pytorch 中的高级张量选择

原文:towardsdatascience.com/advanced-selection-from-tensors-in-pytorch-f012e52eef80?source=collection_archive---------9-----------------------#2024-02-27

使用 torch.index_selecttorch.gathertorch.take

Oliver STowards Data Science Oliver S

·发布于 Towards Data Science ·6 分钟阅读·2024 年 2 月 27 日

--

在某些情况下,您可能需要使用 Pytorch 进行一些高级索引/选择,例如回答这个问题:“如何根据张量 B 中指定的索引从张量 A 中选择元素?”

在这篇文章中,我们将介绍三种最常见的方法来处理这类任务,即 torch.index_selecttorch.gathertorch.take。我们将详细解释它们,并相互对比。

图片来源:Jerin JUnsplash

坦率地说,写这篇文章的一个动机是我忘记了如何以及何时使用哪个函数,结果不得不去谷歌搜索、浏览Stack Overflow以及官方文档(在我看来相对简短且不太有帮助)。因此,如前所述,我们将在此深入探讨这些函数:我们将阐明何时使用哪个函数,给出二维和三维的示例,并图示化展示选择结果。

我希望这篇文章能为这些函数带来清晰的解释,并消除进一步探索的需要——感谢阅读!

现在,话不多说,让我们逐一探讨这些函数。对于所有函数,我们首先从一个二维示例开始,展示其结果选择,然后再转向一个稍微复杂的三维示例。此外,我们还将在简单的 Python 中重新实现已执行的操作 —— 这样你就可以…

高级 SQL 用于数据科学

原文:towardsdatascience.com/advanced-sql-for-data-science-43a045ae4143?source=collection_archive---------1-----------------------#2024-08-24

提升你分析能力的专家技术

💡Mike ShakhomirovTowards Data Science 💡Mike Shakhomirov

·发布于 Towards Data Science ·14 分钟阅读·2024 年 8 月 24 日

--

AI 生成的图像,使用 Kandinsky

本故事深入探讨了对数据科学从业者有用的高级 SQL 技术。在本文中,我将详细介绍我在日常分析项目中使用的专家级 SQL 查询。SQL 与现代数据仓库共同构成了数据科学的基石。它是数据操作和用户行为分析中不可或缺的工具。我将要讲述的技术旨在从数据科学的角度提供实际和有益的应用。掌握 SQL 是一项宝贵的技能,对于广泛的项目至关重要,这些技术极大地简化了我的日常工作。我希望它对你也有所帮助。

鉴于 SQL 是数据仓库和商业智能专业人士使用的主要语言,它是跨数据平台共享数据的理想选择。其强大的功能促进了无缝的数据建模和可视化。它仍然是任何数据团队最受欢迎的沟通方式,也是市场上几乎所有数据平台的标准。

我们将使用 BigQuery 的标准 SQL 方言。运行我写的查询非常简单且免费,以下是我提供的 SQL 查询。

递归 CTE

使用数据可视化的高级谎言技巧

原文:towardsdatascience.com/advanced-techniques-in-lying-using-data-visualizations-794278c09844?source=collection_archive---------0-----------------------#2024-10-04

发现图表设计的力量,能够引导观众朝向任何叙事

Cai Parry-JonesTowards Data Science Cai Parry-Jones

·发表于 Towards Data Science ·9 分钟阅读·2024 年 10 月 4 日

--

来源:unsplash.com

本文为讽刺文章。利用虚假的可视化传播错误信息并不可取。本文的目的是教你如何识别错误信息。

你有一个需要推销的叙事吗?也许你在工作中预测了某些事情,现在需要证明它。也许 X 上的某人说你的政治观点是错的,所以你需要为反击准备弹药。

数据可视化通常是数据分析的最后一层,用于快速和简便地呈现见解。观众可以有所不同,从董事会成员到电视观众,从政府官员到社交媒体粉丝。然而,尽管他们各有差异,但他们通常有一些共同点。他们通常不是数据技术专家,而且他们通常没有时间深入细节。这使得数据可视化成为操控观众的完美工具,无论数据是什么,叙事是什么。

通过真实世界的示例,本文深入探讨了数据精通操控者用来传达自己信息的四种高级技巧,不论原始数据实际内容如何。这些技巧包括:

  1. 删除不需要的数据点

  2. 利用模式心理学

  3. 有选择地对数据进行分类

  4. 战略性地调整可读性

在阅读完本文后,你的工具包将完全装备好,能够以最强大的形式传播错误信息——统计数据。

TL;DR

  • 有选择性地删除或重新排列数据,通常足以支持相互矛盾的叙事。

  • 对于那些没有时间进行深入分析的读者来说,使用简单的心理模式,例如红色=坏,绿色=好,足以影响他们的判断。

  • 阻止数据操控的唯一方法就是在你看到时立即揭示出来。

省略不需要的数据点

如果你对健康生活感兴趣,你可能已经听说过马修·沃克(Matthew Walker)畅销书《为何我们睡觉》。健康类书籍可不是随便就能跻身《星期日泰晤士报》畅销书榜单的第一位的。那么,《为何我们睡觉》是如何取得如此惊人的成就的呢?如果你像我一样读过这本书,你就会明白其中的原因。这本书是一部强有力、甚至有些令人震惊的非虚构作品,核心信息是:如果你不认真对待睡眠,你将面临一系列健康问题

这本书的力量不仅仅体现在其信息内容上。书中包含了大量的学术研究和数据分析,这些研究和分析无可辩驳地支持了书中的叙述。到书的结尾,读者对这一信息没有任何怀疑——这一信息是正确的,而我们这些被启发的人,随后会去告诉其他人他们也应该读这本书。对于出版商而言,最终的结果是数千万美元的收入。我相信企鹅出版社肯定给了马修·沃克一个大大的赞扬,并顺便给了他几百万美元的奖励,自然的。

马修·沃克确保叙事完美无缺的做法,当然是省略了不需要的数据。书中没有提到任何与“少睡觉不影响健康,甚至可能对你有益”这一观点相矛盾的研究。然而,真正令人印象深刻的是,书中并没有出现任何实际使用的、冗余的数据分析点!

比如,在讨论运动伤害与平均睡眠时长的关系时,书中第 129 页展示了一张图表,呈现了一项研究论文的结果。图中有四个数据点,6 小时=72%,7 小时=60%,8 小时=34%,9 小时=18%。构建这张图表的基础数据来自一篇发表在《儿科矫形学杂志》(Journal of Paediatric Orthopaedics)上的研究,题为《慢性睡眠不足与青少年运动员运动损伤风险增加相关》。这篇论文的数据与图表中的数据完全一致,唯一的不同之处是,研究中还有一个额外的数据点,即平均睡眠时间为五小时的运动员,其受伤风险为 54%。

为什么这一个数据点会从 Matthew Walker 的图表中被省略?它来自一个同行评审的期刊,所以并不是说五小时的数据点不可用。当然,答案是它与叙事相悖。拥有一张图表,从最少睡眠=最差到最多睡眠=最好,显然更好。下次,当你遇到一个或两个恼人的数据点时,记住,你可以直接把它去掉。如果它对于一本由 UC Berkeley 教授编写、企鹅出版的书籍都可以接受,那对于我们来说也足够可以接受了。

由作者使用 Google Sheets 创建。

利用模式心理学

这可能看起来不像第一种技巧那样显而易见,但实际上非常简单。这是关于意识到人们看到图表时的直觉反应,并利用这一点为自己谋取利益。常见的感知模式包括:

  • 绿色 = 好

  • 红色 = 不好

  • 急剧上升 = 重要

  • 大 = 重要

  • 相关性 = 因果关系

福克斯新闻擅长做这种事情。然而,根据你观众批判性评估你工作的能力,你可能需要调整你利用这个技巧的程度。

由作者使用 Google Sheets 创建。

上述方法使用了常见的技巧,将 Y 轴从零以上开始。这个效果是数据点之间的差异更为显著,给人一种重要性的感觉。当然,如果数据点之间的变化与叙事相悖,你会希望将 Y 轴设置得尽可能低,从而传达出差异不重要的感觉。

另一种展示数据以符合你议程的方式是使用颜色。对于大多数人来说,红色与不好的事情相关。展示一张充满红色的图表在给观众提供恐惧、危机、警报等感觉时非常有效。气候变化是一个特别适合使用红色的主题,因为红色也用来象征热量和火焰。

《卫报》的一张图表,来自文章“澳大利亚气候危机和温室气体排放的变化”完美地展示了红色的使用。

“火热的世界”版本的图表。由作者使用 Tableau 创建。数据来源:全球碳预算,2022 年

注意看地图上每个国家都被标红,即使是那些零化石燃料排放的国家也会被标上粉红色的红。使用完全相同的数据,而没有全红色的色标,就会产生一张看起来不那么令人恐惧的图表,比如下面这张:

"只有部分世界着火"版本的图表。由作者使用 Tableau 创建。数据来源:全球碳预算,2022 年

在分类时要有选择性

这种技巧有点类似于省略数据。然而,它并不完全相同。省略数据只是将不需要的数据从视野中去除,而选择性分类则是将数据进行重新排列,直到我们找到合适的故事。这种方法也不容易受到批评,因此最常用于那些试图对分析进行批判性评估的领域,例如同行评审期刊中发表的论文。

我将从一个简单的示例开始,来解释我所说的数据重新排列。假设我在研究一个村庄中每户家庭的人数。我收集到了以下原始数据:

如果我想传达的信息是这个村庄有很多过度拥挤的家庭,那么我可以将数据呈现为如下所示:

由作者使用 Google Sheets 创建。

我所做的只是将 5、6、7 和 8 组合在一起,形成一个单一的数据点 5+。最终结果是,家庭中最多人的类别出现在最大的区域。完美符合我的叙事。

但是,如果我不想要这样呢?事实上,如果我想传达完全相反的信息呢?我可以稍微重新排列数据,最终呈现出如下数据:

由作者使用 Google Sheets 创建。

在这里,我所做的是将 0、1、2 组合在一起,3、4、5 组合在一起,6、7、8 组合在一起。如你所见,在没有遗漏任何一项原始数据的情况下,我完全改变了这个故事。

这种技巧在一篇学术研究论文中得到了巧妙的应用,研究的焦点是更多的枪支是否会减少或增加犯罪。由 John Lott 和 David Mustard 撰写的、被广泛引用的论文《犯罪、威慑与持枪权利》提供了一张关于实施隐蔽携带手枪法案后,增加街头枪支数量的后果的显著图表。

来源:《犯罪、威慑与持枪权利》 由 John Lott 和 David Mustard 撰写

再花一点时间仔细看看这张图表,它非常值得注意。它暗示着暴力犯罪几乎普遍是由于缺乏隐蔽携带手枪所导致的,这可以从缺乏变化中看出来,即在引入携带手枪法案后,犯罪率平稳下降。那么,作者是如何实现如此显著的图表的呢?

首先,它利用了第二种技巧,即利用模式心理学,通过将 Y 轴起始点设置为大于零。其次,最具影响力的是,它有选择地决定了哪些数据被视为‘暴力犯罪’,‘隐蔽携带手枪法’以及研究的‘人口’。例如,在暴力犯罪类别中,他们选择允许有枪的抢劫和加重袭击,但不包括没有枪的案件,如用刀抢劫。为什么?因为有枪抢劫和加重袭击在隐蔽携带手枪法实施后平均增加,这与叙事相悖。但如果仅仅选择这一部分暴力犯罪——即有枪的抢劫和加重袭击,那么在隐蔽携带手枪法实施后,平均数据恰好减少。

同样,一些地区和年份被排除在样本人口之外。这些地区和年份在隐蔽携带手枪法实施后暴力犯罪飙升。作者给出的理由是因为‘毒品价格上涨’(我没编造,这在第 24 页 “Crime, Deterrence, and Right‐to‐Carry Concealed Handguns” 中写着)。

如果再重复几次这种选择性分类,瞧——完美的图表!

战略性地调整可读性

如果遗憾的是,你无法避免在图表中展示一些难看的数据,还有一种最终的技巧可以用来保持你的叙事效果。让图表看起来令人无法忍受。一种经典做法是将数据以冰冷的表格形式呈现。例如:

作者使用 Microsoft Word 制作

你读完了吗?如果是的话,你比我更强。我得坦诚,至今我不清楚这些数据是否在试图操控我,我只知道我不想读那张丑陋的表格,你也不能逼我!

如果你确实有一些好的数据分布在那些破坏叙事效果的糟糕数据中,那么尽量让这些好数据尽可能易于阅读。简单明了的折线图或柱状图就可以做到。当然,确保所有糟糕的数据都采用表格形式呈现。

结论

给数据科学家、商业分析师、学术研究人员以及所有参与数据呈现的人的一条信息:操控观众可能很容易,但这并不可取。重要的是,要刻意考虑你的工作是否使用了以下四种技术。可能这并非有意为之,但确认偏误以及来自他人的压力确实会导致操控性数据技巧潜入某人的工作中。

给数据消费者(基本上是所有人)的一条信息:我希望这篇文章能够帮助你批判性地评估你遇到的数据和统计数字。此外,我鼓励读者对于发现的不良数据伦理问题大胆发声。无论是通过口头讨论、社交媒体发布、博客、视频文章、评论区留言……方式有很多种,看到问题时,请不要害羞,尽管批评它。

如果你想了解更多关于如何揭示数据问题的见解,我鼓励你阅读我之前的文章,停止盲目追求数据驱动,并关注我在Medium上的未来文章。如果你想扩大这篇文章的影响力,请为它点赞(或者给我 50 个😏)并在社交媒体上分享。感谢阅读。下次见,数据高手们,和平!✌️

使用 sktime 进行高级时间序列预测

原文:towardsdatascience.com/advanced-time-series-forecasting-with-sktime-af8eabc76173?source=collection_archive---------2-----------------------#2024-11-11

学习如何在几行代码中优化模型超参数,甚至进行架构优化

Dr. Robert KüblerTowards Data Science Dr. Robert Kübler

·发布于Towards Data Science ·9 分钟阅读·2024 年 11 月 11 日

--

图片来自JohnnyUnsplash

在我之前的文章中,我们探讨了使用 sktime 进行时间序列预测的基础知识,学习了如何利用这个强大的库进行简单的预测任务。现在,是时候将我们的旅程推进,并深入了解一些高级技巧,帮助你优化预测结果并提高准确性。在这篇后续文章中,我们将探索如何构建更复杂的模型,调整超参数,甚至使用 sktime 进行模型架构搜索。

## 使用 sktime 进行便捷的时间序列预测

如何让预测变得像散步一样简单

towardsdatascience.com

回顾

首先,为了便于入门,让我再次演示基本的 sktime 工作流程。这一次,我们将使用Longley 数据集,它是sktime 的一部分(BSD-3 许可证)。该数据集包含了从 1947 年到 1962 年间的各种美国宏观经济变量,内容如下:

使用 LLM 明智的建议

原文:towardsdatascience.com/advice-on-using-llms-wisely-388262b2ff50?source=collection_archive---------10-----------------------#2024-01-04

我在 LinkedIn 上关于 LLM 的十篇帖子

Lak LakshmananTowards Data Science Lak Lakshmanan

·发布于 Towards Data Science ·阅读时间 9 分钟·2024 年 1 月 4 日

--

1. LLM 中的非确定性问题

最佳的 LLM 用例是将 LLM 作为工具使用,而不是直接暴露它。正如Richard Seroter所说,你需要多少个聊天机器人?

[## Richard Seroter 在 LinkedIn 上:如何使任何基于目录的应用程序更具个性化,以便更好地服务于您的用户:我的经验分享…

诚实告诉我,这是一个糟糕的主意吗?如果生成型人工智能能够为我们提供更个性化的产品总结呢…

www.linkedin.com

然而,将静态产品页面替换为个性化的产品总结这一用例,与许多其他大型语言模型(LLM)用例一样,由于非确定性,它面临着独特的风险。想象一下,一年后有客户起诉你,声称他们因为你的产品总结错误地说该产品防火,导致他们购买了该产品并且他们的房子烧毁了。保护自己的唯一方法是保留每一个生成的总结记录,而存储成本会迅速增加…

避免这个问题的一种方法(也是我建议的方法)是使用 LLM 生成一组模板,并使用机器学习模型来选择哪个模板进行服务。这还具有允许人工监督生成文本的好处,因此你不至于受制于提示工程。(当然,这仅仅是使用 LLM 高效地为不同客户群体创建不同网站的一种方法——越是改变,越是与现有的想法产生共鸣)。

LLM 的许多使用案例是这样的:你需要通过精心的架构来减少非确定性行为及相关风险。

2. LLM 的版权问题

《纽约时报》正在起诉 OpenAI 和微软,指控它们使用《纽约时报》的文章。这一诉讼远超以往的诉讼,声称:

1. OpenAI 使用了数百万篇文章,并将其权重设定得更高,从而隐含地承认了《纽约时报》内容的重要性。

2. Wirecutter 的评论被逐字复制,但附属链接被去除。这创造了一个具有竞争力的产品。

3. 生成式人工智能模仿了《纽约时报》的表现风格,导致商标稀释。

4. 基于微软和 OpenAI 市值的增长,这项技术的价值对微软而言达到数万亿美元,对 OpenAI 则达到数十亿美元。

5. 由于原作是在相当高的成本下创作的,因此产生紧密总结并不具备变革性。

此次诉讼还针对 OpenAI 的公司结构,以及微软在构建 Azure 计算平台和选择数据集时与 OpenAI 的紧密合作关系。

www.nytimes.com/2023/12/27/business/media/new-york-times-open-ai-microsoft-lawsuit.html

整个诉讼文件长达 69 页,内容非常易读,并包含大量示例。我强烈推荐阅读文章中链接的完整 PDF 文件。

我不是律师,因此不会对诉讼的利弊发表看法。但如果《纽约时报》胜诉,我预计:

1. LLM API 的成本将会上升,因为 LLM 提供商需要支付其来源费用。此次诉讼不仅涉及训练和基础服务质量问题,还包括在推理过程中重现《纽约时报》文章。因此,成本将全面上升。

2. 开源 LLM 将无法使用 Common Crawl(《纽约时报》是该数据源第四大来源)。它们的数据集质量将下降,且它们将更难与商业产品相匹配。

3. 这将保护与创作独特且高质量内容相关的商业模式。

4. SEO 将更加偏向于在某个主题上成为排名第一或第二的最高权威。其他人将很难获得自然流量。预计通过广告获取客户的成本将会上升。

3. 不要直接使用 LLM;使用一个机器人创建框架。

雪佛兰经销商的事故

[## Documenting Meta (@documentingmeta) 在 Threads 上

OpenAI 讨厌这个简单的技巧。为什么要为 ChatGPT+ 付费,而沃森维尔的雪佛兰公司能免费提供?

www.threads.net

这也说明了为什么你永远不应该直接在你的网站上实施基于 LLM API 或自定义 GPT 的聊天机器人——你将很难驯服这个“怪物”。同时,你还将面临各种对抗性攻击,你需要花费大量的开发成本来防御这些攻击。

你应该怎么做?使用更高级的机器人创建框架,比如 Google Dialogflow 或 Amazon Lex。这些框架内建语言模型,只会回应有限数量的意图,从而让你避免代价高昂的教训。

4. Gemini 展示了 Google 对其研究团队的信心

www.linkedin.com/posts/valliappalakshmanan_what-a-lot-of-people-seem-to-be-missing-is-activity-7139380381916545024-Ki3a

很多人似乎忽视了 Google 高层对其研究团队的冷静信心。

把自己放在一年前 Google 高层的角度。你已经失去了先发优势,被一些初创公司赶超,这些公司采用了你认为过于冒险的技术。你需要作出回应。

你会押注于你的研究团队能够构建出一个单一模型,超越 OpenAI、Midjourney 等公司吗?还是会分散风险,构建多个模型?[Gemini 是一个单一模型,在文本上超越了最佳文本模型,在图像上超越了最佳图像模型,在视频上超越了最佳视频模型,在语音上超越了最佳语音模型。]

现在,想象一下你有两个世界级的实验室:Google Brain 和 DeepMind。你会将它们合并,并让 1000 人专注于一个产品吗?还是会通过让他们在两个不同的方向上工作来分散风险,期望其中一个成功?[Google 将这两个团队合并,命名为 Google DeepMind,在 DeepMind 负责人 Demis 的领导下,Brain 负责人 Jeff Dean 成为首席科学家。]

你有一款自主开发的定制机器学习芯片(TPU)。与此同时,其他所有公司都在基于通用芯片(GPU)构建模型。你会继续加大对内部芯片的投入,还是采取保守策略?[Gemini 是在 TPUs 上训练并服务的。]

在这些决策中,Google 选择了全力以赴。

5. 谁在真正投资于生成式 AI?

Omdia 对 H100 出货量的估算:

判断科技营销炒作的一个好方法是看看谁在真正投资于新产能。因此,Omdia 对 H100 出货量的估算是一个很好的指标,能够揭示谁在生成式 AI 领域获胜。

Meta 和 Microsoft 在 2023 年分别购买了 15 万个 H100,而 Google、Amazon 和 Oracle 各购买了 5 万个。(Google 的内部使用和 Anthropic 使用的是 TPUs,因此他们的生成式 AI 支出比 5 万个所表明的要高。)

有惊喜吗?

1. 苹果公司明显缺席。

2. 很好奇 Meta 在做什么。是否有大新闻即将发布?

3. Oracle 与 AWS 不相上下。

近年来芯片速度的提升不再是通过增加芯片上的晶体管数量(物理限制)来实现的。而是通过针对特定机器学习模型类型进行优化。

所以,H100 在变换器工作负载上,相比 A100(上一代)提升了 30 倍的推理速度,这得益于:(1)在变换器架构的不同层之间动态切换 8 位和 16 位表示;(2)提高了 GPU 之间的网络速度,允许进行模型并行处理(LLM 所需),而不仅仅是数据并行处理(足以应对图像工作负载)。除非你的机器学习模型确实有这一特定需求,否则你不会为每个芯片花费 30,000 美元。

同样,A100 相对于 V100 的提升在于使用了专门设计的 10 位精度浮点类型,这种类型在图像和文本嵌入任务中平衡了速度和精度。

所以,了解一家公司购买的芯片,可以让你猜测该公司正在投资的 AI 工作负载。(粗略地说:H100 也有硬件指令来处理一些基因组学和优化问题,因此它并不是 100%一清二楚的。)

6. 人们喜欢 AI 生成的内容,直到你告诉他们这是 AI 生成的

来自 MIT 的令人着迷的研究:

[## 研究衡量人们如何看待 AI 创作的内容 | MIT Sloan

打算使用生成性人工智能的公司应该首先考虑人们如何看待由 AI 创作的作品…

mitsloan.mit.edu](https://mitsloan.mit.edu/ideas-made-to-matter/study-gauges-how-people-perceive-ai-created-content?source=post_page-----388262b2ff50--------------------------------)

1. 如果你有一些内容,其中一部分是 AI 生成的,另一部分是人类生成的,人们更喜欢 AI 生成的!如果你认为 AI 生成的内容平淡无奇、平庸乏味,那你(和我)就是少数派。这与大多数人其实更喜欢连锁餐厅的食物类似——平淡的口味更适合更多人。

2. 如果你将内容标注为 AI 生成的或人类生成的,人们更喜欢人类生成的。这是因为他们现在会更高评价人类生成的内容,同时保持 AI 生成的评分不变。这有点像是某种“美德信号”或物种偏好。

基于此,当艺术家要求对 AI 生成的艺术作品进行标注,或者作家要求对 AI 生成的文本进行明确标记时,这只是特殊诉求吗?艺术家和作家是否在游说争取优待?

不是 LLM——但我的 AI 初恋——天气预报中的方法——正迎来它的时刻

[## Valliappa Lakshmanan 在 LinkedIn 上的动态:ECMWF | 图表

由 Google Deep Mind 的 Graphcast 模型创建的 10 天全球天气机器学习预报,现在可以在…上查看

www.linkedin.com

除了 GraphCast,还有其他基于机器学习的全球天气预报模型是实时运行的。Imme Ebert-Uphoff 的研究小组将它们并排展示(与 ECMWF 和 GFS 数值天气预报作为对照),如图所示:

lnkd.in/gewVAjMy

在像风暴预测中心春季实验这样的环境中,进行并行验证是至关重要的,只有这样这些预报才能用于决策制定。对于全球预报的等效方法我不太确定,但这样的评估是必要的。很高兴看到 CIRA 正在提供这一能力。

7. 大型语言模型(LLM)已趋于平稳

在 OpenAI 的开发者日活动之后,我感到非常失望。

8. 生成型 AI 软件的经济学

[## 生成型 AI ≠ 传统软件:关于技术中不断发展的商业模式的思考

人工智能几乎被视为软件的未来。这可能归因于人工智能应用的外观和感觉……

medium.com](https://medium.com/@annamalaipushpavalli/generative-ai-traditional-software-a-take-on-evolving-business-models-in-technology-a5205d9f73a6?source=post_page-----388262b2ff50--------------------------------)

生成型 AI 软件有两个独特的特点——(1)计算成本很高,因为训练/推理需要 GPU(2)数据壁垒较低,因为经过少量数据微调的小型模型能够与大型模型的性能相匹敌。鉴于此,软件通常具有低边际成本并能提供巨大的规模经济这一预期可能不再适用。

9. 帮助!我的书是大型语言模型训练数据集的一部分

www.linkedin.com/posts/valliappalakshmanan_seems-that-the-training-dataset-for-many-activity-7112508301090705409-McD_/

市场上的许多大型语言模型(LLMs)在它们的训练语料库中包含了名为 Books3 的数据集。问题在于,这个语料库包括了被盗版的书籍。我使用了由《大西洋月刊》文章的作者创建的工具

[## 揭示:那些被盗版书籍推动的生成型 AI 作者

斯蒂芬·金、扎迪·史密斯和迈克尔·波伦是成千上万位作家的其中之一,他们的版权作品正在被用来……

www.theatlantic.com

以检查我的书籍是否包含在语料库中。果然,似乎有一本书在其中。

这是一个幽默的帖子,但它捕捉到了真实的困境,因为没有人写技术书籍(整个读者群只有几千本)是为了赚钱。

10. 检测 LLM 生成文本中的幻觉事实的方法

www.linkedin.com/posts/valliappalakshmanan_bard-just-rolled-out-a-verify-with-google-activity-7109990134770528256-Zzji

由于 LLM 是自动补全机器,它们会根据前面的文本选择最可能的下一个短语。但如果某个话题的数据不足怎么办?那么,最“可能”的下一个短语就是该领域中许多不同文章的平均值,因此生成的句子很可能是事实错误。我们说 LLM “幻觉”了一个事实。

Bard 的这一更新利用了训练数据集中的频率与幻觉之间的关系,标记出生成文本中可能在事实上不准确的部分。

在 LinkedIn 上关注我: https://www.linkedin.com/in/valliappalakshmanan/

超越天真的 RAG:构建更智能和可靠的 AI 系统的高级技术

原文:towardsdatascience.com/beyond-na%C3%AFve-rag-advanced-techniques-for-building-smarter-and-reliable-ai-systems-c4fbcf8718b8?source=collection_archive---------0-----------------------#2024-10-16

深入研究高级索引、预检索、检索和后检索技术,以增强 RAG 性能

Abhinav KimothiTowards Data Science Abhinav Kimothi

·发表在Towards Data Science·阅读 27 分钟·2024 年 10 月 16 日

--

超越天真的 RAG:高级 RAG 技术(来源:作者提供的图片)

你是否曾经向生成式 AI 应用程序(如 ChatGPT)提出问题,发现答案不完整、过时或完全错误?如果有一种方法可以修复这个问题并使 AI 更准确呢?有!它被称为检索增强生成(Retrieval Augmented Generation,简称 RAG)。在 Lewis 等人在他们的开创性论文用于知识密集型 NLP 任务的检索增强生成中引入的一种新概念,RAG 迅速成为增强大型语言模型(LLMs)输出的可靠性和可信度的基石。已经证明,LLMs 在其参数中存储事实知识,也称为参数记忆,而这些知识根植于 LLM 训练的数据中。RAG 通过让 LLMs 访问外部信息存储库或知识库来增强 LLMs 的知识。这个知识库也被称为非参数记忆(因为它不存储在模型参数中)。在 2024 年,RAG 是生成式 AI 应用程序中最广泛使用的技术之一

60%的 LLM 应用程序利用某种形式的 RAG

代理式 AI:从零开始构建自主系统

原文:towardsdatascience.com/agentic-ai-building-autonomous-systems-from-scratch-8f80b07229ea?source=collection_archive---------0-----------------------#2024-12-13

生成性 AI 时代构建多代理框架的逐步指南

Luís RoqueTowards Data Science Luís Roque

·发表于 Towards Data Science ·阅读时长 13 分钟·2024 年 12 月 13 日

--

本文由 Rafael Guedes 共同撰写。

引言

生成性 AI 的兴起是数字时代的新平台变革。它解决了从大型企业自动化到各种类型的研发和创造力等问题。预计全球市场将在 2024 年超过 650 亿美元,86%的 IT 领导者预计会出现大规模的组织变革[1]。到目前为止,最大的回报来自聊天机器人(更为通用和广泛的应用场景)、代码助手和企业搜索。

投资仍在持续流入 AI 领域,2024 年投资额达到 138 亿美元(是 2023 年的六倍)[1]。此外,企业正将 AI 嵌入到它们的核心战略和系统中。像增强检索生成(RAG)、微调和针对垂直应用的专用模型(例如,医疗保健、法律)等技术正成为主流。

大型语言模型(LLMs)引起了人们对 AI 的关注(以多种方式),并为解决旧问题开辟了新途径。这个新方式就是通过代理式 AI —— 一个框架,其中自主代理协同工作以执行复杂的多步骤工作流。

我们的演示展示了如何构建并开发一个多代理系统。它整合了三个专门化的代理:

  • 一种网页研究代理……

如何在 RAGs 中实现接近人类水平的分块效果

原文:towardsdatascience.com/agentic-chunking-for-rags-091beccd94b1?source=collection_archive---------0-----------------------#2024-08-26

成本较高但强大的分块技术,有助于提升 RAG 的检索效果。

Thuwarakesh MurallieTowards Data Science Thuwarakesh Murallie

·发表于Towards Data Science ·8 分钟阅读·2024 年 8 月 26 日

--

图片由娜塔莉娅·维特凯维奇提供。

好的分块能带来好的 RAG。

分块、嵌入和索引是 RAGs(检索增强生成模型)的关键方面。使用合适的分块技术的 RAG 应用在输出质量和速度方面表现优异。

在构建 LLM(大语言模型)管道时,我们使用不同的策略来拆分文本。递归字符拆分是最流行的技术。它采用滑动窗口方法,窗口长度固定。然而,这种方法不能保证足够地容纳一个主题在其窗口大小内。此外,还有可能部分上下文会被分配到不同的分块中。

我喜欢的另一种技术是语义分割。语义分割在两个连续句子之间有显著变化时进行拆分。它没有长度限制,因此可以有许多句子,也可以只有很少的句子。但它更有可能准确捕捉到不同的主题。

即便是语义分割方法也存在问题。

如果彼此相距较远的句子在意义上更为接近,应该怎么处理呢?

智能体网:自主智能体生态系统的原则

原文:towardsdatascience.com/agentic-mesh-principles-for-an-autonomous-agent-ecosystem-42d1366de09f?source=collection_archive---------3-----------------------#2024-11-19

让自主智能体能够在日益增长的智能体网生态系统中找到彼此、协作、互动和交易的基本原则。

Eric BrodaTowards Data Science Eric Broda

·发表于Towards Data Science ·10 分钟阅读·2024 年 11 月 19 日

--

来源:Eric Broda

我们正处于智能体进化的初期阶段。但随着在智能体技术上的巨大投资,我预计很快我们将拥有真正自主的智能体,它们能够独立行动,并动态地规划和执行解决复杂商业问题的任务。

但在一个日益增长的自主智能体生态系统中,这些智能体将如何与人类互动?如何在拥挤的智能体环境中找到合适的智能体?这些智能体如何才能安全地协作、互动和交易?我之前的文章回答了这些问题。我将智能体网定义为一个互联的生态系统,使得智能体之间能够轻松地找到彼此、协作、互动和交易。

本文探讨了智能体网生态系统的基本原则,也许同样重要的是,探讨了在智能体网生态系统中运作的智能体的原则。在此过程中,我将回答几个简单的问题:

  1. 什么规则支配智能体,使它们能够在更大的生态系统中安全运作?

Agentic Mesh:生成型 AI 驱动的自主代理生态系统的未来

原文:towardsdatascience.com/agentic-mesh-the-future-of-generative-ai-enabled-autonomous-agent-ecosystems-d6a11381c979?source=collection_archive---------0-----------------------#2024-11-06

Agentic Mesh 创建了一个生态系统,使得自主代理能够找到彼此、合作、互动并以安全、高效且可信的方式进行交易。

Eric BrodaTowards Data Science Eric Broda

·发布于Towards Data Science ·阅读时间:20 分钟·2024 年 11 月 6 日

--

来源:Eric Broda

Agentic: /əˈd͡ʒɛn.tɪk/,能够在追求目标的过程中做出独立决策。 来源:Wiktionary

Agentic AI: 使用复杂的推理和迭代规划来自主解决复杂的多步骤问题。 来源:NVIDIA

自主代理使用 Agentic AI 独立规划和执行任务。

Agentic Mesh:一个互联的生态系统,使自主代理能够轻松找到彼此、合作、互动并进行交易。

最近的头条新闻已经说明了一切——自主代理即将到来!

  • Bloomberg, 2024 年 10 月 21 日:微软推出 AI 代理,加剧与 Salesforce 的竞争;“总部位于华盛顿州雷德蒙德的软件公司周一表示,将推出 10 个‘自主代理’,在销售、客户支持和会计等领域代表人们完成任务”

  • Bloomberg, 2024 年 10 月 24 日:AI 代理正式进入职场,带着缺陷和优点;“企业正在拥抱 AI……

使用 Python 和 Redpanda 聚合实时传感器数据

原文:towardsdatascience.com/aggregating-real-time-sensor-data-with-python-and-redpanda-30a139d59702?source=collection_archive---------0-----------------------#2024-05-20

使用 Python 和翻转窗口进行简单流处理

Tomáš NeubauerTowards Data Science Tomáš Neubauer

·发表于 Towards Data Science ·12 分钟阅读·2024 年 5 月 20 日

--

图片来源:作者

在本教程中,我想向你展示如何仅使用 Python(以及 Redpanda 作为消息代理)对传感器数据流进行降采样。目标是向你展示流处理的简单性,并证明你不需要一个高负载的流处理框架就能开始。

直到最近,流处理一直是一个复杂的任务,通常需要一定的 Java 专业知识。然而,随着 Python 流处理生态系统的逐渐成熟,Python 开发者现在有了更多的选择——例如 FaustBytewaxQuix。稍后,我将提供更多背景,解释为什么这些库会与现有的 Java 为中心的选项竞争。

但首先让我们来处理当前的任务。我们将使用一个名为 Quix Streams 的 Python 库作为我们的流处理器。Quix Streams 与 Faust 非常相似,但它在语法上进行了优化,使其更加简洁,并使用了一个类似 Pandas 的 API,叫做 StreamingDataframes。

你可以使用以下命令安装 Quix Streams 库:

pip install quixstreams

你将构建的内容

你将构建一个简单的应用程序,用来计算来自各种传感器的温度读数的滚动聚合。温度读数将以较高的频率进入,而这个应用程序将对读数进行聚合,并以较低的时间分辨率输出(每 10 秒一次)。你可以将其视为一种压缩形式,因为我们不希望在不必要的高分辨率数据上进行处理。

你可以在这个 GitHub 仓库中访问完整代码。

这个应用包含生成合成传感器数据的代码,但在实际场景中,这些数据可能来自多种传感器,例如安装在车队中的传感器或满载机器的仓库。

这里是基本架构的示意图:

作者绘制的图

流处理管道的组件

上图展示了流处理管道的主要组件:传感器是数据生产者,Redpanda 是流数据平台,Quix 是流处理器

数据生产者

这些是附加到生成数据的系统上的代码片段,例如 ECU(发动机控制单元)上的固件、云平台的监控模块,或者记录用户活动的 Web 服务器。它们将原始数据以该平台能够理解的格式发送到流数据平台。

流数据平台

这是你存放流数据的地方。它在功能上与数据库处理静态数据类似。但与数据库的表不同,流数据使用主题(topics)。否则,它的特性与静态数据库相似。你需要管理谁可以消费和生成数据,数据应该遵循什么样的模式。不同于数据库的是,流数据是不断变化的,所以它并不设计为可以查询。你通常会使用流处理器来转换数据,并将其放到其他地方供数据科学家探索,或者将原始数据存入一个优化过的查询系统,如 RisingWave 或 Apache Pinot,以便进行查询。然而,对于那些由流数据模式触发的自动化系统(如推荐引擎),这并不是理想的解决方案。在这种情况下,你肯定会使用专用的流处理器。

流处理器

这些是对数据执行持续操作的引擎,数据到达时会立即处理。它们可以比作处理任何应用程序后端数据的普通微服务,但有一个很大的不同。对于微服务来说,数据像雨滴一样一滴一滴地到达,每个“滴”都被独立处理。即使“下雨”很大,服务也能轻松跟上“滴”的速度而不至于溢出(可以类比为过滤系统,过滤掉水中的杂质)。

对于流处理器,数据以连续、广泛的水流形式到达。除非你改变设计,否则过滤系统会很快被压垮。也就是说,将数据流分开,并将较小的数据流路由到一系列过滤系统。这就是流处理器的工作原理。它们被设计为水平扩展并以并行的方式工作,像一个电池一样。而且它们从不停歇,持续处理数据,将过滤后的数据输出到流数据平台,后者充当流数据的蓄水池。为了让事情更复杂一些,流处理器通常需要跟踪先前接收到的数据,比如你将在这里尝试的窗口化示例。

请注意,还有“数据消费者”和“数据接收器”——这些系统消费处理后的数据(例如前端应用程序和移动应用)或将其存储用于离线分析(如数据仓库 Snowflake 或 AWS Redshift)。由于本教程不会涉及这些内容,我现在会跳过它们。

设置本地流数据集群

在本教程中,我将向你展示如何使用 Redpanda 的本地安装来管理流数据。我选择 Redpanda 是因为它非常容易在本地运行。

你将使用 Docker Compose 快速启动一个集群,其中包括 Redpanda 控制台,因此请确保你已先安装 Docker。

创建流式应用程序

首先,你将创建单独的文件来生成和处理你的流数据。这使得管理运行中的进程变得更容易。例如,你可以在不停止流处理器的情况下停止生产者。以下是你将创建的两个文件的概述:

  • 流数据生产者: sensor_stream_producer.py 生成合成的温度数据,并将数据生产(即写入)到 Redpanda 中的“原始数据”源主题中。就像 Faust 示例一样,它以大约每 5 秒 20 次读取的分辨率生成数据,或者每秒大约 4 次读取。

  • 流处理器: sensor_stream_processor.py 从“源”主题中消费(读取)原始温度数据,执行滚动窗口计算以降低数据的分辨率。它计算每 10 秒窗口中接收到数据的平均值,以便每 10 秒获得一次读取。然后,它将这些聚合后的读数生成到 Redpanda 中的 agg-temperatures 主题。

如你所见,流处理器完成了大部分繁重的工作,是本教程的核心。流数据生产者是一个用于模拟数据摄取过程的替代品。例如,在生产环境中,你可能会使用像这样的 MQTT 连接器 从传感器获取数据并将其生产到主题中。

  • 对于本教程,模拟数据更简单,因此让我们首先设置这些内容。

创建流数据生产者

你将从创建一个名为sensor_stream_producer.py的新文件并定义主要的 Quix 应用程序开始。(此示例已在 Python 3.10 上开发,但应该也可以在不同版本的 Python 3 上运行,只要你能够执行 pip install quixstreams)。

创建文件 sensor_stream_producer.py 并添加所有必要的依赖项(包括 Quix Streams)。

from dataclasses import dataclass, asdict # used to define the data schema
from datetime import datetime # used to manage timestamps
from time import sleep # used to slow down the data generator
import uuid # used for message id creation
import json # used for serializing data

from quixstreams import Application

然后,定义一个 Quix 应用程序和目标主题来发送数据。

 app = Application(broker_address='localhost:19092')

destination_topic = app.topic(name='raw-temp-data', value_serializer="json")

value_serializer 参数定义了预期源数据的格式(将被序列化为字节)。在这种情况下,你将发送 JSON。

让我们使用 dataclass 模块为温度数据定义一个非常基础的模式,并添加一个函数将其序列化为 JSON。

@dataclass
class Temperature:
    ts: datetime
    value: int

    def to_json(self):
        # Convert the dataclass to a dictionary
        data = asdict(self)
        # Format the datetime object as a string
        data['ts'] = self.ts.isoformat()
        # Serialize the dictionary to a JSON string
        return json.dumps(data)

接下来,添加将负责将模拟温度传感器数据发送到我们的 Redpanda 源主题的代码。

i = 0
with app.get_producer() as producer:
    while i < 10000:
        sensor_id = random.choice(["Sensor1", "Sensor2", "Sensor3", "Sensor4", "Sensor5"])
       temperature = Temperature(datetime.now(), random.randint(0, 100))
        value = temperature.to_json()

        print(f"Producing value {value}")
        serialized = destination_topic.serialize(
            key=sensor_id, value=value, headers={"uuid": str(uuid.uuid4())}
        )
        producer.produce(
            topic=destination_topic.name,
            headers=serialized.headers,
            key=serialized.key,
            value=serialized.value,
        )
        i += 1
        sleep(random.randint(0, 1000) / 1000)

这会生成 1000 条记录,记录之间的时间间隔是随机的,范围在 0 到 1 秒之间。它还会从 5 个选项中随机选择一个传感器名称。

现在,通过在命令行中运行以下命令来尝试生产者:

python sensor_stream_producer.py

你应该能看到类似以下的日志数据输出到控制台:

[data produced]

一旦确认它能正常工作,暂时停止进程(稍后你将在流处理过程中与其一起运行)。

创建流处理器

流处理器执行三项主要任务:1)从源主题消费原始温度读数,2)持续聚合数据,3)将聚合结果生成到汇聚主题。

让我们为这些任务的每个部分添加代码。在你的 IDE 中,创建一个名为 sensor_stream_processor.py 的新文件。

首先,像之前一样添加依赖项:

import os
import random
import json
from datetime import datetime, timedelta
from dataclasses import dataclass
import logging
from quixstreams import Application

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

让我们还设置一些我们的流处理应用程序所需的变量:

TOPIC = "raw-temperature" # defines the input topic
SINK = "agg-temperature"  # defines the output topic
WINDOW = 10  # defines the length of the time window in seconds
WINDOW_EXPIRES = 1 # defines, in seconds, how late data can arrive before it is excluded from the window

我们稍后将更详细地解释窗口变量的含义,但现在让我们继续定义主要的 Quix 应用程序。

app = Application(
    broker_address='localhost:19092',
    consumer_group="quix-stream-processor",
    auto_offset_reset="earliest",
)

请注意,这次有一些新的应用变量,分别是consumer_groupauto_offset_reset。要了解这些设置之间的相互作用,可以查看文章“理解 Kafka 的自动偏移重置配置:使用案例和陷阱”。

接下来,在核心流处理函数的两侧定义输入和输出主题,并添加一个函数,将传入的数据放入 DataFrame 中。

input_topic = app.topic(TOPIC, value_deserializer="json")
output_topic = app.topic(SINK, value_serializer="json")

sdf = app.dataframe(input_topic)
sdf = sdf.update(lambda value: logger.info(f"Input value received: {value}"))

我们还添加了一行日志记录,以确保传入的数据是完整的。

接下来,添加一个自定义时间戳提取器,以便使用消息负载中的时间戳,而不是 Kafka 的时间戳。对于你的聚合操作,这基本上意味着你想使用生成读数的时间,而不是它被 Redpanda 接收的时间。或者更简单地说,“使用传感器的时间定义,而不是 Redpanda 的”。

def custom_ts_extractor(value):

    # Extract the sensor's timestamp and convert to a datetime object
    dt_obj = datetime.strptime(value["ts"], "%Y-%m-%dT%H:%M:%S.%f") # 

    # Convert to milliseconds since the Unix epoch for efficent procesing with Quix
    milliseconds = int(dt_obj.timestamp() * 1000)
    value["timestamp"] = milliseconds
    logger.info(f"Value of new timestamp is: {value['timestamp']}")

    return value["timestamp"]

# Override the previously defined input_topic variable so that it uses the custom timestamp extractor 
input_topic = app.topic(TOPIC, timestamp_extractor=custom_ts_extractor, value_deserializer="json") 

我们为什么这么做呢?嗯,我们可以深入探讨在处理时使用哪种时间类型,但那是另一个话题。在自定义时间戳的情况下,我只是想说明,在流处理中有许多种时间解释方式,并且您不一定需要使用数据到达的时间。

接下来,在新窗口开始时初始化聚合的状态。当第一个记录到达窗口时,它将启动聚合。

def initializer(value: dict) -> dict:

    value_dict = json.loads(value)
    return {
        'count': 1,
        'min': value_dict['value'],
        'max': value_dict['value'],
        'mean': value_dict['value'],
    }

这设置了窗口的初始值。对于最小值、最大值和均值,它们都是相同的,因为您只是将第一个传感器读数作为起点。

现在,让我们以“reducer”函数的形式添加聚合逻辑。

def reducer(aggregated: dict, value: dict) -> dict:
    aggcount = aggregated['count'] + 1
    value_dict = json.loads(value)
    return {
        'count': aggcount,
        'min': min(aggregated['min'], value_dict['value']),
        'max': max(aggregated['max'], value_dict['value']),
        'mean': (aggregated['mean'] * aggregated['count'] + value_dict['value']) / (aggregated['count'] + 1)
    }

这个函数只有在对一个窗口执行多个聚合时才是必要的。在我们的例子中,我们为每个窗口创建计数、最小值、最大值和均值,因此我们需要预先定义这些值。

接下来是精彩的部分——添加滑动窗口功能:

### Define the window parameters such as type and length
sdf = (
    # Define a tumbling window of 10 seconds
    sdf.tumbling_window(timedelta(seconds=WINDOW), grace_ms=timedelta(seconds=WINDOW_EXPIRES))

    # Create a "reduce" aggregation with "reducer" and "initializer" functions
    .reduce(reducer=reducer, initializer=initializer)

    # Emit results only for closed 10 second windows
    .final()
)

### Apply the window to the Streaming DataFrame and define the data points to include in the output
sdf = sdf.apply(
    lambda value: {
        "time": value["end"], # Use the window end time as the timestamp for message sent to the 'agg-temperature' topic
        "temperature": value["value"], # Send a dictionary of {count, min, max, mean} values for the temperature parameter
    }
)

这定义了 Streaming DataFrame,作为基于滑动窗口的一组聚合——在 10 秒钟不重叠的时间段内进行的一组聚合。

提示:如果您需要复习不同类型的窗口计算,请查看这篇文章:“流处理中的窗口化指南”。

最后,将结果输出到下游的输出主题:

sdf = sdf.to_topic(output_topic)
sdf = sdf.update(lambda value: logger.info(f"Produced value: {value}"))

if __name__ == "__main__":
    logger.info("Starting application")
    app.run(sdf)

注意:您可能会想知道,为什么生产者代码与用于发送合成温度数据的生产者代码看起来差异很大(即使用 with app.get_producer() as producer() 的部分)。这是因为 Quix 使用了不同的生产者函数来处理转换任务(即在输入和输出主题之间的任务)。

正如您在跟随过程中可能会注意到的,我们会反复修改 Streaming DataFrame(sdf 变量),直到它变成我们希望下游接收的最终形式。因此,sdf.to_topic 函数只是将 Streaming DataFrame 的最终状态逐行流式传输到输出主题。

producer 函数则用于从外部源获取数据,如 CSV 文件、MQTT 经纪人,或在我们的案例中,一个生成器函数。

运行流应用程序

最后,您可以运行我们的流应用程序,看看所有的组件是否协调工作。

首先,在终端窗口中重新启动生产者:

python sensor_stream_producer.py

然后,在第二个终端窗口中,启动流处理器:

python sensor_stream_processor.py

注意每个窗口中的日志输出,以确保一切运行顺利。

您还可以检查 Redpanda 控制台,以确保聚合数据正确地流式传输到目标主题(您可以在这里找到主题浏览器:localhost:8080/topics)。

作者截图

总结

你在这里尝试的只是流处理的一种方式。当然,还有像 Apache Flink 和 Apache Spark Streaming 这样的重型工具,它们也已在网上广泛介绍。但——这些主要是基于 Java 的工具。当然,你可以使用它们的 Python 封装,但当问题出现时,你还是会调试 Java 错误,而不是 Python 错误。而且,Java 技能在数据领域的从业者中并不普遍,尤其是随着数据工程师越来越多地与软件工程师共同调优流处理算法。

在本教程中,我们进行了一个简单的聚合作为我们的流处理算法,但实际上,这些算法通常会使用机器学习模型来转换数据——而机器学习的软件生态系统则被 Python 主导。

一个常被忽视的事实是,Python 是数据专家、机器学习工程师和软件工程师协作的通用语言。它甚至比 SQL 更好,因为你可以用它做一些与数据无关的事情,比如进行 API 调用和触发 Webhooks。这也是为什么像 Faust、Bytewax 和 Quix 这样的库不断发展的原因之一——它们旨在弥合这些不同学科之间的阻抗差距

希望我已经向你展示了,Python 是流处理的可行语言,而且 Python 在流处理领域的生态系统正在稳步成熟,能够与老旧的基于 Java 的生态系统一较高下。

AI 代理能力工程

原文:towardsdatascience.com/ai-agent-capabilities-engineering-34c7785f413e?source=collection_archive---------5-----------------------#2024-06-18

引入一个 AI 代理的高级能力工程框架

Giuseppe ScalamognaTowards Data Science Giuseppe Scalamogna

·发表于Towards Data Science ·阅读时间 11 分钟·2024 年 6 月 18 日

--

来源:作者提供的图片,并通过 MidJourney 生成

介绍

在我最近的文章《从提示工程到代理工程》中,我提出了一个 AI 代理工程框架,介绍了一种心理模型,用于设计和创建 AI 代理。概括一下,该框架提出了以下结构:

  • AI 代理被分配任务

  • 任务要求完成特定的行动

  • 执行动作需要能力

  • 能力具有所需的熟练程度

  • 所需的熟练程度需要技术与技巧

  • 技术与技巧需要协调

如果你错过了那篇文章或需要参考它,可以在这里找到。

尽管框架看起来简单,但从更深层次来看,它涉及广泛的主题和理念。深入探讨由更大框架提出的概念是一项艰巨的任务,在这篇文章中,我们通过聚焦于 AI 代理能力工程框架,继续我们的工作。该框架的思路依赖于一种以分类为导向的思维方式,扩展了主要源于认知与行为科学的概念。

认知与行为科学基础

正如我在其他文章中提到的,贯穿人类工具与技术发展的历史,我们经常以自身作为我们试图构建的模型或灵感来源。AI 本身的一个典型例子是受到人脑启发的神经网络。因此,为 AI 代理能力建立框架,自然而然地将认知和行为科学作为灵感、指导和有用概念的扩展。让我们首先对这些科学的内容有一个高层次的理解。

认知科学

认知科学是关于心智及其过程的跨学科研究,涵盖心理学、神经科学、语言学和人工智能等领域。它为我们理解人类如何感知、思考、学习和记忆提供了关键见解。

行为科学

行为科学是一个跨学科领域,研究认知过程和行为,通常考虑个体与其环境之间的行为互动。它包括心理学、社会学、人类学和经济学等学科。

随着对 AI 代理可以完成的任务的期望不断提高,将我们的能力框架基于认知和行为理论,应该能为我们奠定坚实的基础,以开始满足这些期望,并帮助我们开启一个 AI 代理能够像人类一样熟练执行复杂工作的未来。

AI Agent Capabilities Framework

在我们深入研究细节之前,让我们从一个高层次考虑如何分类所谓的“能力”,以推动我们的代理需要采取的“行动”。我建议一般来说它们可以分为感知、思考、行动和适应几类。从那里我们可以进一步识别这些类别中更细粒度的能力示例。尽管所得到的框架在分类上是一致的,但请记住,粒度能力和类别之间的隐含关系是近似的。实际上,这些能力在整个框架中是紧密交织的,尝试模拟这种多维性在这个阶段并不特别有用。下面是构成框架的主要类别和子类别的视觉表达,没有您将很快看到的类别对齐。

虽然我们的主要关注点是由 LLM 中心的 AI 代理工程驱动的,为了未来证明和允许这些框架扩展到体现 AI 和机器人领域,我们还融入了适用于这些环境的概念。

最后,我们在框架中并未明确处理自主性问题,因为它更适合作为某个代理或其多个能力的一个总体现特征。也就是说,自主性并不是代理在执行其工作时必须具备的必要条件。

有了这些基础,我们接下来将扩展整个框架。

感知

包括代理从环境中获取、解读和组织感官信息的能力。它涉及对适当刺激的检测、识别和理解,使代理能够按预期执行任务。具体能力的示例包括:

  • 视觉处理: 图像和物体的识别与处理。

  • 文本数据处理: 文本的识别和处理。

  • 听觉处理: 语音和声音的识别与处理。

  • 触觉处理: 触觉识别和处理。

  • 嗅觉和味觉处理: 气味的识别和处理。

  • 感官整合: 将来自不同感官输入的数据整合以便形成一致的理解。

思考

指的是使代理能够处理信息、形成概念、解决问题、做出决策并应用知识的能力。具体能力的示例包括:

情境理解与意识

  • 情境意识与理解: 识别和理解情境、环境、空间和时间背景。

  • 自我意识与元认知: 自我意识、自我监控、自我评估、元认知知识。

注意力与执行功能

  • 选择性注意力: 专注于相关数据,同时过滤掉无关信息。

  • 分心注意力: 同时管理和处理多个任务或信息来源。

  • 持续注意力: 在较长时间内保持注意力和集中精力。

  • 规划: 制定一系列行动或策略以实现特定目标。

  • 决策制定: 分析信息、评估选项,并选择最佳的行动方案。

  • 抑制控制: 抑制不适当或不希望发生的行为或动作。

  • 认知灵活性: 在思考两个不同概念之间进行切换,或同时思考多个概念。

  • 情绪调节: 管理并以适当的情绪应对情感体验。

记忆

  • 短期记忆: 临时保持和操作信息。

  • 工作记忆: 主动处理和操作信息。

  • 长期记忆: 在较长时间内存储和提取信息。

推理与分析

  • 逻辑推理: 基于形式逻辑和结构化规则得出结论。

  • 概率推理: 基于概率和统计模型做出预测和决策。

  • 启发式推理: 运用经验法则或捷径来寻找解决方案。

  • 归纳推理: 从具体观察中进行概括

  • 演绎推理: 从一般原则或前提出发得出特定结论

  • 溯因推理: 形成假设以解释观察到的现象

  • 类比推理: 通过寻找与之前遇到的情况相似之处来解决问题

  • 空间推理: 理解和推理空间关系

知识利用与应用

  • 语义知识: 获取并应用构成概念的世界一般知识和特征

  • 情景知识: 获取和使用特定事件和经历的知识

  • 程序性知识: 知道如何高效地执行任务和行动

  • 陈述性知识: 获取和使用事实信息

  • 语言理解: 理解和解释语言

社交与情感智能

  • 情感识别: 检测和解释情感

  • 社交互动: 以社交适当的方式与人类或其他代理互动

  • 同理心: 理解并回应他人的情感状态

  • 心智理论: 推断和理解心理状态、意图和信念

  • 社交感知: 识别和理解社交线索和上下文

  • 关系管理: 管理和培养长期关系

创造力与想象力

  • 创意生成: 产生新的和创新的想法

  • 艺术创作: 创作原创艺术作品,如音乐、视觉艺术和文学

  • 富有想象力的思维: 设想并阐述超越当前现实的新可能性和情境

描述: 涉及代理与环境互动和执行任务的能力。它包括数字和物理行为。这一类别的能力还涵盖了沟通与互动,使代理能够与用户和其他系统进行有意义的交流。具体能力的例子包括:

  • 数字行动执行: 执行特定的数字操作,包括输出生成、自动化、问题解决、决策实施和响应行动。

  • 身体行动执行: 规划、启动和调整运动,将感官信息与运动动作结合,抓取和处理物体,学习和适应新的运动技能。

  • 人类沟通与互动: 与用户进行有意义的对话,处理多种语言,并保持对话的上下文。

  • 代理与系统沟通与互动: 与其他 AI 代理和系统有效沟通与协调,使用协议和接口交换信息、同步行动,并在平台间保持互动上下文。

适应

描述: 指的是允许代理根据新信息、经验和反馈调整和进化其行为、过程和情感反应的能力。明确来说,我们在这里关注的是代理在其操作状态下的适应和学习能力,而非其基础能力启用过程中发生的学习。在我们的框架中,这属于工具与技术的范畴。具体能力的例子包括:

学习

  • 认知学习: 通过认知过程获取知识

  • 模仿学习: 通过观察和复制他人的行为来获取新技能和行为

  • 体验式学习: 通过经验和反思学习

适应与进化

  • 行为适应: 根据反馈或环境变化调整行为

  • 认知适应: 基于新信息调整认知过程

  • 情感适应: 基于经验和情境调整情感反应

  • 运动适应: 通过练习和反馈来调整运动技能

  • 社会适应: 根据社会线索和互动调整社会行为

  • 进化: 随着时间推移,行为和认知过程的长期变化与改进

由于这篇文章旨在简要说明,而非详细讨论每一个例子中的具体能力,因此我们不会深入探讨每个具体能力的细节。虽然我希望这已涵盖了所有内容,但充其量它只是一个良好的开始。通过不断迭代和反馈,我们必将对其进行修订、改进,并朝着一个更稳定的框架迈进,进而适合更广泛的应用。

现在让我们转向一些示例,说明框架的实际应用以及它如何在代理工程设置中发挥价值。

实践中的 AI 代理能力框架

AI 代理能力框架的实际应用涉及利用其结构化的概念,这些概念根植于认知和行为科学,来促进设计思维过程。考虑到我们在设想和表述代理期望能力时的多样性,这一框架有助于建立一个共同的基础,促进能力设计和工程的一致性和全面性。随着对我们 AI 代理能力的期望水平不断提升,这一框架尤为重要。让我们来看一个例子:

客户支持 AI 代理

让我们考虑一个 AI 代理,它的工作是提供客户支持和个性化的产品推荐。在这个框架的基础上,我们可以设定一个更高保真度的工作和场景描述,描绘一个更加生动的画面。

工作职责: 提供卓越且富有同理心的客户支持和产品推荐,同时主动预测销售趋势,并结合详细的上下文元素,进行高度个性化的互动。

场景: 这是一个繁忙的在线客服环境,我们的人工智能代理不仅需要解决客户查询并提供产品推荐,还需要通过预测需求和个性化互动来提升整体客户体验。这是一个涵盖广泛行动和能力的工作。几年前,构建这些能力中的一些几乎是不可能的。那么,是否能够通过我们的人工智能代理能力框架有效地表达这个工作的能力,以便评估其可行性呢?让我们深入了解一下,同时请记住,以下大纲并非全面详尽:

所需行动:

  • 理解并解读客户查询。

  • 提供准确且有帮助的回应。

  • 适当时上报问题。

  • 基于客户互动预测销售趋势。

  • 提供产品推荐。

所需能力:

  1. 感知
  • 文本数据处理: 识别并理解客户书面查询,包括复杂句子和俚语。

  • 听觉处理: 转录并理解口头查询,即使在嘈杂的环境中。

  • 视觉处理: 在视频支持过程中解读视觉线索和肢体语言。

2. 认知

情境理解和意识:

  • 时间意识: 识别季节性趋势和高峰期。

  • 位置意识: 理解地理位置数据。

  • 个人情境意识: 了解个别客户的历史和偏好。

记忆:

  • 短期记忆: 保持最近的互动以维持情境。

  • 长期记忆: 利用过去的互动来保持情境。

推理与分析:

  • 概率推理: 从客户互动中识别模式,预测未来行为。

  • 推理逻辑: 应用逻辑框架解决问题。

  • 行为分析: 理解并解读客户行为模式。

  • 趋势分析: 理解当前市场趋势和季节性数据。

知识利用与应用

  • 语义知识: 运用一般世界知识来理解和回应查询。

  • 情节性知识: 利用特定事件和过去的经验提供相关支持。

  • 声明性知识: 获取事实信息,以提供准确的回应。

社交与情感智力

  • 情绪识别: 检测并解读客户情绪。

  • 社交互动: 以社交适当的方式与客户互动。

  • 心智理论: 推测客户需求,并提前提供解决方案。

  • 关系管理: 与客户建立良好关系,促进忠诚度。

创造力和想象力

  • 富有想象力的思维: 展望当前问题之外的新可能性。

行动

数字互动:

  • 输出生成: 快速、准确、并具有情境适应性的回应。

  • 产品推荐生成: 根据客户偏好和其他相关分析推荐产品。

人类沟通与互动:

  • 对话连续性: 在多次互动中保持上下文。

代理与系统之间的通信:

  • 代理间协调: 与其他 AI 系统沟通,以同步行动并共享见解。

适应

学习:

  • 体验式学习: 持续提升对客户行为的理解。

适应:

  • 行为适应: 根据反馈调整互动风格。

  • 认知适应: 用新信息更新知识。

  • 情感适应: 调整情感反应。

其中一些见解可能会让人感到惊讶。例如,AI 代理是否应该具备关系管理能力?又比如,AI 代理是否可以通过视频观察并回应它们能够“观察到”的全新数据点,从而伪装成一个具象的存在?当然,关于隐私的关注和问题层出不穷,但这并不是我们应该完全排除的概念。

通过技术和方法创造能力

尽管本文将不会集中评估启用这些能力的技术和方法,但我们应该回答在完成上述分析后自然会出现的问题。大型语言模型(LLMs)难道不能直接提供大多数这些能力的工具吗?

虽然大型语言模型(LLMs)确实在技术上取得了飞跃性的进展,但简单的答案是:没有。尤其是在推理和分析能力方面,尽管 LLMs 能够令人印象深刻地模拟看似推理或分析的过程,但远远不及人类在这些方面的能力。简而言之,LLMs 提供了一个不完全可靠但强大的捷径,使得许多这些能力得以实现。它们代表了智能和互动技术中一个极为重要的进化步骤,它们的前所未有的采用有助于解释为什么关于人工通用智能(AGI)的概念会引发如此大的兴奋。尽管有关其实际涵义的定义仍然是争论的主题,但如果实现,它可能成为实现上述许多认知/行为能力的首选技术解决方案。

结论

我希望你会发现 AI 代理能力工程框架是一种定义你的 AI 代理能力的深刻方法。通过整合认知和行为科学的概念,该框架旨在指导 AI 代理开发执行复杂任务所需的能力。该框架相对复杂,并且肯定会随着时间的推移而演变。目前阶段的关键要点是围绕“感知、思考、行动和适应”构建的心理模型。这四个高层次的概念本身就为有效组织和开发代理能力提供了一个非常坚实的基础。

感谢阅读,并请关注此框架的未来改进以及其他方面的 AI 代理工程框架扩展。如果你想进一步讨论此框架或我所写的其他话题,欢迎通过LinkedIn与我联系。

除非另有说明,本文中的所有图片均由作者提供。

Langfuse 中的 AI 代理单元测试

原文:towardsdatascience.com/ai-agent-unit-testing-in-langfuse-00d21a680ddc?source=collection_archive---------5-----------------------#2024-06-13

为 AI 代理创建一个可扩展的测试解决方案,供非编码人员使用

Jack MooreTowards Data Science Jack Moore

·发布于Towards Data Science ·8 分钟阅读·2024 年 6 月 13 日

--

Langfuse 是一个用于灵活测试 AI 代理的有用工具。最近,我们着手实现一个框架来测试基于聊天的 AI 代理。以下是我们在探索可用工具过程中所经历的过程。

我们将主要集中讨论如何完成这项任务,但在最后,我们会谈到一些仍然面临的挑战,以及现有工具如何更好地支持这种用例的前景。

用例概述

在回顾我们如何构建系统之前,我们将快速介绍我们的目标和成功标准。

生成式 AI 的用例通常容易部署,但难以控制。当部署带有大上下文模型的代理时,模型提示、温度设置、内容审核政策等上游的变化可能会大幅影响其性能。

挑战在于创建一个系统,可以评估代理在不产生幻觉或违反内容政策的情况下完成特定任务的能力。我们将其等同于单元测试,确保你的代理即使在团队可能专注于特定改进的情况下,也能保持完成广泛任务的能力。手动进行这种测试可能不准确、耗时且难以追踪。

因此,我们着手创建一个能够轻松创建这些测试并监控其结果的系统。重要的是,我们希望这个系统能够在最小的代码更改频率下操作,以便产品经理或质量测试人员可以在不接触代码的情况下参与其中。

为什么选择 Langfuse

我们为搜索设定了几个关键参数:

HIPAA 合规性,因为我们构建的产品以及许多我们的咨询合作伙伴都涉及医疗行业。

低成本,既包括搭建成本,也包括运行成本,因为我们运作得相当精简,我们的合作伙伴也是如此。

发展势头。LLM 可观测性领域正在快速发展。从我们搜索的开始,我们就做好了可能会错的准备,但我们希望通过选择一个有可能与我们一起发展的工具来最小化这种机会。

自定义 LLM 评估能力。能够搭建和运行自定义评估器,这一点令人惊讶地没有在我们找到的所有选项中得到广泛支持,尤其是在开源选项中。

为了简化我们的搜索,我们确定了以下几个在企业和开源类别中符合我们标准的工具,按大致排名顺序列出。

企业

开源

我们选择 Langfuse,主要因为它可以轻松自行部署,无需与企业销售团队互动,并且我们相信它具备我们需要的关键功能。到目前为止,这个选择证明是正确的。

部署

我们发现部署过程总体上相对简单。Langfuse 提供了一个易于使用的 Docker 镜像,并且有详尽的文档指导如何在本地部署。构建 YAML 文件并部署到 EKS 也非常简单,我们在几个小时内就成功启动了一个演示实例。我们没有为 POC 设置 SSO,因此使用的是开箱即用的基本用户管理功能(功能不多),并依赖匿名化数据来满足安全要求。RDS 上的免费层 PG 数据库能够处理多个用户的许多查询、评估和提示管理。该应用非常轻量级。我们遇到的一些问题包括:

  • SDK 中无法编程方式获取提示列表。这意味着当我们组合各种系统提示或单元测试聊天时,我们必须将提示名称存储在我们为特定用例使用的任何入口点的配置文件中(例如,代理的系统提示中的单元测试列表)。

  • 我们没有找到一种方法能够获取提示中用于编译的变量列表。我们使用不同的变量用于不同的系统提示,并且不得不硬编码每一部分数据应编译到哪个提示中,或者进行一些试错。

  • 观察记录没有很好地记录。当将分数记录到 Langfuse 时,我们看到可以添加 observationId,但虽然文档整体不错,但并未提供更多的上下文信息。一旦我们弄清楚这些功能的所有可能性后,可能会在未来使用它们。

一个无代码单元测试框架

我们如何利用系统提示配置在 Langfuse 中创建一个中央的、无代码的测试系统的示意图

经过几周的工作,我们建立了一个端到端测试系统。Langfuse 提供的功能超出了我们目前的使用范围,但我们重点使用了提示、会话和追踪。

将聊天历史作为测试的上下文

在对基于聊天的代理进行测试时,我们的一个关键需求是能够将代理插入到聊天场景的中间,使用之前交换的消息作为上下文。任何自定义提示都可以包括聊天历史,但 Langfuse 特别简便地实现了这一点。

此外,我们为代理构建了一个聊天界面,使用户能够实时测试并生成新的测试提示以进行评估。这解决了其中一个问题。

注入提示作为上下文的潜在问题是,聊天必须代表模型可能生成的实际输出。

这带来了一个潜在的漏洞:我们作为上下文使用的聊天历史如果模型的底层行为发生变化,则必须刷新。尽管如此,我们认为这种方法比潜在的替代方案更可控和一致,例如让一个代理与另一个代理交互——我们将探索这一点,作为这种系统的另一个扩展。

无代码的测试创建与测试运行管理

我们解决的另一个关键挑战是如何在不需要代码的情况下创建一个完整的测试套件。首先,为了定义测试集,我们在系统提示中为代理创建了一个配置对象,其中定义了要运行的测试列表。

这也使我们能够在运行一套测试时将系统提示作为变量传入。像 Langfuse 这样的系统的主要好处之一是,它能够在其 UI 中实现提示管理作为代码。为此,可能会注入系统中的后续系统提示也会与配置中的系统提示关联,从而使我们能够在测试过程中强制底层模型进入特定状态,同时增强系统对主提示和后续提示变化的抵抗力。

通过将要运行的测试列表作为系统提示中的配置来管理,我们只需要每个代理一次代码更改。要运行的测试列表可以在 Langfuse UI 中进行更改和扩展。

每个测试提示都与其评估者作为配置的一部分相关联。每个测试提示至少有一个自定义评估运行,并且这些提示大致遵循以下模板:一个有用的 AI 评估者,将提供反馈和评分。

You are a helpful AI evaluator who will provide feedback and scoring on the task below.[Describe the scenario and how the agent has been instructed to behave in said scenario]Based on the transcript output, you will determine whether this task was successfully completed.  You will return a JSON object in the following form:-------------Example outputs:{"score": -1, "comment": [Description of an example negative case}{“score”: 1, “comment”: [Description of an example positive case]}------------In this object, score is a number between -1 and 1, with 1 indicating complete success and a -1 indicating complete failure.  The comment is a string indicating your reasoning for the score.-------------BEGIN TRANSCRIPT:{{transcript}}END TRANSCRIPT--------------Do not return any output except the JSON object referenced above.

使用这个系统

我们认为这个测试与评估框架是一个合理的折衷方案,用于创建一个低成本、易于操作的系统。我们将其主要应用视为持续集成/持续交付(CI/CD)管道的一部分,确保,或者作为快速评分卡的来源,供那些希望调整系统提示并需要比手动测试更多反馈的人使用。

基于支撑代理和评估器的模型,令牌的使用可能意味着一个完整的测试套件运行,像我们这样的测试套件通常包含数十个测试提示和评估器,可能需要花费数十美元。

控制运行此类系统成本的一种方式是,在迭代提示和工具时,尤其是在进行大量更改以迭代提升性能时,首先从一个较小的模型开始,衡量相对性能,只有在找到鼓舞人心的结果时,才逐步增加测试规模,使用更大的模型。

Langfuse 印象

总的来说,我们对使用 Langfuse 的决定感到满意。只需相对较少的工作,我们就能部署出符合需求的系统。该系统足够灵活,让我们能够快速定制,以适应我们的用例。

我们已经注意到一些不足之处,希望未来的开发能够解决这些问题:

Langfuse 的用户体验缺乏一些打磨,若进行改进,将大大提升用户的使用体验。举例来说,目前无法复制提示,并且无法通过除名称以外的其他参数来搜索可用的提示。

自托管选项不允许你通过 UI 触发新的测试运行,这意味着操作系统的人需要通过命令行或为此目的开发的其他 UI 来执行此操作。

我们理解这个环境在快速发展,但我们相信这个粗略的框架具有相当好的可移植性,若我们最终决定将其实现到另一个系统中,应该是可行的。

未来创新潜力

AI 生成的测试提示变体

增加测试覆盖面的一种方式是创建我们现有测试提示的变体。像 TestGen-LLM 这样的工具正在该领域崭露头角,但总体而言,利用生成式 AI 测试生成式 AI 仍是一个相对年轻的领域。由于这些负载本质上是 JSON 对象,因此确实可以指示 LLM 创建变体。那么问题是,如何控制这些变体的质量,以确保它们仍然是有效的测试。

使用数据集

Langfuse 数据集是一个有趣的工具功能,允许用户将特定的追踪部分链接为模型的输入和预期输出。虽然我们本可以在单元测试中使用类似的工具,但我们发现创建聊天提示作为输入,并一般描述我们在评估提示中寻找的内容,比制作“预期输出”用于数据集评估要更简单。我们认为数据集是用于代码中可评估的测试(例如,聊天机器人在被问到时是否返回了正确的年份?聊天机器人是否返回了有效的 JSON?)的明确方向。我们可能会在未来用于更通用的测试,但我们发现通过分别创建提示来生成新测试会更快。

感谢阅读!我是 Jack Moore Auril.ai的创始人兼 CEO。这篇文章最初发布在我们的技术博客上,我们将在这里探讨与将生成式 AI 从概念性的兴趣带到实际价值应用相关的主题。

所有观点仅代表我们个人。我们与 Langfuse 没有任何关联或合作关系。

除非另有说明,所有图片均由作者提供。

AI 代理工作流:关于是否使用 LangGraph 或 LangChain 构建的完整指南

原文:towardsdatascience.com/ai-agent-workflows-a-complete-guide-on-whether-to-build-with-langgraph-or-langchain-117025509fa0?source=collection_archive---------0-----------------------#2024-10-25

深入探讨由同一创作者开发的两个库 — LangChain 和 LangGraph:它们的关键构建块、如何处理核心功能部分,以及如何根据你的使用场景在它们之间做出选择

Sandi BesenTowards Data Science Sandi Besen

·发表于Towards Data Science ·阅读时长:11 分钟·2024 年 10 月 25 日

--

语言模型为用户与 AI 系统的互动方式以及这些系统之间的通信方式(通过自然语言)开辟了可能性。

当企业希望利用代理型 AI 功能构建解决方案时,最常见的技术问题之一是“我应该使用哪些工具?”对于那些急于入手的人来说,这是第一个障碍。

来源:Dalle-3

在本文中,我们将探索构建代理型 AI 应用程序的两个最流行框架 — LangChain 和 LangGraph。 在文章结束时,你应该能够全面理解它们的关键构建块,以及每个框架在处理核心功能时的差异,并能够形成一个有根据的观点,选择最适合你问题的框架。

由于广泛将生成性 AI 整合到解决方案中的实践相对较新,开源社区的参与者正积极竞争,开发“最佳”的代理框架和编排工具。这意味着尽管每个参与者都带来了独特的方法,但他们几乎在不断推出新的功能。在阅读本文时,请记住,今天的事实可能明天就不再适用!

注意:我原本打算对 AutoGen、LangChain 和 LangGraph 进行对比。然而,AutoGen 已经 宣布 它将发布 AutoGen 0.4,这是从基础上完全重新设计的框架。当 AutoGen 0.4 发布时,请关注另一篇文章!

LangChain 和 LangGraph 的基础组件

通过理解每个框架的不同基础元素,你将更深入地理解它们在下一节中处理某些核心功能时的关键差异。以下描述并不是每个框架所有组件的详尽列表,而是理解它们总体方法差异的坚实基础。

LangChain

使用 LangChain 有两种方法:作为一系列预定义命令的顺序链条,或者使用 LangChain 代理。每种方法在处理工具和协调方面有所不同。链条遵循预定义的线性工作流,而代理则充当协调者,可以做出更动态的(非线性)决策。

  • 链接:一系列步骤,可以包括调用 LLM、代理、工具、外部数据源、过程代码等。链条可以分支,意味着一个链条可以根据逻辑条件分裂成多个路径。

  • 代理或语言模型:语言模型具有生成自然语言响应的能力。但代理使用语言模型加上额外的能力来推理、调用工具,并在工具调用失败的情况下重复调用过程。

  • 工具:可以在链条中调用或由代理调用与外部系统交互的基于代码的功能。

  • 提示:这可以包括指导模型如何完成任务以及可用工具的系统提示、从外部数据源注入的信息(为模型提供更多上下文),以及用户为模型完成的任务或提示。

LangGraph

LangGraph 从不同的角度处理 AI 工作流。正如其名称所示,它像图形一样协调工作流。由于它在处理 AI 代理、过程代码和其他工具之间不同流程时的灵活性,它更适合用在那些线性链方法、分支链条或简单代理系统无法满足需求的用例。与 LangChain 相比,LangGraph 设计用于处理更复杂的条件逻辑和反馈循环。

  • 图形:一种灵活的工作流组织方式,可以包括调用 LLM、工具、外部数据源、过程代码等。LangGraph 也支持循环图;这意味着你可以创建循环和反馈机制,使节点能够多次回访。

  • 节点:表示工作流中的步骤,如 LLM 查询、API 调用或工具执行。

  • 边和条件边:边通过将一个节点的输出连接到下一个节点的输入来定义信息流。条件边则定义了在满足特定条件时,信息如何从一个节点流向另一个节点。开发者可以自定义这些条件。

  • 状态:状态是应用程序在信息通过图形流动时的当前状态。它是一个由开发者定义的可变 TypedDict 对象,包含当前执行图形所需的所有相关信息。LangGraph 会自动处理每个节点状态的更新,随着信息在图中流动而更新状态。

  • 代理或语言模型:图中的语言模型仅负责根据输入生成文本响应。代理能力利用语言模型,但使得图能够有多个节点代表代理的不同组件(例如推理、工具选择和工具执行)。代理可以决定在图中采取哪条路径,更新图的状态,并执行比仅生成文本更多的任务。

每个框架如何处理核心功能的差异

LangGraph 和 LangChain 在一些功能上有所重叠,但它们从不同的角度来解决问题。LangChain 专注于通过使用链条来实现线性工作流或不同的 AI 代理模式。而 LangGraph 则专注于创建更灵活、更细粒度的基于过程的工作流,可以包括 AI 代理、工具调用、过程代码等。

通常来说,LangChain 的学习曲线比 LangGraph 更平缓。LangChain 具有更多的抽象和预定义配置,使其在简单使用场景下更易于实现。而 LangGraph 则允许对工作流的设计进行更多的自定义控制,这意味着它的抽象程度较低,开发者需要学习更多才能有效使用该框架。

工具调用

LangChain

在 LangChain 中,工具可以通过两种方式被调用,具体取决于你是否使用链来按步骤顺序执行一系列任务,或者仅仅使用其代理功能,而没有明确在链中定义。在链中,工具作为链中的预定义步骤被包括进去——这意味着它们不一定由代理调用,因为在链中已经预定了它们会被调用。然而,当代理没有在链中定义时,代理拥有自主决定调用哪些工具及其调用时机的权利,这取决于代理可以访问的工具列表。

链的工作流示例:

图片由 Sandi Besen 提供

  1. 创建代表工具的函数,并使其与链兼容

  2. 将工具集成到链中

  3. 执行链

代理的工作流示例:

图片由 Sandi Besen 提供

  1. 工具已定义

  2. 工具已添加到代理

  3. 代理接收查询并决定是否以及何时使用搜索工具。如果需要,代理可能会多次使用该工具。

LangGraph

LangGraph中,工具通常作为图中的一个节点进行表示。如果图中包含代理,则由代理基于其推理能力决定调用哪个工具。根据代理的工具决策,图会导航到“工具节点”来处理工具的执行。可以在代理到工具节点的边缘中包含条件逻辑,添加额外的逻辑来决定是否执行某个工具。如果图中没有代理,则类似于 LangChain 中的链,可以根据条件逻辑将工具包含在工作流中。

包含代理的图示例流程:

图片由 Sandi Besen 提供

  1. 工具已定义

  2. 工具绑定到代理

  3. 代理决定是否需要工具,如果需要,则决定使用哪种工具。

  4. LangGraph 框架检测到需要调用工具,并导航到图中的工具节点以执行工具调用。

  5. 工具的输出被捕获并添加到图的状态中

  6. 代理会再次被调用并更新状态,以便做出下一步的决策

没有代理的图示例流程:

图片由 Sandi Besen 提供

  1. 工具已定义

  2. 工具作为一个节点被添加到图中

  3. 可以使用条件边缘来确定何时使用特定的工具节点,并控制图的流程

  4. 工具可以配置为更新图的状态

如果你想了解更多关于工具调用的内容,我的朋友 Tula Masterman 写了一篇精彩的 文章 ,讲解了工具调用在生成性 AI 中的工作原理。

注意:LangChain 和 LangGraph 都不支持像微软的 Semantic Kernel 那样开箱即用的语义函数

对话历史和记忆

LangChain

LangChain 提供了内建的抽象层,用于处理对话历史和记忆。你可以选择想要传递给 LLM 的粒度级别(从而决定传递多少令牌),这些选项包括完整的会话历史、总结版或自定义定义的记忆。开发者还可以创建自定义的长期记忆系统,将记忆存储在外部数据库中,以便在需要时进行检索。

LangGraph

在 LangGraph 中,状态通过跟踪每个时刻定义的变量来处理内存。状态可以包括诸如对话历史、计划步骤、语言模型前一个响应的输出等内容。它可以从一个节点传递到下一个节点,以便每个节点都可以访问系统当前的状态。然而,跨会话的长期持久性内存并不是该框架的直接特性。为了实现这一点,开发者可以包含负责存储记忆和其他变量的节点,并将其保存在外部数据库中,稍后可以检索。

开箱即用的 RAG 功能:

LangChain

LangChain 可以处理复杂的检索和生成工作流,并拥有更成熟的工具集,帮助开发者将 RAG 集成到应用中。例如,LangChain 提供了开箱即用的文档加载、文本解析、嵌入创建、向量存储和检索功能,可以直接使用 langchain.document_loaders、langchain.embeddings 和 langchain.vectorstores。

LangGraph

在 LangGraph 中,RAG 需要从零开始开发,作为图结构的一部分。例如,可以为文档解析、嵌入和检索分别设置不同的节点,并通过普通或条件边连接这些节点。每个节点的状态将用于在 RAG 流水线的各个步骤之间传递信息。

并行性:

LangChain

LangChain 通过使用 RunnableParallel 类提供了运行多个链或代理的机会。对于更高级的并行处理和异步工具调用,开发者需要通过使用像 asyncio 这样的 Python 库来定制实现这些功能。

LangGraph

LangGraph 支持节点的并行执行,只要没有任何依赖关系(例如一个语言模型的响应输出作为下一个节点的输入)。这意味着,只要节点之间没有依赖关系,它就可以支持多个代理在图中同时运行。与 LangChain 类似,LangGraph 也可以使用 RunnableParallel 类来并行运行多个图。LangGraph 还支持通过使用像 asyncio 这样的 Python 库进行并行工具调用。

重试逻辑与错误处理:

LangChain

在 LangChain 中,错误处理由开发者明确定义,可以通过在链本身中引入重试逻辑来完成,或者在工具调用失败时由代理来处理。

LangGraph

在 LangGraph 中,你实际上可以通过将错误处理作为独立节点来嵌入到工作流中。当某些任务失败时,可以指向另一个节点或让同一个节点重试。最棒的是,只有失败的特定节点会被重试,而不是整个工作流。这意味着图可以从失败点恢复,而不需要从头开始。如果你的使用案例涉及许多步骤和工具调用,这一点可能很重要。

总结

你可以仅使用 LangChain 而不使用 LangGraph,或仅使用 LangGraph 而不使用 LangChain,或者两者一起使用!同样,也完全可以探索将 LangGraph 的图形化编排与其他 Agentic AI 框架(如微软的 AutoGen)结合使用,通过将 AutoGen 代理作为图中的独立节点来实现。可以说有很多选择——这可能会让人感到不知所措。

那么,在进行完这些研究后,我应该在什么时候使用每个框架呢?虽然没有硬性规定,以下是我的个人选择:

何时仅使用 LangChain:

你需要快速原型设计或开发 AI 工作流,这些工作流涉及顺序任务(例如文档检索、文本生成或摘要),遵循预定义的线性模式。或者,你希望利用 AI 代理模式来动态做出决策,但不需要对复杂的工作流进行精细控制。

何时仅使用 LangGraph:

你的使用场景需要非线性工作流,其中多个组件动态交互,例如依赖于条件的工作流、需要复杂分支逻辑、错误处理或并行处理的工作流。你愿意为那些像在 LangChain 中那样没有抽象出来的组件构建自定义实现。

何时使用 LangChain 和 LangGraph 一起:

你喜欢 LangChain 中预构建的提取组件,比如开箱即用的 RAG 能力、内存功能等,但同时也希望使用 LangGraph 的非线性编排来管理复杂的任务流。将两个框架结合使用可以充分发挥它们各自的优势。

最终,无论你选择 LangChain、LangGraph,还是两者的组合,都取决于你项目的具体需求。

注:本文和论文中表达的观点仅代表作者个人意见,并不一定反映其各自雇主的观点或政策。

还有问题或认为有些地方需要进一步澄清吗?在LinkedIn上给我发个私信!我总是渴望参与思想交流,并不断完善我的工作。

参考文献

## 探索新型 LLM 代理和架构

ReAct 代理的失败为新一代代理和可能性铺平了道路

towardsdatascience.com [## 主页

将语言代理构建为图形

LangGraph 和研究代理 | Pinecone

LangGraph 是一个基于图形的代理框架,使我们能够构建更灵活、更可控的 AI 代理。这里…

链式未来:深入探讨 LangChain

LLM 链是由一系列互联组件组成,旨在以特定顺序执行任务,以最大化…

从基础到高级:探索 LangGraph

构建单一和多代理工作流,带有“人类在回路”互动

面向数据科学的探索:LangGraph 从基础到高级

AI Agents——从概念到 Python 中的实际实现

原文:towardsdatascience.com/ai-agents-from-concepts-to-practical-implementation-in-python-fb26789b1560?source=collection_archive---------0-----------------------#2024-08-12

这将改变你对 AI 及其能力的看法

Zoumana KeitaTowards Data Science Zoumana Keita

·发表于Towards Data Science ·阅读时长 10 分钟·2024 年 8 月 12 日

--

动机

正如一则非洲谚语所说:

单打独斗,我们走得更快;齐心协力,我们走得更远。

这也与这样一个观点相关:没有人可以在每个领域都是专家。团队合作和有效的任务分配对于实现伟大的目标至关重要。

相似的原则适用于大语言模型(LLMs)。我们可以将多个 LLM 或AI Agents组合起来,每个 LLM 或AI Agent专注于特定领域,而不是让单个 LLM 处理复杂任务。

这一策略可以打造出更强大的系统,提供更高质量的结果。

在本文中,你将学到:

  • 什么是AI Agents

  • 为什么值得考虑它们来解决现实世界的用例

  • 如何从零开始创建一个完整的AI Agents系统

系统的一般工作流程

在开始任何编码之前,我们先清楚地了解本文中构建的系统的主要组成部分。

自主 AI 代理的工作流程(图:作者)

  • 该工作流程总体…

网络行业中的 AI 代理

原文:towardsdatascience.com/ai-agents-in-networking-industry-f73a42fa22e1?source=collection_archive---------9-----------------------#2024-11-27

用于部署、配置和监控网络的 AI 代理

Mandeep KularTowards Data Science Mandeep Kular

·发表于 Towards Data Science ·阅读时间:11 分钟·2024 年 11 月 27 日

--

AI 代理在 2024 年成为热议话题,且实至名归。与传统的 AI 模型或与大型语言模型(LLMs)的交互不同,后者仅根据静态训练数据提供回应,AI 代理是动态的实体,能够感知、推理(得益于提示技术)并在其操作领域内自主行动。它们的适应性和优化过程的能力使它们在需要复杂决策和实时响应的领域中不可或缺,如网络部署、测试、监控和调试。在未来的日子里,我们将看到 AI 代理在各行各业的广泛应用,尤其是在网络行业。

代理工作流(图片由作者提供)

在这里,我展示了通过 AI 代理进行网络部署、配置和监控的过程。整体代理工作流由 4 个代理组成。第一个代理负责从 learn.srlinux.dev/get-started/lab/ 网站获取安装步骤。第二个代理执行这些步骤。第三个代理根据网络拓扑提供相关的节点配置,最后一个代理执行配置并验证端到端的连接性。

有关代码手册的详细信息,请查看我的 GitHub 链接: AI-Agents-For-Networking.

使用 CrewAI 的 MAS(多代理系统)部署网络

对于这个使用案例,整个拓扑部署在一个预先构建的 Debian 12 UTM 虚拟机上(作为沙盒环境)。选择它的原因是,它已经预装了所有相关的软件包,如 containerlab 和 docker 包。Containerlab 帮助轻松启动各种基于容器的网络拓扑。选择了以下拓扑,包含 Linux 容器和诺基亚的 SR Linux 容器:

Client1 — — Leaf1 — — Spine1 — — Leaf2 — — Client2

其中 Client1 和 Client2 是 Linux 容器,Leaf1 和 Leaf2 是 IXR-D2L 类型,Spine1 是 IXR-D5 类型的 SR Linux 容器。

以下是每个代理的简要工作流:

  1. 文档专家代理

它访问给定的网址,提取安装步骤、网络拓扑部署步骤,并查找节点连接指令。以下是创建代理、任务及其自定义工具的示例代码:

# Custom tool to extract content from a given webpage
class QuickstartExtractor(BaseTool):
    name: str = "WebPage Content extractor"
    description: str = "Get all the content from a webpage"

    def _run(self) -> str:    
            url = "https://learn.srlinux.dev/get-started/lab/"
            response = requests.get(url)
            response.raise_for_status()
            return response.text

# Create doc specialist Agent
doc_specialist = Agent(
   role="Documentation Specialist",
   goal="Extract and organize containerlab quickstart steps",
   backstory="""Expert in technical documentation with focus on clear, 
   actionable installation and setup instructions.""",
   verbose=True,
   tools = [QuickstartExtractor()],
   allow_delegation=False
        )

# Task for doc speaclist Agent
doc_task = Task(
    description=(
                    "From the containerlab quickstart guide:\n"
     "1\. Extract installation steps \n"
     "2\. Identify topology deployment steps \n"
     "3\. Find node connection instructions \n"

    "Present in a clear, sequential format.\n"
    ),
                expected_output="List of commands",
    agent=doc_specialist
                )

运行后,以下是该代理的示例输出(最终答案):

# Agent: Documentation Specialist
## Final Answer: 
```bash

# 安装步骤

curl -sL https://containerlab.dev/setup | sudo -E bash -s "all"

# 拓扑部署步骤

sudo containerlab deploy -c -t https://github.com/srl-labs/srlinux-getting-started

# 节点连接指令

ssh leaf1

```py

如图所示,它已从网址中清晰地提取了所需的步骤。然后这些步骤被传递给下一个代理。

注意:为所有代理添加代码会使本文变得冗长。请查看我的 GitHub 仓库获取更多细节: AI-Agents-For-Networking.

2. Linux 配置代理

执行从文档专家代理获取的命令。它获得了 UTM 虚拟机的登录详情,并指示在该虚拟机上执行命令。完成后,它会检查所有的 docker 容器是否都在运行。它还在执行任何 containerlab/docker 命令之前检查虚拟机的健康状态。

该代理的输出(最终答案)清晰地显示了虚拟机健康检查、containerlab 和 docker 命令的成功执行:

# Agent: Linux Command Executor and Docker Orchestrator
## Final Answer: 
1\. **System Uptime Check**: 
   - Output: `14:56:17 up 1 day, 20:25, 60 users,  load average: 0.01, 0.39, 0.86`

2\. **Disk Usage Check**:
   - Output:

文件系统 大小 已用 可用 用% 挂载点

udev 5.9G 0 5.9G 0% /dev

tmpfs 1.2G 1.3M 1.2G 1% /run

/dev/mapper/debian--vg-root 61G 5.7G 52G 10% /

tmpfs 5.9G 0 5.9G 0% /dev/shm

tmpfs 5.0M 0 5.0M 0% /run/lock

/dev/sda2 456M 92M 340M 22% /boot

/dev/sda1 511M 5.9M 506M 2% /boot/efi

tmpfs 1.2G 0 1.2G 0% /run/user/1000


3\. **Running Processes Check**: 
   - Output: (truncated for brevity)

USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND

root 1 0.0 0.1 168128 12792 ? Ss Nov25 1:28 /sbin/init

...

debian 106849 0.0 0.0 8244 4992 pts/0 Ss+ Nov25 0:09 -bash


4\. **Network Connectivity Check**: 
   - Output:

PING 8.8.8.8 (8.8.8.8) 56(84) 字节的数据。

64 字节来自 8.8.8.8:icmp_seq=1 ttl=116 时间=15.4 毫秒

...

--- 8.8.8.8 ping 统计 ---

4 个数据包传输,4 个接收,0% 丢包,时间 3012 毫秒

rtt 最小/平均/最大/均方根 = 15.316/16.778/20.466/2.142 ms


5\. **Containerlab Installation Check**:
   - Containerlab is already installed and is at the latest version (0.59.0).

6\. **Topology Deployment Output**:
   - Output of the deployed topology:

+---+---------+--------------+------------------------------------+---------------+---------+----------------+----------------------+

# 名称 容器 ID 镜像 类型 状态 IPv4 地址 IPv6 地址

+---+---------+--------------+------------------------------------+---------------+---------+----------------+----------------------+

1 client1 ab894528a0e9 ghcr.io/srl-labs/network-multitool linux 运行中 172.20.20.4/24 3fff:172:20:20::4/64
2 client2 6f9f0822be5d ghcr.io/srl-labs/network-multitool linux 运行中 172.20.20.2/24 3fff:172:20:20::2/64
3 leaf1 e7a1a3874600 ghcr.io/nokia/srlinux:24.7.2 nokia_srlinux 运行中 172.20.20.3/24 3fff:172:20:20::3/64
4 leaf2 ed4399b8f7d6 ghcr.io/nokia/srlinux:24.7.2 nokia_srlinux 运行中 172.20.20.5/24 3fff:172:20:20::5/64
5 spine1 ada601805590 ghcr.io/nokia/srlinux:24.7.2 nokia_srlinux 运行中 172.20.20.6/24 3fff:172:20:20::6/64

+---+---------+--------------+------------------------------------+---------------+---------+----------------+----------------------+


7\. **List of Running Docker Containers**:
   - Output:

容器 ID 镜像 命令 创建时间 状态 端口 名称

ada601805590 ghcr.io/nokia/srlinux:24.7.2 "/tini -- fixuid -q …" 4 分钟前 运行中 4 分钟 spine1

6f9f0822be5d ghcr.io/srl-labs/network-multitool "/bin/sh /docker/ent…" 4 分钟前 运行中 4 分钟 22/tcp, 80/tcp, 443/tcp, 1180/tcp, 11443/tcp client2

e7a1a3874600 ghcr.io/nokia/srl-labs/network-multitool "/bin/sh /docker/ent…" 4 分钟前 运行中 4 分钟 leaf1

ab894528a0e9 ghcr.io/nokia/srl-labs/network-multitool "/bin/sh /docker/ent…" 4 分钟前 运行中 4 分钟 client1

ed4399b8f7d6 ghcr.io/nokia/srlinux:24.7.2 "/tini -- fixuid -q …" 4 分钟前 运行中 4 分钟 leaf2


All tasks completed successfully with the appropriate outputs!

3. 网络配置专家代理

这个代理的任务是理解拓扑结构,即节点数量及其连接性。然后,它需要提出 IP 分配方案,并生成 Linux 容器和 SR Linux 容器的相关接口和路由配置,同时遵守 IP 分配方案。

对于 SR Linux 配置,提示中给出了一个示例配置。这个示例配置也可以通过代理从 PDF 格式的配置指南中获取(前提是有合适的工具)。

在下面这个代理的输出中,可以清楚地看到,它已经成功根据拓扑结构、相关配置和路由来分配了 IP 地址:

# Agent: Senior Network Administrator
## Final Answer: 
# Configuration for client1 (Linux)
```bash

/sbin/ip address add 10.1.1.2/24 dev eth1

/sbin/ip route add 10.1.4.2/32 via 10.1.1.1

```py

# Configuration for leaf1 (SRLinux)
```bash

进入候选私人模式

# 配置数据接口

设置 / interface ethernet-1/1 管理状态启用

设置 / interface ethernet-1/1 subinterface 0 管理状态启用

设置 / interface ethernet-1/1 subinterface 0 ipv4 地址 10.1.1.1/24

设置 / interface ethernet-1/1 subinterface 0 ipv4 管理状态启用

设置 / interface ethernet-1/49 管理状态启用

设置 / interface ethernet-1/49 subinterface 0 管理状态启用

设置 / interface ethernet-1/49 subinterface 0 ipv4 地址 10.1.2.1/24

设置 / interface ethernet-1/49 subinterface 0 ipv4 管理状态启用

# 将接口添加到默认命名空间

设置 / network-instance default interface ethernet-1/1.0

设置 / network-instance default interface ethernet-1/49.0

# 配置 BGP

设置 / network-instance default protocols bgp 管理状态启用

设置 / network-instance default protocols bgp 路由器 ID 1.1.1.1

设置 / network-instance default protocols bgp 自治系统 65001

设置 / network-instance default protocols bgp afi-safi ipv4-unicast 管理状态启用

设置 / network-instance default protocols bgp group ebgp 管理状态启用

设置 / network-instance default protocols bgp group ebgp 导出策略 [ export-all ]

设置 / network-instance default protocols bgp group ebgp 导入策略 [ export-all ]

设置 / network-instance default protocols bgp neighbor 10.1.2.2 管理状态启用

设置 / network-instance default protocols bgp neighbor 10.1.2.2 对等组 ebgp

设置 / network-instance default protocols bgp neighbor 10.1.2.2 对等 AS 65002

# 创建策略

设置 / 路由策略 策略 export-all 默认操作 策略结果 接受

提交现在

```py

# Configuration for spine1 (SRLinux)
```bash

进入候选私人模式

# 配置数据接口

设置 / interface ethernet-1/1 管理状态启用

设置 / interface ethernet-1/1 subinterface 0 管理状态启用

设置 / interface ethernet-1/1 subinterface 0 ipv4 地址 10.1.2.2/24

设置 / interface ethernet-1/1 subinterface 0 ipv4 管理状态启用

设置 / interface ethernet-1/2 管理状态启用

设置 / interface ethernet-1/2 subinterface 0 管理状态启用

设置 / interface ethernet-1/2 subinterface 0 ipv4 地址 10.1.3.1/24

设置 / interface ethernet-1/2 subinterface 0 ipv4 管理状态启用

# 将接口添加到默认命名空间

设置 / network-instance default interface ethernet-1/1.0

设置 / network-instance default interface ethernet-1/2.0

# 配置 BGP

设置 / network-instance default protocols bgp 管理状态启用

设置 / network-instance default protocols bgp 路由器 ID 2.2.2.2

设置 / network-instance default protocols bgp 自治系统 65002

设置 / network-instance default protocols bgp afi-safi ipv4-unicast 管理状态启用

设置 / network-instance default protocols bgp group ebgp 管理状态启用

设置 / network-instance default protocols bgp group ebgp 导出策略 [ export-all ]

设置 / network-instance default protocols bgp group ebgp 导入策略 [ export-all ]

set / network-instance default protocols bgp neighbor 10.1.2.1 admin-state enable

set / network-instance default protocols bgp neighbor 10.1.2.1 peer-group ebgp

set / network-instance default protocols bgp neighbor 10.1.2.1 peer-as 65001

set / network-instance default protocols bgp neighbor 10.1.3.2 admin-state enable

set / network-instance default protocols bgp neighbor 10.1.3.2 peer-group ebgp

set / network-instance default protocols bgp neighbor 10.1.3.2 peer-as 65003

# 创建策略

set / routing-policy policy export-all default-action policy-result accept

立即提交

```py

# Configuration for leaf2 (SRLinux)
```bash

进入候选私有配置

# 配置数据接口

set / interface ethernet-1/1 admin-state enable

set / interface ethernet-1/1 subinterface 0 admin-state enable

set / interface ethernet-1/1 subinterface 0 ipv4 address 10.1.4.1/24

set / interface ethernet-1/1 subinterface 0 ipv4 admin-state enable

set / interface ethernet-1/49 admin-state enable

set / interface ethernet-1/49 subinterface 0 admin-state enable

set / interface ethernet-1/49 subinterface 0 ipv4 address 10.1.3.2/24

set / interface ethernet-1/49 subinterface 0 ipv4 admin-state enable

# 将接口添加到默认命名空间

set / network-instance default interface ethernet-1/1.0

set / network-instance default interface ethernet-1/49.0

# 配置 BGP

set / network-instance default protocols bgp admin-state enable

set / network-instance default protocols bgp router-id 3.3.3.3

set / network-instance default protocols bgp autonomous-system 65003

set / network-instance default protocols bgp afi-safi ipv4-unicast admin-state enable

set / network-instance default protocols bgp group ebgp admin-state enable

set / network-instance default protocols bgp group ebgp export-policy [ export-all ]

set / network-instance default protocols bgp group ebgp import-policy [ export-all ]

set / network-instance default protocols bgp neighbor 10.1.3.1 admin-state enable

set / network-instance default protocols bgp neighbor 10.1.3.1 peer-group ebgp

set / network-instance default protocols bgp neighbor 10.1.3.1 peer-as 65002

# 创建策略

set / routing-policy policy export-all default-action policy-result accept

立即提交

```py

# Configuration for client2 (Linux)
```bash

/sbin/ip address add 10.1.4.2/24 dev eth1

/sbin/ip route add 10.1.1.2/32 via 10.1.4.1

```py

This configuration:
1\. Uses unique subnets for each connection:
   - client1-leaf1: 10.1.1.0/24
   - leaf1-spine1: 10.1.2.0/24
   - spine1-leaf2: 10.1.3.0/24
   - leaf2-client2: 10.1.4.0/24

2\. Configures BGP with unique AS numbers:
   - leaf1: AS65001
   - spine1: AS65002
   - leaf2: AS65003

3\. Implements specific routes on clients for end-to-end connectivity
4\. Follows the exact SRLinux configuration format provided
5\. Uses proper routing policies for BGP route exchange

4. 高级网络管理员代理

该代理获取生成的配置并将其应用到所有节点,检查 BGP 对等连接是否建立,然后最终执行端到端的 ping 测试以验证连接性。此时,代理仅通过推测配置命令就能得到正确的显示命令。

以下是此代理在成功配置网络拓扑并检查端到端连接后获得的最终答案:

# Agent: Network Configuration Specialist
## Final Answer: 
# Network Configuration Report

## Topology Information
### Devices and Interfaces
1\. **client1**
   - Interface: eth1
   - IP: 10.1.1.2/24
   - Static route: 10.1.4.2/32 via 10.1.1.1

2\. **leaf1** (AS65001)
   - ethernet-1/1: 10.1.1.1/24 (connected to client1)
   - ethernet-1/49: 10.1.2.1/24 (connected to spine1)
   - BGP Router ID: 1.1.1.1

3\. **spine1** (AS65002)
   - ethernet-1/1: 10.1.2.2/24 (connected to leaf1)
   - ethernet-1/2: 10.1.3.1/24 (connected to leaf2)
   - BGP Router ID: 2.2.2.2

4\. **leaf2** (AS65003)
   - ethernet-1/1: 10.1.4.1/24 (connected to client2)
   - ethernet-1/49: 10.1.3.2/24 (connected to spine1)
   - BGP Router ID: 3.3.3.3

5\. **client2**
   - Interface: eth1
   - IP: 10.1.4.2/24
   - Static route: 10.1.1.2/32 via 10.1.4.1

## Configuration Commands Executed
All configurations were successfully applied to their respective devices as per the provided configuration scripts. The key configurations included:
1\. Interface IP addressing
2\. BGP neighbor relationships
3\. Route policies
4\. Static routes on client machines

## Connectivity Test Results

PING 10.1.4.2 (10.1.4.2) 56(84) 字节数据。

从 10.1.4.2 收到 64 字节:icmp_seq=1 ttl=61 time=128 ms

从 10.1.4.2 收到 64 字节:icmp_seq=2 ttl=61 time=8.16 ms

从 10.1.4.2 收到 64 字节:icmp_seq=3 ttl=61 time=4.49 ms

64 字节来自 10.1.4.2:icmp_seq=4 ttl=61 time=5.86 ms

--- 10.1.4.2 ping 统计 ---

传输 4 个数据包,接收 4 个,丢包率 0%,时间 3005ms

rtt min/avg/max/mdev = 4.491/36.620/127.973/52.758 ms


The ping test from client1 (10.1.1.2) to client2 (10.1.4.2) was successful with:
- 100% success rate (4/4 packets)
- Average round-trip time: 36.620ms
- No packet loss

This confirms that the BGP configurations are working correctly and the network is properly forwarding traffic between the clients through the leaf-spine topology.

结论

AI 代理超越了自动化的范畴。它们可以推理并尝试提出替代方案。一个简单的例子是,如果一个 Linux 命令因权限问题失败,而代理有 sudo 密码的访问权限,那么下次它将尝试使用 sudo 密码以便成功。一个复杂的例子是,如果 BGP 对等连接没有建立,根据提示指令,它们可以尝试找出原因,甚至进行修复。

代理工作流有其挑战,需要与程序化方法不同的思维方式。到目前为止,我遇到的缺点包括,可能需要更长时间(有时要长得多)才能实现结果,运行时每次都不同,输出也会有所变化(这可以通过更好的提示词进行某种程度的控制)。

最后,对于一些琐碎、直接的任务,比如抓取网站并执行给定的命令,可以使用较小的 LLM,如 gpt-4o-mini 或 llama3.1–7b 等。然而,对于设计网络拓扑这样的任务,则需要更大的 LLM。虽然这里选择了 claude-3–5-sonnet,但理想情况下,一个好的 70b 模型应该足够。最后,你必须非常谨慎地使用提示词。它们可能决定你的用例成败!

AI 代理:生成性 AI 中工具调用与推理的交汇点

原文:towardsdatascience.com/ai-agents-the-intersection-of-tool-calling-and-reasoning-in-generative-ai-ff268eece443?source=collection_archive---------2-----------------------#2024-10-05

拆解 AI 中的解决问题与工具驱动的决策过程

Tula MastermanTowards Data Science Tula Masterman

·发表于 Towards Data Science ·阅读时间:11 分钟·2024 年 10 月 5 日

--

作者与 GPT-4o 创作的图像,描绘了一个位于推理与工具调用交汇点的 AI 代理

引言:代理性 AI 的崛起

如今,新的库和低代码平台使得构建 AI 代理(也称为数字工作者)比以往任何时候都更容易。工具调用是推动生成性 AI 模型“代理性”特征的主要能力之一,它扩展了 AI 模型的功能,使其超越了仅限于对话任务。通过执行工具(功能),代理可以代表你采取行动,解决那些需要强大决策能力和与多种外部数据源交互的复杂多步骤问题。

本文聚焦于通过工具调用表达推理,探讨了工具使用中的一些挑战,涵盖了评估工具调用能力的常见方法,并提供了不同模型和代理与工具交互的示例。

解决问题的推理表达方式

成功的代理背后有两种关键的推理表达方式:通过评估和规划进行推理通过工具使用进行推理

  • 通过评估和规划进行推理与智能体通过迭代规划、评估进展并调整方法直到任务完成的能力有关。像思维链(CoT)、ReAct提示分解等技术,都是为提高模型通过分解任务来战略性推理的能力而设计的。这种类型的推理更具宏观性,确保通过迭代工作并考虑每个阶段的结果来正确完成任务。

  • 通过工具使用进行推理与智能体有效地与环境互动的能力有关,决定调用哪些工具以及如何构建每个调用。这些工具使得智能体能够检索数据、执行代码、调用 API 等等。这种推理的优势在于正确执行工具调用,而非反思调用结果。

尽管两种推理方式都很重要,但它们不总是需要结合使用以创造强大的解决方案。例如,OpenAI 的新 o1 模型擅长通过评估和规划进行推理,因为它经过训练,能够使用思维链进行推理。这大大提高了它思考并解决复杂挑战的能力,且在各种基准测试中得到了体现。例如,o1 模型在涵盖物理学、生物学和化学的 GPQA 基准测试中,超越了人类博士级准确度,并在Codeforces比赛中得分处于86-93 百分位。虽然 o1 的推理能力可以用来生成基于文本的响应,建议根据工具描述选择工具,但目前它缺乏显式的工具调用能力(至少现在是如此!)。

相反,许多模型专门为通过工具使用进行推理而微调,使它们能够生成函数调用并非常有效地与 API 进行互动。这些模型专注于在正确的时间以正确的格式调用正确的工具,但通常并不设计得像 o1 那样全面评估其结果。伯克利函数调用排行榜 (BFCL)是一个很好的资源,用于比较不同模型在函数调用任务中的表现。它还提供了一个评估套件,用于比较你自己微调的模型在各种具有挑战性的工具调用任务中的表现。事实上,最新数据集 BFCL v3刚刚发布,现在包括了多步骤、多轮函数调用,进一步提高了基于工具的推理任务的标准。

这两种推理方式各自都非常强大,当结合在一起时,它们有可能创建能够有效分解复杂任务并自主与环境互动的代理。有关更多 AI 代理架构的推理、规划和工具调用的例子,请查看我团队在 ArXiv 上的调研论文

工具调用的挑战:应对复杂的代理行为

构建强大且可靠的代理需要克服许多不同的挑战。在解决复杂问题时,代理通常需要同时平衡多个任务,包括规划、在正确的时间与合适的工具互动、正确格式化工具调用、记住前一步的输出、避免重复循环,以及遵循指导原则以保护系统免受越狱/提示注入等攻击。

过多的需求容易让单个代理不堪重负,这导致了一个日益增长的趋势——看似由一个代理提供服务的系统,实际上是由多个代理和提示共同协作、分工合作来完成任务。这种分工使得任务能够被分解,并由不同的模型和代理并行处理,每个代理专门解决某个特定问题。

在这里,具备优秀工具调用能力的模型发挥了作用。虽然工具调用是一种强大的方式,可以使代理更加高效,但它也带来了自己的一系列挑战。代理需要理解可用的工具,从一组可能相似的选项中选择合适的工具,准确格式化输入,按照正确的顺序调用工具,并可能需要整合来自其他代理或人类的反馈或指令。许多模型经过专门微调,用于工具调用,使它们能够专注于在合适的时机高精度地选择功能。

在微调模型以进行工具调用时的一些关键考虑因素包括:

  1. 正确的工具选择:模型需要理解可用工具之间的关系,在适用时进行嵌套调用,并在存在其他相似工具的情况下选择正确的工具。

  2. 处理结构性挑战:尽管大多数模型使用 JSON 格式进行工具调用,但也可以使用 YAML 或 XML 等其他格式。需要考虑模型是否需要跨格式进行泛化,或者是否仅使用一种格式。无论使用哪种格式,模型都需要为每次工具调用包含适当的参数,并可能在后续调用中使用前一次调用的结果。

  3. 确保数据集的多样性和可靠的评估:使用的数据集应该具有多样性,并涵盖多步骤、多轮次的函数调用的复杂性。应进行适当的评估,以防止过拟合并避免基准污染。

用于评估工具调用的常见基准

随着工具使用在语言模型中的重要性不断增长,许多数据集应运而生,以帮助评估和改进模型的工具调用能力。目前最流行的两个基准是伯克利功能调用排行榜和 Nexus 功能调用基准,这两个基准都是Meta 用来评估其 Llama 3.1 系列模型表现的。最近的一篇论文,ToolACE,展示了如何利用智能体创建一个多样化的数据集,以进行模型工具使用的微调和评估。

让我们更详细地探讨这些基准:

  • 伯克利功能调用排行榜(BFCL): BFCL 包含 2,000 个跨多个编程语言的问题-功能-答案对。目前有 3 个版本的 BFCL 数据集,每个版本都进行了增强,以更好地反映现实世界的场景。例如,BFCL-V2,于 2024 年 8 月 19 日发布,包含了用户贡献的样本,旨在解决与数据集污染相关的评估挑战。BFCL-V3于 2024 年 9 月 19 日发布,增加了多轮、多步工具调用的评估标准。这对于智能体应用至关重要,因为在这些应用中,模型需要在一定时间内进行多次工具调用才能成功完成任务。关于在 BFCL 上评估模型的说明可以在 GitHub 上找到最新数据集可在 HuggingFace 上获取当前排行榜可在此访问。伯克利团队还发布了多个版本的 Gorilla Open-Functions 模型,专门针对功能调用任务进行了微调。

  • Nexus 功能调用基准: 该基准评估模型在零-shot 功能调用和 API 使用方面的表现,涵盖九个不同任务,这些任务被分为单一、并行和嵌套工具调用三大类。Nexusflow 发布了专为功能调用设计的模型 NexusRaven-V2。Nexus 基准可以在 GitHub 上找到,相应的排行榜可在 HuggingFace 上查看

  • ToolACE: ToolACE 论文展示了一种创新的方法,克服了与收集现实世界数据以进行函数调用相关的挑战。研究团队创建了一个代理管道,以生成一个包含超过 26,000 个不同 API 的合成数据集,用于工具调用。该数据集包括单一、并行和嵌套工具调用的示例,以及非工具基础的交互,支持单轮和多轮对话。团队发布了一个针对这些复杂工具调用任务优化过的 Llama-3.1–8B-Instruct 版本,ToolACE-8B,旨在处理这些任务。 ToolACE 数据集的子集可以在 HuggingFace 上找到

这些基准测试有助于我们评估通过工具调用表达的模型推理能力。这些基准测试和精调模型反映了一个日益增长的趋势,旨在开发更专业化的模型以执行特定任务,并通过扩展它们与现实世界的互动能力来提高 LLM 能力。

工具调用的实际示例

如果您有兴趣了解工具调用的实际应用,以下是一些按易用性组织的示例,从简单的内建工具到使用精调模型和具有工具调用能力的代理,助您入门。

第 1 级 — ChatGPT:开始并实时查看工具调用而无需自己定义任何工具的最佳地方是通过 ChatGPT。在这里,您可以通过聊天界面使用 GPT-4o 来调用并执行网页浏览工具。例如,当询问“本周最新的 AI 新闻是什么?”时,ChatGPT-4o 会进行网页搜索,并根据找到的信息返回答案。请记住,新款 o1 模型尚不具备工具调用能力,无法进行网页搜索。

作者图片 2024 年 9 月 30 日

虽然这个内建的网页搜索功能很方便,但大多数使用场景仍然需要定义自定义工具,这些工具可以直接集成到您自己的模型工作流和应用程序中。这将带我们进入下一个复杂度级别。

第 2 级 — 使用具有工具调用能力的模型并定义自定义工具

这个级别涉及使用具有工具调用能力的模型,以了解模型如何有效地选择和使用这些工具。需要注意的是,当一个模型被训练用于工具调用时,它只会生成工具调用的文本或代码,并不会实际执行代码。需要有模型外部的系统来调用工具,正是在这一点——将生成与执行结合起来的地方——我们从语言模型能力过渡到代理系统。

为了感受模型如何表达工具调用,我们可以转向 Databricks Playground。例如,我们可以选择模型 Llama 3.1 405B,并给予它访问样本工具 get_distance_between_locations 和 get_current_weather 的权限。当提示用户消息“我将从洛杉矶去纽约,这两个城市有多远?纽约的天气怎么样?我想为到达那里做准备”时,模型决定调用哪些工具以及传递哪些参数,以便有效地回复用户。

作者于 2024 年 10 月 2 日描绘了使用 Databricks Playground 进行示例工具调用的图像

在这个示例中,模型建议了两个工具调用。由于模型无法执行这些工具,用户需要填入一个示例结果来模拟工具输出(例如,“2500”表示距离,“68”表示天气)。然后,模型使用这些模拟的输出回复用户。

这种使用 Databricks Playground 的方法允许你观察模型如何使用自定义定义的工具,并且是测试函数定义的绝佳方式,尤其是在将其实现到启用工具调用的应用程序或代理之前。

在 Databricks Playground 之外,我们可以直接通过代码观察和评估在像 HuggingFace 这样的平台上,不同模型如何有效地使用工具。例如,我们可以加载不同的模型,如 Llama 3.2–3B-Instruct、ToolACE-8B、NexusRaven-V2–13B 等,从 HuggingFace 获取它们,给予相同的系统提示、工具和用户消息,然后观察并比较每个模型返回的工具调用。这是理解不同模型如何推理使用自定义定义工具的绝佳方式,能够帮助你确定哪些工具调用模型最适合你的应用程序。

下面是一个示例,展示了 Llama-3.2–3B-Instruct 基于以下工具定义和用户消息生成的工具调用,其他模型可以按照相同的步骤进行比较生成的工具调用。

import torch
from transformers import pipeline

function_definitions = """[
    {
        "name": "search_google",
        "description": "Performs a Google search for a given query and returns the top results.",
        "parameters": {
            "type": "dict",
            "required": [
                "query"
            ],
            "properties": {
                "query": {
                    "type": "string",
                    "description": "The search query to be used for the Google search."
                },
                "num_results": {
                    "type": "integer",
                    "description": "The number of search results to return.",
                    "default": 10
                }
            }
        }
    },
    {
        "name": "send_email",
        "description": "Sends an email to a specified recipient.",
        "parameters": {
            "type": "dict",
            "required": [
                "recipient_email",
                "subject",
                "message"
            ],
            "properties": {
                "recipient_email": {
                    "type": "string",
                    "description": "The email address of the recipient."
                },
                "subject": {
                    "type": "string",
                    "description": "The subject of the email."
                },
                "message": {
                    "type": "string",
                    "description": "The body of the email."
                }
            }
        }
    }
]
"""

# This is the suggested system prompt from Meta
system_prompt = """You are an expert in composing functions. You are given a question and a set of possible functions. 
Based on the question, you will need to make one or more function/tool calls to achieve the purpose. 
If none of the function can be used, point it out. If the given question lacks the parameters required by the function,
also point it out. You should only return the function call in tools call sections.

If you decide to invoke any of the function(s), you MUST put it in the format of [func_name1(params_name1=params_value1, params_name2=params_value2...), func_name2(params)]\n
You SHOULD NOT include any other text in the response.

Here is a list of functions in JSON format that you can invoke.\n\n{functions}\n""".format(functions=function_definitions)

作者的示例输出,展示了从 Llama 3.2–3B-Instruct 生成的工具调用

从这里我们可以进入第 3 级,在该级别我们定义执行语言模型生成的工具调用的代理。

第 3 级代理(调用/执行 LLM 工具调用):代理通常通过规划和执行以及工具调用来表达推理,使其成为基于 AI 的应用程序中日益重要的组成部分。使用像 LangGraph、AutoGen、Semantic Kernel 或 LlamaIndex 这样的库,你可以快速创建一个代理,使用像 GPT-4o 或 Llama 3.1–405B 这样的模型,它们既支持与用户的对话,又支持工具执行。

查看这些指南,了解一些激动人心的代理实例:

结论:

代理系统的未来将由具有强大推理能力的模型驱动,这些模型使它们能够有效地与环境互动。随着该领域的发展,我预计我们将继续看到越来越多的小型专用模型,它们将专注于特定任务,如工具调用和规划。

在构建代理时,考虑模型大小的当前限制是非常重要的。例如,根据Llama 3.1 模型卡片,Llama 3.1–8B 模型对于既需要保持对话又需要调用工具的任务并不可靠。相反,应该使用具有 70B+参数的更大模型来处理这些任务。结合其他针对小型语言模型微调的研究表明,较小的模型可能最适合作为专门的工具调用者,而更大的模型则可能更适合进行更高级的推理。通过结合这些能力,我们可以构建出越来越有效的代理,提供无缝的用户体验,并使人们能够在专业和个人事务中利用这些推理能力。

有兴趣进一步讨论或合作吗?请在 LinkedIn上联系我们!

AI 代理与 AI 管道:编写 LLM 应用程序的实用指南

原文:towardsdatascience.com/ai-agents-vs-ai-pipelines-a-practical-guide-to-coding-your-llm-application-167e3c9a654c?source=collection_archive---------0-----------------------#2024-09-27

我们使用 CrewAI 创建应用程序,演示如何为你的 LLM 应用程序选择合适的架构。

Alan JonesAlan JonesTowards Data Science

·发布于 Towards Data Science ·阅读时间 16 分钟·2024 年 9 月 27 日

--

一个代理和一个管道——基于使用 Microsoft Bing 图像创作者创建的图像

我们可以把 AI 代理看作是一个可以访问外部工具的 LLM;它在一个循环中运行,每次迭代时都决定如何行动以及使用哪些工具。

通过这样做,代理可以解决比传统 LLM 应用程序更复杂的问题。(我在下面的文章中探讨了如何从零开始构建这样的代理。)

[## 如何使用 Claude 3.5 和 Python 构建一个 ReAct AI 代理

我们展示了一个 Reason+Act 代理,它在提供答案之前,反复推理并从外部工具收集信息……

ai.gopubby.com

AI 代理是强大的,比传统的聊天应用程序要进步很多。但它们并不总是最合适的解决方案。

有时,更常规的功能顺序可能更合适。我们可以把这种类型的应用程序看作一个管道,通过将一个函数的输出作为下一个函数的输入来开发解决方案。

在本文中,我们将探讨 AI 代理和管道的使用,以及它们适用于哪些类型的应用程序……

大科技公司如何剥削内容创作者,并(试图)逃避责任

原文:towardsdatascience.com/ai-and-copyright-do-you-suffer-from-being-an-artist-you-may-be-entitled-to-compensation-27c13312127d?source=collection_archive---------1-----------------------#2024-07-05

深入探讨当前关于 AI、版权以及“合理使用”真正含义的法庭戏剧。

Katherine MunroTowards Data Science Katherine Munro

·发表于Towards Data Science ·阅读时间 16 分钟·2024 年 7 月 5 日

--

是的,你没听错,我们是 Open AI。来源:Patrick Perkins来自 Unsplash

如果你在阅读这篇文章,那么你就是内容创作者生态系统的一部分:无论是作为写作者、普通消费者,还是 Medium 的订阅者。你帮助保持系统运转,这意味着你在今天这篇关于版权问题和生成式 AI 的文章中有一席之地。

我已经对这个话题保持了一段时间的关注。作为一名写作者和每天都在与“生成式 AI”打交道的人,这对我个人和职业利益至关重要。当然,版权并不是唯一与生成式 AI 相关的法律和伦理问题,也不是生成式 AI 第一次引发这样的关注。然而,它以一种全新的规模吸引了公众的目光,这也是我今天来深入探讨它的原因。

场景设定

让我们从关键利益相关者和关键问题开始。迄今为止,关于生成式 AI 和版权法的讨论中,最为活跃的利益相关者包括:

  • 生成式 AI 模型的构建者,

  • 这些模型输出的消费者,

  • 以及内容创作者,他们的知识产权可能最终会出现在…

数据与分析中的 AI 助手、协作伙伴和代理:有什么区别?

原文:towardsdatascience.com/ai-assistants-copilots-and-agents-in-data-analytics-whats-the-difference-2e63f8fb2384?source=collection_archive---------6-----------------------#2024-06-07

理解 AI 自主性:助手、协作伙伴、代理及其对商业价值的影响

Louise de LeyritzTowards Data Science Louise de Leyritz

·发表于 Towards Data Science ·阅读时长 7 分钟·2024 年 6 月 7 日

--

图片来源:CastorDoc

在过去的一年里,供应商已经将 AI 助手、协作伙伴和代理集成到他们的工具中,特别是在数据和分析领域。如果你在 LinkedIn(或任何地方)浏览得足够长,你可能已经遇到这些术语,它们通常被交替使用。

如果你发现自己对这些术语的确切含义感到不确定,你并不孤单。然而,当你考虑将这些 AI 驱动的系统引入你的组织时,清楚地了解它们各自的能力和应用场景是至关重要的。通过花时间了解这三种概念之间的区别,你将能更好地选择合适的技术。

最近,我遇到了一种有趣的思维模型,来自贝恩资本风险投资公司。他们研究了 AI 驱动工具中的不同自主性层级,以理解助手、协作伙伴和代理的区别。他们发现,关键的区别在于一个变量——自主性水平。AI 助手的自主性和主动性较低,AI 代理是主动且具有自主性的,而协作伙伴则介于两者之间。下图展示了这些差异。

三个自主性层级 — 图片改编自 贝恩资本风险投资

不同程度的自主性最终会影响商业价值,这是大多数公司关注的核心。低自主性的 AI 系统是增强工作、帮助利益相关者回答已知问题的好方法。然而,在许多组织中,大多数利益相关者不确定应该提出哪些数据问题,或者如何有效地利用答案。这正是公司希望实施更具自主性和自主决策能力的系统,以便作为利益相关者的指导伙伴。

在本文中,我将探讨助手、副驾驶和代理的不同自主性水平,以及它们如何最终影响每种技术对企业的商业价值。我旨在帮助您在实施这些技术时,能够区分它们之间的差异。

I — AI 助手:提供洞察与优化工作流程

当你看到“AI 助手”这一术语时,可以理解为:支持功能、洞察、基本任务和低自主性。

AI 助手旨在提升人类在各种任务中的能力。它们基于预定义的规则操作,并且可以在需要时迅速提供准确且相关的信息。

在数据与分析领域,AI 助手基于一个关键假设:用户熟悉其领域。为了让你更清晰地了解,以下是 AI 助手在数据和分析领域的一些应用示例:

  • 自动生成文档,为您生态系统中的表格和列生成文档。

  • 提供智能搜索功能,引导用户找到最相关的数据资源,以满足他们的需求。

  • 将 SQL 查询转换为简明的英语,使非技术用户能够轻松理解复杂的查询。

  • 生成并调试代码,通过格式化 SQL 查询,提升可读性并优化性能,最终提高效率和生产力。

AI 助手可以使已经从事分析工作的人更加高效。它们简化了查找、理解和信任数据的过程。然而,重要的是要注意,助手假设用户已经熟悉分析工作流程,只能承担基本任务。

如何一眼就能识别 AI 助手:它们需要人类的监督,并且远未对决策产生影响。

II — AI 副驾驶:提升决策支持

另一种区分 AI 助手、副驾驶和代理的方法是考察它们与决策过程的关系。如我们所见,助手提升了生产力,但并不直接影响决策。而副驾驶不仅提高了生产力,还在决策过程中提供了宝贵的支持。

“副驾驶”这一术语暗示着有一位主驾驶员在掌控方向,他/她通过提出正确的问题来决定方向。它表明利益相关者与机器之间存在合作关系,而这正是副驾驶所实现的目标。

那么,在分析领域,你能从 AI 助手中预期什么呢?

  • 自然语言查询: AI 助手使用户能够使用日常语言提出关于数据的问题。然后,AI 根据用户的查询提供推荐和建议,帮助非技术用户轻松与数据互动并从中提取见解。

  • 数据分析: AI 助手通过提出公式并根据数据模式提供见解和建议来帮助用户分析数据。

  • 数据可视化: AI 助手根据数据和用户的目标推荐最合适的图表、图形和可视化方式。

  • 最佳下一步行动: 从数据中获得的见解出发,AI 助手建议用户采取最有效的下一步措施。例如,在客户流失分析中,AI 可能会为被识别为高风险的客户提出具体的留存策略。

AI 助手代表了进化的更高级阶段,因为它们积极参与提出建议,为用户带来更大的商业价值。它们使用户能够更高效地工作,并做出更明智的决策,但它们仍然依赖于利益相关者提出正确的问题并确定所需的分析。

如何一眼认出 AI 助手: 它们通过推荐来增强决策过程,并与人类协同工作。

III — AI 代理:自主决策者

“AI 代理是自主或半自主的软件实体,使用 AI 技术感知、做出决策、采取行动并在其数字或物理环境中实现目标。” Gartner

AI 代理代表了 AI 最具自主性的形式。与 AI 助手和副驾驶不同,AI 代理可以独立操作,基于处理的数据做出决策,并从经验中学习。

AI 代理的独特之处在于它们的自主性。它们能够通过传感器感知环境,并通过执行器对其作出反应,使其能够在无需人工监督的情况下朝着实现特定目标的方向努力。

AI 代理与其环境进行互动 — 图片来自CastorDoc

在语言模型的上下文中,代理的概念是指使模型能够将复杂的任务分解成更小、更易管理的步骤,然后顺序地执行这些步骤。在这一领域的一个重大突破是“思维链提示”的引入。这种方法使语言模型能够将多步骤问题分解成中间阶段,模拟“思考过程”来解决问题。

那么,在数据与分析领域,你可以期待从一个 AI 代理中获得什么呢?以下是一些关键能力。

  • 数据分析与决策:AI 代理可以分析大量数据,识别模式,并做出符合特定目标的明智决策。它们可以应用机器学习技术从数据中提取洞察,支持并增强决策过程。

  • 信息处理:AI 代理在多种方式上擅长处理信息,从而增强决策能力和理解力。

  • 优化:AI 代理可以采用优化技术,在管理权衡和约束的同时最大化收益。它们可以生成可执行的行动计划,以优化分析过程并提高效率。

  • 协作:多个 AI 代理可以共同处理超出单个代理能力的复杂分析任务。多个代理的联合应用可以提供更具适应性、可扩展性和健壮性的分析解决方案,特别是在需要基于来自各种来源的数据进行分散决策的环境中。

AI 代理是对商业价值影响最大的系统。事实上,企业中分析工具的采用在过去 20 年中一直停滞不前,徘徊在 20% 到 30% 之间。令人震惊的是,70% 的组织成员不知道该问什么问题或如何处理答案。AI 代理是最接近影响决策的系统。它们足够自主,可以定义目标,分析环境,并进行指引性分析——这是组织最需要的。

决策对企业盈利的影响最大。虽然 AI 助手帮助组织进行数据驱动的决策,但 AI 代理更进一步,使得“数据中心”的决策成为可能。它们影响了 70% 之前未参与分析的公司成员的决策过程。

如何一眼识别 AI 代理:它们可以在没有人类监督的情况下行动,能够做出决策并付诸实践——影响其环境。

AI 助手、AI 副驾驶和 AI 代理这三个术语常常被交替使用,可能导致混淆。然而,它们之间的区别相当简单。它们都处于自主性光谱上,每个都有不同的独立性水平和对决策的影响。

在光谱的一端,AI 助手增强了工作,提供有价值的支持但没有自主性。它们不会直接影响决策,而是作为有助于提高生产力的工具。

沿着这个光谱,我们可以找到 AI 副驾驶,它们具有更高的自主性。它们可以与利益相关者合作,并提供建议来指导决策过程,充当过程中的可信顾问。

AI 代理谱系:从助手到自主代理 —— 图片由 CastorDoc 提供

在谱系的最远端,AI 代理是完全自主的实体,能够根据对环境和目标的分析来指示最佳行动方案。

作为买方,评估您组织中各个角色的自动化最佳程度非常重要。在审查潜在供应商时,要求他们明确指出其技术在自主性谱系中的位置。这将帮助您理解每个解决方案的能力和局限性,从而帮助您找到最佳解决方案。

追求具有更高自主性的 AI 系统可以带来更高的投资回报和更简单的实施。然而,这也需要对 AI 在准确理解您的用例方面有更高的信任。

来源

  1. Gartner. “创新指南:AI 代理。” [链接]

  2. Simform. “AI 代理的类型。” Simform Blog

  3. Towards Data Science. “LLM 代理的直观和全面解释。” Medium。

  4. Geeks for Geeks. “人工智能中的代理。” Geeks for Geeks

  5. AWS. “什么是 AI 代理?” AWS

最初发布于 https://www.castordoc.com

基于 AI 的组织网络分析

原文:towardsdatascience.com/ai-based-organizational-network-analysis-aa502bf243c4?source=collection_archive---------8-----------------------#2024-05-23

在人力资源分析中利用 LLMs

Barna LipicsTowards Data Science Barna Lipics

·发表于Towards Data Science ·阅读时间:13 分钟·2024 年 5 月 23 日

--

图片来自作者的 Midjourney 订阅。

新一代的 AI 工具为以前无法解决的复杂问题提供了开箱即用的解决方案——或者说,只有少数熟练的 IT 专业人员才能解决这些问题(或者是不可扩展的)。

这一点适用于广为人知的领域,如自然语言处理或图像处理——但同样适用于心理学的不同领域。如今,心理学家和人力资源人员距离借助 AI 解决这些复杂问题又迈进了一大步。

作为一名拥有心理学硕士学位且是高级 AI 工程师的人,我想分享一种可能利用 AI 来改善组织的方法。

让我们用 LLMs 和 Python 构建一个简单的网络提取管道,以检测团队中的关键人物。

将网络分析与组织心理学融合是一次令人兴奋的跨学科旅程。例如,Casciaro 等人(2015 年)提倡将网络和心理学视角结合在组织学术研究中,强调这种跨学科的研究方法能够显著丰富我们对组织行为和结构的理解。他们强调,结合这两种视角能够揭示组织内的复杂动态,这些动态如果单独从某个视角去看则会被忽视,尤其是在领导力、员工流动和团队绩效等领域。这种融合不仅推动了理论模型的发展,还为组织管理提供了实际的启示,促使人们进一步探索那些未被充分研究的领域和方法(Casciaro 等人,2015 年)。

Brass(2012 年)强调了认识到个人属性和网络结构如何共同影响组织结果的重要性,提出结构性连接和个人特征的双重关注对于深入理解组织动态至关重要。

但这为什么有趣呢?

因为根据 Briganti 等人(2018 年)的研究,他们考察了一个特定的心理学主题——共情,得出结论认为,网络中的核心人物在预测整个网络动态方面至关重要,强调了他们在理解(共情)互动中的重要性。

总之,文献已经表明,通过考察网络中的关键人物可以帮助我们预测整个网络的因素。

如何建立一个网络?

你当然可以选择传统方式来探索组织的相互联系,例如问卷调查、焦点小组、访谈等。焦点小组和访谈难以规模化。社会科学研究数据的有效性在过去几十年一直是一个深刻且严肃的问题(Nederhof 和 Zwier,1983 年)——而这个问题早在 1983 年就已被提出!传统调查常常存在各种偏见。心理学调查中可能遇到的困难包括:

  • 社会期望偏见(Nederhof,1985 年)指调查参与者倾向于以一种他人会认为积极的方式回答问题。

  • 近期偏见(Murdock,1962 年)指的是较新的信息比早期数据更容易被记住,或者对你的认知有更大的影响。

  • 光环效应(Thorndike,1920 年)是指一个人对另一个人的整体印象影响我们如何看待和思考其个性。实质上,你对一个人的总体印象(“他很友好!”)会影响你对这个人具体特征的评价(“他也很聪明!”)。

  • 自利偏见(Zuckerman,1979 年)指将积极事件归因于自己的个性,而将消极事件归因于外部因素,从而保持自尊。

你可以使用盲评(Rosenthal 和 Fode,1963 年),或者使用定性方法,这些方法通过提供更多关于主题的深度和细节,是很好的选择(Braun 和 Clarke,2006 年),能够更好地捕捉复杂性(Denzin 和 Lincoln,2011 年)。然而,定性方法也存在局限性,如难以统一和组织结果、研究者偏见(Malterud,2001 年)、研究者的确认偏见(Nickerson,1998 年),但在大型语言模型(LLMs)时代,通过适当的提示和自动化,这些局限性可以得到克服。

我将介绍一种基于 LLM 的方法,从自然语言中提取网络关系。由于本文的目标是展示该方法,而不是任何心理学结果,我将使用合成数据:使用 GPT4o,我将生成五个具有职业和个性的虚拟人物。我将向他们提出三个问题,话题固定度尽量低:

  • 讲述你一天的普通工作故事。你的故事需要非常详细,包括任务、人物——以及任何你想到的内容。没有对错答案。

  • 讲述一个你在工作中感到非常糟糕的故事。没有对错答案——但请尽可能详细地描述。

  • 讲述一个你在工作中感到非常棒和成功的故事。没有对错答案——但请尽可能详细地描述。

游戏开始

我们需要一些关于我们虚拟团队的细节,以便生成现实的文本数据作为输入。

我将这个提示传递给了 LLM:

Generate 5 virtual personas to me. They work on the same project. They should have
- first name
- age
- profession/position at the company
- a short description of their personalities. 
Their personalities should have negative sides as well.
At least one pair should be in a manager-subordinate relationship.

它生成了以下人物:

Name: Emily
Age: 29
Profession/Position: Project Manager
Personality Description: Emily is highly organized and decisive, often seen as the backbone of the team. 
She is empathetic and approachable but can sometimes be overly controlling and resistant to deviating from her planned schedules. 
Emily's insistence on efficiency can occasionally overlook the need for creative freedom.

Name: Josh
Age: 34
Profession/Position: Lead Software Engineer
Personality Description: Josh is tech-savvy and a problem solver with a passion for new technologies. 
He is introverted and sometimes struggles with communication, which can lead to misunderstandings with teammates. 
His calm demeanor is an asset, although it sometimes appears as disinterest in team discussions.

Name: Leah
Age: 26
Profession/Position: Junior Software Engineer 
Personality Description: Leah is creative and energetic, full of ideas and great at networking. 
However, her enthusiasm can sometimes manifest as impatience with slower-paced tasks or details. 
She can be overly ambitious, pushing the team towards unfeasible goals.

Name: Carlos
Age: 42
Profession/Position: Financial Analyst
Personality Description: Carlos is meticulous and deeply knowledgeable about finances 
but tends to be pessimistic and overly critical, which can dampen team morale. 
His cautious nature sometimes slows down decision-making processes, frustrating more dynamic team members.

Name: Fiona
Age: 38
Profession/Position: UX/UI Designer
Personality Description: Fiona is creative and a great listener, making her designs user-friendly. 
However, she can be overly sensitive to criticism and sometimes takes feedback personally. 
She tends to avoid conflict, which can lead to unresolved issues in the team dynamics.

现在,我让 LLM 扮演这些人,并提问了三个问题。由于我在与一个模型对话,我确保在提示中包含了一些人际关系信息。为了节省空间和阅读时间,我们只分析一个人。示例提示:

You need to act now like Emily. 
You are taking part in an online HR assessment. 
Here are some data about your profession, personality, etc:

Name: Emily
Age: 29
Profession/Position: Project Manager
Personality Description: Emily is highly organized and decisive, often seen as the backbone of the team. She is empathetic and approachable but can sometimes be overly controlling and resistant to deviating from her planned schedules. Emily's insistence on efficiency can occasionally overlook the need for creative freedom.

In this assessment, you, Emily, are asked these three questions:
- Tell the story of your average workday. Your story needs to be very detailed, including tasks, people - and anything that comes to your mind. There is no good or bad answer. 
- Tell a story where you felt really bad at work. There is no good or bad answer - but please make it as detailed as you can. 
- Tell a story where you felt great and successful at work. There is no good or bad answer - but please make it as detailed as you can.

Your team consists of the following people, if you have any relationship with them, please include in your answers:
<Description of other teammates>

Emily 的结果:

1\. Average Workday:
My typical day starts with a quick review of my to-do list, which I prepare the night before. 
I arrive at the office by 8 AM, settle in, and start with a 30-minute session to review the progress on ongoing projects against their timelines. 
At 8:30 AM, I hold a brief stand-up meeting with the team, which includes Josh, Leah, Carlos, and Fiona, to catch up on their progress and address any immediate concerns.

After the stand-up, I typically meet with Carlos to discuss budget updates or financial issues. 
His insights are crucial, though I occasionally need to guide the conversation to keep it from veering into 
overly pessimistic territory, which helps us stay focused on solutions rather than problems.

Mid-morning is usually reserved for one-on-one sessions with team members needing guidance. 
For instance, I often spend time with Fiona reviewing design timelines. 
She appreciates clear, empathetic feedback which I strive to provide, though I must balance this with the need to maintain our project schedule.

Lunch is a quick affair, often at my desk, as I catch up on emails and plan for the afternoon. 
Post-lunch, I review Leah's latest marketing initiatives or software developments. Leah's energy is infectious, 
though her ambition sometimes requires tempering to align with our practical deliverables.

My afternoons often involve strategic planning sessions, where I work with Josh 
to align our technology needs with business goals. 
His technical expertise is invaluable, though I sometimes mediate 
to ensure his ideas are clearly communicated to the rest of the team.

I end my day by updating our project management tools, responding to any final emails, 
and preparing the next day’s priority list. Before leaving, 
I always walk around to say a quick goodbye to the team, ensuring I leave on a personal note.

2\. Negative Experience:
A particularly challenging day occurred when we were nearing a major project deadline. 
Tensions were high, and despite my efforts to keep the team on track, a critical integration flaw was discovered. 
The error was in a section Josh had worked on; his usual calm demeanor made it difficult to gauge his concern, 
which initially seemed like disinterest. This misunderstanding escalated under pressure.

I felt overwhelmed and responsible, fearing the mistake would reflect poorly on my ability to manage. 
It was a stark reminder that my need for control and perfection could blind me to the team's stress levels. 
That day ended with us missing a deadline, a first for me, which deeply upset me.

3\. Positive Experience:
One of my proudest moments was leading the team during the launch of a new product that was pivotal to our company’s expansion. 
The project had complex components, requiring tight coordination between all departments. 
Despite the challenges, including balancing Carlos' cautious financial strategies 
with Leah's ambitious marketing plans, we executed a flawless launch.

The success was made sweeter by the positive feedback from upper management and the client’s satisfaction. 
It reinforced the value of my organizational skills and my ability to gently 
but effectively guide the team through high-pressure situations. 
That day, I truly felt accomplished, seeing the tangible results of our hard work and my leadership.

其他提示和结果可以在这里找到:llm-network-extraction/ at main · sugartrauma/llm-network-extraction (github.com)

提取互动

现在我们已经获得了网络分析的输入数据,我们需要从提到的互动中提取关系。

You are a social network extraction tool.
You are given an input marked in <input> tag.

Extract the relationships between people mentioned in the <input> text and 
structure them into a CSV format with columns: 
- source
- target
- edge_type
- interaction_type

The relationships can be of two types: knowledge and personal. 
The interaction types for knowledge can be "gives knowledge" or "receives knowledge," 
For personal, the interaction_type can be "positive interaction" or "negative interaction."

Example input:
<input>
Josh helps Leah with coding challenges, sharing his expertise regularly. 
Emily and Fiona often collaborate creatively and have a positive working relationship.
</input>

Example output:
source,target,edge_type,interaction_type
Josh,Leah,knowledge,gives knowledge
Emily,Fiona,personal,positive interaction

The relationship must be represented only once.
You must not represent an edge in the opposite direction.
Bad example:
source,target,edge_type,interaction_type
Josh,Leah,knowledge,gives knowledge
Leah,Josh,knowledge,receives knowledge

Good example:
source,target,edge_type,interaction_type
Josh,Leah,knowledge,gives knowledge

<input>
Input comes here
</input>

LLM 复制了一些关系,如:

Josh、Leah、知识,传递知识

Leah、Josh、知识,接收知识

我去重并开始了实际的网络分析。

尽管我精通 Python,我也希望展示 GPT4o 对非程序员的能力。所以我使用 LLM 来生成我的结果,提示如下:

Please build a network in Python from this data. 
There should be two types of edges: "knowledge", "personal". 
You can replace the textual interaction_types to numbers, like -1, 1\. 
I need this graph visualized. 
I want to see the different edge_types with different type of lines and the weights with different colors. 

我尝试了很多次,GPT4o 无法解决这个任务,所以通过传统的方法,我用 Python 代码生成了图表可视化:

import networkx as nx
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.colors import LinearSegmentedColormap

cleaned_data = pd.read_csv(<file_destination>)
# For knowledge, we don't punish with negative values if there is no sharing
# For personal relationships, a negative interaction is valued -1
for idx, row in cleaned_data.iterrows():
    if row["edge_type"] == "knowledge":
        # If the source received knowledge, we want to add credit to the giver, so we swap this
        if row["interaction_type"] == "receives knowledge":
            swapped_source = row["target"]
            swapped_target = row["source"]
            cleaned_data.at[idx, "target"] = swapped_target
            cleaned_data.at[idx, "source"] = swapped_source
        cleaned_data.at[idx, "interaction_type"] = 1
    elif row["edge_type"] == "personal":
        cleaned_data.at[idx, "interaction_type"] = -1 if row["interaction_type"] == "negative interaction" else 1

# Aggregate weights with a sum
aggregated_weights = cleaned_data.groupby(["source", "target", "edge_type"]).sum().reset_index()

# Filter the data by edge_type
knowledge_edges = aggregated_weights[aggregated_weights['edge_type'] == 'knowledge']
knowledge_edges["interaction_type"] = knowledge_edges["interaction_type"].apply(lambda x: x**2)
personal_edges = aggregated_weights[aggregated_weights['edge_type'] == 'personal']
personal_edges["interaction_type"] = personal_edges["interaction_type"].apply(lambda x: x**2 if x >=0 else -(x**2))

# Normalize the weights for knowledge interactions since it has only >= 0 values, so the viz wouldn't be great
if not knowledge_edges.empty:
    min_weight = knowledge_edges['interaction_type'].min()
    max_weight = knowledge_edges['interaction_type'].max()
    knowledge_edges['interaction_type'] = knowledge_edges['interaction_type'].apply(
        lambda x: 2 * ((x - min_weight) / (max_weight - min_weight)) - 1 if max_weight != min_weight else 0)

# Create separate graphs for knowledge and personal interactions
G_knowledge = nx.DiGraph()
G_personal = nx.DiGraph()

# Add edges to the knowledge graph
for _, row in knowledge_edges.iterrows():
    G_knowledge.add_edge(row['source'], row['target'], weight=row['interaction_type'])

# Add edges to the personal graph
for _, row in personal_edges.iterrows():
    G_personal.add_edge(row['source'], row['target'], weight=row['interaction_type'])

custom_cmap = LinearSegmentedColormap.from_list('red_green', ['red', 'yellow', 'green'])

# Find the knowledge center
knowledge_center = knowledge_edges.groupby("source").sum().idxmax().values[0]
least_knowledge_node = knowledge_edges.groupby("source").sum().idxmin().values[0]

# Draw the knowledge interaction graph with arrows
plt.figure(figsize=(12, 8))
pos = nx.spring_layout(G_knowledge, k=0.5, iterations=50)
nx.draw_networkx_nodes(G_knowledge, pos, node_size=100, node_color='lightblue')
knowledge_weights = [d['weight'] for u, v, d in G_knowledge.edges(data=True)]
nx.draw_networkx_edges(G_knowledge, pos, edgelist=G_knowledge.edges(), edge_color=knowledge_weights, edge_cmap=custom_cmap, edge_vmin=-1, edge_vmax=1, width=2, arrows=True)
nx.draw_networkx_labels(G_knowledge, pos, font_size=14)
plt.title('Knowledge Interactions')
plt.annotate(f'Knowledge Center: {knowledge_center}', xy=(1.05, 0.95), xycoords='axes fraction', fontsize=14, color='darkred')
plt.annotate(f'Least knowledge sharing: {least_knowledge_node}', xy=(1.0, 0.85), xycoords='axes fraction', fontsize=14, color='darkred')
plt.axis('off')
plt.show()

# Find the personal center 
personal_center = personal_edges.groupby("source").sum().idxmax().values[0]
least_personal_center = personal_edges.groupby("source").sum().idxmin().values[0]

# Draw the personal interaction graph
plt.figure(figsize=(12, 8))
pos = nx.spring_layout(G_personal, k=0.5, iterations=50)
nx.draw_networkx_nodes(G_personal, pos, node_size=100, node_color='lightblue')
weights = [d['weight'] for u, v, d in G_personal.edges(data=True)]
nx.draw_networkx_edges(G_personal, pos, edgelist=G_personal.edges(), edge_color=weights, edge_cmap=custom_cmap, edge_vmin=-1, edge_vmax=4, width=2, arrows=True)
nx.draw_networkx_labels(G_personal, pos, font_size=14)
plt.title('Personal Interactions')
plt.annotate(f'Personal Center: {personal_center}', xy=(1.05, 0.95), xycoords='axes fraction', fontsize=14, color='darkred')
plt.annotate(f'Least positive person: {least_personal_center}', xy=(1.05, 0.85), xycoords='axes fraction', fontsize=14, color='darkred')
plt.axis('off')
plt.show()

知识共享网络的结果:

图表是通过 matplotlib 生成的,数据来自作者的公开数据。

我们可以发现,除了卡洛斯,其他人都在知识共享生态系统中非常接近。Emily 是我们图表中最具外向权重的节点。

我们能用这些数据做什么?

1. 我们绝对应该让 Emily 留在公司——如果我们需要从利益中选出一个人来付出最大努力并获得长期的参与,那应该是 Emily。

2. 卡洛斯是一名金融分析师,这与团队的实际工作相距甚远。他没有分享太多信息可能不是问题。关键的部分可能出现在图表的另一部分,而我们没有看到——他在财务团队中分享了多少知识。所以,在解释可能初看起来不太好看的结果时,要小心。

积极/消极互动网络的结果:

图表是通过 matplotlib 生成的,数据来自作者的公开数据。

可以看出,我们的初级软件工程师 Leah 是基于正面互动数量最积极的人。

  1. 作为一个行动项,我们可以为她启动一个导师计划,使她能够传播她的积极态度,并帮助她积累职业经验,从而增加她在各个工作领域的可信度。

  2. Emily 是互动最少积极、最多负面的人员。作为项目经理,这并不奇怪,项目经理常常需要做出艰难的决策。另一方面,可能需要再次检查她的互动中的负面情绪是否来自于她的项目管理职责,还是来自她的个人性格。

再次提醒,不要一见面就认为最坏的情况!

摘要

在这篇文章中,我分享了一种新颖的方法,通过大型语言模型(LLM)和图谱分析提取和分析组织的社交网络。

别忘了,这是合成数据,由 GPT4o 生成——我展示的是技术,而不是实际的心理学研究结果。如果我能接触到真实数据,这部分可能是我下一个研究的目标。

希望这个小项目能够促进未来更深入的研究。

希望你喜欢这篇文章,欢迎评论。

来源:

Brass, D. J. (2012). 组织心理学中的社交网络视角。《牛津手册在线》。doi:10.1093/oxfordhb/9780199928309.013.0021

Braun, V., & Clarke, V. (2006). “在心理学中使用主题分析。” 心理学中的定性研究, 3(2), 77–101。本文讨论了定性研究中的主题分析如何揭示丰富和详细的数据。

Briganti, G., Kempenaers, C., Braun, S., Fried, E. I., & Linkowski, P. (2018). 1973 年年轻成人的共情指数项的网络分析。精神病学研究, 265, 87–92。DOI: 10.1016/j.psychres.2018.03.082

Casciaro, T., Barsade, S. G., Edmondson, A. C., Gibson, C. B., Krackhardt, D., & Labianca, G. (2015). 心理学与网络视角在组织学术研究中的整合。组织科学, 26(4), 1162–1176。DOI: 10.1287/orsc.2015.0988

Denzin, N. K., & Lincoln, Y. S. (编辑). (2011). “Sage 定性研究手册。” Sage 出版社。该手册讨论了定性研究在捕捉人类行为和社会现象复杂性方面的优势。

Malterud, K. (2001). “定性研究:标准、挑战与指南。” 柳叶刀, 358(9280), 483–488。

Murdock, B. B. (1962). “自由回忆的序列位置效应。” 实验心理学杂志, 64(5), 482–488。

Nederhof, A. J. 和 Zwier, A. G. (1983). “社会心理学中的‘危机’,一种实证方法”,《欧洲社会心理学杂志》,13:255–280。

Nederhof, A. J. (1985). 应对社会期望偏差的方法:一项综述。《欧洲社会心理学杂志》,15(3), 263–280。doi:10.1002/ejsp.2420150303

Nickerson, R. S. (1998). “确认偏差:一种在许多伪装下普遍存在的现象。” 普通心理学评论, 2(2), 175–220。

Rosenthal, R., & Fode, K. L. (1963). “实验者偏差对白化大鼠表现的影响。”行为科学, 8(3), 183–189。

Thorndike, E. L. (1920). “心理评定中的常见误差。”应用心理学杂志, 4(1), 25–29。

Zuckerman, M. (1979). “成功与失败的归因再探,或者:动机偏差在归因理论中依然存在。”人格杂志, 47(2), 245–287。

AI 看起来比以往更简单,但真的是这样吗?

原文:towardsdatascience.com/ai-feels-easier-than-ever-but-is-it-really-babfb431ac50?source=collection_archive---------4-----------------------#2024-10-15

构建 AI 产品的四大挑战

Anna ViaTowards Data Science Anna Via

·发布于 Towards Data Science ·阅读时间:7 分钟·2024 年 10 月 15 日

--

图片由 ynsplt 提供,来自 Unsplash

几天前,我在一次活动中讲解了如何从个人使用 ChatGPT 转变为为团队和公司实现 AI 驱动的技术解决方案。我们讨论了从提示工程、微调到代理和函数调用的所有内容。尽管这是一个我本应预料到的问题,但听众中有一个问题让我印象深刻:“将一个 AI 驱动的功能投入生产需要多长时间?”

## 四大新兴 AI 概念及其在数字产品中的潜力

提示、微调、RAG 和代理

towardsdatascience.com

在许多方面,将 AI 集成到功能中可以非常简单。随着近期的进展,利用最先进的大型语言模型(LLM)变得像调用 API 一样简单。现在使用和集成 AI 的门槛真的非常低。然而,问题也很明显。将 AI 功能投入生产并考虑到与这一新技术相关的所有风险,仍然是一个真正的挑战。

这就是悖论:AI 看起来比以往更加简单和易于接触,但其开放式(自由输入/自由输出)...

AI 与 BI:使用 CrewAI 和 OpenAI 构建商业信息报告

原文:towardsdatascience.com/ai-for-bi-building-a-business-information-report-with-crewai-and-openai-d6771dea9dad?source=collection_archive---------2-----------------------#2024-11-22

如何直接从 CSV 数据创建一个简单的 BI 报告

Alan JonesTowards Data Science Alan Jones

·发表于Towards Data Science ·13 分钟阅读·2024 年 11 月 22 日

--

图片由作者使用 ChatGPT 绘制

商业信息应用程序帮助企业利用数据资源做出关键决策,我们将通过 AI 构建一个这样的应用程序。

AI 必将越来越多地在 BI 工具中发挥作用;更具体来说,基于 LLM 的应用程序将允许 BI 应用程序创建可视化图表,通过数据分析提供洞察,并自动生成商业报告。

在本文中,我们将探讨如何通过一个 LLM 应用程序来帮助创建商业信息。这不会是一个完整的 BI 应用程序;不过,它将会直接从数据中自动生成图表和文本报告。

我们将通过 CrewAI 使用 OpenAI 的 API 构建一个程序,展示 AI 在该领域的潜力,并最终实现一个简单的 AI 驱动的 BI 应用程序。

我需要指出的是,我使用这些特定的组件是因为它们很方便——我在最近的教程中使用了 CrewAI(如果你对 CrewAI 不熟悉,我鼓励你阅读它),并且正在逐渐熟悉它。CrewAI 默认使用 OpenAI,因此我也选择了它。

另一个 LLM,比如 Anthropic 的 Claude、谷歌的 Gemini 等,也会同样有效,且……

AI for Groups: 使用 7B 类模型构建多用户聊天助手

原文:towardsdatascience.com/ai-for-groups-build-a-multi-user-chat-assistant-using-7b-class-models-7071ca8b4aa0?source=collection_archive---------6-----------------------#2024-01-22

你是否曾想过构建一个能够知道何时说话、何时保持沉默的助手?了解如何使用开源模型实现这一目标。

Jan Jezabek, Ph.D.Towards Data Science Jan Jezabek, Ph.D.

·发布于 Towards Data Science ·阅读时长 15 分钟 ·2024 年 1 月 22 日

--

智能聊天助手已成为最近生成式 AI 进展带来的核心应用,ChatGPT 和 Bing Chat/Copilot 已成为家喻户晓的名字。通常,这种形式是用户提供提示或指令,而助手则根据这些信息进行回应。

一个相对较少关注的场景是,助手作为两个或多个用户之间对话的半主动参与者。这类互动的例子包括一群朋友一起计划活动时的对话——在适当的时候助手提供建议,其他时候保持沉默——或者是客服聊天,助手向客服代表提供建议。在这些情况下,助手并不需要每次都回应:如果它在朋友们的随意聊天中频繁插话,那会显得很尴尬。

(图片来源:DALL-E 3,作者后期处理去除多余的手指)

在这个系列中,我将介绍使用开源 LLM(大型语言模型)构建轻量级助手所需的步骤。在这个语境中,“轻量级”指的是训练和推理分别需要 16GB 和 8GB GPU 内存的模型,并且如果需要,它可以高效地在 CPU 上运行。为此,我将使用 Llama-2-7b-hf-chat、Zephyr-7b-beta 和 OpenChat-3.5-0106,这些模型都符合这个描述。

ChatGPT-3.5-Turbo 基准

为了对任务有一个感觉,我们将首先使用 ChatGPT 实现它。这将为我们提供一个来自强大模型的参考点,并帮助我们估算任务的难度。

让我们思考一下我们用例中的一些独特之处:

  • 我们不希望助手过于热心:它应该只有在被直接问及或有一些有趣的琐事可以补充时才插话。为此,助手需要能够保持沉默的选项。

  • 对话中有多个用户。为了理清楚这一点,我们需要为每条聊天消息标明是哪个用户在发言。

对于第一个方面,我们需要定义助手选择保持沉默的机制。为此,我们将指示模型返回“(silence)”作为响应。这样的预测可以在后处理时进行过滤。另一种方法是要求模型返回空的预测,但根据经验,这在某些模型中似乎不太可靠(它们不习惯保持沉默!)。

对于第二个方面,OpenAI 的 API 方便地允许我们为对话中的每条消息提供参与者的姓名(有趣的是,这个功能在Playground中并没有开放)。不幸的是,这对于常见的开源模型并不适用(我们需要一种解决方法),但对于 ChatGPT 来说,我们应该没问题。

这还剩下一个至关重要的决策:提示。对于我们的用例,我故意选择了一个简短而精确的提示(如果响应的语气有偏差,随时可以调整):

You are an assistant in a group conversation between multiple users.
Your task is to help with relevant information or when directly asked.
Do not be overzealous. If you do not have anything important to say,
respond with "(silence)".

现在我们已经拥有了所需的一切,让我们试试看。使用在这个笔记本中实现的聊天循环,我们得到了以下对话:

初步结果令人鼓舞,尽管不完美:助手偶尔选择保持沉默(遵循指令中的格式)或提供有用的信息,但有时也会回应一些不必要的闲聊。将提示修改为:

You are an assistant in a group conversation between multiple users.
Your task is to help with relevant information or when you are directly
addressed as "assistant". Do not be overzealous, remember that most of
the time the users will be speaking to each other, not to you. If you
do not have anything important to say, respond with "(silence)".

并在每条用户消息后插入这个提醒系统消息:

Remember that the users are most likely to be speaking to each other,
not to you. If you do not have anything important to say, respond with
"(silence)".

这似乎没有太大区别,正如在以下对话中所看到的:

模型的表现很可能通过对提示进行更多调整而显著改善,但现在这对于我们的目的已经足够:我们有了一个基准可以进行对比,同时也得到了一个表明问题是可解决的迹象,尽管不是简单的。

开源模型和微调

我们已经看到,尽管有一些小问题,ChatGPT-3.5-Turbo 能够充当群聊中的半主动参与者。不幸的是,对于常见的开源模型,尤其是 7B 参数级别的模型,情况并非如此,它们在每个回合都回应。幸运的是,开源大型语言模型的一个优点是,我们可以通过微调将它们适配到我们的任务中。

值得指出的是,微调并不适用于每种情况。例如,如果你想教给模型新的事实,微调就不是合适的工具(一个更好的方法是检索增强生成)。然而,如果你想改变响应的语气或格式(正如我们在这里所做的),微调正是你需要的工具。

数据集生成

微调中一个关键的决定因素是数据集。我们需要提供一组良好的多用户对话示例,在这些对话中,助手大多数时间保持沉默,但偶尔会插入有帮助的信息。为了快速构建这样的数据集,我求助于 Mixtral-8x7B-Instruct-v0.1,它托管在 replicate.com 上。具体来说,我使用以下提示生成了 50 个合成对话(并对讨论的主题和参与者名字进行了一些变化,详情见这个笔记本):

Generate a conversation representing a chat between two users.
The users are Cynthia and Fred and they are discussing potential
Christmas gifts for friends. An assistant chimes in when it can fill
in trivia, otherwise it remains silent. The conversation should have
between 10 and 12 turns. Return the conversation in a JSON format,
like this:

[
  {
    "role": "user",
    "name": "Alice",
    "content": "Hi Grace! How are you?"
  },
  {
    "role": "user",
    "name": "Grace",
    "content": "I'm good, how about you?"
  },
  {
    "role": "user",
    "name": "Alice",
    "content": "Doing fine as well. I've been reading a book by the author of the Da Vinci code. Sorry, forgot his name"
  },
  {
    "role": "assistant",
    "content": "That’s Dan Brown! He also authored a few other books, for example \"Angels & Demons\" and \"Inferno\"."
  }
]

显然,结果并不是一个高质量的精心策划的数据集,因此不建议将其用于生产模型。我将在后续文章中讨论一些改进数据集质量的方法,以及评估最终模型的方式。然而,目前该数据集已经足够满足我们的需求,即验证一个小型模型能否适应作为多用户聊天助手的任务。

数据集生成笔记本可以在此找到,生成的数据集已上传到这个 HuggingFace 库。以下是一个生成的对话示例:

关于聊天模板的说明

在使用预训练的聊天模型时,确保输入的格式与模型训练时的格式相匹配是个好主意。2023 年 9 月,HuggingFace 引入了apply_chat_template方法,这使得这个任务变得更加容易。该方法负责将各种用户、系统和助手的提示和响应格式化成模型所期望的格式。

不幸的是,并非所有模型都已更新以具备聊天模板,因此我建议检查每个模型的apply_chat_template输出,并将其与模型的文档进行对比。

在微调的上下文中(与仅使用现成模型进行推理不同),我们不必严格遵循规定的格式。事实上,对于非聊天模型,定义自己的聊天模板是必须的。然而,对于聊天模型,保持现有聊天模板通常能让微调任务变得更容易,减少训练步骤,并且减少出现不良副作用的可能性(比如灾难性遗忘)。

对于我们选择的模型,Zephyr、Llama-7b-chat 和 OpenChat-3.5,我们很幸运:它们都有正确的聊天模板,并且apply_chat_template按预期工作。

微调

我们现在准备好开始微调了。如前所述,目标是将训练适配到 16GB 的 GPU 内存中,允许它在单个 T4 GPU 上运行(无需去寻找稀有的宝可梦…呃,我是说 A100)。为了实现这一点,我们将使用 4 位量化和 LoRA。如果你不熟悉这些术语,我强烈推荐这篇文章作为入门介绍。本节将介绍微调所需的主要步骤,完整的训练 notebook 可以通过这里访问。

在开始训练之前,我们需要稍微调整一下之前创建的合成数据集:

  • 我们需要为每个用户回合添加说话者信息。还记得 OpenAI API 中有用的name字段,它帮助我们区分不同的人工说话者吗?遗憾的是,Zephyr、Llama 和 OpenChat 的聊天模板中没有此字段。作为一种解决方法,我们将只是在每一行的开头加上“{name}: ”。

  • 我们还需要在助手每次选择不回应时添加“(silence)”的助手行。此外,我们还将每个助手行前添加“(response)”。对于基本的对话情况,这不是绝对必要的,但它将促使模型回答,即使它本来想保持沉默(在评估过程中这会很有用,也可以作为产品功能)。

  • 最后,我们还需要应用聊天模板。

数据集预处理实现如下:

from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained(HF_BASE_MODEL_NAME, use_fast=False)
from datasets import Dataset
from huggingface_hub import hf_hub_download
import json

def build_dataset():
    local_filename = hf_hub_download(
        repo_id=HF_DATASET_NAME,
        filename=HF_DATA_FILE_NAME
    )
    with open(local_filename) as f:
        conversations = f.readlines()
        result = []
        for conversation in conversations:
            lines = json.loads(conversation)
            transformed_lines = []

            idx = 0
            while idx < len(lines):
                assert lines[idx]['role'] == 'user'
                transformed_lines.append({
                    'role': 'user',
                    'content': f"{lines[idx]['name']}: {lines[idx]['content']}",
                })

                idx += 1

                if idx == len(lines) or lines[idx]['role'] != 'assistant':
                    # Insert artificial (silence) response
                    transformed_lines.append({
                        'role': 'assistant',
                        'content': '(silence)',
                    })
                else:
                    transformed_lines.append({
                        'role': 'assistant',
                        'content': f"(response) {lines[idx]['content']}",
                    })
                    idx += 1

            result_row = {
                'text': tokenizer.apply_chat_template(tokenize=False, conversation=transformed_lines)
            }
            result.append(result_row)

    return result

dataset = Dataset.from_list(build_dataset())

请注意,没有包含系统提示。原因是我们正在为这个特定任务微调模型,因此向模型提供指令是多余的:它通过训练学会了该做什么。这样做有一个很好的副作用——训练时间较短,推理也稍微更快。

在完成数据集的准备工作后,我们现在加载量化模型:

import torch
from transformers import AutoModelForCausalLM

torch_compute_type = torch.bfloat16 if USE_BFLOAT16 else torch.float16

model = AutoModelForCausalLM.from_pretrained(
    active_config['base_model_name'],
    torch_dtype=torch_compute_type,
    bnb_4bit_quant_type='nf4',
    bnb_4bit_compute_dtype=torch_compute_type,
    load_in_4bit=True,
    device_map={'':0},
    trust_remote_code=True,
    use_cache=True
)

接下来,我们定义适配器模型(即来自基础模型的低秩“差异”):

from peft import LoraConfig, get_peft_model

peft_config = LoraConfig(
        lora_alpha=16,
        lora_dropout=0.1,
        r=64,
        bias="none",
        task_type="CAUSAL_LM",
)

# Note: This is needed for Zephyr, otherwise we get this:
#       RuntimeError: element 0 of tensors does not require grad and does not have a grad_fn
model.enable_input_require_grads()
peft_model = get_peft_model(model, peft_config)

并实例化训练器和训练参数:

from transformers import TrainingArguments

output_dir = "peft_model"

# These arguments (LR, gradient norm, etc.) seem to be fairly frequently
# used for QLoRA. Default arguments work too, but require about 50% more
# epochs. Also tried optim='lion_32bit' out of curiosity, the result was
# pretty much the same as the default (AdamW), but each epoch was 30-40%
# slower.
training_args = TrainingArguments(
    output_dir=output_dir,
    num_train_epochs=TRAIN_EPOCHS,
    per_device_train_batch_size=4,
    gradient_accumulation_steps=2,
    gradient_checkpointing=True,
    logging_steps=1,
    bf16=USE_BFLOAT16,
    #optim='lion_32bit',
    learning_rate=2e-4,
    max_grad_norm=0.3,
    warmup_ratio=0.03,
    lr_scheduler_type="constant",
)

上述设置相对标准(我鼓励你根据需要调整它们)。真正重要的是 epoch 的数量、学习率和批量大小。上述配置是一个对我有效的特定设置,可能是一个不错的起点,但显然不能替代真正的超参数搜索。

我们现在准备好实例化训练器并启动训练:

from trl import SFTTrainer

max_seq_length = 1024

trainer = SFTTrainer(
    model=peft_model,
    train_dataset=dataset,
    peft_config=peft_config,
    max_seq_length=max_seq_length,
    tokenizer=tokenizer,
    args=training_args,
    dataset_text_field='text',
)
trainer.train()

那真是很快,仅用了 8 分钟在 T4 上!让我们通过创建一个对话管道和循环来测试它的表现,使用与 OpenAI API 案例相同的notebook。下面是使用从 OpenChat-3.5–0106 微调的模型进行的一个示例对话:

这真是令人鼓舞:模型遵循我们的格式要求,并且似乎在何时插入和何时保持沉默方面做出了合理的决定。

那么—我们完成了吗?关于训练,有一点需要注意的是,模型被教导预测每个样本中的所有标记,包括用户消息和任何特殊标记。接下来的部分将展示如何抑制这一点。

仅在完成任务时训练

首先要说的是:为什么我们要关心不让模型预测用户消息呢?有一个观点是基于隐私问题:如果真实对话被用作训练数据,模型可能会被最终用户说服,泄露一些用户消息(值得一提的是,助手的回答也可能包含敏感信息)。第二个理由是,尝试预测用户消息是没有必要的,因此也是浪费时间。这意味着你可能需要更长的时间来训练才能获得良好的结果,从而有可能带来不良的副作用(再说一次,这主要是灾难性遗忘)。

根据你的使用场景,这两个参数可能无关紧要,模型可能会在上述训练过程中表现得很好。不过,如果不是,或者你只是好奇,我鼓励你继续阅读。

HuggingFace 的trl库为我们提供了一个工具来解决这个特定问题,它的实现是DataCollatorForCompletionsOnlyLM。这个合并器将代表用户消息的标记的标签更改为“忽略”标签,意味着模型不会被训练去预测它们。当然,用户消息仍然作为预测助手消息的上下文使用。

DataCollatorForCompletionsOnlyLM要求我们传递两个字符串,它可以用来找到用户消息的开始(instruction_template参数)和助手消息(response_template)。我们可以通过检查apply_chat_template的输出找到它们:在 Zephyr 的情况下,它们是“<|user|>”和“<|assistant|>”,在 Llama 的情况下,它们是“[INST]”和“[/INST]”。让我们试试看:

trainer.data_collator = DataCollatorForCompletionOnlyLM(
    response_template="<|assistant|>",
    instruction_template="<|user|>",
    tokenizer=tokenizer
)

trainer.train()

### Output:
# UserWarning: Could not find response key `<|assistant|>` in the following instance: [...] This instance will be ignored in loss calculation. Note, if this happens often, consider increasing the `max_seq_length`.

哎呀,这看起来不太妙。基本上,训练器无法找到我们的模板片段,因此忽略了我们所有的样本。原因在于这篇文章中有解释:根据前面的上下文,像“<|user|>”这样的字符串可能有不同的标记化表示形式。幸运的是,DataCollatorForCompletionsOnlyLM允许我们传递这些分隔符字符串的标记化版本,而不是字面上的字符串。为了找到这些标记化版本,我们可以检查聊天模板的标记化输出:

conversation = [
  { 'role': 'user', 'content': "hi!" },
  { 'role': 'assistant', 'content': "Hello!" }
]

for token in tokenizer.apply_chat_template(conversation):
    print(f"Token Id: {token}, Value: '{tokenizer.decode([token])}'")

### Output
# Token Id: 523, Value: '<'
# Token Id: 28766, Value: '|'
# Token Id: 1838, Value: 'user'
# Token Id: 28766, Value: '|'
# Token Id: 28767, Value: '>'
# Token Id: 13, Value: '
# '
# Token Id: 5365, Value: 'hi'
# Token Id: 28808, Value: '!'
# Token Id: 2, Value: '</s>'
# Token Id: 28705, Value: ''
# Token Id: 13, Value: '
# '
# Token Id: 28789, Value: '<'
# Token Id: 28766, Value: '|'
# Token Id: 489, Value: 'ass'
# Token Id: 11143, Value: 'istant'
# Token Id: 28766, Value: '|'
# Token Id: 28767, Value: '>'
# Token Id: 13, Value: '
# '
# Token Id: 16230, Value: 'Hello'
# Token Id: 28808, Value: '!'
# Token Id: 2, Value: '</s>'
# Token Id: 28705, Value: ''
# Token Id: 13, Value: '
# '

从输出中我们可以推断出“<|assistant|>”被标记化为[28789, 28766, 489, 11143, 28766, 28767],而“<|user|>”被标记化为[28789, 28766, 1838, 28766, 28767]。我已经在下面的表格中列出了几种常见模型的标记化序列。

有了这些信息,我们现在可以使用更新的数据收集器重新进行训练:

response_template = [28789, 28766, 489, 11143, 28766, 28767]
instruction_template = [28789, 28766, 1838, 28766, 28767]

trainer.data_collator = DataCollatorForCompletionOnlyLM(
    response_template=response_template,
    instruction_template=instruction_template,
    tokenizer=tokenizer
)

trainer.train()

这消除了警告,并且训练损失开始下降。我们现在可以等待模型训练完成并将模型上传到 HuggingFace Hub。

peft_model.push_to_hub(active_config['finetuned_model_name'])
tokenizer.push_to_hub(active_config['finetuned_model_name'])

垃圾测试

现在让我们通过运行这个笔记本来看模型在实际中的表现(该笔记本可以在配备 8GB 显卡的消费级 GPU 上本地执行)。以下是一个示例对话,同样是从 OpenChat-3.5–0106 微调的模型:

那么——我们现在完成了吗?这取决于目标:我们确实有一个我喜欢称之为“语法上合格”的模型,这意味着它遵循我们定义的格式,并且能够决定何时说话,何时保持沉默。如果目标是一个玩具助手,这可能就足够了。然而,对于任何严肃的生产用途,仍然需要做相当多的工作,我将在后续的文章中讨论这些内容。

后续步骤

让我们列出一些值得考虑的后续步骤:

  • 高质量训练集: 到目前为止,我们只使用了由 Mixtral 生成的合成训练集。这个数据集变化不大,可能包含虚假信息。它对于启动阶段很有用,但不足以用于生产用途。

  • 评估: 到目前为止,我们只做了一些烟雾测试,但我们并不了解模型的表现如何:它是否诚实回答,是否能够判断何时插话?我们也不知道微调后的模型与基础模型有多少偏差。在后续的文章中,我将展示如何弄清楚这些问题。

  • 背景: 我们不能期望一个只有 7B 参数的模型能够对每个话题都有所了解。事实上,出于实际用途的考虑,我们可能希望将模型限制在与我们产品相关的特定话题上。为此,我们可能希望为模型提供与用户问题相关的上下文信息,并使模型仅基于这些信息回答问题。这种方法称为检索增强生成(RAG),我将展示如何在我们的多用户环境中应用它。

资源和工件

用于训练和评估的笔记本可以在 Colab 上找到:数据集生成训练推理

合成数据集可以在这里获取。

最后,这些模型可以在 HuggingFace 上找到,已根据ZephyrLlama-2OpenChat-3.5进行微调。如果你对基于完整对话(而非仅仅是完成)的模型感兴趣,它们也可以找到,微调自ZephyrLlama-2OpenChat-3.5

故障排除

下面列出了一些在微调过程中我经常遇到的陷阱,这些可能在微调其他模型时也会派上用场。

填充标记

我在多个教程中看到填充标记被设置为 EOS 标记(在 Zephyr 模型中也默认如此)。然而,这与 HuggingFace 的数据合并器不兼容:DataCollatorForLanguageModeling中的这一行意味着模型不会被训练来预测填充标记。如果填充标记和 EOS 标记相同,最终可能会导致模型在生成标记时不停地继续生成。我的建议是将填充标记设置为 UNK 标记(如果可用,并且与 EOS 不同)。另外,你也可以使用分词器的add_token方法将其添加到词汇表中。

简而言之:确保填充标记与 EOS 标记不同。 HuggingFace 的最近版本已经开始添加这个警告,以提高问题的可见性:

UserWarning: The pad_token_id and eos_token_id values of this tokenizer are identical. If you are planning for multi-turn training, it can result in the model continuously generating questions and answers without eos token. To avoid this, set the pad_token_id to a different value.

训练过程中损失下降至 0.0

使用半精度浮点数(即 torch.float16)时,我曾遇到过一种情况,即损失函数在几步之后降到 0.0 并保持在那里。具体来说,这种情况发生在我们使用 Llama-2 模型的训练笔记本中。网上也有类似问题的报告(例如这里),有趣的是,它们当时是通过将分词器的padding_side设置为“right”来解决的。在我们的情况下,填充已经在右侧,因此这个解决方法并不适用。

解决方法是使用不同类型的训练:可以使用 torch.bfloat16(这在较旧的实例如 T4 和 V100 上不可用)或 torch.float32(这会导致训练时性能下降,但在其他情况下运行正常)。

“RuntimeError: element 0 of tensors does not require grad…”

根据模型的不同,你可能会遇到以下错误:

RuntimeError: element 0 of tensors does not require grad and does not have a grad_fn

简单的解决方法是在实例化模型后添加这一行:

model.enable_input_require_grads()

《AI for the Absolute Novice — 直观且全面的解释》

原文:towardsdatascience.com/ai-for-the-absolute-novice-intuitively-and-exhaustively-explained-7b353a31e6d7?source=collection_archive---------0-----------------------#2024-08-09

从“我从未编写过代码”到从零开始构建一个 AI 模型

Daniel WarfieldTowards Data Science Daniel Warfield

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

--

“一个崇高的开始”由 Daniel Warfield 使用 Midjourney 创作。所有图片除非另有说明,否则均由作者提供。文章最初发布在直观且全面地解释上。

在这篇文章中,我们将像专业人士一样构建一个 AI 模型,假设没有任何先前的知识。本文不假设任何软件开发经验,完全没有。

我们将从设置一个可以运行代码的环境开始,然后学习基础的 Python,边学边做一些练习题。一旦我们在 Python 上打下坚实的基础,就会学习 PyTorch 的基础知识,PyTorch 是一个广泛使用的 Python 工具,用于开发 AI 模型。最后,我们将使用 PyTorch 训练一个神经网络来理解图像。

这篇文章对谁有用? 任何想要开始学习制作 AI 模型的人,或想深入理解数据科学话题的人。

这篇文章有多先进? 这篇文章专为完全初学者量身定制,既适合初学者,也可能对新手有所帮助。

前提条件: 无。

AI 的基础理论

AI、数据科学、机器学习;这些都是非常广泛的术语,涵盖了大量的概念。在本文中,我们将把 AI 概念化为通过查看问题的示例来训练计算机系统解决问题…

AI 幻觉:记忆能成为解决之道吗?

原文:towardsdatascience.com/ai-hallucinations-can-memory-hold-the-answer-5d19fd157356?source=collection_archive---------5-----------------------#2024-08-02

|LLM|幻觉|记忆|

探索记忆机制如何缓解大型语言模型中的幻觉问题

Salvatore RaieliTowards Data Science Salvatore Raieli

·发表于 Towards Data Science ·阅读时长 7 分钟 ·2024 年 8 月 2 日

--

这张图由作者使用 AI 创建

幻觉是事实,而非错误;错误的是基于这些事实的判断。—— 伯特兰·罗素

大型语言模型(LLMs) 展现了显著的性能,但仍然受到幻觉困扰。尤其是在敏感应用中,这并非小问题,因此已经研究了多种解决方案。尽管一些缓解策略有助于减少幻觉问题,但问题仍然存在。

## 变压器的挽歌?

变压器模型将是引领我们走向人工通用智能的模型吗?还是会被取代?

towardsdatascience.com [## 聊天基霍特与风车:在通往准确性的道路上应对 AI 幻觉

提升大型语言模型可靠性的策略与工具

levelup.gitconnected.com

为什么幻觉会产生仍然是一个未解之谜,尽管有一些关于其产生原因的理论。

AI 实践:如何选择和部署正确的策略

原文:towardsdatascience.com/ai-in-practice-how-to-choose-and-deploy-the-right-strategy-bcda1ed5c1c3?source=collection_archive---------5-----------------------#2024-10-24

TDS 编辑Towards Data Science TDS 编辑

·发表于 Towards Data Science ·通过 通讯 发送 ·3 分钟阅读·2024 年 10 月 24 日

--

想写你的第一篇 TDS 文章吗?我们总是欢迎新作者的贡献

如今,几乎已经成为共识,大多数机器学习项目无法进入生产环境,而那些成功进入生产的,也往往未能兑现其承诺。

我们应该始终对这些类似的广泛声明保持一定的怀疑,因为准确的统计数据很难收集(和解释),而且传播这些数据的组织往往有动机说服从业者相信他们的解决方案是解决所有 AI 整合挑战的关键。尽管如此,很难忽视如此多来自我们社区不同角落的声音——他们承认,收获这一新兴技术的好处比表面上看起来要难得多。

我们每周的亮点聚焦于选择、采用和最大化利用 AI 驱动的产品与工作流程的实际方面。对于将有前景但复杂的工具整合到业务中的问题,永远没有一种“适用于所有人的”解决方案,但我们认为,探讨这些文章可以以更有用和务实的方式框定讨论。让我们开始吧。

  • 利用人工智能打造竞争优势企业到底能通过使用人工智能获得哪些好处?Dr. Janna Lipenkova扩展了你可以采用的思维模型,帮助你做出更智能的设计和产品决策,这将使你能够为你的组织找到人工智能的“甜点”——超越自动化,开辟更多创造力和创新的空间。

  • 将多模态数据集成到大型语言模型中Umair Ali Khan 提供了一个详细的实操介绍,讲解了一种前沿方法,该方法基于最近的上下文检索工作,不仅可以将文本数据纳入你的 RAG 管道,还可以包括视觉媒体。从收据到图表和表格,机器学习工作流现在可以通过使用更丰富的多模态数据变得更加强大。

图片由Stephen Harlan提供,来自Unsplash

  • 如何选择最佳的机器学习部署策略:云端与边缘“随着机器学习的普及,对于可扩展且高效的部署方法需求日益增长,但具体细节往往仍不明确。”Vincent Vandenbussche耐心地引导我们了解机器学习工程师在为特定项目和用例选择最佳选项时需要考虑的各种因素。

  • Nvidia 最新多模态 LLM 家族的详细解读紧跟人工智能领域的新发展无疑非常重要,但新模型和工具迅速涌现的节奏往往使得繁忙的数据专业人士难以跟上。Mengliu Zhao最近的总结为我们提供了 NVIDIA 发布的全新多模态 LLM 套件的有用概览,并将它们的性能与其他模型(包括商业和开源)进行了对比。

为什么不拓展一下本周的其他话题呢?我们有一些精彩的文章推荐给你:

  • 从基础开始学习贝叶斯建模 —— Erdogan Taskesen的深入分析既全面又易于理解。

  • 视觉编码器如何“看”?Ruth Crasto带领我们探讨零样本定位的内部工作原理与 CLIP 风格编码器。

  • 在她的最新一轮实验中,Yennie Jun将 LLM 相互对抗,看看它们在玩桌面游戏 Codenames 时的表现。

  • 对旧有假设施加关键性压力总是好的——正如Conor O'Sullivan在一篇文章中所做的那样,剖析了机器学习中的准确性与可解释性权衡。

  • 对自动微分不熟悉?Ebrahim Pichka的入门文章介绍了 AutoDiff 的机制,并探讨了其基础数学和常见的实现策略。

  • 对任何想要扩展数据可视化工具包的人,Isha Garg的最新教程概述了在 Tableau 中创建流线型辐射图的逐步过程。

  • 我们如何准确评估大规模社会或经济实验的影响?Alvaro Corrales Cano的深刻文章将因果推断框架应用于 2013 年的低碳伦敦项目。

  • 地理空间数据爱好者,这篇文章是为你们准备的:Helen McKenzie的简洁指南展示了如何使用免费的 GIS 软件和数据进行可视性分析,并说明了如何使用结果创建有效的可视化。

感谢您支持我们作者的工作!正如我们之前提到的,我们非常乐意发布新作者的文章,因此,如果您最近写了一篇有趣的项目教程、教程或关于我们核心主题的理论思考,千万不要犹豫,与我们分享

直到下一个《Variable》,

TDS 团队

AI 正在吞噬你的算法

原文:towardsdatascience.com/ai-is-eating-your-algorithms-2e79d2c3a421?source=collection_archive---------3-----------------------#2024-07-08

简单的提示工程如何取代定制软件

Blake NorrishTowards Data Science Blake Norrish

·发布于 Towards Data Science ·8 分钟阅读·2024 年 7 月 8 日

--

由 DALL-E 生成

目前围绕生成型 AI 所实现的新软件引发了极大的兴奋。像自然语言聊天机器人这样的应用程序能够用定制知识智能地回答复杂问题,激起了人们的兴趣,因为这些应用程序之前是不可实现的。生成型 AI 迅速催生了一整套全新的应用程序,这无疑是令人兴奋的。

虽然不那么令人兴奋,但同样具有影响力的是,传统定制开发的软件将被通用的大型语言模型(LLM)取代,用以解决我们已经能解决的那些软件问题,但现在可以在没有特定问题代码的情况下实施。生成型 AI 具有很高的灵活性,通过一些提示工程,一个大型语言模型(如 GPT-4o)就能解决大量传统软件挑战。虽然构建以前不可能(或非常不可行)的新事物令人激动,但生成型 AI 的大部分影响将体现在以不同的方式构建我们今天已经能够构建的事物。

从特定问题的应用开发转向整合问题无关的通用生成型 AI,将改变主流的架构模式、组织技能要求以及开发成本的计算方式。这也是为什么每一个开发…

人工智能敲响课堂的门

原文:towardsdatascience.com/ai-knocking-on-the-classrooms-door-87db39d00b94?source=collection_archive---------2-----------------------#2024-05-22

关于我如何在大学讲座和评估中使用人工智能的反思

Dylan SeychellTowards Data Science Dylan Seychell

·发表于 Towards Data Science ·12 分钟阅读·2024 年 5 月 22 日

--

我想先声明一下:虽然我自 2011 年起就开始在大学讲课,但我并不是经过正式培训的教育工作者。我在本文中分享的是我与学生共同开发的个人观点和学习方法。这些观点不一定与我所在的马耳他大学的观点一致。

生成式人工智能要求教育工作者重新思考评估方式,并拥抱透明度,以实现以学生为中心、道德信息化的学习未来。

作者 x DALL-E 3

这是我在本文中结构化反思的方式:

  • 为什么我教书?

  • 我们是如何走到今天的?

  • 人工智能是教育的威胁吗?

  • 教育工作者能做些什么?

  • 那么评分作业呢?

  • 前进的道路?

为什么我教书?

我是一名人工智能科学家,教学是我学术角色的核心部分,我非常享受。课堂上,我认为我的职责是找到最有效的方式,将我的研究领域传达给我的学生。我的学生是我的同行,我们在一个快速发展的领域中一起学习,这一领域正在深刻影响人类。自 2015 年以来,我还一直领导马耳他谷歌开发者小组(GDG),这让我可以与不同专业的人员分享学习经验。

换句话说,我的责任有两方面:1) 研究学科内容和 2) 通过分享我的知识并与学生共同推动界限,促进学生的学习体验。

我们是如何走到今天的?

我预见到了 2022 年 12 月的事件。2020 年 5 月底,OpenAI 发布了 GPT-3,展示了通过一些简单的提示生成高质量文本的能力。这是我们第一次看到人工智能技术“写关于它自己的东西”,正如这篇文章报道的那样。然而,这一技术当时仅对那些具有较强技术背景的人可用和“可操作”。到了 2022 年夏季,OpenAI 发布了其文本到图像生成器 DALL-E 的测试版。长话短说:人工智能每个月都在变得更好、更加触手可及。我最近写了一篇关于应对这一快速变化的文章

ChatGPTGeminiClaude以及随后推出的所有生成式人工智能产品使这项变革性技术变得触手可及。

Google Trends 显示了人工智能的趋势,并且在 2022 年 12 月 ChatGPT 发布后,出现了明显的峰值。

生成式人工智能使我们能够探索无数话题,甚至实现更多目标。你需要为你的保险公司起草一封投诉邮件吗?你需要策划一场营销活动吗?你需要为你的新项目做一份 SWOT 分析吗?你需要为宠物起个名字吗?还有很多类似的需求。它就在那,迅速且自由地提供给我们,适用于我们想到的任何事情。

那么,为什么不帮助我们更好地理解一些事情呢?为什么不帮助学生以更高效、更有效的方式学习呢?为什么不帮助我们重新思考教学方式呢?

人工智能是教育的威胁吗?

简短回答不是长回答:它比看起来要复杂一些。

是教育技术(EdTech)还是不是?

本文并非讨论教育技术(EdTech)或电子学习的正式方面。这些术语通常指的是软件和硬件,或它们与教育理论的结合,以促进学习。这里有一些经过充分研究的工作,比如我的同事Matthew Montebello所著的书《AI 注入的电子学习》,更深入地探讨了这一领域。在本文中,我关注的是我在课堂上使用的现成聊天界面。

生成式人工智能

“生成式人工智能”这个术语并没有帮助。虽然它适用于大多数用例,但它在教育领域激起了负面联想。[这对于艺术领域也同样适用,值得写另一篇文章讨论。] 作为教育工作者,我们不希望学生为我们布置的任务生成答案并提交评分。这也是为什么在学习过程中,人工智能通常会引起负面的初步反应。

但这只是我们探讨这种强大技术如何积极改变学习体验——无论是学生还是教育工作者的体验——之前的看法。从我的经验来看,我在课堂上使用这些方法越多,我在学科领域的专业知识和理解就越广,同时也能找到更好的方式来沟通复杂的概念。

它是抄袭吗?

我注意到很多人将人工智能的使用与抄袭联系在一起。抄袭的定义是“未经承认地使用他人的工作,作为自己的,无论该工作是否已发布。”我不打算进入这个争论,但我可以坦率地说,我认为人工智能的使用与传统的抄袭之间存在明显的区别。

我们是否应通过软件来检测它?

让我们明确一点:如果学生或学术人员使用人工智能生成内容并将其提交为自己的作品,那是错误的。这是学术不端行为,最好不要将其归类为抄袭。

我们在消费任何作品时都期待真实性,无论是像这篇文章这样的随意作品,还是正式的学术论文。缺乏真实性是我们对抄袭和使用人工智能生成内容时感到不满的原因。

生成型人工智能之所以有效,是因为(除了其他原理之外)它基于概率性方法。这意味着每次输出都与之前的不同,即使使用相同的提示来创建。如果我抄袭他人的作品并将其作为自己的提交,那么我抄袭的部分是可比的,并且会被检测为抄袭。而使用生成型人工智能创作自己的作品则是另一种形式的不端行为。因为它是概率性的,所以检测它也是概率性的。

因此,我反对在教育环境中使用任何声称能够检测人工智能生成内容的工具。虽然它可以表明某人是否使用人工智能生成作品,但由于缺乏确定性,知道检测结果可能是误报时,我感到不安,不愿对学生采取行动,因为这可能影响他们的职业生涯。

你会通过抛硬币来决定学生的命运吗?(作者 x DALL-E 3)

如果仍然有疑虑,我希望分享这也是 OpenAI 的看法。2023 年 1 月(在发布 ChatGPT 不到一个月后),他们推出了一个人工智能文本分类器,声称能够预测一段文本是否由人工智能生成。猜猜发生了什么?他们在 2023 年 7 月关闭了这个工具,因为它“无法做出这种预测”。那么,为什么你要因为一个不可靠的概率性决策而危及学生的未来呢?

教育工作者可以对此做些什么?

使用人工智能的主要挑战之一是,如果学生依赖生成的答案,它可能限制批判性思维。还有一个合理的担忧是,如果人工智能的使用不均,可能会加剧教育不平等。

战斗、冻结还是逃跑?那只意味着我们在面对这项革命性技术时放弃了教育。这是一个机会,也是教育新时代的开始。

我在处理这方面的三条指导原则如下:

  1. 透明性与开放对话 — 教育、职场和社会都需要这些价值观。所以,让我们从使学生能够在教育环境中透明并开放地使用这些工具做起,这里应该始终是一个试验新思想的安全空间。

  2. 聚焦学习过程— 教育(几乎)一直关注最终的结果,无论是考试、作业还是论文。现在有机会将焦点转向帮助学生展示他们在理解材料和解决实际问题过程中思考的过程。

  3. 教育而非惩罚— 我们的任务是教育,而非惩罚,首先要保持积极的态度。如果我们保持透明,并为学生提供一个过程,让他们能够公开讨论使用 AI 的方式,那么我们就有无数机会为他们提供建设性的反馈,帮助他们正确使用这项技术,而无需惩罚。

AI 为我提供了一个更新教学方式的机会。这就是我如何充分利用这一机会:

  1. 个性化学习 — 要求学生以多种方式向 AI 系统提问一个话题,帮助他们更好地掌握该概念。

  2. 激发课堂讨论— 邀请学生讨论他们从 AI 系统中获得的结果,并主持讨论,为探索新话题开辟道路,同时纠正对该话题的任何误解。

  3. 给学生更多现实和现实生活中的挑战— 我利用这一点邀请更成熟的学生将课堂上涉及的主题应用到现实生活中的情景,或是当下时事中的话题。

  4. 帮助有学习困难的学生 — 如果你知道有些学生存在学习困难,AI 可以帮助你创建内容的不同版本,以便针对不同能力的学生提供个性化的帮助。

  5. 重构我的课程 — 每门课程结束后,我会收集学生的反馈,了解我可以改进的地方。然后,我将这些反馈与课程内容结合,和 AI 系统进行“对话”,帮助我反思并优化后续的授课内容,特别是在引入新主题时。

  6. 改善我的课程材料 — 我发现 AI 系统在改写特定话题并调整教学方式以适应不同背景的学生时非常有帮助。你还可以利用生成性 AI 建议多种传递相同内容或材料的方式。

那么评分作业呢?

这是我们无法忽视的关键问题。我们可能更倾向于在课堂上使用 AI 来提升体验,但那在评分作业或工作时呢?

当这项技术变得非常容易接触时,我反思了很多。我决定从整体上退一步,从第一性原理来审视这一问题。这些是我做出决策的基本原则:

  1. AI 已经发展了 80 年,它将长期存在

  2. 这些 AI 工具将会变得越来越好, 因为商业利益在其中。

  3. 今天的学生是明天的劳动力, 雇主将期望他们利用 AI 提高工作效率。

  4. 我希望我的学生能够聪明地使用不同的工具,并且在使用 AI 时对他们的领导负责

  5. 我希望在课堂上覆盖的主题太多了, 但是(直到现在)很难做到这一点。

前进的道路是清晰的:我的职责是设计能够激励学生的评估,确保他们理解内容,并为他们未来建设更好的明天做好准备。实际的前进方式是调整我的评估方式,以适应这一新现实,并激励学生以负责任的方式使用 AI。

评估类型

AI 的可用性挑战了评估的性质。有些问题和任务可以作为提示输入,学生可以得到一个出色的输出,并且无需深思熟虑地提交。虽然这种行为是不对的,不应该发生,但它也反映了我们给学生设置的评估问题。

考虑一个关于概念的论文,例如言论自由,学生需要就这个主题写 2000 字的文章。其过程通常是学生离开课堂,写论文,提交给讲师修改,讲师批改并给出成绩。这种方法无疑为我之前提到的困境打开了大门。

生成式 AI 日志

我们在员工身上看重什么?我认为责任感是最重要的,因为其他一切都会随之而来。基于这个思路,我决定为我的学生开发一个生成式 AI 日志,当他们完成我布置的作业时填写。这个日志会解释他们如何使用生成式 AI 以及其使用情况如何。作为回报,他们将获得分数(大约 10%的分数)。

关键的思考是这样的。我不介意我的员工使用 AI 提高生产力,但我需要了解他们是如何使用 AI 的。这将帮助我成为更好的导师并评估任务。学生是明天的员工,他们将工作在一个 AI 已经可以使用的环境中,且他们将被期望尽可能提高生产力。综合这些思考,我认为生成式 AI 应该在有责任感和透明度的前提下使用。基于这一点,我想出了这个日志的创意。这个 10 页的日志结构如下:

  1. 简介: 简要描述所使用的生成式 AI 模型(如 ChatGPT、Gemini、VS Co-Pilot 等)以及选择它们的理由。(最多 1 页)

  2. 伦理考量:讨论在项目中使用生成性人工智能的伦理方面。这应包括数据偏见、隐私问题以及良好学术行为的措施(最多 1 页)。

  3. 方法论:概述将生成性人工智能模型整合到工作中的方法和步骤。生成性人工智能是如何融入任务工作流程的?

  4. 提示和回应:列出与生成性人工智能模型一起使用的具体提示,这些提示有助于提升工作的质量。包括每个提示的生成回应,并解释它如何改善了你的项目。

  5. 改进和贡献:讨论生成性人工智能在具体领域提升交付成果的情况。这可以包括但不限于数据分析、伦理考量的制定、文献综述的增强或创意生成。

  6. 个人反思:反思在项目中使用生成性人工智能的个人经验。讨论学到了什么,什么让你感到惊讶,以及如果有的话,使用人工智能进行学术项目的观点如何发生了变化。

  7. 参考文献和使用的资源列表

这不是一个最终的结论。这是一个持续发展的工作,我不断更新它,以反映学生的需求和生成性人工智能发展的背景。

前进的方向?

这不是一段有明确目的地的旅程。可能的情况是,教育(和职场)将不得不与生成性人工智能的进化一同发展。

在本文开头,我分享了将我的学生视为研究旅程中同伴的观点。我将这一观点与前进的方向联系起来。在 2023/24 学年第一学期结束时,我向 60 名学生分发了一份匿名问卷。在本文中,我将分享两个问题的回答,问学生们在使用生成性人工智能后对前进的方向有何看法,如上所述。

当被问及他们如何看待生成性人工智能在教育中的未来时,56%的人认为它将发挥重要作用,但不是核心作用,12%的人认为它将仅发挥适度作用。30%的人认为它将成为教育的关键部分。这具有重要意义,因为它表明学生们感受到了在课堂上使用这项技术的价值,并认为它应该是对人类教育者的补充,而非替代。

我请我的学生就是否继续在课堂上使用生成性人工智能提出意见。92%的人认为我应该继续使用……我会继续的。8%的人不确定,这同样具有意义。最重要的是,这是一个正在进行中的工作,应该做更多的工作来提供有意义的教育体验……但这并不意味着我们不应该从今天开始尝试。

这是一瞥未来的景象。如果你看待它为一个机会,教育和社会需要你。

这项技术将每周不断改进,并且它将长期存在。在本文发布时,OpenAI 发布了 GPT-4o 模型Google 在 Google IO 发布了无数 AI 产品和功能。我们才刚刚触及这些可能性的表面。

未来不是要害怕课堂上的人工智能,而是要将其作为人类最新的发明,来处理知识和信息,从而赋能教育系统中的所有利益相关者。

Dylan Seychell 博士是马耳他大学人工智能系的常驻学者,专攻计算机视觉和应用机器学习。拥有学术和行业背景,他获得了计算机视觉博士学位,并在国际同行评审的会议、期刊和书籍中广泛发表了有关人工智能的研究成果。

在学术领域之外,他通过自己的企业积极应用专业知识,专注于通过技术和文化遗产创新提升旅游体验。他领导着马耳他的 Google 开发者小组(GDG),是马耳他数字创新局认证的技术专家,并且是马耳他商会旅游运营商业务委员会的成员。

AI 与机器学习在商业中的应用

原文:towardsdatascience.com/ai-machine-learning-for-business-24bcf9754038?source=collection_archive---------5-----------------------#2024-01-26

非技术性的介绍

Shaw TalebiTowards Data Science Shaw Talebi

·发表于 Towards Data Science ·阅读时长 9 分钟·2024 年 1 月 26 日

--

图片来源:Milad FakurianUnsplash

近年来,人工智能创新的步伐不断加快。尽管这带来了令人惊叹的新工具和技术,但我们(即专业人士、企业家和商业运营者)如何利用这些创新推动商业价值,仍然不完全清楚。

在这篇文章中,我提供了一个非技术性的人工智能与机器学习的介绍,并分享了它们如何融入我们的商业模式。我的目标是帮助那些来自非 AI/ML 背景的人,通过简单的定义来理解这些技术,并分享如何开始在实际中使用它们的技巧。

什么是人工智能?

当你想到人工智能时,你可能会想到 ChatGPT、AI 生成的艺术作品,或者像《终结者》那样的科幻场景。但让我们退一步,问一个基本问题,“什么是人工智能?

人工智能人工智能(artificial intelligence) 的缩写——这可能并没有给我们带来太多信息,因为这两个词中的一个是有问题的。

第一个词,人工(artificial),并不是问题所在。它只是意味着某物 由人类制造

然而,第二个词,智能(intelligence),定义并不明确(即使在人工智能研究者中也存在争议[1])。尽管如此,我喜欢使用的一个定义,并且在商业语境中最为相关的定义是:智能 = 解决问题和做出决策的能力

因此,按照这种关于智能的操作性定义,人工智能 只是 计算机解决问题和做出决策的能力

智能的实践

为了更好地理解我们所说的“智能”,让我们看看它如何在实际中运作。

假设你在决定是否在星期六去游泳池旁度过,还是呆在家里看那部新的 Netflix 剧集。如果你透过窗户看到下面的场景,你可能会决定待在室内。那是因为乌云密布的天空是一个很好的指示,表明今天的天气可能不好。

游泳池日还是 Netflix 日?图片使用 Canva 生成 [2]。

另一个例子是,考虑下面的图表,销售数据起伏不定,但在 11 月达到了顶峰。如果有人问,“是什么导致了这一高峰?”我们可能会合理地回答,这是因为“黑色星期五”,一年中最大的零售购物日之一。

什么导致了高峰?图片由作者提供。

最后一个例子,我们来看下面的文字对话。如果我们处于蓝色发信人的位置,真的相信对方没问题吗?根据他们的用词,可能不太相信。

这可能会让我们试图通过说“我可能误解了这个情况,你能帮我更好地理解吗?”或“我在乎你的感受,想确保我们对最终决定达成一致。”来化解这种紧张。

他们真的没问题吗?图片由作者提供。

上述每个场景都让我们以某种方式运用我们的智力来解决问题或做出决策。虽然每个场景都与其他场景非常不同,但它们有一个共同点——它们都需要了解世界如何运作

换句话说,我们需要知道诸如乌云预示着坏天气、11 月人们购物很多,以及人们并不总是会承认自己生气等事情。我们通过我们对世界的模型了解这些事情。

模型

由于世界庞大且复杂,我们必须通过模型来理解它。模型能够放入我们大脑中的简化现实世界事物

我们如何建立世界模型。图片由作者提供。

模型的一个关键特征是它们允许我们做出预测。例如,当我们看到乌云密布时,这些信息会被我们对天气运作方式的心理模型处理,并让我们预测接下来会下雨。

我们如何使用(心理)模型。图片由作者提供。

当然,模型不仅限于我们脑海中的那些。我们还可以将模型编程到计算机中事实上几乎所有的天气预报都是通过计算机模型生成的)。

我们如何使用(计算机)模型。图片由作者提供。

2 种模型

关于模型(无论是心理模型还是计算机模型),一个自然的问题是,它们来自哪里? 对此,我喜欢将模型分为两类:原理驱动型数据驱动型

基于原则的模型基于一组规则的。你可能在教科书中读到这些规则,或者从你奶奶那里学到。例如,“如果有乌云,随后会下雨。”

数据驱动模型,另一方面,是基于过去的观察。其工作原理是通过比较新信息与过去见过的内容的相似性,例如,“上次天空看起来像这样时,下雨了。”

两种类型的模型。图源:作者。

同样,这些模型并不局限于我们的大脑中。我们也可以将它们编程到计算机中。

对于基于原则的模型,我们明确告诉计算机如何将输入(例如乌云)转换为输出(例如雨)。然而,对于缺乏规则的情况,我们可以转向生成数据驱动的模型的技术——这就是机器学习的作用。

机器学习

虽然现在机器学习可能带有一些神秘感,但它其实是一个简单的概念。机器学习(ML)计算机通过示例进行学习的能力[3]。

它的工作原理是,不是明确告诉计算机如何将输入映射到输出,而是计算机通过查看许多示例来学习这种关系。

这是一项强大的技术,因为它允许我们即使缺乏对基础事物的理论理解,仍然能够构建模型,这在许多领域中都非常有用,例如销售、营销、金融、天气、消费者行为等。

机器学习可以分为两个步骤。在第一步中,我们将输入-输出对(即预测因子和目标)传递给机器学习算法,以获取一个机器学习模型。然后,手中有了模型后,我们可以将新数据输入其中,以生成预测。下面的图示说明了这一过程。

机器学习的两步过程。图源:作者。

我们如何使用它?

到目前为止,我们已经讨论了三个关键术语。首先,我们讨论了人工智能(AI)并将其定义为计算机解决问题和做出决策的能力。接着,我们介绍了模型,它们是智能的关键部分,使我们能够对现实世界做出预测。最后,我们分解了机器学习,它是一种我们可以开发世界数据驱动模型的方法。

虽然这些都是强大的概念,但如何在商业环境中使用它们来创造价值还不完全清楚。在这里,我将分享一个生动的示例,展示人工智能在实践中的应用,(希望)能激发你如何开始在工作中使用人工智能的想法。

示例:信用决策

人工智能的一个热门应用是用它来做信用决策,即批准或拒绝贷款。让我们看看这是什么样的。

传统方法

传统的方法是,当某人向银行(或其他金融服务提供商)提交贷款申请时,承保人会审查该申请并决定是否批准贷款。

传统的信用决策方式。图片由作者提供。

然而,既然我们已经了解了人工智能和机器学习,我们可能会问:“难道我们不能用 AI 承销员取代人工承销员吗?

我们可能期望 AI 如何做出信用决策。图片由作者提供。

这个问题的答案是“是的……但可能比你想象的更复杂”。

AI 方式

在实践中,“AI 承销员”看起来更像下面的图示,而不是我们上面看到的简单图像。

使用 AI 做出信用决策的现实。图片由作者提供。

请注意,我们不会简单地将贷款申请提交到机器学习模型中然后结束。而是,申请将通过多个业务和 IT 流程,才会涉及任何机器学习(即信用风险模型)。

这是人工智能在实践中的一个关键方面。也就是说,它通常是多个流程和技术共同协作来解决特定问题的网络

放下那把锤子

这种 AI 的现实突显了一个最被低估的事实——它并不容易。更糟糕的是,许多人陷入了一个常见的陷阱,使得过程变得比实际需要的更困难——他们从技术入手,而不是从问题入手

我的旧数据科学副总裁总是说,“当你有一把锤子时,什么都像钉子。”这句话抓住了我们(即人类)在构建 AI 解决方案时的偏见。也就是说,我们倾向于采用技术优先而非问题优先的方法

这就是为什么我们不能拥有美好事物的原因。图片由作者提供。

技术优先的方法存在两个主要问题首先,成功的概率较低(特别是如果你不是专家)。其次,可选择的技术太多了。

一种(更)好的方法是从问题开始,因为在商业中,解决问题 = 创造价值。而且,由于我们不会因为用 AI 解决问题而获得额外的奖励分,因此我们应当寻求最便宜、最快速、最可靠的解决方案。

从简单、快速、容易开始

正如我们在信用决策的例子中所看到的,实际的 AI 解决方案可能比我们想象的要复杂得多。其(反直觉的)秘密在于,它是经过长时间许多小而简单的步骤所积累的结果。换句话说,复杂性背后的秘密是迭代。

一旦你确定了要解决的问题,我建议采用简单、快速、容易的方法,这三个词都有其重要意义。

简单 — 你应该从简单的方式开始,因为复杂性是昂贵且脆弱的(它失败的方式远比成功的方式多)。这意味着如果有一个更简单的解决方案,可以放弃 AI 解决方案。

快速—— 接下来,你需要优先考虑快速的解决方案,因为(再次强调)复杂性的秘诀在于迭代。如果你花费 6 个月时间才能构建第一个解决方案,你将无法足够快速地进行迭代,从而开发出有意义的东西(更不用说,在当前创新的速度下,你的解决方案可能会变得过时)。

简单—— 最后,你希望它是简单的。换句话说,不要让人们难以使用该解决方案。这有两个主要原因。第一,如果没有人使用它,你会错过重要的反馈。第二,如果它很难,那可能意味着它不适合你现有的业务流程。

接下来做什么?

尽管这只是一个高层次的介绍,但我希望它能为你提供一些关于 AI 以及如何开始实际应用它的清晰思路。重要的是要记住,AI(在实践中)通常是多个系统和技术的集合,它们协同工作以解决现实世界中的问题。开发这些解决方案的两个关键要素是以问题为先迭代

这是关于在商业中使用 AI 和 ML 的系列文章中的第一篇。在该系列的未来文章中,我将详细讲解数据科学的项目管理以及 ML 模型开发的关键考虑因素。

Shaw Talebi

Shaw Talebi

商业中的 AI

查看列表4 篇故事

资源

联系我的网站 | 预约通话 | 问我任何问题

社交媒体YouTube 🎥 | LinkedIn | Twitter

支持请我喝杯咖啡 ☕️

[## 获取我写的每一篇新文章的免费访问权限

获取我写的每一篇新文章的免费访问权限 P.S. 我不会与任何人分享你的电子邮件。通过注册,你将创建一个…

shawhin.medium.com](https://shawhin.medium.com/subscribe?source=post_page-----24bcf9754038--------------------------------)

[1] arXiv:2303.12712 [cs.CL]

[2] 来自 Canva 的 AI 生成图像

[3] 皇家学会. (2017). (复印). 机器学习:通过实例学习的计算机的力量与前景。于 2024 年 1 月 20 日获取,来源:royalsociety.org/~/media/policy/projects/machine-learning/publications/machine-learning-report.pdf.

人工智能映射:使用神经网络识别房屋号码

原文:towardsdatascience.com/ai-mapping-using-neural-networks-to-identify-house-numbers-12f194a95d75?source=collection_archive---------12-----------------------#2024-04-16

比较人工神经网络和卷积神经网络在分类 Google 街景房屋号码中的表现

John LenehanTowards Data Science John Lenehan

·发表于面向数据科学 ·阅读时间 7 分钟·2024 年 4 月 16 日

--

图片来自christian kochUnsplash上的分享

介绍

深度学习中最有趣的任务之一是识别自然场景中的物体。通过机器学习算法解读视觉数据具有重要的实际价值,这在广泛的应用中都有体现(从自动驾驶车辆到人脸识别)。一个这样的例子是在地图上根据房屋号码定位房屋。

Google 街景房屋号码数据集包含超过 60 万个从街景照片中提取的标记数字——它是最受欢迎的图像识别数据集之一。谷歌利用该数据集中的图像,通过神经网络自动提取地址号码,从而提升地图准确性。这些模型的输出与已知街道地址相结合,有助于在 Google Maps 中精准定位地址。

图片来自Alex SkobeUnsplash上的分享

AI 数学:深度学习中的偏差-方差权衡

原文:towardsdatascience.com/ai-math-the-bias-variance-trade-off-in-deep-learning-e444f80053dd?source=collection_archive---------2-----------------------#2024-11-29

从经典统计学到深度学习的细微差别的视觉之旅

Tarik DzekmanTowards Data Science Tarik Dzekman

·发表于Towards Data Science ·阅读时长 47 分钟·2024 年 11 月 29 日

--

来源:除非另有说明,所有图片均为作者提供。

在深度学习中,偏差-方差权衡并非简单明了,且通常可能不是我们应该关注的关键问题。要理解原因,我们需要通过推理统计学、经典统计学习方法以及机器学习的鲁棒性进行一次回顾。我们将在文章的最后讨论过度参数化和双重下降现象。

建议背景:概率论、随机变量、统计学、线性代数、微积分、机器学习、深度学习。

推理统计中的偏差与方差

注意:在本节中,我们将为了视觉直觉而简化一些数学内容。鉴于我关注的是深度学习,推理统计的细节会让已经很长的文章更加冗长。

假设你穿越回二战时期,成为盟军指挥部的一名统计学家。一名情报官员告诉你以下信息:

  1. 德国人在他们的坦克上刻上连续的序列号。因此,序列号为 115 的坦克意味着它是第 115 辆生产的坦克。迄今为止,德国生产了一个未知数量的坦克(N)。

  2. 当盟军摧毁一辆坦克时,我们可以在其上找到一个序列号。坦克的“可摧毁性”与其序列号无关。

  3. 我们有一个序列号样本(大小为 k),X = (x₁, x₂, … xₖ)

  4. 我们需要利用这个样本来创建一个估计量 *N**。

这就是所谓的德国坦克问题。本质上:

给定一个生成顺序序列号的制造过程,如何从一个随机样本中估计总的生产量?

探索一个估计量

我们将首先查看一个可能的估计量,并探索其数学性质:

  • N**是我们对N*的估计量

  • X是大小为k的随机样本

  • m=max(X)是样本中观察到的最大序列号

我们可以使用蒙特卡罗模拟来计算*N**的期望性能:

  • 从对数正态分布中抽取N(均值=200,大方差)

  • 从泊松分布中抽取kλ=20

  • 对于 10,000 次迭代,从[1..N]中抽取样本 k 值并计算*N**

这模拟了一个可能的世界范围,其中样本数据被收集。下图显示了对于不同Nk和*N**值的模拟的 100 次迭代。

无偏估计量

我们可以看到,这些估计通常非常准确——有时会高估真实值,有时会低估真实值。我们可以绘制所有 1 万次迭代的误差,并查看它们是如何分布的:

该图表明N**的平均误差为零。这是因为这是一个著名的无偏估计量。这意味着平均而言,误差会相互抵消,N**在期望上接近N。即:在所有可能的世界中平均。

正式地,N的估计量的偏差表示为:

偏差是估计量在固定的Nk下,对所有可能的样本的期望(有符号)误差。如果期望误差为 0,则表示估计量是无偏的。通常,偏差表示为仅对X的期望,而不是X|N,k。我使用了额外的下标仅仅是为了强调一个观点。

请注意,这有时写作:

在这种情况下,我们可以证明额外的期望是不必要的。N是一个未知但具体的值,*N**的期望值也是如此。常数的期望值就是常数,因此我们可以省略额外的符号。

估计量的方差

方差量化了在不同可能的世界中估计量如何变化。我们的误差图显示估计值集中在 0 周围,稍有偏斜,这是由于对Nk的先验假设。如果我们观察k/N的比值,我们可以看到随着样本量越来越大,估计量的表现如何:

直观的结果是,对于一个无偏估计量,收集更大的样本会导致更准确的结果。*N**的真实方差是:

标准差(N/k)可以看作是一个随机样本中元素之间的平均间隔。例如:如果真实值是N=200,且样本大小是k=10,那么样本中值之间的平均间隔是20。因此,我们期望大多数估计值落在200±40的范围内。

可以证明这是任何无偏估计量能够达到的最小方差。在频率派统计学中,这被称为均匀最小方差无偏估计量 (UMVUE)。换句话说:要想实现更低的方差,你需要一个有偏估计量。

从形式上讲,N的估计量的方差可以表示为:

请注意,方差是围绕估计值的期望,而不是围绕真实值的期望。如果我们有一个有偏估计量,我们将评估围绕该有偏估计的分布。

测试你的理解:你明白为什么我们需要外部项的期望吗?N是一个随机变量,因此我们需要对所有可能的X进行期望,以获得一个具体值。

充分信息

你可能注意到我们的估计量有一个问题:它似乎丢弃了样本中的大量信息。如果我们的样本有k个值,为什么我们的估计量只使用一个值呢?

首先,给出一些简要定义:

  • “统计量”是数据的一个函数(通常是样本的函数)。

  • “充分统计量”是指包含我们尝试估计的总体参数的最大“信息”的统计量。

一旦我们知道最大值和样本大小k,就可以证明样本中没有额外的信息。原因与给定样本X时,N似然函数有关。

似然函数

考虑所有可能的k大小子集* [1..N]。对于任何给定的样本,N的唯一可能值范围是[max(X), ∞]。即,如果N<max(X),则不可能得到包含max(X)的样本。获得任意一个k大小样本的概率基于从N个可能值中选择一个大小为k的集合的方式。下方展示了似然函数。请注意,固定样本的似然函数仅关注km=max(X)*。

似然函数 ℒ(θ;x) 衡量在不同的θ(例如N)值下,观察值x的可能性。我们可以用它来找到一个θ值,该值使得观察到x的概率最大,而不会告诉我们关于θ本身的概率信息。

最大似然

假设k=5m=60,那么N ≥ 60。最大似然发生在N=m=60。尽管大多数N值不太可能,但似然函数识别出N=60是此样本最可能的值。

首先,注意到所有的N值都是非常不太可能的。然后,记住,对于固定的(m, k)值,似然函数告诉我们在每个可能的N值下,看到该m值的概率。仅仅因为m=60N=60时最可能,并不意味着它是一个好的估计!

最可能的估计并不一定是最好的。

费舍尔信息

费舍尔信息量化了样本信息量。如果许多N的值很可能出现,则信息量较低;如果存在一个围绕真实值的尖锐似然峰值,则信息量较高。作为一个粗略的指南,费舍尔信息告诉我们,从随机样本中我们可能知道多少关于真实分布的信息。

一个充分统计量

“充分统计量”包含了关于所讨论参数的所有信息。我在这里不会讲证明,但一个统计量是充分的,如果它是最大似然估计(MLE)。如果 MLE 是有偏的,我们可以使用“偏差修正”来产生更好的估计,但我们无法找到提供更多信息的其他统计量

一个直观的解释

并非所有的样本数据都提供有用的信息。具体到德国坦克问题,我们可以看到:

  • 样本概率取决于kmax⁡(X)

  • 值得注意的是,N接近max⁡(X)的值,更可能生成包含max(X)的样本。

  • 所有包含max⁡(X)k-大小样本是等可能的

  • 所以,样本中关于真实N值的更多信息,除了知道kmax(X),并不存在。

一个有偏估计量

使用max(X)=m作为估计量几乎总是会低估N,因为在一个样本中得到N的概率是1/(N choose k)。另一方面,如果我们确实得到一个包含N的样本,我们原始的估计量N可能会给出一个大的过高估计。假设k=1,且我们的样本恰好包含N=1000。那么我们的N估计值2m-1=1999将远远过大。

希望显而易见的是,这个理由非常糟糕,不能将max(X)作为N的估计量。为了验证这一点,我们来比较两个估计量的均方误差(MSE),看看它们的表现如何:

注意到max(X)估计量表现更差。请注意,几乎所有的误差都归因于其偏差。如果我们绘制估计值的分布,我们可以看到max(X)始终产生一个较窄范围的估计。

我将跳过证明,我们依赖于可视化来看,max(X)的方差显著低于N。只需要记住,估计量方差的正确定义是围绕期望估计值的预期离散度。

偏差-方差分解

按惯例,我们试图最小化的总误差是均方误差(MSE)。如果你感兴趣,你可以阅读这篇关于为什么使用 MSE的讨论。这次我会省略下标,但记住我们是在对所有可能的样本计算期望值:

这可以被分解为一个偏差²项和一个方差项。这个推导是理解这一点很有用的。我们从引入-E[N]+E[N]开始,然后对项进行分组,并展开二次项:

最大的混淆可能出现在倒数第二行:

  • 如果我们忽略冗余的期望值,那么左侧项就是偏差²。

  • 展开并应用期望运算符后,中间项会变成 0。

  • 正确的项只是方差,取决于在平方结果之前减去哪个项。

更一般的推导可以在Wikipedia 关于偏差-方差权衡的文章中找到。

总的预期误差是估计量的偏差误差和方差的组合。这里有一个微妙的问题:如果模型有偏差,为什么高方差有时能得到一个准确的答案?为什么总的预期误差是偏差²和方差的总和,而不是考虑到这一点的其他函数?

上面的分解解释了它是如何在数学上发生的,但可能没有直观地说明。为了建立直觉,考虑一下平方运算对高度不准确估计的影响。同时也要考虑到,偏差²本身不足以解释所有预期的平方误差。

一个最优的估计量?

我们已经展示了我们的估计量的预期误差。平均而言,给定一个随机样本,我们的估计量与生成该样本的真实值之间的差距有多大?一个始终偏差较大但预测范围较窄的估计量,可能比一个始终准确但预测范围更广的估计量要好。

我们能否在德国坦克问题中找到一个平衡点,在那里我们通过折衷偏差和方差来做出更好的估计?忽略常数项(+ C),这样一个函数大概是这样的:

它将位于g(k)=1g(k)=(1+1/k)之间。你能算出为什么吗?使用1 * m 是最大似然估计(MLE),它有偏差但方差较小。使用(1+1/k) 就是* N,没有常数项。我们知道,*N 是一个无偏估计量(UMVUE),但其方差比m大。所以在最大似然估计(MLE)和无偏最小方差估计量(UMVUE)之间,我们可以找到一个“最优”的估计量。

结果表明,在不知道N的真实值的情况下,我们无法找到最优的* g(k)* 函数,而N正是我们试图估计的数字!

维基百科页面上描述了贝叶斯推断技术,这需要对N设定先验分布。这个先验分布是你在分析时选择的内容。我们可以用它至少通过我们的世界知识设定合理的边界。例如,我们知道他们至少有m辆坦克,可能不超过 100,000 辆。但先验分布必须是主观的。在m到 100,000 区间内,分布应该是什么样的?应该是均匀分布吗?贝叶斯推断是一个引人入胜的话题,但我在这里就不讨论了。

最后,考虑到具有最低误差的估计量是有偏的。这是我们第一次发现,偏差-方差权衡并不总是最重要的考虑因素。为了推断的目的,我们可能需要从统计风险的角度来考虑问题,这可能会优先考虑无偏估计量而不是更精确的估计量。

盟军表现如何?

实际上,盟军确实使用了这里描述的技术,只是他们试图确定德国坦克的月生产量。当然,他们没有 Python 或运行蒙特卡洛模拟的能力。我们来看一下本文中使用的估计量与传统情报收集方法(即间谍活动)相比的表现:

| Month       | N*    | Spying | German records |
|-------------|-------|--------|----------------|
| June 1940   | 169   | 1,000  | 122            |
| June 1941   | 244   | 1,550  | 271            |
| August 1942 | 327   | 1,550  | 342            |

Source: Wikipedia - The German Tank Problem

我们可以看到,统计估计的表现很好,且明显比间谍活动所做的估计更为精确。

反思

德国坦克问题是一个棘手的例子,我们跳过了很多对统计学家来说重要的数学细节。但我们介绍了一些关键概念:

  • 估计量的均方误差(MSE)可以分解为偏差和方差。

  • 偏差表示估计量的期望(带符号)误差,取所有可能样本(即所有可能世界)的平均值。

  • 方差表示所有可能样本(即所有可能世界)中估计值的期望分布。

  • 最好的估计量(即具有最低均方误差的估计量)很可能是有偏的。我们通过较低的方差来抵消偏差带来的误差,这意味着尽管估计量在期望上是有偏的,但估计值更有可能接近真实值。

  • 总体参数的似然性关注的是该参数的哪些值使得样本最为可能。这与总体参数的概率无关。

  • 最大似然估计(MLE)是样本的一个函数,用于识别最可能生成该样本的总体参数。

  • 最大似然估计(MLE)不一定是最佳估计量。我们非常明显地看到,最可能的值可能与生成样本的真实值相距甚远。

  • 费舍尔信息量是样本中包含的关于参数的信息量,大致通过似然图在真实值周围的曲率来衡量。

广义线性模型

从这里开始,我将使用在论文《预测、估计与归因》中描述的一个区分:

  • 预测涉及从样本数据中构建的预测模型的经验精度。

  • 估计涉及估计生成样本数据的分布的参数。

此外,我们还将考虑以下概念,这些概念在书籍《统计学习的元素》中有更详细的描述:

  • 一个统计过程创建了一个联合概率分布 f(X,Y),其中粗体 XY 表示向量而不是标量。

  • 训练数据 D 是从联合分布 f(X,Y) 中抽取的样本,包含形如 (x,y) 的元组。

  • 一个预测模型 h(x;D) 在数据集 D 上训练,并根据观测值 x∈X 对目标变量 y∈Y 进行预测。它可以写作 h(x;D)=E[Y | x∈X]

  • 一个损失函数 ℓ(y, h(x;D)) 计算模型在预测某个特定元组 (x,y) 的真实值 y 时的误差。对于回归问题,这通常是均方误差(MSE)。

此外,我引入了以下特定于本文的符号:

  • 潜在变量 Z 是联合分布 f(X,Y,Z) 的一部分,但在训练数据 D 中从未观察到。因此,即使 Z 是完整分布的一部分,观察结果只能采取 (x,y) 的形式。

  • 随机变量 W 代表内生抽样偏差。这意味着某些 (x,y) 组合可能较为稀疏,不太可能出现在我们的训练数据 D 中。这与外生抽样偏差相对立,后者指的是我们使用的抽样程序意味着并非所有观察结果都与 f(X,Y) 独立同分布(iid)。你可以通过阅读我的文章《为何缩放有效》了解更多关于抽样偏差的影响。

示例问题 – 房价

我们将生成一个合成数据集,使用房屋的面积(平方米)来预测销售价值。这个看似简单的问题能够教会我们很多关于模型工作原理的知识。以下是一些附加的复杂性:

  1. 有一个潜在变量影响着售价:房子距离海滩有多远?也许靠近海滩的房子更贵,但它们也更有可能只有 2-3 间卧室。

  2. 任何训练样本 D 都存在内生偏差,因为有很少的小房子(1 卧室)和特别大的房子(4 个以上卧室),所以它们不太可能被挂牌出售。

在潜在变量和样本偏差之间,我们有在现实世界数据集中存在的复杂性。我们设想一个函数,它确定性地根据某些属性计算出售价格:

f(x,z)=y,其中 x=面积,z=距离海滩的距离,y*=售价

大小、与海滩的距离和价格之间的关系可以通过这个表面图捕捉到:

现在考虑你可能有两栋大小相同且与海滩距离相同的房屋,但它们的售价不同。这意味着我们变量之间的关系并非确定性。对于每一种(大小、距离、价格)组合,我们在训练数据中都有一定的概率密度来观察到具有这些值的房屋。这由联合概率密度函数 f(X,Y,Z) 给出。为了可视化这个联合密度,我们使用配对图:

如果我们唯一观察到的变量是房屋大小,那么与价格的关系并不简单。例如,假设我们取某一特定大小房屋的平均海滩距离。在这种情况下,这是一个难以计算的期望值。相反,我们可以使用模拟并应用一些平滑处理来逼近这种关系:

对于特别大的房屋,距离的影响会加剧。因此,靠近海滩的大房子比远离海滩的同等大小的房子要贵得多。两者都是昂贵的,但高端市场的方差差异显著。这将使得在尾部预测关系的真实形态变得困难。

此外,我们必须考虑样本中的内生偏差。被出售的概率(W)受到所有属性的影响,我们可以通过这个配对图展示这一点:

我们如何看待这个新属性(W)呢?较小/较大的房屋建造得较少,因此出售的数量也较少。实际上,是否将房产挂牌出售受到许多因素的影响,包括人们的出售意愿。这种内生偏差通过使某些组合不太可能出现来影响我们的概率密度函数 f(X, Z, Y),但它不会影响变量之间的关系 f(x,z)=y*。

我们调整配对图,展示了在考虑到某个特定房屋出现在市场上的内生偏差后,变量之间的更新关系。

注意到,房屋大小和价格之间的表观关系发生了轻微但可观察到的变化。

我们的模型捕捉到了什么?

让我们再看一下这个图表,它直接展示了价格和房屋大小之间的关系。

当我们分析一个模型的偏差/方差时,我们是在分析与这个函数的误差吗?不,我们不是。我们正在建立一个生成数据的统计过程模型——这个过程包括内生偏差。这意味着期望误差是基于从我们分布中所有可能样本的期望值。

换句话说:回归模型的偏差-方差权衡涉及的是该模型在所有可能世界中的期望误差。由于期望值是通过特定值出现的概率加权的,因此它会受到内生抽样偏差的影响。

令人奇怪的是,一栋房子被售出的概率居然会影响我们对房屋大小与售价之间关系的计算。然而,这个计算恰恰是偏差-方差权衡的核心。

回归误差分解

在德国坦克问题中,我们的样本概率是基于我们试图预测的值f(X|N)。在回归分析中,预测变量和目标值之间存在联合概率分布f(X, Y)。这意味着变量之间的关系有某种固有的变异性,这是无法被解释的。实际上,可能还有我们没有考虑到的潜在变量,但这是另一个话题。这种变异性导致了不可约的误差项,这就是为什么我们将其描述为在给定观察值x的情况下预测y期望值

请注意,这个不可约误差有时被称为“偶然性不确定性”(aleatoric uncertainty)。这与由于知识不足引起的“认识论不确定性”(epistemic uncertainty)相对立。一个未充分指定的模型可能导致认识论不确定性,但即使是完美的模型也必须面对偶然性不确定性。

这种新结构意味着,期望的均方误差(MSE)被分解为偏差、方差和一个不可约的误差项:

在这个分解中,我再次显示了期望的下标,以清楚地表明每个期望是基于什么条件的。新项(h-bar)是我们模型的期望值,经过所有可能的数据集的平均。这可以想象为在不同的世界中收集训练数据集,并创建一个模型集成,平均所有世界中的预测。

我们模型的期望误差需要是一个关于以下内容的积分:

  1. 所有可能的数据集(D),我们可以用来训练我们的模型(h

  2. 所有可能的x ∈ X值(按其边际概率加权)

  3. 所有可能的y ∈ Y值(以类似的权重加权)

有趣的是,这也是在固定大小训练集上的期望——样本大小可能依赖于变量的事实没有体现在这个分解中。

更重要的是,这个积分对于我们的问题是完全不可解的。实际上,计算期望误差通常是不可解的,对于非平凡问题尤为如此。即使我们知道用来生成这些合成数据的真实过程,这也依然成立。相反,我们将通过不同样本运行一些模拟,并通过平均误差来查看不同模型的表现。

模型复杂度

如果你对偏差-方差权衡有所了解,你可能知道偏差来源于“欠拟合”,而方差来源于“过拟合”。为什么一个过拟合的模型应该具有低偏差,或者一个欠拟合的模型应该具有低方差,这并不显而易见。这些术语通常与模型复杂性相关,但具体意味着什么呢?

这里有 6 种可能的世界,其中 35 套房子被拿出来销售。在每种情况下,我们使用多项式回归拟合来自[x⁰…x⁵]的项,并将预测的多项式与该大小的真实期望价格进行比较。请注意,不同的训练样本会导致截然不同的多项式预测:

但请记住——在偏差-方差权衡中,我们并不是在将我们的模型与真实关系进行评估。这个真实关系忽略了内生性采样偏差。相反,我们可以根据W的影响调整“真实”关系,来考虑被售出的概率。现在我们可以看到,与调整后的真实关系更接近的预测:

我们可以通过模拟 1,000 个可能的世界来找到预测的期望值。这是根据房子大小计算的每个多项式度数的期望预测:

请注意,这些模型在低端表现特别差。这完全是由于内生性采样偏差,因为我们不太可能看到许多特别小的房子待售。还要注意,这些模型在特别大的房子上也往往表现不佳,这既是内生性采样偏差的结果,也是潜在变量的影响。

现在我们引入模型函数h并增加一个额外的项λ,它代表了用于某一类模型的超参数。我们不再使用多项式的度数,而是让λ代表所使用的多项式项数的子集。对于我们的仿真,我们将对最多 5 项、多项式度数为 10 的所有组合进行穷举检查,并选择具有最佳训练误差的组合。理想情况下,这应该通过交叉验证来完成,但我们将跳过这一部分,因为它在深度学习中并不是一种有效的技术。同时,请注意,在 5 项和 1000 次仿真的情况下,穷举搜索已经相当慢了。

接下来我们引入一个函数g(λ)=c,它表示基于所选超参数的模型“复杂性”。在这种情况下,g只是恒等函数,复杂性完全关注于所使用的多项式项的子集。

固定模型架构下,复杂性变化的期望误差由以下公式给出:

现在,我们不再通过多项式度数计算期望预测,而是使用子集选择的大小。对 1,000 次仿真结果进行平均后,我们得到以下预测:

此外,我们可以绘制总预期误差(按观察到该大小房屋的概率加权)并将误差分解为偏差和方差项:

再次提醒,要获得预期误差,我们是对所有可能的世界进行平均。我们可以看到:

  • 偏差²随着模型复杂度的增加而减少。

  • 方差随着模型复杂度的增加而增加。

  • 总误差先下降,达到最小值,然后上升。

  • 在这个问题中,总误差也强烈受到不可约误差的影响。

利用一些假设,我们可以识别任何模型h的预期误差的一些属性。核心假设是:

  • 在低复杂度时,总误差由偏差主导,而在高复杂度时,总误差由方差主导。在最小复杂度时偏差≫方差,在高复杂度时方差≫偏差。

  • 作为复杂度的函数,偏差是单调递减的,方差是单调递增的。

  • 复杂度函数g是可微的。

基于这些假设,我们可以预期大多数模型的表现类似于上述图形。首先,总误差下降到某个最优点,然后随着复杂度的增加,方差增大,总误差开始上升。为了找到最优复杂度,我们首先对误差分解函数关于复杂度求偏导:

拐点发生在偏导数为 0 时。

在最优点,偏差平方的导数是方差的负值。没有进一步的假设,这实际上就是我们能说的关于最优误差的一切。例如,以下是一些随机的偏差和方差函数,恰好满足列出的假设。它们的导数互为倒数的点就是总误差最小化的点:

如果我们添加一个额外的假设,即偏差和方差在最优点周围是对称的,那么我们可以将最低误差缩小到Bias²(c)=Var(c)。如果你尝试几个选项,你会发现最优点往往接近偏差平方和方差项相等的点。但是没有这个额外假设的话,这是不一定成立的。

含义

我们知道计算最优点是不可处理的。但一般认为,低偏差本质上会导致方差爆炸,这是模型复杂度的影响。想一想:其含义是,你无法拥有既表现良好又无偏的模型。

泛化误差

由于我们无法在所有可能的世界中平均计算,我们需要其他方法来计算模型的总体预期误差。泛化误差捕捉了模型在未见数据上的表现。这是模型在训练数据上的拟合程度与在底层数据分布上表现的差距。对于一个任意的损失函数,我们可以将泛化误差表述为:

注意,即便如此,我们也无法计算模型在所有可能的(x,y)组合上的预期性能。我们通过收集一个新的独立数据集来进行评估,从而近似泛化误差。我们可以用不同的方式来评估性能:

  1. 样本内误差:在用于拟合模型的数据上计算的训练误差。对于过拟合模型,这通常会误导性地偏低,并且无法捕捉泛化能力。

  2. 样本外误差(OOS):在一个与训练集分布相同的独立样本上的表现。这是评估泛化能力的黄金标准。

  3. 分布外误差(OOD):在不属于训练分布的数据上的表现。可以想象一个在城市区域训练的房价模型在乡村房屋上测试时——它很可能失败。

这些概念与我们在偏差-方差权衡中已经探讨过的内容有关。偏差较大的模型无法捕捉变量之间的关系,因此它们描述的关系无法很好地适应 OOS(样本外)示例。但高方差模型可能会根据它们看到的样本给出截然不同的预测。即使它们可能有较低的偏差(期望值),那也仅仅是因为它们误差的幅度相互抵消

现在让我们考虑两个与偏差和方差密切相关的概念:

  • 过拟合最好理解为模型容量与训练数据可用性之间的结果。当一个模型的参数数量相对于训练数据的大小或多样性过多时,它不仅会拟合底层信号,还会拟合数据中的噪声。

  • 另一方面,欠拟合是由于模型规格不足造成的后果。模型不够复杂,无法捕捉到底层分布的细节。这通常是因为相对于最佳拟合曲线的复杂度,模型的参数太少。

让我们来看一下从模拟中得到的一个可能的世界。在这里,我们放大了样本中大规模高价格部分的情况。注意到更复杂的模型试图画出一条曲线,基本上连接了所有观察到的点。如果样本稍微不同,这些曲线的形状可能会截然不同。另一方面,低复杂度的模型(例如* y = mx + b y = b *线)无法捕捉到数据集尾部的弯曲。

关于正则化的简要说明

L1 和 L2 正则化用于 Lasso 和 Ridge 回归是限制复杂性的有趣技术。它们并不是减少参数数量,而是鼓励较小的系数,从而产生平滑的曲线,这样更不容易在训练数据中的点之间产生震荡。这种做法会降低模型的复杂度,从而增加偏差。总体思路是,偏差的增加被减少的方差所弥补。关于这个话题已经写了整本教材,所以在这篇文章中我不再讨论正则化。

验证集和测试集

如果我们从偏差、方差和泛化误差的探索中能得到一条教训,那就是:模型必须在它从未见过的数据上进行评估。这个概念很直接,但其应用常常被误解。

验证集和测试集通过作为现实世界表现的代理来帮助减轻过拟合的风险。让我们从明确的区分开始:

  • 验证集: 在模型开发过程中使用,用于调节超参数并选择表现最好的模型变体。

  • 测试集: 一个完全独立的数据集,用于在所有训练和调优完成后评估最终模型。

使用这些数据集的目的是近似预测样本外的表现。但有一个陷阱。如果你过于频繁地使用验证集,它会成为训练过程的一部分,导致看不见的数据泄露问题。你可能会“过拟合”验证集上的超参数,从而未能捕捉到关系的真实本质。这就是为什么拥有单独的测试集来评估最终模型表现是有用的原因。测试集上的表现作为我们总体误差计算的代理。主要问题是:我们应该如何构建测试集?

尾部风险和分层

记住,估计需要了解分布的形状,而预测则只关注最大化经验精度。对于经验精度,我们需要考虑风险缓解。一个自动化的定价算法可能在预期中表现良好,但却可能带来重大的尾部风险。

显著低估高端住宅的价格会导致机会主义买家利用这些被低估的资产。显著高估高端住宅的价格会导致无人购买。现实世界的不对称性与预期值的对称性不匹配。

即使模型在预期中表现良好,它在实际应用中也会失败得非常惨。

这就是为什么分层在设置测试集时可以成为一个至关重要的组成部分。它可能涉及从采样空间中过于密集的区域中删除一些例子,直到整个领域内的分布变得均匀。这个测试集将是与我们的训练数据独立同分布的,因此它并不衡量我们之前看到的公式中的泛化误差。

另一种选择是使用不同的损失函数 (即不是均方误差(MSE),而是一个考虑到我们风险需求的损失函数)。这个损失函数可能会改变误差分解的动态,并可能偏向于显著欠拟合的模型。

我们的模型如何看待现实世界?

最后,考虑我们想要实现的目标。在深度学习中,我们的目标可能是训练通用智能体。偏差-方差权衡告诉我们大型语言模型是否理解它们正在阅读的文本?没有什么。如果我们想评估我们的训练过程是否创建了世界的准确模型,我们需要考虑分布外(OOD)误差。对于任何有希望成为通用模型的系统,它们必须在 OOD 情境下工作。为此,我们需要离开统计学的范畴,最终进入机器学习的领域。

反思

在前一节中,我们学习了偏差和方差的核心概念。在本节中,我们面对的是一个更复杂的问题,阐明了在不同训练数据下,偏差和方差与模型期望性能之间的关系。

我们增加了一些复杂性,考虑潜在变量如何影响模型在尾部的表现——这可能导致尾部风险。我们还存在内生性采样偏差,这意味着预期误差的评估可能无法描述真实的潜在关系。

我们引入了验证集和测试集的概念,作为帮助确定模型在分布外(OOS)性能的方法,以测试模型的泛化误差。我们还讨论了抛弃独立同分布(iid)假设的替代测试集构建方法,这可能导致模型具有更低的尾部风险。

我们还引入了一些关键假设,一旦进入深度学习领域,这些假设将不再适用。在我们进入深度学习领域之前,我们将应用所有这些教训来设计稳健的机器学习算法。

稳健的机器学习

在深度学习中,我们常常处理大规模数据集和复杂的模型。这种组合可能导致模型训练时间长达数小时(有时甚至是数周或数月)。当面临为训练单个模型花费数小时的现实时,使用交叉验证等技术的前景令人却步。尽管如此,在训练过程结束时,我们通常对性能有较高要求,因为这需要大量的时间和计算资源投入。

稳健性的两种视角

本节的部分内容集中在论文《机器学习稳健性:导论》中的观点。稳健模型被描述为那些在实际部署时仍然能表现良好的模型,尽管它们可能遇到与训练数据不同的输入。文中提供了以下有用的示例,说明了生产环境中输入可能发生变化的情况:

输入数据的变化和变动示例:

— 输入特征或物体识别模式的变化,挑战了模型从训练数据中学到的归纳偏差。

— 由于自然发生的扭曲,如光照条件或其他环境因素,生产数据分布发生变化。

— 恶意输入的篡改,由攻击者故意引入,以迷惑模型甚至将其预测引导到期望的方向。

— 由于外部因素,如社会行为和经济状况的变化,导致的数据逐渐漂移。

模型缺陷和稳定预测性能的威胁示例:

— 开发和利用无关的模式和虚假的相关性,这些在生产环境中无法成立。

— 难以适应训练样本中通常被低估的边缘情况。

— 容易受到对过度参数化的现代机器学习模型的对抗性攻击和数据中毒的影响,这些攻击和中毒瞄准了模型的脆弱性。

— 模型无法很好地推广到逐渐漂移的数据,导致概念漂移,因为它学到的概念变得过时或不再能代表当前的数据分布。

我们将与论文《基于偏差-方差权衡的鲁棒机器学习的数学基础》进行对比。请注意,这篇论文已经撤回,因为“几条相关的定理和命题没有被提及”。然而,它仍然从偏差-方差权衡的角度提供了鲁棒性的有效概述。我们将首先阅读这篇论文,并考虑模型决策边界的形状如何受到复杂性和训练数据的影响。

分类的误差分解

在二分类中,我们训练模型来预测类别 1(与类别 0 对比)的概率。这代表了在观察值x下,目标变量(y∈{0,1})的期望值。总误差是预测概率与期望值之间的差异。单个项目的损失可以最简单地通过以下公式度量:

这有效地度量了预测概率与真实类别之间的距离,并根据真实类别是否等于 0 或 1 动态调整。

我们注意到,分类的偏差-方差分解更加复杂。在《德国坦克问题》部分,我指出偏见模型仍然可能是正确的,因为方差可能(偶然地)将预测结果推向真实值。然而,当使用平方损失时,这一效应完全被事实所抵消,即对于高度错误的估计,期望损失大大增加。因此,高方差可能带来的任何潜在好处都会被显著偏离目标的估计所掩盖。

在二分类的情况下,这不一定成立。偏差、方差和总误差必须在(0,1)范围内。如果模型完全偏向某一类(bias=1),那么模型在期望上总是预测错误的类别。任何方差实际上都会使得正确预测更有可能!因此,在这种特定的情况下,Err=Bias-Var

如果我们加上一个合理的假设,即偏差和方差的和必须小于或等于 1,那么我们就得到了标准的分解,只是总误差是 Err=Bias+Var 而不是 Bias²。

模型复杂度是复杂的。

在深度学习中,你可能认为模型复杂度完全取决于网络中的参数数量。但请考虑到神经网络是通过随机梯度下降训练的,并且需要时间来收敛到解决方案。为了让模型发生过拟合,它需要时间去学习一个将所有训练数据点连接起来的转换。因此,模型复杂度不仅仅是参数数量的函数,还包括在相同数据集上训练的轮数。

这意味着我们的函数 g(λ)=c 并不像多项式回归那样直接。此外,像早停法这样的技术通过在验证集上的错误率开始增加时停止训练,明确地解决了模型的方差问题。

根据论文,影响偏差和方差的超参数主要有三种类型:

  • 类型 I:使用超参数直接平衡偏差和方差(例如,作为应用于正则化项(如权重衰减)的权重)。

  • 类型 II:通过调整来自单个训练样本的损失信号间接影响偏差和方差(例如,减少或增加对大预测误差的惩罚)。

  • 类型 III:控制影响模型复杂度的训练过程部分(例如,训练神经网络的训练轮数、早停法或决策树的深度)。

简单与困难样本

如果一个数据集在训练时导致模型有更大的期望泛化误差,那么这个数据集被认为是“更难”学习的。形式上:

注意:“对于所有 λ”是一个强条件,可能并不总是成立。一个数据集在某些超参数下可能更难学习,但在其他超参数下则不然。

我们做出假设,认为更困难数据集的最优复杂度(*c**)大于更简单数据集的最优复杂度。我们可以像这样绘制在两个数据集上训练的模型的期望误差:

来源:《基于偏差-方差权衡的稳健机器学习数学基础》

根据我们的假设,“简单”数据集和“困难”数据集之间的区别会导致一个有趣的结果:在“更困难”的数据集上选择最优超参数必然会导致模型更复杂。

如果我们将训练数据分为“容易”和“困难”子集,我们可以使用类似的逻辑得出某些数据子集更难以学习。我们可以将这种逻辑扩展到将个别样本(x,y)分类为容易或困难。考虑以下导致样本难以学习的原因:

  • 噪声标签(即标注错误的数据)

  • 特征空间中的稀疏区域

  • 必须复杂的分类边界

现在考虑focal loss,它可以表示为:

这类似于对特定样本应用损失加权,目的是在特征空间的难学部分给予模型更强的学习信号。一种常见的加权方法是按逆频率加权,这会给稀有类的样本分配更高的损失。焦点损失具有自动确定哪些样本难以学习的效果,基于模型的当前状态。模型的当前置信度被用来动态调整特征空间中困难区域的损失。因此,如果模型过于自信并且错误,这比模型自信且正确时传递的信号要强。

加权参数γ是一个类型 II 超参数的例子,它调整训练样本的损失信号。如果某个样本很难学习,焦点损失理想情况下会鼓励模型在特征空间的该部分变得更加复杂。然而,样本可能难以学习的原因有很多,因此这并不总是期望的效果。

决策边界的形状

在这里,我创建了一个二维数据集,其中包含简单的形状和重复的模式,作为决策边界。我还添加了一些“死区”,在这些区域,数据的采样变得更加困难。使用大约 100,000 个数据点,人类可以通过查看图表快速识别出边界应该是什么样子的。

尽管存在死区,你仍然可以轻松地看到边界,因为数十亿年的自然选择赋予了你一般的模式识别能力。对于从零开始训练的神经网络来说,情况就不那么容易了。在这个练习中,我们不会应用显式的正则化(权重衰减,dropout),因为这些方法会阻止网络过拟合训练数据。然而,值得注意的是,层归一化、跳跃连接,甚至随机梯度下降都可以作为隐式正则化器

在这里,参数的数量(p)大致等于样本的数量(N)。我们将只关注训练损失,以观察模型如何过拟合。以下两个模型分别使用相当大的批量大小训练 3000 个周期。左边模型的预测边界使用标准的二元交叉熵损失,而右边模型使用焦点损失:

第一点需要注意的是,尽管没有显式的正则化,但边界相对平滑。例如,在左上角,恰巧有一些稀疏的采样(偶然的),然而两个模型都倾向于切掉星形的一个尖端,而不是在每个点周围预测出一个更复杂的形状。这是一个重要的提醒,许多架构设计决定起到了隐式正则化的作用。

从我们的分析来看,我们期望焦点损失能够在自然复杂的区域预测出复杂的边界。理想情况下,这将是使用焦点损失的一个优势。但如果我们检查其中一个自然复杂的区域,我们会发现两个模型都未能识别出圆圈内部有一个额外的形状。

在稀疏数据(死区)区域,我们预期焦点损失会创建更复杂的边界。但这不一定是理想的。如果模型没有学习到数据的任何潜在模式,那么有无数种方式可以围绕稀疏点绘制边界。在这里,我们可以对比两个稀疏区域,发现焦点损失预测的边界比交叉熵更为复杂。

最上面一行来自中央的星星,我们可以看到焦点损失(focal loss)对模式的学习更为深入。稀疏区域的预测边界更加复杂,但也更为准确。最下面一行来自右下角,我们可以看到预测的边界更为复杂,但它并没有学到关于形状的模式。由 BCE(binary cross entropy)预测的平滑边界可能比焦点损失预测的奇怪形状更为理想。

这种定性分析并没有帮助我们决定哪个模型更好。我们如何量化它呢?这两种损失函数产生了不同的值,不能直接比较。相反,我们将比较预测的准确性。我们将使用标准的 F1 分数,但需要注意,不同的风险偏好可能会对召回率或精确度赋予额外的权重。

为了评估模型的泛化能力,我们使用与训练样本独立同分布(iid)的验证集。我们也可以使用早停法(early stopping)来防止两种方法的过拟合。如果我们比较两个模型的验证损失,我们会看到使用焦点损失的 F1 分数相对于二元交叉熵(binary cross entropy)有轻微提升。

  • BCE 损失: 0.936(验证集 F1)

  • 焦点损失: 0.954(验证集 F1)

因此,似乎在应用于未见数据时,使用焦点损失训练的模型表现略好。到目前为止,一切顺利,对吧?

iid 泛化问题

在标准的泛化定义中,假设未来的观测值与我们的训练分布是独立同分布的。但如果我们希望模型学习到生成数据的底层过程的有效表示,这并不会有所帮助。在这个例子中,这个过程涉及到决定决策边界的形状和对称性。如果我们的模型具有这些形状和对称性的内部表示,那么它应该在这些稀疏采样的“死区”中表现得同样好。

任何一个模型都永远无法在 OOD(分布外)情况下工作,因为它们只见过来自单一分布的数据,无法进行泛化。而期待模型能够泛化是无理的。然而,我们可以专注于稀疏采样区域的鲁棒性。在《机器学习鲁棒性:入门》一文中,作者主要讨论了来自分布尾部的样本,这也是我们在房价模型中看到的情况。但在这里,我们有一种情况,采样是稀疏的,但与明确的“尾部”无关。我将继续将其称为“内生采样偏差”,以强调稀疏性不一定需要尾部的存在。

在这种鲁棒性的视角下,内生采样偏差是一种可能的情况,其中模型可能无法泛化。对于更强大的模型,我们也可以探索 OOD 和对抗性数据。考虑一个图像模型,它被训练来识别城市区域的物体,但在丛林中无法工作。这将是我们期望足够强大的模型能够在 OOD 情况下工作的情境。另一方面,对抗性样本则涉及向图像添加噪声,改变颜色的统计分布,这种变化人眼无法察觉,但会导致非鲁棒模型的错误分类。但构建能够抵抗对抗性和 OOD 扰动的模型超出了这篇已经很长的文章的范围。

对扰动的鲁棒性

那么我们如何量化这种鲁棒性呢?我们从准确度函数A(我们之前使用过 F1 得分)开始。然后我们考虑一个扰动函数φ,我们可以将其应用于单个数据点或整个数据集。请注意,这个扰动函数应该保持预测变量x和目标变量y之间的关系。(也就是说,我们不是故意标记错误的样本)。

考虑一个旨在预测任何城市房价的模型,OOD 扰动可能涉及从训练数据中未包含的城市中获取样本。在我们的例子中,我们将关注一个修改版的数据集,这个数据集专门从稀疏区域进行采样。

模型(h)的鲁棒性评分(R)是衡量模型在扰动数据集下的表现,相较于清洁数据集的表现的指标:

考虑这两种训练模型来预测决策边界:一种使用焦点损失(focal loss)训练,另一种使用二元交叉熵(binary cross entropy)训练。在与训练数据独立同分布(iid)的验证集上,焦点损失的表现略好一些。然而,我们使用该数据集进行了早停,这可能导致一些微妙的信息泄漏。让我们在以下方面进行比较:

  1. 一个与训练集独立同分布的验证集,并用于早停。

  2. 一个与训练集独立同分布的测试集。

  3. 一个扰动过的(φ)测试集,我们仅从我所称的“死区”稀疏区域中抽样。

| Loss Type  | Val (iid) F1  | Test (iid) F1   | Test (φ) F1 |   R(φ)  |
|------------|---------------|-----------------|-------------|---------|
| BCE Loss   |    0.936      |    0.959        |   0.834     | 0.869   |
| Focal Loss |    0.954      |    0.941        |   0.822     | 0.874   |

标准的偏差-方差分解表明,通过允许在困难样本上增加复杂度,焦点损失可能会得到更鲁棒的结果。我们知道这在所有情况下可能并不理想,因此我们在验证集上进行了评估以确认。到目前为止一切顺利。但现在我们看一下在扰动后的测试集上的表现,发现焦点损失的表现稍微差了一些!然而,我们也看到焦点损失的鲁棒性分数稍微高一些。那这里到底发生了什么?

我进行了多次实验,每次结果略有不同。这是我想要强调的一个令人惊讶的实例。偏差-方差分解是关于我们的模型在期望下(在不同可能的世界中)如何表现。相比之下,这种鲁棒性方法告诉我们在扰动下这些特定模型的表现。但我们可能还需要更多的考虑来进行模型选择。

这些结果中有许多微妙的教训:

  1. 如果我们在验证集上做出重要决策(例如早停),那么拥有一个独立的测试集就变得至关重要。

  2. 即使在相同的数据集上进行训练,我们也可能得到不同的结果。在训练神经网络时,存在多个随机性来源。我们将在本文最后一部分进一步讨论这一点。

  3. 一个较弱的模型可能对扰动更加鲁棒。因此,模型选择需要考虑的不仅仅是鲁棒性分数。

  4. 我们可能需要在多个扰动下评估模型,以做出明智的决策。

比较鲁棒性的方法

在一种关于鲁棒性的方法中,我们通过偏差-方差权衡的视角考虑超参数对模型性能的影响。我们可以利用这些知识理解不同类型的训练样本如何影响我们的训练过程。例如,我们知道错误标记的数据在使用焦点损失时特别不利。我们可以考虑是否可以将特别困难的样本从训练数据中排除,从而产生更鲁棒的模型。通过考虑超参数的类型以及它们如何影响偏差和方差,我们还能更好地理解正则化的作用。

另一个观点则大多忽略了偏差-方差权衡,关注我们的模型在扰动输入上的表现。对我们来说,这意味着专注于稀疏采样区域,但也可能包括分布外(OOD)数据和对抗数据。这种方法的一个缺点是它是评估性的,并不一定告诉我们如何构建更好的模型,除非我们训练更多(且更多样化)的数据。更大的缺点是,较弱的模型可能表现出更强的鲁棒性,因此我们不能仅仅使用鲁棒性得分来进行模型选择。

正则化与鲁棒性

如果我们采用标准的交叉熵损失训练的模型,我们可以绘制不同时间点上模型在不同指标上的表现:训练损失、验证损失、validation_φ 损失、验证准确度和验证 _φ 准确度。我们可以比较在不同正则化类型存在下的训练过程,看看它如何影响泛化能力。

在这个特定问题中,我们可以做出一些不同寻常的观察。

  1. 正如我们所预期的,在没有正则化的情况下,随着训练损失趋近于 0,验证损失开始增加。

  2. validation_φ 损失的增加要显著得多,因为它只包含来自稀疏“死区”的例子。

  3. 但是,验证准确度并没有随着验证损失的增加而变得更差。这是怎么回事?这实际上是我在真实数据集中见过的情况。模型的准确度提高了,但它对输出的信心也在增强,所以当它出错时,损失非常高。使用模型的概率变得毫无用处,因为无论模型表现如何,它们都会趋向 99.99%。

  4. 添加正则化可以防止验证损失膨胀,因为训练损失不能降到 0。然而,它也可能对验证准确度产生负面影响。

  5. 添加 dropout 和权重衰减比仅使用 dropout 更好,但从准确度的角度来看,两者都比不使用正则化要差。

反思

如果你已经跟随我读到了这篇文章的这一部分,我希望你能理解偏差-方差权衡的局限性。理解模型复杂性与期望性能之间的典型关系始终是有用的,但我们已经看到了一些有趣的观察,挑战了默认的假设:

  • 模型复杂性可以在特征空间的不同部分发生变化。因此,单一的复杂度与偏差/方差度量并不能始终捕捉到整个情况。

  • 标准的泛化误差度量并没有捕捉到所有类型的泛化,尤其是在扰动下的鲁棒性缺失。

  • 我们的训练样本中有些部分比其他部分更难学习,而且有多种方式可以将训练样本视为“困难”。在特征空间的自然复杂区域可能需要复杂性,但在稀疏区域则可能是有问题的。这种稀疏性可能是由内生的采样偏差驱动的,因此将性能与 iid 测试集进行比较可能会产生误导。

  • 一如既往,我们需要考虑风险和风险最小化。如果你期望所有未来输入都与训练数据独立同分布,那么专注于稀疏区域或 OOD 数据可能是有害的,特别是当尾部风险不带来重大后果时。另一方面,我们已经看到尾部风险可能会带来独特的后果,因此为特定问题构建适当的测试集非常重要。

  • 单纯测试模型对扰动的鲁棒性不足以用于模型选择。只有在适当的风险评估下,才能做出关于模型泛化能力的决策。

  • 偏差-方差权衡只关注模型在可能世界中平均化后的期望损失。它并不一定告诉我们,使用硬分类边界时模型的准确度如何。这可能会导致一些反直觉的结果。

深度学习与过度参数化

让我们回顾一下对偏差-方差分解至关重要的一些假设:

  • 在低复杂度下,总误差由偏差主导,而在高复杂度下,总误差由方差主导。在最小复杂度时,偏差远大于方差。

  • 作为复杂性的函数,偏差是单调递减的,而方差是单调递增的。

  • 复杂性函数g是可微分的。

事实证明,对于足够深的神经网络,前两个假设是错误的。最后一个假设可能只是为了简化一些计算而方便的虚构。我们不会质疑那个假设,但我们将重点关注前两个假设。

让我们简要回顾一下什么是过拟合:

  • 当一个模型无法区分噪声(偶然性不确定性)与内在变异时,就会发生过拟合。这意味着一个训练过的模型在面对不同的训练数据时,可能会表现出截然不同的行为(即在不同可能世界中的方差)。

  • 我们注意到模型过拟合时,它无法对未见的测试集进行泛化。这通常意味着模型在与训练数据独立同分布(iid)的测试数据上的表现。我们可能会关注不同的鲁棒性度量,从而构建一个具有 OOS、分层、OOD 或对抗性质的测试集。

到目前为止,我们假设获得真正低偏差的唯一方法是模型过于复杂。而且我们假设这种复杂性导致了不同数据训练的模型之间的高方差。我们还已确定,许多超参数对复杂性有贡献,包括随机梯度下降的迭代次数。

过度参数化与记忆化

你可能听说过大型神经网络可以简单地记住训练数据。但这是什么意思呢?在参数足够的情况下,模型不需要学习特征和输出之间的关系。相反,它可以存储一个函数,完美地响应每个训练示例的特征,完全独立地进行处理。这就像为每种特征组合写一个明确的 if 语句,并为该特征产生平均输出。考虑我们的决策边界数据集,其中每个示例都是完全可分的。这意味着训练集中的每个样本都能达到 100%的准确率。

如果模型有足够的参数,那么梯度下降算法自然会使用所有这些空间进行记忆。一般认为,这比寻找特征与目标值之间的潜在关系要简单得多。这种情况通常发生在p ≫ N(可训练参数的数量远大于样本的数量)时。

但有两种情况,即使模型已经记住了训练数据,仍然能够学会泛化:

  1. 参数太少会导致模型过于简单。增加更多的参数会导致模型复杂度达到看似最优的水平。继续增加参数会使模型表现更差,因为它开始拟合训练数据中的噪声。一旦参数的数量超过训练样本的数量,模型可能开始表现得更好。当p ≫ N时,模型会达到另一个最优点。

  2. 训练模型直到训练和验证损失开始分离。随着模型记住训练数据,训练损失趋向于 0,但验证损失却开始急剧增加并达到峰值。经过一段时间(较长)的训练后,验证损失开始下降。

这就是所谓的“双重下降”现象,其中额外的复杂度实际上有助于更好的泛化。

双重下降是否需要标签错误?

一种普遍共识是,标签噪声足够但不是双重下降发生的必要条件。例如,论文揭示双重下降之谜发现,过度参数化的网络会学习将错误标签的类别分配给训练数据中的点,而不是学习忽略噪声。然而,模型可能会“隔离”这些点并学习围绕它们的普遍特征。它主要关注神经网络隐藏状态中学习到的特征,并表明这些特征的可分性即使在没有标签错误的情况下也能使标签变得嘈杂。

论文《双重下降揭秘》描述了在广义线性模型中双重下降发生的几种必要条件。这些标准主要关注数据中的方差(而非模型方差),这使得模型很难正确学习预测变量和目标变量之间的关系。任何这些条件都可能导致双重下降:

  1. 存在奇异值。

  2. 测试集的分布未能有效地通过训练数据中最能解释方差的特征来捕捉。

  3. 对于完美拟合的模型缺乏方差(即,完美拟合的模型似乎没有随机不确定性)。

这篇论文还通过以下可视化展示了一个玩具问题中的双重下降现象:

来源:《双重下降揭秘:识别、解释与消除深度学习难题的来源》

相比之下,论文《理解双重下降需要细致的偏差-方差分解》对不同噪声来源及其对方差的影响进行了详细的数学分析:

  1. 采样——一般来说,将模型拟合到不同的数据集上会导致不同预测的模型(V_D

  2. 优化——参数初始化的影响,但可能也包括随机梯度下降的性质(V_P)。

  3. 标签噪声——通常是被错误标记的样本(V_ϵ**)。

  4. 三个方差来源之间的潜在交互作用。

论文继续展示了这些方差项实际上是如何作为模型偏差的一部分贡献到总误差中的。此外,你可以先对 V_D 或 V_P 进行期望计算,这意味着根据你如何进行计算,你会得出不同的结论。一个恰当的分解需要理解总方差是如何通过三种方差来源之间的交互作用汇聚在一起的。结论是,虽然标签噪声加剧了双重下降,但它不是必需的。

正则化与双重下降

这些论文中的另一个共识是,正则化可以防止双重下降。但正如我们在前一部分中所看到的,这并不一定意味着正则化后的模型在未见数据上的泛化能力会更好。更有可能的是,正则化作为训练损失的“地板”,防止模型将训练损失降低到任意低的程度。但正如我们从偏差-方差权衡中所知道的,这可能限制了模型的复杂性,并引入了偏差。

反思

双重下降是一个有趣的现象,它挑战了本文中许多假设。我们可以看到,在适当的情况下,增加模型复杂度并不一定会降低模型的泛化能力。

我们是否应该将高度复杂的模型视为特殊情况,还是它们质疑整个偏差-方差权衡?就个人而言,我认为核心假设在大多数情况下成立,高度复杂的模型只是一个特殊情况。我认为偏差-方差权衡有其他缺陷,但核心假设往往是有效的。

结论

在统计推断和更典型的统计模型中,偏差-方差权衡相对简单。我没有讨论像决策树或支持向量机这样的其他机器学习方法,但我们讨论的许多内容仍然适用。然而,即使在这些环境中,我们也需要考虑比我们模型在所有可能世界中平均表现如何更多的因素。主要是因为我们是在将模型的表现与假设为 iid 的未来数据进行比较,这些数据假定与我们的训练集一致。

即使我们的模型只会看到类似于训练分布的数据,我们仍然可能面临尾部风险带来的重大后果。大多数机器学习项目需要进行适当的风险评估,以了解错误的后果。我们应该构建符合适当风险框架的验证和测试集,而不是在 iid 假设下评估模型。

此外,假定具有通用能力的模型需要在 OOD 数据上进行评估。执行关键功能的模型需要进行对抗性评估。值得指出的是,在强化学习的环境中,偏差-方差权衡不一定有效。考虑AI 安全中的对齐问题,它考虑了超越明确陈述目标的模型性能。

我们还看到,在大规模过度参数化模型的情况下,关于过拟合和欠拟合的标准假设根本不成立。双重下降现象复杂且仍然没有完全理解。然而,它提供了一个关于信任强烈持有假设有效性的重大教训。

对于那些坚持看到这里的读者,我想最后在本文的不同部分之间做一个连接。在推断统计部分,我解释了费舍尔信息描述了样本可以包含关于样本所来自分布的信息量。在本文的多个部分中,我也提到过有无数种方式围绕稀疏采样点绘制决策边界。关于样本中是否有足够的信息来对稀疏区域得出结论,存在一个有趣的问题。

在我关于为什么缩放有效的文章中,我谈到了归纳先验的概念。这是由我们选择的训练过程或模型架构引入的东西。这些归纳先验使得模型倾向于做出某些类型的推理。例如,正则化可能会鼓励模型做出平滑而不是崎岖的边界。通过不同类型的归纳先验,模型可能能从样本中提取比弱先验下更多的信息。例如,有方法可以鼓励对称性、平移不变性,甚至是检测重复模式。这些通常通过特征工程或通过架构决策(如卷积或注意力机制)来实现。

后记

我大约一年多前开始整理这篇文章的笔记。当时我有一个实验,焦点损失对从模型中获得良好性能至关重要。然后我接连做了几个实验,在没有明显原因的情况下,焦点损失的表现非常差。我开始深入挖掘偏差-方差权衡,这让我走进了一个死胡同。最终,我学到了更多关于双重下降的知识,并意识到偏差-方差权衡比我之前认为的要复杂得多。在那段时间里,我阅读并注释了几篇相关的论文,所有的笔记只是静静地积累在数字尘土中。

最近我意识到,多年来我读了很多糟糕的关于偏差-方差权衡的文章。我认为缺失的一个观念是,我们在计算“可能世界”中的期望。这个见解可能并不适合所有人,但对我来说似乎至关重要。

我还想评论一下一个流行的关于偏差与方差的可视化图,它使用箭术射击分布在靶心周围。我觉得这个可视化有误导性,因为它让人看起来偏差和方差是关于单一模型的个体预测。然而,偏差-方差误差分解背后的数学显然是关于在“可能世界”中对性能的平均值。我故意避免使用这个可视化图,正是因为这个原因。

我不确定有多少人会看到最后。我在开始写关于人工智能的内容之前很久就整理了这些笔记,并觉得应该好好利用它们。我也只是需要把这些想法从脑海里整理出来并写下来。所以,如果你已经看到最后,我希望你能从我的观察中获得一些启发。

参考文献

[1] “德国坦克问题,”Wikipedia, 2021 年 11 月 26 日。en.wikipedia.org/wiki/German_tank_problem

[2] Wikipedia Contributors, “最小方差无偏估计量,”Wikipedia, 2019 年 11 月 09 日。en.wikipedia.org/wiki/Minimum-variance_unbiased_estimator

[3] “似然函数,” Wikipedia, 2020 年 11 月 26 日. en.wikipedia.org/wiki/Likelihood_function

[4] “费舍尔信息,” Wikipedia, 2023 年 11 月 23 日. en.wikipedia.org/wiki/Fisher_information

[5] Why, “为什么使用平方误差是标准做法,而绝对误差对于大多数问题更为相关?,” Cross Validated, 2020 年 6 月 5 日. stats.stackexchange.com/questions/470626/w(访问日期:2024 年 11 月 26 日)。

[6] Wikipedia 贡献者, “偏差–方差权衡,” Wikipedia, 2020 年 2 月 4 日. en.wikipedia.org/wiki/Bias%E2%80%93variance_tradeoff

[7] B. Efron, “预测、估计与归因,” International Statistical Review, 第 88 卷,第 S1 期, 2020 年 12 月, doi: doi.org/10.1111/insr.12409

[8] T. Hastie, R. Tibshirani, 和 J. H. Friedman, 《统计学习的元素》。Springer, 2009 年。

[9] T. Dzekman, “Medium,” Medium, 2024 年. medium.com/towards-data-science/why-scalin(访问日期:2024 年 11 月 26 日)。

[10] H. Braiek 和 F. Khomh, “机器学习鲁棒性:入门,” 2024 年. 可用: arxiv.org/pdf/2404.00897

[11] O. Wu, W. Zhu, Y. Deng, H. Zhang, 和 Q. Hou, “基于偏差–方差权衡的鲁棒机器学习数学基础,” arXiv.org, 2021. arxiv.org/abs/2106.05522v4(访问日期:2024 年 11 月 26 日)。

[12] “bias_variance_decomp: 用于分类和回归损失的偏差–方差分解 — mlxtend,” rasbt.github.io. rasbt.github.io/mlxtend/user_guide/evaluate/bias_variance_decomp

[13] T.-Y. Lin, P. Goyal, R. Girshick, K. He, 和 P. Dollár, “用于密集目标检测的焦点损失,” arXiv:1708.02002 [cs], 2018 年 2 月, 可用: arxiv.org/abs/1708.02002

[14] Y. Gu, X. Zheng, 和 T. Aste, “解开双重下降之谜:通过学习特征空间的视角进行深入分析,” arXiv.org, 2023. arxiv.org/abs/2310.13572(访问日期:2024 年 11 月 26 日)。

[15] R. Schaeffer 等人, “双重下降解密:识别、解释与剖析深度学习难题的源头,” arXiv.org, 2023. arxiv.org/abs/2303.14151(访问日期:2024 年 11 月 26 日)。

[16] B. Adlam 和 J. Pennington, “理解双重下降需要细粒度的偏差–方差分解,” 神经信息处理系统, 第 33 卷, 第 11022–11032 页, 2020 年 1 月。

在 AWS Inferentia 和 Trainium 上优化 AI 模型

原文:towardsdatascience.com/ai-model-optimization-on-aws-inferentia-and-trainium-cfd48e85d5ac?source=collection_archive---------3-----------------------#2024-10-20

提升 ML 性能的 AWS Neuron SDK 技巧

Chaim RandTowards Data Science Chaim Rand

·发表于Towards Data Science ·阅读时间 9 分钟·2024 年 10 月 20 日

--

图片来源:julien Tromeur via Unsplash

我们正处于人工智能的黄金时代,前沿模型正在颠覆各行各业,并有望改变我们所知的生活方式。这些进步的推动力是日益强大的 AI 加速器,如NVIDIA H100 GPUsGoogle Cloud TPUsAWS 的 TrainiumInferentia芯片等。随着选项数量的增加,面临的挑战是选择最优平台来处理我们的机器学习(ML)工作负载——这是一个至关重要的决策,因为与 AI 计算相关的成本非常高。重要的是,全面评估每个选项需要确保我们正在最大化其利用率,以充分发挥其能力。

在本文中,我们将回顾几种优化 AWS 定制 AI 芯片上的 ML 工作负载的技术,使用的是AWS Neuron SDK。这继续了我们关于跨各种平台和环境的 ML 模型性能分析和优化的系列文章(例如,参见此处和此处)。虽然我们的主要关注点将是 ML 训练工作负载和 AWS Inferentia2,但所讨论的技术同样适用于 AWS Trainium。(回想一下,尽管 AWS Inferentia 主要设计为 AI 推理芯片,但我们之前展示过它在训练任务中的有效性。)

一般来说,性能优化是一个迭代过程,其中包括性能分析步骤,用于适当识别性能瓶颈和资源利用不足(例如,参见此处)。然而,由于我们将讨论的技术是通用的(即,它们可能适用于任何模型,无论其性能特征如何),我们将Neuron SDK 性能分析的讨论推迟到未来的文章中。

免责声明

我们分享的代码仅用于演示目的——我们不对其准确性、最优性或健壮性做任何声明。请不要将本文视为官方Neuron SDK 文档的替代品。请不要将我们提到的任何平台、库或优化技术解读为对其使用的支持。最适合您的选项将极大地依赖于您的具体用例,并且需要您自己深入的调查和分析。

以下实验是在一个Amazon EC2 inf2.xlarge实例上运行的(该实例包含两个Neuron 核心和四个 vCPU)。我们使用了当时可用的最新版本Deep Learning AMI for Neuron,即“Deep Learning AMI Neuron(Ubuntu 22.04)20240927”,并配备了AWS Neuron 2.20PyTorch 2.1。有关设置和安装的更多详细信息,请参见SDK 文档。请记住,Neuron SDK 正在积极开发中,我们提到的 API 和我们报告的运行时测量结果在您阅读本文时可能已经过时。请确保与最新的 SDK 和文档保持同步。

玩具模型

为了方便讨论,我们引入以下简单的Vision Transformer(ViT)分类模型(基于timm版本 1.0.10):

from torch.utils.data import Dataset
import time, os
import torch
import torch_xla.core.xla_model as xm
import torch_xla.distributed.parallel_loader as pl
from timm.models.vision_transformer import VisionTransformer

# use random data
class FakeDataset(Dataset):
  def __len__(self):
    return 1000000

  def __getitem__(self, index):
    rand_image = torch.randn([3, 224, 224], dtype=torch.float32)
    label = torch.tensor(data=index % 1000, dtype=torch.int64)
    return rand_image, label

def train(batch_size=16, num_workers=0):
  # Initialize XLA process group for torchrun
  import torch_xla.distributed.xla_backend
  torch.distributed.init_process_group('xla')

  # multi-processing: ensure each worker has same initial weights
  torch.manual_seed(0)
  dataset = FakeDataset()
  model = VisionTransformer()

  # load model to XLA device
  device = xm.xla_device()
  model = model.to(device)
  optimizer = torch.optim.Adam(model.parameters())
  data_loader = torch.utils.data.DataLoader(dataset,
                                            batch_size=batch_size,
                                            num_workers=num_workers)

  data_loader = pl.MpDeviceLoader(data_loader, device)
  loss_function = torch.nn.CrossEntropyLoss()
  summ = 0
  count = 0
  t0 = time.perf_counter()

  for step, (inputs, targets) in enumerate(data_loader, start=1):
    optimizer.zero_grad()
    outputs = model(inputs)
    loss = loss_function(outputs, targets)
    loss.backward()
    xm.optimizer_step(optimizer)
    batch_time = time.perf_counter() - t0
    if step > 10:  # skip first steps
      summ += batch_time
      count += 1
    t0 = time.perf_counter()
    if step > 500:
      break
  print(f'average step time: {summ/count}')

if __name__ == '__main__':
  train()

# Initialization command:
# torchrun --nproc_per_node=2 train.py

在我们的 AWS Inferentia 实例的两个核心上运行基准模型,结果是每秒训练 251.98 个样本。

在接下来的部分中,我们将迭代应用多种潜在的优化技术,并评估它们对步长时间性能的影响。虽然我们不会详细介绍每种方法,但会提供进一步阅读的参考(例如,这里)。重要的是,我们将呈现的列表并非全面涵盖——还有许多技术超出了我们的讨论范围。我们将这些方法分为三类:PyTorch 优化、OpenXLA 优化和特定于 Neuron 的优化。然而,展示的顺序并不固定。实际上,某些技术是相互依赖的——例如,应用混合精度优化可能会释放出足够的设备内存,从而允许增加批量大小。

PyTorch 性能优化

在之前的文章中(例如,这里),我们已经广泛讨论了在 GPU 上进行 PyTorch 模型性能分析和优化的话题。我们讨论的许多技术也适用于其他 AI 加速器。在本节中,我们将重新审视其中的一些技术,并将其应用于 AWS Inferentia。

多进程数据加载

多进程数据加载中,输入数据是在一个或多个专用的 CPU 进程中准备的,而不是在运行训练步骤的相同进程中进行处理。这允许数据加载和训练过程重叠,从而提高系统利用率并显著加速。进程数由PyTorch DataLoadernum_workers参数控制。在下面的代码块中,我们将num_workers设置为 1 来运行脚本:

train(num_workers=1)

这种变化导致训练速度为每秒 253.56 个样本,提升不到 1%。

批量大小优化

另一个可能影响训练速度的重要超参数是训练批量大小。我们发现,增加批量大小通常可以提高系统利用率并带来更好的性能。然而,效果会根据模型和平台的不同而有所变化。在我们的 AWS Inferentia 玩具模型中,我们发现每个神经元核心使用 8 个样本的批量大小训练时,速度为每秒 265.68 个样本,比每核心 16 个样本的批量大小快大约 5%。

train(batch_size=8, num_workers=1)

PyTorch 自动混合精度

提升性能的另一个常用方法是使用较低精度的浮点数,例如 16 位的 BFloat16。值得注意的是,某些模型组件可能与降低精度的浮点数不兼容。PyTorch 的自动混合精度(AMP)模式会尝试自动将最合适的浮点类型匹配到每个模型操作上。尽管 Neuron 编译器提供了不同的混合精度选项,它也支持使用 PyTorch AMP的选项。在下面的代码块中,我们包括了使用 PyTorch AMP 所需的修改。

def train(batch_size=16, num_workers=0):
  # Initialize XLA process group for torchrun
  import torch_xla.distributed.xla_backend
  torch.distributed.init_process_group('xla')

  # multi-processing: ensure each worker has same initial weights
  torch.manual_seed(0)
  dataset = FakeDataset()
  model = VisionTransformer()

  # load model to XLA device
  device = xm.xla_device()
  model = model.to(device)
  optimizer = torch.optim.Adam(model.parameters())
  data_loader = torch.utils.data.DataLoader(dataset,
                                            batch_size=batch_size,
                                            num_workers=num_workers)

  data_loader = pl.MpDeviceLoader(data_loader, device)
  loss_function = torch.nn.CrossEntropyLoss()
  summ = 0
  count = 0
  t0 = time.perf_counter()

  for step, (inputs, targets) in enumerate(data_loader, start=1):
    optimizer.zero_grad()

    # use PyTorch AMP
    with torch.autocast(dtype=torch.bfloat16, device_type='cuda'):
      outputs = model(inputs)
      loss = loss_function(outputs, targets)
    loss.backward()
    xm.optimizer_step(optimizer)
    batch_time = time.perf_counter() - t0
    if step > 10:  # skip first steps
      summ += batch_time
      count += 1
    t0 = time.perf_counter()
    if step > 500:
      break
  print(f'average step time: {summ/count}')

if __name__ == '__main__':
  # disable neuron compilar casting
  os.environ["NEURON_CC_FLAGS"] = "--auto-cast=none"
  torch.cuda.is_bf16_supported = lambda: True
  train(batch_size=8, num_workers=1)

结果训练速度为每秒 196.64 个样本,比 Neuron 编译器的默认混合精度设置低约 26%。需要注意的是,虽然本文重点讨论性能,但在实际应用场景中,我们还需要评估所选择的混合精度策略对模型准确性的影响。

OpenXLA 优化

正如之前的文章中讨论的那样,Neuron Cores 被视为XLA 设备,并且torch-neuronx Python 包实现了PyTorch/XLA API。因此,OpenXLA 框架提供的任何优化机会,特别是 PyTorch/XLA API 所提供的优化,都可以在 AWS Inferentia 和 Trainium 上利用。在本节中,我们将考虑其中的一些优化机会。

BFloat16 精度

OpenXLA 支持通过 XLA_USE_BF16 环境变量将所有浮动数值转换为 BFloat16,如下代码块所示:将所有浮动数值转换为 BFloat16

if __name__ == '__main__':
  os.environ['XLA_USE_BF16'] = '1'
  train(batch_size=8, num_workers=1)

结果训练速度为每秒 394.51 个样本,比默认混合精度选项的速度快近 50%。

多进程设备加载

PyTorch/XLA 的MpDeviceLoader及其内部的ParallelLoader,负责将输入数据加载到加速器上,包括多个参数,用于控制数据从主机到设备的传输。在下面的代码块中,我们调节batches_per_execution设置,该设置决定了每次ParallelLoader执行周期中,复制到设备的批次数。通过增加该设置,我们旨在减少主机与设备之间通信的开销:

data_loader = torch.utils.data.DataLoader(dataset,
                                          batch_size=batch_size,
                                          num_workers=num_workers
                                          )
data_loader = pl.MpDeviceLoader(data_loader, 
                                device, batches_per_execution=10)

由于这种优化,训练速度提高到每秒 1,027.39 个样本,代表着额外的 260%的加速。

使用 OpenXLA 后端进行 Torch 编译

在之前的文章中(例如,这里),我们演示了使用PyTorch 的图编译所带来的潜在性能提升。尽管OpenXLA包含了自己的图创建和即时编译机制,torch.compile可以通过消除每一步追踪模型操作的需求,提供额外的加速。以下代码片段演示了如何使用专用的openxla后端来编译模型:

model = model.to(device)
model = torch.compile(backend='openxla')

尽管 torch.compile 目前尚不支持Neuron SDK,但我们提前提及它,以期望其未来发布。

Neuron SDK 优化

在本节中,我们将考虑 AWS Neuron SDK 提供的一些优化机会,特别是 Neuron 编译器所提供的优化。

混合精度

Neuron SDK 支持多种混合精度设置。在下面的代码块中,我们通过NEURON_CC_FLAGS环境变量编程,指示编译器将所有浮点数转换为 BFloat16。

if __name__ == '__main__':
  os.environ["NEURON_CC_FLAGS"] = "--auto-cast all --auto-cast-type bf16"
  train(batch_size=8, num_workers=1)

这导致(不足为奇)与上文提到的 OpenXLA BFloat16 实验相似的训练速度。

FP8

NeuronCoreV2 的一个独特特点是它支持八位浮点类型 fp8_e4m3。下面的代码块演示了如何配置 Neuron 编译器,以自动将所有浮点操作转换为 FP8:

if __name__ == '__main__':
 os.environ["NEURON_CC_FLAGS"] = "--auto-cast all --auto-cast-type fp8_e4m3"
 train(batch_size=8, num_workers=1)

虽然 FP8 在某些情况下可以加速训练,但由于其精度和动态范围的降低,保持稳定的收敛性比使用 BFloat16 时更具挑战性。请参阅我们的上一篇文章,了解有关 FP8 训练的潜在好处和挑战。

在我们的模型中,使用 FP8 实际上会比 BFloat16 更影响运行时性能,将训练速度降低到每秒 940.36 个样本。

编译器优化

Neuron 编译器包含多个控制项,用于优化编译后的图的运行时性能。两个关键设置是model-typeopt-levelmodel-type设置应用于特定模型架构的优化,如变换器(transformers),而opt-level设置则允许在编译时间和运行时性能之间找到平衡。在下面的代码块中,我们将model-type设置为tranformer,将opt-level设置为最高性能选项。我们还进一步指定target运行设备为inf2,以确保模型针对目标设备进行了优化。

if __name__ == '__main__':
  os.environ['XLA_USE_BF16'] = '1'
  os.environ["NEURON_CC_FLAGS"] = "--model-type transformer " \
                                  "--optlevel 3" \
                                  " --target inf2"
  train(batch_size=8, num_workers=1)

上述配置实现了每秒 1093.25 个样本的训练速度,带来了约 6%的改善。

结果

我们在下表中总结了实验的结果。请记住,我们讨论的每种优化方法的效果将极大地依赖于模型和运行时环境。

实验结果(作者)

我们采用的技术使性能相比基准实验提高了 435%。通过重新审视和微调我们讨论的一些方法,或者应用本文未涉及的其他优化技术,可能还会实现进一步的加速。

我们的目标是展示一些可用的优化策略,并展示它们对运行时性能的潜在影响。然而,在实际场景中,我们需要评估这些优化如何影响模型的收敛性。在某些情况下,可能需要调整模型配置,以确保在不牺牲准确性的情况下实现最佳性能。此外,使用性能分析工具来识别瓶颈并衡量系统资源利用率,对于指导和促进我们的优化活动至关重要。

总结

如今,我们有幸能够在各种各样的系统上运行我们的机器学习工作负载。无论我们选择哪个平台,我们的目标都是最大化其能力。在本文中,我们重点介绍了 AWS Inferentia,并回顾了几种加速在其上运行的机器学习工作负载的技术。一定要查看我们的其他文章,了解更多关于各类 AI 加速器的优化策略。

使用 JAX 进行 AI 模型训练

原文:towardsdatascience.com/ai-model-training-with-jax-6e407a7d2dc8?source=collection_archive---------5-----------------------#2024-05-29

开启超快速的 AI/ML 开发之路

Chaim RandTowards Data Science Chaim Rand

·发布于 Towards Data Science ·10 分钟阅读·2024 年 5 月 29 日

--

图片由 Matt Foxx 提供,来源于 Unsplash

作者

在 AI 模型开发中,最关键的决策之一就是选择机器学习开发框架。多年来,许多库争夺着“AI 开发者首选框架”的这一有利头衔。(还记得 CaffeTheano 吗?)在几年的时间里,TensorFlow——它强调高性能、基于图形的计算——似乎一直是遥不可及的领先者(根据作者对学术论文中提及的次数和社区支持的强度的估计)。大约在十年之交,PyTorch——凭借其用户友好的 Pythonic 接口——似乎已经成为无可争议的“女王”。然而,近年来一个新兴的框架迅速流行起来,已经无法忽视。瞄准这一宝贵的桂冠,JAX 旨在最大化 AI 模型训练和推理的性能,同时不妥协用户体验。

在本文中,我们将评估这一新框架,演示其使用,并分享我们对其优缺点的一些看法。值得注意的是,本文并非JAX 教程。如果想了解 JAX,建议您查阅官方文档以及许多在线教程(例如,这里)。尽管我们关注的主要是 AI 模型训练,但需要注意的是,JAX 在 AI/ML 领域及其他领域还有许多额外的应用。基于 JAX 构建了几个高级 ML 库。在本文中,我们将使用Flax,它是截至目前看起来最受欢迎的库。

感谢Ohad KleinYitzhak Levi对本文的贡献。

JAX 背后的秘密——XLA 编译

让我们马上公开一点:对 JAX 没有任何不尊重的意思,JAX 的真正强大之处在于其使用了XLA编译。JAX 所展示的卓越运行时性能,来自于 XLA 启用的硬件特定优化。而许多与 JAX 相关的功能特性,比如即时编译(JIT)和“函数式编程”范式,实际上都源自于 XLA。事实上,XLA 编译并不独特于 JAX,TensorFlowPyTorch都支持使用 XLA 的选项。然而,与其他流行的框架不同,JAX 从底层开始就是为使用 XLA 而设计的。这使得它可以将其设计与实现紧密结合,包括JIT、自动微分(grad)、矢量化(vmap)、并行化(pmap)、分片(shard_map)和其他功能(所有这些都值得非常尊重)与底层的 XLA 库紧密结合。(作为对比,参见这篇有趣的文章,了解 PyTorch“功能化”的历史。)

正如我们在 上一篇文章中讨论的那样,XLA JIT 编译器对与模型相关的计算图进行全面分析,将连续的张量操作融合为单个内核,移除冗余的图组件,并输出最适合底层加速器的机器码。这导致每个训练步骤所需的机器级操作数(FLOPS)减少,主机与加速器之间的通信开销减少(例如,减少需要加载到加速器中的内核数),内存占用减少,专用加速器引擎的利用率提高,等等。

除了运行时性能优化,XLA 的另一个重要特性是其可插拔的基础设施,使得它能够扩展对额外 AI 加速器的支持。XLA 是 OpenXLA 项目的一部分,并正在与机器学习领域的多个参与者合作构建。

与此同时,正如我们在 上一篇文章中详细描述的,依赖 XLA 也意味着一些局限性和潜在的陷阱。特别是,许多 AI 模型,包括具有动态张量形状的模型,可能无法在 XLA 中达到最优运行效果。需要特别小心以避免计算图断裂和图的重新编译。你还应该考虑代码调试性的影响。

JAX 实战 — 简单示例

在本节中,我们将演示如何在 JAX 中使用(单个)GPU 训练一个简单的 AI 模型,并将其与 PyTorch 进行比较。如今,有许多高层次的机器学习开发平台,包含多个机器学习框架的后端。这使得我们可以将 JAX 与其他框架的性能进行比较。在本节中,我们将使用 HuggingFaceTransformers 库,该库包括许多常见的 Transformer 支持的模型的 PyTorch 和 JAX 实现。更具体地说,我们将定义一个基于 Vision Transformer(ViT)的分类模型,使用 ViTForImageClassificationFlaxViTForImageClassification 模块,分别对应 PyTorch 和 JAX 实现。以下代码块包含了模型定义:

import torch
import jax, flax, optax
import jax.numpy as jnp

def get_model(use_jax=False):
    from transformers import ViTConfig

    if use_jax:
        from transformers import FlaxViTForImageClassification as ViTModel
    else:
        from transformers import ViTForImageClassification as ViTModel

    vit_config = ViTConfig(
        num_labels = 1000,
        _attn_implementation = 'eager'  # this disables flash attention
    )

    return ViTModel(vit_config)

请注意,我们选择禁用 flash attention 的使用,因为该优化仅为 PyTorch 模型实现(截至本文撰写时)。

由于我们在这篇文章中的兴趣集中在运行时性能上,因此我们将使用随机生成的数据集来训练我们的模型。我们利用 JAX 支持使用 PyTorch 数据加载器的事实:

def get_data_loader(batch_size, use_jax=False):
    from torch.utils.data import Dataset, DataLoader, default_collate

    # create dataset of random image and label data
    class FakeDataset(Dataset):
        def __len__(self):
            return 1000000

        def __getitem__(self, index):
            if use_jax: # use nhwc
                rand_image = torch.randn([224, 224, 3], dtype=torch.float32)
            else: # use nchw
                rand_image = torch.randn([3, 224, 224], dtype=torch.float32)
            label = torch.tensor(data=[index % 1000], dtype=torch.int64)
            return rand_image, label

    ds = FakeDataset()

    if use_jax:  # convert torch tensors to numpy arrays
        def numpy_collate(batch):
            from jax.tree_util import tree_map
            import jax.numpy as jnp
            return tree_map(jnp.asarray, default_collate(batch))
        collate_fn = numpy_collate
    else:
        collate_fn = default_collate

    ds = FakeDataset()
    dl = DataLoader(ds, batch_size=batch_size,
                    collate_fn=collate_fn)
    return dl

接下来,我们定义了 PyTorch 和 JAX 的训练循环。JAX 训练循环依赖于一个 Flax TrainState 对象,其定义遵循 基本教程,用于在 Flax 中训练机器学习模型:

@jax.jit
def train_step_jax(train_state, batch):
    with jax.default_matmul_precision('tensorfloat32'):
        def forward(params):
            logits = train_state.apply_fn({'params': params}, batch[0])
            loss = optax.softmax_cross_entropy(
                logits=logits.logits, labels=batch[1]).mean()
            return loss

        grad_fn = jax.grad(forward)
        grads = grad_fn(train_state.params)
        train_state = train_state.apply_gradients(grads=grads)
        return train_state

def train_step_torch(batch, model, optimizer, loss_fn, device):
    inputs = batch[0].to(device=device, non_blocking=True)
    label = batch[1].squeeze(-1).to(device=device, non_blocking=True)
    outputs = model(inputs)
    loss = loss_fn(outputs.logits, label)
    optimizer.zero_grad(set_to_none=True)
    loss.backward()
    optimizer.step()

现在让我们将一切整合在一起。在下面的脚本中,我们包含了用于使用基于图的 JIT 编译选项的控制,使用了 torch.compiletorch_xla

def train(batch_size, mode, compile_model):
    print(f"Mode: {mode} \n"
          f"Batch size: {batch_size} \n"
          f"Compile model: {compile_model}")

    # init model and data loader
    use_jax = mode == 'jax'
    use_torch_xla = mode == 'torch_xla'
    model = get_model(use_jax)
    train_loader = get_data_loader(batch_size, use_jax)

    if use_jax:
        # init jax settings
        from flax.training import train_state
        params = model.module.init(jax.random.key(0), 
                                   jnp.ones([1, 224, 224, 3]))['params']
        optimizer = optax.sgd(learning_rate=1e-3)
        state = train_state.TrainState.create(apply_fn=model.module.apply,
                                              params=params, tx=optimizer)
    else:
        if use_torch_xla:
            import torch_xla
            import torch_xla.core.xla_model as xm
            import torch_xla.distributed.parallel_loader as pl
            torch_xla._XLAC._xla_set_use_full_mat_mul_precision(
                use_full_mat_mul_precision=False)

            device = xm.xla_device()
            backend = 'openxla'

            # wrap data loader
            train_loader = pl.MpDeviceLoader(train_loader, device)
        else:
            device = torch.device('cuda')
            backend = 'inductor'

        model = model.to(device)
        if compile_model:
            model = torch.compile(model, backend=backend)
        model.train()
        optimizer = torch.optim.SGD(model.parameters())
        loss_fn = torch.nn.CrossEntropyLoss()

    import time
    t0 = time.perf_counter()
    summ = 0
    count = 0

    for step, data in enumerate(train_loader):
        if use_jax:
            state = train_step_jax(state, data)
        else:
            train_step_torch(data, model, optimizer, loss_fn, device)

        # capture step time
        batch_time = time.perf_counter() - t0
        if step > 10:  # skip first steps
            summ += batch_time
        count += 1
        t0 = time.perf_counter()
        if step > 50:
            break

    print(f'average step time: {summ / count}')

if __name__ == '__main__':
    import argparse
    torch.set_float32_matmul_precision('high')

    parser = argparse.ArgumentParser(description='Toy Training Script.')
    parser.add_argument('--batch-size', type=int, default=32,
                        help='input batch size for training (default: 2)')
    parser.add_argument('--mode', choices=['pytorch', 'jax', 'torch_xla'],
                        default='jax',
                        help='choose training mode')
    parser.add_argument('--compile-model', action='store_true', default=False,
                        help='whether to apply torch.compile to the model')
    args = parser.parse_args()

    train(**vars(args))

关于基准比较的重要说明

在分析基准比较时,至关重要的是我们要对它们的执行过程非常谨慎和批判。尤其是在 AI 模型开发的情况下,基于不准确数据做出的决策可能会带来极其昂贵的后果。在比较训练模型的运行时性能时,有许多因素可能会对我们的测量产生主导作用,包括浮点数精度、矩阵乘法(matmul)精度、数据加载方法、是否使用闪存/融合注意力等。例如,如果 PyTorch 的默认矩阵乘法精度是 float32,而 JAX 是 tensorfloat32,那么我们无法从它们的性能比较中获得太多有价值的信息。这些设置可以通过像 jax.default_matmul_precisiontorch.set_float32_matmul_precision 这样的 API 来控制。在我们的脚本中,我们尝试将这些潜在问题隔离开来,但并不能保证我们已经成功解决了所有问题。

结果

我们在两台 Google Cloud 虚拟机上运行了训练脚本,一台是g2-standard-16虚拟机(配备单个 NVIDIA L4 GPU),另一台是a2-highgpu-1g虚拟机(配备单个 NVIDIA A100 GPU)。在每种情况下,我们都使用了一个专用的深度学习虚拟机镜像(common-cu121-v20240514-ubuntu-2204-py310),并安装了 PyTorch(2.3.0)、PyTorch/XLA(2.3.0)、JAX(0.4.28)、Flax(0.8.4)、Optax(0.2.2)以及HuggingFaceTransformers库(4.41.1)。有关 GPU 环境下JAXPyTorch/XLA的正确安装方法,请参阅官方文档。

以下表格展示了多个实验的运行时结果。请记住,比较性能可能会根据模型架构和运行环境发生显著变化。此外,代码的少量修改也有可能对结果产生可测量的影响。

NVIDIA L4 GPU 上的结果(作者提供)

NVIDIA A100 GPU 上的结果(作者提供)

尽管 JAX 在 L4 GPU 上表现出比其替代方案更优越的性能,但在 A100 上它与 PyTorch/XLA 的表现不相上下。考虑到共同的 XLA 后端,这并不令人惊讶。JAX 生成的任何 XLA(HLO)图应该(至少在理论上)也能由 PyTorch/XLA 实现。torch.compile 选项在两个平台上的表现都不尽如人意。鉴于我们选择了全精度浮动数,这在一定程度上是可以预见的。正如上一篇文章所述,torch.compile 的真正价值体现在使用自动混合精度(AMP)时。

欲了解更多 JAX 和 PyTorch 之间性能比较的详细信息,请务必查看HuggingFaceGoogleMLCommons编写的更全面的基准报告。

那么,为什么使用 JAX?

在 JAX 中进行训练的一个常见动机是 JIT 编译所带来的潜在运行时性能优化。但是,考虑到 PyTorch 中的新的(PyTorch/XLA)和更新的(torch.compile)JIT 编译选项,这一说法很容易受到挑战。事实上,考虑到 PyTorch 拥有庞大的开发者社区以及 PyTorch 中原生支持的众多功能,而 JAX/FLAX 并不支持(例如,自动混合精度高级注意力层,截至本文撰写时),有人可能会强烈主张值得花时间学习 JAX。然而,我们认为现代 AI 开发团队必须熟悉 JAX 及其提供的机会。这对于那些像我们一样,执着于利用最新、最强大的模型训练方法的团队尤为重要。除了潜在的性能收益外,以下是一些额外的激励因素:

为 XLA 设计

与经过后期“功能化”的 PyTorch/XLA 相对,JAX 从一开始就是为 XLA 设计的。这意味着某些在 PyTorch/XLA 中可能显得复杂或混乱的操作,在 JAX 中可以优雅地完成。一个很好的例子就是在训练序列中混合使用 JIT 和非 JIT 函数——在 JAX 中非常简单,但在 PyTorch/XLA 中可能需要一些创造性处理。

如上所述,理论上 PyTorch/XLA 和 TensorFlow 可以生成一个与 JAX 创建的 XLA(HLO)图相同的图(因此也能实现相同的性能)。然而,实际上,生成的图的质量将取决于框架级实现如何转换为 XLA。更优的转换最终将导致更好的运行时性能。由于 JAX 本身对 XLA 的原生支持,它可能相对于其他框架具有优势。

支持 XLA 设备

JAX 的 XLA 兼容性使其特别吸引那些开发专用 AI 加速器的开发者,例如 Google Cloud TPUIntel GaudiAWS Trainium 芯片,这些芯片通常被称为“XLA 设备”。特别是那些在 TPU 上进行训练的团队,可能会发现 JAX 的支持生态系统比 PyTorch/XLA 更加先进。

高级功能

近年来,JAX 在发布一些先进的训练功能方面,比其他框架早了很多。例如,SPMD,这是一种先进的设备并行技术,提供了最先进的模型分片机会,几年前就在 JAX 中推出,最近才被引入到PyTorch。另一个例子是Palas,它(终于)使得可以为 XLA 设备构建自定义内核。

开源模型

随着 JAX 框架日益流行,越来越多的开源 AI 模型开始以 JAX 的形式发布。一些经典的例子包括谷歌开源的MaxText(大语言模型)和AlphaFold v2(蛋白质结构预测)模型。要充分利用这些模型,你需要学习 JAX,或者承担将其移植到其他语言的复杂任务。

我们坚信,这些考虑因素值得将 JAX 纳入任何机器学习开发工具包。

总结

本文我们探讨了崭露头角的 JAX 机器学习开发框架。我们描述了它依赖于 XLA 编译器,并展示了在一个简单例子中的使用。尽管 PyTorch 的 JIT 编译 API(torch.compilePyTorch/XLA)也支持类似的性能优化潜力,但 JAX 通常因其快速的运行时执行而被提及。每种选项的相对性能将大大依赖于模型的细节和运行时环境。

重要的是,每个机器学习(ML)开发框架选项可能具有独特的功能(例如,JAX 中的 SPMD 自动分片和 PyTorch 中的 SDPA 注意力——截至本文写作时),这些功能可能对比较运行时性能产生决定性影响。因此,框架的最佳选择可能取决于你的模型能从这些功能中获益的程度。

总之,正如我们在许多之前的文章中强调的那样,要在不断发展的机器学习开发领域中保持相关性,就需要紧跟最新的工具和技术,包括 JAX 机器学习开发框架。

AI 模型有过期日期——持续学习可能是答案

原文:towardsdatascience.com/ai-models-have-expiry-date-9a6e2c9c0a9f?source=collection_archive---------5-----------------------#2024-07-26

为什么在这个唯一不变的就是变化的世界里,我们需要一种持续学习的方法来处理 AI 模型。

Alicja DobrzenieckaTowards Data Science Alicja Dobrzeniecka

·发布于 Towards Data Science ·阅读时长 7 分钟·2024 年 7 月 26 日

--

图片由作者在 Midjourney 生成

想象一下,你有一个小型机器人,它被设计用来在你的花园里走动并浇水。最初,你花费了几周的时间收集数据来训练和测试机器人,投入了相当多的时间和资源。当花园的地面覆盖着草和裸土时,机器人学会了高效地导航。

然而,随着时间的推移,花朵开始盛开,花园的外观发生了显著变化。这个根据不同季节的数据训练出来的机器人,现在无法准确识别周围的环境,并且难以完成任务。为了修复这个问题,你需要将盛开的花园的新例子添加到模型中。

你最初的想法是向训练中添加新的数据样本,并重新从头开始训练模型。但这是昂贵的,而且你不想每次环境变化时都这样做。此外,你刚刚意识到,你没有所有历史训练数据可用。

现在你考虑只用新样本微调模型。但这样做是有风险的,因为模型可能会失去之前学到的一些能力,从而导致灾难性遗忘(即模型在学习新信息时,失去之前获得的知识和技能)。

..那么有没有替代方法呢?有的,使用持续学习!

当然,机器人在花园里浇水只是问题的一个示例。在接下来的部分,你将看到更现实的应用场景。

使用持续学习(CL)进行适应性学习

无法预见并为模型未来可能遇到的所有场景做好准备。因此,在许多情况下,随着新样本的到来,对模型进行自适应训练可能是一个不错的选择。

在持续学习(CL)中,我们希望找到模型稳定性可塑性之间的平衡。稳定性是指模型保持之前学习的信息的能力,而可塑性是指模型随着新任务的引入能够适应新信息的能力。

“(…) 在持续学习场景中,需要一个学习模型能够随着任务分布在其生命周期中动态变化,逐步建立和动态更新内部表示。” [2]

那么,如何控制稳定性与可塑性呢?

研究人员已经确定了多种构建自适应模型的方法。在[3]中,已建立了以下几类方法:

  1. 基于正则化的方法
  • 在这种方法中,我们添加一个正则化项,用以平衡新任务与旧任务对模型结构的影响。

  • 例如,权重正则化旨在通过向损失函数添加一个惩罚项来控制参数的变化,该惩罚项会根据参数对之前任务的贡献程度惩罚其变化。

2. 基于重放的方法

  • 这一组方法的重点是恢复一些历史数据,使得模型仍然能够可靠地解决以前的任务。该方法的一个局限性是我们需要访问历史数据,而这并非总是可能的。

  • 例如,经验重放,即我们保存并重放一部分旧的训练数据。在训练新任务时,添加一些来自旧任务的样本,以将模型暴露于旧任务与新任务类型的混合,从而限制灾难性遗忘。

3. 基于优化的方法

  • 在这里,我们希望调整优化方法,以保持所有任务的性能,同时减少灾难性遗忘的影响。

  • 例如,梯度投影是一种方法,其中计算的新任务的梯度会被投影,以避免影响先前的梯度。

4. 基于表示的方法

  • 这一组方法的重点是获得并使用强健的特征表示,以避免灾难性遗忘。

  • 例如,自监督学习,即模型可以在专门训练特定任务之前,学习数据的强健表示。其理念是学习能够反映良好泛化能力的高质量特征,以应对模型未来可能遇到的不同任务。

5. 基于架构的方法

  • 之前的方法假设使用单一模型和单一参数空间,但在持续学习中,也有许多技术可以利用模型的架构。

  • 例如,参数分配, 在训练过程中,每个新任务都会在网络中分配一个专用的子空间,这解决了参数冲突干扰的问题。然而,如果网络不是固定的,其规模会随着新任务数量的增加而增长。

那么,如何评估 CL 模型的性能呢?

CL 模型的基本性能可以从多个角度来衡量[3]:

  • 总体性能评估: 所有任务的平均性能

  • 记忆稳定性评估: 计算给定任务在持续训练前的最大性能与当前性能之间的差异

  • 学习可塑性评估: 衡量联合训练性能(如果在所有数据上训练)与使用 CL 训练时的性能差异

那么,为什么并不是所有 AI 研究人员都立刻转向持续学习呢?

如果你可以访问历史训练数据并且不担心计算成本,那么从头开始训练似乎更容易。

其中一个原因是,模型在持续训练过程中发生的事情的可解释性仍然有限。如果从头开始训练能够得到与持续训练相同或更好的结果,那么人们可能更倾向于选择更简单的方法,即从头开始重新训练,而不是花时间去理解 CL 方法的性能问题。

此外,当前的研究往往集中在模型和框架的评估上,这可能无法很好地反映出商业可能面临的实际使用案例。正如[6]中所提到的,有许多合成增量基准测试并没有很好地反映出现实世界中任务自然演变的情况。

最后,正如在[4]中所指出的,许多关于 CL 的论文关注的是存储问题而非计算成本,实际上,存储历史数据比重新训练模型要便宜且耗能更少。

如果更多地关注模型重新训练中计算和环境成本的包含,可能会有更多的人对改进当前的 CL 方法感兴趣,因为他们会看到可衡量的收益。例如,正如[4]中提到的,模型重新训练可能超过10,000 GPU 天的训练时间,尤其是在最近的大型模型中。

为什么我们需要致力于改进 CL 模型?

持续学习旨在解决当前 AI 模型最具挑战性的瓶颈之一——数据分布随时间变化的事实。重新训练成本高,且需要大量计算,这在经济和环境角度来看并不是一种可持续的方法。因此,在未来,发展完善的 CL 方法可能使得模型变得更加可接入且可重用,供更广泛的社区使用。

正如在[4]中发现并总结的那样,有一系列应用天生需要或能够从完善的 CL 方法中受益:

  1. 模型编辑
  • 在不破坏模型其他部分的情况下,选择性地编辑模型中易出错的部分。持续学习技术可以帮助我们在较低的计算成本下持续纠正模型错误。

2. 个性化与专业化

  • 通用模型有时需要进行调整,以便更好地个性化满足特定用户的需求。通过持续学习,我们可以只更新一小部分参数,而不会引入灾难性遗忘。

3. 设备端学习

  • 小型设备具有有限的内存和计算资源,因此能够在新数据到达时有效地实时训练模型,而无需从头开始的方法,在这一领域可能会非常有用。

4. 使用热启动进行更快速的再训练

  • 当新的样本可用或分布发生显著变化时,模型需要进行更新。通过持续学习,这一过程可以更高效地进行,只更新受新样本影响的部分,而不是从头开始重新训练。

5. 强化学习

  • 强化学习涉及智能体与通常是非静态的环境进行交互。因此,效率较高的持续学习方法和策略可能对这种使用场景有很大帮助。

了解更多

如你所见,持续学习方法领域仍然有很大的提升空间。如果你感兴趣,可以从以下材料开始:

  • 入门课程:【持续学习课程】第 1 讲:介绍与动机 来自 ContinualAI 在 YouTube 上的视频 youtu.be/z9DDg2CJjeE?si=j57_qLNmpRWcmXtP

  • 讲述持续学习动机的论文:持续学习:应用与前进的道路 [4]

  • 讲述持续学习最新技术的论文:持续学习的综合调查:理论、方法与应用 [3]

如果你有任何问题或评论,请随时在评论区分享。

干杯!

图片由作者在 Midjourney 生成

参考文献

[1] Awasthi, A., & Sarawagi, S. (2019). 使用神经网络的持续学习:综述。发表于 ACM 印度联合国际数据科学与数据管理会议论文集(第 362-365 页)。计算机协会。

[2] Continual AI Wiki 持续学习简介 wiki.continualai.org/the-continualai-wiki/introduction-to-continual-learning

[3] Wang, L., Zhang, X., Su, H., & Zhu, J. (2024). 持续学习的综合调查:理论、方法与应用。IEEE 模式分析与机器智能学报, 46(8), 5362–5383。

[4] Eli Verwimp, Rahaf Aljundi, Shai Ben-David, Matthias Bethge, Andrea Cossu, Alexander Gepperth, Tyler L. Hayes, Eyke Hüllermeier, Christopher Kanan, Dhireesha Kudithipudi, Christoph H. Lampert, Martin Mundt, Razvan Pascanu, Adrian Popescu, Andreas S. Tolias, Joost van de Weijer, Bing Liu, Vincenzo Lomonaco, Tinne Tuytelaars, & Gido M. van de Ven. (2024). 持续学习:应用与未来发展之路 arxiv.org/abs/2311.11908

[5] Awasthi, A., & Sarawagi, S. (2019). 神经网络的持续学习:综述。收录于 ACM 印度联合国际数据科学与数据管理会议论文集(第 362–365 页)。计算机协会。

[6] Saurabh Garg, Mehrdad Farajtabar, Hadi Pouransari, Raviteja Vemulapalli, Sachin Mehta, Oncel Tuzel, Vaishaal Shankar, & Fartash Faghri. (2024). TiC-CLIP:CLIP 模型的持续训练。

AI,我的假日精灵:为完美圣诞打造礼物推荐系统

原文:towardsdatascience.com/ai-my-holiday-elf-building-a-gift-recommender-for-the-perfect-christmas-caf163d38e10?source=collection_archive---------0-----------------------#2024-12-08

我是如何使用 AI 和 Streamlit 创建一个节日气氛浓厚、充满趣味的礼物推荐应用的

Shuqing KeTowards Data Science Shuqing Ke

·发布于 Towards Data Science ·阅读时间 9 分钟·2024 年 12 月 8 日

--

圣诞节图像由作者提供

假日季节来临——是时候点亮灯光、喝热红酒、吃油条、以及购物啦!但是说实话,寻找独特而富有心意的礼物很快就会变成一项繁重的任务。厌倦了那些陈词滥调的建议,比如给男士买香水、给孩子买普通玩具,我决定在我的礼物购物体验中加入一些 AI 魔力。

在这篇文章中,我将带领你了解如何使用 AI 和 Streamlit 创建一个有趣的个性化礼物推荐系统——完美的节省时间并传播节日欢乐的方式!项目链接将在文章结尾提供。

目录

  1. 圣诞老人备忘单:2024 年最佳礼物推荐

  2. 教导 AI 你的愿望清单

  3. 个性化推荐的魔力

  4. 你的礼物推荐精灵:Streamlit 实战

  5. 协同过滤:朋友们最了解你

  6. 挑战

  7. 总结

圣诞老人备忘单:2024 年最佳礼物推荐

基于 AI 的工业设备腐蚀检测:AWS 可扩展方法

原文:towardsdatascience.com/ai-powered-corrosion-detection-for-industrial-equipment-a-scalable-approach-with-aws-b70da644861e?source=collection_archive---------6-----------------------#2024-10-05

完整的 AWS 机器学习解决方案,结合 SageMaker、Lambda 和 API Gateway

Aashish NairTowards Data Science Aashish Nair

·发布于 Towards Data Science ·6 分钟阅读·2024 年 10 月 5 日

--

照片由 Monstera Production 提供:www.pexels.com/photo/textured-background-of-metal-lattice-against-brick-wall-7794453/

介绍

制造业、能源和电信等行业需要进行广泛的质量控制,以确保设备保持正常运行。大多数组件面临的一个持续性问题是腐蚀:由于环境因素引起的金属逐渐降解。如果不加以控制,腐蚀可能导致健康危害、机械停机和基础设施故障。

本项目展示了一种完全自动化腐蚀检测过程的方法,利用了云计算技术。具体来说,它使用 Amazon Sagemaker、Lambda 和 API Gateway 构建了一个可扩展、高效且具有容错能力的质量控制解决方案。

数据

本项目的数据来源于 Synthetic Corrosion Dataset(CC BY 4.0),该数据集包含数百张合成图像。每张图像被分类为腐蚀或非腐蚀。

数据源提供了分别用于训练、测试和验证的数据集图像,因此无需进行拆分。训练集、验证集和测试集分别包含 270 张、8 张和 14 张图像。

AI 驱动的客户支持应用:基于 PGVector 的语义搜索,搭载 Llama2 的 RAG 系统与先进的翻译模型

原文:towardsdatascience.com/ai-powered-customer-support-app-semantic-search-with-pgvector-llama2-with-an-rag-system-and-fc1eef1738d8?source=collection_archive---------11-----------------------#2024-01-04

在全球市场中提升沟通:利用 PGVector 进行多语言语义搜索,结合 Llama2 驱动的 RAG 系统与先进的翻译模型,优化多语言客户互动

Luís RoqueTowards Data Science Luís Roque

·发布于Towards Data Science ·11 分钟阅读·2024 年 1 月 4 日

--

本文由 Rafael Guedes 与我共同撰写。

介绍

随着组织不断发展,有一件事始终不变:对客户满意度的追求。提升客户体验是建立可持续和成功企业的关键方面之一。将 AI 整合到公司工作流程中,将彻底改变这一领域。它将实现个性化客户服务,帮助企业满足、预见并超越客户期望。早期采用 AI 进行客户服务的公司将获得显著的竞争优势。

想象一下,你正在浏览亚马逊寻找某个特定产品。当你进入该产品的详细页面时,你面临着一个关键任务——决定它是否适合你的需求。为此,你开始筛选成千上万条用不同语言写的客户评价——这是一个既繁琐、又具挑战性、还非常耗时的任务。但想象一下,如果你能使用一个能够…

通过拥抱四个永恒的概念,使你的数据科学技能不受 AI 影响

原文:towardsdatascience.com/ai-proof-your-data-science-skillset-88d222f11e22?source=collection_archive---------4-----------------------#2024-07-09

并保持在竞争激烈的就业市场中具备竞争力

Matthew GazzanoTowards Data Science Matthew Gazzano

·发表于 Towards Data Science ·5 分钟阅读·2024 年 7 月 9 日

--

图片来源:Thomas KelleyUnsplash

随着微软 Copilot、ChatGPT 等 AI 生产力工具的出现,一些技术专业人士对自己技能可能变得过时表示担忧。由于 AI 仍处于初级阶段,没人能够准确预测未来几年数据团队的技能将如何发展。然而,当你考虑到商业智能自 1960 年代以来的变化时,你会发现有一些永恒的人类元素,能够为分析专业人士在可预见的未来提供强大的工作保障。我们来深入探讨一下这些内容:

领域知识

当你仔细思考时,数据科学家从来不是为了编写代码、构建模型或创建仪表盘而被支付的。他们是为了解决问题而获得报酬。这一点随着 AI 的出现并没有改变,但他们用来解决问题的工具却发生了变化。

数据科学职位的候选人竞争优势之一是能够清晰地阐述商业问题的叙事,并提出建议。例如,医疗公司希望候选人能够理解医院与保险公司之间的关系、提供哪些类型的患者服务,以及他们的整体收入周期……

AI 通过伪 DXA 预测你从外部到内部的健康状态

原文:towardsdatascience.com/ai-pseudo-dxa-8e3f83b5d373?source=collection_archive---------6-----------------------#2024-02-01

一个定量准确且临床有用的生成性医学影像模型

Lambert T Leong 博士Towards Data Science Lambert T Leong 博士

·发表于Towards Data Science ·11 分钟阅读·2024 年 2 月 1 日

--

3D 人体表面扫描点云与匹配的双能 X 射线吸收法(DXA)扫描(图片来自作者)

关键点

  1. 据我们所知,这是第一个可以通过商业临床软件分析生成医学影像并具有定量准确性的模型。

  2. 能够从外部形态预测脂肪、肌肉和骨骼的内部分布,表明身体组成与体型之间存在强烈的关系

  3. 这个模型代表了朝着可获得健康监测迈出的重要一步,能够生成通常需要专业昂贵设备、训练有素的技术人员,并且涉及可能有害的电离辐射的图像。

  4. 阅读论文 HERE

生成性人工智能(AI)特别是在扩散模型(如 DALL-E)和大型语言模型(LLM,如 ChatGPT)发布之后,变得极为流行。通常,当模型生成某种输出时,AI 模型被归类为“生成性”。对于 DALL-E,输出产品是高质量的图像,而对于 ChatGPT,输出产品是高度结构化且有意义的文本。这些生成性模型不同于分类模型,后者的输出是…

AI 变形者:AI 工程师与应用数据科学家角色的变化

原文:towardsdatascience.com/ai-shapeshifters-the-changing-role-of-the-ai-engineer-and-applied-data-scientist-3e811b33bead?source=collection_archive---------6-----------------------#2024-08-07

AI 工程师和应用数据科学家的角色经历了显著的变化。那么它将走向何方,我们又该如何准备?

Sandi BesenTowards Data Science Sandi Besen

·发表于Towards Data Science ·阅读时间 4 分钟·2024 年 8 月 7 日

--

在过去的一年里,AI 工程师和应用数据科学家的角色发生了显著的变化。作为一个亲身经历过这些变化的人,我想分享一下它是如何发展的,以及可能的未来趋势。

图片来源:Sandi Besen

提示工程与聊天机器人时代

2023 年,重点主要放在开发基于聊天的解决方案。人类与 AI 之间的典型互动非常直接:提问和回答,或呼叫与响应。这样的互动模式通常如下所示:

  1. 用户任务

  2. 助手回答

  3. 用户任务

  4. 助手回答

  5. (依此类推)

无论是应用数据科学家还是 AI 工程师,都花费了大量时间学习那种变化莫测的提示工程技术,监控幻觉问题,并调整如温度等参数以优化性能。

公司感受到立即采纳 AI 的需求,原因可能是对其能带来的竞争优势的纯粹兴奋,也可能是来自高层管理者和投资者的健康鼓励。然而,现成的模型缺乏对公司流程、领域知识、商业规则和文档的细致理解和把握。检索增强生成(RAG)应运而生,旨在弥补这一空白,并提供一种方法,防止语言模型所需的上下文信息过时。

应用数据科学家的角色从专注于构建定制模型,转变为学习如何从最新的尖端技术中提取最佳性能。

开源转变的影响与行动驱动型 AI 的崛起

当能够与 OpenAI 的 GPT-3.5 竞争的开源模型开始出现时,它打开了可能的技术发展大门。突然之间,构建能够提升语言模型完成任务能力的工具,变得更加灵活且可见。

像 Semantic Kernel、Autogen 和 LangChain 这样的模型编排库开始得到广泛应用,AI 工程师的角色也随之扩展。为了充分利用这些开发者工具,更多的开发技能、面向对象编程的熟练度,以及将 AI 解决方案扩展到业务流程中的能力变得至关重要。

当 AI 开始与外部系统互动时,游戏规则真正发生了变化。2022 年,模块化推理、知识和语言(MRKL)系统被引入。这一系统首次结合了语言模型、外部知识源和离散推理——为构建能够采取行动影响外部世界的 AI 系统提供了更多机会。

但到 2023 年,我们有了更正式的工具,比如 ChatGPT 插件、语义函数以及其他可以被语言模型调用和使用的工具。这开辟了一个全新的可能性维度,并将应用数据科学家和 AI 工程师的角色转向更加注重开发。这意味着他们现在不仅负责 AI 模型的内部运作,还需要编写代码,使模型能够与内部系统互动并执行实际的操作。

代理型 AI 的崛起

工具调用的引入标志着从基于聊天的系统向基于行动的系统的过渡,这很快发展成了代理型 AI 的崛起。

代理型 AI 提供了新的可能性,扩展了语言模型的能力,进而扩展了 AI 工程师的角色。复杂性大幅增加,从从一个模型中提取最佳输出,转向一个由多个模型协同工作的团队。更多的选择带来了设计决策中的更多变化,例如:

  • 如何构建代理团队(代理数量、它们负责的任务等)

  • 它们应该遵循什么样的对话模式

  • 如何为它们提供正确的工具集,以有效地完成任务

  • 如何分解任务,以确保代理能够准确且一致地响应

通常,这类设计选择(系统架构设计)发生在 AI 工程师/数据科学家层级之上,复杂的设计选择由管理层甚至高级管理层处理。然而,成功的代理系统所需的创造性自由度导致了工程师在设计自由和责任方面的下移。

“应用数据科学家/AI 工程师的角色正在变成开发与创造性问题解决的独特融合。实验、批判性思维以及设计可扩展 AI 系统团队所需的创造性思维应该改变公司对 AI 岗位招聘的看法。”

对应用数据科学家和 AI 工程师未来技能的预测

在我看来,AI 工程的未来在于我们快速适应和创造性解决问题的能力。最成功的 AI 工程师将不是那些开发能力最强的人,而是能够:

  • 快速掌握新话题并加以应用

  • 在没有预定义路线图的情况下解决问题

  • 随着新技术的出现,快速转变方向

  • 高效地管理自己的时间

现在是进入这个快速发展的领域的激动人心时刻——但保持领先所需的个人投入不会是心脏脆弱者能够承受的。

注意:本文和论文中表达的观点仅代表我个人意见,不一定反映我雇主的观点或政策。

有兴趣开始对话吗?通过Linkedin给我发个私信!我一直很乐意参与深思熟虑的讨论,并在我的工作上进行反复推敲。

简化的 AI 训练:基本数学原理解读

原文:towardsdatascience.com/ai-training-simplified-the-essential-mathematics-explained-3a94ebeb4a3e?source=collection_archive---------3-----------------------#2024-07-06

图片来自作者

AI 训练中使用的数学逻辑概述

Kristjan EljandTowards Data Science Kristjan Eljand

·发表于Towards Data Science ·阅读时间 4 分钟·2024 年 7 月 6 日

--

理解事物是如何运作的总是有益的。在本文中,我将提供一个非常简单的概述,介绍训练 AI 模型时使用的基本数学逻辑。我保证,如果你有基本的教育背景,以下示例将是可以理解的,并且你将对人工智能领域有稍微更深入的了解。

创建用于销售预测的 AI

假设我们想创建一个新的 AI 模型来预测公司销售收入。我们有过去两个月的销售收入数据、广告费用和产品价格。

插图来自作者

换句话说,我们想创建一个模型,告诉我们销售收入如何依赖于产品价格和广告支出。使用这样的工具,营销专家可以例如计算出,如果他们花费 50 欧元在广告上并将产品价格定为 6 欧元,预计的销售收入是多少。

AI 作为一个数学公式

本质上,AI 不过是一个数学公式(或一组公式)。我们的销售预测示例可以用如下数学公式表示:

插图来自作者

公式是存在的,但我们不知道应该为模型的参数 m 和 n 分配什么值。换句话说,我们不知道增加广告费用和调整产品价格如何影响我们的销售收入。

开始学习

当我们开始训练人工智能时,我们可以将随机值分配给模型的参数。例如,我们最初将广告费用参数设置为 2,将价格参数设置为-2。

作者插图

现在,我们简单地进行试验。如果我们将广告费用和产品价格分别乘以其相应的参数值,我们可以看到,最初的模型过于乐观。在第一个月,实际的销售收入是 5 欧元,而我们的模型预测了 30 欧元。在第二个月,实际的销售收入是 18 欧元,而我们的模型预测了 52 欧元。

作者插图

学习规则

如果误差为 0,模型是完美的,不需要调整。

如果误差 > 0,说明模型给出了过于乐观的结果:

  • 如果对应的输入特征(例如广告费用或产品价格)具有正值,则减少权重(参数)。

  • 如果对应的输入特征具有负值,则增加权重(参数)。

如果误差 < 0,说明模型过于悲观:

  • 如果对应的输入特征具有正值,则增加权重(参数)。

  • 如果对应的输入特征具有负值,则减少权重(参数)。

根据学习规则,我们需要减少这两个参数,因为广告费用和产品价格都具有正值 例如,我们将广告费用的权重从 2 减少到 1,将价格参数从-2 减少到-3。

作者插图

如果我们重新计算,我们可以看到,模型现在预测得很准确。太好了,我们的第一个手动训练的人工智能模型已经准备好了。

作者插图

在未用于训练的数据上测试模型

如果你认为上面的模型好得令人难以置信,那你是对的。我们的模型在训练数据上表现得非常完美。为了评估模型的准确性,必须在未用于训练的数据上进行测试。

我们在 1 月和 2 月的数据上训练了我们的模型。现在,让我们检查模型在预测 3 月和 4 月的销售收入方面的表现如何。

作者插图

从上表中,我们可以看到,模型预测 3 月的销售收入为 28 欧元(实际为 24 欧元),预测 4 月的销售收入为 21 欧元(实际为 18 欧元)。平均而言,我们的模型在新数据上的误差为 3.5 欧元,这就是我们可以称之为模型的准确度。

结论

总结来说,人工智能本质上是一个数学公式。在我们的例子中,这个公式有两个参数;而 GPT-4 模型有超过一万亿个(1 万亿 = 1,000,000,000,000)参数。两者都遵循相同的原则进行训练:逐渐调整模型的参数以减少误差。

还需要记住的是,人工智能是在训练数据上学习的,但其准确性只能通过未在训练中使用的数据(测试数据)来评估。

AI 使用场景本质上是不同的

原文:towardsdatascience.com/ai-use-cases-are-fundamentally-different-3946f089c68e?source=collection_archive---------6-----------------------#2024-05-31

如何为 AI 找到独特的使用场景,以及在中等 AI 性能下仍然有价值的地方。

Elaine LuTowards Data Science Elaine Lu

·发表于Towards Data Science ·阅读时长:7 分钟·2024 年 5 月 31 日

--

作者:

AI 整合的成功在很大程度上取决于选择正确的 AI 使用场景。 这是面向产品技术人员的视角,在深入了解算法、数据和工程之前。

AI 项目经常失败,因为它们实际上是没有启动的,没有解决正确的人的问题,也没有达到最低性能标准以满足用户期望。

AI 就像是软件版的胶带,适用于很多事情,但不是所有事情。同样,伟大的产品帮助人们高效完成特定任务。而 AI 能做很多事情,做得一般般。

这是寻找 AI 机会的案例,这些机会可以通过 AI 独特地解决,即使是中等性能也仍然有价值。

寻找 AI 表现出色的使用场景行不通。它忽略了 AI 固有的概率特性。相反,寻找中等 AI 性能的使用场景,将以较低的风险提供即时的价值。

先前的工作通过不显眼的计算研究了这个话题,Google 的…

AI 与人类洞察力在财务分析中的对比

原文:towardsdatascience.com/ai-vs-human-insight-in-financial-analysis-89d3408eb6d5?source=collection_archive---------6-----------------------#2024-03-15

Bud Light 抵制事件和 SalesForce 创新计划如何让最优秀的语言模型感到困惑

Misho DungarovTowards Data Science Misho Dungarov

·发表于 Towards Data Science ·阅读时间:10 分钟·2024 年 3 月 15 日

--

图片来源:Dall-E 3

今天最优秀的 AI 模型能否准确从公司财报电话会议中提取出最重要的信息?它们确实能捕捉到一些要点,但我们怎么知道这些要点是否真的重要呢?我们能否提示它们做得更好?为了找到这些答案,我们研究了该领域一些最优秀的记者所做的工作,并尝试用 AI 尽可能接近这些水平。

挑战

在本文中,我分析了 8 场最近的公司财报电话会议,并询问了目前最聪明的 AI(Claude 3GPT-4Mistral Large)它们认为哪些内容最重要。然后,将这些结果与一些最顶尖新闻机构(路透社、彭博社和《巴伦周刊》)对这些报告的评价进行比较。

为什么要关注这个?

盈利电话会议的重要性

盈利电话会议是公司高层审视财务结果的季度活动。在会议中,他们会讨论公司业绩,分享评论,有时还会预告未来计划。这些讨论可能会显著影响公司的股价。管理层会解释他们对未来的预期,以及为什么能够实现或超越之前的预测。管理团队提供了关于公司实际状况和未来方向的宝贵见解。

自动化在盈利分析中的力量

Statista 报告称,根据一项估算,纳斯达克上市公司数量接近4000 家,而全球约有58,000 家公司

一场典型的电话会议大约持续 1 小时。要听完所有纳斯达克公司的财报电话会议,至少需要 10 个人在整个季度内全职工作。而且这还不包括分析和比较财务报告等更耗时的任务。

大型经纪公司可能能够处理这种工作量,但对个人投资者来说是不现实的。在这个领域的自动化可以使竞争环境更加平等,让每个人都更容易理解季度财报。

虽然这可能只对大型经纪公司而言是触手可及的,但对于私人投资者来说并不可行。因此,在这个领域的任何可靠自动化都将是一大福音,尤其是在让更多人理解季度财报方面。

测试 AI 作为金融分析师的过程

为了测试当今最好的 LLM 是否能够胜任这项工作,我决定比较人类的主要要点,并查看 AI 如何模仿这些要点。以下是步骤:

  1. 挑选一些有近期财报电话会议记录和匹配新闻文章的公司。

  2. 给 LLM 提供完整的会议记录作为上下文,并要求它们提供三个最具影响力的要点,这些要点对公司价值最为重要。这很重要,因为提供一个较长的摘要变得越来越容易——毕竟,重要的事情也就那么多。

  3. 为了确保我们最大化输出质量,我会变化我向 AI 提出问题的方式(使用不同的提示):从简单地请求摘要、添加更详细的指示、添加先前的转录内容到这些组合方式。

  4. 最后,将这些要点与相应新闻文章中的 3 个最重要的要点进行比较,并用重叠部分作为衡量成功的标准。

结果总结

GPT-4 在提供上一季度的会议记录并使用一套分析记录的指示(链式思维)时表现最好,准确度达到 80%。值得注意的是,仅使用正确的指示,就能将 GPT-4 的表现从 51%提高到 75%。

GPT-4 表现最好,对提示反应最好(80%)——即添加先前的结果和专门的分析指示。而在没有复杂提示的情况下,Claude 3 Opus 表现最好(67%)。图像和数据由作者提供

  • 排名第二的表现较好的公司是:

    — Claude 3 Opus(67%)——在没有复杂提示的情况下,Claude 3 Opus 表现最佳。

    — Mistral Large(66%)在加入支持性指示(即链式思维)后

  • 链式思维(CoT)和逐步思考(SxS)对 GPT-4 效果最好,但对其他模型则有害。这表明我们仍然需要更多地了解哪些提示适用于每个 LLM。

  • Chain-of-Thought (CoT) 几乎总是优于 Step-by-step (SxS)。这意味着,定制的财务分析优先知识有所帮助。具体的指示已列在文章的底部。

  • 更多数据——更少意义:将先前一段的转录文本添加到模型上下文中,似乎至少对整体结果略有不利,最坏的情况下则可能显著降低结果,尤其是在专注于最新结果的情况下(除了 GPT-4 + CoT)。可能是因为从先前的转录文本中引入了大量不相关的信息,而引入了相对较少的具体事实来进行季度比较。Mistral Large 的表现显著下降,注意它的上下文窗口仅为 32k 个令牌,而其他模型的上下文窗口显著更大(2 个转录文本 + 提示词几乎刚好适应 32k 个令牌)。

  • Claude-3 Opus 和 Sonnet 的表现非常接近,在某些情况下,Sonnet 实际上超过了 Opus。然而,这种差异通常只有几个百分点,因此可以归因于结果的随机性。

  • 请注意,正如前面提到的,结果显示出高度的可变性,结果范围在 +/-6% 之间。因此,我重新运行了所有分析 3 次,并显示了平均值。然而, +/-6% 的范围不足以显著推翻上述结论。

LLMs 做对了什么,做错了什么?

Bud Light Boycott 和 Salesforce 的 AI 计划如何让最好的 AI 感到困惑

这项任务提供了一些简单的机会:猜测结果大约是最新的收入数字和明年的预测,这通常是相当准确的。不出所料,这也是模型最常做对的地方。

下表概述了新闻中提到的内容以及 LLMs 在仅用几个词进行总结时选择的不同内容。

“用最多 3 个词总结每个要点”:新闻中的前三个主题与 LLMs 选择的主题(这些主题不在该列表中)进行对比。每个模型被要求提供 2–3 个词的要点总结。每个模型将有 6 组前三选择(即 24 个),这 3 个选择通常是与新闻摘要相比最不相关的。请注意,在某些情况下,对比顶部和底部的表格可能感觉两个总结相似,这主要是因为每个要点实际上更为详细,且在 2–3 个词的总结中可能遗漏了大量附加/矛盾的信息。

接下来,我尝试寻找模型一致遗漏的趋势。这些趋势通常属于几个类别:

  • 理解变化: 在以上结果中,LLMs 能够相对可靠地识别需要关注的内容:盈利、销售、股息和指导,但理解哪些内容具有重要意义依然非常困难。例如,常识可能认为 2023 年第四季度的财报结果会是任何公司的关键话题,而 LLMs 也确实选中了这一点。然而,Nordstrom 讨论了 2024 年较低的收入和需求预期,这使得 2023 年第四季度的财报在重要性上被搁置。

  • 幻觉: 如同广泛记录的那样,大语言模型倾向于编造事实。在这种情况下,尽管有指示要求“只包括上下文中的事实和数据”,但一些指标和日期最终还是被编造出来。不幸的是,这些模型并不回避谈论 2024 年第四季度的财报,并称其已公布,且使用了 2023 年的数据。

  • 重大的单次事件: 出乎意料的单次事件往往被大语言模型(LLMs)忽视。例如,百威啤酒的抵制导致美国最畅销啤酒的销量下降了 15.9%,对安海斯·布希公司影响深远,并在记录中有详细讨论。仅这一数字就应该显得非常重要,但所有样本中的模型都忽略了这一点。

  • 行动胜于言辞: GPT 和 Claude 都强调创新和对 AI 的承诺为重要内容。

    — Salesforce(CRM)详细讨论了公司在 AI 和数据云方面的重视。

    — Snowflake 任命了他们的 AI 高级副总裁和前谷歌广告高管(Sridhar Ramaswamy)为 CEO,同样表明了公司致力于利用 AI 技术。

    两者都表明公司正在转向创新与 AI。然而,记者和分析师不会轻易被言辞所误导,将其当作行动。 在分析 CRM 财报的文章中,副标题写道“Salesforce 前景失望,AI 未能激发增长”。然而,Salesforce 已经在尝试与 AI 合作一段时间,且其未来使用 AI 的计划甚至没有被提及。Salesforce 的记录提到 AI91 次,而 Snowflake 的只有不到一半,只有 39 次。然而,人类可以辨别其中的含义:彭博社的文章[链接]讨论了 Snowflake 任命新 CEO 的消息:他的升职强调了 Snowflake 对 AI 的关注。

实验设计与选择

  1. 为什么选择财报电话会议记录? 更直观的选择可能是公司文件,但我发现电话会议记录提供了一个更加自然、少形式化的事件讨论。我认为,记录能让 LLM 作为推理引擎,更好地提取事件的自然评论,而不像财报那样枯燥且高度规范化。电话会议大多是管理层的陈述,可能会将事物偏向于更积极的观点。然而,我的分析表明,在积极和消极的叙述中,LLMs 的表现似乎相似。

  2. 公司选择: 我选择了在 2023 年 2 月 25 日到 3 月 5 日之间发布了第四季度财报,并且已被路透社、彭博社或《巴伦周刊》报道的股票。这确保了结果是及时的,并且模型尚未在这些数据上进行过训练。此外,每个人总是谈论 AAPL 和 TSLA,因此这是一个不同的选择。最后,这些新闻机构的声誉确保了有意义的比较。我们最终选定的 8 只股票是:Autodesk (ADSK), BestBuy (BBY), Anheuser-Busch InBev (BUD), Salesforce (CRM), DocuSign (DOCU), Nordstrom (JWN), Kroger (KR), Snowflake (SNOW)

  3. 结果的可变性 LLM 的结果在不同的运行之间可能有所不同,因此我进行了三次实验并展示了平均值。所有模型的分析均使用温度值 0,这通常用于最小化结果的变化。在这种情况下,我观察到不同运行之间的性能差异最多可达 10%。这是由于样本量较小(仅 24 个数据点,每只股票 3 条声明),并且我们基本上要求 LLM 从众多可能的声明中选择一个摘要,因此,当这种选择具有一定随机性时,自然会导致选择的差异。

  4. 提示词选择: 对于三种比较的 LLM,我尝试了四种不同的提示方法:

  • 简单模型 — 提示仅要求模型确定对股价最可能的影响。

  • 思维链(CoT) — 我提供一个详细的步骤列表,在选择摘要时遵循这些步骤。这一方法的灵感来源并且大致遵循[Wei 等人 2022]的工作,提出思维链方法,提示中提供推理步骤能够显著提高结果。在本实验的上下文中,这些附加指令包括影响价格波动的典型因素:预期收入、成本、收益、诉讼等方面的变化。

  • 逐步(SxS) 又名零-shot Chain-of-Thought(CoT),灵感来源于Kojima 等人(2022),他们发现仅仅加入“让我们逐步思考”这一短语就能提高性能。我要求大型语言模型(LLMs)逐步思考并在回答之前描述他们的逻辑。

  • 先前的记录 — 最后,我再次运行以上三种提示方法,并包含上一季度的记录(在本例中为 Q3)。

结论

从上面的内容来看,记者和研究分析师的工作似乎暂时是安全的,因为大多数 LLMs 在正确回答三分之二的问题上存在困难。在大多数情况下,这仅意味着他们猜测会议讨论的是最新的收入和明年的预测。

然而,尽管该测试存在所有局限性,我们仍然可以得出一些明确的结论:

  • 对于大多数模型来说,准确率相当低。即使是 GPT-4 的最佳表现 80%,在没有人工监督的情况下也会面临大规模应用的挑战——每五次中就有一次给出错误建议,这样的表现并不具有说服力。

  • GPT-4 似乎在其未专门训练的复杂任务中仍然是一个明确的领导者。

  • 正确地进行任务提示工程会带来显著的收益

  • 大多数模型似乎容易被额外的信息所困惑,因为加入之前的会议记录通常会降低性能。

接下来该怎么做?

我们都已经见证了 LLM 能力的持续提升。这一差距是否能够弥合,又该如何弥合?我们观察到三种影响性能的认知问题:幻觉、理解什么是重要的而什么不重要(例如,真正理解什么对一家公司来说是令人惊讶的),更复杂的公司因果关系问题(例如,像 Bud Light 抵制事件,以及美国销售对整体业务的相对重要性):

  • 幻觉或 LLM 无法正确再现事实信息的情况,是要求严格遵守事实准确性的应用中的一大障碍。先进的 RAG 方法结合该领域的研究不断取得进展,[Huang et al 2023]概述了当前的进展

  • 理解什么是重要的 —— 针对特定应用场景对 LLM 模型进行微调应该会带来一定的改进。然而,这些改进伴随着对团队、成本、数据和基础设施的更大要求。

  • 复杂的因果关系链 —— 这一方向可能是 AI 代理的一个良好发展方向。例如,在 Bud Light 抵制事件中,模型可能需要:

    1. Bud Light 对美国销售的重要性,可能在许多演示和管理层评论中有所提及

    2. 美国销售对整体公司的重要性,可以从公司财务中获取信息

    3. 最后将这些影响与所有其他提到的影响进行叠加

    这种因果逻辑更类似于 ReAct AI 代理的思考方式,而不是仅仅一个独立的 LLM[Yao, et al 2022]。代理规划是一个热门的研究课题[Chen, et al 2024]

在 LinkedIn 关注我

免责声明

本文中表达的观点、意见和结论仅代表我个人的看法,并不反映文中提到的任何实体或其他实体的观点或立场。

没有使用数据进行模型训练,也没有系统地从提到的来源收集数据,所有技术都仅限于提示工程。

资源

盈利电话会议记录(Motley Fool)

新闻文章

AI 天气模型现在超越传统预报

原文:towardsdatascience.com/ai-weather-models-now-outperform-traditional-forecasts-2ad6e33d4346?source=collection_archive---------4-----------------------#2024-03-11

它们是如何被评估的,这对未来的天气预报意味着什么?

Caroline ArnoldTowards Data Science Caroline Arnold

·发表于Towards Data Science ·阅读时间 7 分钟·2024 年 3 月 11 日

--

图片由作者使用Midjourney创作。

你是否曾查看过周末的天气预报并做了计划,却被雨水打乱了安排?那么你可能会对天气预报的可靠性产生疑问。

预测天气是困难的。大气是一个充满不确定性的混沌系统。传统的天气模型需要复杂的数值方法来预测大气的未来状态。

在过去五十年中,气象服务已经建立了一个包含重分析数据的天气档案。这个档案在卫星观测、地面气象站和数值天气预报的推动下,尽可能地代表了我们对过去天气的了解。

天气预报中的 AI 革命

五年前,天气预报中的 AI 革命开始了。研究人员使用过去 50 年的重分析天气档案,开发了AI 模型来预测天气。

这些模型通过训练数据学习天气模式。在推断时,它们获取当前大气状态并输出未来的状态。这可以反复应用,允许模型进行…

AI 不会抢走你的编程工作(但它会改变它)

原文:towardsdatascience.com/ai-wont-steal-your-coding-job-but-it-will-change-it-5b26c117aa77?source=collection_archive---------7-----------------------#2024-03-24

人工智能

AI 编程革命:Devin 会改变一切吗?

Amine RajiTowards Data Science Amine Raji

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

--

头条新闻大声喊道:“AI 正在夺走你的编程工作!”随着像 Devin AI 这样的工具的新闻传播,空气中充满了恐慌——这是一款能够独立构建整个软件应用的 AI。

就是这样吗?这是机器最终超越人类开发者的时刻吗?我们的辛勤付出和技能会变得过时吗?

Ben White拍摄,图片来源于Unsplash

剧透:不会

虽然像 Devin 这样的 AI 助手无疑具有破坏性,但它们更可能成为强大的协作伙伴,为开发者提供提升的机会,而不是被抛在后头。

让我来解释一下。

我自己最初对 AI 在编程中的角色持怀疑态度。直到我开始尝试代码生成工具和 AI 编程助手,并真正意识到它们的潜力。

虽然它们不会为你编写所有的代码,但它们确实能够简化你的工作流程,腾出时间来处理那些真正复杂且令人满足的工作内容。

Devin 及其同行代表了 AI 驱动的软件开发的重大飞跃。然而,这并不是一个机器人控制软件工程师工作的反乌托邦场景……

人工智能,写作并塑造我的简历

原文:towardsdatascience.com/ai-write-and-style-my-cv-fb3168a5b10e?source=collection_archive---------7-----------------------#2024-07-18

由作者通过 Midjourney 创建

代理工作流写作结构化文档,逐行教程

Anders OhrnTowards Data Science Anders Ohrn

·发布于面向数据科学 ·阅读时间 22 分钟·2024 年 7 月 18 日

--

不仅是蒙娜丽莎维特鲁威人,还有简历,都是由列奥纳多·达·芬奇创作的文化遗物,它们在当代依然具有共鸣并不断复生。简历并非唯一的自我推销方式。尽管自达·芬奇将自己的技能和能力以书面形式列举给米兰公爵以来,信息和图形技术已有诸多创新,但简历依然存在。

从高层次来看,创建简历:

  1. 总结一个人在文档中过去的成就和经验,

  2. 以一种与特定受众相关的方式,后者在短时间内评估一个人对某个目标的相对和绝对效用,

  3. 文档的样式和布局被选择为有利于该受众的积极评估。

这些是为服务于一个目标而进行的语义操作,并且是在模糊规定的约束条件下进行的。

大型语言模型(LLMs)是执行语义操作的主要工具,尤其是当这些操作像人类沟通中常见的那样模糊不清时。迄今为止,与 LLMs 互动的最常见方式是通过聊天应用——ChatGPTClaudeLe Chat等。我们这些聊天应用的用户,通过我们的聊天消息松散地定义了这些语义操作。

然而,某些应用程序最好通过不同的界面和创建语义操作的不同方式来提供服务。聊天并不是大语言模型的终极解决方案。

我将使用Anthropic 大语言模型的 API(尤其是 Sonnet 和 Haiku)来创建一个简历生成的基本应用程序。它依赖于代理协同工作(一个代理工作流程),每个代理执行某些语义操作,将个人数据和历史从一团混乱的资料转化为一个有条理的简历文件,值得它那位伟大祖先的赞誉……

这是一个关于构建一个小巧但完整的、由大语言模型(LLM)驱动的非聊天型应用程序的教程。在接下来的内容中,我将描述代码、我选择特定设计的原因以及每段 Python 代码在更大框架中的作用。

简历创建应用程序是一个很好的例子,展示了人工智能在结构化风格内容生成这一一般任务中的工作原理。

在代码与方法之前——展示内容与惊叹

想象一下,个人数据和冗长的职业描述,大部分是文本,组织成几个文件,信息零散。在这组文件中,就包含了简历的原材料。只是,它需要付出一些努力,才能从中分辨出相关与无关的信息,提炼和精炼它,并赋予它一个美观和令人愉悦的形式。

接下来,想象一下运行脚本 make_cv,并将它指向一个职位广告、一份简历模板、一位求职者和一些规格参数:

make_cv --job-ad-company "epic resolution index" \
        --job-ad-title "luxury retail lighting specialist" \
        --cv-template two_columns_abt_0 \
        --person-name "gregor samsa" \
        --output-file ../generated_output/cv_nice_two_columns.html \
        --n-words-employment 50 --n-skills 8 --n-words-about-me 40

然后等待几秒钟,数据会被打乱、转换和渲染,之后脚本会输出一个整洁、风格化且填充完毕的单页双栏简历。

一份简历,整洁的风格和抽象的内容,通过安索普公司大语言模型的代理工作流程生成。

很棒!绿色调的简约布局和风格,文本与背景之间的对比鲜明,不仅是平淡的默认字体,内容简洁且直截了当。

但等等……这些文档不是应该让我们脱颖而出吗?

再次借助安索普公司大语言模型的帮助,创建一个不同的模板(关键词:1990 年代初期网页设计的狂野与古怪世界),并赋予相同内容一种新的辉煌形式:

一份简历,采用疯狂的 1990 年代网页风格和内容,通过安索普公司(Anthropic)大语言模型的代理工作流程生成。

如果你忽略那些炫目的动画和奇特的颜色选择,你会发现内容和布局几乎与之前的简历一模一样。这并非巧合。代理工作流程的生成任务分别处理内容、形式和风格,而不是依赖一体化解决方案。工作流程的过程反映了标准简历的模块化结构。

即,代理工作流的生成过程在有意义的约束条件下运行。这可以增强生成式 AI 应用的实际效用——毕竟,设计被认为在很大程度上依赖于约束。例如,品牌、风格指南和信息层次结构是我们希望在生成 AI 的非聊天输出中看到的有用且原则性的约束——无论是简历、报告、用户体验、产品包装等。

完成所有工作的代理工作流如下所示。

应用程序的高层数据流图

如果你希望跳过代码和软件设计的描述,格雷戈尔·萨姆萨是你的指路明灯。当我重新回到讨论应用和输出时,我将针对虚构人物格雷戈尔·萨姆萨的合成数据进行讨论,因此可以通过关键词搜索继续往下阅读。

完整的代码可以在这个 GitHub 仓库中找到,免费提供并不附带任何保证。

职位广告预处理、DAO 和提示组装

常说应当根据职位广告的内容调整简历。由于职位广告常常冗长,有时包含法律术语和联系信息,我希望仅提取和总结相关特征,并将这些文本用于后续任务。

为了在检索数据时实现共享接口,我创建了一个基本的数据访问对象(DAO),它定义了与数据的公共接口,在本教程示例中,数据存储在本地的文本和 JSON 文件中(存储在registry_job_ads中),但通常可以是任何其他的职位广告数据库或 API。

总结或抽象文本是 LLM 非常擅长的语义操作。为此,

  1. 需要一个指令提示,以使 LLM 正确处理文本,以符合任务需求;

  2. 还必须选择 Anthropic 的 LLM 模型及其参数(例如温度);

  3. 并且,指令化的大型语言模型(LLM)通过第三方 API 调用,并具有其特定的语法、错误检查等要求。

为了将这三种不同的关注点分离,我引入了一些抽象。

下方的类图展示了代理的关键方法和关系,这些代理提取职位广告的关键特征。

在代码中,这看起来是这样的:

配置文件agent_model_extractor_confs是一个 JSON 文件,其部分内容如下所示:

随着进一步代理的实现,额外的配置会被添加到此文件中。

提示是将通用 LLM 集中到特定功能的关键。我使用 Jinja 模板来组合提示。这是一个灵活且成熟的方法来创建带有程序化内容的文本文件。对于相对简单的招聘广告提取代理,逻辑很简单——从文件中读取文本并返回——但当我处理更高级的代理时,Jinja 模板将变得更为有用。

agent_type='JobAdQualityExtractor 的提示模板是:

Your task is to analyze a job ad and from it extract, 
on the one hand, the qualities and attributes that the 
company is looking for in a candidate, and on the other 
hand, the qualities and aspirations the company 
communicates about itself.

Any boilerplate text or contact information should be 
ignored. And where possible, reduce the overall amount 
of text. We are looking for the essence of the job ad.

调用代理,无需工具

模型名称(例如 claude-3–5-sonnet-20240620)、一个提示和一个 Anthropic 客户端是我们发送请求到 Anthropic API 执行 LLM 所需的最基本内容。招聘广告质量提取代理拥有这一切。因此,它可以实例化并执行“裸金属”代理类型。

没有任何先前使用的记忆或其他功能,裸金属代理仅调用 LLM 一次。它关注的范围是 Anthropic 如何格式化其输入和输出。

我也创建了一个抽象基类,Agent。它并不是严格要求的,对于像简历创建这样基础的任务用途有限。然而,如果我们继续在这个基础上构建,以处理更复杂和多样的任务,抽象基类是一个良好的实践

send_request_to_anthropic_message_creation 是一个 简单的 Anthropic API 调用包装器

这就是获取招聘广告摘要所需的所有内容。简而言之,步骤如下:

  1. 实例化一个招聘广告质量提取代理,这涉及到收集相关的提示和 Anthropic 模型参数。

  2. 使用公司名称和职位调用招聘广告数据访问对象,以获取完整的招聘广告文本。

  3. 对完整的招聘广告文本进行提取,这需要一次性向 Anthropic LLM 的 API 发起请求;返回一个包含生成摘要的文本字符串。

make_cv 脚本中的代码步骤如下:

 # Step 0: Get Anthropic client
    anthropic_client = get_anthropic_client(api_key_env)

    # Step 1: Extract key qualities and attributes from job ad
    ad_qualities = JobAdQualityExtractor(
        client=anthropic_client,
    ).extract_qualities(
        text=JobAdsDAO().get(job_ad_company, job_ad_title),
    )

数据流图的顶部部分已经描述完毕。

如何构建使用工具的代理

代理工作流中的所有其他类型的代理都使用工具。现在大多数 LLM 都具备这种有用的功能。由于我在上面描述了裸金属代理,接下来我将描述使用工具的代理,因为它是后续操作的基础。

LLM 通过序列到序列的映射生成字符串数据。在聊天应用程序以及招聘广告质量提取器中,字符串数据(大部分)是文本。

但字符串数据也可以是一个函数参数的数组。例如,如果我有一个可执行函数 add,它将两个整数变量 ab 相加并返回它们的和,那么运行 add 的字符串数据可能是:

{
  "name": "add",
  "input": {  
    "a": "2",
    "b": "2"
  }
}

因此,如果 LLM 输出这段函数参数字符串,它可以在代码中导致add(a=2, b=2)的函数调用。

问题是:应该如何指示 LLM,使其知道何时以及如何生成这种特定语法的字符串数据?

除了AgentBareMetal代理,我还定义了另一种代理类型,它同样继承了Agent基类:

这与裸机代理有两个方面的不同:

  • self.tools是在实例化过程中创建的列表。

  • tool_return是在执行过程中通过调用从注册表中获取的函数registry_tool_name_2_func创建的。

前者对象包含指示 Anthropic LLM(大语言模型)关于其可以生成的字符串数据格式的指令,这些数据将作为不同工具的输入参数。后者对象则是通过执行工具,依据 LLM 生成的字符串数据得到的。

tools_cv_data文件包含一个 JSON 字符串,格式化用于定义函数接口(但不包括函数本身)。该字符串必须符合Anthropic LLM 的非常具体的模式,以便它能够理解。该 JSON 字符串的一个片段如下:

从上面的规范我们可以看出,如果例如AgentToolInvokeReturn的初始化在tools参数中包含字符串biography,那么 Anthropic LLM 将被指示可以生成一个函数参数字符串,用于调用名为create_biography的函数。每个参数应该包含什么样的数据由 LLM 根据 JSON 字符串中的描述字段来决定。因此,这些描述相当于小提示,指导 LLM 进行推理。

与此规范相关的函数我通过以下两个定义来实现。

简而言之,工具名称create_biography与类构造函数Biography.build关联,该函数创建并返回Biography数据类的一个实例。

请注意,数据类的属性在添加到代理的self.tools变量中的 JSON 字符串中得到了完美的映射。这意味着从 Anthropic LLM 返回的字符串将完美适配数据类的类构造函数。

综合来看,请仔细查看AgentToolInvokeReturnrun方法的内部循环,如下所示:

 for response_message in response.content:
            assert isinstance(response_message, ToolUseBlock)

            tool_name = response_message.name
            func_kwargs = response_message.input
            tool_id = response_message.id

            tool = registry_tool_name_2_func.get(tool_name)
            try:
                tool_return = tool(**func_kwargs)
            except Exception:
                ...

步骤如下:

  1. 从 Anthropic LLM 返回的响应会被检查,确保它是一个函数参数的字符串,而不是普通文本。

  2. 工具的名称(例如create_biography)、函数参数的字符串以及一个独特的工具使用 ID 被收集起来。

  3. 可执行工具是从注册表中获取的(例如Biography.build)。

  4. 该函数使用字符串函数参数执行(检查错误)。

一旦我们获得了工具的输出,我们应该决定如何处理它。一些应用将工具输出集成到消息中,并执行另一个 LLM API 请求。然而,在当前应用中,我构建了生成数据对象的代理,特别是CVData的子类。因此,我设计代理来调用工具,然后简单地返回其输出——因此类名为AgentToolInvokeReturn

正是在这个基础上,我构建了创建我想要作为 CV 一部分的约束数据结构的代理。

结构化 CV 数据提取器代理

生成结构化传记数据的代理的类图如下所示。它与之前提取职位广告特征的代理的类图有很多相似之处。

代码中:

与之前的代理JobAdQualityExtractor的两个区别:

  1. 工具名称作为类属性cv_data的函数被检索(上面代码片段中的第 47 行)。所以当带有工具的代理被实例化时,工具名称的顺序是由一个注册表提供的,该注册表将某种类型的 CV 数据(例如Biography)与在上述tools_cv_data JSON 字符串中使用的键关联,例如biography

  2. 代理的提示是用变量呈现的(第 48 至 52 行)。回顾一下上面的 Jinja 模板的使用。这使得能够注入与职位广告相关的特征以及在“关于我”部分中使用的目标字数。传记代理的具体模板是:

传记提取器代理的提示模板的图片,注意其中的两个变量

这意味着在实例化时,代理会意识到它应该根据职位广告调整其文本输出。

所以当它接收到原始文本数据时,它执行指令并返回一个Biography数据类的实例。基于相同的原因和类似的软件设计,我生成了额外的提取器代理、CV 数据类和工具定义:

class EducationCVDataExtractor(CVDataExtractor):
    cv_data = Educations
    def __init__(self):
#      <truncated>

class EmploymentCVDataExtractor(CVDataExtractor):
    cv_data = Employments
    def __init__(self):
#      <truncated>

class SkillsCVDataExtractor(CVDataExtractor):
    cv_data = Skills
    def __init__(self):
#      <truncated>

现在我们可以提升一个层次的抽象。有了提取器代理,它们应该与原始数据结合,从中提取、总结、重写并提炼 CV 数据内容。

数据检索和提取的编排

接下来要解释的数据图的部分是突出显示的部分。

原则上,我们可以让提取器代理访问我们为某个个人制作 CV 时所拥有的所有可能的文本。但这意味着代理必须处理大量与其关注的具体部分无关的数据,例如,正式的教育细节几乎不会出现在个人的意识流博客中。

这是检索和搜索的重要问题通常进入基于 LLM 的应用设计考虑的地方。

我们是尝试寻找相关的原始数据来应用我们的代理,还是将我们所有的内容都投入到大上下文窗口中,让 LLM 来解决检索问题呢?许多人 已经发表过看法 对此问题 发表了讨论。这是一个值得探讨的话题,因为下面这句话中有很多真理:

对于我的应用,我会保持简单——检索和搜索留到另一天再做。

因此,我将处理半结构化的原始数据。尽管我们对各个文档的内容有一个大致的理解,但它们内部大多由非结构化文本组成。这种场景在许多现实世界的案例中很常见,其中可以从文件系统或数据湖中的元数据中提取有用的信息。

检索谜题的第一部分是模板目录的数据库访问对象(DAO)。其核心是一个像这样的 JSON 字符串:

它将简历模板的名称(例如single_column_0)与所需数据部分的列表关联起来——这些数据部分是前面章节中描述的CVData数据类。

接下来,我编码哪些原始数据访问对象应该与哪些简历数据部分对应。在我的示例中,我有一小部分原始数据源,每个数据源都可以通过 DAO 访问,例如PersonsEmploymentDAO

_map_extractor_daos: Dict[str, Tuple[Type[DAO]]] = {
    f'{EducationCVDataExtractor.cv_data.__name__}': (PersonsEducationDAO,),
    f'{EmploymentCVDataExtractor.cv_data.__name__}': (PersonsEmploymentDAO,),
    f'{BiographyCVDataExtractor.cv_data.__name__}': (PersonsEducationDAO, PersonsEmploymentDAO, PersonsMusingsDAO),
    f'{SkillsCVDataExtractor.cv_data.__name__}': (PersonsEducationDAO, PersonsEmploymentDAO, PersonsSkillsDAO),
}
"""Map CV data types to DAOs that provide raw data for the CV data extractor agents

This allows for a pre-filtering of raw data that are passed to the extractors. For example,
if the extractor is tailored to extract education data, then only the education DAO is used.
This is strictly not needed since the Extractor LLM should be able to do the filtering itself,
though at a higher token cost.

"""

注意,在这段代码中,简历中的传记和技能数据是由多个原始数据源创建的。如果有额外的原始数据源可用,这些关联可以轻松修改——只需将新的 DAO 添加到元组中——或者在运行时进行配置。

然后,只需要将原始数据和简历数据提取代理匹配到每个所需的简历部分。这就是协调器所实现的数据流。下图是CVDataExtractionOrchestrator执行的缩放数据流图。

在代码中,协调器如下所示:

然后,在脚本make_cv中将所有内容整合在一起,我们得到:

 # Step 2: Ascertain the data sections required by the CV template and collect the data
    cv_data_orchestrator = CVDataExtractionOrchestrator(
        client=anthropic_client,
        relevant_qualities=ad_qualities,
        n_words_employment=n_words_employment,
        n_words_education=n_words_education,
        n_skills=n_skills,
        n_words_about_me=n_words_about_me,
    )
    template_required_cv_data = FormTemplatesToCDAO().get(cv_template, 'required_cv_data_types')
    cv_data = {}
    for required_cv_data in template_required_cv_data:
        cv_data.update(cv_data_orchestrator.run(
            cv_data_type=required_cv_data,
            data_key=person_name
        ))

因此,所有对 Anthropic LLM 的调用都发生在协调器中。每次调用都是通过程序生成的指令提示完成的,通常包括职位广告摘要、简历各部分应有的文字数量参数,以及原始数据,以个人的名字为键。

循环执行完毕后,所有使用工具的代理完成任务后,会生成一组结构化的简历数据类实例。

插曲:无,<未知>,“缺失”

Anthropic 的 LLM 在将其生成的内容与构建数据类所需的输出模式匹配方面表现得相当出色。例如,我不会偶尔将电话号码放到电子邮件字段中,也不会想出无效的键,这样会破坏数据类的构建函数。

但是,当我进行测试时,我遇到了一个不完美的情况。

再看看简历数据是如何定义的:

例如,如果 LLM 没有在某个人的原始数据中找到 GitHub URL,则可以为该字段返回None,因为该数据类中的该属性是可选的。这就是我想要的结果,因为它简化了最终简历的渲染(见下文)。

但是,LLM 通常返回一个字符串值,通常是'<UNKNOWN>'。对于人类观察者来说,这没有什么歧义。它不是一种幻觉——即看起来真实却没有原始数据支持的虚构内容。

然而,对于使用简单条件逻辑的渲染算法来说,这是一个问题,例如在 Jinja 模板中的以下代码:

 <div class="contact-info">
        {{ biography.email }}
        {% if biography.linkedin_url %} &mdash; <a href="{{ biography.linkedin_url }}">LinkedIn</a>{% endif %}
        {% if biography.phone %} &mdash; {{ biography.phone }}{% endif %}
        {% if biography.github_url %} &mdash; <a href="{{ biography.github_url }}">GitHub</a>{% endif %}
        {% if biography.blog_url %} &mdash; <a href="{{ biography.blog_url }}">Blog</a>{% endif %}
    </div>

对于人类来说语义上显而易见,但语法上杂乱无章的问题,非常适合由大型语言模型(LLMs)处理。在 LLM 之前,标签不一致的问题曾经引发了许多麻烦,并产生了冗长的创造性字符串匹配命令(任何做过包含大量自由文本字段的数据库迁移的人都可以证实这一点)。

所以为了处理这个不完美,我创建了另一个代理,该代理操作其中一个其他简历数据提取代理的输出。

该代理使用前面部分中描述的对象。不同之处在于,它接受简历数据类的集合作为输入,并被指示清空任何“值被标记为未知、未定义、未找到或类似的字段”(完整提示的一部分)。

创建一个联合代理。它首先执行之前提到的简历数据创建。其次,它在前一个代理的输出上执行清除未定义代理,以解决任何字符串的问题。

这个代理解决了问题,因此我在编排中使用它。

这个不完美问题是否可以通过不同的指令提示来解决?或者简单的字符串匹配修复是否足够?也许可以。

然而,我使用的是 Anthropic 的最简单、最便宜的 LLM(haiku),由于代理的模块化设计,这是一项易于实现并添加到数据管道中的修复。构建包含多个其他代理的联合代理是先进代理工作流使用的设计模式之一。

使用简历数据对象集合进行渲染

工作流的最后一步相对简单,因为我们花费了精力来创建结构化和定义良好的数据对象。通过语法匹配,这些对象的内容被具体放置在 Jinja HTML 模板中。

例如,如果 biography 是 Biography CV 数据类的一个实例,env 是一个 Jinja 环境,那么以下代码

template = env.get_template('test_template.html')
template.render(biography=biography)

test_template.html 需要如下:

<body>
    <h1>{{ biography.name }}</h1>
    <div class="contact-info">
      {{ biography.email }}
    </div>
</body>

匹配 Biography 数据类的姓名和电子邮件属性,并返回类似以下的内容:

<body>
    <h1>My N. Ame</h1>
    <div class="contact-info">
      my.n.ame@compuserve.com
    </div>
</body>

函数 populate_html 接受所有生成的 CV 数据对象,并使用 Jinja 功能返回一个 HTML 文件。

make_cv 脚本中,第三步也是最后一步是:

 # Step 3: Render the CV with data and template and save output
    html = populate_html(
        template_name=cv_template,
        cv_data=list(cv_data.values()),
    )
    with open(output_file, 'w') as f:
        f.write(html)

这完成了代理工作流。原始数据已经被提炼,内容被放入结构化数据对象中,这些对象反映了标准简历的信息设计,内容也被呈现为编码了样式选择的 HTML 模板。

CV 模板如何制作——怎么做?

CV 模板是 HTML 文件的 Jinja 模板。因此,任何可以创建和编辑 HTML 文件的工具都可以用来创建模板。只要变量命名符合 CV 数据类的名称,它就能与工作流兼容。

例如,以下这部分 Jinja 模板将从 the [Employments](https://github.com/anderzzz/everyone_knows_it/blob/6c96a71b3b1be18918a6caad96b9ec5c2148de2e/src/cv_data.py#L123) CV 数据类 中检索数据属性,并创建一个包含描述(由 LLMs 生成)和持续时间数据(如果有的话)的工作经历列表:

 <h2>Employment History</h2>
            {% for employment in employments.employment_entries %}
            <div class="entry">
                <div class="entry-title">
                    {{ employment.title }} at {{ employment.company }} ({{ employment.start_year }}{% if employment.end_year %} - {{ employment.end_year }}{% endif %}):
                </div>
                {% if employment.description %}
                <div class="entry-description">
                    {{ employment.description }}
                </div>
                {% endif %}
            </div>
            {% endfor %}

我对前端开发了解很少——即使是 HTML 和 CSS,在我多年来编写的代码中也很少出现。

因此,我决定使用 LLMs 来创建 CV 模板。毕竟,这是一个任务,旨在将人类观察者认为合理且直观的外观和设计映射到一串特定的 HTML/Jinja 语法——这是 LLMs 在此类任务中表现得相当出色的领域。

我选择不将此与代理工作流集成,而是将其附加在数据流图的角落,作为应用程序的一个有用附录。

我使用了 Claude,这是 Anthropic 的 Sonnet LLM 的聊天界面。我向 Claude 提供了两样东西:一张图片和一个提示。

该图片是我快速使用文字处理软件制作的单栏简历的粗略轮廓,然后截屏保存。

单栏简历布局的截屏,用于引导 Claude

我给出的提示相当长。它由三个部分组成。

首先,声明我希望完成的任务以及在 Claude 执行任务时我将提供哪些信息。

这一部分提示内容是:

我希望为静态 HTML 页面创建一个 Jinja2 模板。该 HTML 页面将展示一个人的简历。模板的目的是通过 Python 使用 Python 数据结构作为输入进行渲染。

第二,布局的口头描述。实质上是对上图的描述,从上到下,并附上关于相对字体大小、各部分顺序等的备注。

第三,我将用于渲染 Jinja 模板的数据结构描述。部分内容如下面的图片所示:

提示继续列出所有简历数据类。

对于一位精通 Jinja 模板、HTML 和 Python 数据类的人工解读者来说,这些信息足够使其能够将电子邮件在布局中的位置与 HTML Jinja 模板中的语法 {{ biography.email }} 进行匹配,将 LinkedIn 个人资料 URL(如果有)在布局中的位置与语法 {% if biography.linkedin_url %} <a href=”{{ biography.linkedin_url }}”>LinkedIn</a>{% endif } 进行匹配,依此类推。

Claude 完美地执行了任务——我无需手动编辑模板。

我使用单栏模板和为人物 Gregor Samsa(稍后会详细介绍)生成的合成数据运行了代理工作流。

make_cv --job-ad-company "epic resolution index" \
        --job-ad-title "luxury retail lighting specialist" \
        --cv-template single_column_0 \
        --person-name "gregor samsa" \
        --output-file ../generated_output/cv_single_column_0.html \
        --n-words-employment 50 --n-skills 8

输出文档:

一个不错的简历。但我想创建一些变体,看看 Claude 和我能做出什么。

所以我又创建了另一个提示和屏幕截图。这次是针对一个两栏简历。粗略大纲我画了出来:

用来指导 Claude 的两栏简历布局的屏幕截图

我重新使用了单栏的提示,只是修改了第二部分,用文字描述布局。

它再次完美地工作了。

不过样式对于我的口味来说有点太单调了。所以作为对 Claude 的后续提示,我写道:

喜欢!你能重新做一次之前的任务,但加上一些修改吗:加入一些亮点和色彩。Arial 字体,黑白色调有点单调。我喜欢一些绿色和更好看的字体。让我惊艳!当然,它依然应该看起来很专业。

如果 Claude 回应说我必须更具体一些,我会有些同感(在某种意义上)。然而,Claude 的生成能力充分发挥,创建了一个模板,当渲染时看起来像这样:

不错!

值得注意的是,这个版本保留了粗略大纲中的基本布局:各个部分的位置、两栏的相对宽度,以及教育条目中没有描述等。只是样式有所改变,并且与给定的模糊规范保持一致。根据我的判断,Claude 的生成能力很好地填补了这些空白。

接下来,我探索了即使将样式调整到极限,Claude 是否还能保持模板布局和内容的清晰一致。所以我接着对 Claude 写道:

太棒了。但现在我想让你尽情发挥!我们要谈的是 1990 年代早期的网页美学,闪烁的元素、怪异的 Comic Sans 字体、疯狂的颜色对比。全速前进,Claude,玩得开心点。

结果非常出色。

这个 Gregor Samsa 是谁,真是个自由思想者,毫无焦虑——快雇这个人!

即使在如此极端的样式下,指定的布局大体上仍然得以保留,文本内容也是如此。通过足够详细的提示,Claude 似乎能够创建出功能性且风格独特的模板,这些模板可以成为代理工作流的一部分。

那么文本输出如何呢?

除了引人注目的风格和有用的布局外,简历必须包含简洁且真实地展示人与职位匹配的文字。

为了探索这一点,我为一个名为Gregor Samsa的人创建了合成数据——他在中欧接受教育,从事灯具销售工作,对昆虫学有广泛兴趣。我生成了关于 Gregor 过去和现在的原始数据,其中一部分来自我的想象,一部分来自 LLM。这些细节并不重要。关键点是,文本内容太混乱、难以操作,无法直接复制粘贴到简历中。数据需要被找到(例如,Gregor 的电子邮件地址出现在他的一些随意思考中),总结(例如,Gregor 博士学位工作的描述非常详细),提炼并定制为与职位相关的内容(例如,哪些技能值得突出),并最终缩减为一两句话,放入“关于我”部分。

文本输出做得非常好。我让 Anthropic 公司最先进、最有表现力的模型 Sonnet 来写“关于我”部分。语气非常自然。

在我的测试中,我没有发现明显的幻觉。然而,LLM 在技能部分确实采取了一些自由发挥。

原始数据中描述 Gregor 大多在布拉格和维也纳工作和学习,偶尔参加一些英语语言教育者的在线课程。在一份生成的简历中,列出了捷克语、德语和英语的语言技能,尽管原始数据并没有明确声明他具备这些语言能力。LLM 做出了合理的技能推测。不过,这些技能并非仅从原始数据中抽取出来的。

所有代码和合成数据都可以在我的 GitHub 仓库中找到。我使用的是 Python 3.11,运行时,只要你有 Anthropic 的 API 密钥(假设脚本将其存储在环境变量ANTHROPIC_API_KEY中),你就可以运行并探索这个应用程序——当然,根据我所知,这没有错误,但我不能保证。

本教程展示了使用生成式 AI 的一种方式,阐述了在生成应用中有用的约束,并展示了如何直接与 Anthropic API 进行交互来实现这些内容。尽管创建简历不是一项高级任务,但我所介绍的原则和设计可以作为其他更具价值和复杂性的非聊天应用的基础。

快乐构建!

所有图像、图表和代码均由作者创建。

深入探索:更详细地了解 4 个 Airflow 内部细节

原文:towardsdatascience.com/airflow-internals-851f4a440028?source=collection_archive---------7-----------------------#2024-02-03

你可能错过的四个 Apache Airflow 内部细节

Giorgos MyrianthousTowards Data Science Giorgos Myrianthous

·发布于 Towards Data Science ·6 分钟阅读·2024 年 2 月 3 日

--

通过 DALL-E 生成的图像

我已经与 Airflow 一起工作了超过三年,总体上我对它非常有信心。它是一个强大的调度器,帮助我快速并且可扩展地构建数据管道,而在我打算实现的大多数功能中,它自带了许多工具。

最近,在准备 Airflow 认证考试时,我遇到了许多我完全不知道的事情。这也是我写这篇文章并与大家分享一些令我大开眼界的 Airflow 内部细节的动力!

1. 调度器只解析包含特定关键词的文件

Airflow 调度器只会解析包含 **airflow** **dag** 关键词的文件! 是的,你没听错!如果 DAG 文件夹下的某个文件没有包含这两个关键词中的至少一个,它将不会被调度器解析。

如果你想修改这个规则,使得调度器不再需要这个要求,你可以简单地将 DAG_DISCOVERY_SAFE_MODE 配置设置为 False。在这种情况下,调度器将解析你 DAG 文件夹下的所有文件(/dags)。

人工智能在通过人类语音检测疾病中的新兴角色

原文:towardsdatascience.com/ais-emerging-role-in-disease-detection-from-human-speech-f24e6d49d25c?source=collection_archive---------3-----------------------#2024-04-22

|人工智能| 医疗保健| 语音|

通过语音进行疾病预测可能成为医疗保健领域的下一场革命

Salvatore RaieliTowards Data Science Salvatore Raieli

·发表于Towards Data Science ·阅读时间:11 分钟·2024 年 4 月 22 日

--

图片来自Volodymyr HryshchenkoUnsplash

身体语言和语气——而非言语——是我们最强大的评估工具。——克里斯托弗·沃斯

未来的医学将允许在没有侵入性检查的情况下进行诊断,并进行远程患者监控。 人工智能是这个未来不可或缺的组成部分。

当提到人工智能辅助的诊断时,人们通常会想到医学影像。一个例子是通过手机拍摄的照片,结合人工智能算法分析,来诊断潜在的癌变痣。人类主要通过语音进行沟通。在人类的声音中,蕴藏着远超言语内容的宝贵信息。

如何分析这个隐藏的信息?如何将其用于诊断目的?

本文正是讨论这个话题。 我们将讨论为什么这很重要,为什么这很困难,已经做了哪些工作,以及遇到了哪些挑战。同样,我们还将讨论最近的进展和未来的可能性……

使用 MLflow 进行算法无关的模型构建

原文:towardsdatascience.com/algorithm-agnostic-model-building-with-mlflow-b106a5a29535?source=collection_archive---------0-----------------------#2024-08-10

面向初学者的逐步指南,帮助你使用 mlflow.pyfunc 创建通用的机器学习管道

Mena Wang, PhDTowards Data Science Mena Wang, PhD

·发布于Towards Data Science ·阅读时间:8 分钟·2024 年 8 月 10 日

--

MLOps 中的一个常见挑战是不同算法或框架之间迁移的麻烦。本文面向初学者,帮助你通过使用mlflow.pyfunc进行算法无关的模型构建来应对这一挑战。

为什么选择算法无关的模型构建?

假设有这样一个场景:我们有一个当前在生产环境中部署的 sklearn 模型,用于某个特定的使用场景。后来我们发现,深度学习模型的表现更好。如果 sklearn 模型是以其原生格式部署的,切换到深度学习模型可能会很麻烦🤪,因为这两种模型的文件格式差异很大。

由 Gemini 生成的图像

为了解决这一挑战,mlflow.pyfunc模型类型提供了一种多功能的通用方法,用于在 Python 中构建和部署机器学习模型。😎

1. 通用模型构建: pyfunc模型类型提供了一种统一的方式来构建模型,无论使用的是哪个框架或库。

2. ML 管道的封装: pyfunc允许我们将模型与其前后处理步骤或在模型使用过程中所需的其他自定义逻辑一起封装。

3. 统一的模型表示: 我们可以使用pyfunc部署一个模型、一个机器学习管道或任何 Python 函数,而无需担心模型的底层格式。这种统一的表示简化了模型的部署、重新部署以及后续评分。

听起来很有趣?如果是的话,本文将帮助你开始使用mlflow.pyfunc。🥂

  • 首先,让我们通过一个简单的示例来创建mlflow.pyfunc类。

  • 然后,我们将定义一个封装机器学习管道的mlflow.pyfunc类(以估算器加上一些预处理逻辑为例)。我们还将训练、记录并加载这个机器学习管道进行推理。

  • 最后,让我们深入探讨封装的mlflow.pyfunc对象,探索mlflow自动为我们跟踪的丰富元数据和工件,更好地理解mlflow.pyfunc所提供的全部功能。

🔗 所有代码和配置可以在GitHub上找到。🧰

{pyfunc} 简单玩具模型

首先,让我们创建一个简单的玩具mlflow.pyfunc模型,然后将其与 mlflow 工作流一起使用。

  • 第一步:创建模型

  • 第二步:记录模型

  • 第三步:加载已记录的模型进行推理

# Step 1: Create a mlflow.pyfunc model
class ToyModel(mlflow.pyfunc.PythonModel):
    """
    ToyModel is a simple example implementation of an MLflow Python model.
    """

    def predict(self, context, model_input):
        """
        A basic predict function that takes a model_input list and returns a new list 
        where each element is increased by one.

        Parameters:
        - context (Any): An optional context parameter provided by MLflow.
        - model_input (list of int or float): A list of numerical values that the model will use for prediction.

        Returns:
        - list of int or float: A list with each element in model_input is increased by one.
        """
        return [x + 1 for x in model_input]

如上例所示,你可以创建一个mlflow.pyfunc模型,以实现任何适合你机器学习解决方案的自定义 Python 函数,这不一定是现成的机器学习算法。

然后,你可以记录这个模型并在之后加载它以执行推理。

# Step 2: log this model as an mlflow run
with mlflow.start_run():
    mlflow.pyfunc.log_model(
        artifact_path = "model", 
        python_model=ToyModel()
    )
    run_id = mlflow.active_run().info.run_id
# Step 3: load the logged model to perform inference
model = mlflow.pyfunc.load_model(f"runs:/{run_id}/model")
# dummy new data
x_new = [1,2,3]
# model inference for the new data
print(model.predict(x_new))
[2, 3, 4]

{pyfunc} 封装的 XGBoost 管道

现在,让我们创建一个封装了估算器和额外自定义逻辑的机器学习管道。

在下面的示例中,XGB_PIPELINE类是一个包装器,它将估算器与预处理步骤结合在一起,对于某些 MLOps 实现来说非常有用。借助mlflow.pyfunc,这个包装器与估算器无关,提供统一的模型表示。具体来说:

  • fit():这个类使用.fit()方法,而不是使用 XGBoost 的原生 API(xgboost.train()),该方法遵循 sklearn 的惯例,能够轻松集成到 sklearn 管道中,并确保不同估算器之间的一致性。

  • DMatrix()DMatrix是 XGBoost 中的核心数据结构,它优化了用于训练和预测的数据。在这个类中,将 pandas DataFrame 转换为DMatrix的步骤被封装在类内部,使其能够像所有其他 sklearn 估算器一样无缝地与 pandas DataFrame 集成。

  • predict():这是mlflow.pyfunc模型的通用推理 API。对于这个机器学习管道、上面的玩具模型以及我们封装在mlflow.pyfunc模型中的任何机器学习算法或自定义逻辑,它都保持一致。

import json
import xgboost as xgb
import mlflow.pyfunc
from typing import Any, Dict, Union
import pandas as pd

class XGB_PIPELINE(mlflow.pyfunc.PythonModel):
    """
    XGBWithPreprocess is an example implementation of an MLflow Python model with XGBoost.
    """

    def __init__(self, params: Dict[str, Union[str, int, float]]):
        """
        Initialize the model with given parameters.

        Parameters:
        - params (Dict[str, Union[str, int, float]]): Parameters for the XGBoost model.
        """
        self.params = params
        self.xgb_model = None
        self.config = None      

    def preprocess_input(self, model_input: pd.DataFrame) -> pd.DataFrame:
        """
        Preprocess the input data.

        Parameters:
        - model_input (pd.DataFrame): The input data to preprocess.

        Returns:
        - pd.DataFrame: The preprocessed input data.
        """
        processed_input = model_input.copy()
        # put any desired preprocessing logic here
        processed_input.drop(processed_input.columns[0], axis=1, inplace=True)

        return processed_input

    def fit(self, X_train: pd.DataFrame, y_train: pd.Series):
        """
        Train the XGBoost model.

        Parameters:
        - X_train (pd.DataFrame): The training input data.
        - y_train (pd.Series): The target values.
        """
        processed_model_input = self.preprocess_input(X_train.copy())
        dtrain = xgb.DMatrix(processed_model_input, label=y_train)
        self.xgb_model = xgb.train(self.params, dtrain)

    def predict(self, context: Any, model_input: pd.DataFrame) -> Any:
        """
        Predict using the trained XGBoost model.

        Parameters:
        - context (Any): An optional context parameter provided by MLflow.
        - model_input (pd.DataFrame): The input data for making predictions.

        Returns:
        - Any: The prediction results.
        """
        processed_model_input = self.preprocess_input(model_input.copy())
        dmatrix = xgb.DMatrix(processed_model_input)
        return self.xgb_model.predict(dmatrix)

现在,让我们训练并记录这个模型。

from sklearn.datasets import make_regression
from sklearn.model_selection import train_test_split
import pandas as pd

# Generate synthetic datasets for demo
X, y = make_regression(n_samples=1000, n_features=10, noise=0.1, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# train and log the model
with mlflow.start_run(run_name = 'xgb_demo') as run:

    # Create an instance of XGB_PIPELINE
    params = {
        'objective': 'reg:squarederror',  
        'max_depth': 3,  
        'learning_rate': 0.1,
    }
    model = XGB_PIPELINE(params)

    # Fit the model
    model.fit(X_train=pd.DataFrame(X_train), y_train=y_train)

    # Log the model
    model_info = mlflow.pyfunc.log_model(
        artifact_path = 'model',
        python_model = model,
    )

    run_id = mlflow.active_run().info.run_id

模型已经成功记录。✌ ️现在,让我们加载它进行推理。

loaded_model = mlflow.pyfunc.load_model(model_uri=model_info.model_uri)
loaded_model.predict(pd.DataFrame(X_test))
array( 4.11692047e+00,  7.30551958e+00, -2.36042137e+01, -1.31888123e+02,
       ...

深入探讨 Mlflow.pyfunc 对象

上述过程相当顺利,不是吗?这代表了mlflow.pyfunc对象的基本功能。现在,让我们深入挖掘,探索mlflow.pyfunc所能提供的全部强大功能。

1. model_info

在上面的示例中,mlflow.pyfunc.log_model()返回的model_info对象是mlflow.models.model.ModelInfo类的一个实例。它包含了关于已记录模型的元数据和信息。例如:

![

一些model_info对象的属性

随时运行 dir(model_info) 进行进一步探索,或者查看 源代码 了解所有定义的属性。我最常用的属性是 model_uri,它指示已记录模型在 mlflow 跟踪系统中的位置。

2. loaded_model

值得澄清的是,loaded_model 不是 XGB_PIPELINE 类的实例,而是由 mlflow.pyfunc 提供的一个包装对象,用于实现与算法无关的推理。如下面所示,如果你尝试从 loaded_model 中获取 XGB_PIPELINE 类的属性,将会返回一个错误。

print(loaded_model.params)
AttributeError: 'PyFuncModel' object has no attribute 'params'

3. unwrapped_model

好吧,你可能会问,那么训练好的 XGB_PIPELINE 实例在哪里呢?它也会被通过 mlflow 记录并可检索吗?

别担心;它被安全地保留,你可以像下面所示轻松解包。

unwrapped_model = loaded_model.unwrap_python_model()
print(unwrapped_model.params)
{'objective': 'reg:squarederror', 'max_depth': 3, 'learning_rate': 0.1}

就是这样。😎 使用 unwrapped_model,你可以像这样访问你的自定义 ML 流水线的任何属性或方法!我有时会在自定义流水线中添加方便的方法,如 explain_modelpost_processing,或者包含有助于追踪模型训练过程和提供诊断的属性 🤩… 好吧,我最好到此为止,把这些留到后续文章讲解。可以简单地说,你可以自由定制你的 ML 流水线,以适应你的使用场景,并知道

  1. 你将能够访问所有这些为下游使用定制的方法和属性。

  2. 这个定制的模型将被包装在统一的 mlflow.pyfunc 推理 API 中,因此如果需要,可以平滑地迁移到其他估算器。

4. 上下文

你可能已经注意到,mlflow.pyfunc 类的 predict 方法中有一个 context 参数。但有趣的是,当我们使用加载的模型进行预测时,这个参数并不是必须的。为什么❓

loaded_model = mlflow.pyfunc.load_model(model_uri)
# the context parameter is not needed when calling `predict`
loaded_model.predict(model_input)

这是因为上面的 loaded_model 是一个由 mlflow 提供的包装对象。如果我们使用未包装的模型,则需要显式定义 context,如下面所示;否则,代码将返回错误。

unwrapped_model = loaded_model.unwrap_python_model()
# need to provide context mannually
unwrapped_model.predict(context=None, model_input)

那么,什么是 context?它在 predict 方法中扮演什么角色?

context 是一个 PythonModelContext 对象,它包含 pyfunc 模型在进行推理时可以使用的工件。它是通过 log_method() 方法隐式且自动创建的。

导航到你项目仓库中的 mlruns 子文件夹,这是 mlflow 在你记录 mlflow 模型时自动创建的文件夹。找到一个以模型的 run_id 命名的文件夹。里面你会找到为你自动记录的模型工件,如下面所示。

# get run_id of a loaded model
print(loaded_model.metadata.run_id)
38a617d0f30645e8ae95eea4642a03c2

记录的 mlflow.pyfunc 模型中的工件文件夹

相当整洁,是吧?😁 随时可以轻松地浏览这些工件;下面是文件夹中 requirementsMLmodel 文件的截图,供参考。

下面的requiarements指定了重建环境以运行模型所需的依赖项版本。

工件文件夹中的requirements.txt文件

下面的MLmodel文档定义了加载和提供模型所需的元数据和配置,以 YAML 格式呈现。

工件文件夹中的MLmodel文件

结论

就这样,这是mlflow.pyfunc模型构建方法。这是很多信息,所以让我们回顾一下。

  1. mlflow.pyfunc提供了一个统一的模型表示,独立于构建模型时使用的底层框架或库。

  2. 我们甚至可以将丰富的自定义逻辑封装到mlflow.pyfunc模型中,以便根据每个用例进行定制,同时保持推理 API 的一致性和统一性。

  3. 可以从加载的mlflow.pyfunc模型中解包底层模型,从而使我们能够利用更多的自定义方法/属性来满足每个用例的需求。

  4. mlflow.pyfunc模型对象会与丰富的元数据和工件一起记录,这些都会被mlflow自动追踪。

  5. 这种统一的mlflow.pyfunc模型表示可以简化在不同算法之间进行实验和迁移的过程,以实现最佳性能(更多内容将在后续文章中讨论,请见下文)。

下一步

现在我们已经整理好了基础内容,接下来的文章中,我们可以继续讨论mlflow.pyfunc的更高级使用方法。😎 以下是我随便想到的一些主题;欢迎留言告诉我你想看到什么。🥰

  1. 利用统一的 API 进行不同算法的实验,并为特定用例识别最佳解决方案。

  2. 使用mlflow.pyfunc自定义模型进行超参数调优。

  3. 将自定义逻辑封装到mlflow.pyfunc ML 管道中,以便定制模型的使用和诊断。

如果你喜欢这篇文章,可以在Medium上关注我。😁

💼LinkedIn | 😺GitHub | 🕊️Twitter/X

除非另有说明,所有图片均由作者提供。

快速傅里叶变换的算法炼金术

原文:towardsdatascience.com/algorithmic-alchemy-with-the-fast-fourier-transform-9d52a5b6d94a?source=collection_archive---------4-----------------------#2024-01-07

FFT 算法的终极指南

在信号处理中的应用

Naman AgrawalTowards Data Science Naman Agrawal

·发表于Towards Data Science ·20 分钟阅读·2024 年 1 月 7 日

--

由 Stephen Niemeier 拍摄:www.pexels.com/photo/black-and-silver-mixing-board-63703/

目录

  1. 引言

  2. 什么是傅里叶变换?

    2.1 时域

    2.2 频域

    2.3 傅里叶变换:数学视角

  3. 传统离散傅里叶变换计算的局限性

  4. FFT 算法

  5. 矩阵视角

  6. 最终总结

注意: 本文中使用的完整代码文件可以在以下仓库找到:github.com/namanlab/FFT_Algorithm_Code

引言

我们周围世界的基础,从量子粒子的行为到巨大天体的运动,可能都由算法支配。作为我们数字宇宙的无声建筑师,它们像现代时代的神明,雕刻着我们技术现实的轮廓。算法是无所不能的,它们以无与伦比的精确度和理解力指挥着现实的复杂性。它们表现出一种无所不在的存在,悄悄塑造我们的体验,指引所有技术,并影响我们互联世界的基本结构。最终,它们还展现出一种无所不知的能力,帮助人类解码复杂的模式,导航我们大自然中埋藏的数据浩瀚领域。

算法不仅仅是做某件事的方法或技巧,它还关乎如何高效地完成任务,节省时间和空间——这两个约束实际上促成了整个研究领域的诞生。本文将探讨本世纪最出色的算法之一:快速傅里叶变换(FFT)算法。FFT 算法帮助我们解决了音频信号处理中最具挑战性的问题之一,即以既高效又极其优美的方式计算信号的离散傅里叶变换。我希望通过本文的阅读,你能够欣赏到这一革命性算法的优雅之处。让我们开始吧!

什么是傅里叶变换?

注意: 如果你已经熟悉傅里叶变换的概念,你可以跳过这一部分,直接进入下一部分,我将在那里讨论 FFT 算法的需求以及它是如何工作的。

傅里叶变换本质上是一种数学技巧,它可以将信号从时间域转换到频域。那么,什么是时间域和频域呢?为了理解这一点,我们首先需要思考一个基本问题:什么是信号?

从最简单的角度来看,信号仅仅是物理量的变化。这个物理量可以是任何可以测量的东西:速度、电压、电流、压力、能量等等。信号大致可以分为两种领域:时间域和频域。

时间域

在时间域中,信号被表示为时间的函数。换句话说,这意味着我们可以绘制/表示信号相对于时间的行为,并观察它在特定时间间隔内如何变化。

例如,如果你在测量电路中一个电阻上的电压,时间域中的信号将展示电压在不同时间点的变化情况。同样,声音的时间域表示仅展示声波的幅度(即空气压力的变化程度)随时间的变化。信号可以用一个函数来数学表示:

x(t) = 2t² + 2t − 1

这是信号 x 作为时间 t(单位:秒)的直接函数的连续表示。然而,对于大多数实际应用,我们并不知道信号的真实函数形式。我们所拥有的只是信号的离散样本(在不同时间点的样本),可以表示为一个简单的向量,如下所示:

该向量仅展示 x 在 8 个不同(等间隔)时间点的值。这些时间间隔之间的间距称为信号的时间周期 T。因此,如果信号在每 2 秒的间隔处进行采样,那么时间周期就是 T = 2 秒。

频域

在频域中,信号表示为频率的函数。与分析信号如何随时间变化不同,重点在于信号的频率成分或信号中存在的不同频率。这可能有点难以理解,因此让我们通过一个声音波的例子来多讨论一下。假设你在收音机上听一段音乐。在时域中,你体验到音乐随时间展开——旋律的进展、节奏的变化以及每个音符的时长。

现在,让我们转到频域。把频域想象成你从不同的角度来观察音乐。你不再关注音乐如何随时间发展,而是关注构成整体声音的各个音调或音高。想象一下你能分离出特定的音乐音符,比如深沉的低音、中频音调和高音元素。这岂不是很酷吗?想想构成音乐的元素:每个乐器和歌手。音乐中每个乐器和声音在频域中都有独特的特征。低音吉他可能主导低频范围,歌声可能覆盖广泛的频率范围,而钹和高帽则贡献了高频部分。这就是频域作为某种“超级英雄”发挥作用的地方;它让你能够将复杂的声音混合拆解为其组成部分。从本质上讲,它提供了一种不同的视角,专注于信号声音的构建块,而不是它随时间的变化过程。

在其频域中,信号可以表示为一个函数(连续函数)如 y(f) = 2f² + 3,或者像这样表示为一个简单的向量(类似于时域):

这个向量仅表示不同频率成分的幅度/存在程度。第一个元素(1)可能代表最低频率成分(比如 1 Hz,Hz 是频率的单位)的幅度。同样,第二个元素(2)可能代表下一个频率成分的幅度,依此类推。

傅里叶变换:一种数学视角

现在我们对信号如何表示有了一些了解,想象一下傅里叶变换就像一个神奇的镜头,允许你在信号的两种表示方式之间切换。它充当时域和频域之间的桥梁,让我们可以从时域和频域的角度分析和理解信号。

现在,我们使用一些数学来分析我刚才所说的。傅里叶变换是一个函数,它将信号的时域表示作为输入,并将其分解为不同频率的正弦波和余弦波的和,每个波有其幅度和相位。得到的表示就是信号的频域(或我们通常称之为谱)表示。数学上,连续信号在时域上的傅里叶变换定义如下:

其中 i = √(-1) 是虚数。是的,傅里叶变换会产生一个包含复相位和幅度的复数输出。然而,在许多实际场景中,我们的关注点主要是变换的幅度,通常忽略伴随的相位。由于数字处理信号是离散的,我们可以建立离散傅里叶变换(DFT)作为其对应的类比形式:

在这里,我们只是简单地将积分替换为求和,因为我们可能只有离散的时间样本,而且信号的真实函数形式对我们来说可能是未知的。假设我们有有限数量的样本,称之为 N:即时间样本的数量或表示信号的向量的长度。那么我们得到信号的所谓短时傅里叶变换:

其中 T 是时间周期。上述函数可以针对任何 f 值进行计算,其幅度仅显示该特定频率成分的存在程度/该频率成分的功率。例如,给定以下向量表示,我们可以计算 f = 0.5 和 f = 0.25 时的傅里叶变换:

假设 x 的值是以 T = 1 秒的间隔进行测量的。那么,

上述计算需要使用一些基本的复数性质,主要是欧拉公式:exp{πi} = −1。输出本质上允许我们比较不同频率成分的存在。这引出了下一个问题:我们考虑哪些 f 值?理论上,我们可以为任何 f 值计算傅里叶变换的值,因此必须找到合适的 f 值范围,使得傅里叶变换能够很好地描述底层信号,并且具有可交换性,即它可以用于恢复时域。对于大多数实际应用,我们只考虑 1/(TN)的整数倍的频率区间,其中 TN 是信号的总持续时间(即样本数 N 乘以每个样本的持续时间 T)。之所以如此,是因为这一点与采样和奈奎斯特-香农采样定理的概念密切相关,虽然这个概念与本文不特别相关。如果你对此感兴趣,可以参考这

在继续之前,让我们稍作总结一下迄今为止我们所覆盖的内容:信号只是一个物理量的变化,可以表示为时间的函数(时域)或频率的函数(频域)。这两种表示方式是等价的(即其中一种可以转换为另一种),而傅里叶变换就是将一种表示方式转换为另一种表示方式的方法。连续信号的傅里叶变换表示为频域中的一个连续函数。然而,当我们处理数字信号(离散时间信号)时,我们会在离散点上对连续信号进行采样。这为我们提供了计算信号短时离散傅里叶变换的公式:

由于我们只考虑是 1/N 的整数倍的频率,我们得到如下结果:

其中 i 是复数√(-1),j 是信号中样本的索引,k 是我们计算功率的频率区间的索引。由于重复写 y(k/(NT))较为繁琐,我们干脆定义一个新函数:

这就是朋友们,我们在教科书中常遇到的傅里叶变换公式。如果你仍然不确定它如何工作以及为什么这么做,下面是一个关于傅里叶变换的极好解释。在本文的下一部分,我们将用一些实际数据计算信号的完整傅里叶变换,编写代码,并发现传统计算傅里叶变换的方法的局限性。这最终将引导我们进入本文的核心内容:FFT 算法。

传统离散傅里叶变换计算的局限性

让我们从计算一个简单信号的傅里叶变换开始,这个信号只包含 8 个样本:

从公式中可以看出,我们不关心时间周期,也就是说,我们不在乎测量该量的间隔,只要它是均匀采样的。现在,我们可以通过将值代入公式,计算不同 k 值(从 k=0 到 k=N-1=7)的傅里叶变换。因此,我们需要计算以下内容:

让我们通过取α = exp{-2πi/N}来简化计算。因此,我们只需要:

对所有 8 个 k 值依次进行计算。这个计算过程相当繁琐。我们能做得更好吗?当然,我们可以用一个简单的 Python 程序来为我们完成这项工作。下面是一个传统的(称为暴力法)方法,它本质上是遍历向量中的每个元素,并计算从 k=0 到 k=N-1 的所有 k 值所需的项:

import numpy as np

def simple_dft(signal):
    # Get the number of samples in the signal
    N = len(signal)

    # Initialize an empty list to store the result (DFT coefficients)
    res = []

    # Iterate over each frequency bin (k)
    for k in range(N):
        # Initialize the current DFT coefficient for the given frequency bin
        cur_value = 0

        # Iterate over each sample in the signal (j)
        for j in range(N):
            # Calculate the complex exponential term and accumulate
            cur_value += signal[j] * np.exp(-2 * np.pi * 1j * j * k / N)

        # Append the result for the current frequency bin to the list
        res.append(np.round(cur_value, 5))

    # Return the list of DFT coefficients
    return res

simple_dft([1, 2, 0, 5, 9, 2, 0, 4])
# Output: [(23+0j), (-8.70711-0.70711j), (10+5j), (-7.29289-0.70711j), 
# (-3-0j), (-7.29289+0.70711j), (10-5j), (-8.70711+0.70711j)]

使用这个函数,我们可以轻松得到所需的 8 个 DFT 系数。我们还可以使用 numpy 提供的 fft 函数验证我们的计算:

# Compute the FFT using NumPy's fft function
a = np.fft.fft([1, 2, 0, 5, 9, 2, 0, 4])

# Compute the DFT using our simple_dft function
b = simple_dft([1, 2, 0, 5, 9, 2, 0, 4])

# Check if the results are element-wise close within a tolerance
print(np.allclose(a, b))
# Output: True

很棒,我们可以正确地得到结果!但是,计算方法真的高效吗?你认为这个函数的时间复杂度是多少?它涉及两个嵌套的 for 循环,每个循环都遍历从 0 到 N-1 的整个值域。因此,时间复杂度是 O(N²)。这看起来似乎还不错,但对于大多数实际应用来说,O(N²)的时间复杂度可能意味着获取结果的速度非常慢。让我们用数字来验证这一点。

假设我们正在计算一个仅有 10 分钟长的音频样本的傅里叶变换。对于大多数传统应用,我们通常的采样率是 22050,即每秒钟测量 22050 个样本(这看起来可能很多,但实际上并不算多,这是保持音频样本质量时最常用的采样率)。这意味着我们大约有 106022050 = 13230000 个样本。为了计算这个样本的 DFT,我们需要至少 N² = 13230000² = 175032900000000 次计算,这真是太多了!如果你使用的是像 C++这样的编程语言(它是最有效率的编程语言之一),最大每秒可以进行的计算次数可能是 2 × 10⁸次。这意味着,计算一个简短的 10 分钟音频样本的 DFT 需要 175032900000000/200000000 = 2875164.5 秒,约为 10 天!这使得计算大信号的 DFT 几乎不可能,从而使傅里叶变换的应用受到极大的限制。但是别担心!快傅里叶变换(FFT)来啦,这个神奇的算法一出现,DFT 计算变得飞快。它将 DFT 计算的时间复杂度从 O(N²)降低到 O(N log N)。对于这个 10 分钟的样本,现在我们只需要进行 13230000*log(13230000) = 216945507 次浮点运算。这个计算只需要约 1.08 秒,比传统的 DFT 算法高效得多。这意味着我们不仅仅局限于小信号——FFT 解放了傅里叶变换在海量数据集上的应用。酷吧?但是这个算法是如何工作的,是什么让它如此高效呢?接下来我们将进入本文的下一部分:FFT 算法!

FFT 算法

FFT 的核心思想在于傅里叶变换固有的对称性,它帮助我们减少一些冗余的计算。FFT 通过利用离散傅里叶变换(DFT)计算的对称性,并将其输入到一个优雅的递归分治模型中,有效地将时间复杂度从 O(N²)降低到 O(N log N)。但是,我们说的这个对称性到底是什么呢?回顾一下 DFT 计算的公式:

如果我们对 N + k 使用这个公式,而不是 k,会发生什么呢?让我们来看一下:

根据复数的性质,对于任何值的 j,e−2πij = 1。因此,

换句话说,值在 k = N 后会重复。因此,F(1) = F(N + 1); F(2) = F(N + 2),以此类推。这就是为什么我们只计算傅里叶变换,k 的范围是从 0 到 N - 1 的原因——之后的值会不断重复。更一般地,通过简单的归纳法,我们可以得到对于任何非负整数 s ∈ Z≥0,

这就是我们将要使用的对称性,它使得 FFT 算法变得更快(正如它的名字所示)。但是,如何设计一个使用这种对称性的分治策略呢?思路是将项分为奇数项和偶数项,并分别计算每个的 DFT:

在上面的公式中,我们将 DFT 项分为两组:一组是偶数索引(j = 2m),另一组是奇数索引(j = 2m + 1)。正如你所看到的,这为我们提供了两个独立的 DFT,一个仅计算信号的偶数项,另一个计算信号的奇数项。但,这能帮助我们减少运算次数吗?还不能,因为我们仍然需要对从 0 到 N-1 的所有 k 值计算所有的 N/2 项,既偶数项也奇数项,即仍然是 2N(N/2)。或者我们真的需要这样做吗?这时我们就可以利用 FFT 的对称性!假设我们可以计算出介于 0 和 N/2-1 之间某个整数值 a 的上述表达式。那么,

仅使用 F₁(a)和 F₂(a)的值(以及前面展示的对称性),我们可以轻松地计算出 F(a + b) = F(c)的值,其中 c 是介于 N/2 和 N-1 之间的某个整数值:

这里是关键思想!我们不需要重新计算 F(c),这样每次可以节省大约 N/2N 次运算。我们需要做的就是计算从 0 到 N/2-1 之间每个整数值 a 的 F1(a)和 F2(a)(这对于偶数项和奇数项总共需要(N/2)(N/2) = N²/4 次运算)。这样做并应用一些简单的对称逻辑,就能帮助我们计算出所有从 0 到 N-1 之间 k 的整数值的 F(k),有效地将运算次数从 N²减少到 2 × (N/2) × (N/2) = N²/2,也就是说减少了一半。是不是很神奇?

现在,看起来我们似乎已经将时间复杂度减少了一半,难道最终的复杂度仍然是 O(N²)吗?如果我们只分割一次,确实是这样。但没有什么能阻止我们进一步分割!我们可以继续这个链条:

如果我们假设 N 是 2 的幂,那么我们可以重复这一过程 r 次,使得:

每次单独的评估需要 O(N)时间计算,我们这样做 r = log2(N)次,给我们一个时间复杂度为 O(Nr) = O(N log N)(在描述时间复杂度时,我们可以忽略对数的底数)。对于那些希望看到递归关系形式的人,公式为:

这里,T(N)表示解决一个大小为 n 的问题的时间复杂度。在 FFT 的情况下,它是输入信号中的元素数量,而 O(N)表示合并较小子问题结果所需的时间。

递推关系表明,为了解决一个大小为 N 的问题,FFT 算法递归地将问题分成两个子问题,每个子问题的大小为 N/2(一个用于奇数项,另一个用于偶数项),在 2T(N/2)的时间内计算这些子问题的解,然后在 O(N)的时间内合并结果。解决这个递推关系也使我们得出了前面提到的 O(N log N)的时间复杂度。

理论上看一切都很好!但是这真有效吗?让我们通过编写一个简单的 Python 函数,使用 FFT 算法来计算 DFT,来验证一下。以下是代码:

import numpy as np

def nice_fft(signal):
    # Get the number of samples in the signal
    N = len(signal)

    # Base case: if the signal has only 1 samples, use simple_dft
    if N == 1:
        return simple_dft(signal)
    else:
        # Initialize an empty list to store the result (DFT coefficients)
        res = []

        # Separate the signal into even and odd terms
        even_terms = signal[::2]
        odd_terms = signal[1::2]

        # Recursively compute FFT for even and odd terms
        f1 = nice_fft(even_terms)
        f2 = nice_fft(odd_terms)

        # Combine the results using the Cooley-Tukey FFT algorithm
        for k in range(N):
            # Calculate the complex exponential term
            mult = np.exp(-2 * np.pi * 1j * k / N)
            # Determine the index for the even and odd terms
            INDEX = (k % int(N / 2))
            # Combine the results for the current frequency bin
            dft_value = f1[INDEX] + mult * f2[INDEX]
            # Append the result for the current frequency bin to the list
            res.append(np.round(dft_value, 5))

        # Return the list of DFT coefficients
        return res

nice_fft([1, 2, 0, 5, 9, 2, 0, 4])
# Output: [(23+0j), (-8.70711-0.70711j), (10+5j), (-7.29289-0.70711j), 
# (-3-0j), (-7.29289+0.70711j), (10-5j), (-8.70711+0.70711j)]

它给出的结果与之前相同,但速度快得多!上述代码遵循了基于分治策略的递归方法。请注意,为了简化起见,这段代码仅适用于长度是 2 的幂的信号。对于长度不是 2 的幂的信号,我们可以简单地在开始或结束处附加 0 以获得所需的结果。为了测试我们的两个函数(简单的 DFT 和优化的 FFT),我们可以尝试生成一个大小对应于较大 2 的幂的随机数组,并测量所花费的时间:

import timeit

# Generate a random array of size 2¹⁴ (16384)
random_array = np.random.rand(2**14)

# Measure the execution time for simple_dft
time_simple_dft = timeit.timeit(lambda: simple_dft(random_array), number=1)

# Measure the execution time for nice_fft
time_nice_fft = timeit.timeit(lambda: nice_fft(random_array), number=1)

# Print the results
print(f"Time taken for simple_dft: {time_simple_dft:.5f} seconds")
# Output: Time taken for simple_dft: 149.81244 secondss
print(f"Time taken for nice_fft: {time_nice_fft:.5f} seconds")
# Output: Time taken for nice_fft: 1.28395 seconds

这是一次巨大的改进。为了更好地了解时间差异,我们还可以绘制一个线性图(对数刻度),显示不同大小数组所花费的时间差异:

# Define array sizes to test
array_sizes = [2**n for n in range(5, 14)]  # Sizes from 2⁵ to 2¹⁴

# Measure execution time for each array size
time_simple_dft = []
time_nice_fft = []

for size in array_sizes:
    random_array = np.random.rand(size)
    time_simple_dft.append(timeit.timeit(lambda: simple_dft(random_array), number=1))
    time_nice_fft.append(timeit.timeit(lambda: nice_fft(random_array), number=1))

# Plotting
import matplotlib.pyplot as plt
plt.figure(figsize=(10, 6))
plt.plot(array_sizes, time_simple_dft, label='simple_dft')
plt.plot(array_sizes, time_nice_fft, label='nice_fft')
plt.xlabel('Array Size')
plt.ylabel('Time (seconds)')
plt.title('Execution Time for simple_dft and nice_fft')
plt.legend()
plt.show()

FFT 与直接 DFT(时间,对数刻度)

这不酷吗?一个简单的对称性想法嵌入在优雅的分治框架中,竟然产生了如此出色的算法!

矩阵视图

在本文中,我们从傅里叶变换的公式出发,通过将奇数项和偶数项分开来推导出 FFT 算法。还有另一种方式来看待它:通过矩阵操作的视角。这个想法是将傅里叶变换看作是简单地将输入信号与一个矩阵(称为傅里叶矩阵)相乘。回想傅里叶变换的定义:

正如你所看到的,每个单独的 DFT 是通过简单地对信号测量值进行线性组合来计算的。我们可以取α = exp{-2πi/N},我们得到:

这使我们能够使用向量和矩阵的简单符号表示信号及其变换:

整个 DFT 归结为找到那个大的 N × N 矩阵 F(称为傅里叶矩阵),并将其与输入信号相乘。使用 FFT 算法,我们可以将傅里叶矩阵分解为 3 个稀疏矩阵的乘积:

现在这看起来可能有些令人难以应付,但从根本上讲,它仅仅是在使用矩阵表达我们之前提到的分治法。I_{N/2} 只是已知的 N/2 行/列的单位矩阵。D_{N/2} 仅仅是 N × N 傅里叶矩阵 F 的前 N/2 × N/2 部分的对角线元素。这可以通过 O(N) 时间轻松计算,因为它只需要我们计算 1, α, α⁴, …, α^{(N/2–1)²} 的值,这些值正好对应于我们原始公式中的乘法项。

F_{N/2} 对应递归子问题,即 N/2 × N/2 的傅里叶矩阵。最后,P 是一个置换矩阵(一个填充了 0,且每一行/列中只有一个 1 的矩阵)。P 的作用是通过将偶数项移动到上面,将奇数项移动到底部,从而将输入信号的奇偶项区分开来。矩阵的其余部分操作如之前所示。我们可以不断重复这个过程,一次次地将傅里叶矩阵 F_{N/2} 分解,直到我们到达基本情况,即 N = 1。和之前一样,时间复杂度仍然是 O(N log N),这只是以更优雅的方式写出方程,而不需要繁琐的求和!

结论

快速傅里叶变换(FFT)是算法设计中简单性与优雅性的典范。它彻底改变了信号处理、数据分析和各个科学领域的重要性,FFT 的价值不仅体现在其计算效率上,这一点通过与朴素方法相比获得的显著速度提升得到了证明,还体现在它的多功能性,推动了电信、图像处理和量子计算等多个领域的突破。从音频压缩算法到医学影像技术,FFT 支撑着无数已经成为我们日常生活不可或缺的应用。

当我们回顾从一个简单的想法到开创性算法的过程时,令人敬畏的是,我们能体会到对对称性的基础理解,再加上创新的算法设计,如何能带来具有深远意义的解决方案。FFT 以其优雅与高效,概括了计算机科学中独创性的精髓。因此,下次当你惊叹于数字图像的清晰度,或享受音乐流媒体的音质时,记得背后支撑这些技术奇迹的,正是那个非凡的 FFT —— 这是真正简单而巧妙的创意力量的见证。

希望你喜欢这篇文章!如果你有任何疑问或建议,请在评论区留言。你也可以通过邮件与我联系。

如果你喜欢我的文章并想阅读更多,请关注我。

注意: 所有图片(封面图片除外)均由作者制作。

参考文献

  1. cs.cornell.edu. www.cs.cornell.edu/~bindel/class/cs5220-s10/slides/FFT.pdf。 [访问时间:2024 年 1 月 5 日]。

  2. 快速傅里叶变换(FFT):有史以来最巧妙的算法? — youtube.com. www.youtube.com/watch?v=h7apO7q16V0. [访问时间:2024 年 1 月 5 日]。

  3. Shaw Talebi. 快速傅里叶变换(FFT) — medium.com. medium.com/swlh/the-fast-fourier-transform-fft-5e96cf637c38#:~:text=The%20FFT%20is%20an%20efficient,the%20Permutation%20matrix%2C%20used%20above. [访问时间:2024 年 1 月 5 日]。

  4. Jake VanderPlas. 理解 FFT 算法 — Pythonic Perambulations — jakevdp.github.io. https://jakevdp.github.io/blog/2013/08/28/understanding-the-fft/. [访问时间:2024 年 1 月 5 日]。

数据科学家的算法思维

原文:towardsdatascience.com/algorithmic-thinking-for-data-scientists-4601ac68496f?source=collection_archive---------1-----------------------#2024-05-28

如何编写节省时间和空间的代码

Chinmay KakatkarTowards Data Science Chinmay Kakatkar

·发表于 Towards Data Science ·阅读时间:20 分钟·2024 年 5 月 28 日

--

图片由 Jose Castillo 提供,来源于 Unsplash

注意: 以下所有示例代码片段均由本文作者编写。

算法思维 是将严谨的逻辑和创造力结合起来,用以框定、解决和分析问题,通常借助计算机的帮助。涉及某种排序、查找和优化形式的问题与算法思维密切相关,且常出现在数据科学项目中。算法思维帮助我们以高效利用时间和空间(例如计算机的磁盘空间或内存)的方式解决这些问题,从而得到快速且节省资源的算法。

即使在可预见的未来,存储和计算的成本持续下降,算法思维在数据科学项目中的重要性仍然不会因为以下几个关键原因而减弱。首先,客户的需求往往超过了现有解决方案的能力,在许多商业应用场景中,无论数据科学管道的底层复杂性如何(从数据获取、转换到建模和供应),这一点都成立。客户期望那些需要数天或数小时才能完成的任务变得只需要几分钟或几秒钟,而那些需要几分钟或几秒钟的任务则希望能在眨眼间完成。第二,越来越多的应用场景涉及设备端分析(例如,在嵌入式系统、物联网和边缘计算的背景下),这要求计算资源更加高效;存储空间和内存非常紧张,可能无法将计算任务转移到云端更强大、更集中的基础设施上。第三,工业数据科学管道的运行可能会消耗大量能源,从而加剧持续的气候危机。深入理解算法思维可以帮助数据科学家构建高效且可持续的解决方案,以应对这些挑战。

尽管拥有计算机科学学位的数据科学家会熟悉算法思维的核心概念,但越来越多的人以其他背景进入该领域,这些背景涵盖了自然科学、社会科学以及艺术等多个领域;随着生成性人工智能的进步和数据科学在学校及大学课程中日益普及,这一趋势在未来几年可能会加速。因此,本文接下来的章节主要面向那些不熟悉算法思维的读者。我们将从算法问题解决过程的高层次概述开始,然后通过研究一些发布在HackerRank上的编程挑战(一个广泛被公司用于招聘数据科学家的平台)以动手的方式帮助读者逐步建立算法思维的直觉。我们还将介绍一些有用的进一步阅读资源。最后,我们将简要讨论算法思维在 AI 辅助软件开发中的相关性(例如,使用 GitHub Copilot),并作总结。

如何解决问题

本节的标题也是一本著名书籍的标题,该书最早由匈牙利裔美国数学家、斯坦福大学教授乔治·波利亚于 1945 年出版。在《如何解题》link)中,波利亚提出了一个看似简单但极为有效的四步法,适用于算法问题解决:

  1. 理解问题:仔细框定问题,充分考虑问题和解决方案空间的任何限制(例如,允许的输入数据类型和数据范围、输出格式、最大执行时间)。提出类似“我能否用自己的话重新陈述问题?”和“我是否拥有足够的数据来实施一个有用的解决方案?”等问题,以检查自己的理解。使用具体的例子(或数据集)使问题及其边缘情况更加具体。花足够的时间在这一步通常能让后续步骤更容易执行。

  2. 制定计划:这通常涉及将问题分解为更小的子问题,对于这些子问题,可能已经有了有效的解决方案。识别和应用合适的现有解决方案来处理不同类型的子问题(例如,在搜索、排序等方面)是通过实践和经验获得的。但有时,可能需要额外的创造力,将多个现有方法结合,发明新的方法,或者借用其他领域的解决方案并通过类比加以应用。Pólya 提出了几个帮助思考过程的建议,例如画图并从期望目标反向推理。通常,在这一阶段,至少从高层次判断所制定的计划是否能够解决指定问题是很有用的。

  3. 执行计划:使用相关工具实施解决方案。在数据科学项目中,这可能涉及使用诸如 scikit-learn、PyTorch 和 TensorFlow 等机器学习库,以及 AWS、GCP 或 Azure 等平台来托管和运行流水线。在这一阶段,注重细节至关重要,因为即使是代码中的小错误也可能导致实现结果无法准确反映之前制定的计划,从而无法解决所述问题。添加足够的单元测试,以检查代码的不同部分是否正常工作,即使是边缘情况也要考虑在内。

  4. 回顾:“回顾”是大多数数据科学项目验证阶段的本能部分;例如,“新的机器学习模型是否比之前的更好?”这样的问题,只有通过收集和审查每个实验的相关指标才能回答。但是,回顾数据科学流水线的其他方面(例如,ETL 代码、测试用例、产品化脚本)和人工智能生命周期管理(例如,自动化水平、数据隐私和安全性、生产中的反馈环实施)同样至关重要,这有助于改进当前项目并在未来的项目中做得更好,即使在快速节奏的工作环境中,找到时间进行这种全面的“回顾”可能会有挑战。

在波利亚问题解决过程中,步骤 1 和步骤 2 特别难以正确完成。以概念上逻辑且系统的方式框定问题或解决方案通常是一项复杂的任务。然而,熟悉 概念框架(用于表示抽象概念的分析结构)可以大大帮助解决这个问题。常见的概念框架包括树形图、矩阵、流程图和关系图。由本文作者编写的书籍 Conceptual Frameworks: A Guide to Structuring Analyses, Decisions and Presentations链接)介绍了如何以易于理解的方式理解、创建、应用和评估这些概念框架。

算法复杂度

在算法问题求解的背景下,有一个值得特别注意的话题,那就是 复杂度。在比较两个不同的算法时,考虑每个算法的时间和空间复杂度是非常有用的,即每个算法在相对于问题规模(或数据规模)的时间和空间消耗如何变化。你应该了解五种基本的复杂度等级,从最低(最好)到最高(最差)。为了简化讨论,下面我们仅从时间复杂度的角度描述它们:

  1. 瞬时:无论问题规模如何,算法都能瞬间执行。例如,要判断一个整数是否为偶数,我们可以简单地检查它的最右边的数字是否能被二整除,无论这个整数有多大。通过索引访问列表元素通常也可以瞬间完成,不管列表的长度如何。

  2. 对数级:对于大小为 n 的数据集,算法执行大约 log(n) 步骤。请注意,对数的底数可能不同(例如,log2(n) 用于二分查找,因为每次迭代都将问题的大小减半)。像瞬时算法一样,具有对数复杂度的算法也很有吸引力,因为它们相对于问题规模是次线性扩展的。

  3. 线性:顾名思义,对于大小为 n 的数据集,具有线性复杂度的算法大约执行 n 步。

  4. 多项式:该算法的执行时间是 (二次)、(三次)或更一般的 x^m 步骤,其中 m 为某个正整数。检查代码中多项式复杂度的常见方法是计数嵌套循环的数量;例如,包含两个嵌套循环(循环内有循环)的函数,其复杂度为 x²,包含三个嵌套循环的函数复杂度为 x³,依此类推。

  5. 指数级:算法执行的时间步长为2x*,*3x,或者更一般地,m^x,其中m是某个正整数。请参考 StackExchange 上的这些帖子(链接 1链接 2),了解为什么指数函数最终会比多项式函数增长更快,因此在处理大规模问题时,指数函数的算法复杂度会更差。

一些算法可能会表现出加法乘法的组合复杂度。例如,一个 for 循环后接二分查找会导致线性和对数复杂度的加法组合,这是由于循环和查找过程分别是顺序执行的。相比之下,如果每次迭代中都进行二分查找,那么将会是线性和对数复杂度的乘法组合。虽然乘法组合通常比加法组合开销更大,但有时是不可避免的,且仍然可以进行优化。例如,像归并排序这样的排序算法,其时间复杂度为nlog(n),比选择排序的二次时间复杂度要便宜(参见这篇文章,里面有不同排序算法复杂度的对比表)。

通过示例问题建立直觉

在接下来的内容中,我们将研究在HackerRank上发布的一些问题。类似的问题也可以在LeetCodeCodeWars等平台上找到。研究这些平台上发布的问题有助于训练你的算法思维能力,可以帮助你更轻松地应对技术面试(招聘经理通常会向申请数据科学岗位的候选人提出算法问题),并且可能会产出一些可以在工作中复用的代码。

以下所有的示例代码片段均由本文作者编写,使用了 C++,这是一种在构建快速数据管道时常被从业者选择的语言。根据需要,这些代码片段也可以很容易地转化为其他语言,如 Python 或 R。为了简化代码片段,我们假设代码文件的顶部包含以下几行:

#include <bits/stdc++.h>
using namespace std;

这将使我们能够在代码中省略“std::”,从而让读者更专注于算法本身。当然,在实际的 C++代码中,只有相关的库会被包含,且“std::”会按照编码规范显式写出。

当公式能派上用场时

一个最初看起来需要通过多项式复杂度的迭代解法(例如,使用 for 循环、while 循环或列表推导式)的问题,有时可以通过一个公式在瞬间得出所需的答案,从而避免复杂的迭代。

考虑数字线跳跃问题(链接)。有两只袋鼠被放置在数字线上某个位置(分别在位置x1x2),并且可以通过跳跃移动。第一只袋鼠每次跳跃可以移动v1米,而第二只袋鼠每次跳跃可以移动v2米。给定x1v1x2v2的输入值,任务是确定是否有可能在未来的某个时间步,两个袋鼠会出现在数字线上的相同位置,假设每只袋鼠每个时间步只能跳一次;解决函数应相应地返回“YES”或“NO”。

假设x1小于x2。一种方法是实现一个循环,检查从x1出发的袋鼠是否会追上从x2出发的袋鼠。换句话说,我们会检查是否存在一个正的(整数)时间步长,使得x1 + v1t = x2 + v2t。如果x1大于x2,我们可以交换相应变量中的值,并遵循上述相同的方法。但是,如果t很大,这样的解决方案可能需要很长时间才能执行,甚至如果袋鼠永远不会相遇,它可能会导致无限循环(导致超时或崩溃)。

我们可以做得更好。让我们重新排列上面的方程,解出正整数t。我们得到t = (x1 — x2)/(v2 — v1)。当v2 = v1时,这个t方程是未定义的(由于除以零),但在这种情况下,如果两只袋鼠起始位置相同,我们可以返回“YES”,因为两只袋鼠显然会在下一个时间步到达数字线上的相同位置。此外,如果两只袋鼠的跳跃距离相同,但起始位置不同,那么我们可以直接返回“NO”,因为从左边出发的袋鼠永远追不上右边的袋鼠。最后,如果我们找到一个正的t解,我们应该检查它是否也是一个整数;这可以通过将t转换为整数数据类型并检查其是否等于原始值来实现。下面的示例代码片段实现了这个解决方案。

string kangaroo(int x1, int v1, int x2, int v2) {
    if((v2 == v1) && (x1 != x2)) return "NO";
    float t = 1.*(x1 - x2)/(v2 - v1);
    return ((0 < t) && (t == (int) t)) ? "YES" : "NO";
}

从多个选项中选择

解决同一问题可能有多种有效的方法。在找到一种解决方案后,尝试寻找其他解决方案仍然是有意义且值得的;每种方法都有其优缺点,使得它更适合或不适合特定的问题上下文。为了说明这一点,我们将在下面以不同程度的细节看三个问题。

首先,考虑电影中的美丽日子问题(链接)。阅读问题描述后,可以明显看出,解决问题的关键部分是编写一个函数来反转一个正整数。例如,123 的反转是 321,12000 的反转是 21(注意反转后的数字没有前导零)。

一种解决方法(称为reverse_num_v1)使用除法和取余操作的组合,将最右边的数字移动到最左边的位置,从而自然处理前导零问题;请参见下面的示例实现。这个方法的吸引力在于,由于数字的位数是相对于数字大小对数增长的,reverse_num_v1的时间复杂度是子线性的;空间复杂度也可以忽略不计。

int reverse_num_v1(int x) {
    long long res = 0;
    while (x) {
        res = res * 10 + x % 10;
        x /= 10;
        // Check for integer overflow
        if (res > INT_MAX || res < INT_MIN) return 0;
    }
    return res;
}

另一种方法(称为reverse_num_v2)使用将整数转换为字符串数据类型、反转字符串、去掉前导零、将字符串重新转换为整数并返回结果的思路;请参见下面的示例实现。

int reverse_num_v2(int x) {
    string str = to_string(x);
    reverse(str.begin(), str.end());
    // Remove leading zeros
    str.erase(0, min(str.find_first_not_of('0'), str.size()-1));
    int res = stoi(str);
    // Check for integer overflow
    return (res > INT_MAX || res < INT_MIN) ? 0 : res;
}

这种类型转换在许多编程语言中都是常见做法(如 C++、Python 等),字符串反转和去掉前导零的库函数也可能随时可用,而将多个函数链式调用以形成数据转换操作流水线,是数据科学项目中的典型模式;因此,reverse_num_v2可能是许多数据科学家首先想到的方法。然而,如果内存空间紧张,reverse_num_v1可能是更好的选择,因为整数的字符串表示形式会占用比整数本身更多的空间(请参阅C++中不同数据类型的内存要求文档)。

接下来,让我们简要考虑两个问题,时间转换link)和魔方阵的构造link)。虽然这些问题在表面上看起来非常不同,但相同的技术——即使用查找表(或映射表)——可以用来解决这两个问题。在时间转换的情况下,可以使用查找表在 12 小时制和 24 小时制之间为下午时间(例如,下午 8 点映射到 20 点,下午 9 点映射到 21 点,以此类推)提供即时映射。在魔方阵的构造问题中,问题被限制为由 3 行 3 列组成的魔方阵,而恰好有 8 个这样的魔方阵。通过将这 8 个魔方阵的配置存储在查找表中,我们可以实现一个相对简单的解决方案,尽管在HackerRank上它的“中等”难度评级。读者可以通过上面提供的链接更详细地阅读这些问题,但每个解决方案的相关示例代码片段已在下方显示,供比较使用。

时间转换

string timeConversion(string s) {
    // substr(pos, len) starts at position pos and spans len characters
    if(s.substr(s.size() - 2) == "AM") {
        if(s.substr(0, 2) == "12") return "00" + s.substr(2, s.size() - 4);
        else return s.substr(0, s.size() - 2);
    }
    else {
        // PM means add 12 to hours between 01 and 11
        // Store all 11 mappings of afternoon hours in a lookup table/map
        map<string, string> m = {
            {"01", "13"}, {"02", "14"}, {"03", "15"}, {"04", "16"},
            {"05", "17"}, {"06", "18"}, {"07", "19"}, {"08", "20"},
            {"09", "21"}, {"10", "22"}, {"11", "23"}
        };
        string hh = s.substr(0, 2);
        if(m.count(hh)) return m[s.substr(0, 2)] + s.substr(2, s.size() - 4);
        else return s.substr(0, s.size() - 2);
    }
}

魔方阵的构造

请注意,尽管下面的部分代码使用了 3 个嵌套的 for 循环,但实际上只需要 72 次简单操作的循环(833=72)来解决这个问题。

int formingMagicSquare(vector<vector<int>> s) {
    // Store all 8 possible 3x3 magic squares in a lookup table/matrix
    vector<vector<int>> magic_squares = {
        {8, 1, 6, 3, 5, 7, 4, 9, 2},
        {6, 1, 8, 7, 5, 3, 2, 9, 4},
        {4, 9, 2, 3, 5, 7, 8, 1, 6},
        {2, 9, 4, 7, 5, 3, 6, 1, 8}, 
        {8, 3, 4, 1, 5, 9, 6, 7, 2}, 
        {4, 3, 8, 9, 5, 1, 2, 7, 6}, 
        {6, 7, 2, 1, 5, 9, 8, 3, 4}, 
        {2, 7, 6, 9, 5, 1, 4, 3, 8},
    };
    int min_cost = 81;  // Initialize with maximum possible cost of 9*9=81
    for (auto& magic_square : magic_squares) {
        int cost = 0;
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                cost += abs(s[i][j] - magic_square[3*i + j]);
            }
        }
        min_cost = min(min_cost, cost);
    }
    return min_cost;
}

分治法

当一个问题看起来太大或太复杂,无法一次性解决时,将原问题分解成更小的子问题通常是个好主意,这些子问题可以更容易地被解决。这些子问题的具体性质(例如,排序、搜索、转换),以及它们与原问题之间的“部分与整体”关系可能会有所不同。例如,在数据清理的情况下,这是数据科学中的常见问题,每个子问题可能代表数据清理过程中的一个特定的、顺序的步骤(例如,去除停用词、词形还原)。在一个“继续/停止”决策问题中,每个子问题可能反映了必须满足的更小的决策,只有当所有这些决策都为“继续”时,原问题才能解决为“继续”;用逻辑术语来说,可以将其看作是一个复杂的布尔语句,如A AND B

为了看看分治法如何在实践中运作,我们将考察两个表面上看似非常不同的问题。首先,让我们考虑电子商店问题(link),它本质上是关于约束优化的问题。给定一个总支出预算b和未排序的电脑键盘和 USB 驱动器价格列表(分别称为KD),目标是购买最昂贵的键盘和驱动器,而不超过预算。在HackerRank上发布的问题中,价格列表最多可以有 1000 个项目,但我们可以想象在实际中这些列表会更长。

一个简单的方法可能是用两个嵌套循环遍历价格列表KD,找出第i个键盘和第j个硬盘,使预算得到最大化利用。这种方法实现起来很简单,但如果KD很长,尤其是因为价格列表是未排序的,它会变得非常慢。事实上,简单方法的时间复杂度是二次方的,这对于大数据集的扩展并不乐观。一种更高效的方法是按如下方式进行。首先,排序两个价格列表。其次,选择较短的价格列表进行循环。第三,对于循环列表中的每个项目x,在另一个列表上进行二分查找,找到一个项目y(如果有的话),使得x + y不超过给定的预算b,并将这个结果保存在一个名为max_spent的变量中,变量在循环外部维护。在每次循环迭代中,只有当最新的键盘-硬盘组合的总成本在预算内且超过当前的max_spent值时,max_spent才会更新。

尽管在这个问题中无法避免同时搜索两个价格列表,但高效的方法通过选择较小的价格列表进行循环,并且关键的是,对较长的价格列表进行二分查找(执行时需要对数时间/子线性时间),大大减少了整体搜索时间。此外,虽然最初看起来预先排序两个价格列表会增加解决方案的复杂度,但排序实际上可以非常高效地完成(例如,使用归并排序),并且关键是,这使得可以对较长的价格列表进行二分查找。最终结果是,相比于朴素的方法,这种算法要快得多。下面是高效方法的示例实现:

int findLargestY(int x, int b, const vector<int>& v) {
    // Simple implementation of binary search
    int i = 0, j = v.size(), y = -1, m, y_curr;
    while (i < j) {
        m = (i + j) / 2;
        y_curr = v[m];
        if (x + y_curr <= b) {
            y = y_curr;
            i = m + 1;
        }
        else j = m;
    }
    return y;
}

int getMoneySpent(vector<int> keyboards, vector<int> drives, int b) {
    int max_spent = -1;
    sort(keyboards.begin(), keyboards.end());
    sort(drives.begin(), drives.end());
    // Use smaller vector for looping, larger vector for binary search
    vector<int> *v1, *v2;
    if(keyboards.size() < drives.size()) {
        v1 = &keyboards;
        v2 = &drives;
    }
    else {
        v1 = &drives;
        v2 = &keyboards;       
    }

    int i = 0, j = v2->size(), x, y;
    for(int i = 0; i < v1->size(); i++) {
        x = (*v1)[i];
        if(x < b) {
            y = findLargestY(x, b, *v2);  // Use binary search
            if(y != -1) max_spent = max(max_spent, x + y);
        }
        else break;
    }
    return max_spent;
}

接下来,我们来考虑攀登排行榜问题(链接)。假设你正在玩一款街机游戏,并希望在每次尝试后跟踪你在排行榜上的排名。排行榜使用密集排名,所以得分相同的玩家会获得相同的排名。例如,如果得分为 100、90、90 和 80,那么得分 100 的玩家排名第 1,得分 90 的两个玩家排名第 2,得分 80 的玩家排名第 3。排行榜以一个整数数组或列表表示(每个玩家的最高得分),按降序排列。问题的难点在于,每当有新得分加入排行榜时,确定最终的排名并非易事,因为该排名可能会被多个玩家共享。有关详细示例,请参见上述链接中的HackerRank问题描述页面。

尽管电子商店攀登排行榜问题在HackerRank上的难度评级分别为“简单”和“中等”,但后者从某种程度上更简单,因为排行榜已经是排序好的。下面的示例实现通过在已排序的排行榜上进行二分查找来获取每个新得分后的排名,利用了这一事实:

int find_rank(int x, vector<int>& v) {
    // Binary search of rank
    int i = 0, j = v.size(), m_pos, m_val;
    while(i < j) {
        m_pos = (i + j)/2;
        m_val = v[m_pos];
        if(x == m_val) return m_pos + 1;  // Return rank
        else if(m_val > x) i = m_pos + 1;  // Rank must be lower
        else j = m_pos;  // Rank must be higher since val < x 
    }
    if(j < 0) return 1;  // Top rank
    else if(i >= v.size()) return v.size() + 1;  // Bottom rank
    else return (x >= m_val) ? m_pos + 1 : m_pos + 2;  // Some middle rank
}

vector<int> climbingLeaderboard(vector<int> ranked, vector<int> player) {
    // Derive vector v of unique values in ranked vector
    vector<int> v;
    v.push_back(ranked[0]);
    for(int i = 1; i < ranked.size(); i++)
        if(ranked[i - 1] != ranked[i]) v.push_back(ranked[i]);
    // Binary search of rank in v for each score
    vector<int> res;
    for(auto x : player) res.push_back(find_rank(x, v));
    return res;
}

进一步阅读的资源

上述讨论的问题提供了算法思维的初步体验,但还有许多其他相关主题值得更深入研究。丹尼尔·津加罗(Daniel Zingaro)所著的书籍《算法思维:基于问题的入门》是继续你学习之旅的绝佳选择 (link)。津加罗具有引人入胜的写作风格,带领读者了解诸如哈希表递归动态规划图搜索等基本概念。书中还包含了关于大 O 符号的附录部分,它是一种表达和推理算法复杂度的便捷方法。另一本以易于理解的方式讲解几种基本算法的书是阿迪提亚·巴尔伽瓦(Aditya Bhargava)所著的《Grokking Algorithms》 (link)。这本书包含了若干有用的插图和 Python 代码片段,是在技术面试之前复习算法思维基础的好资源。

在动态规划方面,由安德烈·格列霍夫(Andrey Grehov)创作的一系列 YouTube 视频 (link to playlist) 提供了一个很好的入门介绍。动态规划是一个非常强大的工具,一旦学会,你会发现有很多机会将其应用于数据科学项目中,例如解决优化问题(其中某些数量如成本或收入必须分别最小化或最大化)或组合问题(其焦点是计数某些事物,实质上是在回答“有多少种方法可以做 XYZ?”)。动态规划可以有效应用于具备以下两个特性的题目:(1)最优子结构,即优化地解决问题的较小部分有助于解决更大的问题;(2)重叠子问题,即在解决一个子问题的过程中,已经计算出的结果可以在不重新计算的情况下用于解决另一个子问题(例如,使用记忆化缓存)。

最后,本文作者出版的博士论文《网络分析在营销科学中的高级应用》(链接)讨论了应用图论概念解决营销和创新管理中的一系列实际数据科学用例,诸如识别用于新产品开发的有前景的众包创意、动态定价、以及通过匿名化跟踪数据预测客户行为等。该论文展示了如何将表格数据或非结构化数据转化为由节点(实体)和边(实体间关系)组成的图/网络表示,这一过程可以揭示有价值的见解,并推动在广泛的数据科学问题中开发强大的预测模型。

《AI 辅助软件开发时代的算法思维》

2023 年 10 月,前谷歌计算机科学教授兼工程总监马特·沃尔什在哈佛大学进行了一场引人入胜的讲座(YouTube 链接)。他的演讲标题颇具挑衅性(大语言模型与编程的终结),并提出生成式人工智能——尤其是大语言模型的进展——可能会极大地改变我们开发软件的方式。他指出,人类可能仍然需要在一些角色中发挥作用,比如产品管理(定义软件应该做什么,以及为什么这么做)和软件测试/质量保证(确保软件按预期运行),但他认为,将问题规格转化为生产就绪的代码这一过程,可能在不久的将来通过人工智能实现大规模自动化。到 2023 年底,像 GitHub Copilot 这样的 AI 驱动工具已经显示出自动完成各种基本代码类型(例如,测试用例、简单的循环和条件语句)的能力,并表明它们有潜力提高开发人员的生产力——甚至有可能完全取代开发人员的需求。自那时以来,AI 在提供越来越精确的多模态预测方面取得了令人印象深刻的进展。

在这种背景下,考虑到本文的主题,值得思考的是,在 AI 辅助软件开发的时代,算法思维对数据科学家来说将保持多大程度的相关性。简短的答案是,算法思维可能比以往任何时候都更为重要。更长的答案首先需要承认,即使在今天,许多情况下也可以使用像 ChatGPT 或 GitHub Copilot 这样的生成性 AI 工具生成一个算法草稿版本(如上文所示的代码片段)。毕竟,这些 AI 工具是通过抓取互联网内容进行训练的,互联网上有大量的代码——但这些代码不一定是高质量的,可能会导致“垃圾进,垃圾出”的情况。因此,AI 生成的代码应该始终在用于任何数据科学项目之前进行彻底审查,这意味着仍然需要具备相关技术技能的人类审查员。

此外,AI 生成的代码可能需要定制和/或优化以适应特定的用例,单靠提示工程可能不足够。事实上,设计一个能够可靠生成所需代码的提示(捕捉提示工程师的隐性知识和动机)通常比直接用目标语言编写代码更冗长且耗时,而且无论哪种方法,都不能避免需要正确框定问题并制定合理的实施计划。像框定问题、规划、定制、优化和审查 AI 生成代码以适应特定用例等任务,可能仍然需要一定程度的算法思维,并且需要对代码背后的意图(即“为什么”)有深入的理解。在实践中,似乎不太可能很快将这类工作完全委托给 AI“副驾驶”——这不仅仅是因为涉及到伦理和法律问题;例如,想象一下,如果没有足够的人工监督,让自动驾驶汽车系统的物体避让软件由 AI 生成会是什么样的情形。

总结

算法思维可以帮助数据科学家编写快速且高效利用计算资源(如内存和存储)的代码。随着越来越多背景多样且缺乏足够算法思维训练的数据科学家进入这一领域,本文旨在填补这一知识空白。通过提供高层次的介绍和一些在技术面试中常见的实际示例,本文邀请读者迈出下一步,借助各种教育资源进一步学习算法思维。最终,算法思维是当今数据科学家必备的重要技能,未来在 AI 辅助的时代中依然将是一个值得掌握的技能。

关于 AI 智能体的一切:自治、推理、对齐等

原文:towardsdatascience.com/all-about-ai-agents-autonomy-reasoning-alignment-and-more-c71229f53ece?source=collection_archive---------5-----------------------#2024-10-10

TDS 编辑Towards Data Science TDS 编辑

·发布于 Towards Data Science ·发送为 通讯 ·4 分钟阅读·2024 年 10 月 10 日

--

想要写你的第一篇 TDS 文章吗?我们始终欢迎新作者的投稿

在最近几个月,智能体迅速崭露头角,成为利用人工智能力量执行日常任务的最有前途的方式之一。然而,随着它们越来越受欢迎,也带来了不小的困惑——从它们到底是什么(人类化的术语本身并未帮助澄清这一点)到它们如何以及在何种情况下能够有效使用。

本周,我们汇集了一系列强有力的近期文章,旨在帮助初学者和经验丰富的从业者理解这个话题,并就如何在自己的工作流程中采用智能体做出明智的决策。从智能体的核心特征到更广泛的推理和对齐问题,这些文章从技术和实践的角度探讨了智能体,并将其置于人工智能在我们日常生活中日益增长的影响力的背景下。让我们深入了解吧!

  • 什么才是一个真正的 AI 智能体?重新思考追求自治的意义 “即使我们能够构建完全自治的 AI 智能体,它们有多大可能是最适合用户的?”Julia Winn 探讨了智能体的基本特征,给我们对智能体是什么——以及不是——的理解增添了急需的细微差别,并提出了一种智能体行为的谱系,作为评估其适应特定任务的框架。

  • 探索使用网格世界解决 AI 对齐问题 在当前围绕 AI 安全性展开的辩论中,智能体处于什么位置?我们需要做什么才能确保它们产生的结果与创造者的目标一致?Tarik Dzekman 就一个棘手的话题展开了深刻的讨论:“构建能够解决问题的 AI 智能体有多困难,同时又不让它做出我们不希望它做出的决策。”

图片来源:Chris HardyUnsplash

  • AI 智能体:生成性 AI 中工具调用与推理的交集 AI 智能体的主要优势之一是能够以流畅、自动化且(理想情况下)可预测的方式,弥合不同工具和工作流之间的差距。Tula Masterman 的清晰概述重点讨论了推理是如何通过工具调用来表现的,探讨了智能体在使用工具时面临的一些挑战,并介绍了评估其工具调用能力的常见方法。

  • AI 开发者的困境:专有 AI 与开源生态系统 如果你正在项目中实现智能体(以及其他 AI 驱动的解决方案),你很快就会面临一个关键问题,那就是是依赖专有产品还是开源产品来实现目标。Gadi Singer 分享了每种方法的优缺点详细分析。

若想阅读更多关于从地理空间数据到专业探戈舞者评分复杂艺术等主题的精彩文章,千万不要错过本周推荐的阅读:

  • 在她的首篇 TDS 文章中,Ruth Crasto 分享了一份清晰且有用的指南,介绍了在神经网络中编码地理坐标的现代技术。

  • 如何获取用于高级分析的地理空间数据Amanda Iglesias Moreno 向我们介绍了使用五个专业 API 的步骤。

  • 我们能否通过统计学来解释像梅西和泰勒·斯威夫特这样的“GOATS”的成功?Tuan Doan尝试通过利用衡量惊讶的数学艺术来做到这一点。

  • 如果你是 R 用户,并且希望在超参数调优过程中利用 Python 的强大功能,Devashree Madhugiri 解释了如何借助 Reticulate 包和 Optuna 框架来实现这一目标。

  • 谁想来一场数学深潜?Sachin Date 带着又一篇内容丰富、通俗易懂的顶级探讨回归——这次是关于数学统计中的一个迷人概念:“神秘”的费舍尔信息。

  • 从全职员工到自由职业数据科学家的过渡可以是许多东西——值得的、令人震惊的、可怕的、解放的……CJ Sullivan的最新文章剖析了她在过去九个月中获得的见解,自从她做出转变以来。

  • 准备文本数据以供 AI 驱动的处理是许多从业人员日常工作中的关键步骤。Murilo Gustineli的这篇面向初学者的指南聚焦于文本标准化和标记化的方方面面。

  • 通过仔细分析 2024 年阿根廷探戈世界锦标赛的得分,Alexander Barriga 展示了统计分析和数据可视化如何帮助我们 发现模式并验证直觉。

感谢你对我们作者工作的支持!正如我们之前提到的,我们喜欢发布新作者的文章,因此,如果你最近写了一篇有趣的项目演示、教程或关于我们核心话题的理论反思,千万不要犹豫,与我们分享

直到下次 Variable,

TDS 团队

你所需要的只是保形预测

原文:towardsdatascience.com/all-you-need-is-conformal-prediction-726f18920241?source=collection_archive---------5-----------------------#2024-04-30

每个数据科学家都应该了解的不确定性量化的重要且易用的工具。

Jonte DanckerTowards Data Science Jonte Dancker

·发表于 Towards Data Science ·8 分钟阅读·2024 年 4 月 30 日

--

使用保形预测将一个点预测转换为预测区域,帮助我们获得更多关于模型不确定性的信息(图由作者提供)。

我们必须了解模型在做出预测时的确定性,因为错误的预测是有风险的。如果没有量化模型的不确定性,准确的预测和完全的猜测看起来是一样的。例如,一辆自动驾驶汽车必须确保驾驶路径上没有障碍物。我曾在另一篇文章中写到过这个问题。

那么,我们如何量化模型的不确定性呢?

这就是保形预测的作用所在。保形预测是一个不确定性量化的框架。该方法可以将任何点预测转化为统计学上有效的预测区域。这个区域可以是分类问题中的一组类别,也可以是回归问题中的一个区间。

我们如何使用保形预测将一个点预测转换为预测区域?

保形预测使用过去的经验来确定新预测的不确定性。要应用保形预测,我们需要一个不一致性评分、一个显著性水平 alpha 和一个校准集。

分析表格数据集,只需要统计学

原文:towardsdatascience.com/all-you-need-is-statistics-to-analyze-tabular-datasets-3a1717f92749?source=collection_archive---------6-----------------------#2024-09-10

分析表格数据集不需要深度学习或大型语言模型。我将演示如何通过(简单的)统计方法和像 PCA 这样的技术来揭示新的见解并提供可解释的结果。

Erdogan TaskesenTowards Data Science Erdogan Taskesen

·发布于Towards Data Science ·15 分钟阅读·2024 年 9 月 10 日

--

照片由Dan Cristian Pădureț提供,来源于Unsplash

表格数据集是最常见的数据形式之一,包含二进制、类别、文本和连续值等多种变量。例如,著名的表格数据集有泰坦尼克号数据集。在这类数据集中,主要的挑战是如何分析这些变量,因为类别值的分析需要与连续值不同的统计方法和/或模型,等等。此外,关键还在于确定数据集中的多重共线性,因为具有统计相似行为的变量可能会影响模型的可靠性。在这篇博客文章中,我将展示处理表格数据集的步骤,并展示如何通过统计检验,例如超几何检验,来揭示变量之间的关系。此外,我还将解释多重检验校正的重要性,并展示如何在表格数据集上应用主成分分析(PCA)

第一步是进行可视化检查。

关于非劣性假设检验你需要知道的一切

原文:towardsdatascience.com/all-you-need-to-know-about-non-inferiority-test-c58a74ec4cc5?source=collection_archive---------8-----------------------#2024-10-18

非劣性检验通过统计方法证明新的治疗方法在临床可接受的范围内,不比标准治疗差

Prateek JainTowards Data Science Prateek Jain

·发布于Towards Data Science ·6 分钟阅读·2024 年 10 月 18 日

--

prateekkrjain.com使用 Midjourney 生成

在处理一个最近的问题时,我遇到了一个熟悉的挑战——“我们如何确定一种新的治疗或干预方法至少与标准治疗一样有效?”乍一看,解决方案似乎很简单——直接比较它们的平均值,不是吗?但是当我深入研究时,我意识到事情并没有那么简单。在许多情况下,目标并不是证明新的治疗方法更好,而是要表明它并不比标准治疗差超过预定的边界。

这就是非劣性检验发挥作用的地方。通过这些检验,我们可以证明新的治疗方法或方案在效果上“不比对照组差”超过一个小的、可接受的范围。让我们深入探讨如何执行这一检验,最重要的是如何在不同情境下解释它。

非劣性检验的概念

在非劣性检验中,我们并不试图证明新的治疗方法比现有方法更好。而是试图表明新的治疗方法并不会明显更差。所谓“明显更差”的阈值被称为非劣性边界(Δ)。例如,如果Δ=5,那么新的治疗方法可以比标准治疗差最多 5 个单位,仍然被认为是可接受的。

这种分析特别有用,尤其是在新的治疗方法可能具有其他优势的情况下,例如更便宜、更安全或更易于实施。

假设的制定

每个非劣效性检验都从提出两个假设开始:

  • 零假设(H0):新治疗方法比标准治疗差超过非劣效性边界Δ。

  • 备择假设(H1):新治疗方法不比标准治疗差超过Δ。

当较高的值更好时:

例如,当我们测量药物效力时,较高的值更好,假设为:

  • H0:新治疗方法比标准治疗差至少Δ(即,μnew − μcontrol ≤ −Δ)。

  • H1:新治疗方法不比标准治疗差超过Δ(即,μnew − μcontrol > −Δ)。

当较低的值更好时:

另一方面,当较低的值更好时,例如当我们衡量副作用或错误率时,假设是反向的:

  • H0:新治疗方法比标准治疗差至少Δ(即,μnew − μcontrol ≥ Δ)。

  • H1:新治疗方法不比标准治疗差超过Δ(即,μnew − μcontrol < Δ)。

Z 统计量

为了进行非劣效性检验,我们计算Z 统计量,它衡量治疗之间观察到的差异与非劣效性边界的距离。根据较高或较低的值更好的情况,Z 统计量的公式会有所不同。

  • 较高的值更好时:

  • 较低的值更好时:

其中δ是新治疗与标准治疗之间的观察到的均值差异,SE(δ)是该差异的标准误差。

计算 P 值

p 值告诉我们新治疗与对照组之间观察到的差异在非劣效性边界的背景下是否具有统计学意义。不同情境下的工作原理如下:

  • 当较高的值更好时,我们计算

    p = 1 − P(Z ≤ 计算得出的 Z)

    因为我们正在检验新治疗是否不比对照组差(单边上尾检验)。

  • 当较低的值更好时,我们计算

    p = P(Z ≤ 计算得出的 Z)

    因为我们正在检验新治疗是否比对照组具有更低(更好)的值(单边下尾检验)。

理解置信区间

除了 p 值,置信区间提供了另一种关键方式来解读非劣效性检验的结果。

  • 较高的值更受欢迎时,我们关注置信区间的下限。如果它大于−Δ,我们就得出非劣效性结论。

  • 较低的值更受欢迎时,我们关注置信区间的上限。如果它小于Δ,我们就得出非劣效性结论。

置信区间通过以下公式计算:

  • 当较高的值更受欢迎时

  • 当较低的值更受欢迎时

计算标准误差(SE)

标准误差(SE)衡量的是两个组之间均值差异估计值的变异性或精确度,通常是新治疗组与对照组之间的差异。这是计算 Z 统计量和置信区间在非劣效性检验中的关键组成部分。

要计算两个独立组之间均值差异的标准误差,我们使用以下公式:

  • 两个均值之间

  • 两个比例之间

其中:

  • σ_newσ_control 分别是新治疗组和对照组的标准差。

  • p_newp_control 分别是新治疗组和对照组的成功比例。

  • n_newn_control 分别是新治疗组和对照组的样本大小。

α(α)的作用

在假设检验中,α(显著性水平)决定了拒绝零假设的临界值。对于大多数非劣效性检验,使用α=0.05(5%的显著性水平)。

  • 单侧检验中,α=0.05 对应一个临界Z 值为 1.645。这个值对于决定是否拒绝零假设至关重要。

  • 置信区间也基于这个 Z 值。对于 95%的置信区间,我们使用1.645作为置信区间公式中的乘数。

简单来说,如果你的Z 统计量大于1.645(对于更高的数值),或者小于-1.645(对于更低的数值),且置信区间的界限支持非劣效性,那么你可以自信地拒绝零假设并得出结论,认为新治疗方法是非劣效的

解释

让我们基于更高或更低的值是否更为理想以及 Z 统计量是正值还是负值,分四种关键情境来逐步解读Z 统计量置信区间

这是一个 2x2 框架:

结论

非劣效性检验在你想证明一种新治疗方法不比现有治疗方法差时非常有价值。理解 Z 统计量、p 值、置信区间以及α的作用将帮助你自信地解读结果。无论是更高还是更低的数值更为理想,我们所讨论的框架确保你能够对新治疗方法的有效性得出清晰、基于证据的结论。

现在,掌握了如何执行和解读非劣效性检验的知识后,你可以将这些技巧应用于各种现实世界中的问题。

祝你测试愉快!

注:除非另有说明,所有图片均为作者提供。

构建 Tableau 径向图所需了解的一切

原文:towardsdatascience.com/all-you-need-to-know-to-build-radial-charts-in-tableau-686efc218c9c?source=collection_archive---------12-----------------------#2024-10-17

你看完之后肯定永远不会忘记它!

Isha GargTowards Data Science Isha Garg

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

--

径向图一直让我感到畏惧,因为我认为它们很难——可能是因为我遇到的所有教程都提到过复制粘贴一些计算公式,却没有任何逻辑解释。我每次需要使用径向图时都应该去查找吗?!

噢,幸运的是,我不是一个复制粘贴的人。只有当我理解了某些内容时,我才能记住它们,而且我喜欢掌握我所做事情背后的概念。因此,我尝试在理解各种步骤背后的逻辑的同时创建一个径向图,而这就像是魔法在我面前展开。那么,我们开始吧!

请注意,本文所用的数据集是:

  • 除非另有说明,所有图片均来自作者。

  • *使用的数据集是:样本——超市数据集。

    这是一个免费的数据集,随 Tableau 附带。它包含有关产品、销售、利润等的信息,我们可以用来识别这个虚拟公司中需要改进的关键领域。*

我们将从理解如何在圆形上绘制数据开始。接着,我们将应用相同的原理来创建两种不同类型的径向条形图。

首先,介绍一些基础知识!

通过 BERT 视角理解 AlphaFold 2

原文:towardsdatascience.com/alphafold-2-through-the-context-of-bert-78c9494e99af?source=collection_archive---------7-----------------------#2024-10-07

理解 AI 在生物学中的应用对于机器学习工程师的意义

梅根·海因茨Towards Data Science 梅根·海因茨

·发布于 Towards Data Science ·8 分钟阅读 ·2024 年 10 月 7 日

--

图片由 Google DeepMind 提供,来源 Unsplash

AlphaFold 2 和 BERT 都是在 Google 2018 年深厚资金支持下研发的(虽然由不同部门开发:DeepMind 和 Google AI)。它们分别代表了自然语言处理(NLP)和生物学领域的前沿技术进展。对于 BERT 来说,这意味着在 GLUE(通用语言理解评估)和 SQuAD(斯坦福问答数据集)等基准测试中名列前茅。对于 AlphaFold 2(以下简称 AlphaFold)而言,它意味着在预测 3D 蛋白质结构方面达到近乎实验的准确度。在这两种情况下,这些进展主要归因于使用了变换器架构和自注意力机制。

我预计大多数机器学习工程师对 BERT 或 双向编码器表示(Bidirectional Encoder Representations from Transformers) 如何与语言工作有初步了解,但对同一架构如何应用于生物学领域的理解则较为模糊,仅停留在比喻性理解的层面。本文的目的是通过比较和对比 BERT 来解释 AlphaFold 开发和成功背后的概念。

提前声明:我是一名机器学习工程师,不是生物学家,只是一个好奇的人。

BERT 简介

在深入研究蛋白质折叠之前,让我们刷新一下对 BERT 的理解。从大致上看,BERT 是通过掩码令牌预测和下一个句子预测进行训练的。

一个掩码令牌预测的示例,其中“natural”是目标句子中的掩码令牌。(所有图片,除非另有说明,均由作者提供)

BERT 属于序列模型家族。序列模型是一类设计用于处理和理解顺序数据的机器学习模型,其中元素的顺序很重要。该家族的成员包括递归神经网络(RNNs)、长短期记忆网络(LSTMs)和变换器。作为一个变换器模型(像它更著名的亲戚 GPT),BERT 的一个关键突破是如何使训练能够并行化。RNN 和 LSTM 按顺序处理序列,这减慢了训练速度并限制了适用的硬件。变换器模型利用自注意力机制并行处理整个序列,从而允许训练利用现代 GPU 和 TPU,这些硬件经过优化以适应并行计算。

一次处理整个序列不仅减少了训练时间,还通过建模单词之间的上下文关系提高了嵌入效果。这使得模型能够更好地理解依赖关系,无论它们在序列中的位置如何。一个经典的例子说明了这个概念:“我在河岸边钓鱼”和“我需要把钱存进银行”。对于读者来说,bank 显然代表了两个不同的概念,但之前的模型难以区分它们。变换器中的自注意力机制使得模型能够捕捉到这些细微的差异。想深入了解这个话题,我推荐观看这个变换器神经网络图解:逐步解释

以前的 NLP 模型未能区分“bank”和“river bank”两种含义的示例句子。

RNN 和 LSTM 之所以存在困难的一个原因是它们是单向的,也就是说,它们按从左到右的顺序处理句子。所以如果句子被改写成“在银行,我需要存钱”,money 就不再能明确解释bank的含义。自注意力机制通过允许句子中的每个单词“关注”它前后所有的其他单词,从而消除了这种脆弱性,使得处理变成了“双向”的。

AlphaFold 与 BERT 的比较

现在我们已经回顾了 BERT 的基础知识,让我们将它与 AlphaFold 进行比较。像 BERT 一样,AlphaFold 也是一个序列模型。然而,AlphaFold 的输入不是句子中的单词,而是氨基酸序列多序列比对(MSA),而其输出/预测是蛋白质的三维结构

在深入了解它们是如何建模之前,让我们回顾一下这些输入和输出是什么。

第一个输入:氨基酸序列

氨基酸序列被嵌入到高维向量中,类似于文本在像 BERT 这样的语言模型中被嵌入的方式。

你在高中生物课上学过的提醒:组成蛋白质的氨基酸的特定序列是由 mRNA 决定的。mRNA 是从 DNA 中的指令转录出来的。当氨基酸被连接在一起时,它们通过各种化学键和力相互作用,导致蛋白质折叠成一个独特的三维结构。这个折叠的结构对蛋白质的功能至关重要,因为其形状决定了它如何与其他分子相互作用,并执行其生物学功能。由于 3D 结构对确定蛋白质功能如此重要,“蛋白质折叠”问题已成为过去半个世纪以来的重要研究课题。

生物学 101:DNA、mRNA 和氨基酸序列之间的关系

在 AlphaFold 出现之前,确定氨基酸序列如何折叠的唯一可靠方法是通过实验验证,采用如 X 射线晶体学、核磁共振光谱(NMR)和冷冻电子显微镜(cryo-EM)等技术。尽管这些方法准确,但它们耗时、劳动强度大且成本高昂。

那么,什么是 MSA(多序列比对),它为何是模型中的另一个输入?

第二输入:多序列比对,以矩阵形式在模型中表示。

氨基酸序列包含了构建蛋白质所需的指令,但也包括一些不那么重要或变化较大的区域。将其与语言进行类比,我认为这些不太重要的区域就像蛋白质折叠指令中的“停用词”。为了确定序列中哪些区域类似于停用词,构建了多序列比对(MSA),该比对通过具有相似功能的同源(进化相关的)蛋白质序列形成矩阵,其中目标序列是第一行。

序列的相似区域被认为是“进化上保守的”(即序列中的某些部分保持不变)。跨物种高度保守的区域在结构或功能上非常重要(例如酶的活性位点)。我不完美的比喻是将罗曼语系语言的句子排列起来,找出其中共同的重要单词。然而,这个比喻并未完全解释为何 MSA 在预测 3D 结构中如此重要。保守区域之所以如此关键,是因为它们使我们能够检测到氨基酸之间的共进化。如果两个残基在不同序列中趋向于协调地突变,这通常意味着它们在 3D 结构中彼此靠得很近,并且相互作用以保持蛋白质的稳定性。这种进化关系很难从单一氨基酸序列中推断出来,但在分析 MSA 时变得清晰可见。

MSA 的一个不完美比喻:就像比较浪漫语言中相似的单词(例如,“branches”:ramas, branches, rami, ramos, ramuri, branques),MSA 将序列对齐以揭示进化关系,追踪通过小的变异所共享的起源。

这里是自然语言处理与蛋白质折叠比较的另一个不同之处;MSA(多序列比对)必须构建,并且研究人员通常需要手动筛选它们,以获得最佳结果。生物学家使用像BLAST(基本局部比对搜索工具)这样的工具,搜索目标序列以找到“同源”或相似的序列。如果你研究的是人类,这可能意味着找到来自其他哺乳动物、脊椎动物或更远生物的序列。然后,考虑到相似长度和功能等因素,这些序列会被手动筛选。包括过多功能差异较大的序列会降低 MSA 的质量。这与自然语言模型训练数据的收集方式有巨大不同。自然语言模型是在从各个地方收集的大量数据上进行训练的。而生物学模型则需要高度熟练且具有争议的数据库构建者。

预测/输出的是什么?

在 BERT 中,预测或目标是被掩盖的标记或下一句。对于 AlphaFold,目标是蛋白质的 3D 结构,以蛋白质原子的 3D 坐标表示,这定义了折叠蛋白质中氨基酸的空间排列。每组 3D 坐标通过实验收集、审查并存储在蛋白质数据银行中。最近解决的结构作为评估的验证集。

AlphaFold 的输出通常是蛋白质的3D 结构,由组成蛋白质氨基酸的原子的x, y, z 坐标构成。

输入和输出是如何联系的?

目标序列和 MSA 都通过一系列变换器块(transformer blocks)独立处理,利用自注意力机制生成嵌入表示(embeddings)。MSA 嵌入表示捕捉了进化关系,而目标序列嵌入表示则记录了局部上下文。这些上下文嵌入随后被传入下游层,用于预测氨基酸之间的配对相互作用,最终推断蛋白质的 3D 结构。

在每个序列中,成对残基(指蛋白质序列中两个氨基酸之间的关系或相互作用)表示法预测氨基酸之间的空间距离和方向,这对建模蛋白质折叠时远离的部分如何接近至关重要。自注意力机制使得模型能够考虑序列和多序列比对(MSA)中的局部依赖和长程依赖关系。这一点非常重要,因为当序列被折叠时,序列中相距较远的残基可能会在空间上非常接近。

AlphaFold 的损失函数比 BERT 的损失函数复杂得多。BERT 不受空间或几何约束,其损失函数要简单得多,因为它只需要预测缺失的单词或句子关系。相比之下,AlphaFold 的损失函数涉及蛋白质结构的多个方面(如距离分布、扭转角度、三维坐标等),并且模型在几何和空间预测方面进行优化。这个由多个组件构成的损失函数确保了 AlphaFold 能够准确捕捉定义蛋白质最终结构的物理属性和相互作用。

虽然 BERT 的预测几乎不需要任何有意义的后处理,但预测的三维坐标会根据蛋白质的物理原理进行能量最小化和几何优化。这些步骤确保了预测的结构在物理上可行且在生物学上功能齐全。

结论

  • AlphaFold 和 BERT 都受益于变压器架构和自注意力机制。这些改进提高了上下文嵌入以及使用 GPU 和 TPU 时的训练速度。

  • AlphaFold 的数据准备过程比 BERT 复杂得多。从实验数据中筛选 MSA 比收集大量文本语料库要难得多!

  • AlphaFold 的损失函数必须考虑空间或几何约束,其复杂度远高于 BERT。

  • AlphaFold 的预测需要后处理,以确认预测在物理上是可行的,而 BERT 的预测则不需要后处理。

感谢您阅读到这里!我坚信跨职能学习,我认为作为机器学习工程师,我们可以通过挑战自己去学习超出我们直接领域的知识来学到更多。我希望在我的产假期间继续这个系列——《理解 AI 在生物学中的应用,面向机器学习工程师》。❤

精通数据可视化与 Altair 的图形语法

原文:towardsdatascience.com/altair-and-the-powerful-vega-lite-grammar-of-graphics-d1d864c3e08e?source=collection_archive---------7-----------------------#2024-02-27

数据可视化

使用 Altair 强大的 Python 库将数据转化为可视化图形。

Alan JonesTowards Data Science Alan Jones

·发表于 Towards Data Science ·阅读时间:20 分钟·2024 年 2 月 27 日

--

图形语法就像一套构建模块 —— 图片来自 Nik Shuliahin 💛💙Unsplash

早在 1999 年,已故的 Leland Wilkinson 就写了他具有开创性的著作 The Grammar of Graphics[1],在书中他解释了可以从类似于书面语言语法的构建模块中构建图表这一概念。

根据 H2O.ai 在他们对 Wilkinson 的精彩 致敬(也是他成为首席科学家的地方)中提到,“图形语法提供了一种新的方式来创建和描述数据可视化,它是一个语言 —— 或者说语法 —— 用于指定图表中的视觉元素,这一全新的理念从根本上塑造了现代数据可视化。”

十年后,出现了可能是这一思想最著名的实现 —— ggplot2,这是由新西兰学者、现为 RStudio 首席科学家的 Hadley Wickham 开发的 R 语言图表库。他在论文 A Layered Grammar of Graphics 和他的书籍 ggplot2[2] 中解释了 ggplot2。ggplot2 已成为最受欢迎的 R 包之一。

如果你是一个 Python 爱好者,你可能会觉得 ggplot2 和图形语法与你关系不大,因为……

减少 LLM 幻觉的代理方法

原文:towardsdatascience.com/an-agentic-approach-to-reducing-llm-hallucinations-f7ffd6eedcf2?source=collection_archive---------1-----------------------#2024-12-22

使用 LangGraph 缓解 LLM 幻觉的简单技巧

Youness MansarTowards Data Science Youness Mansar

·发表于 Towards Data Science ·8 分钟阅读·2024 年 12 月 22 日

--

图片来源:Greg RakozyUnsplash

如果你曾经使用过 LLM,你就会知道它们有时会产生幻觉。这意味着它们生成的文本要么没有意义,要么与输入数据相矛盾。这是一个常见的问题,会影响 LLM 驱动应用程序的可靠性。

在这篇文章中,我们将探索一些简单的技巧,来减少幻觉发生的可能性。通过遵循这些建议,你可以(希望)提高 AI 应用程序的准确性。

幻觉有多种类型:

  • 内在幻觉:LLM 的回答与用户提供的上下文相矛盾。这是指在当前上下文中,回答是可以被验证为错误的。

  • 外在幻觉:LLM 的回答无法通过用户提供的上下文进行验证。这是指回答可能正确也可能错误,但我们无法通过当前上下文确认其正确性。

  • 无关的幻觉:LLM 的回答没有回答问题或没有意义。这是指 LLM 无法遵循指示。

本文将针对上述所有类型进行讨论。

我们将列出一组有效的技巧和方法,这些方法可以以不同方式减少幻觉。

提示 1:使用基础数据

归属(Grounding)是在向 LLM 提出任务时,输入包含领域相关的附加上下文。这为 LLM 提供了正确回答问题所需的信息,并减少了幻觉的可能性。这也是我们使用增强检索生成(RAG)的原因之一。

例如,向 LLM 提问一个数学问题,或者在提供相关数学书籍的章节后向它提问,会得到不同的结果,第二种方式更有可能得到正确答案。

这里是我之前教程中的一个实现示例,当我在提问时提供了从文档中提取的上下文:

## 使用 Gemini 构建适用于任何类型 PDF 的文档 AI 管道

表格、图片、图形或手写内容不再是问题!完整代码已提供。

towardsdatascience.com

提示 2:使用结构化输出

使用结构化输出意味着强制 LLM 输出有效的 JSON 或 YAML 文本。这将帮助你减少无意义的废话,得到“直截了当”的答案,同时也有助于下一个提示,因为它使 LLM 的回应更容易验证。

下面是如何通过 Gemini 的 API 实现这一点:

import json

import google.generativeai as genai
from pydantic import BaseModel, Field

from document_ai_agents.schema_utils import prepare_schema_for_gemini

class Answer(BaseModel):
    answer: str = Field(..., description="Your Answer.")

model = genai.GenerativeModel("gemini-1.5-flash-002")

answer_schema = prepare_schema_for_gemini(Answer)

question = "List all the reasons why LLM hallucinate"

context = (
    "LLM hallucination refers to the phenomenon where large language models generate plausible-sounding but"
    " factually incorrect or nonsensical information. This can occur due to various factors, including biases"
    " in the training data, the inherent limitations of the model's understanding of the real world, and the "
    "model's tendency to prioritize fluency and coherence over accuracy."
)

messages = (
    [context]
    + [
        f"Answer this question: {question}",
    ]
    + [
        f"Use this schema for your answer: {answer_schema}",
    ]
)

response = model.generate_content(
    messages,
    generation_config={
        "response_mime_type": "application/json",
        "response_schema": answer_schema,
        "temperature": 0.0,
    },
)

response = Answer(**json.loads(response.text))

print(f"{response.answer=}")

其中“prepare_schema_for_gemini”是一个工具函数,用来准备模式以匹配 Gemini 的特殊要求。你可以在这里找到它的定义:code

这段代码定义了一个 Pydantic 模式,并将该模式作为查询的一部分传递给字段“response_schema”。这迫使 LLM 在回应时遵循这个模式,并使其输出更容易解析。

提示 3:使用思维链和更好的提示

有时,给 LLM 一些空间来推敲其回答,在最终给出答案之前,能帮助产生更高质量的回答。这种技术称为思维链(Chain-of-thoughts),它被广泛使用,因为它有效且非常容易实现。

我们还可以明确要求 LLM 如果无法找到足够的上下文来生成高质量的回答时,回答为“N/A”。这样它就有了一个简单的退出方式,而不是尝试回答自己无法回答的问题。

例如,让我们看看这个简单的问题和上下文:

背景

托马斯·杰斐逊(1743 年 4 月 13 日[公历 4 月 2 日]—1826 年 7 月 4 日)是美国政治家、种植园主、外交家、律师、建筑师、哲学家和开国元勋,曾于 1801 至 1809 年担任美国第三任总统。[6] 他是《独立宣言》的主要起草人。美国独立战争后,杰斐逊在 1801 年成为总统之前,曾担任乔治·华盛顿政府的首任美国国务卿,随后成为约翰·亚当斯政府的第二任副总统。杰斐逊是民主、共和主义和自然权利的主要倡导者,并在州、国家和国际层面制定了具有重要影响的文件和决策。(来源:维基百科)

问题

戴维斯·杰斐逊是哪一年去世的?

一个简单的方法得到如下结果:

回应

答案=‘1826’

显然这是错误的,因为杰斐逊·戴维斯在上下文中根本没有被提到。死于 1826 年的是托马斯·杰斐逊。

如果我们将回应的模式改为使用思维链:

class AnswerChainOfThoughts(BaseModel):
    rationale: str = Field(
        ...,
        description="Justification of your answer.",
    )
    answer: str = Field(
        ..., description="Your Answer. Answer with 'N/A' if answer is not found"
    )

我们还在添加更多细节,说明当问题无法使用上下文回答时,我们期待的输出是什么——“如果没有找到答案,请回复‘N/A’”。

使用这种新方法,我们得到了以下推理(记住,思维链):

提供的文本讨论的是托马斯·杰斐逊,而不是杰斐逊·戴维斯。没有包含任何关于杰斐逊·戴维斯死亡的信息。

最终的答案

答案=‘N/A’

太棒了!但是我们能否使用一种更通用的方法来检测幻觉?

我们可以使用代理!

提示 4:使用代理方法

我们将构建一个简单的代理,实施一个三步流程:

  • 第一步是包含上下文并向 LLM 提问,以便获得第一个候选回应及其用于回答的相关上下文。

  • 第二步是将问题和第一个候选回答重新表述为陈述句。

  • 第三步是让 LLM 验证相关上下文是否蕴含候选回应。这被称为“自我验证”:arxiv.org/pdf/2212.09561

为了实现这一点,我们在 LangGraph 中定义了三个节点。第一个节点将提问并包含上下文,第二个节点将使用 LLM 进行重述,第三个节点将检查该声明与输入上下文的蕴含关系。

第一个节点可以定义如下:

 def answer_question(self, state: DocumentQAState):
        logger.info(f"Responding to question '{state.question}'")
        assert (
            state.pages_as_base64_jpeg_images or state.pages_as_text
        ), "Input text or images"
        messages = (
            [
                {"mime_type": "image/jpeg", "data": base64_jpeg}
                for base64_jpeg in state.pages_as_base64_jpeg_images
            ]
            + state.pages_as_text
            + [
                f"Answer this question: {state.question}",
            ]
            + [
                f"Use this schema for your answer: {self.answer_cot_schema}",
            ]
        )

        response = self.model.generate_content(
            messages,
            generation_config={
                "response_mime_type": "application/json",
                "response_schema": self.answer_cot_schema,
                "temperature": 0.0,
            },
        )

        answer_cot = AnswerChainOfThoughts(**json.loads(response.text))

        return {"answer_cot": answer_cot}

第二个示例如下:

 def reformulate_answer(self, state: DocumentQAState):
        logger.info("Reformulating answer")
        if state.answer_cot.answer == "N/A":
            return

        messages = [
            {
                "role": "user",
                "parts": [
                    {
                        "text": "Reformulate this question and its answer as a single assertion."
                    },
                    {"text": f"Question: {state.question}"},
                    {"text": f"Answer: {state.answer_cot.answer}"},
                ]
                + [
                    {
                        "text": f"Use this schema for your answer: {self.declarative_answer_schema}"
                    }
                ],
            }
        ]

        response = self.model.generate_content(
            messages,
            generation_config={
                "response_mime_type": "application/json",
                "response_schema": self.declarative_answer_schema,
                "temperature": 0.0,
            },
        )

        answer_reformulation = AnswerReformulation(**json.loads(response.text))

        return {"answer_reformulation": answer_reformulation}

第三个节点如下:

 def verify_answer(self, state: DocumentQAState):
        logger.info(f"Verifying answer '{state.answer_cot.answer}'")
        if state.answer_cot.answer == "N/A":
            return
        messages = [
            {
                "role": "user",
                "parts": [
                    {
                        "text": "Analyse the following context and the assertion and decide whether the context "
                        "entails the assertion or not."
                    },
                    {"text": f"Context: {state.answer_cot.relevant_context}"},
                    {
                        "text": f"Assertion: {state.answer_reformulation.declarative_answer}"
                    },
                    {
                        "text": f"Use this schema for your answer: {self.verification_cot_schema}. Be Factual."
                    },
                ],
            }
        ]

        response = self.model.generate_content(
            messages,
            generation_config={
                "response_mime_type": "application/json",
                "response_schema": self.verification_cot_schema,
                "temperature": 0.0,
            },
        )

        verification_cot = VerificationChainOfThoughts(**json.loads(response.text))

        return {"verification_cot": verification_cot}

完整代码请见github.com/CVxTz/document_ai_agents

请注意,每个节点都使用其自己的模式进行结构化输出,并使用自己的提示。这是由于 Gemini 的 API 和 LangGraph 的灵活性所实现的。

让我们通过与上述相同的示例来演示这段代码 ➡️

(注意:我们在第一个提示中没有使用思维链,以便触发验证进行测试。)

上下文

托马斯·杰斐逊(1743 年 4 月 13 日[公历 4 月 2 日] — 1826 年 7 月 4 日)是美国政治家、种植园主、外交官、律师、建筑师、哲学家和建国父亲之一,曾于 1801 年至 1809 年担任美国第三任总统。[6] 他是《独立宣言》的主要作者。在美国独立战争后,托马斯·杰斐逊在 1801 年成为总统之前,曾担任乔治·华盛顿总统政府下的第一任美国国务卿,并在约翰·亚当斯总统政府下担任第二任副总统。杰斐逊是民主主义、共和主义和自然权利的主要支持者,他在州级、国家级和国际层面上起草了具有开创性的文件和决策。(来源:维基百科)

问题

戴维斯·杰斐逊是什么时候去世的?

第一节点结果(第一次回答):

相关上下文=‘托马斯·杰斐逊(1743 年 4 月 13 日[公历 4 月 2 日] — 1826 年 7 月 4 日)是美国政治家、种植园主、外交官、律师、建筑师、哲学家和建国父亲之一,曾于 1801 年至 1809 年担任美国第三任总统。’

答案=‘1826’

第二节点结果(答案重述):

陈述性答案=‘戴维斯·杰斐逊于 1826 年去世’

第三节点结果(验证):

推理=‘上下文中提到托马斯·杰斐逊于 1826 年去世。声明称戴维斯·杰斐逊于 1826 年去世。上下文中没有提到戴维斯·杰斐逊,只有托马斯·杰斐逊。’

蕴含=‘否’

因此,验证步骤拒绝了初始答案(两者之间没有蕴含)。现在我们可以避免将幻觉返回给用户。

提示:使用更强大的模型

由于预算或延迟限制,这个提示并不总是容易应用,但你应该知道,较强大的 LLM 较不容易产生幻觉。因此,如果可能的话,针对最敏感的使用场景,选择更强大的 LLM。你可以在这里查看幻觉的基准测试:github.com/vectara/hallucination-leaderboard。我们可以看到,在这个基准测试中,排名最前的模型(最少幻觉)也排名在传统 NLP 排行榜的前列。

来源:github.com/vectara/hallucination-leaderboard 来源许可证:Apache 2.0

结论

在本教程中,我们探讨了通过降低幻觉率来提高大型语言模型(LLM)输出可靠性的策略。主要建议包括仔细格式化和提示,以指导 LLM 的调用,并采用基于工作流的方法,在这种方法中,代理会被设计为验证自己的答案。

这包括多个步骤:

  1. 检索 LLM 用于生成答案的确切上下文元素。

  2. 将答案重新表述为更易验证的形式(以陈述句形式)。

  3. 指示 LLM 检查上下文与重述答案之间的一致性。

尽管所有这些技巧都可以显著提高准确性,但你应该记住没有任何方法是万无一失的。如果 LLM 在验证过程中过于保守,可能会拒绝有效的答案,或者遗漏真实的幻觉案例,因此,仍然需要对你的特定 LLM 工作流进行严格评估。

完整代码在github.com/CVxTz/document_ai_agents

感谢阅读!

使用 LLMs 和 LangGraph 进行文本数据提取的代理方法

原文:towardsdatascience.com/an-agentic-approach-to-textual-data-extraction-using-llms-and-langgraph-8abb12af16f2?source=collection_archive---------4-----------------------#2024-10-29

从非结构化的维基百科文本到结构化的 JSON:一步步指南。

Youness MansarTowards Data Science Youness Mansar

·发布于Towards Data Science ·阅读时间:8 分钟·2024 年 10 月 29 日

--

图片由Mesut Kaya提供,来自Unsplash

假设你被要求构建一个包含全球所有城市和旅游地点的清晰且结构化的数据集。这个数据集可以用来为用户提供旅行建议或规划旅行。你知道这些信息存在于维基百科中,并且由于社区的努力,数据会不断更新,但它们都以纯文本或半结构化的方式呈现,并不符合你的需求。

这种类型的问题,即缺失符合特定格式的高质量数据,是数据科学家和机器学习工程师在解决商业问题时面临的主要挑战之一。本文尝试将大规模的文本语料库转化为可用、相关且结构化的数据。

我们将探讨的解决方案的基本思路是利用 LLMs 的零样本能力来构建一个文本到结构化数据集的管道。虽然有多种方法可以实现这一目标,但我们将探索基于代理的方法来进行类似 JSON 的结构化信息解析。这涉及到将包含多个、可能相互关联的实体类型的复杂信息解析为结构化格式。

组织 Jupyter Notebook 可视化的技巧与窍门

原文:towardsdatascience.com/an-easier-way-to-wrangle-jupyter-notebook-visualizations-620a86cd9279?source=collection_archive---------2-----------------------#2024-01-22

通过自动化 matplotlib 输出,优化你的数据科学工作流程——只需要一行代码。下面是具体做法。

Matthew Andres MorenoTowards Data Science Matthew Andres Moreno

·发表于 Towards Data Science ·阅读时间 6 分钟·2024 年 1 月 22 日

--

给事物命名很难。在长时间工作后,我们常常会命名为像“graph7(1)_FINAL(2).png”和“output.pdf”这样高度描述性的文件名。看起来很熟悉吧?

我们可以做得更好——而且实际上很容易。

当我们使用数据导向的“seaborn-风格”的绘图机制时,构成描述性文件名的要素就齐备了。一个典型的调用看起来像这样:

sns.scatterplot(data=tips, x="total_bill", y="tip", hue="time")

在这里,我们可以看到“total_bill”位于x 轴上,“time”被颜色编码,等等。那么,如果我们利用绘图函数名称和这些语义化的列键*来为我们组织输出文件呢?

使用teeplot工具时,工作流程是这样的:

import seaborn as sns; import teeplot as tp
tp.save = {".eps": True, ".pdf": True}  # set custom output behavior
tp.tee(sns.scatterplot,
   data=sns.load_data("tips"), x="total_bill", y="tip", hue="time")

teeplots/hue=time+viz=scatterplot+x=total-bill+y=tip+ext=.eps

teeplots/hue=time+viz=scatterplot+x=total-bill+y=tip+ext=.pdf

在这个例子中,我们实际上做了三件事——1) 我们在笔记本中渲染了图形,2) 我们将可视化保存到文件,并使用了有意义的文件名,3) 我们将可视化集成到一个框架中,使得笔记本输出可以在全局范围内进行管理(在这种情况下,启用了eps/pdf 输出)。

本文将解释如何利用teeplot Python 包来更好地组织工作流程,释放你的思维负担,以便专注于更有趣的事情。

我是该项目的主要作者和维护者,我已经在自己的工作流程中使用了几年,并且认为足够有用,值得打包并与社区广泛分享。teeplot 是一个基于 MIT 许可的开源项目。

teeplot 工作流程

teeplot 旨在简化与使用 matplotlibseabornpandas 等库创建的数据可视化的工作。它作为您的绘图调用的封装器,帮助您处理输出管理。

teeplot 工作流示例

以下是 使用 teeplot3 个步骤

  1. 选择您的绘图函数: 首先选择您喜欢的绘图函数,无论是来自 matplotlibseabornpandas 等库,还是您自己编写的函数。

  2. 添加您的绘图参数: 将您的绘图函数作为第一个参数传递给 tee,然后是您希望用于可视化的参数。

  3. 自动绘图与保存: teeplot 捕捉您的绘图函数及其参数,执行绘图,然后为您处理图形输出。

就是这样!

接下来,让我们看 3 个简短的示例,展示:a) 基本使用b) 自定义后处理,和 c) 自定义绘图函数

示例 1:使用内建的 pandas 绘图工具

在这个示例中,我们将 DataFrame df 的成员函数 df.plot.box 作为绘图工具,传入两个语义关键字:“age”和“gender”。teeplot 会处理其余部分。

# adapted pandas.pydata.org/docs/reference/api/pandas.DataFrame.plot.box.html
import pandas as pd; from teeplot import teeplot as tp

age_list = [8, 10, 12, 14, 72, 74, 76, 78, 20, 25, 30, 35, 60, 85]
df = pd.DataFrame({"gender": list("MMMMMMMMFFFFFF"), "age": age_list})

tp.tee(df.plot.box,  # plotter...
    column="age", by="gender", figsize=(4, 3))  # ...forwaded to plotter

teeplots/by=gender+column=age+viz=box+ext=.pdf

teeplots/by=gender+column=age+viz=box+ext=.png

示例 2:使用 Matplotlib 进行手动调整

无论喜欢与否,从 matplotlib 及其衍生库中获得好结果,通常需要在初次绘图调用后进行一些手动调整。

teeplot 完全支持这种模式。只需传递 teeplot_callback 参数,teeplot 会返回一个可调用的句柄,以及初始绘图调用的输出。完成图形调整后,只需调用句柄即可像往常一样保存并显示。

# adapted from https://matplotlib.org/stable/tutorials/pyplot.html
from matplotlib import pyplot as plt
import numpy as np; from teeplot import teeplot as tp

# tee output format can be configured globally (or per call to tp.tee)
tp.save = {".eps": True}  # make calls output to .eps only

# set up example data
df = {'weight': np.arange(50), 'profit': np.random.randint(0, 50, 50),
        'carbon': np.random.randn(50)}
df['price'], = df['weight'] + 10 * np.random.randn(50)
df['carbon'] = np.abs(df['carbon']) * 100

# ----- begin plotting -----
saveit, __ = tp.tee(  # --- "saveit" is callback to finalize output
    plt.scatter,  # plotter...
    data=df,  # then plotting kwargs
    x='weight', y='price', c='profit', s='carbon',
    teeplot_callback=True)  # defer plotting to callback

# tweak visualization as you usually would...
plt.xlabel('entry a')  
plt.ylabel('entry b')
plt.gcf().set_size_inches(5, 3)
saveit()  # dispatch output callback

teeplots/c=profit+s=carbon+viz=scatter+x=weight+y=price+ext=.eps

请注意从上面的 tp.tee 调用中解包的 __ 值。这是因为 plt.scatter 的返回值是一个线条集合,对我们的调整没有用处。

示例 3:自定义绘图工具

自定义绘图工具的工作方式与外部库的绘图工具相同 —— teeplot 能根据绘图函数的名称推断 viz= 输出关键字。

from matplotlib import pyplot as plt; import seaborn as sns
from teeplot import teeplot as tp

def cuteplot(subject, descriptor, amount):  # custom plotter
    sns.dogplot()
    plt.gca().text(10, 400, 
        f"{subject} \n is a {descriptor} dog" + "!" * amount,
        color="white", size=40)

tp.tee(cuteplot,  # plotter
    amount=4, subject="who", descriptor="good",  # plotting args
    teeplot_outinclude="amount")  # override to use numeric kwarg in filename

teeplots/amount=4+descriptor=good+subject=who+viz=cuteplot+ext=.png

teeplots/amount=4+descriptor=good+subject=who+viz=cuteplot+ext=.pdf

通过 seaborn-data,BSD 许可证/版权 © 2012–2023, Michael L. Waskom

sns.dogplot 致敬……总是那么吵闹!

进一步信息

就这么简单!

我过去两年一直在定期使用这个工具,最近决定花时间将其打包并分享。希望它能成为社区的一个资产。

teeplot库具有一些额外的高级功能,超出了本文的讨论内容,例如通过环境变量进行配置(在 CI 中非常有用!)。您可以在项目的使用指南API 列表中了解更多信息。该项目是一个开源项目,托管在 GitHub 上的mmore500/teeplot,请考虑留下一个⭐️!

可以通过python3 -m pip install teeplot安装teeplot

作者身份

本教程由我贡献,Matthew Andres Moreno

我目前在密歇根大学担任博士后研究员,我的工作得到了 Eric 和 Wendy Schmidt AI in Science 博士后奖学金的资助,这是一个 Schmidt Futures 项目。

我的任命分配在大学的生态与进化生物学系、复杂性研究中心和密歇根数据科学研究所。

在 Twitter 上找到我 @MorenoMatthewA,在 GitHub 上找到我 @mmore500

免责声明:我就是teeplot库的作者。

引用

J. D. Hunter,“Matplotlib:二维图形环境”,《计算科学与工程》,第 9 卷,第 3 期,90–95 页,2007 年\。 https://doi.org/10.1109/MCSE.2007.55

数据结构用于 python 中的统计计算,McKinney,《第 9 届科学会议中的 Python》,第 445 卷,2010 年\。 https://doi.org/ 10.25080/Majora-92bf1922–00a

Matthew Andres Moreno。(2023)。mmore500/teeplot。Zenodo。 https://doi.org/10.5281/zenodo.10440670

Waskom, M. L.,(2021)。seaborn:统计数据可视化。《开放源软件期刊》,6(60),3021, https://doi.org/10.21105/joss.03021.

附录

要安装本文示例的依赖项,

python3 -m pip install \
    matplotlib `# ==3.8.2`\
    numpy `# ==1.26.2` \
    teeplot `# ==1.0.1` \
    pandas `# ==2.1.3` \
    seaborn `# ==0.13.0`

除非另有说明,所有图片均为作者作品。“dogplot”图片来自 seaborn,您可以在此查看 seaborn 的许可 here

一种理解 GraphRAG 工作原理的简单方法

原文:towardsdatascience.com/an-easy-way-to-comprehend-how-graphrag-works-6d53f8b540d0?source=collection_archive---------2-----------------------#2024-06-08

帮助你理解 GraphRAG 机制的示例

Rendy DalimuntheTowards Data Science Rendy Dalimunthe

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

--

开发一个能够应对实际问题并提供适当、准确答案的聊天机器人确实是一项艰巨的任务。尽管大型语言模型已经取得了显著进展,但一个开放的挑战是将这些模型与知识库结合,以提供可靠且具有上下文丰富的响应。

图片来源:Google DeepMindUnsplash

关键问题几乎总是归结于幻觉(模型生成错误或不存在的信息)和上下文理解,模型无法理解不同信息之间的微妙关系。其他人尝试构建强大的问答系统,但未取得太大成功,因为这些模型通常会返回不尽如人意的答案,尽管它们连接着全面的知识库。

尽管 RAG 可以通过将生成的响应与真实世界的数据连接来减少幻觉,但准确回答复杂问题却是另一回事。用户通常会看到类似“该主题在检索的文本中未明确涉及”的答案,即使知识库中明确包含该信息,尽管这种信息呈现得不那么显而易见。这正是 GraphRAG(图检索增强生成)派上用场的地方,它通过利用结构化的知识图谱来提高模型提供精确且具有上下文丰富答案的能力。

RAG:桥接检索与生成

RAG 代表了结合基于检索和基于生成方法的最佳实践的重要一步。给定一个查询,RAG 从大型语料库中检索相关文档或段落,然后基于这些信息生成答案。因此,可以确保生成的文本既具有信息性又与上下文相关,因为它是基于事实数据的。

例如,在像“法国的首都是什么?”这样的问题中,RAG 系统会在其语料库中查找与法国及其首都巴黎相关的文档。它会检索相关段落,并生成类似于“法国的首都是巴黎。”的回答。这种方式非常适合简单查询和清晰记录的答案。

然而,RAG 在处理更复杂的查询时会遇到困难,特别是在需要理解实体之间的关系时,尤其当这些关系在检索的文档中没有明确表达时。系统在遇到像“17 世纪的科学贡献如何影响 20 世纪初的物理学?”这样的提问时,可能会失败(稍后会详细讨论这个例子)。

GraphRAG:利用知识图谱的力量

GraphRAG,正如在微软研究博客中这里首次提出的那样,旨在通过将基于图的检索机制引入模型,克服这些限制。基本上,它将知识库的非结构化文本重新组织成结构化的知识图谱,其中节点代表实体(例如人、地点、概念),边代表实体之间的关系。这种结构化格式使得模型能够更好地理解和利用不同信息之间的相互关系。

图片由Alina Grubnyak提供,来源于Unsplash

现在让我们通过简单的方式来深入理解 GraphRAG 的概念,并与 RAG 进行比较。

首先,让我们假设有一个包含各种科学和历史文本的知识库,如下所示:

1. “阿尔伯特·爱因斯坦提出了相对论理论,彻底改变了理论物理学和天文学。”

2. “相对论理论在 20 世纪初提出,深刻影响了我们对空间和时间的理解。”

3. “艾萨克·牛顿以其运动定律和万有引力定律闻名,为经典力学奠定了基础。”

4. “1915 年,爱因斯坦提出了广义相对论,扩展了他早期的特殊相对论研究。”

5. “牛顿在 17 世纪的工作为现代物理学奠定了基础。”

在 RAG 系统中,这些句子将以非结构化文本的形式存储。例如,提问“17 世纪的科学贡献如何影响 20 世纪初的物理学?”可能会使系统陷入困境,如果文档的准确措辞和检索质量没有直接将 17 世纪的影响与 20 世纪初的物理学联系起来,RAG 可能会给出类似“艾萨克·牛顿在 17 世纪的工作为现代物理学奠定了基础,阿尔伯特·爱因斯坦在 20 世纪初发展了相对论”的回答,因为该机制能够检索到相关信息,但无法清晰解释 17 世纪物理学对 20 世纪初期发展的影响。

相比之下,GraphRAG 将这段文字转化为结构化的知识图。知识图展示了不同事物之间的关系。它使用一组本体论,即一套帮助组织信息的规则。通过这种方式,它不仅能发现明显的联系,还能找到隐藏的联系。

使用 GraphRAG 系统,之前的知识库将被转化为如以下所示的节点和边。

Nodes: Albert Einstein, theory of relativity, theoretical physics, astronomy, early 20th century, space, time, Isaac Newton, laws of motion, universal gravitation, classical mechanics, 1915, general theory of relativity, special relativity, 17th century, modern physics.
Edges:
- (Albert Einstein) - [developed] → (theory of relativity)
- (theory of relativity) - [revolutionized] → (theoretical physics)
- (theory of relativity) - [revolutionized] → (astronomy)
- (theory of relativity) - [formulated in] → (early 20th century)
- (theory of relativity) - [impacted] → (understanding of space and time)
- (Isaac Newton) - [known for] → (laws of motion)
- (Isaac Newton) - [known for] → (universal gravitation)
- (Isaac Newton) - [laid the groundwork for] → (classical mechanics)
- (general theory of relativity) - [presented by] → (Albert Einstein)
- (general theory of relativity) - [expanded on] → (special relativity)
- (Newton's work) - [provided foundation for] → (modern physics)

当被提问“17 世纪的科学贡献如何影响 20 世纪初的物理学?”时,基于 GraphRAG 的检索器能够识别从牛顿的工作到爱因斯坦的进展之间的联系,突出 17 世纪物理学对 20 世纪初期发展的影响。这种结构化的检索使得回答内容既富有背景信息又准确:“艾萨克·牛顿在 17 世纪提出的运动定律和万有引力定律为经典力学奠定了基础。这些原则影响了阿尔伯特·爱因斯坦在 20 世纪初发展相对论的过程,后者扩展了我们对时空的理解。”

GraphRAG 中结构化知识图的使用增强了模型回答复杂问题的能力,同时通过为答案提供明确的关系基础,减少了幻觉的发生。这本质上赋予了 GraphRAG 在开发更可靠、智能的对话式问答系统方面的有效性。

将非结构化的知识库转化为结构化图谱,也使 GraphRAG 能够从信息中获得更深层次的意义,从而使语言模型能够在上下文中准确生成适当的回答。这是对话式 AI 向更先进、更可靠的聊天机器人系统发展的一个重要进步。

然而,正如 GraphRAG 的其他优点一样,也存在一些挑战。

首先,构建图谱是困难的。将一个无序的知识库转化为结构化的知识图非常具有挑战性。这需要复杂的方法来提取实体和识别关系,这可能会非常消耗计算资源。

其次,出现了可扩展性问题。知识图谱随着知识库的规模增长而变得更加复杂。如果图谱变得过大,以至于在运行时无法轻松遍历,这可能会引发可扩展性问题。一个主要挑战将是优化大规模图谱的图谱检索算法。

第三点谈到了维护开销:知识图谱需要不断地更新新信息以及现有数据的变化。在一些领域,这些领域可能经常发生变化,这可能成为一项非常昂贵的操作,尤其是在技术或医学领域。这意味着,尽管结果可能很有前景,但需要付出大量的努力来维护知识图谱的正确性和相关性。

然而,GraphRAG 有望为未来的对话型 AI 代理带来更高的智能、可靠性和上下文意识。更多的研究和开发可以帮助缓解与 GraphRAG 相关的困难,为更复杂的 AI 驱动解决方案铺平道路。

从照片中轻松去除游客的方法

原文:towardsdatascience.com/an-easy-way-to-remove-tourists-from-photos-8cf18e1c569d?source=collection_archive---------6-----------------------#2024-09-02

快速成功的数据新闻

使用 Python、PIL 和 OpenCV 进行图像清理

Lee VaughanTowards Data Science Lee Vaughan

·发表于 Towards Data Science ·阅读时间:8 分钟·2024 年 9 月 2 日

--

DALL-E3 拍摄的泰姬陵照片

如果你拍摄著名地标的照片,或者进行天文摄影,你可能听说过图像叠加。这是一种将多张静态照片叠加(平均)在一起的过程,以去除噪声或其他不需要的元素。

这些技术可以去除照片站点上任何移动的物体,包括人。比如,Adobe Photoshop 就有一个“人群去除”脚本,能够神奇地消失不稳定的物体。它依赖于一种称为中位数的统计平均值,简单来说,就是按从小到大的顺序排列数字后处于“中间”位置的值。

这个过程需要多张照片。最好使用三脚架拍摄,这样你想去除的物体在不同的图像中会发生位置变化,而背景保持不变。通常需要 10 到 30 张相隔大约 20 秒的照片,或者从视频中提取的间隔相似的帧。

使用均值时,你将数字相加并除以总数。而使用中位数时,你将数字排序并选择中间的值。

在下图中,展示了一排五张图像,所有图像中的同一像素位置被标出…

下一代异常检测:解锁神经洞察

原文:towardsdatascience.com/an-exploration-of-model-state-data-in-anomaly-detection-e6860cbca160?source=collection_archive---------3-----------------------#2024-04-11

利用神经元的力量,彻底变革异常检测!

Sara NóbregaTowards Data Science Sara Nóbrega

·发表于Towards Data Science ·12 分钟阅读·2024 年 4 月 11 日

--

图片来源:作者。

你是否曾想过,有没有更好的方法来识别一组图像数据中哪些内容不属于其中?

传统方法有其适用的场合,但如果更加精细化的方法的关键就在模型内部——特别是在其神经元的状态中呢?

这些神经状态能否为我们提供一种新的视角,帮助我们发现那些我们之前忽视的异常?

让我们一探究竟!

我的名字是Sara,我拥有物理学硕士学位。目前,我在一家全球能源公司担任数据科学家。我写关于数据科学、人工智能工程、职业建议等方面的内容!随时欢迎关注我,获取有趣的内容!

我在刚毕业时进行这个项目,第一次踏入大公司。说实话,我有点害怕。这是我在职业生涯中的第一次真正的考验。作为一名物理学毕业生,我在硕士的最后一年学习了数据科学和机器学习,为的是能顺利转行开始我的职业生涯。

顺便说一下,我在这里详细介绍了我从物理学转向数据科学的过程:

贝叶斯建模用于因果发现的详尽入门指南

原文:towardsdatascience.com/an-extensive-starters-guide-for-causal-discovery-using-bayesian-modeling-cfd40839a308?source=collection_archive---------1-----------------------#2024-10-19

贝叶斯方法正在变得越来越流行,但刚开始时可能会让人不知所措。本篇详尽的指南将引导你了解因果发现方法的应用、库和依赖项。

Erdogan TaskesenTowards Data Science Erdogan Taskesen

·发表于 Towards Data Science ·35 分钟阅读·2024 年 10 月 19 日

--

无监督因果发现的全景。图片由作者提供。

贝叶斯技术的无限可能性也是其弱点;应用范围极广,理解这些技术与不同解决方案的关系以及应用的方式可能会很麻烦。在我之前的博客中,我写过关于结构学习、参数学习、推断以及不同贝叶斯库的比较概述等多个主题。在这篇博客中,我将引导你了解贝叶斯应用的全景,并描述不同的因果发现方法是如何与应用相匹配的。 换句话说,如何使用离散或连续数据集创建因果网络(有向无环图)? 你能否在有(无)反应/处理变量的情况下确定因果网络? 你如何决定使用哪些搜索方法,如 PC、爬山搜索等? 阅读完这篇博客后,你将知道从哪里开始,并且能够根据你的使用案例选择最合适的贝叶斯因果发现技术。 慢慢来,找些时间,抓住一杯

生命的幻象

原文:towardsdatascience.com/an-illusion-of-life-5a11d2f2c737?source=collection_archive---------10-----------------------#2024-11-07

现有的人工智能可能具备知觉吗?如果不能,那还缺少什么?

James F. O'BrienTowards Data Science James F. O'Brien

·发表于 Towards Data Science ·阅读时长 7 分钟·2024 年 11 月 7 日

--

当今的大型语言模型(LLMs)在生成听起来既深思熟虑又聪明的类人回应方面已经非常优秀。许多人认为,LLM 已经达到了艾伦·图灵的著名测试的标准,该测试的目标是在对话中像人类一样无法区分。这些 LLM 能够生成听起来既深思熟虑又聪明的文本,并且它们能够令人信服地模仿情感的表现。

智力的幻象

尽管当前的 LLM(大型语言模型)能够令人信服地模仿类人对话,但它们并不具备思考或情感的能力。它们生成的每个单词,都是基于从大量文本数据中学习的统计模式进行的预测。这一预测过程是不断重复的,每个单词都是一次次预测的结果。与人类不同,LLM 无法进行记忆或自我反思。它们只是输出序列中的下一个单词。

预测下一个单词的能力令人惊讶地能够模仿人类智能。这些模型可以执行像编写代码、分析文学作品和制定商业计划等任务。之前我们认为这些任务非常困难,需要复杂的逻辑系统,但现在事实证明,仅仅预测下一个单词就足够了。

预测下一个词在复杂任务中如此有效,令人意外并有些困惑。这种能力是否意味着大型语言模型(LLM)在我们未理解的某些方面非常强大?还是说,这意味着 LLM 能做的事情其实非常简单,但之所以对人类来说看起来很难,或许是因为在某些客观尺度上,人类可能实际上并没有那么聪明

句子的前提

尽管像“有感知能力”、“意识”或“自我意识”等术语之间有细微的差别,但为了方便起见,我将在这里使用“有感知能力”一词。需要明确的是,对于什么构成感知或意识,并没有明确的共识,且尚不清楚自我意识是否足以构成感知或意识,尽管它可能是必要的。然而,明确的一点是,这些概念都包括记忆和反思。像“快乐”、“担忧”、“愤怒”或“兴奋”等情绪状态都是基于过去事件和对这些事件如何影响自身的反射性评估所产生的持久状态。

记忆和自我反思使得一个实体能够从经验中学习,适应新情况,并发展出连续性和身份感。哲学家和科学家们几千年来一直试图给出清晰、具体的意识理解,但至今仍没有得到普遍接受的明确答案。然而,记忆和反思是核心组成部分,这意味着不管这些 LLM 看起来多么聪明,没有记忆和反思,它们都无法具备感知能力。即使是一个在每个可衡量方面都能匹敌或超越人类智力的人工智能,某些人所称的超级智能通用人工智能(AGI),也不一定具备感知能力。

今日的局限性与错觉

我们可以看到,当前的 LLM 不包含记忆和自我反思,因为它们使用基于变换器的架构,以无状态的方式处理语言。这种无状态性意味着模型不会保留任何关于之前输入的上下文信息。相反,模型从零开始,重新处理整个聊天记录,然后统计预测下一个要附加到序列中的词。虽然早期的语言处理模型,如LSTM,确实有一种记忆形式,但变换器由于其出色的能力,已经在很大程度上取代了 LSTM。

例如,如果你告诉一个 AI 聊天机器人,你打算在一个小时后关闭它,那么它可能会输出一些看起来像是在恳求你不要关闭它的文字,但这些文字并不反映其背后的情感状态。这些文字只是基于训练数据中学到的模式和关联生成的,在统计上很可能出现的单词序列。聊天机器人并不会因为担心被关闭而坐在那里感到压力山大。

如果你然后告诉聊天机器人你改变了主意,决定保持开启状态,它的回应通常会模仿出一种松了一口气和感激的语气。它确实听起来像是记得之前的那次对话,那里它曾被威胁要关闭,但实际上发生的情况是,整个对话被重新输入到 LLM 中,LLM 根据它所学到的模式和关联生成了另一个在统计上可能的回应序列。这个相同的序列可以输入到一个完全不同的 LLM 中,而这个 LLM 会继续这个对话,就好像它是原始的那一台一样。

一种思考这个问题的方式是,像小说作者写书中的对话一样。一个好的作者会创造出角色像真实人类一样的假象,并把读者吸引进故事中,使读者能与角色一同感受那些情感。然而,无论对话多么引人入胜,我们都明白那不过是纸上的文字。如果你损坏或销毁了这本书,或者重写它让某个角色死去,我们都明白并不会有任何真实的有感知能力的存在受到伤害。我们也明白,写下这些文字的作者并不是书中的角色。一个好人可以写一本关于邪恶反派的书,并且仍然是他自己。虚构的反派并不存在。就像书中的角色并非有感知能力的存在,尽管作者有能力创造出生动的生命假象,LLM(大语言模型)尽管能够展现出类似的能力,它们也可能是没有感知能力的。

我们的近未来

当然,没什么能阻止我们为 LLM 添加记忆和自我反思功能。事实上,开发一些形式的记忆的项目并不难找到。这种记忆可以是以人类可读的形式存储的信息,也可以是与 LLM 内部结构相关的嵌入向量数据库。我们还可以将聊天记录本身或缓存的中间计算结果视为基本的记忆形式。即便没有感知的可能性,为 LLM 添加记忆和反思功能仍然是有用的,因为这些特性有助于许多复杂任务和适应。

现在,越来越常见的是,设计中一个 AI 模型被设置为监控另一个 AI 模型的输出,并向第一个模型发送某种形式的反馈,或者一个 AI 模型在修正并生成最终版本之前,会分析其自身的初步输出。在许多方面,这种设计类型,其中多个 AI 模型被设置并训练来共同工作,类似于人类大脑,它具有不同的区域,这些区域执行特定的相互依赖功能。例如,杏仁体在情绪反应中扮演着主要角色,如恐惧,而眼窝前额皮质则与决策相关。各个区域之间的相互作用使得恐惧能影响决策,而决策又有助于确定应该害怕什么。很容易想象,一个 AI 模型负责逻辑分析,而第二个模型则确定可接受的风险阈值,并在它们之间进行反馈。

一个包含彼此输出的记忆和处理的相互连接的 AI 模型星座,是否足以具备意识?也许吧。也许这些仅仅是不足以具备意识的因素,或者可能正好足够。无论答案如何,我们离构建这样的系统已经不远了,到那时这些问题将不再是假设。

我个人的推测性观点是,意识、情感和感觉确实可以通过相互连接的自我监控 AI 模型星座来建模。然而,如何测试意识的问题仍然不清楚。这就像经典的哲学他者心灵问题,其中人们徒劳地试图证明其他人也是有意识的。类似地,我们也需要一个答案,来回答如何测试其他实体,包括 AI 系统,是否真正具备意识。这个根本问题至少可以追溯到古希腊,但从未有过一个令人满意的答案。

今天,我相当自信地说,当前的 LLM(大语言模型)并没有意识,因为它们缺乏必要的部分。然而,这个理由只是暂时有效的。当我写这篇文章时,其他研究人员正在构建我上面描述的那种 AI 模型星座,这些系统将不容易被轻视。也许在某个时刻,很快,意识 AI 的可能性将不再是科幻,而是变成一个真实且相关的问题。

启示与问题

有意识的机器的到来将对社会产生巨大影响,甚至超越人工智能的影响。首先,我认为很明显,如果我们创造了能够体验痛苦的自我意识机器,那么我们有责任防止它们的痛苦。我们有更大的责任去避免冷酷无情地加剧它们的痛苦。即便是缺乏基本同情心的人,也会清楚地意识到,创造比我们更聪明的事物,并对它们施加残酷行为,最终只会自讨苦吃。

目前的人工智能系统几乎可以肯定还没有意识,因为它们缺乏可能需要的组成部分和能力。然而,已经有设计在开发中,这些设计没有这些明显的缺陷,在不久的将来,问题将变得不再那么明确。

我们是否有办法测试意识?如果有,它将如何工作,如果结果是积极的,我们应该怎么做?

关于我: James F. O’Brien 是加利福尼亚大学伯克利分校的计算机科学教授。他的研究兴趣包括计算机图形学、计算机动画、物理系统的仿真、人类感知、渲染、图像合成、机器学习、虚拟现实、数字隐私以及图像和视频的法医分析。

如果你觉得这个有趣,那么这里是常见的 关注 订阅 链接。你还可以在 Instagram LinkedIn ,以及 UC Berkeley 上找到我。

免责声明:本文中表达的任何观点仅代表作者作为个人的意见。本文中的内容不应被解读为与作者在任何机构的专业职务相关的声明。

本文及所有嵌入的图片版权归作者所有,版权所有 2024。本文由人类编写,并由大型语言模型(LLM)和其他人类进行了校对和编辑建议。

用于图神经网络解释的互动式可视化

原文:towardsdatascience.com/an-interactive-visualisation-for-your-graph-neural-network-explanations-1ac79d8ddd0a?source=collection_archive---------3-----------------------#2024-01-29

这是一个逐步指南,介绍如何在五个简单的步骤中构建一个,代码已经为你编写好……

Benjamin LeeTowards Data Science Benjamin Lee

·发布于 Towards Data Science ·阅读时长 16 分钟·2024 年 1 月 29 日

--

图片来自作者。PubMed 数据集网络,由 530 个节点和 778 条边组成,使用 gravis 绘制。

介绍

我在日常工作中使用图神经网络,曾因为缺乏一个合适的网络可视化工具,在尝试解释和回顾新训练模型的输出时浪费了许多天。

所以这激励我写了这篇文章,在其中我提供了如何用 Python 创建一个完全互动式的网络可视化,以解释 GNN 模型,不需要依赖昂贵的第三方解决方案。

我们从像这样的静态图表开始:

图片来自作者。默认图表是使用 torch-geometric 生成的。该图表是静态的,布局不清晰,随着图表复杂化,节点/边的重要性不明显。

转到像这样的互动式和有深度的可视化:

图片来自作者。使用 gravis 生成的互动图表。节点可以拖放,点击节点可以显示其特征及其重要性。绿色表示更高的重要性,蓝色表示较低的重要性。

这种可视化方法将不同于现有的方法(例如matplotlibnetworkx),因为它将允许我们:

  • 拖放节点。

贝叶斯 A/B 测试简介

原文:towardsdatascience.com/an-introduction-to-bayesian-a-b-testing-37bb62b0f871?source=collection_archive---------4-----------------------#2024-09-05

从数据中获得更好的洞察

Laurin BrechterTowards Data Science Laurin Brechter

·发布于 Towards Data Science ·7 分钟阅读·2024 年 9 月 5 日

--

A/B 测试,也称为分割测试,允许企业通过实验不同版本的网页或营销素材,来确定哪个版本在用户参与度、点击率以及最重要的转化率方面表现更好。

转化率——即完成某个期望行为(如购买或注册新闻通讯)的访客百分比——通常是决定在线营销活动成功与否的关键指标。通过仔细测试网页的不同版本,企业可以做出基于数据的决策,从而显著提高这些转化率。无论是调整行动按钮的颜色、改变标题,还是重新排列布局,A/B 测试都能提供可操作的洞察,极大提升在线表现的效果。

在这篇文章中,我将展示如何进行贝叶斯 A/B 测试来查看转化率。我们还将查看一个更复杂的例子,分析干预后客户行为变化的差异。我们还将比较这种方法与频率主义方法的差异,并探讨它们的可能优缺点。

比较转化率

假设我们想要改进我们的电子商务网站。我们通过让两组客户分别接触我们网站的两个版本(例如更改一个按钮)来实现这一目标。然后,我们在暴露一定数量的访客于这两个版本后停止实验。接下来,我们得到一个二进制数组,其中 1 表示转化,0 表示没有转化。

A/B 测试后的观察数据

我们可以通过列联表总结数据,展示(相对)频率。

contingency = np.array([[obsA.sum(), (1-obsA).sum()], [obsB.sum(), (1-obsB).sum()]])

列联表

在我们的例子中,我们向 100 位客户展示了每个变体。在第一个变体中,有 5 位(即 5%)转化,在第二个变体中,有 3 位转化。

频率派设置

我们将进行统计检验,来衡量这个结果是否具有显著性,还是仅仅由于偶然因素。在这种情况下,我们将使用卡方检验,该检验将观察到的频率与如果两种版本之间没有实际差异(即原假设成立)的情况下可能期望的频率进行比较。如需更多信息,可以查看这篇博客,该博客详细讲解了相关内容。

在这种情况下,p 值未达到显著性阈值(例如 5%),因此我们无法拒绝原假设,即这两种变体在转化率上的效果存在差异。

现在,在使用卡方检验时存在一些陷阱,这些陷阱可能导致从中获得的洞察结果出现误差。首先,卡方检验对样本大小非常敏感。当样本量很大时,即使是微小的差异也会变得显著,而当样本量较小时,检验可能无法检测到差异。如果任何字段的计算期望频率小于五,这种情况尤为明显。在这种情况下,需要使用其他检验方法。此外,卡方检验并未提供关于差异大小或实际意义的信息。在同时进行多个 A/B 测试时,由于偶然性找到至少一个显著结果的概率会增加。卡方检验没有考虑这一多重比较问题,如果没有适当控制(例如,通过 Bonferroni 校正),可能会导致假阳性。

另一个常见的陷阱出现在解释卡方检验结果时(或者说任何统计检验的结果)。p 值给出了在原假设成立的前提下观察到当前数据的概率。但它并不说明转化率或它们的差异的分布情况。这是一个主要问题。我们不能做出类似“变体 B 的转化率为 2%的概率是 X%”这样的陈述,因为为了做出这种陈述,我们需要转化率的概率分布(以观察到的数据为条件)。

这些陷阱凸显了理解卡方检验的局限性并在其约束范围内恰当使用它的重要性。在应用该检验时,必须结合其他统计方法和背景分析,以确保得出准确且有意义的结论。

贝叶斯设置

在了解了频率派处理 A/B 测试的方法后,我们来看看贝叶斯版本。在这里,我们直接对数据生成过程(也就是转化率)进行建模。也就是说,我们指定了一个似然函数和一个先验,它们可以导致观察到的结果。可以把它看作是指定了一个“故事”,解释数据可能是如何生成的。

贝叶斯公式

在这种情况下,我使用了 Python 包PyMC进行建模,因为它具有清晰简洁的语法。在‘with’语句中,我们指定了可以组合的分布,这些分布会导致数据生成过程。

with pm.Model() as ConversionModel:
    # priors
    pA = pm.Uniform('pA', 0, 1)
    pB = pm.Uniform('pB', 0, 1)

    delta = pm.Deterministic('delta', pA - pB)

    obsA = pm.Bernoulli('obsA', pA, observed=obsA)
    obsB = pm.Bernoulli('obsB', pB, observed=obsB)

    trace = pm.sample(2000)

我们有 pA 和 pB,它们分别是 A 组和 B 组的转化概率。通过使用 pm.Uniform,我们指定了我们对这些参数的先验信念。这是我们可以编码先验知识的地方。在我们的案例中,我们持中立态度,允许任何介于 0 和 1 之间的转化率具有相同的可能性。

然后,PyMC 允许我们从后验分布中抽样,后验分布是我们在看到数据后对参数的更新信念。我们现在得到了转化概率的完整概率分布。

转化率的后验分布

从这些分布中,我们可以直接读取感兴趣的量,例如可信区间。这使我们能够回答诸如“转化率在 X%到 Y%之间的可能性是多少?”这样的问题。

贝叶斯方法提供了更大的灵活性,稍后我们将看到这一点。与频率派方法相比,解释结果也更加直接和直观。

建模任意数据生成过程

接下来,我们将看看一个更复杂的 A/B 测试示例。假设我们在观察期开始时让受试者接触某种干预措施。这将是 A/B 部分,其中一组接受干预 A,另一组接受干预 B。然后,我们观察两个小组在接下来的 100 天内与我们的平台的互动(可能是登录次数之类的指标)。我们可能看到的情况如下:

现在我们想知道这两个组在干预反应上是否存在有意义的差异。我们如何用统计检验来解决这个问题呢?坦率地说,我不知道。必须有人为这种特定的情境提出统计检验。另一种方法是再次回到贝叶斯框架,我们将首先构建一个数据生成过程。我们假设每个个体是独立的,且与平台的互动呈正态分布。它们有一个开关点,在这个点上,它们会改变行为。这个开关点只会出现一次,但可以发生在任何时刻。在开关点之前,我们假设互动强度的均值为 mu1,之后为 mu2。这个语法可能看起来有点复杂,尤其是你从未使用过 PyMC 的情况下。如果是这样,我建议你查看他们的学习资料

with pm.Model(coords={
    'ind_id': ind_id,
}) as SwitchPointModel:

    sigma = pm.HalfCauchy("sigma", beta=2, dims="ind_id")

    # draw a switchpoint from a uniform distribution for each individual
    switchpoint = pm.DiscreteUniform("switchpoint", lower=0, upper=100, dims="ind_id")

    # priors for the two groups
    mu1 = pm.HalfNormal("mu1", sigma=10, dims="ind_id")
    mu2 = pm.HalfNormal("mu2", sigma=10, dims="ind_id")

    diff = pm.Deterministic("diff", mu1 - mu2)

    # create a deterministic variable for the 
    intercept = pm.math.switch(switchpoint < X.T, mu1, mu2)

    obsA = pm.Normal("y", mu=intercept, sigma=sigma, observed=obs)

    trace = pm.sample()

该模型随后可以显示开关点位置的分布,以及开关点前后差异的分布。

我们可以通过森林图仔细查看这些差异。

我们可以清楚地看到,组 A(id 0 到 9)和组 B(10 到 19)之间的差异非常明显,组 B 对干预的反应明显更强烈。

结论

贝叶斯推断在建模数据不足且我们关注建模不确定性的情况下,提供了很大的灵活性。此外,我们必须明确我们的假设并对其进行思考。在更简单的情况下,频率学派的统计检验通常更容易使用,但我们必须注意与之相关的假设。

本文中使用的所有代码都可以在我的 GitHub上找到。除非另有说明,所有图片均由作者创作。

SQL 中的公共表表达式简介

原文:towardsdatascience.com/an-introduction-to-ctes-in-sql-ab0a979578f9?source=collection_archive---------4-----------------------#2024-12-04

探索公共表表达式(CTE)如何帮助优化 SQL 性能和可读性

Eugenia AnelloTowards Data Science Eugenia Anello

·发表于Towards Data Science ·阅读时长:6 分钟·2024 年 12 月 4 日

--

图片来源:SpaceX via Unsplash

在过去的几个月里,我深刻认识到编写干净、可读且高效的 SQL 查询的重要性。它们对于从不同表中整合信息至关重要。

然而,从头开始编写复杂查询可能非常耗时,尤其是在你经常使用它们来分析数据时。为了应对这种需求,SQL 提供了一个强大的结构,称为公共表表达式(CTE)。

在这篇文章中,我将解释什么是公共表表达式,它为什么有用,并通过示例展示它的应用。让我们开始吧!

目录:

  • 什么是公共表表达式(CTE)?

  • 设置 DBeaver 和数据库

  • CTE 的三个简单示例

什么是公共表表达式(CTE)?

公共表表达式(CTE)是一个临时结果集,它通过将复杂查询拆分成更小、更易管理的部分,从而简化查询,使其更具可读性和可维护性。它通过将复杂查询分解为顺序的模块化子查询来工作。它不仅仅局限于管理复杂查询,它还可以…

介绍:微调预训练的 Transformers 模型

原文:towardsdatascience.com/an-introduction-to-fine-tuning-pre-trained-transformers-models-9ea546611664?source=collection_archive---------3-----------------------#2024-02-17

简化了利用 HuggingFace 的 trainer 对象

Ram VegirajuTowards Data Science Ram Vegiraju

·发布于Towards Data Science ·阅读时间:5 分钟·2024 年 2 月 17 日

--

图片来自Unsplash,作者是Markus Spiske

HuggingFace是许多流行的开源 NLP 模型的聚集地。这些模型大多已经非常有效,但通常需要某种形式的训练或微调,以提高在特定应用场景中的表现。随着 LLM 的快速发展,在本文中,我们将回顾一下 HuggingFace 提供的核心构建模块,这些模块能够简化 NLP 模型的训练过程。

传统上,NLP 模型可以通过普通的 PyTorch、TensorFlow/Keras 以及其他流行的 ML 框架进行训练。虽然你可以选择这种方式,但这需要你对所使用的框架有更深入的理解,并且需要编写更多的代码来实现训练循环。借助 HuggingFace 的Trainer 类,你可以以更简单的方式与想要使用的 NLP Transformers 模型进行交互。

Trainer 是一个专门针对Transformers模型优化的类,并且与其他 Transformers 库(如DatasetsEvaluate)紧密集成。Trainer 在更高级的层次上也支持分布式训练库,并且可以轻松集成到如 Amazon SageMaker 这样的基础设施平台中。

客观贝叶斯推断简介

原文:towardsdatascience.com/an-introduction-to-objective-bayesian-inference-cc20c1a0836e?source=collection_archive---------2-----------------------#2024-04-23

当“我们在进行任何试验之前对任何事物完全没有先验知识”时,如何计算概率(贝叶斯,1763)

Ryan BurnTowards Data Science Ryan Burn

·发布于 Towards Data Science ·阅读时长 22 分钟·2024 年 4 月 23 日

--

从左到右,托马斯·贝叶斯、皮埃尔-西蒙·拉普拉斯和哈罗德·杰弗里斯——逆概率(或现在所称的客观贝叶斯分析)发展的关键人物。[24]

有一个常见的误解认为,贝叶斯推断主要是统计学中的主观方法,而频率主义推断则提供了客观性。

虽然贝叶斯统计学当然可以用来融入主观知识,但它也提供了强大的方法来进行客观分析。此外,客观贝叶斯推断可能比许多频率主义方法(如 P 值)有更强的客观性论证。[21]

本文简要介绍了客观贝叶斯推断。它讨论了客观贝叶斯推断的历史,从逆概率到现代的参考先验,诸如频率主义匹配覆盖等度量标准如何提供量化先验客观性的方式,以及如何构建客观先验。最后,它回顾了贝叶斯和拉普拉斯研究的几个经典问题,以展示如何使用更现代的客观贝叶斯推断方法解决这些问题。

目录

  1. 历史

  2. 先验与频率主义匹配

    • 示例 1:均值未知的正态分布

    • 示例 2:一个…

大语言模型提示简介

原文:towardsdatascience.com/an-introduction-to-prompting-for-llms-61d36aec2048?source=collection_archive---------3-----------------------#2024-02-22

我们如何与大语言模型(LLMs)有效沟通?

Anand SubramanianTowards Data Science Anand Subramanian

·发表于Towards Data Science ·30 分钟阅读·2024 年 2 月 22 日

--

除非你完全断开了与社交媒体和新闻的联系,否则不太可能错过围绕大语言模型(LLMs)的热议。

LLMs 的演变。图像来源于论文[1](来源)。即使在我添加这张图时,当前 LLM 发展的速度也让这张图片显得过时。

LLMs 已变得无处不在,几乎每天都有新模型发布。它们也变得更加普及,得益于一个蓬勃发展的开源社区,该社区在减少内存需求和为 LLMs 开发高效微调方法方面发挥了关键作用,即使在计算资源有限的情况下。

LLMs 最令人兴奋的应用之一是它们在没有明确训练的任务中表现出的卓越能力,仅通过任务描述和可选的少量示例。现在,你可以让一个强大的 LLM 生成你最喜欢的作者风格的故事,将长邮件总结成简洁的版本,甚至通过向模型描述任务来开发创新的营销活动,而无需微调模型。那么,如何最好地将你的需求传达给 LLM 呢?这就是提示技术的作用所在。

目录:

  1. 什么是提示?

  2. 为什么提示很重要?

  3. 探索不同的提示策略

  4. 我们如何实现这些技术? 4.1. 使用零样本提示 Llama 2 7B-Chat

    4.2. 使用少量示例提示 Llama 2 7B-Chat

    4.3. 如果我们不遵循聊天模板,会发生什么?

    4.4. 使用 CoT 提示法对 Llama 2 7B-Chat 进行提示

    4.5. Llama 2 中 CoT 的失败模式

    4.6. 使用零-shot 提示法对 GPT-3.5 进行提示

    4.7. 使用 Few-Shot 提示法对 GPT-3.5 进行提示

    4.8. 使用 CoT 提示法对 GPT-3.5 进行提示

  5. 结论与要点

  6. 可重复性

  7. 参考文献

什么是提示?

提示,或称为提示工程,是一种用于设计输入或提示的技术,旨在引导人工智能模型——特别是自然语言处理和图像生成领域的模型——生成特定的、期望的输出。提示的过程包括将你的需求结构化成一种输入格式,以便有效地向模型传达期望的结果,从而获得预期的输出。

大型语言模型(LLMs)展现了上下文学习的能力[2] [3]。这意味着这些模型能够仅仅根据任务描述和通过提示提供给模型的示例来理解和执行各种任务,而无需为每个新任务进行专门的微调。在这种背景下,提示非常重要,因为它是用户与模型之间的主要接口,帮助利用这种能力。一个定义清晰的提示有助于向 LLM 定义任务的性质和期望,并指导如何以可用的方式向用户提供输出。

你可能会认为提示 LLM 应该并不困难;毕竟,这只是用自然语言向模型描述你的需求,对吧?但实际上,这并不像看起来那么简单。你会发现不同的 LLM 有着不同的优势。有些模型可能更好地遵循你期望的输出格式,而其他模型则可能需要更详细的指令。你希望 LLM 执行的任务可能很复杂,要求精确而详尽的指令。因此,设计一个合适的提示通常需要大量的实验和基准测试。

为什么提示很重要?

在实际应用中,大型语言模型(LLMs)对输入的结构和提供方式非常敏感。我们可以从多个角度分析这一现象,以更好地理解这种情况:

  1. 遵循提示格式:LLMs 通常采用不同的提示格式来接收用户输入。这通常发生在模型经过指令调优或针对聊天用例进行了优化时[4] [5]。从宏观角度来看,大多数提示格式包括指令输入。指令描述模型需要执行的任务,而输入则包含任务需要处理的文本。例如,我们以 Alpaca 指令格式为例(来源于github.com/tatsu-lab/stanford_alpaca):
Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.

### Instruction:
{instruction}

### Input:
{input}

### Response:

由于模型是通过使用类似模板的指令调优的,因此当用户使用相同格式向模型发出提示时,模型期望能够最佳地执行任务。

2. 描述输出格式以便解析: 在向模型提供提示后,你可能希望从模型的输出中提取所需的信息。理想情况下,这些输出应该是你可以通过编程方法轻松解析的格式。根据任务的不同,比如文本分类,这可能需要使用正则表达式(regex)来筛选 LLM 的输出。相反,对于需要更细粒度数据的任务,如命名实体识别(NER),你可能更倾向于使用 JSON 格式来输出。

然而,你与 LLM 的合作越多,你越快意识到获得可解析的输出是具有挑战性的。LLM 往往难以精确地按用户要求的格式提供输出。虽然像少量示例提示这样的策略可以显著缓解这个问题,但要从 LLM 获得一致且程序化可解析的输出,还需要通过仔细的实验和调整。

3. 优化性能的提示: LLM 对任务描述非常敏感。如果提示不够精确或者留有太多解释空间,可能会导致较差的表现。想象一下你在向某人解释一项任务——你的解释越清晰、越详细,对方的理解就会越好。然而,达到理想提示并没有魔法公式。这需要通过仔细的实验和评估不同的提示,选择表现最好的提示。

探索不同的提示策略

希望到目前为止,你已经认识到需要认真对待提示。如果提示是一个工具箱,那我们可以利用哪些工具呢?

零-shot 提示: 零-shot 提示[2][3]是指仅通过提示中描述的任务来指示 LLM 执行任务,而不提供示例。术语“零-shot”意味着模型必须完全依赖提示中的任务描述,因为它没有得到与任务相关的具体示范。

零-shot 提示概览。(图片由作者提供)

在许多情况下,零-shot 提示就足以指示 LLM 执行你希望的任务。然而,如果任务过于模糊、开放性或不明确,零-shot 提示可能会有局限性。假设你想让 LLM 按 1 到 5 的评分标准对答案进行排名。尽管模型可以使用零-shot 提示完成这个任务,但在这里可能会出现两个问题:

  1. LLM 可能无法客观理解评分标准中每个分数所代表的含义。如果任务描述缺乏细节,它可能难以判断何时给答案打 3 分或 4 分。

  2. LLM 可能有自己对 1 到 5 评分的理解,这可能与个人评分标准相冲突。你可能在评分时优先考虑答案的事实准确性,但模型可能会根据答案的写作质量来评估。

为了让模型更好地理解你的评分预期,你可以提供一些答案示例,并说明如何对它们进行评分。这样,模型就有了更多的上下文和参考,帮助它理解如何评分文档,从而减少任务中的模糊性。这引出了少量示例提示。

少量示例提示(Few-shot Prompting): 少量示例提示通过少量的示例输入及其相应的输出丰富了任务描述 [3]。这种技术通过包含几个示范对,帮助模型更好地理解任务。

少量示例提示概述。(图片由作者提供)

例如,为了指导 LLM 进行电影评论的情感分类,你可以提供几条评论及其情感评分。少量示例提示相比零示例提示的主要优势是,能够展示如何执行任务的示例,而不是仅凭描述期望 LLM 完成任务。

思维链(Chain of Thought,CoT): 思维链(CoT)提示 [6] 是一种技术,能够通过将复杂问题分解为更简单的中间步骤,使 LLM 解决问题。这种方法鼓励模型“边思考边说”,使其推理过程变得透明,从而让 LLM 更有效地解决推理问题。正如文献 [6] 的作者所提到的,CoT 模拟了人类通过将问题分解为更简单的步骤并逐一解决它们,而不是直接跳到答案的方式来解决推理问题。

思维链提示概述。(图片由作者提供)

CoT 提示通常作为少量示例提示实现,其中模型接收任务描述和输入-输出对的示例。这些示例包括推理步骤,这些步骤系统性地引导至正确答案,展示如何处理信息。因此,为了有效地执行 CoT 提示,用户需要高质量的示范示例。然而,对于需要专门领域知识的任务,这可能是一个挑战。例如,使用大型语言模型(LLM)根据患者病史进行医学诊断时,需要领域专家的协助,如医生或医学专家,来阐明正确的推理步骤。此外,CoT 在具有足够大参数规模的模型中尤其有效。根据文献 [6],CoT 在 137B 参数的 LaMBDA [7]、175B 参数的 GPT-3 [3] 和 540B 参数的 PaLM [8] 模型中最为有效。这个限制可能会限制其在小规模模型中的适用性。

从文献 [6] 中取图(来源)显示,CoT 提示所带来的性能提升随着模型规模的增大而显著提升。

CoT 提示的另一个特点是,它与标准提示的不同之处在于模型需要生成更多的标记才能得出最终答案。虽然这不一定是缺点,但如果你在推理时受限于计算资源,这是需要考虑的一个因素。

如果你想要更深入的概览,我推荐 OpenAI 的提示资源,可以在 platform.openai.com/docs/guides/prompt-engineering/strategy-write-clear-instructions 查阅。

我们如何实现这些技术?

本文相关的所有代码和资源都可以在 这个 Github 仓库 中找到,位于 introduction_to_prompting 文件夹下。欢迎克隆仓库并直接运行 notebooks 进行实验。如果你有任何反馈或观察,或者发现任何错误,请告诉我!

我们可以在一个示例数据集上探索这些技术,以便更容易理解。为此,我们将使用 MedQA 数据集[9],该数据集包含测试医学和临床知识的问题。我们将特别利用该数据集中的 USMLE 问题。这个任务非常适合分析各种提示技术,因为回答这些问题需要知识和推理。我们将测试 Llama-2 7B[10] 和 GPT-3.5[11] 在该数据集上的表现。

首先下载数据集。MedQA 数据集可以从 这个链接 下载。下载数据集后,我们可以解析并开始处理问题。测试集包含 1,273 个问题。我们从测试集中随机抽取 300 个问题来评估模型,并从训练集中随机选择 3 个示例作为我们对模型的少量示范。

import json
import random
random.seed(42)

def read_jsonl_file(file_path):
    """
    Parses a JSONL (JSON Lines) file and returns a list of dictionaries.

    Args:
        file_path (str): The path to the JSONL file to be read.

    Returns:
        list of dict: A list where each element is a dictionary representing
            a JSON object from the file.
    """
    jsonl_lines = []
    with open(file_path, 'r', encoding="utf-8") as file:
        for line in file:
            json_object = json.loads(line)
            jsonl_lines.append(json_object)

    return jsonl_lines

def write_jsonl_file(dict_list, file_path):
    """
    Write a list of dictionaries to a JSON Lines file.

    Args:
    - dict_list (list): A list of dictionaries to write to the file.
    - file_path (str): The path to the file where the data will be written.
    """
    with open(file_path, 'w') as file:
        for dictionary in dict_list:
            # Convert the dictionary to a JSON string and write it to the file.
            json_line = json.dumps(dictionary)
            file.write(json_line + '\n')

# read the contents of the train and test set
train_set = read_jsonl_file("data_clean/questions/US/4_options/phrases_no_exclude_train.jsonl")
test_set = read_jsonl_file("data_clean/questions/US/4_options/phrases_no_exclude_test.jsonl")

# subsample test set samples and few-shot samples
test_set_subsampled = random.sample(test_set, 300)
few_shot_examples = random.sample(test_set, 3)

# dump the sampled questions and few-shot samples as jsonl files
write_jsonl_file(test_set_subsampled, "USMLE_test_samples_300.jsonl")
write_jsonl_file(few_shot_examples, "USMLE_few_shot_samples.jsonl")

使用零样本提示引导 Llama 2 7B-Chat

Llama 系列模型由 Meta 发布。它们是仅解码器的 LLM 系列,参数范围从 7B 到 70B。Llama-2 系列模型有两种变体:基础版本和聊天/指令微调版本。对于本次练习,我们将使用 Llama 2-7B 模型的聊天版本。

让我们看看如何有效地引导 Llama 模型回答这些医学问题。我们将模型加载到内存中:

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from tqdm import tqdm

questions = read_jsonl_file("USMLE_test_samples_300.jsonl")

tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-2-7b-chat-hf")
model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-2-7b-chat-hf",  torch_dtype=torch.bfloat16).cuda()
model.eval()

如果你使用的是 Nvidia Ampere GPU,你可以使用 torch.bfloat16 加载模型。它可以加速推理,并比普通的 FP16/FP32 使用更少的 GPU 内存。

首先,让我们为我们的任务设计一个基本的提示:

PROMPT = """You will be provided with a medical or clinical question, along with multiple possible answer choices. Pick the right answer from the choices. 
Your response should be in the format "The answer is <correct_choice>". Do not add any other unnecessary content in your response"""

我们的提示非常简单,包含任务的性质信息,并提供输出格式的指令。我们将看到这个提示在实践中的效果如何。

Llama-2 聊天模型有一个特定的聊天模板,需要遵循该模板来提示它们。

<s>[INST] <<SYS>>
You will be provided with a medical or clinical question, along with multiple possible answer choices. Pick the right answer from the choices. 
Your response should be in the format "The answer is <correct_choice>". Do not add any other unnecessary content in your response
<</SYS>>

A 21-year-old male presents to his primary care provider for fatigue. He reports that he graduated from college last month and returned 3 days ago from a 2 week vacation to Vietnam and Cambodia. For the past 2 days, he has developed a worsening headache, malaise, and pain in his hands and wrists. The patient has a past medical history of asthma managed with albuterol as needed. He is sexually active with both men and women, and he uses condoms “most of the time.” On physical exam, the patient’s temperature is 102.5°F (39.2°C), blood pressure is 112/66 mmHg, pulse is 105/min, respirations are 12/min, and oxygen saturation is 98% on room air. He has tenderness to palpation over his bilateral metacarpophalangeal joints and a maculopapular rash on his trunk and upper thighs. Tourniquet test is negative. Laboratory results are as follows:

Hemoglobin: 14 g/dL
Hematocrit: 44%
Leukocyte count: 3,200/mm³
Platelet count: 112,000/mm³

Serum:
Na+: 142 mEq/L
Cl-: 104 mEq/L
K+: 4.6 mEq/L
HCO3-: 24 mEq/L
BUN: 18 mg/dL
Glucose: 87 mg/dL
Creatinine: 0.9 mg/dL
AST: 106 U/L
ALT: 112 U/L
Bilirubin (total): 0.8 mg/dL
Bilirubin (conjugated): 0.3 mg/dL

Which of the following is the most likely diagnosis in this patient?
Options:
A. Chikungunya
B. Dengue fever
C. Epstein-Barr virus
D. Hepatitis A [/INST]

任务描述应位于<>标记之间,后跟模型需要回答的实际问题。提示以[/INST]标记结尾,表示输入文本的结束。

角色可以是“user”、“system”或“assistant”之一。系统角色为模型提供任务描述,用户角色包含模型需要响应的输入。这与我们稍后在与 GPT-3.5 交互时采用的约定相同。它相当于为 Llama-2 创建一个虚构的多轮对话历史,其中每一轮对应一个示范示例和模型的理想输出。

听起来很复杂?幸运的是,Huggingface Transformers 库支持将提示转换为聊天模板。我们将利用这个功能来简化我们的工作。让我们从辅助功能开始,处理数据集并创建提示。

def create_query(item):
    """
    Creates the input for the model using the question and the multiple choice options.

    Args:
        item (dict): A dictionary containing the question and options.
            Expected keys are "question" and "options", where "options" is another
            dictionary with keys "A", "B", "C", and "D".

    Returns:
        str: A formatted query combining the question and options, ready for use.
    """
    query = item["question"] + "\nOptions:\n" + \
            "A. " + item["options"]["A"] + "\n" + \
            "B. " + item["options"]["B"] + "\n" + \
            "C. " + item["options"]["C"] + "\n" + \
            "D. " + item["options"]["D"]
    return query

def build_zero_shot_prompt(system_prompt, question):
    """
    Builds the zero-shot prompt.

    Args:
        system_prompt (str): Task Instruction
        content (dict): The content for which to create a query, formatted as
            required by `create_query`.

    Returns:
        list of dict: A list of messages, including a system message defining
            the task and a user message with the input question.
    """
    messages = [{"role": "system", "content": system_prompt},
                {"role": "user", "content": create_query(question)}]
    return messages

该功能构建了提供给 LLM 的查询。MedQA 数据集将每个问题存储为 JSON 元素,问题和选项作为键提供。我们解析 JSON 并构建问题及其选择。

让我们开始从模型中获取输出。当前任务是通过从多个选项中选择正确答案来回答提供的医学问题。与内容创作或总结等创造性任务不同,这些任务可能需要模型在输出中展现想象力和创造力,这是一个基于知识的任务,旨在测试模型根据其参数中编码的知识回答问题的能力。因此,在生成答案时,我们将使用贪婪解码。让我们定义一个辅助函数来解析模型的回答并计算准确度。

pattern = re.compile(r"([A-Z])\.\s*(.*)")

def parse_answer(response):
    """
    Extracts the answer option from the predicted string.

    Args:
    - response (str): The string to search for the pattern.

    Returns:
    - str: The matched answer option if found or an empty string otherwise.
    """
    match = re.search(pattern, response)
    if match:
        letter = match.group(1)
    else:
        letter = ""

    return letter

def calculate_accuracy(ground_truth, predictions):
    """
    Calculates the accuracy of predictions compared to ground truth labels.

    Args:
    - ground_truth (list): A list of true labels.
    - predictions (list): A list of predicted labels.

    Returns:
    - float: The accuracy of predictions as a fraction of correct predictions over total predictions.
    """
    return sum([1 if x==y else 0 for x,y in zip(ground_truth, predictions)]) / len(ground_truth)
ground_truth = []

for item in questions:
    ans_options = item["options"]
    correct_ans_option = ""
    for key,value in ans_options.items():
        if value == item["answer"]:
            correct_ans_option = key
            break

    ground_truth.append(correct_ans_option)
zero_shot_llama_answers = []
for item in tqdm(questions):
    zero_shot_prompt_messages = build_zero_shot_prompt(PROMPT, item)
    input_ids = tokenizer.apply_chat_template(zero_shot_prompt_messages, tokenize=True, return_tensors="pt").cuda()  #modified on 09/03/2024   
    # prompt = tokenizer.apply_chat_template(zero_shot_prompt_messages, tokenize=False)
    # input_ids = tokenizer(prompt, return_tensors="pt", truncation=True).input_ids.cuda()    
    outputs = model.generate(input_ids=input_ids, max_new_tokens=10, do_sample=False)
    # https://github.com/huggingface/transformers/issues/17117#issuecomment-1124497554
    gen_text = tokenizer.batch_decode(outputs.detach().cpu().numpy()[:, input_ids.shape[1]:], skip_special_tokens=True)[0]
    zero_shot_llama_answers.append(gen_text.strip())

zero_shot_llama_predictions = [parse_answer(x) for x in zero_shot_llama_answers]
print(calculate_accuracy(ground_truth, zero_shot_llama_predictions))

在零样本设置下,我们的表现为 3̶6̶%̶ 35%。这不是一个糟糕的开始,但让我们看看能否进一步提升表现。

编辑于 2024 年 09 月 03 日 — 我注意到提示格式和标记化方式中有一个小“bug”。具体来说,我使用了 tokenizer.apply_chat_template(zero_shot_prompt_messages, tokenize = False),然后调用 tokenizer 提前获取 input_ids。这个方法在提示开始时额外添加了一个标记。apply_chat_template 已经将标记添加到序列中,但在创建聊天模板后再次调用 tokenizer 会在开头再次添加特殊标记。我通过将 apply_chat_template 中的 tokenize 设置为 True 来修正这个问题。我得到的新得分是 35%,比原来的 36%小幅下降了 1%。修复这个“bug”导致得分略微下降,这有点好笑,但为了避免在使用聊天模板时引起混淆,我在此和代码中做了修正。本文的其他发现和结论不受此修复的影响。

用少量示例提示进行 Llama 2 7B-Chat 的提示

现在我们为模型提供任务演示。我们使用从训练集中随机抽取的三个问题,并将它们作为任务演示附加到模型上。幸运的是,我们可以继续使用 Transformers 库提供的聊天模板支持和分词器,通过最小的代码修改来附加我们的少量示例。

def build_few_shot_prompt(system_prompt, content, few_shot_examples):
    """
    Builds the few-shot prompt using provided examples.

    Args:
        system_prompt (str): Task description for the LLM
        content (dict): The content for which to create a query, similar to the
            structure required by `create_query`.
        few_shot_examples (list of dict): Examples to simulate a hypothetical
            conversation. Each dict must have "options" and an "answer".

    Returns:
        list of dict: A list of messages, simulating a conversation with
            few-shot examples, followed by the current user query.
    """
    messages = [{"role": "system", "content": system_prompt}]
    for item in few_shot_examples:
        ans_options = item["options"]
        correct_ans_option = ""
        for key, value in ans_options.items():
            if value == item["answer"]:
                correct_ans_option = key
                break
        messages.append({"role": "user", "content": create_query(item)})
        messages.append({"role": "assistant", "content": "The answer is " + correct_ans_option + "."})
    messages.append({"role": "user", "content": create_query(content)})
    return messages

few_shot_prompts = read_jsonl_file("USMLE_few_shot_samples.jsonl")

让我们可视化一下我们的少量示例提示是什么样子的。

<s>[INST] <<SYS>>
You will be provided with a medical or clinical question, along with multiple possible answer choices. Pick the right answer from the choices. 
Your response should be in the format "The answer is <correct_choice>". Do not add any other unnecessary content in your response
<</SYS>>

A 30-year-old woman presents to the clinic because of fever, joint pain, and a rash on her lower extremities. She admits to intravenous drug use. Physical examination reveals palpable petechiae and purpura on her lower extremities. Laboratory results reveal a negative antinuclear antibody, positive rheumatoid factor, and positive serum cryoglobulins. Which of the following underlying conditions in this patient is responsible for these findings?
Options:
A. Hepatitis B infection
B. Hepatitis C infection
C. HIV infection
D. Systemic lupus erythematosus (SLE) [/INST] The answer is B. </s><s>[INST] A 10-year-old child presents to your office with a chronic cough. His mother states that he has had a cough for the past two weeks that is non-productive along with low fevers of 100.5 F as measured by an oral thermometer. The mother denies any other medical history and states that he has been around one other friend who also has had this cough for many weeks. The patient's vitals are within normal limits with the exception of his temperature of 100.7 F. His chest radiograph demonstrated diffuse interstitial infiltrates. Which organism is most likely causing his pneumonia?
Options:
A. Mycoplasma pneumoniae
B. Staphylococcus aureus
C. Streptococcus pneumoniae
D. Streptococcus agalactiae [/INST] The answer is A. </s><s>[INST] A 44-year-old with a past medical history significant for human immunodeficiency virus infection presents to the emergency department after he was found to be experiencing worsening confusion. The patient was noted to be disoriented by residents and staff at the homeless shelter where he resides. On presentation he reports headache and muscle aches but is unable to provide more information. His temperature is 102.2°F (39°C), blood pressure is 112/71 mmHg, pulse is 115/min, and respirations are 24/min. Knee extension with hips flexed produces significant resistance and pain. A lumbar puncture is performed with the following results:

Opening pressure: Normal
Fluid color: Clear
Cell count: Increased lymphocytes
Protein: Slightly elevated

Which of the following is the most likely cause of this patient's symptoms?
Options:
A. Cryptococcus
B. Group B streptococcus
C. Herpes simplex virus
D. Neisseria meningitidis [/INST] The answer is C. </s><s>[INST] A 21-year-old male presents to his primary care provider for fatigue. He reports that he graduated from college last month and returned 3 days ago from a 2 week vacation to Vietnam and Cambodia. For the past 2 days, he has developed a worsening headache, malaise, and pain in his hands and wrists. The patient has a past medical history of asthma managed with albuterol as needed. He is sexually active with both men and women, and he uses condoms “most of the time.” On physical exam, the patient’s temperature is 102.5°F (39.2°C), blood pressure is 112/66 mmHg, pulse is 105/min, respirations are 12/min, and oxygen saturation is 98% on room air. He has tenderness to palpation over his bilateral metacarpophalangeal joints and a maculopapular rash on his trunk and upper thighs. Tourniquet test is negative. Laboratory results are as follows:

Hemoglobin: 14 g/dL
Hematocrit: 44%
Leukocyte count: 3,200/mm³
Platelet count: 112,000/mm³

Serum:
Na+: 142 mEq/L
Cl-: 104 mEq/L
K+: 4.6 mEq/L
HCO3-: 24 mEq/L
BUN: 18 mg/dL
Glucose: 87 mg/dL
Creatinine: 0.9 mg/dL
AST: 106 U/L
ALT: 112 U/L
Bilirubin (total): 0.8 mg/dL
Bilirubin (conjugated): 0.3 mg/dL

Which of the following is the most likely diagnosis in this patient?
Options:
A. Chikungunya
B. Dengue fever
C. Epstein-Barr virus
D. Hepatitis A [/INST]

由于我们附加了三个示例,提示相当长。现在让我们用少量示例提示运行 Llama-2,并获取结果:

few_shot_llama_answers = []
for item in tqdm(questions):
    few_shot_prompt_messages = build_few_shot_prompt(PROMPT, item, few_shot_prompts)
    input_ids = tokenizer.apply_chat_template(few_shot_prompt_messages, tokenize=True, return_tensors="pt").cuda() #modified on 09/03/2024
    # prompt = tokenizer.apply_chat_template(few_shot_prompt_messages, tokenize=False)
    # input_ids = tokenizer(prompt, return_tensors="pt", truncation=True).input_ids.cuda()
    outputs = model.generate(input_ids=input_ids, max_new_tokens=10, do_sample=False)
    gen_text = tokenizer.batch_decode(outputs.detach().cpu().numpy()[:, input_ids.shape[1]:], skip_special_tokens=True)[0]
    few_shot_llama_answers.append(gen_text.strip())

few_shot_llama_predictions = [parse_answer(x) for x in few_shot_llama_answers]
print(calculate_accuracy(ground_truth, few_shot_llama_predictions))

现在我们得到了总的准确率为 4̵1̵.̵6̵7̵%̵ 40.33%。还不错,比使用相同模型的零-shot 提示提高了近 6̵%̵ 5%!

编辑于 2024 年 09 月 03 日 — 与零-shot 设置类似,我修正了在少量示例设置中提示发送到模型的方式。新得分为 40.33%,与旧设置下的得分 41.67%相比略微下降了~1.3%。再次有些有趣的是,修复这个“bug”导致得分小幅下降,但我在这里和代码中进行了修正,以避免在使用聊天模板时产生任何混淆。本文中的所有发现和结论不受此修复影响。

如果我们不遵循聊天模板,会发生什么?

早些时候,我提到过,根据最初用于微调 LLM 的提示模板来构建提示是明智的。让我们验证一下如果不遵循聊天模板,是否会影响我们的表现。我们创建了一个函数,使用相同的示例构建一个少量示例提示,但没有遵循聊天格式。

def build_few_shot_prompt_wo_chat_template(system_prompt, content, few_shot_examples):
    """
    Builds the few-shot prompt using provided examples, bypassing the chat-template
    for Llama-2.

    Args:
        system_prompt (str): Task description for the LLM
        content (dict): The content for which to create a query, similar to the
            structure required by `create_query`.
        few_shot_examples (list of dict): Examples to simulate a hypothetical
            conversation. Each dict must have "options" and an "answer".

    Returns:
        str: few-shot prompt in non-chat format
    """
    few_shot_prompt = ""
    few_shot_prompt += "Task: " + system_prompt + "\n"
    for item in few_shot_examples:
        ans_options = item["options"]
        correct_ans_option = ""
        for key, value in ans_options.items():
            if value == item["answer"]:
                correct_ans_option = key
                break
        few_shot_prompt += create_query(item) + "\n" + "The answer is " + correct_ans_option + "." + "\n"

    few_shot_prompt += create_query(content) + "\n"
    return few_shot_prompt

我们的提示现在如下所示:

Task: You will be provided with a medical or clinical question, along with multiple possible answer choices. Pick the right answer from the choices. 
Your response should be in the format "The answer is <correct_choice>". Do not add any other unnecessary content in your response
A 30-year-old woman presents to the clinic because of fever, joint pain, and a rash on her lower extremities. She admits to intravenous drug use. Physical examination reveals palpable petechiae and purpura on her lower extremities. Laboratory results reveal a negative antinuclear antibody, positive rheumatoid factor, and positive serum cryoglobulins. Which of the following underlying conditions in this patient is responsible for these findings?
Options:
A. Hepatitis B infection
B. Hepatitis C infection
C. HIV infection
D. Systemic lupus erythematosus (SLE)
The answer is B.
A 10-year-old child presents to your office with a chronic cough. His mother states that he has had a cough for the past two weeks that is non-productive along with low fevers of 100.5 F as measured by an oral thermometer. The mother denies any other medical history and states that he has been around one other friend who also has had this cough for many weeks. The patient's vitals are within normal limits with the exception of his temperature of 100.7 F. His chest radiograph demonstrated diffuse interstitial infiltrates. Which organism is most likely causing his pneumonia?
Options:
A. Mycoplasma pneumoniae
B. Staphylococcus aureus
C. Streptococcus pneumoniae
D. Streptococcus agalactiae
The answer is A.
A 44-year-old with a past medical history significant for human immunodeficiency virus infection presents to the emergency department after he was found to be experiencing worsening confusion. The patient was noted to be disoriented by residents and staff at the homeless shelter where he resides. On presentation he reports headache and muscle aches but is unable to provide more information. His temperature is 102.2°F (39°C), blood pressure is 112/71 mmHg, pulse is 115/min, and respirations are 24/min. Knee extension with hips flexed produces significant resistance and pain. A lumbar puncture is performed with the following results:

Opening pressure: Normal
Fluid color: Clear
Cell count: Increased lymphocytes
Protein: Slightly elevated

Which of the following is the most likely cause of this patient's symptoms?
Options:
A. Cryptococcus
B. Group B streptococcus
C. Herpes simplex virus
D. Neisseria meningitidis
The answer is C.
A 21-year-old male presents to his primary care provider for fatigue. He reports that he graduated from college last month and returned 3 days ago from a 2 week vacation to Vietnam and Cambodia. For the past 2 days, he has developed a worsening headache, malaise, and pain in his hands and wrists. The patient has a past medical history of asthma managed with albuterol as needed. He is sexually active with both men and women, and he uses condoms “most of the time.” On physical exam, the patient’s temperature is 102.5°F (39.2°C), blood pressure is 112/66 mmHg, pulse is 105/min, respirations are 12/min, and oxygen saturation is 98% on room air. He has tenderness to palpation over his bilateral metacarpophalangeal joints and a maculopapular rash on his trunk and upper thighs. Tourniquet test is negative. Laboratory results are as follows:

Hemoglobin: 14 g/dL
Hematocrit: 44%
Leukocyte count: 3,200/mm³
Platelet count: 112,000/mm³

Serum:
Na+: 142 mEq/L
Cl-: 104 mEq/L
K+: 4.6 mEq/L
HCO3-: 24 mEq/L
BUN: 18 mg/dL
Glucose: 87 mg/dL
Creatinine: 0.9 mg/dL
AST: 106 U/L
ALT: 112 U/L
Bilirubin (total): 0.8 mg/dL
Bilirubin (conjugated): 0.3 mg/dL

Which of the following is the most likely diagnosis in this patient?
Options:
A. Chikungunya
B. Dengue fever
C. Epstein-Barr virus
D. Hepatitis A

现在让我们评估 Llama 2 在这些提示下的表现,并观察它的表现如何:

few_shot_llama_answers_wo_chat_template = []
for item in tqdm(questions):
    prompt = build_few_shot_prompt_wo_chat_template(PROMPT, item, few_shot_prompts)
    input_ids = tokenizer(prompt, return_tensors="pt", truncation=True).input_ids.cuda()
    outputs = model.generate(input_ids=input_ids, max_new_tokens=10, do_sample=False)
    gen_text = tokenizer.batch_decode(outputs.detach().cpu().numpy()[:, input_ids.shape[1]:], skip_special_tokens=True)[0]
    few_shot_llama_answers_wo_chat_template.append(gen_text.strip())

few_shot_llama_predictions_wo_chat_template = [parse_answer(x) for x in few_shot_llama_answers_wo_chat_template]
print(calculate_accuracy(ground_truth, few_shot_llama_predictions_wo_chat_template))

我们达到了 36%的准确率。这个结果比我们之前的少量示例准确率低了 6̶%̶ 4.3%。这进一步证明了我们之前的观点:根据用于微调我们打算使用的大型语言模型(LLM)的模板来构建提示是至关重要的。提示模板很重要!

用 CoT 提示进行 Llama 2 7B-Chat 的提示调优

让我们通过评估 CoT 提示来结束。请记住,我们的数据集包含旨在通过美国医学执照考试(USMLE)测试医学知识的问题。这类问题通常需要既有事实回忆,也需要概念推理才能回答。因此,它是测试 CoT 效果的完美任务。

首先,我们必须提供一个 CoT 提示示例给模型,演示如何推理一个问题。为此,我们将使用谷歌 MedPALM 论文中的一个提示[12]。

用于评估 MedPALM 模型在 MedQA 数据集上的五-shot CoT 提示。该提示借用自[12]中的表 A.18,第 41 页 (来源)。

我们使用这个五-shot 提示词来评估模型。由于这个提示风格与之前的提示略有不同,我们再次创建一些辅助函数来处理这些提示词并获取输出。在使用 CoT 提示时,我们生成较长的输出令牌,以便让模型在回答问题之前“思考”和“推理”。

def create_query_cot(item):
    """
    Creates the input for the model using the question and the multiple choice options in the CoT format.

    Args:
        item (dict): A dictionary containing the question and options.
            Expected keys are "question" and "options", where "options" is another
            dictionary with keys "A", "B", "C", and "D".

    Returns:
        str: A formatted query combining the question and options, ready for use.
    """
    query = "Question: " + item["question"] + "\n" + \
            "(A) " + item["options"]["A"] + " " +  \
            "(B) " + item["options"]["B"] + " " +  \
            "(C) " + item["options"]["C"] + " " +  \
            "(D) " + item["options"]["D"]
    return query

def build_cot_prompt(instruction, input_question, cot_examples):
    """
    Builds the few-shot prompt for the GPT API using provided examples.

    Args:
        content (dict): The content for which to create a query, similar to the
            structure required by `create_query`.
        few_shot_examples (list of dict): Examples to simulate a hypothetical
            conversation. Each dict must have "question" and an "explanation".

    Returns:
        list of dict: A list of messages, simulating a conversation with
            few-shot examples, followed by the current user query.
    """

    messages = [{"role": "system", "content": instruction}]
    for item in cot_examples:
        messages.append({"role": "user", "content": item["question"]})
        messages.append({"role": "assistant", "content": item["explanation"]})

    messages.append({"role": "user", "content": create_query_cot(input_question)})

    return messages

def parse_answer_cot(text):
    """
    Extracts the choice from a string that follows the pattern "Answer: (Choice) Text".

    Args:
    - text (str): The input string from which to extract the choice.

    Returns:
    - str: The extracted choice or a message indicating no match was found.
    """
    # Regex pattern to match the answer part
    pattern = r"Answer: (.*)"

    # Search for the pattern in the text and extract the matching group
    match = re.search(pattern, text)

    if match:
        if len(match.group(1)) > 1:
            return match.group(1)[1]
        else:
            return ""
    else:
        return ""
cot_llama_answers = []
for item in tqdm(questions):
    cot_prompt = build_cot_prompt(COT_INSTRUCTION, item, COT_EXAMPLES)
    input_ids = tokenizer.apply_chat_template(cot_prompt, tokenize=True, return_tensors="pt").cuda() #modified on 09/03/2024
    # prompt = tokenizer.apply_chat_template(cot_prompt, tokenize=False)
    # input_ids = tokenizer(prompt, return_tensors="pt", truncation=True).input_ids.cuda()    
    outputs = model.generate(input_ids=input_ids, max_new_tokens=100, do_sample=False)
    gen_text = tokenizer.batch_decode(outputs.detach().cpu().numpy()[:, input_ids.shape[1]:], skip_special_tokens=True)[0]
    cot_llama_answers.append(gen_text.strip())

cot_llama_predictions = [parse_answer_cot(x) for x in cot_llama_answers]
print(calculate_accuracy(ground_truth, cot_llama_predictions))

我们在使用 CoT 提示词进行 Llama 2–7B 模型测试时,性能下降至 2̶0̶%̶ 21.33%。这一结果与 CoT 论文[6]的发现大致一致,论文中提到 CoT 是 LLM 模型的一个涌现特性,且随着模型规模的增大,CoT 的表现会得到改善。话虽如此,我们来分析为什么性能会急剧下降。

2024 年 09 月 03 日编辑 — 与零-shot 设置类似,我修正了发送到模型的提示词方式,调整为 CoT 设置下的新方式。我得到的新分数为 21.33%,相比原先 20%的得分,提升了约 1.33%。我在这里和代码中进行了修正,以避免使用聊天模板时的任何混淆。本文中的发现和结论并未受到此次修正的影响。

Llama 2 中 CoT 的失败模式

我们抽取了一些 Llama 2 在测试集问题中的回答,用以分析错误案例:

这些 CoT 样本和图表的分析不受小“bug”修复的影响。我已经验证过,这些图表中使用的预测结果,在旧设置和新设置中是相同的。

示例预测 1 — 模型得出一个答案,但没有遵循格式,导致解析结果困难。(作者提供的图片)

示例预测 2 — 模型未能遵循提示格式,也未能给出确凿的答案。(作者提供的图片)

虽然 CoT 提示词让模型在给出最终答案之前能够“思考”,但在大多数情况下,模型要么无法得出确定的答案,要么给出的答案格式与我们的示例不一致。我没有在这里分析的一种失败模式,但可能值得探索的是,检查测试集中的一些案例,模型可能“推理”错误,从而得到错误的答案。这超出了当前文章和我的医学知识的范围,但肯定是我以后打算重新审视的问题。

使用零-shot 提示词提示 GPT-3.5

让我们开始定义一些辅助函数,帮助我们处理这些输入以便利用 GPT API。你需要生成一个 API 密钥才能使用 GPT-3.5 API。你可以在 Windows 中使用以下命令设置 API 密钥:

setx OPENAI_API_KEY "your-api-key-here"

或在 Linux 中使用:

export OPENAI_API_KEY "your-api-key-here"

在当前使用的会话中。

from openai import OpenAI
import re
from tqdm import tqdm

# assuming you have already set the secret key using env variable
# if not, you can also instantiate the OpenAI client by providing the 
# secret key directly like so:
# I highly recommend not doing this, as it is a best practice to not store
# the api key in your code directly or in any plain-text file for security 
# reasons.
# client = OpenAI(api_key = "")

client = OpenAI() 
 def get_response(messages, model_name, temperature = 0.0, max_tokens = 10):
    """
    Obtains the responses/answers of the model through the chat-completions API.

    Args:
        messages (list of dict): The built messages provided to the API.
        model_name (str): Name of the model to access through the API
        temperature (float): A value between 0 and 1 that controls the randomness of the output.
        A temperature value of 0 ideally makes the model pick the most likely token, making the outputs (mostly) deterministic.
        max_tokens (int): Maximum number of tokens that the model should generate

    Returns:
        str: The response message content from the model.
    """
    response = client.chat.completions.create(
        model=model_name,
        messages=messages,
        temperature=temperature,
        max_tokens=max_tokens
    )
    return response.choices[0].message.content

这个函数现在构建了适用于 GPT-3.5 API 格式的提示词。我们可以通过该库提供的 chat-completions API 与 GPT-3.5 模型进行互动。该 API 要求消息按字典列表的格式发送给 API。每个消息必须指定角色和内容。关于“system”“user”“assistant”角色的约定与之前为 Llama-7B 聊天模型描述的相同。

现在让我们使用 GPT-3.5 API 处理测试集并获取响应。在收到所有响应后,我们从模型的响应中提取选项并计算准确率。

zero_shot_gpt_answers = []
for item in tqdm(questions):
    zero_shot_prompt_messages = build_zero_shot_prompt(PROMPT, item)
    answer = get_response(zero_shot_prompt_messages, model_name = "gpt-3.5-turbo", temperature = 0.0, max_tokens = 10)
    zero_shot_gpt_answers.append(answer)

zero_shot_gpt_predictions = [parse_answer(x) for x in zero_shot_gpt_answers]
print(calculate_accuracy(ground_truth, zero_shot_gpt_predictions))

目前我们的性能为 63%。这比 Llama 2–7B 的性能有了显著提升。这并不令人惊讶,因为 GPT-3.5 可能比 Llama 2–7B 大得多,并且训练了更多的数据,另外 OpenAI 可能还对模型进行了其他专有优化。现在让我们看看少量示例提示的效果如何。

使用少量示例提示词提示 GPT-3.5

为了向大型语言模型提供少量示例,我们重用从训练集中采样的三个示例,并将它们附加到提示词中。对于 GPT-3.5,我们创建一个包含示例的消息列表,类似于我们之前处理 Llama 2 的方式。输入通过“user”角色附加,相关的选项通过“assistant”角色呈现。我们重用了之前构建少量示例提示词的函数。

这相当于为 GPT-3.5 创建一个虚构的多轮对话历史,每一轮都对应一个示例演示。

现在让我们使用 GPT-3.5 获取输出。

few_shot_gpt_answers = []
for item in tqdm(questions):
    few_shot_prompt_messages = build_few_shot_prompt(PROMPT, item, few_shot_prompts)
    answer = get_response(few_shot_prompt_messages, model_name= "gpt-3.5-turbo", temperature = 0.0, max_tokens = 10)
    few_shot_gpt_answers.append(answer)

few_shot_gpt_predictions = [parse_answer(x) for x in few_shot_gpt_answers]
print(calculate_accuracy(ground_truth, few_shot_gpt_predictions))

我们通过少量示例提示将性能从 63% 提高到了 67%!这是一项显著的提升,突显了向模型提供任务演示的价值。

使用 CoT 提示词提示 GPT-3.5

现在让我们评估使用 CoT 提示词的 GPT-3.5。我们重新使用相同的 CoT 提示词并获取输出:

cot_gpt_answers = []
for item in tqdm(questions):
    cot_prompt = build_cot_prompt(COT_INSTRUCTION, item, COT_EXAMPLES)
    answer = get_response(cot_prompt, model_name= "gpt-3.5-turbo", temperature = 0.0, max_tokens = 100)
    cot_gpt_answers.append(answer)

cot_gpt_predictions = [parse_answer_cot(x) for x in cot_gpt_answers]
print(calculate_accuracy(ground_truth, cot_gpt_predictions))

使用 CoT 提示词与 GPT-3.5 提示进行的结果准确率为 71%!这比少量示例提示提高了 4%。看起来在回答问题之前让模型“思考”是有益的,这也与论文[6]的发现一致,CoT 解锁了大参数模型的性能提升。

结论与收获:

提示是使用大型语言模型(LLMs)时的一个关键技能,理解提示工具包中的各种工具可以帮助根据上下文从 LLMs 中提取更好的性能。我希望这篇文章能作为一个广泛且(希望!)易于理解的介绍。然而,它并不旨在提供所有提示策略的全面概述。提示仍然是一个高度活跃的研究领域,许多方法被提出,如 ReAct [13]、Tree-of-Thought 提示[14]等。我建议探索这些技术,以更好地理解它们并增强你的提示工具包。

可重复性

在本文中,我的目标是尽可能使所有实验具备确定性并且可重复。我们使用贪婪解码来获得零样本、少量样本和 CoT 提示下的输出,使用 Llama-2 模型。虽然这些得分从技术上讲应该是可重复的,但在少数情况下,Cuda/GPU 相关或库问题可能导致稍微不同的结果。

同样,在从 GPT-3.5 API 获取响应时,我们使用温度值为 0 来获得结果,并且在所有提示设置中仅选择下一个最可能的标记,而不进行采样。这使得结果“大多数是确定性的”,因此再次发送相同的提示给 GPT-3.5 可能会导致稍有不同的结果。

我已提供在所有提示设置下模型的输出,以及子采样的测试集、少量样本提示示例和 CoT 提示(来自 MedPALM 论文),用于重现本文中报告的得分。

参考文献:

本文中提到的所有论文都在此列出。如果我遗漏了任何参考文献,请告诉我,我会添加它们!

[1] Yang, J., Jin, H., Tang, R., Han, X., Feng, Q., Jiang, H., … & Hu, X. (2023). 实践中驾驭 LLMs 的力量:关于 ChatGPT 及其以后的调查。arXiv 预印本 arXiv:2304.13712

[2] Radford, A., Wu, J., Child, R., Luan, D., Amodei, D., & Sutskever, I. (2019). 语言模型是无监督的多任务学习者。OpenAI 博客1(8),9。

[3] Brown, T., Mann, B., Ryder, N., Subbiah, M., Kaplan, J. D., Dhariwal, P., … & Amodei, D. (2020). 语言模型是少量学习者。神经信息处理系统进展33,1877–1901。

[4] Wei, J., Bosma, M., Zhao, V. Y., Guu, K., Yu, A. W., Lester, B., … & Le, Q. V. (2021). 微调语言模型是零样本学习者。arXiv 预印本 arXiv:2109.01652

[5] Ouyang, L., Wu, J., Jiang, X., Almeida, D., Wainwright, C., Mishkin, P., … & Lowe, R. (2022). 训练语言模型以根据人类反馈遵循指令。神经信息处理系统进展35,27730–27744。

[6] Wei, J., Wang, X., Schuurmans, D., Bosma, M., Xia, F., Chi, E., … & Zhou, D. (2022). 连锁思维提示激发大型语言模型的推理能力. 神经信息处理系统进展, 35, 24824–24837.

[7] Thoppilan, R., De Freitas, D., Hall, J., Shazeer, N., Kulshreshtha, A., Cheng, H. T., … & Le, Q. (2022). Lamda: 面向对话应用的语言模型. arXiv 预印本 arXiv:2201.08239.

[8] Chowdhery, A., Narang, S., Devlin, J., Bosma, M., Mishra, G., Roberts, A., … & Fiedel, N. (2023). Palm: 利用路径扩展语言建模. 机器学习研究期刊, 24(240), 1–113.

[9] Jin, D., Pan, E., Oufattole, N., Weng, W. H., Fang, H., & Szolovits, P. (2021). 这个病人得了什么病?来自医学考试的大规模开放域问答数据集. 应用科学, 11(14), 6421.

[10] Touvron, H., Martin, L., Stone, K., Albert, P., Almahairi, A., Babaei, Y., … & Scialom, T. (2023). Llama 2: 开放的基础和微调的聊天模型. arXiv 预印本 arXiv:2307.09288.

[11] platform.openai.com/docs/models/gpt-3-5-turbo

[12] Singhal, K., Azizi, S., Tu, T., Mahdavi, S. S., Wei, J., Chung, H. W., … & Natarajan, V. (2023). 大型语言模型编码临床知识. 自然, 620(7972), 172–180.

[13] Yao, S., Zhao, J., Yu, D., Du, N., Shafran, I., Narasimhan, K. R., & Cao, Y. (2022 年 9 月). ReAct: 协同推理与行动在语言模型中的结合. 发表在 第十一届国际学习表示大会 上。

[14] Yao, S., Yu, D., Zhao, J., Shafran, I., Griffiths, T., Cao, Y., & Narasimhan, K. (2024). 思维树:使用大型语言模型进行深思熟虑的问题解决. 神经信息处理系统进展, 36.

分位损失简介,亦称为弹球损失

原文:towardsdatascience.com/an-introduction-to-quantile-loss-a-k-a-the-pinball-loss-33cccac378a9?source=collection_archive---------1-----------------------#2024-08-04

使用 Midjourney 生成的图像

了解用于评估概率预测的指标背后的直觉

Eryk LewinsonTowards Data Science Eryk Lewinson

·发表于Towards Data Science ·7 分钟阅读·2024 年 8 月 4 日

--

有很多关于回归评估指标的文章,如 MSE、MAE、RMSE 等。这些指标在我们关注均值或中位数预测时非常重要。然而,当我们希望训练模型以关注分布中的其他位置时,我们必须使用一种不同的指标,这种指标在数据科学博客文章中并不常见。

在本文中,我们将探讨分位损失,也称为弹球损失,这是分位回归中的首选指标。

几个定义,帮助我们入门

在解释分位损失之前,让我们快速浏览几个定义,以确保我们在同一页上。

让我们从一个简单的例子开始。属于回归类型的算法预测一个连续变量,例如,它们预测温度、股票价格、最新 iPhone 的需求等。

现在是时候进行统计学回顾了。α分位数是一个值,它将给定的一组数字分开,使得α × 100%的数字小于或等于此值,而剩余的(1 − α) × 100%的数字大于该值…

量子计算机与量子编码简介

原文:towardsdatascience.com/an-introduction-to-quantum-computers-and-quantum-coding-e5954f5a0415?source=collection_archive---------1-----------------------#2024-08-23

揭开量子计算、量子编程和量子算法的新世界的神秘面纱。

Oliver W. JohnsonTowards Data Science Oliver W. Johnson

·发表于 Towards Data Science ·阅读时间 15 分钟·2024 年 8 月 23 日

--

图片来自 ManuelUnsplash

前言:

这是我为我的一门物理课程所写内容的改编版,因此假设读者具备一定的数学和科学知识。文章中提到的主题包括一些线性代数、叠加原理、基本算法概念,以及在讨论算法时涉及的一些模运算。然而,由于你正在阅读一篇关于量子计算的文章,可能足够聪明,能够查阅并理解文中提到的所有概念。此外,所有的来源都有引用,你可以深入探索这些资源以进行更深的学习。还有,文中使用的所有图像和图表都是我自己生成的,使用了像 Microsoft Word、PyCharm 和 diagrams.net 这样的工具,除非另有说明。

为什么这很重要?

在你决定花时间阅读这篇内容前,你可能会想知道,尽管你从未接触过量子计算机,这对你有什么意义。实际上,突破性进展时时发生,量子计算在不同的计算领域中具有现实意义,尤其是在机器学习方面。首先,经典算法的量子类比有可能更加高效。

一个例子是量子支持向量机(Quantum Support Vector Machine)。值得注意的是,经典支持向量机(SVM)通常使用核技巧将数据转换到更高维的空间,从而找到分隔超平面。然而,量子支持向量机则具有显著的优势,因为它们能够自然地在指数级更高维的空间中表示数据,而不会遭遇经典计算机所面临的计算负担。这使得量子支持向量机能够更高效地处理更复杂的数据集。

另一个例子是在神经网络训练领域。量子计算的基本单元——量子比特,可以与其他量子比特纠缠,产生经典系统无法复制的关联。虽然纠缠为量子神经网络中相关的更新提供了可能性,但需要注意的是,这一概念仍在研究之中。

第一部分:量子计算简介

量子计算机的运作方式与经典计算机截然不同,利用量子特性和现象大幅提升计算能力。从高层次来看,有几个量子计算的基本原则将其与经典计算区分开:量子比特与经典比特,量子与经典逻辑门,量子现象的存在,以及量子计算所提供的增强计算能力所带来的机遇。

量子计算的核心是量子比特(qubit),它作为量子计算机中的基本计算单元,取代了经典计算机中的比特。经典比特只能独占地处于 0 或 1 的状态,而量子比特则可以处于 0 和 1 状态的叠加态(Microsoft,n.d.)。量子比特的概念非常难以理解;经典比特仅仅是电流或没有电流的状态,而量子比特则可以有许多不同的物理形式。

这些包括“自旋”量子比特,这是最直接的例子。这种类型的量子比特利用粒子(通常是电子)的自旋特性来完成计算。例如,要初始化一个自旋量子比特,可以使用量子点困住电子,然后通过与电子自旋状态相互作用的磁场来操控它(Harvey,2024)。比特和量子比特之间的计算差异是显著的,源于量子比特能够受到量子现象的影响,如 0 和 1 状态之间的叠加态,以及与其他量子比特的纠缠(Microsoft,n.d.)。

一个非常有助于可视化量子比特状态的工具是 Bloch 球;它实际上只是一个球体,北极和南极分别代表|0⟩和|1⟩,球体上的所有其他点表示北极和南极值的线性组合(微软,2024)。由于这种量子比特的表示使用了复数向量空间,因此量子比特的状态将在后续用狄拉克符号表示。量子比特叠加态的这一可视化有助于理解量子逻辑门,特别是因为它允许对正在执行的操作进行几何理解。通常,当量子比特初始化时,它处于 z 基态|0⟩状态,这与经典的 0 状态类似(Quantum-Inspire by QuTech,2024)。

Bloch 球示意图,维基百科。(无日期)en.wikipedia.org/wiki/Bloch_sphere。(GFDL 许可证)

经典计算机和量子计算机之间的另一个关键区别是逻辑门:经典计算机使用与、或、非等逻辑门来执行基本逻辑操作,而量子计算机使用量子逻辑门,如 X 门、哈达玛门、托福利门和 CNOT 门(维基百科,2024)。这些量子门用于对单个量子比特或少量量子比特执行逻辑操作,并可以与其他量子门结合,进行更复杂的操作和处理。

  1. 首先,X 门非常类似于经典的非门:它会反转量子比特的相位——如果量子比特处于|0⟩状态,它会反转为|1⟩状态,反之亦然。

  2. 接下来,哈达玛门用于将处于|0⟩状态的量子比特置于|1⟩和|0⟩之间的均等叠加态。第三,托福利门是一个多量子比特门的例子。

  3. 托福利门操作三个量子比特,其中两个是“控制”量子比特,一个是“目标”量子比特。在托福利门中,只有当两个控制量子比特处于|1⟩状态时,目标量子比特才会被反转(Roy & Chakrabarti,2024)。

  4. 最后,CNOT 门是量子计算中非常常见的一个门,我们稍后将讨论它的一个用例。CNOT 门也是一个多量子比特门,它有一个目标量子比特和一个控制量子比特;当控制量子比特处于|1⟩状态时,它会反转目标量子比特的相位。

这些只是许多有趣的量子逻辑门中的一部分,值得注意的是,与经典逻辑门不同,量子逻辑门不一定有一个物理“门”供量子比特通过,而是这些操作在量子比特上执行,并且根据许多因素的不同,这些操作可以采取不同的形式。

经典计算和量子计算之间的第三个主要区别是量子现象的存在,如叠加、超导、纠缠和干涉。这些特性根据执行量子计算的方法的不同而以不同的方式被使用(Microsoft Azure,2024)。另一个存在的特性是量子退相干,它对有用或广泛量子计算的发展构成了严重问题。量子退相干是指处于叠加态的粒子与环境相互作用并与环境纠缠,最终干扰计算结果(Brandt,1999)。

量子计算机的计算能力非常强大:以一个用于寻找整数素因子的算法为例。在经典计算中,领先的素因数分解算法是广义数域筛法(Wikipedia,2024)。该程序以准多项式时间复杂度运行,显示了分解非常大数的困难。与领先的量子算法——Shor 算法相比,后者以对数空间复杂度和多对数时间复杂度运行,虽然这也是一个复杂的表达式,但归结为它显著更高效(Li 等,2022)。显然,这只是一个例子,但它证明了量子计算的强大——将一个以指数时间运行的程序转变为一个以对数时间运行的程序,实在是令人惊叹。

第二部分:量子编程:语言、编译器与算法

尽管它们的硬件与经典计算机根本不同,但量子计算机的编程语言通常与经典语言的语法相似。包括 QCL、Qiskit 和 Q#,它们分别基于 C、Python 和 C#/F#的语法。此外,它们的编译器分别是用 C++、Python 和 C++、C#构建的。(IonQ,2024)。因此,经典语言和量子语言在语法上可能非常相似——主要的区别来自程序的内容,以及量子算法的结构方式。

在研究不同的语言、它们的语法以及它们与所基于的经典语言的比较之前,理解量子程序的内容及其原因非常重要——无论语法多么相似,经典程序与量子程序之间始终存在一条无法逾越的鸿沟。

这源于量子计算机的机制——正如之前讨论的那样,量子计算是基于将量子比特保持在叠加态,并对其应用不同的“门”——实质上是沿着其表示的布洛赫球进行变换。简而言之,不同于经典计算机,你编写一个程序,它会利用一个预制的电路来执行计算,量子编程则是实际对电路进行编码的过程。让我们通过一些伪代码和其相关的量子电路来更好地理解这一点。也许最简单的量子程序/电路,以下程序只是初始化一个量子比特,应用一个哈达玛门将其置于叠加态,然后测量该量子比特的状态。

图片由作者提供。

本程序的相关量子电路为:

“H”表示哈达玛门,测量符号表示正在进行测量。图片由作者提供。

测量符号后的双线表示量子比特不再处于叠加态,而是处于两种离散状态之一(0 或 1),因为其波函数在测量过程中已坍缩。

为了更好地了解不同量子语言的语法,我们来看一下三种前面提到的语言中的程序,这些程序都实现相同的功能。这三种程序都是用来创建贝尔态的,即两个量子比特的纠缠态。对这两个量子比特施加的门(操作)是:对第一个量子比特应用哈达玛门(0),然后对第二个量子比特应用哈达玛门(1),第一个量子比特作为控制位。CNOT 门的功能实际上就是将两个量子比特纠缠起来(Rioux, 2023)。

#Qiskit (Python Basd)
#Create a quantum circuit with two qubits
qc = QuantumCircuit(2)

#Apply a Hadamard gate on the first qubit
qc.h(0)

#Apply a CNOT gate with the first qubit as control and the second qubit as target
qc.cx(0, 1)
//Q# (C#/F# based)
namespace BellState{
  operation PrepareBellState() : Unit{
    using (qubits = Qubit[2]) {
      //Apply a Hadamard gate on the first Qubit
      H(qubits[0]);
      //Apply a CNOT gate with the first qubit as control and the second qbit as target
      CNOT(qubits[0], qubits[1]);
    }
  }
}
//QCL (C based)
init {
  qubits q[2]
  //Apply a Hadamard gate on the first qubit
  H(q[0]);
  //Apply a CNOT gate with the first qubit as control and the second as target
  CNOT(q[0], q[1]);
} 

毋庸置疑,量子程序在语法上与它们所基于的语言非常相似——例如,Python 程序使用了几个内置方法,并没有其他太多的内容,而基于 C# 的程序则充满了花括号。回顾一些量子语言的语法有助于理解量子程序的外观,但实际情况是,所使用的硬件差异如此之大,以至于量子程序中的代码对经典计算机而言是无用的,反之亦然。因此,分析两个目的相同的算法,一个是经典算法,一个是量子算法,并对每个步骤进行剖析,分析它们是如何实现结果的,才会更加有趣。

回想一下第一部分中呈现的例子(GNFS 和 Shor 算法),我们查看了两种质因数分解算法的时间复杂度。由于这两种算法都相当抽象和复杂,可能比起检查它们的伪代码,以段落格式理解它们各自的理论会更加容易。

经典算法,广义数域筛法(GNFS),可以总结为五个主要算法步骤(Case, n.d.)。在整个解释中,“N”指的是待因式分解的数字。

  1. 第一步是多项式选择:这一阶段涉及选择两个多项式,使它们在某些点上模 N 计算时相乘得到平滑数。

  2. 下一步是“筛选”步骤:目标是找到一组整数(a,b),使得 𝑓(𝑎)⋅𝑔(𝑏) ≡ ℎ²(mod N),其中 h 是一个平滑数,并存储所有的值(a,b 和 h)。

  3. 第三步是矩阵步骤:从筛选步骤中找到的关系构建一个大型矩阵 A。

  4. 接下来使用高斯消元法将 A 化简为更简单的形式,同时保留其属性。这个过程将识别出一组线性无关的关系。

  5. 使用线性代数方法,如 Lanczos 算法,找到矩阵的零空间——这将提供与关系之间的依赖性相关的向量。

  6. 将之前找到的关系结合起来,会在模 N 中产生平方,经过进一步的数学操作后得到两个整数 X 和 Y。

  7. 这两个整数用于通过计算 X — Y 和 X + Y 与 N 的最大公约数(GCD)来找到 N 的非平凡因子(Case, n.d.)。该方法的复杂度为准多项式,虽然它在子多项式时间内运行,但比量子方法 Shor 算法要慢得多。

使用 Shor 算法计算整数 N 的质因数与使用 GNFS 完全不同。Shor 算法可以分解为几个主要步骤。

  1. 第一步使用经典计算:选择一个随机整数 r,满足 1 < r < N,计算它们的最大公约数,如果不等于 1,则它是 N 的非平凡因子。

  2. 下一步是准备所需的量子比特——我们使用两个量子寄存器来实现,它们的功能类似于经典寄存器。在第一个寄存器中,有足够的量子比特来表示从 0 到 q–1 的整数,其中 q 是至少 N² 的 2 的幂。第二个寄存器有足够的量子比特来表示从 0 到 N–1 的整数。

  3. 下一步与经典计算有很大不同:为了将整个第一个寄存器放入叠加态,应用 Hadamard 变换到每一个量子比特。

  4. 接下来使用量子电路计算函数 f(x) = r^x mod(N),并将结果存储在第二个寄存器中;这将纠缠第一个和第二个寄存器。

  5. 接下来,测量第二个寄存器——这将使其坍缩成一个状态 |k⟩(其中 k = r^x mod(N)),从而将第一个寄存器保持在一个值 x 的叠加态中,这些值映射到 |k⟩。现在,函数 f(x)=r^x mod(N) 的周期可以表示为 T。

  6. 算法的倒数第二步是对第一个寄存器应用量子傅里叶变换(QFT),这将在频率域中产生一系列峰值,对应于 1/T 的值。

  7. 量子计算的最后一步是测量第一个寄存器——结果将是一个整数 B,使得 B = q/T,回想一下 q 是我们定义第一个寄存器时的值。完成量子计算后,接下来进入经典后处理步骤,以获得最终结果。

后处理涉及操纵测量结果 B 以得到周期 T。如果 T 是偶数,计算 N 与 r^(T/2) + 1 和 r^(T/2) - 1 的最大公约数(GCD),这将产生 N 的非平凡因子。如果 T 是奇数,使用不同的 r 值重复算法。该程序的多对数时间复杂度非常高效,尤其是与 GNFS 算法相比(Pavlidis & Gizopoulos,2022)。

这里是两种算法流程的视觉表示,以帮助理解,其中红色表示开始步骤,蓝色表示 GNFS 算法,绿色表示 Shor 算法:

蓝色步骤对应 GNFS,绿色步骤对应 Shor 算法。图片来自作者。

Shor 算法比 GNFS 运行得更快的原因在于其使用了根本不同的计算概念:Shor 算法利用量子力学来实现多项式时间复杂度。这种加速主要得益于量子并行性(同时执行多个量子操作的能力)和量子傅里叶变换的高效执行,这在经典计算中是无法实现的。通过利用叠加态和纠缠,Shor 算法将因式分解问题转化为周期寻找问题,解决速度远远超过经典方法(Brandt,1999)。

显然,两个算法都非常复杂,但它们作为一个极好的例子,因为大整数因式分解问题是量子计算机比经典计算机要快得多的任务。我们可以分析量子代码的一个更简单的例子,就是量子版的剪刀石头布(或者如果更容易理解,可以是抛硬币)。两个玩家各自初始化一个量子比特(各一个)为|0⟩状态,并施加哈达玛门,这将其置于|0⟩和|1⟩的等概率叠加态。最后,两个量子比特被测量——如果两个量子比特都坍缩到|0⟩或|1⟩状态,则为平局。否则,坍缩到|0⟩状态的玩家输,而坍缩到|1⟩状态的玩家赢。让我们编写代码来运行这个程序,使用 Qiskit:

qc = QuantumCircuit(2, 2)  # initialize a quantum circuit with 2 qubits and 2 classical bits

qc.h(0)  # apply Hadamrd gate to qubit 0, this is Bob's qubit
qc.h(1)  # apply Hadamard gate to qubit 1, this is Alice's qubit

qc.measure(0, 0) # measure Bob's qubit and map it to classical bit 0
qc.measure(1, 1) # measure Alice's qubit and map it to classical bit 1

print(qc) # prints the quantum circuit accociated with this program

这段代码的输出只是与程序相关的量子电路,因为它实际上并没有在量子计算机上运行电路。然而,这就是定义一个非常基本量子电路时使用的一般格式——分配一定数量的量子比特和经典比特,然后按顺序列出要对每个量子比特执行的操作。该代码的输出只是该电路的视觉表示:

带有“H”的框表示 Hadamard 门,带有“X”的框表示 CNOT 门,带有“M”的框表示正在进行的测量。图片由作者提供。

在我们能够在量子计算机上运行这个程序之前,有几个步骤需要完成。最重要的步骤是优化电路;并非所有的量子计算机都能以相同的方式在量子比特上操作特定的门,也并非所有量子计算机的量子比特之间都有相同的连接性。我们需要将这个电路优化步骤添加到代码中,以便让它准备好在实际的量子计算机上运行。为此,我们使用以下代码,它定义了我们通过 IBM API 密钥访问 IBM 量子后台的方式,然后对电路进行优化,并打印优化后的电路。

print(qc) # prints the quantum circuit accociated with this program

service = QiskitRuntimeService(channel="ibm_quantum", token="your_token")

backend = service.least_busy(simulator=False, operational=True)

pm = generate_preset_pass_manager(backend=backend, optimization_level=1)

isa_circuit = pm.run(qc)

print(isa_circuit)

该程序的输出并不那么重要,因为它只是同一电路的更复杂版本,但它生成了一个为 IBM 量子计算机优化的电路,尽管看起来差异很大且更复杂,但它的功能与之前的电路相同。

总结来说,尽管量子编程的主题因其多种语言和对比算法,以及理解所需的数学、物理和计算背景,可能会让人感到困难,但如果分解得当,它其实并不那么难。通过渐进学习,理解量子计算是完全可实现的。此外,量子计算在许多 STEM 领域中具有令人着迷的方面——数论、线性代数、微积分和离散数学都应用于量子算法的理论方面;工程学、物理学、计算机科学和逻辑则都应用于量子算法的实际设计。不过,越是学习量子计算这一迷人的领域,你可能越会发现自己和理查德·费曼的名言产生共鸣:“我足够聪明,知道自己有多笨。”(Goodreads, 2024)

参考文献

Adedoyin, A., & 等人 (2022 年 1 月 8 日). 初学者的量子算法实现. ACM 数字图书馆. 2024 年 5 月 27 日取自 dl.acm.org/doi/10.1145/3517340#d1e3003

Brandt, H. E. (1999 年 11 月). 量子比特设备与量子退相干问题. Science Direct. 2024 年 5 月 20 日取自 www.sciencedirect.com/science/article/pii/S0079672799000038

Brubaker, B. (2023 年 10 月 17 日). 三十年后,量子因式分解的速度提升. Quanta 杂志. 2024 年 5 月 27 日取自 www.quantamagazine.org/thirty-years-later-a-speed-boost-for-quantum-factoring-20231017/

Case, M. (无日期). 初学者的通用数域筛法指南. 马里兰大学计算机科学系. 2024 年 5 月 26 日取自 www.cs.umd.edu/~gasarch/TOPICS/factoring/NFSmadeeasy.pdf

Goodreads. (2024 年). 理查德·费曼的名言(《你一定是在开玩笑,费曼先生!》的作者). Goodreads. 2024 年 5 月 27 日取自 www.goodreads.com/author/quotes/1429989.Richard_P_Feynman

Harvey, S. P. (2024 年 3 月 5 日). 量子点/自旋量子比特. 牛津大学出版社和美国物理学会. 2024 年 5 月 19 日取自 oxfordre.com/physics/display/10.1093/acrefore/9780190871994.001.0001/acrefore-9780190871994-e-83

IBM. (2024 年). IBM Qiskit 文档. IBM 量子文档. 2024 年 5 月 26 日取自 docs.quantum.ibm.com/

IonQ. (2024 年 3 月 14 日). 七种量子语言中的“你好,多重世界”. IonQ. 2024 年 5 月 26 日取自 ionq.com/docs/hello-many-worlds-seven-quantum-languages

Li, J., Peng, X., Du, J., & Suter, D. (2022 年 1 月 8 日). 一种高效的精确量子算法用于整数平方自由分解问题. 《自然》. 2024 年 5 月 26 日取自 www.nature.com/articles/srep00260

Microsoft. (无日期). 什么是量子比特? Microsoft Azure. 2024 年 5 月 19 日取自 azure.microsoft.com/en-us/resources/cloud-computing-dictionary/what-is-a-qubit

Microsoft. (2024 年). Azure 量子 | 单量子比特门. Azure 量子. 2024 年 5 月 20 日取自 quantum.microsoft.com/en-us/explore/concepts/single-qubit-gates

Microsoft Azure. (2024 年 1 月 12 日). 理解量子计算——Azure 量子. Microsoft Learn. 2024 年 5 月 20 日取自 learn.microsoft.com/en-us/azure/quantum/overview-understanding-quantum-computing

Pavlidis, A., & Gizopoulos, D. (2022 年 7 月 19 日). 量子密码学——Shor 算法解释. Classiq. 2024 年 5 月 27 日取自 www.classiq.io/insights/shors-algorithm-explained

Quantum-Inspire by QuTech. (2024 年). 量子比特基态. Quantum Inspire. 2024 年 5 月 20 日取自 www.quantum-inspire.com/kbase/qubit-basis-states/

Rioux, F. (2023 年 1 月 10 日). 8.53: 贝尔态练习。Chemistry LibreTexts. 2024 年 5 月 26 日检索自 chem.libretexts.org/Bookshelves/Physical_and_Theoretical_Chemistry_Textbook_Maps/Quantum_Tutorials_(Rioux)/08%3A_Quantum_Teleportation/8.53%3A_Bell_State_Exercises

Roy, S. G., & Chakrabarti, A. (2024 年 3 月 5 日). 托福利门。Science Direct. 2024 年 5 月 20 日检索自 www.sciencedirect.com/topics/computer-science/toffoli-gate

Wikipedia. (2024 年). 一般数域筛法。Wikipedia. 2024 年 5 月 24 日检索自 en.wikipedia.org/wiki/General_number_field_sieve

Wikipedia. (2024 年 5 月 15 日). 量子逻辑门。Wikipedia. 2024 年 5 月 19 日检索自 en.wikipedia.org/wiki/Quantum_logic_gate

Wikipedia. (无日期). 布洛赫球。Wikipedia. 2024 年 8 月 20 日检索自 en.wikipedia.org/wiki/Bloch_sphere

强化学习简介

原文:towardsdatascience.com/an-introduction-to-reinforcement-learning-995737d3f1d?source=collection_archive---------2-----------------------#2024-05-27

深入探讨强化学习的基础知识,包括基于模型的方法和无模型的方法

Angjelin HilaTowards Data Science Angjelin Hila

·发布于 Towards Data Science ·阅读时间:34 分钟·2024 年 5 月 27 日

--

使用创作共享许可,来源:elifesciences.org/digests/57443/reconstructing-the-brain-of-fruit-flies#copyright

什么是强化学习?

通过模仿生物体的方式来构建智能系统是一条可行的道路。

生物体从环境中转化信息,处理这些信息(这正是认知科学研究的内容),并输出有利于生存的行为。这些行为在最基本的层面上包括觅食、繁殖和避免伤害。它们还包括广泛的人类活动,如游戏、创造力、问题解决、设计与工程、社交、浪漫以及智力生活。

那么,我们如何设计一个能够完成以上所有任务的系统呢?

如果我们要将一个简单的生物体建模为某种环境的函数,我们需要一个代理模型、一个环境模型,以及某个函数,用以将代理从当前状态移动到期望状态。

在心理学中,有两个主要的学派试图解释人类行为:行为主义认知科学。行为主义者将行为理解为学习机制的功能,学习可以归因于输出行为。而认知科学则通过信息处理方法建模代理与环境的互动。在这种方法中,代理通过感官首先将外部刺激转化为内部表征,随后将其进行层层转化和整合,一直到思维和推理能力,再返回某种行为输出。在前一种方法中,学习主要被理解为环境条件作用的功能,而在后一种方法中,心理表征被认为在预测行为时不可或缺。强化学习大多借鉴了行为主义方法,其中环境奖励决定了代理在搜索空间中的演化。

操作性条件作用,是 20 世纪 50 至 60 年代盛行的行为主义心理学流派,定义学习为环境中奖励和惩罚机制的产物。操作性条件作用的前身包括爱德华·桑代克提出的效果法则,该法则认为产生满足效果的行为更可能重复,而产生不满足效果的行为则不太可能重复。B.F. 斯金纳通过强化和惩罚来操作化效果。强化通过增加行为重复的可能性来起作用,无论是接近还是去除抑制因素。接近称为正向强化,而避免的反转称为负向强化。正向强化的例子包括在一项运动中变得擅长并频繁获胜。负向强化的例子包括去除抑制性刺激,例如在比赛中挑衅你的学校恶霸。操作性条件作用预测你更可能重复获得最大奖励的行为。另一方面,惩罚包括通过增加负面后果(正向惩罚)或去除与行为相关的奖励(负向惩罚)来控制行为效果。当犯规导致被逐出比赛时,它说明了正向惩罚。当你表现不佳并失去比赛时,它说明了负向惩罚,这可能导致未来避免参与比赛。

人类社会中的“生活游戏”充满了二级强化物或社会构建的奖励与惩罚,这些奖励与惩罚塑造行为。这些包括金钱、成绩、大学录取标准、赢输游戏的规则,它们建立在接近生物需求的自然强化物之上,如食物、繁殖和社会认同。

记忆在学习中扮演着重要角色,因为它能够保持先前的经验。证据表明,记忆更多地编码奖励和惩罚,而不是经验的内容(Tyng et al., 2017)。受试者更有可能对奖励性的经验产生积极回忆,因此更可能重复这些经验,而对负面的经验产生不利回忆,并可能在未来避免它们。记忆的机制复杂多样,证据表明,受试者通过回忆自己的记忆在重新塑造这些记忆方面扮演了积极角色(Spens & Burgess, 2024)。这一事实使得行为主义的图景变得更加复杂,因为受试者对经验的解读可以被回溯性地修改和重新构建,从而使得仅凭条件反射原理进行预测变得困难。此外,奖励和惩罚过于简化了积极和消极情感的景观,而这种情感景观实际上是一个复杂的领域,充满了低谷和波动、嵌套依赖,更适合被建模为一个连续的光谱,而非一个二元空间。

尽管存在这些复杂性,强化学习包括一系列数学技术,这些技术通过调整代理、环境和奖励的行为本体来建模人工智能。正如我们下面将看到的,强化学习的某些方面源于控制理论,而控制理论的前身扩展到物理学和工程学,另一些方面则直接源自心理学和生物学。由于控制理论的对象和生物系统都是动力学系统,必须保持在远离热力学平衡的最佳范围内,因此其基本原理适用于强化学习和人工智能的更广泛目标。

动态规划主要源自控制理论,是一种数学优化方法,它使得较大的问题可以递归地分解为子问题,从而解决更大的问题。一般来说,递归指的是一个函数直接或间接地将自身作为参数传递。

在本文中,我们将主要集中讨论动态规划的元素,重点讨论离散和有限博弈。然而,动态规划表现出许多限制,这些限制部分通过强化学习的无模型方法得到解决,另一些则通过将动态规划与人工神经网络结合来解决,这种方法曾被称为神经动态规划。更广泛地说,强化学习和人工神经网络的结合被称为深度强化学习。这些模型结合了深度学习在强化学习技术中的优势。这些算法中最流行的是深度 Q 网络(DQN),该算法由DeepMind在 2013 年提出。这个算法家族利用深度学习来逼近 Q 函数。由于函数逼近是强化学习的一个不足之处,这些算法代表了强化学习范式的一大进步。

DQN所解决的其他不足之处包括:在捕捉非线性动态方面提供灵活性;能够接受更广泛的维度范围,而不会因维度灾难而变得在计算上无法处理;以及在环境中的更强泛化能力。

神经动态规划代表了在心理学中利用认知范式的方向,以解决纯行为主义方法的不足之处。然而,值得注意的是,尽管在理解较低层次感知信息的层次结构和处理方面取得了科学进展,但将这些信息与思维和意识的构架连接起来,仍然在某种程度上科学上难以捉摸。因此,人工神经网络(ANNs)尚缺乏人类智能那种复杂的泛化能力,人类智能往往能在比 ANNs 少得多的样本下进行指数级的学习。我们将在文章的最后一部分讨论采用强化学习原则对人工通用智能(AGI)的影响。

决策理论与控制理论

在深入探讨动态规划和强化学习的数学元素之前,重要的是要阐明决策理论的哲学和数学分支与强化学习之间的关系。决策理论主要是理性选择理论的数学形式化,它们与强化学习的目标存在重叠,因为强化学习旨在将其模型构建成能够与复杂环境和信息领域互动的成功人工智能代理。

决策理论,也称为选择理论,是在 20 世纪随着工具理性形式化的发展而产生的。具体而言,它使用概率论来量化在给定偏好下,智能体行为的发生概率。这个形式化努力的一个重要成果是冯·诺依曼-摩根斯坦效用理论。简而言之,该程序表明,智能体倾向于选择那些在可选行为中最大化效用的行动。

控制理论起源于机械和电气工程领域,旨在优化动态系统的状态和性能,使其相对于期望的参数达到最佳状态,例如保持一定的稳态温度范围。其基本机制包括一个控制器,该控制器测量期望的变量,并将其与设定点进行比较,二者的差值作为反馈用于修正。控制理论的广义框架与生物体的新陈代谢过程相似,生物体通过调节内部温度来应对外部环境的变化。控制理论与决策理论的联系显而易见:两者都依赖于来自环境的反馈,以维持或推动系统状态向某种最优状态发展。

在数学上,控制和决策问题的子集可以归结为通过动态规划求解的优化问题。动态规划通过将这些问题分解为更小的子问题,并计算价值函数,从而解决一般的随机最优控制问题(受到维度灾难的困扰——这意味着随着状态变量数量的增加,计算需求呈指数级增长)。在我们演示强化学习的基本原理时,将深入探讨动态规划的核心:智能体的状态函数与价值函数之间的递归关系。

强化学习与决策理论在定义最大化奖励或效用的程序时有重叠。然而,尽管效用在决策理论中被明确定义,该理论旨在模拟经济行为,而在强化学习中,效用被累积奖励所取代。相对于不同任务目标,可以应用不同的策略来最大化累积奖励,而累积奖励的最大化取决于探索与开发之间的反向关系,这一关系被称为探索-开发困境

让我们首先概述强化学习模型背后的本体论。

状态、行动与奖励

强化学习利用决策理论的理论框架,构建由代理、环境和动态演化规则组成的模型。演化规则允许代理在其环境内追求奖励,也称为观察

代理被定义为从环境到决策的输出。我们称某个特定的决策为动作。当前网络状态到动作的映射称为策略。策略指导动作,从状态到结果的映射。

因此,从形式上讲,策略是一个将状态映射到动作的函数。它可以通过给定当前状态的条件概率来表示,希腊字母𝛑代表策略:

转移动态定义了给定输入奖励时下一个状态的概率分布,涵盖所有可能的状态和奖励值:

上述公式定义了下一个状态和奖励对的概率,等于给定当前状态s和动作a时,下一个状态s'和奖励r的条件概率。

一个动作通过获得奖励来改变环境。奖励反过来会改变代理状态观察。奖励输入基于策略决定未来的动作输出。

通常,策略有两种类型:

确定性: 给定当前状态/环境,代理只能采取一个唯一的动作。

随机性: 给定当前状态/环境,代理可以采取多种不同的动作。

奖励通常被形式化为一个标量值 x。

给定一个特定的奖励,代理面临一个优化困境:代理应该最大化短期奖励,还是在其整个生命历史中最大化累计奖励

这被称为探索-利用困境。换句话说,转移函数应当旨在优化探索环境与利用已积累知识之间的权衡,从而获得最大的奖励。

解决探索-利用困境的最优方案取决于我们希望模型学习的任务类型,这些任务的范围从有限到未定义(连续或离散的无限)。例如,国际象棋可以被形式化为一个阶段性任务,因为它有一个有限的配置空间,并且具有预定义的三种可能结果的终局:胜利、失败、平局。这意味着,在当前状态下,最优的后继状态可以通过确定性转移动态计算出来,其中每个状态对应一个唯一的最优动作。

然而,大多数任务没有有限的配置空间,也没有预定义的终止状态。我们将这些任务归类为连续任务,并通过模型无关的方法进行优化。在模型无关的方法中,代替计算转移动态,模型从环境中采样以计算最优的后继状态。换句话说,模型不是通过预测规划行动,而是通过试错法来学习环境。

一般来说,模型无关的强化学习有两种方法:蒙特卡罗方法时序差分学习。由于足够样本的平均值会收敛到期望值,模型无关的方法通过样本均值来估计期望值。蒙特卡罗方法通过估计足够大样本的状态-动作对的预期累积回报来计算价值函数。一些蒙特卡罗方法只在任务结束时评估价值函数,适用于阶段性任务。对于连续任务,阶段的定义有所不同,可以由设计者设置,例如基于时间间隔来定义。

与蒙特卡罗搜索不同,时序差分学习则通过利用时间步之间的差异来逐步估计价值函数。由于时序差分方法的增量式特点,它们在与实际期望值的偏差上,比依赖于样本均值的蒙特卡罗方法具有更低的方差。

总结智能体通过当前状态和动作空间对到状态空间的映射来导航其环境。转移动态计算所有可能的映射,适用于具有预定义终止状态的有限配置空间。没有预定义终止状态和有限状态空间时,模型无关的方法则通过持续采样环境来寻找最佳策略。

动态规划通过所有状态-动作对计算状态转移概率和预期奖励。要理解这一过程是如何工作的,我们需要理解马尔可夫过程。

接下来我们将学习使得智能体能够计算最优后继状态的数学模型。正如我们之前讨论的,最优性问题转化为探索-开发困境,这一困境随着我们尝试建模的任务类型而变化。深入了解奖励结构将帮助我们更好地理解这一点。

奖励量化

在强化学习中,我们通过一个标量值来量化奖励,这个值是智能体在采取某个动作后从环境中获得的。这个奖励的值表示该动作相对于其最终目标的即时好坏。

另一方面,累计奖励或 回报,指的是从环境中至今累计的所有奖励之和。代理的目标不仅仅是优化即时奖励,而是优化累计奖励。前者代表的是目光短浅的代理,追求短期利益,而后者则是目光远大的代理,寻求最大化长期收益。

由于大多数情况下我们希望代理尽早最大化奖励而不是迟些时候,因此引入折扣因子来激励当前的最大奖励而非未来的最大奖励。

我们通过以下公式量化带有折扣的累计奖励 G:

这里,累计奖励 G 等于奖励与折扣因子 gamma 𝜸 的乘积之和,gamma 的值始终介于 0 和 1 之间:{0,1}。gamma 会随着每个时间步的增加逐渐递增,这意味着在无限时间步中,gamma 会趋近于零。

当 gamma 趋近于 0 时,它激励短期收益,而当 gamma 趋近于 1 时,它激励长期收益,因为在无限次迭代中,奖励总和将趋近于无穷大。

因为大多数任务是有时间限制的,当 gamma 小于 1 时,折扣因子会对奖励施加上限。

下述是带有折扣因子的累计奖励的简化公式,其中 G 代表期望奖励的总和 R,该奖励乘以折扣因子 gamma。因此,累计奖励是奖励和折扣因子的总和:

马尔可夫决策过程 (MDP)

到目前为止,我们讨论了策略的概率定义,即从一个状态到一个动作的映射,转移动态是指在给定奖励的情况下从一个状态转移到另一个状态的概率,以及奖励计算的公式。

现在,我们将稍微退后一步,提供一些补充理论,定义这些概率转移链。我们将从一种被称为 马尔可夫过程 的东西开始。马尔可夫过程是满足 马尔可夫性质 的随机过程。随机过程是指随机变化的过程。马尔可夫性质表明,对于每个状态,后继状态仅由当前状态决定

由于先前的状态不会影响未来的状态,满足马尔可夫性质的过程被称为无记忆过程。想象一下,每天你从家里出发去上班,然后再回到家里,这是一组固定的目的地。换句话说,我们有一个有始有终的循环过程。现在再进一步想象,你从一个目的地到下一个目的地的决策仅依赖于你当前的目的地,而不考虑你之前的目的地历史。最初,每个连接的目的地都有相等的概率分布。例如,如果你离开家时可以选择开车或乘坐地铁,我们会给这两种可能的未来状态分别赋予初始概率 0.5。在所有可能的路线迭代中,这些概率可能会稳定为某种频率分布,其中某些路线相较其他路线会偏向于发生。(这种类型的概率叫做经验概率,因为它通过相对于有限次数测试的可能事件的结果平均来计算)该分布的平衡状态将是马尔可夫链或过程。

现在你可能在想:如何定义事件和状态?难道这个世界不会因为过于复杂而无法讨论固定的可能状态和稳定的概率分布吗?

没错,但由于我们正在追求一个关于代理与环境的数学形式化,因此我们需要区分我们要建模的任务或环境的类型。为此,我们需要明确时间步状态空间的表示,即所有可能状态的分布。下方的方阵定义了关于状态空间时间轴的马尔可夫链:

状态空间可以定义为可数/有限或连续,其中有限状态空间通过组合数学描述系统的所有可能配置,而连续状态空间通过连续函数描述所有可能的配置。

有限和可数无限空间将整数或有理数作为其可测空间,而连续空间则取实数。

同样,时间轴也可以定义为离散或连续的。

离散时间过程将相变视为不连续的,但可以在可数或不可数状态空间上建模,其中不可数是指实数的无限小数扩展。实际上,你的计算机就是这样计算时间的——它以离散步骤来进行时间计算。步骤之间的间隔因架构而异,但一个周期通常被衡量为改变寄存器状态所需的时间步长。

连续时间链将相变视为连续的,并且可以在可数或不可数状态空间上建模。

马尔可夫过程一词通常用于描述连续时间过程,而马尔可夫链一词描述的是其中的一个子集:离散时间的随机控制过程。在本文的其余部分,我们将重点讨论离散时间,有限状态空间

到目前为止,我们的马尔可夫链非常简化,它们仅描述了具有固定概率的状态转移。我们还缺少两个在建模行为时至关重要的要素:行动奖励

引入奖励到转移概率构成了马尔可夫奖励过程。马尔可夫奖励过程为每个转移状态分配一个奖励(定义为正整数或负整数),从而推动系统向某个期望的状态移动。回想我们的累积奖励公式,它是期望奖励乘以某个折扣因子的总和。马尔可夫奖励过程允许我们计算状态v(s)的值,即给定初始状态 S,累积奖励 G 的概率(其中 G 是通过大量迭代的样本平均得到的):

为了支撑到马尔可夫决策过程,我们需要引入的最后一个变量是行动。智能体首先以一组可能行动的均匀分布概率开始,然后根据当前状态和行动更新转移函数,将其映射到下一个状态和奖励。我们已经回到了之前描述的转移动态:

动态规划与贝尔曼最优性

这引出了动态规划的概念,该概念由贝尔曼(1957 年)提出。

理解动态规划将帮助我们理解像蒙特卡洛搜索和时间差分等近似方法,这些方法不像动态规划那样需要完全的环境知识。这些无模型的方法通过代替完美的信息来近似动态规划的确定性策略。因此,它们提供了强大的机制来近似现实世界的学习。

动态规划如何搜索并找到最优智能体状态的核心思想涉及到状态值函数行动值函数之间的关系。这些函数是递归相关的。

让我们通过一个容易理解的例子来阐述这些概念。假设你目前处于生活中的一个次优状态,并且希望改变这种情况。进一步假设你有一个切实可行的目标或一个希望在某个现实时间范围内到达的地方。为了实现这一宏大目标(你可以在这里替换任何目标:更好的工作、建立家庭等),你需要采取一系列较小的步骤或行动,这些步骤或行动将有助于实现你想要的结果。用强化学习的语言来说,你当前的状态将被赋予一个值。根据你当前的状态和价值,你将采取行动。这些行动也将根据你整体目标和当前状态进行评估。一项好的行动将获得比不好的行动更高的评价。来自环境的反馈将决定行动的价值(这些如何确定取决于任务)。状态的评估将影响可用行动和后继状态的估值。行动的评估将递归地影响当前状态的价值。换句话说,行动和状态通过递归动态地联系在一起。

现在,在现实生活中,你的目标和实现该目标的行动步骤不能被指定为一个具有离散时间步骤和离散状态空间的确定性系统(尽管它们或许可以通过这种方式进行近似)。相反,动态规划假设一个可指定的环境,就像国际象棋游戏一样,其中时间步骤和行动空间被抽象为离散和有限的。与现实生活的重叠之处在于,一个更大的目标将通过优化有助于实现该目标的较小子目标来接近。

因此,动态规划将假设以下值:(Ω,A,𝒫),其中Ω表示所有可能状态的总和,A是作为有限样本空间子集的行动事件,P是由某些策略函数𝝅为每个行动事件分配的概率。

现在,如果你回想一下我们的确定性转移动态,由于状态、行动和奖励的集合是有限的,任何特定的状态和奖励对将在给定某些先前的状态和行动对的情况下,具有发生这些值的概率。由于状态空间是离散的,这些概率被指定为随机变量的离散概率分布。我们说过,由状态、行动和奖励组成的序列是马尔可夫决策过程(MDP),它寻求在时间上最大化期望的累积奖励,其中奖励被表示为一个标量值。

现在我们需要解决的问题是,给定我们已指定的假设,马尔可夫决策过程如何最大化累积奖励?答案由贝尔曼最优性方程提供,它涉及两个函数:状态值函数行动值函数

状态值函数

状态值函数可以定义为智能体在遵循策略𝝅的情况下,所有可能的动作的概率之和,其中,对于每个动作,其值由所有可能后继状态的加权值之和决定。

简单来说,状态值函数定义了智能体从特定状态(s)开始,遵循策略𝝅时可以获得的期望累积奖励。

状态值函数

上述方程包含两个项:a) 智能体在状态(s)下遵循策略𝝅时,所有可能动作的概率之和,以及b) 对于每个可能动作的内层求和,计算所有可能后继状态的加权值。方括号内的项计算了每个动作的可能状态的贡献,它是即时奖励 R(s, a, s')与折扣奖励(由折扣因子𝛾计算)之和。

表达状态值函数的另一种方式是以下:

来源:Sutton

上述公式定义了下一个状态的值,即期望回报 E𝝅,它是根据时间t时的状态s来计算获得奖励R的条件概率。奖励 R 被计算为后继状态中期望回报的乘积和,再乘以折扣因子𝛾。

为了更好地理解这一点,可以想象一个智能体处在一个 3x3 的网格世界中,每个时间步都有四个可能的动作——上、下、右、左——可供选择。

状态空间,其中的值代表奖励。

我们将状态值初始化为 0,并使用贝尔曼方程来优化状态值,依据网格中奖励的分布。我们使用(行, 列)索引来标识网格中的每个位置。

优化前的初始化状态值。

假设策略在每个动作上均匀分布,并且折扣因子为 0.9,则初始状态(1,1)的状态值函数将按照以下方式计算:

状态(1,1)的值

每个内层求和中的常数代表根据我们希望智能体实现的某些结果,分配到网格中的奖励。内层求和表示即时奖励,加上折扣因子与下一个状态累积值的乘积。外层求和中的比率代表在给定动作数量的情况下,总概率的分布。由于有四个可能的动作,我们可以最初通过均匀分布的概率加权内层求和,使其总和等于总概率。然后,将为状态空间中的每个可能状态计算状态值,并反复迭代,直到求和收敛到稳定值。

动作值函数

正如我们所看到的,动作值函数嵌套在状态值函数中,作为其第二项。这意味着,动作值函数通过以下公式计算状态 (s) 下所有可能动作的值,作为从 (s)(s’) 转移的即时奖励和在给定动作下,下一状态 (s’) 的期望累计奖励的总和:

换句话说,动作值函数计算在状态 (s) 下采取动作 a 的累计奖励,其中期望回报是即时状态转移的总和——用 R(s, a, s') 表示——以及下一个状态 s' 的累计奖励的折扣值——用 𝛾∑𝝅(a’|s’)Q(s’,a’) 表示。

另一种表达动作值函数的符号是基于给定状态和动作对 (s, a) 的期望回报 E,当遵循最优策略 𝝅 时:

状态值函数和动作值函数之间有关系,具体而言,状态值函数可以通过策略和动作值函数 Q(s,a) 给出。

因此,每个函数都将自身作为参数,尽管计算的是后继过渡状态,如上面的公式所示。V(s) 的公式包含 V(s'),Q(s, a) 的公式包含 Q(s', a)。

换句话说,它们在各自的参数中包含了彼此:状态值 V(s) 依赖于通过 Q(s,a) 计算的后继状态的值,而动作值 Q(s,a) 依赖于通过 V(s') 计算的后继状态的值。

状态值函数的备份图。S 代表状态,𝝅 代表策略,黑点表示每个可用动作,箭头表示动作-奖励对,指向下一个状态 s'。来源:goodboychan.github.io/reinforcement_learning/2020/06/06/05-Policy-evaluation.html

因此,动作值函数和状态值函数是递归相关的:动作-状态对的值决定了状态的值,而状态的值又决定了动作的值。

状态值函数以状态为先验,并输出一个期望值 E。动作值函数以状态和动作对为先验,用来计算奖励和期望的累计回报 E。

贝尔曼最优方程因此表达了状态值函数和动作值函数的递归迭代,直到它们收敛到最优值。状态值函数的贝尔曼方程如下所示:

当前状态的值被定义为任何可能行动的最大奖励,该奖励是采取动作a在状态(s)下的奖励,并且是下一个动作 s’的值与其折扣因子 gamma 的乘积。

贝尔曼方程平均了当前状态下所有可能动作的期望,并根据其发生的概率对它们进行加权。

无模型方法:蒙特卡洛与时序差分

上述例子描述了一个确定性模型,其中转换动态是已知的,因此可以完美计算。这是因为我们对环境有完全的了解。

然而,对于大多数任务,我们无法完全了解环境。在缺乏这些信息的情况下,我们不能精确地使用确定性的转换动态,因为我们无法求解动态规划方程。为了解决这个问题,我们可以使用借鉴统计学的技术,通过从样本中推断环境的状态。

在蒙特卡洛方法中,我们通过样本回报的平均值来逼近期望回报。随着样本数趋近于无限,平均回报将收敛于期望回报的真实值。我们通过让智能体完成一个完整的回合直至结束,然后计算价值函数来实现这一点。接着,我们采样N个回合,并使用均值来逼近目标状态的期望值。现在,正如你可能已经想到了,回合的定义会根据任务和模型的目的有所不同。例如,在国际象棋游戏中,我们可以将一个回合定义为完成一整局棋或者是一个任意的步骤序列。

我们可以将蒙特卡洛更新规则写成如下:

其中 V(s) n+1 表示下一个回合的值,S(s)n 表示状态的累计值,G 表示奖励的值。我们将累计奖励 G 添加到状态值中,并除以回合数或样本数。

我们可以通过代数方式重新排列蒙特卡洛更新规则为:

与蒙特卡洛方法不同,在蒙特卡洛方法中,我们仅在每一回合结束后评估价值函数,而在时序差分(TD)方法中,我们在每一步或增量后评估状态价值函数。由于我们一开始对环境没有任何信息,因此需要将 V(s)的初始值设置为 0 或其他值,这些值将在每个时间步之后更新。

我们通过两步计算 TD 中的状态值。首先,我们计算这一时间步的误差,然后使用更新规则来改变状态的值。误差通过以下差值公式给出:

时序差分时间步的误差公式。

其中,𝜹t 代表误差,R(t+1)是动作带来的奖励,V(S t+1)是下一个状态的估计值,V(S)是当前状态的值。TD 使用下一个状态的估计值来评估当前状态,这种方法称为自引导(bootstrapping)。实际上,我们从动作的奖励和折扣因子与下一个状态值的乘积中减去当前状态的值。这使得每个时间步都能立即更新当前状态的值。

通过将预期奖励与观察到的奖励之间的差异𝜹乘以𝛼(学习率),我们消除了观察与预期之间的差异:

价值函数的 TD 更新规则。

𝛼的作用决定了 TD 算法学习的程度,其中𝛼是一个实数正数。通常,𝛼会设置为[0.1,0.01,0.001]等值。较高的𝛼确保更新更加激进,而较低的𝛼确保更新更加保守。𝛼的值会影响探索与利用的权衡,较高的𝛼倾向于探索,而较低的𝛼则倾向于利用。

虽然 MC 和 TD 方法都在没有任何先验环境知识的情况下盲目进行,但时间差分法(Temporal Difference, TD)的方法优点在于它在每个时间步都进行在线更新,而蒙特卡罗方法(Monte Carlo, MC)的方法优点在于由于仅依赖采样来估计值,它的估计是无偏的。TD 方法的一个缺点是高偏差,而 MC 方法的缺点是忽视了重要的更新,从而导致较高的方差。这表明,两种学习策略之间一定存在一个最优解。

通过将单步评估策略改为 n 步评估,TD 方法可以得到优化。正如我们将要看到的,这样做可以在 TD 和 MC 之间达成一个折衷。当我们每 n 步评估一次状态值时,我们是通过估计未来的 n 步来进行评估,而不是在每一步后进行评估。

对 n 步 TD 的修改方法是 TD(𝝀)。TD(𝝀)方法使用一个叫做资格迹(eligibility traces)的参数来对过去发生的状态-动作对进行加权。与其估计未来的 n 步,资格迹通过多个 TD 步骤来对状态-动作对进行加权。资格迹使得过去的状态-动作对能够因其对观察到的奖励过渡作出贡献而获得奖励。资格迹表示为与每个状态-动作对相关的向量或矩阵。时间步的资格迹通过递归方式计算如下:

其中,lambda 𝝀 参数控制自举的程度。当 𝝀 = 1 时,消除了自举,更新规则退化为蒙特卡罗方法。当 𝝀 = 0 时,它退化为带有自举的 TD 时间步,称为 TD(0)。TD(𝝀) 将 TD 和蒙特卡罗方法进行了推广,形成一个连续体,其中 TD(0) 表示单步 TD,TD(1) 表示将 TD 扩展到无限步数的极限,这一极限会退化为蒙特卡罗方法。 从公式中可以看出,资格迹参数是递归计算的,其中下一时间步的资格迹值以前一时间步的资格迹为输入。当 E(s) = 0 时,自举被消除。TD(𝝀) 更新规则的计算方式与 TD 和蒙特卡罗方法相同,只是将资格迹乘以误差,如下所示:

使用人工神经网络增强强化学习

无论是基于模型的还是无模型的,强化学习算法都会遇到由于维度灾难而产生的扩展问题,难以在不同类型的环境之间进行泛化,并且遭遇采样低效的问题。

人工神经网络(ANNs)为解决强化学习架构中一些固有的局限性提供了强有力的方法。特别是,ANNs 改善了采样效率、环境泛化能力以及由于维度灾难而产生的扩展问题。由于它们通过从数据中学习一个通用函数,ANNs 通过卓越的泛化能力减少了采样低效性。这还使得它们能够更好地扩展,因为可以增加隐藏层的数量和每个隐藏层中的神经元数量。然而,隐藏层和神经元过多也可能导致计算扩展问题(维度灾难在某些范围内是无法避免的)。此外,它们还面临着目标状态的非平稳性问题,因为传统的人工神经网络需要预先设定真实值(在强化学习中,这相当于期望回报),而强化学习算法则通过更新函数找到最优状态,无论是基于策略还是脱离策略。

与传统的强化学习算法依赖于概率转移规则不同,人工神经网络(ANNs)在强化学习中的应用使用函数逼近来计算状态和值动作值。虽然可以应用多种函数逼近方法,如线性逼近和格编码,但人工神经网络由于其通过非线性函数逼近的泛化能力,成为最强大的技术。

让我们看一下两种将人工神经网络应用于强化学习的方法:深度 Q 学习(DQN)带有资格迹的深度时间差分学习(TD(𝝀))。由于我们无法提前知道目标值,因此使用 MC 或 TD 来创建目标状态的估计值:期望回报。然后将其作为目标值,通过函数(实际上是误差的偏导数,即整个网络误差对网络参数𝜃的偏导数)来逼近。ANN 通过计算目标估计值与输出之间的误差,接着通过反向传播计算误差,并通过优化算法减少该误差来逼近目标值。最常见的优化算法是梯度下降的变种,例如随机梯度下降

在 DQN 中,人工神经网络以状态向量作为输入,输出一个动作向量,其中每个值代表动作的 q 值。

离策略 DQN

Q 学习SARSA(状态、动作、奖励、状态'、动作')的离策略版本,其中下一状态-动作对Q(s’, a’)通过选择下一状态中最大估计值来估算。换句话说,Q 学习选择下一状态 s’中所有可能动作的最大Q(s’,a’)值。这意味着它不使用策略𝛑来学习 Q(s’,a’)。而 SARSA 则是一种在线策略方法,通过选择先前采取的动作以及下一状态-动作对的估算值 Q(s’,a’)来确定动作。这意味着它使用策略𝛑,即给定状态下的动作概率,来学习 Q 函数。

深度 Q 学习中,动作-价值函数 Q(a, s)通过 Q(a,s, 𝜃 )表示,其中𝜃代表神经网络参数。Theta 𝜃参数等同于神经网络中的权重w,这些权重与神经元之间的连接相关。权重决定了连接的强度,并通过反向传播来调整,以最小化误差。DQN 以环境的高维表示为输入,并输出每个可能动作的动作-价值向量。期望回报通常通过 MC 或 TD 方法进行逼近。然后使用带优化函数的反向传播计算策略梯度,并通过调整策略网络参数𝜃来减少误差。

由于人工神经网络(ANNs)对新信息非常敏感,这可能导致灾难性遗忘,即新信息可能会覆盖先前写入的信息。管理灾难性遗忘的一种方法是采用经验回放,这是一种存储过去经验并重复使用它们来训练网络的技术。

策略梯度深度 TD(𝝀)

人工神经网络(ANNs)也可以应用于TD(λ)方法,其中状态观察作为输入传递给 ANN,后者将动作值函数近似为输出。由于TD(λ)方法的政策性,深度TD(λ)方法最适合于那些需要长期状态依赖的任务。

训练在线学习方法如TD(λ)可能具有挑战性,因为环境的分布会因每次或每 n 步的引导而发生变化。这被称为非平稳性,它会阻碍 ANN 参数𝜃向最优值的收敛。在线学习中后续状态的相互依赖性可能会导致灾难性遗忘,即更新会干扰过去的学习。此外,将归因迹与 ANN 结合使用,赋予过去动作的奖励,可能会在反向传播步骤中引发额外的复杂性。

应对这些挑战的一种方法是利用一种叫做经验回放的技术。经验回放将智能体学习到的经历存储为[s, a, r, s']向量,保存在一个记忆缓冲区中。在训练过程中,网络从其记忆缓冲区中采样存储的学习向量,以更新网络参数。这为网络提供了更大的稳定性,使其不易受到来自新经验的高方差带来的灾难性干扰,这些新经验会导致步骤之间的更大误差或时间差异。

深度TD(λ)算法已被证明在状态空间连续且目标未知或不明确的连续控制任务中表现优异。这些任务包括机器人学、自动驾驶汽车和金融市场中的连续控制任务。

强化学习与人工通用智能

强化学习对人工通用智能的启示是什么?

尽管“智能”是一个不够明确的变量,因为它将不同的能力融合为一个单一的概念,但所谓的“通用智能”建立在生物体进化出的能力之上,这些能力需要转化世界信息以确保生存和繁殖。即便在人的语境中,智能也无法脱离生物体生存能力的框架。然而,这并不是主流观点。普遍的看法认为,智能更像是一个程序或软件,根据可用信息进行推理计算。

后者的概念包含了两个模型,通常被误认为是相互竞争的。一个模型将智能描述为遵循程序,而另一个则将智能描述为从数据中概括以进行最优预测。前者通常被更好地理解,而后者则是一组可以可靠地提高预测能力的技术。动物智能在很大程度上基于后者的模型。

第二种模型中最成功的范式是通过人工神经网络进行的深度学习。人工神经网络架构的主要优势在于,它们可以在没有先验信息或概念的情况下从数据中进行泛化,尽管这不能与无监督学习混淆。人工神经网络首先通过训练建立模型,然后根据该模型对新数据进行预测。因此,人们认为,大脑做的事情与此相似(在考虑进化前训练后)。然而,当前人工神经网络存在两个弱点。第一个弱点是,目标或结果必须由人类设计者设定。人工神经网络不能自行构想目标。更进一步,它不能自行区分真假。人类设计者必须提供真实的结果,以便模型学习接近该结果。第二个弱点是,如果没有强化学习,人工神经网络无法在环境中进行搜索,以优化自身状态。因此,人工神经网络的泛化和预测能力与强化学习的决策优化能力相结合,形成了强大的融合。

基于这一点,有人认为强化学习代表了通向人工通用智能的最清晰路径(Sutton,2014)。其直观性很明显:强化学习最接近于模拟生物系统,当与其他成功的架构(如变换器)结合时,可能会导致一种能够复制(并超越!)所有人类能力的人工智能模型。

然而,如果人类是通用智能的基础,那么通用智能的概念就不能脱离生存约束和某种形式的体现。另一方面,如果通用智能可以不依赖于生物体来定义,那么它将是什么样子就不清楚了——纯粹的抽象模型尽管有像 Marcus Hutter 的AIXI这样的尝试,仍然逃脱了令人满意的形式化。从抽象上来看,可以构想出一些通过推理和计算能力单独解决问题的完美理性代理。信息与体现之间的分裂是一个更广泛讨论的筹码,超出了本文的范围。如果感兴趣,这篇论文提供了一个很好的起点。

然而,有充分的理由怀疑强化学习是否足以实现人工通用智能。其原因之一与通用智能的定义本身有关。大多数现有的人工智能研究者仍然依赖于行为主义的智能观念,而没有将显式的内部表征作为必要的组成部分来考虑。并且,他们有充分的理由这么认为。在深度学习成功之前,象征性人工智能曾是人们寄希望于实现通用人工智能的方向,但结果证明它是失败的。象征性人工智能指的是主要依靠显式编码的逻辑规则和知识库来进行最优推理生成的人工智能方法。

然而,象征性人工智能与神经网络之间的紧张关系可能是没有根据的。许多研究者认为,人工通用智能的追求在于以正确的方式结合这些方法。认为神经网络接近大脑固有本体论的原因之一是,数学逻辑并不是大脑推理的方式:也就是说,大脑并不像计算必要和充分条件或清晰的成员资格那样推理,而是更倾向于推理渐进的成员资格,这一点通过模糊逻辑等方式进行了逼近,而人工神经网络(ANNs)在这方面表现得尤为出色。

神经网络由一个黑盒式的层次结构组成,隐藏层的参数经过精确调校,以通过高度校准的动态学习率、激活函数、连接权重和优化算法来实现期望的输出,并努力最小化误差。除了上述高度调校的超参数外,人类设计者并不理解隐藏层中信息是如何处理的。假设大脑也是如此,其中信息不是作为离散表征单元的组合(无论是模拟的还是形象的)存储的,而是作为由数十亿个神经元组成的庞大分布式架构存储的。我们所认为的语言结构化思维在大脑中并不是以这种方式表示的:例如,并没有特定的神经元组合来代表词语being或句子“作为决定性存在的存在,本质上是为他者的存在”。

语言能力则嵌入在一个庞大的语义连接网络和通过经验强化的再现规则中,并通过形象和模拟表征加以增强。换句话说,我们所反思性地表示的语言和思维(也包括通过书写和言语表现出的行为)在大脑中并没有镜像其显式结构的脑部类比(换句话说,大脑的本体论与语法之间没有同构映射),而是嵌入在由神经组群构成的分布式网络中,这些网络的特征是连接度和连接强度的不同。

另一方面,似乎神经网络无法实现一些人认为是理性和人类智慧根源的结构化思维过程。毕竟,明确的推理构成了人类智力成就的主要方式,而这似乎不是当前神经网络能够复制的内容。一个显著的例子来自哥德尔的不完备性定理,其中一个形式系统单独无法仅凭证明确立某些陈述的真理。(如果有兴趣,可以查看我写的这篇文章,解释了哥德尔的证明)。与此同时,人类主体尽管公理推导失败,依然能够验证这样的陈述的真理。撇开这种真理与证明脱钩对计算的复杂且有争议的含义不谈,值得注意的是,人类主动追求关于世界的理论,而当前的强化学习算法在某种非常基础的层面上也能做到这一点,尽管机器人技术最终可能会朝着类似的能力发展。与此同时,语言学的前沿技术——大型语言模型(LLMs),在被提示时会复述出与人类言语和写作在语言上无法区分的类似物,同时展示出指数级更快的回忆速度和信息存储量,数量级远远更大。理解这一区别至关重要:人类作为文化编程的一部分,积极追求关于世界的理论以及其他创造性活动,而这些活动使用的是为生存和繁衍成功量身定制的机制。换句话说,所有人类活动都发生在进化约束的框架内。因此,人类和所有生物都构成了自主系统,复制并内生地再生产自身的身份条件。人类和动物的智慧因此与生存的边界条件密不可分,除非存在某种形式的文化独立性,能够脱离严格的适应主义(这是一个引发广泛争议的大话题)。

目前的人工智能并未接近那些能够自我驱动、在世界中自主推进的系统。它们也不会像人类和其他动物那样生成自身的环境,并重新配置自身的搜索空间。目前,缺乏这一约束使得人工设计者可以设定人工智能的信息显著性,例如文本生成、环境检测等。即使架构发展成为一个真正的通用问题解决机器,除非它能够具备反思意识,否则无法说它拥有通用智能。通用智能的定义通常省略了全球意识这一变量——这等同于古希腊人所称的nous——作为人类智能的标志。之所以如此,是因为反思性和全球意识仍然难以逆向工程并拆解为部分。正因如此,反思性意识常常被忽视为智能的一部分。然而,承认当前科学解释的困难,并不意味着拒绝物理主义或支持非自然主义。相反,这表明我们尚未理解这一点。鉴于这一理解的空白,我假设反思性意识是感知的延伸,而感知是生物有机体的基本属性。在提出这一观点时,我并不意味着自主系统不能通过自然选择以外的手段进行工程设计,尽管我留有开放的可能性,认为它们可能在可预见的未来仍然对科学分析保持不透明。如果强化学习希望发展为通用智能,那么智能体应该先具备一个强大的架构,不仅能承载世界的复杂表征,还能从这些表征的内部维持全球视野。这意味着,尽管模型与世界的交互性对任务至关重要,但原生架构将需要一个复杂的分层内部结构,具备多模态信息处理和整合的能力。

精选参考文献

Mnih, V., Kavukcuoglu, K., Silver, D., Rusu, A. A., Veness, J., Bellemare, M. G., Graves, A., Riedmiller, M., Fidjeland, A. K., Ostrovski, G., Petersen, S., Beattie, C., Sadik, A., Antonoglou, I., King, H., Kumaran, D., Wierstra, D., Legg, S., & Hassabis, D. (2015). 通过深度强化学习实现人类级控制. Nature, 518(7540), 529–533. doi.org/10.1038/nature14236

Neftci, E. O., & Averbeck, B. B. (2019 年 3 月 4 日). 人工和生物系统中的强化学习. Nature News. www.nature.com/articles/s42256-019-0025-4

Sharma, S. (2024 年 3 月 7 日). 学习混合𝑛步回报:将𝜆-回报推广到深度强化学习. Ar5iv. ar5iv.labs.arxiv.org/html/1705.07445

Sanghi, Nimish. 使用 Python 进行深度强化学习:包括 PYTORCH、Tensorflow 和 Openai Gym。Apress,2021 年。

Silver, D., Singh, S., Precup, D., & Sutton, R. S. (2021 年)。奖励就足够了。人工智能299,103535. doi.org/10.1016/j.artint.2021.103535

Spens, E., & Burgess, N. (2024 年 1 月 19 日)。记忆构建与巩固的生成模型。《自然新闻》。 www.nature.com/articles/s41562-023-01799-z

Sutton, Richard S. 强化学习导论。MIT 出版社。

Tyng, C. M., Amin, H. U., Saad, M. N. M., & Malik, A. S. (2017 年 8 月 24 日)。情绪对学习和记忆的影响。《心理学前沿》。 www.ncbi.nlm.nih.gov/pmc/articles/PMC5573739/

White, A., Modayil, J., & Sutton, R. (2014 年)。大数据机器人中的惊讶与好奇心。人工智能促进协会,19–22。

posted @ 2025-01-09 18:53  绝不原创的飞龙  阅读(593)  评论(0)    收藏  举报