无监督学习实用指南-五-

61 阅读53分钟

无监督学习实用指南(五)

原文:annas-archive.org/md5/5d48074db68aa41a4c5eb547fcbf1a69

译者:飞龙

协议:CC BY-NC-SA 4.0

第十二章:生成对抗网络

我们已经探索了两种生成模型:RBM 和 DBN。在本章中,我们将探讨生成对抗网络(GANs),这是无监督学习和生成建模中最新和最有前景的领域之一。

GANs,概念

GANs 是由 Ian Goodfellow 及其蒙特利尔大学的同行研究人员在 2014 年引入的。在 GANs 中,我们有两个神经网络。一个网络称为生成器,根据其已创建的模型生成数据,该模型是使用其作为输入接收到的真实数据样本创建的。另一个网络称为鉴别器,用于区分生成器创建的数据和来自真实分布的数据。

简单类比,生成器就像是伪造者,而鉴别器则是试图识别伪造品的警察。这两个网络处于零和博弈中。生成器试图欺骗鉴别器,使其认为合成数据来自真实分布,而鉴别器则试图揭露合成数据为假。

GANs 是无监督学习算法,因为即使没有标签,生成器也可以学习真实分布的基本结构。生成器通过使用比其训练的数据量明显较少的一些参数来学习基本结构——这是我们在前几章中多次探讨过的无监督学习的核心概念。这一约束迫使生成器有效地捕捉到真实数据分布的最显著方面。这类似于深度学习中发生的表示学习。生成器的每个隐藏层捕捉到数据的底层表示——从非常简单的开始——而后续层通过在简单前层基础上构建更复杂的表示来增强。

使用所有这些层次,生成器学习数据的基本结构,并尝试创建几乎与真实数据相同的合成数据。如果生成器捕捉到了真实数据的本质,那么合成数据看起来将会是真实的。

GANs 的威力

在第十一章中,我们探讨了利用无监督学习模型(如深度信念网络)生成的合成数据来提高监督学习模型性能的能力。像 DBNs 一样,GANs 在生成合成数据方面非常擅长。

如果目标是生成大量新的训练样本,以帮助补充现有的训练数据——例如,以提高图像识别任务的准确性——我们可以使用生成器创建大量合成数据,将新合成数据添加到原始训练数据中,然后在现在大得多的数据集上运行监督式机器学习模型。

GANs 在异常检测方面也表现出色。如果目标是识别异常,例如检测欺诈、黑客攻击或其他可疑行为,我们可以使用判别器对真实数据中的每个实例进行评分。判别器排名为“可能合成”的实例将是最异常的实例,也是最有可能代表恶意行为的实例。

深度卷积 GANs

在本章中,我们将返回到我们在前几章中使用过的 MNIST 数据集,并应用一种 GANs 版本来生成合成数据以补充现有的 MNIST 数据集。然后我们将应用一个监督学习模型来进行图像分类。这是半监督学习的又一版本。

注意

顺便说一句,现在你应该对半监督学习有了更深的理解。因为世界上大部分数据都没有标签,无监督学习自身有效地帮助标记数据的能力非常强大。作为半监督机器学习系统的一部分,无监督学习增强了迄今为止所有成功商业应用的监督学习的潜力。

即使在半监督系统的应用之外,无监督学习也有独立运用的潜力,因为它能够从没有任何标签的数据中学习,并且是 AI 领域中从狭义 AI 向更广义 AI 应用迈进的最有潜力的领域之一。

我们将使用的 GANs 版本称为深度卷积生成对抗网络(DCGANs),这是由 Alec Radford、Luke Metz 和 Soumith Chintala 于 2015 年底首次引入的¹。

DCGANs 是一种无监督学习的形式卷积神经网络(CNNs),在监督学习系统中用于计算机视觉和图像分类方面被广泛使用并取得了巨大成功。在深入研究 DCGANs 之前,让我们首先探讨 CNNs,特别是它们在监督学习系统中用于图像分类的方式。

卷积神经网络

与数值和文本数据相比,图像和视频的计算成本要高得多。例如,一个 4K Ultra HD 图像的尺寸总共为 4096 x 2160 x 3(26,542,080)。直接在这种分辨率的图像上训练神经网络将需要数千万个神经元,并且导致非常长的训练时间。

而不是直接在原始图像上构建神经网络,我们可以利用图像的某些属性,即像素与附近的像素相关联,但通常与远处的像素无关。

卷积(从中卷积神经网络得名)是将图像进行滤波处理以减小图像尺寸而不丢失像素之间关系的过程。²

在原始图像上,我们应用几个特定大小的滤波器,称为核大小,并以小步长移动这些滤波器,称为步幅,以得出新的减少像素输出。卷积后,我们通过逐个小区域获取减少像素输出中的像素的最大值来进一步减小表示的大小。这称为最大池化

我们多次执行这种卷积和最大池化,以降低图像的复杂性。然后,我们展平图像并使用正常的全连接层进行图像分类。

现在让我们构建一个 CNN,并在 MNIST 数据集上进行图像分类。首先,我们将加载必要的库:

'''Main'''
import numpy as np
import pandas as pd
import os, time, re
import pickle, gzip, datetime

'''Data Viz'''
import matplotlib.pyplot as plt
import seaborn as sns
color = sns.color_palette()
import matplotlib as mpl
from mpl_toolkits.axes_grid1 import Grid

%matplotlib inline

'''Data Prep and Model Evaluation'''
from sklearn import preprocessing as pp
from sklearn.model_selection import train_test_split
from sklearn.model_selection import StratifiedKFold
from sklearn.metrics import log_loss, accuracy_score
from sklearn.metrics import precision_recall_curve, average_precision_score
from sklearn.metrics import roc_curve, auc, roc_auc_score, mean_squared_error

'''Algos'''
import lightgbm as lgb

'''TensorFlow and Keras'''
import tensorflow as tf
import keras
from keras import backend as K
from keras.models import Sequential, Model
from keras.layers import Activation, Dense, Dropout, Flatten, Conv2D, MaxPool2D
from keras.layers import LeakyReLU, Reshape, UpSampling2D, Conv2DTranspose
from keras.layers import BatchNormalization, Input, Lambda
from keras.layers import Embedding, Flatten, dot
from keras import regularizers
from keras.losses import mse, binary_crossentropy
from IPython.display import SVG
from keras.utils.vis_utils import model_to_dot
from keras.optimizers import Adam, RMSprop
from tensorflow.examples.tutorials.mnist import input_data

接下来,我们将加载 MNIST 数据集,并将图像数据存储在 4D 张量中,因为 Keras 需要图像数据以这种格式。我们还将使用 Keras 中的to_categorical函数从标签创建独热向量。

为了以后使用,我们还将从数据中创建 Pandas DataFrames。让我们再次使用本书早期的view_digit函数来查看这些图像:

# Load the datasets
current_path = os.getcwd()
file = '\\datasets\\mnist_data\\mnist.pkl.gz'
f = gzip.open(current_path+file, 'rb')
train_set, validation_set, test_set = pickle.load(f, encoding='latin1')
f.close()

X_train, y_train = train_set[0], train_set[1]
X_validation, y_validation = validation_set[0], validation_set[1]
X_test, y_test = test_set[0], test_set[1]

X_train_keras = X_train.reshape(50000,28,28,1)
X_validation_keras = X_validation.reshape(10000,28,28,1)
X_test_keras = X_test.reshape(10000,28,28,1)

y_train_keras = to_categorical(y_train)
y_validation_keras = to_categorical(y_validation)
y_test_keras = to_categorical(y_test)

# Create Pandas DataFrames from the datasets
train_index = range(0,len(X_train))
validation_index = range(len(X_train),len(X_train)+len(X_validation))
test_index = range(len(X_train)+len(X_validation),len(X_train)+ \
                   len(X_validation)+len(X_test))

X_train = pd.DataFrame(data=X_train,index=train_index)
y_train = pd.Series(data=y_train,index=train_index)

X_validation = pd.DataFrame(data=X_validation,index=validation_index)
y_validation = pd.Series(data=y_validation,index=validation_index)

X_test = pd.DataFrame(data=X_test,index=test_index)
y_test = pd.Series(data=y_test,index=test_index)

def view_digit(X, y, example):
    label = y.loc[example]
    image = X.loc[example,:].values.reshape([28,28])
    plt.title('Example: %d Label: %d' % (example, label))
    plt.imshow(image, cmap=plt.get_cmap('gray'))
    plt.show()

现在让我们构建 CNN。

我们将在 Keras 中调用Sequential()开始模型创建。然后,我们将添加两个卷积层,每个层有 32 个大小为 5 x 5 的过滤器,默认步幅为 1,并使用 ReLU 激活函数。然后,我们使用 2 x 2 的池化窗口和 1 的步幅进行最大池化。我们还执行 dropout,你可能记得这是一种正则化形式,用于减少神经网络的过拟合。具体来说,我们将丢弃输入单元的 25%。

在下一阶段,我们再次添加两个卷积层,这次使用 64 个大小为 3 x 3 的过滤器。然后,我们使用 2 x 2 的池化窗口和 2 的步幅进行最大池化。接着,我们添加一个 dropout 层,dropout 比例为 25%。

最后,我们将图像展平,添加一个具有 256 个隐藏单元的常规神经网络,使用 50%的 dropout 比例进行 dropout,并使用softmax函数进行 10 类分类:

model = Sequential()

model.add(Conv2D(filters = 32, kernel_size = (5,5), padding = 'Same',
                 activation ='relu', input_shape = (28,28,1)))
model.add(Conv2D(filters = 32, kernel_size = (5,5), padding = 'Same',
                 activation ='relu'))
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Dropout(0.25))

model.add(Conv2D(filters = 64, kernel_size = (3,3), padding = 'Same',
                 activation ='relu'))
model.add(Conv2D(filters = 64, kernel_size = (3,3), padding = 'Same',
                 activation ='relu'))
model.add(MaxPooling2D(pool_size=(2,2), strides=(2,2)))
model.add(Dropout(0.25))

model.add(Flatten())
model.add(Dense(256, activation = "relu"))
model.add(Dropout(0.5))
model.add(Dense(10, activation = "softmax"))

对于这个 CNN 训练,我们将使用Adam 优化器并最小化交叉熵。我们还将将图像分类的准确性作为评估指标存储。

现在让我们对模型进行一百个 epochs 的训练,并在验证集上评估结果:

# Train CNN
model.compile(optimizer='adam',
              loss='categorical_crossentropy',
              metrics=['accuracy'])

model.fit(X_train_keras, y_train_keras,
          validation_data=(X_validation_keras, y_validation_keras), \
          epochs=100)

图 12-1 显示了训练一百个 epochs 后的准确性。

CNN 结果

图 12-1. CNN 结果

正如您所看到的,我们刚刚训练的 CNN 最终准确率达到了 99.55%,优于本书中迄今为止训练过的任何 MNIST 图像分类解决方案。

重新审视 DCGANs

现在让我们再次转向深度卷积生成对抗网络。我们将建立一个生成模型,生成与原始 MNIST 图像非常相似的合成 MNIST 图像。

要生成接近真实但合成的图像,我们需要训练一个生成器,从原始的 MNIST 图像生成新的图像,以及一个判别器,判断这些图像是否与原始图像相似(基本上执行一种“胡说测试”)。

这里还有另一种思考方式。原始的 MNIST 数据集代表了原始的数据分布。生成器从这个原始分布中学习,并基于所学内容生成新的图像,而判别器则试图确定新生成的图像是否与原始分布几乎无法区分。

对于生成器,我们将使用 Radford、Metz 和 Chintala 在 ICLR 2016 年会议上提出的架构,这是我们之前引用过的(见 图 12-2)。

DCGAN 生成器

图 12-2。DCGAN 生成器

生成器接受一个初始的 噪声向量,这里显示为 100 x 1 的噪声向量,表示为 z,然后将其投影和重塑成 1024 x 4 x 4 张量。这种 投影和重塑 行为是卷积的反向过程,被称为 转置卷积(或在某些情况下称为 反卷积)。在转置卷积中,卷积的原始过程被反转,将一个缩小的张量映射到一个较大的张量³。

在初始的转置卷积之后,生成器应用四个额外的反卷积层映射到最终的 64 x 3 x 3 张量。

这里是各个阶段:

100 x 1 → 1024 x 4 x 4 → 512 x 8 x 8 → 256 x 16 x 16 → 128 x 32 x 32 → 64 x 64 x 3

在设计 MNIST 数据集上的 DCGAN 时,我们将应用类似(但不完全相同)的架构。

DCGAN 的生成器

对于我们设计的 DCGAN,我们将利用 Rowel Atienza 的工作并在此基础上构建⁴。我们首先会创建一个名为 DCGAN 的类,用于构建生成器、判别器、判别器模型和对抗模型。

让我们从生成器开始。我们将为生成器设置几个参数,包括辍学率(默认值为 0.3)、张量的深度(默认值为 256)以及其他维度(默认值为 7 x 7)。我们还将使用批归一化,其默认动量值为 0.8。初始输入维度为一百,最终输出维度为 28 x 28 x 1。

请记住,辍学和批归一化都是正则化器,帮助我们设计的神经网络避免过拟合。

要构建生成器,我们从 Keras 中调用 Sequential() 函数。然后,我们通过调用 Dense() 函数添加一个全连接神经网络层。它的输入维度为 100,输出维度为 7 x 7 x 256。我们将执行批归一化,使用 ReLU 激活函数,并执行辍学:

def generator(self, depth=256, dim=7, dropout=0.3, momentum=0.8, \
              window=5, input_dim=100, output_depth=1):
    if self.G:
        return self.G
    self.G = Sequential()
    self.G.add(Dense(dim*dim*depth, input_dim=input_dim))
    self.G.add(BatchNormalization(momentum=momentum))
    self.G.add(Activation('relu'))
    self.G.add(Reshape((dim, dim, depth)))
    self.G.add(Dropout(dropout))

接下来,我们将进行 上采样转置卷积 三次。每次,我们将输出空间的深度从 256 逐渐减半至 128、64、32,并增加其他维度。我们将保持 5 x 5 的卷积窗口和默认的步幅为一。在每次转置卷积期间,我们将执行批归一化,并使用 ReLU 激活函数。

这是它的样子:

100 → 7 x 7 x 256 → 14 x 14 x 128 → 28 x 28 x 64 → 28 x 28 x 32 → 28 x 28 x 1

    self.G.add(UpSampling2D())
    self.G.add(Conv2DTranspose(int(depth/2), window, padding='same'))
    self.G.add(BatchNormalization(momentum=momentum))
    self.G.add(Activation('relu'))

    self.G.add(UpSampling2D())
    self.G.add(Conv2DTranspose(int(depth/4), window, padding='same'))
    self.G.add(BatchNormalization(momentum=momentum))
    self.G.add(Activation('relu'))

    self.G.add(Conv2DTranspose(int(depth/8), window, padding='same'))
    self.G.add(BatchNormalization(momentum=momentum))
    self.G.add(Activation('relu'))

最后,生成器将输出一个 28 x 28 的图像,与原始 MNIST 图像具有相同的尺寸:

    self.G.add(Conv2DTranspose(output_depth, window, padding='same'))
    self.G.add(Activation('sigmoid'))
    self.G.summary()
    return self.G

DCGAN 的鉴别器

对于鉴别器,我们将将默认的 dropout 百分比设置为 0.3,深度为 64,并将 LeakyReLU 函数的 alpha 设置为 0.3。⁵

首先,我们将加载一个 28 x 28 x 1 的图像,并使用 64 个通道、5 x 5 的滤波器和步幅为二进行卷积。我们将使用 LeakyReLU 作为激活函数,并执行 dropout。我们将继续这个过程三次,每次将输出空间的深度加倍,同时减少其他维度。对于每个卷积,我们将使用 LeakyReLU 激活函数和 dropout。

最后,我们将展平图像,并使用 Sigmoid 函数输出一个概率。这个概率表示鉴别器对输入图像判断为伪造的信心程度(0.0 表示伪造,1.0 表示真实)。

这是它的样子:

28 x 28 x 1 → 14 x 14 x 64 → 7 x 7 x 128 → 4 x 4 x 256 → 4 x 4 x 512 → 1

def discriminator(self, depth=64, dropout=0.3, alpha=0.3):
    if self.D:
        return self.D
    self.D = Sequential()
    input_shape = (self.img_rows, self.img_cols, self.channel)
    self.D.add(Conv2D(depth*1, 5, strides=2, input_shape=input_shape,
        padding='same'))
    self.D.add(LeakyReLU(alpha=alpha))
    self.D.add(Dropout(dropout))

    self.D.add(Conv2D(depth*2, 5, strides=2, padding='same'))
    self.D.add(LeakyReLU(alpha=alpha))
    self.D.add(Dropout(dropout))

    self.D.add(Conv2D(depth*4, 5, strides=2, padding='same'))
    self.D.add(LeakyReLU(alpha=alpha))
    self.D.add(Dropout(dropout))

    self.D.add(Conv2D(depth*8, 5, strides=1, padding='same'))
    self.D.add(LeakyReLU(alpha=alpha))
    self.D.add(Dropout(dropout))

    self.D.add(Flatten())
    self.D.add(Dense(1))
    self.D.add(Activation('sigmoid'))
    self.D.summary()
    return self.D

鉴别器和对抗模型

接下来,我们定义鉴别器模型(即检测伪造品的警察)和对抗模型(即从警察学习的伪造者)。对于对抗模型和鉴别器模型,我们将使用 RMSprop 优化器,将损失函数定义为二元交叉熵,并使用准确率作为我们的报告指标。

对于对抗模型,我们使用之前定义的生成器和鉴别器网络。对于鉴别器模型,我们仅使用鉴别器网络:

def discriminator_model(self):
    if self.DM:
        return self.DM
    optimizer = RMSprop(lr=0.0002, decay=6e-8)
    self.DM = Sequential()
    self.DM.add(self.discriminator())
    self.DM.compile(loss='binary_crossentropy', \
                    optimizer=optimizer, metrics=['accuracy'])
    return self.DM

def adversarial_model(self):
    if self.AM:
        return self.AM
    optimizer = RMSprop(lr=0.0001, decay=3e-8)
    self.AM = Sequential()
    self.AM.add(self.generator())
    self.AM.add(self.discriminator())
    self.AM.compile(loss='binary_crossentropy', \
                    optimizer=optimizer, metrics=['accuracy'])
    return self.AM

用于 MNIST 数据集的 DCGAN

现在让我们为 MNIST 数据集定义 DCGAN。首先,我们将为 28 x 28 x 1 的 MNIST 图像初始化 MNIST_DCGAN 类,并使用之前定义的生成器、鉴别器模型和对抗模型:

class MNIST_DCGAN(object):
    def __init__(self, x_train):
        self.img_rows = 28
        self.img_cols = 28
        self.channel = 1

        self.x_train = x_train

        self.DCGAN = DCGAN()
        self.discriminator =  self.DCGAN.discriminator_model()
        self.adversarial = self.DCGAN.adversarial_model()
        self.generator = self.DCGAN.generator()

train 函数将默认进行两千次训练周期,并使用批大小为 256。在这个函数中,我们将批量的图像输入到刚刚定义的 DCGAN 架构中。生成器将生成图像,鉴别器将判断图像是真实的还是假的。在这个对抗模型中,随着生成器和鉴别器的较量,合成图像变得越来越接近原始的 MNIST 图像:

def train(self, train_steps=2000, batch_size=256, save_interval=0):
    noise_input = None
    if save_interval>0:
        noise_input = np.random.uniform(-1.0, 1.0, size=[16, 100])
    for i in range(train_steps):
        images_train = self.x_train[np.random.randint(0,
            self.x_train.shape[0], size=batch_size), :, :, :]
        noise = np.random.uniform(-1.0, 1.0, size=[batch_size, 100])
        images_fake = self.generator.predict(noise)
        x = np.concatenate((images_train, images_fake))
        y = np.ones([2*batch_size, 1])
        y[batch_size:, :] = 0

        d_loss = self.discriminator.train_on_batch(x, y)

        y = np.ones([batch_size, 1])
        noise = np.random.uniform(-1.0, 1.0, size=[batch_size, 100])
        a_loss = self.adversarial.train_on_batch(noise, y)
        log_mesg = "%d: [D loss: %f, acc: %f]" % (i, d_loss[0], d_loss[1])
        log_mesg = "%s [A loss: %f, acc: %f]" % (log_mesg, a_loss[0], \
                                                  a_loss[1])
        print(log_mesg)
        if save_interval>0:
            if (i+1)%save_interval==0:
                self.plot_images(save2file=True, \
                    samples=noise_input.shape[0],\
                    noise=noise_input, step=(i+1))

我们也来定义一个函数来绘制由这个 DCGAN 模型生成的图像:

def plot_images(self, save2file=False, fake=True, samples=16, \
                noise=None, step=0):
    filename = 'mnist.png'
    if fake:
        if noise is None:
            noise = np.random.uniform(-1.0, 1.0, size=[samples, 100])
        else:
            filename = "mnist_%d.png" % step
        images = self.generator.predict(noise)
    else:
        i = np.random.randint(0, self.x_train.shape[0], samples)
        images = self.x_train[i, :, :, :]

    plt.figure(figsize=(10,10))
    for i in range(images.shape[0]):
        plt.subplot(4, 4, i+1)
        image = images[i, :, :, :]
        image = np.reshape(image, [self.img_rows, self.img_cols])
        plt.imshow(image, cmap='gray')
        plt.axis('off')
    plt.tight_layout()
    if save2file:
        plt.savefig(filename)
        plt.close('all')
    else:
        plt.show()

MNIST DCGAN 的实际应用

现在我们已经定义了MNIST_DCGAN调用,让我们调用它并开始训练过程。我们将使用 256 的批次大小训练 10,000 个 epochs:

# Initialize MNIST_DCGAN and train
mnist_dcgan = MNIST_DCGAN(X_train_keras)
timer = ElapsedTimer()
mnist_dcgan.train(train_steps=10000, batch_size=256, save_interval=500)

下面的代码显示了判别器和对抗模型的损失和准确率:

0:  [D loss: 0.692640, acc: 0.527344] [A loss: 1.297974, acc: 0.000000]
1:  [D loss: 0.651119, acc: 0.500000] [A loss: 0.920461, acc: 0.000000]
2:  [D loss: 0.735192, acc: 0.500000] [A loss: 1.289153, acc: 0.000000]
3:  [D loss: 0.556142, acc: 0.947266] [A loss: 1.218020, acc: 0.000000]
4:  [D loss: 0.492492, acc: 0.994141] [A loss: 1.306247, acc: 0.000000]
5:  [D loss: 0.491894, acc: 0.916016] [A loss: 1.722399, acc: 0.000000]
6:  [D loss: 0.607124, acc: 0.527344] [A loss: 1.698651, acc: 0.000000]
7:  [D loss: 0.578594, acc: 0.921875] [A loss: 1.042844, acc: 0.000000]
8:  [D loss: 0.509973, acc: 0.587891] [A loss: 1.957741, acc: 0.000000]
9:  [D loss: 0.538314, acc: 0.896484] [A loss: 1.133667, acc: 0.000000]
10: [D loss: 0.510218, acc: 0.572266] [A loss: 1.855000, acc: 0.000000]
11: [D loss: 0.501239, acc: 0.923828] [A loss: 1.098140, acc: 0.000000]
12: [D loss: 0.509211, acc: 0.519531] [A loss: 1.911793, acc: 0.000000]
13: [D loss: 0.482305, acc: 0.923828] [A loss: 1.187290, acc: 0.000000]
14: [D loss: 0.395886, acc: 0.900391] [A loss: 1.465053, acc: 0.000000]
15: [D loss: 0.346876, acc: 0.992188] [A loss: 1.443823, acc: 0.000000]

判别器的初始损失波动很大,但始终保持在 0.50 以上。换句话说,判别器最初非常擅长捕捉生成器生成的低质量赝品。随着生成器变得越来越擅长创建赝品,判别器开始困难;其准确率接近 0.50:

9985: [D loss: 0.696480, acc: 0.521484] [A loss: 0.955954, acc: 0.125000]
9986: [D loss: 0.716583, acc: 0.472656] [A loss: 0.761385, acc: 0.363281]
9987: [D loss: 0.710941, acc: 0.533203] [A loss: 0.981265, acc: 0.074219]
9988: [D loss: 0.703731, acc: 0.515625] [A loss: 0.679451, acc: 0.558594]
9989: [D loss: 0.722460, acc: 0.492188] [A loss: 0.899768, acc: 0.125000]
9990: [D loss: 0.691914, acc: 0.539062] [A loss: 0.726867, acc: 0.464844]
9991: [D loss: 0.716197, acc: 0.500000] [A loss: 0.932500, acc: 0.144531]
9992: [D loss: 0.689704, acc: 0.548828] [A loss: 0.734389, acc: 0.414062]
9993: [D loss: 0.714405, acc: 0.517578] [A loss: 0.850408, acc: 0.218750]
9994: [D loss: 0.690414, acc: 0.550781] [A loss: 0.766320, acc: 0.355469]
9995: [D loss: 0.709792, acc: 0.511719] [A loss: 0.960070, acc: 0.105469]
9996: [D loss: 0.695851, acc: 0.500000] [A loss: 0.774395, acc: 0.324219]
9997: [D loss: 0.712254, acc: 0.521484] [A loss: 0.853828, acc: 0.183594]
9998: [D loss: 0.702689, acc: 0.529297] [A loss: 0.802785, acc: 0.308594]
9999: [D loss: 0.698032, acc: 0.517578] [A loss: 0.810278, acc: 0.304688]

合成图像生成

现在 MNIST DCGAN 已经训练完毕,让我们使用它生成一些合成图像的样本(图 12-3)。

MNIST DCGAN 生成的合成图像

图 12-3. MNIST DCGAN 生成的合成图像

这些合成图像——虽然不能完全与真实的 MNIST 数据集区分开来——与真实数字非常相似。随着训练时间的增加,MNIST DCGAN 应该能够生成更接近真实 MNIST 数据集的合成图像,并可用于扩充该数据集的规模。

虽然我们的解决方案相对不错,但有许多方法可以使 MNIST DCGAN 表现更好。论文"Improved Techniques for Training GANs"和其附带的代码深入探讨了改进 GAN 性能的更高级方法。

结论

在本章中,我们探讨了深度卷积生成对抗网络(DCGAN),这是一种专门用于图像和计算机视觉数据集的生成对抗网络形式。

GAN 是一种具有两个神经网络的生成模型,它们被锁定在一个零和博弈中。其中一个网络是生成器(即伪造者),从真实数据中生成合成数据,而另一个网络是判别器(即警察),负责判断伪造品是真实还是假的。⁶ 生成器从判别器中学习的这种零和博弈导致一个总体上生成相当逼真的合成数据的生成模型,并且通常随着训练时间的增加而变得更好。

GAN(生成对抗网络)相对较新 —— 首次由 Ian Goodfellow 等人于 2014 年提出。⁷ GAN 目前主要用于异常检测和生成合成数据,但在不久的将来可能有许多其他应用。机器学习社区仅仅开始探索其可能性,如果你决定在应用的机器学习系统中使用 GAN,一定要做好大量实验的准备。⁸

在第十三章 中,我们将通过探索时间聚类来结束本书的这一部分内容,这是一种用于处理时间序列数据的无监督学习方法。

¹ 想深入了解 DCGANs,可以参考该主题的官方论文

² 想了解更多关于卷积层的内容,可以阅读《深度学习中不同类型卷积的介绍》一文

³ 想了解更多关于卷积层的内容,可以查看《深度学习中不同类型卷积的介绍》一文,这篇文章也在本章中有提及。

⁴ 想获取原始代码基础,请访问Rowel Atienza 的 GitHub 页面

LeakyReLUhttps://keras.io/layers/advanced-activations/)是一种先进的激活函数,类似于普通的 ReLU,但在单元不活跃时允许一个小的梯度。它正在成为图像机器学习问题中首选的激活函数。

⁶ 想获取更多信息,请查阅OpenAI 博客上的生成模型文章

⁷ 想了解更多相关内容,请参阅这篇重要的论文

⁸ 阅读这篇关于如何优化 GANs提升性能的文章,可以了解一些技巧和窍门。

第十三章:时间序列聚类

到目前为止,在本书中,我们主要处理横断面数据,即我们在单个时间点上观察实体的数据。这包括信用卡数据集,记录了两天内的交易,以及 MNIST 数据集,其中包含数字图像。对于这些数据集,我们应用了无监督学习来学习数据的潜在结构,并将相似的交易和图像分组在一起,而不使用任何标签。

无监督学习对处理时间序列数据也非常有价值,其中我们在不同时间间隔内观察单个实体。我们需要开发一种能够跨时间学习数据的潜在结构的解决方案,而不仅仅是针对特定时间点。如果我们开发了这样的解决方案,我们就可以识别出类似的时间序列模式并将它们分组在一起。

这在金融、医学、机器人学、天文学、生物学、气象学等领域具有非常大的影响,因为这些领域的专业人员花费大量时间分析数据,根据当前事件与过去事件的相似性来分类当前事件。通过将当前事件与类似的过去事件分组在一起,这些专业人员能够更自信地决定采取正确的行动。

在本章中,我们将根据模式相似性对时间序列数据进行聚类。时间序列数据的聚类是一种纯无监督方法,不需要对数据进行训练注释,尽管对于验证结果,像所有其他无监督学习实验一样,需要注释数据。

注记

还有一种数据组合,结合了横断面和时间序列数据。这被称为面板纵向数据。

ECG 数据

为了使时间序列聚类问题更具体化,让我们引入一个特定的现实世界问题。想象一下,我们在医疗保健领域工作,需要分析心电图(EKG/ECG)读数。ECG 机器使用放置在皮肤上的电极,在一段时间内记录心脏的电活动。ECG 在大约 10 秒钟内测量活动,并记录的指标有助于检测任何心脏问题。

大多数 ECG 读数记录的是正常的心跳活动,但异常读数是医疗专业人员必须识别的,以在任何不良心脏事件(如心脏骤停)发生之前采取预防性措施。ECG 产生带有峰和谷的折线图,因此将读数分类为正常或异常是一项简单的模式识别任务,非常适合机器学习。

现实世界的 ECG 读数并不是如此清晰显示,这使得将图像分类到这些不同桶中变得困难且容易出错。

例如,波的振幅变化(中心线到峰值或谷值的高度)、周期(从一个峰值到下一个的距离)、相位移(水平移动)和垂直移都是任何机器驱动分类系统的挑战。

时间序列聚类方法

任何时间序列聚类方法都需要处理这些类型的扭曲。正如您可能记得的那样,聚类依赖于距离度量,以确定数据在空间中与其他数据的接近程度,从而将相似的数据组合成不同且同质的簇。

时间序列数据的聚类工作方式类似,但我们需要一个距离度量,该度量是尺度和位移不变的,以便将类似的时间序列数据组合在一起,而不考虑幅度、周期、相位移和垂直移的微小差异。

k-Shape

满足这一标准的时间序列聚类的先进方法之一是k-shape,它由 John Paparrizos 和 Luis Gravano 于 2015 年首次在 ACM SIGMOD 上介绍¹。

k-shape 使用一种距离度量,该度量对缩放和位移不变,以保持比较时间序列序列的形状。具体来说,k-shape 使用标准化的交叉相关来计算簇质心,并在每次迭代中更新时间序列分配到这些簇。

除了对缩放和位移不变之外,k-shape 还是领域无关且可扩展的,需要最少的参数调整。其迭代改进过程在序列数量上线性扩展。这些特性使其成为当今最强大的时间序列聚类算法之一。

到这一点,应该清楚k-shape 的运行方式与k-means 类似:两种算法都使用迭代方法根据数据与最近群组的质心之间的距离来分配数据。关键的区别在于k-shape 计算距离的方式——它使用基于形状的距离,依赖于交叉相关性。

使用k-shape对 ECGFiveDays 进行时间序列聚类

让我们使用k-shape 构建一个时间序列聚类模型。

在本章中,我们将依赖于 UCR 时间序列收集的数据。由于文件大小超过一百兆字节,在 GitHub 上无法访问。您需要从UCR 时间序列网站下载这些文件。

这是最大的公共类标记时间序列数据集收藏,总计有 85 个。这些数据集来自多个领域,因此我们可以测试我们的解决方案在不同领域的表现。每个时间序列只属于一个类别,因此我们也有标签来验证时间序列聚类的结果。

数据准备

让我们开始加载必要的库:

'''Main'''
import numpy as np
import pandas as pd
import os, time, re
import pickle, gzip, datetime
from os import listdir, walk
from os.path import isfile, join

'''Data Viz'''
import matplotlib.pyplot as plt
import seaborn as sns
color = sns.color_palette()
import matplotlib as mpl
from mpl_toolkits.axes_grid1 import Grid

%matplotlib inline

'''Data Prep and Model Evaluation'''
from sklearn import preprocessing as pp
from sklearn.model_selection import train_test_split
from sklearn.model_selection import StratifiedKFold
from sklearn.metrics import log_loss, accuracy_score
from sklearn.metrics import precision_recall_curve, average_precision_score
from sklearn.metrics import roc_curve, auc, roc_auc_score, mean_squared_error
from keras.utils import to_categorical
from sklearn.metrics import adjusted_rand_score
import random

'''Algos'''
from kshape.core import kshape, zscore
import tslearn
from tslearn.utils import to_time_series_dataset
from tslearn.clustering import KShape, TimeSeriesScalerMeanVariance
from tslearn.clustering import TimeSeriesKMeans
import hdbscan

'''TensorFlow and Keras'''
import tensorflow as tf
import keras
from keras import backend as K
from keras.models import Sequential, Model
from keras.layers import Activation, Dense, Dropout, Flatten, Conv2D, MaxPool2D
from keras.layers import LeakyReLU, Reshape, UpSampling2D, Conv2DTranspose
from keras.layers import BatchNormalization, Input, Lambda
from keras.layers import Embedding, Flatten, dot
from keras import regularizers
from keras.losses import mse, binary_crossentropy
from IPython.display import SVG
from keras.utils.vis_utils import model_to_dot
from keras.optimizers import Adam, RMSprop
from tensorflow.examples.tutorials.mnist import input_data

我们将使用 tslearn 包来访问基于 Python 的 k-shape 算法。tslearn 的框架与 Scikit-learn 类似,但专门用于处理时间序列数据。

接下来,让我们从 UCR 时间序列存档下载的 ECGFiveDays 数据集中加载训练和测试数据。此矩阵的第一列是类别标签,其余列是时间序列数据的值。我们将数据存储为 X_trainy_trainX_testy_test

# Load the datasets
current_path = os.getcwd()
file = '\\datasets\\ucr_time_series_data\\'
data_train = np.loadtxt(current_path+file+
                        "ECGFiveDays/ECGFiveDays_TRAIN",
                        delimiter=",")
X_train = to_time_series_dataset(data_train[:, 1:])
y_train = data_train[:, 0].astype(np.int)

data_test = np.loadtxt(current_path+file+
                       "ECGFiveDays/ECGFiveDays_TEST",
                       delimiter=",")
X_test = to_time_series_dataset(data_test[:, 1:])
y_test = data_test[:, 0].astype(np.int)

下面的代码显示了时间序列的数量、唯一类别的数量以及每个时间序列的长度:

# Basic summary statistics
print("Number of time series:", len(data_train))
print("Number of unique classes:", len(np.unique(data_train[:,0])))
print("Time series length:", len(data_train[0,1:]))
Number of time series: 23
Number of unique classes: 2
Time series length: 136

有 23 个时间序列和 2 个唯一类别,每个时间序列长度为 136. Figure 13-1 显示了每个类别的几个示例;现在我们知道这些 ECG 读数是什么样的了:

# Examples of Class 1.0
for i in range(0,10):
    if data_train[i,0]==1.0:
        print("Plot ",i," Class ",data_train[i,0])
        plt.plot(data_train[i])
        plt.show()

ECG Five Days Class 1.0 - First Two Examples

图 13-1. ECGFiveDays 类 1.0—第一组示例

ECG Five Days Class 1.0 - Second Two Examples

图 13-2. ECGFiveDays 类 1.0—第二组示例

这里是绘制 Class 2.0 结果的代码:

# Examples of Class 2.0
for i in range(0,10):
    if data_train[i,0]==2.0:
        print("Plot ",i," Class ",data_train[i,0])
        plt.plot(data_train[i])
        plt.show()

ECG Five Days Class 2.0 - First Two Examples

图 13-3. ECGFiveDays 类 2.0—第一组示例

ECG Five Days Class 2.0 - Second Two Examples

图 13-4. ECGFiveDays 类 2.0—第二组示例

对于未经训练的肉眼来说,来自类 1.0 和类 2.0 的示例看起来无法区分,但这些观察结果已由领域专家注释。这些图表因噪声和失真而复杂。振幅、周期、相移和垂直移位的差异也使得分类成为一项挑战。

让我们准备 k-shape 算法的数据。我们将对数据进行归一化,使其均值为零,标准差为一:

# Prepare the data - Scale
X_train = TimeSeriesScalerMeanVariance(mu=0., std=1.).fit_transform(X_train)
X_test = TimeSeriesScalerMeanVariance(mu=0., std=1.).fit_transform(X_test)

训练和评估

接下来,我们将调用 k-shape 算法,并将集群数量设置为 2,最大迭代次数设置为一百,训练轮数设置为一百:²

# Train using k-Shape
ks = KShape(n_clusters=2, max_iter=100, n_init=100,verbose=0)
ks.fit(X_train)

为了衡量时间序列聚类的好坏,我们将使用 adjusted Rand index,这是一种校正后的元素随机分组机制相似度测量方法。这与准确度测量相关。³

直观地,兰德指数衡量了预测聚类和真实聚类分配之间的一致性。如果模型的调整兰德指数接近 0.0,则表示纯随机分配聚类;如果模型的调整兰德指数接近 1.0,则表示预测聚类完全与真实聚类匹配。

我们将使用 Scikit-learn 中的调整兰德指数实现,称为 adjusted_rand_score

让我们生成聚类预测,然后计算调整兰德指数:

# Make predictions and calculate adjusted Rand index
preds = ks.predict(X_train)
ars = adjusted_rand_score(data_train[:,0],preds)
print("Adjusted Rand Index:", ars)

根据此次运行,调整兰德指数为 0.668. 如果您多次进行此训练和预测,您会注意到调整兰德指数会有所变化,但始终保持在 0.0 以上:

Adjusted Rand Index: 0.668041237113402

让我们在测试集上进行预测,并计算其调整兰德指数:

# Make predictions on test set and calculate adjusted Rand index
preds_test = ks.predict(X_test)
ars = adjusted_rand_score(data_test[:,0],preds_test)
print("Adjusted Rand Index on Test Set:", ars)

测试集上的调整兰德指数明显较低,勉强超过 0. 聚类预测几乎是随机分配——时间序列基于相似性进行分组,但成功率很低:

Adjusted Rand Index on Test Set: 0.0006332050676187496

如果我们有一个更大的训练集来训练基于k-shape 的时间序列聚类模型,我们预计在测试集上会有更好的表现。

使用 ECG5000 进行时间序列聚类

不使用仅有 23 个观测值的ECGFiveDays数据集,而是使用一个更大的心电图读数数据集。ECG5000数据集(也可以在 UCR 时间序列存档中找到),总共有五千个心电图读数(即时间序列),分布在训练集和测试集中。

数据准备

我们将加载数据集并进行自定义的训练集和测试集划分,其中 80%的五千个读数在自定义训练集中,剩余的 20%在自定义测试集中。有了这个更大的训练集,我们应该能够开发出一个时间序列聚类模型,其在训练集和测试集上都有更好的性能:

# Load the datasets
current_path = os.getcwd()
file = '\\datasets\\ucr_time_series_data\\'
data_train = np.loadtxt(current_path+file+
                        "ECG5000/ECG5000_TRAIN",
                        delimiter=",")

data_test = np.loadtxt(current_path+file+
                       "ECG5000/ECG5000_TEST",
                       delimiter=",")

data_joined = np.concatenate((data_train,data_test),axis=0)
data_train, data_test = train_test_split(data_joined,
                                    test_size=0.20, random_state=2019)

X_train = to_time_series_dataset(data_train[:, 1:])
y_train = data_train[:, 0].astype(np.int)
X_test = to_time_series_dataset(data_test[:, 1:])
y_test = data_test[:, 0].astype(np.int)

让我们探索一下这个数据集:

# Summary statistics
print("Number of time series:", len(data_train))
print("Number of unique classes:", len(np.unique(data_train[:,0])))
print("Time series length:", len(data_train[0,1:]))

下面的代码显示了基本的摘要统计信息。在训练集中有四千个读数,分为五个不同的类别,每个时间序列的长度为 140:

Number of time series: 4000
Number of unique classes: 5
Time series length: 140

让我们也考虑一下这些类别的读数数量。

# Calculate number of readings per class
print("Number of time series in class 1.0:",
      len(data_train[data_train[:,0]==1.0]))
print("Number of time series in class 2.0:",
      len(data_train[data_train[:,0]==2.0]))
print("Number of time series in class 3.0:",
      len(data_train[data_train[:,0]==3.0]))
print("Number of time series in class 4.0:",
      len(data_train[data_train[:,0]==4.0]))
print("Number of time series in class 5.0:",
      len(data_train[data_train[:,0]==5.0]))

分布显示在图 13-5 中。大多数读数属于第一类,其次是第二类。第三、第四和第五类的读数显著较少。

让我们取每个类别的平均时间序列读数,以更好地了解各类别的外观。

# Display readings from each class
for j in np.unique(data_train[:,0]):
    dataPlot = data_train[data_train[:,0]==j]
    cnt = len(dataPlot)
    dataPlot = dataPlot[:,1:].mean(axis=0)
    print(" Class ",j," Count ",cnt)
    plt.plot(dataPlot)
    plt.show()

第一类(图 13-5)有一个明显的低谷,随后是一个尖锐的峰值和稳定期。这是最常见的读数类型。

ECG 5000 第一类 1.0

图 13-5. ECG5000 第一类 1.0

第二类(图 13-6)有一个明显的低谷,随后恢复,然后是一个更加尖锐和更低的低谷,并带有部分恢复。这是第二常见的读数类型。

ECG 5000 第二类 2.0

图 13-6. ECG5000 第二类 2.0

第三类(图 13-7)有一个明显的低谷,随后恢复,然后是一个更加尖锐和更低的低谷,并没有恢复。数据集中有一些这样的例子。

ECG 5000 第三类 3.0

图 13-7. ECG5000 第三类 3.0

第四类(图 13-8)有一个明显的低谷,随后恢复,然后是一个较浅的低谷和稳定。数据集中有一些这样的例子。

ECG 5000 Class 4.0

图 13-8. ECG5000 类别 4.0

第 5 类(图 13-9)有一个明显的低谷,然后是不均匀的恢复,一个峰值,然后是不稳定的下降到一个浅低谷。数据集中这样的例子很少。

ECG 5000 Class 5.0

图 13-9. ECG5000 类别 5.0

训练和评估

如前所述,让我们将数据归一化,使其均值为零,标准差为一。然后,我们将使用k-shape 算法,这次将聚类数设为五。其余保持不变:

# Prepare data - Scale
X_train = TimeSeriesScalerMeanVariance(mu=0., std=1.).fit_transform(X_train)
X_test = TimeSeriesScalerMeanVariance(mu=0., std=1.).fit_transform(X_test)

# Train using k-Shape
ks = KShape(n_clusters=5, max_iter=100, n_init=10,verbose=1,random_state=2019)
ks.fit(X_train)

让我们评估训练集上的结果:

# Predict on train set and calculate adjusted Rand index
preds = ks.predict(X_train)
ars = adjusted_rand_score(data_train[:,0],preds)
print("Adjusted Rand Index on Training Set:", ars)

以下代码显示了训练集上的调整兰德指数。这一指数在 0.75 处显著增强:

Adjusted Rand Index on Training Set: 0.7499312374127193

让我们也在测试集上评估结果:

# Predict on test set and calculate adjusted Rand index
preds_test = ks.predict(X_test)
ars = adjusted_rand_score(data_test[:,0],preds_test)
print("Adjusted Rand Index on Test Set:", ars)

测试集上的调整兰德指数也高得多,为 0.72:

Adjusted Rand Index on Test Set: 0.7172302400677499

将训练集增加到四千个时间序列(从 23 个),我们得到了一个表现更好的时间序列聚类模型。

让我们进一步探索预测聚类,以查看它们的同质性。对于每个预测聚类,我们将评估真实标签的分布。如果聚类定义明确且同质,每个聚类中的大多数读数应具有相同的真实标签:

# Evaluate goodness of the clusters
preds_test = preds_test.reshape(1000,1)
preds_test = np.hstack((preds_test,data_test[:,0].reshape(1000,1)))
preds_test = pd.DataFrame(data=preds_test)
preds_test = preds_test.rename(columns={0: 'prediction', 1: 'actual'})

counter = 0
for i in np.sort(preds_test.prediction.unique()):
    print("Predicted Cluster ", i)
    print(preds_test.actual[preds_test.prediction==i].value_counts())
    print()
    cnt = preds_test.actual[preds_test.prediction==i] \
                        .value_counts().iloc[1:].sum()
    counter = counter + cnt
print("Count of Non-Primary Points: ", counter)

以下代码显示了聚类的同质性:

ECG 5000 k-shape predicted cluster analysis

Predicted Cluster 0.0
    2.0   29
    4.0   2
    1.0   2
    3.0   2
    5.0   1
    Name: actual, dtype: int64

Predicted Cluster 1.0
    2.0   270
    4.0   14
    3.0   8
    1.0   2
    5.0   1
    Name: actual, dtype: int64

Predicted Cluster 2.0
    1.0   553
    4.0   16
    2.0   9
    3.0   7
    Name: actual, dtype: int64

Predicted Cluster 3.0
    2.0   35
    1.0   5
    4.0   5
    5.0   3
    3.0   3
    Name: actual, dtype: int64

Predicted Cluster 4.0
    1.0   30
    4.0   1
    3.0   1
    2.0   1
    Name: actual, dtype: int64

Count of Non-Primary Points: 83

每个预测聚类中的大多数读数属于同一个真实标签类。这突显了k-shape 衍生聚类的定义明确和同质性。

使用k-means 对 ECG5000 进行时间序列聚类

为了完整起见,让我们将k-shape 与k-means 的结果进行比较。我们将使用tslearn库进行训练,并像之前一样使用调整兰德指数进行评估。

我们将聚类数设为五,单次运行的最大迭代次数为一百,独立运行次数为一百,距离度量为欧氏距离,随机状态为 2019:

# Train using Time Series k-Means
km = TimeSeriesKMeans(n_clusters=5, max_iter=100, n_init=100, \
                      metric="euclidean", verbose=1, random_state=2019)
km.fit(X_train)

# Predict on training set and evaluate using adjusted Rand index
preds = km.predict(X_train)
ars = adjusted_rand_score(data_train[:,0],preds)
print("Adjusted Rand Index on Training Set:", ars)

# Predict on test set and evaluate using adjusted Rand index
preds_test = km.predict(X_test)
ars = adjusted_rand_score(data_test[:,0],preds_test)
print("Adjusted Rand Index on Test Set:", ars)

TimeSeriesKMean算法甚至比使用欧氏距离度量的k-shape 算法更快。但结果并不如k-shape 那么好:

Adjusted Rand Index of Time Series k-Means on Training Set: 0.5063464656715959

训练集上的调整兰德指数为 0.506:

Adjusted Rand Index of Time Series k-Means on Test Set: 0.4864981997585834

测试集上的调整兰德指数为 0.486。

使用层次 DBSCAN 对 ECG5000 进行时间序列聚类

最后,让我们应用层次 DBSCAN,这是本书前面探讨过的方法,并评估其性能。

我们将使用默认参数运行HDBSCAN,并使用调整兰德指数评估其性能:

# Train model and evaluate on training set
min_cluster_size = 5
min_samples = None
alpha = 1.0
cluster_selection_method = 'eom'
prediction_data = True

hdb = hdbscan.HDBSCAN(min_cluster_size=min_cluster_size, \
                      min_samples=min_samples, alpha=alpha, \
                      cluster_selection_method=cluster_selection_method, \
                      prediction_data=prediction_data)

preds = hdb.fit_predict(X_train.reshape(4000,140))
ars = adjusted_rand_score(data_train[:,0],preds)
print("Adjusted Rand Index on Training Set:", ars)

训练集上的调整兰德指数令人印象深刻,为 0.769:

Adjusted Rand Index on Training Set using HDBSCAN: 0.7689563655060421

训练集上的调整兰德指数令人印象深刻,为 0.769。

让我们在测试集上评估:

# Predict on test set and evaluate
preds_test = hdbscan.prediction.approximate_predict( \
                hdb, X_test.reshape(1000,140))
ars = adjusted_rand_score(data_test[:,0],preds_test[0])
print("Adjusted Rand Index on Test Set:", ars)

训练集上的调整兰德指数同样令人印象深刻,为 0.720:

Adjusted Rand Index on Test Set using HDBSCAN: 0.7200816245545564

比较时间序列聚类算法

HDBSCAN 和 k-shape 在 ECG5000 数据集上表现相似,而 k-means 的表现较差。然而,仅通过评估这三种聚类算法在单个时间序列数据集上的表现,我们无法得出强有力的结论。

让我们运行一个更大的实验,看看这三种聚类算法在彼此之间的表现如何。

首先,我们将加载 UCR 时间序列分类文件夹中的所有目录和文件,以便在实验期间对它们进行迭代。总共有 85 个数据集:

# Load the datasets
current_path = os.getcwd()
file = '\\datasets\\ucr_time_series_data\\'

mypath = current_path + file
d = []
f = []
for (dirpath, dirnames, filenames) in walk(mypath):
    for i in dirnames:
        newpath = mypath+"\\"+i+"\\"
        onlyfiles = [f for f in listdir(newpath) if isfile(join(newpath, f))]
        f.extend(onlyfiles)
    d.extend(dirnames)
    break

接下来,让我们为三种聚类算法中的每一种重复使用代码,并使用我们刚刚准备的数据集列表来运行完整实验。我们将按数据集存储训练和测试的调整后兰德指数,并测量每种聚类算法完成 85 个数据集的整个实验所需的时间。

k-Shape 的完整运行

第一个实验使用了 k-shape。

# k-Shape Experiment
kShapeDF = pd.DataFrame(data=[],index=[v for v in d],
                        columns=["Train ARS","Test ARS"])

# Train and Evaluate k-Shape
class ElapsedTimer(object):
    def __init__(self):
        self.start_time = time.time()
    def elapsed(self,sec):
        if sec < 60:
            return str(sec) + " sec"
        elif sec < (60 * 60):
            return str(sec / 60) + " min"
        else:
            return str(sec / (60 * 60)) + " hr"
    def elapsed_time(self):
        print("Elapsed: %s " % self.elapsed(time.time() - self.start_time))
        return (time.time() - self.start_time)

timer = ElapsedTimer()
cnt = 0
for i in d:
    cnt += 1
    print("Dataset ", cnt)
    newpath = mypath+"\\"+i+"\\"
    onlyfiles = [f for f in listdir(newpath) if isfile(join(newpath, f))]
    j = onlyfiles[0]
    k = onlyfiles[1]
    data_train = np.loadtxt(newpath+j, delimiter=",")
    data_test = np.loadtxt(newpath+k, delimiter=",")

    data_joined = np.concatenate((data_train,data_test),axis=0)
    data_train, data_test = train_test_split(data_joined,
                                        test_size=0.20, random_state=2019)

    X_train = to_time_series_dataset(data_train[:, 1:])
    y_train = data_train[:, 0].astype(np.int)
    X_test = to_time_series_dataset(data_test[:, 1:])
    y_test = data_test[:, 0].astype(np.int)

    X_train = TimeSeriesScalerMeanVariance(mu=0., std=1.) \
                                .fit_transform(X_train)
    X_test = TimeSeriesScalerMeanVariance(mu=0., std=1.) \
                                .fit_transform(X_test)

    classes = len(np.unique(data_train[:,0]))
    ks = KShape(n_clusters=classes, max_iter=10, n_init=3,verbose=0)
    ks.fit(X_train)

    print(i)
    preds = ks.predict(X_train)
    ars = adjusted_rand_score(data_train[:,0],preds)
    print("Adjusted Rand Index on Training Set:", ars)
    kShapeDF.loc[i,"Train ARS"] = ars

    preds_test = ks.predict(X_test)
    ars = adjusted_rand_score(data_test[:,0],preds_test)
    print("Adjusted Rand Index on Test Set:", ars)
    kShapeDF.loc[i,"Test ARS"] = ars

kShapeTime = timer.elapsed_time()

k-shape 算法大约需要一个小时的运行时间。我们已经存储了调整后的兰德指数,并将用这些指数来比较 k-shape 和 k-means 以及 HDBSCAN 的表现。

注意

我们对 k-shape 的测量时间基于我们设置的实验超参数以及机器的本地硬件规格。不同的超参数和硬件规格可能导致实验时间显著不同。

k-Means 的完整运行

接下来是 k-means:

# k-Means Experiment - FULL RUN
# Create dataframe
kMeansDF = pd.DataFrame(data=[],index=[v for v in d], \
                        columns=["Train ARS","Test ARS"])

# Train and Evaluate k-Means
timer = ElapsedTimer()
cnt = 0
for i in d:
    cnt += 1
    print("Dataset ", cnt)
    newpath = mypath+"\\"+i+"\\"
    onlyfiles = [f for f in listdir(newpath) if isfile(join(newpath, f))]
    j = onlyfiles[0]
    k = onlyfiles[1]
    data_train = np.loadtxt(newpath+j, delimiter=",")
    data_test = np.loadtxt(newpath+k, delimiter=",")

    data_joined = np.concatenate((data_train,data_test),axis=0)
    data_train, data_test = train_test_split(data_joined, \
                                        test_size=0.20, random_state=2019)

    X_train = to_time_series_dataset(data_train[:, 1:])
    y_train = data_train[:, 0].astype(np.int)
    X_test = to_time_series_dataset(data_test[:, 1:])
    y_test = data_test[:, 0].astype(np.int)

    X_train = TimeSeriesScalerMeanVariance(mu=0., std=1.) \
                                    .fit_transform(X_train)
    X_test = TimeSeriesScalerMeanVariance(mu=0., std=1.) \
                                    .fit_transform(X_test)

    classes = len(np.unique(data_train[:,0]))
    km = TimeSeriesKMeans(n_clusters=5, max_iter=10, n_init=10, \
                          metric="euclidean", verbose=0, random_state=2019)
    km.fit(X_train)

    print(i)
    preds = km.predict(X_train)
    ars = adjusted_rand_score(data_train[:,0],preds)
    print("Adjusted Rand Index on Training Set:", ars)
    kMeansDF.loc[i,"Train ARS"] = ars

    preds_test = km.predict(X_test)
    ars = adjusted_rand_score(data_test[:,0],preds_test)
    print("Adjusted Rand Index on Test Set:", ars)
    kMeansDF.loc[i,"Test ARS"] = ars

kMeansTime = timer.elapsed_time()

k-means 在所有 85 个数据集上运行不到五分钟:

HDBSCAN 的完整运行

最后,我们有了 HBDSCAN:

# HDBSCAN Experiment - FULL RUN
# Create dataframe
hdbscanDF = pd.DataFrame(data=[],index=[v for v in d], \
                         columns=["Train ARS","Test ARS"])

# Train and Evaluate HDBSCAN
timer = ElapsedTimer()
cnt = 0
for i in d:
    cnt += 1
    print("Dataset ", cnt)
    newpath = mypath+"\\"+i+"\\"
    onlyfiles = [f for f in listdir(newpath) if isfile(join(newpath, f))]
    j = onlyfiles[0]
    k = onlyfiles[1]
    data_train = np.loadtxt(newpath+j, delimiter=",")
    data_test = np.loadtxt(newpath+k, delimiter=",")

    data_joined = np.concatenate((data_train,data_test),axis=0)
    data_train, data_test = train_test_split(data_joined, \
                                    test_size=0.20, random_state=2019)

    X_train = data_train[:, 1:]
    y_train = data_train[:, 0].astype(np.int)
    X_test = data_test[:, 1:]
    y_test = data_test[:, 0].astype(np.int)

    X_train = TimeSeriesScalerMeanVariance(mu=0., std=1.) \
                                    .fit_transform(X_train)
    X_test = TimeSeriesScalerMeanVariance(mu=0., std=1.)  \
                                    .fit_transform(X_test)

    classes = len(np.unique(data_train[:,0]))
    min_cluster_size = 5
    min_samples = None
    alpha = 1.0
    cluster_selection_method = 'eom'
    prediction_data = True

    hdb = hdbscan.HDBSCAN(min_cluster_size=min_cluster_size, \
                          min_samples=min_samples, alpha=alpha, \
                          cluster_selection_method= \
                              cluster_selection_method, \
                          prediction_data=prediction_data)

    print(i)
    preds = hdb.fit_predict(X_train.reshape(X_train.shape[0], \
                                            X_train.shape[1]))
    ars = adjusted_rand_score(data_train[:,0],preds)
    print("Adjusted Rand Index on Training Set:", ars)
    hdbscanDF.loc[i,"Train ARS"] = ars

    preds_test = hdbscan.prediction.approximate_predict(hdb,
                            X_test.reshape(X_test.shape[0], \
                                           X_test.shape[1]))
    ars = adjusted_rand_score(data_test[:,0],preds_test[0])
    print("Adjusted Rand Index on Test Set:", ars)
    hdbscanDF.loc[i,"Test ARS"] = ars

hdbscanTime = timer.elapsed_time()

HBDSCAN 在所有 85 个数据集上运行不到 10 分钟。

比较所有三种时间序列聚类方法

现在让我们比较这三种聚类算法,看看哪一种表现最佳。一种方法是分别计算每种聚类算法在训练集和测试集上的平均调整兰德指数。

每种算法的得分如下:

k-Shape Results

Train ARS     0.165139
Test ARS      0.151103
k-Means Results

Train ARS     0.184789
Test ARS      0.178960
HDBSCAN Results

Train ARS     0.178754
Test ARS 0.158238

结果相当可比,k-means 的兰德指数最高,紧随其后的是 k-shape 和 HDBSCAN。

为了验证这些发现,让我们统计每种算法在所有 85 个数据集中分别获得第一、第二或第三名的次数:

# Count top place finishes
timeSeriesClusteringDF = pd.DataFrame(data=[],index=kShapeDF.index, \
                            columns=["kShapeTest", \
                                    "kMeansTest", \
                                    "hdbscanTest"])

timeSeriesClusteringDF.kShapeTest = kShapeDF["Test ARS"]
timeSeriesClusteringDF.kMeansTest = kMeansDF["Test ARS"]
timeSeriesClusteringDF.hdbscanTest = hdbscanDF["Test ARS"]

tscResults = timeSeriesClusteringDF.copy()

for i in range(0,len(tscResults)):
    maxValue = tscResults.iloc[i].max()
    tscResults.iloc[i][tscResults.iloc[i]==maxValue]=1
    minValue = tscResults .iloc[i].min()
    tscResults.iloc[i][tscResults.iloc[i]==minValue]=-1
    medianValue = tscResults.iloc[i].median()
    tscResults.iloc[i][tscResults.iloc[i]==medianValue]=0
# Show results
tscResultsDF = pd.DataFrame(data=np.zeros((3,3)), \
                index=["firstPlace","secondPlace","thirdPlace"], \
                columns=["kShape", "kMeans","hdbscan"])
tscResultsDF.loc["firstPlace",:] = tscResults[tscResults==1].count().values
tscResultsDF.loc["secondPlace",:] = tscResults[tscResults==0].count().values
tscResultsDF.loc["thirdPlace",:] = tscResults[tscResults==-1].count().values
tscResultsDF

k-shape 在大多数数据集上获得了最多的第一名,其次是 HDBSCAN。k-means 在大多数数据集上获得了第二名,表现既不是最好的也不是最差的(表 13-1)。

表 13-1. 比较总结

kShapekMeanshbdscan
firstPlace31.024.029.0
secondPlace19.041.026.0
thirdPlace35.020.030.0

根据这些比较,很难得出一个算法能够全面击败其他算法的结论。虽然 k-shape 获得了最多的第一名,但它比另外两种算法慢得多。

此外,k-means 和 HDBSCAN 都表现出色,在大量数据集上获得了第一名。

结论

在本章中,我们首次探索了时间序列数据,并展示了不需要任何标签就能根据相似性对时间序列模式进行分组的无监督学习的强大能力。我们详细讨论了三种聚类算法——k-shape、k-means 和 HDBSCAN。虽然k-shape 今天被认为是最好的选择,但另外两种算法也表现不俗。

最重要的是,我们使用的 85 个时间序列数据集的结果突显了实验的重要性。与大多数机器学习一样,没有单一算法能够胜过所有其他算法。你必须不断扩展你的知识广度并进行实验,以找出哪种方法最适合手头的问题。知道在什么时候应用何种方法是一个优秀数据科学家的标志。

希望通过本书学习到的多种不监督学习方法,能够更好地帮助你解决未来可能遇到的各种问题。

¹ 这篇论文可以在这里公开获取。

² 关于超参数的更多信息,请参考官方k-shape 文档

³ 请查阅维基百科,了解更多关于Rand 指数的信息。

第十四章:结论

人工智能正处于自 20 年前互联网时代以来在科技界所未见的炒作周期中。¹ 然而,这并不意味着炒作没有理由或在某种程度上不合理。

虽然前几十年的人工智能和机器学习工作大多是理论性和学术性质的,并且成功的商业应用寥寥无几,但过去十年在这一领域的工作更加应用化和行业化,由 Google、Facebook、Amazon、Microsoft 和 Apple 等公司主导。

专注于为狭义定义的任务(即弱或狭义 AI)开发机器学习应用,而不是更雄心勃勃的任务(即强 AI 或 AGI),使得这一领域对希望在较短的 7 到 10 年时间内获得良好回报的投资者更具吸引力。投资者的更多关注和资本反过来促使该领域在朝着狭义 AI 的进展以及为强 AI 打下基础方面更加成功。

当然,资本并非唯一的催化剂。大数据的崛起,计算机硬件的进步(尤其是由 Nvidia 主导的 GPU 的崛起,用于训练深度神经网络),以及算法研究和开发的突破都在为人工智能的最近成功做出同样有意义的贡献。

像所有炒作周期一样,当前周期最终可能会带来一些失望,但到目前为止,该领域的进展已经使科学界许多人感到惊讶,并且已经吸引了日益主流的观众的想象力。

监督学习

到目前为止,监督学习已经为机器学习中大多数商业成功负责。这些成功可以按数据类型进行分类:

  • 使用图像,我们有光学字符识别、图像分类和面部识别等技术。例如,Facebook 根据新照片中的面部与之前标记面部的相似度自动标记面部,利用了 Facebook 现有的照片数据库。

  • 使用视频,我们有自动驾驶汽车,这些汽车已经在今天美国各地的道路上运行。像 Google、特斯拉和 Uber 这样的主要参与者已经大量投资到自动驾驶车辆中。

  • 使用语音,我们有语音识别,由诸如 Siri、Alexa、Google 助理和 Cortana 等助手提供支持。

  • 使用文本,我们有电子邮件垃圾邮件过滤的经典示例,还有机器翻译(即 Google 翻译)、情感分析、语法分析、实体识别、语言检测和问答系统。在这些成功的基础上,我们在过去几年见证了聊天机器人的激增。

监督学习在时间序列预测方面也表现出色,这在金融、医疗保健和广告技术等领域有许多应用。当然,监督学习应用并不局限于一次只使用一种数据类型。例如,视频字幕系统将图像识别与自然语言处理相结合,对视频应用机器学习并生成文本字幕。

无监督学习

到目前为止,无监督学习的成功远不及监督学习,但它的潜力是巨大的。大多数世界数据都是未标记的。为了将机器学习规模化应用于比监督学习已经解决的更有野心的任务,我们需要同时处理标记和未标记的数据。

无监督学习非常擅长通过学习未标记数据的底层结构来发现隐藏模式。一旦发现了隐藏模式,无监督学习可以根据相似性将隐藏模式分组,使相似模式归为一组。

一旦以这种方式将模式分组,人们可以对每个组抽样一些模式并提供有意义的标签。如果组别定义良好(即成员是同质的,并且与其他组的成员明显不同),那么人类手动提供的少量标签可以应用于该组的其他(尚未标记的)成员。这个过程导致以前未标记的数据非常快速和高效地标记。

换句话说,无监督学习使监督学习方法得以成功应用。无监督学习与监督学习之间的这种协同作用,也称为半监督学习,可能推动成功的机器学习应用的下一波浪潮。

Scikit-Learn

到目前为止,无监督学习的这些主题应该对你来说已经非常熟悉了。但让我们回顾一下我们到目前为止所涵盖的一切。

在第三章中,我们探讨了如何使用降维算法通过学习底层结构来降低数据的维度,仅保留最显著的特征,并将特征映射到较低维度空间。

一旦数据映射到较低维度空间,就能更容易地揭示数据中的隐藏模式。在第四章中,我们通过构建异常检测系统来演示这一点,将正常的信用卡交易与异常的交易分开。

在这个较低维度的空间中,将类似的点分组也更容易;这被称为聚类,在第五章中我们探讨过。聚类的一个成功应用是群组分割,根据它们彼此的相似程度和与其他项的不同程度来分离项目。我们在第六章中对提交贷款申请的借款人执行了此操作。第 3 至 6 章总结了本书中使用 Scikit-Learn 的无监督学习部分。

在第十三章中,我们首次将聚类扩展到时间序列数据,并探索了各种时间序列聚类方法。我们进行了许多实验,并强调了拥有广泛的机器学习方法库的重要性,因为没有一种方法适用于所有数据集。

TensorFlow 和 Keras

第 7 到 12 章探讨了使用 TensorFlow 和 Keras 的无监督学习。

首先,我们介绍了神经网络和表示学习的概念。在第七章中,我们使用自编码器从原始数据中学习了新的更紧凑的表示方式——这是无监督学习从数据中学习底层结构以提取洞见的另一种方式。

在第八章中,我们将自编码器应用于信用卡交易数据集,以构建一个欺诈检测解决方案。而在第九章中,我们将无监督方法与监督方法结合,以改进第八章中基于无监督学习的独立信用卡欺诈检测解决方案,突显了无监督和监督学习模型之间的潜在协同作用。

在第十章中,我们首次介绍了生成模型,从限制玻尔兹曼机开始。我们利用这些模型构建了一个电影推荐系统,这是 Netflix 和 Amazon 等公司使用的推荐系统的一个轻量级版本。

在第十一章中,我们从浅层神经网络转向深层神经网络,并通过堆叠多个限制玻尔兹曼机来构建了一个更先进的生成模型。通过这种所谓的深信度网络,我们生成了 MNIST 数据集的合成图像,以增强现有的图像分类系统。这再次突显了利用无监督学习来改进监督解决方案的潜力。

在第十二章中,我们转向另一类现今流行的生成模型——生成对抗网络。我们利用这些模型生成了更多类似于 MNIST 图像数据集中数字的合成图像。

强化学习

在本书中,我们没有详细介绍强化学习,但这是另一个正在受到越来越多关注的机器学习领域,特别是在棋盘游戏和视频游戏领域取得的最新成功后。

最显著的是,Google DeepMind 几年前向世界介绍了其围棋软件AlphaGo,并在 2016 年 3 月 AlphaGo 历史性地击败了当时的世界冠军李世石,这一壮举被许多人认为是需要 AI 再过一个完整十年才能实现的,这显示了 AI 领域取得的巨大进展。

更近期,Google DeepMind 已将强化学习与无监督学习相结合,开发出了更好的 AlphaGo 软件的版本。这款名为AlphaGo Zero的软件根本不使用人类游戏数据。

从不同机器学习分支的结合中获得的成功案例证实了本书的一个主要主题——机器学习的下一波成功将由发现使用未标记数据来改进现有的依赖于标记数据集的机器学习解决方案的方法来引领。

今天无监督学习最有前景的领域

我们将以无监督学习的现状和可能的未来状态来结束本书。今天,无监督学习在工业界有几个成功的应用程序;在此列表的最上面是异常检测、维度约简、聚类、未标记数据集的高效标记和数据增强。

无监督学习在识别新兴模式方面表现出色,特别是当未来模式看起来与过去模式非常不同时;在某些领域,过去模式的标签对于捕捉未来感兴趣的模式的价值有限。例如,异常检测用于识别各种欺诈行为——信用卡、借记卡、电汇、在线、保险等——以及标记与洗钱、恐怖主义资助和人口贩卖有关的可疑交易。

异常检测也用于网络安全解决方案以识别和阻止网络攻击。基于规则的系统难以捕捉新类型的网络攻击,因此无监督学习已成为该领域的一个基本内容。异常检测还擅长突出显示数据质量问题;通过异常检测,数据分析师可以更有效地找出并解决不良数据捕获问题。

无监督学习还有助于解决机器学习中的一个主要挑战:维度诅咒。数据科学家通常必须选择要在分析数据和构建机器学习模型中使用的特征子集,因为完整的特征集太大了,如果不是棘手的话,会使计算变得困难。无监督学习使数据科学家不仅可以使用原始特征集,还可以在模型构建过程中补充其他特征工程,而无需担心遇到主要的计算挑战。

一旦原始加工过的特征集准备就绪,数据科学家就会应用维度约简来去除冗余特征,并保留最突出、不相关的特征用于分析和模型构建。这种数据压缩也是在监督机器学习系统中的预处理步骤中有用的(特别是在视频和图像中)。

无监督学习还帮助数据科学家和业务人员回答诸如哪些客户行为最不寻常(即与大多数客户非常不同)的问题。这种洞察力来自将相似点聚类在一起,帮助分析师执行群组分割。一旦识别出不同的群体,人类可以探索什么使这些群体特别,并与其他群体有明显不同。从这种练习中获得的洞察力可以应用于更深入地理解业务正在发生的情况并改进企业战略。

聚类使得标记未标记数据变得更加高效。由于类似的数据被分组在一起,人类只需标记每个群组中的少数点。一旦每个群组中的少数点被标记,其他尚未标记的点可以采用已标记点的标签。

最后,生成模型可以生成合成数据来补充现有数据集。我们通过 MNIST 数据集的工作展示了这一点。创建大量新的合成数据——包括图像和文本等多种数据类型——是非常强大的,并且现在才刚开始认真探索这一能力。

无监督学习的未来

我们目前处于人工智能浪潮的早期阶段。当然,迄今为止已经取得了重大成功,但人工智能世界很大程度上建立在炒作和承诺之上。还有许多潜力有待实现。

迄今为止的成功主要集中在由监督学习主导的大多数狭窄定义的任务中。随着当前人工智能浪潮的成熟,希望是从狭义人工智能任务(如图像分类、机器翻译、语音识别、问答机器人)转向更雄心勃勃的强人工智能(能够理解人类语言中的意义并像人类一样自然地对话的聊天机器人,能够理解并在不过度依赖标记数据的情况下操作周围物理世界的机器人,能够展示超人类驾驶性能的自动驾驶汽车,以及能够展示人类级推理和创造力的人工智能)。

许多人认为无监督学习是发展强人工智能的关键。否则,人工智能将受到我们拥有多少标记数据的限制。

人类从出生起在学习执行任务方面擅长于无需许多示例。例如,幼儿仅通过少数示例就能区分猫和狗。今天的人工智能需要更多的示例/标签。理想情况下,人工智能可以学会用尽可能少的标签分离不同类别的图像(即猫与狗),甚至可能只需一个或零个标签。要执行这种一次性或零次性学习将需要在无监督学习领域取得更多进展。

此外,今天大多数人工智能并不具备创造力,它只是基于训练标签来优化模式识别。要构建直观和创造性的人工智能,研究人员需要构建能够理解大量未标记数据的人工智能,以发现甚至人类以前未曾发现的模式。

幸运的是,有一些迹象表明人工智能正在逐渐发展成为更强的类型。

谷歌 DeepMind 的 AlphaGo 软件就是一个典型例子。首个击败人类职业围棋选手(于 2015 年 10 月)的 AlphaGo 版本依赖于过去人类和机器对弈的数据,以及强化学习等机器学习方法(包括能够预测多步并确定哪一步能够显著提高获胜的几率)。

这个版本的 AlphaGo 非常令人印象深刻,它在 2016 年 3 月的首尔高调五番棋系列赛中击败了世界顶级围棋选手李世石。但最新版本的 AlphaGo 更加出色。

原始版的 AlphaGo 依赖于数据和人类专业知识。最新版本的 AlphaGo,称为AlphaGo Zero,纯粹通过自我对弈学习如何玩并获胜围棋。² 换句话说,AlphaGo Zero 不依赖任何人类知识,并且达到了超越人类的表现,以百胜于前一版本的 AlphaGo。³

AlphaGo Zero 从对围棋一无所知开始,在几天内就积累了数千年的人类围棋知识。但它并不止步于此,超越了人类水平的熟练度。AlphaGo Zero 发现了新的知识,并发展了新的非传统的获胜策略。

换句话说,AlphaGo 表现出了创造力。

如果人工智能继续发展,依靠从几乎没有或没有任何先验知识学习(即几乎没有或没有标记的数据),我们将能够开发出具有创造力、推理能力和复杂决策能力的人工智能,这些领域目前还是人类的专利。⁴

最后总结

我们只是初步探索了无监督学习及其潜力,但希望您能更好地理解无监督学习的能力以及它如何应用于您设计的机器学习系统。

至少,您应该对使用无监督学习来发现隐藏模式,获得更深入的商业洞见,检测异常,根据相似性对群组进行聚类,执行自动特征提取以及从未标记数据集生成合成数据集有一个概念性理解和实际操作经验。

人工智能的未来充满了希望。去创造它吧。

¹ 根据PitchBook的数据,2017 年风险投资者在人工智能和机器学习公司投资超过 108 亿美元,这比 2010 年的 5 亿美元增长了一倍多,也比 2016 年的 57 亿美元增长了近一倍。

² “AlphaGo Zero: 从零开始学习”详细介绍了 AlphaGo Zero。

³ 欲了解更多信息,请查阅Nature的文章“不借助人类知识掌握围棋”

⁴ OpenAI 也在应用无监督学习取得了一些显著的成功,用于语言理解,这两者都是强人工智能的重要基石。