附录 A:机器学习项目清单
此清单可以指导您完成机器学习项目。有八个主要步骤:
- 构建问题并全局看问题。
- 获取数据。
- 探索数据以获得见解。
- 准备数据以更好地暴露底层数据模式给机器学习算法。
- 探索许多不同的模型并列出最佳模型。
- 微调您的模型并将它们组合成一个出色的解决方案。
- 展示您的解决方案。
- 启动,监控和维护您的系统。
显然,您应该随时根据自己的需求调整此清单。
构建问题并全局看问题
- 用业务术语定义目标。
- 您的解决方案将如何使用?
- 当前的解决方案/变通方法是什么(如果有的话)?
- 应该如何框定这个问题(监督/无监督,在线/离线等)?
- 应如何衡量性能?
- 性能度量是否与业务目标一致?
- 达到业务目标所需的最低性能是多少?
- 有哪些可比较的问题?您能重复使用经验或工具吗?
- 是否有人类专业知识?
- 您如何手动解决问题?
- 列出到目前为止您(或其他人)已经做出的假设。
- 验证假设(如果可能)。
获取数据
注意:尽可能自动化,以便您可以轻松获得新鲜数据。
- 列出您需要的数据以及需要多少数据。
- 找到并记录您可以获取数据的位置。
- 检查它将占用多少空间。
- 检查法律义务,并在必要时获得授权。
- 获取访问授权。
- 创建一个工作空间(具有足够的存储空间)。
- 获取数据。
- 将数据转换为您可以轻松操作的格式(而不更改数据本身)。
- 确保敏感信息被删除或受到保护(例如,匿名化)。
- 检查数据的大小和类型(时间序列,样本,地理等)。
- 对测试集进行抽样,将其放在一边,永远不要查看(不要窥探数据!)。
探索数据
注意:尝试从领域专家那里获得这些步骤的见解。
- 为探索创建数据副本(如果需要,将其采样到可管理的大小)。
- 创建一个 Jupyter 笔记本来记录您的数据探索。
- 研究每个属性及其特征:
- 名称
- 类型(分类,整数/浮点数,有界/无界,文本,结构化等)
- 缺失值的百分比
- 噪声和噪声类型(随机的,异常值,舍入误差等)
- 任务的实用性
- 分布类型(高斯,均匀,对数等)
- 对于监督学习任务,识别目标属性。
- 可视化数据。
- 研究属性之间的相关性。
- 研究您如何手动解决问题。
- 识别您可能要应用的有前途的转换。
- 识别可能有用的额外数据(返回到“获取数据”步骤)。
- 记录您学到的东西。
准备数据
注:
- 在数据的副本上工作(保持原始数据集完整)。
- 为您应用的所有数据转换编写函数,有五个原因:
- 这样您可以在下次获得新数据时轻松准备数据
- 这样您可以在未来项目中应用这些转换
- 清理和准备测试集。
- 在解决方案上线后,清理和准备新数据实例
- 使您的准备选择易于视为超参数
- 清理数据:
- 修复或删除异常值(可选)。
- 填补缺失值(例如,用零,平均值,中位数…)或删除它们的行(或列)。
- 执行特征选择(可选):
- 删除为任务提供无用信息的属性。
- 在适当的情况下进行特征工程:
- 离散化连续特征。
- 分解特征(例如,分类,日期/时间等)。
- 添加有前途的特征转换(例如,对数(* x ),平方根( x ), x *²等)。
- 将特征聚合成有前途的新特征。
- 执行特征缩放:
- 标准化或归一化特征。
列出有前途的模型
注:
- 如果数据很大,您可能希望对较小的训练集进行抽样,以便在合理的时间内训练许多不同的模型(请注意,这会惩罚复杂模型,如大型神经网络或随机森林)。
- 再次尝试尽可能自动化这些步骤。
- 使用标准参数从不同类别(例如线性、朴素贝叶斯、SVM、随机森林、神经网络等)训练许多快速而粗糙的模型。
- 测量并比较它们的性能:
- 对于每个模型,使用N折交叉验证,并计算N折上性能指标的均值和标准差。
- 分析每个算法的最重要变量。
- 分析模型所犯的错误类型:
- 人类会使用什么数据来避免这些错误?
- 进行一轮快速的特征选择和工程。
- 再进行一两次快速迭代,按照之前五个步骤。
- 列出前三到五个最有前途的模型,更喜欢产生不同类型错误的模型。
微调系统
注意:
- 在这一步骤中,您将希望尽可能使用更多数据,特别是在朝着微调的最后阶段。
- 像往常一样,尽可能自动化。
- 使用交叉验证微调超参数:
- 将数据转换选择视为超参数,特别是在您不确定时(例如,如果您不确定是用零还是中位数替换缺失值,或者只是删除行)。
- 除非要探索的超参数值非常少,否则更喜欢随机搜索而不是网格搜索。如果训练时间很长,您可能更喜欢贝叶斯优化方法(例如,使用高斯过程先验,如Jasper Snoek 等人所述¹)。
- 尝试集成方法。将您最好的模型组合在一起通常会比单独运行它们产生更好的性能。
- 一旦您对最终模型有信心,请在测试集上测量其性能以估计泛化误差。
警告
在测量泛化误差后不要调整模型:您只会开始过拟合测试集。
呈现您的解决方案
- 记录您所做的工作。
- 创建一个漂亮的演示文稿:
- 确保首先突出整体情况。
- 解释为什么您的解决方案实现了业务目标。
- 不要忘记呈现您沿途注意到的有趣点:
- 描述哪些工作了,哪些没有。
- 列出您的假设和系统的限制。
- 确保通过美丽的可视化或易于记忆的陈述传达您的关键发现(例如,“收入中位数是房价的头号预测因子”)。
启动!
- 准备好将您的解决方案投入生产(连接到生产数据输入,编写单元测试等)。
- 编写监控代码,定期检查系统的实时性能,并在性能下降时触发警报:
- 注意缓慢退化:随着数据的演变,模型往往会“腐烂”。
- 测量性能可能需要一个人类管道(例如,通过众包服务)。
- 还要监控输入的质量(例如,故障传感器发送随机值,或者另一个团队的输出变得陈旧)。这对在线学习系统尤为重要。
- 定期在新数据上重新训练您的模型(尽可能自动化)。
¹ Jasper Snoek 等人,“机器学习算法的实用贝叶斯优化”,《第 25 届国际神经信息处理系统会议论文集》2(2012):2951–2959。
附录 B:自动微分
本附录解释了 TensorFlow 的自动微分(autodiff)功能的工作原理,以及它与其他解决方案的比较。
假设您定义一个函数f(x, y) = x²y + y + 2,并且您需要其偏导数∂f/∂x和∂f/∂y,通常用于执行梯度下降(或其他优化算法)。您的主要选择是手动微分、有限差分逼近、前向自动微分和反向自动微分。TensorFlow 实现了反向自动微分,但要理解它,最好先看看其他选项。所以让我们逐个进行,从手动微分开始。
手动微分
计算导数的第一种方法是拿起一支铅笔和一张纸,利用您的微积分知识推导出适当的方程。对于刚刚定义的函数f(x, y),这并不太难;您只需要使用五条规则:
- 常数的导数是 0。
- λx的导数是λ(其中λ是一个常数)。
- xλ的导数是*λx*(λ) ^– ¹,所以x²的导数是 2x。
- 函数和的导数是这些函数的导数之和。
- λ倍函数的导数是λ乘以其导数。
从这些规则中,您可以推导出方程 B-1。
方程 B-1. f(x, y)的偏导数
∂f ∂x = ∂(x 2 y) ∂x + ∂y ∂x + ∂2 ∂x = y ∂(x 2 ) ∂x + 0 + 0 = 2 x y ∂f ∂y = ∂(x 2 y) ∂y + ∂y ∂y + ∂2 ∂y = x 2 + 1 + 0 = x 2 + 1
对于更复杂的函数,这种方法可能变得非常繁琐,您可能会犯错。幸运的是,还有其他选择。现在让我们看看有限差分逼近。
有限差分逼近
回想一下函数h(x)在点x[0]处的导数h′(x[0])是该点处函数的斜率。更准确地说,导数被定义为通过该点x[0]和函数上另一点x的直线的斜率的极限,当x无限接近x[0]时(参见方程 B-2)。
方程 B-2. 函数h(x)在点x[0]处的导数定义
h ' ( x 0 ) = lim x→x 0 h(x)-h(x 0 ) x-x 0 = lim ε→0 h(x 0 +ε)-h(x 0 ) ε
因此,如果我们想计算f(x, y)关于x在x = 3 和y = 4 处的偏导数,我们可以计算f(3 + ε, 4) - f(3, 4),然后将结果除以ε,使用一个非常小的ε值。这种数值逼近导数的方法称为有限差分逼近,这个特定的方程称为牛顿的差商。以下代码正是这样做的:
def f(x, y): return x**2*y + y + 2 def derivative(f, x, y, x_eps, y_eps): return (f(x + x_eps, y + y_eps) - f(x, y)) / (x_eps + y_eps) df_dx = derivative(f, 3, 4, 0.00001, 0) df_dy = derivative(f, 3, 4, 0, 0.00001)
不幸的是,结果不够精确(对于更复杂的函数来说情况会更糟)。正确的结果分别是 24 和 10,但实际上我们得到了:
>>> df_dx 24.000039999805264 >>> df_dy 10.000000000331966
注意,要计算两个偏导数,我们至少要调用f()
三次(在前面的代码中我们调用了四次,但可以进行优化)。如果有 1,000 个参数,我们至少需要调用f()
1,001 次。当处理大型神经网络时,这使得有限差分逼近方法过于低效。
然而,这种方法实现起来非常简单,是检查其他方法是否正确实现的好工具。例如,如果它与您手动推导的函数不一致,那么您的函数可能存在错误。
到目前为止,我们已经考虑了两种计算梯度的方法:手动微分和有限差分逼近。不幸的是,这两种方法都对训练大规模神经网络有致命缺陷。因此,让我们转向自动微分,从正向模式开始。
正向模式自动微分
图 B-1 展示了正向模式自动微分在一个更简单的函数g(x, y) = 5 + xy 上的工作原理。该函数的图在左侧表示。经过正向模式自动微分后,我们得到右侧的图,表示偏导数∂g/∂x = 0 + (0 × x + y × 1) = y(我们可以类似地得到关于y的偏导数)。
该算法将从输入到输出遍历计算图(因此称为“正向模式”)。它从叶节点获取偏导数开始。常数节点(5)返回常数 0,因为常数的导数始终为 0。变量x返回常数 1,因为∂x/∂x = 1,变量y返回常数 0,因为∂y/∂x = 0(如果我们要找关于y的偏导数,结果将相反)。
现在我们有了所有需要的内容,可以向上移动到函数g中的乘法节点。微积分告诉我们,两个函数u和v的乘积的导数是∂(u × v)/∂x = ∂v/∂x × u + v × ∂u/∂x。因此,我们可以构建右侧的图的大部分,表示为 0 × x + y × 1。
最后,我们可以到达函数g中的加法节点。如前所述,函数和的导数是这些函数的导数之和,因此我们只需要创建一个加法节点并将其连接到我们已经计算过的图的部分。我们得到了正确的偏导数:∂g/∂x = 0 + (0 × x + y × 1)。
图 B-1. 正向模式自动微分
然而,这个方程可以被简化(很多)。通过对计算图应用一些修剪步骤,摆脱所有不必要的操作,我们得到一个只有一个节点的更小的图:∂g/∂x = y。在这种情况下,简化相当容易,但对于更复杂的函数,正向模式自动微分可能会产生一个庞大的图,可能难以简化,并导致性能不佳。
请注意,我们从一个计算图开始,正向模式自动微分产生另一个计算图。这称为符号微分,它有两个好处:首先,一旦导数的计算图被生成,我们可以使用它任意次数来计算给定函数的导数,无论x和y的值是多少;其次,如果需要的话,我们可以再次在结果图上运行正向模式自动微分,以获得二阶导数(即导数的导数)。我们甚至可以计算三阶导数,依此类推。
但也可以在不构建图形的情况下运行正向模式自动微分(即数值上,而不是符号上),只需在运行时计算中间结果。其中一种方法是使用双数,它们是形式为a + bε的奇怪但迷人的数字,其中a和b是实数,ε是一个无穷小数,使得ε² = 0(但ε ≠ 0)。您可以将双数 42 + 24ε看作类似于 42.0000⋯000024,其中有无限多个 0(但当然这只是简化,只是为了让您对双数有一些概念)。双数在内存中表示为一对浮点数。例如,42 + 24ε由一对(42.0, 24.0)表示。
双数可以相加、相乘等,如 Equation B-3 所示。
Equation B-3. 双数的一些操作
λ ( a + b ε ) = λ a + λ b ε ( a + b ε ) + ( c + d ε ) = ( a + c ) + ( b + d ) ε ( a + b ε ) × ( c + d ε ) = a c + ( a d + b c ) ε + ( b d ) ε 2 = a c + ( a d + b c ) ε
最重要的是,可以证明h(a + bε) = h(a) + b × h′(a)ε,因此计算h(a + ε)可以一次性得到h(a)和导数h′(a)。图 B-2 显示了使用双重数计算f(x, y)对x在x = 3 和y = 4 时的偏导数(我将写为∂f/∂x (3, 4))。我们只需要计算f(3 + ε, 4);这将输出一个双重数,其第一个分量等于f(3, 4),第二个分量等于∂f/∂x (3, 4)。
图 B-2. 使用双重数进行正向模式自动微分
要计算∂f/∂y (3, 4),我们需要再次通过图进行计算,但这次是在x = 3 和y = 4 + ε的情况下。
因此,正向模式自动微分比有限差分逼近更准确,但至少在输入较多而输出较少时存在相同的主要缺陷(例如在处理神经网络时):如果有 1,000 个参数,将需要通过图进行 1,000 次传递来计算所有偏导数。这就是逆向模式自动微分的优势所在:它可以在通过图进行两次传递中计算出所有偏导数。让我们看看如何做到的。
逆向模式自动微分
逆向模式自动微分是 TensorFlow 实现的解决方案。它首先沿着图的正向方向(即从输入到输出)进行第一次传递,计算每个节点的值。然后进行第二次传递,这次是在反向方向(即从输出到输入)进行,计算所有偏导数。名称“逆向模式”来自于这个对图的第二次传递,在这个传递中,梯度以相反方向流动。图 B-3 代表了第二次传递。在第一次传递中,所有节点值都是从x = 3 和y = 4 开始计算的。您可以在每个节点的右下角看到这些值(例如,x × x = 9)。为了清晰起见,节点标记为n[1]到n[7]。输出节点是n[7]:f(3, 4) = n[7] = 42。
图 B-3. 逆向模式自动微分
这个想法是逐渐沿着图向下走,计算f(x, y)对每个连续节点的偏导数,直到达到变量节点。为此,逆向模式自动微分在方程 B-4 中大量依赖于链式法则。
方程 B-4. 链式法则
∂f ∂x = ∂f ∂n i × ∂n i ∂x
由于n[7]是输出节点,f = n[7],所以∂f / ∂n[7] = 1。
让我们继续沿着图向下走到n[5]:当n[5]变化时,f会变化多少?答案是∂f / ∂n[5] = ∂f / ∂n[7] × ∂n[7] / ∂n[5]。我们已经知道∂f / ∂n[7] = 1,所以我们只需要∂n[7] / ∂n[5]。由于n[7]只是执行n[5] + n[6]的求和,我们发现∂n[7] / ∂n[5] = 1,所以∂f / ∂n[5] = 1 × 1 = 1。
现在我们可以继续到节点n[4]:当n[4]变化时,f会变化多少?答案是∂f / ∂n[4] = ∂f / ∂n[5] × ∂n[5] / ∂n[4]。由于n[5] = n[4] × n[2],我们发现∂n[5] / ∂n[4] = n[2],所以∂f / ∂n[4] = 1 × n[2] = 4。
这个过程一直持续到我们到达图的底部。在那一点上,我们将计算出f(x, y)在x = 3 和y = 4 时的所有偏导数。在这个例子中,我们发现∂f / ∂x = 24 和∂f / ∂y = 10。听起来没错!
反向模式自动微分是一种非常强大和准确的技术,特别是当输入很多而输出很少时,因为它只需要一个前向传递加上一个反向传递来计算所有输出相对于所有输入的所有偏导数。在训练神经网络时,我们通常希望最小化损失,因此只有一个输出(损失),因此只需要通过图两次来计算梯度。反向模式自动微分还可以处理不完全可微的函数,只要您要求它在可微分的点计算偏导数。
在图 B-3 中,数值结果是在每个节点上实时计算的。然而,这并不完全是 TensorFlow 的做法:相反,它创建了一个新的计算图。换句话说,它实现了符号反向模式自动微分。这样,只需要生成一次计算图来计算神经网络中所有参数相对于损失的梯度,然后每当优化器需要计算梯度时,就可以一遍又一遍地执行它。此外,这使得在需要时可以计算高阶导数。
提示
如果您想在 C++中实现一种新类型的低级 TensorFlow 操作,并且希望使其与自动微分兼容,那么您需要提供一个函数,该函数返回函数输出相对于其输入的偏导数。例如,假设您实现了一个计算其输入平方的函数:f(x) = x²。在这种情况下,您需要提供相应的导数函数:f′(x) = 2x。
附录 C:特殊数据结构
在本附录中,我们将快速查看 TensorFlow 支持的数据结构,超出了常规的浮点或整数张量。这包括字符串、不规则张量、稀疏张量、张量数组、集合和队列。
字符串
张量可以保存字节字符串,这在自然语言处理中特别有用(请参阅第十六章):
>>> tf.constant(b"hello world") <tf.Tensor: shape=(), dtype=string, numpy=b'hello world'>
如果尝试构建一个包含 Unicode 字符串的张量,TensorFlow 会自动将其编码为 UTF-8:
>>> tf.constant("café") <tf.Tensor: shape=(), dtype=string, numpy=b'caf\xc3\xa9'>
还可以创建表示 Unicode 字符串的张量。只需创建一个 32 位整数数组,每个整数代表一个单个 Unicode 码点:¹
>>> u = tf.constant([ord(c) for c in "café"]) >>> u <tf.Tensor: shape=(4,), [...], numpy=array([ 99, 97, 102, 233], dtype=int32)>
注意
在类型为tf.string
的张量中,字符串长度不是张量形状的一部分。换句话说,字符串被视为原子值。但是,在 Unicode 字符串张量(即 int32 张量)中,字符串的长度是张量形状的一部分。
tf.strings
包含几个函数来操作字符串张量,例如length()
用于计算字节字符串中的字节数(或者如果设置unit="UTF8_CHAR"
,则计算代码点的数量),unicode_encode()
用于将 Unicode 字符串张量(即 int32 张量)转换为字节字符串张量,unicode_decode()
用于执行相反操作:
>>> b = tf.strings.unicode_encode(u, "UTF-8") >>> b <tf.Tensor: shape=(), dtype=string, numpy=b'caf\xc3\xa9'> >>> tf.strings.length(b, unit="UTF8_CHAR") <tf.Tensor: shape=(), dtype=int32, numpy=4> >>> tf.strings.unicode_decode(b, "UTF-8") <tf.Tensor: shape=(4,), [...], numpy=array([ 99, 97, 102, 233], dtype=int32)>
您还可以操作包含多个字符串的张量:
>>> p = tf.constant(["Café", "Coffee", "caffè", "咖啡"]) >>> tf.strings.length(p, unit="UTF8_CHAR") <tf.Tensor: shape=(4,), dtype=int32, numpy=array([4, 6, 5, 2], dtype=int32)> >>> r = tf.strings.unicode_decode(p, "UTF8") >>> r <tf.RaggedTensor [[67, 97, 102, 233], [67, 111, 102, 102, 101, 101], [99, 97, 102, 102, 232], [21654, 21857]]>
请注意,解码的字符串存储在RaggedTensor
中。那是什么?
不规则张量
不规则张量是一种特殊类型的张量,表示不同大小数组的列表。更一般地说,它是一个具有一个或多个不规则维度的张量,意味着切片可能具有不同长度的维度。在不规则张量r
中,第二个维度是一个不规则维度。在所有不规则张量中,第一个维度始终是一个常规维度(也称为均匀维度)。
不规则张量r
的所有元素都是常规张量。例如,让我们看看不规则张量的第二个元素:
>>> r[1] <tf.Tensor: [...], numpy=array([ 67, 111, 102, 102, 101, 101], dtype=int32)>
tf.ragged
包含几个函数来创建和操作不规则张量。让我们使用tf.ragged.constant()
创建第二个不规则张量,并沿着轴 0 连接它与第一个不规则张量:
>>> r2 = tf.ragged.constant([[65, 66], [], [67]]) >>> tf.concat([r, r2], axis=0) <tf.RaggedTensor [[67, 97, 102, 233], [67, 111, 102, 102, 101, 101], [99, 97, 102, 102, 232], [21654, 21857], [65, 66], [], [67]]>
结果并不太令人惊讶:r2
中的张量是沿着轴 0 在r
中的张量之后附加的。但是如果我们沿着轴 1 连接r
和另一个不规则张量呢?
>>> r3 = tf.ragged.constant([[68, 69, 70], [71], [], [72, 73]]) >>> print(tf.concat([r, r3], axis=1)) <tf.RaggedTensor [[67, 97, 102, 233, 68, 69, 70], [67, 111, 102, 102, 101, 101, 71], [99, 97, 102, 102, 232], [21654, 21857, 72, 73]]>
这次,请注意r
中的第i个张量和r3
中的第i个张量被连接。现在这更不寻常,因为所有这些张量都可以具有不同的长度。
如果调用to_tensor()
方法,不规则张量将转换为常规张量,用零填充较短的张量以获得相等长度的张量(您可以通过设置default_value
参数更改默认值):
>>> r.to_tensor() <tf.Tensor: shape=(4, 6), dtype=int32, numpy= array([[ 67, 97, 102, 233, 0, 0], [ 67, 111, 102, 102, 101, 101], [ 99, 97, 102, 102, 232, 0], [21654, 21857, 0, 0, 0, 0]], dtype=int32)>
许多 TF 操作支持不规则张量。有关完整列表,请参阅tf.RaggedTensor
类的文档。
稀疏张量
TensorFlow 还可以高效地表示稀疏张量(即包含大多数零的张量)。只需创建一个tf.SparseTensor
,指定非零元素的索引和值以及张量的形状。索引必须按“读取顺序”(从左到右,从上到下)列出。如果不确定,只需使用tf.sparse.reorder()
。您可以使用tf.sparse.to_dense()
将稀疏张量转换为密集张量(即常规张量):
>>> s = tf.SparseTensor(indices=[[0, 1], [1, 0], [2, 3]], ... values=[1., 2., 3.], ... dense_shape=[3, 4]) ... >>> tf.sparse.to_dense(s) <tf.Tensor: shape=(3, 4), dtype=float32, numpy= array([[0., 1., 0., 0.], [2., 0., 0., 0.], [0., 0., 0., 3.]], dtype=float32)>
请注意,稀疏张量不支持与密集张量一样多的操作。例如,您可以将稀疏张量乘以任何标量值,得到一个新的稀疏张量,但是您不能将标量值添加到稀疏张量中,因为这不会返回一个稀疏张量:
>>> s * 42.0 <tensorflow.python.framework.sparse_tensor.SparseTensor at 0x7f84a6749f10> >>> s + 42.0 [...] TypeError: unsupported operand type(s) for +: 'SparseTensor' and 'float'
Sklearn、TensorFlow 与 Keras 机器学习实用指南第三版(九)(2)https://developer.aliyun.com/article/1482474