数据科学的数学基础-三-

305 阅读1小时+

数据科学的数学基础(三)

原文:zh.annas-archive.org/md5/773ab81070b00a4b488f78c66a458af9

译者:飞龙

协议:CC BY-NC-SA 4.0

第七章:神经网络

在过去的 10 年里,一种经历复兴的回归和分类技术是神经网络。在最简单的定义中,神经网络是一个包含权重、偏差和非线性函数层的多层回归,位于输入变量和输出变量之间。深度学习是神经网络的一种流行变体,利用包含权重和偏差的多个“隐藏”(或中间)层节点。每个节点在传递给非线性函数(称为激活函数)之前类似于一个线性函数。就像我们在第五章中学到的线性回归一样,优化技术如随机梯度下降被用来找到最优的权重和偏差值以最小化残差。

神经网络为计算机以前难以解决的问题提供了令人兴奋的解决方案。从识别图像中的物体到处理音频中的单词,神经网络已经创造了影响我们日常生活的工具。这包括虚拟助手和搜索引擎,以及我们 iPhone 中的照片工具。

鉴于媒体的炒作和大胆宣称主导着关于神经网络的新闻头条,也许令人惊讶的是,它们自上世纪 50 年代以来就存在了。它们在 2010 年后突然变得流行的原因是由于数据和计算能力的不断增长。2011 年至 2015 年之间的 ImageNet 挑战赛可能是复兴的最大推动力,将对 140 万张图像进行一千个类别的分类准确率提高到了 96.4%。

然而,就像任何机器学习技术一样,它只适用于狭义定义的问题。即使是创建“自动驾驶”汽车的项目也不使用端到端的深度学习,主要使用手工编码的规则系统,卷积神经网络充当“标签制造机”来识别道路上的物体。我们将在本章后面讨论这一点,以了解神经网络实际上是如何使用的。但首先我们将在 NumPy 中构建一个简单的神经网络,然后使用 scikit-learn 作为库实现。

何时使用神经网络和深度学习

神经网络和深度学习可用于分类和回归,那么它们与线性回归、逻辑回归和其他类型的机器学习相比如何?你可能听说过“当你手中只有一把锤子时,所有事情看起来都像钉子”。每种算法都有其特定情况下的优势和劣势。线性回归、逻辑回归以及梯度提升树(本书未涵盖)在结构化数据上做出了相当出色的预测。将结构化数据视为可以轻松表示为表格的数据,具有行和列。但感知问题如图像分类则不太结构化,因为我们试图找到像素组之间的模糊相关性以识别形状和模式,而不是表格中的数据行。尝试预测正在输入的句子中的下四五个单词,或者解密音频剪辑中的单词,也是感知问题,是神经网络用于自然语言处理的例子。

在本章中,我们将主要关注只有一个隐藏层的简单神经网络。

使用神经网络是否有些大材小用?

对于即将介绍的例子来说,使用神经网络可能有些大材小用,因为逻辑回归可能更实用。甚至可以使用公式方法。然而,我一直是一个喜欢通过将复杂技术应用于简单问题来理解的人。你可以了解技术的优势和局限性,而不会被大型数据集所分散注意力。因此,请尽量不要在更实用的情况下使用神经网络。为了理解技术,我们将在本章中打破这个规则。

一个简单的神经网络

这里有一个简单的例子,让你对神经网络有所了解。我想要预测给定颜色背景下的字体应该是浅色(1)还是深色(0)。以下是不同背景颜色的几个示例,见图 7-1。顶部一行最适合浅色字体,底部一行最适合深色字体。

emds 0701

图 7-1. 浅色背景颜色最适合深色字体,而深色背景颜色最适合浅色字体

在计算机科学中,表示颜色的一种方式是使用 RGB 值,即红色、绿色和蓝色值。每个值都介于 0 和 255 之间,表示这三种颜色如何混合以创建所需的颜色。例如,如果我们将 RGB 表示为(红色,绿色,蓝色),那么深橙色的 RGB 值为(255,140,0),粉色为(255,192,203)。黑色为(0,0,0),白色为(255,255,255)。

从机器学习和回归的角度来看,我们有三个数值输入变量redgreenblue来捕捉给定背景颜色。我们需要对这些输入变量拟合一个函数,并输出是否应该为该背景颜色使用浅色(1)或深色(0)字体。

通过 RGB 表示颜色

在线有数百种颜色选择器调色板可供尝试 RGB 值。W3 Schools 有一个这里

请注意,这个例子与神经网络识别图像的工作原理并不相去甚远,因为每个像素通常被建模为三个数值 RGB 值。在这种情况下,我们只关注一个“像素”作为背景颜色。

让我们从高层次开始,把所有的实现细节放在一边。我们将以洋葱的方式来处理这个主题,从更高的理解开始,然后慢慢剥离细节。目前,这就是为什么我们简单地将一个接受输入并产生输出的过程标记为“神秘数学”。我们有三个数值输入变量 R、G 和 B,这些变量被这个神秘的数学处理。然后它输出一个介于 0 和 1 之间的预测,如图 7-2 所示。

emds 0702

图 7-2。我们有三个数值 RGB 值用于预测使用浅色或深色字体

这个预测输出表示一个概率。输出概率是使用神经网络进行分类的最常见模型。一旦我们用它们的数值替换 RGB,我们会发现小于 0.5 会建议使用深色字体,而大于 0.5 会建议使用浅色字体,如图 7-3 所示。

emds 0703

图 7-3。如果我们输入一个粉色的背景色(255,192,203),那么神秘的数学会推荐使用浅色字体,因为输出概率 0.89 大于 0.5

那个神秘的数学黑匣子里到底发生了什么?让我们在图 7-4 中看一看。

我们还缺少神经网络的另一个部分,即激活函数,但我们很快会讨论到。让我们先了解这里发生了什么。左侧的第一层只是三个变量的输入,这些变量在这种情况下是红色、绿色和蓝色值。在隐藏(中间)层中,请注意我们在输入和输出之间产生了三个节点,或者说是权重和偏置的函数。每个节点本质上是一个线性函数,斜率为W i,截距为B i,与输入变量X i相乘并求和。每个输入节点和隐藏节点之间有一个权重W i,每个隐藏节点和输出节点之间有另一组权重。每个隐藏和输出节点都会额外添加一个偏置B i。

emds 0704

图 7-4。神经网络的隐藏层对每个输入变量应用权重和偏置值,输出层对该输出应用另一组权重和偏置

注意,输出节点重复执行相同的操作,将隐藏层的加权和求和输出作为输入传递到最终层,其中另一组权重和偏置将被应用。

简而言之,这是一个回归问题,就像线性回归或逻辑回归一样,但需要解决更多参数。权重和偏置值类似于mb,或者线性回归中的β 1和β 0参数。我们使用随机梯度下降和最小化损失,就像线性回归一样,但我们需要一种称为反向传播的额外工具来解开权重W i和偏置B i值,并使用链式法则计算它们的偏导数。我们将在本章后面详细讨论这一点,但现在让我们假设我们已经优化了权重和偏置值。我们需要先讨论激活函数。

激活函数

接下来让我们介绍激活函数。激活函数是一个非线性函数,它转换或压缩节点中的加权和值,帮助神经网络有效地分离数据,以便进行分类。让我们看一下图 7-5。如果没有激活函数,你的隐藏层将毫无生产力,表现不会比线性回归好。

emds 0705

图 7-5. 应用激活函数

ReLU 激活函数将隐藏节点的任何负输出归零。如果权重、偏置和输入相乘并求和得到负数,它将被转换为 0。否则输出保持不变。这是使用 SymPy (示例 7-1) 绘制的 ReLU 图(图 7-6)。

示例 7-1. 绘制 ReLU 函数
from sympy import *

# plot relu
x = symbols('x')
relu = Max(0, x)
plot(relu)

emds 0706

图 7-6. ReLU 函数图

ReLU 是“修正线性单元”的缩写,但这只是一种将负值转换为 0 的花哨方式。ReLU 在神经网络和深度学习中的隐藏层中变得流行,因为它速度快,并且缓解了梯度消失问题。梯度消失发生在偏导数斜率变得非常小,导致过早接近 0 并使训练停滞。

输出层有一个重要的任务:它接收来自神经网络隐藏层的大量数学,并将其转换为可解释的结果,例如呈现分类预测。对于这个特定的神经网络,输出层使用逻辑激活函数,这是一个简单的 S 形曲线。如果您阅读第六章,逻辑(或 S 形)函数应该很熟悉,它表明逻辑回归在我们的神经网络中充当一层。输出节点的权重、偏置和从隐藏层传入的每个值求和。之后,它通过逻辑函数传递结果值,以便输出介于 0 和 1 之间的数字。就像第六章中的逻辑回归一样,这代表了输入到神经网络的给定颜色建议使用浅色字体的概率。如果大于或等于 0.5,则神经网络建议使用浅色字体,否则建议使用深色字体。

这是使用 SymPy (示例 7-2) 绘制的逻辑函数图(图 7-7)。

示例 7-2. SymPy 中的逻辑激活函数
from sympy import *

# plot logistic
x = symbols('x')
logistic = 1 / (1 + exp(-x))
plot(logistic)

emds 0707

图 7-7. 逻辑激活函数

当我们通过激活函数传递节点的加权、偏置和求和值时,我们现在称之为激活输出,意味着它已通过激活函数进行了过滤。当激活输出离开隐藏层时,信号准备好被馈送到下一层。激活函数可能会增强、减弱或保持信号不变。这就是神经网络中大脑和突触的比喻的来源。

鉴于复杂性的潜在可能性,您可能想知道是否还有其他激活函数。一些常见的激活函数显示在表 7-1 中。

表 7-1. 常见激活函数

名称典型使用层描述注释
线性输出保持值不变不常用
Logistic输出层S 形 sigmoid 曲线将值压缩在 0 和 1 之间,通常用于二元分类
双曲正切隐藏层tanh,在 -1 和 1 之间的 S 形 sigmoid 曲线通过将均值接近 0 来“居中”数据
ReLU隐藏层将负值转换为 0比 sigmoid 和 tanh 更快的流行激活函数,缓解消失梯度问题,计算成本低廉
Leaky ReLU隐藏层将负值乘以 0.01ReLU 的有争议变体,边缘化而不是消除负值
Softmax输出层确保所有输出节点加起来为 1.0适用于多类别分类,重新缩放输出使其加起来为 1.0

这不是激活函数的全面列表,理论上神经网络中任何函数都可以是激活函数。

尽管这个神经网络表面上支持两类(浅色或深色字体),但实际上它被建模为一类:字体是否应该是浅色(1)或不是(0)。如果您想支持多个类别,可以为每个类别添加更多输出节点。例如,如果您试图识别手写数字 0-9,将有 10 个输出节点,代表给定图像是这些数字的概率。当有多个类别时,您可能还考虑在输出时使用 softmax 激活。图 7-8 展示了一个以像素化图像为输入的示例,其中像素被分解为单独的神经网络输入,然后通过两个中间层,最后一个输出层,有 10 个节点代表 10 个类别的概率(数字 0-9)。

emds 0708

图 7-8. 一个神经网络,将每个像素作为输入,并预测图像包含的数字

在神经网络上使用 MNIST 数据集的示例可以在 附录 A 中找到。

我不知道要使用什么激活函数!

如果不确定要使用哪些激活函数,当前最佳实践倾向于在中间层使用 ReLU,在输出层使用 logistic(sigmoid)。如果输出中有多个分类,可以在输出层使用 softmax。

前向传播

让我们使用 NumPy 捕获到目前为止学到的知识。请注意,我尚未优化参数(我们的权重和偏置值)。我们将用随机值初始化它们。

示例 7-3 是创建一个简单的前馈神经网络的 Python 代码,尚未进行优化。前馈 意味着我们只是将一种颜色输入到神经网络中,看看它输出什么。权重和偏置是随机初始化的,并将在本章后面进行优化,因此暂时不要期望有用的输出。

示例 7-3. 一个具有随机权重和偏置值的简单前向传播网络
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split

all_data = pd.read_csv("https://tinyurl.com/y2qmhfsr")

# Extract the input columns, scale down by 255
all_inputs = (all_data.iloc[:, 0:3].values / 255.0)
all_outputs = all_data.iloc[:, -1].values

# Split train and test data sets
X_train, X_test, Y_train, Y_test = train_test_split(all_inputs, all_outputs,
    test_size=1/3)
n = X_train.shape[0] # number of training records

# Build neural network with weights and biases
# with random initialization
w_hidden = np.random.rand(3, 3)
w_output = np.random.rand(1, 3)

b_hidden = np.random.rand(3, 1)
b_output = np.random.rand(1, 1)

# Activation functions
relu = lambda x: np.maximum(x, 0)
logistic = lambda x: 1 / (1 + np.exp(-x))

# Runs inputs through the neural network to get predicted outputs
def forward_prop(X):
    Z1 = w_hidden @ X + b_hidden
    A1 = relu(Z1)
    Z2 = w_output @ A1 + b_output
    A2 = logistic(Z2)
    return Z1, A1, Z2, A2

# Calculate accuracy
test_predictions = forward_prop(X_test.transpose())[3] # grab only output layer, A2
test_comparisons = np.equal((test_predictions >= .5).flatten().astype(int), Y_test)
accuracy = sum(test_comparisons.astype(int) / X_test.shape[0])
print("ACCURACY: ", accuracy)

这里有几点需要注意。 包含 RGB 输入值以及输出值(1 代表亮,0 代表暗)的数据集包含在此 CSV 文件中。 我将输入列 R、G 和 B 的值缩小了 1/255 的因子,使它们介于 0 和 1 之间。 这将有助于后续的训练,以便压缩数字空间。

请注意,我还使用 scikit-learn 将数据的 2/3 用于训练,1/3 用于测试,我们在第五章中学习了如何做到这一点。 n 简单地是训练数据记录的数量。

现在请注意示例 7-4 中显示的代码行。

示例 7-4。 NumPy 中的权重矩阵和偏置向量
# Build neural network with weights and biases
# with random initialization
w_hidden = np.random.rand(3, 3)
w_output = np.random.rand(1, 3)

b_hidden = np.random.rand(3, 1)
b_output = np.random.rand(1, 1)

这些声明了我们神经网络隐藏层和输出层的权重和偏置。 这可能还不明显,但矩阵乘法将使我们的代码变得强大简单,使用线性代数和 NumPy。

权重和偏置将被初始化为介于 0 和 1 之间的随机值。 让我们首先看一下权重矩阵。 当我运行代码时,我得到了这些矩阵:

W hidden = 0.034535 0.5185636 0.81485028 0.3329199 0.53873853 0.96359003 0.19808306 0.45422182 0.36618893W output = 0.82652072 0.30781539 0.93095565

请注意,W hidden 是隐藏层中的权重。第一行代表第一个节点的权重 W 1,W 2 和 W 3。第二行是第二个节点,带有权重 W 4,W 5 和 W 6。第三行是第三个节点,带有权重 W 7,W 8 和 W 9。

输出层只有一个节点,意味着其矩阵只有一行,带有权重 W 10,W 11 和 W 12。

看到规律了吗?每个节点在矩阵中表示为一行。如果有三个节点,就有三行。如果只有一个节点,就只有一行。每一列保存着该节点的权重值。

让我们也看看偏差。由于每个节点有一个偏差,隐藏层将有三行偏差,输出层将有一行偏差。每个节点只有一个偏差,所以只有一列:

B hidden = 0.41379442 0.81666079 0.07511252B output = 0.58018555

现在让我们将这些矩阵值与我们在 图 7-9 中展示的神经网络进行比较。

emds 0709

图 7-9。将我们的神经网络与权重和偏差矩阵值进行可视化

那么除了紧凑难懂之外,这种矩阵形式中的权重和偏差有什么好处呢?让我们把注意力转向 示例 7-5 中的这些代码行。

示例 7-5。我们神经网络的激活函数和前向传播函数
# Activation functions
relu = lambda x: np.maximum(x, 0)
logistic = lambda x: 1 / (1 + np.exp(-x))

# Runs inputs through the neural network to get predicted outputs
def forward_prop(X):
    Z1 = w_hidden @ X + b_hidden
    A1 = relu(Z1)
    Z2 = w_output @ A1 + b_output
    A2 = logistic(Z2)
    return Z1, A1, Z2, A2

这段代码很重要,因为它使用矩阵乘法和矩阵-向量乘法简洁地执行我们整个神经网络。我们在第四章中学习了这些操作。它仅用几行代码将三个 RGB 输入的颜色通过权重、偏置和激活函数运行。

首先,我声明relu()logistic()激活函数,它们分别接受给定的输入值并返回曲线的输出值。forward_prop()函数为包含 R、G 和 B 值的给定颜色输入X执行整个神经网络。它返回四个阶段的矩阵输出:Z1A1Z2A2。数字“1”和“2”表示操作属于第 1 层和第 2 层。“Z”表示来自该层的未激活输出,“A”是来自该层的激活输出。

隐藏层由Z1A1表示。Z1是应用于X的权重和偏置。然后A1获取来自Z1的输出,并通过激活 ReLU 函数。Z2获取来自A1的输出,并应用输出层的权重和偏置。该输出依次通过激活函数,即逻辑曲线,变为A2。最终阶段,A2,是输出层的预测概率,返回一个介于 0 和 1 之间的值。我们称之为A2,因为它是来自第 2 层的“激活”输出。

让我们更详细地分解这个,从Z1开始:

Z 1 = W hidden X + B hidden

首先,我们在W hidden和输入颜色X之间执行矩阵-向量乘法。我们将W hidden的每一行(每一行都是一个节点的权重集)与向量X(RGB 颜色输入值)相乘。然后将偏置添加到该结果中,如图 7-10 所示。

emds 0710

图 7-10。将隐藏层权重和偏置应用于输入X,使用矩阵-向量乘法以及向量加法

那个Z 1向量是隐藏层的原始输出,但我们仍然需要通过激活函数将Z 1转换为A 1。很简单。只需将该向量中的每个值通过 ReLU 函数传递,就会给我们A 1。因为所有值都是正值,所以不应该有影响。

A 1 = R e L U ( Z 1 )A 1 = R e L U ( 1.36054964190909 ) R e L U ( 2.15471757888247 ) R e L U ( 0.719554393391768 ) = 1.36054964190909 2.15471757888247 0.719554393391768

现在让我们将隐藏层输出A 1传递到最终层,得到Z 2,然后A 2。 A 1 成为输出层的输入。

Z 2 = W output A 1 + B outputZ 2 = 0.82652072 0.3078159 0.93095565 1.36054964190909 2.15471757888247 0.719554393391768 + 0.58018555Z 2 = 2.45765202842636 + 0.58018555Z 2 = 3.03783757842636

最后,将这个单个值Z 2通过激活函数传递,得到A 2。这将产生一个约为 0.95425 的预测:

A 2 = l o g i s t i c ( Z 2 ) A 2 = l o g i s t i c ( 3.0378364795204 ) A 2 = 0.954254478103241

执行我们整个神经网络,尽管我们尚未对其进行训练。但请花点时间欣赏,我们已经将所有这些输入值、权重、偏差和非线性函数转化为一个将提供预测的单个值。

再次强调,A2是最终输出,用于预测背景颜色是否需要浅色(1)或深色(1)字体。尽管我们的权重和偏差尚未优化,让我们按照示例 7-6 计算准确率。取测试数据集X_test,转置它,并通过forward_prop()函数传递,但只获取A2向量,其中包含每个测试颜色的预测。然后将预测与实际值进行比较,并计算正确预测的百分比。

示例 7-6. 计算准确率
# Calculate accuracy
test_predictions = forward_prop(X_test.transpose())[3]  # grab only A2
test_comparisons = np.equal((test_predictions >= .5).flatten().astype(int), Y_test)
accuracy = sum(test_comparisons.astype(int) / X_test.shape[0])
print("ACCURACY: ", accuracy)

当我运行示例 7-3 中的整个代码时,我大致获得 55%到 67%的准确率。请记住,权重和偏差是随机生成的,因此答案会有所不同。虽然这个准确率似乎很高,考虑到参数是随机生成的,但请记住,输出预测是二元的:浅色或深色。因此,对于每个预测,随机抛硬币也可能产生这种结果,所以这个数字不应令人惊讶。

不要忘记检查是否存在数据不平衡!

正如在第六章中讨论的那样,不要忘记分析数据以检查是否存在不平衡的类别。整个背景颜色数据集有点不平衡:512 种颜色的输出为 0,833 种颜色的输出为 1。这可能会使准确率产生偏差,也可能是我们的随机权重和偏差倾向于高于 50%准确率的原因。如果数据极度不平衡(例如 99%的数据属于一类),请记住使用混淆矩阵来跟踪假阳性和假阴性。

到目前为止,结构上一切都合理吗?在继续之前,随时可以回顾一切。我们只剩下最后一个部分要讲解:优化权重和偏差。去冲杯浓缩咖啡或氮气咖啡吧,因为这是本书中我们将要做的最复杂的数学!

反向传播

在我们开始使用随机梯度下降来优化我们的神经网络之前,我们面临的挑战是如何相应地改变每个权重和偏差值,尽管它们都纠缠在一起以创建输出变量,然后用于计算残差。我们如何找到每个权重W i 和偏差B i 变量的导数?我们需要使用我们在第一章中讨论过的链式法则。

计算权重和偏差的导数

我们还没有准备好应用随机梯度下降来训练我们的神经网络。我们需要求出相对于权重W i 和偏差B i 的偏导数,而且我们有链式法则来帮助我们。

虽然过程基本相同,但在神经网络上使用随机梯度下降存在一个复杂性。一层中的节点将它们的权重和偏置传递到下一层,然后应用另一组权重和偏置。这创建了一个类似洋葱的嵌套结构,我们需要从输出层开始解开。

在梯度下降过程中,我们需要找出应该调整哪些权重和偏置,以及调整多少,以减少整体成本函数。单个预测的成本将是神经网络的平方输出A 2减去实际值Y:

C = (A 2 -Y) 2

但让我们再往下一层。那个激活输出A 2只是带有激活函数的Z 2:

A 2 = s i g m o i d ( Z 2 )

转而,Z 2是应用于激活输出A 1的输出权重和偏置,这些输出来自隐藏层:

Z 2 = W 2 A 1 + B 2

A 1是由通过 ReLU 激活函数传递的Z 1构建而成:

A 1 = R e L U ( Z 1 )

最后,Z 1是由隐藏层加权和偏置的输入 x 值:

Z 1 = W 1 X + B 1

我们需要找到包含在矩阵和向量W 1,B 1,W 2和B 2中的权重和偏置,以最小化我们的损失。通过微调它们的斜率,我们可以改变对最小化损失影响最大的权重和偏置。然而,对权重或偏置进行微小调整会一直传播到外层的损失函数。这就是链式法则可以帮助我们找出这种影响的地方。

让我们专注于找到输出层权重W 2和成本函数C之间的关系。权重W 2的变化导致未激活的输出Z 2的变化。然后改变激活输出A 2,进而改变成本函数C。利用链式法则,我们可以定义关于W 2的导数如下:

dC dW 2 = dZ 2 dW 2 dA 2 dZ 2 dC dA 2

当我们将这三个梯度相乘在一起时,我们得到了改变W 2将如何改变成本函数C的度量。

现在我们将计算这三个导数。让我们使用 SymPy 计算在示例 7-7 中关于A 2的成本函数的导数。

dC dA 2 = 2 A 2 - 2 y

示例 7-7. 计算成本函数关于A 2的导数
from sympy import *

A2, y = symbols('A2 Y')
C = (A2 - Y)**2
dC_dA2 = diff(C, A2)
print(dC_dA2) # 2*A2 - 2*Y

接下来,让我们找到关于Z 2的导数与A 2的导数(示例 7-8)。记住A 2是激活函数的输出,在这种情况下是逻辑函数。因此,我们实际上只是在求取 S 形曲线的导数。

dA 2 dZ 2 = e -Z 2 1+e -Z 2 2

示例 7-8. 找到关于Z 2的导数与A 2
from sympy import *

Z2 = symbols('Z2')

logistic = lambda x: 1 / (1 + exp(-x))

A2 = logistic(Z2)
dA2_dZ2 = diff(A2, Z2)
print(dA2_dZ2) # exp(-Z2)/(1 + exp(-Z2))**2

Z 2关于W 2的导数将会得到A 1,因为它只是一个线性函数,将返回斜率(示例 7-9)。

dZ 2 dW 1 = A 1

示例 7-9. 关于W 2的导数Z 2
from sympy import *

A1, W2, B2 = symbols('A1, W2, B2')

Z2 = A1*W2 + B2
dZ2_dW2 = diff(Z2, W2)
print(dZ2_dW2) # A1

将所有内容整合在一起,这里是找到改变W 2中的权重会如何影响成本函数C的导数:

dC dw 2 = dZ 2 dw 2 dA 2 dZ 2 dC dA 2 = ( A 1 ) ( e -Z 2 1+e -Z 2 2 ) ( 2 A 2 - 2 y )

当我们运行一个输入X与三个输入 R、G 和 B 值时,我们将得到A 1、A 2、Z 2和y的值。

不要在数学中迷失!

在这一点很容易在数学中迷失并忘记你最初想要实现的目标,即找到成本函数关于输出层中的权重(W 2)的导数。当你发现自己陷入困境并忘记你要做什么时,那就退一步,出去走走,喝杯咖啡,提醒自己你要达成的目标。如果你做不到,你应该从头开始,一步步地找回迷失的地方。

然而,这只是神经网络的一个组成部分,对于W 2的导数。以下是我们在示例 7-10 中使用 SymPy 计算的其余部分偏导数,这些是我们在链式求导中需要的。

示例 7-10。计算我们神经网络所需的所有偏导数
from sympy import *

W1, W2, B1, B2, A1, A2, Z1, Z2, X, Y = \
    symbols('W1 W2 B1 B2 A1 A2 Z1 Z2 X Y')

# Calculate derivative of cost function with respect to A2
C = (A2 - Y)**2
dC_dA2 = diff(C, A2)
print("dC_dA2 = ", dC_dA2) # 2*A2 - 2*Y

# Calculate derivative of A2 with respect to Z2
logistic = lambda x: 1 / (1 + exp(-x))
_A2 = logistic(Z2)
dA2_dZ2 = diff(_A2, Z2)
print("dA2_dZ2 = ", dA2_dZ2) # exp(-Z2)/(1 + exp(-Z2))**2

# Calculate derivative of Z2 with respect to A1
_Z2 = A1*W2 + B2
dZ2_dA1 = diff(_Z2, A1)
print("dZ2_dA1 = ", dZ2_dA1) # W2

# Calculate derivative of Z2 with respect to W2
dZ2_dW2 = diff(_Z2, W2)
print("dZ2_dW2 = ", dZ2_dW2) # A1

# Calculate derivative of Z2 with respect to B2
dZ2_dB2 = diff(_Z2, B2)
print("dZ2_dB2 = ", dZ2_dB2) # 1

# Calculate derivative of A1 with respect to Z1
relu = lambda x: Max(x, 0)
_A1 = relu(Z1)

d_relu = lambda x: x > 0 # Slope is 1 if positive, 0 if negative
dA1_dZ1 = d_relu(Z1)
print("dA1_dZ1 = ", dA1_dZ1) # Z1 > 0

# Calculate derivative of Z1 with respect to W1
_Z1 = X*W1 + B1
dZ1_dW1 = diff(_Z1, W1)
print("dZ1_dW1 = ", dZ1_dW1) # X

# Calculate derivative of Z1 with respect to B1
dZ1_dB1 = diff(_Z1, B1)
print("dZ1_dB1 = ", dZ1_dB1) # 1

注意,ReLU 是手动计算的,而不是使用 SymPy 的diff()函数。这是因为导数适用于平滑曲线,而不是 ReLU 上存在的锯齿角。但通过简单地声明斜率为正数为 1,负数为 0,可以轻松解决这个问题。这是有道理的,因为负数具有斜率为 0 的平坦线。但正数保持不变,具有 1:1 的斜率。

这些偏导数可以链接在一起,以创建相对于权重和偏置的新偏导数。让我们为W 1、W 2、B 1和B 2相对于成本函数的四个偏导数。我们已经讨论了dC dw 2。让我们将其与其他三个链式求导一起展示:

dC dW 2 = dZ 2 dW 2 dA 2 dZ 2 dC dA 2 = ( A 1 ) ( e -Z 2 1+e -Z 2 2 ) ( 2 A 2 - 2 y )dC dB 2 = dZ 2 dB 2 dA 2 dZ 2 dC dA 2 = ( 1 ) ( e -Z 2 1+e -Z 2 2 ) ( 2 A 2 - 2 y )dC dW 1 = dC DA 2 DA 2 dZ 2 dZ 2 dA 1 dA 1 dZ 1 dZ 1 dW 1 = ( 2 A 2 - 2 y ) ( e -Z 2 1+e -Z 2</mn

我们将使用这些链式梯度来计算成本函数C相对于W 1,B 1,W 2和B 2的斜率。

随机梯度下降

现在我们准备整合链式法则来执行随机梯度下降。为了保持简单,我们每次迭代只对一个训练记录进行采样。在神经网络和深度学习中通常使用批量梯度下降和小批量梯度下降,但是在每次迭代中只处理一个样本就足够了,因为其中涉及足够的线性代数和微积分。

让我们来看看我们的神经网络的完整实现,使用反向传播的随机梯度下降,在示例 7-11 中。

示例 7-11. 使用随机梯度下降实现神经网络
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split

all_data = pd.read_csv("https://tinyurl.com/y2qmhfsr")

# Learning rate controls how slowly we approach a solution
# Make it too small, it will take too long to run.
# Make it too big, it will likely overshoot and miss the solution.
L = 0.05

# Extract the input columns, scale down by 255
all_inputs = (all_data.iloc[:, 0:3].values / 255.0)
all_outputs = all_data.iloc[:, -1].values

# Split train and test data sets
X_train, X_test, Y_train, Y_test = train_test_split(all_inputs, all_outputs,
    test_size=1 / 3)
n = X_train.shape[0]

# Build neural network with weights and biases
# with random initialization
w_hidden = np.random.rand(3, 3)
w_output = np.random.rand(1, 3)

b_hidden = np.random.rand(3, 1)
b_output = np.random.rand(1, 1)

# Activation functions
relu = lambda x: np.maximum(x, 0)
logistic = lambda x: 1 / (1 + np.exp(-x))

# Runs inputs through the neural network to get predicted outputs
def forward_prop(X):
    Z1 = w_hidden @ X + b_hidden
    A1 = relu(Z1)
    Z2 = w_output @ A1 + b_output
    A2 = logistic(Z2)
    return Z1, A1, Z2, A2

# Derivatives of Activation functions
d_relu = lambda x: x > 0
d_logistic = lambda x: np.exp(-x) / (1 + np.exp(-x)) ** 2

# returns slopes for weights and biases
# using chain rule
def backward_prop(Z1, A1, Z2, A2, X, Y):
    dC_dA2 = 2 * A2 - 2 * Y
    dA2_dZ2 = d_logistic(Z2)
    dZ2_dA1 = w_output
    dZ2_dW2 = A1
    dZ2_dB2 = 1
    dA1_dZ1 = d_relu(Z1)
    dZ1_dW1 = X
    dZ1_dB1 = 1

    dC_dW2 = dC_dA2 @ dA2_dZ2 @ dZ2_dW2.T

    dC_dB2 = dC_dA2 @ dA2_dZ2 * dZ2_dB2

    dC_dA1 = dC_dA2 @ dA2_dZ2 @ dZ2_dA1

    dC_dW1 = dC_dA1 @ dA1_dZ1 @ dZ1_dW1.T

    dC_dB1 = dC_dA1 @ dA1_dZ1 * dZ1_dB1

    return dC_dW1, dC_dB1, dC_dW2, dC_dB2

# Execute gradient descent
for i in range(100_000):
    # randomly select one of the training data
    idx = np.random.choice(n, 1, replace=False)
    X_sample = X_train[idx].transpose()
    Y_sample = Y_train[idx]

    # run randomly selected training data through neural network
    Z1, A1, Z2, A2 = forward_prop(X_sample)

    # distribute error through backpropagation
    # and return slopes for weights and biases
    dW1, dB1, dW2, dB2 = backward_prop(Z1, A1, Z2, A2, X_sample, Y_sample)

    # update weights and biases
    w_hidden -= L * dW1
    b_hidden -= L * dB1
    w_output -= L * dW2
    b_output -= L * dB2

# Calculate accuracy
test_predictions = forward_prop(X_test.transpose())[3]  # grab only A2
test_comparisons = np.equal((test_predictions >= .5).flatten().astype(int), Y_test)
accuracy = sum(test_comparisons.astype(int) / X_test.shape[0])
print("ACCURACY: ", accuracy)

这里涉及很多内容,但是建立在我们在本章学到的一切基础之上。我们进行了 10 万次随机梯度下降迭代。将训练和测试数据分别按 2/3 和 1/3 划分,根据随机性的不同,我在测试数据集中获得了大约 97-99%的准确率。这意味着训练后,我的神经网络能够正确识别 97-99%的测试数据,并做出正确的浅色/深色字体预测。

backward_prop()函数在这里起着关键作用,实现链式法则,将输出节点的误差(平方残差)分配并向后传播到输出和隐藏权重/偏差,以获得相对于每个权重/偏差的斜率。然后我们在for循环中使用这些斜率,分别通过乘以学习率L来微调权重/偏差,就像我们在第五章和第六章中所做的那样。我们进行一些矩阵-向量乘法,根据斜率向后传播误差,并在需要时转置矩阵和向量,以使行和列之间的维度匹配起来。

如果你想让神经网络更具交互性,这里有一段代码片段在示例 7-12 中,我们可以输入不同的背景颜色(通过 R、G 和 B 值),看看它是否预测为浅色或深色字体。将其附加到之前的代码示例 7-11 的底部,然后试一试!

示例 7-12. 为我们的神经网络添加一个交互式 shell
# Interact and test with new colors
def predict_probability(r, g, b):
    X = np.array([[r, g, b]]).transpose() / 255
    Z1, A1, Z2, A2 = forward_prop(X)
    return A2

def predict_font_shade(r, g, b):
    output_values = predict_probability(r, g, b)
    if output_values > .5:
        return "DARK"
    else:
        return "LIGHT"

while True:
    col_input = input("Predict light or dark font. Input values R,G,B: ")
    (r, g, b) = col_input.split(",")
    print(predict_font_shade(int(r), int(g), int(b)))

从头开始构建自己的神经网络需要大量的工作和数学知识,但它让你深入了解它们的真实本质。通过逐层工作、微积分和线性代数,我们对像 PyTorch 和 TensorFlow 这样的深度学习库在幕后做了什么有了更深刻的理解。

从阅读本整章节中你已经了解到,要使神经网络正常运转有很多要素。在代码的不同部分设置断点可以帮助你了解每个矩阵操作在做什么。你也可以将代码移植到 Jupyter Notebook 中,以获得更多对每个步骤的视觉洞察。

3Blue1Brown 关于反向传播的视频

3Blue1Brown 有一些经典视频讨论反向传播和神经网络背后的微积分

使用 scikit-learn

scikit-learn 中有一些有限的神经网络功能。如果你对深度学习感兴趣,你可能会想学习 PyTorch 或 TensorFlow,并购买一台配备强大 GPU 的计算机(这是购买你一直想要的游戏电脑的绝佳借口!)。我被告知现在所有酷炫的孩子都在使用 PyTorch。然而,scikit-learn 中确实有一些方便的模型可用,包括MLPClassifier,它代表“多层感知器分类器”。这是一个用于分类的神经网络,默认使用逻辑输出激活。

示例 7-13 是我们开发的背景颜色分类应用的 scikit-learn 版本。activation参数指定了隐藏层。

示例 7-13. 使用 scikit-learn 神经网络分类器
import pandas as pd
# load data
from sklearn.model_selection import train_test_split
from sklearn.neural_network import MLPClassifier

df = pd.read_csv('https://bit.ly/3GsNzGt', delimiter=",")

# Extract input variables (all rows, all columns but last column)
# Note we should do some linear scaling here
X = (df.values[:, :-1] / 255.0)

# Extract output column (all rows, last column)
Y = df.values[:, -1]

# Separate training and testing data
X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=1/3)

nn = MLPClassifier(solver='sgd',
                   hidden_layer_sizes=(3, ),
                   activation='relu',
                   max_iter=100_000,
                   learning_rate_init=.05)

nn.fit(X_train, Y_train)

# Print weights and biases
print(nn.coefs_ )
print(nn.intercepts_)

print("Training set score: %f" % nn.score(X_train, Y_train))
print("Test set score: %f" % nn.score(X_test, Y_test))

运行这段代码,我在测试数据上获得了约 99.3% 的准确率。

使用 scikit-learn 的 MNIST 示例

要查看一个使用 MNIST 数据集预测手写数字的 scikit-learn 示例,请参阅附录 A。

神经网络和深度学习的局限性

尽管神经网络具有许多优势,但在某些类型的任务上仍然存在困难。这种对层、节点和激活函数的灵活性使其能够以非线性方式拟合数据...可能太灵活了。为什么?它可能对数据过拟合。深度学习教育的先驱、谷歌大脑前负责人安德鲁·吴在 2021 年的一次新闻发布会上提到这是一个问题。在被问及为什么机器学习尚未取代放射科医生时,这是他在IEEE Spectrum文章中的回答:

结果表明,当我们从斯坦福医院收集数据,然后在同一家医院的数据上进行训练和测试时,确实可以发表论文,显示[算法]在发现某些病况方面与人类放射科医生相媲美。

结果表明,当你将同样的模型,同样的人工智能系统,带到街对面的一家老医院,使用一台老机器,技术人员使用稍有不同的成像协议时,数据漂移会导致人工智能系统的性能显著下降。相比之下,任何一名人类放射科医生都可以走到街对面的老医院并做得很好。

因此,即使在某个特定数据集上的某个时间点上,我们可以展示这是有效的,临床现实是这些模型仍然需要大量工作才能投入生产。

换句话说,机器学习过度拟合了斯坦福医院的训练和测试数据集。当应用到其他设备不同的医院时,由于过度拟合,性能显著下降。

自动驾驶汽车和无人驾驶汽车也面临相同的挑战。仅仅在一个停车标志上训练神经网络是不够的!它必须在围绕该停车标志的无数条件下进行训练:晴天、雨天、夜晚和白天、有涂鸦、被树挡住、在不同的地点等等。在交通场景中,想象所有不同类型的车辆、行人、穿着服装的行人以及将遇到的无限数量的边缘情况!简单地说,通过在神经网络中增加更多的权重和偏差来捕捉在道路上遇到的每种事件是没有效果的。

这就是为什么自动驾驶汽车本身不会以端到端的方式使用神经网络。相反,不同的软件和传感器模块被分解,其中一个模块可能使用神经网络在物体周围画一个框。然后另一个模块将使用不同的神经网络对该框中的物体进行分类,比如行人。从那里,传统的基于规则的逻辑将尝试预测行人的路径,硬编码逻辑将从不同的条件中选择如何反应。机器学习仅限于标记制作活动,而不涉及车辆的战术和机动。此外,像雷达这样的基本传感器在车辆前方检测到未知物体时将会停止,这只是技术堆栈中另一个不使用机器学习或深度学习的部分。

尽管媒体头条报道神经网络和深度学习在诸如国际象棋和围棋等游戏中击败人类,甚至胜过战斗飞行模拟中的飞行员,这可能令人惊讶。在这样的强化学习环境中,需要记住模拟是封闭的世界,在这里可以生成无限量的标记数据,并通过虚拟有限世界进行学习。然而,现实世界并非我们可以生成无限量数据的模拟环境。此外,这不是一本哲学书,所以我们将不讨论我们是否生活在模拟中。抱歉,埃隆!在现实世界中收集数据是昂贵且困难的。除此之外,现实世界充满了无限的不可预测性和罕见事件。所有这些因素驱使机器学习从业者转向数据录入劳动来标记交通物体的图片和其他数据。自动驾驶汽车初创公司通常必须将这种数据录入工作与模拟数据配对,因为需要生成训练数据的里程数和边缘情况场景太过庞大,无法简单地通过驾驶数百万英里的车队来收集。

这些都是人工智能研究喜欢使用棋盘游戏和视频游戏的原因,因为可以轻松干净地生成无限标记数据。谷歌的著名工程师弗朗西斯·乔勒特(Francis Chollet)为 TensorFlow 开发了 Keras(还写了一本很棒的书,Python 深度学习),在一篇Verge文章中分享了一些见解:

问题在于,一旦你选择了一个度量标准,你就会采取任何可用的捷径来操纵它。例如,如果你将下棋作为智能的度量标准(我们从上世纪 70 年代开始一直持续到 90 年代),你最终会得到一个只会下棋的系统,仅此而已。没有理由认为它对其他任何事情都有好处。你最终会得到树搜索和极小化,这并不能教会你任何关于人类智能的东西。如今,将追求在像 Dota 或 StarCraft 这样的视频游戏中的技能作为一种普遍智能的替代品,陷入了完全相同的智力陷阱...

如果我着手使用深度学习以超人水平“解决”《魔兽争霸 III》,只要我有足够的工程人才和计算能力(这类任务需要数千万美元的资金),你可以确信我会成功。但一旦我做到了,我会学到关于智能或泛化的什么?嗯,什么也没有。充其量,我会开发关于扩展深度学习的工程知识。所以我并不认为这是科学研究,因为它并没有教会我们任何我们不已经知道的东西。它没有回答任何未解之谜。如果问题是,“我们能以超人水平玩 X 吗?”,答案肯定是,“是的,只要你能生成足够密集的训练情境样本,并将它们输入到一个足够表达力的深度学习模型中。” 这一点我们已经知道有一段时间了。

换句话说,我们必须小心,不要混淆算法在游戏中的表现与尚未解决的更广泛能力。机器学习、神经网络和深度学习都是狭义地解决定义明确的问题。它们不能广泛推理或选择自己的任务,也不能思考之前未见过的对象。就像任何编码应用程序一样,它们只会执行它们被编程要执行的任务。

无论使用什么工具,解决问题是最重要的。不应该偏袒神经网络或其他任何可用工具。在这一点上,使用神经网络可能不是你面临的任务的最佳选择。重要的是要始终考虑你正在努力解决的问题,而不是把特定工具作为主要目标。深度学习的使用必须是策略性的和有充分理由的。当然有使用案例,但在你的日常工作中,你可能会更成功地使用简单和更偏向的模型,如线性回归、逻辑回归或传统的基于规则的系统。但如果你发现自己需要对图像中的对象进行分类,并且有预算和人力来构建数据集,那么深度学习将是你最好的选择。

结论

神经网络和深度学习提供了一些令人兴奋的应用,我们在这一章中只是触及了表面。从识别图像到处理自然语言,继续应用神经网络及其不同类型的深度学习仍然有用武之地。

从零开始,我们学习了如何构建一个具有一个隐藏层的简单神经网络,以预测在背景颜色下是否应该使用浅色或深色字体。我们还应用了一些高级微积分概念来计算嵌套函数的偏导数,并将其应用于随机梯度下降来训练我们的神经网络。我们还涉及到了像 scikit-learn 这样的库。虽然在本书中我们没有足够的篇幅来讨论 TensorFlow、PyTorch 和更高级的应用,但有很多优秀的资源可以扩展你的知识。

3Blue1Brown 有一个关于神经网络和反向传播的精彩播放列表,值得多次观看。Josh Starmer 的 StatQuest 播放列表关于神经网络也很有帮助,特别是在将神经网络可视化为流形操作方面。关于流形理论和神经网络的另一个优秀视频可以在Art of the Problem 这里找到。最后,当你准备深入研究时,可以查看 Aurélien Géron 的使用 Scikit-Learn、Keras 和 TensorFlow 进行实践机器学习(O’Reilly)和 Francois Chollet 的Python 深度学习(Manning)。

如果你读到了本章的结尾,并且觉得你合理地吸收了一切,恭喜!你不仅有效地学习了概率、统计学、微积分和线性代数,还将其应用于线性回归、逻辑回归和神经网络等实际应用。我们将在下一章讨论你如何继续前进,并开始你职业成长的新阶段。

练习

将神经网络应用于我们在第六章中处理的员工留存数据。您可以在这里导入数据。尝试构建这个神经网络,使其对这个数据集进行预测,并使用准确率和混淆矩阵来评估性能。这对于这个问题是一个好模型吗?为什么?

虽然欢迎您从头开始构建神经网络,但考虑使用 scikit-learn、PyTorch 或其他深度学习库以节省时间。

答案在附录 B 中。

第八章:职业建议与前行之路

随着本书的结束,评估接下来该怎么做是个不错的主意。你学习并整合了广泛的应用数学主题:微积分、概率论、统计学和线性代数。然后,你将这些技术应用于实际应用,包括线性回归、逻辑回归和神经网络。在本章中,我们将讨论如何在前进的同时利用这些知识,航行在数据科学职业这个奇特、激动人心且多样化的领域中。我将强调拥有方向和明确的目标的重要性,而不是仅仅死记工具和技术,而缺乏实际问题的解决方案。

由于我们正在远离基础概念和应用方法,这一章的语调将与书中其余部分有所不同。你可能期待了解如何将这些数学建模技能以专注和实际的方式应用于你的职业生涯。然而,如果你希望在数据科学职业中取得成功,你将需要学习一些更硬的技能,比如 SQL 和编程,以及发展专业意识的软技能。后者尤为重要,这样你就不会在数据科学这个变幻莫测、看不见的市场力量盲目地中迷失方向。

我不会假设我知道你的职业目标或你希望通过这些信息实现什么。但我会打几个保险,因为你正在阅读这本书。我想象你可能对数据科学职业感兴趣,或者你已经在数据分析方面工作,并希望系统化你的分析知识。也许你来自软件工程背景,希望掌握人工智能和机器学习。也许你是某种项目经理,发现你需要了解数据科学或人工智能团队的能力,以便据此进行范围确定。也许你只是一个好奇的专业人士,想知道数学如何在实际层面上有用,而不仅仅是学术上的。

我将尽力满足所有这些群体的关切,并希望总结出一些对大多数读者有用的职业建议。让我们从重新定义数据科学开始。我们已经客观地研究了它,并将在职业发展和该领域的未来的背景下进行讨论。

作者是否在讲故事?

在像这样的任务中很难不显得像轶事,我在这里分享了自己的经验(以及他人的经验),而不是像我在第三章中提倡的大规模、控制的调查和研究。但是我要说的是:我在财富 500 强世界工作了十多年,看到了数据科学运动对组织的转变。我在世界各地的许多技术会议上发表演讲,并听取了无数同行的反应,“这种情况也发生在我们身上!”我阅读了许多博客和受人尊敬的出版物,从华尔街日报福布斯,我学会了认识到流行期望与现实之间的脱节。我特别关注各行各业的权威人士、领导者和追随者,以及他们如何通过数据科学和人工智能来制定或跟随市场。目前,我在南加州大学教授和指导人工智能在安全关键应用中的利益相关者,涉及航空安全和安全项目。

我只是把简历放在这里,来展示虽然我没有进行正式的调查和研究,也许我正在整理轶事信息,但我在所有这些信息源中找到了持续的叙述。Vicki Boykis,Tumblr 的敏锐的机器学习工程师,写了一篇博客文章分享了与我类似的发现,我强烈建议你阅读一下。你可以带着怀疑的心态接受我的发现,但要密切关注你自己工作环境中正在发生的事情,并识别你的管理层和同事们认同的假设。

重新定义数据科学

数据科学是分析数据以获得可操作洞见。实际上,它是将不同与数据相关的学科融合在一起的一个整体:统计学、数据分析、数据可视化、机器学习、运筹学、软件工程……只是举几个例子。几乎任何涉及数据的学科都可以被标志为“数据科学”。这种缺乏明确定义对该领域造成了问题。毕竟,任何缺乏定义的事物都容易被解释,就像一幅抽象艺术品一样。这就是为什么人力资源部门在“数据科学家”职位发布时感到困惑,因为它们往往七零八落。图 8-1 显示了一个涵盖不同学科和工具的伞形图,可以归入数据科学的范畴。

emds 0801

图 8-1. 数据科学的伞形图

我们是如何到达这一步的?一个像数据科学这样缺乏定义的东西如何在企业世界中成为如此有吸引力的力量?最重要的是,它的定义(或缺乏定义)如何影响你的职业生涯?这些都是我们在本章讨论的重要问题。

这就是为什么我告诉我的客户,数据科学的更好定义是具备统计学、机器学习和优化技能的软件工程。如果去掉这四个学科中的任何一个(软件工程、统计学、机器学习和优化),数据科学家的表现就会受到威胁。大多数组织在界定使数据科学家有效的技能集方面一直存在困难,但提供的定义应该能带来清晰度。尽管有些人可能认为软件工程是一个有争议的要求,但考虑到行业发展的方向,我认为这是非常必要的。我们将在后面讨论这一点。

但首先,理解数据科学的最佳方式是追溯这个术语的历史。

数据科学的简史

我们可以追溯数据科学一直到统计学的起源,早在17 世纪甚至 8 世纪。为了简洁起见,让我们从 1990 年代开始。分析师、统计学家、研究员、“量化分析师”和数据工程师通常拥有不同的角色。工具堆栈通常包括电子表格、R、MATLAB、SAS 和 SQL。

当然,在 2000 年后,事情开始迅速改变。互联网和连接设备开始产生大量数据。随着 Hadoop 的诞生,谷歌推动分析和数据收集达到了前所未有的高度。随着 2010 年的临近,谷歌的高管们坚信统计学家将在接下来的十年中拥有“性感”的工作。接下来发生的事情证明了他们的预见。

2012 年,《哈佛商业评论》推广了这个称为数据科学的概念,并宣称它是“21 世纪最性感的工作”。在《哈佛商业评论》文章之后,许多公司和企业工作者争相填补数据科学的空白。管理顾问们则准备好向财富 500 强领导者传授如何将数据科学引入他们的组织中。SQL 开发人员、分析师、研究员、量化分析师、统计学家、工程师、物理学家以及众多其他专业人士纷纷将自己重新打造为“数据科学家”。科技公司感到传统的角色名称如“分析师”、“统计学家”或“研究员”听起来过时,因此将这些角色改称为“数据科学家”。

自然地,财富 500 强公司的管理层受到 C 级高管的压力,要跟上数据科学的潮流。最初的理由是正在收集大量数据,因此大数据正在成为一种趋势,需要数据科学家从中获取洞见。在此期间,“数据驱动”这个词成为各行各业的格言。企业界相信,与人不同,数据是客观和无偏的。

提醒:数据不是客观和无偏的!

直到今天,许多专业人士和管理者都陷入了数据客观和无偏见的谬误中。希望在阅读本书后,您知道这简直是不可能的,需要有关此事的提醒,请参阅第三章。

公司的管理层和人力资源,在无法与 FAANG(Facebook、Amazon、Apple、Netflix 和 Google)抢购的专业深度学习博士竞争的情况下,仍然承受着数据科学的压力,他们做出了一个有趣的举动。他们把现有的分析师、SQL 开发人员和 Excel 高手重新打造成“数据科学家”。Google 的首席决策科学家 Cassie Kozyrkov 在 2018 年的 Hackernoon 博客文章中描述了这个公开的秘密:

在我担任每一个数据科学家职称的时候,实际上我早在重新品牌的 HR 官员对员工数据库进行小小的整形手术之前,已经在用另一种名字做同样的工作了。我的职责丝毫没有改变。我不是个例外;我的社交圈子里到处都是曾经的统计学家、决策支持工程师、量化分析师、数学教授、大数据专家、商业智能专家、分析主管、研究科学家、软件工程师、Excel 高手、小众博士幸存者……所有这些人都是今天自豪的数据科学家。

从技术上讲,数据科学并不排除这些专业人士中的任何一个,因为他们都在使用数据来获取洞见。当然,科学界也有些反对意见,他们不愿意把数据科学正式认定为一门真正的科学。毕竟,你能想到一门不使用数据的科学吗?2011 年,现任 Google TensorFlow 主管的 Pete Warden 在一篇 O’Reilly 文章中对数据科学运动进行了有趣的辩护。他也清楚地阐述了反对者对缺乏定义的论点:

[关于数据科学缺乏定义的问题] 可能是最深刻的反对意见,也是最有力的一条。对于数据科学的范围内外界限,尚无广泛接受的界定。它只是统计学的时尚重新包装吗?我不这么认为,但我也没有一个完整的定义。我相信最近数据的丰富使世界上发生了一些新的事情,当我四处看时,我看到有共同特征的人们,他们不适合传统的分类。这些人往往超越主导企业和机构世界的狭窄专业领域,处理从查找数据、大规模处理、可视化到撰写成故事的一切。他们似乎首先从数据告诉他们的内容开始,并选择追随有趣的线索,而不是传统科学家的方法,先选择问题然后找数据来阐明。

Pete 讽刺地也无法给数据科学提供一个定义,但他清楚地阐述了为什么数据科学是有缺陷但有用的。他还强调了研究放弃科学方法,转而采用曾经被摒弃的数据挖掘等实践的转变,我们在第三章中谈到过。

数据科学在《哈佛商业评论》文章发表几年后发生了有趣的转变。也许这更像是与人工智能和机器学习的合并,而不是一个转变。无论如何,当机器学习和深度学习在 2014 年左右成为头条新闻时,数据被称为创造人工智能的“燃料”。这自然地扩大了数据科学的范围,并与 AI/机器学习运动相结合。特别是,ImageNet 挑战激发了对 AI 的新兴兴趣,并引发了机器学习和深度学习的复兴。Waymo 和特斯拉等公司承诺几年内推出自动驾驶汽车,这得益于深度学习的进步,进一步推动了媒体头条新闻和夏令营的报名。

这种对神经网络和深度学习的突然兴趣产生了一个有趣的副作用。像决策树、支持向量机和逻辑回归这样的回归技术,几十年来一直隐藏在学术界和专业统计领域中,随着深度学习的光环走进了公众的视野。与此同时,像 scikit-learn 这样的库降低了进入该领域的门槛。这带来了一个隐藏的成本,即创建了一些不理解这些库或模型如何工作但仍然使用它们的数据科学专业人士。

由于数据科学的学科发展速度比对其定义的感知需求快得多,一个数据科学家的角色往往是完全不确定的。我曾经遇到过几位在财富 500 强公司担任数据科学家职位的人。有些人在编码方面非常熟练,甚至可能有软件工程背景,但对统计显著性一无所知。还有一些人局限于使用 Excel,几乎不懂 SQL,更不用说 Python 或 R 了。我曾经遇到过自学了一些 scikit-learn 函数的数据科学家,很快发现自己陷入困境,因为那是他们所知道的全部。

那么这对你意味着什么呢?在这样一个充斥着术语和混乱的环境中,你如何才能蓬勃发展呢?一切都取决于你对什么类型的问题或行业感兴趣,并且不要急于依赖雇主来定义角色。你不必成为数据科学家才能从事数据科学。有很多领域可以利用你现在拥有的知识为你带来优势。你可以成为分析师、研究员、机器学习工程师、顾问、咨询师,以及许多其他不一定称为数据科学家的角色。

但首先,让我们讨论一些你可以继续学习并在数据科学就业市场上找到优势的方法。

发现你的优势

实际数据科学专业人士需要的不仅仅是统计和机器学习的理解。在大多数情况下,期望数据能够轻松地用于机器学习和其他项目是不现实的。相反,你会发现自己在追寻数据来源、编写脚本和软件、抓取文档、抓取 Excel 工作簿,甚至创建自己的数据库。至少 95%的编码工作与机器学习或统计建模无关,而是创建、移动和转换数据以便使用。

此外,你还需要了解你的组织的全局视角和动态。你的管理者可能会在定义你的角色时做出一些假设,识别这些假设非常重要,这样你才能意识到它们对你的影响。当你依赖你的客户和领导的行业专业知识时,你应该在提供技术知识并表达可行性方面发挥作用。让我们来看看你可能需要的一些硬技能和软技能。

SQL 熟练度

SQL,也称为结构化查询语言,是一种用于检索、转换和写入表数据的查询语言。关系数据库是组织数据的最常见方式,将数据存储到表中,这些表像 Excel 中的 VLOOKUP 一样相互连接。MySQL、Microsoft SQL Server、Oracle、SQLite 和 PostgreSQL 等关系数据库平台都支持 SQL。正如你可能注意到的,SQL 和关系数据库紧密耦合,以至于“SQL”经常用于关系数据库的品牌营销,例如“MySQL”和“Microsoft SQL Server”。

示例 8-1 是一个简单的 SQL 查询,从CUSTOMER表中检索CUSTOMER_IDNAME字段,条件是STATE'TX'

示例 8-1. 一个简单的 SQL 查询
SELECT CUSTOMER_ID, NAME
FROM CUSTOMER
WHERE STATE = 'TX'

简而言之,作为数据科学专业人士,没有 SQL 的熟练技能很难有所成就。企业使用数据仓库,而 SQL 几乎总是检索数据的手段。SELECTWHEREGROUP BYORDER BYCASEINNER JOINLEFT JOIN 这些 SQL 关键字都应该很熟悉。更好的是,了解子查询、派生表、公共表表达式和窗口函数,以充分利用你的数据。

厚颜无耻的自荐:作者有一本 SQL 书!

我曾为 O'Reilly 写过一本入门级的 SQL 书,名为Getting Started with SQL。它只有一百多页,可以在一天内完成。书中涵盖了基本内容,包括连接和聚合,以及创建自己的数据库。该书使用 SQLite,可以在不到一分钟内设置好。

还有 O'Reilly 出版的其他出色的 SQL 书籍,包括 Alan Beaulieu 的《Learning SQL, 3rd Ed.》和 Alice Zhao 的《SQL Pocket Guide, 4th Ed.》。在速读完我那一百页的入门手册之后,也可以查看这两本书。

SQL 在让 Python 或其他编程语言轻松访问数据库方面也非常关键。如果你想从 Python 向数据库发送 SQL 查询,你可以将数据作为 Pandas DataFrames、Python 集合和其他结构返回。

示例 8-2 展示了使用 SQLAlchemy 库在 Python 中运行简单 SQL 查询。它以命名元组的形式返回记录。只需确保下载这个SQLite 数据库文件并放置在你的 Python 项目中,然后运行pip install sqlalchemy

示例 8-2. 使用 SQLAlchemy 在 Python 中运行 SQL 查询
from sqlalchemy import create_engine, text

engine = create_engine('sqlite:///thunderbird_manufacturing.db')
conn = engine.connect()

stmt = text("SELECT * FROM CUSTOMER")
results = conn.execute(stmt)

for customer in results:
    print(customer)

关于 Pandas 和 NoSQL 呢?

我经常收到关于与 SQL 的“替代品”(如 NoSQL 或 Pandas)的问题。事实上,这些并不是替代品,而是数据科学工具链中其他地方的不同工具。以 Pandas 为例,在示例 8-3 中,我可以创建一个 SQL 查询,从表CUSTOMER中提取所有记录并将它们放入 Pandas 的DataFrame中。

示例 8-3. 将 SQL 查询导入到 Pandas DataFrame 中
from sqlalchemy import create_engine, text
import pandas as pd

engine = create_engine('sqlite:///thunderbird_manufacturing.db')
conn = engine.connect()

df = pd.read_sql("SELECT * FROM CUSTOMER", conn)
print(df) # prints SQL results as DataFrame

SQL 被用来在关系数据库和我的 Python 环境之间建立桥梁,并将数据加载到 Pandas 的DataFrame中。如果我有需要使用 SQL 处理的复杂计算任务,通过 SQL 在数据库服务器上执行要比在本地计算机上使用 Pandas 更高效。简单来说,Pandas 和 SQL 可以共同工作,而不是竞争技术。

NoSQL 也是如此,包括像 Couchbase 和 MongoDB 这样的平台。虽然一些读者可能会有不同意见并提出合理的论点,但我认为将 NoSQL 与 SQL 进行比较就像是在比较苹果和橙子。是的,它们都存储数据并提供查询功能,但我不认为它们是竞争关系。它们针对不同的用例具有不同的特性。NoSQL 代表“不仅仅是 SQL”,更适合存储非结构化数据,如图片或自由格式的文本文章。SQL 更适合存储结构化数据。SQL 比 NoSQL 更积极地维护数据完整性,尽管以计算开销和较低的可扩展性为代价。

编程熟练度

通常,许多数据科学家并不擅长编程,至少不像软件工程师那样。然而,掌握编程技能变得越来越重要。这提供了获得优势的机会。学习面向对象编程、函数式编程、单元测试、版本控制(例如 Git 和 GitHub)、Big-O 算法分析、密码学以及其他你遇到的相关计算机科学概念和语言特性。

为什么呢?假设你创建了一个有前途的回归模型,比如逻辑回归或神经网络,基于一些给定的样本数据。你请求内部 IT 部门的程序员将其“插入”到现有软件中。

他们警惕地看着你的提议。“我们需要用 Java 重写这个,而不是 Python,”他们不情愿地说道。“你的单元测试在哪里?”另一个问道。“你没有定义任何类或类型吗?我们必须重新设计这段代码使其面向对象。”除此之外,他们不理解你的模型的数学,并担心它在未见过的数据上表现不佳。由于你没有定义单元测试(这在机器学习中并不直接),他们不确定如何验证你的模型的质量。他们还问两个代码版本(Python 和 Java)如何管理?

你开始感到不在自己的领域内,并说,“我不明白为什么 Python 脚本不能直接插入。”其中一人沉思片刻后回答道,“我们可以使用 Flask 创建一个 web 服务,避免重新用 Java 编写。但是,其他问题并没有消失。我们接下来必须担心可扩展性和高访问量对 web 服务的影响。等等…也许我们可以部署到微软 Azure 云上作为一个虚拟机规模集,但我们仍然必须设计后端。看,无论你如何处理,这都必须重新设计。”

这正是为什么许多数据科学家的工作从不离开他们的笔记本电脑。事实上,将机器学习投入生产已经变得如此难以捉摸,以至于它已经成为近年来的一个独角兽和热门话题。数据科学家和软件工程师之间存在巨大的鸿沟,因此自然而然地,数据科学专业人员现在也要成为软件工程师。

这可能听起来令人不知所措,因为数据科学的范围已经很广泛,涵盖了许多学科和要求。但是,这并不意味着你需要学习 Java。你可以成为一名有效的软件工程师,使用 Python(或者你喜欢的任何就业语言),但你必须擅长它。学习面向对象编程、数据结构、函数式编程、并发编程和其他设计模式。两本解决这些问题的好书包括 Luciano Ramalho 的《流畅的 Python,第二版》(O’Reilly)和 Al Sweigart 的《Python 基础进阶》(No Starch)。

之后,学习解决实际任务,包括数据库 API、web 服务JSON 解析正则表达式网页抓取安全和加密、云计算(Amazon Web Services、Microsoft Azure)以及其他任何有助于你在建立系统时提高生产力的东西。

正如前文所述,你精通的编程语言不一定非得是 Python。它可以是其他语言,但建议选择那些普遍使用和具有就业能力的语言。目前具有较高就业能力的语言包括 Python、R、Java、C# 和 C++。Swift 和 Kotlin 在苹果和安卓设备上占据主导地位,它们都是出色的、得到很好支持的语言。尽管这些语言大多数不是数据科学的主流,学习至少另外一门语言以增加曝光是有帮助的。

锚定偏差与首选编程语言

对技术专业人士而言,对技术和平台产生偏爱和情感投入是很常见的,特别是对编程语言。请不要这样做!这种部落主义是没有生产力的,它忽略了每种编程语言服务不同质量和用例的现实。另一个现实是,一些编程语言流行而另一些不流行,通常原因与语言设计的优点无关。如果一家大公司不支付其支持费用,它的生存机会就很渺茫。

我们在第三章讨论了不同类型的认知偏差。另一个是锚定偏差,它指出我们可能会偏爱第一次学到的东西,比如一门编程语言。如果你感觉有义务学习一门新语言,要开放心态,给它一个机会!没有完美的语言,重要的是它能完成任务。

但是,如果语言的支持情况堪忧,比如它处于维护状态,没有更新,或者缺乏企业支持者,就要小心了。例如,微软的VBA,Red Hat 的Ceylon,以及Haskell

数据可视化

另一项你应该具备一定程度熟练度的技术技能是数据可视化。能够轻松制作图表、图形和绘图,不仅能向管理层讲述故事,还能帮助自己的数据探索工作。你可以用 SQL 命令总结数据,但有时柱状图或散点图能够更快地让你了解数据。

当涉及到选择数据可视化工具时,这个问题就变得更加难以回答,因为选择太多,而且分散。如果你在传统办公环境中工作,Excel 和 PowerPoint 通常是首选的可视化工具,你知道吗?它们完全可以胜任!虽然我不是所有事情都用它们,但它们确实能完成绝大多数任务。需要在小/中型数据集上绘制散点图?或者直方图?没问题!只需将数据复制/粘贴到 Excel 工作簿中,几分钟内就能生成一个。这对于一次性图形可视化非常方便,在这种情况下使用 Excel 毫无羞耻感。

然而,有些情况下,你可能希望脚本化创建图表,以便重复使用或与你的 Python 代码集成。matplotlib已经是一段时间以来的首选,当你的平台是 Python 时,很难避免。Seaborn在 matplotlib 之上提供了一个包装器,使其更易于使用常见的图表类型。SymPy,在本书中我们经常使用的库,使用 matplotlib 作为其后端。然而,有些人认为 matplotlib 非常成熟,已经接近遗留状态。类似Plotly的库已经崭露头角,使用起来非常愉快,它基于 JavaScript 的D3.js 库。就个人而言,我在Manim方面取得了成功。它产生的 3Blue1Brown 风格的可视化效果非常出色,给客户带来了“哇!”的感觉,而且考虑到它具有的动画功能,其 API 使用起来令人惊讶地简单。然而,这是一个年轻的库,尚未成熟,这意味着每个发布版本的演变可能会导致代码变更。

探索所有这些解决方案不会有错,如果雇主/客户没有偏好,可以找到最适合自己的一个。

商业许可证平台如Tableau在某种程度上还算不错。他们致力于创建专门用于可视化的专有软件,并创建了一个拖放界面,使其对非技术用户也可访问。Tableau 甚至有一篇标题为“让你的组织中每个人都成为数据科学家”的白皮书,这并没有解决之前提到的数据科学家定义问题。我发现 Tableau 的挑战在于它只擅长可视化,并需要昂贵的许可证。虽然你可以在TabPy中部分集成 Python,但除非雇主想使用 Tableau,否则你可能会选择使用之前提到的功能强大的开源库。

软件许可证可能涉及政治问题。

想象一下,你创建了一个 Python 或 Java 应用程序,请求用户输入一些信息,检索和处理不同的数据源,运行一些高度定制的算法,然后呈现可视化和显示结果的表格。经过几个月的辛苦工作后,在会议上展示出来,但是其中一位经理举手发问:“为什么不直接在 Tableau 中完成这个任务呢?”

对于一些经理来说,这是一个难以接受的现实,他们花费了数千美元购买企业软件许可证,而您来了,使用一个更强大(尽管使用起来更复杂)且没有许可成本的开源解决方案。您可以强调 Tableau 不支持您必须创建的这些算法或集成工作流程。毕竟,Tableau 只是可视化软件。它不是一个从头开始编码的平台,无法创建定制的高度量身定制的解决方案。

领导层通常被告知 Tableau、Alteryx 或其他商业工具可以做到一切。毕竟,他们为此花了大量的钱,而且可能供应商给了他们一个很好的销售演示。自然地,他们想要为这些成本辩护,并希望尽可能多的人使用许可证。他们可能还花了额外的预算培训员工使用该软件,并希望其他人能够维护您的工作。

对此要敏感。如果管理层要求您使用他们支付的工具,请探索是否可以让其发挥作用。但如果在您特定的任务中存在限制或严重的可用性妥协,请在一开始就礼貌地提出来。

了解您的行业

让我们比较两个行业:视频流媒体(例如 Netflix)和航空航天防务(例如洛克希德·马丁)。它们有共同之处吗?几乎没有!两者都是技术驱动型公司,但一个是为消费者提供电影流媒体,另一个是制造带弹药的飞机。

当我在人工智能和系统安全方面提供建议时,我首先指出这两个行业对风险的容忍度有很大不同。一个电影流媒体公司可能会吹嘘他们拥有一个能学习推荐给消费者电影的 AI 系统,但当它给出一个糟糕的推荐时,情况有多严重?最坏的情况是消费者可能会稍感失望,浪费了两个小时观看他们不喜欢的电影。

但是航空航天防务公司呢?如果一架战斗机上搭载了自动射击目标的 AI,如果它出错了会有多严重?现在我们谈论的是人类生命,而不是电影推荐!

这两个行业的风险容忍度差距很大。自然地,航空航天防务公司在实施任何实验性系统时都会更为保守。这意味着官僚主义和安全工作组会评估并阻止他们认为风险不可接受的任何项目,这是理所当然的。然而,有趣的是,在硅谷初创公司中,AI 在像电影推荐这样的低风险应用中取得了成功,这引发了防务工业高管和领导层的 FOMO(“错失良机的恐惧”)。这可能是因为这两个领域之间的风险容忍度差距没有被充分强调。

当然,在这两个行业之间的风险严重程度之间有着广泛的谱系,“恼怒的用户”和“人类生命的丧失”之间。银行可能会使用 AI 来确定谁有资格获得贷款,但这带来了在歧视某些人口统计学特征方面的风险。刑事司法系统在假释和监视系统中尝试使用 AI,但也遇到了同样的歧视问题。社交媒体可能使用 AI来确定哪些用户发布的内容是可接受的,但当它压制“无害”内容(虚假阳性)时,会激怒其用户,也会让立法者感到不满,因为“有害”内容没有被压制(虚假阴性)。

这表明了了解自己所在行业的必要性。如果你想要做大量的机器学习,你可能会希望在低风险行业工作,其中虚假阳性和虚假阴性不会危及或激怒任何人。但如果这些都不吸引你,你想要从事像自动驾驶汽车、航空和医学等更大胆的应用,那么期望你的机器学习模型会被频繁拒绝。

在这些高风险行业中,如果需要特定的博士学位或其他正式证书,不要感到意外。即使拥有专业的博士学位,虚假阳性和虚假阴性也不会神奇地消失。如果你不想追求这种专业化,你可能最好学习除了机器学习之外的其他工具,包括软件工程、优化、统计学和业务规则系统/启发式方法。

有效的学习

在 2008 年的单口喜剧特别节目中,喜剧演员布莱恩·雷根将自己对不读报纸的人的缺乏好奇心与那些读报纸的人进行了对比。指出头版新闻故事永远不会完结,他说他没有兴趣翻到指定的页面去找出它的结局。“经过九年的审判,陪审团最终做出了继续在第 22 页第 C 栏上继续的裁决……我想我永远也不会知道,”他轻蔑地开玩笑说。然后他与那些翻页的人进行对比,喊道,“我想学习!我想成为一个学习者!”

尽管布莱恩·雷根可能本意是自嘲,也许他在某些方面是对的。仅仅为了学习而学习一个主题几乎没有动力,而且对于缺乏兴趣并不总是一件坏事。如果你拿起一本微积分教科书,没有学习它的目的,你可能最终会感到沮丧和挫败。你需要有一个项目或目标,如果你觉得某个主题无趣,为什么要去学习它呢?就我个人而言,当我允许自己对我认为无关紧要的主题失去兴趣时,这是非常解放的。更令人惊讶的是,我的工作效率飙升了。

这并不意味着你不应该保持好奇心。然而,那里有如此多的信息,优先考虑你学习的内容是一项无价的技能。你可以问为什么某些东西有用,如果你得不到一个直接的答案,那就允许自己继续前进!所有人都在谈论自然语言处理吗?这并不意味着你非得去做!大多数企业根本不需要自然语言处理,所以说它不值得你的努力和时间是可以的。

无论你在工作中有项目还是为了自学而自主创建项目,都应该有具体的目标。只有可以决定什么值得学习,你可以摒弃在追求你感兴趣和相关的事物时可能产生的焦虑。

从业者与顾问

这可能是一种概括,但有两种类型的知识专家:从业者和顾问。为了找到你的优势,辨别你想成为哪种类型,并相应地调整你的职业发展。

在数据科学和分析领域,从业者们编写代码、创建模型、搜寻数据,并试图直接创造价值。顾问就像顾问,告诉管理层他们的目标是否合理,帮助制定战略,并提供方向。有时候,从业者可以逐步成为顾问。有时候,顾问从未是从业者。每种角色都有其利弊。

从业者可能喜欢编码、进行数据分析,并执行直接能够创造价值的具体工作。从业者的好处在于他们实际上开发并拥有硬技能。然而,沉迷于代码、数学和数据很容易让人失去大局观,并与组织和行业的其他部分脱节。我经常听到经理抱怨的一个常见问题是,他们的数据科学家想要解决他们认为有趣但对组织没有价值的问题。我也听到从业者抱怨,他们想要曝光和上升空间,但感觉在组织中被束缚和隐藏。

顾问在某些方面确实有一个更轻松的工作。他们向经理们提供建议和信息,并帮助企业提供战略方向。他们通常不是编写代码或搜寻数据的人,但他们帮助管理层雇佣那些从事这些工作的人。他们的职业风险是不同的,因为他们不必担心满足冲刺期限、处理代码错误或者模型运行不良的问题,就像从业者们那样。但是他们确实需要担心保持知识更新、可信和相关性。

要成为一个有效的顾问,您必须真正了解并掌握其他人不了解的知识。这必须是与客户需求密切相关的关键信息。要保持相关性,您每天都必须阅读,寻找并综合他人忽视的信息。仅仅熟悉机器学习、统计学和深度学习是不够的。您还必须关注客户所在的行业以及其他行业,追踪谁在成功谁在失败。您还必须学会将正确的解决方案与正确的问题匹配,在一个许多人寻找银弹的商业环境中。为了做到这一切,您必须成为一个有效的沟通者,并以帮助客户为目标分享信息,而不仅仅是展示自己的知识。

顾问面临的最大风险是提供错误的信息。一些顾问非常善于将责任推向外部因素,比如“行业内没有人预见到这一点”或“这是一个六西格玛事件!”意味着一个不希望发生的事件出现的概率是五亿分之一,但却发生了。另一个风险是没有从业者的硬技能,并且与业务技术层面脱节。这就是为什么定期在家练习编码和建模,或者至少将技术书籍作为阅读的一部分是个好主意。

最后,一个好的顾问努力成为客户与他们最终目标之间的桥梁,经常填补存在的巨大知识空白。这不是为了计费最大小时数和编造繁忙工作,而是真正识别客户的困扰,并帮助他们安心入睡。

当项目计划基于工具而非问题时,项目成功的可能性很低。这意味着作为顾问,您必须磨练自己的倾听技巧,并识别客户难以提出甚至回答的问题。如果一家主要快餐连锁店聘请您协助“AI 战略”,而您看到他们的人力资源部门急于聘请深度学习人才,您的工作就是问,“您试图解决深度学习中的哪些问题?”如果得不到明确答案,您需要鼓励管理层退后一步,评估他们真正面临的行业问题。他们是否存在员工排班效率低下的问题?嗯,他们不需要深度学习,他们需要线性规划!对于一些读者而言,这可能显得很基础,但如今许多管理人员往往难以进行这些区分。我曾多次遇到将其线性规划解决方案品牌化为 AI 的供应商和顾问,这在语义上可以与深度学习混淆。

数据科学职位需注意事项

要理解数据科学职位市场,可能需要将其与一部深刻的美国电视作品进行比较。

在 2010 年,有一部美国电视剧Better Off Ted。其中一集名为“Jabberwocky”(第 1 季,第 12 集),深刻揭示了企业行话的某种本质。在节目中,主角泰德(Ted)在公司虚构了一个名为“Jabberwocky”的项目来隐藏资金。由此引发了一连串滑稽的结果,他的经理、CEO,最终整个公司都开始“参与”这个“Jabberwocky”项目,甚至都不知道这究竟是什么。局面逐渐升级,成千上万的员工假装在“Jabberwocky”上工作,而没有人停下来问他们究竟在做什么。原因在于:没有人愿意承认自己不了解这个重要事务,处于不在圈内、无知的状态。

Jabberwocky 效应是一个轶事性理论,即一个行业或组织可以不定义清楚一个流行词或项目,却能持续推广它。组织会周期性地陷入这种行为中,允许术语在没有明确定义的情况下循环流传,并且群体行为使得模糊性得以存在。常见的例子包括区块链、人工智能、数据科学、大数据、比特币、物联网、量子计算、NFT、以“数据驱动”为核心的技术、云计算和“数字颠覆”。即使是具体、备受关注且特定的项目,也可能变成只有少数人理解却被广泛讨论的神秘流行词。

要阻止 Jabberwocky 效应,你必须成为促进有效对话的催化剂。对项目或倡议的方法和手段感到好奇(而不仅仅是品质或结果)。在角色选择时,公司是因为担心错过机会(FOMO)而雇佣你去做“Jabberwocky”,还是因为他们实际上有特定和实用的需求?做出这种判断可以决定你是否与公司匹配良好,顺利前行,还是在职业生涯中遇到沉闷的障碍。

在这种背景下,让我们现在来考虑一下在数据科学岗位中需要注意的几个事项,从角色定义开始。

角色定义

假设你被聘为数据科学家。面试进行得很顺利。你问了一些关于角色的问题,得到了直接的回答。你被提供了一份工作,而最重要的是,你应该知道自己将要从事哪些项目。

你总是希望进入一个角色清晰定义、目标明确的岗位。不应该猜测自己该做什么。更好的情况是,你应该有一个明确愿景的领导层,理解业务需求。你成为明确定义目标的执行者,并了解你的客户。

相反,如果你被聘用是因为部门想要“数据驱动”或在“数据科学”中拥有竞争优势,这是一个警告信号。很有可能你将被负担着寻找问题并销售任何低成本的解决方案。当你寻求战略指导时,你会被告知将“机器学习”应用于业务。但当然,当你只有一把锤子时,一切都开始看起来像钉子。数据科学团队感到压力要在甚至没有目标或问题的情况下提供解决方案(例如机器学习)。一旦发现问题,获得利益相关者的支持和资源对齐就变得困难,重点开始从一个低成本的解决方案转移到另一个低成本的解决方案。

问题在于你是基于流行词被聘用的,而不是基于功能。不良的角色定义往往会传播到下面讨论的其他问题。让我们转向组织的关注点。

组织的专注和支持

另一个需要注意的因素是组织在特定目标上的对齐程度以及所有各方是否全力支持。

自从数据科学的兴起以来,许多组织进行了重组,成立了一个中央数据科学团队。高管的愿景是让数据科学团队流动,提供建议,并帮助其他部门实现数据驱动,并采用机器学习等创新技术。他们还可能被要求消除部门之间的数据孤岛。虽然这在理论上听起来是个好主意,但许多组织发现这充满挑战。

原因在于:管理层成立了一个数据科学团队,但缺乏明确的目标。因此,这个团队的任务是寻找需要解决的问题,而不是有权力解决已知问题。正如所述,这就是为什么数据科学团队以先有解决方案(例如机器学习),而后有目标而闻名。他们尤其不适合成为消除数据孤岛的推动力,因为这完全超出了他们的专业领域。

打破数据孤岛是 IT 的工作!

在组织中使用数据科学团队“打破数据孤岛”是错误的。数据孤岛往往是由于缺乏数据仓库基础设施,各部门将其数据存储在电子表格和秘密数据库中,而不是集中和支持的数据库中。

如果数据孤岛被视为问题,你需要服务器、云实例、认证的数据库管理员、安全协议以及 IT 工作组来把所有这些整合起来。数据科学团队通常没有必要的专业知识、预算和组织权力来完成这项任务,除非是在非常小的公司里。

一旦发现问题,获得利益相关者的支持和资源对齐是困难的。如果发现机会,需要强大的领导力来做以下几点:

  • 明确定义目标和路线图

  • 获得预算来收集数据并支持基础设施

  • 获得数据访问权限并协商数据所有权

  • 包括利益相关者的支持和领域知识

  • 从利益相关者那里预算时间和会议

在数据科学团队被聘用之后,这些要求要比之前更难实现,因为数据科学团队的角色是在对现有情况做出反应的基础上确定和预算的。如果高层领导没有对所有必要方方面面的资源和支持做好对齐,数据科学项目就不会成功。这就是为什么有无数的文章指责组织没有准备好数据科学,从哈佛商业评论MIT 斯隆评论

最好是在组织上与其客户处于同一部门的数据科学团队进行工作。信息、预算和沟通更自由和紧密地共享。这样,减少了跨部门政治的紧张局势,因为将所有人置于同一团队而非政治竞争之中。

数据访问是具有政治性的

组织对其数据保持保护并非秘密,但这并不仅仅是出于安全或不信任的考虑。数据本身是一种极具政治性的资产,许多人甚至不愿意向自己的同事提供访问权限。同一组织内部的部门出于这个原因也不愿意共享数据:他们不希望其他人来做他们的工作,更不希望他们做错。解释数据可能需要他们全职专业知识,也需要他们的领域知识。毕竟,他们的数据就是他们的业务!如果你请求访问他们的数据,那就是在要求介入他们的业务。

此外,数据科学家可能高估了解释外部数据集所需的能力和领域专业知识。要克服这一障碍,你必须与每个具有专业知识的合作伙伴建立信任和支持,协商知识转移,如果需要,让他们在项目中扮演重要角色。

足够的资源

另一个需要警惕的风险是没有得到足够的资源来完成工作。被投入到一个角色中而没有必要的支持是很困难的。当然,适应性和善于利用资源是一种宝贵的特质。但即使是最能打的软件工程师/数据科学家明星也很快会发现自己超出了自己的能力范围。有时候你需要花钱来购买必需的东西,而你的雇主却没有预算。

假设你需要一个数据库来进行预测工作。你与第三方数据库的连接很差,经常出现宕机和断开连接的情况。你最不想听到的就是“让它工作”,但这就是你的处境。你考虑在本地复制数据库,但为此你需要每天存储 40 GB 的数据,因此需要一个服务器或云实例。现在,你显然已经超出了自己的能力范围,一个数据科学家成为一个没有 IT 预算的 IT 部门!

在这些情况下,你必须考虑如何在不损害项目的情况下省去一些步骤。你能否仅保留最新的滚动数据并删除其余数据?你能否创建一些错误处理的 Python 脚本,在断开连接时重新连接,并将数据分批处理,以便从上一批次的最后成功点重新开始?

如果这个问题和解决方案听起来很具体,是的,我确实遇到过这种情况,而且这确实有效!能够提出解决方案并简化流程,而不增加更多成本,是令人满意的事情。但不可避免地,对于许多数据项目,你可能需要数据管道、服务器、集群、基于 GPU 的工作站以及其他桌面电脑无法提供的计算资源。换句话说,这些东西是需要成本支持的,而你的组织可能无法为它们预算。

数学建模在哪里?

如果你想知道为什么你被聘请来做回归、统计、机器学习和其他应用数学,结果却发现自己在做独行的 IT 工作,那在当前的企业氛围中并不罕见。

然而,你正在处理数据,这本质上可能会导致类似 IT 的工作。重要的是确保你的技能仍然与工作和所需的结果相匹配。我们将在本章的其余部分讨论这一点。

合理的目标

这是一个需要特别注意的问题。在充满炒作和远大承诺的环境中,很容易遇到不切实际的目标。

有些情况下,经理聘请数据科学家,并期望他们毫不费力地为组织增加指数级的价值。如果组织仍在进行手工操作,并且到处都有自动化的机会,这当然是可能的。例如,如果组织所有工作都在电子表格中进行,并且预测纯粹是猜测,那么对于数据科学专业人员来说,将流程优化为数据库,并使用简单的回归模型取得进展,这是一个很好的机会。

另一方面,如果组织聘请数据科学家将机器学习应用到他们的软件中,特别是识别图像中的对象,那就更加困难了。一个了解情况的数据科学家必须向管理层解释,这是一个至少会花费数十万美元的努力!不仅需要收集图片,还必须雇佣人工来标记图像中的对象。而这仅仅是收集数据的第一步!

数据科学家通常要花费他们最初的 18 个月向管理层解释为什么他们尚未交付成果,因为他们仍在努力收集和清洗数据,这占据了机器学习工作的 95%。管理层可能对此感到幻灭,因为他们陷入了一个普遍的叙事中,即机器学习和人工智能将消除手工流程,但事实上他们发现自己只是将一套手工流程换成了另一套:获得标记数据。

因此要警惕那些設置不合理目標的環境,尤其是當其他人向管理層承諾了“EASY 按鈕”的時候,要找到外交的方式來管理期望。來自其他值得信賴的商業期刊和高額管理咨詢公司的聲稱認為超智能 AI 即將來臨。缺乏技術專業知識的經理人可能會成為這種炒作敘述的受害者。

惠誰?

拉丁語表達“cui bono”意思是“誰受益?”當您試圖理解 Jabberwocky 效應時,這是一個很好的問題。當媒體宣傳有關人工智能的故事時,誰從中受益?無論您的答案是什麼,媒體也會從點擊和廣告收入中受益。高額管理咨詢公司圍繞“AI 戰略”創建更多計費時間。晶片製造商可以推廣深度學習以銷售更多顯卡,而雲平台可以為機器學習項目銷售更多數據存儲和 CPU 時間。

所有這些方面有什麼共同點?不僅是他們將 AI 作為銷售產品的手段,而且他們對客戶的長期成功沒有利益。他們在賣的是單位,而不是項目結果,就像在淘金熱時賣鏟子一樣。

不過,我不是在說這些媒體和供應商的動機是不道德的。他們的員工的工作是為公司賺錢並養活家庭。宣傳其產品的主張甚至可能是合法和可實現的。但是,不能否認一旦宣傳了一個主張,要撤回它是很困難的,即使它被認為是無法實現的。許多企業可能會轉向並重新指定他們的努力,而不是承認他們的主張沒有實現。因此,只需注意這種動態,並始終問“cui bono?”

與現有系統競爭

這種警告可能屬於“合理目標”的一部分,但我認為這種情況非常普遍,足以獨立成類別。一種微妙但存在問題的角色類型是與實際上沒有問題的現有系統競爭。這些情況可能出現在工作環境中,缺乏工作且需要忙碌起來的地方。

幾年前,您的雇主與供應商簽訂合同安裝了一個銷售預測系統。現在您的經理要求您通過增加 1%的準確性來改進預測系統。

您看到這裡的統計問題了嗎?如果您讀過第三章,1%不應該在統計上感到顯著,隨機性可以輕易給您帶來這 1%,而您本人無需任何努力。相反,隨機性可能會朝相反的方向擺動,市場力量超出您的控制範圍,可以抵消您實施的任何努力。一個糟糕的銷售季度和公司市場中的競爭對手等因素可能使收入減少-3%,而不是您無可避免的 p-hack 的 1%。

这里的主要问题除了工作的冗余外,还有结果不在你的影响范围内。这可能会变得不太理想。如果你要与一个没有问题、没有自动化的现有系统竞争,那么你将会面临困难时期。如果可能的话,当你遇到这种项目时,最好逃之夭夭。

一个角色并不是你期望的那样

当你开始一个角色,并发现它并不是你期望的那样时,你会怎么做?例如,你被告知你的角色将是统计和机器学习,但实际上你发现自己在做类似 IT 工作,因为组织的数据基础根本不足以进行机器学习。

你也许能够制作柠檬水。当然,你可以接受你的数据科学家角色转变为 IT 角色,并且在此过程中获得一些数据库和编程技能。你甚至可能成为驻场 SQL 专家或技术大师,这样职业上会更加舒适。当你简化企业的数据操作和工作流程时,你正在为未来更复杂的应用做好准备。当你的运营顺利进行时,你可以分配时间学习和在你感兴趣的领域中职业生涯成长。

另一方面,如果你期望做统计分析和机器学习,但最终发现自己在调试损坏的电子表格、Microsoft Access 和 VBA 宏,那么你可能会感到失望。在这种情况下,至少要成为变革的倡导者。推动现代化工具的使用,主张使用 Python 和现代数据库平台,如 MySQL 甚至 SQLite。如果你能做到这一点,那么至少你将站在一个允许更多创新的平台上,并且离应用本书中的概念更近一步。这也将有益于组织,因为支持和工具的灵活性将会增加,而 Python 人才比 Microsoft Access 和 VBA 等过时技术更容易找到。

你梦想的工作不存在吗?

虽然你总是可以放弃一个角色,但一定要评估你的期望是否真实可行。你正在追求的尖端技术也许太尖端了吗?

以自然语言处理为例。例如,你想使用深度学习构建聊天机器人。然而,实际上做这种工作的公司并不多,因为大多数公司并不真正需要聊天机器人。为什么呢?因为聊天机器人目前还不够成熟。虽然像OpenAI 进行了有趣的研究,如 GPT-3,但大部分仍然只是有趣的研究。最终,GPT-3 只是基于概率的模式识别器,将单词链接在一起,因此它没有常识。有研究证明了这一点,包括纽约大学的 Gary Marcus 的一些研究

这意味着,创建具有更广泛应用的聊天机器人仍然是一个未解决的问题,并且对大多数企业来说还没有形成价值主张。如果自然语言处理是你真正想追求的领域,而且你发现与职业机会存在脱节,你最好的选择可能是进入学术界进行研究。虽然像 Alphabet 这样的公司进行类似学术研究,但许多员工都是从学术界过来的。

所以,在你探索职场时,请保持对期望的现实态度。如果你的期望超出了职场能提供的范围,强烈考虑学术路线。当你所追求的工作类型经常要求博士学位或特定的学术证书,并且这成为你实现梦想工作的障碍时,你也应该考虑这条路线。

Where Do I Go Now?

现在我们已经覆盖了数据科学的格局,接下来我们该去哪里?数据科学的未来又是什么?

首先,考虑担任数据科学家职称的负担。这意味着隐含要求具备知识的无边界性,主要是由于定义缺乏标准化的限制性。如果我们从过去 10 年观察数据科学运动中学到了任何东西,那就是定义很重要。数据科学家正在演变成为精通统计学、优化和机器学习的软件工程师。甚至可能不再拥有“数据科学家”这个职称。尽管这比“21 世纪最性感职位”宣布时的要求更为广泛,但具备这些技能正在变得必要。

另一种选择是专注于更专业的职称,过去几年这种情况越来越多见。像计算机视觉工程师、数据工程师、数据分析师、研究员、运筹分析师和顾问/咨询师这样的角色正在回归。我们看到数据科学家角色的数量减少,这一趋势可能在未来 10 年继续,主要是由于角色专业化。遵循这种趋势当然是一个选择。

需要注意的是,劳动市场已经发生了巨大变化,这就是为什么你需要本章列出的竞争优势。尽管在 2014 年,数据科学家被视为独角兽,拥有六位数的薪水,但如今任何公司的数据科学家职位很容易收到数百甚至数千份申请,而薪水可能只有五位数。数据科学学位和短期培训班已经创造了大量的数据科学专业人才供给。因此,争取市场标榜为数据科学家或数据科学家总体职位的工作非常具有竞争性。这就是为什么追求分析师、运筹研究和软件开发者等角色并不一定是一个坏主意!Vicki Boykis,Tumblr 的机器学习工程师,在她的博客文章“数据科学现在不同了”中或许表达得最好:

记住最终目标……是要超过那些正在攻读数据科学学位、参加训练营和通过教程学习的人群。

你想要踏入门槛,获得与数据相关的职位,并朝着你梦寐以求的工作迈进,同时尽可能多地了解科技行业的情况。

不要陷入分析的瘫痪。选择一小部分内容并从那里开始。做一些小事。学到一些小东西,建立一些小东西。告诉其他人。记住,你在数据科学方面的第一份工作可能不会是数据科学家。

结论

这是本书中与其他章节不同的一章,但如果你想要在数据科学职场中航行并有效地应用本书中的知识,它是很重要的。当你发现大部分工作将把你引向其他工作时,可能会感到困惑,因为你已经学习了统计工具和机器学习。当这种情况发生时,抓住机会继续学习和提升技能。当你将基本的数学知识与编程和软件工程的熟练程度结合起来时,你的价值将因为理解 IT 和数据科学之间的差距而增加数十倍。

记住要摒弃炒作,倾向于实际解决方案,不要陷入技术视野而被市场力量所蒙蔽。了解管理和领导动机,以及人们普遍的动机。理解事物的为什么而不仅仅是如何。要好奇一个技术或工具是如何解决问题的为什么,而不仅仅是它的技术方面是如何操作的。

学习不是为了学习本身,而是为了发展能力,并将正确的工具与正确的问题匹配起来。学习的最有效方法之一是选择一个你觉得有趣的问题(而不是工具!)。解决这个问题会引发对其他事物的好奇心,然后又是另一个,再接着是另一个。你心中有一个目标,所以继续朝着正确的兔子洞深入,并知道何时从其他兔子洞中抽身而出。采取这种方法绝对是有益的,你会惊讶于在短时间内能获得多少专业知识。

附录 A. 补充主题

使用 SymPy 进行 LaTeX 渲染

当你对数学符号更加熟悉时,将你的 SymPy 表达式显示为数学符号可能会很有帮助。

最快的方法是在 SymPy 中使用 latex() 函数对你的表达式,然后复制结果到 LaTeX 数学查看器中。

示例 A-1 是一个将简单表达式转换为 LaTeX 字符串的示例。当然,我们也可以对导数、积分和其他 SymPy 操作的结果进行渲染成 LaTeX。但让我们保持示例简单。

示例 A-1. 使用 SymPy 将表达式转换为 LaTeX
from sympy import *

x,y = symbols('x y')

z = x**2 / sqrt(2*y**3 - 1)

print(latex(z))

# prints
# \frac{x^{2}}{\sqrt{2 y^{3} - 1}}

这个 \frac{x^{2}}{\sqrt{2 y^{3} - 1}} 字符串是格式化的 mathlatex,有多种工具和文档格式可以适应它。但为了简单地渲染 mathlatex,可以去 LaTeX 方程编辑器。这里有两个我在线使用的不同的:

在 图 A-1 中,我使用 Lagrida 的 LaTeX 编辑器来渲染数学表达式。

emds aa01

图 A-1. 使用数学编辑器查看 SymPy LaTeX 输出

如果你想要省略复制/粘贴步骤,你可以将 LaTeX 直接附加为 CodeCogs LaTeX 编辑器 URL 的参数,就像示例 A-2 中展示的那样,它将在你的浏览器中显示渲染后的数学方程。

示例 A-2. 使用 CodeCogs 打开一个 mathlatex 渲染。
import webbrowser
from sympy import *

x,y = symbols('x y')

z = x**2 / sqrt(2*y**3 - 1)

webbrowser.open("https://latex.codecogs.com/png.image?\dpi{200}" + latex(z))

如果你使用 Jupyter,你也可以使用插件来渲染 mathlatex

从头开始的二项分布

如果你想从头实现一个二项分布,这里是所有你需要的部分在 示例 A-3 中。

示例 A-3. 从头构建一个二项分布
# Factorials multiply consecutive descending integers down to 1
# EXAMPLE: 5! = 5 * 4 * 3 * 2 * 1
def factorial(n: int):
    f = 1
    for i in range(n):
        f *= (i + 1)
    return f

# Generates the coefficient needed for the binomial distribution
def binomial_coefficient(n: int, k: int):
    return factorial(n) / (factorial(k) * factorial(n - k))

# Binomial distribution calculates the probability of k events out of n trials
# given the p probability of k occurring
def binomial_distribution(k: int, n: int, p: float):
    return binomial_coefficient(n, k) * (p ** k) * (1.0 - p) ** (n - k)

# 10 trials where each has 90% success probability
n = 10
p = 0.9

for k in range(n + 1):
    probability = binomial_distribution(k, n, p)
    print("{0} - {1}".format(k, probability))

使用 factorial()binomial_coefficient(),我们可以从头构建一个二项分布函数。阶乘函数将一系列整数从 1 到n相乘。例如,5! 的阶乘将是 1 2 3 4 5 = 120 。

二项式系数函数允许我们从 n 个可能性中选择 k 个结果,而不考虑顺序。如果你有 k = 2 和 n = 3,那将产生集合 (1,2) 和 (1,2,3)。在这两组之间,可能的不同组合将是 (1,3),(1,2) 和 (2,3)。因此,这将是一个二项式系数为 3。当然,使用 binomial_coefficient() 函数,我们可以避免所有这些排列工作,而是使用阶乘和乘法来实现。

在实现binomial_distribution()时,请注意我们如何取二项式系数,并将其乘以成功概率p发生k次(因此指数)。然后我们乘以相反情况:失败概率1.0 - pn - k次中发生。这使我们能够跨多次试验考虑事件发生与否的概率p

从头开始构建贝塔分布

如果你想知道如何从头开始构建贝塔分布,你将需要重新使用我们用于二项分布的factorial()函数,以及我们在第二章中构建的approximate_integral()函数。

就像我们在第一章中所做的那样,我们在感兴趣的范围内根据曲线包装矩形,如图 A-2 所示。

emds aa02

图 A-2. 包装矩形以找到面积/概率

这只是使用六个矩形;如果我们使用更多矩形,将会获得更高的准确性。让我们从头实现beta_distribution()并在 0.9 到 1.0 之间使用 1,000 个矩形进行积分,如示例 A-4 所示。

示例 A-4. 从头开始的贝塔分布
# Factorials multiply consecutive descending integers down to 1
# EXAMPLE: 5! = 5 * 4 * 3 * 2 * 1
def factorial(n: int):
    f = 1
    for i in range(n):
        f *= (i + 1)
    return f

def approximate_integral(a, b, n, f):
    delta_x = (b - a) / n
    total_sum = 0

    for i in range(1, n + 1):
        midpoint = 0.5 * (2 * a + delta_x * (2 * i - 1))
        total_sum += f(midpoint)

    return total_sum * delta_x

def beta_distribution(x: float, alpha: float, beta: float) -> float:
    if x < 0.0 or x > 1.0:
        raise ValueError("x must be between 0.0 and 1.0")

    numerator = x ** (alpha - 1.0) * (1.0 - x) ** (beta - 1.0)
    denominator = (1.0 * factorial(alpha - 1) * factorial(beta - 1)) / \
	    (1.0 * factorial(alpha + beta - 1))

    return numerator / denominator

greater_than_90 = approximate_integral(a=.90, b=1.0, n=1000,
    f=lambda x: beta_distribution(x, 8, 2))
less_than_90 = 1.0 - greater_than_90

print("GREATER THAN 90%: {}, LESS THAN 90%: {}".format(greater_than_90,
    less_than_90))

请注意,使用beta_distribution()函数时,我们提供一个给定的概率x,一个衡量成功的alpha值,以及一个衡量失败的beta值。该函数将返回观察到给定概率x的可能性。但是,要获得观察到概率x的概率,我们需要在x值范围内找到一个区域。

幸运的是,我们已经定义并准备好使用来自第二章的approximate_integral()函数。我们可以计算成功率大于 90%和小于 90%的概率,如最后几行所示。

推导贝叶斯定理

如果你想理解为什么贝叶斯定理有效而不是听信我的话,让我们进行一个思想实验。假设我有一个 10 万人口的人群。将其与我们给定的概率相乘,以得到喝咖啡的人数和患癌症的人数:

N = 100,000 P ( 喝咖啡的人 ) = .65 P ( 癌症 ) = .005 喝咖啡的人数 = 65,000 癌症患者数 = 500

我们有 65,000 名咖啡饮用者和 500 名癌症患者。现在,这 500 名癌症患者中,有多少是咖啡饮用者?我们提供了条件概率P ( 癌症|咖啡 ),我们可以将其乘以这 500 人,得到应有 425 名喝咖啡的癌症患者:

P ( 喝咖啡的人|癌症 ) = .85 喝咖啡的癌症患者数 = 500 × .85 = 425

现在,喝咖啡的人中得癌症的百分比是多少?我们需要除以哪两个数字?我们已经知道喝咖啡的人数 得癌症的人数。因此,我们将这个比例与总的喝咖啡的人数进行比较:

P ( 癌症|喝咖啡的人 ) = 喝咖啡的癌症患者数 喝咖啡的人数P ( 癌症|喝咖啡的人 ) = 425 65,000P ( 癌症|喝咖啡的人 ) = 0.006538

稍等片刻,我们是不是刚刚颠倒了条件概率?是的!我们从P ( Coffee Drinker|Cancer )开始,最终得到P ( Cancer|Coffee Drinker )。通过取两个子集(65,000 名咖啡饮用者和 500 名癌症患者),然后应用我们拥有的条件概率来应用联合概率,我们最终在我们的人口中得到了既饮咖啡又患癌症的 425 人。然后我们将这个数字除以咖啡饮用者的数量,得到患癌症的概率,假如一个人是咖啡饮用者。

但是贝叶斯定理在哪里呢?让我们专注于P ( Cancer|Coffee Drinker )表达式,并用我们先前计算的所有表达式扩展它:

P ( Cancer|Coffee Drinker ) = 100,000×P(Cancer)×P(CoffeeDrinker|Cancer) 100,000×P(CoffeeDrinker)

注意人口N为 10 万,在分子和分母中都存在,所以可以取消。这看起来现在熟悉吗?

P ( Cancer|Coffee Drinker ) = P(Cancer)×P(CoffeeDrinker|Cancer) P(CoffeeDrinker)

毫无疑问,这应该与贝叶斯定理相符!

P ( A|B ) = P(B|A)*P(B) P(A) P ( Cancer|Coffee Drinker ) = P(Cancer)×P(CoffeeDrinker|Cancer) P(CoffeeDrinker)

如果你对贝叶斯定理感到困惑或者在其背后的直觉上挣扎,尝试基于提供的概率数据获取固定人口的子集。然后你可以追踪你的方式来颠倒一个条件概率。

从头开始的 CDF 和逆 CDF

要计算正态分布的面积,我们当然可以使用我们在第一章中学到的矩形填充法,之前在附录中应用于 beta 分布。它不需要累积密度函数(CDF),而只需在概率密度函数(PDF)下填充矩形。使用这种方法,我们可以找到金毛寻回犬体重在 61 至 62 磅之间的概率,如示例 A-5 所示,使用 1,000 个填充的矩形对正态 PDF 进行估算。

示例 A-5. Python 中的正态分布函数
import math

def normal_pdf(x: float, mean: float, std_dev: float) -> float:
    return (1.0 / (2.0 * math.pi * std_dev ** 2) ** 0.5) *
      math.exp(-1.0 * ((x - mean) ** 2 / (2.0 * std_dev ** 2)))

def approximate_integral(a, b, n, f):
    delta_x = (b - a) / n
    total_sum = 0

    for i in range(1, n + 1):
        midpoint = 0.5 * (2 * a + delta_x * (2 * i - 1))
        total_sum += f(midpoint)

    return total_sum * delta_x

p_between_61_and_62 = approximate_integral(a=61, b=62, n=7,
  f= lambda x: normal_pdf(x,64.43,2.99))

print(p_between_61_and_62) # 0.0825344984983386

这将为我们提供大约 8.25%的概率,即金毛犬重量在 61 至 62 磅之间。如果我们想利用已经为我们集成且不需要任何矩形包装的 CDF,我们可以像在示例 A-6 中所示从头开始声明它。

示例 A-6. 在 Python 中使用称为ppf()的逆 CDF
import math

def normal_cdf(x: float, mean: float, std_dev: float) -> float:
    return (1 + math.erf((x - mean) / math.sqrt(2) / std_dev)) / 2

mean = 64.43
std_dev = 2.99

x = normal_cdf(66, mean, std_dev) - normal_cdf(62, mean, std_dev)

print(x)  # prints 0.49204501470628936

math.erf()被称为误差函数,通常用于计算累积分布。最后,要从头开始做逆 CDF,您需要使用名为erfinv()erf()函数的逆函数。示例 A-7 使用从头编码的逆 CDF 计算了一千个随机生成的金毛犬重量。

示例 A-7. 生成随机金毛犬重量
import random
from scipy.special import erfinv

def inv_normal_cdf(p: float, mean: float, std_dev: float):
    return mean + (std_dev * (2.0 ** 0.5) * erfinv((2.0 * p) - 1.0))

mean = 64.43
std_dev = 2.99

for i in range(0,1000):
    random_p = random.uniform(0.0, 1.0)
    print(inv_normal_cdf(random_p, mean, std_dev))

使用 e 预测随时间发生的事件概率

让我们看看您可能会发现有用的e的另一个用例。假设您是丙烷罐的制造商。显然,您不希望罐子泄漏,否则可能会在开放火焰和火花周围造成危险。测试新罐设计时,您的工程师报告说,在给定的一年中,有 5%的机会它会泄漏。

你知道这已经是一个不可接受的高数字,但你想知道这种概率如何随时间复合。现在你问自己,“在 2 年内发生泄漏的概率是多少?5 年?10 年?”随着时间的推移,暴露的时间越长,看到罐子泄漏的概率不是越来越高吗?欧拉数再次派上用场!

P leak = 1.0 - e -λT

此函数建模随时间的事件发生概率,或者在本例中是罐子在T时间后泄漏的概率。 e 再次是欧拉数,lambda λ 是每个单位时间(每年)的失效率,T 是经过的时间量(年数)。

如果我们绘制此函数,其中T是我们的 x 轴,泄漏的概率是我们的 y 轴,λ = .05,图 A-3 显示了我们得到的结果。

emds aa03

图 A-3. 预测随时间泄漏概率

这是我们在 Python 中为λ = . 05和T = 5年建模此函数的方式,参见示例 A-8。

示例 A-8. 用于预测随时间泄漏概率的代码
from math import exp

# Probability of leak in one year
p_leak = .05

# number of years
t = 5

# Probability of leak within five years
# 0.22119921692859512
p_leak_5_years = 1.0 - exp(-p_leak * t)

print("PROBABILITY OF LEAK WITHIN 5 YEARS: {}".format(p_leak_5_years))

2 年后罐子失效的概率约为 9.5%,5 年约为 22.1%,10 年约为 39.3%。随着时间的推移,罐子泄漏的可能性越来越大。我们可以将此公式推广为预测任何在给定期间内具有概率的事件,并查看该概率在不同时间段内的变化。

爬坡和线性回归

如果您发现从头开始构建机器学习中的微积分令人不知所措,您可以尝试一种更加蛮力的方法。让我们尝试一种 爬山 算法,其中我们通过添加一些随机值来随机调整 mb,进行一定次数的迭代。这些随机值将是正或负的(这将使加法操作有效地成为减法),我们只保留能够改善平方和的调整。

但我们随机生成任何数字作为调整吗?我们更倾向于较小的移动,但偶尔也许会允许较大的移动。这样,我们主要是微小的调整,但偶尔如果需要的话会进行大幅跳跃。最好的工具来做到这一点是标准正态分布,均值为 0,标准差为 1。回想一下第三章 中提到的标准正态分布在 0 附近有大量的值,而远离 0 的值(无论是负向还是正向)的概率较低,如 图 A-4 所示。

emds aa04

图 A-4. 标准正态分布中大多数值都很小且接近于 0,而较大的值在尾部出现的频率较低

回到线性回归,我们将从 0 或其他起始值开始设置 mb。然后在一个 for 循环中进行 150,000 次迭代,我们将随机调整 mb,通过添加从标准正态分布中采样的值。如果随机调整改善/减少了平方和,我们将保留它。但如果平方和增加了,我们将撤消该随机调整。换句话说,我们只保留能够改善平方和的调整。让我们在 示例 A-9 中看一看。

示例 A-9. 使用爬山算法进行线性回归
from numpy.random import normal
import pandas as pd

# Import points from CSV
points = [p for p in pd.read_csv("https://bit.ly/2KF29Bd").itertuples()]

# Building the model
m = 0.0
b = 0.0

# The number of iterations to perform
iterations = 150000

# Number of points
n = float(len(points))

# Initialize with a really large loss
# that we know will get replaced
best_loss = 10000000000000.0

for i in range(iterations):

    # Randomly adjust "m" and "b"
    m_adjust = normal(0,1)
    b_adjust = normal(0,1)

    m += m_adjust
    b += b_adjust

    # Calculate loss, which is total sum squared error
    new_loss = 0.0
    for p in points:
        new_loss += (p.y - (m * p.x + b)) ** 2

    # If loss has improved, keep new values. Otherwise revert.
    if new_loss < best_loss:
        print("y = {0}x + {1}".format(m, b))
        best_loss = new_loss
    else:
        m -= m_adjust
        b -= b_adjust

print("y = {0}x + {1}".format(m, b))

您将看到算法的进展,但最终您应该得到一个大约为 y = 1.9395722046562853x + 4.731834051245578 的拟合函数。让我们验证这个答案。当我使用 Excel 或 Desmos 进行线性回归时,Desmos 给出了 y = 1.93939x + 4.73333。还不错!我几乎接近了!

为什么我们需要一百万次迭代?通过实验,我发现这足够多的迭代次数使得解决方案不再显著改善,并且收敛到了接近于最优的 mb 值以最小化平方和。您将发现许多机器学习库和算法都有一个迭代次数的参数,它确切地做到这一点。您需要足够多的迭代次数使其大致收敛到正确的答案,但不要太多以至于在已经找到可接受的解决方案时浪费计算时间。

你可能会问为什么我将 best_loss 设置为一个极大的数字。我这样做是为了用我知道会被覆盖的值初始化最佳损失,并且它将与每次迭代的新损失进行比较,以查看是否有改进。我也可以使用正无穷 float('inf') 而不是一个非常大的数字。

爬山算法与逻辑回归

就像之前的线性回归示例一样,我们也可以将爬山算法应用于逻辑回归。如果你觉得微积分和偏导数一次性学习太过于复杂,可以再次使用这一技术。

爬山方法完全相同:用正态分布的随机值调整 mb。然而,我们确实有一个不同的目标函数,即最大似然估计,在 第六章 中讨论过。因此,我们只采用增加似然估计的随机调整,并在足够的迭代之后应该会收敛到一个拟合的逻辑回归模型。

所有这些都在 示例 A-10 中演示。

示例 A-10. 使用爬山算法进行简单的逻辑回归
import math
import random

import numpy as np
import pandas as pd

# Desmos graph: https://www.desmos.com/calculator/6cb10atg3l

points = [p for p in pd.read_csv("https://tinyurl.com/y2cocoo7").itertuples()]

best_likelihood = -10_000_000
b0 = .01
b1 = .01

# calculate maximum likelihood

def predict_probability(x):
    p = 1.0 / (1.0001 + math.exp(-(b0 + b1 * x)))
    return p

for i in range(1_000_000):

    # Select b0 or b1 randomly, and adjust it randomly
    random_b = random.choice(range(2))

    random_adjust = np.random.normal()

    if random_b == 0:
        b0 += random_adjust
    elif random_b == 1:
        b1 += random_adjust

    # Calculate total likelihood
    true_estimates = sum(math.log(predict_probability(p.x)) \
       	for p in points if p.y == 1.0)
    false_estimates = sum(math.log(1.0 - predict_probability(p.x)) \
        for p in points if p.y == 0.0)

    total_likelihood = true_estimates + false_estimates

    # If likelihood improves, keep the random adjustment. Otherwise revert.
    if best_likelihood < total_likelihood:
        best_likelihood = total_likelihood
    elif random_b == 0:
        b0 -= random_adjust
    elif random_b == 1:
        b1 -= random_adjust

print("1.0 / (1 + exp(-({0} + {1}*x))".format(b0, b1))
print("BEST LIKELIHOOD: {0}".format(math.exp(best_likelihood)))

参见 第六章 获取更多关于最大似然估计、逻辑函数以及我们为什么使用 log() 函数的详细信息。

线性规划简介

每个数据科学专业人士都应熟悉的技术是线性规划,它通过使用“松弛变量”来适应方程组来解决不等式系统。当你在线性规划系统中有离散整数或二进制变量(0 或 1)时,它被称为整数规划。当使用线性连续和整数变量时,它被称为混合整数规划

尽管比数据驱动更多地依赖算法,线性规划及其变体可用于解决广泛的经典人工智能问题。如果将线性规划系统称为人工智能听起来有些靠不住,但许多供应商和公司都将其视为一种常见做法,因为这会增加其感知价值。

在实践中,最好使用众多现有的求解器库来为你执行线性规划,但是在本节末尾将提供如何从头开始执行的资源。在这些示例中,我们将使用 PuLP,虽然 Pyomo 也是一个选择。我们还将使用图形直觉,尽管超过三个维度的问题不能轻易地进行可视化。

这是我们的例子。你有两条产品线:iPac 和 iPac Ultra。iPac 每件产品盈利 200,而iPacUltra每件产品盈利200,而 iPac Ultra 每件产品盈利 300。

然而,装配线只能工作 20 小时,而制造 iPac 需要 1 小时,制造 iPac Ultra 需要 3 小时。

一天只能提供 45 套装备,而 iPac 需要 6 套,而 iPac Ultra 需要 2 套。

假设所有供应都将被销售,我们应该销售多少个 iPac 和 iPac Ultra 以实现利润最大化?

让我们首先注意第一个约束条件并将其拆分:

…装配线只能工作 20 小时,生产一个 iPac 需要 1 小时,生产一个 iPac Ultra 需要 3 小时。

我们可以将其表示为一个不等式,其中x是 iPac 单元的数量,y是 iPac Ultra 单元的数量。两者必须为正,并且图 A-5 显示我们可以相应地绘制图形。

x + 3 y ≤ 20 ( x ≥ 0 , y ≥ 0 )emds aa05

图 A-5. 绘制第一个约束条件

现在让我们看看第二个约束条件:

一天只能提供 45 个套件,其中 iPac 需要 6 个套件,而 iPac Ultra 需要 2 个套件。

我们还可以根据图 A-6 进行建模和绘图。

6 x + 2 y ≤ 45 ( x ≥ 0 , y ≥ 0 )emds aa06

图 A-6. 绘制第二个约束条件

注意在图 A-6 中,现在这两个约束条件之间存在重叠。我们的解决方案位于该重叠区域内,我们称之为可行区域。最后,我们正在最大化我们的利润Z,下面给出了 iPac 和 iPac Ultra 的利润金额。

Z = 200 x + 300 y

如果我们将这个函数表示为一条线,我们可以尽可能地增加Z,直到这条线不再位于可行区域内。然后我们注意在图 A-7 中可视化的 x 和 y 值。

Objective Function 的 Desmos 图

如果您需要以更交互式和动画的方式看到这个图形,请查看Desmos 上的这张图

emds aa07

图 A-7. 将我们的目标线增加到不再位于可行区域内

当利润Z尽可能地增加时,当那条线“刚好触及”可行区域时,您将落在可行区域的一个顶点或角上。该顶点提供了将最大化利润所需的 x 和 y 值,如图 A-8 所示。

尽管我们可以使用 NumPy 和一堆矩阵运算来进行数值求解,但使用 PuLP 会更容易,如示例 A-11 所示。请注意,LpVariable定义了要解决的变量。LpProblem是线性规划系统,使用 Python 操作符添加约束和目标函数。然后通过在LpProblem上调用solve()来求解变量。

emds aa08

A-8. 线性规划系统的最大化目标
A-11. 使用 Python PuLP 解决线性规划系统
# GRAPH" https://www.desmos.com/calculator/iildqi2vt7

from pulp import *

# declare your variables
x = LpVariable("x", 0)   # 0<=x
y = LpVariable("y", 0) # 0<=y

# defines the problem
prob = LpProblem("factory_problem", LpMaximize)

# defines the constraints
prob += x + 3*y <= 20
prob += 6*x +2*y <= 45

# defines the objective function to maximize
prob += 200*x + 300*y

# solve the problem
status = prob.solve()
print(LpStatus[status])

# print the results x = 5.9375, y = 4.6875
print(value(x))
print(value(y))

也许你会想知道是否有意义构建 5.9375 和 4.6875 单位。如果您的变量能容忍连续值,线性规划系统会更高效,也许您可以在之后对它们进行四舍五入处理。但某些类型的问题绝对需要处理整数和二进制变量。

要将xy变量强制为整数,可以使用cat=LpInteger参数,如示例 A-12 所示。

A-12. 强制变量作为整数解决
# declare your variables
x = LpVariable("x", 0, cat=LpInteger) # 0<=x
y = LpVariable("y", 0, cat=LpInteger) # 0<=y

从图形上来看,这意味着我们用离散点填充我们的可行区域,而不是连续的区域。我们的解决方案不一定会落在一个顶点上,而是接近顶点的点,如图 A-9 所示。

线性规划中有一些特殊情况,如图 A-10 所示。有时可能有多个解决方案。有时可能根本没有解决方案。

这只是线性规划的一个快速介绍示例,不幸的是,这本书里没有足够的空间来充分讨论这个话题。它可以用于一些出人意料的问题,包括调度受限资源(如工人、服务器任务或房间)、解决数独和优化金融投资组合。

emds aa09

A-9. 离散线性规划系统

emds aa10

A-10. 线性规划的特殊情况

如果您想了解更多,有一些很好的 YouTube 视频,包括PatrickJMTJosh Emmanuel。如果您想深入研究离散优化,Pascal Van Hentenryck 教授在 Coursera 上组织了一门极好的课程,请点击这里

使用 scikit-learn 进行 MNIST 分类器

示例 A-13 展示了如何使用 scikit-learn 的神经网络进行手写数字分类。

A-13. scikit-learn 中的手写数字分类器神经网络示例
import numpy as np
import pandas as pd
# load data
from sklearn.model_selection import train_test_split
from sklearn.neural_network import MLPClassifier

df = pd.read_csv('https://bit.ly/3ilJc2C', compression='zip', delimiter=",")

# Extract input variables (all rows, all columns but last column)
# Note we should do some linear scaling here
X = (df.values[:, :-1] / 255.0)

# Extract output column (all rows, last column)
Y = df.values[:, -1]

# Get a count of each group to ensure samples are equitably balanced
print(df.groupby(["class"]).agg({"class" : [np.size]}))

# Separate training and testing data
# Note that I use the 'stratify' parameter to ensure
# each class is proportionally represented in both sets
X_train, X_test, Y_train, Y_test = train_test_split(X, Y,
    test_size=.33, random_state=10, stratify=Y)

nn = MLPClassifier(solver='sgd',
                   hidden_layer_sizes=(100, ),
                   activation='logistic',
                   max_iter=480,
                   learning_rate_init=.1)

nn.fit(X_train, Y_train)

print("Training set score: %f" % nn.score(X_train, Y_train))
print("Test set score: %f" % nn.score(X_test, Y_test))

# Display heat map
import matplotlib.pyplot as plt
fig, axes = plt.subplots(4, 4)

# use global min / max to ensure all weights are shown on the same scale
vmin, vmax = nn.coefs_[0].min(), nn.coefs_[0].max()
for coef, ax in zip(nn.coefs_[0].T, axes.ravel()):
    ax.matshow(coef.reshape(28, 28), cmap=plt.cm.gray, vmin=.5 * vmin, vmax=.5 * vmax)
    ax.set_xticks(())
    ax.set_yticks(())

plt.show()

附录 B. 练习答案

第一章

  1. 62.6738 是有理数,因为它的小数位数有限,可以表示为分数 626738 / 10000。

  2. 10 7 10 -5 = 10 7+-5 = 10 2 = 100

  3. 81 1 2 = ( 81 ) = 9

  4. 25 3 2 = (25 1/2 ) 3 = 5 3 = 125

  5. 结果金额为$1,161.47。Python 脚本如下:

    from math import exp
    
    p = 1000
    r = .05
    t = 3
    n = 12
    
    a = p * (1 + (r/n))**(n * t)
    
    print(a) # prints 1161.4722313334678
    
  6. 结果金额为$1161.83。Python 脚本如下:

    from math import exp
    
    p = 1000 # principal, starting amount
    r = .05 # interest rate, by year
    t = 3.0 # time, number of years
    
    a = p * exp(r*t)
    
    print(a) # prints 1161.834242728283
    
  7. 导数计算为 6x,这使得x=3 时的斜率为 18。SymPy 代码如下:

    from sympy import *
    
    # Declare 'x' to SymPy
    x = symbols('x')
    
    # Now just use Python syntax to declare function
    f = 3*x**2 + 1
    
    # Calculate the derivative of the function
    dx_f = diff(f)
    print(dx_f) # prints 6*x
    print(dx_f.subs(x,3)) # 18
    
  8. 曲线在 0 到 2 之间的面积为 10。SymPy 代码如下:

    from sympy import *
    
    # Declare 'x' to SymPy
    x = symbols('x')
    
    # Now just use Python syntax to declare function
    f = 3*x**2 + 1
    
    # Calculate the integral of the function with respect to x
    # for the area between x = 0 and 2
    area = integrate(f, (x, 0, 2))
    
    print(area) # prints 10
    

第二章

  1. 0.3 × 0.4 = 0.12;参见“条件概率和贝叶斯定理”。

  2. (1 - 0.3) + 0.4 - (.03 × 0.4) = 0.98;参见“联合概率”,请记住我们在寻找没有雨,因此从 1.0 中减去该概率。

  3. 0.3 × 0.2 = 0.06;参见“条件概率和贝叶斯定理”。

  4. 以下 Python 代码计算出 0.822 的答案,将 50 名以上未出现乘客的概率相加:

    from scipy.stats import binom
    
    n = 137
    p = .40
    
    p_50_or_more_noshows = 0.0
    
    for x in range(50,138):
        p_50_or_more_noshows += binom.pmf(x, n, p)
    
    print(p_50_or_more_noshows) # 0.822095588147425
    
  5. 使用 SciPy 中显示的 Beta 分布,获取到 0.5 的面积并从 1.0 中减去。结果约为 0.98,因此这枚硬币极不可能是公平的。

    from scipy.stats import beta
    
    heads = 8
    tails = 2
    
    p = 1.0 - beta.cdf(.5, heads, tails)
    
    print(p) # 0.98046875
    

第三章

  1. 平均值为 1.752,标准差约为 0.02135。Python 代码如下:

    from math import sqrt
    
    sample = [1.78, 1.75, 1.72, 1.74, 1.77]
    
    def mean(values):
        return sum(values) /len(values)
    
    def variance_sample(values):
        mean = sum(values) / len(values)
        var = sum((v - mean) ** 2 for v in values) / len(values)
        return var
    
    def std_dev_sample(values):
        return sqrt(variance_sample(values))
    
    mean = mean(sample)
    std_dev = std_dev_sample(sample)
    
    print("MEAN: ", mean) # 1.752
    print("STD DEV: ", std_dev) # 0.02135415650406264
    
  2. 使用 CDF 获取 30 到 20 个月之间的值,大约是 0.06 的面积。Python 代码如下:

    from scipy.stats import norm
    
    mean = 42
    std_dev = 8
    
    x = norm.cdf(30, mean, std_dev) - norm.cdf(20, mean, std_dev)
    
    print(x) # 0.06382743803380352
    
  3. 有 99%的概率,卷筒的平均丝径在 1.7026 到 1.7285 之间。Python 代码如下:

    from math import sqrt
    from scipy.stats import norm
    
    def critical_z_value(p, mean=0.0, std=1.0):
        norm_dist = norm(loc=mean, scale=std)
        left_area = (1.0 - p) / 2.0
        right_area = 1.0 - ((1.0 - p) / 2.0)
        return norm_dist.ppf(left_area), norm_dist.ppf(right_area)
    
    def ci_large_sample(p, sample_mean, sample_std, n):
        # Sample size must be greater than 30
    
        lower, upper = critical_z_value(p)
        lower_ci = lower * (sample_std / sqrt(n))
        upper_ci = upper * (sample_std / sqrt(n))
    
        return sample_mean + lower_ci, sample_mean + upper_ci
    
    print(ci_large_sample(p=.99, sample_mean=1.715588,
        sample_std=0.029252, n=34))
    # (1.7026658973748656, 1.7285101026251342)
    
  4. 营销活动的 p 值为 0.01888。Python 代码如下:

    from scipy.stats import norm
    
    mean = 10345
    std_dev = 552
    
    p1 = 1.0 - norm.cdf(11641, mean, std_dev)
    
    # Take advantage of symmetry
    p2 = p1
    
    # P-value of both tails
    # I could have also just multiplied by 2
    p_value = p1 + p2
    
    print("Two-tailed P-value", p_value)
    if p_value <= .05:
        print("Passes two-tailed test")
    else:
        print("Fails two-tailed test")
    
    # Two-tailed P-value 0.01888333596496139
    # Passes two-tailed test
    

第四章

  1. 向量落在[2, 3]。Python 代码如下:

    from numpy import array
    
    v = array([1,2])
    
    i_hat = array([2, 0])
    j_hat = array([0, 1.5])
    
    # fix this line
    basis = array([i_hat, j_hat])
    
    # transform vector v into w
    w = basis.dot(v)
    
    print(w) # [2\. 3.]
    
  2. 向量落在[0, -3]。Python 代码如下:

    from numpy import array
    
    v = array([1,2])
    
    i_hat = array([-2, 1])
    j_hat = array([1, -2])
    
    # fix this line
    basis = array([i_hat, j_hat])
    
    # transform vector v into w
    w = basis.dot(v)
    
    print(w) # [ 0, -3]
    
  3. 行列式为 2.0。Python 代码如下:

    import numpy as np
    from numpy.linalg import det
    
    i_hat = np.array([1, 0])
    j_hat = np.array([2, 2])
    
    basis = np.array([i_hat,j_hat]).transpose()
    
    determinant = det(basis)
    
    print(determinant) # 2.0
    
  4. 是的,因为矩阵乘法允许我们将多个矩阵合并为一个表示单一转换的矩阵。

  5. x = 19.8,y = –5.4,z = –6。代码如下:

    from numpy import array
    from numpy.linalg import inv
    
    A = array([
        [3, 1, 0],
        [2, 4, 1],
        [3, 1, 8]
    ])
    
    B = array([
        54,
        12,
        6
    ])
    
    X = inv(A).dot(B)
    
    print(X) # [19.8 -5.4 -6\. ]
    
  6. 是的,它是线性相关的。尽管在 NumPy 中存在一些浮点不精确性,行列式有效地为 0。

    from numpy.linalg import det
    from numpy import array
    
    i_hat = array([2, 6])
    j_hat = array([1, 3])
    
    basis = array([i_hat, j_hat]).transpose()
    print(basis)
    
    determinant = det(basis)
    
    print(determinant) # -3.330669073875464e-16
    

    为了解决浮点问题,使用 SymPy,你将得到 0:

    from sympy import *
    
    basis = Matrix([
        [2,1],
        [6,3]
    ])
    
    determinant = det(basis)
    
    print(determinant) # 0
    

第五章

  1. 有很多工具和方法可以执行线性回归,就像我们在第五章中学到的一样,但是这里使用 scikit-learn 进行解决。斜率是 1.75919315,截距是 4.69359655。

    import pandas as pd
    import matplotlib.pyplot as plt
    from sklearn.linear_model import LinearRegression
    
    # Import points
    df = pd.read_csv('https://bit.ly/3C8JzrM', delimiter=",")
    
    # Extract input variables (all rows, all columns but last column)
    X = df.values[:, :-1]
    
    # Extract output column (all rows, last column)
    Y = df.values[:, -1]
    
    # Fit a line to the points
    fit = LinearRegression().fit(X, Y)
    
    # m = 1.75919315, b = 4.69359655
    m = fit.coef_.flatten()
    b = fit.intercept_.flatten()
    print("m = {0}".format(m))
    print("b = {0}".format(b))
    
    # show in chart
    plt.plot(X, Y, 'o') # scatterplot
    plt.plot(X, m*X+b) # line
    plt.show()
    
  2. 我们得到高达 0.92421 的相关性和测试值 23.8355,显著统计范围为±1.9844。这种相关性绝对有用且统计上显著。代码如下:

    import pandas as pd
    
    # Read data into Pandas dataframe
    df = pd.read_csv('https://bit.ly/3C8JzrM', delimiter=",")
    
    # Print correlations between variables
    correlations = df.corr(method='pearson')
    print(correlations)
    
    # OUTPUT:
    #          x        y
    # x  1.00000  0.92421
    # y  0.92421  1.00000
    
    # Test for statistical significance
    from scipy.stats import t
    from math import sqrt
    
    # sample size
    n = df.shape[0]
    print(n)
    lower_cv = t(n - 1).ppf(.025)
    upper_cv = t(n - 1).ppf(.975)
    
    # retrieve correlation coefficient
    r = correlations["y"]["x"]
    
    # Perform the test
    test_value = r / sqrt((1 - r ** 2) / (n - 2))
    
    print("TEST VALUE: {}".format(test_value))
    print("CRITICAL RANGE: {}, {}".format(lower_cv, upper_cv))
    
    if test_value < lower_cv or test_value > upper_cv:
        print("CORRELATION PROVEN, REJECT H0")
    else:
        print("CORRELATION NOT PROVEN, FAILED TO REJECT H0 ")
    
    # Calculate p-value
    if test_value > 0:
        p_value = 1.0 - t(n - 1).cdf(test_value)
    else:
        p_value = t(n - 1).cdf(test_value)
    
    # Two-tailed, so multiply by 2
    p_value = p_value * 2
    print("P-VALUE: {}".format(p_value))
    
    """
    TEST VALUE: 23.835515323677328
    CRITICAL RANGE: -1.9844674544266925, 1.984467454426692
    CORRELATION PROVEN, REJECT H0
    P-VALUE: 0.0 (extremely small)
    """
    
  3. 在x = 50时,预测区间在 50.79 到 134.51 之间。代码如下:

    import pandas as pd
    from scipy.stats import t
    from math import sqrt
    
    # Load the data
    points = list(pd.read_csv('https://bit.ly/3C8JzrM', delimiter=",") \
        .itertuples())
    
    n = len(points)
    
    # Linear Regression Line
    m = 1.75919315
    b = 4.69359655
    
    # Calculate Prediction Interval for x = 50
    x_0 = 50
    x_mean = sum(p.x for p in points) / len(points)
    
    t_value = t(n - 2).ppf(.975)
    
    standard_error = sqrt(sum((p.y - (m * p.x + b)) ** 2 for p in points) / \
        (n - 2))
    
    margin_of_error = t_value * standard_error * \
                      sqrt(1 + (1 / n) + (n * (x_0 - x_mean) ** 2) / \
                           (n * sum(p.x ** 2 for p in points) - \
    	sum(p.x for p in points) ** 2))
    
    predicted_y = m*x_0 + b
    
    # Calculate prediction interval
    print(predicted_y - margin_of_error, predicted_y + margin_of_error)
    # 50.792086501055955 134.51442159894404
    
  4. 将测试数据集分成三分,并使用 k-fold(其中k = 3)评估时表现还不错。在这三个数据集中,均方误差大约为 0.83,标准偏差为 0.03。

    import pandas as pd
    from sklearn.linear_model import LinearRegression
    from sklearn.model_selection import KFold, cross_val_score
    
    df = pd.read_csv('https://bit.ly/3C8JzrM', delimiter=",")
    
    # Extract input variables (all rows, all columns but last column)
    X = df.values[:, :-1]
    
    # Extract output column (all rows, last column)\
    Y = df.values[:, -1]
    
    # Perform a simple linear regression
    kfold = KFold(n_splits=3, random_state=7, shuffle=True)
    model = LinearRegression()
    results = cross_val_score(model, X, Y, cv=kfold)
    print(results)
    print("MSE: mean=%.3f (stdev-%.3f)" % (results.mean(), results.std()))
    """
    [0.86119665 0.78237719 0.85733887]
    MSE: mean=0.834 (stdev-0.036)
    """
    

第六章

  1. 当你在 scikit-learn 中运行这个算法时,准确率非常高。我运行时,平均在测试折叠上至少获得 99.9%的准确率。

    import pandas as pd
    from sklearn.linear_model import LogisticRegression
    from sklearn.metrics import confusion_matrix
    from sklearn.model_selection import KFold, cross_val_score
    
    # Load the data
    df = pd.read_csv("https://bit.ly/3imidqa", delimiter=",")
    
    X = df.values[:, :-1]
    Y = df.values[:, -1]
    
    kfold = KFold(n_splits=3, shuffle=True)
    model = LogisticRegression(penalty='none')
    results = cross_val_score(model, X, Y, cv=kfold)
    
    print("Accuracy Mean: %.3f (stdev=%.3f)" % (results.mean(),
    results.std()))
    
  2. 混淆矩阵将产生大量的真阳性和真阴性,以及很少的假阳性和假阴性。运行这段代码,你会看到:

    import pandas as pd
    from sklearn.linear_model import LogisticRegression
    from sklearn.metrics import confusion_matrix
    from sklearn.model_selection import train_test_split
    
    # Load the data
    df = pd.read_csv("https://bit.ly/3imidqa", delimiter=",")
    
    # Extract input variables (all rows, all columns but last column)
    X = df.values[:, :-1]
    
    # Extract output column (all rows, last column)\
    Y = df.values[:, -1]
    
    model = LogisticRegression(solver='liblinear')
    
    X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=.33)
    model.fit(X_train, Y_train)
    prediction = model.predict(X_test)
    
    """
    The confusion matrix evaluates accuracy within each category.
    [[truepositives falsenegatives]
     [falsepositives truenegatives]]
    
    The diagonal represents correct predictions,
    so we want those to be higher
    """
    matrix = confusion_matrix(y_true=Y_test, y_pred=prediction)
    print(matrix)
    
  3. 下面展示了一个用于测试用户输入颜色的交互式 shell。考虑测试黑色(0,0,0)和白色(255,255,255),看看是否能正确预测暗色和浅色字体。

    import pandas as pd
    from sklearn.linear_model import LogisticRegression
    import numpy as np
    from sklearn.model_selection import train_test_split
    
    # Load the data
    df = pd.read_csv("https://bit.ly/3imidqa", delimiter=",")
    
    # Extract input variables (all rows, all columns but last column)
    X = df.values[:, :-1]
    
    # Extract output column (all rows, last column)
    Y = df.values[:, -1]
    
    model = LogisticRegression(solver='liblinear')
    
    X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=.33)
    model.fit(X_train, Y_train)
    prediction = model.predict(X_test)
    
    # Test a prediction
    while True:
        n = input("Input a color {red},{green},{blue}: ")
        (r, g, b) = n.split(",")
        x = model.predict(np.array([[int(r), int(g), int(b)]]))
        if model.predict(np.array([[int(r), int(g), int(b)]]))[0] == 0.0:
            print("LIGHT")
        else:
            print("DARK")
    
  4. 是的,逻辑回归在预测给定背景颜色的浅色或暗色字体方面非常有效。准确率不仅极高,而且混淆矩阵在主对角线的右上到左下有很高的数字,其他单元格的数字则较低。

第七章

显然,你可以尝试不同的隐藏层、激活函数、不同大小的测试数据集等进行大量的实验和尝试。我尝试使用一个有三个节点的隐藏层,使用 ReLU 激活函数,但在我的测试数据集上难以得到良好的预测。混淆矩阵和准确率一直很差,我运行的任何配置更是表现不佳。

神经网络可能失败的原因有两个:1)测试数据集对于神经网络来说太小(神经网络对数据需求极大),2)对于这类问题,像逻辑回归这样更简单和更有效的模型存在。这并不是说你不能找到一个适用的配置,但你必须小心,避免通过 p-hack 的方式使得结果过拟合到你所拥有的少量训练和测试数据。

这是我使用的 scikit-learn 代码:

import pandas as pd
# load data
from sklearn.metrics import confusion_matrix
from sklearn.model_selection import train_test_split
from sklearn.neural_network import MLPClassifier

df = pd.read_csv('https://tinyurl.com/y6r7qjrp', delimiter=",")

# Extract input variables (all rows, all columns but last column)
X = df.values[:, :-1]

# Extract output column (all rows, last column)
Y = df.values[:, -1]

# Separate training and testing data
X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=1/3)

nn = MLPClassifier(solver='sgd',
                   hidden_layer_sizes=(3, ),
                   activation='relu',
                   max_iter=100_000,
                   learning_rate_init=.05)

nn.fit(X_train, Y_train)

print("Training set score: %f" % nn.score(X_train, Y_train))
print("Test set score: %f" % nn.score(X_test, Y_test))

print("Confusion matrix:")
matrix = confusion_matrix(y_true=Y_test, y_pred=nn.predict(X_test))
print(matrix)