fast.ai 机器学习笔记(四)(2)https://developer.aliyun.com/article/1482644
持续时间 [4:32]
让我们来谈谈这个持续时间部分,起初可能看起来有点具体,但实际上并不是。我们要做的是看三个字段:“促销”
、“州假期”
、“学校假期”
所以基本上我们有一个表格:
- 对于每个商店,对于每个日期,那个商店在那天有促销活动
- 那个地区的那家店铺在那天有学校假期吗
- 那个地区的那家店铺在那天有州假期吗
这些事情是事件。带有事件的时间序列非常常见。如果你正在查看石油和天然气钻探数据,你试图说的是通过这根管道的流量,这里是一个代表何时触发了某个警报的事件,或者这里是一个钻头卡住的事件,或者其他。所以像大多数时间序列一样,某种程度上会倾向于代表一些事件。事件发生在某个时间点本身就很有趣,但很多时候时间序列也会显示事件发生前后的情况。例如,在这种情况下,我们正在进行杂货销售预测。如果即将到来一个假期,销售额在假期前后很可能会更高,在假期期间会更低,如果这是一个城市店铺的话。因为你要在离开前备货带东西,然后回来时,你就得重新填满冰箱,例如。虽然我们不必进行这种特征工程来专门创建关于假期前后的特征,但是神经网络,我们能够给神经网络提供它需要的信息,它就不必学习这些信息。它学习的越少,我们就能够利用我们已有的数据做更多事情,利用我们已有的规模架构做更多事情。因此,即使对于神经网络这样的东西,特征工程仍然很重要,因为这意味着我们将能够利用我们拥有的有限数据取得更好的结果,利用我们拥有的有限计算能力取得更好的结果。
因此,这里的基本思想是,当我们的时间序列中有事件时,我们希望为每个事件创建两个新列[7:20]:
- 还有多久这个事件再次发生。
- 上次那个事件发生已经多久了。
换句话说,距离下一个州假期还有多久,距离上一个州假期已经多久了。所以这不是我知道存在的库或任何其他东西。所以我手工写在这里。
def get_elapsed(fld, pre): day1 = np.timedelta64(1, 'D') last_date = np.datetime64() last_store = 0 res = [] for s,v,d in zip( df.Store.values, df[fld].values, df.Date.values ): if s != last_store: last_date = np.datetime64() last_store = s if v: last_date = d res.append(((d-last_date).astype('timedelta64[D]') / day1)) df[pre+fld] = res
因此,重要的是,我需要按店铺来做这个。所以我想说,对于这家店铺,上次促销是什么时候(即自上次促销以来多长时间),下次促销还有多长时间,例如。
我要做的是这样的。我将创建一个小函数,它将接受一个字段名,然后我将依次传递Promo
、StateHoliday
和SchoolHoliday
。让我们以学校假期为例。所以我们说字段等于学校假期,然后我们说get_elapsed('SchoolHoliday', 'After')
。让我告诉你这将会做什么。我们首先按店铺和日期排序。现在当我们循环遍历时,我们将在店铺内循环遍历。所以店铺#1,1 月 1 日,1 月 2 日,1 月 3 日,依此类推。
fld = 'SchoolHoliday' df = df.sort_values(['Store', 'Date']) get_elapsed(fld, 'After') df = df.sort_values(['Store', 'Date'], ascending=[True, False]) get_elapsed(fld, 'Before')
当我们循环遍历每家店铺时,我们基本上会说这一行是学校假期还是不是[8:56]。如果是学校假期,那么我们将跟踪名为last_date
的变量,表示我们看到学校假期的最后日期。然后我们将追加到我们的结果中自上次学校假期以来的天数。
使用zip
的重要性[9:26]
有一些有趣的特性。其中一个是使用zip。我实际上可以通过编写for row in df.iterrows():
然后从每行中获取我们想要的字段来更简单地编写这个。结果表明,这比我现在的版本慢 300 倍。基本上,遍历 DataFrame 并从行中提取特定字段具有很多开销。更快的方法是遍历 numpy 数组。因此,如果您取一个 Series(例如df.Store
),然后在其后添加.values
,那么就会获取该系列的 numpy 数组。
这里有三个 numpy 数组。一个是商店 ID,一个是fld
是什么(在这种情况下,那是学校假期),还有日期。因此,现在我想要循环遍历每个列表的第一个、第二个和第三个。这是一个非常常见的模式。我基本上在我写的每个笔记本中都需要做类似的事情。而要做到这一点的方法就是使用 zip。因此,zip
意味着逐个循环遍历这些列表。然后这里是我们可以从第一个列表、第二个列表和第三个列表中获取元素的地方:
因此,如果您还没有使用 zip 进行过多尝试,那么这是一个非常重要的函数需要练习。就像我说的,我几乎在我写的每个笔记本中都使用它——每次您都必须同时循环遍历一堆列表。
因此,我们将循环遍历每个商店、每个学校假期和每个日期。
问题:它是否循环遍历了所有可能的组合?不是。只有 111、222 等。
因此,在这种情况下,我们基本上想要说让我们获取第一个商店、第一个学校假期和第一个日期。因此,对于商店 1,1 月 1 日,学校假期是真还是假。因此,如果是学校假期,我会通过记录上次看到学校的日期来跟踪这一事实,并附加自上次学校假期以来的时间长度。如果商店 ID 与上一个商店 ID 不同,那么我现在已经到了一个全新的商店,这种情况下,我基本上必须重置一切。
问题:对于我们没有最后一个假期的第一个点会发生什么?是的,所以我只是将其设置为一些任意的起始点(np.datetime64()
),最终会得到,我记不清了,要么是最大的日期,要么是最小的日期。您可能需要在之后用缺失值或零替换它。不过好处是,由于 ReLU 的存在,神经网络很容易截断极端值。因此,在这种情况下,我没有对其做任何特殊处理。我最终得到了这些像负十亿日期时间戳,但仍然可以正常工作。
接下来要注意的是,我需要对训练集和测试集进行一些处理。在前一节中,我实际上添加了一个循环,对训练 DataFrame 和测试 DataFrame 进行以下操作:
对于每个数据框中的每个单元格,我都进行了以下操作:
接下来,有一系列单元格我首先要为训练集和测试集运行。在这种情况下,我有两个不同的单元格:一个将 df 设置为训练集,一个将其设置为测试集。
我使用的方法是,我只运行第一个单元格(即跳过 df=test[columns]
),然后运行下面的所有单元格,这样就可以对训练集进行全部操作。然后我回来运行第二个单元格,然后运行下面的所有单元格。因此,这个笔记本不是设计为从头到尾顺序运行的。但是它被设计为以这种特定方式运行。我提到这一点是因为这可能是一个有用的技巧。当然,你可以将下面的所有内容放在一个函数中,将数据框传递给它,并在测试集上调用一次,在训练集上调用一次。但我更喜欢有点实验,更交互地看着每一步。因此,这种方式是在不将其转换为函数的情况下在不同数据框上运行某些内容的简单方法。
如果我按店铺和日期排序,那么这就是在追踪上次发生某事的时间[15:11]。因此 d - last_date
最终会告诉我距离上次学校假期有多少天:
现在如果我按日期降序排列并调用完全相同的函数,那么它会告诉我距离下一个假期还有多久:
所以这是一个很好的技巧,可以将任意事件时间添加到你的时间序列模型中。例如,如果你现在正在进行厄瓜多尔杂货比赛,也许这种方法对其中的各种事件也会有用。
为了国家假期,为了促销,我们来做一下:
fld = 'StateHoliday' df = df.sort_values(['Store', 'Date']) get_elapsed(fld, 'After') df = df.sort_values(['Store', 'Date'], ascending=[True, False]) get_elapsed(fld, 'Before') fld = 'Promo' df = df.sort_values(['Store', 'Date']) get_elapsed(fld, 'After') df = df.sort_values(['Store', 'Date'], ascending=[True, False]) get_elapsed(fld, 'Before')
滚动函数[16:11]
我们在这里看的下一件事是滚动函数。在 pandas 中,滚动是我们创建所谓的窗口函数的方式。假设我有这样的一些数据。我可以说好,让我们在这个点周围创建一个大约 7 天的窗口。
然后我可以取得那七天窗口内的平均销售额。然后我可以在这里做同样的事情,取得那七天窗口内的平均销售额。
所以如果对每个点都这样做,并连接起那些平均值,你最终会得到一个移动平均值:
移动平均值的更通用版本是窗口函数,即将某个函数应用于每个点周围的一些数据窗口。很多时候,我在这里展示的窗口实际上并不是你想要的。如果你试图构建一个预测模型,你不能将未来作为移动平均的一部分。因此,通常你实际上需要一个在某个点结束的窗口(而不是点位于窗口中间)。那就是我们的窗口函数:
Pandas 允许你使用这里的滚动来创建任意窗口函数:
bwd = df[['Store']+columns].sort_index() \ .groupby("Store").rolling(7, min_periods=1).sum() fwd = df[['Store']+columns].sort_index(ascending=False) \ .groupby("Store").rolling(7, min_periods=1).sum()
第一个参数表示我想将函数应用到多少个时间步。第二个参数表示如果我处于边缘,换句话说,如果我处于上图的左边缘,你应该将其设置为缺失值,因为我没有七天的平均值,或者要使用的最小时间段数是多少。所以这里,我设置为 1。然后你还可以选择设置窗口在周期的开始、结束或中间。然后在其中,你可以应用任何你喜欢的函数。所以这里,我有我的按店铺每周求和。所以有一个很简单的方法来得到移动平均值或其他内容。
我应该提到,如果你去 Pandas 的时间序列页面,左侧有一个很长的索引列表。这是因为 Wes McKinney 创造了这个,他最初是在对冲基金交易中,我相信。他的工作都是关于时间序列的。所以我认为 Pandas 最初非常专注于时间序列,而且现在它可能仍然是 Pandas 最强大的部分。所以如果你在处理时间序列计算,你绝对应该尝试学习整个 API。关于时间戳、日期偏移、重采样等方面有很多概念性的内容需要理解。但这绝对值得,否则你将手动编写这些循环。这将比利用 Pandas 已经做的事情花费更多时间。当然,Pandas 将为你使用高度优化的向量化 C 代码,而你的版本将在 Python 中循环。所以如果你在处理时间序列的工作,学习完整的 Pandas 时间序列 API 是绝对值得的。它们几乎和其他任何时间序列 API 一样强大。
好的,经过所有这些,你可以看到这些起始值,我提到的 —— 稍微偏向极端。所以你可以看到,9 月 17 日,商店 1 距上次学校假期 13 天。16 日是 12,11,10,依此类推。
我们目前处于促销期。这里,这是促销前一天:
在它的左边,我们在上次促销之后有 9 天等等。这就是我们如何可以向我们的时间序列添加事件计数器的方式,当你在处理时间序列时,这通常是一个好主意。
分类与连续 [21:46]
现在我们已经做到了,我们的数据集中有很多列,所以我们将它们分成分类和连续列。我们将在回顾部分更多地讨论这一点,但这些将是我将为其创建嵌入的所有内容:
而 contin_vars
是我将直接输入模型的所有东西。例如,我们有 CompetitionDistance
,这是到最近竞争对手的距离,最高温度,以及我们有一个分类值 DayOfWeek
。所以这里,我们有最高温度,可能是 22.1,因为德国使用摄氏度,我们有到最近竞争对手的距离,可能是 321.7 公里。然后我们有星期几,也许星期六是 6。前两个数字将直接进入我们要输入神经网络的向量中。我们将在稍后看到,但实际上我们会对它们进行归一化,或多或少。但这个分类变量,我们不会。我们需要将它通过一个嵌入层。所以我们将有一个 7x4 的嵌入矩阵(例如,维度为 4 的嵌入)。这将查找第 6 行以获取四个项目。所以星期六将变成长度为 4 的向量,然后添加到这里。
这就是我们的连续和分类变量将如何工作。
然后我们将所有的分类变量转换为 Pandas 的分类变量,方式与之前相同:
for v in cat_vars: joined[v] = joined[v].astype('category').cat.as_ordered()
然后我们将应用相同的映射到测试集。如果在训练集中星期六是 6,apply_cats
确保在测试集中星期六也是 6:
apply_cats(joined_test, joined)
对于连续变量,确保它们都是浮点数,因为 PyTorch 期望所有东西都是浮点数。
for v in contin_vars: joined[v] = joined[v].fillna(0).astype('float32') joined_test[v] = joined_test[v].fillna(0).astype('float32')
然后这是我使用的另一个小技巧。
idxs = get_cv_idxs(n, val_pct=150000/n) joined_samp = joined.iloc[idxs].set_index("Date") samp_size = len(joined_samp); samp_size150000
这两个单元格(上面和下面)都定义了一个叫做joined_samp
的东西。其中一个将它们定义为整个训练集,另一个将它们定义为一个随机子集。所以我的想法是,我在样本上做所有的工作,确保一切都运行良好,尝试不同的超参数和架构。然后当我对此满意时,我会回过头来运行下面这行代码,说,好,现在让整个数据集成为样本,然后重新运行它。
samp_size = n joined_samp = joined.set_index("Date")
这是一个很好的方法,与我之前向您展示的类似,它让您可以在笔记本中使用相同的单元格首先在样本上运行,然后稍后回来并在完整数据集上运行。
数据标准化
现在我们有了joined_samp
,我们可以像以前一样将其传递给 proc_df 来获取因变量以处理缺失值。在这种情况下,我们传递了一个额外的参数do_scale=True
。这将减去均值并除以标准差。
df, y, nas, mapper = proc_df(joined_samp, 'Sales', do_scale=True) yl = np.log(y)
这是因为如果我们的第一层只是一个矩阵乘法。这是我们的权重集。我们的输入大约是 0.001 和另一个是 10⁶,例如,然后我们的权重矩阵已经初始化为 0 到 1 之间的随机数。然后基本上 10⁶的梯度将比 0.001 大 9 个数量级,这对优化不利。因此,通过将所有内容标准化为均值为零标准差为 1 开始,这意味着所有的梯度将在同一种规模上。
我们在随机森林中不需要这样做,因为在随机森林中,我们只关心排序。我们根本不关心值。但是对于线性模型和由线性模型层构建而成的东西,即神经网络,我们非常关心规模。因此,do_scale=True
为我们归一化我们的数据。现在,由于它为我们归一化了数据,它会返回一个额外的对象mapper
,其中包含了每个连续变量被归一化时的均值和标准差。原因是我们将不得不在测试集上使用相同的均值和标准差,因为我们需要我们的测试集和训练集以完全相同的方式进行缩放;否则它们将具有不同的含义。
因此,确保您的测试集和训练集具有相同的分类编码、相同的缺失值替换和相同的缩放归一化的细节非常重要,因为如果您没有做对,那么您的测试集根本不会起作用。但是如果您按照这些步骤操作,它将正常工作。我们还对因变量取对数,这是因为在这个 Kaggle 竞赛中,评估指标是均方根百分比误差。均方根百分比误差意味着我们根据我们的答案和正确答案之间的比率受到惩罚。我们在 PyTorch 中没有一个叫做均方根百分比误差的损失函数。我们可以编写一个,但更简单的方法是对因变量取对数,因为对数之间的差异与比率相同。因此,通过取对数,我们可以轻松地得到这个效果。
你会注意到 Kaggle 上绝大多数的回归竞赛要么使用均方根百分比误差,要么使用对数的均方根误差作为他们的评估指标。这是因为在现实世界的问题中,大多数时候,我们更关心比率而不是原始差异。因此,如果您正在设计自己的项目,很可能您会考虑使用因变量的对数。
然后我们创建一个验证集,正如我们之前学到的,大多数情况下,如果你的问题涉及时间因素,你的验证集可能应该是最近的时间段,而不是一个随机子集。所以这就是我在这里做的:
val_idx = np.flatnonzero( (df.index<=datetime.datetime(2014,9,17)) & (df.index>=datetime.datetime(2014,8,1)))
当我完成建模并找到一个架构、一组超参数、一定数量的 epochs 以及所有能够很好工作的东西时,如果我想让我的模型尽可能好,我会重新在整个数据集上进行训练 — 包括验证集。现在,至少目前为止,Fast AI 假设你有一个验证集,所以我的一种折中方法是将我的验证集设置为只有一个索引,即第一行:
val_idx=[0]
这样所有的代码都能继续运行,但实际上没有真正的验证集。显然,如果你这样做,你需要确保你的最终训练与之前的完全相同,包括相同的超参数、相同数量的 epochs,因为现在你实际上没有一个正确的验证集来进行检查。
问题:我有一个关于之前讨论过的 get_elapsed 函数的问题。在 get_elapsed 函数中,我们试图找出下一个假期还有多少天。所以每年,假期基本上是固定的,比如 7 月 4 日、12 月 25 日都会有假期,几乎没有变化。那么我们不能从以前的年份查找,然后列出今年将要发生的所有假期吗?也许可以。我的意思是,在这种情况下,我猜对于Promo
和一些假期会改变,比如复活节,这种方法可以适用于所有情况。而且运行起来也不会花太长时间。如果你的数据集太大,导致运行时间太长,你可以在一年内运行一次,然后以某种方式复制。但在这种情况下,没有必要。我总是把我的时间看得比电脑的时间更重要,所以我尽量保持事情尽可能简单。
创建一个模型
现在我们可以创建我们的模型。要创建我们的模型,我们必须像在 Fast AI 中一样创建一个模型数据对象。所以一个列模型数据对象只是一个代表训练集、验证集和可选测试集的标准列结构化数据的模型数据对象。
md = ColumnarModelData.from_data_frame( PATH, val_idx, df, yl.astype(np.float32), cat_flds=cat_vars, bs=128, test_df=df_test )
我们只需要告诉它哪些变量应该被视为分类变量。然后传入我们的数据框。
对于我们的每个分类变量,这里是它所拥有的类别数量。因此,对于我们的每个嵌入矩阵,这告诉我们该嵌入矩阵中的行数。然后我们定义我们想要的嵌入维度。如果你在进行自然语言处理,那么需要捕捉一个词的含义和使用方式的所有细微差别的维度数量经验性地被发现大约是 600。事实证明,当你使用小于 600 的嵌入矩阵进行自然语言处理模型时,结果不如使用大小为 600 的好。超过 600 后,似乎没有太大的改进。我会说人类语言是我们建模的最复杂的事物之一,所以我不会指望你会遇到许多或任何需要超过 600 维度的嵌入矩阵的分类变量。另一方面,有些事物可能具有相当简单的因果关系。例如,StateHoliday
——也许如果某事是假日,那么在城市中的商店会有一些行为,在乡村中的商店会有一些其他行为,就是这样。也许这是一个相当简单的关系。因此,理想情况下,当你决定使用什么嵌入大小时,你应该利用你对领域的知识来决定关系有多复杂,因此我需要多大的嵌入。实际上,你几乎永远不会知道这一点。你只知道这一点,因为也许别人以前已经做过这方面的研究并找到了答案,就像在自然语言处理中一样。因此,在实践中,你可能需要使用一些经验法则,并尝试一些经验法则后,你可以尝试再高一点,再低一点,看看哪种方法有帮助。所以这有点像实验。
cat_sz=[ (c, len(joined_samp[c].cat.categories)+1) for c in cat_vars ] cat_sz ''' [('Store', 1116), ('DayOfWeek', 8), ('Year', 4), ('Month', 13), ('Day', 32), ('StateHoliday', 3), ('CompetitionMonthsOpen', 26), ('Promo2Weeks', 27), ('StoreType', 5), ('Assortment', 4), ('PromoInterval', 4), ('CompetitionOpenSinceYear', 24), ('Promo2SinceYear', 9), ('State', 13), ('Week', 53), ('Events', 22), ('Promo_fw', 7), ('Promo_bw', 7), ('StateHoliday_fw', 4), ('StateHoliday_bw', 4), ('SchoolHoliday_fw', 9), ('SchoolHoliday_bw', 9)] '''
fast.ai 机器学习笔记(四)(4)https://developer.aliyun.com/article/1482648