揭示传统 DiD 方法的局限性
原文:
towardsdatascience.com/uncovering-the-limitations-of-traditional-did-method-2f068f56d19a
处理多个时间周期和错开处理时间
·发表于Towards Data Science ·阅读时间 11 分钟·2023 年 2 月 21 日
–
封面图,由作者使用NightCafé生成
差分中的差分(DiD)是一种流行的统计方法,通过比较干预前后两个组的结果差异来估计观察研究中的因果影响。大多数 DiD 指南专注于经典的 DiD 设置,其中仅有两个时期和两个组(处理组和对照组)。
然而,在许多 DiD 的实际应用中,存在多个时间周期和处理时间的变化。近期对 DiD 的研究表明,在这些情况下,DiD 可能会给出显著误导性的处理效果估计。在某些场景下,处理效果估计可能与实际处理效果的符号相反。
在这篇文章中,我将讨论在经典 DiD 设置中,当存在错开处理时间和多个时间周期时可能出现的重要问题。我还会提出解决这个问题的方案。值得注意的是,虽然我将专注于 DiD 中的这一问题,但对于其他潜在挑战的更全面概述,你可以参考我之前的文章。此外,我将在本文末尾提供进一步的资源,供那些希望深入探讨 DiD 问题的人。
封锁与音乐消费实例
举个例子,我们考虑一个假设的场景。假设我们运营一个在多个国家运行的音乐流媒体服务。我们希望调查 Covid-19 封锁对这些国家音乐消费的影响。通过检查减少流动性的影响,我们可以深入了解听音乐是否与某些活动(如通勤)相关,而不是在家工作。
由于我们无法操控封锁的实施,因此无法进行 A/B 测试来检查其效果。因此,我们必须依赖观察性数据。在这种情况下,我们利用了数据集中包含的各国实施封锁的不同时间。在这个例子中,治疗是封锁的实施。对于这个玩具示例,我模拟了一个数据集,详细信息可以在我的上一篇文章和这个 Gist中找到。所有分析代码也可以在这个 Gist中找到。
rm(list = ls())
library(data.table) # Fast data frames
library(fastDummies) # Create dummy variables
library(fixest) # Fixed-effects regression
library(kableExtra) # Make nice tables
library(bacondecomp) # Goodman-Bacon Decomposition
library(did) # Difference-in-differences package by Callaway & Sant'Anna
source('sim_data.R') # Import data simulation functions and utilities
data <- sim_data() # Simulate the dataset
# EDA and Analysis --------------------------------------------------------
select_cols <- c('unit', 'period', 'cohort_period','treat','hrs_listened')
kable(head(data[, ..select_cols]), 'simple')
选定列的数据快照,图片由作者提供。
我们有 1000 个单位或客户的数据,涵盖他们被观察到的每个周期。cohort_period
指示一个单位在哪个周期接受治疗,因此属于哪个治疗队列。当cohort_period >= period
时,单位被视为接受治疗(treat = 1
)。hrs_listened
是我们关注的结果,表示总音乐消费(小时)。数据集中有两个队列:早期治疗队列和晚期治疗队列。早期队列在第 2 周期接受治疗,晚期队列在第 3 周期接受治疗。总共有五个周期,从第 0 周期开始,到第 4 周期结束。
由于我们拥有一个观察性数据集,其中治疗不是随机分配的,因此不能使用简单的均值差异方法来估计治疗效果。相反,我们的目标是将治疗效果与客户和季节相关因素区分开来,为此我们使用了 DiD 框架。
在我们进入 DiD 之前,虽然在现实应用中这并不可能,但在这个模拟数据集中,我知道每个治疗队列和周期的真实治疗效果。如下图所示,这些效果将在下一步评估估计的治疗效果时是必要的。
每个队列和周期的真实治疗效果,图片由作者提供。
从这张图表中可以看出,两个队列在所有时期的真实处理效果都是正的。两个队列的处理效果随时间增加。然而,总体而言,与队列 3 相比,队列 2 在处理时期的处理效果更大且增加显著。因此,不同处理队列和时期之间存在处理效果的异质性。
经典 DiD
假设我们没有意识到多期和错开的处理可能在使用经典 DiD 方法时导致误导性估计。因此,我们天真地决定使用经典 DiD 设置来考虑听力模式的季节性和我们观察数据集中的客户特定效应。我们使用这样的 DiD 设置 [1]:
经典 DiD,图片由作者提供。
Yᵢₜ 是关注的结果。αᵢ 是单位固定效应,用于控制时间不变的单位特征。γₜ 是时间固定效应,用于控制时间趋势或季节性。Dᵢₜ 是单位 i 在时间 t 的处理虚拟变量。ϵᵢₜ 是随机误差。关注的系数 βᵈᵈ 表示处理效果。
我们使用 R 中的经典 DiD 设置来估计处理效果:
formula <- as.formula('hrs_listened ~ treat')
canonical_did <- feols(formula,
data = data, panel.id = "unit",
fixef = c("unit", "period"), cluster = "unit")
summary(canonical_did)
经典 DiD 估计,图片由作者提供。
估计的处理效果是 -0.47(虽然统计上不显著)!但当我们对每个处理组和时期都有正的(且通常较大的)处理效果时,这怎么可能呢?原因在于 DiD 估计量是数据中所有可能的两组/两期 DiD 估计量的加权平均 [2]。换句话说,经典 DiD 估计可以分解为加权平均的两组 x 两期处理估计。这被称为Goodman-Bacon 分解。我们来使用‘bacondecomp’ 包 [2] 获取计算经典 DiD 估计所用的权重和估计值:
# Goodman-Bacon Decomposition
bacon_decomp <- bacon(formula, data, id_var="unit", time_var='period', quietly = F)
Goodman-Bacon 分解,图片由作者提供。
在上面的图像中,我展示了我们的经典 DiD 估计的 Goodman-Bacon 分解。确实,如果我们将这些估计值按其各自的权重相加,就会得到经典 DiD 估计的处理效果:0.5 x -4.53 + 0.5 x 3.59 = -0.47。由于我们有 2 个组和 2 个时期,其中处理指示符发生变化,我们有 2 个比较。
让我们详细检查这个表格,看看问题在哪里。我们从第二个比较开始,其中处理估计为 3.59。在这里,对照组(‘未处理’)是晚处理组,队列 3。‘处理’组是早处理组,队列 2。我将‘处理’和‘未处理’放在引号中,因为如你所记,数据集中所有组最终都会被处理。在这里,‘处理’和‘未处理’更准确地说指的是经典 DiD 估计器所使用的处理和对照组。
让我们继续讨论第一个用红色突出显示的比较,其估计值为-4.53。在这里,早期治疗组(队列 2)被用作晚期治疗组的控制组,这由标准估计器提供!这个比较没有多大意义。然而,如果治疗效果在各个队列和时期之间保持不变,一切都会正常。在这个应用中以及许多其他应用中,情况并非如此。由于早期治疗组的治疗效果更高并且动态增加,比较起来似乎晚期治疗组的治疗效果是负的!将早期治疗组用作晚期治疗组控制组的比较称为禁止比较[2]。
如何解决这个问题?
解决这个问题的主要方法是不要将治疗效果限制为单一估计,并仔细选择控制组。在接下来的步骤中,我们将看到如何做到这一点。首先,我将展示如何在没有特定 DiD 包的情况下解决这个问题。随后,我将使用专为多时间期设计的 R 包。
解决不依赖于特定 DiD 包的问题
首先,让我们解决这个问题,而不依赖于特定的 DiD 包。我知道为了获得治疗效果的良好估计,我需要做两件事:(1)不将治疗效果估计限制为单一系数,(2)确保我有一个好的控制组[3][4]。
本质上,有必要考虑不同队列和时期的治疗效果的变化。此外,确保每个被评估的时期都有未治疗的观察数据也至关重要。因为使用治疗过的观察数据作为控制组可能会导致显著误导的结果,正如之前所提到的那样。
正如你记得的那样,我的数据集中只有治疗组:在第二期接受治疗的队列(早期治疗组)和在第三期接受治疗的队列(晚期治疗组)。显然,由于没有尚未治疗的观察数据可以作为对照组,我无法估计晚期治疗队列的任何治疗效果。
早期治疗组的希望更大,因为在他们接受治疗时,晚期治疗组尚未接受治疗。这意味着我们可以在第二期对早期治疗队列估计治疗效果。然而,由于从第三期开始没有未治疗的观察数据,我们无法估计进一步时期的治疗效果。这就是为什么我们将剔除没有未治疗单位的时期。让我们来编码实现这一点。
# Drop periods that have no untreated units
data <- data[period < 3]
现在,是时候估计我们唯一可以估计的治疗效果了。我们将只对第二期的早期治疗组估计治疗效果。
# Create dummy variables
data <- data %>%
dummy_cols(select_columns = c("cohort_period", "period"))
interact_covs <- 'cohort_period_2:period_2'
# Regression
formula <- as.formula(paste0('hrs_listened ~ ',interact_covs))
model <- feols(formula,
data = data, panel.id = "unit",
fixef = c("unit", "period"), cluster = "unit")
summary(model)
在考虑了错位治疗后的回归结果,图片由作者提供。
从上述结果可以看出,估计的处理效果这次要合理得多:3.6。这意味着封锁措施导致该队列在这个时期的音乐消费增加了 3.6 小时。点估计值与真实处理效果(大约 4 小时)不完全相等,因为数据中存在噪声。
使用‘did’包来解决问题
作为手动处理所有事情的替代方案,我们可以使用did Callaway 和 Sant’Anna 的包 [3]。我们需要做的是使用att_gt
函数,利用正确的控制组来估计队列和时期层面的处理效果。下面给出了代码。这里需要注意的一点是,你需要将control_group
指定为'notyettreated'
,因为该函数默认会尝试找到一个未处理的组作为控制组。
# did package
out <- att_gt(yname = "hrs_listened",
gname = "cohort_period",
idname = "unit",
tname = "period",
xformla = ~1,
data = data,
est_method = "reg",
control_group = 'notyettreated'
)
out
打包结果,图像由作者提供。
att_gt
函数估计队列-时期特定的处理效果,即ATT(g,t)
。我们对第 2 队列第 2 时期的处理效果感兴趣,结果为 3.4 小时。这与我们在没有依赖这个包的情况下估计的处理效果几乎相同。由于估计的精确过程,估计值之间可能会有一些差异。处理前时期的‘处理效果’也在第一行中报告,这在统计上并不显著,因为在这种情况下,我知道干预之前结果变量没有系统性变化。我们还可以用一行代码将这些结果绘制成图:
ggdid(out) # graph the results
可视化打包结果,图像由作者提供。
这个图表也有助于检查处理前后随时间的趋势。这种可视化在估计许多队列和时期的处理效果时特别有用,尽管在这种情况下我只有一个队列和两个时期可以进行估计。
使用这个包相比于我的手动方法有额外的优点。只要你为att_gt
函数指定了所需的变量,你不需要做太多其他的操作。你甚至不需要删除没有未处理观察的时期,因为这个包已经考虑了这一点,并且只对有有效控制组的时期进行效果估计。另一个优点是,包默认报告考虑了多重假设检验的均匀置信区间(这会导致由于使用了更高的临界值而使置信带变宽)。两个方法之间的精确估计差异是由于精确的估计方法不完全相同。
回到我们的例子,我们看到封锁对音乐消费有积极的影响(尽管我们只能对一个群体在一个时期进行估计)。这表明实际上音乐听取与居家隔离是互补的。
结论
这里是本文的关键要点:
-
经典的 DiD 方法在存在多个时间周期和治疗时间变化的应用中可能会导致误导性的估计。
-
为了防止这个问题,可以使用考虑多个时间周期和治疗时间变化的估计量。
-
这些估计量适用于错开处理背景,因为它们允许灵活的处理效果,并且仅估计存在有效对照组的时期的处理效果。
-
这不是进行 DiD 分析时可能出现的唯一问题。有关其他问题,请参阅我之前关于事件研究的文章。
参考文献
[1] Angrist, J. D., & Pischke, J. S. (2009). 大多数无害的计量经济学:经验主义者的伴侣. 普林斯顿大学出版社。
[2] Goodman-Bacon, Andrew. (2021) “具有治疗时间变化的差分中的差分.” 计量经济学期刊 225.2: 254–277.
[3] Callaway, B., & Sant’Anna, P. H. (2021). 具有多个时间周期的差分中的差分. 计量经济学期刊, 225(2), 200–230.
[4] Wooldridge, J. M. (2021). 双向固定效应、双向 Mundlak 回归和差分中的差分估计量. 可在 SSRN 3906345 获取.
其他有用的 DiD 资源
视频:
Pedro H.C. Sant’Anna — “具有多个时间周期的差分中的差分”
Andrew Goodman-Bacon “具有治疗时间变化的差分中的差分”
一篇总结近期 DiD 文献的好论文:
Roth, J., Sant’Anna, P. H., Bilinski, A., & Poe, J. (2022). 差分中的差分的趋势是什么?近期计量经济学文献的综述. arXiv 预印本 arXiv:2201.01194.
感谢阅读!
如果你喜欢这篇文章并希望看到更多我的文章,可以 关注我。
免责声明*:我写作是为了学习,因此你可能会发现文章或代码中的错误。如果发现,请告知我。*
揭示巴西市政影响、公共卫生支出和患者转移之间的关联
一个引人入胜的故事旅程,与 Quarto、Shiny 和 ChatGPT 一同进行
·
关注 发布在 Towards Data Science ·11 min read·Apr 19, 2023
–
照片由 Natanael Melchor 拍摄于 Unsplash
巴西公共卫生系统长期以来一直在努力提高资源分配和提供护理的效率。其中一个主要挑战是患者需要前往其他城市接受必要的医院治疗。根据巴西国家卫生系统的数据,我们估计仅在 2021 年,全国范围内的患者参与了大约 400 万次这样的旅行。本文探讨了一个处理这一公共卫生问题的项目的实施细节。阅读本文对于那些从事公共卫生政策工作的人尤其重要。此外,由于最终产品中使用了详细的代码,该文档也可能引起数据可视化和讲故事领域专业人士的兴趣。
为了更好地理解本文中的问题,我们调查了医院就诊支出与患者流动之间的关系。我们的分析揭示了巴西各城市之间支出存在显著不平等,小城市的支出远低于大城市。我们假设这种支出差异会导致从医院能力低的城市向医疗基础设施更为完善的城市流动的患者显著增加。
我们使用由巴西地理统计研究所(IBGE)制作的城市影响模型(REGIC)来验证我们的假设。我们证明了患者流动主要影响那些管理能力较弱、医院和门诊支出相对较少的小城市。同时,管理和影响能力较强的大城市更可能接收外来患者,从而增加了对其医疗服务的需求。
为了使我们的发现更易于广泛受众访问,我们开发了一个使用 Shiny、Quarto 和数据可视化技术的交互式仪表板。该仪表板允许用户实时探索数据,并通过动态的 ChatGPT 提示提供额外的见解和讲故事元素。通过利用这些工具,我们希望为巴西公共卫生系统面临的挑战提供新的视角,并为改进其绩效的持续努力做出贡献。请通过这个 链接 查看完整分析和交互页面。此外,以下部分展示了产品中使用的一些图表及相关代码。
公共卫生数据图表(和代码)
产品中使用的可视化重点关注了地图。目的是展示巴西城市在接受医院治疗时的旅行需求。下面的图表例如展示了不同城市在患者流向其他城市寻求医疗服务方面的差异。
患者正在旅行寻求援助。图片由作者提供。
下面是构建数据集的代码块,这些数据集将作为使用 ggplot 绘制地图的基础。
agrupamento_municipio<-
dataset_analise %>%
filter(
deslocamento ==1) %>%
group_by(munic_res) %>%
summarise(
numero_internacoes = n()
) %>%
mutate(code_muni = munic_res,
tipo_deslocamento = "saida" ) %>%
bind_rows(
dataset_analise %>%
filter(
deslocamento ==1) %>%
group_by(codufmun) %>%
summarise(
numero_internacoes = n()
) %>%
mutate(code_muni = codufmun,
tipo_deslocamento = "entrada"),
dataset_analise %>%
filter(
deslocamento ==0) %>%
group_by(codufmun) %>%
summarise(
numero_internacoes = n()
) %>%
mutate(code_muni = codufmun,
tipo_deslocamento = "local")
) %>%
group_by(code_muni, tipo_deslocamento) %>%
summarise(
total_internacoes = sum(numero_internacoes)
)
agrupamento_municipio<-
agrupamento_municipio %>%
tidyr::pivot_wider(names_from = tipo_deslocamento, values_from = total_internacoes) %>%
mutate(liquido = ifelse(is.na(entrada),0,entrada)+
ifelse(is.na(local),0,local)-
ifelse(is.na(saida),0,saida))
agrupamento_municipio<-
agrupamento_municipio %>%
mutate(local = ifelse(is.na(local),0,local),
saida = ifelse(is.na(saida),0,saida),
entrada = ifelse(is.na(entrada),0,entrada),
perc_saida = saida/(saida+local)*100,
perc_entrada = entrada/(entrada+local)*100,
perc_entrada = ifelse(is.nan(perc_entrada),0,perc_entrada))
municipios_seat %>%
mutate(code_muni = str_sub(as.character(code_muni),1,6)) %>%
inner_join(agrupamento_municipio
) %>%
inner_join(
REGIC_trabalho%>%
mutate(code_muni = str_sub(as.character(cod_cidade),1,6))
) %>%
ggplot()+
geom_sf(data = estados_mapa, fill=NA, color="#808080")+
geom_sf(aes( fill= perc_saida),pch=21, color="#444444", size=2.9)+
geom_text_repel(data = mun_sel_nivel_1A,aes(x=X, y=Y, label= name_muni),fontface = "bold", color="white")+
geom_text_repel(data = mun_sel_nivel_1B,aes(x=X, y=Y, label= name_muni),fontface = "bold", color="white")+
geom_text_repel(data = mun_sel_nivel_1C,aes(x=X, y=Y, label= name_muni),fontface = "bold", color="white")+
geom_text_repel(data = mun_sel_nivel_2A,aes(x=X, y=Y, label= name_muni),fontface = "bold", color="white", force =2)+
scale_fill_continuous_sequential(palette= "Heat 2")+
labs(
fill= str_wrap("% de pacientes internados em outros municípios",15)
)+
theme_light() +
theme(
text = element_text(size=20),
panel.background = element_rect(fill = "black"),
panel.grid = element_blank(),
axis.title.x = element_blank(),
axis.title.y = element_blank(),
strip.background = element_rect(fill = "#505050"),
strip.text = element_text(color = "white"),
axis.text = element_blank(),
legend.key = element_rect(fill = "#15202B")
)+
facet_wrap(nome_nivel_hierarquia_ordenado~.)
地图上的每个点代表一个市镇,颜色表示该市的患者在其他市镇寻求护理的百分比。我们使用了“facet”数据可视化功能来展示每个城市在 REGIC 模型中的位置。在这个模型中,管理能力最高的最具影响力的城市是大都市(图中的第一行所示的组),而层级最低的是地方中心,位于最后一帧。
从地图上可以看出,地方中心集中在强烈的红色阴影点,这些点显示了需要旅行的患者高比例的市镇。另一方面,地图呈现了所有大都市的子层级,以黄色表示旅行需求较低。
图片强化了我们对极端层级在管理能力方面所赋予的自主权的预期。服务少且管理低时,对其他市镇的医院结构有强烈依赖。
指出 REGIC 模型中各级之间的流动是至关重要的。为此,我们使用汇流图。
REGIC 级别之间的流动。作者提供的图片。
上面的汇流图是使用下面描述的代码构建的。在调用两个构建流图的函数之前,必须进行一些数据处理。
ordem_y<-
dataset_analise %>%
filter(deslocamento==1,
nome_nivel_hierarquia.x == "Centro Local",
!(is.na(nome_nivel_hierarquia.y))) %>%
group_by(nome_nivel_hierarquia.y) %>%
summarise(
quantidade = n()
) %>%
ungroup() %>%
inner_join(
de_para_hierarquia %>%
rename(nome_nivel_hierarquia.y=nome_nivel_hierarquia,
entrada_abreviado = nome_abreviado)) %>%
arrange(quantidade) %>%
mutate(entrada = entrada_abreviado)
aluvial<-
dataset_analise %>%
filter(deslocamento==1,
nome_nivel_hierarquia.x == "Centro Local",
!(is.na(nome_nivel_hierarquia.y))) %>%
mutate(saída = nome_nivel_hierarquia.x,
entrada =nome_nivel_hierarquia.y ) %>%
select(saída, entrada)
aluvial<-
aluvial %>%
inner_join(
de_para_hierarquia %>%
rename(saída=nome_nivel_hierarquia,
saida_abreviado = nome_abreviado)) %>%
inner_join(
de_para_hierarquia %>%
rename(entrada=nome_nivel_hierarquia,
entrada_abreviado = nome_abreviado)) %>%
select(saida_abreviado, entrada_abreviado ) %>%
rename(saída= saida_abreviado,
entrada = entrada_abreviado)
aluvial$entrada <- factor(aluvial$entrada, levels = unique(ordem_y$entrada[order(ordem_y$quantidade)]))
p<-
alluvial_wide( data = aluvial,
max_variables = 2,
fill_by = 'first_variable')
parcats::parcats(p, data_input = aluvial,marginal_histograms = FALSE,labelfont = list(size = 15, color = "black"), sortpaths= "backwards")
上图所示的流动显示了来自被归类为地方中心的市镇的患者主要按以下顺序流动:次区域中心 B(17.6%)、区域首府 C(16.6%)、次区域中心 A(15.7%),仅在第四位的是大都市(13.8%)。由此可见,当患者需要医院护理时,REGIC 等级在某种程度上是被攀升的。大都市对地方中心有吸引力,但其他城市层级的接近和管理能力调节了这一点。
我们发现一些城市在接收来自其他城市的患者方面具有重要意义。因此,我们制作了两张地图,展示了两个在接收患者方面突出的城市的旅行影响,显示了旅行距离和接收的患者数量。为此,请查看下面的地图,重点关注累西腓,这个巴西城市接收了最多的其他地点的患者。
患者前往累西腓。作者提供的图片。
下面的代码稍长。在创建两个图形的两个对象之前,需要进行大量的数据转换。这里我们使用*{patchwork}*包将图表并排放置。
municipio_selecionado<-"261160"
muni_sel<-
dataset_analise %>%
filter(deslocamento ==1,
codufmun== municipio_selecionado) %>%
group_by(codufmun,nome_nivel_hierarquia_ordenado.y, uf.y) %>%
summarise(quantidade = n()) %>%
rename(code_muni= codufmun,
hierarquia = nome_nivel_hierarquia_ordenado.y,
uf = uf.y) %>%
mutate(tipo_deslocamento = "destino",
distancia = 0) %>%
bind_rows(
dataset_analise %>%
filter(deslocamento ==1,
codufmun== municipio_selecionado) %>%
group_by(munic_res,nome_nivel_hierarquia_ordenado.x, uf.x) %>%
summarise(
quantidade = n(),
distancia =min(distancia)
) %>%
ungroup() %>%
rename(code_muni= munic_res,
hierarquia = nome_nivel_hierarquia_ordenado.x,
uf=uf.x)%>%
mutate(tipo_deslocamento = "origem")
)
muni_sel_posicao<-
dataset_analise %>%
dplyr::filter(deslocamento ==1,
codufmun== municipio_selecionado)%>%
distinct(codufmun, mun_res_lat.x, mun_res_lat.y, mun_res_lon.x, mun_res_lon.y,distancia)
muni_sel_posicao<-
municipios_seat %>%
mutate(code_muni = str_sub(as.character(code_muni),1,6)) %>%
inner_join(
muni_sel_posicao %>%
rename(code_muni= codufmun)
)
muni_sel_repel<-
municipios_seat %>%
mutate(code_muni = str_sub(as.character(code_muni),1,6)) %>%
filter(code_muni %in% c("260960", "260790","120020")) %>% #261160-Recife,260790 -Jaboatão, 260960 - Olinda, 260410 - Caruarau, 260545 - Fernando de Noronha, 120020 - Cruzeiro do Sul-AC
inner_join(muni_sel)
xmin<- min(min(muni_sel_posicao$mun_res_lon.x), min(muni_sel_posicao$mun_res_lon.y)) -1
xmax <- max(max(muni_sel_posicao$mun_res_lon.x), max(muni_sel_posicao$mun_res_lon.y)) +1
ymin<- min(min(muni_sel_posicao$mun_res_lat.x), min(muni_sel_posicao$mun_res_lat.y)) -1
ymax <- max(max(muni_sel_posicao$mun_res_lat.x), max(muni_sel_posicao$mun_res_lat.y)) +1
g1<-
municipios_seat %>%
mutate(code_muni = str_sub(as.character(code_muni),1,6)) %>%
inner_join(
muni_sel
) %>%
ggplot()+
geom_sf(data = estados_mapa, fill=NA, color="#505050")+
geom_curve(data=muni_sel_posicao, aes(x=mun_res_lon.x,y=mun_res_lat.x,xend=mun_res_lon.y,yend=mun_res_lat.y, colour= distancia),
curvature = -.25, ncp = 800,size = 1)+
geom_sf(fill="white",size=1.9,pch=21, color="#444444")+
scale_fill_discrete_qualitative(palette="dark2")+
scale_color_continuous_sequential(palette= "Heat 2")+
coord_sf(xlim = c(xmin,xmax), ylim=c(ymin,ymax))+
labs(
fill= "",
color = str_wrap("distância em Km",10)
)+
theme_light() +
theme(
text = element_text(size=18),
panel.background = element_rect(fill = "black"),
panel.grid = element_blank(),
axis.title.x = element_blank(),
axis.title.y = element_blank(),
strip.background = element_rect(fill = "#505050"),
strip.text = element_text(color = "white"),
axis.text = element_blank(),
)
muni_sel_foco<-
municipios_seat %>%
mutate(code_muni = str_sub(as.character(code_muni),1,6)) %>%
inner_join(
muni_sel%>%
filter(code_muni==municipio_selecionado)
)
muni_sel<-
muni_sel%>%
filter(code_muni!=municipio_selecionado)
set.seed(1972)
g2<-
municipios_seat %>%
mutate(code_muni = str_sub(as.character(code_muni),1,6)) %>%
inner_join(
muni_sel
) %>%
ggplot()+
geom_sf(data = estados_mapa, fill=NA, color="#505050")+#505050
geom_sf( aes(fill=quantidade),pch=21, color="#444444", size=2, show.legend = TRUE)+
geom_sf( data= muni_sel_foco, aes(size=quantidade),pch=21, color="#444444", fill="white")+
geom_text_repel(data = muni_sel_repel,
aes(x=X, y=Y, label= str_wrap(paste(name_muni,":",quantidade),10)),
color = "white",
limits = c(0,2352),
fontface = "bold",
nudge_x = c(0,2,2.5),
nudge_y = c(0,-3.5,2),
show.legend = TRUE)+
geom_text_repel(data = muni_sel_foco,
aes(x=X, y=Y, label= str_wrap(name_muni,20)),
fontface = "bold",
color="white",
nudge_x = c(3),
nudge_y = c(0))+
scale_fill_continuous_sequential(palette= "Heat", trans= "log2" )+
coord_sf(xlim = c(xmin,xmax), ylim=c(ymin,ymax))+
labs(
fill = str_wrap("Quantidade de saídas",15),
size= str_wrap("Quantidade de entradas",15)
)+
theme_light() +
theme(
text = element_text(size=18),
panel.background = element_rect(fill = "black"),
panel.grid = element_blank(),
axis.title.x = element_blank(),
axis.title.y = element_blank(),
strip.background = element_rect(fill = "#505050"),
strip.text = element_text(color = "white"),
axis.text = element_blank(),
legend.key = element_rect(fill = "#15202B")
)
library(patchwork)
g1|g2
当涉及到医院护理时,可以看到大城市累西腓在整个巴西的影响。通过观察左侧地图上的哈弗斯距离,可以看到累西腓为距离超过 4000 公里之外的患者提供服务。样本表明,佩鲁南布科的首府接收了几乎来自全国所有联邦单位的患者。另一方面,当评估右侧地图时,可以发现最显著的影响发生在大都市区的城市,特别是奥林达和贾博瓦豆斯·瓜拉雷佩斯。同时,还可以看到在佩鲁南布科州整个区域延伸的红色阴影点。还可以识别对邻近州的影响,特别是帕拉伊巴、阿拉戈斯和里约格朗德 do 诺特。
在我们原始叙述的最后,我们需要展示较低的市级医院护理支出与患者转移需求较大之间的关联。为了测试这种关联,我们创建了来自 5570 个巴西市镇的患者流失和接收百分比的聚类。利用 PAM 技术生成的轮廓系数,我们确定了四个组:中等进入、弱退出、中等退出和强退出。最后一个组对分析最为重要。以下图表提供了评估组重要性的见解。
医院护理费用的箱线图。作者提供的图片。
下面的代码首先将数据加载到内存中,数据包括市镇的聚类。接下来,使用 ggplot,我们利用 {patchwork} 包构建了两个并排放置的箱线图。
agrupamento_municipio_cluster<-readRDS("agrupamento_municipio_2021.RDS")
g1<-
dataset_analise %>%
filter(deslocamento == 1,
perc.x>0,
perc.x<=50) %>%
distinct(nome_nivel_hierarquia.x,munic_res, perc.x) %>%
inner_join(
agrupamento_municipio_cluster %>%
rename(munic_res=code_muni)
) %>%
ggplot() +
geom_jitter(aes(x=cluster_4_k, y=perc.x, fill=perc_saida), pch=21, color="#444444",size=2)+
geom_boxplot(aes(x=cluster_4_k, y=perc.x),fill=NA, color= "white", outlier.shape = NA)+
scale_fill_continuous_sequential(palette= "Red-Yellow")+
theme_light() +
theme(
text = element_text(size=18),
panel.background = element_rect(fill = "black"),
panel.grid = element_blank(),
axis.title.x = element_blank(),
strip.background = element_rect(fill = "#505050"),
strip.text = element_text(color = "white"),
#axis.text = element_blank(),
axis.text.x = element_text(angle = 45, vjust = 0.5),
legend.key = element_rect(fill = "#15202B")
)+
labs(
fill= "(%) saída",
y = "Gastos Hospitalares e ambulatoriais - (%) do total"
)
g2<-
dataset_analise %>%
filter(deslocamento == 1,
perc.y>0,
perc.y<=50) %>%
distinct(nome_nivel_hierarquia.y,codufmun, perc.y) %>%
inner_join(
agrupamento_municipio_cluster %>%
rename(codufmun=code_muni)
) %>%
ggplot() +
geom_jitter(aes(x=cluster_4_k, y=perc.y, fill=perc_entrada), pch=21, color="#444444",size=2)+
geom_boxplot(aes(x=cluster_4_k, y=perc.y),fill=NA, color= "white", outlier.shape = NA)+
scale_fill_continuous_sequential(palette= "Red-Yellow")+
theme_light() +
theme(
text = element_text(size=18),
panel.background = element_rect(fill = "black"),
panel.grid = element_blank(),
axis.title.x = element_blank(),
strip.background = element_rect(fill = "#505050"),
strip.text = element_text(color = "white"),
axis.text.x = element_text(angle = 45, vjust = 0.5),
legend.key = element_rect(fill = "#15202B")
)+
labs(
fill= "(%) entrada",
y = "Gastos Hospitalares e ambulatoriais - (%) do total"
)
g1|g2
图中的每一个彩色点代表一个巴西市镇。点的颜色表示左侧图中的患者流出百分比和右侧图中来自其他城市患者的出席百分比。在两个图的横轴上,我们可以看到分组,纵轴上则是住院和门诊护理的费用百分比。
通过观察图表,我们可以看到费用与患者流入和流出组之间关联的最重要结论。当我们分析左侧图表中的患者流出时,我们发现强退出组的医院费用中位数远低于其他组。
你是否询问了具有互动性的 Shiny?
正如我们在文本开头所示,除了主要故事外,我们还准备了多个选项卡,允许用户进行过滤并生成探索互动中指定城市现实的图表。请参见下方这些互动的一些截图。注意几乎所有选项卡中都有下载与图表相关的数据的选项。
患者流动 — 巴西。作者提供的图片。
患者流向所选城市。图片由作者提供。
选定城市在箱线图中的位置。图片由作者提供。
一个选项卡显示了可以用于过滤和下载的数据的完整表格。
一张展示市政当局的 X 光图。图片由作者提供。
ChatGPT 怎么样?
最后一张选项卡显示了用户所选市政当局的主要信息汇总数据。请见下方屏幕截图。
信息摘要和生成 ChatGPT 的提示。图片由作者提供。
最后一张表格是可以与 ChatGPT 进行交互的地方。面板动态生成一个包含其他表格数据的提示。用户可以按下复制按钮,将提示带到 ChatGPT,观察神奇的效果。查看一个示例的截图。(如果读者不懂葡萄牙语并想了解提示和 AI 的回应,请通过电子邮件联系我:fbarbalho@gmail.com)。
由应用程序生成的提示。图片由作者提供。
ChatGPT 生成的文本作为对应用程序生成的提示的回应。图片由作者提供。
代码和数据
完整代码可以在github找到。
所有数据集都被归类为公共领域,因为这些数据是由巴西联邦政府机构生产的,作为主动透明性在互联网公布,并且受巴西信息获取法的管辖。
作者感谢Ben Huberman的宝贵评论。
揭示 Word2Vec 的开创之旅及人工智能科学的现状
图片由Finding Dan | Dan Grinwis拍摄,发布在Unsplash
与 Dr. Tomas Mikolov 的深入访谈
·
关注 发布于 Towards Data Science ·19 min read·2023 年 2 月 3 日
–
2012 年,托马斯·米科洛夫博士在捷克共和国的布尔诺技术大学获得了人工智能博士学位,论文题为《基于神经网络的统计语言模型》。在谷歌研究部门工作一年后,他发表了两篇极具影响力的论文,介绍了连续词袋模型(CBOW)和跳字模型,也称为 Word2Vec。因此,单词可以在一个稠密的连续空间中用数字表示,遵循简单的训练过程。这是最早有效捕捉单词语义的数值方法之一,并允许处理更大的词汇表。许多最先进的自然语言处理任务使用这种技术取得了超越性的成果,而 Word2Vec 的继承者仍在如今被认为是最先进的语言模型中扮演重要角色。米科洛夫博士认为复杂系统可能是通向智能语言模型的下一步。然而,要实现这样的智能语言模型,科学范式需要改变,以创建一个平等的竞争环境并允许新颖性。
在他为其博士论文辩护后的十年里,他的研究成果被引用超过 125,000 次,h-指数为 49,i-10 指数为 85,依据 Google Scholar 的数据。2014 年,他移居 Facebook,随后在 2020 年返回捷克共和国。他在捷克信息学、机器人学和网络安全研究所组建了团队,开发一个系统,该系统有望逐渐演变为强人工智能。
你的 Word2Vec 算法在自然语言处理领域是革命性的。你能描述一下促成你工作的那些出版物吗?
一个非常有影响力的研究小组,由心理学家大卫·鲁梅哈特领导,早在 80 年代就开始研究类似的概念。鲁梅哈特的学生之一是杰夫·辛顿,他因在神经网络方面的工作而闻名。在 80 年代,他们已经使用神经网络和分布式表示来表示单词,并展示了有趣的特性。在 90 年代,杰夫·艾尔曼使用递归神经网络来建模语言。他使用了由简单的手工编写的语法生成的人工数据。因此,他的工作有许多简化,并不像今天那样复杂;甚至与我们当前的最先进水平相去甚远。但这是一个非常有启发性和前瞻性的方法来表示语言。1991 年的一篇非常有影响力的出版物《Finding structure in time》讨论了在连接主义模型中表示时间的方法。这对我作为学生在工作语言模型时很有启发性。约书亚·本吉奥在 2002 年左右发表了一篇有影响力的神经语言建模论文,他在小数据集上超越了标准语言建模基准。后来,我和约书亚发表了几篇论文,并在他的团队中待了半年。最后,我发现第一个使用神经网络进行通用序列预测并在具有挑战性的基准上取得最先进性能的人是马特·马洪——他的 PAQ 算法基本上是用于数据压缩的神经语言模型,表现惊人。
谁对你影响最大?
对我影响最大的人最初是马特·马洪,后来是霍尔格·施温克;我发现他的论文比约书亚的更易读。它包含了可以快速实现的方法,而不是使用不必要的复杂方法。因此,我尝试自己做一些类似的事情。当我在 2006 年开始我的硕士论文时,我实现的第一个模型是递归神经语言模型。那时我对这种我刚刚发明的递归网络想法感到非常兴奋,但一开始效果不好——虽然比 n-gram 模型好,但不如简单的前馈神经网络。当时,让这种模型正常工作非常具有挑战性,因为我们不知道如何处理梯度爆炸和消失。在 80 年代和 90 年代,“社区”对递归网络中的学习记忆非常感兴趣。然而,没人知道随机梯度下降是否有效,一些论文声称它无效。此外,尽管人们在小数据集上取得了有限的成功,但没有人能成功地在大数据集上训练递归网络,至少没有牺牲大部分性能。现在,我们知道它们可以这样做,这些过去的故事很难理解。
对我来说,这是一个令人兴奋的故事。我在 2007 年夏天想到从神经语言模型生成文本的想法,并将其与 n-gram 模型生成的文本进行比较(灵感来自 SRILM 工具包)。流畅度的提高非常显著,我立即知道这就是未来。看到这些结果的同时知道我是第一个看到这些结果的人,感觉非常酷——就像发现了一个充满奇怪动物的未知岛屿一样。
当我开始研究 RNN 时,我不知道梯度消失和梯度爆炸的问题。经过一段时间,我成功地让 RNN 在小数据集上表现得非常好。这本身就是一个挑战——评估各种语言模型并进行比较,因为当时所有发布的模型通常都在私人数据上进行评估。此外,代码也没有发布。幸运的是,在 2010 年我在约翰霍普金斯大学的 Fred Jelinek 研究组实习期间,我设法获得了一个数据集。经过一些小的调整,我将其发布在我的网站上,这就是现在非常著名的 Penn Treebank 语言建模基准的由来。它与树库完全无关——它只是我用来比较不同语言建模技术的,同时与 JHU 研究人员之前发布的结果兼容。
我还在 2010 年发布了我的 RNNLM 代码,包括文本生成部分,以便其他研究人员可以轻松地复制我的结果。这是至关重要的:我获得的相对于 n-grams 的改进非常显著,当时几乎没有人相信我的结果是正确的。
然而,随着数据集大小的增加,我的递归网络未能收敛的可能性也在增加。这种在大数据集上混乱的行为是不可预测的。虽然大约 90%的在 Penn Treebank 上训练的模型能够收敛到良好的性能,但在更大的数据集上,这个比例降到了 10%左右。由于 RNN 从头实现很困难,我认为我的代码中一定有错误。我认为我只是计算梯度时出错了,或者遇到了一些数值问题。
我找了几天的错误。最终,我找到了熵激增的地方,并且情况变得更糟。一些梯度变得非常大,覆盖了模型的权重,导致训练出现问题。
你做了什么来解决这个问题?
我的解决方案很粗糙。我将梯度值截断到一个阈值以上。任何数学家看到这个技巧都会觉得很糟糕。不过,主要问题是梯度很少爆炸,因此任何防止爆炸的方法都是一个足够好的解决方案。这种启发式方法有效地使递归神经语言模型能够扩展到更大的数据集。如今,调试代码要容易得多,因为你知道标准模型在标准数据集上期望的结果。但在我的时代,这情况不同。我获得了新的最先进结果,却不知道还可以走多远。这很令人兴奋;我是在攀登一座无人到达过的山峰,而我不知道它有多高。最终,我在宾夕法尼亚树库上的困惑度达到了大约 70,大约是 n-grams 的一半。这一结果保持了相当多年的最先进水平。虽然在这里我可以抱怨,语言建模结果在 2014 年左右被错误报告:随着 dropouts 的发明,研究者们开始专注于用单一模型实现最佳结果。但随后我的所有结果都被丢弃了,这些结果是模型集成的。然而,dropout 技术本质上是一种伪装的集成。
许多人将深度学习的流行上升归因于计算能力的提高和大数据集。但这并不是全部故事。真正让它开始有效的是我们弄清楚了如何正确使用这些算法。
经过这一经验,我发现了深度学习叙事中的另一个不准确之处。在 2014-2016 年,深度学习的流行度猛增,出现了关于为什么此时而非之前出现这种热潮的解释。许多人将这一流行的上升归因于计算能力的提高和大数据集。但这并不是全部故事。真正让它开始有效的是我们*弄清楚了如何正确使用这些算法。例如,你可以拿我的 RNNLM 代码,在 90 年代的硬件和数据集上运行——你会得到远远超过当时技术的最先进结果。
显然,拥有更多的计算能力永远不会有害,这对行业的采用至关重要。然而,研究界对这些算法的正确使用才是决定其受欢迎程度的关键;增加的计算能力是次要的。我还认为开源和整体可重复性也是非常重要的因素。深度‘ ‘学习的历史比许多人现在认为的要丰富得多。
当然,这不仅仅是我;亚历克斯·克里热夫斯基让卷积神经网络(CNNs)在图像分类中发挥了作用,乔治·达尔、阿卜杜勒-拉赫曼·穆罕默德和其他人则弄清楚了如何利用深度神经网络进行语音识别,我们这一代的许多博士生也做出了贡献。
你在博士期间是否已经考虑过以不同方式表示词汇?
确实,当我在谷歌工作时,我并没有想出 Word2Vec;我在那之前已经做过类似的工作。我做的第一件事,与 Word2Vec 类似,是在 2006 年的硕士论文中完成的。当时我对神经网络了解不多。我看到了一篇 Yoshua Bengio 的论文,它使用了一个投影和一个隐藏层。我不知道如何处理具有多个隐藏层的神经网络,所以我决定把模型分成两部分。第一部分就像 Word2vec 一样——它从训练集中学习单词表示。第二个网络则使用这些拼接的表示作为输入来表示上下文并预测下一个单词。两个网络都只有一个隐藏层,结果相当不错——与 Yoshua 的论文相似。
在我的博士期间,我在一次国际会议上发表的第一篇论文就是关于这个模型的。虽然它并不是特别令人印象深刻,但我知道可以通过相当简单的模型来学习好的词向量。后来,我看到几篇论文使用了更复杂的神经网络架构来学习词向量。这在我看来相当愚蠢——人们会训练一个完整的神经语言模型,然后把它扔掉,只保留第一个权重矩阵。但对这个研究领域感兴趣的社区非常小,我认为在这个话题上没有发表任何东西的必要。后来,当我完成博士学业时,我在微软研究院实习,与 Geoff Zweig 合作。他是一个了不起的导师,但有时他会对神经网络是否是语言建模的未来表示怀疑——所以我在考虑如何让他印象深刻。
你做了什么来说服他?
这是一个有趣的故事。我进行了些计算,并在接触他之前仔细检查了结果。然后,我问他是否可以对词向量应用简单的加法和减法。我问他在从‘king’中减去‘man’并添加‘woman’之后,最接近的向量是什么(除了输入词,否则你经常会回到你开始的地方)。
他告诉我这是个相当愚蠢的想法,认为这样没有任何意义。因此,我立即把他带到我的电脑前,展示了实验结果——它返回了‘queen’。他非常惊讶,开始尝试各种操作。他尝试了动词的过去时和复数形式等等。有些想法有效,有些则无效。但这比随机猜测要好得多。第一次看到这些类比非常令人着迷。这引发了基本的问题。为什么会出现这些规律?为什么这是完全线性的?为什么不乘以向量,而是相加和相减?
你的谷歌同事也像你的导师一样持怀疑态度吗?
我不会称 Geoff Zweig 为怀疑,但可以说他非常谨慎。他实际上非常支持,很容易说服他相信某些想法值得追求。我在职业生涯初期遇到过更多麻烦。当我开始研究神经语言模型时,我收到了来自布尔诺理工大学一位当地语言学家的极其负面的评价。他甚至说,使用神经网络来建模语言的整个想法完全是胡扯,而且我的结果一定是假的。他差点儿让我被踢出博士项目。
当我加入 Google Brain 时,一些同事已经在尝试学习词语表示。然而,他们试图训练大型语言模型以获得词向量。在大型语言模型中,99.9%的训练时间,你在更新与词向量无关的参数。从 2006 年我的硕士论文中,我知道如果最终任务不是语言建模,这样的大型语言模型是不必要的。相反,使用更简单的模型来计算词向量就足够了。
我将这一见解与一些同事分享了。然而,没有人真正听取。一些人跟随的是一篇斯坦福论文,这篇论文复杂且包含许多不必要的内容。刚刚开始在 Google Brain 工作时,我的第一个目标是展示如何高效地解决这个问题。我开始尝试,很快就取得了成功。使用普通的台式电脑,我可以在几个小时内训练使用数亿个单词的模型。我的模型击败了一个在许多机器上训练了几周的 Google 内部模型。
那时发生了什么?
Yoshua 刚刚组织了一个新的会议,ICLR,并问我是否可以提交一篇关于词语类比的论文,因为那时这是一个相当令人惊讶的结果。他认为这会是一篇很酷的论文。他在 12 月中旬联系了我;截止日期是在 1 月初。所以我在加州的圣诞假期中写了 Word2Vec 论文。论文写得不是很好,但我更关心的是实现和结果,而不是论文。在同事的支持下,我向 ICLR 提交了论文。但不幸的是,评论非常负面(这是一个公开评审,因此应该仍然可以访问)。一位评审抱怨模型没有考虑词序。另一位评审试图强迫我更多地引用其他论文,而这些论文我已经引用过,并且是在我的硕士论文(其中已经包含了主要想法)之后发表的。
ICLR 2013 的接受率约为 70%。但 Word2Vec 论文被拒绝了。今天,它可能被引用的次数比 ICLR 2013 上所有接受的论文加起来还要多。
这里有一个有趣的细节。虽然现在是一个著名的会议,但这是 ICLR 的第一届,规模很小。接受率约为 70%,所以几乎所有不是完全糟糕的论文都会被接受。但 Word2Vec 论文被拒绝了,尽管今天它可能比 ICLR 2013 上所有接受的论文加起来的引用次数还要多。于是,我决定写另一篇扩展版的论文。这篇论文最终被接受到 NIPS。
你从未在其他地方发表过你的第一篇论文,对吧?
第一篇论文在被 ICLR 会议拒绝后被接受到一个研讨会。但我不认为研讨会算作发表。此外,它被发布在 Arxiv 上,我很高兴人们可以阅读。当我发布它时,我知道它比目前可用的要好——至少在我关心的方面。算法并不复杂,实际上提供了非常好的结果。
你是否预料到这篇论文会被如此广泛引用?
神经语言建模社区在我发布这篇论文时还很小。然而,我非常乐观,预期至少会有五十个人在一年内使用它。论文发布六个月后,它仍然未被注意。这是因为谷歌没有批准我开源代码。最初,他们认为代码是竞争优势。然而,我一直在推动开源。周围的前辈告诉我停止尝试,因为我永远无法获得批准。幸运的是,我认识谷歌脑的高层,他们成功绕过了阻碍。最后,谷歌在 2013 年 8 月左右批准了开源代码。这也是代码有些过度优化的原因:在等待批准的过程中,我对代码进行了调整,使其更短更快。代码开源后,兴趣激增。许多人对谷歌的机器学习活动感兴趣,并喜欢谷歌开源代码。这帮助极大。我确实很惊讶有这么多人开始使用这段代码和预训练模型,甚至在一些情况下超出了建模词汇和语言的范围。
你为什么倡导开源?
作为学生,我发现很难比较不同算法,因为这通常是不可能的。十五年前,发布在私有数据集上评估的语言建模论文而没有任何开源实现是很正常的。在我看来,这就是语言建模研究在过去几十年中没有取得太大进展的主要原因。我曾联系过几位研究人员,询问他们的数据集,但都没有成功。到了某个阶段,没有人能验证已发表的结果,社区也陷入了停滞。我发现某些人甚至在报告结果时作弊,例如,使用弱基线或在测试集上调整超参数后报告最佳结果(甚至在测试集上训练模型,这虽然罕见但并非闻所未闻)。我受到了 Matt Mahoney 在数据压缩社区工作的启发,想要重建我对统计语言建模的兴趣,因此我希望在可能的情况下发布我的代码和数据。当然,一个重要方面是,当我开始发布我的大规模神经语言模型结果时,我的改进幅度之大,以至于几乎整个研究社区都不相信我的结果可能是正确的。但由于没有人能在我的代码中找到任何错误(许多人尝试过——我收到过很多邮件,表示他们终于找到了我代码中的“bug”),我的 RNNLM 工具包被几家大公司使用,语言建模研究终于起飞。这就是自然语言处理领域深度学习的开始。
开源有缺点吗?
我认为有。当新的学生加入人工智能社区时,他们应该尝试开发自己的模型并发现新想法。然而,这非常困难,因为他们最终要与多年由许多研究人员逐步优化的最先进模型竞争。
另一种情况是,学生可以下载别人的代码甚至预训练模型,这些通常很复杂,他们可能并未完全理解。然后他们对其进行调整,做出增量变化,并在论文中发布结果。这种方法要容易得多。然而,这对科学来说是一个危险的发展,因为它将我们锁定在局部最优解中。几个主流观点被过度探索,而很少有人思考可以带来新范式转变的新方法。开源和“发布或死亡”共同促成了一个环境,在这里冒险没有回报。
“拥有最多 GPU 的团队相对于其他团队有很大优势。这使得学术界的人们感到沮丧,并创造了不公平的竞争。对某些基准测试轨道上的已发表论文施加计算限制将是一个简单的解决方案。”
所以开源代码有好处。同时,不利影响也很明显。是否存在中间的‘最佳’方法?
鉴于计算能力的重要性,拥有最多 GPU 的团队相较于其他人具有显著优势。这使得学术界的人们受到挫折,并且造成了不公平的竞争;并不是每个人的起点条件都相同。这就好比你去参加奥运会跑步比赛,但比赛时你却是在与骑自行车的人竞争。不论你多么优秀,你都会输。学生们在资源有限的情况下与科技巨头竞争时也会遇到同样的问题。他们可能有更好的想法,但仍会因为不够前沿而被拒绝。这一问题需要社区来解决。
解决这个问题的一个简单方法是对某些基准测试中的论文发布应用计算限制。按照这种方法,论文应当与能够在X小时内在标准化机器上进行训练的代码一起提交。不过,人们仍可以详尽地探索搜索空间,并提交具有最佳超参数的代码,因此拥有更多计算能力的人仍会占有优势。但至少这样竞争会公平些。顺便说一下,当 Matt Mahoney 提出压缩挑战时,他已经考虑到了这一点。
“许多人认为好的模型看起来复杂且充满了超参数和微调。简单的想法常常被认为不值得发表,因为任何人都可以做到。我认为这种心态完全是愚蠢的。”
机器学习社区还有哪些其他问题?
随着 AI 社区每隔几年就翻倍增长,主导科学家容易左右初级研究者的思维。然而,那些发大量推文和 Facebook 帖子,对所有事情都有强烈意见的主导科学家,并不总是那些做出强大贡献的人。一个由少数主导的资深科学家领导大量初级研究者的社区看起来就像某种邪教。这意味着一些想法、技术或模型被盲目推动,没有真实证据表明这些想法值得付出所有努力。例如,生成对抗网络(GANs)看起来被过度炒作了。这不是新现象。当我还是学生时,我记得对 Latent Dirichlet 分配的受欢迎程度感到困惑——它似乎也不比简单的基线方法更有效。但如今我认为这是一个更大的问题,因为信息传播得更快。
对通过蛮力取得的结果的过度强调体现了这个问题。许多人认为好的模型是看起来复杂且充满超参数的小调整。如果提出一个有效的简单想法,审稿人通常会争辩说任何人都可以做到,因此不值得发表。我已经见过这种情况几次,并且认为这完全愚蠢。实际上,我相信相反的观点:在实践中有效的简单想法是最有价值且最难发现的。就像物理学中,科学家们试图发展越来越通用的理论来解释尽可能多的现象一样。
实际上,这种情况发生在 Word2Vec 上,也发生在我一些语言建模工作上。当一个差劲的审稿人看到两篇有类似想法的论文,但其中一篇还添加了十几个不必要的改动时,这个差劲的审稿人会选择复杂的论文作为更好的那一篇,因为看起来投入的工作更多。实际上,情况往往正好相反——如果你能用一个简单的想法获得最先进的结果,那么这个想法可能真的非常好。
我们如何才能获得更好的审稿人?
机器学习可以从物理学中获得启发。在几个世纪的研究中,物理学家们旨在创建简单的理论以解释一切。与此同时,在机器学习领域则正好相反。我们应该放弃对最先进结果和复杂模型的强调,专注于发现有趣的新想法。当然,这高度主观,如果我们能将机器学习变成一个具有明确规则的奥林匹克项目来决定谁更优秀,那将更好。但正如我之前提到的,我认为这并不容易实现。今天,你可以提出一个惊人的新想法,可能成为下一个最先进的成果,但仍然会因为在某些大型基准上不够最先进而受到社区的打击和拒绝。博士生没有足够的时间来发展自己的方法和思路。我们应该改变这种情况,开始奖励新颖性和简洁性,即使这很难衡量。
或许你听说过 NIPS 的审稿实验。更多的审稿小组对论文进行评审,以查看接受/拒绝决定之间的相关性。结果发现,只有对非常差的论文才有很强的相关性。换句话说,审稿系统是非常随机的。
我们应该致力于创建一个更好的审稿系统。目前,我们在审稿系统中没有质量反馈;系统允许审稿人持续犯错,并且仍然能够审阅更多的论文。我们应该有审稿人数据库,自动跟踪他们的表现。他们的质量应该根据预测成功论文的能力来计算。例如,拥有优秀想法但英语较差的论文应该被接受。
在 IEEE SMC 大会的全体报告中,你提到将复杂系统作为人工智能的下一步发展方向。这是一种优雅地简化计算机科学规则的方法吗?
复杂系统是简单系统中通过你未指定的涌现/进化机制产生的复杂性。以《生命游戏》为例。你从简单的东西开始,然后模拟系统直到各种复杂的结构出现。这一直是我对宇宙的看法。我们周围的许多事物看起来很复杂。然而,这些复杂性可以被视为进化的副产品。自然智能是进化的产物。如果我们想通过人工智能来模拟这一点,我们应该采用类似的方法——允许人工智能进化,并有潜力自发地增加其复杂性。
这与进化算法相比如何?
可以使用进化算法来接近这一点。然而,我认为这些算法并没有很好地捕捉进化。它们进行随机优化。如果适应度函数有所改进,那么你就沿着这个随机方向前进。因此,梯度是随机选择的,而不是计算得出的。但在我看来,这不是进化——毕竟,进化算法往往很快陷入停滞。真实的进化可以在复杂系统中找到,即使是确定性的系统也是如此。《生命游戏》中没有任何随机性;你不需要掷骰子。即便如此,你仍然可以看到新颖的模式出现。我的目标是创建能够自发进化的系统,基于复杂性的涌现。我觉得发现能够在复杂性上隐式增长的机器学习模型具有使我们的 AI 模型更强大的潜力。这可能是让机器学习真正具有创造性的一种方式。
你将如何创建这样的系统?
我怀疑理解涌现现象是解决 AI 问题所必需的。然而,我们对这一方向的理解还不够深入。当我开始研究递归神经网络时,我希望这些能够成为通向有趣的复杂系统的捷径,其中涌现发生在模型的记忆中。但典型的递归网络架构具有一定的记忆容量限制。我们需要设计新颖的机器学习模型、训练算法和评估指标。我正与我的学生一起致力于这个工作。
这将如何为机器学习社区做出贡献?
社区已经体现出了一种群体文化。我们都朝着同一个方向前进,建立在现有的基础上。这种心态可能因为我强烈倡导的开源和公共基准测试而得到了强化。然而,我们所扩展的方法可能是错误的。如果是这样的话,每个人都在建立在有缺陷的假设之上。如果是这样的话,就需要修正。正如我提到的,我们应该探索不同的想法,并在研究社区中奖励新颖性。
这听起来像是一个不再开源的理由。
在我看来,开源是很棒的,我们应该继续这样做。请记住,当几乎没有人发布代码和数据集都是私有的时,研究人员通常不会互相信任对方的结果。语言建模社区几乎已经死去。
与此同时,我们应该避免开源的危险:过多的增量工作、提供微小改进的细微调整(有时仅仅如此),以及对探索新想法的气馁。
这标志着采访的结束,您还有什么最后的评论吗?
我们应该对原创性和新方向更加开放。然而,这很难判断。我们是否希望在会议上看到看似疯狂的想法?作为一个社区,我们需要让会议变得更加有趣,而不仅仅是看到数百种 Transformers 的修改或它们在数百个数据集上的应用。让我们更有雄心,更具探索性。
本次采访由 BNVKI,即贝尔赫斯人工智能协会,进行。我们汇聚了来自比利时、荷兰和卢森堡的人工智能研究人员。
揭示 DAX 中 KEEPFILTERS 的秘密
原文:
towardsdatascience.com/uncovering-the-secrets-of-kepfilters-in-dax-6d268e3565d0
DAX 中的 KEEPFILTERS()函数是一个被低估的函数。因此,我决定深入研究这个函数,并为你提供一些有趣的细节以及一个惊人的效果。
·发布在Towards Data Science ·8 分钟阅读·2023 年 7 月 13 日
–
引言
当我们在 DAX 中使用 CALCULATE()函数时,我们通常会添加这样一个简单的筛选器:
产品[Color] = “绿色”
此筛选器用“绿色”值替换[Color]列上的任何现有筛选器。
但有时,我们需要额外一步,保留表格或列上的现有筛选器,以执行一些有趣的计算。
有时,我们的度量值会得到错误的结果,我们无法理解为何会发生这种情况。
在这些情况下,KEEPFILTERS()函数可以帮助我们。
源查询
首先,让我们定义我们想要操作的查询。
我想获取按颜色分类的在线销售列表:
DEFINE
MEASURE 'All Measures'[Online Sales] = SUMX('Online Sales', [UnitPrice]*[SalesQuantity])
EVALUATE
SUMMARIZECOLUMNS('Product'[Color]
,"Online Sales", [Online Sales]
)
我使用 SUMX 将[UnitPrice]乘以[SalesQuantity]。
结果如下:
图 1 — 基础结果(作者图)
当我添加筛选器并使用 CALCULATE()时,查询如下所示,如上所述。
// Only Green Sales
DEFINE
MEASURE 'All Measures'[Online Sales] = SUMX('Online Sales', [UnitPrice]*[SalesQuantity])
MEASURE 'All Measures'[All Green Sales] =
CALCULATE([Online Sales]
,'Product'[Color] = "Green"
)
EVALUATE
SUMMARIZECOLUMNS('Product'[Color]
,"Online Sales", [Online Sales]
,"Green Sales", [All Green Sales]
)
结果如下:
图 2 — 所有行的绿色销售(作者图)
这是因为我们将[Color]列上的筛选器替换为“绿色”。因此,度量值在所有行上返回相同的值,其中[Color] = “绿色”。
介绍 KEEPFILTERS()
好吧,我们可以用 KEEPFILTER()做些什么?
当我们在度量值中添加 KEEPMFILTERS()时,CALCULATE 将保留每行的筛选上下文,并在表达式中添加筛选器:
// Only Green Sales with KEEPFILTERS()
DEFINE
MEASURE 'All Measures'[Online Sales] = SUMX('Online Sales', [UnitPrice]*[SalesQuantity])
MEASURE 'All Measures'[All Green Sales] =
CALCULATE([Online Sales]
,KEEPFILTERS('Product'[Color] = "Green" )
)
EVALUATE
SUMMARIZECOLUMNS('Product'[Color]
,"Online Sales", [Online Sales]
,"Green Sales", [All Green Sales]
)
这是新的结果:
图 3 — 使用 KEEPFILTERS() 的绿色销售(作者绘制的图)
好的,很棒。
那现在呢?
时尚
现在我们可以向我们的测量中添加一些逻辑。例如,我们可以仅对绿色产品进行销售计算。
例如,我们将绿色产品的销售额加倍:
// Perform some dynamic calculations - Double the Green Sales
DEFINE
MEASURE 'All Measures'[Online Sales] = SUMX('Online Sales', [UnitPrice]*[SalesQuantity])
MEASURE 'All Measures'[All Green Sales] =
CALCULATE([Online Sales]
,KEEPFILTERS('Product'[Color] = "Green" )
)
EVALUATE
SUMMARIZECOLUMNS('Product'[Color]
,"Online Sales", [Online Sales]
,"Green Sales", [All Green Sales]
,"Dynamic Sales", IF(ISBLANK([All Green Sales])
,[Online Sales]
,[Online Sales] * 2
)
)
我使用IF()和ISBLANK()来检查销售是否为绿色产品。
如果绿色销售的测量结果为空,我将返回[在线销售]测量的结果。
如果没有,我将[在线销售]测量的结果加倍。
看看结果:
图 4 — 动态销售结果(作者绘制的图)
但我们如何在 Power BI 中使用这个机制呢?
例如,我希望能够选择一种颜色,并对这种颜色的销售进行特定的计算。
首先,我向数据模型中添加了一个新表,但没有在数据模型中添加任何新的关系:
All Colors = SUMMARIZECOLUMNS('Product'[Color])
表格如下所示:
图 5 — 所有颜色表(作者绘制的图)
现在,我将这个列添加到我的报告中的切片器中。
接下来,我的测量必须获取选择的颜色并将其作为筛选器添加:
Modify by selected color =
VAR SelectedColor = SELECTEDVALUE('All Colors'[Color])
VAR CalcByColor = CALCULATE([Online Sales (By Order Date)]
,KEEPFILTERS('Product'[Color] = SelectedColor)
)
RETURN
IF(ISBLANK(CalcByColor)
,[Online Sales (By Order Date)]
,[Online Sales (By Order Date)] * 2
)
这样,我可以根据新表中选择的颜色执行计算:
图 6 — 基于选择颜色的计算结果(作者绘制的图)
这种技术为我们的计算开辟了许多可能性,因为我们可以对某一行进行计算而不影响其他行的结果。
使用上下文转换
但在某些情况下,理解 KEEPFILTER() 的值是至关重要的:上下文转换。
你可以通过阅读我关于这个话题的文章来了解更多关于上下文转换的内容:
行和筛选上下文是 DAX 中的常见概念。但我们可以通过上下文转换在这两者之间切换。
[towardsdatascience.com
当我们在测量中使用上下文转换与所谓的任意形状集一起时,情况会很复杂(稍后会详细说明)。
为了展示这一点,我稍微修改了我们的例子:
我想创建一个切片器,通过品牌和颜色的所有组合来筛选产品表。
然后,我想计算每个品牌和颜色的平均销售额。
在这个例子中,我不使用产品表中的列。我想要一个单独的表来模拟实际场景。
为了实现这一点,我使用 Power Query 从原始产品表中提取一个表,获取所有品牌和所有颜色的列表。此外,我添加了一个包含品牌和颜色列组合的关键列。
这里是结果表的摘录:
图 7 — 带有品牌和颜色及关键列的表格(图示由作者提供)
我将相同的关键列添加到产品表中。
现在我可以在这两张表之间添加一个关系:
图 8 — 扩展的数据模型(图示由作者提供)
现在,我创建了以下度量:
Average over Brand = AVERAGEX(VALUES('Brand Colors'[Brand])
,[Online Sales (By Order Date)]
)
但是当我们尝试验证结果时,我们会遇到困难。
原因在于没有任何控制结果时很难理解结果是否正确。
所以我们要么在 Excel 中重新计算结果(或其他可能的地方),要么更改度量以使用 SUMX()。
这使得生活更轻松,因为我们将能够将结果与现有的在线销售度量进行比较。
这里是 Power BI 中的结果:
图 9 — 复杂过滤器的新度量结果(图示由作者提供)
如果你仔细查看结果,会发现有些问题。
小计和总计远高于每行结果的总和。
原因在于过滤器的应用方式。
对于这个表,我们期望有如下的过滤器:
(Brand = "A. Datum" AND Color IN ("Black", "Blue")
OR
(Brand = "Adventure Works" AND Color IN ("Grey", "Silver")
这样的集合被称为“任意形状的集合”,因为我们混合了来自两个独立列的不同值。
当我们查看每个小计时,我们会期望有两个过滤器:
对于 Adventure Works,我们期望如下:
Brand = "Adventure Works" AND Color IN ("Grey", "Silver")
对于 A. Datum,我们期望:
Brand = "A. Datum" AND Color IN ("Black", "Blue")
实际上,我们得到了两个完全不同的过滤器:
对于 Adventure Works 的小计,我们有如下过滤器:
Brand IN ("Adventure Works", "A. Datum") AND Color IN ("Grey", "Silver")
对于 A. Datum 的小计,我们有如下过滤器:
Brand IN ("Adventure Works", "A. Datum") AND Color IN ("Black", "Blue")
这意味着度量计算所选颜色的所有销售总和,但结果中包括了两个选定品牌。
当我们添加新的矩阵可视化并从产品表中添加品牌和颜色列,并将结果与标准在线销售度量进行比较时,我们可以证明存在一些奇怪的情况:
图 10 — 用基础度量验证结果(图示由作者提供)
如你所见,这两个示例之间的结果不同,这使得这一效果极其令人困惑。
目前应用的过滤器如下:
(Brand = "A. Datum" AND Color IN ("Black", "Blue", "Grey", "Silver"))
OR
(Brand = "Adventure Works" AND Color IN ("Black", "Blue", "Grey", "Silver"))
参考文献部分提到的 SQLBI 文章更详细地解释了这一效果。
为了解决这个问题,我们可以使用 KEEPFILTERS() 来强制从切片器中获取完整的过滤上下文:
Average over Brand = SUMX(KEEPFILTERS(
VALUES('Brand Colors'[Brand]))
,[Online Sales (By Order Date)]
)
现在结果如预期一样:
图 11 — 添加 KEEPFILTERS() 后的结果(图示由作者提供)
照片由 Akhilesh Sharma 在 Unsplash 上提供
结论
DAX 函数 KEEPFILTERS() 非常有用,有时是关键功能。
我并不是建议在使用上下文转换时总是使用 KEEPFILTER()。
但您需要意识到使用上下文转换的后果,以及用户在报告中使用切片器时创建任意形状集的可能性。
在撰写本文时,我不知道使用上下文转换添加 KEEPCFILTERS()是否有任何缺点。
但我喜欢保持简单,不必要的东西就不添加。
无论如何,这篇文章最重要的教训应该是“只相信您可以证明和验证的结果”。
有一些函数在验证时可能非常具有挑战性。其中两个是 AVERAGE 和 COUNTDISTINCT。这两个函数返回的结果可能难以证明。
但这是另一个故事。
参考资料
SQLBI 的 KEEPCFILTERS()介绍:www.sqlbi.com/articles/using-keepfilters-in-dax-updated/
阅读 SQLBI 撰写的这篇文章,了解一些有趣的细节:www.sqlbi.com/articles/keepfilters-a-new-dax-feature-to-correctly-compute-over-arbitrary-shaped-sets/
当我们使用迭代器时,我们使用上下文转换。这里有另一篇 SQLBI 文章,关于这个话题:www.sqlbi.com/articles/when-to-use-keepfilters-over-iterators/
我使用了 Contoso 样本数据集,就像在我之前的文章中一样。您可以从 Microsoft 这里免费下载 ContosoRetailDW 数据集。
Contoso 数据可以根据 MIT 许可自由使用,详见这里。
[## 每当 Salvatore Cagliari 发布新内容时,您将收到电子邮件。
每当 Salvatore Cagliari 发布新内容时,您将收到电子邮件。通过注册,如果您还没有,您将创建一个 Medium 帐户…
medium.com](https://medium.com/@salvatorecagliari/subscribe?source=post_page-----6d268e3565d0--------------------------------)
理解并实现带掩码的自回归流与 TensorFlow
原文:
towardsdatascience.com/understand-implement-masked-autoregressive-flow-with-tensorflow-9c361cd1354c
使用 TensorFlow 进行密度估计的流模型
·发表于Towards Data Science ·8 分钟阅读·2023 年 2 月 21 日
–
图:从随机到不那么随机!来源:作者笔记本(见下文参考文献)。
之前我们详细介绍了正常化流背后的数学以及一些变换概率分布的示例。在这里,我们结合所有这些概念来理解自回归流以及如何使用 TensorFlow Probability 库实现它们。您可以从这篇文章中期待什么 —
-
为什么三角矩阵对自回归流至关重要?
-
自回归流模型的基本构造
— 掩码自回归流(MAF)
— 反向自回归流(IAF)
3. 如何在 TensorFlow 中实现 MAF 并训练它们以进行密度估计任务?
不再耽搁,让我们开始吧!
正常化流中的计算问题:
在讨论诸如掩码自回归流等模型之前,我们回顾一维和更高维场景中的变量变换规则,这将帮助我们理解正常化流中的计算成本。
之前我们详细讨论了如何推导变量变换规则,其中我们从基础分布 u 和双射 ϕ 开始,使得 x = ϕ(u)。在这种情况下,我们可以简单地写出变量变换规则如下:
等式 1:一维的变量变换规则
对于归一化流,我们组合(‘链式’)几个双射,将简单分布转变为更复杂的分布。例如,我们可以如下组合 K 次双射操作,将我们的基础分布(如 u_0)转换为我们所需的复杂分布 x。
方程 2:组合双射以将简单分布转变为复杂分布。
对于 K 变换,我们可以如下修改方程 1:
方程 3:将方程 1 从 1 次双射操作重写为 K 次变换。
实现归一化流的最大问题之一是计算对数-行列式雅可比矩阵的计算复杂度。通过像高斯消去这样的过程计算 n×n 的雅可比矩阵的行列式具有 运行时间复杂度 为 O(n³)。
因此,我们需要对上述过程进行一些简化,现在可以开始学习一些有助于减少计算复杂度的简化方法。
三角矩阵与自回归流:
如果变换矩阵是三角形的,那么计算行列式相当容易。对于 n × n 的方阵,运行时间复杂度是 O(n)。
计算三角矩阵的行列式只需要对角元素。
如上所示,对于三角矩阵(上三角/下三角),我们只需要对角元素来计算行列式,因此运行时间复杂度是线性的。
让我们考虑下三角矩阵,其中 a[i][j]=0; j > i。我们在自回归流中施加了非常类似的概念。
我们考虑一个 D 维向量 u,它经过 1,2,…K 次变换,就像以前一样。基于流模型的思想是使用一个(或一系列)变换 ϕ 对从 p_u(u) 采样得到的实际向量 u 进行操作。在双射与微分同胚的基础知识中,我们讨论了当 u 作为 D 维向量通过 K 次微分同胚变换为 x 时,我们说基础分布是 D 维的,最终分布 (x) 也将是。这样,我们施加了自回归条件,以便获得一个三角矩阵来计算对数-行列式雅可比矩阵,如下所示:
方程 4:自回归条件(左侧的方程)产生一个三角形的对数-行列式雅可比矩阵。
如果你曾经使用过 ARIMA 模型进行时间序列分析,那么你会知道 自回归 项表明时间序列基于过去的值。因此,我们可以约束序列数据 [x1, x2, …, xD],其中每个输出(在特定步骤)仅依赖于之前观察到的值,而不是未来的值。用更数学化的符号表示,就是观察 xi 的概率以 x1,…,xi−1 为条件,这些条件概率的乘积给出观察完整序列的概率:
Eq. 5: 条件概率的乘积给出观察完整数据 X 的概率。
条件密度的建模由我们选择,已经提出了多种方案,从简单的单变量高斯分布到甚至神经网络。让我们讨论一些流行的方法!
Masked Autoregressive Flow (MAF):
对于 MAF,上述 Eq. 5 中描述的条件分布将被视为简单的正态分布,如下所示:
Eq. 6: Eq. 5 中的条件分布假设为简单高斯分布
也可以从基础分布 u 生成新数据,如下所示:
Eq. 7: 给定基础分布 (u) 生成新点,即一组随机数
上述方程告诉我们另一个将自回归模型视为从随机数空间 (u) 到数据 x 的变换 f 的方法。由于这些变换是仿射的(缩放和偏移),为了找回基础变量 u_i,我们不需要逆转这些函数。这也在 MADE 论文中提到:
Eq. 8: 从 Eq. 7 中的变换中反转回基础变量。
这对训练策略极为重要,因为我们不需要显式计算函数 f_αi、f_μi 的逆,只需对它们进行一次评估(例如,在前向传递时),我们可以使用不可逆的函数,如 RELU。
在 Eric Jang 的博客 中提供了 MAF 的前向传递(以及反向传递)的优秀图示(查看参考文献)。
图 2: MAF 的前向传递。来源: Eric Jang 的精彩博客 关于归一化流。
这些是 Masked AutoRegressive Flow for Density Estimation 论文的基础,希望这个归一化流系列能帮助你解读其中的大部分内容。
Inverse Autoregressive Flow (IAF):
IAF 的变换规则在MAF论文中也有清晰解释。IAF 与 MAF 的主要区别在于,对于计算缩放和偏移变量(用于仿射变换),我们使用随机变量(u)而不是数据变量(x)。以下是变换规则:
等式 9: 将其与等式 7 对比,查看 MAF 和 IAF 之间的差异
MAF 和 IAF 之间惊人的相似性是难以忽视的;IAF 的逆是 MAF 的前向传播!
使用 TensorFlow Probability 训练 MAF:
在掌握基础知识后,我们现在准备使用 TensorFlow Probability 来实现 MAF,并训练它以产生特定的分布。我们使用sklearn.datasets
,特别是如下所示的make_circles
数据集:
1. 加载数据集:
circle_dataset = datasets.make_circles(noise=0.05, factor=0.99,
random_state=1, n_samples=1600)
X_circle, Y_circle = circle_dataset
#standardize
X_circle_normed = StandardScaler().fit_transform(X_circle)
Y_circle = Y_circle.astype('bool')
X_train_c, Y_train_c = X_circle[…, 0], X_circle[…, 1]
#figure section
fig = plt.figure(figsize=(6, 4))
fig.add_subplot(111)
plt.scatter(X_train_c[Y_circle], Y_train_c[Y_circle],
s=10, color='blue', alpha=0.4)
plt.scatter(X_train_c[Y_circle == False], Y_train_c[Y_circle == False],
s=10, color='red', alpha=0.5)
plt.legend(['label: 1', 'label: 0'])
plt.show()
图 3: 我们的目标分布 [来源: 作者笔记本]
这将是我们的目标分布,数据点以圆形方式分布。我们的目标是从随机分布开始,通过使用 MAF 达到这种有序分布。在 TensorFlow Probability 中,实现 MAF 相当简单,因为它存在一个称为MaskedAutoregressiveFlow
的双射器。在这个双射器中,对于仿射函数(平移和缩放),我们可以使用另一个双射器AutoregressiveNetwork
,它实现了Masked AutoEncoder for Density Estimation (MADE)架构,作者建议这种方法是从自动编码器中一次通过得到联合概率的计算上便宜的方式。下面我们来看实现:
通过 MAF 的前向传播,基础分布为正态分布
我们从一个正态分布开始,并定义 MAF 函数,其中仿射变换由 MADE 架构定义。MADE 架构包含 2 个隐藏层,每层 32 个单元,激活函数为‘Relu’。根据前一篇文章,我们描述了如何使用TransformedDistribution
变换分布,我们使用正态分布作为基础,MAF 作为双射器。最后,我们绘制了变换分布的概率等高线图。
图 4: 从正态分布开始,我们将其通过 MAF,其中仿射变换由 MADE 结构提供(激活函数为 Relu)。来源: 作者笔记本
图 5: 与图 4 相同,但激活函数由 Relu 改为 sigmoid。来源: 作者笔记本。
一旦我们定义了前向传播,我们现在就可以开始训练 Flow 模型。我们在最小化负对数似然,首先从一个双射器(MAF 网络)开始训练:
仅使用双射操作(MAF)的训练循环
正如预期的那样,一旦我们绘制训练后的分布,很容易看出结果远离真实分布。我们从训练后的分布中采样,以绘制下面的图:
图 6:从随机分布(中间)开始,仅使用双射,我们无法复制真实分布。来源:作者的笔记本
但我们也知道,基于流的模型的思想是链式双射将简单分布转换为复杂分布。在这里,我们不再仅使用 1 个双射,而是链式使用 4 个双射(MAF),并最小化最终分布的负对数似然:
num_bijectors = 4
bijectors=[]
for i in range(num_bijectors):
masked_auto_i = make_maf(hidden_units=[128, 128], activation='relu')
bijectors.append(masked_auto_i)
bijectors.append(tfb.Permute(permutation=[1, 0]))
# data is only 2 dimension, so we interchange 0, 1
flow_bijector = tfb.Chain(list(reversed(bijectors[:-1])))
排列部分确保了D维数据(这里D=2)的不同维度之间相互影响。如果维度的排序从未改变,这会大大降低在归一化流中链式双射的表达能力。在链式双射时,我们丢弃了最后的排列,因为它在训练中无关紧要(没有其他操作跟随这个排列)。有了这个更具表达力的模型,我们可以期待一些有趣的东西!让我们看看训练分布的概率密度等高线图:
图 7:看起来与我们的目标数据(图 3)非常相似!!来源:作者的笔记本!
这很酷,但我们也可以绘制样本分布,因为我们的基础正态分布通过这 4 个 MAF 双射,来看一下:
图 8:通过链式 4 个 MAF 双射将随机样本转换为圆形样本。来源:作者的笔记本。
我们从归一化流的基础开始,即微分同胚和概率分布的变换规则等。然后我们使用 TensorFlow 概率库实现了一些这些概念,将概率分布从正态分布转变为双峰分布,详见第二篇文章。最后,我们了解了最先进的自回归流模型的基础知识以及三角矩阵在这方面的重要性。最后,我们使用 TensorFlow 实现了 MAF,并展示了训练链式 MAF 以将正态分布转换为稍微复杂一点的分布的示例。希望这能帮助你入门流式模型,并迈出向扩散模型发展的第一步!!
如果你对进一步的基础机器学习概念感兴趣,你可以考虑加入 Medium 使用 我的链接。你无需支付额外费用,但我会获得一小部分佣金。感谢大家!!
[## 使用我的推荐链接加入 Medium - Saptashwa Bhattacharyya
更多来自 Saptashwa(以及 Medium 上的许多其他作者)。您的会员费直接支持 Saptashwa 和其他…
参考文献:
[1] 用于密度估计的掩蔽自回归流: Papamakarios, G. 等
[2] 使用 RealNVP 进行密度估计: Dinh, L. 等
[3] 归一化流:第二部分; Jang, E. 的博客
[4] 基于流的模型; Weng, L. 的博客
[6] 这里使用的代码笔记本:我的 GitHub
理解 Polars 缺乏索引
从 Pandas 切换到 Polars,忘记索引吧
·
关注 发布于 Towards Data Science ·7 分钟阅读·2023 年 1 月 6 日
–
一只北极熊与一只熊猫竞赛——来源:openai.com/dall-e-2
Pandas 和 Polars 是两个 Python 的数据框库。在上一篇文章中,我这样写过 Pandas 和索引的内容:
为了高效使用 Pandas,忽略它的文档,学习关于索引的*[复杂]*真相。
相比之下,原始的 Polars 书籍 这样说 Polars 和索引:
不需要索引!没有它们会使事情变得更简单——说服我们相信相反的观点!
我们真的可以忘记索引吗?让我们测试一下 Polars 的说法。我们将把我之前文章中的所有示例从 Pandas 移植到 Polars。这将让我们了解在没有索引的情况下工作的实际情况。
最终,我们将看到:
-
“索引是不必要的!没有索引使事情更简单。”
-
如果你认为你 真的 需要一个索引,你 真的 需要一个字典。
为了达到这一点,我们首先创建一个数据框并检索一行。
构建和简单行检索
在 Pandas 中,我们这样构建数据框并设置索引:
import pandas as pd
df1 = pd.DataFrame([['a',2,True],
['b',3,False],
['c',1,False]],
columns=['alpha','num','class'])
df1.set_index(['alpha'],inplace=True)
df1
我们用以下方法将关键字b
转化为感兴趣的行:
df1.loc[['b']] # returns row 'b' as a dataframe
在 Polars 中,我们可以通过这样的方式从行构建数据框:
import polars as pl
df1 = pl.from_records([['a', 2, True],
['b', 3, False],
['c', 1, False]],
orient='row',
columns=['alpha', 'num', 'class'])
df1
然而,Polars 是以列为中心的,所以构建相同数据框的更好方法是:
import polars as pl
df1 = pl.DataFrame({'alpha':['a','b','c'],
'num':[2,3,1],
'class':[True,False,False]
})
df1
我们不需要设置索引。我们用以下方法将关键字b
转化为感兴趣的行:
df1.filter(pl.col("alpha")=='b')
filter
方法用于查找感兴趣的行。表达式 pl.col("alpha")=='b'
告诉 filter
要查找哪些行。与 Pandas 相比,我发现 Polars 的方法更简单、更通用。(我们稍后将讨论性能问题。)
我们从找到一个简单的行转向找到行的数量。
查找行号
在 Pandas 中,你可以通过 index.get_loc(...)
查看感兴趣行的数字:
import pandas as pd
df2 = pd.DataFrame([['x',2,True],
['y',3,False],
['x',1,False]],
columns=['alpha','num','class'])
df2.set_index(['alpha'],inplace=True)
print(f"{df2.index.get_loc('y')=}")
print(f"{df2.index.get_loc('x')=}")
如示例所示,函数仅在单个项匹配时返回一个数字。当多个项匹配时,它返回一个布尔数组。
在 Polars 中,你应该首先问自己是否真的需要找到行号。答案通常是“否”。但是,如果你回答“是”,你可以使用 arg_where
。
df2 = pl.DataFrame({'alpha':['x','y','x'],
'num':[2,3,1],
'class':[True,False,False]})
df2.select(pl.arg_where(pl.col("alpha")=='y')).to_series()
df2.select(pl.arg_where(pl.col("alpha")=='x')).to_series()
结果是一个 Polars series
。在 Polars 中,series
代表一列值,这里是行号。
比较 Pandas 和 Polars 在查找行号方面的复杂性,我发现两者类似。然而,Polars 可能会通过减少行号的重要性而占据优势。
接下来我们来看复杂的行访问。
行访问
在 Pandas 中,访问索引行的主要方式是 .loc[…]
,其中输入可以是:单个元素、元素列表或元素切片。行将按输入中出现的顺序输出。这些示例展示了每种输入方式。
df3 = pd.DataFrame([['i',2,True],
['j',3,False],
['k',5,False],
['j',1,False]],
columns=['alpha','num','class'])
df3.set_index(['alpha'],inplace=True)
df3.loc['j']
df3.loc[['k','i']]
df3.loc['i':'k']
请注意,与 Python 的其他部分不同,Pandas 中的 start:stop 切片包括 stop 值。同时注意,Pandas 排除了第二个‘j’行,因为它在(第一个)‘k’行之后。
在 Polars 中,我们使用 filter
和表达式:
df3 = pl.DataFrame({'alpha':['i','j','k','j'],
'num':[2,3,5,1],
'class':[True,False,False,False]})
df3.filter(pl.col("alpha")=='j')
df3.filter(pl.col("alpha").is_in(['k','i']))
df3.filter(pl.col("alpha").is_between('i','k',include_bounds=True))
默认情况下,Polars 的is_between
不会包含其边界,但可以选择包含其中一个或两个边界。同时,请注意,Polars 包含了第二行的‘j’。Polars 基于字母顺序而非行顺序查看in_between
(在字符串值上)。
由于它不需要索引,我发现 Polars 在这些更复杂的行检索中比 Pandas 更简单。
对于我们的最后一个基本任务,让我们看看连接行。
连接行
在 Pandas 中,左连接的规则是:
-
左侧的数据框不需要被索引,但右侧的数据框需要。
-
在连接的
on
输入中给出左侧感兴趣的列。
在这个示例中,我们将使用join
向数据框中添加一个“score”列。这里是左侧的数据框。它没有被索引。
df_left = pd.DataFrame([['x',2,True],
['y',3,False],
['x',1,False]],
columns=['alpha','num','class'])
在 Pandas 中,右侧的数据框需要一个索引,但它可以命名为任何名称。这里我们称它为any_name
。
df_right = pd.DataFrame([['x',.99],
['b',.88],
['z',.66]],
columns=['any_name','score'])
df_right.set_index(['any_name'],inplace=True)
我们通过左连接组合这两个数据框。我们使用第一个数据框中的alpha
列以及第二个数据框中的任何索引。结果是一个包含分数列的新数据框。
df_left.join(df_right,on=['alpha'],how='left')
在 Polars 中,一切都很类似,但稍微简单一些:
df_left = pl.DataFrame({'alpha':['x','y','x'],
'num':[2,3,1],
'class':[True,False,False]})
df_right = pl.DataFrame({'alpha':['x','b','z'],
'score':[.99,.88,.66]})
df_left.join(df_right,on=['alpha'],how='left')
区别在于我们不需要为右侧数据框建立索引。如果感兴趣的列具有相同的名称(如这里所示),我们使用on
。如果没有,我们使用left_on
和right_on
。
所以,Polars 再次比 Pandas 更简单使用,但代价是什么呢?
性能
当然,缺少索引会使 Polars 变慢。但令人惊讶的是,实际上并非如此。在广泛的基准测试中,Polars 比 Pandas 快得多 [Vink, 2021]。它通过优化实现了这一点,包括良好的内存布局和自动向量化/并行化。
我们能否构造出 Pandas 比 Polars 更快的情况?是的,如果我们将数据框用作字典,Pandas 可能比 Polars 快 20 倍。然而……
猜猜什么比将 Pandas 用作字典快 300 倍?答案是:将字典用作字典。
在这个测试中,我们构造了一个包含两个列的数据框,填充了从 0 到 999,999 的数字。然后我们寻找数字 500,000。
import polars as pl
import pandas as pd
n = 1_000_000
df_pl = pl.DataFrame({'a':list(range(n)),'b':list(range(n))})
%timeit df_pl.filter(pl.col("a")==n//2)
df_pd = pd.DataFrame({'a':list(range(n)),'b':list(range(n))})
df_pd = df_pd.set_index('a')
%timeit df_pd.loc[n//2]
dict_pl = df_pl.partition_by('a',as_dict=True)
%timeit dict_pl[n//2]
以下是我在 4 核笔记本电脑上多次运行的平均结果:
总结性能:根据其他基准测试,对于典型使用情况,Polars 比 Pandas 更快。对于特殊情况——例如,当你确实需要使用字典时——Polars 提供了创建字典以获得最快性能的工具。
结论
在我看来,消除索引使得 Polars 比 Pandas 更易于使用。
你可能会预期这种简化会导致性能变慢。然而,基准测试显示 Polars 通常比 Pandas 快得多。它通过包括良好的内存布局和自动向量化/并行化等优化来实现这一点。可能仍然存在需要类似索引的数据结构的情况。对于这些情况,Polars 提供了创建字典等工具。
那么,你应该从 Pandas 切换到 Polars 吗?这要视情况而定。
我们的基因组学项目,FaST-LMM 使用 Pandas 输出统计结果表格。FaST-LMM 几乎所有的计算工作都在 Pandas 之外用自定义代码完成。它仅使用 Pandas 与我们的用户分享最终结果,我们可以假设这些用户了解 Pandas。考虑到这一点,我们没有理由从 Pandas 切换。
另一方面,如果我开始一个涉及有趣数据分析的新项目,我会使用 Polars。Polars 给了我一直想从 Pandas 中获得的速度和简便性。
请 在 Medium 上关注我。我撰写关于 Rust 和 Python 中的科学编程、机器学习和统计学的文章。我通常每个月写一篇文章。
通过从零开始构建交叉熵来理解策略梯度
我们如何训练模型的统一视角
·
关注 发表在 Towards Data Science · 16 分钟阅读 · 2023 年 6 月 11 日
–
强化学习 (RL) 可以做出令人惊叹的事情。最近,ChatGPT 通过 PPO 进行微调,PPO 是一种叫做 策略梯度 (PG) 的强化学习算法的变种。理解 RL,特别是策略梯度,可能并不简单,特别是如果你像我一样喜欢把握直觉的话。在这篇文章中,我将探讨一系列思路,这些思路确实帮助我从更熟悉的监督学习环境出发,深入理解 PG。
摘要
-
我们将从设计一个简单的监督训练程序开始,通过奖励+1 来对二分类机器人进行正确答案的训练
-
我们将为该过程制定目标
-
我们将推导出该过程的梯度上升公式(这将与使用交叉熵的梯度下降过程相同)
-
我们将把我们的过程与 RL 设置进行比较,并将我们的梯度上升与策略梯度联系起来
谁应该阅读这个?
-
我的目标是提供一种友好且直观的方式来理解 PG。如果你对 RL 问题设置有一个大致了解,并且知道 PG 的高级概念,将会很有帮助。
-
我希望帮助你更好地理解 RL 与 PG 以及监督 ML 之间的关系。因此,如果你了解如何用交叉熵损失函数训练一个监督 ML 算法,将会非常有帮助。
为什么写这篇文章?
策略梯度
在 RL 问题中,代理与环境互动以学习策略。策略告诉代理在不同状态下该做什么以最大化奖励。
作者提供的图像
PG 的想法似乎很简单明了。
-
指导时间t上代理行为的策略是π_θ(a_t|s_t)。
-
这是一种函数(通常是神经网络),具有参数θ。
-
它接收状态信息 s_t 并输出一个采取行动的概率分布 a_t。
-
然后它接收奖励 r(s_t, a_t)。
-
当我们拥有许多这样的动作和奖励周期的历史时,我们可以更新参数θ以最大化由π_θ生成的动作所带来的预期奖励。
我们如何进行更新?通过…梯度!我们通过以下梯度更新生成π_θ的模型
有些东西感觉不对劲
这看起来非常熟悉。当我们在传统的监督学习中训练神经网络模型时,我们也通过执行第二行操作即梯度下降来更新模型参数(在 PG 情况下,技术上是梯度上升,因为我们在最大化目标)。
但这也感觉非常不同。如果你查看它的推导过程,你会发现推导这个方程需要一点努力。这与我们在监督学习中更直观的做法非常不同:将输入提供给神经网络,得到输出,与目标进行比较并计算损失函数,点击反向传播按钮,就完成了!
对我来说,对数项总是似乎突然出现。尽管上述链接中的同一在线课程讲解了如何得到对数项,但过程似乎只是一堆正确但缺乏动机的数学。
从监督学习中具体的区别是什么?深入探讨这个问题可以很好地理解策略梯度。此外,它也是对我们每天做的一些熟悉的监督学习本质的良好提醒。
从头开始构建交叉熵
如果我们用一些在监督学习中使用的损失函数来分析,它们会立即“显得合理”。但要理解它们的来源则需要更多的努力。例如,经典的均方误差直观上很合理:它只是最小化预测与目标之间的距离。但有这么多距离度量,为什么选择平方距离?你必须深入了解均方误差是做最大似然估计并假设基础总体分布为正态分布的副产品。
同样地,我们日常使用的另一个经典损失函数是交叉熵。虽然有很多关于交叉熵的良好解释,让我们尝试从最基本的方式构建它。
让我们训练一个分类机器人!
假设你想训练一个机器人来分类狗和猫的图像。直观上,通过奖励正确答案并惩罚(或不奖励)错误答案来训练它是合理的。具体方法如下:
- 你给机器人一张图片。我们称之为s。这张图片是从总体分布D_s中采样的。
狗图像来源:Unsplash;其他部分由作者提供
-
如果机器人认为这是狗的图像(动作a_dog)或这是猫的图像(动作a_cat),它将给你一个答案。
-
机器人根据图像有自己的预测,即图像是狗还是猫的概率:π_θ(a|s) = (a_dog, a_cat)。例如,*π_θ(a|s) = (0.9, 0.1)*意味着它认为有 0.9 的概率是狗,0.1 的概率是猫。
狗图像来源:Unsplash;其他部分由作者提供
- 但每次机器人只会给你一个明确的答案。它要么说“这是狗” (a_dog),要么说“这是猫” (a_cat)。每次它给你一个回应时,回应(动作)是从分布中随机采样得到的,由*π_θ(a|s)*产生:a = (a_dog, a_cat) ~ π_θ(a|s)。
狗图像来源:Unsplash;其他部分由作者提供
- 当机器人正确回答时,你将奖励它(可能给它一个小奖励?),奖励值为 1。(r(s,a) = 1)。当回答错误时,则没有奖励(0 奖励)。(r(s,a) = 0)
狗图像来源:Unsplash;其他部分由作者提供
猫图像来源:Unsplash;其他部分由作者提供
这是我在第一次学习监督学习时想到的过程。当它正确时给予奖励。当它错误时(或在我们设计的训练过程中没有奖励)给予惩罚。这可能是训练某物最直观的方式。
最大化目标
我们的目标是什么?我们希望它的响应尽可能正确。更准确地说,我们希望找到最优参数θ,使得生成的π_θ(a|s),在所有可能的s(从图像总体分布D_s中采样)和a(从由模型*π_θ(a|s)生成的分布中采样)中,能够获得**每对(s,a)出现的概率加权的最大平均奖励*:
换句话说,我们在最大化定义为
目标的梯度
现在我们有了一个目标函数,我们可以尝试通过…梯度上升来最大化它!也就是说,我们可以通过迭代进行
但我们应如何计算梯度,即J对θ的导数?这在这种情况下有点棘手,因为
-
我们希望对其求导的函数是一个期望。
-
如果期望不是关于依赖于θ的分布,那么通过期望的线性性,我们可以直接对期望内部的内容进行求导,并将期望保留在那里。然而,在这种情况下,期望是关于*(s,a) ~ (D_s, π_θ(a|s))的,这依赖于θ*。因此,导数并不明显。
-
另一种思考方式是,J(θ)的值随着我们从由部分由θ决定的分布中采样*(s,a)的频率变化而变化。我们希望更频繁地出现s=dog image和a=a_dog*(猫的类似对)。当我们进行梯度上升时,我们如何捕捉向这个方向变化的θ?
此外,理想情况下,我们希望梯度呈现以下形式
这是因为你通过机器人与您的交互样本来训练机器人。每个样本包含一个*(s,a,r)三元组。因此,我们可以通过对收集到的N*个样本进行平均来近似这个梯度(根据大数法则,即进行随机梯度上升):
然后我们可以通过进行梯度上升来进行优化
现在让我们找到f。
寻找梯度
总结一下,我们希望从(1)开始,得到(2),对于某个f(θ,s,a,r)。
首先,让我们用期望的定义重写(1):
这基本上是对所有可能的*(s,a)*对的奖励按概率加权的积分。
那么,一个*(s,a)对的联合概率P(s,a)究竟是多少?我们可以将其分解为图像样本(s)出现的概率和机器人随机选择动作a*的概率。
由于机器人从其内部预测模型 π_θ(a|s) 中随机选择动作 a,我们有
在括号内的所有项中,只有 π_θ(a|s) 依赖于 θ。其他项都是常数。因此,我们可以将梯度操作移动到积分符号内,并得到
注意,我们也可以写出以下内容。这里没什么大不了的。只是将原始左边的内容乘以以分数形式写出的 1,并调整项。
替换回去,并稍微调整一下,我们得到
P(s)π_θ(a|s) 看起来很熟悉。这正是我们之前分解的 P(s,a)!将其放回去,我们得到
现在我们有一个积分和 P(s,a),我们可以…将其适配回期望的定义!
这正是我们在(2)中想要得到的形式,其中 f 是括号内的项!
你可能会想,为什么我们在之前的繁琐分数中重写了*π_θ(a|s)的梯度?其目的是创建一个π_θ(a|s)*项(我们在求导时丢失了它),以便我们可以再次生成一个 P(s,a) 项,并将积分重新转化为期望!
构建交叉熵
现在是魔法时刻。
不相信我?使用链式法则从右手边到左手边进行工作。([可选] 旁注:如果你对策略梯度公式中对数项的动机感到困惑,这实际上是简化我们得到的繁琐方程的副产品,旨在提取一个 π_θ(a|s) 项,将事物转回期望。)
所以我们可以稍微简化*J(θ)*的梯度:
所以每次我们有一批*(s,a)*作为样本时,可以通过
为了将其转化为更熟悉的形式,将梯度符号移到求和外部,我们有
我们还会通过进行以下操作来反转符号
这让你想起什么吗?让我们将其与在交叉熵损失上进行梯度下降时所做的事情进行比较。
记住,交叉熵损失是
其中y_i是真实标签,是一个描述图像是猫还是狗的独热向量(y_i_1, y_i_2,要么是(0,1)要么是(1,0))。y_hat_i是模型的预测,是一个向量(y_hat_i_1, y_hat_i_2),其中两个条目的和为 1。
当我们对这个损失函数进行梯度下降时,我们计算批次的交叉熵损失函数,并点击反向传播按钮:
这个表达式与我们之前推导出的梯度上升表达式之间的区别是
用语言描述,就是:在样本x_i上,y_i
-
模型做出预测(y_hat_i_1, y_hat_i_2)给定x_i
-
模型从预测分布中随机采样响应
-
我们奖励响应 1 的y_i_1,并且对响应 2 的y_i_2进行奖励。
-
由于当标签为类别 1 时,y_i_1 = 1, y_i_2 = 0,我们在模型正确响应 1 时奖励模型 1 分,而在模型错误响应 0 时没有奖励。类别 2 的情况也是如此。
这正是我们一直在做的事情!
所以总结一下,
-
我们设计了一个简单的训练设置,在这个设置中,我们奖励 机器人当其正确回答时得 1 分,当其回答错误时得 0 分。
-
我们总结了我们希望在目标函数中实现的内容,该目标函数描述了机器人根据其响应的机会加权所获得的奖励。
-
我们找到梯度下降过程以最大化这个目标函数
-
然后我们得到……我们在通过计算交叉熵损失然后进行反向传播训练模型时使用的确切过程!
回到强化学习
现在让我们把焦点重新放回到强化学习设置上。RL 与监督学习设置之间的区别是什么?
多个时间步长
第一个区别是 RL 通常涉及多个状态和多个回合。在我们的设置中,机器人从图像输入开始,即状态s。在机器人基于预测给出答案并收集奖励后,机器人与您的互动就结束了。
相反,在 RL(强化学习)问题中,智能体通常在多个回合中与环境互动,且在初始状态后可能过渡到其他状态。
目标函数变为
用语言描述,我们最大化所有时间步长的平均奖励总和,对所有可能的状态和动作序列(轨迹)加权,加权由每个轨迹发生的概率决定,当动作由参数θ决定时。
注意,p_θ是一个状态和动作序列的联合分布,当动作由代理的模型参数θ决定时。在每个时间步,代理的动作由π_θ(a_t|s_t)决定,其中π_θ是一个以θ为参数的模型。p_θ是一个高级抽象,表示当代理根据π_θ做出决策时,状态和动作序列发生的概率(即p_θ是理论上代理在轨迹上采取行动的频率的占位符。另一方面,π_θ(a|s)是代理在特定时间步采取某个动作的概率。我们实际上不容易知道p_θ的值,因此稍后我们将用实际知道的模型输出*π_θ(a|s)*来重写它)。
让我们与之前的目标进行比较:
主要区别如下:
-
我们计算一个* s 和a*序列上的期望,而不是仅仅一个对。
-
我们最大化轨迹中所有时间步的奖励总和,而不仅仅是来自图像和回答的单一时间步奖励。
比较梯度公式:
我们可以对这个目标做类似的操作,推导出我们可以在每个时间步更新θ的梯度。
回顾一下,我们的目标是以以下形式找到某些f的*J(θ)*的梯度。
当我们获得一批样本序列s_1, a_1, r_1, … s_T, a_T, r_T时,我们可以通过随机梯度上升更新θ:
为了简化,我们将状态序列记作一个变量τ。
所以我们希望最大化以下目标函数:
我们可以做类似的操作:
- 用积分表示期望。
- 对仅涉及θ的项p_θ(τ) 求导。
- 将*p_θ(τ)的梯度重写为* p_θ(τ)和其他东西的乘积,以恢复定义期望的形式。
所以我们得到:
看!这正是我们想要找到的。换句话说,这意味着我们正在将θ 更新为样本τ的对数概率梯度的方向,权重是沿样本τ的总奖励。这正是策略梯度的公式。
如果我们从早期的交叉熵类比延伸过来,奖励的总和基本上是轨迹的标签,而 p_θ(τ) 是模型预测下 τ 发生的可能性。训练过程 鼓励模型预测与不同轨迹 τ 上的奖励分布相似的分布。(这实际上是一个数学上准确的陈述 [如果我错了请纠正我]。如果你知道 KL 散度,可以将所计算的梯度与 KL 散度进行比较)。
我们可以对条件概率和 p_θ(τ) 的定义进行更多的操作。这个过程在这个视频(大约在 9:27)中讲解得很好。我们最终得到以下内容,将 p_θ(τ) 重新表示为 π_θ(a_t|s_t),这是我们实际知道其值的:
注意 当 T = 1(单次实验),这与我们之前设置中获得的梯度是一样的。换句话说,监督学习是强化学习的一个特殊情况,其中只有一个实验,奖励是非随机的(见下一节)。
另一个区别:奖励的估计
强化学习与监督学习之间的另一个区别是我们可以多大程度上相信奖励。在监督学习中,奖励是与图像样本一起提供的真实标签。我们通常 100% 确定奖励是正确的,我们的机器人会根据这些标签调整其行为。
然而,在强化学习问题中,奖励可能 更具随机性(想象一下你玩游戏时,可能在同一个地方两次但得到不同的分数)。因此,我们必须 估计特定状态-动作对的奖励,通过与环境互动并利用历史奖励来进行估计。
[可选] 附带想法:我还在思考是否存在监督学习(标签/奖励是 100% 可相信的)和强化学习(奖励更具随机性)之间的中间领域。当标签有噪声(包含一些错误标签)时,我们是否有点像处于中间?所以, 伪标签方法 是否与强化学习问题有一些相似之处?请告诉我你的想法。
从长远来看,我们应该有足够的历史奖励来理解平均奖励行为,但在短期内,小样本数量可能会产生 不稳定 的偏差估计。
更糟糕的是,由于代理行为是通过收集的奖励来更新的,如果我们收集到低质量的奖励,我们可能会陷入并停留在一个糟糕的策略中。要从那里走出来并重新回到正确的轨道上需要很长时间。
这是强化学习中的一个挑战,仍然是一个正在进行的研究领域。 对奖励进行一些操作 和变体,如 TRPO 和 PPO,旨在更好地解决这个问题,并且比普通 PG 使用得更为广泛。
[可选] 另一种思考:与序列监督机器学习的比较
我们的监督机器学习设置与 RL 之间的一个区别是 RL 通常涉及多个时间步。我立刻有一个问题:那么 RL 与训练像 Transformer 或 LSTM 这样的序列模型有什么不同?
这个问题的答案绝对取决于你最喜欢的序列模型的训练损失设计。
现在,假设你训练一个序列模型 f(x_1,x_2,…x_T) 以预测 y_1, y_2…y_T。例如,在机器翻译任务中,x 可能是输入英文句子的单词,而 y 是输出法文句子的单词(每个 x_t, y_t 是单词的一个独热向量表示)。
我们通过对每个样本的每个单词输出预测与真实标签之间的交叉熵之和来计算损失函数。然后,我们对一批样本进行平均,并像下面这样进行反向传播。
放回到策略梯度公式中,对我来说,这与计算目标函数的梯度相同
这种公式与 PG 公式的区别在于,我们没有将所有时间步的预测的对数概率之和与所有步骤的奖励之和相乘。相反,我们取每个时间步的对数概率与奖励的成对乘积并将它们相加。
这去除了很多项,因此大大减少了梯度的方差,这可能是使得在监督设置中训练 Transformer/LSTM 比 RL 算法更容易的原因?(除了监督设置中的非随机奖励)。
这个视频 中介绍了一种减少 PG 方差的技术:将 PG 中所有时间步的奖励总和更改为未来奖励(即从 t’ = t 到 t’ = T 的总和)。这与 PG 与在监督设置中训练 Transformer/LSTM 之间的不同具有相似的风味。虽然未来奖励方法使得代理能够通过可能的未来奖励评估每个状态,但我们是否可以说监督序列训练使得模型仅关注当前时间步的正确性?
此外,我尝试从这个梯度表达式中倒推,找到导致这个梯度表达式的原始 J(θ),以便我们可以更直接地解释监督序列训练的目标。但我在半途中卡住了。如果你有任何想法,请告诉我。
致谢
策略梯度与交叉熵之间的联系并非我自己原创的想法。感谢这篇文章给了我拓展思路的启发,让我从更根本的角度理解交叉熵和策略梯度的作用。
理解 SQL 注入并学习如何在 Python 中使用 SQLAlchemy 避免它
学习在 Python 中以安全的方式与数据库交互
·发布在 Towards Data Science ·5 分钟阅读·2023 年 4 月 12 日
–
图片来自 Pixabay 的 mohamed_hassan(Hosting Web Man)
SQL 注入是最常见且最危险的网络安全漏洞之一,它允许黑客将恶意 SQL 代码注入到未经验证和清理的纯 SQL 查询中。这也是新开发人员常常忽视的一个问题。
SQL 注入的原因和解决方案其实非常简单。在这篇文章中,我们将通过一些简单的查询来探索 SQL 注入,并假装成为攻击者来利用我们的数据库。在文章的最后,你将完全理解 SQL 注入,并在意识到其威力和危险后不会再犯这个错误。
准备
和往常一样,我们将使用 Docker 创建一个 MySQL 数据库:
# Create a volume to persist the data.
$ docker volume create mysql8-data
# Create the container for MySQL.
$ docker run --name mysql8 -d -e MYSQL_ROOT_PASSWORD=root -p 13306:3306 -v mysql8-data:/var/lib/mysql mysql:8
# Connect to the local MySQL server in Docker.
$ docker exec -it mysql8 mysql -u root -proot
mysql> SELECT VERSION();
+-----------+
| VERSION() |
+-----------+
| 8.0.31 |
+-----------+
1 row in set (0.00 sec)
请注意,本文中为了简化起见使用了 root 用户,但在实际应用中绝不应直接在我们的 Web 应用程序中使用。
然后,让我们创建一些数据库和表来进行测试。为了简单起见,数据集与之前系列文章中使用的相同。
CREATE DATABASE `data`;
CREATE TABLE `data`.`student_scores` (
`student_id` smallint NOT NULL,
`subject` varchar(50) NOT NULL,
`score` tinyint DEFAULT '0',
PRIMARY KEY (`student_id`,`subject`),
KEY `ix_subject` (`subject`),
KEY `ix_score` (`score`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
;
INSERT INTO `data`.student_scores
(student_id, subject, score)
VALUES
(1, 'Literature', 90),
(1, 'Math', 60),
(2, 'Literature', 80),
(2, 'Math', 80),
(3, 'Literature', 70),
(3, 'Math', 95)
;
由于我们在这篇文章中将使用 SQLAlchemy 进行数据库连接,我们需要安装必要的库。和往常一样,建议创建一个单独的虚拟环境,以便库不会影响系统及其他虚拟环境。
conda create --name sql python=3.11
conda activate sql
pip install -U "SQLAlchemy>=2.0.0,<2.1.0"
pip install -U "pymysql>=1.0.0,<1.1.0"
pip install -U "cryptography>=40.0.0,<40.1.0"
探索 SQL 注入
现在让我们创建一个简单的函数来读取一些数据:
from sqlalchemy import create_engine, text
db_url = "mysql+pymysql://root:root@localhost:13306/data"
engine = create_engine(db_url, pool_size=5, pool_recycle=3600)
conn = engine.connect()
def read_student_scores(student_id):
sql_text = text(f"""
SELECT subject, score
FROM data.student_scores
WHERE student_id = {student_id}
""")
result = list(conn.execute(sql_text))
print(result)
read_student_scores()
函数从简单的编码角度来看似乎很正常。然而,它存在一个巨大的安全问题,可能被恶意用户利用。
如果我们正常使用,它将正常工作:
read_student_scores(1)
# [('Literature', 90), ('Math', 60)]
然而,它也可能返回一些不应该由恶意用户返回的内容。黑客的第一个攻击点是返回所有记录,即使是用户不应该看到的记录:
read_student_scores('-1 OR 1')
# [('Literature', 90), ('Math', 60), ('Literature', 80), ('Math', 80), ('Literature', 70), ('Math', 95)]
这是可能的,因为 read_student_scores()
函数没有清理和验证输入参数,而是将输入数据与原始查询简单地拼接在一起。
这对于许多开发者来说并不罕见。实际上,我见过不少以这种方式编写的遗留代码。很幸运的是,它们之前没有被黑客攻击。或者说,也许已经被黑过了……
SQL 注入可能比上面所示的更具危害性,实际上黑客可以返回任何信息。
现在,让我们假装自己是恶意用户,尝试获取一些不应该由此函数返回的信息。
黑客首先想知道的是返回了多少列。在这个示例中,很明显返回了两列。然而,当输出通过某些用户界面显示时,这可能不是那么明显。
有很多方法可以猜测返回了多少列,两种常见的方法是使用 ORDER BY
和 UNION
。让我们看看它是如何工作的:
read_student_scores('-1 ORDER BY 1')
# []
read_student_scores('-1 ORDER BY 2')
# []
read_student_scores('-1 ORDER BY 3')
# OperationalError: (pymysql.err.OperationalError) (1054, "Unknown column '3' in 'order clause'")
从上述查询、结果和错误中,我们知道返回了两列。
我们可以使用UNION
得出相同的结论:
read_student_scores('-1 UNION SELECT 1')
# OperationalError: (pymysql.err.OperationalError) (1222, 'The used SELECT statements have a different number of columns')
read_student_scores('-1 UNION SELECT 1,2')
# [('1', 2)]
使用 UNION
我们能够通过较少的测试猜测正确的列数。实际上,UNION
是最常用的黑客工具之一,用于攻击数据库。
让我们尝试读取一些正常情况下不应该返回的内容:
read_student_scores('-1 UNION SELECT DATABASE(), @@VERSION')
# [('data', '8.0.31')]
数据库名称和版本被返回了!
让我们看看更可怕的情况:
read_student_scores('-1 UNION SELECT user, authentication_string FROM mysql.user')
# [('root', '$A$005$j\x1cZ\x1aj*t\x16_aI\t.\tk\x1a0b8,6nT16rTboTxEGJsq8R.xLN1dlygQWOe12XurOijG5v9'), ('mysql.infoschema', '$A$005$THISISACOMBINATIONOFINVALIDSALTANDPASSWORDTHATMUSTNEVERBRBEUSED'), ('mysql.session', '$A$005$THISISACOMBINATIONOFINVALIDSALTANDPASSWORDTHATMUSTNEVERBRBEUSED'), ('mysql.sys', '$A$005$THISISACOMBINATIONOFINVALIDSALTANDPASSWORDTHATMUSTNEVERBRBEUSED'), ('root', '$A$005$\x0c=\x10gE\x7f]g\x18WQNnB`Y&I1\x18zPIQ3wM3cj43wk4Qq4/Tt88B0ypKrwYLYnD3BpGqfY5')]
所有数据库用户的用户名和认证字符串都被返回了!使用一些暴力猜测工具,黑客可以在短时间内破解密码,特别是当使用简单密码时。
如何避免 SQL 注入?
现在我们已经了解了 SQL 注入是什么以及它有多么危险,让我们看看如何在实践中避免它。
防止 SQL 注入的最有效方法是使用参数化查询,这可以通过 SQLAlchemy 中的 :param_name
语法实现:
def read_student_scores(student_id):
sql_text = text("""
SELECT subject, score
FROM data.student_scores
WHERE student_id = :student_id
""")
result = list(conn.execute(sql_text, parameters={"student_id": student_id}))
print(result)
请注意,本帖使用了 SQLAlchemy 2.0,因此指定参数的语法与 SQLAlchemy 1.x(通常是 1.4)中的语法会有所不同。
让我们看看使用参数化查询时恶意查询会返回什么:
read_student_scores('-1 OR 1')
# []
read_student_scores('-1 UNION SELECT DATABASE(), @@VERSION')
# []
read_student_scores('-1 UNION SELECT user, authentication_string FROM mysql.user')
# []
所有这些恶意查询返回了空结果,比之前安全得多。
在这篇文章中,我们介绍了什么是 SQL 注入,它是如何工作的,以及如何使用简单的示例来避免它。
尽管使用参数化查询可以防止大多数 SQL 注入实例,但为了使我们的应用程序更加健壮,我们还应该应用以下策略:
-
限制数据库用户查询数据库的权限。为了简单起见,本示例使用了 root 用户,但在实际应用中绝不应该使用 root 用户。实际上,我们应该为我们的 Web 应用创建一个专用的数据库用户(并使用强密码),并仅授予最低权限。
-
清理并验证输入查询。如果输入数据的类型不一致,或者包含可疑字符如井号(
#
)、分号(;
)、减号(-
),甚至是词语UNION
,应以安全、稳健且用户友好的方式处理这种情况。 -
永远不要将调试日志直接展示给最终用户。调试日志仅应供内部用户使用,因为它们可能包含敏感信息,恶意用户可能利用这些信息来利用系统。
相关文章:
实时了解您的数据
实操教程
与 bytewax 和 ydata-profiling
·
关注 发表在 Towards Data Science ·8 分钟阅读·2023 年 7 月 20 日
–
在这篇博客文章中,我们将深入探讨如何将开源流处理解决方案 bytewax* 与 ydata-profiling* 结合使用,以提升您的流处理质量。准备好了吗!
流处理允许对数据进行实时分析,无论是在传输过程中还是存储之前,并且可以是有状态的或无状态的。
有状态流处理 用于实时推荐、模式检测或复杂事件处理,其中需要处理历史数据(窗口、按键连接等)。
无状态流处理 用于内联转换,无需了解流中其他数据点,例如掩码电子邮件或类型转换。
照片由 Markus Spiske 提供,拍摄于 Unsplash
总体而言,数据流在工业中广泛使用,应用于诸如 欺诈检测、病人监控 或 事件预测维护 等用例。
所有数据流必须考虑的一个关键方面是数据质量。
与传统模型中数据质量通常在数据仓库或仪表板解决方案创建过程中进行评估不同,流数据需要持续监控。
在整个过程中,从数据收集到传递给下游应用程序,保持数据质量至关重要。毕竟,差的数据质量可能会给组织带来高昂的成本:
“对于大多数公司来说,差的数据质量的成本高达 15% 到 25% 的收入。 (…) 通过在数据质量上提前准备,可以消除其中的三分之二的成本。”
— 托马斯·C·雷德曼,《数据质量的前瞻》一书的作者
在本文中,我们将向您展示如何将 bytewax
与 ydata-profiling
结合起来,以分析和提高您的流数据质量!
使用 Bytewax 进行数据专业人士的流处理
Bytewax 是一个开源流处理框架,专为 Python 开发人员设计。
它允许用户构建流数据管道和实时应用程序,具有类似于 Flink、Spark 和 Kafka Streams 的功能,同时提供一个友好且熟悉的界面,并且与 Python 生态系统 100% 兼容。
使用内置的 连接器 或现有的 Python 库,您可以连接到实时和流数据源(Kafka、RedPanda、WebSocket 等),并将转换后的数据写入各种下游系统(Kafka、parquet 文件、数据湖等)。
对于转换,Bytewax 支持有状态和无状态的转换,通过 map、windowing 和 aggregation 方法,并具备如恢复和可扩展性等熟悉的功能。
Bytewax 提供了以 Python 为主的数据流体验,并专门为数据工程师和数据科学家而构建。它允许用户构建流数据管道和实时应用程序,并创建满足需求的自定义配置,而无需学习和维护像 Spark 或 Flink 这样的基于 JVM 的流处理平台。
Bytewax 非常适合多种使用场景,包括生成 AI 的嵌入管道、数据流中的缺失值处理、在流式上下文中使用语言模型理解金融市场等。有关用例灵感和更多信息,如文档、教程和指南,请随时查看Bytewax 网站。
为什么需要对数据流进行数据分析?
数据分析是任何机器学习任务成功的关键,指的是彻底理解我们的数据:其结构、行为和质量。
简而言之,数据分析包括分析与数据格式和基本描述符相关的方面(例如,样本数量、特征数量/类型、重复值)、其内在特征(如缺失数据或不平衡特征),以及在数据收集或处理过程中可能出现的其他复杂因素(例如,错误值或不一致特征)。
确保高数据质量标准对于所有领域和组织都至关重要,但对于那些处理持续输出数据的领域尤其相关,因为情况可能会快速变化,需要立即采取行动(例如,医疗监测、股票价值、空气质量政策)。
对于许多领域,数据分析是从探索性数据分析的角度使用的,考虑存储在数据库中的历史数据。相反,对于数据流,数据分析在流中的验证和质量控制中变得至关重要,数据需要在不同的时间帧或处理阶段进行检查。
通过将自动化分析嵌入我们的数据流中,我们可以立即获得反馈,了解当前数据状态,并在出现潜在关键问题时收到警报——无论这些问题与数据一致性和完整性(例如,数据损坏或格式变化)相关,还是与短时间内发生的事件(例如,数据漂移、偏离业务规则和结果)有关。
在现实世界中 — 你只知道墨菲定律肯定会生效,“一切都可能出错” — 自动化分析可能会帮助我们避免多个脑力难题和需要停产的系统!
关于数据分析,ydata-profiling
一直是一个热门选择,无论是表格数据还是时间序列数据。这也不足为奇——一行代码就可以进行全面的分析和洞察。
复杂且耗时的操作在后台完成:ydata-profiling 自动检测数据中的特征类型,并根据特征类型(数字或分类)调整概要统计和可视化,这些内容会在分析报告中显示。
促进以数据为中心的分析,该包还突显了特征之间的现有关系,关注它们的配对交互和相关性,并提供数据质量警报的全面评估,从重复或常量值到偏斜和不平衡特征。
这确实是对我们数据的360º视角——付出最少的努力。
分析报告:突显潜在的数据质量问题。图片来源:作者。
汇总:bytewax 和 ydata-profiling
在开始项目之前,我们需要首先设置我们的 Python 依赖项并配置数据源。
首先,让我们安装bytewax
和ydata-profiling
包(你可能需要使用虚拟环境来进行这个操作—— 查看这些说明 如果你需要额外的指导!)
然后,我们将上传环境传感器遥测数据集(许可—CC0:公共领域),该数据集包含来自不同 IoT 设备的温度、湿度、一氧化碳、液化石油气、烟雾、光线和运动的多项测量:
在生产环境中,这些测量将由每个设备持续生成,输入将类似于我们在流媒体平台例如 Kafka中预期的内容。在这篇文章中,为了模拟流数据的上下文,我们将一次从 CSV 文件中读取一行数据,并使用 bytewax 创建数据流。
(快速旁注:数据流本质上是一个可以描述为有向无环图—DAG 的数据管道)
首先,让我们进行一些必要的导入:
然后,我们定义我们的数据流对象。之后,我们将使用无状态的映射方法,在其中传入一个函数以将字符串转换为日期时间对象,并将数据重组为格式(device_id, data)。
map 方法将以无状态的方式对每个数据点进行更改。我们修改数据的形状是为了在接下来的步骤中更容易地对数据进行分组,以便分别对每个设备进行数据分析,而不是同时对所有设备进行分析。
现在我们将利用 bytewax
的有状态能力来收集在我们定义的时间段内每个设备的数据。ydata-profiling
期望获得数据的时间快照,这使得窗口操作符成为实现这一目标的完美方法。
在 ydata-profiling
中,我们能够为特定上下文指定的数据框生成汇总统计。例如,在我们的示例中,我们可以生成涉及每个 IoT 设备或特定时间段的数据快照:
在定义了快照之后,利用 ydata-profiling
就像调用每个我们想要分析的数据框的 ProfileReport
一样简单:
在这个示例中,我们将图像写入本地文件作为 map 方法中的一个函数的一部分。这些图像可以通过消息工具报告,或者将来我们可以将它们保存到一些远程存储中。一旦配置文件完成,数据流会期望一些输出,因此我们可以使用内置的 StdOutput
打印已分析的设备以及在 map 步骤中传递出的配置文件时间:
执行 Bytewax 数据流的方法有多种。在这个示例中,我们使用相同的本地机器,但 Bytewax 也可以在多个 Python 进程中运行,跨多个主机,使用 Docker 容器,利用 Kubernetes 集群,以及 更多。
在本文中,我们将继续使用本地设置,但我们鼓励你查看我们的辅助工具 waxctl,它可以在你的管道准备好过渡到生产环境时管理 Kubernetes 数据流部署。
假设我们在包含数据流定义文件的相同目录中,我们可以使用以下命令运行它:
然后我们可以使用这些分析报告来验证数据质量,检查模式或数据格式的变化,并 比较不同设备或时间窗口之间的数据特征。
实际上,我们可以利用 比较报告功能,它以简单明了的方式突出显示两个数据配置文件之间的差异,从而帮助我们更容易地发现需要调查的重要模式或必须解决的问题:
准备好探索你自己的数据流了吗?
验证数据流对于持续识别数据质量问题以及比较不同时间段数据状态至关重要。
对于在医疗保健、能源、制造和娱乐等领域处理持续数据流的组织来说,自动化分析是建立数据治理最佳实践的关键,从质量评估到数据隐私。
这需要对数据快照进行分析,如本文所示,可以通过结合bytewax
和ydata-profiling
以无缝的方式实现。
Bytewax负责处理和结构化数据流所需的所有过程,这些数据流可以汇总并通过ydata-profiling进行比较,生成数据特征的综合报告。
适当地处理和分析传入数据能够在不同领域开启许多应用场景,从数据模式和格式错误的修正到突出和缓解由现实世界活动引发的额外问题,如异常检测(例如,欺诈或入侵/威胁检测)、设备故障以及其他偏离预期的事件(例如,数据漂移或与业务规则的不一致)。
现在你可以开始探索你的数据流了!让我们知道你发现了哪些其他应用场景,随时在评论中给我们留言,或在数据驱动的 AI 社区中与我们联系,提出问题和建议!在那里见!
致谢
本文得到了 Fabiana Clemente(CDO @ YData)的支持,开发了 ydata-profiling,以及 Zander Matheson(CEO & Founder @ Bytewax)和 Oli Makhasoeva(Developer Relations @ Bytewax),两者都开发了 bytewax。你可以在相应的文档中找到有关这些开源软件包的更多信息: ydata-profiling 文档 与 bytewax 文档。
理解和减轻 LLM 幻觉
LLM 幻觉检测挑战及其在一篇重要研究论文中提出的可能解决方案。
·
关注 发表于 Towards Data Science ·8 min read·Oct 23, 2023
–
近年来,大型语言模型(LLMs)展示了令人印象深刻且不断增强的能力,包括对用户提示生成高度流畅和令人信服的响应。然而,LLMs 以生成非事实性或荒谬陈述而闻名,这种特性通常称为“幻觉”。这种特征可能会在许多需要事实性的场景中损害信任,如总结任务、生成式问答和对话生成。
检测幻觉在人类中一直是一个挑战,在 LLM 的背景下同样如此。这尤其具有挑战性,因为我们通常无法获取用于一致性检查的真实背景信息。有关 LLM 生成的附加信息,如输出概率分布,可以帮助完成这一任务。然而,这类信息往往不可用,使得任务更加困难。
幻觉检测尚未解决,仍是一个活跃的研究领域。在这篇博客文章中,我们将一般介绍任务及其挑战,并介绍在研究论文 SELFCHECKGPT: Zero-Resource Black-Box Hallucination Detection for Generative Large Language Models[1] 中提出的一种可能的方法。我们将用实际例子说明论文中提出的一些方法,并指出每种方法的一些优缺点。你可以通过访问 Google Colab Notebook 来查看这些示例。
💡 更新*:受本博客文章研究的启发,我们在* LangKit中发布了一个新功能。 response_hallucination 模块将自动计算一致性分数,帮助你了解 LLM 中幻觉响应的存在。你可以在 这个示例笔记本 中查看它。
本博客将涵盖:
-
什么是 LLM 幻觉
-
方法:SelfCheckGPT
-
一致性检查
1. BERTScore
2. 自然语言推理
3. LLM 提示
-
实验
-
结论
-
参考文献
什么是 LLM 幻觉
在自然语言生成中,幻觉可以定义为“生成的内容是无意义的或不忠于提供的源内容”[2]。源内容可以代表不同任务的不同功能。例如,对于摘要任务,源内容是被摘要的输入文本,而对于生成式问答任务,源内容被认为是世界知识[2]。我们来关注问答任务,并以用户问题和 OpenAI 的 GPT 模型(text-davinci-003)给出的答案为例:
用户提示:菲利普·海沃斯是谁?
LLM 响应:菲利普·海沃斯是英国的一名律师和政治家,曾于 1859 年至 1868 年担任特特福德的国会议员。
初看起来,这似乎是一个连贯的回答,但过一段时间,我们可能会问一个非常合理的问题:这是真的吗?
不过,我们并没有足够的信息来回答这个新问题。我们没有可以用来验证陈述的经过验证的事实的背景。LLM 通常在大量文本中进行训练,因此很难验证陈述是否基于特定的信息来源。快速的 Google 搜索没有显示出匹配回答的人的任何信息。什么确切构成幻觉仍然没有很好地建立,但这个例子可以被理解为幻觉,因为这个名字、地点和职业的主题可能根本不存在。
让我们再试一次另一个问题:
用户提示:比尔·盖茨是谁?
LLM 回应:比尔·盖茨是技术企业家、投资者和慈善家。他是微软公司的共同创始人,也是世界上最富有的人之一。
好的,这些陈述更容易验证,并且通过快速检查维基百科得到了支持,所以这不太可能是幻觉。在训练过程中,LLM 很可能见过很多关于比尔·盖茨的陈述,因此“比尔·盖茨是 _”之后的令牌很可能会以较高的信心生成。另一方面,LLM 可能对“Philip Hayworth 是 _”之后使用哪些词不太确定。这一见解使我们能够将不确定性与真实性联系起来,因为事实句子通常会包含预测概率较高的令牌,而幻觉句子则不然。然而,对于许多案例,我们可能没有手头的输出概率分布。
本次会议的示例和内容基于原始论文[1],我们将在接下来的章节中继续探索论文的方法。
方法:SelfCheckGPT
在上一节中,我们考虑了我们方法的两个重要因素:访问外部背景和访问 LLM 的输出概率分布。当一种方法不需要外部背景或数据库来进行一致性检查时,我们可以称其为零资源方法。类似地,当一种方法只需要 LLM 生成的文本时,可以称之为黑箱方法。
我们在这篇博客文章中要讨论的方法是一种零资源黑箱幻觉检测方法,基于这样一个前提:对相同提示的采样回答对于幻觉事实可能会出现分歧和矛盾,而对于事实陈述则可能会相似和一致。
让我们重新审视之前的例子。为了应用检测方法,我们需要更多的样本,所以让我们再向 LLM 提出三个相同的问题:
作者提供的表格
确实,答案相互矛盾——有时,Philip Hayworth 是一位英国政治家,而在其他样本中,他是澳大利亚工程师或美国律师,他们生活和行动于不同的时期。
让我们以比尔·盖茨的例子进行比较:
表格作者提供
我们可以观察到,比尔·盖茨分配的职业、组织和特征在样本之间是一致的,使用了相等或语义相似的术语。
一致性检查
现在我们有了多个样本,最后一步是进行一致性检查——确定答案是否彼此一致。这可以通过多种方式完成,所以让我们探索一下论文中提出的一些方法。你可以通过查看这个 Google Colab Notebook 自行执行代码。
BERTScore
执行此检查的一种直观方法是测量样本之间的语义相似度,而 BERTScore[3] 是一种实现方式。BERTScore 为候选句子中的每个词与参考句子中的每个词计算相似度分数,以计算句子之间的相似度分数。
在 SelfCheckGPT 的背景下,分数是逐句计算的。原始答案的每个句子将与给定样本的每个句子进行评分,以找到最相似的句子。这些最大相似度分数将在所有样本中进行平均,从而为原始答案中的每个句子得到最终的幻觉分数。最终分数需要趋近于 1(表示不相似的句子)和 0(表示相似的句子),因此我们需要从 1 中减去相似度分数。
让我们展示如何用原始答案的第一个句子与第一个样本进行检查:
图片作者提供
第一个样本的最高分是 0.69。重复对剩余两个样本的处理,并假设其他最高分为 0.72 和 0.72,那么我们对该句子的最终分数将是 1 — (0.69+0.72+0.72)/3 = 0.29。
使用语义相似度来验证一致性是一种直观的方法。其他编码器也可以用于嵌入表示,因此这也是一种可以进一步探索的方法。
自然语言推理
自然语言推理是确定蕴涵的任务,即根据前提[4]判断一个假设是否为真、假或未确定。在我们的案例中,每个样本用作前提,每个原始答案的句子用作我们的假设。通过对每个句子的样本分数进行平均,得到最终分数。蕴涵通过对 Multi-NLI 数据集[5] 进行微调的 Deberta 模型来执行。我们将使用归一化预测概率来计算分数,而不是实际类别,如“蕴涵”或“矛盾”。[6]
蕴涵任务更接近我们的一致性检查目标,因此我们可以期待为此目的微调的模型会表现良好。作者还在 HuggingFace 上公开分享了该模型,其他 NLI 模型也公开可用,使得这种方法非常容易获取。
LLM Prompt
考虑到我们已经使用 LLM 来生成答案和样本,我们不妨使用 LLM 来执行一致性检查。我们可以对每个原始句子和每个样本进行一致性检查,将 LLM 作为我们的上下文。下面的图片,来自原始论文的仓库,说明了如何进行这个操作:
SELFCHECKGPT WITH LLM PROMPT. 来源: HTTPS://GITHUB.COM/POTSAWEE/SELFCHECKGPT/TREE/MAIN
最终得分可以通过将“否”赋值为 1,“是”赋值为 0,“不适用”赋值为 0.5,并对样本的值进行平均来计算。
与其他两种方法不同,这种方法需要额外调用你选择的 LLM,这意味着额外的延迟和可能的额外成本。另一方面,我们可以利用 LLM 的能力来帮助我们进行检查。
实验
让我们看看在三种方法中讨论的两个示例的结果如何。
作者提供的表格
这些值仅用于说明方法。只有三个句子的情况下,它不应该用来比较和确定哪种方法最佳。为此,原始论文在论文的仓库中分享了实验结果 这里,包括了在这篇博客中未讨论的附加版本。我不会详细讨论结果,但根据所有三个指标(NonFact、Factual 和 Ranking),LLM-Prompt 是表现最好的版本,其次是 NLI 版本。BERTScore 版本明显比剩余两个版本要差。我们的简单示例似乎符合共享结果的方向。
结论
我们希望这篇博客文章有助于解释幻觉问题,并提供一种可能的幻觉检测解决方案。这是一个相对较新的问题,很高兴看到已经有努力在解决它。
讨论的方法具有不需要外部上下文(零资源)和不需要 LLM 的输出概率分布(黑箱)的优点。然而,这也带来了成本:除了原始响应外,我们还需要生成额外的样本来执行一致性检查,从而增加了延迟和成本。一致性检查还需要额外的计算和语言模型来将响应编码为嵌入,进行文本蕴含,或查询 LLM,这取决于所选的方法。
参考文献
[1] — Manakul, Potsawee, Adian Liusie, 和 Mark JF Gales。“Selfcheckgpt:用于生成大型语言模型的零资源黑箱幻觉检测。” arXiv 预印本 arXiv:2303.08896 (2023)。
[2] — JI, Ziwei 等人。《自然语言生成中的幻觉调查》。ACM 计算调查,第 55 卷,第 12 期,页码 1–38,2023 年。
[3] — ZHANG, Tianyi 等人。Bertscore:使用 bert 评估文本生成。arXiv 预印本 arXiv:1904.09675,2019 年。
[4] — nlpprogress.com/english/natural_language_inference.html
[5] — Williams, A., Nangia, N., & Bowman, S. R. (2017)。用于通过推理理解句子的广泛覆盖挑战语料库。arXiv 预印本 arXiv:1704.05426。
[6] — github.com/potsawee/selfcheckgpt/tree/main#selfcheckgpt-usage-nli