完成表格的learner的正则化
今天之所以提及的这个平台的原因之一是稍后我们将会在本课深入讲卷积运算,届时我们会回到这里(platform.ai),结合卷积知识简单讲解platform.ai背后的原理,让大家有所了解。不过在这之前,我们要先结束上节课关于正则化的讨论。我们关于正则化的讨论,主要基于tabular_learner(表格数据学习器)。因为在tabular_learner里,
这是init方法,我们的目标是理解这里的每一行代码,但我们还没有达到这个要求。上周我们看过了adult数据集,这些数据集是过于简单的数据集, 仅供练手而已。这周,我们来看一个更加有趣的数据集。一个kaggle竞赛数据集。我们就能见识到世界上顶级的模型。比起学术论文的研究结果,kaggle比赛结果的记录往往更难打破。因为有更多的人致力于kaggle比赛,而不是大部分的学术数据集,因此在Kaggle比赛数据上做尝试,并且取得好名次,是非常有挑战的。
Rossmann数据集是欧洲3000家药房的数据,比赛目标是预测在接下来几周里Rossmann会出售多少产品。比较有趣的一点是,test set(测试数据集)在一个时间段上,比training set(训练集)更晚一些,这很常见,因为如果你要进行预测,就没有必要预测training set采集时间段内的数据结果,你要预测的是未来。
另外比较有趣的是,Rossmann提供的度量函数是Root Mean Squared Percent Error(均方根百分比误差)就是正常的均方根百分比误差(root mean squared error)。我们只是更进一步:。换言之,这就是百分比误差,平方求和,取平均,在开方。这些就是我觉得有趣的地方,比赛的排行榜也很有意思,第一名的成绩是.01,我们根据论文复现的模型,得分是0.105,0.106,3000第一个参赛队伍里的第十名得分大约是0.11,稍逊一些。
我们现在要跳过一些内容,就是主办方提供的比赛数据,主办方提供了少量数据,他们也允许参赛选手提供额外的外部数据,只要数据与其他参赛者进行共享,因此实际上,我们使用的数据集,有6-7个表格之多,学习如何连接整合表格,不是深度学习课程的一部分,我会跳过它,你们可以参考给程序员的机器学习介绍,里面会带你一步步做数据预处理。我们在rossman_data_clean.ipynb里提供了这个,你可以在里面看到整个的过程。你需要运行这个notebook来创建我们在这里(lesson6-rossmann.ipynb)用到的pickle文件:
时间序列和add_datepart
Rossmann data clean notebook里有一个地方要讲一下,你可以看到里面有个叫add_datepart的方法,我想解释一下这个是做什么的。
add_datepart(train, "Date", drop=False)
add_datepart(test, "Date", drop=False)
我一直说我们会学习时间序列,很多听我讲过这个的人以为我会做一些循环神经网络(recurrent neural network)。不是这样的。有意思的是,研究时间序列的主要学术人员是计量经济学家,他们倾向于研究特定类型的时间序列,唯一的数据类型是单变量的时间序列,就好像除了单一序列再无其他似的。然而现实生活中,基本没有这样的情况。一般我们会有一些关于它代表的商店的信息,或者关于它代表的客户/供应商的信息。我们有元数据,我们有相同时间段测量的时间序列信息,也有不同时间段测量的信息。所以大多数时候,我发现在实际中,使用来自现实世界的数据集的竞赛里,取得最好成绩的结果都没有用循环神经网络,而是会把时间因素考虑进去。比如在这个例子里,就是数据集里的日期,他们添加了一大堆元数据。对于我们的这个例子,我们加入了“Day of week”。因此我们有了一个日期,我们添加了星期、年、月、第几周、这个月里的第几天、这一年里的第几天、是否是月初/月末的布尔值、是否是季度初/季度末的布尔值、从1970年开始计算的时间戳,等等。
如果你运行这个函数add_datepart
,并传入日期信息,它会添加所有这些新的数据到你的数据集里。这意味着什么呢?
我们看个合理的例子,购买行为可能会受发薪日影响。发薪日可能是每月15号。如果你有“这个月里第几天”这样一列数据,程序就能识别出时间是不是每个月15号有一些不同的消费变化,这对应着嵌入矩阵中15号有着较高的数值。这种方法,从根本上讲,我们不能期望神经网络完成所有的特征工程。我们可以期待它能帮助寻找输入输出间的非线性关系和相互影响和类似的情形。但是像这样的格式的时间(2015-07-31 00:00:00),让神经网络发现每个月第15号这样有意思的事情,是不太容易的。如果我们能为网络提供相关信息就会好很多。
这是一个很有用的函数。你完成了这部分操作,可以像普通的表格问题一样处理很多种时间序列问题。我说的是“很多”,不是“所有”。如果有时间序列里有很复杂的和状态相关的东西,比如股票交易或类似的东西,这可能不适用,或者这不是你唯一要做的事。但在这个例子里,它可以给出很好的结果,在实践中,大多数时候,我发现它的效果很好。
结构化表格数据一般在Pandas处理,因此我们将个表格数据保存成标准的Python pickle文件。我们可以读取它们。我们可以看看前五行记录。
train_df.head().T
93 rows × 5 columns
这里的关键是,我们要对一个特定的时间和特定的商店id,预测销售额(sales)的值。sales是因变量。
预处理
首先要讲的是预处理(pre-processes)。你们已经学过了数据变换(transform)。数据变换是一组代码,每次运行时,会从数据集里抓取数据进行处理。它很适合做我们今天要学习的数据增强。这组代码会在每次取样时得到一个随机的值。数据预处理和数据变换很类似,但有一点区别,就是数据预处理主需要在网络训练前运行一次。重要的是,它们在训练集上运行一次,然后生成的所有相关状态和元数据会与验证集、测试集共享。
我来给你们看个例子。当我们做图像识别时,我们有一组类别,比如不同的宠物品种,品种信息已被转换成数字。做这个的东西就是在后台创建的一个预处理函数(preprocessor)。这保证训练集的类别和验证集、测试集的类别是一样的。我们这里也会做一些类似的事情。比如,如果我们从数据里取了一个小的子集做尝试。这是在处理一个新的数据集时很好的做法。
idx = np.random.permutation(range(n))[:2000]
idx.sort()
small_train_df = train_df.iloc[idx[:1000]]
small_test_df = train_df.iloc[idx[1000:]]
small_cont_vars = ['CompetitionDistance', 'Mean_Humidity']
small_cat_vars = ['Store', 'DayOfWeek', 'PromoInterval']
small_train_df = small_train_df[small_cat_vars + small_cont_vars + ['Sales']]
small_test_df = small_test_df[small_cat_vars + small_cont_vars + ['Sales']]
我随机取了2000个ID。然后分成一个小的训练集和小的测试集,各一半。然后取出5列。然后用这个做些尝试。简单好用。这是训练集的前几行:
small_train_df.head()
第一个展示给你们的与处理函数,叫做Categorify()。Categorify做的事情在图片识别里方面,和data.classes函数对因变量进行的处理的事情一样。它会从这些字符串中,找出全部独特的字符串,然后生成一个字符串对应的列表,然后把字符串转成数字。然后函数会把字符串转换成数字。因此,如果在训练集中调用这个函数,函数就会为这些数据创建类别(categpries)。在测试集中,我设置test = True就可以调用了,测试集跟之前的训练集使用的是相同的类别。现在我们调用pandas里的.head()值指令看起来一模一样。
这是因为Pandas把Promointerval转成了一个类别变量,内部存储了数字,对外显示的是字符串。 所有我们看一下Promointerve的类别。 用cat.categories 就会显示出来,这是标准的Pandas方法,向我展示了一张包含所有类的类别。它显示了一个列表,这在fastai里叫“classes”,在Pandas叫“categories”。
small_train_df.PromoInterval.cat.categories
Index(['Feb,May,Aug,Nov', 'Jan,Apr,Jul,Oct', 'Mar,Jun,Sept,Dec'], dtype='object')
small_train_df['PromoInterval'].cat.codes[:5]
280 -1
584 -1
588 1
847 -1
896 1
dtype: int8
如果我查看cat.codes
,你可以看到这里的list存的是数字,值是(-1, -1, 1, -1, 1)。这些-1代表什么?代表NaN,它们代表“缺失(missing,没有数据)”。Pandas用这个特殊的标志-1表示没有数据,NA。
你们知道,这些最终会放进一个embedding矩阵。我们不能在embedding矩阵里查找-1。所以在fastai内部,我们对所有这些都加了1。
Preprocessor: Fill Missing(填充缺失)
另外一个有用的预处理方法是FillMissing
。还是一样,你可以在data frame上调用它,在测试集调用时加上test=true
。
fill_missing = FillMissing(small_cat_vars, small_cont_vars)
fill_missing(small_train_df)
fill_missing(small_test_df, test=True)
small_train_df[small_train_df['CompetitionDistance_na'] == True]
这会对所有存在缺失内容的数据,创建一个额外的列,列名是在原来的列名后添加下划线加na(例如CompetitionDistance_na)。再新列中,缺失值的对应位置,通常被设置为True。然后我们用中位数替换掉 competition distance中的缺失值 。为什么这样做?通常,缺失数据本身就很耐人寻味(这个缺失可以帮助预测收入)。我们把这个信息放在一个布尔列里,这样深度学习模型可以用它来做预测。正如你所料,数据有所缺失,实际上反而有助于你做预测。因此我们当然想把这样的信息保留在一个便于管理的布尔数列中。这样我们的深度学习模型就能用它(布尔数列)来做预测。
但是,我们需要让 competition distance 是一个连续变量,这样我们可以在模型的连续变量部分使用它。我们可以用任何数替代它,因为是否缺失很重要,模型可以用CompetitionDistance_na 和 CompetitionDistance的关系来做预测。这就是FillMissing做的。
你不需要手工调用预处理方法。当你调用任何itemList创建函数时,将预处理函数传入ItemList的参量中,就像这样构造(与处理函数):
procs=[FillMissing, Categorify, Normalize]
data = (TabularList.from_df(df, path=path, cat_names=cat_vars, cont_names=cont_vars, procs=procs)
.split_by_idx(valid_idx)
.label_from_df(cols=dep_var, label_cls=FloatList, log=True)
.databunch())
这就是说“好,我想调用fill missing,categorify,normalize这几个函数,Normalize:对于连续变量,减去平均值,除以标准差,使训练更加轻松。这样,你就可把这些procs(预处理过后的数据)传入这个函数,就是这么简单。
data = (TabularList.from_df(df, path=path, cat_names=cat_vars, cont_names=cont_vars, procs=procs,)
.split_by_idx(valid_idx)
.label_from_df(cols=dep_var, label_cls=FloatList, log=True)
.add_test(TabularList.from_df(test_df, path=path, cat_names=cat_vars, cont_names=cont_vars))
.databunch())
然后,你可以执行data.export,这个指令可以把所有元数据都存储在数据堆里,然后你就可以在后续加载(这些数据),前提是你准确地知道类别编码是什么,用来替代缺失值的中位数是什么,以及归一化数据的平均值和标准差是什么。
类别变量,连续变量(Categorical and Continuous Variables)
要创建一个表格数据的data bunch,你要告诉它你的类别变量是什么、你的连续变量是什么。就像我们上周简要讲过的,你的类别不仅是字符串之类的东西,也包括星期、日期之类的。尽管它们是数字,它们也是类别变量。因为,比如说,日期(一个月里第几天),我认为它并不会拟合成光滑的曲线。我认为一个月的15号、1号和30号和一个月中的其它日期相比会有不同的消费行为。因此,如果我把这类参量作为一个类别变量,它最终会创建一个embedding矩阵,这些不寻常的日子可以显现出客户不同的购物行为。
你实际上必须仔细思考,哪些变量应该设计成类别变量。总的来说,当你对一个变量的类型有疑惑时,同时如果你的类别里的水平数量也不太多,这个数值关系被称作技术(cardinality),如果基数不是很高,我会把它当成类别变量。你可以每种都试下,看看哪个效果更好。
cat_vars = ['Store', 'DayOfWeek', 'Year', 'Month', 'Day', 'StateHoliday',
'CompetitionMonthsOpen', 'Promo2Weeks', 'StoreType', 'Assortment',
'PromoInterval', 'CompetitionOpenSinceYear', 'Promo2SinceYear', 'State',
'Week', 'Events', 'Promo_fw', 'Promo_bw', 'StateHoliday_fw',
'StateHoliday_bw','SchoolHoliday_fw', 'SchoolHoliday_bw']
cont_vars = ['CompetitionDistance', 'Max_TemperatureC', 'Mean_TemperatureC',
'Min_TemperatureC', 'Max_Humidity', 'Mean_Humidity', 'Min_Humidity',
'Max_Wind_SpeedKm_h', 'Mean_Wind_SpeedKm _h', 'CloudCover', 'trend',
'trend_DE','AfterStateHoliday', 'BeforeStateHoliday', 'Promo',
'SchoolHoliday']
dep_var = 'Sales'
df = train_df[cat_vars + cont_vars + [dep_var,'Date']].copy()
我们最终要传递到模型里去的data frame数据,其实就是训练集,它包含cat_vars(类别变量)、cont_vars(连续变量)、dep_var(因变量)还有date(时间)。
test_df['Date'].min(), test_df['Date'].max()
cut = train_df['Date'][(train_df['Date'] == train_df['Date'][len(test_df)])].index.max()
cut
valid_idx = range(cut)
df[dep_var].head()
我们根据日期创建验证集,验证集是这样设定的:截止记录结束时,验证集的数据量与测试集的数据量相当,这样我们就能很好地评估我们的模型了。我们现在可以创建一个TabularList(表格列表)可,这是你们先前看到过几次的,我们的标准API数据模块:
- from_df函数从dataframe传入所有信息,
- 使用split_by_idx吧数据分割成验证集和训练集。
- label_from_df对数据进行标注,其中还有因变量的信息
data = (TabularList.from_df(df, path=path, cat_names=cat_vars, cont_names=cont_vars, procs=procs,)
.split_by_idx(valid_idx)
.label_from_df(cols=dep_var, label_cls=FloatList, log=True)
.add_test(TabularList.from_df(test_df, path=path, cat_names=cat_vars, cont_names=cont_vars))
.databunch())
这个东西你可能没有见过,label class (label_cls=FloatList
)。这是我们的因变量(上面的df[dep_var].head()),可以看到,这是sales。它不是浮点型。它是int64。如果它是浮点型,fastai会自动认为你想做回归。但这不是浮点,它是一个整型。所以fastai会认为你想做分类。所以我们标注时,我们要告诉fastai我们想要的标签类别是什么,是一列浮点数,否则默认的类别是分类序列。经过这样的设置(fast.ai里)就自动把问题转变成回归问题。然后我们创建了一个data bunch。
关于Doc
doc(FloatList)
我想再提一下doc()
,用它你可以使我们了解其中更多的详情。这种情况下,在data blocks API中的所有标注函数,会把所有他们无法识别的关键词,传递给label_cls,这里我输入函数的一组数据就是log,实际上log最终会传递到FloatList里面,如果我运行doc(FloatList),我就可以看到一个摘要,我甚至还可以查看完整的文档。
文档中显示如果log是true,那么就会对因变量去对数。为啥这么做?程序这里实际上自动帮我的y去了log,这么做的原因正如前面提到的, 评估模型的度量函数,是均方根百分比误差,不管是fastai还是pytorch,都没有在损失函数中自带RMSPE这个指标,我也不太确定用这个指标的损失函数是否非常好用。
但如果你花时间思考一下,你就会发现这个比值的计算过程中对和首先去了log对数,然后对它们进行了求差而非求比值。换言之,你对y做log对数,最后得到的结果就是均方根误差(RMSE),这其实就是我们想要做的。我们对取对数,然后再直接使用均方根误差函数计算,均方根误差是回归问题中的默认指标,这里我们都不需要专门提及。
这里的log =True
这样设置, 是因为它太常见了,基本上任何时候做数值预测,比如预测人口或销售额,这些数据的分布都倾向于呈现长尾分布。在这种分布下,你会更加关注误差的百分比误差,而非精确的绝对值误差,因此你会想要设置函数内的参数log = True
,然后来测量均方根百分比误差(RNSPE).
y_range
max_log_y = np.log(np.max(train_df['Sales'])*1.2)
y_range = torch.tensor([0, max_log_y], device=defaults.device)
我们之前学过了y_range
,它用sigmoid帮助我们得到数据正确的范围。因为这一次的值首先取了对数,因此我们要确保y_range也要经过log计算处理,所以我在这里取了销售额这一列的最大值,并把值放大了1.2倍。我们曾经说过,这个区间比数据区间稍大一些会更好,然后我们对这些数据取log,这就是区间内新的最大值。因此y_range的范围就是0到比最大值大一点的数值。现在我们有了数据堆,然后就可以用它来创建数据学习期,我们要让y_range
也取对数。所以我取sales列的最大值。我会用比1大一点的数乘以它,记得吗,我们讲过你的range最好比数据的range宽一些。然后就可以用它来创建数据学习器。正如我们简单讨论过。