Spark 深度学习实用指南(三)

原文:annas-archive.org/md5/ddd311fd2802b8b875714761e8af3c7e

译者:飞龙

协议:CC BY-NC-SA 4.0

第十二章:文本分析与深度学习

在上一章中,我们了解了自然语言处理NLP)的核心概念,然后我们通过 Scala 和 Apache Spark 中的一些实现示例,学习了两个开源库的应用,并了解了这些解决方案的优缺点。本章将通过实际案例展示使用 DL 进行 NLP 的实现(使用 Scala 和 Spark)。以下四个案例将被覆盖:

  • DL4J

  • TensorFlow

  • Keras 与 TensorFlow 后端

  • DL4J 和 Keras 模型导入

本章涵盖了关于每种 DL 方法的优缺点的考虑,以便读者可以了解在何种情况下某个框架比其他框架更受青睐。

动手实践 NLP 与 DL4J

我们将要检查的第一个示例是电影评论的情感分析案例,与上一章中展示的最后一个示例(动手实践 NLP with Spark-NLP部分)相同。不同之处在于,这里我们将结合 Word2Vec(en.wikipedia.org/wiki/Word2vec)和 RNN 模型。

Word2Vec 可以看作是一个只有两层的神经网络,它接受一些文本内容作为输入,然后返回向量。它不是一个深度神经网络,但它用于将文本转换为深度神经网络能够理解的数字格式。Word2Vec 非常有用,因为它可以在向量空间中将相似词汇的向量聚集在一起。它通过数学方式实现这一点。它在没有人工干预的情况下,创建了分布式的单词特征的数值表示。表示单词的向量被称为神经词嵌入。Word2Vec 训练词汇与输入文本中的邻近词汇之间的关系。它通过上下文来预测目标词汇(连续词袋模型CBOW))或使用一个词汇来预测目标上下文(跳字模型)。研究表明,当处理大型数据集时,第二种方法能够产生更精确的结果。如果分配给某个单词的特征向量不能准确预测它的上下文,那么该向量的组成部分就会发生调整。每个单词在输入文本中的上下文变成了“教师”,通过反馈错误进行调整。这样,通过上下文被认为相似的单词向量就会被推得更近。

用于训练和测试的数据集是大型电影评论数据集,可以在ai.stanford.edu/~amaas/data/sentiment/下载,且免费使用。该数据集包含 25,000 条热门电影评论用于训练,另外还有 25,000 条用于测试。

本示例的依赖项包括 DL4J NN、DL4J NLP 和 ND4J。

像往常一样,使用 DL4J 的NeuralNetConfiguration.Builder类来设置 RNN 配置,如下所示:

val conf: MultiLayerConfiguration = new NeuralNetConfiguration.Builder
   .updater(Updater.ADAM)
   .l2(1e-5)
   .weightInit(WeightInit.XAVIER)
   .gradientNormalization(GradientNormalization.ClipElementWiseAbsoluteValue)
   .gradientNormalizationThreshold(1.0)
   .list
   .layer(0, new GravesLSTM.Builder().nIn(vectorSize).nOut(256)
     .activation(Activation.TANH)
     .build)
   .layer(1, new RnnOutputLayer.Builder().activation(Activation.SOFTMAX)
     .lossFunction(LossFunctions.LossFunction.MCXENT).nIn(256).nOut(2).build)
   .pretrain(false).backprop(true).build

该网络由一个 Graves LSTM RNN 构成(更多细节请参见第六章,《递归神经网络》),加上 DL4J 特定的 RNN 输出层RnnOutputLayer。该输出层的激活函数是 SoftMax。

现在我们可以使用前面设置的配置来创建网络,如下所示:

val net = new MultiLayerNetwork(conf)
 net.init()
 net.setListeners(new ScoreIterationListener(1))

在开始训练之前,我们需要准备训练集,以使其准备好供使用。为此,我们将使用 Alex Black 的dataset iterator,该迭代器可以在 DL4J 的 GitHub 示例中找到(github.com/deeplearning4j/dl4j-examples/blob/master/dl4j-examples/src/main/java/org/deeplearning4j/examples/recurrent/word2vecsentiment/SentimentExampleIterator.java)。它是用 Java 编写的,因此已经被改编为 Scala 并添加到本书的源代码示例中。它实现了DataSetIterator接口(static.javadoc.io/org.nd4j/nd4j-api/1.0.0-alpha/org/nd4j/linalg/dataset/api/iterator/DataSetIterator.html),并且专门针对 IMDB 评论数据集。它的输入是原始的 IMDB 数据集(可以是训练集或测试集),以及一个wordVectors对象,然后生成准备好用于训练/测试的数据集。这个特定的实现使用了 Google News 300 预训练向量作为wordVectors对象;可以从github.com/mmihaltz/word2vec-GoogleNews-vectors/ GitHub 库中免费下载 GZIP 格式的文件。需要解压缩后才能使用。一旦提取,模型可以通过WordVectorSerializer类的loadStaticModel方法加载(static.javadoc.io/org.deeplearning4j/deeplearning4j-nlp/1.0.0-alpha/org/deeplearning4j/models/embeddings/loader/WordVectorSerializer.html),如下所示:

val WORD_VECTORS_PATH: String = getClass().getClassLoader.getResource("GoogleNews-vectors-negative300.bin").getPath
 val wordVectors = WordVectorSerializer.loadStaticModel(new File(WORD_VECTORS_PATH))

现在可以通过自定义数据集迭代器SentimentExampleIterator准备训练和测试数据:

val DATA_PATH: String = getClass.getClassLoader.getResource("aclImdb").getPath
 val train = new SentimentExampleIterator(DATA_PATH, wordVectors, batchSize, truncateReviewsToLength, true)
 val test = new SentimentExampleIterator(DATA_PATH, wordVectors, batchSize, truncateReviewsToLength, false)

然后,我们可以在 DL4J 和 Spark 中测试和评估模型,具体内容参见第六章,《递归神经网络》、第七章,《使用 Spark 训练神经网络》,以及第八章,《监控和调试神经网络训练》。请注意,本文中使用的 Google 模型非常大(约 3.5 GB),因此在训练该示例中的模型时,需考虑所需的资源(特别是内存)。

在这个第一个代码示例中,我们使用了 DL4J 主模块的常用 API,这些 API 通常用于不同用例场景中的不同 MNN。我们还在其中明确使用了 Word2Vec。无论如何,DL4J API 还提供了一些针对 NLP 的基础设施,这些设施是基于 ClearTK(cleartk.github.io/cleartk/)构建的,ClearTK 是一个开源的机器学习(ML)和自然语言处理(NLP)框架,适用于 Apache UIMA(uima.apache.org/)。在本节接下来展示的第二个示例中,我们将使用这些设施。

该第二个示例的依赖项是 DataVec、DL4J NLP 和 ND4J。尽管它们已通过 Maven 或 Gradle 正确加载为传递性依赖项,但以下两个库需要明确声明在项目依赖项中,以避免在运行时发生NoClassDefFoundError

groupId: com.google.guava
 artifactId: guava
 version: 19.0
groupId: org.apache.commons
 artifactId: commons-math3
 version: 3.4

一个包含大约 100,000 个通用句子的文件已作为此示例的输入。我们需要将其加载到我们的应用程序中,操作如下:

val filePath: String = new ClassPathResource("rawSentences.txt").getFile.getAbsolutePath

DL4J NLP 库提供了SentenceIterator接口(static.javadoc.io/org.deeplearning4j/deeplearning4j-nlp/1.0.0-alpha/org/deeplearning4j/text/sentenceiterator/SentenceIterator.html)以及多个实现。在这个特定的示例中,我们将使用BasicLineIterator实现(static.javadoc.io/org.deeplearning4j/deeplearning4j-nlp/1.0.0-alpha/org/deeplearning4j/text/sentenceiterator/BasicLineIterator.html),以便去除输入文本中每个句子开头和结尾的空格,具体操作如下:

val iter: SentenceIterator = new BasicLineIterator(filePath)

我们现在需要进行分词操作,将输入文本切分成单个词语。为此,我们使用DefaultTokenizerFactory实现(static.javadoc.io/org.deeplearning4j/deeplearning4j-nlp/1.0.0-alpha/org/deeplearning4j/text/tokenization/tokenizerfactory/DefaultTokenizerFactory.html),并设置CommomPreprocessorstatic.javadoc.io/org.deeplearning4j/deeplearning4j-nlp/1.0.0-alpha/org/deeplearning4j/text/tokenization/tokenizer/preprocessor/CommonPreprocessor.html)作为分词器,去除标点符号、数字和特殊字符,并将所有生成的词元强制转换为小写,具体操作如下:

val tokenizerFactory: TokenizerFactory = new DefaultTokenizerFactory
 tokenizerFactory.setTokenPreProcessor(new CommonPreprocessor)

模型现在可以构建,如下所示:

val vec = new Word2Vec.Builder()
   .minWordFrequency(5)
   .iterations(1)
   .layerSize(100)
   .seed(42)
   .windowSize(5)
   .iterate(iter)
   .tokenizerFactory(tokenizerFactory)
   .build

如前所述,我们使用的是 Word2Vec,因此模型是通过 Word2Vec.Builder 类 (static.javadoc.io/org.deeplearning4j/deeplearning4j-nlp/1.0.0-alpha/org/deeplearning4j/models/word2vec/Word2Vec.Builder.html) 构建的,设置为先前创建的分词器工厂。

让我们开始模型拟合:

vec.fit()

完成后,可以将词向量保存在文件中,具体如下:

WordVectorSerializer.writeWordVectors(vec, "wordVectors.txt")

WordVectorSerializer 工具类 (static.javadoc.io/org.deeplearning4j/deeplearning4j-nlp/1.0.0-alpha/org/deeplearning4j/models/embeddings/loader/WordVectorSerializer.html) 处理词向量的序列化和持久化。

可以通过以下方式测试模型:

val lst = vec.wordsNearest("house", 10)
 println("10 Words closest to 'house': " + lst)

生成的输出如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/e3123b10-3450-4244-9fa8-e00c7c0f457e.png

GloVe (en.wikipedia.org/wiki/GloVe_(machine_learning)),与 Word2Vec 类似,是一种分布式词表示模型,但采用了不同的方法。Word2Vec 从一个旨在预测相邻词语的神经网络中提取嵌入,而 GloVe 则直接优化嵌入。这样,两个词向量的乘积等于这两个词在一起出现次数的对数。例如,如果词语 catmouse 在文本中一共出现了 20 次,那么 (vec(cat) * vec(mouse)) = log(20)。DL4J NLP 库也提供了 GloVe 模型的实现,GloVe.Builder (static.javadoc.io/org.deeplearning4j/deeplearning4j-nlp/1.0.0-alpha/org/deeplearning4j/models/glove/Glove.Builder.html)。因此,这个示例可以适配到 GloVe 模型。与 Word2Vec 示例相同的包含约 100,000 个通用句子的文件作为新的输入。SentenceIterator 和分词方法没有变化(与 Word2Vec 示例相同)。不同之处在于构建的模型,如下所示:

val glove = new Glove.Builder()
   .iterate(iter)
   .tokenizerFactory(tokenizerFactory)
   .alpha(0.75)
   .learningRate(0.1)
   .epochs(25)
   .xMax(100)
   .batchSize(1000)
   .shuffle(true)
   .symmetric(true)
   .build

我们可以通过调用其 fit 方法来拟合模型,具体如下:

glove.fit()

拟合过程完成后,我们可以使用模型执行多项操作,例如查找两个词之间的相似度,具体如下:

val simD = glove.similarity("old", "new")
 println("old/new similarity: " + simD)

或者,找到与给定词语最相似的 n 个词:

val words: util.Collection[String] = glove.wordsNearest("time", 10)
 println("Nearest words to 'time': " + words)

产生的输出将如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/c8400fd5-a95a-4df1-af5f-ab92a0e621f5.png

在看到这最后两个例子后,你可能会想知道到底哪个模型更好,是 Word2Vec 还是 GloVe。其实没有绝对的赢家,这完全取决于数据。你可以选择一个模型并以某种方式训练它,使得最终编码的向量变得特定于模型工作所在的用例场景的领域。

用 TensorFlow 进行实践中的 NLP

在本节中,我们将使用 TensorFlow(Python)进行深度学习情感分析,使用与上一节第一个示例相同的大型电影评论数据集。本示例的前提是 Python 2.7.x、PIP 包管理器和 TensorFlow。在 JVM 中导入 Python 模型与 DL4J一节位于第十章的部署到分布式系统部分,涵盖了设置所需工具的详细信息。我们还将使用 TensorFlow Hub 库(www.tensorflow.org/hub/),这是为可重用的机器学习模块而创建的。需要通过pip安装,如下所示:

pip install tensorflow-hub

该示例还需要pandaspandas.pydata.org/)数据分析库,如下所示:

pip install pandas

导入必要的模块:

import tensorflow as tf
 import tensorflow_hub as hub
 import os
 import pandas as pd
 import re

接下来,我们定义一个函数,将所有文件从输入目录加载到 pandas DataFrame 中,如下所示:

def load_directory_data(directory):
   data = {}
   data["sentence"] = []
   data["sentiment"] = []
   for file_path in os.listdir(directory):
     with tf.gfile.GFile(os.path.join(directory, file_path), "r") as f:
       data["sentence"].append(f.read())
       data["sentiment"].append(re.match("\d+_(\d+)\.txt", file_path).group(1))
   return pd.DataFrame.from_dict(data)

然后,我们定义另一个函数来合并正面和负面评论,添加一个名为polarity的列,并进行一些随机打乱,如下所示:

def load_dataset(directory):
   pos_df = load_directory_data(os.path.join(directory, "pos"))
   neg_df = load_directory_data(os.path.join(directory, "neg"))
   pos_df["polarity"] = 1
   neg_df["polarity"] = 0
   return pd.concat([pos_df, neg_df]).sample(frac=1).reset_index(drop=True)

实现第三个函数来下载电影评论数据集,并使用load_dataset函数创建以下训练集和测试集 DataFrame:

def download_and_load_datasets(force_download=False):
   dataset = tf.keras.utils.get_file(
       fname="aclImdb.tar.gz",
       origin="http://ai.stanford.edu/~amaas/data/sentiment/aclImdb_v1.tar.gz",
       extract=True)

   train_df = load_dataset(os.path.join(os.path.dirname(dataset),
                                        "aclImdb", "train"))
   test_df = load_dataset(os.path.join(os.path.dirname(dataset),
                                       "aclImdb", "test"))

   return train_df, test_df

这个函数在第一次执行代码时会下载数据集。然后,除非你删除它们,否则后续执行将从本地磁盘获取它们。

这两个 DataFrame 是通过这种方式创建的:

train_df, test_df = download_and_load_datasets()

我们还可以将训练数据集的前几行漂亮地打印到控制台,以检查一切是否正常,如下所示:

print(train_df.head())

示例输出如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/0892372b-9b6d-40d6-a465-3292fffac720.png

现在我们已经有了数据,可以定义模型了。我们将使用Estimator API(www.tensorflow.org/guide/estimators),这是 TensorFlow 中的一个高级 API,旨在简化机器学习编程。Estimator提供了一些输入函数,作为 pandas DataFrame 的封装。所以,我们定义如下函数:train_input_fn,以在整个训练集上进行训练,并且不限制训练轮次:

train_input_fn = tf.estimator.inputs.pandas_input_fn(
     train_df, train_df["polarity"], num_epochs=None, shuffle=True)
predict_train_input_fn 

对整个训练集进行预测,执行以下操作:

predict_train_input_fn = tf.estimator.inputs.pandas_input_fn(
     train_df, train_df["polarity"], shuffle=False)

然后我们使用predict_test_input_fn对测试集进行预测:

predict_test_input_fn = tf.estimator.inputs.pandas_input_fn(
     test_df, test_df["polarity"], shuffle=False)

TensorFlow hub 库提供了一个特征列,它会对给定的输入文本特征应用一个模块,该特征的值是字符串,然后将模块的输出传递到下游。在这个示例中,我们将使用nnlm-en-dim128模块(tfhub.dev/google/nnlm-en-dim128/1),该模块已经在英文 Google News 200B 语料库上进行了训练。我们在代码中嵌入和使用该模块的方式如下:

embedded_text_feature_column = hub.text_embedding_column(
     key="sentence",
     module_spec="https://tfhub.dev/google/nnlm-en-dim128/1")

出于分类目的,我们使用 TensorFlow hub 库提供的DNNClassifierwww.tensorflow.org/api_docs/python/tf/estimator/DNNClassifier)。它扩展了Estimatorwww.tensorflow.org/api_docs/python/tf/estimator/Estimator),并且是 TensorFlow DNN 模型的分类器。所以,在我们的示例中,Estimator是这样创建的:

estimator = tf.estimator.DNNClassifier(
     hidden_units=[500, 100],
     feature_columns=[embedded_text_feature_column],
     n_classes=2,
     optimizer=tf.train.AdagradOptimizer(learning_rate=0.003))

请注意,我们将embedded_text_feature_column指定为特征列。两个隐藏层分别具有500100个节点。AdagradOptimizerDNNClassifier的默认优化器。

模型的训练可以通过一行代码实现,方法是调用我们的Estimator*的train方法,如下所示:

estimator.train(input_fn=train_input_fn, steps=1000);

由于这个示例使用的训练数据集大小为 25 KB,1,000 步相当于五个 epoch(使用默认的批量大小)。

训练完成后,我们可以对训练数据集进行预测,具体如下:

train_eval_result = estimator.evaluate(input_fn=predict_train_input_fn)
 print("Training set accuracy: {accuracy}".format(**train_eval_result))

测试数据集如下:

test_eval_result = estimator.evaluate(input_fn=predict_test_input_fn)
 print("Test set accuracy: {accuracy}".format(**test_eval_result))

这是应用程序的输出,显示了两种预测的准确性:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/19709820-20cd-46cc-81e6-039c059c3b41.png

我们还可以对模型进行评估,正如在第九章中所解释的,解释神经网络输出,在分类评估部分中,计算混淆矩阵以了解错误分类的分布。首先让我们定义一个函数来获取预测值,具体如下:

def get_predictions(estimator, input_fn):
   return [x["class_ids"][0] for x in estimator.predict(input_fn=input_fn)]

现在,从训练数据集开始创建混淆矩阵,具体如下:

with tf.Graph().as_default():
   cm = tf.confusion_matrix(train_df["polarity"],
                            get_predictions(estimator, predict_train_input_fn))
   with tf.Session() as session:
     cm_out = session.run(cm)

然后,将其归一化,使每行的总和等于1,具体如下:

cm_out = cm_out.astype(float) / cm_out.sum(axis=1)[:, np.newaxis]

屏幕上显示的混淆矩阵输出将如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/1a61cf30-383f-406d-aafe-31d1bff21dd3.png

然而,你也可以使用你选择的 Python 图表库以更优雅的方式呈现它。

你可能已经注意到,尽管这段代码很简洁且不需要高级的 Python 知识,但它并不是机器学习(ML)和深度学习(DL)初学者的易入门点,因为 TensorFlow 隐式要求对 ML 概念有一定了解,才能理解其 API。与 DL4J API 比较时,你可以明显感觉到这种差异。

使用 Keras 和 TensorFlow 后端进行实践 NLP

如第十章《在分布式系统上部署》中所述,在使用 DL4J 在 JVM 中导入 Python 模型部分,当在 Python 中进行深度学习时,TensorFlow 的替代方案是 Keras。它可以作为一个高层 API,在 TensorFlow 的支持下使用。在本节中,我们将学习如何在 Keras 中进行情感分析,最后,我们将比较此实现与之前 TensorFlow 中的实现。

我们将使用与前面通过 DL4J 和 TensorFlow 实现相同的 IMDB 数据集(25,000 个训练样本和 25,000 个测试样本)。此示例的先决条件与 TensorFlow 示例相同(Python 2.7.x,PIP 包管理器和 TensorFlow),当然还需要 Keras。Keras 代码模块内置了该数据集:

from keras.datasets import imdb

所以,我们只需要设置词汇表的大小并从那里加载数据,而不是从其他外部位置加载,如下所示:

vocabulary_size = 5000

 (X_train, y_train), (X_test, y_test) = imdb.load_data(num_words = vocabulary_size)

下载完成后,您可以打印下载评论的样本以供检查,如下所示:

print('---review---')
 print(X_train[6])
 print('---label---')
 print(y_train[6])

输出结果如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/d6c6c8d4-2558-41b2-bd8c-f261f020c74e.png

您可以看到,在这个阶段,评论已作为整数序列存储,这些整数是预先分配给单个单词的 ID。另外,标签是一个整数(0 表示负面,1 表示正面)。不过,您仍然可以通过使用imdb.get_word_index()方法返回的字典,将下载的评论映射回它们原始的单词,如下所示:

word2id = imdb.get_word_index()
 id2word = {i: word for word, i in word2id.items()}
 print('---review with words---')
 print([id2word.get(i, ' ') for i in X_train[6]])
 print('---label---')
 print(y_train[6])

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/f6aedc98-82c7-44d2-addf-c3a8c3f67bcb.png

在前面的截图中,您可以看到输入评论中使用的单词的返回字典。我们将使用 RNN 模型进行此示例。为了向模型输入数据,所有输入数据的长度必须相同。通过查看下载评论的最大和最小长度(以下是获取此信息的代码及其输出):

print('Maximum review length: {}'.format(
 len(max((X_train + X_test), key=len))))
 print('Minimum review length: {}'.format(
 len(min((X_test + X_test), key=len))))

输出结果如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/3517618f-f769-4a64-a8e6-e54fb4178429.png

我们可以看到它们的长度不完全相同。因此,我们需要将最大评论长度限制为 500 个单词,比如通过截断较长的评论,并用零填充较短的评论。这可以通过sequence.pad_sequences Keras 函数实现,如下所示:

from keras.preprocessing import sequence

 max_words = 500
 X_train = sequence.pad_sequences(X_train, maxlen=max_words)
 X_test = sequence.pad_sequences(X_test, maxlen=max_words)

让我们设计 RNN 模型,如下所示:

from keras import Sequential
 from keras.layers import Embedding, LSTM, Dense, Dropout

 embedding_size=32
 model=Sequential()
 model.add(Embedding(vocabulary_size, embedding_size, input_length=max_words))
 model.add(LSTM(100))
 model.add(Dense(1, activation='sigmoid'))

这是一个简单的 RNN 模型,包含三层:嵌入层、LSTM 层和全连接层,如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/d5d01892-b8c9-4725-8cc3-0d848814f30e.png

此模型的输入是一个最大长度为500的整数单词 ID 序列,输出是一个二进制标签(01)。

此模型的学习过程配置可以通过其compile方法来完成,如下所示:

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

在设置好批量大小和训练周期数之后,如下所示:

batch_size = 64
 num_epochs = 3

我们可以开始训练,如下所示:

X_valid, y_valid = X_train[:batch_size], y_train[:batch_size]
 X_train2, y_train2 = X_train[batch_size:], y_train[batch_size:]

 model.fit(X_train2, y_train2, validation_data=(X_valid, y_valid), batch_size=batch_size, epochs=num_epochs)

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/478ba6b3-4b25-4568-84a7-955ff745d274.png

当训练完成后,我们可以使用测试数据集评估模型的准确性,方法如下:

scores = model.evaluate(X_test, y_test, verbose=0)
 print('Test accuracy:', scores[1])

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/0cf6ea34-cc97-4000-b328-39e87834b7ee.png

查看这个示例的代码,你应该已经注意到,相较于之前的 TensorFlow 示例,这个示例更高层次,开发时的重点主要放在特定问题模型的实现细节上,而不是其背后的机器学习/深度学习机制。

使用 Keras 模型导入到 DL4J 的实战 NLP

在第十章,在分布式系统上部署在 JVM 中使用 DL4J 导入 Python 模型部分,我们学习了如何将现有的 Keras 模型导入到 DL4J 中,并在 JVM 环境中使用它们进行预测或重新训练。

这适用于我们在使用 Keras 和 TensorFlow 后端的实战 NLP部分中实现并训练的模型,我们使用 Keras 和 TensorFlow 作为后端。我们需要修改该示例的代码,通过以下方式将模型序列化为 HDF5 格式:

model.save('sa_rnn.h5')

生成的sa_rnn.h5文件需要被复制到 Scala 项目的资源文件夹中。项目的依赖项包括 DataVec API、DL4J 核心、ND4J 以及 DL4J 模型导入库。

我们需要按照第 12.1 节中解释的方式导入并转换大型电影评论数据库,如果我们希望通过 DL4J 重新训练模型。然后,我们需要按如下方式编程导入 Keras 模型:

val saRnn = new ClassPathResource("sa_rnn.h5").getFile.getPath
 val model = KerasModelImport.importKerasSequentialModelAndWeights(saRnn)

最后,我们可以通过调用model(这是MultiLayerNetwork的实例,和在 DL4J 中的常见做法一样)的predict方法,传入输入数据作为 ND4J DataSet(static.javadoc.io/org.nd4j/nd4j-api/1.0.0-alpha/org/nd4j/linalg/dataset/api/DataSet.html)来进行预测。

总结

本章结束了对 Scala 实现过程的 NLP 解释。在本章和前一章中,我们评估了这种编程语言的不同框架,并详细列出了每种框架的优缺点。本章的重点主要放在了深度学习方法(DL)在 NLP 中的应用。为此,我们介绍了一些 Python 的替代方案,并强调了这些 Python 模型在 JVM 环境中与 DL4J 框架的潜在集成。此时,读者应该能够准确评估出哪些方案最适合他/她的特定 NLP 应用案例。

从下一章开始,我们将深入学习卷积和卷积神经网络(CNN)如何应用于图像识别问题。通过展示不同框架(包括 DL4J、Keras 和 TensorFlow)的不同实现,将解释图像识别。

第十三章:卷积

前两章介绍了通过 RNN/LSTM 在 Apache Spark 中进行的 NLP 实际用例实现。在本章及接下来的章节中,我们将做类似的事情,探讨 CNN 如何应用于图像识别和分类。本章特别涉及以下主题:

  • 从数学和深度学习的角度快速回顾卷积是什么

  • 现实问题中物体识别的挑战与策略

  • 卷积在图像识别中的应用,以及通过深度学习(卷积神经网络,CNN)实践中图像识别用例的实现,采用相同的方法,但使用以下两种不同的开源框架和编程语言:

    • Keras(使用 TensorFlow 后端)在 Python 中的实现

    • DL4J(及 ND4J)在 Scala 中的实现

卷积

第五章,卷积神经网络,介绍了 CNN 的理论,当然卷积也是其中的一部分。在进入物体识别之前,让我们从数学和实际的角度回顾一下这个概念。在数学中,卷积是对两个函数的操作,生成一个第三个函数,该函数是前两个函数的乘积积分结果,其中一个函数被翻转:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/40f744d1-b124-4366-b7a9-e4e3b99513a4.png

卷积在 2D 图像处理和信号过滤中被广泛应用。

为了更好地理解幕后发生了什么,这里是一个简单的 Python 代码示例,使用 NumPy 进行 1D 卷积(www.numpy.org/):

import numpy as np

x = np.array([1, 2, 3, 4, 5])
y = np.array([1, -2, 2])
result = np.convolve(x, y)
print result

这会产生如下结果:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/5543edab-f7f9-41ec-ad54-c2ba7fd928ec.png

让我们看看xy数组之间的卷积如何产生该结果。convolve函数首先做的事情是水平翻转y数组:

[1, -2, 2]变为[2, -2, 1]

然后,翻转后的y数组滑动在x数组上:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/28dfda6e-a187-41a7-a2c5-4d899b4dd754.png

这就是如何生成result数组[ 1 0 1 2 3 -2 10]的。

2D 卷积使用类似机制发生。以下是一个简单的 Python 代码示例,使用 NumPy:

import numpy as np
from scipy import signal

a = np.matrix('1 3 1; 0 -1 1; 2 2 -1')
print(a)
w = np.matrix('1 2; 0 -1')
print(w)

f = signal.convolve2d(a, w)
print(f)

这次,使用了 SciPy(www.scipy.org/)中的signal.convolve2d函数来执行卷积。前面代码的结果如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/e61da606-2f9b-4d33-840c-9658d42569af.png

当翻转后的矩阵完全位于输入矩阵内部时,结果被称为valid卷积。通过这种方式计算 2D 卷积,只获取有效结果,如下所示:

f = signal.convolve2d(a, w, 'valid')

这将产生如下输出:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/7e17957f-f56d-4d09-823e-9100dcc1239e.png

以下是这些结果的计算方式。首先,w数组被翻转:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/dcc92c16-742f-4472-8959-e64f4532b799.png变为https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/a8f37131-695b-44d2-822e-ae0ebdf2bc11.png

然后,和 1D 卷积相同,a 矩阵的每个窗口与翻转的 w 矩阵逐元素相乘,结果最终按如下方式求和:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/77311521-ca82-42ee-b893-4c1877dcedc5.png    (1 x -1) + (0 x 3) + (0 x 2) + (-1 x 1) = -2

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/e6c864b0-1bca-4d37-894d-f4c9471a8cad.png    (3 x -1) + (1 x 0) + (-1 x 2) + (1 x 1) = -4

依此类推。

物体识别策略

本节介绍了在数字图像中实现自动物体识别时使用的不同计算技术。首先,我们给出物体识别的定义。简而言之,它是任务:在场景的 2D 图像中找到并标记对应场景内物体的部分。以下截图展示了由人类用铅笔手动执行的物体识别示例:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/4cc4dd9a-cc15-443d-94b4-439444a02389.png

图 13.1:手动物体检测示例

图像已被标记和标签化,显示可以识别为香蕉和南瓜的水果。这与计算物体识别过程完全相同;可以简单地认为它是绘制线条、勾画图像区域的过程,最后为每个结构附上与其最匹配的模型标签。

在物体识别中,必须结合多种因素,如场景上下文的语义或图像中呈现的信息。上下文在图像解读中特别重要。让我们先看看以下截图:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/1b8e0b43-cf2f-40f6-82eb-727ab48b7242.png

图 13.2:孤立物体(无上下文)

几乎不可能单独识别图像中心的物体。现在让我们看看接下来的截图,其中同一物体出现在原始图像中的位置:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/93026019-5f1b-47e6-809e-65501ef207f1.png

图 13.3:图 13.2 中物体的原始上下文

如果不提供进一步的信息,仍然很难识别该物体,但相比于图 13.2,难度要小一些。给定前面截图中图像是电路板的上下文信息,初始物体更容易被识别为一个极化电容器。文化背景在正确解读场景中起着关键作用。

现在我们考虑第二个示例(如下截图所示),一个显示楼梯间的一致性 3D 图像:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/b09f73bd-f417-4f95-a7e1-11367ab01c0d.png

图 13.4:显示楼梯间的一致性 3D 图像

通过改变该图像中的光线,最终结果可能使得眼睛(以及计算机)更难看到一致的 3D 图像(如下图所示):

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/c7131e66-53d6-46e4-b912-0c45380910f9.png

图 13.5:在图 13.4 中应用不同光线后的结果

与原始图像(图 13.3)相比,它的亮度和对比度已经被修改(如以下截图所示):

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/6bc78e8e-316f-4388-9c29-fcf3c04657eb.png

图 13.6:图 13.3 中的图像,具有改变的亮度和对比度

眼睛仍然能够识别三维的阶梯。然而,使用与原始图像不同的亮度和对比度值,图像如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/f85f93fe-1ea7-412c-b1e7-b0a55ab6495f.png

图 13.7:图 13.3 中的图像,具有不同的亮度和对比度

几乎无法识别出相同的图像。我们学到的是,尽管前面截图中的修饰图像保留了原始图像中的重要视觉信息(图 13.3),但图 13.4和前面的截图中的图像由于修饰去除了三维细节,变得更加难以解读。所提供的例子证明,计算机(就像人眼一样)需要合适的上下文模型才能成功完成物体识别和场景解读。

物体识别的计算策略可以根据其对复杂图像数据或复杂模型的适用性进行分类。数字图像中的数据复杂度对应于其信噪比。具有语义歧义的图像对应于复杂(或嘈杂)的数据。图像中包含完美轮廓的模型实例数据被称为简单数据。具有较差分辨率、噪声或其他类型异常数据,或容易混淆的虚假模型实例,被称为复杂数据。模型复杂度通过图像中数据结构的细节级别以及确定数据形式所需的技术来表示。如果一个模型通过简单的标准定义(例如,单一形状模板或优化一个隐式包含形状模型的单一函数),那么可能不需要其他上下文来将模型标签附加到给定的场景中。但是,在许多原子模型组件必须组合或某种方式按层次关系建立以确认所需模型实例的存在时,需要复杂的数据结构和非平凡的技术。

基于前面的定义,物体识别策略可以分为四大类,如下所示:

  • 特征向量分类:这依赖于对象图像特征的一个简单模型。通常,它仅应用于简单数据。

  • 拟合模型到光度数据:当简单的模型足够用,但图像的光度数据存在噪声和歧义时,应用此方法。

  • 拟合模型到符号结构:当需要复杂的模型时应用,但可以通过简单数据准确推断可靠的符号结构。这些方法通过匹配表示全局对象部件之间关系的数据结构,来寻找对象的实例。

  • 组合策略:在数据和所需模型实例都很复杂时应用。

本书中详细介绍的主要开源框架所提供的用于构建和训练 CNNs(卷积神经网络)进行对象识别的可用 API 实现时,已考虑到这些因素和策略。尽管这些 API 是非常高层次的,但在选择模型的适当隐藏层组合时,应该采取相同的思维方式。

卷积在图像识别中的应用

在这一部分,我们将通过实现一个图像识别模型来动手实践,同时考虑本章第一部分中讨论的相关事项。我们将使用两种不同的框架和编程语言实现相同的用例。

Keras 实现

我们将要实现的第一个对象识别是在 Python 中使用 Keras 框架进行的。为了训练和评估模型,我们将使用一个名为 CIFAR-10 的公共数据集 (www.cs.toronto.edu/~kriz/cifar.html)。它包含 60,000 张(50,000 张用于训练,10,000 张用于测试)小的(32 x 32 像素)彩色图像,分为 10 类(飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船和卡车)。这 10 个类别是互斥的。CIFAR-10 数据集(163 MB)可以从 www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz 免费下载。

实现此功能的前提条件是 Python 2.7.x、Keras、TensorFlow(作为 Keras 的后端使用)、NumPy 以及 scikit-learn (scikit-learn.org/stable/index.html),这是一个用于机器学习的开源工具。第十章,在分布式系统上部署,涵盖了为 Keras 和 TensorFlow 设置 Python 环境的详细信息。scikit-learn 可以按以下方式安装:

sudo pip install scikit-learn

首先,我们需要导入所有必要的 NumPy、Keras 和 scikit-learn 命名空间和类,如下所示:

import numpy as np
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import Dropout
from keras.layers import Flatten
from keras.constraints import maxnorm
from keras.optimizers import SGD
from keras.layers.convolutional import Conv2D
from keras.layers.convolutional import MaxPooling2D
from keras.utils import np_utils
from keras.datasets import cifar10
from keras import backend as K
from sklearn.model_selection import train_test_split

现在,我们需要加载 CIFAR-10 数据集。不需要单独下载,Keras 提供了一个可以通过编程方式下载它的功能,如下所示:

K.set_image_dim_ordering('th')
 (X_train, y_train), (X_test, y_test) = cifar10.load_data()

load_data 函数在第一次执行时会下载数据集。后续的运行将使用已下载到本地的数据集。

我们通过常量值初始化 seed,以确保结果是可重复的,如下所示:

seed = 7
 np.random.seed(seed)

输入数据集的像素值范围为 0 到 255(每个 RGB 通道)。我们可以通过将值除以 255.0 来将数据归一化到 0 到 1 的范围,然后执行以下操作:

X_train = X_train.astype('float32')
 X_test = X_test.astype('float32')

 X_train = X_train / 255.0
 X_test = X_test / 255.0

我们可以使用独热编码(one-hot encoding)将输出变量转换为二进制矩阵(因为它们被定义为整数向量,范围在 0 到 1 之间,针对每个 10 类),如下所示:

y_train = np_utils.to_categorical(y_train)
 y_test = np_utils.to_categorical(y_test)
 num_classes = y_test.shape[1]

让我们开始实现模型。首先实现一个简单的 CNN,验证它的准确性,必要时我们将使模型更加复杂。以下是可能的第一次实现:

model = Sequential()
model.add(Conv2D(32,(3,3), input_shape = (3,32,32), padding = 'same', activation = 'relu'))
model.add(Dropout(0.2))
model.add(Conv2D(32,(3,3), padding = 'same', activation = 'relu'))
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Conv2D(64,(3,3), padding = 'same', activation = 'relu'))
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Flatten())
model.add(Dropout(0.2))
model.add(Dense(512,activation='relu',kernel_constraint=maxnorm(3)))
model.add(Dropout(0.2))
model.add(Dense(num_classes, activation='softmax'))

你可以在训练开始前在控制台输出中看到模型层的详细信息(请参见以下截图):

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/0ec53bf4-5723-433c-bf95-a12a27ca5ffa.png

该模型是一个Sequential模型。从前面的输出可以看到,输入层是卷积层,包含 32 个大小为 3 x 3 的特征图,并使用修正线性单元ReLU)激活函数。为了减少过拟合,对输入应用了 20% 的 dropout,接下来的层是第二个卷积层,具有与输入层相同的特征。然后,我们设置了一个大小为 2 x 2 的最大池化层。接着,添加了第三个卷积层,具有 64 个大小为 3 x 3 的特征图和 ReLU 激活函数,并设置了第二个大小为 2 x 2 的最大池化层。在第二个最大池化层后,我们加入一个 Flatten 层,并应用 20% 的 dropout,然后将输出传递到下一个层,即具有 512 个单元和 ReLU 激活函数的全连接层。在输出层之前,我们再应用一次 20% 的 dropout,输出层是另一个具有 10 个单元和 Softmax 激活函数的全连接层。

现在我们可以定义以下训练属性(训练轮数、学习率、权重衰减和优化器,在此特定情况下已设置为随机梯度下降SGD)):

epochs = 25
 lrate = 0.01
 decay = lrate/epochs
 sgd = SGD(lr=lrate, momentum=0.9, decay=decay, nesterov=False)

配置模型的训练过程,如下所示:

model.compile(loss='categorical_crossentropy', optimizer=sgd, metrics=['accuracy'])

现在可以使用 CIFAR-10 训练数据开始训练,如下所示:

model.fit(X_train, y_train, validation_data=(X_test, y_test), epochs=epochs, batch_size=32)

当完成时,可以使用 CIFAR-10 测试数据进行评估,如下所示:

scores = model.evaluate(X_test,y_test,verbose=0)
 print("Accuracy: %.2f%%" % (scores[1]*100))

该模型的准确率大约为75%,如以下截图所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/ab1124af-18fb-4c99-9c6c-795166d338f6.png

结果不是很好。我们已经在 25 个训练轮次上执行了训练,轮数比较少。因此,当训练轮次增多时,准确率会有所提高。不过,首先让我们看看通过改进 CNN 模型、使其更深,能否提高结果。添加以下两个额外的导入:

from keras.layers import Activation
 from keras.layers import BatchNormalization

对之前实现的代码的唯一更改是网络模型。以下是新的模型:

model = Sequential()
model.add(Conv2D(32, (3,3), padding='same', input_shape=x_train.shape[1:]))
model.add(Activation('elu'))
model.add(BatchNormalization())
model.add(Conv2D(32, (3,3), padding='same'))
model.add(Activation('elu'))
model.add(BatchNormalization())
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Dropout(0.2))

model.add(Conv2D(64, (3,3), padding='same'))
model.add(Activation('elu'))
model.add(BatchNormalization())
model.add(Conv2D(64, (3,3), padding='same'))
model.add(Activation('elu'))
model.add(BatchNormalization())
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Dropout(0.3))

model.add(Conv2D(128, (3,3), padding='same'))
model.add(Activation('elu'))
model.add(BatchNormalization())
model.add(Conv2D(128, (3,3), padding='same'))
model.add(Activation('elu'))
model.add(BatchNormalization())
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Dropout(0.4))

model.add(Flatten())
model.add(Dense(num_classes, activation='softmax'))

基本上,我们所做的就是重复相同的模式,每种模式使用不同数量的特征图(32、64 和 128)。添加层的优势在于每一层都会学习不同抽象级别的特征。在我们的例子中,训练一个 CNN 来识别物体时,我们可以看到第一层会训练自己识别基本特征(例如,物体的边缘),接下来的层会训练自己识别形状(可以认为是边缘的集合),然后的层会训练自己识别形状集合(以 CIFAR-10 数据集为例,这些可能是腿、翅膀、尾巴等),接下来的层会学习更高阶的特征(物体)。多个层更有利于因为它们能够学习从输入(原始数据)到高层分类之间的所有中间特征:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/0f26b44d-ca4c-4fdf-917d-5911f867b7e9.png

再次运行训练并进行该新模型的评估,结果是80.57%

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/45d9b0f4-7b69-4730-83a1-2f9e53d2fb5c.png

与先前的模型相比,这是一个合理的改进,考虑到我们目前只进行了 25 个 epoch 的训练。但现在,让我们看看是否可以通过图像数据增强来进一步提升性能。通过查看训练数据集,我们可以看到图像中的物体位置发生了变化。通常情况下,数据集中的图像会有不同的条件(例如亮度、方向等)。我们需要通过使用额外修改过的数据来训练神经网络来应对这些情况。考虑以下简单示例:一个仅包含两类的汽车图像训练数据集,大众甲壳虫和保时捷 Targa。假设所有的大众甲壳虫汽车都排列在左侧,如下图所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/6aa90be6-7d08-4d19-9a87-489635148c0e.png

图 13.8:大众甲壳虫训练图像

然而,所有的保时捷 Targa 汽车都排列在右侧,如下图所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/26cb8adb-cec1-41c0-bf25-d81bc5b345f5.png

图 13.9:保时捷 Targa 训练图像

在完成训练并达到较高准确率(90%或 95%)后,输入以下截图所示的图像进行模型预测:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/804ed180-0143-44c5-8588-4590f5b151fe.png

图 13.10:大众甲壳虫输入图像

这里存在一个具体的风险,可能会将这辆车误分类为 Porsche Targa。为了避免这种情况,我们需要减少训练数据集中无关特征的数量。以这辆车为例,我们可以做的一件事是将训练数据集中的图像水平翻转,使它们朝向另一侧。经过再次训练神经网络并使用这个新数据集后,模型的表现更有可能符合预期。数据增强可以在离线(适用于小数据集)或在线(适用于大数据集,因为变换应用于喂给模型的小批次数据)进行。让我们尝试在本节示例的最新模型实现中,使用 Keras 中的 ImageDataGenerator 类进行程序化的在线数据增强,方法如下:

from keras.preprocessing.image import ImageDataGenerator

datagen = ImageDataGenerator(
    rotation_range=15,
    width_shift_range=0.1,
    height_shift_range=0.1,
    horizontal_flip=True,
    )
datagen.fit(X_train)

然后在拟合模型时使用它,如下所示:

batch_size = 64

model.fit_generator(datagen.flow(X_train, y_train, batch_size=batch_size),\
                 steps_per_epoch=X_train.shape[0] // batch_size,epochs=125,\
                 verbose=1,validation_data=(X_test,y_test),callbacks=[LearningRateScheduler(lr_schedule)])

在开始训练之前,还需要做一件事,那就是对模型的卷积层应用一个核正则化器(keras.io/regularizers/),如下所示:

weight_decay = 1e-4
model = Sequential()
model.add(Conv2D(32, (3,3), padding='same', kernel_regularizer=regularizers.l2(weight_decay), input_shape=X_train.shape[1:]))
model.add(Activation('elu'))
model.add(BatchNormalization())
model.add(Conv2D(32, (3,3), padding='same', kernel_regularizer=regularizers.l2(weight_decay)))
model.add(Activation('elu'))
model.add(BatchNormalization())
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Dropout(0.2))

model.add(Conv2D(64, (3,3), padding='same', kernel_regularizer=regularizers.l2(weight_decay)))
model.add(Activation('elu'))
model.add(BatchNormalization())
model.add(Conv2D(64, (3,3), padding='same', kernel_regularizer=regularizers.l2(weight_decay)))
model.add(Activation('elu'))
model.add(BatchNormalization())
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Dropout(0.3))

model.add(Conv2D(128, (3,3), padding='same', kernel_regularizer=regularizers.l2(weight_decay)))
model.add(Activation('elu'))
model.add(BatchNormalization())
model.add(Conv2D(128, (3,3), padding='same', kernel_regularizer=regularizers.l2(weight_decay)))
model.add(Activation('elu'))
model.add(BatchNormalization())
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Dropout(0.4))

model.add(Flatten())
model.add(Dense(num_classes, activation='softmax'))

正则化器允许我们在网络优化过程中对层参数应用惩罚(这些惩罚会被纳入损失函数中)。

在这些代码更改后,使用相对较小的训练轮次(64)和基本的图像数据增强来训练模型。下图显示,准确率提高到了接近 84%:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/941edd1f-b724-4338-8b23-102e12c4aea4.png

通过训练更多的轮次,模型的准确率可能会增加到大约 90% 或 91%。

DL4J 实现

我们要做的第二个物体识别实现是基于 Scala,并涉及到 DL4J 框架。为了训练和评估模型,我们仍然使用 CIFAR-10 数据集。本项目的依赖项包括 DataVec 数据图像、DL4J、NN 和 ND4J,以及 Guava 19.0 和 Apache commons math 3.4。

如果你查看 CIFAR-10 数据集下载页面(见下图),你会发现有专门为 Python、MatLab 和 C 编程语言提供的归档文件,但没有针对 Scala 或 Java 的归档文件:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/9af22061-e703-445e-9694-c9380aee9369.png

图 13.11:CIFAR-10 数据集下载页面

我们不需要单独下载并转换数据集用于 Scala 应用;DL4J 数据集库提供了 org.deeplearning4j.datasets.iterator.impl.CifarDataSetIterator 迭代器,可以编程获取训练集和测试集,如下所示:

val trainDataSetIterator =
                 new CifarDataSetIterator(2, 5000, true)
 val testDataSetIterator =
                 new CifarDataSetIterator(2, 200, false)

CifarDataSetIterator 构造函数需要三个参数:批次数、样本数以及一个布尔值,用于指定数据集是用于训练(true)还是测试(false)。

现在我们可以定义神经网络了。我们实现一个函数来配置模型,如下所示:

def defineModelConfiguration(): MultiLayerConfiguration =
     new NeuralNetConfiguration.Builder()
        .seed(seed)
        .cacheMode(CacheMode.DEVICE)
        .updater(new Adam(1e-2))
        .biasUpdater(new Adam(1e-2*2))
        .gradientNormalization(GradientNormalization.RenormalizeL2PerLayer)
        .optimizationAlgo(OptimizationAlgorithm.STOCHASTIC_GRADIENT_DESCENT)
        .l1(1e-4)
        .l2(5 * 1e-4)
        .list
        .layer(0, new ConvolutionLayer.Builder(Array(4, 4), Array(1, 1), Array(0, 0)).name("cnn1").convolutionMode(ConvolutionMode.Same)
            .nIn(3).nOut(64).weightInit(WeightInit.XAVIER_UNIFORM).activation(Activation.RELU)
            .biasInit(1e-2).build)
        .layer(1, new ConvolutionLayer.Builder(Array(4, 4), Array(1, 1), Array(0, 0)).name("cnn2").convolutionMode(ConvolutionMode.Same)
            .nOut(64).weightInit(WeightInit.XAVIER_UNIFORM).activation(Activation.RELU)
            .biasInit(1e-2).build)
        .layer(2, new SubsamplingLayer.Builder(PoolingType.MAX, Array(2,2)).name("maxpool2").build())

        .layer(3, new ConvolutionLayer.Builder(Array(4, 4), Array(1, 1), Array(0, 0)).name("cnn3").convolutionMode(ConvolutionMode.Same)
            .nOut(96).weightInit(WeightInit.XAVIER_UNIFORM).activation(Activation.RELU)
            .biasInit(1e-2).build)
        .layer(4, new ConvolutionLayer.Builder(Array(4, 4), Array(1, 1), Array(0, 0)).name("cnn4").convolutionMode(ConvolutionMode.Same)
            .nOut(96).weightInit(WeightInit.XAVIER_UNIFORM).activation(Activation.RELU)
            .biasInit(1e-2).build)

        .layer(5, new ConvolutionLayer.Builder(Array(3,3), Array(1, 1), Array(0, 0)).name("cnn5").convolutionMode(ConvolutionMode.Same)
            .nOut(128).weightInit(WeightInit.XAVIER_UNIFORM).activation(Activation.RELU)
            .biasInit(1e-2).build)
        .layer(6, new ConvolutionLayer.Builder(Array(3,3), Array(1, 1), Array(0, 0)).name("cnn6").convolutionMode(ConvolutionMode.Same)
            .nOut(128).weightInit(WeightInit.XAVIER_UNIFORM).activation(Activation.RELU)
            .biasInit(1e-2).build)

        .layer(7, new ConvolutionLayer.Builder(Array(2,2), Array(1, 1), Array(0, 0)).name("cnn7").convolutionMode(ConvolutionMode.Same)
            .nOut(256).weightInit(WeightInit.XAVIER_UNIFORM).activation(Activation.RELU)
            .biasInit(1e-2).build)
        .layer(8, new ConvolutionLayer.Builder(Array(2,2), Array(1, 1), Array(0, 0)).name("cnn8").convolutionMode(ConvolutionMode.Same)
            .nOut(256).weightInit(WeightInit.XAVIER_UNIFORM).activation(Activation.RELU)
            .biasInit(1e-2).build)
        .layer(9, new SubsamplingLayer.Builder(PoolingType.MAX, Array(2,2)).name("maxpool8").build())

        .layer(10, new DenseLayer.Builder().name("ffn1").nOut(1024).updater(new Adam(1e-3)).biasInit(1e-3).biasUpdater(new Adam(1e-3*2)).build)
        .layer(11,new DropoutLayer.Builder().name("dropout1").dropOut(0.2).build)
        .layer(12, new DenseLayer.Builder().name("ffn2").nOut(1024).biasInit(1e-2).build)
        .layer(13,new DropoutLayer.Builder().name("dropout2").dropOut(0.2).build)
        .layer(14, new OutputLayer.Builder(LossFunctions.LossFunction.NEGATIVELOGLIKELIHOOD)
            .name("output")
            .nOut(numLabels)
            .activation(Activation.SOFTMAX)
            .build)
        .backprop(true)
        .pretrain(false)
        .setInputType(InputType.convolutional(height, width, channels))
        .build

与在Keras 实现部分中实现的模型完全相同的考虑因素也适用于此。因此,我们跳过所有中间步骤,直接实现一个复杂的模型,如下图所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/e8ac8a7f-f58c-462c-b4d9-5b4caf1b7734.png

图 13.12:本节示例的模型图示

下面是模型的详细信息:

层类型输入大小层大小参数数量权重初始化更新器激活函数
输入层
卷积层3643,136XAVIER_UNIFORMAdamReLU
卷积层646465,600XAVIER_UNIFORMAdamReLU
下采样(最大池化)
卷积层649698,400XAVIER_UNIFORMAdamReLU
卷积层9696147,552XAVIER_UNIFORMAdamReLU
卷积层96128110,720XAVIER_UNIFORMAdamReLU
卷积层128128147,584XAVIER_UNIFORMAdamReLU
卷积层128256131,328XAVIER_UNIFORMAdamReLU
卷积层256256262,400XAVIER_UNIFORMAdamReLU
下采样(最大池化)
全连接层16,3841,02416,778,240XAVIERAdamSigmoid
Dropout000Sigmoid
全连接层1,0241,0241,049,600XAVIERAdamSigmoid
Dropout000Sigmoid
输出层1,0241010,250XAVIERAdamSoftmax

那么我们接下来初始化模型,如下所示:

val conf = defineModelConfiguration
 val model = new MultiLayerNetwork(conf)
 model.init

然后,开始训练,如下所示:

val epochs = 10
 for(idx <- 0 to epochs) {
     model.fit(trainDataSetIterator)
 }

最后,评估它,如下所示:

val eval = new Evaluation(testDataSetIterator.getLabels)
 while(testDataSetIterator.hasNext) {
     val testDS = testDataSetIterator.next(batchSize)
     val output = model.output(testDS.getFeatures)
     eval.eval(testDS.getLabels, output)
 }
 println(eval.stats)

我们在这里实现的神经网络具有相当多的隐藏层,但按照前一节的建议(增加更多层,做数据增强,以及训练更多的 epoch),可以大幅提高模型的准确度。

训练当然可以使用 Spark 完成。前述代码中需要的更改,正如在第七章中详细介绍的那样,使用 Spark 训练神经网络,涉及 Spark 上下文初始化、训练数据并行化、TrainingMaster 创建,以及使用 SparkDl4jMultiLayer 实例执行训练,如下所示:

// Init the Spark context
 val sparkConf = new SparkConf
 sparkConf.setMaster(master)
   .setAppName("Object Recognition Example")
 val sc = new JavaSparkContext(sparkConf)

 // Parallelize data
 val trainDataList = mutable.ArrayBuffer.empty[DataSet]
 while (trainDataSetIterator.hasNext) {
   trainDataList += trainDataSetIterator.next
 }
 val paralleltrainData = sc.parallelize(trainDataList)

 // Create the TrainingMaster
 var batchSizePerWorker: Int = 16
 val tm = new
   ParameterAveragingTrainingMaster.Builder(batchSizePerWorker)
   .averagingFrequency(5)
   .workerPrefetchNumBatches(2)
   .batchSizePerWorker(batchSizePerWorker)
   .build

 // Training
 val sparkNet = new SparkDl4jMultiLayer(sc, conf, tm)
 for (i <- 0 until epochs) {
   sparkNet.fit(paralleltrainData)
   println("Completed Epoch {}", i)
 }

总结

在回顾了卷积概念和物体识别策略分类之后,本章中,我们以实践的方式,使用不同的语言(Python 和 Scala)以及不同的开源框架(第一种情况下使用 Keras 和 TensorFlow,第二种情况下使用 DL4J、ND4J 和 Apache Spark),实现并训练了卷积神经网络(CNN)进行物体识别。

在下一章中,我们将实现一个完整的图像分类 web 应用程序,其背后使用了 Keras、TensorFlow、DL4J、ND4J 和 Spark 的组合。

第十四章:图像分类

在前一章中,我们简要回顾了卷积的概念,并通过 Python(Keras)和 Scala(DL4J)的示例深入学习了物体识别的策略及更多实现细节。本章将介绍如何实现一个完整的图像分类 web 应用程序或 web 服务。这里的目标是向你展示如何将上一章的概念应用到端到端的分类系统中。

完成这一目标的步骤如下:

  • 选择一个合适的 Keras(带 TensorFlow 后端)预训练 CNN 模型

  • 在 DL4J(和 Spark)中加载并测试它

  • 了解如何在 Apache Spark 上重新训练 Python 模型

  • 实现一个使用该模型的图像分类 web 应用程序

  • 实现一个使用该模型的替代图像分类 web 服务

在前几章中,我们学习使用 DL 场景时遇到的所有开源技术,都在这里的实现过程中得到了应用。

实现一个端到端的图像分类 web 应用程序

使用我们在本书前几章学到的所有知识,现在我们应该能够实现一个实际的 web 应用程序,允许用户上传图像并对其进行正确的分类。

选择一个合适的 Keras 模型

我们将使用一个现有的、预训练的 Python Keras CNN 模型。Keras 应用程序(keras.io/applications/)是一组包含预训练权重的 DL 模型,作为框架的一部分提供。其中的模型包括 VGG16,这是一个由牛津大学视觉几何组在 2014 年实现的 16 层 CNN。该模型兼容 TensorFlow 后端,并且已经在 ImageNet 数据库(www.image-net.org/)上进行了训练。ImageNet 数据集是一个非常适合一般图像分类的优秀训练集,但它不适合面部识别模型的训练。下面是加载和使用 Keras 中的 VGG16 模型的方法。我们使用 TensorFlow 后端。让我们导入该模型:

from keras.applications.vgg16 import VGG16

然后,我们需要导入其他必要的依赖(包括 NumPy 和 Pillow):

from keras.preprocessing import image
from keras.applications.vgg16 import preprocess_input
import numpy as np
from PIL import Image

现在,我们可以创建模型的实例:

model = VGG16(weights='imagenet', include_top=True)

预训练的权重将在第一次运行该应用程序时自动下载。后续运行将从本地 ~/.keras/models/ 目录中加载权重。

这是模型的架构:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/4dec12d6-3628-42c8-9da5-50ed04ec9345.png

我们可以通过加载一张图像来测试模型:

img_path = 'test_image.jpg'
 img = image.load_img(img_path, target_size=(224, 224))

我们可以将其准备好作为模型的输入(通过将图像像素转换为 NumPy 数组并进行预处理):

x = image.img_to_array(img)
 x = np.expand_dims(x, axis=0)
 x = preprocess_input(x)

然后,我们可以进行预测:

features = model.predict(x)

最后,我们保存模型配置(以 JSON 格式):

model_json = model.to_json()
 with open('vgg-16.json', 'w') as json_file:
     json_file.write(model_json)

我们还可以保存模型的权重,以便导入到 DL4J 中:

model.save_weights("vgg-16.h5")

然后,我们将以下图像作为输入传递给模型:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/0d17d699-4fe3-495d-8af8-24915b71bb35.png

该图像被正确分类为虎斑猫,可能性接近 64%。

在 DL4J 中导入并测试模型

在第十章中,在分布式系统上部署,我们学习了如何将预训练的 Keras 模型导入到 DL4J 中。现在我们在这里应用相同的过程。

Scala 项目的依赖项包括 DL4J DataVec、NN、模型导入、动物园和 ND4J,以及 Apache common math 3。

我们需要做的第一件事是将模型配置(来自vgg-16.json文件)和权重(来自vgg-16.h5文件)复制到项目的资源文件夹中。然后,我们可以通过KerasModelImport类的importKerasModelAndWeights方法加载它们:

val vgg16Json = new ClassPathResource("vgg-16.json").getFile.getPath
 val vgg16 = new ClassPathResource("vgg-16.h5").getFile.getPath
 val model = KerasModelImport.importKerasModelAndWeights(vgg16Json, vgg16, false)

传递给方法的第三个参数是一个布尔值;如果为false,则表示该预训练模型仅用于推理,不会重新训练。

让我们使用前面截图中的图像来测试模型。我们需要将它复制到应用程序的资源目录中。然后,我们可以加载它,并将其调整为所需的大小(224 × 224 像素):

val testImage = new ClassPathResource("test_image.jpg").getFile

 val height = 224
 val width = 224
 val channels = 3
 val loader = new NativeImageLoader(height, width, channels)

为此,我们使用的是 DataVec 图像 API 中的NativeImageLoader类(jar-download.com/javaDoc/org.datavec/datavec-data-image/1.0.0-alpha/org/datavec/image/loader/NativeImageLoader.html)。

然后,我们需要将图像转换为 NDArray 并进行预处理:

val image = loader.asMatrix(testImage)
 val scaler = new VGG16ImagePreProcessor
 scaler.transform(image)

之后,我们需要通过模型进行推理:

val output = model.output(image)

为了以人类可读的格式消费结果,我们使用org.deeplearning4j.zoo.util.imagenet.ImageNetLabels类,它在 DL4J 的动物园库中可用。该类decodePredictions方法的输入是从模型的output方法返回的 NDArray 数组:

val imagNetLabels = new ImageNetLabels
 val predictions = imagNetLabels.decodePredictions(output(0))
 println(predictions)

以下截图展示了前面代码的输出。它呈现了上传图像的预测结果(按降序排列)。根据模型的预测,最高概率(大约 53.3%)是输入图像中的主要物体是一只虎斑猫(这是正确的):

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/043c9836-e850-4181-b971-7b41f3c87ebc.png

你应该注意到,一旦模型被导入,通过 DL4J API 加载图像并进行推理的步骤与我们在上一节中展示的 Keras 示例相同。

在模型经过测试后,最好通过ModelSerializer类将其保存:

val modelSaveLocation = new File("Vgg-16.zip")
 ModelSerializer.writeModel(model, modelSaveLocation, true)

然后,我们可以通过相同的类加载它,因为与从 Keras 加载相比,这样的资源消耗更少。

在 Apache Spark 中重新训练模型

为了提高我们在本章使用案例中考虑的 Keras VGG16 预训练模型的准确性,我们还可以决定对其进行再训练,并应用我们从上一章学到的所有最佳实践(运行更多的 epochs、图像增强等等)。一旦模型导入到 DL4J 中,其训练可以按照 第七章《使用 Spark 训练神经网络》(使用 DL4J 和 Apache Spark 进行训练)中解释的方式进行。在加载后,会创建一个 org.deeplearning4j.nn.graph.ComputationGraph 实例,因此,训练多层网络的相同原则在这里同样适用。

为了信息的完整性,你需要知道,Keras 模型也可以在 Apache Spark 上以并行模式进行训练。这可以通过 dist-keras Python 框架实现(github.com/cerndb/dist-keras/),该框架是为 分布式深度学习 (DDL) 创建的。可以通过 pip 安装该框架:

sudo pip install dist-keras

它需要 TensorFlow(将作为后端使用)并且需要设置以下变量:

export SPARK_HOME=/usr/lib/spark
 export PYTHONPATH="$SPARK_HOME/python/:$SPARK_HOME/python/lib/py4j-0.9-src.zip:$PYTHONPATH"

让我们快速看一下使用 dist-keras 进行分布式训练的典型流程。以下代码不是完整的工作示例;这里的目标是让你了解如何设置数据并行训练。

首先,我们需要导入 Keras、PySpark、Spark MLLib 和 dist-keras 所需的所有类。我们将首先导入 Keras:

from keras.optimizers import *
 from keras.models import Sequential
 from keras.layers.core import Dense, Dropout, Activation

然后,我们可以导入 PySpark:

from pyspark import SparkContext
 from pyspark import SparkConf

然后,我们导入 Spark MLLib:

from pyspark.ml.feature import StandardScaler
 from pyspark.ml.feature import VectorAssembler
 from pyspark.ml.feature import StringIndexer
 from pyspark.ml.evaluation import MulticlassClassificationEvaluator
 from pyspark.mllib.evaluation import BinaryClassificationMetrics

最后,我们导入 dist-keras

from distkeras.trainers import *
 from distkeras.predictors import *
 from distkeras.transformers import *
 from distkeras.evaluators import *
 from distkeras.utils import *

然后,我们需要创建 Spark 配置,如下所示:

conf = SparkConf()
 conf.set("spark.app.name", application_name)
 conf.set("spark.master", master)
 conf.set("spark.executor.cores", num_cores)
 conf.set("spark.executor.instances", num_executors)
 conf.set("spark.locality.wait", "0")
 conf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer");

然后我们可以使用它来创建一个 SparkSession

sc = SparkSession.builder.config(conf=conf) \
     .appName(application_name) \
     .getOrCreate()

数据集现在如下所示:

raw_dataset = sc.read.format('com.databricks.spark.csv') \
                     .options(header='true', inferSchema='true').load("data/some_data.csv")

我们可以使用此数据集通过 Spark 核心和 Spark MLLib 提供的 API 执行数据预处理和标准化(策略取决于数据集的性质,因此在此无法展示代码)。一旦完成此阶段,我们可以使用 Keras API 来定义我们的模型。

这是一个简单的 Sequential 模型的示例:

model = Sequential()
 model.add(Dense(500, input_shape=(nb_features,)))
 model.add(Activation('relu'))
 model.add(Dropout(0.4))
 model.add(Dense(500))
 model.add(Activation('relu'))
 model.add(Dense(nb_classes))
 model.add(Activation('softmax'))

最后,你可以通过选择 dist-keras 提供的多个优化算法之一来启动训练过程:

  • 顺序训练器

  • ADAG

  • 动态 SDG

  • AEASGD

  • AEAMSGD

  • DOWNPOUR

  • 集成训练

  • 模型平均

虽然列表中的后面几种方法性能更好,但第一个 SingleTrainer,通常作为基准 trainer 使用,在数据集过大无法完全加载到内存时,可能是一个不错的 trainer 选择。以下是使用 SingleTrainer 进行训练的代码示例:

trainer = SingleTrainer(keras_model=model, worker_optimizer=optimizer,
                         loss=loss, features_col="features_normalized",
                         label_col="label", num_epoch=1, batch_size=32)
 trained_model = trainer.train(training_set)

实现 Web 应用程序

让我们回到主要任务,开始实现一个允许用户上传图片的网页应用程序,然后使用序列化的 VGG16 模型对其进行推断。JVM 上有多个框架可以用来实现网页应用程序。在这种情况下,为了最小化我们的工作量,我们将使用 SparkJava(sparkjava.com/,不要与 Apache Spark 混淆),这是一个为 JVM 编程语言设计的微框架,旨在快速原型开发。与其他网页框架相比,它的模板代码最少。SparkJava 不仅仅是为网页应用程序设计的;也可以用非常少的代码行来实现 REST API(它将在下一节中用于实现我们的图像分类网页服务)。

我们必须将 SparkJava 添加到 Java 项目的依赖项列表中:

groupId: com.sparkjava
 artifactId: spark-core
 version: 2.7.2

本示例的参考版本为2.7.2(在写这本书时是最新版本)。

在最简单的实现中,一个 SparkJava 网页应用程序只需在main方法中写一行代码:

get("/hello", (req, res) -> "Hello VGG16");

运行应用程序后,hello页面可以通过以下 URL 从网页浏览器访问:

http://localhost:4567/hello

4567是 SparkJava 网页应用程序的默认端口。

SparkJava 应用程序的主要构建块是路由。路由由三部分组成:一个动词(getpostputdeleteheadtraceconnectoptions是可用的动词)、一个路径(在前面的代码示例中是/hello)和一个回调(requestresponse)。SparkJava API 还包括用于会话、Cookie、过滤器、重定向和自定义错误处理的类。

让我们开始实现我们的网页应用程序。项目的其他依赖项包括 DL4J 核心、DataVec、NN、模型导入和动物园(zoo),以及 ND4J。我们需要将 DL4J 的序列化模型(Vgg-16.zip文件)添加到项目的资源中。然后,可以通过ModelSerializer类在程序中加载该模型:

ClassLoader classLoader = getClass().getClassLoader();
 File serializedModelFile = new File(classLoader.getResource("Vgg-16.zip").getFile());
 ComputationGraph vgg16 = ModelSerializer.restoreComputationGraph(serializedModelFile);

我们需要创建一个目录,用于存放用户上传的图片:

File uploadDir = new File("upload");
 uploadDir.mkdir();

下一步是创建一个表单,让用户可以上传图片。在 SparkJava 中,可以为网页使用自定义样式。在这个例子中,我们将添加响应式的 Foundation 6 框架(foundation.zurb.com/)和 CSS。我们将最小的 Foundation CSS 库(foundation-float.min.css)添加到项目资源文件夹下的一个名为public的子目录中。这样,网页应用程序就可以在类路径中访问它。静态文件的位置可以通过编程方式注册:

staticFiles.location("/public");

Foundation CSS 和其他静态 CSS 文件可以在页面的头部注册。这里是为此示例实现的方法:

private String buildFoundationHeader() {
     String header = "<head>\n"
           + "<link rel='stylesheet' href='foundation-float.min.css'>\n"
           + "</head>\n";

     return header;
 }

我们现在实现一个名为buildUploadForm的方法,它返回该表单的 HTML 内容:

private String buildUploadForm() {
      String form =
              "<form method='post' action='getPredictions' enctype='multipart/form-data'>\n" +
              " <input type='file' name='uploadedFile'>\n" +
              " <button class='success button'>Upload picture</button>\n" +
              "</form>\n";

     return form;
 }

然后我们在定义上传页面路由时使用这个方法:

String header = buildFoundationHeader();
 String form = buildUploadForm();
 get("Vgg16Predict", (req, res) -> header + form);

现在我们可以定义post请求:

post("/doPredictions", (req, res)

我们这样做是为了处理图像上传和分类。在此post请求的主体中,我们需要执行以下操作:

  1. 将图像文件上传到upload目录

  2. 将图像转换为 NDArray

  3. 删除文件(转换后不需要将其保留在 Web 服务器磁盘上)

  4. 预处理图像

  5. 执行推理

  6. 显示结果

当转换成 Java 时,代码如下所示:

// Upload the image file
Path tempFile = Files.createTempFile(uploadDir.toPath(), "", "");

req.attribute("org.eclipse.jetty.multipartConfig", new MultipartConfigElement("/temp"));

try (InputStream input = req.raw().getPart("uploadedFile").getInputStream()) {
  Files.copy(input, tempFile, StandardCopyOption.REPLACE_EXISTING);
}

// Convert file to INDArray
File file = tempFile.toFile();

NativeImageLoader loader = new NativeImageLoader(224, 224, 3);
INDArray image = loader.asMatrix(file);

// Delete the physical file
file.delete();

// Pre-processing the image to prepare it for the VGG-16 model
DataNormalization scaler = new VGG16ImagePreProcessor();
scaler.transform(image);

// Do inference
INDArray[] output = vgg16.output(false,image);

// Get the predictions
ImageNetLabels imagNetLabels = new ImageNetLabels();
String predictions = imagNetLabels.decodePredictions(output[0]);

// Return the results
return buildFoundationHeader() + "<h4> '" + predictions + "' </h4>" +
  "Would you like to try another image?" +
  form;

你会注意到,通过 DL4J 进行的图像准备和推理部分与独立应用程序中的完全相同。

启动应用程序后,可以通过以下 URL 访问它:

http://localhost:4567/Vgg16Predict

可以通过编程方式设置不同的监听端口:

port(8998);

以下截图展示了上传页面的布局:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/04332b95-57a3-44fc-bfc5-ee17830ec73c.png

以下截图展示了我们上传所需图像:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/9026c652-8ee6-417f-afef-21b458a182b5.png

结果如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/0cf3cf17-6a52-4762-b774-6a55a5974cd0.png

实现 Web 服务

正如我们在前一节中提到的,SparkJava 可以快速实现 REST API。我们在前一节中实现的示例 Web 应用程序是单体的,但回顾其源代码,我们可以注意到将前端与后端分离并将其移至 REST API 会变得非常容易。

提供图像提交表单的前端客户端可以通过任何 Web 前端框架实现。客户端然后会调用通过 SparkJava 实现的 REST 服务,后者使用 VGG16 模型进行推理,最终返回 JSON 格式的预测结果。让我们看看从现有的 Web 应用程序代码开始,实现这个服务有多么简单。

Web 服务是一个带有主方法作为入口点的 Java 类。我们来定义一个自定义监听端口:

port(8998);

现在我们已经完成了这一步,我们需要定义upload端点:

post("/upload", (req, res) -> uploadFile(req));

我们需要将原始post体中的代码移到uploadFile方法中(唯一的区别是返回值,它只是预测内容,而不是完整的 HTML 内容):

private String uploadFile(Request req) throws IOException, ServletException {
    // Upload the image file
    Path tempFile = Files.createTempFile(uploadDir.toPath(), "", "");

    req.attribute("org.eclipse.jetty.multipartConfig", new MultipartConfigElement("/temp"));

    try (InputStream input = req.raw().getPart("file").getInputStream()) {
      Files.copy(input, tempFile, StandardCopyOption.REPLACE_EXISTING);
    }

    // Convert file to INDArray
    File file = tempFile.toFile();

    NativeImageLoader loader = new NativeImageLoader(224, 224, 3);
    INDArray image = loader.asMatrix(file);

    // Delete the physical file
    file.delete();

    // Pre-processing the image to prepare it for the VGG-16 model
    DataNormalization scaler = new VGG16ImagePreProcessor();
    scaler.transform(image);

    // Do inference
    INDArray[] output = vgg16.output(false,image);

    // Get the predictions
    ImageNetLabels imagNetLabels = new ImageNetLabels();
    String predictions = imagNetLabels.decodePredictions(output[0]);

    // Return the results
    return predictions;
}

运行应用程序后,你可以通过简单的curlcurl.haxx.se/)命令进行测试:

curl -s -X POST http://localhost:8998/upload -F 'file=@/home/guglielmo/dlws2/vgg16/src/main/resources/test_image-02.jpg'

输出将如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/228ec6e2-a872-41d9-8a70-049fdf144063.png

如果我们希望以 JSON 格式返回输出,这是我们需要对 Web 服务代码进行的唯一更改:

Gson gson = new Gson();
 post("/upload", (req, res) -> uploadFile(req), gson::toJson);

我们只需要创建一个com.google.gson.Gson实例,并将其作为最后一个参数传递给post方法。我们的示例输出将如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/6751468a-ed0b-4424-91ea-12c955d7bdba.png

总结

在本章中,我们通过结合本书前几章中学习的多个开源框架,成功实现了我们的第一个端到端图像分类 web 应用程序。读者现在应该掌握了构建块的所有知识,可以开始使用 Scala 和/或 Python 以及 DL4J 和/或 Keras 或 TensorFlow 开发自己的 DL 模型或应用程序。

本章结束了本书的实践部分。接下来的最后一章将讨论 DL 和 AI 的未来,重点讨论 DL4J 和 Apache Spark。

第十五章:深度学习的下一步是什么?

本章的最后将尝试概述深度学习DL)的未来,以及更广泛的人工智能的未来。

本章将涵盖以下主题:

  • 深度学习(DL)与人工智能(AI)

  • 热点话题

  • Spark 和 强化学习 (RL)

  • 生成对抗网络GANs)在 DL4J 中的支持

技术的快速进步不仅加速了现有人工智能理念的实施,还在这一领域创造了新的机会,这些机会在一两年前是不可想象的。人工智能日复一日地在各个领域发现新的实际应用,并且正在彻底改变我们在这些领域中的业务方式。因此,覆盖所有的新场景是不可能的,所以我们将专注于一些特定的领域/情境,那里我们直接或间接地有所参与。

对深度学习和人工智能的未来期望

如前所述,技术每天都在进步,同时计算能力的提升变得更加普及,且变得更加廉价,数据的可得性也在增加,这一切都推动着更深层次和更复杂模型的实现。因此,深度学习和人工智能的边界似乎没有上限。尝试理解我们对这些领域的期望,可能有助于我们清晰地了解短期内(2-3 年)会发生什么,但接下来可能发生的事情则较为不可预测,因为在这一领域中,任何新的想法都带来了其他的想法,并且正在推动多个行业的业务方式发生根本性的变化。因此,我将在本节中描述的是关于近期未来的内容,而非长期的变化。

深度学习在塑造人工智能的未来中发挥了关键作用。在某些领域,如图像分类和识别、物体检测和自然语言处理(NLP)中,深度学习已经超越了机器学习(ML),但这并不意味着机器学习算法已经过时。对于一些特定问题,深度学习可能有些过于复杂,因此机器学习仍然足够。在一些更复杂的情况下,深度学习与非深度学习算法的结合已经取得了显著成果;一个完美的例子是 DeepMind 团队的 AlphaGo 系统(deepmind.com/research/alphago/),它使用蒙特卡洛树搜索MCTS):mcts.ai/about/,结合深度学习网络来快速寻找制胜的棋步。深度学习的这一巨大进展也促成了其他更复杂和更先进的技术,如强化学习和生成对抗网络,这些将在本章的最后两节中讨论。

然而,虽然算法和模型正在取得令人惊人的快速进展,仍然存在许多障碍需要显著的人工干预(和额外的时间),才能在将数据提取并转化为机器智能之前消除它们。正如谷歌研究小组在论文Hidden Technical Debt in Machine Learning Systemspapers.nips.cc/paper/5656-hidden-technical-debt-in-machine-learning-systems.pdf)中讨论的那样,在 DL 和 ML 系统中,数据依赖的成本难以检测,并且可能轻易高于代码依赖的成本。以下图表取自同一篇谷歌研究论文,显示了 ML 或 DL 系统中 ML 或 DL 代码相对于其他依赖的比例:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/9b3e64a5-dd2b-48ef-b401-925ae338744e.png

图 15.1:现实世界中的大部分 ML/DL 系统只有一个很小的部分(图像中央的黑色矩形)由 ML/DL 代码组成

正如您从上述图表中可以看到的那样,诸如数据收集、设置和维护服务基础设施等事项比模型的实施和训练更为耗时和花费。因此,我期望在自动化这些任务时会有显著的改进。

需关注的话题

在过去的几个月中,关于所谓的可解释 AI,即不是黑匣子类型(我们只理解其基础数学原理)并且其行动或决策可以被人类轻松理解的 AI,引发了一场新的辩论。批评也已经开始(一般针对 AI,但特别是 DL),关于模型生成的结果不符合GDPR(即通用数据保护条例)的问题:ec.europa.eu/commission/priorities/justice-and-fundamental-rights/data-protection/2018-reform-eu-data-protection-rules_en ,涉及欧盟公民的数据,或其他可能在全球其他地区定义的数据法规,这些法规要求有权要求解释,以防止基于不同因素的歧视效应。

尽管这是一个非常热门且不容忽视的话题,而且已有一些有趣的分析和提案(例如来自都柏林科技学院的 Luca Longo 博士的www.academia.edu/18088836/Defeasible_Reasoning_and_Argument-Based_Systems_in_Medical_Fields_An_Informal_Overview以及ie.linkedin.com/in/drlucalongo的研究),我(以及本书的读者们大概也是)有机会听取了其他一些人对深度学习(DL)未来不佳的预测观点,认为 DL 的应用将仅限于非商业性应用和游戏。在本节中,我不会对这种观点做评论,因为它通常更多是基于意见而非事实,而且有时由那些没有完全参与 DL 或机器学习(ML)领域的生产或研究项目的人提出。相反,我更愿意展示一份关于仍然有效且可以持续一段时间的实际 DL 应用列表。

医疗保健是 AI 和 DL 实际应用数量较多的行业之一。Optum(www.optum.com/),作为 UnitedHealth Group 的一部分,已经在其整体战略中取得了显著成果,特别是在将自然语言处理(NLP)应用于多个商业用例中。AI 理解结构化和非结构化数据的能力在医疗记录审查中起着至关重要的作用(大多数数据都是非结构化的)。Optum 的所谓临床智能 NLP 能够解锁非结构化内容,以获取结构化数据元素,如诊断、治疗过程、药物、实验室检查等,进而形成完整且准确的临床文档。

来自非结构化来源的数据通过 NLP 技术自动提取,与通过更传统临床模型和规则引擎获得的结构化数据互补。这种自动化水平能够准确识别诊断、相关疾病和治疗过程,以实施提供的护理,但它也有助于定义适当的报销、质量措施以及其他关键的医疗保健操作。然而,理解记录中已经记录了什么,仅仅是 NLP 在医疗领域价值的一个方面。临床智能 NLP 技术还能够识别文档中的空白;它不仅能理解记录中有什么,还能理解缺少了什么。通过这种方式,临床医生可以获得有价值的反馈,从而改善文档记录。Optum 在 AI 方面的其他显著应用还包括支付完整性、简化的人口分析和呼叫中心等。

人工智能的另一个热门话题是机器人技术。从技术角度讲,机器人学是一个独立的领域,但它与人工智能有很多交集。深度学习(DL)和强化学习(RL)的进展为机器人技术中的多个问题提供了解决方案。机器人定义为首先能够感知,然后计算传感器的输入,最后根据这些计算结果采取行动。人工智能的介入使得机器人摆脱了工业化的“步步重复”模式,使它们变得更加智能。

在这一方向上,一个成功的用户案例是德国初创公司 Kewazo(www.kewazo.com/)。他们实施了一种智能机器人脚手架运输系统,解决了人手不足、效率低下、高成本、耗时工作和工人安全等问题。人工智能使得他们能够实现一个机器人系统,通过实时传递关于整体脚手架组装过程的数据,实现持续控制和显著优化或调优。人工智能还帮助 Kewazo 的工程师识别出其他应用场景,例如屋顶或太阳能面板安装,机器人可以在这些场景中工作并帮助实现与脚手架组装相同的结果。

物联网IoT)是人工智能日益普及的另一个领域。物联网的基本概念是日常使用的物理设备通过互联网连接,并能够相互通信以交换数据。这些收集到的数据可以被智能处理,从而使设备变得更加智能。随着连接设备数量的急剧增加(以及由这些设备生成的数据),人工智能和物联网的应用场景也在不断增长。

在这些应用场景中,我想提到人工智能在智能建筑中的潜力。过去五年,由信息技术、银行、金融和制药等行业推动的爱尔兰经济迅速增长,导致我目前工作的地区发生了根本性变化,即都柏林市中心的 Docklands 和 Grand Canal Dock 之间。为了应对新兴或扩张中的公司对办公空间日益增长的需求,数百座新建筑应运而生(还有更多在建)。所有这些新建的建筑都使用了一些人工智能技术,结合物联网,使建筑变得更智能。在以下领域取得了显著成果:

  • 让建筑对人类更加舒适

  • 让建筑对人类更加安全

  • 改善能源节约(并有助于环保)

传统的控制器(例如温度、灯光、门等)使用有限数量的传感器自动调整设备以实现恒定的最终结果。这个范式以前忽视了一个重要的因素:建筑物是由人类居住的,但无论是否有人在场,建筑物的控制方式都是一样的。这意味着像让人们感到舒适或节省能源之类的问题,根本没有被考虑在内。物联网与人工智能相结合,可以填补这个关键的空白。因此,建筑物可以有优先级,而不仅仅是遵循严格的编程范式。

物联网和人工智能的另一个有趣的实际应用场景是农业。农业部门(特别是乳制品)是爱尔兰国内生产总值的重要组成部分,也是爱尔兰出口中不可忽视的一部分。农业面临着新的和旧的挑战(例如在相同的土地面积上生产更多的食物、满足严格的排放要求、保护种植园免受害虫侵害、考虑气候及全球气候变化、控制水流、监控大规模果园、抗击火灾、监控土壤质量、监测动物健康等等)。这意味着农民不能仅仅依赖传统的做法。人工智能、物联网和物联网支持的传感器正在帮助他们解决我们之前提到的挑战以及更多的问题。爱尔兰已经在多个实际应用中部署了智能农业(其中一些在 2018 年 Predict 大会上展示过:www.tssg.org/projects/precision-dairy/),预计 2019 年还会有更多应用落地。

说到人工智能和物联网,边缘分析是另一个热门话题。边缘分析是传统的大数据分析的替代方案,传统大数据分析通常是在集中式方式下执行的,而边缘分析则是在系统中的某个非中心点(例如连接设备或传感器)分析数据。目前,边缘分析在工业 4.0 领域中已有多个实际应用(但不限于此)(en.wikipedia.org/wiki/Industry_4.0)。在数据生成的同时进行分析,可以减少决策过程中的延迟,尤其是在连接设备上。

例如,假设在某制造系统中,传感器数据指示某个特定部件可能会故障;内置在机器学习(ML)或深度学习(DL)算法中的规则可以自动在网络边缘解读这些数据,进而关闭机器并向维修经理发送警报,以便及时更换该部件。与将数据传输到集中数据位置进行处理和分析相比,这可以节省大量时间,并减少甚至避免计划外机械停机的风险。

边缘分析还带来了可伸缩性方面的好处。在那些组织中,连接设备数量增加(以及生成和收集的数据量也增加)的情况下,通过将算法推送到传感器和网络设备,可以减轻企业数据管理和集中分析系统的处理压力。在这个领域有一些值得关注的有前途的开源项目。DL4J 本身就是其中之一;其移动特性允许在 Android 设备上定义多层神经网络模型、训练和推理(由于 Android 是 JVM 框架的自然选择,其他移动平台不支持)。TensorFlow Lite (www.tensorflow.org/lite/) 可以在几种移动操作系统(包括 Android、iOS 和其他)和嵌入式设备上实现低延迟、小二进制大小的设备端 ML 推理。StreamSets 数据收集器边缘的最新版本(streamsets.com/products/sdc-edge)允许在设备上触发高级分析和 ML(TensorFlow)(Linux、Android、iOS、Windows 和 MacOS 是其支持的操作系统)。我期待在这个领域会有更多来自开源世界的发展。

DL 的崛起促使研究人员开发出可以直接实现神经网络架构的硬件芯片。它们设计为在硬件级别模仿人脑。在传统芯片中,数据需要在 CPU 和存储块之间传输,而在神经形态芯片中,数据既在芯片内处理又存储,并且在需要时可以生成突触。这种第二种方法不会产生时间开销,并且节省能量。因此,未来的人工智能很可能更多地基于神经形态芯片而不是基于 CPU 或 GPU。人类大脑中大约有 1000 亿个神经元密集地打包在一个小体积内,使用非常少的能量就能以闪电般的速度处理复杂计算。在过去几年中,出现了受大脑启发的算法,可以做到识别人脸、模仿声音、玩游戏等。但软件只是更大图景的一部分。我们现代的计算机实际上无法运行这些强大的算法。这就是神经形态计算进入游戏的地方。

本节中展示的场景确实证实了,在考虑到 GDPR 或其他数据法规时,深度学习和人工智能绝对不会局限于无用的应用。

Spark 准备好接受 RL 了吗?

在本书中,我们已经理解了深度学习(DL)如何解决计算机视觉、自然语言处理和时间序列预测中的多个问题。将 DL 与强化学习(RL)结合起来,可以解决更复杂的问题,并带来更惊人的应用。那么,什么是强化学习(RL)呢?它是机器学习(ML)中的一个特定领域,在这个领域中,代理必须采取行动,以最大化给定环境中的奖励。强化学习这个术语来源于这种学习过程与孩子们通过糖果获得激励的相似性;当 RL 算法做出正确决策时会获得奖励,做出错误决策时会受到惩罚。RL 与监督学习不同,后者中的训练数据本身就带有答案,然后通过正确的答案来训练模型。在 RL 中,代理决定该做什么来完成任务,如果没有训练数据集可用,它们只能通过自己的经验来学习。

强化学习的一个主要应用领域是计算机游戏(其中最优秀且最受欢迎的成果之一是来自 Alphabet 公司 DeepMind 团队的 AlphaGo,详见deepmind.com/research/alphago/),但它也可以应用于其他领域,如机器人技术、工业自动化、聊天机器人系统、自动驾驶汽车、数据处理等。

在了解 Apache Spark 中对 RL 的支持以及它可能的发展之前,让我们先看一下强化学习的基本概念。

下面是主要的概念:

  • 代理:它是执行动作的算法。

  • 动作:它是代理可以采取的可能行动之一。

  • 折扣因子:它量化了即时奖励与未来奖励在重要性上的差异。

  • 环境:它是代理所处的世界。环境以代理的当前状态和动作作为输入,返回代理的奖励和下一个状态作为输出。

  • 状态:它是代理所处的具体情境。

  • 奖励:它是衡量代理行动成功或失败的反馈(该行动使得从一个状态到另一个状态的转变)。

  • 策略:它是一个代理根据当前状态来决定其下一步动作的策略。

  • 价值:它是当前状态下,在给定策略下的预期长期回报。

  • Q 值:它类似于价值,但还考虑了当前的动作。

  • 轨迹:它是影响状态和动作的状态与动作序列。

我们可以总结强化学习(RL)如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/ebe1cc81-df0a-459d-bb4a-6b96cfe1df3c.png

图 15.2:强化学习反馈回路

一个很好的例子来解释这些概念的是流行的吃豆人视频游戏,详见en.wikipedia.org/wiki/Pac-Man;请看下面的截图:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/6280cd9e-4e53-44ba-b03f-eeb290ac771b.png

图 15.3:吃豆人视频游戏

在这里,代理是吃豆人角色,目标是在迷宫中吃掉所有食物,同时避开一些试图杀死它的鬼怪。迷宫是代理的环境。它吃到食物会获得奖励,被鬼怪杀死时则会受到惩罚(游戏结束)。状态是代理在迷宫中的位置。总累计奖励是代理赢得游戏并进入下一个关卡。开始探索后,吃豆人(代理)可能会发现迷宫四个角落附近的四颗能量豆(使它对鬼怪免疫),并决定花费所有时间利用这一发现,不断绕着迷宫的这小块区域转,永远不深入迷宫的其他部分去追求更大的奖励。为了构建一个最优策略,代理面临一个两难选择:一方面是探索新的状态,另一方面是最大化其奖励。这样,它可能错过了最终奖励(进入下一个关卡)。这被称为探索与利用的权衡。

最流行的 RL 算法是马尔可夫决策过程MDP):en.wikipedia.org/wiki/Markov_decision_processQ 学习 (en.wikipedia.org/wiki/Q-learning),和A3C (arxiv.org/pdf/1602.01783.pdf)。

Q 学习广泛应用于游戏(或类似游戏的)领域。它可以用以下方程式概括(源代码来自 Q 学习的 Wikipedia 页面):

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/f16df61e-ff74-4da3-aba9-809885b22226.png

在这里,s[t] 是时刻 t 的状态,a[t] 是代理采取的行动,r[t] 是时刻 t 的奖励,s[t+1] 是新的状态(时刻 t+1),https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/645aec1f-4361-4b81-ac15-021c0fae6d8e.png 是学习率 (https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/653aec36-6dd8-4058-b906-55647168e2d6.png),而 https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/d2d3c192-68f8-46c1-afc4-962f9294af48.png 是折扣因子。最后一个参数决定了未来奖励的重要性。如果它为零,代理会变得目光短浅,因为它只会考虑当前的奖励。如果其值接近 1,代理则会努力实现长期的高奖励。如果折扣因子值为 1 或更高,则行动值可能会发散。

Apache Spark 的 MLLib 组件目前没有针对 RL 的任何功能,而且在编写本书时,似乎没有计划在未来的 Spark 版本中实现对此的支持。然而,确实有一些与 Spark 集成的开源稳定 RL 项目。

DL4J 框架提供了一个专门的 RL 模块——RL4J,最初是一个独立的项目。和所有其他 DL4J 组件一样,它完全与 Apache Spark 集成。它实现了 DQN(深度 Q 学习与双 DQN)和 AC3 RL 算法。

英特尔的杨宇豪(www.linkedin.com/in/yuhao-yang-8a150232)做了有趣的实现,促成了 Analytics Zoo 计划的启动(github.com/intel-analytics/analytics-zoo)。这是他在 2018 年 Spark-AI 峰会上的演讲链接(databricks.com/session/building-deep-reinforcement-learning-applications-on-apache-spark-using-bigdl)。Analytics Zoo 提供了一个统一的分析和 AI 平台,可以将 Spark、TensorFlow、Keras 和 BigDL 程序无缝集成到一个可以扩展到大规模 Spark 集群进行分布式训练或推理的管道中。

虽然 RL4J 作为 DL4J 的一部分,为 JVM 语言(包括 Scala)提供 API,而 BigDL 则为 Python 和 Scala 提供 API,但 Facebook 提供了一个仅支持 Python 的端到端开源平台,用于大规模强化学习。这个平台的名称是 Horizon(github.com/facebookresearch/Horizon)。Facebook 自己也在生产环境中使用它来优化大规模环境中的系统。它支持离散动作 DQN、参数化动作 DQN、双 DQN、DDPG(arxiv.org/abs/1509.02971)和 SAC(arxiv.org/abs/1801.01290)算法。该平台中的工作流和算法都是建立在开源框架(PyTorch 1.0、Caffe2 和 Apache Spark)上的。目前尚不支持与其他流行的 Python 机器学习框架(如 TensorFlow 和 Keras)一起使用。

RISELab(rise.cs.berkeley.edu/)的 Ray 框架(ray-project.github.io/)值得特别提及。尽管 DL4J 和我们之前提到的其他框架是在 Apache Spark 之上以分布式模式工作,但在伯克利的研究人员眼中,Ray 是 Spark 本身的替代品,他们认为 Spark 更通用,但并不完全适合某些实际的 AI 应用。Ray 是用 Python 实现的,完全兼容最流行的 Python 深度学习框架,包括 TensorFlow 和 PyTorch;并且它允许在同一应用中使用多个框架的组合。

在强化学习(RL)的特定情况下,Ray 框架还提供了一个专门的库 RLLib(ray.readthedocs.io/en/latest/rllib.html),它实现了 AC3、DQN、进化策略(en.wikipedia.org/wiki/Evolution_strategy)和 PPO(blog.openai.com/openai-baselines-ppo/)算法。写这本书时,我不知道任何真实世界的 AI 应用正在使用这个框架,但我相信值得关注它如何发展以及行业的采用程度。

DeepLearning4J 未来对 GAN 的支持

生成对抗网络GANs)是包括两个互相对抗的网络的深度神经网络架构(这也是名称中使用对抗一词的原因)。GAN 算法用于无监督机器学习。GAN 的主要焦点是从零开始生成数据。在 GAN 的最流行应用场景中,包括从文本生成图像、图像到图像的翻译、提高图像分辨率以制作更真实的图片,以及对视频的下一帧进行预测。

如前所述,GAN 由两个深度网络组成,生成器判别器;第一个生成候选数据,第二个评估这些候选数据。让我们从很高的层次来看生成性和判别性算法是如何工作的。判别性算法尝试对输入数据进行分类,因此它们预测输入数据属于哪个标签或类别。它们唯一关心的是将特征映射到标签。生成性算法则不同,它们在给定某个标签时尝试预测特征,而不是像判别性算法那样预测标签。实际上,它们做的事情正好与判别性算法相反。

下面是 GAN 的工作原理。生成器生成新的数据实例,而判别器评估这些数据以判断其真实性。使用本书中多次举例的相同 MNIST 数据集(yann.lecun.com/exdb/mnist/),让我们通过一个场景来明确 GAN 中发生的过程。假设我们有一个生成器生成像手写数字这样的 MNIST 数据集,然后将它们传递给判别器。生成器的目标是生成看起来像手写数字的图像,而不被发现;而判别器的目标是识别出来自生成器的这些图像是假手写数字。参考下面的图示,GAN 的步骤如下:

  1. 生成器网络接受一些随机数字作为输入,然后返回一张图像。

  2. 生成的图像被用来喂给判别器网络,同时传入其他从训练数据集中获取的图像流。

  3. 判别器在接收真实和伪造图像时,会返回概率值,这些概率值介于零和一之间。零代表伪造的预测,而一代表真实性的预测:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/hsn-dl-spk/img/ab766101-1770-4c23-a226-532ce41bf581.png

图 15.4:MNIST 示例 GAN 的典型流程

在实现方面,判别器网络是一个标准的卷积神经网络(CNN),可以对输入的图像进行分类,而生成器网络是一个反向卷积神经网络(CNN)。这两个网络在零和博弈中优化不同且相对立的损失函数。该模型本质上是一个演员-评论员模型(cs.wmich.edu/~trenary/files/cs5300/RLBook/node66.html),其中判别器网络会改变其行为,生成器网络也是如此,反之亦然。

在撰写本书时,DL4J 并未提供任何直接支持生成对抗网络(GAN)的 API,但它允许你导入现有的 Keras(如你可以在github.com/eriklindernoren/Keras-GAN找到的那样,这是我们的 GitHub 仓库)或 TensorFlow(如这个:github.com/aymericdamien/TensorFlow-Examples/blob/master/examples/3_NeuralNetworks/gan.py)的 GAN 模型,然后在 JVM 环境中(包括 Spark)使用 DL4J API 重新训练它们和/或进行预测,正如第十章《在分布式系统上的部署》和第十四章《图像分类》所解释的那样。目前,DL4J 没有针对 GAN 的直接功能,但导入 Python 模型是训练和推理的有效方法。

总结

本章总结了本书的内容。在本书中,我们熟悉了 Apache Spark 及其组件,随后我们开始探索深度学习(DL)的基础知识,并开始实际操作。我们通过理解如何从不同的数据源(无论是批处理还是流模式)导入训练和测试数据,并通过 DataVec 库将其转化为向量,开始了我们的 Scala 实操之旅。接着,我们探索了卷积神经网络(CNN)和递归神经网络(RNN)的细节,以及通过 DL4J 实现这些网络模型的方法,如何在分布式和基于 Spark 的环境中训练它们,如何使用 DL4J 的可视化工具监控它们并获取有用的见解,以及如何评估它们的效率并进行推理。

我们还学习了一些配置生产环境进行训练时应遵循的技巧和最佳实践,以及如何将已经在 Keras 和/或 TensorFlow 中实现的 Python 模型导入并使其在基于 JVM 的环境中运行(或重新训练)。在本书的最后部分,我们将之前学到的知识应用于先使用深度学习实现自然语言处理(NLP)应用场景,再到实现一个端到端的图像分类应用。

我希望所有阅读完本书所有章节的读者都达成了我的初衷目标:他们已经掌握了所有的构建块,可以开始在分布式系统(如 Apache Spark)中,使用 Scala 和/或 Python 处理他们自己特定的深度学习(DL)应用场景。

附录 A:Scala 中的函数式编程

Scala 将函数式编程和面向对象编程结合在一个高级语言中。该附录包含了有关 Scala 中函数式编程原则的参考。

函数式编程(FP)

在函数式编程中,函数是第一类公民——这意味着它们像其他值一样被对待,可以作为参数传递给其他函数,或者作为函数的返回结果。在函数式编程中,还可以使用所谓的字面量形式来操作函数,无需为其命名。让我们看一下以下的 Scala 示例:

val integerSeq = Seq(7, 8, 9, 10)
integerSeq.filter(i => i % 2 == 0)

i => i % 2 == 0是一个没有名称的函数字面量。它检查一个数字是否为偶数。它可以作为另一个函数的参数传递,或者可以作为返回值使用。

纯度

函数式编程的支柱之一是纯函数。一个纯函数是类似于数学函数的函数。它仅依赖于其输入参数和内部算法,并且对于给定的输入始终返回预期的结果,因为它不依赖于外部的任何东西。(这与面向对象编程的方法有很大的不同。)你可以很容易理解,这使得函数更容易测试和维护。一个纯函数不依赖外部的任何内容,这意味着它没有副作用。

纯粹的函数式程序在不可变数据上进行操作。与其修改现有的值,不如创建修改后的副本,而原始值则被保留。这意味着它们可以在旧副本和新副本之间共享,因为结构中未改变的部分无法被修改。这样的行为带来的一个结果是显著的内存节省。

在 Scala(以及 Java)中,纯函数的例子包括Listsize方法(docs.oracle.com/javase/8/docs/api/java/util/List.html)或者Stringlowercase方法(docs.oracle.com/javase/8/docs/api/java/lang/String.html)。StringList都是不可变的,因此它们的所有方法都像纯函数一样工作。

但并非所有抽象都可以直接通过纯函数实现(例如读取和写入数据库或对象存储,或日志记录等)。FP 提供了两种方法,使开发人员能够以纯粹的方式处理不纯抽象,从而使最终代码更加简洁和可维护。第一种方法在某些其他 FP 语言中使用,但在 Scala 中没有使用,即通过将语言的纯函数核心扩展到副作用来实现。然后,避免在只期望纯函数的情况下使用不纯函数的责任就交给开发人员。第二种方法出现在 Scala 中,它通过引入副作用来模拟纯语言中的副作用,使用monadswww.haskell.org/tutorial/monads.html)。这样,虽然编程语言保持纯粹且具有引用透明性,但 monads 可以通过将状态传递到其中来提供隐式状态。编译器不需要了解命令式特性,因为语言本身保持纯粹,而通常实现会出于效率原因了解这些特性。

由于纯计算具有引用透明性,它们可以在任何时间执行,同时仍然产生相同的结果,这使得计算值的时机可以延迟,直到真正需要时再进行(懒惰计算)。这种懒惰求值避免了不必要的计算,并允许定义和使用无限数据结构。

通过像 Scala 一样仅通过 monads 允许副作用,并保持语言的纯粹性,使得懒惰求值成为可能,而不会与不纯代码的副作用冲突。虽然懒惰表达式可以按任何顺序进行求值,但 monad 结构迫使这些副作用按正确的顺序执行。

递归

递归在函数式编程(FP)中被广泛使用,因为它是经典的,也是唯一的迭代方式。函数式语言的实现通常会包括基于所谓的尾递归alvinalexander.com/scala/fp-book/tail-recursive-algorithms)的优化,以确保重度递归不会对内存消耗产生显著或过度的影响。尾递归是递归的一个特例,其中函数的返回值仅仅是对自身的调用*。* 以下是一个使用 Scala 语言递归计算斐波那契数列的例子。第一段代码表示递归函数的实现:

def fib(prevPrev: Int, prev: Int) {
    val next = prevPrev + prev
    println(next)
    if (next > 1000000) System.exit(0)
    fib(prev, next)
}

另一段代码表示同一函数的尾递归实现:

def fib(x: Int): BigInt = {
    @tailrec def fibHelper(x: Int, prev: BigInt = 0, next: BigInt = 1): BigInt = x match {
        case 0 => prev
        case 1 => next
        case _ => fibHelper(x - 1, next, (next + prev))
    }
    fibHelper(x)
}

虽然第一个函数的返回行包含对自身的调用,但它还对输出做了一些处理,因此返回值并不完全是递归调用的返回值。第二个实现是一个常规的递归(特别是尾递归)函数。

附录 B:Spark 图像数据准备

卷积神经网络(CNN)是本书的主要话题之一。它们被广泛应用于图像分类和分析的实际应用中。本附录解释了如何创建一个 RDD<DataSet> 来训练 CNN 模型进行图像分类。

图像预处理

本节描述的图像预处理方法将文件分批处理,依赖于 ND4J 的 FileBatch 类(static.javadoc.io/org.nd4j/nd4j-common/1.0.0-beta3/org/nd4j/api/loader/FileBatch.html),该类从 ND4J 1.0.0-beta3 版本开始提供。该类可以将多个文件的原始内容存储在字节数组中(每个文件一个数组),包括它们的原始路径。FileBatch 对象可以以 ZIP 格式存储到磁盘中。这可以减少所需的磁盘读取次数(因为文件更少)以及从远程存储读取时的网络传输(因为 ZIP 压缩)。通常,用于训练 CNN 的原始图像文件会采用一种高效的压缩格式(如 JPEG 或 PNG),这种格式在空间和网络上都比较高效。但在集群中,需要最小化由于远程存储延迟问题导致的磁盘读取。与 minibatchSize 的远程文件读取相比,切换到单次文件读取/传输会更快。

将图像预处理成批次会带来以下限制:在 DL4J 中,类标签需要手动提供。图像应存储在以其对应标签命名的目录中。我们来看一个示例——假设我们有三个类,即汽车、卡车和摩托车,图像目录结构应该如下所示:

imageDir/car/image000.png
imageDir/car/image001.png
...
imageDir/truck/image000.png
imageDir/truck/image001.png
...
imageDir/motorbike/image000.png
imageDir/motorbike/image001.png
...

图像文件的名称并不重要。重要的是根目录下的子目录名称必须与类的名称一致。

策略

在 Spark 集群上开始训练之前,有两种策略可以用来预处理图像。第一种策略是使用 dl4j-spark 中的 SparkDataUtils 类在本地预处理图像。例如:

import org.datavec.image.loader.NativeImageLoader
import org.deeplearning4j.spark.util.SparkDataUtils
...
val sourcePath = "/home/guglielmo/trainingImages"
val sourceDir = new File(sourcePath)
val destinationPath = "/home/guglielmo/preprocessedImages"
val destDir = new File(destinationPath)
val batchSize = 32
SparkDataUtils.createFileBatchesLocal(sourceDir, NativeImageLoader.ALLOWED_FORMATS, true, destDir, batchSize)

在这个示例中,sourceDir 是本地图像的根目录,destDir 是保存预处理后图像的本地目录,batchSize 是将图像放入单个 FileBatch 对象中的数量。createFileBatchesLocal 方法负责导入。一旦所有图像都被预处理,目标目录 dir 的内容可以被复制或移动到集群中用于训练。

第二种策略是使用 Spark 对图像进行预处理。在原始图像存储在分布式文件系统(如 HDFS)或分布式对象存储(如 S3)的情况下,仍然使用 SparkDataUtils 类,但必须调用一个不同的方法 createFileBatchesLocal,该方法需要一个 SparkContext 作为参数。以下是一个示例:

val sourceDirectory = "hdfs:///guglielmo/trainingImages"; 
val destinationDirectory = "hdfs:///guglielmo/preprocessedImages";    
val batchSize = 32

val conf = new SparkConf
...
val sparkContext = new JavaSparkContext(conf)

val filePaths = SparkUtils.listPaths(sparkContext, sourceDirectory, true, NativeImageLoader.ALLOWED_FORMATS)
SparkDataUtils.createFileBatchesSpark(filePaths, destinationDirectory, batchSize, sparkContext)

在这种情况下,原始图像存储在 HDFS 中(通过sourceDirectory指定位置),预处理后的图像也保存在 HDFS 中(位置通过destinationDirectory指定)。在开始预处理之前,需要使用 dl4j-spark 的SparkUtils类创建源图像路径的JavaRDD<String>filePaths)。SparkDataUtils.createFileBatchesSpark方法接受filePaths、目标 HDFS 路径(destinationDirectory)、放入单个FileBatch对象的图像数量(batchSize)以及 SparkContext(sparkContext)作为输入。只有所有图像都经过 Spark 预处理后,训练才能开始。

训练

无论选择了哪种预处理策略(本地或 Spark),以下是使用 Spark 进行训练的步骤。

首先,创建 SparkContext,设置TrainingMaster*,*并使用以下实例构建神经网络模型:

val conf = new SparkConf
...
val sparkContext = new JavaSparkContext(conf)
val trainingMaster = ...
val net:ComputationGraph = ...
val sparkNet = new SparkComputationGraph(sparkContext, net, trainingMaster)
sparkNet.setListeners(new PerformanceListener(10, true))

之后,需要创建数据加载器,如以下示例所示:

val imageHeightWidth = 64      
val imageChannels = 3          
val labelMaker = new ParentPathLabelGenerator
val rr = new ImageRecordReader(imageHeightWidth, imageHeightWidth, imageChannels, labelMaker)
rr.setLabels(new TinyImageNetDataSetIterator(1).getLabels())
val numClasses = TinyImageNetFetcher.NUM_LABELS
val loader = new RecordReaderFileBatchLoader(rr, minibatch, 1, numClasses)
loader.setPreProcessor(new ImagePreProcessingScaler)

输入图像具有分辨率为 64 x 64 像素(imageHeightWidth)和三个通道(RGB,imageChannels)。加载器通过ImagePreProcessingScaler类将 0-255 值像素缩放到 0-1 的范围内(deeplearning4j.org/api/latest/org/nd4j/linalg/dataset/api/preprocessor/ImagePreProcessingScaler.html)。

训练可以从以下示例开始:

val trainPath = "hdfs:///guglielmo/preprocessedImages"
val pathsTrain = SparkUtils.listPaths(sc, trainPath)
val numEpochs = 10
for (i <- 0 until numEpochs) {
    println("--- Starting Training: Epoch {} of {} ---", (i + 1), numEpochs)
    sparkNet.fitPaths(pathsTrain, loader)
} 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值