使用 reactable 在 R 中重新创建出版物质量的交互式表
复制《经济学人》的表格,只在 R
《经济学人》最近发布了一系列关于“超额死亡率”的国家级数据集,超额死亡率一词用于描述“死于任何原因的总人数与同一地点同一时间的历史平均值之间的差距。”更简单地说,这种方法捕捉到了发生了多少不该发生的死亡事件。
(免费中!)的文章,我偶然发现了下表:
我以为桌子是干净的,发出了明确的信息。内联条形图的添加不会造成干扰,但仍有助于读者了解数据。这是一张相当漂亮的桌子。最近偶然看到 Greg Lin 的包[reactable](https://glin.github.io/reactable/index.html)
,我想这可能是一个很好的机会来尝试重新创作上述作品。
(巧合的是,当我在做这个项目的时候,Malcolm Barrett 发布了一个类似的博客帖子,记录了他使用gt
重新制作 NYT 桌子的过程。看看吧!)
装载包
我们的过程使用标准的包:reactable
(显然),htmltools
作为它的伙伴,lubridate
用于日期和时间,hrbrthemes
用于经济学家的字体,tidyverse
用于通用数据争论。
library(reactable)
library(htmltools)
library(lubridate)
library(hrbrthemes)
library(tidyverse)
收集数据
如果您对数据收集和清理过程不感兴趣,您肯定可以跳过这一步。
不幸的是,这个项目中最耗时的步骤之一是获取与《经济学家》杂志文章中使用的格式相同的数据。他们发布的数据以一系列国家级 CSV 的形式出现。虽然这有助于国家级别的分析,但这意味着我们必须将数据修改为连接格式才能创建一个表。
让我们从创建一个函数开始,该函数读入每个单独的 CSV ,选择相关的列,并将特定的数据帧存储在全局环境中。
create_dataframe <- function(country) {
## for URL (below)
country <- str_replace(country, " ", "_")
## read in CSV, given country parameter
data <-
readr::read_csv(
paste0(
'[https://raw.githubusercontent.com/TheEconomist/covid-19-excess-deaths-tracker/master/output-data/excess-deaths/'](https://raw.githubusercontent.com/TheEconomist/covid-19-excess-deaths-tracker/master/output-data/excess-deaths/'), country, '_excess_deaths.csv'
)
)
## select relevant columns
data <- data %>%
select(
country,
region,
start_date,
end_date,
population,
total_deaths,
covid_deaths,
expected_deaths,
excess_deaths,
non_covid_deaths
)
assign(country, rbind(data), envir = .GlobalEnv)
}
创建了这个函数后,我们想对经济学人包含的每个国家进行循环。
为此,我们从 GitHub 获取他们的来源列表,并将每个国家/地区纳入一个列表:
country_names <-
readr::read_csv(
'[https://raw.githubusercontent.com/TheEconomist/covid-19-excess-deaths-tracker/master/source-data/list_of_sources.csv'](https://raw.githubusercontent.com/TheEconomist/covid-19-excess-deaths-tracker/master/source-data/list_of_sources.csv')
) %>%
select(country) %>%
distinct() %>%
mutate(country = stringr::str_to_lower(country)) %>%
filter(country != 'all') %>%
pull()
然后,我们循环!
for (country in country_names) {
create_dataframe(country)
}
现在,我们有一个数据框架列表,每个框架包含一个国家的超额死亡率数据。
最后,我们将这些新数据帧合并成一个主数据集。这里,我们在dfs
中定义了一个全局环境中属于结构化数据框的所有对象的列表。然后,我们一起rbind
他们!
dfs = sapply(.GlobalEnv, is.data.frame)data <- do.call(rbind, mget(names(dfs)[dfs]))
但不幸的是,这还不是全部。我们需要过滤我们的数据,只包括在经济学家的表中的地方。更困难的是,该表的标识行被命名为“地区/国家”,并包括来自 CSV 中两个单独的行的数据。
让我们首先根据经济学家所包含的国家和地区进行手动定义和过滤。(这个选择似乎没有一个顺序;因此,它必须是手动的)。
good_countries <-
c("Britain",
"Spain",
"Italy",
"France",
"Netherlands",
"Belgium",
"Sweden",
"Austria")good_regions <- c("New York City", "Istanbul", "Jakarta")data_filtered_countries <- data %>%
filter(country %in% good_countries) %>%
filter(country == region)
因为表格中只有一行国家/地区,并相应地对它们进行分组,所以我们可以将data_filtered_regions
数据框中的country
变量替换为region
。
data_filtered_regions <- data %>%
filter(region %in% good_regions) %>%
# replace for the sake of the table
mutate(country = region)
并合并:
data_filtered <-
rbind(data_filtered_countries, data_filtered_regions)
接下来,我们注意到表格标题显示“自地区/国家前 50 例 covid 死亡以来的超额死亡率*”这意味着我们需要排除在一个地区有 50 例 COVID 死亡之前的超额死亡数。*
data_filtered <- data_filtered %>%
group_by(country) %>%
mutate(csum = cumsum(covid_deaths))
此时(仅选择相关列后),我们的数据如下所示:
data_filtered %>%
select(country, start_date, end_date, covid_deaths,
excess_deaths, covid_deaths, csum) %>%
reactable()
我们需要根据与新冠肺炎相关的总死亡人数和超额死亡人数对每个国家进行分组。然后,利用这两个数字,我们计算出新冠肺炎造成的超额死亡的百分比。这可以作为一个国家新冠肺炎病例漏报的衡量标准。
data_for_table <- data_filtered %>%
filter(excess_deaths > 0) %>%
group_by(country) %>%
summarise(
excess_deaths = round(sum(excess_deaths)),
covid_deaths = round(sum(covid_deaths)),
perc = covid_deaths / excess_deaths
) %>%
select(country, covid_deaths, excess_deaths, perc)reactable(data_for_table, pagination = FALSE)
此时唯一缺少的是日期范围。为了找到并显示日期,我们需要找到给定国家/地区达到 50 个新冠肺炎病例后的第一个日期和该国家/地区数据中的最后一个日期。
我们如何做到这一点?首先,我们将创建一个名为append_date_suffix
的函数,它根据给定的日期添加适当的后缀。
append_date_suffix <- function(dates) {
suff <- case_when(
dates %in% c(11, 12, 13) ~ "th",
dates %% 10 == 1 ~ 'st',
dates %% 10 == 2 ~ 'nd',
dates %% 10 == 3 ~ 'rd',
TRUE ~ "th"
)
paste0(dates, suff)
}
然后,我们将按country
变量分组,并找到最小和最大日期(最小日期仅在一个国家出现 50 例 COVID 死亡后出现)。然后,我们对单个的日期和月份进行大量的格式化,并以经济学家的风格将它们用破折号附加在一起。抱歉,这里发生了很多事。
dates_data <-
data_filtered %>%
# only looking at date ranges starting post-50 deaths
filter(csum > 50) %>%
group_by(country) %>%
summarise(start_date = min(start_date),
end_date = max(end_date)) %>%
mutate(
clean_start_day = format(start_date, "%d"),
clean_start_day = append_date_suffix(as.numeric(clean_start_day)),
clean_start_month = format(start_date, "%b"),
clean_end_day = format(end_date, "%d"),
clean_end_day = append_date_suffix(as.numeric(clean_end_day)),
clean_end_month = format(end_date, "%b")
) %>%
mutate(
clean_range = paste0(
clean_start_month," ", ## Mar
clean_start_day, "-", ## 6-
clean_end_month, " ", ## May
clean_end_day ## 18
)
) %>%
select(country, clean_range)
这将创建如下所示的日期范围:
将这些日期与我们现有的数据结合起来…
data_for_table <- data_filtered %>%
filter(excess_deaths > 0) %>%
group_by(country) %>%
summarise(
excess_deaths = round(sum(excess_deaths)),
covid_deaths = round(sum(covid_deaths)),
perc = covid_deaths / excess_deaths
) %>%
left_join(dates_data, by = 'country') %>%
select(country, clean_range, covid_deaths, excess_deaths, perc)
我们得到了最终的数据集:
创建表
最后,我们准备好获取数据集并创建我们的表。我们可以从定义一些参数开始,这些参数使表格更容易使用,更美观。在这里,我们根据超额死亡进行排序(但不包括箭头),使其紧凑,并在一页上显示所有结果。
reactable(
data_for_table,
defaultSortOrder = 'desc',
defaultSorted = 'excess_deaths',
showSortIcon = FALSE,
compact = TRUE,
pagination = FALSE)
样式标题
接下来,让我们使列标题在风格上类似于 The Economist 。我们使用 reactable 的defaultColDef
来实现这一点,在这里我们定义了一个带有标题和常规单元格样式的colDef
。在这里,我们可以包括 CSS(您可以通过检查手边的表找到它)。在这篇文章中,你会注意到我经常提到font_es
。这是来自鲍勃·鲁迪斯的《T4》。它包含经济学人 Sans Condensed 的字体名称,这是经济学人使用的字体!
reactable(
data_for_table,
defaultSortOrder = 'desc',
defaultSorted = 'excess_deaths',
showSortIcon = FALSE,
compact = TRUE,
pagination = FALSE,
######## NEW ########
defaultColDef = colDef(
### define header styling
headerStyle = list(
textAlign = "left",
fontSize = "11px",
lineHeight = "14px",
textTransform = "uppercase",
color = "#0c0c0c",
fontWeight = "500",
borderBottom = "2px solid #e9edf0",
paddingBottom = "3px",
verticalAlign = "bottom",
fontFamily = font_es
),
### define default column styling
style = list(
fontFamily = font_es,
fontSize = "14px",
verticalAlign = "center",
align = "left"
)
)
)
格式化列
现在,我们可以开始适当地格式化特定的列。最简单的三栏是地区/国家、时间段、新冠肺炎占总数的百分比。在每一列中,我们都创建了一个colDef
,它定义了列名以及一些样式。
您会注意到在我们的百分比栏中增加了JS
。这允许我们在列和列标题中包含 JavaScript。我用它来做一些简单的事情,比如换行。你可以将 JS 用于许多更复杂的目的,这里记录了其中的一些。
reactable(
data_for_table,
defaultSortOrder = 'desc',
defaultSorted = 'excess_deaths',
showSortIcon = FALSE,
compact = TRUE,
pagination = FALSE,
defaultColDef = colDef(
headerStyle = list(
textAlign = "left",
fontSize = "11px",
lineHeight = "14px",
textTransform = "uppercase",
color = "#0c0c0c",
fontWeight = "500",
borderBottom = "2px solid #e9edf0",
paddingBottom = "3px",
verticalAlign = "bottom",
fontFamily = font_es
),
style = list(
fontFamily = font_es,
fontSize = "14px",
verticalAlign = "center",
align = "left"
)
),
####### NEW #######
columns = list(
country = colDef(
name = "Region / Country",
style = list(fontFamily = font_es,
fontWeight = "400")
),
perc = colDef(
html = TRUE,
header = JS("
function(colInfo) {
return 'COVID-19 as<br>% of total'
}"),
name = "COVID-19 as % of Total",
align = "right",
maxWidth = 100,
format = colFormat(percent = TRUE, digits = 0),
style = list(fontFamily = font_es_bold),
headerStyle = list(
fontSize = "11px",
lineHeight = "14px",
textTransform = "uppercase",
color = "#0c0c0c",
fontWeight = "500",
borderBottom = "2px solid #e9edf0",
paddingBottom = "3px",
verticalAlign = "bottom",
fontFamily = font_es,
textAlign = "right"
)
),
clean_range = colDef(
name = "Time Period",
style = list(
color = '#3f5661',
fontSize = '12px',
fontFamily = font_es
)
)
)
)
添加条形图
我们现在可以创建包含条形图的“死亡”列。
由于集成了 JavaScript,向表格添加条形图变得非常容易。这里,我从reactable
网站上的一个示例中提取,并使用以下代码:
reactable(
data_for_table,
defaultSortOrder = 'desc',
defaultSorted = 'excess_deaths',
showSortIcon = FALSE,
compact = TRUE,
pagination = FALSE,
defaultColDef = colDef(
headerStyle = list(
textAlign = "left",
fontSize = "11px",
lineHeight = "14px",
textTransform = "uppercase",
color = "#0c0c0c",
fontWeight = "500",
borderBottom = "2px solid #e9edf0",
paddingBottom = "3px",
verticalAlign = "bottom",
fontFamily = font_es
),
style = list(
fontFamily = font_es,
fontSize = "14px",
verticalAlign = "center",
align = "left"
)
),
columns = list(
country = colDef(
name = "Region / Country",
style = list(fontFamily = font_es,
fontWeight = "400")
),
perc = colDef(
html = TRUE,
header = JS("
function(colInfo) {
return 'COVID-19 as<br>% of total'
}"),
name = "COVID-19 as % of Total",
align = "right",
maxWidth = 100,
format = colFormat(percent = TRUE, digits = 0),
style = list(fontFamily = font_es_bold),
headerStyle = list(
fontSize = "11px",
lineHeight = "14px",
textTransform = "uppercase",
color = "#0c0c0c",
fontWeight = "500",
borderBottom = "2px solid #e9edf0",
paddingBottom = "3px",
verticalAlign = "bottom",
fontFamily = font_es,
textAlign = "right"
)
),
clean_range = colDef(
name = "Time Period",
style = list(
color = '#3f5661',
fontSize = '12px',
fontFamily = font_es
)
),
###### NEW ######
covid_deaths = colDef(
name = "COVID-19 Deaths",
cell = function(value) {
width <- paste0(value * 100 / max(data_for_table$covid_deaths), "%")
value <- format(value, big.mark = ",")
value <- format(value, width = 10, justify = "right")
bar <- div(
class = "bar-chart",
style = list(marginRight = "6px"),
div(
class = "bar",
style = list(width = width, backgroundColor = "#F15A3F")
)
)
div(class = "bar-cell", span(class = "number", value), bar)
}
),
excess_deaths = colDef(
name = "Total Excess Deaths",
cell = function(value) {
width <-
paste0(value * 100 / max(data_for_table$excess_deaths), "%")
value <- format(value, big.mark = ",")
value <- format(value, width = 10, justify = "right")
bar <- div(
class = "bar-chart",
style = list(marginRight = "6px"),
div(
class = "bar",
style = list(width = width, backgroundColor = "#3F5661")
)
)
div(class = "bar-cell", span(class = "number", value), bar)
}
)
)
)
让我们把重点放在covid_deaths
上,一步一步地分解它。
首先,我们需要定义一些 CSS。reactable
允许您轻松地将 CSS is RMarkdown 文档包含在定义为css
的块中。
.bar-cell {
display: flex;
align-items: center;
}.number {
font-size: 13.5px;
white-space: pre;
}.bar-chart {
flex-grow: 1;
margin-left: 6px;
height: 22px;
}.bar {
height: 100%;
}
现在,让我们看看我们如何定义covid_deaths
:
covid_deaths = colDef(
### define the name
name = "COVID-19 Deaths",
### create a 'cell' function
cell = function(value) {
### define the bar width according to the specified value
width <- paste0(value * 100 / max(data_for_table$covid_deaths), "%")
### add a comma to the label
value <- format(value, big.mark = ",")
### justify and provide padding with width
value <- format(value, width = 10, justify = "right")
### create the barchart div
bar <- div(
### with a class of 'bar-chart'
class = "bar-chart",
### give the bar a margin
style = list(marginRight = "6px"),
### create the *actual* bar, with the red economist color
div(
class = "bar",
style = list(width = width, backgroundColor = "#F15A3F")
)
)
### bring it all together, with the 'value' (number) preceding the bar itself
div(class = "bar-cell", span(class = "number", value), bar)
}
)
这将创建一个如下所示的表:
添加标题
最后,我们可以添加表格标题和副标题。我们通过在我们的环境中存储上表来做到这一点。(这是最终的表码!)
table <- reactable(
data_for_table,
defaultSortOrder = 'desc',
defaultSorted = 'excess_deaths',
showSortIcon = FALSE,
compact = TRUE,
pagination = FALSE,
defaultColDef = colDef(
headerStyle = list(
textAlign = "left",
fontSize = "11px",
lineHeight = "14px",
textTransform = "uppercase",
color = "#0c0c0c",
fontWeight = "500",
borderBottom = "2px solid #e9edf0",
paddingBottom = "3px",
verticalAlign = "bottom",
fontFamily = font_es
),
style = list(
fontFamily = font_es,
fontSize = "14px",
verticalAlign = "center",
align = "left"
)
),
columns = list(
country = colDef(
name = "Region / Country",
style = list(fontFamily = font_es,
fontWeight = "400")
),
covid_deaths = colDef(
name = "COVID-19 Deaths",
# align = "left",
cell = function(value) {
width <- paste0(value * 100 / max(data_for_table$covid_deaths), "%")
value <- format(value, big.mark = ",")
# value <- str_pad(value, 6, pad = "")
value <- format(value, width = 10, justify = "right")
bar <- div(
class = "bar-chart",
style = list(marginRight = "6px"),
div(
class = "bar",
style = list(width = width, backgroundColor = "#F15A3F")
)
)
div(class = "bar-cell", span(class = "number", value), bar)
}
),
excess_deaths = colDef(
name = "Total Excess Deaths",
# align = "left",
cell = function(value) {
width <-
paste0(value * 100 / max(data_for_table$excess_deaths), "%")
value <- format(value, big.mark = ",")
value <- format(value, width = 10, justify = "right")
bar <- div(
class = "bar-chart",
style = list(marginRight = "6px"),
div(
class = "bar",
style = list(width = width, backgroundColor = "#3F5661")
)
)
div(class = "bar-cell", span(class = "number", value), bar)
}
),
perc = colDef(
html = TRUE,
header = JS("
function(colInfo) {
return 'COVID-19 as<br>% of total'
}"),
name = "COVID-19 as % of Total",
align = "right",
maxWidth = 100,
format = colFormat(percent = TRUE, digits = 0),
style = list(fontFamily = font_es_bold),
headerStyle = list(
fontSize = "11px",
lineHeight = "14px",
textTransform = "uppercase",
color = "#0c0c0c",
fontWeight = "500",
borderBottom = "2px solid #e9edf0",
paddingBottom = "3px",
verticalAlign = "bottom",
fontFamily = font_es,
textAlign = "right"
)
),
clean_range = colDef(
name = "Time Period",
style = list(
color = '#3f5661',
fontSize = '12px',
fontFamily = font_es
)
)
),
)
现在,我们可以在表格上方包含一个标题和副标题。我们使用一些htmltools
函数来创建 div、标题和段落。
div(
div(
h2("Excess mortality since region/country’s first 50 covid deaths"),
p(
### create the 'Updated on ...' and make it dynamic
paste0(
"Updated on ", format(Sys.Date(), "%B "),
append_date_suffix(
as.numeric(format(Sys.Date(), "%d"))
), " ",
strftime(Sys.time(), format = "%H:%M"), " UCT"
)
),
),
table)
呀!那些字体大小不太符合经济学人的风格。让我们给我们的 div 添加类来匹配它们的风格。
.table {
margin: 0 auto;
width: 675px;
}.tableTitle {
margin: 6px 0;
font-size: 16px;
font-family: 'Econ Sans Cnd'
}.tableTitle h2 {
font-size: 16px;
font-weight: bold;
font-family: 'Econ Sans Cnd'
}.tableTitle p {
font-size: 14px;
font-weight: 500;
}
现在我们可以重新运行上面的代码:
div(class = "tableTitle",
div(
class = "title",
h2("Excess mortality since region/country’s first 50 covid deaths"),
p(
paste0("Updated on ", format(Sys.Date(),"%B "),
append_date_suffix(
as.numeric(format(Sys.Date(), "%d"))
), " ",
strftime(Sys.time(), format = "%H:%M"), " UCT"
)
),
),
table)
让我们将它与我们试图复制的表进行比较。请注意,在《经济学家》杂志发表他们的表格和我创建我的表格之间的时间里,有些数据已经改变了。
这篇文章的代码可以在我的 GitHub 上找到。你可以把任何关于复制的问题(或任何其他问题)发送给 me@connorrothschild.com,或者在我的推特上。
https://twitter.com/CL_Rothschild
最初发表在我的博客上。
在 PyTorch 中重新创建 Keras 代码——入门教程
初学者的深度学习
学习在 PyTorch 中创建神经网络的基础知识
作者于 Imgflip 创建
我爱 Keras,我说对了!然而……
作为一名应用数据科学家,没有什么比用三行代码快速构建一个功能性的神经网络更让我高兴的了!然而,随着我开始更深入地钻研神经网络的黑暗网络,我愿意接受 Pytorch 确实让你对你的网络架构有更大的控制权这一事实。
鉴于我们大多数人对 Keras 都相当熟悉(如果不熟悉,请参见这里的 Keras 热情介绍),学习在 Pytorch 中创建一个类似的网络(同时学习 Pytorch 基础知识)一点也不困难。我们开始吧!
注意:我们不会复制 Keras 代码,因为我想在这个入门教程中介绍 PyTorch 的更多特性和功能!
快速回顾一下 Keras 代码的样子:
下面是创建模型架构、编译模型和最终训练模型的代码片段(来自我之前关于 Keras 中神经网络的帖子)。它是一个贷款评估模型,输出贷款应该被接受还是拒绝。
*# Model architecture*
model_m = Sequential([
Dense(units = 8, input_shape= (2,), activation = 'relu'),
Dense(units = 16, activation = 'relu'),
Dense(units = 2, activation = 'softmax')
])*# Model compilation*
model_m.compile(optimizer= Adam(learning_rate = 0.0001),
loss = 'sparse_categorical_crossentropy',
metrics = ['accuracy']
)*# Model Training and Validation*
model_m.fit(x = scaled_train_samples_mult,
y = train_labels,
batch_size= 10,
epochs = 30,
validation_split= 0.1,
shuffle = True,
verbose = 2
)
总结一下,我们构建了一个有三个隐层的网络,都是Dense
。三个隐藏层的激活功能分别是relu
、relu
和softmax
。input_shape
是一个元组(2,0)
,意味着我们有两个预测特征。Adam
优化器和学习率lr = 0.0001
已用于根据训练数据迭代更新网络权重。我们将在每次迭代中监控的损失是sparse_categorical_crossentropy
。accuracy
将用于判断网络的好坏。对于培训,我们将使用 30 个epochs
和 10 个batch_size
。如果上述任何术语的含义不清楚,请参见此处的或此处的或。
注:如果你想跟随本教程,请查看 Github 上的笔记本。
让我们从 PyTorch 开始吧
为了与我们之前使用 Keras 构建的网络保持一定程度的一致性,我将使用相同的数据集,即贷款申请数据集。它看起来是这样的:
在对数据集应用一些预处理后,主要是在 0-1 的范围内缩放所有输入特征(age
和area
)并对结果特征(application_outcome
)进行标签编码,这就是我们最终得到的结果:
在 PyTorch 中定义数据集
我们将使用 PyTorch 中的torch.utils.data.Dataset
类为贷款申请数据集定义一个Dataset
对象。我们将称它为CVSDataset
。
我们将在我们的CSVDataset
类中有四个方法— __init__
、__len__
、__getitem__
和get_splits
。
class CSVDataset(Dataset):
*# reading the csv and defining predictor and output columns*
def __init__(self):
# store the input and output features
self.X = df.values[:,:-1]
self.y = df.values[:,-1]
# ensure all data is numerical - type(float)
self.X = self.X.astype('float32')
self.y = self.y.astype('float32')
*# number of rows in dataset*
def __len__(self):
return len(self.X)
*# get a row at an index*
def __getitem__(self, index):
return [self.X[index], self.y[index]] # split into train and testset - using `random_split`
def get_splits(self, split_ratio = 0.2):
test_size = round(split_ratio * len(self.X))
train_size = len(self.X) - test_size
return random_split(self, [train_size, test_size])
注意:在非常基础的层面上,你为自己的数据集扩展的
Dataset
类应该有__init__
、__len__()
和__getitem__
方法。
我们首先在__init__()
中定义预测器X
和输出特征y
,并确保类型是浮点型的,因为神经网络只处理数字数据。
__len__
返回数据集中的行数,而__getitem__
返回数据集中特定索引处的项目。
此外,我们还定义了一个(可选的)方法get_splits
,让我们将数据集分成训练集和测试集。我们可以在 Sklearn 中使用train_test_split
很好地完成这项工作,但这只是展示如何在 PyTorch 中使用torch.utils.data.random_split
完成这项工作的一种方式。
定义模型架构
这是我们定义所有隐藏层在我们的网络中想要什么以及这些层如何相互作用的地方。为此,我们将利用torch.nn.Module
类并扩展它。我们将称呼我们的班级为myNeuralNetwork
。
它包含定义层的__init__
方法和解释输入如何通过定义的层向前传播的forward
方法。
class myNeuralNetwork(Module):
def __init__(self, n_inputs):
.
.
. def forward(self,X):
.
.
.
我们先来看一下init
方法:
def __init__(self, n_inputs):
# calling constructor of parent class
super().__init__()
# defining the inputs to the first hidden layer
self.hid1 = Linear(n_inputs, 8)
kaiming_uniform_(self.hid1.weight, nonlinearity='relu')
self.act1 = ReLU()
# defining the inputs to the second hidden layer
self.hid2 = Linear(8, 16)
kaiming_uniform_(self.hid2.weight, nonlinearity='relu')
self.act2 = ReLU()
# defining the inputs to the third hidden layer
self.hid3 = Linear(16, 2)
xavier_uniform_(self.hid3.weight)
self.act3 = Softmax(dim=1)
我们首先使用super().__init__()
调用父类的构造函数。接下来,我们定义三个隐藏层hid1
、hid2
和hid3
,以及它们的权重初始化和激活函数——act1
、act2
和act3
。
如果您还记得本文开头对 Keras 模型的总结,我们有三个密集的隐藏层。 Pytorch 相当于 Keras 致密层的是 **Linear**
。
第一个隐藏线性层hid1
采用n_inputs
个输入,输出 8 个神经元/单元。
注意:
n_inputs
粗略地翻译为我们有多少预测列(在我们的例子 2 中)。
第二隐层以 8 个神经元为输入,输出 16 个单元。
第三个隐藏层将 16 个神经元作为输入,并产生 2 个单元作为输出(这是贷款申请为approved
或rejected
的概率,因为 softmax 激活函数在该层中起作用)。
每个隐藏层的激活函数都存储在act1
、act2
和act3
中。
注意:Pytorch 中激活函数的常见例子有
ReLu
、Sigmoid
、LogSigmoid
等。
此外,PyTorch 允许您初始化每个隐藏层的权重张量。常见的例子有kaiming_uniform
、xavier_uniform
和orthogonal
。你可以在文档页面上了解更多细节。
现在,让我们看看forward
方法:
def forward(self, X):
#input and act for layer 1
X = self.hid1(X)
X = self.act1(X)
#input and act for layer 2
X = self.hid2(X)
X = self.act2(X)
#input and act for layer 3
X = self.hid3(X)
X = self.act3(X)
return X
每层的输入和激活在该函数中定义。总之,输入X
进入hid1
,第一激活功能act1
应用于此。然后输出被传递到hid2
,在此应用第二激活功能act2
等等。由于最后一层有一个 softmax 激活函数,返回的X
将是一个包含 n 个元素的张量,其中 n 是输出类的数量(在我们的例子中是两个——批准与拒绝)。
张量只不过是 Pytorch 自己的 Numpy 数组。这是一个通用的 n 维数组,用于任意数值计算。
在 PyTorch 中准备数据
在我们训练之前,使用torch.utils.data.DataLoader
加载用于训练的数据是很重要的。你会问,为什么需要这样做?它实际上创建了要作为模型输入发送的批次(针对训练和测试集)。它将行的样本、批量大小以及是否重排行作为输入。
注:我们在 *test_dl*
的情况下显式设置 *shuffle = False*
是因为我们需要在末尾添加未混淆的标签来绘制混淆矩阵。
# Preparing the dataset before training the model# load the dataset
dataset = CSVDataset()# get the train and test split
train, test = dataset.get_splits()# prepare dataloaders -
train_dl = DataLoader(train, batch_size = 32, shuffle = True)
test_dl = DataLoader(test, batch_size= 32, shuffle= False)
训练模型
我们首先通过创建myNeuralNetwork
对象并传递 2 作为输入来定义模型。这里的 2 是指输入特征的数量。
我们还将设置epochs = 10
,这意味着模型将根据所有数据训练10 次。
对于训练过程,我们将使用随机梯度下降(SGD) optimizer
。
优化器的选择包括
Adam
、AdaGrad
、SparseAdam
、SGD
等,它们的用例可以在文档中找到。
最后,我们要在训练时监控CrossEntropyLoss
。
# define the network
model = myNeuralNetwork(2) # 2 because we only have 2 input features# define the number of epochs
epochs = 10# define the optimizer - SGD
optimizer = SGD(model.parameters(), lr=0.01, momentum=0.9)# define the loss function
criterion = CrossEntropyLoss()
为了训练网络,我们将使用两个 用于 循环。以下代码中的外循环是训练历元数所必需的,内循环用于迭代train_dl
中的样本批次。也就是说,对于train_dl
中的每一批样本,我们将清除梯度、计算模型输出、计算损耗并进行反向传播以更新权重。
# iterate through all the epoch
for epoch in range(epochs):
# go through all the batches generated by dataloader
for i, (inputs, targets) in enumerate(train_dl):
# clear the gradients
optimizer.zero_grad()
# compute the model output
yhat = model(inputs)
# calculate loss
loss = criterion(yhat, targets.type(torch.LongTensor))
# credit assignment
loss.backward()
# update model weights
optimizer.step()
评估模型
为了评估模型,我们将再次使用 作为 循环来检查test_dl
中的所有批次。对于每个样本,我们将把它传递给模型并得到输出,即y_pred
。由于y_pred
包含两个概率值(一个用于approved
,一个用于rejected
,我们将选择概率最高的索引作为最终输出。
最后,我们将实际输出和预测输出存储在两个列表中— actuals
和predictions
。
# Evaluate the model
predictions, actuals = list(), list()# loop over all batches in test setfor i, (inputs, targets) in enumerate(test_dl):
# pass input to the model
y_pred = model(inputs)
# retrieve the numpy array
y_pred = y_pred.detach().numpy()
# pick the index of the highest values
res = np.argmax(y_pred, axis = 1)
# actual output
actual = targets.numpy()
actual = actual.reshape(len(actual), 1)
# store the values in respective lists
predictions.append(list(res))
actuals.append(list(actual))
actuals = [val for sublist in vstack(list(chain(*actuals))) for val in sublist]
predictions = [val for sublist in vstack(list(chain(*predictions))) for val in sublist]
为了打印准确度分数,我们将使用sklearn.metrics
中的accuracy_score
*# evaluating how good the model is!*
from sklearn.metrics import accuracy_scoreprint("The accuracy is" , accuracy_score(actuals, predictions))
*********************
The accuracy is 0.7495
我们成功了!
我们已经使用 PyTorch 创建了一个基本网络。希望这不是太多的信息超载。如果你想获得全部代码,请查看 Github上的笔记本。在下一篇教程中,我们将深入探讨 Pytorch 提供的一些高级功能。
在那之前:)
[## 阅读瓦希塔·谢尔博士的每一家书店(以及媒体上成千上万的其他作家)
作为一个媒体会员,你的会员费的一部分给了你阅读的作家,你可以完全接触到每一个故事…
varshitasher.medium.com](https://varshitasher.medium.com/membership) [## 使用 Python 中的 Keras 构建人工神经网络的初学者指南
创建网络架构、训练、验证和保存模型并使用它进行推理的提示和技巧。
towardsdatascience.com](/beginners-guide-to-building-artificial-neural-networks-using-keras-in-python-bdc4989dab00) [## 在数据科学面试中解释你的 ML 项目的逐步指南。
在结尾有一个额外的样本脚本,让你谨慎地展示你的技术技能!
towardsdatascience.com](/step-by-step-guide-to-explaining-your-ml-project-during-a-data-science-interview-81dfaaa408bf) [## 使用 Scikit、Pandas 和 Numpy 进行时间序列建模
直观地利用季节性来提高模型准确性。
towardsdatascience.com](/time-series-modeling-using-scikit-pandas-and-numpy-682e3b8db8d1)
用 PyTorch 重新创建 Keras 功能 API
让我们把喀拉斯的经历带到 PyTorch
作者图片
介绍
Francois Chollet(Keras 的创始人)的书《用 Python 进行深度学习》让我进入了深度学习的世界。从那时起,我就爱上了 Keras 的风格。
Keras 是我的第一个框架,然后跳进一点 Tensorflow 然后进来 PyTorch,剩下的就是历史了。
老实说,我真的很兴奋在 Keras 的模型训练中出现的进度条,这真是太棒了:)
那么,为什么不试着把 Keras 训练模特的经验带到 PyTorch 呢?
这个问题让我开始,我最终用所有花哨的进度条重新创建了 Keras 的密集层、卷积层和平坦层。
可以通过将一层堆叠在另一层的顶部来创建模型,并通过简单地调用 fit 方法来训练模型,这与 Keras 的工作方式类似。
让我们建造它
对于那些没有使用过 Keras 的人来说,在 Keras 中构建和训练一个模型如下所示:
在 keras 培训全连接网络
1。导入所需的库
你可能不熟悉 pkbar 库,它是用来显示进度条的。
导入所需的库
2。输入层和密集层
输入层简单地采用将被传递到神经网络并返回它的数据的单个实例的形状,对于全连接网络,它将类似于(1,784)并且对于卷积神经网络,它将是图像的维度(高度×宽度×通道)。
使用大写字母来命名 python 函数是违反规则的,但我们暂时忽略它(Keras 源代码的某些部分使用了相同的约定)。
输入层
密集类通过传递该层的输出神经元的数量和激活函数来初始化。当密集层被调用时,前面的层作为输入被传递。
现在我们有了前一层的信息。如果前一层是输入层,则会创建 PyTorch 线性层,其中包含从输入层返回的形状以及在密集类初始化期间作为参数提供的输出神经元的数量。
如果前一层是密集层,我们通过添加 PyTorch 线性层和用户提供给密集类的激活层来扩展神经网络。
而如果前一层是卷积或展平层,我们会创建一个名为 get_conv_output() 的效用函数,得到图像经过卷积和展平层后的输出形状。此维度是必需的,因为如果不向 in_features 参数传递值,我们就无法在 PyTorch 中创建线性图层。
get_conv_output() 函数将图像形状和卷积神经网络模型作为输入。然后,它创建一个与图像形状相同的虚拟张量,并将其传递给卷积网络(带有扁平化层),并返回从中输出的数据大小,该大小作为值传递给 PyTorch 的线性层中的 in_features 参数。
致密层
3。展平图层
为了创建展平图层,我们将创建一个名为 flattened layer 的自定义图层类,它接受一个张量作为输入,并在向前传播期间返回张量的展平版本。
我们将创建另一个名为 flatten 的类,当这个层被调用时,前面的层作为输入被传递,然后 flatten 类通过在前面的层上添加我们自定义创建的 flattened layer 类来扩展网络。
因此,所有到达展平层的数据都使用我们自定义创建的展平层类进行展平。
展平图层
4。卷积层
我们将通过传入过滤器数量、内核大小、步幅、填充、膨胀和激活函数来初始化 Conv2d 层。
现在,当调用 Conv2d 层时,前面的层被传递给它,如果前面的层是输入层,则创建一个 PyTorch Conv2d 层,它具有提供的过滤器数量、内核大小、步幅、填充、膨胀和激活函数的值,其中 in_channels 的值取自输入形状中的通道数量。
如果前一层是卷积层,则通过添加 PyTorch Conv2d 层和激活函数来扩展前一层,激活函数的 in_channels 值取自前一层的 out_channels。
在填充的情况下,如果用户需要保留从该层输出的数据的维度,那么填充的值可以被指定为“相同”而不是整数。
如果填充的值被指定为“same ”,则使用一个名为 same_pad() 的实用函数来获取填充的值,以保持给定输入大小、内核大小、步幅和膨胀的尺寸。
可以使用前面讨论过的 get_conv_output() 实用函数获得输入大小。
5。模型类别
在构建了我们的模型的架构之后,通过传入输入层和输出层来初始化模型类。但是我给出了一个额外的参数叫做 device,这是 Keras 中没有的,这个参数接受的值是“CPU”或“CUDA ”,它会将整个模型移动到指定的设备。
model 类的 parameters 方法用于返回要提供给 PyTorch 优化器的模型的参数。
模型类有一个名为 compile 的方法,它接受训练模型所需的优化器和损失函数。model 类的 summary 方法借助 torchsummary 库显示创建的模型的概要。
拟合方法用于训练模型,该方法以输入特征集、目标数据集和历元数为自变量。它显示由损失函数计算的损失和使用 pkbar 库的训练进度。
评估法用于计算试验数据的损失和精度。
当使用 PyTorch 数据加载器加载数据时,将使用 fit_generator、evaluate_generator 和 predict_generator。fit_generator 将训练集数据加载器和时期作为参数。evaluate_generator 和 predict_generator 分别采用验证集数据加载器和测试数据加载器来衡量模型对未知数据的执行情况。
模型类
最后的想法
我已经使用密集层和卷积神经网络在 CIFAR100、CIFAR10 和 MNIST 数据集上测试了代码。它运行良好,但有巨大的改进空间。
这是一个有趣的项目,我工作了 3-4 天,它真正推动了我用 PyTorch 编程的极限。
你可以在这里查看在上述数据集上完成训练的完整代码,或者你可以在 colab 中自由调整代码以适合你的喜好。
在 R 中重建网飞的分位数自举
我在做一些随意的数据科学阅读,然后嘣!我在网飞的技术博客上重新发现了一篇很棒的文章:马丁·廷利的《网飞流式视频实验:可视化实践和统计意义》。
它有什么酷的?它提供了一种可扩展的方法来探索大规模在线实验中的异构处理效果。心智融化?好的,我再试一次。它描述了一种方式,实验者可以学到更多的东西,而不仅仅是他们是否有整体影响。相反,它让实验者看到他们如何影响不同的群体。此外,它还可以扩展到处理海量数据!
马丁和新合著者最近的文章,分享了一个很好的例子来说明这种方法是如何帮助的。下面您可以看到用户的视频质量如何在现有的流配置(生产,黑色设置为零)和新的(治疗,紫色)之间进行比较。
图来自“大规模流实验的数据压缩”。详细信息:“带阴影置信区间的差异图表明,在分布的最低百分位数处,视频质量有实际和统计上的显著提高”
请记住,特别之处在于看到不同用户的影响如何不同。对质量差的用户有积极的影响(见左边),对其他人没有显著影响。这意味着产品团队可以从说“质量提高了”变成更有用的话,比如“我们为体验最差的用户提高了质量,而不影响已经有高质量体验的用户。”
这对我来说太棒了,我决定剔除细节,边走边编码学习。更好的是,我正在分享我所做的来节省你的时间!
注意:我在本文中粘贴了一些有用的代码片段。可以从我的 GitHub repo 中 访问完整的代码演练。
入门指南
library(tidyverse)
模拟数据
没有数据我们做不了什么。我将为一组实验对象模拟一个连续的度量标准x
。想象一下x
对于网飞的每一个用户来说代表着类似“秒延迟”的东西。为了看起来更合理,我们来看看卡方分布数据:
x <- rchisq(n = 200, df = 8)
模拟指标的分布,“x”
获取分位数
提醒: 分位数 是将数据分割成不同大小/百分位数的数值。例如,0.5 分位数将是将数据分成两半的值(因此 50%的值低于该值)。这也是一种叫做中位数的特例。. 25 分位数将是大于数据的 25%的值。要找到特定的分位数,您需要排列数据(从最小到最大)并在 x%位置找到数据点。
r 有一个俏皮的功能,quantile()
。让我们用它来得到一个数值范围(用seq()
设置),如下所示:
quantile_df <- tibble(percentile = seq(0, 1, by = .2)) %>%
mutate(value = quantile(x, percentile))
quantile_df#> # A tibble: 6 x 2
#> percentile value
#> <dbl> <dbl>
#> 1 0 1.02
#> 2 0.2 4.08
#> 3 0.4 6.31
#> 4 0.6 8.58
#> 5 0.8 11.0
#> 6 1 20.0
轻松点。
签到:我们已经可以做一点了
这已经足以产生类似网飞的情节。我们可以模拟两种情况(生产和处理)的数据,并计算每种情况的分位数。
*# Simulate data sets. For treatment, we'll bind two distributions
# skewed either side of production*
x_production <- rchisq(200, 8)
x_treatment <- c(rchisq(100, 12), rchisq(100, 4))group_quantiles_df <- tibble(percentile = seq(0, 1, by = .2)) %>%
mutate(production = quantile(x_production, percentile),
treatment = quantile(x_treatment, percentile))
group_quantiles_df#> # A tibble: 6 x 3
#> percentile production treatment
#> <dbl> <dbl> <dbl>
#> 1 0 1.80 0.180
#> 2 0.2 4.76 2.83
#> 3 0.4 6.44 5.22
#> 4 0.6 7.96 8.04
#> 5 0.8 10.3 13.2
#> 6 1 27.9 36.2
网飞图显示了与产量(产量= 0)相比的“差异”,对我们来说是这样的:
对于几行代码来说相当不错!好吧,我们的数据和他们的不同,但是概念都是一样的。
自举差异
我们已经走了很远,但是缺少了一些重要的东西:置信区间(原始图中的透明带)。这些告诉我们两组之间是否有显著差异。让我们迎接挑战,开始行动吧。我们需要考虑的第一件事是引导。
提醒:Bootstrapping是一个随机抽取一堆数据进行替换的过程。您可以使用这个新样本计算所有感兴趣的统计数据(例如,分位数)。然后重复多次,每次得到的结果都略有不同,并建立起一种不确定感。
我们可以很容易地用 R 的sample()
和replace = TRUE
创建一个自举。结果将是一个向量,其长度与原始数据相同,但使用替换对其进行采样。
x_bootstrap <- sample(x, replace = TRUE)
对于每个数据集(生产和处理),我们多次这样做,每次都保存自举样本的分位数。
n_bootstraps <- 100
quantile_df_template <- tibble(percentile = seq(0, 1, by = .2))bootstrapped_quantiles <- map_df(seq_len(n_bootstraps),function(i) {
bootstrapped_production <- sample(x_production, replace = TRUE)
bootstrapped_treatment <- sample(x_treatment, replace = TRUE)
quantile_df_template %>%
mutate(bootstrap = i,
production = quantile(bootstrapped_production,
percentile),
treatment = quantile(bootstrapped_treatment,
percentile))
})bootstrapped_quantiles#> # A tibble: 600 x 4
#> percentile bootstrap production treatment
#> <dbl> <int> <dbl> <dbl>
#> 1 0 1 1.80 0.180
#> 2 0.2 1 4.84 3.16
#> 3 0.4 1 6.41 5.58
#> 4 0.6 1 8.24 7.48
#> 5 0.8 1 11.0 14.9
#> 6 1 1 27.9 36.2
#> 7 0 2 1.80 0.180
#> 8 0.2 2 4.80 2.93
#> 9 0.4 2 7.14 5.07
#> 10 0.6 2 8.78 7.79
#> # … with 590 more rows
这给了我们每个百分点的很多点(每个引导一个点)。下图以红色显示了原始production
数据的范围和平均值。
现在,我们真正感兴趣的是两组之间的差异。让我们为生产和处理自举分位数之间的“差异”重复这个过程。然后使用这些数据计算与生产数据相比,围绕处理条件绘制的“范围”。
置信区间
提醒: 置信区间 为可以被认为与我们观察到的数据显著不同的东西设定界限。显著性由一个称为显著性标准的概率阈值决定。
上面我们绘制了自举分位数差异的完整范围,但这不是我们想要的。我们实际上想要置信区间。为了得到这些,我们需要运行一定数量的自举,然后计算出截断最高和最低自举值的点,以给我们自己留下特定百分比的结果。例如,如果我们的显著性标准是 5%,这是非常典型的,我们希望得到砍掉最低和最高 2.5%数据的点。额外收获:这些对应于自举差异的 0.025 和 0.975 分位数!
这段代码可以让我们快速到达目的地:
bootstrapped_cis <- bootstrapped_quantiles %>%
mutate(q_dif = treatment - production) %>%
group_by(percentile) %>%
summarise(lower_bound = quantile(q_dif, .025),
upper_bound = quantile(q_dif, .975))bootstrapped_cis#> # A tibble: 6 x 3
#> percentile lower_bound upper_bound
#> <dbl> <dbl> <dbl>
#> 1 0 -2.28 -1.00
#> 2 0.2 -2.53 -0.984
#> 3 0.4 -2.09 -0.351
#> 4 0.6 -1.19 2.03
#> 5 0.8 0.825 4.75
#> 6 1 -4.72 16.3
我们现在可以将这些数据绘制成透明带!
看起来不错!由此,我们可以得出结论,我们的治疗组明显低于较低百分位数的产量,可能高于 80%左右,在其他地方没有不同。
提高数字
你已经得到了提高数字所需的一切。更大的数据集,更多的数量,更多的引导。您可能会得到这样的结果:
现在我们正在谈话!
是时候获得(更多)技术了
好了,该认真了。Martin 继续分享如何处理(1)多重比较和(2)大量数据的“一些技术细节”。让我们按顺序解决它们。
多重比较
当你运行一个实验并比较组平均值时,你做了一个统计测试。你可以使用 95%的置信区间,有 5%的机会错误地说有差异,而实际上没有差异(统计学家说有 5%的假阳性率)。在这里,我们实际上运行了许多测试:每个分位数一个。Lots =倍数;测试=比较。多重比较!这可能不太直观,但请相信我,对所有这些测试使用 95%的置信区间会使你出错的几率大大高于 5%。不酷😬
幸运的是,有一个解决方案。我们可以调整显著性阈值,使出错的总体几率达到期望的水平。这可以通过 Bonferroni 修正来实现,我们可以估计独立测试的数量(而不是因为使用了更多的分位数而受到惩罚)。我先不为新手读者解释这个,让我们看看代码。
PSA:我已经根据我所能读到的拼凑了这些,但我愿意接受修改。
*# Sum of the correlations between bootstrapped
# differences at each quantile*
cor_sum <- bootstrapped_quantiles %>%
transmute(bootstrap, percentile,
q_dif = treatment - production) %>%
pivot_wider(names_from = percentile, values_from = q_dif) %>%
select(-bootstrap) %>%
cor() %>%
.[-upper.tri(.)] %>%
sum()*# Estimated number of independent tests*
n_tests_estimate <- length(quantile_seq) ^ 2 / cor_sum
n_tests_estimate#> [1] 2.289841# Bonferroni adjusted confidence interval
desired_error_rate <- 0.05
adjusted_error_rate <- desired_error_rate / n_tests_estimate
adjusted_error_rate#> [1] 0.022
为了处理我们的多重比较并保证只有 5%的错误率,我们应该使用 97.8%的置信区间(从1 — 0.022
)而不是 95% ( 1 — 0.05
)。因此,我们不是在 0.025 和 0.975 分位数处截断自举数据,而是在 0.011 和 0.989 处截断。
这显示了不同之处:
请注意,经过适当调整的绿色置信区间比原始区间更宽。因此,我们对统计显著性的评估更为保守,将我们的总体误差率保持在 5%。
不错!
基于大数据的快速引导
到目前为止,一切都很酷,但如果你有从数千次实验中产生的大量数据,就会崩溃。聪明的解决方案是通过将数据压缩到均匀大小的桶中来近似计算结果,并引导这些桶而不是原始数据。这意味着无论原始大小如何,数据都可以压缩到设定数量的存储桶。
提示:我发现跟随最近的文章 会更容易理解这个话题。如果你想看一个不同的应用程序,请阅读我之前关于回归的文章。
第一步,获取有序存储桶中的数据,跟踪每个存储桶的计数和汇总统计数据(我将使用中间值)。我们可以在cut_number()
(来自 tidyverse 自带的 ggplot2)的帮助下做如下操作。
# Define number of buckets per group
n_buckets <- 100# Compress data into buckets of even counts
compressed_data <- tibble(production = x_production,
treatment = x_treatment) %>%
pivot_longer(c(production, treatment),
names_to = "group", values_to = "x") %>%
group_by(group) %>%
mutate(x_bucket = cut_number(x, n_buckets)) %>%
group_by(group, x_bucket) %>%
summarise(count = n(),
x = median(x)) %>%
ungroup()compressed_data#> # A tibble: 200 x 4
#> group x_bucket count x
#> <chr> <chr> <int> <dbl>
#> 1 production (1.53,1.94] 10 1.69
#> 2 production (1.94,2.15] 10 2.06
#> 3 production (10.1,10.3] 10 10.2
#> 4 production (10.3,10.4] 10 10.4
#> 5 production (10.4,10.6] 10 10.5
#> 6 production (10.6,10.8] 10 10.7
#> 7 production (10.8,10.9] 10 10.8
#> 8 production (10.9,11] 10 11.0
#> 9 production (11,11.2] 10 11.1
#> 10 production (11.2,11.3] 10 11.2
#> # … with 190 more rows
下一个挑战是获得分位数,因为quantile()
不再工作了。没什么大不了的。让我们添加一个累积百分比列,并编写一个函数来根据需要搜索数据框。
# Add cumulative percentile to compressed data for easy search
compressed_data <- compressed_data %>%
group_by(group) %>%
arrange(x) %>%
mutate(cum_percentile = cumsum(count) / sum(count)) %>%
ungroup()
compressed_data#> # A tibble: 200 x 5
#> group x_bucket count x cum_percentile
#> <chr> <chr> <int> <dbl> <dbl>
#> 1 treatment [0.0539,0.412] 10 0.223 0.01
#> 2 treatment (0.412,0.547] 10 0.447 0.02
#> 3 treatment (0.547,0.769] 10 0.675 0.03
#> 4 treatment (0.769,0.881] 10 0.812 0.04
#> 5 treatment (0.881,0.986] 10 0.926 0.05
#> 6 treatment (0.986,1.08] 10 1.01 0.06
#> 7 production [0.493,1.53] 10 1.08 0.01
#> 8 treatment (1.08,1.21] 10 1.11 0.07
#> 9 treatment (1.21,1.33] 10 1.23 0.08
#> 10 treatment (1.33,1.43] 10 1.38 0.09
#> # … with 190 more rows*# Function we can reuse to get `x` quantile from the compressed data*
x_at_percentile <- **function**(percentile, compressed_df) {
i <- 1
**while** (compressed_df$cum_percentile[i] < percentile) {
i <- i + 1
}
**return** (compressed_df$x[i])
}# Test
compressed_data %>%
filter(group == "production") %>%
x_at_percentile(.5, .)#> [1] 7.361729
最好确保它能工作!下面我用这两种方法从原始数据和压缩数据中得到生产数据的分位数。
quantile_df <- tibble(percentile = seq(0, 1, by = .1)) %>%
mutate(original = quantile(x_production, percentile)) %>%
mutate(compressed = map_dbl(
percentile,
x_at_percentile,
filter(compressed_data, group == "production")
))
当我们绘制原始和压缩的结果时,我们得到:
看起来是一个很好的近似值,我们没有使用太多的数据或桶。这只会随着更多的原始数据和更多的桶而变得更准确(显然“几千”应该可以做到)。
我们现在可以压缩我们的数据,无论数据有多大,都可以大规模管理!
我们需要知道的最后一件事是如何引导压缩数据集。我们现在对压缩数据帧的行进行采样(包含计数和汇总值)。这可以通过sample_n()
(来自 tidyverse 附带的 dplyr)来完成。下面是一个简单的示例,它对压缩的生产数据进行采样,并重新计算累积的百分位数:
# A bootstrap of the compressed data
compressed_data %>%
filter(group == "production") %>%
sample_n(n(), replace = TRUE) %>%
arrange(x) %>%
mutate(cum_percentile = cumsum(count) / sum(count))#> # A tibble: 100 x 5
#> group x_bucket count x cum_percentile
#> <chr> <chr> <int> <dbl> <dbl>
#> 1 production (1.53,1.94] 10 1.69 0.01
#> 2 production (2.15,2.39] 10 2.19 0.02
#> 3 production (2.39,2.65] 10 2.48 0.03
#> 4 production (2.39,2.65] 10 2.48 0.04
#> 5 production (3.29,3.38] 10 3.31 0.05
#> 6 production (3.38,3.51] 10 3.43 0.06
#> 7 production (3.69,3.82] 10 3.75 0.07
#> 8 production (3.69,3.82] 10 3.75 0.08
#> 9 production (3.92,4.05] 10 4.02 0.09
#> 10 production (3.92,4.05] 10 4.02 0.1
#> # … with 90 more rows
请注意,正如原始数据一样,引导会导致行被多次采样(例如,行 3 和 4、行 7 和 8 或行 9 和 10)。这就是为什么我们重新计算百分点,一切都应该工作。
注意:如果每行的计数不相等,您需要做一些加权,以确保具有较高计数的行以较高的速率被采样。但是,我们将数据分成大小相等的组,因此可以忽略这一点,并以相等的概率对行进行采样。
那都是乡亲们!
嗯,我们也是。这些都是你开始大规模实施网飞分位数自举技术需要解决的问题。
尽管如此,我还是要给你一个重要的警告。撰写本文和代码是为了帮助理解正在发生的事情。这绝对不是为了表现。所以不要把它扔进任何实验管道,因为 s*!%会出问题。想要启动并运行这项技术吗?使用一个小数据集来试验这些代码,直到您理解发生了什么。然后慢慢地改进性能,反复测试准确性和处理更大数据的能力。完成后,请回到这里分享您的作品:)
在 PowerBI 中重现 Gapminder 情节
COVID19 会改变 2020 年的趋势吗?
在过去的几周里,我看到了许多关于 COVID19 对全球健康和经济影响的分析和可视化。
这不仅仅是一场寻找疾病治疗方法的竞赛,它还激发了人们对数据科学的兴趣,因为它适用于抗击疫情。
我也要加入这股潮流。
对于初学者来说,我认为汉斯·罗斯林教授著名的 Gapminder 图是最好的选择。罗斯林教授做了一件了不起的工作,用这个图讲述了一个故事,从那时起,它已经成为事实上的初学者数据可视化项目。
事不宜迟,我们来做剧情吧!
准备数据
首先,我们必须获得用于可视化的数据。你可以从 Gapminder 网站这里或者我的 GitHub repo 下载数据。
这些数据与以下内容有关
- 预期寿命(年)
- 人均收入(国内生产总值/人均,经通货膨胀调整的购买力平价美元)
- 总人口
- 世界各地区
打开 CSV 文件,您会注意到它们是“宽”格式(区域除外),如下所示
“宽”格式的数据
我们必须将数据转换为“长”或“整齐”格式,以创建 Gapminder 图。
先说预期寿命数据。
- 创建一个新的 PowerBI 文件并转到主页>获取数据> Text/CSV 。选择预期寿命 CSV 文件
- 在 PowerQuery 编辑器中,将第一行提升为列标题。
- 要变成“整齐”格式,选择
geo
列并点击转换>取消透视其他列。我们希望年份和值组成新的列。 - 将这些列分别重命名为
Country
、Year
和Life Expectancy
。 - 为尾随空格修剪
Country
列
数据现在应该如下所示
长格式的预期寿命数据
对收入和人口数据重复上述过程。
这样做之后,收入和人口数据应该如下所示
“长”格式的收入数据
“长”格式的人口数据
区域数据已经是“长”或“整齐”的格式,但我们必须进一步格式化它,以便稍后进行合并。
- 将
name
列重命名为Country
- 删除除
Country
、region
和sub-region
列之外的其他列。 - 南极洲没有标明区域或次区域。我们只是通过创建条件列将南极洲分配给这些列。举个例子,
上面创建了一个新的列Subregion
,如果Country
是南极洲,则使用南极洲,否则使用sub-region
列的值。对Region
栏进行同样的操作。之后去掉旧的region
和sub-region
列。
4.最后,为尾随空格修剪Country
列
区域数据现在应该如下所示
组合数据
下一步是结合预期寿命、收入和人口数据。
我们必须这样做有三个原因
- 我们的数据没有相同的行数。预期寿命数据有 186 行,收入数据有 193 行,人口数据有 195 行。
- 我们将把
Years
列限制为 5 的倍数。这是为了在我们将数据动画化后使趋势更加明显。
让我们从结合我们的数据开始。
- 创建新的合并查询(新查询>将>合并查询合并为新的)
- 将预期寿命和收入数据合并如下。
我们首先选择预期寿命数据,因为它的行数最少。通过选择左连接,没有匹配预期寿命的国家将在结果合并中被移除。这是为了确保一旦合并完成,所有国家都有预期寿命、收入和人口。
3.扩展列以包含人均收入数据。
展开合并的数据
对人口数据重复上述过程。数据最终应该如下所示
结果数据有 186 行,我们可以确定所有国家都有所有必需的字段,因为我们前面使用了左连接。
过滤数据
为了在我们将数据制成动画后使趋势更加明显,让我们将Years
列过滤为 5 年间隔。我们可以用 PowerQuery 中的Number.Mod
函数来实现这一点
1.按如下方式创建新列
2.筛选自定义列以仅包含 0 或可被 5 整除的年份。随后删除自定义列
结果数据应该如下所示
过滤数据
将合并的数据重命名为 Gapminder。
可视化数据
经过所有这些准备,我们现在终于准备好创建我们的可视化。
来绘制 Gapminder 图。
1。将散点图拖放到我们的 PowerBI 窗口。适当调整大小。
2.将字段拖动到绘图中适当的轴上
X-axis — Gapminder[Income Per Person]
Y-axis — Gapminder[Life Expectancy] summarized by Average
Size — Gapminder[Population]
Play Axis — Gapminder[Year]
Details — Gapminder[Country]
3.让我们在Subregion
的基础上给Countries
着色。为此,让我们首先在 Gapminder(合并)数据和 Regions 数据之间创建一个关系。
转到Modelling > Manage Relationships
。按如下方式创建关系
请注意,我选择利用关系,而不是合并数据,因为它不像前面的过程那样混乱。现在,这两个数据的主键只是Country
列。
4.让我们格式化我们的图表,使它更清晰。转到格式面板,按如下方式调整轴
X-axis — make it a logarithmic scale, max of 120,000
Legend — put in the Right
Category Labels — turn it on to show Country names
Title — put “How Does Income Relate to Life Expectancy?”
最终的情节现在看起来像这样
最终 Gapmidner 图
你可以在这里下载 T4。
如果你玩这个 1910 年到 1920 年的 PowerBI,你会注意到,与 1910 年和 1915 年相比,1920 年很少有圆点移动得更低。
由于西班牙流感,这些国家在 20 世纪 20 年代移到了左下方
这当然是因为西班牙流感从 1918 年 1 月到 1920 年 12 月持续了 36 个月,感染了 5 亿人——约占当时世界人口的三分之一。
【2020 年,COVID19 也会这样吗?
我们不知道,我也希望不知道。然而,一旦 2020 年的数据出来,我很高兴能重现这个图。
感谢您阅读我的文章。关注我上Twitter和Linkedin。
退房还我的书PowerQuery 熊猫指南 上Leanpub。
RecSys 2020:第 14 届推荐系统会议纪要
莱昂纳多·马里尼奥在 Unsplash 上的照片
第 14 届 ACM 推荐系统会议在许多方面都很特别:一个完全虚拟的会议,它在保持社交互动方面做了令人惊叹的工作——即使许多人在位置和时间上都很远。另一方面:偏见和公平从来没有像现在这样普遍和受到重视。这篇博客引导你参加会议,提供了演示文稿、材料和论文的有用链接。所以,(再次)享受这个会议吧。
计划在里约热内卢举行(首次在南美举办)今年的 RecSys 于 9 月 22 日至 26 日举行,其中前三天是主要会议,随后两天是教程和研讨会。第 0 天——一如既往——是为博士研讨会预留的。2019 年 10 月拉丁美洲推荐系统学校率先推出了整个南美和拉丁美洲 RecSys 体验。
RecSys 2020 的几个数字
会议完全是虚拟的。现场论文、海报和辅导会议分两次进行,每次间隔 11 小时,以适应来自全球所有时区的演示者和参与者。然而,会议仍然是单轨制,允许每个人看到和参与所有的事情。
- 1200 多名参与者:64%来自工业界,36%来自学术界,21%是学生
- 主会议: 3 场主题演讲,9 场论文会议,12 场社交会议,3 场海报会议
- 6 教程, 12 工作坊
- 39/218 长论文提交被接受(18%) —新的提交记录!
- 26/128 篇短文被接受(20%)
- 2/7 的论文(29%)通过了新引入的再现性跟踪
- 25%的提交来自美国
- 话题焦点从算法(2020 年为 47%对 26%)转移到应用(2020 年为 17%对 39%)
- 接受 10/20 工业捐款
- Linus Dietz 跑了 46.6 公里的总距离,获得了最佳跑步者奖
关于操纵、偏见和对话式人工智能代理的主题演讲
每个主要会议日都以主题演讲开始:
菲利普·门策尔:社交媒体让我们容易被操纵的 4 个原因
在第一天,来自印第安纳大学社交媒体观察站的菲利普·门泽尔谈到了“社交媒体让我们容易被操纵的 4 个原因”(在 YouTube 上找到相同的演讲)。考虑到最近美国大选或当前疫情电晕的社交媒体反响,他提出了有趣的模拟和分析,概述了以下四个原因:
- 操纵
- 平台偏差
- 信息过载
- 回声室
通过对内容病毒式传播,特别是假新闻传播和社交机器人行为的说明,这次谈话非常有趣,也非常具体。他还展示了在不同的用户注意力和信息过载以及特定的信息共享模式下,质量和受欢迎程度的相关性。门策尔还分享了在他的实验室创造的各种有趣的工具。在他的总结中,他声称“认知、社会和算法偏见的相互作用使我们容易受到错误信息的影响”,并且“社交机器人利用了这些漏洞”。这很好地反映了今年 RecSys 在偏见研究方面的许多贡献。绝对值得笑的是他对成年人批判性思维能力的评论:
孩子们的批判性思维不如成年人,相信我,成年人也没有多少批判性思维。
里卡多·巴埃萨-耶茨:搜索和推荐系统中的偏见
第二天的主题演讲完美地延续了第一天的内容。来自东北大学的里卡多·巴埃萨-耶茨教授发表了“搜索和推荐系统中的偏见”(非常相似的演讲在这里)。他构建了渗透在个性化网络中的各种偏见的复杂相互作用,其中搜索和推荐系统是主导力量。通过活动、算法和认知偏差等总体偏差,他深入研究了这种复杂相互作用中出现的一些恶性反馈循环的复杂性。
图片来自里卡多·巴埃萨-耶茨:搜索和推荐系统中的偏见
图片来自里卡多·巴埃萨-耶茨:搜索和推荐系统中的偏见
幸运的是,他还触及了去偏置技术,不同目标的相关性,如多样性、意外收获或仅次于准确性的新颖性。他还提出了在不损害长期商业目标的情况下进行更多探索的理由。这是一个警告,但仍然是令人鼓舞的谈话。
米歇尔·周:“你真的懂我”——能够真正理解和帮助用户的对话式人工智能代理
还讨论了这些对话代理在现实世界中的应用。不幸的是,网上没有类似的谈话,但你可能会发现最近与她的对话部分的采访很有趣。
最佳论文
会议再次向最佳长篇和短篇论文以及最佳评论者颁奖。
最佳长论文奖由腾讯的唐等人颁发给【渐进式分层抽取(PLE):一种新颖的多任务学习(MTL)个性化推荐模型】),以表彰他们改进腾讯视频推荐系统的方法。他们的解决方案解决了负迁移,负迁移描述了推荐系统中多个松散相关甚至冲突的任务之间不利的折衷。作者将最先进的 MTL 模型中的结果现象描述为跷跷板现象,其中一些任务得到改善,而牺牲了其他任务。他们提出了共享学习结构,以提高共享学习效率,从而缓解跷跷板现象和负迁移。通过工业和公共基准数据集,他们提供了他们的方法优于先前方法的证据。
图片来自唐等:、:一种新颖的多任务学习(MTL)个性化推荐模型)
Gustavo Penha 和 Rodrygo L. T. Santos 的“利用性能评估增加推荐集合”获得了最佳长论文亚军。在他们的工作中,他们提出了一种个性化的方法来组合推荐器集成的基本估计器,以在最先进的集成推荐器结果中实现显著更高的准确性。至此,他们利用历史用户反馈来生成性能估计,当生成集合推荐时,该性能估计用作基本推荐器的个性化权重。
图片来自 Gustavo Penha 和 Rodrygo L. T. Santos: 利用性能评估来扩充推荐集合
最后,最佳短文是米等人的“【ADER:面向连续学习的基于会话的推荐的自适应提取样本重放”。作者提出了一种基于会话的推荐器的连续更新方法,减轻了灾难性遗忘的风险。因此,他们确定并使用一小组有代表性的历史序列数据(样本),以便在对新数据以及蒸馏损失进行训练时进行重放。
图片来自 Mi 等人:自适应提取样本回放,实现基于会话推荐的持续学习
引起我兴趣的其他论文,也是我阅读清单上的下一篇:
- Afchar 和 Hennequin (Deezer 研究):用属性使神经网络可解释:应用于隐含信号预测
- Sato 等人(富士施乐):对建议的因果效应进行无偏学习
- 黄等:将数据集偏差排除在模拟之外:基于强化学习的推荐系统去偏差模拟器
- Schnabel 等人(微软):用小型注释数据集消除项目间推荐的偏见
- Rendle 等人(谷歌研究):神经协同过滤与矩阵分解再探
- 李等:层叠式混血儿土匪:在线学习相关性和多样性排名
- 戈登堡等人(Booking.com): 免费午餐!投资回报约束内动态促销推荐的追溯提升建模
- Aridor 等人:解构过滤泡沫——用户决策和推荐系统
- Saito: 双稳健估计器,用于对点击后转化率指标进行排名
- 王等:推荐系统的因果推理
- 郭等(推特):深度贝叶斯盗匪:探索在线个性化推荐
主导主题:偏见,公平,因果关系,强盗和强化学习
在我个人看来,近年来对狭隘地关注准确性的强烈抗议已经被机构群体所听到。会议的主要议题包括承认偏见和发展消除偏见的技术,超越相关性和试图模拟因果关系,以及解决公平和问责问题。我相信,RecSys 研究团体对这些问题的了解比一般社会所认为的要多得多。然而,我的观点偏向于我在会议上看到的,而不是每个系统背后发生的事情。但也有证据表明,解决这些问题可以推动有益的长期商业目标,因此符合行业自身的利益。
这篇博客不能总结所有的发展和趋势,但它应该作为那些不能参加或想重温的人的一个切入点。关于回顾和思考观点,来自网飞的 Justin Basilico 做了一个非常翔实和精心制作的概述:“网飞最近的个性化趋势”
衡量标准的层次(图片来自 Justin Basilico 的网飞最近的个性化趋势
他为网飞确定的趋势似乎很好地反映了整个会议的大部分内容,这就是为什么我强烈建议看一下幻灯片:
- 因果关系
- 盗匪
- 强化学习
- 目标
- 公平
- 体验个性化
练习技能和加强交流的教程和研讨会
第四个会议日安排了 6 个针对 RecSys 实践的高级主题的教程:
- 对话式推荐系统:视频
- 推荐系统的特征工程(Nvidia rapids.ai): 视频和代码
- 在搜索和推荐系统中抵消偏见并增加公平性:视频、幻灯片和参考文献
- 推荐系统中的土匪简介:视频和代码
- 基于贝叶斯值的推荐:基于代理和反事实策略的推荐(Criteo)的一种基于模型的替代方案:视频和代码
- 对抗性学习推荐:安全和生成性任务的应用—概念到代码:视频、幻灯片和代码
在过去的两天里,我们还举办了 12 场有趣的研讨会,以强化 RecSys 研究中的特定主题:
- CARS :情境感知推荐系统研讨会
- ComplexRec :关于复杂环境中的建议的研讨会
- FAccTRec :关于负责任的建议的研讨会
- 时尚与零售推荐系统研讨会
- 健康推荐系统研讨会
- impacters:关于推荐系统影响的研讨会
- IntRS :关于推荐系统的界面和人类决策的联合研讨会
- OHARS :在线错误信息和危害感知推荐系统研讨会
- 在线推荐系统和用户建模研讨会
- 播客推荐研讨会
- 揭示:关于从用户交互中学习 Bandit 和强化的研讨会
- RecSys 挑战 2020 研讨会
请登录rec sys 官方网站链接到您的专用车间。
与去年类似,REVEAL workshop 吸引了最多的关注,共有 900 多名参与者。你一定要去看看。还有开放 bandit 管道的发布——这是一个用于 Bandit 算法和非策略评估的 python 库,被认为是本次研讨会的亮点之一。
关于 RecSys Challenge 2020 Workshop,请查看 NVIDIA 获胜团队的博客帖子,在此处描述他们的解决方案。
社会的
由于完全虚拟的环境以及社交活动在 RecSys 会议中发挥着至关重要的作用,会议的社交方面是迄今为止最具挑战性的。但是,在回顾中,虚拟代孕超出了我所有的期望!Whova event 应用程序提供了一个简单而强大的单一入口:查看议程、与同行联系、开始讨论、观看演示等。使用(网络)应用程序时非常好用。然而,这仅仅是开始。远程个人联系和快乐的主要驱动力是gather . town——一个类似神奇宝贝的 2D 虚拟环境,有你可以导航的小化身。只要碰到一堆其他头像,上面贴着参与者的名字,就可以启动一个即时视频会议,覆盖所有你家附近的人。使用交互式 pdf 覆盖的海报会议,当踩在他们的海报区地毯上时与演讲者建立联系,撞上某人并进行小聊天,或在桌子旁进行私人谈话,或邀请人们到你家或更现实的里约热内卢海滩,一切都真正带来了与你的同伴或结识新朋友建立联系的感觉。这是一个爆炸。这个小工具真的激发了人们的喜悦之情,他们喜欢面对面的交流,因为这感觉就像是真的一样。
作者图片
这使得从 it 部门启动我们的远程卡拉 ok 会议、参加鸡尾酒会或只是听一些现场音乐变得更加令人愉快。亲爱的组委会:你们做了出色的工作!
图片取自https://twitter.com/DrCh0le/status/1309232717272289280
更多有用的链接
- 所有论文都可以在会议论文集中免费获得
- 推特上的 RecSys 2020 挑战赛
- 谷歌大脑发布 TensorFlow 推荐器
- 在 AIcrowd 上重新发布 Spotify 百万播放列表数据集
- 更多即将发布的材料将在此处发布,敬请关注!
阿姆斯特丹的 RecSys 2021
带着所有这些信息和美好的经历,全球 RecSys 社区期待着 2021 年的第 15 次聚会。就我个人而言,我希望科罗纳疫情将得到很好的控制,然后允许一个存在的事件。如果是这样的话,全球 RecSys 社区将于 9 月 27 日至 10 月 1 日在阿姆斯特丹举行会议,地点就在阿姆斯特丹会议中心——阿姆斯特丹证券交易所的前身。因此,它的口号不足为奇:“一个聚会和交流的地方”。希望在那里见到你。
图片取自https://recsys.acm.org/recsys21/
基于奇异值分解的推荐系统变体的实现
模型包括 lightFM、RDF SVD 和神经协同过滤
一般来说,推荐算法的发展分为三个领域:基于内容的模型、协同过滤模型和基于深度神经网络的模型。在本文中,我们将重点讨论 CF,尤其是基于 SVD 的算法。我们不仅要练习建立基于奇异值分解的模型,还要练习它的变体,比如一个正则化模型和另一个采用神经网络的模型。
另外,基于 SVD 的分类模型只考虑了用户/iterm 交互。它既不考虑用户与项目交互时的上下文信息(例如:用户在地铁列车上浏览手机上的产品列表),也不利用隐藏在连续动作中的含义。然而,包含这些信息会使我们的模型更加复杂,因此我们不会在本文中讨论这些问题。如果你对应用这些技术感兴趣,欢迎查看 YouTube(2016) 和华为(2017) 发表的论文。这里也有相关的 git repos 供你参考——实现 YouTube 和华为。
熟悉数据集
我们这里使用的数据集来自作者 moorissa 的 github repo 。它由客户的杂货购买记录组成。首先,让我们看看数据是如何构造的。
采购记录的数据布局
显然,这个数据集还不能用于模型训练。products
列堆满了物品 id(与|
连接在一起)。我们需要对其进行预处理,并将其转换为模型期望接受的内容。
下面,我们将准备两种类型的数据格式,一种是用户-iterm 交互矩阵,另一种是长格式数据帧(见下面的演示)。
预期格式 01:用户-项目互动矩阵
用户-项目矩阵格式
在用户-项目交互矩阵中,每行代表一个用户,每列代表一个产品项目。对应索引(I,j)处的条目是重复购买的次数(或者更广义地解释为交互强度)。这种数据格式通常适用于传统的 SVD 建模。
预期格式 02:长格式数据帧
长格式数据帧
在长格式数据帧中,每一行代表一对(用户、物品)的重复购买次数(或交互强度)。参见上面的演示,我们有第一列为 customerId ,第二列为 productId ,因此是(用户,项目)对。最后一列 purchase_count 填入相应的购买次数。长格式数据帧是非常普遍和广泛使用的。它更适合基于神经网络(NN)的建模。
我们已经为数据预处理和转换设定了目标,现在让我们看看如何实现它。
逐步—数据预处理
步骤 01:转换数据类型和格式转换
下面的代码是从作者moorissa【1】那里借来的,他是这个数据集的同一个回购作者。主要的想法是拆分条目的字符串(最初用|
连接),并将其保存为 pd.Series。然后我们使用 pd.melt()将数据从宽格式转换为长格式。最后,我们应用 df.groupby()来聚合每个(用户、商品)购买记录。就是这样。现在,我们手头有了一个干净的长格式数据集。
我们可以直接在数据集上应用trx_transform()
来完成任务。
步骤 02:重新缩放数据集
我们知道每个用户去商场的频率不同。最终,每个用户的购买数量是不同的。为了让模型更好地正确、一致地解释每个用户的交互,我们最好将每个用户的购买计数重新调整到范围(0,1)。
data["max_count"] = data.groupby("customerId")["purchase_count"].transform(
lambda x: x.max())data["purchase_count_norm"] = data["purchase_count"] / data["max_count"]
步骤 03:创建用户-项目互动矩阵
在这一步,我们将应用df2interact_mat()
将长格式数据集转换为交互矩阵。代码借用本帖【2】稍作改编。
(可选)一旦我们创建了交互矩阵,我们也可以计算矩阵的稀疏度。当稀疏度大于 99.5%(意味着不到 0.5%的总条目为非零)时,协同过滤模型表现不佳。一种补救方法是删除一些几乎全是零的用户或项目,从而使矩阵变绿。
步骤 04:创建训练/测试集
此外,我们需要为模型评估创建训练/测试集。值得注意的一点是,在推荐建模下,训练/测试分割是相当不同的。在传统的训练/测试分裂中,我们从训练集中分离出行的子集作为测试集。然而,在这种情况下,我们采用类似于屏蔽的方法,对用户部分屏蔽一些项目。这就像假装用户还没有看到该项目。并使用未屏蔽的项目来训练模型。一旦模型准备好,然后我们使用屏蔽的项目作为测试集标签,并查看模型的预测是否正确地将这些项目包括在用户的推荐列表中。
推荐情况下的训练/测试分割
同样,我将它包装在一个函数train_test_split()
中,以简化这个过程。
使用该功能时,我们需要提供:
- 评级:用户-项目交互矩阵。
- split_count:(整数)每个用户从训练集转移到测试集的项目交互次数。
- fractions: (float)用户将他们的一些项目交互拆分到测试集中的部分。如果没有,则考虑所有用户。
该函数将返回三个对象。
- 列车组:列车交互矩阵
- 测试集:测试交互矩阵
- user_index:对测试集屏蔽了一些交互的用户索引,存储为索引以备后用
至此,我们手头已经有了训练和测试交互矩阵。然而,我们也可以在这里为神经网络建模准备长格式数据帧。
步骤 05:准备长格式数据帧
幸运的是,Scipy 首席运营官矩阵有一堆方便的方法。我们可以将这个矩阵转换成首席运营官数据类型。然后我们只需简单地调用 coo.row、coo.col 和 coo.data 来访问各自的索引和数据。最后,我们将这些片段放在一起,创建长格式数据集。
# Ref: [https://stackoverflow.com/a/36587845](https://stackoverflow.com/a/36587845)train_long = train.tocoo(copy=True)
test_long = test.tocoo(copy=True)# Access `row`, `col` and `data` properties of coo matrix.
train_long = pd.DataFrame({
'user_id': train_long.row,
'item_id': train_long.col,
'rating': train_long.data
})[['user_id', 'item_id',
'rating']].sort_values(['user_id', 'item_id']).reset_index(drop=True)# Apply the same operation on test data.
test_long = pd.DataFrame({
'user_id': test_long.row,
'item_id': test_long.col,
'rating': test_long.data
})[['user_id', 'item_id',
'rating']].sort_values(['user_id', 'item_id']).reset_index(drop=True)
开始建模吧!
恭喜你!我们刚刚完成了所有繁琐的数据预处理步骤。现在我们可以做一些更有趣的事情了。我们将总共测试三种型号。首先是 lightFM 模型,其次是 RDF 模型,最后是 NN 模型。
什么是因式分解机(FM)?
在讨论第二种模型时,我们将提出奇异值分解基本上是调频的一种特殊情况。但在此之前,让我们快速回顾一下什么是 FM,并了解一下 lightFM 模块如何利用这项技术。
下图清楚地展示了不同之处。
之前:
原创模型(鸣谢:陈鸿轩)
- FM 将对相互作用系数的估计( θ )简化为对潜在嵌入的估计( V )。
- 成对潜在嵌入的内积代替了成对交互系数估计(见下文)。
之后:
从这篇帖子解释因式分解模型
lightFM 丰富了什么?
lightFM 的作者在 FM 方法的基础上做了一点小小的修改,使得 lightFM 模型可以在新用户的推荐下使用。
正如在论文【4】中所述,lightFM 模块非常灵活。如果只提供用户-项目交互矩阵,那么模型只是一个矩阵分解模型。但是,我们也可以从用户和项目提供额外的功能。然后,该模型将合并这些信息,并转而使用因式分解机。下面的过程是 first lightFM 将这些特征以及用户/项目指示符(索引)转换为 K 维嵌入。每个用户或物品的表征是所有潜在嵌入特征的总和。(见下图)这种将用户或项目表示为特征嵌入总和的方式允许模型对新用户进行预测。这解决了协同过滤模型中最棘手的问题之一——冷启动问题。
旁注:冷启动意味着新用户或项目没有过去的用户-项目交互记录,因此不可能向新用户推荐或向现有用户推荐新项目。
lightFM 将用户/项目表示为其内容特征的线性组合。(来自论文)
一旦我们熟悉了 lightFM 背后的概念,让我们试着实现它。
第一个模型:通过 lightFM 模块构建推荐器
A.构建和训练模型
使用 lightFM 构建推荐器非常简单。只需实现下面的代码,并在模型初始化中指定自定义参数。
# Initialize model with self-defined configuration.model = LightFM(no_components=20,
learning_rate=0.005,
loss='bpr',
random_state=12)# Start training.model.fit(train, epochs=50, verbose=True) #train: interaction matrix
B.模型预测和评估
在完成模型训练后,我们可以使用两个指标来评估它。一个是 RMSE,另一个是 Top@K precision。
对于像我们这样的机器学习实践者来说,RMSE 是一个非常常见的指标。它可以衡量实际评分(或在这种情况下的购买强度)和预测评分之间的差异。同时,Top@K 用于衡量用户实际购买推荐的 top k 商品的比例。
现在,让我们使用模型进行预测。
# Ref: [https://
github.com/lyst/lightfm/blob/9ffeacbdc4688e9b58c6e5edfdeb52b037608a6b/lightfm/lightfm.py#L784](https://github.com/lyst/lightfm/blob/9ffeacbdc4688e9b58c6e5edfdeb52b037608a6b/lightfm/lightfm.py#L784)# predict() takes only numpy.array
predictions = model.predict(val_user_ids,
val_item_ids)
这里, val_user_ids 和 val_item_ids 来自长格式测试数据帧。
-计算 RMSE
我们定义了一个名为compute_rmse
的函数来帮助完成这项任务。
def compute_rmse(X_test, X_pred):
# Ref: [https://github.com/ncu-dart/rdf/blob/master/rdf/utils.py](https://github.com/ncu-dart/rdf/blob/master/rdf/utils.py)
sse = 0.
for i in range(len(X_test)):
sse += (X_test[i] - X_pred[i]) ** 2
return (sse / len(X_test)) ** .5
这个函数有两个参数。
- X_test: (arr)正常化的真实购买值
- X_pred: (arr)标准化的预测采购值
-计算 Top@K
在计算 Top@K 之前,我们需要首先将模型的预测转换为交互矩阵。
predict_mat = sparse.csr_matrix(
(predictions, (val_user_ids, val_item_ids)),
shape=(n_users, n_items)
)
然后,我们使用之前保存的 user_index 提取这些测试用户的购买记录。
test_sub = test[user_index] # true user purchase
predict_mat_sub = predict_mat[user_index] # predicted user purchase
最后,我们准备计算 Top@K。假设我们只想计算前 4 个预测项的准确性。我们只是运行下面的代码。
top_k = 4 # hyper-parameterpredict_top_k = []
for i in range(len(user_index)):
# csr.indices and csr.data only return non-zero entries
predict_r = predict_mat_sub[i].indices[
predict_mat_sub[i].data.argsort()[::-1]][:top_k]
true_r = test_sub[i].indices[
test_sub[i].data.argsort()[::-1][:top_k]]
pre = len(set(predict_r) & set(true_r))/ float(top_k)
predict_top_k.append(pre)np.mean(predict_top_k)
这样,我们可以获得 lightFM 模型的 RMSE 和 Top@K 性能,如下所示:
- RMSE: 0.721
- Top@K: 1
第二个模型:使用 RDF 方法构建推荐器
让我们在另一个叫做 RDF-SVD 的模型上进行实验——正则化微分函数。这个方法【5】是由台湾 NCU 的陈鸿轩教授和他的研究助理陈普提出的。
在讲 RDF 之前,我们先来回顾一下 SVD。在 SVD 的损失函数中,我们可以在每个系数估计上包括正则化项( λ )。它用于防止模型中的过度拟合。
具有偏差和正则项的奇异值分解损失函数(来自论文
特别是,\hat{r}(预测评级)等于…
奇异值分解预测额定值方程(来自论文
将上面的等式与第一个模型中的 FM 进行比较,可以清楚地看出 SVD 只是 FM 模型的一个特例。
陈教授的方法(RDF)也是 lightFM 的设计目标,旨在解决冷启动问题。lightFM 试图通过引入特征潜在嵌入来解决这个问题,而 RDF 则侧重于奇异值分解损失函数中的正则项。
总的想法是,对于已经被更多用户评级(或购买)的项目,我们可以对模型的预测有信心。这同样适用于那些比其他人评价更多项目的用户。因为我们从这些受欢迎的项目和活跃用户那里收集了更多的数据,所以建立在此基础上的模型不太可能过度拟合和不准确。尽管如此,对于评分较低的项目和活跃用户,我们没有太多的记录。因此,我们对这些项目或用户的系数估计使用了更大的正则项。这导致我们的损失函数如下:
带有正则化微分函数的损失函数(来自论文
等式中的“*****”代表项目被评分的次数或用户评分的次数。 f() 是一个单调递增的函数。基于本文的实验,采用这种改进的 RDF 的模型在精度上有了很大的提高。在这里,让我们尝试使用这种方法建立一个模型。
A.首先,导入 rdf
陈教授和将这种方法封装在一个叫做 rdf 的模块中。这是源代码链接。我们只需要在使用之前克隆 repo 并安装模块。
B.选择长格式数据帧作为训练/测试数据集
长格式数据帧
我们首先选择长格式数据帧作为输入数据集。然后,我们需要再次将它转换成 rdf 模块要求的格式。对于每条记录,它必须以下列格式存储。
[(客户标识,产品标识,评级),(客户标识,产品标识,评级),…]
下面的代码为我们完成了这项工作。
ll = train_long.apply(lambda x: (x[0], x[1], x[2]), axis = 1)
X = list(ll)# Apply the same operation on test data.
ll = test_long.apply(lambda x: (x[0], x[1], x[2]), axis = 1)
X_test = list(ll)
C.启动和训练模型
rdf 模块能够对各种算法应用微分正则化,包括 SVD、SVD++和 NMF。在本文中,为了简单起见,我们选择了基本的 SVD 模型,并应用了 repo 中的默认超参数。
model = rdf.rdfsvd.RDFSVD(n_users=n_users,
n_items=n_items,
lr=.005,
lmbda_p=500,
lmbda_q=500,
lmbda_u=.01,
lmbda_i=.01,
method="linear")model.train(X)
D.模型预测和评估
一旦模型训练完成,我们就可以用它在 RMSE 和 Top@K 上进行预测并评估其性能
# make predictions
X_pred = model.predict(X_test)
测试-(用户标识,项目标识,评级)元组上 RDF-SVD 模型预测的演示
将上述代码应用于 rmse 和 top@k 计算,我们得到如下指标:
- RMSE: 0.245
- Top@K: 1
第三个模型:使用神经协同过滤构建推荐器
最后,让我们尝试将深度学习框架整合到我们的推荐器中。这里让我们介绍一个非常通用的框架,叫做神经协同过滤(NCF)。这是报纸的链接【6】。另外,我强烈推荐 Steeve Huang 写的这篇中帖【7】。如果你想节省时间,跳过阅读原文,这是让你快速掌握 NCF 概貌的必读文章。
神经协同过滤模型(来自论文
A.模型算法
在论文中,作者提出了两个框架,一个是广义矩阵分解(GMF),如上图(左)所示,另一个是多层感知器(MLP),如上图(右)所示。在模型的最后一层,来自两个框架的输出将连接在一起,作为最终输出。
如果我们更仔细地观察,我们会发现曼氏金融只是 GMF 的另一个特例。
与 MF 的关系(信用: Steeve Huang )
Eq 1
是 GMF 下的用户-物品交互。 L 为激活函数, J_kx1 为输出层的边权重。在 GMF 框架下, L 可以是线性的也可以是非线性的, J_kx1 代表潜在向量中各维度的权重。
假设我们说 L 是一个线性激活函数,而 J_kx1 是一个所有值都为 1 的向量(意味着每个维度具有相同的权重),那么 GMF 将还原为原始 MF,即 pᵤ和qᵢ的内积,如Eq 3
所示。
我在此引用教皇的话:
在 NCF 框架下,物流可以很容易地推广和扩展…它将物流推广到一个非线性的设置,这可能比线性物流模型更有表现力。
然而,NCF 并没有就此止步。它还包括多层感知器(MLP)作为模型中的另一个分支,以便捕捉用户和项目之间的高阶非线性交互。我在这里引用:
我们可以赋予模型很大程度的灵活性和非线性,以了解 pᵤ和 qᵢ之间的相互作用,而不是像 GMF 那样只使用它们的固定元素乘积。
在 MLP 框架中,作者也做了几次尝试来测试多少层对模型来说是最合适的。一般来说,模型越深,精度越高。在基本结构中,它有 3 层,每层的节点比上一层减少一半。
还有另外三个有趣的点值得一提。
- 进行预培训:我们可以将 GLM 和 MLP 分开,对每个模型进行预培训。然后我们使用这些模型的权重作为 NCF 模型的初始化权重。实验表明,该方法比直接随机初始化 NCF 模型的权重具有更好的性能。
- 使用负抽样:对于那些用户没有交互的项目,我们可以进行负(不交互)到正(有交互)抽样。在训练集中添加一部分负样本可以提高模型的性能。但是比例是多少呢?在实验中,1:1 的正负比例并不能达到最佳效果。在训练集中,负样本的比率最好大于 1。作者最后选择了 4:1(阴性:阳性)。
- 输出层的加权初始化:这是模型调整的可选步骤。特别是在培训前的情况下。NCF 的输出层是 GMF 和 MLP 输出的拼接。我们可以做得更巧妙的是,给每个输出分配权重(α)和(1-α),以便对一个输出比另一个输出给予更多的重视。权重(α)是自定义的指定超参数。
在下面的实现中,为了简单起见,不包括上面提到的任何实践。我们只是构建 GMP 和 MLP 模型,并将两者结合起来。无负采样,两个输出无加权初始化(α)。
B.建立模型
这里我使用了 tensorflow 2.0 下的 tf.keras API 来构建模型。我主要采用了 Steeve Huang 的回购【8】的模型,只是对超参数做了一些修改,以便与原始论文中的参数保持一致。
建立 NCF 模型
C.训练模型
我们再次需要使用长格式数据集来训练我们的 NCF 模型。
from tensorflow.keras.callbacks import EarlyStopping# early stopping wait for 3 epoch
callbacks = [EarlyStopping(patience=3, restore_best_weights=True)]# train and test set
train_user_ids = train_long["user_id"]
train_movie_ids = train_long["item_id"]
train_ratings = train_long["rating"]val_user_ids = test_long["user_id"]
val_movie_ids = test_long["item_id"]
val_ratings = test_long["rating"]# train for 50 epochs
model.fit([train_user_ids, train_movie_ids],
train_ratings,
validation_data=(
[val_user_ids, val_movie_ids], val_ratings),
epochs=50,
batch_size=128,
callbacks=callbacks)
D.模型预测和评估
最后,下面是 NCF 在 RMSE 和 Top@K 上的表现指标
- RMSE: 0.231
- Top@K: 1
最终反射
我们在这里看到三个模型有趣的结果。所有模型在 Top@K 中得到了 1(在 Top 4 的情况下),意味着模型推荐的前 4 件商品实际上都是测试用户在现实中购买的。也许这不是偶然发生的。在数据预处理中,我尽量去除数据集中一些交互记录不多的项目或用户。它导致更致密的相互作用矩阵。手头剩余的项目和用户,大多数都有相当多的交互记录。因此,任何协作过滤模型表现良好都不足为奇。
尽管如此,让我们比较另一个指标,RMSE。我们看到,lightFM 的误差最大(0.72),NCF 的误差最小(0.23),尽管与 RDF-SVD 的误差(0.24)相差不大。
所以总的来说,我们可以说 NCF 仍然有最好的表现。
如果你对整个实现感兴趣,欢迎查看我的回购。希望你喜欢这篇文章,并欢迎提供任何进一步改进的反馈。干杯!
参考
[1] moorissa,如何构建购买数据的推荐系统(循序渐进)(2018)Github
[2]杰西·斯坦威格·伍兹,含蓄反馈推荐系统的温和介绍 (2016),个人博客
[3] Jimmy Wu, Factorization Machines — 稀疏資料的救星 (2019), Medium
[4]马切伊·库拉,用户和项目冷启动的元数据嵌入
建议 (2015),Lyst.com
[5]陈鸿轩和陈普,区分正则化权重——一种减轻推荐系统冷启动的简单机制(2019)美国计算机学会数据知识发现汇刊
[6] X 何等,神经协同过滤 (2017),国大
[7] Steeve Huang,论文综述:神经协同过滤解说&实现 (2018),走向数据科学
[8] Steeve Huang,2019, Github
推荐系统系列第 4 部分:用于协同过滤的矩阵分解的 7 种变体
RecSys 系列
矩阵分解的数学深度探究
更新: 本文是我探索学术界和工业界推荐系统系列文章的一部分。查看完整系列: 第一部分 , 第二部分 , 第三部分 , 第四部分 , 第五部分 和
协同过滤是任何现代推荐系统的核心,它已经在亚马逊、网飞和 Spotify 等公司取得了相当大的成功。它的工作原理是收集人们对某个特定领域的项目的判断(称为评级),并将具有相同信息需求或相同品味的人匹配在一起。协同过滤系统的用户共享他们对他们消费的每个项目的分析判断和意见,以便系统的其他用户可以更好地决定消费哪些项目。反过来,协同过滤系统为新项目提供有用的个性化推荐。
协同过滤的两个主要领域是(1)邻域方法和(2)潜在因素模型。
- 邻域法专注于计算项目之间或用户之间的关系。这种方法基于同一用户对相邻项目的评级来评估用户对项目的偏好。一个项目的邻居是由同一用户评价时倾向于获得相似评价的其他产品。
- 潜在因素法通过从评级模式中推断出的许多因素来描述项目和用户的特征,从而解释评级。例如,在音乐推荐中,发现的因素可能测量精确的维度,如嘻哈与爵士、高音量或歌曲长度,以及不太明确的维度,如歌词背后的含义,或完全无法解释的维度。对于用户来说,每个因素衡量用户有多喜欢在相应歌曲因素上得分高的歌曲。
一些最成功的潜在因素模型是基于**矩阵分解。**在其自然形式中,矩阵因子分解使用从项目评级模式推断的因子向量来表征项目和用户。项目和用户因素之间的高度对应导致推荐。
Sharmistha Chatterjee —使用 Python 的矩阵分解技术概述(https://towardsdatascience . com/Overview-of-Matrix-Factorization-Techniques-using-Python-8e 3d 118 a9 b 39)
在这篇文章和接下来的文章中,我将介绍推荐系统的创建和训练,因为我目前正在做这个主题的硕士论文。
- 第 1 部分提供了推荐系统的高级概述,它们是如何构建的,以及它们如何用于改善各行业的业务。
- 第 2 部分对正在进行的关于这些模型的优势和应用场景的研究计划进行了仔细的回顾。
- 第 3 部分提供了几个可能与推荐系统学者社区相关的研究方向。
在第 4 部分中,我深入探讨了矩阵分解的数学细节,这可以说是目前推荐系统研究中最常见的基线模型。更具体地说,我将向您展示可以构建的矩阵分解的七种变体——从边特征的使用到贝叶斯方法的应用。
1 —标准矩阵分解
一个简单的矩阵分解模型将用户和项目都映射到一个维度为 D 的联合潜在因素空间,这样用户-项目交互就被建模为该空间中的内积。
- 因此,每个项目 I 与向量 q_i 相关联,并且每个用户 u 与向量 p_u 相关联
- 对于一个给定的项目 I,q_i 的元素测量该项目拥有这些因素的程度,积极的或消极的。
- 对于给定的用户 u,p_u 的元素测量用户对项目的兴趣程度,该项目在相应的正面或负面因素上是高的。
- 得到的点积(q_i * p_u)抓住了用户 u 和物品 I 之间的交互,是用户对物品特征的整体兴趣。
因此,我们有如下等式 1:
最大的挑战是计算每个项目和用户到因子向量 q_i 和 p_u 的映射。矩阵分解通过最小化已知评级集的正则化平方误差来实现这一点,如下面的等式 2 所示:
通过拟合先前观察到的评级来学习该模型。然而,目标是以预测未来/未知评级的方式来概括那些先前的评级。因此,我们希望通过向每个元素添加 L2 正则化惩罚来避免过度拟合观察到的数据,并同时利用随机梯度下降来优化学习到的参数。
Shay Palachy 的这篇文章很好地解释了直觉,以下是快速注释:
- 当我们使用 SGD 来将模型的参数拟合到手头的学习问题时,我们在算法的每次迭代中在解空间中朝着损失函数相对于网络参数的梯度前进一步。由于我们推荐的用户-项目交互矩阵非常稀疏,这种学习方法可能会过度适应训练数据。
- L2(也称为吉洪诺夫正则化或岭回归)是一种正则化成本函数的特定方法,增加了一个表示复杂性的项。该项是用户和项目潜在因素的平方欧几里得范数。添加一个附加参数λ,以允许控制正则化的强度。
- 加入 L2 项通常会使整个模型的参数变得更小。
让我们看看这在代码中是什么样子的:
这个模型的完整实验可以在这里访问:https://github . com/khanhnamle 1994/transfer-rec/tree/master/Matrix-Factorization-Experiments/Vanilla-MF
2 —有偏差的矩阵分解
协作过滤的矩阵分解方法的一个好处是它在处理各种数据方面和其他应用特定的需求方面的灵活性。回想一下,等式 1 试图捕捉产生不同评级值的用户和项目之间的交互。然而,许多观察到的评分值的变化是由于与用户或项目相关的影响,称为偏差,与任何交互无关。这背后的直觉是,一些用户给出比其他人高的评级,一些项目系统地获得比其他人高的评级。
因此,我们可以将等式 1 扩展到等式 3,如下所示:
- 总体平均评级中包含的偏差用 b 表示。
- 参数 w_i 和 w_u 分别表示项目 I 和用户 u 与平均值的观测偏差。
- 请注意,观察到的评级分为 4 个部分:(1)用户-项目互动,(2)全球平均,(3)项目偏见,和(4)用户偏见。
通过最小化新的平方误差函数来学习该模型,如下面的等式 4 所示:
让我们看看这在代码中是什么样子的:
这个模型的完整实验可以在这里访问:https://github . com/khanhnamle 1994/transfer-rec/tree/master/Matrix-Factorization-Experiments/MF-bias
3 —带辅助特征的矩阵分解
协同过滤中的一个常见挑战是冷启动问题,因为它无法处理新项目和新用户。或者许多用户提供非常少的评级,使得用户-项目交互矩阵非常稀疏。缓解这个问题的一个方法是加入关于用户的额外信息来源,也就是的侧面特征。这些可以是用户属性(人口统计)和隐式反馈。
回到我的例子,假设我知道用户的职业。对于这个侧面特征,我有两种选择:将其作为一种偏见添加(艺术家比其他职业更喜欢电影)和作为一种向量添加(房地产经纪人喜欢房地产节目)。矩阵分解模型应该将所有信号源与增强的用户表示相结合,如等式 5 所示:
- 对职业的偏好用 d_o 表示,这意味着职业像速率一样变化。
- 职业的向量由 t_o 表示,意味着职业根据项目(q_i * t_o)而变化。
- 请注意,必要时项目可以得到类似的处理。
损失函数现在是什么样子的?下面的等式 6 表明:
让我们看看这在代码中是什么样子的:
这个模型的完整实验可以在这里访问:https://github . com/khanhnamle 1994/transfer-rec/tree/master/Matrix-Factorization-Experiments/MF-Side-Features
4-具有时间特征的矩阵分解
到目前为止,我们的矩阵分解模型是静态的。在现实中,项目受欢迎程度和用户偏好是不断变化的。因此,我们应该考虑反映用户-项目交互的动态性质的时间效应。为了实现这一点,我们可以添加一个影响用户偏好的时间项,从而影响用户和项目之间的交互。
为了更复杂一点,让我们用时间 t 的评级的动态预测规则来尝试下面的新等式 7:
- p_u (t)将用户因素作为时间的函数。另一方面,q_i 保持不变,因为项目是静态的。
- 我们根据用户的不同有职业的变化(p_u * t_o)。
等式 8 显示了包含时间特征的新损失函数:
让我们看看这在代码中是什么样子的:
这个模型的完整实验可以在这里访问:https://github . com/khanhnamle 1994/transfer-rec/tree/master/Matrix-Factorization-Experiments/MF-Temporal-Features
5 —因式分解机器
推荐系统的一个更强大的技术叫做因式分解机器,它具有强大的表达能力来概括矩阵因式分解方法。在许多应用程序中,我们有大量的项目元数据可以用来进行更好的预测。这是对特征丰富的数据集使用因式分解机的好处之一,对于这种情况,有一种自然的方式可以将额外的特征包括在模型中,并且可以使用维度参数 d 对高阶交互进行建模。对于稀疏数据集,二阶因式分解机模型就足够了,因为没有足够的信息来估计更复杂的交互。
Berwyn Zhang —因式分解机()http://Berwyn Zhang . com/2017/01/22/machine _ learning/Factorization _ Machines/)
等式 9 显示了二阶 FM 模型的情况:
其中 v 代表与每个变量(用户和项目)相关的 k 维潜在向量,括号运算符代表内积。根据 Steffen Rendle 关于因式分解机的原始论文,如果我们假设每个 x(j)向量仅在位置 u 和 I 处非零,我们得到带有偏差的经典矩阵因式分解模型(等式 3):
这两个方程之间的主要区别是因式分解机器在潜在向量方面引入了更高阶的相互作用,潜在向量也受到分类或标签数据的影响。这意味着模型超越了共现,以发现每个特征的潜在表示之间更强的关系。
因式分解机器模型的损失函数就是均方误差和特征集的和,如公式 10 所示:
让我们看看这在代码中是什么样子的:
这个模型的完整实验可以在这里访问:https://github . com/khanhnamle 1994/transfer-rec/tree/master/Matrix-Factorization-Experiments/Factorization-Machines
6-混合口味的矩阵分解
到目前为止,提出的技术隐含地将用户的口味视为单峰的——也就是在单个潜在向量中。这可能会导致在代表用户时缺乏细微差别,在这种情况下,主流口味可能会压倒更多的小众口味。此外,这可能降低项目表示的质量,减少属于多个品味/风格的项目组之间的嵌入空间的分离。
马切伊·库拉提出并评估了将用户表示为几种不同口味的混合物,由不同的口味向量来表示。每个味道向量都与一个注意力向量相关联,描述了它在评估任何给定项目时的能力。然后,用户的偏好被建模为所有用户口味的加权平均值,权重由每个口味与评估给定项目的相关程度提供。
等式 11 给出了这种混合味道模型的数学公式:
- U_u 是代表用户 u 的 m 个口味的 m×k 矩阵。
- A_u 是一个 m×k 矩阵,表示来自 U_u 的每一种口味的相似性,用于表示特定的项目。
- \sigma 是软最大激活函数。
- \sigma(A_u * q_i)给出混合概率。
- U_u * q_i 给出了每种混合物成分的推荐分数。
- 请注意,我们假设所有混合成分的单位方差矩阵。
因此,下面的等式 12 表示损失函数:
让我们看看这在代码中是什么样子的:
这个模型的完整实验可以在这里访问:https://github . com/khanhnamle 1994/transfer-rec/tree/master/Matrix-Factorization-Experiments/MF-Mixture-Tastes
7 —变分矩阵分解
我想介绍的矩阵分解的最后一个变体叫做变分矩阵分解。到目前为止,这篇博文讨论的大部分内容是关于优化模型参数的点估计,而 variable 是关于优化后验估计,粗略地说,它表达了一系列与数据一致的模型配置。
以下是改变的实际原因:
- 变分法可以提供替代的正则化。
- 变分法可以度量你的模型不知道的东西。
- 变分法可以揭示蕴涵以及分组数据的新方法。
我们可以通过以下方式改变等式 3 中的矩阵分解:(1)用来自分布的样本替换点估计,以及(2)用正则化新的分布替换正则化该点。数学是相当复杂的,所以我不会试图在这篇博文中解释它。关于变分贝叶斯方法的维基百科页面是一个有用的入门指南。最常见的变分贝叶斯使用 Kullback-Leibler 散度作为去相似性函数的选择,这使得损失最小化变得容易处理。
让我们看看这在代码中是怎样的:
这个模型的完整实验可以在这里访问:https://github . com/khanhnamle 1994/transfer-rec/tree/master/Matrix-Factorization-Experiments/variation-MF
模型评估
你可以在 this repository 查看我为 MovieLens1M 数据集做的所有 7 个矩阵分解实验。所有模型都经过 50 个时期的训练,结果在 TensorBoard 中捕获。评估指标是均方误差,即预测评分和实际评分之间所有平方差的总和。
结果表位于自述文件的底部,如您所见:
- 变分矩阵分解的训练损失最小。
- 具有边特征的矩阵分解具有最低的测试损失。
- 因式分解机器的训练时间最快。
结论
在这篇文章中,我讨论了矩阵分解的直观意义及其在协同过滤中的应用。我还谈到了它的许多不同的扩展:(1)添加偏见,(2)添加侧面特征,(3)添加时间特征,(4)升级到因子分解机器以利用高阶交互,(5)使用带有“注意”机制的混合口味,(6)使模型变得不同。我希望您已经发现这种深入矩阵分解世界的数学方法是有帮助的。请继续关注本系列未来的博客文章,这些文章将超越矩阵分解的领域,深入探讨协作过滤的深度学习方法。
参考
- 推荐系统的矩阵分解技术 *。*耶胡达·科伦,罗伯特·贝尔,克里斯·沃林斯基。2009 年 8 月
- py torch中简单灵活的深度推荐器。克里斯·穆迪。2018
现在继续上 推荐系统第五部分 !
如果你想关注我在推荐系统、深度学习和数据科学新闻方面的工作,你可以查看我的 中的 和GitHub,以及在【https://jameskle.com/】的其他项目。你也可以在 推特 , 直接发邮件给我 ,或者 在 LinkedIn 上找我 。 注册我的简讯 就在你的收件箱里接收我关于数据科学、机器学习和人工智能的最新想法吧!
推荐系统系列之五:用于协同过滤的多层感知器的 5 种变体
RecSys 系列
将神经架构引入建议
更新: 本文是我探索学术界和工业界推荐系统系列文章的一部分。查看完整系列: 第一部分 , 第二部分 , 第三部分 , 第四部分 , 第五部分 和
协同过滤算法是推荐系统应用中最常用的算法。由于互联网的使用和产生的大量信息,用户找到他们的偏好变成了一项非常乏味的任务。用户对项目的偏好以评分矩阵的形式表示,用于建立用户和项目之间的关系,以找到用户的相关项目。因此,目前的协同过滤算法面临着大数据集和评分矩阵稀疏的问题。
在各种协同过滤技术中,矩阵分解是最流行的一种,它将用户和项目投影到一个共享的潜在空间,用一个潜在特征向量来表示一个用户或一个项目。之后,用户对一个项目的交互被建模为他们潜在向量的内积。
Kiran Shivlingkar—Spotify 发现算法如何工作(https://blog . prototypr . io/How-Spotify-Discovery-Algorithm-Works-fae8f 63466 ab)
尽管矩阵分解对于协同过滤是有效的,但是众所周知,它的性能会受到交互作用函数的简单选择的阻碍:内积。例如,对于对显式反馈的评级预测的任务,众所周知,矩阵分解模型的性能可以通过将用户和项目偏好项合并到交互函数中来提高。虽然对于内积运算符来说,这似乎只是一个微不足道的调整,但它指出了设计一个更好的、专用的交互函数来建模用户和项目之间的潜在功能交互的积极效果。简单地线性组合潜在特征的乘积的内积可能不足以捕获用户交互数据的复杂结构。
在这篇文章和接下来的文章中,我将介绍推荐系统的创建和训练,因为我目前正在做这个主题的硕士论文。
- 第 1 部分提供了推荐系统的高级概述,它们是如何构建的,以及它们如何用于改善各行业的业务。
- 第 2 部分提供了关于这些模型的优势和应用场景的正在进行的研究计划的仔细回顾。
- 第 3 部分提供了几个可能与推荐系统学者社区相关的研究方向。
- 第 4 部分提供了可以构建的矩阵分解的 7 种变体的本质数学细节:从使用巧妙的侧面特征到应用贝叶斯方法。
阿汉·莫汉提— MLP 真实世界银行数据模型(https://becoming human . ai/multi-layer-perceptron-MLP-Models-on-Real-World-Banking-Data-f6dd 3d 7 e 998 f)
在第 5 部分中,我探索了使用多层感知器进行协同过滤。多层感知器是一种前馈神经网络,在输入层和输出层之间有多个隐藏层。它可以解释为非线性变换的堆叠层,用于学习等级要素制图表达。这是一个简洁但实用的网络,可以将任何可测量的函数逼近到任何期望的精确度(这种现象被称为通用逼近定理)。因此,它是许多高级方法的基础,并被广泛应用于许多领域。
更具体地说,我将浏览 5 篇将多层感知器整合到推荐框架中的论文。
1 —广泛而深入的学习
记忆和归纳对于推荐系统都是至关重要的。谷歌的论文“推荐系统的广泛和深度学习”(2016)提出了一个结合广泛线性模型和深度神经网络的优势的框架,以解决这两个问题。该框架已经在大规模商业应用商店 Google Play 的推荐系统上进行了生产和评估。
如下图所示,宽学习组件是一个单层感知器,可以使用叉积特征变换有效记忆稀疏特征交互。深度学习组件是一个多层感知器,可以通过低维嵌入归纳到以前看不到的特征交互。
Google Inc .—&深度学习推荐系统(https://arxiv.org/pdf/1606.07792.pdf)
从数学上来说,广泛学习被定义为:
其中 y 是预测值,x 是特征向量,W 是模型参数向量,b 是偏差。特征集包括原始输入和转换输入(通过叉积转换来获取特征之间的相关性)。
在深度学习组件中,每个隐藏层执行以下计算:
其中 l 是层数,f 是激活函数,a_l 是激活向量,b_l 是偏差向量,W_l 是第 l 层的模型权重向量。
通过融合这些模型来获得广泛和深度的学习模型:
其中 Y 是二进制类标签,W_{wide}是所有宽模型权重的向量,W_{deep}是应用于最终激活 a_{last}的权重的向量,b 是偏差项。
让我们看看这在代码中是什么样子的:
这种方法的完整 PyTorch 实现可以在这里查看:https://github . com/khanhnamle 1994/transfer-rec/tree/master/multi layer-Perceptron-Experiments/Wide-and-Deep-py torch。
2-深度因子分解机器
作为宽深度学习方法的扩展,由 Huifeng Guo 等人提出的“ DeepFM:一个基于因子分解机的神经网络用于 CTR 预测”(2017)是一个端到端的模型,无缝集成了因子分解机(宽组件)和多层感知器(深度组件)。与宽和深模型相比,DeepFM 不需要繁琐的特征工程。
如下图所示,因式分解机器利用加法和内积运算来捕获特征之间的线性和成对交互。多层感知器利用非线性激活和深层结构来模拟高阶交互。
郭慧峰等——DeepFM:一种基于因子分解机器的 CTR 预测神经网络())
从数学上来说,DeepFM 的输入是一个 m-fields 数据,由(u,I)对组成,它们是用户和项目的身份和特征,以及一个表示用户点击行为的二进制标签 y(y = 1 表示用户点击了项目,否则 y = 0)。这里的任务是建立一个预测模型,以估计用户在给定的上下文中点击特定应用程序的概率。
对于任何特定的特征 I,标量 w_i 用于衡量其一阶重要性,潜在向量 V_i 用于衡量其与其他特征的相互作用的影响。将 V_i 馈入宽分量以模拟二阶特征相互作用,馈入深分量以模拟高阶特征相互作用。包括 w_i、V_i 和网络参数在内的所有参数被联合训练用于组合预测模型:
其中 y_hat 是预测的 CTR(在 0 和 1 之间),y_{FM}是宽因式分解机组件的输出,y_{DNN}是多层感知器组件的输出。
在宽分量中,除了特征之间的线性(一阶)交互之外,因子分解机器将成对(二阶)特征交互建模为各个特征潜在向量的内积。当数据集稀疏时,这有助于非常有效地捕捉二阶特征交互。因式分解机的输出是一个加法单元和几个内积单元的和:
对于给定的特征 I 和 j,加法单位(第一项)反映一阶特征的重要性,内积单位(第二项)表示二阶特征相互作用的影响。
在深层组件中,多层感知器的输出如下所示:
其中|H|是隐藏层的数量,a 是嵌入层的向量输出,W 是模型权重的向量,b 是偏差单元的向量。
让我们看看这在代码中是什么样子的:
这种方法的完整 PyTorch 实现可以在这里查看:https://github . com/khanhnamle 1994/transfer-rec/tree/master/multi layer-Perceptron-Experiments/DeepFM-py torch。
3-极端深度因式分解机器
作为深度因子分解机的扩展,简训廉等人的《 xDeepFM:结合显式和隐式特征交互的推荐系统》(2018)可以对显式和隐式特征交互进行联合建模。显式高阶特征交互通过压缩交互网络 k 学习,而隐式高阶特征违反通过多层感知器学习。该模型也不需要人工特征工程,并且将数据科学家从繁琐的特征搜索工作中解放出来。
压缩交互网络的设计考虑了以下因素:
- 交互应用于向量级,而不是位级。
- 高阶特征相互作用被明确地测量。
- 网络的复杂性不会随着相互作用的程度呈指数增长。
压缩交互网络的结构非常类似于递归神经网络,其中下一个隐藏层的输出依赖于最后一个隐藏层和附加输入。各层嵌入向量的结构保持现状;因此,相互作用应用于向量水平。
简训廉等——xDeepFM:结合显式和隐式特征交互的推荐系统())
- 看上面的图 4a,中间张量 Z^{k+1}是沿着隐藏层 x^k 和原始特征矩阵 x⁰.的每个嵌入维度的外积计算每个隐藏层 x^k 的过程与计算机视觉中众所周知的卷积神经网络有很强的联系。在这里,Z^{k+1}可以被视为一种特殊类型的图像,而 W^{k,h}是一个过滤器。
- 如图 4b 所示,作者沿着嵌入维度将过滤器滑过 Z^{k+1},得到了一个隐藏向量 x{k+1}——这在计算机视觉中通常被称为特征图。因此,xk 是 H_k 个不同特征地图的集合。
- 图 4c 提供了压缩交互网络架构的概述。设 T 表示网络的深度。X^k 的每个隐藏层都与输出单元有联系。作者在隐藏层的每个特征图上应用和池,并为第 k 个隐藏层获得长度为 H_k 的池向量 p^k。隐藏层的所有池向量在连接到输出单元之前被连接:p+ = [p,p,…,p^T]
xDeepFM 通过广度和深度学习框架将上述压缩的交互网络与简单的多层感知器相结合。一方面,该模型既包括低阶特征交互,也包括高阶特征交互;另一方面,它还包含隐式和显式的特征交互。这里显示了架构。
简训廉等 xDeepFM:结合显式和隐式特征交互的推荐系统()
从数学上来说,产生的输出单位是:
其中 a 是原始特征的向量,x_{mlp}是普通多层感知器的输出向量,p+是交叉交互网络的输出向量。w 和 b 是可学习的参数,分别是权重和偏差。
让我们看看这在代码中是什么样子的:
这种方法的完整 PyTorch 实现可以在这里查看:https://github . com/khanhnamle 1994/transfer-rec/tree/master/multi layer-Perceptron-Experiments/xDeepFM-py torch
4-神经分解机器
另一个无缝集成因子分解机器和多层感知机的并行工作是 Xiangnan He 和 Tat-Seng Chua 的“用于稀疏预测分析的神经因子分解机器”(2017)。该模型将线性因式分解机器的有效性与非线性神经网络的强大表示能力结合起来,用于稀疏预测分析。
如下所示,其架构的关键是一种称为双线性交互池的操作,它允许神经网络模型在较低级别学习更多信息的功能交互。通过在双线性交互层上堆叠非线性层,作者能够深化浅层线性因式分解机,有效地建模高阶和非线性特征交互,以提高因式分解机的表达能力。与简单连接或平均低层嵌入向量的传统深度学习方法相比,双线性交互池的这种使用编码了更多信息的特征交互,极大地促进了后续“深度”层学习有意义的信息。
【https://arxiv.org/pdf/1708.05027.pdf】
让我们深入研究神经分解机器模型的数学。给定稀疏向量 x 作为输入,模型估计目标为:
其中第一项模拟特征数据的全局偏差,第二项模拟特征权重的全局偏差,第三项 f(x)是模拟特征交互的多层感知器(如图 2 所示)。f(x)的设计由这些层组件组成:
嵌入层
这是一个完全连通的图层,将每个要素投影到密集矢量表示中。设 v_i 为第 I 个特征的嵌入向量。然后在嵌入步骤之后,作者获得一组嵌入向量来表示输入特征向量 x。
由于 x 的可能的稀疏表示,作者仅包括非零特征的嵌入向量,其中 x_i 不等于 0。
双线性相互作用层
然后,嵌入集 V_x 被馈送到双线性交互层,双线性交互层是将一组嵌入向量转换成一个向量的汇集操作:
其中 v_i.x_j 表示两个向量 v_i 和 x_j 的按元素的乘积,该汇集的输出是 k 维向量,其编码嵌入空间中特征之间的二阶交互。
隐藏层
双线性交互池层之上是一堆完全连接的层,这些层能够学习要素之间的高阶交互。这些隐藏层的定义是:
其中 L 是隐藏层数;W_L、b_L 和 activation_L 分别对应于第 L 层的权重矩阵、偏置向量和激活函数。激活函数的选择可以是 sigmoid、tanh 或 ReLU,以非线性地学习高阶特征交互。
预测层
最后,最后一个隐藏层 z_L 的输出向量被转换成最终预测分数:
其中 h^T 表示预测层的神经元权重。
让我们看看这在代码中是什么样子的:
这种方法的完整 PyTorch 实现可以在这里查看:https://github . com/khanhnamle 1994/transfer-rec/tree/master/multi layer-Perceptron-Experiments/Neural-FM-py torch
5 —神经协同过滤
向南和等人的论文“神经协同过滤”(2018)将多层感知器用于从数据中学习交互函数的应用又推进了一步。这里注意,他们也是上面提到的神经因子分解机器论文的同一作者。他们形式化了一种协作过滤的建模方法,这种方法专注于隐式反馈**,它通过观看视频、购买产品和点击商品等行为间接反映用户的偏好。与评分和评论等显性反馈相比,隐性反馈可以被自动跟踪,因此对于内容提供商来说,收集隐性反馈要自然得多。然而,利用它更具挑战性,因为没有观察到用户满意度,并且存在负面反馈的固有稀缺性。**
用于模拟用户隐式反馈的用户-项目交互值 y_ui 可以是 1 或 0。值 1 指示在用户 u 和项目 I 之间存在交互,但是这并不意味着 u 喜欢 I。这在从隐式数据中学习时提出了挑战,因为它仅提供关于用户偏好的噪声信号。虽然观察到的条目至少反映了用户对项目的兴趣,但未观察到的条目可能只是缺失的数据,并且负面反馈自然很少。
作者采用多层表示来建模用户-项目交互 y_ui,如下所示,其中一层的输出作为下一层的输入。
- 底部的输入层由 2 个描述用户 u 和物品 I 的特征向量组成,可以定制以支持用户和物品的广泛建模。特别地,本文仅使用用户和物品的身份作为输入特征,通过一键编码将其转换为二值化的稀疏向量。有了这样一个输入的通用特征表示,通过使用内容特征来表示用户和项目,可以很容易地调整这个框架来解决冷启动问题。
- 在输入层之上是嵌入层**——一个将稀疏表示投影到密集向量的全连接层。在潜在因素模型的上下文中,所获得的用户/项目嵌入可以被视为用户/项目的潜在向量。**
- 用户嵌入和项目嵌入然后被馈送到多层神经架构(称为神经协同过滤层**)以将潜在向量映射到预测分数。神经协同过滤层的每一层都可以被定制以发现用户-项目交互的特定潜在结构。最后一个隐藏层 X 的尺寸决定了模型的能力。**
- 最终的输出层为预测得分 y-hat_ui,通过最小化 y-hat_ui 与其目标值 y_ui 之间的逐点损失来进行训练。
【https://www.comp.nus.edu.sg/~xiangnan/papers/ncf.pdf】)
上述框架可以用下面的评分函数来概括:
其中 y-hat_ui 是交互 y_ui 的预测得分,θ表示模型参数。f 是将模型参数映射到预测得分的多层感知器。更具体地,P 是用户的潜在因素矩阵,Q 是项目的潜在因素矩阵,v_u^U 是与用户特征相关联的辅助信息,而 v_i^I 是与项目特征相关联的辅助信息。
本文认为,传统的矩阵分解可以看作是神经协同过滤的一个特例。因此,将矩阵分解的神经解释与多层感知器相结合以形成更通用的模型是方便的,该模型利用矩阵分解的线性和多层感知器的非线性来提高推荐质量。
让我们看看这在代码中是什么样子的:
这种方法的完整 PyTorch 实现可以在这里查看:https://github . com/khanhnamle 1994/transfer-rec/tree/master/multi layer-Perceptron-Experiments/Neural-CF-py torch-version 2。
模型评估
你可以查看我在这个知识库中构建的所有五个基于多层感知器的推荐模型:https://github . com/khanhnamle 1994/transfer-rec/tree/master/Multilayer-Perceptron-Experiments。
- 数据集是 MovieLens 1M ,类似于我在上一篇文章中的矩阵分解实验。目标是预测用户对特定电影的评分——评分范围为 1 到 5。
- 唯一的区别是,为了使用基于因式分解机器的模型来预测点击率,我使用了二进制评级**。小于等于 3 的评级被视为 0,大于 3 的评级被视为 1。**
- 因此,考虑到这是一个二元分类问题(而不是像上次一样的 RMSE),评估度量是 AUC 。
- 所有模型都经过 100 个时期的训练,结果在 权重和偏差 中捕获。对于那些不熟悉的人来说,这是一个出色的工具,它将所有模型超参数和输出指标存储在一个地方,以跟踪实验并毫不费力地再现结果。
结果表位于自述文件的底部,正如您从重量和偏差仪表盘可视化中看到的:
- 广度和深度学习模型在测试和验证集上都具有最好的 AUC 结果。
- 另一方面,极深因式分解机分别具有最低的 AUC。
- 神经协同过滤运行速度最快,极深因式分解机运行速度最慢。
结论
在这篇文章中,我讨论了多层感知器的直观含义及其在协同过滤中的应用。我还浏览了使用 MLP 作为推荐框架的 5 篇不同的论文:(1)广泛和深度学习,(2)深度因子分解机,(3)极端深度因子分解机,(4)神经因子分解机,以及(5)神经协同过滤。这些模型补充了主流的浅层协同过滤模型,从而为基于深度学习的推荐开辟了一条新的研究途径。
请继续关注本系列未来的博文,它们超越了区分模型,进入了协同过滤的生成模型领域。
现在开始阅读 推荐系统系列第六部 !
参考
- 针对推荐系统的宽深度学习。Cheng-Tze Cheng、Levent Koc、Jeremiah Harmsen、Tal Shaked、Tushar Chandra、Hrishi Aradhye、Glen Anderson、Greg Corrado、、Mustafa Ispir、Rohan Anil、Zakaria Haque、Hong、Jain、和 Hemal Shah。2016 年 6 月
- DeepFM:基于因子分解机的神经网络,用于 CTR 预测 。郭慧峰,唐瑞明,叶云明,,何秀强。2017 年 3 月。
- **神经协同过滤 。何湘南,廖,汉王张,聂,,蔡达生。2017 年 8 月
- 用于稀疏预测分析的神经分解机 。何湘南和蔡达生。2017 年 8 月
- xDeepFM:结合显式和隐式特征交互的推荐系统 。连建勋、周小欢、张辅政、陈忠霞、谢星和。2018 年 5 月
如果你想关注我在推荐系统、深度学习和数据科学新闻方面的工作,你可以查看我的 中的 和GitHub,以及在https://jameskle.com/的其他项目。你也可以在 推特 , 直接发邮件给我 ,或者 在 LinkedIn 上找我 。 注册我的简讯 就在你的收件箱里接收我对机器学习研究和行业的最新想法吧!**