Python-数据科学本质论-三-

83 阅读1小时+

Python 数据科学本质论(三)

原文:Python Data Science Essentials

协议:CC BY-NC-SA 4.0

五、可视化,见解和结果

在探索了机器学习之后,但并不是因为这个主题比其他主题没有那么重要,所以我们将说明如何使用 Python 创建可视化来丰富您的数据科学项目。 可视化在帮助您交流从数据和学习过程中获得的结果和见解方面发挥着重要作用。

在本章中,您将学习如何执行以下操作:

  • 使用matplotlib包中的基本pyplot函数
  • 利用 Pandas DataFrame进行探索性数据分析EDA
  • 与 Seaborn 一起创建漂亮的交互式图表
  • 可视化我们在第 3 章,“数据管道”和第 4 章,“机器学习”中讨论的 的机器学习和优化过程
  • 了解并以视觉方式传达变量的重要性及其与目标结果的关系
  • 设置使用 HTTP 接受并提供预测作为服务的预测服务器

Matplotlib 的基础知识介绍

可视化是数据科学的基本方面,它使数据科学家可以更好,更有效地将其发现传达给他们所从事的组织,数据专家和非专家。 提供信息交流和精心制作引人入胜的可视化效果背后的原理的基本知识超出了本书的范围,但是如果您想提高自己的技能,我们可以建议合适的资源。

对于基本的可视化规则,您可以访问这个页面。 我们还推荐 Edward Tufte 教授有关分析设计和可视化的书。

取而代之的是,我们可以提供一系列快速,重要的基本秘籍,使您可以开始使用 Python 进行可视化,并且可以在需要创建特定图形图表时随时参考。 将所有代码片段都视为可视化构建块; 您只需使用我们将为您提供的大量参数,就可以为它们安排不同的配置和功能。

matplotlib是用于绘制图形的 Python 包。 它是由约翰·亨特(John Hunter)创建的,旨在解决 Python 与具有图形功能(例如 MATLAB 或 gnuplot)的外部软件之间缺乏集成的问题。 matplotlib在很大程度上受到 MATLAB 的操作方式和功能的影响,其语法非常相似。 特别是,与 MATLAB 完全兼容的matplotlib.pyplot模块将成为我们介绍所有必要的图形工具以表示数据和分析的基本介绍的核心。 MATLAB 确实是数据分析和科学界可视化的标准,因为它在探索性分析方面具有公认的功能,这主要是因为它具有平滑且易于使用的绘图功能。

每个pyplot命令都会对最初实例化的图形进行更改。 设置图形后,所有其他命令都将在该图形上运行。 因此,很容易逐步改进和丰富您的图形表示。 为了使您能够利用代码并能够根据需要进行个性化设置,以下所有示例均与注释的构建块一起提供,以便您以后可以起草基本表示形式,然后在本章中查找特定的参数。 在示例中,以按计划改进图表。

使用pyplot.figure()命令,您可以初始化新的可视化效果,尽管它足以调用绘图命令来自动启动它。 相反,通过使用pyplot.show(),您可以关闭正在使用的图形,然后可以打开并使用新图形。

在开始一些可视化示例之前,让我们导入必要的包以运行所有示例:

In: import numpy as np
    import matplotlib.pyplot as plt
    import matplotlib as mpl  

这样,我们始终可以将 MATLAB 风格的模块pyplot称为plt,并在mpl的帮助下访问完整的matplotlib函数集。

如果您正在使用 Jupyter 笔记本(或 Jupyter Lab),则可以使用以下行魔术:%matplotlib内联。 在笔记本的单元格中写入命令并运行命令后,您可以直接在笔记本本身上绘制绘图,而不必在单独的窗口中显示图形(默认情况下,matplotlib的 GUI 后端是TkAgg后端)。 如果您更喜欢 Qt 等通常随 Python 科学发行版一起分发的后端,则只需运行以下行魔术:%matplotlib Qt。

尝试曲线图

我们的第一个问题将要求您使用pyplot绘制函数。 绘制函数非常简单; 您只需要获取一系列x坐标,然后使用要绘制的函数将它们映射到y轴即可。 由于映射结果存储在两个向量中,因此plot函数将处理曲线表示。 如果映射的点足够多(50 个点是一个好的采样数),则表示的精度将更高:

In: import numpy as np
    import matplotlib.pyplot as plt
    x = np.linspace(0, 5, 50)
    y_cos = np.cos(x)
    y_sin = np.sin(x)   

使用 NumPy linspace()函数,我们将创建一系列 50 个等距的数字,范围从 0 到 5。我们可以使用它们将y映射到余弦和正弦函数:

In: plt.figure() # initialize a figure
    plt.plot(x,y_cos) # plot series of coordinates as a line
    plt.plot(x,y_sin)
    plt.xlabel('x') # adds label to x axis
    plt.ylabel('y') # adds label to y axis
    plt.title('title') # adds a title
    plt.show() # close a figure  

这是您的第一个绘图:

pyplot.plot命令可以在一个序列中绘制更多曲线,每个曲线根据内部颜色模式使用不同的颜色,可以通过阐述喜欢的颜色序列进行自定义。 为此,您必须操作包含matplotlib使用的颜色序列的列表:

In: list(mpl.rcParams['axes.prop_cycle']) 

Out: [{'color': '#1f77b4'},
      {'color': '#ff7f0e'},
      {'color': '#2ca02c'},
      {'color': '#d62728'},
      {'color': '#9467bd'},
      {'color': '#8c564b'},
      {'color': '#e377c2'},
      {'color': '#7f7f7f'},
      {'color': '#bcbd22'},
      {'color': '#17becf'}]

#1f77b4, #ff7f0e, #2ca02c, 以及所有其他颜色均为以十六进制形式表示的颜色。 为了弄清楚它们的外观,您可以使用 colorhexa 网站,为您提供有关它们的有用信息

可以通过使用cycler函数并向其提供一串字符串名称来完成该黑客操作,这些字符串名称涉及您要依次使用的颜色:

In: mpl.rcParams['axes.prop_cycle'] = mpl.cycler('color', 
 ['blue', 'red', 'green'])

此外,plot命令(如果未提供任何其他信息)将假定您要绘制一条线。 因此,它将链接曲线中所有提供的点。 如果添加新参数,例如'.' –即plt.plot(x,y_cos,'.') –,则表示您想绘制一系列分离的点(直线的字符串为'-',但是我们将很快显示另一个示例 )。

这样,如果您按照先前的建议自定义了rcParams['axes.prop_cycle'],则下一个图形将首先具有蓝色曲线,然后第二个图形将具有红色曲线,第三个图形将具有绿色曲线。 然后,色彩循环将重新开始。 我们将这个决定留给您。 本章中的所有示例都将遵循标准的颜色顺序,但是您可以随意尝试使用更好的颜色设置。

请注意,您还可以设置图形标题,并用标题pyplot中的xlabelylabel标记轴。

使用面板的更清晰表示

我们的第二个示例将向您演示如何创建多个图形面板并在每个图形面板上绘制表示形式。 我们还将尝试通过使用不同的颜色,大小和样式来个性化绘制的曲线。 这是示例:

In: import matplotlib.pyplot as plt
    # defines 1 row 2 column panel, activates figure 1
    plt.subplot(1,2,1) 
    plt.plot(x,y_cos,'r--')
    # adds a title
    plt.title('cos')
    # defines 1 row 2 column panel, activates figure 2
    plt.subplot(1,2,2) 
    plt.plot(x,y_sin,'b-')
    plt.title('sin') 
    plt.show()

该图在两个不同的图形面板上显示余弦和正弦曲线:

subplot命令接受subplot(nrows, ncols, plot_number)参数形式。 因此,在实例化时,它会基于nrowsncols参数以及plot_number区域(从左侧的1区域开始)的绘图数量为表示保留一定的空间。

您还可以将plot命令坐标与另一个字符串参数一起使用,这对于定义颜色和所表示曲线的类型很有用。 字符串通过组合您可以在以下链接上找到的代码来工作:

  • matplotlib.lines.Line2D.set_linestyle:将显示不同的线型。
  • 颜色 API:提供基本内置颜色的完整概述。 该页面还指出,您可以将color参数与 HTML 名称或十六进制字符串一起用于颜色,也可以通过使用 RGB 元组来定义所需的颜色,其中元组的每个值都在[0,1]。 例如,有效参数为color = (0.1,0.9,0.9),它将创建由 10% 红色,90% 绿色和 90% 蓝色构成的颜色。
  • 标记 API:列出您可以为点采用的所有可能的标记样式。

为数据关系绘制散点图

散点图将两个变量绘制为平面上的点,它们可以帮助您找出两个变量之间的关系。 如果要表示组和集群,它们也非常有效。 在我们的示例中,我们将创建三个数据集群,并在散点图中以不同的形状和颜色表示它们:

In: from sklearn.datasets import make_blobs
    import matplotlib.pyplot as plt
    D = make_blobs(n_samples=100, n_features=2, 
                   centers=3, random_state=7)
    groups = D[1]
    coordinates = D[0]  

由于必须绘制三个不同的组,因此必须使用三个不同的plot命令。 每个命令指定不同的颜色和形状('ys''m*''rD'字符串,其中第一个字母是颜色,第二个字母是标记)。 另请注意,每个绘图实例均由label参数标记,该参数用于为稍后在图例中报告的组分配名称:

In: plt.plot(coordinates[groups==0,0], 
             coordinates[groups==0,1], 
             'ys', label='group 0') # yellow square
    plt.plot(coordinates[groups==1,0], 
             coordinates[groups==1,1], 
             'm*', label='group 1') # magenta stars
    plt.plot(coordinates[groups==2,0], 
             coordinates[groups==2,1], 
             'rD', label='group 2') # red diamonds
    plt.ylim(-2,10) # redefines the limits of y axis
    plt.yticks([10,6,2,-2]) # redefines y axis ticks
    plt.xticks([-15,-5,5,-15]) # redefines x axis ticks
    plt.grid() # adds a grid
    plt.annotate('Squares', (-12,2.5)) # prints text at coordinates
    plt.annotate('Stars', (0,6))
    plt.annotate('Diamonds', (10,3))
    plt.legend(loc='lower left', numpoints= 1) 
    # places a legend of labelled items
    plt.show()

结果图将是三个组的散点图,并附有它们各自的标签:

我们还添加了一个图例(pyplot.legend),固定了两个轴(pyplot.xlimpyplot ylim)的限制,并精确地说明了必须放在它们上的刻度线(plt.xticksplt.yticks) 通过指定值列表。 因此,网格(pyplot.grid)将图精确地划分为 9 个象限,使您可以更好地了解组的位置。 最后,我们打印了一些文本,指出了组名(pyplot.annotate)。

直方图

直方图可以有效地表示变量的分布。 在这里,我们将可视化两个正态分布,均以单位标准差为特征,一个均值为0,另一个均值为3.0

In: import numpy as np
    import matplotlib.pyplot as plt
    x = np.random.normal(loc=0.0, scale=1.0, size=500)
    z = np.random.normal(loc=3.0, scale=1.0, size=500)
    plt.hist(np.column_stack((x,z)), 
             bins=20, 
             histtype='bar', 
             color = ['c','b'], 
             stacked=True)
    plt.grid()
    plt.show()

如果存在分类问题,则联合分布可以对数据提供不同的见解:

有几种方法可以个性化此类图并获得有关分析分布的进一步见解。 首先,通过更改箱子的数量,您将更改分布的离散方式(离散是将连续函数或一系列值转换为可减少的,可数的一组数字的过程)。 通常,10 到 20 个箱子可以很好地理解分布,尽管它实际上取决于数据集的大小以及分布。 例如,Freedman-Diaconis 规则规定,直方图中要有效地可视化数据的最佳箱子数量取决于箱子的宽度,可以使用四分位间距IQR)和观察次数:

在计算了h(箱宽)后,通过将最大值和最小值之间的差除以h来计算箱数:

bins = (max-min) / h

通过将参数从histtype='bar'更改为histtype='step',我们还可以将可视化类型从条形更改为阶梯形。 通过将stacked布尔参数更改为False,曲线将不会重叠到重叠部分中的唯一条形中,但是您会清楚地看到每个单独的条形。

条形图

条形图对于比较不同类别的数量很有用。 它们可以水平或垂直排列以呈现平均估计和误差带。 它们可用于显示预测变量的各种统计数据以及它们与目标变量的关系。

在我们的示例中,我们将给出鸢尾花数据集的四个变量的平均值和标准差:

In: from sklearn.datasets import load_iris 
 import numpy as np 
 import matplotlib.pyplot as plt 
 iris = load_iris() 
 average = np.mean(iris.data, axis=0) 
 std = np.std(iris.data, axis=0) 
 range_ = range(np.shape(iris.data)[1]) 

在我们的表示中,我们将准备两个子图:一个带有水平条(plt.barh),另一个带有垂直条(plt.bar)。 标准误差由误差条表示,根据图形方向,我们可以将xerr参数用于水平条并将yerr参数用于垂直条:

In: plt.subplot(1,2,1) # defines 1 row, 2 columns panel, activates figure 1
    plt.title('Horizontal bars')
    plt.barh(range_,average, color="r", 
             xerr=std, alpha=0.4, align="center")
    plt.yticks(range_, iris.feature_names)
    plt.subplot(1,2,2) # defines 1 row 2 column panel, activates figure 2
    plt.title('Vertical bars')
    plt.bar(range_,average, color="b", yerr=std, alpha=0.4, align="center")
    plt.xticks(range_, range_)
    plt.show()

现在,水平和垂直条都在同一图中:

重要的是注意使用plt.xticks命令(纵坐标轴使用plt.yticks)。 第一个参数告知命令必须在轴上放置的刻度数,第二个参数说明必须放置在刻度上的标签。

注意的另一个有趣参数是alpha,它已用于设置条的透明度级别。 alpha参数是一个从 0.0(完全透明)到 1.0 的浮点数,它使颜色以不同的不透明度级别显示。

图像可视化

我们使用matplotlib探索的最后一个可视化与图像有关。 在处理图像数据时,求助于plt.imgshow很有用。 让我们以 Olivetti 数据集为例,该数据集是一个由 40 个人组成的开源图像集,这些人在不同时间提供了 10 张自己的图像(并且使用不同的表达式,这使得测试面部识别算法更具挑战性)。 来自该数据集的图像作为像素强度的特征向量提供。 因此,重要的是对向量进行整形,以使其类似于像素矩阵。 将插值设置为'nearest'有助于平滑图像:

In: from sklearn.datasets import fetch_olivetti_faces
    import numpy as np
    import matplotlib.pyplot as plt
    dataset = fetch_olivetti_faces(shuffle=True, random_state=5)
    photo = 1
    for k in range(6):
        plt.subplot(2, 3, k+1)
        plt.imshow(dataset.data[k].reshape(64, 64), 
                   cmap=plt.cm.gray, 
                   interpolation='nearest')
        plt.title('subject '+str(dataset.target[k]))
        plt.axis('off')
    plt.show()

将绘制完整的图像面板:

我们还可以可视化手写数字或字母。 在我们的示例中,我们将绘制 Scikit-learn 手写数字数据集中的前九个数字,并设置两个轴的范围(通过使用extent参数并提供最小值和最大值的列表)以将像素对齐网格:

In: from sklearn.datasets import load_digits
    digits = load_digits()
    for number in range(1,10):
        fig = plt.subplot(3, 3, number)
        fig.imshow(digits.images[number], 
                   cmap='binary',
                   interpolation='none', 
                   extent=[0,8,0,8])
        fig.set_xticks(np.arange(0, 9, 1))
        fig.set_yticks(np.arange(0, 9, 1))
        fig.grid()
    plt.show()

通过仅打印一张图像可以获得单个编号的简单特写:

In: plt.imshow(digits.images[0],
               cmap='binary',
               interpolation='none', 
               extent=[0,8,0,8]) # Extent defines the images max and min 
# of the horizontal and vertical values plt.grid() 

生成的图像清楚地突出显示像素如何构成图像及其灰度级:

精选的 Pandas 图形实例

使用适当设置的超参数,许多机器学习算法可以最佳地学习如何根据目标结果映射数据。 但是,通过解决数据中隐藏和细微的问题,可以进一步提高其预测表现。 这不仅仅是检测任何丢失或异常情况的问题。 有时,这取决于数据中是否存在任何组或异常分布(例如,多峰分布)。 明确起草的数据图可以阐明变量之间的关系,并且可以导致创建新的更好的特征,从而以更高的准确率预测目标变量。

刚刚描述的实践称为探索性数据分析EDA),并且可以通过以下方式相应地获得有效的结果:

  • 它应该很快,可以让您探索和发展新的想法,进行测试,然后以新的探索和新想法重新开始
  • 它应基于图形表示,以便更好地整体描述数据,无论其维数多高

pandas``DataFrame提供了许多 EDA 工具,可以帮助您进行探索。 但是,首先必须将数据转换为DataFrame

In: import pandas as pd
    print ('Your pandas version is: %s' % pd.__version__)

 from sklearn.datasets import load_iris    iris = load_iris()
    iris_df = pd.DataFrame(iris.data, columns=iris.feature_names)
    groups = list(iris.target)
    iris_df['groups'] = pd.Series([iris.target_names[k] for k in groups]) Out: Your pandas version is: 0.23.1  

请检查您的 Pandas 版本。 我们在pandas的 0.23.1 版本下测试了该书中的代码,它在以后的版本中也应适用。

以下段落中介绍的所有示例都将使用iris_df DataFrame

pandas包实际上依赖于 Matplotlib 函数进行可视化。 它只是为其他复杂的绘图指令提供了一个方便的包装器。 这在速度和简便性方面具有优势,这是任何 EDA 过程的核心价值。 相反,如果您的目的是通过使用精美的可视化效果最好地传达发现,您可能会注意到自定义 Pandas 图形输出并不是那么容易。 因此,当最重要的是要创建特定的图形输出时,最好使用 Matplotlib 指令从头开始直接工作。

使用箱线图和直方图

分发应该始终是数据中要检查的第一个方面。 盒子图可以起草分布图中的关键数据,并帮助您发现异常值。 只需在DataFrame上使用boxplot方法即可获得快速概述:

In: boxplots = iris_df.boxplot(return_type='axes')

这是数据集的所有数字变量的箱形图:

如果您的数据中已经有分组(来自类别变量,或从无监督学习中得出),只需指出变量,您需要在箱线图中表示数据,并指定您需要按分组将其分开(使用by参数,然后是分组变量的字符串名称):

In: boxplots = iris_df.boxplot(column='sepal length (cm)', 
                               by='groups', 
                               return_type='axes')

运行代码后,将按组获取箱线图:

通过这种方式,您可以快速知道变量是否是组差异的良好区分器。 无论如何,箱形图无法以直方图和密度图的形式为您提供完整的分布图。 例如,通过使用直方图和密度图,您可以确定是否存在分布峰或谷:

In: densityplot = iris_df.plot(kind='density')

该代码显示数据集所有数字变量的分布:

In: single_distribution = iris_df['petal width (cm)'].plot(kind='hist',
                                                           alpha=0.5)

这是由直方图表示的结果分布:

您可以使用plot方法获得直方图和密度图。 该方法使您可以表示整个数据集,特定的变量组(您只需要提供字符串名称列表并进行一些华丽的索引编制),甚至单个变量即可。

绘制散点图

散点图可用于有效地了解变量是否处于非线性关系中,并且您可以对变量的最佳转换方式有所了解,以实现线性化。 如果您使用基于线性组合的算法,例如线性或逻辑回归,那么弄清楚如何使它们的关系更线性地呈现将有助于您获得更好的预测能力:

In: colors_palette = {0: 'red', 1: 'yellow', 2:'blue'}
    colors = [colors_palette[c] for c in groups]
    simple_scatterplot = iris_df.plot(kind='scatter', x=0, y=1, c=colors)

运行代码后,将出现一个精美绘制的散点图:

散点图可以变成六边形合并图。 此外,它们还可以帮助您有效地可视化点密度,这些点自然会更多地聚集在一起,从而揭示隐藏在数据中的群集。 为了获得这样的结果,可以使用数据集中最初存在的一些变量,或者使用 PCA 或其他降维算法获得的尺寸:

In: hexbin = iris_df.plot(kind='hexbin', x=0, y=1, gridsize=10)

这是生成的hexbin图:

gridsize参数指示图表将在一个网格中汇总多少个数据点。 较大的网格将创建大型网格单元,而较小的网格将创建小型网格单元。

散点图是双变量的。 因此,每个变量组合都需要一个图。 如果变量的数量不是很多(否则,可视化将变得混乱),一种快速的解决方案是使用pandas命令自动绘制散点图矩阵(使用核密度估计'kde'依次在图表的对角线上绘制每个特征的分布):

In: from pandas.plotting import scatter_matrix
    colors_palette = {0: "red", 1: "green", 2: "blue"}
    colors = [colors_palette[c] for c in groups]
    matrix_of_scatterplots = scatter_matrix(iris_df, 
                                            alpha=0.2, 
                                            figsize=(6, 6),
                                            color=colors, 
                                            diagonal='kde')

运行之前的代码后,您将获得完整的图形矩阵(对角线上的密度):

一些参数可以控制散点图矩阵的各个方面。 alpha参数控制透明度的量,figsize提供以英寸为单位的矩阵的宽度和高度。 最后,color接受一个列表,该列表指示图中每个点的颜色,从而允许描绘数据中的不同组。 另外,通过在diagonal参数上选择'kde''hist',您可以选择在散布矩阵对角线上代表每个变量的密度曲线或直方图。

通过平行坐标发现模式

散点图矩阵可以通知您有关特征的联合分布。 它可以帮助您找到数据中的组并验证它们是否可区分。 平行坐标是另一种绘图,有助于为您提供有关数据中存在的最多区分组变量的提示。

通过将所有观测值相对于所有可能的变量绘制为平行线(在横坐标上任意对齐),平行坐标将帮助您发现是否有观测流分组为您的类,并了解最能区分这些流的变量( 最有用的预测变量)。 自然地,为了使图表有意义,图中的特征应具有与鸢尾花数据集相同的比例尺(否则将其标准化):

In: from pandas.tools.plotting import parallel_coordinates
    pll = parallel_coordinates(iris_df,'groups') 

前面的代码将输出平行坐标:

parallel_coordinates是一个 Pandas 函数,为了正常工作,仅需要数据DataFrame和包含要测试其可分离性的组的变量的字符串名称作为参数。 因此,您应该在数据集中使用组变量。 但是,使用DataFrame.drop('variable name', axis=1, inplace=True)方法完成探索后,不要忘记将其删除。

总结 Matplotlib 的命令

正如我们在上一段中所看到的那样,Pandas 可以将可视化的数据浏览速度提高,因为它可以打包成单个命令,而使用 Matplotlib 则需要完整的代码片段。 其背后的想法是,除非需要定制和配置特殊的可视化效果,否则使用包装器可以使您更快地创建标准图形。

除 Pandas 外,其他包还将来自 Matplotlib 的低级指令组装为更用户友好的命令,以用于特定的表示形式和用法:

  • Seaborn 是一个包,它通过为您提供一组统计图来扩展趋势图和区分组的功能,从而扩展了您的可视化功能
  • ggplot是流行的 R 库ggplot2的端口,它基于 Leland Wilkinson 的书《图形语法》中提出的可视化语法。 R 库是不断开发的,它提供了很多功能; Python 移植具有基础知识并且其完整的开发工作仍在进行中
  • MPLD3 利用 JavaScript 库进行图形处理 D3.js,以便轻松地将任何 Matplotlib 输出转换为 HTML 代码,可以使用浏览器和工具(如 Jupyter 笔记本; 或在互联网网站中。
  • Bokeh 是一个交互式可视化包,它利用 JavaScript 和浏览器呈现的输出。 它是 D3.js 的理想替代品,因为您只需要 Python 即可利用 JavaScript 的功能以交互方式快速表示数据。

在接下来的页面中,我们将介绍 Seaborn,并提供一些构建块以利用它们在您的数据科学项目中的可视化效果。

Seaborn 介绍

Seaborn 由 Michael Waskom 创建并托管在 PyData 网站上,是一个将底层 Matplotlib 与整个 pyData 栈包装在一起的库,允许集成图表和来自 NumPy 和 Pandas 的数据结构,以及 SciPy 和 StatModels 的统计例程。 借助内置主题以及专为揭示数据模式而设计的调色板,所有这些工作都特别注重美学。

如果您当前尚未在系统上安装 Seaborn(例如,Anaconda 发行版默认提供它),则可以通过pipconda轻松获得它(提醒您conda版本可能落后于直接从 PyPI(Python 包索引)获取的pip版本)。

$> pip install seaborn 
$> conda install seaborn

在这些示例中,我们使用了 Seaborn 包的 0.9 版。

您可以通过以下方式上传包并将 Seaborn 样式设置为默认的 Matplotlib 样式:

In: import seaborn as sns 
    sns.set() 

这足以将您所有基于 Matplotlib 的表示形式转换为更具视觉吸引力的图表:

In: x = np.linspace(0, 5, 50) 
    y_cos = np.cos(x) 
    y_sin = np.sin(x) 
    plt.figure() 
    plt.plot(x,y_cos) 
    plt.plot(x,y_sin) 
    plt.xlabel('x') 
    plt.ylabel('y') 
    plt.title('sin/cos functions') 
    plt.show()

结果如下:

您可以从任何以前看到的图表中获得有趣的结果,甚至可以使用 Pandas 中的图形方法生成的图表(毕竟,Pandas 还依赖于 Matplotlib 来创建其探索性图表)。

Seaborn 中有五个预设主题:

  • darkgrid
  • whitegrid
  • dark
  • white
  • ticks

默认值为darkgrid。 您可以通过使用set_style命令和首选主题的名称,然后运行plot命令轻松地尝试每个命令:

In: sns.set_style('whitegrid')

您所要做的只是确定哪个主题可以帮助您更好地传达图表上的信息。 您可以将样式限制为包含该样式的单个表示形式:

In: with sns.axes_style('whitegrid'):
 # Your plot commands here pass

其他时髦的变化可能涉及到轴线,这是图表的边界。 使用despine命令,您可以轻松删除上下边框:

In: sns.despine()

此外,您可以使用left=True参数删除左边框,使用offset参数偏移轴,并对其进行修剪(使用trim=True)。 否则,仅凭 Matplotlib 命令就无法访问所有这些操作。

Seaborn 允许您使用的另一个有用控件是图表的比例。 某个特定的图表比例(涉及不同的线条粗细,字体大小等)称为上下文,可用的上下文是自说明纸,笔记本,谈话和海报,这是可能的选择。 例如,如果您的图表必须显示在 MS PowerPoint 演示文稿上,则只需在创建图形之前运行以下命令:

In: sns.set_context("talk")

让我们在最初的 sin/cos 图表上看到一些此类时尚效果的示例:

In: sns.set_context("talk")
 with sns.axes_style('whitegrid'): plt.figure() plt.plot(x,y_cos) plt.plot(x,y_sin) plt.show() sns.set()

该代码将绘制以下图表:

同样,选择正确的颜色周期或设置可能有助于图形表示效果。 为此,Seaborn 提供了color_palette()命令,该命令不仅会告诉当前调色板的 RBG 值(如果不带参数运行); 它还将接受 Seaborn 提供的任何调色板的名称或任何 Matplotlib 颜色图。 它甚至接受您以任何 Matplotlib 格式提供的自定义颜色列表(RGB 元组,十六进制颜色代码或 HTML 颜色名称),以便创建自己的调色板:

In: current_palette = sns.color_palette()
 print (current_palette) sns.palplot(current_palette)

运行代码后,您将在值和颜色上可视化当前调色板:

如上所述,有一些调色板可用。 首先,所有 Seaborn 调色板如下:

  • deep
  • muted
  • bright
  • pastel
  • dark
  • colorblind

您还必须添加hlshusl和所有 Matplotlib 颜色图,可以通过在名称后添加_r来反转它们,或者通过添加_d使其更暗。

Matplotlib 颜色图的名称和示例都可以在以下网页上找到

hls颜色空间是 RGB 值刻度的自动转换,由于颜色的强度不同(例如,黄色和绿色被感知为较亮,而蓝色被视为为较暗),因此它可能对您的表示形式有效或无效。 。

作为hsl的替代,您可以使用husl调色板,它对人眼更友好,如这个页面所述。

最后,您可以使用 Color Brewer 工具创建个性化的调色板,该工具可以在网上找到或在 Jupyter 笔记本中的应用中是所需的。 在笔记本单元中,使用choose_colorbrewer_palette命令将显示一个交互式工具。 为了使一切正常工作,必须将data_type作为参数指定,这是一个字符串,该字符串说明与要表示的数据相关的调色板的性质:

  • 顺序:如果要表示连续性
  • 发散:用于表示反差
  • 定性:当您只想区分不同的类别时

让我们看看如何创建自定义顺序调色板并使用它:

In: your_palette = sns.choose_colorbrewer_palette('sequential')

完整的仪表板将出现:

设置颜色后,your_palette将变成 RGB 值列表:

In: print(your_palette) Out:[(0.91109573770971852, 0.90574395025477683, 0.94832756940056306),   
     (0.7764706015586853, 0.77908498048782349, 0.88235294818878174),   
     (0.61776242186041452, 0.60213766261643054, 0.78345253116944269), 
     (0.47320263584454858, 0.43267974257469177, 0.69934642314910889), 
     (0.35681661753093497, 0.20525952297098493, 0.58569783322951374)]

选择完毕后,您只需调用sns.set_palette(your_palette),并获得绘制所有图表时使用的颜色即可。

如果您只需要对具有某些特定颜色的图表进行操作,则可以使用with语句并在其下嵌套图表摘要即可,就像我们之前针对主题所看到的那样。 相反,如果您确实需要设置某个调色板,请使用set_palette

调色板由六种颜色组成,可帮助您区分至少六个趋势或类别。 如果您需要进一步区分,只需使用hls调色板进行操作,并指出需要循环的颜色数量:

In: new_palette=sns.color_palette('hls', 10)
 sns.palplot(new_palette)

这是生成的调色板:

最后,关闭主题和颜色部分,因为 Seaborn 是使用 Matplotlib 提供的功能的另一种更智能的方式,因此提醒您,可以使用来自 Matplotlib 本身的任何基本命令来进一步修改结果图表。 或者,它们可以通过 MPLD3 或 Bokeh 之类的包进一步转换为 JavaScript。

增强您的 EDA 功能

Seaborn 不仅使您的图表更加美观,而且在其方面也易于控制。 它还为您提供了用于 EDA 的新工具,可帮助您发现变量之间的分布和关系。

在继续之前,让我们重新加载包,并以 Pandas DataFrame格式准备好 Iris 和波士顿数据集:

In: import seaborn as sns 
    sns.set()  

    from sklearn.datasets import load_iris 
    iris = load_iris() 
    X_iris, y_iris = iris.data, iris.target 
    features_iris = [a[:-5].replace(' ','_') for a in iris.feature_names] 
    target_labels = {j: flower \
                        for j, flower in enumerate(iris.target_names)} 
    df_iris = pd.DataFrame(X_iris, columns=features_iris) 
    df_iris['target'] = [target_labels[y] for y in y_iris]  

    from sklearn.datasets import load_boston 
    boston = load_boston() 
    X_boston, y_boston = boston.data, boston.target 
    features_boston = np.array(['V'+'_'.join([str(b), a]) 
                                for a,b in zip(boston.feature_names, 
                                range(len(boston.feature_names)))]) 
    df_boston = pd.DataFrame(X_boston, columns=features_boston) 
    df_boston['target'] = y_boston 
    df_boston['target_level'] = pd.qcut(y_boston,3) 

至于鸢尾花数据集,目标变量已转换为鸢尾花种类的描述性文本。 对于波士顿数据集,连续目标变量(自有住房的中位数)已分为三个相等的部分,分别代表较低,中位数和较高的价格(使用 Pandas 函数qcut)。

Seaborn 首先可以找出离散值或分类变量与数字变量之间的关系,从而帮助您进行数据探索。 这可以通过catplot函数实现:

In: with sns.axes_style('ticks'): 
        sns.catplot(data=df_boston, x='V8_RAD', y='target', kind='point')

您会发现探索类似的图很有见地,因为它们明确了目标水平及其变化:

在我们的示例中,在波士顿数据集中,对离散值的径向公路的可及性指标与目标进行了比较,以检查其关系的函数形式以及每个级别的相关方差。

而是在数字变量之间进行比较; Seaborn 提供了增强的散点图,并结合了回归拟合曲线趋势,当关系不是线性的时,可以提示您进行可能的数据转换:

In: with sns.axes_style("whitegrid"): 
        sns.regplot(data=df_boston, x='V12_LSTAT', y="target", order=3)

装配线将立即显示:

Seaborn 中的regplot可以可视化任何顺序的回归图(我们显示了二次多项式拟合)。 在可用的回归图中,如果检查的特征之一是二元的,则可以使用标准线性回归,稳健回归甚至逻辑回归。

在也需要考虑分布的地方,jointplot将在散点图的侧面提供其他图:

In: with sns.axes_style("whitegrid"): 
        sns.jointplot("V4_NOX", "V7_DIS", 
                      data=df_boston, kind='reg', 
                      order=3)

jointplot产生以下图表:

通过作用于kind参数来表示双变量关系的理想选择,jointplot也可以表示简单的散点图或密度(种类= scatter或种类= kde)。

当目的是要发现区分类别的内容时,FacetGrid可以以可比的方式排列不同的图,并帮助您了解哪里存在差异。 例如,我们可以检查鸢尾花种类的散点图,以确定它们是否占据了特征状态的不同部分:

In: with sns.axes_style("darkgrid"): 
        chart = sns.FacetGrid(df_iris, col="target_level")   
        chart.map(plt.scatter, "sepal_length", "petal_length") 

该代码将很好地打印一个面板,该面板代表基于组的比较:

可以使用分布(sns.distplot)或回归斜率(sns.regplot)进行类似的比较:

In: with sns.axes_style("darkgrid"):
 chart = sns.FacetGrid(df_iris, col="target") chart.map(sns.distplot, "sepal_length") 

第一个比较基于分布:

随后的比较基于拟合线性回归线:

In: with sns.axes_style("darkgrid"):
 chart = sns.FacetGrid(df_boston, col="target_level")chart.map(sns.regplot, "V4_NOX", "V7_DIS") 

这是基于回归的比较:

至于评估跨类别的数据分布,Seaborn 提供了另一种工具,即提琴图。 提琴图只是一个箱形图,其框是根据密度估计来成形的,因此可以直观地传达信息:

In: with sns.axes_style("whitegrid"):     
        ax = sns.violinplot(x="target", y="sepal_length", 
                            data=df_iris, palette="pastel") 
        sns.despine(offset=10, trim=True) 

先前代码生成的提琴图可以为数据集提供有趣的见解:

最后,Seaborn 通过使用pairplot命令提供了一种更好的方法来创建散点图矩阵,并允许您定义组颜色(参数色相)以及如何填充对角线行。 通过使用diag_kind参数,它可以是直方图('hist')或核密度估计('kde'):

In: with sns.axes_style("whitegrid"): 
        chart = sns.pairplot(data=df_iris, hue="target", diag_kind="hist") 

先前的代码将为数据集输出完整的散点图矩阵:

学习高级数据的表示

一些有用的表示可以从数据科学过程中得出。 也就是说,表示不是直接从数据中完成,而是通过使用机器学习过程来实现的,机器学习过程向我们介绍了算法的运行方式,并为我们提供了每个预测变量在获得的预测中的作用的更精确概述。 特别地,学习曲线可以提供快速诊断以改善模型。 这可以帮助您确定是需要更多观察还是需要丰富变量。

学习曲线

学习曲线是有用的诊断图形,它描述了机器学习算法(您的假设)相对于可用观察量的行为。 这样做的目的是比较使用不同样本内大小的交叉验证(通常为十倍)时训练表现(样本内案例的误差或准确率)如何表现。

就训练误差而言,您应该期望它在开始时会很高,然后再减少。 但是,根据假设的偏差和方差级别,您会注意到不同的行为:

  • 高偏差假说倾向于从平均误差表现开始,在暴露于更复杂的数据时迅速降低,然后再保持相同的表现水平,无论您进一步添加多少种情况。
  • 低偏的学习器在很多情况下倾向于更好地概括,但是他们近似复杂数据结构的能力有限,因此其表现有限。
  • 高方差的假设往往会开始提高误差表现,然后随着您添加更多案例而逐渐降低。 由于它具有记录样本内特性的高容量,因此趋于缓慢降低。

至于交叉验证,我们可以注意到两种行为:

  • 高偏见的假设往往始于低表现,但增长很快,直到达到与训练几乎相同的表现。 然后,它停止增长。
  • 高方差的假设往往始于非常低的表现。 然后,随着更多案例的推广,它稳步但缓慢地改善了。 它几乎不读取样本中的演奏,并且它们之间始终存在差距。

能够估计您的机器学习解决方案表现为高偏差还是高方差假设,将立即帮助您决定如何改善数据科学项目。 尽管有了learning_curve类,但 Scikit-learn 使得计算可视化图形所需的所有统计信息更为简单,尽管正确地可视化它们还需要一些进一步的计算和命令:

In: import numpy as np
    from sklearn.learning_curve import learning_curve, validation_curve
    from sklearn.datasets import load_digits
    from sklearn.linear_model import SGDClassifier

    digits = load_digits()
    X, y = digits.data, digits.target
    hypothesis = SGDClassifier(loss='log', shuffle=True, 
                               n_iter=5, penalty='l2', 
                               alpha=0.0001, random_state=3)
    train_size, train_scores, test_scores = learning_curve(hypothesis, X, 
                              y, train_sizes=np.linspace(0.1,1.0,5), cv=10, 
                               scoring='accuracy',                  
                               exploit_incremental_learning=False,
                               n_jobs=-1)
    mean_train  = np.mean(train_scores,axis=1)
    upper_train = np.clip(mean_train + np.std(train_scores,axis=1),0,1)
    lower_train = np.clip(mean_train - np.std(train_scores,axis=1),0,1)
    mean_test = np.mean(test_scores,axis=1)
    upper_test = np.clip(mean_test + np.std(test_scores,axis=1),0,1)
    lower_test = np.clip(mean_test - np.std(test_scores,axis=1),0,1)
    plt.plot(train_size,mean_train,'ro-', label='Training')
    plt.fill_between(train_size, upper_train, 
                     lower_train, alpha=0.1, color='r')
    plt.plot(train_size,mean_test,'bo-', label='Cross-validation')
    plt.fill_between(train_size, upper_test, lower_test, 
                     alpha=0.1, color='b')
    plt.grid()
    plt.xlabel('sample size') # adds label to x axis
    plt.ylabel('accuracy') # adds label to y axis
    plt.legend(loc='lower right', numpoints= 1)
    plt.show() 

根据不同的样本量,您很快就会获得学习曲线图:

learning_curve类需要以下内容作为输入:

  • 一系列存储在列表中的训练值
  • 指示要使用的折数以及误差度量
  • 您要测试的机器学习算法(参数估计器)
  • 预测变量(参数X)和目标结果(参数y

结果,该类将产生三个数组。 第一个包含有效训练量,第二个显示在每次交叉验证迭代中获得的训练分数,最后一个显示交叉验证分数。

通过对训练和交叉验证应用均值和标准差,可以在曲线图中显示曲线趋势及其变化。 您还可以提供有关录制的演奏稳定性的信息。

验证曲线

由于学习曲线适用于不同的样本大小,因此验证曲线会根据超参数可以采用的值来估计训练和交叉验证表现。 与在学习曲线中一样,可以应用类似的注意事项,尽管这种特殊的可视化将使您对参数的优化行为有更深入的了解,并在视觉上向您建议应将搜索重点放在超参数空间的一部分:

In: from sklearn.learning_curve import validation_curve
    testing_range = np.logspace(-5,2,8)
    hypothesis = SGDClassifier(loss='log', shuffle=True, 
                               n_iter=5, penalty='l2', 
                               alpha=0.0001, random_state=3)
    train_scores, test_scores = validation_curve(hypothesis, X, y, 
                                 param_name='alpha', 
                                 param_range=testing_range, 
                                 cv=10, scoring='accuracy', n_jobs=-1)
    mean_train  = np.mean(train_scores,axis=1)
    upper_train = np.clip(mean_train + np.std(train_scores,axis=1),0,1)
    lower_train = np.clip(mean_train - np.std(train_scores,axis=1),0,1)
    mean_test = np.mean(test_scores,axis=1)
    upper_test = np.clip(mean_test + np.std(test_scores,axis=1),0,1)
    lower_test = np.clip(mean_test - np.std(test_scores,axis=1),0,1)
    plt.semilogx(testing_range,mean_train,'ro-', label='Training')
    plt.fill_between(testing_range, upper_train, lower_train, 
                     alpha=0.1, color='r')
    plt.fill_between(testing_range, upper_train, lower_train, 
                     alpha=0.1, color='r')
    plt.semilogx(testing_range,mean_test,'bo-', label='Cross-validation')
    plt.fill_between(testing_range, upper_test, lower_test, 
                     alpha=0.1, color='b')
    plt.grid()
    plt.xlabel('alpha parameter') # adds label to x axis
    plt.ylabel('accuracy') # adds label to y axis
    plt.ylim(0.8,1.0)
    plt.legend(loc='lower left', numpoints= 1)
    plt.show() 

经过一些计算,您将获得参数验证曲线的表示形式:

validation_curve类的语法类似于先前看到的learning_curve的语法,但对于param_nameparam_range参数,应分别为它们提供超参数和必须测试的范围。 至于结果,训练和测试结果以数组形式返回。

随机森林的特征重要性

正如在第 3 章,“数据管道”的结论中所讨论的那样,选择正确的变量可以通过减少噪声,估计方差和过多计算负担来改善学习过程 。 集成方法(例如,随机森林)可以为您提供与数据集中其他变量一起使用时变量所扮演角色的不同视图。

在这里,我们向您展示如何提取随机森林和极端树模型的重要性。 重要性的计算方式最初由 Breiman,Friedman 等人在《分类和回归树》一书中描述。 1984 年。这是一个真正的经典,为分类树奠定了坚实的基础。 在本书中,重要性用基尼重要性平均减少不纯度来描述,这是由于在整个集成树上平均一个特定变量而导致的节点杂质的总减少量。 换句话说,平均减少不纯度是在该变量上分割的节点的总误差减少乘以路由到每个节点的样本数。 值得注意的是,根据这种重要性计算方法,减少误差不仅取决于误差度量-基尼或熵用于分类,而 MSE 用于回归,而且树顶的拆分也被认为更重要,因为它们涉及处理更多问题。 例子。

在几个步骤中,我们将学习如何获取此类信息并将其投影到清晰的可视化文件中:

In: from sklearn.datasets import load_boston
    boston = load_boston()
    X, y = boston.data, boston.target
    feature_names = np.array([' '.join([str(b), a]) for a,b in 
                              zip(boston.feature_names,range(
                              len(boston.feature_names)))])
    from sklearn.ensemble import RandomForestRegressor
    RF = RandomForestRegressor(n_estimators=100, 
                               random_state=101).fit(X, y)
    importance = np.mean([tree.feature_importances_ for tree in 
                          RF.estimators_],axis=0)
    std = np.std([tree.feature_importances_ for tree in 
                  RF.estimators_],axis=0)
    indices = np.argsort(importance)
    range_ = range(len(importance))
    plt.figure()
    plt.title("Random Forest importance")
    plt.barh(range_,importance[indices],
             color="r", xerr=std[indices], alpha=0.4, align="center")
    plt.yticks(range(len(importance)), feature_names[indices])
    plt.ylim([-1, len(importance)])
    plt.xlim([0.0, 0.65])
    plt.show() 

该代码将产生以下图表,突出显示该模型的重要特征:

对于每个估计量(在我们的示例中,我们有 100 个模型),该算法估计得分以对每个变量的重要性进行排名。 随机森林模型由决策树组成,决策树可以由许多分支组成,因为该算法试图获得非常小的终叶。 如果在随意排列其原始值之后,排列后的模型的结果预测与原始模型的预测相比在准确率方面有很大差异,则认为该变量之一很重要。

重要性向量在估计器的数量上平均,并且估计的标准差通过列表理解(重要性和std的赋值)进行计算。 现在,根据重要性得分(向量索引)进行排序,结果将投影到带有标准差提供的误差线的条形图上。

在我们的 LSTAT 分析中,该区域中处于较低地位的人口百分比和 RM(即每个住宅的平均房间数)被指出是我们随机森林模型中最具决定性的变量。

梯度提升树的部分依赖图

对特征重要性的估计是一条信息,可以帮助您根据最佳选择来确定要使用的特征。 有时,您可能需要更好地理解为什么变量对预测特定结果很重要。 通过控制分析中涉及的所有其他变量的效果,梯度提升树为您提供了变量与预测结果之间关系的清晰观点。 与使用非常有效的 EDA 可能获得的信息相比,此类信息可以为您提供更多因果关系动态的见解:

In: from sklearn.ensemble.partial_dependence import 
    plot_partial_dependence
    from sklearn.ensemble import GradientBoostingRegressor
    GBM = GradientBoostingRegressor(n_estimators=100, 
                                    random_state=101).fit(X, y)
    features = [5,12,(5,12)]
    fig, axis = plot_partial_dependence(GBM, X, features, 
                                        feature_names=feature_names)   

作为输出,您将获得三个图,它们构成了 RM 和 LSTAT 特征的局部图:

在您提供分析计划后,plot_partial_dependence类将自动为您提供可视化效果。 您需要提供要单独绘制的特征的索引列表,以及要在热图上绘制的那些索引的元组(特征是轴,并且热值对应于结果) 。

在前面的示例中,平均房间数和较低身份人口的百分比均已表示,因此显示了预期的行为。 有趣的是,热图解释了它们如何共同为结果的值做出贡献,它揭示了它们没有以任何特定的方式相互作用(这是一次爬坡)。 但是,还显示出 LSTAT 大于 5 时,它是所得房屋价值的有力分隔符。

使用机器学习即服务创建预测服务器

很多时候,在您作为数据科学家的职业生涯中,您会发现自己需要与当前正在研究的代码分离的预测变量。 例如,如下:

  • 您正在为手机开发应用,并且想要节省内存
  • 您正在使用非 Python 编程语言(Java,Scala,C,C++ 等)进行编码,并且需要调用在 Python 中开发的预测变量
  • 您正在处理大数据,并且模型在存储数据的同一远程位置进行了训练

在所有这些情况下,最好通过 HTTP 进行服务即预测的服务,或者一般来说,将任何机器学习即服务ML-AAS)。

Bottle 是一个 Python 网络框架,是 HTTP 上微型应用的起点。 这是一个非常简单的 Python 库,提供了创建 Web 应用所需的基本对象和功能。 而且,它可以与 Python 中可用的所有其他库配对。 在进入即服务预测之前,让我们看看如何使用 Bottle 构建基本的Hello World程序。 请注意,以下清单作为脚本用于 Python REPL,而不用于 Jupyter 笔记本:

# File: bottle1.py from bottle import route, run, template port = 9099 @route('/personal/<name>') def homepage(name):
 return template('Hi <b>{{name}}</b>!', name=name)  print("Try going to http://localhost:{}/personal/Tom".format(port)) print("Try going to http://localhost:{}/personal/Carl".format(port))  run(host='localhost', port=port)

在执行代码之前,让我们逐行分析代码:

  1. 我们开始从 Bottle 模块中导入所需的函数和类。
  2. 然后,我们指定 HTTP 服务器将监听的端口。
  3. 在示例中,我们选择端口9099; 您可以随意将其更改为另一个,但是首先请检查是否有其他服务正在使用它(请记住 HTTP 位于 TCP 之上)。
  4. 下一步是 API 端点的定义。 当对指定为参数的路径执行 HTTP 调用时,route装饰器将应用其后定义的函数。 请注意,在路径中,它表示name,这是即将到来的函数的参数。 这意味着name是调用的参数; 您可以在 HTTP 调用中选择所需的任何字符串,然后您的选择将作为参数名称传递给函数。
  5. 然后,在函数主页中,返回了带有 HTML 代码的模板。 以一种更简单的方式,将其视为template函数,该函数将创建您将从浏览器中看到的页面。

模板,是一个示例,它只是一个普通的 HTML 页面,但它可能更复杂(它实际上可以是一个模板页面,其中需要填充一些空白)。 模板的完整描述超出了本节的范围,因为我们仅将框架用于简单的纯输出。 如果您需要其他信息,请浏览 Bottle 帮助页面。

  1. 最后,在print函数之后,有核心run函数。 这是一个阻止函数,它将在作为参数提供的主机和端口上设置 Web 服务器。 当您运行清单中的代码时,一旦执行了该函数,就可以打开浏览器并将其指向http://localhost:9099/personal/Carl,您将找到以下文本:Hi Carl!

当然,将 HTTP 调用中的名称从Carl更改为Tom或任何其他名称将导致不同的页面,其中包含调用中指定的名称。

请注意,在此虚拟示例中,我们仅定义了/personal/<name>路由。 除非代码中定义,否则任何其他调用都将导致Error 404

要关闭它,我们需要在命令行中按Ctrl + C(记住run函数正在阻止)。

现在让我们创建一个更加面向数据科学的服务; 我们将创建一个 HTML 页面,该页面的表单要求隔垫的长度和宽度以及花瓣的长度和宽度,以对鸢尾花样本进行分类。 在此示例中,我们将使用鸢尾花数据集来训练 Scikit-learn 分类器。 然后,对于每个预测,我们只需在分类器上调用predict函数,将预测发送回去:

# File: bottle2.py  from sklearn.datasets import load_iris from sklearn.linear_model import LogisticRegression from bottle import run, request, get, post import numpy as np  port = 9099  @get('/predict') def predict():
 return ''' <form action="/prediction" method="post"> Sepal length [cm]: <input name="sl" type="text" /><br/> Sepal width [cm]: <input name="sw" type="text" /><br/> Petal length [cm]: <input name="pl" type="text" /><br/> Petal width [cm]: <input name="pw" type="text" /><br/> <input value="Predict" type="submit" /> </form> '''  @post('/prediction') def do_prediction():
 try: sample = [float(request.POST.get('sl')), float(request.POST.get('sw')), float(request.POST.get('pl')), float(request.POST.get('pw'))] pred = classifier.predict(np.matrix(sample))[0] return "<p>The predictor says it's a <b>{}</b></p>"\
               .format(iris['target_names'][pred]) except: return "<p>Error, values should be all numbers</p>"  iris = load_iris() classifier = LogisticRegression() classifier.fit(iris.data, iris.target) 
 print("Try going to http://localhost:{}/predict".format(port)) run(host='localhost', port=port)  # Try insert the following values: # [ 5.1, 3.5, 1.4, 0.2] -> setosa # [ 7.0  3.2, 4.7, 1.4] -> versicolor # [ 6.3, 3.3, 6.0, 2.5] -> virginica

导入后,在这里我们使用get装饰器,指定仅对 HTTP GET 调用有效的路由。 装饰器以及后面的函数没有参数,因为所有特征都应插入predict函数中定义的 HTML 表单中。 表单提交后,将使用 HTTP POST传递到/prediction页面。

现在,我们需要为此调用创建一个路由,这就是我们在do_prediction函数中所做的。 它的装饰器是/prediction页面上的post(即与get相反;它仅定义POST路由)。 数据被解析并转换为double(默认参数为字符串),然后将特征向量馈送到classifier全局变量中以获得预测。 这是使用简单模板返回的。 对象请求包含传递给服务的所有参数,包括我们 POST 到路由的整个变量。 最后,似乎我们只需要定义全局变量分类器-即在鸢尾花数据集上训练的分类器-最后,我们可以调用run函数。

对于此虚拟示例,我们将逻辑回归器用作分类器,并在完整的鸢尾花数据集上进行训练,而所有参数均保留为默认值。 在实际情况下,这里您将尽可能优化分类器。

运行此代码后,如果一切正常,则可以将浏览器指向http://localhost:9099/predict,您会看到以下表格:

单击预测按钮后插入值(5.13.51.40.2),应将您重定向到http://localhost:9099/prediction,其中应显示The predictor says it's a setosa字符串。 另外,请注意,如果您在表单中插入无效的条目(例如,将其保留为空或插入字符串而不是数字),则会显示一个 HTML 页面,指出存在错误。

我们已经完成了本节的一半,并且已经看到了用 Bottle 创建 HTTP 端点是多么容易和快捷。 现在,让我们尝试创建一个可以在任何程序中调用的预测即服务。 我们将以get调用的形式提交特征向量,并且返回的预测将为 JSON 格式。 这是此解决方案的代码:

# File: bottle3.py  from sklearn.datasets import load_iris from sklearn.linear_model import LogisticRegression from bottle import run, request, get, response import numpy as np import json  port = 9099  @get('/prediction') def do_prediction():
 pred = {} try: sample = [float(request.GET.get('sl')), float(request.GET.get('sw')), float(request.GET.get('pl')), float(request.GET.get('pw'))] pred['predicted_label'] =             iris['target_names'] [classifier.predict(np.matrix(sample))[0]]
 pred['status'] = "OK" except: pred['status'] = "ERROR" response.content_type = 'application/json' return json.dumps(pred)  iris = load_iris() classifier = LogisticRegression() classifier.fit(iris.data, iris.target)  print("Try going to http://localhost:{}/prediction\
       sl=5.1&sw=3.5&pl=1.4&pw=0.2".format(port)) print("Try going to http://localhost:{}/prediction\
       sl=A&sw=B&pl=C&pw=D".format(port)) run(host='localhost', port=port)

该解决方案非常简单明了。 不过,让我们逐步分析它。 函数的入口点由/prediction路径上的get装饰器定义。 在此,我们将访问GET值以提取预测(请注意,如果您的分类器需要许多特征,则最好在此处使用POST调用)。 与前面的示例完全相同,生成了预测; 最后,将该值与status键的OK值一起插入 Python 字典中。 如果此函数引发异常,将不会进行预测,但status键中将出现ERROR字符串。 然后,将输出应用格式设置为 JSON,然后将 Python 字典序列化为 JSON 字符串。

当它运行时,我们可以访问 URL localhost:9099/prediction,后跟特征值,然后我们将预测作为 JSON 取回。 请注意,由于它是 JSON,因此不需要浏览器来解释返回的 HTTP 响应。 因此,我们可以从不同的应用(wget,浏览器或curl)或任何编程语言(包括 Python 本身)调用终结点。 要使其正常运行,请启动它并将您的浏览器指向(或以任何方式请求 URL)http://localhost:9099/prediction?sl=5.1&sw=3.5&pl=1.4&pw=0.2。 您将取回有效的 JSON:{"predicted_label": "setosa", "status": "OK"}。 另外,如果参数解析出错,您将得到以下 JSON:{"status": "ERROR"}。 这是您的第一个 ML-AAS!

尽管简单快捷,但是 Bottle 还有许多其他功能需要探索。 但是,它不如其他框架完整。 如果您的应用需要一些非凡的功能,请查看 Flask 或 Django 模块。

总结

本章通过提供数据,机器学习过程和结果的基本和高级图形表示的示例,概述了基本数据科学。 我们探索了来自 Matplotlib 的pylab模块,该模块可让您最轻松,最快地访问该包的图形功能。 我们将 Pandas 用于 EDA,并测试了 Scikit-learn 提供的图形工具。 所有示例都像构建块一样,并且都易于定制,以便为您提供快速的可视化模板。

在下一章中,将向您介绍,它们是与预测变量/目标平坦矩阵的有趣偏差。 现在,它们已成为数据科学中的热门话题。 期望深入研究非常复杂和复杂的网络。

六、社交网络分析

“社交网络分析”(通常称为 SNA)创建模型并研究以网络形式存在的一组社交实体的关系。 实体可以是人,计算机或网页,关系可以是喜欢,链接或友谊(即,实体之间的连接)。

在本章中,您将了解以下内容:

  • 图,因为社交网络通常以这种形式表示
  • 用于从图中获取见解的重要算法
  • 如何加载,转储和采样大型图

图论概论

基本上,图是一种能够表示对象集合中的关系的数据结构。 在这种范式下,对象是图的节点,关系是图的链接(或边)。 如果链接具有方向(在概念上,它们就像城市的单向街道),则图是有向的。 否则,该图是无向的。 下表提供了众所周知的图的示例:

图示例类型节点
全球资讯网导演网页链接
Facebook无向人们友谊
推特导演人们追随者
IP 网络无向主持人电线/连接
导航系统导演地点/地址街道
维基百科导演页数锚链接
科学文献导演文件引文
马尔可夫链导演状态排放概率

如在传统的关系数据库管理系统RDBMS)(例如 MySQL 或 Postgres)中,所有上述示例都可以表示为节点之间的关系。 现在,我们将发现图数据结构的优点,并开始考虑以下 SQL 查询对于诸如 Facebook 之类的社交网络的复杂程度(考虑一种推荐系统,该系统可以帮助您找到您可能认识的人 ):

  1. 检查以下查询:
Find all people who are friends of my friends, but not my friends  
  1. 在图上将前面的查询与下面的查询进行比较:
Get all friends connected to me having distance=2  
  1. 现在,让我们看看如何使用 Python 创建图或社交网络。 在本章中将广泛使用的库称为NetworkX。 它能够处理中小型图,并且功能齐全:
In: %matplotlib inline
 import networkx as nx
 import matplotlib.pyplot as plt

 G = nx.Graph()
 G.add_edge(1,2)
 nx.draw_networkx(G)
 plt.show()

下图是上述代码的可视化视图,显示了两个节点及其连接边缘:

该命令是不言自明的。 检查之前的代码,在包导入之后,我们将首先定义一个(NetworkX)图对象(默认情况下,它是无向的)。 然后,我们将在两个节点之间添加一条边(即一个连接)(由于节点尚未在图中,因此会自动创建它们)。 最后,我们将绘制图。 库自动生成图布局(节点的位置)。

使用.add_note()方法,将其他节点添加到图非常简单。 例如,如果要添加节点34,则可以简单地使用以下代码:

    In: G.add_nodes_from([3, 4])
        nx.draw_networkx(G)
        plt.show()

现在,我们的图变得越来越复杂,如您从图中所看到的:

前面的代码将添加两个节点。 由于它们未链接到其他节点,因此将断开连接。 同样,要向图添加更多边,可以使用以下代码:

    In: G.add_edge(3,4)
 G.add_edges_from([(2, 3), (4, 1)]) nx.draw_networkx(G) plt.show()  

通过使用前面的代码,我们已经完成了图中的节点的连接:

要获取图中的节点集合,只需使用.nodes()方法。 同样,.edges()给出边列表作为连接节点的列表:

In: G.nodes() Out: [1, 2, 3, 4] 
In: G.edges() Out: [(1, 2), (1, 4), (2, 3), (3, 4)]  

有几种表示和描述图的方法。 在以下部分中,我们将说明最受欢迎的。 第一种选择是使用邻接表。 它列出了每个节点的邻居; 即,list[0]包含以邻接表格式表示的邻接节点:

In: list(nx.generate_adjlist(G)) Out: ['1 2 4', '2 3', '3 4', '4']  

按照这种格式,第一个数字始终是来源,后面的数字始终是目标,如以下 URL 所示

为了使描述独立,可以将图表示为列表的字典。 由于其简洁,这是描述图的最流行(也是实用)方法。 在这里,节点的名称是字典键,其值是节点的邻接表:

In: nx.to_dict_of_lists(G) Out: {1: [2, 4], 2: [1, 3], 3: [2, 4], 4: [1, 3]}  

另一方面,您可以将图描述为边的集合。 在输出中,每个元组的第三个元素是边的属性。 实际上,每个边缘都可以具有一个或多个属性(例如其权重,其基数等)。 由于我们创建了一个非常简单的图,因此在以下示例中,我们没有属性:

In: nx.to_edgelist(G) Out: [(1, 2, {}), (1, 4, {}), (2, 3, {}), (3, 4, {})]

最后,图可以描述为 NumPy 矩阵。 如果矩阵在(i, j)位置包含1,则表示ij节点之间存在链接。 由于矩阵通常只包含很少的 1(与零的数量相比),因此通常表示为稀疏(SciPy)矩阵,NumPy 矩阵或 Pandas DataFrame

请注意,矩阵描述是详尽无遗的。 因此,将无向图转换为有向图,将连接(i, j)的链接转换为两个链接,即(i, j)(j, i)。 该表示形式通常被称为邻接矩阵或连接矩阵。

因此,将创建一个对称矩阵,如以下示例所示:

In: nx.to_numpy_matrix(G) Out: matrix([[ 0., 1., 0., 1.],
             [ 1., 0., 1., 0.],
             [ 0., 1., 0., 1.],
             [ 1., 0., 1., 0.]]) In: print(nx.to_scipy_sparse_matrix(G)) Out:   (0, 1) 1
       (0, 3) 1
       (1, 0) 1
       (1, 2) 1
       (2, 1) 1
       (2, 3) 1
       (3, 0) 1
       (3, 2) 1 In: nx.convert_matrix.to_pandas_adjacency(G)    

下表显示了结果输出:

当然,如果要加载 NetworkX 图,则可以使用相反的函数(将函数名称中的from更改为to),并且可以加载 NetworkX 列表,边列表以及 NumPy,SciPy 和pandas结构的字典中的图。

图中每个节点的重要度量是其度。 在无向图中,节点的度数表示该节点具有的链接数。 对于有向图,度有两种类型:入度和出度。 这些分别计算节点的入站和出站链接。

让我们添加一个节点(以使图不平衡)并计算节点的度,如下所示:

In: G.add_edge(1, 3)
 nx.draw_networkx(G) plt.show()  

图的结果图如下:

本章中的图可能与在本地计算机上获得的图有所不同,因为图布局的初始化是使用随机参数进行的。

节点的度显示如下:

In: G.degree() Out: {1: 3, 2: 2, 3: 3, 4: 2}

对于大型图,此度量是不切实际的,因为输出字典的每个节点都有一个项目。 在这种情况下,通常使用节点度的直方图来近似其分布。 在以下示例中,构建了一个具有 10,000 个节点且链接概率为 1% 的随机网络。 然后,提取节点度的直方图,如下所示:

In: k = nx.fast_gnp_random_graph(10000, 0.01).degree()
 plt.hist(list(dict(k).values())) 

前面代码的直方图如下:

图算法

为了从图中获得见解,已经开发了许多算法。 在本章中,我们将在NetworkX中使用一个众所周知的图,即Krackhardt Kite图。 它是一个包含 10 个节点的伪图,通常用于证明图算法。 大卫·克拉克哈特(David Krackhardt)是风筝形状的结构的创造者。 它由两个不同的区域组成。 在第一个区域(由节点 0 到 6 组成)中,节点相互链接; 在另一个区域(节点 7 至 9)中,它们以链状连接:

In: G = nx.krackhardt_kite_graph()
 nx.draw_networkx(G) plt.show()

在以下图表中,您可以检查 Krackhardt Kite 的图结构:

让我们从连通性开始。 如果图的两个节点之间至少存在一条路径(即一系列节点),则将它们连接起来。

如果存在至少一条路径,则这两个节点之间的最短路径就是从源节点到目标节点必须经过(或遍历)的节点集合最短的路径。

请注意,在有向图中,必须遵循链接的指示。

NetworkX中,检查两个节点之间是否存在路径,计算最短路径并获取其长度非常容易。 例如,要检查节点 1 和 9 之间的连通性和路径,可以使用以下代码:

In: print(nx.has_path(G, source=1, target=9))
 print(nx.shortest_path(G, source=1, target=9)) print(nx.shortest_path_length(G, source=1, target=9)) Out: True
 [1, 6, 7, 8, 9] 4

此函数仅给出从一个节点到另一节点的最短路径。 如果我们要查看从节点 1 到达节点 9 的所有路径,该怎么办? Jin Yen 提出的算法提供了此答案,并在 NetworkX 的shortest_simple_paths函数中实现。 此函数返回图中节点源和目标之间从最短到最长的所有路径的生成器:

In: print (list(nx.shortest_simple_paths(G, source=1, target=9))) Out: [[1, 6, 7, 8, 9], [1, 0, 5, 7, 8, 9], [1, 6, 5, 7, 8, 9], 
      [1, 3, 5, 7, 8, 9], [1, 4, 6, 7, 8, 9], [1, 3, 6, 7, 8, 9], 
      [1, 0, 2, 5, 7, 8, 9], [...]]

最后,NetworkX 提供的另一个便捷函数是all_pairs_shortest_path函数,它返回一个 Python 字典,其中包含网络中所有节点对之间的最短路径。 例如,要查看从节点5的最短路径,您只需要查看键5的内容:

In: paths = list(nx.all_pairs_shortest_path(G))
 paths[5][1] Out: {0: [5, 0],
      1: [5, 0, 1],
      2: [5, 2],
      3: [5, 3],
      4: [5, 3, 4],
      5: [5],
      6: [5, 6],
      7: [5, 7],
      8: [5, 7, 8],
      9: [5, 7, 8, 9]}

不出所料,5与所有其他节点之间的路径以5本身开始。 请注意,此结构也是字典,因此,为了获得节点ab之间的最短路径,可以将其称为path[a][b]。 在大型网络上,请谨慎使用此功能。 实际上,在后台,它以O(N²)的计算复杂度来计算所有成对的最短路径。

节点中心性的类型

现在我们将开始讨论节点中心性,这大致代表了节点在网络内部的重要性。 它还给出了节点连接网络的程度的想法。 我们将在此处查看多种类型的中心性,包括中间性中心,度中心性,接近性中心性,谐波中心性和特征向量中心性。

  • 中间性中心性:这种类型的中心性可让您大致了解节点所在的最短路径的数量。 具有高度中间性的节点是网络的核心组件,许多最短的路径都经过它们。 在下面的示例中,NetworkX提供了一种直接的方法来计算所有节点之间的中间性:
In: nx.betweenness_centrality(G) Out: {0: 0.023148148148148143,
      1: 0.023148148148148143,
      2: 0.0,
      3: 0.10185185185185183,
      4: 0.0,
      5: 0.23148148148148148,
      6: 0.23148148148148148,
      7: 0.38888888888888884,
      8: 0.2222222222222222,
      9: 0.0}

可以想象,最高的中间性是通过节点7实现的。 这似乎非常重要,因为它是连接元素89(这是它们到网络的网关)的唯一节点。 相反,诸如924之类的节点位于网络的极端边界上,并且它们不存在于网络的任何最短路径中。 因此,可以删除这些节点而不会影响网络的连接性。

  • 度中心性:这种类型的中心性只是入射到节点上的顶点的百分比。 请注意,在有向图中,每个节点都有两个度中心:即度中心和度中心。 让我们看下面的例子:
In: nx.degree_centrality(G) Out: {0: 0.4444444444444444,
      1: 0.4444444444444444,
      2: 0.3333333333333333,
      3: 0.6666666666666666,
      4: 0.3333333333333333,
      5: 0.5555555555555556,
      6: 0.5555555555555556,
      7: 0.3333333333333333,
      8: 0.2222222222222222,
      9: 0.1111111111111111}

不出所料,节点3具有最高的中心度,因为它是链接数最多的节点(已连接到其他六个节点)。 相反,节点9是度数最低的节点,因为它只有一个边缘。

  • 紧密度中心性:要为每个节点计算此距离,请计算到所有其他节点的最短路径距离,将其平均后除以最大距离,然后取该值的倒数。 这导致得分在 0(平均距离较大)和 1(平均距离较低)之间。 在我们的示例中,对于节点9,最短路径距离为[1, 2, 3, 3, 4, 4, 4, 5, 5]。 然后将平均值(3.44)除以 5(最大距离)并从 1 中减去,得出的紧密度中心得分为 0.31。 您可以使用以下代码来计算示例图中所有节点的接近中心性:
In: nx.closeness_centrality(G) Out: {0: 0.5294117647058824,
      1: 0.5294117647058824,
      2: 0.5,
      3: 0.6,
      4: 0.5,
      5: 0.6428571428571429,
      6: 0.6428571428571429,
      7: 0.6,
      8: 0.42857142857142855,
      9: 0.3103448275862069}

具有高度接近中心性的节点是563。 实际上,它们是存在于网络中间的节点,平均而言,它们可以通过几跳到达所有其他节点。 最低分属于节点9。 实际上,它到达所有其他节点的平均距离相当高。

  • 谐波中心性:此度量类似于接近中心点,但不是距离的倒数之和的倒数,而是距离的倒数的总和。 这样做强调了距离的极限。 让我们看看我们的网络中的谐波距离是什么样的:
In: nx.harmonic_centrality(G) Out: {0: 6.083333333333333,
      1: 6.083333333333333,
      2: 5.583333333333333,
      3: 7.083333333333333,
      4: 5.583333333333333,
      5: 6.833333333333333,
      6: 6.833333333333333,
      7: 6.0,
      8: 4.666666666666666,
      9: 3.4166666666666665}

节点3是具有最高谐波中心度的节点,而56具有可比较但较低的值。 同样,这些节点位于网络的中心,平均而言,它们可以通过几跳到达所有其他节点。 相反,节点9具有最低的谐波中心度; 实际上,它平均距离所有其他节点最远。

  • 特征向量中心性:如果图是有向的,则节点表示 Web 页面,而边缘表示页面链接。 稍作修改的版本称为 PageRank。 该指标由拉里·佩奇(Larry Page)发明,是 Google,必应(Bing)以及其他搜索引擎的核心排名算法。 从随机冲浪者的角度来看,它可以使每个节点衡量该节点的重要性。 其名称源于以下事实:如果将图视为马尔可夫链,则图表示与最大特征值相关的特征向量。 因此,从这个角度来看,这种概率测度表示访问节点概率的静态分布。 让我们看下面的例子:
In: nx.eigenvector_centrality(G) Out: {0: 0.35220918419838565,
      1: 0.35220918419838565,
      2: 0.28583482369644964,
      3: 0.481020669200118,
      4: 0.28583482369644964,
      5: 0.3976909028137205,
      6: 0.3976909028137205,
      7: 0.19586101425312444,
      8: 0.04807425308073236,
      9: 0.011163556091491361}  

在此示例中,根据特征向量中心度度量,节点39的得分最高和最低。 与度中心性相比,特征值中心性提供了关于冲浪者在网络中的静态分布的想法,因为它针对每个节点不仅考虑直接连接的邻居(如度中心性),而且还考虑网络的整个结构。 如果该图表示网页及其连接,则表示它们是访问次数最多/最少(可能)的页面。

作为结束语,我们将介绍聚类系数。 简而言之,就是节点的邻居的比例也就是彼此的邻居(即存在的可能的三元组或三角形的比例)。 较高的值表示较高的湿度。 之所以这样命名,是因为它代表节点趋于聚集在一起的程度。 让我们看下面的例子:

In: nx.clustering(G) Out: {0: 0.6666666666666666,
      1: 0.6666666666666666,
      2: 1.0,
      3: 0.5333333333333333,
      4: 1.0,
      5: 0.5,
      6: 0.5,
      7: 0.3333333333333333,
      8: 0.0,
      9: 0.0}

在图的高度连接的部分中可以看到较高的值,而在连接最少的区域中可以看到较低的值。

分割网络

现在,让我们看一下将网络划分为节点的多个子网络的方式。 最常用的算法之一是 Louvain 方法,该方法专门用于精确检测大型图(具有一百万个节点)中的社区。 我们将首先介绍模块化措施。 这是对图结构的度量(它不是面向节点的),其形式上的数学定义非常长且复杂,并且超出了本书的范围(读者可以在这个页面)。 它直观地衡量将图划分为社区的质量,将实际的社区联系与随机的联系进行比较。 模块化得分在 -0.5 至 +1.0 之间; 值越高,划分就越好(组内连接密集,而组间连接稀疏)。

这是一个两步迭代的算法:首先是局部优化,然后是全局优化,然后是局部优化,依此类推:

  1. 第一步,该算法局部最大化小社区的模块化。
  2. 然后,它聚合同一社区的节点,并按层次构建一个图,其节点为社区。
  3. 该方法迭代地重复这两个步骤,直到达到最大的全局模块化评分。

为了在实际示例中窥视该算法,我们首先需要创建一个更大的图。 让我们考虑一个具有 100 个节点的随机网络:

  1. 在此示例中,我们将使用powerlaw算法构建图,该算法试图维持近似平均聚类。
  2. 对于添加到图中的每个新节点,还将向其中添加m个随机边缘,每个随机边缘都有p的概率来创建三角形。
  3. 源代码未包含在NetworkX中,但在名为community的单独模块中。 以下示例显示了该算法的实现:
In: import community 
    # Module for community detection and clustering 
 G = nx.powerlaw_cluster_graph(100, 1, .4, seed=101)
 partition = community.best_partition(G)

 for i in set(partition.values()):
 print("Community", i)
 members = [nodes for nodes in partition.keys() 
 if partition[nodes] == i]
 print(members)

 values = [partition.get(node) for node in G.nodes()]
 nx.draw(G, pos=nx.fruchterman_reingold_layout(G),
 cmap = plt.get_cmap('jet'), 
 node_color = values, 
 node_size=150,
 with_labels=False)
 plt.show()
 print ("Modularity score:", community.modularity(partition, G))

Out: Community 0
     [0, 46, 50, 61, 73, 74, 75, 82, 86, 96]
     Community 1
     [1, 2, 9, 16, 20, 28, 29, 35, 57, 65, 78, 83, 89, 93]
     [...] Modularity score: 0.7941026425874911 

程序的第一个输出是图中检测到的社区列表(每个社区都是节点的集合)。 在这种情况下,该算法检测到八个组。 我们想强调的是,我们没有指定要查找的输出社区的数量,但是它是由算法自动确定的。 这是并非所有聚类算法都具有的理想功能(例如,K 均值需要聚类数作为参数)。

然后,我们打印了图,为每个社区分配了不同的颜色。 您可以看到边缘节点上的颜色非常均匀:

最后,该算法返回解决方案的模块化评分:0.79(这是一个很高的评分)。

关于图的简短介绍将要介绍的最后一个算法是coloring。 这是一种为节点分配标签的图方式,邻居(即具有链接的节点)必须具有不同的标签(或颜色)。 为了解释该算法为何如此重要,我们将使用一个实际示例。 电信网络由遍布地球的不同频率的天线组成。 将每个天线视为一个节点,并将频率视为该节点的标签。 如果天线的距离小于定义的距离(假设距离足够近,会引起干扰),则说明它们与边缘相连。 我们是否可以找到分配的最低频率数(以使公司必须支付的费用降至最低)并避免相邻天线之间的干扰(即通过为链接节点分配不同的频率)?

该解决方案由图着色算法给出。 从理论上讲,这类算法的解决方案是 NP 难解的,尽管有很多近似方法可以快速获得次优解决方案,但几乎找不到最佳解决方案。 NetworkX 实现贪婪方法来解决着色问题。 该函数返回的是一个字典,其中每个节点(字典中的键)包含颜色(字典中键的值)。 作为示例,让我们在示例图中查看颜色的分配,然后让我们看到颜色:

In: G = nx.krackhardt_kite_graph()
    d = nx.coloring.greedy_color(G)
    print(d)
    nx.draw_networkx(G, 
       node_color=[d[n] for n in sorted(d.keys())])
    plt.show() Out:{3: 0, 5: 1, 6: 2, 0: 2, 1: 1, 2: 3, 4: 3, 7: 0, 8: 1, 9: 0}

这是图的图形,对链接的节点使用了不同的颜色:

正如预期的那样,链接的节点具有不同的颜色。 似乎对于这种网络配置,需要四种颜色。 如果这代表一个电信网络,它将向我们显示需要四个频率来避免干扰。

图加载,转储和采样

除了NetworkX以外,还可以使用其他软件来生成和分析图和网络。 可用于分析的最佳开源多平台软件之一称为 Gephi。 这是一种视觉工具,不需要编程技能。 可在这个页面免费获得。

与机器学习数据集中一样,甚至图都具有用于存储,加载和交换的标准格式。 这样,您可以使用 NetworkX 创建图,将其转储到文件中,然后使用 Gephi 进行加载和分析。

最常用的格式之一是图建模语言GML)。 现在,让我们看看如何将图转储到 GML 文件中:

In: dump_file_base = "dumped_graph"

    # Be sure the dump_file file doesn't exist
    def remove_file(filename):
        import os
        if os.path.exists(filename):
            os.remove(filename)

    G = nx.krackhardt_kite_graph()

    # GML format write and read
    GML_file = dump_file_base + '.gml'
    remove_file(GML_file)

    to_string = lambda x: str(x)
    nx.write_gml(G, GML_file, stringizer=to_string)
    to_int = lambda x: int(x)
    G2 = nx.read_gml(GML_file, destringizer = to_int)

    assert(G.edges() == G2.edges())

在前面的代码块中,我们执行了以下操作:

  1. 如果转储文件确实存在,我们将其删除。
  2. 然后,我们创建了一个图(风筝),然后,我们将其转储并加载。
  3. 最后,我们比较了原始结构和加载的结构,断言它们是相等的。

除了 GML,还有多种格式。 这些格式均具有不同的功能。 请注意,其中一些会删除与网络有关的信息(例如边缘/节点属性)。 与write_gml函数及其等效项read_gml相似,以下内容(名称不言而喻):

  • 邻接表(read_adjlistwrite_adjlist
  • 多行邻接表(read_multiline_adjlistwrite_multiline_adjlist
  • 边缘列表(read_edgelistwrite_edgelist
  • GEXF(read_gexfwrite_gexf
  • 泡菜(read_gpicklewrite_gpickle
  • GraphML(read_graphmlwrite_graphml
  • LEDA(read_ledaparse_leda
  • YAML(read_yamlwrite_yaml
  • 帕杰克(read_pajekwrite_pajek
  • GIS Shapefile(read_shpwrite_shp
  • JSON(加载/加载和转储/转储,并提供 JSON 序列化)

本章的最后一个主题是采样。 为什么要采样图? 我们对图进行采样是因为处理大型图有时是不切实际的(请记住,在最佳情况下,处理时间与图的大小成正比)。 因此,最好对其进行采样,通过在小规模场景中工作来创建算法,然后在全面问题上对其进行测试。 有几种采样图的方法。 在这里,我们将介绍三种最常用的技术。

在第一种技术(称为节点采样)中,节点的有限子集及其链接形成了采样集。 在第二种技术(称为链接采样)中,链接的子集形成采样集。 这两种方法既简单又快速,但是它们可能会为网络创建不同的结构。 第三种方法称为雪球采样。 初始节点,它的所有邻居以及邻居的邻居(以这种方式扩展选择,直到我们达到最大遍历深度参数)都形成了采样集。 换句话说,选择就像滚雪球一样。

请注意,您还可以对遍历的链接进行二次采样。 换句话说,每个链接都有p的概率,必须在输出集中进行跟踪和选择。

最后一种采样方法不是NetworkX的一部分,但是您可以在snowball_sampling.py文件中找到相同的实现。

在此示例中,我们将从具有alberto ID 的人开始对LiveJournal网络进行二次采样,然后递归扩展两次(在第一个示例中)和三次(在第二个示例中)。 在后一种情况下,每个链接后面都有 20% 的概率,因此减少了检索时间。 这是一个说明相同的示例:

In: import snowball_sampling
    import matplotlib.pyplot as plot
    my_social_network = nx.Graph()
    snowball_sampling.snowball_sampling(my_social_network, 2, 'alberto')
    nx.draw(my_social_network)
    ax = plot.gca()
    ax.collections[0].set_edgecolor("#000000")
    plt.show() Out:  Reching depth 0
 new nodes to investigate: ['alberto'] Reching depth 1 new nodes to investigate: ['mischa', 'nightraven', 'seraph76',
      'adriannevandal', 'hermes3x3', 'clymore', 'cookita', 'deifiedsoul',   
      'msliebling', 'ph8th', 'melisssa', '______eric_', 'its_kerrie_duhh',
      'eldebate']

这是采样代码的结果:

现在,我们将使用特定的采样率0.2

In: my_sampled_social_network = nx.Graph()
 snowball_sampling.snowball_sampling(my_sampled_social_network, 3,   
                                        'alberto', sampling_rate=0.2)
 nx.draw(my_sampled_social_network)
 ax = plot.gca()
 ax.collections[0].set_edgecolor("#000000")
 plt.show()  Out:  Reching depth 0
 new nodes to investigate: ['alberto']
 Reching depth 1 new nodes to investigate: ['mischa', 'nightraven', 'seraph76',
      'adriannevandal', 'hermes3x3', 'clymore', 'cookita', 'deifiedsoul', 
      'msliebling', 'ph8th', 'melisssa', '______eric_', 'its_kerrie_duhh', 
      'eldebate']
      Reching depth 2
      new nodes to investigate: ['themouse', 'brynna', 'dizzydez', 'lutin',  
      'ropo', 'nuyoricanwiz', 'sophia_helix', 'lizlet', 'qowf', 'cazling',   
      'copygirl', 'cofax7', 'tarysande', 'pene', 'ptpatricia', 'dapohead', 
      'infinitemonkeys', 'noelleleithe', 'paulisper', 'kirasha', 'lenadances', 
      'corianderstem', 'loveanddarkness', ...] 

结果图更详细:

总结

在本章中,我们学习了社交网络是什么,包括社交网络的创建和修改,表示以及社交网络及其节点的一些重要度量。 最后,我们讨论了大图的加载和保存以及处理方法。

在本章中,几乎所有基本数据科学算法都已介绍。 在第 4 章,“机器学习”中讨论了机器学习技术,并在此讨论了社交网络分析方法。 我们将在下一章“超越基础的深度学习”中最终讨论深度学习和神经网络的最先进,最前沿的技术。

七、超越基础的深度学习

在本章中,我们将介绍深度模型,并且将显示三个如何构建深度模型的示例。 更具体地说,在本章中,您将学习以下内容:

  • 深度学习的基础
  • 如何优化深度网络
  • 速度/复杂性/准确率问题
  • 如何将 CNN 用于图像分类
  • 如何将预训练的网络用于分类和迁移学习
  • 如何将 LSTM 用于序列操作

我们将使用 Keras 包,这是用于深度学习的高级 API,它将使用于深度学习的神经网络更加容易和易于理解,因为其特点是类似于乐高的方法(这里的积木是神经网络的组成元素)。

接近深度学习

深度学习是使用神经网络的经典机器学习方法的扩展:我们可以堆叠数百层,而不是构建几层网络(所谓的浅层网络) 创建一个精巧但功能更强大的学习器。 深度学习是当今最流行的人工智能AI)方法之一,因为它非常有效并且有助于解决模式识别中的许多问题,例如对象或序列识别。 使用标准的机器学习工具似乎牢不可破。

神经网络的思想来自人类的中枢神经系统,其中将能够处理简单信息的多个节点(或神经元)连接在一起以创建能够处理复杂信息的网络。 实际上,神经网络之所以这么称呼是因为它们可以自动和自适应地学习模型的权重,并且在足够复杂的网络架构下,它们能够近似任何非线性函数。 在深度学习中,节点通常称为单元或神经元。

让我们看看如何构建深度架构及其组成部分。 我们将从一个由三层组成的分类问题的小型深度架构开始,如下图所示:

该网络具有以下特征:

  • 它分为三层。 左边的一层称为输入层,右边的一层称为输出层,中间的一层称为隐藏层。 通常,在神经网络中,总是有一个输入层和一个输出层,以及零个或多个隐藏层(当隐藏层为零时,整个神经架构将有效地转化为逻辑回归系统)。
  • 输入层由五个单元组成,这意味着每个观察向量由五个数值特征组成(即观察矩阵有五列)。 请注意,特征必须是数字并且在值的有限范围内(为实现更好的数值收敛,理想情况下范围为 0 到 +1,但 -1 到 +1 也可以)。 因此,必须对分类特征进行预处理才能使其成为数值。
  • 输出层由三个单元组成,这意味着我们要在三个输出类之间进行区分(即执行三类分类)。 在发生回归问题的情况下,这一层应该只有一个单元。
  • 隐藏层由八个单元组成。 请注意,对于在深层结构中应显示多少个隐藏层以及每个应具有多少个单元没有任何规则。 这些参数留给科学家使用,通常,它们需要进行一些优化和微调才能发挥最佳表现。
  • 每个连接都有一个与之关联的权重。 在学习算法中对此进行了优化。

输入层的每个单元都连接到下一层的所有单元。 在同一层中的单元之间既没有连接,在彼此之间的距离大于 1 的两个层中的单元之间也没有连接。

在该示例中,信息流从输入到输出(最终通过隐藏层)传递; 在文献中,此网络称为前馈神经网络

它如何产生最终预测? 让我们逐步了解它的工作方式:

  1. 从隐藏层的顶部单元开始,它在第一层的输出向量(即输入观察向量)与第一层和第一层的第一单元之间的连接权重向量之间执行点积。 隐藏层。
  2. 然后使用该单元的激活函数转换该值。
  3. 对隐藏层中的所有单元重复此操作。
  4. 最后,我们可以以相同的方式计算隐藏层和输出层之间的前馈传播值,从而产生网络的输出。

这个过程看起来非常简单,它由多个令人尴尬的并行任务组成。 解释的最后一个缺失点是激活函数:它是什么,为什么需要它? 激活函数有助于使二元决策更加可分离(它使决策边界成为非线性,从而有助于更好地分离示例),并且它是每个单元的属性(或属性); 理想情况下,每个单元应该具有不同的激活函数,尽管通常将它们按层分组。

典型的激活函数是 Sigmoid,双曲正切和softmax(用于分类问题)函数,尽管目前最流行的函数是整流线性单元(或 ReLU),其输出是 0 和输入之间的最大值(其中输入是上一层输出和连接权重之间的点积)。

激活函数(单元数量和隐藏层数量)是深层网络的参数,科学家应该对其进行优化以获得更好的表现。

训练具有多个层次的神经网络是一项艰巨的操作,因为要调整的参数非常多(有时数百万):权重。 向连接分配权重的最常见方法是使用类似的方法来梯度下降,这种方法称为反向传播,因为它会将误差从输出层传播回输入层,并根据误差的梯度成比例地更新每个权重。 指向网络。 最初,权重是随机分配的,但是经过几步,它们应该收敛到最佳值。

这是对深度学习和神经网络的简短介绍; 如果您发现该主题很有趣,并且希望对其进行深入研究,我们建议您从 Packt 观看以下视频系列,在其中可以找到更好的解释和一些不错的技巧来掌握学习过程:

现在让我们看一些实用的东西:如何用神经网络解决分类问题。 在此示例中,我们将使用 Keras。 第一个是用于低级原语的 Python 库,通常用于深度学习,并能够利用最新的 GPU 和数值加速来有效处理多维数组。 Keras 是用于神经网络的高级,快速且模块化的 Python 库,能够在不同的数值计算框架之上运行,例如 TensorFlow,Microsoft Cognitive Tool(以前称为 CNTK)或 Theano。

使用 CNN 的图像分类

现在让我们将深度神经网络应用于图像分类问题。 在这里,我们将尝试根据其图像预测交通标志。 对于此任务,我们将使用 CNN卷积神经网络),该技术能够利用图像中附近像素之间的空间相关性,这是目前在解决此类问题时的深度学习水平。

数据集可在此处找到。 我们要感谢团队免费发布了数据集,并参考了涉及该数据集的出版物:

J. Stallkamp, M. Schlipsing, J. Salmen, and C. Igel. The German Traffic Sign Recognition Benchmark: A multi-class classification competition. In Proceedings of the IEEE International Joint Conference on Neural Networks, pages 14531460. 2011.

首先,下载数据集,然后将其解压缩。 数据集的文件名为GTSRB_Final_Training_Images.zip,解压缩后会发现一个名为GTSRB的新目录,其中包含与 Jupyter 笔记本相同目录中的所有图像。

下一步是导入 Keras 并检查后端是否配置正确。 在本章中,我们将使用 TensorFlow 后端,并且所有代码都在该后端上进行了测试。

后端选择是可逆的。 如果要从 TensorFlow 切换到另一个后端,请遵循此处的指南。 无论使用哪种后端,使用 Keras 编写的脚本都可以成功运行(尽管在计算时间和最小化错误方面的表现可能有所不同)。

要检查您的后端,请运行以下代码,并检查操作是否成功执行,并且结果输出与此处报告的结果匹配。

In: import keras

Out: Using TensorFlow backend.

现在是时候开始处理了,因此我们必须为任务定义一些静态参数。 其中有两种,主要是:我们要识别的不同信号的数量(即类别的数量)和图片的大小。 类别数量是 43; 也就是说,我们可以识别 43 种不同的交通标志。

第二个参数,图像大小,很重要,因为输入图像可以具有不同的大小和形状。 我们需要将它们调整为标准尺寸,以便对它们运行深层网络。 我们选择32x32像素作为标准像素:它足够小以识别信号,并且同时不需要太多内存(也就是说,每个灰度图像仅使用 1,024 字节或 1 KB)。 增加大小意味着增加保存数据集所需的内存,再加上深网的输入层和计算所需的时间。 在文献中,对于只有一个项目的图像,32x32是相当标准的选择。 因此,就我们而言,我们有充分的理由来决定该大小。

In: N_CLASSES = 43
 RESIZED_IMAGE = (32, 32)

在这一点上,我们必须读取图像并调整其大小,以创建观察矩阵和标签数组。 为此,我们执行以下步骤:

  1. 导入处理所需的模块。 最重要的是 Scikit-learn(即 Sklearn),其中包含处理图像的功能。

  2. 我们一个接一个地读取图像。 标签包含在路径中。 例如,图像GTSRB/Final_Training/Images/00000/00003_00024.ppm的标签为00000,即0; 并且图像GTSRB/Final_Training/Images/00025/00038_00005.ppm带有标签00025,即25。 标签存储为标记编码的数组,该数组是一个 43 单元长的数组,只有一个值为1的数组(所有其他均为0)。

  3. 图像以 PPM可移植像素图)格式存储,这是一种将像素存储在图像中的无损方式。 Scikit 图像或只是 skimage 可以使用函数imread读取该格式。 如果您的系统上尚未安装 Scikit 映像,只需在 shell 中键入以下内容:conda install scikit-imagepip install -U scikit-image。 返回的对象是 3D NumPy 数组。

  4. 然后将包含图像像素表示的 3D NumPy 数组(具有三个通道-红色,蓝色和绿色)转换为灰度。 在这里,我们首先转换为 LAB 颜色空间(请参见这个页面-该颜色空间比线性颜色更具感知性) 其他,这意味着颜色值的相同变化量应产生相同视觉重要性的影响),然后保留第一个通道(包含亮度)。 再次,此操作很容易使用 Skimage 完成。 结果,我们有一个包含图像像素的一维 NumPy 数组。

  5. 最终,再次使用 Skimage 函数将图像调整为32x32像素格式。

  6. 最后,将所有图像压缩到一个 4 维矩阵中:第一维用于索引数据集中的图像; 第二个和第三个分别代表图像的高度和宽度; 最后一个维度是渠道。 因此,拥有 39,208 张图像,并且所有32x32像素均为灰度,因此观察矩阵的形状为39,208, 32, 32, 1

  7. 标签被压缩成二维矩阵。 第一维是图像的索引,第二维是类。 由于具有相同数量的图像和 43 种可能的类别,因此将对矩阵进行整形39,208, 43

下面显示了翻译成代码的所有七个步骤:

In: import matplotlib.pyplot as plt
 import glob
 from skimage.color import rgb2lab
 from skimage.transform import resize
 from collections import namedtuple
 import numpy as np
 np.random.seed(101)
 %matplotlib inline

 Dataset = namedtuple('Dataset', ['X', 'y'])

 def to_tf_format(imgs):
 return np.stack([img[:, :, np.newaxis] for img in imgs], 
 axis=0).astype(np.float32)

 def read_dataset_ppm(rootpath, n_labels, resize_to):
 images = []
 labels = []
 for c in range(n_labels):
 full_path = rootpath + '/' + format(c, '05d') + '/'
 for img_name in glob.glob(full_path + "*.ppm"):
 img = plt.imread(img_name).astype(np.float32)
 img = rgb2lab(img / 255.0)[:,:,0]
 if resize_to:
 img = resize(img, resize_to, mode='reflect', 
 anti_aliasing=True)
 label = np.zeros((n_labels, ), dtype=np.float32)
 label[c] = 1.0
 images.append(img.astype(np.float32))
 labels.append(label)

 return Dataset(X = to_tf_format(images).astype(np.float32),
 y = np.matrix(labels).astype(np.float32))

 dataset = read_dataset_ppm('GTSRB/Final_Training/Images', N_CLASSES, 
 RESIZED_IMAGE)
 print(dataset.X.shape)
 print(dataset.y.shape)

Out: (39209, 32, 32, 1) 
     (39209, 43)

数据集由近 40,000 张图像组成; 在更改颜色和调整大小之后,让我们看看它们的第一个样子:

In: plt.imshow(dataset.X[0, :, :, :].reshape(RESIZED_IMAGE))
 print("Label:", dataset.y[0, :])

Out: Label: [[1\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 
              0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 
              0\. 0\. 0.]]

以下是绘制的样本图像:

即使图像的清晰度很低(32x32像素),我们也可以立即识别出代表哪个符号。 到目前为止,整形操作似乎使图像甚至对人类都无法理解。 再次注意,标签是一个 43 维向量。 由于此图像属于第一类(即类00000),因此仅标签的第一元素不为null

不同类别的另一个元素如下所示。 这是数据集中的图像编号 1,000,其类别为2(实际上,这是一个不同的符号):

现在让我们将数据集分为训练和测试。 我们使用 Scikit-learn 随机分离并随机排列图像。 在此单元格中,我们选择数据集的 25% 作为测试集; 也就是说,几乎有 10,000 张图像,剩下的 29K+ 张图像用于训练深层网络:

In: from sklearn.model_selection import train_test_split
 idx_train, idx_test = train_test_split(range(dataset.X.shape[0]), 
 test_size=0.25, 
                                           random_state=101)
 X_train = dataset.X[idx_train, :, :, :]
 X_test = dataset.X[idx_test, :, :, :]
 y_train = dataset.y[idx_train, :]
 y_test = dataset.y[idx_test, :]

 print(X_train.shape)
 print(y_train.shape)
 print(X_test.shape)
 print(y_test.shape)

Out: (29406, 32, 32, 1) 
     (29406, 43)
     (9803, 32, 32, 1)
     (9803, 43)

这是创建卷积深度网络的时刻。 我们从一个简单易懂的神经网络开始。 然后,我们将转到更复杂但更准确的地方。

用 Keras 创建深层网络非常容易:您必须依次定义所有层。 Keras 对象需要按名为Sequential的序列定义层。 在这里,我们将创建一个包含三层的深层网络:

  1. 输入层定义为卷积 2D 层​​(实际上是图像和内核之间的卷积运算),包含 32 个3x3像素形状的过滤器,并带有 ReLU 类型的激活层。
  2. 平滑前一个输出层; 也就是说,将展开方形观察以创建一维数组。
  3. 激活softmax的密集输出层,由 43 个单元组成,每个单元一个。

然后编译模型,最后将其拟合到训练数据。 在此操作期间,我们选择了以下内容:

  • 优化器:SGD,最简单的一种
  • 批量大小:每批 32 张图像
  • 周期数:10

这是将生成我们刚刚描述的模型的代码:

In: from keras.models import Sequential
 from keras.layers.core import Dense, Flatten
 from keras.layers.convolutional import Conv2D
 from keras.optimizers import SGD
 from keras import backend as K
 K.set_image_data_format('channels_last')

 def cnn_model_1():
 model = Sequential()
 model.add(Conv2D(32, (3, 3), 
 padding='same',
 input_shape=(RESIZED_IMAGE[0], RESIZED_IMAGE[1], 1),
 activation='relu'))
 model.add(Flatten())
 model.add(Dense(N_CLASSES, activation='softmax'))
 return model 
    cnn = cnn_model_1()
 cnn.compile(loss='categorical_crossentropy',
 optimizer=SGD(lr=0.001, decay=1e-6),
 metrics=['accuracy'])
 cnn.fit(X_train, y_train,
 batch_size=32,
 epochs=10,
 validation_data=(X_test, y_test))

Out: Train on 29406 samples, validate on 9803 samples
 Epoch 1/10
 29406/29406 [==============================] - 11s 368us/step - 
 loss: 2.7496 - acc: 0.5947 - val_loss: 0.6643 - val_acc: 0.8533 
 Epoch 2/10
 29406/29406 [==============================] - 10s 343us/step - 
 loss: 0.4838 - acc: 0.8937 - val_loss: 0.4456 - val_acc: 0.9001
 [...]
 Epoch 9/10
 29406/29406 [==============================] - 10s 337us/step - 
 loss: 0.0739 - acc: 0.9876 - val_loss: 0.2306 - val_acc: 0.9553
 Epoch 10/10
 29406/29406 [==============================] - 10s 343us/step - 
 loss: 0.0617 - acc: 0.9897 - val_loss: 0.2208 - val_acc: 0.9574

训练集的最终准确率接近 99%,而测试集则接近 96%。 我们有点过拟合,但让我们在测试集中查看该模型的混淆矩阵和分类报告。 我们还将打印混淆矩阵的log2,以更好地识别错误分类。

为此,我们首先需要预测标签,然后应用argmax运算符选择最可能的类别:

In: from sklearn.metrics import classification_report, confusion_matrix

 def test_and_plot(model, X, y):
 y_pred = cnn.predict(X)
 y_pred_softmax = np.argmax(y_pred, axis=1).astype(np.int32)
 y_test_softmax = np.argmax(y, axis=1).astype(np.int32).A1
 print(classification_report(y_test_softmax, y_pred_softmax))
 cm = confusion_matrix(y_test_softmax, y_pred_softmax)
 plt.imshow(cm, interpolation='nearest', cmap=plt.cm.Blues)
 plt.colorbar()
 plt.tight_layout()
 plt.show()
         # And the log2 version, to emphasize the misclassifications
 plt.imshow(np.log2(cm + 1), interpolation='nearest', 
 cmap=plt.get_cmap("tab20"))
 plt.colorbar()
 plt.tight_layout()
 plt.show()

test_and_plot(cnn, X_test, y_test)

Out:
 precision    recall  f1-score   support
 0       0.87      0.90      0.88        67
 1       0.97      0.94      0.95       539
 2       0.93      0.94      0.94       558
 [........]
 40       0.93      0.96      0.95        85
 41       0.92      0.94      0.93        47
 42       1.00      0.91      0.95        53
     avg / total       0.96      0.96      0.96      9803

以下是诊断图,可为您提供有关模型表现的证据:

这是混淆矩阵的log2版本:

分类似乎已经很好了。 我们可以做得更好,避免过拟合吗? 是的,这是我们可以使用的:

  • 丢弃层:这等效于正则化,并且可以防止过拟合。 基本上,在训练的每个步骤中,都会停用一部分单元,因此该层的输出不会过分依赖其中的一些单元。
  • 批量规范化层:通过减去批次平均值并将其除以标准差,此 z 标准化层。 这对于更新数据很有用,并且在每个步骤都会放大/衰减信号。
  • 最大池化:这是一个非线性变换,用于通过对内核下的每个区域应用最大过滤器来对输入进行下采样。 用于选择最大特征,该特征在同一类中的位置可能略有不同。

除此之外,总是有改变深层网络和训练属性的空间。 也就是说,优化程序(及其参数),批量大小和周期数。 在下一个单元格中,这里是具有以下几层的改进的深层网络:

  1. 卷积层,具有 32 个3x3过滤器和 ReLU 激活
  2. BatchNormalization
  3. 另一个卷积层,然后是BatchNormalization
  4. 丢弃层,被丢弃的可能性为 0.4
  5. 展开层
  6. 512 单元密集层,具有 ReLU 激活
  7. BatchNormalization
  8. 丢弃层,被丢弃的可能性为 0.5
  9. 输出层; 如上例所示,这是一个具有 43 个单元的softmax密集层

那将如何在我们的数据集上执行?

In: from keras.layers.core import Dropout
 from keras.layers.pooling import MaxPooling2D
 from keras.optimizers import Adam
 from keras.layers import BatchNormalization

 def cnn_model_2():
 model = Sequential()
 model.add(Conv2D(32, (3, 3), padding='same',
 input_shape=(RESIZED_IMAGE[0], RESIZED_IMAGE[1], 1),
 activation='relu'))
 model.add(BatchNormalization())
 model.add(Conv2D(32, (3, 3), 
 padding='same', 
 input_shape=(RESIZED_IMAGE[0], RESIZED_IMAGE[1], 1),
 activation='relu'))
 model.add(BatchNormalization())
 model.add(MaxPooling2D(pool_size=(2, 2)))
 model.add(Dropout(0.4))
 model.add(Flatten())
 model.add(Dense(512, activation='relu'))
 model.add(BatchNormalization())
 model.add(Dropout(0.5))
 model.add(Dense(N_CLASSES, activation='softmax'))
 return model

 cnn = cnn_model_2()
 cnn.compile(loss='categorical_crossentropy',
 optimizer=Adam(lr=0.001, decay=1e-6), metrics=['accuracy'])
 cnn.fit(X_train, y_train,
 batch_size=32,
 epochs=10,
 validation_data=(X_test, y_test))

Out: Train on 29406 samples, validate on 9803 samples
 Epoch 1/10
 29406/29406 [==============================] - 24s 832us/step - 
 loss: 0.7069 - acc: 0.8145 - val_loss: 0.1611 - val_acc: 0.9584
 Epoch 2/10
 29406/29406 [==============================] - 23s 771us/step - 
 loss: 0.1784 - acc: 0.9484 - val_loss: 0.1065 - val_acc: 0.9714
 [...]
 Epoch 10/10
 29406/29406 [==============================] - 23s 770us/step - 
 loss: 0.0370 - acc: 0.9878 - val_loss: 0.0332 - val_acc: 0.9920

 <keras.callbacks.History at 0x7fd7ac0f17b8>

训练集的准确率与测试集的准确率相似,均为约 99%; 也就是说,每 100 张图像中有 99 张用正确的标签分类! 该网络更长,需要更多的内存和计算能力,但不太容易过拟合并且表现更好。

现在,让我们看一下分类报告和混淆矩阵(完整版本和log2版本):

In: test_and_plot(cnn, X_test, y_test)

Out: 
 precision    recall  f1-score   support
 0       1.00      0.97      0.98        67
 1       1.00      0.98      0.99       539
 2       0.99      1.00      0.99       558
 [..........]
 38       1.00      1.00      1.00       540
 39       1.00      1.00      1.00        60
 40       1.00      1.00      1.00        85
 41       0.98      0.96      0.97        47
 42       1.00      1.00      1.00        53
 avg / total       0.99      0.99      0.99      9803

这是结果的视觉表示:

显然,错误分类的数量已大大减少。 现在,让我们尝试通过更改参数来做更好的事情。

使用预训练的模型

如您在前面的示例中所见,增加网络的复杂性会增加时间和训练所需的内存。 有时,我们必须接受我们没有足够强大的机器来尝试所有组合的机器。 在那种情况下我们该怎么办? 基本上,我们可以做两件事:

  • 简化网络; 也就是说,通过删除参数和变量
  • 使用预训练的网络,该网络已经由功能足够强大的人进行过训练

在这两种情况下,我们都将在次优条件下工作,因为深度网络不会像我们曾经使用的那样强大。 更具体地说,在第一种情况下,网络的精度不是很高,因为我们的参数较少。 在第二种情况下,我们必须应对其他人的决策和训练。 尽管这不是一件容易的事,但预训练的模型也可以根据您的数据集进行微调。 在这种情况下,网络将不会随机初始化参数。 尽管这很有趣,但是此操作不在本书的讨论范围之内。

在本节中,我们将快速展示如何使用预训练的模型,这是一种常见的处理方法。 请记住,预训练模型可以在多种情况下使用:

  • 特征增强,以将特征(在这种情况下为预测标签)以及观察向量添加到模型中
  • 迁移学习,以将更多特征(来自一层或模型层的系数)以及观察向量添加到模型中
  • 预测;也就是说,计算标签

现在让我们看看如何使用预训练的网络来实现我们的目的。

在 Keras 中,可以从此处获得各种预训练的模型

首先下载一些图像进行测试。 在以下示例中,我们将使用 Caltech 提供的数据集,该数据集可在此处使用

我们要感谢数据集的作者,并建议阅读他们的论文:L. Fei-Fei, R. Fergus and P. Perona. One-Shot learning of object categories. IEEE Trans. Pattern Recognition and Machine Intelligence

它包含 101 个类别的几张图像,并以tar.gz格式显示。

现在,用一个新的笔记本,导入我们将要使用的模块。 在此示例中,我们将使用 InceptionV3 预训练网络,该网络能够很好地识别图像中的对象。 它是由 Google 开发的,其输出可与人眼媲美。

  1. 首先,我们导入建立网络,预处理输入并提取预测所需的特征:
In: from keras.applications.inception_v3 import InceptionV3
    from keras.applications.inception_v3 import preprocess_input
 from keras.applications.inception_v3 import decode_predictions
 from keras.preprocessing import image
 import numpy as np
 import matplotlib.pyplot as plt
 %matplotlib inline

Out: Using TensorFlow backend.
  1. 现在,让我们加载庞大的网络及其系数:
In: model = InceptionV3(weights='imagenet')

很简单,不是吗?

  1. 下一步(也是最后一步)是创建一个函数进行预测。 在这种情况下,我们将预测前三个标签:
In: def predict_top_3(model, img_path):
 img = image.load_img(img_path, target_size=(299, 299))
 plt.imshow(img)
 x = image.img_to_array(img)
 x = np.expand_dims(x, axis=0)
 x = preprocess_input(x)
 preds = model.predict(x)
 print('Predicted:', decode_predictions(preds, top=3)[0])

基本上,此函数会加载图像并将其调整为299x299像素(这是预训练网络 InceptionV3 的默认输入大小),并将图像转换为模型的正确格式。 之后,它会预测图像的所有标签并选择(并打印)前三个。

让我们通过使用预训练的模型并根据概率来询问前三个预测,看看它如何与示例图像配合使用:

In: predict_top_3(model, "101_ObjectCategories/umbrella/image_0001.jpg")

我们要预测的图像以及前三个预测的结果输出如下:

Out: Predicted: [('n04507155', 'umbrella', 0.88384396), 
                 ('n04254680', 'soccer_ball', 0.07257448), 
                 ('n03888257', 'parachute', 0.012849103)]

我们确认这是一个很好的结果; 第一个标签(得分为 88%)是一把雨伞,其次是足球和降落伞。 现在让我们测试一个肯定更困难的图像,该图像的标签未包含在 InceptionV3 训练集中:

In: predict_top_3(model, "101_ObjectCategories/bonsai/image_0001.jpg")

这是图像及其最重要的三个结果:

Out: Predicted: [('n02704792', 'amphibian', 0.20315942), 
                 ('n04389033', 'tank', 0.07383019), 
                 ('n04252077', 'snowmobile', 0.055828683)]

正如预期的那样,由于它不在其预定义的类别之内,因此网络无法识别第一个预测标签中的盆景。

实际上,可以通过所谓的迁移学习技术来训练预训练的模型甚至识别全新的类别。 这项技术不在本书的讨论范围之内,但是您可以从 Keras 的博客中的示例中了解它

最后,让我们看看如何从中间层提取特征,如下所示:

  1. 第一步,让我们验证标签名称:
In: print([l.name for l in model.layers])

Out: ['input_1', 'conv2d_1', 'batch_normalization_1', 
      ..........
 'activation_94', 'mixed10', 'avg_pool', 'predictions']
  1. 我们可以选择任何一层; 我们将继续进行softmax预测之前的预测。 让我们创建一个对象Model,其输出为avg_pool层:
In: from keras.models import Model
 feat_model = Model(inputs=model.input, 
 outputs=model.get_layer('avg_pool').output) 
 def extract_features(feat_model, img_path):
 img = image.load_img(img_path, target_size=(299, 299))
 x = image.img_to_array(img)
 x = np.expand_dims(x, axis=0)
 x = preprocess_input(x)
 return feat_model.predict(x)
  1. 最后,要提取图片的特征,让我们用图像调用上一个函数:
In: f = extract_features(feat_model, 
                    "101_ObjectCategories/bonsai/image_0001.jpg")
 print(f.shape)
 print(f)

Out: (1, 2048)
 [[0.12340261 0.0833823 0.7935947 ... 0.50869745 0.34015656]]

如您所见,avg_pool层包含2048单元,该函数的输出正好是 2,048D 数组。 现在,您可以将此数组连接到您选择的任何其他特征数组。

处理时间序列

本章的最后一个示例是关于时间序列的。 更具体地说,我们将看到如何处理文本,这是一个可变长度的单词序列。

一些数据科学算法使用“词袋”方法处理文本。 也就是说,他们不在乎单词的位置以及它们在文本中的放置方式,他们只是在乎它们的存在与否(以及频率)。 取而代之的是,专门设计一类特殊的深度网络来对顺序很重要的序列进行操作。

一些示例如下:

  • 根据历史数据预测未来股价:在这种情况下,输入是数字序列,而输出是数字

  • 预测市场是上升还是下降:在这种情况下,给定一系列数字,我们要预测一个类别(上升或下降)

  • 将英语文本翻译为法语:在这种情况下,输入序列将转换为另一个序列

  • 聊天机器人:在这种情况下,输入和输出都是序列(使用相同语言)

对于此示例,让我们做些简单的事情。 我们将尝试检测电影评论的情感。 在此特定示例中,输入数据是单词序列(和顺序计数!),输出是二进制标签(即情感为或为)。

让我们开始导入数据集。 幸运的是,Keras 已经包含了该数据集,并且已经进行了预索引。 也就是说,每个评论不是由单词组成,而是由字典索引组成。 另外,可以只选择最重要的单词,并使用此代码选择包含最重要的25000单词的字典:

In: from keras.datasets import imdb
 ((data_train, y_train), 
     (data_test, y_test)) = imdb.load_data(num_words=25000)

让我们看看datashape里面是什么:

In: print(data_train.shape)
 print(data_train[0])
 print(len(data_train[0]))

Out: (25000,) 
     [1, 14, 22, 16, 43, 530, .......... 19, 178, 32]
     218

首先,有25000条评论; 即观察。 其次,每条评论由 1 到 24,999 之间的数字序列组成; 1 表示序列的开始,而最后一个数字表示不在词典中的单词。 请注意,每个评论的大小都不同; 例如,第一个是218个字的长度。

现在是时候将所有序列修剪或填充到特定大小。 使用 Keras,这很容易做到,并且为了填充,添加了整数0

In: from keras.preprocessing.sequence import pad_sequences
 X_train = pad_sequences(data_train, maxlen=100) X_test = pad_sequences(data_test, maxlen=100)

现在,我们的训练矩阵为矩形。 修剪/填充操作之后的第一个元素变为:

In: print(X_train[0])
 print(X_train[0].shape) Out: [1415, .......... 19, 178, 32]
     (100,)

对于此观察,仅保留最后的100字。 总体而言,现在,所有观测值均具有100维。 现在,让我们创建一个时间深度模型来预测评论情感。

这里提出的模型分为三层:

  1. 嵌入层。 原始词典设置为 25,000 个单词,组成嵌入的单元数(即层的输出)为 256。
  2. LSTM 层。 LSTM 代表长短期记忆,它是最强大的序列深度模型之一。 得益于其深厚的架构,它能够从序列中相距较远的单词中提取信息(因此得名)。 在此示例中,像元数设置为256(作为上一层输出尺寸),其中0.4的缺失用于正则化。
  3. 具有 Sigmoid 活化的密集层。 这就是我们需要的二分类器。

这是执行此操作的代码:

In: from keras.models import Sequential
 from keras.layers import LSTM, Dense
 from keras.layers.embeddings import Embedding
 from keras.optimizers import Adam
 model = Sequential()
 model.add(Embedding(25000, 256, input_length=100))
 model.add(LSTM(256, dropout=0.4, recurrent_dropout=0.4))
 model.add(Dense(1, activation='sigmoid'))
 model.compile(loss='binary_crossentropy',
 optimizer=Adam(),
 metrics=['accuracy'])

 model.fit(X_train, y_train,
 batch_size=64,
 epochs=10,
 validation_data=(X_test, y_test))
 Out: Train on 25000 samples, validate on 25000 samples
 Epoch 1/10
 25000/25000 [==============================] - 139s 6ms/step - 
 loss:0.4923 - acc:0.7632 - val_loss:0.4246 - val_acc:0.8144
 Epoch 2/10
 25000/25000 [==============================] - 139s 6ms/step - 
 loss:0.3531 - acc:0.8525 - val_loss:0.4104 - val_acc: 0.8235
 Epoch 3/10
 25000/25000 [==============================] - 138s 6ms/step - 
 loss:0.2564 - acc:0.9000 - val_loss:0.3964 - val_acc: 0.8404
 ...
 Epoch 10/10
 25000/25000 [==============================] - 138s 6ms/step - 
 loss:0.0377 - acc:0.9878 - val_loss:0.8090 - val_acc:0.8230

这就是 25K 审查测试数据集的准确率。 这是可以接受的结果,因为我们通过这样一个简单的模型实现了 80% 以上的正确分类。 如果您想对其进行改进,可以尝试使架构更复杂,但是请始终记住,通过增加网络的复杂性,训练和预测结果所需的时间,以及内存占用量会增加。

总结

在本章中,我们了解了深度网络的基本知识和一些高级模型。 我们介绍了神经网络的工作原理以及浅层网络和深度学习之间的区别。 然后,我们学习了如何构建能够对交通标志图像进行分类的 CNN 深度网络。 我们还使用预训练的网络预测了图像的类别。 使用评论中发现的文字来检测电影评论的情感也是学习的一部分。

深度学习模型确实非常强大,但是要付出很多处理自由度和许多系数来训练的代价,这需要手头有大量数据。

在下一章中,我们将看到当数据量太大而无法由单台计算机处理和处理时,Spark 将如何提供帮助。

八、大数据和 Spark

世界上存储的数据量正以准指数的方式增长。 如今,对于数据科学家而言,每天必须处理几 TB 的数据已不再是不寻常的请求,并且要使事情变得更加复杂,这意味着必须处理来自许多不同的异构系统的数据。 此外,尽管您需要处理的数据量很大,但您的业务期望是在短时间内不断生成模型,因为您只是在处理玩具数据集。

在围绕数据科学要点的旅程的总结中,我们不能忽略数据科学中如此关键的必要性。 因此,我们将向您介绍一种处理大量数据,通过多台计算机进行扩展以获取数据,对其进行处理以及构建有效的机器学习算法的新方法。 在我们进行必要的介绍之后,处理大量数据并产生有效的机器学习模型就不会了。

在本章中,您将:

  • 了解分布式框架,解释 Hadoop,MapReduce 和 Spark 技术
  • 从 PySpark(Spark 的 Python API 接口)开始
  • 试验弹性分布式数据集,这是一种处理大数据的新方法
  • 在 Spark 中的分布式系统中定义和共享变量
  • 使用 Spark 中的DataFrame处理数据
  • 在 Spark 中应用机器学习算法

在本章的最后,只要有适当的计算机集群,您将可以面对任何数据科学问题,而无论手头的数据规模如何。

从一台独立的机器到一堆节点

处理大数据不仅仅是大小问题; 这实际上是一个多方面的现象。 实际上,根据 3V 模型(体积,速度和变化),可以使用三个(正交)标准对在大数据上运行的系统进行分类:

  • 要考虑的第一个标准是系统处理数据所达到的速度。 尽管几年前,速度还是用来表示系统处理批量的速度,但如今,速度表示系统是否可以提供流数据的实时输出。
  • 第二个标准是体积; 也就是说,可以处理多少信息。 它可以用行数或特征数来表示,也可以仅用字节数表示。 在流数据中,卷指示到达系统的数据的吞吐量。
  • 最后一个标准是种类; 即数据源的类型。 几年前,这种类型受到结构化数据集的限制,但如今,数据可以结构化(表,图像等),半结构化(JSON,XML 等)和非结构化(网页, 社交数据,等等)。 通常,大数据系统会尝试处理尽可能多的相关源并将各种源混合在一起。

除了这些标准,最近几年还出现了许多其他 V,它们试图解释大数据的其他特征。 其中一些如下:

  • 准确率:提供数据中包含的异常,偏差和噪声的指示; 最终表明其准确率
  • 波动率:指示可以将数据用于提取有意义的信息的时间
  • 有效率:数据的正确性
  • 价值:根据数据指示投资回报率

近年来,所有 V 都急剧增加。 现在,许多公司发现他们保留的数据具有可被货币化的巨大价值,并且他们希望从中提取信息。 技术挑战已经转向具有足够的存储和处理能力,以便能够快速,大规模并使用不同的输入数据流来提取有意义的见解。

当前的计算机,甚至是最新,最昂贵的计算机,其磁盘,内存和 CPU 的数量也有限。 每天处理太字节(PB)的信息并及时生成模型似乎非常困难。 此外,需要复制同时包含数据和处理软件的独立服务器。 否则,它可能成为系统的单点故障。

因此,大数据世界已经转移到了集群:它们由可变数量的并不十分昂贵的节点组成,并且位于高速互联网连接上。 通常,有些集群专用于存储数据(大硬盘,少 CPU 和少量内存),而另一些专用于处理数据(功能强大的 CPU,中等到大量内存,以及小硬盘)。 此外,如果正确设置了群集,则可以确保可靠性(没有单点故障)和高可用性。

理解为什么我们需要分布式框架

建立集群的最简单方法是将某些节点用作存储节点,而另一些节点用作处理节点。 这种配置似乎非常易于使用,因为我们不需要复杂的框架来处理这种情况。 实际上,许多小型集群正是以这种方式构建的:几个服务器处理数据(加上副本),另一个服务器处理数据。 尽管这可能是一个不错的解决方案,但是由于许多原因,它并不常用:

  • 它仅适用于令人尴尬的并行算法。 如果算法要求在处理服务器之间共享一个公共的内存区域,则无法使用此方法。
  • 如果一个或多个存储节点死亡,则不能保证数据是一致的。 (考虑一种情况,其中节点及其副本同时死亡,或者节点在尚未复制的写操作之后死亡。)
  • 如果处理节点死亡,我们将无法跟踪其正在执行的进程,从而很难在另一个节点上恢复处理。
  • 如果网络出现故障,则在恢复正常后很难预测这种情况。

崩溃事件(甚至不止一个)很可能发生,这是一个事实,要求必须事先考虑这种情况并进行适当处理,以确保对数据进行操作的连续性。 此外,当使用便宜的硬件或更大的群集时,几乎可以肯定至少有一个节点将发生故障。 到目前为止,绝大多数集群框架都使用名为分治的方法(拆分和征服):

  • 有用于数据节点的专用模块和用于数据处理节点(也称为工作器)的其他一些专用模块。
  • 数据跨数据节点复制,一个节点为主节点,确保写入和读取操作均成功。
  • 处理步骤在工作节点之间划分。 它们不共享任何状态(除非存储在数据节点中),并且它们的主设备确保所有任务都正确且正确地执行。

Hadoop 生态系统

Apache Hadoop 是用于群集上的分布式存储和分布式处理的非常流行的软件框架。 它的优势在于价格(免费),灵活性(它是开源的,尽管它是用 Java 编写的,但可以被其他编程语言使用),可伸缩性(它可以处理由数千个节点组成的集群)和鲁棒性。 (它的灵感来自 Google 的已发表论文,并且自 2011 年以来一直存在),使其成为处理和处理大数据的事实上的标准。 此外,Apache 基金会的许多其他项目都扩展了其功能。

Hadoop 架构

从逻辑上讲,Hadoop 由两部分组成:分布式存储(HDFS)和分布式处理(YARN 和 MapReduce)。 尽管代码非常复杂,但整体架构相当容易理解。 客户端可以通过两个专用模块访问存储和处理。 然后,他们负责在所有工作节点之间分配作业,如下图所示:

所有 Hadoop 模块均作为服务(或实例)运行; 也就是说,物理或虚拟节点可以运行其中的许多节点。 通常,对于小型集群,所有节点都运行分布式计算和处理服务。 对于大型集群,最好将两个功能分开并专门化节点。

我们将详细看到两层提供的功能。

Hadoop 分布式文件系统

**Hadoop 分布式文件系统(HDFS)**是容错的分布式文件系统,旨在在低成本硬件上运行,并且能够处理非常大的数据集(数百 PB 到 EB 级) )。 尽管 HDFS 需要快速的网络连接来跨节点传输数据,但是延迟不能像传统文件系统中那样低(它可能在几秒钟的时间内)。 因此,HDFS 被设计用于批量和高吞吐量。 每个 HDFS 节点都包含文件系统数据的一部分; 在其他实例中也将复制相同的数据,这可确保高吞吐量访问和容错。

HDFS 的架构是主从结构。 如果主服务器(称为名称节点)发生故障,则表明有一个辅助节点/备份节点可以控制。 所有其他实例都是从属(数据节点); 如果其中一个发生故障,这并不是问题,因为 HDFS 就是在设计时考虑到这一点的,因此不会丢失任何数据(进行冗余复制),并且可以将操作迅速重新分配给尚存的节点。 数据节点包含数据块:HDFS 中保存的每个文件被分解成块(或块),每个文件通常为 64 MB,然后在一组数据节点中进行分发和复制。 名称节点仅存储分布式文件系统中文件的元数据; 它不存储任何实际数据,而只是存储有关如何访问其管理的多个数据节点中文件的正确指示。

请求读取文件的客户端必须首先联系名称节点,它将返回一个表,其中包含块及其位置的有序列表(如数据节点中所示)。 此时,客户端应分别与数据节点联系,下载所有块并重建文件(通过将块附加在一起)。

要写入文件,客户端应首先联系名称节点,后者将首先决定如何处理请求,然后更新其记录并使用文件每个块的写入位置的数据节点的有序列表回复客户端。 客户端现在将根据名称节点答复中的报告,将块联系并上传到数据节点。 命名空间查询(例如,列出目录内容,创建文件夹等)由名称节点通过访问其元数据信息完全处理。

此外,名称节点还负责正确处理数据节点故障(如果未接收到心跳数据包,则标记为死),并将其数据复制到其他节点。

尽管这些操作很长且难以实现,但是由于许多库和 HDFS Shell,它们对用户完全透明。 在 HDFS 上进行操作的方式与当前在文件系统上进行的操作非常相似,这是 Hadoop 的一大优势:隐藏复杂性并让用户简单地使用它。

MapReduce

MapReduce 是在 Hadoop 的最早版本中实现的编程模型。 这是一个非常简单的模型,旨在并行处理分布式群集上的大型数据集。 MapReduce 的核心由两个可编程功能组成:一个执行过滤的映射器和一个执行聚合的归约器,以及一个将对象从映射器移动到正确的归约器的 shuffler。 Google 于 2004 年在 MapReduce 上获得了专利,专利发表了几个月。

具体来说,这是 MapReduce 用于 Hadoop 实现的步骤:

  • 数据分块器:从文件系统读取数据并将其拆分为块。 块是输入数据集的一部分,通常是固定大小的块(例如,从数据节点读取的 HDFS 块)或另一个更合适的拆分。 例如,如果我们要计算文本文件中的字符,单词和行数,则很好的拆分可以是一行文本。
  • 映射器:从每个块中生成一系列键值对。 每个映射器实例将相同的映射函数应用于不同的数据块。 继续前面的示例,对于每一行,在此步骤中生成了三个键值对-一个键值对包含该行中的字符数(键可以简单地是字符串),其中一个包含单词个数(在这种情况下,键必须不同,所以假设是单词),其中一个包含行数,该行数始终为 1(在这种情况下,键可以是)。
  • 打乱器:从可用键的数量和可用的化简器的数量,混洗器将具有相同键的所有键-值对分配给同一异化器。 通常,此操作是计算键的哈希值,将其除以精简器的数量,然后使用余数来指出特定的精简器。 这应确保每个异径管有足够数量的钥匙。 该功能不是用户可编程的,而是由 MapReduce 框架提供的。
  • 归约器:每个简化器都接收一组特定键的所有键值对,并且可以产生零个或多个聚合结果。 在该示例中,所有与单词键相关的值都到达简化器; 它的工作只是总结所有值。 其他键也一样,这将产生三个最终值:字符数,单词数和行数。 请注意,这些结果可能在不同的减速器上。
  • 输出编写器:减速器的输出写在文件系统(或 HDFS)上。 在默认的 Hadoop 配置中,每个归约器都会写入一个文件(part-r-00000是第一个归约器的输出,part-r-00001是第二个归约器的输出,依此类推)。 要在文件上显示完整的结果列表,应将所有结果连接起来。

在视觉上,可以简单地传达和理解此操作,如下所示:

映射步骤之后,每个映射器实例还可以运行一个可选步骤,即合并器。 基本上可以预期,如果可能的话,在映射器上执行减少步骤,通常用于减少要重新整理的信息量,从而加快了处理速度。 在前面的示例中,如果一个映射程序处理输入文件的多行,则在(可选)组合器步骤中,它可以预聚合结果,并输出较少数量的键值对。 例如,如果映射器在每个块中处理 100 行文本,那么当可以将信息汇总为三时,为什么输出 300 个键值对(字符数为 100,单词为 100,行为 100)? 这实际上是合并器的目标。

在 Hadoop 提供的 MapReduce 实现中,随机分配操作被分配,从而优化了通信成本,并且每个节点可以运行多个映射器和归约器,从而充分利用了节点上可用的硬件资源。 此外,Hadoop 基础架构还提供了冗余和容错功能,因为同一任务可以分配给多个工作器。

Apache Spark 介绍

Apache Spark 是 Hadoop 的演进,并在最近几年变得非常流行。 与 Hadoop 及其以 Java 和批量为重点的设计相比,Spark 能够以快速简便的方式生成迭代算法。 此外,它具有用于多种编程语言的非常丰富的 API 套件,并且本身支持许多不同类型的数据处理(机器学习,流传输,图形分析,SQL 等)。

Apache Spark 是一个集群框架,旨在用于大数据的快速通用处理。 速度的提高之一来自以下事实:与 Hadoop,MapReduce 和 HDFS 一样,每项工作之后的数据都保留在内存中,而不是存储在文件系统中(除非您愿意这样做)。 由于内存提供的延迟和带宽比物理磁盘更具表现,因此这种事情使迭代作业(例如群集 K 均值算法)越来越快。 因此,运行 Spark 的集群需要为每个节点配备大量 RAM。

尽管 Spark 是在 Scala(类似于 Java 一样在 JVM 上运行)中开发的,但它具有适用于多种编程语言的 API,包括 Java,Scala,Python 和 R。在本书中,我们将重点介绍 Python。

Spark 可以两种不同的方式运行:

  • 独立模式:它在本地计算机上运行。 在这种情况下,最大并行化是本地计算机的内核数,并且可用内存量与本地计算机完全相同。
  • 群集模式:它使用群集管理器(例如 YARN)在多个节点的群集上运行。 在这种情况下,最大并行化是组成集群的所有节点上的核心数,而内存量是每个节点的内存量之和。

PySpark

为了使用 Spark 功能(或包含 Spark 的 Python API 的 PySpark),我们需要实例化一个名为SparkContext的特殊对象。 它告诉 Spark 如何访问集群,并包含一些特定于应用的参数。 在虚拟机提供的 Jupyter 笔记本中,此变量已可用,并称为sc(启动 IPython 笔记本时的默认选项)。 让我们在下一部分中查看它包含的内容。

从 PySpark 开始

Spark 使用的数据模型名为弹性分布式数据集RDD),它是可以并行处理的元素的分布式集合。 可以从现有集合(例如 Python 列表)或从外部数据集创建 RDD,这些数据以文件形式存储在本地计算机,HDFS 或其他来源上。

设置本地 Spark 实例

从头开始完整安装 Apache Spark 并非易事。 通常,这是在通常可以在云上访问的计算机群集上完成的,并且将其委派给技术专家(即数据工程师)。 这可能是一个限制,因为您可能无法访问测试本章将学到的内容的环境。

但是,为了测试本章的内容,实际上您不需要进行太复杂的安装。 通过使用 Docker,您可以在自己计算机上的 Linux 服务器上访问 Spark 的安装以及 Jupyter 笔记本和 PySpark。 不论是 Linux,macOS 还是基于 Windows 的计算机都没有关系。

实际上,这主要是因为 Docker。 Docker 允许操作系统级别的虚拟化,也称为容器化。 容器化意味着允许一台计算机运行多个隔离的文件系统实例,其中每个实例彼此之间只是简单地分开(尽管共享相同的硬件资源),就好像它们是单台计算机本身一样。 基本上,在 Docker 中运行的任何软件都包装在一个完整,稳定且预先定义的文件系统中,该文件系统完全独立于运行 Docker 的文件系统。 使用 Docker 容器意味着您的代码将按预期(如本章所述)完美运行。 命令执行的一致性是 Docker 是将解决方案投入生产的最佳方法的主要原因:您只需要将使用的容器移到服务器中,并创建一个 API 即可访问您的解决方案(我们之前在这个页面,“可视化,见解和结果”,我们在其中介绍了 Bottle 包装。

这是您需要采取的步骤:

  1. 首先,首先安装适合您系统的 Docker 软件。 您可以在这里找到所需的全部信息,具体取决于您所使用的操作系统:

| Windows | | Linux | | MacOS |

安装非常简单,但是在从中下载软件的同一页面上,您可以找到可能需要的任何其他信息。

  1. 完成安装后,我们可以使用可在这个页面找到的 Docker 映像。 它包含完整的 Spark 安装,可通过 Jupyter 笔记本访问,以及 Miniconda 安装以及最新版本的 Python 2 和 3。您可以在此处找到有关映像内容的更多信息
  2. 此时,只需打开 Docker 接口即可; 在那里,将出现一个带有鲸鱼的 ASCII 文字和 IP 地址的外壳。 只要记下 IP 地址(在我们的例子中是192.168.99.100)。 现在,在 shell 中运行以下命令:
$> docker run -d -p 8888:8888 --name spark jupyter/pyspark-notebook start-notebook.sh –NotebookApp.token=''
  1. 如果您更喜欢安全性而不是易用性,只需键入以下内容:
$> docker run -d -p 8888:8888 --name spark jupyter/pyspark-notebook start-notebook.sh –NotebookApp.token='mypassword'

用您选择的密码替换mypassword占位符。 请注意,Jupyter 笔记本电脑在启动时会要求输入该密码。

  1. 运行上述命令后,Docker 将开始下载pyspark-notebook image(可能需要一段时间); 为它分配名称spark,将 Docker 映像上的8888端口复制到您计算机上的8888端口,然后执行start-notebook.sh脚本,并将笔记本密码设置为空(这样您就可以仅通过前面提到的 IP 地址和8888端口立即访问 Jupyter)。

此时,您需要做的唯一另一件事就是在浏览器中输入以下内容:

http://192.168.99.100:8888/

也就是说,在您的浏览器中输入 Docker 启动时给您的 IP 地址,冒号,然后是8888(即端口号)。 Jupyter 应该立即出现。

  1. 作为一个简单的测试,您可以立即打开一个新的笔记本并进行以下测试:
In: import pyspark
 sc = pyspark.SparkContext('local[*]')

 # do something to prove it works
 rdd = sc.parallelize(range(1000))
 rdd.takeSample(False, 5)
  1. 同样重要的是要注意,您有停止 Docker 机器的命令以及什至会破坏它的命令。 这个 shell 命令将停止它:
$> docker stop spark

为了在停止容器后销毁该容器,请使用以下命令(顺便说一下,您将丢失容器中的所有工作):

$> docker rm spark

如果您的容器尚未被销毁,则要使其在停止后再次运行,只需使用以下 shell 命令:

$> docker start spark

此外,您需要知道,在 Docker 机器上,您在/home/jovyan目录上进行操作,并且可以直接从 Docker shell 获取其内容列表:

$> docker exec -t -i spark ls /home/jovyan

您还可以执行任何其他 Linux bash 命令。

值得注意的是,您还可以在容器之间来回复制数据(否则,您的工作将只保存在机器的操作系统中)。 假设您必须将 Windows 桌面目录中的文件(file.txt)复制到 Docker 计算机:

$> docker cp c:/Users/Luca/Desktop/spark_stuff/file.txt spark:/home/jovyan/file.txt

同样,相反的情况也是可能的:

$> docker cp spark:/home/jovyan/test.ipynb c:/Users/Luca/Desktop/spark_stuff/test.ipynb

这就是全部。 仅需几个步骤,您就可以在本地运行的 Spark 环境中运行所有实验(显然,它仅使用一个节点,并且仅限于单个 CPU 的功能)。

试验弹性分布式数据集

现在,让我们创建一个包含 0 到 9 之间的整数的弹性分布式数据集。为此,我们可以使用SparkContext对象提供的parallelize方法:

In: numbers = range(10)
 numbers_rdd = sc.parallelize(numbers)
 numbers_rdd

Out: PythonRDD[2672] at RDD at PythonRDD.scala:49

如您所见,您不能简单地打印 RDD 内容,因为它会分为多个分区(并分布在群集中)。 分区的默认数量是 CPU 数量的两倍(因此,在提供的 VM 中为四个),但是可以使用parallelize方法的第二个参数手动设置。

要打印出 RDD 中包含的数据,应调用collect方法。 请注意,此操作在群集上运行时会收集节点上的所有数据。 因此,该节点需要有足够的内存来包含所有内容:

In: numbers_rdd.collect()

Out: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

要仅获得部分预览,请使用take方法,指示您希望看到多少个元素。 请注意,由于它是分布式数据集,因此不能保证元素的插入顺序与插入时的顺序相同:

In: numbers_rdd.take(3)

Out: [0, 1, 2]

要读取文本文件,我们可以使用SparkContext提供的textFile方法。 它允许读取 HDFS 文件和本地文件,并在换行符上拆分文本; 因此,RDD 的第一个元素是文本文件的第一行(使用第一种方法)。 请注意,如果您使用的是本地路径,则组成集群的所有节点都应通过相同的路径访问相同的文件。 为此,我们首先下载威廉·莎士比亚的完整剧集:

In: import urllib.request
 url = "http://www.gutenberg.org/files/100/100-0.txt"
 urllib.request.urlretrieve(url, "shakespeare_all.txt")

In: sc.textFile("file:////home//jovyan//shakespeare_all.txt").take(6)

Out: ['',
'Project Gutenberg\'s The Complete Works of William Shakespeare, by William',
'Shakespeare', '',
'This eBook is for the use of anyone anywhere in the United States and',
'most other parts of the world at no cost and with almost no restrictions']

要将 RDD 的内容保存到磁盘上,可以使用 RDD 提供的saveAsTextFile方法:

In: numbers_rdd.saveAsTextFile("file:////home//jovyan//numbers_1_10.txt")

RDD 仅支持两种类型的操作:

  • 转换,将数据集转换为另一数据集。 转换的输入和输出都是 RDD。 因此,可以将多个转换链接在一起,从而达到一种编程的函数式风格。 而且,转换是懒惰的。 也就是说,他们不会立即计算出结果。
  • 动作从 RDD 返回值,例如元素的总和和计数,或者仅收集所有元素。 动作是执行(惰性)转换链的触发器,因为需要输出。

典型的 Spark 程序是一系列转换,最后是一个动作。 默认情况下,RDD 上的所有转换都是在您每次执行操作时执行的(即,不保存每个转换器之后的中间状态)。 但是,只要您想cache转换后的元素的值,就可以使用persist方法(在 RDD 上)覆盖此行为。 persist方法允许内存和磁盘持久性。

在下面的示例中,我们将对 RDD 中包含的所有值求平方,然后对其求和; 该算法可以通过一个映射器(正方形元素),然后是一个约简器(对数组求和)来执行。 根据 Spark 的说法,map方法是一种转换器,因为它只是逐个元素地转换数据。 归约是一个动作,因为它可以从所有元素中共同创造出值。

让我们逐步解决此问题,以了解可以进行操作的多种方式。 首先,我们将使用一个函数来转换(映射)所有数据:首先定义一个函数,该函数返回输入参数的平方,然后将该函数传递给 RDD 中的map方法, 最后,我们收集 RDD 中的元素:

In: def sq(x):
 return x**2 numbers_rdd.map(sq).collect()

Out: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

尽管输出正确,但是sq函数占用大量空间; 借助 Python 的lambda表达式,我们可以更简洁地重写转换:

In: numbers_rdd.map(lambda x: x**2).collect()

Out: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

您还记得为什么我们需要调用collect来打印转换后的 RDD 中的值吗? 这是因为map方法不会付诸实践,而只会被懒惰地评估。 另一方面,reduce方法是一个动作; 因此,将reduce步骤添加到先前的 RDD 应该会输出一个值。 至于mapreduce将应具有两个自变量(左值和右值)的函数作为自变量,并应返回一个值。 在这种情况下,它可以是用def定义的verbose函数或lambda函数:

In: numbers_rdd.map(lambda x: x**2).reduce(lambda a,b: a+b)

Out: 285

为了使其更简单,我们可以使用sum动作来代替归约器:

In: numbers_rdd.map(lambda x: x**2).sum()

Out: 285

现在让我们前进一个步骤,并介绍键值对。 尽管 RDD 可以包含任何类型的对象(到目前为止,我们已经看到了整数和文本行),但是当元素是由两个元素组成的元组时,可以进行一些操作:键和值。

举个例子,让我们将 RDD 中的数字分组为偶数和偶数,然后分别计算两组的总和。 对于 MapReduce 模型,最好将每个数字与一个键(奇数或偶数)映射,然后对于每个键,使用求和运算来减少。

我们可以从map操作开始:首先创建一个标记数字的函数,如果参数编号为偶数,则输出even,否则输出odd。 然后,我们将创建一个键值映射,为每个数字创建一个键值对,其中键是标记,值是数字本身:

In: def tag(x):
 return "even" if x%2==0 else "odd"

 numbers_rdd.map(lambda x: (tag(x), x)).collect()

Out: [('even', 0),
      ('odd', 1),
      ('even', 2),
      ('odd', 3),
      ('even', 4),
      ('odd', 5),
      ('even', 6),
      ('odd', 7),
      ('even', 8),
      ('odd', 9)]

为了分别减少每个键,我们现在可以使用reduceByKey方法(这不是 Spark 动作)。 作为参数,我们应该传递需要应用于每个键的所有值的函数。 在这种情况下,我们将总结所有这些。 最后,我们应该调用collect方法来打印结果:

In: numbers_rdd.map(lambda x: (tag(x), x) ) \
 .reduceByKey(lambda a,b: a+b).collect()

Out: [('even', 20), ('odd', 25)]

现在,让我们列出 Spark 中可用的一些最重要的方法。 它不是详尽的指南,仅包括最常用的指南。

我们从转变开始; 它们可以应用于 RDD,并产生 RDD:

  • map(function):这将返回通过将每个元素 传递给函数而形成的 RDD。
  • flatMap(function):这将返回一个 RDD,该 RDD 是通过将 的输出对输入 RDD 的每个元素进行平坦化而形成的。 当 输入上的每个值都可以映射到 0 个或更多输出元素时使用。 例如,要计算每个单词在文本中出现的次数, 我们应该将每个单词映射到一个键-值对(该单词将是键,而 1 是值) ,以这种方式为 文本的每个输入行生成多个键值元素。
  • filter(function):这将返回由所有值组成的数据集,其中 函数返回true
  • sample(withReplacement, fraction, seed):这将引导 RDD, 允许您创建采样的 RDD(有或没有替换),其 长度是输入长度的一小部分。
  • distinct():这将返回包含输入 RDD 的不同元素的 RDD。
  • coalesce(numPartitions):这减少了 RDD 中的分区数。
  • repartition(numPartitions):更改 RDD 中的分区数。 这种方法总是通过网络混洗所有数据。
  • groupByKey():这将创建一个 RDD,其中对于每个键,其值是在输入数据集中具有该键的 值序列。
  • reduceByKey(function):这将通过键汇总输入的 RDD,然后, 将reduce函数应用于每个组的值。
  • sortByKey(ascending):按 升序或降序对 RDD 中的元素进行排序。
  • union(otherRDD):这会将两个 RDD 合并在一起。
  • intersection(otherRDD):这将返回一个仅由同时出现在输入和参数 RDD 中的 值组成的 RDD。
  • join(otherRDD):这将返回一个数据集,在该数据集中,将键值输入 (在键上)连接到参数 RDD。

与 SQL 中的join函数类似,这些方法也可用:cartesianleftOuterJoinrightOuterJoinfullOuterJoin

现在,让我们概述一下 PySpark 中最受欢迎的动作是什么。 请注意,动作通过链中的所有转换器触发 RDD 的处理:

  • reduce(function):聚合 RDD 的元素并产生 输出值
  • count():这将返回 RDD 中元素的计数
  • countByKey():这将返回一个 Python 字典,其中每个键都是 ,与带有该键的 RDD 中的元素数相关联
  • collect():这将本地返回转换后的 RDD 中的所有元素
  • first():这将返回 RDD 的第一个值
  • take(N):这将返回 RDD 中的前 N 个值
  • takeSample(withReplacement, N, seed):这将返回 RDD 中包含或不具有替换的 N 个元素的引导程序,最终使用提供的随机种子作为参数
  • takeOrdered(N, ordering):按值排序(升序或降序)后,它返回 RDD 中的前 N 个元素
  • saveAsTextFile(path):这会将 RDD 作为一组文本文件保存在 指定的目录中

还有一些既不是转换器也不是动作的方法:

  • cache():这将缓存 RDD 的元素; 因此,将来基于相同 RDD 的 计算可以将其重新用作起点
  • persist(storage):与cache相同,但是您可以指定 在何处存储 RDD 的元素(内存,磁盘或两者)。
  • unpersist():撤消persistcache操作

现在,让我们尝试使用 RDD 的示例,以计算一些文本统计信息并从大文本中提取最受欢迎的单词(莎士比亚的戏剧)。 使用 Spark 时,用于计算文本统计信息的算法应如下所示:

  1. 在 RDD 上读取输入文件并对其进行并行化。 可以使用SparkContext提供的textFile方法完成此操作。

  2. 对于输入文件的每一行,将返回三个键值对:一对包含字符数,一个包含单词数,最后一个包含行数。 在 Spark 中,这是flatMap操作,因为每条输入线都会生成三个输出。

  3. 对于每个键,我们总结所有值。 这可以通过reduceByKey方法完成。

  4. 最后,收集结果。 在这种情况下,我们可以使用collectAsMap方法,该方法在 RDD 中收集键值对并返回 Python 字典。 注意这是一个动作; 因此,将执行 RDD 链并返回结果:

In: def emit_feats(line):
 return [("chars", len(line)), \
 ("words", len(line.split())), \
 ("lines", 1)] print((sc.textFile("file:////home//jovyan//shakespeare_all.txt")
 .flatMap(emit_feats)
 .reduceByKey(lambda a,b: a+b)
 .collectAsMap()))

Out: {'chars': 5535014, 'words': 959893, 'lines': 149689}

要确定文本中最受欢迎的单词,请按照下列步骤操作:

  1. 使用textFile方法在 RDD 上读取并并行化输入文件。
  2. 对于每一行,提取所有单词。 对于此操作,我们可以使用flatMap方法和正则表达式。
  3. 现在,文本中的每个单词(即 RDD 的每个元素)都映射到一个键值对:键是小写单词,值始终是1。 这是一个映射操作。
  4. 通过reduceByKey调用,我们计算每个单词(键)在文本(RDD)中出现的次数。 输出是键值对,其中键是一个单词,值是单词在文本中出现的次数。
  5. 我们翻转键和值并创建一个新的 RDD。 这是一个映射操作。
  6. 我们将 RDD 降序排列,然后提取(获取)第一个元素。 这是一项操作,可以通过takeOrdered方法执行一次操作。

我们实际上可以进一步改进该解决方案,将第二步和第三步合并在一起(flatMap-为每个单词分配一个键-值对,其中键是小写单词,值是出现的次数),以及第五步和第六步(将 RDD 中的第一个元素按值排序,即该对中的第二个元素):

In: import re
 WORD_RE = re.compile(r"[\w']+")
 print((sc.textFile("file:////home//jovyan//shakespeare_all.txt")
 .flatMap(lambda line: [(word.lower(), 1) for word in 
 WORD_RE.findall(line)])
 .reduceByKey(lambda a,b: a+b)
 .takeOrdered(1, key = lambda x: -x[1])))

Out: [('the', 29998)]

跨集群节点共享变量

当我们在分布式环境中工作时,有时需要跨节点共享信息,以便所有节点都可以使用一致的变量进行操作。 Spark 通过提供两种变量来处理这种情况:只读和只写变量。 通过不再确保共享变量是可读写的,它也降低了一致性要求,从而使管理这种情况的辛苦工作落在了开发人员的肩膀上。 通常,由于 Spark 确实具有灵活性和自适应性,因此可以很快找到解决方案。

只读广播变量

广播变量是驱动程序节点共享的变量; 也就是说,该节点在我们的配置中运行 IPython 笔记本,并且所有节点都在集群中。 这是一个只读变量,因为该变量由一个节点广播,并且如果另一个节点对其进行更改,则永远不会回读。

现在,让我们在一个简单的示例中查看其工作方式:我们希望对仅包含性别信息作为字符串的数据集进行一次热编码。 虚拟数据集仅包含一个特征,该特征可以是男性M,女性F或未知U(如果信息丢失)。 具体来说,我们希望所有节点都使用定义的“单热编码”,如以下词典中列出:

In: one_hot_encoding = {"M": (1, 0, 0), "F": (0, 1, 0),
 "U": (0, 0, 1)}

在我们的解决方案中,我们首先在映射函数内部广播 Python 字典(调用SparkContext提供的sc.broadcast方法); 使用其value属性,我们现在可以访问它。 完成此操作后,我们有了一个通用的map函数,该函数可以在任何一个热图字典上运行:

In: bcast_map = sc.broadcast(one_hot_encoding)
 def bcast_map_ohe(x, shared_ohe):
 return shared_ohe[x]

 (sc.parallelize(["M", "F", "U", "F", "M", "U"])
 .map(lambda x: bcast_map_ohe(x, bcast_map.value))
 .collect())

广播变量保存在组成集群的所有节点的内存中; 因此,它们永远不会共享大量数据,这些数据会填满它们并使后续处理变得不可能。

要删除广播变量,请在广播变量上使用unpersist方法。 此操作将释放所有节点上该变量的内存:

In: bcast_map.unpersist()

只写累加器变量

Spark 集群中可以共享的其他变量是累加器。 累加器是只写变量,可以将它们加在一起,通常用于实现总和或计数器。 只有运行 IPython 笔记本的驱动程序节点可以读取其值; 其他所有节点都无法读取。 让我们通过一个示例看一下它是如何工作的:我们要处理一个文本文件,并了解在处理文本文件时有多少行是空的。 当然,我们可以通过两次扫描数据集(使用两个 Spark 作业)来做到这一点,第一个计数空行,第二个进行实际处理,但是这种解决方案不是很有效。 之后,您将执行处理文本文件并计算其行数所需的所有步骤。

所需步骤如下:

  1. 首先,我们从网上下载要处理的文本文件,由古腾堡计划提供的亚瑟·柯南·道尔爵士的《福尔摩斯历险记》:
In: import urllib.request
 url = "http://gutenberg.pglaf.org/1/6/6/1661/1661.txt"
 urllib.request.urlretrieve(url, "sherlock.tx")
  1. 然后,我们实例化一个累加器变量(初始值为0),并在处理输入文件的每一行(带有映射)时为发现的每一个空行添加1。 同时,我们可以在每一行上进行一些处理; 例如,在下面的代码中,我们只为每一行返回1,就以此方式对文件中的所有行进行计数。

  2. 在处理的最后,我们将获得两条信息:第一条是行数,来自对变换后的 RDD 的count()操作的结果,第二条是累加器的属性中包含的空行数。 请记住,在扫描数据集一次之后,这两个选项均可用:

In: accum = sc.accumulator(0)
 def split_line(line):
 if len(line) == 0:
 accum.add(1)
 return 1

 filename = 'file:////home//jovyan//sherlock.txt'
 tot_lines = (
 sc.textFile(filename)
 .map(split_line)
 .count())

 empty_lines = accum.value
 print("In the file there are %d lines" % tot_lines)
 print("And %d lines are empty" % empty_lines)

Out: In the file there are 13053 lines
 And 2666 lines are empty

广播变量和累加器变量一起——示例

尽管广播和累加器变量很简单,并且变量非常有限(一个变量是只读变量,另一个变量是只写变量),但是可以将它们有效地用于创建非常复杂的操作。 例如,让我们尝试在分布式环境中的iris数据集上应用不同的机器学习算法。 我们将通过以下方式构建 Spark 作业:

  • 读取数据集并将其广播到所有节点(因为它足够小以适合内存)。
  • 每个节点将在数据集上使用不同的分类器,并在整个数据集上返回分类器名称及其准确率得分。 请注意,在此简单示例中,为了使事情变得简单,我们将不进行任何预处理,训练/测试拆分或超参数优化。
  • 如果分类器引发异常,则错误的字符串表示形式以及分类器名称应存储在累加器中。
  • 最终输出应包含执行分类任务且没有错误的分类器列表及其准确率得分。

第一步,我们加载iris数据集并将其广播到集群中的所有节点:

In: from sklearn.datasets import load_iris
 bcast_dataset = sc.broadcast(load_iris())

现在,让我们继续通过创建自定义累加器进行编码。 它将包含一个元组列表,用于存储分类器名称和它作为字符串遇到的异常。 定制累加器是使用AccumulatorParam类派生的,并且应至少包含两个方法:zero(在初始化时调用)和addInPlace(在累加器上调用add方法时调用)。

以下代码显示了执行此操作的最简单方法,然后将其初始化为空列表。 请记住,加法运算有点棘手:我们需要合并两个元素(一个元组和一个列表),但是我们不知道哪个元素是列表,哪个是元组; 因此,我们首先确保两个元素都是列表,然后我们可以通过简单的方式(使用加号运算符)将它们连接起来:

In: from pyspark import AccumulatorParam
 class ErrorAccumulator(AccumulatorParam):
 def zero(self, initialList):
 return initialList
 def addInPlace(self, v1, v2):
 if not isinstance(v1, list):
 v1 = [v1]
 if not isinstance(v2, list):
 v2 = [v2]
 return v1 + v2

 errAccum = sc.accumulator([], ErrorAccumulator())

现在,让我们定义映射函数:每个节点都应在广播iris数据集上训练,测试和评估分类器。 作为参数,该函数将接收分类器对象,并应返回一个元组,该元组包含分类器名称及其准确率分数(包含在列表中)。

如果这样做引发异常,则将分类器名称和以字符串引用的异常添加到累加器中,并返回一个空列表:

In: def apply_classifier(clf, dataset):
 clf_name = clf.__class__.name
        X = dataset.value.data
 y = dataset.value.target
 try:
 from sklearn.metrics import accuracy_score
 clf.fit(X, y)
 y_pred = clf.predict(X)
 acc = accuracy_score(y, y_pred)
 return [(clf_name, acc)]
 except Exception as e:
 errAccum.add((clf_name, str(e)))
 return []

最后,我们到达了工作的核心。 现在,我们从 Scikit-learn 实例化一些对象(为了测试累加器,其中一些不是分类器)。 我们将它们转换为 RDD,并应用在上一个单元格中创建的map函数。 由于返回值是一个列表,因此我们可以使用flatMap来仅收集未陷入异常的映射器的输出:

In: from sklearn.linear_model import SGDClassifier
 from sklearn.dummy import DummyClassifier
 from sklearn.decomposition import PCA
 from sklearn.manifold import MDS

 classifiers = [DummyClassifier('most_frequent'),
 SGDClassifier(),
 PCA(),
 MDS()]

 (sc.parallelize(classifiers)
 .flatMap(lambda x: apply_classifier(x, bcast_dataset))
 .collect())

Out: [('DummyClassifier', 0.33333333333333331), 
 ('SGDClassifier', 0.85333333333333339)]

如预期的那样,输出中仅包含真实分类器。 让我们看看哪些分类器产生了错误。 毫不奇怪,这里我们从前面的输出中发现了两个缺失的:

In: print("The errors are:", errAccum.value)

Out: The errors are: [('PCA', "'PCA' object has no attribute 'predict'"), 
 ('MDS', "'MDS' object has no attribute 'predict'")]

最后,让我们清理广播数据集:

In: bcast_dataset.unpersist()

请记住,在此示例中,我们使用了一个可以广播的小型数据集。 在实际的大数据问题中,您需要从 HDFS 加载数据集并广播 HDFS 路径。

Spark 中的数据预处理

到目前为止,我们已经看到了如何从本地文件系统和 HDFS 加载文本数据。 文本文件可以包含非结构化数据(例如文本文档)或结构化数据(例如 CSV 文件)。 至于半结构化数据,就像包含 JSON 对象的文件一样,Spark 具有特殊的例程,可以将文件转换为DataFrame,类似于 R 和 Python 包 Pandas 中的DataFrameDataFrame与 RDBMS 表非常相似,其中设置了模式。

CSV 文件和 Spark DataFrame

我们首先向您展示如何读取 CSV 文件并将其转换为 Spark DataFrame。 只需按照以下示例中的步骤操作:

  1. 为了导入兼容 CSV 的文件,我们首先需要通过从本地SparkContext创建一个SQLContext对象来创建一个 SQL 上下文:
In: from pyspark.sql import SQLContext
 sqlContext = SQLContext(sc)
  1. 对于我们的示例,我们创建了一个简单的 CSV 文件,该文件是一个具有六行三列的表,其中缺少某些属性(例如user_id=0用户的性别属性):
In: data = """balance,gender,user_id
 10.0,,0
 1.0,M,1
 -0.5,F,2
 0.0,F,3
 5.0,,4
 3.0,M,5
 """
 with open("users.csv", "w") as output:
 output.write(data)
  1. 使用sqlContext提供的read.format方法,我们已经使表格式正确,并且在变量中具有所有正确的列名称。 输出变量类型是 Spark DataFrame。 要在一个漂亮的格式化表中显示该变量,请使用其show方法:
In: df = sqlContext.read.format('com.databricks.spark.csv')\
 .options(header='true', inferschema='true').load('users.csv')
 df.show()

Out: +-------+------+-------+ 
     |balance|gender|user_id| 
     +-------+------+-------+ 
     |   10.0|  null|      0| 
     |    1.0|     M|      1| 
     |   -0.5|     F|      2| 
     |    0.0|     F|      3| 
     |    5.0|  null|      4| 
     |    3.0|     M|      5| 
     +-------+------+-------+
  1. 此外,我们可以使用printSchema方法研究DataFrame的架构。 我们认识到,在读取 CSV 文件时,数据会推断出每种列的类型(在前面的示例中,user_id列包含长整数,gender列由字符串组成,并且余额是双浮点数) :
In: df.printSchema()

Out: root
 |-- balance: double (nullable = true) |-- gender: string (nullable = true) |-- user_id: long (nullable = true)
  1. 就像 RDBMS 中的表格一样,我们可以对DataFrame中的数据进行切片和切块,选择列,并按属性过滤数据。 在此示例中,我们要打印不丢失gender且余额严格大于0的用户的balancegenderuser_id。 为此,我们可以使用filterselect方法:
In: (df.filter(df['gender'] != 'null')
 .filter(df['balance'] > 0)
 .select(['balance', 'gender', 'user_id'])
 .show())

Out: +-------+------+-------+
     |balance|gender|user_id|
     +-------+------+-------+
     |    1.0|     M|      1|
     |    3.0|     M|      5|
     +-------+------+-------+
  1. 我们还可以使用类似 SQL 的语言重写前面的作业中的每一项。 实际上,filterselect方法可以接受 SQL 格式的字符串:
In: (df.filter('gender is not null')
 .filter('balance > 0').select("*").show())
  1. 我们还可以仅使用一次对filter方法的调用:
In: df.filter('gender is not null and balance > 0').show()

处理丢失的数据

数据预处理的一个常见问题是如何处理丢失的数据。 SparkDataFrame与 Pandas DataFrame类似,提供了您可以对它们执行的多种操作。 例如,获得仅包含完整行的数据集的最简单选择是丢​​弃包含缺失信息的行。 为此,在 SparkDataFrame中,我们首先必须访问DataFramena属性,然后调用drop方法。 结果表将仅包含完整的行:

In: df.na.drop().show() 
Out: +-------+------+-------+
     |balance|gender|user_id|
     +-------+------+-------+
     |    1.0|     M|      1|
     |   -0.5|     F|      2|
     |    0.0|     F|      3|
     |    3.0|     M|      5|
     +-------+------+-------+

如果这样的操作删除了太多的行,我们总是可以决定应该考虑哪些列来删除该行(作为drop方法的扩展子集):

In: df.na.drop(subset=["gender"]).show()

另外,如果要为每列设置默认值而不是删除行数据,则可以使用fill方法,传递由列名组成的字典(作为字典键)和默认值(作为字典中键的值)以替换该列中丢失的数据。

例如,如果要确保将变量平衡(缺少的地方)设置为0并将变量性别(缺少的地方)设置为U,则只需执行以下操作:

In: df.na.fill({'gender': "U", 'balance': 0.0}).show()

Out: +-------+------+-------+
     |balance|gender|user_id|
     +-------+------+-------+
     |   10.0|     U|      0|
     |    1.0|     M|      1|
     |   -0.5|     F|      2|
     |    0.0|     F|      3|
     |    5.0|     U|      4|
     |    3.0|     M|      5|
     +-------+------+-------+

在内存中分组和创建表

要将函数应用于一组行(与 SQL GROUP BY完全相同),可以使用两种类似的方法。 在以下示例中,我们要计算每个性别的平均余额:

In:(df.na.fill({'gender': "U", 'balance': 0.0})
 .groupBy("gender").avg('balance').show())

Out: +------+------------+ 
     |gender|avg(balance)| 
     +------+------------+ 
     |     F|       -0.25| 
     |     M|         2.0| 
     |     U|         7.5|
     +------+------------+

到目前为止,我们已经使用了DataFrame,但是,如您所见,DataFrame方法和 SQL 命令之间的距离很小。 实际上,使用 Spark 可以将DataFrame注册为 SQL 表,以充分享受 SQL 的强大功能。 该表将保存在内存中,并以类似于 RDD 的方式进行分发。 要注册该表,我们需要提供一个名称,该名称将在以后的 SQL 命令中使用。 在这种情况下,我们决定将其命名为users

In: df.registerTempTable("users")

通过调用 Spark SQL 上下文提供的 SQL 方法,我们可以运行任何与 SQL 兼容的表:

In: sqlContext.sql("""
 SELECT gender, AVG(balance)
 FROM users
 WHERE gender IS NOT NULL
 GROUP BY gender""").show()

Out: +------+------------+
     |gender|avg(balance)|
     +------+------------+
     |     F|       -0.25|
     |     M|         2.0|
     +------+------------+

毫不奇怪,命令输出的表(以及users表本身)是 Spark DataFrame类型的:

In: type(sqlContext.table("users")) 
Out: pyspark.sql.dataframe.DataFrame

DataFrame,表和 RDD 紧密相连,并且可以在DataFrame上使用 RDD 方法。 请记住,DataFrame的每一行都是 RDD 的元素。 让我们详细了解一下,首先收集整个表:

In: sqlContext.table("users").collect()

Out: [Row(balance=10.0, gender=None, user_id=0), 
      Row(balance=1.0, gender='M', user_id=1), 
      Row(balance=-0.5, gender='F', user_id=2), 
      Row(balance=0.0, gender='F', user_id=3), 
      Row(balance=5.0, gender=None, user_id=4), 
      Row(balance=3.0, gender='M', user_id=5)]

In: a_row = sqlContext.sql("SELECT * FROM users").first()
 print(a_row)

Out: Row(balance=10.0, gender=None, user_id=0)

输出是Row对象的列表(它们看起来像 Python 的namedtuple)。 让我们深入研究。 Row包含多个属性,可以将它们作为属性或字典键来访问; 也就是说,要从第一行获取balance,我们可以选择以下两种方式:

In: print(a_row['balance'])
 print(a_row.balance)

Out: 10.0
 10.0

另外,可以使用RowasDict方法将Row收集为 Python 字典。 结果包含属性名称作为键和属性值(作为字典值):

In: a_row.asDict() 
Out: {'balance': 10.0, 'gender': None, 'user_id': 0}

将预处理的DataFrame或 RDD 写入磁盘

要将DataFrame或 RDD 写入磁盘,我们可以使用write方法。 我们可以选择多种格式; 在这种情况下,我们会将其另存为 CSV 文件在本地计算机上:

In: (df.na.drop().write
 .save("file:////home//jovyan//complete_users.csv", format='csv'))

检查本地文件系统上的输出后,我们立即发现与我们期望的有所不同:此操作将创建多个文件(part-r-…)。 它们每个都包含一些序列化为 JSON 对象的行,将它们合并在一起将创建全面的输出。 随着 Spark 可以处理大型分布式文件,为此调整了write操作,并且每个节点都写入了完整 RDD 的一部分:

In: !ls -als ./complete_users.json 
Out: total 20
     4 drwxr-sr-x  2 jovyan users 4096 Jul 21 19:48 .
     4 drwsrwsr-x 20 jovyan users 4096 Jul 21 19:48 ..
     4 -rw-r--r--  1 jovyan users   33 Jul 21 19:48 
     part-00000-bc9077c5-67de-46b2-9ab7-c1da67ffcadd-c000.csv
     4 -rw-r--r--  1 jovyan users   12 Jul 21 19:48 
     .part-00000-bc9077c5-67de46b2-9ab7-c1da67ffcadd-c000.csv.crc
     0 -rw-r--r--  1 jovyan users    0 Jul 21 19:48 _SUCCESS
     4 -rw-r--r--  1 jovyan users    8 Jul 21 19:48 ._SUCCESS.crc

为了读回它,我们不必创建一个独立的文件-在读取操作中即使多个都可以。 还可以在 SQL 查询的FROM子句中读取 CSV 文件。 现在让我们尝试在不创建中间DataFrame的情况下打印刚刚写入磁盘的 CSV:

In: sqlContext.sql("""SELECT * FROM 
 csv.`file:////home//jovyan//complete_users.csv`""").show()

Out: +----+---+---+
     | _c0|_c1|_c2|
     +----+---+---+
     | 1.0|  M|  1|
     |-0.5|  F|  2|
     | 0.0|  F|  3|
     | 3.0|  M|  5|
     +----+---+---+

除 JSON 外,还有另一种在处理结构化大型数据集时非常流行的格式:Parquet 格式。 Parquet 是 Hadoop 生态系统中可用的列式存储格式。 它压缩和编码数据,并可以使用嵌套结构; 所有这些品质使其变得非常高效。 保存和加载与 CSV 非常相似,即使在这种情况下,此操作也会产生多个文件写入磁盘:

In: (df.na.drop().write
 .save("file:////home//jovyan//complete_users.parquet", 
          format='parquet'))

使用 Spark DataFrame

到目前为止,我们已经描述了如何从 CSV 和 Parquet 文件加载DataFrame,但是没有描述如何从现有 RDD 创建它们。 为此,您只需为 RDD 中的每条记录创建一个Row对象,然后调用 SQL 上下文的createDataFrame方法。 最后,您可以将其注册为temp表,以充分利用 SQL 语法的强大功能:

In: from pyspark.sql import Row
 rdd_gender = \
 sc.parallelize([Row(short_gender="M", long_gender="Male"),
 Row(short_gender="F", long_gender="Female")])
 (sqlContext.createDataFrame(rdd_gender)
 .registerTempTable("gender_maps"))

 sqlContext.table("gender_maps").show()

Out: +-----------+------------+
     |long_gender|short_gender|
     +-----------+------------+
     |       Male|           M|
     |     Female|           F|
     +-----------+------------+

这也是处理 CSV 文件的首选方式。 首先,使用sc.textFile读取文件; 然后,使用split方法,Row构造器和createDataFrame方法创建最终的DataFrame

当您有多个内存中的DataFrame或可以从磁盘加载时,您可以加入并使用经典 RDBMS 中可用的所有操作。 在此示例中,我们可以将从 RDD 中创建的DataFrame与存储在 Parquet 文件中的users数据集结合在一起。 结果令人惊讶:

In: sqlContext.sql("""
 SELECT balance, long_gender, user_id
 FROM parquet.`file:////home//jovyan//complete_users.parquet`
 JOIN gender_maps ON gender=short_gender""").show()

Out: +-------+-----------+-------+
     |balance|long_gender|user_id|
     +-------+-----------+-------+
     |    3.0|       Male|      5|
     |    1.0|       Male|      1|
     |    0.0|     Female|      3|
     |   -0.5|     Female|      2|
     +-------+-----------+-------+

由于表是内存中的,所以最后要做的就是通过释放用于保留它们的内存来进行清理。 通过调用sqlContext提供的tableNames方法,我们可以获得当前内存中所有表的列表。 然后,为了释放它们,我们可以使用dropTempTable作为表名作为参数。 除此之外,对这些表的任何进一步引用都将返回错误:

In: sqlContext.tableNames() 
Out: ['gender_maps', 'users']

In: for table in sqlContext.tableNames():
 sqlContext.dropTempTable(table)

从 Spark 1.3 开始,在执行数据科学操作时,DataFrame已成为对数据集进行操作的首选方式。

Spark 机器学习

在本章的这一点上,我们完成了您的主要任务:创建一个模型来预测数据集中缺少的一个或多个属性。 对于此任务,我们可以使用一些机器学习建模,而 Spark 可以在这种情况下为我们提供帮助。

MLlib 是 Spark 机器学习库; 尽管它是用 Scala 和 Java 构建的,但是它的功能也可以在 Python 中使用。 它包含分类,回归,推荐算法,一些用于降维和特征选择的例程,并且具有许多用于文本处理的功能。 它们所有人都能够应对庞大的数据集,并利用集群中所有节点的力量来实现其目标。

到目前为止,它由两个主要包组成:MLlib(可在 RDD 上运行)和 ML(可在DataFrame上运行)。 由于后者表现良好,并且是表示数据科学中数据的最流行方式,因此开发人员选择贡献和改进 ML 分支,让 ML 分支保留下来,但没有进一步的发展。 MLlib 乍看之下似乎是一个完整的库,但是,在开始使用 Spark 之后,您会注意到默认包中既没有统计库,也没有数值库。 在这里,SciPy 和 NumPy 会为您提供帮助,它们再次对数据科学至关重要。

在本节中,我们将尝试探索pyspark.ml包的功能; 截至目前,与最新的 Scikit-learn 库相比,它仍处于早期阶段,但它肯定在未来具有很大的潜力。

Spark 是一款高级,分布式且复杂的软件,应仅在大数据上以及具有多个节点的集群使用; 实际上,如果数据集可以容纳在内存中,那么使用其他库(例如 Scikit-learn 或类似库)就更方便了,这些库仅关注问题的数据科学方面。 在小型数据集的单个节点上运行 Spark 的速度可能比 Scikit-learn 等价算法慢五倍。

KDD99 数据集上的 Spark

让我们使用真实数据集:KDD99 数据集进行此探索。 竞赛的目的是创建一个网络入侵检测系统,该系统能够识别哪个网络流是恶意的,哪些不是。 而且,数据集中有许多不同的攻击。 目标是使用数据集中包含的数据包流的特征准确预测它们。

作为数据集的补充说明,它在发布后的最初几年中为开发入侵检测系统IDS)的强大解决方案非常有用。 如今,作为其结果,数据集中包含的所有攻击都非常易于检测,因此不再用于 IDS 开发。 功能包括协议(tcpicmpudp),服务(httpsmtp等),数据包的大小,协议中有效的标志,尝试访问的次数。 成为根,等等。

有关 KDD99 挑战和数据集的更多信息,请访问这个页面

尽管这是一个经典的多类分类问题,但我们将对其进行深入研究,以向您展示如何在 Spark 中执行此任务。

读取数据集

首先,让我们下载并解压缩数据集。 我们将非常保守,只使用原始训练数据集的 10%(75 MB,未压缩),因为我们所有的分析都是在小型虚拟机上运行的。 如果想尝试一下,可以取消注释以下代码段中的行,并下载完整的训练数据集(未压缩的 750 MB)。 我们使用 bash 命令下载训练数据集,测试(47 MB​​)和功能名称:

In: !mkdir datasets
 !rm -rf ./datasets/kdd*
 # !wget -q -O datasets/kddtrain.gz \
 # http://kdd.ics.uci.edu/databases/kddcup99/kddcup.data.gz
 !wget -q -O datasets/kddtrain.gz \
 http://kdd.ics.uci.edu/databases/kddcup99/kddcup.data_10_percent.gz
 !wget -q -O datasets/kddtest.gz \
 http://kdd.ics.uci.edu/databases/kddcup99/corrected.gz
 !wget -q -O datasets/kddnames \
 http://kdd.ics.uci.edu/databases/kddcup99/kddcup.names
 !gunzip datasets/kdd*gz

现在,打印前几行以了解格式。 显然,这是不带标题的经典 CSV,每行的末尾都包含一个点。 此外,我们可以看到有些字段是数字字段,但有些字段是文本字段,并且目标变量包含在最后一个字段中:

In: !head -3 datasets/kddtrain

Out:
0,tcp,http,SF,181,5450,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,8,8,0.00,0.00,0.00,0.00,1.00,0.00,0.00,9,9,1.00,0.00,0.11,0.00,0.00,0.00,0.00,0.00,normal. 0,tcp,http,SF,239,486,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,8,8,0.00,0.00,0.00,0.00,1.00,0.00,0.00,19,19,1.00,0.00,0.05,0.00,0.00,0.00,0.00,0.00,normal. 0,tcp,http,SF,235,1337,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,8,8,0.00,0.00,0.00,0.00,1.00,0.00,0.00,29,29,1.00,0.00,0.03,0.00,0.00,0.00,0.00,0.00,normal.

要创建具有命名字段的DataFrame,我们应该首先阅读kddnames文件中包含的标头。 目标字段将简单地命名为target。 读取并解析文件后,我们打印问题的特征数量(请记住目标变量不是特征)及其前十个名称:

In: with open('datasets/kddnames', 'r') as fh:
 header = [line.split(':')[0]
 for line in fh.read().splitlines()][1:]
 header.append('target')

 print("Num features:", len(header)-1)
 print("First 10:", header[:10])

Out: Num features: 41
     First 10: ['duration', 'protocol_type', 'service', 'flag', 
     'src_bytes', 'dst_bytes', 'land', 'wrong_fragment', 'urgent', 'hot']

现在,让我们创建两个单独的 RDD-一个用于训练数据,另一个用于测试数据:

In: train_rdd = sc.textFile('file:////home//jovyan//datasets//kddtrain')
 test_rdd = sc.textFile('file:////home//jovyan//datasets//kddtest')

现在,我们需要解析每个文件的每一行以创建一个DataFrame。 首先,我们将 CSV 文件的每一行拆分为单独的字段,然后将每个数字值转换为浮点,并将每个文本值转换为字符串。 最后,我们删除每行末尾的点。

最后一步,通过使用sqlContext提供的createDataFrame方法,我们可以为训练和测试数据集创建两个带有命名列的 Spark DataFrame

In: def line_parser(line):
 def piece_parser(piece):
 if "." in piece or piece.isdigit():
 return float(piece)
 else:
 return piece
 return [piece_parser(piece) for piece in line[:-1].split(',')]

 train_df = sqlContext.createDataFrame(train_rdd.map(line_parser),header)
 test_df = sqlContext.createDataFrame(test_rdd.map(line_parser), header)

到目前为止,我们仅编写了 RDD 转换器。 让我们介绍一个操作,以查看我们在数据集中有多少个观察值,同时检查前面代码的正确性:

In: print("Train observations:", train_df.count())
 print("Test observations:", test_df.count())

Out: Train observations: 494021
     Test observations: 311029

尽管我们使用的是整个 KDD99 数据集的十分之一,但我们仍在处理 50 万个观测值。 乘以特征数量41,我们可以清楚地看到,我们将在包含超过 2000 万个值的观察矩阵上训练分类器。 对于 Spark 来说,这不是一个很大的数据集(也不是完整的 KDD99); 世界各地的开发人员已经在 PB 级和数十亿条记录上使用了它。 如果数字看起来很大,请不要害怕:Spark 旨在解决这些问题。 现在,让我们看一下它在DataFrame架构上的外观。 具体来说,我们要确定哪些字段是数字字段,哪些字段包含字符串(请注意,为简洁起见,结果已被截断):

In: train_df.printSchema()

Out: root
      |-- duration: double (nullable = true)
      |-- protocol_type: string (nullable = true)
      |-- service: string (nullable = true)
      |-- flag: string (nullable = true)
      |-- src_bytes: double (nullable = true)
      |-- dst_bytes: double (nullable = true)
 ... |-- target: string (nullable = true

特征工程

从视觉分析来看,只有四个字段是字符串:protocol_typeserviceflagtarget(这是预期的多类目标标签)。 由于我们将使用基于树的分类器,因此我们希望将每个级别的文本编码为每个变量的数字。 使用 Scikit-learn,可以使用sklearn.preprocessing.LabelEncoder对象完成此操作。 在 Spark 中等效的是pyspark.ml.feature包的StringIndexer。 我们需要用 Spark 编码四个变量,然后我们必须将四个StringIndexer对象以级联的方式链接在一起:它们每个都将在DataFrame的特定列上操作,并输出一个带有附加列的DataFrame(类似map操作)。 映射是自动的,按频率排序:Spark 在所选列中对每个级别的计数进行排名,将最流行的级别映射到0,然后映射到1,依此类推。 请注意,通过此操作,您将遍历数据集一次以计算每个级别的出现次数。 如果您已经知道该映射,则广播该映射并使用map操作会更有效,如本章开头所示。

更笼统地说,pyspark.ml.feature包中包含的所有类都用于从DataFrame中提取,转换和选择特征。 他们所有人都在DataFrame中读取一些列并创建其他列。

类似地,我们可以使用单热编码器生成数值观察矩阵。 对于单编码器,在DataFrame中将有多个输出列,每个分类特征的每个级别对应一个输出列。 为此,Spark 提供了pyspark.ml.feature.OneHotEncoderEstimator类。

从 Spark 2.3.1 开始,Python 可用的功能操作包含在以下详尽列表中(所有这些可以在pyspark.ml.feature包中找到)。 除了几个名称外,其他名称应直观易懂,这些内容将以内联或稍后的方式进行解释。

回到该示例,我们现在想将每个分类变量中的级别编码为离散数字。 正如我们所解释的,为此,我们将为每个变量使用StringIndexer对象。 此外,我们可以使用 ML 管道并将其设置为其阶段。

然后,要适合所有索引器,只需调用管道的fit方法。 在内部,它将顺序地适合所有暂存的对象。 完成拟合操作后,将创建一个新对象,我们可以将其称为拟合管道。 调用此新对象的transform方法将顺序调用所有已登台的元素(已装配),每个元素都在上一个元素完成后被调用。 在以下代码片段中,您将看到运行中的管道。 请注意,转换器组成了管道。 因此,由于不存在任何动作,因此实际上不执行任何操作。 在输出DataFrame中,您会注意到另外四列,它们的名称与原始分类的列相同,但后缀为_cat

In: from pyspark.ml import Pipeline
 from pyspark.ml.feature import StringIndexer

 cols_categorical = ["protocol_type", "service", "flag","target"]
 preproc_stages = []
 for col in cols_categorical:
 out_col = col + "_cat"
 preproc_stages.append(
 StringIndexer(
 inputCol=col, outputCol=out_col, handleInvalid="skip"))

 pipeline = Pipeline(stages=preproc_stages)
 indexer = pipeline.fit(train_df)
 train_num_df = indexer.transform(train_df) 
 test_num_df = indexer.transform(test_df)

让我们进一步研究管道。 在这里,我们将看到管道中的阶段:不适合的管道和适合的管道。 请注意,Spark 和 Scikit-learn 之间有很大的区别:在 Scikit-learn 中,fittransform在同一个对象上调用,在 Spark 中,fit方法产生一个新对象(通常,其名称与Model后缀,就像PipelinePipelineModel一样),您可以在其中调用transform方法。 这种差异源于闭包-拟合的对象易于分布在流程和集群中:

In: print(pipeline.getStages(), '\n')
 print(pipeline)
 print(indexer)

Out: [StringIndexer_44f6bd05e502a8ace0aa, 
     StringIndexer_414084eb873c15c387cd, 
 StringIndexer_4ca38a4ad6ffeb6ddc95,  
     StringIndexer_489c92cd030c80c6f677] 

     Pipeline_46a68853ff9dcdece078
     PipelineModel_4f61afaf96ccc4be4b02

DataFrame中提取某些列就像在 SQL 查询中使用SELECT一样容易。 现在,让我们为所有数字特征建立一个名称列表。 从标题中找到的名称开始,我们删除分类名称,并将其替换为 数字衍生的名称。 最后,由于只需要特征,因此删除了目标变量及其从数值上导出的等效项:

In: features_header = set(header) \
    - set(cols_categorical) \
    | set([c + "_cat" for c in cols_categorical]) \
    - set(["target", "target_cat"])
    features_header = list(features_header)
    print(features_header)
    print("Total numerical features:", len(features_header))

Out: ['flag_cat', 'count', 'land', 'serror_rate', 'num_compromised', 
 'num_access_files', 'dst_host_srv_serror_rate', 'src_bytes', 
     'num_root', 'srv_serror_rate', 'num_shells', 'diff_srv_rate', 
     'dst_host_serror_rate', 
 'rerror_rate', 'num_file_creations', 'same_srv_rate', 
     'service_cat', 
 'num_failed_logins', 'duration', 'dst_host_diff_srv_rate', 'hot', 
 'is_guest_login', 'dst_host_same_srv_rate', 'num_outbound_cmds', 
 'su_attempted', 'dst_host_count', 'dst_bytes', 
     'srv_diff_host_rate', 
 'dst_host_srv_count', 'srv_count', 'root_shell', 
     'srv_rerror_rate', 
 'wrong_fragment', 'dst_host_rerror_rate', 'protocol_type_cat', 
     'urgent', 
 'dst_host_srv_rerror_rate', 'dst_host_srv_diff_host_rate', 
     'logged_in', 
 'is_host_login', 'dst_host_same_src_port_rate']
     Total numerical features: 41

在这里,VectorAssembler类可以帮助我们构建特征矩阵。 我们只需要传递要选择的列作为参数,并传递要在DataFrame中创建的新列。 我们决定将输出列简单地命名为features。 我们将此转换应用于训练和测试数据集,然后仅选择我们感兴趣的两列featurestarget_cat

In: from pyspark.ml.feature import VectorAssembler

 assembler = VectorAssembler(
 inputCols=features_header,
 outputCol="features")
 Xy_train = (assembler
 .transform(train_num_df)
 .select("features", "target_cat"))
 Xy_test = (assembler
 .transform(test_num_df)
 .select("features", "target_cat"))

同样,VectorAssembler的默认行为是生成DenseVectorsSparseVectors。 在这种情况下,由于特征的向量包含许多零,因此它将返回稀疏向量。 要查看输出内容,我们可以打印第一行。 请注意,这是一个动作。 因此,将在打印结果之前执行作业:

In: Xy_train.first() 
Out: Row(features=SparseVector(41, {1: 8.0, 7: 181.0, 15: 1.0, 16: 2.0, 22:   
         1.0, 25: 9.0, 26: 5450.0, 28: 9.0, 29: 8.0, 34: 1.0, 38: 1.0, 
         40: 0.11}), target_cat=2.0)

训练学习器

最后,我们到达了任务的热点:训练分类器。 分类器包含在pyspark.ml.classification包中,对于本示例,我们使用随机森林。 对于 Spark 2.3.1,可以在这个页面中找到可用的算法的广泛列表。 算法列表非常完整,包括线性模型,SVM,朴素贝叶斯和树集成。 请注意,并非所有这些工具都能够处理多类问题,并且可能具有不同的参数。 始终检查与使用版本有关的文档。 除分类器外,Spark 2.3.1 中使用 Python 接口实现的其他学习器如下:

  • 群集(pyspark.ml.clustering包):KMeans
  • 推荐人(pyspark.ml.recommendation包):ALS(协同过滤推荐器,基于交替最小二乘法)

让我们回到 KDD99 挑战的目标。 现在,该实例化随机森林分类器并设置其参数了。 要设置的参数是featuresCol(包含特征矩阵的列),labelCol(包含目标标签的数据帧的列),seed(使实验可复制的随机种子)和maxBins (用于树的每个节点中的分割点的最大箱子数)。 森林中树木数量的默认值为20,每棵树的最大深度为 5 级。 此外,默认情况下,此分类器在DataFrame中创建三个输出列:rawPrediction(用于存储每个可能标签的预测得分),probability(用于存储每个标签的可能性)和prediction(最可能的标签):

In: from pyspark.ml.classification import RandomForestClassifier
 clf = RandomForestClassifier(
 labelCol="target_cat", featuresCol="features",
 maxBins=100, seed=101)
 fit_clf = clf.fit(Xy_train)

即使在这种情况下,训练有素的分类器也是一个不同的对象。 与以前完全一样,训练有素的分类器的名称与带Model后缀的分类器相同:

In: print(clf)
 print(fit_clf)
Out: RandomForestClassifier_4c47a18a99f683bec69e
     RandomForestClassificationModel   
     (uid=RandomForestClassifier_4c47a18a99f683bec69e) with 20 trees

在训练好的classifier对象(即RandomForestClassificationModel)上,可以调用transform方法。 我们预测训练和test数据集上的标签,并打印test数据集的第一行。 根据分类器中的定义,预测将在名为prediction的列中找到:

In: Xy_pred_train = fit_clf.transform(Xy_train)
 Xy_pred_test = fit_clf.transform(Xy_test)
 print("First observation after classification stage:")
 print(Xy_pred_test.first())

Out: First observation after classification stage:
     Row(features=SparseVector(41, {1: 1.0, 7: 105.0, 15: 1.0, 16: 1.0, 19: 
 0.01, 22: 1.0, 25: 255.0, 26: 146.0, 28: 254.0, 29: 1.0, 34: 2.0}), 
 target_cat=2.0, rawPrediction=DenseVector([0.0152, 0.0404, 19.6276, 
 0.0381, 0.0087, 0.0367, 0.034, 0.1014, 0.0641, 0.0051, 0.0105, 0.0053, 
 0.002, 0.0005, 0.0026, 0.0009, 0.0018, 0.0009, 0.0009, 0.0006, 0.0013, 
 0.0006, 0.0008]), probability=DenseVector([0.0008, 0.002, 0.9814, 
     0.0019, 
 0.0004, 0.0018, 0.0017, 0.0051, 0.0032, 0.0003, 0.0005, 0.0003, 
     0.0001, 
 0.0, 0.0001, 0.0, 0.0001, 0.0, 0.0, 0.0, 0.0001, 0.0, 0.0]), 
 prediction=2.0)

评估学习器的表现

任何数据科学任务的下一步都是检查学习器在训练和测试数据集上的表现。 对于此任务,我们将使用F1-score,因为它是合并精度和召回表现的好指标。 评估指标包含在pyspark.ml.evaluation包中; 在我们拥有的几种选择中,我们正在使用一种来评估多类分类器:MulticlassClassificationEvaluator。 作为参数,我们提供指标(precisionrecallaccuracyF1-score等)以及包含真实标签和预测标签的列的名称:

In: from pyspark.ml.evaluation import MulticlassClassificationEvaluator
 evaluator = MulticlassClassificationEvaluator(
 labelCol="target_cat", 
 predictionCol="prediction", 
 metricName="f1")
 f1_train = evaluator.evaluate(Xy_pred_train)
 f1_test = evaluator.evaluate(Xy_pred_test)
 print("F1-score train set: %0.3f" % f1_train)
 print("F1-score test set: %0.3f" % f1_test)

Out: F1-score train set: 0.993
 F1-score test set: 0.968

所获得的值非常高,并且训练数据集和测试数据集的表现之间存在很大差异。 除了用于多类分类器的评估器之外,用于回归器(度量标准可以是 MSE,RMSE,R2 或 MAE)和二分类器的评估器对象在同一包中可用。

机器学习管道的力量

到目前为止,我们已经逐步构建并显示了输出。 也可以将所有操作层叠在一起并将它们设置为管道的各个阶段。 实际上,我们可以将到目前为止所看到的内容(四个标签编码器,向量生成器和分类器)链接到一个独立的管道中,将其适合训练数据集,最后在测试数据集上使用它来获得预测 。

这种操作方式更有效,但是您将失去逐步分析的探索能力。 建议作为数据科学家的读者仅在完全确定内部情况后才使用端到端管道,并且仅构建生产模型。 为了显示该管道与我们到目前为止所看到的等效,我们在测试数据集上计算F1-score并进行打印。 毫不奇怪,它的值完全相同:

In: full_stages = preproc_stages + [assembler, clf]
 full_pipeline = Pipeline(stages=full_stages)
 full_model = full_pipeline.fit(train_df)
 predictions = full_model.transform(test_df)
 f1_preds = evaluator.evaluate(predictions)
 print("F1-score test set: %0.3f" % f1_preds)

Out: F1-score test set: 0.968

在运行 IPython 笔记本的驱动程序节点上,我们还可以使用matplotlib库来可视化我们的分析结果。 例如,要显示分类结果的归一化混淆矩阵(在每个类的支持下归一化),我们可以创建以下函数:

In: import numpy as np
 import matplotlib.pyplot as plt
 %matplotlib inline

 def plot_confusion_matrix(cm):
 cm_normalized = \
 cm.astype('float') / cm.sum(axis=1)[:, np.newaxis]
 plt.imshow(
 cm_normalized, interpolation='nearest', cmap=plt.cm.Blues)
 plt.title('Normalized Confusion matrix')
 plt.colorbar()
 plt.tight_layout()
 plt.ylabel('True label')
 plt.xlabel('Predicted label')

Spark 可以构建confusion矩阵,但是该方法在pyspark.mllib包中。 为了能够使用此包中的方法,我们必须使用.rdd方法将DataFrame转换为 RDD:

In: from pyspark.mllib.evaluation import MulticlassMetrics

 metrics = MulticlassMetrics(
 predictions.select("prediction", "target_cat").rdd)
 conf_matrix = metrics.confusionMatrix()toArray()
 plot_confusion_matrix(conf_matrix)

这是从前面的代码片段得到的绘制的混淆矩阵:

手动调整

尽管F1-score接近0.97,但归一化的混淆矩阵显示类别非常不平衡,并且分类器刚刚学会了如何正确地对最受欢迎的类别进行分类。 为了改善结果,我们可以对每个类别进行重新采样,从而有效地尝试更好地平衡训练数据集。

首先,让我们计算一下每个类别的训练数据集中有多少个案例:

In: train_composition = (train_df.groupBy("target")
 .count()
 .rdd
 .collectAsMap())
 print(train_composition)

Out: {'neptune': 107201,
      'nmap': 231,
      'portsweep': 1040,
      'back': 2203,
      'warezclient': 1020,
      'normal': 97278,
 ... 'loadmodule': 9,
      'phf': 4}

这清楚地表明了严重的不平衡。 我们可以尝试通过对稀有类进行过度采样和对太受欢迎的类进行二次采样来提高表现。 在此示例中,我们将创建一个训练数据集,其中每个类别至少代表 1,000 次,但最多代表 25,000 次。 为此,我们执行以下步骤:

  1. 让我们首先创建子采样/过采样率,并将其在整个集群中广播,然后flatMap训练数据集的每一行对其进行正确的重新采样:
In: def set_sample_rate_between_vals(cnt, the_min, the_max):
 if the_min <= cnt <= the_max:
 # no sampling
 return 1
 elif cnt < the_min:
 # Oversampling: return many times the same observation
 return the_min/float(cnt)
 else:
 # Subsampling: sometime don't return it
 return the_max/float(cnt)

 sample_rates = {k:set_sample_rate_between_vals(v, 1000, 25000) 
 for k,v in train_composition.items()}
 sample_rates

Out: {'neptune': 0.23320677978750198,
      'nmap': 4.329004329004329,
      'portsweep': 1,
      'back': 1,
      'warezclient': 1,
      'normal': 0.2569954152017928,
 ... 'loadmodule': 111.11111111111111,
      'phf': 250.0}

In: bc_sample_rates = sc.broadcast(sample_rates)

 def map_and_sample(el, rates):
 rate = rates.value[el['target']]
 if rate > 1:
 return [el]*int(rate)
 else:
 import random
 return [el] if random.random() < rate else []

 sampled_train_df = (train_df
 .rdd
 .flatMap(
 lambda x: map_and_sample(x, 
                                       bc_sample_rates))
 .toDF()
 .cache())
  1. sampled_train_df``DataFrame变量中的这些采样数据集也被缓存; 我们将在超参数优化步骤中多次使用它。 它应该很容易装入内存,因为行数少于原始数:
In: sampled_train_df.count()

Out: 96559
  1. 为了了解其中的内容,我们可以打印第一行。 非常快速地打印出值,不是吗? 当然,这已经非常快了,因为它已经被缓存了:
In: sampled_train_df.first()

Out: Row(duration=0.0, protocol_type='tcp', service='http', 
     flag='SF', 
 src_bytes=210.0, dst_bytes=624.0, land=0.0, 
     wrong_fragment=0.0, 
 urgent=0.0, hot=0.0, num_failed_logins=0.0, logged_in=1.0, 
 num_compromised=0.0, root_shell=0.0, su_attempted=0.0, 
     num_root=0.0, 
 num_file_creations=0.0, num_shells=0.0, num_access_files=0.0, 
 num_outbound_cmds=0.0, is_host_login=0.0, is_guest_login=0.0, 
     count=18.0, 
 srv_count=18.0, serror_rate=0.0, srv_serror_rate=0.0, 
     rerror_rate=0.0, 
 srv_rerror_rate=0.0, same_srv_rate=1.0, diff_srv_rate=0.0, 
 srv_diff_host_rate=0.0, dst_host_count=18.0, 
     dst_host_srv_count=109.0, 
 dst_host_same_srv_rate=1.0, dst_host_diff_srv_rate=0.0, 
 dst_host_same_src_port_rate=0.06, 
     dst_host_srv_diff_host_rate=0.05, 
 dst_host_serror_rate=0.0, dst_host_srv_serror_rate=0.0, 
 dst_host_rerror_rate=0.0, dst_host_srv_rerror_rate=0.0, 
     target='normal')
  1. 现在,让我们使用我们创建的管道进行一些预测,并打印此新解决方案的F1-score
In:  full_model = full_pipeline.fit(sampled_train_df)
 predictions = full_model.transform(test_df)
 f1_preds = evaluator.evaluate(predictions)
 print("F1-score test set: %0.3f" % f1_preds)

Out: F1-score test set: 0.967
  1. 50树的分类器上进行测试。 为此,我们可以构建另一个管道(名为refined_pipeline),并用新的分类器代替最后一个阶段。 即使训练数据集的大小已减少,表现似乎也相同:
In: clf = RandomForestClassifier(
 numTrees=50, maxBins=100, seed=101,
 labelCol="target_cat", featuresCol="features")
 stages = full_pipeline.getStages()[:-1]
 stages.append(clf)
 refined_pipeline = Pipeline(stages=stages)
 refined_model = refined_pipeline.fit(sampled_train_df)
 predictions = refined_model.transform(test_df)
 f1_preds = evaluator.evaluate(predictions)
 print ("F1-score test set: %0.3f" % f1_preds )

Out: F1-score test set: 0.968

到此,我们结束了有关在 Spark 上调整模型的示例。 最终测试为我们提供了关于模型在生产中的有效性的合理估计。

交叉验证

在尝试了许多不同的配置之后,我们可以继续进行手动优化并找到合适的模型。 这样做会导致时间的大量浪费(以及代码的可重用性),并使测试数据集过拟合。 交叉验证是运行超参数优化的正确关键。 现在让我们看看 Spark 如何执行这项关键任务。

首先,由于训练将被多次使用,因此我们可以cache。 因此,让我们在所有转换后使用cache

In: pipeline_to_clf = Pipeline(
 stages=preproc_stages + [assembler]).fit(sampled_train_df)
 train = pipeline_to_clf.transform(sampled_train_df).cache()
 test = pipeline_to_clf.transform(test_df)

pyspark.ml.tuning包中包含使用交叉验证进行超参数优化的有用类。 两个特征是必不可少的:参数的网格图(可以使用ParamGridBuilder构建)和实际的交叉验证过程(由CrossValidator类运行)。

在此示例中,我们要设置分类器的一些参数,这些参数在整个交叉验证过程中都不会改变。 就像 Scikit-learn 一样,它们是在创建classification对象时设置的(在这种情况下,是列名,种子和最大数量的容器)。

然后,借助网格生成器,我们决定应为交叉验证算法的每次迭代更改哪些参数。 在此示例中,我们要检查分类表现是否将森林中每棵树的最大深度从3更改为12(增加 3),并将森林中的树数从 20 或 50 改变为最后。 设置网格图,我们要测试的分类器和折叠次数后,启动交叉验证(使用fit方法)。 参数评估器是必不可少的:它会告诉我们哪种是交叉验证后保持的最佳模型。 请注意,此操作可能需要 15-20 分钟才能运行(在引擎盖下,训练并测试了4 * 2 * 3 = 24模型):

In: from pyspark.ml.tuning import ParamGridBuilder, CrossValidator

 rf = RandomForestClassifier(
 cacheNodeIds=True, seed=101, labelCol="target_cat",
 featuresCol="features", maxBins=100)
 grid = (ParamGridBuilder()
 .addGrid(rf.maxDepth, [3, 6, 9, 12])
 .addGrid(rf.numTrees, [20, 50])
 .build())
 cv = CrossValidator(
 estimator=rf, estimatorParamMaps=grid,
 evaluator=evaluator, numFolds=3)
 cvModel = cv.fit(train)

最后,我们可以使用交叉验证的模型来预测标签,因为我们本身就是在使用管道或分类器。 在这种情况下,通过交叉验证选择的分类器的表现要比前一种情况稍好,这使我们能够克服0.97障碍:

In: predictions = cvModel.transform(test)
 f1_preds = evaluator.evaluate(predictions)
 print("F1-score test set: %0.3f" % f1_preds)

Out: F1-score test set: 0.970

此外,通过绘制归一化的混淆矩阵,您会立即意识到,该解决方案能够发现更广泛的攻击,甚至是不太流行的攻击:

In: metrics = MulticlassMetrics(
 predictions.select("prediction", "target_cat").rdd)
 conf_matrix = metrics.confusionMatrix().toArray()
 plot_confusion_matrix(conf_matrix)

这次,输出是归一化的混淆矩阵,显示了预测中错位最多的地方:

最终清理

在这里,我们处于分类任务的结尾。 记住要从cache中删除所有已使用的变量和已创建的临时表:

In: bc_sample_rates.unpersist()
 sampled_train_df.unpersist()
 train.unpersist()

清除 Spark 内存后,我们可以关闭 Jupyter 笔记本电脑。

总结

在本章中,我们向您介绍了 Hadoop 生态系统,包括架构,HDFS 和 PySpark。 介绍之后,我们开始设置本地 Spark 实例,并在群集节点之间共享变量后,我们使用 RDD 和DataFrame在 Spark 中进行了数据处理。

在本章的稍后部分,我们学习了使用 Spark 进行机器学习的知识,其中包括读取数据集,训练学习器,机器学习管道的功能,交叉验证,甚至测试通过示例数据集获得的知识。

到此结束我们围绕 Python 进行数据科学基础研究的旅程,下一章只是刷新和增强 Python 基础的附录。 总之,通过本书的所有章节,我们已经完成了数据科学项目的介绍,接触了项目的所有关键步骤,并向您展示了使用 Python 成功操作自己的项目的所有基本工具。 作为学习工具,这本书陪同您完成了数据科学的所有阶段,从数据加载到机器学习和可视化,阐述了无论数据大小的最佳实践和避免常见陷阱的方法。 作为参考,本书涉及各种命令和包,为您提供了简单明了的说明和示例,如果在项目中重复使用这些示例和示例,则可以在工作期间节省大量时间。

从现在开始,Python 肯定会在您的项目开发中扮演更重要的角色,到目前为止,我们很高兴能陪同您一起为数据科学掌握 Python。