docs-merge-09
TowardsDataScience 2024 中文翻译(十)
人工智能如何加剧科学和技术中的再现性危机
通过一些具体的例子进行讨论,并勾画出如何开发更好的 AI 系统的广泛指南
LucianoSphere (Luciano Abriata, PhD)
·发表于Towards Data Science ·13 分钟阅读·2024 年 1 月 3 日
--

人工智能已成为科学研究中的一个重要工具,但人们越来越担心这些强大工具的误用正在导致科学及其技术应用中的再现性危机。让我们探讨导致这一不利影响的根本问题,这不仅适用于科学研究中的 AI,也适用于 AI 的开发和一般利用。
人工智能(Artificial Intelligence,简称 AI)已成为社会和技术中不可或缺的一部分,每个月都在医学、工程学和科学领域找到多个新应用。特别是,AI 已成为科学研究和新技术产品开发中非常重要的工具。它使研究人员能够识别数据中可能不易被人眼察觉的模式,以及其他类型的计算数据处理。所有这些无疑引发了一场革命,这场革命在许多情况下以改变游戏规则的软件解决方案的形式呈现出来。在众多例子中,一些像可以应用于的大型语言模型…
Bend 如何工作:一种“感觉像 Python,但扩展性像 CUDA”的并行编程语言
Lambda 演算、交互组合子以及它们如何用于在 Bend / HVM 上并行化操作的简要介绍。
·发表于 Towards Data Science ·阅读时间:22 分钟·2024 年 6 月 26 日
--

图片来自作者
引言
如果你正在阅读这篇文章,你可能最近听说过 Bend,一种新的编程语言,旨在实现大规模并行计算,但你无需担心像线程创建等常见的并行编程概念。
如果你不知道我在说什么,请观看下面的视频:
他们声称“它感觉像 Python,但扩展性像 CUDA”。作为并行编程的爱好者,这立刻引起了我的注意。经过一番阅读,我发现 Bend 是由 HVM(高级虚拟机)驱动的,这就是所有魔法发生的运行时环境。也就是说,在 Bend 程序中,Bend 代码会被编译成 HVM,HVM 通过一些魔法使得程序以本质上并行的方式运行。从某种意义上说,所有可以并行化的操作都被这个运行时自动并行化了。
立刻,我就想了解所有 HVM 魔法是如何发生的。如何才能做到这一切?经过一些阅读,我了解到 HVM 背后的魔法主要基于交互组合子(Interaction Combinators),这是一种基于图形和图形规则的计算模型,由 Yves Lafont 在 1990 年代开发。因此,我打开了 Lafont 的论文,翻了几页,看到了这一部分:

???? 交互组合子外星代码。图像来源于作者,灵感来自于 Lafont, 1997
我感觉就像是在电影 降临 中,外星人试图用一种奇怪的符号语言与我们交流。
就是在那时,我关闭了笔记本电脑,放弃了理解这个东西的尝试。
一段时间后,当我再次打开计算机时,那些符号依然在那里,盯着我看,仿佛它们在要求我理解它们。
经过大量阅读、观看视频和外星人的帮助后,我不知怎的开始理解这一点。
本文的目的是简要阐明 HVM 魔法是如何发生的,并通过解释一些你在学习过程中可能遇到的常见术语,帮助你进一步理解。为此,我们需要先了解一些基本概念。
λ-计算(Lambda 计算)
Lambda 计算是由阿隆佐·丘奇(Alonzo Church)在 1930 年代创建的数学逻辑中的一个形式系统。它的目的是从纯数学的角度研究逻辑理论的某些方面。丘奇的目标是用数学术语定义什么是可计算性(即什么可以通过一组基本规则来计算)。让我们开始:
你可能已经使用过 Lambda 计算了。例如,假设有一个将数字乘以二的函数:
f(x) = 2 * x
在 Python 中,你可以这样表示一个命名函数:
def multiply_by_two(x):
return 2 * x
print(multiply_by_two(2))
# 4
但你也可以使用 lambda 来表示,它本质上是一个匿名函数:
multiply_by_two_lambda = lambda x: x * 2
print(multiply_by_two_lambda(2))
# 4
所以,让我们回到数学。在 Lambda 计算中,你可以使用符号 λx.2x 来表示相同的函数,其中 x 是 参数,而 2x 是 主体。
λ
这叫做 抽象。一个抽象 λx.t 表示一个匿名函数,该函数接受一个输入变量 x 并返回 t。例如,λx.(x²+2x) 是一个抽象,表示由 f(x) = x²+2x 定义的函数。所以,抽象基本上定义了一个函数,但并没有调用它。
你也可以有一个像 λx.(x+y) 这样的项,它是 f(x) = x+y 的定义。在这里,y 还没有定义。表达式 λx.(x+y) 是一个有效的抽象,表示一个将输入加上尚未知道的 y 的函数。
如果使用 λx.2x 定义一个函数,(λx.2x)a 就是“调用”一个带有参数“a”的函数。也就是说,我们基本上是将变量“x”替换为“a”。
f(x) = 2x
f(2) = 4
这与以下等价:
λx.2x
(λx.2x)2 = 4
这叫做 应用。我们正在将 抽象 (λx.2x) 应用到数字 2 上。
你也可以将一个 lambda 表达式应用到另一个 lambda 表达式中,像是嵌套函数:
设 f(x) = 2x 和 g(x) = x³
然后你想要 g(f(x)):
你可以使用 Lambda 表达式来表示它:
λx.2x
λx.x³
=> (λx.x³)(λx.2x)
现在不要急着解决它,先理解符号的含义,稍后我会展示如何解决!
重要的是不要混淆括号。例如:
1 — λx.((λx.x)x) 是一个 抽象(函数定义)。
2 — (λx.(λx.x))x 是一个 应用(函数应用)。
在示例 1 中,我们定义了一个函数 λx.B,其中 B 是表达式 (λx.x)x,即将匿名函数 λx.x 应用于输入 x。
在示例 2 中,我们将匿名函数 λx.(λx.x) 应用到输入 x。
应用 也可以表示为 f x(将函数 f 应用到变量 x)。
我们也可以使用 Lambda 计算表示具有 n 个参数的函数。这可以通过使用嵌套函数来完成,每个函数接受一个单一参数:f(x,y,z) → λx.λy.λz
因此,f(x, y, z) = x + y + z 可以通过 抽象 表示为:
λx.λy.λz.(x + y + z)。
使用这个 抽象 我们可以构造 应用:
(λx.λy.λz.(x + y + z))1 2 3 => 6
在学习 Lambda 计算时,还有两个常见的术语你可能会遇到:
Alpha 转换(α-转换)和 Beta 约简(β-约简)
Alpha 转换
在评估更复杂的 Lambda 表达式时,你可能会得到这样的表达式:
(λx.(λx.x+x)x)
在这个表达式中,内部的 x 可能会被错误地解释为外部的 x。为了避免这种情况,我们可以重命名内部变量 x:
(λx.(λy.y+y)x)
这个过程就是所谓的 α-转换,虽然名字看起来有点复杂,但其实就是这样简单的重命名变量以避免错误。
λx.x → λy.y (α-转换)
两个表达式表示相同的函数。α-转换 并不会改变函数的行为,只是更改了变量的名称。
Beta 约简
β-约简 仅仅是通过将函数应用于表达式来计算结果的过程。例如:
(λx.xy)z
在输出 xy 时,将每个出现的 x 替换为 z
= zy
你还可能会看到以下表示法:
(λ param . output)input => output [param := input] => result
这基本上意味着要得到结果,你需要查看 输出,并将每个出现的 param 替换为 input。在之前的表达式中:
(λx.xy)z => (xy)[x := z] => zy
示例
回到我们的例子 f(x) = 2x;g(x) = x³,我们想计算 g(f(1))。
为了避免错误混淆术语,我们可以重写:
f(x) = 2x 和 g(y) = y³
然后,我们将 f 替换到 g 中:
g(f(1)) = (f(1))³
=> g(f(1)) = (2*1)³
=> 8*x = 8.
现在使用 Lambda 计算:
λx.2x
λx.x³
=> (λx.x³)((λx.2x)1)
首先,应用 α-转换 以避免混淆:
(λy.y³)((λx.2x)1)
然后,对最内层的表达式进行 β-约简 (λx.2x)1:
(λ param . output)input => output [param := input] => result
(λx.2x)1 => 2x [x := 1] => 21 = 2.*
然后,再次对得到的表达式 (λy.y³)2 进行 β-约简:
(λ param . output)input => output [param := input] => result
(λy.y³)2 => y³[y := 2] => 2³ => 8.
我们得到了相同的结果!这真是太棒了吧?
⚠️ 如果此时你开始感到困惑,请不要关闭文章!!我明白一开始可能会很具挑战性,但我保证你,今天晚上睡觉时,明天醒来时,事情会更加清晰!所以,请继续阅读,享受接下来的内容 😃
在 Lambda 计算之后的几年,艾伦·图灵提出了图灵机的概念,这是一种能够模拟任何可以用数学描述的算法过程的计算机抽象数学模型。在 Church 和 Turing 的工作基础上,已确定 Lambda 计算与图灵机之间存在理论等价性。这种等价性意味着,尽管 Lambda 计算没有数字或布尔值,但任何可以由图灵机计算的问题也可以用 Lambda 计算的术语表达。因此,我们可以用 Lambda 计算来表达任何可计算的算法!让我们了解一下如何实现这一点。
数字
我之前提到过 Lambda 计算没有数字,只有 Lambda 表达式。但那么,我怎么能写出像 λx.(x+2) 这样的东西呢?
好吧,我骗了你…… 😞
但别生气,这只是为了便于理解 😀
现在,让我们了解一下 Church 如何仅通过 Lambda 表达式表示数字:
Church 数字表示法一开始可能有点复杂,但随着深入理解,它会变得更清晰。
Church 数字 n 被定义为一个函数,它接受一个函数 f 并返回将 f 应用于其参数 n 次。
0: λf.λx.x (将 f 应用于 x 0 次)
1: λf.λx.f x (将 f 应用于 x 1 次)
2: λf.λx.f(f x) (将 f 应用于 x 2 次)
3: λf.λx.f(f(f x)) (将 f 应用于 x 3 次)
依此类推……
这似乎很混乱,但稍微思考一下后,你会开始理解的。Church 数字 n 只是意味着做某事 n 次。
一种很好的说明方式是记住数字的概念来源于计数的过程。例如,假设你有一个 20 步的楼梯。当说到爬楼梯时,你需要爬 20 步,这意味着你要爬 20 次,每次爬一步,对吧?这正是 Church 编码的概念:你有一个函数 f,它表示“爬一步”,如果你想表达 20 步的概念,你就将 f 应用 20 次。
数值运算
定义了 Church 数字之后,我们可以定义数值运算。第一个是定义一个后继函数 s。它基本上是一个将 Church 数字增加 1 的函数。因此,后继函数是一个接受表示数字 n 的 Church 数字,并返回表示 n+1 的 Church 数字的函数。
例如,如果 λf.λx.f(f x) 表示数字 2,当我们将后继函数 s 应用到它时,我们将得到 λf.λx.f(f(f x))(数字 3 的 Church 表示法)。
后继函数定义如下:
s(n) =λn.λf.λx.f((n f) x), 其中 n 是 Church 数字 n。
让我们分析一下:
-
输入:
n(一个教堂数字),f(一个函数),和x(一个参数) -
*n 的应用:术语
(nf)x表示将函数f应用到参数x上n次。 -
附加应用:术语
f((nf)x)将函数f再应用一次到(nf)x的结果。
如果教堂数字 n 表示做某事 n 次,s n 则表示做某事 n+1 次。
例如,让我们将后继函数应用于数字 1 的教堂数字表示法:
教堂数字 2:λf.λx.f(f x)
应用此表达式的后继:
我们知道 s(n) = λn.λf.λx.f((n f) x)
我们的 n = 2 = λf.λx.f(f x)
因此,我们对其应用后继函数:
s(λf.λx.f(f x)) = ( λn.λf.λx.f((n f) x) )( λf.λx.f(f x) )
对 应用 表达式进行第一次 β-约简:
(λ param . output)input => output [param := input] => result
( λn.λf.λx.f((n f) x) )( λf.λx.f(f x) ) => λf.λx.f((n f) x) [n := λf.λx.f(f x)]
=> λf.λx.f((λf.λx.f(f x)f x)
现在,让我们分析内部的 应用 表达式:
(λf.λx.f(fx)f x
下划线部分是教堂数字 2,对吧?它可以解释为:
给定一个函数 f,将 f 应用两次于其参数 x。
(λf.λx.f(fx)f x 变成 f(f x)
将我们的表达式 λf.λx.f((λf.λx.f(fx)f x), 代入后,我们得到:
λf.λx.f(f(f x)), 这正是数字 3 的教堂数字表示法!
所以,我们刚刚定义了后继 lambda 操作。通过使用这个概念,如果我们定义 0 = λf.λx.x,我们可以通过递归地应用后继函数来获得其他教堂数字:
1 = s 0
2 = s(s 0)
3 = s(s(s 0))
…
我们可以利用这些函数来实现其他操作,如加法和乘法。
两个数字 m + n 的加法定义为:
ADD(m, n) = λm.λn.λf.λx.(m f)((n f) x)
因此,如果我们将 m 和 n 定义为分别表示数字 3 和 4 的教堂数字表示法,然后应用这个加法函数,我们将得到数字 7 的教堂数字表示法。
同样的逻辑适用于两个数字 m * n 的乘法:
MUL(m, n) = λm.λn.λf.λx.m (n f)
任何时候都可以尝试应用自己!
布尔值
在进入教堂定义之前,让我们先将布尔值视为我们可以用来进行 选择 的某种操作。在两个选项 A 和 B 之间,依据某个条件,选择 A 或 B。
IF [CONDITION] THEN [RESULT A] ELSE [RESULT B]。
例如,在某个应用程序执行过程中,如果我们想使用布尔值来更改屏幕的背景颜色:
“red_theme = True”
这只有在程序的其他部分我们进行某种 选择 时才有用:
background_color = 如果 red_theme 为真,则为 red,否则为 white。
因此,我们从布尔值中需要的只是某种条件选择两个选项的方式。
基于此,在λ演算中,true和false的 Church 定义被定义为两个参数的函数:
-
true选择第一个参数。
-
false选择第二个参数。
TRUE = λx.λy.x
FALSE = λx.λy.y
看起来有点奇怪,对吧?但是让我们定义一些布尔操作,看看会发生什么:
NOT:接受一个布尔值并返回其相反值。
NOT = λp. p FALSE TRUE
这意味着:“取一个布尔函数p。将p应用到两个参数FALSE和TRUE。”
还记得 Church 编码中的布尔值定义吗?TRUE返回第一个参数,FALSE返回第二个参数?因此:
→ 如果p是TRUE,则返回FALSE。
→ 如果p是FALSE,则返回TRUE。
AND:接受两个布尔值,如果两个都为TRUE,则返回TRUE,否则返回FALSE。
AND = λp.λq.p q p
这意味着:“取两个布尔函数p和q。将p应用于q和p。”
让我们在实践中尝试:
AND TRUE FALSE = (*λp.λq.p q p) TRUE FALSE:
给定TRUE和FALSE,返回TRUE FALSE TRUE:
=> TRUE FALSE TRUE = λx.λy.x FALSE TRUE
给定FALSE和TRUE,返回第一个参数:
λx.λy.x FALSE TRUE = FALSE
其他布尔操作如 OR、XOR 等的定义遵循相同的思路。
实践
现在,让我们在实践中使用一些λ演算:
# Lambda function abstraction
def L(f):
return f
# Church numeral 0
ZERO = L(lambda f: L(lambda x: x))
# Successor function: λn.λf.λx.f (n f x)
SUCC = L(lambda n: L(lambda f: L(lambda x: f(n(f)(x)))))
# Addition: λm.λn.λf.λx.m f (n f x)
ADD = L(lambda m: L(lambda n: L(lambda f: L(lambda x: m(f)(n(f)(x))))))
# Multiplication: λm.λn.λf.m (n f)
MUL = L(lambda m: L(lambda n: L(lambda f: m(n(f)))))
# Convert integer to Church numeral
def to_church(n):
if n == 0:
return ZERO
else:
return SUCC(to_church(n - 1))
# Helper function to compare Church numerals
def church_equal(church_number_1, church_number_2):
f = lambda x: x + 1
return church_number_1(f)(0) == church_number_2(f)(0)
church_two = to_church(2)
church_three = to_church(3)
church_five = to_church(5)
church_six = to_church(6)
# Successor of 2 is 3
assert church_equal(SUCC(church_two), church_three)
# 2 + 3 = 5
assert church_equal(ADD(church_two)(church_three), church_five)
# 2 * 3 = 6
assert church_equal(MUL(church_two)(church_three), church_six)
print("All tests passed.")
如你所见,我们只使用λ函数执行数值操作!!此外,通过扩展这个方法并结合λ布尔逻辑,我们甚至可以仅使用λ函数实现 if/else、循环,甚至整个编程语言!是不是很神奇?
好的,在简要介绍完λ演算之后,我们可以进入我们旅程的下一个话题。
交互网
在直接进入交互组合子之前,我们先学习一下 Yves Lafont 的另一项早期工作:交互网。这一基础将使理解交互组合子变得更加容易。
交互网是由 Yves Lafont 在 1990 年创建的一种计算模型。它们使用类似图形的结构和一组交互规则来表示算法。
我们需要定义的第一件事是一个单元。它由某个符号例如α,一个主端口和 n 个辅助端口组成,如下图所示:

单元 — 图片由作者提供
当一个单元的辅助端口数 n = 0 时,它的表示如下:

arity n=0 的单元 — 图片由作者提供
通过将一组单元通过它们的端口用电线连接,我们构造一个网络。例如,一个包含单元α, β和γ的网络,其中arity分别为 n = 2, 1 和 0。

图片由作者提供,灵感来自Lafont, 1997
请注意,一个电线可以连接同一个单元的两个端口,并且一个网络不一定需要完全连接。此外,在这个示例中有三个自由端口x、y和z。
每当一对单元通过它们的主要 端口连接时,就会发生一次交互。交互是一条规则,它会修改网络。这些通过活动端口连接并准备交互的单元对称为活动对(或红雷)。
在前面的例子中,第一轮有两个可能的交互(活动对)。

作者图像,灵感来自于 Lafont, 1997
在应用这些规则后,网络将被修改。然后,我们会将这些规则反复应用到结果生成的网络上,直到达到一个不可简化的形式,即无法再应用任何交互规则时为止。这个反复应用交互规则的过程也被称为归约。
交互系统是由一组可以毫不含糊地应用的交互规则构成的。也就是说,如果我们为活动对(αi, αj)定义了一个交互规则,那么对于所有出现的(αi, αj),这个规则都是一样的。
在这个简短的解释之后,让我们做一些练习。
构建一个算术交互系统
让我们构建一个用于做算术的交互系统。为了创建它,我们首先要忘记关于数字的基本直觉,尝试创建一个能够建模自然数的系统。在 1889 年,Giuseppe Peano 提出了五条公理来形式化自然数,这类似于欧几里得为几何定义的公理。Peano 的公理使得一个有限符号集和规则可以生成一个无限的集合。通过这些公理,Peano 为自然数及其算术性质定义了一些规则:
0 → 表示数字零
s(n) → 表示后继函数。它返回下一个自然数。
使用s和0,我们可以定义自然数,正如我们之前在λ演算学习中所看到的:
1 = s(0)
2 = s(s(0))
3 = s(s(s(0)))
依此类推……
+ → 表示加法。它是一个递归定义的函数,如下所示:
基础情况: 0 + a = a
递归: a + s(b) = s(a+b)
例如:
a + 3:
= a + s(2)
= s(a+2)
= s(a+s(1))
= s(s(a+1))
= s(s(a+s(0)))
= s(s(s(a+0)))
= s(s(s(a)))
×:表示乘法。它是一个递归定义的函数,如下所示:
基础情况: b × 0 = 0
递归: s(a) × b = (a × b) + b
受此启发,Yves Lafont 构建了一个交互系统,用以建模自然数和算术。让我们来理解:
首先,他为s和0符号定义了单元:

作者图像,灵感来自于 Lafont, 1997
然后,定义加法运算的单元:

作者图像,灵感来自于 Lafont, 1997
我知道这看起来很奇怪,但我保证它会变得更加有意义。
如果所有自然数都可以仅使用符号 0 和后继 s 来表示,那么对于加法,我们只需要定义两个 交互 规则:加法如何与后继和 0 进行交互。因此,Lafont 提出了以下两个 交互 规则:

图像来自作者,灵感来源于 Lafont, 1997
将这些规则与皮亚诺加法的方程进行比较,它们正好是相同的表达式:
s(x) + y = s(x+y)
0 + y = y
现在,让我们理解乘法的 交互 规则。乘法的 单元格 定义如下:

图像来自作者,灵感来源于 Lafont, 1997
现在,看看皮亚诺的方程:
y × 0 = 0
s(x) × y = (x × y) + y
注意,第一个方程“抹除”了 y 变量(y 出现在方程的左侧,但在右侧没有出现)。在第二个方程中,y 被“复制”了,通过另一个乘法和加法。
因此,还需要另外两个符号:ε (抹除器) 和 δ (复制器)。

图像来自作者,灵感来源于 Lafont, 1997
这些符号的思想是,表示自然数的网络在连接到 ε 的主端口时会被抹除,而连接到 δ 的主端口时则会被复制。现在,乘法规则可以表示为:

图像来自作者,灵感来源于 Lafont, 1997
尝试反思它们与皮亚诺表达式的相似之处:
s(x) × y = (x × y) + y
y × 0 = 0
交互 规则对于 复制器 和 抹除器 与 后继 和 0 的定义如下:

图像来自作者,灵感来源于 Lafont, 1997
因此,我们有一组六个符号 {0, s, +, ×, δ, ε} 和以下八个 交互 规则:{(s, +), (0, +), (s, ×), (0, ×), (s, δ), **(0, δ), **(s, ε), **(0**, ε)}。让我们通过操作 2 × 2 来实际分析它们。

2 x 2。图像来自作者,灵感来源于 Lafont, 1997
如果你仔细看,会发现有一个活跃的对 (s, ×),我们可以应用规则 #3。

应用交互规则#3。图片来源:作者,灵感来自于 Lafont, 1997
因此,运算通过应用交互规则来解决,直到我们得到一个不可约的形式:

2x2 = 4。图片来源:作者,灵感来自于 Lafont, 1997
看看我们已经得到的最终形式:s(s(s(s 0))).

图片来源:作者,灵感来自于 Lafont, 1997
这正是数字 4 的定义,2 × 2 的结果!令人惊讶,对吧?经过一些奇怪符号的操作后,我们竟然能解出一个算术运算!😀
那么,为什么要做这么复杂的事情呢?使用这些操作来解决问题有什么优势呢?
Lafont 的网络有一个有趣的性质:如果一个网络μ能在一步内减少到两个不同的可能网络v或v’,那么v和v’都能在一步内减少到一个共同的网络ξ。

图片来源:作者,灵感来自于 Lafont, 1997
这种汇聚性质的结果是,如果一个网络μ在n步内减少到v,那么任何一系列的减少都会在n步内达到v。换句话说,交互规则的应用顺序并不重要,网络将在相同的步数内达到相同的形式!
你感受到这种性质的力量了吗?基本上,如果交互的顺序无关紧要,我们就可以并行应用它们!🤯
例如,在我们之前的 2 × 2 运算中,我们可以在类似的时刻并行应用这些规则,而不是一条条逐个应用:

图片来源:作者,灵感来自于 Lafont, 1997
在实际执行中,这两条规则可以通过在两个独立的线程中并行运行,从而避免线程冲突和其他与并行性相关的常见问题。这也是 HVM/Bend 所基于的核心原则之一!基于这一点,所有可以并行化的操作都将被本质地并行化!
现在我们已经理解了交互网络,让我们再迈进一步。之前在本文中,我提到 HVM 是基于交互组合子的,那么让我们来理解这些概念之间的关系。
交互组合子
基于他早期的交互网工作,Yves Lafont 创建了交互组合子。这个思想是使用一个最小的基本符号集(称为组合子)来创建计算的表示。虽然交互网通过图重写显式地建模计算,交互组合子则通过专注于基本的组合逻辑来改进这一点。这一转变提供了一个更加抽象但更强大的框架,用于表达计算过程。
对于交互组合子,Lafont 定义了三个符号(也称为组合子):γ(构造子),δ(复制子)和ε(擦除子)。
使用这三个组合子,总共创建了仅六条规则。这些规则分为:
交换 — 当两个不同符号的单元互相作用时(γδ, γε, δε);
湮灭 — 当两个相同符号的单元互相作用时(γγ,δδ,εε)。
规则如下所定义:

交换规则。图片来自作者,灵感来源于 Lafont, 1997

湮灭规则。图片来自作者,灵感来源于 Lafont, 1997
因此,仅使用这六条规则,你就可以建模任何可计算的算法!惊人吧,对吧?
然而,HVM 运行时使用的是 Lafont 的交互组合子的一个变体,称为对称交互组合子 (SIC) (Mazza, 2007)。这个变体是一个简化版,它对所有符号使用相同的重写规则:

对称交互组合子规则。图片来自作者,灵感来源于 Mazza, 2007
如你所见,唯一的区别是规则γγ和δδ现在是相似的。 关键的汇聚性质得以保持,保留了其并行化能力。
从现在开始,我们将使用 SIC 规则进行示例,因此请专注于它们。
Lambda 演算 → 对称交互组合子
现在你可能会问:“我该如何使用这些写程序?如何将我的 Python 函数转换为交互组合子图形?”
我之前提到过,你可以使用λ演算表示任何可计算的算法,对吧?
现在有另外一条信息:你可以将λ演算转换为交互组合子!
因此,任何程序都可以转换为λ演算,然后转换为交互组合子,进行并行运行,再转换回来!

图片来自作者
那么,让我们理解如何将λ转换为交互组合子!
Lambda 表达式 ( λ ) 和 应用(@)可以通过构造子γ表示。例如,lambda 表达式 λx.y 可以表示为:

使用 SIC 的 Lambda 表达式。作者提供的图片
对于给定的 应用 f x,我们可以将其表示为:

使用 SIC 的 Lambda 应用。作者提供的图片
使用这些表示法,我们可以表达恒等式 表达式 λx.x(给定 x,返回 x 本身):

λx.x。作者提供的图片
现在,假设我们想进行 应用(λx.x)y:

(λx.x)y 作者提供的图片
如果我们简化表达式 (λx.x)y,我们会得到 y 作为结果。让我们分析一下使用 SIC 规则可以得到什么?
请注意,当一个 应用 应用于一个 lambda 表达式 时,会有一个 活动对,我们可以简化它!在这种情况下,我们将应用交互规则 γγ。同时,从现在开始,我们将使用圆圈来标识我们感兴趣的最终计算结果。

作者提供的图片
正如你所看到的,(λx.x)y 被正确地简化为 y!很神奇,对吧?
现在,假设我们想表达 λf.ff(给定 f,将 f 应用到它自身)。正如你所注意到的,参数 f 在主体部分是 重复 的。这时,复制器(*δ)就派上用场了!我们可以使用 复制器 来复制(重复)值:

作者提供的图片
让我们回到我们的表达式 λf.ff。首先,确认这是一个 表达式,它接受输入 f,并输出 f 应用到它自身。因此,它可以表示为:

“给定 f,输出 f 应用到 f”。作者提供的图片*
除了复制,变量也可以消失。例如,我们来看看教堂数字 0 := λf.λx.x。这个表达式可以解读为“给定两个变量 f 和 x,返回 x”。正如你所注意到的,变量 f 在输出中没有被使用。如果我们试图用当前的 SIC 知识表示它,我们将得到:

作者提供的图片
f 线漂浮着。似乎有点问题,对吧?这就是我们需要 橡皮擦 ε 的原因!为了表示这个变量的消失,我们这样做:

作者提供的图片。
总结一下,我们可以通过以下方式使用对称交互组合子处理 Lambda 演算:

作者提供的图片。灵感来源于 zicklag.katharos.group/blog/interaction-nets-combinators-calculus/
示例
现在,我们已经覆盖了这些转换,能够执行更复杂的操作了。
乔治·教堂数字
让我们画一些乔治·教堂数字!

作者提供的图片
在我们继续之前,试着自己做一下!拿张纸开始画吧!例如,让我们一起尝试画出教会数 4: λf.λx.f(f(f(f x)))。
我画的东西是外部的 lambda 表达式 λf.____

给定 f,输出 λx.f(f(f(f x)))。图片由作者提供
然后,第二个 lambda 表达式 __.λx.____:

给定 x,输出 f(f(f(f x)))。图片由作者提供
现在,我们需要绘制 应用(@)。但首先,请注意我们有 f 重复了四次。因此,我们需要复制(重复) f 三次(所以我们需要三个连续的复制器):

f 的复制。图片由作者提供
现在我们有了四个 f 的副本,我们可以按顺序绘制 f 到 f 的 应用!

使用 SIC 表示法的教会数 4。图片由作者提供
使用相同的策略,我们可以轻松构造其他表达式。
后继函数
让我们实现后继函数。它表示为 λn.λf.λx.f((n f) x)。

后继函数。图片由作者提供
让我们将后继函数应用于数字 0 并分析得到的结果。

SUCC 0。图片由作者提供
让我们应用交互规则。为了便于阅读,我将 复制器 δ 画为黑色单元,而 构造器 γ 画为白色单元:

SUCC 0 的简化。图片由作者提供
好吧,我们应该已经得到了教会数 1,对吧?出了什么问题?看看与复制器 δ(黑色)辅助端口连接的 橡皮擦 ε:

图片由作者提供
这个 橡皮擦 让这个左侧辅助端口变得多余!通过这个复制器传递的所有信息都会被擦除。对于任何与这个 复制器 交互的 单元,其 左侧 部分将被 擦除。
因此,我们可以去除这个多余的 复制器,并直接连接 电线:

图片由作者提供。
然后,瞧! 在简化 SUCC(0) 后,我们得到了正好是教会数 1,正如预期的那样!
让我们再次将 SUCC 应用到数字 1 上,看看我们能否得到数字 2:

SUCC 1。图片由作者提供

SUCC 1 = 2。图片由作者提供
我们正好得到了教会数 2!很神奇,对吧?
加法
到目前为止,我们只进行了顺序化简。让我们做一个更复杂的操作,例如加法,来可视化交互组合器的完整并行化潜力。下面是加法的 SIC 表示法: ADD(m, n) = λm.λn.λf.λx.(m f)((n f) x)。

加法。图片由作者提供
让我们计算 ADD 1 1:

ADD 1 1。图片由作者提供
执行简化操作:

图片由作者提供
看一下这一步。这里有两个活跃的配对!像这种情况,我们可以并行地简化这两个。在实际的程序中,我们可以在两个不同的线程中运行它们。
让我们继续:

ADD 1 1 = 2. 图片由作者提供
在简化 ADD 1 1 之后,我们得到了恰好是教堂数字 2 的表示!
这就是如何使用交互组合子并行化操作的过程。在每一步,如果有多个活跃的配对,它们都会在不同的线程中运行。
结论
在这篇文章中,我们涵盖了λ演算的基本概念、交互组合子以及它们如何结合以并行化操作。我希望我能简要地解释 Bend/HVM 是如何工作的,更多信息,请访问他们的网站。
同时,关注我在这里和我的LinkedIn 个人资料,以便及时了解我的最新文章!
参考文献
偏倚与方差如何影响你的模型
学习概念和实践。了解模型在每种情况下的表现。
·发布于 Towards Data Science ·阅读时间 7 分钟·2024 年 12 月 24 日
--

击中靶心 | Google Gemini 2.0 Flash,2024. https://gemini.google.com
介绍
自从我开始转向数据科学以来,我听说过著名的 偏倚与方差权衡。
但我学到了足够的知识,以便继续我的研究,并且没有过多回头看。我一直知道,高度偏倚的模型会导致数据欠拟合,而高变异的模型会导致过拟合,这两者在训练机器学习模型时都不是好选择。
我也知道,我们应该在这两种状态之间寻找平衡,这样我们就能得到 良好的拟合 或一个能够将模式很好地泛化到新数据的模型。
但我可能会说,我从未走得更远。我从未搜索或创建高度偏倚或高度变异的模型,仅仅是为了看看它们对数据的实际影响,以及这些模型的预测结果如何。
当然,直到今天,因为这正是我们在这篇文章中所做的。让我们继续进行一些定义。
高偏倚

过度简化 = 用锤子敲打任何东西 | Google Gemini,2024. gemini.google.com
你的回归模型有多偏差?

图片来源:作者
深入探讨回归模型中的偏差原因、影响及补救措施
·发布于Towards Data Science ·阅读时长 27 分钟·2024 年 9 月 13 日
--
这一包花生 M&M 糖的重量是 45 克。

(图片来源:作者)
Mars 公司可能已经精确地定价了这一包。但 Mars 是如何知道要将多少颗花生 M&M 糖放入一个 45 克重的包裹中的呢?
我们可以简单地将 45 克除以一颗花生 M&M 糖的平均重量,然后四舍五入到最接近的整数(呃!)。
但这又引出了另一个问题:
一颗花生 M&M 糖的平均重量是多少?
到今年年底,Mars 公司工厂将生产出约 1500 亿颗 M&M 糖。明年生产的数量会更多,未来几年也会继续增加。那么,你如何知道一颗 M&M 糖的平均重量呢?
顺便说一下,究竟是谁在吃掉这些 M&M 糖?
无论如何,计算一颗 M&M 糖的平均重量——所谓的总体均值——几乎是不可能的。
如果我们无法得知它的值,那么我们必须估计它,而估计总是伴随着误差。
自动驾驶汽车如何更好地工作?
Waymo 的 EMMA 及其他端到端驾驶系统的深远影响
·发布于Towards Data Science ·7 分钟阅读·2024 年 11 月 28 日
--

想象一下你是一位饥饿的登山者,迷失在远离城市的小径上。走了很多英里后,你终于发现了一条道路,并看到一辆车的轮廓正向你驶来。你心里准备了一番同情话语,想向司机求得帮助,但当你意识到这辆车是自动驾驶时,你的希望转为恐惧。车里没有人可以展示你的可信度,或向其寻求同情。
在决定不冲到车前之后,你尝试招车,但汽车的软件将你识别为一个奇怪的行人,迅速从你身边驶过。
有时候,仅仅拥有一个紧急呼叫按钮或一个实时帮助热线 [以满足加利福尼亚州法律要求] 是不够的。某些极端情况需要干预,随着自动驾驶汽车在我们道路上的普及,这类情况将会变得更加频繁。像这样的极端情况尤其棘手,因为它们需要逐案处理。解决这些问题不像编写一个困惑脸表情分类器那么简单,除非你想让人们摆出困惑的表情来免费搭车。也许汽车可以利用人工支持,Zoox 所称的“远程引导”,以筛选出真正的案例,同时确保系统不被滥用,这种现实而无聊的解决方案可以奏效……至少现在是这样。自动驾驶汽车研究中的一个有趣进展为更复杂的解决方案提供了钥匙。
通常,自动驾驶算法通过将驾驶任务分解为多个模块来实现,并在这些模块上不断提高技能。不同公司可能有不同的分解方式,但 Waymo 和 Zoox 使用的常见分解方式包括:地图绘制、感知、预测和规划模块。

图 1:传统自动驾驶汽车核心的基础模块。来源:作者提供的图片。
这些模块每个只专注于它们被大量训练的某一功能,这使得它们更容易调试和优化。然后,在这些模块之上设计接口,将它们连接起来,使其协同工作。

图 2:模块如何通过接口连接的简化示意图。来源:作者,Ramsha Ali (@ramsha4103) 提供的图片。
通过接口连接这些模块后,整个管道将进一步在模拟环境中进行训练,并在现实世界中进行测试。

图 3:自动驾驶汽车中不同软件模块如何结合在一起。来源:作者,Ramsha Ali (@ramsha4103) 提供的图片。
这种方法有效,但效率较低。由于每个模块都是单独训练的,接口常常难以让它们良好地协同工作。这意味着汽车在面对新环境时适应能力较差。通常,模块间会积累误差,且这些误差由于不灵活的预设规则而变得更严重。解决方案似乎是让它们在不太可能的场景上训练,直觉上这看起来是可行的,但实际上却并不可行。原因在于,驾驶场景遵循长尾分布。

图 4:长尾分布,展示了在不太可能的场景上训练汽车时,随着训练场景的增加,回报逐渐递减。来源:作者,Ramsha Ali (@ramsha4103) 提供的图片。
这意味着我们拥有最有可能的场景,容易训练,但也存在许多不太可能的场景,尝试在这些场景上训练模型会导致极高的计算成本和时间消耗,而得到的回报却微乎其微。比如鹰嘴俯冲下落,突发地陷形成,电力杆倒塌,或者跟在一辆刹车灯熔断的车后面行驶。对于一辆仅在高度相关数据上训练、没有任何世界知识、且难以适应新解决方案的汽车来说,这意味着必须不断追赶,去考虑所有这些不可能发生的场景,或者更糟的是,当出现严重问题时,不得不强行添加更多训练场景。
两周前,Waymo 研究团队发布了一篇关于 EMMA 的论文,这是一个端到端的多模态模型,可以从根本上改变问题的处理方式。这个端到端模型与其传统的模块化组件不同,它将所有的世界知识都集成在一个全知的 LLM 核心中,然后进一步微调以实现驾驶功能。例如,Waymo 的 EMMA 建立在 Google 的 Gemini 之上,而 DriveGPT 则建立在 OpenAI 的 ChatGPT 之上。
这个核心模型随后使用精心设计的提示进行训练,以提供上下文并提出问题,推导出其空间推理、道路图估算和场景理解能力。还要求 LLM 提供解码后的可视化结果,分析文本解释是否与 LLM 在模拟中可能的行为一致。通过语言输入的多模态融合,使得训练过程变得更加简化,因为可以使用单一模型同时训练多个任务,从而通过简单变更任务提示实现任务特定的预测。

图 5:端到端视觉语言模型如何被训练来驾驶。来源:作者图像,Ramsha Ali(@ramsha4103)
另一个有趣的输入变量通常是一个自我变量,这与车辆是否感觉优越无关,而是存储诸如车辆位置、速度、加速度和方向等数据,以帮助车辆规划出一条平稳一致的驾驶路线。这通过更加平滑的行为过渡和与周围代理的连续一致互动来提升性能。
这些端到端模型在通过模拟测试时,能够在公共基准测试中提供最先进的性能。GPT 知道如何填写 1040 表格,这如何帮助它开车更好呢?丰富的世界知识和逻辑推理能力意味着在新颖情况下的更好表现。该模型还允许我们在多个任务上共同训练,相较于单任务模型,其性能提高了超过 5.5%,尽管输入量更少(没有高清地图、没有接口、也没有激光雷达或雷达的访问权限)。它们在理解手势、转向信号或其他司机的口头指令方面也表现得更好,并且在社交上更善于评估周围汽车的驾驶行为和攻击性,并相应调整预测。你还可以要求它们为决策提供依据,从而绕过它们的“黑箱”特性,使得决策的验证和可追溯性变得更加容易。
除此之外,LLM(大语言模型)还可以帮助创建它们可以进行测试的模拟环境,因为它们可以为图像加标签,并能接收文本输入来生成图像。这可以显著简化构建一个易于控制的环境,用于测试和验证自动驾驶系统的决策边界,并模拟各种驾驶情境。
这种方法仍然较慢,能输入的图像帧有限,并且计算要求较高,但随着我们的 LLM(大语言模型)变得更好、更快、计算成本更低,并且能结合激光雷达和雷达等附加模态,我们将看到这种多模态方法在三维物体检测质量上以指数级的速度超过专门的专家模型,但这可能还需要几年时间。
随着端到端自动驾驶汽车行驶时间的增加,观察它们对周围人类驾驶员的影响,以及在每个城市中发展出独特的“自动气质”或个性,将会很有趣。这将成为一个全球驾驶行为的迷人案例研究。更为迷人的是,看看它们如何影响周围的人类驾驶员。
一个端到端系统还意味着能够与汽车进行对话,就像你与 ChatGPT 对话一样,或者能够在街上走到一辆车前并向它询问方向。这也意味着我将听到更少来自朋友的故事,他们发誓再也不坐 Waymo 了,因为它差点撞上了一辆超速行驶的救护车,或者没有为低飞的鸟停下。
想象一辆自动驾驶汽车不仅知道自己在什么时间、什么地点(在午夜接近的荒凉高速公路上),还理解这意味着什么(行人不合时宜且可能处于危险中)。想象一辆汽车不仅能够呼叫帮助(因为加利福尼亚州法律要求它这么做),而且能够真正提供帮助,因为它可以从伦理角度做出逻辑推理。那样的车,才值得一试。
参考文献:
Chen, L., Sinavski, O., Hünermann, J., Karnsund, A., Willmott, A. J., Birch, D., Maund, D., & Shotton, J. (2023). Driving with LLMs: Fusing Object-Level Vector Modality for Explainable Autonomous Driving (arXiv:2310.01957). arXiv. doi.org/10.48550/arXiv.2310.01957
Cui, C., Ma, Y., Cao, X., Ye, W., Zhou, Y., Liang, K., Chen, J., Lu, J., Yang, Z., Liao, K.-D., Gao, T., Li, E., Tang, K., Cao, Z., Zhou, T., Liu, A., Yan, X., Mei, S., Cao, J., … Zheng, C. (2024). A Survey on Multimodal Large Language Models for Autonomous Driving. 2024 IEEE/CVF Winter Conference on Applications of Computer Vision Workshops (WACVW), 958–979. doi.org/10.1109/WACVW60836.2024.00106
Fu, D., Lei, W., Wen, L., Cai, P., Mao, S., Dou, M., Shi, B., & Qiao, Y. (2024). LimSim++: A Closed-Loop Platform for Deploying Multimodal LLMs in Autonomous Driving (arXiv:2402.01246). arXiv. doi.org/10.48550/arXiv.2402.01246
Hwang, J.-J., Xu, R., Lin, H., Hung, W.-C., Ji, J., Choi, K., Huang, D., He, T., Covington, P., Sapp, B., Zhou, Y., Guo, J., Anguelov, D., & Tan, M. (2024). EMMA: End-to-End Multimodal Model for Autonomous Driving (arXiv:2410.23262). arXiv. doi.org/10.48550/arXiv.2410.23262
“全栈”:自动驾驶背后的故事。(n.d.). Zoox. 取自 2024 年 11 月 26 日,zoox.com/autonomy
Wang, B., Duan, H., Feng, Y., Chen, X., Fu, Y., Mo, Z., & Di, X. (2024). 大语言模型能否理解自动驾驶游戏中的社会规范? (arXiv:2408.12680). arXiv. doi.org/10.48550/arXiv.2408.12680
Wang, Y., Jiao, R., Zhan, S. S., Lang, C., Huang, C., Wang, Z., Yang, Z., & Zhu, Q. (2024). 利用大语言模型赋能自动驾驶:安全视角 (arXiv:2312.00812). arXiv. doi.org/10.48550/arXiv.2312.00812
Xu, Z., Zhang, Y., Xie, E., Zhao, Z., Guo, Y., Wong, K.-Y. K., Li, Z., & Zhao, H. (2024). DriveGPT4: 通过大语言模型实现可解释的端到端自动驾驶 (arXiv:2310.01412). arXiv. doi.org/10.48550/arXiv.2310.01412
Yang, Z., Jia, X., Li, H., & Yan, J. (n.d.). LLM4Drive:大语言模型在自动驾驶中的应用综述。
我们如何持续适应视觉-语言模型?
探索 CLIP 的持续学习策略
·发布于 Towards Data Science ·8 分钟阅读·2024 年 8 月 26 日
--

图片由作者在 Midjourney 中创作
目前对大语言模型的研究和应用兴趣日益增长。然而,这些模型只能处理文本数据,这限制了它们在某些应用中的实用性。人类能够处理跨多个模态的信息,如书面和口语语言,以及对我们周围现实的视觉理解。我们期望模型能够进行类似的处理。
视觉-语言模型能够处理文本和视觉数据,这在图像分析(例如医学图像)、物体识别和更好的场景理解(例如自动驾驶汽车)、为图像生成描述、回答视觉问题、与图像进行对话等领域有广泛的应用。
不幸的是,多模态模型面临着与单模态模型相同的挑战。一旦训练完成,随着新数据样本的到来或数据分布的变化,它们可能会随着时间的推移变得过时。
在我的上一篇文章中,我介绍了持续学习(CL)方法,适用于 AI 模型。持续学习试图找到持续训练模型的方法,这可能是未来更可持续的解决方案。在本文中,我想探讨将 CL 应用于视觉-语言模型(VLMs)的可能性——特别是对比语言-图像预训练(CLIP)模型的应用。
但什么是 CLIP?
对比语言-图像预训练(CLIP)由 OpenAI 在 2021 年的 从自然语言监督学习可转移的视觉模型 论文中提出 [1]。
CLIP 模型的目标是理解文本与图像之间的关系。如果你输入一段文本,它应该返回在给定图像集合中最相关的图像。同样,如果你输入一张图像,它应该从可用的文本集合中给出最匹配的文本。
CLIP 是在一个大型的文本-图像对数据集上训练的。通过对比学习,将匹配的文本-图像对拉近到嵌入空间中,并将不匹配的对远离彼此。然后,这个学习到的共享嵌入空间在推理时用于理解文本和图像之间的关系。如果你想了解更多关于 CLIP 的信息,我推荐阅读以下文章,它详细描述了 CLIP。
为什么我们需要为视觉-语言模型进行持续学习?
大型基础模型可能会因为分布的变化或新数据样本的到来而变得过时。重新训练这些模型既昂贵又耗时。TiC-CLIP 论文的作者 [7] 显示,当前的评估实践往往未能捕捉到在考虑时间演变数据时性能的差异。
在图 1 中,你可以看到如果我们比较 2020 年前训练的 OpenAI 模型和 2022 年前训练的 OpenCLIP 模型,尽管它们在 Imagenet 上的鲁棒性(左侧图像)差异不大,但在从 2014-2016 年和 2021-2022 年的检索任务上比较时(右侧图像),它们之间存在性能差距,表明 OpenAI 模型在时间演变的数据上零-shot 鲁棒性较差 [7]。

图 1. 来自论文 TiC-CLIP: Continual Training of Clip Models [7] 的图像。
此外,持续学习可能是某些使用案例的自然选择,比如在线终身学习(OLL)[8],其中数据来自于连续和非平稳的数据流,并随着时间变化而演化。
最后,正如 [4] 中指出的,CLIP 展示了显著的零-shot 能力,但对于某些领域,由于预训练时某些类别的数据不足,它可能难以实现良好的性能。
挑战
随着一些当前最先进的视觉-语言模型需要越来越多的计算时间和资源,找到一种不断适应这些模型而无需重新训练的方法似乎变得至关重要。然而,持续适应这些模型也面临一些挑战:
-
灾难性遗忘—— 学习新任务可能会损害旧任务的性能。
-
丧失零-shot 能力 — 预训练模型可以表现出零-shot 行为,意味着它们可以执行没有接收过训练数据的任务,即在训练时未见过的图像类别的分类。这个能力在持续训练时可能会丧失。
-
文本和图像表示之间的错位 — 正如[12]的作者所指出的,在 CLIP 的持续学习过程中,多模态表示空间的对齐可能会退化,这可能导致长期的性能下降。
CLIP 的持续学习方法
目前正在进行的研究旨在改善多模态模型的持续学习方面。以下是一些现有的策略和应用场景:
- 专家混合(MoE)
-
为了持续训练 CLIP,[2]的作者提出了使用任务特定适配器的MoE方法。他们在冻结的 CLIP 模型上构建了一个动态扩展架构。
-
这里的想法是,随着新任务的训练,添加新的适配器。同时,训练分布判别自选器,以便在推理阶段,模型可以自动选择测试数据是否应进入 MoE 适配器,或进入预训练的 CLIP 进行零-shot 检测。
2. CoLeCLIP
-
[4]的作者关注的是开放领域中视觉-语言模型的持续学习问题——在这些领域中,我们可能拥有来自不同已见和未见领域的包含新类的数据集。
-
解决开放领域挑战对于AI 助手、自动驾驶系统和机器人等应用场景尤为重要,因为这些模型在复杂且变化的环境中运行[4]。
-
CoLeCLIP基于 CLIP,但进行了调整以应对开放领域问题。
-
在 CoLeCLIP 中,每个任务都附加了一个外部可学习的参数高效微调(PEFT)模块,连接到 CLIP 的冻结文本编码器,以学习类的文本嵌入[4]。
3. 持续语言学习(CLL)
-
[3]的作者指出,当前的预训练视觉-语言模型通常只支持英语。同时,创建多语言模型的流行方法成本高昂,并且需要大量数据。
-
在他们的论文中,提出通过使用CLL来扩展语言能力,其中语言知识是增量更新的。
-
CLL-CLIP使用可扩展的嵌入层来存储语言差异。它仅训练令牌嵌入,并优化图像和多语言文本之间的对齐学习[3]。
-
作者还提出了一种新颖的方法,确保在初始化时,所有令牌嵌入的分布是相同的,并且在训练过程中得到了正则化。你可以在他们的论文中的图 2 看到这一过程的可视化。

图 2. 来自论文《通过持续语言学习拥抱 CLIP 中的语言包容性和多样性》中的图像[3]。
4. 对称图像-文本调优策略(SIT)
-
在[8]中,作者观察到,在他们的在线终身学习场景中,进行参数高效调优(PET)时,文本与图像之间出现不对称,这可能导致灾难性遗忘。
-
他们提出使用 SIT 策略来缓解这个问题。该方法仅在在线学习期间,在当前批次内匹配图像和类别标签。
-
目标是在不引入编码器之间不对称性的情况下,保持 CLIP 的泛化能力,同时提高其在特定下游任务或数据集上的表现。
持续学习模型的评估
持续学习(CL)的评估标准似乎仍在完善中。许多现有的评估 CL 模型有效性的基准在构建数据集时并没有考虑时间因素。正如[7]中提到的,性能差距有时只有在我们重新创建时间演变的测试数据设置时才会显现出来。
此外,许多现有的视觉-语言模型基准仅关注单张图像输入,而未衡量多图像理解,这在某些应用中可能至关重要。[5]的作者开发了一个多图像评估基准,可以更细致地评估当前最先进模型的局限性和能力。
持续学习并不能解决所有问题…
像 CLIP 这样的视觉-语言模型有其不足之处。在[6]中,作者探讨了 CLIP 的视觉嵌入空间与纯粹的视觉自监督学习之间的差距。他们研究了嵌入空间中的错误匹配,在这些地方,图像的编码相似,尽管它们不应如此。
根据他们的结果可以得出结论,如果一个预训练模型存在弱点,那么在模型适应过程中,这些弱点可能会被传播。学习视觉表示仍然是一个开放的挑战,视觉模型可能会成为多模态系统的瓶颈,因为仅仅扩展模型规模并不能解决像 CLIP 这样的模型内在的局限性。[6]
结论
本文探讨了将持续学习应用于视觉-语言模型的机遇与挑战,重点介绍了 CLIP 模型。希望本文能给您提供一个初步印象,表明虽然持续学习似乎是未来 AI 模型的一个良好方向,但仍有大量工作需要完成,才能使其完全可用。
如果您有任何问题或评论,请随时在评论区分享。
下次再见!

图像由作者在 Midjourney 中生成。
参考文献
[1] Radford, A., Kim, J., Hallacy, C., Ramesh, A., Goh, G., Agarwal, S., Sastry, G., Askell, A., Mishkin, P., Clark, J., Krueger, G., & Sutskever, I. (2021). 从自然语言监督中学习可转移的视觉模型。载于第 38 届国际机器学习会议论文集(第 8748–8763 页)。PMLR。
[2] 余家左, 朱戎智, 张璐, 胡平, 王东, 陆虎川, & 何友. (2024). 通过专家混合适配器提升视觉-语言模型的持续学习。
[3] 杨邦, 戴勇, 程旭鑫, 李耀伟, 阿西夫·拉扎, & 邹跃贤. (2024). 通过持续语言学习拥抱 CLIP 中的语言包容性和多样性。
[4] 李宇坤, 庞冠松, 苏伟, 景晨晨, 谷凌曦, 刘玲桥, 陈昊, 梁国强, & 王鹏. (2024). CoLeCLIP:通过联合任务提示和词汇学习实现开放域持续学习。
[5] 赵冰晨, 宗永硕, 张乐天, & 提莫西·霍斯佩达雷斯. (2024). 视觉和语言模型中的多图像理解基准测试:感知、知识、推理和多跳推理。
[6] 孙胜邦, 刘庄, 斋月翔, 马怡, 扬·勒昆, & 谢赛宁. (2024). 眼睛紧闭?探索多模态 LLM 的视觉缺陷。
[7] 索拉布·加尔格, 哈迪·普尔·安萨里, 梅赫达德·法拉吉塔巴尔, 萨钦·梅塔, 拉维特贾·维穆拉帕利, 恩切尔·图泽尔, 维沙尔·尚卡尔, & 法尔塔什·法赫里 (2023). TiC-CLIP:CLIP 模型的持续训练. 在NeurIPS 工作坊中。
[8] 王乐源, 向柳玉, 魏宇杰, 王云龙, & 何兆锋. (2024). CLIP 模型是一个高效的在线终身学习者。
[9] 维沙尔·腾甘, 萨尔曼·汗, 穆纳瓦尔·哈亚特, & 法赫德·汗. (2023). CLIP 模型是一个高效的持续学习者。
[10] 丁宇轩, 刘玲桥, 田春娜, 杨景元, & 丁昊轩. (2022). 别停下学习:面向 CLIP 模型的持续学习。
[11] 阿卡什·戈什, 阿尔卡迪普·阿查亚, 斯里帕尔娜·萨哈, 维尼娅·简, & 阿曼·查达. (2024). 探索视觉-语言模型的前沿:当前方法论与未来方向的调查。
[12] 倪子, 魏林, 唐爽, 庄煜, & 田琦. (2023). 通过离对角线信息进行持续的视觉-语言表示学习. 在第 40 届国际机器学习会议论文集中。JMLR.org。
ChatGPT 如何改变我们教授软件开发的方式
在人工智能助手已经掌握编程技能的情况下学习编码
·发表于 Towards Data Science ·阅读时间:8 分钟·2024 年 1 月 1 日
--

图片由作者使用 Midjourney 创建。
这个启示发生在 2023 年夏天,那时我接了一位高中生做暑期实习生。任务是开发一个机器学习模型,用于预测我们城市的空气质量,使用 Jupyter 笔记本、基础 Python 和 scikit-learn。
一天,我正在与我的实习生讨论算法的性能,并让他们更改一张图表:我要求他们展示预测值和真实值之间的差异,而不是绘制预测值与真实值的对比图。
那位学生切换到另一个浏览器标签页,提示 ChatGPT “计算两个数组 y1 和 y2 之间的差异”,并继续将答案 “y1 — y2” 复制粘贴到笔记本中。
起初,我觉得他们会向人工智能助手询问如此简单的代码行很有趣,确实自己写比提示、等待、复制和粘贴要快得多。但随后我开始思考人工智能助手对我们教授软件开发方式的影响,以及对学生学习成果的影响。
以下是我根据个人经验概述的人工智能助手崛起对编程技能教学的影响……
如何通过群体分析改变您的客户洞察
了解如何通过群体分析追踪客户行为,从而改善您的业务的客户参与和留存策略
· 发表在 Towards Data Science · 6 分钟阅读 · 2024 年 9 月 25 日
--

图片由 Clker-Free-Vector-Images 提供,来源于 Pixabay
尽管进行了几个月的营销努力,您的销售额仍在下降,客户参与度也在下滑。到底出了什么问题?如果有一种方法,能够精确地找出您的客户何时以及为何失去兴趣,您该怎么办?群体分析可能正是您所需要的工具。它可以帮助识别哪些客户群体推动了这种下降,以及他们在何时失去兴趣。与其将您的客户群体视为一个整体,群体分析让您能够追踪特定群体的行为变化。这是深入理解客户参与度和留存度的关键,并能够在问题变得更糟之前,做出有据可依的决策来解决这些问题。
为什么群体分析超越了细分
客户细分是将您的受众分解为更有意义群体的第一步。然而,尽管细分有助于基于共同特征对客户进行分类,但它只提供了一个静态视角。群体分析通过引入时间视角,将细分提升到一个新层次。您可以将细分看作是…
计算机是如何工作的:为我们中的新手提供的指南。
了解个人电脑如何运作的初学者指南。
·发布于 Towards Data Science ·阅读时间:7 分钟·2024 年 3 月 18 日
--

图片由 Dan Cristian Pădureț 提供
作为一个自称的极客,昨天当我工作中的一位资深工程师辩称如今很少有工程师真正理解计算机是如何工作的时,我感到有些羞愧。当然,我的第一反应是开始辩解:“我在大学时就学过这个,这是基础!”但我很快自己笑了笑。教育系统何时真正为我进入行业做好准备了?
当然,从我做软件开发时,我知道计算机是如何运作的一些零星知识,再加上在大学时的广泛(如果说基础)了解。但最终,我意识到我无法进行一次有意义的讨论,谈论我每天都在使用的工具:我的个人电脑。怎么会这样?我怎么能够称自己为网络专业人士,却连这些都不知道?
本文旨在以易于理解的格式解决这一问题。我并不满足于自己缺乏知识,如果你已经读到这里,我怀疑你也不是。
什么是计算机?
计算机的发明源于人类的低效。还有懒惰。设计一台为我们思考的奇妙机器,比我们自己去思考要好得多。你的个人电脑就是一台计算机;但 安提基希拉机制,它大约建于公元前 100 年,用于导航星空,也是一台计算机。计算机并不是近代才有的现象,但本文专注于现代数字计算机。
计算机接受输入数据,对其进行处理,然后输出一些结果数据。艾伦·图灵的理论模型(图灵机)将计算机描述为能够执行任何可以通过一组逻辑规则描述的计算的机器。用英语来说:任何你可以用笔和纸(并且有足够的时间)理论上完成的任务,‘计算机’也应该能够做。它可能比你做得更快。

计算机工作原理的过度简化表示。图片由作者提供。
计算机由什么构成?
由于与物理相关的因素,计算机只能理解两种状态:高电压和低电压。你会听到计算机极客称这些状态为 1 和 0(也就是‘真’和‘假’)。用户要求计算机执行的每一个操作,都始于 1 和 0。无法绕过这一点。
为了使这些 1 和 0 发生作用,我们使用逻辑门。逻辑门是电路中一种物理组件,包含(许多)晶体管。当这些逻辑门连接在一起时,计算机就能够执行逻辑和算术运算。
以下面的‘OR’门为例。如果我们只接收到两个低电压,就没有电力。但是如果接收到一个高电压,就有电力。

OR 门及其真值表。图片由维基百科提供,表格由作者制作。
如果你仍然感到困惑,可以尝试从一个普通的、非计算的背景来思考。例如,蛋糕!

‘蛋糕’表示的 OR 门。图片由作者提供。
有不同类型的逻辑门来做出不同的决策。请查看这里以获得一个很好的解释。
计算机是如何思考的?
到目前为止,我们知道计算机是什么,以及它的电路是由这些被称为逻辑门的东西构成的。那么,是什么神奇的事情发生,意味着某些东西会在屏幕上显示出来呢?为什么你可以使用应用程序,或者访问互联网?
进入中央处理单元(CPU)。
中央处理单元(CPU)由数百万个晶体管和逻辑门组成。可以将其视为计算机的大脑。就像我们的大脑与其他器官一起工作一样,CPU 也与其他组件(如存储单元)一起工作,完成它的工作:即运行程序。
程序只是一个长长的指令序列。当我们将新程序下载到 PC 上时, CPU 会检查该程序并确定如何执行它的指令。它通过 获取、解码、执行 循环来实现——这本身就是一个完整的过程,稍后会有一篇新的文章来详细讲解。在此期间,我建议你阅读关于冯·诺依曼架构的内容:这个视频来自 Computerphile,非常棒。
计算机将数据存储在哪里?
我们已经确定计算机需要存储数据。即使你没有技术背景,你也许能理解这一点:大多数技术广告中都有 RAM 规格,而且,假如计算机无法存储任何你想要的东西,那它还有什么意义呢?
你可能需要理解两种类型的 PC 内部存储:
-
RAM(随机存取存储器)作为计算机的短期记忆,使其能够访问当前运行进程所需的数据和指令。与存储设备不同,RAM 是易失性的,意味着计算机关机或重启时其内容会丢失。想象一下当你的 PC 崩溃时,结果是你在未保存的文档中丢失了一些文字。那是因为计算机关机时 RAM 被清空了。RAM 越快,性能越顺畅,因为它能让 CPU 更快地访问数据。
-
ROM(只读存储器)是只能读取而不能写入的存储器。它通常包含固件或计算机电路上 永久 存储的关键指令。与 RAM 不同,ROM 即使在计算机关机时也会保持其内容。ROM 包含计算机的操作系统,并在操作系统接管之前初始化重要的硬件组件。
这里有一个表格,帮助更清楚地区分这些概念:

RAM 和 ROM 的区别。图片来自作者。
什么是操作系统,为什么我应该关心它?
我们在上一节中提到过操作系统,它最初是从 ROM 中加载的。‘操作系统’(OS)只是一个花哨的术语,用来描述作为软件(即应用程序)与硬件(例如 CPU)之间的翻译程序。Windows、Mac 和 Unix 是最常用的操作系统;IOS 和 Android 也同样常见。
操作系统决定了程序何时可以由 CPU 运行,以及该程序可以访问哪些资源。这就是为什么恶意软件(如 Rootkit)会被制作出来的原因:如果你能够控制操作系统,你几乎可以控制 PC 上的一切。内核被认为是操作系统的核心。

操作系统的保护环。来源:维基百科
操作系统有多种用途。我可以用一种冗长无趣的方式列举出来,但这里我提供一个思维导图:

操作系统的用途。由作者创建,使用 Miro
当我按下 PC 的电源按钮时,实际上发生了什么?
坦白说,当你启动 PC 时会发生很多事情。这里是一个简化版的过程:
-
当电源按钮上的灯亮起时,它会向电源供应单元发送信号,开始向计算机的各个组件供电。
-
主板从电源单元(PSU)获取电力并开始初始化过程。这包括检查各种组件,如 CPU、RAM 和外设,以确保它们正常工作。
-
CPU 也获得电力,并开始执行来自 BIOS(基本输入输出系统)或 UEFI(统一可扩展固件接口)固件的指令,这些固件存储在主板上的内存芯片中。随后,这些组件执行自检(POST),以检查硬件错误。
-
一旦 BIOS/UEFI 初始化完成,主板将控制权交给安装在计算机上的操作系统引导加载程序。引导加载程序将操作系统内核加载到内存中,并启动操作系统。
-
最终,操作系统会向用户展示登录界面或桌面环境,让用户能够与计算机进行互动。
摘要
你可能不会感到惊讶,计算机的工作原理涉及到很多内容。这并不是一个简单的问题:经过多年的技术进步,怎么可能有一个简单的答案呢?你无法将所有内容浓缩成一个简单的声明,坦白说,本文的内容只是触及了表面。
如果你想了解更多,我鼓励你进行一些传统的阅读;更好的是,买一台便宜的 PC 并破解它。正如厄尔·奈廷格尔曾经宣称:
“每天一小时的学习,就足够了。每天一小时的学习,三年内你会成为你所在领域的佼佼者。五年内,你将成为全国权威。七年后,你将成为世界上最顶尖的人之一。”
我不知道这些指标是否仍然适用。但保持好奇心并挖掘答案,总不会有坏处,不管问题有多大。
数据工程自 2014 年以来的演变
帮助数据管道轻松扩展的主要趋势
·发布于 Towards Data Science ·阅读时间:13 分钟·2024 年 7 月 27 日
--

使用 AI 生成的图像,来源于Kandinsky
在这次讨论中,我旨在探讨数据编排和数据建模中的不断发展趋势,重点介绍工具的进步及其对数据工程师的核心好处。尽管自 2014 年以来,Airflow 一直是主导者,但数据工程的格局已经发生了显著变化,现在能够应对更复杂的用例和需求,包括支持多种编程语言、集成和增强的可扩展性。我将考察一些当代的、或许是非传统的工具,这些工具简化了我的数据工程流程,使我能够轻松创建、管理和编排强大、耐用且可扩展的数据管道。
在过去的十年里,我们见证了各种用于数据提取、转换和编排的 ETL 框架的“寒武纪大爆发”。不难发现,其中许多框架是开源的,并且基于 Python。
最流行的框架:
-
Airflow,2014 年
-
Luigi,2014 年
-
Prefect,2018 年
-
Temporal,2019 年
-
Flyte,2020 年
-
Dagster,2020 年
-
Mage,2021 年
-
Orchestra,2023 年
Open Food Facts 如何利用开源 LLM 修复 OCR 提取的成分?
深入了解一个端到端的机器学习项目,旨在提升 Open Food Facts 数据库的质量。
·发表于Towards Data Science ·阅读时间:13 分钟·2024 年 10 月 6 日
--

使用 Flux1 生成的图像
Open Food Facts 的目标是创建全球最大的开源食品数据库。至今,它已经收录了超过 300 万种产品及其相关信息,得益于社区贡献者的帮助。
营养价值、生态评分、产品来源……各种定义每个产品的数据,为消费者和研究人员提供关于他们所放置在餐盘上的食品的深刻见解。
这些信息由用户和贡献者社区提供,他们通过移动应用积极添加产品数据、拍照,并填写数据库中任何缺失的数据。
使用产品图片,Open Food Facts 通过光学字符识别(OCR)技术提取通常位于包装背面的成分列表。然后对产品成分进行解析并将其添加到数据库中。

产品包装上的成分列表
然而,文本提取过程往往并不顺利……
Ingrédients: Jambon do porc, sel, dextrose, arôme naturels, antioxydant: E316, conservateur: E250
^
这些拼写错误看起来可能微不足道,但当成分列表被解析以提取单个成分时,此类错误会导致无法识别的成分,从而影响数据库的质量。光线反射、包装折叠、低质量图片等因素都使得成分解析过程变得更加复杂。


OCR 失败的包装图片示例(来自 Open Food Facts 数据库)
Open Food Facts 多年来一直尝试使用正则表达式和现有解决方案(如 Elasticsearch 的校正器)来解决这个问题,但未成功。直到最近。
感谢人工智能的最新进展,我们现在可以使用强大的大语言模型(Large Language Models),也称为LLMs。
通过训练我们自己的模型,我们创建了成分拼写检查,不仅在这个任务上超越了如GPT-4o或Claude 3.5 Sonnet等专有 LLM,而且还将数据库中未识别的成分数量减少了11%。
本文将带您了解项目的不同阶段,并展示我们如何利用机器学习提高数据库质量。
享受阅读!
定义问题
当一个产品由贡献者添加时,它的图片会经过一系列过程以提取所有相关信息。其中一个关键步骤是提取成分列表。
当一个词被识别为成分时,它会与包含预定义成分列表的分类法进行交叉引用。如果该词与分类法中的某个条目匹配,它就会被标记为成分并添加到产品信息中。
这个标签化过程确保了成分的标准化,且易于搜索,为消费者和分析工具提供准确的数据。
但是,如果某个成分未被识别,过程就会失败。

成分“Jambon do porc”(猪肉火腿)未被解析器识别(来自产品编辑页面)
因此,我们在过程中过引入了一个额外的层次:成分拼写检查,旨在在成分解析器处理之前修正成分列表。
一种更简单的方法是Peter Norvig 算法,它通过应用一系列字符删除、添加和替换操作来处理每个单词,从而识别潜在的拼写纠正。
然而,由于几个原因,这种方法证明对我们的用例不够充分:
-
特殊字符和格式:诸如逗号、括号和百分号等元素在成分列表中具有重要作用,影响产品组成和过敏原标签(例如,“盐(1.2%)”)。
-
多语言挑战:数据库包含来自世界各地的产品,语言种类繁多。这进一步使得像诺尔维格的字符基础方法那样的语言无关的方法变得复杂。
相反,我们转向了机器学习的最新进展,特别是大语言模型(LLMs),它们在包括拼写纠正在内的各种自然语言处理(NLP)任务中表现优异。
这是我们决定采取的路径。
评估
你无法改善你无法衡量的东西。
什么是好的更正?如何衡量更正者的性能,LLM 或非 LLM?
我们的第一步是理解并分类成分解析器遇到的各种错误。
此外,评估错误是否应该被更正也是至关重要的。有时,试图更正错误可能会适得其反:
flour, salt (1!2%)
# Is it 1.2% or 12%?...
基于这些原因,我们创建了拼写检查指南,这是一套限制更正范围的规则。这些指南将在项目的各个阶段为我们提供帮助,从数据集生成到模型评估。
该指南特别用于创建拼写检查基准,这是一个经过精心策划的数据集,包含大约 300 个手动更正的成分列表。
这个基准是项目的基石。它使我们能够在我们的用例中评估任何解决方案,无论是机器学习还是简单的启发式方法。
它与评估算法一起使用,这是我们开发的自定义解决方案,能够将一组更正转化为可度量的指标。
评估算法
大多数现有的文本相关任务评估算法通过计算参考与预测之间的相似性来评估性能,例如用于语言翻译或总结的BLEU或ROUGE分数。
然而,在我们的案例中,这些指标并不完全适用。
我们希望评估拼写检查算法如何识别和修正成分列表中的正确单词。因此,我们将精确度和召回率指标适配到我们的任务中:
精确度 = 模型正确更正的单词数 / 模型所做的总更正数
召回率 = 模型正确更正的单词数 / 错误总数
然而,我们没有细粒度的视角来查看哪些单词应该被更正……我们只能访问:
-
原始数据:数据库中现有的成分列表;
-
参考数据:我们期望这个列表被更正的方式;
-
预测:模型的更正。
是否有方法计算被正确更正的错误数、拼写检查未能更正的错误,以及最终被错误更正的错误数?
答案是肯定的!
Original: "Th cat si on the fride,"
Reference: "The cat is on the fridge."
Prediction: "Th big cat is in the fridge."
根据上述示例,我们可以轻松地找出应该被更正的单词:The、is 和 fridge;以及哪些单词被错误更正:on 被更正为 in。最后,我们看到添加了一个额外的单词:big。
如果我们将这三组序列配对对齐:original-reference 和 original-prediction,我们可以检测出哪些单词应该被更正,哪些没有被更正。这种对齐问题在生物信息学中是典型的,称为序列对齐,其目的是识别相似性区域。
这是我们拼写检查评估任务的完美类比。
Original: "Th - cat si on the fride,"
Reference: "The - cat is on the fridge."
1 0 0 1 0 0 1
Original: "Th - cat si on the fride,"
Prediction: "Th big cat is in the fridge."
0 1 0 1 1 0 1
FN FP TP FP TP
通过为每一对标注0或1,表示单词是否发生了变化,我们可以计算模型正确修正错误的次数(真正例 — TP)、错误改变了正确单词的次数(假正例 — FP),以及漏掉应该修正的错误的次数(假负例 — FN)。
换句话说,我们可以计算拼写检查的精确度和召回率!
我们现在拥有一个强大的算法,能够评估任何拼写检查解决方案!
你可以在项目仓库中找到该算法。
大型语言模型
大型语言模型(LLMs)已被证明在各个行业中处理自然语言任务时提供了极大的帮助。
它们构成了我们必须为使用案例探索的路径。
许多 LLM 供应商在排行榜上炫耀他们模型的表现,但它们在修正配料清单中的错误表现如何呢?因此,我们对它们进行了评估!
我们使用我们的自定义基准测试和评估算法,评估了OpenAI的GPT-3.5和GPT-4o、Anthropic的Claude-Sonnet-3.5以及Google的Gemini-1.5-Flash。
我们提供了详细的指令,以便将修正方向定向到我们的自定义指南。

LLM 在我们的基准测试上的评估(图来自作者)
GPT-3.5-Turbo在指标和人工评审方面都提供了最好的性能,优于其他模型。特别提到的是Claude-Sonnet-3.5,它在修正错误方面表现出色(高召回率),但常常提供额外的无关解释,降低了其精确度。
太好了!我们有了一个有效的 LLM!是时候在应用程序中创建这个功能了!
好吧, 别急…
使用私有 LLM 会带来许多挑战:
-
缺乏所有权:我们变得依赖于供应商及其模型。新版本的模型频繁发布,改变了模型的行为。这种不稳定性,主要是因为该模型设计是为了通用目的,而非针对我们特定的任务,从而使长期维护变得复杂。
-
模型删除风险:我们没有防范措施来应对供应商删除旧模型的情况。例如,尽管 GPT-3.5 是完成此任务的最佳模型,但它正逐渐被更高效的模型替代!
-
性能限制:私有 LLM 的性能受限于其提示。换句话说,我们改进输出的唯一方式是通过更好的提示,因为我们无法通过在我们自己的数据上训练来修改模型的核心权重。
基于这些原因,我们选择将精力集中在开源解决方案上,这将使我们能够完全控制并超越通用 LLM。
训练我们自己的模型

模型训练工作流程:从数据集提取到模型训练(图来自作者)
任何机器学习解决方案都始于数据。在我们的案例中,数据就是经过修正的配料列表。
然而,并非所有的配料列表都是一样的。有些列表没有未识别的配料,而有些则难以阅读,甚至没有必要纠正它们。
因此,我们通过选择包含10%到 40%未识别配料的配料列表找到了一个完美的平衡。我们还确保数据集内没有重复项,并且与基准数据也没有重复,以防在评估阶段出现数据泄漏。
我们使用DuckDB从 Open Food Facts 数据库中提取了 6000 个未修正的列表,DuckDB 是一个快速的进程内 SQL 工具,能够在一秒钟内处理数百万行数据。
然而,这些提取的列表还没有被纠正,手动标注它们将需要大量时间和资源……
然而,我们可以访问已经在相同任务上进行评估的 LLMs。因此,我们提示 GPT-3.5-Turbo,这是我们基准上的最佳模型,按照我们的指南修正每一个列表。
该过程用了不到一个小时,费用几乎为2$。
然后,我们使用Argilla手动审查数据集,Argilla 是一个开源标注工具,专门用于自然语言处理任务。这个过程确保数据集质量足够高,以训练出可靠的模型。
现在我们手头有一个 训练数据集 和一个 评估基准 ,可以用来训练我们自己的拼写检查模型。
训练
对于这一阶段,我们决定使用序列到序列语言模型。换句话说,这些模型以文本作为输入,并返回文本作为输出,适合拼写检查的过程。
有几种模型适合这个角色,例如T5 系列,由谷歌于 2020 年开发,或当前的开源 LLMs,如Llama和Mistral,它们专为文本生成和执行指令而设计。
模型训练包括一系列步骤,每个步骤都需要不同的资源分配,例如云 GPU、数据验证和日志记录。出于这个原因,我们决定使用Metaflow来协调训练,它是一个专为数据科学和机器学习项目设计的管道编排工具。
训练管道的组成如下:
-
配置和超参数是从配置的 YAML 文件导入到管道中的;
-
训练任务通过AWS Sagemaker在云端启动,使用一组模型超参数和自定义模块(如评估算法)。一旦任务完成,模型工件将存储在 AWS S3 桶中。所有训练细节都通过Comet ML进行跟踪;
-
然后使用评估算法在基准上评估微调后的模型。根据模型的大小,这个过程可能非常漫长。因此,我们使用了vLLM,一个旨在加速 LLM 推理的 Python 库;
-
针对基准的预测结果,存储在 AWS S3 中,发送到Argilla进行人工评估。
在多次迭代数据精炼和模型训练之后,我们在拼写检查任务上达到了与专有 LLM 相媲美的性能,F1 分数为0.65。


LLMs 在我们基准上的评估(图像来源:作者)
该模型是微调后的Mistral-7B-Base-v0.3,可在 Hugging Face 平台上获取,并且是公开可用的,连同其数据集和评估基准。
[## openfoodfacts/spellcheck-mistral-7b · Hugging Face
我们正在努力通过开源和开放科学推动并普及人工智能。
huggingface.co](https://huggingface.co/openfoodfacts/spellcheck-mistral-7b?source=post_page-----d74dfe02e0e4--------------------------------)
此外,我们估计拼写检查减少了 11%的未识别成分,这很有前景!
现在进入项目的最后阶段:将模型集成到 Open Food Facts 中。
部署与集成
我们的模型很大!
70 亿个参数,这意味着在float16格式下运行时需要14 GB内存,还不包括20%的开销因子。
此外,大型模型通常意味着推理期间吞吐量低,这可能使其不适合实时服务。我们需要配备大内存的 GPU 来在生产环境中运行该模型,例如Nvidia L4,其配备了 24GB 的显存。
但在云端运行这些实例的成本相当昂贵……
然而,提供实时体验的可能性,而不需要 24/7 运行 GPU 实例,就是批处理推理。
成分列表定期通过模型进行批处理处理,然后存储在数据库中。这样,我们只需为批处理过程中使用的资源付费!
批处理作业
我们开发了一个批处理系统,通过Google Batch Job高效地处理大规模文本处理任务。

批处理系统(图像来源:作者)
该过程首先通过使用 DuckDB 从 Open Food Facts 数据库提取数据,在不到 2 分钟的时间内处理 43GB 的数据!
提取的数据随后发送到 Google Bucket,触发 Google 批处理作业。
该作业使用一个预先准备的 Docker 镜像,包含所有必要的依赖项和算法。为了优化资源密集型的 LLM 处理,我们重用了 vLLM,取得了令人印象深刻的表现,仅用一块 GPU L4 就能在 20 分钟内修正 10,000 个食材列表!
经过成功处理后,修正后的数据保存在一个中间数据库中,包含 Open Food Facts 中所有模型的预测,由Robotoff提供服务。
当贡献者修改产品详情时,他们将看到拼写检查的修正建议,确保用户仍然是 Open Food Facts 数据质量过程中的关键决策者。
该系统使 Open Food Facts 能够利用先进的 AI 能力来提高数据质量,同时保持其社区驱动的方法。

GCP 中的批处理作业(图片来自作者)
结论
在这篇文章中,我们带您了解了食材拼写检查的开发和集成,这是一个由 LLM 驱动的功能,用于修正 OCR 提取的食材列表。
我们首先制定了一套规则,拼写检查指南,来限制修正范围。我们创建了一个经过修正的食材列表基准,并通过自定义评估算法来评估任何解决方案。
在这种设置下,我们评估了各种私有 LLM,并确定GPT-3.5-Turbo是最适合我们特定用例的模型。然而,我们也展示了依赖私有 LLM 带来的显著限制,包括缺乏所有权和对如此大型模型(1750 亿个参数)进行改进或微调的机会受限。
为了应对这些挑战,我们决定开发我们自己的模型,并在从数据库提取的合成修正文本上进行微调。经过几轮迭代和实验,我们成功地通过一个开源模型达到了良好的表现。我们不仅匹配了私有 LLM 的性能,还解决了我们面临的所有权问题,使我们对模型拥有完全控制权。
我们随后将该模型集成到 Open Food Facts中,使用批处理推理部署,使我们能够定期处理成千上万的食材列表。预测结果存储在 Robotoff 数据库中作为洞察,然后由贡献者验证,将 OFF 的数据质量所有权留给贡献者。
下一步
拼写检查集成仍在进行中。我们正在设计用户界面,以提供机器学习生成的修正建议,并让贡献者接受、拒绝或修改这些修正。我们预计将在年底前完成该功能的完全集成。

用户验证的拼写检查已内置于Hugging Face Space
此外,我们计划通过持续的迭代改进来不断完善模型。通过提高训练数据的质量并结合用户反馈,可以显著提升其性能。这种方法将使我们能够持续微调模型,确保其始终保持高效,并与实际应用场景高度契合。
该模型及其数据集可以在官方Hugging Face 仓库中找到。用于开发该模型的代码可以在OpenFoodFacts-ai/spellcheck的 Github 仓库中找到。
感谢你阅读到这里!我们希望你喜欢这篇文章。
如果你也想为 Open Food Facts 做出贡献,你可以:
-
为 Open Food Facts 的GitHub做贡献:探索与您技能相匹配的开放问题,
-
下载 Open Food Facts 的移动应用:通过扫描条形码,轻松添加新产品或改进现有产品。
-
加入 Open Food Facts 的Slack,并与其他贡献者在 OFF 社区中开始讨论。
我们迫不及待地想看到你加入社区!
不要犹豫,查看我们的其他文章:
[## DuckDB 与 Open Food Facts:掌中最大的开放食品数据库 🦆🍊
利用 DuckDB 的强大功能,探索食品市场中最大的开放数据库。
计算机究竟是如何进行计算的?
数据科学新手的计算机硬件入门
·发表于Towards Data Science ·9 分钟阅读·2024 年 6 月 10 日
--
入门知识
你常常听到计算机被称为“数字计算机”。它们以远远超过人类能力的速度处理数据,并能每秒做出成千上万次逻辑决策。但它们是如何实现这一点的呢?这些非常昂贵的“石块盒子”有什么特别之处,竟能在某些方面超越其创造者的思维?这就是我们在本文中要解答的问题。

图片来源:Clément Hélardot 于Unsplash
这是我新系列的第二篇文章,《数据科学新手的计算机硬件入门》。本系列不假设读者具备任何物理学、电气工程或低级计算机科学的背景,旨在为希望加深对工作工具理解的新手(或有经验的)数据科学家提供计算机硬件的入门知识。毕竟,哪个优秀的工匠不想熟悉自己的工具呢?
你不需要先阅读系列中的前一篇文章就能跟得上本篇内容(不过,如果你想的话,可以在这里阅读:为什么计算机使用二进制?)。如果你对计算机的工作原理感兴趣,但没有太多相关背景知识,那么这个系列将是一个很好的入门选择。
计算机是如何记住东西的?
初学者数据科学家与计算机硬件的介绍
·发布于Towards Data Science ·12 分钟阅读·2024 年 6 月 20 日
--
介绍
你是否曾经想过计算机究竟是如何记住东西的?当然,你可能听说过它使用 RAM 作为“短期”记忆,而你的 HDD 或 SSD 则是“长期”记忆。但像这些设备是如何真正保存信息的呢?这就是我们在本文中要回答的问题。

图片由Markus Spiske提供,来源于Unsplash
作为数据科学家,我们通常使用的是高级工具。Python 很棒,但它为了简化工作,隐藏了许多让计算机运作的细节。然而,计算机是我们工作中的工具——难道我们不应该理解它们是如何工作的吗?本文是我系列文章《初学者数据科学家与计算机硬件的介绍》的第三篇。如果你错过了前两篇文章,不用担心——你不需要它们也能继续阅读,但它们会帮助你更深入地理解(如果你想阅读其他文章,可以在本文底部找到相关链接)。
本系列的目的是让新的(或有经验的)数据科学家了解他们每天使用的工具是如何运作的,而不需要具备物理学、电气工程学或低级计算机科学的知识。如果你对计算机如何运作感到好奇……
我们如何知道人工智能是否只是烟雾弹?
思考“人工智能革命”更像是印刷术还是加密货币。(剧透:两者都不是。)
·发布于Towards Data Science ·10 分钟阅读·2024 年 4 月 17 日
--

图片由Daniele Levis Pelusi提供,发布在Unsplash
我不是第一个坐下来真正思考人工智能的到来对我们世界意味着什么的人,但这是一个我仍然看到人们在提问和讨论的问题。然而,我认为大多数这些对话似乎都忽视了关键因素。
在开始之前,允许我给你讲三个小故事,这些故事展示了最近塑造我思考的关于这个问题的不同方面。
-
最近,我和我的财务顾问进行了一次对话。他提到,他所在机构的高层正在传播一个观点,认为人工智能是经济领域的重大变化,投资策略应该把它看作是革命性的,而不仅仅是一个炒作周期或昙花一现。他想知道我作为机器学习行业的从业者有什么看法。我告诉他,正如我之前对朋友和读者所说的,人工智能的炒作过于夸大,我们仍然在等待看到其中哪些是真实的。炒作周期仍在继续。
-
本周,我还听了《技术不会拯救我们》关于技术新闻和卡拉·斯威舍的这一集。 嘉宾 Edward Ongweso Jr.提到,他认为斯威舍有一个模式,就是在新技术刚出现时会显得非常轻信,但当这些新技术证明并不像承诺的那样令人印象深刻或革命性时,她就会改变立场(比如自动驾驶汽车和加密货币)。他认为这个现象再次发生了,这次是关于人工智能的。
-
我和我的伙伴都从事技术工作,经常讨论科技新闻。他曾经提到过一个现象:当某个特定的专家或技术思想家讨论的主题是你不太了解的领域时,你会觉得他们有非常深刻的见解,但当他们开始谈论你熟悉的领域时,你突然意识到他们的观点是完全错误的。你会回过头来想,“我知道他们在这个问题上是错的,那他们之前说的那些话也许也错了?”最近,我在机器学习这一主题上偶尔也会有这样的体验。
很难知道新技术如何最终发展,以及它们对我们社会的长期影响是什么。历史学家会告诉你,回顾过去时很容易认为“这是唯一可能的发展方式”,但实际上,在当时没有人知道接下来会发生什么,历史可能有无数种转折,任何一种转折都可能改变最终结果,其可能性与最终发生的事情一样大,甚至更大。
简而言之
人工智能不是彻底的骗局。机器学习确实为我们提供了自动化复杂任务和有效扩展的机会。AI也不会改变我们世界和经济的一切。它是一个工具,但在绝大多数情况下,它不会取代我们经济中的人类劳动。而且,AGI(通用人工智能)也不是一个现实的前景。
人工智能不是彻底的骗局。……人工智能也不会改变我们世界和经济的一切。
我为什么这么说呢?让我解释一下。
首先,我想说,机器学习相当棒。我认为,教会计算机解析那些人类自己无法真正理解的复杂模式是非常有趣的,而且它为计算机解决问题创造了大量机会。机器学习已经在各个方面影响着我们的生活,并且已经持续了很多年。当我构建一个模型,能够完成一个对人来说非常枯燥或几乎不可能完成的任务,并且这个模型被部署后能够解决同事们的问题时,那种满足感是非常强烈的。这只是生成性 AI 领域一些前沿技术的小规模版本,但它在同一个广泛的范畴下。
期望
与外行人交谈和与机器学习从业者交谈,会让你对人工智能的期待产生完全不同的理解。我以前写过关于这个话题的文章,但值得再次强调。我们期望人工智能为我们做些什么?当我们使用“人工智能”这个术语时,我们到底是什么意思?
对我来说,人工智能基本上是“使用机器学习模型来自动化任务”。就这样。 如果机器学习模型非常复杂,它可能帮助我们自动化一些复杂的任务,但即使是做相对狭窄任务的小模型,仍然是其中的一部分。我曾经详细讨论过机器学习模型真正做的事情,但简单来说:数学地解析并复制数据中的模式。所以,这意味着我们在使用模式的数学表示来自动化任务。人工智能是我们根据历史记录中的事件模式来选择接下来该做什么,无论是人们写的文本的历史,房价的历史,还是其他任何事情。
人工智能是我们根据历史记录中的事件模式来选择接下来该做什么,无论是人们写的文本的历史,房价的历史,还是其他任何事情。
然而,对许多人来说,人工智能意味着一些远比此更复杂的东西,甚至有些接近科幻的模样。在某些情况下,他们模糊了人工智能与通用人工智能(AGI)之间的界限,而在我们的讨论中,通用人工智能的定义也非常模糊。很多时候,我认为人们自己也不清楚这些术语具体意味着什么,但我感觉他们期望的是比现实所能提供的更复杂、更通用的东西。
例如,大型语言模型(LLM)理解人类语言的语法和句法,但对具体意义没有内在的概念。大型语言模型知道的所有内容都是内在的指代——“国王”对大型语言模型来说,仅仅通过它与其他词汇(如“女王”或“男人”)的关系来定义。所以,如果我们需要一个模型来帮助我们处理语言学或语义问题,那是完全可以接受的。让它为我们提供同义词,甚至为某个特定主题积累一段充满相关词汇的段落,这些词汇听起来非常符合人类的语言风格,它会做得很好。
但这与“知识”之间有着明显的区别。随便丢块石头,你就能找到一串社交媒体的讨论,讽刺 ChatGPT 总是搞错事实,时常产生幻觉。ChatGPT 不是,也永远不会是一个“产生事实的机器人”;它是一个大型语言模型。它做的是语言。知识甚至比事实更进一步,在这里,相关实体具有对事实意义的理解,以及更多的内容。按照我们当前使用的方法和技术,机器学习模型远未达到这一点,有些人称之为“通用人工智能(AGI)”。
知识甚至比事实更进一步,在这里,相关实体具有对事实意义的理解,以及更多的内容。按照我们当前使用的方法和技术,机器学习模型还远未达到这一点。
如果人们看到 ChatGPT 并期待 AGI,即某种具有与人类相当或更优理解信息或现实的机器学习模型,那是完全不现实的期望。(注:一些行业中的人会在公关中大肆宣传 AGI 即将到来,但当被问及时,他们会将 AGI 的定义退回到更简单的形式,以避免被追究他们自己的炒作。)
顺便说一下,我并不认为机器学习所做的事情和我们的模型所能做的事情属于与人类大脑运作相同的谱系。认为今天的机器学习可以导致 AGI 的观点假设人类智能是通过不断增加检测和利用模式的能力来定义的,虽然这确实是人类智能的一部分,但我并不认为这就是定义我们自己的唯一标准。
面对我对 AI 是否具有革命性的怀疑时,我的财务顾问提到了一个例子:快餐店在自驾车取餐时切换到语音识别 AI,以减少人类操作员无法理解顾客从车内说话的问题。这个例子可能很有趣,但并非启示。它是一个机器学习模型作为工具来帮助人们稍微更好地完成工作。正如我提到的,它让我们能够自动化小任务并减少一些人类劳动。然而,这并非生成性 AI 领域的独特之处!我们已经通过机器学习自动化任务并减少了十多年的人类劳动,将大语言模型(LLMs)加入其中只是程度上的不同,而非一种根本性的变化。
我们已经通过机器学习自动化任务并减少了十多年的人类劳动,将大语言模型(LLMs)加入其中只是程度上的不同,而非一种根本性的变化。
我的意思是,使用机器学习确实可以并且的确为我们在做很多事情的速度和效率上带来渐进的改善,但我们的期望应该基于对这些模型是什么以及它们不是什么的真实理解。
实际限制
你可能会认为我第一个论点是基于当前的技术能力和今天使用的训练方法,这是一个合理的观点。如果我们不断推动训练和技术,生产出越来越复杂的生成性 AI 产品呢?我们是否会达到某个时刻,创造出一些全新的东西,或许是备受推崇的“AGI”?难道天空不是无限的吗?
机器学习支持解决问题的潜力与我们实现这一潜力的能力是截然不同的。在拥有无限资源(资金、电力、芯片所需的稀土金属、人类生成的训练内容等)的情况下,我们可以通过机器学习获得一种层次的模式表示。然而,在我们所生活的真实世界中,所有这些资源都是有限的,我们已经遇到了一些资源的限制。
机器学习支持解决问题的潜力与我们实现这一潜力的能力有很大的不同。
我们已经知道多年,用于训练 LLM 的优质数据正在逐渐枯竭,而将生成的数据作为训练数据的尝试证明非常有问题。(感谢 Jathan Sadowski 发明了“哈布斯堡 AI”这一术语,指的是“一个系统过度依赖其他生成性 AI 输出进行训练,导致它成为一个近亲繁殖的变种,可能具有夸张、丑陋的特征。”)我认为还值得一提的是,我们在很多情况下辨别生成数据和有机数据的能力很差,因此我们可能甚至不知道在生成哈布斯堡 AI 的过程中,它的退化可能悄悄发生。
今天我将跳过讨论资金/能源/金属的限制,因为我计划写一篇关于 AI 对自然资源和能源影响的文章,但你可以跳转到 Verge 网站查看关于电力的精彩讨论。我想我们都知道,能源不是无限的资源,即便是可再生能源,而我们已经在为训练模型投入相当于小国的电力消耗——这些模型远未达到 AI 推销者所宣称的承诺。
我也认为,正如我之前写过的,针对 AI 公司的监管和法律挑战具有潜力,这必定会对它们能做的事情产生限制。没有任何机构应该凌驾于法律之上或没有限制,为了尝试创造 AGI 而浪费我们地球的所有自然资源将是令人憎恶的。
我的观点是,我们在理论上可以做的事情,假设有无限的银行账户、矿山和数据来源,并不等同于我们实际上能做的事情。我不认为即使没有这些限制,机器学习有可能实现 AGI,部分原因是我们训练的方式,但我知道在现实世界的条件下,我们无法实现类似的目标。
[我们在理论上可以做的事情,假设有无限的银行账户、矿山和数据来源,并不等同于我们实际上能做的事情。]
即使我们不担心 AGI,而仅仅集中精力关注我们目前实际拥有的那类模型,资源分配仍然是一个真正的关注点。正如我提到的,流行文化中所谓的 AI 其实只是“利用机器学习模型自动化任务”,这听起来并没有那么引人注目。更重要的是,这也揭示了这一工作的非统一性。AI 并不是单一的,它是遍布各地的无数小模型,插入到我们用来完成任务的工作流和管道中,这些模型都需要资源来构建、集成和维护。我们正在将大型语言模型(LLMs)作为潜在的选择,插入到这些工作流中,但这并不会让过程变得不同。
作为一个有经验的人,曾在争取商业支持、资源和时间来构建这些模型的过程中付出了努力,这并不像“我们能做到吗?”那么简单。真正的问题是“在面对竞争的优先事项和有限的资源时,这是否是正确的做法?”通常,构建一个模型并实施它来自动化任务,并不是花费公司时间和金钱的最有价值方式,许多项目最终会被搁置。
结论
机器学习及其结果非常令人振奋,它们在被妥善利用时,提供了解决问题和改善人类生活的巨大潜力。然而,这并不是新鲜事,且没有免费的午餐。机器学习在我们社会各个领域的应用将可能继续发展,就像过去十多年一样。将生成型人工智能加入工具箱,不过是一个程度上的差异。
到目前为止,AGI(通用人工智能)仍然是一个完全不同且完全虚构的存在。我甚至还没有触及是否我们希望 AGI 存在的问题,即使它有可能存在,但我认为这只是一个有趣的哲学话题,并非一个突发的威胁。(这是另一个话题,留待以后讨论。)但当有人告诉我,他们认为人工智能将在不久的将来完全改变我们的世界时,这就是我持怀疑态度的原因。机器学习可以大大帮助我们,并且已经帮助了我们很多年。像用于开发生成型人工智能的那些新技术,在某些情况下是有趣且有用的,但远不如我们被引导相信的那样,带来深远的改变。
查看我的更多作品:www.stephaniekirmer.com
参考文献
这些发明曾被认为将改变世界,但你可能从未听说过它们中的任何一个。
www.vox.com [## 7 个曾被认为是愚蠢时尚的世界改变发明
人们曾认为这些惊人的发明只是昙花一现。他们错了,错得离谱。
www.vox.com [## 我们可能会耗尽训练 AI 语言程序的数据]
研究人员可能需要发挥创造力,让训练数据尽可能使用更久。
计算生成性 AI 的能耗成本可能很复杂,但即使是研究人员所做的粗略估算也……
图像-文本多模态基础模型如何工作
了解图像-文本多模态模型如何执行图像分类、图像检索和图像字幕生成
·发布于Towards Data Science ·阅读时间:23 分钟·2024 年 6 月 1 日
--

图片来源:Bozhin Karaivanov 于Unsplash
现在,基于多模态的基础模型正在快速增长。它们能够理解不同类型的数据,包括文本、图像、视频、音频,并且可以执行需要多种数据模态知识的任务。你是否曾经想过这些模型是如何工作的?
理解多模态模型的关键是弄清楚它如何建立不同数据模态之间的对齐。
本文使用了一个简单的图像-文本双模态模型,名为CoCa,来解释这些模型的内部工作原理。我喜欢 CoCa,因为它的设计直观,借鉴了其他一些多模态模型的思想。
为什么要使用图像-文本模型?
为什么我们需要一个图像-文本双模态模型?在回答这个问题之前,让我们先来思考一下单模态模型能做什么,不能做什么。
仅图像模型
单一图像模态模型,例如ResNet,只学习图像信息,并可以执行基本的图像理解任务,比如图像分类——将图像分类到某一类别中……
温度如何影响 LLMs 中的下一个标记预测?
·发布于Towards Data Science ·阅读时间 4 分钟·2024 年 5 月 6 日
--
简而言之
1. 在温度为 1 时,概率值与标准 softmax 函数得出的概率值相同。
2. 提高温度会增加较不可能的标记的概率,从而扩展模型预测下一个标记的潜在候选范围(或多样性)。
3. 降低温度则会使最可能标记的概率接近 1.0,从而增强模型的信心。减少温度有效地消除了模型中的不确定性。
介绍
大型语言模型(LLMs)是多功能的生成模型,适用于广泛的任务。它们可以生成一致、可重复的输出,也可以通过将不太可能的单词组合在一起生成创造性内容。温度设置允许用户微调模型的输出,控制预测的可预见性程度。
让我们通过一个假设的例子来理解温度对下一个标记预测的影响。
我们让一个大型语言模型(LLM)完成句子“这是一个美妙的 _____。” 假设潜在的候选标记是:
| token | logit |
|------------|-------|
| day | 40 |
| space | 4 |
| furniture | 2 |
| experience | 35 |
| problem | 25 |
| challenge | 15 |
对数值通过 softmax 函数处理,使得值的总和等于 1。实际上,softmax 函数为每个标记生成概率估计。

标准 softmax 函数
让我们在 Python 中计算概率估计值。
import numpy as np
import seaborn as sns
import pandas as pd
import matplotlib.pyplot as plt
from ipywidgets import interactive, FloatSlider
def softmax(logits):
exps = np.exp(logits)
return exps / np.sum(exps)
data = {
"tokens": ["day", "space", "furniture", "experience", "problem", "challenge"],
"logits": [5, 2.2, 2.0, 4.5, 3.0, 2.7]
}
df = pd.DataFrame(data)
df['probabilities'] = softmax(df['logits'].values)
df
| No. | tokens | logits | probabilities |
|-----|------------|--------|---------------|
| 0 | day | 5.0 | 0.512106 |
| 1 | space | 2.2 | 0.031141 |
| 2 | furniture | 2.0 | 0.025496 |
| 3 | experience | 4.5 | 0.310608 |
| 4 | problem | 3.0 | 0.069306 |
| 5 | challenge | 2.7 | 0.051343 |
ax = sns.barplot(x="tokens", y="probabilities", data=df)
ax.set_title('Softmax Probability Estimates')
ax.set_ylabel('Probability')
ax.set_xlabel('Tokens')
plt.xticks(rotation=45)
for bar in ax.patches:
ax.text(bar.get_x() + bar.get_width() / 2, bar.get_height(), f'{bar.get_height():.2f}',
ha='center', va='bottom', fontsize=10, rotation=0)
plt.show()

带温度的 softmax 函数定义如下:

其中 (T) 是温度,(x_i) 是输入向量 (logits) 的第 (i) 个分量,(n) 是向量中分量的数量。
def softmax_with_temperature(logits, temperature):
if temperature <= 0:
temperature = 1e-10 # Prevent division by zero or negative temperatures
scaled_logits = logits / temperature
exps = np.exp(scaled_logits - np.max(scaled_logits)) # Numerical stability improvement
return exps / np.sum(exps)
def plot_interactive_softmax(temperature):
probabilities = softmax_with_temperature(df['logits'], temperature)
plt.figure(figsize=(10, 5))
bars = plt.bar(df['tokens'], probabilities, color='blue')
plt.ylim(0, 1)
plt.title(f'Softmax Probabilities at Temperature = {temperature:.2f}')
plt.ylabel('Probability')
plt.xlabel('Tokens')
# Add text annotations
for bar, probability in zip(bars, probabilities):
yval = bar.get_height()
plt.text(bar.get_x() + bar.get_width()/2, yval, f"{probability:.2f}", ha='center', va='bottom', fontsize=10)
plt.show()
interactive_plot = interactive(plot_interactive_softmax, temperature=FloatSlider(value=1, min=0, max=2, step=0.01, description='Temperature'))
interactive_plot
当 T = 1 时,

在温度为 1 时,概率值与标准 softmax 函数推导出的概率值相同。
当 T > 1 时,

提高温度会膨胀不太可能出现的标记的概率,从而扩大模型下一个标记预测的潜在候选范围(或多样性)。
当 T < 1 时,

降低温度则会使最可能的标记的概率接近 1.0,从而提高模型的信心。降低温度有效地消除了模型中的不确定性。
结论
大型语言模型(LLMs)利用温度参数为其预测提供灵活性。模型在温度为 1 时表现得可预测,紧跟原始的 softmax 分布。提高温度会引入更多的多样性,放大不太可能的标记。相反,降低温度则使预测更加集中,通过减少不确定性来增强模型对最可能标记的信心。这种适应性使得用户可以根据不同任务调整大型语言模型的输出,在创意探索和确定性输出之间找到平衡。
除非另有说明,所有图片均为作者提供。
Segment-Anything 模型(SAM)的解码器是如何工作的?
·发表于 Towards Data Science ·18 分钟阅读·2024 年 3 月 24 日
--

图片来自 dylan nolte 在 Unsplash
深入探讨 Segment-Anything 模型的解码过程,重点讲解其自注意力和交叉注意力机制是如何工作的。
本文只关注 SAM 的解码器。如果你对 SAM 的编码器感兴趣,请参考我的另一篇文章 “Segment-Anything 模型(SAM)的编码器是如何工作的?”。
Segment-Anything (SAM) 模型是一个 2D 交互式分割模型,或者说是引导模型。SAM 需要用户提示来分割图像。这些提示告诉模型应该在哪个位置进行分割。模型的输出是不同层级的分割掩码集合,并且每个掩码都有一个对应的置信度分数。
分割掩码是一个与输入图像大小相同的 2D 二进制数组。在这个 2D 数组中,位于 (x, y) 位置的条目如果模型认为该像素属于分割区域,则其值为 1,否则为 0。那些置信度分数表示模型对每个分割质量的信任程度,分数越高,质量越高。
SAM 的网络架构由编码器和解码器组成:
-
编码器接收图像和用户提示输入,生成图像嵌入、图像位置嵌入和用户提示嵌入。
-
解码器接收……
Segment-Anything 模型(SAM)的编码器是如何工作的?
深入探讨图像内容嵌入、正弦和余弦位置嵌入、引导点击嵌入以及密集掩码嵌入是如何生成的
·发布于 Towards Data Science ·阅读时间 16 分钟 ·2024 年 5 月 14 日
--

照片由 benjamin lehman 提供,来源于 Unsplash
本文解释了 Segment-Anything 模型(SAM)的编码器是如何工作的。SAM 使用 ImageEncoderViT 类来生成输入图像的内容嵌入。它还使用 PromptEncoder 类来生成以下嵌入:
-
引导点击,包括正面和负面
-
边界框
-
图像位置编码
-
图像密集掩码编码
ImageEncoderViT 是一个标准的 ViT 网络,所以我在这里不会详细解释。本文仅专注于 PromptEncoder 如何生成上述嵌入。那些嵌入会被传递给 SAM 的解码器,用于生成分割掩码。了解解码器如何使用这些嵌入会让理解编码过程更加容易。你可以通过阅读我另一篇文章 “Segment-Anything 模型(SAM)解码器是如何工作的?” 进一步了解 SAM 的解码过程。
编码引导点击
在 SAM 中,引导点击可以是正向的,告诉模型在哪里进行分割,或者是…
银河距离
我们距离外星文明有多远?(德雷克方程系列第四部分)
·发布于 Towards Data Science ·阅读时间:16 分钟·2024 年 9 月 8 日
--
总结:
在我们至今的旅程中,我们已经通过德雷克方程估算了银河系中可能存在的文明数量。我们探讨了星星、可能支持生命的行星,以及可能发展出智能文明的行星数量。在第三部分中,我们估算了那些文明中有多少能够与我们现在进行沟通。现在,在第四部分,我们面临一个重大问题:它们有多远?
银河系是浩瀚无垠的。我们知道外面可能有成千上万的文明,但银河系横跨 10 万光年——即使它们存在,我们能否听到它们的声音呢?本文探讨了我们与这些潜在文明之间令人震惊的距离,以及这对我们寻找外星生命意味着什么。

所有图片均由作者使用 Midjourney 开发。
宇宙有多大?将银河系的广阔与我们自身的视角对比
为了更好地理解空间的广阔,我们可以从想象银河系开始,它的直径约为 100,000 光年。现在,如果我们把银河系缩小到地球的大小(地球的直径约为 12,742 公里),那么地球本身的相对大小将变得难以想象。实际上,地球将比现在小 7420 亿倍——大约相当于一个人类红血球的大小。这个对比形象地展示了我们的银河系有多么庞大,也突显了跨越如此辽阔的距离进行探索或通讯的巨大挑战。试想,在一个相当于地球大小的空间中寻找一个红血球的困难!
第 8 步:估算到最近外星文明的距离
第三部分的结果表明,当前我们银河系中可能有多达2,363个外星文明正在进行通讯。但这在实际意义上意味着什么呢?如果它们存在,最近的一个到底有多近?
为什么这一步很重要
即使我们知道有成千上万的文明,距离仍然是一个巨大的障碍。它们越远,我们探测到它们信号的难度就越大——或者它们探测到我们的信号也会更加困难。如果我们想为与这些文明的接触设定现实的期望,我们需要知道它们的距离。如果它们距离我们几百甚至几千光年,那么要实时通信将会非常困难。试想发送一条短信,却要等上 2,000 年才得到回复!
我们的方法
我们假设外星文明在银河系中是随机分布的。这为我们提供了一个均匀分布的银河盘面,银河的直径约为 100,000 光年,厚度为 1,300 光年。
这种方法保持了简单性,同时也承认我们做出了一些重大假设。例如,我们没有考虑可能更“适合生命”的星系区域,也忽略了某些区域可能是荒芜的。但就我们的目的而言,这个模型为计算到最近文明的平均距离提供了坚实的基础。
第 8 步的代码:计算到最近外星文明的距离
***********************************************************************
*Calculate the distance to the closest civilization assuming that they
*are randomly distributed across the galaxy
***********************************************************************
data closest_civilization;
/* Set seed for reproducibility */
call streaminit(123);
/* Define Parameters */
%let num_civilizations = 2363;
%let num_iterations = 100;
%let galactic_radius = 50000;
%let galactic_height = 1300;
/* Initialize variables */
length iteration 8 distance 8;
array distances[&num_iterations]
/* Calculate distances */
do i = 1 to &num_iterations;
/* Generate random coordinates for civilizations */
do j = 1 to &num_civilizations;
x = rand("Uniform", -&galactic_radius, &galactic_radius);
y = rand("Uniform", -&galactic_radius, &galactic_radius);
z = rand("Uniform", -&galactic_height, &galactic_height);
/* Calculate distance from Earth */
distance = sqrt(x**2 + y**2 + z**2)
/* Update closest distance if applicable */
if j = 1 then closest_distance = distance;
else if distance < closest_distance then closest_distance = distance;
end;
/* Store closest distance */
distances[i] = closest_distance;
end;
/* Output distances to dataset */
do iteration = 1 to &num_iterations;
distance = distances[iteration];
output;
end;
/* Keep only the necessary variables */
keep iteration distance;
run;
一个小小的坦白:我作弊了(并且给了我们更好的机会)
好了,时间来一点小小的坦白:地球其实并不在银河系的中心,尽管我设置了代码来计算到最近外星文明的距离。实际上,我们大约处于银河系边缘的四分之三处。如果我把地球放置在它真正的位置,银河另一侧的外星文明的平均距离将会更大。但通过“作弊”将我们移到银河中心,我无意中减少了与潜在邻居的平均距离——进而提高了我们与外星文明接触的几率。所以,是的,这种有偏的做法对我们有利,但我相信你们不会介意让我让宇宙看起来稍微亲切一些!

解析代码:计算到最近文明的距离
这段代码旨在模拟并计算地球到最近外星文明的距离,假设文明在整个银河系中是随机分布的。让我们逐步解析代码的关键组件和逻辑。
关键变量概览
-
num_civilizations:该值设为 2,363,这是我们在第三部分估算的与我们同时通讯的文明数量。这个参数是我们计算的基础,因为我们要计算的是这些文明中最靠近我们的文明的距离。
-
银河半径和银河高度:这些参数定义了银河系的大小。银河系被建模为一个直径为 100,000 光年的盘面(即半径为 50,000 光年),并且厚度为 1,300 光年。
-
num_iterations:代码将运行 100 次模拟,这意味着它将随机分布这些文明并多次重新计算距离,以获得与最近文明的可能距离范围。
设置模拟
-
随机种子(
call streaminit(123)):这是为了保证可重复性。通过设置种子值(123),我们确保每次运行该代码时,都能获得相同的随机值,使得多次运行结果一致。 -
随机银河坐标:每个外星文明通过生成随机的 (x, y, z) 坐标在银河系中随机分布,其中 x 和 y 代表银河盘面内的位置(半径),z 代表相对于银河高度(其厚度)的位置。
rand("Uniform",...)函数在指定的范围内生成随机值: -
x 和 y 坐标从 -50,000 到 +50,000 光年的范围内选取,覆盖整个银河系的宽度。
-
z 坐标从 -1,300 到 +1,300 光年的范围内选取,以表示银河系的厚度。
计算地球到最近文明的距离
代码的下一部分使用 3D 距离公式,计算从地球(位于原点,[0, 0, 0])到每个随机分布的外星文明的距离:

该公式根据每个文明的随机银河坐标,计算地球到各个文明的直线距离。
寻找最近的文明
现在我们有了距离,代码将检查哪个距离是最近的:
-
第一文明:在第一次迭代时(当
j = 1时),计算出的第一个距离被存储为最近的距离,因为在这一点上,没有其他的距离可以进行比较。 -
后续文明:对于每个额外的文明,代码将其距离与之前存储的最近距离进行比较。如果新距离较小,则更新
closest_distance的值。
存储和输出结果
-
存储最近的距离:每次迭代中找到的最近距离存储在数组
distances[]中。这使我们能够跟踪每次模拟运行中的最近文明。 -
输出距离:最后的
do循环输出每次迭代的距离数据,因此我们可以分析这些结果的分布。它本质上生成了一个数据集,记录了所有 100 次模拟运行中的距离。
这段代码在做什么
这个模拟在每次迭代中为 2,363 个文明创建随机的银河坐标,计算每个文明到地球的距离,并确定最近的文明。它重复这一过程 100 次,基于我们做出的假设,给出了到最近外星文明的可能距离范围。
为什么这很重要
这是理解与外星文明接触这一实际挑战的关键步骤。即使有成千上万的文明,最接近的那个仍然可能远远超出我们的接触范围。通过模拟随机分布,我们可以大致了解我们需要跨越的最小距离,以便与另一个文明接触——这为寻找外星生命设定了现实的期望。
输出与解释:到最近文明的距离
现在来看结果!在运行模拟后,基于我们之前的计算,我们得到了以下关于距离最近外星文明的估算:

-
最小文明数量(1):39,594 光年之外。
-
平均文明数量(2,363):1,283 光年之外。
-
最大文明数量(500,299):214 光年之外。
这告诉我们什么?
-
可能的最近文明:如果我们假设有 50 万外星文明的乐观情景,那么最近的一个将距离我们约 200 光年。这是一个我们理论上可以通过现有技术探测到的距离范围,但仍然是一个非常难以实现的目标。
-
最可能的情景:根据我们对 2,363 个文明的平均估算,最近的文明可能位于1,283 光年之外。为了更好理解,这意味着我们今天发送的信号要到达它们需要超过一千年——而且我们要等到另一个千年过去,才能收到回复!
-
最坏情境:如果只有一个外星文明,它可能距离我们近 40,000 光年之遥,使得与之接触几乎不可能。
为什么这很重要
即使有成千上万外星文明的激动人心的可能性,我们之间的巨大距离仍然让人望而生畏。平均来说,最近的文明距离我们超过 1,000 光年,这意味着通信几乎不可能——至少在我们当前的技术条件下是这样。但这并不意味着寻找外星生命的努力是没有希望的。它只是意味着我们需要调整期望,并思考更多间接的方法来探测智能生命的迹象。
第 9 步:跨越宇宙的距离——相对论的作用
假设我们知道某个外星文明的位置。那么,达到那里需要多长时间?这就是更具挑战性的问题——因为当你谈论星际距离时,你必须考虑爱因斯坦的特殊相对论理论。
理解洛伦兹因子
在特殊相对论的核心是洛伦兹因子。这一概念解释了物体接近光速时,时间和空间如何变化,它是理解星际旅行如何运作的关键。
这是公式:

其中:
-
v是太空船(或任何在太空中运动的物体)的速度。
-
c是光速。
这个方程告诉我们,当物体的速度v接近光速c时,洛伦兹因子γ会变得更大。这会导致两个关键的效应:
-
时间膨胀:太空船上的旅行者的时间变慢。如果你以接近光速的速度穿越太空,你经历的时间会比地球上的人少。因此,从地球的角度来看需要几千年的旅行,可能对旅行者来说只会感觉像是几年的时间。
-
长度收缩:到达目的地的距离看起来变短了。随着你接近光速,太空船上的人会感觉到距离在缩短。
现在,让我们将其应用到我们的场景中。
第 9 步代码:计算旅行时间和洛伦兹因子
***************************************************************;
*How long is the travel time given different distances and ;
*different "% of speed of light" ;
***************************************************************;
/* Constants */
data _null_;
speed_of_light = 299792.458; /* km/s */
distance_ly = 100; /* Light years */
distance_km = distance_ly * 9.461 * 10**12; /* Conversion to kilometers */
travel_distance = distance_km;
travel_time = travel_distance / (0.9 * speed_of_light); /* Convert to seconds and reduce top speed of ship
to % of the speed of light*/
put "Travel Distance (km): " distance_km;
put "Travel Time (seconds): " travel_time;
velocity = travel_distance / travel_time;
lorentz_factor = 1 / sqrt(1 - (velocity**2 / (speed_of_light**2)));
proper_time_sp = travel_time / lorentz_factor; /* unit of time experienced on spaceship */
time_dilation_sp = proper_time_sp / (365.25 * 24 * 60 * 60); /* converting to years */
time_earth = travel_time / (365.25 * 24 * 60 * 60); /* converting to years */
put "Velocity: " velocity;
put "Lorentz Factor: " lorentz_factor;
put "Proper Time (Spaceship): " proper_time_sp " seconds";
put "Time Dilation (Spaceship): " time_dilation_sp " years";
put "Time (Earth): " time_earth " years";
run;
分析代码:计算旅行时间和时间膨胀
这段代码用于计算太空船以光速的某一分数,旅行从地球到最近的外星文明所需的时间。它还计算了旅行者由于爱因斯坦的特殊相对论理论所经历的时间膨胀。让我们逐步分析,理解这些计算如何运作以及它们的重要性。
常数和基本计算
- 光速:
- 光速,用
speed_of_light表示,是一个著名的常数,设定为 299,792.458 km/s。根据物理法则,这是任何物体的最大速度,也是我们旅行计算的基础。
- 到最近文明的距离:
- 我们将到最近文明的距离(
distance_ly)设置为 1,283 光年(一个占位符值)。这个数字代表了我们在早期步骤中估算的基于文明分布的距离。
- 将距离转换为千米:
- 由于光速是以 km/s 为单位,我们需要将光年转换为千米:

一光年约等于 9.461 万亿千米,因此这个转换将给出我们所需的千米总距离,这是旅行时间计算的关键。
计算旅行时间
- 光速的分数:
- 以下示例计算了一艘航天器以光速的 1%速度旅行的时间。用于计算旅行时间的公式是:

这个公式将总距离转换为航天器以光速的 1%速度覆盖该距离所需的时间。输出结果以秒为单位,从而提供最精确的计算。
2. 打印旅行距离和时间:
- 程序输出总距离(以公里为单位)和旅行时间(以秒为单位),让我们感受到所涉及的尺度。
特殊相对论与时间膨胀
在这部分代码中,我们重新审视了爱因斯坦的特殊相对论,重点讨论了时间膨胀如何与星际旅行相关。我们之前讨论了洛伦兹因子,它在确定接近光速的物体如何使时间变慢中起着至关重要的作用。
但为什么我们在这里使用洛伦兹因子?
洛伦兹因子为何重要
在星际旅行的背景下,当航天器的速度接近光速时,洛伦兹因子会显著增加。这会产生两个重要的影响:
-
时间膨胀:船员在航天器上经历的时间要比地球观察者经历的时间短得多。实际上,尽管从地球的角度看,旅程可能需要几个世纪或千年,但船员可能只经历几年时间。
-
距离感知:航天器旅行速度越快,船员感知到的距离越短。这种由洛伦兹因子引起的空间收缩意味着船员会感知到的旅程比实际的跨越太空的距离要短得多。
旅行时间的影响
通过引入洛伦兹因子,代码让我们能够计算出两个重要的结果:
-
航天器上的适当时间:船员在穿越广阔距离时所经历的时间。由于时间膨胀,这段时间将比地球上经过的时间要短得多。
-
地球上的时间:从地球视角出发,达到一个遥远外星文明所需的总时间。即使是高速旅行,旅程也可能持续几个世纪或更长时间。
这就是洛伦兹因子至关重要的原因:它帮助我们理解在相对论速度下,时间和空间是如何变化的,让我们对长时间的太空旅行可能对旅行者的影响有所了解,尽管从地球的角度看,这依然是极其具有挑战性的。
最终输出
代码输出几个关键见解:
-
速度:航天器接近光速的百分比时的速度。
-
洛伦兹因子:由于航天器的速度,时间膨胀的程度。
-
适当时间:船员所经历的旅程持续时间。
-
地球上的时间:在旅程进行时,地球上人们经历的时间。
这为何重要
这一部分突显了星际旅行的惊人复杂性。尽管航天员们会因时间膨胀而经历更短的旅行,但地球与潜在外星文明之间的距离依然是巨大的。洛伦兹因子展示了物理法则如何影响旅行速度的同时,也影响了时间和距离的感知。虽然时间膨胀为长距离旅行提供了潜在优势,但前往遥远文明的旅程仍然是一项艰巨的任务,需要远超我们目前所拥有的技术。
输出与解释:穿越宇宙距离
现在,我们来到了真相时刻——了解在浩瀚的宇宙和不同的旅行速度下,达到最近的外星文明需要多长时间。
前往最近文明的旅行时间
我们假设最近的外星文明距离地球 1,283 光年,这是基于我们之前的估算。接下来就是令人着迷的地方。旅行时间会根据航天器相对于光速的速度变化而大不相同。让我们来探讨这些数字:

-
以光速的 1%速度旅行:从地球的角度来看,这段旅程将需要惊人的128,303 年。为了让你理解这一点,如果这次旅行从那时开始,人类将和尼安德特人以及剑齿虎一起生活在冰河时代!
-
以光速的 10%速度旅行:这段旅程仍然需要12,830 年从地球的角度来看——那大约是冰河时代结束时,人类开始迁徙,像猛犸象这样的巨型动物正面临灭绝。
-
以光速的 50%速度旅行:我们开始看到更加可管理的数字。从地球的角度来看,这段旅程将需要2,566 年,这意味着当航天器出发时,人类正处于耶稣时代、古希腊、古埃及和佛教兴起的时期。
-
以光速的 90%速度旅行:现在我们有了进展!从地球的角度来看,这段旅程将需要1,425 年。想一想:当旅行者出发时,拜占庭帝国正处于鼎盛时期,唐朝在中国统治,玛雅文明正在繁荣。
-
以光速的 99%速度旅行:现在这段旅程只需要1,295 年从地球的角度来看。为了让你更好理解,维京人正在航海,盎格鲁-撒克逊王国正在英国的领土上建立。
但还有更多。这些数据显示的是从地球的视角来看时间,但对于航天器上的宇航员来说,因接近光速的旅行导致的时间膨胀意味着他们会经历更短的时间流逝。
为什么时间膨胀如此重要
随着航天器接近光速,时间膨胀成为旅行者的一个极大优势:
-
以光速的 50%的速度下,当地球上过去了2,566 年时,航天员只会经历2,222 年的时间流逝。
-
以光速的 90%的速度下,当地球上过去了1,425 年时,航天员只会感觉到621 年的时间流逝。这足以让旅程跨越多个人类世代,但也使得星际旅行的概念变得更加可行——至少对于宇宙飞船上的人来说。
-
以光速的 99%的速度下,这种效应变得更加戏剧化。虽然从地球的角度来看,旅程将需要1,295 年,但航天员只会经历183 年。
时间膨胀效应意味着,对于航天员来说,旅程可能感觉更短,尽管地球上可能已经过去了几个世纪或千年。这就是为什么狭义相对论在理解星际旅行可行性方面如此重要。它不仅关乎旅行需要多久——更关乎时间本身如何因旅行速度的不同而有所不同地体验。
为什么这很重要
这个模拟展示了星际旅行可能是可行的,但它并非没有巨大的挑战。时间膨胀为宇航员提供了优势,但浩瀚的距离意味着即便以接近光速的速度,旅程从地球的视角看仍可能需要几个世纪或更长时间。
距离、速度和时间的相互作用揭示了穿越宇宙的难度——但也揭示了它的迷人之处。虽然旅行者可能只经历了几百年的时间,但从地球的角度来看,他们的目的地可能依然距离地球上千年之遥。这提醒我们,在思考与外星文明接触的可能性时,必须考虑到极其庞大的尺度。
转向第五部分:最后的挑战——我们是否曾遇到过外星生命?
现在我们已经解决了文明之间的巨大距离问题,我们面临最后的挑战:我们是否已经遇到过外星生命却没有意识到?或者更令人兴奋的是,我们在未来遇到外星生命的几率有多大?在第五部分中,我们将深入探讨这些概率,探索过去和未来的相遇,并分析它们对人类未来和寻找外星智慧生命的意义。
敬请关注《德雷克方程式》系列的最终篇章!
本系列的下一篇:我们是否孤独?:与外星生命相遇的真实几率?(《德雷克方程式》系列第五部分)。或者,如果你错过了上一部分,请回去这里。
除非另有说明,所有图片均来自作者
MLX 的运行速度有多快?针对 10 款 Apple Silicon 芯片和 3 款 CUDA GPU 的全面基准测试
MLX、PyTorch MPS 和 CUDA GPU 上的主要操作和层的基准测试。
·发表于Towards Data Science ·6 分钟阅读·2024 年 2 月 2 日
--

作者提供的图片:软最大操作的基准测试示例
在首次发布不到两个月的时间里,Apple 的机器学习研究团队最新的创作——MLX,已经在机器学习社区取得了显著进展。令人惊讶的是,这个新框架的关注度如此之高,正如GitHub上的 12k 多颗星标和Hugging Face上超过 500 名成员的不断增长的社区所证明的那样 🤗。
在上一篇文章中,我们展示了 MLX 在训练简单的图卷积网络(GCN)时的表现,并将其与包括CPU、PyTorch 的MPS以及CUDA GPU在内的多种设备进行了基准测试。结果令人启发,展示了 MLX 在高效运行模型方面的潜力。
在这次探索中,我们深入探讨,旨在基准测试神经网络中常用的多个关键操作。
测试平台
在我们的基准测试中,每个操作都基于多种实验进行评估,这些实验在输入形状和大小上有所不同。我们已经在不同的进程中依次运行并多次测试这些操作,以确保稳定可靠的运行时度量。
谷歌如何利用你的数据来改进他们的音乐 AI
MusicLM 根据用户偏好进行了微调
·发表于 Towards Data Science ·阅读时长 7 分钟·2024 年 2 月 28 日
--

图片来自 Firmbee.com 由 Unsplash 提供
什么是 MusicLM?
MusicLM 是谷歌的旗舰文本到音乐的 AI,最初于 2023 年初发布。即使在其基础版本中,它也代表了一个重大的突破,并让音乐行业感到震惊。然而,几周前,MusicLM 进行了重大更新。以下是两个选定提示的并排比较:
提示:“带有旋律合成线和琶音的舞曲”:
-
旧版 MusicLM:
google-research.github.io/seanet/musiclm/rlhf/audio_samples/musiclm-7.wav -
新版 MusicLM:
google-research.github.io/seanet/musiclm/rlhf/audio_samples/musicrlhf-ru-7.wav
提示:“由手风琴乐队演奏的怀旧旋律”
-
旧版 MusicLM:
google-research.github.io/seanet/musiclm/rlhf/audio_samples/musiclm-27.wav -
新 MusicLM:
google-research.github.io/seanet/musiclm/rlhf/audio_samples/musicrlhf-ru-27.wav
质量的提高可以归因于谷歌研究发布的一篇新论文,标题为:“MusicRL: 将音乐生成与人类偏好对齐”。显然,这次升级被认为非常重要,以至于他们决定重新命名该模型。然而,在底层,MusicRL 在关键架构上与 MusicLM 是完全相同的。唯一的区别是:微调。
什么是微调?
从零开始构建 AI 模型时,它的知识是从零开始的,基本上是在进行随机猜测。然后,模型通过在数据上训练提取有用的模式,并随着训练的进行,开始表现出越来越智能的行为。这种方法的一个缺点是,从零开始训练需要大量数据。微调的理念是使用一个现有的模型,并将其调整到新任务上,或者调整它以不同的方式处理相同的任务。因为模型已经学习了最重要的模式,所需的数据量要少得多。
例如,一个像 Mistral7B 这样的强大开源 LLM,原则上任何人都可以从零开始训练。然而,要产生即使是稍微有用的输出所需的数据量是巨大的。相反,公司使用现有的 Mistral7B 模型,并为其提供少量专有数据,以使其能够解决新的任务,无论是编写 SQL 查询还是分类电子邮件。
关键 takeaway是微调不会改变模型的基本结构。它只是稍微调整其内部逻辑,以便在特定任务上表现得更好。现在,让我们利用这些知识来了解谷歌是如何在用户数据上微调 MusicLM 的。
谷歌是如何收集用户数据的
在 MusicLM 论文发布几个月后,作为谷歌 AI 测试厨房的一部分,发布了一个公共演示。在那里,用户可以免费试验文本到音乐的模型。然而,你可能听过这样一句话:如果产品是免费的,那么你就是产品。毫不奇怪,谷歌也不例外。在使用 MusicLM 的公共演示时,用户偶尔会遇到两个生成的输出,并被要求表明自己更喜欢哪一个。通过这种方式,谷歌能够在几个月内收集到300,000 个用户偏好。

以下是用户偏好评级的示例,这些数据在 MusicLM 公共平台中收集。图片摘自MusicRL 论文。
从截图中可以看到,用户并没有明确被告知他们的偏好将被用于机器学习。虽然这可能感觉不公平,但需要注意的是,我们在互联网上的许多行为都被用来进行机器学习训练,无论是我们的谷歌搜索历史、Instagram 的点赞,还是我们的私人 Spotify 播放列表。与这些个人且敏感的情况相比,MusicLM 平台上的音乐偏好似乎微不足道。
Linkedin 协作文章中的用户数据收集示例
需要注意的是,机器学习中的用户数据收集是随时发生的,通常是在没有明确同意的情况下。如果你在 Linkedin 上,你可能会被邀请参与所谓的“协作文章”。本质上,用户被邀请为自己擅长领域的问题提供建议。以下是一个关于如何写一首成功的民谣歌曲的协作文章(这是我以前从未知道我需要的)。

一篇关于写歌的协作文章的标题。在右侧,我被要求参与贡献以赚取“顶尖声音”徽章。
用户们被激励参与其中,获得平台上的“顶尖声音”徽章。然而,我的印象是其实没有人真正阅读这些文章。这让我相信,这些成千上万的问答对正被微软(Linkedin 的拥有者)用来训练一个专家级的人工智能系统。如果我的猜测准确,我会觉得这个例子比谷歌让用户提供他们最喜欢的曲目要更有问题。
但让我们回到 MusicLM!
Google 是如何利用这些用户数据的
下一个问题是,谷歌是如何利用这个庞大的用户偏好数据集来微调 MusicLM 的。秘诀在于一种叫做人类反馈强化学习(RLHF)的技术,这是 2022 年 ChatGPT 的一个关键突破。在 RLHF 中,利用人类的偏好来训练一个人工智能模型,该模型学习模仿人类的偏好决策,从而生成一个人工人类评分员。一旦这个所谓的奖励模型被训练完成,它就可以接收任意两首曲目,并预测哪一首更有可能受到人类评分员的喜爱。
在设置好奖励模型后,MusicLM 可以被微调,以最大化其输出的用户偏好预测。这意味着,文本到音乐的模型生成了成千上万的曲目,每首曲目都获得了奖励模型的评分。通过对模型权重的反复调整,MusicLM 学会了生成人工评分员“喜欢”的音乐。

RLHF 解释。图片来源于MusicRL论文。
除了在用户偏好上的微调外,MusicLM 还在另外两个标准上进行了微调:
1. 提示符合度
MuLan,Google 的专有文本到音频嵌入模型,用于计算用户提示与生成音频之间的相似度。在微调过程中,这一符合度得分被最大化。
2. 音频质量
Google 还基于用户数据训练了另一个奖励模型,用于评估其生成输出的主观音频质量。这些用户数据似乎是在不同的调查中收集的,而不是 MusicLM 的公共演示中的数据。
新版 MusicLM 到底好多少?
新的、经过微调的模型似乎在可靠性上超越了旧版 MusicLM,可以在演示页面上试听提供的样本。当然,精选的公共演示可能会让人产生误导,因为作者们有动力展示那些能够使新模型看起来尽可能优秀的例子。希望我们很快能在公共平台上测试 MusicRL。
然而,论文也提供了主观质量的定量评估。为此,Google 进行了一项研究,要求用户比较为同一提示生成的两首曲目,并为每首曲目打分,分数范围为 1 到 5。使用这种名为平均意见得分(MOS)的指标,我们不仅可以比较每个模型在直接比较中的获胜次数,还可以计算平均评分(MOS)。

定量基准。图像来源于MusicRL论文。
这里,MusicLM 代表原始的 MusicLM 模型。MusicRL-R 仅在音频质量和提示符合度上进行了微调。MusicRL-U 仅在人工反馈(奖励模型)上进行了微调。最后,MusicRL-RU 在三个目标上都进行了微调。毫不奇怪,MusicRL-RU 在直接比较中以及平均评分上都超越了所有其他模型。
论文还报告称,完全微调的模型 MusicRL-RU 在 87%的直接比较中胜过了 MusicLM。通过分析 MusicRL-R 和 MusicRL-RU 之间的直接比较,可以显示 RLHF 的重要性。在这里,后者的胜率为 66%,可靠地超越了其竞争对手。
这意味着什么?
尽管输出质量的差异是显著的,无论在定性还是定量上,新版 MusicLM 在大多数情况下仍然远未达到人类水平的输出。即使是在公共演示页面上,许多生成的输出也听起来很奇怪,节奏不对,未能捕捉到提示的关键元素,或是音色不自然。
在我看来,这篇论文仍然具有重要意义,因为它是首次尝试使用 RLHF 进行音乐生成。RLHF 在文本生成中已经广泛应用超过一年。但为什么这花了这么长时间?我怀疑收集用户反馈并进行模型微调是相当昂贵的。Google 可能发布了公开的 MusicLM 演示,主要目的是收集用户反馈。这是一个聪明的举动,让他们在竞争中领先于 Meta,后者也有同样强大的模型,但没有公开平台来收集用户数据。
总的来说,Google 通过借鉴 ChatGPT 的经过验证的微调方法,成功地将自己推到了竞争对手的前面。尽管即便采用了 RLHF,新的 MusicLM 仍未达到人类水平的质量,但 Google 现在能够保持并更新其奖励模型,通过相同的微调程序改进未来的文本到音乐模型。
看看其他竞争者,如 Meta 或 Stability AI,是否会赶超,什么时候赶超,应该会很有趣。对我们这些用户来说,这一切都是好消息!我们可以获得免费的公共演示和更强大的模型。
对于音乐人来说,当前发展的步伐可能有些让人感到威胁——而且这是有充分理由的。我预计在未来 1 到 3 年内,会看到人类水平的文本到音乐生成。我的意思是,文本到音乐的 AI 至少能像 ChatGPT 发布时那样,具备生成音乐的能力。音乐人必须了解 AI 及其如何在日常工作中已经能够支持他们。随着音乐行业再次遭到颠覆,好奇心和灵活性将是成功的关键。
对音乐 AI 感兴趣吗?
如果你喜欢这篇文章,你可能会想看看我的其他作品:
-
“2024 年期待的 3 项音乐 AI 突破”。Medium 博客
-
“生成性 AI 音乐现状如何?”。关于 Sync My Music 的 YouTube 访谈
-
“MusicLM — Google 解决了 AI 音乐生成问题了吗?” Medium 博客文章
你也可以在Linkedin上关注我,随时了解音乐 AI 的新论文和趋势。
感谢阅读本文!
参考文献
Agostinelli 等人,2023 年。MusicLM:从文本生成音乐。arxiv.org/abs/2301.11325
Cideron 等人,2024 年。MusicRL:将音乐生成与人类偏好对齐。arxiv.org/abs/2402.04229
数据科学面试在 4 年中如何变化?
对比过去和现在的总结:2020 年与 2024 年——一些主要的挫折和积极的收获。
·发表于Towards Data Science ·阅读时间 7 分钟·2024 年 12 月 14 日
--

是的,这实际上是我自己 LinkedIn 的截图[1]。
目录
-
介绍
-
申请过程
-
面试过程
-
总结
-
参考文献
介绍
本文适用于那些希望更换公司的数据科学家、考虑申请和面试成为数据科学家的人群,以及对竞争激烈的就业市场在几年间的变化感兴趣的读者。我将首先讨论 2020 年的申请过程,并将其与 2024 年进行对比,分享我的个人经验以及一些他人的轶事观察。接着,我们将深入探讨一旦公司接受你的申请后的面试过程。我不会透露我申请过的公司名称,以保持评论的匿名性,而是将总结从申请和面试多个不同公司的趋势。如果你希望了解更多关于我在数据科学就业市场中的经历,以及这些经历如何对你有所帮助或引发兴趣,请继续阅读。
我如何评估我的 Python 代码的内存消耗

由作者在 Canva 中创建
测量变量或函数内存消耗的不同方法
·发布于Towards Data Science ·6 分钟阅读·2024 年 5 月 28 日
--
在编写代码时,我们过去常常关注性能。如果我们搜索“python performance”,很容易找到成千上万篇文章。然而,我很少看到有文章告诉我们如何优化代码的内存消耗。
当然,性能在编程中至关重要,但内存消耗将决定我们需要在硬件上花费多少资金。试想我们要在一个大数据集上训练一个模型。我们可能需要一台 64GB 内存的虚拟机来完成这个任务。然而,如果我们能够优化我们的代码,32GB 内存可能就能实现类似的运行时间。那样的话,我们可能会为硬件资源节省大量成本。
在我优化代码以减少内存消耗之前,我们需要一些有效的方法来衡量它。此外,通过了解衡量内存消耗的方法,我们将能够量化并基准测试它。因此,在本文中,我将介绍我常用的一些内存测量方法。
1. Python 内置的“获取大小”方法
我是如何成为一名拥有破碎英语的国际学生数据科学家的
花了很长时间,但我找到了自己愿意做一辈子的事
·发表于Towards Data Science ·阅读时间 5 分钟·2024 年 4 月 9 日
--
目前,我是一名拥有超过 4 年经验的数据科学家。我想深入讲述自己的故事——这是一个逐步发现自己对数据科学的热情的过程。我花了相当长的时间才弄清楚自己的职业道路,但这段旅程无疑是值得的。我希望我的故事能激励那些可能在努力成为数据科学家或怀疑自己能力的人。

作者使用 Canva 制作的图片
到达美国时,英语水平有限
“我在一个毒品实验室工作”。这就是我刚搬到美国时英语有多差。我不知道“meth”和“math”之间有区别。大约花了一年时间,我才终于弄清楚这个区别。我并没有参与制毒;相反,我是在数学实验室担任数学辅导员,工作地点是旧金山城市学院(CCSF)。
在韩国高考失败后,我搬到了美国,坚信自己可以在这里建立新生活。我是一个大胆而天真的 18 岁女孩。我清晰地记得刚到旧金山市中心的几天,我甚至连在星巴克点一杯简单的咖啡都做不到。我结巴地说话,让员工一脸困惑,她用快速的英语回应我。我感到不知所措,最终用一种源自韩国文化的鞠躬表示感谢,匆忙离开了店铺。
在社区大学发现我对统计学的热情
为了提高我的英语水平,我报名参加了一所社区大学。在那里,我探索了各种学科,包括德语、游泳、计算机科学、数学、英语和统计学。在这个探索的过程中,我找到了我的热情。我的童年梦想是成为一名数学老师,因为我对数学和孩子们充满热爱。当我修读相关课程时,我发现学习统计学是一个重要的转折点。我爱上了统计学的各个方面,这最终促使我在转学时选择了统计学作为我的专业。

作者提供的图片,我的社区大学成绩单
成为第一代大学生
我的父母从未离开过韩国,更不用说上大学了。当我获得了多个大学的录取时,他们对加利福尼亚大学伯克利分校这样的名校并不熟悉。然而,他们愿意在经济上支持我,支付学费和生活费,为我能接受大学教育而感到自豪。

作者提供的图片,截图来自我的 UC 转学申请
努力追求精算职业
作为一名在加利福尼亚大学伯克利分校主修统计学的大三学生,我开始探索潜在的职业道路。我加入了统计学俱乐部和精算俱乐部,以获取更多的见解。最初,成为一名精算师似乎是一个合乎逻辑的选择——我喜欢解决数学问题,已经通过了几次精算考试,并完成了一次精算实习。然而,我在寻找全职精算职位时遇到了困难。尽管如此,我现在把这段经历视为一次学习的机会。回顾过去,我意识到我目前的工作比我当时在精算领域能找到的职位带给我更多的满足感。


作者提供的图片。我曾担任加利福尼亚大学精算联盟和本科统计学协会(目前称为统计学本科学生协会)的财务主管
商业数据科学简介
毕业时,我收到了两个工作机会:一个是初创公司的数据分析实习职位,另一个是电商公司的商品规划师职位。我选择了成为商品规划师,并有机会与那里的数据科学家合作。他们正在开发一个用于需求预测和定价的机器学习模型,这是我在加利福尼亚大学伯克利分校时非常喜欢学习的一个话题。目睹机器学习在商业中的实际应用让我感到非常有成就感。在寻求指导时,我找到了公司里的一位数据科学家。他建议我进一步深造,成为一名数据科学家,特别是在我作为国际学生面临签证挑战的情况下。
统计学硕士学位的价值(现在被称为应用数据科学)
我搬到了宾夕法尼亚州的 Pittsburgh,攻读卡内基梅隆大学(CMU)的硕士学位。在阿列克斯·赖因哈特教授的统计计算课程中,我提升了自己的 Python 技能,而在 Sivaraman·巴拉克里什南教授的数据挖掘课程中,我为机器学习面试做好了充分准备。我几乎每周都会去他们的办公时间,他们的帮助极大地促进了我的成长。我在 CMU 接受的优质教育,使我能够充分发挥自己的编程技能、项目经验和课程知识,成功地应对数据科学面试。

图片由作者提供
最终获得了 4 个数据分析师和数据科学家的工作邀请
这并不容易。在 4 到 5 个月的时间里,我申请了大约 700 个职位,目标是初级数据科学家岗位。我不知道哪些公司愿意雇佣外国员工,所以我全都申请了。尽管存在不确定性,我仍然收到了 20 多家公司的回复,并且与其中 4 家公司进入了最后阶段,最终都收到了工作邀请。
所学的经验
到达这个阶段花费了相当多的时间和精力,过程充满了挑战和不可预测性。求职之路充满了挫折和压力,但我对自己能力的信心支撑着我走下去,并最终让我在这一领域取得了成功。我坚信,任何具备正确技能和积极心态的人,都能够在数据科学领域开创自己的职业生涯。我希望我的故事能激励他人!
我是如何在没有“完美”学位的情况下成为 Meta 的数据科学家的
4 份工作和 2 次职业转型
·发布于 Towards Data Science ·阅读时间:13 分钟·2024 年 8 月 6 日
--

图片由 Muhammad Asyfaul 提供,来自 Unsplash
曾经梦想转行进入数据科学,但担心自己没有合适的背景?
也许你认为没有传统学位就落后于竞争对手?
或者你渴望进入一家大型科技公司工作,却觉得自己不够资格?
如果你也有过这些想法,别担心,你并不孤单。
我曾经也站在你的立场上。而且,我也与许多有志于成为数据科学家的朋友交谈过,他们面临着同样的疑虑。
这篇文章是写给那些想要进入数据科学领域,但没有典型资历的人们。 虽然它可能无法回答所有问题,但我希望它能为你提供清晰的思路和信心,帮助你追寻梦想中的职业。
这里是我旅程的一个概览,分为六个阶段:
-
发现
-
渴望
-
投资
-
第一次转型
-
第二次转型
-
机会敲门
你好!
我是 Mandy Liu,曾是 Meta 的数据科学家,拥有 8 年行业经验 😃
我是如何在加入 LinkedIn 之前成为一名数据科学家的
这些实践项目对职业转型至关重要
·发表于 Towards Data Science ·8 分钟阅读·2024 年 3 月 7 日
--

2023 年在 LinkedIn 总部与同事合影,已获授权使用(来源)
一个人是如何首次获得梦想中的数据科学家职位的?
我的梦想工作是成为 LinkedIn 数据科学部门的一员。
然而,我并没有接受过正式的数据科学教育,也没有担任过数据科学家的职务。当时,数据科学是一个新兴领域,它不像现在这样在学校里普及,也不像大多数公司现在那样普遍存在。
在我被 LinkedIn 招聘之前,我仍然需要证明自己能够胜任这项工作。
我很感激最终被 LinkedIn 招聘。我后来成为 LinkedIn 数据科学团队的负责人,带领由 14 位数据科学家组成的团队,负责获取和发展我们在数十亿美元规模的在线广告业务中的客户。
我将与大家分享我最初是如何开始从事数据科学工作的。
职业转型的规划
如果你还没有在数据科学领域工作过,获得第一份数据科学家的工作是很具挑战性的。也许你即将毕业,或者你已经在工程师、金融分析师或科学研究员的岗位上工作了一段时间,但现在想转行…
我是如何成为一名数据科学家的——没有计算机科学学位,没有训练营
我是如何从厌恶编程到成为一名完全合格的数据科学家的
·发表于Towards Data Science ·阅读时间 11 分钟·2024 年 1 月 6 日
--

毋庸置疑,成为一名数据科学家可以说是(以我谦虚的观点)目前最酷的工作之一,尤其是在今年人工智能的热潮之下。
在本文中,我想带你走过我成为数据科学家的整个过程,并分享一些如何让你自己也能成为数据科学家的建议和启示!
当然,成为数据科学家没有最好的方法,条条大路通罗马。然而,我认为有一些强烈推荐的策略,可以增加你获得理想的第一份工作的机会。
快速说明:我的目标是成为一名专注于机器学习的数据科学家。数据科学在不同公司有不同的定义,所以了解你自己想成为什么样的数据科学家很重要。然而,对于初级职位来说,这种定义的差异性并不大(这是一种英国俚语)。
我的背景
我是如何从零开始构建一个基于大型语言模型(LLM)的游戏
第一部分:游戏概念与 LLM 的因果图
·发表于Towards Data Science ·14 分钟阅读·2024 年 6 月 11 日
--

LLM 游戏的主页,由 Dalle 3 生成
引言
几个月前,我和一个朋友就大型语言模型(LLM)在游戏设计中的应用展开了辩论。
我的朋友认为 LLM 太不可预测,无法在游戏中稳定使用,不应该用于“实时机制”,而我则相信,如果通过正确的框架进行控制,它们可以提供创新的体验。
这场讨论促使我开始了一个新的副项目,并且这个项目已经成为我最为自豪的成就。
本系列文章旨在提供我在过去几个月中所做的所有工作的高级视角,以及截至目前我仍需面对的挑战。
以下是展示截至 2024 年 2 月的游戏短视频。
在本文中,我们将重点介绍游戏的核心概念,并详细说明我设想的架构,以应对我遇到的一些最大问题,例如信息泄露和幻觉。
我是如何构建 BeatBuddy:一款分析你的 Spotify 数据的 Web 应用
·发表于 Towards Data Science ·9 分钟阅读·2024 年 8 月 5 日
--

图像由 DALL·E 3 生成
你好,欢迎阅读这篇文章!我将向你解释我是如何构建 BeatBuddy 的,这是一款分析你在 Spotify 上收听内容的 web 应用。受 Spotify Wrapped 的启发,它旨在解读你当前的心情,并根据分析结果提供你可以调整的推荐。
如果你不想阅读所有内容,只是想试一试,你可以点击这里: BeatBuddy。其余内容请继续阅读!
项目的诞生
我是一名数据分析师,也是一个音乐爱好者,我相信数据分析是理解我们所生活的世界以及我们作为个体身份的一种强大方式。
音乐,尤其是,可以作为一面镜子,反映出你在某一时刻的身份和情感。你选择的音乐类型通常取决于你当前的活动和心情。例如,如果你在锻炼,你可能会选择一个充满活力的播放列表来激励自己。
另一方面,如果你正忙于学习或专注于处理一些数据,或许你会想听一些平静和宁静的音乐。我甚至听说过有人在集中注意力时听白噪声,这可以被描述为你在高速公路上打开汽车窗户时听到的声音。
另一个能反映你情绪的音乐例子是在聚会中。试想你和朋友们开派对,你需要选择音乐。如果是一个轻松的晚餐聚会,你可能想播放一些轻柔的爵士乐或者柔和的曲调。但如果你目标是那种每个人最后都跳到家具上或尽力演唱一首 80 年代热门歌曲的狂欢派对,你就需要选择一些充满活力、适合跳舞的歌曲。稍后我们会回到这些概念。
事实上,你听的所有音乐和你做出的选择可以揭示你在任何时刻个性和情感状态的有趣方面。如今,人们越来越喜欢分析自己,而这已经成为一种全球趋势!这个趋势被称为“量化自我”,是一个人们通过分析跟踪自己的活动(例如健身、睡眠和工作效率)以做出明智决定(或不做决定)的运动。
别误会,作为一个数据迷,我喜欢所有这些东西,但有时候它也有点过头——就像AI 连接的牙刷。首先,我不需要一把带 Wi-Fi 天线的牙刷。其次,我也不需要一张展示过去六周我刷牙情况变化的折线图。
不管怎样,我们回到音乐产业的话题。Spotify 是将用户数据收集转变为酷炫事物的先驱之一,他们称之为 Spotify Wrapped。

图 I :Spotify Wrapped 示例 | 图片来自作者
到了年底,Spotify 会整理你所听过的音乐并制作 Spotify Wrapped,这一内容会在社交媒体上迅速传播开来。它的受欢迎程度在于它能揭示你个性和偏好的一些方面,并且你可以与朋友们进行比较。
Spotify 如何收集和汇总数据以制作这些年终总结的概念一直让我着迷。我记得曾经问自己,“他们是怎么做到的?”而这种好奇心正是这个项目的起点。
嗯,实际上不完全是。让我们实话实说:分析 Spotify 数据的想法最初写在一张标有“数据项目”的便签上——你知道的,那种充满了你可能永远不会开始或完成的想法的便签。它就这样放了一年。
有一天,我又看了一下这个清单,凭借着对自己数据分析能力的新信心(感谢 ChatGPT 一年来的成长和进步),我决定选一个项目开始动手。
起初,我只是想访问和分析我的 Spotify 数据,并没有什么特别的目的。我只是单纯地好奇看看能用这些数据做些什么。
第一步:获取你的 Spotify 数据
开始这样一个项目时,你首先要问自己的是数据源在哪里,哪些数据是可以获得的。从本质上来说,有两种方法可以获取你的数据:
-
在隐私设置中,你可以请求一份历史数据的副本,但需要 30 天才能交付——这并不太方便。
-
使用 Spotify 的 API,它允许你按需获取自己的数据,并使用不同的参数来调整 API 调用,获取各种信息。
显然,我选择了第二种方式。为此,首先你需要创建一个开发者项目来获取 API 密钥,然后就可以开始了。
API 响应示例
记得我们讨论过某些曲目比其他曲目更容易跳舞的事实。作为人类,我们很容易感觉一首歌是否适合跳舞——这完全取决于你身体的感觉,对吧?但是计算机是如何判断这一点的呢?
Spotify 使用自己的算法来分析其目录中的每一首歌曲。对于每首歌曲,它们都会提供一系列与之相关的特征。这些分析的一个应用就是创建播放列表并为你提供推荐。好消息是,他们的 API 通过 audio_features 接口提供对这些分析的访问,允许你获取任何歌曲的所有特征。
例如,让我们分析一下著名歌曲《Macarena》的音频特征,我相信每个人都知道这首歌。我不会详细讲解曲目的每个参数,但我们可以集中关注一个方面,以更好地理解它是如何工作的——跳舞得分为 0.823。

图 II:Macarena 音频特征示例 | 作者提供的图像
根据 Spotify 的文档,跳舞得分描述了一首曲目基于多种音乐元素的适舞性,包括节奏、节奏稳定性、节拍强度和整体规律性。得分为 0.0 表示最不适合跳舞,1.0 则表示最适合跳舞。得分为 0.823(或 82.3%)意味着这首曲子非常适合跳舞。
三种时间维度
在继续之前,我需要介绍一个 Spotify API 中的概念——time_range。这个有趣的参数允许你通过指定 time_range 来检索不同时间段的数据:
-
short_term:过去 4 周的听歌活动
-
medium_term:过去 6 个月的听歌活动
-
long_term:你整个听歌活动的生命周期
让我们通过一个例子来说明:如果你想查看过去 4 周的前 10 首曲目,你可以调用相应的接口,并像这样传递 time_range 参数:api.spotify.com/v1/me/top/artists?time_range=short_term&limit=10
调用此接口将返回过去一个月的前 10 名艺术家。
步骤 2:解读结果
有了所有这些信息,我的想法是创建一个数据产品,让用户了解他们正在听什么,并通过比较不同的时间段来检测他们情绪的变化。通过这种分析,我们可以看到生活中的变化是如何反映在我们选择的音乐中的。
例如,我最近重新开始跑步,而这一变化影响了我的音乐偏好。现在,我听的音乐比以前通常听的更快、更有活力。当然,这是我的解读,但看到身体活动的变化如何影响我听的音乐,还是挺有趣的。
这只是一个例子,因为每个人的音乐之旅都是独一无二的,且可以根据个人经历和生活变化进行不同的解读。通过分析这些模式,我觉得能够将我们的生活方式选择与我们喜欢听的音乐之间建立联系非常酷。
使数据洞察变得触手可及
随着我深入这个项目,我越来越意识到,虽然我可以分析我的数据并得出某些结论,但我希望每个人都能做到这一点。
对我来说,与非技术人员分享数据洞察并让其变得极为可及的最简单方法不是通过华丽的 BI 仪表盘。我的想法是创建一些每个人都能轻松访问的东西,这促使我开发了一个移动友好的网页应用,任何人都可以使用。
要使用这个应用,你只需要一个 Spotify 账户,点击一个按钮将其连接到 BeatBuddy,就完成了!

图 III:应用界面示例 | 作者提供的图片
测量音乐情感
让我们看一下这个应用的另一个功能:测量你所听音乐的快乐程度,这可能反映你当前的情绪状态。该应用聚合了你最近的热门歌曲数据,专注于“情感值”参数,它代表了音乐的幸福感,1 表示超级快乐的音乐。例如,如果你当前歌曲的平均情感值是 0.432,而你所有时间的平均值是 0.645,这可能意味着你最近的音乐偏向更为忧郁。
然而,这些分析应该谨慎对待,因为这些数字代表的是趋势,而非绝对真理。有时候,我们不应总是试图找出这些数字背后的原因。
例如,如果你正在追踪你的步伐速度并发现你最近走得更快,这并不一定意味着你更急躁——这可能是由天气变化、新鞋子或只是潜意识的转变等各种小因素导致的。有时候,变化发生时并没有明确的原因,虽然可以衡量这些变化,但它们并不总是需要直接的解释。
话虽如此,注意到你的音乐听取习惯发生了显著变化是很有趣的。这可以帮助你思考你的情绪状态或生活情况可能是如何影响你的音乐偏好的。BeatBuddy 的这一方面提供了一个有趣的视角,尽管值得注意的是,这些解读只是我们情感和经历复杂拼图中的一部分。
步骤 3:做出数据驱动的决策
说实话,分析你的听歌习惯是一回事,但如何基于这些分析采取行动呢?归根结底,数据驱动的决策是数据分析的最终目标。这就是推荐系统发挥作用的地方。
根据你选择的心情推荐
BeatBuddy 的一个有趣功能是它能够根据你选择的心情和你喜欢的音乐提供个性化的音乐推荐。
比如,你可能会意识到自己听的音乐流行度得分为 75%(相当高),你希望根据自己的口味寻找一些隐藏的宝藏。你可以调整“流行度”滑块,比如设置为 25%,以创建一个流行度平均得分为 25% 的新播放列表。

图 IV:将流行度滑块调整至 25% | 图片来自作者
在后台,系统会调用 Spotify 的算法,基于你选择的标准生成推荐。这个调用会根据你的个人口味和你设置的心情参数生成个性化的播放列表推荐。它会使用你最近的 5 首歌曲来根据你的选择微调 Spotify 的推荐算法。

图 V:API 端点说明 | 图片来自作者
一旦你对播放列表满意,你可以直接将其保存到你的 Spotify 库中。每个播放列表都附带有描述,详细说明你选择的参数,帮助你记住每个播放列表所要唤起的情感氛围。

图 VI:将播放列表保存到 Spotify | 图片来自作者
最终思考
开发一个分析 Spotify 数据的网页应用程序是一个充满挑战但回报丰厚的过程。我被迫走出舒适区,学习了多个领域的知识,包括网页 API、cookie 管理、网页安全、OAuth2、前端开发、移动优化和 SEO。下面是该应用程序的高层架构图:

图 VII:高层架构 | 图片来自作者
我最初的目标是启动一个简单的数据项目,分析我的听歌习惯。然而,它变成了一个为期三个月,充满学习和发现的项目。
在整个过程中,我意识到数据分析和网页开发是密切相关的,特别是当我们需要提供一个不仅具有功能性,而且用户友好、易于访问的解决方案时。最终,软件开发本质上是将数据从一个地方移动到另一个地方。
最后一个说明:我希望创建一个简洁、无缝的用户体验应用。这就是为什么 BeatBuddy 完全没有广告,且没有任何数据被出售或与第三方共享。我创建这个应用的唯一目的是为用户提供一种更好地理解自己音乐选择和发现新曲目的方式。
你可以在这里试用这个应用:www.beatbuddy.cloud
如果你有任何意见或建议,我愿意倾听!你的反馈非常重要。
对于那些有兴趣深入了解的人,请留意我即将发布的文章。
干杯!
拉扎尔
如何构建我的第一个 RAG 流水线
一个 RAG 流水线来回答你所有招聘人员的问题!
·发布于 Towards Data Science ·阅读时长 5 分钟·2024 年 8 月 2 日
--
大型语言模型的幻觉问题即使对于像 Google 这样的科技巨头也一直存在(只需问 Gemini 每天推荐吃多少块岩石… 剧透警告,答案是每天一块)。虽然我们仍然不知道如何教大型语言模型常识,但我们可以做的是为其提供足够的上下文,针对你的具体应用场景。这就是检索增强生成(RAG)派上用场的地方!在这篇文章中,我将带你了解 我是如何实现一个 RAG 流水线,让它可以读取我的简历并为我与招聘人员对话的!
嘘!如果你没有会员资格,你可以在这里阅读这篇文章。
什么是检索增强生成(RAG)?
首先,让我们打好基础,确保理解什么是 RAG 以及它是如何工作的。简而言之,检索增强生成(RAG)是一种技术,其中大型语言模型(LLM)生成答案时,通过检索从知识库中获得的相关信息来增强其回答。RAG 流水线从你的私有数据中挑选出最相关的文本片段,让大型语言模型与提示一起阅读并生成答案。例如,在这篇文章中,我正在构建一个简单的聊天机器人,它可以回答招聘人员的问题。为了让大型语言模型准确地完成任务,我必须“告诉”它我是谁。通过使用 RAG 流水线,我可以让它检索我简历中最相关的部分,以回答每一个招聘人员的问题,从而增强大型语言模型的…
我如何按照 CRISP-DM 生命周期创建数据科学项目
一个使用 CRISP-DM 框架的端到端项目
·发表于 Towards Data Science ·22 分钟阅读·2024 年 11 月 13 日
--

由 AI 生成的图像。OpenAI。CRISP-DM 数据科学项目生命周期框架。DALL-E,2024 年。
介绍
CRISP-DM 代表跨行业数据挖掘标准过程,这是一个开放的数据挖掘框架,任何人都可以使用它。
它的第一个版本由 SPSS、戴姆勒-奔驰和 NCR 创建。后来,一组公司开发并演变成了 CRISP-DM,如今它已经成为数据科学领域最著名和最广泛采用的框架之一。
该过程由 6 个阶段组成,且具有灵活性。它更像是一个活的有机体,你可以(而且可能应该)在各个阶段之间来回迭代与改进,提升结果。
该过程包括以下几个阶段:
业务理解
数据理解
数据准备
建模
评估
部署
小箭头展示了从业务理解到部署的自然路径——直接发生交互的地方——而圆圈则表示阶段之间的循环关系。这意味着项目并不会在部署后结束,而是可以因为项目触发的新业务问题而重新启动,或者…
我是如何使用 Streamlit 为我的学生创建类似 Kaggle 的平台,以及你也可以如何做到这一点
使用 Streamlit 和 Google Sheets 将机器学习学生项目游戏化
Luis Fernando PÉREZ ARMAS, Ph.D.
·发表于 Towards Data Science ·24 分钟阅读·2024 年 6 月 20 日
--

排行榜和积分可以成为一种很好的动力来源(图片由 DALLE-3 生成)
我喜欢 Kaggle,并且相信它在传播数据科学和机器学习方面的贡献是无价的。作为一个视频游戏和游戏化的爱好者,我非常欣赏 Kaggle 的排名和积分系统,它鼓励参与者通过健康的、建设性的竞争来提升自己的模型。Kaggle 已经变得如此受欢迎,以至于许多讲师将 Kaggle 作为他们教授机器学习时的首选工具。
然而,作为一名商学院讲师,我教授一些机器学习课程,我发现将 Kaggle 作为评估学生最终机器学习项目的工具有些繁琐。首先,跟踪学生的提交工作是繁重且手动的,而且我的学生(请注意,他们大多数是数据科学和编程的初学者)经常会感到沮丧,因为他们的努力成果排名总是排在 Kaggle 排行榜的底部。尽管如此,我认为有必要承认 Kaggle 并非为教学工具而设计,这也解释了它在这一背景下的局限性。
我如何应对 AI 初创公司中的幻觉问题
以及弱基础与强基础的区别
·发表于Towards Data Science ·6 分钟阅读·2024 年 9 月 22 日
--

图片由作者提供
我是一名 AI 工程师,专注于一个特定的细分领域:文档自动化和信息提取。在我的行业中,使用大型语言模型在应对幻觉问题时遇到了一些挑战。想象一下,当 AI 将发票金额错误地解读为 $100,000 而不是 $1,000,导致支付超额 100 倍。当面对这种风险时,防止幻觉成为构建稳健 AI 解决方案的关键方面。这些是我在设计可能容易产生幻觉的解决方案时专注的一些关键原则。
使用验证规则和“人类介入”。
有多种方法可以将人工监督融入 AI 系统中。有时,提取的信息总是会展示给人工进行审核。例如,解析后的简历可能会在提交给申请人跟踪系统(ATS)之前展示给用户。更常见的是,提取的信息会自动添加到系统中,只有在出现潜在问题时才会标记并供人工审核。
任何 AI 平台的关键部分之一是确定何时需要加入人工监督。这通常涉及不同类型的验证规则:
1. 简单规则,例如确保行项目总额与发票总额匹配。
2. 查找和集成,例如在会计系统中验证总金额是否与采购订单一致,或验证付款细节是否与供应商的历史记录匹配。

需要人工介入时的一个示例验证错误。来源:Affinda
这些过程是好事。但是,我们也不希望一个 AI 总是触发安全保障并迫使人工干预。如果 AI 不断触发这些安全措施,幻觉就会破坏使用 AI 的目的。
小型语言模型
防止幻觉的一个解决方案是使用小型语言模型(SLM),这些模型是“提取式的”。这意味着模型会标记文档的部分内容,我们将这些标签收集成结构化的输出。我建议尽可能使用 SLM,而不是每个问题都默认使用 LLM。例如,在简历解析中,等待超过 30 秒让 LLM 处理一份简历通常是不可接受的。对于这种用例,我们发现 SLM 可以在 2 到 3 秒内提供比像 GPT-4o 这样的较大模型更高的准确性。
我们流程中的一个例子
在我们的初创公司中,一份文档可以由最多 7 个不同的模型处理——其中只有 2 个可能是大型语言模型(LLM)。这是因为大型语言模型并不总是完成任务的最佳工具。一些步骤,例如检索增强生成(Retrieval Augmented Generation),依赖于一个小型的多模态模型来创建有用的嵌入用于检索。第一步——检测某个内容是否为文档——使用一个小型且超快速的模型,该模型的准确率达到 99.9%。将问题分解成小块,然后弄清楚哪些部分最适合由 LLM 来处理,这是至关重要的。这样,你就能减少幻觉发生的几率。
区分幻觉与错误
我特别区分幻觉(模型编造信息)和错误(模型误解现有信息)。例如,选择错误的金额作为收据总额是一个错误,而生成一个不存在的金额则是幻觉。提取式模型只能犯错误,而生成式模型则可能同时犯错误和幻觉。
风险容忍度与基础
在使用生成模型时,我们需要一种方法来消除幻觉现象。
基础指的是任何迫使生成 AI 模型通过参考某些权威信息来为其输出提供证明的技术。如何管理基础问题取决于每个项目的风险容忍度。
例如——一个具有通用收件箱的公司可能会试图识别需要执行的任务。通常,要求执行的电子邮件会直接发送给账户经理。一个充满发票、垃圾邮件和简单回复(“谢谢”,“好的”等)的通用收件箱,包含了太多的邮件,人工很难逐一检查。当任务错误地被发送到这个通用收件箱时,会发生什么呢?任务经常会被忽略。如果一个模型犯了错误但总体上准确,它已经比什么都不做要好。在这种情况下,容忍错误/幻觉的程度可以较高。
其他情况下可能要求特别低的风险容忍度——例如财务文档和“直通处理”。这是指提取的信息会在没有人工审核的情况下自动添加到系统中。例如,一家公司可能不允许自动将发票添加到会计系统,除非(1)支付金额与采购订单中的金额完全匹配,并且(2)支付方式与供应商的上一笔支付方式一致。
即使风险较低,我仍然倾向于谨慎行事。每当我专注于信息提取时,我会遵循一个简单的规则:
如果从文档中提取文本,则必须与文档中找到的文本完全一致。
当信息是结构化的(例如表格)时,这变得很棘手,尤其是因为 PDF 文件没有任何关于页面上单词顺序的信息。例如,某个条目的描述可能会跨越多行,因此目标是围绕提取的文本绘制一个连贯的框,而不考虑单词的左右顺序(或在一些语言中是从右到左的顺序)。
强制模型指向文档中的精确文本被称为“强绑定”。强绑定不限于信息提取。例如,客户服务聊天机器人可能需要从内部知识库中的标准化回复中逐字引用(照搬)。鉴于标准化回复可能无法真正回答客户的问题,这种做法并不总是理想的。
另一个棘手的情况是当信息需要从上下文中推断时。例如,医疗助手 AI 可能会根据症状推断出某种疾病的存在,而没有明确指出该疾病。识别出这些症状提到的地方将是一种“弱绑定”。回应的理由必须存在于上下文中,但确切的输出只能从提供的信息中合成。进一步的绑定步骤可能是强制模型查找医疗状况,并证明这些症状是相关的。即使如此,仍可能需要弱绑定,因为症状通常可以以多种方式表达。
复杂问题的绑定
使用 AI 解决日益复杂的问题可能使得使用绑定变得困难。例如,如果模型需要进行“推理”或从上下文中推断信息,如何进行绑定呢?以下是向复杂问题添加绑定时的一些考虑事项:
-
识别可以分解成一组规则的复杂决策。与其让模型生成最终决策的答案,不如让它生成该决策的组成部分。然后,使用规则来显示结果。(警告——这有时会加剧幻觉问题。向模型提出多个问题会给它多个产生幻觉的机会。只问一个问题可能会更好。但我们发现目前的模型在复杂的多步骤推理中通常表现较差。)
-
如果某个内容可以用多种方式表达(例如症状的描述),第一步可以是让模型对文本进行标注并标准化它(通常称为“编码”)。这可能为更强的基础打下基础。
-
为模型设置“工具”以调用,约束输出到非常特定的结构。我们不希望执行 LLM 生成的任意代码。我们希望创建模型可以调用并对这些工具中的内容设定限制的工具。
-
在可能的情况下,包括工具使用的基础支持——例如,在将响应发送到下游系统之前,先验证它们与上下文的一致性。
-
有没有方法验证最终输出?如果手工规则不可行,我们是否可以为验证构建一个提示?(并且同样遵循上述规则来验证模型)。
主要结论
-
在信息提取方面,我们不容忍输出中没有原始上下文的信息。
-
我们随后会进行验证步骤,以捕捉错误和幻觉。
-
我们所做的一切,超出此范围的内容,都是关于风险评估和风险最小化。
-
将复杂问题分解成更小的步骤,并识别是否需要 LLM。
-
对于复杂问题,使用系统化的方法来识别可验证的任务:
— 强基础支持迫使 LLM 从可信来源逐字引用。总是首选使用强基础支持。
— 弱基础支持迫使 LLM 引用可信来源,但允许合成和推理。
— 当一个问题可以分解成更小的任务时,尽可能在任务上进行强基础支持。
Affinda AI 平台
我们已经构建了一个强大的AI 文档处理平台,全球各地的组织都在使用它。
关于作者
我是 Affinda 的首席 AI 工程师。我花了 10 年时间做出职业转型,从 UX 转向 AI。关于我从 UX 到 AI 的职业转型。想深入了解生成式 AI 吗?阅读我的深度分析:大型语言模型实际上理解什么。
如何将 Apache Flink、Kafka 和 PostgreSQL Docker 化,实现实时数据流处理
使用 Docker 集成 pyFlink、Kafka 和 PostgreSQL
·发表于 Towards Data Science ·阅读时间:10 分钟·2024 年 6 月 19 日
--

使用 Docker 准备你的 pyFlink 应用程序 — 作者使用 www.dall-efree.com/ 生成的图片
为什么要阅读这篇文章?
-
真实世界的洞察:从我个人克服集成难题的经历中获取实用的建议。
-
完整设置:学习如何使用 Docker-Compose 无缝集成 Flink、Kafka 和 PostgreSQL。
-
逐步指南:非常适合初学者和有经验的开发者,帮助你简化数据流处理架构。
设置场景
我开始了一个任务,旨在使用 Docker 集成 Apache Flink、Kafka 和 PostgreSQL。这项工作尤其令人兴奋,因为我使用了 pyFlink —— Flink 的 Python 版本 —— 它既强大又相对罕见。这个设置的目标是高效地处理和存储实时数据。在接下来的部分,我将展示我是如何实现这一目标的,讨论我遇到的挑战以及如何克服它们。最后,我将提供一个逐步指南,帮助你自己搭建并实验这个数据流管道。
我们将要构建的基础设施如下所示。从外部来看,有一个发布者模块,用于模拟物联网传感器消息,类似于之前的文章中讨论的内容。在 Docker 容器内部,我们将创建两个 Kafka 主题。第一个主题 sensors 用于实时存储来自物联网设备的消息。Flink 应用程序将从该主题消费消息,过滤出温度高于 30°C 的消息,并将其发布到第二个主题 alerts。此外,Flink 应用程序还将把消费的消息插入到专门为此目的创建的 PostgreSQL 表中。这个设置使我们能够以结构化的表格格式持久化传感器数据,提供进一步转化和分析的机会。像 Tableau 或 Power BI 这样的可视化工具可以连接到这些数据,用于实时绘图和仪表盘展示。
此外,alerts 主题可以被其他客户端消费,以根据其包含的消息启动相应的动作,例如启用空调系统或触发消防安全协议。

docker 容器中包含的服务 —— 图片由作者提供
为了跟随本教程,你可以克隆以下仓库。项目根目录中包含一个 docker-compose.yml 文件,便于你初始化多容器应用程序。此外,你可以在 README 文件中找到详细的说明。
docker-compose.yml 中 Kafka 端口的问题
最初,在使用 confluentinc Kafka Docker 镜像时,我遇到了 Kafka 端口配置的问题,这是这种设置的常见选择。通过日志可以明显看到这个问题,突显出在初始设置和故障排除阶段,不应该以分离模式(-d)运行 docker-compose up 的重要性。
失败的原因是内部和外部主机使用了相同的端口,导致了连接问题。我通过将内部端口更改为 19092 来解决了这个问题。我发现这篇博客文章对问题的解释相当清晰。
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:19092,PLAINTEXT_HOST://localhost:9092
配置 Flink 会话模式
要在会话模式下运行 Flink(允许在一个集群中运行多个作业),我在 docker-compose.yml 文件中使用了以下指令。
PyFlink 的自定义 Docker 镜像
鉴于默认的 Apache Flink Docker 镜像不包括 Python 支持,我为 pyFlink 创建了一个自定义 Docker 镜像。这个自定义镜像确保 Flink 可以运行 Python 作业,并包含与 Kafka 和 PostgreSQL 集成所需的依赖项。用于创建此镜像的 Dockerfile 位于 pyflink 子目录中。
-
基础镜像:我们从官方 Flink 镜像开始。
-
Python 安装:安装了 Python 和 pip,并将 pip 升级到最新版本。
-
依赖管理:通过 requirements.txt 安装依赖项。或者,可以将某些行注释掉,演示如何从本地文件手动安装依赖项,这对于在没有互联网访问的环境中部署很有用。
-
连接器库:Kafka 和 PostgreSQL 的连接器会直接下载到 Flink 的 lib 目录中。这使得 Flink 在作业执行过程中能够与 Kafka 和 PostgreSQL 进行交互。
-
脚本复制:从代码库中的脚本会复制到 /opt/flink 目录,由 Flink 任务管理器执行。
使用这个自定义 Docker 镜像,我们确保 pyFlink 可以在 Docker 容器中正常运行,并配备了与 Kafka 和 PostgreSQL 无缝交互所需的库。这种方法提供了灵活性,适用于开发和生产环境。
注意: 请确保根据您的部署环境的政策,处理下载连接器和其他依赖项时的网络或安全性问题。
集成 PostgreSQL
要将 Apache Flink 连接到 PostgreSQL 数据库,必须使用适当的 JDBC 连接器。pyFlink 的自定义 Docker 镜像会下载与 PostgreSQL 16 兼容的 PostgreSQL JDBC 连接器。
为了简化这一过程,代码库中包含一个 download_libs.sh 脚本,模拟了在 Flink Docker 容器中执行的操作。该脚本自动下载所需的库,确保 Docker 和本地环境之间的一致性。
注意: 连接器通常有两个版本。在这种情况下,由于我使用的是最新的稳定版本 Flink 1.18,我下载了 3.1.2–1.18 版本。我的猜测是,第一个版本跟踪了多个数据库的 JDBC 实现。它们可以在 maven 目录中找到。
env.add_jars(
f"file://{current_dir}/flink-connector-jdbc-3.1.2–1.18.jar",
f"file://{current_dir}/postgresql-42.7.3.jar"
)
定义 JDBC Sink
在我们的 Flink 任务中,有一个至关重要的函数,名为 configure_postgre_sink,位于 usr_jobs/postgres_sink.py 文件中。这个函数负责配置一个通用的 PostgreSQL 接收器。为了有效使用它,你需要提供 SQL 数据操作语言(DML)语句和相应的值类型。用于流数据的类型定义为 TYPE_INFO... 我花了些时间才找出正确的声明方式 😅。
还需要注意的是,JdbcSink 有一个可选参数,用于定义 ExecutionOptions。在这个特定的例子中,我将使用 1 秒的更新间隔,并将行数限制为 200。你可以在官方文档中找到更多信息。是的,你猜对了,由于我定义了一个间隔,这可以视为一个微批次 ETL。然而,由于 Flink 的并行性,你可以在一个简单的脚本中同时处理多个数据流,而且它非常易于理解。
注意: 别忘了在 Postgres 中创建 raw_sensors_data 表,这是接收来自 IoT 传感器的原始数据的地方。这个步骤在下面的逐步指南中已经涵盖。
将数据传输到 Kafka
我在之前的讨论中介绍了如何从 Kafka 主题消费数据。然而,我还没有配置接收器,接下来我们就会配置。这个配置有一些复杂之处,它是定义在一个函数中的,类似于 Postgres 接收器。此外,在将数据流传输到 Kafka 之前,你必须定义数据流的类型。请注意,alarms_data 流在传输到 Kafka 之前已经正确地转换为字符串,output_type=Types.STRING(),因为我已经将序列化器声明为 SimpleStringSchema()。
接下来的步骤我将展示如何从 alerts 主题获取数据。
本地或容器化配置
这个 Docker 配置的一个最大优点是,你可以在本地或容器内部以托管任务的方式运行 Flink。下图展示了本地 Flink 设置,你可以看到我们的 Flink 应用程序与 Docker 容器是分离的。这有助于排查 Flink 的问题,因为 Flink 本身并没有很好的本地可观察工具。实际上,我们想尝试一下datorios提供的 Flink 工具,它们在监控方面非常有前景。

在本地运行 Flink 应用程序,并且容器内部还运行着其他服务 — 图像来自作者
如果你想在本地尝试 Flink 应用程序,你需要正确地定义脚本所使用的主机和端口,这实际上是 usr_jobs/postgres_sink.py 文件中的两个常量:
容器运行时,使用:
KAFKA_HOST = "kafka:19092"
POSTGRES_HOST = "postgres:5432"
本地运行时,使用:
KAFKA_HOST = "localhost:9092"
POSTGRES_HOST = "localhost:5432"
默认情况下,仓库设置 Flink 应用程序在容器内运行。你可以通过 Web UI 监控正在运行的作业,访问localhost:8081。如果你选择在本地运行作业,则无法查看。

显示 Flink Web UI 中运行作业的截图——作者提供的图片
注意:如果在本地运行作业,则需要安装位于 requirements.txt 中的 Flink 依赖项。如果你想使用 poetry 设置环境,还提供了一个 pyproject.toml 文件。
分步指南:运行流处理管道
第 1 步:启动多容器应用程序
通过运行 docker-compose 启动容器。我更倾向于不使用分离模式,以便在容器启动并运行时查看日志。
docker-compose up
检查日志以查看服务是否正常运行。
第 2 步:创建 Kafka 主题
接下来,我们将创建主题以接收来自 IoT 传感器的数据,并存储 Flink 应用程序过滤后的警报。
docker-compose exec kafka kafka-topics \
-- create - topic sensors \
-- bootstrap-server localhost:9092 \
-- partitions 1 \
-- replication-factor 1
docker-compose exec kafka kafka-topics \
-- create - topic alerts \
-- bootstrap-server localhost:9092 \
-- partitions 1 \
-- replication-factor 1
要检查主题是否正确创建,可以执行以下命令
docker-compose exec kafka kafka-topics \
-- bootstrap-server localhost:9092 \
-- list
第 3 步:创建 Postgres 表
登录到 Postgres 控制台
psql -h localhost -U flinkuser -d flinkdb
输入密码 flinkpassword 登录到 Postgres 控制台,请记住这是本地配置,因此默认访问权限已在 docker-compose.yml 中配置。然后创建表格
CREATE TABLE raw_sensors_data (
message_id VARCHAR(255) PRIMARY KEY,
sensor_id INT NOT NULL,
message TEXT NOT NULL,
timestamp TIMESTAMPTZ NOT NULL
);
你可以通过以下方式检查表是否正确创建
flinkdb=# \d raw_sensors_data
这将显示类似以下的结果:

第 4 步:启动 Kafka 生产者
使用 conda 或 poetry 创建一个本地环境并安装 python kafka 包:
pip install kafka-python
然后执行 Kafka 生产者,它模拟 IoT 传感器消息并将消息发布到传感器主题。
python pyflink/usr_jobs/kafka_producer.py
让它在接下来的教程中一直运行。
第 5 步:初始化 Flink 任务
我们将从容器内启动 Flink 应用程序,因此你可以通过 Web UI 通过 localhost:8081 监控它。从仓库根目录运行以下命令:
docker-compose exec flink-jobmanager flink run \
-py /opt/flink/usr_jobs/postgres_sink.py
你将看到一些日志信息,此外,警报也会显示在 flink-jobmanager 容器的日志中。同时,你可以从 Flink Web UI localhost:8081/#/job/running检查作业是否正在运行。

运行作业的详细信息——作者提供的图片
显然,监控显示 Flink 作业中没有消息流动,但这不是真的,因为可以在 docker 日志中看到警报。

我们将通过 Postgres 表检查消息,并读取为此目的创建的警报主题。
第 6 步:读取 Kafka 主题中的警报
要读取警报主题中的数据,可以执行以下命令:
docker-compose exec kafka kafka-console-consumer \
-- bootstrap-server localhost:9092 \
-- topic alerts \
-- from-beginning
这将展示到目前为止该主题接收到的所有消息。
第 7 步:从 Postgres 表读取原始数据
此外,你还可以查询 IoT 传感器的原始消息,甚至在 PostgreSQL 中解析 JSON 数据:
SELECT
*,
(message::json->>'temperature')::numeric as temperature
FROM raw_sensors_data
LIMIT 10;
第 8 步:停止服务
你可以通过在 Docker 终端按 Ctrl-c 来轻松停止所有服务。如果你更倾向于正确地关闭服务,可以按照以下步骤进行操作:
-
在 Web UI 的作业详情页右上角点击以取消 Flink 作业。
-
停止本地运行的 kafka_producer.py 脚本。
-
在 Docker 终端按 Ctrl-c 停止服务
会话中交换的信息,在服务运行期间会被永久存储。因此,如果你想查询 Postgres 表或 Kafka 主题,数据将会在其中。
使用多个 Sink 在 PyFlink 作业中的见解
在用于演示的 Flink 作业中,我同时管理 2 个数据流,它们在同一个任务中运行。一个是写入来自传感器主题(IoT 设备)的原始数据,另一个是过滤后的警报数据,写入另一个主题。这种方式有一些优点和缺点,简单总结如下:
单一作业使用多个 Sink 的优点:
-
资源管理的简便性。
-
数据流的一致性。
单一作业的缺点:
-
随着逻辑的增长,可能变得复杂。
-
可扩展性可能是个问题。
多个作业的优点:
-
更好的故障隔离。
-
聚焦优化。
多个作业的缺点:
-
资源开销。
-
协调复杂性。
结论
该设置为实时数据流和处理提供了一个强大的解决方案,有效地集成了 Flink、Kafka 和 PostgreSQL。使用 Postgres 的主要目的是检查来自 IoT 设备的原始消息,而不是依赖于直接查询主题本身。它还展示了如何使用 JDBC 连接器进行数据输出,这可能是一个相对标准的做法。消息转换通过 DataStream API 完成。我希望进一步深入学习 SQL API,它提供了一个更加友好的接口。最后,关于如何管理数据流,建议根据具体需求选择单一作业或多个作业,确保可扩展性和可维护性。
下一步
1. 使用 SQL API 进行数据转换。
2. 根据作业复杂性优化资源使用。
3. 探索 Flink 的高级功能以应对复杂的数据处理任务。
快乐流式处理!🚀
敬请期待更多关于如何通过 Docker 集成和扩展数据工程解决方案的教程!
如有任何问题或建议,请随时在下方评论区与我们联系!
准备好优化你的流数据应用了吗?
解锁数据的全部潜力,使用我们的专家咨询服务,为流数据应用量身定制。无论你是想增强实时分析、优化数据管道,还是提升性能,我们都能为你提供帮助。
参考资料
www.confluent.io/blog/kafka-client-cannot-connect-to-broker-on-aws-on-docker-etc/
nightlies.apache.org/flink/flink-docs-master/docs/connectors/datastream/jdbc/
nightlies.apache.org/flink/flink-docs-master/docs/deployment/overview/#session-mode
medium.com/@sant1/flink-docker-kafka-faee9c0f1580
如何通过两个小习惯提高我的数据科学家工作效率
如何将这些习惯融入到日常生活中
·发表于 Towards Data Science ·阅读时间:7 分钟·2024 年 10 月 31 日
--

公司希望 IT 专家和数据科学家能迅速完成工作,无论是部署机器学习模型、修复重大漏洞,还是创建可扩展的数据管道。现在有了 GenAI?那就更难了:要求更高了。
但是事情是这样的。平均办公室员工每天真正工作的时间只有不到 4 小时。没错,你没看错。我们每天坐在桌前八小时或更长时间,但很多时间都是在做与工作无关的事情,比如刷社交媒体或与同事聊天。即使我们试图专注,面对各种持续不断的干扰,也并不总是容易做到。
你可能会有共鸣。你已经将自己的热情与技能对齐。虽然这提高了你的工作效率,但并没有解决所有问题。然后你探索了各种技巧——从番茄工作法到小憩。尽管你付出了最大的努力,你仍然发现自己在拖延,或者对自己的工作效率不满意。
我发现自己也处于类似的境地。我总是因为难以长时间集中注意力而感到烦恼。然后,我养成了两个简单的习惯,它们让我提高了 50%以上的工作效率。这些习惯不仅容易实行,而且对数据科学家或任何长时间坐在电脑前的人来说都非常有效。
寻找能够断开连接并充电的活动
— 弗里德里希·尼采
阿尔伯特·爱因斯坦被公认为是最著名的科学家之一。在 1905 年,26 岁的他度过了他那令人称奇的奇迹年(miraculous year). 仅仅一年,他就发表了四篇具有开创性的论文,对现代物理学产生了深远的影响。在他的第四篇论文中,他提出了质量与能量等价的概念,这一公式通常被称为E = mc²。
他因其在理论物理学方面的重要贡献,尤其是发现光电效应定律,于 1921 年获得诺贝尔物理学奖。他的工作改变了科学家的思维方式,并使 20 世纪及以后的科技进步成为可能。
爱因斯坦的生活并非全是工作。在 1929 年与《星期六晚邮报》的采访中,他曾著名地说过:“想象力比知识更重要。知识是有限的,想象力包围着整个世界。”他相信直觉和灵感。换句话说,他鼓励人们超越我们已知的边界,保持对未知的开放态度。
他会从智力工作中休息,让自己的思维游走,滋养自己的想象力。他喜欢长时间散步、航行和拉小提琴。这些活动给了他放松和解放思维的机会。正是在这些时刻,他才常常找到最好的创意和灵感。
他在想象自己追逐一束光时,提出了一些最伟大的想法,如狭义相对论。他后来在他的自传性笔记中回忆到,这个思想实验在他发展狭义相对论的过程中起到了重要作用。
事情还不止这些。
查尔斯·达尔文每天都有一个仪式,他会在他的“思考小径”上散步,这条小径位于达尔文的唐恩屋。他的日常散步,加上收集昆虫、园艺和观察自然等爱好,是他发展自然选择进化论的基础。
从艺术到弹奏竖琴,列奥纳多·达·芬奇有着多种爱好和兴趣。这些兴趣使他能够从不同角度解决问题。他对鸟类及其飞行能力的痴迷促使他深入研究飞行的力学原理,最终画出了鸟型飞行器的草图,这是一种飞行装置。

图片来自克里斯蒂娜·戈塔尔迪,发布于Unsplash。
我知道你在想什么:“我没有时间做爱好,我忙着调整我的最新模型和赶项目的截止日期!”相信我,我曾经也有同样的想法。我曾经认为,工作越多,我取得的成就就越多。就像爱因斯坦、达·芬奇和达尔文一样,数据科学家从事着高度智力的工作。而正如爱因斯坦拉小提琴或达尔文走他的“思考小道”一样,我们都需要那些可以断开联系的时刻。
在做我们喜欢的活动后,我们更放松,感觉充满能量。我们的工作效率也会更高,带来的新视角是我们长时间只工作时无法获得的。
你不需要马上开始每天 2 小时的爱好。即使只有 30 分钟、20 分钟,甚至 10 分钟,也是一个很好的开始。对我来说,每天至少花 30 分钟做一件能让我从工作中脱离的事情是理想的。重要的是你从某个地方开始。
数据科学家的工作具有挑战性。分析大型数据集和构建复杂模型需要长时间专注地面对计算机。因此,理想情况下你需要一些能让你远离计算机屏幕的活动。
从事一种让你完全断开联系的活动,在做这项活动时你不会去想其他任何事情。想一想一种活动,它在你做完后让你感到完全充电(或几乎如此),即使它在当下没有完全让你断开。
让我最能放松心情的活动是攀岩和户外活动。攀岩时,我什么也不想;否则,我可能会摔倒。即使是一次小小的攀岩,或者时间紧迫,我也会感到充电满满。
此外,对我的爱好保持严格要求也是非常重要的。这是不可妥协的,是我每周的常规活动。我每周工作后和周末都会去攀岩三到四次。甚至我的旅行安排也是围绕户外活动进行的。
这不仅仅是一个爱好,它是必需的。当我把这项活动放在首位时,我会处于最佳状态,它让我能够有效充电。
培养心理韧性
——老子
詹森·黄(Jensen Huang)于 1993 年共同创立了 NVIDIA,旨在创造突破性的图形处理技术。在人工智能的未来和潜力尚不明朗的时刻,NVIDIA 在他的领导下大胆押注深度学习。
这项赌注最终是成功的。从训练 AI 模型到推动自动驾驶汽车和基于云的 AI 服务,NVIDIA 的 GPU 已成为行业标准。这些决策推动 NVIDIA 成为全球市值最大的公司之一。
在2024 年 SIEPR 经济峰会上,黄先生被问到:“你会给斯坦福大学的学生什么建议,以提高他们成功的机会?”他没有给出常见的“追随梦想”回答,而是强调了韧性的必要性。伟大来自于面对挑战。他甚至幽默地祝愿学生们“充足的痛苦和苦难”来塑造他们的性格。
黄先生的核心观点是什么?成功不仅仅在于聪明。真正能带来不同的是从挫折中恢复的能力。这种对韧性的强调突出了心理韧性在成功中的重要性。
在快速发展的科技行业中,聪明和拥有正确的技能固然重要,但这还不够。你可能已经走在正确的轨道上,在做你应该做的事,但要真正卓越,你需要培养心理韧性。
这也与近期的心理学研究相一致,研究表明心理灵活性是心理健康和情感福祉中最重要的技能。心理灵活性包括保持当下、面对困难经历并开放心态,以及按照个人价值观做事——这些都是心理韧性的关键组成部分。
我正在做我热爱的事情,做我必须做的事。我对我的工作充满热情,但我有时也会感到疲惫或缺乏动力吗?会的。我真的喜欢在空闲时间攀岩吗?我知道这项运动对我来说是合适的吗?是的。但有时即使我一般很有纪律性,我还是会感到动力不足吗?是的。它会让我不去行动吗?不会,因为我知道这是我必须做的事。
有时,当我在审查代码或构建模型时,我的效率较低。没关系。心理韧性就像一块肌肉,随着时间的推移,你会逐渐锻炼它,它会让你在长期内变得更加高效。
你在做自己喜欢的事吗?你在你应该在的地方吗?你是否有时感到疲惫或不太专注?没关系。继续做你正在做的事。关键是要坚持度过这些时刻,知道它们只是暂时的,是过程的一部分。
通过坚持度过低效的时期并保持纪律性,你正在锻炼心理韧性,这将对你长期发展大有裨益。记住,重要的不是时刻完美,而是坚持和毅力。
喜欢这篇文章吗?请表示支持!
👏 点赞最多可达 50 次
🤝 发送 LinkedIn 连接请求,保持联系并讨论机会。
我是如何获得 Spotify 数据科学实习机会的:我的顶尖科技公司求职指南
面试和简历技巧,助你顺利通过申请过程
·发表于Towards Data Science ·阅读时长:11 分钟·2024 年 4 月 21 日
--

图片来源:作者
我的职业生涯始于 2021 年,当时我在 Spotify 担任数据科学实习生。
然后,我进行了另一段实习,并作为机器学习研究员完成了硕士论文。
所以,我算得上是一个实习专家了。我的同事们甚至曾开玩笑说我成了“高级实习生”。因此,如果你是一个梦想加入 Spotify 的学生,这篇文章就是为你准备的。
那年我被录取时,大约有成千上万的申请者。无需多言,这个竞争非常激烈。
而且,很容易理解为什么。
对我们中的许多人来说,包括我自己,Spotify 是一个梦寐以求的公司。没有什么比音乐更能与人产生共鸣的了。许多学生梦想加入这支团队,且有充分的理由。
获得实习通常是毕业后获得全职工作机会的最佳途径,即使在科技行业也是如此。
两年前,我很幸运地成为了其中一个获得留任机会的人,从那时起,我就不断收到学生们问我如何做到的消息。这是最常被问到的问题。
如何在 2 周内(从零开始)学习 SQL
我是如何熟练掌握 SQL 并成功获得我的第一份数据科学工作的
·发表于 Towards Data Science ·阅读时间 8 分钟·2024 年 5 月 10 日
--

那么,你想学习 SQL 吗?
好的,在本文中,我将介绍我是如何在短短 2 周内学习 SQL 的,这帮助我获得了第一份入门级数据科学工作。
让我们开始吧!
我的背景
当我开始学习 SQL 时,我正处于成为数据科学家的过程中,并且正在攻读物理学硕士学位的最后一年。
到那时,我已经在大学的前几年用 Fortran 编程多年,并刚刚完成了一门 Python 课程,同时也在做一些编程题目。
我提到这一点是因为我并没有抱有任何幻想,认为我在学习 SQL 时,拥有一些其他人可能没有的优势。
我是如何学会编程的(没有计算机科学学位,没有训练营)
我的编程历程总结以及我希望自己曾经得到的建议
·发表于Towards Data Science ·阅读时长:8 分钟·2024 年 3 月 9 日
--

图片由Christopher Gower提供,来源于Unsplash
学习编程可能是我一生中做出的最好的决定之一。它为我打开了许多机会,让我找到了自己热爱的职业。不仅如此,能够编程是当今非常宝贵的技能,你可以获得几个薪水高于平均水平的工作。
所以,在这篇文章中,我想回顾一下我的整个编程历程,并为那些刚刚开始的人提供一些建议。
我的故事

图片由Clemens van Lay提供,来源于Unsplash
我并不是那种十岁就写出自己第一个编译器的天才。事实上,我是在 18 岁时,在大学第一年写下了我的第一行代码。
我年轻时的梦想是成为一名物理学研究员,灵感来源于我 13 岁时观看的《生活大爆炸》。我基本上是想在纸上乱涂乱写,或者……
我如何学会停止担忧并爱上偏自相关系数

2015 年 8 月,太平洋是地球上你最不想待的地方。向厄尔尼诺致敬!来源:NOAA
除了显而易见的对《奇爱博士》的致敬之外,我们将学习如何使用 PACF 精准地选择最具影响力的回归变量。
·发表于Towards Data Science ·阅读时长 21 分钟·2024 年 6 月 26 日
--
作为一个概念,偏相关系数适用于时间序列数据和横截面数据。在时间序列分析中,它通常被称为偏自相关系数。在本文中,我将更多地关注偏自相关系数及其在配置自回归(AR)模型时的应用,特别是在它如何帮助你剔除不相关的回归变量,从而改进你的 AR 模型。
在接下来的部分,我将解释:
-
为什么你需要偏相关系数(PACF),
-
如何计算偏(自)相关系数和偏自相关函数,
-
如何判断偏(自)相关系数是否具有统计显著性,以及
-
PACF 在构建自回归时间序列模型中的应用。
我还将解释偏相关的概念如何应用于构建线性……
如何在一份全职工作中为一切腾出时间
我是如何在全职担任数据科学家的同时,每周制作 YouTube 视频、写博客,并发布新闻通讯的
·发布于 Towards Data Science ·阅读时长 8 分钟·2024 年 9 月 14 日
--

图片由作者提供。
我曾经说过:“我没有足够的时间。” 但实际上,我只是把优先级搞错了,之后我显著提高了我的时间管理技巧。
甚至有好几次有人问我,如何在全职工作之外,持续发布 YouTube 视频和博客文章,每周写一份新闻通讯,并几乎每天锻炼身体。
好吧,在这篇文章中,我想分享一下我的过程,并希望为那些在管理所有优先事项时感到困扰的人提供一些建议。
我还有多少时间?
我总是从审查我的日程开始,看看我到底还有多少时间可以真正支配。
由于我有一份全职数据科学家的工作,周一到周五,早上 9 点到下午 5:30,我的时间完全被占用。当然,可能会有一些空隙…
如何自学数据科学
我的数据科学和技术领域学习技巧与方法
·发表于 Towards Data Science ·阅读时间:8 分钟·2024 年 4 月 12 日
--

图片由 Firmbee.com 提供,来源于 Unsplash
你是否曾因数据科学的庞大规模而感到不知所措,不知道从哪里开始,或者如何让你的学习更加深入?
我曾经在学习数据科学的过程中漫无目的地尝试,但现在我已经有了一种更系统的方法,这种方法彻底改变了我对这一领域的理解和直觉。
在这篇文章中,我想分享一些我的技巧和建议,这些方法在我的学习过程中证明了是有效的,并且还有一些保持一致性的建议。
决定学习什么
学习某个内容的第一步自然是决定你要学习什么。大多数人可能已经有了一些初步的想法,但单纯地说“我想学习数据科学”可能不足够,因为这太模糊了。数据科学涵盖了许多领域,比如数学、统计学和编程,显而易见的是这些内容,但它们还可以进一步细分。
如何作为数据科学家保持对人工智能的最新了解
这比简单的几次谷歌搜索要复杂一些
·发表于 Towards Data Science ·阅读时间 8 分钟·2024 年 1 月 23 日
--

图片由作者提供
2023 年之前,如果你提到人工智能,人们可能会翻白眼,觉得你是个疯狂的技术怪人。然而,现在情况不同了。ChatGPT 已经是家喻户晓的名字,每个人都知道 OpenAI 是谁。
显然,作为数据科学家,我们与人工智能密切相关,这是工作的一部分。然而,投入到这一领域的研究和资金正在呈现爆炸式增长。似乎每周都有一些大型科技公司宣布他们的新型变压器模型,他们声称该模型将超越竞争对手(不点名批评)。
让我们坦诚一点:没有人有时间每周阅读几篇研究论文。即使我们有时间,论文的语言通常也很深奥且复杂,这使得阅读起来相当困难且枯燥。
这就是为什么我想要解释我用来每周获取人工智能资讯并跟进行业动态的方法。当然,每个人有自己的偏好。对我有效的方法可能不适用于你。但我相信,本文中的所有方法都是低成本的,不会占用你太多时间。
我如何使用 LlamaIndex 工作流简化我的研究和演示过程
一个通过 AI 工作流实现可靠性、灵活性和可控性的示例
·发表于 Towards Data Science ·16 分钟阅读·2024 年 9 月 10 日
--
LlamaIndex 最近推出了一项新功能:工作流。对于那些希望创建既可靠又灵活的 AI 解决方案的人来说,这非常有用。为什么呢?因为它允许你定义具有控制流的自定义步骤。它支持循环、反馈和错误处理。它就像一个 AI 驱动的管道。但与通常实现为有向无环图(DAG)的典型管道不同,工作流还支持循环执行,使其成为实现代理式和其他更复杂过程的良好候选。
[## Introducing workflows beta: a new way to create complex AI applications with LlamaIndex …
LlamaIndex 是一个简单灵活的数据框架,用于将自定义数据源连接到大型语言模型(LLMs)。
在这篇文章中,我将展示如何使用 LlamaIndex 工作流简化我的研究过程,帮助我研究某个主题的最新进展,然后将这些研究成果转化为 PowerPoint 演示文稿。
当涉及到查找新的研究出版物或论文时,ArXiv.org是我主要的来源。然而,网站上的论文非常多。截至 2024 年 9 月,ArXiv 上大约有 250 万篇论文,其中 17,000 篇是仅在 8 月份提交的(统计数据在这里)。即使限制在一个特定主题下,要阅读的内容依然非常庞大。但这并不是一个新问题。长期以来,学术研究人员必须浏览大量的文献以进行自己的研究。过去两年大型语言模型(LLM)的兴起为我们提供了诸如ResearchGPT、papersGPT和许多在OpenAI平台上为特定研究目的构建的定制 GPT 工具,这些工具有助于文献搜索、摘要提取和展示。
尽管这些工具很有用,但我选择使用 LlamaIndex 工作流来构建自己的工作流,原因有几个关键点:
-
我已经有一个特定的研究过程,并希望保持它,但提高效率。
-
我想利用 LLM 和代理行为,并保持对大多数步骤的控制。
-
我的目标不仅仅是获得最终的 PowerPoint 演示文稿;我还希望能访问中间结果,以便在整个过程中观察、调整和排除故障。
-
我需要一个一体化的解决方案,能够端到端处理所有任务,而无需在不同的工具之间切换来进行摘要或创建幻灯片等任务。
-
如果我的需求发生变化,我可以轻松地扩展或修改工作流。
我将设置一个工作流,用户提供一个研究主题(例如:“使用 GenAI 制作 PowerPoint 幻灯片”),然后从 arxiv.org 网站拉取几篇论文,并使用 LLM 对每篇论文进行总结。更具体地说,我希望总结的一些关键信息包括:方法类型、模型的组件、预训练或微调方法、数据集、评估方法指标和结论。所有这些的输出将是一个 PowerPoint 演示文稿,每篇论文一张幻灯片,包含来自总结的关键洞见。
在我解释如何实现这个工作流之前,理解 LlamaIndex 工作流中的两个关键概念非常重要:事件和步骤。
-
步骤:步骤是工作流的构建块。它们是代表工作流各个组件的 Python 函数。每个步骤执行特定任务,如发送网页查询、获取 LLM 响应或处理数据。步骤可以通过接收和发出事件与其他步骤进行交互。步骤还可以访问共享的上下文,从而实现跨不同步骤的状态管理。 -
Event:事件作为数据承载体和工作流的流程控制器,以 Pydantic 对象的形式实现。它们控制工作流的执行路径,使工作流具有动态性和灵活性。用户可以自定义事件的属性。两个预定义的特殊事件类型StartEvent和StopEvent控制工作流的开始和结束点。
LlamaIndex 提供了几个笔记本示例和视频系列,详细介绍了这些概念。
除了基本组件外,我的工作流还使用了:
-
异步和并行执行:为了提高效率,能够并发地完成多个任务。
-
嵌套工作流:工作流中更复杂的层级结构。
-
LLM 的结构化输出:为了确保数据在步骤之间传递时是结构化的。
-
不同的 LLM 模型:为了在步骤之间使用具有不同能力和推理速度的模型(
gpt-4o和gpt-4o-mini)。 -
代码执行的动态会话:为了允许在隔离环境中执行代码。
-
不同步骤的独立代理:在过程中使用特定的代理来处理特定任务。
你可以在Github上找到这个工作流的完整代码。要运行它,你需要 Tavily 搜索、Semantic Scholar 和 Azure OpenAI 的 API 密钥(由于这个实现使用了 Azure 资源,但你可以很容易地将其切换为 OpenAI 或其他模型,使用 LlamaIndex)。在接下来的部分,我将介绍一些构建这个工作流的关键细节和步骤。
主工作流
主工作流由两个嵌套的子工作流组成:
-
summary_gen:这个子工作流会查找给定主题的研究论文并生成摘要。它通过网页查询进行文献检索,并利用 LLM 根据指示获取见解和摘要。 -
slide_gen:这个子工作流负责使用前一步的摘要生成 PowerPoint 幻灯片。它使用提供的 PowerPoint 模板格式化幻灯片,并通过创建和执行 Python 代码(使用python-pptx库)生成幻灯片。

主工作流概述(作者提供的图像)
摘要生成子工作流
让我们仔细看看这些子工作流。首先是summary_gen工作流,它相当简单。它遵循一个简单的线性过程。它基本上作为一个“数据处理”工作流,某些步骤会向 LLM 发送请求。

摘要生成工作流(作者提供的图像)
工作流首先通过获取用户输入(一个研究主题)开始,并经过以下步骤:
-
tavily_query:使用 Tavily API 查询与主题相关的学术论文,并返回结构化的响应。 -
get_paper_with_citations:对于从 Tavily 查询返回的每篇论文,此步骤使用 SemanticScholar API 获取论文元数据以及被引用论文的元数据。 -
filter_papers:由于并非所有检索到的引用都与原始主题直接相关,因此此步骤对结果进行精炼。每篇论文的标题和摘要会被发送到 LLM,以评估它们的相关性。此步骤定义如下:
@step(num_workers=4)
async def filter_papers(self, ev: PaperEvent) -> FilteredPaperEvent:
llm = new_gpt4o_mini(temperature=0.0)
response = await process_citation(ev.paper, llm)
return FilteredPaperEvent(paper=ev.paper, is_relevant=response)
在process_citation()函数中,我们使用LlamaIndex 的 FunctionCallingProgram来获取结构化的响应:
IS_CITATION_RELEVANT_PMT = """
You help a researcher decide whether a paper is relevant to their current research topic: {topic}
You are given the title and abstract of a paper.
title: {title}
abstract: {abstract}
Give a score indicating the relevancy to the research topic, where:
Score 0: Not relevant
Score 1: Somewhat relevant
Score 2: Very relevant
Answer with integer score 0, 1 or 2 and your reason.
"""
class IsCitationRelevant(BaseModel):
score: int
reason: str
async def process_citation(citation, llm):
program = FunctionCallingProgram.from_defaults(
llm=llm,
output_cls=IsCitationRelevant,
prompt_template_str=IS_CITATION_RELEVANT_PMT,
verbose=True,
)
response = await program.acall(
title=citation.title,
abstract=citation.summary,
topic=citation.topic,
description="Data model for whether the paper is relevant to the research topic.",
)
return response
-
download_papers:此步骤收集所有筛选后的论文,根据相关性得分和在 ArXiv 上的可用性对它们进行优先级排序,并下载最相关的论文。 -
paper2summary_dispatcher:每篇下载的论文都会为生成摘要进行准备,通过设置存储图像和摘要的路径。此步骤使用self.send_event()来启用每篇论文的paper2summary步骤并行执行。它还通过变量ctx.data[“n_pdfs”]设置工作流上下文中的论文数量,以便后续步骤知道需要处理的论文总数。
@step(pass_context=True)
async def paper2summary_dispatcher(
self, ctx: Context, ev: Paper2SummaryDispatcherEvent
) -> Paper2SummaryEvent:
ctx.data["n_pdfs"] = 0
for pdf_name in Path(ev.papers_path).glob("*.pdf"):
img_output_dir = self.papers_images_path / pdf_name.stem
img_output_dir.mkdir(exist_ok=True, parents=True)
summary_fpath = self.paper_summary_path / f"{pdf_name.stem}.md"
ctx.data["n_pdfs"] += 1
self.send_event(
Paper2SummaryEvent(
pdf_path=pdf_name,
image_output_dir=img_output_dir,
summary_path=summary_fpath,
)
)
paper2summary:对于每篇论文,它将 PDF 转换为图像,然后将图像发送到 LLM 进行摘要生成。一旦生成摘要,它会保存在一个 Markdown 文件中,以供将来参考。特别地,这里生成的摘要非常详细,像一篇小文章,因此还不太适合直接放入演示文稿中。但它会被保留下来,以便用户查看这些中间结果。在后续的步骤中,我们将使这些信息更具可展示性。提供给 LLM 的提示包含关键指令,以确保生成准确且简明的摘要:
SUMMARIZE_PAPER_PMT = """
You are an AI specialized in summarizing scientific papers.
Your goal is to create concise and informative summaries, with each section preferably around 100 words and
limited to a maximum of 200 words, focusing on the core approach, methodology, datasets,
evaluation details, and conclusions presented in the paper. After you summarize the paper,
save the summary as a markdown file.
Instructions:
- Key Approach: Summarize the main approach or model proposed by the authors.
Focus on the core idea behind their method, including any novel techniques, algorithms, or frameworks introduced.
- Key Components/Steps: Identify and describe the key components or steps in the model or approach.
Break down the architecture, modules, or stages involved, and explain how each contributes to the overall method.
- Model Training/Finetuning: Explain how the authors trained or finetuned their model.
Include details on the training process, loss functions, optimization techniques,
and any specific strategies used to improve the model’s performance.
- Dataset Details: Provide an overview of the datasets used in the study.
Include information on the size, type and source. Mention whether the dataset is publicly available
and if there are any benchmarks associated with it.
- Evaluation Methods and Metrics: Detail the evaluation process used to assess the model's performance.
Include the methods, benchmarks, and metrics employed.
- Conclusion: Summarize the conclusions drawn by the authors. Include the significance of the findings,
any potential applications, limitations acknowledged by the authors, and suggested future work.
Ensure that the summary is clear and concise, avoiding unnecessary jargon or overly technical language.
Aim to be understandable to someone with a general background in the field.
Ensure that all details are accurate and faithfully represent the content of the original paper.
Avoid introducing any bias or interpretation beyond what is presented by the authors. Do not add any
information that is not explicitly stated in the paper. Stick to the content presented by the authors.
"""
finish:此工作流收集所有生成的摘要,验证它们是否正确存储,并记录流程的完成情况,并返回StopEvent作为最终结果。
如果此工作流独立运行,执行将在此处结束。然而,由于这是主流程的一个子工作流,完成后将触发下一个子工作流——slide_gen。
幻灯片生成子工作流
此工作流基于前一步骤中创建的摘要生成幻灯片。以下是slide_gen工作流的概述:

幻灯片生成工作流(图片来自作者)
当前一个子工作流完成且摘要 Markdown 文件准备好时,启动以下工作流:
-
get_summaries:此步骤读取摘要文件的内容,针对每个文件触发SummaryEvent,再次使用self.send_event()以便启用并行执行,促进更快速的处理。 -
summary2outline:此步骤通过使用 LLM 将摘要转化为幻灯片大纲文本。它将摘要缩短为句子或项目符号,以便放入演示文稿中。 -
gather_feedback_outline:在此步骤中,它将提议的幻灯片大纲与论文摘要一起呈现给用户以供他们审阅。用户提供反馈,如果需要修改,可能会触发OutlineFeedbackEvent。这个反馈循环会继续进行,直到用户批准最终大纲为止,届时会触发OutlineOkEvent。
@step(pass_context=True)
async def gather_feedback_outline(
self, ctx: Context, ev: OutlineEvent
) -> OutlineFeedbackEvent | OutlineOkEvent:
"""Present user the original paper summary and the outlines generated, gather feedback from user"""
print(f"the original summary is: {ev.summary}")
print(f"the outline is: {ev.outline}")
print("Do you want to proceed with this outline? (yes/no):")
feedback = input()
if feedback.lower().strip() in ["yes", "y"]:
return OutlineOkEvent(summary=ev.summary, outline=ev.outline)
else:
print("Please provide feedback on the outline:")
feedback = input()
return OutlineFeedbackEvent(
summary=ev.summary, outline=ev.outline, feedback=feedback
)
-
outlines_with_layout:它通过包括来自给定 PowerPoint 模板的页面布局细节,使用 LLM 增强每个幻灯片大纲。在这个阶段,所有幻灯片页面的内容和设计都会保存在一个 JSON 文件中。 -
slide_gen:它使用ReAct 代理根据给定的大纲和布局细节制作幻灯片文档。这个代理具有一个代码解释器工具,可以在隔离环境中运行和修正代码,还具有一个布局检查工具,用来查看给定的 PowerPoint 模板信息。该代理会使用python-pptx来创建幻灯片,并能观察并修正错误。
@step(pass_context=True)
async def slide_gen(
self, ctx: Context, ev: OutlinesWithLayoutEvent
) -> SlideGeneratedEvent:
agent = ReActAgent.from_tools(
tools=self.azure_code_interpreter.to_tool_list() + [self.all_layout_tool],
llm=new_gpt4o(0.1),
verbose=True,
max_iterations=50,
)
prompt = (
SLIDE_GEN_PMT.format(
json_file_path=ev.outlines_fpath.as_posix(),
template_fpath=self.slide_template_path,
final_slide_fname=self.final_slide_fname,
)
+ REACT_PROMPT_SUFFIX
)
agent.update_prompts({"agent_worker:system_prompt": PromptTemplate(prompt)})
res = self.azure_code_interpreter.upload_file(
local_file_path=self.slide_template_path
)
logging.info(f"Uploaded file to Azure: {res}")
response = agent.chat(
f"An example of outline item in json is {ev.outline_example.json()},"
f" generate a slide deck"
)
local_files = self.download_all_files_from_session()
return SlideGeneratedEvent(
pptx_fpath=f"{self.workflow_artifacts_path}/{self.final_slide_fname}"
)
validate_slides:检查幻灯片文档,确保它符合给定的标准。这个步骤包括将幻灯片转化为图像,并让 LLM 根据指南对其进行视觉检查,以确保内容正确且风格一致。根据 LLM 的发现,如果有问题,它会发送SlideValidationEvent,如果一切看起来良好,则会发送StopEvent。
@step(pass_context=True)
async def validate_slides(
self, ctx: Context, ev: SlideGeneratedEvent
) -> StopEvent | SlideValidationEvent:
"""Validate the generated slide deck"""
ctx.data["n_retry"] += 1
ctx.data["latest_pptx_file"] = Path(ev.pptx_fpath).name
img_dir = pptx2images(Path(ev.pptx_fpath))
image_documents = SimpleDirectoryReader(img_dir).load_data()
llm = mm_gpt4o
program = MultiModalLLMCompletionProgram.from_defaults(
output_parser=PydanticOutputParser(SlideValidationResult),
image_documents=image_documents,
prompt_template_str=SLIDE_VALIDATION_PMT,
multi_modal_llm=llm,
verbose=True,
)
response = program()
if response.is_valid:
return StopEvent(
self.workflow_artifacts_path.joinpath(self.final_slide_fname)
)
else:
if ctx.data["n_retry"] < self.max_validation_retries:
return SlideValidationEvent(result=response)
else:
return StopEvent(
f"The slides are not fixed after {self.max_validation_retries} retries!"
)
用于验证的标准是:
SLIDE_VALIDATION_PMT = """
You are an AI that validates the slide deck generated according to following rules:
- The slide need to have a front page
- The slide need to have a final page (e.g. a 'thank you' or 'questions' page)
- The slide texts are clearly readable, not cut off, not overflowing the textbox
and not overlapping with other elements
If any of the above rules are violated, you need to provide the index of the slide that violates the rule,
as well as suggestion on how to fix it.
"""
modify_slides:如果幻灯片未通过验证检查,上一阶段会发送SlideValidationEvent事件。在这里,另一个ReAct 代理会根据验证反馈更新幻灯片,更新后的幻灯片将被保存并返回进行再次验证。根据SlideGenWorkflow类的max_validation_retries变量属性,这个验证循环可能会多次发生。
为了运行完整的端到端工作流,我们通过以下步骤启动过程:
class SummaryAndSlideGenerationWorkflow(Workflow):
@step
async def summary_gen(
self, ctx: Context, ev: StartEvent, summary_gen_wf: SummaryGenerationWorkflow
) -> SummaryWfReadyEvent:
print("Need to run reflection")
res = await summary_gen_wf.run(user_query=ev.user_query)
return SummaryWfReadyEvent(summary_dir=res)
@step
async def slide_gen(
self, ctx: Context, ev: SummaryWfReadyEvent, slide_gen_wf: SlideGenerationWorkflow
) -> StopEvent:
res = await slide_gen_wf.run(file_dir=ev.summary_dir)
return StopEvent()
async def run_workflow(user_query: str):
wf = SummaryAndSlideGenerationWorkflow(timeout=2000, verbose=True)
wf.add_workflows(
summary_gen_wf=SummaryGenerationWorkflow(timeout=800, verbose=True)
)
wf.add_workflows(slide_gen_wf=SlideGenerationWorkflow(timeout=1200, verbose=True))
result = await wf.run(
user_query=user_query,
)
print(result)
@click.command()
@click.option(
"--user-query",
"-q",
required=False,
help="The user query",
default="powerpoint slides automation",
)
def main(user_query: str):
asyncio.run(run_workflow(user_query))
if __name__ == "__main__":
draw_all_possible_flows(
SummaryAndSlideGenerationWorkflow, filename="summary_slide_gen_flows.html"
)
main()
结果
现在让我们看一下为论文LayoutGPT: 基于大语言模型的组合视觉规划与生成生成的一个中间总结示例:
# Summary of "LayoutGPT: Compositional Visual Planning and Generation with Large Language Models"
## Key Approach
The paper introduces LayoutGPT, a framework leveraging large language models (LLMs) for compositional visual planning and generation. The core idea is to utilize LLMs to generate 2D and 3D scene layouts from textual descriptions, integrating numerical and spatial reasoning. LayoutGPT employs a novel prompt construction method and in-context learning to enhance the model's ability to understand and generate complex visual scenes.
## Key Components/Steps
1\. **Prompt Construction**: LayoutGPT uses detailed task instructions and CSS-like structures to guide the LLMs in generating layouts.
2\. **In-Context Learning**: Demonstrative exemplars are provided to the LLMs to improve their understanding and generation capabilities.
3\. **Numerical and Spatial Reasoning**: The model incorporates reasoning capabilities to handle numerical and spatial relationships in scene generation.
4\. **Scene Synthesis**: LayoutGPT generates 2D keypoint layouts and 3D scene layouts, ensuring spatial coherence and object placement accuracy.
## Model Training/Finetuning
LayoutGPT is built on GPT-3.5 and GPT-4 models, utilizing in-context learning rather than traditional finetuning. The training process involves providing the model with structured prompts and examples to guide its generation process. Loss functions and optimization techniques are not explicitly detailed, as the focus is on leveraging pre-trained LLMs with minimal additional training.
## Dataset Details
The study uses several datasets:
- **NSR-1K**: A new benchmark for numerical and spatial reasoning, created from MSCOCO annotations.
- **3D-FRONT**: Used for 3D scene synthesis, containing diverse indoor scenes.
- **HRS-Bench**: For evaluating color binding accuracy in generated scenes.
These datasets are publicly available and serve as benchmarks for evaluating the model's performance.
## Evaluation Methods and Metrics
The evaluation involves:
- **Quantitative Metrics**: Precision, recall, and F1 scores for layout accuracy, numerical reasoning, and spatial reasoning.
- **Qualitative Analysis**: Visual inspection of generated scenes to assess spatial coherence and object placement.
- **Comparative Analysis**: Benchmarking against existing methods like GLIGEN and ATISS to demonstrate improvements in layout generation.
## Conclusion
The authors conclude that LayoutGPT effectively integrates LLMs for visual planning and scene generation, achieving state-of-the-art performance in 2D and 3D layout tasks. The framework's ability to handle numerical and spatial reasoning is highlighted as a significant advancement. Limitations include the focus on specific scene types and the need for further exploration of additional visual reasoning tasks. Future work suggests expanding the model's capabilities to more diverse and complex visual scenarios.
毋庸置疑,总结对于 LLM 来说并不是一个特别具有挑战性的任务。只需提供论文的图像,LLM 便能有效地捕捉到提示中概述的所有关键内容,并且相当好地遵循了样式要求。
至于最终结果,以下是生成的几张演示文稿幻灯片示例:

生成的幻灯片(图片由作者提供)

生成的幻灯片(图片由作者提供)
在填写摘要内容时,按照模板的布局保持文本风格,将总结要点以项目符号格式呈现,并包含幻灯片中所需的所有相关论文时,工作流程运行得很好。唯一的问题是,有时主内容占位符中的文本没有调整大小以适应文本框,文本溢出幻灯片边界。这类错误可能通过使用更有针对性的幻灯片验证提示来修复。
最后的想法
在本文中,我展示了如何使用 LlamaIndex 工作流程来简化我的研究和展示过程,从查询学术论文到生成最终的 PowerPoint 幻灯片。以下是我在实施该工作流程时的一些想法和观察,以及我认为可能改进的方面。
**gpt-4o** 模型与 **gpt-4o-mini** 模型:虽然声称gpt-4o-mini的性能与gpt-4o相当,但我发现gpt-4o-mini在完成复杂任务时明显存在问题,如在工作流程中作为 ReAct 代理进行规划和修正错误。然而,它在简单任务(如内容摘要)中表现得足够好。
创建中间文件:生成中间文件(摘要的 Markdown 文件和摘要布局的 JSON 文件)是一个有用的方法,它减轻了代理必须跟踪内容和幻灯片样式的负担,同时生成幻灯片的代码。
处理边缘案例:从头到尾运行工作流程揭示了许多边缘案例,特别是在验证幻灯片样式时。目前,通过迭代修改相关提示来处理这些问题。但我认为,促进某种类型的协作和人类参与机制将大大有助于此,同时也能提供更高的准确性。
python-pptx 的局限性。工作流程受限于 python-pptx 在 PowerPoint 幻灯片中能够实际渲染和操作的内容。因此,值得进一步考虑其他高效的幻灯片生成方式,例如使用 VBA。
用于摘要生成的代理和工具:与严格的逐步摘要生成过程不同,使用一个或多个具有工具访问权限的代理(目前是步骤函数)可以使工作流程更灵活,更适应未来的变化。
增强人类参与的互动。目前的实现不允许太多用户交互。让最终用户更多地参与工作流程,尤其是在涉及用户判断的任务中,如内容验证和精炼,这非常有益。一个方法是增加更多步骤,工作流程可以向用户请求验证,并考虑用户的反馈。人类的参与对于实时修复错误和进行更改是无价的。
论文查询引擎。还可以为每篇论文构建查询引擎,使用户能够提出问题并根据需要修改摘要。这有助于工作流结果的个性化。
综上所述,LlamaIndex 工作流是一个非常灵活且可定制的工具,用于构建复杂且量身定制的 AI 解决方案。它让我可以自由定义我的流程,同时具备可控性和灵活性,并能够利用库中的许多内置工具。
下一步是什么?
如前所述,主要的改进将是实现更多人机协作(human-in-the-loop)类型的功能。例如,允许设置更多的交互式检查点,用户可以在需要时覆盖步骤执行,将交互步骤集成到工作流中,并提供用户在任何阶段检查工作流是否产生满意输出的机会。与提供更好用户体验的目标一致,构建Streamlit 前端也是一个不错的补充,可以提供更多关于工作流执行的深入信息。拥有前端将使得用户能够实时监控工作流的进展,并根据需要更快速地调整轨迹。此外,获取用户反馈和验证、可视化中间结果和最终输出将为工作流增加透明度。所以请关注下一篇文章,了解这些变化!😃
感谢阅读!查看我的GitHub以获取完整实现。我期待听到你的想法、意见和反馈。我目前在Inmeta担任数据科学顾问,Inmeta 是Crayon Group的一部分。欢迎在LinkedIn与我建立联系。😊
我如何在两周内学习 LLM:一个全面的学习路线图

图片由 Midjourney 创作。
从初学者到高级者的逐日详细 LLM 学习路线图,以及一些学习建议
·发布于Towards Data Science ·8 分钟阅读·2024 年 10 月 18 日
--
被付费墙困住了?免费阅读!
理解 LLM 如何在后台运作,已经成为机器学习中的基本技能。不论是选择适合应用的模型,了解该领域的基础知识,还是关注关于 LLM 及其潜力的讨论——例如理解、创造,或是走向 AGI,第一步是了解它们是什么。
在本文中,我将分享我的学习经验,以及我在大约 14 天内学习 LLM 基础知识时找到的最有用的资源,告诉你如何在相对较短的时间内完成学习。这条学习路线图可以帮助你掌握几乎所有的要点:
为什么我开始这段旅程
我沉迷于深入理解概念,即使我已经了解它们。我已经能够阅读并理解关于 LLM 的研究,也可以构建代理或……
我是如何将 IPL 数据转化为令人着迷的条形图竞赛
数据故事化中创建引人入胜的动画可视化的逐步指南
·发表于 Towards Data Science ·阅读时间 8 分钟·2024 年 10 月 6 日
--
你是否曾在社交媒体上滚动时,被那些展示人口、公司,甚至是迷因股票涨跌的动画图表迷住了?
我知道我有过——像你们中的许多人一样,我一直对那些出现在社交媒体上的动画可视化着迷。你们知道的,就是那些迷人的条形图竞赛,展示人口增长,或是动态气泡图,说明人们如何度过一天的时间。我会一遍遍地看,惊叹于它们讲述的故事,哪怕没有一句话。


动画图表的示例——条形图竞赛(左)和动态气泡图(右)
所以,我对自己说:“我们来试试创建一个吧!更棒的是,我们可以用低代码方式来实现!”
这里有个 confession:虽然我已经做了数据和应用科学家一段时间了,但直到最近,我才知道这些图表叫做“动画图表”,而这些特定类型的图表被称为“条形图竞赛”,等等。
在这篇文章中,我分享了如何勇敢尝试并创建了我的第一个惊艳的动画可视化。这是最终效果的一个小预览:

最终可视化的一部分(2020–24)(由作者创建)
为什么选择动画可视化?
在我们快节奏的数字世界中,吸引并维持观众注意力比以往任何时候都更加具有挑战性。动画可视化正是在此时大放异彩。它们可以:
-
以易于理解的格式传达复杂的趋势变化
-
激发观众的情感,使数据更加难忘
-
鼓励在数据集中进行探索和发现
-
简化向非技术观众传达关键信息
作为数据专业人士,紧跟这些强大叙事工具的步伐至关重要。让我们深入了解如何有效地利用这些工具。
那么,我该从哪里开始呢?
作为一名狂热的板球迷,我知道我想做一些与 印度超级联赛(IPL) 相关的事情。但我不想只做一个普通的可视化。我想做一些能让即便是最随意的板球迷也会惊叹的作品。
就在那时,我突然意识到:如果我能展示每支 IPL 球队随时间推移的胜利次数呢? 那将是像在眼前展开整个 IPL 历史一样的体验!
在 Kaggle 上快速搜索让我发现了一个宝藏 —— 一个包含 2008 到 2024 年比赛结果的全面 IPL 数据集。我下载它的速度比 Jasprit Bumrah 的 yorker 还快,并将其保存为 ipl_matches_2008-2024.csv。
最新且完整的 IPL 数据集(更新至 2024 赛季)
现在,手头有了我的数据集,我面临着一个令人生畏的问题:我该如何将这些数据转化为那些炫酷的动画图表呢?
虽然我很擅长使用 Python 从零开始编写可视化代码,但我对低代码工具的高效性产生了兴趣。于是,遇到了 Flourish——一个我偶然发现的工具,承诺在不需要编码的情况下帮助创建令人惊叹的可视化 —— 真是太酷了!

在搜索工具以创建动画可视化时,我偶然发现了 “Flourish”。
Flourish 提供的可视化选项种类丰富(即使是免费版用户也可以使用)。在选择“条形图竞赛”选项后,点击“数据”切换按钮,我迅速理解了 Flourish 生成条形图竞赛可视化所需的数据要求。

理解 Flourish 中的数据需求
数据整理
当然,原始的 IPL 数据集远远没有达到这种格式,这意味着我有机会利用 GitHub Copilot 的强大功能来整理数据,并将其转换为所需的格式。
以下是我与 GitHub Copilot 进行的交互序列,帮助我将原始的 IPL 数据转换成所需格式(还包含一些有趣的 IPL 相关细节):
第一步,我大致解释了数据集最初的格式,以及我希望最终表示的方式:
这是 GitHub Copilot 返回的结果:
我惊喜地发现,这段代码几乎第一次就正确了!所示过程很简单:
提取每场比赛的唯一日期和独特队伍后,初始化一个数据框,将队伍作为行,日期作为列。现在,遍历每个日期并执行以下操作:
-
筛选出截至当前日期的比赛
-
计算每支队伍的累计胜场
-
更新数据框架以显示累计胜场
唯一的问题是这段多余的代码——去掉它就解决了我的问题:
# Ensure cumulative sum
if date != unique_dates[0]:
cumulative_wins.at[team, date] += cumulative_wins.at[team, unique_dates[unique_dates.index(date) - 1]]
cumulative_wins.head() 得到如下结果:

等等,别急!IPL,像所有好的戏剧一样,有着自己的曲折变化……
处理 IPL 的细节
尽管这些数据本身已经足够用于在 Flourish 中做条形图竞赛可视化,但我决定处理一些 IPL 的细节,以使可视化更加真实。
队伍更名
在 IPL 的历史中,曾多次发生过俱乐部老板更换,导致队伍重新品牌化的情况。还记得 德干充电器 变成了 太阳神海得拉巴 吗?
在数据集中,这些被视为单独的队伍,在单独的行中,其中在更名后,旧队伍会保留到那一年的累计胜场,而新队伍则从零胜场开始。
为了修改这个,我尝试了以下提示:
经过几处小修正,最终的代码如下:
现在,为了将其推广并应用于各种变化,我尝试将其转化为一个函数,并使用 Bing 查找这些重大队伍变动的年份,然后将该函数应用于它们:
太棒了!我已经能考虑到多年来发生的所有俱乐部更换情况。尽管如此,有一件事仍然困扰我……
解散的队伍
IPL 中有一些队伍只出现在几个赛季中,然后消失不见。它们在最终可视化中的存在,看起来没有必要。
所以,我尝试通过以下提示在它们最后一次出现后将其移除:
这给了我另一个很棒的功能,我可以将其应用于那些多年来解散的队伍(再次说明,这些数据是我从 Bing 找到的)。
队伍徽标
在 Flourish 的演示数据中,我注意到有一列包含了每个条形图竞赛中可以使用的图像链接。
为了让我的可视化更加突出,我想做一些类似的事情。所以,我找到了我们数据集中每支独特队伍的徽标图片,并将它们添加到一个新列中:
这个小小的细节显著提升了观众在动画过程中跟踪各队的能力
现在,我已经准备好创建可视化了!
将一切汇总
数据准备好并经过精细打磨后,是时候进行主要活动了!
上传到 Flourish 轻松无比——它能自动正确识别 标签、值 和 图像 列。
切换到 预览 切换按钮后,我得到了几乎符合我设想的效果——做了一些设置调整后,我们就完成了。
以下是我所做的更改:
-
使用“条形上的标签(坐标轴中的图像)”作为标签模式下的“标签”,以获得简洁的外观
-
将图像的 尺寸 设置为“适应”并将 形状 设置为“矩形”,以便清晰显示标志
-
减少了 计时器 % 大小 和 总计器 下的 计数器 & 总计器,以保持平衡
就这样——从一个简单的 CSV 文件到一个动态、有趣的可视化,讲述了 IPL 团队多年来的表现——只需要一点创意和一些数据魔法。🎉
这是最终输出👇🏼
IPL 团队获胜的最终条形图比赛可视化(由作者创建)
数据专业人士的关键收获
这个简单却富有吸引力的项目为数据科学家和分析师提供了几个有价值的见解,帮助他们提升故事讲述的能力:
-
低代码并不意味着低质量: 像 Flourish 这样的工具可以生成复杂的可视化,堪比定制编码的解决方案。
-
数据准备依然至关重要: 我们的数据科学技能在准备和结构化数据以进行有效可视化方面是无价的。
-
效率提升: 对于某些项目,低代码工具可以显著减少从数据到洞察的时间,而不牺牲质量。
-
可访问性: 这些工具可以帮助弥合数据团队与非技术利益相关者之间的沟通差距,促进洞察的更好传达。
作为数据专业人士,我们很容易沉浸在编码技能的强大之中。然而,重要的是要记住保持对新工具和方法的开放心态。像 Flourish 这样的低代码可视化平台并不取代我们的专业知识——它们增强了我们的能力,使我们能够更高效地创造引人入胜的数据故事。
通过将我们对数据处理和统计分析的深入理解与这些新工具的快速原型设计能力相结合,我们可以将数据故事讲述提升到新的高度。无论是向高层展示,还是为社交媒体创建内容,或是探索数据以获得自己的洞察,考虑将低代码可视化工具加入你的工具箱。
数据科学的未来不仅仅是处理数字——更是讲述能引起共鸣的故事。通过拥抱能够增强我们创造引人入胜可视化能力的工具,我们可以确保我们的洞察不仅能提供信息,还能激发灵感并推动行动。
关于作者
嗨,大家好!👋🏼
我是Tezan Sahu,一名微软的软件工程师 2(前数据与应用科学家 2),亚马逊 #1 畅销书作者(著作《Beyond Code》),同时也是《The Vision, Debugged》AI 时事通讯的共同作者。
我热衷于帮助有志成为数据科学家和软件开发者的人们启动他们的职业生涯,持续地产生影响,并成为人工智能和数据科学领域的有差异化的专业人士。
如果你有兴趣了解如何在数据科学行业保持领先,并通过领导力和思维方式的实用技巧提升你的成果,在 LinkedIn 上与我联系。
我如何作为数据科学家使用 ChatGPT
ChatGPT 如何提高了我作为数据科学家的工作效率
·发表于Towards Data Science ·阅读时间 8 分钟·2024 年 6 月 2 日
--

摄影:Shantanu Kumar:www.pexels.com/photo/chatgpt-webpage-open-on-iphone-16474955/
全世界都听说过 ChatGPT,但根据最近的新闻研究,只有 2%的人每天使用它。
作为数据科学家,我每天都会用它做很多事情。老实说,它在某些领域让我生产力翻倍。
在这篇文章中,我想解释一下我如何使用 ChatGPT,希望能给你一些新思路,帮助你更频繁地使用它,以提高你的产出,无论你是不是数据科学家。
学习与研究
我最喜欢的提示之一是“像我 5 岁那样解释”(ELI5)。它基本上让 ChatGPT 用一种非常简单的方式向我解释一个话题,往往比许多在线资源还要清晰。
例如,如果我们输入“像我 5 岁那样解释递归神经网络”。
作为数据工程师,我如何使用生成型 AI

我使用 AI。图片由作者提供
生成型 AI 目前非常流行。在本文中,我们深入探讨了一些数据工程师的实际应用示例。
·发表于 Towards Data Science ·阅读时长 6 分钟·2024 年 3 月 26 日
--
我是 Hugo, Orchestra* 的 CEO,Orchestra 是 Airflow 和现代数据堆栈的现代替代方案。*
引言
将生成型 AI 嵌入数据工程工作流和数据管道实际上是非常直接且令人满意的。
作为软件和业务用户之间的桥梁,数据团队处于一个无与伦比的位置,可以快速迭代具有重要商业影响的生成型 AI 用例。
具体来说,生成型 AI 可以用来总结大量的结构化和非结构化信息,这不仅扩大了数据团队可用数据的广度,同时也增加了这些数据的深度。
然而——很容易被生成型 AI 和它的“酷”或“潮流”吸引,而不是真的去使用它 来推动组织内有影响力的增长。这就是为什么数据团队需要一个数据和 AI 产品的集中可见性点如此基础性的原因。
在本文中,我们将讨论几种可以在现有数据管道中利用生成型 AI 的方法,以及如何量化结果。
如何赢得一场价值 10,000 美元的写作比赛
数据科学中的沟通
·发表于Towards Data Science ·阅读时长:5 分钟·2024 年 1 月 8 日
--
最近,我很幸运地赢得了 Kaggle 的一个写作比赛,并将我的作品发表在他们的2023 年 AI 报告中,尽管我绝不认为自己是一个完美的沟通者,但我想分享一些我认为帮助我赢得奖项的经验教训!
我特别为这个成就感到自豪,因为在我的经验中,作为数据科学家,我们所做的最重要但常常被忽视的一件事就是沟通和分享我们的结果。
在这篇文章中,我们将讨论使用何种类型的语言、如何谈论抽象概念、如何利用图表传达模式,以及整体结构等内容。希望这些技巧和方法能在下次你进行沟通或演讲时对你有所帮助!

图片来源:作者。(AI 生成)
第一条:避免行话
听起来很明显,对吧?但这件事尤其难——我们上大学/学院,或学习课程来提升我们的技能,这些课程向我们介绍了一系列的技术概念,每个概念都有一个特定的名称,我们逐渐理解它们。接下来,无论何时与其他学生交流,我们都使用这些术语,最终,等我们进入行业时,这些技术术语已经深深烙印在我们心中,我们不加思索地使用它们。
这有什么问题吗?在现实世界中,这些词对大多数人来说毫无意义。
让我给你举个例子——在表格数据领域,你可能正在训练一个模型,试图最小化一个基于缩略词的误差指标,比如平均预测误差(MAPE)。然而,通常我们与之沟通的人根本不理解这些概念。那么当我们告诉他们我们在最新的预测模型中如何降低 MAPE 时会发生什么呢?他们肯定不会感到印象深刻——最多他们会失去兴趣——最糟糕的情况是,他们可能会感到不安, wondering 是否应该知道我们在说什么。
那么解决方案是什么呢?关键是用更简单的日常语言来沟通复杂的概念。
在上面的例子中,当然我们仍然想突出我们的模型是准确的,但我们更好的做法是用简单的英语来解释,比如说我们已经提高了平均百分比误差。更好的做法是从模型的目标出发进行解释。例如,如果我们在预测天气,我们可能会说,我们把 12 小时预报的摄氏度误差从 5%降到了 1%。
第 2 点:使用例子
让我们面对现实吧——数据科学中的许多概念都非常抽象。你曾试过向妈妈解释你整天在做什么吗?是的,我也试过,结果从来不太好。
好消息是,大多数抽象概念可以通过一个具体的现实世界例子来解释清楚。在我附上的文章中,我尝试通过至少一个例子,甚至多个例子来让每个观点变得清晰。这在口头解释概念时也是如此,尤其是在与技术背景较少的听众沟通时更为重要。
那么哪些例子效果最好呢?这里的黄金法则是,例子与受众的相关性越强,效果越好。例如,在我的文章中,我经常以 Kaggle 项目为例,因为我知道阅读这篇文章的每个人都在 Kaggle 上,并且能产生共鸣!如果你是在向老师们传达大语言模型(LLM)的价值,或许你会提到它们在总结教科书章节,或者提供定制写作反馈方面的应用。
核心观点是:了解你的受众,内容自然就会跟随!
第 3 点:使用(清晰的)视觉效果
视觉效果很有趣!至少我觉得是...
一个巧妙的视觉效果或图形可以为任何演示增色不少。我个人认为,一个好的视觉效果是通过一种应该能让目标模式显而易见的方式来生成的。
以这个简单的注释表格为例——它展示了在 Kaggle 举办的简单表格回归和分类比赛中,获胜解决方案使用的模型类型。即便什么都不懂,我们的眼睛也能看到一个模式:LightGBM 和集成方法的列中有很多勾选标记,所以无论这些是什么,它们肯定很受欢迎。这是一个简单的见解,但通过将数据以恰当的方式排列,结果变得显而易见。
清晰的视觉效果不仅能吸引注意力,还能使你的数据叙事更加有效且值得记住,并且能节省大量的解释时间。

图片由作者提供。
第 4 条:强调关键点
我从小就不喜欢写文章——事实上,考虑到可能读这篇文章的人,我敢打赌你们大多数人也不喜欢!但有一个教训一直伴随着我,那就是我高中老师简单的文章结构——首先告诉他们,然后再告诉他们,然后再告诉他们一次,也就是说,介绍、正文和结论都在说同样的事情。
当时我并没有真正理解,但后来我一次又一次地看到了其中的智慧。如果你直接开始一个演示,那么人们就不会明白他们应该关注什么。如果你忘记总结演示,人们可能只会记住你的最后一个要点!为了避免这些陷阱,几乎总是从你的关键点开始,详细阐述它们,然后最后再总结一次,这样的结构无论在哪种媒介中都很有用——从文章到演示文稿。
这在数据科学中尤为重要,因为考虑到行业变化的速度,总有新的话题值得讨论,而且可能并不是每个人都像你一样理解你的专业领域,所以重复有助于突出主要观点。
所以,考虑到这一点,让我们总结一下我们的一些关键点……(看到了吗? 😃 )
结论
-
避免使用行话——不要让观众费劲地去理解最新的流行词汇或缩略语。
-
尽可能使用真实世界的例子——如果它们与目标观众相关,那就更棒了。
-
包括简单的视觉效果——确保信息明确。
-
也不要忘记清晰结构的重要性。
希望在考虑到这些想法后,你能够更好地吸引下一个观众的注意力。
感谢阅读,下次见!
我将如何在 2024 年从零开始学习 Python
我将如何学习世界上最流行的编程语言之一
·发表于 Towards Data Science ·阅读时间 8 分钟·2024 年 2 月 17 日
--

图片来源:James Harrison 于 Unsplash
如今,Python 是数据科学的首选语言。大多数前沿的机器学习库都基于 Python,而且你几乎无法找到没有 Python 作为要求之一的数据科学职位招聘信息。
然而,Python 在计算机科学的多个领域都有应用:
-
网页开发
-
游戏开发
-
后端工程
因此,对于想进入编程、数据科学领域,或希望成为开发者的人来说,Python 是一门非常有用且多功能的语言。
我已经用 Python 编程超过 4 年,在这篇文章中,我想解释如果我从零开始学习 Python,我会怎么做。
第一步:选择你的课程
首先,我会选择一个我喜欢的入门课程,或者…
如何成为一名数据科学家(如果我需要重新开始的话)
数据科学求职路线图和技巧
·发表于Towards Data Science ·阅读时间:11 分钟·2024 年 6 月 14 日
--

我已经作为一名数据科学家工作了近三年,在这篇文章中,我想分享如果我要从头开始学习数据科学,我会遵循的路线图,从你需要的知识到如何获得第一份工作。
数学

图片来自Antoine Dautry,来源:Unsplash
毋庸置疑,我会从数学开始。
数学是许多技术工作核心的一部分,我恐怕无法避免这一点。
数据科学就是用数据和算法解决问题,而数学是实现这一目标所需的最基本概念。工具和技术不断更替,但数学知识始终不变。
幸运的是,成为一名数据科学家所需的数学技能并不是博士级别的,大多数技能是……
如何学习人工智能(路线图)
一份关于如何有效学习人工智能的完整分解
·发表于Towards Data Science ·阅读时间 10 分钟·2024 年 8 月 5 日
--

人工智能无处不在;我们在新闻中听到它,工作中接触到它,甚至在与家人交谈时也会提到它。显然,它将继续存在,并将在可预见的未来影响我们的生活。
如果你和我一样,我总是对新技术及其工作原理充满好奇,所以在本文中,我将介绍一份路线图,帮助你从零开始学习人工智能。
让我们开始吧!
注意:你也可以观看这个博客文章的视频版,并在我的 YouTube 频道下载完整的路线图 PDF:
什么是人工智能,为什么要学习它?
自从 2022 年 ChatGPT 发布以来,人工智能引发了大量的炒作。然而,人工智能作为一个概念已经存在很长时间,按照目前的形式可以追溯到 1950 年代,当时神经网络的起源就已经出现。
最近的爆发可以归因于许多因素,比如计算资源的增加、更多可用的数据,以及……
如果我能从头开始,如何在 2025 年学习 AI
今天领域的 5 步路线图
·发布于 Towards Data Science ·6 分钟阅读·2024 年 12 月 13 日
--
如今,越来越多的人尝试学习 AI。尽管网上有无数免费的学习资源,但在这个快速发展的领域中,导航可能让人感到不知所措(尤其是对于初学者)。在本文中,我将讨论,如果从现在开始学习 AI,基于我当前的知识和现有的工具,我会如何着手。

图片来自 Canva。
适合谁?
鉴于如今对 AI 感兴趣的背景范围非常广泛,我尽力使这篇指南具有普遍的可访问性。然而,没有哪份指南可以适合所有人。以下是我考虑的几个特定群体。
-
技术专业人士尝试提升技能以促进职业发展
-
商业领袖那些多年没有编写代码的人,试图跟上不断变化的技术环境
-
创业者构建 AI 原生产品
-
学生们尝试提升他们的技术 AI 技能
我如何在 6 年后重新学习机器学习
一些粗略的指导原则
·发表于Towards Data Science ·6 分钟阅读·2024 年 12 月 11 日
--
到本月为止,我已经有 6 年的机器学习经验了。
我第一次接触机器学习时,是经典的机器学习方法(比如支持向量机、k-means)。那时我觉得这个话题相当无聊:太多理论,什么都做不出来。随着我参加越来越多的机器学习课程,这种情况发生了变化。

图片由Arian Darvishi提供,来源于Unsplash
我记得坐在一堂关于自然语言处理(NLP)和神经网络的讲座上。那位出色的讲师展示了最新 NLP 模型的参数数量图表:参数数量高达数十亿!
他接着说:哦,这个图已经过时了。现在的模型更大了。
哇!真是个目睹如此进展的好时机;从百万到十亿,再到…万亿参数。大约在那个时候,我开始迷上了所有关于深度学习的内容,参加了所有可以上的 AI 课程,还做了一些在线课程(Coursera、MIT 深度学习讲座)。
我越是学习机器学习和深度学习,就越意识到:总会有更多东西等待被发现。6 年后,我可能比过去的自己多知道一些,但我仍然是一个彻底的初学者。
我如何学习机器学习(如果我可以重新开始)
如何在今年有效学习机器学习的详细步骤
·发表于 Towards Data Science ·阅读时间:8 分钟·2024 年 1 月 27 日
--

作者提供的图片。
我已经作为数据科学家工作超过两年了。随着时间的推移,我学习并主要研究了机器学习(ML)。对我来说,这可能是工作中最迷人的部分。
ML 是一个庞大的领域,有很多内容需要学习和理解。然而,一步一步来会使整个过程不那么令人生畏,而且更容易处理。
在这篇文章中,我想回顾一下,如果我需要从零开始学习机器学习,我会采取哪些步骤。让我们开始吧!
数学
机器学习围绕算法展开,算法本质上是一系列数学运算。这些算法可以通过各种方法和多种编程语言实现,但它们的基本数学原理是相同的。
一个常见的论点是,学习机器学习不需要数学,因为大多数现代……
我如何在 2024 年学习成为一名数据分析师
…如果我可以重新开始的话。
·发表于Towards Data Science ·阅读时长 8 分钟·2024 年 9 月 11 日
--

图片来源:Lukas Blazek 通过Unsplash
我在 2020 年得到了我的数据分析师工作。
我享受着在家工作的好处,赚了不错的钱,并且有灵活的工作时间。
不幸的是,从那时起,情况发生了变化。
每天,我都会在收件箱中收到数十条来自候选人的 LinkedIn 消息,他们尽管已经掌握了必要的分析技能,但仍在为找到工作而苦苦挣扎。
这些人已经花费了数千美元参加训练营和在线课程,却只得到了低薪的工作机会和拒绝邮件。
若想观看视频版本,请点击这里。
今天,成为一名数据分析师比 2020 年更加困难。
这是为什么:
1. 竞争
灵活的工作时间和丰厚的薪水吸引了来自各行各业的人们转行进入分析领域。
2. 技能的普及
因果推断在学术界和行业中的不同之处?

图片来源:Element5 Digital来自Unsplash
《为什么的书》(The Book of Why)系列的附加文章
·发表于Towards Data Science ·7 分钟阅读·2024 年 1 月 17 日
--
在两个月内,我们读完了《为什么的书》(The Book of Why,),这为我们展示了因果关系迷人的世界。正如承诺的那样,我有一篇附加文章来正式结束我的第一篇和我一起读书系列。
受到我自己作为学术研究者的背景启发,在博士课程期间我研究了经济学中的因果推断,以及作为数据科学家在行业中建立因果模型以进行需求预测的经验,本文作为附加文章,我想分享我对因果推断概念的理解,以及它在学术界和工业界应用中的相似性与差异性。
差异
由于学术研究和行业应用在性质和目的上的不同,因果推断的工作流程在两者之间存在显著差异。
速度
学术研究通常以较慢的节奏进行,从形成想法到得出最终结论。它不仅关注因果结论本身的可信度,还关注涉及的数据、使用的方法以及研究的稳健性。因此,研究过程往往会延长,以验证数据的适用性,进行…
如何利用大型语言模型推动基因编辑革命
|人工智能| 大型语言模型(LLM)| 基因编辑| 医学中的 AI|
基因编辑有可能治愈大多数疾病,而大型语言模型可以加速这一梦想的实现
·发表于Towards Data Science ·9 分钟阅读·2024 年 5 月 9 日
--

由作者使用 AI 生成的图像
基因就像故事,而 DNA 则是写故事的语言。——山姆·基恩
生成性 AI 可以创作诗歌、代码、博客文章等。这一切都是通过对文本的训练实现的。我们常常忘记,文本其实是字符的序列,这些字符可以以复杂的方式组合,进而产生无限且复杂的意义。同样,生命也由少数几个基本字符组成(DNA只有 4 种,蛋白质只有 20 种),这些字符的无限组合使得我们今天能拥有如此惊人的生物多样性。
如果我们是由序列构成的,而语言模型能够分析这些序列,那为什么不利用语言模型来分析 DNA 和蛋白质序列呢?
这就是过去两年革命的基础。 这场革命始于AlphaFold2,研究人员通过使用一个训练有素的蛋白质序列语言模型,成功解决了一个困扰了 100 年的问题……
LLM 如何思考
研究论文概览:“扩展单义性:从 Claude 3 Sonnet 中提取可解释特征”
·发表于Towards Data Science ·阅读时间 10 分钟·2024 年 6 月 7 日
--

图片由 DALL-E 生成
你是否曾想过 AI 模型是如何“思考”的?试想一下,能够窥视机器的大脑,看着它的齿轮转动。这正是 Anthropic 的一篇突破性论文所探讨的内容。论文标题为《扩展单义性:从 Claude 3 Sonnet 中提取可解释特征》,研究深入了解并解释 AI 的思维过程。
研究人员成功从 Claude 3 Sonnet 模型中提取出一些特征,展示了该模型在思考关于名人、城市,甚至软件安全漏洞时的表现。这就像是窥视 AI 的大脑,揭示了它理解并用于做决策的概念。
研究论文概述
在这篇论文中,Anthropic 团队,包括 Adly Templeton、Tom Conerly、Jonathan Marcus 等人,旨在让 AI 模型更加透明。他们专注于 Claude 3 Sonnet,一个中型 AI 模型,目标是扩展单义性——本质上是确保模型中的每个特征都有明确的、单一的含义。
但为什么扩展单义性如此重要?单义性究竟是什么意思?我们很快就会深入探讨这个问题。
LLM 将如何使探索性数据分析民主化
或者,当你觉得生活太艰难时,不妨去和 Claude 聊一聊
·发表于Towards Data Science ·阅读时间 15 分钟·2024 年 6 月 9 日
--

当我想到理解复杂系统所面临的挑战时,我常常回想起在 Tripadvisor 工作时发生的一件事。我曾帮助我们的机器学习团队为增长营销团队进行分析,以了解哪些客户行为能够预测高生命周期价值(LTV)。我们与一位才华横溢的博士级数据科学家合作,他训练了一个逻辑回归模型,并将系数作为第一次初步输出。
当我们和增长团队一起查看分析结果时,他们感到困惑——逻辑回归系数很难解读,因为它们的尺度不是线性的,而最终最具预测性的特征也不是增长团队可以轻松影响的东西。我们都沉思了一会儿,开启了一个后续分析的工单,但像往常一样,两个团队很快就转向了他们下一个闪亮的想法。数据科学家有一些优先级很高的工作要做,比如我们的搜索排名算法,实际上,增长团队将这份分析丢进了垃圾堆。
我仍然会回想起那次练习——我们是不是太早放弃了?如果反馈循环更紧密会怎样?如果双方都继续挖掘,结果会怎样?第二次或第三次分析会揭示什么?
在大规模解锁探索性分析
上面的轶事描述了一个探索性分析,它并未完全成功。探索性分析不同于描述性分析,后者仅旨在描述发生了什么。探索性分析寻求对一个系统有更深的理解,而不是针对一个明确的问题。考虑以下几种你在商业环境中可能遇到的问题:

注意,探索性问题是开放式的,旨在提升对复杂问题空间的理解。探索性分析通常需要更多的循环和“领域专家”与实际进行分析的人之间更紧密的合作,而这两者通常不是同一个人。在上面的轶事中,合作不够紧密,反馈循环不够短,我们没有投入足够的循环。
这些挑战是为什么许多专家倡导“配对分析”方法来进行数据探索。类似于配对编程,配对分析将分析师和决策者聚集在一起,实时进行探索。不幸的是,由于资源和时间的限制,分析师和决策者之间这种紧密的合作伙伴关系在实践中很少发生。
现在想一想你所在的组织——如果每个决策者都有一位经验丰富的分析师与他们配对会怎样?如果他们能够得到分析师的全程关注,并且随时向他们提出后续问题呢?如果这些分析师能够轻松切换上下文,跟随他们合作伙伴的思维流畅地自由联想各种想法和假设呢?
这是大语言模型(LLM)在分析领域所带来的机会——承诺任何人都可以在技术分析师的帮助下进行探索性分析。
让我们来看看这在实践中如何体现。以下案例研究和演示展示了具有领域专长的决策者如何与能够查询和可视化数据的 AI 分析师有效配对。我们将比较 ChatGPT 的 40 模型与使用 Tableau 进行的手动分析的数据探索体验,这也将作为潜在幻觉的错误检查。
关于数据隐私的说明: 以下部分链接的视频演示使用的是纯合成数据集,旨在模仿真实的商业模式。有关 AI 分析师的数据隐私和安全性的一般说明,请参见数据隐私。
案例研究:电子商务分析
想象一下:你是一个电子商务服装网站的忙碌高管。你有一个包含预定义的高级关键绩效指标(KPI)的执行摘要仪表板,但有一天早上你查看它时,发现了一些令人担忧的事情:月度市场收入比上个月下降了 45%,但为什么下降并不立即清楚。

你的思维会同时朝几个不同的方向展开:是什么导致了收入下降?这个问题是否仅限于某些渠道?问题是否局限于某些消息类型?
但更重要的是,我们能做些什么呢?最近什么有效?什么无效?这一季我们看到哪些季节性趋势?我们如何利用这些趋势?
为了回答这些开放性问题,您需要进行一个中等复杂度的多变量分析。这正是 AI 分析师可以提供帮助的类型的任务。
诊断分析
让我们从更仔细地观察一下月度收入的下降开始。
在我们的示例中,我们看到与营销活动相关的整体收入出现了大幅下降。作为分析师,有两个并行的思路可以开始诊断根本原因:
将整体收入分解成多个输入指标:
-
总消息发送量:我们发送的消息是否减少了?
-
打开率:人们是否打开了这些消息?即,邮件主题是否存在问题?
-
点击率:受众是否不太可能点击消息中的链接?即,消息内容是否存在问题?
-
转化率:点击后,受众是否不太可能购买?即,着陆页体验是否存在问题?
在不同类别维度下隔离这些趋势
-
渠道:这个问题是所有渠道都有,还是仅限于某些渠道?
-
消息类型:所有消息类型中是否都有观察到此问题?
在这种情况下,通过几个提示,LLM 能够识别出这两段时间内发送的消息类型之间的巨大差异——即在 7 月进行了 50%的折扣促销,而在 8 月没有进行。
即席数据可视化
所以现在这个下降看起来更有意义了,但我们不可能每个月都进行 50%的折扣促销。我们还能做些什么来确保充分利用营销接触点呢?让我们看看我们表现最好的活动,看看除了促销活动外,还有什么能进入前 10 名。
数据可视化工具支持点选界面来构建数据可视化。今天,像 ChatGPT 和 Julius AI 这样的工具已经能够忠实地复制迭代的数据可视化工作流程。
这些工具利用 Python 库来创建和呈现静态数据可视化以及交互式图表,直接在聊天界面中进行。这些可视化可以通过自然语言进行调整和迭代,流程相当顺畅。随着代码模块、图像渲染和交互式图表元素的引入,聊天界面已接近于 jupyter 笔记本所普及的“笔记本”格式。
通过几个提示,您通常可以像使用数据可视化工具如 Tableau 的高级用户一样快速得到数据可视化。在这种情况下,您甚至不需要查阅帮助文档来学习 Tableau 的双轴图表如何工作。
在这里,我们可以看到,“新品到货”消息即使在大量发送的情况下,也能为每个接收者带来强大的收入:

季节性与预测
所以,“新品到货”似乎产生了共鸣,那么下个月我们应该确保推出哪些新产品呢?我们即将进入九月,想了解客户购买模式在这个时节如何变化。我们预计哪些产品类别会增加?哪些会减少?

再次通过几个提示,我们得到了清晰、准确的数据可视化,而且我们甚至不需要弄清楚如何使用 Tableau 的快速表计算功能!
市场篮子分析
既然我们已经知道下个月哪些产品类别可能会增加,我们可能想要调整一些交叉销售的推荐。那么,如果男士运动外套将迎来最大增幅,我们如何查看哪些其他类别是与这些商品最常一起购买的?
这通常被称为“市场篮子分析”,进行这种分析所需的数据转换稍显复杂。事实上,在 Excel 中进行市场篮子分析如果没有使用笨重的插件几乎是不可能的。但有了 LLM,只需稍作停顿,清晰地提出问题即可:
“嘿 GPT,对于包含男士运动外套的订单,哪些产品类型是同一客户在同一购物车中最常购买的?”
企业流程将如何发展,以融入人工智能?
上面的演示展示了一些 LLM 如何在大规模数据驱动决策中提供支持的例子。主要参与者已意识到这一机会,整个生态系统正在迅速发展,将 LLM 融入到分析工作流中。请考虑以下内容:
-
当 OpenAI 去年发布其“代码解释器”测试版时,很快将该功能更名为“高级数据分析”,以便与早期用户的使用方式相匹配。
-
使用 GPT4o,OpenAI 现在支持渲染交互式图表,包括更改颜色编码、悬停时显示工具提示、排序/筛选图表、选择图表列并应用计算的功能。
-
像 Julius.ai 这样的工具正在出现,专门解决关键的分析用例,在合适的情况下提供对多个模型的访问。Julius 提供对 OpenAI 和 Anthropic 的模型访问。
-
提供商正不断简化数据共享过程,从静态文件上传扩展到 Google Sheet 连接器以及更高级的 API 选项。
-
像 Voiceflow 这样的工具正在出现,支持以检索增强生成(RAG)用例(如数据分析)为重点的 AI 应用开发。这使得第三方开发者更容易将自定义数据集连接到多个 LLM 提供商的不同模型。
鉴于此,让我们花一点时间想象一下商业智能分析在未来 12-24 个月可能的发展。以下是一些预测:
人类分析师在提出正确问题、解读模糊数据和不断完善假设方面仍将至关重要。
LLM 在分析领域的主要优势在于其能够……
-
将自然语言转化为代码(Python、R、SQL)
-
智能清理数据集
-
有效地可视化数据
仍然需要领域专家来解读趋势并迭代假设。人类将减少查询和数据可视化构建,但在推动探索性分析方面依然至关重要。
这一领域的技能将比查询和数据可视化的技术技能更为重要。那些发展出强大数据素养和批判性思维能力的决策者,将大大扩展他们在 LLM 帮助下探索和理解复杂系统的能力。
企业大规模采用 AI 分析师的最大障碍将是关于数据隐私的担忧。这些担忧将通过多种方式成功解决。
尽管主要 LLM 提供商的隐私政策非常完善,数据共享仍将是一个主要的焦虑来源。然而,已经有多种方法正在探索解决这一问题。LLM 提供商已经开始尝试使用具有增强隐私和安全措施的 专用实例。其他解决方案可能包括在通过 API 共享数据时进行加密/解密,以及仅使用 LLM 进行代码生成并共享虚拟数据。
预计 LLM 提供商和云数据库提供商(如 Gemini / BigQuery、OpenAI / Microsoft Azure、Amazon Olympus / AWS)之间的垂直对接将进一步加强,以帮助解决这一问题。
一旦数据隐私和安全问题得到解决,早期采用者最初将面临速度、错误处理和小范围幻觉等挑战。这些挑战将通过 LLM 提供商、组织和个人的共同努力得到克服。
主要的 LLM 提供商正在以高速提高速度和准确性。 预计这一趋势将继续,分析用例将从专业化和选择性上下文(记忆、工具访问和狭义指令)中受益。
随着更深度的集成出现并且成本下降,组织将有动力投资于基于 LLM 的分析。
个人将能够比 SQL、Python / R 和数据可视化工具更快速地学习有效的提示词编写方法。
BI 团队将减少对分析请求的响应,更加专注于构建和支持底层数据架构。全面的数据集文档将成为未来所有 BI 数据集的关键。
一旦 LLM 达到一定的组织信任水平,分析将主要由用户自助完成。
“数据字典”一直是 BI 组织的次要关注点,但未来任何希望利用 AI 分析师的组织都必须将其作为基本要素。
随着 LLM 能够实时执行复杂的数据转换,所需的汇总表将会减少。 LLM 使用的表将更接近“银级”而非“金级” 奖章架构。
一个明显的例子是本文稍后讨论的市场篮子分析。传统的市场篮子分析涉及通过复杂的转换创建一个全新的数据集。使用 LLM 时,可以使用“原始”表,并且这些转换可以在“恰好及时”的情况下执行,仅针对最终用户感兴趣的产品或类别。
这里的反面观点是,原始数据集更大,因此会产生更高的成本,因为它们需要更大量的令牌作为输入。 这是一个重要的权衡,取决于用于利用 LLM API 的成本模型。
语音将成为 LLM 交互的主导输入方式,包括在分析用例中。
语音技术通过语音助手(如 Siri 和 Alexa)一直未能真正普及。然而,随着 GPT-4 和其在实时对话语音中的能力的发布,预计语音交互将跃升为主流应用。
交互式可视化将成为分析用例中主导的输出形式。
已经有充分的研究表明,人脑在处理视觉信息方面远比其他任何媒介更高效。 预计探索性分析的用户体验将遵循语音/可视化模式,用户提出问题,AI 分析师尽可能地将信息可视化。
最有效的 LLM 分析系统将利用来自单一提供商的多个模型,智能地使用针对特定任务所需的最低复杂度模型,从而节省令牌成本。
随着 LLM 提供商不断发布新模型,一种基于令牌的定价结构应运而生,该结构根据所使用模型的复杂性应用乘数。也就是说,GPT-4o 的回答比 GPT-3.5 的回答要贵。为了降低成本,基于 AI 的应用将需要优化不同模型之间的使用。由于发送大量数据所带来的成本,预计组织将限制数据共享,仅与单一提供商共享数据。
扩展的上下文窗口将允许用户进行长期分析,保持上下文的连贯性,随着新数据的到来和假设的变化,能够跨越几天或几周进行分析
LLM 的上下文窗口大小决定了可以作为输入传递多少信息。对于像数据分析这样的 RAG(检索增强生成)用例,这个值可能特别高。更大的上下文窗口允许传递更多信息,并且能够进行更长时间的对话交换。
目前,谷歌的 Gemini 1.5 模型具有所有 LLM 中最大的可用上下文窗口:

2024 年设置 AI 分析师
如果你有兴趣尝试将 LLM 用于分析用例,进行一些初始配置是必要的。阅读以下部分,获取一些关于如何获得最佳性能的建议,并避免一些常见的陷阱。
初始配置
数据集和数据字典
首先,您需要为 LLM 提供访问数据的权限。尽管这听起来显而易见,您共享的数据集必须包含回答您希望向 LLM 提出的问题所需的数据。更重要的是,数据集中所有字段需要有清晰、书写良好的数据字典,以便 LLM 能够充分理解每个指标和维度。上述演示中使用的数据集和数据字典可以在这里查看:
数据连接
与 LLM 共享数据的方式有多种,这取决于您使用的模型:
-
文件上传: 当前最简单的方法是通过网页应用的 UI 上传静态文件。ChatGPT、Claude 和 Julius.ai 都支持通过网页 UI 上传文件。
-
Google Sheets: ChatGPT 和 Julius.ai 原生支持从 Google Sheets 导入数据。
-
API: 对于更复杂的用例或 AI 驱动的应用,您可以通过 API 共享数据集。更多细节请参阅您提供商的开发者文档。
数据隐私
目前,除非您安装了像Meta 的 Llama这样的开源 LLM 并在本地运行,否则必须通过上述方法之一将数据发送到 LLM。这引发了明显的安全性和数据隐私问题。
如果您想分析包含 PII(个人身份信息)如 customer_ids 的数据集,请考虑是否聚合后的数据集足以满足您的需求。如果您认为预先聚合的数据集不足以满足需求,请确保在上传前加密这些字段,以免泄露 PII。
还要考虑是否在分享有关贵组织的机密信息。如果发生数据泄露,您不希望自己因为将敏感信息暴露给第三方而承担责任。
您可以在此查看文中提到的模型的隐私政策:
-
Anthropic:
support.anthropic.com/en/collections/4078534-privacy-legal -
Julius:
julius.ai/docs/privacy-policy
自定义指令
如果您正在创建一个自定义 GPT,可以为该 GPT 提供一套自定义指令来指导其行为。对于其他不支持自定义实例的模型,如 Claude 或 Gemini,您可以通过包含这些指南的长格式提示来开始对话。在上面演示中使用的自定义 GPT 的指令可以在这里找到:电商 BI 分析师自定义指令
根据我的经验,以下指南对 AI 分析师非常有用:
-
身份: 告诉 LLM 它的身份。例如:“你是一个专家级的商业智能分析师,负责解答关于电商服装零售商的问题。”
-
数据集: 如果您计划始终使用相同的数据集,值得为 LLM 提供一些关于数据集性质的额外内容。这可以补充数据字典,并澄清 LLM 对数据的理解。例如:“该数据集包含了电商服装零售商在 2022 年 7 月和 8 月两个月内记录的所有购买数据。每一行代表一次购买事件,包含关于所购买商品以及购买者的客户数据。”
-
数据可视化最佳实践: LLM 渲染的图表可能需要一些微调,以提高人类的可读性。这可能随着时间的推移得到改善,但目前我发现将一些数据可视化的最佳实践“硬编码”到自定义指令中非常有用。例如:“始终确保轴标签通过以下一些方式变得清晰可见:减少显示标签的数量(例如,每隔一个标签或每隔三个标签显示一个标签),重新格式化标签的方向,或添加额外的填充。”
-
计算字段: 为了消除派生指标计算中的任何歧义,向 LLM 提供明确的指导是有用的,关于如何计算这些指标。例如:“点击率” = “唯一点击”总和 ÷ “唯一打开”总和
-
一般指导: 在你尝试使用 LLM 时,注意任何不希望看到或意外的行为,并考虑向自定义指令中添加明确的指导。例如:“当问题中包含‘显示我’这几个词时,始终尝试以数据可视化的方式回答。”
有时需要对 GPT 实例进行“引导”,以确保它已经阅读并理解了这些指令。在这种情况下,简单地问它一些关于自定义指令内容的问题,以确认它已经掌握了这些内容。
需要注意的事项
处理时间: GPT 有时可能需要相当长的时间来生成回应。上面的示范已经为简洁性和清晰度进行了编辑。根据系统当前的负载情况,你可能需要保持耐心,因为 LLM 正在分析数据。
错误信息: 当你开始尝试使用 LLM 进行分析时,可能会遇到频繁的错误信息,尤其是在调整自定义指令和优化提示词时。通常,LLM 在遇到错误时会自动重试并成功修正。有时,它可能会直接失败。当这种情况发生时,你可能需要检查正在使用的代码,并进行故障排除。
信任,但要验证: 对于更复杂的分析,进行抽查以确认模型输出是否正确。在大型语言模型(LLM)进一步发展之前,你可能需要在 Jupyter Notebook 中本地运行 Python 代码,以了解模型的处理方式。
幻觉: 我发现,当讨论引用具有客观真实数据集的知识库时,幻觉的可能性要小得多。然而,偶尔我也会观察到 LLM 在计算的指标定义上出错,或在分析元素的性质上口误。
结论
LLMs 将深刻改变分析领域,自动化查询和可视化工作流程。在未来的 2-3 年里,能够将 LLMs 融入分析工作流程的组织,并重新调整 BI 团队以支持这一新技术的,将在战略决策中占据重要优势。这里的潜在价值足以克服最初在数据隐私和用户体验方面的挑战。
用不朽的 Al Swearengen 的话来说:“一切都会变化;不要害怕。”
除非另有说明,所有图片均为作者提供
从零开始训练 LLM 需要多长时间?
估算训练 X 亿个 LLM、Y 万亿个 tokens 以及 Z 个 GPU 计算所需时间的指南
·发表于Towards Data Science ·阅读时间:5 分钟·2024 年 10 月 28 日
--

图片由作者提供
简介
每个从事 LLM 训练的机器学习工程师都曾面临过来自经理或产品负责人提出的问题:“训练这个 LLM 需要多长时间?”
当我第一次尝试在网上找到答案时,我遇到了许多涵盖通用话题的文章——训练技巧、模型评估等。但没有一篇文章解决了我核心的问题:我该如何估算训练所需的时间?
因为缺乏明确的实用指导,我决定自己创建一套方法。在这篇文章中,我将带你了解一种简单的估算方法,帮助你快速估算基于 LLM 的训练所需的时间,这个估算基于模型的规模、数据量以及可用的 GPU 算力。
方法
目标是量化训练过程中处理数据和更新模型参数所需的计算要求,以FLOPs(浮点运算次数)表示。接下来,我们基于所选 GPU 的类型和数量,估算系统的吞吐量,以FLOPS(每秒浮点运算次数)表示。一旦所有内容都在相同的尺度上表达,我们就可以轻松计算训练模型所需的时间。
所以最终的公式是相当直接的:

让我们深入了解如何估算这些变量。
数据和模型的 FLOPs
基于 Transformer 的 LLM 在正向传播过程中,每个 token 的大致加减乘除运算量约为以下的 FLOPs:

从 论文中估算 Transformer 模型在前向传播时每个 token 的 FLOP 数
其中,乘法累加操作在矩阵乘法中引入了二的因素。
反向传播所需的计算量大约是前向传播的两倍。这是因为在反向传播过程中,我们需要计算每个权重的梯度以及相对于中间激活值的梯度,特别是每一层的激活值。
考虑到这一点,每个训练 token 的浮动点运算量可以估算为:

从 论文中估算 Transformer 模型大小为 N 在前向和反向传播时每个 token 的 FLOP 数
计算这些估算值的更详细数学推导可以在作者的论文中找到,链接见 这里。
总结一下,训练大小为 N 的 Transformer 模型和包含 P 个 token 的数据集的 FLOPS 可以估算为:

训练基础设施的 FLOPS
今天,大多数大型语言模型(LLM)都是使用 GPU 加速器进行训练的。每种 GPU 模型(如 Nvidia 的 H100、A100 或 V100)都有其自己的 FLOPS 性能,具体取决于所使用的数据类型(形态)。例如,使用 FP64 进行的运算比使用 FP32 的运算要慢,依此类推。特定 GPU 的峰值理论 FLOPS 通常可以在其产品规格页面上找到(例如,这里 是 H100 的页面)。
然而,对于 GPU 的理论最大 FLOPS,通常在训练大型语言模型时并不那么相关。这是因为这些模型通常在成千上万的互联 GPU 上进行训练,其中网络通信效率变得至关重要。如果设备之间的通信成为瓶颈,它可能会大幅降低整体速度,使得系统的实际 FLOPS 远低于预期。
为了解决这个问题,重要的是跟踪一个叫做模型 FLOPS 利用率(MFU)的指标——即观察到的吞吐量与理论最大吞吐量的比率,假设硬件在没有内存或通信开销的情况下以峰值效率运行。在实践中,随着参与训练的 GPU 数量增加,MFU 通常会下降。使用当前的设置,要实现超过 50% 的 MFU 是具有挑战性的。
例如,LLaMA 3 论文的作者报告称,在使用 16,000 个 GPU 进行训练时,MFU 为 38%,即每个 GPU 的吞吐量为 380 太 FLOPS。

在 论文中报告了不同配置下每个 GPU 训练 Llama3 模型时的 TFLOPs 吞吐量。
总结一下,当进行模型训练的简易计算时,遵循以下步骤:
-
确定你的选择的 GPU 支持的数据类型的理论峰值 FLOPS。
-
根据 GPU 的数量和网络拓扑结构,通过基准测试或参考开源数据(如 Meta 工程师的报告,见上表)估算 MFU(模型 FLOPS 利用率)。
-
将理论 FLOPS 乘以 MFU,得到每个 GPU 的平均吞吐量。
-
将步骤 3 的结果乘以参与训练的 GPU 总数。
Llama 3 405B 的案例研究
现在,让我们用我们粗略的计算来估算训练一个 405B 参数模型需要多长时间。
LLaMA 3.1 (405B)使用 15.6 万亿个标记进行训练——一个巨大的数据集。训练如此规模的模型所需的总 FLOPs 可以按如下方式计算:

作者使用了 16,000 块 H100 GPU 进行训练。根据论文,平均吞吐量为每个 GPU 400 teraflops。这意味着训练基础设施可以提供总吞吐量:

最后,通过将所需的总 FLOPs 除以可用吞吐量并将结果转换为天数(因为我们真正关心的是训练天数),我们得到:

奖励:训练 Llama 3.1 405B 的费用是多少?
一旦知道了训练设置中每个 GPU 的 FLOPS,你就可以计算出训练给定规模和数据集的模型所需的总 GPU 小时数。然后,你可以将这个数字乘以云服务提供商的 GPU 每小时费用(或你自己的 GPU 每小时费用)。
例如,如果一块 H100 GPU 的费用大约是每小时 2 美元,那么训练这个模型的总费用大约为 5200 万美元!下面的公式解释了这个数字是如何得出的:

参考文献
[1] 神经语言模型的扩展法则 由 Jared Kaplan 等人编写。
[2] Llama 3 模型群体 由 Meta AI 团队编写
一碗幸运魅力麦片有多幸运?
·发表于 Towards Data Science ·阅读时间 16 分钟·2024 年 3 月 16 日
--
**tl;dr** 版本: 一组学生帮助设计并进行了一项实验,以确定一盒幸运魅力麦片中每碗是否都同样“幸运”。结果发现,并非如此。我们估计每增加一碗,平均减少约 2.7 个魅力糖果。这意味着从第一碗到最后一碗,糖果数量下降了超过 50%。麦片的重量似乎也起到了作用,保持碗恒定的情况下,每增加 1 克麦片,我们估计大约多出 0.5 个糖果。碗和重量之间的相互作用在统计上并不显著。
请参阅这个 GitHub 仓库获取数据、代码、照片、等等。

图 1:它们神奇地美味!(图片由作者提供)
背景
在 2010 年代初期,互联网上发生了一场关于“双重夹心”奥利奥是否真的是双重夹心的调查风波。(答案是否定的。)这是一个引人入胜的想法,从那时起,关于这一主题已经写了大量的资料,可以从这里开始了解。这一讨论引起了足够的关注,以至于一些老师显然将这个实验作为课堂活动重复进行,而当地学生也报告说,在超过 10 年后的今天,他们在自己的学校进行过类似的实验。
引言
2023 年夏天的一个早晨,我正吃着一碗幸运符号做早餐。盒子几乎空了,我叹了口气自言自语:“真希望这盒快点吃完,好让我能打开新的一盒……”如果你和我一样,或者和其他数百万喜爱幸运符号的人一样,那你一定很喜欢它,并且从你记事起就一直喜欢,毕竟它已经存在了60 年。它们真的神奇美味。可是,坐在那个不满的夏日早晨,手里拿着勺子,我突然意识到,我吃的这碗麦片似乎没有以前的那么神奇。它好像缺少了些什么。(当然是那些符号了。)这是我的想象吗?这种效果真的存在吗?如果有的话,能测量吗?
那时我正好在教一门本科的概率与统计课程,和四名学生一起,我们决心要弄明白这个问题。
实验和数据
经过一些讨论,团队决定采用以下材料和方法。
材料
-
六(6)盒家庭装幸运符号(18.6 盎司,527 克)
-
电子厨房秤
-
两个塑料“碗”,容器 A 和容器 B,分别为 40.125 克和 28.375 克。
-
大碗用于丢弃物,一些垃圾袋,其他配件
幸运符号是从我们当地的零售商沃尔玛购买的。n = 6 盒并没有什么特别的,它仅仅是一个人能用双手一次性携带到卡法罗大厅 6 楼的盒数。厨房秤是用来称量麦片的,团队认为这可能很重要,而且秤还会帮助数据收集,因为我们不想过于专注于每次取相同数量的麦片。

图 2:称重中。(图片由作者提供)
方法
为了本次实验,“碗”被定义为大约 1 份麦片,按照盒子上的推荐(1 杯或 36 克),尽管除了一个小小的魔法妖精之外,没人会用 36 克幸运符号作为早餐。团队对碗的大小并没有特别挑剔,任何接近 1 杯的量都算合适。反正我们是通过厨房秤来计算麦片的质量的,目标是得到一组健康的观察重量范围。

图 3:加文·杜韦(左)和布雷娜·布罗克(右) (图片由作者提供)
每碗麦片直接从盒子倒入塑料容器中称重,然后倒在桌面上进行计数。将烤燕麦和棉花糖分开并丢弃。接下来,识别出以下八(8)种符号,并记录它们的数量:粉红色心形、彩虹、紫色马蹄铁、蓝色月亮、绿色三叶草、独角兽、美味的红色气球和橙色星星。

图 4。这被计为 3 个紫色马蹄铁。 (图像由作者提供)
偶尔碗里会有一些小棉花糖块;并非每个符号都保持 100%完好。为了解决这个问题,团队尝试将这些小块归类为某种符号类型(绿色四叶草、蓝色月亮、等等),如果能够确定类型,那么该小块会被计入相应类别。如果该小块无法辨识或者太小无法确定类型,则会被丢弃。
数据
数据在两次独立的课堂会议中收集。学生们两人一组,负责倒符号和计数。我负责使用秤并记录称重数据,帮助学生输入计算机。团队进入了数据收集的状态,实验结束时,所有 4 名学生都能独立倒符号和计数。

图 5。数据几乎是最原始的形式。 (图像由作者提供)
每一轮中,塑料容器和谷物一起称重,并从观察到的总重量中减去容器的重量(在实验开始时测量)。然后,将符号输入到各自的列中并汇总。

图 6。图片中的人:Kate Coppola (图像由作者提供)
测量变量
-
Box:盒子编号(1 至 6) -
Bowl:每个盒子的顺序碗(范围从 1 到 13) -
Observation:跨盒子的碗的观察顺序(1 至 69) -
Totweight:塑料容器和谷物的总重量,单位为克 -
Weight:谷物的重量,单位为克,减去容器的重量 -
Hearts,Stars,等等:该碗中某种符号的数量 -
Totcharms:各种符号的总和

图 7。图片中的人:Haziq Rabbani (图像由作者提供)
这里有一些R代码,用于读取并显示数据集的顶部(前 6 行)。数据和所有代码都共享在这个 GitHub 仓库中。
library(readxl)
Lucky <- read_excel("Lucky.xlsx")
Lucky$Box <- as.factor(Lucky$Box)
head(Lucky)
幸运符数据集的前 6 行。
拥有这些数据后,我们可以报告一些信息,例如,观察到的Weight的平均值约为 46.3 克,任一碗中某种符号的最大数量为 15(粉色心形符号与紫色马蹄铁并列),等等。实际上,我们可以花一整天时间计算这个数据集的统计数据,尽情享受我们对粉色心形符号的热爱,但目前我们主要关注的是Totcharms及其与Bowl的关系,可能还有Weight的较小影响。
这是Totcharms与Bowl的图表,按Box着色:
Lucky |> ggplot(aes(x = Bowl, y = Totcharms, color = Box)) +
geom_point(size = 3) +
labs(y = '# Charms') -> p1
p1

图 8。Totcharms 与碗的散点图,按盒子颜色区分
在这里,我们看到随着Bowl的增加,Totcharms呈现出明显的下降趋势,而且这种模式出奇地线性。可能存在轻微的曲率。由于颜色较难分辨,我们可以制作一张线图并突出显示几条系列:
sizes <- c(2, 1, 2, 1, 1, 1)
alphas <- c(1, 0.2, 1, 0.2, 0.2, 0.2)
Lucky |> ggplot(aes(x = Bowl, y = Totcharms)) +
geom_line(aes(colour = Box, linewidth = Box, alpha = Box)) +
scale_discrete_manual("linewidth", values = sizes) +
scale_alpha_manual(values = alphas, guide = "none")

图 9.按盒子着色的选定系列幸运符号的折线图
所有系列整体呈下降趋势,但不同的盒子到达这一趋势的路径各不相同。注意第 3 盒是如何从高处开始,并在几个碗后保持高位,之后平稳下降到碗 10,随即急剧下滑。再看看第 1 盒是如何从最底部开始,在碗 5 之后逐渐增加,直到碗 8 达到顶峰,然后急剧下降到碗 12。数据表明,第 3 盒的幸运符号更集中在顶部附近,而第 1 盒的幸运符号则更集中在中间部分。某些盒子呈现出上下波动的趋势,而其他盒子则更倾向于沿着直线下降。总的来说,趋势是递减的且接近线性。请注意,每个盒子至少到达了Bow1 = 11,但只有两个盒子有 12 个碗,且只有一个盒子(第 4 盒)持续到了Bow1 = 13。
现在让我们来看一下幸运符号总数与重量的关系:
Lucky |> ggplot(aes(x = Weight, y = Totcharms, color = Box)) +
geom_point(size = 3) +
labs(x = 'Weight (g)', y = '# Charms') -> p2
p2

图 10.按盒子着色的幸运符号数量与谷物重量的散点图。
这个图表很杂乱,正如我们可能猜到的那样。我们有一个不错的重量范围,从不到 30 克到接近 70 克。请注意,有一个碗的重量异常沉重。这个离群值没有明显的解释,但如果我们稍微深入一点,绘制重量与碗数的关系图,可能能得到一些启示:
Lucky |> ggplot(aes(x = Bowl, y = Weight, color = Box)) +
geom_point(size = 3) + ylim(5, 75) +
labs(y = 'Weight (g)') -> p3
p3

图 11.按盒子着色的重量与碗数的散点图
我们看到,这个特别沉重的碗是第 1 盒的最后一个碗Bow = 12。那个特定数据点的来源遗憾地已经随时间消逝,但考虑到它是团队第一次完成的盒子,接近最后时,可能很难判断剩余的谷物有多少,也许所有剩余的谷物都被倒入了最后一个碗——我在早餐时也经常做这种事,当接近一个谷物盒的尾声时。如果那第 12 碗的 70 克被分成(比如)两碗,分别为 40 克和 30 克,那么可能会有两个盒子都能坚持到 13 碗,而不仅仅是一个,或许下面的模型就能更好地拟合数据。唉!我们永远也不知道了。科学的进程就是如此。
尽管幸运符号总数与重量之间没有明显的线性关系,但幸运符号总数、碗数和重量之间存在一种隐藏的关系,最适合通过 3D 可视化来探讨:
library(plotly)
fig <- plot_ly(Lucky, x = ~Bowl, y = ~Weight, z = ~Totcharms, color = ~Box) |>
add_markers() |>
layout(scene = list(xaxis = list(title = 'Bowl'),
yaxis = list(title = 'Weight (g)'),
zaxis = list(title = '# Charms')),
legend=list(title=list(text='Box')))
fig

图 13.(静态)幸运符号的 3D 散点图。快去试试互动版本吧!
3D 图表非常酷,但上面的静态展示无法充分表现数据。我已在以下链接设置了互动版本的图表,它在大多数手机和桌面浏览器中应该都能正常工作:
[## 互动幸运 3D 散点图
幸运麦片数据的交互式 3D 散点图
请到那里去,旋转数据,缩放,平移——查看一下。如果你旋转得正好,你会看到点在三维空间中的平面上松散地散布开来。这正是我们在多元线性回归模型中寻找的那种关系(稍后我们会讨论这个)。
模型拟合
现在让我们尝试量化这些变量之间的线性关系。我们将从一个简单的线性回归模型开始,关联 Totcharms 和 碗。
对于碗
这是模型:
mod1 <- lm(Totcharms ~ Bowl, data = Lucky)
summary(mod1)
##
## Call:
## lm(formula = Totcharms ~ Bowl, data = Lucky)
##
## Residuals:
## Min 1Q Median 3Q Max
## -16.7629 -5.7629 -0.4327 6.2277 22.2277
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 55.1309 2.1237 25.960 < 2e-16 ***
## Bowl -2.6698 0.2985 -8.945 4.81e-13 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 8.313 on 67 degrees of freedom
## Multiple R-squared: 0.5442, Adjusted R-squared: 0.5374
## F-statistic: 80.01 on 1 and 67 DF, p-value: 4.807e-13
我们看到 碗 与 Totcharms 之间存在强烈的线性关系。碗的斜率约为 −2.7,也就是说,每多吃一碗幸运麦片,我们估计 Totcharms 平均会减少 2.7 个 charms。我们的决定系数为 R² = 0.5442,也就是说,约 54% 的 Totcharms 方差可以通过以 碗 为预测因子的回归模型来解释。接下来,我们本应进行适当的残差分析,但我们将跳过这一部分。可以简单地说,残差图相对良好。让我们查看一个带置信区间带的回归线拟合线图(默认):
p1 + geom_smooth(method = "lm", aes(group=1), colour="black")

图 14. 具有置信区间带的拟合线图,表示 Totcharms 与碗的关系
这是一个具有明显递减趋势的良好关系。
对于体重
我们将对 体重 做相同的事情,暂时忽略 碗 变量。开始吧:
mod2 <- lm(Totcharms ~ Weight, data = Lucky)
summary(mod2)
##
## Call:
## lm(formula = Totcharms ~ Weight, data = Lucky)
##
## Residuals:
## Min 1Q Median 3Q Max
## -27.0151 -8.7745 0.6901 7.8328 24.4701
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 22.1370 10.5650 2.095 0.0399 *
## Weight 0.3502 0.2256 1.552 0.1254
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 12.1 on 67 degrees of freedom
## Multiple R-squared: 0.0347, Adjusted R-squared: 0.02029
## F-statistic: 2.409 on 1 and 67 DF, p-value: 0.1254
我们发现 体重 作为单独的预测变量并不是一个很有用的 Totcharms 预测因子,这与我们之前看到的散点图一致。我们注意到,体重的斜率估计值为 0.3502,也就是说,每增加 1 克的幸运麦片,Totcharms 平均增加 0.35 个 charms。这听起来合理:麦片越多,charms 越多。决定系数相当差:R² = 0.0347,换句话说,约 没有% 的 Totcharms 方差可以通过以 体重 为预测因子的回归模型来解释。没关系;体重 更像是一个辅助工具,用来控制麦片数量的变异性。这里的残差分析结果实际上并不像想象的那么糟糕,这让人宽慰,考虑到体重数据的高端和低端存在极端观测值,我们本来就应该预期会有一些问题。为了完整性,我们再包括一个拟合线图:
p2 + geom_smooth(method = "lm", aes(group=1), colour="black")

图 15. Totcharms 与体重的拟合线图,按盒子着色
我最初打算使用ggpubr包将这些拟合线图组合在一起,并尽量节省讨论中的空间,但图表显得很拥挤,并且不太具有信息性。无论如何,这就是我原本打算做的:
library(ggpubr)
ggarrange(p1 + geom_smooth(method = "lm", aes(group=1), colour="black"),
p2 + geom_smooth(method = "lm", aes(group=1), colour="black"),
align = 'h', labels=c('A', 'B'), legend = "right",
common.legend = TRUE)
多元回归
现在进入有趣的部分:我们分别探讨了Totcharms ~ Bowl和Totcharms ~ Weight的关系,但如果将它们结合起来会怎样呢?让我们一起看看:
mod3 <- lm(Totcharms ~ Bowl + Weight, data = Lucky)
summary(mod3)
##
## Call:
## lm(formula = Totcharms ~ Bowl + Weight, data = Lucky)
##
## Residuals:
## Min 1Q Median 3Q Max
## -12.8825 -5.4425 -0.9975 5.2475 26.5304
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 33.3168 6.8655 4.853 7.78e-06 ***
## Bowl -2.7552 0.2796 -9.855 1.35e-14 ***
## Weight 0.4819 0.1452 3.318 0.00148 **
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 7.754 on 66 degrees of freedom
## Multiple R-squared: 0.6094, Adjusted R-squared: 0.5976
## F-statistic: 51.49 on 2 and 66 DF, p-value: 3.363e-14
看看这个!现在Bowl和Weight都与Totcharms呈强线性关联。Bowl的斜率几乎与之前相同,仍为−2.7,但Weight的估计斜率现在已增加到每增加 1 克谷物大约 0.5 个幸运符号。我们的(调整后的)多重R²已跃升至接近 60%—考虑到样本量小(n = 6)、数据集的普遍噪声水平以及可能存在的一些可疑设计选择(每个小棉花糖算作 1,等等),这实在是非常了不起。回想起来,数据竟然没有变得更糟,真是挺令人惊讶的。手工收集的真实数据在野外很少这么“温顺”。
添加回归平面
这个可视化的代码比其他示例要复杂一些,因此为了简洁起见没有展示,但你可以在这个 GitHub Gist中查看完整代码。接下来让我们继续绘图:

图 16. 带有回归平面的幸运符号 3D 散点图
再次提醒:这是一个超级酷的图表,但静态版本并不能充分展示数据的价值。请查看互动版:
[## 互动版幸运符号 3D 与回归平面
添加回归平面的幸运符号 3D 散点图
gkerns.people.ysu.edu](https://gkerns.people.ysu.edu/lucky3Dplane.html?source=post_page-----9040fe2cc560--------------------------------)
互动式 3D 图表非常有趣。我希望你能像我一样享受玩这个图表。最后提一句,在tl;dr的声明中,我们说过Bowl和Weight之间的交互作用并不显著。读者可以通过以下(输出省略)来验证这一点:
summary(lm(Totcharms ~ Bowl * Weight, data = Lucky))
讨论与问题
我最初以为,整个实验要么会是我的想象产物,要么效应太小,根本无法在没有大量幸运符号的情况下检测到。我在这两方面都错了。这个效应是真的,而且足够大,即便只有几盒,也能检测到,真的就像两只手装的那么多。
幸运符号的下降趋势
完整的模型迅速带我们得出了一些令人吃惊的结论。例如,我们估算在一盒幸运符号的第一碗中大约有多少个幸运符号?我们之前看到,这项研究中的平均Weight是 46.3 克。当Box = 1时,模型估算平均Totcharms是
33.3168 + (-2.7552)*1 + 0.4819*46.3
## [1] 52.87357
也就是说,在第一碗谷物中大约有 53 个幸运符号—嗯,嘴巴已经开始流口水了。那最后一碗呢?好吧,并不是每个盒子都到了Bowl 13,但它们都达到了Bowl = 11。那么有多少个幸运符号?
33.3168 + (-2.7552)*11 + 0.4819*46.3
## [1] 25.32157
哇。 平均每盒有 25.3 颗幸运宝石。这意味着从第一碗到第十一碗,幸运宝石数量减少了 52%。不,这绝对不是我的想象。忘掉多元线性回归模型和花哨的 3D 图表,一个饥饿的幼儿戴着眼罩也能察觉到这种差异。
下一个问题:为什么会出现下降?从物理角度进行分析可能是这样的:将一盒幸运宝石看作是由涂霜的烤燕麦和棉花糖组成的简单机械混合物。在其生命周期中,许多外部力量会影响这盒子,比如运输过程中的摇晃、商店货架上的摆放以及运输到家中的过程,更不用说在橱柜内外的活动了。这不可避免地会导致内容物的变化,较轻的棉花糖会向盒子顶部迁移,而较重的烤燕麦则会沉积到底部。
这个理由至少是合乎逻辑的。但是,它留下了一些相关问题没有回答:
-
这种模式对各类单独的幸运宝石也适用吗?(匆忙看了一眼,似乎是“不适用”)。
-
这种关联真的是线性的,还是更复杂的模型能更好地描述这种关系?
-
我们忽略了哪些其他重要因素?
-
有没有什么策略可以减缓幸运宝石的下降速度?
-
我们能不能巧妙地摇动盒子(某种方式)来更好地混合棉花糖?
-
那么,存储方法如何呢?如果盒子倒置存放,是否有帮助?
-
还是平放在一旁?
-
等等。
这些未解的问题只能留待以后再讨论。
下一步
自 2023 年夏天的第一次实验以来,我已经和其他学生小组进行了几次实验。第一次是在 2023 年 11 月,与初中生一起在YSU MegaMath Day上进行的。我没有给 MegaMath 学生们非常具体的指示,结果不久后所有小组都把塑料袋从盒子里取出来,并在桌面上摊开袋子中间的麦片进行舀取。我不能怪他们;在袋子展开的情况下,从中间舀取麦片确实更容易。不幸的是,这种做法完全破坏了可能存在的任何自然密度排序,而这种排序正是我们怀疑起作用的关键因素,从而损害了实验的完整性。而且,我怀疑没有哪个家长会允许孩子这样吃他们的幸运宝石。
第二次是在 2024 年 2 月,与高中生一起在YSU 数学节的两个工作坊系列中进行的。这次我为他们做好了准备。我整理并分发了一个数据收集表格(你可以在这里找到),提供了更详细的指导。你可以在 GitHub 上查看额外的数据集,位于在 [extraData](https://github.com/gjkerns/luckyCharms/tree/master/extraData) 目录。
展望未来,我们需要更多的数据来更好地估计幸运符号的掉落情况,同时测试在盒子中更均匀分布幸运符号的策略也很有趣。如果成功了,盒子的第一个碗可能不那么神奇,但另一方面,也许那些最后的碗在等待打开下一个全新盒子时就不会觉得那么枯燥乏味了!
致谢
这个实验和这些结果如果没有 2023 年夏季 STAT 3743 课程的四位学生:Brenna Brocker、Kate Coppola、Gavin Duwe 和 Haziq Rabbani 的热情感染和对细节的不懈关注,是不可能实现的。我感谢他们与我一起走上了这条统计学道路。我还要感谢扬斯敦州立大学数学与统计学系支持这项研究以及在 YSU MegaMath Day 和 YSU MathFest 期间进行的额外数据收集。
免责声明
如果还没有足够明显,作者是幸运符号的粉丝,四位学生也是如此。这里报告的结果并不,也不是为了批评通用磨坊公司及其子公司、它们的工厂生产标准,或那些在那里勤奋工作的好人和机器人。我们都受同样的物理法则制约,这包括早餐麦片的盒子。
完整披露:我已经查看了在实验重新进行时收集的额外数据。从我所知,效果仍然存在,但不如之前那么戏剧化。我不知道这是否意味着效果比我们最初估计的要小,还是与中学/高中环境中的数据收集协议有关。只有时间——和更多的数据——才能揭晓答案。
参考文献和代码示例
在撰写这篇文章时,我尽量记录了我访问过的地方,以便找到构建我想要的图表的代码,以下是一个几乎完整的列表,但也许我漏掉了一些链接。如果你发现我遗漏了什么,请在评论中提醒我,我会修正。
-
blog.recursiveprocess.com/2013/03/03/oreo-original-vs-double-vs-mega/ -
stackoverflow.com/questions/38331198/add-regression-plane-to-3d-scatter-plot-in-plotly -
stackoverflow.com/questions/38593153/plotly-regression-line-r -
stackoverflow.com/questions/15633714/adding-a-regression-line-on-a-ggplot
这张航拍图像中有多少辆车?让我们从头开始用 YOLOv8 来统计它们!
从头到尾的步骤指南,教你如何在自定义数据库上部署 YOLOv8 进行物体检测和计数。
·发布在 Towards Data Science ·阅读时长 12 分钟·2024 年 7 月 8 日
--

通过 YOLO 检测到的航拍图像中的车辆,图像由作者提供
目录
-
🌟 介绍
-
🚀 设置 Google Colab
-
🛰️ 在哪里可以找到免费的航拍图像
-
🔄 将 GeoTIFF 转换为 JPG
-
📐 将图像拆分为子图像
-
🏷️ 标注子图像中的车辆
-
🛠️ 准备训练和测试数据库
-
🧠 训练 YOLOv8
-
🖼️ 在整个图像上部署 YOLOv8
-
📄 结论
-
📚 参考文献
🌟 介绍
几乎一个月前,我在一篇著名报纸上读到一篇文章,分析了使用超高分辨率图像(分辨率低于 20 厘米)统计每个库存中特斯拉汽车的数量。文章旨在估算特斯拉在 2024 年第二季度交付的汽车数量。读完之后,我认为这可以成为一个很好的物体检测算法应用,用来检测和统计航拍图像中的车辆,评估汽车库存状态……
微调 Gemini 需要多少数据?
探索 Gemini Flash 的学习曲线和样本效率,附带代码示例。
·发表于 Towards Data Science ·阅读时间 8 分钟·2024 年 9 月 19 日
--

图片来源:Mohammad Emami 通过 Unsplash
在大多数常见的机器学习和自然语言处理任务中,达到最佳性能通常需要在训练数据量和模型准确度之间做出权衡。本博文将探讨样本效率这一概念,以使用 PII 掩码数据集来微调 Google 的 Gemini Flash 模型为实际例子。我们将分析随着数据量的增加,微调过程如何影响已调优模型的能力。
什么是样本效率?为什么它如此重要?
样本效率指的是一个模型在有限的训练数据下实现高准确度的能力。这是机器学习(ML)发展的一个关键方面,特别是在处理那些大量标注数据可能稀缺或获取成本高昂的任务或领域时。一个样本高效的模型可以通过更少的例子有效学习,从而减少数据收集和训练所需的时间、成本和努力。研究表明,LLMs(大语言模型)在样本效率方面表现得非常好,甚至能够通过少量的示例进行上下文学习,从而显著提高性能。本博文的主要动机是通过以 Gemini Flash 为例来探讨这一方面。我们将评估……
大语言模型消耗多少能源?
我们使用 EnergyMeter 这一 Python 工具来测量不同 LLM(大语言模型)的能耗,包括 Llama、Dolly 和 BLOOM。
·发表于 Towards Data Science ·12 分钟阅读·2024 年 7 月 2 日
--

回答所有这些问题需要大量的能源![由 AI 制作 Designer。]
大语言模型(LLM)正在成为我们日常执行的几个任务的新主流:搜索日常问题的答案、总结文本、制作幻灯片或为本文创建图像;LLM 是强大的工具,可以说它们的使用将在未来几年增加。它们惊人的能力部分得益于它们拥有的(非常)大量可训练参数;举个例子,ChatGPT 背后的模型(GPT-3)有超过 1750 亿个参数。训练如此庞大的模型需要极其强大的计算资源(特别是 GPU),这意味着购买或租用这些资源的成本非常高,进而带来很高的能源需求,以供给这些资源的动力。例如,据估计,训练 GPT-3 需要 1287 MWh,相当于 420 人的年平均能源消耗[1]。
但是,为了保持这些 LLM 的运行,进行推理时消耗的能量更多。实际上,OpenAI 宣布每周有 1 亿人使用 ChatGPT[2],即使我们假设每个用户每周只提问四个问题,这意味着 ChatGPT 至少…
神经网络如何学习:一种概率视角
理解训练神经网络的损失函数
·发表于 Towards Data Science ·阅读时间 8 分钟·2024 年 12 月 26 日
--
机器学习是非常实践性的,每个人都在探索自己的路径。它不像传统课程那样有一套标准的学习路径,也没有所谓的“机器学习 101”课程。然而,这也常常会导致理解上的空白。如果你像我一样,这些空白会让人感觉不舒服。例如,我曾经对我们随便做的一些事情感到困惑,比如选择损失函数。我承认,有些做法是通过启发式和经验学到的,但大多数概念都有坚实的数学基础。当然,并不是每个人都有时间或动机深入研究这些基础知识——除非你是一个研究人员。
我试图呈现一些关于如何解决机器学习问题的基本思路。理解这些背景知识将帮助实践者在设计选择上更加自信。我所覆盖的概念包括:
-
使用交叉熵量化概率分布之间的差异。
-
神经网络模型的概率视角。
-
推导并理解不同应用的损失函数。
熵
在信息理论中,熵是衡量随机变量取值的不确定性。换句话说,它用于量化分布的扩散程度。分布越狭窄,熵越低,反之亦然。从数学角度来看,分布 p(x) 的熵定义为:

通常使用以 2 为底的对数,在这种情况下,熵以比特为单位进行度量。下图比较了两种分布:蓝色的分布具有较高的熵,而橙色的分布具有较低的熵。

分布具有高熵和低熵的可视化示例 — 该图由作者使用 Python 创建。
我们还可以测量两个分布之间的熵。例如,假设我们观察到某些数据具有分布p(x),并且有一个分布q(x),它可能作为观察数据的模型。在这种情况下,我们可以计算数据分布p(x)与模型分布q(x)之间的交叉熵Hpq(X)。从数学上讲,交叉熵可以表示为:

通过使用交叉熵,我们可以比较不同的模型,交叉熵最小的模型更适合数据。以下图所示,我们有两个候选模型,想要决定哪个模型更适合观察到的数据。正如我们所看到的,与数据分布完全匹配的模型的交叉熵低于略有偏差的模型。

比较数据分布 p(x)与两个候选模型的交叉熵。(a)候选模型完全匹配数据分布,交叉熵较低。(b)候选模型与数据分布不匹配,因此交叉熵较高 — 该图由作者使用 Python 创建。
另一种表述相同内容的方式是:当模型分布偏离数据分布时,交叉熵增加。在尝试将模型拟合到数据时,即训练一个机器学习模型时,我们的目标是最小化这种偏差。由于偏离数据分布而导致的交叉熵增加被定义为相对熵,通常被称为KL 散度,或简称为KL-Divergence。

因此,我们可以使用交叉熵或 KL 散度来量化两个概率分布之间的差异。为了训练一个模型,我们可以调整模型的参数,使其最小化交叉熵或 KL 散度。请注意,最小化交叉熵或 KL 散度会得到相同的解决方案。KL 散度有一个更好的解释,因为其最小值为零,这种情况发生在模型与数据完全匹配时。
另一个重要的考虑因素是我们如何选择模型分布?这一点由两个因素决定:我们要解决的问题和我们偏好的解决问题的方式。假设我们有一个分类问题,其中有(X, Y)数据对,X表示输入特征,Y表示真实的类别标签。我们的目标是训练一个模型来正确分类输入数据。我们可以通过两种方式来解决这个问题。
判别模型与生成模型
生成方法是指建模联合分布p(X,Y),使其学习数据生成过程,因此得名“生成式”。在当前讨论的示例中,模型学习类别标签的先验分布p(Y),并且在给定类别标签Y的情况下,学习使用p(X|Y)生成特征X。

应该清楚的是,学习到的模型能够生成新的数据(X,Y)。然而,可能不太明显的是,它也可以使用贝叶斯定理对给定的特征X进行分类,尽管根据模型的复杂性,这可能并不总是可行。可以说,使用这种方法进行分类任务可能不是一个好主意,因此我们应该采取直接的方法。

判别式与生成式建模方法 — 作者使用 Python 创建。
判别方法是指直接建模输入特征X和输出标签Y之间的关系,即建模条件分布p(Y|X)。因此,学习到的模型不必捕捉特征X的所有细节,而只需关注其类别区分特征。正如我们之前所看到的,可以通过最小化观测数据和模型分布之间的交叉熵来学习模型的参数。判别模型的交叉熵可以写成如下形式:

其中最右侧的求和是样本平均值,它近似于数据分布的期望。由于我们的学习规则是最小化交叉熵,我们可以将其称为我们的通用损失函数。

学习的目标(训练模型)是最小化这个损失函数。从数学上讲,我们可以将同样的陈述写成如下形式:

现在让我们考虑一些具体的判别模型示例,并将通用损失函数应用到每个示例中。
二分类
正如名称所示,这种问题的类别标签Y要么是0,要么是1。这可能适用于人脸检测器、猫狗分类器或预测疾病是否存在的模型。我们如何建模二元随机变量?没错——它是一个伯努利随机变量。伯努利变量的概率分布可以写成如下形式:

其中π是获取1的概率,即p(Y=1) = π。
由于我们想要建模p(Y|X),让我们将π表示为X的函数,即模型的输出π(X)依赖于输入特征X。换句话说,我们的模型接受特征X并预测Y=1 的概率。请注意,为了在模型输出端获得有效的概率,它必须被限制在0和1之间。这是通过在输出端应用 sigmoid 非线性函数来实现的。

为了简化,我们将其明确重写为真实标签和预测标签的形式,如下所示:

我们可以将此特定条件分布的一般损失函数写成如下形式:

这通常被称为二元交叉熵(BCE)损失。
多类分类
对于一个多类问题,目标是为每个输入特征X从C个类别中预测一个类别。在这种情况下,我们可以将输出Y建模为一个类别随机变量,一个从所有可能的C个状态中选择一个状态的随机变量。作为类别随机变量的例子,可以想象一个六面骰子,它可以在每次掷骰时取六个可能状态之一。

我们可以将上述表达式看作是将二元随机变量的情况扩展到具有多个类别的随机变量。我们可以通过将λ表示为输入特征X的函数来建模条件分布p(Y|X)。基于此,让我们将条件类别分布Y用预测概率的形式表示如下:

使用这个条件模型分布,我们可以利用之前推导出的交叉熵的通用损失函数写出损失函数,如下所示:

这在 PyTorch 中被称为交叉熵损失。需要注意的是,我是根据每个类别的预测概率来写这个公式的。为了在所有C个类别中得到一个有效的概率分布,我们在模型的输出端应用了 softmax 非线性函数。softmax 函数写作如下:

回归
考虑数据(X, Y)的情况,其中X表示输入特征,Y表示可以取任意实数值的输出。由于Y是实值的,我们可以使用高斯分布来建模其分布。

再次强调,由于我们对建模条件分布p(Y|X)感兴趣。我们可以通过将Y的条件均值表示为X的函数来捕捉对X的依赖关系。为了简化,我们将方差设为 1。条件分布可以写成如下形式:

我们现在可以将这个条件模型分布的通用损失函数写成如下形式:

这是用于训练回归模型的著名 MSE 损失。请注意,这里的常数因子是无关紧要的,因为我们只关注最小值的位置,可以忽略不计。
总结
在这篇简短的文章中,我介绍了熵、交叉熵和 KL 散度的概念。这些概念对于计算分布之间的相似性(或散度)至关重要。通过使用这些思想,以及模型的概率解释,我们可以定义通用损失函数,也称为目标函数。训练模型或“学习”则归结为最小化与模型参数相关的损失。这个优化过程通常通过梯度下降来完成,大多数深度学习框架如 PyTorch 会处理这个过程。希望这对你有帮助——祝你学习愉快!
如何(不)利用数据可视化作弊
一份有些不同的指南……
·发表于 Towards Data Science ·阅读时长 17 分钟·2024 年 4 月 1 日
--

来源:作者通过 ChatGPT 生成的图像。
不久前,我发布了一篇关于使用 KNIME 进行数据可视化的文章。在开头的故事中,我以纪录片《不愿面对的真相》为例。电影展示了,包括复杂数据在内,如何通过可视化有效地呈现。电影中的数据、概念以及呈现的内容引发了许多争议和辩论。我的文章开头部分也引发了类似的讨论。
一步步的全面指南
[towardsdatascience.com
作者删除了我文章中的批评评论(或者至少我现在看不见它们了)。然而,这些批评启发我深入探索一个相对未被发掘的领域:创建具有误导性的数据显示方法的注意事项和禁忌。本文正是旨在探讨这一话题,引导读者理解数据呈现中的完整性微妙之处。
在这篇文章中,我将介绍十四种你必须严格避免在演示工具包中使用的数据操控手法。如果你发现你的作品中藏有这些手法,你必须毫不犹豫地将其剔除……
OpenAI 的 Sora 如何改变游戏规则:深入了解其核心技术
一项代表前沿技术的杰作
·发布于Towards Data Science ·12 分钟阅读·2024 年 2 月 19 日
--

图片由Kaushik Panchal提供,来源于Unsplash
2024 年 2 月 15 日,OpenAI 在 2022 年底震惊世界发布 ChatGPT 之后,再次通过揭示 Sora 震撼全球。这项技术能够根据文本提示生成最长达一分钟的视频,毫无疑问,它将成为一次突破。
在这篇博客文章中,我将基于 OpenAI 发布的技术报告,介绍这项令人惊叹的技术背后的基础方法和研究。
顺便提一下,“Sora”在日语中意味着“天空”。尽管官方尚未宣布这一命名是否有意为之,但据推测可能是有意的,因为他们的官方发布推文中有一段以东京为主题的视频。
OpenAI 通过 X 平台向全球发布 Sora
目录
-
关于 Sora
-
它背后是什么样的技术和研究?
-
这些研究努力为 Sora 带来的能力
-
Sora 的未来
关于 Sora
Sora 是 OpenAI 开发的先进文本到视频转换模型,其能力和应用范围展示了现代 AI 技术的新视野。这个模型不仅限于生成几秒钟的视频;它可以创建最长一分钟的视频,在保持高视觉质量的同时,忠实地再现用户指令。就像是将梦想变为现实。
OpenAI Sora 通过 X 平台进行演示
基于现实世界生成复杂场景
Sora 理解提示中描述的元素如何在物理世界中存在和运作。这使得模型能够准确地呈现用户意图的动作和视频中的运动。例如,它可以逼真地重现一个人奔跑的场景或自然现象的运动。此外,它还能够再现多个角色、各种运动类型以及主题和背景的精确细节。
以前,使用生成性 AI 创建视频面临着在不同场景之间保持一致性和可重现性的巨大挑战。这是因为在生成每个场景或帧时,完全理解先前的上下文和细节,并将其适当传递到下一个场景是困难的。然而,这个模型通过将对语言的深刻理解与视觉上下文结合,准确解释提示,从而保持叙事一致性。它还能够捕捉角色的情感和个性,并在视频中呈现为富有表现力的角色。
Bill Peebles(OpenAI)通过 X 发布的帖子
它背后是什么样的技术和研究?

由Markus Spiske拍摄,来自Unsplash
Sora 是建立在先前图像数据生成建模研究的基础之上的。先前的研究使用了各种方法,如递归网络、生成对抗网络(GANs)、自回归 Transformer 和扩散模型,但通常集中于狭窄类别的视觉数据、较短的视频或固定尺寸的视频。Sora 超越了这些局限,并在生成各种时长、纵横比和分辨率的视频方面取得了显著进展。在这一部分,我将介绍支持这些进展的核心技术。
1. Transformer
Vaswani 等人(2017),《Attention is all you need》
Transformer 模型是一种神经网络架构,彻底改变了自然语言处理(NLP)领域。它由 Vaswani 等人于 2017 年首次提出。该模型显著克服了传统递归神经网络(RNNs)和卷积神经网络(CNNs)面临的挑战,作为一种创新方法,今天支持了多种突破性的技术。

图 1:Transformer——模型架构。|Vaswani 等人(2017)
RNNs 的问题:
-
长期依赖问题:尽管 RNNs 理论上可以通过时间传递信息,但在实践中它们难以捕捉长时间跨度的依赖关系。
-
并行化的局限性:由于 RNN 中每一步的计算依赖于前一步的输出,因此必须进行顺序处理(例如,逐一处理文本中的单词或句子),无法利用现代计算机架构所提供的并行处理优势。这使得在大规模数据集上的训练效率低下。
CNN 的问题:
-
固定感受野大小:虽然 CNN 在提取局部特征方面表现优秀,但其固定的感受野大小限制了其捕捉全局上下文中长距离依赖的能力。
-
自然语言层级结构建模的困难:直接建模语言的层级结构具有挑战性,而这对于深层次的上下文理解可能不够充分。
Transformer 的新特性:
-
注意力机制:使得能够直接建模序列中任意位置之间的依赖关系,从而直接捕捉长距离依赖和广泛的上下文。
-
并行化的实现:由于输入数据一次性整体处理,实现了高度的并行计算,大大加速了在大规模数据集上的训练。
-
可变感受野:注意力机制使得模型能够根据需要动态调整“感受野”的大小。这意味着模型可以在某些任务或数据中自然地聚焦于局部信息,而在其他情况下考虑更广泛的上下文。
更多关于 Transformer 的详细技术解释:
Transformer 是一种神经网络架构,近年来越来越受到关注。Transformer 最近…
towardsdatascience.com
2. 视觉 Transformer(ViT)
Dosovitskiy 等人(2020 年),“一张图等于 16x16 个词:用于大规模图像识别的 Transformer。”
本研究将革命性地改变自然语言处理(NLP)的 Transformer 原理应用于图像识别,开辟了新的视野。
Token 和 Patch
在原始的 Transformer 论文中,tokens 主要代表单词或句子的部分,分析这些 tokens 之间的关系可以深入理解句子的含义。在本研究中,为了将这种 token 的概念应用于视觉数据,图像被划分为 16x16 的小块(patches),每个小块被视为 Transformer 中的一个“token”。这种方法使得模型能够学习每个小块在整个图像中的关系,从而基于此进行整个图像的识别和理解。它突破了传统 CNN 模型在图像识别中固定感受野大小的局限性,实现了在图像内任意位置关系的灵活捕捉。

图 1:模型概述。|Dosovitskiy 等人(2020)
关于 ViT 的更详细技术解释:
machinelearningmastery.com/the-vision-transformer-model/
3. 视频视觉变换器(ViViT)
Arnab 等人(2021),“Vivit: A video vision transformer.”
ViViT 进一步扩展了视觉变换器的概念,将其应用于视频的多维数据。视频数据更为复杂,因为它既包含静态图像信息(空间元素),又包含随时间变化的动态信息(时间元素)。ViViT 将视频分解为时空补丁,并将其视为变换器模型中的令牌。通过引入时空补丁,ViViT 能够同时捕捉视频中的静态和动态元素,并建模它们之间的复杂关系。

图 3:Tubelet(时空输入体积)嵌入图像。|Arnab 等人(2021)
关于 ViViT 的更详细技术解释:
ICCV 2021 ✨,谷歌研究
4. Masked Autoencoders (MAE)
He 等人(2022),“Masked autoencoders are scalable vision learners.”
本研究通过使用一种自监督预训练方法,称为 Masked Autoencoder,显著提高了传统上在高维度和大量信息数据集上训练时的高计算成本和低效率。
具体来说,通过对输入图像的部分区域进行遮挡,网络被训练去预测隐藏部分的信息,从而更有效地学习图像中的重要特征和结构,并获得丰富的视觉数据表示。这一过程使得数据的压缩与表示学习变得更加高效,减少了计算成本,并增强了不同类型视觉数据和任务的通用性。
本研究的方法与 BERT(双向编码器表示的变换器)在语言模型发展中的应用紧密相关。BERT 通过 Masked Language Modeling(MLM)实现了对文本数据的深层上下文理解,而 He 等人将类似的遮挡技术应用于视觉数据,从而实现了对图像的更深理解与表示。

图 1:Masked Autoencoders 图像。|He 等人(2022)
关于 MAE 的更详细技术解释:
## 论文解析:Masked Autoencoders 是可扩展的视觉学习者
如何重建图像中的遮挡部分能带来好处
[towardsdatascience.com
5. 本地分辨率视觉 Transformer(NaViT)
Dehghani 等人 (2023),“Patch n’Pack: NaViT,一种适用于任何纵横比和分辨率的视觉 Transformer。”
本研究提出了本地分辨率 ViTransformer(NaViT),这是一个旨在进一步扩展视觉 Transformer(ViT)应用于任何纵横比或分辨率图像的模型。
传统 ViT 的挑战
视觉 Transformer 通过将图像分割为固定大小的补丁并将这些补丁作为令牌来处理,从而提出了一种开创性的方法,将 Transformer 模型应用于图像识别任务。然而,这种方法假设模型是针对特定分辨率或纵横比进行优化的,因此需要对不同大小或形状的图像进行模型重新调整。这是一个重大限制,因为实际应用中通常需要处理不同大小和纵横比的图像。
NaViT 的创新
NaViT 设计旨在高效处理任何纵横比或分辨率的图像,使其能够直接输入模型而无需预先调整。Sora 也将这种灵活性应用于视频,显著提升了灵活性和适应性,能够无缝处理各种大小和形状的图像与视频。

图 2:|Dehghani 等人 (2023)
6. 扩散模型
Sohl-Dickstein 等人 (2015),“使用非平衡热力学的深度无监督学习。”
除了 Transformer,扩散模型也是支撑 Sora 的核心技术之一。此研究为扩散模型奠定了理论基础,扩散模型是一种使用非平衡热力学的深度学习模型。扩散模型引入了一个扩散过程的概念,该过程从随机噪声(没有任何模式的数据)开始,逐步去除噪声,生成类似于实际图像或视频的数据。
例如,可以想象从仅仅是随机的点开始,这些点逐渐转变为美丽景观或人物的视频。这种方法后来被应用于生成复杂的数据,如图像和声音,促进了高质量生成模型的发展。

去噪过程的图像|图像来源 (OpenAI)
Ho 等人 (2020),“去噪扩散概率模型。”
Nichol 和 Dhariwal (2021),“改进的去噪扩散概率模型。”
基于 Sohl-Dickstein 等人(2015 年)提出的理论框架,开发了被称为去噪扩散概率模型(DDPM)的实用数据生成模型。该模型在高质量图像生成方面表现出了特别显著的效果,证明了扩散模型的有效性。
扩散模型对 Sora 的影响
通常,训练机器学习模型需要大量的标注数据(例如,被告知“这是一张猫的图片”)。然而,扩散模型也可以从未标注的数据中学习,使其能够利用互联网上海量的视觉内容生成各种类型的视频。换句话说,Sora 可以观察不同的视频和图像,并学习“这就是正常视频的样子”。
有关扩散模型的更详细技术解释:
理解去噪扩散概率模型的基础
towardsdatascience.com ## 通过苏格拉底式方法理解去噪扩散概率模型
深入探讨去噪扩散模型背后的动机以及损失函数的详细推导
towardsdatascience.com
7. 潜在扩散模型
Rombach 等人(2022 年),《使用潜在扩散模型进行高分辨率图像合成》
该研究在使用扩散模型进行高分辨率图像合成领域作出了重要贡献。它提出了一种方法,通过在潜在空间中利用扩散模型,在保持质量的同时显著降低了与直接高分辨率图像生成相比的计算成本。换句话说,它展示了通过对潜在空间(一个表示图像压缩表示的低维空间)中的数据进行编码并引入扩散过程,而不是直接操作图像,可以用更少的计算资源实现目标。
Sora 将这一技术应用于视频数据,将视频的时空数据压缩到低维潜在空间,然后经过一系列的过程将其分解为时空块。这种在潜在空间中进行高效数据处理和生成的能力对于 Sora 能够更快速地生成更高质量的视觉内容起着至关重要的作用。

视觉编码图|图片来源(OpenAI)
关于潜在扩散模型的更多技术细节:
尽管 OpenAI 已凭借其生成文本模型主导了自然语言处理领域,但他们的图像……
towardsdatascience.com
8. 扩散变压器(DiT)
Peebles 和 Xie. (2023),《具有变压器的可扩展扩散模型》。
这项研究可能是实现 Sora 的最关键研究。如 OpenAI 发布的技术报告中所提到,Sora 并不是采用普通的(标准)变压器,而是采用了扩散变压器(DiT)。
重要的是,Sora 是一种扩散 变压器。(来源:OpenAI Sora 技术报告)
该研究引入了一种新的模型,该模型用变压器结构替代了扩散模型中常用的 U-net 组件。通过对潜在图像块进行变压器操作,这种结构使潜在扩散模型成为可能。该方法使得图像块的处理更加高效,能够生成高质量的图像,同时有效利用计算资源。这一变压器的引入,与 Stability AI 在 2022 年发布的 Stable Diffusion 不同,被认为有助于实现更加自然的视频生成。

图 1:扩散变压器生成的图像|Peebles 和 Xie. (2023)
此外,值得注意的是,他们的验证结果证明了 DiT 的可扩展性,显著促进了 Sora 的实现。可扩展性意味着模型的性能会随着变压器深度/宽度的增加(使模型更复杂)或输入标记数量的增加而提升。

图 8 和 9:扩散变压器的可扩展性|Peebles 和 Xie. (2023)
-
Gflops(计算性能):衡量计算机运算速度的单位,等于每秒十亿次浮点运算。在本文中,网络复杂度通过 Gflops 来衡量。
-
FID(Fréchet 生成距离):图像生成的评价指标之一,值越低表示准确度越高。它通过测量生成图像和真实图像之间特征向量的距离,定量评估生成图像的质量。
这一点在自然语言处理领域已经被观察到,如 Kaplan 等人(2020)和 Brown 等人(2020)所确认,支持了 ChatGPT 创新成功背后的关键特征。
Kaplan 等人(2020),《神经语言模型的扩展法则》。
Brown 等人(2020 年),《语言模型是少量学习者》
这一显著特点,除了由于 Transformer 的优势在较低计算成本下生成高质量图像之外,还表明,通过更大的计算资源,甚至可以生成更高质量的图像。Sora 将这一技术应用于视频生成。

视频生成的可扩展性|图像来源(OpenAI)
关于 DiT 的更详细技术解释:
通过 YouTube 观看 DiT 的论文评审
这些研究成果为 Sora 提供的能力
可变的持续时间、分辨率、纵横比
主要得益于 NaViT,Sora 可以采样宽屏 1920x1080p 视频、垂直 1080x1920 视频以及介于两者之间的所有内容。这意味着它能够为各种设备类型在任何分辨率下创建视觉效果。
使用图像和视频进行提示
目前,Sora 生成的视频,如演示所示,是以文本到视频的格式创建的,其中指令通过文本提示给出。然而,正如从之前的研究中可以轻易预见的那样,也可以使用现有的图像或视频作为输入,而不仅仅是文本。这使得图像的动画化成为可能,或者 Sora 可以想象并输出现有视频的过去或未来作为视觉效果。
三维一致性
尽管尚不清楚上述研究如何直接参与其中,但 Sora 能够生成带有动态摄像机运动的视频。当摄像机移动和旋转时,人物和场景元素会在三维空间中一致地移动。
Sora 的未来
在这篇博文中,我已经解释了 OpenAI 用于生成视频的 AI 技术——Sora,这项技术已经震惊了全世界。一旦它公开发布并能被更广泛的用户访问,它势必将在全球范围内产生更为深远的影响。
这一突破的影响预计将跨越视频创作的各个方面,但预测它可能从视频演变为三维建模的进一步发展。如果真是如此,不仅是视频创作者,连虚拟空间(如元宇宙)中的视觉内容生产也可能很快由 AI 轻松生成。
这样的未来的到来已经在以下内容中有所暗示:
Martin Nebelong 通过 X 发布的关于 Micael Rublof 产品的帖子
目前,Sora 被视为“仅仅”一个视频生成模型,但来自 Nvidia 的 Jim Fan 暗示,它可能是一个基于数据的物理引擎。这表明,AI 通过大量的现实世界视频以及(尽管未明确提及)像虚幻引擎那样考虑物理行为的视频,可能理解物理定律和现象。如果是这样,未来不久,文本到三维的出现也是高度可能的。
Jim Fan 通过 X 发布的引人注目的帖子
***非常感谢您阅读这篇文章。
你对这篇文章的点赞和对***我的新闻通讯 的订阅会给我带来很大的动力!
你的时间序列预测到底有多可靠?
如何通过交叉验证、可视化和统计假设检验来揭示最佳的预测时间范围
·发表于Towards Data Science ·13 分钟阅读·2024 年 4 月 5 日
--

图片由Nigel Tadyanehondo提供,来自Unsplash
想象一下,你拥有一颗水晶球——一个神秘的家族传家宝,代代相传。它显得有些老旧,清澈度和光泽早已不复存在,表面上还散落着一些缺口。
尽管它的来源模糊不清,但你在其中看到的事情似乎总会以某种方式变为现实,至少在短期内是如此。它经常向你展示未来的事件,但你到底能有多大程度上信任它呢?
我这里提到的水晶球当然是我们的时间序列模型,我们按照与 Meta 的 Prophet 套件相同的方法构建了这些模型。我调皮地将我的实现称为“假先知”,但它看起来绝非如此,所产生的预测结果看起来相当准确(而且我有交叉验证结果来证明这一点)。
然而,它毕竟只是一个模型,除了通常会出现错误外,模型还往往在极端情况和边缘情形下表现不佳;在这个背景下,极端情况指的是预测时间跨度极大的情况。
接下来,我们将构建一个时间序列模型来预测英国的道路交通事故……
ReLU 如何使神经网络能够逼近连续非线性函数?
了解如何通过使用 ReLU 激活的单隐藏层神经网络来表示连续非线性函数。
·发表于Towards Data Science ·5 分钟阅读·2024 年 1 月 21 日
--
激活函数在神经网络(NN)中起着至关重要的作用,因为它们引入了非线性,使得网络能够学习比简单线性回归更复杂的特征和函数。最常用的激活函数之一是修正线性单元(ReLU),已经有理论研究表明,ReLU 能够使神经网络逼近广泛的连续函数,使其成为强大的函数逼近器。
在这篇文章中,我们特别研究了连续非线性(CNL)函数的逼近,这是使用神经网络(NN)而非简单线性回归模型的主要目的。更具体地,我们考察了 CNL 函数的两个子类别:连续分段线性(CPWL)函数和连续曲线(CC)函数。我们将展示如何使用包含一个隐藏层的神经网络,通过足够的神经元和 ReLU 激活,来表示这两种函数类型。
为了说明问题,我们仅考虑单一特征输入,但这个概念同样适用于多个特征输入。
ReLU 激活

图 1:修正线性单元(ReLU)函数。
ReLU 是一个分段线性函数,由两部分线性函数组成:一部分将负值截断,输出为零;另一部分为非负值提供连续的线性映射。
连续分段线性函数逼近
CPWL 函数是具有多个线性部分的连续函数。每个部分的斜率是一致的,然后通过添加新的线性函数在过渡点发生突变。

图 2:使用神经网络(NN)进行 CPWL 函数近似的示例。在每个过渡点,新的 ReLU 函数被加入到/从输入中减去,以增加/减少斜率。
在使用 ReLU 激活函数和线性输出层的单隐藏层神经网络中,激活输出被聚合以形成 CPWL 目标函数。隐藏层的每个单元负责一个线性片段。在每个单元处,新增的 ReLU 函数对应于斜率的变化,从而产生新的斜率(参见图 2)。由于此激活函数始终为正,增加斜率的单元对应的输出层权重将为正,反之,减小斜率的单元对应的权重将为负(参见图 3)。新函数在过渡点添加,并且由于 ReLU 激活函数的禁用范围,它在该点之前(有时也在该点之后)不会对结果函数产生贡献。

图 3:使用包含一个带 ReLU 激活和线性输出层的隐藏层的神经网络对图 2 中的 CPWL 目标函数的近似。
示例
为了让它更加具体,我们考虑一个由 4 个线性片段组成的 CPWL 函数,定义如下。

图 4:PWL 函数的示例。
为了表示该目标函数,我们将使用一个包含 4 个单元的 1 隐藏层神经网络,并且一个线性层输出前一层激活输出的加权和。我们来确定网络的参数,以便隐藏层的每个单元表示目标的一个片段。为了这个示例,输出层的偏置(b2_0)设置为 0。

图 5:建模图 4 中定义的 PWL 函数的网络架构。


图 6:单元 0(a1_0)的激活输出。


图 7:单元 1(a1_1)的激活输出,聚合到输出(a2_0)以产生片段(2)。红色箭头表示斜率的变化。


图 8:单元 2(a1_2)的输出,聚合到输出(a2_0)以产生片段(3)。红色箭头表示斜率的变化。


图 9:单元 3(a1_3)的输出,它被聚合到输出(a2_0)中生成段(4)。红色箭头表示斜率的变化。
连续曲线函数近似
我们将要研究的下一类连续非线性函数是 CC 函数。虽然这一子类别没有明确的定义,但一种非正式的定义方式是 CC 函数是那些不是分段线性的连续非线性函数。一些 CC 函数的例子包括:二次函数、指数函数、正弦函数等。
一个 CC 函数可以通过一系列微小的线性片段来近似,这被称为该函数的分段线性近似。线性片段的数量越多,每个片段的大小越小,近似结果就越接近目标函数。因此,与之前相同的网络架构,如果具有足够数量的隐藏单元,也可以很好地近似曲线函数。
然而,在实际情况中,网络是为了拟合给定的数据集而训练的,其中输入输出映射函数是未知的。具有过多神经元的架构容易出现过拟合、高方差,并且需要更多的训练时间。因此,隐藏单元的适当数量应该足够大以正确拟合数据,同时又要足够小以避免过拟合。此外,在神经元数量有限的情况下,一个具有低损失的良好近似会在有限域内有更多的过渡点,而不是均匀采样方式下的等距过渡点(如图 10所示)。

图 10:两种分段线性近似连续曲线函数(以虚线表示)。近似 1 在有限域内有更多的过渡点,并且比近似 2 更好地拟合目标函数。
总结
在这篇文章中,我们研究了 ReLU 激活函数如何允许多个单元为结果函数做出贡献而不相互干扰,从而实现连续的非线性函数近似。此外,我们还讨论了选择网络架构和隐藏单元数量的问题,以获得良好的近似结果。
希望这篇文章对你的机器学习学习过程有所帮助!
进一步思考的问题:
-
如果增加具有 ReLU 激活函数的隐藏层数量,近似能力会如何变化?
-
ReLU 激活函数如何用于分类问题?
除非另有说明,所有图片均由作者提供
你应该如何测试你的机器学习项目?初学者指南
通过使用标准库如 Pytest 和 Pytest-cov,为你提供友好的机器学习项目测试入门介绍
·发表于 Towards Data Science ·10 分钟阅读·2024 年 7 月 4 日
--

代码测试,图像由作者提供
介绍
测试是软件开发中至关重要的一个组成部分,但根据我的经验,在机器学习项目中它往往被忽视。很多人知道应该测试他们的代码,但并不是所有人都知道该如何做以及真正去做。
本指南旨在介绍如何测试机器学习管道中各个部分的基本内容。我们将专注于在IMDb 数据集上为文本分类微调 BERT,并使用行业标准库如 pytest 和 pytest-cov 进行测试。
我强烈建议你跟随这个GitHub 仓库上的代码:
[## GitHub - FrancoisPorcher/awesome-ai-tutorials: 最佳的 AI 教程集合,让你成为...
最佳的 AI 教程集合,让你成为数据科学大师!- FrancoisPorcher/awesome-ai-tutorials
项目概述
这是该项目的简要概述。
bert-text-classification/
├── src/
│ ├── data_loader.py
│ ├──…
Spotify 是如何实现个性化有声书推荐的?
使用图神经网络的个性化有声书推荐
·发布于Towards Data Science ·8 分钟阅读·2024 年 11 月 22 日
--
介绍
Spotify 是全球最受欢迎的音乐流媒体应用程序。除了歌曲和专辑,Spotify 还拥有大量播客和脱口秀节目。最近,他们在应用中推出了有声书。和其他产品一样,Spotify 希望确保其有声书推荐符合用户的偏好。因此,他们开发了一种基于图神经网络的推荐算法来个性化有声书推荐。
本文讨论了 Spotify 在提供个性化有声书推荐时面临的挑战,以及为解决这些挑战所进行的数据探索分析。它探讨了 Spotify 的创新解决方案:一个双塔图神经网络模型,旨在增强有声书的个性化推荐。

图片由Jukka Aalho提供,来源于Unsplash
挑战
由于有声书是 Spotify 内容库中的新成员,他们面临了一些挑战——
- 由于有声书是新近推出的内容类型,数据稀缺成为一个问题。与其他内容类型相比,有声书的用户互动较少。许多用户甚至不知道 Spotify 有提供有声书。
大脑与人工智能如何克服遗忘
弥合差距:大脑启发的解决方案应对人工神经网络中的灾难性遗忘
·发表于 Towards Data Science ·13 分钟阅读 ·2024 年 2 月 2 日
--

记忆对我们的生存和身份至关重要。学习的目标是将记忆存储在大脑中,以便在需要时能够快速或直观地提取。它适用于我们日常生活中的一切,例如技能、习惯、推理、社交互动和决策。虽然我们从出生开始就不断学习,但许多人希望我们能够更快地学习,记得更多。实际上,我们经常抱怨忘记了很多事情,而有时我们却无法摆脱那些想要忘记的坏记忆。随着年龄的增长,痴呆症成为影响我们生活及周围人生活的主要关注问题之一。
尽管人工智能(AI)借鉴了人类大脑的灵感,并在深度学习方面取得了显著进展,但人工神经网络(ANNs)面临的一个挑战是灾难性遗忘,即当网络在学习新信息时,之前掌握的任务出现突如其来的、剧烈的性能下降。灾难性遗忘通常发生在人工神经网络按顺序学习多个任务时。然而,人类可以在一生中学习大量任务,而不会忘记之前学过的内容。
大语言模型如何在网络中迷失并发现图形推理
|图形|大语言模型|推理|图形推理|
提升大语言模型:通过图形推理与指令调优的旅程
·发布于 Towards Data Science ·阅读时间 10 分钟·2024 年 9 月 12 日
--

该图像由作者使用人工智能创建
在一个长篇故事的格式中,你必须为你的角色设定一个图形。—— 苏尼尔·格罗弗
大语言模型 (LLMs) 展现了令人难以置信的能力,这些能力最近已经扩展到文本之外。一方面,我们见证了多模态模型(例如,视觉-语言模型);另一方面,我们也见证了模型能力的扩展,涵盖了需要推理的技能。例如,现在我们有专门用于解决数学问题或编写代码的模型。
然而,最近,另一种数据类型引起了研究人员的关注。事实上,现实世界中的大量数据可以以图形的形式表示。例如,社交网络就是一种以图形结构组织的数据,因为表示各种实体之间的关系非常重要。这并不是唯一的例子:在生物医学科学中,通常使用图形表示分子,以及蛋白质之间的相互作用。然而,大语言模型与图形的交互是最近才出现的……
小型神经网络如何表示基本函数
通过简单的算法示例,温和地介绍机械解释性
·发布于 Towards Data Science ·阅读时长 9 分钟 ·2024 年 9 月 10 日
--

介绍
本文展示了小型人工神经网络(NN)如何表示基本函数。目标是提供关于神经网络如何工作的基本直觉,并作为机械解释性的温和介绍——这是一个旨在逆向工程神经网络的领域。
我展示了三个基本函数示例,使用简单算法描述每个示例,并展示这些算法如何“编码”到神经网络的权重中。然后,我探讨了网络是否能通过反向传播学习这些算法。我鼓励读者将每个示例视为一个谜题,在阅读解答之前稍作思考。
机器学习拓扑
本文试图将神经网络(NN)分解为离散操作,并将其描述为算法。另一种可能更常见且自然的方法是,观察不同层中线性变换的连续拓扑解释。
以下是一些有助于增强拓扑直觉的优秀资源:
-
Tensorflow Playground — 一个用于建立分类任务基本直觉的简单工具。
-
ConvnetJS 演示 — 一个更复杂的工具,用于可视化神经网络在分类任务中的表现。
-
神经网络、流形与拓扑 — 一篇很好的文章,帮助建立神经网络如何工作的拓扑直觉。
三个基本函数
在以下所有示例中,我将“神经元”一词用于神经网络计算图中的单个节点。每个神经元只能使用一次(没有循环;例如,不能是 RNN),它执行以下三个操作,顺序如下:
-
与输入向量的内积。
-
添加偏置项。
-
运行一个(非线性)激活函数。

我仅提供最简洁的代码片段,以便阅读更加流畅。这个 Colab 笔记本 包含了完整的代码。
< 操作符
需要多少个神经元来学习“x < 10”的函数?编写一个神经网络,当输入小于 10 时返回 1,否则返回 0。
解决方案
让我们从创建一个符合我们想要学习的模式的示例数据集开始
X = [[i] for i in range(-20, 40)]
Y = [1 if z[0] < 10 else 0 for z in X]

创建并可视化“< 操作符”的训练数据
这个分类任务可以使用 逻辑回归 和 Sigmoid 作为输出激活函数来解决。使用一个神经元,我们可以将函数写成 Sigmoid(ax+b)。b,偏置项,可以被认为是神经元的阈值。直观地,我们可以设定 b = 10 和 a = -1,得到 F=Sigmoid(10-x)
让我们使用 PyTorch 实现并运行 F
model = nn.Sequential(nn.Linear(1,1), nn.Sigmoid())
d = model.state_dict()
d["0.weight"] = torch.tensor([[-1]]).float()
d['0.bias'] = torch.tensor([10]).float()
model.load_state_dict(d)
y_pred = model(x).detach().reshape(-1)

Sigmoid(10-x)
看起来是正确的模式,但我们能做出更精确的近似吗?例如,F(9.5) = 0.62,我们希望它接近 1。
对于 Sigmoid 函数,当输入接近 -∞ / ∞ 时,输出分别接近 0 / 1。因此,我们需要让我们的 10 - x 函数返回较大的数值,这可以通过将其乘以一个更大的数,比如 100,来实现,得到 F=Sigmoid(100(10-x)),现在我们会得到 F(9.5) =~1。

Sigmoid(100(10-x))
的确,当使用一个神经元训练网络时,它会收敛到 F=Sigmoid(M(10-x)),其中 M 是一个标量,在训练过程中不断增大,使得近似更加精确。

Tensorboard 图 — X 轴表示训练轮次,Y 轴表示网络的偏置和权重值。偏置和权重成反比例增加/减少。也就是说,网络可以写成 M(10-x),其中 M 是一个在训练过程中不断增长的参数。
为了澄清,我们的单神经元模型仅仅是“<10”函数的一个近似。我们永远无法达到零损失,因为神经元是一个连续函数,而“<10”不是一个连续函数。
Min(a, b)
编写一个神经网络,输入两个数字,返回它们之间的最小值。
解决方案
如前所述,让我们先创建一个测试数据集并进行可视化
X_2D = [
[random.randrange(-50, 50),
random.randrange(-50, 50)]
for i in range(1000)
]
Y = [min(a, b) for a, b in X_2D]

可视化 Min(a, b) 的训练数据。两个横轴表示输入的坐标。垂直轴标记为“Ground Truth”,即预期输出——即两个输入坐标的最小值
在这种情况下,ReLU 激活是一个不错的选择,因为它本质上是一个最大值函数(ReLU(x) = max(0, x))。事实上,使用 ReLU 可以将最小值函数写成如下形式
min(a, b) = 0.5 (a + b -|a - b|) = 0.5 (a + b - ReLU(b - a) - ReLU(a - b))
[方程 1]
现在,让我们构建一个小型网络,能够学习方程 1,并尝试使用梯度下降训练它
class MinModel(nn.Module):
def __init__(self):
super(MinModel, self).__init__()
# For ReLU(a-b)
self.fc1 = nn.Linear(2, 1)
self.relu1 = nn.ReLU()
# For ReLU(b-a)
self.fc2 = nn.Linear(2, 1)
self.relu2 = nn.ReLU()
# Takes 4 inputs
# [a, b, ReLU(a-b), ReLU(b-a)]
self.output_layer = nn.Linear(4, 1)
def forward(self, x):
relu_output1 = self.relu1(self.fc1(x))
relu_output2 = self.relu2(self.fc2(x))
return self.output_layer(
torch.cat(
(x, Relu_output1, relu_output2),
dim=-1
)
)

MinModel 计算图的可视化。绘图使用了 Torchview 库
训练 300 个周期足以收敛。让我们看看模型的参数
>> for k, v in model.state_dict().items():
>> print(k, ": ", torch.round(v, decimals=2).numpy())
fc1.weight : [[-0\. -0.]]
fc1.bias : [0.]
fc2.weight : [[ 0.71 -0.71]]
fc2.bias : [-0.]
output_layer.weight : [[ 1\. 0\. 0\. -1.41]]
output_layer.bias : [0.]
许多权重被归零,我们剩下的结果看起来相当不错
model([a,b]) = a - 1.41 * 0.71 ReLU(a-b) ≈ a - ReLU(a-b)
这不是我们预期的解决方案,但它是一个有效的解决方案,甚至比方程 1更简洁!通过观察这个网络,我们学到了一个新的、看起来不错的公式!证明:
证明:
-
如果 a <= b: model([a,b]) = a — ReLU(a-b) = a — 0 = a
-
如果 a > b: a — ReLU(a-b) = a — (a-b) = b
是否为偶数?
创建一个神经网络,输入一个整数 x,返回 x mod 2。也就是说,如果 x 是偶数,输出 0;如果 x 是奇数,输出 1。
这个看起来相当简单,但出人意料的是,使用标准的非周期性激活函数(如 ReLU),无法创建一个有限大小的网络,正确分类 (-∞, ∞) 范围内的每个整数。
定理:is_even 需要至少 log 个神经元
一个带有 ReLU 激活函数的网络至少需要 n 个神经元,才能正确地将每个 2^n 连续自然数分类为偶数或奇数(即解决 is_even 问题)。
证明:使用归纳法
基础:n == 2: 直观地说,单个神经元(形如 ReLU(ax + b))无法解决 S = [i + 1, i + 2, i + 3, i + 4],因为它不是线性可分的。例如,假设 a > 0 且 i + 2 是偶数。如果 ReLU(a(i + 2) + b) = 0, 那么 ReLU(a(i + 1) + b) = 0(单调函数),但 i + 1 是奇数。
更多的细节可以在经典的《感知机》书中找到。
假设对于 n,观察 n+1: 设 S = [i + 1, …, i + 2^(n + 1)],并假设为了矛盾,S 可以用大小为 n 的网络解决。从第一层的输入神经元 f(x) = ReLU(ax + b) 开始,其中 x 是网络的输入。WLOG a > 0。根据 ReLU 的定义,存在一个 j,使得:
S’ = [i + 1, …, i + j], S’’ = [i + j + 1, …, i + 2^(n + 1)]
f(x ≤ i) = 0
f(x ≥ i) = ax + b*
需要考虑两种情况:
-
情况 |S’| ≥ 2^n:删除 f 及其所有边缘不会改变网络在 S’ 上的分类结果。因此,存在一个大小为 n-1 的网络解决了 S’。矛盾。
-
情况 |S’’|≥ 2^n:对于每个神经元 g,它将 f 作为输入 g(x) = ReLU(cf(x) + d + …) = ReLU(c ReLU(ax + b) + d + …),删除神经元 f 并直接将 x 连接到 g,得到 ReLU(cax + cb + d + …)。一个大小为 n — 1 的网络解决了 S’’。矛盾。
对数算法
需要多少个神经元才能分类 [1, 2^n]?我已经证明了 n 个神经元是必要的。接下来,我将证明 n 个神经元也是足够的。
一个简单的实现是一个不断加减 2 的网络,并检查是否在某一时刻达到 0。这将需要 O(2^n) 个神经元。一个更高效的算法是加减 2 的幂,这将只需要 O(n) 个神经元。更正式地:
f_i(x) := |x — i|
f(x) := f_1∘ f_1∘ f_2 ∘ f_4∘ … ∘ f_(2^(n-1)) (|x|)*
证明:
-
根据定义:*∀ x ϵ[0, 2^i]: f_(2^(i-1)) (x) ≤ 2^(i-1)。
即,将区间一分为二。*
-
递归地 f_1∘ f_1∘ f_2 ∘ … ∘ f_(2^(n-1)) (|x|) ≤ 1
-
对于每个偶数 i: is_even(f_i(x)) = is_even(x)
-
同样,is_even(f_1( f_1(x))) = is_even(x)
-
我们得到了 f(x) ϵ {0,1} 且 is_even(x) =is_even(f(x))。证毕。
实现
让我们尝试使用神经网络在一个小的范围内实现这个算法。我们再次从定义数据开始。
X = [[i] for i in range(0, 16)]
Y = [z[0] % 2 for z in X]

在小范围 [0, 15] 上的 is_even 数据和标签
因为该范围包含 2⁴ 个整数,所以我们需要使用 6 个神经元。5 个用于 f_1∘ f_1∘ f_2 ∘ f_4∘ f_8, + 1 个输出神经元。让我们构建网络并硬编码权重。
def create_sequential_model(layers_list = [1,2,2,2,2,2,1]):
layers = []
for i in range(1, len(layers_list)):
layers.append(nn.Linear(layers_list[i-1], layers_list[i]))
layers.append(nn.ReLU())
return nn.Sequential(*layers)
# This weight matrix implements |ABS| using ReLU neurons.
# |x-b| = Relu(-(x-b)) + Relu(x-b)
abs_weight_matrix = torch_tensor([[-1, -1],
[1, 1]])
# Returns the pair of biases used for each of the ReLUs.
get_relu_bias = lambda b: torch_tensor([b, -b])
d = model.state_dict()
d['0.weight'], d['0.bias'] = torch_tensor([[-1],[1]]), get_relu_bias(8)
d['2.weight'], d['2.bias'] = abs_weight_matrix, get_relu_bias(4)
d['4.weight'], d['4.bias'] = abs_weight_matrix, get_relu_bias(2)
d['6.weight'], d['6.bias'] = abs_weight_matrix, get_relu_bias(1)
d['8.weight'], d['8.bias'] = abs_weight_matrix, get_relu_bias(1)
d['10.weight'], d['10.bias'] = torch_tensor([[1, 1]]), torch_tensor([0])
model.load_state_dict(d)
model.state_dict()
如预期的那样,我们可以看到该模型在 [0,15] 范围内做出了完美的预测。

正如预期的那样,它无法推广到新的数据点。

我们看到我们可以硬编码该模型,但使用梯度下降法时,模型是否会收敛到相同的解呢?

答案是——并非如此简单!相反,它卡在了一个局部最小值——预测均值。
这是一个已知现象,其中梯度下降可能会卡在局部最小值。它在非光滑的错误面上,特别是对于高度非线性的函数(如 is_even)更为常见。
更多细节超出了本文的范围,但为了获得更多直观理解,可以参考许多研究经典 XOR 问题的工作。即使是这样一个简单的问题,我们也可以看到梯度下降法在找到解决方案时可能会遇到困难。特别是,我推荐理查德·布兰德的短篇书籍《学习 XOR:探索经典问题的空间》——这是对 XOR 问题误差表面的严谨分析。
结语
希望这篇文章帮助你理解了小型神经网络的基本结构。分析大型语言模型要复杂得多,但这是一个快速发展的研究领域,充满了引人入胜的挑战。
在使用大型语言模型时,很容易专注于提供数据和计算能力,以实现令人印象深刻的结果,而不理解它们的运作方式。然而,解释性提供了关键的洞察力,能够帮助解决公平性、包容性和准确性等问题,这些问题在我们越来越依赖 LLM 做决策时变得愈加重要。
为了进一步探索,我推荐关注AI 对齐论坛。
*所有图片均由作者创作。介绍图是使用 ChatGPT 创作的,其余图像是使用 Python 库创建的。
如何在数据科学面试中脱颖而出
提高获得数据岗位的机会
·发布于Towards Data Science ·阅读时间:7 分钟·2024 年 7 月 27 日
--

Sebastian Herrmann的照片,来源于Unsplash
在获得我第一个数据科学职位之前,我申请了超过 300 家公司。现在,经过近三年的工作经验和多次面试,我可以公平地说,我已经学到了一些关于数据科学面试的知识。
因此,在本文中,我想向你介绍你应该做什么,以提高通过任何面试的机会!
我假设你已经安排了几天后的面试,所以你有一点时间来准备。此外,这些要点主要针对初级和中级职位,因为这是我经验的所在。
👉 注意:你也可以观看这篇博客文章的视频版本
了解职位要求
有时,我们会随便申请那些标注为“数据科学家”的职位,而不仔细阅读职位要求和职责。我以前也做过这件事,这不一定是坏事,因为你只是想先获得机会...
如何在数据科学领域取得进展
我的成为高质量数据科学家的建议和经验
·发表于 Towards Data Science ·阅读时长:8 分钟·2024 年 3 月 16 日
--

图片来源:Tima Miroshnichenko:www.pexels.com/photo/man-in-white-dress-shirt-using-laptop-7567434/
一旦你成为了一名数据科学家,知道自己通过几天、几周,甚至几个月的学习和研究最终得到了回报,这是一种非常棒的感觉。
然而,这仅仅是一个开始。
你可能不想成为一个普通的数据科学家,而是想成为一个出色的数据科学家。所以,在这篇文章中,我将分享一些根据我的个人经验以及我从顶尖数据科学家身上观察到的,能帮助你成为数据科学领域顶尖 1%的一些方法。
持续改进

图片来源:David Gavi 在Unsplash
书籍《原子习惯》(作者:James Clear)中提到的一个观点是,每天进步 1%,随着时间的推移,这种进步会大大复利。
-
每天提高 1%意味着 1.01³⁶⁵ ~ 38
-
每天下降 1%意味着 0.99⁹³⁶⁵…
如何在数据分析师的职业生涯中取得进展
2024 年提升职业的 5 步指南
·发表于Towards Data Science ·阅读时间 6 分钟·2024 年 1 月 16 日
--

照片来自Vidar Nordli-Mathisen于Unsplash
在当今快速发展的技术世界中,进入数据行业是迈向更大数据职业的第一步。既然你已经是一个数据分析师,接下来会发生什么?作为职业发展的常见方式,人们通常专注于扩大自己的技能,涵盖更复杂的概念,获得认证,或者创建一个作品集,帮助你迈向下一步。
如果你是一个刚开始作为数据分析师的人,想要展望未来的职业发展,或者你已经在这个角色中一段时间,正在寻求晋升,那么这份指南正是你需要的,它帮助你深入了解如何在这个需求量大的数据分析领域起步并推动职业发展。
1. 设定新一年的目标
一切从设定目标开始!
在每年年底(假期前),抽出一些时间写下新一年的目标和计划。这是反思过去一年的最佳时机——你学到了什么,哪些方面可以做得更好,你的成功时刻,以及评估自己现在的位置。
一旦你思考了自己目前的处境,你就可以设想自己未来想要什么……
市场营销科学 101:如何使用合成控制分析基于地理的活动
使用 Meta 的 GeoLift 库
·发表于 Towards Data Science ·6 分钟阅读·2024 年 5 月 22 日
--

(如果你不是 Medium 会员,可以在 这里) 阅读完整文章
导航基于位置的测试的复杂性可能是一个挑战。
幸运的是,GeoLift 在这里可以优化这个过程。
由 Meta 的市场营销科学团队开发,这个开源库旨在进行基于地理位置的实验。它使我们能够:
-
轻松选择测试市场,且
-
生成一个合成控制市场,用来与选定的测试地点进行基准对比
虽然 GeoLift 文档提供了关于选择测试市场的全面指南,但本文将重点介绍如何创建和使用合成控制市场。
让我们深入了解。
合成控制方法(SCM)
这些图表展示了“差异中的差异”方法与合成控制方法(SCM)之间的比较。


由作者使用 ChatGPT x Wolfram 生成
根据维基百科,SCM
该方法旨在估算在没有实施干预的情况下,处理组可能的结果。与差异中的差异方法不同,SCM 通过加权控制组,使其在干预前尽可能接近处理组,从而调整时间变化的混杂因素。
简单来说,SCM 构建了一个“平行宇宙”,使您能够观察如果没有启动活动,潜在的结果会是什么,从而清晰地比较和衡量活动的真实增量效果。
精简版合成对照创建指南
1. 处理数据
2. 生成权重
3. 可视化合成对照
4. 测量增量效果
前两步在 GeoLift 文档中有详细说明。为了保持一致性,我会在这里包括代码和输出。
我们将在 R 中执行初步步骤,并在 Python 中执行最后两步。
如果您主要使用 Python,请不要担心——R 代码简单易懂,我保证这将非常容易跟随!
1. 处理数据
您的数据应该是什么样子的:
-
日常或每周粒度
-
至少是测试持续时间的 4-5 倍的前期活动历史数据
-
至少 20 个或更多的地理单元
-
更多最佳实践
安装包:
加载数据:
- 将 GeoLift 包中包含的 GeoLift 数据加载到 R 中。这些数据是基于 40 个美国城市在 90 天内的模拟数据,时间范围是 2021 年 1 月 1 日到 2021 年 3 月 31 日。
library(GeoLift)
data(GeoLift_PreTest)
- 检查数据
> head(GeoLift_PreTest)
location Y date
1 new york 3300 2021-01-01
2 new york 3202 2021-01-02
3 new york 4138 2021-01-03
4 new york 3716 2021-01-04
5 new york 3270 2021-01-05
6 new york 3260 2021-01-06
- 格式化数据
GeoTestData_PreTest <- GeoDataRead(data = GeoLift_PreTest,
date_id = "date",
location_id = "location",
Y_id = "Y",
X = c(), #empty list as we have no covariates
format = "yyyy-mm-dd",
summary = TRUE)
##################################
##### Summary #####
##################################
* Raw Number of Locations: 40
* Time Periods: 90
* Final Number of Locations (Complete): 40
-
Y_id 是您的关键绩效指标,如收入或活跃用户数。
-
X = c() 允许您包含与目标指标动态相关的可选协变量,例如用户留存,而不是像城市人口或中位收入这样的静态指标。
2. 生成权重
假设我们的实验将在德克萨斯州的奥斯汀市进行。我们的目标是创建一个合成对照,它在实验开始之前尽可能地反映奥斯汀的实际情况。
创建权重:
- 指定奥斯汀作为测试城市
weights <- GetWeights(Y_id = "Y",
location_id = "location",
time_id = "time",
data = GeoTestData_PreTest,
locations = c("austin"),
pretreatment_end_time = 90,
fixed_effects = TRUE)
-
pretreatment_end_time = 90 使用测试开始前 90 天的历史数据来合成对照城市。根据需要调整此持续时间以适应您的实验。
-
要在多个城市进行测试,请在locations中指定它们,例如,locations = c(“austin”, “dallas”)
排除任何市场不作为对照:
exclude_markets <- c("honolulu","washington")
GeoTestData_PreTest_Excl <- subset(GeoTestData_PreTest, !location %in% exclude_markets)
然后将GeoTestData_PreTest_Excl传入上面的GetWeights函数。
显示前几名权重:
> head(dplyr::arrange(weights, desc(weight)))
location weight
1 cincinnati 0.35232541
2 detroit 0.27955009
3 honolulu 0.12960818
4 minneapolis 0.10951033
5 portland 0.06265098
6 san antonio 0.01844960
下载权重和市场数据到 csv:
write.csv(weights, "/Users/mandyliu/Documents/R/geolift_weights.csv", row.names=FALSE)
write.csv(GeoLift_PreTest, "/Users/mandyliu/Documents/R/market_data.csv", row.names=FALSE)
3. 可视化合成对照
现在我们已经有了权重文件,我们将创建一个合成对照地理对象,与我们的测试市场——奥斯汀进行比较。
设置:
- 在 Python 中导入包
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
from datetime import datetime, timedelta, date
- 读取数据
df_weights = pd.read_csv('/Users/mandyliu/Documents/R/geolift_weights.csv')
df_markets = pd.read_csv('/Users/mandyliu/Documents/R/market_data.csv')


作者在 Jupyter notebook 中创建
将权重和城市结合在一起,创建一个单一的对照:
# convert to pandas datetime
df_markets['date']=pd.to_datetime(df_markets['date'],format = '%Y-%m-%d')
# combine control markets with weights
df_markets_weights = df_markets.merge(df_weights, on='location')
df_markets_weights['weighted_Y'] = df_markets_weights['Y'] * df_markets_weights['weight']
# sum weighted_Y by date to create a single synthetic control city
df_syn_control = df_markets_weights.groupby('date').sum('weighted_Y')\
.reset_index()[['date','weighted_Y']].rename(columns = {'weighted_Y':'Y'})
df_syn_control['location'] = 'syn_control'
# append Austin data to syn control
df_markets_austin = df_markets[df_markets['location']=='austin'].reset_index(drop = True)
df_syn_control_austin = pd.concat([df_markets_austin,df_syn_control],ignore_index = True)
可视化奥斯汀与合成对照
sns.lineplot(data=df_syn_control_austin, x="date", y="Y",hue = 'location',palette=['purple', 'pink'])
plt.xticks(rotation=45)

作者在 Jupyter notebook 中创建
检查相关性:
df_corr = df_syn_control[['date','Y']].merge(df_markets_austin[['date','Y']],on = 'date')
df_corr['Y_x'].corr(df_corr['Y_y'])
奥斯汀与合成对照的相关性为0.950——非常稳固!
测量增量效果
有一点小的变化。
尽管奥斯丁及其对照组显示出高度的相关性,图表揭示了对照组始终呈现更高的值。
为了方便直接比较,我们可以应用一个乘数来对齐启动前的数值。
创建乘数:
# create multiplier
austin_Y = df_markets_austin.loc[df_markets_austin['date']==df_markets_austin['date'].max(),'Y'].iloc[0]
syn_control_Y = df_syn_control.loc[df_syn_control['date']==df_syn_control['date'].max(),'Y'].iloc[0]
M = austin_Y/syn_control_Y
M = 0.9556415990151904
模拟启动后的数据:
- 在 2021 年 4 月 1 日启动日期后创建两周的数据
# create a list of dates
date_list = []
start_date = df_markets_austin['date'].max()
k = 15
for day in range(1,k):
date = start_date + timedelta(days=day)
date_list.append(date)
# create fake austin data post-launch
data_austin_new = {
"date":date_list,
"Y": df_markets_austin.tail(14)['Y'].values*1.2, #assuming a 20% lift
"location": ['austin'] * 14
}
df_austin_new = pd.DataFrame(data_austin_new)
df_markets_austin_test = pd.concat([df_markets_austin,df_austin_new])
# create fake synthetic control data post-launch
data_syn_control_new = {
"date":date_list,
"Y": df_syn_control.tail(14)['Y'].values,
"location": ['syn_control'] * 14
}
df_syn_control_new = pd.DataFrame(data_syn_control_new)
df_syn_control_test = pd.concat([df_syn_control,df_syn_control_new])
#adjust synthetic control with multiplier M
df_syn_control_adj = df_syn_control_test.copy()
df_syn_control_adj['Y'] = df_syn_control_adj['Y']*M
# combine austin and adjusted control data
df_syn_control_austin_adj = pd.concat([df_markets_austin_test,df_syn_control_adj]\
,ignore_index = True)
可视化奥斯丁与调整后的对照组:
ax = sns.lineplot(data=df_syn_control_austin_adj, x="date", y="Y",hue = 'location',palette=['purple', 'pink'])
ax.axvline(x = date(2021,3,31),c='b', linestyle = "dashed")
plt.xticks(rotation=45)

作者在 Jupyter notebook 中创建
我们创建了一张引人注目的图表,完美展示了启动前后的不同!
收获
尽管现实数据可能更为复杂,但通过比较测试市场与合成对照,我们能够观察到随时间推移的增量提升
由于我们使用历史数据将对照组在干预前紧密地校准到处理组,合成方法通常比差异中的差异方法提供更可靠且更具可解释性的结果。
资源
你可能也会喜欢这个:
今天就从他们那里学习,快速推进你的职业生涯
towardsdatascience.com
如何通过数据回答商业问题
数据分析是通过回答抽象的商业问题来推动商业决策的关键,但这很难做到准确无误
·发表于Towards Data Science ·阅读时间:13 分钟·2024 年 11 月 19 日
--

图片由作者提供
“我们观察到核心指标下降,发生了什么?”
“客户流失的驱动因素是什么?”
作为数据科学家,我们每天都会遇到这样的问题。利益相关者遇到他们想要更好理解的内容,他们依赖我们将这样一个开放性问题解答并找出其中的逻辑。
然而,即使是经验丰富的数据科学家,有时也会对这些问题的模糊性或开放性感到震惊。因此,他们不知道从哪里开始,也不知道如何推进这些分析。
实际上,许多分析要么停留在 30,000 英尺的高空,只有皮毛,无法深入,要么陷入一个离原始问题非常远的死胡同。
如开头所述,数据科学家通常会遇到两类开放性商业问题:
- 那些更探索性的问题,旨在识别过去事件的驱动因素;大多数公司/人员称之为根本原因分析(例如:“XX 指标一周一周地下降,原因是什么…”)
如何在 Power BI 中基于多个列应用 RLS
通常,RLS 数据访问是基于单列的数据。但有时,数据访问必须由多个列来控制。我们该如何做到这一点呢?
·发布于Towards Data Science ·10 分钟阅读·2024 年 5 月 23 日
--

图片由Patrick Robert Doyle提供,来自Unsplash
介绍
在之前的文章中,我详细介绍了如何使用行级安全性(RLS)在 Power BI 中进行数据访问。我探讨了基础知识,并深入研究了高级技巧,现在我想讨论如何基于多个列来实现 RLS。
在“正常”情况下,我可以根据某一列中的数据控制对数据的访问。
在这种情况下,RLS 角色可能相对简单,正如我在之前的关于 RLS 的文章中所描述的那样:
在 Power BI 中实现行级安全性是开发人员常遇的任务。让我们来看看实现这一目标的技巧……
towardsdatascience.com
即使我们需要检查 RLS 角色中的多个列,我们也可以创建一个 RLS 表达式来通过LOOKUPVALUE()函数来处理此问题。
然而,我想在本文中进一步讨论,加入一种建模技巧,简化 RLS 角色中的 DAX 表达式。
目标是如何准备数据,将 RLS 角色简化到最小:
Email = USERPRINCIPALNAME()
但首先,我必须向你展示数据和其中的挑战:
场景和我们的数据
首先,让我们看看我的数据。
我的公司,Evergreen Products,有一份员工列表,列出了他们在组织层级中的职位:

图 1 — 我的数据摘录,展示了不同层级的几位经理(作者绘制)
看看这三位标记的经理。三位经理都负责一个层级中的分支,从不同的层级开始。
-
Andrea Adams 负责整个销售团队。
-
Andrea Madson 负责中央地区的销售。
-
John Carter 负责大宗产品的生产。
每位经理必须能够访问组织内整个分支。
这里的挑战是如何高效地实现这一点。
我可以编写一个漂亮的 DAX 表达式,并用一些魔法来实现这个 RLS 角色中的功能。
但是每当用户访问这个数据模型时,这个魔术般复杂的 DAX 表达式会被执行,可能会大大减慢报告的速度。
我想避免这个问题。
准备数据模型
好的,我该如何解决这个挑战?
我必须添加计算表和列,以将每位经理与他以及所有下属的组织单位在层级中的映射起来。
逻辑目标数据模型如下所示:

图 2 — 逻辑目标数据模型(作者绘制)
RLS-Access 表是我将在其中应用 DAX 表达式的 RLS 角色表。
我写下了“逻辑目标数据模型”,因为最终的数据模型将包含一个额外的表格。不过稍后会详细介绍。
我将使用 Power Query 创建这些表,而不是 DAX,因为这种方法能更高效地存储数据模型中的数据。
首先,我需要一个包含每个层级的“Key”列。
为此,我将把每个层级连接成一个键列。每个层级的列通过下划线分隔,使用以下 M 表达式:
[Org Level 1] & "_" &
(if [Org Level 2] = null then "" else [Org Level 2]) & "_" &
(if [Org Level 3] = null then "" else [Org Level 3]) & "_" &
(if [Org Level 4] = null then "" else [Org Level 4]) & "_" &
(if [Org Level 5] = null then "" else [Org Level 5])
)
我必须添加多个“if”语句,以覆盖空单元格。
这是因为我从 SQL Server 数据库中获取数据,表达式被转换为 SQL 并发送到源数据库(参见 查询折叠)。
在 SQL 中将数据与空数据(NULL)连接会导致结果为空。
接下来,我将通过引用员工表创建一个新表,但只保留必要的列:

图 3 — RLS-Access 表摘录(作者绘制)
然后,我过滤此表格,只保留经理,通过将 IsManager 列过滤为 True。
不幸的是,这还不够。
我仍然无法基于多个层级控制数据访问。
为了解决这个问题,我需要一个映射表,将 RLS-Access 表和员工表连接起来,映射每个层级与每个角色组合之间的关系。
我通过将 RLS-Access 表相乘并附加每个变体,形成一个大型映射表来实现这一点。
这个映射表是我上面将数据模型称为“逻辑”的原因,因为它只是一个技术上的必要性,而不是一个与业务相关的表。
完整的技术数据模型如下:

图 4 — 完整技术数据模型(图源:作者)
创建映射表
我需要做的是为每个层级创建一张表,其中每个层级有一组行。
例如,我将为整个层级创建一组映射到 CEO 顶层的映射。
然后,我将为 CEO 以下的管理层创建一组,以此类推。
我在 Power Query 中创建一个名为 CEOLevel 的新引用,来源于员工表。
接下来,我删除“ID”、“FirstName”、“LastName”、“Name”和“EMail”列。
然后,我添加一个新的计算列,称为 RLS-Key,使用以下 M 表达式:
[Org Level 1] & "_CEO___"
最后,我删除所有列,保留层级键和新的 RLS-Key 列。
表格现在看起来是这样的:

图 5 — 从 CEO 层级表中提取数据(图源:作者)
该表包含每个现有组织单元的行,以便 CEO 可以访问所有这些单元。稍后我将解释其机制。
该表设置为不加载到 Power BI 中:

图 6 — 禁用 CEOLevel 表的加载(图源:作者)
这是因为它是一个中介表,将作为构建最终“RLS-Mapping”表的构建块使用。
现在,我重复这些步骤,创建一个名为 OrgLevel2 的另一个中介表。
但创建 RLS-Key 列的表达式是不同的:
[Org Level 1] & "_" &
(if [Org Level 2] = null then "" else [Org Level 2]) & "___"
如您所见,结果中我通过这个表达式包括了组织的第二级:

图 7 — OrgLevel2 表的提取数据(图源:作者)
基于这些步骤,我复制 CEOLevel 表并重复这些步骤,为 OrgLevel3–5 创建另外三个中介表。
对于每个表,我修改 RLS-Key 列的表达式,加入层级中的另一级:
对于 OrgLevel3,我使用以下表达式:
[Org Level 1] & "_" &
(if [Org Level 2] = null then "" else [Org Level 2])
& "_" &
(if [Org Level 3] = null then "" else [Org Level 3])
& "__"
请注意,我每次在代码的最后一行中删除一个下划线,因为它包含在表达式的前一部分中。
这是 OrgLevel4 的情况:
[Org Level 1] & "_" &
(if [Org Level 2] = null then "" else [Org Level 2])
& "_" &
(if [Org Level 3] = null then "" else [Org Level 3])
& "_" &
(if [Org Level 4] = null then "" else [Org Level 4])
& "_"
最后,对于 OrgLevel5,不会有最终的下划线,因为第五级是层级中的最后一级:
[Org Level 1] & "_" &
(if [Org Level 2] = null then "" else [Org Level 2])
& "_" &
(if [Org Level 3] = null then "" else [Org Level 3])
& "_" &
(if [Org Level 4] = null then "" else [Org Level 4])
& "_" &
(if [Org Level 5] = null then "" else [Org Level 5])
现在,我可以创建最终的 RLS 映射表:
我点击 CEOLevel 表,然后点击追加查询来创建一个新表:

图 8 — 调用“追加查询”功能来创建 RLS-mapping 表(图源:作者)
现在,我将把所有五个表合并成一个新表:

图 9 — 将所有中介表添加到新表中(图源:作者)
新的表格叫做“Append1”,我将其重命名为 RLS-Mapping。
最后,我将通过选择这两列并执行“删除重复项”来删除表中的所有重复项:

图 10 — 从 RLS-Mapping 表中删除重复项(图示作者提供)
为了使一切看起来干净整洁,我将这两列的数据显示类型改为文本。
现在,我可以将这个表格整合到我的数据模型中并实现 RLS 角色。
实现 RLS
在将新表加载到 Power BI 后,我必须像上面所描述的那样将其添加到数据模型中。
我删除了“RLS-Access”表与“Employees”表之间的关系,并添加了两个新关系:
-
RLS-Mapping[RLS-Key]到 RLS-Access[HierarchyKey]。 多对一
-
Employees[HierarchyKey]到 RLS-Mapping[HierarchyKey]。 多对多。RLS-Mapping 过滤 Employees
接下来,我可以将一个新的 RLS 角色添加到数据模型中:

图 11 — 将新的 RLS 角色添加到数据模型中(图示作者提供)
就这样。
我可以为 Andrea Madson 测试该功能:

图 12 — 测试 RLS 角色(图示作者提供)
这是结果(记住,Andrea Madson 负责中央区域的销售):

图 13 — 测试 RLS 角色的结果,针对 Andrea Madson(图示作者提供)
空级别的问题可以通过用上一级的内容填充它们来解决。某些可视化工具可以在层次结构中折叠重复的级别(如 Zebra BI)。
但标准的矩阵可视化无法做到这一点。
因此,我们只能接受这种效果。
它是如何工作的?
那么,它是如何工作的?
让我们看一下 Andrea Madson 的示例。
在 Employees 表中,她的行具有 HierarchyKey“Evergreen Products_Sales_Sales Central__”。
这与 RLS-Access 表中的值相同。
当我们将 RLS 角色应用到 RLS-Access 表时,这个过滤器会通过关系应用到 RLS-Mapping 表的 RLS-Key 列:
这个过滤器导致以下行保留在 RLS-Mapping 表中:

图 14 — 在为 Andrea Madson 应用 RLS-Key 列过滤器后剩余的行(图示作者提供)
HierarchyKey 列中的剩余值然后通过关系传递到 Employees 表,剩余的行是组织中 Sales Central 单位的一部分:

图 15 — 过滤后的 Andrea Madson 组织单元的行(图示作者提供)
这个机制适用于所有其他单位,因为我已经为 RLS-Mapping 表复制了行。
结论
这种方法使我能够在 RLS 角色中实现一个非常简单的 DAX 表达式。
如我在上一篇关于 RLS 的文章中所解释的(该文章位于下面参考部分的链接列表中),简化 RLS 角色中的 DAX 表达式是非常重要的。
原因是,这些 DAX 表达式会添加到每个发送到数据模型的 DAX 查询中。复杂的 DAX 表达式可能会导致报告性能变差。
因此,以此方式建模数据的积极效果是,我可以遵循这条规则。
即使这意味着在 Power BI 和 Power Query 中建模数据时要付出额外的努力。
你的用户可能不会感谢你,但他们也不会抱怨,这更好。

图片来源:Brett Jordan于Unsplash
参考资料
数据是通过虚构的名字自生成的。
我通过将一份名字和姓氏的列表相乘,生成了这个完整的列表。
然后,我基于随机数字生成了一个事实表。
以下是我关于数据访问和 RLS 的其他文章链接:
在我发表的第一篇关于这个主题的文章一年后,这里有关于 Power BI 中新安全功能的更新
[towardsdatascience.com ## 在 Power BI 中开发和测试 RLS 规则
很多时候,并不是所有的用户都应该有权限访问报告中的所有数据。在这里,我将解释如何开发 RLS……
[towardsdatascience.com [## 在 Power BI 中实现 RLS 后,计算总百分比
在实施 RLS 之后,某些用户只能看到数据集的部分内容。但当他们必须进行比较时,会发生什么呢……
探索实现 RLS 规则的所有方法 ## 探索实现 RLS 规则的所有方法
在 Power BI 中实现行级安全性是开发人员的一个常见任务。让我们来看看这些技术……
towardsdatascience.com [## 每当 Salvatore Cagliari 发布新内容时,您将收到电子邮件。
每当 Salvatore Cagliari 发布新内容时,您将收到电子邮件。通过注册,您将创建一个 Medium 账户,如果您还没有的话……
medium.com](https://medium.com/@salvatorecagliari/subscribe?source=post_page-----2c67f980983f--------------------------------)
我使我的文章对所有人可访问,即使 Medium 有付费墙。这让我能够从每个读者那里赚取一些收入,但我关闭了付费墙,您可以免费阅读我的文章。
您可以通过
buymeacoffee.com/salvatorecagliari
或扫描此二维码:

任何支持都将不胜感激,并帮助我找到更多时间为你创作更多内容。
非常感谢您。
如何将中心极限定理应用于约束数据
我们可以如何描述在区间[a, b]内分布的数据的均值?
·发表于数据科学前沿 ·14 分钟阅读·2024 年 12 月 10 日
--
假设我们正在衡量一位不受欢迎的政治家的支持率。假设我们随机抽取了十次民意调查,并得到以下结果:
我们如何构造关于该政治家支持率均值的后验分布?
假设这些民意调查是独立且同分布的随机变量,X_1, …, X_n。中心极限定理告诉我们,样本均值将渐近地趋近于一个正态分布,方差为σ²/n。
其中μ和σ²分别是 X_i 的均值和方差。

图 1:我们不受欢迎的政治家的样本批准均值的标准化直方图以及正态分布近似图,n=1、n=3、n=5、n=7、n=10 和 n=20。我们可以看到,当 n=10 时,样本均值分布已经相当接近其正态分布近似。图由作者提供。
受此渐近极限的启发,我们用以下公式来近似观察数据y的似然:

使用客观先验
(稍后会详细说明),并且通过对σ²的积分,我们得到了后验分布的 t 分布,π(µ|y)
其中
让我们来看一下表 1 中数据的后验分布。
如何作为初学者接近复杂的数据科学主题
·发表于 Towards Data Science ·通过 电子报 发送 ·3 分钟阅读·2024 年 7 月 25 日
--
感到有动力写下你的第一篇 TDS 文章吗?我们始终欢迎新作者的投稿。
当我们遇到一个新问题、话题或挑战时,迈出第一步往往是最难的部分。正是这个时刻,自我怀疑开始涌现,已有的知识感觉模糊且不充分,拖延似乎成了唯一可接受的选择。
本周我们的亮点文章可能无法神奇地解决你作为数据科学家或机器学习工程师所面临的每一个挑战,但它们都提供了一个务实的、以行动为导向的路线图,帮助你克服学习过程中的初期障碍。
从扩展你的基础统计学知识到成为一名更好的写作者,这些文章涵盖了成功的数据专业人员所擅长的各个领域和技能。祝你阅读愉快!
-
什么是因果推断? 从随机对照试验、差异中的差异到合成控制和 A/B 测试,Khin Yadanar Lin 提供了一份易于理解、详细(但不令人不知所措)的因果推断及其在日常工作流中的实际应用的入门介绍。
-
理解条件概率与贝叶斯定理
有时候,追溯一个概念的起源可以帮助我们全面理解其重要性及应用场景。Sachin Date正是通过他对条件概率和贝叶斯定理起源的深入回顾,阐述了它们在回归分析中的应用。
-
手工深入解析 LSTMs 与 xLSTMs通过结合强有力的叙述结构和精心制作的插图,Srijanie Dey 博士的《By Hand》系列一直取得了成功;她的最新一篇作品也不例外,深入探讨了长短期记忆网络(LSTMs)及其最近的变种 xLSTMs(扩展长短期记忆网络)的基础数学原理。

图片来自S. Tsuchiya在Unsplash上的作品
-
线性规划优化:基础
在他关于线性规划系列的首篇文章中,"一种用于改进决策的强大优化技术",Jarom Hulet重点为学习者建立了一个坚实的基础,涵盖了在进入更复杂的实践方法之前,你需要了解的关键概念。
-
如何开始技术写作与博客
我们都知道如何写作,但迈向更有意识、更有规律的写作实践可能会让人望而却步。Egor Howell多年来一直是数据科学(及其他技术话题)的成功博主,现在他分享了切实可行的见解,帮助他人成长并在这个潜在的职业发展的领域中取得成功。
想让你的学习向其他方向拓展吗?不要错过我们本周推荐的其他阅读,它们涵盖了人工智能、数据可视化等前沿话题。
-
富有吸引力且发人深省,Joshua Banks Mailman 博士的关于反事实的深度分析邀请我们思考,它们如何"帮助我们以不同的方式思考生成性 AI 的陷阱与潜力"。
-
如果你想了解图结构数据的新研究进展,我们强烈推荐Qitian Wu对这一激动人心的机器学习子领域三篇近期论文的全面概述。
-
最近对 ChatGPT 的更新如何影响其在数据分析任务中的表现?Yu Dong反思了聊天机器人作为未来商业智能工具的潜力。
-
基于他在开发创新数据可视化格式方面的成功经验,Nick Gerend推出了他最新的创作:路径群集和超群集图表,利用圆形排列来表示单个观测值。
-
对于任何想要直接进入代码的人,Shreya Rao在这里提供了一个快速简洁的神经网络实现指南,帮助大家在 TensorFlow 和 PyTorch 中实现神经网络。
-
为什么数据质量项目很少实现其潜力?Barr Moses解析了有效构建这些项目的挑战,并分享了如何避免过去错误的可操作见解。
-
我们本周的精选以另一个有趣且实用的实践教程结束:Carmen Adriana Martínez Barbosa 博士和 José Arturo Celis-Gil 的云检测与分割项目教程,该项目利用卫星图像和几种强大的算法。
感谢你支持我们作者的工作!我们喜欢发表新作者的文章,如果你最近写了一篇有趣的项目教程、操作指南或关于我们核心话题的理论反思,欢迎与我们分享。
直到下一个 Variable,
TDS 团队
如何作为数据科学家更好地沟通
我在麦肯锡的经历中学到的最宝贵的教训
·发表于 Towards Data Science ·阅读时间 6 分钟·2024 年 5 月 29 日
--

图片由作者提供
在我之前的文章中,我提到过:“传达我们的工作和编写代码、构建模型与产品一样重要。”
不幸的是,实际上我观察到很多数据科学家(尤其是初级数据科学家)在讲故事的部分遇到了困难。他们手里有所有的数据,但由于某些原因,信息似乎无法传递给听众,分析结果最终变成了另一本被搁置在书架上、积满灰尘的漂亮报告,毫无影响力。那么,到底出了什么问题呢?
以下是我多年来学到的一些技巧,能帮助你作为数据科学家提升沟通能力。
始终使用金字塔原理
许多数据科学家以线性方式进行沟通(如下图左侧所示)。这可以理解,因为我们日常生活中的经验就是如此,我们讲故事也是按这种方式进行的。但这并不是传递重要信息的最有效方式,因为观众很容易在这种沟通方式中迷失或者感到无聊。
如何利用缓存技术提升 Python 性能

图片由作者在 Canva 中制作
Python 中的高级但开箱即用的缓存装饰器
·发表于Towards Data Science ·8 分钟阅读·2024 年 4 月 29 日
--
一旦你不再是 Python 新手,就该开始探索 Python 中的内置特性了。我敢打赌,Python 中有许多开箱即用的内置特性会让你惊讶。今天,我将在本文中介绍其中一个。
你是否遇到过需要多次执行某个函数且一些结果可以复用的场景?在下面的第一个示例中,你将看到缓存机制使得递归函数的性能提升了 120 倍。因此,在本文中,我将介绍自 Python 3.2 版本以来内置的lru_cache装饰器。
在functools模块中,还有一个装饰器叫做cache,它更为直观。然而,易于使用有时意味着灵活性较差。因此,我将从cache装饰器开始,介绍它的局限性。接下来,我将重点讲解lru_cache如何为我们提供更多的灵活性。
1. @cache 装饰器回顾

图片由作者在 Canva 中制作
如何将 SQL Server 数据导入 Microsoft Fabric
选项,选项……在本文中,你将了解将本地 SQL Server 数据导入 Microsoft Fabric 的各种可能性。
·发布于 Towards Data Science ·10 分钟阅读·2024 年 12 月 4 日
--

图片来自作者
选项,选项,选项……能够以多种不同方式执行某个任务,通常是一个很棒的“问题”,尽管很常见的情况是并非每个选项都同样有效。而且,Microsoft Fabric 就是关于“选项”的……你想导入数据吗?没问题,你可以使用笔记本、管道、数据流或 T-SQL。需要数据转换吗?一点也不担心——同样,你可以利用笔记本、T-SQL、数据流……需要数据处理吗?湖仓(Spark)、数据仓库(SQL)、实时智能(KQL)、Power BI……再次选择权在你手中。
简而言之,几乎在 Microsoft Fabric 中,每个任务都可以通过多种方式完成,没有“正确”或“错误”的工具,只要它能完成工作(当然,尽可能高效)。
因此,本文不是关于以“正确”的方式将本地 SQL Server 数据导入 Fabric,而是概述了我们目前可以使用的选项。探讨这些选项并写作的主要动机是我最近几个月常被问到的问题:“我们有我们的……
如何为数据科学团队构建能力框架
对于那些领导数据科学团队的人,这里有 6 项关键能力,可以以稳健和客观的方式区分初级和高级人员。
·发表于Towards Data Science ·阅读时长 10 分钟·2024 年 9 月 12 日
--

2021 年,365 DataScience 对成千上万的 LinkedIn 个人资料进行了一项研究,以了解数据科学领域的趋势。几个真正突出的观点是:“很少有人(不到 2%)在同一职位上待超过 5 年”[1]和“我们的研究中,数据科学家在职的中位数时间为 1.7 年”[1]。幸运的是,我在我的团队中没有看到这种高流动性,但我认识许多数据科学家,他们大多数人认为‘角色不明确’**是他们面临的前三大挑战之一。如果你感觉自己或团队中正在发生这种情况,我希望这篇文章能帮助你建立一个适应性强、公平且稳健的能力框架,以解决这个‘角色不明确’的问题。
PS:所有图片均由我创作,除非另有说明。
首先,为什么我们需要能力框架?
能力框架是团队用来传达特定角色或层级所需、被重视、被认可和奖励的行为的手段。例如……
如何构建数据驱动的客户管理系统
客户基础管理
构建具有战略影响的 CBM 系统的高级框架
·发布于 Towards Data Science ·阅读时间 13 分钟·2024 年 11 月 20 日
--

图片由作者使用 Canva 制作
想象一下,如果您的客户数据能够讲述一个故事——一个能够驱动关键决策、优化定价并准确预测未来收入的故事。
作为一名数据科学家,我的职业生涯中有很大一部分时间是在设计和构建客户基础管理(CBM)系统。这些系统可以监控和预测从客户流失和客户价格弹性到推荐客户最有可能购买的产品等各方面的内容。
我亲眼见证了 CBM 系统如何成功地被组织采用,并帮助它们通过有效定价、改善客户保持率和更有针对性的销售活动执行战略。结合预测功能和仪表盘的视觉效果,CBM 系统为管理者提供了一种有效的方式,与高层领导进行沟通。这增强了决策能力,并帮助领导者更好地理解其行动的后果,尤其是在客户基础和未来预计收入方面。
在本文中,我们将探讨 CBM 系统的基础组成部分,以及那些真正提升价值生成的更高级功能。通过本指南的学习,您将清晰地了解 CBM 系统的关键组件,以及如何通过先进模块增强这些基础,从而为您的公司带来竞争优势。
强大的 CBM 系统的基础

图片由作者使用 Canva 制作
CBM 系统的三个主要基础组件是:
-
ELT(提取-加载-转换)
-
基础流失建模
-
仪表板
拥有这三个要素后,管理者可以对流失有一个基本的理解,对数据有一个可视化的把握,此外,它还允许他们将任何发现传达给领导层和其他利益相关者。以下我们将详细阐述每个组件。
ELT — 获取数据
提取-加载-转换,简称 ELT,是客户基础管理系统中最初且最关键的部分。这个组件为系统提供数据,通常是构建 CBM 系统时最先搭建的组件。为了开始工作,你通常会与某种数据平台进行交互,这些平台上大多数初步的数据操作已经完成(在这种情况下,你技术上只需要执行“加载”和“转换”),但有时你也需要直接从源系统提取数据,进行“提取”。
无论数据来自哪里,都需要将其加载到系统中,并转换成易于输入到你所使用的机器学习模型中的格式。你可能还需要将数据转换为便于从数据中生成仪表板的格式。对于某些仪表板,还可能需要将大量数据预先汇总到较小的表格中,以提高查询和绘图性能。
很容易看出,如果 ELT 过程中出现错误,或者传入的数据有误,这可能会严重影响 CBM 系统。由于系统中的一切都基于这些传入的数据,因此必须格外小心,确保数据的准确性和与业务规则的对齐。
我曾多次看到 ELT 过程失败,导致流失预测和仪表板出现错误。监控进入系统的数据一致性的一种方法是记录每个变量的分布,并跟踪该分布随时间的变化。如果分布发生了剧烈变化,你可以迅速检查是否是 ELT 过程中的逻辑出了问题,或者问题是否来自更上游的数据问题。
基础流失建模
了解客户群体的第二个关键组件是识别客户流失的时间、原因及对象(对于非从业者来说,“流失”是指客户停止使用某个产品或服务的时点)。一个好的流失预测算法可以帮助企业将客户保持工作的重点放在最重要的地方,并能帮助识别出那些面临较高流失风险的客户。
在 1990 年代中期,电信公司、银行、保险公司和公用事业公司是最早开始在客户群体中使用流失建模的行业之一,开发基础流失模型相对来说比较简单。
首要任务是确定流失的定义。在许多情况下,这个定义非常简单,例如当客户取消了电信合同时。然而,在其他行业,例如电子商务中,确定流失的定义时需要一定的判断。例如,可以定义客户在最后一次购物后的 200 天内没有再次购物,即为流失。
确定了流失之后,我们需要为模型选择一个结果期,即我们希望观察流失的时间框架。例如,如果我们想创建一个结果期为 10 周的流失模型,这将生成一个预测客户在评分时刻与接下来 10 周之间的任何时刻流失的可能性的模型。或者,我们可以选择一年的结果期,这将生成一个预测客户在未来一年内任何时间点流失的模型。
在确定了结果期和流失定义之后,分析师需要将数据转化为一种格式,使机器学习模型能够轻松进行训练,并且在后续推断时也能使用。
在模型训练完成并且预测正在对活跃客户群体进行时,有多个不同的应用场景。例如,我们可以使用流失评分来识别那些高风险流失的客户,并通过特定的留存活动或定价促销来针对这些客户。我们还可以根据客户的流失评分为不同客户群体创建差异化的营销材料。或者,我们可以将流失评分与客户拥有的产品结合使用,开发客户生命周期价值模型,进而用于优先安排各种客户活动。显然,适当的流失模型可以为公司提供战略性优势,使其在管理客户群体方面比那些忽视这一基本组件的竞争对手更具竞争力。
仪表板
仪表板、商业智能和分析曾在 2000 年代和 2010 年代初期风靡一时,在机器学习算法逐渐成熟后,焦点转向了预测而非描述性和往往是回顾性的数据。然而,对于 CBM 工具来说,仪表板仍然是一个至关重要的组件。它们使管理者能够有效地与领导层沟通,特别是当与价格优化等高级功能结合使用时。可视化特定定价策略的潜在影响,对于决策制定具有强大的支持作用。
和任何数据科学项目一样,你可能会投入成千上万小时来构建一个系统,但通常,仪表板是经理和高管的主要交互点。如果仪表板不够直观或性能不好,它可能会掩盖你所构建的其他所有内容的价值。
此外,仪表盘提供了一种对数据进行视觉检查的方法,有时能够揭示未被发掘的机会。尤其是在系统实施后的早期阶段,可能在所有控制流程尚未投入生产之前,持续进行视觉检查所有变量和模型表现,可以作为一个很好的安全网。
在主要的基础性要素到位之后,我们可以探索更先进的功能,这些功能有潜力带来更大的价值和洞察力。
通过高级组件增强战略优势

图片由作者使用 Canva 创建
尽管一个基本的 CBM 系统会提供一些稳定的好处和洞察,但要从 CBM 系统中获取最大价值,需要更多的高级组件。下面我们讨论一些最重要的组件,比如拥有多个时间视野的流失模型、增加价格优化、使用基于模拟的预测以及加入竞争对手定价数据。
多时间视野流失模型
有时候,从不同的角度审视流失是有意义的,其中一个角度是你允许模型的时间视野——或结果周期——的长短。对于某些业务场景,拥有一个短期结果周期的模型是合理的,而对于其他场景,拥有一个 1 年的结果周期模型则可能更加合适。
为了更好地解释这个概念,假设你建立了一个结果周期为 10 周的流失模型。这个模型可以用来预测一个客户是否会在 10 周内流失。然而,假设现在你已经确定了一个具体的事件,知道它会导致流失,并且你有一个短暂的窗口期,可能只有 3 周来实施预防措施。在这种情况下,训练一个 3 周时间视野的流失模型是合理的,且条件是这个你已知会导致流失的具体事件。这样,你就可以将任何保持活动集中在那些最有可能流失的客户身上。
这种差异化的方法允许更具战略性的资源分配,将重点放在最需要的高影响力干预上。通过将模型的时间视野调整为特定的情境,企业可以优化其客户保持工作,最终提升客户生命周期价值,并减少不必要的流失。
定价优化与客户价格弹性
价格在许多情况下是战略执行的最终部分,而胜者是那些能够有效地将战略转化为有效定价体系的公司。这正是带有价格优化的 CBM 系统可以帮助公司做到的事情。虽然价格优化的话题本身就值得写成一篇文章,但我们在下面简要总结了其关键理念。
开始时需要做的第一件事是获取历史价格数据。最好是跨时间和其他解释性变量的不同价格水平。这可以帮助你制定价格弹性的估计值。一旦这个基础做好了,你可以根据不同价格点开发预期的流失率,并用它来预测预期的收入。通过从客户级别汇总,可以得到产品层面的预期值和预期流失率,从而找出每个产品的最优价格。在更复杂的情况下,每个产品还可以有多个具有各自最优价格点的客户群体。
例如,假设一家公司有两个不同的产品,产品 A 和产品 B。对于产品 A,公司希望扩大用户基础,并且只愿意接受一定量的流失,同时保持在市场上的竞争力。然而,对于产品 B,他们愿意接受一定量的流失,以换取与预期收入相关的最优价格。CBM 系统允许实施这样的策略,并为领导层提供该策略未来预期收入的预测。
基于模拟的预测
基于模拟的预测提供了一种比仅仅基于预期值做点估计更为稳健的方式来生成预测估计。通过使用像蒙特卡罗模拟这样的方式,我们能够为结果生成概率密度,从而为决策者提供预测的区间。这比仅仅使用点估计更强大,因为我们能够量化不确定性。
为了理解基于模拟的预测如何使用,我们可以通过一个例子来说明。假设我们有 10 个客户,每个客户都有给定的流失概率,并且每个客户都有一个年度预期收入。(实际上,我们通常有一个多变量的流失函数,能够预测每个客户的流失情况。)为了简化,假设如果客户流失,我们的收入为 0,如果他们不流失,我们保留所有收入。我们可以使用 Python 来使这个例子更具体:
import random
# Set the seed for reproducibility
random.seed(42)
# Generate the lists again with the required changes
churn_rates = [round(random.uniform(0.4, 0.8), 2) for _ in range(10)]
yearly_revenue = [random.randint(1000, 4000) for _ in range(10)]
churn_rates, yearly_revenue
这为我们提供了churn_rates和yearly_revenue的以下值:
churn_rates: [0.66, 0.41, 0.51, 0.49, 0.69, 0.67, 0.76, 0.43, 0.57, 0.41]
yearly_revenue: [1895, 1952, 3069, 3465, 1108, 3298, 1814, 3932, 3661, 3872]
使用上述数字,并假设流失事件是独立的,我们可以轻松计算出平均流失率以及总预期收入。
# Calculate the total expected revenue using (1 - churn_rate) * yearly_revenue for each customer
adjusted_revenue = [(1 - churn_rate) * revenue for churn_rate, revenue in zip(churn_rates, yearly_revenue)]
total_adjusted_revenue = sum(adjusted_revenue)
# Recalculate the expected average churn rate based on the original data
average_churn_rate = sum(churn_rates) / len(churn_rates)
average_churn_rate, total_adjusted_revenue
以下是average_churn_rate和total_adjusted_revenue的数值:
average_churn_rate:0.56,
total_adjusted_revenue: 13034.07
因此,我们可以预计约 56%的流失率和 13034 的总收入,但这并没有告诉我们关于可能出现的变化的任何信息。为了更深入地理解我们可以预期的结果范围,我们转向蒙特卡洛模拟。我们不再取流失率和总收入的期望值,而是让情况模拟 10000 次(10000 是这里任意选定的;这个数字应根据所需的分布细度来确定),在每次模拟中,客户以churn_rate的概率流失,或者以1 - churn_rate的概率留存。
import pandas as pd
simulations = pd.DataFrame({
'churn_rate': churn_rates * 10000,
'yearly_revenue': yearly_revenue * 10000
})
# Add a column with random numbers between 0 and 1
simulations['random_number'] = (
[random.uniform(0, 1) for _ in range(len(simulations))])
# Add a column 'not_churned' and set it to 1, then update it to 0 based on the random number
simulations['not_churned'] = (
simulations['random_number'] >= simulations['churn_rate']).astype(int)
# Add an 'iteration' column starting from 1 to 10000
simulations['iteration'] = (simulations.index // 10) + 1
这将产生如下表格:

模拟数据框的头部 / 图片来自作者
我们可以使用以下代码来总结我们的结果:
# Group by 'iteration' and calculate the required values
summary = simulations.groupby('iteration').agg(
total_revenue=('yearly_revenue',
lambda x: sum(x * simulations.loc[x.index, 'not_churned'])),
total_churners=('not_churned', lambda x: 10 - sum(x))
).reset_index()
最后,用plotly绘制得到的图表是:

总收入的直方图 / 图片来自作者

总流失者的直方图 / 图片来自作者
上面的图表比我们开始时的两个点估计值 0.56 和 13034 所讲述的故事更加丰富。我们现在对可能的结果有了更多的了解,并且可以进行一场有关我们可以接受哪些流失率和收入水平的有根据的讨论。
继续以上面的例子,我们可以举个例子来说,我们只愿意接受 0.1%的概率出现 8 次或更多的流失事件。通过使用个别客户的价格弹性和基于模拟的预测,我们可以调整客户的预期churn_rates,以便精确地实现这一结果。这种客户基础控制只有在先进的 CBM 系统下才能实现。
竞争对手定价的重要性
定价中最重要的因素之一是竞争对手的价格。竞争对手的激进程度在很大程度上决定了公司在定价上能有多灵活。对于公用事业或电信等商品化业务尤其如此,因为这些行业的提供商很难实现差异化。然而,尽管竞争对手定价非常重要,许多公司选择不将这些数据集成到自己的定价优化算法中。
不将竞争对手定价纳入定价算法的原因有很多。一些公司声称,收集数据过于困难且耗时,即使他们现在开始,也无法拥有足够的历史数据来训练所有价格弹性模型。其他人则认为,竞争对手产品的价格与自家产品不完全可比,收集这些数据会很困难。最后,大多数公司还声称,他们有专门的价格经理手动监控市场,当竞争对手采取行动时,他们会调整自家的价格,因此不需要在算法中包含这些数据。
第一个问题可以通过良好的网页抓取技术和其他情报收集方法逐渐得以缓解。如果这些还不够,有时也可以通过一些机构提供各行业和领域的历史市场数据,尤其是价格数据。至于关于没有可比产品的第二个问题,还可以通过机器学习技术来剖析单个产品组件的实际成本。另一种方法是使用不同的用户角色来估算一组特定产品或单一产品的总月度成本。
最终,不包含竞争对手价格会使定价算法和优化引擎处于不利地位。在价格计算器和比较网站使客户越来越容易了解市场的行业中,公司面临着被更先进的竞争对手在价格上超越的风险。
结论
在本文中,我们讨论了客户基础管理系统的主要组成部分,以及一些使这些系统变得无价的先进功能。就个人而言,在构建过几个这样的系统后,我认为结合价格优化算法——基于广泛的内部和外部数据集——以及强大的可视化界面(如仪表盘形式)是管理客户的最有效工具之一。工具的这种组合使得管理者和高层领导能够真正掌控客户管理过程,并理解他们行动的战略后果。
正如杰夫·贝索斯——其中一位最为客户至上的领导——所说:
“我们可以向你保证,我们将继续执着于客户。我们坚信这种方法——从长远来看——对于所有者和客户同样重要。” —— 杰夫·贝索斯,亚马逊 2009 年致股东信
对客户管理的承诺,以数据和人工智能驱动的洞察为支撑,不仅提升了客户满意度,而且为利益相关者确保了长期价值。
感谢阅读!
想在我发布新文章时收到通知吗? ➡️ 点击这里订阅我的新闻通讯 ⬅️。免费订阅,您可以随时取消!
如果您喜欢阅读这篇文章并希望访问我的更多内容,请随时在 LinkedIn 上与我连接,网址是 https://www.linkedin.com/in/hans-christian-ekne-1760a259/ ,或访问我的网页 https://www.ekneconsulting.com/ ,探索我提供的一些服务。请随时通过电子邮件联系我,邮箱是 hce@ekneconsulting.com
如何构建一个用于从收据中提取信息的生成式 AI 工具

DALLE-2 对“未来主义的工业文档扫描设施”的诠释
使用 LangChain 和 OpenAI 工具从存储在 Google Drive 中的收据图片中提取结构化信息
·发表于 Towards Data Science ·阅读时间 15 分钟·2024 年 4 月 10 日
--
本文详细介绍了如何使用开源 Python 包,例如 LangChain、pytesseract 和 PyPDF,结合 gpt-4-vision 和 gpt-3.5-turbo,从收据图片中识别并提取关键信息。生成的数据集可以用于“与收据对话”应用程序。完整代码请查看 这里。
纸质收据有各种风格和格式,代表了一个有趣的自动化信息提取目标。它们还提供了大量逐项列出的费用,如果将这些费用汇总到数据库中,将非常有助于那些希望比银行对账单提供的更详细地追踪支出的人。
如果你能拍一张收据的照片,上传到某个应用程序,然后提取其信息并将其附加到你的个人支出数据库中,接着可以用自然语言查询这些信息,那不是很酷吗?你可以像“我上次去宜家时买了什么?”或者“我在 Safeway 花最多钱买了哪些物品?”这样提问。这种系统也可能自然地扩展到公司财务和支出跟踪。在本文中,我们将构建一个简单的应用程序,处理这个过程的第一部分——即提取收据中的信息,准备存储到数据库中。我们的系统将监控一个 Google Drive 文件夹中新添加的收据,处理它们并将结果附加到.csv 文件中。
1. 背景和动机
从技术角度讲,我们将进行一种自动化的信息提取,称为模板填充。我们有一个预定义的字段架构,旨在从收据中提取这些字段,任务是填写这些字段,或者在适当的地方留空。这里的一个主要问题是,收据图像或扫描中的信息是非结构化的,尽管光学字符识别(OCR)或 PDF 文本提取库在查找文本时可能做得相当好,但它们在保留文档中单词相对位置方面做得不够好,这可能会使得例如将某个项目的价格与其成本匹配变得困难。
传统上,这个问题通过模板匹配来解决,其中会创建一个预定义的文档几何模板,然后只在已知包含重要信息的区域进行提取。关于这个方法的一个很好的描述可以在这里找到。然而,这个系统缺乏灵活性。如果添加了新的收据格式怎么办?
为了绕过这个问题,像AWS Textract和AWS Rekognition这样的更高级服务使用了预训练的深度学习模型结合物体检测、边界框生成和命名实体识别(NER)。我实际上还没有在手头的问题上尝试过这些服务,但如果能进行尝试,比较它们与我们使用 OpenAI 的 LLMs 构建的结果将会非常有趣。
像 gpt-3.5-turbo 这样的“大型语言模型”(LLM)在从非结构化文本中进行信息提取和模板填充方面也表现出色,尤其是在给定一些示例后。这使得它们比模板匹配或微调更加灵活,因为添加一些新的收据格式示例比重新训练模型或构建新的几何模板要快得多且更便宜。
如果我们要在从收据中提取的文本上使用 gpt-3.5-turbo,那么问题就是如何构建它可以学习的示例?当然我们可以手动完成,但这样做的扩展性不好。这里我们将探索使用 gpt-4-vision 的选项。这个版本的 gpt-4 可以处理包含图像的对话,并且特别擅长描述图像内容。给定一张收据的图像和我们想要提取的关键信息的描述,gpt-4-vision 应该能够一举完成任务,前提是图像足够清晰。
为什么我们不单独使用 gpt-4-vision 来完成这个任务,而是放弃 gpt-3.5-turbo 或其他较小的 LLM?从技术上讲,我们当然可以这样做,而且结果可能会更准确。但 gpt-4-vision 非常昂贵,且 API 调用次数有限,因此该系统也无法扩展。或许在不远的未来,视觉 LLM 会成为从文档中提取信息这一领域的标准工具。
这篇文章的另一个动机是探索如何使用 Langchain 构建这个系统,Langchain 是一个流行的开源 LLM 编排库。为了强制 LLM 返回结构化输出,需要进行提示工程,而 Langchain 有一些非常棒的工具可以做到这一点。我们还将尝试确保我们的系统构建得具有可扩展性,因为这只是可能成为更大“与收据对话”项目的第一部分。
简要的背景介绍完毕后,让我们开始写代码吧!在这里,我将使用 Python3.9 和 Langchain 0.1.14,完整的细节可以在仓库中找到。
2. 连接到 Google Drive
我们需要一个方便的地方来存储我们的原始收据数据。Google Drive 是一个选择,它提供了相对易于使用的 Python API。为了捕捉收据,我使用了GeniusScan应用,它可以将.pdf、.jpeg 或其他文件类型从手机直接上传到 Google Drive 文件夹。该应用还进行一些有用的预处理,如自动裁剪文档,这有助于提取过程。
要设置 Google Drive 的 API 访问权限,您需要创建服务账户凭据,可以按照这里的说明生成。作为参考,我在我的驱动器中创建了一个名为“receiptchat”的文件夹,并设置了一个密钥对,使其能够读取该文件夹中的数据。
以下代码可以用来设置一个驱动服务对象,它提供了访问各种方法来查询 Google Drive 的功能:
import os
from googleapiclient.discovery import build
from oauth2client.service_account import ServiceAccountCredentials
class GoogleDriveService:
SCOPES = ["https://www.googleapis.com/auth/drive"]
def __init__(self):
# the directory where your credentials are stored
base_path = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
# The name of the file containing your credentials
credential_path = os.path.join(base_path, "gdrive_credential.json")
os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = credential_path
def build(self):
# Get credentials into the desired format
creds = ServiceAccountCredentials.from_json_keyfile_name(
os.getenv("GOOGLE_APPLICATION_CREDENTIALS"), self.SCOPES
)
# Set up the Gdrive service object
service = build("drive", "v3", credentials=creds, cache_discovery=False)
return service
在我们的简单应用中,我们实际上只需要做两件事:列出驱动器文件夹中的所有文件,并下载其中一些文件。以下类处理了这一点:
import io
from googleapiclient.errors import HttpError
from googleapiclient.http import MediaIoBaseDownload
import googleapiclient.discovery
from typing import List
class GoogleDriveLoader:
# These are the types of files we want to download
VALID_EXTENSIONS = [".pdf", ".jpeg"]
def __init__(self, service: googleapiclient.discovery.Resource):
self.service = service
def search_for_files(self) -> List:
"""
See https://developers.google.com/drive/api/guides/search-files#python
"""
# This query searches for objects that are not folders and
# contain the valid extensions
query = "mimeType != 'application/vnd.google-apps.folder' and ("
for i, ext in enumerate(self.VALID_EXTENSIONS):
if i == 0:
query += "name contains '{}' ".format(ext)
else:
query += "or name contains '{}' ".format(ext)
query = query.rstrip()
query += ")"
# create drive api client
files = []
page_token = None
try:
while True:
response = (
self.service.files()
.list(
q=query,
spaces="drive",
fields="nextPageToken, files(id, name)",
pageToken=page_token,
)
.execute()
)
for file in response.get("files"):
# Process change
print(f'Found file: {file.get("name")}, {file.get("id")}')
file_id = file.get("id")
file_name = file.get("name")
files.append(
{
"id": file_id,
"name": file_name,
}
)
page_token = response.get("nextPageToken", None)
if page_token is None:
break
except HttpError as error:
print(f"An error occurred: {error}")
files = None
return files
def download_file(self, real_file_id: str) -> bytes:
"""
Downloads a single file
"""
try:
file_id = real_file_id
request = self.service.files().get_media(fileId=file_id)
file = io.BytesIO()
downloader = MediaIoBaseDownload(file, request)
done = False
while done is False:
status, done = downloader.next_chunk()
print(f"Download {int(status.progress() * 100)}.")
except HttpError as error:
print(f"An error occurred: {error}")
file = None
return file.getvalue()
运行这个代码会得到以下结果:
service = GoogleDriveService().build()
loader = GoogleDriveLoader(service)
all_files loader.search_for_files() #returns a list of unqiue file ids and names
pdf_bytes = loader.download_file({some_id}) #returns bytes for that file
太好了!现在我们可以连接到 Google Drive,并将图像或 PDF 数据导入本地机器。接下来,我们必须处理这些数据并提取文本。
3. 从 PDF 和图像中提取原始文本
有多个文档化良好的开源库可以从 PDF 和图像中提取原始文本。对于 PDF,我们将在这里使用 PyPDF,尽管为了更全面地了解类似的包,我推荐这篇文章。对于 JPEG 格式的图像,我们将使用 pytesseract,它是 tesseract OCR 引擎的包装器。关于如何安装它的说明可以在这里找到。最后,我们还希望能够将 PDF 转换为 JPEG 格式。这可以通过 pdf2image 包实现。
PyPDF 和 pytesseract 都提供了从文档中提取文本的高级方法。它们也都提供了调优的选项。例如,pytesseract 可以提取文本和边界框(请参见这里),如果我们将来想要为 LLM 提供有关其处理的收据文本格式的更多信息,这可能会很有用。pdf2image 提供了一种将 PDF 字节转换为 JPEG 图像的方法,这正是我们在这里想做的。为了将 JPEG 字节转换为可视化的图像,我们将使用 PIL 包。
from abc import ABC, abstractmethod
from pdf2image import convert_from_bytes
import numpy as np
from PyPDF2 import PdfReader
from PIL import Image
import pytesseract
import io
DEFAULT_DPI = 50
class FileBytesToImage(ABC):
@staticmethod
@abstractmethod
def convert_bytes_to_jpeg(file_bytes):
raise NotImplementedError
@staticmethod
@abstractmethod
def convert_bytes_to_text(file_bytes):
raise NotImplementedError
class PDFBytesToImage(FileBytesToImage):
@staticmethod
def convert_bytes_to_jpeg(file_bytes, dpi=DEFAULT_DPI, return_array=False):
jpeg_data = convert_from_bytes(file_bytes, fmt="jpeg", dpi=dpi)[0]
if return_array:
jpeg_data = np.asarray(jpeg_data)
return jpeg_data
@staticmethod
def convert_bytes_to_text(file_bytes):
pdf_data = PdfReader(
stream=io.BytesIO(initial_bytes=file_bytes)
)
# receipt data should only have one page
page = pdf_data.pages[0]
return page.extract_text()
class JpegBytesToImage(FileBytesToImage):
@staticmethod
def convert_bytes_to_jpeg(file_bytes, dpi=DEFAULT_DPI, return_array=False):
jpeg_data = Image.open(io.BytesIO(file_bytes))
if return_array:
jpeg_data = np.array(jpeg_data)
return jpeg_data
@staticmethod
def convert_bytes_to_text(file_bytes):
jpeg_data = Image.open(io.BytesIO(file_bytes))
text_data = pytesseract.image_to_string(image=jpeg_data, nice=1)
return text_data
上面的代码使用了抽象基类的概念来提高可扩展性。假设我们未来想要添加对另一种文件类型的支持。如果我们编写相关的类并从FileBytesToImage继承,就必须在其中编写convert_bytes_to_image和convert_bytes_to_text方法。这可以减少我们在大型应用程序中引入错误的可能性。
代码可以如下使用:
bytes_to_image = PDFBytesToImage()
image = PDFBytesToImage.convert_bytes_to_jpeg(pdf_bytes)
text = PDFBytesToImage.convert_bytes_to_jpeg(pdf_bytes)

使用上述代码从 PDF 文档中提取的文本示例。由于收据包含个人身份信息(PII),这里我们仅使用上传到 Google Drive 的随机文档进行演示。图像由作者生成。
4. 使用 gpt-4-vision 提取信息
现在,让我们使用 Langchain 提示 gpt-4-vision 从我们的收据中提取一些信息。我们可以通过使用 Langchain 对 Pydantic 的支持来创建一个输出模型。
from langchain_core.pydantic_v1 import BaseModel, Field
from typing import List
class ReceiptItem(BaseModel):
"""Information about a single item on a reciept"""
item_name: str = Field("The name of the purchased item")
item_cost: str = Field("The cost of the item")
class ReceiptInformation(BaseModel):
"""Information extracted from a receipt"""
vendor_name: str = Field(
description="The name of the company who issued the reciept"
)
vendor_address: str = Field(
description="The street address of the company who issued the reciept"
)
datetime: str = Field(
description="The date and time that the receipt was printed in MM/DD/YY HH:MM format"
)
items_purchased: List[ReceiptItem] = Field(description="List of purchased items")
subtotal: str = Field(description="The total cost before tax was applied")
tax_rate: str = Field(description="The tax rate applied")
total_after_tax: str = Field(description="The total cost after tax")
这非常强大,因为 Langchain 可以使用这个 Pydantic 模型为 LLM 构建格式说明,这些说明可以包含在提示中,强制它生成带有指定字段的 JSON 输出。添加新字段就像更新模型类一样简单。
接下来,让我们构建提示,这将只是静态的:
from dataclasses import dataclass
@dataclass
class VisionReceiptExtractionPrompt:
template: str = """
You are an expert at information extraction from images of receipts.
Given this of a receipt, extract the following information:
- The name and address of the vendor
- The names and costs of each of the items that were purchased
- The date and time that the receipt was issued. This must be formatted like 'MM/DD/YY HH:MM'
- The subtotal (i.e. the total cost before tax)
- The tax rate
- The total cost after tax
Do not guess. If some information is missing just return "N/A" in the relevant field.
If you determine that the image is not of a receipt, just set all the fields in the formatting instructions to "N/A".
You must obey the output format under all circumstances. Please follow the formatting instructions exactly.
Do not return any additional comments or explanation.
"""
现在,我们需要构建一个类,该类将接收图像,并将其与提示和格式说明一起发送给 LLM。
from langchain.chains import TransformChain
from langchain_core.messages import HumanMessage
from langchain_core.runnables import chain
from langchain_core.output_parsers import JsonOutputParser
import base64
from langchain.callbacks import get_openai_callback
class VisionReceiptExtractionChain:
def __init__(self, llm):
self.llm = llm
self.chain = self.set_up_chain()
@staticmethod
def load_image(path: dict) -> dict:
"""Load image and encode it as base64."""
def encode_image(path):
with open(path, "rb") as image_file:
return base64.b64encode(image_file.read()).decode("utf-8")
image_base64 = encode_image(path["image_path"])
return {"image": image_base64}
def set_up_chain(self):
extraction_model = self.llm
prompt = VisionReceiptExtractionPrompt()
parser = JsonOutputParser(pydantic_object=ReceiptInformation)
load_image_chain = TransformChain(
input_variables=["image_path"],
output_variables=["image"],
transform=self.load_image,
)
# build custom chain that includes an image
@chain
def receipt_model_chain(inputs: dict) -> dict:
"""Invoke model"""
msg = extraction_model.invoke(
[
HumanMessage(
content=[
{"type": "text", "text": prompt.template},
{"type": "text", "text": parser.get_format_instructions()},
{
"type": "image_url",
"image_url": {
"url": f"data:image/jpeg;base64,{inputs['image']}"
},
},
]
)
]
)
return msg.content
return load_image_chain | receipt_model_chain | JsonOutputParser()
def run_and_count_tokens(self, input_dict: dict):
with get_openai_callback() as cb:
result = self.chain.invoke(input_dict)
return result, cb
这里需要理解的主要方法是 set_up_chain,我们将逐步讲解这些步骤。这些步骤的灵感来自于这篇 博客文章。
-
初始化提示,在这种情况下,它只是一个包含一些通用指令的文本块。
-
从我们上面创建的 Pydantic 模型中创建一个
JsonOutputParser。这将把模型转换为一组格式化指令,能够添加到提示中。 -
创建一个
TransformChain,允许我们将自定义函数(在这种情况下是load_image函数)集成到整体链中。请注意,链将接受一个名为image_path的变量,并输出一个名为image的变量,这是一个表示图像的 base64 编码字符串。这是 gpt-4-vision 接受的格式之一。 -
据我所知,
ChatOpenAI目前还不支持同时发送文本和图像。因此,我们需要创建一个自定义链,将我们创建的ChatOpenAI实例与编码图像、提示和格式化指令一起调用。
请注意,我们还利用了 openai 回调函数来统计每次调用的令牌数和相关的支出。
为了运行这个,我们可以按以下步骤操作:
from langchain_openai import ChatOpenAI
from tempfile import NamedTemporaryFile
model = ChatOpenAI(
api_key={your open_ai api key},
temperature=0, model="gpt-4-vision-preview",
max_tokens=1024
)
extractor = VisionReceiptExtractionChain(model)
# image from PDFBytesToImage.convert_bytes_to_jpeg()
prepared_data = {
"image": image
}
with NamedTemporaryFile(suffix=".jpeg") as temp_file:
prepared_data["image"].save(temp_file.name)
res, cb = extractor.run_and_count_tokens(
{"image_path": temp_file.name}
)
给定我们上面的随机文档,结果如下所示:
{'vendor_name': 'N/A',
'vendor_address': 'N/A',
'datetime': 'N/A',
'items_purchased': [],
'subtotal': 'N/A',
'tax_rate': 'N/A',
'total_after_tax': 'N/A'}
可能不太令人兴奋,但至少它按照正确的方式进行了结构化!当提供有效的收据时,这些字段将被填写,并且根据我对不同收据进行的一些测试评估,结果非常准确。
我们的回调函数如下所示:
Tokens Used: 1170
Prompt Tokens: 1104
Completion Tokens: 66
Successful Requests: 1
Total Cost (USD): $0.01302
这是跟踪成本所必需的,因为在测试像 gpt-4 这样的模型时,成本可能会迅速增长。
5. 使用 gpt-3.5-turbo 进行信息提取
假设我们已经使用第四部分中的步骤生成了一些示例并将它们保存为 json 文件。每个示例包含一些提取的文本和根据我们的 ReceiptInformation Pydantic 模型定义的相应关键信息。现在,我们想将这些示例注入到对 gpt-3.5-turbo 的调用中,期望它能将从这些示例中学到的东西推广到新的收据。少量示例学习是提示工程中的一种强大工具,如果它有效,对于这个用例将非常有用,因为每当检测到新的收据格式时,我们可以使用 gpt-4-vision 生成一个示例,并将其附加到用于提示 gpt-3.5-turbo 的示例列表中。然后,当出现类似格式的收据时,可以使用 gpt-3.5-turbo 来提取其内容。从某种程度上说,这就像模板匹配,但不需要手动定义模板。
有很多方法可以促使基于文本的 LLM 从一段文本中提取结构化信息。我发现的最新且最强大的方法之一可以在 Langchain 文档的 此处 找到。其思路是创建一个包含占位符的提示语,用于存放一些示例,然后将这些示例注入到提示语中,就像它们是通过某个 LLM 调用的函数返回的一样。这是通过 model.with_structured_output() 功能实现的,您可以在 此处 阅读相关信息。请注意,目前这个功能还处于测试阶段,可能会发生变化!
让我们看一下代码,了解这是如何实现的。我们将首先编写提示语。
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
@dataclass
class TextReceiptExtractionPrompt:
system: str = """
You are an expert at information extraction from images of receipts.
Given this of a receipt, extract the following information:
- The name and address of the vendor
- The names and costs of each of the items that were purchased
- The date and time that the receipt was issued. This must be formatted like 'MM/DD/YY HH:MM'
- The subtotal (i.e. the total cost before tax)
- The tax rate
- The total cost after tax
Do not guess. If some information is missing just return "N/A" in the relevant field.
If you determine that the image is not of a receipt, just set all the fields in the formatting instructions to "N/A".
You must obey the output format under all circumstances. Please follow the formatting instructions exactly.
Do not return any additional comments or explanation.
"""
prompt: ChatPromptTemplate = ChatPromptTemplate.from_messages(
[
(
"system",
system,
),
MessagesPlaceholder("examples"),
("human", "{input}"),
]
)
提示语文本与第四部分中的完全相同,只是我们现在有了一个 MessagesPlaceholder 来保存我们即将插入的示例。
class Example(TypedDict):
"""A representation of an example consisting of text input and expected tool calls.
For extraction, the tool calls are represented as instances of pydantic model.
"""
input: str
tool_calls: List[BaseModel]
class TextReceiptExtractionChain:
def __init__(self, llm, examples: List):
self.llm = llm
self.raw_examples = examples
self.prompt = TextReceiptExtractionPrompt()
self.chain, self.examples = self.set_up_chain()
@staticmethod
def tool_example_to_messages(example: Example) -> List[BaseMessage]:
"""Convert an example into a list of messages that can be fed into an LLM.
This code is an adapter that converts our example to a list of messages
that can be fed into a chat model.
The list of messages per example corresponds to:
1) HumanMessage: contains the content from which content should be extracted.
2) AIMessage: contains the extracted information from the model
3) ToolMessage: contains confirmation to the model that the model requested a tool correctly.
The ToolMessage is required because some of the chat models are hyper-optimized for agents
rather than for an extraction use case.
"""
messages: List[BaseMessage] = [HumanMessage(content=example["input"])]
openai_tool_calls = []
for tool_call in example["tool_calls"]:
openai_tool_calls.append(
{
"id": str(uuid.uuid4()),
"type": "function",
"function": {
# The name of the function right now corresponds
# to the name of the pydantic model
# This is implicit in the API right now,
# and will be improved over time.
"name": tool_call.__class__.__name__,
"arguments": tool_call.json(),
},
}
)
messages.append(
AIMessage(content="", additional_kwargs={"tool_calls": openai_tool_calls})
)
tool_outputs = example.get("tool_outputs") or [
"You have correctly called this tool."
] * len(openai_tool_calls)
for output, tool_call in zip(tool_outputs, openai_tool_calls):
messages.append(ToolMessage(content=output, tool_call_id=tool_call["id"]))
return messages
def set_up_examples(self):
examples = [
(
example["input"],
ReceiptInformation(
vendor_name=example["output"]["vendor_name"],
vendor_address=example["output"]["vendor_address"],
datetime=example["output"]["datetime"],
items_purchased=[
ReceiptItem(
item_name=example["output"]["items_purchased"][i][
"item_name"
],
item_cost=example["output"]["items_purchased"][i][
"item_cost"
],
)
for i in range(len(example["output"]["items_purchased"]))
],
subtotal=example["output"]["subtotal"],
tax_rate=example["output"]["tax_rate"],
total_after_tax=example["output"]["total_after_tax"],
),
)
for example in self.raw_examples
]
messages = []
for text, tool_call in examples:
messages.extend(
self.tool_example_to_messages(
{"input": text, "tool_calls": [tool_call]}
)
)
return messages
def set_up_chain(self):
extraction_model = self.llm
prompt = self.prompt.prompt
examples = self.set_up_examples()
runnable = prompt | extraction_model.with_structured_output(
schema=ReceiptInformation,
method="function_calling",
include_raw=False,
)
return runnable, examples
def run_and_count_tokens(self, input_dict: dict):
# inject the examples here
input_dict["examples"] = self.examples
with get_openai_callback() as cb:
result = self.chain.invoke(input_dict)
return result, cb
TextReceiptExtractionChain 将接受一个包含多个示例的列表,每个示例都有 input 和 output 键(注意这些键是如何在 set_up_examples 方法中使用的)。对于每个示例,我们将创建一个 ReceiptInformation 对象。然后,我们将结果格式化为一个消息列表,可以传递给提示语。tool_examples_to_messages 中的所有工作只是为了在不同的 Langchain 格式之间进行转换。
运行这段代码与我们之前使用视觉模型时非常相似:
# Load the examples
EXAMPLES_PATH = "receiptchat/datasets/example_extractions.json"
with open(EXAMPLES_PATH) as f:
loaded_examples = json.load(f)
loaded_examples = [
{"input": x["file_details"]["extracted_text"], "output": x}
for x in loaded_examples
]
# Set up the LLM caller
llm = ChatOpenAI(
api_key=secrets["OPENAI_API_KEY"],
temperature=0,
model="gpt-3.5-turbo"
)
extractor = TextReceiptExtractionChain(llm, loaded_examples)
# convert a PDF file form Google Drive into text
text = PDFBytesToImage.convert_bytes_to_text(downloaded_data)
extracted_information, cb = extractor.run_and_count_tokens(
{"input": text}
)
即使是 10 个示例,这个调用的成本也不到 gpt-4-vision 的一半,而且返回速度也更快。随着示例数量的增加,您可能需要使用 gpt-3.5-turbo-16k,以避免超出上下文窗口。
输出数据集
收集了一些收据后,您可以运行第 4 和第五部分中描述的提取方法,并将结果收集到一个数据框中。然后,它将被存储,并且每当 Google Drive 中出现新的收据时,可以将其附加到这个数据框中。

输出数据集的示例,显示了从多个收据中提取的字段。图片由作者生成
一旦我的提取的收据信息数据库变得更大一些,我计划在此基础上探索基于 LLM 的问答功能,敬请期待相关的文章!我还对为这个项目探索一种更正式的评估方法并将其与 AWS Textract 或类似产品的结果进行比较感到好奇。
感谢您看到最后!请随时在此处探索完整的代码库 github.com/rmartinshort/receiptchat。任何关于改进或扩展功能的建议都将不胜感激!
如何使用 Llama 3 构建本地文件的生成式搜索引擎
使用 Qdrant、NVIDIA NIM API 或 Llama 3 8B 在本地构建您的本地 GenAI 助手
Nikola Milosevic (Data Warrior)
·发布于Towards Data Science ·阅读时间:12 分钟·2024 年 6 月 8 日
--
5 月 23 日,我收到了一封来自 Nvidia 的邮件,邀请我参加NVIDIA 和 LangChain 的生成式 AI 代理开发者大赛。我第一反应是时间太紧,考虑到我们最近有了孩子,而且我的父母也正好要来,我应该没时间参与。但接着我又有了第二个想法,我决定可以编写一些代码并提交。我思考了几天应该做什么,最终有一个想法深深打动了我——一个开源生成式搜索引擎,允许你与本地文件进行交互。微软 Copilot 已经提供了类似的功能,但我想我可以做一个开源版本,出于兴趣,并分享我在快速编码系统过程中收获的一些经验。
系统设计
为了构建一个本地生成式搜索引擎或助手,我们需要几个组件:
-
一个包含本地文件内容的索引,以及一个信息检索引擎,用于根据给定的查询/问题检索最相关的文档。
-
一个语言模型,用于从本地文档中选择内容并生成总结性回答
-
一个用户界面
下面的图表展示了各个组件如何交互。

系统设计与架构。Qdrant 用于向量存储,而 Streamlit 用于用户界面。Llama 3 通过 Nvidia NIM API(70B 版本)或通过 HuggingFace 下载(8B 版本)使用。文档切块使用 Langchain 完成。图像由作者提供。
首先,我们需要将本地文件编入可以查询本地文件内容的索引。然后,当用户提问时,我们将使用创建的索引,以及一些非对称的段落或文档嵌入来检索可能包含答案的最相关文档。这些文档的内容和问题将传递给部署的大型语言模型,模型将使用这些文档内容生成答案。在指令提示中,我们会要求大型语言模型还返回使用文档的参考资料。最终,所有内容将通过用户界面展示给用户。
现在,让我们更详细地看看每个组件。
语义索引
我们正在构建一个语义索引,它将根据文件内容的相似性和给定查询提供最相关的文档。为了创建这样的索引,我们将使用 Qdrant 作为向量存储。值得注意的是,Qdrant 客户端库不需要完整安装Qdrant 服务器,并且可以进行文档相似性匹配,只要文档能够适应工作内存(RAM)。因此,我们所需要做的就是通过 pip 安装 Qdrant 客户端。
我们可以通过以下方式初始化 Qdrant(请注意,hf 参数稍后根据故事流程定义,但在使用 Qdrant 客户端时,您已经需要定义所使用的向量化方法和度量标准):
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams
client = QdrantClient(path="qdrant/")
collection_name = "MyCollection"
if client.collection_exists(collection_name):
client.delete_collection(collection_name)
client.create_collection(collection_name,vectors_config=VectorParams(size=768, distance=Distance.DOT))
qdrant = Qdrant(client, collection_name, hf)
为了创建一个向量索引,我们需要将文档嵌入硬盘中。对于嵌入,我们需要选择合适的嵌入方法和向量比较度量。有几种段落、句子或单词的嵌入方法可以使用,结果各不相同。基于文档创建向量搜索的主要问题是非对称搜索问题。非对称搜索问题在信息检索中很常见,当查询较短而文档较长时就会发生。单词或句子嵌入通常会进行微调,以根据相似大小的文档(句子或段落)提供相似度评分。一旦情况发生变化,正确的信息检索可能会失败。
然而,我们可以找到一种适用于非对称搜索问题的嵌入方法。例如,在 MSMARCO 数据集上微调的模型通常效果不错。MSMARCO 数据集基于 Bing 搜索查询和文档,由微软发布。因此,它非常适合我们正在处理的问题。
对于这个特定的实现,我选择了一个已经微调过的模型,名为:
sentence-transformers/msmarco-bert-base-dot-v5
该模型基于 BERT,并使用点积作为相似度度量进行了微调。我们已经初始化了 qdrant 客户端,以便在线使用点积作为相似度度量(请注意该模型的维度为 768):
client.create_collection(collection_name,vectors_config=VectorParams(size=768, distance=Distance.DOT))
我们可以使用其他度量标准,如余弦相似度,然而,鉴于该模型是使用点积微调的,我们使用该度量标准能获得最佳性能。此外,从几何角度来看:余弦相似度仅关注角度差异,而点积则同时考虑了角度和幅度。通过将数据归一化为具有相同幅度,两个度量标准变得等效。在忽略幅度有利的情况下,余弦相似度是有用的。然而,如果幅度重要,点积是更合适的相似度度量。
初始化 MSMarco 模型的代码如下(如果您有可用的 GPU,请使用它。无论如何):
model_name = "sentence-transformers/msmarco-bert-base-dot-v5"
model_kwargs = {'device': 'cpu'}
encode_kwargs = {'normalize_embeddings': True}
hf = HuggingFaceEmbeddings(
model_name=model_name,
model_kwargs=model_kwargs,
encode_kwargs=encode_kwargs
)
下一个问题是:我们需要处理的是 BERT 类模型的上下文大小受限,由于变换器模型的二次内存需求。在许多 BERT 类模型中,这个上下文大小被设置为 512 个标记。有两个选项:(1)我们可以仅根据前 512 个标记生成答案,忽略文档的其余部分,或(2)创建一个索引,其中一个文档会被拆分成多个块,并作为块存储在索引中。在第一种情况下,我们将失去大量重要信息,因此我们选择了第二种方案。为了拆分文档,我们可以使用 LangChain 中的预构建拆分器:
from langchain_text_splitters import TokenTextSplitter
text_splitter = TokenTextSplitter(chunk_size=500, chunk_overlap=50)
texts = text_splitter.split_text(file_content)
metadata = []
for i in range(0,len(texts)):
metadata.append({"path":file})
qdrant.add_texts(texts,metadatas=metadata)
在提供的代码部分中,我们将文本拆分为 500 个标记的块,每个块有 50 个重叠的标记。这样,我们在块的结束或开始处保留了一些上下文。在代码的其余部分,我们创建了带有文档路径的元数据,并将这些块和元数据添加到索引中。
然而,在我们将文件内容添加到索引之前,需要先读取它。甚至在读取文件之前,我们需要获取所有需要索引的文件。为了简化,在此项目中,用户可以定义他/她希望索引的文件夹。索引器将递归地从该文件夹及其子文件夹中检索所有文件,并索引支持的文件(我们将探讨如何支持 PDF、Word、PPT 和 TXT 文件)。
我们可以以递归方式检索给定文件夹及其子文件夹中的所有文件:
def get_files(dir):
file_list = []
for f in listdir(dir):
if isfile(join(dir,f)):
file_list.append(join(dir,f))
elif isdir(join(dir,f)):
file_list= file_list + get_files(join(dir,f))
return file_list
一旦所有文件都被检索到列表中,我们就可以读取包含文本的文件内容。在这个工具中,初始支持 MS Word 文档(扩展名为“.docx”)、PDF 文档、MS PowerPoint 演示文稿(扩展名为“.pptx”)和纯文本文件(扩展名为“.txt”)。
为了读取 MS Word 文档,我们可以使用 docx-python 库。将文档读取为字符串变量的函数大致如下所示:
import docx
def getTextFromWord(filename):
doc = docx.Document(filename)
fullText = []
for para in doc.paragraphs:
fullText.append(para.text)
return '\n'.join(fullText)
对于 MS PowerPoint 文件,也可以做类似的操作。为此,我们需要下载并安装 pptx-python 库,并编写一个类似这样的函数:
from pptx import Presentation
def getTextFromPPTX(filename):
prs = Presentation(filename)
fullText = []
for slide in prs.slides:
for shape in slide.shapes:
fullText.append(shape.text)
return '\n'.join(fullText)
阅读文本文件非常简单:
f = open(file,'r')
file_content = f.read()
f.close()
对于 PDF 文件,在这种情况下我们将使用 PyPDF2 库:
reader = PyPDF2.PdfReader(file)
for i in range(0,len(reader.pages)):
file_content = file_content + " "+reader.pages[i].extract_text()
最后,整个索引函数大致如下所示:
file_content = ""
for file in onlyfiles:
file_content = ""
if file.endswith(".pdf"):
print("indexing "+file)
reader = PyPDF2.PdfReader(file)
for i in range(0,len(reader.pages)):
file_content = file_content + " "+reader.pages[i].extract_text()
elif file.endswith(".txt"):
print("indexing " + file)
f = open(file,'r')
file_content = f.read()
f.close()
elif file.endswith(".docx"):
print("indexing " + file)
file_content = getTextFromWord(file)
elif file.endswith(".pptx"):
print("indexing " + file)
file_content = getTextFromPPTX(file)
else:
continue
text_splitter = TokenTextSplitter(chunk_size=500, chunk_overlap=50)
texts = text_splitter.split_text(file_content)
metadata = []
for i in range(0,len(texts)):
metadata.append({"path":file})
qdrant.add_texts(texts,metadatas=metadata)
print(onlyfiles)
print("Finished indexing!")
正如我们所说,我们使用 LangChain 中的 TokenTextSplitter 将文本分割成每个包含 500 个令牌并有 50 个令牌重叠的片段。现在,当我们创建了索引后,可以创建一个 Web 服务来查询索引并生成答案。
生成式搜索 API。
我们将使用 FastAPI 创建一个 Web 服务来托管我们的生成式搜索引擎。该 API 将访问我们在上一节中创建的索引数据,使用向量相似度度量进行搜索,使用最相关的片段与 Llama 3 模型生成答案,并最终将答案返回给用户。
为了初始化和导入生成式搜索组件的库,我们可以使用以下代码:
from fastapi import FastAPI
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_qdrant import Qdrant
from qdrant_client import QdrantClient
from pydantic import BaseModel
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
import environment_var
import os
from openai import OpenAI
class Item(BaseModel):
query: str
def __init__(self, query: str) -> None:
super().__init__(query=query)
如前所述,我们使用 FastAPI 来创建 API 接口。我们将利用 qdrant_client 库访问我们创建的索引数据,并使用 langchain_qdrant 库提供额外支持。对于嵌入和本地加载 Llama 3 模型,我们将使用 PyTorch 和 Transformers 库。此外,我们还将使用 OpenAI 库调用 NVIDIA NIM API,API 密钥存储在我们创建的 environment_var(包括 Nvidia 和 HuggingFace)文件中。
我们创建了一个 Item 类,继承自 Pydantic 中的 BaseModel,用于作为请求函数的参数传递。它将包含一个字段,名为 query。
现在,我们可以开始初始化我们的机器学习模型。
model_name = "sentence-transformers/msmarco-bert-base-dot-v5"
model_kwargs = {'device': 'cpu'}
encode_kwargs = {'normalize_embeddings': True}
hf = HuggingFaceEmbeddings(
model_name=model_name,
model_kwargs=model_kwargs,
encode_kwargs=encode_kwargs
)
os.environ["HF_TOKEN"] = environment_var.hf_token
use_nvidia_api = False
use_quantized = True
if environment_var.nvidia_key !="":
client_ai = OpenAI(
base_url="https://integrate.api.nvidia.com/v1",
api_key=environment_var.nvidia_key
)
use_nvidia_api = True
elif use_quantized:
model_id = "Kameshr/LLAMA-3-Quantized"
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(
model_id,
torch_dtype=torch.float16,
device_map="auto",
)
else:
model_id = "meta-llama/Meta-Llama-3-8B-Instruct"
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(
model_id,
torch_dtype=torch.float16,
device_map="auto",
)
在前几行代码中,我们加载了针对 MSMARCO 数据进行微调的基于 BERT 的模型权重,这个模型也用于索引我们的文档。
然后,我们检查是否提供了 nvidia_key,如果提供了,我们使用 OpenAI 库调用 NVIDIA NIM API。当我们使用 NVIDIA NIM API 时,可以使用一个大版本的 Llama 3 指令模型,拥有 70B 参数。如果未提供 nvidia_key,我们将本地加载 Llama 3。然而,在本地环境下,至少对于大多数消费电子产品,无法加载 70B 参数模型。因此,我们将加载 Llama 3 8B 参数模型或已量化的 Llama 3 8B 参数模型。通过量化,我们节省了空间,并使模型能够在较少的 RAM 上运行。例如,Llama 3 8B 通常需要大约 14GB 的 GPU RAM,而量化后的 Llama 3 8B 则可以在 6GB 的 GPU RAM 上运行。因此,我们将根据参数加载完整模型或量化模型。
现在,我们可以初始化 Qdrant 客户端。
client = QdrantClient(path="qdrant/")
collection_name = "MyCollection"
qdrant = Qdrant(client, collection_name, hf)
同时,使用 FastAPI 并创建第一个模拟 GET 函数。
app = FastAPI()
@app.get("/")
async def root():
return {"message": "Hello World"}
这个函数将返回 JSON 格式的数据 {“message”:”Hello World”}
然而,为了使这个 API 能够正常工作,我们将创建两个功能,一个仅执行语义搜索,另一个则执行搜索并将前 10 个片段作为上下文生成答案,并引用它使用的文档。
@app.post("/search")
def search(Item:Item):
query = Item.query
search_result = qdrant.similarity_search(
query=query, k=10
)
i = 0
list_res = []
for res in search_result:
list_res.append({"id":i,"path":res.metadata.get("path"),"content":res.page_content})
return list_res
@app.post("/ask_localai")
async def ask_localai(Item:Item):
query = Item.query
search_result = qdrant.similarity_search(
query=query, k=10
)
i = 0
list_res = []
context = ""
mappings = {}
i = 0
for res in search_result:
context = context + str(i)+"\n"+res.page_content+"\n\n"
mappings[i] = res.metadata.get("path")
list_res.append({"id":i,"path":res.metadata.get("path"),"content":res.page_content})
i = i +1
rolemsg = {"role": "system",
"content": "Answer user's question using documents given in the context. In the context are documents that should contain an answer. Please always reference document id (in squere brackets, for example [0],[1]) of the document that was used to make a claim. Use as many citations and documents as it is necessary to answer question."}
messages = [
rolemsg,
{"role": "user", "content": "Documents:\n"+context+"\n\nQuestion: "+query},
]
if use_nvidia_api:
completion = client_ai.chat.completions.create(
model="meta/llama3-70b-instruct",
messages=messages,
temperature=0.5,
top_p=1,
max_tokens=1024,
stream=False
)
response = completion.choices[0].message.content
else:
input_ids = tokenizer.apply_chat_template(
messages,
add_generation_prompt=True,
return_tensors="pt"
).to(model.device)
terminators = [
tokenizer.eos_token_id,
tokenizer.convert_tokens_to_ids("<|eot_id|>")
]
outputs = model.generate(
input_ids,
max_new_tokens=256,
eos_token_id=terminators,
do_sample=True,
temperature=0.2,
top_p=0.9,
)
response = tokenizer.decode(outputs[0][input_ids.shape[-1]:])
return {"context":list_res,"answer":response}
两个功能都是 POST 方法,我们使用我们的 Item 类通过 JSON 体传递查询。第一个方法返回 10 个最相似的文档片段,包含路径,并分配文档 ID 从 0 到 9。因此,它仅执行使用点积作为相似度度量的简单语义搜索(这一点在 Qdrant 的索引过程中定义——记住那一行包含 distance=Distance.DOT)。
第二个功能叫做 ask_localai,稍微复杂一些。它包含来自第一个方法的搜索机制(因此可以更容易地通过代码理解语义搜索),但增加了生成部分。它为 Llama 3 生成了一个提示,包含一个系统提示消息,内容如下:
使用上下文中提供的文档回答用户的问题。在上下文中是应该包含答案的文档。请始终引用用于做出声明的文档 ID(用方括号表示,例如[0]、[1])。根据需要使用足够多的引用和文档来回答问题。
用户的消息包含一个文档列表,结构为 ID(0–9),后跟文档片段在下一行。为了保持 ID 与文档路径之间的映射,我们创建了一个名为 list_res 的列表,其中包含 ID、路径和内容。用户提示以“Question”一词结束,后面跟着用户的查询。
响应包含上下文和生成的答案。然而,答案是由 Llama 3 70B 模型(使用 NVIDIA NIM API)、本地 Llama 3 8B 或本地量化的 Llama 3 8B 生成的,这取决于传递的参数。
API 可以从一个包含以下代码行的单独文件中启动(假设我们的生成组件位于名为 api.py 的文件中,Uvicorn 的第一个参数映射到文件名):
import uvicorn
if __name__=="__main__":
uvicorn.run("api:app",host='0.0.0.0', port=8000, reload=False, workers=3)
简单的用户界面
我们的本地生成搜索引擎的最终组件是用户界面。我们将使用Streamlit构建一个简单的用户界面,包含一个输入框、一个搜索按钮、一个显示生成答案的区域,以及一个可以打开或下载的参考文档列表。
Streamlit 中用户界面的全部代码不到 45 行(准确来说是 44 行):
import re
import streamlit as st
import requests
import json
st.title('_:blue[Local GenAI Search]_ :sunglasses:')
question = st.text_input("Ask a question based on your local files", "")
if st.button("Ask a question"):
st.write("The current question is \"", question+"\"")
url = "http://127.0.0.1:8000/ask_localai"
payload = json.dumps({
"query": question
})
headers = {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
response = requests.request("POST", url, headers=headers, data=payload)
answer = json.loads(response.text)["answer"]
rege = re.compile("\[Document\ [0-9]+\]|\[[0-9]+\]")
m = rege.findall(answer)
num = []
for n in m:
num = num + [int(s) for s in re.findall(r'\b\d+\b', n)]
st.markdown(answer)
documents = json.loads(response.text)['context']
show_docs = []
for n in num:
for doc in documents:
if int(doc['id']) == n:
show_docs.append(doc)
a = 1244
for doc in show_docs:
with st.expander(str(doc['id'])+" - "+doc['path']):
st.write(doc['content'])
with open(doc['path'], 'rb') as f:
st.download_button("Downlaod file", f, file_name=doc['path'].split('/')[-1],key=a
)
a = a + 1
最终效果将是这样的:

用户界面中已回答问题的示例。截图由作者提供。
可用性
该项目的完整代码可以在 GitHub 上找到,网址为github.com/nikolamilosevic86/local-genAI-search。过去,我曾参与多个生成式搜索项目,并且也有一些相关的出版物。你可以查看www.thinkmind.org/library/INTERNET/INTERNET_2024/internet_2024_1_10_48001.html或arxiv.org/abs/2402.18589。
结论
本文展示了如何利用生成式 AI 结合 Qdrant 进行语义搜索。它通常是一个检索增强生成(RAG)管道,作用于本地文件,并提供引用本地文档声明的指令。整个代码大约有 300 行,我们甚至增加了复杂性,给用户提供了在 3 个不同的 Llama 3 模型之间选择的选项。对于这个使用案例,8B 和 70B 参数模型都能很好地工作。
我想解释一下我所做的步骤,希望这能对将来某些人有所帮助。不过,如果你想使用这个特定的工具,最简单的方式就是直接从GitHub获取,它是完全开源的!
如何从头开始用 Python 构建遗传算法
一步步讲解如何从头开始用 Python 构建遗传算法,并附带实际用例
·发表于 Towards Data Science ·15 分钟阅读·2024 年 8 月 30 日
--

图像由 DALL-E 生成
介绍
遗传算法的美妙之处在于它们直接受到自然的启发,特别是自然选择的过程:
自然选择是指通过选择性地繁殖基因型或遗传构成的变化,使得生物体能够适应其 环境的过程 [1]。
更广泛地说,自然选择是进化的主要机制之一。我对这个机制感到着迷,它在数百万年的时间里改变了自然界。更令人兴奋的是,我们可以通过遗传算法利用自然选择的力量。
遗传算法模拟了自然选择在非自然环境中的过程,通常类似于商业资源优化。然而,它的应用并不限于这些类型的用例。
在本文中,我将向读者展示如何用 Python 构建自己的遗传算法,并将其应用于实际用例。
为什么要使用遗传算法?
如何在 6 步内构建一个基于图的神经网络进行异常检测
学习构建一个能够处理异构图数据的图卷积网络,以进行链接预测
·发布于 Towards Data Science ·阅读时间 17 分钟·2024 年 2 月 12 日
--

图片来自 Pixabay
本文是关于如何构建一个强大的模型,用于处理包含不同类型实体的图数据(异构图数据)进行异常检测的详细技术深度分析。
你将学习的模型基于一篇题为《在二分节点和边属性图上聚焦交互的异常检测》的论文,该论文由亚洲科技公司 Grab 在 2023 年国际联合神经网络会议(IJCNN)上展示。
这个图卷积网络(GCN)模型可以处理异构图数据,意味着节点和边是不同类型的。这些图结构复杂,因为它们表示不同类型的实体或节点之间的关系。
可以处理异构图数据的图卷积网络(GCNs)是一个活跃的研究领域。该模型中的卷积操作已经适应了处理不同节点类型及其关系的挑战,特别是在异构图中。
相比之下,同质图包含相同类型的节点和边。这种图的结构相对简单…
如何构建一个 Graph RAG 应用

作者提供的图片
使用知识图谱和人工智能检索、筛选和总结医学期刊文章
·发表于Towards Data Science ·阅读时间:25 分钟·6 天前
--
该应用和笔记本的附带代码 在这里。
知识图谱(KGs)和大型语言模型(LLMs)是天作之合。我在之前的文章和另一篇文章中更详细地讨论了这两种技术的互补性,简而言之就是,“LLMs 的主要弱点之一是它们是黑箱模型,且在事实知识方面存在困难,而这正是知识图谱的优势。知识图谱本质上是事实的集合,并且它们是完全可解释的。”
本文全部内容是关于构建一个简单的 Graph RAG 应用。那么,什么是 RAG 呢?RAG,或者说检索增强生成(Retrieval-Augmented Generation),是通过检索相关信息来增强发送给 LLM 的提示,从而生成响应。Graph RAG 是将知识图谱作为检索部分的一部分的 RAG。如果你从未听说过 Graph RAG,或者想复习一下,我建议观看这个视频。
基本的想法是,与其将提示直接发送给没有经过您数据训练的 LLM,不如用相关信息补充您的提示,以便 LLM 能够准确回答您的问题。我常用的一个例子是将职位描述和我的简历复制到 ChatGPT 中,以撰写求职信。如果我给它我的简历和我申请的职位描述,那么 LLM 能够为我的提示“写一封求职信”提供一个更加相关的回应。由于知识图谱是用来存储知识的,它们是存储内部数据并通过额外的上下文补充 LLM 提示的完美方式,从而提高响应的准确性和上下文理解能力。
这项技术有许多应用,如客户服务机器人、药物 发现、生命科学中的自动化监管报告生成、HR 的人才招聘和管理、法律研究与写作以及财富顾问助手。由于其广泛的适用性以及提升 LLM 工具性能的潜力,Graph RAG(这是我在这里使用的术语)在受欢迎度上呈现爆炸式增长。以下是一个基于 Google 搜索的图表,展示了这一趋势。

Graph RAG 的搜索兴趣激增,甚至超越了“知识图谱”和“检索增强生成”这样的术语。请注意,Google Trends 衡量的是相对的搜索兴趣,而不是搜索的绝对数量。2024 年 7 月 Graph RAG 搜索量的激增与微软宣布其 GraphRAG 应用将在GitHub上线的那一周恰好重合。
然而,围绕 Graph RAG 的兴奋并不仅仅限于微软。三星在 2024 年 7 月收购了知识图谱公司 RDFox。该收购公告文章并未明确提到 Graph RAG,但在 2024 年 11 月发表的《福布斯》文章中,一位三星发言人表示:“我们计划开发知识图谱技术,作为个性化 AI 的核心技术之一,并与生成式 AI 有机结合,以支持面向用户的服务。”
2024 年 10 月,领先的图数据库公司 Ontotext 与语义网公司 Semantic Web 公司合并,成立了Graphwise。根据新闻稿,此次合并的目标是“使图谱 RAG 作为一个类别的演进更加普及”。
尽管围绕 Graph RAG 的一些热议可能源于人们对聊天机器人和生成式 AI 的广泛兴奋,但它反映了知识图谱在解决复杂的现实问题方面的真正演进。一个例子是 LinkedIn 应用了 Graph RAG来改善其客户服务技术支持。由于该工具能够检索相关数据(如先前解决的类似工单或问题)供 LLM 使用,回答更加准确,平均解决时间从 40 小时减少到了 15 小时。
本文将通过构建一个相当简单,但我认为具有代表性的例子,展示 Graph RAG 如何在实践中工作。最终结果是一个非技术用户可以交互的应用程序。像我上一篇文章一样,我将使用由 PubMed 的医学期刊文章组成的数据集。这个应用程序的目的是让医学领域的人可以用来进行文献综述。然而,同样的原则也可以应用于许多其他用例,这就是 Graph RAG 如此令人兴奋的原因。
该应用程序的结构,以及本文的内容如下:
第零步是准备数据。下面我将详细解释,但总体目标是将原始数据向量化,并单独将其转化为 RDF 图谱。只要在向量化之前我们保持与文章的 URI 关联,我们就可以在文章图谱和文章向量空间之间进行导航。然后,我们可以:
-
搜索文章:利用向量数据库的强大功能,根据搜索词初步搜索相关的文章。我将使用向量相似度来检索与搜索词最相似的文章。
-
细化术语: 探索医学主题词(MeSH)生物医学词汇表以选择用于过滤步骤 1 中文章的术语。这个控制词汇表包含医学术语、替代名称、更窄的概念以及许多其他属性和关系。
-
过滤与总结: 使用 MeSH 术语来过滤文章,以避免“上下文污染”。然后将剩余的文章与附加提示一起发送给 LLM,比如:“用要点总结。”
在我们开始之前,有一些关于此应用和教程的说明:
-
这个设置仅使用知识图谱来处理元数据。这之所以可行,是因为我数据集中的每篇文章已经被标注上了属于丰富控制词汇的术语。我使用图谱来处理结构和语义,使用向量数据库来进行基于相似度的检索,确保每项技术都用于它最擅长的领域。向量相似度可以告诉我们“食管癌”与“口腔癌”在语义上相似,但知识图谱可以告诉我们“食管癌”和“口腔癌”之间关系的详细信息。
-
我用于此应用的数据集来自 PubMed 的医学期刊文章(数据详情见下文)。我选择这个数据集是因为它是结构化的(表格形式),但也包含每篇文章的摘要文本,并且已经用与公认的控制词汇(MeSH)对齐的主题术语进行了标注。由于这些是医学文章,我将这个应用命名为“医学图谱 RAG”。但这种结构可以应用于任何领域,并不限于医学领域。
-
我希望本教程和应用程序展示的是,通过在检索步骤中加入知识图谱,你可以提高 RAG 应用的准确性和可解释性。我将展示知识图谱如何通过两种方式提高 RAG 应用的准确性:一种是为用户提供过滤上下文的方式,确保 LLM 只接收到最相关的信息;另一种是通过使用由领域专家维护和策划的、具有密切关系的特定领域控制词汇来进行过滤。
-
本教程和应用程序没有直接展示的两个重要方面是知识图谱(KGs)如何增强 RAG 应用程序:治理、访问控制和合规性;以及效率和可扩展性。在治理方面,KGs 不仅可以过滤相关内容以提高准确性——它们还可以执行数据治理政策。例如,如果用户没有权限访问某些内容,那么这些内容可以从他们的 RAG 流程中排除。在效率和可扩展性方面,KGs 可以帮助确保 RAG 应用程序不会被“搁置”。虽然创建一个令人印象深刻的单一 RAG 应用程序很容易(这正是本教程的目的),但许多公司却在大量孤立的 POC(概念验证)中挣扎,这些 POC 缺乏一个统一的框架、结构或平台。这意味着这些应用程序中的许多可能难以长期生存。由 KGs 驱动的元数据层可以打破数据孤岛,提供建立、扩展和有效维护 RAG 应用程序所需的基础。使用像 MeSH 这样丰富的受控词汇作为这些文章的元数据标签,是确保该图形 RAG 应用程序能够与其他系统集成并减少成为孤岛风险的方式。
步骤 0:准备数据
准备数据的代码在 这个 笔记本中。
如前所述,我再次决定使用这个来自 PubMed 库的 50,000 篇研究文章数据集(许可证CC0: 公共领域)。该数据集包含文章的标题、摘要以及一个元数据标签字段。这些标签来自医学主题词表(MeSH)受控词汇库。PubMed 文章实际上只是文章的元数据——每篇文章都有摘要,但我们没有完整的文本。数据已经是表格格式,并附有 MeSH 术语的标签。
我们可以直接对这个表格数据集进行向量化。我们本可以在向量化之前将其转换为图(RDF),但我在这个应用程序中没有这样做,而且我不确定对这种数据类型最终结果是否有帮助。向量化原始数据最重要的一点是,我们首先为每篇文章添加统一资源标识符(URI)。URI 是用于导航 RDF 数据的唯一标识符,它对于在向量和图中的实体之间来回切换是必需的。此外,我们将为 MeSH 术语在向量数据库中创建一个单独的集合。这将使用户能够在不事先了解受控词汇的情况下搜索相关术语。下面是我们为准备数据所做工作的示意图。

图片由作者提供
我们在向量数据库中有两个可以查询的集合:文章和术语。我们还有以 RDF 格式表示的数据作为图。由于 MeSH 有一个 API,我将直接查询该 API 以获取术语的替代名称和更窄的概念。
在 Weaviate 中向量化数据
首先导入所需的包并设置 Weaviate 客户端:
import weaviate
from weaviate.util import generate_uuid5
from weaviate.classes.init import Auth
import os
import json
import pandas as pd
client = weaviate.connect_to_weaviate_cloud(
cluster_url="XXX", # Replace with your Weaviate Cloud URL
auth_credentials=Auth.api_key("XXX"), # Replace with your Weaviate Cloud key
headers={'X-OpenAI-Api-key': "XXX"} # Replace with your OpenAI API key
)
读取 PubMed 期刊文章。我使用的是Databricks来运行这个笔记本,因此根据你运行的环境,可能需要进行更改。这里的目标只是将数据加载到 pandas DataFrame 中。
df = spark.sql("SELECT * FROM workspace.default.pub_med_multi_label_text_classification_dataset_processed").toPandas()
如果你是在本地运行,只需执行以下操作:
df = pd.read_csv("PubMed Multi Label Text Classification Dataset Processed.csv")
然后稍微清理一下数据:
import numpy as np
# Replace infinity values with NaN and then fill NaN values
df.replace([np.inf, -np.inf], np.nan, inplace=True)
df.fillna('', inplace=True)
# Convert columns to string type
df['Title'] = df['Title'].astype(str)
df['abstractText'] = df['abstractText'].astype(str)
df['meshMajor'] = df['meshMajor'].astype(str)
现在,我们需要为每篇文章创建一个 URI,并将其作为新列添加。这非常重要,因为 URI 是将文章的向量表示与文章的知识图谱表示相连接的方式。
import urllib.parse
from rdflib import Graph, RDF, RDFS, Namespace, URIRef, Literal
# Function to create a valid URI
def create_valid_uri(base_uri, text):
if pd.isna(text):
return None
# Encode text to be used in URI
sanitized_text = urllib.parse.quote(text.strip().replace(' ', '_').replace('"', '').replace('<', '').replace('>', '').replace("'", "_"))
return URIRef(f"{base_uri}/{sanitized_text}")
# Function to create a valid URI for Articles
def create_article_uri(title, base_namespace="http://example.org/article/"):
"""
Creates a URI for an article by replacing non-word characters with underscores and URL-encoding.
Args:
title (str): The title of the article.
base_namespace (str): The base namespace for the article URI.
Returns:
URIRef: The formatted article URI.
"""
if pd.isna(title):
return None
# Replace non-word characters with underscores
sanitized_title = re.sub(r'\W+', '_', title.strip())
# Condense multiple underscores into a single underscore
sanitized_title = re.sub(r'_+', '_', sanitized_title)
# URL-encode the term
encoded_title = quote(sanitized_title)
# Concatenate with base_namespace without adding underscores
uri = f"{base_namespace}{encoded_title}"
return URIRef(uri)
# Add a new column to the DataFrame for the article URIs
df['Article_URI'] = df['Title'].apply(lambda title: create_valid_uri("http://example.org/article", title))
我们还需要创建一个包含所有用于标记文章的 MeSH 术语的 DataFrame。稍后在搜索相似的 MeSH 术语时,这将非常有用。
# Function to clean and parse MeSH terms
def parse_mesh_terms(mesh_list):
if pd.isna(mesh_list):
return []
return [
term.strip().replace(' ', '_')
for term in mesh_list.strip("[]'").split(',')
]
# Function to create a valid URI for MeSH terms
def create_valid_uri(base_uri, text):
if pd.isna(text):
return None
sanitized_text = urllib.parse.quote(
text.strip()
.replace(' ', '_')
.replace('"', '')
.replace('<', '')
.replace('>', '')
.replace("'", "_")
)
return f"{base_uri}/{sanitized_text}"
# Extract and process all MeSH terms
all_mesh_terms = []
for mesh_list in df["meshMajor"]:
all_mesh_terms.extend(parse_mesh_terms(mesh_list))
# Deduplicate terms
unique_mesh_terms = list(set(all_mesh_terms))
# Create a DataFrame of MeSH terms and their URIs
mesh_df = pd.DataFrame({
"meshTerm": unique_mesh_terms,
"URI": [create_valid_uri("http://example.org/mesh", term) for term in unique_mesh_terms]
})
# Display the DataFrame
print(mesh_df)
对文章 DataFrame 进行向量化:
from weaviate.classes.config import Configure
#define the collection
articles = client.collections.create(
name = "Article",
vectorizer_config=Configure.Vectorizer.text2vec_openai(), # If set to "none" you must always provide vectors yourself. Could be any other "text2vec-*" also.
generative_config=Configure.Generative.openai(), # Ensure the `generative-openai` module is used for generative queries
)
#add ojects
articles = client.collections.get("Article")
with articles.batch.dynamic() as batch:
for index, row in df.iterrows():
batch.add_object({
"title": row["Title"],
"abstractText": row["abstractText"],
"Article_URI": row["Article_URI"],
"meshMajor": row["meshMajor"],
})
现在对 MeSH 术语进行向量化:
#define the collection
terms = client.collections.create(
name = "term",
vectorizer_config=Configure.Vectorizer.text2vec_openai(), # If set to "none" you must always provide vectors yourself. Could be any other "text2vec-*" also.
generative_config=Configure.Generative.openai(), # Ensure the `generative-openai` module is used for generative queries
)
#add ojects
terms = client.collections.get("term")
with terms.batch.dynamic() as batch:
for index, row in mesh_df.iterrows():
batch.add_object({
"meshTerm": row["meshTerm"],
"URI": row["URI"],
})
现在,你可以直接对向量化数据集运行语义搜索、相似性搜索和 RAG。我在这里不会详细讲解这些,但你可以查看我随附的笔记本中的代码来实现这些功能。
将数据转化为知识图谱
我只是使用了我们在上一篇文章中使用的相同代码来实现这一点。我们基本上是将数据中的每一行转化为我们知识图谱中的“文章”实体。然后,我们为这些文章提供标题、摘要和 MeSH 术语等属性。我们还将每个 MeSH 术语也转化为一个实体。此代码还为每篇文章添加了随机的发布日期(作为名为 date published 的属性)和介于 1 到 10 之间的随机数字(作为名为 access 的属性)。我们在这个演示中不会使用这些属性。下面是我们从数据中创建的图的可视化表示。

图片来源:作者
下面是如何遍历 DataFrame 并将其转化为 RDF 数据:
from rdflib import Graph, RDF, RDFS, Namespace, URIRef, Literal
from rdflib.namespace import SKOS, XSD
import pandas as pd
import urllib.parse
import random
from datetime import datetime, timedelta
import re
from urllib.parse import quote
# --- Initialization ---
g = Graph()
# Define namespaces
schema = Namespace('http://schema.org/')
ex = Namespace('http://example.org/')
prefixes = {
'schema': schema,
'ex': ex,
'skos': SKOS,
'xsd': XSD
}
for p, ns in prefixes.items():
g.bind(p, ns)
# Define classes and properties
Article = URIRef(ex.Article)
MeSHTerm = URIRef(ex.MeSHTerm)
g.add((Article, RDF.type, RDFS.Class))
g.add((MeSHTerm, RDF.type, RDFS.Class))
title = URIRef(schema.name)
abstract = URIRef(schema.description)
date_published = URIRef(schema.datePublished)
access = URIRef(ex.access)
g.add((title, RDF.type, RDF.Property))
g.add((abstract, RDF.type, RDF.Property))
g.add((date_published, RDF.type, RDF.Property))
g.add((access, RDF.type, RDF.Property))
# Function to clean and parse MeSH terms
def parse_mesh_terms(mesh_list):
if pd.isna(mesh_list):
return []
return [term.strip() for term in mesh_list.strip("[]'").split(',')]
# Enhanced convert_to_uri function
def convert_to_uri(term, base_namespace="http://example.org/mesh/"):
"""
Converts a MeSH term into a standardized URI by replacing spaces and special characters with underscores,
ensuring it starts and ends with a single underscore, and URL-encoding the term.
Args:
term (str): The MeSH term to convert.
base_namespace (str): The base namespace for the URI.
Returns:
URIRef: The formatted URI.
"""
if pd.isna(term):
return None # Handle NaN or None terms gracefully
# Step 1: Strip existing leading and trailing non-word characters (including underscores)
stripped_term = re.sub(r'^\W+|\W+$', '', term)
# Step 2: Replace non-word characters with underscores (one or more)
formatted_term = re.sub(r'\W+', '_', stripped_term)
# Step 3: Replace multiple consecutive underscores with a single underscore
formatted_term = re.sub(r'_+', '_', formatted_term)
# Step 4: URL-encode the term to handle any remaining special characters
encoded_term = quote(formatted_term)
# Step 5: Add single leading and trailing underscores
term_with_underscores = f"_{encoded_term}_"
# Step 6: Concatenate with base_namespace without adding an extra underscore
uri = f"{base_namespace}{term_with_underscores}"
return URIRef(uri)
# Function to generate a random date within the last 5 years
def generate_random_date():
start_date = datetime.now() - timedelta(days=5*365)
random_days = random.randint(0, 5*365)
return start_date + timedelta(days=random_days)
# Function to generate a random access value between 1 and 10
def generate_random_access():
return random.randint(1, 10)
# Function to create a valid URI for Articles
def create_article_uri(title, base_namespace="http://example.org/article"):
"""
Creates a URI for an article by replacing non-word characters with underscores and URL-encoding.
Args:
title (str): The title of the article.
base_namespace (str): The base namespace for the article URI.
Returns:
URIRef: The formatted article URI.
"""
if pd.isna(title):
return None
# Encode text to be used in URI
sanitized_text = urllib.parse.quote(title.strip().replace(' ', '_').replace('"', '').replace('<', '').replace('>', '').replace("'", "_"))
return URIRef(f"{base_namespace}/{sanitized_text}")
# Loop through each row in the DataFrame and create RDF triples
for index, row in df.iterrows():
article_uri = create_article_uri(row['Title'])
if article_uri is None:
continue
# Add Article instance
g.add((article_uri, RDF.type, Article))
g.add((article_uri, title, Literal(row['Title'], datatype=XSD.string)))
g.add((article_uri, abstract, Literal(row['abstractText'], datatype=XSD.string)))
# Add random datePublished and access
random_date = generate_random_date()
random_access = generate_random_access()
g.add((article_uri, date_published, Literal(random_date.date(), datatype=XSD.date)))
g.add((article_uri, access, Literal(random_access, datatype=XSD.integer)))
# Add MeSH Terms
mesh_terms = parse_mesh_terms(row['meshMajor'])
for term in mesh_terms:
term_uri = convert_to_uri(term, base_namespace="http://example.org/mesh/")
if term_uri is None:
continue
# Add MeSH Term instance
g.add((term_uri, RDF.type, MeSHTerm))
g.add((term_uri, RDFS.label, Literal(term.replace('_', ' '), datatype=XSD.string)))
# Link Article to MeSH Term
g.add((article_uri, schema.about, term_uri))
# Path to save the file
file_path = "/Workspace/PubMedGraph.ttl"
# Save the file
g.serialize(destination=file_path, format='turtle')
print(f"File saved at {file_path}")
好的,现在我们已经有了数据的向量化版本和图(RDF)版本。每个向量都有一个与之关联的 URI,URI 对应于知识图谱中的一个实体,因此我们可以在数据格式之间来回转换。
构建应用
我决定使用Streamlit来构建这个图 RAG 应用的界面。与上一篇博客文章类似,我保持了相同的用户流程。
-
搜索文章: 首先,用户使用搜索词进行文章搜索。这完全依赖于向量数据库。用户的搜索词会被发送到向量数据库,返回与该词在向量空间中最接近的十篇文章。
-
优化术语: 第二,用户决定使用哪些 MeSH 术语来过滤返回的结果。由于我们也对 MeSH 术语进行了向量化处理,因此用户可以输入自然语言提示,以获得最相关的 MeSH 术语。接着,我们允许用户展开这些术语,查看它们的替代名称和更窄的概念。用户可以选择任意数量的术语作为过滤条件。
-
过滤与总结: 第三,用户将选择的术语应用为过滤条件,作用于原始的十篇期刊文章。由于 PubMed 文章已经标注了 MeSH 术语,我们可以实现这一功能。最后,我们允许用户输入额外的提示,并将其与过滤后的期刊文章一起发送给 LLM。这是 RAG 应用中的生成步骤。
让我们逐步完成这些操作。你可以在我的 GitHub 上查看完整的应用和代码,但以下是其结构:
-- app.py (a python file that drives the app and calls other functions as needed)
-- query_functions (a folder containing python files with queries)
-- rdf_queries.py (python file with RDF queries)
-- weaviate_queries.py (python file containing weaviate queries)
-- PubMedGraph.ttl (the pubmed data in RDF format, stored as a ttl file)
搜索文章
首先,我们要做的是实现 Weaviate 的向量相似性搜索。由于我们的文章已被向量化,我们可以将搜索词发送到向量数据库,并返回相似的文章。

作者插图
在 app.py 中,搜索相关期刊文章的主要函数如下所示:
# --- TAB 1: Search Articles ---
with tab_search:
st.header("Search Articles (Vector Query)")
query_text = st.text_input("Enter your vector search term (e.g., Mouth Neoplasms):", key="vector_search")
if st.button("Search Articles", key="search_articles_btn"):
try:
client = initialize_weaviate_client()
article_results = query_weaviate_articles(client, query_text)
# Extract URIs here
article_uris = [
result["properties"].get("article_URI")
for result in article_results
if result["properties"].get("article_URI")
]
# Store article_uris in the session state
st.session_state.article_uris = article_uris
st.session_state.article_results = [
{
"Title": result["properties"].get("title", "N/A"),
"Abstract": (result["properties"].get("abstractText", "N/A")[:100] + "..."),
"Distance": result["distance"],
"MeSH Terms": ", ".join(
ast.literal_eval(result["properties"].get("meshMajor", "[]"))
if result["properties"].get("meshMajor") else []
),
}
for result in article_results
]
client.close()
except Exception as e:
st.error(f"Error during article search: {e}")
if st.session_state.article_results:
st.write("**Search Results for Articles:**")
st.table(st.session_state.article_results)
else:
st.write("No articles found yet.")
该功能使用存储在 weaviate_queries 中的查询来建立 Weaviate 客户端(initialize_weaviate_client),并搜索文章(query_weaviate_articles)。然后,我们将返回的文章以表格形式展示,包括它们的摘要、距离(与搜索词的接近程度)以及它们所标记的 MeSH 术语。
在 weaviate_queries.py 中查询 Weaviate 的函数如下所示:
# Function to query Weaviate for Articles
def query_weaviate_articles(client, query_text, limit=10):
# Perform vector search on Article collection
response = client.collections.get("Article").query.near_text(
query=query_text,
limit=limit,
return_metadata=MetadataQuery(distance=True)
)
# Parse response
results = []
for obj in response.objects:
results.append({
"uuid": obj.uuid,
"properties": obj.properties,
"distance": obj.metadata.distance,
})
return results
如你所见,我在这里将结果限制为十个,以简化操作,但你可以更改这个限制。这只是使用 Weaviate 中的向量相似性搜索来返回相关结果。
应用中的最终结果如下所示:

作者插图
作为演示,我将搜索“口腔癌治疗方法”这一术语。如你所见,返回了 10 篇文章,且大多相关。这展示了基于向量的检索的优点与局限性。
优势在于,我们可以用最少的努力,在数据上构建语义搜索功能。如上所示,我们所做的只是设置客户端并将数据发送到向量数据库。一旦数据被向量化,我们就可以进行语义搜索、相似性搜索,甚至是 RAG。我已经将部分内容放入了与本文配套的笔记本中,但在 Weaviate 的官方文档中有更多详细信息。
如我之前所提到的,基于向量的检索的弱点是它们是黑盒模型,并且在处理事实性知识时存在困难。在我们的示例中,看起来大多数文章都是关于某种癌症治疗或疗法的。有些文章专门讨论口腔癌,有些则讨论口腔癌的亚型,如牙龈癌(牙龈癌)和腭癌(腭癌)。但也有关于鼻咽癌(上咽部癌症)、下颌癌(下颌癌)和食管癌(食管癌)的文章。所有这些(上咽部、下颌或食管癌)都不被认为是口腔癌。可以理解,关于针对鼻咽肿瘤的特定癌症放疗的文章可能会与“口腔癌治疗”这一提示相关,但如果你仅仅是想寻找口腔癌的治疗方法,它可能并不相关。如果我们将这十篇文章直接输入到 LLM 的提示中并要求它“总结不同的治疗选项”,我们将得到错误的信息。
RAG 的目的是为大语言模型(LLM)提供一组非常具体的附加信息,以便更好地回答您的问题——如果这些信息不正确或不相关,可能会导致 LLM 给出误导性回答。这通常被称为“上下文污染”。上下文污染特别危险的一点是,回答不一定是事实不准确的(LLM 可能准确地总结了我们提供的治疗选项),也不一定是基于不准确的数据(假设期刊文章本身是准确的),只是使用了错误的数据来回答您的问题。在这个例子中,用户可能正在阅读如何治疗错误类型癌症的文章,这看起来是非常糟糕的。
精炼术语
知识图谱(KGs)可以通过精炼来自向量数据库的结果,帮助提高回答的准确性,并减少上下文污染的可能性。下一步是选择我们希望使用的 MeSH 术语来筛选文章。首先,我们在术语集合中对向量数据库进行另一次向量相似度搜索。这样做是因为用户可能不熟悉 MeSH 控制词汇。在我们上面的示例中,我搜索了“口腔癌的治疗方法”,但是“口腔癌”不是 MeSH 中的术语——他们使用的是“口腔肿瘤”(Mouth Neoplasms)。我们希望用户能够在没有事先了解这些术语的情况下开始探索 MeSH 术语——无论使用什么元数据来标记内容,这都是一种良好的实践。

图片来自作者
获取相关 MeSH 术语的功能与之前的 Weaviate 查询几乎相同。只需要将文章替换为术语:
# Function to query Weaviate for MeSH Terms
def query_weaviate_terms(client, query_text, limit=10):
# Perform vector search on MeshTerm collection
response = client.collections.get("term").query.near_text(
query=query_text,
limit=limit,
return_metadata=MetadataQuery(distance=True)
)
# Parse response
results = []
for obj in response.objects:
results.append({
"uuid": obj.uuid,
"properties": obj.properties,
"distance": obj.metadata.distance,
})
return results
这是在应用程序中的样子:

图片来自作者
如你所见,我搜索了“口腔癌”(mouth cancer),返回了最相似的术语。口腔癌没有被返回,因为这不是 MeSH 中的术语,但口腔肿瘤(Mouth Neoplasms)在列表中。
下一步是允许用户展开返回的术语,以查看替代名称和更具体的概念。这需要查询MeSH API。这是这个应用中最棘手的部分,原因有很多。最大的问题是 Streamlit 要求每个项目都有唯一的 ID,而 MeSH 术语可能会重复——如果返回的某个概念是另一个概念的子概念,那么当你展开父概念时,会出现重复的子概念。我认为我已经解决了大部分问题,应用应该能正常运行,但这个阶段可能还会有 bug。
我们依赖的函数可以在 rdf_queries.py 中找到。我们需要一个来获取术语的替代名称:
# Fetch alternative names and triples for a MeSH term
def get_concept_triples_for_term(term):
term = sanitize_term(term) # Sanitize input term
sparql = SPARQLWrapper("https://id.nlm.nih.gov/mesh/sparql")
query = f"""
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX meshv: <http://id.nlm.nih.gov/mesh/vocab#>
PREFIX mesh: <http://id.nlm.nih.gov/mesh/>
SELECT ?subject ?p ?pLabel ?o ?oLabel
FROM <http://id.nlm.nih.gov/mesh>
WHERE {{
?subject rdfs:label "{term}"@en .
?subject ?p ?o .
FILTER(CONTAINS(STR(?p), "concept"))
OPTIONAL {{ ?p rdfs:label ?pLabel . }}
OPTIONAL {{ ?o rdfs:label ?oLabel . }}
}}
"""
try:
sparql.setQuery(query)
sparql.setReturnFormat(JSON)
results = sparql.query().convert()
triples = set()
for result in results["results"]["bindings"]:
obj_label = result.get("oLabel", {}).get("value", "No label")
triples.add(sanitize_term(obj_label)) # Sanitize term before adding
# Add the sanitized term itself to ensure it's included
triples.add(sanitize_term(term))
return list(triples)
except Exception as e:
print(f"Error fetching concept triples for term '{term}': {e}")
return []
我们还需要一些功能来获取给定术语的更具体(子概念)概念。我有两个函数实现了这一点——一个获取术语的直接子概念,另一个递归函数返回给定深度的所有子概念。
# Fetch narrower concepts for a MeSH term
def get_narrower_concepts_for_term(term):
term = sanitize_term(term) # Sanitize input term
sparql = SPARQLWrapper("https://id.nlm.nih.gov/mesh/sparql")
query = f"""
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX meshv: <http://id.nlm.nih.gov/mesh/vocab#>
PREFIX mesh: <http://id.nlm.nih.gov/mesh/>
SELECT ?narrowerConcept ?narrowerConceptLabel
WHERE {{
?broaderConcept rdfs:label "{term}"@en .
?narrowerConcept meshv:broaderDescriptor ?broaderConcept .
?narrowerConcept rdfs:label ?narrowerConceptLabel .
}}
"""
try:
sparql.setQuery(query)
sparql.setReturnFormat(JSON)
results = sparql.query().convert()
concepts = set()
for result in results["results"]["bindings"]:
subject_label = result.get("narrowerConceptLabel", {}).get("value", "No label")
concepts.add(sanitize_term(subject_label)) # Sanitize term before adding
return list(concepts)
except Exception as e:
print(f"Error fetching narrower concepts for term '{term}': {e}")
return []
# Recursive function to fetch narrower concepts to a given depth
def get_all_narrower_concepts(term, depth=2, current_depth=1):
term = sanitize_term(term) # Sanitize input term
all_concepts = {}
try:
narrower_concepts = get_narrower_concepts_for_term(term)
all_concepts[sanitize_term(term)] = narrower_concepts
if current_depth < depth:
for concept in narrower_concepts:
child_concepts = get_all_narrower_concepts(concept, depth, current_depth + 1)
all_concepts.update(child_concepts)
except Exception as e:
print(f"Error fetching all narrower concepts for term '{term}': {e}")
return all_concepts
第 2 步的另一个重要部分是允许用户选择术语并将其添加到“已选择术语”列表中。这些术语将出现在屏幕左侧的侧边栏中。有很多方法可以改进这一步,例如:
-
没有办法清除所有内容,但你可以清除缓存或刷新浏览器(如果需要的话)。
-
目前没有办法“选择所有更具体的概念”,这将会很有帮助。
-
目前没有添加过滤规则的选项。现在我们只是简单地假设文章必须包含术语 A 或术语 B 或术语 C 等。最后的排名是基于文章被标记的术语数量。
这是在应用中看到的样子:

图片来自作者
我可以展开口腔肿瘤(Mouth Neoplasms)来查看替代名称,在这种情况下是“口腔癌”,以及所有更具体的概念。如你所见,大多数更具体的概念都有自己的子概念,你也可以展开这些子概念。为了这个演示的目的,我将选择口腔肿瘤的所有子概念。

图片来自作者
这一步很重要,不仅因为它允许用户过滤搜索结果,还因为它为用户提供了探索 MeSH 图谱并从中学习的方式。例如,在这里用户可以学到鼻咽肿瘤并不是口腔肿瘤的子集。
过滤和总结
现在你已经有了文章和过滤条件,可以应用过滤器并总结结果。在这一步中,我们将第一步返回的原始 10 篇文章与精炼后的 MeSH 术语列表结合在一起。我们允许用户在将提示发送给 LLM 之前添加额外的上下文。

图片来自作者
我们进行过滤的方法是,我们需要从原始搜索中获取这 10 篇文章的 URI。然后,我们可以查询知识图谱,看看这些文章中哪些被标记了相关的 MeSH 术语。此外,我们保存这些文章的摘要,以便在下一步使用。这也是我们可以基于访问控制或其他用户控制参数(如作者、文件类型、发布日期等)进行过滤的地方。我在这个应用中没有包括这些内容,但我确实添加了访问控制和发布日期的属性,以防我们以后想在这个用户界面中添加这些功能。
下面是 app.py 中的代码样式:
if st.button("Filter Articles"):
try:
# Check if we have URIs from tab 1
if "article_uris" in st.session_state and st.session_state.article_uris:
article_uris = st.session_state.article_uris
# Convert list of URIs into a string for the VALUES clause or FILTER
article_uris_string = ", ".join([f"<{str(uri)}>" for uri in article_uris])
SPARQL_QUERY = """
PREFIX schema: <http://schema.org/>
PREFIX ex: <http://example.org/>
SELECT ?article ?title ?abstract ?datePublished ?access ?meshTerm
WHERE {{
?article a ex:Article ;
schema:name ?title ;
schema:description ?abstract ;
schema:datePublished ?datePublished ;
ex:access ?access ;
schema:about ?meshTerm .
?meshTerm a ex:MeSHTerm .
FILTER (?article IN ({article_uris}))
}}
"""
# Insert the article URIs into the query
query = SPARQL_QUERY.format(article_uris=article_uris_string)
else:
st.write("No articles selected from Tab 1.")
st.stop()
# Query the RDF and save results in session state
top_articles = query_rdf(LOCAL_FILE_PATH, query, final_terms)
st.session_state.filtered_articles = top_articles
if top_articles:
# Combine abstracts from top articles and save in session state
def combine_abstracts(ranked_articles):
combined_text = " ".join(
[f"Title: {data['title']} Abstract: {data['abstract']}" for article_uri, data in
ranked_articles]
)
return combined_text
st.session_state.combined_text = combine_abstracts(top_articles)
else:
st.write("No articles found for the selected terms.")
except Exception as e:
st.error(f"Error filtering articles: {e}")
这使用了 rdf_queries.py 文件中的 query_rdf 函数。该函数如下所示:
# Function to query RDF using SPARQL
def query_rdf(local_file_path, query, mesh_terms, base_namespace="http://example.org/mesh/"):
if not mesh_terms:
raise ValueError("The list of MeSH terms is empty or invalid.")
print("SPARQL Query:", query)
# Create and parse the RDF graph
g = Graph()
g.parse(local_file_path, format="ttl")
article_data = {}
for term in mesh_terms:
# Convert the term to a valid URI
mesh_term_uri = convert_to_uri(term, base_namespace)
#print("Term:", term, "URI:", mesh_term_uri)
# Perform SPARQL query with initBindings
results = g.query(query, initBindings={'meshTerm': mesh_term_uri})
for row in results:
article_uri = row['article']
if article_uri not in article_data:
article_data[article_uri] = {
'title': row['title'],
'abstract': row['abstract'],
'datePublished': row['datePublished'],
'access': row['access'],
'meshTerms': set()
}
article_data[article_uri]['meshTerms'].add(str(row['meshTerm']))
#print("DEBUG article_data:", article_data)
# Rank articles by the number of matching MeSH terms
ranked_articles = sorted(
article_data.items(),
key=lambda item: len(item[1]['meshTerms']),
reverse=True
)
return ranked_articles[:10]
如你所见,这个功能还会将 MeSH 术语转换为 URI,这样我们就可以通过图形进行过滤。在将术语转换为 URI 时要小心,并确保它与其他功能一致。
下面是应用程序中的界面样式:

图片来自作者
如你所见,我们从前一步选择的两个 MeSH 术语已经列出。如果我点击“过滤文章”,它将使用我们在第 2 步中的过滤条件过滤原始的 10 篇文章。文章将返回其完整的摘要,以及标记的 MeSH 术语(见下图)。

图片来自作者
返回了 5 篇文章。其中两篇标记为“口腔肿瘤”,一篇标记为“牙龈肿瘤”,两篇标记为“腭肿瘤”。
现在我们已经有了一个精炼的文章列表,准备用来生成回应,我们可以进入最后一步。我们希望将这些文章发送给 LLM 生成回应,但我们也可以在提示中加入额外的上下文。我有一个默认的提示,内容是:“用要点总结这里的关键信息。使其对没有医学学位的人也能理解。”对于这个演示,我将调整提示以反映我们的原始搜索词:

结果如下:

结果看起来对我来说更好,主要是因为我知道我们总结的文章大概是关于口腔癌治疗的。这些数据集不包含实际的期刊文章,只有摘要。因此,这些结果只是摘要的摘要。这可能有一定价值,但如果我们在构建一个真正的应用程序,而不仅仅是一个演示的话,这是我们可以将文章的全文加入的步骤。或者,这也是用户/研究人员自己去阅读这些文章的地方,而不是完全依赖于 LLM 来进行总结。
结论
本教程演示了如何将向量数据库和知识图谱相结合,以显著提升 RAG 应用程序的效果。通过利用向量相似性进行初步搜索,结合结构化的知识图谱元数据进行筛选和组织,我们可以构建一个提供准确、可解释和领域特定结果的系统。MeSH(医学主题词表)的集成展示了领域专业知识在策划元数据中的重要性,它确保了检索步骤与应用程序的独特需求保持一致,同时与其他系统保持互操作性。这种方法不限于医学领域——其原理可以应用于任何结构化数据与文本信息共存的领域。
本教程强调了充分利用每种技术各自擅长领域的重要性。向量数据库擅长基于相似性的检索,而知识图谱则在提供上下文、结构和语义方面表现出色。此外,扩展 RAG 应用程序需要一个元数据层,以打破数据孤岛并实施治理策略。深思熟虑的设计,根植于特定领域的元数据和强有力的治理,是构建既准确又可扩展的 RAG 系统的关键路径。
如何构建一个本地开源的 LLM 聊天机器人,结合 RAG 技术
与谷歌的 Gemma-2b-it、LangChain 和 Streamlit 一起与 PDF 文档对话
·发布于 Towards Data Science ·阅读时间 12 分钟·2024 年 3 月 31 日
--

本文中我们将构建一个结合 RAG 的 LLM 聊天机器人,它使用洗衣机用户手册回答具体问题。图片由作者提供
介绍
大型语言模型(LLM)在将世界知识压缩到它们数十亿个参数中非常出色。
然而,LLM 有两个主要的局限性:它们只能获取到上次训练迭代时的最新知识。而且在被问及具体问题时,它们有时会“编造”知识(出现幻觉)。
使用 RAG 技术,我们可以让预训练的 LLM 在回答问题时,访问非常具体的信息作为额外的上下文。
在这篇文章中,我将通过理论与实践,演示如何利用 Hugging Face 的 transformers 库、LangChain 以及 Faiss 向量数据库,实现谷歌的 LLM Gemma,并增加 RAG 功能。
下图展示了 RAG 管道的概述,我们将一步一步地实现它。

RAG 管道实现概述。图片由作者提供
检索增强生成(RAG)
如何构建一个多目标回归模型进行宏观经济预测
逐步指南:预测多个经济指标。
·发表于Towards Data Science ·21 分钟阅读·2024 年 10 月 16 日
--

图片来源:Joachim Schnürle via Unsplash
我现在带来的是一个数据科学项目,应用于经济学,通过多目标回归预测几个宏观经济指标。我现在需要学习经济学吗?嗯,无论你是否从事数据科学工作,了解一些经济学知识总是个好主意,对吧?它影响着地球上每个个体的生活。
你是一个属于城市、州、国家,最终属于全球经济的社区的一部分。因此,经济学以某种方式影响着你的生活。至少了解一些基本概念,已经是个很好的主意。
对于任何公司而言,处理宏观经济指标通常是必要的。公司需要做出投资决策。理解这些指标并做出预测是非常有用的。公司可能会计划将业务扩展到其他城市或州。
通过宏观经济指标来理解GDP——国内生产总值——不仅是为了整个国家,也包括各个州,这会很有趣,对吧?这直接影响着公司如何做出日常决策。
如何构建一个带自查询检索器的 RAG 系统
RAG + 元数据过滤 = 极好的电影推荐 🍿
·发表于 Towards Data Science ·阅读时间 12 分钟·2024 年 4 月 25 日
--

电视观看者的图片。图片由 DALL·E 3 创建。
目录
-
检索数据
-
将文档上传到 Pinecone
-
创建自查询检索器
-
创建聊天模型
-
演示
链接
-
编辑:我更新了电影搜索,并将其重新命名为 Rosebud。我还将其改为免费使用! 在这里查看网站。
-
还可以查看我关于改进这个应用程序的新文章! 链接.
最近,我在浏览 Max,试图找到一部电影观看。通常这包括浏览呈现给我的各种列表,阅读几个描述,然后挑选出听起来略微有趣的东西。有时候这是一个不错的选择,有时候则不尽如人意。如果我知道自己想看的电影标题或演员的名字,我通常才会使用搜索功能。否则,搜索功能就不太有用了。
我突然有了一个想法:为什么我不能使用自然语言,根据电影的氛围或内容来查询电影,而不仅仅是基于标题或演员呢?例如,为什么我不能打开 Max、Netflix 或 Hulu,然后在搜索栏中输入以下查询之一:
-
帮我找一些英文的戏剧电影,时长不超过 2 小时,并且有宠物。
-
推荐僵尸电影,但要确保它们是有趣的。
-
我喜欢《瞬息全宇宙》。给我一部类似的电影,但更黑暗一些。
这种方法的美妙之处不仅仅在于提供了更自然的搜索电影方式。这种方法还保护了用户的隐私。这个系统完全不使用任何用户数据。所需的唯一内容就是一个查询。
所以我构建了电影搜索系统。这是一个基于 RAG(检索增强生成)的系统,它接收用户查询,将其嵌入并进行相似性搜索,找到类似的电影。但它不仅仅是一个普通的 RAG 系统。这个系统使用了所谓的自查询检索器。这使得在进行相似性搜索之前,能够通过电影的元数据对其进行筛选。所以,如果用户的查询是“推荐 1980 年后制作的恐怖电影,并且包含大量爆炸场面”,搜索会首先过滤掉所有不是“1980 年后制作的恐怖电影”的影片,然后再进行关于“包含大量爆炸场面”的相似性搜索。
在本文中,我将提供一个关于如何构建这个系统的高层次概述。如果你想深入了解,完整的代码可以通过上面的链接获取。
让我们深入探讨。
获取数据
这个项目的数据来自于The Movie Database (TMDB),并且得到了所有者的许可。他们的 API 简单易用,维护良好,并且限制不多。我从他们的 API 中提取了以下电影属性:
-
片名
-
片长(分钟)
-
语言
-
概述
-
上映年份
-
类型
-
描述电影的关键词
-
演员
-
导演
-
流媒体播放地点
-
购买地点
-
租赁地点
-
制作公司列表
以下是如何使用 TMDB API 和 Python 的响应库提取数据的片段:
def get_data(API_key, Movie_ID, max_retries=5):
"""
Function to pull details of your film of interest in JSON format.
parameters:
API_key (str): Your API key for TMBD
Movie_ID (str): TMDB id for film of interest
returns:
dict: JSON formatted dictionary containing all details of your film of
interest
"""
query = 'https://api.themoviedb.org/3/movie/' + Movie_ID + \
'?api_key='+API_key + '&append_to_response=keywords,' + \
'watch/providers,credits'
for i in range(max_retries):
response = requests.get(query)
if response.status_code == 429:
# If the response was a 429, wait and then try again
print(
f"Request limit reached. Waiting and retrying ({i+1}/{
max_retries})")
time.sleep(2 ** i) # Exponential backoff
else:
dict = response.json()
return dict
请注意,查询需要电影 ID(这些 ID 也是通过 TMDB 获得的),以及append_to_response,它允许我拉取多种类型的数据,例如关键词、观看提供者、信用信息(导演和演员)以及一些关于电影的基本信息。这里还包括一些基本的框架代码,以防我遇到速率限制,尽管我从未遇到过这种情况。
接下来,我们需要解析 JSON 响应。以下是一个片段,展示了如何解析参与电影制作的演员和导演信息:
credits = dict[‘credits’]
actor_list, director_list = [], []
# Parsing cast
cast = credits['cast']
NUM_ACTORS = 5
for member in cast[:NUM_ACTORS]:
actor_list.append(member["name"])
# Parsing crew
crew = credits['crew']
for member in crew:
if member['job'] == 'Director':
director_list.append(member["name"])
actor_str = ', '.join(list(set(actor_list)))
director_str = ', '.join(list(set(director_list)))
请注意,我将每部电影的演员数量限制在前五名。我还必须指定只关心导演,因为响应中还包含了其他类型的工作人员,比如剪辑师、服装设计师等。
所有这些数据随后被汇总成了 CSV 文件。上面列出的每个属性都变成了一个列,而每一行现在代表一部特定的电影。以下是从2008_movie_collection_data.csv文件中提取的一小段电影数据,这个文件是通过编程生成的。在这个项目中,我大约获取了 1920 年至 2023 年间的 100 部顶级电影。
用于演示的电影数据片段。作者提供。
信不信由你,我至今还没有看过《功夫熊猫》。或许在这个项目之后我得去看一看。
上传文档到 Pinecone
接下来,我需要将 csv 数据上传到 Pinecone。在 RAG 系统中,通常需要对数据进行分块,但这里每个“文档”(CSV 文件中的一行)都相对较短,因此不需要分块。我首先将每个 CSV 文件转换为 LangChain 文档,然后指定哪些字段应该是主要内容,哪些字段应该是 metadata。
下面是用于构建这些文档的代码片段:
# Loading in data from all csv files
loader = DirectoryLoader(
path="./data",
glob="*.csv",
loader_cls=CSVLoader,
show_progress=True)
docs = loader.load()
metadata_field_info = [
AttributeInfo(
name="Title", description="The title of the movie", type="string"),
AttributeInfo(name="Runtime (minutes)",
description="The runtime of the movie in minutes", type="integer"),
AttributeInfo(name="Language",
description="The language of the movie", type="string"),
...
]
for doc in docs:
# Parse the page_content string into a dictionary
page_content_dict = dict(line.split(": ", 1)
for line in doc.page_content.split("\n") if ": " in line)
doc.page_content = 'Overview: ' + page_content_dict.get(
'Overview') + '. Keywords: ' + page_content_dict.get('Keywords')
doc.metadata = {field.name: page_content_dict.get(
field.name) for field in metadata_field_info}
# Convert fields from string to list of strings
for field in fields_to_convert_list:
convert_to_list(doc, field)
# Convert fields from string to integers
for field in fields_to_convert_int:
convert_to_int(doc, field)
来自 LangChain 的DirectoryLoader负责将所有 csv 文件加载为文档。然后,我需要指定什么内容应该是page_content,什么内容应该是metadata。这是一个重要的决定。page_content将被嵌入并在检索阶段用于相似度搜索。metadata仅在相似度搜索之前用于过滤。 我决定将overview和keywords属性嵌入到page_content中,而其他属性则作为 metadata。进一步调整应考虑是否将title也包含在page_content中,但我发现这个配置对于大多数用户查询效果很好。
然后,文档需要上传到 Pinecone。这是一个相当直接的过程:
# Create empty index
PINECONE_KEY, PINECONE_INDEX_NAME = os.getenv(
'PINECONE_API_KEY'), os.getenv('PINECONE_INDEX_NAME')
pc = Pinecone(api_key=PINECONE_KEY)
# Uncomment if index is not created already
pc.create_index(
name=PINECONE_INDEX_NAME,
dimension=1536,
metric="cosine",
spec=PodSpec(
environment="gcp-starter"
)
)
# Target index and check status
pc_index = pc.Index(PINECONE_INDEX_NAME)
print(pc_index.describe_index_stats())
embeddings = OpenAIEmbeddings(model='text-embedding-ada-002')
vectorstore = PineconeVectorStore(
pc_index, embeddings
)
# Create record manager
namespace = f"pinecone/{PINECONE_INDEX_NAME}"
record_manager = SQLRecordManager(
namespace, db_url="sqlite:///record_manager_cache.sql"
)
record_manager.create_schema()
# Upload documents to pinecome
index(docs, record_manager, vectorstore,
cleanup="full", source_id_key="Website")
在这里,我只想强调几点:
-
使用
SQLRecordManager可以确保如果这段代码运行多次,重复的文档不会被上传到 Pinecone。如果文档被修改,只有该文档会在向量存储中被更新。 -
我们使用的是 OpenAI 的经典
text-embedding-ada-002嵌入模型。
创建自查询检索器
自查询检索器将允许我们通过之前定义的 metadata 过滤在 RAG 过程中检索的电影。这将大大提高我们电影推荐系统的实用性。
选择向量存储时,一个重要的考虑因素是确保它支持通过 metadata 进行过滤,因为并非所有向量存储都支持。这是 LangChain 支持自查询检索的数据库列表。另一个重要的考虑因素是每个向量存储允许哪些类型的比较器。比较器是我们通过 metadata 进行过滤的方法。例如,我们可以使用eq比较器来确保我们的电影属于科幻类型:eq('Genre', 'Science Fiction')。并非所有向量存储都允许所有比较器。作为例子,请查看Weaviate 中允许的比较器,以及它们与Pinecone 中的比较器的差异。我们需要告诉模型允许哪些比较器,以防止它意外地写出不允许的查询。
除了告诉模型存在哪些比较器,我们还可以给模型提供一些用户查询的示例和相应的筛选条件。这被称为少量样本学习,它对于帮助指导你的模型非常宝贵。
为了看看这有什么帮助,看看以下两个用户查询:
-
“推荐一些由约尔戈斯·兰西莫斯导演的电影。”
-
“类似于约尔戈斯·兰西莫斯电影的电影。”
对于我的元数据筛选模型来说,即使我希望它们被不同对待,也很容易为每个示例编写相同的筛选查询。第一个应该只返回由兰西莫斯导演的电影,而第二个则应该返回与兰西莫斯电影有相似氛围的电影。为了确保这种行为,我向模型提供了我期望的行为示例。语言模型的美妙之处在于,它们可以利用其“推理”能力和世界知识,从这些少量的示例中推广到其他用户查询。
document_content_description = "Brief overview of a movie, along with keywords"
# Define allowed comparators list
allowed_comparators = [
"$eq", # Equal to (number, string, boolean)
"$ne", # Not equal to (number, string, boolean)
"$gt", # Greater than (number)
"$gte", # Greater than or equal to (number)
"$lt", # Less than (number)
"$lte", # Less than or equal to (number)
"$in", # In array (string or number)
"$nin", # Not in array (string or number)
"$exists", # Has the specified metadata field (boolean)
]
examples = [
(
"Recommend some films by Yorgos Lanthimos.",
{
"query": "Yorgos Lanthimos",
"filter": 'in("Directors", ["Yorgos Lanthimos]")',
},
),
(
"Films similar to Yorgos Lanthmios movies.",
{
"query": "Dark comedy, absurd, Greek Weird Wave",
"filter": 'NO_FILTER',
},
),
...
]
metadata_field_info = [
AttributeInfo(
name="Title", description="The title of the movie", type="string"),
AttributeInfo(name="Runtime (minutes)",
description="The runtime of the movie in minutes", type="integer"),
AttributeInfo(name="Language",
description="The language of the movie", type="string"),
...
]
constructor_prompt = get_query_constructor_prompt(
document_content_description,
metadata_field_info,
allowed_comparators=allowed_comparators,
examples=examples,
)
output_parser = StructuredQueryOutputParser.from_components()
query_constructor = constructor_prompt | query_model | output_parser
retriever = SelfQueryRetriever(
query_constructor=query_constructor,
vectorstore=vectorstore,
structured_query_translator=PineconeTranslator(),
search_kwargs={'k': 10}
)
除了示例,模型还必须了解每个元数据字段的描述。这有助于它理解可能的元数据筛选。
最后,我们构建我们的链条。这里的query_model是使用 OpenAI API 的 GPT-4 Turbo 实例。我建议使用 GPT-4 而不是 3.5 来编写这些元数据筛选查询,因为这是一个关键步骤,而 3.5 在这一点上更容易出错。search_kwargs={'k':10}告诉检索器根据用户查询拉取最相似的 10 部电影。
创建聊天模型
最后,在构建自查询检索器之后,我们可以在其基础上构建标准的 RAG 模型。我们从定义聊天模型开始。我称之为总结模型,因为它接受一个上下文(检索到的电影 + 系统消息),并以每个推荐的摘要进行回应。如果你想降低成本,可以使用 GPT-3.5 Turbo,或者如果你想要最好的结果,可以使用 GPT-4 Turbo。
在系统消息中,我告诉机器人它的目标是什么,并提供一系列建议和限制,其中最重要的是不要推荐任何没有通过自查询检索器提供的电影。在测试中,当用户查询没有从数据库中检索到电影时,我遇到了问题。例如,查询:“推荐一些由马特·达蒙主演、韦斯·安德森导演的恐怖片,且是在 1980 年之前制作的”会导致自查询检索器无法检索到任何电影(因为虽然听起来很棒,但这样的电影并不存在)。在没有电影数据的上下文中,模型会使用它自己的(错误的)记忆来尝试推荐一些电影。这种行为不好。我不希望 Netflix 推荐系统讨论数据库中不存在的电影。下面的系统消息成功阻止了这种行为。我确实注意到,GPT-4 比 GPT-3.5 更善于遵循指令,这是预期之中的。
chat_model = ChatOpenAI(
model=SUMMARY_MODEL_NAME,
temperature=0,
streaming=True,
)
prompt = ChatPromptTemplate.from_messages(
[
(
'system',
"""
Your goal is to recommend films to users based on their
query and the retrieved context. If a retrieved film doesn't seem
relevant, omit it from your response. If your context is empty
or none of the retrieved films are relevant, do not recommend films
, but instead tell the user you couldn't find any films
that match their query. Aim for three to five film recommendations,
as long as the films are relevant. You cannot recommend more than
five films. Your recommendation should be relevant, original, and
at least two to three sentences long.
YOU CANNOT RECOMMEND A FILM IF IT DOES NOT APPEAR IN YOUR
CONTEXT.
# TEMPLATE FOR OUTPUT
- **Title of Film**:
- Runtime:
- Release Year:
- Streaming:
- (Your reasoning for recommending this film)
Question: {question}
Context: {context}
"""
),
]
)
def format_docs(docs):
return "\n\n".join(f"{doc.page_content}\n\nMetadata: {doc.metadata}" for doc in docs)
# Create a chatbot Question & Answer chain from the retriever
rag_chain_from_docs = (
RunnablePassthrough.assign(
context=(lambda x: format_docs(x["context"])))
| prompt
| chat_model
| StrOutputParser()
)
rag_chain_with_source = RunnableParallel(
{"context": retriever, "question": RunnablePassthrough()}
).assign(answer=rag_chain_from_docs)
format_docs 用于格式化呈现给模型的信息,以便它易于理解和解析。我们将 page_content(概述和关键词)以及 metadata(所有其他电影属性)同时呈现给模型;任何它可能需要的信息,以便更好地向用户推荐电影。
rag_chain_from_docs 是一个链条,它接受检索到的文档,通过 format_docs 格式化,然后将格式化后的文档输入模型上下文中,模型基于这些内容回答问题。最后,我们创建了 rag_chain_with_source,这是一个 RunnableParallel,顾名思义,它同时执行两个操作:自查询检索器去检索相似的文档,而查询则通过 RunnablePassthrough() 直接传递给模型。然后,来自并行组件的结果被组合,rag_chain_from_docs 用来生成答案。这里的 source 指的是检索器,它访问所有的‘源’文档。
因为我希望答案是逐步流式传输的(例如,像 ChatGPT 一样逐块展示给用户),我们使用以下代码:
for chunk in rag_chain_with_source.stream(query):
for key in chunk:
if key == 'answer':
yield chunk[key]
演示
现在是有趣的部分:与模型互动。如前所述,Streamlit 被用来创建前端并托管应用程序。我在这里不讨论 UI 的代码;有关实现的详细信息,请参见原始代码。它相当直接,而且在Streamlit 网站上有很多其他示例。

电影搜索界面。作者提供。
有几个建议可以使用,但让我们尝试我们自己的查询:

示例查询和模型响应。作者提供。
在后台,自查询检索器确保过滤掉任何非法语的电影。然后,它执行了一个“成长故事”的相似性搜索,结果是上下文中有十部电影。最后,摘要机器人选择了五部电影进行推荐。请注意推荐的电影范围:一些电影的上映日期早至 1959 年,最晚的为 2012 年。为了方便起见,我确保机器人包括电影的片长、上映年份、流媒体提供商,以及由机器人精心制作的简短推荐。
(附注:如果你还没有看过 400 击,立刻停下你正在做的事情,去 立即观看。)
在大型语言模型中,通常被视为负面的特性,比如其响应的非确定性,现在变成了积极的。你如果向模型问同一个问题两次,可能会得到略有不同的推荐。
需要注意当前实现的一些限制:
-
不支持保存推荐。用户可能希望重新访问以前的推荐。
-
手动更新来自电影数据库的原始数据。将其自动化并每周更新一次是一个不错的主意。
-
自查询检索中的元数据过滤不准确。例如,查询“Ben Affleck films”可能会出现问题。这可能指的是由本·阿弗莱克主演的电影,或者是由本·阿弗莱克执导的电影。这是一个需要澄清查询的例子。
对这个项目的可能改进之一是在检索后进行文档重新排序。还可以有一个聊天模型,可以进行多轮对话,而不仅仅是一个问答机器人。还可以创建一个推荐代理,如果查询不明确,可以向用户提出澄清问题。
玩得开心,搜索电影吧!
如何构建一个 AI 简历优化工具
逐步指南和示例 Python 代码
·发布于 Towards Data Science ·6 分钟阅读·6 天前
--
在之前的一篇博客文章中,我分享了你可以在这个周末构建的 5 个 AI 项目,其中第一个项目的创意是简历优化工具。自那以后,许多人请求更多关于如何实现这个项目的指导。在本文中,我将通过一个使用 Python 和 OpenAI API 的示例实现来讲解这个过程。

图片来自 Canva。
根据不同的职位描述调整你的简历是找工作的一个有效但繁琐的步骤。即使你已经为某个特定职位定制了简历,企业可能对类似职位名称有不同的期望。
例如,以下是 Netflix 和 Google 的两个相似数据科学家职位,它们的职位描述略有不同。幸运的是,借助今天的 AI 工具,我们可以构建一个应用程序来简化这个过程。

相似数据科学家职位的对比。图像由作者提供。
我不能使用 ChatGPT 吗?
如何构建一个语义搜索引擎来搜索表情符号
寻找你想要的情感 🔍🤔😀🚀
·发表于Towards Data Science ·阅读时间 15 分钟·2024 年 1 月 10 日
--

使用自定义表情符号搜索引擎进行“halloween”的语义搜索。
如果你曾经使用过 Google Docs 或 Slack,你可能注意到,当你输入一个“:”紧接着另一个字符时,会弹出一个表情符号列表:

自从我发现这个功能以来,我就一直在大量使用它。我在我的消息、博客文章和其他书面作品中添加了比我曾经想象的更多的表情符号。事实上,我已经习惯了这种添加表情符号的方式,以至于我安装了Rocket——一款免费应用程序,它将相同的表情符号搜索功能带入到电脑上的所有文本框和文本编辑器中。这真是一个改变游戏规则的工具。
但是,随着我使用这些表情符号搜索引擎的次数越来越多,我注意到一个令人沮丧的限制:所有搜索都基于你查询中的精确文本,以及表情符号的名称和描述。实际上,你需要非常精确地搜索某个词,才能显示出任何结果。
这里有一个例子:如果我们搜索“audio”,一个结果都不会显示出来:

这并不是因为表情符号集合中缺少音频类的表情符号。如果我们输入“music”或“speaker”,我们会得到一长串结果。实际上,这与“audio”这个特定文本串的名称或描述中并没有出现任何表情符号相关的事实有关。

这个相对较小的不便让我感到如此困扰,以至于我决定构建这个:
“这个”指的是一个开源的语义表情符号搜索引擎,具有基于 UI 和 CLI 两个版本。Python CLI 库可以在这里找到,基于 UI 的版本可以在这里找到。你还可以在线尝试一个托管的(也是免费的)UI 表情符号搜索引擎,点击这里。

语义表情符号搜索引擎的命令行版本
构建这个过程并不像我最初希望的那样简单或直接。这花了很多实验,很多我认为相当聪明的想法最终都失败了。但最终,我成功地创建了一个相当好用的表情符号搜索引擎。
下面是我如何构建它、哪些方面有效、哪些方面无效,以及在过程中学到的经验教训。
-
什么是表情符号
-
数据
-
表情符号与图片和文本
-
弥合模态差距
-
使用表情符号搜索引擎
什么是表情符号
在构建一个表情符号语义搜索引擎之前,值得简要解释一下什么是表情符号。emoji 这个词源自日语汉字絵(eh),意思是图片,以及文字(mōji),意思是字母或字符。从词源学的角度来看,emoji 本质上是一种象形文字,虽然它与英语单词emotion(情感)有关,但它并不是“情感图标”——这才是表情符号。
与字母数字字符、非洲点击音、数学符号和几何符号、装饰符号以及计算机控制序列一起,表情符号可以作为 Unicode 字符表示,从而使它们可以被计算机读取。然而,与字母数字字符和其他符号不同,表情符号由Unicode 联盟进行维护。该联盟会征集新表情符号的提案,并定期选择哪些表情符号将被加入到标准中。
截至写作时,2023 年 11 月,已经有超过 3600 个被认可的表情符号,它们象征着各种各样的思想和情感。有些表情符号由单一的 Unicode 字符或编码点表示。例如,“露齿而笑的脸”表情符号,😀,在 Unicode 中表示为 U+1F600。
其他的则通过代码点序列表示。这些序列将单个代码点表情符号与零宽度连接符 unicode 字符结合,称为 ZWJ 序列,允许将概念组合在一起,就像汉字部首可以组合成一个讲述故事的字符一样。例如,表情符号👨👩👧就是通过 ZWJ 代码点 U+200D 连接的男人👨(U+1F468)、女人👩(U+1F469)和女孩👧(U+1F467)表情符号:
👨👩👧 = U+1F468 U+200D U+1F469 U+200D U+1F467
根据 Unicode 联盟的统计,全球 92%的在线用户在通信中使用表情符号,2021 年使用频率最高的十个表情符号是:😂 ❤️ 🤣 👍 😭 🙏 😘 🥰 😍 😊。
从数据开始
由于表情符号在某种程度上是象形文字,我希望在搜索过程中同时利用文本信息和视觉信息。我的初步假设是,对于许多表情符号来说,名称——用于调出表情符号的文本字符串——只传达了其含义的一部分。这可能是由于多种原因,包括自然语言的局限性,以及文化和视觉相似性所赋予的附加含义。为了真正展现表情符号的完整本质,我需要利用视觉信息。
我找到了一份 2021 年的Kaggle Emojis 数据集,其中包含 1816 个表情符号的数据,包括表情符号的表示、与之相关的文本、unicode 编码(或编码)和一个base64编码的图像。以下是该数据集的前几行,加载为 pandas DataFrame 后呈现的样子:

由于表情符号的渲染方式取决于计算机、网站或应用程序,因此存在名为Apple、Google、Facebook等的单独列。我从 base64 解码这些图像并将其转换为Pillow图像。以下是 Kaggle 数据集中的第一张图像(露齿笑脸):
import base64
from io import BytesIO
from PIL import Image
## decode and convert first row Apple image
im_str = df.Apple[0].replace('data:image/png;base64,', '')
im = Image.open(BytesIO(base64.b64decode(im_str)))
im

然而,在转换后很明显,这些图像的分辨率非常低。例如,这张图像仅为 72x72 像素。为了提高将要传递给下游模型的图像质量,并且为了改善最终基于 UI 的应用程序中的体验,我将所有这些低分辨率图像传递到Real-ESRGAN,将分辨率提高了 10 倍。
结果图像看起来是这样的:

由于并非所有表情符号在 pandas DataFrame 中的所有图像列都有图像,因此我为每一行使用了第一个有效的 base64 编码。
表情符号与图像和文本
在深入探讨之前,我想强调表情符号的一个关键元素,它使得表情符号如此特别,并且值得拥有自己的语义搜索引擎:从某种意义上说,它们既是图像也是文本。从人类的角度来看,我们可以将每个表情符号表示为一个 Unicode 字符,与文本字符处于同一层面,同时也可以将其表示为一个独立的图像,这两者在前一节中都已经看到。换句话说,如果我们用一只眼睛眯起,我们可以把象形符号看作是一张图片,而如果我们用另一只眼睛眯起,我们可以将同一个象形符号看作是文本。
然而,计算机并不以其眯眼能力而闻名。尽管计算机可以将一个 Unicode 码点显示为表情符号,但机器学习模型可能没有很好的方法将该表情符号解释为文本或图像。
每当我在进行连接图像和文本的语义搜索应用时,我都会从一个被称为 对比语言图像预训练(CLIP)的一组模型开始。这些模型通过对图像-文本对进行训练,生成图像及其标题的相似向量表示或 嵌入,当图像与其他文本字符串配对时,生成不相似的向量。有多种 CLIP 风格的模型,包括 OpenCLIP 和 MetaCLIP,但为了简便起见,我们将重点关注 OpenAI 的原始 CLIP 模型。没有任何模型是完美的,在根本层面上,也没有正确的方式来比较图像和文本,但 CLIP 无疑提供了一个很好的起点。
将表情符号解释为文本
在高层次上,语言模型通过将输入文本转换为一个有序的标记序列,然后编码这些标记和位置信息为一个密集的数值向量。每个语言模型都有自己的词汇表,用于将文本字符串分解为标记,涵盖了从单个字母到完整单词的范围。一些标记对于人类来说很容易理解,而另一些则不容易理解,在 CLIP 的情况下,词汇表包含了 49,408 个条目。
让我们看一个明确的例子。假设已安装 CLIP 库,我们可以使用以下方式对文本字符串“a dog”进行分词处理:
import clip
text_tokens = clip.tokenize("a dog")
print(text_tokens)
tensor([[49406, 320, 1929, 49407, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0]], dtype=torch.int32)
输出张量包含四个非零条目:49406、320、1929 和 49407。为了理解这些条目的含义,我们可以将这些值映射回 CLIP 词汇字典中的键。第一个数字 49406 对应于键“<|startoftext|>”,最后一个数字 49407 对应于键“<|endoftext|>”。这些是表示要编码的文本字符串开始和结束的特殊标记。第二个数字 320 映射回“a”,表示字符“a”后跟一个新单词。最后,1929 是键“dog”的值。
然而,如果我们尝试标记化包含表情符号的字符串,我们会很快遇到问题:表情符号不像其他字符那样被标记化。让我们从狗表情符号🐶开始:
clip.tokenize("🐶")
## [49406, 10631, 49407, 0, ...]
对与 10,631 相关的键进行反向查找时,我们得到标记“ðŁIJ¶”。但是如果我们将这个字符串传入分词器,我们会得到一组完全不同的标记 ID:
clip.tokenize("ðŁIJ¶")
## [49406, 127, 108, 40419, 72, 329, 126, 370, 49407, 0, ...]
一个更为有趣的案例是关于国旗表情符号的。如果我们以喀麦隆国旗的表情符号为例,我们得到:
clip.tokenize("🇨🇲")
## [49406, 8989, 366, 49407, 0, ...]
这里的两个非开始/结束标记对应于“ðŁĩ¨ðŁĩ”和“²”。如果我们将第一个标记重新输入分词器,我们会得到一组完全不同的标记 ID,但第二个标记会映射回其自身。
当我们开始比较文本字符串的嵌入与通过这个分词器解析为文本字符串的表情符号嵌入时,情况变得更加复杂。毕竟,我们希望根据文本查询找到最相关的表情符号。我们可以使用余弦距离来衡量两个向量的相似性或差异性——间接地,衡量生成这些嵌入向量的输入之间的差异。0 的距离意味着两个向量完全对齐,而 1 的距离则意味着两个向量正交。如果我们想把表情符号当作文本处理,我们希望表情符号的名称与嵌入空间中的标记化表情符号相对接近,但这并不总是如此!
以下工具将比较一个表情符号和一系列文本提示:
!pip install fiftyone
from scipy.spatial.distance import cosine
import fiftyone.zoo as foz
model = foz.load_zoo_model("clip-vit-base32-torch")
def compare_emoji_to_texts(emoji, texts):
emoji_embedding = model.embed_prompt(emoji)
text_embeddings = model.embed_prompts(texts)
for text, text_embedding in zip(texts, text_embeddings):
print(f"Dist b/w {emoji} and {text}: {cosine(emoji_embedding, text_embedding):.4f}")
这是一个示例,根据 CLIP 的结果,"生日"表情符号🎂的编码更接近“男人”而非“生日”,更接近“狗”而非“生日礼物”,更接近“车”而非“蜡烛”、“约会”或“假期”:
texts=["birthday", "birthday present", "cake", "candle", "car", "date", "dog", "holiday", "man"]
compare_emoji_to_texts("🎂", texts)
Dist b/w 🎂 and birthday: 0.1205
Dist b/w 🎂 and birthday present: 0.1385
Dist b/w 🎂 and cake: 0.1238
Dist b/w 🎂 and candle: 0.2030
Dist b/w 🎂 and car: 0.1610
Dist b/w 🎂 and date: 0.1921
Dist b/w 🎂 and dog: 0.1344
Dist b/w 🎂 and holiday: 0.1844
Dist b/w 🎂 and man: 0.0849
有时,表情符号及其名称(以及类似的概念)在嵌入空间中彼此接近,但有时它们显然并非如此。
我们也可以反过来,检索出与输入文本提示的嵌入最匹配的表情符号。例如,对于输入“爱”,我们得到以下结果:

当然,我们可以做得比这更好!
将表情符号解释为图像
我们使用 Real-ESRGAN 生成的高分辨率表情符号图像提供了一条替代的表情符号搜索路径:将表情符号视为图像。我们可以使用 CLIP 的视觉编码器将这些图像嵌入到相同的向量空间中,然后通过输入文本提示查询这些图像嵌入。
对于跨模态检索(或通过文本搜索图像)等应用,CLIP 通常在将图像嵌入与包含用户查询的文本提示“一个关于
然而,当我使用这个模板时,结果并不理想。例如,以下是查询“狗的照片”时的前 25 个结果:

因为表情符号并不完全是照片,所以我决定深入挖掘这一点,并尝试几种模板或包装策略。为了全面测试,我测试了五种文本提示格式:
-
<emoji_name>
-
一张<emoji_name>表情符号的照片
-
一个<emoji_name>的表情符号
-
一张<emoji_name>表情符号的照片
-
一个<emoji_name>表情符号
我用这些方法为所有 1816 个表情符号生成了嵌入,并计算了这些向量与相应图像嵌入向量的 CLIPScore(余弦相似度乘以 100)。
这里是综合结果:
Method Min Mean Max
A 16.96 29.04 37.49
B 15.85 29.47 38.43
C 18.94 33.25 44.60
D 19.47 32.59 42.57
E 18.95 31.83 43.35
根据这些统计数据,我认为“An emoji of”描述最适合,因为它们具有最高的均值和最大值。但当我尝试使用这个时,结果依然不理想。它们似乎偏好面部表情(例如😄😢🙃👦👧),忽视了其他表情符号,如符号、动物和旗帜。在语义表情符号搜索时,我发现直接输入原始文本通常效果最好。换句话说,“dog”这个词的 CLIP 嵌入效果比“A photo of a dog”或“An emoji of a dog”要好。
这里有几点收获:
-
图像-文本的“对齐”整体上对语义搜索不一定重要
-
表情符号的图像在某种程度上编码了它们不是照片的事实
-
“emoji”这个词使 CLIP 偏向面部
弥合模态差距
到此为止,我已经得出结论,将表情符号仅视为图像或仅视为文本会遗漏很多丰富的信息。为了构建一个强大的语义表情符号搜索引擎,我希望结合文本和图像信息,弥合这两种模态之间的差距。
我尝试使用 Adept 的多模态模型Fuyu-8b生成表情符号图像的描述,但这些描述过于详细;我尝试使用其他类似 CLIP 的模型,如MetaCLIP,但表现与 CLIP 相同;我甚至尝试使用GPT-4V生成表情符号图像的标题,但由于模型的查询频率限制为每天 100 次,我被 OpenAI 中断了。
最终,我能够将表情符号的 Unicode 字符传递到基础的 GPT-4 API 中,提示为:
QUERY_TEXT = """
Your task is to write a brief description of the emoji {emoji}, in the format 'A photo of a ...'. For example, 'A photo of a dog'. Do not include the emoji name or unicode in your description. Do not include the skin tone of the emoji. Do not include the word yellow in your response. You may include the word 'emoji' in your description, but it is not necessary. Your description should be a single phrase, of no more than 10 words.
"""
在对这些标题进行后处理后,我删除了“A photo of”前缀,并将这些描述用于语义搜索管道中。
表情符号搜索引擎的工作原理如下,输入query:
-
生成 100 个候选表情符号(从 1816 个中选择),使用图像相似度搜索将图像嵌入与查询嵌入进行比较。保存此排序,clip_image_ordering。
-
按照表情符号名称的 CLIP 嵌入与查询嵌入的相似度对这些候选表情符号进行排序(clip_name_ordering)。
-
使用 cross-encoder,根据表情符号名称的相似性(cross_encoder_name_ordering)和由 GPT-4 生成的描述(cross_encoder_description_ordering)对表情符号进行排序。
-
使用 互惠排名融合 将四种排序结合起来,并返回排名靠前的结果!
结果搜索引擎虽然不完美,但在结合文本和视觉信息方面做得不错。由于使用 cross-encoder 计算开销较大(且延迟较高),因此它仅用于简化后的候选集。我使用的是 distilroberta-base 检查点,并配合 Sentence Transformers 库中的 CrossEncoder 类。
当所有这些步骤结合在一起时,结果如下:

再次强调,它不是完美的,但它还不错!
使用表情符号搜索引擎
有三种方式可以使用这个表情符号搜索引擎:托管版(免费)、通过用户界面在本地使用(开源)或者通过命令行在本地使用(同样是开源的)。这三种方式都非常简单!
在线版
访问 try.fiftyone.ai/datasets/emojis,登录(免费),然后点击网格上方菜单中的表情符号按钮。就是这样!
通过用户界面本地使用
如果你想在本地使用相同的视觉界面进行表情符号搜索,可以使用 Emoji Search 插件 进行 FiftyOne 搜索。
首先,安装 FiftyOne:
pip install fiftyone
然后下载 Emoji Search 插件并安装其依赖:
fiftyone plugins download https://github.com/jacobmarks/emoji-search-plugin
fiftyone plugins requirements @jacobmarks/emoji_search --install
启动 FiftyOne 应用:
fiftyone app launch
点击“浏览操作”文本,搜索“emoji”,然后点击“创建 Emoji 数据集”条目。这将下载表情符号的高清图像、嵌入以及所有相关数据。在应用的左上角,点击“选择数据集”框,选择“Emojis”。现在,你应该看到与托管版本相同的用户界面。
通过 CLI 本地使用
最后,你可以通过命令行使用 Emoji Search Python CLI 库进行搜索。可以通过以下命令从 GitHub 仓库安装该软件包:
pip install git+https://github.com/jacobmarks/emoji_search.git
然后,你可以使用 emoji-search 命令进行搜索,后面跟上文本查询(带不带引号均可)。
emoji-search beautiful sunset
+-------+-----------------+---------+
| Emoji | Name Unicode |
+-------+-----------------+---------+
| 🌞 | sun with face | U+1F31E |
| 🌇 | sunset | U+1F307 |
| 🌅 | sunrise | U+1F305 |
| 🔆 | bright button | U+1F506 |
| 🌆 |cityscape at dusk| U+1F306 |
+-------+-----------------+---------+
第一次执行搜索时,如果需要,会将嵌入下载到你的设备上。
所有三个版本都支持通过 pyperclip 复制表情符号到剪贴板。在用户界面中,点击表情符号的图像,你会看到菜单中出现一个复制按钮。在命令行界面中,使用 -c 参数将顶部结果复制到剪贴板。
结论
表情符号可能看起来是一个无聊的主题,让人感到过度关注。但实际上,语义表情符号搜索引擎相比于词汇表情符号搜索,其实用性可能有限。这个工作的真正价值在于理解我们传统上认为是独立的两种模式——图像和文本——之间的边界和重叠。表情符号恰好位于这个交汇点,因此,它们让我们能够探究今天多模态模型的优缺点——它们的能力与局限。
我最终构建的语义表情符号搜索引擎远非完美。坦率来说,表情符号具有主观性,代表不同的人可能有不同的含义,这一点是无法精确地封装的。但回到激发这个想法的例子,当我输入“一个音频播放器”时,我得到了几个不错的结果:

我将以哈佛大学甘尼迪学院教授、前《时代》杂志执行编辑南希·吉布斯的一句话作为结尾:
表情符号之所以特别,是因为[它们]帮助了数百万人比牛津词典中广泛的词汇还要更好地表达自己。
南希·吉布斯
注意:文章中的所有图像由作者创作,除非另有说明
如何使用 Burr、FastAPI 和 React 构建流媒体代理
如何利用开源工具构建一个简单的代理聊天机器人,使用流媒体技术。
·发表于Towards Data Science ·阅读时间 12 分钟·2024 年 7 月 25 日
--

我们的代理应用模型。我们将展示如何通过流媒体构建这个模型,让你能够创造出出色的用户体验。图片由作者提供。
在这篇文章中,我们将介绍如何构建一个代理聊天机器人,通过流媒体将响应传递给用户,利用Burr(我也是作者)的流媒体功能,FastAPI的StreamingResponse以及由React查询的服务器推送事件(SSEs)。这些都是开源工具。本文面向那些希望了解如何在 Python 中使用流媒体,以及如何为他们的代理/应用程序添加交互性的人。虽然我们使用的工具相对具体,但所学的内容应该适用于广泛的流媒体响应实现方式。
首先,我们将讨论流媒体的重要性。接着,我们将介绍我们使用的开源工具。我们将通过一个示例讲解,并提供你可以使用的代码,帮助你入门,之后分享更多资源和替代实现方案。
你可以通过Burr + FastAPI 代码和前端代码这里跟着一起操作。你也可以通过运行 pip install “burr[start]” && burr,然后访问 localhost:7241/demos/streaming-chatbot 来运行这个示例(你需要一个 OPENAI_API_KEY 环境变量);浏览器会自动打开,点击左侧的 demos/streaming-chatbot 即可。注意,这个示例要求 burr>=0.23.0。
为什么要使用流式传输?
虽然通过网络流式传输媒体是90 年代的技术,并且如今已无处不在(视频游戏、流媒体电视、音乐等),但最近生成式 AI 应用的兴起使得逐字流式传输文本的兴趣重新回到了人们的视野。
LLMs 是一种有趣的技术(甚至可能是有用的),但运行相对较慢,用户不喜欢等待。幸运的是,可以通过流式传输结果,让用户在 LLM 的响应生成过程中实时看到结果。此外,鉴于 LLM 的普遍机械且呆板的特性,流式传输可以使其看起来更具互动性,几乎就像它们在思考一样。
一个正确的实现将允许跨多个服务边界进行流式通信,使得中间代理能够在将数据呈现给用户时增强/存储流式数据。

一个简单的聊天机器人架构展示。图片由作者提供。
虽然这些都不是火箭科学,但使网页开发变得简单且高度标准化的工具(如 OpenAPI / FastAPI / React + 朋友们等)在流式传输支持上存在不同程度的差异,这意味着你常常会遇到与自己习惯的工具不同的选择。流式传输通常是框架设计中的事后考虑,导致一些限制,直到你开发到一半时才会发现。
让我们先了解一些将用来实现上面技术栈的工具,然后一起看个例子。
开源工具
我们用来构建这个的工具相互解耦得很好——如果你愿意,可以互换相似的工具,依然可以应用相同的教训/代码。
Burr
Burr 是一个轻量级的 Python 库,用于构建作为状态机的应用程序。你可以通过一系列动作(这些可以是装饰过的函数或对象)构建你的应用程序,这些动作声明来自状态的输入,以及来自用户的输入。这些定义了自定义逻辑(可以委托给任何框架),以及如何更新状态的指令。状态是不可变的,这允许你在任何时候检查它。Burr 处理编排、监控、持久化等工作。
@action(reads=["count"], writes=["count"])
def counter(state: State) -> State:
return state.update(counter=state.get("count", 0) +1)
你将 Burr 操作作为应用程序的一部分运行——这允许你将它们通过一系列(可选的)条件转换串联起来,从一个动作到另一个动作。
from burr.core import ApplicationBuilder, default, expr
app = (
ApplicationBuilder()
.with_actions(
count=count,
done=done # implementation left out above
).with_transitions(
("counter", "counter", expr("count < 10")), # Keep counting if the counter is < 10
("counter", "done", default) # Otherwise, we're done
).with_state(count=0)
.with_entrypoint("counter") # we have to start somewhere
.build()
)
Burr 配有一个用户界面,可以进行监控/遥测,并且提供挂钩功能来持久化状态/在执行过程中执行任意代码。
你可以将其视为一个流程图,即图形/状态机:

Burr 免费为你提供这张图片。图片由作者提供。
并使用本地遥测调试器进行监控:

操作系统遥测 UI 在任何给定时刻都会告诉你应用程序的状态。图像由作者提供。
虽然上述示例是一个简单的插图,但 Burr 通常用于代理(如本示例中所示)、RAG 应用程序和人类环中 AI 接口。请参阅仓库中的示例以获取(更详尽的)使用案例。稍后我们将详细介绍流式传输和一些更强大的功能。
FastAPI
FastAPI是一个框架,可以让你将 Python 函数暴露为 REST API。它有一个简单的接口——你编写函数然后为其添加装饰器,最后运行脚本——它会变成一个带有自文档化端点的服务器,通过OpenAPI进行描述。
@app.get("/")
def read_root():
return {"Hello": "World"}
@app.get("/items/{item_id}")
def read_item(item_id: int, q: Union[str, None] = None):
return {"item_id": item_id, "q": q}
FastAPI 提供了许多好处。它是原生支持异步的,通过 OpenAPI 提供文档,并且易于在任何云提供商上部署。它与基础设施无关,并且通常可以横向扩展(只要考虑到状态管理)。欲了解更多信息,请参见此页面。
React
React 无需介绍——它是一个极受欢迎的工具,支撑了互联网的许多部分。即便是最近流行的工具(如 next.js/remix)也建立在它之上。欲了解更多信息,请访问react.dev。我们将使用 React、typescript和tailwind,但你也可以替换为自己喜欢的前端工具,并且大部分内容都可以复用。
构建一个简单的代理型聊天机器人
让我们构建一个简单的代理型聊天机器人——它将是代理型的,因为它实际上会进行两次 LLM 调用:
-
一个调用,用于确定要查询的模型。我们的模型将有几个“模式”——生成诗歌、回答问题等……
-
一个调用,指向实际的模型(在本例中为提示+模型组合)
使用 OpenAI API 时,这是一个玩具示例——他们的模型是令人印象深刻的多面手。也就是说,这种工具委托的模式在各种 AI 系统中都可以看到,且这个示例可以干净地外推。
在 Burr 中建模代理
作为状态机建模
为了利用 Burr,我们将我们的代理型应用建模为一个状态机。基本的逻辑流程如下:

我们从用户输入的提示开始(顶部)。然后检查安全性,如果不安全,我们将进入“非安全”响应。否则,我们根据 mode 状态字段的值决定模式,并进行切换。每个操作都返回一个流式响应。流式传输完成后,它会返回到提示并等待另一个用户输入……图像由作者提供。
为了在 Burr 中建模这一过程,我们首先将创建相应的操作,使用流式 API。然后我们将它们组合成一个应用程序。
流式操作
在 Burr 中,操作可以同时使用同步和异步 API。在这个例子中,我们将使用异步。Burr 中的流式函数也可以与非流式操作混合使用,但为了简化,我们将一切都实现为流式操作。因此,无论是从 OpenAPI 流式传输(它有自己的异步流式接口),还是返回一个固定的 抱歉,我无法回答这个问题 的响应,它仍然会作为生成器实现。
对于那些不熟悉的人,生成器是 Python 中的一种结构,可以高效地、懒惰地对一系列值进行评估。它们通过 yield 关键字创建,yield 会将控制权从函数交还给调用者,直到需要下一个项目时才会继续执行。异步生成器的功能类似,不过它们在 yield 时还会将事件循环的控制权交出。阅读更多关于同步生成器和异步生成器的内容。
Burr 中的流式操作作为生成器实现,生成包含元组的流,其中包括:
-
中间结果(在此情况下,消息中的增量标记)
-
如果状态更新已完成,则为最终状态更新,若仍在生成中,则为 None
因此,最终的 yield 将表明流已完成,并输出最终结果,以便稍后进行存储/调试。一个基本的响应,代理到 OpenAI 并进行一些自定义提示处理,长这样:
@streaming_action(reads=["prompt", "chat_history", "mode"], writes=["response"])
async def chat_response(
state: State, prepend_prompt: str, model: str = "gpt-3.5-turbo"
) -> AsyncGenerator[Tuple[dict, Optional[State]], None]:
"""A simple proxy.
This massages the chat history to pass the context to OpenAI,
streams the result back, and finally yields the completed result
with the state update.
"""
client = _get_openai_client()
# code skipped that prepends a custom prompt and formats chat history
chat_history_for_openai = _format_chat_history(
state["chat_history"],
prepend_final_promprt=prepend_prompt)
result = await client.chat.completions.create(
model=model, messages=chat_history_api_format, stream=True
)
buffer = []
async for chunk in result:
chunk_str = chunk.choices[0].delta.content
if chunk_str is None:
continue
buffer.append(chunk_str)
yield {"delta": chunk_str}, None
result = {
"response": {"content": "".join(buffer), "type": "text", "role": "assistant"},
}
yield result, state.update(**result).append(chat_history=result["response"])
在这个示例中,我们还有一些其他的流式操作——这些操作将代表“终端”操作——即在状态机完成时触发工作流暂停的操作。
构建应用程序
为了构建应用程序,我们首先将构建一个图形。我们将使用 Burr 的图形 API,使我们能够将图形的形状与其他应用程序关注点解耦。在 Web 服务中,图形 API 是一种非常简洁的方式来表达状态机逻辑。你可以全局构建一次,然后在每个单独的应用实例中重复使用它。图形构建器是这样的——请注意它引用了上面提到的 chat_response 函数:
# Constructing a graph from actions (labeled by kwargs) and
# transitions (conditional or default).
graph = (
GraphBuilder()
.with_actions(
prompt=process_prompt,
check_safety=check_safety,
decide_mode=choose_mode,
generate_code=chat_response.bind(
prepend_prompt="Please respond with *only* code and no other text"
"(at all) to the following",
),
# more left out for brevity
)
.with_transitions(
("prompt", "check_safety", default),
("check_safety", "decide_mode", when(safe=True)),
("check_safety", "unsafe_response", default),
("decide_mode", "generate_code", when(mode="generate_code")),
# more left out for brevity
)
.build()
)
最后,我们可以将这些内容组合到一个应用程序中——它暴露了正确的执行方法,以便服务器与之交互:
# Here we couple more application concerns (telemetry, tracking, etc…).
app = ApplicationBuilder()
.with_entrypoint("prompt")
.with_state(chat_history=[])
.with_graph(graph)
.with_tracker(project="demo_chatbot_streaming")
.with_identifiers(app_id=app_id)
.build()
)
当我们想要运行它时,我们可以调用astream_results。这个方法接受一组停止条件,并返回一个AsyncStreamingResultContainer(一个缓存结果的生成器,并确保调用 Burr 跟踪),以及触发停止的动作。
# Running the application as you would to test,
# (in a jupyter notebook, for instance).
action, streaming_container = await app.astream_result(
halt_after=["generate_code", "unsafe_response", ...], # terminal actions
inputs={
"prompt": "Please generate a limerick about Alexander Hamilton and Aaron Burr"
}
)
async for item in streaming_container:
print(item['delta'], end="")
在 Web 服务器中暴露
现在我们有了 Burr 应用程序,我们希望通过使用服务器推送事件 (SSEs) 将其与 FastAPI 的流式响应 API集成。虽然我们不会深入讨论 SSEs,简而言之,它们作为 WebSocket 的单向(服务器 → 客户端)版本工作。你可以通过文末的链接了解更多信息。
要在 FastAPI 中使用这些内容,我们声明一个端点作为一个返回 StreamingResponse 的函数——StreamingResponse 是一个包装生成器的类。标准做法是以一种特殊的格式提供流式响应,“data:
我们已经单独实现了 _get_application,这是一个通过 ID 获取/加载应用程序的工具函数。
该功能将是一个 POST 端点,因为我们正在向服务器添加数据,尽管也可以轻松改为 PUT。
@app.post("/response/{project_id}/{app_id}", response_class=StreamingResponse)
async def chat_response(project_id: str, app_id: str, prompt: PromptInput) -> StreamingResponse:
"""A simple API that wraps our Burr application."""
burr_app = _get_application(project_id, app_id)
chat_history = burr_app.state.get("chat_history", [])
action, streaming_container = await burr_app.astream_result(
halt_after=chat_application.TERMINAL_ACTIONS, inputs=dict(prompt=prompt.prompt)
)
async def sse_generator():
yield f"data: {json.dumps({'type': 'chat_history', 'value': chat_history})}\n\n"
async for item in streaming_container:
yield f"data: {json.dumps({'type': 'delta', 'value': item['delta']})} \n\n"
return StreamingResponse(sse_generator())
请注意,我们在函数内部定义了一个生成器,它包装了 Burr 结果并将其转换为适合 SSE 的输出。这使我们能够对结果施加一些结构,后端将使用这些结构。不幸的是,我们将不得不自行解析它,因为 fastAPI 不支持对 StreamingResponse 进行严格的类型定义。
此外,我们实际上会在执行前,最开始就输出整个状态。虽然这并非严格必要(我们也可以有一个单独的 API 用于聊天历史),但它会让渲染更加容易。
要测试这一点,你可以使用 requests 库的Response.iter_lines API。
构建用户界面
现在我们已经有了服务器、状态机和 LLM,让我们让它看起来更好!这就是一切整合的地方。虽然你可以下载并玩转完整的代码,示例中有全部内容,但我们将专注于在点击“发送”时查询 API 的函数。

这是 UI 的样子。你可以通过 Burr 附带的打包 Telemetry UI 运行这个。图像来自作者。
首先,让我们使用 fetch 查询我们的 API(显然,你需要根据你的端点调整,当前我们将所有的 /api 调用代理到另一个服务器):
// A simple fetch call with getReader()
const response = await fetch(
`/api/v0/streaming_chatbot/response/${props.projectId}/${props.appId}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt: currentPrompt })
}
);
const reader = response.body?.getReader();
这看起来像一个普通的 API 调用,利用了 TypeScript 的 async API。它提取了一个 reader 对象,帮助我们随着结果的到来进行流式传输。
让我们定义一些数据类型,以便利用我们上面创建的结构。除了 ChatItem 数据类型(这是通过 openapi-typescript-codegen 生成的),我们还将定义两个类,它们对应服务器返回的数据类型。
// Datatypes on the frontend.
// The contract is loose, as nothing in the framework encodes it
type Event = {
type: 'delta' | 'chat_history';
};
type ChatMessageEvent = Event & {
value: string;
};
type ChatHistoryEvent = Event & {
value: ChatItem[];
};
接下来,我们将遍历读取器并进行解析。这假设在 React 中存在以下状态变量:
-
setCurrentResponse/currentResponse -
setDisplayedChatHistory
我们通过“data:”进行拆分,然后循环遍历拆分结果,并根据事件类型进行解析/反应。
// Loop through, continually getting the stream.
// For each item, parse it as our desired datatype and react appropriately.
while (true) {
const result = await reader.read();
if (result.done) {
break;
}
const message = decoder.decode(result.value, { stream: true });
message
.split('data: ')
.slice(1)
.forEach((item) => {
const event: Event = JSON.parse(item);
if (event.type === 'chat_history') {
const chatMessageEvent = event as ChatHistoryEvent;
setDisplayedChatHistory(chatMessageEvent.value);
}
if (event.type === 'delta') {
const chatMessageEvent = event as ChatMessageEvent;
chatResponse += chatMessageEvent.value;
setCurrentResponse(chatResponse);
}
});
}
我们省略了一些清理/错误处理代码(例如在请求之前/之后清除、初始化状态变量、处理失败等)——你可以在示例中看到更多内容。
最后,我们可以渲染它(请注意,这指的是在上面的代码之外设置/取消设置的额外状态变量,以及一个简单显示聊天消息和相应图标的 ChatMessage React 组件)。
<!-- More to illustrates the example -->
<div className="flex-1 overflow-y-auto p-4 hide-scrollbar" id={VIEW_END_ID}>
{displayedChatHistory.map((message, i) => (
<ChatMessage
message={message}
key={i}
/>
))}
{isChatWaiting && (
<ChatMessage
message={{
role: ChatItem.role.USER,
content: currentPrompt,
type: ChatItem.type.TEXT
}}
/>
)}
{isChatWaiting && (
<ChatMessage
message={{
content: currentResponse,
type: ChatItem.type.TEXT,
role: ChatItem.role.ASSISTANT
}}
/>
)}
</div>
<!-- Note: We've left out the isChatWaiting and currentPrompt state fields above,
see StreamingChatbot.tsx for the full implementation. -->
我们最终完成了整个应用程序!要查看所有的代码,点击这里。
替代的 SSE 工具
请注意,我们上面展示的仅仅是使用 FastAPI/react/Burr 进行流式传输的一种方法。你还可以使用其他许多工具,包括:
-
EventSource API——标准但仅限于 get/ 请求
-
FetchEventSource API(看起来不再维护,但构建得很好)
以及其他一些很棒的博客文章(我看这些文章来入门)。这些文章也能帮助你更好地理解架构。
总结
在这篇文章中,我们讲解了很多内容——我们介绍了 Burr、FastAPI 和 React,讨论了如何使用 OpenAI API 构建一个流式 代理 聊天机器人,构建了整个堆栈,并进行了数据流式传输!虽然你可能不会使用所有的技术,但每个单独的部分应该都能独立工作。
要下载并尝试这个示例,你可以运行:
pip install "burr[start]"
burr # will open up in a new window
请注意,您需要一个来自OpenAI 的 API 密钥来运行这个特定的示例。您可以在Burr + FastAPI 代码中找到相关代码,在前端代码中也可以找到。
额外资源
-
Burr 的 Github 仓库(如果您喜欢看到的内容,请给我们点个星!)
如何使用 OpenAI + Python 构建 AI 助手
使用 Assistants API 和 Fine-tuning 的逐步指南
·发布于 Towards Data Science ·阅读时间 13 分钟·2024 年 2 月 8 日
--

图片来自 Canva。
我从客户那里收到的最常见问题之一是:“我如何用我的数据创建一个自定义聊天机器人?” 虽然六个月前,这可能需要几个月的开发时间,但今天情况不一定是这样。在本文中,我将提供一个逐步指南,介绍如何使用 OpenAI 的 Assistants 和 Fine-tuning API 创建一个自定义 AI。每种方法都提供了 Python 示例代码。
聊天机器人与助手
在深入示例代码之前,我想简要区分 AI 聊天机器人和助手。虽然这些术语经常被交替使用,但在这里,我将它们表示为不同的概念。
聊天机器人是一种可以与之对话的 AI,而AI 助手是一种可以使用工具的聊天机器人。工具可以是网页浏览器、计算器、Python 解释器或任何其他扩展聊天机器人功能的东西 [1]。
例如,如果你使用的是免费版的 ChatGPT,那就是一个聊天机器人,因为它只有基本的聊天功能。然而,如果你使用的是 ChatGPT 的付费版,那就是一个助手,因为它具备网页浏览、知识检索和图像生成等功能。
如何构建一个 LLM 驱动的应用程序与 PapersWithCode 聊天
跟上最新的机器学习研究进展
·发表于Towards Data Science ·阅读时间:11 分钟·2024 年 2 月 1 日
--

图片来自Patrick Tomasso 在Unsplash
您是否觉得跟不上最新的机器学习研究进展?是否被大量关于 LLM、向量数据库或 RAG 的论文压得喘不过气来?
在这篇文章中,我将展示如何构建一个 AI 助手,轻松挖掘大量信息。您将以自然语言向它提问,它会根据找到的相关论文回答您的问题,论文来自Papers With Code。
在后端方面,这个助手将由一个基于可扩展无服务器向量数据库、来自 VertexAI 的嵌入模型和来自 OpenAI 的 LLM 的检索增强生成(RAG)框架提供支持。
在前端方面,助手将集成到一个互动式且易于部署的 Web 应用程序中,该程序是使用Streamlit构建的。
本文将详细介绍此过程的每个步骤,并提供一个您可以重用和调整的源代码👇。
准备好了吗?让我们开始吧🔍。
如果您对机器学习内容、详细教程以及来自行业的实用技巧感兴趣,请关注我的新闻通讯。它叫做The Tech Buffet.
如何构建一个 OpenAI 兼容的 API
创建一个服务器来复制 OpenAI 的 Chat Completions API,使任何 LLM 都能与为 OpenAI API 编写的工具集成
·发表于 Towards Data Science ·阅读时长 6 分钟·2024 年 3 月 24 日
--

图片由作者使用 OpenAI DALL-E 生成
现在是 2024 年初,生成型 AI 市场由 OpenAI 主导。原因很简单——他们具有先发优势,是第一个提供易用 API 的 LLM 提供商,而且他们提供的 GPT-4 可能是迄今为止最强大的 LLM。鉴于此,各种工具的开发者(如 代理程序、个人助手、编码扩展)纷纷转向 OpenAI 以满足他们的 LLM 需求。
虽然有许多理由让你使用 OpenAI 的 GPT 来推动你的生成型 AI 创作,但也有许多理由选择其他替代方案。有时候,使用 OpenAI 可能不够具有成本效益,而有时候你的数据隐私政策可能禁止你使用 OpenAI,或者你可能在托管一个开源的 LLM(或者你自己的 LLM)。
OpenAI 的市场主导地位意味着许多你可能想要使用的工具仅支持 OpenAI API。像 OpenAI、Anthropic 和 Google 这样的生成型 AI 和 LLM 提供商似乎在创建不同的 API 模式(可能是故意的),这增加了开发者为支持所有这些平台而需做的额外工作。
所以,作为一个快速的周末项目,我决定实现一个兼容 OpenAI API 规范的 Python FastAPI 服务器,目的是让你可以将几乎任何你喜欢的 LLM(无论是像 Anthropic 的 Claude 这样的托管模型,还是自托管的模型)包装起来,以模仿 OpenAI API。幸运的是,OpenAI API 规范中有一个 base_url 参数,可以将其设置为指向你的服务器,而不是 OpenAI 的服务器,大多数上述工具的开发者允许你根据需要设置这个参数。
为了实现这一点,我参照了 OpenAI 的聊天 API 参考文档,公开可用的链接在此,并在 vLLM的代码帮助下进行了一些修改。vLLM 是一个 Apache-2.0 许可证下的推理服务器,支持 LLMs,同时兼容 OpenAI API。
游戏计划
我们将构建一个模拟的 API,模仿 OpenAI 的聊天完成 API(/v1/chat/completions)的工作方式。尽管这个实现是用 Python 和 FastAPI 编写的,我将其保持得非常简单,以便能够轻松迁移到像 TypeScript 或 Go 这样的其他现代编程语言。我们将使用 Python 官方的OpenAI 客户端库进行测试——我们的想法是,如果我们能让库认为我们的服务器是 OpenAI 的服务器,那么任何使用该库的程序也会认为是同样的。
第一步 — 聊天完成 API,非流式处理
我们将从实现非流式部分开始。首先建模我们的请求:
from typing import List, Optional
from pydantic import BaseModel
class ChatMessage(BaseModel):
role: str
content: str
class ChatCompletionRequest(BaseModel):
model: str = "mock-gpt-model"
messages: List[ChatMessage]
max_tokens: Optional[int] = 512
temperature: Optional[float] = 0.1
stream: Optional[bool] = False
PyDantic 模型代表了来自客户端的请求,旨在复制 API 参考。为了简洁起见,这个模型没有实现完整的规范,而是实现了使其正常工作的基本部分。如果你缺少一个属于API 规范的参数(比如 top_p),你只需将其添加到模型中。
ChatCompletionRequest 模型化了 OpenAI 在请求中使用的参数。聊天 API 规范要求指定一个 ChatMessage 列表(类似于聊天历史,通常由客户端负责保持并在每次请求时反馈)。每个聊天消息有一个 role 属性(通常是 system、assistant 或 user)和一个 content 属性,包含实际的消息文本。
接下来,我们将编写 FastAPI 聊天完成端点:
import time
from fastapi import FastAPI
app = FastAPI(title="OpenAI-compatible API")
@app.post("/chat/completions")
async def chat_completions(request: ChatCompletionRequest):
if request.messages and request.messages[0].role == 'user':
resp_content = "As a mock AI Assitant, I can only echo your last message:" + request.messages[-1].content
else:
resp_content = "As a mock AI Assitant, I can only echo your last message, but there were no messages!"
return {
"id": "1337",
"object": "chat.completion",
"created": time.time(),
"model": request.model,
"choices": [{
"message": ChatMessage(role="assistant", content=resp_content)
}]
}
就这么简单。
测试我们的实现
假设这两个代码块位于名为 main.py 的文件中,我们将在选择的环境中安装两个 Python 库(最好创建一个新的环境):pip install fastapi openai,并通过终端启动服务器:
uvicorn main:app
使用另一个终端(或在后台启动服务器),我们将打开一个 Python 控制台,并复制粘贴以下代码,这些代码直接来自OpenAI 的 Python 客户端参考:
from openai import OpenAI
# init client and connect to localhost server
client = OpenAI(
api_key="fake-api-key",
base_url="http://localhost:8000" # change the default port if needed
)
# call API
chat_completion = client.chat.completions.create(
messages=[
{
"role": "user",
"content": "Say this is a test",
}
],
model="gpt-1337-turbo-pro-max",
)
# print the top "choice"
print(chat_completion.choices[0].message.content)
如果你一切操作正确,服务器的响应应该会正确打印。检查chat_completion对象也很有价值,可以确认所有相关属性是否与我们服务器发送的一致。你应该能看到类似下面的内容:

由作者编写的代码,使用Carbon格式化
升级——支持流式传输
由于 LLM 生成通常较慢(计算开销大),将生成的内容流式传输回客户端是值得的,这样用户可以在生成过程中看到响应,而不必等到它完全生成完毕。如果你记得,我们给ChatCompletionRequest添加了一个布尔型的stream属性——这让客户端可以请求将数据流式传输回去,而不是一次性发送。
这使得事情变得稍微复杂了一些。我们将创建一个生成器函数来包装我们的模拟响应(在实际场景中,我们将需要一个连接到 LLM 生成的生成器)
import asyncio
import json
async def _resp_async_generator(text_resp: str):
# let's pretend every word is a token and return it over time
tokens = text_resp.split(" ")
for i, token in enumerate(tokens):
chunk = {
"id": i,
"object": "chat.completion.chunk",
"created": time.time(),
"model": "blah",
"choices": [{"delta": {"content": token + " "}}],
}
yield f"data: {json.dumps(chunk)}\n\n"
await asyncio.sleep(1)
yield "data: [DONE]\n\n"
现在,我们将修改原始的端点,当stream==True时返回一个 StreamingResponse
import time
from starlette.responses import StreamingResponse
app = FastAPI(title="OpenAI-compatible API")
@app.post("/chat/completions")
async def chat_completions(request: ChatCompletionRequest):
if request.messages:
resp_content = "As a mock AI Assitant, I can only echo your last message:" + request.messages[-1].content
else:
resp_content = "As a mock AI Assitant, I can only echo your last message, but there wasn't one!"
if request.stream:
return StreamingResponse(_resp_async_generator(resp_content), media_type="application/x-ndjson")
return {
"id": "1337",
"object": "chat.completion",
"created": time.time(),
"model": request.model,
"choices": [{
"message": ChatMessage(role="assistant", content=resp_content) }]
}
测试流式传输实现
重启 uvicorn 服务器后,我们将打开 Python 控制台并输入这段代码(同样是来自 OpenAI 的文档)
from openai import OpenAI
# init client and connect to localhost server
client = OpenAI(
api_key="fake-api-key",
base_url="http://localhost:8000" # change the default port if needed
)
stream = client.chat.completions.create(
model="mock-gpt-model",
messages=[{"role": "user", "content": "Say this is a test"}],
stream=True,
)
for chunk in stream:
print(chunk.choices[0].delta.content or "")
你应该看到服务器响应中的每个单词都被慢慢打印出来,模拟标记生成。我们可以检查最后一个chunk对象,看到类似下面的内容:

由作者编写的代码,使用Carbon格式化
将所有内容整合起来
最后,在下面的代码片段中,你可以看到服务器的完整代码。
最后的注意事项
-
这里我们可以做很多其他有趣的事情,比如支持其他请求参数,和其他 OpenAI 的抽象,如函数调用和助手 API。
-
LLM API 缺乏标准化使得切换提供商变得困难,既对于公司也对于开发 LLM 封装包的开发者而言。在没有任何标准的情况下,我在这里采取的做法是将 LLM 抽象化,基于最大且最成熟的 API 的规格进行封装。
如何为机器学习构建数据管道
面向初学者的介绍,附带 Python 代码
·发布于Towards Data Science ·阅读时间:10 分钟·2024 年 5 月 2 日
--
这是关于全栈数据科学(FSDS)系列文章的第三篇。在上一篇文章中,我介绍了一个用于构建机器学习(ML)解决方案的五步项目管理框架。虽然机器学习可能让人联想到复杂的算法和技术,但ML 解决方案的质量取决于 可用数据的质量。这就提出了在 FSDS 中对数据工程(DE)技能的需求。本文将讨论在此背景下最关键的 DE 技能,并通过一个真实的例子进行讲解。

图片由the blowup提供,来源于Unsplash
全栈数据科学(FSDS)涉及端到端管理和实施机器学习解决方案。数据工程对于这个过程至关重要,即使数据能够轻松地用于分析和机器学习应用[1]。
虽然这可能涉及广泛的任务(例如数据建模、设计架构、管理分布式系统),但在 FSDS 的背景下,数据工程归结为一件关键的事情——构建数据管道。
数据管道 将数据从 A 点传输到 B 点。例如,抓取网页、重格式化数据并将其加载到数据库中。数据管道包括三个步骤——提取(E)、转换(T)和加载(L),可以通过两种方式组合——ETL 或 ELT。
如何构建用于节点分类的神经网络
学习如何在 13 分钟内使用 PyTorch Geometric 从 CSV 文件构建基于图的神经网络
·发表于Towards Data Science ·13 分钟阅读·2024 年 5 月 13 日
--
介绍
许多数据类型本质上是关系型的,基于图的神经网络是强大的模型,可以学习网络中的复杂模式。
在本文中,我们将介绍如何构建 PyTorch Data对象以处理同质图数据,然后训练不同类型的神经网络来预测一个节点属于哪个类别。这种预测问题通常被称为节点分类。
我们将使用来自《多尺度属性节点嵌入》论文中的 Facebook 大型页面-页面网络¹ 数据集,该论文由 Benedek Rozemberczki、Carl Allen 和 Rik Sarkar 于 2019 年发表在《复杂网络期刊》上。
该数据集包含 22,470 个 Facebook 页面,这些页面根据主题被分类为四个类别之一。每个出版物通过一个大小不一的特征向量来表示。数据集还包含有关 Facebook 页面的跟随信息,总共有 171,992 个链接或边缘。
本文详细介绍了从表格数据构建基于图的神经网络的五个步骤:
-
从 URL 下载数据,
-
从表格数据创建 PyTorch
Data对象,
如何在公司内建立提示工程专业技能
·发表于Towards Data Science ·7 分钟阅读·2024 年 12 月 2 日
--

你决定在公司内使用生成型人工智能,并且已经进行了初步的实验。现在问题来了:我是否需要专门的人来处理即将到来的提示工作?
尽管近年来对提示工程的兴趣保持稳定,许多公司在建立提示工程能力的第一步上遇到了困难,因为他们根本不知道从哪里开始。
像提示工程岗位的高薪这样的新闻并没有帮助,反而使得人们的自然反应是直接去自由市场寻找提示工程师,这样做太冒险。这是因为这些公司仍处于生成型人工智能采用的初期阶段,并不确定在这个阶段对新员工进行如此大量的投资是否值得。

来源:www.businessinsider.com/ai-prompt-engineer-jobs-pay-salary-requirements-no-tech-background-2023-3
此外,考虑到生成型人工智能在 2023 年至 2024 年间的迅速发展,许多领导者都在问一个非常合理的问题:提示工程师会长期存在吗?未来是否仍然需要编写提示,还是只需要向大型语言模型(LLM)简单描述待解决的问题,用几句话就能满足需求?
虽然任何公司都可以自由选择如何满足他们对能够进行提示工程的人员的需求,但在这篇文章中,我将重点讨论如何在公司内部培养这种专业技能。这条路可能不像直接雇佣一个有现成提示工程经验的人那样简捷,但它带来了一些积极的副作用——稍后会详细说明。
提示工程师应该具备深厚的技术背景吗?
绝对不是。目前的提示工程状态提供了大约 20 种高级技术,其中一些可能对于实现你生成 AI 项目的特定目标非常有效,但没有一种技术要求深入了解编程语言,或者仅仅由提示工程师来构建复杂的提示交互。
提示工程师通常从定义 LLM(大语言模型)需要解决的问题开始。通过尝试不同的提示内容,如何构建提示,或者如何将多个提示连接起来,做提示工程的人需要确保 LLM 输出满足预期质量。
上述所有内容都可以“纸上谈兵”完成,而且不需要写出一行代码。在这里,纸指的是每个 LLM 供应商提供的操作平台。如果需要让多个提示协同工作,前一个提示的输出可以手动注入到下一个提示中。
什么样的人会在提示工程师岗位上表现突出?
我们目前处于一个独特的境地,这个角色的形态尚未最终确定,并且正在根据行业需求不断调整:几乎每半年,AI 工具和提示技术的进展都要求从事提示工作的人不断扩展自己的技能。
但有两点是默认至关重要的:好奇心和创造力。
一个真正对自己所从事领域充满好奇的人,无论这个领域是什么,都能够交付最佳的结果。不断跟进最新的提示技术进展,了解大型语言模型的独特能力(以及市场上众多商业和开源模型的多样性),将使他们不仅仅是把“一个 GPT”应用到每个他们解决的问题上,而是能立即识别出,例如,非复杂的任务可以由较不强大的、但更便宜且更快速的模型来解决。
另一个至关重要的技能是在进行提示工程时保持创造性。虽然已经有一些提示工程方法可以保证稳定的结果,但我们还远未理解哪些提示或技术能产生最佳输出。如果仅仅依靠直接简洁的提示,这些人永远也不会发现那些能在统计上提升模型输出表现的方法,例如“我要给$xxx 的小费换一个更好的解决方案!”以及其他疯狂且出人意料的想法。LLMs 是我们以前从未拥有的工具,在它们当前的发展阶段,保持创造性地使用它们,并给予正确的指令,将带来最佳的结果——所以,确保你为这个角色设想的人能够进行原创性的思考。
在某些时候,提示工程师将会对已部署到生产环境中的提示进行更改,确保 LLM 输出质量不会回退将是他们的首要任务。当然,有一些工具和方法可以帮助降低这个风险,但没有什么能够替代一个专心的人,通过仔细阅读模型的输出,比较更改前后的内容,并识别出负面模式。
如果你设想的未来提示工程师具备上述三种特质,不必担心他们例如尚不完全了解大型语言模型(LLMs)如何运作——好奇心会引导他们自然而然地学习,而创造力将赋予他们解决那些不明显问题时新颖的技巧和方法。对细节的敏锐眼光将在长期内发挥作用,并防止输出质量出现意外的下降。
这里有另一个关于优秀提示工程师应具备的其他特质的讨论。
在哪里可以找到从事提示工作的人
虽然从外部招聘某人始终是一个选项,但这样的人不会立即了解你希望从 LLMs 中获得的输出。由于 LLMs 的非确定性特性,它们的输出可以有多种形式和风格,而这正是提示工程师的工作——使得这种结果变得更加可预测。
谁最了解你的 LLM 助手应该产生哪种输出?(例如,答案应该有多深入,使用什么语气?)
对了。这些是你公司内部的员工,他们已经深度参与了你产品的开发工作。仔细看看:也许他们当中已经有人对生成式 AI 的能力感到兴奋,并想尝试一个新的角色?
这些人将是成为提示工程师的理想候选人:他们的领域和产品知识足够深入,能够知道模型输出应该达到什么程度的复杂性和准确性。通常,他们还拥有其他部门的有用内部关系,而这些技术人员未必具备。例如,一位来自客户成功部门并成为提示工程师的人,会更容易知道他们所参与的基于 LLM 的产品的最终输出应该是什么样的,而不像昨天的技术部门的工程师那样,通常只专注于一个单一产品领域的深度技术工作。
如何培养你的提示工程师
随着时间的推移,你将面临需要培养公司内部提示编写人员的需求。对这些专家的成长不仅仅意味着能够快速找到适合给定问题的最佳提示技术(这通过经验积累),而是扩展他们的视野,让他们能够在超越仅仅定义 LLM 应用的系统提示之外,看到更多的可能性。
除了紧跟 LLM 研究和最新提示工程技术的进展外,更高级的提示工程师需要解决 LLM 评估工具的问题——这些工具提供关于模型/提示性能的反馈(类似于软件工程中的单元测试)。
通常,评估工具可以是基于 LLM 的(例如,模型 B 评估模型 A 的输出)或基于代码的(例如,Python 函数检查模型输出是否符合预期的 JSON 架构)。尽管基于代码的评估工具不需要精通编程技能,但实现这些工具的人必须对他们所使用的编程语言(主要是 Python)有一个高层次的理解——因此,提升这个技能可能是提示工程师成长的一个方向。
想象一下:某人交付了一个不仅“有效”且其中包含指令,且这些指令有测试/评估工具确保类似传统软件开发中单元测试提供的安全网的提示。
提示工程不仅仅是关于提示技巧、输出质量和评估工具。从更高水平来看,从事提示工作的人员必须更深入地理解 LLM 超参数对输出的影响。这意味着这样的人员可能需要成长的另一个方向——学习机器学习的基础知识,并投入更多精力了解 LLM 如何在后台运作。
理想情况下,你的组织已经有一位在软件开发与经典机器学习(或生成性人工智能)交集处有经验的领导者。这样的人可以更精确地引导提示工程师的成长,通过引导他们的开发进入上述领域。
构建提示工程师的专业能力
构建提示工程专业知识没有“一刀切”的方法,因为每个组织对这些提示所应用的大型语言模型有不同的要求。在不同的组织中,构建专业知识的意义可能大相径庭。
但有一点始终不变:您的提示工程师必须深入参与他们所从事的产品相关工作,同时具备与其独特角色相关的特定知识:前者使他们能够更快地实现所需的模型输出质量,后者则确保这些结果是可持续的,并且符合生成性人工智能这一快速发展的领域中的最佳实践。
给予您的提示工程师自由探索新方法的空间,同时让他们对所交付的结果负责:尽管大型语言模型(LLM)输出的非确定性特征存在,但我们可以并且应该减少意外输出质量偏差的风险,而且有工具可以使这些措施量化。
在您的组织中构建提示工程专业知识,不仅仅是适应当前人工智能的趋势——它还关乎塑造未来公司如何利用技术进行创新。通过赋能您的团队掌握提示工程,您将培养一种创造力、效率和前瞻性的文化。
如何构建自己的书签搜索 AI 助手?
自动化基于 GPT 的书签搜索管道逐步指南
·发表于 Towards Data Science ·阅读时长 9 分钟·2024 年 4 月 9 日
--

图像来自作者。
你是否曾在 Chrome 浏览器中搜索特定书签时,发现自己被数量庞大的书签淹没?在面对庞大的书签收藏时,逐一筛选变得相当繁琐且消耗精力。

书签海洋 — 来自作者 Google Chrome 书签的快照。
事实上,我们可以简单地把这件事交给 ChatGPT,它是目前最流行的 AI 模型,几乎能回答我们所问的一切。试想一下,如果它能够访问我们拥有的书签,那么问题就解决了!我们可以要求它从整个书签存储中提供我们想要的特定链接,就像下面的 GIF 一样:

展示 AI 助手的使用 — 来自屏幕录制的 GIF。
为了实现这一目标,我建立了一个实时管道,将 Chrome 书签更新到一个向量数据库中,ChatGPT 将使用这个数据库作为我们提问时的上下文。本文将逐步解释如何构建这样的管道,最终你也将拥有自己的管道!
特性
在开始之前,让我们先总结一下它的优势:
- 与传统的搜索引擎(如 Chrome 书签管理器中的搜索)不同,AI 模型可以很好地理解每个标题的语义。如果你在搜索书签时忘记了确切的关键词,你只需提供一个大致的框架,ChatGPT 就能找到它!它甚至可以理解不同语言的标题,真是令人惊叹。

ChatGPT 能做什么 — 来自 AI 助手界面的快照。

当 Chrome 书签搜索引擎失败时。 — 来自作者的 Google Chrome 书签管理器截图
2. 一切都自动保持最新。每个新添加的书签将在几分钟内自动反映在 AI 知识数据库中。
管道概览

管道中的所有组件。 — 图片由作者提供。
在这里,您可以看到我们管道中每个组件的作用。Chrome 书签通过我们定制的 Chrome 插件提取到 Google 表格的行中。Estuary Flow 获取(或捕获)所有的表格数据,然后通过 OpenAI API 使用嵌入模型将其向量化(或物化)。所有嵌入(每个向量对应表格中的一行——即每个单独的书签)将被检索并存储在 Pinecone 向量数据库中。之后,用户可以使用 Streamlit 和 LangChain 向构建的应用程序发出提示(例如:“dinov2 的链接是什么?”)。它将首先从 Pinecone 中检索一些相似的嵌入(获取上下文/几个潜在的书签选项),并将它们与用户的问题一起作为输入提供给 ChatGPT。然后 ChatGPT 将考虑每个可能的书签,并给出最终的答案。这一过程也被称为 RAG:检索增强生成。
在接下来的部分中,我们将逐步展示如何构建这样的管道。
用于书签检索的 Chrome 插件
代码:github.com/swsychen/Boomark2Sheet_Chromeplugin
为了将书签传输到 Google 表格中以供进一步处理,我们首先需要构建一个定制的 Chrome 插件(或扩展)。

Chrome 插件的代码结构概览。 — 来自作者的 vscode 截图。
Chrome 扩展的最重要文件是manifest.json,它定义了插件的高级结构和行为。在这里,我们添加了必要的权限来使用 Google Chrome 的书签 API,并跟踪书签的变化。我们还设置了一个oauth2 认证字段,因为我们将使用 Google 表格 API。您需要在此字段中填写自己的client_id。您可以主要参考此链接中的设置您的环境部分来获取client_id和 Google 表格 API 密钥(稍后我们会使用它)。有一点需要注意的是:
-
在OAuth 同意屏幕中,您需要将自己(Gmail 地址)添加为测试用户。否则,您将无法使用这些 API。
-
在创建 OAuth 客户端 ID时,您应该选择的应用类型是Chrome 扩展(而不是快速入门链接中的 Web 应用)。需要指定的项目 ID是插件 ID(我们在加载插件时会获得它,您可以在扩展管理器中找到它)。

模糊的部分是项目 ID。— 来自作者的 Google Chrome 扩展管理器截图。
核心功能文件是 background.js,它可以在后台执行所有的同步操作。我已经为您准备了 GitHub 链接中的代码,您需要做的唯一修改是在 JavaScript 文件的开头更改 spreadsheetId。这个 ID 您可以通过创建的 Google 表格的分享链接来识别(位于 d/ 和 /edit 之间,是的,您需要先手动创建一个 Google 表格!):
docs.google.com/spreadsheets/d/{**spreadsheetId}**/edit#gid=0
代码的主要逻辑是监听书签中的任何更改,并在插件触发时(例如,当您添加新的书签时),刷新(清除 + 写入)您的 Google 表格,更新所有书签。它将每个书签的 ID、标题和 URL 写入您指定的 Google 表格中的单独一行。

这就是在您的 Google 表格中的样子。— 来自作者的 Google 表格截图。
最后的文件 popup.html 基本上并不太有用,因为它只是定义了在您点击浏览器插件按钮时,弹出窗口显示的内容。
在确保所有文件都在同一个文件夹后,现在您可以准备上传您的插件:
-
打开 Chrome 浏览器,进入 Extensions>Manage Extensions,并在页面右上角启用 Developer mode。
-
点击 Load unpacked 并选择代码文件夹。然后您的插件将上传并开始运行。点击超链接 service worker 来查看代码打印的日志信息。
上传后,只要 Chrome 浏览器处于打开状态,插件就会保持运行。而且当您重新打开浏览器时,它也会自动启动。
设置 Estuary Flow 和 Pinecone
Estuary Flow 本质上是一个连接器,用于将数据库与您提供的数据源进行同步。在我们的案例中,当 Estuary Flow 将数据从 Google 表格同步到向量数据库 Pinecone 时,它还会调用嵌入模型,将数据转换为嵌入向量,然后将其存储在 Pinecone 数据库中。
关于 Estuary Flow 和 Pinecone 的设置,YouTube 上已经有一个相当全面的视频教程:youtu.be/qyUmVW88L_A?si=xZ-atgJortObxDi-
但请注意!由于 Estuary Flow 和 Pinecone 正在快速开发中,视频中的一些内容已经发生了变化,这可能会导致一些困惑。在这里,我列出了一些视频更新,以便您可以轻松地复制所有操作:
- (Estuary Flow>create Capture) 在行批量大小中,您可以根据 Google 表格中书签的总行数设置较大的数字。(例如,如果您已经有 400 多行书签,可以设置为 600)
2. (Estuary Flow>创建捕获) 设置目标集合时,删除光标字段“row_id”,并像以下截图一样添加一个新的“ID”。你可以将命名空间保持为空。

更改光标字段。— 来自Sources的快照,拍摄于 Estuary Flow(2024 年 4 月)
3. (Estuary Flow>创建捕获) 然后切换到COLLECTION子标签,点击编辑将 Key 从/row_id 更改为/ID。同时,你还应该像下面这样将架构代码中的“required”字段更改为“ID”:

更改密钥和架构。— 来自Sources的快照,拍摄于 Estuary Flow(2024 年 4 月)
//...skipped
"URL": {
"type": "string"
},
"row_id": {
"type": "integer"
}
},
"required": [
"ID"
],
"type": "object"
}
在“保存并发布”后,你可以看到Collections>{你的集合名称}>概览>数据预览会显示每个书签的正确 ID。
4. (Estuary Flow>创建捕获) 在最后一步,你可以看到一个高级规格编辑器(在页面底部)。在这里,你可以添加一个字段“interval”:10m,将刷新频率降至每 10 分钟一次(如果未指定,默认设置为每 5 分钟一次)。每次刷新都会调用 OpenAI 嵌入模型重新做所有嵌入,这会产生一些费用。降低刷新频率可以节省一半的费用。你可以忽略“backfill”字段。

指定间隔。— 来自Sources的快照,拍摄于 Estuary Flow(2024 年 4 月)
//...skipped
"syncMode": "full_refresh"
},
"target": "CJQ/mybookmark/bookmarks_v3"
}
],
"interval": "10m"
}
5. (Estuary Flow>创建物化) 对于免费的 Pinecone 索引,Pinecone 环境通常是“gcp-starter”,或者对于标准计划用户是“us-east-1-aws”(我不使用 Pinecone 的无服务器模式,因为 Estuary Flow 尚未为 Pinecone 无服务器模式提供连接器)。Pinecone 索引是你在 Pinecone 中创建索引时指定的索引名称。
6. (Estuary Flow>创建物化) 这里有一些难点。
-
首先,你应该使用蓝色按钮“从捕获中选择源”来选择源捕获,然后在“配置”中将 Pinecone 命名空间保持为空(Pinecone 的免费层必须使用空命名空间)。
-
第二步,在点击“下一步”后,在弹出的高级规格编辑器中,必须确保“bindings”字段不是空的。如果为空或该字段不存在,请根据以下截图填写内容,否则将无法发送任何数据到 Pinecone。此外,还需要使用你自己的集合路径更改“source”字段(与前面截图中的“target”相同)。如果点击“下一步”后出现错误,在看到编辑器之前,请再次点击“下一步”,你将看到高级规格编辑器。然后你可以指定“bindings”并点击“保存并发布”。这一步完成后,一切应该没问题。错误出现的原因是我们之前没有指定“bindings”。
-
如果在你发布一切并返回到 Destination 页面后,出现另一个错误信息,告诉你没有添加集合,只要你看到 OVERVIEW 的直方图中使用量不为零,就可以忽略它(见下图)。这个直方图基本上表示它向 Pinecone 发送了多少数据。

确保“bindings”字段像这样填写。— 来自Destinations的快照,来自 Estuary Flow(2024 年 4 月)
"bindings": [
{
"resource": {},
"source": "CJQ/mybookmark/bookmarks_v3",
"fields": {
"recommended": true
}
}
],

不要因错误而慌张,再次点击“下一步”。— 来自Destinations的快照,来自 Estuary Flow(2024 年 4 月)

确保 OVERVIEW 中的使用量不是空的。— 来自Destinations的快照,来自 Estuary Flow(2024 年 4 月)
7. (Pinecone>创建索引) Pinecone 已推出无服务器索引模式(免费,但 Estuary Flow 目前尚不支持),但我在这个项目中没有使用它。这里我们仍然使用基于 pod 的选项(自 2024 年 4 月 14 日以来不再免费),但它足以满足我们书签嵌入存储的需求。创建索引时,你只需要设置索引名称和维度。
8. (Pinecone>索引>{你的索引}) 完成 Pinecone 索引的创建后,并确保 Estuary Flow 中的物化(materialization)中正确填写了索引名称和环境,你就完成了。在 Pinecone 控制台中,转到 索引>{你的索引},你应该能看到显示你书签总数的向量数量。可能需要几分钟,直到 Pinecone 从 Estuary Flow 接收信息并显示正确的向量数量。

这里我有 402 个书签,因此向量数量显示为 402。— 来自Pinecone的快照(2024 年 4 月)
使用 Streamlit 和 Langchain 构建你自己的应用
代码:github.com/swsychen/BookmarkAI_App
我们快到了!最后一步是构建一个漂亮的界面,就像原版的 ChatGPT 一样。在这里,我们使用了一个非常方便的框架,叫做 Streamlit,利用它我们可以用几行代码就构建一个应用。Langchain 也是一个非常易于使用的框架,可以用最少的代码来操作任何大型语言模型。
我也为这个应用准备了代码。请按照 GitHub 链接中的安装和使用指南进行操作,享受吧!
代码的主要逻辑是:
获取用户提示 → 创建一个包含 ChatGPT 和 Pinecone 的检索链 → 将提示输入到链中并获取响应 → 将结果流式传输到 UI

代码的核心部分。— 来自作者的 vscode 快照。
请注意,由于 Langchain 仍在开发中,如果您使用的版本与 requirements.txt 中指定的版本不同,代码可能会被弃用。如果您想深入了解 Langchain 并使用其他 LLM 进行书签搜索,可以随时查看 Langchain 的官方文档。
结语
这是我写的第一篇教程文章。如果有任何不清楚的地方需要改进或澄清,欢迎留言。
如何在 5 分钟内构建自己的 Google AI 聊天机器人
充分利用 Google LLM 和你的私人知识的力量
·发表于 Towards Data Science ·14 分钟阅读 ·2024 年 2 月 11 日
--
· 问题
· 解决方案
· 使用案例
· 逐步指南
∘ 第 1 步:环境设置
∘ 第 2 步:准备私人知识并将其存储到 Google 云存储(低代码)
∘ 第 3 步:创建聊天机器人和聊天机器人背后的数据存储(无代码)
∘ 第 4 步:测试聊天机器人(无代码)
∘ 第 5 步:发布/集成聊天机器人(低代码)
∘ 第 6 步(可选):通过一个精美的应用发布它(低代码)
· 是什么让这变得“神奇”?
· 一些观察
· 总结
· 喜欢这个故事吗?
问题
你可能已经熟悉了由大语言模型(LLM)驱动的 AI 聊天,如 OpenAI ChatGPT 或 Google Bard。而且你可能注意到一件事——这些 LLM 拥有广泛的世界通用知识,但当你询问一个非常具体或专业的领域时,它们可能无法给出令人满意的答案,尤其是当这个领域的知识并不公开或不易共享时。
你是否想过将你的私人知识“赋予”LLM,并创建自己的聊天机器人?
你知道吗,这可以在 5 分钟内通过无代码或低代码完成?
最终产品将是这样的:

Github 链接:github.com/bianbianzhu/property-hunter
解决方案
在亚太地区的 Google Cloud 应用 AI 峰会期间,Alan Blount 来自 Google 分享了一个有趣的想法,利用 Google Cloud Vertex AI 搜索与对话实现这一目标,我觉得这个方法非常吸引人,值得尝试。
这个想法很简单,首先将一批私有知识文档放到 Google Cloud Storage 上:

然后创建一个数据存储,并将文档从 Cloud Storage 导入到数据存储中:

最后将数据存储接入 Dialogflow CX:

然后我们就完成了!
我们可以像这样测试聊天机器人:

如果我们想通过一个漂亮的应用程序发布它,Google 提供了一个公开的 Git 仓库,可以用于一个聊天应用程序。通过一点编程知识,我们可以将 Dialogflow 聊天机器人链接插入到这个聊天应用程序中,并自定义界面,像这样:

或者是这个:

使用案例
在这个案例中,假设我是一个电商网站的所有者。我想创建一个聊天机器人,让我的用户可以在网站上提问关于任何商品(价格、产品、服务、运输等)的具体问题。聊天机器人将获得“私有知识”并基于网站内容回答问题。
鉴于我并没有真正拥有一个电商网站,我将采用一种变通方法,从互联网上现有的网站抓取内容。这有些棘手,因为大多数网站在其使用条款中声明反爬虫,并且抓取电商网站如 Amazon、eBay、Alibaba 等可能是非法的。
ChatGPT 给了我一个完美的选项 —
要抓取的书籍 (
books.toscrape.com/)。这是一个专门为网页抓取练习设计的模拟书店。它提供了一个简单的结构,用于抓取书籍的详细信息,如标题、价格和评分。
在这个使用案例中,我假设我是这个 Books to Scrape 网站 的所有者,并基于该网站创建聊天机器人。
步骤指南
一开始可能看起来有些冗长,因为它涵盖了你所需要的每个详细步骤。一旦你跑通了,你就能在 5 分钟内完成相同的操作。
步骤 1:环境设置
我们将使用的工具位于 Google Vertex AI 上,我们需要一个 Google Cloud Platform (GCP) 账户。
Google 提供了一个 免费套餐程序,为新的 Google Cloud Platform (GCP) 用户提供为期 90 天的试用期,其中包括 $300 的免费云计费积分。
按照这里的教程设置免费的Google Cloud 账户。
在你设置好 Google Cloud 账户并能够访问控制台后,创建一个存储桶(分步指南在这里)供下一步使用。
步骤 2:准备私有知识并将其存储到 Google Cloud Storage(低代码)
如上所述,在此情况下的私有知识将是位于书店网站上的内容。
对于电商网站的所有者,你需要做的就是提供网站的 URL,Google 会自动从你定义的域名列表中抓取网站内容。
由于我不是实际的所有者,我将通过爬取解决此问题。来自 Google 的 Alan Blount 提供了一个非常有用的笔记本来实现这一点。所有的代码片段所做的就是从你指定的网站抓取网页并将其存储到你指定的 Google Cloud Storage 存储桶中。
这就是你需要做的全部:
2.1 将笔记本保存到你自己的云盘中
回想一下,在步骤 2 中,当你注册 Google Cloud 时,你创建了一个新的 Google 账户?你的 Google 账户将拥有Google Drive,你可以将这个笔记本保存到你的云盘中。
从“文件”下拉菜单中选择“在 Drive 中保存副本”选项

图片来自Google Colab 笔记本 由 Alan Blount 提供
然后,如果你进入Google Drive,你将能够看到你创建的笔记本。根据需要自由重命名它。

2.2 在你自己的笔记本中,定位以下内容并指定

图片来自 Google Colab 笔记本
website_url 指的是你想要抓取的网页 URL。
storage_bucket 指的是你在步骤 1 中创建的 Google Cloud Storage。
metadata_filename 指的是将与网页一起创建并存储的 JSON 文件。你可能需要通过将 applied_ai_summit_flutter_search 改为能够描述你的使用场景的名称来使其与网站相关。
这是我的版本:

图片来自 Google Colab 笔记本
2.3 运行所有

图片来自 Google Colab 笔记本
2.4 当系统提示你授权 Google Colab 笔记本访问你的 Google 凭证时,点击“允许”->“继续”

来自 Google Colab 笔记本的图像
然后,脚本应该会运行,并在底部显示抓取进度,就像这样:

来自 Google Colab 笔记本的图像
如果你查看 Google Cloud 存储桶,你会看到这些 HTML 文件被正确抓取并存储在你的存储桶中:

来自 Google Cloud 控制台的图像
需要注意的是,代码片段并非为所有用例设计,你可能需要对代码进行一些微调,以达到你的目标。
例如,在我的案例中,我通过修改代码进行了一些调整
blob.upload_from_string(html_string)
转换为
blob.upload_from_string(html_string, content_type='text/html')
默认情况下,html_string 将作为 text/plain 上传。通过将其改为 text/html,我希望能够在后续阶段正确显示这些 HTML 内容。
你可以根据需要调整代码。
步骤 3:创建聊天机器人以及其背后的数据存储(无需代码)
前往 Google Cloud 控制台 (console.cloud.google.com/) 并输入“搜索与对话”作为服务:

创建“新应用”:

来自 Google Cloud 控制台的图像
选择“聊天”:

来自 Google Cloud 控制台的图像
提供你的“公司名称”和“代理名称”。请注意,这里的“代理名称”将是聊天机器人的名称,你可能想为你的用户取个好名字。

来自 Google Cloud 控制台的图像
在此“数据”页面,选择“创建新数据存储”:

来自 Google Cloud 控制台的图像
对于电子商务网站的拥有者,选择“网站 URL”并配置你的网站 URL
由于我已经将网站内容抓取到 Cloud Storage,我们可以在此选择“Cloud Storage”:

来自 Google Cloud 控制台的图像
指定 Cloud Storage 存储桶名称,并在下方选择“非结构化文档”:

来自 Google Cloud 控制台的图像
给你的数据存储命名,然后点击“创建”

来自 Google Cloud 控制台的图像
你会看到数据存储列出,然后点击“创建”

来自 Google Cloud 控制台的图像
你的数据存储将如下所示创建

来自 Google Cloud 控制台的图像
如果你点击进去,你会看到你的数据存储正在“处理数据”,并从我们之前指定的 Cloud Storage 存储桶中导入文档:

来自 Google Cloud 控制台的图像
如果我们点击“ACTIVITY”标签,我们可以看到导入正在进行中:

来自 Google Cloud 控制台的图片
导入过程可能需要几分钟到几小时,具体取决于你 Cloud Storage 存储桶中的文档数量。
在我的情况下,我有超过 1000 个文件,几分钟内就完成了。
导入完成后,突出显示的状态已发生变化:

来自 Google Cloud 控制台的图片
如果你切换回“文档”标签,你将看到导入到数据存储中的文件列表:

来自 Google Cloud 控制台的图片
这意味着你已经准备好所有材料,可以开始工作了!
步骤 4:测试聊天机器人(无代码)
在上面的第 3 步中,我们已经创建了一个聊天机器人应用程序以及其背后的数据存储。
点击顶部的“应用”:

来自 Google Cloud 控制台的图片
你将看到你在第 3 步中创建的聊天机器人:

来自 Google Cloud 控制台的图片
如果你点击聊天机器人名称,你将被引导到类似下面的 Dialogflow CX 页面:

来自 Google Cloud 控制台的图片
要测试聊天机器人,选择右上角的“测试代理”:

来自 Google Cloud 控制台的图片
然后对话框会弹出:

来自 Google Cloud 控制台的图片
你可以通过说“hi”并开始向聊天机器人提问来启动对话:

来自 Google Cloud 控制台的图片
它工作正常!
步骤 5:发布 / 集成你的聊天机器人(低代码)
如果你对聊天机器人满意,可以轻松地将其与你的 web 应用程序集成
进入左侧面板,选择“管理” -> “集成” -> “Dialogflow Messenger”

来自 Google Cloud 控制台的图片
你可以根据自己的需求选择 API 类型和 UI 风格
为了演示,我选择了“未认证的 API”作为 API 和“弹出式”作为 UI 风格:

来自 Google Cloud 控制台的图片
选择“完成”后,下一页会生成如下的 HTML 代码片段:

来自 Google Cloud 控制台的图片
你可以复制代码片段并轻松将其粘贴到你的应用程序中进行集成。
为了演示,我将这个 HTML 代码片段复制并粘贴到 JSFiddle 中运行,然后我看到我的小聊天机器人在右下角正常工作!

来自 JSFiddle 的图片
步骤 6(可选):通过美观的应用程序发布(低代码)
如果您还没有应用程序并希望创建一个,Google 提供了一个很好的起点,通过一个公开的 Git 仓库 聊天应用。
这是一个用 Node.js 编写的聊天机器人应用程序,您可以通过稍微修改 chat-app/src/routes/+page.svelte 中的代码片段,轻松将其适配到您自己的需求。
您需要将 project-id、agent-id 和 chat-title 更改为您自己的。

该图片来自 Git 仓库 github.com/GoogleCloudPlatform/generative-ai/tree/main/conversation/chat-ap
一旦您运行/部署该应用程序,您将获得如下所示的 Web UI:

该图片来自 Git 仓库 github.com/GoogleCloudPlatform/generative-ai/tree/main/conversation/chat-app
当然,您可以根据需要更改 UI 的外观。
现在,您可以拥有自己的应用程序了!
是什么让这个“魔法”得以实现?
回想我们在开头提到的解决方案设计。看起来有点像魔法,因为您只需要将您的私人知识提供给 Google Cloud Storage 存储桶,就能轻松获得自己的 LLM 驱动的聊天机器人。

这一切的实现得益于 Google 在幕后做了大量的集成工作,通过将 Vertex AI 平台 与聊天机器人代理服务 Dialogflow CX 集成,并推出了一个新的抽象层,称为 Vertex AI 对话(前身为 Gen 应用构建器)。这个新的抽象层还支持 搜索 和 推荐,该服务的全名为“Vertex AI 搜索与对话”。
正如我们所见,这种新的“Vertex AI 搜索与对话”的抽象位于 Vertex AI 之上,后者协调了多个基础模型,并通过用户提供的最新真实世界信息进行“增强”,使其能够将这些信息纳入其回应的上下文。

该图片来自 Google Cloud CEO 的演讲幻灯片 生成式 AI:开发者的下一个前沿
该集成非常出色,因为它可以帮助至少两类人群 —
-
传统的聊天机器人构建者,以及
-
人们在探索生成式 AI 解决方案,但还未找到合适的应用场景
想象你是一个使用 Dialogflow CX 的传统聊天机器人构建者,你在创建页面、意图和路由,将客户意图路由到相应的页面。基本上,你在定义“如果客户说这个,那么我用这个回应”,这有点像硬编码。现在,谷歌将 Vertex AI 接入进来,它可以利用 LLM 模型(例如 text-bison、gemini)生成智能的代理回应,并控制对话流程。这可以显著减少代理设计时间,并提高代理质量。
另一方面,想象你在探索 LLM 和生成式 AI 的强大功能,但不确定如何使用它。这个Vertex AI 对话功能可以让你轻松构建并快速启动自己的聊天机器人应用,并使其适用于实际用例。这可以显著缩短 LLM 和生成 AI 解决方案的上市时间。
一些观察
尽管看起来有些“魔力”,我们观察到一些值得与考虑使用“Vertex AI 搜索与对话”功能的开发者分享的事项。
我们的直觉是,这是谷歌通过“整合”几个现有工具推出的一款新产品,并且仍在努力使其变得更好。集成如何在幕后发生,以及开发者如何理解和配置它,仍然不够清晰。
我很快就得到了我们的聊天机器人,但一旦开始查看如何微调它,我花了相当多的时间来弄明白 Dialogflow CX 是如何工作的,什么是“生成器”,以及它是如何工作的。此刻,我仍然困惑为什么这个聊天机器人在我甚至没有配置任何谷歌文档中描述的“生成器”的情况下运作得如此出色,是否/如何通过使用“生成器”能使它变得更好。
开发过程中还有一些其他观察:
-
索引一个网站或一组文档可能需要几分钟或几天,具体取决于数据量。对此过程所需时间没有明确估计,开发者能做的就是等待并定期检查。
-
我们知道如何将数据存储与聊天机器人应用连接,但看起来我们无法“断开”连接。
-
尽管有一定的基础,用户提供的数据质量仍然会显著影响聊天机器人的性能。“垃圾进,垃圾出”依然在很大程度上适用。
-
通过提供私人数据和知识来“增强”可以解决大型语言模型(LLM)的一个问题——缺乏更新的现实世界信息。但幻觉问题依然存在,因为有时聊天机器人可能会给出“虚假”信息(当然,这取决于你提供的私人知识的数据质量)。
-
聊天机器人在与用户聊天时会提供相关网页/文档页(例如 PDF)的链接。这很棒,但聊天机器人提供的链接是 Google Cloud Storage 授权的 URL,仅能被具有授权权限的用户访问。开发人员需要找出如何将这些 URL 转换为签名 URL,这样可以安全地与公众匿名用户分享,而不是使用 Google Cloud Storage 授权的 URL。
-
支撑聊天机器人的数据存储最适合处理非结构化数据。对于结构化数据,它支持连接到 CSV 格式的结构化数据,但必须采用 Google 文档中提到的“问题”和“答案”格式:

图片来自 Google Cloud 控制台

图片来自 Google Cloud Dialogflow 指南
总结
在上述用例中,我假设自己是一家在线书店的老板,并根据我的电子商务网站内容(HTML 格式)创建了一个聊天机器人。
同样,您可以将“私人知识”以 博客、文件(例如 PDF、HTML、TXT) 和 各种网站 的格式提供给 Google Cloud Storage,并创建自己的聊天机器人。
这使得个人/企业能够充分利用 Google LLMs(如 text-bison、gemini 等)的强大功能,将其与私人知识结合,并以非常快速的方式创建自己的聊天机器人。
本文到此结束。希望您觉得它有帮助!
(附言:我正在制作一段视频,以便让这个逐步指南更易于跟随。如果完成的话,我会在不久的将来分享。)
喜欢这个故事吗?
Selina Li (Selina Li, LinkedIn) 是一位首席数据工程师,现工作于澳大利亚墨尔本的Officeworks。Selina 热衷于人工智能/机器学习、数据工程和投资。
Jason Li (Tianyi Li, LinkedIn) 是一位全栈开发工程师,现工作于澳大利亚墨尔本的Mindset Health。Jason 热衷于人工智能、前端开发以及与太空相关的技术。
Selina 和 Jason 很乐意探索各种技术,帮助人们实现他们的目标。
除非另有说明,所有图片均由作者提供。
如何为成功的数据科学职业生涯构建自己的路线图
·发表于 Towards Data Science ·发送为 电子通讯 ·4 分钟阅读·2024 年 9 月 19 日
--
是否有了写下你第一篇 TDS 文章的灵感?我们始终欢迎新作者的投稿。
在过去几年中,获得你的第一个数据科学或机器学习工作的过程和要求发生了显著变化。你在现有职位上取得卓越表现的定义也发生了变化。我们可以将此归因于许多因素:大语言模型(LLMs)和人工智能工具的崛起、不利的经济状况(以及随之而来的裁员和招聘冻结)以及远程工作环境的变化等都浮现在脑海中。
过渡期和不确定性时期可能很难应对,尤其是当你进入这个领域时,曾期待在这个蓬勃发展的、高回报的行业中顺利前行。然而,没有理由绝望:个体的数据专业人员可能无法单独扭转局面,但他们可以采取行动,变得更加职业韧性强,并为自己的职业轨迹提供抗冲击能力。
本周我们为你精选的文章集中在你应当发展的核心技能,以便在面对不可预测的趋势时能变得更加免疫——并列出了你可以采取的具体步骤来培养这些技能。从为近期毕业生提供的如何抓住第一份实习的建议,到如何有效管理数据团队的见解,它们面向各类从业者,无论是职位新手还是资深专家。让我们深入了解一下。
-
我将在 2024 年如何学习成为一名数据分析师 “每天,我都会在 LinkedIn 收件箱里收到数十条来自候选人的信息,尽管他们已经获得了必要的分析技能,但还是难以找到工作。”Natassha Selvaraj回顾了自 2020 年开始作为数据分析师以来,她在招聘过程中所见到的变化,并分享了对于希望更新方法、在当前环境中脱颖而出并取得成功的求职者的一些有用建议。
-
通向成功的路径:如何获得机器学习和数据科学实习机会 迈出职业生涯的第一步往往是最艰难的——在竞争激烈的就业市场中更是如此。Sara Nóbrega刚刚完成了两次实习,拥有最新的第一手经验,分享了她如何克服这一初期障碍,并通过一份结构清晰、内容全面的指南为求职者提供了宝贵的经验。
-
作为数据科学家个体贡献者如何请求反馈 即使你已经有了几年职业经验,依然总有成长的机会——以及与公司上下层同事培养更强沟通习惯的机会。Jose Parreño为那些希望获得建设性、宝贵且具体反馈的个体贡献者提供了详细的建议,并包含了数十个可以根据自身需求调整的示例问题。
-
领导数据科学团队走向成功 变动的截止日期、多学科的队友、技术复杂性……领导一个数据科学项目需要跟踪众多动态的部分,但正如Hans Christian Ekne所解释的那样,经理可以利用多种技巧来确保项目顺利进行:“应对这些问题的关键在于了解每个团队成员的能力、弱点和优势,做好规划,并专注于目标的实现。”

图片来自Sandra Grünewald在Unsplash上的作品
准备探索一些其他话题吗?以下是过去几周中的几篇亮眼文章:
-
如果你曾经考虑过为开源项目做贡献,不要错过Siavash Yasini对他最近经历的反思以及他在此过程中学到的实用经验。
-
在一篇详细且耐心解释的指南中,Xichu Zhang拆解了泰勒级数的内部机制和其基础数学原理。
-
Ian Xiao在数据科学与商业战略交汇处的写作中,他的最新文章探讨了一个棘手的问题:为什么如此多的客户个性化项目失败?
-
如果你想深入了解当前关于 AI 支持残障人士潜力的辩论(及研究)及其如何解决各种可访问性问题的内容,别错过Stephanie Kirmer的概述,其中还讨论了数据隐私和文化抹除等领域的权衡。
-
跟进最新的时间序列预测进展 — Marco Peixeiro深入探讨了可逆 KAN 混合模型(RMoK)的细节,分析了其架构,并通过 Python 演示了它的强大功能。
-
要让 AI 代理“记住”需要什么?基于她自己的实验,Sandi Besen回报了关于代理在处理简单和复杂记忆任务中的潜力和局限性的研究,并概述了应对一些主要挑战的有效方法。
感谢您支持我们作者的工作!正如我们之前提到的,我们非常喜欢发表新作者的文章,因此,如果您最近写了一篇有趣的项目教程、教程或关于我们核心话题的理论反思,请毫不犹豫地与我们分享。
直到下一期 Variable,
TDS 团队
如何计算 DAX 中多个周期的移动平均
在 DAX 中计算移动聚合是很简单的。然而,当计算跨时间的移动平均时,会有一些陷阱。由于其中一些陷阱涉及定义的问题,我们必须小心不要选择错误的方法。让我们来看看具体细节。
·发表于 Towards Data Science ·8 分钟阅读·2024 年 10 月 1 日
--

图片由 Antoine Dautry 提供,来源 Unsplash
首先,先来做一些数学计算。
计算平均数很简单:将值的总和除以实例的数量。
虽然值的总和很容易计算,但实例的数量并不像你想象的那样简单。
例如,让我们看一下以下的表格:

图 1 — 数字列表(图源:作者)
计算值列的平均数很简单:
<值的总和> / <行数> = 534.68 / 10 = 53.47
现在,让我们删除一个值,这会改变结果。

图 2 — 带有间隙的数字列表(图源:作者)
突然间,我有两种计算平均数的方法:
<值的总和> / <值的数量> = 547.23 / 9 = 60.8
或者
<值的总和> / <行数> = 547.23 / 10 = 54.72
第二种方法仅仅是不同的定义。
比如,假设第一列是客户 ID,我想计算所有客户的平均销售额或活跃客户的数量等情况,在这种情况下,第二种计算平均数的方法可能是更好的选择。
现在,让我们将其转换为 DAX。
移动聚合 — 起点
首先,让我们构建移动聚合的典型案例。
我想要获取过去四个月的移动销售数据。
目前,我使用 SUM() 的销售数据,因为比起计算平均值,验证四个月的结果要容易得多。
Sales Moving Sum =
VAR MaxDate = MAX( 'Date'[Date] )
VAR MinDate =
CALCULATE(
MIN('Date'[Date])
, DATEADD( 'Date'[Date], - 3, MONTH )
)
VAR DateRange =
CALCULATETABLE(
DATESBETWEEN( 'Date'[Date]
,MinDate
,MaxDate
)
)
VAR Result = CALCULATE([Sum Online Sales]
,DateRange
)
RETURN
Result
首先,我获取当前筛选上下文(例如当前月份)的最后日期
第二,我使用 DATEADD() 来回退三个月。我只回退三个月,因为我包括了当前月。
如果我想排除当前月份,我必须以不同的方式操作。在这种情况下,我需要获取第一个日期,并回退一天来获取上个月的最后日期(或者使用 [EOMONTH(MAX(‘Date’Date), -1) ),然后使用 DATEADD() 回退四个月)。
第三,我使用 DATESBETWEEN() 获取两个变量之间的日期列表。
最后,我将日期列表传递给 CALCULATE() 来返回最终结果。
这是结果:

图 3 — 四个月移动总和的表格(图由作者提供)
我可以通过移除 DATESBETWEEN() 函数,并将两个变量直接传递给 CALCULATE() 来简化度量值:
VAR MaxDate = MAX( 'Date'[Date] )
VAR MinDate =
CALCULATE(
MIN('Date'[Date])
, DATEADD( 'Date'[Date], - 1, MONTH )
)
VAR Result = CALCULATE([Sum Online Sales]
,'Date'[Date] >= MinDate
&& 'Date'[Date] <= MaxDate
)
RETURN
Result
结果是相同的,但使用 DATESBETWEEN() 的性能略好(我的事实表中有 1700 万行数据)。
由于多种因素可能会影响性能,我建议你使用自己的数据和使用场景尝试这两种变体,并检查差异。
你可以在这里阅读更多关于在 Power BI 中衡量性能的内容:
## 如何通过 DAX Studio 从 Power BI 获取性能数据
有时我们会遇到报告加载缓慢的情况,我们需要找出原因。接下来我们将看到如何收集性能数据,以及……
[towardsdatascience.com
我们来做平均值
现在,最终,我将开始计算平均值。
我使用与我的 Sum Online Sales 度量值相同的逻辑,并使用 AVERAGEX() 来计算平均销售额:
Average Online Sales = AVERAGEX('Online Sales'
,('Online Sales'[UnitPrice] * 'Online Sales'[SalesQuantity]) - 'Online Sales'[DiscountAmount]
)
接下来,我复制了上述度量值来计算销售移动平均值,结果如下:

图 4 — 基本平均值和移动平均值的结果(图由作者提供)
我可以在这里结束并写道,“任务完成。”但请停一下。
在开始时,我提到了计算平均值的不同方法。
所以,我开始编写度量值来进行测试。
我编写了以下度量值,作为分母,而使用了[Sum Online Sales]作为分子:
-
计算在线销售数量 = COUNTROWS(‘Online Sales’)
-
客户数量 = DISTINCTCOUNT(‘Online Sales’[CustomerKey])
-
在线订单数量 = DISTINCTCOUNT(‘Online Sales’[SalesOrderNumber])
结果变量的代码如下(以[在线销售计数]为例):
VAR Result = CALCULATE([Sum Online Sales] / [Count Online Sales]
,DateRange
)
我可以想到更多的变体(例如,计算所有客户的平均值,包括那些在该期间没有订单的客户)。但我决定在此停止,以避免混淆。
毫不奇怪,每个都得出了不同的结果:

图 5 — 所有变体的平均值结果(图表由作者提供)
结果之间的差异非常明显。
你可能会遇到其他计算平均值的方法。
因此,我强烈建议你清楚地定义如何计算平均值。否则,你可能会向你的受众提供意外的甚至错误的结果。
按月销售额的平均值
计算平均值时还有一种变体:作为按月销售额的平均值。
让我们再看一下包含月度销售额的结果:

图 6 — 仅月度销售额(图表由作者提供)
我想计算四个月的月度销售平均值。
例如,在九月,我想计算六月、七月、八月和九月的月度销售平均值:
(275’061’552.33 + 303’302’950.82 + 273’004’268.56 + 262’971’889.59) / 4 = ~278’585’165.3
(~ 因为由于四舍五入差异,我们可能会得到稍有不同的结果)
为了满足这个需求,我必须思考如何做。
我需要提前计算每个月的销售额。然后仅考虑每行所需的四个月。最后,计算这四个值的平均值。
这意味着我必须首先生成一个包含所有月度结果的表格,并仅使用计算平均值所需的值,这样效率低下。然后 Power BI 会为表格中的每一行计算这个值,以可视化结果。
当我从每行的过滤器上下文的角度来看时,我可以做得更好。
为什么不只为每行在可视化中相关的那些月份计算销售总和呢?
基于这种方法,我编写了以下度量:
Moving Average by Month =
// 1\. Get the first and last Date for the current Filter Context
VAR MaxDate = MAX( 'Date'[Date] )
VAR MinDate =
CALCULATE(
MIN('Date'[Date])
, DATEADD( 'Date'[Date], - 3, MONTH )
)
// 2\. Generate the Date range needed for the Moving average (Four months)
VAR DateRange =
CALCULATETABLE(
DATESBETWEEN( 'Date'[Date]
,MinDate
,MaxDate
)
)
// 3\. Generate a table filtered by the Date Range generated at step 2
// This table contains only four rows
VAR SalesByMonth =
CALCULATETABLE(
SUMMARIZECOLUMNS(
'Date'[MonthKey]
, "#Sales", [Sum Online Sales]
)
,DateRange
)
RETURN
// 4\. Calculate the Average over the four values in the table generated in Step 3
AVERAGEX(SalesByMonth, [#Sales])
这次,我添加了内联注释来解释那里的情况。
结果如下:

图 7 — 按月平均值的结果(图表由作者提供)
我在 Excel 中检查了结果,它们是正确的。
如果你正在考虑为每个月创建一个预计算表格,再考虑一下吧。
你将被迫向所有维度添加引用,并增加数据的粒度,直到你不得不编写这个度量来满足受众按所有维度筛选数据的需求。
这个解决方案非常高效,因为计算结果花费的时间不到 0.4 秒。
即使扩展所有月份,计算结果也不需要更多的时间。
顺便提一下,这种方法在计算平均值的平均数时也很适用。
结论
平均值不等于平均值。我想这点很清楚。
但更重要的是,必须理解应该计算什么,以及如何计算。
理解为什么需要计算一个数字,有助于你选择正确的计算逻辑。
当逻辑清晰时,编写 DAX 代码的方法应当被确定。记住,从筛选上下文的角度来做。有时这可能不直观,但它将有助于开发高效的代码。
希望你今天学到了新东西。
下次见。

图片由Tim Mossholder拍摄,来自Unsplash
参考文献
像我之前的文章一样,我使用的是 Contoso 示例数据集。你可以从微软这里免费下载 ContosoRetailDW 数据集。
如这里所述,Contoso 数据可以在 MIT 许可下自由使用。
我更改了数据集,将数据移动到当代日期。
[## 当 Salvatore Cagliari 发布新文章时,获取电子邮件通知。
当 Salvatore Cagliari 发布新文章时,你会收到电子邮件通知。通过注册,你将创建一个 Medium 账户,如果你还没有的话……
medium.com](https://medium.com/@salvatorecagliari/subscribe?source=post_page-----2a6a8105850a--------------------------------)
尽管 Medium 设置了付费墙,我仍让我的文章对所有人可读。这让我能从每个读者那里赚取一些费用,但我关闭了它,所以你可以免费阅读我的文章。
我在晚上和周末写这些文章,这是一项繁重的工作。
你可以通过以下方式支持我的工作:
buymeacoffee.com/salvatorecagliari
或扫描此二维码:

任何支持都将不胜感激,并帮助我找到更多时间为你创作更多内容。
非常感谢。
如何计算 PPC 营销中竞价的弹性
价格弹性是计量经济学的核心内容。那么,我们如何将弹性应用于竞价呢?
·发表于Towards Data Science ·9 分钟阅读·2024 年 10 月 3 日
--

图像由 Dall-E 创作
价格弹性是计量经济学的核心内容。通常你听到的是“需求弹性”,其中你试图理解如果我们提高价格一定幅度,需求会增加多少。在竞价生态系统中,市场营销人员尝试理解如果我们将竞价提高 X%,会获得多少“点击”。在本文中,我将向你展示如何在 PPC 营销中计算竞价的弹性。
PS 1: 本文中的例子来源于我管理的 100 多个活动中的 1 个 Skyscanner。为了保护共享信息,我已移除所有标识符,但你将能够看到如何利用真实分布数据计算出竞价的弹性。
PS 2: 所有图片除非另有说明,均由我本人创作。
供给弹性
在深入计算竞价弹性之前,让我们回到计量经济学 101,了解什么是供给弹性。直接引用这篇文章:“价格弹性供给表示生产者在价格变化时调整生产水平的速度。” [1]如…
如何挑战自己的分析,避免他人挑战
掌握合理性检查的艺术,提升你的工作质量
·发表于 Towards Data Science ·12 分钟阅读·2024 年 7 月 3 日
--

作者提供的图片
你是否曾经做过一个分析,然后被经理一顿批评?或者在演讲中被问到一个问题,让你觉得“为什么我之前没有检查这个?”
有时候,你会觉得经理和高管们有一种神奇的能力,能够迅速找出你工作中的一个薄弱环节。他们是如何如此迅速地发现问题的,尤其是当他们是第一次看到你的工作时?
看似像超能力的东西其实是任何人都能学会的,这篇文章将告诉你如何做到。
通过定期对你的工作进行“合理性检查”,你可以主动识别出薄弱环节,并在与更广泛的受众分享之前确保结果是合乎逻辑的。
我将要讲解:
-
什么是合理性检查,它为什么重要
-
合理性检查与大多数人检查工作的方式有何不同
-
如何进行合理性检查
-
如何使用合理性检查来提升你的可信度
-
如何利用人工智能来为你的工作进行合理性检查
我们有很多内容要覆盖,所以让我们开始吧。
什么是合理性检查,为什么它很重要?
想象一下你正在从零开始构建一个详细的模型,仔细选择每一个假设并将它们结合起来,最终得出你的输出(例如:预测、公司估值等)。
每个假设看起来都合情合理,你检查了两次数学运算,所以输出应该是可靠的。对吧?对吧??
在过去十年的经验中,我发现我们在构建模型或进行分析时,常常会忽略整体,只关注局部。我们将太多的假设叠加在一起,以至于最终的结果会迅速从合理变得荒谬。
这就是健全性检查的作用:健全性检查帮助我们判断我们分析结果的正确性。
我们都会犯错,偶尔会有。没关系;现实并不总是按照我们预期的方式发展。但你应该尽量做到大多数时候都正确。
让我们深入了解如何做到这一点。
[## 每当 Torsten Walbaum 发布文章时,获取一封电子邮件。
每当 Torsten Walbaum 发布文章时,获取一封电子邮件。通过注册,如果你还没有 Medium 账户,你将创建一个…
健全性检查与其他方法有什么不同?
在检查我们的工作时,大多数时候我们是逐步检查的,看看是否有错误。单元格是否正确链接?我是否把公式拉到底部?我的 SQL 中的所有连接是否正确?

作者提供的图片
这种机械的“质量控制”方法可以帮助我们发现问题,但它并不能确保输出结果符合直观的商业逻辑。
而“健全性检查”则是从另一个角度回顾并验证输出结果。如果你通过两种方式得出相同的结论,你可以对自己的工作更有信心。
如何进行健全性检查
健全性检查有三大类:自下而上与自上而下、基准测试和直觉。我将详细介绍每一类,并展示如何在工作中应用它们。
自下而上与自上而下
我们的分析通常是自上而下或者自下而上。那么这是什么意思呢?

作者提供的图片
让我们看一个(简化的)例子。假设你在一家 B2B SaaS 公司工作,想要了解你能够为即将推出的新产品吸引多少客户。
-
在自上而下的方法中,我们试图了解我们能赢得多少市场份额。因此,我们会从查看美国总企业数量开始,排除我们不针对的行业和我们无法支持的公司规模,假设有多少比例的公司正在寻求更换软件供应商,最后假设我们能够赢得这些公司中多少比例(与我们的竞争对手相比)。
-
在自下而上的方法中,我们试图根据我们可用的渠道了解能吸引多少公司。因此,我们会查看以往的发布,看看从 LinkedIn 获得多少潜在客户,分析关键词来确定预期的 SEM 量,基于我们可以瞄准的公司数量和预期的转化率等来预测电子邮件潜在客户数量。
这两种方法都能为我们提供一个大致的方向感,但它们各自都有一个致命的弱点。
自上而下的方法并未考虑我们将如何获取这些客户,而自下而上的方法忽略了目标市场的规模。
因此,确保你的工作合理性检查的最佳方式是结合自上而下与自下而上的分析。

作者提供的图片
基准对比
确保一个计划或预测合理的最佳方式是将其与基准进行对比。例如,如果你正在预测一个新市场的表现,比较其与类似国家的过去发布会有所帮助。
如果你的分析大幅偏离基准,你需要能够解释为什么。
你应该检查任何模型、预测或预测中的几个常见事项:
-
规模: 最终结果与基准如何对比?例如,你是否预测法国会成为公司比英国更大的市场?
-
增长假设: 你预测的趋势是什么?例如,新的产品预计相比过去的发布会增长得更快吗?
-
季节性: 你的预测是否显示出与基准相同的重复模式?例如,如果其他市场在 12 月假期期间出现放缓,你为什么预测新国家不会有这个趋势?
这并不意味着你总是必须按照基准来建模;但你总是需要能够解释为什么会有所偏离。
基准对比示例 1:新市场发布
场景: 你计划进入英国市场,并预测用户增长
✅ 合理性检查: 你将预测与最近的两次发布(德国和法国)进行比较。你的预测比最近两次发布更加激进,并且没有显示任何季节性波动。
❓ 你需要能够回答的问题:
-
什么让你有信心认为这是可能的?英国市场是否在结构上有所不同(例如,市场更大)?我们的产品是否更适合英国市场?我们是否采用了不同的市场进入策略?
-
为什么法国没有季节性波动?假期期间是否有所不同?B2B 买家的季节性购买模式是否不同?
👉 如果你无法充分说明新市场与过去发布的差异,最好将预测保持一致。
基准对比示例 2:营销计划
场景: 你正在预测按渠道划分的营销支出和表现指标。计划是将营销预算按年翻倍。
✅ 合理性检查: 你将预测的营销效率与过去的趋势进行比较。你的预测显示,随着我们增加营销投入,效率(每条潜在客户成本)会提高,但过去的数据却显示出相反的趋势。
❓ 你需要能够回答的问题:
-
为什么你期望效率更好?
-
我们在每个营销渠道中部署了哪些具体改进将推动这一变化?
-
我们是否做了任何能够改善整体营销表现的事情,例如投资品牌建设?
👉 如果你没有明确的计划来提高营销效率,你应该假设历史上支出与效率之间的关系依然有效。
与直觉对比
很多时候,你可以用常识来进行理智检查。你的直觉并不总是正确的,但它通常能揭示需要进一步验证的潜在问题。
以下是一些例子:
-
你建立了一个贴现现金流(DCF)财务模型,而终端增长率远高于国内生产总值(GDP);这意味着你隐含假设公司将永远超越更广泛的经济。这合理吗?
-
你正在建立一个账户评分模型,而该模型将销售认为是浪费时间的公司评为“适合”。这并不意味着销售是对的(毕竟,你建立模型是为了发现新的见解),但你应该考虑他们的经验,因为违背直觉的结果往往揭示了模型的弱点
使用理智检查来提高你的可信度
理智检查不仅是为了防止别人挑毛病。它们还是一个可以提升你可信度的工具。
不要仅仅在幕后完成这些工作,然后再分享改进后的成果,而是将理智检查的过程一并分享。通过展示你如何验证分析结果,你将赢得听众的信任。如果你不分享你做的理智检查,听众就不得不假设他们需要当场检查你的工作。
你可以在幻灯片上通过展示上下游方法如何得出相同结果,或者将你的数据与基准进行对比,来直观地完成这一过程。
但你也可以通过口头方式进行:
✅ “我们计划到十月在英国增加到 50 个邮件潜在客户;这个目标基于与加拿大相似的转化假设,并对应于我们总可寻址市场中 3% 的每月渗透率。
如何使用 AI 来进行理智检查
理智检查可能非常耗时;毕竟,你需要从多个角度来解决同一个问题。幸运的是,AI 工具可以为你节省大量时间。
这不是替代你理智检查技能的工具;ChatGPT 需要你的指导才能做好工作,因此你仍然需要知道如何进行有力的理智检查。AI 的任务只是为你完成繁重的工作,并提出一些你可能遗漏的点。
这里有一个逐步指南,讲解如何使用 ChatGPT 完成这个过程;所有截图来自我与 ChatGPT 的实际对话,我在对话中要求 ChatGPT 对我的新市场发布预测进行理智检查。
免责声明:在上传任何专有数据之前,请始终检查你雇主关于使用像 ChatGPT 这样的 AI 工具的政策。
第 1 步:将你的工作上传到 ChatGPT
第一步是上传你希望 ChatGPT 进行理智检查的工作。ChatGPT 可以处理多种文件类型,包括 PDF、Excel、CSV 文件等。
你还可以直接与多个工具集成;例如,在这个例子中,我链接了包含我的预测的 Google 表格:

作者提供的图片
即使你的实际模型不在电子表格中(例如在 Python 中),我也建议将输出导入 Google Sheets 进行合理性检查;毕竟,你想要的是 ChatGPT 验证你的输出,而不是验证你模型的机制。
对于这个示例,我给了 ChatGPT 一个简单的市场推广预测,用于新国家的市场推广启动(你可以复制该文件并进行自己的合理性检查)。
我经历了四次合理性检查;以下是日志:
-
第一次尝试和第二次尝试还可以,但我觉得我需要提供很多指导,并且并不总是能得到我想要的结果。
-
第三次尝试相当不错,但 ChatGPT 和我都忘记了深入研究市场渠道组合(几天后我想起来时,ChatGPT 无法从我们停下的地方继续)。
-
第四次尝试很有前景,但在重新预测以包含季节性时,最初调整了错误的月份。
你可能需要尝试几次,直到得到一个非常好的表现。记住:
不要盲目使用 AI 生成的任何内容;它可能(并且确实会)犯错误,最终结果由你负责。AI 可以提供有用的输入并节省时间,但它不能替代批判性思维。
第 2 步:写一个提示,要求 ChatGPT 进行合理性检查。
在读取你的文件后,你需要写一个提示,要求 ChatGPT 对你的工作进行合理性检查。
这是我用来合理性检查我上面链接的市场推广计划的内容:

作者提供的图片
我发现,提供一些数据集的背景信息是有帮助的(尽管不是绝对必要的)。另外,别忘了说“请”和“谢谢”,以防 AI 变得有意识;这样可以避免你进入“坏孩子”名单。
你也可以给 ChatGPT 这篇文章,或者另一篇关于如何进行合理性检查的总结,这样就不需要在提示中包含太多的说明。
第 3 步:确保 ChatGPT 正确读取了数据。
在读取文件后,ChatGPT 通常会简要总结它所看到的内容以及它认为数据所代表的含义,并列出它进行分析的步骤:

来自我的第三次尝试截图;图片来源:作者
ChatGPT 还经常在聊天中重新陈述一些数据,这有助于确保它正确提取了数据:

来自我的第三次尝试截图;图片来源:作者
备注: 最初,ChatGPT 在处理我的电子表格时遇到了一些问题。以下是如何进行故障排除:
-
如果 ChatGPT 出现错误或显然没有正确获取数据,你可以通过点击 ♻️ 图标要求它重新处理。
-
如果它在处理你的文件时反复出现问题,你可能需要清理文件。例如,我发现如果我删除了非必要的行和列(例如章节标题、注释等),错误会显著减少。如果文件只包含相关的表格,ChatGPT 更容易将它们转换为 Python 中的数据框。给列和行提供描述性标题也有助于 ChatGPT 理解数据。
步骤 4:与 ChatGPT 一起进行合理性检查
接下来,ChatGPT 会开始提供一些初步观察,像这样:

来自我的第三次尝试截图;图片来源:作者
ChatGPT 正确识别出该预测是保守的,并且最终会追赶上英国,尽管开始时比较慢。如果它能够拉取一些统计数据来进行验证,比如每个国家的小型企业数量,那会更好,但根据我的经验,你需要明确要求它这样做。
有时它还会主动可视化关键趋势;其他时候,你需要提示它做这件事。
以下是对话的继续部分:

来自我的第三次尝试截图;图片来源:作者
很高兴看到 ChatGPT 能够从单独的标签页中提取启动日期,并根据启动日期为每个国家绘制性能图表。
然后我们开始讨论季节性;正如你所看到的,我需要提供初步的推动,并且进行了几轮反复对话,但 ChatGPT 完成了识别正确模式的工作:

来自我的第三次尝试截图;图片来源:作者
在我的几次尝试中,ChatGPT 还简要地总结了市场营销组合的要点,像这样:

来自我的第四次尝试截图;图片来源:作者
我认为总体上这是可以的。
它正确地强调了有机营销和电子邮件营销是值得进一步关注的关键领域。不幸的是,它也得出了一些奇怪的结论;考虑到法国是一个新市场,没有建立的客户基础,推荐系统在法国的表现与美国一样高似乎不太合理。
步骤 5:[可选] 请 ChatGPT 重新做一遍你的工作
如果你使用 ChatGPT 来进行预测的合理性检查,它有时会根据你的讨论提供重新预测的建议。
在我的情况下,我让它加入了季节性因素:

图片来源:作者
最后的思考
如果你投入大量精力进行分析,却被别人彻底推翻,那真的很让人沮丧。在分享之前进行合理性检查,可以大大降低这种情况发生的几率。
这也是与更高级别的利益相关者建立信任的好方法,并且能展示你像高管一样思考。
若想获取更多实际的分析建议,可以考虑在 Medium 上关注我,或者在LinkedIn 或Substack上关注我。
如何使用 iPhone 与任何开源 LLM 免费聊天
使用 Ollama 和 Google Colab 免费 T4 GPU 在 iPhone 上构建开源“ChatGPT”应用
·发布于 Towards Data Science ·阅读时间:6 分钟·2024 年 2 月 5 日
--

开源“ChatGPT” UI 演示。图像由作者提供。
厌倦了支付高昂的订阅费用,或不愿与 OpenAI 分享个人数据?
如果有免费的、更安全的替代方案,使用功能强大的开源模型呢?
如果你感兴趣,那么这篇指南就是为你准备的。让我们一起在 iPhone 上构建自己的“ChatGPT”,由最强大的开源模型驱动!
在后端,我们将利用Ollama和 Google Colab 的免费 T4 GPU来服务 LLMs。前端我们将使用Enchanted,一款优雅的开源 iOS 应用,与 Llama 2、Mistral、Phi-2 等模型进行交互。
在本指南结束时,你将拥有一个强大的 AI 随时掌握——而且完全免费。最棒的是?你可以根据需求轻松切换最佳的开源模型!
准备好了吗?让我们开始吧!
关键组件的快速概述
要构建我们的开源“ChatGPT”,我们将使用以下关键组件:
-
Google Colab 笔记本
-
Ollama:一款开源工具…
如何为你的 GenAI 应用选择架构
一个框架,用于选择最简单、最快、最便宜的架构,以平衡 LLM 的创造力和风险。
·发表于 Towards Data Science ·阅读时间:16 分钟 ·2024 年 10 月 3 日
--
看任何 LLM 教程时,建议的使用方法是调用 API,发送提示,并使用其响应。假设你希望 LLM 生成一封感谢信,你可以这样做:
import openai
recipient_name = "John Doe"
reason_for_thanks = "helping me with the project"
tone = "professional"
prompt = f"Write a thank you message to {recipient_name} for {reason_for_thanks}. Use a {tone} tone."
response = openai.Completion.create("text-davinci-003", prompt=prompt, n=1)
email_body = response.choices[0].text
虽然这种方式适用于概念验证(PoC),但如果直接将这种架构投入生产,将 LLM 仅仅当作另一个文本到文本(或文本到图像/音频/视频)的 API 使用,最终会导致一个在风险、成本和延迟方面都设计不足的应用。
解决方案不是走向另一个极端,通过微调 LLM 和每次添加保护措施等方式过度设计你的应用。目标,和任何工程项目一样,是找到合适的平衡点,兼顾复杂性、适用性、风险、成本和延迟,以适应每个用例的具体需求。在本文中,我将描述一个框架,帮助你找到这个平衡点。
LLM 应用架构的框架
这是我建议你用来决定你的 GenAI 应用或代理架构的框架。我将在接下来的部分中详细讲解图中展示的八种替代方案。

为你的 GenAI 应用选择合适的应用架构。图示来自作者。
这里的轴(即决策标准)是风险和创造力。对于每个将要使用 LLM 的用例,首先要识别你需要的 LLM 创造力以及该用例所承载的风险。这有助于你缩小选择范围,找到最适合你的平衡点。
请注意,是否使用代理系统与此完全是正交的决策——当任务过于复杂,单个 LLM 调用无法完成,或者任务需要非 LLM 能力时,可以使用代理系统。在这种情况下,你需要将复杂任务拆解成更简单的任务,并在代理框架中协调它们。本文将向你展示如何构建一个 GenAI 应用(或代理),以执行这些简单任务之一。
为什么第一个决策标准是创造力
为什么创造力和风险是轴线?LLM 是一种非确定性技术,如果你不需要在创建的内容中有太多独特性,它带来的麻烦远大于其价值。
例如,如果你正在生成一堆产品目录页面,它们到底需要有多大的不同?你的客户希望获得准确的产品信息,可能并不关心所有 SLR 相机页面是否都以相同的方式解释 SLR 技术的优势——实际上,一定程度的标准化可能更有利于便于比较。这是一个 LLM 的创造力要求非常低的例子。
事实证明,减少非确定性的方法也减少了 LLM 调用的总次数,因此也有减少使用 LLM 总成本的副作用。由于 LLM 调用比典型的 Web 服务要慢,这也有减少延迟的良好副作用。这就是为什么 y 轴代表创造力,并且我们在该轴上还展示了成本和延迟的原因。

说明性:按创造力排序的使用案例。图表由作者提供
你可以查看上图中列出的说明性使用案例,并辩论它们是否需要低创造力或高创造力。这实际上取决于你的业务问题。如果你是一个杂志或广告公司,即使是你的资讯内容网页(与产品目录页不同)可能也需要有创造力。
为什么第二个决策标准是风险
LLM 有时会产生幻觉,反映其训练数据中的偏见和毒性。鉴于此,直接将 LLM 生成的内容发送给最终用户存在风险。解决这个问题需要增加大量的工程复杂性——你可能需要引入人类审查环节,来审核内容,或者为应用添加防护措施,以验证生成的内容是否违反政策。
如果你的使用案例允许最终用户向模型发送提示,并且应用程序在后台采取行动(这是许多 SaaS 产品中的常见情况)生成面向用户的响应,那么与错误、幻觉和毒性相关的风险相当高。
相同的用例(艺术生成)可能根据上下文承担不同级别和种类的风险,如下图所示。例如,如果你正在为电影生成背景器乐音乐,可能会面临不小心重现版权音符的风险,而如果你正在生成广告图片或视频并向数百万用户广播,你可能会担心有害内容。这些不同类型的风险与不同程度的风险相关。另一个例子是,如果你正在构建一个企业搜索应用程序,该应用程序从公司的文档库或技术文档中返回文档片段,LLM 相关的风险可能会很低。如果你的文档库包含医学教科书,搜索应用程序返回的上下文不符的内容的风险可能会很高。

示例:按风险排序的用例。图表由作者提供
就像按创造力排序的用例列表一样,你可以对按风险排序的用例排序提出异议。但一旦你识别出与用例相关的风险及其所需的创造力,建议的架构可以作为起点考虑。然后,如果你理解了这些架构模式背后的“为什么”,你就能选择一个平衡需求的架构。
在本文的其余部分,我将描述从图中#1 开始的架构。
1. 每次生成(适用于高创造力、低风险任务)
这是作为默认架构使用的模式——每次需要生成内容时,调用已部署的大型语言模型(LLM)API。这是最简单的方式,但每次都需要进行 LLM 调用。
通常,你会使用 PromptTemplate,并根据运行时参数将你发送给 LLM 的提示进行模板化。使用一个允许更换 LLM 的框架是个不错的主意。
在我们根据提示发送电子邮件的示例中,我们可以使用 langchain:
prompt_template = PromptTemplate.from_template(
"""
You are an AI executive assistant to {sender_name} who writes letters on behalf of the executive.
Write a 3-5 sentence thank you message to {recipient_name} for {reason_for_thanks}.
Extract the first name from {sender_name} and sign the message with just the first name.
"""
)
...
response = chain.invoke({
"recipient_name": "John Doe",
"reason_for_thanks": "speaking at our Data Conference",
"sender_name": "Jane Brown",
})
因为每次都在调用 LLM,它只适用于需要极高创造力的任务(例如,你希望每次都收到不同的感谢信)并且你不担心风险的情况(例如,如果最终用户可以在点击“发送”之前阅读并编辑信件)。
这种模式常用于交互式应用程序(因此需要响应各种提示),这些应用程序是为内部用户设计的(因此风险较低)。
2. 响应/提示缓存(适用于中等创造力、低风险任务)
你可能不希望向同一个人再次发送相同的感谢信。你希望每次都不一样。
但是,如果你正在构建一个基于过去工单的搜索引擎,例如为了帮助内部客户支持团队,该怎么办?在这种情况下,你确实希望重复的问题每次都生成相同的答案。
大幅降低成本和延迟的一种方法是缓存过去的提示和响应。你可以使用 langchain 在客户端进行这样的缓存:
from langchain_core.caches import InMemoryCache
from langchain_core.globals import set_llm_cache
set_llm_cache(InMemoryCache())
prompt_template = PromptTemplate.from_template(
"""
What are the steps to put a freeze on my credit card account?
"""
)
chain = prompt_template | model | parser
当我尝试时,缓存的回应只用了 1/1000 的时间,完全避免了 LLM 调用。
缓存不仅限于客户端缓存精确的文本输入和相应的回应(见下图)。Anthropic 支持“提示缓存”,通过这种方式,您可以要求模型在服务器端缓存部分提示(通常是系统提示和重复的上下文),同时在每次后续查询中继续发送新的指令。使用提示缓存可以减少每次查询的成本和延迟,同时不影响创意性。在 RAG(检索增强生成)、文档提取和少量示例提示中,当示例变得庞大时,这尤其有帮助。

响应缓存减少了 LLM 调用的次数;上下文缓存减少了每次调用中处理的令牌数量。两者结合起来,减少了总体令牌数量,从而降低了成本和延迟。图示由作者提供。
Gemini 将此功能分为上下文缓存(减少成本和延迟)和系统指令(虽然不减少令牌数量,但能减少延迟)。OpenAI 最近宣布支持提示缓存,其实现会自动缓存先前发送到 API 的最长前缀,只要提示超过 1024 个令牌。这类服务器端缓存不会降低模型的能力,仅会减少延迟和/或成本,因为即便是相同的文本提示,您仍然可能获得不同的结果。
内置的缓存方法需要精确的文本匹配。然而,可以通过一种利用具体情况微妙差异的方式来实现缓存。例如,您可以将提示重写为规范化形式,以增加缓存命中的机会。另一个常见的技巧是存储最常见的 100 个问题,对于任何相似的问题,您可以将提示重写为询问存储的问题。在多轮对话的聊天机器人中,您可以通过用户确认来验证这种语义相似性。像这样的语义缓存技术会稍微降低模型的能力,因为即便是相似的提示也会得到相同的回应。
3. 预生成模板(适用于中等创意性、低中风险任务)
有时候,您并不介意对每个处于相同情况的人生成相同的感谢信。也许您正在写感谢信给购买了某个产品的客户,您并不介意对任何购买了该产品的客户生成相同的感谢信。
与此同时,这种使用场景也存在较高的风险,因为这些通信将直接发送给最终用户,且没有内部工作人员能够在发送之前编辑每封生成的信件。
在这种情况下,预生成模板化的响应可能会很有帮助。例如,假设你是一家旅行社,并提供 5 种不同的套餐。你所需要的,只是为每种套餐准备一条感谢信息。也许你希望为独自旅行者、家庭旅行者和团队旅行者提供不同的信息。但你所需要的消息数量仍然是套餐数量的三倍。
prompt_template = PromptTemplate.from_template(
"""
Write a letter to a customer who has purchased a tour package.
The customer is traveling {group_type} and the tour is to {tour_destination}.
Sound excited to see them and explain some of the highlights of what they will see there
and some of the things they can do while there.
In the letter, use [CUSTOMER_NAME] to indicate the place to be replaced by their name
and [TOUR_GUIDE] to indicate the place to be replaced by the name of the tour guide.
"""
)
chain = prompt_template | model | parser
print(chain.invoke({
"group_type": "family",
"tour_destination": "Toledo, Spain",
}))
结果是针对特定团体类型和旅游目的地的消息,如下所示:
Dear [CUSTOMER_NAME],
We are thrilled to welcome you to Toledo on your upcoming tour! We can't wait to show you the beauty and history of this enchanting city.
Toledo, known as the "City of Three Cultures," boasts a fascinating blend of Christian, Muslim, and Jewish heritage. You'll be mesmerized by the stunning architecture, from the imposing Alcázar fortress to the majestic Toledo Cathedral.
During your tour, you'll have the opportunity to:
* **Explore the historic Jewish Quarter:** Wander through the narrow streets lined with ancient synagogues and traditional houses.
* **Visit the Monastery of San Juan de los Reyes:** Admire the exquisite Gothic architecture and stunning cloisters.
* **Experience the panoramic views:** Take a scenic walk along the banks of the Tagus River and soak in the breathtaking views of the city.
* **Delve into the art of Toledo:** Discover the works of El Greco, the renowned painter who captured the essence of this city in his art.
Our expert tour guide, [TOUR_GUIDE], will provide insightful commentary and share fascinating stories about Toledo's rich past.
We know you'll have a wonderful time exploring the city's treasures. Feel free to reach out if you have any questions before your arrival.
We look forward to welcoming you to Toledo!
Sincerely,
The [Tour Company Name] Team
你可以生成这些消息,进行人工审核,然后将它们存储在数据库中。
如你所见,我们要求 LLM 在消息中插入占位符,这样我们就可以动态地进行替换。每当需要发送响应时,只需从数据库中检索消息,并将占位符替换为实际数据。
使用预生成模板将一个原本需要每天审核数百条消息的问题,转变为只需在新增旅游产品时审核少量消息的问题。
4. 小型语言模型(低风险,低创意)
最近的研究表明,LLM 中的幻觉现象无法完全消除,因为幻觉源于学习我们希望的所有可计算函数之间的矛盾。对于更有针对性的任务,一个较小的 LLM 比一个过于庞大的模型更不容易产生幻觉。你可能会使用一个前沿的 LLM 来处理一些不需要其强大功能和世界知识的任务。
在一些使用场景中,如果你有一个非常简单的任务,且该任务不需要太多创造力,并且对风险的容忍度非常低,你可以选择使用小型语言模型(SLM)。这确实会牺牲准确性——在一项2024 年 6 月的研究中,微软的研究人员发现,对于从非结构化文本中提取发票相关的结构化数据,他们的小型文本模型(Phi-3 Mini 128K)能够达到 93%的准确率,而 GPT-4o 可以达到 99%的准确率。
LLMWare 团队评估了各种 SLM。在写作时(2024 年),他们发现 Phi-3 是最好的,但随着时间推移,越来越小的模型也在实现这一性能。
通过图示表示这两项研究,SLM(小型语言模型)正越来越以更小的尺寸实现更高的准确性(因此幻觉现象越来越少),而 LLM(大型语言模型)则专注于提高任务能力(因此幻觉现象越来越多)。在像文档提取这样的任务中,这两种方法的准确性差异已经趋于稳定(见图)。

这一趋势是 SLMs 通过越来越小的模型获得相同的准确性,而 LLMs 则通过越来越大的模型专注于更多的能力。在简单任务上的准确性差距已经稳定。图表由作者提供。
如果这一趋势持续下去,预计您将在越来越多的企业任务中使用 SLMs 和非前沿 LLMs,这些任务只需要较低的创造力,并且对风险的容忍度较低。从文档创建嵌入,例如用于知识检索和主题建模,是适合这种情况的用例。对于这些任务,使用小型语言模型。
5. 组装重新格式化(中等风险,低创造力)
组装重新格式化的基本思路是通过预生成来降低动态内容的风险,只使用 LLM 进行提取和总结,这些任务虽然是“实时”完成的,但仅引入较低级别的风险。
假设您是一家机械部件制造商,需要为产品目录中的每个项目创建一个网页。显然,您非常关心准确性。您不想错误地声称某个物品是耐热的,但它实际上并不是。您也不希望 LLM 产生安装该部件所需的工具。
您可能有一个数据库,描述了每个部件的属性。一种简单的方法是使用 LLM 为每个属性生成内容。与预生成的模板(上面的模式#3)一样,请确保在将内容存储到内容管理系统之前,经过人工审查。
prompt_template = PromptTemplate.from_template(
"""
You are a content writer for a manufacturer of paper machines.
Write a one-paragraph description of a {part_name}, which is one of the parts of a paper machine.
Explain what the part is used for, and reasons that might need to replace the part.
"""
)
chain = prompt_template | model | parser
print(chain.invoke({
"part_name": "wet end",
}))
然而,仅仅将所有生成的文本附加起来会导致读起来不太愉快。相反,您可以将所有这些内容组装到提示的上下文中,并要求 LLM 将内容重新格式化为所需的网站布局:
class CatalogContent(BaseModel):
part_name: str = Field("Common name of part")
part_id: str = Field("unique part id in catalog")
part_description: str = Field("short description of part")
price: str = Field("price of part")
catalog_parser = JsonOutputParser(pydantic_object=CatalogContent)
prompt_template = PromptTemplate(
template="""
Extract the information needed and provide the output as JSON.
{database_info}
Part description follows:
{generated_description}
""",
input_variables=["generated_description", "database_info"],
partial_variables={"format_instructions": catalog_parser.get_format_instructions()},
)
chain = prompt_template | model | catalog_parser
如果您需要总结评论或与该商品相关的交易文章,您可以通过批处理处理流水线来完成这一任务,并将摘要也输入到上下文中。
6. ML 模板选择(中等创造力,中等风险)
组装重新格式化的方法适用于内容相当静态的网页(例如产品目录页面)。然而,如果您是一家电子商务零售商,且希望创建个性化推荐,则内容将更加动态。您需要 LLM 有更高的创造力。就准确性而言,您的风险容忍度仍然差不多。
在这种情况下,您可以继续使用为每个产品预生成的模板,然后利用机器学习来选择您将使用的模板。
例如,对于个性化推荐,您可以使用传统的推荐引擎来选择展示给用户的产品,并拉取适当的预生成内容(图片+文字)来展示该产品。
结合预生成+机器学习的方法也可以用于根据不同客户旅程定制你的网站。你将预生成着陆页,并使用倾向模型来选择下一个最佳动作。
7. 微调(高创意,中等风险)
如果你的创意需求较高,就无法避免使用大型语言模型(LLM)来生成所需的内容。但每次生成内容意味着你无法扩展人工审查。
有两种方式可以解决这个难题。从工程复杂度的角度来看,更简单的方式是教会 LLM 生成你想要的内容,并避免生成你不希望的内容。这可以通过微调来实现。
有三种方法可以微调基础模型:适配器调优、蒸馏和人类反馈。这些微调方法解决了不同的风险:
-
适配器调优保留了基础模型的全部能力,但允许你选择特定风格(例如符合公司语气的内容)。这里解决的风险是品牌风险。
-
蒸馏方法接近基础模型的能力,但仅适用于有限的任务,并使用一个可以在本地或防火墙后部署的较小模型。这里解决的风险是保密性问题。
-
通过 RLHF 或 DPO 获取的人类反馈可以让模型一开始就具备合理的准确性,但通过人类反馈,模型会变得更好。这里解决的风险是适用性问题。
微调的常见使用案例包括能够创建品牌内容、机密信息的摘要和个性化内容。
8. 防护措施(高创意,高风险)
如果你需要全面的能力范围,并且有多种风险需要缓解——也许你担心品牌风险、机密信息泄漏,或者有兴趣通过反馈进行持续改进呢?
到那时,唯一的选择就是全力以赴,构建防护措施。防护措施可能包括预处理输入到模型的信息、后处理模型输出的结果,或根据错误条件迭代提示。
预构建的防护措施(例如 Nvidia 的 NeMo)存在,用于检查越狱、掩码输入中的敏感数据和自查事实等常见功能。

你可能需要自己构建的防护措施。图示由作者提供。
然而,你可能不得不自己实现一些防护措施(见上图)。需要与可编程防护措施一起部署的应用程序是实现 GenAI 应用的最复杂方式。在走这条路之前,确保这种复杂性是合理的。
总结
我建议您使用一种平衡创造性和风险的框架,以决定您的 GenAI 应用或代理的架构。创造性指的是生成内容中所需的独特性水平。风险则与 LLM 生成不准确、有偏见或有毒内容的影响相关。应对高风险场景需要更多的工程复杂性,例如人工审核或防护措施。
该框架包括八种架构模式,旨在应对不同的创造性与风险组合:
1. 每次生成:每次内容生成请求都调用 LLM API,提供最大的创造性,但成本和延迟较高。适用于风险较低的互动型应用程序,例如内部工具。
2. 响应/提示缓存:适用于中等创造性、低风险的任务。缓存过去的提示和响应,以减少成本和延迟。对于需要一致答案的场景非常有用,例如内部客户支持搜索引擎。像提示缓存、语义缓存和上下文缓存等技术可以提高效率,同时不牺牲创造性。
3. 预生成模板:采用预生成、经过审查的模板来处理重复性任务,减少持续人工审查的需求。适用于中等创造性、低到中风险的情况,需要标准化但个性化的内容,如旅游公司中的客户沟通。
4. 小型语言模型(SLM):使用较小的模型来减少幻觉现象和成本,相较于更大的 LLM。这对于低创造性、低风险的任务(如知识检索嵌入创建或主题建模)非常理想。
5. 组装重格式化:使用 LLM 进行重新格式化和总结,借助预生成的内容确保准确性。适用于如产品目录这类需要在部分内容上保证准确性,而在其他部分需要创意写作的内容。
6. 机器学习模板选择:利用机器学习根据用户上下文选择适当的预生成模板,平衡个性化与风险管理。适用于个性化推荐或动态网站内容。
7. 微调:涉及对大型语言模型(LLM)进行微调,以生成期望的内容,同时最小化不希望出现的输出,处理与品牌声音、机密性或准确性相关的风险。适配器微调专注于风格调整,蒸馏专注于特定任务,而人类反馈则用于持续改进。
8. 防护措施:高创造性、高风险的任务需要防护措施,通过预处理、后处理和迭代提示来减轻多种风险,包括品牌风险和机密性风险。现成的防护措施可以解决诸如破解和敏感数据掩蔽等常见问题,而针对特定行业/应用需求可能需要定制的防护措施。
使用上述框架来构建生成式人工智能(GenAI)应用时,你将能够为每个使用案例平衡复杂性、适用性、风险、成本和延迟。
(周期性提醒:这些文章仅代表我个人观点,并非我过去或现在雇主的立场。)
如何选择最佳的机器学习部署策略:云端 vs. 边缘
选择云端还是边缘部署可能决定了你的项目成败
·发表于 Towards Data Science ·14 分钟阅读·2024 年 10 月 14 日
--

照片由Jakob Owens提供,来源于Unsplash
作为一名机器学习工程师,我经常看到社交媒体上的讨论强调部署机器学习模型的重要性。我完全同意——模型部署是 MLOps 的关键组成部分。随着机器学习的普及,对可扩展且高效的部署方法的需求日益增加,但具体方法往往仍不明确。
那么,这是否意味着模型部署在任何情况下都是相同的呢?事实上,正好相反:我已经部署机器学习模型大约十年了,在不同项目之间,部署的方式可能差异很大。部署机器学习模型有很多种方式,拥有一种方法的经验并不意味着你对其他方法也足够熟练。
剩下的问题是:部署机器学习模型的方法有哪些,以及我们如何选择合适的方法?
模型可以通过多种方式进行部署,但通常可以分为两大类:
-
云端部署
-
边缘部署
这听起来可能很简单,但实际上有一个陷阱。对于这两大类部署,实际上还有很多子类别。以下是我们将在本文中探讨的一个非详尽的部署图示:

本文中探讨的部署子类别图示。图像来自作者。
在谈论如何选择合适的方法之前,让我们探索一下每个类别:它是什么,优点,缺点,典型的技术栈,我还将分享一些我在这个背景下做的部署的个人示例。让我们深入了解!
云部署
从我所见,云部署似乎是迄今为止 在 ML 部署中最受欢迎的选择。这通常是部署模型时需要掌握的内容。但云部署通常意味着以下几种方式之一,具体取决于上下文:
-
API 部署
-
无服务器部署
-
批量处理
即使在这些子类别中,可能还会有另一个层次的分类,但我们在这篇文章中不会探讨那么深入。让我们看看它们的含义、优缺点以及典型的技术栈。
API 部署
API 代表应用程序编程接口。这是一种在云中部署模型的非常流行的方式。许多流行的机器学习模型都是作为 API 部署的:例如,Google Maps 和 OpenAI 的 ChatGPT 都可以通过它们的 API 进行查询。
如果你不熟悉 API,了解它通常是通过简单的查询调用的。例如,在终端中输入以下命令来获取前 20 个宝可梦的名字:
curl -X GET https://pokeapi.co/api/v2/pokemon
在幕后,当调用一个 API 时,实际发生的可能更为复杂。API 部署通常涉及包括负载均衡器、自动扩展器和与数据库交互的标准技术栈:

这是在云基础设施中进行 API 部署的典型示例。图片由作者提供。
备注:API 可能有不同的需求和基础设施,本文中的示例已简化以便于理解。
API 部署因几个原因而流行:
-
易于实现并集成到各种技术栈中
-
它易于扩展:使用云中的横向扩展可以高效地进行扩展;此外,云提供商的托管服务可能减少手动干预的需求。
-
它允许集中管理模型版本和日志,从而实现高效的跟踪和可重现性。
虽然 API 是一个非常流行的选择,但它也有一些缺点:
-
可能会面临延迟问题,特别是由于潜在的网络开销或地理距离;当然,这也需要一个良好的互联网连接。
-
对于高流量,成本可能会迅速攀升(假设自动扩展)。
-
维护开销可能会变得很昂贵,无论是托管服务的费用还是基础设施团队的成本。
总结来说,API 部署在许多初创公司和技术公司中被广泛使用,因为它的灵活性和较短的市场投入时间。然而,对于高流量,成本可能会迅速上升,维护成本也可能相当高。
关于技术栈:开发 API 有很多方式,但在机器学习中最常见的可能是FastAPI和Flask。然后,它们可以通过 docker 镜像非常容易地部署到主要的云提供商(AWS、GCP、Azure 等)。可以通过托管服务或 Kubernetes 进行编排,具体取决于团队的选择、规模和技能。
作为一个 API 云部署的例子,我曾经为一个面向客户的网页应用部署了一个机器学习解决方案,用来自动化电动汽车充电站的定价。如果你想了解更多,可以在这里查看这个项目:
我如何在几个月内构建并部署一个基于机器学习的解决方案,帮助雷诺扩大电动汽车的销售……
即使这篇文章没有涉及代码,它也能让你对 API 部署能做些什么有一个很好的了解。
API 部署因其易于集成到任何项目中而非常流行。但有些项目可能需要更多的灵活性和更低的维护成本:这时无服务器部署可能是一种解决方案。
无服务器部署
另一个受欢迎但可能使用较少的选项是无服务器部署。无服务器计算意味着你运行你的模型(或者实际上是任何代码)无需拥有或配置任何服务器。
无服务器部署提供了几个显著的优势,并且非常容易设置:
-
无需管理或维护服务器。
-
不需要处理高流量情况下的扩展问题。
-
你只需为使用的部分付费:没有流量意味着几乎没有成本,因此没有任何开销。
但它也有一些局限性:
-
与托管 API 相比,它通常不适用于大量查询时的成本效益。
-
冷启动延迟是一个潜在问题,因为服务器可能需要启动,从而导致延迟。
-
内存占用通常是按设计限制的:你不能总是运行大型模型。
-
执行时间也有限:不可能运行超过几分钟的任务(例如 AWS Lambda 限制为 15 分钟)。
总的来说,我认为无服务器部署是一个在你启动新项目时,流量不大且不想在基础设施管理上花费太多的好选择。
所有主要的云提供商都以不同的名称提供无服务器计算:最受欢迎的有AWS Lambda、Azure Functions和Google Cloud Functions。
就我个人而言,我从未部署过无服务器解决方案(由于我主要从事深度学习工作,通常会受到上述无服务器限制的制约),但有很多文档可以帮助你正确地进行部署,例如AWS 的这篇文章。
虽然无服务器部署提供了一种灵活的按需解决方案,但某些应用可能需要更有计划的方法,比如批处理。
批处理
另一种在云上部署的方式是通过定时批处理。虽然无服务器架构和 API 通常用于实时预测,但在某些情况下,批量预测更有意义。
无论是数据库更新、仪表盘更新、缓存预测……只要没有实时预测的需求,批处理通常是最好的选择:
-
处理大批量数据比实时处理更具资源效率,且能够减少开销。
-
处理可以安排在非高峰时段进行,从而减少总体负载,进而降低成本。
当然,这也有其相关的缺点:
-
批处理会导致资源使用激增,如果没有适当的规划,可能会导致系统超载。
-
错误处理在批处理过程中至关重要,因为你需要一次性优雅地处理整个批次。
对于任何不需要实时结果的任务,都应该考虑批处理:通常它更加具有成本效益。但当然,对于任何实时应用,它并不是一个可行的选择。
它在许多公司中得到了广泛应用,主要是在 ETL(提取、转换、加载)管道中,这些管道可能包含机器学习,也可能不包含。以下是一些最流行的工具:
-
Apache Airflow,用于工作流编排和任务调度
-
Apache Spark,用于快速、大规模数据处理
作为批处理的一个例子,我曾经从事 YouTube 视频收入预测的工作。根据视频收入的初始数据点,我们会预测未来最多 5 年的收入,使用多目标回归和曲线拟合:

表示初始数据、多目标回归预测和曲线拟合的图表。图像来源:作者。
对于这个项目,我们需要每月重新进行所有数据的重新预测,以确保我们的初始预测与最新预测之间没有偏差。为此,我们使用了托管的 Airflow,每月会自动根据最新的数据触发新的预测,并将这些数据存储到我们的数据库中。如果你想了解更多关于这个项目的信息,可以查看这篇文章:
该方法在对数十个 YouTuber 的收入预测中取得了低于 6%的错误率
在探索了用于云部署的各种策略和工具后,显然这种方法提供了显著的灵活性和可扩展性。然而,云部署并不总是每个机器学习应用的最佳选择,特别是当实时处理、隐私问题或财务资源限制成为因素时。

云部署的优缺点列表。图片由作者提供。
这就是边缘部署作为一种可行选项的焦点所在。让我们现在深入了解边缘部署,看看什么时候它可能是最佳选择。
边缘部署
根据我自己的经验,边缘部署很少被视为主要的部署方式。几年前,连我自己也认为它并不是一个真正有趣的部署选项。但现在通过更多的视角和经验,我认为它必须在任何可以的情况下作为首选部署方式。
就像云部署一样,边缘部署涵盖了广泛的应用场景:
-
原生手机应用
-
网络应用
-
边缘服务器和特定设备
虽然它们都具有一些相似的特性,例如资源有限和水平扩展的限制,但每种部署选择可能都有其独特的特点。让我们来看看。
原生应用
我们现在看到越来越多的智能手机应用集成了 AI,未来这种趋势可能会进一步增长。虽然一些大科技公司如 OpenAI 或谷歌为他们的大型语言模型(LLM)选择了 API 部署方式,但苹果目前正在通过像OpenELM这样的解决方案,专注于 iOS 应用部署模型,这是一种小型 LLM。实际上,这种方式有几个优点:
-
基础设施成本几乎为零:无需维护云端,一切都在设备上运行
-
更好的隐私:你不需要将任何数据发送到 API,所有计算都可以在本地完成
-
你的模型直接集成到你的应用中,无需维护多个代码库
此外,苹果为 iOS 上的模型部署打造了一个极好的生态系统:你可以在他们的 Apple 芯片(如 M1、M2 等)上高效运行机器学习模型,并利用神经引擎进行非常快速的推理。据我所知,安卓略微落后,但也有一个出色的生态系统。
虽然在许多情况下这可能是一个非常有益的方法,但仍然存在一些限制:
-
手机资源限制了模型的大小和性能,并且这些资源与其他应用共享
-
重型模型可能会很快消耗电池,这可能会对用户体验产生误导性影响
-
设备碎片化,以及 iOS 和 Android 应用使得覆盖整个市场变得困难
-
与云部署相比,去中心化的模型更新可能会更加具有挑战性
尽管存在一些缺点,但原生应用部署通常是针对在应用中运行的机器学习解决方案的一个强有力的选择。在开发阶段看起来可能更复杂,但一旦部署,相比云部署,它将便宜得多。
在技术栈方面,实际上有两种主要的部署方式:iOS 和 Android。它们各自有自己的栈,但共享相同的属性:
-
应用开发:iOS 使用 Swift,Android 使用 Kotlin
-
模型格式:iOS 使用 Core ML,Android 使用 TensorFlow Lite
-
硬件加速器:iOS 的 Apple Neural Engine,Android 的 Neural Network API
注意:这只是对技术栈的简化描述。这个不完全的概述旨在覆盖要点,并让你如果有兴趣,可以进一步深入了解。
作为这种部署的个人例子,我曾经参与过一个 Android 平台的图书阅读应用,在这个应用中,他们希望让用户通过手机的运动来浏览书籍。例如,左摇动手机翻到上一页,右摇动翻到下一页,其他特定的动作执行特定的命令。为此,我训练了一个模型,使用手机加速度计的特征来识别运动,并且使用了一个相对较小的模型。然后,这个模型直接作为 TensorFlow Lite 模型部署到应用中。
原生应用具有强大的优势,但仅限于一种类型的设备,例如在笔记本电脑上无法运行。Web 应用可以克服这些局限。
Web 应用
Web 应用部署意味着在客户端运行模型。基本上,它意味着在浏览器使用的设备上运行模型推理,无论是平板电脑、智能手机还是笔记本电脑(等等)。这种部署方式非常方便:
-
你的部署可以在任何能够运行 Web 浏览器的设备上工作
-
推理成本几乎为零:无需服务器,无需维护基础设施……只需要客户的设备
-
所有设备只需一个代码库:无需同时维护 iOS 应用和 Android 应用
注意:在服务器端运行模型将等同于上面提到的云部署选项之一。
虽然 Web 部署提供了诱人的好处,但它也有显著的局限性:
-
使用 TensorFlow.js 时,合理利用资源,尤其是 GPU 推理,可能会面临挑战
-
你的 Web 应用必须能够在所有设备和浏览器上运行:无论是否有 GPU,使用 Safari 还是 Chrome,是否有 Apple M1 芯片等等……这可能是一个很重的负担,且维护成本较高
-
你可能需要为较慢和较老的设备准备一个备份方案:如果设备太慢而无法处理你的模型怎么办?
与原生应用不同,模型没有官方的大小限制。然而,较小的模型下载速度更快,整体体验也更加流畅,因此应该优先考虑。而且一个非常大的模型可能根本无法正常工作。
总结来说,虽然网页部署功能强大,但它有显著的局限性,必须谨慎使用。另一个优点是,它可能是我未提及的另一种部署方式的入口:微信小程序。
技术栈通常与网页开发相同:HTML、CSS、JavaScript(以及你想要的任何框架),当然还有用于模型部署的 TensorFlow Lite。如果你对如何在浏览器中部署机器学习感兴趣,可以看看这篇文章,我从零开始在浏览器中运行了一个实时人脸识别模型:
BlazeFace 模型训练的逐步指南,从 Python 训练管道到 JavaScript 演示,详细介绍了整个过程……
[towardsdatascience.com
本文从 PyTorch 中的模型训练讲起,最终实现了一个可工作的网页应用,可能对这种特定类型的部署有所启发。
在某些情况下,原生应用和网页应用都不是可行的选择:我们可能没有合适的设备,无法联网,或者受到其他限制。这时,边缘服务器和特定设备就派上了用场。
边缘服务器和特定设备
除了原生应用和网页应用,边缘部署还包括其他情况:
-
在边缘服务器上的部署:在某些情况下,会有本地服务器运行模型,例如某些工厂生产线、CCTV 等…由于隐私要求,这种解决方案有时是唯一可用的。
-
在特定设备上的部署:无论是传感器、微控制器、智能手表、耳塞、自动驾驶汽车等,都可能内部运行机器学习模型。
在边缘服务器上的部署可以与云端 API 部署非常接近,技术栈也可能非常相似。
备注:也可以在边缘服务器上运行批处理,或者使用一个单一的脚本来完成所有任务。
但在特定设备上的部署可能涉及使用FPGA或低级语言。这是另一种完全不同的技能集,针对每种设备类型可能有所不同。这个领域有时被称为 TinyML,是一个非常有趣且不断发展的话题。
在这两种情况下,它们面临与其他边缘部署方法相同的一些挑战:
-
资源有限,通常无法进行横向扩展。
-
电池可能是一个限制因素,模型大小和内存占用也是如此。
即使有这些限制和挑战,在某些情况下,它仍然是唯一可行的解决方案,或者是最具成本效益的方案。
我曾为一家公司做过一个边缘服务器部署示例,该公司希望自动检查快餐店订单的有效性。一个俯视的摄像头会观察平台,使用计算机视觉和物体检测技术将摄像头看到的内容与实际订单进行对比,在发现不匹配时发出警报。由于某些原因,该公司希望将此系统部署在快餐店内部的边缘服务器上。
总结一下,以下是主要的部署类型及其优缺点:

云部署的优缺点列表。图片来自作者。
鉴于此,如何实际选择合适的部署方式?这个问题没有单一的答案,但让我们尝试在下一节给出一些规则,以便简化选择过程。
如何选择合适的部署方式
在得出结论之前,让我们制作一个决策树,帮助你选择最适合的解决方案。
选择合适的部署方式需要理解具体的需求和限制,通常需要与利益相关者进行讨论。记住,每个案例都是特定的,可能是一个边缘案例。但在下面的图表中,我试图概括出最常见的情况,帮助你做出决策:

部署决策图。请注意,每个使用场景都是特定的。图片来自作者。
这个图表虽然相当简化,但可以归结为几个问题,帮助你走向正确的方向:
-
你需要实时处理吗?如果不需要,首先考虑批处理;如果需要,请考虑边缘部署
-
你的解决方案是运行在手机上还是在网页上?在可能的情况下,探索这些部署方式
-
处理过程是否相当复杂且繁重?如果是,请考虑云部署
再次强调,这虽然很简单,但在许多情况下非常有帮助。同时,请注意,为了清晰起见,省略了一些问题,但在某些情况下,它们实际上非常重要:你有隐私限制吗?你有连接性限制吗?你团队的技能如何?
根据使用场景,可能还会出现其他问题;随着经验的积累和对你生态系统的了解,这些问题会变得越来越自然。但希望这些内容能帮助你更轻松地进行机器学习模型的部署。
结论和最终思考
尽管云部署通常是机器学习模型的默认选择,但边缘部署可以提供显著的优势:成本效益和更好的隐私控制。尽管处理能力、内存和能源约束等挑战存在,但我相信边缘部署在许多情况下都是一个值得考虑的选项。最终,最佳的部署策略应该与您的业务目标、资源限制和具体需求相契合。
如果你读到这里,我很想听听你在项目中使用过的部署方法的看法。
如何使用 Python 的正则表达式清理杂乱的文本数据
如何使用正则表达式将半结构化文本转化为可用数据
·发表于 Towards Data Science ·9 分钟阅读·2024 年 11 月 6 日
--

我们可以通过 Python 对杂乱的数据进行筛查,寻找正则表达式。图片由Leonardo AI生成。
想象一下:你被分配了分析一份包含文本和表格的冗长 PDF 报告中的数值数据的任务。一位同事已经使用光学字符识别技术提取了信息(见上周的文章)。
不幸的是,这个文件并不是一个结构化的数据集,而是相当杂乱——你会发现多余的标题、冗余的脚注和不规则的换行符。数字格式不一致,数据描述散布各处,这使得没有经过大量预处理就几乎无法进行有意义的分析。看起来,今天你将面对数小时的繁琐数据清理工作。
幸运的是,你已经发现了正则表达式。正则表达式(Regex)是用于文本模式匹配的强大工具。它听起来简单,但允许用户定义、搜索并操作文本中的特定模式,这使得它在处理杂乱数据时非常有效。
本文将为你提供一些关于正则表达式的背景知识,并介绍它在 Python 中的实现。接着我们会深入探讨用于数据清理的正则表达式的关键功能,并提供一个实际操作的示例(这是我们最近在 Wangari 遇到的),以说明这在实践中的应用。如果你…
如何清洗你的数据以应对现实生活中的数据科学项目
简化数据科学
我如何处理缺失值——快速的 Python 指南
·发布于 Towards Data Science ·阅读时间 7 分钟·2024 年 12 月 23 日
--

图片来自 Wannapik
我们常常听到——“哦,现在有很多可以做所有事情的软件包!使用这些软件包跑模型只需 10 分钟。”是的,确实有这些软件包——但它们只有在你已经准备好干净的数据集时才能工作。那么,从多个来源创建、整理并清洗一个适合用途的数据集需要多长时间呢?问问那些正在努力创建数据集的数据科学家吧。所有那些曾经花费数小时清洗数据、研究、阅读并重写代码、失败然后再重写的人,都会同意我的看法!这就引出了一个问题:
‘现实生活中的数据科学是 70%的数据清洗和 30%的实际建模或分析’
因此,我想,不如回归基础,学习如何清洗数据集,并使其能够更高效地解决业务问题。我们将以缺失值处理作为本系列的起点。以下是议程:
-
什么是缺失值
-
数据集中的缺失值的原因是什么
-
缺失值为什么很重要
-
处理缺失值的方法
如何为 Polars DataFrame 上色
在继续使用 Polars 库的同时,能够为表格上色并进行样式化
·发布于 Towards Data Science ·6 分钟阅读·2024 年 8 月 27 日
--

由ChatGPT生成的 AI 图像。提示:一幅北极熊在雪地中画画的图。
自 2022 年发布以来,Polars 库由于其超高速的 DataFrame 功能而迅速获得了人气。与 Pandas 相比,白熊测试表明其速度远远更快。根据官方Polars 网站,它声称可以实现超过 30 倍的性能提升。
然而,没有什么是完美的。Polars 库似乎有一些限制。
在样式化表格时,Polars 提供的选项较少,而 Pandas 有一个内置的样式工具。如果你想为 Polars DataFrame 添加颜色,一个简单的解决方案是将表格转换为 Pandas。
等等……如果某些代码稍后需要运行怎么办?


示例展示了文章中 Polars 表格样式化前后的效果。图片由作者提供。
这意味着我们必须运行 Pandas,这可能会导致速度显著变慢。另一个选择是在样式化后将表格转换回 Polars。然后,如果我们想要样式化结果,必须重复相同的过程。尽管这些解决方案有效,但它们相当不便。
如何将分类模型与基线模型进行比较
数据科学,机器学习
一份可以直接运行的 Python 和 scikit-learn 教程,用于评估分类模型与基线模型的对比
·发表于 Towards Data Science ·6 分钟阅读·2024 年 2 月 18 日
--

图片来源:Kier in Sight Archives 在 Unsplash
前几天,我需要判断我的分类算法的表现是否令人满意。我获得了 64% 的精确度、召回率和准确率,老实说,我以为这是一个糟糕的结果。事实上,64% 稍微好于一个随机模型。实际上,如果问题本身较简单,这个结论是成立的。例如,在只有两个值的情况下,随机算法有 50% 的概率预测正确结果。因此,在这种情况下,准确率为 64% 的算法比随机算法要好。
如果你正在处理一个多类别算法,其中类别的数量大于两个,那么问题就不同了。在我的例子中,我有大约 1000 个类别。在这种情况下,问题比二分类问题复杂得多,因此 64% 的准确率可能甚至是一个不错的算法!
那么,如何判断获得的模型性能是否令人满意呢?解决方案是将模型与一个代表该问题的虚拟模型进行比较。如果我们的模型表现优于虚拟模型,那么结果是有希望的……
如何使用 Taipy 在 Python 中构建多页面数据科学 Web 应用
Taipy 支持在 Python Web 应用中轻松进行页面间导航——我们创建了一个简单的 CO2 排放应用
·发布于 Towards Data Science ·13 分钟阅读·2024 年 9 月 5 日
--

Taipy 是一个用于在 Python 中构建数据科学和 AI Web 应用的框架。因此,它是 Streamlit 或 Dash 等工具的竞争者,但与这两个产品有本质区别。
Taipy 将用户界面与程序的其他逻辑分离,并使用回调函数为用户控件添加功能。从这个角度看,它更接近于 Dash,而不是 Streamlit,因为后者的用户界面控件通常会融入到主 Python 代码中。
Dash 和 Taipy 都是基于 Flask 微框架构建的,因此不应感到惊讶的是它们之间有相似之处。不过,在 Dash 应用中,构建用户界面时的感觉更像是用 HTML(但使用 Python 函数编写),而 Taipy 则有一个额外的抽象层,让用户定义的用户控件更接近于 Streamlit 而非 Dash。
那么,Taipy 是不是兼具两全其美的优势呢?我在 用 Taipy 构建纯 Python 数据仪表盘 中写了一个介绍,探讨了如何在 Taipy 中构建 Web 应用,您可以自行判断它的易用性。
如何使用 Python 将单一的 HEX 颜色代码转换为单色调色板
剧透:这比你想象的要难。
·发表于Towards Data Science ·8 分钟阅读·2024 年 9 月 27 日
--

文章缩略图(作者提供的图片)
颜色很难处理,尤其是当你没有设计眼光时。
我们大多数技术专业人士并没有。好的一面是,Python 可以为你完成大部分繁重的工作。它可以生成一整套单色调色板,在任何堆叠的图表上看起来都很惊艳。
不利的一面是,达到目标需要编写相当多的代码。你需要编写自定义函数将颜色从 HEX 转换为 HSL,反之亦然,还要弄清楚起始颜色是否过于明亮,以及每个下一个颜色应该变得多么更亮。
更糟糕的是,我还没找到一个完全能实现这个任务的 Python 库。
这就是本文的重点所在。
如果你是我的Substack 订阅者,你可以跳过阅读,直接下载笔记本。
HEX、HSL、RGB —— 这些术语是什么意思?
让我们快速了解你需要知道的三种颜色格式:
- HEX —— 一个六位数字的代码,通常用于网页开发和图形设计。颜色代码以
#开始,后面跟着六个十六进制数字。每对……
如何创建自定义 Matplotlib 主题并让你的图表从乏味变得惊艳
最棒的是?你只需要做一次。
·发表于Towards Data Science ·7 分钟阅读·2024 年 9 月 5 日
--

文章缩略图(图片由作者提供)
每一个 Matplotlib 图表都有可能成为病毒式传播的热点。但是,使用默认主题是做不到的。
说实话:默认样式不会让任何人停下脚步。如果你想吸引读者的注意,你需要的不仅仅是提高分辨率或更换字体。你需要的是一个自定义主题。
这是让你的图表看起来属于你的唯一方法。这是让你的读者停下滚动的唯一方法。好消息是? Matplotlib 让你从零开始编写自定义样式表变得极其简单。更棒的是——你可以让你的自定义主题在全系统范围内可用!
在今天的文章中,你将学到如何做到这一点。
绘制条形图和折线图所需的两个函数
为了确保可重复性,我将为你提供两个函数,用于绘制条形图和折线图。
设置自定义 Matplotlib 主题的目标不是改变底层的 Python 代码——而是保持其不变——看看视觉效果上有什么不同。
如何在 Tableau 中创建网络图
《在 Tableau 中创建网络图的逐步指南》
·发表于Towards Data Science ·阅读时间 8 分钟·2024 年 11 月 5 日
--

图片由Radoslaw Sikorski提供
并由 Pexels.com 提供
在一个越来越依赖互联数据的世界里,理解不同实体之间的关系比以往任何时候都更为重要。网络图,也叫网络图谱或关系图,提供了一种引人注目的方式来可视化这些连接,为复杂的数据集提供了清晰度和洞察力。无论你是在分析社交网络、组织结构、沟通流程,还是任何由互相关联的元素组成的系统,网络图都能帮助你看到更广阔的图景。
在这篇博客中,我将带你走一遍使用 Tableau 创建网络图的过程,特别关注组织内经理与员工之间的关系结构。了解员工与经理之间的连接对于可视化组织层级、增强团队协作和识别潜在的沟通差距至关重要。
网络图的关键组成部分
在深入了解网络图的创建之前,首先需要理解网络可视化的架构。让我们以一个小公司的组织结构图为例,来展示网络图的形式。
如何为 Gmail 创建强大的 AI 电子邮件搜索系统(使用 RAG)
学习如何开发一个应用来使用 RAG 搜索电子邮件
·发布于Towards Data Science ·13 分钟阅读·2024 年 9 月 10 日
--
在本文中,我将向你展示如何开发MailDiscoverer应用,以使用 RAG 搜索 Gmail 电子邮件。首先,我将展示如何设置身份验证管道来访问用户的电子邮件(如果已获得同意)。然后,使用 OpenAI 的文本嵌入器将电子邮件嵌入,并将其存储在 Pinecone 向量数据库中。这允许用户就电子邮件提出问题,RAG 系统将检索最相关的电子邮件并提供问题的答案。

学习如何开发一个 RAG 系统来搜索你的电子邮件。图片来自 ChatGPT。OpenAI。(2024 年)。ChatGPT(4.0)[大型语言模型]。chatgpt.com/c/66dd8280-5bc4-8012-9e7e-68fd66ccbfeb
本文中开发的应用可以在Streamlit 上找到。我在 GitHub 上的代码库也可以访问。
下面的视频展示了该应用如何工作。在登录并上传电子邮件后,你可以提出问题,应用将提供答案,并展示用于提供答案的最相关电子邮件。
动机
如何从文档中创建 RAG 评估数据集
使用 LLM 自动创建任何语言的领域特定数据集
·发表于Towards Data Science ·12 分钟阅读·2024 年 11 月 3 日
--

我们在 Hugging Face Hub 上自动生成的 RAG 评估数据集(PDF 输入文件来自欧盟,遵循CC BY 4.0许可)。图片由作者提供
在本文中,我将向你展示如何从任何语言的文档中创建你自己的 RAG 数据集,包括上下文、问题和答案。
检索增强生成(RAG)[1]是一种技术,允许 LLM 访问外部知识库。
通过上传 PDF 文件并将其存储在向量数据库中,我们可以通过向量相似度搜索来检索这些知识,然后将检索到的文本插入到 LLM 的提示中作为附加上下文。
这为 LLM 提供了新的知识,并减少了 LLM 编造事实(幻觉)的可能性。

基本的 RAG 管道。图片由作者提供,来自文章“如何使用 RAG 构建本地开源 LLM 聊天机器人”
然而,在 RAG 管道中有许多参数需要设置,研究人员总是在提出新的改进建议。我们如何知道选择哪些参数,哪些方法能真正提高我们特定用例的性能呢?
这就是为什么我们需要一个验证/开发/测试数据集来评估我们的 RAG 管道。数据集应来自我们感兴趣的领域……
如何创建成功的数据演示
在六个简单步骤中像专业人士一样展示你的数据……
·发表于 Towards Data Science ·6 分钟阅读·2024 年 3 月 11 日
--

图片由 Teemu Paananen 提供,来源于 Unsplash
在当今快速发展的技术世界中,数据已经成为每个对话的基石,包括商业决策。数据专业人士必须知道如何最好地展示他们处理的数据。
前几天我在喝咖啡时遇到了一位同行,巧合的是我们俩都在为高管准备演示文稿,我们几乎异口同声地说:“为高管做数据演示真的是完全不同”。我们展示数据和洞察的方式根据受众的不同而差异极大。从极其高层的概述到提出潜在的细微警示,数据本身可以以许多不同的方式呈现,正是这种根据不同受众调整讲故事方式的艺术,激励我写下这篇博客。
每个人都知道如何使用 PowerPoint、Prezi、Canva、Visme 等工具来创建引人注目的演示,但最重要的是数据呈现的方式!
人们比起原始数据,更容易理解故事
知道如何开发和呈现基于数据的演示已经成为当今数据专业人士必备的技能。仅仅有数据是无法支持一场精彩的演示的。通过数据讲故事是一项备受推崇的技能……
如何为色盲人士创建可访问的图表
包括一个 Python 色盲模拟器和合适的配色方案
·发表于Towards Data Science ·阅读时间 7 分钟·2024 年 2 月 5 日
--

该图片由作者使用 Midjourney 创建。
色盲,或更准确地说,色觉缺陷,影响了多达 8%的男性和 0.5%的女性。最常见的类型是红绿色盲,这是一种先天性疾病,患者的视网膜缺少红色或绿色的感光细胞。因此,他们无法区分如红与绿、青与灰、蓝与紫等颜色对。
对数据科学家来说,在选择图表的配色方案时考虑色盲用户非常重要。相当一部分用户会有某种形式的色觉障碍,他们可能会以不同于预期的方式理解图表。实际上,当我的直属上司无法阅读我的折线图时,我意识到了这一点,后来才发现他在区分红色和绿色时有困难。
在这篇文章中,我将分享
-
一个用于模拟最常见色盲类型的 Python 工具
-
对 matplotlib 和 seaborn 默认配色方案的可访问性测试
-
使用 ColorBrewer 创建可访问的配色方案
色盲是什么样子的?
如何在 BigQuery 中创建 RFM 模型
了解什么是 RFM 模型,如何创建一个,并如何根据结果进行细分
·发布在Towards Data Science ·阅读时间:8 分钟·2024 年 3 月 5 日
--

通过 DALL-E 创建
在本文中,我们将讨论:
-
什么是 RFM 细分及其在营销中的重要性
-
如何在 BigQuery 中创建 RFM 分位数
-
如何根据 RFM 分位数创建 RFM 细分
-
创建你自己的 RFM 模型时需要考虑的事项
*注意 — 本文中使用的所有数据均为虚构,并由我在 BigQuery 中生成。
让我们从基础开始,什么是 RFM 模型?
RFM(最近度、频率、金额)模型是一种客户细分技术,利用过去的购买行为将客户划分为不同的群体。
-
最近度(Recency)衡量客户最近一次购买的时间
-
频率(Frequency)评估客户交易的频率
-
金额(Monetary)衡量客户的消费金额
仅这三个细分可以帮助你更好地理解客户群体,但你可以将它们结合起来形成更多的细分。这些细分帮助你识别哪些客户是你的最佳客户,哪些客户正在流失,或者……
如何在 Matplotlib 中创建自定义颜色调色板 — 离散与线性颜色图的解释
可操作的指南,教你如何将自定义颜色应用于个性化你的图表
·发布于 Towards Data Science ·阅读时间 6 分钟·2024 年 8 月 29 日
--

文章缩略图(图片来自作者)
如果有一件事能够让一个好的图表变得更好,那就是颜色的选择。
你可以使用 Matplotlib 将任何一组十六进制颜色代码转换为颜色调色板,本文将向你展示如何操作。你还将了解离散和线性颜色调色板的区别,以及为什么其中一种优于另一种。
如果你想获得与我相同的数据可视化质量,请在继续之前按照本文中的步骤操作:
如何在 Matplotlib 中创建自定义颜色图
下面是你需要使用的库:
import numpy as np
import pandas as pd
import matplotlib as mpl
import matplotlib.pyplot as plt
Matplotlib 允许你创建两种类型的颜色调色板:
- 离散的 — 调色板包含有限数量的颜色值。非常适合类别数据,但你需要确保调色板中至少有与类别数量相等的颜色。
如何在数据科学职位申请中创造机会并获得成功
我从成功与失败中汲取的宝贵经验,将帮助你在职业道路上走得更远
·发布于Towards Data Science ·阅读时间 8 分钟·2024 年 10 月 29 日
--

事实上,是否被招聘人员注意到,往往取决于你在 LinkedIn 上的定位以及你如何准备招聘过程。在本文中,我将分享过去几年中我所学到的一些经验,这些经验让我被像 Mercado Livre、亚马逊、Meta、TikTok、Uber 等公司邀请面试。
1. 显而易见的:让招聘人员看到你
我收到的大部分面试邀请,都是因为我在 LinkedIn 上保持活跃和可见。我开始定期发布内容,至少每两周一次,分享我的故事并与他人的帖子互动。以下是一些关键建议。
讲述(相关部分)你的故事: 分享你的过往经历和抱负。保持你的个人资料更新,并确保与你希望从事的领域相关。这篇文章提供了一些关于如何提升个人资料的建议(仅提供葡萄牙语版本,但图表自解释)。
创作内容并保持一致性: 我收到的所有面试邀请(即当招聘人员主动联系我时)都发生在我开始在 LinkedIn 上创作更多内容之后。而且这并不需要过于复杂。例如,我会像这样写关于我经历的文章,并直接在 LinkedIn 上发布为“个人文章”。这将有助于增加你的互动,并避免分享外部链接(如 Medium、YouTube 等)时出现的可见性下降。

图片由作者提供
2. 寻找机会时不要过于谦虚
保持面试动力的最佳方法是实际安排面试。为了做到这一点,申请那些对你有意义的职位,但不要只局限于那些“完全匹配”的职位。记住:最终决定你是否合格的应该是招聘人员,而不是你。如果你不尝试,你永远不知道!
超级重要的额外建议——请求推荐:如果你认识招聘公司的某个人,可以询问他们是否能推荐你。许多公司有内部平台来进行推荐,一个好的推荐可以为你打开大门。我将在下一节中详细讲解。
3. 建立你的社交资本(人脉网络)
建立人脉并不意味着要非常社交,而是意味着要维持有意义的联系。让我举一些例子,说明这如何帮助我:
示例 1: 一年后,我在一个训练营中认识的人推荐我申请了他们公司的一份职位。当然,这不是偶然发生的。在训练营期间,我们在 LinkedIn 上建立了联系,并时不时在线互动,讨论数据科学的话题。
示例 2: 这就是推荐的力量。我曾在投递简历申请工作后,通过其中一封自动邮件被拒绝,但在一次内部推荐之后,公司重新考虑了我的申请,并邀请我继续参加面试直至最后阶段。
与来自会议、训练营以及以前工作单位的同事保持联系。记住不要自私:尽可能帮助别人,因为人脉是双向的。
4. 为招聘过程的每个阶段做好充分准备
面试通常遵循一个模式:首先与 HR 进行初步对话,看看你是否适合该职位,然后是与公司其他人的几轮技术性对话,包括招聘经理。在与 HR 的初步对话后,一些公司会向你发送更详细的招聘过程资料或链接。如果没有,你可以询问他们是否能提供更多的详细信息和指引。
我倾向于直接阅读招聘人员发给我的所有资料。此外,我还会写一份可能会被问到的问题脚本,这有助于我为接下来的面试阶段做准备。
一些避免浪费机会的小贴士:
了解你的面试官: 在 LinkedIn 上查看他们的个人资料,找出共同的兴趣点,并利用这些信息在谈话开始时破冰。通常,这段破冰谈话只会持续几分钟,以免占用太多面试时间。
讲述正确的故事: 不要讲述你整个职业生涯的经历——集中讲述与你申请的职位最相关的经历。谈谈什么样的经历会让面试官对你感兴趣,想聘用你来解决该职位的相关问题。在非技术性面试中,避免使用行话——面试官不一定来自你的领域。提供直观的解释,从基础开始,然后深入讲解。
一个超级简化的例子:[我做了什么] 我的背景是经济学,曾获得博士学位,专注于影响评估和因果推断(...)。[我现在做的] 在我目前的工作中,我参与了像 x、y 和 z 这样的项目,这些项目让我获得了所需的经验,以便于(...)。[我能做的] 这些经验与该职位需要解决的问题密切相关,原因是 a、b 和 c。
向面试官提问: 精心准备的问题展示了你的准备情况和真实的兴趣。询问团队面临的最大挑战、与其他部门的互动方式以及公司内的成长期望。
练习讲述你的故事: 很多筛选过程会要求你展示成功项目的例子以及遇到问题的项目。成功的项目展示了你取得积极结果的能力,而失败的项目则展示了你学习和改进的能力。
列出你最重要的项目,解释它们如何与公司的挑战相关,你面临了哪些困难,如何克服它们,以及你为组织带来了什么价值。
创建应用于公司的假设场景: 准备一些如何在职位角色中应用重要方法和工具的例子。将这些项目与其应用所需的方法和假设相关联。
比如,在一次面试中,我被问到如何在公司背景下应用“差异中的差异”方法来衡量市场营销活动的效果。
准备应对不舒服问题的答案: 要准备好回答一些让人不舒服的问题,比如如何应对负面反馈、如何获得同事的信任、如何承认错误、如何与老板意见不合,或如何面对紧迫的截止日期和模糊的决策。
你的困难不总是通过经典的“你最大的弱点是什么?”这个问题来解决。如果你最近换了职业,面试官可能会想了解更多关于这个转变的情况。
准备一个解释你动机的故事。你也可能会被问及薪资期望。要透明,说明对你来说合理的价值,并准备好为你的期望提供理由。
5. STAR 方法:有效地讲述例子和案例研究
许多公司要求你提供成功或面临困难时的具体案例。提前使用 STAR 方法(情境、任务、行动、结果)准备这些案例,以便清晰简洁地组织你的回答。这个方法有助于组织你的思路,并向面试官提供有影响力的答案。让我们详细说明每一步:

图片由作者使用 Chat GPT 生成
情境: 首先描述情境的背景。要具体,提到发生的时间和地点。例如:“在我为 XPTO 公司工作期间,我们在一个特定活动中遇到了转化率大幅下降的情况。”
任务: 谈谈你在该情境中的责任。需要做什么?要解决什么问题?例如:“我的任务是分析数据,找出导致转化率下降的因素,并提出解决方案来提高转化率。”
行动: 详细描述你为解决问题所采取的行动。要清晰并突出你主导的举措。例如:“我使用因果推断方法来理解该活动的效果。我使用‘差异中的差异’技术对问题进行建模,以识别每个活动组件的影响,并模拟了一种新的受众细分格式。此外,我还协调了一支分析团队,在对照组中测试这些新策略。”
结果: 解释你所采取的行动的具体结果。如果可能,量化这些结果。例如:“通过分析和实施新策略,我们成功地在接下来的阶段将转化率提高了 15%,并带来了 50 万美元的收入增长。我还将过程进行了文档化,以便团队在未来的活动中可以应用。”
STAR 方法在许多公司(如亚马逊)面试过程中都有推荐使用(见此处的“STAR 回应格式”部分)。它是一个极好的工具,能够确保你涵盖经历的每个重要方面,使你的回答更具清晰度和深度。理想情况下,准备 3-4 个使用 STAR 结构的不同面试问题案例。
创建假设场景: 思考如何将你领域中的重要方法应用到公司背景中。例如,如何使用“差异中的差异”来衡量营销活动的影响?这些模拟有助于展示你如何将技术知识应用于实际情况,并且是你 STAR 回答的有力补充。
6. 一系列可能令人不舒服的问题
许多面试会包含具有挑战性的问题,就像我在第四部分最后提到的那些问题。以下是更多可能帮助你准备的问题。同样,依靠 STAR 方法来构建你的回答:
-
在面对负面反馈时,你是如何处理的?讲讲那个案例,描述你是如何应对的,学到了什么,以及如果有机会重新经历那种情况,你今天会怎么做。
-
你是如何赢得那些不太愿意合作的团队的信任的?
-
你曾经在哪些情况下不得不承认错误或与老板意见不合等?
-
你如何处理承诺但未能按时完成的截止日期?以及在面对模糊决策时(例如,当没有明确正确答案时)你是如何做出决定的?
收集真实案例有助于更清晰、自信地回答问题。这些问题的目的是了解你如何应对挑战,以及你是否符合公司的文化。
招聘过程不仅仅是证明你的技术能力:更是展示你适应能力、合作能力和学习能力的过程。记住,每一个环节都是一个学习机会,即使最终没有立刻收到录用通知。我已经经历了很多次这种情况。
如果你需要一些鼓励,或者只是想聊聊关于艰难决定的事,给我发个消息。我很乐意帮助你,毫无附加条件,只是为了回报别人曾经给予我的帮助。
感谢阅读。请关注我以便查看后续内容!
如果你想查看更多内容,可以通过这里和这里关注我,在那里我分享关于数据科学和职业发展的文章。如果你注意到任何错误或有建议,我会很高兴听到你的意见。
如何通过结合多模态信息创建强大的 AI 表示
学习如何将多模态信息融入到你的机器学习系统中
·发布于 Towards Data Science ·阅读时间:10 分钟·2024 年 4 月 2 日
--
在本文中,我将讨论如何将来自不同模态的信息融入到你的机器学习系统中。这些模态可以是图像、文本或音频等信息。也可以是例如从不同角度拍摄的同一物体的几张图像。通过加入来自不同模态的信息,机器学习系统可以获得更多的信息,从而提高系统的性能。

在本文中,您将学习如何将来自不同模态的信息结合在一起。图像由 ChatGPT 提供。“在机器学习中结合多模态信息的图像”提示。ChatGPT,4,OpenAI,2024 年 4 月 1 日。chat.openai.com.
动机
我写这篇文章的动机是,我目前正在处理一个问题,涉及到来自两种不同模态的信息。第一种模态是文档的视觉信息,第二种模态是文档中的文本信息。如果单独使用文档中的视觉数据或文本数据,机器学习系统可以取得不错的表现。然而,如果只使用两种模态中的一种,你需要向机器学习系统提供尽可能多的信息,以获得最佳表现。因此,你应该结合不同的模态,以确保最佳效果……
如何从您的数据中创建强大的嵌入以供 AI 使用
本文将展示您可以采取的不同方法来为您的数据创建嵌入
·发表于 Towards Data Science ·14 分钟阅读·2024 年 2 月 21 日
--
从您的数据中创建高质量的嵌入对于您的 AI 系统的有效性至关重要。本文将展示您可以采用的不同方法,将图像、文本和音频等格式的数据转换为强大的嵌入,这些嵌入可以用于您的机器学习任务。您创建高性能嵌入的能力将对 AI 系统的性能产生重大影响,因此学习和理解如何打造优质嵌入至关重要。

从一张照片中制作嵌入。图片由 ChatGPT 提供。 “制作一张展示 AI 从照片中制作嵌入的图片”提示。ChatGPT,4,OpenAI,2024 年 2 月 18 日。chat.openai.com.
简介
本文的动机在于,从您的数据中创建良好的嵌入对大多数 AI 系统至关重要,因此它是您经常需要做的事情,制作更好的嵌入是提升您所有未来 AI 系统的有效方法。创建嵌入的应用场景包括聚类、相似性搜索和异常检测,所有这些任务都可以通过更好的嵌入大大受益。本文将探讨两种主要的嵌入计算方法:使用在线模型或训练您自己的模型,接下来的部分将详细讨论这两种方法。
如何创建精美样式的 Streamlit 数据框架,第一部分:使用 Pandas Styler
Streamlit 和 pandas 的 Styler 对象并不兼容。但我们将改变这一点!
·发表于Towards Data Science ·阅读时长 7 分钟·2024 年 8 月 14 日
--

我一直是pandas 中的 styler 方法的粉丝。当我开始构建 Streamlit 应用时,我很清楚我希望为数据框架添加样式,以帮助可视化数据框架,但……惊喜!截至目前,Streamlit st.dataframe() 不支持 styler 对象,只支持数据框架对象。好吧,修正一下,它确实支持它们,但 UI 显示非常糟糕!
这就是为什么我想与大家分享我的解决方法和想法,用于在 Streamlit 中构建一个样式良好的数据框架。我们将涵盖:
-
如何在数字中添加千位分隔符。
-
如何将数字显示为百分比(将数据中的 0.24 转为 UI 中的 24%)
-
如何添加货币符号。
-
如何为单元格添加颜色。 更棒的是,我将分享我最喜欢的颜色分级函数。
-
如何添加表情符号! 是的,我们离不开表情符号😊!
st.dataframe()的默认视图
Streamlit 实际上在根据数据类型推断最佳显示效果方面表现得相当不错。想象一下……
如何创建漂亮的 Streamlit 数据框,第二部分:使用 AgGrid
pandas Styler 很酷,但 AgGrid 更酷。让你的 Streamlit 数据框更加互动和惊艳。
·发布于 Towards Data Science ·阅读时间 12 分钟·2024 年 8 月 21 日
--

在我上一篇文章中,我们讲解了如何使用 pandas Styler 对象创建漂亮的数据框。在这篇文章中,我想告诉你第二种选择:Streamlit AgGrid。我将分享如何构建像上面展示的那样的数据框。阅读完本文后,你将学到:
-
AgGrid 内的关键组件。 这些包括
gridOptions()、configure_column()、configure_default_column()和configure_side_bar()*。 -
启用通过 UI 直接过滤和聚合表格的主要选项。再也不需要为简单的转换构建临时查询了!
-
通过 Javascript 函数使数据框更美观。 如果你愿意,你可以将它们复制并粘贴到你的代码中。或者查看我的 Git 仓库。
免责声明 1:我与 AgGrid 没有任何合作关系或伙伴关系。我只是觉得这个开源产品非常有价值。AgGrid 确实有付费的分层产品,但这篇博客只会使用 AgGrid 的免费组件。
免责声明 2:除非另有说明,所有图片和 GIF 均由我本人创作。
如何使用卫星图像创建你自己的 CV 数据集:来自太空的野火
收集图像以训练 CNN
·发表于 Towards Data Science ·阅读时间:7 分钟·2024 年 5 月 9 日
--

除非另有说明,所有图像均由作者提供,基于 Sentinel-2 数据。
你是否曾想过,基于机器学习(ML)应用于卫星图像的个人项目,可能会显著增强你的数据科学作品集?或者你是否基于别人开发的数据集训练过一些模型,却没有使用过自己的数据集?如果答案是肯定的,我有个好消息要告诉你!
在这篇文章中,我将引导你通过创建一个由高分辨率卫星图像组成的计算机视觉(CV)数据集的过程,这样你就可以使用类似的方法,打造一个扎实的个人项目!
🔥问题: 野火检测(二分类任务)。
🛰️仪器: Sentinel 2(10/20 米分辨率)。
⏰时间范围: 2017/01/01–2024/01/01。
🇬🇧兴趣区域: 英国。
🐍Python 代码: GitHub.
I. 收集关于野火的信息。
在获取任何图像之前,了解野火发生的地点和时间至关重要。为了获取此类数据,我们将使用美国国家航空航天局(NASA)的“资源管理火灾信息系统”(FIRMS)档案。根据你的需求,你可以在此选择数据源和感兴趣区域,提交请求,并在几分钟内获取数据。

我决定使用基于 MODIS 的数据,形式为 csv 文件。数据包含了许多不同的变量,但我们只关心纬度、经度、采集时间、置信度和类型。最后两个变量对我们尤为重要。如你所料,置信度基本上是指野火发生的概率。因此,为了排除“误报”,我决定过滤掉置信度低于 70% 的数据。第二个重要的变量是类型,基本上它是对野火的分类。我只关心燃烧的植被,因此只保留了类别 0。最终的数据集包含了 1087 起野火事件。
df = pd.read_csv('./fires.csv')
df = df[(df.confidence>70)&(df.type==0)]
现在我们可以将热点叠加在英国的形状上。
proj = ccrs.PlateCarree()
fig, ax = plt.subplots(subplot_kw=dict(projection=proj), figsize=(16, 9))
shape.geometry.plot(ax=ax, color='black')
gdf.geometry.plot(ax=ax, color='red', markersize=10)
ax.gridlines(draw_labels=True,linewidth=1, alpha=0.5, linestyle='--', color='black')

图像由 作者 提供。
II. 收集 Sentinel-2 图像用于野火事件。
工作的第二阶段涉及我最喜欢的 Google Earth Engine(GEE)及其 Python 版本 ee(你可以查看我的其他文章,了解该服务的功能)。
在理想条件下,Sentinel-2 提供的图像具有 5 天的时间分辨率和 10 米的空间分辨率(RGB 波段)以及 20 米的空间分辨率(SWIR 波段)(稍后我们将讨论这些是什么)。然而,这并不意味着我们每 5 天就能获取一次每个位置的图像,因为有许多因素影响图像采集,包括云层。因此,我们不可能获得 1087 张图像,实际数量会远低于此。
让我们创建一个脚本,为每个点获取一张云量小于 50% 的 Sentinel-2 图像。对于每对坐标,我们创建一个缓冲区,并将其拉伸为矩形,稍后从更大的图像中剪切出来。所有图像都转换为多维数组并保存为 .npy 文件。
import ee
import pandas as pd
ee.Authenticate()
ee.Initialize()
uk = ee.FeatureCollection('FAO/GAUL/2015/level2').filter(ee.Filter.eq('ADM0_NAME', 'U.K. of Great Britain and Northern Ireland'))
SBands = ['B2', 'B3','B4', 'B11','B12']
points = []
for i in range(len(df)):
points.append(ee.Geometry.Point([df.longitude.values[i], df.latitude.values[i]]))
for i in range(len(df)):
startDate = pd.to_datetime(df.acq_date.values[i])
endDate = startDate+datetime.timedelta(days=1)
S2 = ee.ImageCollection('COPERNICUS/S2_SR_HARMONIZED')\
.filterDate(startDate.strftime('%Y-%m-%d'), endDate.strftime('%Y-%m-%d'))\
.filterBounds(points[i].buffer(2500).bounds())\
.select(SBands)\
.filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', 50))
if S2.size().getInfo()!=0:
S2_list = S2.toList(S2.size())
for j in range(S2_list.size().getInfo()):
img = ee.Image(S2_list.get(j)).select(SBands)
img = img.reproject('EPSG:4326', scale=10, crsTransform=None)
roi = points[i].buffer(2500).bounds()
array = ee.data.computePixels({
'expression': img.clip(roi),
'fileFormat': 'NUMPY_NDARRAY'
})
np.save(join('./S2',f'{i}_{j}.npy'), array)
print(f'Index: {i}/{len(df)-1}\tDate: {startDate}')
那么,SWIR 波段(特别是 11 和 12 波段)是什么?SWIR 代表短波红外。SWIR 波段是电磁波谱的一部分,涵盖了大约 1.4 到 3 微米的波长范围。
SWIR 波段因多个原因在野火分析中得到应用:
-
热敏感性: SWIR 波段对温度变化敏感,可以探测与野火相关的热源。因此,SWIR 波段可以捕捉到火灾的地点和强度信息。
-
烟雾穿透: 野火产生的烟雾会遮挡 RGB 图像的可见度(即你根本看不见云层下方)。与可见光范围相比,SWIR 辐射可以更好地穿透烟雾,从而即使在烟雾弥漫的情况下,也能进行更可靠的火灾检测。
-
火灾区域识别: SWIR 波段可以通过检测火灾造成的表面反射变化来帮助识别烧毁区域。烧毁的植被和土壤在 SWIR 波段中常常表现出独特的光谱特征,从而能够描绘出火灾影响区域的范围。
-
夜间检测: SWIR 传感器可以检测火灾的热辐射,即使在夜间,当可见光和近红外传感器由于缺乏阳光而无法工作时。这使得可以全天候连续监控野火。
所以,如果我们查看从收集的数据中随机选取的图像,我们会发现,当基于 RGB 图像时,很难判断这是不是烟雾或云层,而 SWIR 波段则清晰地显示火灾的存在。

III. 手动清理。
现在是我最不喜欢的部分。检查所有图片并确保每张图像是否有野火(记住,70%的置信度)并且图像本身是正确的,这是至关重要的。
例如,像这些这样的图像(没有热点)被采集并自动下载到野火文件夹中:


清理后的图像总数:228。
IV. 获取无火灾图像。
最后阶段是获取没有热点的图像用于我们的数据集。由于我们正在构建一个分类任务的数据集,我们需要平衡两个类别,因此我们需要获取至少 200 张图片。
为了实现这一点,我们将从英国的领土中随机抽取点(我决定抽取 300 个):
min_x, min_y, max_x, max_y = polygon.bounds
points = []
while len(points)<300:
random_point = Point(np.random.uniform(min_x, max_x), np.random.uniform(min_y, max_y))
if random_point.within(polygon):
points.append(ee.Geometry.Point(random_point.xy[0][0],random_point.xy[1][0]))
print('Done!')
然后,应用上面写的代码,我们获取 Sentinel-2 图像并保存它们。
V. 再次手动清理 😦(((
又是枯燥的阶段。现在我们需要确保这些点中没有野火、干扰或错误的图像。
做完这些后,我得到了242张这样的图像:

VI. 数据增强。
最终阶段是图像增强。简单来说,目的是利用已有的图像增加数据集中图像的数量。在这个数据集中,我们将简单地将图像旋转 180°,从而使数据集中的图片数量增加一倍!


结果。
现在可以随机抽取两类图像并可视化它们了。
无火灾:

有火灾:

就这样,我们完成了!正如你所看到的,如果使用 GEE,收集大量遥感数据并不难。我们现在创建的数据集可以用于训练不同架构的卷积神经网络(CNN)并比较它们的性能。在我看来,这是一个完美的项目,可以加入到你的数据科学作品集中,因为它解决了一个非平凡且重要的问题。
希望这篇文章对你有所启发,能带给你信息!
===========================================
参考文献:
-
NASA 近实时和 MCD14DL MODIS 活跃火灾检测(TXT/CSV 格式)。数据集。在线提供[/learn/find-data/near-real-time/firms/active-fire-data]
-
哥白尼 Sentinel 数据 2017–2024。
===========================================
我在 Medium 上的所有出版物都是免费的并且开放访问的,因此如果你在这里关注我,我将非常感激!
附言:我对(地理)数据科学、机器学习/人工智能和气候变化充满热情。如果你想一起合作某个项目,请通过LinkedIn与我联系。
🛰️更多内容请关注🛰️
如何在 Python 中对面板数据进行交叉验证

西班牙加那利群岛兰萨罗特岛的 Salinas de Janubio。图片来源:imag3s 4 u
使用 PanelSplit 进行面板数据交叉验证简介
·发布于 Towards Data Science ·6 分钟阅读·2024 年 3 月 8 日
--
动机: 作为一名处理面板数据的人员,我经常需要进行交叉验证。这涉及到在某个时间点之前进行训练,在一部分观察数据上进行测试,然后在更远的时间点继续训练,再在不同的观察数据子集上进行测试,并在面板数据集上反复进行这种过程。听起来很熟悉吧?手动实现这个过程可能会非常令人沮丧。为了简化这一过程,我创建了一个名为PanelSplit的包,它能在处理面板数据时提供帮助。
本文展示了如何在处理面板数据时使用 PanelSplit;从特征工程到超参数调优,再到生成预测,PanelSplit 都可以为您提供帮助!
什么是面板数据?
我所说的面板数据是指存在多个实体随时间变化的数据。这些实体可以是国家、个人、组织或任何其他分析单元。对这些多个实体在不同时间点进行了多次观察记录。
什么是交叉验证?
假设我们想要估计在使用模型时预测的准确性。我们该如何进行?标准方法是交叉验证,它涉及将数据分割成连续的多个折叠,每个折叠都有其独特的训练集和测试集。下方的可视化展示了时间序列数据的交叉验证过程。

时间序列交叉验证的示例。
虽然已经有一个名为 TimeSeriesSplit 的 scikit-learn 函数可以用于时间序列交叉验证,但它不适用于面板数据。与单一实体的时间序列不同,面板数据包含多个实体,我们需要一个工具来处理多个实体。
这就是 PanelSplit 的作用所在。PanelSplit 是一个允许我们将 TimeSeriesSplit 扩展到面板数据的包。它还提供了转换、预测等功能,但在本文中,我将只介绍基本内容。
使用 PanelSplit 执行交叉验证
现在我们已经介绍了什么是面板数据,以及在这种环境下交叉验证的样子,让我们看看如何使用 PanelSplit 执行交叉验证。
首先,让我们生成一些示例数据进行操作:
import pandas as pd
import numpy as np
# generate example data
num_countries = 3
years = range(2000, 2005)
num_years = len(years)
data = {
'country_id': [c for c in range(1, num_countries + 1) for _ in years],
'year': [year for _ in range(num_countries) for year in years],
'y': np.random.normal(0, 1, num_countries * num_years),
'x1': np.random.normal(0, 1, num_countries * num_years),
'x2': np.random.normal(0, 1, num_countries * num_years)
}
panel_data = pd.DataFrame(data)
# display the generated panel data
display(panel_data)

生成的面板数据。观察的国家有 3 个,时间范围是 2001–2004 年。
生成面板数据集后,我们现在可以应用 PanelSplit。
初始化 PanelSplit
当我们初始化 PanelSplit 时,需要定义我们将使用的交叉验证方法。
-
periods 参数接受时间序列。在这种情况下,序列是年份列。
-
n_splits、gap 和 test_size 是 TimeSeriesSplit 用来拆分时间序列的参数。
-
通过指定 plot=True,可以生成一张可视化图,描述每次拆分中的训练集和测试集。
!pip install panelsplit
from panelsplit import PanelSplit
panel_split = PanelSplit(periods = panel_data.year, n_splits = 3, gap = 0, test_size=1, plot=True)

初始化 PanelSplit 时的输出(当 plot=True)。根据我们提供的参数,拆分次数为 3 次,训练集和测试集之间没有间隔,每次拆分的测试集大小为一个周期。
理解 PanelSplit 的工作原理
为了更好地了解拆分的情况,让我们使用 split() 函数返回每次拆分中的不同训练集和测试集。
splits = panel_split.split()
splits 对象包含了交叉验证过程中的 3 次拆分。在每次拆分中,有一个列表,其中包含训练集索引(第一个项)和测试集索引(第二个项)。这些索引是布尔值,表示某一行是否属于某个特定拆分的训练集/测试集。可以利用这些索引对数据进行过滤,选择不同的数据子集,如下图所示。

演示每次拆分中的不同训练集和测试集。
超参数调整
现在我们已经创建了 PanelSplit 的实例,让我们进行一些 超参数调整!
在这里,我们进行一个基本的超参数搜索,使用 Ridge 模型,并指定GridSearchCV的 cv 参数为 panel_split。在 GridSearchCV 的拟合过程中,它调用 panel_split 的 split()函数,返回每个训练集和测试集的索引。它使用这些索引来过滤提供给 fit()函数的 X 和 y 数据。
from sklearn.linear_model import Ridge
from sklearn.model_selection import GridSearchCV
param_grid = {'alpha':[.1, .5]} # define the hyper-parameter grid space
# define the gridsearch and call fit, specifying panel_split for the cv argument
gridsearch = GridSearchCV(estimator = Ridge(), param_grid=param_grid, cv=panel_split)
gridsearch.fit(X = panel_data[['x1','x2']], y = panel_data['y'])
print(gridsearch.best_params_)

在这次搜索中,Ridge 模型的最佳 alpha 值为 0.5。
太棒了!我们已经找到了最佳的超参数组合。现在我们可以使用这些参数进行预测。
注意:在实际应用中,我们会区分用于超参数调整的测试集和用于评估性能的测试集,但在这个示例中我们将验证集和测试集保持一致。
使用 cross_val_fit_predict 生成预测
使用 PanelSplit 生成预测非常简单。
使用cross_val_fit_predict,我们指定要使用我们最好的 Ridge 模型、我们的 X 和 y,PanelSplit 将在每个训练集上进行拟合,并在每个测试集上进行预测,每个分割都是如此。
predictions, models = panel_split.cross_val_fit_predict(estimator = Ridge(gridsearch.best_params_),
X = panel_data[['x1','x2']],
y = panel_data['y'])
预测结果以及拟合模型会返回。如果我们想要在预测中包含标识符,可以使用gen_test_labels生成标签,然后在我们的 predictions_df DataFrame 中创建一个新的 Pandas Series。
predictions_df = panel_split.gen_test_labels(panel_data[['country_id','year']])
predictions_df['y_pred'] = y_pred
display(predictions_df)

预测的 DataFrame。
PanelSplit 还能做什么?
这只是一个基础演示,但 PanelSplit 可以做更多的事情!例如:
-
使用cross_val_fit_transform,我们可以在训练集上进行拟合,在测试集上进行转换。如果我们有需要插补的缺失特征,这非常有用。
-
如果我们想要对数据进行缩放,并且每个分割需要它自己独立的“快照”以保持缩放变换的独立性怎么办?我们可以使用gen_snapshots来实现这一点!或者在 cross_val_fit_predict 中使用 scikit-learn 管道作为估算器 😃
-
如果我们缺少某个时间段怎么办?通过在初始化时使用unique periods参数和drop_splits参数,PanelSplit 可以处理这个问题,并在没有观测值的地方丢弃分割。
如果您想查看更多示例并亲自尝试 PanelSplit,可以查看我创建的Jupyter notebook,其中我介绍了一些额外的功能。
这是我写的第一个包,所以在这个项目中我学到了很多东西。感谢您的阅读,希望 PanelSplit 能在您的下一个面板数据项目中提供帮助!
注意:除非另有说明,所有图片均由作者提供。
如何通过提示压缩将 RAG 成本降低 80%
通过提示压缩加速推理
·发表于Towards Data Science ·阅读时间 11 分钟·2024 年 1 月 4 日
--

图片来源:作者。AI 生成。
推理过程是大规模语言模型使用中显著增加金钱和时间成本的因素之一。对于较长的输入,这一问题尤为突出。以下是模型性能与推理时间之间的关系。

性能评分与推理吞吐量[1]
快速模型每秒生成更多的标记,但在 Open LLM 排行榜中往往得分较低。扩大模型规模可以提高性能,但代价是推理吞吐量降低。这使得在实际应用中部署这些模型变得困难[1]。
提高 LLM 的速度并减少资源需求将使得它们能被个人或小型组织更广泛使用。
提出了多种提高 LLM 效率的解决方案;有些专注于模型架构或系统。然而,像 ChatGPT 或 Claude 这样的专有模型只能通过 API 访问,因此我们无法更改其内部算法。
我们将讨论一种简单且廉价的方法,该方法仅依赖于改变输入给模型的提示……
如何处理时间序列中的异常值
理解、检测和替换时间序列中的异常值
·发表于Towards Data Science ·阅读时长:6 分钟·2024 年 8 月 31 日
--

图片由Milton Villemar提供,来源于Unsplash
在这篇文章中,我们将探索:
-
时间序列异常值的不同类型
-
基于预测和基于估计的方法来检测异常值
-
如何通过替换处理不需要的异常值
异常值的类型
异常值是指那些与正常行为显著偏离的观测值。
时间序列可能会因为一些不寻常且不可重复的事件而出现异常值。这些异常值会影响时间序列分析,并使实践者得出错误的结论或产生不准确的预测。因此,识别和处理异常值是确保可靠时间序列建模的关键步骤。
在时间序列中,异常值通常分为两种类型:加性异常值和创新异常值。
加性异常值
加性异常值是指相较于历史数据,某个观测值显得异常高(或低)。
一个加性异常值的例子是由于促销或相关的病毒内容导致某个产品销量激增。有时这些异常值的产生是由于数据收集错误。加性异常值与...
如何交付成功的数据科学咨询项目
数据科学咨询
成功交付数据科学咨询项目并建立持久客户关系的关键建议
·发表于 Towards Data Science ·阅读时长:9 分钟·2024 年 7 月 13 日
--

图片由作者使用 DALL-E 生成
介绍
我不羞于承认:数据科学咨询并不总是轻松的!它可能是残酷的——尤其是在高级职位上,当你需要通过产生销售来保持竞争力时。即使让客户满意是你的首要任务,这在数据科学项目中也并非总是简单的事情。
回顾十多年交付数据科学和数据工程项目的经验——其中大部分是作为顾问——我见过一些项目为客户带来了巨大的价值,但我也见过一些项目失败,未能交付理想结果,通常是由于规划不当、期望不匹配和技术难题。
很明显,成功的数据科学咨询不仅仅是成为 Python 和 R 的高手——在 Hackerrank 的数据科学编程竞赛中表现出色;它远比这更深刻,能够在项目交付中融合战略、技术和分析。在本文中,我将从成功和具有挑战性的项目中汲取经验,并说明数据科学咨询如何与管理、IT 和分析咨询交汇。通过了解这些更传统和成熟的咨询角色之间的差异与相似性,我们可以更清晰地看到如何在我们的数据科学项目中交付持久且有影响力的解决方案。
与传统咨询角色的相似性与差异性

数据科学咨询 / 作者提供的图片
管理咨询与数据科学咨询
管理咨询项目和数据科学咨询项目通常有许多相同的特点。它们都旨在提升业务绩效,并且常常交付具有战略重要性的项目。它们通常也涉及高级管理人员——至少作为项目所有者——并且都需要详细的前期规划。然而,虽然我发现管理咨询项目通常遵循传统的瀑布模型,但数据科学咨询则可以从结合瀑布中的结构化规划与敏捷迭代的混合方法中受益更多。
就交付物而言,管理咨询项目通常会产生战略计划、组织评估或定价建议,通常以 PowerPoint 演示文稿或 Excel 工作簿的形式呈现。与此相对,数据科学项目的主要交付物通常是预测模型、数据管道和仪表盘。
反馈周期也有所不同,传统的管理咨询项目通常会有计划性的定期反馈,而数据科学咨询项目则更依赖于持续的利益相关者参与和迭代反馈周期,以确保项目的成功。
IT 咨询与数据科学咨询
IT 咨询和数据科学咨询都需要强大的技术专长以及对 IT 基础设施的理解,有些人可能认为数据科学咨询是 IT 咨询的一个子类别。我不认同这一观点,但这两个领域确实有相当大的重叠。
虽然 IT 咨询项目产生系统架构设计、实施计划和更多核心系统软件开发,但数据科学咨询更侧重于生成预测模型、供给模型的数据管道和数据驱动的洞察。
此外,数据科学项目通常与组织的业务部门密切相关,并且在许多情况下,项目的负责人和赞助人是非 IT 领域的领导。例如,你可能会直接向 CMO(首席营销官)或 CDO(首席数据官)汇报,而不是向 CIO(首席信息官)或 CTO(首席技术官)汇报,这通常出现在更多由 IT 驱动的项目中。这意味着你的方法和策略需要与纯 IT 项目有所不同,你通常需要更多地考虑连续性、运营和组织的成熟度。(CIO 和 CTO 通常已经有更完善的系统来应对这些问题。)
然而,随着数据科学在过去几年的成熟,我们看到像 MLOps 这样的领域有了显著发展,同时项目中也出现了将代码开发和编写方式更接近现代软件开发的趋势。我认为这是数据科学朝着更专业、更结构化领域发展的自然过程。
分析咨询与数据科学咨询
最后,将分析咨询与数据科学咨询进行比较,我们发现它们都专注于数据,通常涉及较短的项目周期和针对性的交付物。它们还强调基于数据和反馈的快速迭代和持续优化。然而,分析咨询通常解决更狭窄范围的特定业务问题,而数据科学咨询则整合了更广泛的战略对齐和更复杂的模型。
在分析咨询中,经典的交付物通常是数据分析报告、数据集市和仪表板,而数据科学咨询的交付物——正如我们之前提到的——更多集中在数据管道、模型以及战略性和集成的数据产品上。
最后,项目的评估方式通常也不同。分析类项目更多关注分析的准确性和洞察力,而数据科学项目通常还会依据预测准确性和投资回报率来评估。
对批判性读者的备注:以上描述绝非一成不变,旨在展示不同咨询类型之间的一般差异和相似性。在许多情况下,它们显然会比上述描述更加分化或融合。
成功的数据科学咨询的关键见解
鉴于数据科学咨询与其他咨询类型之间的上述相似性和差异,提出如何调整我们的方法以确保项目的长期成功和可行性是很自然的。除了显而易见的要素,如高质量的交付物、及时的项目交付和强有力的利益相关者管理,还有哪些其他要素是必须到位的才能成功?
确保稳健的数据产品
管理咨询通常专注于即时的组织变革和一次性的交付物,而数据科学咨询则需要对稳健性和可持续性保持长远的视角。这带来了一些后果。你可能会并且将会被评判你工作的持续表现,并且应该采取措施,确保你不仅在交接时交付出好的结果,而且还能够在未来的几年里持续交付好的成果。(这与 IT 咨询类似,在 IT 咨询中,持续的表现和维护至关重要。)
例如,我曾构建过已在生产中使用超过 6 年的数据产品!我见证了由于数据管道不够强健,导致系统崩溃和模型结果错误的直接后果。我也曾见过模型变量和标签随时间显著漂移,导致系统性能下降,甚至在某些情况下产生完全错误的见解。

作者使用 DALL-E 创作的图片
我知道这显然不是最吸引眼球的话题,在预算紧张和时间有限的项目中,很难提出额外花时间和资源来构建强健的数据管道和监控变量漂移的论点。然而,我强烈建议你花时间和客户一起探讨这些话题,并将其直接整合进你的项目时间表中。
-
关注长期可持续性。
-
实施强健的数据管道。
-
持续监控模型和变量漂移。
我在之前的一篇文章中写过数据管道的一个方面(变量的独热编码),旨在阐述这个话题,并提供 Python 和 R 的解决方案。
生产级独热编码技术在 Python 和 R 中的应用
[towardsdatascience.com
文档与知识传递
在数据科学咨询中,适当的文档和知识传递至关重要。与可能涉及较少复杂模型的分析咨询不同,数据科学项目需要详细的文档以确保项目的持续性。客户经常面临人员变动,良好的文档化流程有助于减轻信息丧失的风险。我曾多次接到之前客户的联系,请求我解释我们所构建的模型和系统的各个方面。这并不总是容易的,尤其是在你已经几年没见过代码库的情况下——此时,拥有适当文档化的 Jupyter 笔记本或 Markdown 文档,描述决策过程和分析,就显得非常有用。这确保了任何决策或初步结果都能轻松追溯和解决。
-
确保详尽的文档记录。
-
使用 Jupyter 笔记本、Markdown 文档或类似工具。
-
促进知识传递,减轻人员变动的影响。
构建端到端解决方案
构建端到端解决方案是数据科学咨询中的另一个关键考虑因素。与可能专注于提供见解和报告的分析咨询不同,数据科学咨询需要确保模型的可部署性和可操作化。这类似于 IT 咨询,集成到现有的 CI/CD 管道中至关重要。
我曾见过公司因人员变动和未完成的集成任务,导致从模型开发到生产部署浪费了数年的时间。如果我们坚持将项目推进至完全准备好的生产状态,客户本可以更早地享受到模型的全部好处。考虑到项目成本可能达到数百万欧元,这将是一次显著的改进。
-
构建可部署的模型。
-
确保可操作化。
-
集成到现有的 CI/CD 管道中。
可视化工件
包括视觉化工件,如仪表板或小部件,有助于展示项目所创造的价值。虽然管理咨询的交付物包括战略计划和评估——通常是以一次性的 PPT 形式呈现——但数据科学咨询则得益于提供持续洞察的视觉工具,能够反映解决方案的影响和收益。这些工件充当了项目价值的提醒,并帮助衡量其成功,类似于分析咨询中可视化的作用。
我最成功的项目之一是为客户构建了一个定价解决方案,他们开始直接在每月的定价委员会会议中使用仪表板组件。尽管仪表板只是项目的一小部分,但它是管理层和公司高层唯一可以互动的部分,因此成为了我们工作的强有力提醒。
-
创建可视化工件,如仪表板。
-
通过可视化展示项目价值。
-
使用工件来衡量成功,并与客户保持相关性。
评估组织成熟度
在规划项目时评估组织的成熟度对于避免过度工程化解决方案至关重要。根据客户的成熟度水平调整解决方案的复杂性,能够确保更好的采纳和可用性。请始终记住,当你完成项目时,所有权通常会转交给内部数据科学家和数据工程师。如果客户拥有一支由 20 名数据科学家组成的团队,并且拥有现代化的数据基础设施,能够将你的模型直接集成到现有的 DevOps 中,那当然非常好,但这种情况并不常见。相反,设想一种场景:你正在为一家拥有 20 名员工的公司开发工具,而这家公司只有一名初级数据科学家和一名过度劳累的数据工程师。你会如何调整你的策略?
-
评估组织和分析成熟度。
-
避免过度工程化解决方案。
-
根据客户准备情况调整复杂性。
遵循 IT 开发最佳实践
遵循 IT 开发最佳实践变得越来越重要,并且在数据科学咨询中往往是必需的。与可能不涉及大量编码的分析咨询不同,数据科学咨询应该忠实于软件开发实践,以确保可扩展性和可维护性。这与现代 IT 咨询相似,在现代 IT 咨询中,编写模块化、文档完善的代码,并包括测试用的样本数据,是至关重要的实践。
这也与前面提到的文档和知识传递相关。适当文档化并结构化的代码,打包成易于安装的软件包和库,比起成千上万行杂乱无章的代码,更容易维护和管理。当人员变动发生时,如果代码已经得到了适当的开发,你将处于一个更有利的位置。
-
遵循 IT 开发最佳实践。
-
编写模块化且文档齐全的代码。
-
包括用于测试的示例数据。
我想以一段史蒂夫·乔布斯谈咨询的视频来结束这篇文章。他显然对传统的咨询顾问并没有太多同情,但我认为作为数据科学顾问,我们需要更加忠实于他关于真正承担我们所提供的建议和产品的想法。我们的评估标准不仅仅是项目的成功完成,还包括我们创造的持续和长期价值。
史蒂夫·乔布斯谈咨询,来自 YouTube / 视频由 Malonus 提供
结论
数据科学咨询是一个激动人心且复杂的职业,它融合了管理、IT 和分析咨询的内容。通过理解各自的相似性与差异,并应用每个领域的最佳实践,你可以交付成功的数据科学咨询项目,为客户创造长期价值。我认为,如果你想建立一个成功的数据科学咨询业务,这是必不可少的。我的个人经验突出了稳健解决方案、全面文档、端到端交付物、可视化产物、评估组织成熟度以及遵循 IT 开发最佳实践在确保数据科学咨询项目成功中的重要性。
感谢阅读!
如果你喜欢这篇文章并希望访问更多我的内容,请随时在 LinkedIn 上与我联系,链接为 https://www.linkedin.com/in/hans-christian-ekne-1760a259/ ,或者访问我的网页 https://www.ekneconsulting.com/ 以探索我提供的一些服务。如果有任何问题,请随时通过电子邮件联系我,邮箱地址是 hce@ekneconsulting.com
如何使用 FastAPI、Docker 和 GCP 部署机器学习解决方案
带有 Python 示例代码的实操指南
·发布于Towards Data Science ·10 分钟阅读·2024 年 6 月 5 日
--
这是关于全栈数据科学的更大系列文章中的第五篇文章。在本文中,我将介绍基于机器学习的搜索 API 的部署过程。虽然我们可以用无数种方式实现这一目标,但我在这里讨论了一种简单的三步方法,它可以应用于几乎任何机器学习解决方案。示例代码可以在GitHub 仓库中免费获取。

图片由Andy Hermawan提供,来自Unsplash
尽管机器学习看起来主要是训练复杂的模型,但在现实世界中,一个模型(单独存在时)并不会产生价值。要将机器学习模型转化为机器学习解决方案(即具有价值的东西),我们必须“部署它”。
这可以采取多种形式。例如,创建一个 Web 界面,用户可以与模型互动,将模型集成到现有的软件系统中,甚至仅仅为开发者设置一个 API 来访问该模型(想想 OpenAI 等等)。
我在这里描述的三步策略与所有这些示例兼容。它包括:
-
创建推理 API(使用 FastAPI)
-
将 API 容器化(通过 Docker)
-
在云平台上运行容器(这里我使用 GCP)
如何设计批处理
从业务和技术角度理解批处理
·发布于 Towards Data Science ·阅读时间 10 分钟·2024 年 1 月 10 日
--

摄影:由Dannie Sorum提供,来自Unsplash
我们生活在一个每个人类互动都会成为系统中的事件的世界,不管是在线或线下购买衣服、浏览社交媒体,还是打车使用 Uber。这些事件都在某种程度上被处理。某些事件需要快速响应,因此它们会立即处理。例如,当完成一次 Uber 打车后,你会在几秒钟内收到收据。输入和输出通常是 1 对 1 的。

2 种不同的数据处理模式
另外一些事件在后台集体处理时能创造更大的价值。举个例子,生成月度报告时,你需要将本月的所有交易合并在一起。输入和输出通常是多对 1。这也叫做批处理。
作为数据从业者,我们每天都在处理批次数据。它是一个传统但依然非常强大的数据处理方法,每个数据人都应该了解。由于它是如此基础的领域,值得深入探讨。在本文中,我将从批处理的应用场景开始——即企业如何从中受益,接着讲述它的…
如何设计以数据为驱动的故事
逐步指南
·发布于 Towards Data Science ·18 分钟阅读·2024 年 3 月 1 日
--

来源:作者在 ChatGPT 中生成的图像。
本文内容是什么?
今天,一切都依赖数据!
在我去年九月的文章中,我提到过,今天无法处理数据——即分析和解读数据——已成为“文盲”的标准。
为数据专业人士提供的逐步教程
towardsdatascience.com
想想我们周围海量的数据,这些数据不仅被公司或政府使用,甚至我们自己也在使用。考虑一下你在手机上花了多少时间,浏览推文、阅读信息和观看视频,也就是在吸收大量数据,而这些常常是你在进行其他数据密集型任务时,比如通勤时,所做的事情。为进入新市场投资制定商业计划并没有想象的那么复杂,稍微加上专业知识就可以了。你可能会争辩说,像大型语言模型(LLMs)或生成性 AI 这样的工具可以帮助我们。而你说得对。然而,这些工具的泛滥和日益复杂化,使得它们的使用本身变成了一个挑战。
当今世界几乎没有哪个领域能在没有数据的情况下运作,尤其是在商业领域。从核心的复杂性到……
如何设计更好的指标
来自像 Uber 和 Meta 等领先公司的 9 个最佳实践
·发布于 Towards Data Science ·阅读时间:11 分钟·2024 年 6 月 26 日
--

图片来自作者(通过 Midjourney 创建)
指标是一个强大的工具;它们帮助你衡量你关心的事情。拥有远大的目标是很好的,但为了知道自己是否在取得进展、激励团队并创造问责制,你需要能够用数字来表达这些目标。
但这说起来容易做起来难。有成百上千的指标看似在衡量相同的东西,每天都有新的时髦指标被发明出来。你应该使用哪些指标,应该避免哪些指标呢?这篇文章将帮助你做出决定。
在过去的十年里,我一直在研究和关注度量指标,发现有一些通用的原则可以区分好的指标和坏的指标:
原则一:一个指标应该是你试图衡量的事情的良好代理
你通常不能直接衡量你关心的具体事物。
假设我的目标是衡量我的新闻简报文章的质量;我该怎么做呢?“质量”是主观的,并没有公认的评估公式。因此,我必须选择一个最佳(或最不坏的)代理来衡量我的目标,是我实际能够衡量的。在这个例子中,我可以使用开启率、点赞数等作为质量的代理。

图片来自作者
这与人们常说的“指标的相关性”密切相关:如果你改善这个指标,它会为业务创造价值吗?如果不会,那为什么还要衡量它?
例如,假设你在 Uber 工作,想了解你的供应端是否健康。你可能认为平台上司机的数量,或者他们在应用程序上在线的时间,是一个不错的衡量标准。
这些指标并不糟糕,但它们并不能真正告诉你你的供给端是否真正健康(即足以满足需求)。可能是需求超出了司机的增长,或者大部分需求增长发生在早晨,而供给大多是在下午增长。
更好的指标应该是一个结合供给和需求的指标;例如,骑手打开应用时没有司机可用的次数。
[## 每当 Torsten Walbaum 发布内容时,您将收到电子邮件通知。
每当 Torsten Walbaum 发布内容时,您将收到电子邮件通知。通过注册,如果您还没有账户,将会创建一个 Medium 账户…
medium.com](https://medium.com/@twalbaum/subscribe?source=post_page-----9bad7bc8c875--------------------------------)
原则二:该指标应该容易计算和理解
人们喜欢复杂的指标;毕竟,复杂的分析正是你支付给数据团队的费用,对吧?但复杂的指标有几个原因是危险的:
-
🤔 它们难以理解。 如果你不完全理解一个指标是如何计算的,你就不知道如何解读其变化,或者如何影响它。
-
🧑🔬 它们迫使分析的集中化。 通常,数据科学团队是唯一能够计算复杂指标的团队。这剥夺了其他团队进行去中心化分析的能力。
-
⚠️ 它们容易出错。 复杂的指标通常需要多个团队的输入;我已经数不清多少次因为上游的某个输入出现问题而发现了错误。更糟糕的是,由于公司中只有少数人能够计算这些指标,同行评审非常少,错误往往会长时间无人察觉。
-
🔮 它们通常涉及预测。 许多复杂的指标依赖于预测(例如,根据过去的数据预测群体表现)。这些预测通常不准确,并且随着新数据的到来而不断变化,从而造成困惑。
以 LTV:CAC 为例:
除了它不是最适合的指标外,它还很危险,因为它计算起来非常复杂。分子 CAC 需要你基于群体将市场营销和销售的各种成本进行汇总,而分母 LTV 则是基于保留率、增销等因素的预测。
这类指标是你会在两年后才意识到方法论存在问题,并且你一直在看“错误”的数据。
原则三:一个好的(操作性)指标应该是响应式的
如果你希望持续管理一个基于某个指标的业务,它需要是响应式的。如果一个指标滞后,即需要几周或几个月的时间才能看到变化影响到指标,那么你就无法获得反馈回路,也就无法进行持续改进。
你可能会倾向于通过预测变化的影响来解决这个问题,而不是等待它们在指标中显现出来,但这通常是不明智的(参见上面的原则#2)。
当然,像收入这样的滞后指标很重要,需要跟踪(特别是对于财务或领导层),但大多数团队应该将大部分时间花在查看领先指标上。
原则 4:一个指标应该难以被操控
一旦你选择了一个指标并让人们对改进这个指标负责,他们就会找到最有效的方式去实现目标。通常,这会导致意想不到的结果。这里有一个例子:
-
Facebook 希望向用户展示相关内容,以增加他们在网站上的停留时间
-
由于“相关性”很难衡量,他们使用参与度指标作为代理(点赞、评论等)
-
出版商和创作者理解算法的运作方式,并找到心理操控的方式来增加参与度 ➡ 点击诱饵和愤怒诱饵应运而生
“当一个衡量标准成为目标时,它就不再是一个好的衡量标准。”
—— 古德哈特定律
在上面的例子中,Facebook 可能会对质量的下降感到无所谓,只要用户继续在平台上花时间。但在许多情况下,如果指标在大规模上被操控,可能会造成严重的损害。
假设你提供了一个推荐奖励,用户通过推荐他人注册获得奖励。最有可能发生什么情况?人们会尝试创建大量虚假账户来领取奖励。一个更好的推荐标准应该要求在平台上进行一定金额的交易(例如,$25)才能获得奖励。
所以,防止操控的一种方式是设计指标以限制你预见到的 unwanted 行为。另一种方法是将指标配对。 这种方法由安迪·格罗夫在他的《高效能管理》一书中提出:
“因为指标会引导一个人的活动,所以你应该警惕过度反应。你可以通过将指标配对来做到这一点,这样就能同时衡量效果和反效果。”
—— 安迪·格罗夫,《高效能管理》
那在实践中是什么样的呢? 如果你只通过“首次响应时间”来激励客户支持人员,因为你希望客户能够立即得到帮助,他们就会简单地对每个新工单发送一条通用消息。但如果你将其与工单解决时间(或客户满意度)作为目标配对,你就能确保代理商真正关注更快地解决客户问题。
原则 5:一个好的指标不应有任意的门槛
许多你会在科技公司看到的流行指标都与某个门槛相关。
例如:
-
至少有 5 个连接的用户
-
视频观看超过 1,000 次
这有道理;通常,单纯采取某个行动本身并不是一个非常有价值的信号,你需要设定一个阈值,才能让这个指标变得有意义。看完大部分视频的人与仅仅点击视频的人是非常不同的。
但是:阈值不应是任意的。
不要因为“1,000 次观看”是一个很好的整数就选择它;阈值应该基于数据来确定。1,000 次观看的视频之后会有更高的点击率吗?或者会产生更多后续的内容?更高的创作者留存率?
例如,Twitch 衡量用户观看流媒体至少五分钟的次数。虽然数据显然对这个选择有所影响,但并不完全清楚他们为什么最终选择了五分钟。
在 Uber,我们尝试让数据告诉我们阈值应该设在哪里。例如,我们发现附近有很多其他餐厅的餐厅在 UberEats 上更可靠,因为这样更容易保持骑手的活跃度。我们根据图表中看到的“肘部”设定了我们认为低密度餐厅的阈值:

作者提供的图片
这种方法在许多业务领域都很有效;例如,我们还发现,一旦骑手或司机在平台上完成了一定数量的初始行程,他们就更可能留存。
你不总是能找到像这样的“魔法”阈值,但在接受一个任意值之前,你应该尽量识别出一个阈值。
原则六:好的指标创造背景
没有背景的绝对数字很少有帮助。你经常会看到像这样的新闻发布:
-
“为我们的客户处理了 10 亿条数据”,或者
-
“我们平台向创作者支付了 1 亿美元的收入”
这些数字本身并没有任何意义。为了使它们有意义,必须将它们放入背景中。平台上的每个创作者平均赚了多少钱?在什么时间范围内?换句话说,将绝对数字转化为比例,就能为它们增加背景信息。

作者提供的图片
当然,在上述例子中,有些是有意为之;公司并不希望公众知道这些细节。但这个问题不仅仅局限于新闻稿和博客文章。
从绝对值来看你的销售管道可能告诉你它是否随着时间增长;但要让它真正有意义,你必须将它与销售团队的规模或他们的配额联系起来。这给出了管道覆盖率,即管道与配额的比例,这是一个更有意义的指标。
创建这些类型的比例也使得比较更加有洞察力和公平;例如,比较每个部门的收入会让大部门看起来更好,但比较每个员工的收入则能真实反映生产力。
原则七:一个指标需要有明确的负责人来控制该指标
如果你想看到某个指标的变化,你需要有一个人负责改进它。
即使多个团队的工作有助于推动该指标,你仍然需要一个单一的“负责人”来负责实现目标(否则你最终只会看到很多相互指责)。
这里有三种可能的问题场景:
-
没有负责人。 如果没有人专注于改进该指标,那么它将继续沿着当前的轨迹发展。
-
多个负责人。 不明确的责任分配会导致摩擦和缺乏问责。例如,在 UberEats 曾有一段时间,不清楚某些指标是由本地城市团队还是中央运营团队负责。在一段短时间内,我们花更多时间开会讨论这个问题,而不是实际执行。
-
缺乏控制。 指派一个无力(或感觉无力)推动指标变化的负责人,是失败的又一个原因。这可能是因为负责人没有直接控制该指标的杠杆,缺乏预算,或者缺乏其他团队的支持。
原则 8:好的指标最小化噪音
一个指标只有在你能够解读其变化时才具有可操作性。为了获得清晰的解读,你需要尽可能消除“噪音”源。
例如: 假设你是一个小型 B2B SaaS 初创公司,并且将网站流量作为你漏斗顶部的领先指标。如果你仅仅看“原始”访问量,你将会受到自己员工、朋友和家人以及现有客户访问网站的噪音干扰,你可能会发现网站流量与漏斗下游指标之间几乎没有关联。
如果可能,排除这些流量来源,以便你能更好地了解你的潜在客户漏斗的实际情况。
原则 9:某些指标应成为行业标准
对于某些指标,能够在公司之间进行比较是非常重要的。例如,如果你在 B2B SaaS 领域,你的 CFO 会希望将你的净收入保持率(NRR)、客户获取成本回收期(CAC Paybacks)或魔法数字(Magic Number)与竞争对手进行比较(你的投资者也会有同样的需求)。
如果你以非市场标准的方式计算这些指标,你将无法通过基准测试获得任何洞察,还会引起大量混乱。这并不是说你不应该创造新的指标;事实上,我在我的职业生涯中自己也创造了几个(并且可能会写一篇单独的文章,介绍如何做到这一点)。
但大多数财务和效率指标的定义最好不要随意修改。
总结
以上所有内容都说明了一点:没有适用于所有场景的完美指标。每个指标都会有缺点,你需要选择“最不糟糕”的那个。
希望以上原则能帮助你做到这一点。
想要更多实用的分析建议,可以考虑在 Medium 上关注我,或者在LinkedIn或Substack上关注我。
奖金:度量标准的耻辱殿堂
一个度量标准不应该被创造出来以支持商业叙事或掩盖不便的真相。这个听起来很显而易见,但实际上有很多奇怪的度量标准是为了这个目的而创造的:
1. WeWork 的社区调整 EBITDA:
最“创造性”的度量标准颁给了 WeWork 的社区调整 EBITDA。
调整后的 EBITDA 一直被认为是一个什么都可以做的领域;但 WeWork 的度量标准乍一看显得特别“有创意”。除了利息、税收、折旧和摊销外,WeWork 还扣除了如市场营销、一般及行政费用等项目。
其目的是展示单位经济学的一个衡量标准,这并不罕见。但 WeWork 并没有很好地解释这一度量标准的目的,导致了(可以理解的)反感和嘲笑。
2. 埃隆·马斯克为 X 公司提出的“未后悔用户分钟数”:
当你的核心互动度量标准,如 DAU,暴跌时,你会怎么做?你告诉人们这些度量标准并不重要,并创造一个新的度量标准来专注于它。于是:未后悔用户分钟数。
你问,怎么衡量这个?X 之外的人都不知道;如果我要猜的话,X 内部的人可能也不知道。
社交媒体绝对是一个可以从单纯的互动度量转向更考虑用户体验质量的领域,但这更可能是一个(掩饰得很薄弱的)尝试,用来转移人们对 X 公司困境的注意力。
3. Netflix 2019 年“观看”定义的变化:
你如何在不做任何实际操作的情况下提高平台上的互动?
你改变了互动的计量标准!
直到 2019 年底,Netflix 将任何观看超过 70%的电影或电视剧集视为一次观看。到了 2019 年底,他们将阈值设定为 2 分钟;这甚至不足以看完大部分电视剧的开场片段。所以,如果有人在片头字幕播放之前退出,也算作一次观看。毫无意外,新数据显示的数字大约高出了 35%。
Netflix 此后再次更改了他们的度量标准,公平地说,新的标准看起来更为合理(总观看时长除以片长;即实际上是“完整观看”)。
如何在没有标签的情况下检测概念漂移
使用参考窗口进行无监督变化检测,并附有 Python 示例
·发表于Towards Data Science ·6 分钟阅读·2024 年 3 月 12 日
--

图片由Chris Czermak提供,来源于Unsplash
在上一篇文章中,我们探讨了概念漂移的基础知识。当数据集的分布发生变化时,就会发生概念漂移。
本文将继续探讨这一主题。在这里,你将学习如何在没有标签的情况下检测概念漂移。这个任务具有挑战性,因为没有标签我们无法评估模型的表现。
让我们深入了解。
介绍
随着时间变化的数据集容易受到概念漂移的影响。分布的变化可能会破坏模型及其预测的准确性。因此,检测并适应这些变化至关重要,以保持模型的最新性。
大多数变化检测方法依赖于追踪模型的错误。其思路是,当错误显著增加时触发警报。然后,某种适应机制启动,例如重新训练模型。
在上一篇文章中,我们讨论了在某些情况下获取标签可能会很困难的问题。这种情况在许多领域中都有出现,例如欺诈检测或信用风险评估。在后者中,个人违约所需的时间(以及提供标签的时间)是一个关键因素……
如何在卫星影像中检测洪水,案例研究:迪拜洪水
使用简单的分类方法检测和监测卫星影像中的洪水区域
·发布于 Towards Data Science ·阅读时间 11 分钟 ·2024 年 4 月 24 日
--
在周末,当我刷 Twitter 动态时,看到有关迪拜机场在一次罕见的暴风雨中被淹的新闻(24 小时降水量超过 250 毫米!!)。我希望能够找到清晰的卫星图像,展示一种简单的方法来区分被淹和未被淹的区域。幸运的是,Sentinel-2 卫星在 4 月 7 日(洪水前)和 4 月 17 日(洪水后)拍摄了两幅几乎没有云的迪拜图像。这些图像激发了我写一篇关于使用卫星图像检测洪水事件的文章。
在这篇文章中,我们首先通过 Python 脚本下载了迪拜某洪水地区的 Sentinel-2 卫星影像。然后,我们将使用 rasterio 包读取影像,并利用近红外和绿色波段计算归一化差异水体指数(NDWI)。之后,我们将绘制洪水前后影像的 NDWI 直方图。通过比较这些直方图,我们可以看到洪水前的干旱区域如何转变为洪水后的湿润区域。最后,我们将通过从直方图分析中提取的阈值分离出被淹没的像素,并绘制洪水区域地图。如果这听起来很有趣,继续阅读吧!
目录
-
🌅 引言
-
💾 下载 Sentinel-2 卫星影像
如何开发一个有效的 AI 驱动法律助手
创建基于机器学习的法律判决搜索工具
·发布于 Towards Data Science ·阅读时长 10 分钟·2024 年 11 月 27 日
--
在这篇文章中,我描述了如何创建一个应用程序来搜索挪威的最高法院判决。这个应用程序是一个有用的工具,可以快速获取关于不同主题的判决信息,特别适合你想了解最高法院在特定问题上的立场时。它也对你想了解如何创建高级 AI 搜索以查找文档非常有帮助。

在这篇文章中,我将向你展示我如何制作一个强大的法律助手。图片来源:ChatGPT。
你可以在这里访问本文开发的应用程序:
[## 法律判决
搜索最高法院判决
legal-share-frontend.vercel.app](https://legal-share-frontend.vercel.app/?source=post_page-----096550746987--------------------------------)
动机
我写这篇文章的动机是为了描述如何利用最新的语言模型技术创建一个法律助手。这个工具有潜力为律师在研究各种主题时节省大量时间。未来,我计划扩展该应用程序,加入检索相关法律、发布的法律意见等功能。这样,它就可以成为一个完整的工具,帮助…
如何在 Power Query 中动态限制数据导入
当我们存储大量数据时,我们可能会问自己:我真的需要所有这些数据来进行报告吗?如果你有这个问题,并且想了解如何控制 Power BI 文件中的数据量,那么这篇文章是你需要阅读的。
·发布于 Towards Data Science ·阅读时间 8 分钟·2024 年 8 月 6 日
--

图片来源:Claudio Schwarz via Unsplash
引言
我的最大数据集包含 20 年的数据。
虽然当我想使用大数据集进行一些测试时,这是很好的,但报告有时需要超过几年的历史数据也并不罕见。
我的客户通常只需要两三年的数据。
而现在,挑战来了:
如何在将数据导入 Power BI 时控制时间范围?
当处理 SQL 数据库或访问可以通过编程控制或传递参数的 Web REST API 等数据源时,这并不算什么大问题。
然而,有时我必须自己控制这一点。在这种情况下,我必须在 Power Query 中进行操作。
理想情况下,我希望只构建一次,它就能永远运行,无需进一步干预。
这正是本文的核心内容。
准备工作
如果你还没有启用,请在 Power Query 中启用公式栏,以便按照我的指示操作。
打开 Power BI 设置,并切换到 Power Query 编辑器页面。
在这里,启用“显示公式栏”选项:

图 1 — 启用 Power Query 编辑器的公式栏(图由作者提供)
创建动态参数
好吧,标题有点误导。
Power Query 的参数本身并不是动态的。
它们可以基于查询,但我无法用表达式设置值,即使查询可以是动态的或数据驱动的。
所以,我必须构建一个或两个查询,然后可以用来筛选我们的数据。
当数据包含对预期报告目标有用的时间点时,我需要一个查询来导入最早的日期。
如果我需要限制开始和结束日期,例如感兴趣的最新数据点的日期,我需要两个查询。
假设我想加载从两年前开始到上个月底的数据:从 2022 年年初到 2024 年 7 月底。
让我们开始吧。
打开 Power Query 后,我创建一个新的空白查询:

图 2 — 在 Power Query 中创建一个空白查询(图由作者提供)
这将包含我数据的开始日期。
我将使用 M 函数 #date、Date.Year 和 DateTime.LocalNew 来设置它:
#date(Date.Year(DateTime.LocalNow())-2, 1, 1)
-
DateTime.LocalNow() 获取当前的日期和时间
-
Date.Year() 从给定日期中提取年份
-
date() 根据年份、月份和日期的参数创建一个日期
当我从年份中减去 2 时,我得到 2022 作为年份,并且使用以下两个参数,我将得到 2022–01–01。
但是结果并不像预期的那样:

图 3 — 空白查询中的 M 表达式结果(图由作者提供)
当我们打开高级编辑器时,会看到 Power Query 已将表达式设置为字符串。
我们必须移除引号,然后它就能正常工作:

图 4 — 移除引号以按预期设置表达式(图由作者提供)
但是,在继续之前,我将查询名称从 Query1 改为“StartDate”。
接下来,我创建第二个空白查询(或者复制第一个查询),并以与上面相同的方式设置以下表达式:
Date.EndOfMonth(Date.AddMonths(DateTime.LocalNow(), -1))
-
DateTime.LocalNow 获取当前的日期和时间
-
Date.AddMonths() 向给定日期添加月份。
在这种情况下,它会回溯一个月。
-
Date.EndOfMonth() 将日期移到给定月份的最后一天。
在我的例子中,我将得到 2024–07–31。
结果如下:

图 5 — 获取结束日期的表达式结果(图由作者提供)
由于我只需要日期,可以通过将结果转换为日期来去除时间部分:

图 6 — 将日期/时间输出转换为仅日期(图由作者提供)
在重命名新查询后,一切就绪,可以根据这两个查询筛选数据。
筛选数据
接下来,我可以在我的事实表中添加一个筛选表达式。
我找到我的数据的日期列,并选择一个“between”筛选器:

图 7 — 为日期列选择“between”筛选器(图由作者提供)
接下来,我在两个字段中输入两个任意日期(稍后我会将它们替换为新创建的查询):

图 8 — 在“between”筛选器中添加任意日期(图由作者提供)
点击确定后,数据已经被筛选。
但现在,我想更改筛选器,以使用这两个查询。
为此,我替换筛选器表达式中的日期以使用它们:
当我查看 Power Query 编辑器顶部的公式栏时,我看到选定步骤的表达式。
我将表达式从以下内容更改为:
Table.SelectRows(#"Renamed Columns", each [Date] >= **#date(2024, 8, 1)** and [Date] <= **#date(2024, 8, 31)**)
如此:
Table.SelectRows(#"Renamed Columns", each [Date] >= **StartDate** and [Date] <= **EndDate**)
完成。
由于我有两个事实表,我复制这个表达式并将其添加到另一个表中:
我选择表格,并点击 fx 按钮。之后,我可以将与之前相同的表达式粘贴到公式栏中:

图 9 — 为第二个表格添加新表达式以进行筛选(图由作者提供)
最后,我将两个表格的步骤名称更改为更有意义的名称:

图 10 — 更改步骤名称以筛选数据(图由作者提供)
添加一个参数来控制年份数
最后,我可以添加一个 Power Query 参数,以控制我要在 Power BI 中导入的年份。

图 11 — 创建一个新参数来存储年份数(图由作者提供)
我输入一个名称,将类型设置为十进制数字,并为当前值输入 2:

图 12 — 设置新参数(图由作者提供)
接下来,我更改 StartDate 查询的 M 表达式,以使用此参数:
#date(Date.Year(DateTime.LocalNow())- **NumberOfYearsToImport**, 1, 1)
结果不会改变,因为我之前使用过两个。
现在,我可以通过 Power BI 更改参数和数据量,而无需更改 Power Query 中的表达式。
此外,在将报告发布到 Power BI 云端后,我可以更改此参数:
打开语义模型的设置,打开“参数”部分,并更改值。
在 Power BI 中加载数据后,我只看到从 2022 年 1 月 1 日到 2024 年 7 月末的数据:

图 13 — 加载到 Power BI 后的筛选数据(图由作者提供)
那么,DateId 呢?
如果你的数据不包含日期列,而是包含数字 DateId 列,那么上述表达式将不再有效。
对于不熟悉 DateId 的人来说,它是日期的数字表示。
例如,2024–07–15 会被转换为 20240715。
在这种情况下,使用这些表达式来计算 DateId:
对于 StartDate:
(Date.Year(DateTime.LocalNow())- NumberOfYearsToImport) * 10000 + 101
对于 EndDate,这是完整的 M 脚本(在创建空查询后,将脚本替换到高级编辑器中):
let
Source = Date.EndOfMonth(Date.AddMonths(DateTime.LocalNow(), -1)),
#"Extracted Date" = Date.Year(Date.From(Source)) * 10000 + Date.Month(Date.From(Source)) * 100 + Date.Day(Date.From(Source))
in
#"Extracted Date"
数据过滤与上述相同,只不过使用的是数值而非日期。
关闭新查询的加载
没有进一步的操作,两个新查询已被加载到 Power BI 中:

图 14 — Power BI 中的 Start-和 EndDate 表(图由作者提供)
由于这两张表在 Power BI 中用途不大(除非你想在报告中展示这两个值作为信息),我将禁用 Power Query 中这两张表的加载:

图 15 — 禁用不需要的表加载到 Power BI 中(图由作者提供)
你将收到一个关于可能数据丢失的警告。你可以确认它。
这些表的名称将以斜体显示,并且它们不再被加载到 Power BI 中。
结论
这里展示的方法非常有用,可以动态定义导入到 Power BI 中的数据的时间范围。
在我的案例中,我使用 SQL Server 作为我的数据源。
Power Query 可以将数据过滤转换为 SQL,并将查询发送(折叠)到数据源。
如果你想了解更多关于查询折叠的信息,请阅读这个:
查询折叠是 Power Query 中的一个重要功能,它将处理步骤转移到源端。这里我们进一步探讨…
towardsdatascience.com
然而,这种通用的方法也适用于任何其他数据源。
但要注意:如果 Power Query 无法将过滤器传递给数据源,它将首先加载整个数据集,然后在加载完所有数据后过滤掉不需要的行。
如果你拥有大量数据,这可能会成为一个问题。
尽管这种方法很有用,但将所需数据放在源端是至关重要的。请向你的数据提供者寻求帮助。
然而,在开发过程中,限制加载到 Power BI 中的数据量可能会有所帮助,等解决方案发布后再加载整个数据集。
无论如何,我希望你能学到一些 Power Query 的新知识。
参考文献
和我之前的文章一样,我使用了 Contoso 示例数据集。你可以从微软这里免费下载 ContosoRetailDW 数据集。
Contoso 数据可以根据 MIT 许可自由使用,详情见这里。
我扩大了数据集,以便让 DAX 引擎处理得更加繁重。
Online Sales 表包含 7100 万行(而不是 1260 万行),而 Retail Sales 表包含 1850 万行(而不是 340 万行)。
[## 每当 Salvatore Cagliari 发布时,订阅电子邮件。
每当萨尔瓦托雷·卡利亚里发布新内容时,您将收到电子邮件通知。通过注册,您将创建一个 Medium 账户,如果您还没有的话…
尽管 Medium 有付费墙,我仍然让我的文章对所有人开放。这使我可以为每位读者赚取少许收入,但我关闭了付费墙,您可以免费阅读我的文章。
您可以通过以下方式支持我的工作,我是在空闲时间完成这些工作的:
buymeacoffee.com/salvatorecagliari
或扫描此二维码:

任何支持都将不胜感激,并帮助我找到更多时间为您创作更多内容。
非常感谢。
如何轻松部署本地生成式搜索引擎使用 VerifAI
一个开源倡议,帮助你基于本地文件和自托管(Mistral, Llama 3.x)或商业 LLM 模型(GPT4, GPT4o 等)部署生成式搜索。
Nikola Milosevic (Data Warrior)
·发布于Towards Data Science ·8 分钟阅读·2024 年 11 月 21 日
--
我之前曾写过关于构建自己的简单生成式搜索引擎的文章,也写过关于 VerifAI 项目的文章,发布在《Towards Data Science》网站上。然而,这次有一个重大更新值得重新关注。最初,VerifAI 被开发为一个生物医学生成式搜索引擎,提供参考资料和 AI 验证的答案。这个版本仍然可以使用,我们现在称之为VerifAI BioMed。可以在这里访问:app.verifai-project.com/。
然而,主要的更新是,你现在可以索引本地文件,并将它们转化为你自己的生成式搜索引擎(或者称之为生产力引擎,正如一些人称基于 GenAI 的这些系统)。它也可以作为企业或组织的生成式搜索引擎。我们称这个版本为VerifAI Core,因为它作为其他版本的基础。在本文中,我们将探讨如何通过几个简单的步骤来部署它并开始使用。鉴于它是用 Python 编写的,所以可以在任何操作系统上运行。
架构
描述生成式搜索引擎的最佳方式是将其分解为三个部分(或组件,在我们的案例中):
-
索引
-
检索增强生成(RAG)方法
-
VerifAI 包含一个附加组件,即一个验证引擎,位于常规生成式搜索功能之上。
在 VerifAI 中进行索引可以通过将其索引脚本指向包含 PDF、MS Word、PowerPoint、Text 或 Markdown (.md) 文件的本地文件夹来完成。该脚本读取并索引这些文件。索引是以双重模式执行的,同时利用词汇和语义索引。
对于词汇索引,VerifAI 使用OpenSearch。对于语义索引,它使用在配置文件中指定的嵌入模型对文档块进行向量化(支持来自Hugging Face的模型),然后将这些向量存储在Qdrant中。这个过程的视觉表示如下图所示。

索引架构(图由作者提供)
当使用 VerifAI 来回答问题时,方法有些复杂。用户问题以自然语言编写,经过预处理(例如,排除停用词),然后转化为查询。
对于OpenSearch,只执行词汇处理(例如,排除停用词),并检索最相关的文档。对于Qdrant,查询会使用与存储在 Qdrant 中的文档块相同的模型进行嵌入。这些嵌入随后用于查询 Qdrant,基于点积相似度检索最相似的文档。使用点积是因为它考虑了向量的角度和大小。
最后,必须将两个引擎的结果合并。这是通过将每个引擎的检索得分标准化到 0 和 1 之间来完成的(通过将每个得分除以其各自引擎中的最高得分)。然后,将对应于同一文档的得分相加,并按合并后的得分降序排序。
使用检索到的文档,构建一个提示。该提示包含指令、最相关的文档和用户的问题。然后,将该提示传递给选择的大型语言模型(可以在配置文件中指定,如果未设置模型,则默认为我们本地部署并微调的 Mistral 版本)。最后,应用验证模型以确保没有幻觉,答案通过 GUI 呈现给用户。该过程的示意图如下图所示。

检索、生成和验证架构(图由作者提供)。该模型基于以下论文的结合:arxiv.org/pdf/2407.11485,aclanthology.org/2024.bionlp-1.44/
安装必要的库
要安装 VerifAI 生成式搜索,可以先从 GitHub 克隆最新的代码库或使用可用的版本之一。
git clone https://github.com/nikolamilosevic86/verifAI.git
在安装 VerifAI Search 时,建议首先创建一个干净的 Python 环境。我已经在 Python 3.6 上进行了测试,但它应该适用于大多数 Python 3 版本。然而,Python 3.10 及以上版本可能会与某些依赖项存在兼容性问题。
要创建一个 Python 环境,可以使用 venv 库,如下所示:
python -m venv verifai
source verifai/bin/activate
激活环境后,你可以安装所需的库。需求文件位于verifAI/backend目录中。你可以运行以下命令来安装所有依赖项:
pip install -r requirements.txt
配置系统
下一步是配置 VerifAI 及其与其他工具的交互。可以通过直接设置环境变量或使用环境文件(首选选项)来完成。
在 backend 文件夹中提供了一个 VerifAI 环境文件的示例,文件名为 .env.local.example。你可以将此文件重命名为 .env,VerifAI 后端将自动读取该文件。文件结构如下:
SECRET_KEY=6293db7b3f4f67439ad61d1b798242b035ee36c4113bf870
ALGORITHM=HS256
DBNAME=verifai_database
USER_DB=myuser
PASSWORD_DB=mypassword
HOST_DB=localhost
OPENSEARCH_IP=localhost
OPENSEARCH_USER=admin
OPENSEARCH_PASSWORD=admin
OPENSEARCH_PORT=9200
OPENSEARCH_USE_SSL=False
QDRANT_IP=localhost
QDRANT_PORT=6333
QDRANT_API=8da7625d78141e19a9bf3d878f4cb333fedb56eed9097904b46ce4c33e1ce085
QDRANT_USE_SSL=False
OPENAI_PATH=<model-deployment-path>
OPENAI_KEY=<model-deployment-key>
OPENAI_DEPLOYMENT_NAME=<name-of-model-deployment>
MAX_CONTEXT_LENGTH=128000
USE_VERIFICATION = True
EMBEDDING_MODEL="sentence-transformers/msmarco-bert-base-dot-v5"
INDEX_NAME_LEXICAL = 'myindex-lexical'
INDEX_NAME_SEMANTIC = "myindex-semantic"
一些变量相当直观。第一个密钥和算法用于前端与后端之间的通信。
接下来是配置访问PostgreSQL数据库的变量。需要提供数据库名称(DBNAME)、用户名、密码以及数据库所在的主机地址。在我们的例子中,它位于 localhost 上,是在 Docker 镜像中。
下一部分是配置OpenSearch访问。包括 IP 地址(在我们的例子中仍是 localhost)、用户名、密码、端口号(默认端口为 9200),以及定义是否使用 SSL 的变量。
一个类似的配置部分是Qdrant,对于 Qdrant,我们使用一个 API 密钥,必须在此处定义。
下一部分定义了生成模型。VerifAI 使用了 OpenAI Python 库,这已成为行业标准,并允许它同时使用OpenAI API、Azure API、以及通过 vLLM、OLlama 或 Nvidia NIMs。 用户需要定义接口路径、API 密钥以及将要使用的模型部署名称。我们即将增加支持,允许用户修改或更改用于生成的提示。如果没有提供接口路径和密钥,模型将下载我们已经微调的 Mistral 7B 模型,并部署到本地。但是,如果你的 GPU 内存不足,或者内存总量不足,这可能会失败,或者运行非常缓慢。
你还可以设置MAX_CONTEXT_LENGTH,在这种情况下它设置为 128,000 个 token,因为这是 GPT4o 的上下文大小。上下文长度变量用于构建上下文。通常,它通过输入有关回答问题的指令(要事实性回答,并提供参考资料),然后提供检索到的相关文档和问题来构建上下文。然而,文档可能很大,超过上下文长度。如果发生这种情况,文档将被分割成多个部分,并选择最适合上下文大小的前 n 个部分。
下一部分包含用于在 Qdrant 中嵌入文档的模型的 HuggingFace 名称。最后,还列出了在 OpenSearch (INDEX_NAME_LEXICAL) 和 Qdrant (INDEX_NAME_SEMANTIC) 中的索引名称。
如我们之前所说,VerifAI 有一个组件,用于验证生成的声明是否基于提供的和引用的文档。然而,这个功能可以开启或关闭,因为对于某些用例,可能不需要此功能。可以通过将 USE_VERIFICATION 设置为 False 来关闭此功能。
安装数据存储
安装的最后一步是运行 install_datastores.py 文件。在运行此文件之前,您需要安装 Docker 并确保 Docker 守护进程正在运行。由于该文件读取用于设置用户名称、密码或 API 密钥的配置,因此需要首先创建配置文件。下一节会对此进行说明。
该脚本设置了必要的组件,包括 OpenSearch、Qdrant 和 PostgreSQL,并在 PostgreSQL 中创建了数据库。
python install_datastores.py
请注意,此脚本在没有 SSL 证书的情况下安装 Qdrant 和 OpenSearch,以下说明假设不需要 SSL。如果生产环境中需要 SSL,您需要手动配置它。
另外,请注意,我们在此讨论的是在 Docker 上的本地安装。如果您已经部署了 Qdrant 和 OpenSearch,只需更新配置文件,指向这些实例即可。
索引文件
此配置由索引方法和后台服务共同使用。因此,必须在索引之前完成该配置。配置完成后,可以通过将 index_files.py 指向包含要索引文件的文件夹来运行索引过程:
python index_files.py <path-to-directory-with-files>
我们在仓库中包含了一个名为 test_data 的文件夹,其中包含几个测试文件(主要是我的论文和其他过去的写作)。您可以用自己的文件替换这些文件,并运行以下命令:
python index_files.py test_data
这将对该文件夹及其子文件夹中的所有文件进行索引。完成后,可以运行 VerifAI 服务,供后台和前端使用。
运行生成性搜索
只需运行以下命令,即可启动 VerifAI 后台:
python main.py
这将启动 FastAPI 服务,作为后台服务,传递请求给 OpenSearch 和 Qdrant,以检索给定查询的相关文件,并部署 LLM 生成答案,同时利用本地模型进行声明验证。
前端是一个名为 client-gui/verifai-ui 的文件夹,使用 React.js 编写,因此需要本地安装 Node.js 和 npm。然后,您可以通过运行 npm install 安装依赖项,并通过运行 npm start 启动前端:
cd ..
cd client-gui/verifai-ui
npm install
npm start
最后,安装完成后,系统应该大致如下所示:

启用验证功能后的一个示例问题(绿色文本)并引用文件,文件可以下载(截图由作者提供)

截图展示了已验证声明的工具提示,并呈现了文章中最相似的句子(截图由作者提供)
贡献与未来方向
到目前为止,VerifAI 得到了来自欧盟资助的“下一代互联网搜索”项目的帮助。它由塞尔维亚人工智能研究与发展研究所和拜耳公司(Bayer A.G.)联合开发。第一版作为一个面向生物医学的生成搜索引擎开发。该产品将继续在 app.verifai-project.com/ 上运行。然而,最近我们决定扩展该项目,使其能够真正成为一个开源的生成搜索引擎,能够为任何文件提供可验证的答案,且可以被不同的企业、中小型公司、非政府组织或政府广泛使用。这些修改是由 Natasa Radmilovic 和我自愿开发的(特别感谢 Natasa!)。
然而,鉴于这是一个开源项目,托管在 GitHub 上(github.com/nikolamilosevic86/verifAI),我们欢迎任何人通过拉取请求、错误报告、功能请求、讨论或其他任何形式的贡献(欢迎随时联系我们——对于 BioMed 和 Core 版本(如此处所述的文档生成搜索),网站将保持不变——verifai-project.com)。因此,我们欢迎您贡献代码、启动我们的项目,并在未来关注我们。
如何轻松为你的本地 LLM 设置一个简洁的用户界面
一步步的指南:如何使用 Open WebUI 在本地运行 Llama3
·发表于Towards Data Science ·阅读时间 6 分钟·2024 年 8 月 28 日
--

图片由 AI(Midjourney)生成,作者提供
#1 为什么选择本地 LLM
无论是由于公司限制,还是希望安全地处理个人数据,许多人因为数据隐私问题避免使用 ChatGPT。
幸运的是,有一些解决方案可以让你无限制地使用 LLM,而不需要将敏感数据发送到云端。
在我之前的文章中,我探讨了通过 Ollama 在本地运行 Llama 3 的一种解决方案。
一步步的指南:如何使用 Python 在本地运行 Llama3
towardsdatascience.com
前提条件:在上一篇文章的结尾,我们已经通过 Ollama 在本地运行了 Llama 3,并且可以通过终端或在 Jupyter Notebook 中使用它。
在本文中,我将解释如何通过简洁的用户界面在几分钟内使本地 LLM 的使用变得更加用户友好!
如何轻松使用 Pandera 验证数据
学习如何构建一个简单的数据模型,通过类型提示来验证数据
·发布于Towards Data Science ·阅读时间:6 分钟·2024 年 8 月 14 日
--

许多网上的pandas教程教授如何操作和清理数据,但很少有教程展示如何验证数据是否正确。这就是使用pandera进行数据验证的意义所在。
为什么需要数据验证?
就像任何人一样,当我第一次查看数据时,我会进行基本的调查,比如查看数据类型、检查空值,并可视化数据分布,来大致判断如何处理数据。
然而,我们需要数据验证来确保数据遵循业务逻辑。例如,对于包含产品信息的数据,我们需要验证产品价格没有负值;或者当用户提供电子邮件地址时,我们需要验证该电子邮件地址符合已知的模式。
忽视数据验证会对分析和建模产生下游影响,因为数据质量差会导致偏差、噪声和不准确性增加。
最近一个数据验证不当的例子是Zillow 的房价算法,其高估了 Zillow 所购买的 2/3 的房产,导致 2021 年第三季度和第四季度 Zillow 房产估值下降了 5 亿美元。
这表明,你不仅需要关注数据是否符合验证标准,还需要关注数据是否反映了现实情况,而在 Zillow 的案例中,数据并未做到这一点。
什么是 Pandera?
[Pandera](https://pandera.readthedocs.io/en/stable/index.html)是一个 Python 包,提供了一个文档齐全且灵活的 API,能够与pandas和polars这两个主要的 Python 数据库进行集成。
我们可以使用pandera通过业务逻辑和领域知识来验证数据框架的数据类型和属性。
大纲
本文将覆盖以下内容:
-
如何开始使用
pandera -
如何定义
pandera数据模型 -
如何验证数据并处理错误
设置
安装依赖
pip install pandas
pip install pandera
数据
本文使用的数据是通过 Claude.ai 生成的假足球市场数据。
定义验证模型
该包允许你定义验证模式或数据验证模型,后者与另一个很棒的数据验证包[Pydantic](https://docs.pydantic.dev/latest/)非常相似。
对于本次练习,我们将专注于验证模型,因为它允许与我们的 Python 代码集成类型提示,并且我发现它比验证模式稍微更易于阅读。然而,如果你想利用验证模式,模型也有方法将其转换为模式。
你可以在这里找到关于两种验证方法的信息:
加载数据
data = {
'dob': pd.to_datetime(['1990-05-15', '1988-11-22', '1995-03-10', '1993-07-30', '1992-01-18', '1994-09-05', '1991-12-03', '1989-06-20', '1996-02-14', '1987-08-08']),
'age': [34, 35, 29, 31, 32, 30, 32, 35, 28, 37],
'country': ['England', 'Spain', 'Germany', 'France', 'Italy', 'Brazil', 'Argentina', 'Netherlands', 'Portugal', 'England'],
'current_club': ['Manchester United', 'Chelsea', 'Bayern Munich', 'Paris Saint-Germain', 'Juventus', 'Liverpool', 'Barcelona', 'Ajax', 'Benfica', 'Real Madrid'],
'height': pd.array([185, 178, None, 176, 188, 182, 170, None, 179, 300], dtype='Int16'),
'name': ['John Smith', 'Carlos Rodriguez', 'Hans Mueller', 'Pierre Dubois', 'Marco Rossi', 'Felipe Santos', 'Diego Fernandez', 'Jan de Jong', 'Rui Silva', 'Gavin Harris'],
'position': ['Forward', 'Midfielder', 'Defender', 'Goalkeeper', 'Defender', 'Forward', 'Midfielder', 'Defender', 'Forward', 'Midfielder'],
'value_euro_m': [75.5, 90.2, 55.8, 40.0, 62.3, 88.7, 70.1, 35.5, 45.9, 95.0],
'joined_date': pd.to_datetime(['2018-07-01', '2015-08-15', None, '2017-06-30', '2016-09-01', None, '2021-07-15', '2014-08-01', '2022-01-05', '2019-06-01']),
'number': [9, 10, 4, 1, 3, 11, 8, 5, 7, 17],
'signed_from': ['Everton', 'Atletico Madrid', 'Borussia Dortmund', None, 'AC Milan', 'Santos', None, 'PSV Eindhoven', 'Sporting CP', 'Newcastle United'],
'signing_fee_euro_m': [65.0, 80.5, 45.0, None, 55.0, 75.2, 60.8, None, 40.5, 85.0],
'foot': ['right', 'left', 'right', 'both', 'right', 'left', 'left', 'right', 'both', 'right'],
}
df = pd.DataFrame(data)

图片来自作者
检查数据类型
data.types

图片来自作者
我们可以使用数据类型来帮助定义我们的数据模型。
创建数据模型
class PlayerSchema(pa.DataFrameModel):
dob: Series[pd.Timestamp] = pa.Field(nullable=False, ge=pd.Timestamp('1975-01-01'))
age: Series[pa.Int64] = pa.Field(ge=0, le=50, nullable=False)
country: Series[pa.String] = pa.Field(nullable=False)
current_club: Series[pa.String] = pa.Field(nullable=False)
height: Series[pa.Int16] = pa.Field(ge=120, le=210, nullable=True)
name: Series[pa.String] = pa.Field(nullable=False)
position: Series[pa.String] = pa.Field(nullable=False)
value_euro_m: Series[pa.Float64] = pa.Field(ge=0, le=200)
joined_date: Series[pd.Timestamp] = pa.Field(nullable=True, ge=pd.Timestamp('2000-01-01'))
number: Series[pa.Int64] = pa.Field(ge=0, le=99)
signed_from: Series[pa.String] = pa.Field(nullable=True)
signing_fee_euro_m: Series[pa.Float64] = pa.Field(ge=0, le=300, nullable=True)
foot: Series[pa.String] = pa.Field(nullable=False, isin=['right', 'left', 'both', 'unknown'])
上面,我们通过子类化pa.DataFrameModel定义了一个模式,这与在[Pydantic](https://docs.pydantic.dev/latest/)中子类化BaseModel的方式相同。然后,我们用相应的数据集中的列填充了该模式,提供了每列的预期数据类型,并使用pa.Field方法定义了边界。
Pandera与pandas的集成非常好,意味着你可以使用pandas数据类型(例如pd.Timestamp)以及 pandera 数据类型(例如pa.Int64)来定义每一列。
重用字段
为了避免重复字段,我们可以通过使用内建的 Python 库functools中的partial来重用字段。
from functools import partial
NullableField = partial(pa.Field, nullable=True)
NotNullableField = partial(pa.Field, nullable=False)
partial类创建了一个应用原始函数指定子集参数的新函数。
上面,我们为模型变量创建了两个可重用字段,这些变量要么包含空值,要么不包含空值。
我们更新后的数据模型如下所示
class PlayerSchema(pa.DataFrameModel):
dob: Series[pd.Timestamp] = pa.Field(nullable=False, ge=pd.Timestamp('1975-01-01'))
age: Series[pa.Int64] = pa.Field(ge=0, le=50, nullable=False)
country: Series[pa.String] = NotNullableField()
current_club: Series[pa.String] = NotNullableField()
height: Series[pa.Int16] = pa.Field(ge=120, le=250, nullable=True)
name: Series[pa.String] = NotNullableField()
position: Series[pa.String] = NotNullableField()
value_euro_m: Series[pa.Float64] = pa.Field(ge=0, le=200)
joined_date: Series[pd.Timestamp] = pa.Field(nullable=True, ge=pd.Timestamp('2000-01-01'))
number: Series[pa.Int64] = pa.Field(ge=0, le=99)
signed_from: Series[pa.String] = NullableField()
signing_fee_euro_m: Series[pa.Float64] = pa.Field(ge=0, le=300, nullable=True)
foot: Series[pa.String] = pa.Field(nullable=False, isin=['right', 'left', 'both', 'unknown'])
由于partial创建了一个新函数,我们必须使用括号来调用我们的新字段。
验证数据
现在数据模型已经定义好,我们可以用它来验证数据。
@pa.check_types
def load_data() -> DataFrame[PlayerSchema]:
return pd.read_parquet('../data/player_info_cleaned.parquet')
为了验证数据,我们使用类型提示和装饰器的结合。@pa.check_types装饰器表示数据应该按照返回类型提示DataFrame[PlayerSchema]中定义的模式进行验证。
@pa.check_types
def validate_data(df: DataFrame) -> DataFrame[PlayerSchema]:
try:
return df
except pa.errors.SchemaError as e:
print(e)
validate_data(df)
# error in check_types decorator of function 'load_data': Column 'height' failed element-wise validator number 1: less_than_or_equal_to(210) failure cases: 300
通过使用 try-except 块,我们可以捕获加载和验证数据时抛出的任何错误。结果显示,‘height’列未通过小于或等于测试,其中一个标记为 300cm 的身高是不正确的。
清理数据
清理数据有很多策略,其中一些我在之前的文章中已详细介绍。
通过利用 pandas 实现关键的数据处理策略,构建模块化、可重用且高效的数据管道。
为了简化处理,我将使用人口的中位数身高来填补所有大于 210cm 的值。
def clean_height(df: DataFrame) -> DataFrame[PlayerSchema]:
data = df.copy()
data.loc[data[PlayerSchema.height] > 210, PlayerSchema.height] = round(data[PlayerSchema.height].median())
return data
df = clean_height(df)
pandera的另一个有用特性是,架构可以用来指定你想要分析的列名,例如,我们可以使用PlayerSchema.height来提高代码的可读性并鼓励一致性,而不是显式地标记‘height’。
重新验证
由于我们的数据已经被加载进来,我们可以调整之前的函数,使其接受数据框作为输入,运行数据框并通过 try-except 块进行评估,并使用装饰器和类型提示。
validate_data_2(df)
# if there are no errors, the dataframe is the output.
这次没有错误,意味着数据框被作为函数的输出返回。
结论
这篇文章概述了为什么验证数据非常重要,以确保数据与业务逻辑一致并反映现实世界,以 Zillow 为例,说明了缺乏数据验证可能带来的严重后果。
使用了Pandera来展示如何轻松地将数据验证与pandas集成,以快速验证一个架构,该架构与Pydantic非常相似,使用了装饰器和类型提示。还展示了如何使用pa.Field来设置数据的边界,并且当与partial一起使用时,可以创建可重用字段,从而提高代码的可读性。
希望你觉得这篇文章有用,感谢阅读。如果有任何问题,可以通过LinkedIn与我联系!
如何有效地使用 Meta 的图像分割模型:SAM 2
学习如何利用 Meta 的新 SAM 2 模型进行任何物体的分割
·发布于Towards Data Science ·9 分钟阅读·2024 年 8 月 27 日
--
Segment Anything Model 2(SAM2)是 Meta 最新的图像分割模型,能够检测并标记图像和代码中的物体。本文将展示如何下载并利用这个模型,同时回顾该模型及其功能。使用图像分割模型是令人兴奋的,因为你可以立即看到模型的结果,并了解其表现如何,因为分割是一个你的大脑本身就擅长的任务。因此,你可以迅速判断一个图像分割模型的表现是否优秀。

这是一个 SAM 2 应用于武士雕像的示例。图片展示了 SAM 2 如何有效地检测图像中的不同物体。图片由作者提供。
动机
我写这篇文章的动机来源于我关于跟进机器学习领域最新模型的系列文章。SAM2 是 Meta 最近发布的一个模型,Meta 是一家不断推出先进开源机器学习模型的公司。我之前已经写过关于亚马逊 Chronos 预测模型,Llama3,以及其他几个 AI 模型的文章。本文将重点介绍 SAM2,如何使用这个模型,模型可以应用于哪些任务,以及它的表现如何。这个模型也以 Apache 2.0 许可证发布,意味着你可以自由地在商业环境中使用该模型,在我看来,这使得这个模型更加...
如何有效地使用亚马逊的新时间序列预测模型进行时间序列预测
了解亚马逊新的时间序列模型,您可以使用它来预测能源使用、交通拥堵和天气。
·发布于 Towards Data Science ·阅读时间 12 分钟·2024 年 4 月 9 日
--
我将讨论亚马逊新的 Chronos 时间序列预测模型[1]。该模型可用于多种时间序列预测任务,例如预测能源使用、交通/拥堵预测或天气预测。这使得它既灵活又强大。我将讨论该模型的性能、优缺点,以及如何在本地实现和运行该模型。

图像来自 ChatGPT,关于可视化时间序列预测。图像由 ChatGPT 提供。OpenAI。(2024 年)。ChatGPT(4)[大型语言模型]。 chat.openai.com
动机
本文的动机是跟进机器学习领域的最新模型。我通过查看 PapersWithCode,了解到了这个模型。PapersWithCode 是我定期查看的一个网站,帮助我跟上机器学习领域的最新趋势。每当我发现有趣的内容时,我喜欢将其实现并感受该模型及其性能。本文将讨论如何使用该模型、该模型可以应用于哪些任务,以及我对该模型性能的看法。
在运行该模型后,您将能够像下图所示那样进行模型预测:
如何高效逼近一个或多个变量的函数
使用稀疏网格和切比雪夫插值法来构建多变量函数的精确近似。
·发表于 Towards Data Science ·阅读时长 13 分钟·2024 年 6 月 28 日
--

在切比雪夫-高斯-洛巴托节点上的自适应稀疏网格。图由作者提供。
考虑这个近似问题:假设你有一个函数 f(x), x ∈ [-1, 1]^p,该函数计算代价昂贵;因此,你希望构建一个新的函数 f’,它计算便宜,但能准确地在 [-1, 1]^p 上逼近 f,并且使用尽可能少的 f 评估。
这种近似方法在统计学及其他领域有着广泛的应用。例如,f 可能表示一个概率密度函数,其中评估需要昂贵的积分运算,而高效的近似方法可以为你提供一种计算可信集的机制。
在这篇博客中,我将介绍两种构建函数近似的强大技术:切比雪夫插值法和自适应稀疏网格。
切比雪夫插值法是近似的基本构建块。如果 f 稍微光滑一点(例如,满足利普希茨连续性),那么切比雪夫插值法将会收敛;如果 f 是可高阶微分的或者是解析的,切比雪夫插值法将极其迅速地收敛(比其他技术如三次样条插值要好得多)[1, 2]。
如何轻松利用 OCR 和 GPT-4o mini 提取收据信息
利用 OCR 和强大的 GPT-4o mini 模型对收据进行信息提取
·发表在Towards Data Science·阅读 13 分钟·2024 年 8 月 20 日
--
在本文中,我将向您展示如何从收据中提取信息,给出收据的简单图像。首先,我们将利用 OCR 从收据中提取信息。然后,这些信息将被发送到 GPT-4o mini 模型进行信息提取。我这个项目的目标是开发一个应用程序,通过拍摄收据的图像并选择哪些物品属于哪个人,来简单地帮助朋友们分账。本文将重点放在这个目标的信息提取部分。

利用 OCR 和 GPT-4o mini 从收据中提取信息。图片由 ChatGPT 提供。OpenAI。 (2024)。ChatGPT (4o) [大型语言模型]。chatgpt.com/c/c567fd8c-1955-4af9-8566-0a9393e970e5
本文中开发的应用程序可以在Google Play上访问。
动机
浏览收据并计算每个人的份额是一件麻烦事,例如,在餐厅用餐后。我多次遇到这个问题,因此希望找到一个解决方案,使这个过程更有效。因此,我想到了 BillSplitter 应用程序。这个想法是用户可以拍摄收据的图像,应用程序将利用 OCR 和语言模型处理收据并提取每个物品及其对应价格,用户只需简单选择...
如何通过 GPU 强化 Pandas
数据科学
简要介绍 cuDF,一个 NVIDIA 框架,用于加速 Pandas
·发布于 Towards Data Science ·6 分钟阅读·2024 年 4 月 7 日
--

图片由 BoliviaInteligente 提供,来源:Unsplash
Pandas 依然是数据分析和机器学习领域中不可或缺的工具,提供了广泛的功能用于数据读取、转换、清理和写入等任务。然而,它在处理大数据集时的效率有所限制,这使得它在生产环境中的应用或构建强健的数据管道时存在障碍,尽管它在数据科学项目中得到了广泛的使用。
类似于 Apache Spark,Pandas 将数据加载到内存中进行计算和转换。但与 Spark 不同,Pandas 并不是一个分布式计算平台,因此所有操作都必须在单个系统的 CPU 和内存上进行(单节点处理)。这一特点限制了 Pandas 在两个方面的使用:
-
在单个系统上,Pandas 无法处理大量数据。
-
即使是能够适应单个系统内存的数据,处理相对较小的数据集也可能需要相当长的时间。
Pandas 升级版
第一个问题可以通过像 Dask 这样的框架来解决。Dask DataFrame 通过在分布式计算机集群上并行化 Pandas,帮助你处理大规模的表格数据。从许多方面来看……
如何为神经网络的输出编码约束
可用方法总结
·发表于Towards Data Science ·12 分钟阅读·2024 年 4 月 14 日
--

图片由 ChatGPT 根据本文内容生成。
神经网络确实非常强大。然而,随着神经网络应用范围从“标准”的分类和回归任务扩展到更复杂的决策和科学 AI,逐渐显现出一个缺点:神经网络的输出通常是没有约束的,或者更准确地说,通常仅受简单的 0-1 范围(Sigmoid 激活函数)、非负约束(ReLU 激活函数)或加和为 1 的约束(Softmax 激活函数)限制。这些“标准”激活层曾用于处理分类和回归问题,并见证了深度学习的蓬勃发展。然而,随着神经网络开始广泛应用于决策、优化求解以及其他复杂的科学问题,这些“标准”激活层显然已经不再足够。本文将简要讨论当前可以为神经网络的输出添加约束的现有方法,并包含一些个人见解。欢迎批评和讨论相关话题。
如果一次不行,尝试多次
如果你熟悉强化学习,你可能已经知道我在说什么。将约束应用于一个 n 维向量看似困难,但你可以将 n 维向量分解为 n 个输出。每次生成一个输出时,你可以手动编写代码,限制下一个变量的行动空间,以确保其值保持在一个可行的范围内。这种所谓的“自回归”方法有明显的优势:它简单并且能够处理各种约束(只要你能编写代码)。然而,它的缺点也很明显:一个 n 维向量需要进行 n 次网络前向计算调用,这效率较低;此外,这种方法通常需要建模为马尔可夫决策过程(MDP)并通过强化学习进行训练,因此强化学习中的常见挑战,如庞大的行动空间、稀疏奖励函数和长时间训练,也难以避免。
在使用神经网络解决组合优化问题的领域,自回归方法结合强化学习曾是主流,但目前正在被更高效的方法所取代。
或许…我们来学习一下约束条件吧?
在训练过程中,可以向目标函数中添加惩罚项,表示当前神经网络输出违反约束的程度。在传统的优化领域,拉格朗日对偶法也提供了类似的技巧。不幸的是,当应用到神经网络时,这些方法迄今为止只在一些简单的约束下得到了验证,目前尚不清楚它们是否适用于更复杂的约束。一个缺点是,模型的部分能力不得不用于学习如何满足相应的约束,从而限制了模型在其他方向(如优化求解)上的能力。
例如,Karalias 和 Loukas,NeurIPS’21“Erdo˝s Goes Neural: 一个无监督学习框架用于图上的组合优化”展示了所谓的“盒约束”,即变量值位于[a, b]之间,可以通过惩罚项学习,网络能够解决一些相对简单的组合优化问题。然而,我们的进一步研究发现,这种方法缺乏泛化能力。在训练集上,神经网络能够很好地维持约束;但是在测试集上,约束几乎完全丧失。此外,尽管在理论上添加惩罚项可以适用于任何约束,但它无法处理更复杂的约束。我们的论文Wang 等,ICLR’23“朝向一次性神经组合优化求解器:基于基数约束的理论与实证分析”讨论了上述现象并提供了理论分析。
另一方面,生成模型的设计理念要求输出符合特定分布,这似乎更适合“学习约束”方法。Sun 和 Yang, NeurIPS’23 “DIFUSCO: 基于图的扩散求解器用于组合优化”表明,扩散模型可以输出满足旅行商问题约束的解(即,能够输出完整的路径)。我们进一步展示了Li 等人, NeurIPS’23 “T2T: 从训练中的分布学习到测试中的梯度搜索,用于组合优化”,其中生成模型(扩散模型)负责满足约束,另一个优化器则在扩散的逐步去噪过程中提供优化指导。这个策略在实验中表现得相当好,超越了所有之前的神经网络求解器。
另一个有趣的视角:求解一个凸优化问题
也许你担心自回归模型效率过低,而生成模型可能无法解决你的问题。你可能在考虑一个只进行一次前向传播的神经网络,而输出需要满足给定的约束——这可能吗?
答案是肯定的。我们可以求解一个凸优化问题,将神经网络的输出投影到由凸约束界定的可行域中。这种方法利用了凸优化问题在其 KKT 条件下可微的特性,因此这个投影步骤可以视为一个激活层,嵌入到端到端的神经网络中。这种方法由 Zico Kolter 的团队在 CMU 提出并推广,他们目前提供了cvxpylayers 包来简化实现步骤。相应的凸优化问题是
其中y是无约束的神经网络输出,x是有约束的神经网络输出。因为这一步的目的是仅仅进行投影,所以线性目标函数可以实现这一点(添加熵正则化也是合理的)。Ax ≤ b是你需要施加的线性约束,也可以是二次或其他凸约束。
这是个人的备注:似乎有一些已知问题,并且这个仓库似乎很长时间没有更新/维护了(04/2024)。如果有人愿意调查一下发生了什么,我将非常感激。
对于非凸问题:你更倾向于使用哪种梯度近似方法?
使用 KKT 条件推导梯度在理论上是可行的,但它无法解决非凸或不连续问题。事实上,对于不连续问题,当问题参数的变化导致解跳跃时,真实的梯度变成了一个δ函数(即在跳跃处无穷大),显然不能在神经网络训练中使用。幸运的是,有一些梯度近似方法可以解决这个问题。
马克斯·普朗克研究所的 Georg Martius 小组提出了一种黑箱近似方法Vlastelica 等人,ICLR 2020 “黑箱组合求解器的微分”,将求解器视为黑箱。它首先调用一次求解器,然后沿特定方向扰动问题参数,再次调用求解器。两次求解器调用的输出之间的残差作为近似梯度。如果将这种方法应用于神经网络的输出以强制执行约束,我们可以定义一个线性目标函数的优化问题:
其中y是未约束的神经网络输出,x是受约束的神经网络输出。你的下一步是实现一个算法来解决上述问题(不一定是最优的),然后可以将其集成到黑箱近似框架中。黑箱近似方法的一个缺点是它只能处理线性目标函数,但线性目标函数恰好在你寻找强制约束的方法时起作用;此外,由于它只是一个梯度近似方法,如果超参数没有调得很好,可能会遇到稀疏梯度和收敛问题。
另一种近似梯度的方法是使用大量随机噪声扰动,反复调用求解器来估计梯度,正如在Berthet 等人,NeurIPS 2020 “使用可微扰动优化器进行学习”中讨论的那样。从理论上讲,通过这种方式获得的梯度应该与通过 LinSAT 方法获得的梯度类似(将在下一节讨论),即一个熵正则化线性目标函数的梯度;然而,实际上,这种方法需要大量的随机样本,这在我的使用案例中有点不切实际。
自我推销时间:在不解决优化问题的情况下进行投影
无论是从 KKT 条件推导凸问题的梯度,还是近似非凸方法的梯度,都需要调用/编写求解器,因此 CPU-GPU 通信可能成为瓶颈,因为大多数求解器通常是为 CPU 设计和实现的。是否有一种方法可以像激活层一样直接在 GPU 上投影特定的约束,而不显式地解决优化问题?
答案是肯定的,我们的 Wang 等人, ICML'2023 “LinSATNet: 正线性可满足性神经网络” 论文提供了一条可行的路径,并推导了该算法的收敛性质。LinSAT 代表 Linear SATisfiability Network(线性可满足性网络)。
LinSAT 可以看作是一个激活层,使您能够对神经网络的输出应用一般的正线性约束。

图片由作者提供
LinSAT 层是完全可微的,真实的梯度通过自动求导计算,就像其他激活层一样。我们的实现现在支持 PyTorch。
您可以通过以下方式安装:
pip install linsatnet
并开始使用
from LinSATNet import linsat_layer
一个简单的示例
如果您下载并运行源代码,您会发现一个简单的示例。在这个示例中,我们对一个 3×3 的矩阵施加了双重随机约束。
要运行示例,首先克隆仓库:
git clone https://github.com/Thinklab-SJTU/LinSATNet.git
进入仓库并运行示例代码:
cd LinSATNet
python LinSATNet/linsat.py
在这个示例中,我们尝试对一个 3×3 的矩阵施加双重随机约束。双重随机约束意味着矩阵的所有行和列的和都应为 1。
3x3 矩阵被展平为一个向量,然后考虑以下正线性约束(对于 Ex=f):
E = torch.tensor(
[[1, 1, 1, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 1, 1, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 1, 1, 1],
[1, 0, 0, 1, 0, 0, 1, 0, 0],
[0, 1, 0, 0, 1, 0, 0, 1, 0],
[0, 0, 1, 0, 0, 1, 0, 0, 1]], dtype=torch.float32
)
f = torch.tensor([1, 1, 1, 1, 1, 1], dtype=torch.float32)
我们随机初始化 w,并将其视为某些神经网络的输出:
w = torch.rand(9) # w could be the output of neural network
w = w.requires_grad_(True)
我们还有一个“真实目标”,它是 linsat_layer 输出的目标,在这个示例中,它是一个对角矩阵:
x_gt = torch.tensor(
[1, 0, 0,
0, 1, 0,
0, 0, 1], dtype=torch.float32
)
LinSAT 的前向/反向传播遵循标准的 PyTorch 风格,可以轻松集成到现有的深度学习管道中。
前向传播:
linsat_outp = linsat_layer(w, E=E, f=f, tau=0.1, max_iter=10, dummy_val=0)
反向传播:
loss = ((linsat_outp — x_gt) ** 2).sum()
loss.backward()
您还可以将 E 设置为稀疏矩阵,以提高时间和内存效率(尤其是对于大尺寸输入)。以下是一个简单的示例(建议为了最佳效率构造稀疏形式的 E):
linsat_outp = linsat_layer(w, E=E.to_sparse(), f=f, tau=0.1, max_iter=10, dummy_val=0)
我们还可以对 w 进行基于梯度的优化,使得 linsat_layer 的输出更接近 x_gt。这就是您训练时发生的情况。
神经网络。
niters = 10
opt = torch.optim.SGD([w], lr=0.1, momentum=0.9)
for i in range(niters):
x = linsat_layer(w, E=E, f=f, tau=0.1, max_iter=10, dummy_val=0)
cv = torch.matmul(E, x.t()).t() — f.unsqueeze(0)
loss = ((x — x_gt) ** 2).sum()
loss.backward()
opt.step()
opt.zero_grad()
print(f’{i}/{niters}\n’
f’ underlying obj={torch.sum(w * x)},\n’
f’ loss={loss},\n’
f’ sum(constraint violation)={torch.sum(cv[cv > 0])},\n’
f’ x={x},\n’
f’ constraint violation={cv}’)
在训练过程中,您可能会看到损失值逐步减小。
有关完整的 API 参考,请查看 GitHub 仓库。
LinSAT 是如何工作的?
警告,接下来有大量数学内容!如果您只是使用 LinSAT,您可以安全跳过这一部分。
如果您想了解更多细节和证明,请参阅 主论文。
在这里,我们介绍 LinSAT 内部的机制。它通过将 Sinkhorn 算法扩展到多个集合的边际来工作(据我们所知,我们是第一个研究具有多集合边际的 Sinkhorn 算法的人)。然后,通过将约束转化为边际,来强制执行正线性约束。
经典 Sinkhorn 单集边际
让我们从经典的 Sinkhorn 算法开始。给定一个大小为m×n的非负评分矩阵S,以及一组行(大小为m的非负向量v)和列(大小为n的非负向量u)的边际分布,其中:
Sinkhorn 算法输出一个标准化矩阵Γ,大小为m×n,值域在[0,1]之间,因此
从概念上讲,Γᵢ ⱼ表示比例,即从uⱼ移动到vᵢ的部分。
算法步骤如下:
注意,上述公式是对传统 Sinkhorn 公式的修改。Γᵢ ⱼ uⱼ等价于“运输”矩阵中的元素,如(Cuturi 2013)等论文所示。我们更倾向于采用这种新的公式,因为它能够平滑地扩展到以下带有多集边际分布的 Sinkhorn 算法。
为了更清晰的对比,(Cuturi 2013)中的运输矩阵是P,大小为 m×n,约束条件为:
Pᵢ ⱼ表示从 uⱼ到 vᵢ移动的精确质量。
算法步骤如下:
扩展 Sinkhorn 算法与多集边际分布
我们发现 Sinkhorn 算法可以推广到多个边际分布集。回顾一下,Γᵢ ⱼ ∈ [0,1]表示从uⱼ移动到vᵢ的比例。有趣的是,如果我们简单地将u、v替换为另一个边际分布集,得到的公式是相同的,这表明 Sinkhorn 算法有潜力扩展到多个边际分布集。设有k个边际分布集,这些边际分布集被联合施加约束以适应更复杂的现实场景。这些边际分布集为:
然后我们得到:
假设存在一个标准化的Z ∈ [0,1],大小为m×n,使得:
即,多个边际分布集有一个非空的可行区域(你可以在阅读下一节关于如何处理正线性约束时理解“非空可行区域”的含义)。通过遍历 Sinkhorn 迭代来联合执行多个边际分布集的约束,多个边际分布集可以共同施加约束。算法步骤如下:
在我们的论文中,我们证明了多集边际分布的 Sinkhorn 算法与经典 Sinkhorn 算法具有相同的收敛模式,且其基础公式也与经典 Sinkhorn 相似。
将正线性约束转换为边际分布
然后我们展示如何将正线性约束转换为边际分布,这些边际分布由我们提出的多集 Sinkhorn 处理。
编码神经网络的输出 对于一个长度为l的向量y(它可以是神经网络的输出,也可以是linsat_layer的输入),构建以下矩阵:
其中W的大小为 2 × (l + 1),β是虚拟变量,默认值为β = 0。y位于W的左上区域。然后,施加熵正则化器以控制离散性并处理潜在的负输入:
得分矩阵 S 被作为 Sinkhorn 算法的输入来处理多集合边际。
从线性约束到边际
1)打包约束 Ax ≤ b。假设只有一个约束,我们将其重写为
遵循 Sinkhorn 的“运输”视角,输出 x 最多移动 b 单位的质量,从 a₁, a₂, …, aₗ,并且虚拟维度允许通过 移动 质量来实现不等式。还确保 uₚ* 的总和等于 vₚ 的总和。边际分布被定义为
2)覆盖约束 Cx ≥ d。假设只有一个约束,我们将其重写为
我们引入了乘数
因为我们总是有
(否则约束是不可行的),如果没有这个乘数,我们无法得到一个可行的解,其中所有 x 中的元素都是 1。我们的公式确保至少 d 单位的质量通过 x 从 c₁, c₂, …, cₗ* 被 移动,从而表示“大于”的覆盖约束。还确保 u_c 的总和等于 v_c 的总和。边际分布被定义为
3)等式约束 Ex = f。表示等式约束更加直接。假设只有一个约束,我们将其重写为
输出 x 移动 e₁, e₂, …, eₗ* 到 f,我们在 uₑ 中不需要虚拟元素,因为这是一个等式约束。还确保 uₑ 的总和等于 vₑ 的总和。边际分布被定义为
在对所有约束进行编码并将它们堆叠为多个边际集合后,我们可以调用 Sinkhorn 算法来处理多集合边际,从而编码约束。
LinSAT 实验验证
在我们的 ICML 论文中,我们验证了 LinSATNet 方法在路由约束方面的有效性,超出了普通情况(用于解决旅行商问题的变体)、部分图匹配约束(用于仅有部分图匹配的图匹配问题)和一般线性约束(用于特定的偏好与投资组合优化)。所有这些问题都可以用正线性约束表示,并通过 LinSATNet 方法处理。在实验中,神经网络能够学习如何解决这三种问题。
需要注意的是,LinSATNet 方法只能处理正线性约束,这意味着它无法处理像 x₁ — x₂ ≤ 0 这样包含负项的约束。然而,正线性约束已经涵盖了大量的场景。对于每个具体问题,数学建模往往不是唯一的,在许多情况下,可以找到合理的正线性表达式。除了上述提到的示例外,让网络输出有机分子(表示为图,忽略氢原子,只考虑骨架结构)时,可以考虑像 C 原子最多有 4 个键,O 原子最多有 2 个键这样的约束。
后记
向神经网络添加约束具有广泛的应用场景,目前已有几种方法可供选择。需要注意的是,没有一个公认的标准来判断它们之间的优劣——最好的方法通常与特定场景相关。
当然,我推荐尝试 LinSATNet!反正它和网络中的激活层一样简单。
如果你觉得这篇文章对你有帮助,欢迎随时引用:
@inproceedings{WangICML23,
title={LinSATNet: The Positive Linear Satisfiability Neural Networks},
author={Wang, Runzhong and Zhang, Yunhao and Guo, Ziao and Chen, Tianyi and Yang, Xiaokang and Yan, Junchi},
booktitle={International Conference on Machine Learning (ICML)},
year={2023}
}
所有前述内容已经在本文中讨论过。
如何使用 Jackknife 估计确保模型的稳定性
如何确保模型的稳健性并检测影响力较大的数据观测值
·发表于Towards Data Science ·6 分钟阅读·2024 年 12 月 21 日
--
在许多情况下,确保模型的稳健性对于模型的一致性和未见数据的泛化能力至关重要。检测影响力较大的单个数据观测值也是避免不准确结果的另一个重要原因。
这个过程通常涉及评估模型输出的变异性并识别潜在的偏差,特别是在处理小数据集时。解决这些挑战的一个强有力的统计工具是Jackknife 估计方法。
在本文中,我们将深入探讨 Jackknife 估计的概念,走过一个实际例子,并逐步探索它是如何工作的。

图片由Ryoji Iwata提供,来源于Unsplash
什么是 Jackknife 估计?
与自助法(Bootstrapping)类似,Jackknife 估计是一种重采样统计技术,用于估计估计量的偏差和方差。其工作原理是一次性从数据集中排除一个观测值,计算剩余数据的估计量,然后使用得到的估计值来计算整体估计。为了说明这一技术的应用,稍后我们将解释一个关于流失预测的常见实际例子。
如何从单张图像中估计深度
使用 Hugging Face 和 FiftyOne 运行并评估单目深度估计模型
·发表于 Towards Data Science ·10 分钟阅读·2024 年 1 月 25 日
--

使用 Marigold 在 NYU 深度 v2 图像上生成的单目深度热图。图片由作者提供。
人类通过两只眼睛看世界。双眼视力的一个主要优点是能够感知深度——物体离近还是远。人脑通过比较左眼和右眼同时捕捉到的图像,并解释这些差异,从而推断物体的深度。这个过程被称为立体视觉。
就像深度感知在人类视觉和导航中发挥着关键作用一样,估计深度的能力对于广泛的计算机视觉应用也至关重要,从自动驾驶到机器人,再到增强现实。然而,由于空间限制和预算约束等一系列实际考虑,常常使这些应用仅限于单一摄像头。
单目深度估计(MDE)是从单张图像中预测场景深度的任务。由于从单张图像计算深度本质上是模糊的,因为同一个 3D 场景可以有多种方式投影到图像的 2D 平面上,因此 MDE 是一项具有挑战性的任务,需要(显式或隐式)考虑许多线索,如物体大小、遮挡和透视。
在这篇文章中,我们将展示如何加载和可视化深度图数据,运行单目深度估计模型,并评估深度预测。我们将使用来自 SUN RGB-D 数据集的数据进行演示。
具体来说,我们将涵盖以下内容:
-
加载和可视化 SUN-RGBD 真实深度图
-
使用 Marigold 和 DPT 进行推理
-
评估相对深度预测
我们将使用 Hugging Face 的transformers和diffusers库进行推理,使用FiftyOne进行数据管理和可视化,使用scikit-image进行评估指标。所有这些库都是开源的,可以免费使用。免责声明:我在 Voxel51 工作,该公司是这些库之一(FiftyOne)的主要维护者。
在开始之前,确保你已经安装了所有必要的库:
pip install -U torch fiftyone diffusers transformers scikit-image
然后,我们将导入在整个文章中使用的模块:
from glob import glob
import numpy as np
from PIL import Image
import torch
import fiftyone as fo
import fiftyone.zoo as foz
import fiftyone.brain as fob
from fiftyone import ViewField as F
加载和可视化 SUN-RGBD 深度数据
SUN RGB-D 数据集包含 10,335 张 RGB-D 图像,每张图像都有对应的 RGB 图像、深度图像和相机内参。它包含来自NYU Depth v2、伯克利B3DO和SUN3D数据集的图像。SUN RGB-D 是最受欢迎的单目深度估计和语义分割任务数据集之一!
💡对于本教程,我们只使用 NYU Depth v2 部分。NYU Depth v2 是允许商业使用的许可(MIT 许可证),可以直接从 Hugging Face 下载。
下载原始数据
首先,从这里下载 SUN RGB-D 数据集并解压,或者使用以下命令直接下载:
curl -o sunrgbd.zip https://rgbd.cs.princeton.edu/data/SUNRGBD.zip
然后解压它:
unzip sunrgbd.zip
如果你想将数据集用于其他任务,你可以完全转换注释并加载到你的fiftyone.Dataset中。但是,对于本教程,我们只使用深度图像,所以我们只会使用 RGB 图像和深度图像(存储在depth_bfx子目录中)。
创建数据集
因为我们只关注传达核心内容,所以我们将限制在前 20 个样本,这些样本都来自 NYU Depth v2 数据集部分:
## create, name, and persist the dataset
dataset = fo.Dataset(name="SUNRGBD-20", persistent=True)
## pick out first 20 scenes
scene_dirs = glob("SUNRGBD/kv1/NYUdata/*")[:20]
samples = []
for scene_dir in scene_dirs:
## Get image file path from scene directory
image_path = glob(f"{scene_dir}/image/*")[0]
## Get depth map file path from scene directory
depth_path = glob(f"{scene_dir}/depth_bfx/*")[0]
depth_map = np.array(Image.open(depth_path))
depth_map = (depth_map * 255 / np.max(depth_map)).astype("uint8")
## Create sample
sample = fo.Sample(
filepath=image_path,
gt_depth=fo.Heatmap(map=depth_map),
)
samples.append(sample)
## Add samples to dataset
dataset.add_samples(samples);
这里我们将深度图像存储为热图。所有内容都以标准化的相对距离表示,其中 255 代表场景中的最大距离,0 代表场景中的最小距离。这是表示深度图像的常见方式,尽管这不是唯一的方式。如果我们关注绝对距离,我们可以存储每个样本的最小和最大距离参数,并使用这些参数从相对距离中重建绝对距离。
可视化真实数据
有了存储在样本中的热图,我们可以可视化地面真实数据:
session = fo.launch_app(dataset, auto=False)
## then open tab to localhost:5151 in browser

来自 SUN RGB-D 数据集的样本的地面真实深度图。图片由作者提供。
在处理深度图时,热图的色彩方案和透明度非常重要。我是色盲,因此我发现viridis色图并将透明度调到最大最适合我。

热图的可视化设置。图片由作者提供。
地面真实值?
通过检查这些 RGB 图像和深度图,我们可以看到地面真实深度图中存在一些不准确之处。例如,在这张图像中,图像中心的深色裂缝实际上是场景中最远的部分,但地面真实深度图却显示它是场景中最近的部分:

来自 SUN RGB-D 数据集的样本的地面真实深度数据问题。图片由作者提供。
这是 MDE 任务中的一个关键挑战:地面真实数据很难获得,而且通常存在噪声!在评估你的 MDE 模型时,了解这一点至关重要。
运行单目深度估计模型
现在我们已经加载了数据集,可以对我们的 RGB 图像运行单目深度估计模型!
长时间以来,像DORN和DenseDepth这样的单目深度估计的最先进模型都是基于卷积神经网络构建的。然而,最近,基于变换器的模型,如DPT和GLPN,以及基于扩散的模型,如Marigold,都取得了显著的成果!
在本节中,我们将展示如何使用 DPT 和 Marigold 生成 MDE 深度图预测。在这两种情况下,你可以选择使用各自的 Hugging Face 库在本地运行模型,或者通过Replicate进行远程运行。
要通过 Replicate 运行,请安装 Python 客户端:
pip install replicate
并导出你的 Replicate API 令牌:
export REPLICATE_API_TOKEN=r8_<your_token_here>
💡 使用 Replicate 时,模型加载到服务器内存可能需要一些时间(冷启动问题),但一旦加载完成,预测应该只需几秒钟。根据你的本地计算资源,与本地运行相比,使用服务器运行可能会大大提高速度,特别是对于 Marigold 和其他基于扩散的深度估计方法。
使用 DPT 进行单目深度估计
我们将首先运行一个密集预测变换器(DPT)。DPT 模型在单目深度估计(MDE)和语义分割等任务中非常有用,这些任务需要“密集”的像素级预测。
以下的检查点使用了MiDaS,它返回的是反向深度图,因此我们需要将其反转回来,以获得可比较的深度图。
要在本地使用transformers运行,首先加载模型和图像处理器:
from transformers import AutoImageProcessor, AutoModelForDepthEstimation
## swap for "Intel/dpt-large" if you'd like
pretrained = "Intel/dpt-hybrid-midas"
image_processor = AutoImageProcessor.from_pretrained(pretrained)
dpt_model = AutoModelForDepthEstimation.from_pretrained(pretrained)
接下来,我们将推理代码封装在一个样本中,包括预处理和后处理:
def apply_dpt_model(sample, model, label_field):
image = Image.open(sample.filepath)
inputs = image_processor(images=image, return_tensors="pt")
with torch.no_grad():
outputs = model(**inputs)
predicted_depth = outputs.predicted_depth
prediction = torch.nn.functional.interpolate(
predicted_depth.unsqueeze(1),
size=image.size[::-1],
mode="bicubic",
align_corners=False,
)
output = prediction.squeeze().cpu().numpy()
## flip b/c MiDaS returns inverse depth
formatted = (255 - output * 255 / np.max(output)).astype("uint8")
sample[label_field] = fo.Heatmap(map=formatted)
sample.save()
在这里,我们将预测结果存储在样本的label_field字段中,用热图表示,和真实标签一样。
请注意,在apply_dpt_model()函数中,在模型的前向传递和热图生成之间,我们调用了torch.nn.functional.interpolate()。这是因为模型的前向传递是在图像的下采样版本上运行的,而我们希望返回一个与原始图像大小相同的热图。
为什么我们需要这样做?如果我们只是想查看热图,这一点并不重要。但如果我们想逐像素地比较真实深度图和模型的预测结果,就需要确保它们的大小一致。
剩下的就是遍历数据集:
for sample in dataset.iter_samples(autosave=True, progress=True):
apply_dpt_model(sample, dpt_model, "dpt")
session = fo.launch_app(dataset)

使用混合 MiDaS DPT 模型在 SUN RGB-D 样本图像上预测的相对深度图。图片由作者提供。
要使用 Replicate 运行,你可以使用这个模型。以下是 API 的样式:
import replicate
## example application to first sample
rgb_fp = dataset.first().filepath
output = replicate.run(
"cjwbw/midas:a6ba5798f04f80d3b314de0f0a62277f21ab3503c60c84d4817de83c5edfdae0",
input={
"model_type": "dpt_beit_large_512",
"image":open(rgb_fp, "rb")
}
)
print(output)
使用 Marigold 进行单目深度估计
源于在文本到图像领域的巨大成功,扩散模型正在被应用于越来越广泛的问题。Marigold “重新利用”基于扩散的图像生成模型进行单目深度估计。
要在本地运行 Marigold,你需要克隆这个 git 仓库:
git clone https://github.com/prs-eth/Marigold.git
这个仓库介绍了一个新的扩散器管道MarigoldPipeline,使得应用 Marigold 变得更加简单:
## load model
from Marigold.marigold import MarigoldPipeline
pipe = MarigoldPipeline.from_pretrained("Bingxin/Marigold")
## apply to first sample, as example
rgb_image = Image.open(dataset.first().filepath)
output = pipe(rgb_image)
depth_image = output['depth_colored']
接下来需要对输出的深度图像进行后处理。
如果改为通过 Replicate 运行,我们可以创建一个apply_marigold_model()函数,类似于上面的 DPT 案例,并遍历数据集中的样本:
import replicate
import requests
import io
def marigold_model(rgb_image):
output = replicate.run(
"adirik/marigold:1a363593bc4882684fc58042d19db5e13a810e44e02f8d4c32afd1eb30464818",
input={
"image":rgb_image
}
)
## get the black and white depth map
response = requests.get(output[1]).content
return response
def apply_marigold_model(sample, model, label_field):
rgb_image = open(sample.filepath, "rb")
response = model(rgb_image)
depth_image = np.array(Image.open(io.BytesIO(response)))[:, :, 0] ## all channels are the same
formatted = (255 - depth_image).astype("uint8")
sample[label_field] = fo.Heatmap(map=formatted)
sample.save()
for sample in dataset.iter_samples(autosave=True, progress=True):
apply_marigold_model(sample, marigold_model, "marigold")
session = fo.launch_app(dataset)

使用 Marigold 端点在 SUN RGB-D 样本图像上预测的相对深度图。图片由作者提供。
评估单目深度估计模型
现在我们有了多个模型的预测结果,让我们来评估它们!我们将利用scikit-image来应用三个常用于单目深度估计的简单指标:均方根误差(RMSE)、峰值信噪比(PSNR)和结构相似性指数(SSIM)。
💡较高的 PSNR 和 SSIM 分数表示更好的预测,而较低的 RMSE 分数表示更好的预测。
请注意,我得到的具体数值是我在此过程中执行的特定前后处理步骤的结果。重要的是相对性能!
我们将定义评估流程:
from skimage.metrics import peak_signal_noise_ratio, mean_squared_error, structural_similarity
def rmse(gt, pred):
"""Compute root mean squared error between ground truth and prediction"""
return np.sqrt(mean_squared_error(gt, pred))
def evaluate_depth(dataset, prediction_field, gt_field):
"""Run 3 evaluation metrics for all samples for `prediction_field`
with respect to `gt_field`"""
for sample in dataset.iter_samples(autosave=True, progress=True):
gt_map = sample[gt_field].map
pred = sample[prediction_field]
pred_map = pred.map
pred["rmse"] = rmse(gt_map, pred_map)
pred["psnr"] = peak_signal_noise_ratio(gt_map, pred_map)
pred["ssim"] = structural_similarity(gt_map, pred_map)
sample[prediction_field] = pred
## add dynamic fields to dataset so we can view them in the App
dataset.add_dynamic_sample_fields()
然后将评估应用于两个模型的预测:
evaluate_depth(dataset, "dpt", "gt_depth")
evaluate_depth(dataset, "marigold", "gt_depth")
计算某个模型/度量标准的平均性能,只需对该字段调用数据集的mean()方法:
print("Mean Error Metrics")
for model in ["dpt", "marigold"]:
print("-"*50)
for metric in ["rmse", "psnr", "ssim"]:
mean_metric_value = dataset.mean(f"{model}.{metric}")
print(f"Mean {metric} for {model}: {mean_metric_value}")
Mean Error Metrics
--------------------------------------------------
Mean rmse for dpt: 49.8915828817003
Mean psnr for dpt: 14.805904629602551
Mean ssim for dpt: 0.8398022368184576
--------------------------------------------------
Mean rmse for marigold: 104.0061165272178
Mean psnr for marigold: 7.93015537185192
Mean ssim for marigold: 0.42766803372861134
所有的度量标准似乎都一致认为 DPT 优于 Marigold。然而,重要的是要注意,这些度量标准并不完美。例如,RMSE 对异常值非常敏感,而 SSIM 对小误差不太敏感。为了更全面的评估,我们可以在应用程序中通过这些度量标准进行筛选,以可视化模型做得好的地方和做得差的地方——或者是度量标准未能捕捉到模型表现的地方。
最后,切换遮罩的开关是可视化真实值与模型预测差异的好方法:

由两种 MDE 模型预测的热力图与真实值的视觉对比。图片由作者提供。
结论
总结一下,我们学习了如何在我们的数据上运行单目深度估计模型,如何使用常见的度量标准评估预测结果,以及如何可视化结果。我们还了解到,单目深度估计是一个公认的困难任务。
数据质量和数量是严重的限制因素;模型往往难以推广到新的环境;而且度量标准并不总是好的模型性能指示器。量化模型性能的具体数值可能会因你的处理流程而有所不同。即使是你对预测深度图的定性评估,也可能会受到你的色彩方案和不透明度比例的强烈影响。
如果你从这篇文章中学到一件事,我希望是这一点:查看深度图本身,而不仅仅是度量标准,这一点至关重要!
注意:所有图片由作者提供。本篇文章中展示的所有工作流和视觉效果使用的子数据集是 NYU depth v2,该数据集是允许商业使用的开源许可证(MIT)。
如何评估任何语言中的多语言 LLM
在全球大规模多任务语言理解(Global-MMLU)基准上,评估特定语言 LLM 的准确性(使用 Python)
·发表于《Towards Data Science》 ·8 分钟阅读·2024 年 12 月 9 日
--

图片来自Benjamin Kaufmann 在Unsplash
每当一个新的大型语言模型(LLM)发布时,我们自然会问自己一个明显的问题:这个 LLM 是否比我当前使用的更好?
LLM 通常会在大量基准测试中进行评估,其中大多数基准仅为英语。
对于多语言模型来说,能够找到每种特定语言的评估指标是非常罕见的,这些语言出现在训练数据中。
有时评估指标是针对基础模型发布的,而不是针对经过指令微调的模型。通常,评估也不会在我们实际本地使用的量化模型上进行。
因此,很难找到在英语以外的特定语言中,来自多个大型语言模型(LLM)的可比较评估结果。
因此,在本文中,我们将使用全球大规模多任务语言理解(Global-MMLU)数据集,并通过我们选择的语言,利用广泛使用的 MMLU 基准进行评估。
目录
如何评估没有地面真实数据的 RAG
向量相似度搜索阈值、合成数据生成、LLM 作为判定者以及框架
·发布于 Towards Data Science ·10 分钟阅读·2024 年 9 月 24 日
--
评估检索增强生成(RAG)模型时,如果有地面真实数据可以对比会容易得多。但如果没有呢?这时事情会变得有些棘手。然而,即使没有地面真实数据,仍然有方法来评估你的 RAG 系统表现如何。下面,我们将介绍三种有效策略,如何从零开始创建地面真实数据集,当你拥有数据集时可以使用的度量,以及现有框架如何帮助你完成这个过程。
两种类型的 RAG 评估:检索评估和生成评估
以下每种策略都会被标记为检索评估、生成评估或两者兼有。
如何评估没有地面真实数据的 RAG?
向量相似度搜索阈值
类型:检索评估
如果你正在使用像 Pinecone 这样的向量数据库,可能对向量相似度的概念比较熟悉。本质上,数据库是根据查询的向量与潜在结果的向量之间的接近程度来检索信息的。即使没有“正确”的答案进行对比,你仍然可以依赖像余弦相似度这样的度量来评估检索到的文档质量。

余弦距离。[作者提供的图片]
例如,Pinecone 会返回余弦相似度值,显示每个结果与查询的相似程度。
# Create a pinecone (vector database) index that uses cosine similarity
pc.create_index(
name=index_name,
dimension=2,
metric="cosine",
spec=ServerlessSpec(
cloud='aws',
region='us-east-1'
)
)
# Retrieving top 3 closest vectors
query_results = index.query(
namespace="example-namespace1",
vector=[1.0, 1.5],
top_k=3,
include_values=True
)
# query_results
# The "score" here is the cosine similarity value
# {'matches': [{'id': 'vec1', 'score': 1.0, 'values': [1.0, 1.5]},
# {'id': 'vec2', 'score': 0.868243158, 'values': [2.0, 1.0]},
# {'id': 'vec3', 'score': 0.850068152, 'values': [0.1, 3.0]}],
通过展示相似性分数,你可以为检索到的文档设定通过或不通过的标准。较高的阈值(如 0.8 或以上)意味着要求更严格,而较低的阈值则会带入更多的数据,这些数据可能有用,也可能只是噪声。
这个过程并不是要立即找到一个完美的数字——而是关于不断试错。当结果在我们的特定应用中始终感觉有用时,我们就知道找到了最佳点。
使用多个 LLM 来评判回应
类型:检索 + 生成评估
评估你的 RAG 系统的另一种创造性方式是通过利用多个 LLM 来评判回应。尽管 LLM 在缺少真实数据的情况下无法提供完美的答案,但你仍然可以利用它们的反馈来比较回应的质量。
通过比较不同 LLM 的回应,并查看它们如何对这些回应进行排名,你可以评估检索和生成的整体质量。这不是完美的,但它是一种创造性的方法,可以从多个角度评估你系统输出的质量。
人工反馈:涉及专家
类型:检索 + 生成评估
有时,评估系统的最佳方法是采用传统方式——请人类进行判断。从领域专家那里获取反馈,可以提供即便是最好的模型也无法匹配的洞察。
设置评分标准
为了使人工反馈更可靠,帮助建立清晰一致的评分标准很重要。你可以让审稿人对以下方面进行评分:
-
相关性: 检索到的信息是否解决了查询?(检索评估)
-
正确性: 内容是否事实准确?(检索评估)
-
流畅度: 它读起来是否顺畅,还是感觉生硬或强迫?(生成评估)
-
完整性: 它是否完全覆盖了问题,还是留下了空白?(检索 + 生成评估)
有了这些标准,你可以更系统地了解你的系统表现如何。
获取基准
评估人工反馈质量的一个聪明方法是检查不同审稿人之间的一致性。你可以使用像Pearson 相关系数这样的度量,看看他们的判断有多接近。如果审稿人之间意见分歧很大,可能意味着评分标准不够清晰,也可能是任务比你预期的更具主观性。

Pearson 相关系数。[图片由作者提供]
减少噪声
人工反馈可能会有噪声,尤其是在评分标准不明确或任务具有主观性的情况下。以下是几种应对方法:
-
平均评分: 通过对多位审稿人的评分进行平均,你可以消除个体偏见或不一致性。
-
专注于一致性: 另一种方法是只考虑审阅者一致同意的情况。这将为你提供更清晰的评估集,并帮助确保反馈的质量。
从零开始创建真实数据集
在评估没有真实数据集的 RAG 系统时,另一种方法是自己创建数据集。听起来可能有些令人生畏,但有几个策略可以简化这个过程,从寻找相似数据集到利用人工反馈,甚至合成生成数据。让我们来看看如何做到这一点。
在线寻找相似的数据集
这看起来可能显而易见,大多数得出没有真实数据集结论的人通常已经用尽了这个选项。但仍值得一提的是,可能会有一些数据集与您的需求类似。也许它来自不同的业务领域,但它与您正在使用的问答格式相匹配。像 Kaggle 这样的网站拥有丰富的公共数据集,你可能会惊讶于有多少数据集与你的问题空间对接。
示例:
手动创建真实数据集
如果你在线上找不到完全符合需求的内容,你可以手动创建真实数据集。这时,人工反馈(human-in-the-loop)就显得特别有用。还记得我们之前讨论的领域专家反馈吗?你可以利用这些反馈来构建你自己的小型数据集。
通过整理一组人工审核的示例——这些示例的相关性、准确性和完整性已经得到了验证——你为扩展评估数据集打下了基础。
Katherine Munro 还有一篇很好的文章,讲述了敏捷聊天机器人开发的实验方法。
训练 LLM 作为评判者
一旦你拥有了最小的真实数据集,你就可以进一步提升,训练一个 LLM 来充当评判者,并评估你模型的输出。
但在依赖大型语言模型(LLM)作为评判者之前,我们首先需要确保它能够准确地评估我们模型的输出,至少是可靠的。你可以按照以下方法来实现:
-
构建人工审核的示例: 根据你的使用场景,20 到 30 个示例应该足够让你了解 LLM 在对比中有多可靠。请参考前一节关于评估标准和如何衡量冲突评分的内容。
-
创建您的 LLM 评判者: 向 LLM 提出问题,要求它根据您提供给领域专家的相同标准给出评分。获取评分并比较 LLM 的评分与人工评分的一致性。您可以使用类似皮尔逊相关系数的度量来帮助评估。高相关性得分将表明 LLM 的表现与评审员一样出色。
-
应用 提示工程最佳实践:提示工程能够决定这个过程的成败。使用像预热 LLM 上下文或提供一些示例(少量学习)等技术,可以显著提高模型在评判时的准确性。
创建特定的数据集
提升基础真相数据集质量和数量的另一种方式是将文档按主题或语义分组进行切分。与其整体查看文档,不如将其拆解成更小、更集中的段落。
例如,假设您有一个文档(文档 ID: 123),其中提到:
“在推出产品 ABC 之后,XYZ 公司在 2024 年第一季度的收入增长了 10%。”
这句话包含了两条不同的信息:
-
推出产品 ABC
-
2024 年第一季度收入增长了 10%
现在,您可以将每个主题扩展为其自己的查询和上下文。例如:
-
查询 1: “XYZ 公司推出了什么产品?”
-
上下文 1: “推出产品 ABC”
-
查询 2: “2024 年第一季度收入变化是多少?”
-
上下文 2: “XYZ 公司在 2024 年第一季度的收入增长了 10%”
通过将数据划分为具体的主题,您不仅可以为训练创建更多的数据点,还可以使数据集更加精准和聚焦。此外,如果您希望将每个查询追溯到原始文档以保证可靠性,您可以轻松地为每个上下文段落添加元数据。例如:
-
查询 1: “XYZ 公司推出了什么产品?”
-
上下文 1: “推出产品 ABC(文档 ID: 123)”
-
查询 2: “2024 年第一季度收入变化是多少?”
-
上下文 2: “XYZ 公司在 2024 年第一季度收入增长了 10%(文档 ID: 123)”
这样,每个段落都会与其来源关联,使得您的数据集在评估和训练时更加有用。
合成创建数据集
如果其他方法都失败了,或者您需要更多的数据而手动收集不足,合成数据生成可以是一个改变游戏规则的解决方案。使用数据增强或甚至 GPT 模型等技术,您可以基于现有示例创建新的数据点。例如,您可以对基础查询和上下文集进行轻微修改,生成不同的变体。
例如,首先处理查询:
- “XYZ 公司推出了什么产品?”
您可以合成生成类似的变体:
-
“XYZ 公司推出了哪款产品?”
-
“XYZ 公司推出的是什么产品?”
这将帮助您构建一个更大的数据集,而无需从头开始手动编写新的示例。
还有一些框架可以自动化生成合成数据的过程,我们将在最后一部分进行探讨。
一旦你有了数据集:是时候进行评估了
现在你已经收集或创建了数据集,是时候进入评估阶段了。RAG 模型包括两个关键领域:检索和生成。两者都很重要,理解如何评估每个领域将有助于你调整模型,更好地满足需求。
评估检索:检索到的数据有多相关?
RAG 中的检索步骤至关重要——如果你的模型不能拉取正确的信息,它将难以生成准确的响应。以下是你需要关注的两个关键指标:
-
上下文相关性: 这衡量检索到的上下文与查询的匹配度。本质上,你在问:这个信息与所提问题相关吗? 你可以使用数据集来计算相关性分数,可以通过人工判断或通过比较查询与检索文档之间的相似性度量(如余弦相似度)来实现。
-
上下文召回: 上下文召回关注的是检索到的相关信息的多少。可能拉取了正确的文档,但只包含了必要信息的一部分。为了评估召回率,你需要检查你的模型检索的上下文是否包含了所有关键信息,以便完全回答查询。理想情况下,你希望得到高召回率:你的检索应该抓取到你需要的信息,且不会遗漏任何关键信息。
评估生成:响应是否既准确又有用?
一旦正确的信息被检索到,下一步是生成一个不仅能回答查询,而且能够忠实且清晰地回答的响应。以下是两个关键方面需要评估:
-
忠实度: 这衡量生成的响应是否准确地反映了检索到的上下文。本质上,你希望避免“幻觉”——即模型编造了检索数据中没有的信息。忠实度是关于确保答案基于模型检索到的文档所呈现的事实。
-
答案相关性: 这指的是生成的答案与查询的匹配程度。即便信息忠实于检索到的上下文,它仍然需要与所提问题相关。你不希望模型提取出正确的信息,但没有准确回答用户的问题。
做加权评估
一旦你评估了检索和生成,你可以通过加权方式进一步整合这些评估。也许你更关心相关性而非召回率,或者忠实度是你的首要任务。你可以根据特定的使用场景为每个指标分配不同的权重。
例如:
-
检索: 60% 上下文相关性 + 40% 上下文召回率
-
生成: 70% 忠实度 + 30% 答案相关性
这种加权评估方法为你提供了在优先考虑应用最重要因素时的灵活性。如果你的模型需要 100%的事实准确性(如法律或医学领域),你可能会更重视忠实度。另一方面,如果完整性更为重要,你可能会关注召回率。
简化评估过程的现有框架
如果创建你自己的评估系统让你感到不知所措,不用担心——有一些很好的现有框架,已经为你完成了大部分繁重的工作。这些框架内置了专门用于评估 RAG 系统的指标,使评估检索和生成性能变得更加简单。让我们看看其中一些最有帮助的框架。
RAGAS(检索增强生成评估)
RAGAS 是一个专门设计的框架,用于评估 RAG 模型的性能。它包括评估检索和生成的指标,提供了一种全面的方式来衡量系统在每个步骤中的表现。它还通过采用进化生成范式来提供合成测试数据生成。
灵感来自于诸如 Evol-Instruct等作品,RAGAS 通过采用进化生成范式实现这一点,在这一过程中,具有不同特征的问题,如推理、条件性、多上下文等,会从提供的文档集中系统性地构建出来。——RAGAS 文档
ARES: 使用合成数据和 LLM 裁判的开源框架
ARES 是另一个强大的工具,它将合成数据生成与基于 LLM 的评估相结合。ARES 使用合成数据——由 AI 模型生成的数据,而不是从现实世界交互中收集的数据——来构建数据集,以便测试和优化你的 RAG 系统。
该框架还包括一个 LLM 裁判,正如我们之前所讨论的,它可以通过将模型输出与人工标注或其他参考数据进行比较来帮助评估模型的表现。
结论
即使没有真实数据,这些策略也可以帮助你有效地评估 RAG 系统。无论你是使用向量相似度阈值、多种 LLM、LLM 作为裁判、检索指标还是框架,每种方法都为你提供了一种衡量性能并改进模型结果的方式。关键是找到最适合你特定需求的方法,并且不要害怕在过程中进行调整。🙂
加入讨论! 订阅 获取实用的 AI 技巧、真实案例以及我在公开构建过程中的幕后见解。
想了解更多 LLM 的信息吗?查看我们的 AI-in-Action 博客 ,了解 AI 代理、提示工程和 LLMOps。
如何评估你的预测
注意你选择的度量
·发表于Towards Data Science ·阅读时间 15 分钟·2024 年 5 月 17 日
--

摄影:来自Isaac Smith在Unsplash上的照片
通过比较模型在测试集上的预测结果来测试和基准评估机器学习模型,即使是在部署后,这一点至关重要。为此,需要考虑一种评分,它接受一个预测值和一个测试点,并为预测相对于测试点的成功程度分配一个值。然而,在选择适当的评分方法时应该谨慎。特别是,在选择评估预测的方法时,我们应该坚持适当评分规则的理念。这里我只给出了这个概念的一个宽泛定义,但基本上,我们希望有一个得分在我们想要衡量的对象上最小化!
一般来说:可以使用 MSE 来评估均值预测,使用 MAE 来评估中位数预测,使用分位数得分来评估更一般的分位数预测,使用能量或 MMD 得分来评估分布预测。
考虑你想要预测的变量,比如一个随机变量Y,它来源于一个协变量向量X。在下面的示例中,Y是收入,X是某些特征,如年龄和教育水平。我们在一些训练数据上学到了一个预测器f,现在我们用f(x)来预测Y。通常,当我们希望尽可能准确地预测变量Y时,我们会预测给定x的Y的期望值,即f(x)应该逼近E[Y | X=x]。但更一般地,f(x)可以是中位数的估计值、其他分位数,甚至是完整的条件分布P(Y | X=x)。
现在,对于一个新的测试点y,我们希望评分你的预测,也就是说,我们需要一个函数S(y,f(x)),当f(x)是你能够做出的最佳预测时,S(y,f(x))在期望值下是最小的。例如,如果我们想预测E[Y | X=x],则这个得分给定为均方误差(MSE):S(y, f(x))= (y-f(x))²。
这里我们更详细地研究了在测试集(y_i,x_i), i=1,…,ntest上评分预测器f的原理。在所有示例中,我们将比较理想的估计方法和一个明显错误或天真的方法,并展示我们的得分是如何按预期工作的。这里使用的完整代码也可以在Github上找到。
示例
为了说明问题,我将模拟一个简单的数据集,该数据集应当模拟收入数据。我们将在本文中使用这个简单的示例来说明这些概念。
library(dplyr)
#Create some variables:
# Simulate data for 100 individuals
n <- 5000
# Generate age between 20 and 60
age <- round(runif(n, min = 20, max = 60))
# Define education levels
education_levels <- c("High School", "Bachelor's", "Master's")
# Simulate education level probabilities
education_probs <- c(0.4, 0.4, 0.2)
# Sample education level based on probabilities
education <- sample(education_levels, n, replace = TRUE, prob = education_probs)
# Simulate experience correlated with age with some random error
experience <- age - 20 + round(rnorm(n, mean = 0, sd = 3))
# Define a non-linear function for wage
wage <- exp((age * 0.1) + (case_when(education == "High School" ~ 1,
education == "Bachelor's" ~ 1.5,
TRUE ~ 2)) + (experience * 0.05) + rnorm(n, mean = 0, sd = 0.5))
hist(wage)
虽然这个模拟可能过于简化,但它反映了这类数据的一些公认特征:较大的年龄、较高的教育水平和更丰富的经验通常与更高的工资相关。使用“exp”运算符会导致工资分布严重偏斜,这是在此类数据集中常见的现象。

整个模拟人口的工资分布。来源:作者
关键是,即使我们固定年龄、教育和经验为某些特定值,这种偏斜现象仍然存在。假设我们观察一个特定的人,Dave,他 30 岁,拥有经济学学士学位,并有 10 年的工作经验,我们来看看根据我们的数据生成过程他的实际收入分布:
ageDave<-30
educationDave<-"Bachelor's"
experienceDave <- 10
wageDave <- exp((ageDave * 0.1) + (case_when(educationDave == "High School" ~ 1,
educationDave == "Bachelor's" ~ 1.5,
TRUE ~ 2)) + (experienceDave * 0.05) + rnorm(n, mean = 0, sd = 0.5))
hist(wageDave, main="Wage Distribution for Dave", xlab="Wage")

Dave 的工资分布。来源:作者
因此,给定我们对 Dave 的所有信息,Dave 的可能工资分布仍然严重偏斜。
我们还生成了一个由几个人组成的测试集:
## Generate test set
ntest<-1000
# Generate age between 20 and 60
agetest <- round(runif(ntest, min = 20, max = 60))
# Sample education level based on probabilities
educationtest <- sample(education_levels, ntest, replace = TRUE, prob = education_probs)
# Simulate experience correlated with age with some random error
experiencetest <- agetest - 20 + round(rnorm(ntest, mean = 0, sd = 3))
## Generate ytest that we try to predict:
wagetest <- exp((agetest * 0.1) + (case_when(educationtest == "High School" ~ 1,
educationtest == "Bachelor's" ~ 1.5,
TRUE ~ 2)) + (experiencetest * 0.05) + rnorm(ntest, mean = 0, sd = 0.5))
我们现在从简单开始,首先查看均值和中位数预测的得分。
均值和中位数预测的得分
在数据科学和机器学习中,兴趣通常集中在一个单一数字上,这个数字表示我们希望预测的分布的“中心”或“中位数”,即(条件)均值或中位数。为此,我们使用均方误差(MSE):

以及平均绝对误差(MAE):

一个重要的结论是,均方误差(MSE)是预测条件均值的适当度量,而平均绝对误差(MAE)则是用于条件中位数的度量。均值和中位数对于像我们在这里研究的偏态分布来说是不同的。
让我们通过非常简单的估计器(在实际生活中我们是无法获得的,仅用于说明)来阐明上述例子:
conditionalmeanest <-
function(age, education, experience, N = 1000) {
mean(exp((age * 0.1) + (
case_when(
education == "High School" ~ 1,
education == "Bachelor's" ~ 1.5,
TRUE ~ 2
)
) + (experience * 0.05) + rnorm(N, mean = 0, sd = 0.5)
))
}
conditionalmedianest <-
function(age, education, experience, N = 1000) {
median(exp((age * 0.1) + (
case_when(
education == "High School" ~ 1,
education == "Bachelor's" ~ 1.5,
TRUE ~ 2
)
) + (experience * 0.05) + rnorm(N, mean = 0, sd = 0.5)
))
}
也就是说,我们通过简单地从模型中模拟固定的年龄、教育和经验值来估计均值和中位数(这将是从正确的条件分布中进行模拟),然后我们简单地取这些值的均值/中位数。让我们在 Dave 身上测试这个方法:
hist(wageDave, main="Wage Distribution for Dave", xlab="Wage")
abline(v=conditionalmeanest(ageDave, educationDave, experienceDave), col="darkred", cex=1.2)
abline(v=conditionalmedianest(ageDave, educationDave, experienceDave), col="darkblue", cex=1.2)

蓝色:Dave 的估计条件中位数,红色:Dave 的估计条件均值。来源:作者
显然,均值和中位数是不同的,这正是我们从这样的分布中预期的结果。事实上,正如收入分布的典型特点,均值比中位数更高(更受高值的影响)。
现在让我们在测试集上使用这些估计器:
Xtest<-data.frame(age=agetest, education=educationtest, experience=experiencetest)
meanest<-sapply(1:nrow(Xtest), function(j) conditionalmeanest(Xtest$age[j], Xtest$education[j], Xtest$experience[j]) )
median<-sapply(1:nrow(Xtest), function(j) conditionalmedianest(Xtest$age[j], Xtest$education[j], Xtest$experience[j]) )
这给出了多种条件均值/中位数的值。现在我们计算 MSE 和 MAE:
(MSE1<-mean((meanest-wagetest)²))
(MSE2<-mean((median-wagetest)²))
MSE1 < MSE2
### Method 1 (the true mean estimator) is better than method 2!
# but the MAE is actually worse of method 1!
(MAE1<-mean(abs(meanest-wagetest)) )
(MAE2<-mean( abs(median-wagetest)))
MAE1 < MAE2
### Method 2 (the true median estimator) is better than method 1!
这显示了理论上已知的结果:MSE 在(条件)期望值E[Y | X=x]处最小,而 MAE 在条件中位数处最小。通常,当你尝试评估你的均值预测时,使用 MAE 是没有意义的。 在许多应用研究和数据科学中,人们使用 MAE 或两者结合来评估均值预测(我知道,因为我也做过)。虽然在某些应用中这可能是合理的,但对于不对称的分布,这可能会带来严重后果,正如我们在这个例子中看到的:当看 MAE 时,方法 1 看起来比方法 2 差,尽管前者正确地估计了均值。事实上,在这个高度偏态的例子中,方法 1 的 MAE 应该比方法 2 低。
要对条件均值预测进行评分,使用均方误差(MSE),而不是平均绝对误差(MAE)。MAE 在条件中位数处最小。
分位数和区间预测的评分
假设我们想要对分位数q_x的估计f(x)进行评分,使得


简单的分位数说明。来源:作者
在这种情况下,我们可以考虑分位数得分:

其中

为了解释这个公式,我们可以考虑两种情况:
(1) y 小于 f(x):

即,我们会遭遇惩罚,这个惩罚随着y与f(x)的差距增大而加剧。
(2) y 大于 f(x):

即,如果 y 距离 f(x) 越远,惩罚就越大。
请注意,权重的设置使得对于较高的 alpha,如果估计的分位数 f(x) 小于 y,惩罚会更大。这是有意设计的,确保正确的分位数确实是 S(y, f(x)) 的期望值最小化器。这个得分实际上就是 分位数损失(最多一个因子 2),例如,请参阅这篇 很棒的文章。它已经在 R 的 scoringutils 包中的 quantile_score 函数中实现。最后,注意对于 alpha=0.5:

这仅仅是 MAE!这很有道理,因为 0.5 分位数就是中位数。
凭借预测分位数的能力,我们也可以构建预测区间。考虑 (l_x, u_x),其中 l_x ≤ u_x 是满足以下条件的分位数:

实际上,如果 l_x 是 alpha/2 分位数,而 u_x 是 1-alpha/2 分位数,那么这一条件就满足。因此我们现在估计并评分这两个分位数。考虑 f(x)=(f_1(x), f_2(x)), 其中 f_1(x) 是 l_x 的估计,f_2(x) 是 u_x 的估计。我们提供了两种估计方法,一种是“理想”方法,通过从真实过程再次模拟来估计所需的分位数,另一种是“天真”方法,虽然覆盖率正确,但结果过大:
library(scoringutils)
## Define conditional quantile estimation
conditionalquantileest <-
function(probs, age, education, experience, N = 1000) {
quantile(exp((age * 0.1) + (
case_when(
education == "High School" ~ 1,
education == "Bachelor's" ~ 1.5,
TRUE ~ 2
)
) + (experience * 0.05) + rnorm(N, mean = 0, sd = 0.5)
)
, probs =
probs)
}
## Define a very naive estimator that will still have the required coverage
lowernaive <- 0
uppernaive <- max(wage)
# Define the quantile of interest
alpha <- 0.05
lower <-
sapply(1:nrow(Xtest), function(j)
conditionalquantileest(alpha / 2, Xtest$age[j], Xtest$education[j], Xtest$experience[j]))
upper <-
sapply(1:nrow(Xtest), function(j)
conditionalquantileest(1 - alpha / 2, Xtest$age[j], Xtest$education[j], Xtest$experience[j]))
## Calculate the scores for both estimators
# 1\. Score the alpha/2 quantile estimate
qs_lower <- mean(quantile_score(wagetest,
predictions = lower,
quantiles = alpha / 2))
# 2\. Score the alpha/2 quantile estimate
qs_upper <- mean(quantile_score(wagetest,
predictions = upper,
quantiles = 1 - alpha / 2))
# 1\. Score the alpha/2 quantile estimate
qs_lowernaive <- mean(quantile_score(wagetest,
predictions = rep(lowernaive, ntest),
quantiles = alpha / 2))
# 2\. Score the alpha/2 quantile estimate
qs_uppernaive <- mean(quantile_score(wagetest,
predictions = rep(uppernaive, ntest),
quantiles = 1 - alpha / 2))
# Construct the interval score by taking the average
(interval_score <- (qs_lower + qs_upper) / 2)
# Score of the ideal estimator: 187.8337
# Construct the interval score by taking the average
(interval_scorenaive <- (qs_lowernaive + qs_uppernaive) / 2)
# Score of the naive estimator: 1451.464
我们可以清楚地看到,平均而言,正确的估计方法得分远低于天真的方法!
因此,通过分位数得分,我们有了一个可靠的方式来评分单个分位数预测。然而,平均上界和下界分位数得分来评分预测区间的方法可能显得有些随意。幸运的是,事实证明这导致了所谓的 区间得分:

因此,通过一些代数技巧,我们可以通过平均 alpha/2 和 1-alpha/2 分位数的得分来评分预测区间,正如我们所做的那样。有趣的是,得到的区间得分奖励较窄的预测区间,并且如果观测值未落在区间内,则会施加惩罚,惩罚的大小取决于 alpha。除了使用分位数得分的平均值,我们还可以直接使用 scoringutils 包来计算这个得分。
alpha <- 0.05
mean(interval_score(
wagetest,
lower=lower,
upper=upper,
interval_range=(1-alpha)*100,
weigh = T,
separate_results = FALSE
))
#Score of the ideal estimator: 187.8337
这是我们在上面计算两个区间得分的平均值时得到的完全相同的数字。
在 R 的 scoringutils 包中实现的分位数得分可以用来评分分位数预测。如果想直接对预测区间进行评分,可以使用 interval_score 函数。
分布预测的得分
越来越多的领域必须处理 分布预测 问题。幸运的是,针对这个问题已经有了得分方法。特别是在这里,我关注的是所谓的 能量得分:

对于 f(x) 作为分布 P(Y | X=x) 的估计,第二项计算两个独立样本间的欧几里得距离期望值,这类似于一个归一化项,用来衡量如果比较的是相同的分布时的值。第一项则将样本点 y 与从 f(x) 中抽取的 X 进行比较。在对 P(Y | X=x) 从 Y 中抽取的期望下,这个值会在 f(x)=P(Y | X=x) 时最小化。
因此,我们不再仅仅预测均值或分位数,而是尝试预测每个测试点的工资分布。实际上,我们试图预测并评估我们为 Dave 绘制的条件分布。这有点复杂;我们如何表示学习到的分布呢?在实际中,这通过假设我们可以从预测的分布中获得样本来解决。因此,我们将从预测的分布中获得的 N 个样本与单个测试点进行比较。这可以通过 R 中的 scoringRules 包的 es_sample 实现:
library(scoringRules)
## Ideal "estimate": Simply sample from the true conditional distribution
## P(Y | X=x) for each sample point x
distributionestimate <-
function(age, education, experience, N = 100) {
exp((age * 0.1) + (
case_when(
education == "High School" ~ 1,
education == "Bachelor's" ~ 1.5,
TRUE ~ 2
)
) + (experience * 0.05) + rnorm(N, mean = 0, sd = 0.5))
}
## Naive Estimate: Only sample from the error distribution, without including the
## information of each person.
distributionestimatenaive <-
function(age, education, experience, N = 100) {
exp(rnorm(N, mean = 0, sd = 0.5))
}
scoretrue <- mean(sapply(1:nrow(Xtest), function(j) {
wageest <-
distributionestimate(Xtest$age[j], Xtest$education[j], Xtest$experience[j])
return(scoringRules::es_sample(y = wagetest[j], dat = matrix(wageest, nrow=1)))
}))
scorenaive <- mean(sapply(1:nrow(Xtest), function(j) {
wageest <-
distributionestimatenaive(Xtest$age[j], Xtest$education[j], Xtest$experience[j])
return(scoringRules::es_sample(y = wagetest[j], dat = matrix(wageest, nrow=1)))
}))
## scoretrue: 761.026
## scorenaive: 2624.713
在上述代码中,我们再次将“完美”估计(即从真实分布P(Y | X=x))采样)与一种非常简单的估计进行比较,后者并未考虑工资、教育或经验的任何信息。同样,评分可靠地识别了两种方法中更好的那一个。
能够用于对分布预测评分的能量评分,可以通过 R 包 scoringRules 来实现,如果可以从预测的分布中获得一个样本。
结论
我们已经研究了不同的预测评分方法。思考正确的度量标准来测试预测是很重要的,因为错误的度量标准可能会使我们选择并保持错误的模型来执行预测任务。
应该注意的是,特别对于分布预测来说,这种评分是一项困难的任务,评分在实际中可能没有多大作用。也就是说,即使一种方法导致了较大的改进,其评分可能只是略微更低。然而,这本身并不是问题,只要评分能够可靠地识别两种方法中更好的那一个。
参考文献
[1] Tilmann Gneiting & Adrian E Raftery (2007) 严格适当的评分规则、预测与估计,《美国统计学会期刊》,102:477, 359–378, DOI: 10.1198/016214506000001437
附录:所有代码汇总
这个文件也可以在Github上找到。
library(dplyr)
#Create some variables:
# Simulate data for 100 individuals
n <- 5000
# Generate age between 20 and 60
age <- round(runif(n, min = 20, max = 60))
# Define education levels
education_levels <- c("High School", "Bachelor's", "Master's")
# Simulate education level probabilities
education_probs <- c(0.4, 0.4, 0.2)
# Sample education level based on probabilities
education <- sample(education_levels, n, replace = TRUE, prob = education_probs)
# Simulate experience correlated with age with some random error
experience <- age - 20 + round(rnorm(n, mean = 0, sd = 3))
# Define a non-linear function for wage
wage <- exp((age * 0.1) + (case_when(education == "High School" ~ 1,
education == "Bachelor's" ~ 1.5,
TRUE ~ 2)) + (experience * 0.05) + rnorm(n, mean = 0, sd = 0.5))
hist(wage)
ageDave<-30
educationDave<-"Bachelor's"
experienceDave <- 10
wageDave <- exp((ageDave * 0.1) + (case_when(educationDave == "High School" ~ 1,
educationDave == "Bachelor's" ~ 1.5,
TRUE ~ 2)) + (experienceDave * 0.05) + rnorm(n, mean = 0, sd = 0.5))
hist(wageDave, main="Wage Distribution for Dave", xlab="Wage")
## Generate test set
ntest<-1000
# Generate age between 20 and 60
agetest <- round(runif(ntest, min = 20, max = 60))
# Sample education level based on probabilities
educationtest <- sample(education_levels, ntest, replace = TRUE, prob = education_probs)
# Simulate experience correlated with age with some random error
experiencetest <- agetest - 20 + round(rnorm(ntest, mean = 0, sd = 3))
## Generate ytest that we try to predict:
wagetest <- exp((agetest * 0.1) + (case_when(educationtest == "High School" ~ 1,
educationtest == "Bachelor's" ~ 1.5,
TRUE ~ 2)) + (experiencetest * 0.05) + rnorm(ntest, mean = 0, sd = 0.5))
conditionalmeanest <-
function(age, education, experience, N = 1000) {
mean(exp((age * 0.1) + (
case_when(
education == "High School" ~ 1,
education == "Bachelor's" ~ 1.5,
TRUE ~ 2
)
) + (experience * 0.05) + rnorm(N, mean = 0, sd = 0.5)
))
}
conditionalmedianest <-
function(age, education, experience, N = 1000) {
median(exp((age * 0.1) + (
case_when(
education == "High School" ~ 1,
education == "Bachelor's" ~ 1.5,
TRUE ~ 2
)
) + (experience * 0.05) + rnorm(N, mean = 0, sd = 0.5)
))
}
hist(wageDave, main="Wage Distribution for Dave", xlab="Wage")
abline(v=conditionalmeanest(ageDave, educationDave, experienceDave), col="darkred", cex=1.2)
abline(v=conditionalmedianest(ageDave, educationDave, experienceDave), col="darkblue", cex=1.2)
Xtest<-data.frame(age=agetest, education=educationtest, experience=experiencetest)
meanest<-sapply(1:nrow(Xtest), function(j) conditionalmeanest(Xtest$age[j], Xtest$education[j], Xtest$experience[j]) )
median<-sapply(1:nrow(Xtest), function(j) conditionalmedianest(Xtest$age[j], Xtest$education[j], Xtest$experience[j]) )
(MSE1<-mean((meanest-wagetest)²))
(MSE2<-mean((median-wagetest)²))
MSE1 < MSE2
### Method 1 (the true mean estimator) is better than method 2!
# but the MAE is actually worse of method 1!
(MAE1<-mean(abs(meanest-wagetest)) )
(MAE2<-mean( abs(median-wagetest)))
MAE1 < MAE2
### Method 2 (the true median estimator) is better than method 1!
library(scoringutils)
## Define conditional quantile estimation
conditionalquantileest <-
function(probs, age, education, experience, N = 1000) {
quantile(exp((age * 0.1) + (
case_when(
education == "High School" ~ 1,
education == "Bachelor's" ~ 1.5,
TRUE ~ 2
)
) + (experience * 0.05) + rnorm(N, mean = 0, sd = 0.5)
)
, probs =
probs)
}
## Define a very naive estimator that will still have the required coverage
lowernaive <- 0
uppernaive <- max(wage)
# Define the quantile of interest
alpha <- 0.05
lower <-
sapply(1:nrow(Xtest), function(j)
conditionalquantileest(alpha / 2, Xtest$age[j], Xtest$education[j], Xtest$experience[j]))
upper <-
sapply(1:nrow(Xtest), function(j)
conditionalquantileest(1 - alpha / 2, Xtest$age[j], Xtest$education[j], Xtest$experience[j]))
## Calculate the scores for both estimators
# 1\. Score the alpha/2 quantile estimate
qs_lower <- mean(quantile_score(wagetest,
predictions = lower,
quantiles = alpha / 2))
# 2\. Score the alpha/2 quantile estimate
qs_upper <- mean(quantile_score(wagetest,
predictions = upper,
quantiles = 1 - alpha / 2))
# 1\. Score the alpha/2 quantile estimate
qs_lowernaive <- mean(quantile_score(wagetest,
predictions = rep(lowernaive, ntest),
quantiles = alpha / 2))
# 2\. Score the alpha/2 quantile estimate
qs_uppernaive <- mean(quantile_score(wagetest,
predictions = rep(uppernaive, ntest),
quantiles = 1 - alpha / 2))
# Construct the interval score by taking the average
(interval_score <- (qs_lower + qs_upper) / 2)
# Score of the ideal estimator: 187.8337
# Construct the interval score by taking the average
(interval_scorenaive <- (qs_lowernaive + qs_uppernaive) / 2)
# Score of the naive estimator: 1451.464
library(scoringRules)
## Ideal "estimate": Simply sample from the true conditional distribution
## P(Y | X=x) for each sample point x
distributionestimate <-
function(age, education, experience, N = 100) {
exp((age * 0.1) + (
case_when(
education == "High School" ~ 1,
education == "Bachelor's" ~ 1.5,
TRUE ~ 2
)
) + (experience * 0.05) + rnorm(N, mean = 0, sd = 0.5))
}
## Naive Estimate: Only sample from the error distribution, without including the
## information of each person.
distributionestimatenaive <-
function(age, education, experience, N = 100) {
exp(rnorm(N, mean = 0, sd = 0.5))
}
scoretrue <- mean(sapply(1:nrow(Xtest), function(j) {
wageest <-
distributionestimate(Xtest$age[j], Xtest$education[j], Xtest$experience[j])
return(scoringRules::es_sample(y = wagetest[j], dat = matrix(wageest, nrow=1)))
}))
scorenaive <- mean(sapply(1:nrow(Xtest), function(j) {
wageest <-
distributionestimatenaive(Xtest$age[j], Xtest$education[j], Xtest$experience[j])
return(scoringRules::es_sample(y = wagetest[j], dat = matrix(wageest, nrow=1)))
}))
## scoretrue: 761.026
## scorenaive: 2624.713
如何将 Stata “笔记本” 导出为 HTML
创建一个可以分享的 HTML 文档,其中包含你的代码、输出结果和图表
·发表于 Towards Data Science ·阅读时长 8 分钟·2024 年 10 月 17 日
--
在数据项目中进行合作时,如何有效地分享工作至关重要。虽然在某些情况下,发送带注释的代码可能足够了,但有时这远远不够。
我在这里会诚实地说,我不喜欢使用 Stata。从编程的角度来看,我觉得它的语法非常不直观。但我主要的问题是,如果不通过分享数据和 do 文件给对方,让他们自己运行,最好的分享方式并不明确。
如果你们两个人都在使用这段代码,那没问题。然而,很多时候,另一个人只是想查看结果,并理解你是如何得到这些结果的。让他们自己运行代码既耗时又在最坏情况下可能带来后勤上的挑战。
尽管如此,我仍然需要使用 Stata,因为它在经济研究中被广泛使用。
但现在我发现了解决这个问题的最佳方法。那就是使用webdoc命令。
阅读完这篇文章后,你将能够将你的代码,例如下面这个:
* Stata code
* Name: Your Name
* Date: October 2024
clear all
cd "your/directory/"
sysuse auto
* Regression n°1
regress price mpg
* Graphs
twoway (scatter price mpg) (lfit price mpg)
如何通过 REST API 暴露 Delta 表
三种架构讨论并测试用于服务 Delta 表
·发布于 Towards Data Science ·7 分钟阅读·2024 年 5 月 6 日
--

通过内外数据暴露——图片由 Joshua Sortino on Unsplash 提供
1. 引言
金奖架构 中的 Delta 表通常用于创建数据产品。这些数据产品用于数据科学、数据分析和报告。然而,一个常见的问题是如何通过 REST API 暴露数据产品。这个想法是将这些 API 嵌入具有更严格性能要求的 Web 应用程序中。以下是一些重要问题:
-
从 Delta 表读取数据是否足够快以支持 Web 应用程序?
-
是否需要计算层以使解决方案更具可扩展性?
-
是否需要存储层来满足严格的性能要求?
为了深入探讨这些问题,评估了三种架构,如下所示:架构 A——API 中的库,架构 B——计算层,架构 C——存储层。请参阅下图。

三种架构用于暴露 Delta 表——图片由作者提供
在博客文章的其余部分中,将描述、部署和测试这三种架构。然后得出结论。
2. 架构描述
2.1 架构 A:使用 DuckDB 和 PyArrow 的 API 中的库
在此架构中,API 直接连接到 delta 表,中间没有计算层。这意味着数据通过 API 本身的内存和计算进行分析。为了提高性能,使用了嵌入式数据库 DuckDB和PyArrow的 Python 库。这些库确保只加载相关的数据(例如,只加载 API 所需的列)。

架构 A:API 中的库 — 作者提供的图像
这种架构的优点是数据不需要复制,并且 API 和 delta 表之间不需要中间层。这意味着移动部件更少。
这种架构的缺点是它更难扩展,所有的工作都需要在 API 本身的计算和内存中完成。如果需要分析大量数据,这尤其具有挑战性。这些数据可能来自大量记录、大量列和/或大量并发请求。
2.2 架构 B:使用 Synapse、Databricks 或 Fabric 的计算层
在此架构中,API 连接到计算层,而不是直接连接到 delta 表。计算层从 delta 表中获取数据并进行分析。计算层可以是Azure Synapse、Azure Databricks或Microsoft Fabric,并且通常能够很好地扩展。数据不会复制到计算层,但可以在计算层应用缓存。在本文的剩余部分中,测试使用了Synapse 无服务器。

架构 B:计算层 — 作者提供的图像
这种架构的优点是数据不需要复制,架构能够很好地扩展。此外,它可以用于处理大数据集。
这种架构的缺点是需要在 API 和 delta 表之间添加一个额外的层。这意味着需要维护和保护更多的移动部件。
2.3 架构 C:使用 Azure SQL 或 Cosmos DB 的优化存储层
在此架构中,API 不是连接到 delta 表,而是连接到另一个存储层,在该存储层中,delta 表被复制。不同的存储层可以是 Azure SQL 或 Cosmos DB。存储层可以针对快速检索数据进行优化。本文的其余部分中,测试使用了 Azure SQL。

架构 C:优化存储层 — 作者提供的图像
这种架构的优点是存储层可以通过使用索引、分区和物化视图来优化数据读取速度。这通常是请求-响应型 Web 应用场景中的需求。
这种架构的缺点是需要对数据进行重复存储,并且在 API 和 Delta 表之间需要一个额外的层。这意味着需要维护和保护更多的组件。
在接下来的博客中,将部署和测试这些架构。
3. 部署和测试架构
3.1 部署架构
为了部署这些架构,创建了一个 GitHub 项目,该项目部署了前一章中讨论的三种解决方案。该项目可以通过以下链接找到:
https://github.com/rebremer/expose-deltatable-via-restapi
执行 GitHub 项目时将部署以下内容:
-
一个源自标准测试数据集 WideWorldImportersDW full 的 Delta 表。该测试数据集包含 5000 万条记录和 22 个列,其中包含一个大描述列。该 Delta 表将使用年份和季度进行分区。
-
所有架构: Azure Function 作为 API。
-
架构 B:Synapse Serverless 作为计算层。
-
架构 C:Azure SQL 作为优化存储层。
一旦部署完成,就可以执行测试。测试将在下一个段落中描述。
3.2 测试架构
为了测试架构,将应用不同类型的查询和不同的扩展方式。不同类型的查询可以描述如下:
-
查找 20 条记录,每条记录包含 11 个小列(字符型、整数型、日期时间型)。
-
查找 20 条记录,其中 2 列包含一个大描述列,每个字段超过 500 个字符。
-
使用 group by、having、max、average 等进行数据聚合。
以下是查询的示例。
-- Query 1: Point look up 11 columns without large texts
SELECT SaleKey, TaxAmount, CityKey, CustomerKey, BillToCustomerKey, SalespersonKey, DeliveryDateKey, Package
FROM silver_fact_sale
WHERE CityKey=41749 and SalespersonKey=40 and CustomerKey=397 and TaxAmount > 20
-- Query 2: Description column with more than 500 characters
SELECT SaleKey, Description
FROM silver_fact_sale
WHERE CityKey=41749 and SalespersonKey=40 and CustomerKey=397 and TaxAmount > 20
-- Query 3: Aggregation
SELECT MAX(DeliveryDateKey), CityKey, AVG(TaxAmount)
FROM silver_fact_sale
GROUP BY CityKey
HAVING COUNT(CityKey) > 10
扩展性可以描述如下:
-
对于架构 A,数据处理将在 API 本身中完成。这意味着将使用 API 的 应用服务计划 来利用 API 的计算和内存资源。将使用 SKU Basic(1 核心和 1.75 GB 内存)和 SKU P1V3 SKU(2 核心,8 GB 内存)进行测试。对于架构 B 和 C,这不相关,因为处理是在其他地方进行的。
-
对于架构 B,使用 Synapse Serverless。扩展将自动完成。
-
对于架构 C,使用 标准层 的 Azure SQL 数据库,提供 125 DTU。将分别测试没有索引和在 CityKey 上有索引的情况。这个索引可以在之后应用,并且不需要重新分区数据。
下一段将描述结果。
3.3 结果
部署并测试架构后,可以获得结果。这是结果的总结:

测试结果总结
架构 A 不能使用 SKU B1 部署。如果使用 SKU P1V3,则在列大小不太大的情况下,结果可以在 15 秒内计算出来。请注意,所有数据都在 API 应用服务计划中进行分析。如果加载了过多数据(无论是通过很多行、大列和/或大量并发请求),这个架构将难以扩展。
使用 Synapse Serverless 的架构 B 在 10 到 15 秒内完成计算。计算在自动扩展的 Synapse Serverless 上进行,用于提取和分析数据。所有三种类型的查询的性能是一致的。
使用 Azure SQL 的架构 C 在创建索引时表现最佳。对于查询 1 和查询 2,API 的响应时间大约为 1 秒。查询 3 需要进行全表扫描,其性能与其他解决方案相当。
3. 结论
Medallion 架构中的 Delta 表通常用于创建数据产品。这些数据产品用于数据科学、数据分析和报告。然而,一个常见的问题是如何通过 REST API 暴露 delta 表。在这篇博客中,描述了三种架构及其优缺点。
架构 A:使用 DuckDB 和 PyArrow 的 API 中的库。在这个架构中,API 直接连接到 delta 表,并且中间没有任何层级。这意味着所有的数据都在 Azure Function 的内存和计算中进行分析。
-
这个架构的优点是无需额外的资源。这意味着需要维护和保护的部件更少。
-
这个架构的缺点是它的扩展性差,因为所有的数据都必须在 API 本身进行分析。因此,它只能用于少量的数据。
架构 B:使用 Synapse、Databricks 或 Fabric 的计算层。在这个架构中,API 连接到计算层。这个计算层从 delta 表中提取和分析数据。
-
这个架构的优点是它具有良好的扩展性,且数据不会重复。它非常适合需要聚合并处理大数据集的查询。
-
这个架构的缺点是,对于查询操作,无法始终在 5 秒内返回响应。此外,还需要确保和维护额外的资源。
架构 C:使用 Azure SQL 或 Cosmos DB 的优化存储层。
在这个架构中,API 连接到一个优化过的存储层。Delta 表预先复制到这个存储层,并且该存储层用于提取和分析数据。
-
这种架构的优点在于,可以通过使用索引、分区和物化视图来优化查询性能。这通常是请求-响应型 Web 应用的要求。
-
这种架构的缺点在于,数据需要复制到另一个存储层,并且需要保持同步。此外,还需要保障和维护额外的资源。
不幸的是,并没有一种“灵丹妙药”式的解决方案。本文旨在为通过 REST API 暴露增量表选择最佳架构提供指导。
如何使用 NetworkX 提取图形特征以进行机器学习
图形机器学习 — 从零到英雄
使用 Python 的逐步指南
·发表于 Towards Data Science ·阅读时间 17 分钟·2024 年 9 月 3 日
--
图是非常重要的结构,能够建模各个领域中实体之间的连接关系,涵盖了如在线社交平台、交通网络和生物系统等多种场景。通过从这些图中提取有意义的特征,我们能够获得宝贵的洞察并提升机器学习算法的性能。
在之前的文章中,我们展示了如何使用 NumPy 和 NetworkX 来表示图。在本文中,我们将探索如何使用 NetworkX 在不同层次(节点、边和图本身)提取重要的图形特征。
## 如何表示图结构 — 从 NumPy 到 NetworkX
让我们理解如何使用 Python 创建和可视化网络信息
towardsdatascience.com
我们将使用 Zachary 的空手道俱乐部网络,这是 NetworkX 提供的一个示例图(查看许可证详情)。这个广为人知的数据集代表了一个大学空手道俱乐部的社交网络,是理解基于图的特征提取的一个绝佳起点。
在深入细节之前,让我们定义一些帮助我们显示图表的代码。首先,我们介绍一些辅助函数来指定默认的可视化选项。在下面的代码中,你将看到两个函数…
如何找到并解决有价值的生成性 AI 用例
80%的 AI 项目因用例不当或技术知识不足而失败。生成性 AI 简化了复杂性,现在我们必须挑选正确的战斗。
·发布于Towards Data Science ·阅读时间 7 分钟·2024 年 6 月 18 日
--

“纸夹与朋友”公司想要搭上 AI 的热潮。他们能走多远?
显而易见,AI 项目很难。一些人估计,80%的 AI 项目失败。 尽管如此,生成性 AI 已经存在并将持续发展,企业正在寻找如何将其应用到运营中。AI 项目失败,是因为它们未能创造价值。失败的根本原因是将 AI 应用到错误的用例中。寻找正确用例的解决方案有三个步骤:
-
衡量问题的规模
-
回顾性地衡量解决方案的准确性
-
衡量解决方案的准确性,实时进行
这些步骤应按顺序调查。如果问题规模不足以带来所需的价值,不要构建。如果在历史数据上的解决方案准确性不够高,不要部署。如果实时准确性不够高,调整解决方案。

每个衡量阶段必须通过,才能进入下一个阶段。
我将讨论生成式人工智能,而不是一般的人工智能。生成式人工智能是人工智能的一个小子领域。在一般的人工智能项目中,目标是找到一个模型来近似数据的生成方式——你必须具备高度的机器学习算法和数据处理能力。而在生成式人工智能中,模型已经给定,如 LLM(例如 chatGPT),目标是使用现有的模型来解决一些业务问题。后者需要的技术能力较少,更多的是问题解决知识。生成式人工智能的应用场景更容易实现和验证,因为算法创建的步骤被省略了,而数据(文本)是(相对)标准化的。
衡量问题的严重程度
每个人都能在日常工作中识别出问题。挑战在于确定哪些问题足够重要,值得解决,并且 AI 可以并且应该被应用在哪里。
我们不需要解决所有主观问题并找到数据来验证它们的存在,而是可以集中于生成文本数据的过程。这个方法将范围缩小到可衡量的问题,在这些问题上,AI 和自动化可以带来明显的价值。
具体来说,我们不是问客户支持专家“你在工作中有哪些问题?”,而是应该衡量员工花费最多时间的地方。让我们通过一个例子来说明这一点。

Paperclips & Friends 是一家回形针公司。看看那些快乐的面孔。
Paperclips & Friends(P&F)是一家制造回形针的公司。他们有一个支持频道#P&FSupport,客户在该频道讨论有关回形针的问题。 P&F 及时回应客户的问题,但该频道变得越来越忙。客户支持专家听说了 ChatGPT,并希望它能帮助回答客户问题。

回形针客户支持专家已经厌倦了所有的客户问题。
在 P&F 的数据科学团队开始解决问题之前,他们衡量了进来的问题数量,以了解问题的严重程度。他们注意到每天有成百上千个询问。
数据科学团队开发了一个检索增强生成(RAG)聊天机器人,使用了 ChatGPT 和 P&F 的内部文档。他们发布了聊天机器人,供客户支持专家进行测试,并收到了一些混合反馈。一些专家喜欢这个解决方案,并提到它解决了大多数问题,而另一些人则批评它,声称它没有价值。
P&F 数据科学团队面临一个挑战——谁说的是实话? 这个聊天机器人靠谱吗?然后,他们想起了第二个衡量标准:
“衡量 解决方案 的准确性回顾”
注意: 验证是否能够解决根本原因是至关重要的。如果 Paperclips & Friends 发现,90%的支持频道消息都与不清晰的使用说明相关,那么 P&F 可以创建一个聊天机器人来回答这些消息。然而,如果 P&F在纸夹发货时附上了简单的说明指南,客户根本就不会提出这些问题。

P&F 数据科学团队尽力构建聊天机器人的插图。
回顾性地衡量解决方案的准确性。
P&F 数据科学团队面临一个挑战:他们必须平等地权衡每个专家的意见,但无法满足每个人的要求。于是,他们决定根据历史客户问题来评估聊天机器人,而不是专注于专家的主观意见。现在,专家们不需要提出问题来测试聊天机器人,这样评估就更接近真实世界的条件。毕竟,邀请专家参与的初衷是因为他们相较于 P&F 数据科学团队,更了解真实的客户问题。
结果发现,P&F 常见的问题与纸夹的技术说明有关。P&F 的客户希望了解纸夹的详细技术规格。P&F 有成千上万种不同的纸夹类型,客户支持需要花费大量时间回答这些问题。
理解测试驱动开发后,数据科学团队从对话历史中创建了一个数据集,包括客户问题和客户支持回复:

数据集来自 Paperclips & Friends 的 Discord 频道。
拥有问题和答案的数据集后,P&F 可以回顾性地测试和评估聊天机器人的表现。他们创建了一个新列,“聊天机器人回复”,并存储了聊天机器人对这些问题的示例回复。

增强的数据集,包含建议的聊天机器人回答。
我们可以让专家和 GPT-4 一起评估聊天机器人回复的质量。最终目标是通过利用 GPT-4 自动化聊天机器人准确性评估。如果专家和 GPT-4 的评估结果相似,这一目标是可行的。
专家们为每个专家的评估创建了一个新的 Excel 表格,数据科学团队则添加了 GPT-4 的评估。

增强的数据集,包含专家和 GPT-4 的评估。
对于不同专家评估同样的聊天机器人回复,存在冲突。GPT-4 的评估方式类似于专家的多数投票,这表明我们可以使用 GPT-4 进行自动评估。然而,每个专家的意见都很有价值,解决专家之间冲突的评估偏好非常重要。
P&F 组织了一个研讨会,邀请专家们为历史问题数据集创建黄金标准回复。

评估的黄金标准数据集。
以及评估 最佳实践指南,这是所有专家达成一致的意见。

客户支持专家定义的聊天机器人“最佳实践指南”评估。
通过研讨会获得的见解,数据科学团队可以为 GPT-4 创建更详细的评估提示,涵盖边缘情况(即“聊天机器人不应要求提交支持票”)。现在,专家可以利用时间改进回形针文档并定义最佳实践,而不是进行繁琐的聊天机器人评估**。
通过衡量正确聊天机器人回复的百分比,P&F 可以决定是否将聊天机器人部署到支持渠道。他们批准了准确性并部署了聊天机器人。
实时衡量解决方案准确性
最后,是时候保存所有聊天机器人回复并计算其在解决实际客户咨询方面的表现。由于客户可以直接回应聊天机器人,因此记录客户的回应也很重要,以便了解客户的情绪。
相同的评估工作流程可以用来实际衡量聊天机器人的成功,而无需地面真实回复。但现在,客户会收到聊天机器人的初步回复,我们不知道客户是否喜欢它。我们应该调查客户如何对聊天机器人的回复作出反应。我们可以自动检测客户回复中的负面情绪,并指派客户支持专家处理愤怒的客户。
结论
在这篇简短的文章中,我解释了避免 AI 项目失败的三个步骤:
-
衡量 问题的规模
-
回顾性地衡量解决方案的准确性
-
实时衡量解决方案的准确性
前两步至关重要,是许多项目失败的主要原因。虽然没有衡量问题的规模或解决方案的准确性也可能成功,但主观估计通常是有缺陷的,因为存在数百种人类偏见。正确设计的数据驱动方法几乎总是能提供更好的结果。
如果你对如何实现这样的聊天机器人感兴趣,可以查看我关于RAG 的博客文章以及关于高级 RAG 的博客文章。
最后,如果你愿意阅读类似的文章,欢迎在 LinkedIn 上与我建立联系并关注我 😃
LinkedIn: @sormunenteemu
除非另有说明,所有图片和数据均来自作者。
如何为你的 RAG 找到最佳的多语言嵌入模型
优化嵌入空间以提升 RAG
·发布于 Towards Data Science ·8 分钟阅读·2024 年 1 月 27 日
--

图片来自作者,AI 生成。
嵌入是捕捉单词或句子语义含义的向量表示。除了拥有优质的数据,选择一个好的嵌入模型是优化你的 RAG 应用程序中最重要且最被低估的步骤。多语言模型尤其具有挑战性,因为大多数模型都是在英语数据上预训练的。正确的嵌入会带来巨大的差异——不要仅仅选择你看到的第一个模型!
语义空间决定了单词和概念之间的关系。准确的语义空间能够提高检索性能。不准确的嵌入会导致无关的片段或丢失的信息。更好的模型直接提升你 RAG 系统的能力。
在本文中,我们将从 PDF 文档中创建一个问答数据集,以便为我们的任务和语言找到最佳模型。在 RAG 过程中,如果能够检索到期望的答案,这意味着嵌入模型将问题和答案在语义空间中定位得足够接近。
虽然我们专注于法语和意大利语,但这个过程可以适应任何语言,因为最佳的嵌入可能会有所不同。
嵌入模型
如何在数字化世界中找到自我
在不确定性下自我定位的概率方法
·发表于Towards Data Science ·阅读时长 8 分钟·2024 年 3 月 4 日
--
目录:
-
什么是定位,机器人为什么需要它?
-
为什么使用概率工具来计算定位?
-
端到端示例:如何使用贝叶斯算法在不确定性下确定机器人的位置?
自动驾驶汽车如何在时速 60 英里下保持在车道内?i-robot 如何避免从楼梯上摔下来?送货机器人如何知道它们是否正朝着正确的饥饿顾客前进?这些只是自动驾驶汽车必须在没有人类干预的情况下回答的一些问题。
1. 什么是定位?机器人为什么需要它?
正如你可能想象的那样,准确的车辆位置对于自动驾驶汽车是否能有效且安全地完成任务至关重要。从传感器数据估算车辆位置的过程被称为定位。随着提供更多信息的传感器加入,定位精度会提高,而随着车辆的运动引入噪声,定位精度会降低。
2. 为什么使用概率工具来计算定位?
概率工具可以被用来提高定位精度,尤其是在传感器或运动都不是 100%精确的情况下。
什么是概率?
根据词典定义,概率是“描述某事件发生可能性的数值”(维基百科)。然而,关于概率的意义,答案并不那么简单。频率主义者和贝叶斯主义者两个主要阵营对概率有不同的解释。
频率主义方法将概率解释为时间的相对频率;如果我多次重复一个实验,我能得到理想结果的次数是多少?
这种方法是客观的,因为任何进行实验的人(例如抛硬币)在长时间内都会得到相同的结果。
贝叶斯方法将概率解释为事件发生的确定性程度。在专家知识和可用数据的基础上,我对自己获得期望结果的确定性有多高?这种方法是主观的,因为它通过结合(主观的)先前知识和实验数据,代表了当前的信念状态。它允许估算我们无法多次实验的单一事件的概率,在这种情况下,频率学派的解释并不适用。
例如,如果约会后第一次见面时对方回复你短信的概率是 0.8,那么我们可以 80%确定你们度过了一段美好时光,并且对方会回复你短信;我们并不是指他们会在你不断重复第一次约会时 80%的概率回复短信。
贝叶斯概率的好处是什么?
贝叶斯概率让我们既能量化我们的信念程度,又能根据新的证据更新它。

贝叶斯定理:根据来自传感器的证据 E 更新机器人位置的假设 P(H),也称为先验,返回 P(H|E),也称为后验。
在我们的背景下,P(H)是我们对机器人当前位置的初步猜测,P(H|E)是我们在测量传感器证据 E 之后更新的猜测。
假设概率分布量化了我们对机器人当前位置的确定性。

(1) 均匀分布:如果我们不知道机器人在哪里,所有位置的概率是相等的 (2) 正态分布,均值=最佳猜测,标准差=不确定性
假设可以根据证据进行变化
传感器数据越有信息量和准确性,它的影响就越大。如果传感器是完美的,机器人的位置将与传感器的读数一致;否则,如果传感器数据非常嘈杂或缺乏信息量,机器人的位置将保持不变。

更新我们的假设:在证据 P(H|E)之后,假设与我们最初的信念 P(H)有多大不同?
更新可以结合多个证据来源
我们可以通过使用链式法则,也叫做一般乘积法则,来将贝叶斯定律应用于多个数据源的结合。它允许将多个证据的联合分布简化为条件概率的乘积。

贝叶斯定理通过应用链式法则,推广到多个证据 E1 和 E2 的情况。
例如,我们通常使用 GPS 从当前位置导航,但 GPS 在晴朗的天空下效果最佳,并且其精度仅限于几米。自动驾驶汽车不能仅仅依赖 GPS 来保持在几米宽的车道上,并在隧道或地下停车场内导航。自动驾驶汽车可以通过添加更多的信息来源,如摄像头,来弥补 GPS 的不足。
3. 端到端示例:如何使用贝叶斯算法在不确定性下确定机器人的位置?
让我们深入探讨一个贝叶斯滤波器,它通过贝叶斯推断递归地改善定位概率估计。递归特性意味着,在时间 t_0 时,滤波器的输出 P(H|E) 作为下一个时间戳 t_1 时刻 P(H) 的假设输入。
假设一个配送机器人正在太空站内绕着圆形路径运输物资。机器人有一张地图,详细标出了地形和传感器的位置。

机器人的地图显示了路线和传感器的位置,信标 1 用橙色表示,信标 2 用蓝色表示。路线的圆形特性使得机器人在到达最后一个区间时会返回起点。(机器人图像由 Dall-E 生成)
- 问题定义:
我们将估计的机器人位置称为机器人状态空间。例如,一个二维向量(即一对有序数字)表示 x 轴位置和 x 轴速度,可以在一维中追踪机器人的位置和变化速度。可以扩展机器人的状态空间到更多维度,以追踪多个位置维度(y,z),方向等。
为了简化起见,我们可以假设机器人以恒定速度移动。运动会增加计算的不确定性,因为它并非 100% 可靠。发动机可能无法以特定的速度运行,或者机器人可能会遇到障碍物,这将导致机器人超越或未达到预期的运动位置。
我们的机器人将通过测量信标的存在来感知其位置。传感器读取数据,也叫做测量空间,并不是 100% 精确的。传感器可能会把噪声误认为是信标信号,导致误报,或者根本无法探测到信号。

机器人的向量:(1)表示时间 t 时刻 x 位置的状态空间向量(2)表示时间 t 时刻信标存在的测量空间向量
- 算法:直方图滤波器
使用这个贝叶斯滤波器,机器人状态空间通过有限数量的区间或区域表示为直方图。它是一个离散滤波器,意味着机器人只能位于这些区域中的一个,我们计算机器人位于每个区域的概率。此外,在每个区间内,例如 5 平方米的区域,位于任何特定点的概率是相同的。如果我们想要增加粒度,必须添加更多的区间。
这个滤波器是非参数化的,意味着它不对机器人的状态表示做出任何强假设,也不局限于像高斯分布这样的单一分布类型。它能够表示复杂的位置估计,比如保持多个最佳猜测的多模态假设,但这也带来了计算成本——指数级的复杂性。为了增加一个维度,从 1D 到 2D 同时保持相同的粒度,我们需要 10x10 的垃圾桶;如果要转到 3D,我们需要 10x10x10 的垃圾桶,以此类推。这对于跟踪多个维度且在内存和计算能力上有限的机器人来说,是一个显著的限制。
- 计算:
- 初始猜测: 我们从一个未知的位置开始,并配有一张地图。一开始,每个区域的概率是相同的,所有垃圾桶的分布是均匀的。

x 位置被分成 10 个离散区域,表示机器人的状态。最初,我们没有关于机器人所在位置的信息,因此每个区域的概率相同,都是 1/10。
2. 移动函数: 模拟机器人的运动。机器人的运动是随机的,这意味着它不保证每次都能移动到期望的垃圾桶。在每次移动后,为了更新机器人的位置,我们计算机器人在下一个时间步中位于每个区域的概率。这个计算考虑了机器人保持在同一区域的概率以及移动到其他区域的概率。
For each movement:
For each region:
Region probability at time t+1 =
Region probability at time t x stay probability +
Probability of robot coming from the neighboring region x move probability

移动函数:假设机器人每次移动一步,那么成功移动一步的概率是多少?机器人移动到下一个垃圾桶的概率为 95%,停留在当前位置的概率为 5%。
如下面的方程所示,机器人一次移动不会改变位置估计,因为采用的是均匀分布,每个区域保持和移动的概率相等。

机器人在移动一步之后,位于垃圾桶#2 的概率是多少?
即使我们最初从一个完全确定的位置开始(100%),运动中的固有随机性也会逐渐加入噪声,导致我们随着时间推移趋向于均匀分布。我们需要添加信息!
3. 感知函数: 结合使用贝叶斯定理,通过测量来增加信息。
After each movement:
For each region:
Region probability at time t+1 given measurement at time t+1 =
Likelihood of the measurement given the robot is in that region
x Region probability at time t+1 after movement
x normlization to ensure that all the probabilities sum to 1.
传感器的可靠性通过概率表示,因为它们不是 100%准确的。下面的方程展示了当传感器检测到橙色时,机器人位于橙色垃圾桶的概率为 90%,而传感器错误的概率为 10%,即机器人实际上位于蓝色垃圾桶中。

感知函数:给定机器人在一个橙色垃圾桶内,机器人感知到“橙色”的概率是多少?
下述计算展示了,与运动相比,传感器提供了信息并改进了我们对机器人位置的理解。例如,因为第 2 号箱子不是橙色的,机器人在其中的概率从 0.1 降低到 0.02。

给定机器人感知到“橙色”,机器人在第二个箱子里的概率是多少?
下图显示了在结合了运动和传感器数据后更新的位置假设。

多亏了在一步之后的感知函数,我们对机器人所在位置有了更多的确定性。我们需要更多的步骤来增加在橙色箱子中的确定性。
最后思考
机器人在哪里?我们可以通过使用递归贝叶斯滤波器,不断细化对这个问题的回答,从一个均匀分布开始,保持所有猜测的概率相等,直到收敛到最可能的答案。
贝叶斯滤波器帮助我们衡量对机器人位置的信心,通过将(有噪声的)传感器数据与先前的信息(即运动后的机器人预估位置)结合来更新这一信念。
来源:
-
这些是我从推荐度很高的 edX 课程“自驾车的贝叶斯算法”的第一次讲座中整理的总结笔记,讲师是 Roi Yozevitch 博士。
-
概率机器人学 GitBook: 非参数滤波器
-
Daniel Sabinasz 的个人 直方图滤波器博客
-
从 latex 到 png 的在线转换器。
-
图片:机器人头像是由 Dall-E 创建的。本文中使用的其他所有图片均由作者创建。
如何在卫星数据上微调预训练的视觉变换器
PyTorch Lightning 中的逐步教程
·发表于Towards Data Science ·6 分钟阅读·2024 年 3 月 21 日
--

图像由作者使用Midjourney创建。
Vision Transformer是一个强大的图像分类 AI 模型。它于 2020 年发布,将高效的变换器架构引入了计算机视觉领域。
在预训练阶段,AI 模型会摄取大量数据并学习常见的模式。视觉变换器是在ImageNet-21K数据集上进行预训练的,该数据集包含 1400 万张图像和 21,000 个类别。
卫星图像不包含在 ImageNet-21K 数据集中,如果直接应用,视觉变换器的性能会很差。
在这里,我将向您展示如何在来自EuroSat 数据集的 27,000 张卫星图像上微调一个预训练的视觉变换器。我们将预测土地覆盖类型,如森林、农田和工业区。

来自EuroSAT RGB 数据集的示例图像。根据欧盟法律,哨兵数据是免费的并且对公众开放。
我们将在PyTorch Lightning中进行工作,这是一个基于 PyTorch 的深度学习库。Lightning 减少了需要编写的代码量,让我们可以专注于建模。
所有代码均可在GitHub上找到。
设置项目
预训练的 Vision Transformer 可以在 Huggingface 上找到。可以从 GitHub 安装模型架构和权重。我们还需要安装 PyTorch Lightning。我在本教程中使用了版本 2.2.1,但任何版本> 2.0 应该都可以使用。
pip install -q git+https://github.com/huggingface/transformers
pip install lightning=2.2.1
我们可以将我们的项目分为四个步骤,下面我们将详细介绍:
-
预训练的 Vision Transformer:Lightning 模块
-
EuroSAT 数据集
-
在 EuroSAT 数据集上训练 Vision Transformer
-
计算测试集上的准确率
将 Vision Transformer 适应我们的数据集
来自 Huggingface 的 Vision Transformer 已针对具有 1,000 个类别的 ImageNet 子集进行了优化。
我们的数据集只有 10 个类别,分别代表不同类型的土地覆盖。因此,我们需要修改 Vision Transformer 的输出部分,换成一个具有正确类别数量的新分类头。

Vision Transformer 架构。由作者根据原始论文修改。 [arxiv]
从 Huggingface 实例化预训练模型的代码使这变得简单。我们只需通过num_labels指定新的类别数量,并告诉模型忽略我们更改输出大小的事实。
from transformers import ViTForImageClassification
ViTForImageClassification.from_pretrained("google/vit-base-patch16-224",
num_labels=10,
ignore_mismatched_sizes=True
)
模型提醒我们,现在需要重新训练:
ViTForImageClassification 的一些权重未从 google/vit-base-patch16–224 模型检查点初始化,而是因为形状不匹配而重新初始化:
…
你可能需要在下游任务上训练这个模型,以便能够用于预测和推理。
我们可以选择不同版本的 Vision Transformer,在这里我们使用的是vit-base-patch16–224,这是最小的模型,它使用从大小为224 x 224像素的图像中提取的16 x 16块。该模型有 8580 万个参数,需要 340 MB 的内存。
将 Vision Transformer 作为 Lightning 模块
在 PyTorch Lightning 中,深度学习模型被定义为一个Lightning 模块。我们只需要指定
-
模型设置:加载预训练的 Vision Transformer
-
前向步骤:将模型应用于一批数据
-
训练、验证和测试步骤
-
训练中使用的优化器
训练步骤必须返回损失,这里是交叉熵损失,用于量化预测类别与真实类别之间的差异。
日志记录非常方便。通过调用self.log,我们可以直接将训练和评估的指标记录到我们首选的日志记录器中——在我的情况下是TensorBoard。在这里,我们记录了训练损失和验证准确率。
请注意,为了访问 Huggingface 的 Vision Transformer 做出的预测,我们需要从模型输出中提取它们,作为predictions.logits。
EuroSAT 数据集的 Lightning DataModule
你可以从Zenodo下载 EuroSAT 数据集。确保选择 RGB 版本,它已经从原始卫星图像转换过来。我们将在LightningDataModule中定义数据集。
设置阶段使用了torchvision的变换函数。为了符合 Vision Transformer 期望的输入,我们需要将卫星图像放大到 224 x 224 像素,将图像转换为 torch 数据类型并进行标准化。
我们将数据集分割为 70%用于训练(微调),10%用于验证,20%用于测试。通过在类标签上进行分层抽样,我们确保三个子集中的类分布相等。
train_dataloader等函数对于在后续的运行脚本中设置数据加载器非常方便。
将所有内容整合起来:运行脚本
现在我们已经有了数据集和模型的构建块,可以编写执行微调的运行脚本了。
为了清晰起见,我为数据集(eurosat_module)和模型(vision_transformer)创建了单独的模块,需要在脚本中导入。
训练模型
Lightning Trainer接收一个模型和用于训练及验证的数据加载器。它提供了多种可以自定义的标志—在这里我们仅使用了其中的三个:
-
devices,用于仅使用一块 GPU 进行训练
-
early stopping callback,如果验证损失在六个 epoch 内没有改善,则停止训练
-
logger,用于将训练过程记录到 TensorBoard
PyTorch Lightning 的优点是训练现在只需一行代码:
trainer.fit(model=model, train_dataloaders=train_dataloader, val_dataloaders=valid_dataloader)
在幕后,训练器使用反向传播和 Adam 优化器来更新模型权重。当验证准确率在指定的 epoch 数量内没有提高时,训练停止。
实际上,在 EuroSAT 数据集上的微调在几个 epoch 内就完成了。面板展示了训练损失,这是由 TensorBoard 记录的。经过两个 epoch 后,模型已经达到了 98.3%的验证准确率。

作者创建的 TensorBoard 快照。
在测试集上进行评估
我们的数据集很小,微调速度很快,所以我们不需要单独存储训练好的模型,可以直接将其应用于测试集。
只需一行代码,我们就能计算测试集上的准确率:
trainer.test(model=model, dataloaders=test_dataloader, verbose=True)
只进行了一次微调,我就达到了 98.4%的测试集准确率,这意味着几乎所有的卫星图像的地表类型都被正确分类了。
即使样本较少,我们也能获得很高的准确性。面板展示了在微调过程中只看到一次的不同训练样本数量下的测试集准确性。仅使用 320 张卫星图像,每类平均 32 张,测试集准确性已经达到 80%。

图片由作者创建。
关键要点
预训练模型是减少训练时间的一个好方法。它们已经在一般任务上表现得很好,只需要适应你的特定数据集。
在现实应用中,数据通常稀缺。EuroSAT 数据集仅包含 27,000 张图像,大约是 ImageNet-21K 数据量的 0.5%。预训练利用了更大的数据集,我们可以高效地使用特定应用数据集进行微调。
Lightning 非常适合训练深度学习模型,而无需担心所有技术细节。LightningModule 和 Trainer API 提供了方便的抽象,并且具有高性能。
如果你想在 Huggingface 生态系统内微调视觉变换器,我推荐这个教程。
完整代码,包括允许你添加自己数据集的配置文件,已在 GitHub 上发布:
[## GitHub - crlna16/pretrained-vision-transformer: 使用 PyTorch 的预训练视觉变换器…
使用 PyTorch Lightning 的预训练视觉变换器 - crlna16/pretrained-vision-transformer
参考文献
-
EuroSAT 数据集:10.5281/zenodo.7711096(Copernicus Sentinel 数据许可)
-
Dosovitskiy 等人,图像值 16x16 个单词:用于大规模图像识别的变换器,
arxiv.org/abs/2010.11929(2020 年) -
视觉变换器在Huggingface和GitHub上的资料(Apache 2.0)
如何预测层次化时间序列
初学者的销售预测对账指南
·发表于Towards Data Science ·阅读时间 12 分钟·2024 年 8 月 20 日
--

图片由Krista Mangulsone提供,来源于Unsplash
想象一下,你是一家迷人的小宠物店的数据科学家,这家店专注于五种产品:两种猫粮和三种狗粮。你的任务是什么?通过准确预测每种产品的每周销售量,帮助这家小生意蓬勃发展。目标是提供一份全面的销售预测——包括总销售量,以及猫粮和狗粮的详细预测,甚至是每种产品的销售预测。
数据
你拥有不同种类的猫粮 A 和 B,以及不同种类的狗粮 C、D 和 E 在 200 天内的销售数据。幸运的是,这些数据非常干净,没有缺失值,也没有异常值。此外,数据中没有趋势。看起来是这样的:

图片来自作者。
注意:我自己生成了这些数据。
除了单独的销售数据,我们还有所有猫粮产品、所有狗粮产品和所有产品的汇总销售数据。我们将这样的时间序列集合称为层次化时间序列。在我们的案例中,它们遵循以下销售层级结构:
如何使用任何监督学习模型预测时间序列数据
将时间序列数据特征化为标准表格格式,以便应用经典的机器学习模型,并通过使用 AutoML 提高准确性
·发布于Towards Data Science ·10 分钟阅读·2024 年 2 月 22 日
--

本文深入探讨了通过使用开源库将时间序列数据集转换为表格格式,提升每日能耗水平预测过程的效率。我们探索了流行的多类别分类模型的应用,并利用 Cleanlab Studio 与 AutoML 大幅提高了我们的样本外准确性。
本文的关键要点是,我们可以通过将时间序列数据转换为表格结构,利用更通用的方法来建模该数据集,甚至在预测时间序列数据时找到提升的空间。
拍摄快照
总体而言,我们将会:
-
通过将 Prophet 预测模型拟合到我们的时间序列数据,建立一个基准准确度
-
通过使用开源特征化库将我们的时间序列数据转换为表格格式,并展示如何通过标准的多类别分类(梯度提升)方法比我们的 Prophet 模型减少 67%的预测误差(在样本外准确度上增加 38%的原始百分比点)。
-
使用 AutoML 解决方案进行多类别分类,相比我们的梯度提升模型,减少了 42%的预测误差(在样本外准确度上增加 8%的原始百分比点),相比我们的 Prophet 预测模型,减少了 81%的预测误差(在样本外准确度上增加 46%的原始百分比点)。
要运行本文中演示的代码,请参阅完整的笔记本。
检查数据
你可以在这里下载数据集。
数据代表 PJM 每小时的能源消耗(以兆瓦为单位)。PJM Interconnection LLC(PJM)是美国的一个区域传输组织(RTO)。它是东部电力互联系统的一部分,运营着一个电力传输系统,服务于多个州。
让我们来看一下我们的数据集。数据包括一个日期时间列(object 类型)和一个我们试图预测的 Megawatt 能源消耗列(float64 类型),作为离散变量(对应于每小时能源消耗水平的四分位数)。我们的目标是训练一个时间序列预测模型,能够预测明天的每日能源消耗水平,属于以下四个等级之一:低,低于平均,高于平均 或 高(这些等级是基于整体每日消耗分布的四分位数确定的)。我们首先演示如何将时间序列预测方法(如 Prophet)应用于这个问题,但这些方法限制于某些适合时间序列数据的机器学习模型。接下来,我们演示如何将这个问题重新构造为一个标准的多类别分类问题,这样我们就可以应用任何机器学习模型,并展示如何通过使用强大的监督学习获得更优的预测结果。
我们首先将这些数据转换为每日平均能源消耗,并将列重命名为 Prophet 预测模型所期望的格式。这些实数值的每日能源消耗水平被转换为四分位数,这是我们试图预测的值。我们的训练数据如下所示,并标明了每个每日能源消耗水平所属的四分位数。四分位数是使用训练数据计算的,以防止数据泄漏。
现在,我们展示下面的训练数据,这是我们用来拟合预测模型的数据。

包含每日能源消耗水平四分位数的训练数据
然后我们展示下面的测试数据,这是我们用来评估预测结果的数据。

包含每日能源消耗水平四分位数的测试数据
训练和评估 Prophet 预测模型
如上图所示,我们将使用 2015-04-09 作为训练数据的结束日期,并从 2015-04-10 开始我们的测试数据。我们仅使用训练数据计算每日能源消耗的四分位数阈值。这避免了数据泄漏——即使用仅在未来才会获得的样本外数据。
接下来,我们将预测测试数据期间每日 PJME 能源消耗水平(以 MW 为单位),并将预测值表示为离散变量。该变量表示每日能源消耗水平属于哪个四分位数,分类表示为 1(低)、2(低于平均)、3(高于平均)或 4(高)。为了评估,我们将使用scikit-learn中的accuracy_score函数来评估模型的性能。由于我们是以这种方式构建问题,我们能够使用分类准确率来评估模型对次日预测的表现(并比较未来的模型)。
import numpy as np
from prophet import Prophet
from sklearn.metrics import accuracy_score
# Initialize model and train it on training data
model = Prophet()
model.fit(train_df)
# Create a dataframe for future predictions covering the test period
future = model.make_future_dataframe(periods=len(test_df), freq='D')
forecast = model.predict(future)
# Categorize forecasted daily values into quartiles based on the thresholds
forecast['quartile'] = pd.cut(forecast['yhat'], bins = [-np.inf] + list(quartiles) + [np.inf], labels=[1, 2, 3, 4])
# Extract the forecasted quartiles for the test period
forecasted_quartiles = forecast.iloc[-len(test_df):]['quartile'].astype(int)
# Categorize actual daily values in the test set into quartiles
test_df['quartile'] = pd.cut(test_df['y'], bins=[-np.inf] + list(quartiles) + [np.inf], labels=[1, 2, 3, 4])
actual_test_quartiles = test_df['quartile'].astype(int)
# Calculate the evaluation metrics
accuracy = accuracy_score(actual_test_quartiles, forecasted_quartiles)
# Print the evaluation metrics
print(f'Accuracy: {accuracy:.4f}')
>>> 0.4249
样本外准确度相当低,仅为 43%。通过这种方式建模我们的时间序列数据,我们仅能使用时间序列预测模型(这是可能的机器学习模型的一个有限子集)。在下一节中,我们将探讨如何通过适当的特征化,将时间序列转换为标准的表格数据集,从而更灵活地建模这些数据。一旦时间序列被转换为标准的表格数据集,我们就能够使用任何监督学习模型来预测每日的能源消耗数据。
通过特征化将时间序列数据转换为表格数据
现在,我们将时间序列数据转换为表格格式,并使用开源库sktime、tsfresh和tsfel对数据进行特征化。通过使用这些库,我们可以提取出广泛的特征,捕捉时间序列数据的潜在模式和特征。这些特征包括统计特征、时间特征,以及可能的频谱特征,它们提供了数据随时间变化的全面快照。通过将时间序列分解为单独的特征,变得更加容易理解数据的不同方面如何影响目标变量。
TSFreshFeatureExtractor是sktime库中的一个特征提取工具,它利用tsfresh的功能从时间序列数据中提取相关特征。tsfresh旨在自动计算大量的时间序列特征,这对于理解复杂的时间动态非常有用。在我们的应用场景中,我们使用TSFreshFeatureExtractor提取最小且必要的特征集来对数据进行特征化。
tsfel(时间序列特征提取库)提供了一套全面的工具,用于从时间序列数据中提取特征。我们利用一个预定义的配置,可以从能源消耗的时间序列数据中构建一系列丰富的特征(例如,统计特征、时间特征、频谱特征),捕捉到可能与分类任务相关的各种特征。
import tsfel
from sktime.transformations.panel.tsfresh import TSFreshFeatureExtractor
# Define tsfresh feature extractor
tsfresh_trafo = TSFreshFeatureExtractor(default_fc_parameters="minimal")
# Transform the training data using the feature extractor
X_train_transformed = tsfresh_trafo.fit_transform(X_train)
# Transform the test data using the same feature extractor
X_test_transformed = tsfresh_trafo.transform(X_test)
# Retrieves a pre-defined feature configuration file to extract all available features
cfg = tsfel.get_features_by_domain()
# Function to compute tsfel features per day
def compute_features(group):
# TSFEL expects a DataFrame with the data in columns, so we transpose the input group
features = tsfel.time_series_features_extractor(cfg, group, fs=1, verbose=0)
return features
# Group by the 'day' level of the index and apply the feature computation
train_features_per_day = X_train.groupby(level='Date').apply(compute_features).reset_index(drop=True)
test_features_per_day = X_test.groupby(level='Date').apply(compute_features).reset_index(drop=True)
# Combine each featurization into a set of combined features for our train/test data
train_combined_df = pd.concat([X_train_transformed, train_features_per_day], axis=1)
test_combined_df = pd.concat([X_test_transformed, test_features_per_day], axis=1)
接下来,我们通过删除与目标变量——平均每日能耗水平——有较高相关性(大于 0.8)和那些与目标变量无关的特征来清理数据集。高相关性的特征可能导致过拟合,使得模型在训练数据上表现良好,但在未见数据上表现不佳。另一方面,无相关性的特征没有任何价值,因为它们与目标变量缺乏可定义的关系。
通过排除这些特征,我们旨在提高模型的泛化能力,并确保我们的预测是基于平衡且有意义的数据输入集。
# Filter out features that are highly correlated with our target variable
column_of_interest = "PJME_MW__mean"
train_corr_matrix = train_combined_df.corr()
train_corr_with_interest = train_corr_matrix[column_of_interest]
null_corrs = pd.Series(train_corr_with_interest.isnull())
false_features = null_corrs[null_corrs].index.tolist()
columns_to_exclude = list(set(train_corr_with_interest[abs(train_corr_with_interest) > 0.8].index.tolist() + false_features))
columns_to_exclude.remove(column_of_interest)
# Filtered DataFrame excluding columns with high correlation to the column of interest
X_train_transformed = train_combined_df.drop(columns=columns_to_exclude)
X_test_transformed = test_combined_df.drop(columns=columns_to_exclude)
如果我们查看现在的训练数据的前几行,这是它的样子。我们现在有 73 个特征,这些特征是通过我们使用的时间序列特征化库添加的。我们将基于这些特征预测的标签是第二天的能耗水平。

新特征化后的前 5 行训练数据,采用表格格式
需要注意的是,我们使用了一项最佳实践,即分别对训练数据和测试数据应用特征化过程,以避免数据泄漏(并且保留的测试数据是我们最新的观察数据)。
此外,我们计算了我们的离散四分位数值(使用我们最初定义的四分位数),并使用以下代码来获得我们的训练/测试能耗标签,这就是我们的 y_labels。
# Define a function to classify each value into a quartile
def classify_into_quartile(value):
if value < quartiles[0]:
return 1
elif value < quartiles[1]:
return 2
elif value < quartiles[2]:
return 3
else:
return 4
y_train = X_train_transformed["PJME_MW__mean"].rename("daily_energy_level")
X_train_transformed.drop("PJME_MW__mean", inplace=True, axis=1)
y_test = X_test_transformed["PJME_MW__mean"].rename("daily_energy_level")
X_test_transformed.drop("PJME_MW__mean", inplace=True, axis=1)
energy_levels_train = y_train.apply(classify_into_quartile)
energy_levels_test = y_test.apply(classify_into_quartile)
在特征化的表格数据上训练和评估 GradientBoostingClassifier 模型
使用我们特征化的表格数据集,我们可以应用任何监督式机器学习模型来预测未来的能耗水平。这里我们将使用 Gradient Boosting Classifier (GBC) 模型,它是大多数数据科学家处理表格数据时的首选武器。
我们的 GBC 模型是从sklearn.ensemble模块实例化的,并配置了特定的超参数,以优化其性能并避免过拟合。
from sklearn.ensemble import GradientBoostingClassifier
gbc = GradientBoostingClassifier(
n_estimators=150,
learning_rate=0.1,
max_depth=4,
min_samples_leaf=20,
max_features='sqrt',
subsample=0.8,
random_state=42
)
gbc.fit(X_train_transformed, energy_levels_train)
y_pred_gbc = gbc.predict(X_test_transformed)
gbc_accuracy = accuracy_score(energy_levels_test, y_pred_gbc)
print(f'Accuracy: {gbc_accuracy:.4f}')
>>> 0.8075
81%的样本外准确度明显优于我们之前的 Prophet 模型结果。
使用 AutoML 简化操作
现在我们已经了解了如何特征化时间序列问题以及应用强大机器学习模型如 Gradient Boosting 的好处,一个自然的问题浮现出来:我们应该应用哪种监督式机器学习模型?当然,我们可以尝试多种模型,调整它们的超参数,并将它们进行集成。一个更简单的解决方案是让 AutoML 来处理这一切。
这里我们将使用 Cleanlab Studio 提供的一个简单 AutoML 解决方案,该方案不需要任何配置。我们只需提供我们的表格数据集,平台会自动训练多种类型的监督式机器学习模型(包括 Gradient Boosting 等),调整它们的超参数,并确定哪些模型最适合合并成一个单一的预测器。以下是训练和部署 AutoML 监督分类器所需的所有代码:
from cleanlab_studio import Studio
studio = Studio()
studio.create_project(
dataset_id=energy_forecasting_dataset,
project_name="ENERGY-LEVEL-FORECASTING",
modality="tabular",
task_type="multi-class",
model_type="regular",
label_column="daily_energy_level",
)
model = studio.get_model(energy_forecasting_model)
y_pred_automl = model.predict(test_data, return_pred_proba=True)
以下是我们在 AutoML 平台上看到的模型评估结果,展示了所有自动拟合和评估的不同类型的机器学习模型(包括多个梯度提升模型),以及通过最佳组合它们的预测结果构建的集成预测器。

不同类型模型的 AutoML 结果
在对我们的测试数据进行推理,获得次日能源消耗预测后,我们发现测试准确度为 89%,相比我们之前的梯度提升方法,原始百分比提高了 8 个百分点。

我们在日常能源消耗数据上的 AutoML 测试准确度
结论
对于我们的 PJM 日常能源消耗数据,我们发现将数据转换为表格格式并进行特征化处理,实现了67%的预测误差减少(相比我们用 Prophet 预测模型建立的基准准确度,样本外准确度提高了 38%)。
我们还尝试了一个简单的 AutoML 方法进行多类别分类,该方法使预测误差减少了 42%(相比我们的梯度提升模型,样本外准确度提高了 8%),并且使预测误差减少了 81%(相比我们的 Prophet 预测模型,样本外准确度提高了 46%)。
通过采用如上所示的方法来建模时间序列数据集,超越仅考虑预测方法的局限性,我们可以应用更通用的监督学习技术,并在某些类型的预测问题上取得更好的结果。
除非另有说明,所有图片均由作者提供。
如何从任何文档中生成 LLM 微调的指令数据集
使用轻量级库经济高效地生成高质量的合成数据集
·发表于 Towards Data Science ·阅读时间 7 分钟·2024 年 3 月 6 日
--
大语言模型(LLM)是功能强大且通用的工具,但它们通常缺乏特定领域的知识,而这些知识常常储存在企业的仓库中。
使用自己的数据进行定制化 LLM(大语言模型)微调可以弥补这一差距,而数据准备是这一过程的第一步。这也是一个至关重要的步骤,能够显著影响微调模型的表现。
然而,手动创建数据集可能是昂贵且耗时的。另一种方法是利用 LLM 生成合成数据集,通常使用高性能模型如 GPT-4,但这可能会非常昂贵。
在本文中,我旨在向您介绍一种成本效益高的替代方案,用于自动化从各种文档中创建指令数据集。这个解决方案涉及使用一个名为 Bonito 的轻量级开源库。

图像由作者使用 Bing 聊天生成,Bing 聊天由 DALL.E 3 驱动
开始使用 Bonito,开源解决方案
理解指令
如何生成用于目标检测任务的合成图像
使用 Blender、Python 和 3D 资源的逐步教程
·发表于 Towards Data Science ·10 分钟阅读·2024 年 3 月 8 日
--

图像由作者创建
数据不足是当今深度学习中的最大问题之一。
计算机视觉任务的一个有前景的解决方案是自动生成带有标注的合成图像。
在本文中,我将首先概述一些用于合成图像数据的图像生成技术。
然后,我们生成一个不需要手动标注的训练数据集,并用它来训练一个 Faster R-CNN 目标检测模型。
最后,我们在真实图像上测试我们训练的模型。
图像生成技术
从理论上讲,合成图像是完美的。你可以在不需要手动标注的情况下生成几乎无限数量的图像。
使用真实图像和手动标注的训练数据集可能包含大量人为标注错误,并且它们通常是有偏的数据集(例如,汽车图像大多是从侧面/正面拍摄的,并且是在道路上)。
然而,合成图像存在一个被称为模拟到真实领域差距的问题。
如何使用 Open-Sora-Plan 视频生成模型生成视频
学习如何使用最新的图像生成模型之一生成图像。
·发表于Towards Data Science ·阅读时间 10 分钟·2024 年 4 月 15 日
--
在本文中,你将学习如何使用Open-Sora-Plan 视频生成模型[1],这是一个强大的模型,允许你像在OpenAI Sora中看到的那样,创建你自己的视频。我将讨论可以应用该模型的不同任务,例如生成动画视频和创建合成数据集。然后,我会分享对该模型性能的看法,并讨论它的优点和缺点。最终,你应该能更清楚地了解从这个模型中能获得的实际效用。

Open-Sora-Plan 根据提示“写一篇文章”生成了图像。模型似乎理解了该提示,因为它展示了纸上的手写字迹,但图像质量较低。图像由作者使用 Open-Sora-Plan 模型制作。
动机
我写这篇文章的动机是,作为数据科学家,重要的一部分工作就是跟上机器学习领域内最新的模型。紧跟人工智能的不断进展需要时间和精力。因此,我在一系列文章中提到了一些我在PapersWithCode、GitHub和HuggingFace等页面上发现的有前景的模型。在发现一个模型有趣之后,我会亲自测试它,看看它是否能帮助我正在进行的工作或个人项目。如果该模型可能有益,我就会考虑将其应用到我需要的任务中。
如何获得数据科学研究生项目/实习
我给大学和学院学生的关于如何进入数据科学领域的建议
·发布于 Towards Data Science ·阅读时间:7 分钟·2024 年 8 月 20 日
--

图片来源:Mikael Kristenson 于Unsplash
大学毕业后,我花了几个月时间和超过 300 份申请才拿到我的第一个数据科学研究生项目。
回想起来,有很多我本可以做得更好的事情,接下来我将与大家分享这些经验,帮助你增加获得数据科学实习或研究生项目的机会。
让我们开始吧!
👉 你也可以在 YouTube 上观看这篇文章的视频版本!
学习一些数据科学
在申请实习或研究生项目之前,你应该学习基本的数据科学和机器学习知识。
我不是说你需要成为强化学习方面的高手,或者稳定扩散的专家,但了解数据科学的基本内容会帮助你申请职位时脱颖而出。它还会帮助你确认,数据科学是你长远规划中的职业。
如何从 LLM 获取 JSON 输出:实用指南
使用 Llama.cpp 或 Gemini API 强制 JSON 输出的教程
·发表于 Towards Data Science ·6 分钟阅读·2024 年 8 月 16 日
--

图片来自 Etienne Girardet 由 Unsplash 提供
大型语言模型(LLMs)擅长生成文本,但获得像 JSON 这样的结构化输出通常需要巧妙的提示,并希望 LLM 能理解。幸运的是,JSON 模式在 LLM 框架和服务中变得越来越普遍。这使你可以定义你想要的确切输出模式。
本文讨论了使用 JSON 模式进行约束生成。我们将使用一个复杂的、嵌套的、真实的 JSON 模式示例来引导 LLM 框架/APIs(如 Llama.cpp 或 Gemini API)生成结构化数据,特别是旅游地点信息。这建立在之前关于使用 Guidance 进行约束生成的帖子基础上,但重点关注更广泛采用的 JSON 模式。
使用 Guidance 使 LLM 输出符合你的期望
[towardsdatascience.com
尽管比 Guidance 更有限,但 JSON 模式的更广泛支持使其更加易于访问,尤其是在基于云的 LLM 提供商中。
在个人项目中,我发现虽然使用 Llama.cpp 时 JSON 模式非常直接,但要使其…
如何在数据科学领域晋升
帮助我获得第一次晋升的建议和技巧
·发表于Towards Data Science ·阅读时间 8 分钟·2024 年 5 月 16 日
--

照片来自Cytonn Photography于Unsplash
现在,不吹牛,年初的时候,我获得了晋升!!!!
我从一个初级数据科学家成长为现在的中级数据科学家。我在 2021 年 9 月开始作为数据科学家工作,所以大约花了 2 年半的时间才获得晋升。
在这篇文章中,我想回顾一些帮助我晋升的技巧和建议,帮助你比我更快地达到下一个级别!
我的成长历程
我想简要总结一下我在数据科学领域的成长历程,以便为我的晋升提供一些背景。
所以,从 2017 年 9 月到 2021 年 7 月,我学习并获得了物理学硕士学位。在最后一年,我意识到物理学研究并不适合我,于是我决定在观看了AlphaGO纪录片后转行做数据科学家(这部纪录片很棒,如果你还没看过,强烈推荐!)。
如何使用 GitHub API 获取拉取请求数据
获取任意两次提交之间的差异
·发表于 Towards Data Science ·阅读时间 5 分钟·2024 年 9 月 29 日
--

照片由 Bofu Shaw 提供,来自 Unsplash
GitHub 就像是代码的维基百科。GitHub 上的所有内容不能轻易假定,但它包含了如何创建一些最佳软件工具的精华和历史。
如果没有一个用于访问如此宝贵资源的 API,那将是非常遗憾的。幸运的是,我们确实有一个,令人惊讶的是,它叫做 GitHub API。
让我首先说明一下本文不讨论的内容。我们不会讨论 git 注释或如何在软件开发中使用 git。
本文更多地是关于如何使用 GitHub API 进行分析。分析的首要要求是数据,而 GitHub 拥有大量数据。
我们可以通过 GitHub API 获取的信息量和种类简直令人惊叹。此外,它是一个维护良好且有详细文档的 API,所以我们不会遇到太多困难来获取所需的信息。
我们可以通过 GitHub API 获取很多数据,例如:
-
每个拉取请求的提交数量
-
仓库的文件夹和文件结构
-
每次提交编辑的文件平均数量
-
基于开发者的数据,例如谁在上个月推送了最多的提交
-
基于文件的数据,例如…
如何开始你的数据科学职业之旅
初学者在选择数据科学及 AI/ML 提升资源时需要考虑的六个要点
·发布于Towards Data Science ·6 分钟阅读·2024 年 10 月 20 日
--

图片由Zach Graves提供,来自Unsplash
介绍

图片由Jonathan Kemper提供,来自Unsplash
显而易见,数据科学在过去十年中已经发展成为市场上最受追捧的技能之一。传统公司、科技公司、咨询公司、初创企业——不管是哪个行业——都在不断招聘数据科学专业人士。高需求和相对较少的经验丰富的专家,使得这一领域成为一个非常有利可图的职业机会。要进入并在这个领域取得成功,你不仅需要深入理解现有的算法和包,还需要培养直觉,理解哪些方法适合哪些使用场景。此外,你还需要学习如何将现实世界的问题转化为数据科学框架。在这篇文章中,我将讨论初学者如何构建对这一领域的基础性和深入理解,以便开启数据科学职业之旅。
从哪里开始?

图片由marianne bos提供,来自Unsplash
鉴于有大量的学习资源可供选择,挑选学习方式可能会让人感到困惑。虽然最终选择会取决于你的个人情况和目标,但在选择学习资源时,你可能需要考虑以下几个标准:
· 内容:通常,一个全面的资源将涵盖监督学习(例如线性回归、逻辑回归、决策树、集成方法)、无监督学习(例如聚类、PCA),以及统计学和概率学的基础知识。许多课程还专门讲解高级方法,如深度学习、计算机视觉、自然语言处理和生成性 AI。有些课程甚至提供免费的课程内容预览、学习视频和编程项目,帮助学习者做出决定。虽然很难找到包含每个主题细节的课程,但你转向数据科学领域的目的应该决定了课程的选择。
· 受众:“谁会从这门课程中受益?”是确定一个课程的重要问题。如果你是初学者,那么中级课程可能会成为学习的障碍。相反,对于已经有一定分析方法背景的学习者来说,过于基础的内容可能会显得不太有用。
· 时间投入:根据你的个人情况和偏好,这一考虑因素可能会缩小你可以选择的选项。例如,如果你是一个在职人士,你可能无法每周花费超过几个小时的时间。许多在线课程提供灵活的时间安排,预计每周需花费 5 到 7 小时的时间,旨在帮助兼职学习者。课程的总体时长也决定了技能发展的程度。虽然大学中的高级课程(例如数据科学硕士课程)可能会在 18 到 24 个月内帮助你深入发展专业知识,并且可能每周需要几个小时的专注努力,但持续 4 到 6 周的短期课程可能不足以帮助你获得启动新职业所需的技能。
· 学习者体验:你可以在课程页面或其他在线平台上查看用户评价。这些评价有助于了解课程内容和教学质量,尤其是当评价数量较多且有评论时。此外,课程的受欢迎程度也可以通过目前注册的学习者数量和已经完成课程的学习者数量来衡量。有人可能会认为,验证评论的真实性和学习者数量可能存在挑战,但这些评价可以作为一种三角验证点,结合其他信息,从整体上评估一个课程。
· 费用:课程费用可能是选择时的重要因素。课程费用可以从免费的到数千美元不等。
· 职业资源:许多项目提供支持,帮助进行新的求职和职业转换,包括面试准备、简历审查以及与行业领域专家建立网络。虽然这可能不是选择一个项目时的首要考虑因素,但这些资源可以在你开始职业转型之旅时帮助你迈出第一步。
推荐的学习机制

图片由Shubham Sharan提供,来自Unsplash
虽然有很多在线资源可以帮助你获取特定主题的知识,但我建议初学者考虑一个结构化的学习路径,这样可以全面了解数据科学领域。根据我的经验,我建议至少最初选择三种类型的项目(你可以随时通过额外的材料来补充对单个主题的学习):
· 专业化或职业证书
· 微硕士/纳米学位
· 训练营
通常,这些项目会持续几个月。我发现这些项目在单一/短期课程可能不足以培养所需的数据科学敏锐度和学制较长的正规教育之间提供了平衡。教育科技公司通常与知名大学合作,提供这些项目。通过选择标准的并排比较有助于决定前进的路径——图 1 展示了专业化和职业证书的比较示例。这里列出的课程仅是在线项目多样性的一个小样本,并不代表全部。此外,每个项目的细节来自某个特定时点,可能会有所变化。

图 1. 选择的 AI/ML 专业化和职业认证的比较。图片由作者制作。*数据取自某一时点,可能会有所变动。项目价格可能根据促销和计划而有所不同。
虽然建议选择并从一个项目开始,但有时可能需要通过多个课程才能对该领域感到更为熟悉。在我个人的案例中,我首先选择了 Andrew Ng 的《机器学习》课程,它详细讲解了基础知识,但由于当时课程中有 Matlab/Octave 的练习,我决定再参加 IBM 的另一门课程,一边学习 Python,一边学习人工智能/机器学习。根据我的个人学习偏好,我认为需要一个更正式的教学机制,于是我选择了得克萨斯大学奥斯汀分校与Great Learning合作的 AI/ML 研究生项目。这个项目为我提供了更多的结构化学习,因为我需要通过视频模块学习理论,每周投入 5 到 7 小时,参加每周的测验,参与每个周末由导师主导的在线小组互动课程,以及定期提交编程作业。虽然理论学习和测验帮助我理解了概念,但实际的代码开发练习才是我内化学习的关键。这项练习让我深入了解如何设置问题、清洗和分析原始数据、确定应用哪种算法、完善解决方案并解释结果。
总结…

摄影:来自 Kelly Sikkema 于 Unsplash
在开始技能提升之旅时,做出明智的决策至关重要。作为初学者,考虑一个更有结构的学习路径可能是明智之选,等你对基础知识有了基本了解后,再深入学习具体领域。在开始时花时间研究和对比不同的学习项目,也能帮助学习者了解数据科学领域中的关键和热门话题。
感谢阅读。希望你觉得这篇文章有用。如果有任何意见,欢迎随时发送至 rkumar5680@gmail.com。让我们在LinkedIn上建立联系。
如何在不感到停滞的情况下发展职业生涯
晋升手册
无论你是刚刚起步,还是渴望再跳跃一层,都要确保你已经准备好,并且让你的老板知道这一点。
·发布于Towards Data Science ·11 分钟阅读·2024 年 4 月 17 日
--

图片由作者使用Imagen生成
为什么约翰经验年限较少,却比马克更快获得晋升,而马克在公司待的时间比约翰还要久,甚至早在约翰上大学之前就开始工作,而且负担了很多艰巨的任务?为什么莎拉总是得到最有前景的项目,而本提出了那么多想法,却仍然被冷落在一旁?
我曾经也有过类似的疑问,有时,我会觉得某些人虽然不配,却在职业生涯中走得比我更远,而我则在尽全力工作,却没有得到应有的关注。
我花了一些时间才意识到,职业生涯的发展从来不会是直线的,除非在极少数情况下。虽然在最简单、最公平的世界里,做最多工作的人理应得到晋升,但在现实生活中,这种情况很少发生。
把你的职业生涯看作一个产品。就像任何产品一样,它需要做到优秀。你必须在合适的时间将它卖给合适的人,并做好市场营销。如果某个环节有所欠缺,晋升到下一个层次可能会变得非常困难。越早掌握晋升所需的要素,越容易成功。
如何处理机器学习项目中的不平衡数据集
处理不平衡数据集的技术、示例及 Python 代码片段
·发表于 Towards Data Science ·阅读时长 9 分钟·2024 年 10 月 3 日
--

图片来源:Nick Fewings 来自 Unsplash
假设你训练了一个预测模型,准确率高达 0.9。像精确度、召回率和 F1 分数等评估指标看起来也很有前景。但你的经验和直觉告诉你事情并不对劲,于是你进行了进一步调查,发现了以下情况:

Image_1 — 作者截图
该模型看似强大的表现是由其目标变量中占多数的类别0驱动的。由于多数类与少数类之间明显的不平衡,模型在预测其多数类0时表现优秀,而少数类1的表现则远远不尽人意。然而,由于类别1仅占目标变量的一小部分,其表现对这些评估指标的整体得分影响不大,这给人一种模型很强的错觉。
这并非罕见的情况。相反,数据科学家在实际项目中经常会遇到不平衡的数据集。不平衡数据集指的是类别或种类分布不均的数据集……
如何处理:时间序列的缺失数据
你应该丢弃、插值还是填充?
·发表于Towards Data Science ·阅读时长 5 分钟·2024 年 5 月 25 日
--

图片来源:Mika Baumeister来自Unsplash
没有完美的数据集。每个数据科学家在数据探索时都有过这种感觉,当他们执行以下操作时:
df.info()
并且看到如下内容:

UCI 机器学习库空气质量数据集信息(CC BY 4.0)。图片由作者提供
大多数机器学习模型无法处理 NaN 或空值,因此,如果你的特征或目标变量中包含这些值,在尝试拟合模型之前,必须适当处理它们。
在本文中,我将探讨三种处理时间序列数据集中空值/缺失数据的简单方法。
1. 丢弃空值
这可能是处理缺失数据最简单、最直接的方法:直接丢弃它。
# Drop any and all nulls across all columns
df.dropna(inplace=True)
默认情况下,pandas 的dropna函数会在所有列中查找空值,并删除任何行,只要其中任何一列存在空值。然而,可以通过使用各种参数来修改这一行为。
如何处理时间序列缺失数据
缺失值的原因及如何使用 Python 处理它们
·发表于 Towards Data Science ·6 分钟阅读·2024 年 2 月 1 日
--

图片来自 Anton Nazaretian 于 Unsplash
介绍
数据收集中的问题可能导致缺失数据。这个问题可能由于各种原因引起,例如传感器维护或传输故障。
缺失数据通常通过数据插补策略来解决,例如用中心统计值替代缺失值。对于时间序列,插补过程更具挑战性,因为观测值是有序的。除此之外,选择一个考虑到缺失数据机制的策略也可能很有用。
在本文中,你将了解时间序列缺失数据的主要模式,以及如何处理它们。
缺失数据的原因及其重要性
缺失数据的模式,如其频率,取决于导致数据缺失的机制。
一般来说,缺失数据的原因可以归为以下几类:
- 完全随机缺失:当没有系统性过程导致观测数据缺失时。也就是说,缺失的数据与 1)观测值和 2)过去或未来的观测值之间没有关系,无论这些值是否也缺失…
如何识别影响购买决策的因素
没有什么比简洁更复杂。因子分析如何被用来揭示隐藏的因素。
·发表于Towards Data Science ·阅读时间 19 分钟·2024 年 9 月 4 日
--

图片由Joshua Rawson-Harris提供,来自Unsplash
项目概述
和我一起想象这个场景: 你刚刚被聘为一家零售公司的数据科学家。这家公司有多家门店,多个部门,每天销售着无数产品。作为你的第一项任务,你的经理问你:影响顾客购买决策的因素有哪些?
请注意,你的经理并没有要求你预测顾客是否会进行购买。事实上,这样的任务要简单得多。建立这样的模型是直接的。经理想要的是这个:哪些因素影响顾客的购买决策?——现在这就变得更复杂了。
你能想象这个情况吗?这不仅仅是预测一个数值,这就是我们通常在机器学习中的线性回归所做的事情。它是关于识别各种场景,找出哪些因素推动了购买决策。这时你就需要一种不同的技术,这正是我将在这个项目中通过因子分析——一种降维策略——引入的内容。
如何实现基于 Amazon EC2 的定制训练解决方案
云端 ML 训练管理的简单解决方案 — 第二部分
·发表于 Towards Data Science ·11 分钟阅读·2024 年 1 月 30 日
--

这是对上一篇文章的续集,上一篇文章讨论了如何使用低级实例配置服务构建定制的云端机器学习(ML)模型开发解决方案。在本文中,我们的重点将放在 Amazon EC2 上。
云服务提供商(CSP)通常会提供完全托管的解决方案,用于在云端训练机器学习模型。例如,Amazon SageMaker是亚马逊提供的机器学习开发托管服务,它显著简化了训练过程。SageMaker 不仅自动化了端到端的训练执行 —— 从自动配置所需的实例类型、设置训练环境、执行训练任务,到保存训练产物并关闭所有服务 —— 还提供了多个辅助服务,支持机器学习开发,如自动模型调优、平台优化的分布式训练库等。然而,正如高层次解决方案常有的情况,SageMaker 的易用性提高的同时,也伴随着对底层流程控制的一定丧失。
在我们的上一篇文章中,我们提到了一些由托管训练服务(如 SageMaker)有时强加的限制,包括用户权限减少、某些实例类型无法访问、对多节点设备放置的控制减少等。一些场景需要对环境规范和训练流程有更高的自主权。在本文中,我们通过在 Amazon EC2 上构建一个自定义训练解决方案,展示了解决这些情况的一种方法。
非常感谢 Max Rabin 对本文的贡献。
穷人的托管训练在 Amazon EC2 上
在我们上一篇文章中,我们列出了自动化训练解决方案所需的最小功能集,并逐步展示了如何在 Google Cloud Platform (GCP) 上实现这些功能。尽管相同的步骤序列可以应用于任何其他云平台,但由于每个平台的独特细节,实际情况可能会有所不同。本文的目的是提出一个基于 Amazon EC2 的实现方案,使用 AWS Python SDK(版本 1.34.23)的 create_instances 命令。与上一篇文章一样,我们将从一个简单的 EC2 实例创建命令开始,并逐步补充其他组件,以加入我们需要的管理功能。create_instances 命令支持许多控制选项。在本演示中,我们将只关注与我们解决方案相关的部分。我们假设已存在一个 默认 VPC 和一个具有适当权限的 IAM 实例配置文件(包括访问 Amazon EC2、S3 和 CloudWatch 服务的权限)。
请注意,使用 Amazon EC2 来满足我们定义的最小功能集有多种方式。我们选择演示其中一种可能的实现方式。请不要将我们选择 AWS、EC2 或我们所选特定实现的任何细节视为推荐。适合您的最佳机器学习训练解决方案将极大地依赖于您项目的具体需求和细节。
1. 创建 EC2 实例
我们从一个最小的单个 EC2 实例请求示例开始。我们选择了一个 GPU 加速的 g5.xlarge 实例类型和一个最新的 深度学习 AMI(带有 Ubuntu 20.4 操作系统)。
import boto3
region = 'us-east-1'
job_id = 'my-experiment' # replace with unique id
num_instances = 1
image_id = 'ami-0240b7264c1c9e6a9' # replace with image of choice
instance_type = 'g5.xlarge' # replace with instance of choice
ec2 = boto3.resource('ec2', region_name=region)
instances = ec2.create_instances(
MaxCount=num_instances,
MinCount=num_instances,
ImageId=image_id,
InstanceType=instance_type,
)
2. 自动启动训练
我们希望应用的第一个增强功能是,使我们的训练工作负载在实例启动并运行后自动开始,无需任何人工干预。为了实现这一目标,我们将利用create_instances API 的UserData参数,该参数使你能够指定启动时运行的内容。在下面的代码块中,我们提出了一系列命令,用于设置训练环境(即更新PATH环境变量,使其指向我们镜像中包含的预构建 PyTorch 环境)、从Amazon S3下载我们的训练代码、安装项目依赖项、运行训练脚本,并将输出产物同步到持久化的 S3 存储。示例假设训练代码已经创建并上传到云端,并且包含两个文件:一个是依赖文件(requirements.txt),另一个是独立的训练脚本(train.py)。实际上,启动序列的具体内容将取决于项目需求。我们还包括了指向我们预定义的 IAM 实例配置文件的指针,该配置文件是访问 S3 所必需的。
import boto3
region = 'us-east-1'
job_id = 'my-experiment' # replace with unique id
num_instances = 1
image_id = 'ami-0240b7264c1c9e6a9' # replace with image of choice
instance_type = 'g5.xlarge' # replace with instance of choice
instance_profile_arn = 'instance-profile-arn' # replace with profile arn
ec2 = boto3.resource('ec2', region_name=region)
script = """#!/bin/bash
# environment setup
export PATH=/opt/conda/envs/pytorch/bin/python:$PATH
# download and unpack code
aws s3 cp s3://my-s3-path/my-code.tar .
tar -xvf my-code.tar
# install dependencies
python3 -m pip install -r requirements.txt
# run training workload
python3 train.py
# sync output artifacts
aws s3 sync artifacts s3://my-s3-path/artifacts
"""
instances = ec2.create_instances(
MaxCount=num_instances,
MinCount=num_instances,
ImageId=image_id,
InstanceType=instance_type,
IamInstanceProfile={'Arn':instance_profile_arn},
UserData=script
)
请注意,上面的脚本只会在训练结束时同步训练产物。一种更具容错性的解决方案是在整个训练任务期间同步中间模型检查点。
3. 完成后自毁
当你使用托管服务进行训练时,实例会在脚本完成后自动关闭,以确保你只为需要的部分付费。在下面的代码块中,我们在UserData脚本的末尾附加了一个自毁命令。我们通过 AWS CLI terminate-instances 命令来实现这一点。该命令要求我们知道实例的instance-id和托管region,这些信息可以从实例元数据中提取。我们的更新脚本假设我们的 IAM 实例配置文件具有适当的实例终止权限。
script = """#!/bin/bash
# environment setup
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H \
"X-aws-ec2-metadata-token-ttl-seconds: 21600")
INST_MD=http://169.254.169.254/latest/meta-data
CURL_FLAGS="-H \"X-aws-ec2-metadata-token: ${TOKEN}\" -s"
INSTANCE_ID=$(curl $CURL_FLAGS $INST_MD/instance-id)
REGION=$(curl $CURL_FLAGS $INST_MD/placement/region)
export PATH=/opt/conda/envs/pytorch/bin/python:$PATH
# download and unpack code
aws s3 cp s3://my-s3-path/my-code.tar .
tar -xvf my-code.tar
# install dependencies
python3 -m pip install -r requirements.txt
# run training workload
python3 train.py
# sync output artifacts
aws s3 sync artifacts s3://my-s3-path/artifacts
# self-destruct
aws ec2 terminate-instances --instance-ids $INSTANCE_ID \
--region $REGION
"""
我们强烈建议引入额外的机制来验证适当的实例删除,以避免系统中出现未使用的(“孤立”)实例,从而积累不必要的费用。在一篇最近的文章中,我们展示了如何使用无服务器函数来解决这种问题。
4. 为 EC2 实例应用自定义标签
Amazon EC2 允许你通过使用 EC2 实例标签为实例应用自定义元数据。这使得你可以将关于训练工作负载和/或训练环境的信息传递给实例。在这里,我们使用TagSpecifications设置传入实例名称和唯一的训练任务 ID。我们使用该唯一 ID 来为我们的任务工件定义一个专用的 S3 路径。请注意,我们需要通过MetadataOptions设置显式启用实例访问元数据标签的功能。
import boto3
region = 'us-east-1'
job_id = 'my-experiment' # replace with unique id
num_instances = 1
image_id = 'ami-0240b7264c1c9e6a9' # replace with image of choice
instance_type = 'g5.xlarge' # replace with instance of choice
instance_profile_arn = 'instance-profile-arn' # replace with profile arn
ec2 = boto3.resource('ec2', region_name=region)
script = """#!/bin/bash
# environment setup
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H \
"X-aws-ec2-metadata-token-ttl-seconds: 21600")
INST_MD=http://169.254.169.254/latest/meta-data
CURL_FLAGS="-H \"X-aws-ec2-metadata-token: ${TOKEN}\" -s"
INSTANCE_ID=$(curl $CURL_FLAGS $INST_MD/instance-id)
REGION=$(curl $CURL_FLAGS $INST_MD/placement/region)
JOB_ID=$(curl $CURL_FLAGS $INST_MD/tags/instance/JOB_ID)
export PATH=/opt/conda/envs/pytorch/bin/python:$PATH
# download and unpack code
aws s3 cp s3://my-s3-path/$JOB_ID/my-code.tar .
tar -xvf my-code.tar
# install dependencies
python3 -m pip install -r requirements.txt
# run training workload
python3 train.py
# sync output artifacts
aws s3 sync artifacts s3://my-s3-path/$JOB_ID/artifacts
# self-destruct
aws ec2 terminate-instances --instance-ids $INSTANCE_ID \
--region $REGION
"""
instances = ec2.create_instances(
MaxCount=num_instances,
MinCount=num_instances,
ImageId=image_id,
InstanceType=instance_type,
IamInstanceProfile={'Arn':instance_profile_arn},
UserData=script,
MetadataOptions={"InstanceMetadataTags":"enabled"},
TagSpecifications=[{
'ResourceType': 'instance',
'Tags': [
{'Key': 'NAME', 'Value': 'test-vm'},
{'Key': 'JOB_ID', 'Value': f'{job_id}'}
]
}],
)
使用元数据标签将信息传递给我们的实例,在接下来的部分中将特别有用。
5. 将应用程序日志写入持久存储
自然,我们需要能够分析训练过程中的和训练后的应用程序输出日志。这要求日志能够定期同步到持久化日志中。在这篇文章中,我们使用Amazon CloudWatch来实现这一点。以下我们定义了一个最小的 JSON 配置文件,用于启用 CloudWatch 日志收集,并将其添加到我们的源代码 tar 包中,文件名为cw_config.json。有关CloudWatch 设置和配置的详细信息,请参阅官方文档。
{
"logs": {
"logs_collected": {
"files": {
"collect_list": [
{
"file_path": "/output.log",
"log_group_name": "/aws/ec2/training-jobs",
"log_stream_name": "job-id"
}
]
}
}
}
}
在实际操作中,我们希望log_stream_name能够唯一标识训练任务。为此,我们使用sed命令将通用的“job-id”字符串替换为上一节中的作业 ID 元数据标签。我们增强后的脚本还包括 CloudWatch 启动命令,并修改了将标准输出管道到 CloudWatch 配置文件中定义的output.log的部分。
script = """#!/bin/bash
# environment setup
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H \
"X-aws-ec2-metadata-token-ttl-seconds: 21600")
INST_MD=http://169.254.169.254/latest/meta-data
CURL_FLAGS="-H \"X-aws-ec2-metadata-token: ${TOKEN}\" -s"
INSTANCE_ID=$(curl $CURL_FLAGS $INST_MD/instance-id)
REGION=$(curl $CURL_FLAGS $INST_MD/placement/region)
JOB_ID=$(curl $CURL_FLAGS $INST_MD/tags/instance/JOB_ID)
export PATH=/opt/conda/envs/pytorch/bin/python:$PATH
# download and unpack code
aws s3 cp s3://my-s3-path/$JOB_ID/my-code.tar .
tar -xvf my-code.tar
# configure cloudwatch
sed -i "s/job-id/${JOB_ID}/g" cw_config.json
/opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl \
-a fetch-config -m ec2 -c file:cw_config.json -s
# install dependencies
python3 -m pip install -r requirements.txt 2>&1 | tee -a output.log
# run training workload
python3 train.py 2>&1 | tee -a output.log
# sync output artifacts
aws s3 sync artifacts s3://my-s3-path/$JOB_ID/artifacts
# self-destruct
aws ec2 terminate-instances --instance-ids $INSTANCE_ID \
--region $REGION
"""
6. 支持多节点训练
现在,训练任务在多个节点并行运行已经相当常见。修改我们的实例请求代码以支持多个节点,只需简单修改num_instances设置即可。挑战在于如何以支持分布式训练的方式配置环境,即一种能够 — 并且优化 — 实例之间数据传输的方式。
为了最小化实例之间的网络延迟并最大化吞吐量,我们在 ec2 实例请求的Placement字段中添加了一个指向预定义的集群放置组的指针。以下命令行演示了如何创建一个集群放置组。
aws ec2 create-placement-group --group-name cluster-placement-group \
--strategy cluster
为了让我们的实例能够相互通信,它们需要知道彼此的存在。在这篇文章中,我们将演示运行数据并行训练所需的最小环境配置,使用PyTorch。对于 PyTorch 的DistributedDataParallel(DDP),每个实例需要知道主节点的 IP 地址、主端口、实例总数以及在所有节点中的序列rank。下面的脚本展示了如何使用环境变量MASTER_ADDR、MASTER_PORT、NUM_NODES和NODE_RANK配置数据并行训练作业。
import os, ast, socket
import torch
import torch.distributed as dist
import torch.multiprocessing as mp
def mp_fn(local_rank, *args):
# discover topology settings
num_nodes = int(os.environ.get('NUM_NODES',1))
node_rank = int(os.environ.get('NODE_RANK',0))
gpus_per_node = torch.cuda.device_count()
world_size = num_nodes * gpus_per_node
node_rank = nodes.index(socket.gethostname())
global_rank = (node_rank * gpus_per_node) + local_rank
print(f'local rank {local_rank} '
f'global rank {global_rank} '
f'world size {world_size}')
# init_process_group assumes the existence of MASTER_ADDR
# and MASTER_PORT environment variables
dist.init_process_group(backend='nccl',
rank=global_rank,
world_size=world_size)
torch.cuda.set_device(local_rank)
# Add training logic
if __name__ == '__main__':
mp.spawn(mp_fn,
args=(),
nprocs=torch.cuda.device_count())
节点的 rank 可以从ami-launch-index中获取。节点的数量和主端口在create_instances调用时已知,并可以作为 EC2 实例标签传递。然而,主节点的 IP 地址仅在主实例创建后确定,并且只能在create_instances调用之后传递给其他实例。在下面的代码块中,我们选择通过专门调用AWS Python SDK的create_tagsAPI 将主节点地址传递给每个实例。我们还使用相同的调用根据每个实例的启动索引值更新它们的名称标签。
多节点训练的完整解决方案如下:
import boto3
region = 'us-east-1'
job_id = 'my-multinode-experiment' # replace with unique id
num_instances = 4
image_id = 'ami-0240b7264c1c9e6a9' # replace with image of choice
instance_type = 'g5.xlarge' # replace with instance of choice
instance_profile_arn = 'instance-profile-arn' # replace with profile arn
placement_group = 'cluster-placement-group' # replace with placement group
ec2 = boto3.resource('ec2', region_name=region)
script = """#!/bin/bash
# environment setup
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H \
"X-aws-ec2-metadata-token-ttl-seconds: 21600")
INST_MD=http://169.254.169.254/latest/meta-data
CURL_FLAGS="-H \"X-aws-ec2-metadata-token: ${TOKEN}\" -s"
INSTANCE_ID=$(curl $CURL_FLAGS $INST_MD/instance-id)
REGION=$(curl $CURL_FLAGS $INST_MD/placement/region)
JOB_ID=$(curl $CURL_FLAGS $INST_MD/tags/instance/JOB_ID)
export NODE_RANK=$(curl $CURL_FLAGS $INST_MD/ami-launch-index)
export NUM_NODES=$(curl $CURL_FLAGS $INST_MD/NUM_NODES)
export MASTER_PORT=$(curl $CURL_FLAGS $INST_MD/tags/instance/MASTER_PORT)
export PATH=/opt/conda/envs/pytorch/bin/python:$PATH
# download and unpack code
aws s3 cp s3://my-s3-path/$JOB_ID/my-code.tar .
tar -xvf my-code.tar
# configure cloudwatch
sed -i "s/job-id/${JOB_ID}_${NODE_RANK}/g" cw_config.json
/opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl \
-a fetch-config -m ec2 -c file:cw_config.json -s
# install dependencies
python3 -m pip install -r requirements.txt 2>&1 | tee -a output.log
# retrieve master address
# should be available but just in case tag application is delayed...
while true; do
export MASTER_ADDR=$(curl $CURL_FLAGS $INST_MD/tags/instance/MASTER_ADDR)
if [[ $MASTER_ADDR == "<?xml"* ]]; then
echo 'tags missing, sleep for 5 seconds' 2>&1 | tee -a output.log
sleep 5
else
break
fi
done
# run training workload
python3 train.py 2>&1 | tee -a output.log
# sync output artifacts
aws s3 sync artifacts s3://my-s3-path/$JOB_ID/artifacts
# self-destruct
aws ec2 terminate-instances --instance-ids $INSTANCE_ID \
--region $REGION
"""
instances = ec2.create_instances(
MaxCount=num_instances,
MinCount=num_instances,
ImageId=image_id,
InstanceType=instance_type,
IamInstanceProfile={'Arn':instance_profile_arn},
UserData=script,
MetadataOptions={"InstanceMetadataTags":"enabled"},
TagSpecifications=[{
'ResourceType': 'instance',
'Tags': [
{'Key': 'NAME', 'Value': 'test-vm'},
{'Key': 'JOB_ID', 'Value': f'{job_id}'},
{'Key': 'MASTER_PORT', 'Value': '7777'},
{'Key': 'NUM_NODES', 'Value': f'{num_instances}'}
]
}],
Placement={'GroupName': placement_group}
)
if num_instances > 1:
# find master_addr
for inst in instances:
if inst.ami_launch_index == 0:
master_addr = inst.network_interfaces_attribute[0]['PrivateIpAddress']
break
# update ec2 tags
for inst in instances:
res = ec2.create_tags(
Resources=[inst.id],
Tags=[
{'Key': 'NAME', 'Value': f'test-vm-{inst.ami_launch_index}'},
{'Key': 'MASTER_ADDR', 'Value': f'{master_addr}'}]
)
7. 支持使用 Spot 实例
一种减少训练成本的流行方法是使用折扣Amazon EC2 Spot 实例。有效地利用 Spot 实例要求你实现一种检测中断的方式(例如,通过监听终止通知)并采取适当的行动(例如,恢复未完成的工作负载)。在下面,我们展示了如何修改脚本,使用InstanceMarketOptions API 设置来使用 Spot 实例。
import boto3
region = 'us-east-1'
job_id = 'my-spot-experiment' # replace with unique id
num_instances = 1
image_id = 'ami-0240b7264c1c9e6a9' # replace with image of choice
instance_type = 'g5.xlarge' # replace with instance of choice
instance_profile_arn = 'instance-profile-arn' # replace with profile arn
placement_group = 'cluster-placement-group' # replace with placement group
instances = ec2.create_instances(
MaxCount=num_instances,
MinCount=num_instances,
ImageId=image_id,
InstanceType=instance_type,
IamInstanceProfile={'Arn':instance_profile_arn},
UserData=script,
MetadataOptions={"InstanceMetadataTags":"enabled"},
TagSpecifications=[{
'ResourceType': 'instance',
'Tags': [
{'Key': 'NAME', 'Value': 'test-vm'},
{'Key': 'JOB_ID', 'Value': f'{job_id}'},
]
}],
InstanceMarketOptions = {
'MarketType': 'spot',
'SpotOptions': {
"SpotInstanceType": "one-time",
"InstanceInterruptionBehavior": "terminate"
}
}
)
请查看我们之前的文章(例如,这里和这里),了解一些如何实现 Spot 实例生命周期管理解决方案的思路。
概要
针对 AI 开发的托管云服务可以简化模型训练,并降低潜在从业者的入门门槛。然而,在某些情况下,需要对训练过程有更大的控制权。在这篇文章中,我们展示了一种基于 Amazon EC2 构建定制化托管训练环境的方法。当然,解决方案的具体细节将大大依赖于具体项目的需求。
一如既往,欢迎随时通过评论、提问或修改来回复此帖子。


浙公网安备 33010602011771号