fast-ai-机器学习笔记-全-

fast.ai 机器学习笔记(全)

译者:飞龙

协议:CC BY-NC-SA 4.0

机器学习 1:第 1 课

原文:medium.com/@hiromi_suenaga/machine-learning-1-lesson-1-84a1dc2b5236

译者:飞龙

协议:CC BY-NC-SA 4.0

来自机器学习课程的个人笔记。随着我继续复习课程以“真正”理解它,这些笔记将继续更新和改进。非常感谢 JeremyRachel 给了我这个学习的机会。

简要课程大纲

根据时间和班级兴趣,我们将涵盖类似以下内容(不一定按照这个顺序):

训练 vs. 测试

  • 有效的验证集构建

树和集成

  • 创建随机森林

  • 解释随机森林

什么是机器学习?为什么我们使用它?

  • 什么构成了一个好的机器学习项目?

  • 结构化 vs 非结构化数据

  • 失败/错误的例子

特征工程

  • 领域特定 — 日期,URL,文本

  • 嵌入/潜在因子

使用 SGD 训练的正则化模型

  • 广义线性模型,Elasticnet 等(注意:查看 James 讲解的内容)

基本神经网络

  • PyTorch

  • 广播,矩阵乘法

  • 训练循环,反向传播

KNN

CV / bootstrap(糖尿病数据集?)

伦理考虑

跳过:

  • 降维

  • 交互

  • 监控训练

  • 协同过滤

  • 动量和学习率退火

随机森林:蓝皮书对于推土机

笔记本 / Kaggle

%load_ext autoreload
%autoreload 2
%matplotlib inline
from fastai.imports import *
from fastai.structured import *
from pandas_summary import DataFrameSummary
from sklearn.ensemble import RandomForestRegressor, RandomForestClassifier
from IPython.display import display
from sklearn import metrics

数据科学 ≠ 软件工程 [08:43]。你会看到一些不符合 PEP 8 的代码和import *之类的东西,但暂时跟着走一段时间。我们现在正在做的是原型模型,原型模型有一套完全不同的最佳实践,这些实践在任何地方都没有教授。关键是能够非常互动和迭代地进行操作。Jupyter 笔记本使这变得容易。如果你曾经想知道display是什么,你可以做以下三件事之一:

  1. 在一个单元格中键入display,然后按 shift+enter — 它会告诉你它来自哪里<function IPython.core.display.display>

  2. 在一个单元格中键入?display,然后按 shift+enter — 它会显示文档

  3. 在一个单元格中键入??display,然后按 shift+enter — 它会显示源代码。这对于 fastai 库特别有用,因为大多数函数都很容易阅读,而且不超过 5 行。

下载数据 [12:05]

参加 Kaggle 竞赛将让你知道你是否在这种模型中的这种数据上有竞争力。准确率低是因为数据太嘈杂,你无法做得更好吗?还是实际上是一个简单的数据集,而你犯了错误?当你在自己的项目中使用自己的数据集时,你将得不到这种反馈 — 我们只需要知道我们有良好的有效技术来可靠地构建基线模型。

机器学习应该帮助我们理解数据集,而不仅仅是对其进行预测 [15:36]。因此,选择一个我们不熟悉的领域,这是一个很好的测试,看看我们是否能够建立理解。否则,你的直觉可能会使你很难保持足够开放的心态去看待数据真正的含义。

有几种下载数据的选项:

  1. 下载到您的计算机并通过scp传输到 AWS

  2. 从 Firefox [17:32],按ctrl + shift + i打开 Web 开发者工具。转到Network选项卡,点击Download按钮,然后取消对话框。它会显示已启动的网络连接。然后右键单击它,选择Copy as cURL。粘贴命令并在末尾添加-o bulldozer.zip(可能删除 cURL 命令中的— — 2.0

Jupyter 技巧[21:39] - 您可以打开基于 Web 的终端,如下所示:

这个竞赛的目标是使用包含截至 2011 年底的数据的训练集来预测推土机的销售价格。

让我们看看数据[25:25]:

结构化数据:代表各种不同类型事物的列,如标识符、货币、日期、大小。

非结构化数据:图像

当您处理通常作为pd导入的结构化数据时,pandas是最重要的库。

df_raw = pd.read_csv(
    f'{PATH}Train.csv', 
    low_memory=False, 
    parse_dates=["saledate"]
)
  • parse_dates - 包含日期的任何列的列表

  • low_memory=False - 强制它读取更多文件以确定类型。

def display_all(df):
    with pd.option_context("display.max_rows", 1000): 
        with pd.option_context("display.max_columns", 1000): 
            display(df)
            display_all(df_raw.tail().transpose())

在 Jupyter Notebook 中,如果您键入一个变量名并按ctrl+enter,无论是 DataFrame、视频、HTML 等 - 它通常会找到一种显示方式供您使用[32:13]。

我们想要预测的变量在这种情况下被称为因变量,在这种情况下我们的因变量是SalePrice

问题:因为过拟合的风险而永远不应查看数据吗?[33:08] 我们至少想知道我们已经成功导入了足够的数据,但在这一点上通常不会真正研究它,因为我们不想对它做太多假设。许多书籍建议首先进行大量的探索性数据分析(EDA)。我们今天将学习机器学习驱动的 EDA。

项目目的 - 评估[34:06]

均方根对数误差。我们使用对数的原因是因为通常,您更关心的不是差$10,而是差 10%。所以如果是$1000,000 的物品,您差$100,000,或者如果是$10,000 的物品,您差$1,000 - 我们会认为这些是等价的规模问题。

df_raw.SalePrice = np.log(df_raw.SalePrice)
  • np - Numpy 让我们将数组、矩阵、向量、高维张量视为 Python 变量。

什么是随机森林?[36:37]

随机森林是一种通用的机器学习技术。

  • 它可以预测任何类型的东西 - 它可以是一个类别(分类),一个连续变量(回归)。

  • 它可以预测任何类型的列 - 像素、邮政编码、收入等(即结构化和非结构化数据)。

  • 它通常不会过度拟合,而且很容易阻止过度拟合。

  • 通常情况下不需要单独的验证集。即使只有一个数据集,它也可以告诉您它的泛化程度如何。

  • 它几乎没有任何统计假设。它不假设您的数据是正态分布的,关系是线性的,或者您已经指定了交互作用。

  • 它需要非常少的特征工程。对于许多不同类型的情况,您不必对数据取对数或将交互作用相乘。

问题:维度的诅咒是什么?[38:16] 你经常听到两个概念 - 维度的诅咒和没有免费午餐定理。它们两者在很大程度上是毫无意义的,基本上是愚蠢的,然而许多领域的人不仅知道这一点,而且认为相反,因此值得解释。维度的诅咒是这样一个想法,即你拥有的列越多,就会创造出一个越来越空的空间。有这样一个迷人的数学思想,即你拥有的维度越多,所有点就越多地位于该空间的边缘。如果你只有一个随机的维度,那么它们就会分散在各处。另一方面,如果是一个正方形,那么它们在中间的概率意味着它们不能在任一维度的边缘,因此它们不太可能不在边缘。每增加一个维度,点不在至少一个维度的边缘上的可能性就会成倍减少,因此在高维度中,一切都位于边缘。从理论上讲,这意味着点之间的距离变得不那么有意义。因此,如果我们认为这很重要,那么它会暗示当你有很多列并且没有小心删除你不关心的列时,事情将不起作用。出于许多原因,结果并非如此

  • 点之间仍然有不同的距离。只是因为它们在边缘上,它们仍然在彼此之间的距离上有所不同,因此这一点在这一点上比在那一点上更相似。

  • 所以像 k 最近邻居这样的东西在高维度中实际上表现得非常好,尽管理论家们声称的不同。这里真正发生的是,在 90 年代,理论主导了机器学习。有这样一个概念,支持向量机在理论上得到了很好的证明,极易进行数学分析,你可以证明关于它们的事情 - 我们失去了十年的真正实际发展。所有这些理论变得非常流行,比如维度的诅咒。如今,机器学习的世界变得非常经验主义,事实证明,在实践中,在许多列上构建模型确实效果非常好。

  • 没有免费午餐定理[41:08] - 他们声称没有一种模型适用于任何类型的数据集。在数学意义上,任何随机数据集的定义都是随机的,因此不会有一种方法可以查看每个可能的随机数据集,使其在某种程度上比其他方法更有用。在现实世界中,我们看的是不随机的数据。从数学上讲,我们会说它位于某个较低维度的流形上。它是由某种因果结构创建的。其中存在一些关系,因此事实是我们并没有使用随机数据集,因此实际上有一些技术比其他技术在你查看的几乎所有数据集上都要好得多。如今,有经验的研究人员研究哪些技术在大多数情况下效果很好。决策树的集成,其中随机森林是其中最常见的技术之一。Fast.ai 提供了一种标准的方法来适当地预处理它们并设置它们的参数。

scikit-learn[42:54]

Python 中最受欢迎和重要的机器学习包。它并非在所有方面都是最好的(例如,XGBoost 比梯度提升树更好),但在几乎所有方面都表现得相当不错。

m = RandomForestRegressor(n_jobs=-1)
  • RandomForestRegressor - 回归器是一种预测连续变量(即回归)的方法

  • RandomForestClassifier - 分类器是一种预测分类变量(即分类)的方法

m.fit(df_raw.drop('SalePrice', axis=1), df_raw.SalePrice)

scikit-learn 中的所有内容都具有相同的形式。

  • 为机器学习模型创建一个对象的实例

  • 通过传入独立变量(你要用来预测的东西)和因变量(你想要预测的东西)来调用fit

  • axis=1表示删除列。

  • 在 Jupyter Notebook 中按下shift + tab将显示函数的参数检查。

  • “类似列表”意味着任何你可以在 Python 中索引的东西。

以上的代码会导致错误。数据集“Conventional”中有一个值,它不知道如何使用该字符串创建模型。我们必须将大多数机器学习模型和随机森林转换为数字。因此,第一步是将所有内容转换为数字。

这个数据集包含了连续分类变量的混合。

  • continuous — 数字,其含义是数值,比如价格。

  • categorical — 要么是数字,其含义不是连续的,比如邮政编码,要么是字符串,比如“大”,“中”,“小”

以下是我们可以从日期中提取的一些信息 — 年份、月份、季度、月中的日期、星期几、一年中的周数、是否是假期?周末?下雨了吗?那天有体育赛事吗?这取决于你在做什么。如果你正在预测 SoMa 地区的苏打销售额,你可能想知道那天是否有旧金山巨人队的比赛。日期中包含的信息是你可以进行的最重要的特征工程之一,没有任何机器学习算法可以告诉你那天巨人队是否在比赛,以及这一点有多重要。因此,这就是你需要进行特征工程的地方。

add_datepart方法从完整的日期时间中提取特定的日期字段,以构建分类变量。在处理日期时间时,你应该始终考虑这个特征提取步骤。如果不将日期时间扩展到这些额外字段,你就无法捕捉到任何趋势/周期性行为,作为时间的函数在任何这些粒度上。

def add_datepart(df, fldname, **drop=True**):
    fld = df[fldname]
    if not np.issubdtype(fld.dtype, np.datetime64):
        df[fldname] = fld = pd.to_datetime(
            fld, infer_datetime_format=True)
    targ_pre = re.sub('[Dd]ate$', '', fldname)
    for n in (
        'Year', 'Month', 'Week', 'Day', 'Dayofweek', 
        'Dayofyear', 'Is_month_end', 'Is_month_start', 
        'Is_quarter_end', 'Is_quarter_start', 'Is_year_end', 
        'Is_year_start'
    ):
        df[targ_pre+n] = getattr(fld.dt,n.lower()) 
        df[targ_pre+'Elapsed'] = fld.astype(np.int64) // 10**9
    if drop: df.drop(fldname, axis=1, inplace=True)
  • getattr — 查找对象内部并找到具有该名称的属性

  • drop=True — 除非指定,它将删除日期时间字段,因为我们不能直接使用“saledate”,因为它不是一个数字。

fld = df_raw.saledate
fld.dt.year
  • fld — Pandas 系列

  • dtfld没有“year”,因为它只适用于 Pandas 系列,这些系列是日期时间对象。因此,Pandas 会将不同的方法拆分到特定于它们的属性中。因此,日期时间对象将有dt属性定义,那里你会找到所有日期时间特定的属性。

add_datepart(df_raw, 'saledate')
df_raw.saleYear.head()

问题:[55:40] df['saleYear']df.saleYear 之间有什么区别?在分配值时最好使用方括号,尤其是在列不存在的情况下。

运行add_datepart后,它添加了许多数字列并删除了saledate列。这还不足以解决我们之前看到的错误,因为我们仍然有其他包含字符串值的列。Pandas 有一个类别数据类型的概念,但默认情况下它不会将任何内容转换为类别。Fast.ai 提供了一个名为train_cats的函数,它会为所有是字符串的内容创建分类变量。在幕后,它创建了一个整数列,并将从整数到字符串的映射存储在其中。train_cats被称为“train”,因为它是特定于训练数据的。验证和测试集将使用相同的类别映射(换句话说,如果你在训练数据集中使用 1 表示“高”,那么在验证和测试数据集中 1 也应该表示“高”)。对于验证和测试数据集,使用apply_cats

train_cats(df_raw)
df_raw.UsageBand.cat.categories
'''
Index(['High', 'Low', 'Medium'], dtype='object)
'''
  • df_raw.UsageBand.cat — 类似于fld.dt.year.cat让你可以访问假设某个东西是一个类别的内容。

顺序并不太重要,但由于我们将创建一个在单个点(即 vs. vs. )分割事物的决策树,这有点奇怪。为了以合理的方式对它们进行排序,您可以执行以下操作:

df_raw.UsageBand.cat.set_categories(
    ['High', 'Medium', 'Low'],
    ordered=True, inplace=True
)
  • inplace将要求 Pandas 更改现有数据框而不是返回一个新的。

有一种称为“有序”的分类变量。有序分类变量具有某种顺序(例如“低” < “中” < “高”)。随机森林对此事实并不敏感,但值得注意。

display_all(df_raw.isnull().sum().sort_index()/len(df_raw))

上述操作将为每个系列添加一些空值,我们按索引排序它们([pandas.Series.sort_index](https://pandas.pydata.org/pandas-docs/stable/generated/pandas.Series.sort_index.html)),并除以数据集的数量。

读取 CSV 大约需要 10 秒,处理另外需要 10 秒,因此如果我们不想再等待,最好将它们保存下来。这里我们将以 feather 格式保存。这将以与 RAM 中相同基本格式保存到磁盘。这是迄今为止最快的保存和读取方式。Feather 格式不仅在 Pandas 中成为标准,而且在 Java、Apache Spark 等中也是如此。

os.makedirs('tmp', exist_ok=True)
df_raw.to_feather('tmp/bulldozers-raw')

我们可以这样读取它:

df_raw = pd.read_feather('tmp/raw')

我们将用它们的数字代码替换类别,处理缺失的连续值,并将因变量拆分为一个单独的变量。

df, y, nas = proc_df(df_raw, 'SalePrice')

structured.py 中的 proc_df

  • df — 数据框

  • y_fld — 依赖变量的名称

  • 它会复制数据框,获取依赖变量值(y_fld),并从数据框中删除依赖变量。

  • 然后它将fix_missing(见下文)

  • 然后我们将遍历数据框并调用numericalize(见下文)。

  • dummies — 有少量可能值的列,可以放入虚拟变量而不是数值化它们。但我们现在不会这样做。

fix_missing

  • 对于数值数据类型,首先我们检查是否有空列。如果有,它将创建一个新列,名称末尾附加_na,如果缺失则设置为 1;否则设置为 0(布尔值)。然后将缺失值替换为中位数。

  • 我们不需要为分类变量执行此操作,因为 Pandas 会自动处理它们并将它们设置为-1

numericalize

  • 如果不是数字且是分类类型,我们将用其代码加 1 替换该列。默认情况下,Pandas 对缺失使用-1,因此现在缺失将具有 ID 为0
df.head()

现在我们有所有的数值值。请注意,布尔值被视为数字。因此我们可以创建一个随机森林。

m = RandomForestRegressor(n_jobs=-1)
m.fit(df, y)
m.score(df,y)

随机森林是极易并行化的 — 意味着如果您有多个 CPU,可以将数据分配到不同的 CPU 上并且它会线性扩展。因此,您拥有的 CPU 越多,花费的时间就会按照该数字减少(不完全准确,但大致如此)。n_jobs=-1告诉随机森林回归器为每个 CPU 创建一个单独的作业/进程。

m.score将返回 r²值(1 是好的,0 是坏的)。我们将在下周学习 r²。

哇,r²为 0.98 — 那很棒,对吧?嗯,也许不是...

机器学习中最重要的想法之一是拥有单独的训练和验证数据集。作为动机,假设您不将数据分割,而是使用全部数据。假设您有很多参数:

欠拟合和过拟合

图片中数据点的误差对于最右侧的模型最低(蓝色曲线几乎完美地穿过红色点),但这并不是最佳选择。为什么呢?如果您收集一些新的数据点,它们很可能不会在右侧图表中的那条曲线上,而是会更接近中间图表中的曲线。

这说明如何使用所有数据可能导致过拟合。验证集有助于诊断这个问题。

def split_vals(a,n): 
    return a[:n].copy(), a[n:].copy()
n_valid = 12000  # same as Kaggle's test set size
n_trn = len(df)-n_valid
raw_train, raw_valid = split_vals(df_raw, n_trn)
X_train, X_valid = split_vals(df, n_trn)
y_train, y_valid = split_vals(y, n_trn)
X_train.shape, y_train.shape, X_valid.shape
'''
((389125, 66), (389125,), (12000, 66))
'''

基础模型

通过使用验证集,您会发现验证集的 r²为 0.88。

def rmse(x,y): 
    return math.sqrt(((x-y)**2).mean())
def print_score(m):
    res = [
        rmse(m.predict(X_train), y_train),
        rmse(m.predict(X_valid), y_valid),
        m.score(X_train, y_train), 
        m.score(X_valid, y_valid)
    ]
    if hasattr(m, 'oob_score_'): 
        res.append(m.oob_score_)
    print(res)
m = RandomForestRegressor(n_jobs=-1)
%time m.fit(X_train, y_train)
print_score(m)
'''
CPU times: user 1min 3s, sys: 356 ms, total: 1min 3s
Wall time: 8.46 s
[0.09044244804386327, 0.2508166961122146, 
 0.98290459302099709, 0.88765316048270615]
'''

*[训练集 rmse,验证集 rmse,训练集 r²,验证集 r²]

如果您查看 Kaggle 竞赛的公共榜单,RMSE 为 0.25 的排名将在前 25%左右。随机森林非常强大,这种完全标准化的过程对任何数据集都非常好。

下节课之前

请尝试使用这个过程尽可能多地解决 Kaggle 竞赛。您很可能会惊喜地发现,仅仅一小时的讲座您就能做得相当不错。

机器学习 1:第 2 课

原文:medium.com/@hiromi_suenaga/machine-learning-1-lesson-2-d9aebd7dd0b0

译者:飞龙

协议:CC BY-NC-SA 4.0

来自机器学习课程的个人笔记。随着我继续复习课程以“真正”理解它,这些笔记将继续更新和改进。非常感谢 JeremyRachel 给了我这个学习的机会。

随机森林深入研究

笔记本 / 视频

在接下来的几堂课中,我们将研究:

  • 随机森林的实际工作原理

  • 如果它们不能正常工作该怎么办

  • 优缺点是什么

  • 我们可以调整什么

  • 如何解释结果

Fastai 库是一组实现最先进结果的最佳技术。对于结构化数据分析,scikit-learn 有很多优秀的代码。因此,fastai 的作用是帮助我们将事物转换为 scikit-learn,然后从 scikit-learn 中解释事物。

正如我们所指出的,深入理解评估指标是非常重要的。

均方根对数误差(RMSLE):

因此,我们取价格的对数并使用均方根误差(RMSE)。

df_raw.SalePrice = np.log(df_raw.SalePrice)

然后我们通过以下方式将数据集中的所有内容转换为数字:

  • add_datepart — 提取日期时间特征 Elapsed 表示自 1970 年 1 月 1 日以来经过的天数。

  • train_cats — 将 string 转换为 pandas category 数据类型。然后我们通过运行 **proc_df** 将分类列替换为类别代码。

  • proc_df 还用中位数替换连续列的缺失值,并添加名为 [column name]_na 的列,并将其设置为 true 以指示它是缺失的。

m = RandomForestRegressor(n_jobs=-1)
m.fit(df, y)
m.score(df, y)
'''
0.98304680998313232
'''

什么是 R²?

Jeremy 的回答

  • yi : 实际/目标数据

  • ȳ : 平均值

  • SStot: 数据变化的程度

  • 上周我们想出的最简单的非愚蠢模型是创建一个平均值的列并将其提交给 Kaggle。在这种情况下,RMSE = SStot(即一个天真模型的 RMSE)

  • fi: 预测

  • SSres 是实际模型的 RMSE

  • 如果我们正好像只预测平均值一样有效,SSres/SStot = 1 并且 R² = 0

  • 如果我们完美(即 yi = fi 对于所有情况),SSres/SStot = 0 并且 R² = 1

R²的可能范围是多少?

正确答案:任何等于或小于 1 的值。如果你为每一行预测无穷大,R² = 1 −∞

因此,当你的 R²为负数时,这意味着你的模型比预测平均值更差。

R² 不一定是你实际尝试优化的内容,但它是一个你可以用于每个模型的数字,你可以开始感受到 0.8 看起来是什么样子,0.9 看起来是什么样子。你可能会发现有趣的是创建具有不同随机噪声量的合成 2D 数据集,并查看它们在散点图上的样子和它们的 R²,以了解它们与实际值有多接近。

R²是你的模型有多好(RMSE)与天真的平均模型有多好(RMSE)之间的比率。

过拟合 [17:33]

在我们的案例中,R²= 0.98 是一个非常好的模型。然而,可能会出现像右边这样的情况:

它擅长运行我们给定的点,但不会很好地运行我们没有给定的点。这就是为什么我们总是希望有一个验证集的原因。

创建验证集是进行机器学习项目时最重要的事情。您需要做的是提出一个数据集,您的模型在该数据集上的得分将代表您的模型在真实世界中的表现如何。

如果您的数据集中有一个时间部分(如蓝皮书比赛中),您可能希望预测未来的价格/价值等。Kaggle 所做的是在训练集中给我们提供代表特定日期范围的数据,然后测试集呈现了训练集中没有的未来日期集。因此,我们需要创建一个具有相同属性的验证集:

def split_vals(a,n): 
    return a[:n].copy(), a[n:].copy()
n_valid = 12000  # same as Kaggle's test set size
n_trn = len(df)-n_valid
raw_train, raw_valid = split_vals(df_raw, n_trn)
X_train, X_valid = split_vals(df, n_trn)
y_train, y_valid = split_vals(y, n_trn)
X_train.shape, y_train.shape, X_valid.shape
'''
((389125, 66), (389125,), (12000, 66))
'''

现在我们有了一个希望看起来像 Kaggle 测试集的东西-足够接近,使用这个将给我们相当准确的分数。我们想要这样做的原因是因为在 Kaggle 上,您只能提交很多次,如果您提交得太频繁,最终您会适应排行榜。在现实生活中,我们希望构建一个在生产中表现良好的模型。

问题:您能解释验证集和测试集之间的区别吗[20:58]?今天我们要学习的一件事是如何设置超参数。超参数是会改变模型行为的调整参数。如果您只有一个保留集(即一个您不用来训练的数据集),并且我们用它来决定使用哪组超参数。如果我们尝试一千种不同的超参数组合,我们可能最终会过拟合到那个保留集。因此,我们要做的是有一个第二个保留集(测试集),在那里我们可以说我已经尽力了,现在就在最后一次,我要看看它是否有效。

必须实际上从数据中删除第二个保留集(测试集),将其交给其他人,并告诉他们在您承诺完成之前不要让您看到它。否则很难不去看它。在心理学和社会学领域,这被称为复制危机或 P-值调整。这就是为什么我们想要有一个测试集。

问题:我们已经将分类变量转换为数字,但其他模型使用独热编码将其转换为不同的列-应该使用哪种方法[22:55]?我们今天将解决这个问题。

基础模型[23:42]

正如您所看到的,训练集上的 R²为 0.982,验证集上仅为 0.887,这让我们认为我们过拟合得相当严重。但并不是太糟糕,因为 RMSE 为 0.25 会让我们进入比赛的前 25%。

问题:为什么不选择随机行集作为验证集[24:19]?因为如果我们这样做,我们将无法复制测试集。如果您实际上查看测试集中的日期,您会发现这些日期比训练集中的任何日期都要新。因此,如果我们使用一个随机样本作为验证集,那将更容易,因为我们正在预测在这一天的工业设备的价值,而我们已经有了那一天的一些观察结果。一般来说,每当您构建具有时间元素的模型时,您希望您的测试集是一个单独的时间段,因此您确实需要您的验证集也是一个单独的时间段。

问题:最终不会过拟合验证集吗?[25:30] 是的,实际上这就是问题所在。最终可能会在验证集上过拟合,当你在测试集上尝试或提交到 Kaggle 时,结果可能并不好。这在 Kaggle 竞赛中经常发生,他们实际上有第四个数据集,称为私人排行榜集。每次提交到 Kaggle 时,你实际上只会得到在公共排行榜集上的表现反馈,你不知道它们是哪些行。在比赛结束时,你将根据完全不同的数据集进行评判,称为私人排行榜集。避免这种情况的唯一方法是成为一个优秀的机器学习从业者,并尽可能有效地设置这些参数,这部分我们今天和未来几周将会做。

PEP8[27:09]

def rmse(x,y): 
    return math.sqrt(((x-y)**2).mean())

这是一个代码不符合 PEP8 规范的例子。能够用眼睛一次看到某些东西,并随着时间学会立即看出发生了什么具有很大的价值。在数据科学中,始终使用特定的字母或缩写表示特定的含义是有效的。但是如果你在家里面试中测试,要遵循 PEP8 标准。

执行时间[29:29]

如果你加上%time,它会告诉你花了多长时间。经验法则是,如果某个操作花费超过 10 秒,那么用它进行交互式分析就太长了。所以我们要确保事情能在合理的时间内运行。然后当我们一天结束时,我们可以说好了,这个特征工程,这些超参数等都运行良好,我们现在将以大慢精确的方式重新运行它。

加快速度的一种方法是将 subset 参数传递给 proc_df,这将随机抽样数据:

df_trn, y_trn, nas = proc_df(
    df_raw, 'SalePrice', 
    subset=30000, 
    na_dict=nas
)
X_train, _ = split_vals(df_trn, 20000)
y_train, _ = split_vals(y_trn, 20000)
  • 请确保验证集不会改变

  • 还要确保训练集与日期不重叠

如上所示,在调用split_vals时,我们没有将结果放入验证集。_表示我们丢弃了返回值。我们希望保持验证集始终相同。

在将训练集重新采样为 30,000 个子集中的前 20,000 个后,运行时间为 621 毫秒。

构建单棵树[31:50]

我们将建立由树组成的森林。让我们从树开始。在 scikit-learn 中,他们不称之为树,而是估计器

m = RandomForestRegressor(
    n_estimators=1, 
    max_depth=3,
    bootstrap=False, 
    n_jobs=-1
)
m.fit(X_train, y_train)
print_score(m)
  • n_estimators=1 — 创建只有一棵树的森林

  • max_depth=3 — 使其成为一棵小树

  • bootstrap=False — 随机森林会随机化很多东西,我们希望通过这个参数关闭它

这棵小的确定性树在拟合后的 R²为 0.4028,所以这不是一个好模型,但比平均模型好,因为它大于 1,我们实际上可以绘制[33:00]:

一棵树由一系列二进制决策组成。

  • 第一行表示二进制分割标准

  • 根节点的samples为 20,000,因为这是我们在拆分数据时指定的。

  • 较深的颜色表示较高的value

  • value是价格的对数的平均值,如果我们构建了一个模型,只使用平均值,那么均方误差mse将为 0.495

  • 我们能够做出的最佳单一二进制分割结果是Coupler_system ≤ 0.5,这将使mse在错误路径上提高到 0.109;在正确路径上为 0.414。

我们想要从头开始构建一个随机森林[36:28]。第一步是创建一棵树。创建树的第一步是创建第一个二进制决策。你打算如何做?

  • 我们需要选择一个变量和一个值来分割,使得这两个组尽可能不同

  • 对于每个变量,对于该变量的每个可能值,看看哪个更好。

  • 如何确定哪个更好?取两个新节点的加权平均值

  • 得到的模型将类似于平均模型——我们有一个具有单一二进制决策的模型。对于所有coupler_system大于 0.5 的人,我们将填入 10.345,对于其他人,我们将填入 9.363。然后我们将计算这个模型的 RMSE。

现在我们有一个单一数字来表示一个分割有多好,这个数字是创建这两个组的均方误差的加权平均值。我们还有一种找到最佳分割的方法,就是尝试每个变量和每个可能的值,看哪个变量和哪个值给出了最佳得分的分割。

问题:是否有情况下最好分成 3 组?在一个级别上永远不需要做多次分割,因为你可以再次分割它们。

这就是创建决策树的全部过程。停止条件:

  • 当达到所请求的限制(max_depth

  • 当你的叶节点只有一个元素时

让我们的决策树更好

现在,我们的决策树的 R²为 0.4。让我们通过去掉max_depth=3来使其更好。这样做后,训练 R²变为 1(因为每个叶节点只包含一个元素),验证 R²为 0.73——比浅树好,但不如我们希望的那么好。

m = RandomForestRegressor(
    n_estimators=1, 
    bootstrap=False, 
    n_jobs=-1
)
m.fit(X_train, y_train)
print_score(m)
'''
[6.5267517864504e-17, 0.3847365289469930, 1.0, 0.73565273648797624]
'''

为了让这些树更好,我们将创建一个森林。要创建一个森林,我们将使用一种称为bagging的统计技术。

Bagging

迈克尔·乔丹开发了一种称为小自助袋的技术,他展示了如何对任何类型的模型使用 bagging,使其更加稳健,并为您提供置信区间。

随机森林——一种 bagging 树的方法。

那么什么是 bagging?Bagging 是一个有趣的想法,即如果我们创建了五个不同的模型,每个模型只是有些预测性,但这些模型给出的预测彼此之间没有相关性。这意味着这五个模型对数据中的关系有着截然不同的见解。如果你取这五个模型的平均值,你实际上是将每个模型的见解带入其中。因此,平均模型的这种想法是一种集成技术。

如果我们创建了很多树——大的、深的、过度拟合的树,但每棵树只选择数据的随机 1/10。假设我们这样做了一百次(每次使用不同的随机样本)。它们都过度拟合了,但由于它们都使用不同的随机样本,它们在不同的方面以不同的方式过度拟合。换句话说,它们都有错误,但这些错误是随机的。一堆随机错误的平均值是零。如果我们取这些树的平均值,每棵树都是在不同的随机子集上训练的,那么错误将平均为零,剩下的就是真正的关系——这就是随机森林。

m = RandomForestRegressor(n_jobs=-1) 
m.fit(X_train, y_train) 
print_score(m)

n_estimators默认为 10(记住,estimators 就是树)。

问题:你是在说我们平均了 10 个糟糕的模型,然后得到了一个好模型吗?确实是这样。因为这些糟糕的模型是基于不同的随机子集,它们的错误之间没有相关性。如果错误是相关的,这种方法就行不通。

这里的关键见解是构建多个比没有好的模型,而且错误尽可能不相关的模型。

要使用的树的数量是我们要调整的第一个超参数,以实现更高的度量。

问题:您选择的子集,它们是互斥的吗?是否可以重叠?[52:27]我们讨论了随机选择 1/10,但 scikit-learn 默认情况下是对n行进行替换选择n行——这称为自助法。如果记忆无误,平均而言,63.2%的行将被表示,其中许多行将多次出现。

机器学习中建模的整个目的是找到一个模型,告诉您哪些变量很重要,它们如何相互作用以驱动您的因变量。在实践中,使用随机森林空间来找到最近邻居与欧几里得空间之间的区别是一个模型做出良好预测和做出无意义预测之间的区别。

有效的机器学习模型能够准确地找到训练数据中的关系,并且能够很好地泛化到新数据[55:53]。在装袋中,这意味着您希望每个单独的估计器尽可能具有预测性,但希望每棵树的预测尽可能不相关。研究界发现,更重要的事情似乎是创建不相关的树,而不是更准确的树。在 scikit-learn 中,还有另一个称为ExtraTreeClassifier的类,它是一种极端随机树模型。它不是尝试每个变量的每个分割,而是随机尝试几个变量的几个分割,这样训练速度更快,可以构建更多的树——更好的泛化。如果您有糟糕的单独模型,您只需要更多的树来获得一个好的最终模型。

提出预测[1:04:30]

preds = np.stack([t.predict(X_valid) for t in m.estimators_]) 
preds[:,0], np.mean(preds[:,0]), y_valid[0]
'''
(array([ 9.21034,  8.9872 ,  8.9872 ,  8.9872 ,  8.9872 ,  9.21034,  8.92266,  9.21034,  9.21034,  8.9872 ]),  
9.0700003890739005,  
9.1049798563183568)
'''
preds.shape
'''
(10, 12000)
'''

每棵树都存储在名为estimators_的属性中。对于每棵树,我们将使用验证集调用predictnp.stack将它们连接在一起形成一个新轴,因此结果preds的形状为(10, 12000)(10 棵树,12000 个验证集)。对于第一个数据的 10 个预测的平均值为 9.07,实际值为 9.10。正如你所看到的,没有一个单独的预测接近 9.10,但平均值最终相当不错。

这里是给定前i棵树的 R²值的图。随着我们添加更多的树,R²值会提高。但似乎已经趋于平缓。

正如你所看到的,添加更多的树并没有太大帮助。它不会变得更糟,但也不会显著改善事情。这是要学会设置的第一个超参数——估计器的数量。一种设置的方法是,尽可能多地拟合,而且实际上似乎有所帮助。

添加更多的树会减慢速度,但使用更少的树仍然可以获得相同的见解。所以当 Jeremy 构建大部分模型时,他从 20 或 30 棵树开始,项目结束或当天工作结束时,他会使用 1000 棵树并在夜间运行。

袋外(OOB)得分[1:10:04]

有时您的数据集会很小,您不想提取验证集,因为这样做意味着您现在没有足够的数据来构建一个好的模型。然而,随机森林有一个非常聪明的技巧,称为袋外(OOB)误差,可以处理这种情况(以及更多!)

我们可以意识到,在我们的第一棵树中,一些行没有用于训练。我们可以通过第一棵树传递那些未使用的行,并将其视为验证集。对于第二棵树,我们可以通过未用于第二棵树的行,依此类推。实际上,我们将为每棵树创建一个不同的验证集。为了计算我们的预测,我们将对所有未用于训练的行进行平均。如果您有数百棵树,那么很可能所有行都会在这些袋外样本中多次出现。然后,您可以在这些袋外预测上计算 RMSE、R²等。

m = RandomForestRegressor(
    n_estimators=40, 
    n_jobs=-1, 
    oob_score=True
)
m.fit(X_train, y_train)
print_score(m)
'''
[0.10198464613020647, 0.2714485881623037, 0.9786192457999483, 0.86840992079038759, 0.84831537630038534]
'''

oob_score设置为 true 将执行此操作,并为模型创建一个名为oob_score_的属性,如您在 print_score 函数中看到的,如果具有此属性,它将在最后打印出来。

问题oob_score_难道不总是低于整个森林的分数吗[1:12:51]?准确率往往较低,因为在 OOB 样本中,每行出现的树较少,而在完整树集中出现的次数较多。因此,OOB R²会稍微低估模型的泛化能力,但是您添加的树越多,这种低估就越不严重。

在设置超参数时,OOB 分数会派上用场[1:13:47]。我们将设置相当多的超参数,并希望找到一种自动化的方法来设置它们。一种方法是进行网格搜索。Scikit-learn 有一个名为网格搜索的函数,您可以传入要调整的所有超参数的列表以及要尝试的所有这些超参数的值。它将在所有这些超参数的所有可能组合上运行您的模型,并告诉您哪一个是最佳的。OOB 分数是一个很好的选择,可以告诉您哪一个是最佳的。

子采样[1:14:52]

之前,我们取了 30,000 行,并创建了使用该 30,000 行不同子集的所有模型。为什么不每次取一个完全不同的 30,000 子集?换句话说,让我们保留全部 389,125 条记录,如果我们想加快速度,每次选择一个不同的 30,000 子集。因此,而不是对整个行集进行自助抽样,只需随机抽取数据的一个子集。

df_trn, y_trn = proc_df(df_raw, 'SalePrice')
X_train, X_valid = split_vals(df_trn, n_trn)
y_train, y_valid = split_vals(y_trn, n_trn)
set_rf_samples(20000)

set_rf_samples:与之前一样,我们在训练集中使用 20,000 个样本(之前是 30,000,这次是 389,125)。

这将花费与之前相同的时间来运行,但是每棵树都可以访问整个数据集。在使用 40 个估计器后,我们得到了 R²分数为 0.876。

问题:这个 OOB 分数是在哪些样本上计算的[1:18:26]?Scikit-learn 不支持这个功能,因此set_rf_samples是一个自定义函数。因此,在使用set_rf_samples时,需要关闭 OOB 分数,因为它们不兼容。reset_rf_samples()将把它恢复到原来的状态。

最重要的提示[1:20:30]:大多数人总是使用最佳参数在所有时间内在所有数据上运行所有模型,这是毫无意义的。如果您想找出哪些特征重要以及它们之间的关系,那么准确度的第四位小数点根本不会改变您的任何见解。在足够大的样本量上运行大多数模型,使得您的准确度合理(在最佳准确度的合理距离内),并且训练时间短,以便您可以交互式地进行分析。

另外两个参数[1:21:18]

让我们为这个完整集合建立一个基准来进行比较:

reset_rf_samples()
m = RandomForestRegressor(
    n_estimators=40, 
    n_jobs=-1, 
    oob_score=True
)
m.fit(X_train, y_train)
print_score(m)
'''
[0.07843013746508616, 0.23879806957665775, 0.98490742269867626, 0.89816206196980131, 0.90838819297302553]
'''

这里 OOB 高于验证集。这是因为我们的验证集是不同的时间段,而 OOB 样本是随机的。预测不同时间段要困难得多。

min_sample

m = RandomForestRegressor(
    n_estimators=40, 
    min_samples_leaf=3, 
    n_jobs=-1, 
    oob_score=True
) 
m.fit(X_train, y_train) 
print_score(m)
'''
[0.11595869956476182, 0.23427349924625201, 0.97209195463880227, 0.90198460308551043, 0.90843297242839738]
'''
  • min_sample_leaf=3:当叶节点具有 3 个或更少的样本时停止训练树(之前我们一直下降到 1)。这意味着将减少一到两个决策级别,这意味着我们需要训练的实际决策标准数量减半(即更快的训练时间)。

  • 对于每棵树,我们不仅仅取一个点,而是取至少三个点的平均值,我们期望每棵树都能更好地泛化。但是每棵树本身的能力会稍微减弱。

  • 效果很好的数字是 1、3、5、10、25,但相对于您的整体数据集大小而言。

  • 通过使用 3 而不是 1,验证 R²从 0.89 提高到 0.90

max_feature [1:24:07]

m = RandomForestRegressor(
    n_estimators=40, 
    min_samples_leaf=3, 
    max_features=0.5, 
    n_jobs=-1, 
    oob_score=True
) 
m.fit(X_train, y_train) 
print_score(m)
'''
[0.11926975747908228, 0.22869111042050522, 0.97026995966445684, 0.9066000722129437, 0.91144914977164715]
'''
  • max_features=0.5:这个想法是,树之间的相关性越小,越好。想象一下,如果有一列比其他所有列更好地预测,那么您构建的每棵树总是从那一列开始。但是可能存在一些变量之间的相互作用,其中该相互作用比单个列更重要。因此,如果每棵树总是首次在相同的内容上分裂,那么这些树的变化就不会很大。

  • 除了取一部分行之外,在每个单独的分割点,取不同的列子集。

  • 对于行抽样,每棵新树都基于一组随机行,对于列抽样,每个单独的二元分割,我们从不同的列子集中选择。

  • 0.5 意味着随机选择其中一半。您可以使用特殊值,如sqrtlog2

  • 使用的好值是10.5log2sqrt

0.2286 的 RMSLE 将使我们进入这场比赛的前 20 名——只需使用一些无脑的随机森林和一些无脑的次要超参数调整。这就是为什么随机森林不仅是机器学习的第一步,而且通常是唯一的一步。很难搞砸。

为什么随机森林效果如此好[1:30:21]

让我们看看小单树中的一个分割点。

fiProductClassDesc ≤ 7.5将分割:

为什么这样做有效呢?想象一下,唯一重要的是液压挖掘机,履带−0.0 到 2.0 公吨,其他都不重要。它可以通过首先分割fiProductClassDesc ≤ 5.5然后fiProductClassDesc > 4.5来选择单个元素。只需两次分割,我们就可以提取出一个单一类别。即使是具有分类变量的树也是无限灵活的。如果有一个具有不同价格水平的特定类别,它可以通过多次分割逐渐缩小到这些组。随机森林非常易于使用,而且非常弹性。

下一课,我们将学习如何分析模型,了解更多关于数据的信息,使其变得更好。

机器学习 1:第 3 课

原文:medium.com/@hiromi_suenaga/machine-learning-1-lesson-3-fa4065d8cb1e

译者:飞龙

协议:CC BY-NC-SA 4.0

来自机器学习课程的个人笔记。随着我继续复习课程以“真正”理解它,这些笔记将继续更新和改进。非常感谢 JeremyRachel 给了我这个学习的机会。

今天的课程内容:

笔记本 / 视频

通过使用机器学习更好地理解数据

  • 这个想法与常见的说法相反,即随机森林等东西是隐藏我们意义的黑匣子。事实恰恰相反。随机森林让我们比传统方法更深入更快地理解我们的数据。

如何查看更大的数据集

问题:何时使用随机森林[2:41]?我无法想到任何绝对不会至少有些用处的情况。因此,值得一试。真正的问题可能是在什么情况下我们应该尝试其他方法,简短的答案是对于非结构化数据(图像,声音等),您几乎肯定要尝试深度学习。对于协同过滤模型(杂货竞赛属于这种类型),随机森林和深度学习方法都不是您想要的,您需要做一些调整。

上周回顾[4:42]

读取 CSV 花了一两分钟,我们将其保存为羽毛格式文件。羽毛格式几乎与 RAM 中的格式相同,因此读写速度非常快。我们在第 2 课笔记本中做的第一件事是读取羽毛格式文件。

proc_df 问题[5:28]

在这一周期间提出的一个有趣的小问题是在proc_df函数中。proc_df函数执行以下操作:

  • 查找具有缺失值的数值列,并创建一个额外的布尔列,同时用中位数替换缺失值。

  • 将分类对象转换为整数代码。

问题#1:您的测试集中可能有一些列中的缺失值,这些列在训练集中不存在,反之亦然。如果发生这种情况,当您尝试进行随机森林时,您将会出现错误,因为“缺失”布尔列出现在训练集中,但不在测试集中。

问题#2:测试集中数值的中位数可能与训练集不同。因此,它可能将其处理为具有不同语义的内容。

解决方案:现在有一个额外的返回变量nasproc_df,它是一个字典,其键是具有缺失值的列的名称,字典的值是中位数。可选地,您可以将nas作为参数传递给proc_df,以确保它添加这些特定列并使用这些特定中位数:

df, y, nas = proc_df(df_raw, 'SalePrice', nas)

Corporación Favorita 杂货销售预测[9:25]

让我们走一遍当您处理一个真正大的数据集时的相同过程。几乎相同,但有一些情况下我们不能使用默认值,因为默认值运行速度有点慢。

能够解释您正在处理的问题是很重要的。在机器学习问题中理解的关键事项是:

  • 独立变量是什么?

  • 什么是因变量(您试图预测的东西)?

在这个比赛中

  • 因变量 — 在两周期间每天每个商店销售了多少种产品。

  • 自变量 — 过去几年每个产品在每个商店每天销售了多少单位。对于每个商店,它的位置在哪里以及它是什么类型的商店(元数据)。对于每种产品,它是什么类别的产品等。对于每个日期,我们有元数据,比如油价是多少。

这就是我们所说的关系数据集。关系数据集是指我们可以将许多不同信息连接在一起的数据集。具体来说,这种关系数据集是我们所说的“星型模式”,其中有一张中心交易表。在这个比赛中,中心交易表是train.csv,其中包含了按日期store_nbritem_nbr销售的数量。通过这个表,我们可以连接各种元数据(因此称为“星型”模式 — 还有一种叫做“雪花”模式)。

读取数据[15:12]

types = {
  'id': 'int64',
  'item_nbr': 'int32',
  'store_nbr': 'int8',
  'unit_sales': 'float32',
  'onpromotion': 'object'
}
%%time df_all = pd.read_csv(
    f'{PATH}train.csv', 
    parse_dates=['date'], 
    dtype=types, 
    infer_datetime_format=True
)
'''
CPU times: user 1min 41s, sys: 5.08s, total: 1min 46s
Wall time: 1min 48s
'''
  • 如果设置low_memory=False,无论您有多少内存,它都会耗尽内存。

  • 为了在读取时限制占用的空间量,我们为每个列名创建一个字典,指定该列的数据类型。您可以通过运行或在数据集上使用lesshead来找出数据类型。

  • 通过这些调整,我们可以在不到 2 分钟内读取 125,497,040 行数据。

  • Python 本身并不快,但几乎我们在 Python 中进行数据科学时想要做的一切都已经为我们用 C 或更常见的 Cython 编写好了,Cython 是一种类似 Python 的语言,可以编译成 C。在 Pandas 中,很多代码是用汇编语言编写的,这些代码经过了大量优化。在幕后,很多代码都是调用基于 Fortran 的线性代数库。

问题:指定int64int是否有任何性能考虑[18:33]?这里的关键性能是使用尽可能少的位数来完全表示列。如果我们对item_nbr使用int8,最大的item_nbr大于 255,将无法容纳。另一方面,如果我们对store_nbr使用int64,它使用的位数比必要的多。鉴于这里的整个目的是避免耗尽 RAM,我们不希望使用比必要多 8 倍的内存。当您处理大型数据集时,很多时候最慢的部分是读取和写入 RAM,而不是 CPU 操作。另外,作为一个经验法则,较小的数据类型通常会运行得更快,特别是如果您可以使用单指令多数据(SIMD)矢量化代码,它可以将更多数字打包到一个单独的矢量中一次运行。

问题:我们不再需要对数据进行洗牌了吗[20:11]?尽管在这里我已经读取了整个数据,但当我开始时,我从不会一开始就读取整个数据。通过使用 UNIX 命令shuf,您可以在命令提示符下获取数据的随机样本,然后您可以直接读取该样本。这是一个很好的方法,例如,找出要使用的数据类型 — 读取一个随机样本,让 Pandas 为您找出。通常情况下,我会尽可能多地在样本上工作,直到我确信我理解了样本后才会继续。

要使用shuf从文件中随机选择一行,请使用-n选项。这将限制输出为指定的数量。您还可以指定输出文件:

shuf -n 5 -o sample_training.csv train.csv

'onpromotion': ‘object' [21:28]— object是一个通用的 Python 数据类型,速度慢且占用内存。原因是它是一个布尔值,还有缺失值,所以我们需要在将其转换为布尔值之前处理它,如下所示:

df_all.onpromotion.fillna(False, inplace=True)
df_all.onpromotion = df_all.onpromotion.map({
    'False': False, 
    'True': True
})
df_all.onpromotion = df_all.onpromotion.astype(bool)
%time df_all.to_feather('tmp/raw_groceries')
  • fillna(False): 我们不会在没有先检查的情况下这样做,但一些探索性数据分析显示这可能是一个合适的做法(即缺失值表示 false)。

  • map({‘False’: False, ‘True’: True})object通常读取为字符串,所以用实际的布尔值替换字符串‘True’‘False’

  • astype(bool):最后将其转换为布尔类型。

  • 拥有超过 1.25 亿条记录的 feather 文件占用了不到 2.5GB 的内存。

  • 现在它以一个很好的快速格式,我们可以在不到 5 秒的时间内将它保存为 feather 格式。

Pandas 通常很快,所以你可以在 20 秒内总结所有 1.25 亿条记录的每一列:

%time df_all.describe(include='all')

  • 首先要看的是日期。日期很重要,因为你在实践中放入的任何模型,都会在比你训练的日期晚的某个日期放入。所以如果世界上的任何事情发生变化,你需要知道你的预测准确性也会如何变化。所以对于 Kaggle 或你自己的项目,你应该始终确保你的日期不重叠。

  • 在这种情况下,训练集从 2013 年到 2017 年 8 月。

df_test = pd.read_csv(
    f'{PATH}test.csv', 
    parse_dates = ['date'],
    dtype=types, 
    infer_datetime_format=True
)
df_test.onpromotion.fillna(False, inplace=True)
df_test.onpromotion = df_test.onpromotion.map({
    'False': False, 
    'True': True
})
df_test.onpromotion = df_test.onpromotion.astype(bool)
df_test.describe(include='all')

  • 在我们的测试集中,它们从第二天开始直到月底。

  • 这是一个关键的事情 —— 除非你理解这个基本的部分,否则你无法真正做出任何有用的机器学习。你有四年的数据,你正在尝试预测接下来的两周。在你能够做好这个工作之前,这是你需要理解的基本事情。

  • 如果你想使用一个较小的数据集,我们应该使用最近的 —— 而不是随机的集合。

问题:四年前大约在同一时间段重要吗(例如在圣诞节左右)?确实。并不是说四年前没有有用的信息,所以我们不想完全抛弃它。但作为第一步,如果你要提交平均值,你不会提交 2012 年销售额的平均值,而可能想要提交上个月销售额的平均值。之后,我们可能希望更高权重最近的日期,因为它们可能更相关。但我们应该进行大量的探索性数据分析来检查。

df_all.tail()

这是数据底部的样子。

df_all.unit_sales = np.log1p(np.clip(df_all.unit_sales, 0, None))
  • 我们必须对销售额取对数,因为我们正在尝试预测根据比率变化的某些东西,而他们告诉我们,在这个比赛中,均方根对数误差是他们关心的事情。

  • np.clip(df_all.unit_sales, 0, None): 有一些代表退货的负销售额,组织者告诉我们在这个比赛中将它们视为零。clip截断到指定的最小值和最大值。

  • np.log1p:值加 1 的对数。比赛细节告诉你他们将使用均方根对数加 1 误差,因为 log(0)没有意义。

%time add_datepart(df_all, 'date')
'''
CPU times: user 1min 35s, sys: 16.1 s, total: 1min 51s
Wall time: 1min 53s
'''

我们可以像往常一样添加日期部分。这需要几分钟,所以我们应该先在样本上运行所有这些,以确保它有效。一旦你知道一切都是合理的,然后回去在整个集合上运行。

n_valid = len(df_test)
n_trn = len(df_all) - n_valid
train, valid = split_vals(df_all, n_trn)
train.shape, valid.shape
'''
((122126576, 18), (3370464, 18))
'''

这些代码行与我们在推土机比赛中看到的代码行是相同的。我们不需要运行train_catsapply_cats,因为所有的数据类型已经是数字的了(记住apply_cats将相同的分类代码应用于验证集和训练集)。

%%time
trn, y, nas  = proc_df(train, 'unit_sales')
val, y_val, nas = proc_df(valid, 'unit_sales', nas)

调用proc_df来检查缺失值等。

def rmse(x,y): 
    return math.sqrt(((x-y)**2).mean())
def print_score(m):
    res = [
      rmse(m.predict(X_train), y_train),
      rmse(m.predict(X_valid), y_valid),
      m.score(X_train, y_train), 
      m.score(X_valid, y_valid)
    ]
    if hasattr(m, 'oob_score_'): 
        res.append(m.oob_score_)
    print(res)

这些代码行再次是相同的。然后有两个变化:

set_rf_samples(1_000_000)
%time x = np.array(trn, dtype=np.float32)
'''
CPU times: user 1min 17s, sys: 18.9 s, total: 1min 36s
Wall time: 1min 37s
'''
m = RandomForestRegressor(
    n_estimators=20, 
    min_samples_leaf=100, 
    n_jobs=8
)
%time m.fit(x, y)

我们上周学习了set_rf_samples。我们可能不想从 1.25 亿条记录中创建一棵树(不确定需要多长时间)。你可以从 10k 或 100k 开始,然后找出你可以运行多少。数据集的大小与构建随机森林所需时间之间没有关系,关系在于估计器数量乘以样本大小。

问题: n_job是什么?过去,它总是-1[29:42]。作业数是要使用的核心数。我在一台大约有 60 个核心的计算机上运行这个,如果你尝试使用所有核心,它会花费很多时间来启动作业,速度会变慢。如果你的计算机有很多核心,有时你想要更少(-1表示使用每个核心)。

另一个变化是x = np.array(trn, dtype=np.float32)。这将数据框转换为浮点数组,然后我们在其上进行拟合。在随机森林代码内部,他们无论如何都会这样做。鉴于我们想要运行几个不同的随机森林,使用几种不同的超参数,自己做一次可以节省 1 分 37 秒。

分析器:%prun[30:40]

如果你运行一行需要很长时间的代码,你可以在前面加上%prun

%prun m.fit(x, y)
  • 这将运行一个分析器,并告诉你哪些代码行花费了最多的时间。这里是 scikit-learn 中将数据框转换为 numpy 数组的代码行。

  • 查看哪些事情占用了时间被称为“分析”,在软件工程中是最重要的工具之一。但数据科学家往往低估了它。

  • 有趣的是,尝试不时运行%prun在需要 10-20 秒的代码上,看看你是否能学会解释和使用分析器输出。

  • Jeremy 在分析器中注意到的另一件事是,当我们使用set_rf_samples时,我们不能使用 OOB 分数,因为如果这样做,它将使用其他 124 百万行来计算 OOB 分数。此外,我们希望使用最近日期的验证集,而不是随机的。

print_score(m)
'''
[0.7726754289860, 0.7658818632043, 0.23234198105350, 0.2193243264]
'''

所以这让我们得到了 0.76 的验证均方根对数误差。

m = RandomForestRegressor(
    n_estimators=20, 
    min_samples_leaf=10, 
    n_jobs=8
)
%time m.fit(x, y)

这使我们降到了 0.71,尽管花了更长的时间。

m = RandomForestRegressor(
    n_estimators=20, 
    min_samples_leaf=3, 
    n_jobs=8
)
%time m.fit(x, y)

这将错误降低到 0.70。min_samples_leaf=1并没有真正帮助。所以我们在这里有一个“合理”的随机森林。但是这在排行榜上并没有取得好的结果[33:42]。为什么?让我们回头看看数据:

这些是我们必须用来预测的列(以及由add_datepart添加的内容)。关于明天预计销售多少的大部分见解可能都包含在关于商店位置、商店通常销售的物品种类以及给定物品的类别是什么的细节中。随机森林除了在诸如星期几、商店编号、物品编号等方面创建二元分割之外,没有其他能力。它不知道物品类型或商店位置。由于它理解正在发生的事情的能力有限,我们可能需要使用整整 4 年的数据才能得到一些有用的见解。但是一旦我们开始使用整整 4 年的数据,我们使用的数据中有很多是非常陈旧的。有一个 Kaggle 内核指出,你可以做的是[35:54]:

  1. 看看最后两周。

  2. 按商店编号、物品编号、促销情况的平均销售额,然后跨日期取平均。

  3. 只需提交,你就能排在第 30 名左右🎉

我们将在下一堂课上讨论这个问题,但如果你能找出如何从那个模型开始并使其变得更好一点,你将排在第 30 名以上。

问题:您能否尝试通过创建新列来捕捉季节性和趋势效应,比如 8 月份的平均销售额?这是一个很好的主意。要解决的问题是如何做到这一点,因为有一些细节需要正确,这些细节很困难-不是在智力上困难,而是以一种让你在凌晨 2 点撞头的方式困难。

为机器学习编码是非常令人沮丧和非常困难的。如果你弄错了一个细节,很多时候它不会给你一个异常,它只会默默地比原来稍微差一点。如果你在 Kaggle 上,你会知道你的表现不如其他人。但否则,你没有什么可以比较的。你不会知道你的公司模型是否只有它可能的一半好,因为你犯了一个小错误。这就是为什么现在在 Kaggle 上练习是很好的。

你将练习找到所有可能令人恼火地搞砸事情的方法,你会感到惊讶。

即使对于 Jeremy 来说,这些验证集也是非常丰富的。当你开始了解它们是什么时,你将开始知道如何在进行时检查它们。你应该假设你按下的每个按钮都会按错按钮。只要你有一种找出来的方法就可以。

不幸的是,没有一套你应该总是做的具体事情,你只需要考虑一下我即将做的事情的结果。这里有一个非常简单的例子。如果你创建了一个基本的条目,其中你按日期、店铺编号、促销状态取平均值,然后提交了它,并得到了一个合理的分数。然后你认为你有一些稍微好一点的东西,你为此做了预测。你可以创建一个散点图,显示你的平均模型预测在一个轴上,与你的新模型预测在另一个轴上。你应该看到它们几乎形成一条直线。如果不是,那么这非常明显地表明你搞砸了什么。

问题:您多久从其他来源获取数据来补充您已有的数据集?非常频繁。星型模式的整个重点是你有一个中心表,你有其他表与之相连,提供关于它的元数据。在 Kaggle 上,大多数比赛的规则是你可以使用外部数据,只要在论坛上发布并且是公开可用的(双重检查规则!)。在 Kaggle 之外,你应该始终寻找可能利用的外部数据。

问题:如何添加厄瓜多尔的假期来补充数据?这个信息实际上是提供的。一种解决这种问题的一般方法是创建许多新列,其中包含假期销售平均数量,一月和二月之间销售平均百分比变化等。在德国的一个杂货连锁店曾经有一场先前的比赛,几乎是一样的。获胜者是一个领域专家,擅长做物流预测。他根据自己的经验创建了许多列,这些列通常对于做预测是有用的。所以这是一个可以奏效的方法。然而,第三名获奖者几乎没有进行特征工程,而且他们也有一个大的疏忽,这可能导致他们失去第一名。随着比赛的进行,我们将学到更多关于如何赢得这场比赛以及类似比赛的知识。

好的验证集的重要性

如果你没有一个好的验证集,要创建一个好的模型是困难的,甚至是不可能的。如果你试图预测下个月的销售额,并建立模型。如果你无法知道你建立的模型是否擅长提前一个月预测销售额,那么当你将模型投入生产或在测试集上使用时,你就无法知道它是否真的会很好。你需要一个可靠的验证集,告诉你你的模型是否有可能在投入生产或在测试集上使用时表现良好。

通常情况下,你不应该对测试集做任何其他操作,除非在比赛结束时或项目结束时使用它来查看你的表现。但是有一件事你可以在测试集中使用 —— 那就是校准你的验证集[46:02]。

Terrance 在这里做的是建立了四种不同的模型,并将这四种模型分别提交到 Kaggle 上,以找出它们的得分。X 轴是 Kaggle 在排行榜上告诉我们的得分,y 轴是他在一个特定的验证集上绘制的得分,他试图看看这个验证集是否会很好。如果你的验证集很好,那么排行榜得分(即测试集得分)之间的关系应该是一条直线。理想情况下,它将位于y = x线上,但老实说,这并不太重要,只要相对来说告诉你哪些模型比哪些模型更好,那么你就知道哪个模型是最好的。在这种情况下,Terrance 设法找到了一个看起来能够很好地预测 Kaggle 排行榜得分的验证集。这真的很酷,因为他可以尝试一百种不同类型的模型、特征工程、加权、调整、超参数等等,看看它们在验证集上的表现,而不必提交到 Kaggle。因此,你将得到更多的迭代,更多的反馈。这不仅适用于 Kaggle,而且适用于你做的每一个机器学习项目。一般来说,如果你的验证集没有显示出良好的拟合线,你需要仔细思考[48:02]。测试集是如何构建的?我的验证集有什么不同?你将不得不绘制很多图表等等来找出。

问题:如何构建一个与测试集尽可能接近的验证集[48:23]?以下是 Terrance 的一些建议:

  • 日期接近(即最近的)

  • 首先看一下测试集的日期范围(16 天),然后看一下描述如何在排行榜上获得 0.58 分的内核的日期范围(14 天)。

  • 测试集从发薪日的第二天开始,到下一个发薪日结束。

  • 绘制很多图片。即使你不知道今天是发薪日,你也想绘制时间序列图,希望看到每两周有一个高峰,并确保验证集中有与测试集相同数量的高峰。

解释机器学习模型[50:38 / 笔记本]

PATH = "data/bulldozers/"

df_raw = pd.read_feather('tmp/raw')
df_trn, y_trn, nas = proc_df(df_raw, 'SalePrice')

我们首先读取蓝色书籍对推土机比赛的 feather 文件。提醒:我们已经读取了 CSV 文件,将其处理为类别,并保存为 feather 格式。接下来我们调用proc_df将类别转换为整数,处理缺失值,并提取出因变量。然后创建一个像上周一样的验证集:

def split_vals(a,n): 
    return a[:n], a[n:]n_valid = 12000
n_trn = len(df_trn)-n_valid
X_train, X_valid = split_vals(df_trn, n_trn)
y_train, y_valid = split_vals(y_trn, n_trn)
raw_train, raw_valid = split_vals(df_raw, n_trn)

绕道到第 1 课笔记本[51:59]

上周,在proc_df中有一个 bug,当传入subset时会打乱数据框,导致验证集不是最新的 12000 条记录。这个问题已经修复。

## From lesson1-rf.ipynb
df_trn, y_trn, nas = proc_df(
    df_raw, 'SalePrice', 
    subset=30000, 
    na_dict=nas
)
X_train, _ = split_vals(df_trn, 20000)
y_train, _ = split_vals(y_trn, 20000)

问题:为什么nas既是该函数的输入又是输出[53:03]?proc_df返回一个告诉您哪些列丢失以及每个丢失列的中位数的字典。

  1. 当您在较大的数据集上调用proc_df时,不需要传入nas,但您希望保留该返回值。

  2. 稍后,当您想要创建一个子集(通过传入subset)时,您希望使用相同的丢失列和中位数,因此您传入nas

  3. 如果发现子集来自完全不同的数据集并且具有不同的丢失列,它将使用附加键值更新字典。

  4. 它会跟踪您在传递给proc_df的任何内容中遇到的任何丢失列。

回到第 2 课笔记本[54:40]

一旦我们完成了proc_df,它看起来是这样的。SalePrice是销售价格的对数。

我们已经知道如何进行预测。我们在通过每棵树运行特定行后,在每棵树的每个叶节点中取平均值。通常,我们不仅想要一个预测 - 我们还想知道我们对该预测的信心有多大。

如果我们没有看到许多类似这一行的示例,我们对预测会更不自信。在这种情况下,我们不希望任何树都通过 - 这有助于我们预测该行。因此,在概念上,您会期望当您通过不同树传递此不寻常的行时,它会最终出现在非常不同的位置。换句话说,与其只取树的预测平均值并说这是我们的预测,不如我们取树的预测标准差呢?如果标准差很高,这意味着每棵树都为我们提供了对该行预测的非常不同的估计。如果这是一种非常常见的行,树将已经学会为其做出良好的预测,因为它已经看到了许多基于这些行的分割机会。因此,跨树的预测标准差至少让我们相对了解我们对该预测的信心有多大[56:39]。这在 scikit-learn 中不存在,因此我们必须创建它。但我们已经有几乎需要的确切代码。

对于模型解释,没有必要使用完整的数据集,因为我们不需要一个非常准确的随机森林 - 我们只需要一个指示所涉及关系性质的随机森林。

只需确保样本大小足够大,以便如果多次调用相同的解释命令,每次都不会得到不同的结果。在实践中,50,000 是一个很高的数字,如果这还不够的话会令人惊讶(而且运行时间只需几秒)。

set_rf_samples(50000)m = RandomForestRegressor(
    n_estimators=40, 
    min_samples_leaf=3, 
    max_features=0.5, 
    n_jobs=-1, 
    oob_score=True
)
m.fit(X_train, y_train)
print_score(m)

这里我们可以做与上次完全相同的列表推导[58:35]:

%time preds = np.stack([t.predict(X_valid) for t in m.estimators_])
np.mean(preds[:,0]), np.std(preds[:,0])
'''
CPU times: user 1.38 s, sys: 20 ms, total: 1.4 s
Wall time: 1.4 s**(9.1960278072006023, 0.21225113407342761)
'''

这是针对一个观察结果的方法。这需要相当长的时间,特别是它没有充分利用我的计算机有很多核心这一事实。列表推导本身是 Python 代码,Python 代码(除非您在做一些特殊的事情)运行在串行模式下,这意味着它在单个 CPU 上运行,不利用您的多 CPU 硬件。如果我们想在更多树和更多数据上运行此代码,执行时间会增加。墙上时间(实际花费的时间)大致等于 CPU 时间,否则如果它在许多核心上运行,CPU 时间将高于墙上时间[1:00:05]。

原来 Fast.ai 库提供了一个方便的函数称为parallel_trees

def get_preds(t): 
    return t.predict(X_valid)
%time preds = np.stack(parallel_trees(m, get_preds))
np.mean(preds[:,0]), np.std(preds[:,0])
'''
CPU times: user 100 ms, sys: 180 ms, total: 280 ms
Wall time: 505 ms**(9.1960278072006023, 0.21225113407342761)
'''
  • parallel_trees接受一个随机森林模型m和要调用的某个函数(这里是get_preds)。这会并行在每棵树上调用此函数。

  • 调用该函数对每棵树应用后返回一个结果列表。

  • 这将把墙上的时间缩短到 500 毫秒,并给出完全相同的答案。如果时间允许,我们将讨论更一般的编写并行代码的方法,这对数据科学非常有用,但这里有一种我们可以用于随机森林的方法。

绘制[1:02:02]

我们首先将数据复制一份,并将预测的标准差和预测本身(均值)作为新列添加进去:

x = raw_valid.copy()
x['pred_std'] = np.std(preds, axis=0)
x['pred'] = np.mean(preds, axis=0)
x.Enclosure.value_counts().plot.barh();

你可能还记得上一课中我们有一个叫做Enclosure的预测变量,这是一个重要的变量,我们稍后会看到。让我们从做一个直方图开始。Pandas 的一个好处是它具有内置的绘图功能

问题:你能提醒我围栏是什么吗[01:02:50]?我们不知道它的意思,也不重要。这个过程的整个目的是我们将学习关于事物是什么(或者至少重要的事物,然后弄清楚它们是什么以及它们的重要性)。所以我们一开始对这个数据集一无所知。我们只是要看一下一个叫做Enclosure的东西,里面有一个叫做EROPSROPS的东西,我们甚至还不知道这是什么。我们只知道在任何大量出现的情况下,只有OROPSEROPS w ACEROPS。这在数据科学家中非常常见。你经常发现自己在看一些你不太熟悉的数据,并且你必须弄清楚哪些部分需要更仔细地研究,哪些部分似乎很重要,等等。在这种情况下,至少知道EROPS ACNO ROPSNone or Unspecified我们真的不关心,因为它们基本上不存在。所以我们将关注OROPSEROPS w ACEROPS

在这里,我们取了我们的数据框,按Enclosure分组,然后取了 3 个字段的平均值[1:04:00]:

flds = ['Enclosure', 'SalePrice', 'pred', 'pred_std']
enc_summ = x[flds].groupby('Enclosure', as_index=False).mean()
enc_summ

我们已经开始在这里学习一点:

  • 预测和销售价格平均接近(好迹象)

  • 标准差有些变化

enc_summ = enc_summ[~pd.isnull(enc_summ.SalePrice)]
enc_summ.plot('Enclosure', 'SalePrice', 'barh', xlim=(0,11));

enc_summ.plot(
    'Enclosure', 'pred', 'barh', 
    xerr='pred_std', 
    alpha=0.6, 
    xlim=(0,11)
);

我们使用了上面预测的标准差来绘制误差线。这将告诉我们是否有一些组或一些行我们并不是很有信心。我们可以对产品尺寸做类似的事情:

raw_valid.ProductSize.value_counts().plot.barh();

flds = ['ProductSize', 'SalePrice', 'pred', 'pred_std']
summ = x[flds].groupby(flds[0]).mean()
summ

你期望,平均而言,当你预测一个更大的数字时,你的标准差会更高。所以你可以按照预测的标准差与预测本身的比率排序[1:05:51]。

(summ.pred_std/summ.pred).sort_values(ascending=False)

这告诉我们的是产品尺寸LargeCompact,我们的预测不太准确(相对于总价格的比率)。所以如果我们回头看一下,你会明白为什么。这些是直方图中最小的组。正如你所期望的,在小组中,我们的工作效果不太好。

你可以用这个置信区间做两个主要目的:

  1. 你可以按组查看平均置信区间,以找出你似乎不太有信心的组。

  2. 也许更重要的是,你可以查看特定行的重要性。当你投入生产时,你可能总是想看到置信区间。例如,如果你正在进行信用评分来决定是否给某人贷款,你可能不仅想知道他们的风险水平,还想知道我们有多大的信心。如果他们想借很多钱,而我们对我们的预测能力毫无信心,我们可能会给他们较小的贷款。

特征重要性 [1:07:20]

在实践中,我总是首先查看特征重要性。无论我是在参加 Kaggle 竞赛还是在进行真实世界项目,我都会尽快构建一个随机森林,试图让它达到明显优于随机的水平,但不必比那更好太多。接下来我要做的事情是绘制特征重要性。

特征重要性告诉我们在这个随机森林中,哪些列很重要。在这个数据集中有几十列,而在这里,我们挑选出前十个。rf_feat_importance 是 Fast.ai 库的一部分,它接受一个模型 m 和一个数据框 df_trn(因为我们需要知道列的名称),然后会返回一个 Pandas 数据框,按重要性顺序显示每列的重要性。

fi = rf_feat_importance(m, df_trn); fi[:10]

fi.plot('cols', 'imp', figsize=(10,6), legend=False);

由于 fi 是一个 DataFrame,我们可以使用 DataFrame 绘图命令 [1:09:00]。重要的是要看到一些列真的很重要,而大多数列实际上并不重要。在你在现实生活中使用的几乎每个数据集中,你的特征重要性都会是这个样子。只有少数几列是你关心的,这就是为什么 Jeremy 总是从这里开始的原因。在这一点上,就学习这个重型工业设备拍卖领域,我们只需要关心那些重要的列。我们是否要去了解 Enclosure?取决于 Enclosure 是否重要。结果表明它出现在前十名,所以我们需要了解 Enclosure

我们也可以将其绘制为条形图:

def plot_fi(fi): 
    return fi.plot('cols','imp','barh', figsize=(12,7), legend=False)
plot_fi(fi[:30]);

现在最重要的事情是和你的客户、数据字典,或者任何你的信息来源坐下来,然后说“好的,告诉我关于 YearMade。那是什么意思?它来自哪里?”[1:10:31] 绘制很多东西,比如 YearMade 的直方图和 YearMade 与价格的散点图,尽可能学到更多,因为 YearMadeCoupler_System —— 这些才是重要的事情。

在现实项目中经常发生的情况是,你和客户坐在一起,你会说“结果 Coupler_System 是第二重要的事情”,而他们可能会说“这毫无意义”。这并不意味着你的模型有问题,而是意味着他们对他们给你的数据的理解有问题。

让我举个例子。我参加了一个 Kaggle 竞赛,目标是预测大学的资助申请哪些会成功。我使用了这种确切的方法,发现了一些几乎完全预测因变量的列。具体来说,当我查看它们是如何预测的时候,结果是它们是否缺失是数据集中唯一重要的事情。由于这一发现,我最终赢得了那场比赛。后来,我听说了发生了什么。原来在那所大学,填写其他数据库是一项行政负担,因此对于很多资助申请,他们没有为那些未被接受的申请者填写数据库。换句话说,数据集中的这些缺失值表明这笔资助没有被接受,因为如果被接受,行政人员会输入那些信息。这就是我们所说的数据泄漏。数据泄漏意味着在我建模时数据集中有信息,而在大学在做决定时实际上并没有这些信息。当他们实际决定哪些资助申请要优先考虑时,他们不知道行政人员将来会添加信息的哪些申请,因为事实证明它们被接受了。

这里你会发现的一个关键问题是数据泄漏问题,这是一个你需要处理的严重问题。另一个问题是你经常会发现共线性的迹象。就像Coupler_System发生的情况一样。Coupler_System告诉你特定类型的重型工业设备是否具有特定功能。但如果根本不是那种工业设备,它就会缺失。因此,它指示了是否属于某一类重型工业设备。这不是数据泄漏。这是你在正确时间实际拥有的信息。你只需要谨慎解释它。因此,你应该至少查看前 10 个或寻找自然的分界点,并仔细研究这些事情。

为了让生活更轻松,有时候最好抛弃一些数据,看看是否会有任何不同。在这种情况下,我们有一个随机森林,r²为 0.889。在这里,我们筛选出那些重要性等于或小于 0.005 的数据(即只保留重要性大于 0.005 的数据)。

to_keep = fi[fi.imp>0.005].cols; len(to_keep)
df_keep = df_trn[to_keep].copy()
X_train, X_valid = split_vals(df_keep, n_trn)
m = RandomForestRegressor(
    n_estimators=40, 
    min_samples_leaf=3, 
    max_features=0.5, 
    n_jobs=-1, 
    oob_score=True
)
m.fit(X_train, y_train)
print_score(m)
'''
[0.20685390156773095, 0.24454842802383558, 0.91015213846294174, 0.89319840835270514, 0.8942078920004991]
'''

r²并没有太大变化 - 实际上略微增加了一点。一般来说,删除多余的列不应该使情况变得更糟。如果情况变得更糟,那么这些列实际上并不多余。这可能会使结果略微好一点,因为当决定要分裂时,它需要考虑的事情更少,不太可能偶然发现一个糟糕的列。因此,有稍微更好的机会创建一个稍微更好的树,使用稍微更少的数据,但不会有太大变化。但这会使速度更快,让我们专注于重要的事情。让我们在这个新结果上重新运行特征重要性。

fi = rf_feat_importance(m, df_keep)
plot_fi(fi);

把这个文本翻译成中文。

发生的关键事情是,当你移除冗余列时,你也在移除共线性的来源。换句话说,可能彼此相关的两列。共线性不会使你的随机森林更少预测,但如果 A 列与 B 列稍微相关,而 B 是独立变量的一个强驱动因素,那么重要性将在 A 和 B 之间分配。通过移除一些对结果影响很小的列,使得你的特征重要性图更清晰。之前YearMadeCoupler_System相当接近。但肯定有一堆与YearMade共线的东西,现在你可以看到YearMade真的很重要。这个特征重要性图比之前更可靠,因为它减少了很多共线性,不会让我们感到困惑。

让我们谈谈这是如何运作的[1:17:21]

这不仅非常简单,而且是一种你可以用于任何类型的机器学习模型的技术。有趣的是,几乎没有人知道这一点。许多人会告诉你,没有办法解释这种特定类型的模型(模型的最重要解释是知道哪些因素是重要的),这几乎肯定不会是真的,因为我要教给你的技术实际上适用于任何类型的模型。

  • 我们拿我们的推土机数据集,我们有一个列Price我们正在尝试预测(因变量)。

  • 我们有 25 个自变量,其中之一是YearMade

  • 我们如何确定YearMade有多重要?我们有一个完整的随机森林,我们可以找出我们的预测准确性。因此,我们将把所有这些行通过我们的随机森林,它将输出一些预测。然后我们将它们与实际价格进行比较(在这种情况下,我们得到我们的均方根误差和 r²)。这是我们的起点。

  • 让我们做完全相同的事情,但这次,拿YearMade列并随机洗牌它(即随机排列只是那一列)。现在YearMade与之前完全相同的分布(相同的均值,相同的标准差)。但它与我们的因变量没有任何关系,因为我们完全随机重新排序了它。

  • 之前,我们可能发现我们的 r²是 0.89。在我们洗牌YearMade之后,我们再次检查,现在 r²是 0.80。当我们破坏那个变量时,得分变得更糟了。

  • 好的,让我们再试一次。我们把YearMade恢复到原来的状态,这次让我们拿Enclosure来洗牌。这次,r²是 0.84,我们可以说YearMade得分减少了 0.09,而Enclosure的得分减少了 0.05。这将为我们提供每一列的特征重要性。

问题:我们不能只排除这一列然后检查性能的下降吗[1:20:31]?你可以删除该列并训练一个全新的随机森林,但那将会非常慢。而通过这种方式,我们可以保留我们的随机森林,再次测试其预测准确性。因此,相比之下,这种方式更快速。在这种情况下,我们只需将每个洗牌列的每一行再次通过森林运行一遍。

问题:如果你想做多重共线性,你会做两个然后随机洗牌,然后三个[1:21:12]?我认为你不是指多重共线性,我认为你是指寻找交互效应。因此,如果你想知道哪些变量对是最重要的,你可以依次对每对变量做完全相同的事情。实际上,有更好的方法来做到这一点,因为显然这在计算上是非常昂贵的,所以如果可能的话,我们将尝试找时间来做到这一点。

我们现在有一个稍微更准确的模型,我们对它了解更多。所以我们时间不够了,我建议你在下一堂课之前尝试做的是,查看前 5 或 10 个预测变量,尝试学习如何在 Pandas 中绘制图表,并尝试回来带一些关于以下事项的见解:

  • YearMade和因变量之间的关系是什么

  • YearMade的直方图是什么样的

  • 现在你知道YearMade非常重要,检查一下这一列中是否有一些噪音,我们可以修复。

  • 检查一下这一列中是否有一些奇怪的编码问题,我们可以修复。

  • Jeremy 提出的这个想法,也许Coupler_System完全存在是因为它与其他某些东西共线,你可能想要尝试弄清楚这是否属实。如果是这样,你会怎么做呢?

  • fiProductClassDesc这个让人警惕的名字——听起来可能是一个高基数分类变量。它可能是一个有很多级别的东西,因为它听起来像是一个型号名称。所以去看看那个型号名称——它有一定的顺序吗?你能把它变成一个有序变量吗?它在字符串中有一些层次结构,我们可以通过连字符拆分它来创建更多的子列。

想一想这个问题。试着让你回来时,你有一些新的,最好是比我刚刚展示的更准确的东西,因为你找到了一些新的见解,或者至少你可以告诉班上一些你学到的有关重型工业设备拍卖的实际工作方式的事情。

机器学习 1:第 4 课

原文:medium.com/@hiromi_suenaga/machine-learning-1-lesson-4-a536f333b20d

译者:飞龙

协议:CC BY-NC-SA 4.0

来自机器学习课程的个人笔记。随着我继续复习课程以“真正”理解它,这些笔记将继续更新和改进。非常感谢 JeremyRachel 给了我这个学习的机会。

在开始之前有一个问题:我们能否总结随机森林的超参数与过拟合、处理共线性等之间的关系?绝对可以。回到第 1 课笔记本

感兴趣的超参数:

  1. 设置 _rf_samples
  • 确定每棵树中有多少行。因此,在我们开始新树之前,我们要么对整个数据进行自助抽样(即有放回地抽样),要么从中抽取较少行数的子样本,然后从中构建一棵树。

  • 第一步是我们有一个完整的大数据集,我们随机抽取几行数据,并将它们转换成一个较小的数据集。然后,我们构建一棵树。

  • 假设树在我们生长过程中保持平衡,这棵树将有多少层深(假设我们生长到每个叶子的大小为一)?log2(20000)。树的深度实际上并不会因为样本数量的不同而变化太大,因为它与大小的对数相关。

  • 当我们一直走到底部时,会有多少叶节点?20K。叶节点的数量与样本大小之间存在线性关系。因此,当你减少样本大小时,可以做出的最终决策就会减少。因此,树在预测方面会变得不那么丰富,因为它做出的个别决策更少,也做出更少的二元选择来达到这些决策。

  • 将 RF 样本设置较低意味着过拟合的可能性较小,但也意味着每个单独的树模型的准确性会降低。随机森林的发明者 Breiman 描述了这一点,即在使用装袋法构建模型时,你要做两件事情。一是确保每个单独的树/估计器尽可能准确(因此每个模型都是一个强预测模型)。但是在估计器之间,相关性要尽可能低,这样当将它们平均在一起时,你会得到一个泛化的模型。通过降低set_rf_samples的数量,实际上是降低了估计器的能力并增加了相关性,那么这会对你的验证集结果产生更好还是更差的影响呢?这取决于情况。这就是在进行机器学习模型时必须要考虑的妥协。

关于oob=True的问题[6:46]。oob=True的作用就是说,无论你的子样本是什么(可能是一个自助采样或一个子样本),将所有其他行(对于每棵树)放入一个不同的数据集中,并计算这些行的错误。因此,它实际上并不影响训练。它只是给你一个额外的度量,即 OOB 错误。因此,如果你没有验证集,那么这允许你免费获得一种准验证集。

问题:如果我不执行set_rf_samples,那会被称为什么?默认情况是,如果你说reset_rf_samples,那会导致引导,因此它将对原始数据集进行重新采样,但是会有替换。

set_rf_samples的第二个好处是你可以更快地运行。特别是当你在一个非常庞大的数据集上运行,比如一亿行,就不可能在完整的数据集上运行。所以你要么在开始之前自己选择一个子样本,要么使用set_rf_samples

  1. min_samples_leaf [8:48]

之前,我们假设min_samples_leaf=1,如果设置为 2,树的新深度为log2(20000)-1。每次将min_samples_leaf加倍,我们都会从树中移除一层,并将叶节点数量减半(即 10k)。增加min_samples_leaf的结果是现在每个叶节点中都有多于一个元素,因此我们在每棵树中计算的平均值会更加稳定。我们的深度稍微减少(即我们需要做出的决策更少),叶节点数量也减少。因此,我们预期每个估算器的结果会更少预测性,但估算器之间的相关性也会减少。这可能有助于我们避免过拟合。

问题:我不确定每个叶节点是否一定会有两个节点。不,不一定会有两个。不均匀分裂的例子,比如一个叶节点包含 100 个项目,当它们在因变量方面都相同时(假设是这样,但更有可能是因变量)。所以,如果你到达一个叶节点,每一个都有相同的拍卖价格,或者在分类中每一个都是一只狗,那么你无法进行任何可以改善你的信息的分裂。记住,“信息”是我们在随机森林中使用的一个术语,用来描述我们从分裂中创造的额外信息的差异量,我们通过分裂改善模型的程度。所以你经常会看到这个词“信息增益”,意思是通过添加额外的分裂点,模型变得更好了多少,这可能基于 RMSE 或交叉熵或与标准差的差异等。

这就是我们可以做的第二件事情。这将加快我们的训练速度,因为它少了一组决策要做。尽管少了一组决策,但这些决策的数据量与之前的一样多。因此,树的每一层可能比前一层花费的时间多一倍。因此,它肯定可以加快训练速度并且泛化得更好。

3. max_features [12:22]

在每次分裂时,它会随机抽样列(与set_rf_samples选择每棵树的行子集相对)。听起来是一个小的区别,但实际上这是一种完全不同的思考方式。我们使用set_rf_samples来提取我们的子样本或自举样本,并将其保留整个树中,其中包含所有列。使用max_features=0.5,在每次分裂时,我们会选择不同的一半特征。我们这样做的原因是因为我们希望树尽可能丰富。特别是,如果您只做了少量的树(例如 10 棵树),并且在整个树中选择了相同的列集,那么您实际上并没有获得太多不同种类的发现。因此,这种方式,至少在理论上,似乎会通过在每个决策点处选择不同的随机特征子集来给我们提供更好的树集。

max_features 的整体效果是相同的 - 这意味着每棵单独的树可能会更不准确,但树的变化会更多。特别是在这里,这可能是至关重要的,因为想象一下,你有一个特征是非常具有预测性的。它是如此具有预测性,以至于你查看的每个随机子样本总是从相同的特征开始分裂,那么这些树在某种意义上将非常相似,因为它们都具有相同的初始分裂。但可能会有一些其他有趣的初始分裂,因为它们会创建不同的变量交互。因此,有一半的时间该特征甚至不会出现在树的顶部,至少有一半的树会有不同的初始分裂。这绝对可以给我们更多的变化,因此可以帮助我们创建更具一般性的树,这些树之间的相关性更小,即使单独的树可能不会那么具有预测性。

在实践中,当你添加更多的树时,如果你设置max_features=None,那么每次都会使用所有的特征。然后在很少的树的情况下,这仍然可以给你一个相当不错的误差。但是随着你创建更多的树,它不会帮助太多,因为它们都很相似,它们都在尝试每一个变量。另外,如果你设置max_features=sqrtlog2,那么随着我们添加更多的估计器,我们会看到改进,所以这两者之间存在有趣的互动。上面的图表来自 scikit-learn 文档。

4. 完全不影响我们训练的事情

n_jobs:简单地指定我们运行在多少个 CPU 或核心上,因此在某种程度上会使其更快。一般来说,将其设置为超过 8 个左右,可能会有递减的回报。-1 表示使用所有核心。默认使用一个核心似乎有点奇怪。通过使用更多核心,您肯定会获得更好的性能,因为现在大多数计算机都有多个核心。

oob_score=True: 这只是让我们看到 OOB 得分。如果你将 set_rf_samples 设置得相对较小,而数据集很大,OOB 将需要很长时间来计算。希望在某个时候,我们能够修复库,使其不再发生这种情况。没有理由需要那样,但目前,库就是这样工作的。

所以它们是我们可以更改的关键基本参数。您可以在文档中查看更多内容,或者按shift+tab查看它们,但您已经看到的是我发现有用的,可以随意尝试其他参数。一般来说,这些值效果很好。

max_features: None, 0.5, sqrt, log2

min_samples_leaf : 1, 3, 5, 10, 25, 100… 随着增加,如果你注意到当你达到 10 时,情况已经变得更糟,那么继续下去就没有意义了。如果你达到 100 时情况仍在好转,那么你可以继续尝试。

随机森林解释 [18:50]

随机森林解释是你可以用来创建一些非常酷的 Kaggle 内核的东西。基于树方差的置信度是其他地方不存在的。特征重要性肯定存在,并且已经在许多 Kaggle 内核中。如果你正在看一个竞赛或一个数据集,没有人做过特征重要性,成为第一个这样做的人总是会赢得很多票,因为最重要的是哪些特征是重要的。

基于树方差的置信度

正如我所提到的,当我们进行模型解释时,我倾向于将set_rf_samples设置为某个子集——足够小,可以在不到 10 秒内运行一个模型,因为运行一个超级准确的模型没有意义。五万个样本已经足够了,每次运行解释时,你会得到相同的结果,只要这是真的,那么你已经在使用足够的数据了。

set_rf_samples(50000)
m = RandomForestRegressor(
    n_estimators=40, 
    min_samples_leaf=3, 
    max_features=0.5, 
    n_jobs=-1, 
    oob_score=True
)
m.fit(X_train, y_train)
print_score(m)

特征重要性

我们学到它是通过随机洗牌一列,每次一列,然后看看在将所有数据传递给预训练模型时,当其中一列被洗牌时,模型的准确性如何。

课后我收到的一些问题让我想起,很容易低估这种方法有多么强大和神奇。为了解释,我会提到我听到的一些问题。

一个问题是“如果我们一次只取一列,然后在那一列上创建一棵树会怎样”。然后我们会看到哪一列的树是最具预测性的。为什么这可能会导致关于特征重要性的误导性结果?我们将失去特征之间的相互作用。如果我们只是随机打乱它们,那么会增加随机性,我们就能捕捉到特征之间的相互作用和重要性。这种相互作用的问题并不是一个细枝末节。它非常重要。想想这个推土机数据集,例如,有一个字段叫做“制造年份”,另一个字段叫做“销售日期”。如果我们想一想,很明显重要的是这两者的组合。换句话说,两者之间的区别是设备在售出时的年龄。因此,如果我们只包含其中一个,我们将严重低估该特征的重要性。现在,这里有一个非常重要的观点。如果你事先知道你需要哪些变量,它们如何相互作用,以及它们需要如何转换,那么几乎总是可以创建一个简单的逻辑回归,它和几乎任何随机森林一样好。在这种情况下,例如,我们可以创建一个新字段,它等于销售年份减去制造年份,然后将其输入模型并为我们获取该相互作用。但关键是,我们永远不知道这一点。你可能会猜测 — 我认为其中一些事物是以这种方式相互作用的,我认为这个东西我们需要取对数,等等。但事实是,世界运作的方式,因果结构,有许多许多事物以许多微妙的方式相互作用。这就是为什么使用树,无论是梯度提升机还是随机森林,都能够如此出色地工作。

Terrance 的评论: 多年前咬我一口的一件事也是我尝试一次只处理一个变量,认为“哦,好吧,我会弄清楚哪个与因变量最相关”[24:45]。但它没有分开的是,如果所有变量基本上都是复制的同一个变量,那么它们看起来都同样重要,但实际上只是一个因素。

这在这里也是正确的。如果我们有一列出现两次,那么对该列进行洗牌不会使模型变得更糟。如果你考虑它是如何构建的,特别是如果我们设置了max_features=0.5,有时我们会得到列的版本 A,有时我们会得到列的版本 B。因此,一半的时间,对列的版本 A 进行洗牌会使树变得稍微糟糕,一半的时间对列的版本 B 进行洗牌会使其稍微糟糕,因此它将显示这两个特征都有一定重要性。它将在这两个特征之间共享重要性。这就是为什么“共线性”(我写的是共线性,但它意味着它们是线性相关的,所以这不太对)——但这就是为什么拥有两个彼此密切相关的变量或更多彼此密切相关的变量意味着您经常会低估它们在使用这种随机森林技术时的重要性。

问题:一旦我们洗牌并获得一个新模型,这些重要性的单位究竟是什么?这是否是 R²的变化?这取决于我们使用的库。所以这些单位有点像……我从来没有考虑过它们。我只知道在这个特定的库中,0.005 经常是我倾向于使用的一个截止值。但我真正关心的是这张图片(每个变量的特征重要性排序):

然后放大,将其转换为条形图,然后找到其变平的地方(约 0.005)。

所以我在那时将它们移除,并检查验证分数没有变差。

to_keep = fi[fi.imp>0.005].cols; len(to_keep)

如果情况变得更糟,我只需稍微降低截止值,直到情况不再恶化。因此,这个度量单位并不太重要。顺便说一下,我们以后会学习另一种计算变量重要性的方法。

移除它们的目的是什么?在查看我们的特征重要性图后,我们发现小于 0.005 的那些是无聊的长尾。所以我说让我们尝试只选择大于 0.005 的列,创建一个名为df_keep的新数据框,其中只包含那些保留的列,创建一个只包含这些列的新训练和验证集,创建一个新的随机森林,并查看验证集得分。验证集的 RMSE 发生了变化,变得更好了一点。所以如果它们大致相同或稍微好一点,那么我的想法是这是一个同样好的模型,但现在更简单。

因此,当我重新进行特征重要性分析时,相关性较小。在这种情况下,我发现制造年份从略优于下一个最好的特征(连接器系统)变得更好了,但现在它更好了。因此,它似乎确实改变了这些特征的重要性,并希望能给我一些更多的见解。

问题:那么这如何帮助我们的模型呢?我们现在要深入研究这个问题。基本上,它告诉我们,例如,如果我们正在寻找如何处理缺失值,数据中是否有噪音,如果是高基数分类变量——这些都是我们会采取的不同步骤。例如,如果原来是一个字符串的高基数分类变量,也许在上面的情况下是 fiProductClassDesc,我记得我们前几天看的一个,首先是车辆类型,然后是一个连字符,然后是车辆的大小。我们可能会看到这个并说“好的,这是一个重要的列。让我们尝试在连字符上分割它成两部分,然后取那部分,即它的大小,并解析它并转换为整数。”我们可以尝试进行一些特征工程。基本上,直到你知道哪些是重要的,你就不知道在哪里集中特征工程的时间。你可以与负责创建这些数据的客户或相关人员交谈。如果你实际上在一个推土机拍卖公司工作,你现在可能会去找实际的拍卖人,说“我真的很惊讶,连接器系统似乎对人们的定价决策产生了如此大的影响。你认为这可能是为什么?”他们可能会告诉你“哦,实际上是因为只有这些类别的车辆有连接器系统,或者只有这个制造商有连接器系统。所以实际上这并不是告诉你关于连接器系统的,而是关于其他事情。哦,嘿,这让我想起来,我们实际上还测量了其他东西。它在另一个不同的 CSV 文件中。我去拿给你。”所以它帮助你集中注意力。

问题:所以你知道,这个周末我遇到了一个有趣的小问题。我在我的随机森林中引入了一些疯狂的计算,突然间它们就像是哦,这些是最重要的变量,压制了其他所有变量。但是我得到了一个糟糕的分数,那是因为我现在认为我的分数计算正确了吗,我注意到重要性飙升了,但验证集仍然很糟糕,甚至更糟。这是因为某种计算方式让训练几乎像一个标识符映射到了训练答案,但当然这并不能推广到验证集。这就是我观察到的吗?你的验证分数可能不太好的两个原因。

所以我们得到了这五个数字:训练的 RMSE,验证的 RMSE,训练的 R²,验证的 R²和 OOB 的 R²。最终我们关心的是这个 Kaggle 竞赛的验证集的 RMSE,假设我们已经创建了一个好的验证集。Terrance 的情况,他说当我进行一些特征工程时,验证的 RMSE 变糟了。为什么呢?有两个可能的原因。

  • 原因一是你过拟合了。如果你过拟合了,那么你的 OOB 也会变得更糟。如果你在一个大数据集上使用了一个小的set_rf_samples,以至于无法使用 OOB,那么可以创建一个第二个验证集,这个验证集是一个随机样本。换句话说,如果你的 OOB 或者随机样本验证集变得更糟,那么你一定是过拟合了。我认为在你的情况下,Terrance,这不太可能是问题,因为随机森林不会过拟合得那么严重。除非你使用一些非常奇怪的参数,比如只有一个估计器,否则很难让它们过拟合得那么严重。一旦我们有了十棵树,应该有足够的变化,你肯定可以过拟合,但不会过度到添加一个变量就破坏你的验证分数。所以我认为你会发现这可能不是问题,但很容易检查。如果不是这种情况,那么你会发现你的 OOB 分数或者随机样本验证分数并没有变得更糟。

  • 如果您的验证分数变差的第二个原因是,如果您的 OOB 分数没有变差,那么您并没有过拟合,但是您的验证分数变差了,这意味着您在训练集中做了一些在验证集中不成立的事情。因此,这种情况只会发生在您的验证集不是随机抽样的情况下。例如,在这个推土机比赛或者杂货购物比赛中,我们故意制作了一个验证集,该验证集涵盖了不同的日期范围——最近的两周。因此,如果在最近两周发生了与之前几周不同的事情,那么您可能会完全破坏您的验证集。例如,如果有一种在两个日期段中不同的唯一标识符,那么您可能会学会在训练集中使用该标识符来识别事物。但是最近的两周可能有完全不同的 ID 集或不同的行为集,这可能会变得更糟。尽管您所描述的情况并不常见。所以我有点怀疑——这可能是一个错误,但希望您现在有足够的方法来确定是否是一个错误。我们将很乐意听到您学到了什么。

线性回归,逻辑回归

这就是特征重要性。我想将其与在机器学习之外的行业和学术界(如心理学、经济学等)通常进行的特征重要性比较一下。一般来说,在这些环境中,人们倾向于使用某种线性回归、逻辑回归、一般线性模型等方法。他们从数据集开始,然后说我要假设我知道自己的自变量和因变量之间的参数关系。所以我要假设这是一个线性关系或者一个带有链接函数(如 sigmoid)的线性关系,从而创建逻辑回归。所以假设我已经知道了这一点,我现在可以将其写成一个方程。所以如果你有 x1、x2 等等。

我可以说我的 y 值等于ax1 + bx2 = y,因此我可以通过查看这些系数并看到哪个最高来很容易地找出特征的重要性,特别是如果您首先对数据进行了归一化。有一个常见的误解是,这种方法在某种程度上更准确,更纯粹,更好,但事实并非如此。如果您考虑一下,如果您缺少一个交互作用,如果您缺少所需的转换,或者如果您在任何预处理方面不完美,以至于您的模型是情况的绝对正确真相 - 除非您全部正确,否则您的系数是错误的。您的系数告诉您“在您完全错误的模型中,这些事物有多重要”,这基本上是毫无意义的。而另一方面,随机森林的特征重要性告诉您,在这种极高参数、高度灵活的函数形式中,几乎没有任何统计假设,这是您的特征重要性。所以我会非常谨慎。

再次强调,当您离开这个程序时,您更多地会看到人们谈论逻辑回归系数,而不是随机森林变量重要性。每当您看到这种情况发生时,您应该非常怀疑您所看到的内容。每当您阅读经济学或心理学的论文,或者市场营销部门告诉您这种回归或其他内容时,这些系数都会受到模型中任何问题的严重偏见。此外,如果他们进行了大量的预处理,实际上模型相当准确,那么现在您看到的系数将会像来自 PCA 的某个主成分的系数或某个集群的某个距离的系数。在这种情况下,它们非常难以解释。它们不是实际的变量。所以这是我看到的人们尝试使用经典统计技术来进行等效变量重要性时的两种选择。我认为事情开始慢慢改变。有一些领域开始意识到这完全是错误的做法。但自从随机森林出现以来已经将近 20 年了,所以需要很长时间。人们说,只有当上一代人死去时,知识才会真正进步,这在某种程度上是真的。特别是学者,他们以擅长某个特定子领域而成为职业,通常直到下一代人出现时,人们才会注意到实际上这不再是一个好的做事方式。我认为这就是这里发生的事情。

我们现在有一个模型,从预测准确性的角度来看并没有更好,但我们有一种很好的感觉,似乎有四个主要重要的因素:YearMade,Coupler_System,ProductSize,fiProductClassDesc。

一热编码

然而,我们还可以做另一件事,那就是我们可以做一种称为独热编码的东西。这就是我们在谈论分类变量时所说的。记住,分类变量,假设我们有一个字符串高、低、中(我们得到的顺序有点奇怪——默认按字母顺序排列)。所以我们将其映射为 0、1、2。当它进入我们的数据框时,现在它是一个数字,因此随机森林不知道它最初是一个类别——它只是一个数字。因此,当构建随机森林时,它基本上会说它是否大于 1 或不大于 1。或者它是否大于 0 或不大于 0。这基本上是它可以做出的两个可能决定。对于有 5 或 6 个级别的东西,可能只有一个类别级别是有趣的。也许唯一重要的是它是否未知。也许不知道它的大小会以某种方式影响价格。因此,如果我们想要能够识别这一点,特别是如果恰好数字编码的方式是未知的最终出现在中间,那么它将需要两次分割才能看到实际上重要的是未知的事情。因此,这有点低效,我们正在浪费树的计算。浪费树的计算很重要,因为每次我们进行分割时,我们至少要减少一半的数据量来进行更多的分析。因此,如果我们没有以方便它进行所需工作的方式提供数据,那么我们的树将变得不那么丰富和有效。

我们可以做的是为每个类别创建 6 列,每列包含 1 和 0。在我们的数据集中添加了 6 列后,随机森林现在可以选择其中一列并说“哦,让我们看看 is_unknown”。我可以做一个可能的拟合,即 1 对 0。让我们看看这是否有效。因此,它现在可以在一个步骤中提取一个类别级别,并且这种编码称为独热编码。对于许多类型的机器学习模型,这样的东西是必要的。如果你正在进行逻辑回归,你不可能放入一个分类变量,它经过 0 到 5,因为显然它与任何东西之间没有线性关系。因此,许多人错误地认为所有机器学习都需要独热编码。但在这种情况下,我将向您展示如何可以选择使用它,并查看它是否有时可能会改善事情。

问题:如果我们有六个类别,就像在这种情况下一样,为每个类别添加一列会有什么问题吗?在线性回归中,如果有六个类别,我们应该只对其中五个进行操作。你当然可以说,让我们不要担心添加is_medium,因为我们可以从其他五个中推断出来。我会建议无论如何都要包括它,因为否则,随机森林就必须做出五个决定才能到达那一点。你不包括一个在线性模型中的原因是因为线性模型讨厌共线性,但在这里我们不在乎这个。

因此,我们可以很容易地进行独热编码,我们的做法是向proc_df传递一个额外的参数,即最大类别数(max_n_cat)。因此,如果我们说是七,那么任何级别少于七的东西都将被转换为一组独热编码的列。

df_trn2, y_trn, nas = proc_df(df_raw, 'SalePrice', max_n_cat=7) 
X_train, X_valid = split_vals(df_trn2, n_trn) 
m = RandomForestRegressor(
       n_estimators=40, 
       min_samples_leaf=3, 
       max_features=0.6, 
       n_jobs=-1, 
       oob_score=True
) 
m.fit(X_train, y_train) 
print_score(m)
'''
[0.2132925755978791, 0.25212838463780185, 0.90966193351324276, 0.88647501408921581, 0.89194147155121262]
'''

例如邮政编码有超过六个级别,因此将保留为数字。一般来说,您显然不希望对邮政编码进行独热编码,因为这只会创建大量数据、内存问题、计算问题等。因此,这是您可以尝试的另一个参数。

所以如果我尝试一下,像往常一样运行随机森林,你可以看到验证集的 R²和验证集的 RMSE 会发生什么变化。在这种情况下,我发现它变得稍微糟糕了。这并不总是这样,这将取决于你的数据集。这取决于你的数据集是否有单个类别往往相当重要。在这种特殊情况下,它并没有使预测更准确。然而,它所做的是我们现在有了不同的特征。proc_df 将变量的名称、下划线和级别名称放在一起。有趣的是,结果表明以前说围栏是有些重要的。当我们将其进行独热编码时,它实际上说Enclosure_EROPS w AC是最重要的事情。所以至少在解释模型的目的上,你应该尝试对你的变量进行独热编码。我经常发现大约 6 或 7 个变量相当不错。你可以尝试将这个数字尽可能地提高,这样计算不会花费太长时间,而且特征重要性不会包括那些不感兴趣的非常小的级别。这取决于你自己去尝试,但在这种情况下,我发现这非常有趣。它清楚地告诉我我需要找出Enclosure_EROPS w AC是什么,为什么它很重要,因为现在对我来说毫无意义,但它是最重要的事情。所以我应该去弄清楚。

问题:你能解释一下如何改变类别的最大数量吗?因为对我来说,似乎只有五个类别或六个类别[49:15]。它所做的就是这里有一个叫做邮政编码、使用频段和性别的列,例如。比如说,邮政编码有 5,000 个级别。类别中的级别数量,我们称之为“基数”。所以它的基数是 5,000。使用频段可能有六个基数。性别有两个基数。所以当 proc_df 遍历并说好的时候,这是一个分类变量,我应该进行独热编码吗?它会检查基数与max_n_cat进行比较,说 5,000 大于七,所以我不进行独热编码。然后它转到使用频段——6 小于 7,所以我进行独热编码。它转到性别,2 小于 7,所以也进行独热编码。所以它只是为每个变量决定是否进行独热编码。一旦我们决定进行独热编码,它就不会保留原始变量。

如果你确实努力将你的有序变量转换为适当的有序变量,使用 proc_df 可能会破坏这一点。避免这种情况的简单方法是,如果我们知道我们总是想要使用使用频段的代码,你可以直接替换它:

现在它是一个整数。所以它永远不会改变。

去除冗余特征[54:57]

我们已经看到,基本上测量相同事物的变量会混淆我们的变量重要性。它也会使我们的随机森林稍微不那么好,因为需要更多的计算来做同样的事情,还有更多的列要检查。所以我们要做一些额外的工作来尝试去除冗余特征。我做的方法是做一些叫做“树状图”的东西。它有点像分层聚类。

聚类分析是一种尝试查看对象的方法,它们可以是数据集中的行或列,并找出彼此相似的对象。通常你会看到人们特别谈论聚类分析,他们通常指的是数据的行,并会说“让我们绘制它”并找出聚类。一种常见的聚类分析类型,如果时间允许,我们可能会详细讨论一下,被称为 k 均值。基本上,你假设你根本没有任何标签,然后随机选择几个数据点,逐渐找到靠近它的数据点,并将它们移动到离质心更近的位置,然后再次重复这个过程。这是一种迭代的方法,你告诉它你想要多少个聚类,它会告诉你它认为哪些类别在哪里。

一个真正被低估的技术(20 或 30 年前比今天更受欢迎)是层次聚类,也称为凝聚聚类。在层次或凝聚聚类中,我们查看每对对象,并说哪两个对象最接近。然后我们取最接近的一对,删除它们,并用两者的中点替换它们。然后再重复这个过程。由于我们正在删除点并用它们的平均值替换它们,您逐渐通过成对组合减少了点的数量。很酷的是,您可以绘制出来。

from scipy.cluster import hierarchy as hc
corr = np.round(scipy.stats.spearmanr(df_keep).correlation, 4)
corr_condensed = hc.distance.squareform(1-corr)
z = hc.linkage(corr_condensed, method='average')
fig = plt.figure(figsize=(16,10))
dendrogram = hc.dendrogram(
      z, 
      labels=df_keep.columns, 
      orientation='left', 
      leaf_font_size=16
)
plt.show()

就像这样。不是看点,而是看变量,我们可以看到哪两个变量最相似。saleYearsaleElapsed非常相似。因此,这里的横轴是正在比较的两个点有多相似。如果它们更靠近右侧,那意味着它们非常相似。因此,saleYearsaleElapsed已经被合并,并且它们非常相似。

在这种情况下,我实际上使用了斯皮尔曼相关系数 R。你们已经熟悉相关系数了吗?所以相关性几乎与 R²完全相同,但它是在两个变量之间而不是一个变量和它的预测之间。普通相关性的问题在于,如果你有这样的数据,那么你可以进行相关性分析,你会得到一个好的结果。

但是如果你有这样的数据,并尝试进行相关性分析(假设是线性的),那就不太好了。

因此有一种称为秩相关的东西,这是一个非常简单的想法。用每个点的秩替换它。

从左到右,我们按照 1、2、…6 的顺序排名。然后你也要对 y 轴做同样的操作。然后你创建一个新的图,不是绘制数据,而是绘制数据的排名。如果你仔细想一想,这个数据集的排名看起来会像一条直线,因为每当 x 轴上的某个值更大时,y 轴上的值也更大。因此,如果我们对排名进行相关性分析,那就是称为排名相关性。

因为我们想要找到那些在某种方式上与随机森林发现它们相似的列(随机森林不关心线性,它们只关心排序),所以秩相关是正确的思考方式。所以斯皮尔曼相关系数是最常见的秩相关的名称。但你可以用数据的秩替换数据,然后将其传递给常规相关性,你将得到基本相同的答案。唯一的区别在于如何处理并列的数据,这是一个相当次要的问题。

一旦我们有了一个相关矩阵,基本上有几个标准步骤可以将其转换为树状图,每次我都必须在 stackoverflow 上查找。你基本上将其转换为一个距离矩阵,然后创建一个告诉你哪些东西在层次上连接到彼此的东西的东西。所以这是你总是必须做的三个标准步骤来创建一个树状图:

corr_condensed = hc.distance.squareform(1-corr)
z = hc.linkage(corr_condensed, method='average')
dendrogram = hc.dendrogram(
       z, 
       labels=df_keep.columns, 
       orientation='left', 
       leaf_font_size=16
)

然后你可以绘制它[1:01:30]。saleYearsaleElapsed基本上在衡量相同的东西(至少在排名上),这并不奇怪,因为saleElapsed是自我的数据集中的第一天以来的天数,所以显然这两者几乎完全相关。Grouser_TracksHidraulics_FlowCoupler_System似乎在衡量相同的东西。这很有趣,因为记住,Coupler_System被认为非常重要。所以这更支持了我们的假设,这与是否是一个连接器系统无关,而是与它是什么类型的车辆具有这种特征。ProductGroupProductGroupDesc似乎在衡量相同的东西,fiBaseModelfiModelDesc也是如此。一旦我们超过这一点,突然之间的距离更远,所以我可能不会担心那些。所以我们将研究那些非常相似的四组。

如果你只想知道这个东西与那个东西有多相似,最好的方法是查看 Spearman's R 相关矩阵[1:03:43]。这里没有使用随机森林。距离度量完全是基于秩相关性进行的。

然后我做的是,我取这些组并创建一个小函数get_oob(获取 Out Of Band 分数)[1:04:29]。它为某个数据框执行一个随机森林。我确保已经将该数据框拆分为训练集和验证集,然后调用fit并返回 OOB 分数。

def get_oob(df):
    m = RandomForestRegressor(
       n_estimators=30, 
       min_samples_leaf=5, 
       max_features=0.6, 
       n_jobs=-1, 
       oob_score=True
)
    x, _ = split_vals(df, n_trn)
    m.fit(x, y_train)
    return m.oob_score_

基本上我要做的是尝试逐个去掉这 9 个左右的变量中的每一个,看看哪些我可以去掉而不会使 OOB 分数变得更糟。

get_oob(df_keep)
'''
0.89019425494301454
'''

每次我运行这个,我得到稍微不同的结果,所以实际上看起来上一次我有 6 个而不是 9 个。所以你可以看到,我只是循环遍历我认为可能可以去掉的每一个东西,因为它是多余的,然后打印出模型的列名和在去掉那个列之后训练的模型的 OOB 分数。

for c in (
       'saleYear', 'saleElapsed', 'fiModelDesc', 'fiBaseModel', 
       'Grouser_Tracks', 'Coupler_System'
):
    print(c, get_oob(df_keep.drop(c, axis=1)))

整个数据框的 OOB 分数为 0.89,然后在去掉每一个这些东西之后,基本上没有一个变得更糟。saleElapsedsaleYear要糟糕得多。但看起来其他几乎所有的东西,我只能去掉一个小数点问题。所以显然,你必须记住树状图。让我们看看 fiModelDesc 和 fiBaseModel,它们非常相似。所以这意味着的不是我可以去掉它们中的两个,而是我可以去掉其中一个,因为它们基本上在衡量同一件事情。

saleYear 0.889037446375
saleElapsed 0.886210803445
fiModelDesc 0.888540591321
fiBaseModel 0.88893958239
Grouser_Tracks 0.890385236272
Coupler_System 0.889601052658

然后我尝试了。让我们尝试每组中去掉一个:

to_drop = ['saleYear', 'fiBaseModel', 'Grouser_Tracks']
get_oob(df_keep.drop(to_drop, axis=1))
'''
0.88858458047200739
'''

我们从 0.890 到 0.888,再次,它们之间的差距太小以至于无关紧要。听起来不错。简单就是好。所以我现在要从我的数据框中删除这些列,然后我可以尝试再次运行完整的模型。

df_keep.drop(to_drop, axis=1, inplace=True)
X_train, X_valid = split_vals(df_keep, n_trn)
np.save('tmp/keep_cols.npy', np.array(df_keep.columns))
keep_cols = np.load('tmp/keep_cols.npy')
df_keep = df_trn[keep_cols]

reset_rf_samples意味着我使用了整个自助采样。有 40 个估计器,我们得到了 0.907。

reset_rf_samples()
m = RandomForestRegressor(
       n_estimators=40, 
       min_samples_leaf=3, 
       max_features=0.5, 
       n_jobs=-1, 
       oob_score=True
)
m.fit(X_train, y_train)
print_score(m)
'''
[0.12615142089579687, 0.22781819082173235, 0.96677727309424211, 0.90731173105384466, 0.9084359846323049]
'''

现在我有了一个更小更简单的模型,并且得分很好。所以在这一点上,我已经尽可能地去掉了许多列(那些要么没有很好的特征重要性,要么与其他变量高度相关,当我去掉它们时,模型没有显著变差)。

部分依赖[1:07:34]

现在我到了想要通过利用模型更好地了解我的数据的阶段。我们将使用一种称为偏依赖的技术。再次强调,这是你可以在 Kaggle 内核中使用的东西,很多人会欣赏这一点,因为几乎没有人知道偏依赖,它是一种非常强大的技术。我们要做的是找出对于重要的特征,它们如何与因变量相关。让我们来看看。

from pdpbox import pdp
from plotnine import *

再次,由于我们正在进行解释,我们将设置set_rf_samples为 50,000,以便快速运行事务。

set_rf_samples(50000)

我们将获取我们的特征重要性,并注意我们正在使用max_n_cat,因为我实际上对看到解释的各个级别很感兴趣。

df_trn2, y_trn, nas = proc_df(df_raw, 'SalePrice', max_n_cat=7)
X_train, X_valid = split_vals(df_trn2, n_trn)
m = RandomForestRegressor(
       n_estimators=40, 
       min_samples_leaf=3, 
       max_features=0.6, 
       n_jobs=-1
)
m.fit(X_train, y_train);

这是前 10 个:

plot_fi(rf_feat_importance(m, df_trn2)[:10]);

让我们尝试更多地了解那些前 10 个。YearMade是第二重要的。所以一个明显的事情是我们可以做的是绘制YearMadesaleElapsed的关系,因为正如我们已经讨论过的,它们似乎是重要的,但很可能它们是结合在一起找出产品在销售时的年龄。所以我们可以尝试绘制YearMadesaleElapsed,看看它们之间的关系。

df_raw.plot('YearMade', 'saleElapsed', 'scatter', alpha=0.01, figsize=(10,8));

当我们这样做时,我们得到了这个非常丑陋的图表。它告诉我们YearMade实际上有很多是一千。显然,这是我会倾向于回到客户那里并说好的,我猜这些推土机实际上不是在公元 1000 年制造的,他们可能会对我说“是的,这些是我们不知道制造地点的产品”。也许“1986 年之前,我们没有追踪”或者“在伊利诺伊州销售的产品,我们没有提供这些数据”等等——他们会告诉我们一些原因。为了更好地理解这个图,我只是要从分析的解释部分中将它们移除。我们只会获取YearMade大于 1930 的数据。

x_all = get_sample(df_raw[df_raw.YearMade>1930], 500)
ggplot(x_all, aes('YearMade', 'SalePrice')) + \
       stat_smooth(se=True, method='loess')

现在让我们看一下YearMadeSalePrice之间的关系。有一个非常棒的包叫做ggplotggplot最初是一个 R 包(GG 代表图形语法)。图形语法是一种非常强大的思考方式,可以以非常灵活的方式生成图表。我在这门课上不会谈论它太多。网上有很多信息可供参考。但我绝对推荐它作为一个很棒的包来使用。ggplot可以通过pip安装,它已经是 fast.ai 环境的一部分。Python 中的ggplot基本上具有与 R 版本相同的参数和 API。R 版本有更好的文档,所以你应该阅读它的文档以了解如何使用它。但基本上你会说“好的,我想为这个数据框(x_all)创建一个图。当你创建图时,你使用的大多数数据集都太大而无法绘制。例如,如果你做一个散点图,它会创建很多点,导致一团糟,而且会花费很长时间。记住,当你绘制东西时,你是在看它,所以没有必要绘制一个有一亿个样本的东西,当你只使用十万个时,它们会完全相同。这就是为什么我首先调用get_sampleget_sample只是抓取一个随机样本。

ggplot(x_all, aes('YearMade', 'SalePrice')) + \
       stat_smooth(se=True, method='loess')

所以我只是从我的数据框中抓取 500 个点,然后绘制YearMadeSalePriceaes代表“美学” - 这是你在ggplot中设置列的基本方式。然后在ggplot中有一个奇怪的东西,“+”表示添加图表元素。所以我要添加一个平滑线。通常你会发现散点图很难看清楚发生了什么,因为有太多的随机性。或者,平滑线基本上为图的每个小子集创建一个小线性回归。这样可以连接起来,让你看到一个漂亮的平滑曲线。这是我倾向于查看单变量关系的主要方式。通过添加se=True,它还会显示这个平滑线的置信区间。loess代表局部加权回归,这是做许多小型回归的想法。

所以我们可以在这里看到,YearMadeSalePrice之间的关系非常混乱,这并不是我们所期望的。我本来以为最近卖出的东西可能会更贵,因为通货膨胀和更现代的型号。问题在于,当你看一个像这样的单变量关系时,会有很多共线性发生 - 很多互动被忽略了。例如,价格为什么会下降?是因为 1991 年至 1997 年之间制造的东西价值更低吗?还是因为大部分产品在那个时期也被卖出,那时可能有经济衰退?或者是因为在那个时期卖出的产品,更多人购买了价格更低的车辆类型?有各种各样的原因。所以再次,作为数据科学家,你将会看到的一件事是,在你加入的公司里,人们会拿着这种单变量图来找你,他们会说“哦天啊,我们在芝加哥的销售量消失了。变得很糟糕。”或者“人们不再点击这个广告了”,然后他们会给你看一个看起来像这样的图表,问发生了什么。大多数情况下,你会发现答案是“发生了什么”的问题是有其他原因的。比如,“实际上上周在芝加哥,我们在做一个新的促销活动,这就是为什么我们的收入下降了 - 不是因为人们不再在芝加哥购买东西了;价格更低了”。

所以我们真正想要做的是说“嗯,SalePriceYearMade之间的关系是什么,其他所有事情都相等。” “其他所有事情都相等”基本上意味着如果我们在 1990 年和 1980 年卖了同样的东西给同样的人在同样的拍卖会上等等,价格会有什么不同?为了做到这一点,我们做了一个叫做部分依赖图的东西。

x = get_sample(X_train[X_train.YearMade>1930], 500)

有一个非常好的库,没有人听说过,叫做pdp,它可以做这些部分依赖图,发生的情况是这样的。我们有 500 个数据点的样本,我们要做一些非常有趣的事情。我们将对这 500 个随机选择的拍卖会进行处理,然后我们将从中制作一个小数据集。

这是我们的数据集,有 500 个拍卖品,这是我们的列,其中一个是我们感兴趣的事物YearMade。我们现在要尝试创建一个图表,在这个图表中我们说在 1960 年,其他所有事物都相等的情况下,拍卖品的成本是多少?我们将用 1960 年替换YearMade列。我们将一直复制值 1960,直到最后。现在每一行,制造年份都是 1960,所有其他数据都将完全相同。我们将使用我们的随机森林,将所有这些数据传递给我们的随机森林来预测销售价格。这将告诉我们,对于所有被拍卖的物品,如果那个物品是在 1960 年制造的,我们认为它将被卖出多少钱。这就是我们将在右侧绘制的内容。

我们将为 1961 年做同样的事情。

问题:明确一点,我们已经拟合了随机森林,然后我们只是传递一个新的年份,看看它确定的价格应该是多少?是的,这很像我们做特征重要性的方式。但是,我们不是随机洗牌列,而是用一个常数值替换列。随机洗牌列告诉我们当您不再使用该列时它有多准确。用一个常数值替换整个列为我们估计了如果那个产品是在 1961 年制造的,我们将在那天在那个地方的那个拍卖会上卖出那个产品多少钱。然后我们取所有从那个随机森林计算出的销售价格的平均值。我们在 1961 年这样做,得到这个值:

def plot_pdp(feat, clusters=None, feat_name=None):
    feat_name = feat_name or feat
    p = pdp.pdp_isolate(m, x, feat)
    return pdp.pdp_plot(
       p, feat_name, 
       plot_lines=True, 
       cluster=clusters is not None, 
       n_cluster_centers=clusters
    )
plot_pdp('YearMade')

所以这里的部分依赖图(PDP)向我们展示的是每一条浅蓝色线实际上都显示了所有 500 条线。因此,对于我们数据集中的第 1 行,如果我们在 1960 年卖出它,我们将将其索引为零,称之为零。如果在 1970 年卖出那个特定的拍卖品,它将在这里,等等。我们实际上绘制了所有 500 个预测,即如果我们用不同的值替换其YearMade,那么这 500 个拍卖品中的每一个将会卖出多少钱。然后这条深色线是平均值。因此,这告诉我们,如果所有这些产品实际上是在 1985 年、1990 年、1993 年等制造的,我们将平均卖出这些拍卖品多少钱。因此,您可以看到,这里发生的情况是,至少在我们有相当多数据的时期,即自 1990 年以来,这基本上是一条完全直线,这是您所期望的。因为如果在同一日期卖出,而且是同一种拖拉机,卖给同一个人在同一个拍卖行,那么您会期望更近期的车辆更昂贵,因为通货膨胀和它们是更新的。您会期望这种关系大致是线性的,这正是我们发现的。通过消除所有这些外部因素,通常能够更清楚地看到真相。

这个部分依赖图是使用随机森林来更清晰地解释我们数据中发生的情况。步骤是:

  1. 首先看一下未来的重要性,告诉我们我们认为我们关心哪些事情。

  2. 然后使用部分依赖图告诉我们平均情况下发生了什么。

我们可以用 PDP 做另一件很酷的事情,那就是我们可以使用聚类。聚类的作用是利用聚类分析来查看这 500 行中的每一行,并判断这 500 行中是否有一些行以相同的方式移动。我们可以看到似乎有很多行是先下降然后上升,还有一些行是先上升然后趋于平缓。看起来似乎有一些不同类型的行为被隐藏了,所以这里是进行聚类分析的结果:

plot_pdp('YearMade', clusters=5) 

我们仍然得到相同的平均值,但这里列出了我们看到的五种最常见的形状。这就是你可以进去并说好吧,看起来有些车辆在 1990 年后,它们的价格相当稳定。在那之前,它们是相当线性的。其他一些车辆则恰恰相反,所以不同种类的车辆有不同的形状。因此,这是你可以深入研究的内容。

问题:那么我们要如何处理这些信息呢?解释的目的是为了了解数据集,那么你为什么想要了解一个数据集呢?因为你想要对其进行某些操作。所以在这种情况下,如果你试图赢得 Kaggle 竞赛,这并不是什么大不了的事情——一些洞察可能让你意识到我可以转换这个变量或创建这种互动等等。显然,特征重要性对于 Kaggle 竞赛非常重要。但这更多地是为了现实生活。所以当你与某人交谈时,你对他们说“好的,你一直向我展示的那些图表实际上表明在 1990 年至 1997 年之间基于某些因素价格出现了下降。实际上并没有。实际上它们是在增长,那时发生了其他事情。”这基本上是让你说出,无论我试图在我的业务中推动的结果是什么,这就是某种驱动力。所以如果我在看广告技术,是什么在推动点击,我实际上正在深入研究,看看点击是如何被推动的。这实际上是在推动它的变量。这是它们之间的关系。因此,我们应该以这种方式改变我们的行为。这实际上是任何模型的目标。我想有两个可能的目标:一个模型的目标只是为了获得预测,比如如果你在进行对冲基金交易,你可能想知道那只股票的价格会是多少。如果你在做保险,你可能只想知道那个人会有多少索赔。但大多数情况下,你实际上是在尝试改变你的业务方式——你如何做市场营销,如何做物流,所以你真正关心的是这些事物之间的关系。

问题:你能再解释一下为什么这个下降并不意味着我们所认为的吗?是的。这是一个经典的无聊的单变量图。这只是将所有的点,所有的选项,将制造年份与销售价格进行绘图,并且我们只是通过它们拟合一个粗略的平均值。在我们的数据集中,1992 年至 1997 年制造的产品平均销售价格较低。在商业中,你经常会听到有人看到这样的情况并说“我们应该停止拍卖那些在这些年份制造的设备,因为我们得到的钱更少”,例如。但事实上,可能是在那些年份,人们制造了更多的小型工业设备,你会期望它们的售价更低,而实际上我们的利润也同样高。或者并不是那些年份制造的东西现在会更便宜,而是在那些年份卖东西时,它们更便宜,因为当时正值经济衰退。如果你真的想根据这个采取一些行动,你可能并不只关心那些年份制造的东西平均更便宜,而是这对今天有什么影响。因此,我们采用 PDP 方法,实际上是说让我们尝试消除所有这些外部因素。因此,如果同一天向同一人出售同一种类型的车辆,那么实际上制造年份如何影响价格。这基本上是说,例如,如果我在拍卖会上决定买什么,那么这对我来说意味着平均而言,购买一辆更近期的车辆确实会给你更多的钱,这并不是单变量图所说的。

评论:2010 年生产的推土机可能与 1960 年生产的推土机类型不太接近。如果你拿一个非常不同的东西,比如 2010 年的推土机,然后试图说“哦,如果它是 1960 年生产的”,这可能会导致预测不准确,因为它远远超出了训练集的范围。绝对。这是一个很好的观点。然而,这是一个限制,如果你有一个数据点在它以前没有见过的空间中,比如也许 1960 年的推土机没有安装空调,你在说这台带空调的推土机在 1960 年会卖多少钱,你实际上没有任何信息来知道这一点。这仍然是我知道的最好的技术,但并不完美。你希望树仍然能找到一些有用的真相,即使它以前没有见过这些特征的组合。但是,是的,这是需要注意的事情。

feats = ['saleElapsed', 'YearMade']
p = pdp.pdp_interact(m, x, feats)
pdp.pdp_interact_plot(p, feats)

您也可以在 PDP 交互图中执行相同的操作[1:27:36]。而我真正想要的 PDP 交互图是,saleElapsed 和 YearMade 如何共同影响价格。如果我做一个 PDP 交互图,它会显示给我 saleElapsed vs. price,YearMade vs. price,以及两者的组合 vs. price。请记住,这里始终是价格的对数。这就是为什么这些价格看起来很奇怪。您可以看到 saleElapsed 和 YearMade 的组合正如您所期望的那样——价格最高的是那些经过的时间最短和最近制造的年份。右上角是 saleElapsed 和价格之间的单变量关系,左下角是 YearMade 和价格之间的单变量关系,右下角是两者的组合。足以清楚地看到这两个因素共同推动价格。您还可以看到这些不是简单的对角线,因此存在一些有趣的交互作用。根据观察这些图,我认为,也许我们应该加入某种交互项并看看会发生什么。所以让我们稍后再回到这个问题,但让我们先看几个例子。

请记住,在这种情况下,我们进行了独热编码——在最开始时,我们说max_n_cat=7[1:29:18]。因此,我们有像Enclosure_EROPS w AC这样的变量。因此,如果您有独热编码的变量,您可以将它们的数组传递给plot_pdp,它将把它们视为一个类别。

因此,在这种情况下,我将创建这三个类别的 PDP 图,并将其命名为“Enclosure”。

plot_pdp([
       'Enclosure_EROPS w AC', 
       'Enclosure_EROPS', 
       'Enclosure_OROPS'
], 5, 'Enclosure')

我可以看到Enclosure_EROPS w AC的平均价格要高于Enclosure_EROPSEnclosure_OROPS。实际上,后两者看起来相似,或者Enclosure_EROPS w AC更高。因此,此时我可能倾向于跳转到 Google 并搜索“erops orops”以了解这些内容,然后我们继续。

事实证明,EROPS 是封闭式翻转保护结构,因此如果您的推土机完全封闭,则可以选择安装空调。因此,这实际上告诉我们它是否有空调。如果是开放式结构,那么显然根本没有空调。这就是这三个级别的含义。因此,我们现在知道,其他条件相同的情况下,同一时间销售,同一时间制造,销售给同一人的推土机,如果有空调,价格会比没有空调的要高得多。因此,我们再次获得了这种很好的解释能力。现在我花了一些时间处理这个数据集,我肯定注意到了知道这一点是最重要的事情,您会注意到现在有更多的带空调的推土机,比过去有更多,因此日期和这之间肯定存在交互作用。

df_raw.YearMade[df_raw.YearMade<1950] = 1950
df_keep['age'] = df_raw['age'] = df_raw.saleYear-df_raw.YearMade
X_train, X_valid = split_vals(df_keep, n_trn)
m = RandomForestRegressor(
       n_estimators=40, 
       min_samples_leaf=3, 
       max_features=0.6, 
       n_jobs=-1
)
m.fit(X_train, y_train)
plot_fi(rf_feat_importance(m, df_keep));

根据之前的交互分析,首先我尝试将 1950 年之前的所有内容设置为 1950 年,因为这似乎是某种缺失值[1:31:25]。我将age设置为saleYear - YearMade。然后我尝试在此基础上运行随机森林。确实,age现在是最重要的因素,saleElapsed 远远落后,YearMade 也落后。因此,我们使用这个找到了一个交互作用。但请记住,随机森林可以通过具有多个分割点来创建交互作用,因此我们不应该假设这实际上会带来更好的结果。实际上,当我查看我的得分和 RMSE 时,我发现添加 age 实际上效果稍差。也许在下一节课中我们会看到更多相关内容。

树解释器[1:32:34]

最后一件事是树解释器。这也属于大多数人不知道存在的事物类别,但它非常重要。对于 Kaggle 竞赛几乎毫无意义,但对于现实生活非常重要。这是个想法。假设你是一家保险公司,有人打电话给你,你给他们报价,他们说“哦,比去年贵了 500 美元。为什么?”总的来说,你从某个模型中做出了预测,有人问为什么。这就是我们使用的这种叫做树解释器的方法。树解释器的作用是允许我们取出特定的一行。

from treeinterpreter import treeinterpreter as ti
df_train, df_valid = split_vals(df_raw[df_keep.columns], n_trn)

所以在这种情况下,我们将选择零行。

row = X_valid.values[None,0]; row

这是零行中的所有列。

我可以用树解释器做的是,我可以调用ti.predict,传入我的随机森林和我的行(这将是这个特定客户的保险信息,或者在这种情况下是这个特定拍卖)。它会给我三件事:

  • 预测: 随机森林的预测

  • 偏差: 整个原始数据集的平均销售价格

  • 贡献度: 一列和要拆分的值(即预测器),以及它对预测值的影响有多大。

prediction, bias, contributions = ti.predict(m, row)

所以你可以这样想[1:34:51]。整个数据集的平均对数销售价格为 102。那些联接器系统≤0.5的数据集平均为 10.3。联接器系统≤0.5围栏≤2.0的数据集为 9.9,然后最终我们一直到这里,还有ModelID≤4573.0,为 10.2。所以你可以问,为什么我们为这个特定行预测了 10.2?

这是因为我们从 10.19 开始:

  • 因为联接器系统小于 0.3,我们增加了大约 0.2(所以我们从 10.19 增加到 10.34)。

  • 因为围栏小于 2,我们减去了大约 0.4。

  • 然后因为模型 ID 小于 4573,我们增加了大约 0.7

所以你可以看到,通过一棵树,你可以分解为什么我们预测了 10.2。在每一个决策点,我们都会对值进行一点点的加减。然后我们可以对所有树都这样做,然后我们可以取平均值。每次我们看到围栏,我们增加还是减少了值,以及多少?每次我们看到模型 ID,我们增加还是减少了值,以及多少?我们可以取所有这些的平均值,这就是所谓的贡献度

prediction[0], bias[0]
'''
(9.1909688098736275, 10.10606580677884)
'''
idxs = np.argsort(contributions[0])
[
    o for o in zip(
       df_keep.columns[idxs], 
       df_valid.iloc[0][idxs], 
       contributions[0][idxs]
    )
]

这里是我们所有的预测因子和每个值[1:37:54]。

[('ProductSize', 'Mini', -0.54680742853695008),
 ('age', 11, -0.12507089451852943),
 ('fiProductClassDesc',
  'Hydraulic Excavator, Track - 3.0 to 4.0 Metric Tons',
  -0.11143111128570773),
 ('fiModelDesc', 'KX1212', -0.065155113754146801),
 ('fiSecondaryDesc', nan, -0.055237427792181749),
 ('Enclosure', 'EROPS', -0.050467175593900217),
 ('fiModelDescriptor', nan, -0.042354676935508852),
 ('saleElapsed', 7912, -0.019642242073500914),
 ('saleDay', 16, -0.012812993479652724),
 ('Tire_Size', nan, -0.0029687660942271598),
 ('SalesID', 4364751, -0.0010443985823001434),
 ('saleDayofyear', 259, -0.00086540581130196688),
 ('Drive_System', nan, 0.0015385818526195915),
 ('Hydraulics', 'Standard', 0.0022411701338458821),
 ('state', 'Ohio', 0.0037587658190299409),
 ('ProductGroupDesc', 'Track Excavators', 0.0067688906745931197),
 ('ProductGroup', 'TEX', 0.014654732626326661),
 ('MachineID', 2300944, 0.015578052196894499),
 ('Hydraulics_Flow', nan, 0.028973749866174004),
 ('ModelID', 665, 0.038307429579276284),
 ('Coupler_System', nan, 0.052509808150765114),
 ('YearMade', 1999, 0.071829996446492878)]

视频中存在排序问题,因为没有使用索引排序,但上面的示例是已更正的版本。

contributions[0].sum()
'''
-0.7383536391949419
'''

然后有一个叫做偏差的东西,偏差只是我们在开始进行任何拆分之前的平均值[1:39:03]。如果你从平均对数值开始,然后我们沿着每棵树走,每次看到 YearMade 时,我们有一些影响,联接器系统有一些影响,产品尺寸有一些影响,等等。

我们可能会在下次回到树解释器,但基本思想(这是我们关键解释点的最后一个)是,我们希望不仅能告诉我们关于整个模型以及平均工作原理的信息,还要查看模型如何为单个行进行预测。这就是我们在这里所做的。

机器学习 1:第 5 课

原文:medium.com/@hiromi_suenaga/machine-learning-1-lesson-5-df45f0c99618

译者:飞龙

协议:CC BY-NC-SA 4.0

来自机器学习课程的个人笔记。随着我继续复习课程以“真正”理解它,这些笔记将继续更新和改进。非常感谢 JeremyRachel 给了我这个学习的机会。

视频

复习

  • 测试集,训练集,验证集和 OOB

我们有一个数据集,其中有很多行,我们有一些因变量。机器学习和其他任何工作之间的区别是什么?区别在于,在机器学习中,我们关心的是泛化准确性或泛化错误,而在其他几乎所有情况下,我们只关心我们能够如何将观察结果映射到。所以,泛化是机器学习的关键独特部分。如果我们想知道我们是否做得好,我们需要知道我们是否做得好泛化。如果我们不知道这一点,我们什么也不知道。

问题:泛化,你是指缩放吗?能够扩展吗?[1:26] 不,我一点也不是指缩放。缩放在许多领域中是重要的。就像好吧,我们有一个在我的电脑上使用 1 万个项目的东西,现在我需要让它每秒处理 1 万个项目。所以缩放很重要,但不仅仅是对于机器学习,而是对于我们投入生产的几乎所有东西都很重要。泛化是我说的,好吧,这是一个可以预测猫和狗的模型。我看了五张猫的图片和五张狗的图片,我建立了一个完美的模型。然后我看了另一组五只猫和狗,结果全都错了。所以在这种情况下,它学到的不是猫和狗之间的区别,而是学到了那五只具体的猫长什么样,那五只具体的狗长什么样。或者我建立了一个预测特定产品的杂货销售模型,比如上个月新泽西的卫生纸销售,然后我把它投入生产,它扩展得很好(换句话说,延迟很低,没有高 CPU 负载),但它无法预测除了新泽西的卫生纸之外的任何东西。事实证明,它只在上个月做得好,而不是下个月。这些都是泛化失败。

人们检查泛化能力最常见的方法是创建一个随机样本。他们会随机选择几行数据并将其提取到一个测试集中。然后他们会在其余的行上构建所有模型,当他们完成时,他们会检查在测试集上获得的准确性(其余的行被称为训练集)。所以在建模过程结束时,在训练集上,他们得到了 99%的准确性,预测猫和狗,最后,他们会将其与测试集进行比较,以确保模型真正泛化。

现在的问题是,如果不行怎么办?嗯,我可以回去改变一些超参数,做一些数据增强,或者尝试创建一个更具泛化性的模型。然后我再次回去,做了所有这些之后,检查结果仍然不好。我会一遍又一遍地这样做,直到最终,在尝试了五十次之后,它泛化了。但它真的泛化了吗?因为也许我所做的一切只是偶然找到了这个恰好适用于那个测试集的模型,因为我尝试了五十种不同的方法。所以如果我有一个东西,巧合地 5%的时间是正确的,那么很可能不会偶然得到一个好的结果。所以我们通常会将第二个数据集(验证集)放在一边。然后,不在验证集或测试集中的所有内容现在都是训练集。我们训练一个模型,对其进行验证以查看其是否泛化,重复几次。然后当我们最终得到了我们认为会根据验证集成功泛化的东西(在项目结束时),我们会对其进行测试。

问题:所以基本上通过制作这两层测试集和验证集,如果一个对了另一个错了,你就是在双重检查你的错误?它检查我们是否过度拟合验证集。所以如果我们一遍又一遍地使用验证集,那么我们最终可能得不到一组适用于训练集和验证集的可泛化的超参数,而只是一组恰好适用于训练集和验证集的超参数。所以如果我们对验证集尝试了 50 种不同的模型,然后在所有这些之后,我们再对测试集进行检查,结果仍然是泛化的,那么我们会说好的,我们实际上已经得到了一个可泛化的模型。如果不是,那么就会说我们实际上现在过度拟合了验证集。在那一点上,你会陷入麻烦。因为你没有留下任何东西。所以想法是在建模过程中使用有效的技术,以防止这种情况发生。但如果它确实发生了,你希望找出原因——你需要那个测试集,否则当你投入生产时,结果却不能泛化,那将是一个非常糟糕的结果。你最终会发现点击你的广告的人会减少,或者销售你的产品会减少,或者为高风险车辆提供汽车保险的人会减少。

问题:所以只是为了确保,我们需要检查验证集和测试集是否一致,还是只保留测试集?如果你像我刚刚做的那样随机抽样,没有特定的原因需要检查,只要它们足够大。但我们将在稍后的不同情境中回答你的问题。

我们学到的另一个随机森林的技巧是一种不需要验证集的方法。我们学到的方法是使用 OOB 分数。这个想法是,每次我们在随机森林中训练一棵树时,都会有一些观察结果被保留,因为这就是我们获得一些随机性的方式。因此,让我们基于这些保留样本计算每棵树的分数,然后通过对每行不参与训练的树进行平均,得到整个森林的分数。因此,OOB 分数给我们提供了与验证分数非常相似的东西,但平均来看,它稍微差一些。为什么?因为每一行都将使用一部分树来进行预测,而树越少,我们知道预测就越不准确。这是一个微妙的问题,如果你没有理解,那就在这一周内考虑一下,直到你明白为什么,因为这是对你对随机森林理解的一个非常有趣的测试。为什么 OOB 分数平均来看比你的验证分数差一些?它们都使用随机保留的子集。

总的来说,这通常足够了。那么在使用随机森林时为什么还要有一个验证集呢?如果是一个随机选择的验证集,严格来说并不是必需的,但你有四个层次的测试——所以你可以在 OOB 上测试,当那个工作良好时,你可以在验证集上测试,希望到检查测试集时不会有什么意外,这将是一个很好的理由。

Kaggle 的做法是相当聪明的。Kaggle 的做法是将测试集分成两部分:一个公共部分和一个私有部分。他们不告诉你哪个是哪个。所以你将你的预测提交给 Kaggle,然后随机选择其中 30%用来告诉你排行榜分数。但是在比赛结束时,这部分将被丢弃,他们将使用另外 70%来计算你的真实分数。这样做的目的是确保你不会持续使用来自排行榜的反馈来找出一组在公共部分表现良好但实际上不具有泛化性的超参数。这是一个很好的测试。这也是为什么在比赛结束时使用 Kaggle 是一个很好的做法的原因之一,因为在比赛的最后一天,当他们使用私有测试集时,你的排名会下降一百名,然后你会明白,这就是过拟合的感觉,练习和获得这种感觉要比在公司中有数亿美元的风险要好得多。

这就像是可能的最简单情况,你可以使用一个随机样本作为你的验证集。为什么我可能无法使用一个随机样本作为我的验证集,或者可能失败呢?我的观点是,通过使用一个随机验证集,我们可能会对我们的模型完全产生错误的看法。要记住的重要事情是,当你构建一个模型时,你总是会有一个系统误差,即你将在比构建模型时晚的时间使用该模型。你将在生产中使用它,到那时,世界已经不同于你现在所处的世界,即使在构建模型时,你使用的数据也比今天的数据旧。因此,在你构建模型的数据和实际使用的数据之间存在一些滞后。大部分时间,如果不是大部分时间,这是很重要的。

因此,如果我们正在预测谁会在新泽西购买卫生纸,而我们需要两周时间将其投入生产,并且我们使用了过去几年的数据进行预测,那时情况可能会大不相同。特别是我们的验证集,如果我们随机抽样,而且是从四年的时间段中抽取的,那么绝大多数数据将是一年多以前的。也许新泽西人的购买习惯可能已经发生了巨大变化。也许他们现在正经历着严重的经济衰退,无法再购买高质量的卫生纸。或者也许他们的造纸工业飞速发展,突然间他们购买更多卫生纸,因为价格便宜。世界在变化,因此如果你为验证集使用随机样本,那么实际上你正在检查你在预测完全过时的事物方面有多好?你有多擅长预测四年前发生的事情?这并不有趣。因此,在实践中,每当有一些时间因素时,我们要做的是假设我们已经按时间排序,我们将使用最新的部分作为我们的验证集。我想,实际上应该正确地执行:

这是我们的验证集,这是我们的测试集。所以剩下的就是我们的训练集,我们使用它并尝试能够建立一个模型,使其仍然适用于比模型建立时更晚的时间。因此,我们不仅仅是在某种抽象意义上测试泛化性能,而是在一个非常具体的时间意义上,即它是否能泛化到未来。

问题:正如您所说,数据中存在一些时间顺序,那么在这种情况下,是明智地使用整个数据进行训练,还是只使用最近的一些数据集进行训练?是的,这是一个完全不同的问题。那么如何确保验证集的质量良好呢?我在所有训练数据上构建了一个随机森林。它在训练数据上看起来不错,在 OOB 上也看起来不错。这实际上是有 OOB 的一个很好的原因。如果在 OOB 上看起来不错,那么这意味着你在统计意义上没有过拟合。它在一个随机样本上表现良好。但是然后在验证集上看起来不好。那么发生了什么?嗯,发生的是你在某种程度上未能预测未来。你只是预测了过去,所以 Suraj 有一个关于如何解决这个问题的想法。好吧,也许我们应该只训练,也许我们不应该使用整个训练集。我们应该只尝试最近的一段时间。现在缺点是,我们现在使用的数据更少,因此我们可以创建更少的丰富模型,但好处是,这是更为最新的数据。这是你必须尝试的事情。大多数机器学习函数都有能力为每一行提供一个权重。例如,对于随机森林,你可以在每一行上设置一个权重,并以某种概率随机选择该行。因此,我们可以设置概率,使得最近的行有更高的被选中的概率。这可能非常有效。这是你必须尝试的事情,如果你没有一个代表未来的验证集,与你正在训练的数据相比,你就无法知道哪些技术是有效的。

你如何在数据量和数据新旧之间做出妥协?我倾向于这样做,当我遇到这种时间问题时,也许大部分时间都是这样,一旦我在验证集上找到了一个表现良好的模型,我就不会直接在测试集上使用那个模型,因为测试集比训练集要远得多。所以我会重新构建那个模型,但这次我会将训练和验证集合并起来重新训练模型。在那一点上,你没有办法对验证集进行测试,所以你必须确保你有一个可重现的脚本或笔记本,以确保完全相同的步骤,因为如果你出错了,你会发现在测试集上出现了问题。所以我在实践中所做的是,我需要知道我的验证集是否真正代表了测试集。所以我在训练集上建立了五个模型,并尝试让它们在我认为它们有多好的方面有所不同。然后我在验证集上对我的五个模型进行评分,然后我也在测试集上对它们进行评分。所以我没有作弊,因为我没有使用来自测试集的任何反馈来改变我的超参数——我只是用它来检查我的验证集。所以我从验证集和测试集中得到了五个分数,然后我检查它们是否在一条线上。如果不是,那么你将无法从验证集中获得足够好的反馈。所以继续这个过程,直到你得到一条线,这可能会很棘手。试图创建尽可能接近真实结果的东西是困难的。在现实世界中,创建测试集也是如此——测试集必须尽可能接近生产。那么实际使用这个产品的客户组合是什么样的,你构建模型和投入生产之间实际会有多少时间?你能够多频繁地更新模型?这些都是在构建测试集时需要考虑的事情。

问题:所以首先你在训练数据上建立了五个模型,如果没有得到直线关系,就改变你的验证和测试集?通常你不能真正改变测试集,所以这是假设测试集已经给定,你改变验证集。所以如果你开始用一个随机样本验证集,然后结果千奇百怪,你意识到哦,我应该选择最近的两个月。然后你选择了最近的两个月,结果还是千奇百怪,你意识到哦,我应该选择从每个月的第一天到第十五天,然后不断改变验证集,直到找到一个能够反映你的测试集结果的集合。

问题:对于五个模型,你可能从随机数据、平均值等开始?也许不是五个糟糕的模型,但你想要一些变化,尤其是你想要一些在时间上可能泛化得更好的模型。一个是在整个训练集上训练的,一个是在最后两周训练的,一个是在最后六周训练的,一个使用了很多列可能会过拟合一些。所以你想要知道如果我的验证集在时间上无法泛化,我想看到这一点,如果在统计上无法泛化,我也想看到这一点。

问题:你能详细解释一下你所说的改变验证集以使其表示测试集是什么意思吗?看起来是什么样子?让我们以杂货竞赛为例,我们试图预测接下来两周的杂货销售额。Terrance 和我尝试过的可能的验证集是:

  • 随机样本(4 年)

  • 最近一个月的数据(7 月 15 日至 8 月 15 日)

  • 过去的两周(8 月 1 日至 15 日)

  • 一个月前的同一天范围(7 月 15 日至 30 日)

这个比赛中的测试集是 8 月 15 日至 30 日。所以上面是我们尝试的四个不同的验证集。随机的结果是完全不稳定的。上个月的结果不错但也不是很好。过去的两周,有一些看起来不好,但总体上还不错。一个月前的同一天范围内,他们有一个基本完美的线。

问题:我们到底是在与测试集中的什么进行比较?我建立了 5 个模型,可能是 1.只是预测平均值,2.对整个数据集进行某种简单的组平均,3.对过去一个月的数据进行某种组平均,4.构建整个数据集的随机森林,5.从过去三周构建随机森林。在每一个上,我计算验证分数。然后我在整个训练集上重新训练模型,并在测试集上进行相同的计算。所以现在每个点告诉我它在验证集上表现如何,它在测试集上表现如何。如果验证集有用,我们会说每次验证分数提高,测试集分数也应该提高。

问题:当你说“重新训练”时,你是指在训练和验证集上重新训练模型吗?是的,所以一旦我得到了基于训练集的验证分数,然后在训练和验证集上重新训练,并与测试集进行对比。

问题:通过测试集,你是指将其提交到 Kaggle 并检查分数吗?如果是 Kaggle,那么你的测试集就是 Kaggle 的排行榜。在现实世界中,测试集是你放在一边的第三个数据集。这第三个数据集反映真实世界生产差异是机器学习项目中最重要的一步。为什么这是最重要的一步?因为如果你搞砸了其他一切但没有搞砸这个,你会知道你搞砸了。如果你有一个好的测试集,那么你会知道你搞砸了,因为你搞砸了其他东西并测试了它,结果不尽人意,没关系。你不会毁掉公司。如果你搞砸了创建测试集,那将是可怕的。因为那样你就不知道自己是否犯了错误。你尝试构建一个模型,你在测试集上测试它,看起来不错。但测试集并不代表真实世界环境。所以你实际上不知道你是否会毁掉公司。希望你有逐渐将事物投入生产的方式,这样你就不会真的毁掉公司,但至少会毁掉你在工作中的声誉。哦,Jeremy 试图将这个东西投入生产,在第一周我们尝试的队伍中,他们的销售额减半了,我们再也不会让 Jeremy 做机器学习工作了。但如果 Jeremy 使用了适当的测试集,那么他会知道,哦,这只有我的验证集说的一半好,我会继续尝试。现在我不会惹麻烦了。我实际上很喜欢 Jeremy - 他能提前识别出将会出现泛化问题的情况。

这是每个人在机器学习课程中都会谈论一点的事情,但通常停在你学到了 sklearn 中有一个叫做 make train_test_split的东西,它返回这些东西,然后你就可以继续了,或者这里是交叉验证函数。这些东西总是给你随机样本的事实告诉你,如果不是大部分时间,你不应该使用它们。随机森林免费提供 OOB,这很有用,但只告诉你这在统计意义上是泛化的,而不是在实际意义上。

交叉验证

在课外,你们一直在讨论很多,这让我觉得有人一直在过分强调这种技术的价值。所以我会解释什么是交叉验证,然后解释为什么你大部分时间可能不应该使用它。

交叉验证意味着我们不只是拿出一个验证集,而是拿出五个,例如。所以让我们首先假设我们要随机洗牌数据。这是至关重要的。

  1. 随机洗牌数据。

  2. 将其分成五组

  3. 对于模型 №1,我们将第一个称为验证集,底部四个称为训练集。

  4. 我们将训练并检查验证,得到一些 RMSE、R²等。

  5. 我们将重复这个过程五次,然后取 RMSE、R²等的平均值,这是交叉验证的平均准确度。

使用交叉验证相比标准验证集的好处是什么?你可以使用所有的数据。你不必留下任何东西。而且你还有一个小小的好处,你现在有了五个模型,可以将它们组合在一起,每个模型使用了 80%的数据。有时这种集成可以很有帮助。

有哪些原因你不会使用交叉验证呢?对于大型数据集,它会花费很长时间。我们必须拟合五个模型而不是一个,所以时间是一个关键的缺点。如果我们在进行深度学习,需要一天的时间来运行,突然之间需要五天,或者我们需要 5 个 GPU。那么关于我之前关于验证集的问题呢?我们之前对为什么随机验证集是一个问题的担忧在这里完全相关。这些验证集是随机的,所以如果一个随机验证集对你的问题不合适,很可能是因为,例如,时间问题,那么这五个验证集都不好。它们都是随机的。所以如果你有像之前一样的时间数据,就没有办法进行交叉验证,或者没有好的方法进行交叉验证。你希望你的验证集尽可能接近测试集,而你不能通过随机抽样不同的东西来做到这一点。你可能不需要进行交叉验证,因为在现实世界中,我们通常并没有那么少的数据 —— 除非你的数据是基于一些非常昂贵的标记过程或一些昂贵的实验。但如今,数据科学家并不经常做这种工作。有些人在做,如果是这样,那么这是一个问题,但我们大多数人不是。所以我们可能不需要。即使我们这样做了,它会花费很多时间,即使我们这样做了并花费了所有的时间,它可能会给我们完全错误的答案,因为随机验证集对我们的问题是不合适的。

我不会花太多时间在交叉验证上,因为我认为它是一个有趣的工具,易于使用(sklearn 有一个可以使用的交叉验证工具),但在我看来,它并不经常是你工具箱中的重要部分。有时会用到。所以这就是验证集。

树解释[38:02]

树解释器是做什么的,它是如何做到的呢?让我们从树解释器的输出开始[38:51]。这里是一棵树:

树的根在任何分割之前。因此,10.189 是我们训练集中所有选项的平均对数价格。然后,如果我选择 Coupler_System ≤ 0.5,那么我们得到一个平均值为 10.345(共 16815 个子集)。在 Coupler_System ≤0.5 的人中,我们然后取 Enclosure ≤ 2.0 的子集,那里的平均对数价格为 9.955。然后最后一步是 ModelID ≤ 4573.0,这给我们 10.226。

然后我们可以计算每个额外标准对平均对数价格的变化。我们可以将其绘制为所谓的瀑布图。瀑布图是我知道的最有用的图之一,奇怪的是,Python 中没有任何工具可以绘制它们。这是其中一种情况,管理咨询和商业领域中每个人都经常使用瀑布图,而学术界却不知道这些是什么。每当你有一个起点、一些变化和一个终点时,瀑布图几乎总是展示它们的最佳方式。

在 Excel 2016 中,它是内置的。你只需点击插入瀑布图,它就在那里。如果你想成为一个英雄,为 matplotlib 创建一个瀑布图包,将其放在 pip 上,每个人都会喜欢你的。实际上,这些非常容易构建。你基本上做一个堆叠柱状图,底部全是白色。你可以做到这一点,但如果你能整理好它,把点放在正确的位置并精心着色,那将是非常棒的。我认为你们都有能力做到,这对你的作品集来说将是一件了不起的事情。

一般来说,它们从所有开始,然后逐个变化,然后所有这些的总和将等于最终预测[43:38]。所以如果我们只是做一个决策树,有人问“为什么这个特定拍卖的预测是这个特定价格?”,这就是你可以回答“因为这三个事物产生了这三个影响”的方式。

对于随机森林,我们可以在所有树中执行相同的操作。所以每次我们看到联接器时,我们累加那个变化。每次我们看到围栏时,我们累加那个变化,依此类推。然后将它们全部组合在一起,我们就得到了树解释器的功能。所以你可以查看树解释器的源代码,它并不是非常复杂的逻辑。或者你可以自己构建它,看看它是如何做到这一点的。

from treeinterpreter import treeinterpreter as ti
df_train, df_valid = split_vals(df_raw[df_keep.columns], n_trn)
row = X_valid.values[None,0]; row
'''
array([[4364751, 2300944, 665, 172, 1.0, 1999, 3726.0, 3, 3232, 1111, 0, 63, 0, 5, 17, 35, 4, 4, 0, 1, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 3, 0, 0, 0, 2, 19, 29, 3, 2, 1, 0, 0, 0, 0, 0, 2010, 9, 37,
        16, 3, 259, False, False, False, False, False, False, 7912, False, False]], dtype=object)
'''
prediction, bias, contributions = ti.predict(m, row)

所以当你使用随机森林模型对某个特定拍卖进行 treeinterpreter.predict 时(在这种情况下是零索引行),它告诉你:

  • prediction: 与随机森林预测相同

  • 偏差: 这将始终是相同的 - 这是树中每个随机样本的每个人的平均销售价格

  • contributions: 每次我们在树中看到特定列出现时所有贡献的总和。

prediction[0], bias[0]
'''
(9.1909688098736275, 10.10606580677884)
'''

上次我犯了一个错误,没有正确排序这个。所以这次 np.argsort 是一个非常方便的函数。它实际上并不对 contributions[0] 进行排序,它只是告诉你如果对其进行排序,每个项目将移动到哪里。所以现在通过将 idxs 传递给每个列、级别和贡献,我可以按正确的顺序打印出所有这些。

idxs = np.argsort(contributions[0])
[
   o for o in zip(
      df_keep.columns[idxs], 
      df_valid.iloc[0][idxs], 
      contributions[0][idxs]
   )
]
'''
[('ProductSize', 'Mini', -0.54680742853695008),
 ('age', 11, -0.12507089451852943),
 ('fiProductClassDesc',
  'Hydraulic Excavator, Track - 3.0 to 4.0 Metric Tons',
  -0.11143111128570773),
 ('fiModelDesc', 'KX1212', -0.065155113754146801),
 ('fiSecondaryDesc', nan, -0.055237427792181749),
 ('Enclosure', 'EROPS', -0.050467175593900217),
 ('fiModelDescriptor', nan, -0.042354676935508852),
 ('saleElapsed', 7912, -0.019642242073500914),
 ('saleDay', 16, -0.012812993479652724),
 ('Tire_Size', nan, -0.0029687660942271598),
 ('SalesID', 4364751, -0.0010443985823001434),
 ('saleDayofyear', 259, -0.00086540581130196688),
 ('Drive_System', nan, 0.0015385818526195915),
 ('Hydraulics', 'Standard', 0.0022411701338458821),
 ('state', 'Ohio', 0.0037587658190299409),
 ('ProductGroupDesc', 'Track Excavators', 0.0067688906745931197),
 ('ProductGroup', 'TEX', 0.014654732626326661),
 ('MachineID', 2300944, 0.015578052196894499),
 ('Hydraulics_Flow', nan, 0.028973749866174004),
 ('ModelID', 665, 0.038307429579276284),
 ('Coupler_System', nan, 0.052509808150765114),
 ('YearMade', 1999, 0.071829996446492878)]
'''

所以小型工业设备意味着它更便宜。如果它是最近制造的,那就意味着更昂贵,等等。所以这实际上对 Kaggle 不会有太大帮助,因为你只需要预测。但在生产环境甚至是预生产阶段,这将对你有很大帮助。所以任何一个好的经理应该做的事情是,如果你说这里有一个机器学习模型,我认为我们应该使用它,他们应该离开并抓取一些实际客户或实际拍卖的例子,检查你的模型是否看起来直观。如果它说我的预测是很多人会真的喜欢这部糟糕的电影,而实际上是“哇,那是一部真的糟糕的电影”,那么他们会回来问你“解释一下为什么你的模型告诉我我会喜欢这部电影,因为我讨厌那部电影”。然后你可以回答说,这是因为你喜欢这部电影,因为你是这个年龄段,你是这个性别,平均而言,实际上像你这样的人确实喜欢那部电影。

问题:每个元组的第二个元素是什么[47:25]?这是说对于这一行,'ProductSize'是'Mini',它已经 11 岁了,等等。所以它只是反馈并告诉你。因为这实际上就是它的样子:

array([[4364751, 2300944, 665, 172, 1.0, 1999, 3726.0, 3, 3232, 1111, 0, 63, 0, 5, 17, 35, 4, 4, 0, 1, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 3, 0, 0, 0, 2, 19, 29, 3, 2, 1, 0, 0, 0, 0, 0, 2010, 9, 37,
        16, 3, 259, False, False, False, False, False, False, 7912, False, False]], dtype=object)

就是这些数字。所以我只是回到原始数据中实际提取出每个描述性版本。

所以如果我们把所有的贡献加在一起,然后加到偏差中,那将给我们最终的预测。

contributions[0].sum()*-0.7383536391949419*

这是一个几乎完全未知的技术,这个特定的库也几乎完全未知。所以这是一个展示很多人不知道的东西的绝佳机会。在我看来,这是非常关键的,但很少有人这样做。

所以这基本上是随机森林解释部分的结束,希望你现在已经看到足够多,当有人说我们不能使用现代机器学习技术,因为它们是不可解释的黑匣子时,你有足够的信息来说你是在胡说。它们是非常可解释的,我们刚刚做的事情——试图用线性模型做到这一点,祝你好运。即使你可以用线性模型做类似的事情,试图做到不给你完全错误答案并且你不知道它是错误答案将是一个真正的挑战。

外推[49:23]

在我们尝试构建自己的随机森林之前,我们要做的最后一步是处理这个棘手的外推问题。所以在这种情况下,如果我们看一下我们最近树的准确性,我们仍然在我们的验证分数和训练分数之间有很大的差异。

实际上,在这种情况下,OOB(0.89420)和验证(0.89319)之间的差异实际上非常接近。所以如果有很大的差异,我会非常担心我们是否正确处理了时间方面的问题。这是最近的模型:

在 Kaggle 上,你需要那个最后的小数点。在现实世界中,我可能会停在这里。但很多时候你会看到你的验证分数和 OOB 分数之间有很大的差异,我想向你展示如何处理这个问题,特别是因为我们知道 OOB 分数应该稍微差一点,因为它使用的树较少,所以这让我感觉我们应该做得更好一点。我们应该能够做得更好一点的方法是更好地处理时间组件。

当涉及外推时,随机森林存在问题。当你有一个包含四年销售数据的数据集时,你创建了你的树,它说如果它在某个特定的商店和某个特定的物品上特价,这里是平均价格。它实际上告诉我们整个训练集上的平均价格,这可能相当古老。所以当你想要向前迈进到下个月的价格时,它从未见过下个月。而线性模型可以找到时间和价格之间的关系,即使我们只有这么多数据,当你预测未来的某事时,它可以外推。但是随机森林做不到这一点。如果你考虑一下,树无法说下个月价格会更高。所以有几种处理这个问题的方法,我们将在接下来的几堂课中讨论,但一个简单的方法就是尽量避免使用时间变量作为预测因子,如果有其他东西可以使用,可以给我们更好或更强的关系,那实际上会在未来起作用[52:19]。

因此,在这种情况下,我想要做的第一件事是弄清楚我们的验证集和训练集之间的差异。如果我了解我们的验证集和训练集之间的差异,那么这告诉我哪些预测变量具有强烈的时间成分,因此到了未来时间段可能是无关紧要的。因此,我做了一些非常有趣的事情,我创建了一个随机森林,其中我的因变量是“是否在验证集中”(is_valid)。我回去拿到了整个数据框,包括训练和验证全部在一起,然后我创建了一个新列叫做is_valid,我将其设置为 1,然后对于所有在训练集中的东西,我将其设置为 0。因此,我有了一个新列,只是这个是否在验证集中,然后我将其用作我的因变量并构建一个随机森林。这是一个随机森林,不是为了预测价格,而是为了预测这是否在验证集中。因此,如果您的变量不是时间相关的,那么应该不可能弄清楚某样东西是否在验证集中。

df_ext = df_keep.copy()
df_ext['is_valid'] = 1
df_ext.is_valid[:n_trn] = 0
x, y, nas = proc_df(df_ext, 'is_valid')

这是 Kaggle 中的一个很棒的技巧,因为他们通常不会告诉您测试集是否是随机样本。因此,您可以将测试集和训练集放在一起,创建一个新列叫做is_test,看看您是否可以预测它。如果可以,那么您就没有一个随机样本,这意味着您必须弄清楚如何从中创建一个验证集。

m = RandomForestClassifier(
      n_estimators=40, 
      min_samples_leaf=3, 
      max_features=0.5, 
      n_jobs=-1, 
      oob_score=True
)
m.fit(x, y);
m.oob_score_
'''
0.99998753505765037
'''

在这种情况下,我可以看到我没有一个随机样本,因为我的验证集可以用 0.9999 的 R²来预测。

因此,如果我查看特征重要性,最重要的是SalesID[54:36]。这非常有趣。它清楚地告诉我们SalesID不是一个随机标识符,而可能是随着时间的推移而连续设置的某些东西——我们只是增加SalesIDsaleElapsed是自我们数据集中第一个日期以来的天数,因此不足为奇,它也是一个很好的预测变量。有趣的是MachineID——显然每台机器也被标记为一些连续的标识符,然后重要性大幅下降,所以我们就到此为止。

fi = rf_feat_importance(m, x); fi[:10] 

接下来让我们获取前三个,然后我们可以查看它们在训练集和验证集中的值。[55:22]

feats='SalesID', 'saleElapsed', 'MachineID'.describe()

(X_valid[feats]/1000).describe()

例如,我们可以看到,销售 ID 在训练集中平均为 180 万,在验证集中为 580 万(请注意该值已除以 1000)。因此,您可以确认它们是非常不同的。

所以让我们把它们删除。

x.drop(feats, axis=1, inplace=True)
m = RandomForestClassifier(
   n_estimators=40, 
   min_samples_leaf=3, 
   max_features=0.5, 
   n_jobs=-1, 
   oob_score=True
)
m.fit(x, y);
m.oob_score_
'''
0.9789018385789966
'''

所以在我删除它们之后,现在让我们看看我是否可以预测某样东西是否在验证集中。我仍然可以用 0.98 的 R²来预测。

fi = rf_feat_importance(m, x); fi[:10]

一旦您删除了一些东西,其他东西就会浮现出来,现在显然老年——年龄较大的东西更有可能在验证集中,因为在训练集中的早期阶段,它们还不可能那么老。YearMade 也是同样的原因。因此,我们也可以尝试删除这些——从第一个中删除SalesIDsaleElapsedMachineID,从第二个中删除ageYearMadesaleDayofyear。它们都是与时间有关的特征。如果它们很重要,我仍然希望它们出现在我的随机森林中。但如果它们不重要,那么如果有其他一些非时间相关的变量效果一样好——那将更好。因为现在我将拥有一个更好地泛化时间的模型。

set_rf_samples(50000)
feats=['SalesID', 'saleElapsed', 'MachineID', 'age', 'YearMade', 'saleDayofyear']X_train, X_valid = split_vals(df_keep, n_trn)
m = RandomForestRegressor(
   n_estimators=40, 
   min_samples_leaf=3, 
   max_features=0.5, 
   n_jobs=-1, 
   oob_score=True
)
m.fit(X_train, y_train)
print_score(m)
'''
[0.21136509778791376, 0.2493668921196425, 0.90909393040946562, 0.88894821098056087, 0.89255408392415925]
'''

所以在这里,我将逐个查看这些特征并逐个删除,重新训练一个新的随机森林,并打印出分数。在我们做任何这些之前,我们的验证分数是 0.88,OOB 是 0.89。你可以看到,当我删除 SalesID 时,我的分数上升了。这正是我们所希望的。我们删除了一个时间相关的变量,还有其他变量可以找到类似的关系而不依赖于时间。因此,删除它导致我们的验证分数上升。现在 OOB 没有上升,因为这实际上在统计上是一个有用的预测变量,但它是一个时间相关的变量,我们有一个时间相关的验证集。这是非常微妙的,但它可能非常重要。它试图找到能够提供跨时间泛化预测的因素,这里是你可以看到的方式。

for f in feats:
    df_subs = df_keep.drop(f, axis=1)
    X_train, X_valid = split_vals(df_subs, n_trn)
    m = RandomForestRegressor(
         n_estimators=40, 
         min_samples_leaf=3, 
         max_features=0.5, 
         n_jobs=-1, 
         oob_score=True
    )
    m.fit(X_train, y_train)
    print(f)
    print_score(m)
SalesID 
'''
[0.20918653475938534, 0.2459966629213187, 0.9053273181678706, 0.89192968797265737, 0.89245205174299469]
'''
saleElapsed 
'''
[0.2194124612957369, 0.2546442621643524, 0.90358104739129086, 0.8841980790762114, 0.88681881032219145]
'''
MachineID
'''
[0.206612984511148, 0.24446409479358033, 0.90312476862123559, 0.89327205732490311, 0.89501553584754967]
'''
age
'''
[0.21317740718919814, 0.2471719147150774, 0.90260198977488226, 0.89089460707372525, 0.89185129799503315]
'''
YearMade
'''
[0.21305398932040326, 0.2534570148977216, 0.90555219348567462, 0.88527538596974953, 0.89158854973045432]
'''
saleDayofyear
'''
[0.21320711524847227, 0.24629839782893828, 0.90881970943169987, 0.89166441133215968, 0.89272793857941679]
'''

我们肯定应该删除SalesID,但saleElapsed没有变得更好,所以我们不想要。MachineID变得更好了-从 0.888 到 0.893,所以实际上好了很多。age有点变好了。YearMade变得更糟了,saleDayofyear有点变好了。

reset_rf_samples()

现在我们可以说,让我们摆脱那三个我们知道摆脱它实际上使它变得更好的东西。因此,我们现在达到了 0.915!所以我们摆脱了三个时间相关的因素,现在如我们所料,我们的验证比 OOB 更好。

df_subs = df_keep.drop(
   ['SalesID', 'MachineID', 'saleDayofyear'], 
   axis=1
)
X_train, X_valid = split_vals(df_subs, n_trn)
m = RandomForestRegressor(
   n_estimators=40, 
   min_samples_leaf=3, 
   max_features=0.5, 
   n_jobs=-1, 
   oob_score=True
)
m.fit(X_train, y_train)
print_score(m)
'''
[0.1418970082803121, 0.21779153679471935, 0.96040441863389681, 0.91529091848161925, 0.90918594039522138]
'''

所以那是一个非常成功的方法,现在我们可以检查特征的重要性。

plot_fi(rf_feat_importance(m, X_train));

np.save('tmp/subs_cols.npy', np.array(df_subs.columns))

让我们继续说好吧,那真是相当不错。现在让它静置一段时间,给它 160 棵树,让它消化一下,看看效果如何。

我们的最终模型!

m = RandomForestRegressor(
   n_estimators=160, 
   max_features=0.5, 
   n_jobs=-1, 
   oob_score=True
)
%time m.fit(X_train, y_train)
print_score(m)
'''
CPU times: user 6min 3s, sys: 2.75 s, total: 6min 6s
Wall time: 16.7 s
[0.08104912951128229, 0.2109679613161783, 0.9865755186304942, 0.92051576728916762, 0.9143700001430598]
'''

正如你所看到的,我们进行了所有的解释,所有的微调基本上都是用较小的模型/子集进行的,最后,我们运行了整个过程。实际上,这只花了 16 秒,所以我们现在的 RMSE 是 0.21。现在我们可以将其与 Kaggle 进行比较。不幸的是,这是一个较旧的比赛,我们不允许再参加,看看我们会取得怎样的成绩。所以我们能做的最好的就是检查它是否看起来我们可能会根据他们的验证集做得很好,所以应该在正确的范围内。根据这一点,我们本来会得第一名。

我认为这是一系列有趣的步骤。所以你可以在你的 Kaggle 项目和更重要的是你的现实世界项目中按照相同的步骤进行。其中一个挑战是一旦你离开这个学习环境,突然间你周围都是从来没有足够时间的人,他们总是希望你赶快,他们总是告诉你这样做然后那样做。你需要找到时间远离一下然后回来,因为这是一个你可以使用的真正的现实世界建模过程。当我说它提供了世界级的结果时,我的意思是真的。赢得这个比赛的人,Leustagos,不幸地去世了,但他是有史以来最顶尖的 Kaggle 竞争者。我相信他赢得了数十个比赛,所以如果我们能够得到一个甚至接近他的分数,那么我们做得真的很好。

澄清:这两者之间 R²的变化不仅仅是因为我们删除了这三个预测因子。我们还进行了reset_rf_samples()。因此,要真正看到仅仅删除的影响,我们需要将其与之前的最终步骤进行比较。

实际上与 0.907 的验证相比。因此,删除这三个因素使我们的分数从 0.907 提高到 0.915。最终,当然最重要的是我们的最终模型,但只是澄清一下。

从头开始编写随机森林!

笔记本

我的原始计划是实时进行,但当我开始做的时候,我意识到那样会很无聊,所以,我们可能会一起更多地走一遍代码。

实现随机森林算法实际上是相当棘手的,不是因为代码很棘手。一般来说,大多数随机森林算法在概念上都很容易。一般来说,学术论文和书籍往往让它们看起来很困难,但从概念上讲并不困难。困难的是把所有细节搞对,知道什么时候是对的。换句话说,我们需要一种好的测试方法。因此,如果我们要重新实现已经存在的东西,比如说我们想在一些不同的框架、不同的语言、不同的操作系统中创建一个随机森林,我总是从已经存在的东西开始。因此,在这种情况下,我们只是把它作为学习练习,用 Python 编写一个随机森林,因此为了测试,我将把它与现有的随机森林实现进行比较。

这是至关重要的。每当你在涉及机器学习中的非平凡量的代码时,知道你是对还是错是最困难的部分。我总是假设在每一步都搞砸了一切,所以我在想,好吧,假设我搞砸了,我怎么知道我搞砸了。然后令我惊讶的是,有时候我实际上做对了,然后我可以继续。但大多数时候,我做错了,所以不幸的是,对于机器学习来说,有很多方法可以让你出错,而不会给你错误。它们只会让你的结果稍微不那么好,这就是你想要发现的。

%load_ext autoreload
%autoreload 2
%matplotlib inline

from fastai.imports import *
from fastai.structured import *
from sklearn.ensemble import RandomForestRegressor, 
                             RandomForestClassifier
from IPython.display import display
from sklearn import metrics

因此,考虑到我想要将其与现有实现进行比较,我将使用我们现有的数据集,我们现有的验证集,然后为了简化事情,我只会从两列开始。因此,让我们继续开始编写一个随机森林。

PATH = "data/bulldozers/"

df_raw = pd.read_feather('tmp/bulldozers-raw')
df_trn, y_trn, nas = proc_df(df_raw, 'SalePrice')
def split_vals(a,n): 
   return a[:n], a[n:]
n_valid = 12000
n_trn = len(df_trn)-n_valid
X_train, X_valid = split_vals(df_trn, n_trn)
y_train, y_valid = split_vals(y_trn, n_trn)
raw_train, raw_valid = split_vals(df_raw, n_trn)
x_sub = X_train[['YearMade', 'MachineHoursCurrentMeter']]

我写代码的方式几乎都是自顶向下的,就像我的教学一样。因此,从顶部开始,我假设我想要的一切都已经存在。换句话说,我想要做的第一件事是,我将称之为树集合。要创建一个随机森林,我首先要问的问题是我需要传入什么。我需要初始化我的随机森林。我将需要:

  • x:一些自变量

  • y:一些因变量

  • n_trees:选择我想要的树的数量

  • sample_sz:我将从一开始使用样本大小参数,因此您希望每个样本有多大

  • min_leaf:然后可能是一些可选参数,表示最小叶子大小。

class TreeEnsemble():
    def __init__(self, x, y, n_trees, sample_sz, min_leaf=5):
        np.random.seed(42)
        self.x,self.y,self.sample_sz,self.min_leaf = x,y,sample_sz,min_leaf
        self.trees = [self.create_tree() for i in range(n_trees)]

    def create_tree(self):
        rnd_idxs = np.random.permutation(len(self.y))[:self.sample_sz]
        return DecisionTree(
            self.x.iloc[rnd_idxs], 
            self.y[rnd_idxs],
            min_leaf=self.min_leaf
         )

    def predict(self, x):
        return np.mean([t.predict(x) for t in self.trees], axis=0)

对于测试,最好使用一个固定的随机种子,这样每次都会得到相同的结果。np.random.seed(42)是设置随机种子的方法。也许值得一提的是,对于那些不熟悉的人来说,计算机上的随机数生成器根本不是随机的。它们实际上被称为伪随机数生成器,它们的作用是在给定一些初始起点(在这种情况下是 42)的情况下,生成一系列确定性(始终相同)的数字,这些数字被设计为:

  • 尽可能与前一个数字不相关

  • 尽可能不可预测

  • 尽可能与具有不同随机种子的东西不相关(因此以 42 开头的序列中的第二个数字应该与以 41 开头的序列中的第二个数字非常不同)

通常,它们涉及使用大素数,取模等等。这是一个有趣的数学领域。如果你想要真正的随机数,唯一的方法就是你可以购买一种叫做硬件随机数生成器的硬件,里面会有一点放射性物质,以及一些检测它输出了多少东西的东西,或者会有一些硬件设备。

问题:当前系统时间是否是有效的随机数生成器[1:09:25]?这可能是一个随机种子(我们用来启动函数的东西)。一个非常有趣的领域是,在您的计算机中,如果您没有设置随机种子,它会被设置为什么。通常,人们会使用当前时间来确保安全性 - 显然,我们在安全方面使用了很多随机数,比如如果您正在生成 SSH 密钥,它需要是随机的。事实证明,人们可以大致确定您创建密钥的时间。他们可以查看id_rsa的时间戳,然后尝试在该时间戳周围的所有不同纳秒起始点上尝试随机数生成器,并找出您的密钥。因此,在实践中,许多需要高度随机性的应用程序实际上都有一步说“请移动鼠标并在键盘上输入一段时间的随机内容”,这样就可以让您成为“熵”的来源。另一种方法是他们会查看一些日志文件的哈希值或类似的东西。这是一个非常有趣的领域。

在我们的情况下,我们的目的实际上是消除随机性[1:10:48]。所以我们说,好吧,生成一系列以 42 开始的伪随机数,所以它应该始终相同。

如果您在 Python 面向对象方面没有做过太多事情,这基本上是标准习语,至少我是这样写的,大多数人不这样写,但是如果您传入五个要保存在此对象内部的东西,那么您基本上必须说self.x = x,等等。我们可以从元组中赋值给元组。

这是我的编码方式。大多数人认为这很糟糕,但我更喜欢一次看到所有东西,这样我就知道在我的代码中,每当我看到类似这样的东西时,它总是在方法中设置的所有内容。如果我以不同的方式做,那么现在一半的代码会从页面底部消失,你就看不到了。

这是我考虑的第一件事 - 要创建一个随机森林,您需要哪些信息。然后我需要将该信息存储在我的对象内部,然后我需要创建一些树。随机森林是一些树的集合。因此,我基本上想到使用列表推导来创建一组树。我们有多少棵树?我们有n_trees棵树。这就是我们要求的。range(n_trees)给我从 0 到n_trees-1的数字。因此,如果我创建一个列表推导,循环遍历该范围,每次调用create_tree,我现在有了n_trees棵树。

为了写这个,我根本不用思考。这一切都是显而易见的。所以我把思考推迟到了这一点,就像好吧,我们没有东西来创建一棵树。好吧,没关系。但让我们假装我们有。如果我们有了,我们现在创建了一个随机森林。我们仍然需要在此基础上做一些事情。例如,一旦我们有了它,我们需要一个预测函数。好的,让我们写一个预测函数。在随机森林中如何进行预测?对于特定的行(或行),遍历每棵树,计算其预测。因此,这里是一个列表推导,它正在为x的每棵树计算预测。我不知道x是一行还是多行,这并不重要,只要tree.predict对其起作用。一旦你有了一系列东西,要知道的一个很酷的事情是你可以传递numpy.mean一个常规的非 numpy 列表,它将取平均值 - 你只需要告诉它axis=0表示跨列表平均。因此,这将返回每棵树的.predict()的平均值。

我发现列表推导允许我按照大脑的方式编写代码[1:14:24]。你可以将这些单词翻译成这段代码,或者你可以将这段代码翻译成单词。所以当我写代码时,我希望它尽可能像那样。我希望它是可读的,所以希望当你查看 fast.ai 代码时,试图理解 Jeremy 是如何做 x 的,我尽量以一种你可以阅读并在脑海中转化为英语的方式来写东西。

我们几乎已经完成了我们的随机森林的编写,不是吗[1:15:29]?现在我们只需要编写create_tree。我们将从数据的随机样本构建一个决策树(即非随机树)。所以再次,我们在这里延迟了任何实际的思考过程。我们基本上说好吧,我们可以选择一些随机 ID。这是一个很好的技巧要知道。如果你调用np.random.permutation传入一个int,它会给你一个从零到那个int的随机洗牌序列。所以如果你获取那个的前:n项,那现在就是一个随机子样本。所以这里不是在这里做引导法(即我们不是在进行有放回的抽样),我认为这是可以接受的。对于我的随机森林,我决定它将是一种我们进行子抽样而不是引导法的情况。

np.random.permutation(len(self.y))[:self.sample_sz]

所以这是一个很好的代码行,知道如何编写,因为它经常出现。我发现在机器学习中,我使用的大多数算法都有些随机,所以我经常需要某种随机样本。

就我个人而言,我更喜欢这种方式,而不是引导法,因为我觉得大多数时候,我们拥有的数据比我们一次想要放入树中的数据要多[1:18:54]。当 Breiman 创建随机森林时,是 1999 年,那是一个非常不同的世界。现在我们有太多的数据。所以人们倾向于启动一个 Spark 集群,当他们没有意义时,他们会在数百台机器上运行它,因为如果他们每次只使用一个子样本,他们可以在一台机器上完成。Spark 的开销是巨大的 I/O 开销。如果你在单台机器上做某事,通常会快上数百倍,因为你没有这种 I/O 开销,而且编写算法也更容易,可视化更容易,更便宜等等。所以我几乎总是避免分布式计算,我一生都是这样。即使在 25 年前我开始学习机器学习时,我也没有使用集群,因为我总觉得无论我现在用集群能做什么,五年后我都可以用一台机器做到。所以为什么不专注于始终尽可能地在一台机器上做得更好呢。这将更具互动性和迭代性。

所以再次,我们延迟思考到必须编写决策树的时候[1:20:26]。希望你能明白这种自顶向下的方法,目标是我们将不断延迟思考,直到最终我们不知不觉地写完整个东西而不必实际思考。请注意,你永远不必设计任何东西。你只需说,如果有人已经给我提供了我需要的确切 API,我该如何使用它?然后实现下一个阶段,我需要实现的确切 API 是什么?你继续下去,直到最终你注意到哦,这已经存在了。

这假设我们有一个名为DecisionTree的类,所以我们将不得不创建它。我们知道我们将不得不传递什么,因为我们刚刚传递了它。所以我们传递了xy的随机样本。我们知道决策树将包含决策树,这些决策树本身包含决策树。因此,当我们沿着决策树向下走时,原始数据的某个子集将被包含在内,因此我将传递我们实际上将在这里使用的数据的索引。所以最初,它是整个随机样本。我们还传递min_leaf的大小。因此,我们为构建随机森林所做的一切,我们将传递给决策树,除了当然不包括对于决策树无关的num_tree

class DecisionTree():
   def __init__(self, x, y, idxs=None, min_leaf=5):
      if idxs is None: 
         idxs=np.arange(len(y))
      self.x,self.y,self.idxs,self.min_leaf = x,y,idxs,min_leaf
      self.n,self.c = len(idxs), x.shape[1]
      self.val = np.mean(y[idxs])
      self.score = float('inf')
      self.find_varsplit()

   # This just does one decision; we'll make it recursive later
   def find_varsplit(self):
      for i in range(self.c): self.find_better_split(i)

   # We'll write this later!
   def find_better_split(self, var_idx): 
      pass

   @property
   def split_name(self): 
      return self.x.columns[self.var_idx]

   @property
   def split_col(self): 
      return self.x.values[self.idxs,self.var_idx] 
      
   @property
   def is_leaf(self): 
      return self.score == float('inf')

   def __repr__(self):
      s = f'n: {self.n}; val:{self.val}'
      if not self.is_leaf:
         s += f'; score:{self.score}; split:{self.split}; var:{self.split_name}'
      return s
  • self.n: 这棵树中有多少行(我们给定的索引数量)

  • self.c: 我们有多少列(独立变量中有多少列)

  • self.val: 对于这棵树,它的预测是什么。这棵树的预测是我们依赖变量的均值。当我们谈论索引时,我们并不是在谈论用于创建树的随机抽样。我们假设这棵树现在有一些随机样本。在决策树内部,整个随机抽样的过程已经消失了。那是由随机森林完成的。所以在这一点上,我们正在构建的只是一棵普通的决策树。它不以任何方式是随机抽样的任何东西。所以索引实际上是我们到目前为止在这棵树中得到的数据的哪个子集。

一个快速的面向对象编程入门

我会跳过这部分,但这里有关于self的有趣内容:

你可以随意命名它。如果你把它命名为除了“self”之外的任何其他名称,每个人都会讨厌你,你是个坏人。

机器学习 1:第 6 课

原文:medium.com/@hiromi_suenaga/machine-learning-1-lesson-6-14bbb8180d49

译者:飞龙

协议:CC BY-NC-SA 4.0

来自机器学习课程的个人笔记。随着我继续复习课程以“真正”理解它,这些笔记将继续更新和改进。非常感谢 JeremyRachel 给了我这个学习的机会。

视频 / 幻灯片

我们已经看过很多不同的随机森林解释技术,论坛上有一些问题是这些到底有什么用?它们如何帮助我在 Kaggle 上获得更好的分数,我的答案是“它们不一定会”。因此,我想更多地谈谈为什么我们要做机器学习。这有什么意义?为了回答这个问题,我想向你展示一些非常重要的东西,即人们主要在商业中如何使用机器学习的例子,因为这是你们大多数人在结束后可能会在某家公司工作。我将向你展示机器学习的应用,这些应用要么基于我自己亲身参与的事情,要么是我知道直接在做这些事情的人,因此这些都不是假设的——这些都是人们正在做的实际事情,我有直接或间接的了解。

两组应用[1:26]

  • 水平:在商业中,水平意味着跨不同类型的业务进行的事情。即涉及营销的所有事情。

  • 垂直:在企业内部或供应链或流程中进行的某些事情。

水平应用

几乎每家公司都必须尝试向其客户销售更多产品,因此进行营销。因此,这些框中的每一个都是人们在营销中使用机器学习的一些示例:

让我们举个例子——流失。流失是指试图预测谁会离开的模型。最近在电信领域做了一些流失建模。我们试图弄清楚这家大型手机公司的哪些客户会离开。这本身并不那么有趣。构建一个高度预测性的模型,说 Jeremy Howard 几乎肯定会在下个月离开,可能并不那么有帮助,因为如果我几乎肯定会在下个月离开,你可能无法做任何事情——为了留住我,成本可能太高。

因此,为了理解我们为什么要进行流失建模,我有一个可能对你有帮助的小框架:设计出色的数据产品。几年前我和几位同事一起写了这篇文章,在其中,我描述了我将机器学习模型转化为赚钱的东西的经验。基本技巧是我称之为驱动器方法,这是这四个步骤:

定义目标[3:48]

将机器学习项目转化为实际有用的起点是知道我试图实现什么,这意味着我试图实现高 ROC 曲线下面积或尝试实现类之间的巨大差异。这可能是我试图销售更多的书,或者我试图减少下个月离开的客户数量,或者我试图更早地检测肺癌。这些都是目标。因此,目标是公司或组织实际想要的东西。没有公司或组织是为了创建更准确的预测模型而存在的。这是有原因的。所以这就是你的目标。显然,这是最重要的事情。如果你不知道你为何建模,那么你不可能做好这项工作。希望人们开始在数据科学领域意识到这一点,但有趣的是,很少有人谈论但同样重要的是下一步,即杠杆。

杠杆[5:04]

杠杆是组织可以实际采取的行动,以推动目标的实现。所以让我们以流失建模为例。组织可以采取什么杠杆来减少离开的客户数量?他们可以打电话给某人,问:“你满意吗?我们能做些什么?”他们可以在下个月购买价值 20 美元的产品时赠送免费的钢笔或其他物品。你可以给他们提供特别优惠。所以这些就是杠杆。当你作为数据科学家工作时,不断回头思考我们试图实现什么(我们指的是组织),以及我们如何实现它,即我们可以做哪些实际的事情来实现这个目标。因此,构建模型绝对不是杠杆,但它可以帮助你使用杠杆。

数据[7:01]

接下来的步骤是组织拥有哪些数据可能帮助他们设置杠杆以实现目标。这不是指他们在项目开始时给你的数据。而是从第一原则的角度考虑——好吧,我在一家电信公司工作,他们给了我一些特定的数据,但我肯定他们必须知道他们的客户住在哪里,上个月打了多少电话,打了多少次客服电话等等。所以想一想,如果我们试图决定主动给谁提供特别优惠,那么我们想要弄清楚我们有哪些信息可能帮助我们确定谁会对此做出积极或消极的反应。也许更有趣的是,如果我们正在进行欺诈算法。我们试图弄清楚谁不会支付他们从商店拿出的手机,他们正在进行某种 12 个月的付款计划,然后我们再也没有见到他们。在这种情况下,我们可以获得的数据,数据库中有什么并不重要,重要的是当客户在商店时我们可以获得什么数据。因此,我们通常会受到我们实际可以使用的数据的限制。因此,我们需要知道我试图实现什么目标,这个组织实际上可以具体做些什么来改变结果,以及在做出决定时,他们拥有或可以收集到哪些数据。

模型[8:45]

然后我把所有这些放在一起的方式是通过一个模型。这不是一个预测模型,而是一个模拟模型。我在这篇论文中给出的一个主要例子是,我花了很多年时间建立的一个模型,即如果一个保险公司改变他们的价格,这将如何影响他们的盈利能力。通常你的模拟模型包含了许多预测模型。比如,我有一个叫做弹性模型的预测模型,它说对于一个特定的客户,如果我们为他们的某个产品收取一个特定的价格,他们会在新业务时和一年后续保的概率是多少。然后还有另一个预测模型,即他们会提出索赔的概率以及索赔金额是多少。然后你可以将这些模型结合起来,然后说好,如果我们将我们的定价降低 10%适用于 18 到 25 岁的所有人,然后我们可以通过这些模型运行,将它们结合成一个模拟,那么我们在 10 年后的市场份额的整体影响是 X,我们的成本是 Y,我们的利润是 Z 等等。

实际上,大多数时候,你真的更关心那个模拟的结果,而不是直接关心预测模型。但大多数人目前并没有有效地做到这一点。例如,当我去亚马逊时,我读了道格拉斯·亚当斯的所有书,所以在我读完所有道格拉斯·亚当斯的书之后,下次我去亚马逊,他们说你想买道格拉斯·亚当斯的全部作品吗。这是在我已经买了他的每一本书之后。从机器学习的角度来看,一些数据科学家可能会说,购买道格拉斯·亚当斯的一本书的人通常会继续购买他的全部作品。但向我推荐购买道格拉斯·亚当斯的全部作品并不明智。这实际上在很多方面都不明智。不仅是因为我不太可能购买一个我已经有每一本书的合集,而且这也不会改变我的购买行为。我已经了解道格拉斯·亚当斯,我已经知道我喜欢他,所以占用你宝贵的网页空间来告诉我,嘿,也许你应该购买更多你已经熟悉并多次购买的作者的作品实际上不会改变我的行为。那么,如果亚马逊不是创建一个预测模型,而是建立一个能够模拟的优化模型,然后说如果我们向杰里米展示这个广告,他会有多大可能继续购买这本书,如果我不向他展示这个广告,他会有多大可能继续购买这本书。这就是对立事实。对立事实是否则会发生什么,然后你可以计算差异,然后说我们应该推荐他什么才能最大程度地改变他的行为。所以最大程度地导致更多的书籍,所以你可能会说,哦,他从来没有买过特里·普拉切特的书,他可能不了解特里·普拉切特,但很多喜欢道格拉斯·亚当斯的人确实喜欢特里·普拉切特,所以让我们向他介绍一个新的作者。

因此,一方面是预测模型,另一方面是优化模型之间的区别。所以这两者往往是相辅相成的。首先,我们有一个模拟模型。模拟模型是在说,如果我们把特里·普拉切特的书放在亚马逊的首页上给杰里米·霍华德看,会发生什么。他有 94%的概率会购买。这告诉我们,通过这个杠杆,我今天应该在杰里米的首页上放什么,我们说好,把特里·普拉切特放在首页上的不同设置会产生最高的模拟结果。然后这就是最大化我们从杰里米今天访问亚马逊网站中的利润的事情。

一般来说,你的预测模型会输入到这个模拟模型中,但你必须考虑它们如何共同工作。例如,让我们回到流失问题。结果表明,Jeremy Howard 很可能会在下个月离开他的手机公司。我们该怎么办?让我们给他打电话。我可以告诉你,如果我的手机公司现在给我打电话说“只是打电话告诉你我们爱你”,我会立刻取消。那将是一个糟糕的主意。因此,你会想要一个模拟模型,来说 Jeremy 现在接到电话后改变行为的概率是多少。所以我有一个杠杆是给他打电话。另一方面,如果明天我收到一封信,说每个月你和我们在一起,我们会给你十万美元。那肯定会改变我的行为,对吧?但是将这个输入到模拟模型中,结果是这将是一个不盈利的选择。你看到这是如何相互配合的吗?

所以当我们看流失这样的问题时,我们要考虑我们可以拉动的杠杆是什么。我们可以用什么样的数据构建什么样的模型来帮助我们更好地拉动这些杠杆以实现我们的目标。当你这样思考时,你会意识到这些应用的绝大部分实际上并不是关于预测模型。它们是关于解释的。它们是关于理解“如果发生了什么”。因此,我们可以实际使用我们的随机森林特征重要性告诉我们我们实际上可以做些什么来产生影响。然后我们可以使用部分依赖来构建这种模拟模型,来说如果我们改变了那个,会发生什么。

所以有很多例子,当你思考你正在处理的机器学习问题时,我希望你考虑为什么有人会关心这个问题。对他们来说一个好的答案是什么样的,你如何实际上对这个业务产生积极影响。所以如果你在创建一个 Kaggle 内核,试着从竞赛组织者的角度思考。他们想知道什么,你如何给他们这些信息。另一方面,像欺诈检测,你可能只是想知道谁是欺诈的。所以你可能只关心预测模型。但是你必须仔细考虑这里的数据可用性。所以好吧,我们需要知道在我们即将向他们交付产品时谁是欺诈的。例如,查看一个月后可用的数据是没有意义的。所以你必须考虑你正在工作的实际运营约束。

人力资源应用。

在人力资源领域有很多有趣的应用,比如员工流失,这是另一种流失模型,其中发现杰里米·霍华德已经厌倦了讲课,他明天就要离开了。你会怎么做?知道这个事实实际上并不会有帮助。那将会太迟了。你实际上想要一个模型,告诉你什么样的人会离开 USF,结果发现每个去楼下咖啡厅的人都会离开 USF。我猜他们的食物很糟糕或者其他什么原因。或者我们支付不到 50 万美元一年的人都会离开 USF,因为他们无法负担旧金山的基本住房。因此,你可以使用员工流失模型,不是为了知道哪些员工讨厌我们,而是为什么员工离开。再次强调,真正重要的是解释。

问题:对于流失模型,听起来你需要预测两个预测因子——一个是流失,另一个是你需要优化你的利润。那么它是如何工作的[18:30]?是的,确切地说,这就是模拟模型的全部内容。你找出我们试图最大化的目标,即公司的盈利能力。你可以创建一个相当简单的 Excel 模型或其他模型,它说这是收入,这是成本,成本等于我们雇佣的人数乘以他们的工资等。在这个 Excel 模型中,有一些单元格/输入是随机的或不确定的。但我们可以用模型来预测,这就是我要做的,我要说好,我们需要一个预测模型,来预测如果我们改变他们的工资,某人留下的可能性有多大,如果我现在增加他们的工资,明年他们离开的可能性有多大等。因此,你需要一堆不同的模型,然后你可以用简单的商业逻辑将它们绑定在一起,然后进行优化。然后你可以说,如果我给杰里米·霍华德 50 万美元,那可能是一个非常好的主意,如果我付给他更少,那可能就不是了,或者其他什么。你可以找出整体影响。所以我真的很惊讶,为什么这么少的人这样做。但大多数行业中的人用 AUC 或 RMSE 等来衡量他们的模型,这实际上并不是你真正想要的。

更多水平应用...[22:04]

潜在客户优先级是一个非常有趣的领域。我展示的每一个方框,通常都可以找到一家或多家公司,他们的唯一工作就是构建该领域的模型。因此,有很多公司销售潜在客户优先级系统,但问题是我们如何利用这些信息。如果我们的最佳潜在客户是杰里米,他是最有可能购买的人。这意味着我应该派一个销售人员去找杰里米,还是不应该?如果他很有可能购买,为什么我要浪费时间呢。因此,你真的需要一种模拟,来告诉你如果我派出最好的销售人员去鼓励他签约,杰里米的行为可能会发生什么变化。我认为今天世界上有很多机会让数据科学家不仅仅局限于预测建模,而是将所有内容整合在一起。

垂直应用[23:29]

除了这些基本适用于每家公司的横向应用之外,还有许多应用程序是针对世界各地的每个部分特定的。对于那些最终进入医疗保健领域的人,你们中的一些人将成为这些领域的专家之一。比如再入院风险。那么这位患者再次入院的概率是多少呢?根据司法管辖区的细节,当有人再次入院时,这可能对医院造成灾难。如果你发现这位患者有高再入院的可能性,你会怎么做?再次,预测模型本身是有帮助的。它更多地暗示我们不应该立即让他们回家,因为他们会再次入院。但如果我们有树解释器,并且它告诉我们,他们高风险的原因是因为我们没有最近的心电图。没有最近的心电图,我们就无法对他们的心脏健康有高度的信心。在这种情况下,我们不会说让他们在医院呆两周,而是让他们做一个心电图。因此,这是解释和预测准确性之间的互动。

问题:所以我理解你的意思是,预测模型确实很棒,但为了真正回答这些问题,我们确实需要专注于这些模型的可解释性?是的,我想是这样。更具体地说,我正在说我们刚刚学习了一整套随机森林解释技术,所以我正在试图证明为什么。原因是因为我会说大多数时候解释是我们关心的事情。你可以创建一个图表或表格而不需要机器学习,实际上这就是大多数世界的工作方式。大多数经理们在没有任何机器学习的情况下构建各种表格和图表。但他们经常做出糟糕的决定,因为他们不知道他们感兴趣的目标的特征重要性,所以他们创建的表格实际上是那些最不重要的东西。或者他们只是做一个单变量图表,而不是一个部分依赖图,所以他们实际上没有意识到他们认为自己在看的关系完全是由其他因素造成的。所以我在争论数据科学家应该更深入地参与战略,并尝试使用机器学习来真正帮助企业实现所有目标。有一些公司像 dunnhumby 这样的公司,他们什么都不做,只做零售应用的机器学习。我相信有一种 dunnhumby 产品可以帮助你弄清楚,如果我把我的新店放在这个位置而不是那个位置,有多少人会在那里购物。或者如果我把尿布放在商店的这个部分而不是那个部分,这将如何影响购买行为等等。因此,也很重要意识到,在技术媒体或其他地方你经常听到的机器学习应用的子集是这种极其偏见的微小子集,谷歌和 Facebook 做的就是这种。而实际上让世界运转的绝大部分应用是这些实际上帮助人们制造东西、购买东西、销售东西、建造东西等等的应用。

问题:关于树的解释,我们看了哪个特征对于特定观察结果更重要。对于企业来说,他们有大量数据,他们希望对很多观察结果进行这种解释,那么他们如何自动化呢?他们设置阈值吗[27:50]?绝大多数机器学习模型并不自动化任何东西。它们被设计为向人类提供信息。所以例如,如果你是一个保险公司的客服电话操作员,你的客户问你为什么我的续保费比上次贵了 500 美元,那么希望保险公司在你的终端提供那些显示树解释结果的小屏幕。这样你就可以跳过去告诉客户,去年你住在一个车辆被盗率较低的邮政编码区,而今年你还把车换成了更贵的车。所以这并不是关于阈值和自动化,而是关于让这些模型输出对组织中的决策者可用,无论是在顶层战略层面,比如我们是否要关闭整个产品,还是在操作层面,比如与客户进行个别讨论。

另一个例子是飞机调度和登机口管理。有很多公司在做这个,基本上是有人在机场,他们的工作是告诉每架飞机去哪个登机口,什么时候关闭舱门等等。所以这个想法是给他们一个软件,里面有他们需要做出良好决策所需的信息。所以机器学习模型最终嵌入在那个软件中,比如说,那架目前从迈阿密飞来的飞机,有 48%的概率会晚 5 分钟以上,如果晚了,那么整个航站楼会受到影响,例如。这就是这些东西是如何结合在一起的。

其他应用[31:02]

有很多应用,我希望你花一些时间去思考它们。和你的朋友坐下来,谈论一些例子。比如说,我们如何进行制造业的故障分析,谁会做这个,为什么会做这个,他们可能会使用什么样的模型,可能会使用什么样的数据。开始练习并获得感觉。然后当你在工作场所和经理们交谈时,你希望能立即认识到你正在交谈的人——他们想要实现什么,他们有哪些杠杆可以拉动,他们有哪些数据可用来拉动这些杠杆以实现那个目标,因此我们如何构建模型来帮助他们做到这一点,他们可能需要做出什么样的预测。这样你就可以与这些人进行深思熟虑的共情对话,然后说“为了减少离开的客户数量,我猜你正在努力找出应该给谁提供更好的定价”等等。

问题:社会科学中人们面临的解释问题是否可以使用机器学习或者已经被使用,或者这并不是真正的领域[32:29]?我与社会科学领域的人们进行了很多关于这个问题的讨论,目前机器学习在经济学或心理学等领域并没有得到很好的应用。但我相信它可以,原因正如我们所讨论的那样。因此,如果您要尝试进行某种行为经济学研究,并且试图理解为什么有些人的行为与其他人不同,使用具有特征重要性图的随机森林将是一个很好的开始。更有趣的是,如果您尝试进行某种基于大型社交网络数据集的社会学实验或分析,在那里您进行了一项观察性研究,您真的想要尝试提取所有外生变量的来源(即所有外部发生的事情),因此如果您使用具有随机森林的部分依赖图,这将自动发生。几年前,我在麻省理工学院做了一个关于数字实验的第一次会议的演讲,这次会议真正讨论了我们如何在诸如社交网络等数字环境中进行实验,经济学家们都使用经典的统计检验方法,但在这种情况下,我与之交谈的经济学家们对此非常着迷,他们实际上要求我在麻省理工学院为经济学系的各种教员和研究生们举办一个机器学习入门课程。其中一些人已经写了一些相当有名的书籍,希望这对他们有所帮助。现在还处于早期阶段,但这是一个巨大的机会。但正如 Yannet 所说,仍然存在很多怀疑。这种怀疑主要来自对这种完全不同方法的陌生感。因此,如果您花了 20 年时间研究计量经济学,然后有人过来说这是一种完全不同于计量经济学家所做的所有事情的方法,那么您的第一反应自然会是“证明它”。这是公平的,但我认为随着时间的推移,下一代与机器学习一起成长的人们中,一些人将进入社会科学领域,他们将产生前所未有的巨大影响,人们将开始感到惊讶。就像计算机视觉中发生的一样。当计算机视觉花了很长时间的人们说“也许你应该使用深度学习来进行计算机视觉”,而计算机视觉领域的每个人都说“证明它。我们在计算机视觉中有几十年的工作,开发了令人惊叹的特征检测器。”然后在 2012 年,辛顿和克里赞斯基出现了,他们说“我们的模型比你们的好两倍,而我们刚刚开始” ,每个人都被说服了。如今,几乎每个计算机视觉研究人员基本上都使用深度学习。因此,我认为在这个领域也会出现这样的时刻。

随机森林解释方法[37:17]

在谈论它们为什么重要之后,让我们现在提醒自己它们是什么。

基于树方差的置信度

这告诉我们什么?为什么我们对此感兴趣?它是如何计算的?

树的预测方差。通常预测只是平均值,这是树的方差。

在这里填充一个细节,我们通常只取一行/观察结果,然后找出我们对此有多自信(即树中有多少方差)或者我们可以像我们在这里做的那样为不同的组找出答案[39:34]。

我们在这里所做的是看是否有任何我们非常不确定的组(这可能是由于观察很少)。我认为更重要的是当你在操作中使用这个时。比如说你正在做一个信用决策算法。所以我们正在确定 Jeremy 是一个好风险还是一个坏风险。我们应该借给他一百万美元吗。随机森林说“我认为他是一个好风险,但我一点也不自信。” 在这种情况下,我们可能会说好吧,也许我不应该给他一百万美元。而在另一种情况下,如果随机森林说“我认为他是一个好风险,而且我非常确定”,那么我们就更愿意给他一百万美元。而且我是一个非常好的风险。所以请随意给我一百万美元。我之前检查过随机森林——在另一个笔记本中。不在 repo 里 😆

对于我来说,很难给你们直接的经验,因为这种单个观察解释实际上是你需要把它放到前线的那种东西。这不是你在 Kaggle 上可以真正使用的东西,而更像是如果你实际上在发布一些可能会花费很多钱的大决策的算法,你可能不太关心随机森林的平均预测,而更可能是你实际上关心平均值减去几个标准差(即最坏情况的预测)。也许有一个我们不确定的整个组,所以这是基于树方差的置信度。

特征重要性 [42:36]

学生: 基本上是为了找出哪些特征是重要的。你取每个特征,洗牌特征中的值,然后检查预测如何变化。如果非常不同,那意味着该特征实际上很重要;否则就不那么重要。

Jeremy: 那太棒了。一切都是完全正确的。有一些细节被略过了一点。还有其他人想要详细描述一下它是如何计算的吗?我们如何准确计算特定特征的重要性?

学生: 在构建随机森林模型后,你取每一列并随机洗牌它。然后运行一个预测并检查验证分数。如果在洗牌其中一列后变糟了,那意味着那列很重要,所以它具有更高的重要性。我不太确定我们如何量化特征的重要性。

Jeremy: 好的,很好。你知道我们如何量化特征的重要性吗?那是一个很好的描述。为了量化,我们可以计算 R²或某种得分的差异。所以假设我们有我们的因变量是价格,还有一堆独立变量,包括制造年份。我们使用所有这些来构建一个随机森林,这给我们我们的预测。然后我们可以比较得到 R²、RMSE,或者你对模型感兴趣的任何东西。

现在关键的是我不想重新训练整个随机森林。那太慢又无聊了,所以使用现有的随机森林。我如何找出年份的重要性呢?建议是,让我们随机洗牌整个列。现在那一列完全没用了。它的均值、分布都是一样的。关于它的一切都是一样的,但实际年份制造和现在那一列之间根本没有联系。我已经随机洗牌了。所以现在我把这个新版本放到同一个随机森林中(所以没有重新训练),得到一些新的ŷ(ym)。然后我可以将其与实际值进行比较,得到 RMSE(ym)。所以现在我可以开始创建一个小表,我有原始 RMSE(例如为 3),年份制造混乱的 RMSE 为 2。围栏混乱的 RMSE 为 2.5。然后我只需要取这些差值。对于年份制造,重要性为 1,围栏为 0.5,等等。在我洗牌了那个变量之后,我的模型变得更糟了多少。

问题:所有的重要性加起来会等于一吗?老实说,我从来没有看过单位是什么,所以我不太确定。如果有人感兴趣,我们可以在这周内查看一下。看一下 sklearn 的代码,看看这些度量单位到底是什么,因为我从来没有费心去检查。虽然我不会专门检查度量单位,但我会检查相对重要性。这里有一个例子。

所以,与其只说前十名,昨天一个实习生问我一个特征重要性的问题,他们说“哦,我认为这三个很重要”,我指出排名第一的比第二个重要一千倍。所以看看这里的相对数字。所以在这种情况下,就像“不要看前三名,看那个重要一千倍的,忽略其他所有的。”你的自然倾向是想要准确和小心,但这就是你需要覆盖的地方,要非常实际。这个东西重要一千倍。不要花时间在其他任何事情上。然后你可以去和你的项目经理谈谈,告诉他这个东西重要一千倍。然后他们可能会说“哦,那是个错误。它不应该在那里。我们实际上在决策时没有那个信息,或者由于某种原因我们实际上不能使用那个变量。”那么你可以移除它并查看。或者他们可能会说“天哪,我完全不知道那比其他所有东西加起来都重要得多。所以让我们忘掉这个随机森林的东西,专注于理解如何更好地收集那个变量并更好地使用那个变量。”这是一个经常出现的情况,实际上昨天刚刚发生了另一个地方。另一个实习生问我“我正在做这个医学诊断项目,我的 R²是 0.95,这是一个据说很难诊断的疾病。这是随机森林天才还是出了什么问题?”我说记住,建立随机森林之后你要做的第二件事是进行特征重要性分析,所以进行特征重要性分析,你可能会发现排名第一的列是不应该在那里的。这就是发生的事情。他半小时后回到我这里,他说“是的,我做了特征重要性分析,你是对的。排名第一的列基本上是另一个对因变量的编码。我把它移除了,现在我的 R²是-0.1,这是一个改进。”

我喜欢看的另一件事是这个图表:

基本上它说的是在哪些方面趋于平缓,我应该真正关注哪些方面。这是最重要的。当我在电信行业进行信用评分时,我发现有九个变量基本上可以准确预测谁最终会支付他们的电话费,谁不会。除了最终得到一个每年节省三十亿美元欺诈和信用成本的模型外,它还让他们基本上重新调整了他们的流程,以便更好地收集这九个变量。

部分依赖 [50:46]

这是一个有趣的问题。非常重要,但在某种程度上有点难以理解。

让我们稍后再来看如何计算这个问题,但首先要意识到的是,绝大多数情况下,当有人向您展示一个图表时,它将是一个单变量图表,只会从数据库中获取数据,然后绘制 X 与 Y。然后管理人员往往希望做出决策。所以可能会是“哦,这里有一个下降,所以我们应该停止处理 1990 年至 1995 年之间制造的设备”。这是一个大问题,因为现实世界的数据中有很多这样的相互作用。也许在那些东西被出售的时候正值经济衰退,或者也许在那个时候,人们更多地购买了不同类型的设备。因此,通常我们实际上想知道的是,在其他所有条件相等的情况下,YearMade 和 SalePrice 之间的关系。因为如果您考虑到驱动器方法的杠杆思想,您真的希望一个模型说如果我改变这个杠杆,它将如何改变我的目标。通过使用部分依赖来分开它们,您可以说实际上这是 YearMade 和 SalePrice 之间的关系,在其他所有条件相等的情况下:

那么我们如何计算呢?

学生:例如,对于变量 YearMade,您保持所有其他变量不变。然后,您将传递 YearMade 的每个值,然后训练模型。因此,对于每个模型,您将有浅蓝色线条,中位数将是黄色线条。

Jeremy:那么让我们尝试绘制出来。通过“保持其他一切不变”,她的意思是将它们保持为数据集中的任何值。就像我们进行特征重要性时一样,我们将保持数据集的其余部分不变。我们将对 YearMade 进行部分依赖图。因此,我们有所有这些其他数据行,我们将保持它们不变。与其随机洗牌 YearMade,我们将用完全相同的东西——1960 来替换每个值。就像以前一样,我们现在将通过我们尚未重新训练或更改的现有随机森林来传递这些数据,以获得一组预测y1960。然后我们可以在图表上绘制出来——YearMade 与部分依赖。

现在我们可以为 1961 年、1962 年、1963 年等等做到这一点。我们可以对所有这些平均做到这一点,或者我们可以只对其中一个做到这一点。因此,当我们只对其中一个做到这一点,并且改变它的 YearMade 并将这个单个事物通过我们的模型,这给我们一个这些蓝线中的一个。因此,每一条这些蓝线都是一个单独的行,当我们将其 YearMade 从 1960 年改变到 2008 年。因此,我们可以简单地取所有这些蓝线的中位数,以便平均地说,YearMade 和价格之间的关系是什么,其他所有事物都相等。为什么它有效呢?为什么这个过程告诉我们 YearMade 和价格之间的关系,其他所有事物都相等呢?也许考虑一个非常简化的方法会有帮助。一个非常简化的方法会说什么是平均拍卖?什么是平均销售日期,我们最常见的机器类型是什么?我们主要在哪个地点销售物品?然后我们可以得出一个代表平均拍卖的单行,然后我们可以说,好的,让我们通过随机森林运行这一行,但用 1960 年替换它的 YearMade,然后再用 1961 年再做一次,我们可以在我们的小图表上绘制这些。这将给我们一个 YearMade 和销售价格之间关系的版本,其他所有事物都相等。但是如果拖拉机看起来像那样,而挖掘机看起来像一条平直的线:

然后取平均值会隐藏这些完全不同的关系的事实。因此,我们基本上说,好吧,我们的数据告诉我们我们倾向于销售什么样的东西,我们倾向于向谁销售,以及我们倾向于何时销售,所以让我们利用这一点。然后我们实际上发现,对于每一条蓝线,这里有这些关系的实际例子。因此,我们可以做的是除了绘制中位数之外,我们可以进行聚类分析,找出几种不同的形状。

在这种情况下,它们看起来基本上是同一件事的不同版本,具有不同的斜率,所以我从中得出的主要结论是销售价格与制造年份之间的关系基本上是一条直线。请记住,这是销售价格的对数,因此实际上向我们展示了一个指数。这就是我会引入领域专业知识的地方,比如“好吧,事物随着时间按照一个恒定比率贬值,因此,我会预期较旧的东西制造年份具有这种指数形状。”所以这就是我提到的,我的机器学习项目的开始,我通常尽量避免使用尽可能多的领域专业知识,让数据说话。因此,今天早上我收到的一个问题是“有一个销售 ID 和型号 ID,我应该抛弃它们,对吧?因为它们只是 ID。”不要。不要对数据做任何假设。保留它们,如果它们被证明是非常重要的预测因子,你会想要找出原因。但是,现在我已经完成了我的特征重要性,我已经从那个树状图中提取出了一些东西(即冗余特征),我正在查看部分依赖性,现在我在思考,这种形状是否符合我的预期?因此,更好的做法是,在绘制之前,首先考虑我期望这种形状是什么。因为事后向自己证明,哦,我知道它会看起来像这样,总是很容易。所以你期望什么形状,然后它是那种形状吗?在这种情况下,我会说这是我所期望的。而之前的图表则不符合我的预期。因此,部分依赖性图表真正揭示了潜在的真相。

问题:假设您有 20 个重要特征,您会为每一个特征测量偏依赖性吗?如果有 20 个重要特征,那么我将对所有这些特征进行偏依赖性分析,其中重要意味着它是一个我实际可以控制的杠杆,其大小的幅度与其他十九个特征的差异不大,您知道,基于所有这些因素,这是一个我应该关心的特征,那么我将想要了解它是如何相关的。在我的经验中,拥有这么多在操作和建模角度上都重要的特征是相当不寻常的。

问题:您如何定义重要性?重要意味着它是一个杠杆(即我可以改变的东西),它位于这个尾巴(左侧)的尖端:

或者它可能不是直接的杠杆。也许它像邮政编码一样,我实际上无法告诉我的客户在哪里居住,但我可以将我的新营销注意力集中在不同的邮政编码上。

问题:对于每一对特征组合进行成对洗牌,保持其他所有内容不变,以查看交互作用并比较分数是否有意义?你不会为了偏依赖性而这样做。我认为您的问题实际上是在询问我们是否可以为特征重要性这样做。我认为交互特征重要性是一个非常重要且有趣的问题。但是通过随机洗牌每一对列来做到这一点,如果您有一百列,这听起来计算量很大,可能不可行。所以我要做的是在我们讨论树解释器之后,我将谈论一个有趣但在很大程度上未被探索的方法,这可能会起作用。

树解释器

Prince:我认为这更像是特征重要性,但特征重要性是针对完整的随机森林模型,而这个树解释器是针对特定观察的特征重要性。所以让我们说这是关于医院再入院的。如果患者 A 将要再次入院,那么对于该特定患者,哪个特征会产生影响,我们如何改变这种情况。它是从平均预测开始计算,然后看每个特征如何改变该特定患者的行为。

Jeremy:我在微笑,因为这是我很长时间以来听到的最好的技术沟通示例之一,所以思考为什么这么有效是非常有意义的。Prince 所做的是,他尽可能具体地举例说明。人类在理解抽象概念方面要差得多。因此,如果您说“它需要某种特征,然后在该特征的观察中”,而不是医院再入院。因此,我们举了一个具体的例子。他做的另一件非常有效的事情是将类比与我们已经理解的东西联系起来。因此,我们已经理解了数据集中所有行的特征重要性的概念。因此,现在我们将为单个行执行此操作。因此,我真的希望我们从这次经验中学到如何成为有效的技术沟通者。因此,Prince 在使用我们可以利用的所有技巧进行有效的技术沟通方面是一个非常好的榜样。希望您觉得这个解释有用。除了向您展示它是什么样子之外,我没有太多要补充。

使用树解释器,我们挑选出一行:

还记得我们在一开始谈到的置信区间吗(即基于树方差的置信度)?我们说你主要用于一行。所以这也是为一行。就像“为什么这个患者可能会再次入院?”这是我们关于该患者或在这种情况下该拍卖会的所有信息。为什么这个拍卖会这么贵?然后我们调用ti.predict,我们得到价格的预测,偏差(即树的根 - 这只是每个人的平均价格,所以这总是一样的),然后是贡献,即这些事情有多重要:

我们计算的方法是说一开始,平均价格是 10。然后我们根据围栏进行分割。对于那些有这个围栏的人,平均价格是 9.5。然后我们根据制造年份小于 1990 进行分割,对于那些有这个制造年份的人,平均价格是 9.7。然后我们根据米数进行分割,对于这个分支,我们得到了 9.4。

然后我们有一个特定的拍卖会,我们通过树传递它。碰巧它走了最顶层的路径。一行只能通过树有一条路径。所以我们最终到了 9.4。然后我们可以创建一个小表格。当我们逐步进行时,我们从顶部开始,我们从 10 开始 - 这是我们的偏差。我们说围栏导致了从 10 变为 9.5(即-0.5)。制造年份将其从 9.5 变为 9.7(即+0.2),然后米数将其从 9.7 降至 9.4(-0.3)。然后如果我们把所有这些加在一起(10-0.5+0.2-0.3),哎呀,那就是预测。

这让我们来到我们的 Excel 电子表格:

上周,我们使用 Excel 进行了这项工作,因为没有一个很好的 Python 库可以制作瀑布图。所以我们看到我们得到了我们的起点这是偏差,然后我们有我们每个贡献,最后我们得到了我们的总数。现在世界变得更美好了,因为 Chris 为我们创建了一个 Python 瀑布图模块,并将其放在 pip 上。所以我们再也不必使用 Excel 了。我想指出,瀑布图至少在我从事业务以来一直在商业沟通中非常重要 - 大约 25 年了。Python 可能已经有几十年的历史了。但尽管如此,Python 世界中没有人真正想到“你知道,我要制作一个瀑布图”,所以直到两天前它们才存在,也就是说这个世界充满了应该存在但不存在的东西。而且并不一定需要花费很多时间来构建。Chris 花了大约 8 个小时,所以数量相当可观但不过分。现在以后,当人们想要 Python 瀑布图时,他们将最终到达 Chris 的 Github 存储库,并希望找到许多其他美国大学的贡献者,他们使其变得更好。

为了帮助改进 Chris 的 Python 瀑布,您需要知道如何做到这一点。因此,您需要提交一个拉取请求。如果您使用一个叫做hub的东西,那么提交拉取请求将变得非常容易。他们建议您将git别名为hub,因为事实证明,hub 实际上是 git 的一个严格的超集。它让您可以执行git forkgit pushgit pull-request,然后您现在已经向 Chris 发送了一个拉取请求。没有 hub,这实际上是一种痛苦,需要像去网站并填写表格之类的事情。因此,这给您没有理由不提交拉取请求。我提到这一点是因为当您面试工作时,我可以向您保证,您正在与之交谈的人将检查您的 github,如果他们看到您有提交经过深思熟虑的拉取请求并被接受到有趣的库,那看起来很棒。这看起来很棒,因为它表明您是一个实际做出贡献的人。它还表明,如果它们被接受,那么您知道如何创建符合人们编码标准、具有适当文档、通过测试和覆盖率等的代码。因此,当人们看着您并说哦,这里是一个有着成功贡献历史的人,接受了开源库的拉取请求,这是您作品集的一个很好的部分。您可以具体引用它。因此,无论是我是构建 Python 瀑布的人,这是我的存储库,还是我是为 Python 瀑布贡献货币数字格式化的人,这是我的拉取请求。每当您在使用任何开源软件时看到某些不正常的东西,这不是问题,而是一个很好的机会,因为您可以修复它并发送拉取请求。所以试一试。第一次有拉取请求被接受时,感觉真的很棒。当然,一个很大的机会是 fastai 库。由于我们的一位学生,我们现在对fastai.structured库的大部分文档字符串都有了,这也是通过拉取请求完成的。

有人对如何计算这些随机森林解释方法或为什么我们可能想要使用它们有任何问题吗?在本周末,您将需要能够从头开始构建所有这些。

问题:只是看着树解释器,我注意到一些值是nan。我明白为什么要保留它们在树中,但nan如何有特征重要性呢?让我把问题传递给你。为什么不呢?换句话说,Pandas 中如何处理nan,因此在树中呢?有人记得,注意这些都是分类变量,Pandas 如何处理分类变量中的nan,fastai 又是如何处理的?Pandas 将它们设置为-1 类别代码,而 fastai 将所有类别代码加一,因此最终变为零。换句话说,记住,当它到达随机森林时,它只是一个数字,只是零。然后我们将其映射回这里的描述。所以问题实际上是为什么随机森林不能在零上分裂?它只是另一个数字。所以它可以是nan= 0,1,2,3。因此,缺失值是通常教得很糟糕的事情之一。通常人们被教导要删除具有缺失值的列或删除具有缺失值的行,或者替换缺失值。这绝不是我们想要的,因为缺失通常是非常非常有趣的。因此,我们实际上从我们的特征重要性中学到,耦合器系统nan是最重要的特征之一。出于某种原因,嗯,我可以猜测,对吧?耦合器系统nan可能意味着这是一种没有耦合器系统的工业设备。现在我不知道是什么类型,但显然是更昂贵的类型。

我参加了一个为大学拨款研究成功而举办的比赛,迄今为止最重要的预测因素是某些字段是否为空[1:15:41]。结果表明,这是数据泄漏,这些字段大多数情况下只在研究拨款被接受后填写。所以这让我赢得了那个 Kaggle 比赛,但实际上并没有对大学有太大帮助。

外推[1:16:16]

我要做一些冒险和危险的事情,我们将进行一些现场编码。我们要进行一些现场编码的原因是我想和你一起探索外推,我也想让你感受一下在这个笔记本环境中如何快速编写代码。这是你在现实世界和考试中需要做的事情,快速创建我们将要讨论的代码。

每当我尝试调查某事的行为时,我都非常喜欢创建合成数据集,因为如果我有一个合成数据集,我知道它应该如何表现。

提醒我,我们在做这个之前,我承诺我们会谈论交互重要性,我差点忘了。

交互重要性[1:17:24]

树解释器告诉我们基于树中差异的特定行的贡献。我们可以为数据集中的每一行计算这个值并将它们相加。这将告诉我们特征的重要性。它将以不同的方式告诉我们特征的重要性。评估特征重要性的一种方法是逐个对列进行洗牌。另一种方法是为每一行进行树解释并将它们相加。两种方法都没有更正确的一种。它们实际上都被广泛使用,所以这是一种类型 1 和类型 2 的特征重要性。所以我们可以尝试扩展一下。不仅仅是单变量特征重要性,还有交互特征重要性。现在这里有一点。我要描述的东西很容易描述。当随机森林首次被发明时,Breiman 就描述过这个方法,它也是 Salford 系统商业软件产品的一部分,他们拥有随机森林的商标。但我不知道它是任何开源库的一部分,我从来没有看到过一篇真正研究它的学术论文。所以我要在这里描述的是一个巨大的机会,但也像有很多细节需要完善。但这里是基本思想。

这里的这个特定差异(红色)不仅仅是因为 year made,而是因为 year made 和 enclosure 的组合[1:19:15]:

这是 9.7 的原因是因为 enclosure 在这个分支中,year made 在这个分支中。换句话说,我们可以说 enclosure 与 year made 的交互作用是-0.3。

那么 9.5 和 9.4 之间的差异呢?那是 year made 和表计上的小时数的交互作用。我在这里使用星号不是表示“乘”,而是表示“与...交互”。这是一种常见的做法,就像 R 的公式也是这样做的。所以 year made 与表计的交互作用贡献了-0.1。

也许我们还可以说从 10 到 9.4,这也显示了仪表和围栏之间的相互作用,中间有一件事。所以我们可以说仪表与围栏的相互作用等于...应该是多少呢?应该是-0.6 吗?有些方式似乎不公平,因为我们还包括了年份的影响。所以也许应该是-0.6,也许我们应该加回这个 0.2(9.5 → 9.7)。这些都是我实际上不知道答案的细节。我们应该如何最好地为这条路径中的每对变量分配贡献?但从概念上来说,我们可以。该路径中的变量对都代表相互作用。

问题:为什么你不强制它们在树中相邻[1:21:47]?我不会说这是错误的方法。但我不认为这是正确的方法。因为在这条路径中,仪表和围栏是相互作用的。所以似乎不承认这种贡献是在丢弃信息。但我不确定。几年前,我在 Kaggle 的员工中有一个实际上对此进行了一些研发,他们确实发现了(我不够接近知道他们如何处理这些细节),但他们做得相当不错。但不幸的是,它从未成为软件产品问世。但也许你们中的一群人可以聚在一起并构建。做一些搜索来检查,但我真的不认为任何开源库中有任何相互作用特征重要性部分。

问题:但这样会排除那些在相互作用之前并不重要的变量之间的相互作用吗?所以说,如果你的行永远不选择沿着那条路径分裂,但是那个变量与另一个变量的相互作用成为你最重要的分裂。我不认为会发生这种情况。因为如果有一个相互作用是重要的,只是因为它是一个相互作用(而不是在单变量基础上),它有时会出现,假设你将最大特征设置为小于一,因此它会出现在某些路径中。

问题:相互作用是什么意思?是乘法、比率、加法吗?相互作用意味着出现在树的同一路径上。在上面的例子中,由于我们在围栏上分支,然后在年份上分支,所以围栏和年份之间存在相互作用。因此,要达到 9.7,我们必须有某个特定的围栏值和某个特定的年份值。

问题:如果你走在你试图观察的两件事之间的中间叶子上,你也会考虑最终的度量是什么吗?我的意思是,如果我们向下延伸树,你会有很多度量,既包括你试图观察的两件事,也包括中间步骤。似乎有一种方法可以在它们之间平均信息吗?也许有。我认为我们应该在论坛上讨论这个。我觉得这很有趣,希望我们能构建出一些伟大的东西,但我需要进行现场编码。这是一个很好的讨论。继续思考并进行一些实验。

回到现场编码。

因此,为了尝试这个,你几乎肯定想先创建一个合成数据集。就像 y = x1 + x2 + x1*x2 或者其他什么。有一些你知道存在交互效应,有一些你知道不存在交互效应,你想确保最终得到的特征重要性是你期望的。

所以可能第一步是使用树解释器风格的单变量特征重要性。这种方法的一个好处是,你拥有多少数据并不重要。你只需要遍历树来计算特征重要性。所以你应该能够以一种相当快速的方式编写代码,所以即使只用纯 Python 编写,也可能足够快,这取决于你的树的大小。

我们将讨论外推和我想要做的第一件事是创建一个具有简单线性关系的合成数据集。我们将假装它就像一个时间序列。所以我们需要创建一些 x 值。创建这种类型的合成数据的最简单方法是使用linspace,它默认创建 50 个观测值在开始和结束之间均匀分布的数据。

然后我们将创建一个因变量,所以让我们假设 x 和 y 之间存在线性关系,并且让我们添加一点随机性。在低和高之间使用random.uniform,所以我们可以添加-0.2 到 0.2 之间的某个值,例如。

接下来我们需要一个形状,基本上就是你想要这些随机数的维度是什么,显然我们希望它们与x的形状相同。所以我们可以直接说x.shape

换句话说,(50,)x.shape。记住,当你看到括号里有逗号的时候,那就是一个只有一个元素的元组。所以这是形状为 50,我们添加了 50 个随机数。现在我们可以绘制这些数值。

好的,这就是我们的数据。当你作为数据科学家工作或在这门课程中做考试时,你需要能够快速地创建一个类似的数据集,将其绘制在图表中而不用考虑太多。正如你所看到的,你不必真的记住太多东西。你只需要知道如何按shift + tab来检查参数的名称,搜索一下,或者尝试找到linspace如果你忘记了它的名字。

所以让我们假设这是我们的数据。我们现在要构建一个随机森林模型,我想要构建一个随机森林模型,让它像一个时间序列一样运行。所以我将左边部分作为训练集。然后将右边部分作为我们的验证或测试集,就像我们在购物或推土机中所做的那样。

我们可以使用与我们在split_vals中使用的完全相同的代码。所以我们可以说:

x_trn, x_val = x[:40], x[40:]

这将数据分为前 40 个和后 10 个。我们可以对 y 做同样的操作。

y_trn, y_val = y[:40], y[40:]

接下来要做的是创建一个随机森林并拟合它,这需要 x 和 y。

m = RandomForestRegressor().fit(x, y)

这实际上会导致错误,原因是它期望 x 是一个矩阵,而不是一个向量,因为它期望 x 有多列数据。

重要的是要知道,只有一列的矩阵和向量不是同一回事。

所以如果我尝试运行这个代码,“预期 2D 数组,实际得到 1D 数组”:

所以我们需要将一维数组转换为二维数组。记住我说过x.shape(50,)。所以x有一个轴,x 的秩是 1。变量的秩等于它的形状的长度 - 它有多少个轴。我们可以将向量看作是秩为 1 的数组,将矩阵看作是秩为 2 的数组。我很少使用向量和矩阵这样的词,因为它们有点毫无意义 - 它们只是更一般的东西的具体例子,它们都是 N 维张量或 N 维数组。所以 N 维数组可以说是秩为 N 的张量。它们基本上意味着相同的事情。物理学家听到这个会很疯狂,因为对于物理学家来说,张量有一个非常具体的含义,但在机器学习中,我们通常以相同的方式使用它。

那么我们如何将一维数组转换为二维数组。我们可以这样做的方法有几种,但基本上我们是切片。冒号(:)表示给我在那个轴上的所有东西。:, None表示给我第一个轴上的所有东西(这是我们唯一拥有的轴),然后None是一个特殊的索引器,表示在这里添加一个单位轴。所以让我给你看看。

它的形状是(50, 1),所以它是秩为 2 的。它有两个轴。其中一个是一个非常无聊的轴 - 它是一个长度为一的轴。所以让我们将None移到左边。这是(1, 50)。然后提醒你,原始的是(50,)。

所以你可以看到我可以将None作为一个特殊的索引器放在那里引入一个新的单位轴。x[None, :]有一行和五十列。x[:, None]有五十行和一列 - 这就是我们想要的。在这门课程和深度学习课程中,这种对秩和维度的玩耍将变得越来越重要。所以花很多时间用 None 切片,用其他东西切片,尝试创建三维、四维张量等。我会向你展示两个技巧。

第一个是你永远不需要写,:,因为它总是被假定的。所以这些是完全相同的:

你会在代码中一直看到这样的写法,所以你需要认识到它。

第二个技巧是x[:, None]是在第二维度(或我猜是索引 1 维度)添加一个轴。如果我总是想把它放在最后一个维度怎么办?通常我们的张量在我们不注意的情况下改变维度,因为你从一个单通道图像变成了一个三通道图像,或者你从一个单个图像变成了一个图像的小批量。突然之间,新的维度出现了。所以为了让事情更一般化,我会说...,这意味着你需要多少维度来填充这个。所以在这种情况下(x[…, None].shape),它是完全相同的,但我总是尝试以这种方式写,因为这意味着当我得到更高维度的张量时,它将继续工作。

所以在这种情况下,我想要 50 行和一列,所以我会称之为 x1。现在让我们在这里使用它,这样就是一个二维数组,所以我可以创建我的随机森林。

然后我可以绘制出来,这就是你需要打开大脑的地方,因为今天早上的人们非常快地理解了这一点,这是非常令人印象深刻的。我将绘制y_trnm.predict(x_trn)。在我开始之前,这会是什么样子?它应该基本上是一样的。我们的预测希望与实际相同。所以这应该落在一条线上,但有一些随机性,所以不会完全相同。

那是容易的。现在让我们做困难的,有趣的那个。那会是什么样子?

想想树的作用,想想右边有一个验证集,左边有一个训练集:

所以想想森林只是一堆树。

Tim: 我猜测由于所有新数据实际上都在原始范围之外,所以它们基本上都是一样的 - 就像一个巨大的群体。

Jeremy: 是的,对。所以忘掉森林,让我们创建一棵树。所以我们可能首先在这里分裂,然后在这里分裂,... 所以我们的最终分裂是最右边的节点。当我们从验证集中取一个时,它会将其通过森林,最终预测最右边的平均值。它无法预测比那更高的任何东西,因为没有更高的东西可以平均。

因此,要意识到随机森林并不是魔法。它只是返回附近观察值的平均值,其中附近在这种“树空间”中。所以让我们运行它,看看 Tim 是不是对的。

天啊,太糟糕了。如果你不知道随机森林是如何工作的,那么这将完全搞砸你。如果你认为它实际上能够对任何它以前没有见过的数据进行外推,尤其是未来的时间段,那就错了。它只是无法做到。它只是对它已经看到的东西进行平均。这就是它能做的全部。

好的,我们将讨论如何避免这个问题。在上一课中,我们稍微谈到通过避免不必要的时间相关变量来避免这个问题。但最终,如果你真的有一个看起来像这样的时间序列,我们实际上必须处理一个问题。我们可以处理这个问题的一种方法是使用神经网络。使用一些实际上具有可以拟合这样的函数或形状的东西,这样它将可以很好地外推:

另一种方法是使用你们在早上课上学到的所有时间序列技术来拟合某种时间序列,然后去趋势化。然后你会得到去趋势化的点,然后使用随机森林来预测这些点。这特别酷,因为想象一下你的随机森林实际上试图预测两种不同状态的数据。所以蓝色的在下面,红色的在上面。

如果你尝试使用随机森林,它将做得相当糟糕,因为时间看起来更加重要。所以它基本上仍然会像这样分裂,然后像那样分裂,最终一旦到达左下角,它会像“哦,好吧,现在我可以看到两种状态之间的差异了。”

换句话说,当你有这么大的时间序列时,直到每棵树都处理时间,你才能看到随机森林中的其他关系。因此,解决这个问题的一种方法是使用梯度提升机(GBM)。GBM 的作用是创建一棵小树,然后通过第一棵小树运行所有内容(可能是时间树),然后计算残差,下一棵小树只是预测残差。所以这有点像去趋势化,对吧?GBM 仍然无法对未来进行外推,但至少它们可以更方便地处理时间相关数据。

在接下来的几周里,我们将更多地讨论这个问题,最终的解决方案将是使用神经网络。但是现在,使用某种时间序列分析,去趋势化,然后在随机森林上使用它并不是一种坏技术。如果你在玩类似厄瓜多尔杂货竞赛之类的东西,那将是一个非常好的东西来尝试。

机器学习 1:第 7 课

原文:medium.com/@hiromi_suenaga/machine-learning-1-lesson-7-69c50bc5e9af

译者:飞龙

协议:CC BY-NC-SA 4.0

来自机器学习课程的个人笔记。随着我继续复习课程以“真正”理解它,这些笔记将继续更新和改进。非常感谢 JeremyRachel 给了我这个学习的机会。

视频

我们将完成从头开始构建我们自己的随机森林!但在此之前,我想解决一些在这一周中出现的问题。

随机森林在一般情况下的位置

我们大约花了一半的课程时间来讲解随机森林,然后在今天之后,这门课程的第二半将广义地讲解神经网络。这是因为这两者代表了覆盖几乎所有你可能需要做的技术的两个关键类别。随机森林属于决策树集成技术类别,与梯度提升机是另一种关键类型,还有一些变体,比如极端随机树。它们的好处是它们非常易解释,可扩展,灵活,适用于大多数类型的数据。它们的缺点是它们完全不会对超出你所见范围的数据进行外推,就像我们在上周课程结束时看到的那样。但它们是一个很好的起点。我认为有很多机器学习工具的目录,很多课程和书籍并没有试图筛选出来,说对于这些问题,使用这个,对于那些问题,使用那个,完了。但它们更像是这里有 100 种不同算法的描述,而你根本不需要它们。比如,我不明白为什么今天你会使用支持向量机。我想不出任何理由去做那样的事情。人们在 90 年代喜欢研究它们,因为它们在理论上非常优雅,你可以写很多关于支持向量机的数学,人们确实这样做了,但实际上我认为它们没有任何用武之地。

在详尽的列表中,有很多技术可以包括在人们采用机器学习问题的每一种方式中,但我更愿意告诉你如何实际解决机器学习问题。我们即将结束今天的第一堂课,这是一种决策树集成的一种类型,在第二部分,Yanett 将告诉你另一种关键类型,即梯度提升,我们即将启动下一课程,介绍神经网络,其中包括各种广义线性模型(GLM)、岭回归、弹性网络套索、逻辑回归等都是神经网络的变体。

有趣的是,创造随机森林的 Leo Breiman 在他的晚年才做出了这一成就,不幸的是,他在之后不久就去世了。因此,关于随机森林的学术文献很少,部分原因是因为在那个时候支持向量机(SVM)开始流行,其他人没有关注它们。另一个原因是随机森林在理论层面上相当难以理解(在理论上分析它们),很难撰写关于它们的会议论文或学术论文。因此,关于它们的研究并不多。但近年来出现了一股新的经验机器学习浪潮,关注的是什么实际上有效。Kaggle 是其中的一部分,但也有像亚马逊和谷歌这样的公司利用机器学习赚取大量利润。因此,如今很多人都在写关于决策树集成的文章,并为决策树集成创建更好的软件,如 GBM 和 xgboost,以及 R 的 ranger 和 scikit-learn 等等。但很多这方面的工作是在工业界而不是学术界进行的,但这是令人鼓舞的。当然,目前在学术界进行的深度学习工作比决策树集成要多,但两者都在取得很大进展。如果看看今天用于决策树集成的软件包,排名前五或六名的最好的软件包,我不确定其中有哪个在五年前真的存在,也许除了 sklearn 之外,甚至三年前也没有。这是一个好的现象。但我认为还有很多工作要做。比如,上周我们讨论了找出哪些交互作用最重要的问题。你们中一些人在论坛中指出,实际上已经有一个梯度提升机的项目,这很棒,但似乎还没有类似的项目用于随机森林。随机森林相比 GBM 有一个很好的优势,那就是它们更难出错,更容易扩展。因此,希望这个社区可以帮助解决这个问题。

您的验证集大小 [5:42]

另一个我在这周期间遇到的问题是关于验证集的大小。它应该有多大呢?所以要回答这个关于验证集需要多大的问题,你首先需要回答这个算法的准确度我需要知道多精确。如果你的验证集显示这是 70%的准确率,如果有人问说,那是 75%还是 65%还是 70%,答案是“我不知道,这个范围内的任何值都足够接近”,那就是一个答案。另一方面,如果是 70%还是 70.01%还是 69.99%呢?那又是另一回事了。所以你需要先问自己,我需要多准确。

例如,在深度学习课程中,我们一直在研究狗和猫的图像,我们所研究的模型在验证集上的准确率大约为 99.4%,99.5%。验证集的大小为 2000。实际上,让我们在 Excel 中做这个,这会更容易一些。因此,错误的数量大约是(1 - 准确率) * n

所以我们大约有 12 只错误。我们拥有的猫的数量是一半,所以错误的猫的数量大约是 6 只。然后我们运行了一个新模型,发现准确率提高到了 99.2%。然后就像,好吧,这个模型在找猫方面是否不太好?嗯,它多找错了 2 只猫,所以可能不是。这重要吗?99.4 和 99.2 有关系吗?如果不是关于猫和狗,而是关于发现欺诈,那么 0.6%的错误率和 0.8%的错误率之间的差异就相当于欺诈成本的 25%,这可能是巨大的。

今年早些时候,ImageNet 发布时真的很有趣,新的竞赛结果出来了,错误率从 3%降到了 2%,我看到很多人在互联网上,一些著名的机器学习研究人员都觉得,一些中国人把准确率从 97%提高到了 98% —— 这在统计上甚至不重要,谁在乎呢。但实际上我觉得哇塞,这支中国团队刚刚超越了最先进的图像识别技术,旧技术的准确率比新技术低了 50%。这才是正确的思考方式,不是吗。因为我们试图识别哪些西红柿是成熟的,哪些不是,而旧方法,有 50%的时间多让进了未成熟的西红柿,或者说有 50%的时间,我们接受了欺诈性的客户。这是一个非常大的差异。所以仅仅因为这个特定的验证集,我们看不出 6 和 8 的区别,并不意味着 0.2%的差异不重要。它可能很重要。所以我的经验法则是,你实际上看了多少观察值,我希望这个数字通常要高于 22。为什么是 22?因为 22 是 t-分布大致变成正态分布的魔法数字。所以你可能已经学过,t-分布是小数据集的正态分布。换句话说,一旦我们有了 22 个或更多的东西,它开始在两个意义上表现得有点正常,就像它更加稳定,你可以更好地理解它。所以当有人问我是否有足够的东西时,我通常会说你是否有 22 个感兴趣的事物的观察值。所以如果你在研究肺癌,你有一个数据集,其中有一千个没有肺癌的人和 20 个患有肺癌的人,我会说我非常怀疑我们会取得多少进展,因为我们甚至没有得到你想要的 20 个东西。同样适用于验证集。如果你没有你想要的 20 个东西,那很可能是没有用的,或者说不符合我们需要的准确度水平。这不是加减 20,只是我在考虑时会有点小心。

问题:所以清楚一点,你想要每组样本的数量是 22,就像在验证集、测试集和训练集中一样吗?所以我的意思是,如果任何一组中某个类别的样本少于 22 个,那么在那一点上就会变得非常不稳定。这就像是第一个经验法则。但接下来我会开始练习我们学到的关于二项分布或伯努利分布的知识。那么 n 个样本和概率 p 的二项分布的均值是多少?n*p。n 乘以 p 就是均值。所以如果你有 50%的机会抛硬币得到正面,你抛 100 次,平均得到 50 次正面。那么标准差是多少?n*p*(1-p)

所以第一个数字你不必记住——这是直观明显的。第二个数字是一个你要永远记住的数字,因为它不仅经常出现,你与之合作的人都会忘记,所以你会成为对话中唯一能立即说出“我们不必运行这个 100 次,我可以立即告诉你这是二项式,它将是n*p*(1-p)的人。

然后是标准误差。标准误差是指如果你运行一堆试验,每次得到一个平均值,那么平均值的标准偏差是多少。我不认为你们已经涵盖了这个内容。这是非常重要的,因为这意味着如果你训练了一百个模型,每次验证集的准确率就像是一个分布的平均值。因此,验证集准确率的标准偏差可以用标准误差来计算,这等于标准偏差除以 n 的平方根。

因此,确定我的验证集是否足够大的一种方法是,每次使用完全相同的超参数训练模型 5 次,然后查看每次的验证集准确率,可以计算出 5 个数字的平均值和标准差,或者可以使用最大值和最小值。但为了节省时间,您可以立即确定,我有一个 0.99 的准确率,无论我是否正确地识别了猫。因此,标准差等于 0.99 * 0.01,然后可以得到标准误差。因此,您需要的验证集大小,就像它必须足够大,以便您对准确性的洞察对于您特定的业务问题足够好。因此,简单的方法是选择一个大小为一千的验证集,训练 5 个模型,查看验证集准确率的变化情况,如果它们都足够接近您所需的水平,那么就可以了。如果不是,也许您应该使其更大,或者考虑改用交叉验证。因此,可以看到,这取决于您试图做什么,您的较不常见类别有多常见,以及您的模型有多准确。

问题:关于较少常见的类别,如果你少于 22 个,比如你有一个样本,比如是一个脸,我只有一个来自那个特定国家的代表,我是把它放入训练集并增加多样性,还是完全从数据集中删除,或者我把它放入测试集而不是验证集?所以你肯定不能把它放入测试集或验证集,因为你在问我能否识别我以前从未见过的东西。但实际上,关于我能否识别我以前从未见过的东西,实际上有一个专门用于这个目的的模型类别——它被称为一次性学习,你只能看到一次东西,然后必须再次识别它,或者零次学习,你必须识别你以前从未见过的东西。我们在本课程中不会涵盖它们,但它们对于像人脸识别这样的事情可能会有用,比如这是我以前见过的同一个人吗。所以一般来说,显然,为了使这样的事情起作用,不是你以前从未见过一个脸,而是你以前从未见过 Melissa 的脸。所以你看到 Melissa 的脸一次,然后你必须再次识别它。所以一般来说,你的验证集和测试集需要具有与你将在实际生产中看到的观察频率相同的混合或频率。你的训练集应该每个类别有相等数量,如果没有,只需复制较少常见的类别直到相等。我想我们之前提到过这篇论文,一篇最近发表的论文,他们尝试了许多不同的方法来训练不平衡的数据集,并一直发现,直到较少常见的类别与较常见的类别大小相同为止,过采样较少常见的类别总是正确的做法。所以你可以简单地复制,比如我只有十个患癌症的人的例子,而没有百个,所以我可以再复制这 10 个另外 90 次,这在一定程度上是一种内存效率低下的方式,包括我认为 sklearn 的随机森林有一个类别权重参数,每次你进行自助抽样或重新采样时,我希望你以更高的概率对较少常见的类别进行抽样。或者如果你正在进行深度学习,确保在你的小批量中,不是随机抽样,而是较少常见的类别的分层样本更频繁地被选中。

回到完成随机森林的部分 18:39

笔记本

让我们回到完成随机森林的工作。今天我们要做的是完成编写我们的随机森林,然后在今天之后,你的作业就是拿这节课并添加我们学到的所有随机森林解释算法。显然,为了能够做到这一点,你需要完全理解这节课的工作原理,所以在我们进行时,请尽可能多地提问。提醒一下,我们再次使用推土机 Kaggle 竞赛数据集。我们将其分为 12,000 个验证集(最后 12,000 条记录),为了更容易跟踪我们的工作,我们将从中挑选两列开始:YearMadeMachineHoursCurrentMeter

from fastai.imports import *
from fastai.structured import *
from sklearn.ensemble import RandomForestRegressor, RandomForestClassifier
from IPython.display import display
from sklearn import metrics
PATH = "data/bulldozers/"

df_raw = pd.read_feather('tmp/bulldozers-raw')
df_trn, y_trn, nas = proc_df(df_raw, 'SalePrice')
def split_vals(a,n): 
    return a[:n], a[n:]
n_valid = 12000
n_trn = len(df_trn)-n_valid
X_train, X_valid = split_vals(df_trn, n_trn)
y_train, y_valid = split_vals(y_trn, n_trn)
raw_train, raw_valid = split_vals(df_raw, n_trn)
x_sub = X_train[['YearMade', 'MachineHoursCurrentMeter']]

上次我们做的是创建了一个树集合,这个树集合包含了一堆树,实际上是一个包含n_trees棵树的列表,每次我们只是调用create_treecreate_tree包含了一个样本大小(sample_sz)的随机索引(rnd_idxs)。这里是无重复抽样。所以记住,自助法意味着有放回抽样。通常在 scikit-learn 中,如果有 n 行数据,我们用有放回抽样抽取 n 行数据,这意味着很多行会出现多次。所以每次我们得到一个不同的样本,但它的大小总是与原始数据集相同。然后我们有一个set_rf_samples函数,我们可以使用它进行少于 n 行的有放回抽样。create_tree再次做的是无重复抽样sample_sz行。因为我们对从零到self.y-1的数字进行排列,然后抽取其中的前self.sample_sz个。实际上有一种更快的方法可以做到这一点。你可以直接使用np.random.choice(而不是np.random.permutation),这是一种稍微更直接的方法,但这种方法也可以。所以rnd_idxs是我们n_trees棵树中的一个的随机样本。然后我们将创建一个DecisionTree。我们的决策树,我们不会传递所有的x,而是传递这些特定的索引,记住 x 是一个 Pandas DataFrame,所以如果我们想用一堆整数对其进行索引,我们使用iloc(整数位置),这使得它在索引方面的行为就像 numpy 一样。现在y向量是 numpy,所以我们可以直接对其进行索引。然后我们将跟踪最小叶子大小(min_leaf)。

class TreeEnsemble():
  def __init__(self, x, y, n_trees, sample_sz, min_leaf=5):
    np.random.seed(42)
    self.x,self.y,self.sample_sz,self.min_leaf = x,y,sample_sz,min_leaf
    self.trees = [self.create_tree() for i in range(n_trees)]

  def create_tree(self):
    rnd_idxs = np.random.permutation(len(self.y))[:self.sample_sz]
    return DecisionTree(
        self.x.iloc[rnd_idxs], 
        self.y[rnd_idxs],
        min_leaf=self.min_leaf
    )

然后在集成中我们真正需要的另一件事情就是一个地方来进行预测。因此我们只需要对每棵树的预测取平均值。就是这样。

def predict(self, x):
    return np.mean([t.predict(x) for t in self.trees], axis=0)
class DecisionTree():
    def __init__(self, x, y, idxs=None, min_leaf=5):
        self.x,self.y,self.idxs,self.min_leaf = x,y,idxs,min_leaf
        m = TreeEnsemble(
            X_train, y_train, 
            n_trees=10, 
            sample_sz=1000, 
            min_leaf=3
        )

然后为了能够运行它,我们需要一个决策树类,因为它被create_tree调用。所以我们开始吧。这就是起点。接下来我们需要做的是完善我们的决策树。所要记住的重要一点是我们所有的随机性都发生在TreeEnsemble中。我们将要创建的 DecisionTree 类中没有随机性。

问题:现在我们正在构建一个随机树回归器,这就是为什么我们要取树输出的平均值。如果我们要处理分类,我们要取最大值吗?就像分类器会给你零或一[22:36]?不,我仍然会取平均值。因此,每棵树都会告诉你叶节点中包含猫的百分比和包含狗的百分比。然后我会平均所有这些百分比,并说在所有树上平均,有 19%的猫和 81%的狗。

随机树分类器几乎与随机树回归器相同,或者几乎可以相同。我们今天要使用的技术基本上完全适用于分类。对于二元分类,您可以使用完全相同的代码。对于多类分类,您只需要更改数据结构,使其像一个独热编码矩阵或一个整数列表,您将其视为一个独热编码矩阵。

所以我们的决策树,记住,我们的想法是我们要尽量避免思考,所以我们基本上会写成如果我们需要的一切已经存在的样子。我们知道当我们创建决策树时,我们将传入 x、y 和最小叶子大小。所以在这里我们需要确保在__init__中有xymin_leaf。还有一件事是,当我们将树分割成子树时,我们需要跟踪哪些行索引进入了树的左侧,哪些进入了树的右侧。所以我们还会有一个叫做idxs的东西。起初,我们根本不费心传入idxs,所以如果没有传入idxs(即if idxs is None),那么我们就会将其设置为 y 的整个长度。np.arange在 Python 中与range相同,但它返回一个 numpy 数组。所以决策树的根包含所有行。这实际上就是决策树根的定义(第 0 行,第 1 行,直到第 y-1 行)。我们将存储我们得到的所有信息。我们将跟踪有多少行,有多少列。

然后树中的每个叶子和每个节点都有一个值/预测。该预测只是等于因变量的平均值。因此,树中的每个节点,用idxs索引的y是在树的这一分支中的因变量的值,因此这里是平均值。树中的一些节点还有一个分数,这就像这里的分割有多有效。但只有在它不是叶子节点时才会成立。叶子节点没有进一步的分割。在创建树时,我们还没有进行任何分割,因此其分数开始时为无穷大。构建了树的根节点后,我们的下一个任务是找出应该在哪个变量上进行分割,以及应该在该变量的哪个水平上进行分割。因此,让我们假设有一个可以做到这一点的东西——find_varsplit。然后我们就完成了。

class DecisionTree():
    def __init__(self, x, y, idxs=None, min_leaf=5):
        if idxs is None: 
            idxs=np.arange(len(y))
        self.x,self.y,self.idxs,self.min_leaf = x,y,idxs,min_leaf
        self.n,self.c = len(idxs), x.shape[1]
        self.val = np.mean(y[idxs])
        self.score = float('inf')
        self.find_varsplit()

那么我们如何找到一个变量来分割呢?嗯,我们可以逐个检查每个潜在的变量,所以c包含我们拥有的列数,逐个检查并查看是否能在该列上找到比目前更好的分割。现在请注意,这并不是完整的随机森林定义。这是假设最大特征被设置为全部的情况。请记住,我们可以将最大特征设置为 0.5,这样我们就不会检查从零到c的所有数字,而是会随机检查从零到c的一半数字。因此,如果您想将其转换为支持最大特征的随机森林,您可以轻松添加一行代码来实现。但是在我们今天的实现中,我们不打算这样做。因此,我们只需要找到更好的分割点,由于我们目前不感兴趣,所以现在我们将其留空。

 # This just does one decision; we'll make it recursive later
    def find_varsplit(self):
        for i in range(self.c): 
            self.find_better_split(i)

    # We'll write this later!
    def find_better_split(self, var_idx): 
        pass

    @property
    def split_name(self): 
        return self.x.columns[self.var_idx]

    @property
    def split_col(self): 
        return self.x.values[self.idxs,self.var_idx]

    @property
    def is_leaf(self): 
        return self.score == float('inf')

    def __repr__(self):
        s = f'n: {self.n}; val:{self.val}'
        if not self.is_leaf:
            s += f'; score:{self.score}; split:{self.split}; var: {self.split_name}'
        return s

在开始编写一个类时,我喜欢做的另一件事是,我想要有一种方法来打印出该类中的内容。如果你输入 print,后面跟着一个对象,或者在 Jupyter Notebook 中,你只需输入对象的名称。目前,它只是打印出<__main__.DecisionTree at 0x7f645ec22358>,这并不是很有帮助。所以如果我们想要用有用的东西来替换它,我们必须定义一个特殊的 Python 方法,名为__repr__,以获得这个对象的表示。所以当我们在 Jupyter Notebook 单元格中基本上只写出名称时,在幕后,它调用那个函数,而该方法的默认实现只是打印出那些无用的东西。所以我们可以替换它,而不是说让我们创建一个格式化字符串,在这里我们将打印出f'n: **{self.n}**; val:**{self.val}'**,所以这个节点中有多少行,以及因变量的平均值是多少。然后,如果它不是叶节点,也就是说如果它有一个分裂,那么我们还应该能够打印出分数,我们分裂出的值,以及我们分裂的变量。现在你会注意到这里,self.is_leaf被定义为一个方法,但我后面没有加括号。这是一种特殊类型的方法,称为属性。属性看起来像一个普通的变量,但实际上是动态计算的。所以当我调用is_leaf时,实际上调用的是**def** is_leaf(self)函数。但我有这个特殊的装饰器@property。这意味着当你调用它时,你不必包括括号。所以它会说这是一个叶子还是不是。所以叶子是我们不分裂的东西。如果我们没有对它进行分裂,那么它的分数仍然设置为无穷大,这就是我的逻辑。

这个@符号被称为装饰器。基本上是告诉 Python 关于你的方法的更多信息的一种方式。任何之前使用过像 Flask 或类似框架进行过 web 编程的人都必须声明这个方法将响应 URL 的这一部分,要么是 POST,要么是 GET,并将其放在一个特殊的装饰器中。在幕后,这告诉 Python 以一种特殊的方式处理这个方法。所以@property是另一个装饰器。如果你在 Python 中变得更加高级,你实际上可以学习如何编写自己的装饰器,就像之前提到的那样,基本上插入一些额外的代码,但现在只需要知道有一堆预定义的装饰器可以用来改变我们的方法的行为,其中之一就是@property,这基本上意味着你不再需要加括号,当然这意味着你不能再添加任何参数了,只能是self

问题:如果分数是无穷大,为什么它是叶子?无穷大不是意味着你在根节点吗?不,无穷大意味着你不在根节点。它意味着你在叶子节点。所以根节点将会有一个分裂,假设我们找到一个。一切都会分裂,直到我们到达底部(即叶子节点),所以叶子节点的分数将是无穷大,因为它们不会分裂。

m = TreeEnsemble(
    X_train, y_train, 
    n_trees=10, 
    sample_sz=1000,
    min_leaf=3
)
m.trees[0]
'''
n: 1000; val:10.079014121552744
'''

这就是我们的决策树。它并没有做太多事情,但至少我们可以创建一个集成。10 棵树,样本量为 1,000,我们可以打印出来。现在当我们输入m.trees[0]时,它不会显示<__main__.DecisionTree at 0x7f645ec22358>,而是显示我们要求它显示的内容。这是叶子节点,因为我们还没有在其上进行分割,所以我们没有更多要说的。

然后索引是,所有从零到一千的数字,因为树的基础包含了一切。这是随机样本中的所有内容,因为当我们到达决策树的时候,我们不再需要担心随机森林中的任何随机性。

给定变量找到最佳分割点

让我们尝试编写找到分割点的函数。因此,我们需要实现find_better_split。它将接受一个变量的索引,并找出最佳的分割点,确定它是否比我们目前为止的任何分割更好,对于第一个变量,答案总是肯定的,因为到目前为止最好的分割点是没有分割,这是无穷糟糕的。

所以让我们首先确保我们有东西可以进行比较。我们要进行比较的是 scikit-learn 的随机森林。我们需要确保 scikit-learn 的随机森林获得与我们完全相同的数据,因此我们首先创建集成,从中提取一棵树,然后找出这棵树使用了哪个特定的随机样本xy,然后将它们存储起来,以便我们可以将它们传递给 scikit-learn(这样我们就有完全相同的信息)。

ens = TreeEnsemble(x_sub, y_train, 1, 1000)
tree = ens.trees[0]
x_samp,y_samp = tree.x, tree.y

所以让我们继续使用 scikit-learn 创建一个随机森林。一个树(n_estimators),一个决策(max_depth),没有自助采样,所以整个数据集。所以这应该与我们即将创建的东西完全相同。让我们试试看。

m = RandomForestRegressor(
    n_estimators=1, 
    max_depth=1,
    bootstrap=False
)
m.fit(x_samp, y_samp)
draw_tree(m.estimators_[0], x_samp, precision=2)

我们需要定义find_better_split函数,并且它需要一个变量。让我们定义我们的x(即自变量),说好,它是树中的所有内容,但只有在这个节点中的那些索引,而在树的顶部是所有内容,只有这一个变量(var_idx)。然后对于我们的y,它就是在这个节点中的索引处的因变量。所以这就是我们的xy

让我们现在逐个检查我们独立变量中的每个值。我会告诉你接下来会发生什么。假设我们的独立变量是 YearMade,它不会按顺序排列。所以我们要去到第一行,然后说好,这里的 YearMade 是 3。那么我要做的是尝试计算如果我们决定以数字 3 为分支时的得分。我需要知道哪些行大于 3,哪些行小于或等于 3,它们将成为我的左侧和右侧。然后我们需要一个得分。我们可以使用很多得分,所以在随机森林中,我们称之为信息增益。信息增益就像我们的得分因为我们将数据分成这两组而变得更好了多少。我们可以用很多方法来计算它:基尼系数、交叉熵、均方根误差等等。如果你考虑一下,有一个均方根误差的替代公式,数学上与一个约束尺度内相同,但稍微容易处理一些,那就是我们要找到一个分割点,使得这两组数据的标准差尽可能低。所以我想找到一个分割点,把所有的猫放在这边,所有的狗放在那边。所以如果这些都是猫,那些都是狗,那么这边的标准差为零,那边的标准差也为零。否则,这是一群完全随机混合的猫和狗,那是一群完全随机混合的猫和狗,它们的标准差会高得多。明白了吗?事实证明,如果找到一个最小化这两组标准差或者具体来说是两个标准差的加权平均的分割点,数学上与最小化均方根误差是相同的。如果你想的话,课后你可以自己证明这一点。

首先,我们需要找到,将其分成两组[37:29]。那么所有大于三的东西在哪里?4、6 和 4。所以我们需要它们价格的标准差。

接下来是标准差小于或等于 3,我们只需取这两者的加权平均值,这就是我们的得分。如果我们在 3 上分割,那就是我们的得分。

然后下一步是尝试在 4 上分割,尝试在 1 上分割,尝试在 6 上分割,多余地再次尝试在 4 上分割,然后再次在 1 上分割,找出哪个效果最好。这就是我们的代码:

def find_better_split(self, var_idx):
   x,y = self.x.values[self.idxs,var_idx], self.y[self.idxs] 
   for i in range(1,self.n-1):
      lhs = x<=x[i]
      rhs = x>x[i]
      if rhs.sum()==0: 
          continue
      lhs_std = y[lhs].std()
      rhs_std = y[rhs].std()
      curr_score = lhs_std*lhs.sum() + rhs_std*rhs.sum()
      if curr_score<self.score: 
          self.var_idx,self.score,self.split = var_idx,curr_score,x[i]

我们将逐行进行,假设左侧是x中小于或等于特定值的任何值。右侧是x中大于此特定值的每个值。

lhsrhs 中将包含什么数据类型?它们实际上会包含什么?它们将是布尔数组,我们可以将其视为零和一。因此,lhs 将是一个数组,每次它不小于或等于时为 false;否则为 true,而 rhs 将是相反的布尔数组。现在我们不能对空集合取标准差,所以如果没有任何大于这个数字 (x[i]) 的值,那么 rhs 将全部为 false,这意味着总和为零。在这种情况下,让我们不再继续这一步,因为没有什么可以取标准差,显然这不是一个有用的分割。

假设我们已经走到这一步,现在我们可以计算左侧和右侧的标准差,然后取加权平均值或求和,这两者对于一个标量来说是相同的,因此这就是我们的得分。然后我们可以检查这个得分是否比迄今为止的最佳得分更好,我们迄今为止的最佳得分,最初将其初始化为无穷大,因此最初这是更好的。如果更好,让我们存储所有我们需要的信息:哪个变量找到了这个更好的分割,我们找到的得分是多少,以及我们分割的值是多少。就是这样。如果我们运行这个,并且我正在使用%timeit,它会看这个命令运行需要多长时间,并试图给出一个统计上有效的度量,这样你就可以看到,它已经运行了 10 次以获得平均值,然后又运行了 7 次以获得运行间的平均值和标准差,所以它花了我 76 毫秒加减 11。

%timeit find_better_split(tree,1)
tree
'''
76.6 ms ± 11.8 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
n: 1000; val:10.079014121552744; score:681.0184057251435; split:3744.0; var:MachineHoursCurrentMeter
'''

所以让我们来检查这是否有效。find_better_split(tree, 0),0 代表YearMade,1 代表MachineHoursCurrentMeter,所以当我们用 1 时,我们得到了MachineHoursCurrentMeter,得分为 681.0184057251435,然后我们再次用零运行,得到了更好的分数(658),并分割了 1974。

find_better_split(tree,0); tree
'''
n: 1000; val:10.079014121552744; score:658.5510186055949; split:1974.0; var:YearMade
'''

所以 1974 年,让我们与上面的 scikit-learn 的随机森林进行比较,是的,这棵树也是这样做的。所以我们确认了这种方法给出了与 sklearn 的随机森林相同的结果。你还可以在这里看到值 10.08 与 sklearn 的根节点的值匹配。所以我们有了一个可以处理一个分割的东西。

问题:为什么我们不在 x 上放一个 unique?因为我还没有尝试优化性能。你可以在 Excel 中看到我检查了这个1两次,4两次,这是不必要的。

好的,Yannet 已经在考虑性能,这是好事。那告诉我这段代码的计算复杂度是多少?

O(n²) 是因为有一个循环和 x<=x[i],我们必须检查每个值,看它是否小于 x[i]。了解如何快速计算计算复杂度是很有用的。我可以保证你做的大多数面试都会要求你即兴计算计算复杂度。而且当你编码时,你希望它成为第二天性。这种技巧基本上是“有循环吗?”如果有,那么显然我们要做这个 n 次,所以涉及到一个 n。循环里面还有循环吗?如果有,那么你需要将它们两个相乘。在这种情况下,没有。循环里面有任何不是常数时间操作的东西吗?所以你可能会看到一个排序在里面,你只需要知道排序是 nlog(n) —— 这应该是第二天性的。如果你看到一个矩阵相乘,你需要知道那是什么。在这种情况下,有一些东西在进行逐元素数组操作,所以要留意任何地方,numpy 在对数组的每个值做一些操作。在这种情况下,它正在检查每个 x 的值是否小于一个常数,所以它将不得不这样做 n 次。所以要将这个扩展成一个计算复杂度,你只需要将循环中的事物数量乘以循环内部的最高计算复杂度,nn

问题:在这种情况下,我们不能只是预先对列表进行排序,然后进行一次nlog(n)的计算吗?有很多事情可以做来加快速度,所以在这个阶段我们只关心的是计算复杂度。但绝对可以。当然,它肯定不是最好的。所以接下来我们要做的就是这个。就像好吧,n²不太好,所以让我们试着让它变得更好。

所以这是我尝试改进的地方。首先,标准差的方程式是什么?

实际上,我们通常不使用那个公式,因为它要求我们多次计算x减去平均值。有谁知道只需要 x 和 x²的公式吗?

www.wolframalpha.com/input/?i=standard+deviation

这是一个非常好的知识点,因为现在你可以计算任何东西的方差或标准偏差。你只需要首先抓取列本身。列的平方。只要你把它们存储在某个地方,你就可以立即计算标准偏差。

所以这对我们有用的原因是,如果我们首先对我们的数据进行排序。然后如果你考虑一下,当我们一步一步地向下走时,每一组都与左边的前一组完全相同,只是多了一件东西,右边则少了一件东西。因此,我们只需要跟踪 x 的总和和 x²的总和,我们只需在左边添加一个东西,x²再添加一个东西,在右边移除一个东西。因此,我们不必每次都遍历整个数据集,因此我们可以将其转化为 O(n)算法。这就是我在这里所做的一切:

tree = TreeEnsemble(x_sub, y_train, 1, 1000).trees[0]
def std_agg(cnt, s1, s2): 
    return math.sqrt((s2/cnt) - (s1/cnt)**2)

def find_better_split_foo(self, var_idx):
  x,y = self.x.values[self.idxs,var_idx], self.y[self.idxs]

  sort_idx = np.argsort(x)
  sort_y,sort_x = y[sort_idx], x[sort_idx]
  rhs_cnt,rhs_sum,rhs_sum2 = self.n, sort_y.sum(), (sort_y**2).sum()
  lhs_cnt,lhs_sum,lhs_sum2 = 0,0.,0.

  for i in range(0,self.n-self.min_leaf-1):
    xi,yi = sort_x[i],sort_y[i]
    lhs_cnt += 1; rhs_cnt -= 1
    lhs_sum += yi; rhs_sum -= yi
    lhs_sum2 += yi**2; rhs_sum2 -= yi**2
    if i<self.min_leaf or xi==sort_x[i+1]:
      continue

    lhs_std = std_agg(lhs_cnt, lhs_sum, lhs_sum2)
    rhs_std = std_agg(rhs_cnt, rhs_sum, rhs_sum2)
    curr_score = lhs_std*lhs_cnt + rhs_std*rhs_cnt
    if curr_score<self.score: 
      self.var_idx,self.score,self.split = var_idx,curr_score,xi

我对数据进行排序,然后我会跟踪右侧的事物数量(rhs_cnt),右侧事物的总和(rhs_sum)和右侧的平方和(rhs_sum2)。最初所有事物都在右侧。因此最初n是计数,y.sum()是右侧的总和,y²(y**2)的总和是右侧的平方和。然后最初左侧没有任何事物,因此为零。然后我们只需要循环遍历每个观察值:

  • 左手计数加一,右手计数减一。

  • 将值加到左手总和,从右手总和减去。

  • 将值的平方加到左手,从右手减去。

现在我们需要小心,因为如果我们说小于或等于一,例如,我们不会停在第一行,而是必须将该组中的所有内容都包括在内。

所以我要做的另一件事是确保下一个值不同于这个值。如果是的话,我会跳过它。所以我只是要再次检查这个值和下一个值不相同(**if** xi==sort_x[i+1]:)。只要它们不相同,我就可以继续前进,通过传入计数、总和和平方和来计算我的标准偏差。那个公式就在那里:

现在我们可以对右侧进行同样的操作,这样我们就可以像之前一样计算加权平均分数,下面的所有行都是一样的。

所以我们把 O(n²)的算法转换成了 O(n)的算法。一般来说,像这样的东西会给你带来比将某些东西推送到 Spark 集群或者更快的 RAM 或者在 CPU 中使用更多核心等更多价值。这是你想要改进你的代码的方式。具体来说,编写代码时不要过多考虑性能。运行它,看看它是否对你的需求足够快。如果是,那么你就完成了。如果不是,进行性能分析。在 Jupyter 中,你可以使用%prun,它会告诉你算法中时间花在哪里。然后你可以去看看实际花费时间的部分,思考它在算法上是否尽可能高效。在这种情况下,我们运行它,从 76 毫秒降到不到 2 毫秒。现在一些新手可能会认为“哦,太好了,我节省了 60 多毫秒”,但关键是这将被运行数千万次。所以 76 毫秒版本太慢了,对于实际使用的任何随机森林来说都是不切实际的。而另一方面,我找到的 1 毫秒版本实际上是相当可接受的。

%timeit find_better_split_foo(tree,1)
tree
'''
2.2 ms ± 148 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
n: 1000; val:10.079014121552744; score:658.5510186055565; split:1974.0; var:YearMade
'''

然后检查,数字应该与之前完全相同,而且确实如此。

find_better_split_foo(tree,0); tree
'''
n: 1000; val:10.079014121552744; score:658.5510186055565; split:1974.0; var:YearMade
'''

现在我们有了一个函数find_better_split,它可以做我们想要的事情,我想把它插入到我的DecisionTree类中。这是一个非常酷的 Python 技巧。Python 可以动态执行所有操作,因此我们实际上可以说DecisionTree中名为find_better_split的方法就是我刚刚创建的那个函数。

DecisionTree.find_better_split = find_better_split_foo

将其放在该类中。现在我告诉你这件事稍微令人困惑的地方是,左边的find_better_split和右边的find_better_split实际上并没有任何关系。它们只是恰好以相同的顺序拥有相同的字母。所以我可以将其命名为find_better_split_foo,然后我可以调用它。现在我的函数实际上被称为find_better_split_foo,但我期望调用的方法是名为 DecisionTree.find_better_split 的东西。所以在这里,我可以说:

DecisionTree.find_better_split = find_better_split_foo

在使用的每种语言中,了解命名空间的工作原理是很重要的。其中最重要的一点是了解它是如何确定名称所指的内容的。所以这里(DecisionTree.find_varsplit)意味着DecisionTree类内部定义的find_better_split,而不是其他地方。右边的这个意味着全局命名空间中的find_better_split_foo。许多语言没有全局命名空间,但 Python 有。因此,即使它们恰好以相同的顺序拥有相同的字母,它们也不以任何方式指向相同的内容。就像这边的家庭可能有一个叫 Jeremy 的人,而我的家庭也有一个叫 Jeremy 的人。我们的名字恰好相同,但我们并不是同一个人。

现在我们已经将find_better_split方法放入了具有这个新定义的DecisionTree中,当我现在调用TreeEnsemble构造函数时,决策树集合构造函数会调用create_treecreate_tree实例化DecisionTreeDecisionTree调用find_varsplit,它会遍历每一列以查看是否可以找到更好的分割点,我们现在已经定义了find_better_split,因此当我们创建TreeEnsemble时,它已经执行了这个分割点。

tree = TreeEnsemble(x_sub, y_train, 1, 1000).trees[0]; tree
'''
n: 1000; val:10.079014121552744; score:658.5510186055565; split:1974.0; var:YearMade
'''

好的。这很不错,对吧?我们一次只做一点点,测试每一步。当你们实现随机森林解释技术时,你们可能想尝试以这种方式编程,检查每一步是否与 scikit-learn 所做的匹配,或者与你们构建的测试匹配。

完整的单棵树[55:13]

在这一点上,我们应该尝试更深入地探究。现在让我们将 max_depth 设置为 2。这是 scikit-learn 所做的。在 YearMade 74 处中断后,它接着在 MachineHoursCurrentMeter 2956 处中断。

m = RandomForestRegressor(
    n_estimators=1, 
    max_depth=2, 
    bootstrap=False
) 
m.fit(x_samp, y_samp) 
draw_tree(m.estimators_[0], x_samp, precision=2)

所以我们有一个叫做find_varsplit的东西,它只是遍历每一列,尝试看看是否有更好的分割点。但实际上,我们需要再进一步。我们不仅需要遍历每一列,看看这个节点是否有更好的分割点,而且还需要看看我们刚刚创建的左侧和右侧是否有更好的分割点。换句话说,左侧和右侧应该成为决策树本身。

在右侧创建这棵树和在左侧创建这棵树之间没有任何区别,除了左侧包含 159 个样本,右侧包含一千个。

因此,第一行代码与之前完全相同。然后我们检查它是否是叶节点。如果是叶节点,那么我们就没有更多的事情要做了。这意味着我们就在底部,没有进行分割,所以我们不需要做任何进一步的操作。另一方面,如果它不是叶节点,那么我们需要将其分割成左侧和右侧。现在,早些时候,我们创建了一个左侧和右侧的布尔数组。最好是有一个索引数组,因为我们不想在每个节点中都有一个完整的布尔数组。因为请记住,尽管在这个大小的树中看起来似乎没有很多节点,但当它完全展开时,底层(即如果最小叶大小为 1)包含与整个数据集相同数量的节点。因此,如果每个节点都包含整个数据集大小的完整布尔数组,那么内存需求会增加。另一方面,如果我们只存储此节点中所有内容的索引,那么它将变得越来越小。

def find_varsplit(self):
    for i in range(self.c): 
        self.find_better_split(i)
    if self.is_leaf: return
    x = self.split_col
    lhs = np.nonzero(x<=self.split)[0]
    rhs = np.nonzero(x>self.split)[0]
    self.lhs = DecisionTree(self.x, self.y, self.idxs[lhs])
    self.rhs = DecisionTree(self.x, self.y, self.idxs[rhs])

np.nonzero 与x<=self.split完全相同,它得到布尔数组,但将其转换为true的索引[58:07]。因此,这个lhs现在是左侧和右侧的索引列表。现在我们有了左侧和右侧的索引,我们现在可以继续创建一个决策树。所以self.lhs是我们左侧的决策树,self.rhs是我们右侧的决策树。我们不需要做其他事情。我们已经写好了这些。我们已经有一个可以创建决策树的构造函数。所以当你真正思考这在做什么时,会有点让人头疼,对吧?因为find_varsplit被调用的原因是因为决策树构造函数调用了它。但是find_varsplit本身又调用了决策树构造函数。所以我们实际上有循环递归。我并不聪明到足以能够思考递归,所以我选择不去想。我只是写出我的意思,然后不再考虑。我想要什么?找到一个变量分割。我必须遍历每一列,看看是否有更好的东西,如果成功进行了分割,找出左侧和右侧,然后将它们转换为决策树。现在尝试思考这两种方法如何相互调用会让我发疯,但我不需要这样做。我知道我有一个有效的决策树构造函数,我知道我有一个有效的find_varsplit,所以就这样。这就是我进行递归编程的方式,就是假装我没有。我只是忽略它。这是我的建议。你们中很多人可能足够聪明,能够比我更好地思考这个问题,那就好。如果你能的话。

DecisionTree.find_varsplit = find_varsplit

所以现在我已经写好了,我可以将其打补丁到 DecisionTree 类中,一旦我这样做了,TreeEnsemble 构造函数将会使用它,因为 Python 是动态的。

tree = TreeEnsemble(x_sub, y_train, 1, 1000).trees[0]; tree
'''
n: 1000; val:10.079014121552744; score:658.5510186055565; split:1974.0; var:YearMade
'''

现在我可以检查1:00:31。我的左手边应该有 159 个样本和值为 9.66。

tree.lhs
'''
n: 159; val:9.660892662981706; score:76.82696888346362; split:2800.0; var:MachineHoursCurrentMeter
'''

右手边,841 个样本和 10.15。

tree.rhs
'''
n: 841; val:10.158064432982941; score:571.4803525045031; split:2005.0; var:YearMade
'''

左手边的左手边,150 个样本和 9.62。

tree.lhs.lhs
'''
n: 150; val:9.619280538108496; score:71.15906938383463; split:1000.0; var:YearMade
'''

所以你可以看到,因为我并不聪明到足以编写机器学习算法,不仅我第一次写不正确,通常每一行我写的都是错误的。所以我总是从这样的假设开始,我刚刚输入的代码几乎肯定是错误的。我只需要看看为什么以及如何。所以我只是确保。最终我会到达这样一个点,让我很惊讶的是,它不再出错了。所以在这里,我可以感觉到好吧,如果所有这些事情碰巧与 scikit-learn 完全相同,那将是令人惊讶的。所以看起来还不错。

tree.lhs.rhs
'''
n: 9; val:10.354428077535193
'''

预测 [1:01:43]

现在我们有了一个可以构建整个树的东西,我们想要有一个可以计算预测的东西。所以提醒一下,我们已经有了一个可以为TreeEnsemble计算预测的东西(通过调用tree.predict(x)),但在DecisionTree中没有叫做tree.predict的东西,所以我们需要写一个。

为了让这更有趣,让我们开始增加我们使用的列数。

cols = [
    'MachineID', 'YearMade', 'MachineHoursCurrentMeter',
    'ProductSize', 'Enclosure','Coupler_System', 'saleYear'
]

让我们再次创建我们的TreeEnsemble

%time tree = TreeEnsemble(X_train[cols], y_train, 1, 1000).trees[0]
x_samp,y_samp = tree.x, tree.y
'''
CPU times: user 288 ms, sys: 12 ms, total: 300 ms
Wall time: 297 ms
'''

这一次,让我们将最大深度设为 3。

m = RandomForestRegressor(
    n_estimators=1, 
    max_depth=3, 
    bootstrap=False
)
m.fit(x_samp, y_samp)
draw_tree(m.estimators_[0], x_samp, precision=2, ratio=0.9, size=7)

所以现在我们的树变得更加有趣。现在让我们定义如何为树创建一组预测。因此,树的一组预测就是每一行的预测。就是这样。这就是我们的预测。因此,树的预测是数组中每一行的预测。所以再次,我们跳过思考,思考很困难。所以让我们继续推迟。这个**for** xi **in** x很方便,对吧?请注意,无论数组的秩如何,您都可以使用 numpy 数组中的for blah。无论数组中的轴数是多少。它的作用是遍历主轴。

这些概念在我们进入越来越多的神经网络时将变得非常重要,因为我们将一直在进行张量计算。因此,向量的主轴是向量本身。矩阵的主轴是行。三维张量的主轴是表示切片的矩阵等等。在这种情况下,因为 x 是一个矩阵,这将循环遍历行。如果您以这种方式编写您的张量代码,那么它将很好地推广到更高的维度。在这个 x 中有多少维度并不重要。这将循环遍历每个主轴。因此,我们现在可以称之为 DecisionTree.predict

def predict(self, x): 
    return np.array([self.predict_row(xi) for xi in x])

所以我需要做的就是编写predict_row。我一直在拖延思考,这很好,实际上我需要做工作的地方,现在基本上是微不足道的。如果我们在叶节点,那么预测值就等于我们在原始树构造函数中计算的那个值(即y的平均值)。如果不是叶节点,那么我们必须弄清楚是沿左路径还是右路径进行预测。因此,如果这一行中的变量(xi[self.var_idx])小于或等于我们决定拆分的值,则我们沿左路径前进;否则我们沿右路径前进。然后,确定我们想要的路径/树之后,我们只需在其上调用predict_row。再次,我们无意中创建了递归的东西。如果是叶节点,则返回该值;否则根据需要返回左侧或右侧的预测值。

def predict_row(self, xi):
    if self.is_leaf: return self.val
    t = self.lhs if xi[self.var_idx]<=self.split else self.rhs
    return t.predict_row(xi)
DecisionTree.predict_row = predict_row

注意这里的self.lhs **if** xi[self.var_idx]<=self.split **else** self.rhs,这个 if 与上面的 if 没有任何关系:

if something:
    x= do1()
else:
    x= do2()

上面的这个 if 是一个控制流语句,告诉 Python 沿着这条路径或那条路径进行一些计算。下面的这个 if 是一个返回值的运算符。

x = do1() if something else do2()

所以你们做过 C 或 C++的人会认出它与这个是完全相同的(即三元运算符):

x = something ? do1() : do2()

基本上我们要做的是,我们要得到一个值,如果something为真,我们会说这个值是(do1()),否则是另一个值(do2())。你可以用冗长的方式来写,但那将需要写 4 行代码来做一件事,而且还需要你编写的代码,如果你自己或向别人阅读时,表达方式并不自然。我想说“我要走的树是左边,如果变量小于分割值,否则是右边。所以我想按照我思考或说代码的方式来编写我的代码。因此,这种三元运算符对此非常有帮助。

所以现在我已经对一行进行了预测,我可以将其放入我的类中:

DecisionTree.predict = predict

现在我可以计算预测。

%time preds = tree.predict(X_valid[cols].values)
'''
CPU times: user 156 ms, sys: 4 ms, total: 160 ms
Wall time: 162 ms
'''

现在我可以将我的实际数据与我的预测数据进行对比。当你做散点图时,通常会有很多点重叠在一起,所以一个好的技巧是使用 alpha。Alpha 表示透明度,不仅在 matplotlib 中,在世界上几乎所有的图形包中都是如此。因此,如果将 alpha 设置为小于 1,那么这意味着你需要将 20 个点叠加在一起才能完全显示为蓝色。这是一个很好的方法来看看有多少点重叠在一起 - 散点图的一个好技巧。

plt.scatter(preds, y_valid, alpha=0.05)

这是我的 R²。

metrics.r2_score(preds, y_valid)
'''
0.50371522136882341
'''

那么现在让我们继续进行一个没有最大分裂次数的随机森林,我们的树集合也没有最大分裂次数,我们可以将我们的 R²与他们的 R²进行比较。

m = RandomForestRegressor(
    n_estimators=1, 
    min_samples_leaf=5, 
    bootstrap=False
)
%time m.fit(x_samp, y_samp)
preds = m.predict(X_valid[cols].values)
plt.scatter(preds, y_valid, alpha=0.05)

metrics.r2_score(preds, y_valid)
'''
0.47541053100694797
'''

它们并不相同,但实际上我们的稍微好一点。我不知道我们做了什么不同,但我们会接受它😊 所以现在我们有了一个对于一个只有一棵树的森林,在使用一个真实的实际数据集(推土机的蓝皮书)进行验证时,与 scikit-learn 相比提供了同样好的准确性。

把它放在一起

让我们继续完善这个。现在我想要做的是创建一个包含这段代码的包。我通过创建一个方法,再创建一个方法,然后将它们拼接在一起来创建这个包。现在我回到笔记本中,收集了所有实现方法的单元格,然后将它们全部粘贴在一起。

class TreeEnsemble():
  def __init__(self, x, y, n_trees, sample_sz, min_leaf=5):
    np.random.seed(42)
    self.x,self.y,self.sample_sz,self.min_leaf = x,y,sample_sz,min_leaf
    self.trees = [self.create_tree() for i in range(n_trees)] 
  def create_tree(self):
    idxs = np.random.permutation(len(self.y))[:self.sample_sz]
    return DecisionTree(
        self.x.iloc[idxs], 
        self.y[idxs], 
        idxs=np.array(range(self.sample_sz)), 
        min_leaf=self.min_leaf
    )

  def predict(self, x):
    return np.mean([t.predict(x) for t in self.trees], axis=0)def std_agg(cnt, s1, s2): return math.sqrt((s2/cnt) - (s1/cnt)**2)class DecisionTree():
  def __init__(self, x, y, idxs, min_leaf=5):
    self.x,self.y,self.idxs,self.min_leaf = x,y,idxs,min_leaf
    self.n,self.c = len(idxs), x.shape[1]
    self.val = np.mean(y[idxs])
    self.score = float('inf')
    self.find_varsplit()

  def find_varsplit(self):
    for i in range(self.c): self.find_better_split(i)
    if self.score == float('inf'): return
    x = self.split_col
    lhs = np.nonzero(x<=self.split)[0]
    rhs = np.nonzero(x>self.split)[0]
    self.lhs = DecisionTree(self.x, self.y, self.idxs[lhs])
    self.rhs = DecisionTree(self.x, self.y, self.idxs[rhs]) def find_better_split(self, var_idx):
    x,y = self.x.values[self.idxs,var_idx], self.y[self.idxs]
    sort_idx = np.argsort(x)
    sort_y,sort_x = y[sort_idx], x[sort_idx]
    rhs_cnt,rhs_sum,rhs_sum2 = self.n,sort_y.sum(),(sort_y**2).sum()
    lhs_cnt,lhs_sum,lhs_sum2 = 0,0.,0. for i in range(0,self.n-self.min_leaf-1):
      xi,yi = sort_x[i],sort_y[i]
      lhs_cnt += 1; rhs_cnt -= 1
      lhs_sum += yi; rhs_sum -= yi
      lhs_sum2 += yi**2; rhs_sum2 -= yi**2
      if i<self.min_leaf or xi==sort_x[i+1]:
       continue lhs_std = std_agg(lhs_cnt, lhs_sum, lhs_sum2)
      rhs_std = std_agg(rhs_cnt, rhs_sum, rhs_sum2)
      curr_score = lhs_std*lhs_cnt + rhs_std*rhs_cnt
      if curr_score<self.score: 
       self.var_idx,self.score,self.split = var_idx,curr_score,xi @property
  def split_name(self): return self.x.columns[self.var_idx]

  @property
  def split_col(self): return self.x.values[self.idxs,self.var_idx] @property
  def is_leaf(self): return self.score == float('inf')

  def __repr__(self):
    s = f'n: **{self.n}**; val:**{self.val}**'
    if not self.is_leaf:
      s += f'; score:**{self.score}**; split:**{self.split}**; var:
           **{self.split_name}**'
    return s def predict(self, x):
    return np.array([self.predict_row(xi) for xi in x]) def predict_row(self, xi):
    if self.is_leaf: return self.val
    t = self.lhs if xi[self.var_idx]<=self.split else self.rhs
    return t.predict_row(xi)

就是这样。这就是我们一起编写的代码。

ens = TreeEnsemble(X_train[cols], y_train, 5, 1000)
preds = ens.predict(X_valid[cols].values)
plt.scatter(y_valid, preds, alpha=0.1, s=6);

metrics.r2_score(y_valid, preds)
'''
0.71011741571071241
'''

这里我们有一个蓝色推土机的模型,使用了我们完全从头开始编写的随机森林,R²为 71。这很酷。

性能和 Cython

当我尝试比较这个与 scikit-learn 的性能时,这个要慢得多,原因是虽然很多工作是由 numpy 完成的,numpy 是优化良好的 C 代码,但想想树的最底层。如果我们有一百万个数据点,树的底层有大约 500,000 个决策点,底下有一百万个叶子。这就像调用了 500,000 个分割方法,其中包含多次调用 numpy,而 numpy 只有一个要计算的项目。这是非常低效的。这是 Python 在性能方面特别不擅长的事情(即多次调用大量函数)。我们可以看到它并不差。对于 15 年前被认为是相当大的随机森林来说,这被认为是相当不错的性能。但是现在,这至少比应该的速度慢了几百倍。

scikit-learn 的开发人员为了避免这个问题所做的是,他们使用了一种叫做 Cython 的东西来实现。Cython 是 Python 的一个超集。所以你写的任何 Python 代码基本上都可以作为 Cython 来使用。但是 Cython 运行方式有所不同。它不是直接传递给 Python 解释器,而是将其转换为 C 语言,编译,然后运行该 C 代码。这意味着,第一次运行时会花费一些时间,因为需要进行翻译和编译,但之后运行会快得多。所以我想快速向你展示一下这是什么样子,因为你肯定会遇到 Cython 可以帮助你工作的情况,而你大部分一起工作的人可能从未使用过它(甚至可能不知道它的存在),所以拥有这种超能力是非常棒的。

在笔记本中使用 Cython,你可以这样说:

%load_ext Cython

这里是一个 Python 函数fib1

def fib1(n):
    a, b = 0, 1
    while b < n:
        a, b = b, a + b

这里是一个 Cython 函数。它与顶部的%%cython完全相同。实际上,它的运行速度大约是fib1的两倍,因为它进行了编译。

%%cython
def fib2(n):
    a, b = 0, 1
    while b < n:
        a, b = b, a + b

这里是同样的版本,我使用了一个特殊的 Cython 扩展叫做cdef,它定义了返回值和每个变量的 C 数据类型。基本上这就是你可以用来开始加快运行速度的技巧。在那一点上,现在它知道它不只是一个名为 T 的 Python 对象。所以 fib3,它和之前完全一样,但我们说我们传递给它的东西的数据类型是什么,然后定义每个变量的数据类型。

%%cython
def fib3(int n):
    cdef int b = 1
    cdef int a = 0
    cdef int t = 0
    while b < n:
        t = a
        a = b
        b = a + b

所以如果我们这样做,现在我们有了一个快 10 倍的东西。

%timeit fib1(50)
'''
705 ns ± 62.5 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
'''
%timeit fib2(50)
'''
362 ns ± 26.5 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
'''
%timeit fib3(50)
'''
70.7 ns ± 4.07 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
'''

这并不需要太多额外的工作,只是用一点 Python 和一些标记,所以知道它的存在是很好的,因为如果有一些定制的东西你想要做,实际上要去 C 语言编译并链接回来是很痛苦的。而在这里做起来相当容易。

问题:当你在使用 Cython 版本时,对于 numpy 数组,是否有特定的 C 类型[1:17:16]?是的,有很多特定的内容用于将 Cython 与 numpy 集成,有一个完整的页面介绍了这些内容。所以我们不用担心过多细节,但你可以阅读那个页面,基本上可以了解基本思想。

使用 NumPy 与 Cython — Cython 0.29a0 文档

cython.readthedocs.io

有这个cimport,基本上是将某种类型的 Python 库导入到代码的 C 部分,然后你可以在 Cython 中使用它。这很简单直接。

所以你现在的任务是实现:

  • 基于树方差的置信度

  • 特征重要性

  • 部分依赖

  • 树解释器

对于那个随机森林。删除冗余特征根本不使用随机森林,所以你不必担心这个。外推不是一种解释技术,所以你也不必担心这个。所以只是其他的。所以基于树方差的置信度,我们已经编写了那段代码,所以我怀疑我们在笔记本中拥有的完全相同的代码应该继续工作。所以你可以尝试确保让它工作。特征重要性是通过变量洗牌技术实现的,一旦你让它工作,偏依赖只是几行代码之遥,因为你不是洗牌一列,而是用一个常数值替换它。几乎是相同的代码。

然后树解释器,这将需要您编写一些代码并思考。一旦您编写了树解释器,如果您愿意,您就非常接近创建特征重要性的第二种方法 - 即在所有行中累加重要性的方法。这意味着,您将非常接近进行交互重要性。事实证明,xgboost 实际上有一个非常好的交互重要性库,但似乎没有一个适用于随机森林,因此您可以从使其在我们的版本上运行开始(如果您想进行交互重要性),然后您可以使其在原始的 scikit-learn 版本上运行,这将是一个很酷的贡献。有时,针对自己的实现编写代码更好,因为您可以清楚地看到发生了什么。

如果在任何时候遇到困难,请在论坛上提问。关于如何寻求帮助的整个页面都在维基上有。当你在 Slack 上向同事寻求帮助,当你在 Github 或 Discourse 上向技术社区的人寻求帮助时,正确地寻求帮助将有助于让人们愿意帮助你并能够帮助你。

  • 搜索你遇到的错误,看看是否已经有人问过。

  • 你已经尝试过如何修复它了吗?

  • 你认为出了什么问题?

  • 你用的是什么样的电脑?它是如何设置的?软件版本是什么?

  • 你确切地输入了什么,确切地发生了什么?

你可以通过截屏来做到这一点,所以确保你有一些非常容易使用的截屏软件。所以如果我要截屏,我只需按下一个按钮,选择区域,复制到剪贴板,转到论坛,粘贴进去,然后就完成了(你甚至可以将图像缩小!)。

更好的做法是,如果有几行代码和错误消息需要查看,可以创建一个 Gist。Gist 是一个很方便的 Github 工具,基本上可以让你分享代码。如果我想要创建一个这样的 Gist,我实际上有一个扩展:

点击那个,给它起个名字,然后点击“公开”。这样就可以将我的 Jupyter 笔记本公开分享。然后我可以复制那个 URL,复制链接位置,然后粘贴到我的论坛帖子中。然后当人们点击它时,他们会立即看到我的笔记本。

现在,这个特定的按钮是一个扩展,所以在 Jupyter 上,您需要点击 Nbextensions,然后点击 Gist-it。当您在那里时,您还应该点击 Collapsible Headings,这是我使用的一个方便的功能,让我可以折叠和展开内容。

如果您打开 Jupyter 时没有看到这个 Nbextensions 按钮,那么只需搜索 Jupyter Nbextensions — 它会告诉您如何使用 pip 安装并设置它。

神经网络广义定义 [1:23:20]

笔记本

除了作业之外,我们已经完成了随机森林,直到下一个课程,当你看到 GBMs 时,我们已经完成了决策树集成。我们将转向广义的神经网络。神经网络将使我们能够超越随机森林的最近邻方法。所有随机森林能做的就是对已经看到的数据进行平均。它不能外推或计算。线性回归可以计算和外推,但只能以非常有限的方式。神经网络给我们带来了两全其美的好处。

我们将从将它们应用于非结构化数据开始。非结构化数据指的是像素、声波振幅或单词 - 数据中所有列中的所有内容都是相同类型,而不是数据库表中有收入、成本、邮政编码和州名(结构化数据)。我们也将用它来处理结构化数据,但稍后再做。非结构化数据稍微容易一些,也是更多人长期以来一直在应用深度学习的领域。

如果您也在学习深度学习课程,您会发现我们将从两个不同的方向接近相同的结论。因此,深度学习课程从解决复杂的卷积神经网络开始,使用复杂的优化方案,我们将逐渐深入了解它们的工作原理。而机器学习课程则更多地从随机梯度下降的实际工作原理开始,我们可以用单层来创建逻辑回归等内容。当我们添加正则化时,它如何给我们提供岭回归、弹性网络套索等内容。当我们添加更多层时,它如何让我们处理更复杂的问题。在这个机器学习课程中,我们只会看到全连接层,我认为下个学期与 Yannet 一起,您可能会看到一些更复杂的方法。因此,这个机器学习课程中,我们将更多地关注矩阵的实际运算过程,而深度学习则更多地关注如何以世界级水平解决真实世界的深度学习问题的最佳实践。

下周,我们将研究经典的 MNIST 问题,即如何识别数字。如果你感兴趣,你可以提前尝试使用随机森林来解决这个问题,你会发现效果不错。考虑到随机森林基本上是一种最近邻的类型(它在树空间中找到最近的邻居),那么随机森林绝对可以识别出这个 9,这些像素与我们在其他图像中看到的像素相似,而且平均来说,它们也是 9。因此,它绝对可以使用随机森林解决这类问题。但我们最终会受到数据限制,因为每次我们增加一个决策点,我们的数据大致减半,所以这就限制了我们可以进行的计算量。而神经网络,我们将能够使用大量的参数,通过我们将学习的正则化技巧,我们将能够进行大量的计算,实际上我们几乎没有什么限制可以计算的结果。

祝你在随机森林解释方面好运,下次再见。

机器学习 1:第 8 课

原文:medium.com/@hiromi_suenaga/machine-learning-1-lesson-8-fa1a87064a53

译者:飞龙

协议:CC BY-NC-SA 4.0

来自机器学习课程的个人笔记。随着我继续复习课程以“真正”理解它,这些笔记将继续更新和改进。非常感谢 JeremyRachel 给了我这个学习的机会。

广义定义的神经网络

视频 / 笔记本

正如我们在上一课结束时讨论的那样,我们正在从决策树集成转向广义定义的神经网络。如你所知,随机森林和决策树受到一个限制,即它们基本上只是在做最近邻。它们所能做的就是返回一堆其他点的平均值。因此,它们无法外推,如果你在考虑如果我将价格提高 20%,而你以前从未定价到那个水平,或者明年的销售情况会发生什么,显然我们以前从未见过明年,外推是非常困难的。它也很难,因为它只能做大约对数 2 的 N 次决策,所以如果有一个时间序列需要拟合,需要 4 步才能到达正确的时间区域,然后突然它没有多少决策可以做了,所以它可以做的计算量有限。因此,它可以建模的关系复杂度有限。

问题:我可以问一个关于随机森林的另一个缺点吗?如果我们有一个数据作为分类变量,这些变量不是按顺序排列的,对于随机森林,我们对它们进行编码并将它们视为数字,假设我们有 20 个基数,那么随机森林给出的分割结果可能是小于 5 或小于 6。但如果类别不是按顺序排列(即没有任何顺序),那意味着什么?所以如果你有,比如说,让我们回到推土机,EROPS,带空调的 EROPS,OROPS,N/A 等,我们任意地将它们标记为 0 到 3。实际上我们知道真正重要的是是否有空调。那会发生什么?基本上它会说,如果我将 EROPS w A/C 和 OROPS 组合在一起,将 N/A 和 EROPS 组合在一起,这是一个有趣的分割,因为碰巧所有带空调的都会最终出现在右侧。做完这一步后,它会进一步注意到在 EROPS w A/C 和 OROPS 组中,它还需要将其进一步分成两组。最终它会到达那里。它会提取带有空调的类别。只是它需要更多的分割,比我们理想中希望的要多。所以这有点类似于它要建模一条线,只能通过大量分割并且只是近似地完成。

后续问题:那么随机森林对于不是连续的类别也可以吗?是的,它可以。只是在某些方面它不够理想,因为我们需要做比我们想要的更多的分割点,但它可以做到。它做得相当不错。因此,尽管随机森林确实存在一些缺陷,但它们非常强大,特别是因为它们几乎没有假设,所以很难出错。用随机森林赢得 Kaggle 比赛有点困难,但很容易进入前 10%。因此,在现实生活中,通常第三位小数并不是很重要,随机森林通常是你最终会做的事情。但对于像厄瓜多尔杂货比赛这样的事情,用随机森林很难得到好的结果,因为有一个巨大的时间序列组件,几乎所有的东西都是这两个大规模高基数的分类变量,即店铺和商品。因此,甚至没有太多的层可以用随机森林,每对店铺之间的差异在不同方面都是不同的,因此有一些事情即使对于随机森林来说也很难得到相对好的结果。

另一个例子是识别数字。你可以用随机森林得到可以接受的结果,但最终,空间结构之间的关系变得重要。你可能想要能够进行像查找边缘或其他计算一样的计算,这些计算会在计算中继续进行。因此,仅仅做一个聪明的最近邻类似于随机森林的方法并不理想。所以对于这样的事情,神经网络是理想的。神经网络被证明对于像厄瓜多尔杂货比赛(即通过店铺和商品预测销售额)和识别数字这样的事情非常有效。所以在这两个事情之间,神经网络和随机森林,我们覆盖了领域。我很长一段时间以来一直没有使用除了这两个方法之外的任何其他方法。在某个时候,我们将学习如何将这两种方法结合起来,因为你可以以非常酷的方式将它们结合起来。

MNIST [6:37]

这是 Adam Geitgey 的一张图片。一张图片只是一堆数字,每个数字都是从 0 到 255,暗的接近 255,亮的接近 0。这是来自 MNIST 数据集的一个数字的例子。MNIST 是一个非常古老的,就像神经网络的 hello world 一样。所以这是一个例子。

这里有 28x28 个像素。如果是彩色的话,会有三个 —— 一个红色的,一个绿色的,一个蓝色的。我们的任务是查看数字数组并弄清楚这是一个棘手的数字 8。我们如何做到这一点?

我们将使用一小部分 FastAI 的内容,并逐渐去除更多,直到最后,我们将从头开始实现自己的神经网络,自己的训练循环,以及自己的矩阵乘法。因此,我们将逐渐深入挖掘更多。

数据 [7:54]

from fastai.imports import *
from fastai.torch_imports import *
from fastai.io import *
path = 'data/mnist/'
import os
os.makedirs(path, exist_ok=True)

MNIST 的数据,这个非常著名的数据集的名称,可以从这里获取:

URL='http://deeplearning.net/data/mnist/'
FILENAME='mnist.pkl.gz'
def load_mnist(filename):
   return pickle.load(gzip.open(filename, 'rb'), encoding='latin-1')

我们在 fastai.io 中有一个叫做 get_data 的东西,它会从 URL 中获取数据并将其存储在你的计算机上,除非它已经存在,否则它将继续使用它。我们这里有一个叫做 load_mnist 的小函数,它简单地加载数据。你会看到它是压缩的,所以我们可以使用 Python 的 gzip 来打开它。然后它也被 pickled,所以如果你有任何类型的 Python 对象,你可以使用这个内置的 Python 库叫做 pickle 来将其转储到你的磁盘上,分享它,稍后加载它,你会得到与开始时相同的 Python 对象。你已经看到了类似于 Pandas 的 feather 格式的东西。Pickle 不仅仅适用于 Pandas,也不仅仅适用于任何东西,它基本上适用于几乎每个 Python 对象。这可能会引发一个问题,为什么我们不为 Pandas 的 DataFrame 使用 pickle。答案是 pickle 适用于几乎每个 Python 对象,但对于几乎任何 Python 对象来说,它可能不是最佳选择。因此,因为我们正在查看具有超过一亿行的 Pandas DataFrames,我们真的希望快速保存,所以 feather 是专门为此目的设计的格式,因此它会非常快速地完成。如果我们尝试 pickle 它,那将需要更长的时间。另外请注意,pickle 文件仅适用于 Python,因此你不能将它们交给其他人,而 feather 文件可以传递。所以值得知道 pickle 的存在,因为如果你有一些字典或某种对象漂浮在周围,你想要稍后保存或发送给其他人,你总是可以将其 pickle 化。所以在这种特殊情况下,deeplearning.net 的人们很友好地提供了一个 pickled 版本。

Pickle 随着时间的推移有些变化,所以像这样的旧 pickle 文件(这是 Python 2 的一个),你实际上必须告诉它是使用这个特定的 Python 2 字符集编码的。但除此之外,Python 2 和 3,你通常可以打开彼此的 pickle 文件。

get_data(URL+FILENAME, path+FILENAME)
((x, y), (x_valid, y_valid), _) = load_mnist(path+FILENAME)

一旦我们加载了这个,我们就像这样加载 ((x, y), (x_valid, y_valid), _)。所以我们这里正在做的事情叫做解构。解构意味着 load_mnist 给我们返回了一个元组的元组。如果在等号的左边有一个元组的元组,我们可以填充所有这些内容。所以我们得到了一个训练数据的元组,一个验证数据的元组,以及一个测试数据的元组。在这种情况下,我不关心测试数据,所以我把它放到一个名为 _ 的变量中,Python 的人们倾向于认为这是一个特殊的变量,我们把要丢弃的东西放进去。它实际上并不特殊,但非常常见。如果你看到有东西被赋值给下划线,那可能意味着你只是要丢弃它。

顺便说一下,在 Jupyter 笔记本中它确实有一个特殊的含义,即你计算的最后一个单元格始终在下划线中可用。但这是一个独立的问题。

然后元组中的第一件事本身就是一个元组,所以我们将把它放入 x 和 y 中作为我们的训练数据,然后第二个元组放入 x 和 y 中作为我们的验证数据。所以这就是所谓的解构,它在许多语言中都很常见。有些语言不支持它,但那些支持的语言,生活会变得更容易。一旦我看到一些新的数据集,我就会查看我得到了什么。它是什么类型?Numpy 数组。它的形状是什么?50,000 x 784。那么因变量呢?那是一个数组,它的形状是 50,000。

type(x), x.shape, type(y), y.shape
'''
(numpy.ndarray, (50000, 784), numpy.ndarray, (50000,))
'''

我们之前看到的 8 的图像不是长度为 784,而是大小为 28 乘以 28。所以这里发生了什么?事实证明,他们只是将第二行连接到第一行,将第三行连接到第二行,将第四行连接到第三行。换句话说,他们将整个 28 乘以 28 展平成一个单一的一维数组。这有意义吗?所以它的大小将是 28²。这绝对不是正常的,所以不要认为你看到的一切都会是这样。大多数时候,当人们分享图像时,他们会将它们分享为 JPEG 或 PNG 格式,你加载它们,你会得到一个漂亮的二维数组。但在这种特殊情况下,出于某种原因,他们拿出来的东西被展平成了 784。这个“展平”这个词在处理张量时非常常见,所以当你展平一个张量时,这意味着你将它转换为比你开始的更低秩的张量。在这种情况下,我们为每个图像开始时是一个秩为 2 的张量(即矩阵),然后我们将每个图像转换为一个秩为 1 的张量(即向量)。所以整体来说,整个东西是一个秩为 2 的张量,而不是一个秩为 3 的张量。

所以只是为了提醒我们这里的行话,这在数学中我们会称之为向量。在计算机科学中,我们会称之为一维数组,但是因为深度学习的人们必须表现得比其他人更聪明,我们不得不称之为秩为 1 的张量。它们基本上意思相同,除非你是物理学家——在这种情况下,这意味着其他事情,你会对深度学习的人们感到非常生气,因为你会说“这不是张量”。所以就是这样。不要责怪我。这只是人们说的话。

所以这要么是一个矩阵,要么是一个二维数组,要么是一个秩为 2 的张量。

一旦我们开始进入三维,我们开始用完数学名字,这就是为什么我们开始友好地说秩为 3 的张量。所以实际上,没有什么特别的关于向量和矩阵使它们比秩为 3 或秩为 4 的张量更重要。所以我尽量不使用向量和矩阵这些术语,因为我真的不认为它们比其他秩的张量更特别。所以习惯将numpy.ndarray (50,000, 784)看作秩为 2 的张量是很好的。

然后是行和列。如果我们是计算机科学人员,我们会称之为零维和一维。但如果我们是深度学习人员,我们会称之为轴零和轴一。然后为了更加混淆,如果你是一个图像人员,列是第一个轴,行是第二个轴。

所以如果你想到电视,1920 乘以 1080——列乘以行。其他人包括深度学习和数学家,行乘以列。所以如果你使用 Python 图像库,你会得到列乘以行;几乎其他所有情况,行乘以列。所以要小心。[一个学生问“为什么他们这样做?”]因为他们讨厌我们,因为他们是坏人,我猜😆

在深度学习中有很多,许多不同领域汇集在一起,如信息论、计算机视觉、统计学、信号处理,最终形成了深度学习中的这种混杂的命名法。通常,每个版本的事物都会被使用,所以今天,我们将听到一些被称为负对数似然或二项式或分类交叉熵的东西,这取决于你来自哪里。我们已经看到了一些被称为独热编码或虚拟变量的东西,这取决于你来自哪里。实际上,这只是相同的概念在不同领域中有点独立地被发明,最终它们找到了通往机器学习的道路,然后我们不知道该如何称呼它们,所以我们称它们为以上所有的东西——就像这样。所以我认为这就是计算机视觉中的行和列发生的事情。

归一化

有这样一个概念,即对数据进行归一化,即减去均值并除以标准差。一个问题给你。通常,归一化数据很重要,这样我们就可以更容易地训练模型。你认为在训练随机森林时,归一化独立变量是否重要呢?

学生:老实说,我不知道为什么我们不需要归一化,我只知道我们不需要。

好的,有人想想为什么吗?真正的关键是,当我们决定在哪里分割时,唯一重要的是顺序。就像唯一重要的是它们是如何排序的,所以如果我们减去均值除以标准差,它们仍然按相同的顺序排序。所以记住当我们实现随机森林时,我们说对它们进行排序,然后完全忽略值。我们只是说现在一次添加一个来自依赖变量的东西。所以随机森林只关心独立变量的排序顺序。它们根本不关心它们的大小。这就是为什么它们对异常值非常免疫的原因,因为它们完全忽略了它是异常值,它们只关心哪个比其他东西更高。所以这是一个重要的概念。它不仅出现在随机森林中。它也出现在一些指标中。例如,ROC 曲线下面积,你会经常遇到,ROC 曲线下面积完全忽略了比例,只关心排序。当我们做树状图时,我们看到了另一种情况。斯皮尔曼相关是一种秩相关——只关心顺序,不关心比例。所以随机森林的许多美好之处之一是我们可以完全忽略许多这些统计分布问题。但是对于深度学习来说不行,因为在深度学习中,我们试图训练一个参数化模型。所以我们需要对数据进行归一化。如果不这样做,那么创建一个有效训练的网络将会更加困难。

所以我们抓取我们训练数据的均值和标准差,减去均值,除以标准差,这给我们一个均值为零,标准差为一的结果。

mean = x.mean()
std = x.std()

x=(x-mean)/std
mean, std, x.mean(), x.std()
'''
(0.13044983, 0.30728981, -3.1638146e-01, 0.99999934)
'''

现在对于我们的验证数据,我们需要使用训练数据的标准差和均值。我们必须以相同的方式对其进行标准化。就像分类变量一样,我们必须确保它们的相同索引映射到随机森林中的相同级别。或者缺失值,我们必须确保在替换缺失值时使用相同的中位数。你需要确保你在训练集中做的任何事情,在测试和验证集中都要完全相同。所以在这里,我减去了训练集的均值并除以训练集的标准差,所以这不是完全是零和一,但它非常接近。总的来说,如果你发现你在验证集或测试集上尝试某些东西,而它比你的训练集差得多得多,那可能是因为你以不一致的方式进行了标准化或编码类别或其他一些不一致的方式。

x_valid = (x_valid-mean)/std
x_valid.mean(), x_valid.std()
'''
(-0.0058509219, 0.99243325)
'''

查看数据[22:03]

让我们来看一下这些数据。所以我们在验证集中有 10,000 张图像,每张图像都是长度为 784 的秩为 1 的张量。

x_valid.shape(10000, 784)

为了显示它,我想将它转换为一个 28x28 的秩为 2 的张量。Numpy 有一个 reshape 函数,它接受一个张量并将其重塑为你请求的任何大小的张量。现在如果你考虑一下,你只需要告诉它有D个轴,你只需要告诉它你想要的D-1个轴,因为最后一个,它可以自己算出来。所以总共,这里一共有 10,000 乘以 784 个数字。所以如果你说我希望我的最后一个轴是 28x28,那么你可以算出(第一个轴)这必须是 10,000,否则它就不会适合。所以如果你放-1,它会说让它尽可能大或尽可能小以使其适合。所以你可以看到,它算出来必须是 10,000。你会看到这种方法在神经网络软件的预处理中经常使用。我可以在这里写 10,000,但我试图养成一种习惯,就是每当我提到输入中有多少项时,我倾向于使用-1,因为这意味着以后我可以使用子样本,这段代码不会出错。如果它是不平衡的,我可以进行一些分层抽样,这段代码不会出错。所以通过在这里使用-1 这种大小,它使得以后的更改更具弹性。这是一个很好的习惯。

x_imgs = np.reshape(x_valid, (-1,28,28)); x_imgs.shape(10000, 28, 28)

能够取张量并重新塑形、改变轴线等等的想法是你需要能够完全不假思索地做到的[23:56]。因为这种情况会经常发生。比如,这里有一个例子。我尝试读取一些图像,它们是扁平化的,我需要将它们重新塑形成一堆矩阵——好的,重新塑形。我用 OpenCV 读取了一些图像,结果发现 OpenCV 按照蓝绿红的顺序排列通道,其他所有的都希望它们是红绿蓝的。我需要颠倒最后一个轴。如何做到这一点?我用 Python 图像库读取了一些图像。它将它们读取为行、列、通道,PyTorch 希望通道、行、列。我该如何转换。所以这些都是你需要能够不假思索地做到的事情,就像立刻就能做到。因为这种情况经常发生,你绝不想坐在那里想了很久。所以确保你在这一周花很多时间练习今天你将看到的所有东西:重新塑形、切片、重新排序维度等等。所以最好的方法是自己创建一些小张量,开始思考,比如我应该尝试什么。

问题:在归一化时,您说许多机器学习算法在数据归一化时表现更好,但您也刚刚说过尺度并不重要?我说对于随机森林来说并不重要。因此,随机森林只会根据顺序输出结果,所以我们喜欢它们。我们喜欢随机森林是因为它们对分布假设不太担心。但我们现在不是在做随机森林,我们在做深度学习。而深度学习确实会在乎尺度。

问题:如果我们有参数化,那么我们应该进行尺度调整。如果我们有非参数化,我们就不需要进行尺度调整?不完全是这样。因为像 k 最近邻是非参数化的,尺度很重要,所以我会说涉及树的事情通常只会在某个点进行分割,所以你可能不在乎尺度,但你可能需要考虑这是一个使用顺序还是使用具体数字的算法。

问题:您能直观解释一下为什么它需要尺度吗,因为这可能会澄清一些问题?直到我们开始进行随机梯度下降时才需要,所以我们会讲到那一点。所以现在,我们只能说相信我的话。

问题:您能解释一下尺度是什么意思吗?因为当我想到尺度时,我认为所有数字应该大致相同大小。在我们进行深度学习时,猫和狗的情况是这样的,你可能有一只小猫和一只大猫,但它仍然知道它们都是猫?我想这是语言被重载的问题之一。在计算机视觉中,当我们对图像进行缩放时,实际上是增加了猫的大小。在这种情况下,我们正在缩放实际的像素值。因此,在这两种情况下,缩放意味着使某物变大和变小。在这种情况下,我们将数字从零到 255,并使它们的平均值为零,标准差为一。

问题:您能解释一下是按列还是按行?一般来说,当您进行缩放时,不仅仅考虑图像,而是输入到机器学习的内容。好的,当然。这有点微妙,但在这种情况下,我只有一个平均值和一个标准差。所以基本上,平均有多少黑色。因此,平均而言,我们有一个平均值和一个标准差跨越所有像素。在计算机视觉中,我们通常会按通道进行操作,所以通常会有一个数字代表红色,一个数字代表绿色,一个数字代表蓝色。一般来说,您需要为每个您希望表现不同的事物准备不同的归一化系数。因此,如果我们像处理一个结构化数据集,其中包括收入、公里数和孩子数量,您需要为这些事物准备三个单独的归一化系数,因为它们是非常不同的事物。因此,在这里有点领域特定。在这种情况下,所有像素都是灰度级别,因此我们只有一个缩放数字。而在其他情况下,如果它们是红色与绿色与蓝色,您需要以不同方式缩放这些通道。

问题:所以我有点难以想象如果在这种情况下不进行归一化会发生什么。我们会讨论到那里。这就是 Yannet 所说的为什么我们要归一化的原因,目前我们正在归一化是因为我说我们必须这样做。当我们开始研究随机梯度下降时,我们基本上会发现,如果你...基本上为了稍微提前一点,我们将会通过一堆权重进行矩阵乘法。我们将以这样一种方式选择这些权重,以便当我们进行矩阵乘法时,我们将尝试保持数字与它们最初的规模相同。这基本上需要我们知道初始数字的规模。因此,如果我们知道它们一直是均值为零,标准差为一,那么就更容易为许多不同类型的输入创建一个单一的神经网络架构。这将是简短的答案。但我们将学到更多关于它的知识,如果在几节课后你仍然不太明白为什么,让我们回过头来讨论,因为这是一个非常有趣的话题。

问题:我试图可视化我们正在使用的坐标轴。所以在绘图中,当你写x_valid.shape时,我们得到了 10,000 乘以 784。这意味着我们带入了那个维度的 10,000 张图片吗?是的,确切地说。问题继续:在下一行,当你选择重塑时,为什么将 28、28 作为 Y 或 Z 坐标?或者它们按照那个顺序有什么原因吗?

是的,有的。几乎所有的神经网络库都假设第一个轴相当于一行。就像一个单独的东西,它是一个句子或一幅图像或销售示例。所以我希望每个图像都是第一个轴的单独项目。然后,这样就为图像的行和列留下了另外两个轴。这是完全标准的。我认为我从来没有见过一个不是这样工作的库。

问题:在归一化验证数据时,我看到你使用了 x 数据的均值和标准差(即训练数据)。我们不应该使用验证数据的均值和标准差吗?不,因为你看,那样的话,你将使用不同的数字对验证集进行归一化,因此现在这个像素的值在验证集中的含义与在训练集中的含义不同。这就好像如果我们将一周的天数编码,使得星期一在训练集中是 1,在验证集中是 0。现在我们有了两组不同的数据,其中相同的数字具有不同的含义。

让我举个例子。假设我们正在处理全彩色图像,我们的训练集包含绿色青蛙、绿色蛇和灰色大象。我们正在训练以弄清楚它们各自是什么。然后我们使用每个通道的均值进行了归一化。然后我们有一个验证集和一个测试集,里面只有绿色青蛙和绿色蛇。如果我们使用验证集的统计数据进行归一化,我们最终会说平均而言都是绿色。所以我们会去除所有的绿色,因此我们现在将无法有效地识别绿色青蛙和绿色蛇。所以我们实际上希望使用我们训练时使用的相同的归一化系数。对于那些正在学习深度学习课程的人,我们实际上做得更多。当我们使用预训练的网络时,我们必须使用原始作者训练时使用的相同的归一化系数。因此,这个数字在你使用它的每个数据集中都需要有一致的含义。这意味着当你查看测试集时,你需要根据训练集的均值和标准差对测试集进行归一化。

show(x_imgs[0], y_valid[0])

所以验证 y 值只是一个长度为 10,000 的秩为 1 的张量。记住这是一种奇怪的 Python 事情,一个包含这一个东西的元组需要一个尾随逗号。所以这是一个长度为 10,000 的秩为 1 的张量。

y_valid.shape(10000,)

这里有一个例子。只是一个数字 3。这就是我们的标签。

y_valid[0]
'''
3
'''

切片

这里是另一件你需要能够做到熟练的事情。切片成一个张量。在这种情况下,我们用 0 切片到第一个轴,这意味着我们正在获取第一个切片。因为这是一个单一数字,这将减少张量的秩一次。它将把一个 3 维张量变成一个 2 维张量。所以你可以看到,这现在只是一个矩阵。然后我们将抓取 10 到 14 行,10 到 14 列,这就是它。所以这是你需要非常熟练的事情——抓取片段,查看数字,查看图片。

x_imgs[0,10:15,10:15]
'''
array([[-0.42452, -0.42452, -0.42452, -0.42452,  0.17294],
       [-0.42452, -0.42452, -0.42452,  0.78312,  2.43567],
       [-0.42452, -0.27197,  1.20261,  2.77889,  2.80432],
       [-0.42452,  1.76194,  2.80432,  2.80432,  1.73651],
       [-0.42452,  2.20685,  2.80432,  2.80432,  0.40176]], dtype=float32)
'''

这里是第一张图像的一小部分的例子。所以你应该习惯这样的想法,如果你在处理像图片或音频这样的东西,这是你的大脑真的很擅长解释的东西。所以尽可能经常展示你正在做的事情的图片。但也记住在幕后它们是数字,所以如果出现奇怪的情况,打印出一些实际的数字。你可能会发现其中一些变成了无穷大,或者它们全都是零,或者其他什么。所以在探索数据时利用这个交互式环境。

问题:只是一个快速的语义问题。为什么当它是一个秩为 3 的张量时,它被存储为 XYZ 而不是对我来说,将它存储为 2D 张量的列表会更有意义?它不是存储为任何一种。所以让我们把这看作是一个 3D。这里是一个 3D。所以一个 3D 张量被格式化为基本上显示一系列 2D 张量。

问题:但为什么不像x_imgs[0][10:15][10:15]那样?哦,因为那有不同的含义。这就是张量和不规则数组之间的区别。所以基本上如果你做类似a[2][3]这样的事情,那就是说取第二个列表项,然后从中获取第三个列表项。所以当我们有一个叫做不规则数组的东西时,我们倾向于使用这种方式,其中每个子数组的长度可能不同。而在其他情况下,我们有一个三维的单一对象。所以我们试图说我们想要它的哪一小部分。所以这个想法是一个单一的切片对象去抓取那一部分出来。

show(x_imgs[0,10:15,10:15])

plots(x_imgs[:8], titles=y_valid[:8])

这里有一些图像及其标签的例子。这种东西,你希望能够用 matplotlib 很快地完成。这将帮助你很多,这样你就可以看看 Rachel 在写plots时写的东西。我们可以使用 add_subplot 来创建这些小的独立图。你需要知道imshow是我们如何将一个 numpy 数组绘制成图片的。然后我们还添加了顶部的标题。所以就是这样。

def show(img, title=None):
    plt.imshow(img, cmap="gray")
    if title is not None: plt.title(title)
def plots(ims, figsize=(12,6), rows=2, titles=None):
    f = plt.figure(figsize=figsize)
    cols = len(ims)//rows
    for i in range(len(ims)):
        sp = f.add_subplot(rows, cols, i+1)
        sp.axis('Off')
        if titles is not None: 
            sp.set_title(titles[i], fontsize=16)
        plt.imshow(ims[i], cmap='gray')

神经网络

让我们拿这些数据来尝试构建一个神经网络。对于那些已经在进行深度学习的人来说,这将是很多复习。神经网络实际上只是一个特定的数学函数或数学函数类,但它是一个非常重要的类,因为它具有支持所谓的通用逼近定理的属性。这意味着神经网络可以任意接近地逼近任何其他函数。换句话说,理论上,只要我们使它足够大,它就可以做任何事情。这与只能执行一种特定功能的函数如 3x + 5 非常不同。或者只能表示不同斜率的线条上下移动不同量的函数类ax + b。甚至函数ax² + bx + c + sin d也只能表示一个非常具体的关系子集。然而,神经网络是一个可以任意接近地表示任何其他函数的函数。

因此,我们要做的是学习如何取一个函数,比如ax + b,并学习如何找到其参数(在这种情况下为ab),使其尽可能接近一组数据。这里展示了我们将在深度学习课程中查看的笔记本中的示例,基本上展示了当我们使用称为随机梯度下降来尝试设置ab时会发生什么。基本上,我们将从一个随机的a和一个随机的b开始,然后我们基本上要弄清楚我是否需要增加或减少a来使线条接近点?我是否需要增加或减少b来使线条接近点?然后只需多次增加和减少ab。这就是我们要做的,为了回答是否需要增加或减少ab的问题,我们将取导数。因此,函数关于ab的导数告诉我们当我们改变ab时该函数将如何变化。这基本上就是我们要做的。但我们不会仅仅从一条线开始,想法是我们要逐步建立一个实际上具有神经网络的模型,因此这将是完全相同的想法,但由于它是一个无限灵活的函数,我们将能够使用这个完全相同的技术来适应任意复杂的关系。这基本上就是这个想法。

那么你需要知道的是,神经网络实际上是一件非常简单的事情。神经网络实际上是一个以输入为向量的东西,通过该向量进行矩阵乘积。因此,如果向量的大小为r,矩阵为r乘以c,则矩阵乘积将输出大小为c的结果。然后我们进行一种称为非线性的操作,基本上是我们要丢弃所有负值(即max(0, x))。然后我们将通过另一个矩阵乘法,再通过另一个max(0, x),再通过另一个矩阵乘法,直到最终得到我们想要的单个向量。换句话说,我们神经网络的每个阶段,关键的事情是进行矩阵乘法,换句话说,是一个线性函数。因此,基本上,深度学习中大部分的计算是大量的线性函数,但在每个线性函数之间,我们将用零替换负数。

问题:为什么我们要丢弃负数?我们将看到。简短的答案是,如果你对一个线性函数应用另一个线性函数,它仍然只是一个线性函数。所以这完全没有用。但是如果你丢弃负数,那实际上是一个非线性转换。结果表明,如果你对我们丢弃的负数应用一个线性函数,然后将其应用于创建神经网络的线性函数,结果就是这个东西可以任意接近任何其他函数。所以这个微小的差异实际上产生了很大的不同。如果你对此感兴趣,请查看我们涵盖这一内容的深度学习视频,因为我实际上展示了一个直观的证明,不是我创造的,而是 Michael Nielsen 创造的。或者,如果你想直接跳转到他的网站,你可以访问 Michael Nielsen 的通用逼近定理,他有一个非常好的步骤指南,其中包含许多动画,您可以看到为什么这样运作。

为什么你(是的,你)应该写博客

我觉得在互联网上开始技术写作最困难的事情就是发布你的第一篇文章。在这篇博客中,Rachel 实际上说她给年轻自己的最好建议是尽早开始写博客。她列举了为什么你应该这样做的原因,她写博客的一些地方对她和她的职业都很有帮助,以及一些如何开始的建议。

我记得当我第一次建议 Rachel 考虑写博客时,因为她有很多有趣的事情要说,起初她对自己能写博客这个想法感到有些惊讶。现在人们在会议上走过来对我们说:“你是 Rachel Thomas!我喜欢你的文章!”所以我看到了从“哇,我能写博客吗?”到被认为是一位优秀的技术作者的过渡。所以如果你仍然需要说服,或者想知道如何开始,请查看这篇文章。因为第一篇是最难的,也许你的第一篇应该是对你来说非常容易写的东西。所以可以是这样的,这是我们机器学习课程第 3 课的前 15 分钟的摘要 - 这是有趣的地方,这是我们学到的东西。或者可以是这样的,这是我如何使用随机森林解决实习中的特定问题的摘要。

我经常被问到“哦,我的实习,我的组织,我们有敏感的商业数据” - 没关系。只需找到另一个数据集,然后在那个数据集上进行操作以展示示例,或者对所有值进行匿名化并更改变量的名称等。您可以与雇主或实习合作伙伴交谈,以确保他们对您写的任何内容感到舒适。总的来说,人们喜欢他们的实习生写博客,讲述他们正在做的事情,因为这让他们看起来很酷。就像“嘿,我是在这家公司实习的,我写了这篇关于我所做的酷分析的文章”,然后其他人会说哇,这看起来是一家很棒的公司。一般来说,你会发现人们非常支持。此外,有很多数据集可用,所以即使您不能以您正在进行的工作为基础,您也肯定可以找到类似的东西。

PyTorch

我们将开始构建我们的神经网络。我们将使用一个叫做 PyTorch 的东西来构建它。PyTorch 是一个基本上看起来很像 numpy 的库。但是当你用 PyTorch 创建一些代码时,你可以在 GPU 上运行它而不是 CPU。所以 GPU 基本上可能会比你为 CPU 编写的代码快至少一个数量级,可能是数百倍,特别是涉及大量线性代数的东西。所以在深度学习、神经网络中,如果你没有 GPU,你可以在 CPU 上做,但会非常慢。Mac 没有我们可以用于这个的 GPU,因为我们需要 NVIDIA GPU。我实际上更希望我们可以使用你的 Mac,因为竞争是很好的。但 NVIDIA 确实是第一个创建支持通用图形编程单元(GPGPU)的 GPU 的公司,换句话说,这意味着使用 GPU 进行除了玩电脑游戏以外的事情。他们创建了一个叫做 CUDA 的框架。这是一个非常好的框架,在深度学习中几乎被普遍使用。如果你没有 NVIDIA GPU,你就不能使用它,目前没有任何 Mac 有 NVIDIA GPU。任何类型的大多数笔记本电脑都没有 NVIDIA GPU。如果你有兴趣在笔记本电脑上进行深度学习,好消息是你需要购买一台非常适合玩电脑游戏的笔记本电脑。有一个地方叫做XOTIC PC Gaming Laptops,你可以去那里购买一台适合进行深度学习的优秀笔记本电脑。你可以告诉你的父母,你需要这笔钱来进行深度学习。你通常会发现一大堆带有 predator 和 viper 等名称的笔记本电脑,上面有机器人和其他东西的图片。无论如何,话虽如此,我不认识很多人在笔记本电脑上做很多深度学习的。大多数人会登录到云环境中。我知道的最容易使用的是Crestle。使用 Crestle,你基本上可以注册,然后立即得到的第一件事就是你被直接投入到一个 jupyter 笔记本中。它支持 GPU,每小时 60 美分,所有 Fast AI 库和数据都已经可用。这使得生活变得非常容易。它比使用亚马逊网络服务选项的 AWS less 灵活,某些方面也不那么快。它的成本稍微高一点,每小时 90 美分而不是 60 美分。但很可能你的雇主已经在使用它,了解一下也是好的。他们在 GPU 周围有更多不同的选择,这是一个不错的选择。如果你是学生,可以搜索 github 学生包,你可以立即获得 150 美元的信用额度。所以这是一个开始的好方法。

问题:我想知道你对英特尔最近发布的一种提升常规软件包的开源方式的看法,他们声称这相当于使用底层 GPU。在你的 CPU 上,如果你使用他们的提升软件包,你可以获得相同的性能。实际上,英特尔制作了一些很棒的数值编程库,特别是这个叫做 MKL 的库,矩阵核心库。它们确实比不使用这些库更快,但如果你看一下性能随时间变化的图表,GPU 在过去 10 年中一直保持着大约每秒 10 次浮点运算,现在也是如此,而且通常价格只有相同性能的 CPU 的 1/5。因此,几乎所有进行深度学习的人基本上都是在 NVIDIA GPU 上进行的,因此使用除了 NVIDIA GPU 之外的任何东西目前都非常烦人——更慢、更昂贵、更烦人。我真的希望在这个领域尤其是在 AMD GPU 周围会有更多的活动,但 AMD 确实需要多年的追赶,所以可能需要一段时间。

评论:我只是想指出,你也可以购买一个 GPU 扩展器连接到笔记本电脑,这可能是在购买新笔记本电脑或 AWS 之前的第一步解决方案[52:46]。是的,我认为大约$300 左右,你可以购买一个插入到你的 Thunderbolt 端口的东西,如果你有一台 Mac,然后再花$500 或$600,你可以购买一个 GPU 插入其中。话虽如此,大约$1000,你就可以创建一个相当不错的基于 GPU 的台式机,所以如果你在考虑这个,Fast AI 论坛有很多帖子,人们在特定价格点上互相帮助。

总之,我建议一开始使用 Crestle,然后当你准备好投入一些额外的时间时,使用 AWS。要使用 AWS,当你到达那里时,去 EC2。AWS 上有很多东西,EC2 是我们可以按小时租用计算机的部分。

现在,我们需要一个基于 GPU 的实例。不幸的是,当你第一次注册 AWS 时,他们不会给你访问权限。所以去到 Limits(左上角)。

我们将使用的主要 GPU 实例称为 p2。所以滚动到 p2.xlarge,你需要确保数字不是零。如果你刚刚注册了一个新账户,它可能是零,这意味着你将无法创建一个。所以你必须去“请求限制增加”,其中的诀窍是当它问你为什么要增加限制时,输入“fast.ai”,因为 AWS 知道要留意,他们知道 fast.ai 的人是好人,所以他们会很快处理。通常需要一两天的时间。

所以一旦你收到批准使用 p2 实例的邮件,你就可以回到这里并点击 Launch Instance:

我们基本上设置了一个拥有一切你需要的东西。所以如果你点击 Community AMIs,AMI 是 Amazon Machine Image 的缩写——它基本上是一个完全设置好的计算机。所以如果你输入 fastai(连在一起),你会在这里找到 fastai DL part 1 v2 for p2。所以一切都准备就绪。

所以如果你点击 Select,它会问你想要什么样的计算机。所以我们必须说我想要一个“GPU 计算”类型,具体来说我想要 p2.xlarge。然后你可以点击“Review and Launch”。

我假设你已经知道如何处理 SSH 密钥和所有这些东西。如果你不知道,可以查看我们在线的入门教程和工作坊视频,或者在网上搜索 SSH 密钥。这是一个非常重要的技能。所以希望你通过所有这些,你可以在 GPU 上运行 Fast AI repo。如果你使用 Crestle,只需cd fastai2,repo 已经在那里,git pull。AWS,cd fastai,repo 已经在那里,git pull。如果是你自己的电脑,你只需要git clone,然后就可以开始了。

PyTorch 是预安装的,所以 PyTorch 基本上意味着我们可以编写看起来很像 numpy 的代码,但在 GPU 上运行速度非常快。其次,由于我们需要知道参数如何移动以改善我们的损失,我们需要知道函数的导数。PyTorch 有这个神奇的功能,任何你使用 PyTorch 库编写的代码,它都可以自动为你计算导数。所以我们在这门课程中不会涉及任何微积分。我在我的课程中从来没有看过微积分,也从来没有在我的工作中计算过导数,因为这些都是由库来完成的。只要你写好 Python 代码,导数就会被计算出来。所以成为一个有效的从业者,你真正需要了解的微积分只是导数是什么意思。你还需要知道链式法则,我们会讲到。

PyTorch 中的逻辑回归神经网络[57:45]

好的,所以我们将从上到下开始,创建一个神经网络,并假设很多东西。然后逐渐我们将深入研究每个部分。所以要创建神经网络,我们需要导入 PyTorch 神经网络库。有趣的是,PyTorch 并不叫 PyTorch——它叫 torch。所以torch.nn是负责神经网络的 PyTorch 子部分。我们将称之为 nn。我们将从 Fast AI 中导入一些部分,以使我们的生活变得更容易。

from fastai.metrics import *
from fastai.model import *
from fastai.dataset import *

import torch.nn as nn

这里是如何在 PyTorch 中创建神经网络的。最简单的神经网络,你说 Sequential。Sequential 意味着我现在要给你一个我想要在我的神经网络中的层的列表。所以在这种情况下,我的列表中有两个东西。第一件事说我想要一个线性层。现在线性层基本上会执行y = ax + b,但是矩阵矩阵相乘,而不是单变量。所以它基本上会执行一个矩阵乘积。矩阵乘积的输入将是一个长度为 28 乘以 28 的向量,因为这是我们有多少像素,输出需要是大小为 10(我们稍后会讨论原因)。目前这就是我们如何定义一个线性层。然后,我们将详细讨论这一点,但是几乎每个神经网络中的线性层之后都必须有一个非线性。然后我们将在一会儿学习这个特定的非线性,它被称为 softmax,如果你已经学过深度学习课程,你已经见过这个。这就是我们如何定义一个神经网络。这是一个两层神经网络。

net = nn.Sequential(
    nn.Linear(28*28, 10),
    nn.LogSoftmax()
).cuda()

还有一种隐含的额外第一层,即输入层,但是使用 PyTorch,你不必显式提及输入。但通常我们在概念上认为输入图像也是一种层。因为我们正在相当手动地进行操作,使用 PyTorch 我们没有利用 Fast AI 中构建你的东西的任何便利性,我们必须然后写.cuda(),这告诉 PyTorch 将这个神经网络复制到 GPU 上。从现在开始,该网络实际上将在 GPU 上运行。如果我们没有说,它将在 CPU 上运行。这给我们返回了一个神经网络——一个非常简单的神经网络。

数据[1:00:22]

然后我们将尝试将神经网络拟合到一些数据上。所以我们需要一些数据。Fast AI 有一个 ModelData 对象的概念,基本上是将训练数据、验证数据和可选的测试数据包装在一起的东西。所以要创建一个 ModelData 对象,你可以这样说:

  • 我想创建一些图像分类器数据(ImageClassifierData

  • 我将从一些数组中获取它(from_arrays

  • 这是我将保存任何临时文件的路径(path

  • 这是我的训练数据数组((x,y)

  • 这是我的验证数据数组((x_valid,y_valid)

这只是返回一个将所有这些包装起来的对象。所以我们将能够拟合到这些数据上。

md = ImageClassifierData.from_arrays(path, (x,y),(x_valid, y_valid))

现在我们有了一个神经网络和一些数据,我们将在一会儿回到这里,但基本上我们说我们想使用什么损失函数,想使用什么优化器,然后说拟合[1:01:07]。

loss=nn.NLLLoss()
metrics=[accuracy]
opt=optim.Adam(net.parameters())

我们说将这个网络net拟合到这个数据md上,每次遍历每个图像一次(n_epochs),使用这个损失函数loss,这个优化器opt,并打印出这些指标metrics

fit(net, md, n_epochs=1, crit=loss, opt=opt, metrics=metrics)

这里说这是 91.8%的准确率。所以这就是最简单的神经网络。它正在创建一个矩阵乘法,然后是一个非线性,它试图找到这个矩阵的值(nn.Linear(28*28, 10)),基本上是尽可能好地拟合数据,最终预测这是 1,这是 9,这是 3。

损失函数[1:02:08]

所以我们需要一些关于“尽可能好”的定义。那个东西的一般术语叫做损失函数。所以损失函数是一个函数,如果这个函数更低,那么就更好。就像随机森林一样,我们有信息增益的概念,我们得选择用什么函数来定义信息增益,我们主要看的是均方根误差。大多数机器学习算法我们称之为类似于“损失”的东西。所以损失是我们如何评分我们有多好的东西。最终,我们将计算损失对我们正在乘以的权重矩阵的导数,以找出如何更新它。

我们将使用一种称为负对数似然损失(NLLLoss)的东西。负对数似然损失也被称为交叉熵,它们实际上是一样的。有两个版本,一个称为二元交叉熵或二元负对数似然,另一个称为分类交叉熵。它们是一样的,一个是当你只有一个零或一个依赖时,另一个是如果你有猫、狗、飞机或马,或者 0、1、到 9 等等。所以这里我们有交叉熵的二元版本:

def binary_loss(y, p):
    return np.mean(-(y * np.log(p) + (1-y)*np.log(1-p)))

所以这里的定义是-(y * np.log(p) + (1-y)*np.log(1-p))。我认为理解这个定义的最简单方法可能是看一个例子。假设我们试图预测猫和狗。1 代表猫,0 代表狗。所以这里,我们有猫、狗、狗、猫([1, 0, 0, 1])。这是我们的预测([0.9, 0.1, 0.2, 0.8])。我们说 90%确定是猫,90%确定是狗,80%确定是狗,80%确定是猫。所以我们可以通过调用我们的函数来计算二元交叉熵。

对于第一个,我们有y=1p=0.9(即(1 * np.log(0.9),因为第二项被跳过了)。对于第二个,第一部分被跳过(乘以 0),第二部分将是(1-0)*np.log(0.9)。换句话说,这个的第一部分和第二部分将给出完全相同的数字,这是有道理的,因为第一个我们说我们对是猫 90%有信心,而实际上是,第二个我们说我们对是狗 90%有信心,而实际上是。所以在每种情况下,损失都来自于我们本可以更有信心。所以如果我们说我们 100%有信心,损失将为零。

acts = np.array([1, 0, 0, 1])
preds = np.array([0.9, 0.1, 0.2, 0.8])
binary_loss(acts, preds)
'''
0.164252033486018
'''

所以让我们在 Excel 中看一下。从顶部行开始:

  1. 我们的预测

  2. 实际/目标值

  3. 1 减去实际/目标值

  4. 我们的预测的对数

  5. 我们的预测的对数的 1 减

  6. 总和

如果你仔细想一想,我希望你在这一周内考虑一下,你可以用一个 if 语句来替换这个(np.mean(-(y * np.log(p) + (1-y)*np.log(1-p)))),而不是 y,因为 y 总是 1 或 0,所以它只会使用np.log(p)(np.log(1-p)中的一个。所以你可以用一个 if 语句来替换这个。所以我希望你在这一周内尝试用一个 if 语句来重写这个。

然后看看你是否能将其扩展为分类交叉熵。所以分类交叉熵的工作方式是这样的。假设我们试图预测 3、6、7、2。

所以如果我们试图预测 3,而实际上预测了 5,或者试图预测 3,却意外地预测了 9。5 而不是 3 并不比 9 而不是 3 更好。所以我们实际上不会说实际数字有多远。我们会用不同的方式表达它。换句话说,如果我们试图预测猫、狗、马和飞机。猫和马之间有多远?所以我们会稍微不同地表达这些。与其把它看作是一个 3,不如把它看作是一个在第三个位置上有一个 1 的向量:

不要把它看作是一个 6,让我们把它看作是一个零向量,第 6 个位置是 1。换句话说,独热编码。所以让我们对我们的因变量进行独热编码。这样现在,我们不再试图预测一个数字,而是预测十个数字。让我们预测它是 0 的概率,它是 1 的概率,依此类推。

所以让我们假设我们正在预测 2,这里是我们的分类交叉熵[1:07:50]。所以它只是在说,这个预测是否正确,有多大偏差,依此类推,对每一个进行计算,然后将它们全部加起来。分类交叉熵与二元交叉熵是相同的。我们只需要将它们加起来跨越所有的类别。

所以尝试将 Python 中的二元交叉熵函数转换为 Python 中的分类交叉熵。也许创建带有 if 语句的版本和带有求和和乘积的版本。

这就是为什么在我们的 PyTorch 中,我们将这个矩阵的输出维度设置为 10,因为当我们将一个有 10 列的矩阵相乘时,我们将得到一个长度为 10 的结果,这正是我们想要的[1:08:35]。我们想要有 10 个预测。

这就是我们正在使用的损失函数。然后我们可以拟合模型,它会遍历每个图像,这么多次(epochs)。所以在这种情况下,它只是查看每个图像一次,并且会根据这些梯度稍微更新那个权重矩阵中的值。

所以一旦我们训练好了,我们就可以用这个模型(net)在验证集(md.val_dl)上进行predict

preds = predict(net, md.val_dl)

现在这会输出一个 10,000 乘以 10 的东西。我们有 10,000 张图像进行验证,实际上每张图像进行 10 次预测。换句话说,每一行都是它是 0 的概率,它是 1 的概率,它是 2 的概率,依此类推。

preds.shape
'''
(10000, 10)
'''

Argmax [1:10:22]

在数学中,有一个我们经常做的操作叫做argmax。当我说它很常见时,很有趣的是在高中,我从来没有见过 argmax。大一,我也从来没有见过 argmax。但不知何故,大学毕业后,一切都与 argmax 有关。所以有些事情在学校里似乎并没有真正教,但实际上它非常关键。argmax 既是数学中的一个东西(它只是完整地写出argmax),它在 numpy 中,在 PyTorch 中,非常重要。它的作用是让我们拿这些预测数组,然后在给定的轴上(axis=1 - 记住,轴 1 是列),就像 Chis 所说的,对于每一行的 10 个预测,让我们找出哪个预测值最高,然后返回不是那个值(如果只是说 max,它会返回值),argmax 返回值的索引。所以通过说argmax(axis=1),它将返回实际上是数字本身的索引。所以让我们取前 5 个:

preds.argmax(axis=1)[:5]
'''
array([3, 8, 6, 9, 6])
'''

这就是我们如何将我们的概率转换回预测的方法。我们保存下来并称之为preds。然后我们可以说preds何时等于真实值。这将返回一个布尔数组,我们可以将其视为 1 和 0,一堆 1 和 0 的平均值就是平均值。这给了我们 91.8%的准确率。

preds = preds.argmax(1)
np.mean(preds == y_valid)
'''
0.91820000000000002
'''

所以你想要能够复制你看到的数字,这里就是。这里是我们的 91.8%。

所以当我们训练这个模型时,最后一件事告诉我们的是我们要求的任何指标,我们要求的是准确率。然后在此之前,我们得到了训练集的损失。损失又是我们要求的任何损失(nn.NLLLoss()),第二件事是验证集的损失。PyTorch 不使用损失这个词,他们使用准则这个词。所以你会在这里看到crit,这就是准则等于损失。这就是我们想要使用的损失函数,他们称之为准则。同样的事情。所以np.mean(preds == y_valid)就是我们如何重新创建准确率的方法。

plots(x_imgs[:8], titles=preds[:8])

因此,现在我们可以继续绘制八幅图像以及它们的预测。对于我们预测错误的那些,您可以看到它们为什么错误。数字 4 的图像非常接近数字 9。它只是在顶部少了一个小交叉。数字 3 非常接近数字 5。它在顶部有一点额外的部分。所以我们已经开始了。到目前为止,我们实际上还没有创建一个深度神经网络。我们实际上只有一个层。因此,我们实际上所做的是创建了一个逻辑回归。逻辑回归就是我们刚刚构建的内容,您可以尝试使用 sklearn 的逻辑回归包来复制这个过程。当我这样做时,我得到了类似的准确性,但这个版本运行得更快,因为它在 GPU 上运行,而 sklearn 在 CPU 上运行。因此,即使对于像逻辑回归这样的东西,我们也可以使用 PyTorch 非常快速地实现它。

问题:当我们创建我们的网络时,我们必须执行.cuda()。如果不这样做会有什么后果?它只是不会快速运行。它将在 CPU 上运行。

问题:为什么我们必须先进行线性操作,然后再进行非线性操作?简短的答案是因为这是通用逼近定理所说的结构,可以为任何函数形式提供任意精确的函数。长答案是通用逼近定理为何有效的细节。另一个简短答案是,这就是神经网络的定义。因此,神经网络的定义是一个线性层,后跟一个激活函数,再后跟一个线性层,再后跟一个激活函数,依此类推。我们在深度学习课程中会更详细地讨论这一点,但就此目的而言,知道它有效就足够了。到目前为止,当然,我们实际上还没有构建一个深度神经网络。我们只是构建了一个逻辑回归。因此,在这一点上,如果你考虑一下,我们所做的就是将每个输入像素乘以每个可能结果的权重。因此,我们基本上是在说,平均而言,数字 1 具有这些像素点亮。数字 2 具有这些像素点亮。这就是为什么它不是非常准确的原因。这不是现实生活中数字识别的工作方式。但到目前为止,这就是我们构建的全部内容。

问题:所以你一直在说这个通用逼近定理。你有定义过吗?是的,但让我们再次讨论一下,因为这值得谈论。因此,Michael Nielsen 有一个名为神经网络与深度学习的优秀网站。他的第四章现在实际上很有名,其中他通过演示神经网络可以以足够大的规模逼近任何其他函数,只要它足够大,来详细介绍这一点。我们在深度学习课程中详细讨论了这一点,但基本的诀窍是,他展示了通过几个不同的数字,您基本上可以使这些事物创建小盒子,您可以将盒子上下移动,您可以将它们移动,您可以将它们连接在一起,最终基本上可以创建像塔一样的连接,您可以用来逼近任何类型的表面。

因此,这基本上就是诀窍。因此,我们所需要做的就是,鉴于此,找到神经网络中每个线性函数的参数。因此,找到每个矩阵中的权重。到目前为止,我们只有一个矩阵,我们只是构建了一个简单的逻辑回归。

问题:我只是想确认一下,当你展示被错误分类的图像的例子时,它们看起来是矩形的,所以只是在渲染时,像素被不同地缩放了吗?它们是 28 乘 28 的。我认为它们看起来是矩形的,因为它们顶部有标题。Matplotlib 经常会调整它认为的黑色与白色以及具有不同大小轴等的东西。因此,有时你必须小心一点。

自己定义逻辑回归

希望现在这会更有意义,因为我们要深入一层,定义逻辑回归,而不使用nn.Sequentialnn.Linearnn.LogSoftmax。因此,我们将几乎所有的层定义都从头开始做。为了做到这一点,我们将不得不定义一个 PyTorch 模块。PyTorch 模块基本上是一个神经网络或神经网络中的一层,这实际上是一个强大的概念。基本上,任何可以像神经网络一样行为的东西本身可以成为另一个神经网络的一部分。这就是我们如何构建特别强大的架构,结合了许多其他部分。

def get_weights(*dims): 
    return nn.Parameter(torch.randn(dims)/dims[0])
def softmax(x): 
    return torch.exp(x)/(torch.exp(x).sum(dim=1)[:,None])

class LogReg(nn.Module):
    def __init__(self):
        super().__init__()
        self.l1_w = get_weights(28*28, 10)  # Layer 1 weights
        self.l1_b = get_weights(10)         # Layer 1 bias

    def forward(self, x):
        x = x.view(x.size(0), -1)
        x = (x @ self.l1_w) + self.l1_b  # Linear Layer
        x = torch.log(softmax(x)) # Non-linear (LogSoftmax) Layer
        return x

因此,要创建一个 PyTorch 模块,只需创建一个 Python 类,但它必须继承自nn.Module。因此,除了继承之外,这是我们已经在面向对象中看到的所有概念。基本上,如果你在这里(在类名后面)放入括号中的内容,意味着我们的类会免费获得这个类的所有功能。这被称为子类化。因此,我们将获得 PyTorch 作者提供的神经网络模块的所有功能,然后我们将添加额外的功能。当你创建一个子类时,有一件重要的事情你需要记住,那就是当你初始化你的类时,你首先必须初始化超类。因此,超类是nn.Module。因此,在你开始添加你的部分之前,必须先构建nn.Module。这就像你可以复制并粘贴到你的每一个模块中的东西。你只需说super().__init__()。这意味着首先构造超类。

这样做之后,我们现在可以定义我们的权重和偏差。我们的权重是权重矩阵。这是我们将要用来乘以我们的数据的实际矩阵。正如我们讨论过的,它将有 28 乘 28 行和 10 列。这是因为如果我们取一个我们已经展平成一个 28 乘 28 长度向量的图像,然后我们可以将它乘以这个权重矩阵,得到一个长度为 10 的向量,然后我们可以将其视为一组预测。这就是我们的权重矩阵。现在问题是我们不只是想要y = ax。我们想要y = ax + b。因此,在神经网络中,+ b被称为偏差。因此,除了定义权重,我们还将定义偏差。由于这个get_weights(28*28, 10)将为每个图像输出长度为 10 的东西。这意味着我们需要创建一个长度为 10 的向量作为我们的偏差。换句话说,对于每个 0、1、2、3 直到 9,我们将有一个不同的加b。因此,我们有我们的数据矩阵,它的长度是 10,000 乘以 28 乘以 28。然后我们有我们的权重矩阵,它是 28 乘以 28 乘以 10。因此,如果我们将它们相乘,我们将得到一个大小为 10,000 乘以 10 的东西。

然后我们想要添加我们的偏差,如下所示:

我们以后会学到更多关于这个的知识,但是当我们像这样添加一个向量时,基本上它会被添加到每一行。因此,那个偏差将被添加到每一行。因此,我们首先定义这些。为了定义它们,我们创建了一个名为get_weights的小函数,它基本上只是创建一些正态分布的随机数。torch.randn返回一个填充有正态分布随机数的张量。

然而,我们必须要小心。当我们进行深度学习时,比如以后添加更多的线性层。想象一下,如果我们有一个矩阵,平均倾向于增加我们输入的大小。如果我们将其乘以许多相同大小的矩阵,它会使数字变得越来越大,指数级增长。或者如果我们让它们变小一点呢?它会使它们变得越来越小,指数级减小。因为深度网络应用了许多线性层,如果平均而言它们导致的结果比起始值稍微大一点或稍微小一点,那么它将指数级地放大这种差异。因此,我们需要确保权重矩阵的大小适当,使得输入到它的(更具体地说,输入的均值)不会改变。

事实证明,如果你使用正态分布的随机数并除以权重矩阵中的行数,这种随机初始化可以保持你的数字在大约正确的范围内。因此,这个想法是,如果你做过线性代数,基本上如果第一个特征值大于 1 或小于 1,它会导致梯度变得越来越大或越来越小。这就是梯度爆炸。我们将在深度学习课程中更多地讨论这个问题,但如果你感兴趣,你可以查看Kaiming He 初始化,并阅读有关这个概念的所有内容,但现在,知道如果你使用这种类型的随机数生成(即torch.randn(dims)/dims[0]),你将得到行为良好的随机数。你将从均值为 0 标准差为 1 的输入开始。一旦你通过这组随机数,你仍然会得到大约均值为 0 标准差为 1 的东西。这基本上就是目标。

PyTorch 的一个好处是你可以玩弄这些东西[1:25:44]。所以试一试。每当你看到一个函数被使用时,运行它并查看一下。所以你会发现,它看起来很像 numpy,但它不返回一个 numpy 数组。它返回一个张量。

事实上,现在我在进行 GPU 编程。

调用.cuda(),现在它在 GPU 上运行。

我在 GPU 上非常快地将那个矩阵乘以 3!这就是我们如何使用 PyTorch 进行 GPU 编程。

正如我们所说,我们创建了一个 28*28 乘以 10 的权重矩阵,另一个只是 10 的秩 1 偏差[1:26:29]。我们必须将它们设为参数。这基本上告诉 PyTorch 在执行 SGD 时要更新哪些内容。这是一个非常微小的技术细节。

创建了权重矩阵后,我们定义了一个名为forward的特殊方法。这是一个特殊的方法,而在 PyTorch 中,名称 forward 具有特殊含义。在 PyTorch 中,称为 forward 的方法是在计算层时将被调用的方法名称。因此,如果你创建了一个神经网络或一个层,你必须定义 forward,它将传递前一层的数据。我们的定义是对输入数据和权重进行矩阵乘法,并加上偏差。就是这样。这就是我们之前说的nn.Linear时发生的事情。它为我们创建了这个东西。

不幸的是,我们并没有得到一个 28 乘以 28 的长向量。我们得到的是一个 28 行乘以 28 列的矩阵,所以我们必须将其展平。不幸的是,在 PyTorch 中,它们倾向于重新命名事物。他们将“resize”拼写为view。所以view意味着重塑。因此,你可以看到这里x.view(x.size(0), -1),我们最终得到的是一个图像数量(x.size(0))不变。然后我们将行替换为列,形成一个单一轴。再次,-1的意思是尽可能长。这就是我们使用 PyTorch 展平的方法。

所以我们将其展平,进行矩阵乘法,最后进行 softmax。所以 softmax 是我们使用的激活函数。如果您查看深度学习存储库,您会发现一个名为熵示例的内容,您将在其中看到 softmax 的示例。Softmax 简单地获取我们最终层的输出,因此我们从线性层获取输出。我们所做的是对每个输出进行e的(e^)运算。

然后我们取这个数字,除以e的幂的总和。

这就是所谓的 softmax。为什么我们这样做?因为我们正在将这个(exp)除以总和,这意味着这些本身的总和必须加起来为一。这就是我们想要的。我们希望所有可能结果的概率总和为一。此外,因为我们使用e^,这意味着我们知道这些(softmax)中的每一个都在零和一之间。我们知道概率将在零和一之间。最后,因为我们使用e的幂,这意味着输入中稍大的值会变成输出中的更大值。因此,通常情况下,您会看到我的 softmax 中有一个大数和许多小数。这就是我们想要的,因为我们知道输出是一热编码的。换句话说,softmax 激活函数,softmax 非线性,是一种返回类似概率的东西的东西,其中其中一个概率更有可能是高的,其他概率更有可能是低的。我们知道这就是我们想要映射到我们的一热编码的内容,因此 softmax 是一个很好的激活函数,可以帮助神经网络更容易地映射到您想要的输出。这通常是我们想要的。当我们设计神经网络时,我们尝试提出一些小的架构调整,使其尽可能容易地匹配我们想要的输出。

这基本上就是这样。与其使用 Sequential 和nn.Linear以及nn.LogSoftmax,我们从头开始定义了它。现在我们可以说,就像以前一样,我们的net2等于LogReg().cuda(),我们可以说fit,我们得到了几乎完全相同的输出,只是有轻微的随机偏差。

net2 = LogReg().cuda()
opt=optim.Adam(net2.parameters())
fit(net2, md, n_epochs=1, crit=loss, opt=opt, metrics=metrics)
'''
[ 0\.       0.32209  0.28399  0.92088]
'''

所以我希望你在这一周里尝试使用torch.randn生成一些随机张量,使用torch.matmul开始将它们相乘,相加,尝试确保你可以自己从头开始重写 softmax。尝试玩弄一下重塑、view 等等,这样到下周你回来时就会感觉对 PyTorch 相当舒适。

如果您搜索 PyTorch 教程,您会看到PyTorch 网站上有很多很好的材料可以帮助您,向您展示如何创建张量,修改它们以及对它们进行操作。

问题:我看到前向是在每个线性层之后应用的层。

Jeremy:不完全是。前向只是模块的定义,这是我们实现 Linear 的方式。

继续:这是否意味着在每个线性层之后,您必须应用相同的函数?假设我们不能在第一层之后应用 LogSoftmax,然后在第二层之后应用其他函数,如果我们有一个多层神经网络?

Jeremy:所以通常我们这样定义神经网络:

我们只是说这里是我们想要的层的列表。您不必编写自己的前向。我们刚刚做的是说,与其这样做,不如完全不使用这些,而是自己手写所有内容。因此,您可以按任何顺序编写任意数量的层。重点是在这里,我们没有使用任何这些:

我们已经编写了自己的matmul加偏置项,自己的 softmax,所以这只是 Python 代码。您可以在 forward 函数内编写任何您喜欢的 Python 代码来定义自己的神经网络。通常情况下,您不会自己这样做。通常您只会使用 PyTorch 提供的层,并使用.Sequential将它们组合在一起。或者更有可能的是,您会下载一个预定义的架构并使用它。我们只是为了学习它在幕后是如何工作的。

好的,太棒了。谢谢大家!

机器学习 1:第 9 课

原文:medium.com/@hiromi_suenaga/machine-learning-1-lesson-9-689bbc828fd2

译者:飞龙

协议:CC BY-NC-SA 4.0

来自机器学习课程的个人笔记。随着我继续复习课程以“真正”理解它,这些笔记将继续更新和改进。非常感谢 JeremyRachel 给了我这个学习的机会。

学生的作品[0:00]

欢迎回到机器学习!我非常兴奋能够分享一些由旧金山大学学生在这一周内构建或撰写的惊人内容。我将向你展示的许多东西已经在互联网上广泛传播:大量的推文和帖子以及各种各样的事情发生。

用随机森林着色

Tyler White提供

他开始说,如果我创建一个合成数据集,其中自变量是 x 和 y,因变量是颜色,会怎样。有趣的是,他向我展示了一个早期版本,那时他没有使用颜色。他只是把实际的数字放在这里。

这个东西一开始根本不起作用。一旦他开始使用颜色,它就开始运行得非常好。所以我想提一下,不幸的是我们在 USF 没有教给你的一件事是人类感知的理论,也许我们应该。因为实际上,当涉及到可视化时,最重要的事情是了解人眼或大脑擅长感知的是什么。关于这个有一个整个学术研究领域。我们最擅长感知的事情之一就是颜色的差异。这就是为什么当我们看这张他创建的合成数据的图片时,你可以立刻看到,哦,这里有四个较浅红色的区域。他所做的是,他说好,如果我们尝试创建一个关于这个合成数据集的机器学习模型,具体来说他创建了一棵树。而酷的事情是你实际上可以绘制这棵树。所以在他创建了这棵树之后,他在 matplotlib 中完成了所有这些。Matplotlib 非常灵活。他实际上绘制了树的边界,这已经是一个相当不错的技巧——能够实际绘制这棵树。

然后他做了更聪明的事情,他说好的,那么树做出了什么预测?嗯,这是每个区域的平均值,所以为了做到这一点,我们实际上可以绘制平均颜色。这实际上相当漂亮。这是树所做的预测。现在这里变得非常有趣。你可以,如你所知,通过重新采样随机生成树,所以这里有通过重新采样生成的四棵树。它们都非常相似但略有不同。

现在我们实际上可以可视化装袋,为了可视化装袋,我们简单地取四张图片的平均值。这就是装袋。就是这样:

这是一个随机森林的模糊决策边界,我觉得这很神奇。因为这就像,我真希望在我开始教你们随机森林的时候有这个,我本来可以跳过几节课的。就像“好的,这就是我们做的”。我们创建决策边界,对每个区域进行平均,然后重复几次并对所有结果进行平均。这就是随机森林的做法,我认为这只是一个通过图片将复杂问题变得简单的很好的例子。所以恭喜 Tyler。事实上,他实际上重新发明了别人已经做过的事情。一个叫 Christian Innie(?)的人,他后来成为了世界上最重要的机器学习研究者之一,实际上在他写的一本关于决策森林的书中几乎完全包含了这个技术。所以 Tyler 最终重新发明了一个世界上最重要的决策森林专家创造的东西,这实际上很酷。我觉得这很有趣。很好,因为当我们在 Twitter 上发布这个时,引起了很多关注,最终有人能够说“哦,你知道吗,这实际上已经存在了”。所以 Tyler 已经开始阅读那本书。

Parfit - 快速而强大的超参数优化与可视化 [4:16]

由 Jason Carpenter

另一件很酷的事情是 Jason Carpenter 创建了一个全新的库叫做 Parfit。Parfit 是为了选择超参数而并行拟合多个模型。我真的很喜欢这个。他展示了如何使用它的清晰示例,API 看起来非常类似于其他基于网格搜索的方法,但它使用了 Rachel 写的验证技术,我们几周前学到的使用一个好的验证集。他在介绍它的博客文章中,回顾了什么是超参数,为什么我们必须训练它们,并解释了每一步。然后这个模块本身非常完善。他为其添加了文档,为其添加了一个很好的 README。当你实际看代码时,你会发现它非常简单,这绝对不是坏事,简单是好事。通过编写这段代码并将其打包得如此完美,他使其他人使用这个技术变得非常容易,这很棒。

如何使用 parfit 使 SGD 分类器表现得和逻辑回归一样好 [5:40]

Vinay Patlolla

我真的很高兴看到的一件事是 Vinay 继续结合了我们课堂上学到的两件事:一是使用 Parfit,另一是使用我们在上一课中学到的加速 SGD 分类方法,将两者结合起来,说“好的,现在让我们使用 Parfit 来帮助我们找到 SGD 逻辑回归的参数”。我认为这真的是一个很好的主意。

随机森林的直观解释 [6:14]

由 Prince Grover

我认为很棒的另一件事是,Prince 基本上总结了我们在随机森林解释中学到的几乎所有内容。他甚至比那更进一步,因为他描述了随机森林解释的每种不同方法。他描述了如何做到这一点,例如,通过变量置换来计算特征重要性,每个方法都有一个小图片,然后非常酷,这里是从头开始实现它的代码。我认为这是一篇非常好的文章,描述了很多人不理解的东西,并且准确展示了它是如何工作的,既有图片又有实现代码。所以我认为这真的很棒。我在这里真的很喜欢的一件事是,对于树解释器,他实际上展示了如何将树解释器的输出输入到由 USF 学生 Chris 构建的新瀑布图包中,以展示如何实际上在瀑布图中可视化树解释器的贡献。所以再次,这是一种很好的结合我们学习和作为一个团队构建的多种技术。

Keras Model for Beginners (0.210 on LB)+EDA+R&D [7:37]

Davesh Maheshwari提供

有一些有趣的内核分享,下周我会分享更多,Davesh 写了这篇非常好的内核,展示了在检测冰山与船只的 Kaggle 竞赛中的挑战。这是一种很难可视化的奇怪的双通道卫星数据,他实际上通过并基本上描述了这些雷达散射的工作原理的公式,然后实际上设法编写了一个代码,使他能够重新创建实际的 3D 冰山或船只。我以前没有见过这样做。如何可视化这些数据是非常具有挑战性的。然后他继续展示如何构建神经网络来尝试解释这一点,这也非常棒。

SGD [9:53]

笔记本

让我们回到 SGD。所以我们正在回顾这份笔记本,Rachel 基本上带领我们从头开始学习 SGD,目的是进行数字识别。实际上,今天我们看的很多东西都将紧随计算线性代数课程的一部分,你可以在 fast.ai 上找到MOOCs,或者在 USF,它将成为明年的选修课。所以如果你觉得这些东西有趣,我希望你会考虑报名参加选修课或在线观看视频。

所以我们正在构建神经网络。我们假设已经下载了 MNIST 数据,通过减去平均值并除以标准差对其进行了标准化。这些数据略有不同,虽然它们代表图像,但它们被下载为每个图像都是 784 个长的秩为 1 的张量,因此已经被展平。为了绘制它的图片,我们必须将其调整为 28x28。但实际的数据不是 28x28,而是 784 个长的展平数据。

我们要采取的基本步骤是从训练世界上最简单的神经网络开始,基本上是一个逻辑回归。因此没有隐藏层。我们将使用一个名为 Fast AI 的库进行训练,并使用一个名为 PyTorch 的库构建网络。然后我们将逐渐摆脱所有库。首先,我们将摆脱 PyTorch 中的nn(神经网络)库,并自己编写。然后我们将摆脱 Fast AI 的fit函数,并自己编写。然后我们将摆脱 PyTorch 的优化器,并自己编写。因此,在本笔记本的最后,我们将自己编写所有部分。我们最终依赖的唯一两个 PyTorch 给我们的关键事物是:

  • 具有编写 Python 代码并在 GPU 上运行的能力

  • 具有编写 Python 代码并使其自动为我们进行微分的能力。

因此,这两件事我们不打算自己尝试编写,因为这很无聊且毫无意义。但除此之外,我们将尝试在这两件事的基础上自己编写其他所有内容。

我们的起点不是自己做任何事情。基本上所有事情都已经为我们完成。因此,PyTorch 有一个nn库,其中包含神经网络的内容。您可以通过使用Sequential函数创建一个多层神经网络,然后传入您想要的层的列表,我们要求一个线性层,然后是一个 softmax 层,这定义了我们的逻辑回归。

from fastai.metrics import *
from fastai.model import *
from fastai.dataset import *

import torch.nn as nnnet = nn.Sequential(
    nn.Linear(28*28, 10),
    nn.LogSoftmax()
).cuda()

我们线性层的输入是 28 乘以 28,输出是 10,因为我们希望为我们的图像中的每个数字 0 到 9 之间的每个数字获得一个概率。.cuda()将其放在 GPU 上,然后fit拟合模型。

loss=nn.NLLLoss()
metrics=[accuracy]
opt=optim.Adam(net.parameters())
fit(net, md, n_epochs=5, crit=loss, opt=opt, metrics=metrics)

因此,我们从一组随机权重开始,然后使用梯度下降进行拟合以使其更好。我们必须告诉拟合函数要使用什么标准,换句话说,什么算作更好,我们告诉它使用负对数似然。我们将在下一课中了解这是什么。我们必须告诉它要使用什么优化器,我们说请使用optim.Adam,这方面的细节我们在本课程中不会涉及。我们将构建一个更简单的称为 SGD 的东西。如果您对 Adam 感兴趣,我们刚刚在深度学习课程中涵盖了这一点。您想要打印出什么指标,我们决定打印出准确率。就是这样。所以在我们拟合后,我们通常会得到大约 91、92%的准确率。

定义模块

接下来我们要做的是,我们将重复这完全相同的事情,所以我们将重建这个模型 4 到 5 次,逐渐减少使用的库。我们上次做的第二件事是尝试开始自己定义模块。所以我们不再使用那个库,而是尝试从头开始自己定义它。为了做到这一点,我们必须使用面向对象,因为这是我们在 PyTorch 中构建所有东西的方式。我们必须创建一个类,该类继承自nn.Module。所以nn.Module是一个 PyTorch 类,它接受我们的类并将其转换为神经网络模块,这基本上意味着您从nn.Module继承的任何东西,您几乎可以将其插入到神经网络中作为一个层,或者您可以将其视为一个神经网络。它将自动获得作为神经网络的一部分或整个神经网络运行所需的所有内容。现在我们将在今天和下一课中详细讨论这意味着什么。

因此我们需要构建这个对象,这意味着我们需要定义构造函数 dunder init。重要的是,这是一个 Python 的东西,如果你继承自其他对象,那么你首先必须创建你继承的东西。因此当你说super().__init__()时,这意味着首先构建那个nn.Module部分。如果你不这样做,那么nn.Module的东西就永远没有机会被实际构建。因此这就像一个标准的 Python 面向对象子类构造函数。如果其中有任何地方让你感到困惑,那么你知道这就是你绝对需要抓住一个 Python 面向对象的入门,因为这是标准的方法。

def get_weights(*dims): 
    return nn.Parameter(torch.randn(dims)/dims[0])
def softmax(x): 
    return torch.exp(x)/(torch.exp(x).sum(dim=1)[:,None])

class LogReg(nn.Module):
    def __init__(self):
        super().__init__()
        self.l1_w = get_weights(28*28, 10)  # Layer 1 weights
        self.l1_b = get_weights(10)         # Layer 1 bias

    def forward(self, x):
        x = x.view(x.size(0), -1)
        x = (x @ self.l1_w) + self.l1_b  # Linear Layer
        x = torch.log(softmax(x)) # Non-linear (LogSoftmax) Layer
        return x

因此,在我们的构造函数中,我们想要做的相当于nn.Linearnn.Linear所做的是,它将我们的 28 乘以 28 的向量,即 784 个元素的向量,作为矩阵乘法的输入。因此,现在我们需要创建一个具有 784 行和 10 列的矩阵。因为这个的输入将是一个大小为 64 乘以 784 的小批量数据。因此我们将进行这个矩阵乘法运算。因此当我们在 PyTorch 中说nn.Linear时,它将为我们构建一个 784 乘以 10 的矩阵。因此,由于我们没有使用它,我们是从头开始做事情,我们需要自己制作它。为了自己制作它,我们可以说生成具有这种维度的正态随机数torch.randn(dims),我们在这里传入了28*28, 10。这样我们就得到了我们随机初始化的矩阵。

然后我们想要添加到这个。我们不只是想要 y = ax,我们想要 y = ax + b。所以我们需要添加在神经网络中称为偏置向量的东西。因此,我们在这里创建一个长度为 10 的偏置向量 self.l1_b = get_weights(10),同样是随机初始化的,所以现在我们有了两个随机初始化的权重张量。

这就是我们的构造函数。现在我们需要定义前向传播。为什么我们需要定义前向传播?这是一个 PyTorch 特有的东西。当你在 PyTorch 中创建一个模块时,你得到的对象会表现得好像它是一个函数。你可以用括号调用它,我们马上就会这样做。因此,你需要以某种方式定义当你调用它时会发生什么,就好像它是一个函数,答案是 PyTorch 调用一个叫做 forward 的方法。这是他们选择的 PyTorch 方法。所以当它调用 forward 时,我们需要做这个模块或层的输出的实际计算。这就是在逻辑回归中实际计算的东西。基本上我们取得我们的输入 x,它被传递给 forward —— 这基本上是 forward 的工作原理,它接收到小批量数据,并且我们将其与构造函数中定义的第一层权重进行矩阵乘法运算。然后我们加上构造函数中也定义的第一层偏置。实际上,现在我们可以更加优雅地使用 Python 3 的矩阵乘法运算符@来定义这个。

当你使用它时,我认为你最终会得到更接近数学符号的样子,所以我觉得这更好看。

好了,这就是我们逻辑回归中的线性层(即我们的零隐藏层神经网络)。然后我们对其进行 softmax。我们得到这个矩阵乘法的输出,它的维度是 64 乘以 10。我们得到这个输出矩阵,然后我们通过 softmax 函数处理它。为什么我们要通过 softmax 函数处理它?我们要通过 softmax 函数处理它是因为最终,对于每个图像,我们希望得到一个概率,它是 0、1、2、3 或 4。所以我们希望得到一堆概率,它们加起来等于 1,其中每个概率都在 0 到 1 之间。所以 softmax 函数正好为我们做到了这一点。

例如,如果我们不是从零到 10 中挑选数字,而是挑选猫、狗、飞机、鱼或建筑物,那么一个特定图像的矩阵乘积的输出可能看起来像这样(输出列)。这些只是一些随机数。为了将其转换为 softmax,我首先对这些数字中的每一个进行e的幂运算。我将这些e的幂相加。然后我将这些e的幂中的每一个除以总和。这就是 softmax。这就是 softmax 的定义。

因为它是e的幂,这意味着它始终是正的。因为它被总和除以,这意味着它始终在 0 和 1 之间,并且还意味着它们总是加起来等于 1。因此,通过应用这个 softmax 激活函数,每当我们有一层输出,我们称之为激活,然后我们对其应用一些非线性函数,将一个标量映射到一个标量,比如 softmax(我们称之为激活函数)。因此,softmax 激活函数将我们的输出转换为类似概率的东西。严格来说,我们不需要它。我们仍然可以尝试训练直接输出概率的东西。但是通过使用这个函数,它自动使它们始终表现得像概率,这意味着网络需要学习的内容更少,因此它会学得更好。因此,一般来说,每当我们设计一个架构时,我们都会尽可能地设计它,以便它尽可能地创建我们想要的形式。这就是为什么我们使用 softmax 的原因。

这就是基本步骤。我们有我们的输入,它是一堆图像,它被一个权重矩阵相乘,我们还添加一个偏置以获得线性函数的输出。我们将其通过一个非线性激活函数,这种情况下是 softmax,这给我们带来了我们的概率。

所以这就是全部。PyTorch 也倾向于使用 softmax 的对数,原因并不需要现在困扰我们。基本上是为了数值稳定性的便利。因此,为了使这与我们在上面看到的nn.LogSoftmax()版本相同,我也将在这里使用对数。好的,现在我们可以实例化这个类(即创建这个类的对象)。

问题:我有一个关于之前的概率的问题。如果我们有一张照片上有一只猫和一只狗,那会改变它的工作方式吗?或者它的基本工作方式是一样的。这是一个很好的问题。所以如果你有一张照片上有一只猫和一只狗,你希望它同时输出猫和狗,这将是一个非常糟糕的选择。Softmax 是我们专门用于分类预测的激活函数,我们只想预测其中一种东西。因此,部分原因是因为正如你所看到的,因为我们使用e的幂,稍微更大的e会产生更大的数字。因此,通常我们只有一两个大的东西,其他东西都很小。因此,如果我重新计算这些随机数(在 Excel 表中),你会看到它往往是一堆零和一两个高数字。因此,它真的是设计成试图让预测这一件事变得容易。如果你正在进行多标签预测,所以我只想找到这张图片中的所有东西,而不是使用 softmax,我们将使用 sigmoid。Sigmoid 会导致这些东西中的每一个都在 0 和 1 之间,但它们不再加起来等于 1。

关于最佳实践的许多细节是我们在深度学习课程中涵盖的内容,我们在机器学习课程中不会涵盖大量这些内容。我们更感兴趣的是机制。但是如果它们很快,我们会尝试涵盖它们。

现在我们已经得到了这个,我们可以实例化该类的一个对象[27:30]。当然我们想将其复制到 GPU 上。这样我们可以在那里进行计算。再次,我们需要一个优化器,我们很快会讨论这是什么。但你看到这里,我们在我们的类上调用了一个名为parameters的函数。但我们从未定义过一个叫做 parameters 的方法,这将会起作用的原因是因为它实际上是在nn.Module内部为我们定义的。所以nn.Module会自动遍历我们创建的属性,并找到任何我们说这是一个参数的东西。你说某个东西是参数的方式是将它包装在nn.Parameter中。这只是告诉 PyTorch 这是我想要优化的东西的方式。所以当我们创建权重矩阵时,我们只是用nn.Parameter包装它,这与我们很快要学习的常规 PyTorch 变量完全相同。这只是一个小标志,告诉 PyTorch 你应该优化这个。所以当你在我们创建的net2对象上调用net2.parameters()时,它会遍历我们在构造函数中创建的所有东西,检查是否有任何一个是Parameter类型,如果是,它会将所有这些东西设置为我们要用优化器训练的东西。我们稍后将从头开始实现优化器。

net2 = LogReg().cuda()
opt=optim.Adam(net2.parameters())

做完这些,我们可以拟合[28:51]。我们应该基本上得到与之前相同的答案(即 91 左右)。看起来不错。

fit(net2, md, n_epochs=1, crit=loss, opt=opt, metrics=metrics)

数据加载器[29:05]

那么我们实际上在这里构建了什么?嗯,正如我所说的,我们实际上构建的是可以像常规函数一样运行的东西。所以我想向你展示如何实际上将其作为一个函数调用。为了能够将其作为一个函数调用,我们需要能够向其传递数据。为了能够向其传递数据,我需要获取一个 MNIST 图像的小批量。为了方便起见,我们使用了 Fast AI 的ImageClassifierData.from_arrays方法,它会为我们创建一个 PyTorch DataLoader。PyTorch DataLoader 是一种获取几张图像并将它们放入一个小批量并使其可用的东西。你基本上可以说给我另一个小批量,给我另一个小批量,给我另一个小批量。所以在 Python 中,我们称这些东西为生成器。生成器是一种东西,你基本上可以说我想要另一个,我想要另一个,我想要另一个。迭代器和生成器之间有非常紧密的联系,我现在不打算担心它们之间的区别。但你会看到,为了获得我们可以说请给我另一个的东西,为了获取我们可以用来生成小批量的东西,我们必须取出我们的数据加载器,这样你就可以从我们的模型数据对象中请求训练数据。你会看到有很多不同的数据加载器可以请求:测试数据加载器、训练数据加载器、验证数据加载器、增强图像数据加载器等等。

dl = iter(md.trn_dl)

所以我们要获取为我们创建的训练数据加载器。这是一个标准的 PyTorch 数据加载器,稍微被我们优化了一下,但是思路是一样的。然后你可以说这个(iter)是一个标准的 Python 东西,我们可以说将其转换为一个迭代器,即我们可以一次从中获取另一个的东西。一旦你这样做了,我们就有了一个可以迭代的东西。你可以使用标准的 Python next函数从生成器中获取一个更多的东西。

xmb,ymb = next(dl)

所以这是从一个小批量返回 x 和 y。在 Python 中,您可以使用for循环来使用生成器和迭代器。我也可以说 x 小批量逗号 y 小批量在数据加载器中,然后做一些事情:

所以当你这样做时,实际上是在幕后,它基本上是调用next很多次的语法糖。所以这都是标准的 Python 东西。

所以它返回了一个大小为 64 乘以 784 的张量,这是我们所期望的。我们使用的 Fast AI 库默认使用小批量大小为 64,这就是为什么它这么长。这些都是背景零像素,但它们实际上并不是零。在这种情况下,为什么它们不是零呢?因为它们被标准化了。所以我们减去了平均值,除以标准差。

xmb
'''
-0.4245 -0.4245 -0.4245  ...  -0.4245 -0.4245 -0.4245
-0.4245 -0.4245 -0.4245  ...  -0.4245 -0.4245 -0.4245
-0.4245 -0.4245 -0.4245  ...  -0.4245 -0.4245 -0.4245
          ...             ⋱             ...          
-0.4245 -0.4245 -0.4245  ...  -0.4245 -0.4245 -0.4245
-0.4245 -0.4245 -0.4245  ...  -0.4245 -0.4245 -0.4245
-0.4245 -0.4245 -0.4245  ...  -0.4245 -0.4245 -0.4245
[torch.FloatTensor of size 64x784 (GPU 0)]
'''

现在我们要做的是将其传递给我们的逻辑回归。所以我们可能会这样做,我们将使用vxmb(变量 x 小批量),我可以取出我的 x 小批量,我可以将其移动到 GPU 上,因为记住我的net2对象在 GPU 上,所以我们的数据也必须在 GPU 上。然后我要做的第二件事是,我必须将其包装在Variable中。那么变量是做什么的呢?这是我们免费获得自动微分的方式。PyTorch 可以自动微分几乎任何张量。但这需要内存和时间,所以它不会总是跟踪。要进行自动微分,它必须跟踪确切的计算方式。我们将这些东西相加,我们将其乘以那个,然后我们取了这个的符号等等。你必须知道所有的步骤,因为然后要进行自动微分,它必须使用链式法则对每个步骤求导,然后将它们相乘。所以这是缓慢和占用内存的。所以我们必须选择说“好的,这个特定的东西,我们以后会对其进行求导,所以请为我们跟踪所有这些操作。”我们选择的方式是将一个张量包装在Variable中。这就是我们的做法。

你会发现它看起来几乎和一个张量一样,但现在它说“包含这个张量的变量”。所以在 PyTorch 中,一个变量的 API 与张量完全相同,或者更具体地说,是张量 API 的超集。我们对张量可以做的任何事情,我们也可以对变量做。但它会跟踪我们做了什么,以便我们以后可以求导。

vxmb = Variable(xmb.cuda())
vxmb
'''
Variable containing:
-0.4245 -0.4245 -0.4245  ...  -0.4245 -0.4245 -0.4245
-0.4245 -0.4245 -0.4245  ...  -0.4245 -0.4245 -0.4245
-0.4245 -0.4245 -0.4245  ...  -0.4245 -0.4245 -0.4245
          ...             ⋱             ...          
-0.4245 -0.4245 -0.4245  ...  -0.4245 -0.4245 -0.4245
-0.4245 -0.4245 -0.4245  ...  -0.4245 -0.4245 -0.4245
-0.4245 -0.4245 -0.4245  ...  -0.4245 -0.4245 -0.4245
[torch.cuda.FloatTensor of size 64x784 (GPU 0)]
'''

所以我们现在可以将其传递给我们的net2对象。记住我说过你可以将其视为函数。所以请注意,我们没有调用.forward(),我们只是将其视为函数。然后记住,我们取了对数,为了撤销这个操作,我正在使用.exp(),这将给我概率。所以这是我的概率,它返回的大小是 64 乘以 10,所以对于小批量中的每个图像,我们有 10 个概率。你会看到,大多数概率都非常接近零。而其中一些则要大得多,这正是我们所希望的。就像好吧,它不是零,不是 1,不是 2,它3,不是 4 等等。

preds = net2(vxmb).exp(); preds[:3]
'''
Variable containing:

Columns 0 to 5 
 1.6740e-03  1.0416e-05  2.5454e-05  1.9119e-02  6.5026e-05  9.7470e-01
 3.4048e-02  1.8530e-04  6.6637e-01  3.5073e-02  1.5283e-01  6.4995e-05
 3.0505e-08  4.3947e-08  1.0115e-05  2.0978e-04  9.9374e-01  6.3731e-05

Columns 6 to 9 
 2.1126e-06  1.7638e-04  3.9351e-03  2.9154e-04
 1.1891e-03  3.2172e-02  1.4597e-02  6.3474e-02
 8.9568e-06  9.7507e-06  7.8676e-04  5.1684e-03
[torch.cuda.FloatTensor of size 3x10 (GPU 0)]
'''

我们可以调用net2.forward(vxmb),它会做完全相同的事情。但这并不是 PyTorch 的所有机制实际上是如何工作的。他们实际上将其称为函数。这实际上是一个非常重要的想法,因为这意味着当我们定义自己的架构或其他内容时,任何你想要放入函数的地方,你都可以放入一个层;任何你想要放入一个层的地方,你都可以放入一个神经网络;任何你想要放入一个神经网络的地方,你都可以放入一个函数。因为就 PyTorch 而言,它们都只是它将调用的东西,就像它们是函数一样。所以它们是可以互换的,这是非常重要的,因为这就是我们通过混合和匹配许多部分并将它们组合在一起来创建非常好的神经网络的方式。

让我举个例子。这是我的逻辑回归,准确率达到了 91%多一点。我现在要把它转换成一个带有一个隐藏层的神经网络。

我要做的是创建更多的层。我要改变这个,使其输出 100 而不是 10,这意味着这个输入将是 100 而不是 10。现在这样还不能让事情变得更好。为什么这肯定不会比之前更好呢?因为两个线性层的组合只是一个线性层,但参数不同。

所以我们有两个线性层,这只是一个线性层。为了使事情变得有趣,我将用零替换第一层中的所有负数。因为这是一个非线性转换,这个非线性转换被称为修正线性单元(ReLU)。

所以nn.Sequential简单地会依次调用每个层对每个小批量进行操作。所以做一个线性层,用零替换所有负数,再做一个线性层,最后做一个 softmax。这现在是一个有一个隐藏层的神经网络。所以让我们尝试训练这个。准确率现在已经提高到 96%。

所以这个想法是,我们在这节课中学习的基本技术在你开始将它们堆叠在一起时变得强大。

问题:为什么你选择了 100?没有原因。输入一个额外的零更容易。神经网络层中应该有多少激活是深度学习从业者的规模问题,我们在深度学习课程中讨论过,而不是在这门课程中。

问题:在添加额外的层时,如果你做了两个 softmax,这会有影响吗,或者这是你不能做的事情?你绝对可以在那里使用 softmax。但这可能不会给你想要的结果。原因是 softmax 倾向于将大部分激活推向零。激活,只是为了明确,因为在深度学习课程中我收到了很多关于什么是激活的问题,激活是在一个层中计算出来的值。这就是一个激活:

这不是一个权重。权重不是一个激活。它是你从一个层计算出来的值。所以 softmax 会倾向于使大部分激活接近于零,这与你想要的相反。通常你希望你的激活尽可能丰富、多样且被使用。所以没有什么能阻止你这样做,但它可能不会工作得很好。基本上,你的所有层几乎都会跟随非线性激活函数,通常是 ReLU,除了最后一层。

问题:在做多层时,比如说 2 或 3 层,你想要改变这些激活层吗?不。所以如果我想要更深,我会直接这样做。

现在这是一个两个隐藏层的网络。

问题:所以我想我听到你说有几种不同的激活函数,比如修正线性单元。有一些例子,为什么会使用每一个呢?是的,很好的问题。所以基本上当你添加更多的线性层时,你的输入进来,你把它通过一个线性层然后一个非线性层,线性层,非线性层,线性层和最终的非线性层。最终的非线性层正如我们讨论过的,如果它是多类别分类但你只选择其中一个,你会使用 softmax。如果是二元分类或多标签分类,你会使用 sigmoid。如果是回归,通常你根本不会有,尽管我们在昨晚的深度学习课程中学到有时你也可以在那里使用 sigmoid。所以它们基本上是最终层的主要选项。对于隐藏层,你几乎总是使用 ReLU,但还有另一个你可以选择的,有点有趣,叫做 leaky ReLU。基本上如果它大于零,它是y = x,如果小于零,它就像y = 0.1x。所以它与 ReLU 非常相似,但不是等于 0,而是接近于 0。所以它们是主要的两种:ReLU 和 Leaky ReLU。

还有其他一些,但它们有点像那样。例如,有一种叫做 ELU 的东西相当受欢迎,但细节并不太重要。像 ELU 这样的东西有点像 ReLU,但在中间稍微弯曲一些。它通常不是基于数据集选择的东西。随着时间的推移,我们发现了更好的激活函数。所以两三年前,每个人都使用 ReLU。一年前,几乎每个人都使用 Leaky ReLU。今天,我想大多数人开始转向 ELU。但老实说,激活函数的选择实际上并不太重要。人们实际上已经表明,你可以使用相当任意的非线性激活函数,甚至是正弦波,它仍然有效。

所以尽管今天我们要做的是展示如何创建这个没有隐藏层的网络,将其转变为下面这个网络(下面)准确率为 96%左右将会很简单。这是你可能应该在这一周尝试做的事情,创建这个版本。

现在我们有了一个可以传递我们的变量并得到一些预测的网络,这基本上就是我们调用fit时发生的事情。所以我们将看看这种方法如何用于创建这种随机梯度下降。需要注意的一件事是将预测的概率转换为预测的数字是,我们需要使用 argmax。不幸的是,PyTorch 并不称之为 argmax。相反,PyTorch 只是称之为 max,并且 max 返回两个东西:它返回给定轴上的实际最大值(所以max(1)将返回列的最大值),它返回的第二件事是该最大值的索引。所以 argmax 的等价物是调用 max 然后获取第一个索引的东西:

这就是我们的预测。如果这是 numpy,我们将使用np.argmax()

preds = predict(net2, md.val_dl).argmax(1)
plots(x_imgs[:8], titles=preds[:8])

所以这是我们手动创建的逻辑回归的预测,在这种情况下,看起来我们几乎全部正确。

接下来我们要尝试摆脱使用库的是我们将尝试避免使用矩阵乘法运算符。相反,我们将尝试手动编写。

广播[46:58]

因此,接下来,我们将学习一些似乎是一个小的编程概念的东西。但实际上,至少在我看来,这将是我们在这门课程中教授的最重要的编程概念,也可能是你需要构建机器学习算法的所有重要编程概念。这就是广播的概念。我将通过示例展示这个概念。

如果我们创建一个数组 10、6、-4 和一个数组 2、8、7,然后将它们相加,它会依次添加这两个数组的每个分量——我们称之为“逐元素”。

a = np.array([10, 6, -4])
b = np.array([2, 8, 7]) a + b
'''
array([12, 14,  3])
'''

换句话说,我们不必编写循环。在过去,我们必须循环遍历每一个并将它们相加,然后将它们连接在一起。今天我们不必这样做。它会自动为我们发生。因此,在 numpy 中,我们自动获得逐元素操作。我们可以用 PyTorch 做同样的事情。在 Fast AI 中,我们只需添加一个大写 T 来将某物转换为 PyTorch 张量。如果我们将它们相加,结果完全相同。

因此,这些库中的逐元素操作在这种情况下是相当标准的。有趣的不仅仅是因为我们不必编写 for 循环,而且实际上更有趣的是由于这里发生的性能问题。

性能

第一个是如果我们在 Python 中进行 for 循环,那将会发生。即使你使用 PyTorch,它仍然在 Python 中执行 for 循环。它没有优化 for 循环的方法。因此,在 Python 中,for 循环的速度大约比在 C 中慢 10,000 倍。这是你的第一个问题。我记不清是 1,000 还是 10,000。

然后,第二个问题是,你不仅希望它在 C 中得到优化,而且你希望 C 利用你所有 CPU 所做的事情,这被称为 SIMD,单指令多数据。你的 CPU 能够一次处理 8 个向量中的 8 个元素,并将它们相加到另一个包含 8 个元素的向量中,在一个 CPU 指令中。因此,如果你能利用 SIMD,你立即就会快 8 倍。这取决于数据类型有多大,可能是 4,可能是 8。

你的计算机中还有多个进程(多个核心)。因此,如果向量相加发生在一个核心中,你可能有大约 4 个核心。因此,如果你使用 SIMD,你会快 8 倍,如果你可以使用多个核心,那么你会快 32 倍。然后如果你在 C 中这样做,你可能会快 32k 倍。

所以好处是当我们执行a + b时,它利用了所有这些东西。

更好的是,如果你在 PyTorch 中执行这个操作,并且你的数据是用.cuda()创建的,然后将其放在 GPU 上,那么你的 GPU 可以一次执行大约 10,000 个操作。因此,这将比 C 快 100 倍。因此,这对于获得良好的性能至关重要。你必须学会如何通过利用这些逐元素操作来编写无循环的代码。这不仅仅是加号(+)。我还可以使用小于号(<),这将返回 0,1,1。

或者如果我们回到 numpy,False,True,True。

因此,你可以使用这个来做各种事情而不需要循环。例如,我现在可以将其乘以a,这里是所有小于ba的值:

或者我们可以取平均值:

(a < b).mean()
'''
0.66666666666666663
'''

这是a中小于b的值的百分比。因此,你可以用这个简单的想法做很多事情。

进一步

但要进一步,要进一步超越这种逐元素操作,我们将不得不走到下一步,到一种称为广播的东西。让我们从看一个广播的例子开始。

a
'''
array([10,  6, -4])
'''

a 是一个具有一维的数组,也称为秩 1 张量,也称为向量。我们可以说a大于零:

a > 0
'''
array([ True,  True, False], dtype=bool)
'''

这里,我们有一个秩 1 张量(a)和一个秩 0 张量(0)。秩 0 张量也称为标量,秩 1 张量也称为向量。我们之间有一个操作:

现在你可能已经做了一千次,甚至没有注意到这有点奇怪。你有不同等级和不同大小的这些东西。那么它实际上在做什么呢?它实际上是将那个标量复制 3 次(即[0, 0, 0]),并实际上逐个元素地给我们三个答案。这就是所谓的广播。广播意味着复制我的张量的一个或多个轴,以使其与另一个张量的形状相同。但它实际上并没有复制。它实际上是存储了一种内部指示器,告诉它假装这是一个三个零的向量,但实际上它不是去下一行或下一个标量,而是回到它来的地方。如果你对这个特别感兴趣,它们将该轴上的步幅设置为零。这对于那些好奇的人来说是一个较为高级的概念。

所以我们可以做 a + 1[54:52]。它将广播标量 1 为[1, 1, 1],然后进行逐元素加法。

a + 1
'''
array([11,  7, -3])
'''

我们可以对一个矩阵做同样的操作。这是我们的矩阵。

m = np.array([[1, 2, 3], [4,5,6], [7,8,9]]); m
'''
array([[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9]])
'''

那个矩阵的 2 倍将广播 2 为[[2, 2, 2],[2,2,2],[2,2,2]],然后进行逐元素乘法。这就是我们广播的最简单版本。

2*m
'''
array([[ 2,  4,  6],
       [ 8, 10, 12],
       [14, 16, 18]])
'''

将向量广播到矩阵[55:27]

这是广播的一个稍微复杂的版本。这里有一个名为c的数组。这是一个秩 1 张量。

c = np.array([10,20,30]); c
'''
array([10, 20, 30])
'''

这是之前的矩阵m——秩 2 张量。我们可以添加m + c。那么这里发生了什么?

m + c
'''
array([[11, 22, 33],
       [14, 25, 36],
       [17, 28, 39]])
'''

你可以看到它所做的是将[10, 20, 30]添加到每一行。

所以我们可以想象它似乎做了与广播标量相同类型的想法,就像复制了它。然后将这些视为秩 2 矩阵。现在我们可以进行逐元素加法。

问题:通过查看这个例子,它将其复制下来生成新的行。如果我们想要获得新的列,我们应该如何做[56:50]?我很高兴你问了。所以我们会这样做:

现在将其视为我们的矩阵。要让 numpy 这样做,我们需要传入一个矩阵而不是一个向量,而是传入一个具有一列的矩阵(即秩 2 张量)。基本上,numpy 会将这个秩 1 张量视为秩 2 张量,表示一行。换句话说,它是 1 乘 3。所以我们想要创建一个 3 乘 1 的张量。有几种方法可以做到这一点。一种方法是使用np.expand_dims(c,1),如果你传入这个参数,它会说“请在这里插入一个长度为 1 的轴”。所以在我们的情况下,我们想将其转换为 3 乘 1,所以如果我们说expand_dims(c,1),它会将形状改变为(3, 1)。所以如果我们看看它是什么样子的,它看起来像一列。

np.expand_dims(c,1).shape
'''
(3, 1)
'''
np.expand_dims(c,1)
'''
array([[10],
       [20],
       [30]])
'''

所以如果我们现在加上m,你可以看到它确实做了我们希望它做的事情,即将 10、20、30 添加到列[58:50]:

m + np.expand_dims(c,1)
'''
array([[11, 12, 13],
       [24, 25, 26],
       [37, 38, 39]])
'''

现在由于单位轴的位置是如此重要,所以通过实验创建这些额外的单位轴并知道如何轻松地做到这一点是非常有帮助的。在我看来,np.expand_dims并不是最容易的方法。最简单的方法是使用一个特殊的索引None来索引张量。None的作用是在那个位置创建一个长度为 1 的新轴。因此,这将在开始添加一个新的长度为 1 的轴。

c[None]
'''
array([[10, 20, 30]])
'''
c[None].shape
'''
(1, 3)
'''

这将在末尾添加一个新的长度为 1 的轴。

c[:,None]
'''
array([[10],
       [20],
       [30]])
'''
c[:,None].shape
'''
(3, 1)
'''

或者为什么不两者都做呢

c[None,:,None].shape
'''
(1, 3, 1)
'''

所以如果你考虑一下,一个张量中有 3 个元素,可以是任何你喜欢的阶数,你可以随意添加单位轴。这样,我们可以决定我们希望广播的方式。所以在 numpy 中有一个非常方便的东西叫做broadcast_to,它的作用是将我们的向量广播到那个形状,并展示给我们看看那会是什么样子。

np.broadcast_to(c, (3,3))
'''
array([[10, 20, 30],
       [10, 20, 30],
       [10, 20, 30]])
'''

所以如果你对某个广播操作中发生的事情感到不确定,你可以使用broadcast_to。例如,在这里,我们可以说,而不是(3,3),我们可以说m.shape,看看将会发生什么。

np.broadcast_to(c, m.shape)
'''
array([[10, 20, 30],
       [10, 20, 30],
       [10, 20, 30]])
'''

这就是在我们将其添加到m之前会发生的事情。所以如果我们说将其转换为列,那就是它的样子:

np.broadcast_to(c[:,None], m.shape)
'''
array([[10, 10, 10],
       [20, 20, 20],
       [30, 30, 30]])
'''

所以这就是广播的直观定义。现在希望我们可以回到那个 numpy 文档并理解它的含义。

Numpy 文档 [1:01:37]:

广播这个术语描述了 numpy 在算术运算期间如何处理具有不同形状的数组。在一定的约束条件下,较小的数组(较低秩的张量)被“广播”到较大的数组上,以便它们具有兼容的形状。广播提供了一种向量化数组操作的方法,使循环发生在 C 而不是 Python 中。它可以在不进行不必要的数据复制的情况下实现这一点,并通常导致高效的算法实现。

“向量化”通常意味着使用 SIMD 等技术,以便多个操作同时进行。它实际上并不会进行不必要的数据复制,只是表现得好像进行了复制。所以这就是我们的定义。

现在在深度学习中,你经常处理 4 阶或更高阶的张量,并且经常将它们与 1 阶或 2 阶的张量结合在一起,仅凭直觉正确地执行这些操作几乎是不可能的。所以你真的需要了解规则。

这里是m.shapec.shape [1:02:45]。所以规则是我们将逐个元素地比较我们两个张量的形状。我们将一次查看一个,并且我们将从末尾开始向前移动。当这两个条件之一为真时,两个维度将是兼容的。所以让我们检查一下我们的mc是否兼容。所以我们将从末尾开始(首先是尾部维度)并检查“它们是否兼容?”如果维度相等,则它们是兼容的。让我们继续下一个。哦,我们缺少了。c缺少一些东西。如果有东西缺失会发生什么,我们会插入一个 1。这就是规则。所以现在让我们检查一下——这些是否兼容?其中一个是 1,是的,它们是兼容的。所以现在你可以看到为什么 numpy 将一维数组视为一个代表行的 2 阶张量。这是因为我们基本上在前面插入了一个 1。这就是规则。

在对两个数组进行操作时,Numpy/PyTorch 会逐个元素地比较它们的形状。它从尾部维度开始,逐步向前推进。当两个维度兼容

  • 它们是相等的,或者

  • 其中之一是 1

数组不需要具有相同数量的维度。例如,如果你有一个256*256*3的 RGB 值数组,并且你想要按不同的值缩放图像中的每种颜色,你可以将图像乘以一个具有 3 个值的一维数组。根据广播规则对齐这些数组的尾部轴的大小,显示它们是兼容的:

Image  (3d array): 256 x 256 x 3
Scale  (1d array):             3
Result (3d array): 256 x 256 x 3

例如,上面是你经常需要做的事情,即你从一幅图像开始,256 像素乘以 256 像素乘以 3 个通道。你想要减去每个通道的平均值。所以你有 256 乘以 256 乘以 3,你想要减去长度为 3 的东西。是的,你可以做到。绝对可以。因为 3 和 3 是兼容的,因为它们是相同的。256 和空是兼容的,因为它会插入一个 1。256 和空是兼容的,因为它会插入一个 1。所以你最终会得到这个(每个通道的平均值)会在所有这个轴(从右边数第二个)上广播,然后整个东西将在这个最左边的轴上广播,所以我们最终会得到一个 256 乘以 256 乘以 3 的有效张量。

有趣的是,数据科学或机器学习社区中很少有人理解广播,大多数时候,例如,当我看到人们为计算机视觉进行预处理时,比如减去平均值,他们总是在通道上写循环。我认为不必这样做非常方便,而且通常不必这样做速度更快。所以如果你擅长广播,你将拥有这种非常少数人拥有的超级有用的技能。这是一种古老的技能。它可以追溯到 APL 的时代。APL 是上世纪 50 年代的,代表着 A Programming Language,肯尼斯·艾弗森写了一篇名为“符号作为思维工具”的论文,他在其中提出了一种新的数学符号。他提出,如果我们使用这种新的数学符号,它会给我们提供新的思维工具,让我们能够思考以前无法思考的事情。他的一个想法就是广播,不是作为计算机编程工具,而是作为数学符号的一部分。因此,他最终将这种符号实现为一种思维工具,作为一种名为 APL 的编程语言。他的儿子继续将其进一步发展为一种名为 J 的软件,这基本上是当你将 60 年来非常聪明的人们致力于这个想法时得到的结果。通过这种编程语言,你可以用一两行代码表达非常复杂的数学思想。我们有 J 是很棒的,但更棒的是这些想法已经进入我们所有人使用的语言中,比如在 Python 中的 numpy 和 PyTorch 库。这些不仅仅是一些小众的想法,它们是思考数学和进行编程的基本方式。

让我举个例子,这种符号作为思维工具的例子。这里我们有 c:

这里我们有 c[None]

注意现在有两个方括号。这有点像一个一行向量张量。这里是一个小列:

这将会做什么呢?

所以从广播规则的角度来看,我们基本上是将这个列(维度为(3,1))和这个行(维度为(1,3))进行操作。为了使这些符合我们的广播规则,列必须被复制 3 次,因为它需要匹配 3。行必须被复制 3 次以匹配 3。所以现在我有两个矩阵可以进行逐元素乘积。

所以正如你所说,这是我们的外积。

现在有趣的是,突然间这不再是一个特殊的数学案例,而只是广播这个一般概念的一个特定版本,我们可以像外加一样:

或者外大于:

或者其他。所以突然间我们有了这个概念,我们可以用它来构建新的想法,然后我们可以开始尝试这些新的想法。

有趣的是,numpy 有时会使用这个方法。例如,如果你想创建一个网格,numpy 就是这样做的:

实际上返回的是 0、1、2、3、4;一个作为列,一个作为行。所以我们可以说好的,这是 x 网格(xg)逗号 y 网格(yg),现在你可以做类似这样的事情:

所以突然间我们把它扩展成了一个网格。所以有趣的是一些简单的概念是如何被不断地建立和发展的。所以如果你熟悉 APL 或 J,这是一个由许多层层叠加而成的整个环境。虽然在 numpy 中我们没有这样深层次的环境,但你肯定可以看到这种广播的想法在简单的事情中体现出来,比如我们如何在 numpy 中创建一个网格。

实现矩阵乘法[1:12:30]

这就是广播,现在我们可以使用这个来实现矩阵乘法。那么为什么我们要这样做呢?显然我们不需要。矩阵乘法已经被我们的库完美地处理了。但是很多时候你会发现在各种领域,特别是在深度学习中,会有一些特定类型的线性函数,你想要做的事情并没有完全为你做好。例如,有一个叫做张量回归和张量分解的领域,目前正在得到很大的发展,它们讨论的是如何将高阶张量转化为行、列和面的组合。事实证明,当你这样做时,你基本上可以处理非常高维的数据结构,而不需要太多的内存和计算时间。例如,有一个非常棒的库叫做TensorLy,它为你做了很多这样的事情。所以这是一个非常重要的领域。它涵盖了所有的深度学习,还有很多现代机器学习。所以即使你不会定义矩阵乘法,你很可能会想要找到一些其他略有不同的张量积。所以了解如何做这个是非常有用的。

让我们回过头来看看我们的二维数组和一维数组,秩为 2 的张量和秩为 1 的张量[1:14:27]。

m, c
'''
(array([[1, 2, 3],
        [4, 5, 6],
        [7, 8, 9]]), array([10, 20, 30]))
'''

记住,我们可以使用@符号或旧方法np.matmul进行矩阵乘法。当我们这样做时,实际上我们基本上是在说1*10 + 2*20 + 3*30 = 140,所以我们对每一行都这样做,然后我们可以继续对下一行做同样的事情,依此类推以获得我们的结果。

m @ c  # np.matmul(m, c)
'''
array([140, 320, 500])
'''

你也可以在 PyTorch 中这样做

T(m) @ T(c)
'''
140
 320
 500
[torch.LongTensor of size 3]
'''

但是(m * c)这不是矩阵乘法。那是什么?逐元素广播。但请注意,它创建的数字[10, 40, 90]正是我在做矩阵乘法的第一部分时需要计算的三个确切数字(1*10 + 2*20 + 3*30)。

m * c
'''
array([[ 10,  40,  90],
       [ 40, 100, 180],
       [ 70, 160, 270]])
'''

换句话说,如果我们对列求和,即轴等于 1,我们就得到了矩阵向量乘积:

(m * c).sum(axis=1)
'''
array([140, 320, 500])
'''

所以我们可以在不依赖库的特殊帮助下做这些事情。现在让我们将这扩展到矩阵矩阵乘积。

matrixmultiplication.xyz/

矩阵矩阵乘积看起来是这样的。有一个很棒的网站叫做matrixmultiplication.xyz,它向我们展示了当我们将两个矩阵相乘时会发生什么。从操作的角度来看,这就是矩阵乘法。换句话说,我们刚刚做的是首先取第一列和第一行相乘得到 15:

然后我们取第二列和第一行得到 27:

所以我们基本上是在做我们刚刚做的事情,矩阵向量乘积,我们只是做了两次。一次用这一列(左边),一次用那一列(右边),然后我们把这两个连接在一起。所以我们现在可以继续这样做:

(m * n[:,0]).sum(axis=1)
'''
array([140, 320, 500])
'''
(m * n[:,1]).sum(axis=1)
'''
array([ 25, 130, 235])
'''

这就是我们矩阵乘法的两列。

我不想让我们的代码太乱,所以我不打算真的使用那个,但是现在我们有了它,如果我们想要的话。我们不再需要使用 torch 或 numpy 矩阵乘法。我们有自己的方法可以使用,只使用逐元素操作、广播和求和。

这是我们从头开始的逻辑回归类[1:18:37]。我只是把它复制到这里。

class LogReg(nn.Module):
    def __init__(self):
        super().__init__()
        self.l1_w = get_weights(28*28, 10)  # Layer 1 weights
        self.l1_b = get_weights(10)         # Layer 1 bias

    def forward(self, x):
        x = x.view(x.size(0), -1)
        x = x @ self.l1_w + self.l1_b 
        return torch.log(softmax(x))

这是我们实例化对象的地方,复制到 GPU。我们创建一个优化器,我们将在稍后学习。然后我们调用 fit。

net2 = LogReg().cuda()
opt=optim.Adam(net2.parameters())

fit(net2, md, n_epochs=1, crit=loss, opt=opt, metrics=metrics)
'''
[ 0\.       0.31102  0.28004  0.92406]
'''

编写我们自己的训练循环[1:18:53]

所以目标是现在重复这一过程,而无需调用 fit。为此,我们需要一个循环,每次抓取一个小批量的数据。对于每个小批量的数据,我们需要将其传递给优化器,并说“请尝试为这个小批量提供稍微更好的预测”。

正如我们学到的,为了一次抓取训练集的一个小批次,我们必须向模型数据对象请求训练数据加载器。我们必须将其包装在iter中以创建一个迭代器或生成器。这样就给我们了我们的数据加载器。所以 PyTorch 将其称为数据加载器。我们实际上编写了自己的 Fast AI 数据加载器,但基本上是相同的思路。

dl = iter(md.trn_dl) # Data loader

接下来我们要做的是获取 x 和 y 张量,从我们的数据加载器中获取下一个。将其包装在Variable中以表明我需要能够对使用此计算的导数进行求导。因为如果我不能求导,那么我就无法得到梯度,也无法更新权重。而且我需要将其放在 GPU 上,因为我的模块在 GPU 上(net2 = LogReg().cuda())。所以现在我们可以将该变量传递给我们实例化的对象(即我们的逻辑回归)。记住,我们的模块,我们可以将其用作函数,因为这就是 PyTorch 的工作原理。这给我们提供了一组预测,就像我们以前看到的那样。

xt, yt = next(dl)
y_pred = net2(Variable(xt).cuda())

现在我们可以检查损失[1:20:41]。我们定义损失为负对数似然损失对象。我们将在下一课中学习如何计算它,现在,只需将其视为分类问题的均方根误差。所以我们也可以像调用函数一样调用它。所以你可以看到这在 PyTorch 中是一个非常普遍的想法,将一切都理想地视为函数。在这种情况下,我们有一个负对数似然损失对象,我们可以将其视为函数。我们传入我们的预测和实际值。同样,实际值需要转换为变量并放在 GPU 上,因为损失是我们实际想要求导的东西。这给我们带来了我们的损失,就是这样。

l = loss(y_pred, Variable(yt).cuda())
print(l)
'''
Variable containing:
 2.4352
[torch.cuda.FloatTensor of size 1 (GPU 0)]
'''

这是我们的损失 2.43。所以它是一个变量,因为它是一个变量,它知道它是如何计算的。它知道它是用这个损失函数(loss)计算的。它知道预测是用这个网络(net2)计算的。它知道这个网络由这些操作组成:

所以我们可以自动获取梯度。要获取梯度,我们调用l.backward()。记住l是包含我们损失的东西。所以l.backward()是添加到任何变量的东西。然后调用.backward(),这表示请计算梯度。这样就计算了梯度并将其存储在内部,基本上对于用于计算的每个权重/参数,现在都存储在.grad中,我们稍后会看到,但基本上存储了梯度。然后我们可以调用optimizer.step(),我们很快将手动执行这一步。这部分表示请让权重变得更好一点。

optimizer.step [1:22:49]

因此,optimizer.step()正在做的是,如果你有一个非常简单的函数像这样,优化器所做的就是说好的,让我们选择一个起始点,计算损失的值,计算导数告诉我们哪个方向是向下的。因此,它告诉我们我们需要朝那个方向走。然后我们迈出一小步。

然后我们再次取导数,采取一个小步骤,并重复,直到最终我们采取的步骤如此之小以至于停止。

这就是梯度下降的作用。小步骤有多大?基本上在这里取导数,所以让我们说导数是 8。然后我们乘以一个小数,比如 0.01,这告诉我们要采取什么步骤大小。这里的这个小数被称为学习率,它是设置的最重要的超参数。如果你选择的学习率太小,那么你的下降步骤将会很小,而且会花费很长时间。学习率太大,你会跳得太远,然后你会跳得太远,最终会发散而不是收敛。

在这节课中我们不会讨论如何选择学习率,但在深度学习课程中,我们实际上向你展示了一种非常可靠地选择一个非常好的学习率的特定技术。

因此,基本上正在发生的是,我们计算导数,我们调用执行step的优化器,换句话说,根据梯度和学习率更新权重。

希望在这样做之后,我们的损失比之前更好。因此,我刚刚重新运行了这个,得到了一个 4.16 的损失。

一步之后,现在是 4.03。

所以它按照我们希望的方式运行,基于这个小批量,它更新了我们网络中的所有权重,使它们比之前更好。因此,我们的损失下降了。

训练循环

因此,让我们将其转化为一个训练循环。我们将进行一百步:

  • 从数据加载器中获取另一个小批量数据

  • 从我们的网络中计算预测

  • 从预测和实际值计算我们的损失

  • 每 10 次,我们将打印出准确率,只需取平均值,看它们是否相等。

  • 一个 PyTorch 特定的事情,你必须将梯度清零。基本上,你可以有许多不同的损失函数的网络,你可能想要将所有的梯度加在一起。因此,你必须告诉 PyTorch 何时将梯度设置为零。因此,这只是说将所有的梯度设置为零。

  • 计算梯度,这被称为反向传播

  • 然后进行一步优化器,使用梯度和学习率更新权重

for t in range(100):
  xt, yt = next(dl)
  y_pred = net2(Variable(xt).cuda())
  l = loss(y_pred, Variable(yt).cuda())

  if t % 10 == 0:
    accuracy = np.mean(to_np(y_pred).argmax(axis=1) == to_np(yt))
    print("loss: ", l.data[0], "\t accuracy: ", accuracy)

  optimizer.zero_grad()
  l.backward()
  optimizer.step()
'''
loss:  2.2104923725128174 	 accuracy:  0.234375
loss:  1.3094730377197266 	 accuracy:  0.625
loss:  1.0296542644500732 	 accuracy:  0.78125
loss:  0.8841525316238403 	 accuracy:  0.71875
loss:  0.6643403768539429 	 accuracy:  0.8125
loss:  0.5525785088539124 	 accuracy:  0.875
loss:  0.43296846747398376 	 accuracy:  0.890625
loss:  0.4388267695903778 	 accuracy:  0.90625
loss:  0.39874207973480225 	 accuracy:  0.890625
loss:  0.4848807752132416 	 accuracy:  0.875
'''

一旦我们运行它,你会看到损失下降,准确率上升。所以这是基本的方法。下一课,我们将看到optimizer.step()做了什么。我们将详细看一下。我们不会深入研究l.backward(),因为我说过我们基本上会将导数的计算视为给定的。但基本上,在任何深度网络中,你有一个类似于线性函数的函数,然后将其输出传递到另一个可能类似于 ReLU 的函数中。然后将其输出传递到可能是另一个线性层的函数中,依此类推:

i( h( g( f(x) ) ) )

因此,这些深度网络只是函数的函数的函数。因此,你可以用数学方式写出它们。因此,反向传播所做的就是说(让我们将其简化为深度为二的版本),我们可以说:

g( f(x) )
u = f(x)

因此,我们可以用链式法则计算g(f(x))的导数:

g'(u) f'(x)

所以你可以看到,我们可以对函数的函数的函数做同样的事情。因此,当你将一个函数应用于一个函数的函数时,你可以通过将这些层的导数的乘积来计算导数。在神经网络中,我们称之为反向传播。因此,当你听到反向传播时,它只是意味着使用链式法则来计算导数。

所以当你看到一个神经网络像这样定义时:

如果按顺序定义,字面上,所有这意味着将这个函数应用于输入,将这个函数应用于那个,将这个函数应用于那个,依此类推。因此,这只是定义了一个函数到一个函数到一个函数到一个函数的组合。因此,虽然我们不打算自己计算梯度,但现在你可以看到为什么它可以这样做,只要它内部知道幂函数的导数是什么,正弦函数的导数是什么,加法的导数是什么,依此类推。然后我们在这里的 Python 代码只是将这些东西组合在一起。因此,它只需要知道如何用链式法则将它们组合在一起,然后就可以运行了。

所以我认为我们现在可以把它留在这里,在下一堂课中,我们将看看如何编写我们自己的优化器,然后我们将自己从头解决 MNIST 问题。到时见!

机器学习 1:第 10 课

原文:medium.com/@hiromi_suenaga/machine-learning-1-lesson-10-6ff502b2db45

译者:飞龙

协议:CC BY-NC-SA 4.0

来自机器学习课程的个人笔记。随着我继续复习课程以“真正”理解它,这些笔记将继续更新和改进。非常感谢 JeremyRachel 给了我这个学习的机会。

pip 上的 Fast AI [0:00]

欢迎回到机器学习!这周最令人兴奋的事情当然是 Fast AI 现在在 pip 上了,所以你可以pip install fastai

[## fastai

fastai 使得使用 PyTorch 进行深度学习更快、更准确、更容易

pypi.org](https://pypi.org/project/fastai/?source=post_page-----6ff502b2db45--------------------------------)

最简单的方法可能仍然是执行conda env update,但有几个地方更方便的是执行pip install fastai,如果你在笔记本之外的地方工作,那么这将使你在任何地方都可以访问 Fast AI。他们还向 Kaggle 提交了一个拉取请求,试图将其添加到 Kaggle 内核中。所以希望你很快就能在 Kaggle 内核上使用它。你可以在工作中或其他地方使用它,所以这很令人兴奋。我不会说它已经正式发布了。显然,现在还很早,我们仍在添加(你也在帮助添加)文档和所有这些东西。但很高兴现在有这个。

Kaggle 内核 [1:22]

这周有几个来自 USF 学生的很酷的内核。我想强调两个都来自文本规范化竞赛的内核,该竞赛旨在尝试将标准英语文本转换为文本,还有一个俄语的。你要尝试识别可能是“第一,第二,第三”之类的东西,并说这是一个基数,或者这是一个电话号码或其他什么。我快速搜索了一下,发现学术界曾尝试使用深度学习来做这个,但他们没有取得太多进展,实际上我注意到Alvira 的内核在这里得到了 0.992 的排名,我认为是前 20 名。这完全是启发式的,是特征工程的一个很好的例子。在这种情况下,整个事情基本上完全是特征工程。基本上是通过查看和使用大量正则表达式来弄清楚每个标记是什么。我认为她在这里做得很好,清楚地列出了所有不同的部分以及它们如何相互配合。她提到她也许希望将这个变成一个库,我认为这将是很好的。你可以使用它来提取文本中的所有部分。这是自然语言处理社区希望能够做到的事情,而不需要像这样大量手写代码。但目前,我很感兴趣看看获胜者到底做了什么,但我还没有看到机器学习被用来做这个特别好。也许最好的方法是将这种特征工程与一些机器学习结合起来。但我认为这是一个有效特征工程的很好例子。

这位是另一位 USF 的学生,她做了类似的事情,得到了类似的分数,但使用了自己不同的规则。同样,这也会让你在排行榜上获得一个不错的位置。所以我觉得看到我们的一些学生参加比赛并通过基本的手写启发式方法获得前 20 名结果的例子很有趣。这就是,例如,六年前的计算机视觉仍然是这样。基本上最好的方法是大量仔细手写的启发式方法,通常结合一些简单的机器学习。所以我认为随着时间的推移,这个领域肯定在努力向更多自动化方向发展。

波尔图塞古罗的安全驾驶员预测获奖者 [4:21]

实际上有趣的是,在刚刚结束的安全驾驶员预测比赛中,一个 Netflix 奖获得者赢得了这个比赛,他发明了一种处理结构化数据的新算法,基本上不需要任何特征工程。所以他使用了五个深度学习模型和一个梯度提升机获得了第一名。他的基本方法与我们迄今为止在这门课程中学到的以及明天将要学习的内容非常相似,即使用全连接的神经网络和独热编码,特别是我们将要学习的嵌入。但他有一个非常聪明的技巧,就是在这个比赛中有很多未标记的数据。换句话说,他们不知道那个司机是否会提出索赔。所以当你有一些标记和一些未标记的数据时,我们称之为半监督学习。在现实生活中,大多数学习都是半监督学习。在现实生活中,通常有一些被标记的东西和一些未被标记的东西。所以这是最实用的学习方式。而结构化数据是公司日常处理的最常见的数据类型。所以这个比赛是一个半监督的、结构化数据比赛,使其非常实用。

所以他赢得比赛的技巧是进行数据增强,你们在深度学习课程中学到的,基本上就是这样的想法,比如如果你有图片,你会水平翻转它们或者稍微旋转一下。数据增强意味着创建新的数据示例,这些示例是你已经拥有的数据的略微不同的版本。他的做法是对于数据中的每一行,他会随机地用另一行替换 15%的变量。所以现在每一行代表的是原始行的 85%混合,15%是随机选择的另一行。这是一种随机改变数据的方法,然后他使用了一种叫做自动编码器的东西,我们可能要等到深度学习课程的第二部分才会学习,但自动编码器的基本思想是你的因变量与自变量相同。换句话说,你试图预测你的输入,如果你被允许进行恒等变换,例如,显然可以轻松地预测输入,但自动编码器的技巧是至少在一个层中具有比输入更少的激活。所以如果你的输入是一个百维向量,你通过一个 100 乘以 10 的矩阵,创建十个激活,然后必须从中创建原始的 100 维向量。那么你基本上已经有效地对其进行了压缩。事实证明,这种类型的神经网络被迫在数据中找到相关性、特征和有趣的关系,即使数据没有标记。所以他使用了这个而不是进行任何手工工程。这些是一些有趣的方向,如果你继续进行机器学习研究,特别是如果你明年参加深度学习课程的第二部分,你会学到的。你可以看到特征工程正在消失,这就是刚刚一个小时前。所以这确实是非常近期的。但这是我长时间以来看到的最重要的突破之一。

MNIST 继续[8:32]

笔记本

这(LogReg)是我们创建的那个小手写的nn.Module类[9:15]。我们定义了损失。我们定义了学习率,我们定义了优化器。optim.SGD是我们接下来要尝试手写的东西。所以nn.NLLLossoptim.SGD,我们是从 PyTorch 中借鉴的,但我们自己编写了模块LogReg和训练循环。

net2 = LogReg().cuda()
loss=nn.NLLLoss()
learning_rate = 1e-2
optimizer=optim.SGD(net2.parameters(), lr=learning_rate)
for epoch in range(1):
    losses=[]
    dl = iter(md.trn_dl)
    for t in range(len(dl)):
        # Forward pass: compute predicted y and loss by passing x to
        # the model.
        xt, yt = next(dl)
        y_pred = net2(V(xt))
        l = loss(y_pred, V(yt))
        losses.append(l) 
        # Before the backward pass, use the optimizer object to zero
        # all of the gradients for the variables it will update
        # (which are the learnable weights of the model)
        optimizer.zero_grad() 
        # Backward pass: compute gradient of the loss with respect
        # to model parameters
        l.backward() 
        # Calling the step function on an Optimizer makes an update
        # to its parameters
        optimizer.step()

    val_dl = iter(md.val_dl)
    val_scores = [score(*next(val_dl)) for i in range(len(val_dl))]
    print(np.mean(val_scores))

所以基本思想是我们将经历一些时期[9:39],所以让我们经历一个时期。我们将跟踪每个小批次的损失,以便在最后报告。我们将把我们的训练数据加载器转换为迭代器,以便我们可以循环遍历它 - 遍历每个小批次。现在我们可以继续说for张量in数据加载器的长度,然后我们可以调用next来从该迭代器中获取下一个自变量和因变量。

然后记住,我们可以通过调用模型来将 x 张量(xt)传递给我们的模型,就好像它是一个函数一样。但首先,我们必须将其转换为一个变量。上周,我们在输入Variable(blah).cuda()来将其转换为一个变量,Fast AI 中的一个简写是大写V。所以张量的大写T,变量的大写V。这只是 Fast AI 中的一个快捷方式。这将返回我们的预测。

接下来我们需要做的是计算我们的损失,因为如果我们没有计算损失,就无法计算损失的导数。所以损失接受预测值和实际值。实际值再次是 y 张量,我们必须将其转换为一个变量。变量跟踪所有计算步骤。实际上,在 PyTorch 网站上有一个很棒的教程。

接下来我们需要做的是计算我们的损失,因为如果我们没有计算损失,就无法计算损失的导数。所以损失接受预测值和实际值。实际值再次是 y 张量,我们必须将其转换为一个变量。变量跟踪所有计算步骤。实际上,在 PyTorch 网站上有一个很棒的教程。在 PyTorch 网站上,有一个教程部分,其中有一个关于 Autograd 的教程。Autograd 是随 PyTorch 一起提供的自动微分包的名称,它是自动微分的实现。因此,Variable 类是真正的关键类,因为它可以将张量转换为我们可以跟踪其梯度的东西。基本上在这里他们展示了如何创建一个变量,对变量进行操作,然后可以回头查看 grad 函数(grad_fn),这是它跟踪以计算梯度的函数。因此,当我们对这个变量和从该变量计算出的变量进行更多操作时,它会继续跟踪。稍后,我们可以进行.backward(),然后打印.grad并找出梯度。所以你会注意到我们从未定义过梯度,我们只是定义它为(x + 2)² * 3之类的表达式,它可以计算梯度。

这就是为什么我们需要将其转换为一个变量。所以l现在是一个包含损失的变量。它包含了这个小批量的损失的单个数字。但它不仅仅是一个数字。它是一个作为变量的数字,所以它是一个知道如何计算的数字。

我们将把这个损失添加到我们的数组中,这样我们以后可以得到它的平均值。现在我们要计算梯度。所以l.backward()是一个指令,表示计算梯度。所以记住当我们调用网络时,实际上是调用我们的前向函数。这就像是向前走一遍。然后向后就像是使用链式法则向后计算梯度。

optimizer.step()是我们即将编写的内容,即根据梯度和学习率更新权重。zero_grad(),我们将在手动编写时解释。

然后最后,我们可以将验证数据加载器转换为迭代器。然后我们可以遍历它的长度,每次取出一个 x 和 y,并询问我们定义的分数,即你预测了哪个,实际上是哪个,并检查它们是否相等。然后这些的平均值将是我们的准确率。

def score(x, y):
    y_pred = to_np(net2(V(x)))
    return np.sum(y_pred.argmax(axis=1) == to_np(y))/len(y_pred)

问题: 为什么你要将其转换为迭代器而不是使用普通的 Python 循环[14:53]?我们正在使用普通的 Python 循环,所以问题实际上是与什么进行比较。所以,也许你在考虑的替代方案可能是,我们可以使用类似带有索引器的列表。问题在于我们每次获取一个新的小批次时,我们希望它是随机的。我们希望有一个不同的洗牌过的东西。所以这个for t in range(len(dl)),你实际上可以无限迭代。你可以循环遍历它任意次数。所以这种想法在不同的语言中被称为不同的东西,但很多语言称之为流处理,这是一种基本的想法,而不是说我想要第三个或第九个东西,而是说我想要下一个东西。这对网络编程非常有用——从网络中获取下一个东西。对于 UI 编程也非常有用——获取下一个事件,比如有人点击了一个按钮。事实证明,这对于这种数值编程也非常有用——就像我只想要下一个数据批次。这意味着数据可以是任意长的,因为我们一次只获取一部分。我想简短的回答是因为这是 PyTorch 的工作方式。PyTorch 的数据加载器被设计为以这种方式调用。所以 Python 有这个生成器的概念,它是一种可以创建行为像迭代器的函数的方式。Python 已经认识到这种流处理编程方法非常方便和有用,并且在各处支持它。所以基本上任何你使用for ... in循环的地方,任何你使用列表推导的地方,这些东西都可以是生成器或迭代器。通过这种方式编程,我们获得了很大的灵活性。听起来对吗,Terrence?你是编程语言专家。

Terrence: 是的,我的意思是你说的很对。你可能会提到空间的问题,但在这种情况下,所有这些数据都必须在内存中,因为我们有...

Jeremy: 不需要在内存中。事实上,大多数情况下,使用 PyTorch,小批次将根据需要从分布在磁盘上的单独图像中读取,所以大多数情况下它不在内存中。

Terrence: 但一般来说,你希望尽可能少地一次性保存在内存中。所以流处理的想法也很棒,因为你可以进行组合。你可以将数据传送到另一台机器。

Jeremy: 是的,组合很棒。你可以从这里获取下一个东西,然后将其发送到下一个流中,然后你可以获取它并做其他事情。

Terrence: 你们都认识到,当然,在命令行管道和 I/O 重定向中。

Jeremy: 谢谢,Terrence。与真正知道自己在谈论什么的人一起工作的好处。

实现随机梯度下降[18:24]

现在让我们拿掉优化器。我们唯一剩下的是负对数似然损失函数,实际上我们也可以替换。我们在笔记本中有 Yannet 从头开始编写的实现。正如我们之前学到的那样,只需要一行代码。你可以用一个 if 语句来做到。所以我不知道为什么我如此懒惰,要包括这个。

所以我们要做的是,再次获取我们自己编写的模块(逻辑回归模块)。我们将再次进行一个 epoch。我们将循环遍历迭代器中的每个元素。我们将获取我们的独立和依赖变量用于小批量,将其传递给我们的网络,计算损失。所以这一切和以前一样,但现在我们要摆脱optimizer.step(),我们要手动做。所以基本的技巧是,正如我提到的,我们不会手动进行微积分。我们调用l.backward()来自动计算梯度,这将填充我们的矩阵。这就是我们构建的模块:

所以线性层权重的权重矩阵,我们称为l1_w,偏差我们称为l1_b。它们是我们创建的属性。所以我只是把它们放到了叫做wb的东西里,基本上是为了节省一些输入。所以 w 是我们的权重,b 是我们的偏差。所以权重,记住权重是一个变量,要从变量中获取张量,我们必须使用.data。所以我们想要更新这个变量中的实际张量,所以我们说:

w.data -= w.grad.data * lr

  • -=我们想要朝着梯度的相反方向前进。梯度告诉我们哪个方向是向上的。我们想要向下。

  • w.grad.data * lr当前梯度乘以学习率。

这就是梯度下降的公式。正如你所看到的,这是你可能想象到的最简单的事情。就像更新权重等于它们现在的值减去梯度乘以学习率一样。对偏差也是同样的操作。

net2 = LogReg().cuda()
loss_fn=nn.NLLLoss()
lr = 1e-2
w,b = net2.l1_w,net2.l1_b

for epoch in range(1):
    losses=[]
    dl = iter(md.trn_dl)
    for t in range(len(dl)):
        xt, yt = next(dl)
        y_pred = net2(V(xt))
        l = loss(y_pred, Variable(yt).cuda())
        losses.append(loss)

        # Backward pass: compute gradient of the loss with respect 
        # to model parameters
        l.backward()
        w.data -= w.grad.data * lr
        b.data -= b.grad.data * lr

        w.grad.data.zero_()
        b.grad.data.zero_()   

    val_dl = iter(md.val_dl)
    val_scores = [score(*next(val_dl)) for i in range(len(val_dl))]
    print(np.mean(val_scores))

问题:当我们在顶部执行next时,当循环结束时,我们如何获取下一个元素?所以这个(**for** t **in** range(len(dl)):)是在长度范围内的每个索引进行循环,所以这是 0、1、2...在这个循环结束时,它将打印出验证集的平均值,然后回到 epoch 的开始,在这一点上,它将创建一个新的迭代器。所以基本上在 Python 的后台当你调用iter(md.trn_dl)时,它基本上告诉它重置其状态以创建一个新的迭代器。如果你对它是如何工作感兴趣,所有的代码都可以供你查看。md.trn_dlfastai.dataset.ModelDataLoader,所以我们可以看一下它的代码,看看它是如何构建的。所以你可以在这里看到,__next__函数跟踪了它在self.i中经历了多少次,这里是__iter__函数,当你创建一个新的迭代器时会调用这个函数。你可以看到它将其传递给另一个类型为 DataLoader 的东西,然后如果你对它是如何实现的感兴趣,你可以查看 DataLoader。

所以我们编写的 DataLoader 基本上使用多线程,允许同时进行多个操作。这非常简单。只有大约一屏的代码,所以如果你对简单的多线程编程感兴趣,这是一个值得一看的好东西。

问题:为什么你把这个包装在for epoch in range(1)中,因为这只会运行一次?因为在现实生活中,我们通常会运行多个 epochs。就像在这种情况下,因为这是一个线性模型,它实际上在一个 epoch 内训练到了它能达到的最好状态,所以如果我在这里输入 3,你会看到在第一个 epoch 之后它实际上不会有太大的改进。但当我们回到顶部时,我们将看一些稍微更深入和更有趣的版本,这将需要更多的 epochs。所以如果我要把这个转换成一个函数,我会像这样写def train_mdl,你会传入一些 epochs 的数量之类的东西。

要记住的一件事是,当你创建这些神经网络层时,记住这个(LogReg())在 PyTorch 看来只是一个 nn.Module ——我们可以将其用作一个层,我们可以将其用作一个函数,我们可以将其用作一个神经网络。PyTorch 不认为这些是不同的东西。因此,这可能是另一个网络中的一层。那么梯度是如何工作的呢?所以如果你有一个层,我们可以将其看作是激活或通过某些其他非线性/线性激活函数计算出的一些激活。然后从该层,很可能我们会将其通过矩阵乘积来创建一些新层。因此,如果我们抓取像这样的一个激活,实际上会用来计算每一个输出。

因此,如果要计算导数,你必须知道这个权重矩阵如何影响每个输出,并将所有这些加在一起以找到一个激活在所有输出上的总影响。这就是为什么在 PyTorch 中你必须告诉它何时将梯度设置为零。因为想法是你可能有很多不同的损失函数或者下一组激活中的很多不同的输出,所有这些都会增加或减少你的梯度。所以你基本上必须说,好的,这是一个新的计算 — 重置。所以这就是我们这样做的地方:

在我们执行l.backward()之前,我们说重置。所以让我们拿出我们的权重,拿出梯度,拿出它们指向的张量,然后zero_。在 PyTorch 中,下划线作为后缀意味着“原地”,听起来像一个小技术细节,但记住这一点非常有用。几乎每个函数都有一个下划线版本的后缀,它会原地执行操作。所以通常 zero 返回一个特定大小的零张量,所以 zero_ 意味着用一堆零替换这个内容。

好了,就是这样。这就是从头开始的 SGD。如果我去掉我的菜单栏,我们可以正式说它适合在一个屏幕内。当然,我们还没有得到我们的逻辑回归的定义,那是另外半个屏幕,但基本上没有太多内容。

问题:为什么我们需要多个 epochs?简单回答就是,假设我们的学习率很小。那么它就不会走得很远。没有什么规定说通过一个 epoch 就足以让你达到目标。所以这就像是,好吧,让我们增加学习率。当然,我们可以增加学习率,但谁能说最高的学习率是稳定学习的足够多呢。对于大多数数据集和大多数架构来说,一个 epoch 很少能让你达到最好的结果。线性模型的行为非常好。所以你通常可以使用更高的学习率并更快地学习。此外,通常无法获得如此好的准确性,因此也没有太多的提升空间。因此,进行一个 epoch 将是罕见的。

向后走

让我们往回走。所以往回走,我们基本上会说让我们不要一遍又一遍地写这些代码(在左侧)。让别人替我们做。

这些版本之间唯一的区别就是,不是我们自己说.zero_-= gradient * lr,而是这些操作已经为我们封装好了(在右侧)。

这里还有一个问题,即左侧更新权重的方法实际上效率很低。它没有利用动量和曲率。因此,在深度学习课程中,我们也学习了如何从头开始实现动量。因此,如果我们实际上只是使用普通的 SGD 而不是 Adam,它们实际上是完全相同的,你会看到左侧版本学习得更慢。

让我们自动做更多的事情。考虑到每次训练时,我们必须循环遍历 epoch、batch,进行前向传播,计算损失,梯度清零,反向传播,优化器进行一步操作,让我们把所有这些放在一个函数中。这个函数被称为fit

就是这样。让我们看看 fit:

然后这是步骤:

梯度清零,计算损失(记住,PyTorch 倾向于称之为准则而不是损失),进行反向传播。然后,还有一些我们在这里没有学到的东西,但我们在深度学习课程中学到的,那就是“梯度裁剪”,所以你可以忽略它。所以你可以看到,我们学到的所有东西,当你查看实际框架内部时,那就是你看到的代码。这就是 fit 的作用。

然后下一步就是有一些权重和偏差,进行矩阵乘法和加法,让我们把它放在一个函数中。进行对数 softmax 的操作,让我们把它放在一个函数中。然后首先进行这个操作,然后进行那个操作,将函数链接在一起的想法,让我们把它放在一个函数中。最终我们得到了:

所以 Sequential 简单地意味着通过这个函数执行这个操作,将结果传递给这个函数,依此类推。而 Linear 意味着创建权重矩阵,创建偏差。就是这样。

然后,正如我们开始讨论的那样,通过将其放入 100 个激活中,而不是直接将其发送到 10 个激活中,我们可以将其转换为一个深度神经网络。我们可以选择任何我们喜欢的数字。通过 ReLU 使其非线性化,通过另一个线性层,再通过一个 ReLU,然后通过我们的最终输出和最终激活函数。

现在这是一个深度网络。所以我们可以拟合它。这一次,因为它更深了,我实际上要运行更多的 epochs。你可以看到准确性在增加:

如果你尝试将学习率从 0.1 进一步增加,它实际上开始变得不稳定。

学习率退火

我会告诉你一个技巧。这被称为学习率退火,技巧就是这样。当你试图拟合一个函数时,你已经走了几步。当你接近底部时,你的步伐可能会变得更小。否则,通常会发生的情况是你发现自己在同样的地方来回摆动。

你可以在上面的准确性中实际看到它开始变得平坦。这可能是因为它已经做得尽可能好了,或者可能是因为它在前进和后退。所以在训练后期降低学习率并采取更小的步骤是个好主意。这就是所谓的学习率退火。在 Fast AI 中有一个名为set_lrs(设置学习率)的函数,你可以传入你的优化器和新的学习率,看看是否有帮助。很多时候确实有帮助。你应该减少大约一个数量级。在深度学习课程中,我们学习了一种比这更好的技术,可以自动进行学习率退火并在更细粒度的级别上进行。但如果你手动操作,一次减少一个数量级是人们通常做的事情。

你会看到论文中谈论学习率调度,这就像一个学习率调度。这个调度让我们达到了 97%。我尝试继续下去,但似乎我们无法比这更好。所以我们得到了一个可以达到 97%准确性的东西。

问题:我有一个关于数据加载的问题。我知道这是一个 Fast AI 函数,但你能详细介绍一下它是如何创建批次的,如何完成的,以及如何做出这些决定吗?当然。基本上,PyTorch 中有一个非常好的设计,他们基本上说让我们创建一个叫做数据集的东西。数据集基本上看起来像一个列表。它有一个长度(例如数据集中有多少图像),并且可以像列表一样进行索引。所以如果你有数据集d,你可以这样做:

d = Dataset(...)
len(d)
d[i]

这基本上就是 PyTorch 关心的数据集。所以你从数据集开始,就像d[3]给你第三张图像,等等。所以你拿一个数据集,你可以把它传递给一个数据加载器的构造函数dl = DataLoader(d)。这会给你一个现在可迭代的东西。所以你现在可以说iter(dl),这是你可以调用 next 的东西(即next(iter(dl)))。当你调用数据加载器的构造函数时,你可以选择打开或关闭洗牌。打开洗牌意味着给我随机的小批量,关闭洗牌意味着按顺序进行。所以当你调用next时,假设你说shuffle=True并且批量大小是 64,它会在 0 到长度之间抓取 64 个随机整数,并调用这个(d[i])64 次以获取 64 个不同的项目并将它们组合在一起。所以 Fast AI 使用完全相同的术语和完全相同的 API。我们只是以不同的方式处理一些细节。特别是在计算机视觉中,你经常想要做很多数据增强,比如翻转、稍微改变颜色、旋转,这些都是计算密集型的。甚至只是读取 JPEG 文件也是计算密集型的。所以 PyTorch 使用一种方法,即启动多个处理器并行进行处理,而 Fast AI 库则使用一种称为多线程的方法,这可能是更快的方法。

问题:在“epoch”中,所有元素都会被返回一次吗?是在 epoch 开始时进行洗牌吗?是的,并非所有库都以相同的方式工作。有些会进行有放回抽样,有些则不会。Fast AI 库实际上将洗牌交给了实际的 PyTorch 版本,我相信 PyTorch 版本实际上会进行洗牌,一个 epoch 会覆盖所有元素,我相信。

现在问题是,当你开始使用这些更大的网络时,潜在地你会得到相当多的参数。我想要求你计算一下有多少参数,但让我们记住这里我们有 28 乘以 28 的输入进入 100 个输出,100 进入 10。然后对于每一个,我们有权重和偏置。

所以我们实际上可以这样做。net.parameters返回一个列表,列表中的每个元素都是一个包含参数的张量,不仅仅是那一层,如果是一个既有权重又有偏置的层,那就是两个参数。所以基本上给我们返回了一个包含所有参数的张量的列表。PyTorch 中的numel告诉你它有多大。

所以如果我运行这个,这里是每一层中的参数数量。所以我有 784 个输入,第一层有 100 个输出,因此第一个权重矩阵的大小是 78400。第一个偏置向量的大小是 100。然后下一个是 100 乘以 100,有 100。然后下一个是 100 乘以 10,10 是偏置。所以每一层中的元素数量就是这样。我把它们加起来,差不多有十万个。所以我可能有过拟合的风险。所以我们可能需要考虑使用正则化。

正则化

在所有机器学习中,一种非常简单常见的正则化方法叫做 L2 正则化。这非常重要,非常方便,你可以将它用于几乎任何东西。基本思想是这样的。通常我们会说我们的损失等于(让我们用 RMSE 来保持简单)我们的预测减去我们的实际值的平方,然后求和,取平均值,再开平方。

如果我们想说,如果我有很多参数,除非它们真的足够有用,否则不要使用它们。如果你有一百万个参数,而你只真正需要 10 个参数来有用,那就只用 10 个。那么我们如何告诉损失函数做到这一点呢?基本上我们想说的是,嘿,如果一个参数是零,那没问题。就好像它根本不存在一样。所以让我们惩罚一个参数不为零。我们如何衡量这一点呢?我们如何计算我们的参数有多不为零?L1 是权重平均值的绝对值。L2 是权重本身的平方。然后我们想要说好,我们想要惩罚不为零的程度有多大?因为如果我们实际上没有那么多参数,我们根本不想要进行正则化。如果我们有很多参数,我们确实想要进行大量正则化。所以我们加入一个参数 a:

除了可能打印出来之外,我们实际上并不关心损失。我们真正关心的是损失的梯度。aw²的梯度是2aw。所以有两种方法可以做到这一点:

  1. 我们实际上可以修改我们的损失函数来添加这个平方惩罚。

  2. 我们可以修改我们说的权重等于权重减去梯度乘以学习率的那个东西,也加上2aw

这些基本上是等价的,但它们有不同的名称。第一个称为 L2 正则化,第二个称为权重衰减。第一个版本是最初在神经网络文献中提出的,而第二个版本是在统计文献中提出的,它们是等价的。

正如我们在深度学习课程中讨论的那样,事实证明它们并不完全等价,因为当你有动量和 Adam 等因素时,它们的行为可能会有所不同。两周前,一位研究人员找到了一种方法来在现代优化器中实现正确的权重衰减,我们 Fast AI 的一位学生在 Fast AI 库中实现了这一点,因此 Fast AI 现在是第一个实际支持这一功能的库。

无论如何,现在让我们使用 PyTorch 称为权重衰减的版本,但实际上根据两周前的这篇论文,它实际上是 L2 正则化。这并不完全正确,但足够接近。所以在这里,我们可以说权重衰减是 1e-3。

这将把我们的惩罚乘数 a 设置为 1e-3,并将其添加到损失函数中。让我们复制这些单元格,以便我们可以比较它们的训练方式。你可能会注意到这里有一些反直觉的地方[48:54]。0.23547 是我们的训练误差。你可能会期望我们的带有正则化的训练误差会更糟,因为我们正在惩罚那些可以使其更好的参数。然而实际上,它开始时并不是更糟(之前是 0.29756)。这可能是为什么呢?

这样做的原因是,如果你有一个看起来像这样的函数:

训练可能需要很长时间,否则,如果你有一个看起来更像这样的函数:

它会训练得更快。有些事情你可以做,有时只是可以让一个可怕的函数变得不那么可怕。有时候权重衰减实际上可以使你的函数行为更好,这实际上在这里发生了。所以我只是提到这一点是为了说不要让这使你困惑。权重衰减确实对训练集进行惩罚,严格来说,我们最终得到的训练集的数字不应该更好,但有时候它可以更快地训练。

问题:我不明白。为什么会使它更快?训练时间重要吗?不,这是在一个时代之后。底部是我们没有使用权重衰减的训练,顶部是使用了权重衰减的训练。这与时间无关,只与一个时代有关。在一个时代之后,我的观点是,所有其他条件相同,你会预期使用权重衰减的训练集会有更糟的损失,因为我们在惩罚它。我说“哦,不是这样。这很奇怪。”

原因在于,在一个单独的时代中,重要的是你是在尝试优化一个非常崎岖的东西,还是在尝试优化一个平滑的东西。如果你试图优化一个非常崎岖的东西,想象一下在某个高维空间中,你最终会在所有这些不同的管道和隧道中滚动。而如果它只是平滑的,你只是一下就到了。想象一颗大理石滚下山坡,其中一个是旧金山的隆巴德街 - 前进,后退,前进,后退,需要很长时间才能开到尽头。而如果你骑摩托车直接越过山顶,速度就快得多。因此,损失函数表面的形状定义了优化的难易程度。因此,根据这些结果,似乎在这里使用权重衰减使得这个函数更容易优化。

问题:所以只是为了确保,惩罚会使优化器更有可能达到全局最小值吗?不,我不会这么说。我的观点实际上是,最终,它可能在训练集上表现得不太好,确实看起来是这样。最终,在五个时代之后,我们的训练集现在比使用权重衰减时更糟糕。这是我所期望的。就像我从不使用全局最优这个术语,因为我们对此没有任何保证。我们并不真正关心。我们只关心在经过一定数量的时代之后我们到达了哪里。我们希望我们找到了一个好的解决方案。因此,当我们达到一个好的解决方案时,使用权重衰减的训练集,损失更糟糕,因为它是惩罚。但在验证集上,损失更好,因为我们对训练集进行了惩罚,以便尝试创建一个更好泛化的东西。因此,无用的参数现在为零,泛化更好。所以我们所说的只是在一个时代之后它达到了一个好的点。

问题:这总是真的吗?如果你的意思是权重衰减总是使函数表面更加平滑,那么不,这并不总是真的。但值得记住,如果你在训练一个函数时遇到困难,添加一点点权重衰减可能会有所帮助。

问题:通过对参数进行正则化,它的作用是使损失函数表面变得更加平滑?我的意思是,这不是我们这样做的原因。我们这样做的原因是因为我们想惩罚那些不为零的东西,告诉它不要让这个参数变得很大,除非它真的对损失有很大帮助。如果可以的话,将其设置为零,因为将尽可能多的参数设置为零意味着它会更好地泛化。这就像拥有一个更小的网络一样。这就是我们这样做的原因。但它也可以改变学习方式。

我想检查一下我们实际上在这里是怎么做的。所以在第二个时代之后,你可以看到这里确实有帮助。在第二个时代之后,我们之前达到了 97%的准确率,现在我们几乎达到了 98%的准确率。你可以看到损失是 0.08 对 0.13。所以添加正则化使我们能够找到更好的解决方案(3%对 2%)。

问题:所以这有两个部分——一个是 L2 正则化和权重衰减?不,我的观点是它们是同一件事。所以权重衰减是 L2 正则化的版本,如果你只是对 L2 正则化求导,你会得到权重衰减。所以你可以通过改变损失函数来实现它,加入平方损失惩罚,或者你可以通过将权重本身添加到梯度中来实现它。

问题:我们可以在卷积层中使用正则化吗?当然可以。卷积层只是权重。

问题:你能解释一下为什么你认为在这个特定问题中需要权重衰减吗?不容易。我是说,除了说这是我总是会尝试的事情之外。继续提问:过拟合?所以如果我的训练损失高于验证损失,那么我就是欠拟合。所以肯定没有必要正则化。那总是不好的。那总是意味着你的模型需要更多的参数。在这种情况下,我是过拟合的。这并不一定意味着正则化会有帮助,但值得一试。

问题:你如何选择最佳的时代数?你参加我的深度学习课程😆 这是一个很长的故事。我们没有时间在这堂课上讨论最佳实践。我们将学习基础知识。

现代机器学习技术的秘密

在深度学习课程中我们详细讨论的一点是,我认为现代机器学习技术的秘密是对问题的解决方案进行大规模的参数化,就像我们刚刚做的那样。当我们只有少量的 28x28 图像时,我们有大约 10 万个权重,然后使用正则化。这与几十年前几乎所有统计和学习的做法完全相反,大多数大学的大多数领域的高级讲师仍然具有这种背景,他们学习到构建模型的正确方式是尽可能少地使用参数。因此,希望我们迄今已经学到了两件事。一是即使模型有很多参数,我们也可以构建非常准确的模型。随机森林有很多参数,这里的深度网络也有很多参数,它们可以很准确。我们可以通过使用装袋或使用正则化来实现。在神经网络中,正则化意味着权重衰减(也称为“某种程度的” L2 正则化)或者我们在这里不会过多担心的 dropout。这是一种构建有用模型的非常不同的思考方式。我只是想警告你,一旦你离开这个教室,甚至可能当你去听下一个教员的讲座时,甚至在美国旧金山大学也会有完全受过小参数模型训练的人。你的下一个老板可能是在小参数模型的世界中接受过培训的。他们认为这些模型在某种程度上更纯净、更容易、更好、更可解释或者其他什么。我相信这不是真的 - 可能永远不是真的。当然很少是真的。实际上,正如我们从随机森林解释的整个课程中学到的那样,具有大量参数的模型可以非常可解释。你可以使用大部分相同的技术来处理神经网络,但是神经网络更容易。记得我们是如何通过随机化一列来计算特征重要性的吗,以查看该列的变化如何影响输出?嗯,这就像一种愚蠢的计算梯度的方式。改变这个输入会如何改变输出?对于神经网络,我们实际上可以计算其梯度。因此,使用 PyTorch,你实际上可以说输出相对于这一列的梯度是多少?你可以使用相同的方法来使用神经网络进行偏依赖图。对于那些对产生真正影响感兴趣的人,基本上没有人为神经网络编写这些东西。因此,整个领域需要编写库、撰写博客文章。一些论文已经写了,但只在非常狭窄的领域,比如计算机视觉。据我所知,没有人写过关于如何进行结构化数据神经网络解释方法的论文。因此,这是一个非常令人兴奋的大领域。

NLP [1:02:04]

然而,我们将从应用简单的线性模型开始。对我来说有点可怕,因为我们将进行 NLP,而我们的 NLP 专家就在房间里。因此,如果我搞砸了,David,请大声告诉我。NLP 指的是我们处理自然语言文本的任何建模。有趣的是,我们将看一个情况,线性模型在解决特定问题时非常接近最先进技术。几周前,我实际上使用递归神经网络超越了最先进技术,但这实际上将向你展示线性模型非常接近最先进技术。

IMDb

笔记本

我们将使用 IMDb 数据集。这是一个电影评论数据集。您可以按照以下步骤下载它:

To get the dataset, in your terminal run the following commands:wget http://ai.stanford.edu/~amaas/data/sentiment/aclImdb_v1.tar.gzgunzip aclImdb_v1.tar.gztar -xvf aclImdb_v1.tar

一旦你下载了它,你会看到你有一个训练和一个测试目录,在你的训练目录中,你会看到有一个负面和一个正面目录。在你的正面目录中,你会看到有一堆文本文件。

PATH='data/aclImdb/'
names = ['neg','pos']
%ls {PATH}
'''
aclImdb_v1.tar.gz  imdbEr.txt  imdb.vocab  models/  README  test/  tmp/  train/
'''
%ls {PATH}train
'''
aclImdb/  all_val/         neg/  tmp/    unsupBow.feat  urls_pos.txt
all/      labeledBow.feat  pos/  unsup/  urls_neg.txt   urls_unsup.txt
'''
%ls {PATH}train/pos | head
'''
0_9.txt
10000_8.txt
10001_10.txt
10002_7.txt
10003_8.txt
10004_8.txt
10005_7.txt
10006_7.txt
10007_7.txt
10008_7.txt
...
'''

这里是一个文本文件的例子:

trn[0]
'''
"Story of a man who has unnatural feelings for a pig. Starts out with a opening scene that is a terrific example of absurd comedy. A formal orchestra audience is turned into an insane, violent mob by the crazy chantings of it's singers. Unfortunately it stays absurd the WHOLE time with no general narrative eventually making it just too off putting. Even those from the era should be turned off. The cryptic dialogue would make Shakespeare seem easy to a third grader. On a technical level it's better than you might think with some good cinematography by future great Vilmos Zsigmond. Future stars Sally Kirkland and Frederic Forrest can be seen briefly."
'''

所以我们不知何故选出了一个男人对猪有不自然感情的故事作为我们的第一个选择。这并不是故意的,但没关系。

我们将查看这些电影评论,对于每一个,我们将查看它们是积极的还是消极的。所以它们被放入了其中一个文件夹。它们是从 IMDb(电影数据库和评论网站)下载的。那些非常积极的评论放在/pos中,非常消极的评论放在/neg中,而其余的则没有标签(/unsup)。所以只有高度极化的评论。

所以在上面的例子中,我们有一个疯狂的暴力暴民,不幸的是太荒谬了,太令人反感了,那些来自那个时代的人应该被关掉。所以这个的标签是零,即负面的,所以这是一个负面的评论。

trn_y[0]
'''
0
'''

在 Fast AI 库中,有很多函数和类可以帮助你处理大多数机器学习的领域。对于自然语言处理,我们有一个简单的东西,就是来自文件夹的文本。

trn,trn_y = texts_labels_from_folders(f'{PATH}train',names)
val,val_y = texts_labels_from_folders(f'{PATH}test',names)

这将遍历并找到这里的所有文件夹(第一个参数f'{PATH}train')与这些名称(第二个参数names)并创建一个带标签的数据集。不要让这些事情阻止你理解幕后发生的事情。我们可以获取它的源代码,你会看到它很小,就像 5 行。

我不喜欢把这些东西写得很详细,而是把它们隐藏在一些小函数后面,这样你就可以重复使用它们。但基本上,它将遍历每个目录,遍历该目录中的每个文件,然后将其放入一个文本数组中,找出它在哪个文件夹中,并将其放入一个标签数组中。这就是我们最终得到的东西,我们有一个评论数组和一个标签数组。

这就是我们的数据。所以我们的工作将是接受一部电影评论并预测标签。我们将要做的是,我们将丢弃关于语言的所有有趣的东西,即单词的顺序。这通常不是一个好主意,但在这种情况下,它将不会太糟糕。所以让我告诉你我所说的丢弃单词顺序是什么意思。通常,单词的顺序非常重要。如果你在某个单词前面有一个“not”,那么这个“not”就指的是那个东西。但在这种情况下,我们试图预测某物是积极的还是消极的。如果你看到“荒谬”或“神秘”这样的词经常出现,也许这是一个迹象表明这不是很好。所以我们的想法是将其转换为一个称为术语文档矩阵的东西,对于每个文档(即每个评论),我们只是创建一个包含其中的单词列表,而不是它们的顺序。

术语文档矩阵示例:

朴素贝叶斯.xlsx

这里有四个我编造的电影评论。所以我将把这些转换成一个术语文档矩阵。我需要做的第一件事是创建一个称为词汇表的东西。词汇表是出现的所有唯一单词的列表。这是我的词汇表:this, movie, is, good, the bad。这就是所有的单词。现在我将把我的每个电影评论转换成一个向量,显示哪些单词出现以及它们出现的频率。在这种情况下,我的单词没有出现两次。所以这被称为术语文档矩阵:

这种表示法,我们称之为词袋表示法。所以这里是评论的一个词袋表示。

它不再包含文本的顺序。它只是一袋词(即其中包含哪些词)。它包含“bad”,“is”,“movie”,“this”。所以我们要做的第一件事是将其转换为一种词袋表示。这对于线性模型来说很方便的原因是,这是一个我们可以进行数学运算的漂亮矩阵。具体来说,我们可以进行逻辑回归,这就是我们要做的。我们将达到一个进行逻辑回归的点。不过,在那之前,我们将做另一件事,那就是朴素贝叶斯。sklearn 有一个可以为我们创建术语文档矩阵的东西,叫做CountVectorizer,所以我们将使用它。

标记化 [1:09:01]

现在在自然语言处理中,你必须将文本转换为单词列表,这就是所谓的标记化。这实际上并不是微不足道的,因为如果这实际上是This movie is good.This “movie” is good.,你如何处理标点符号呢?更有趣的是,如果这是This "movie" isn’t good.,你如何将一段文本转换为标记列表呢?一个好的标记器会将这个转换为:

之前:This "movie" isn’t good.

之后:This " movie " is n’t good .

所以你可以看到,在这个版本中,如果我现在按空格分割这个文本,每个标记要么是一个单独的标点符号,要么是这个后缀n't,被视为一个单词。这就是我们可能想要对这段文本进行标记化的方式,因为你不希望good.成为一个对象。没有good."movie"是一个对象的概念。所以标记化是我们交给标记器的事情。Fast AI 中有一个我们可以使用的标记器,这就是我们如何使用标记器创建我们的术语文档矩阵:

veczr = CountVectorizer(tokenizer=tokenize)

sklearn 有一个相当标准的 API,这很好。我相信你以前见过几次。一旦我们建立了某种“模型”,我们可以把CountVectorizer看作是一种模型,这只是定义它将要做什么。我们可以调用fit_transform来执行这个操作。

trn_term_doc = veczr.fit_transform(trn)
val_term_doc = veczr.transform(val)

在这种情况下,fit_transform将创建词汇表,并基于训练集创建术语文档矩阵。transform有点不同。它表示使用先前拟合的模型,这在这种情况下意味着使用先前创建的词汇表。我们不希望验证集和训练集在矩阵中有不同顺序的单词。因为那样它们会有不同的含义。所以这里说的是使用相同的词汇表为验证集创建一个词袋。

问题:如果验证集中有不同于训练集的单词集合怎么办[1:11:40]?这是一个很好的问题。通常,大多数这些词汇创建方法会为未知单词设定一个特殊标记。有时你也可以说,如果一个单词出现少于三次,就称之为未知。但是,如果你看到了以前没有见过的东西,就称之为未知。所以(即“未知”)只会成为词袋中的一列。

当我们创建这个术语文档矩阵时,训练集有 25,000 行,因为有 25,000 条电影评论,有 75,132 列,这是唯一单词的数量。

trn_term_doc
'''
<25000x75132 sparse matrix of type '<class 'numpy.int64'>'
     with 3749745 stored elements in Compressed Sparse Row format>
'''

现在,大多数文档并不包含这 75,132 个单词中的大部分。所以我们不想把它实际存储为内存中的普通数组。因为这样会非常浪费。所以,我们将其存储为稀疏矩阵。稀疏矩阵的作用是将其存储为一种只指示非零值位置的东西。所以它会说,文档编号 1,单词编号 4 出现了 4 次。文档编号 1,术语编号 123 出现了一次,依此类推。

(1, 4) → 4
(1, 123) → 1

这基本上就是它的存储方式。实际上有许多不同的存储方式,如果你学习 Rachel 的计算线性代数课程,你将了解不同类型的存储方式以及为什么选择它们,如何转换等等。但它们都类似于这样,你不需要太担心细节。重要的是它是高效的。

所以我们可以拿到第一条评论,这给了我们一个 75,000 个长稀疏的一行长矩阵,其中有 93 个存储元素。换句话说,这些单词中有 93 个实际上在第一个文档中使用。

trn_term_doc[0]
'''
<1x75132 sparse matrix of type '<class 'numpy.int64'>'
	with 93 stored elements in Compressed Sparse Row format>
'''

我们可以通过说veczr.get_feature_names来查看词汇表,这给我们提供了词汇表。这里是一些特征名称的元素的示例:

vocab = veczr.get_feature_names(); vocab[5000:5005]
'''
['aussie', 'aussies', 'austen', 'austeniana', 'austens']
'''

我并没有故意选择那个有澳大利亚的,但那是重要的单词,显然😄我这里没有使用分词器。我只是按空格分割,所以这与矢量化器所做的不完全相同。但为了简化事情,让我们拿到所有小写单词的集合。通过将其设置为集合,我们使它们成为唯一的。所以这大致是可能出现的单词列表。

w0 = set([o.lower() for o in trn[0].split(' ')]); w0
'''
{'a',
 'absurd',
 'an',
 'and',
 'audience',
 'be',
 'better',
 'briefly.',
 'by',
 'can',
 ...
}
'''
len(w0)
'''
91
'''

这个长度是 91,与 93 相似,唯一的区别是我没有使用真正的分词器。所以基本上就是这样。创建了这个唯一的单词列表并将它们映射。我们可以通过调用veczr.vocabulary_来查找特定单词的 ID。所以这就像veczr.get_feature_names的反向映射,它将整数映射到单词,veczr.vocabulary_将单词映射到整数。

veczr.vocabulary_['absurd']
'''
1297
'''

所以我们看到“荒谬”在第一个文档中出现了两次,所以让我们检查一下:

trn_term_doc[0,1297]
'''
2
'''

这就是,这是 2。否则,不幸的是,澳大利亚人没有出现在与猪有不自然关系的电影中,所以这是零:

trn_term_doc[0,5000]
'''
0
'''

这就是我们的术语文档矩阵。

问题:它是否关心单词之间的关系,比如单词的顺序?不,我们抛弃了顺序。这就是为什么它是一个词袋。我并不是在声称这一定是一个好主意。我要说的是,在过去几十年里进行的绝大多数自然语言处理工作通常使用这种表示,因为我们并不真正知道更好的方法。如今,我们越来越多地使用递归神经网络,我们将在第 1 部分的最后一个深度学习课程中学习。但有时这种表示方法效果还不错,实际上在这种情况下也会效果不错。

事实上,当我在我的电子邮件公司 FastMail 时,我们做的很多垃圾邮件过滤都使用了下一个技术朴素贝叶斯,这是一种词袋方法。如果你收到很多包含“伟哥”一词的电子邮件,而且它们一直是垃圾邮件,你从来没有收到朋友谈论“伟哥”的电子邮件,那么很可能说“伟哥”的东西无论语言的细节如何,都可能来自垃圾邮件发送者。所以这就是使用术语文档矩阵进行分类的基本理论。

朴素贝叶斯

让我们来谈谈朴素贝叶斯。这里是基本思想。我们将从我们的术语文档矩阵开始。前两个是我们的正面评论语料库。接下来两个是我们的负面评论语料库。所以这里是我们所有评论的整个语料库:

我们倾向于更通用地称这些列为“特征”而不是“单词”。this是一个特征,movie是一个特征,等等。所以现在更像是机器学习语言。一列是一个特征。在朴素贝叶斯中我们称这些为f。所以我们基本上可以说,给定类别为 1 时你会看到单词this的概率(即积极评论)就是你在积极评论中看到this的频率的平均值。现在我们必须要小心一点,因为如果你在某个类别中从未看到某个单词,所以如果我从未收到过朋友发来的提到“伟哥”的电子邮件,这并不意味着朋友给我发送有关伟哥的电子邮件的概率是零。实际上并不是零。我希望明天不会收到 Terrence 发来的电子邮件,说“Jeremy,你可能需要这个关于伟哥的广告”,但这种情况可能发生。我相信这对我有利🤣所以我们说实际上到目前为止我们看到的并不是所有可能发生的一切的完整样本。这更像是迄今为止发生的事情的样本。所以让我们假设你接下来收到的电子邮件实际上提到了伟哥和其他所有可能的单词。所以基本上我们要添加一行 1。

就像包含每个可能单词的电子邮件一样。这样,没有什么是无限不可能的。所以我取我积极语料库中所有this出现的平均次数再加上 1:

所以这就像是在文档中feature = 'this'出现的概率,假设class = 1(即thisp(f|1))。

毫不奇怪,这里是相同的概率,即给定class = 0时出现this的概率:

相同的计算,只是对于零行。显然这些是相同的,因为this在积极评论中出现一次,在消极评论中也出现一次。

我们可以为每个特征的每个类别做同样的计算[1:20:40]

所以我们现在的技巧基本上是使用贝叶斯规则来填充这个。所以我们想要的是在给定这个特定文档的情况下(所以有人给我发送了这封特定的电子邮件,或者我有这个特定的 IMDb 评论),其类别等于积极的概率是多少。所以对于这个特定的电影评论,它的类别是积极的概率是多少。所以我们可以说,嗯,这等于我们得到这个特定电影评论的概率,假设它的类别是积极的,乘以任何电影评论的类别是积极的概率除以得到这个特定电影评论的概率。

这就是贝叶斯规则。所以我们可以计算所有这些东西,但实际上我们真正想知道的是这是类别 0 还是类别 1 更有可能。如果我们实际上计算了类别 1 的概率并除以类别 0 的概率会怎样呢?

好的,如果这个数字大于 1,那么更有可能是类别 1,如果小于 1,更有可能是类别 0。所以在这种情况下,我们可以将整个事情除以类别 0 的相同版本,这等同于乘以倒数。所以好处是现在p(d)被取消了,下面是给定类别 0 时得到数据的概率,这里是得到类别 0 的概率。

基本上这意味着我们想要计算的是在类别为 1 的情况下得到这个特定文档的概率乘以类别为 1 的概率除以在类别为 0 的情况下得到这个特定文档的概率乘以类别为 0 的概率:

所以类别为 1 的概率就等于标签的平均值[1:23:20]。类别为 0 的概率是 1 减去那个值。所以有这两个数字:

我有相同数量的两者,所以都是 0.5。

如果这个文档的类别是 1,获得这个文档的概率是多少?学生:看看所有类别等于 1 的文档,1 除以那个会给你……[1:24:02] Jeremy: 所以记住这将是针对特定文档的。例如,会说这个评论是积极的概率是多少。所以你走在正确的轨道上,但我们要做的是看看它包含的单词,然后将类别等于 1 的概率相乘在一起。所以类别 1 的评论包含this的概率是 2/3,包含movie的概率是 1,is是 1,good是 1。所以它包含所有这些的概率是所有这些相乘在一起。有点。Tyler,为什么不是真的?所以很高兴你看起来震惊和怀疑 😄

Tyler: 选择不是独立的吗?

Jeremy: 谢谢。所以没有人能说 Tyler 天真,因为这是朴素贝叶斯的原因,因为这是如果你以朴素的方式使用贝叶斯定理会发生的事情。而 Tyler 并不天真。完全不是。所以朴素贝叶斯说让我们假设如果你有“这部电影太愚蠢了,我讨厌它”但讨厌的概率独立于愚蠢的概率独立于愚蠢的概率,这显然是不正确的。所以朴素贝叶斯实际上并不是很好,但我教给你们是因为它将成为我们即将学习的某些东西的便利工具。背景: 而且它通常效果还不错。Jeremy: 还好。我的意思是我永远不会选择它。我不认为它比任何其他同样快速和同样简单的技术更好。但这是你可以做的事情,肯定会成为有用的基础。

所以现在这是计算我们得到这个特定文档的概率,假设它是一个积极的评论[1:26:08]:

这是给定它是负面的概率

这里是比率。比率大于 1,所以我们将说我们认为这可能是一个积极的评论。

这就是 Excel 版本。所以你可以看出我让 Yannet 来处理这个,因为里面有 LaTeX。我们有实际的数学。所以这里是相同的东西;每个特征f的对数计数比。

所以这是用 Python 写出来的。我们的自变量是我们的术语文档矩阵,我们的因变量只是y的标签。所以使用 numpy,这个x[y==1]将抓取因变量为 1 的行。然后我们可以对行求和,以获得该特征在所有文档中的总词数,再加 1 — Terrence 今天肯定会给我发关于伟哥的东西,我能感觉到。就是这样。然后对负面评论做同样的事情。然后当然最好取对数,因为如果我们取对数,那么我们可以将事物相加而不是相乘。一旦你将足够多的这些东西相乘在一起,它将接近零,你可能会用完浮点数。所以我们取这些比率的对数。然后,正如我所说的,我们将将其相乘,或者用对数,将其加到整个类别概率的比率上。

def pr(y_i):
    p = x[y==y_i].sum(0)
    return (p+1) / ((y==y_i).sum()+1)
x=trn_term_doc
y=trn_yp = x[y==1].sum(0)+1 
q = x[y==0].sum(0)+1
r = np.log((p/p.sum())/(q/q.sum()))
b = np.log(len(p)/len(q))

为了对每个文档说,将贝叶斯概率乘以计数,我们可以使用矩阵乘法。然后添加类别比率的对数,你可以使用+ b。所以我们最终得到的东西看起来很像逻辑回归。但我们并没有学到任何东西。不是从 SGD 的角度来看。我们只是使用这个理论模型进行计算。正如我所说,我们可以将其与零进行比较,看看它是更大还是更小 — 不再是 1,因为我们现在处于对数空间。然后我们可以将其与均值进行比较。这样的准确率约为 81%。所以朴素贝叶斯并不是没有用的。它给了我们一些东西。

pre_preds = val_term_doc @ r.T + b
preds = pre_preds.T>0
(preds==val_y).mean()
'''
0.80691999999999997
'''

事实证明,这个版本实际上是在看 a 出现的频率,比如“荒谬”出现了两次,至少对于这个问题来说,通常无论“荒谬”出现了两次还是一次都无关紧要[1:29:03]。重要的是它出现了。所以人们倾向于尝试的是取术语矩阵文档并使用.sign(),它会将任何正数替换为1,将任何负数替换为-1(显然我们没有负计数)。这样就实现了二值化。它表示我不在乎你看到“荒谬”两次,我只在乎你看到它。所以如果我们对二值化版本做完全相同的事情,那么结果会更好。

pre_preds = val_term_doc.sign() @ r.T + b
preds = pre_preds.T>0
(preds==val_y).mean()
'''
0.82623999999999997
'''

逻辑回归[1:30:01]

现在这就是理论和实践之间的区别。理论上,朴素贝叶斯听起来还可以,但是它是朴素的,不像泰勒,它是朴素的。所以泰勒可能会说,与其假设我应该使用这些系数r,为什么不让我们学习它们呢?所以让我们学习它们。我们完全可以学习它们。

那么让我们创建一个逻辑回归,并拟合一些系数。这实际上会给我们提供与之前完全相同的功能形式,但现在我们不再使用理论上的r和理论上的b,而是根据逻辑回归计算这两个值。这样更好。

m = LogisticRegression(C=1e8, dual=True)
m.fit(x, y)
preds = m.predict(val_term_doc)
(preds==val_y).mean()
'''
0.85504000000000002
'''

所以这有点像是,为什么要基于某种理论模型进行某些操作呢?因为理论模型几乎永远不会像数据驱动模型那样准确。因为理论模型,除非你在处理某种物理问题或者某种你认为这实际上是世界如何运作的东西,否则没有……我不知道,我们是在真空中工作,有确切的重力等等。但是在现实世界中,事情是这样的——更好的方法是学习你的系数并计算它们。

Yannet:这个dual=True是什么[1:31:30]?我希望你会忽略,不会注意到,但你看到了。基本上,在这种情况下,我们的术语文档矩阵比高度更宽。逻辑回归有一个几乎在数学上等价的重新表述,当它比高度更宽时,速度会更快。简短的答案是,每当它比高度更宽时,加上dual=True,它会运行得更快。这只需要 2 秒。如果你不在这里加上它,那么需要几分钟。因此,在数学中,有一种问题的双重版本的概念,这些版本在某些情况下可能更适合。

这是二值化版本[1:32:20]。差不多一样。所以你可以看到我用术语文档矩阵的符号进行了拟合,并用val_term_doc.sign()进行了预测。

m = LogisticRegression(C=1e8, dual=True)
m.fit(trn_term_doc.sign(), y)
preds = m.predict(val_term_doc.sign())
(preds==val_y).mean()
'''
0.85487999999999997
'''

现在问题是,对于我们词汇表中大约有 75,000 个术语的每个术语都会有一个系数,考虑到我们只有 25,000 条评论,这似乎是很多系数[1:32:38]。所以也许我们应该尝试对此进行正则化。

所以我们可以使用内置在 sklearn 的 LogisticRegression 类中的正则化,它使用的参数是C。这有点奇怪,较小的参数表示更多的正则化。这就是为什么我使用1e8基本上关闭了正则化。

所以如果我打开正则化,将其设置为 0.1,那么现在是 88%:

m = LogisticRegression(C=0.1, dual=True)
m.fit(x, y)
preds = m.predict(val_term_doc)
(preds==val_y).mean()
'''
0.88275999999999999
'''

这是有道理的。你会认为对于 25,000 个文档的 75,000 个参数,很可能会过拟合。事实上,它确实过拟合了。因此,这是添加 L2 正则化以避免过拟合。

我之前提到过,除了 L2(查看权重的平方)之外,还有 L1(仅查看权重的绝对值)[1:33:37]。

我之前在措辞上有点粗心,我说 L2 试图将事物变为零。这在某种程度上是正确的,但如果你有两个高度相关的事物,那么 L2 正则化会将它们一起降低。它不会使其中一个变为零,另一个变为非零。因此,L1 正则化实际上具有这样的特性,它会尽可能使尽可能多的事物变为零,而 L2 正则化具有这样的特性,它倾向于使一切变得更小。实际上,在任何现代机器学习中,我们并不关心这种差异,因为我们很少直接尝试解释系数。我们尝试通过我们学到的技术来审查我们的模型。我们关心 L1 与 L2 的原因仅仅是哪一个在验证集上的错误更小。你可以尝试两种方法。使用 sklearn 的 LogisticRegression,L2 实际上会更快,因为你不能使用dual=True,除非你使用 L2,而 L2 是默认的。所以我并没有太担心这种差异。

所以你可以看到,如果我们使用正则化和二值化,我们实际上做得相当不错:

m = LogisticRegression(C=0.1, dual=True)
m.fit(trn_term_doc.sign(), y)
preds = m.predict(val_term_doc.sign())
(preds==val_y).mean()
'''
0.88404000000000005
'''

问题:在我们学习关于类似于组合 L1 和 L2 的 Elastic-net 之前。我们可以这样做吗?是的,你可以这样做,但需要更深层次的模型。我从来没有见过有人发现这有用。

最后我要提到的是,当你做 CountVectorizer 时,你也可以要求 n-gram。默认情况下,我们得到的是单字,也就是单个单词。但是如果我们说ngram_range=(1,3),那也会给我们二元组和三元组。

veczr =  CountVectorizer(
    ngram_range=(1,3), 
    tokenizer=tokenize,
    max_features=800000
)
trn_term_doc = veczr.fit_transform(trn)
val_term_doc = veczr.transform(val)
trn_term_doc.shape
'''
(25000, 800000)
'''
vocab = veczr.get_feature_names()
vocab[200000:200005]
'''
['by vast', 'by vengeance', 'by vengeance .', 'by vera', 'by vera miles']
'''

也就是说,如果我现在说好的,让我们继续使用 CountVectorizer,并获取特征名称,现在我的词汇表包括二元组:'by vast''by vengeance'和三元组:'by vengeance .''by vera miles'。所以现在做的事情与之前相同,但在分词之后,它不仅仅是抓取每个单词并说这是你的词汇表的一部分,而是抓取相邻的每两个单词和每三个单词。这实际上对利用词袋方法非常有帮助,因为我们现在可以看到not goodnot badnot terrible之间的区别。甚至像"good"这样的词可能是讽刺的。因此,实际上使用三元组特征将使朴素贝叶斯和逻辑回归变得更好。这确实让我们走得更远,使它们变得更有用。

问题:我有一个关于分词器的问题。你说max_features,那么这些二元组和三元组是如何选择的?由于我使用的是线性模型,我不想创建太多特征。即使没有max_features,它实际上也可以正常工作。我想我有大约 7000 万个系数。它仍然有效。但没有必要有 7000 万个系数。所以如果你说max_features=800,000,CountVectorizer 将按照所有内容出现的频率对词汇表进行排序,无论是单字、二元组还是三元组,然后在前 800,000 个最常见的 n 元组之后截断。N-gram 只是单字、二元组和三元组的通用词。

所以这就是为什么train_term_doc.shape现在是 25,000 乘以 800,000。如果你不确定最大值应该是多少,我只是选择了一个非常大的数字,不太担心,似乎也没问题。这并不是非常敏感的。

好了,我们时间到了,明天我们将看到什么...顺便说一句,我们可以用我们的 PyTorch 版本替换这个 LogisticRegression:

明天,我们实际上会在 Fast AI 库中看到一个可以做到这一点的东西,但明天我们还将看到如何将逻辑回归和朴素贝叶斯结合在一起,以获得比任何一个都更好的结果。然后我们将学习如何从那里开始创建一个更深层的神经网络,以获得几乎是结构化学习的最新结果。好的,到时候见。

机器学习 1:第 11 课

原文:medium.com/@hiromi_suenaga/machine-learning-1-lesson-11-7564c3c18bbb

译者:飞龙

协议:CC BY-NC-SA 4.0

来自机器学习课程的个人笔记。随着我继续复习课程以“真正”理解它,这些笔记将继续更新和改进。非常感谢 JeremyRachel 给了我这个学习的机会。

使用 SGD 优化多层函数的回顾[0:00]

这个想法是我们有一些数据(x),然后我们对这些数据做一些操作,例如,我们用一个权重矩阵乘以它(f(x))。然后我们对这个结果做一些操作,例如,我们通过 softmax 或 sigmoid 函数处理它(g(f(x)))。然后我们对这个结果做一些操作,比如计算交叉熵损失或均方根误差损失(h(g(f(x))))。这将给我们一些标量。这里没有隐藏层。这有一个线性层,一个非线性激活函数是 softmax,一个损失函数是均方根误差或交叉熵。然后我们有我们的输入数据。

例如[1:16],如果非线性激活函数是 sigmoid 或 softmax,损失函数是交叉熵,那就是逻辑回归。那么我们如何计算对权重的导数?

为了做到这一点,基本上我们使用链式法则:

因此,为了对权重求导,我们只需使用那个确切的公式计算对 w 的导数[3:29]。然后如果我们在这里进一步,有另一个带有权重 w2 的线性层,现在计算对所有参数的导数没有区别。我们仍然可以使用完全相同的链式法则。

所以不要把多层网络想象成在不同时间发生的事情。它只是函数的组合。所以我们只需使用链式法则一次计算所有导数。它们只是在函数的不同部分出现的一组参数,但微积分并没有不同。所以计算对 w1 和 w2 的导数,你现在可以称之为 w,说 w1 就是所有这些权重。

那么你将会得到一个参数列表[5:26]。这里是 w1,可能是某种高阶张量。如果是卷积层,它将是一个三阶张量,但我们可以展开它。我们将把它变成一个参数列表。这里是 w2。这只是另一个参数列表。这是我们的损失,是一个单一的数字。因此,我们的导数就是同样长度的向量。改变 w 的值会对损失产生多大影响?你可以把它想象成一个函数,比如y = ax1 + bx2 + c,然后问对 a、b 和 c 的导数是多少?你会得到三个数字:对 a、b 和 c 的导数。就是这样。如果对那个权重求导,那个权重,...

为了到达这一点,在链式法则中,我们必须计算像雅可比这样的导数,当你进行矩阵乘法时,你现在得到的是一个权重矩阵和输入向量,这些是来自前一层的激活,还有一些新的输出激活。所以现在你必须说对于这个特定的权重,改变这个特定的权重如何改变这个特定的输出?改变这个特定的权重如何改变这个特定的输出?等等。所以你最终会得到这些更高维度的张量,显示每个权重如何影响每个输出。然后当你到达损失函数时,损失函数将有一个均值或和,所以它们最终会被加起来。

尝试手动计算或逐步考虑这一点让我有点疯狂,因为你倾向于像…你只需要记住,对于每个权重对于每个输出,你都必须有一个单独的梯度。

一个很好的方法是学习使用 PyTorch 的.grad属性和.backward方法,并手动查阅 PyTorch 教程。这样你就可以开始设置一些具有向量输入和向量输出的计算,然后输入.backward,然后输入grad并查看它。然后对只有 2 或 3 个项目的输入和输出向量进行一些非常小的操作,比如加 2 或其他操作,看看形状是什么,确保它是有意义的。因为向量矩阵微积分在严格意义上并没有为你在高中学到的任何概念引入新的概念。但是对这些形状如何移动有了一定的感觉需要大量的练习。好消息是,你几乎永远不必担心这个。

NLP 的朴素贝叶斯和逻辑回归回顾[9:53]

笔记本 / Excel

我们正在讨论使用这种逻辑回归进行 NLP。在达到这一点之前,我们正在讨论使用朴素贝叶斯进行 NLP。基本思想是我们可以取一个文档(例如电影评论),并将其转换为一个词袋表示,其中包含每个单词出现的次数。我们称单词的唯一列表为词汇表。我们使用 sklearn 的 CountVectorizer 自动生成词汇表,他们称之为“特征”,并创建词袋表示,所有这些袋表示的整体称为术语文档矩阵。

我们有点意识到,我们可以通过简单地平均积极评论中单词“this”出现的次数来计算积极评论包含单词“this”的概率,我们可以对消极评论做同样的事情,然后我们可以取它们的比率,得到一个结果,如果大于一,则表示该单词在积极评论中出现得更频繁,如果小于一,则表示该单词在消极评论中出现得更频繁。

然后我们意识到,使用贝叶斯规则并取对数,我们基本上可以得到一个结果,我们可以将这些(下面突出显示的)的对数加起来,再加上类别 1 与类别 0 的概率比的对数,最终得到一个可以与零进行比较的结果[11:32]。如果结果大于零,我们可以预测文档是积极的,如果结果小于零,我们可以预测文档是消极的。这就是我们的贝叶斯规则。

我们从数学的第一原理开始做了这个,我认为我们都同意“朴素”在朴素贝叶斯中是一个很好的描述,因为它假设了独立性,而这显然是不正确的。但这是一个有趣的起点,当我们实际上到达这一点时,我们计算了概率的比率并取了对数,现在不是将它们相乘,当然,我们必须将它们相加。当我们实际写下这个时,我们意识到哦,这只是一个标准的权重矩阵乘积加上一个偏差:

然后我们意识到,如果这不是很好的准确率(80%),为什么不通过说,嘿,我们知道其他计算一堆系数和一堆偏差的方法,即在逻辑回归中学习它们来改进呢?换句话说,这是我们用于逻辑回归的公式,那么为什么我们不只是创建一个逻辑回归并拟合它呢?它会给我们同样的结果,但与基于独立性假设和贝叶斯规则的理论上正确的系数和偏差不同,它们将是实际上在这些数据中最好的系数和偏差。这就是我们的结论。

这里的关键见解是,几乎所有的机器学习最终都变成了一棵树或一堆矩阵乘积和非线性[13:54]。一切似乎最终都归结为相同的事情,包括贝叶斯规则。然后事实证明,无论该函数中的参数是什么,它们都比基于理论计算更好地学习。事实上,当我们实际尝试学习这些系数时,我们得到了 85%的准确率。

然后我们注意到,与其采用整个术语文档矩阵,我们可以只取单词存在或不存在的一和零。有时这样做同样有效,但后来我们实际尝试了另一种方法,即添加正则化。通过正则化,二值化方法结果略好一些。

然后正则化是我们取损失函数,再次,让我们从 RMSE 开始,然后我们将讨论交叉熵。损失函数是我们的预测减去我们的实际值,将其相加,取平均再加上一个惩罚。

具体来说,这是 L2 惩罚。如果这是 w 的绝对值,那就是 L1 惩罚。我们还注意到,我们实际上并不关心损失函数本身,我们只关心它的导数,这实际上是更新权重的东西,因此因为这是一个总和,我们可以分别对每个部分求导,所以惩罚的导数只是 2aw。因此,我们了解到,尽管这些在数学上是等价的,但它们有不同的名称。这个版本(2aw)被称为权重衰减,这个术语在神经网络文献中使用。

交叉熵[16:34]

Excel

另一方面,交叉熵只是另一个损失函数,就像均方根误差一样,但它专门设计用于分类。这是一个二元交叉熵的例子。假设这是我们的“是猫还是狗?”所以说isCat是 1 还是 0。而Preds是我们的预测,这是我们神经网络的最终层的输出,一个逻辑回归等等。

然后我们所做的就是说好吧,让我们取实际值乘以预测的对数,然后加上 1 减去实际值乘以 1 减去预测的对数,然后取整个东西的负值。

我建议你们尝试写出这个 if 语句版本,希望你们现在已经做到了,否则我将为你们揭示。所以这是:

我们如何将这写成一个 if 语句?

if y == 1: 
    return -log(ŷ)
else: 
    return -log(1-ŷ)

所以关键的洞察是 y 有两种可能性:1 或 0。所以很多时候数学会隐藏关键的洞察,我认为这里发生了,直到你真正思考它可以取什么值。所以这就是它所说的。要么给我:-log(ŷ),要么给我:-log(1-ŷ)

好的,那么多类别版本就是同样的事情,但你说的不仅仅是 y == 1,而是 y == 0, 1, 2, 3, 4, 5 . . . ,例如 [19:26]。所以这个损失函数有一个特别简单的导数,另外一个你可以在家里尝试的东西是想一想在它之前加上一个 sigmoid 或 softmax 之后导数是什么样子。结果会是非常好的导数。

人们使用均方根误差用于回归和交叉熵用于分类的原因有很多,但大部分都可以追溯到最佳线性无偏估计的统计概念,基于可能性函数的结果表明这些函数具有一些良好的统计性质。然而,实际上,尤其是均方根误差的性质可能更多是理论上的而不是实际的,实际上,现在使用绝对偏差而不是平方偏差的和通常效果更好。所以在实践中,机器学习中的一切,我通常都会尝试两种。对于特定的数据集,我会尝试两种损失函数,看哪一个效果更好。当然,如果是 Kaggle 竞赛的话,那么你会被告知 Kaggle 将如何评判,你应该使用与 Kaggle 评估指标相同的损失函数。

所以这真的是关键的洞察 [21:16]。让我们不要使用理论,而是从数据中学习。我们希望我们会得到更好的结果。特别是在正则化方面,我们确实得到了。然后我认为这里的关键正则化洞察是让我们不要试图减少模型中的参数数量,而是使用大量的参数,然后使用正则化来找出哪些实际上是有用的。

更多的 n-grams 特征 [21:41]

笔记本

然后我们进一步说,鉴于我们可以通过正则化做到这一点,让我们通过添加二元组和三元组来创建更多。例如 by vastby vengeance 这样的二元组,以及 by vengeance .by vera miles 这样的三元组。为了让事情运行得更快一些,我们将其限制为 800,000 个特征,但即使使用完整的 70 百万个特征,它的效果也一样好,而且速度并没有慢多少。

veczr =  CountVectorizer(
    ngram_range=(1,3), 
    tokenizer=tokenize, 
    max_features=800000
)
trn_term_doc = veczr.fit_transform(trn)
val_term_doc = veczr.transform(val)
trn_term_doc.shape
'''
(25000, 800000)
'''
vocab = veczr.get_feature_names()
vocab[200000:200005]
'''
['by vast', 'by vengeance', 'by vengeance .', 'by vera', 'by vera miles']
'''

所以我们使用了完整的 n-grams 集合为训练集和验证集创建了一个术语文档矩阵。现在我们可以继续说我们的标签是训练集标签如前所述,我们的自变量是二值化的术语文档矩阵如前所述:

y=trn_y
x=trn_term_doc.sign()
val_x = val_term_doc.sign()
p = x[y==1].sum(0)+1
q = x[y==0].sum(0)+1
r = np.log((p/p.sum())/(q/q.sum()))
b = np.log(len(p)/len(q))

然后让我们对其进行逻辑回归拟合,并进行一些预测,我们得到了 90% 的准确率:

m = LogisticRegression(C=0.1, dual=True)
m.fit(x, y);

preds = m.predict(val_x)
(preds.T==val_y).mean()
'''
0.90500000000000003
'''

所以看起来很不错。

回到朴素贝叶斯 [22:54]

让我们回到我们的朴素贝叶斯。在我们的朴素贝叶斯中,我们有这个术语文档矩阵,然后对于每个特征,我们正在计算如果它是类别 1 出现的概率,如果它是类别 0 出现的概率,以及这两者的比率。

在我们实际基于的论文中,他们将 p(f|1) 称为 p,将 p(f|0) 称为 q,将比率称为 r

然后我们说让我们不要把这些比率作为矩阵乘法中的系数。而是,尝试学习一些系数。也许开始用一些随机数,然后尝试使用随机梯度下降找到稍微更好的系数。

所以你会注意到这里一些重要的特征。r向量是一个秩为 1 的向量,其长度等于特征的数量。当然,我们的逻辑回归系数矩阵也是秩为 1 且长度等于特征数量的。我们说它们是计算相同类型的东西的两种方式:一种基于理论,一种基于数据。所以这里是r中的一些数字:

r.shape, r
'''
((1, 800000),
matrix([[-0.05468, -0.161  , -0.24784, ...,  1.09861, -0.69315, -0.69315]]))
'''

记住它使用对数,所以这些小于零的数字代表更有可能是负数的东西,而大于零的数字可能是正数。所以这里是 e 的幂次方。所以这些是我们可以与 1 而不是 0 进行比较的数字:

np.exp(r)
'''
matrix([[ 0.94678,  0.85129,  0.78049, ...,  3\.  ,  0.5 ,  0.5  ]])\
'''

我要做一些希望看起来很奇怪的事情。首先,我会说我们要做什么,然后我会尝试描述为什么这很奇怪,然后我们会讨论为什么它可能并不像我们最初想的那么奇怪。所以这就是我们要做的事情。我们将取我们的术语文档矩阵,然后将其乘以r。这意味着,我可以在 Excel 中做到这一点,我们将说让我们抓取我们的术语文档矩阵中的所有内容,并将其乘以向量r中的等值。所以这就像是一个广播的逐元素乘法,而不是矩阵乘法。

所以这是术语文档矩阵乘以r的值,换句话说,在术语文档矩阵中出现零的地方,在乘以版本中也出现零。而在术语文档矩阵中每次出现一个的地方,等效的r值出现在底部。所以我们并没有真正改变太多。我们只是将一个变成了其他东西,即来自该特征的r。所以我们现在要做的是,我们将使用这些独立变量,而不是在我们的逻辑回归中。

所以在这里。x_nb(x 朴素贝叶斯版本)是x乘以r。现在让我们使用这些独立变量进行逻辑回归拟合。然后对验证集进行预测,结果我们得到了一个更好的数字:

x_nb = x.multiply(r)
m = LogisticRegression(dual=True, C=0.1)
m.fit(x_nb, y);

val_x_nb = val_x.multiply(r)
preds = m.predict(val_x_nb)
(preds.T==val_y).mean()
'''
0.91768000000000005
'''

让我解释为什么这可能会令人惊讶。这是我们的独立变量(下面突出显示),然后逻辑回归得出了一些系数集(假设这些是它恰好得出的系数)。

现在我们可以说,好吧,让我们不使用这组独立变量(x_nb),而是使用原始的二值化特征矩阵。然后将所有系数除以r中的值,数学上我们将得到完全相同的结果。

所以我们有了我们的独立变量的 x 朴素贝叶斯版本(x_nb),以及一组权重/系数(w1),它发现这是一个用于进行预测的好系数集。但是 x_nb 简单地等于x乘以(逐元素)r

换句话说,这(xnb·w1)等于x*r·w1。所以我们可以只改变权重为r·w1,得到相同的数字。这应该意味着我们对独立变量所做的更改不应该有任何影响,因为我们可以在不进行这种改变的情况下计算出完全相同的结果。所以问题就在这里。为什么会有所不同呢?为了回答这个问题,你需要考虑哪些数学上不同的事情。为什么它们不完全相同?提出一些假设,也许我们实际上得到了更好的答案的一些原因。要弄清楚这一点,首先我们需要弄清楚为什么会有不同的答案?这是微妙的。

讨论

它们受到正则化的影响是不同的。我们的损失等于基于预测和实际值的交叉熵损失加上我们的惩罚:

所以如果你的权重很大,那么惩罚(aw²)就会变大,而且会淹没掉交叉熵部分(x.e.(xw, y))。但那实际上是我们关心的部分。我们实际上希望它是一个很好的拟合。所以我们希望尽可能少地进行正则化。所以我们希望权重更小(我有点把“更少”和“更小”这两个词用得有点等同,这不太公平,我同意,但想法是接近零的权重实际上是不存在的)。

这就是问题所在。我们的r值,我不是一个贝叶斯怪胎,但我仍然要使用“先验”这个词。它们有点像一个先验 - 我们认为不同级别的重要性和这些不同特征的积极或消极可能是这样的。我们认为“坏”可能与负面更相关,而不是“好”。所以我们之前的隐含假设是我们没有先验,换句话说,当我们说平方权重(w²)时,我们是在说非零权重是我们不想要的。但实际上我想说的是,与朴素贝叶斯的期望不同是我不想做的事情。除非你有充分的理由相信其他情况,否则只有与朴素贝叶斯先验有所不同。

这实际上就是这样做的。我们最终说我们认为这个值可能是 3。所以如果你要把它变得更大或更小,那将会导致权重的变化,从而使平方项增加。所以如果可以的话,就让所有这些值保持与现在大致相似。这就是现在惩罚项正在做的事情。当我们的输入已经乘以r时,它在说惩罚那些与我们的朴素贝叶斯先验有所不同的事物。

问题:为什么只与 r 相乘,而不是像 r²这样,这次方差会高得多呢?因为我们的先验来自一个实际的理论模型。所以我说我不喜欢依赖理论,但如果我有一些理论,那么也许我们应该将其作为我们的起点,而不是假设一切都是相等的。所以我们的先验说,嘿,我们有这个叫做朴素贝叶斯的模型,朴素贝叶斯模型说,如果朴素贝叶斯的假设是正确的,那么r就是这个特定公式中的正确系数。这就是我们选择它的原因,因为我们的先验是基于那个理论的。

这是一个非常有趣的见解,我从未真正看到过。这个想法是我们可以使用这些传统的机器学习技术,通过将我们的理论期望纳入我们给模型的数据中,赋予它们这种贝叶斯感。当我们这样做时,这意味着我们就不必那么经常进行正则化了。这很好,因为我们经常进行正则化...让我们试试吧!

记住,在 sklearn 逻辑回归中,C是正则化惩罚的倒数。所以我们通过使其变小(1e-5)来增加大量的正则化。

这真的会影响我们的准确性,因为现在它非常努力地降低这些权重,损失函数被需要减少权重的需求所压倒。而现在使其具有预测性似乎完全不重要。所以通过开始并说不要推动权重降低,以至于最终忽略这些项,而是推动它们降低,以便尝试消除那些忽略了我们基于朴素贝叶斯公式期望的差异的权重。这最终给我们带来了一个非常好的结果

基线和二元组:简单、良好的情感和主题分类

论文

这种技术最初是在 2012 年提出的。Chris Manning 是斯坦福大学出色的自然语言处理研究人员,而 Sida Wang 我不认识,但我认为他很棒,因为他的论文很棒。他们基本上提出了这个想法。他们所做的是将其与其他方法在其他数据集上进行比较。其中一件事是他们尝试了 IMDB 数据集。这里是大二元组上的朴素贝叶斯 SVM:

正如你所看到的,这种方法胜过了他们研究的其他基于线性的方法,以及他们研究的一些受限玻尔兹曼机的神经网络方法。如今,有更好的方法来做这个,事实上在深度学习课程中,我们展示了我们在 Fast AI 刚刚开发的最新成果,可以达到 94%以上的准确率。但是特别是对于一种简单、快速、直观的线性技术来说,这还是相当不错的。你会注意到,当他们这样做时,他们只使用了二元组。我猜这是因为我看了他们的代码,发现它相当慢且难看。我找到了一种更优化的方法,正如你所看到的,所以我们能够使用三元组,因此我们得到了更好的结果,我们的准确率是 91.8%,而不是 91.2%,但除此之外,它是相同的。哦,他们还使用了支持向量机,在这种情况下几乎与逻辑回归相同,所以有一些细微的差异。所以我认为这是一个相当酷的结果。

我要提一下,在课堂上你看到的是我经过多周甚至多个月的研究得出的结果。所以我不希望你认为这些东西是显而易见的。完全不是。就像阅读这篇论文,论文中没有描述为什么他们使用这个模型,它与其他模型有何不同,为什么他们认为它有效。我花了一两周的时间才意识到它在数学上等同于普通的逻辑回归,然后又花了几周的时间才意识到区别实际上在于正则化。这有点像机器学习,我相信你从你参加的 Kaggle 竞赛中已经注意到了。就像你提出了一千个好主意,其中 999 个无论你有多么自信它们会很棒,最终都会变成垃圾。然后最终在四周后,其中一个终于奏效,给了你继续度过另外四周的痛苦和挫折的热情。这是正常的。而且我可以确定,我所认识的机器学习领域最优秀的从业者都有一个共同的特点,那就是他们非常顽强,也被称为固执和执着,这绝对是我似乎拥有的声誉,可能是公平的,还有另一点,他们都是非常擅长编码的。他们非常擅长将他们的想法转化为新的代码。对我来说,几个月前通过这个工作是一个非常有趣的经历,试图至少弄清楚为什么这个当时的最新成果存在。

更好的版本:NBSVM++ [43:31]

所以一旦我弄清楚了,我实际上能够在此基础上进行改进,并且我会向你展示我做了什么。这就是我非常幸运能够使用 PyTorch 的原因,因为我能够创建出我想要的定制化内容,并且通过使用 GPU 也非常快速。这就是 Fast AI 版本的 NBSVM。实际上,我的朋友 Stephen Merity 是一位在自然语言处理领域出色的研究人员,他将其命名为 NBSVM++,我觉得这很可爱,所以这就是,尽管没有 SVM,但是它是一个逻辑回归,但正如我所说,几乎完全相同。

所以首先让我向你展示代码。一旦我弄清楚这是我能想到的最好的线性词袋模型的方法,我将其嵌入到 Fast AI 中,这样你只需写几行代码就可以了。

sl=2000
# Here is how we get a model from a bag of words
md = TextClassifierData.from_bow(
    trn_term_doc, 
    trn_y, 
    val_term_doc,
    val_y, sl
)

所以代码基本上是,嘿,我想为文本分类创建一个数据类,我想从词袋(from_bow)中创建它。这是我的词袋(trn_term_doc),这是我们的标签(trn_y),这是验证集的相同内容,并且每个评论最多使用 2000 个独特的单词,这已经足够了。

然后从那个模型数据中,构建一个学习器,这是 Fast AI 对基于朴素贝叶斯点积的模型的泛化,然后拟合该模型。

learner = md.dotprod_nb_learner()
learner.fit(0.02, 1, wds=1e-6, cycle_len=1)
'''
[ 0\.       0.0251   0.12003  0.91552]
'''
learner.fit(0.02, 2, wds=1e-6, cycle_len=1)
'''
[ 0\.       0.02014  0.11387  0.92012]                         
[ 1\.       0.01275  0.11149  0.92124]
'''
learner.fit(0.02, 2, wds=1e-6, cycle_len=1)
'''
[ 0\.       0.01681  0.11089  0.92129]                           
[ 1\.       0.00949  0.10951  0.92223]
'''

经过 5 个时代,我的准确率已经达到了 92.2。所以现在已经远远超过了线性基准(在原始论文中)。所以让我给你展示一下那段代码。

所以代码非常简短。就是这样。这看起来也非常熟悉。这里有一些小调整,假装这个写着Embedding的东西实际上写着Linear。我马上会展示给你看 embedding。所以我们基本上有一个线性层,特征的数量作为行,记住,sklearn 特征意味着基本上是单词的数量。然后对于每个单词,我们将创建一个权重,这是有道理的——逻辑回归,每个单词有一个权重。然后我们将它乘以r值,所以每个单词,我们有一个r值每个类。所以我实际上做了这个,这样可以处理不仅仅是正面和负面,还可以找出是哪个作者创作了这个作品——例如可能有五六个作者。

基本上我们使用这些线性层来得到权重和r的值,然后我们取权重乘以r,然后相加。所以这只是一个简单的点积,就像我们为任何逻辑回归所做的那样,然后进行 softmax。我们为了获得更好的结果所做的非常小的调整是这个+self.w_adj

我添加的东西是,这是一个参数,但我几乎总是使用这个默认值 0.4。那么这是做什么的呢?这再次改变了先验。如果你考虑一下,即使我们将这个r乘以文档矩阵作为它们的自变量,你真的想从一个问题开始,好的,惩罚项仍然在将w推向零。

那么w为零意味着什么?如果我们的系数都是 0 会怎么样?

当我们将这个矩阵与这些系数相乘时,我们仍然得到零。所以权重为零最终会说“我对这个事情是正面还是负面没有意见。”另一方面,如果它们都是 1,那么基本上就是说我的意见是朴素贝叶斯系数是完全正确的。所以我说零几乎肯定不是正确的先验。我们不应该真的说如果没有系数,那就意味着忽略朴素贝叶斯系数。1 可能太高了,因为我们实际上认为朴素贝叶斯只是答案的一部分。所以我尝试了几个不同的数据集,基本上是说取这些权重并加上一些常数。所以零在这种情况下会变成 0.4。换句话说,正则化惩罚将权重推向这个值而不是零。我发现在许多数据集中,0.4 效果非常好且非常稳健。再次,基本思想是在使用简单模型从数据中学习的同时,尽可能地融入我们的先验知识。所以结果是,当你说让权重矩阵的零实际上意味着你应该使用大约一半的r值时,这比权重应该全部为零的先验效果更好。

问题:w是表示所需正则化量的点吗?w是权重。所以x = ((w+self.w_adj)*r/self.r_adj).sum(1)正在计算我们的激活。我们计算我们的激活等于权重乘以r,然后求和。所以这只是我们的正常线性函数。被惩罚的是我的权重矩阵。这就是受到惩罚的地方。所以通过说,嘿,你知道,不要只使用w —— 使用w+0.4。0.4(即self.w_adj)不受惩罚。它不是权重矩阵的一部分。因此,权重矩阵实际上免费获得了 0.4。

问题:通过这样做,即使经过正则化,每个特征都会获得一些形式的最小权重吗?不一定,因为它最终可能会为一个特征选择一个系数为-0.4,这将表示“你知道,即使朴素贝叶斯说对于这个特征r应该是什么,我认为你应该完全忽略它”。

休息期间有几个问题。第一个是关于这里正在发生的事情的总结:

这里有w加上权重调整乘以r

所以通常,我们所做的是说逻辑回归基本上是wx(我将忽略偏差)。然后我们将其更改为rx·w。然后我们说让我们先做x·w这部分。这里的这个东西,我实际上称之为 w,这可能很糟糕,实际上是w乘以x

所以,我没有r(x·w),我有w·x加上一个常数乘以r。所以这里的关键思想是正则化希望权重为零,因为它试图减少Σw²。所以我们所说的是,好吧,我们希望将权重推向零,因为这是我们的默认起点期望。所以我们希望处于这样一种情况,即如果权重为零,那么我们有一个对我们来说在理论上或直观上有意义的模型。这个模型(r(x·w)),如果权重为零,对我们来说没有直观意义。因为它在说,嘿,将所有东西乘以零会消除一切。我们实际上在说“不,我们实际上认为我们的r是有用的,我们实际上想保留它。”所以,让我们取(x·w)并加上 0.4。所以现在,如果正则化器将权重推向零,那么它将将总和的值推向 0.4。

因此,它将整个模型推向 0.4 倍r。换句话说,如果您将所有权重一起正则化到 0.4 倍r,那么我们的默认起点是说“是的,您知道,让我们使用一点r。这可能是一个好主意。”这就是这个想法。这个想法基本上是当权重为零时会发生什么。您希望那是有意义的,否则正则化权重朝着那个方向移动就不是一个好主意。

第二个问题是关于 n-grams。所以 n-gram 中的 N 可以是 uni,bi,tri,等等。1,2,3,等等个 grams。所以“This movie is good”有四个 unigrams:Thismovieisgood。它有三个 bigrams:This moviemovie isis good。它有两个 trigrams:This movie ismovie is good

问题:您介意回到w_adj0.4的内容吗?我在想这种调整会不会损害模型的可预测性,因为想象一下极端情况,如果不是 0.4,如果是 4,000,那么所有系数基本上会是...?确切地说。因此,我们的先验需要有意义。这就是为什么它被称为 DotProdNB,因此先验是我们认为朴素贝叶斯是一个好的先验的地方。因此,朴素贝叶斯认为r = p/q是一个好的先验,我们不仅认为这是一个好的先验,而且我们认为rx+b是一个好的模型。这就是朴素贝叶斯模型。换句话说,我们期望系数为 1 是一个好的系数,而不是 4,000。具体来说,我们认为零可能不是一个好的系数。但我们也认为也许朴素贝叶斯版本有点过于自信。所以也许 1 有点高。因此,我们相当确定,假设朴素贝叶斯模型是适当的,正确的数字在 0 和 1 之间。

问题继续:但我在想的是只要不是零,您就会将那些应该为零的系数推到非零的地方,并使高系数与零系数之间的差异变小。嗯,但是您看,它们本来就不应该是零。它们应该是r。请记住,这是在我们的前向函数中,所以这是我们正在计算梯度的一部分。所以基本上是说,好吧,您仍然可以将 self.w 设置为您喜欢的任何值。但是正则化器希望它为零。所以我们所说的是,好吧,如果您希望它为零,那么我将尝试使零给出一个合理的答案。

没有人说 0.4 对于每个数据集都是完美的。我尝试了一些不同的数据集,并发现在 0.3 和 0.6 之间有一些最佳值。但我从未发现一个比零更好的数据集,这并不奇怪。我也从未发现一个更好的数据集。因此,这个想法是一个合理的默认值,但这是另一个您可以玩耍的参数,我有点喜欢。这是另一件您可以使用网格搜索或其他方法来找出对您的数据集最佳的东西。实际上,关键在于在这个模型之前的每个模型,据我所知,都隐含地假设它应该为零,因为它们没有这个参数。顺便说一句,我实际上还有第二个参数(r_adj=10),它是我对 r 做的相同的事情,实际上是通过一个参数除以 r,我现在不会太担心,但这是另一个您可以用来调整正则化性质的参数。最终,我是一个实证主义者,而不是一个理论家。我认为这似乎是一个好主意。几乎所有我认为是一个好主意的事情最终都被证明是愚蠢的。这个特定的想法在这个数据集上给出了良好的结果,也在其他一些数据集上给出了良好的结果。

问题:我仍然对w + w_adj感到困惑。你提到我们执行w + w_adj是为了不让系数设为零,我们对先验赋予了一些重要性。但你也说过学习的效果可能是w被设为负值,这可能会使w + w_adj为零。所以如果我们允许学习过程确实将系数设为零,那为什么这与只有w不同呢?因为正则化。因为我们通过Σw²对其进行惩罚。换句话说,我们在说,你知道,如果忽略r是最好的选择,那将会花费你(Σw²)。你将不得不将w设为负数。所以只有在这显然是一个好主意的情况下才这样做。除非这显然是一个好主意,否则你应该将其保留在原处。这是唯一的原因。今天我们所做的所有事情基本上都是为了最大化我们从正则化中获得的优势,并且说正则化将我们推向某种默认假设,几乎所有的机器学习文献都假设默认假设是所有事物都是零。我在说的是,从理论上讲这是有道理的,而从经验上讲,事实证明你应该决定你的默认假设是什么,这将给你带来更好的结果。

问题继续:那么可以这样说,在某种程度上你是在前往将所有系数设为零的过程中设置了一个额外的障碍,如果确实值得的话,它将能够做到这一点吗?是的,确实如此。所以我会说,没有这个默认障碍,使系数非零是障碍。现在我要说的是,不,障碍是使系数不等于 0.4r

问题:所以这是 w²乘以某个常数的总和。如果常数是,比如说 0.1,那么权重可能不会趋向于零。那么我们可能就不需要权重衰减了?如果常数的值为零,那么就没有正则化。但如果这个值大于零,那么就会有一些惩罚。而且可以推测,我们将其设置为非零是因为我们过拟合了。所以我们想要一些惩罚。所以如果有一些惩罚,那么我的观点是我们应该惩罚那些与我们的先验不同的事物,而不是惩罚那些与零不同的事物。我们的先验是事物应该大致等于r

嵌入[1:05:17]

我想谈谈嵌入。我说假装它是线性的,实际上我们可以假装它是线性的。让我向你展示我们可以多么地假装它是线性的,就像nn.Linear,创建一个线性层。

这是我们的数据矩阵,这是我们的系数r如果我们正在进行r版本。所以如果我们将r放入列向量中,那么我们可以通过系数对数据矩阵进行矩阵乘法。

因此,这个自变量矩阵乘以这个系数矩阵的矩阵乘法将给我们一个答案。所以问题是,好吧,为什么 Jeremy 没有写nn.Linear?为什么 Jeremy 写了nn.Embedding?原因是,如果你回忆一下,我们实际上并不是这样存储的。因为这实际上是宽度为 800,000,高度为 25,000。所以我们实际上是这样存储的:

我们的存储方式是,这个词袋包含哪些单词索引。这是一种稀疏的存储方式。它只列出每个句子中的索引。鉴于此,我现在想要执行我刚刚向你展示的那种矩阵乘法,以创建相同的结果。但我想要从稀疏表示中执行。这基本上是一种独热编码:

这有点像一个虚拟矩阵版本。它有一个单词“this”吗?它有一个单词“movie”吗?等等。所以如果我们采用简单版本的有没有单词“this”(即 1, 0, 0, 0, 0, 0),然后我们将其乘以r,那么它只会返回第一个项目:

总的来说,一个独热编码向量乘以一个矩阵等同于查找该矩阵中的第 n 行。 所以这只是说找到第 0、第一个、第二个和第五个系数:

它们完全相同。 在这种情况下,每个特征只有一个系数,但实际上我这样做的方式是为每个类别的每个特征都有一个系数。 所以在这种情况下,类别是正面和负面。 所以我实际上有 r 正面 (p/q),r 负面 (q/r):

在二进制情况下,显然同时拥有两者是多余的。 但是如果是像这个文本的作者是谁? 是 Jeremy,Savannah 还是 Terrence? 现在我们有三个类别,我们想要三个 r 的值。 所以做这个稀疏版本的好处是,你可以查找第 0、第一个、第二个和第五个。

再次强调,从数学上讲,这与乘以一个独热编码矩阵是相同的。 但是,当输入稀疏时,效率显然要高得多。 因此,这个计算技巧在数学上与乘以一个独热编码矩阵是相同的,而不是概念上类似于。 这被称为嵌入。 我相信大多数人可能已经听说过嵌入,比如词嵌入:Word2Vec,GloVe 等。 人们喜欢把它们说成是这种令人惊叹的新复杂神经网络东西。 但事实并非如此。 嵌入意味着通过简单的数组查找来加快乘以一个独热编码矩阵的过程。 这就是为什么我说你可以把这个想象成说 self.w = nn.Linear(nf+1, 1)

因为它实际上做的是相同的事情。 它实际上是一个具有这些维度的矩阵。 这是一个线性层,但它期望我们要给它的输入实际上不是一个独热编码矩阵,而是一个整数列表 —— 每个项目的每个单词的索引。 所以你可以看到 Fast AI 中的 forward 函数自动获取(对于 DotProdNB leaner)特征索引(feature_idx):

所以它们自动来自稀疏矩阵。 Numpy 使得很容易只需抓取这些索引。 换句话说,我们在这里(feat_idx)有一个包含这个文档中的 800,000 个单词索引的列表。 所以这里(self.w(feat_idx))说的是查找我们的嵌入矩阵中的每一个,该矩阵有 800,000 行,并返回你找到的每一个东西。 从数学上讲,这与乘以一个独热编码矩阵是相同的。 这就是所有嵌入的含义。 这意味着我们现在可以处理构建任何类型的模型,比如任何类型的神经网络,其中我们的输入可能是非常高基数的分类变量。 然后我们只需将它们转换为介于零和级别数之间的数字代码,然后我们可以学习一个线性层,就好像我们已经对其进行了独热编码,而实际上并没有构建独热编码版本,也没有进行矩阵乘法。 相反,我们将只存储索引版本并简单地进行数组查找。 因此,回流的梯度基本上是在独热编码版本中,所有为零的东西都没有梯度,因此回流的梯度只会更新我们使用的嵌入矩阵的特定行。 这对于自然语言处理非常重要,就像在这里一样,我想创建一个 PyTorch 模型,该模型将实现这个非常简单的方程。

如果没有这个技巧,那意味着我要输入一个 25,000 x 80,000 元素的数组,这将有点疯狂。 所以这个技巧让我写下了这个。 我只是用 Embedding 替换了 Linear,用一些只输入索引而不是输入独热编码的东西来替换那个,就这样。 然后它继续工作,所以现在每个时代的训练时间大约是一分钟。

现在我们可以把这个想法应用到不仅仅是语言,而是任何东西上。 例如,预测杂货店商品的销售情况。

问题:我们实际上并没有查找任何东西,对吧?我们只是看到了那个带有索引的数组表示?所以我们正在查找。现在存储的词袋的表示不再是 1 1 1 0 0 1,而是 0 1 2 5。因此,我们实际上必须进行矩阵乘法。但是我们不是进行矩阵乘法,而是查找第零个东西,第一个东西,第二个东西和第五个东西。

问题继续:这意味着我们仍然保留了独热编码矩阵吗?不,我们没有。这里没有使用独热编码矩阵。目前没有突出显示独热编码矩阵。我们目前突出显示的是索引列表和权重矩阵中的系数列表:

所以现在我们要做的是更进一步,我们要说根本不使用线性模型,让我们使用一个多层神经网络。让我们的输入可能包括一些分类变量。这些分类变量,我们将只将其作为数值索引。因此,这些的第一层不会是一个普通的线性层,它们将是一个嵌入层,我们知道在数学上它的行为与线性层完全相同。因此,我们的希望是现在我们可以使用这个来为任何类型的数据创建一个神经网络。

罗斯曼竞赛

笔记本

几年前在 Kaggle 上有一个名为 Rossmann 的竞赛,这是一个德国的杂货连锁店,他们要求预测他们商店中商品的销售情况。这包括分类和连续变量的混合。在 Guo/Berkhahn 的这篇论文中,他们描述了他们的第三名作品,这比第一名作品简单得多,但几乎一样好,但简单得多,因为他们利用了这个所谓的实体嵌入的想法。在论文中,他们认为他们发明了这个,实际上早些时候由 Yoshua Bengio 和他的合著者在另一个 Kaggle 竞赛中写过。尽管如此,我觉得 Guo 在描述这个如何在许多其他方面使用上走得更远,所以我们也会谈论这个。

笔记本在深度学习存储库中,因为我们在深度学习课程中讨论了一些深度学习特定方面,在这门课程中,我们主要将讨论特征工程,我们还将讨论这个嵌入的想法。

让我们从数据开始。所以数据是,2015 年 7 月 31 日,第 1 号店开业。他们正在进行促销活动。有学校假期。不是国家假期,他们卖出了 5263 件商品。这是他们提供的关键数据。所以目标显然是在没有销售信息的测试集中预测销售额。他们还告诉你,对于每家商店,它是某种特定类型的,销售某种特定种类的商品,其最近的竞争对手距离一定距离,竞争对手在 2008 年 9 月开业,还有一些关于促销的更多信息,我不知道这意味着什么。就像许多 Kaggle 竞赛一样,他们允许您下载外部数据集,只要您与其他竞争者分享。他们还告诉您每家商店所在的州,因此人们下载了德国不同州的名称,他们为德国每周下载了一些谷歌趋势数据。我不知道他们得到了什么具体的谷歌趋势,但是有的。对于每个日期,他们下载了一堆温度信息。就是这样。

这里一个有趣的见解是,Rossmann 可能在某种程度上犯了一个错误,设计这个比赛是一个可以使用外部数据的比赛。因为实际上,你并不能知道下周的天气或下周的谷歌趋势。但当你参加 Kaggle 比赛时,你并不在乎这些。你只是想赢,所以你会利用一切可以得到的。

数据清理

首先让我们谈谈数据清理。在这个获得第三名的参赛作品中,实际上并没有进行太多的特征工程,特别是按照 Kaggle 的标准,通常每一个细节都很重要。这是一个很好的例子,展示了使用神经网络可以取得多大的成就,这让我想起了昨天我们谈到的索赔预测比赛,获胜者没有进行任何特征工程,完全依赖深度学习。房间里的笑声,我猜,是来自那些在比赛中进行了一点点特征工程的人们😄

顺便提一下,我发现在比赛中努力工作,然后比赛结束了你没有赢得比赛。然后获胜者出来说这就是我赢得比赛的方法。这是你学到最多的时候。有时候这种情况发生在我身上,我会想,哦,我想到了那个,我试过了,然后我回去发现我那里有个 bug,我没有正确测试,然后我意识到,哦,好吧,我真的需要学会以不同的方式测试这个东西。有时候就像,哦,我想到了那个,但我假设它不会起作用,我真的要记住在做任何假设之前检查一切。你知道,有时候就像,哦,我没有想到那个技术,哇,现在我知道它比我刚刚尝试的一切都要好。否则,如果有人说,嘿,你知道这是一个非常好的技术,你会说好的。但是当你花了几个月的时间尝试做某事,然后别人用那个技术做得更好时,那就相当有说服力了。所以这有点困难,我站在你面前说这里有一堆我用过的技术,我赢得了一些 Kaggle 比赛,我得到了一些最先进的结果。但是当这些信息传达给你时,已经是二手信息了。所以尝试一些东西真的很棒。而且尤其是在深度学习课程中,我注意到,我的一些学生尝试了我说的这个技术,他们第二天就进入了 Kaggle 比赛的前十名,他们说,好的,这算是非常有效。Kaggle 比赛有很多原因是有帮助的。但其中一个最好的方式是比赛结束后发生的事情,所以对于现在即将结束的比赛,确保你观看论坛,看看人们在分享解决方案方面分享了什么,如果你想了解更多,可以自由地问问获胜者,嘿,你能告诉我更多关于这个或那个吗。人们通常很乐意解释。然后最好是尝试自己复制一下。这可以变成一个很棒的博客文章或很棒的内核,可以说,某某说他们使用了这个技术,这里是这个技术的一个非常简短的解释,这里是一点代码展示它是如何实现的,这里是结果展示你可以得到相同的结果。这也可以是一个非常有趣的写作。

数据尽可能易于理解总是很好的。因此,在这种情况下,来自 Kaggle 的数据使用各种整数表示假期。我们可以只使用一个布尔值来表示是否是假期。所以只需清理一下:

train.StateHoliday = train.StateHoliday!='0'
test.StateHoliday = test.StateHoliday!='0'

我们有很多不同的表需要将它们全部合并在一起。我有一种用 Pandas 合并事物的标准方法。我只是使用了 Pandas 的合并函数,具体来说我总是进行左连接。左连接是保留左表中的所有行,你有一个关键列,将其与右侧表中的关键列匹配,然后合并那些也存在于右表中的行。

def join_df(left, right, left_on, right_on=None, suffix='_y'):
    if right_on is None: 
        right_on = left_on
    return left.merge(
        right, 
        how='left', 
        left_on=left_on,
        right_on=right_on, 
        suffixes=("", suffix)
    )

我总是进行左连接的关键原因是,在进行连接之后,我总是检查右侧是否有现在为空的内容:

store = join_df(store, store_states, "Store")
len(store[store.State.isnull()])

因为如果是这样,那就意味着我漏掉了一些东西。我没有在这里展示,但我也检查了行数在之前和之后是否有变化。如果有变化,那就意味着右侧表不是唯一的。所以即使我确定某件事是真的,我也总是假设我搞砸了。所以我总是检查。

我可以继续将州名合并到天气中:

weather = join_df(weather, state_names, "file", "StateName")

如果你看一下谷歌趋势表,它有这个周范围,我需要将其转换为日期以便加入它:

在 Pandas 中这样做的好处是,Pandas 让我们可以访问所有的 Python。例如,在系列对象内部,有一个.str属性,可以让你访问所有的字符串处理函数。就像.cat让你访问分类函数一样,.dt让你访问日期时间函数。所以现在我可以拆分该列中的所有内容。

googletrend['Date']=googletrend.week.str.split(' - ',expand=True)[0]
googletrend['State']=googletrend.file.str.split('_', expand=True)[2]
googletrend.loc[googletrend.State=='NI', "State"] = 'HB,NI'

使用这些 Pandas 函数非常重要,因为它们将被向量化,加速,通常通过 SIMD 至少通过 C 代码,以便运行得又快又顺利。

和往常一样,让我们为我们的日期添加日期元数据:

add_datepart(weather, "Date", drop=False)
add_datepart(googletrend, "Date", drop=False)
add_datepart(train, "Date", drop=False)
add_datepart(test, "Date", drop=False)

最后,我们基本上是在对所有这些表进行去规范化。我们将把它们全部放入一个表中。因此,在谷歌趋势表中,它们主要是按州划分的趋势,但也有整个德国的趋势,所以我们将整个德国的趋势放入一个单独的数据框中,以便我们可以加入它:

trend_de = googletrend[googletrend.file == 'Rossmann_DE']

因此,我们将有这个州的谷歌趋势和整个德国的谷歌趋势。

现在我们可以继续为训练集和测试集同时加入。然后检查两者都没有空值。

store = join_df(store, store_states, "Store")
len(store[store.State.isnull()])
'''
0
'''
joined = join_df(train, store, "Store")
joined_test = join_df(test, store, "Store")
len(joined[joined.StoreType.isnull()]),len(joined_test[joined_test.StoreType.isnull()])
'''
(0, 0)
'''
joined = join_df(joined, googletrend, ["State","Year", "Week"])
joined_test = join_df(joined_test, googletrend, ["State","Year", "Week"])
len(joined[joined.trend.isnull()]),len(joined_test[joined_test.trend.isnull()])
'''
(0, 0)
'''
joined = joined.merge(trend_de, 'left', ["Year", "Week"], suffixes=('', '_DE'))
joined_test = joined_test.merge(trend_de, 'left', ["Year", "Week"], suffixes=('', '_DE'))
len(joined[joined.trend_DE.isnull()]),len(joined_test[joined_test.trend_DE.isnull()])
'''
(0, 0)
'''
joined = join_df(joined, weather, ["State","Date"])
joined_test = join_df(joined_test, weather, ["State","Date"])
len(joined[joined.Mean_TemperatureC.isnull()]),len(joined_test[joined_test.Mean_TemperatureC.isnull()])
'''
(0, 0)
'''

我的合并函数,如果有两列是相同的,我将左侧的后缀设置为空,这样它就不会影响名称,右侧设置为_y

在这种情况下,我不想要任何重复的内容,所以我只是浏览并删除了它们:

for df in (joined, joined_test):
    for c in df.columns:
        if c.endswith('_y'):
            if c in df.columns: 
                df.drop(c, inplace=True, axis=1)
for df in (joined,joined_test):
    df['CompetitionOpenSinceYear'] = \
        df.CompetitionOpenSinceYear.fillna(1900).astype(np.int32)
    df['CompetitionOpenSinceMonth'] = \
        df.CompetitionOpenSinceMonth.fillna(1).astype(np.int32)
    df['Promo2SinceYear'] = \
        df.Promo2SinceYear.fillna(1900).astype(np.int32)
    df['Promo2SinceWeek'] = \
        df.Promo2SinceWeek.fillna(1).astype(np.int32)

这家商店的主要竞争对手自某个日期以来一直开业。因此,我们可以使用 Pandas 的to_datetime,我传入年、月和日。所以这将给我们一个错误,除非它们都有年和月,所以我们将缺失的部分填充为 1900 年和 1 月(见上文)。而我们真正想知道的是这家商店在这个特定记录时已经开业多久了,所以我们可以进行日期相减:

for df in (joined,joined_test):
    df["CompetitionOpenSince"] = \
        pd.to_datetime(dict(
            year=df.CompetitionOpenSinceYear,
            month=df.CompetitionOpenSinceMonth, 
            day=15
        ))
    df["CompetitionDaysOpen"] = \
          df.Date.subtract(df.CompetitionOpenSince).dt.days

现在如果你考虑一下,有时竞争对手的开业时间晚于这一行,所以有时会是负数。而且可能没有意义有负数(即将在 x 天后开业)。现在话虽如此,我绝不会在没有先运行包含它和不包含它的模型的情况下放入这样的东西。因为我们对数据的假设往往是不正确的。在这种情况下,我没有发明任何这些预处理步骤。我写了所有的代码,但它都是基于第三名获奖者的 GitHub 存储库。因此,知道在 Kaggle 竞赛中获得第三名需要做什么,我相当肯定他们会检查每一个这些预处理步骤,并确保它实际上提高了他们的验证集分数。

for df in (joined,joined_test):
    df.loc[df.CompetitionDaysOpen<0, "CompetitionDaysOpen"] = 0
    df.loc[df.CompetitionOpenSinceYear<1990,"CompetitionDaysOpen"]=0

[1:30:44]

因此,我们将创建一个神经网络,其中一些输入是连续的,而另一些是分类的。这意味着在我们的神经网络中,我们基本上会有这种初始权重矩阵。我们将有这个输入特征向量。一些输入将只是普通的连续数字(例如最高温度,到最近商店的公里数),而另一些将被有效地独热编码。但我们实际上不会将其存储为独热编码。我们实际上会将其存储为索引。

因此,神经网络模型需要知道这些列中的哪些应该基本上创建一个嵌入(即哪些应该被视为独热编码),哪些应该直接输入到线性层中。当我们到达那里时,我们将告诉模型哪个是哪个,但实际上我们需要提前考虑哪些我们想要视为分类变量,哪些是连续变量。特别是,我们要将其视为分类的东西,我们不希望创建比我们需要的更多的类别。让我告诉你我的意思。

这次比赛的第三名决定将比赛开放的月数作为一个他们要用作分类变量的东西。为了避免创建比需要的更多的类别,他们将其截断到 24 个月。他们说,超过 24 个月的任何东西,截断到 24 个。因此,这里是比赛开放的唯一值,从零到 24。这意味着将会有一个嵌入矩阵,基本上会有一个嵌入向量,用于尚未开放的事物(0),用于一个月开放的事物(1),依此类推。

for df in (joined,joined_test):
    df["CompetitionMonthsOpen"] = df["CompetitionDaysOpen"]//30
    df.loc[df.CompetitionMonthsOpen>24,"CompetitionMonthsOpen"] = 24
joined.CompetitionMonthsOpen.unique()
'''
array([24,  3, 19,  9,  0, 16, 17,  7, 15, 22, 11, 13,  2, 23, 12,  4, 10,  1, 14, 20,  8, 18,  6, 21,  5]
'''

现在,他们绝对可以将其作为一个连续变量来处理[1:33:14]。他们本可以只是在这里放一个数字,表示开放了多少个月,然后将其视为连续变量,直接输入到初始权重矩阵中。但我发现,显然这些竞争对手也发现了,尽可能地将事物视为分类变量是最好的。这样做的原因是,当你通过一个嵌入矩阵传递一些内容时,意味着每个级别可以被完全不同地处理。例如,在这种情况下,某物是否开放了零个月或一个月是非常不同的。因此,如果你将其作为连续变量输入,神经网络将很难找到具有这种巨大差异的功能形式。这是可能的,因为神经网络可以做任何事情。但如果你不让它变得容易。另一方面,如果你使用嵌入,将其视为分类变量,那么零和一将有完全不同的向量。因此,尤其是在你有足够的数据时,尽可能地将列视为分类变量是一个更好的主意。当我说尽可能时,基本上意味着基数不要太高。因此,如果这是每一行上唯一不同的销售 ID 号码,你不能将其视为分类变量。因为那将是一个巨大的嵌入矩阵,而且每样东西只出现一次,或者是距离最近商店的公里数到小数点后两位,你也不会将其作为分类变量。

这是他们在这次比赛中都使用的经验法则。事实上,如果我们滚动到他们的选择,这是他们的做法:

他们的连续变量是真正连续的东西,比如到竞争对手的公里数,温度等。而其他一切,基本上,他们都视为分类变量。

今天就到这里。下次,我们将结束这个话题。我们将看看如何将这个转化为神经网络,并总结一下。到时见!

机器学习 1:第 12 课

原文:medium.com/@hiromi_suenaga/machine-learning-1-lesson-12-6c2512e005a3

译者:飞龙

协议:CC BY-NC-SA 4.0

来自机器学习课程的个人笔记。随着我继续复习课程以“真正”理解它,这些笔记将继续更新和改进。非常感谢 JeremyRachel 给了我这个学习的机会。

视频 / 笔记本

我想今天我们可以完成在这个 Rossmann 笔记本中的工作,看一下时间序列预测和结构化数据分析。然后我们可能会对我们学到的一切进行一个小小的复习,因为信不信由你,这就是结尾。关于机器学习没有更多需要知道的东西,只有你将在下个学期和余生中学到的一切。但无论如何,我没有别的要教的了。所以我会做一个小小的复习,然后我们将涵盖课程中最重要的部分,那就是思考如何正确、有效地使用这种技术,以及如何让它对社会产生积极影响的方式。

上次,我们谈到了这样一个想法,当我们试图构建这个 CompetitionMonthsOpen 派生变量时,实际上我们将其截断为不超过 24 个月,我们谈到了原因,因为我们实际上希望将其用作分类变量,因为分类变量,由于嵌入,具有更多的灵活性,神经网络可以如何使用它们。所以这就是我们离开的地方。

for df in (joined,joined_test):
    df["CompetitionMonthsOpen"] = df["CompetitionDaysOpen"]//30
    df.loc[df.CompetitionMonthsOpen>24, "CompetitionMonthsOpen"]= 24
joined.CompetitionMonthsOpen.unique()
'''
array([24,  3, 19,  9,  0, 16, 17,  7, 15, 22, 11, 13,  2, 23, 12,  4, 10,  1, 14, 20,  8, 18,  6, 21,  5])
'''

让我们继续进行下去。因为这个笔记本中发生的事情可能适用于你处理的大多数时间序列数据集。正如我们所讨论的,虽然我们在这里使用了df.apply,但这是在每一行上运行一段 Python 代码,速度非常慢。所以只有在找不到可以一次对整列进行操作的矢量化 pandas 或 numpy 函数时才这样做。但在这种情况下,我找不到一种方法可以在不使用任意 Python 的情况下将年份和周数转换为日期。

还值得记住这个 lambda 函数的概念。每当你尝试将一个函数应用到某个东西的每一行或张量的每个元素时,如果没有已经存在的矢量化版本,你将不得不调用像DataFrame.apply这样的东西,它将运行你传递给每个元素的函数。所以这基本上是函数式编程中的映射,因为很多时候你想要传递给它的函数是你只会使用一次然后丢弃的东西。使用这种 lambda 方法非常常见。所以这个 lambda 是为了告诉df.apply要使用什么而创建的函数。

for df in (joined,joined_test):
    df["Promo2Since"] = pd.to_datetime(df.apply(
        lambda x: Week(x.Promo2SinceYear, x.Promo2SinceWeek).monday(), 
        axis=1
    ).astype(pd.datetime))
    df["Promo2Days"] = df.Date.subtract(df["Promo2Since"]).dt.days

我们也可以用不同的方式来写这个 [3:16]。以下两个单元格是相同的:

一种方法是定义函数(create_promo2since(x)),然后通过名称传递它,另一种方法是使用 lambda 在现场定义函数。所以如果你不熟悉创建和使用 lambda,练习和玩弄df.apply是一个很好的练习方法。

持续时间 [4:32]

让我们来谈谈这个持续时间部分,起初可能看起来有点具体,但实际上并不是。我们要做的是看三个字段:“促销”“州假期”“学校假期”

所以基本上我们有一个表格:

  • 对于每个商店,对于每个日期,那个商店在那天有促销活动

  • 那个地区的那家店铺在那天有学校假期吗

  • 那个地区的那家店铺在那天有州假期吗

这些事情是事件。带有事件的时间序列非常常见。如果你正在查看石油和天然气钻探数据,你试图说的是通过这根管道的流量,这里是一个代表何时触发了某个警报的事件,或者这里是一个钻头卡住的事件,或者其他。所以像大多数时间序列一样,某种程度上会倾向于代表一些事件。事件发生在某个时间点本身就很有趣,但很多时候时间序列也会显示事件发生前后的情况。例如,在这种情况下,我们正在进行杂货销售预测。如果即将到来一个假期,销售额在假期前后很可能会更高,在假期期间会更低,如果这是一个城市店铺的话。因为你要在离开前备货带东西,然后回来时,你就得重新填满冰箱,例如。虽然我们不必进行这种特征工程来专门创建关于假期前后的特征,但是神经网络,我们能够给神经网络提供它需要的信息,它就不必学习这些信息。它学习的越少,我们就能够利用我们已有的数据做更多事情,利用我们已有的规模架构做更多事情。因此,即使对于神经网络这样的东西,特征工程仍然很重要,因为这意味着我们将能够利用我们拥有的有限数据取得更好的结果,利用我们拥有的有限计算能力取得更好的结果。

因此,这里的基本思想是,当我们的时间序列中有事件时,我们希望为每个事件创建两个新列[7:20]:

  1. 还有多久这个事件再次发生。

  2. 上次那个事件发生已经多久了。

换句话说,距离下一个州假期还有多久,距离上一个州假期已经多久了。所以这不是我知道存在的库或任何其他东西。所以我手工写在这里。

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

因此,重要的是,我需要按店铺来做这个。所以我想说,对于这家店铺,上次促销是什么时候(即自上次促销以来多长时间),下次促销还有多长时间,例如。

我要做的是这样的。我将创建一个小函数,它将接受一个字段名,然后我将依次传递PromoStateHolidaySchoolHoliday。让我们以学校假期为例。所以我们说字段等于学校假期,然后我们说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)]
'''

这里是我的经验法则。我的经验法则是看看该类别有多少个离散值(即嵌入矩阵中的行数),并使嵌入的维度为该值的一半。所以如果是星期几,第二个,有八行和四列。这里是(c+1)//2 ——列数除以二。但是我说不要超过 50。在这里你可以看到对于商店(第一行),有 116 家商店,只有一个维度为 50。为什么是 50?我不知道。到目前为止似乎效果还不错。你可能会发现你需要一些稍微不同的东西。实际上,对于厄瓜多尔杂货比赛,我还没有真正尝试过调整这个,但我认为我们可能需要一些更大的嵌入大小。但这是可以摆弄的东西。

emb_szs = [(c, min(50, (c+1)//2)) for _,c in cat_sz]
emb_szs
'''
[(1116, 50),
 (8, 4),
 (4, 2),
 (13, 7),
 (32, 16),
 (3, 2),
 (26, 13),
 (27, 14),
 (5, 3),
 (4, 2),
 (4, 2),
 (24, 12),
 (9, 5),
 (13, 7),
 (53, 27),
 (22, 11),
 (7, 4),
 (7, 4),
 (4, 2),
 (4, 2),
 (9, 5),
 (9, 5)]
'''

问题:随着基数大小变得越来越大,您正在创建越来越宽的嵌入矩阵。因此,您是否会因为选择了 70 个参数而极大地增加过拟合的风险,因为模型永远不可能捕捉到数据实际巨大的所有变化[36:44]?这是一个很好的问题,所以让我提醒您一下现代机器学习和旧机器学习之间的区别的黄金法则。在旧的机器学习中,我们通过减少参数数量来控制复杂性。在现代机器学习中,我们通过正则化来控制复杂性。所以简短的答案是不。我不担心过拟合,因为我避免过拟合的方式不是通过减少参数数量,而是通过增加丢弃率或增加权重衰减。现在说到这一点,对于特定的嵌入,没有必要使用比我需要的更多的参数。因为正则化是通过给模型更多的随机数据或实际上对权重进行惩罚来惩罚模型。所以我们宁愿不使用比必要更多的参数。但是在设计架构时,我的一般经验法则是在参数数量方面慷慨一些。在这种情况下,如果经过一些工作后,我们觉得商店实际上似乎并不那么重要。那么我可能会手动去修改它,使其更小。或者如果我真的发现这里的数据不够,我要么过拟合了,要么使用的正则化比我感到舒适的要多,那么您可能会回去。但我总是会从参数慷慨的角度开始。在这种情况下,这个模型表现得相当不错。

好的,现在我们有一个包含每个嵌入矩阵的行数和列数的元组列表[38:41]。所以当我们调用get_learner来创建我们的神经网络时,这是我们传入的第一件事:

  • emb_szs: 我们的每个嵌入有多大

  • len(df.columns)-len(cat_vars): 我们有多少连续变量

  • [1000,500]: 每一层要创建多少个激活

  • [0.001,0.01]: 每一层使用的丢弃率是多少

m = md.get_learner(
    emb_szs, 
    len(df.columns)-len(cat_vars),
    0.04, 1, 
    [1000,500], 
    [0.001,0.01], 
    y_range=y_range
)
m.summary()

然后我们可以继续调用fit。我们训练了一段时间,得到了大约 0.1 的分数。

m.fit(lr, 1, metrics=[exp_rmspe])
'''
[ 0\.       0.01456  0.01544  0.1148 ]
'''
m.fit(lr, 3, metrics=[exp_rmspe])
'''
[ 0\.       0.01418  0.02066  0.12765]                           
[ 1\.       0.01081  0.01276  0.11221]                           
[ 2\.       0.00976  0.01233  0.10987]
'''
m.fit(lr, 3, metrics=[exp_rmspe], cycle_len=1)
'''
[ 0\.       0.00801  0.01081  0.09899]                            
[ 1\.       0.00714  0.01083  0.09846]                            
[ 2\.       0.00707  0.01088  0.09878]
'''

所以我尝试在测试集上运行这个,并且上周我把它提交到了 Kaggle,这里是结果[39:25]:

私人分数 0.107,公共分数 0.103。所以让我们看看这将如何进行。让我们从公共排行榜开始:

在 3000 个参赛者中排名第 340。这不太好。让我们试试私人排行榜,排名是 0.107。

哦,第 5 名[40:30]。所以希望您现在在想,哦,有一些 Kaggle 比赛即将结束,我参加了,并且花了很多时间在公共排行榜上取得好成绩。我想知道那是否是个好主意。答案是否定的,那不是。Kaggle 公共排行榜并不是要取代您精心开发的验证集。例如,如果您正在进行冰山比赛(哪些是船只,哪些是冰山),那么他们实际上在公共排行榜中放入了大约 4000 张合成图像,而在私人排行榜中没有放入任何图像。所以这是 Kaggle 测试您的一个非常好的方面,“您是否创建了一个好的验证集并且是否信任它?”因为如果您更信任排行榜的反馈而不是验证反馈,那么当您认为自己排名第 5 时,您可能会发现自己排名第 350。在这种情况下,我们实际上有一个相当不错的验证集,因为正如您所看到的,它大约是 0.1,而我们实际上确实得到了大约 0.1。所以在这种情况下,这个比赛的公共排行榜完全没有用。

问题:那么,公共排行榜的前几名实际上与私人排行榜的前几名有多少对应呢?因为在流失预测挑战中,有 4 个人完全超过了其他人[42:07]。这完全取决于情况。如果他们随机抽取公共和私人排行榜,那么这应该是非常有指示性的。但也可能不是。所以在这种情况下,公共排行榜上的第二名最终赢得了比赛。公共排行榜上的第一名排在第七位。事实上,你可以看到这里的小绿色标记。而另外一个人则跃升了 96 个名次。

如果我们用刚刚看过的神经网络进入,我们将跃升 350 个名次。所以这取决于情况。有时他们会告诉你公共排行榜是随机抽样的。有时他们会告诉你不是。通常你必须通过查看验证集结果和公共排行榜结果之间的相关性来判断它们之间的相关性有多好。有时,如果有 2 或 3 个人远远领先于其他人,他们可能已经发现了某种泄漏或类似的情况。这通常是一种存在某种技巧的迹象。

好的,这就是 Rossmann,这也是我们所有材料的结束。

回顾[44:21]

我们学会了两种训练模型的方法。一种是通过构建树,另一种是使用 SGD。因此,SGD 方法是一种可以训练线性模型或具有非线性层之间的线性层堆叠的模型的方法。而树构建具体将给我们一棵树。然后,我们可以将树构建与装袋结合起来创建随机森林,或者与提升结合起来创建 GBM,或者其他一些略有不同的变体,比如极端随机树。因此,值得提醒自己这些东西是做什么的。所以让我们看一些数据。实际上,让我们具体看看分类数据。因此,分类数据可能看起来有几种可能性。比如我们有邮政编码,所以我们有 94003 作为我们的邮政编码。然后我们有销售额,比如 50。对于 94131,销售额为 22,等等。因此,我们有一些分类变量。我们可以表示这种分类变量的几种方式。一种是只使用数字。也许一开始它不是一个数字。也许根本不是一个数字。也许一个分类变量是像旧金山、纽约、孟买和悉尼这样的城市。但我们可以通过任意决定给它们编号来将其转换为数字。因此,它最终成为一个数字。我们可以使用这种任意的数字。因此,如果发现相邻的邮政编码具有相似的行为,那么邮政编码与销售额的图表可能看起来像这样,例如:

或者,如果相邻的两个邮政编码在任何方面都没有相似的销售行为,你会期望看到更像这样的情况:

有点到处都是。所以有两种可能性。如果我们只是用这种方式对邮政编码进行编码,那么随机森林会做什么是,它会说,好的,我需要找到我的最佳分割点——使两侧的标准差尽可能小或在数学上等效地具有最低均方根误差的分割点。因此,在这种情况下,它可能会选择这里作为第一个分割点,因为在左侧有一个平均值,在另一侧有另一个平均值[48:07]。

然后对于它的第二个分割点,它会说如何分割右侧,它可能会说我会在这里分割,因为现在我们有了这个平均值和这个平均值:

最后,它会说我们如何拆分中间部分,然后它会说好的,我会在中间拆分。所以你可以看到,即使它贪婪地自上而下一次一次地进行拆分,它仍然能够专注于它需要的拆分集合。唯一的原因是如果两半总是完全平衡,那么它就无法做到这一点。但即使发生这种情况,也不会是世界末日。它会在其他地方拆分,下一次,两部分树仍然完全平衡的可能性非常小。因此,在实践中,这完全没问题。

在第二种情况下,它可以做同样的事情。即使一个邮政编码与其相邻的邮政编码之间在数字上没有关系。我们仍然可以看到,如果在这里拆分,一侧的平均值,另一侧的平均值可能大约在这里:

那么接下来它会在哪里拆分?可能是在这里,因为这一侧的平均值,另一侧的平均值在这里。

所以,可以做同样的事情。它将需要更多的拆分,因为它将需要缩小到每个单独的大邮政编码和每个单独的小邮政编码。但这仍然没问题。所以当我们处理为随机森林或 GBM 构建决策树时,我们 tend to encode our variables just as ordinals.

另一方面,如果我们正在做神经网络或者像线性回归或逻辑回归这样的最简单版本,它能做的最好就是(绿色),这一点一点也不好:

而且这个也是一样的:

所以一个序数对于线性模型或将线性和非线性模型堆叠在一起的模型来说并不是一个有用的编码。所以,我们创建一个独热编码。就像这样:

通过这种编码,可以有效地创建一个小直方图,其中每个级别都会有一个不同的系数。这样,它可以做到它需要做的事情。

问题:在什么时候这对你的系统变得太繁琐?几乎从来没有。因为请记住,在现实生活中,我们实际上不需要创建那个矩阵。相反,我们可以只有四个系数,然后进行索引查找,这在数学上等同于在独热编码上进行乘法。所以这不是问题。

有一件事要提到。我知道你们已经学到了更多关于事物的分析解决方案。在像线性回归这样的分析解决方案中,你无法解决这种程度的共线性问题。换句话说,如果不是孟买、纽约或旧金山,你就知道某样东西在悉尼。换句话说,这四个类别中的第四个与其他三个之间存在百分之百的共线性。因此,如果你尝试以这种方式在分析上解决线性回归问题,整个事情就会崩溃。现在请注意,对于 SGD,我们没有这样的问题。像 SGD 为什么会在乎呢?我们只是沿着导数走一步。它会在乎一点,因为最终,共线性的主要问题在于有无限数量的同样好的解决方案。换句话说,我们可以增加左边的所有这些,减少这个。或者减少所有这些,增加这个。它们会平衡。

当存在无限多个好的解决方案时,意味着损失曲面上有很多平坦区域,这可能会使优化变得更加困难。因此,摆脱所有这些平坦区域的真正简单方法是添加一点正则化。因此,如果我们添加了一点权重衰减,比如 1e-7,那么这就表示这些解决方案不再是完全相同的,最好的解决方案是参数最小且彼此最相似的解决方案,这将使其再次成为一个良好的损失函数。

问题:您能澄清一下您提到为什么独热编码不会那么繁琐的那一点吗?当然。如果我们有一个独热编码向量,并且将其乘以一组系数,那么这完全等同于简单地说让我们找到其中值为 1 的那个值。换句话说,如果我们将1000存储为零,0100存储为一,0020存储为二,那么这完全等同于只是说嘿,查找数组中的那个值。

所以我们称这个版本为嵌入。因此,嵌入是一个权重矩阵,您可以将其与独热编码相乘。这只是一个计算快捷方式。但从数学上讲,它是一样的。

解决线性模型的解析方法与使用 SGD 解决的方法之间存在关键差异。使用 SGD,我们不必担心共线性等问题,至少不像解析方法那样。然后使用 SGD 解决线性模型或单层或多层模型与使用树的区别;树会对更少的事情提出异议。特别是,您可以将序数作为您的分类变量,并且正如我们之前学到的,对于树,我们也不必担心对连续变量进行归一化,但是对于这些使用 SGD 训练的模型,我们必须担心。

然后我们还学到了很多关于解释随机森林的知识。如果您感兴趣,您可能会尝试使用相同的技术来解释神经网络。如果您想知道在神经网络中哪些特征很重要,您可以尝试同样的方法;依次对每列进行洗牌,看看它对准确性的影响有多大。这将是您神经网络的特征重要性。然后,如果您真的想玩得开心,认识到,那么,对该列进行洗牌只是计算输出对该输入的敏感性的一种方式,换句话说,就是输出对该输入的导数。因此,也许您可以直接要求 PyTorch 给您输出关于输入的导数,看看是否会得到相同类型的答案。

您可以对偏依赖图做同样的事情。您可以尝试使用神经网络做完全相同的事情;用相同的值替换列中的所有内容,对 1960、1961、1962 进行绘图。我不知道有没有人在以前做过这些事情,不是因为这是火箭科学,而只是可能没有人想到或者不在库中,我不知道。但如果有人尝试过,我认为您会发现它很有用。这将成为一篇很棒的博客文章。甚至如果您想进一步,也可以写成论文。所以这是一个您可以尝试的想法。因此,大多数这些解释技术并不特别适用于随机森林。像树解释器这样的东西当然适用,因为它们都是关于树内部的东西。

问题:在树解释器中,我们正在查看特征的路径及其贡献。在神经网络的情况下,我猜每个激活在其路径上的贡献会是相同的,对吗?是的,也许。我不知道。我还没有考虑过这个。问题继续:我们如何从激活中推断出结论?Jeremy:说“推断”这个词要小心,因为人们通常使用“推断”这个词来特指测试时间的预测。你的意思是对模型进行一种询问。我不确定。我们应该考虑一下。实际上,Hinton 和他的一位学生刚刚发表了一篇关于如何用树来近似神经网络的论文,就是出于这个原因。我还没有看过这篇论文。

问题:在线性回归和传统统计学中,我们关注的一件事是变化的统计显著性之类的东西。所以在考虑树解释器或者瀑布图,我猜这只是一种可视化。我猜这在哪里适用?因为我们可以看到,哦,是的,这看起来很重要,因为它导致了很大的变化,但我们怎么知道它在传统上是否具有统计显著性?所以大多数时候,我不关心传统的统计显著性,原因是现在,统计显著性的主要驱动因素是数据量,而不是实际重要性。而且现在,您构建的大多数模型都会有如此多的数据,以至于每一个微小的事情都会在统计上显著,但其中大多数在实际上并不重要。因此,我的主要关注点是实际重要性,即这种影响的大小是否影响您的业务?在我们处理的数据较少时,统计显著性更为重要。如果您确实需要了解统计显著性,例如,因为您有一个非常小的数据集,因为标记成本很高或者很难收集,或者是一个罕见疾病的医疗数据集,您总是可以通过自助法来获得统计显著性,也就是说,您可以随机重新对数据集进行多次抽样,多次训练您的模型,然后您可以看到预测的实际变化。因此,通过自助法,您可以将任何模型转化为能够给出置信区间的东西。有一篇由 Michael Jordan 撰写的论文,其中有一种被称为小自助袋的技术,实际上将这一点推进了一步,如果您感兴趣,那么这篇论文是值得一读的。

问题:你说如果你在做随机森林时不需要一个独热编码矩阵。如果我们这样做会发生什么,模型会有多糟糕?我们实际上确实这样做了。记得我们有那个最大类别大小,我们确实创建了一个独热编码,我们这样做的原因是我们的特征重要性会告诉我们个别级别的重要性,我们的部分依赖图,我们可以包括个别级别。所以这并不一定会使模型变得更糟,它可能会使它变得更好,但它可能根本不会改变太多。在这种情况下,它几乎没有改变。问题继续:这也是我们在真实数据中注意到的一点,如果基数更高,比如说有 50 个级别,如果你做独热编码,随机森林表现得非常糟糕?Jeremy:是的,没错。这就是为什么在 Fast.AI 中,我们有最大分类大小,因为在某个时候,你的独热编码变量会变得太稀疏。所以我通常在 6 或 7 处截断。另外,当你超过那个点时,它变得不那么有用,因为对于特征重要性来说,要看的级别太多了。问题继续:它是否可以只查看那些不重要的级别,然后将那些显著的特征视为重要?Jeremy:是的,这样也可以。就像一旦基数增加得太高,你基本上就是把你的数据分割得太多了,所以在实践中,你的有序版本可能会更好。

没有时间来审查所有内容,但这是关键概念,然后记住我们可以使用的嵌入矩阵可能不只有一个系数,实际上我们将有几个系数的维度,这对于大多数线性模型来说并不实用。但一旦你有了多层模型,那么现在就创建了一个相当丰富的类别表示,你可以用它做更多的事情。

伦理与数据科学

幻灯片

现在让我们谈谈最重要的部分。我们在这门课程的早期谈到了很多机器学习实际上是有些误导的。人们关注预测准确性,比如亚马逊有一个协同过滤算法用于推荐书籍,最终推荐出它认为你最有可能高评的书。结果他们最终可能会推荐一本你已经有或者你已经知道并且本来就会购买的书,这并没有太大价值。他们应该做的是找出哪本书可以推荐给你,从而改变你的行为。这样,我们实际上最大化了由于推荐而带来的销售增长。所以这种优化影响你的行为与仅仅提高预测准确性之间的区别的想法。提高预测准确性是一个非常重要的区别,在学术界或行业中很少被讨论,令人惊讶。在行业中更多地被讨论,但在大多数学术界中被忽视。所以这是一个非常重要的想法,即最终你的模型的目标,想必是影响行为。所以请记住,我实际上提到了我写的一篇关于这个的论文,在那里我介绍了一种叫做传动系统方法的东西,我谈到了如何将机器学习纳入到我们如何实际影响行为的方式中。这是一个起点,但接下来的问题是,如果我们试图影响行为,我们应该影响什么样的行为,以及如何影响,当我们开始影响行为时可能意味着什么。因为现在很多公司,你最终会在那里工作,都是大公司,你将会构建能够影响数百万人的东西。那意味着什么呢?

实际上我不会告诉你这意味着什么,因为我不知道[1:05:34]。我要做的只是让你意识到一些问题,并让你相信其中两件事:

  1. 你应该关心。

  2. 这些是重大的当前问题。

我希望你关心的主要原因是因为我希望你想成为一个好人,并向你展示不考虑这些事情会让你成为一个坏人。但如果你不觉得有说服力,我会告诉你这个事实。大众汽车被发现在排放测试中作弊。最终被判刑的人是实施那段代码的程序员。他们只是按照指示去做。所以如果你认为自己只是一个技术人员,只是按照指示去做,那是我的工作。我告诉你,如果你这样做,你可能会因为听从指示而被送进监狱,所以 a)不要只是听从指示,因为你可能会成为一个坏人,b)你可能会被送进监狱。

第二件要意识到的事情是,在当下,你在工作中与二十个人开会,大家都在讨论如何实施这个新功能,每个人都在讨论[1:06:49]。每个人都在说“我们可以这样做,这是一种建模的方式,然后我们可以实施它,这是这些约束条件”,你心里有一部分在想我们是否应该这样做?那不是考虑这个问题的正确时机,因为在那时很难站出来说“对不起,我不确定这是一个好主意”。你实际上需要提前考虑如何处理这种情况。所以我希望你现在考虑这些问题,并意识到当你陷入其中时,你可能甚至意识不到正在发生什么。那只是一个像其他会议一样的会议,一群人在讨论如何解决这个技术问题。你需要能够认识到哦,这实际上是一个具有道德影响的事情。

Rachel 实际上写了所有这些幻灯片。很抱歉她不能在这里展示,因为她对这个问题进行了深入研究。她实际上曾身处困难环境,亲眼目睹这些事情发生,我们知道这是多么困难。但让我给你们一个了解发生的情况。

所以工程师试图解决工程问题并导致问题并不是一件新事。在纳粹德国,IBM,被称为 Hollerith 的集团,Hollerith 是 IBM 的原始名称,它来自于实际发明用于跟踪美国人口普查的打孔卡的人。这是世界上第一次大规模使用打孔卡进行数据收集。这变成了 IBM,所以在这一点上,仍然被称为 Hollerith。因此,Hollerith 向纳粹德国出售了一个打孔卡系统,每张打孔卡都会编码,比如这是犹太人,8,吉普赛人,12,用毒气室处决,6。这里有一张描述如何杀死这些不同人的卡片。因此,一名瑞士法官裁定 IBM 的技术协助促成了纳粹分子的任务,并犯下了反人类罪行。这导致了大约二千万平民的死亡。根据我从犹太虚拟图书馆得到这些图片和引用的观点,他们认为“犹太人民的毁灭变得更不重要,因为 IBM 的技术成就的振奋性质只会因为可以获得的奇幻利润而进一步提高”。这是很久以前的事情,希望你们不会最终在促成种族灭绝的公司工作。但也许你们会。

www.nytimes.com/2017/10/27/world/asia/myanmar-government-facebook-rohingya.html www.nytimes.com/2017/10/24/world/asia/myanmar-rohingya-ethnic-cleansing.html

也许你们会去 Facebook,他们现在正在促成种族灭绝。我认识在 Facebook 工作的人,他们根本不知道他们在做什么。现在,在 Facebook,罗辛亚人正处于缅甸的种族灭绝中,他们是缅甸的一个穆斯林族群。婴儿被从母亲怀中抢走扔进火里,人们被杀害,数以百千计的难民。在接受采访时,进行这些行为的缅甸将军表示,我们非常感谢 Facebook 让我们知道“罗辛亚假新闻”,这些人实际上不是人类,他们实际上是动物。现在,Facebook 并不是要促成缅甸罗辛亚人的种族灭绝,不是。相反,发生的是他们想要最大化印象和点击量。结果是,Facebook 的数据科学家发现,如果你拿人们感兴趣的东西并向他们提供稍微更极端的版本,你实际上会得到更多的印象,项目经理们说要最大化这些印象,人们点击了,这就产生了这种情况。潜在的影响是非常巨大和全球性的。这实际上正在发生。这是 2017 年 10 月。正在发生。

问题:我只是想澄清这里发生了什么。所以这是在促进虚假新闻或不准确的媒体吗?是的,让我详细介绍一下,发生的事情是在 2016 年中期,Facebook 解雇了它的人类编辑。所以是人类决定如何在你的主页上排序的。这些人被解雇,被机器学习算法取代。所以这些机器学习算法是由像你们这样的数据科学家编写的,他们有清晰的指标,他们试图最大化他们的预测准确性,并且像“我们认为如果我们把这个东西放在比这个东西更高的位置,我们会得到更多的点击”。结果表明,这些用于在 Facebook 新闻源上放置内容的算法倾向于说“哦,人类的本性是我们倾向于点击那些刺激我们观点的东西,因此更极端版本的我们已经看到的东西”。这对于最大化参与度的 Facebook 收入模型来说是很好的,它在他们所有的关键绩效指标上看起来都很好。那时,有一些负面新闻,我不确定 Facebook 现在放在他们的热门部分的东西是否真的那么准确,但从人们在 Facebook 优化的指标的角度来看,这看起来很棒。所以回到 2016 年 10 月,人们开始注意到一些严重的问题。

例如,在美国,将住房针对某些种族是非法的。这是非法的,然而一个新闻机构发现 Facebook 在 2016 年 10 月确实在做这件事。再次强调,并不是因为数据科学团队中的某个人说“让黑人不能住在好社区”。而是,他们发现他们的自动聚类和分割算法发现了一群不喜欢非裔美国人的人,如果你用这种广告针对他们,那么他们更有可能选择这种住房或其他什么。但有趣的是,即使被告知了三次,Facebook 仍然没有解决这个问题。这就是说这些不仅仅是技术问题。它们也是经济问题。当你开始说你得到报酬的事情(也就是广告),你必须改变你构建这些方式的方式,这样你要么使用更多花钱的人,要么减少对少数群体或其他基于算法的人的针对性,这可能会影响收入。我提到这个原因是因为在你的职业生涯中,你会在某个时候发现自己在一次对话中,你会想“我不确定这在道德上是否可以接受”,而你正在与之交谈的人在心里想“这会让我们赚很多钱”,你永远不会成功地进行对话,因为你在谈论不同的事情。所以当你与比你更有经验和更资深的人交谈时,他们可能听起来像他们知道他们在谈论什么,只要意识到他们的激励并不一定会集中在如何成为一个好人上。就像他们并不是在想如何成为一个坏人,但在我看来,你在这个行业中花的时间越多,你对这些事情的麻木程度就越高,比如也许得到晋升和赚钱并不是最重要的事情。

例如,我有很多擅长计算机视觉的朋友,其中一些人已经创建了似乎几乎是为了帮助威权政府监视他们的公民而量身定制的初创公司。当我问我的朋友们,你们有没有考虑过这种方式的使用,他们通常会对我提出的问题感到有点冒犯。但我要求你们考虑这个问题。无论你最终在哪里工作,如果你最终创建了一个初创公司,工具可以被用于善良或邪恶。所以我并不是说不要创建优秀的计算机视觉目标跟踪和检测工具,因为你可以继续创建一个更好的外科手术干预机器人工具包。我只是说要意识到这一点,考虑一下,谈论一下。

所以我发现这个很有趣。这实际上是一个很酷的事情,meetup.com 做了。他们考虑到了这一点。他们实际上考虑到了这一点。他们认为你知道,如果我们建立一个像我们在课堂上学到的协同过滤系统来帮助人们决定去哪个 meetup。它可能会注意到在旧金山,比起女性,更多的男性倾向于参加技术 meetup,因此它可能会开始决定向更多男性推荐技术 meetup。结果是更多男性会参加技术 meetup,结果是当女性参加技术 meetup 时,她们会觉得哦,这里都是男性,我真的不想去技术 meetup。结果是算法会得到新的数据,表明男性更喜欢技术 meetup,所以继续下去。因此,算法的这种初步推动可能会产生这种恶性循环。最终,你可能会得到几乎全是男性的技术 meetup。因此,这种反馈循环是一个微妙的问题,当你考虑构建算法时,你真的要考虑到你正在改变的行为是什么。

所以另一个有点可怕的例子是在这篇论文中,作者描述了美国许多部门现在正在使用预测性执法算法。那么我们去哪里找即将犯罪的人。你知道,算法简单地反馈给你基本上你给它的数据。所以如果你的警察局过去曾经进行过种族歧视,那么它可能会稍微更频繁地建议你去黑人社区检查是否有人犯罪。结果是你的警察会更多地去黑人社区,结果是他们逮捕更多黑人,结果是数据显示黑人社区更不安全。结果是算法告诉警察,也许你应该更频繁地去黑人社区,依此类推。

这不像是未来可能发生的模糊可能性。这是来自认真研究数据和理论的顶尖学者的记录工作。这些严肃的学术工作表明,不,这正在发生。再次,我相信开始创建这种预测性执法算法的人并没有想过如何逮捕更多黑人。希望他们在想,天哪,我希望我的孩子在街上更安全,我该如何创造一个更安全的社会。但他们没有考虑到这种恶性循环。

关于社交网络算法的这篇文章实际上是最近《纽约时报》上我一个朋友 Renee Diresta 写的,她做了一些令人惊讶的事情。她建立了一个第二个 Facebook 账号,一个假账号。那时她对反疫苗运动非常感兴趣,所以她开始关注一些反疫苗者,并访问了一些反疫苗链接。突然间,她的新闻动态开始充斥着反疫苗者的新闻,以及其他一些东西,比如化学尾迹和深层国家阴谋论等等。于是她开始点击这些内容,她点击得越多,Facebook 推荐的极端离谱的阴谋论就越多。所以现在当 Renee 去那个 Facebook 账号时,整个页面都充满了愤怒、疯狂、极端的阴谋论内容。这就是她看到的一切。所以如果这是你的世界,那么在你看来,这就像是一个不断提醒和证明所有这些东西的连续。所以再次强调,这种失控的反馈循环最终告诉缅甸将军们,他们的 Facebook 主页上罗兴亚人是动物、假新闻,以及其他一切。

很多这些也来自偏见。所以让我们具体谈谈偏见。图像软件中的偏见来自数据中的偏见。所以我认识的大多数在谷歌大脑构建计算机视觉算法的人,很少有人是有色人种。所以当他们用家人和朋友的照片来训练算法时,他们用的是很少有色人种的照片。所以当 FaceApp 决定我们要尝试查看很多 Instagram 照片,看看哪些被点赞最多时,他们并没有意识到,答案是浅色面孔。所以他们建立了一个生成模型让你看起来更“性感”,这是实际照片,这是更性感的版本。所以更性感的版本更白,鼻孔更少,看起来更欧洲化。这显然不受欢迎。再次强调,我不认为 FaceApp 的任何人会说“让我们创造一些让人看起来更白的东西”。他们只是用周围的人的一堆图像来训练它。这也有严重的商业影响。他们不得不撤回这个功能。他们受到了大量的负面反弹,这是应该的。

这里有另一个例子。谷歌照片创建了这个照片分类器,飞机、摩天大楼、汽车、毕业典礼和大猩猩。所以想想这对大多数人来说是什么样子。对大多数人来说,他们看到这个,他们不了解机器学习,他们会说“搞什么鬼,谷歌的某个人写了一些代码把黑人称为大猩猩”。这就是它看起来的样子。我们知道那不是发生的事情。我们知道发生的是谷歌计算机视觉专家团队中没有或只有少数有色人种的人建立了一个分类器,使用他们可以获得的所有照片,所以当系统遇到皮肤较黑的人时,它会认为“哦,我之前主要只见过大猩猩中的这种情况,所以我会把它放在那个类别中”。再次强调,数据中的偏见会导致软件中的偏见,商业影响也非常显著。谷歌因此受到了很多负面公关影响,这是应该的。有人在他们的 Twitter 上发布了这张照片。他们说看看谷歌照片刚刚决定做的事情。

你可以想象第一届由人工智能评选的国际选美大赛发生了什么。基本上结果是所有美丽的人都是白人。所以你可以看到这种图像软件中的偏见,这要归功于数据中的偏见,也要归功于构建团队缺乏多样性。

在自然语言处理中也可以看到同样的情况。这里是土耳其语。O 是土耳其语中没有性别的代词。没有他和她之分。但当然在英语中,我们并没有一个广泛使用的无性别单数代词,所以谷歌翻译将其转换为这个。现在有很多人在网上看到这个并且字面上说“那又怎样?”它正确地反馈了英语中的通常用法。我知道这是如何训练的,就像 Word2vec 词向量一样,它是在谷歌新闻语料库、谷歌图书语料库上训练的,它只是告诉我们事情是如何的。从某种角度来看,这是完全正确的。用来创建这种有偏见算法的有偏见数据实际上是人们几十年来写书和报纸文章的实际数据。但这是否意味着这是你想要创建的产品?这是否意味着这是你必须创建的产品?仅仅因为你训练模型的特定方式导致它最终做出这样的结果,这真的是你想要的设计吗?你能想到这可能会产生什么负面影响和反馈循环吗?如果这些事情中的任何一件让你感到不安,那么现在,幸运的是,你有一个新的很酷的工程问题要解决。我如何创建无偏见的自然语言处理解决方案?现在有一些初创公司开始做这个,并开始赚钱。所以这对你来说是机会。就像嘿,这里有一些人因为他们糟糕的模型而创造出了扭曲的社会结果。你可以去建立一些更好的东西。Word2vec 词向量中的偏见的另一个例子是餐厅评论中排名较低的墨西哥餐厅,因为“墨西哥”这个词在美国新闻和书籍中更常与犯罪相关的词语联系在一起。同样,这是一个正在发生的真实问题。

Rachel 实际上对普通的 Word2vec 词向量进行了一些有趣的分析。她基本上把它们拿出来,根据一些在其他地方进行的研究来看这些类比。所以你可以看到,Word2vec 向量方向显示父亲对医生就像母亲对护士一样。男人对计算机程序员就像女人对家庭主妇一样,等等。所以很容易看出这些词向量中包含了什么。它们在我们今天使用的几乎所有自然语言处理软件中都是基础的。

这里有一个很好的例子。ProPublica 实际上在这个领域做了很多有益的工作。现在许多法官都可以访问判决指南软件。因此,判决指南软件告诉法官对于这个个体,我们建议这种类型的刑罚。当然,法官不懂机器学习,所以他们有两个选择,要么按照软件的建议行事,要么完全忽视它,有些人属于每个类别。对于那些属于按照软件建议行事的人,这里是发生的事情。对于那些被标记为高风险的人,实际上没有再次犯罪的人中,白人约四分之一,非裔美国人约一半。所以,那些没有再次犯罪的人中,如果是非裔美国人,他们被标记为高风险的情况几乎是白人的两倍。而那些被标记为低风险但实际上再次犯罪的人中,白人约一半,非裔美国人只有 28%。所以这是数据,我希望没有人会刻意创造这样的情况。但是当你从有偏见的数据开始,数据显示白人和黑人吸食大麻的比率大致相同,但黑人被监禁的频率大约是白人的 5 倍,美国的司法系统的性质,至少目前来看,是不平等的,不公平的。因此,输入到机器学习模型中的数据基本上会支持这种现状。然后由于负面反馈循环,情况只会变得越来越糟。

现在我要告诉你关于这个有趣的事情,研究员阿贝·龚指出了一些问题。以下是一些正在被问到的问题。所以让我们来看一个。你的父亲是否曾被逮捕过?你对这个问题的回答将决定你是否被关押以及关押多久。现在作为一个机器学习研究员,你认为这可能会提高你算法的预测准确性并获得更好的 R²。可能会,但我不知道。也许会。你试试看,哦,我有了更好的 R²。那么这意味着你应该使用它吗?还有另一个问题。你认为因为一个人的父亲是谁而把他关押更长时间是合理的吗?然而,这些实际上是我们现在问罪犯的问题的例子,然后将其放入一个机器学习系统中来决定他们的命运。再次,设计这个的人可能他们是专注于技术卓越,获得 ROC 曲线下面积的最大值,我发现了这些很好的预测因子,让我又多了 0.02。我猜他们没有停下来思考,这样决定谁应该更长时间被关押是否合理。

因此,将这些放在一起,你可以看到这会变得越来越可怕。我们以 Taser 这样的公司为例,Taser 是一种会给你一个大电击的设备,Taser 设法与一些学术研究人员建立了良好的关系,这些研究人员似乎会说他们要他们说的话,以至于现在如果你看数据,结果表明如果你被电击,你可能会死亡的概率相当高。这并不罕见。然而,他们支付给研究这个问题的研究人员一直回来说“哦不,这与电击无关。他们之后立即死亡完全无关。这只是一个偶然事件,事情发生了”。因此,这家公司现在拥有 80%的身体摄像机市场份额。他们开始收购计算机视觉 AI 公司。他们将尝试使用这些警察身体摄像机视频来预测犯罪活动。那意味着什么?这就像是我现在有一些增强现实显示,告诉我电击这个人,因为他们即将做一些坏事?这是一个令人担忧的方向,所以我确信没有人在 Taser 或他们收购的公司中的数据科学家会认为这是他们想要帮助创造的世界,但他们可能会发现自己或你可能会发现自己处于这种讨论的中心,虽然这不是明确讨论的话题,但你内心可能会有一部分在想“我想知道这可能会被如何使用”。我不知道在这种情况下应该做什么才是正确的,因为你可以询问,当然人们会说“不不不”。那你能做什么?你可以要求一些书面承诺,你可以决定离开,你可以开始研究事情的合法性,比如说,至少保护自己的法律情况。我不知道。想一想你会如何应对。

这些是 Rachel 提出的一些问题,作为需要考虑的事情。如果你正在考虑构建一个数据产品或使用模型,如果你正在构建机器学习模型,那是有原因的。你正在尝试做某事。那么数据中可能存在什么偏见?因为无论数据中存在什么偏见,最终都会成为你预测的偏见,潜在地影响你正在影响的行动,潜在地影响你返回的数据,你可能会得到一个反馈循环。

如果构建它的团队不够多样化,你可能会错过什么?例如,Twitter 的一位高级执行官在选举之前很久就警告了 Twitter 存在严重的俄罗斯机器人问题。那时 Twitter 高管团队中唯一的黑人。唯一的一个。不久之后,他们失去了工作。因此,拥有一个更多样化的团队意味着拥有更多样化的观点、信仰、想法和寻找的事物等等。因此,非多样化的团队似乎会犯更多这样的错误。

我们能审计代码吗?它是开源的吗?检查不同群体之间的不同错误率。我们是否可以使用一个极易解释和易于沟通的简单规则?如果出了问题,我们是否有一个处理问题的好方法?

当我们与人们谈论这个问题时,很多人都去找 Rachel 说我对我的组织正在做的事情感到担忧,我该怎么办[1:36:21]?或者我只是担心我的 toxic workplace,我该怎么办?而 Rachel 往往会说你有考虑过离开吗?他们会说我不想失去工作。但实际上,如果你会编程,你是整个人口的 0.3%。如果你会编程和做机器学习,你可能是整个人口的 0.01%。你是极其极其受欢迎的。所以实际上,显然一个组织不希望你觉得自己可以随时离开找另一份工作,这不符合他们的利益。但这绝对是真的。所以我希望你们在这门课程中能够获得足够的自信,认识到你有能力找到工作,特别是一旦你有了第一份工作,第二份工作就会容易很多。所以这很重要,不仅是为了让你觉得你实际上有能力去做出道德行为,而且也很重要意识到如果你发现自己处在一个 toxic 环境中,这是相当普遍的不幸。在湾区尤其存在很多糟糕的科技文化/环境。如果你发现自己处在这样的环境中,最好的做法就是赶紧离开。如果你没有自信认为自己可以找到另一份工作,你就会被困住。所以这真的很重要。很重要的是要知道你在结束这个项目时拥有非常受欢迎的技能,特别是在你有了第一份工作之后,你现在是一个拥有受欢迎技能和在该领域就业记录的人。

问题:这只是一个广泛的问题,你知道人们正在做一些什么来处理数据中的偏见吗[1:38:41]?你知道,这目前是一个有争议的话题,有人试图使用算法方法,他们基本上试图说我们如何识别偏见并将其减去。但我知道的最有效的方法是试图在数据层面上处理它。所以从一个更多元化的团队开始,特别是一个团队包括来自人文学科的人,比如社会学家、心理学家、经济学家,了解反馈循环和对人类行为的影响,他们往往配备了识别和跟踪这类问题的良好工具。然后尝试将解决方案纳入到过程中。但我无法告诉你有一个标准的处理方法,告诉你如何解决它。如果有这样的东西,我们还没有找到。简短的答案是,需要一个多样化的聪明团队意识到问题并努力解决。

评论:这只是针对整个班级的一个普遍性建议,如果你对这些感兴趣,我读过一本很酷的书,Jeremy 你可能听说过,Cathy O’Neil 的《Weapons of Math Destruction》。它涵盖了很多相同的内容[1:40:09]。是的,谢谢你的推荐。Kathy 很棒。她还有一个 TED 演讲。我没能完成这本书。它太令人沮丧了。我只是说“不要再看了”。但是,它确实很好。

好的。就这样了。谢谢大家。对我来说,这真的很紧张。显然,这原本是我和 Rachel 分享的事情。所以我最终做了我一生中最困难的事情之一,那就是独自教授两个人的课程,照顾生病的妻子和一个幼儿,还要学习深度学习课程。而且还要用我刚写的新库来完成所有这些。所以我期待着能好好休息一下。但这一切都是完全值得的。因为你们太棒了。我对你们对我给予的机会作出的反应以及我给予你们的反馈感到非常高兴。所以恭喜。

posted @ 2026-03-26 08:48  布客飞龙II  阅读(1)  评论(0)    收藏  举报