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

50 阅读31分钟

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

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

译者:飞龙

协议:CC BY-NC-SA 4.0

第九章:半监督学习

到目前为止,我们将监督学习和无监督学习视为机器学习的两个独立而不同的分支。当我们的数据集有标签时,适合使用监督学习,当数据集没有标签时,需要使用无监督学习。

在现实世界中,区分并不是那么清晰。数据集通常是部分标记的,我们希望在利用标记集中的信息的同时,有效地标记未标记的观察结果。使用监督学习,我们必须丢弃大多数未标记的数据集。使用无监督学习,我们会有大部分数据可供使用,但不知道如何利用我们拥有的少量标记。

半监督学习领域融合了监督学习和无监督学习的优点,利用少量可用标记来揭示数据集的结构并帮助标记其余部分。

在本章中,我们将继续使用信用卡交易数据集来展示半监督学习。

数据准备

像之前一样,让我们加载必要的库并准备数据。现在这应该很熟悉了:

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

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

%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
from sklearn.metrics import precision_recall_curve, average_precision_score
from sklearn.metrics import roc_curve, auc, roc_auc_score

'''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
from keras.layers import BatchNormalization, Input, Lambda
from keras import regularizers
from keras.losses import mse, binary_crossentropy

像之前一样,我们将生成一个训练集和一个测试集。但是我们会从训练集中删除 90%的欺诈信用卡交易,以模拟如何处理部分标记的数据集。

尽管这看起来可能是一个非常激进的举措,但涉及支付欺诈的真实世界问题同样具有很低的欺诈率(每 10,000 例中可能只有 1 例欺诈)。通过从训练集中删除 90%的标签,我们正在模拟这种现象:

# Load the data
current_path = os.getcwd()
file = '\\datasets\\credit_card_data\\credit_card.csv'
data = pd.read_csv(current_path + file)

dataX = data.copy().drop(['Class','Time'],axis=1)
dataY = data['Class'].copy()

# Scale data
featuresToScale = dataX.columns
sX = pp.StandardScaler(copy=True, with_mean=True, with_std=True)
dataX.loc[:,featuresToScale] = sX.fit_transform(dataX[featuresToScale])

# Split into train and test
X_train, X_test, y_train, y_test = \
    train_test_split(dataX, dataY, test_size=0.33, \
                     random_state=2018, stratify=dataY)

# Drop 95% of the labels from the training set
toDrop = y_train[y_train==1].sample(frac=0.90,random_state=2018)
X_train.drop(labels=toDrop.index,inplace=True)
y_train.drop(labels=toDrop.index,inplace=True)

我们还将重用anomalyScoresplotResults函数:

def anomalyScores(originalDF, reducedDF):
    loss = np.sum((np.array(originalDF) - \
                   np.array(reducedDF))**2, axis=1)
    loss = pd.Series(data=loss,index=originalDF.index)
    loss = (loss-np.min(loss))/(np.max(loss)-np.min(loss))
    return loss
def plotResults(trueLabels, anomalyScores, returnPreds = False):
    preds = pd.concat([trueLabels, anomalyScores], axis=1)
    preds.columns = ['trueLabel', 'anomalyScore']
    precision, recall, thresholds = \
        precision_recall_curve(preds['trueLabel'], \
                               preds['anomalyScore'])
    average_precision = average_precision_score( \
                        preds['trueLabel'], preds['anomalyScore'])

    plt.step(recall, precision, color='k', alpha=0.7, where='post')
    plt.fill_between(recall, precision, step='post', alpha=0.3, color='k')

    plt.xlabel('Recall')
    plt.ylabel('Precision')
    plt.ylim([0.0, 1.05])
    plt.xlim([0.0, 1.0])

    plt.title('Precision-Recall curve: Average Precision = \
 {0:0.2f}'.format(average_precision))

    fpr, tpr, thresholds = roc_curve(preds['trueLabel'], \
                                     preds['anomalyScore'])
    areaUnderROC = auc(fpr, tpr)

    plt.figure()
    plt.plot(fpr, tpr, color='r', lw=2, label='ROC curve')
    plt.plot([0, 1], [0, 1], color='k', lw=2, linestyle='--')
    plt.xlim([0.0, 1.0])
    plt.ylim([0.0, 1.05])
    plt.xlabel('False Positive Rate')
    plt.ylabel('True Positive Rate')
    plt.title('Receiver operating characteristic: Area under the \
 curve = {0:0.2f}'.format(areaUnderROC))
    plt.legend(loc="lower right")
    plt.show()

    if returnPreds==True:
        return preds, average_precision

最后,这里有一个新函数叫做precisionAnalysis,帮助我们在某个召回率水平上评估模型的精度。具体来说,我们将确定模型在测试集中捕捉到 75%的欺诈信用卡交易的精度。精度越高,模型越好。

这是一个合理的基准。换句话说,我们希望能够捕捉到 75%的欺诈行为,并且尽可能高精度。如果我们没有达到足够高的精度,我们将不必要地拒绝良好的信用卡交易,可能会激怒我们的客户群体:

def precisionAnalysis(df, column, threshold):
    df.sort_values(by=column, ascending=False, inplace=True)
    threshold_value = threshold*df.trueLabel.sum()
    i = 0
    j = 0
    while i < threshold_value+1:
        if df.iloc[j]["trueLabel"]==1:
            i += 1
        j += 1
    return df, i/j

监督模型

为了对我们的半监督模型进行基准测试,让我们先看看单独使用监督模型和无监督模型的效果如何。

我们将从基于轻量梯度提升的监督学习解决方案开始,就像在第二章中表现最佳的那个。我们将使用k-折交叉验证来创建五个折叠:

k_fold = StratifiedKFold(n_splits=5,shuffle=True,random_state=2018)

接下来,设定梯度提升的参数:

params_lightGB = {
    'task': 'train',
    'application':'binary',
    'num_class':1,
    'boosting': 'gbdt',
    'objective': 'binary',
    'metric': 'binary_logloss',
    'metric_freq':50,
    'is_training_metric':False,
    'max_depth':4,
    'num_leaves': 31,
    'learning_rate': 0.01,
    'feature_fraction': 1.0,
    'bagging_fraction': 1.0,
    'bagging_freq': 0,
    'bagging_seed': 2018,
    'verbose': 0,
    'num_threads':16
}

现在,让我们训练算法:

trainingScores = []
cvScores = []
predictionsBasedOnKFolds = pd.DataFrame(data=[], index=y_train.index, \
                                        columns=['prediction'])

for train_index, cv_index in k_fold.split(np.zeros(len(X_train)), \
                                          y_train.ravel()):
    X_train_fold, X_cv_fold = X_train.iloc[train_index,:], \
        X_train.iloc[cv_index,:]
    y_train_fold, y_cv_fold = y_train.iloc[train_index], \
        y_train.iloc[cv_index]

    lgb_train = lgb.Dataset(X_train_fold, y_train_fold)
    lgb_eval = lgb.Dataset(X_cv_fold, y_cv_fold, reference=lgb_train)
    gbm = lgb.train(params_lightGB, lgb_train, num_boost_round=2000,
                   valid_sets=lgb_eval, early_stopping_rounds=200)

    loglossTraining = log_loss(y_train_fold, gbm.predict(X_train_fold, \
                                num_iteration=gbm.best_iteration))
    trainingScores.append(loglossTraining)

    predictionsBasedOnKFolds.loc[X_cv_fold.index,'prediction'] = \
        gbm.predict(X_cv_fold, num_iteration=gbm.best_iteration)
    loglossCV = log_loss(y_cv_fold, \
        predictionsBasedOnKFolds.loc[X_cv_fold.index,'prediction'])
    cvScores.append(loglossCV)

    print('Training Log Loss: ', loglossTraining)
    print('CV Log Loss: ', loglossCV)

loglossLightGBMGradientBoosting = log_loss(y_train, \
        predictionsBasedOnKFolds.loc[:,'prediction'])
print('LightGBM Gradient Boosting Log Loss: ', \
        loglossLightGBMGradientBoosting)

现在,我们将使用这个模型来预测信用卡交易测试集上的欺诈行为。

图 9-1 展示了结果。

监督模型的结果

图 9-1. 监督模型的结果

基于精度-召回曲线的测试平均精度为 0.62。要捕捉 75%的欺诈案例,我们的精度仅为 0.5%。

无监督模型

现在让我们使用无监督学习构建欺诈检测解决方案。具体来说,我们将构建一个稀疏的两层过完备自动编码器,使用线性激活函数。我们将在隐藏层中有 40 个节点,并且 2%的丢失率。

然而,我们将通过过采样我们拥有的欺诈案例来调整我们的训练集。过采样是一种用于调整给定数据集中类分布的技术。我们希望向我们的数据集中添加更多的欺诈案例,以便我们训练的自动编码器更容易将正常/非欺诈交易与异常/欺诈交易区分开来。

记住,在从训练集中删除 90%欺诈案例后,我们只剩下 33 个欺诈案例。我们将取这 33 个欺诈案例,复制它们 100 次,然后添加到训练集中。我们还会保留非过采样训练集的副本,以便在机器学习流水线的其余部分使用它们。

记住,我们不会触及测试集——测试集不进行过采样,只有训练集进行过采样:

oversample_multiplier = 100

X_train_original = X_train.copy()
y_train_original = y_train.copy()
X_test_original = X_test.copy()
y_test_original = y_test.copy()

X_train_oversampled = X_train.copy()
y_train_oversampled = y_train.copy()
X_train_oversampled = X_train_oversampled.append( \
        [X_train_oversampled[y_train==1]]*oversample_multiplier, \
        ignore_index=False)
y_train_oversampled = y_train_oversampled.append( \
        [y_train_oversampled[y_train==1]]*oversample_multiplier, \
        ignore_index=False)

X_train = X_train_oversampled.copy()
y_train = y_train_oversampled.copy()

现在让我们训练我们的自动编码器:

model = Sequential()
model.add(Dense(units=40, activation='linear', \
                activity_regularizer=regularizers.l1(10e-5), \
                input_dim=29,name='hidden_layer'))
model.add(Dropout(0.02))
model.add(Dense(units=29, activation='linear'))

model.compile(optimizer='adam',
              loss='mean_squared_error',
              metrics=['accuracy'])

num_epochs = 5
batch_size = 32

history = model.fit(x=X_train, y=X_train,
                    epochs=num_epochs,
                    batch_size=batch_size,
                    shuffle=True,
                    validation_split=0.20,
                    verbose=1)

predictions = model.predict(X_test, verbose=1)
anomalyScoresAE = anomalyScores(X_test, predictions)
preds, average_precision = plotResults(y_test, anomalyScoresAE, True)

图 9-2 展示了结果。

无监督模型的结果

图 9-2. 无监督模型的结果

基于精度-召回曲线的测试平均精度为 0.57。要捕捉 75%的欺诈案例,我们的精度仅为 45%。虽然无监督解决方案的平均精度与监督解决方案相似,但在 75%召回率下的 45%精度更佳。

然而,单独的无监督解决方案仍然不是很好。

半监督模型

现在,让我们取自动编码器学到的表示(隐藏层),将其与原始训练集结合起来,并将其馈送到梯度提升算法中。这是一种半监督方法,充分利用了监督和无监督学习。

要获取隐藏层,我们从 Keras API 中调用Model()类并使用get_layer函数:

layer_name = 'hidden_layer'

intermediate_layer_model = Model(inputs=model.input, \
                                 outputs=model.get_layer(layer_name).output)
intermediate_output_train = intermediate_layer_model.predict(X_train_original)
intermediate_output_test = intermediate_layer_model.predict(X_test_original)

让我们将这些自动编码器表示存储到 DataFrame 中,然后将它们与原始训练集结合起来:

intermediate_output_trainDF = \
    pd.DataFrame(data=intermediate_output_train,index=X_train_original.index)
intermediate_output_testDF = \
    pd.DataFrame(data=intermediate_output_test,index=X_test_original.index)

X_train = X_train_original.merge(intermediate_output_trainDF, \
                                 left_index=True,right_index=True)
X_test = X_test_original.merge(intermediate_output_testDF, \
                               left_index=True,right_index=True)
y_train = y_train_original.copy()

现在我们将在这个新的 69 个特征的训练集上训练梯度提升模型(29 个来自原始数据集,40 个来自自动编码器的表示):

trainingScores = []
cvScores = []
predictionsBasedOnKFolds = pd.DataFrame(data=[],index=y_train.index, \
                                        columns=['prediction'])

for train_index, cv_index in k_fold.split(np.zeros(len(X_train)), \
                                          y_train.ravel()):
    X_train_fold, X_cv_fold = X_train.iloc[train_index,:], \
        X_train.iloc[cv_index,:]
    y_train_fold, y_cv_fold = y_train.iloc[train_index], \
        y_train.iloc[cv_index]

    lgb_train = lgb.Dataset(X_train_fold, y_train_fold)
    lgb_eval = lgb.Dataset(X_cv_fold, y_cv_fold, reference=lgb_train)
    gbm = lgb.train(params_lightGB, lgb_train, num_boost_round=5000,
                   valid_sets=lgb_eval, early_stopping_rounds=200)

    loglossTraining = log_loss(y_train_fold,
                                gbm.predict(X_train_fold, \
                                num_iteration=gbm.best_iteration))
    trainingScores.append(loglossTraining)

    predictionsBasedOnKFolds.loc[X_cv_fold.index,'prediction'] = \
        gbm.predict(X_cv_fold, num_iteration=gbm.best_iteration)
    loglossCV = log_loss(y_cv_fold, \
            predictionsBasedOnKFolds.loc[X_cv_fold.index,'prediction'])
    cvScores.append(loglossCV)

    print('Training Log Loss: ', loglossTraining)
    print('CV Log Loss: ', loglossCV)

loglossLightGBMGradientBoosting = log_loss(y_train, \
                        predictionsBasedOnKFolds.loc[:,'prediction'])
print('LightGBM Gradient Boosting Log Loss: ', \
                        loglossLightGBMGradientBoosting)

图 9-3 展示了结果。

半监督模型的结果

图 9-3. 半监督模型的结果

基于精度-召回曲线的测试集平均精度为 0.78。这比监督和无监督模型都高出许多。

要捕获 75%的欺诈,我们的精度达到了 92%。这是一个显著的改进。在这种精度水平下,支付处理器应该对拒绝模型标记为潜在欺诈的交易感到放心。不到十分之一会出错,而我们将捕获大约 75%的欺诈行为。

监督学习和非监督学习的威力

在这种半监督信用卡欺诈检测解决方案中,监督学习和非监督学习都发挥了重要作用。探索的一种方式是分析最终梯度提升模型发现的最重要的特征是哪些。

让我们从刚刚训练的模型中找出并存储这些特征重要性数值:

featuresImportance = pd.DataFrame(data=list(gbm.feature_importance()), \
                        index=X_train.columns,columns=['featImportance'])
featuresImportance = featuresImportance/featuresImportance.sum()
featuresImportance.sort_values(by='featImportance', \
                               ascending=False,inplace=True)
featuresImportance

表 9-1 显示了按降序排列的一些最重要的特征。

表 9-1. 半监督模型的特征重要性

featImportance
V280.047843
Amount0.037263
210.030244
V210.029624
V260.029469
V120.028334
V270.028024
60.027405
280.026941
360.024050
50.022347

正如您在这里所看到的,一些顶级特征是自动编码器学习的隐藏层特征(非“V”特征),而其他特征则是原始数据集的主要成分(“V”特征)以及交易金额。

结论

半监督模型击败了独立的监督模型和独立的非监督模型的性能。

我们只是初步探讨了半监督学习的潜力,但这应该有助于从辩论监督和非监督学习之间的选择转变为在寻找最佳应用解决方案中结合监督和非监督学习。

第四部分:使用 TensorFlow 和 Keras 进行深度无监督学习

到目前为止,我们只使用了浅层神经网络;换句话说,只有少数隐藏层的网络。浅层神经网络在构建机器学习系统时确实很有用,但过去十年中机器学习中最强大的进展来自于具有许多隐藏层的神经网络,称为深度神经网络。这个机器学习的子领域称为深度学习。在大型标记数据集上进行的深度学习已经在计算机视觉、物体识别、语音识别和机器翻译等领域取得了重大的商业成功。

我们将专注于大型无标记数据集上的深度学习,这通常被称为深度无监督学习。这个领域仍然非常新,充满潜力,但与监督变体相比商业成功较少。在接下来的几章中,我们将构建深度无监督学习系统,从最简单的构建块开始。

第十章涵盖了受限玻尔兹曼机,我们将使用它来构建电影推荐系统。在第十一章,我们将把受限玻尔兹曼机堆叠在一起,创建称为深信网的深度神经网络。在第十二章,我们将使用生成对抗网络生成合成数据,这是当今深度无监督学习中最热门的领域之一。然后在第十三章,我们将回到聚类,但这次是处理时间序列数据。

这是很多高级材料,但很多深度无监督学习都依赖于我们在本书前面介绍的基本原理。

第十章:使用受限玻尔兹曼机的推荐系统

在本书的早期,我们使用无监督学习来学习未标记数据中的潜在(隐藏)结构。具体而言,我们进行了降维,将高维数据集减少到具有更少维度的数据集,并构建了异常检测系统。我们还进行了聚类,根据对象彼此之间的相似性或不相似性将它们分组。

现在,我们将进入生成式无监督模型,这涉及从原始数据集学习概率分布,并用它对以前未见过的数据进行推断。在后面的章节中,我们将使用这些模型生成看似真实的数据,有时几乎无法与原始数据区分开来。

到目前为止,我们主要研究了判别模型,这些模型根据算法从数据中学到的内容来分离观察结果;这些判别模型不会从数据中学习概率分布。判别模型包括监督学习模型,如逻辑回归和决策树(来自第二章),以及聚类方法,如k-均值和层次聚类(来自第五章)。

让我们从最简单的生成式无监督模型开始,即受限玻尔兹曼机

玻尔兹曼机

玻尔兹曼机最早由 Geoffrey Hinton(当时是卡内基梅隆大学的教授,现在是深度学习运动的先驱之一,多伦多大学的教授,以及谷歌的机器学习研究员)和 Terry Sejnowski(当时是约翰霍普金斯大学的教授)于 1985 年发明。

玻尔兹曼机——无限制型——由具有输入层和一个或多个隐藏层的神经网络组成。神经网络中的神经元或单元根据训练中输入的数据和玻尔兹曼机试图最小化的成本函数,做出是否启动的随机决策。通过这种训练,玻尔兹曼机发现数据的有趣特征,有助于模拟数据中复杂的潜在关系和模式。

然而,这些无限制的玻尔兹曼机使用神经网络,其中神经元不仅连接到其他层中的神经元,而且连接到同一层中的神经元。这与许多隐藏层的存在一起,使得无限制的玻尔兹曼机的训练效率非常低。由于这个原因,无限制的玻尔兹曼机在 20 世纪 80 年代和 90 年代几乎没有商业成功。

受限玻尔兹曼机

在 2000 年代,Geoffrey Hinton 等人开始通过使用修改后的原始无限制玻尔兹曼机取得商业成功。这些受限玻尔兹曼机(RBM)具有一个输入层(也称为可见层)和一个单独的隐藏层,神经元之间的连接受限,使得神经元仅连接到其他层的神经元,而不连接同一层的神经元。换句话说,没有可见-可见的连接和隐藏-隐藏的连接。¹

Geoffrey Hinton 还展示了这样简单的受限玻尔兹曼机(RBM)可以堆叠在一起,以便一个 RBM 的隐藏层的输出可以被馈送到另一个 RBM 的输入层。这种 RBM 堆叠可以多次重复,以逐步学习原始数据更细致的隐藏表示。这种多个 RBM 组成的网络可以看作是一个深层、多层次的神经网络模型——因此,深度学习领域从 2006 年开始蓬勃发展。

注意,RBM 使用随机方法来学习数据的潜在结构,而例如自编码器则使用确定性方法。

推荐系统

在本章中,我们将使用 RBM 构建一个推荐系统,这是迄今为止最成功的机器学习应用之一,在行业中广泛用于帮助预测用户对电影、音乐、书籍、新闻、搜索、购物、数字广告和在线约会的偏好。

推荐系统有两大主要类别——协同过滤推荐系统和基于内容的推荐系统。协同过滤涉及根据用户的过去行为以及与用户相似的其他用户的行为来构建推荐系统。这种推荐系统可以预测用户可能感兴趣的项目,即使用户从未明确表达过兴趣。Netflix 上的电影推荐就依赖于协同过滤。

基于内容的过滤涉及学习一个项目的独特属性,以推荐具有类似属性的其他项目。Pandora 上的音乐推荐就依赖于基于内容的过滤。

协同过滤

基于内容的过滤并不常用,因为学习项目的独特属性是一个相当困难的任务——目前人工机器很难达到这种理解水平。收集和分析大量关于用户行为和偏好的信息,并基于此进行预测,要容易得多。因此,协同过滤更广泛地被使用,也是我们这里将重点关注的推荐系统类型。

协同过滤不需要了解底层物品本身。相反,协同过滤假设在过去达成一致的用户将来也会达成一致,并且用户的偏好随时间保持稳定。通过建模用户与其他用户的相似性,协同过滤可以进行相当强大的推荐。此外,协同过滤不必依赖于显式数据(即用户提供的评分)。相反,它可以使用隐式数据,例如用户观看或点击特定项目的时间长短或频率来推断用户的喜好和厌恶。例如,过去 Netflix 要求用户对电影进行评分,但现在使用用户的隐式行为来推断用户的喜好和厌恶。

然而,协同过滤也存在其挑战。首先,它需要大量用户数据来进行良好的推荐。其次,这是一个非常计算密集的任务。第三,数据集通常非常稀疏,因为用户只对可能物品宇宙中的一小部分物品展现了偏好。假设我们有足够的数据,我们可以使用技术来处理数据的稀疏性并高效解决这个问题,我们将在本章中进行讨论。

Netflix 奖励

2006 年,Netflix 赞助了一场为期三年的比赛,旨在改进其电影推荐系统。该公司向那支能将其现有推荐系统的准确性提高至少 10%的团队提供了 100 万美元的大奖。它还发布了一个包含超过 1 亿部电影评分的数据集。2009 年 9 月,BellKor 的 Pramatic Chaos 团队赢得了这一奖项,他们使用了多种不同算法方法的集成。

这样一场备受关注的比赛,拥有丰富的数据集和有意义的奖金,激励了机器学习社区,并推动了推荐系统研究的实质性进展,为工业界在过去几年里开发出更好的推荐系统铺平了道路。

在本章中,我们将使用一个类似的电影评分数据集来构建我们自己的推荐系统,使用 RBM(Restricted Boltzmann Machines)。

MovieLens 数据集

不同于 Netflix 的 1 亿条评分数据集,我们将使用一个更小的电影评分数据集,称为MovieLens 20M 数据集,由明尼苏达大学双城分校计算机科学与工程系的研究实验室 GroupLens 提供。该数据集包含了从 1995 年 1 月 9 日到 2015 年 3 月 31 日,138,493 位用户对 27,278 部电影进行的 20,000,263 次评分。我们将随机选择至少评分了 20 部电影的用户子集。

这个数据集比 Netflix 的 1 亿条评分数据集更易于处理。由于文件大小超过了 100 兆字节,该文件在 GitHub 上不可访问。您需要直接从MovieLens 网站下载该文件。

数据准备

如前所述,让我们加载必要的库:

'''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

%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
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
from keras.layers import BatchNormalization, Input, Lambda
from keras import regularizers
from keras.losses import mse, binary_crossentropy

接下来,我们将加载评分数据集并将字段转换为适当的数据类型。我们只有几个字段。用户 ID,电影 ID,用户为电影提供的评分,以及提供评分的时间戳:

# Load the data
current_path = os.getcwd()
file = '\\datasets\\movielens_data\\ratings.csv'
ratingDF = pd.read_csv(current_path + file)

# Convert fields into appropriate data types
ratingDF.userId = ratingDF.userId.astype(str).astype(int)
ratingDF.movieId = ratingDF.movieId.astype(str).astype(int)
ratingDF.rating = ratingDF.rating.astype(str).astype(float)
ratingDF.timestamp = ratingDF.timestamp.apply(lambda x: \
                datetime.utcfromtimestamp(x).strftime('%Y-%m-%d %H:%M:%S'))

表 10-1 展示了数据的部分视图。

表 10-1. MovieLens 评分数据

用户 ID电影 ID评分时间戳
0123.52005-04-02 23:53:47
11293.52005-04-02 23:31:16
21323.52005-04-02 23:33:39
31473.52005-04-02 23:32:07
41503.52005-04-02 23:29:40
511123.52004-09-10 03:09:00
611514.02004-09-10 03:08:54
712234.02005-04-02 23:46:13
812534.02005-04-02 23:35:40
912604.02005-04-02 23:33:46
1012934.02005-04-02 23:31:43
1112964.02005-04-02 23:32:47
1213184.02005-04-02 23:33:18
1313373.52004-09-10 03:08:29

让我们确认唯一用户数、唯一电影数和总评分数,还将计算用户提供的平均评分数量:

n_users = ratingDF.userId.unique().shape[0]
n_movies = ratingDF.movieId.unique().shape[0]
n_ratings = len(ratingDF)
avg_ratings_per_user = n_ratings/n_users

print('Number of unique users: ', n_users)
print('Number of unique movies: ', n_movies)
print('Number of total ratings: ', n_ratings)
print('Average number of ratings per user: ', avg_ratings_per_user)

数据正如我们所预期的那样:

Number of unique users: 138493
Number of unique movies: 26744
Number of total ratings: 20000263
Average number of ratings per user: 144.4135299257002

为了减少这个数据集的复杂性和大小,让我们集中于排名前一千的电影。这将把评分数从约 20 百万减少到约 12.8 百万。

movieIndex = ratingDF.groupby("movieId").count().sort_values(by= \
                "rating",ascending=False)[0:1000].index
ratingDFX2 = ratingDF[ratingDF.movieId.isin(movieIndex)]
ratingDFX2.count()

我们还将随机抽取一千名用户的样本,并仅过滤这些用户的数据集。这将把评分数从约 12.8 百万减少到 90,213 个。这个数量足以展示协同过滤的效果:

userIndex = ratingDFX2.groupby("userId").count().sort_values(by= \
    "rating",ascending=False).sample(n=1000, random_state=2018).index
ratingDFX3 = ratingDFX2[ratingDFX2.userId.isin(userIndex)]
ratingDFX3.count()

同样,让我们重新索引movieIDuserID到 1 到 1,000 的范围内,用于我们的简化数据集:

movies = ratingDFX3.movieId.unique()
moviesDF = pd.DataFrame(data=movies,columns=['originalMovieId'])
moviesDF['newMovieId'] = moviesDF.index+1

users = ratingDFX3.userId.unique()
usersDF = pd.DataFrame(data=users,columns=['originalUserId'])
usersDF['newUserId'] = usersDF.index+1

ratingDFX3 = ratingDFX3.merge(moviesDF,left_on='movieId', \
                              right_on='originalMovieId')
ratingDFX3.drop(labels='originalMovieId', axis=1, inplace=True)

ratingDFX3 = ratingDFX3.merge(usersDF,left_on='userId', \
                              right_on='originalUserId')
ratingDFX3.drop(labels='originalUserId', axis=1, inplace=True)

让我们计算我们简化数据集中的唯一用户数、唯一电影数、总评分数以及每个用户的平均评分数量:

n_users = ratingDFX3.userId.unique().shape[0]
n_movies = ratingDFX3.movieId.unique().shape[0]
n_ratings = len(ratingDFX3)
avg_ratings_per_user = n_ratings/n_users

print('Number of unique users: ', n_users)
print('Number of unique movies: ', n_movies)
print('Number of total ratings: ', n_ratings)
print('Average number of ratings per user: ', avg_ratings_per_user)

结果如预期:

Number of unique users: 1000
Number of unique movies: 1000
Number of total ratings: 90213
Average number of ratings per user: 90.213

让我们从这个简化的数据集中生成一个测试集和一个验证集,使得每个留出集占简化数据集的 5%:

X_train, X_test = train_test_split(ratingDFX3,
 test_size=0.10, shuffle=True, random_state=2018)

X_validation, X_test = train_test_split(X_test,
 test_size=0.50, shuffle=True, random_state=2018)

下面显示了训练集、验证集和测试集的大小:

Size of train set: 81191
Size of validation set: 4511
Size of test set: 4511

定义成本函数:均方误差

现在我们已经准备好处理这些数据了。

首先,让我们创建一个m x n的矩阵,其中m是用户数,n是电影数。这将是一个稀疏填充的矩阵,因为用户只对电影的一小部分进行评分。例如,一个拥有一千个用户和一千部电影的矩阵在训练集中只有 81,191 个评分。如果每个一千个用户都对每一千部电影进行评分,我们将得到一个百万个评分的矩阵,但是平均而言用户只对少数电影进行评分,因此我们在训练集中只有 81,191 个评分。其余的值(矩阵中近 92%的值)将为零:

# Generate ratings matrix for train
ratings_train = np.zeros((n_users, n_movies))
for row in X_train.itertuples():
    ratings_train[row[6]-1, row[5]-1] = row[3]

# Calculate sparsity of the train ratings matrix
sparsity = float(len(ratings_train.nonzero()[0]))
sparsity /= (ratings_train.shape[0] * ratings_train.shape[1])
sparsity *= 100
print('Sparsity: {:4.2f}%'.format(sparsity))

我们将为验证集和测试集生成类似的矩阵,它们会更加稀疏,当然:

# Generate ratings matrix for validation
ratings_validation = np.zeros((n_users, n_movies))
for row in X_validation.itertuples():
    ratings_validation[row[6]-1, row[5]-1] = row[3]

# Generate ratings matrix for test
ratings_test = np.zeros((n_users, n_movies))
for row in X_test.itertuples():
    ratings_test[row[6]-1, row[5]-1] = row[3]

在构建推荐系统之前,让我们定义我们将用来评判模型好坏的成本函数。我们将使用均方误差(MSE),这是机器学习中最简单的成本函数之一。MSE 测量了预测值与实际值之间的平均平方误差。要计算 MSE,我们需要两个大小为*[n,1]的向量,其中n*是我们正在预测评分的数量 —— 对于验证集是 4,511。一个向量包含实际评分,另一个向量包含预测值。

让我们首先将验证集中带有评分的稀疏矩阵展平。这将是实际评分的向量:

actual_validation = ratings_validation[ratings_validation.nonzero()].flatten()

进行基准实验

作为基准,让我们预测验证集的平均评分为 3.5,并计算 MSE:

pred_validation = np.zeros((len(X_validation),1))
pred_validation[pred_validation==0] = 3.5
pred_validation

mean_squared_error(pred_validation, actual_validation)

这种非常天真预测的 MSE 是 1.05。这是我们的基准:

Mean squared error using naive prediction: 1.055420084238528

让我们看看是否可以通过预测用户对给定电影的评分来改善结果,基于该用户对所有其他电影的平均评分:

ratings_validation_prediction = np.zeros((n_users, n_movies))
i = 0
for row in ratings_train:
    ratings_validation_prediction[i][ratings_validation_prediction[i]==0] \
        = np.mean(row[row>0])
    i += 1

pred_validation = ratings_validation_prediction \
    [ratings_validation.nonzero()].flatten()
user_average = mean_squared_error(pred_validation, actual_validation)
print('Mean squared error using user average:', user_average)

均方误差(MSE)改善到 0.909:

Mean squared error using user average: 0.9090717929472647

现在,让我们基于所有其他用户对该电影的平均评分来预测用户对给定电影的评分:

ratings_validation_prediction = np.zeros((n_users, n_movies)).T
i = 0
for row in ratings_train.T:
    ratings_validation_prediction[i][ratings_validation_prediction[i]==0] \
        = np.mean(row[row>0])
    i += 1

ratings_validation_prediction = ratings_validation_prediction.T
pred_validation = ratings_validation_prediction \
    [ratings_validation.nonzero()].flatten()
movie_average = mean_squared_error(pred_validation, actual_validation)
print('Mean squared error using movie average:', movie_average)

这种方法的 MSE 为 0.914,与使用用户平均值发现的 MSE 类似:

Mean squared error using movie average: 0.9136057106858655

矩阵分解

在使用 RBM 构建推荐系统之前,让我们首先使用矩阵分解来构建一个。矩阵分解将用户-物品矩阵分解为两个较低维度矩阵的乘积。用户在较低维度潜在空间中表示,物品也是如此。

假设我们的用户-物品矩阵是 R,有 m 个用户和 n 个物品。矩阵分解将创建两个较低维度的矩阵,HWH 是一个 "m 用户" x "k 潜在因子" 的矩阵,W 是一个 "k 潜在因子" x "n 物品" 的矩阵。

评分通过矩阵乘法计算:R = H__W

k 潜在因子的数量决定了模型的容量。k 越高,模型的容量越大。通过增加k,我们可以提高对用户评分预测的个性化能力,但如果k过高,模型将过度拟合数据。

所有这些对你来说应该是熟悉的。矩阵分解学习了用户和物品在较低维度空间中的表示,并基于新学到的表示进行预测。

一个潜在因子

让我们从最简单的矩阵分解形式开始 —— 只使用一个潜在因子。我们将使用 Keras 来执行我们的矩阵分解。

首先,我们需要定义图表。输入是用户嵌入的一维向量和电影嵌入的一维向量。我们将这些输入向量嵌入到一个潜在空间中,然后展平它们。为了生成输出向量 product,我们将采用电影向量和用户向量的点积。我们将使用 Adam 优化器 来最小化我们的损失函数,该损失函数定义为 mean_squared_error

n_latent_factors = 1

user_input = Input(shape=[1], name='user')
user_embedding = Embedding(input_dim=n_users + 1, output_dim=n_latent_factors,
 name='user_embedding')(user_input)
user_vec = Flatten(name='flatten_users')(user_embedding)

movie_input = Input(shape=[1], name='movie')
movie_embedding = Embedding(input_dim=n_movies + 1, output_dim=n_latent_factors,
 name='movie_embedding')(movie_input)
movie_vec = Flatten(name='flatten_movies')(movie_embedding)

product = dot([movie_vec, user_vec], axes=1)
model = Model(inputs=[user_input, movie_input], outputs=product)
model.compile('adam', 'mean_squared_error')

让我们通过训练集中的用户和电影向量来训练模型。我们还将在训练过程中对验证集进行评估。我们将根据实际评分计算 MSE。

我们将训练一百个 epochs,并记录训练和验证结果的历史。让我们也来绘制结果:

history = model.fit(x=[X_train.newUserId, X_train.newMovieId], \
                    y=X_train.rating, epochs=100, \
                    validation_data=([X_validation.newUserId, \
                    X_validation.newMovieId], X_validation.rating), \
                    verbose=1)

pd.Series(history.history['val_loss'][10:]).plot(logy=False)
plt.xlabel("Epoch")
plt.ylabel("Validation Error")
print('Minimum MSE: ', min(history.history['val_loss']))

图 10-1 展示了结果。

使用 MF 和一个潜在因子的验证 MSE 图

图 10-1. 使用矩阵因子化和一个潜在因子的验证 MSE 图

使用矩阵因子化和一个潜在因子的最小 MSE 为 0.796。这比之前的用户平均和电影平均方法更好。

看看我们是否可以通过增加潜在因子的数量(即模型的容量)来进一步改进。

三个潜在因子

图 10-2 展示了使用三个潜在因子的结果。

使用 MF 和三个潜在因子的验证 MSE 图

图 10-2. 使用矩阵因子化和三个潜在因子的验证 MSE 图

最小 MSE 为 0.765,比使用一个潜在因子更好。

五个潜在因子

现在让我们构建一个使用五个潜在因子的矩阵因子化模型(参见 图 10-3 的结果)。

使用 MF 和五个潜在因子的验证 MSE 图

图 10-3. 使用矩阵因子化和五个潜在因子的验证 MSE 图

最小 MSE 未能改进,在前 25 个 epochs 左右明显出现过拟合迹象。验证误差下降然后开始增加。增加矩阵因子化模型的容量将不会帮助太多。

使用 RBM 进行协同过滤

让我们再次回到 RBM。回想一下,RBM 有两层——输入/可见层和隐藏层。每一层中的神经元与另一层中的神经元进行通信,但不与同一层中的神经元进行通信。换句话说,神经元之间没有同层通信——这就是 RBM 中“限制”的一部分。

RBM 的另一个重要特征是层之间的通信是双向的,而不仅仅是单向的。例如,对于自编码器,神经元只能通过前向传递与下一层通信。

使用 RBM,可见层中的神经元与隐藏层通信,然后隐藏层将信息传回可见层,来回多次交换。 RBM 执行此通信——在可见层和隐藏层之间来回传递——以开发生成模型,使得从隐藏层输出的重构与原始输入相似。

换句话说,RBM 正在尝试创建一个生成模型,该模型将根据用户评分的电影之间的相似性以及用户与其他评分该电影的用户的相似性,帮助预测用户是否会喜欢用户从未看过的电影。

可见层将有 X 个神经元,其中 X 是数据集中电影的数量。 每个神经元将具有从零到一的归一化评分值,其中零表示用户未看过电影。 归一化评分值越接近一,表示用户越喜欢神经元表示的电影。

可见层中的神经元将与隐藏层中的神经元通信,后者将试图学习表征用户-电影偏好的潜在特征。

注意,RBM 也被称为对称的二分图、双向图——对称是因为每个可见节点与每个隐藏节点相连,二分是因为有两层节点,双向是因为通信是双向的。

RBM 神经网络架构

对于我们的电影推荐系统,我们有一个m x n矩阵,其中m为用户数,n为电影数。 要训练 RBM,我们将一批k用户及其n电影评分传递到神经网络,并训练一定数量的epochs

每个传入神经网络的输入x表示单个用户对所有n部电影的评分偏好,例如,我们的示例中n为一千。 因此,可见层有n个节点,每个节点对应一个电影。

我们可以指定隐藏层中节点的数量,通常比可见层中的节点少,以尽可能有效地让隐藏层学习原始输入的最显著方面。

每个输入v0都与其相应的权重W相乘。 权重是从可见层到隐藏层的连接学习的。 然后我们在隐藏层添加一个称为hb的偏置向量。 偏置确保至少有一些神经元会激活。 这个Wv0+hb*结果通过激活函数传递。

之后,我们将通过一种称为Gibbs sampling的过程对生成的输出样本进行采样。 换句话说,隐藏层的激活结果以随机方式生成最终输出。 这种随机性有助于构建性能更好、更强大的生成模型。

接下来,吉布斯采样后的输出—称为h0—通过神经网络反向传播回去,进行所谓的反向传播。在反向传播中,吉布斯采样后的前向传播中的激活被馈送到隐藏层,并与之前相同的权重W相乘。然后我们在可见层添加一个新的称为vb的偏置向量。

这个W_h0+vb通过激活函数传递,并进行吉布斯采样。这个输出是v1,然后作为新的输入传递到可见层和神经网络中,进行另一次前向传播。

RBM 通过一系列前向和反向传播的步骤来学习最优权重,试图构建一个健壮的生成模型。RBM 是我们探索的第一种生成学习模型。通过执行吉布斯采样和通过前向和反向传播重新训练权重,RBM 试图学习原始输入的概率分布。具体来说,RBM 最小化Kullback–Leibler 散度,该散度用于衡量一个概率分布与另一个之间的差异;在这种情况下,RBM 最小化原始输入的概率分布与重建数据的概率分布之间的差异。

通过迭代调整神经网络中的权重,受限玻尔兹曼机(RBM)学习尽可能地逼近原始数据。

通过这个新学习到的概率分布,RBM 能够对以前未见过的数据进行预测。在这种情况下,我们设计的 RBM 将尝试基于用户与其他用户的相似性及其他用户对这些电影的评分来预测用户从未看过的电影的评分。

构建 RBM 类的组件

首先,我们将用几个参数初始化这个类;这些参数包括 RBM 的输入大小、输出大小、学习率、训练时的周期数以及训练过程中的批处理大小。

我们还将创建用于权重矩阵、隐藏偏置向量和可见偏置向量的零矩阵:

# Define RBM class
class RBM(object):

    def __init__(self, input_size, output_size,
                 learning_rate, epochs, batchsize):
        # Define hyperparameters
        self._input_size = input_size
        self._output_size = output_size
        self.learning_rate = learning_rate
        self.epochs = epochs
        self.batchsize = batchsize

        # Initialize weights and biases using zero matrices
        self.w = np.zeros([input_size, output_size], "float")
        self.hb = np.zeros([output_size], "float")
        self.vb = np.zeros([input_size], "float")

接下来,让我们定义前向传播、反向传播和在这些传播过程中数据抽样的函数。

这里是前向传播,其中h是隐藏层,v是可见层:

def prob_h_given_v(self, visible, w, hb):
    return tf.nn.sigmoid(tf.matmul(visible, w) + hb)

这里是反向传播的过程:

def prob_v_given_h(self, hidden, w, vb):
    return tf.nn.sigmoid(tf.matmul(hidden, tf.transpose(w)) + vb)

这里是抽样函数的定义:

def sample_prob(self, probs):
    return tf.nn.relu(tf.sign(probs - tf.random_uniform(tf.shape(probs))))

现在我们需要一个函数来执行训练。由于我们使用 TensorFlow,我们首先需要为 TensorFlow 图创建占位符,在我们将数据馈送到 TensorFlow 会话中时使用。

我们将为权重矩阵、隐藏偏置向量和可见偏置向量创建占位符。我们还需要用零初始化这三者的值。此外,我们需要一个集合来保存当前值和一个集合来保存先前的值:

_w = tf.placeholder("float", [self._input_size, self._output_size])
_hb = tf.placeholder("float", [self._output_size])
_vb = tf.placeholder("float", [self._input_size])

prv_w = np.zeros([self._input_size, self._output_size], "float")
prv_hb = np.zeros([self._output_size], "float")
prv_vb = np.zeros([self._input_size], "float")

cur_w = np.zeros([self._input_size, self._output_size], "float")
cur_hb = np.zeros([self._output_size], "float")
cur_vb = np.zeros([self._input_size], "float")

同样,我们需要一个可见层的占位符。隐藏层是从可见层和权重矩阵的矩阵乘法以及隐藏偏置向量的矩阵加法导出的:

v0 = tf.placeholder("float", [None, self._input_size])
h0 = self.sample_prob(self.prob_h_given_v(v0, _w, _hb))

在反向传播期间,我们取隐藏层输出,与正向传播期间使用的权重矩阵的转置相乘,并加上可见偏置向量。请注意,权重矩阵在正向和反向传播期间是相同的。然后,我们再次执行正向传播:

v1 = self.sample_prob(self.prob_v_given_h(h0, _w, _vb))
h1 = self.prob_h_given_v(v1, _w, _hb)

要更新权重,我们执行对比散度[²]。

我们还将误差定义为 MSE。

positive_grad = tf.matmul(tf.transpose(v0), h0)
negative_grad = tf.matmul(tf.transpose(v1), h1)

update_w = _w + self.learning_rate * \
    (positive_grad - negative_grad) / tf.to_float(tf.shape(v0)[0])
update_vb = _vb +  self.learning_rate * tf.reduce_mean(v0 - v1, 0)
update_hb = _hb +  self.learning_rate * tf.reduce_mean(h0 - h1, 0)

err = tf.reduce_mean(tf.square(v0 - v1))

有了这些,我们可以使用刚刚定义的变量初始化 TensorFlow 会话。

一旦我们调用sess.run,我们可以输入数据批次开始训练。在训练过程中,将进行前向和反向传播,并根据生成数据与原始输入的比较更新 RBM 权重。我们将打印每个 epoch 的重构误差。

with tf.Session() as sess:
 sess.run(tf.global_variables_initializer())

 for epoch in range(self.epochs):
     for start, end in zip(range(0, len(X),
      self.batchsize),range(self.batchsize,len(X), self.batchsize)):
         batch = X[start:end]
         cur_w = sess.run(update_w, feed_dict={v0: batch,
          _w: prv_w, _hb: prv_hb, _vb: prv_vb})
         cur_hb = sess.run(update_hb, feed_dict={v0: batch,
          _w: prv_w, _hb: prv_hb, _vb: prv_vb})
         cur_vb = sess.run(update_vb, feed_dict={v0: batch,
          _w: prv_w, _hb: prv_hb, _vb: prv_vb})
         prv_w = cur_w
         prv_hb = cur_hb
         prv_vb = cur_vb
     error = sess.run(err, feed_dict={v0: X,
      _w: cur_w, _vb: cur_vb, _hb: cur_hb})
     print ('Epoch: %d' % epoch,'reconstruction error: %f' % error)
 self.w = prv_w
 self.hb = prv_hb
 self.vb = prv_vb

训练 RBM 推荐系统

要训练 RBM,让我们从ratings_train创建一个名为inputX的 NumPy 数组,并将这些值转换为 float32。我们还将定义 RBM 以接受一千维的输入,输出一千维的输出,使用学习率为 0.3,训练五百个 epoch,并使用批量大小为两百。这些参数只是初步的参数选择;您应该通过实验找到更优的参数,鼓励进行实验:

# Begin the training cycle

# Convert inputX into float32
inputX = ratings_train
inputX = inputX.astype(np.float32)

# Define the parameters of the RBMs we will train
rbm=RBM(1000,1000,0.3,500,200)

让我们开始训练:

rbm.train(inputX)
outputX, reconstructedX, hiddenX = rbm.rbm_output(inputX)

图 10-4 显示了重构误差的图。

RBM 错误图

图 10-4. RBM 错误图

长时间训练后,误差项通常会减少。

现在让我们将开发的 RBM 模型应用于预测验证集中用户的评分(该验证集与训练集中的用户相同):

# Predict ratings for validation set
inputValidation = ratings_validation
inputValidation = inputValidation.astype(np.float32)

finalOutput_validation, reconstructedOutput_validation, _ = \
    rbm.rbm_output(inputValidation)

接下来,让我们将预测转换为数组,并根据真实验证评分计算 MSE:

predictionsArray = reconstructedOutput_validation
pred_validation = \
    predictionsArray[ratings_validation.nonzero()].flatten()
actual_validation = \
    ratings_validation[ratings_validation.nonzero()].flatten()

rbm_prediction = mean_squared_error(pred_validation, actual_validation)
print('Mean squared error using RBM prediction:', rbm_prediction)

以下代码显示了验证集上的 MSE:

Mean squared error using RBM prediction: 9.331135003325205

这个 MSE 是一个起点,随着更多的实验,可能会有所改进。

结论

在本章中,我们探讨了受限玻尔兹曼机,并用它们构建了一个电影评分的推荐系统。我们构建的 RBM 推荐系统学习了给定用户之前评分和他们最相似用户的评分情况下电影评分的概率分布。然后,我们使用学习的概率分布来预测以前未见的电影的评分。

在第十一章,我们将堆叠 RBM 以构建深度信念网络,并使用它们执行更强大的无监督学习任务。

¹ 这类 RBM 的最常见训练算法被称为基于梯度的对比散度算法。

² 更多关于这个主题的内容,请参阅论文“对比散度学习”

第十一章:使用深度信念网络进行特征检测

在第十章中,我们探索了限制玻尔兹曼机并使用它们构建了一个电影评分的推荐系统。在本章中,我们将堆叠 RBM 构建深度信念网络(DBNs)。DBNs 是由多伦多大学的杰弗·辛顿于 2006 年首次提出的。

RBM 只有两层,一个可见层和一个隐藏层;换句话说,RBM 只是浅层神经网络。DBN 由多个 RBM 组成——一个 RBM 的隐藏层作为下一个 RBM 的可见层。因为它们涉及许多层,所以 DBN 是深度神经网络。事实上,它们是我们迄今为止介绍的第一种深度无监督神经网络。

浅层无监督神经网络,比如 RBM,不能捕获图像、声音和文本等复杂数据的结构,但 DBN 可以。DBN 已被用于识别和聚类图像、视频捕获、声音和文本,尽管过去十年中其他深度学习方法在性能上已超过了 DBN。

深度信念网络详解

与 RBM 一样,DBN 可以学习输入的基本结构并以概率方式重构它。换句话说,DBN——就像 RBM 一样——是生成模型。而且,与 RBM 一样,DBN 中的层之间只有连接,但每一层内部的单元之间没有连接。

在 DBN 中,一次训练一层,从第一个隐藏层开始,它与输入层一起组成第一个 RBM。一旦训练了第一个 RBM,第一个 RBM 的隐藏层将作为下一个 RBM 的可见层,并用于训练 DBN 的第二个隐藏层。

这个过程会一直持续到 DBN 的所有层都被训练完毕。除了 DBN 的第一层和最后一层之外,DBN 中的每一层都既充当了一个隐藏层,也充当了一个 RBM 的可见层。

DBN 是一种表示的层次结构,就像所有神经网络一样,它是一种表示学习形式。请注意,DBN 不使用任何标签。相反,DBN 一次学习输入数据中的一个层的底层结构。

标签可以用来微调 DBN 的最后几层,但只有在初始无监督学习完成后才能这样做。例如,如果我们想要 DBN 成为一个分类器,我们会先进行无监督学习(称为预训练过程),然后使用标签微调 DBN(称为微调过程)。

MNIST 图像分类

让我们再次使用 DBN 构建图像分类器。我们将再次使用 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

%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
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

然后我们将加载数据并将其存储在 Pandas DataFrames 中。我们还将将标签编码为 one-hot 向量。这与我们在本书早期介绍 MNIST 数据集时所做的工作类似:

# 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]

# 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()

def one_hot(series):
    label_binarizer = pp.LabelBinarizer()
    label_binarizer.fit(range(max(series)+1))
    return label_binarizer.transform(series)

# Create one-hot vectors for the labels
y_train_oneHot = one_hot(y_train)
y_validation_oneHot = one_hot(y_validation)
y_test_oneHot = one_hot(y_test)

限制玻尔兹曼机

接下来,让我们定义一个 RBM 类,这样我们就可以快速连续训练多个 RBM(它们是 DBN 的构建模块)。

请记住,RBM 具有输入层(也称为可见层)和单个隐藏层,神经元之间的连接受到限制,使得神经元仅连接到其他层中的神经元,而不连接同一层中的神经元。还要记住,层间通信是双向的,不仅是单向的或者像自编码器那样的前向方式。

在 RBM 中,可见层的神经元与隐藏层通信,隐藏层从 RBM 学习的概率模型生成数据,然后隐藏层将这个生成的信息传递回可见层。可见层接收来自隐藏层的生成数据样本,对其进行采样,将其与原始数据进行比较,并根据生成数据样本与原始数据之间的重构误差,向隐藏层发送新信息,以再次重复此过程。

通过这种双向通信方式,RBM 开发了一个生成模型,使得从隐藏层输出的重构数据与原始输入相似。

构建 RBM 类的组件

就像我们在第十章中所做的那样,让我们逐步了解RBM类的各个组成部分。

首先,我们将使用几个参数来初始化这个类;它们是 RBM 的输入大小、输出大小、学习速率、训练时的时代数以及批处理大小。我们还将创建权重矩阵、隐藏偏置向量和可见偏置向量的零矩阵:

# Define RBM class
class RBM(object):

    def __init__(self, input_size, output_size,
                 learning_rate, epochs, batchsize):
        # Define hyperparameters
        self._input_size = input_size
        self._output_size = output_size
        self.learning_rate = learning_rate
        self.epochs = epochs
        self.batchsize = batchsize

        # Initialize weights and biases using zero matrices
        self.w = np.zeros([input_size, output_size], "float")
        self.hb = np.zeros([output_size], "float")
        self.vb = np.zeros([input_size], "float")

接下来,让我们定义正向传递、反向传递以及在每次传递期间对数据进行采样的函数。

这里是正向传递,其中h是隐藏层,v是可见层:

def prob_h_given_v(self, visible, w, hb):
    return tf.nn.sigmoid(tf.matmul(visible, w) + hb)

这里是向后传递:

def prob_v_given_h(self, hidden, w, vb):
    return tf.nn.sigmoid(tf.matmul(hidden, tf.transpose(w)) + vb)

这里是采样函数:

def sample_prob(self, probs):
    return tf.nn.relu(tf.sign(probs - tf.random_uniform(tf.shape(probs))))

现在我们需要一个执行训练的函数。因为我们使用的是 TensorFlow,所以我们首先需要为 TensorFlow 图创建占位符,当我们将数据提供给 TensorFlow 会话时将使用这些占位符。

我们将为权重矩阵、隐藏偏置向量和可见偏置向量设立占位符。我们还需要使用零初始化这三者的值。并且,我们需要一个集合来保存当前值,另一个集合来保存先前的值:

_w = tf.placeholder("float", [self._input_size, self._output_size])
_hb = tf.placeholder("float", [self._output_size])
_vb = tf.placeholder("float", [self._input_size])

prv_w = np.zeros([self._input_size, self._output_size], "float")
prv_hb = np.zeros([self._output_size], "float")
prv_vb = np.zeros([self._input_size], "float")

cur_w = np.zeros([self._input_size, self._output_size], "float")
cur_hb = np.zeros([self._output_size], "float")
cur_vb = np.zeros([self._input_size], "float")

同样地,我们需要一个可见层的占位符。隐藏层是通过可见层和权重矩阵的矩阵乘法以及隐藏偏置向量的矩阵加法派生的:

v0 = tf.placeholder("float", [None, self._input_size])
h0 = self.sample_prob(self.prob_h_given_v(v0, _w, _hb))

在向后传递期间,我们获取隐藏层输出,将其与在正向传递期间使用的权重矩阵的转置相乘,并添加可见偏置向量。请注意,权重矩阵在正向和向后传递期间都是相同的。

然后我们再次执行正向传递:

v1 = self.sample_prob(self.prob_v_given_h(h0, _w, _vb))
h1 = self.prob_h_given_v(v1, _w, _hb)

要更新权重,我们执行对比散度,我们在 第十章 中介绍过。我们还定义误差为均方误差(MSE):

positive_grad = tf.matmul(tf.transpose(v0), h0)
negative_grad = tf.matmul(tf.transpose(v1), h1)

update_w = _w + self.learning_rate * \
    (positive_grad - negative_grad) / tf.to_float(tf.shape(v0)[0])
update_vb = _vb +  self.learning_rate * tf.reduce_mean(v0 - v1, 0)
update_hb = _hb +  self.learning_rate * tf.reduce_mean(h0 - h1, 0)

err = tf.reduce_mean(tf.square(v0 - v1))

有了这个,我们就可以用刚刚定义的变量初始化 TensorFlow 会话了。

一旦我们调用 sess.run,我们就可以提供数据的批次开始训练。在训练过程中,将进行前向和反向传播,并根据生成数据与原始输入的比较更新 RBM 的权重。我们将打印每个周期的重建误差:

with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())

    for epoch in range(self.epochs):
        for start, end in zip(range(0, len(X), self.batchsize), \
                range(self.batchsize,len(X), self.batchsize)):
            batch = X[start:end]
            cur_w = sess.run(update_w, \
                feed_dict={v0: batch, _w: prv_w, \
                           _hb: prv_hb, _vb: prv_vb})
            cur_hb = sess.run(update_hb, \
                feed_dict={v0: batch, _w: prv_w, \
                           _hb: prv_hb, _vb: prv_vb})
            cur_vb = sess.run(update_vb, \
                feed_dict={v0: batch, _w: prv_w, \
                           _hb: prv_hb, _vb: prv_vb})
            prv_w = cur_w
            prv_hb = cur_hb
            prv_vb = cur_vb
        error = sess.run(err, feed_dict={v0: X, _w: cur_w, \
                                        _vb: cur_vb, _hb: cur_hb})
        print ('Epoch: %d' % epoch,'reconstruction error: %f' % error)
    self.w = prv_w
    self.hb = prv_hb
    self.vb = prv_vb

使用 RBM 模型生成图像

让我们也定义一个函数,从 RBM 学习的生成模型中生成新图像:

def rbm_output(self, X):

    input_X = tf.constant(X)
    _w = tf.constant(self.w)
    _hb = tf.constant(self.hb)
    _vb = tf.constant(self.vb)
    out = tf.nn.sigmoid(tf.matmul(input_X, _w) + _hb)
    hiddenGen = self.sample_prob(self.prob_h_given_v(input_X, _w, _hb))
    visibleGen = self.sample_prob(self.prob_v_given_h(hiddenGen, _w, _vb))
    with tf.Session() as sess:
        sess.run(tf.global_variables_initializer())
        return sess.run(out), sess.run(visibleGen), sess.run(hiddenGen)

我们将原始图像矩阵 X 输入到函数中。我们为原始图像矩阵、权重矩阵、隐藏偏置向量和可见偏置向量创建 TensorFlow 占位符。然后,我们将输入矩阵推送以产生正向传播的输出(out)、隐藏层的样本生成(hiddenGen)以及模型生成的重建图像的样本(visibleGen)。

查看中间特征检测器

最后,让我们定义一个函数来显示隐藏层的特征检测器:

def show_features(self, shape, suptitle, count=-1):
    maxw = np.amax(self.w.T)
    minw = np.amin(self.w.T)
    count = self._output_size if count == -1 or count > \
            self._output_size else count
    ncols = count if count < 14 else 14
    nrows = count//ncols
    nrows = nrows if nrows > 2 else 3
    fig = plt.figure(figsize=(ncols, nrows), dpi=100)
    grid = Grid(fig, rect=111, nrows_ncols=(nrows, ncols), axes_pad=0.01)

    for i, ax in enumerate(grid):
        x = self.w.T[i] if i<self._input_size else np.zeros(shape)
        x = (x.reshape(1, -1) - minw)/maxw
        ax.imshow(x.reshape(*shape), cmap=mpl.cm.Greys)
        ax.set_axis_off()

    fig.text(0.5,1, suptitle, fontsize=20, horizontalalignment='center')
    fig.tight_layout()
    plt.show()
    return

现在我们将在 MNIST 数据集上使用这些函数及其他函数。

训练深度信念网络的三个 RBM

现在我们将使用 MNIST 数据来依次训练三个 RBM,其中一个 RBM 的隐藏层将作为下一个 RBM 的可见层。这三个 RBM 将组成我们正在构建的用于图像分类的深度信念网络(DBN)。

首先,让我们将训练数据转换为 NumPy 数组并存储起来。接下来,我们将创建一个名为 rbm_list 的列表来保存我们训练的 RBM。然后,我们将定义三个 RBM 的超参数,包括输入大小、输出大小、学习率、训练周期数以及训练的批次大小。

所有这些都可以使用我们之前定义的 RBM 类来构建。

对于我们的深度信念网络(DBN),我们将使用以下的受限玻尔兹曼机(RBM):第一个将接收原始的 784 维输入,并输出一个 700 维的矩阵。接下来的 RBM 将使用第一个 RBM 输出的 700 维矩阵,并输出一个 600 维的矩阵。最后,我们训练的最后一个 RBM 将接收 600 维的矩阵,并输出一个 500 维的矩阵。

我们将使用学习率为 1.0 来训练所有三个 RBM,每个训练 100 个周期,并使用批次大小为两百:

# Since we are training, set input as training data
inputX = np.array(X_train)

# Create list to hold our RBMs
rbm_list = []

# Define the parameters of the RBMs we will train
rbm_list.append(RBM(784,700,1.0,100,200))
rbm_list.append(RBM(700,600,1.0,100,200))
rbm_list.append(RBM(600,500,1.0,100,200))

现在让我们训练 RBM。我们将把训练好的 RBM 存储在名为 outputList 的列表中。

注意,我们使用我们之前定义的 rbm_output 函数来生成输出矩阵,换句话说,是后续我们训练的 RBM 的输入/可见层:

outputList = []
error_list = []
#For each RBM in our list
for i in range(0,len(rbm_list)):
    print('RBM', i+1)
    #Train a new one
    rbm = rbm_list[i]
    err = rbm.train(inputX)
    error_list.append(err)
    #Return the output layer
    outputX, reconstructedX, hiddenX = rbm.rbm_output(inputX)
    outputList.append(outputX)
    inputX = hiddenX

随着训练的进行,每个 RBM 的误差都在下降(参见图 11-1,11-2 和 11-3)。请注意,RBM 误差反映了给定 RBM 可见层重构数据与输入数据有多相似。

第一个 RBM 的重构误差

图 11-1. 第一个 RBM 的重构误差

第二个 RBM 的重构误差

图 11-2. 第二个 RBM 的重构误差

第三个 RBM 的重构误差

图 11-3. 第三个 RBM 的重构误差

检查特征探测器

现在让我们使用之前定义的rbm.show_features函数来查看每个 RBM 学到的特征:

rbm_shapes = [(28,28),(25,24),(25,20)]
for i in range(0,len(rbm_list)):
    rbm = rbm_list[i]
    print(rbm.show_features(rbm_shapes[i],
     "RBM learned features from MNIST", 56))

图 11-4 展示了各个 RBM 学到的特征。

如您所见,每个 RBM 从 MNIST 数据中学到的特征越来越抽象。第一个 RBM 的特征模糊地类似于数字,而第二个和第三个 RBM 的特征则越来越微妙且难以辨认。这在图像数据的特征探测器中是非常典型的;神经网络的深层逐渐识别原始图像中越来越抽象的元素。

RBM 的学习特征

图 11-4. RBM 的学习特征

查看生成的图像

在我们构建完整的 DBN 之前,让我们查看我们刚刚训练的某个 RBM 生成的一些图像。

为了简化问题,我们将原始的 MNIST 训练矩阵输入我们训练过的第一个 RBM 中,进行前向传播和反向传播,然后生成我们需要的图像。我们将比较 MNIST 数据集的前十张图像与新生成的图像:

inputX = np.array(X_train)
rbmOne = rbm_list[0]

print('RBM 1')
outputX_rbmOne, reconstructedX_rbmOne, hiddenX_rbmOne =
 rbmOne.rbm_output(inputX)
reconstructedX_rbmOne = pd.DataFrame(data=reconstructedX_rbmOne,
 index=X_train.index)
for j in range(0,10):
    example = j
    view_digit(reconstructedX, y_train, example)
    view_digit(X_train, y_train, example)

图 11-5 展示了第一个 RBM 生成的第一张图像与第一张原始图像的比较。

第一个 RBM 的第一张生成图像

图 11-5. 第一个 RBM 的第一张生成图像

如您所见,生成的图像与原始图像相似——两者都显示数字五。

让我们查看更多这样的图像,将 RBM 生成的图像与原始图像进行比较(参见 11-6 到 11-9 图)。

第一个 RBM 的第二张生成图像

图 11-6. 第一个 RBM 的第二张生成图像

第一个 RBM 的第三张生成图像

图 11-7. 第一个 RBM 的第三张生成图像

第一个 RBM 的第四张生成图像

图 11-8. 第一个 RBM 的第四张生成图像

第一个 RBM 的第五张生成图像

图 11-9. 第一个 RBM 的第五张生成图像

这些数字分别是零,四,一和九,并且生成的图像与原始图像看起来相似。

完整的 DBN

现在,让我们定义 DBN 类,它将接受我们刚刚训练的三个 RBM,并添加一个第四个 RBM,执行前向和后向传递,以完善基于 DBN 的生成模型。

首先,让我们定义类的超参数。这些包括原始输入大小,我们刚刚训练的第三个受限玻尔兹曼机(RBM)的输入大小,我们希望从深度置信网络(DBN)得到的最终输出大小,学习率,我们希望训练的周期数,用于训练的批量大小,以及我们刚刚训练的三个 RBM。和以前一样,我们需要生成权重矩阵,隐藏偏置和可见偏置的零矩阵:

class DBN(object):
    def __init__(self, original_input_size, input_size, output_size,
                 learning_rate, epochs, batchsize, rbmOne, rbmTwo, rbmThree):
        # Define hyperparameters
        self._original_input_size = original_input_size
        self._input_size = input_size
        self._output_size = output_size
        self.learning_rate = learning_rate
        self.epochs = epochs
        self.batchsize = batchsize
        self.rbmOne = rbmOne
        self.rbmTwo = rbmTwo
        self.rbmThree = rbmThree

        self.w = np.zeros([input_size, output_size], "float")
        self.hb = np.zeros([output_size], "float")
        self.vb = np.zeros([input_size], "float")

类似之前,我们将定义函数执行前向传递和后向传递,并从每个中获取样本:

def prob_h_given_v(self, visible, w, hb):
    return tf.nn.sigmoid(tf.matmul(visible, w) + hb)

def prob_v_given_h(self, hidden, w, vb):
    return tf.nn.sigmoid(tf.matmul(hidden, tf.transpose(w)) + vb)

def sample_prob(self, probs):
    return tf.nn.relu(tf.sign(probs - tf.random_uniform(tf.shape(probs))))

对于训练,我们需要权重,隐藏偏置和可见偏置的占位符。我们还需要用于以前和当前权重,隐藏偏置和可见偏置的矩阵:

def train(self, X):
    _w = tf.placeholder("float", [self._input_size, self._output_size])
    _hb = tf.placeholder("float", [self._output_size])
    _vb = tf.placeholder("float", [self._input_size])

    prv_w = np.zeros([self._input_size, self._output_size], "float")
    prv_hb = np.zeros([self._output_size], "float")
    prv_vb = np.zeros([self._input_size], "float")

    cur_w = np.zeros([self._input_size, self._output_size], "float")
    cur_hb = np.zeros([self._output_size], "float")
    cur_vb = np.zeros([self._input_size], "float")

我们将为可见层设置一个占位符。

接下来,我们将初始输入——可见层——通过之前训练的三个 RBM。这导致了输出forward,我们将其传递到我们作为这个 DBN 类一部分训练的第四个 RBM:

v0 = tf.placeholder("float", [None, self._original_input_size])
forwardOne = tf.nn.relu(tf.sign(tf.nn.sigmoid(tf.matmul(v0, \
                self.rbmOne.w) + self.rbmOne.hb) - tf.random_uniform( \
                tf.shape(tf.nn.sigmoid(tf.matmul(v0, self.rbmOne.w) + \
                self.rbmOne.hb)))))
forwardTwo = tf.nn.relu(tf.sign(tf.nn.sigmoid(tf.matmul(forwardOne, \
                self.rbmTwo.w) + self.rbmTwo.hb) - tf.random_uniform( \
                tf.shape(tf.nn.sigmoid(tf.matmul(forwardOne, \
                self.rbmTwo.w) + self.rbmTwo.hb)))))
forward = tf.nn.relu(tf.sign(tf.nn.sigmoid(tf.matmul(forwardTwo, \
                self.rbmThree.w) + self.rbmThree.hb) - \
                tf.random_uniform(tf.shape(tf.nn.sigmoid(tf.matmul( \
                forwardTwo, self.rbmThree.w) + self.rbmThree.hb)))))
h0 = self.sample_prob(self.prob_h_given_v(forward, _w, _hb))
v1 = self.sample_prob(self.prob_v_given_h(h0, _w, _vb))
h1 = self.prob_h_given_v(v1, _w, _hb)

我们将像之前一样定义对比散度:

positive_grad = tf.matmul(tf.transpose(forward), h0)
negative_grad = tf.matmul(tf.transpose(v1), h1)

update_w = _w + self.learning_rate * (positive_grad - negative_grad) / \
                tf.to_float(tf.shape(forward)[0])
update_vb = _vb +  self.learning_rate * tf.reduce_mean(forward - v1, 0)
update_hb = _hb +  self.learning_rate * tf.reduce_mean(h0 - h1, 0)

一旦我们通过这个 DBN 进行完整的前向传递——包括我们早先训练的三个 RBM 和最新的第四个 RBM——我们需要将第四个 RBM 的隐藏层输出再通过整个 DBN。

这需要通过第四个 RBM 进行反向传递,以及通过前三个 RBM 进行反向传递。我们还将像以前一样使用均方误差(MSE)。以下是反向传递发生的方式:

backwardOne = tf.nn.relu(tf.sign(tf.nn.sigmoid(tf.matmul(v1, \
                    self.rbmThree.w.T) + self.rbmThree.vb) - \
                    tf.random_uniform(tf.shape(tf.nn.sigmoid( \
                    tf.matmul(v1, self.rbmThree.w.T) + \
                    self.rbmThree.vb)))))
backwardTwo = tf.nn.relu(tf.sign(tf.nn.sigmoid(tf.matmul(backwardOne, \
                    self.rbmTwo.w.T) + self.rbmTwo.vb) - \
                    tf.random_uniform(tf.shape(tf.nn.sigmoid( \
                    tf.matmul(backwardOne, self.rbmTwo.w.T) + \
                    self.rbmTwo.vb)))))
backward = tf.nn.relu(tf.sign(tf.nn.sigmoid(tf.matmul(backwardTwo, \
                    self.rbmOne.w.T) + self.rbmOne.vb) - \
                    tf.random_uniform(tf.shape(tf.nn.sigmoid( \
                    tf.matmul(backwardTwo, self.rbmOne.w.T) + \
                    self.rbmOne.vb)))))

err = tf.reduce_mean(tf.square(v0 - backward))

这是 DBN 类的实际训练部分,与之前的 RBM 非常相似:

with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())

    for epoch in range(self.epochs):
        for start, end in zip(range(0, len(X), self.batchsize), \
                range(self.batchsize,len(X), self.batchsize)):
            batch = X[start:end]
            cur_w = sess.run(update_w, feed_dict={v0: batch, _w: \
                                prv_w, _hb: prv_hb, _vb: prv_vb})
            cur_hb = sess.run(update_hb, feed_dict={v0: batch, _w: \
                                prv_w, _hb: prv_hb, _vb: prv_vb})
            cur_vb = sess.run(update_vb, feed_dict={v0: batch, _w: \
                                prv_w, _hb: prv_hb, _vb: prv_vb})
            prv_w = cur_w
            prv_hb = cur_hb
            prv_vb = cur_vb
        error = sess.run(err, feed_dict={v0: X, _w: cur_w, _vb: \
                            cur_vb, _hb: cur_hb})
        print ('Epoch: %d' % epoch,'reconstruction error: %f' % error)
    self.w = prv_w
    self.hb = prv_hb
    self.vb = prv_vb

让我们定义函数来从 DBN 生成图像并展示特征。这些与之前的 RBM 版本类似,但我们将数据通过 DBN 类中的所有四个 RBM,而不仅仅是一个单独的 RBM:

def dbn_output(self, X):

    input_X = tf.constant(X)
    forwardOne = tf.nn.sigmoid(tf.matmul(input_X, self.rbmOne.w) + \
                               self.rbmOne.hb)
    forwardTwo = tf.nn.sigmoid(tf.matmul(forwardOne, self.rbmTwo.w) + \
                               self.rbmTwo.hb)
    forward = tf.nn.sigmoid(tf.matmul(forwardTwo, self.rbmThree.w) + \
                            self.rbmThree.hb)

    _w = tf.constant(self.w)
    _hb = tf.constant(self.hb)
    _vb = tf.constant(self.vb)

    out = tf.nn.sigmoid(tf.matmul(forward, _w) + _hb)
    hiddenGen = self.sample_prob(self.prob_h_given_v(forward, _w, _hb))
    visibleGen = self.sample_prob(self.prob_v_given_h(hiddenGen, _w, _vb))

    backwardTwo = tf.nn.sigmoid(tf.matmul(visibleGen, self.rbmThree.w.T) + \
                                self.rbmThree.vb)
    backwardOne = tf.nn.sigmoid(tf.matmul(backwardTwo, self.rbmTwo.w.T) + \
                                self.rbmTwo.vb)
    backward = tf.nn.sigmoid(tf.matmul(backwardOne, self.rbmOne.w.T) + \
                             self.rbmOne.vb)

    with tf.Session() as sess:
        sess.run(tf.global_variables_initializer())
        return sess.run(out), sess.run(backward)
def show_features(self, shape, suptitle, count=-1):
    maxw = np.amax(self.w.T)
    minw = np.amin(self.w.T)
    count = self._output_size if count == -1 or count > \
            self._output_size else count
    ncols = count if count < 14 else 14
    nrows = count//ncols
    nrows = nrows if nrows > 2 else 3
    fig = plt.figure(figsize=(ncols, nrows), dpi=100)
    grid = Grid(fig, rect=111, nrows_ncols=(nrows, ncols), axes_pad=0.01)

    for i, ax in enumerate(grid):
        x = self.w.T[i] if i<self._input_size else np.zeros(shape)
        x = (x.reshape(1, -1) - minw)/maxw
        ax.imshow(x.reshape(*shape), cmap=mpl.cm.Greys)
        ax.set_axis_off()

    fig.text(0.5,1, suptitle, fontsize=20, horizontalalignment='center')
    fig.tight_layout()
    plt.show()
    return

DBN 训练的工作原理

每个我们已经训练的三个 RBM 都有自己的权重矩阵,隐藏偏置向量和可见偏置向量。在作为 DBN 一部分训练的第四个 RBM 期间,我们不会调整这前三个 RBM 的权重矩阵,隐藏偏置向量和可见偏置向量。相反,我们将使用这前三个 RBM 作为 DBN 的固定组件。我们将仅调用这前三个 RBM 执行正向和反向传播(并使用这三个生成的数据样本)。

在训练 DBN 的第四个 RBM 时,我们只会调整第四个 RBM 的权重和偏置。换句话说,DBN 中的第四个 RBM 以前三个 RBM 的输出作为给定值,并执行前向和反向传播,学习生成模型,以使其生成的图像与原始图像之间的重构误差最小化。

训练 DBN 的另一种方法是允许 DBN 在执行整个网络的前向和反向传播时学习和调整所有四个 RBM 的权重。然而,DBN 的训练会非常昂贵(也许今天的计算机不算,但从 2006 年首次引入 DBN 的标准来看,肯定是如此)。

话虽如此,如果我们希望进行更细致的预训练,我们可以允许单个受限玻尔兹曼机(RBM)的权重在每次网络前向和反向传播的批次中进行调整。我们不会深入讨论这一点,但我鼓励你在自己的时间里进行实验。

训练 DBN

现在我们将训练 DBN。我们设置原始图像尺寸为 784,第三个 RBM 的输出尺寸为 500,DBN 的期望尺寸也为 500。我们将使用学习率为 1.0 进行 50 个 epochs 的训练,并使用批量大小为 200。最后,我们将前三个训练好的 RBM 称为 DBN 的一部分:

# Instantiate DBN Class
dbn = DBN(784, 500, 500, 1.0, 50, 200, rbm_list[0], rbm_list[1], rbm_list[2])

现在,让我们开始训练:

inputX = np.array(X_train)
error_list = []
error_list = dbn.train(inputX)

图 11-10 展示了训练过程中深度信念网络(DBN)的重构误差。

DBN 的重构误差

图 11-10. DBN 的重构误差

图 11-11 展示了 DBN 最后一层——第四个 RBM 的隐藏层——学到的特征。

DBN 中第四个 RBM 的学习特征

图 11-11. DBN 中第四个 RBM 的学习特征

重构误差和学习到的特征看起来都很合理,并且与我们之前分析的单独 RBM 的情况相似。

无监督学习如何帮助监督学习

到目前为止,我们所做的所有关于训练 RBM 和 DBN 的工作都涉及无监督学习。我们完全没有使用任何图像的标签。相反,我们通过从 50,000 个示例训练集中的原始 MNIST 图像中学习相关的潜在特征来构建生成模型。这些生成模型生成的图像看起来与原始图像相似(最小化重构误差)。

让我们退一步,以理解这种生成模型的用处。

请记住,世界上大多数数据都是无标签的。因此,尽管监督学习非常强大和有效,我们仍然需要无监督学习来帮助理解所有存在的无标签数据。仅靠监督学习是不够的。

为了展示无监督学习的有效性,想象一下,如果训练集中的 MNIST 图像只有 5000 张标记图像,而不是 50000 张标记图像,有监督学习的图像分类器的效果将大不如拥有 50000 张图像的有监督学习的图像分类器。我们拥有的标记数据越多,机器学习解决方案就越好。

无监督学习在这种情况下如何帮助?无监督学习能提供帮助的一种方式是生成新的带标签示例,以帮助补充最初的标记数据集。然后,有监督学习可以在一个更大的标记数据集上进行,从而获得更好的整体解决方案。

生成图像以构建更好的图像分类器

为了模拟无监督学习能够提供的这种好处,让我们将 MNIST 训练数据集缩减到仅有五千个标记示例。我们将把前五千个图像存储在一个名为inputXReduced的数据框中。

然后,从这五千张标记图像中,我们将使用刚刚构建的生成模型来生成新的图像,使用 DBN。我们将重复这个过程 20 次。换句话说,我们将生成五千个新图像,共创建一个包含十万个样本的数据集,所有这些数据都将被标记。从技术上讲,我们存储的是最终的隐藏层输出,而不是直接重构的图像,尽管我们也会存储重构的图像,以便尽快评估它们。

我们将这 100,000 个输出存储在名为generatedImages的 NumPy 数组中:

# Generate images and store them
inputXReduced = X_train.loc[:4999]
for i in range(0,20):
    print("Run ",i)
    finalOutput_DBN, reconstructedOutput_DBN = dbn.dbn_output(inputXReduced)
    if i==0:
        generatedImages = finalOutput_DBN
    else:
        generatedImages = np.append(generatedImages, finalOutput_DBN, axis=0)

我们将循环使用训练标签中的前五千个标签,称为y_train,重复 20 次以生成名为labels的标签数组:

# Generate a vector of labels for the generated images
for i in range(0,20):
    if i==0:
        labels = y_train.loc[:4999]
    else:
        labels = np.append(labels,y_train.loc[:4999])

最后,我们将在验证集上生成输出,这将用于评估我们即将构建的图像分类器:

# Generate images based on the validation set
inputValidation = np.array(X_validation)
finalOutput_DBN_validation, reconstructedOutput_DBN_validation = \
    dbn.dbn_output(inputValidation)

在使用我们刚生成的数据之前,让我们查看一些重构的图像:

# View reconstructed images
for i in range(0,10):
    example = i
    reconstructedX = pd.DataFrame(data=reconstructedOutput_DBN, \
                                  index=X_train[0:5000].index)
    view_digit(reconstructedX, y_train, example)
    view_digit(X_train, y_train, example)

DBN 的第一张生成图像

图 11-12. DBN 的第一张生成图像

正如您在图 11-12 中所看到的,生成的图像与原始图像非常相似——两者都显示数字五。与我们之前看到的由 RBM 生成的图像不同,这些更类似于原始的 MNIST 图像,包括像素化的部分。

让我们再查看几张这样的图像,以比较 DBN 生成的图像与原始 MNIST 图像(参见图 11-13 到图 11-16)。

DBN 的第二张生成图像

图 11-13. DBN 的第二张生成图像

DBN 的第三张生成图像

图 11-14. DBN 的第三张生成图像

DBN 的第四张生成图像

图 11-15. DBN 的第四张生成图像

DBN 的第五张生成图像

图 11-16. 深度信念网络生成的第五张图像

还要注意,DBN 模型(以及 RBM 模型)是生成型的,因此图像是使用随机过程生成的。图像不是使用确定性过程生成的,因此同一示例的图像在不同的 DBN 运行中会有所不同。

为了模拟这个过程,我们将采用第一张 MNIST 图像,并使用深度信念网络生成一张新图像,重复这个过程 10 次:

# Generate the first example 10 times
inputXReduced = X_train.loc[:0]
for i in range(0,10):
    example = 0
    print("Run ",i)
    finalOutput_DBN_fives, reconstructedOutput_DBN_fives = \
        dbn.dbn_output(inputXReduced)
    reconstructedX_fives = pd.DataFrame(data=reconstructedOutput_DBN_fives, \
                                        index=[0])
    print("Generated")
    view_digit(reconstructedX_fives, y_train.loc[:0], example)

正如您从图 11-17 到 11-21 所看到的,所有生成的图像都显示数字五,但它们的图像会因为使用相同的原始 MNIST 图像而有所不同。

数字五的第一和第二生成图像

图 11-17. 数字五的第一和第二生成图像

数字五的第三和第四生成图像

图 11-18. 数字五的第三和第四生成图像

数字五的第五和第六生成图像

图 11-19. 数字五的第五和第六生成图像

数字五的第七和第八生成图像

图 11-20. 数字五的第七和第八生成图像

数字五的第九和第十生成图像

图 11-21. 数字五的第九和第十生成图像

使用 LightGBM 的图像分类器

现在让我们使用本书前面介绍的监督学习算法构建一个图像分类器:梯度提升算法LightGBM

仅监督学习

第一个图像分类器仅依赖于前五千个标记的 MNIST 图像。这是从原始的 50,000 个标记的 MNIST 训练集中减少的集合;我们设计这个集合来模拟现实世界中标记示例相对较少的问题。由于本书前面已经深入讨论了梯度提升和 LightGBM 算法,因此我们在这里不会详细介绍。

让我们为算法设置参数:

predictionColumns = ['0','1','2','3','4','5','6','7','8','9']

params_lightGB = {
    'task': 'train',
    'application':'binary',
    'num_class':10,
    'boosting': 'gbdt',
    'objective': 'multiclass',
    'metric': 'multi_logloss',
    'metric_freq':50,
    'is_training_metric':False,
    'max_depth':4,
    'num_leaves': 31,
    'learning_rate': 0.1,
    'feature_fraction': 1.0,
    'bagging_fraction': 1.0,
    'bagging_freq': 0,
    'bagging_seed': 2018,
    'verbose': 0,
    'num_threads':16
}

接下来,我们将在 5,000 个标记的 MNIST 训练集(减少后的集合)上进行训练,并在 10,000 个标记的 MNIST 验证集上进行验证:

trainingScore = []
validationScore = []
predictionsLightGBM = pd.DataFrame(data=[], \
                        index=y_validation.index, \
                        columns=predictionColumns)

lgb_train = lgb.Dataset(X_train.loc[:4999], y_train.loc[:4999])
lgb_eval = lgb.Dataset(X_validation, y_validation, reference=lgb_train)
gbm = lgb.train(params_lightGB, lgb_train, num_boost_round=2000,
                   valid_sets=lgb_eval, early_stopping_rounds=200)

loglossTraining = log_loss(y_train.loc[:4999], \
    gbm.predict(X_train.loc[:4999], num_iteration=gbm.best_iteration))
trainingScore.append(loglossTraining)

predictionsLightGBM.loc[X_validation.index,predictionColumns] = \
    gbm.predict(X_validation, num_iteration=gbm.best_iteration)
loglossValidation = log_loss(y_validation,
    predictionsLightGBM.loc[X_validation.index,predictionColumns])
validationScore.append(loglossValidation)

print('Training Log Loss: ', loglossTraining)
print('Validation Log Loss: ', loglossValidation)

loglossLightGBM = log_loss(y_validation, predictionsLightGBM)
print('LightGBM Gradient Boosting Log Loss: ', loglossLightGBM)

下面的代码显示了这种仅监督学习解决方案的训练和验证 log loss:

Training Log Loss: 0.0018646953029132292
Validation Log Loss: 0.19124276982588717

下面的代码显示了这种仅监督学习图像分类解决方案的总体准确性:

predictionsLightGBM_firm = np.argmax(np.array(predictionsLightGBM), axis=1)
accuracyValidation_lightGBM = accuracy_score(np.array(y_validation), \
                                            predictionsLightGBM_firm)
print("Supervised-Only Accuracy: ", accuracyValidation_lightGBM)
Supervised-Only Accuracy: 0.9439

无监督和监督解决方案

现在,我们不再训练五千个标记的 MNIST 图像,而是训练来自 DBN 生成的 10 万张图像:

# Prepare DBN-based DataFrames for LightGBM use
generatedImagesDF = pd.DataFrame(data=generatedImages,index=range(0,100000))
labelsDF = pd.DataFrame(data=labels,index=range(0,100000))

X_train_lgb = pd.DataFrame(data=generatedImagesDF,
                           index=generatedImagesDF.index)
X_validation_lgb = pd.DataFrame(data=finalOutput_DBN_validation,
                                index=X_validation.index)
# Train LightGBM
trainingScore = []
validationScore = []
predictionsDBN = pd.DataFrame(data=[],index=y_validation.index,
                              columns=predictionColumns)

lgb_train = lgb.Dataset(X_train_lgb, labels)
lgb_eval = lgb.Dataset(X_validation_lgb, y_validation, reference=lgb_train)
gbm = lgb.train(params_lightGB, lgb_train, num_boost_round=2000,
                   valid_sets=lgb_eval, early_stopping_rounds=200)

loglossTraining = log_loss(labelsDF, gbm.predict(X_train_lgb, \
                            num_iteration=gbm.best_iteration))
trainingScore.append(loglossTraining)

predictionsDBN.loc[X_validation.index,predictionColumns] = \
    gbm.predict(X_validation_lgb, num_iteration=gbm.best_iteration)
loglossValidation = log_loss(y_validation,
    predictionsDBN.loc[X_validation.index,predictionColumns])
validationScore.append(loglossValidation)

print('Training Log Loss: ', loglossTraining)
print('Validation Log Loss: ', loglossValidation)

loglossDBN = log_loss(y_validation, predictionsDBN)
print('LightGBM Gradient Boosting Log Loss: ', loglossDBN)

下面的代码显示了这种无监督增强图像分类解决方案的 log loss:

Training Log Loss: 0.004145635328203315
Validation Log Loss: 0.16377638170016542

下面的代码显示了这种无监督增强图像分类解决方案的总体准确性:

DBN-Based Solution Accuracy: 0.9525

正如您所看到的,这个解决方案提高了近一个百分点,这是相当可观的。

结论

在第十章,我们介绍了第一类生成模型——限制玻尔兹曼机。在本章中,我们基于这一概念介绍了更先进的生成模型,称为深度信念网络,它由多个堆叠的 RBM 组成。

我们展示了深度玻尔兹曼机(DBNs)的工作原理——在纯无监督的情况下,DBN 学习数据的潜在结构,并利用其学习生成新的合成数据。根据新合成数据与原始数据的比较,DBN 改善其生成能力,以至于合成数据越来越像原始数据。我们还展示了 DBNs 生成的合成数据如何补充现有的标记数据集,通过增加整体训练集的大小来提高监督学习模型的性能。

我们开发的半监督解决方案利用了 DBNs(无监督学习)和梯度提升(监督学习),在我们所面对的 MNIST 图像分类问题中,其表现优于纯监督解决方案。

在第十二章,我们介绍了无监督学习(特别是生成建模)中的最新进展之一,即生成对抗网络。