Java 数据科学(四)
十、视觉和听觉分析
声音、图像和视频的使用正在成为我们日常生活中一个更重要的方面。依赖语音命令的电话交谈和设备越来越普遍。人们定期与世界各地的其他人进行视频聊天。照片和视频分享网站已经迅速扩散。利用各种来源的图像、视频和声音的应用程序变得越来越普遍。
在这一章中,我们将展示几种 Java 可以用来处理声音和图像的技术。本章的第一部分讲述声音处理。语音识别和文本到语音(TTS)API 都将被展示。具体来说,我们将使用 FreeTTS(【freetts.sourceforge.net/docs/index.… 将文本转换为语音,然后演示 CMU Sphinx 语音识别工具包。
Java 语音 API(JSAPI)(【www.oracle.com/technetwork… JDK 的一部分,但受第三方供应商支持。它的目的是支持语音识别和语音合成器。有几个供应商支持 JSAPI,包括 FreeTTS 和 Festival(www.cstr.ed.ac.uk/projects/fe…)。
此外,还有几个基于云的语音 API,包括 IBM 通过 Watson Cloud 语音转文本功能提供的支持。
接下来,我们将研究图像处理技术,包括面部识别。这包括识别图像中的人脸。这项技术很容易使用 OpenCV(opencv.org/)来实现,我们将在识别人脸部分演示。
我们将以对 Neuroph Studio 的讨论来结束本章,Neuroph Studio 是一个基于 Java 的神经网络编辑器,用于对图像进行分类和执行图像识别。我们将继续使用人脸,并尝试训练一个网络来识别人脸图像。
文本到语音转换
语音合成产生人类语音。TTS 将文本转换成语音,对许多不同的应用都很有用。它被用在许多地方,包括电话帮助台系统和订购系统。TTS 流程通常由两部分组成。第一部分将文本标记和处理成语音单元。第二部分将这些单位转换成语音。
TTS 的两种主要方法使用拼接合成和共振峰合成。拼接合成经常组合预先录制的人类语音来创建所需的输出。共振峰合成不使用人类语音,而是通过创建电子波形来生成语音。
我们将使用自由 TTS(freetts.sourceforge.net/docs/index.…)来演示 TTS。最新版本可以从 sourceforge.net/projects/fr…](sourceforge.net/projects/fr…)
TTS/FreeTTS 中使用了几个重要术语:
- 话语 -这个概念大致对应于组成一个单词或短语的声音
- 条目 -代表话语部分的特征集(名称/值对)
- Relationship -一个条目列表,FreeTTS 使用它在一个话语中前后迭代
- 电话 -一种独特的声音
- 双音素 -一对相邻的音素
FreeTTS 程序员指南(freetts.sourceforge.net/docs/Progra…)详细介绍了将文本转换为语音的过程。这是一个多步骤的过程,其主要步骤包括:
- 标记化 -从文本中提取标记
- TokenToWords -转换某些单词,如 1910 年到 1910 年
- PartOfSpeechTagger -这一步目前什么也不做,但旨在识别词性
- 短语器 -为话语创建短语关系
- 分段器 -确定音节断开出现的位置
- 暂停生成器(pause generator)-这个步骤在语音中插入暂停,比如在说话之前
- 发音者 -决定口音和音调
- 后置词汇分析器 -这一步修复诸如可用双音素和需要说出的双音素不匹配之类的问题
- 持续时间 -决定音节的持续时间
- ContourGenerator -计算话语的基频曲线,该曲线将频率与时间对应起来,有助于生成音调
- 单元选择器 -将相关的双音素组合成一个单元
- 音高标记生成器 -决定话语的音高
- 单元连接器 -将双音素数据连接在一起
下图来自 *FreeTTS 程序员指南,图 11:*unit concator处理后的发声,并描绘了流程。这是对 TTS 流程的高度概括,暗示了该流程的复杂性:
使用免费软件
TTS 系统方便了不同声音的使用。例如,这些差异可能存在于语言、说话者的性别或说话者的年龄。
MBROLA 项目的目标是支持尽可能多的语言的语音合成器。MBROLA 是一个语音合成器,可以与 FreeTTS 等 TTS 系统一起使用,以支持 TTS 合成。
从tcts.fpms.ac.be/synthesis/m…下载适用于适当平台的二进制 MBROLA。从同一个页面,下载页面底部找到的任何想要的 MBROLA 声音。对于我们的例子,我们将使用usa1、usa2和usa3。关于设置的更多细节可在freetts.sourceforge.net/mbrola/READ…找到。
以下语句说明了访问 MBROLA 声音所需的代码。setProperty方法指定找到 MBROLA 资源的路径:
System.setProperty("mbrola.base", "path-to-mbrola-directory");
为了演示如何使用 TTS,我们使用下面的语句。我们获得了一个VoiceManager类的实例,它将提供对各种声音的访问:
VoiceManager voiceManager = VoiceManager.getInstance();
为了使用一个特定的声音,向getVoice方法传递声音的名称,并返回一个Voice类的实例。在这个例子中,我们使用了mbrola_us1,这是一个美国英语,年轻,女性的声音:
Voice voice = voiceManager.getVoice("mbrola_us1");
一旦我们获得了Voice实例,就使用allocate方法来加载语音。然后使用speak方法将传递给该方法的单词合成为一个字符串,如下所示:
voice.allocate();
voice.speak("Hello World");
执行的时候要听到"Hello World"这几个字。如下一节所述,用其他声音和文本尝试一下,看看哪种组合最适合某个应用。
获取关于声音的信息
VoiceManager class' getVoices方法用于获取当前可用的声音数组。这对于向用户提供可供选择的声音列表很有用。我们将使用这里的方法来说明一些可用的声音。在下一个代码序列中,方法返回数组,然后显示数组的元素:
Voice[] voices = voiceManager.getVoices();
for (Voice v : voices) {
out.println(v);
}
输出将类似于以下内容:
CMUClusterUnitVoice
CMUDiphoneVoice
CMUDiphoneVoice
MbrolaVoice
MbrolaVoice
MbrolaVoice
getVoiceInfo方法提供了潜在的更有用的信息,尽管它有些冗长:
out.println(voiceManager.getVoiceInfo());
输出的第一部分如下:显示VoiceDirectory目录,随后是语音的详细信息。请注意,目录名包含声音的名称。KevinVoiceDirectory包含两种声音:kevin和kevin16:
VoiceDirectory 'com.sun.speech.freetts.en.us.cmu_time_awb.AlanVoiceDirectory'
Name: alan
Description: default time-domain cluster unit voice
Organization: cmu
Domain: time
Locale: en_US
Style: standard
Gender: MALE
Age: YOUNGER_ADULT
Pitch: 100.0
Pitch Range: 12.0
Pitch Shift: 1.0
Rate: 150.0
Volume: 1.0
VoiceDirectory 'com.sun.speech.freetts.en.us.cmu_us_kal.KevinVoiceDirectory'
Name: kevin
Description: default 8-bit diphone voice
Organization: cmu
Domain: general
Locale: en_US
Style: standard
Gender: MALE
Age: YOUNGER_ADULT
Pitch: 100.0
Pitch Range: 11.0
Pitch Shift: 1.0
Rate: 150.0
Volume: 1.0
Name: kevin16
Description: default 16-bit diphone voice
Organization: cmu
Domain: general
Locale: en_US
Style: standard
Gender: MALE
Age: YOUNGER_ADULT
Pitch: 100.0
Pitch Range: 11.0
Pitch Shift: 1.0
Rate: 150.0
Volume: 1.0
...
Using voices from a JAR file
声音可以存储在 JAR 文件中。VoiceDirectory类提供了对以这种方式存储的声音的访问。FreeTTs 可用的语音目录位于 lib 目录中,包括以下内容:
cmu_time_awb.jarcmu_us_kal.jar
语音目录的名称可以从命令提示符处获得:
java -jar fileName.jar
例如,执行以下命令:
java -jar cmu_time_awb.jar
它生成以下输出:
VoiceDirectory 'com.sun.speech.freetts.en.us.cmu_time_awb.AlanVoiceDirectory'
Name: alan
Description: default time-domain cluster unit voice
Organization: cmu
Domain: time
Locale: en_US
Style: standard
Gender: MALE
Age: YOUNGER_ADULT
Pitch: 100.0
Pitch Range: 12.0
Pitch Shift: 1.0
Rate: 150.0
Volume: 1.0
收集语音信息
Voice类提供了许多允许提取或设置语音特征的方法。正如我们前面所演示的,VoiceManager class' getVoiceInfo方法提供了关于当前可用声音的信息。然而,我们可以使用Voice类来获取关于特定声音的信息。
在下面的例子中,我们将显示关于声音kevin16的信息。我们首先使用getVoice方法获得这个voice的一个实例:
VoiceManager vm = VoiceManager.getInstance();
Voice voice = vm.getVoice("kevin16");
voice.allocate();
接下来,我们调用一些Voice类的get方法来获取关于声音的具体信息。这包括以前由getVoiceInfo方法提供的信息和其他不可用的信息;
out.println("Name: " + voice.getName());
out.println("Description: " + voice.getDescription());
out.println("Organization: " + voice.getOrganization());
out.println("Age: " + voice.getAge());
out.println("Gender: " + voice.getGender());
out.println("Rate: " + voice.getRate());
out.println("Pitch: " + voice.getPitch());
out.println("Style: " + voice.getStyle());
此示例的输出如下:
Name: kevin16
Description: default 16-bit diphone voice
Organization: cmu
Age: YOUNGER_ADULT
Gender: MALE
Rate: 150.0
Pitch: 100.0
Style: standard
这些结果是不言自明的,并让您了解可用信息的类型。还有其他方法可以让您访问通常不感兴趣的关于 TTS 过程的细节。这包括诸如正在使用的音频播放器、特定于话语的数据以及特定电话的功能等信息。
已经演示了如何将文本转换为语音,现在我们将研究如何将语音转换为文本。
理解语音识别
将语音转换为文本是一个重要的应用程序功能。这种能力越来越多地用于各种各样的环境中。仅举几个例子,语音输入用于控制智能电话,作为帮助台应用的一部分自动处理输入,以及帮助残疾人。
语音由复杂的音频流组成。声音可以拆分成个音素,这些音素是相似的声音序列。成对的这些音素被称为双音素。话语由单词和单词间各种类型的停顿组成。
转换过程的本质是通过话语间的沉默来分离声音。然后,将这些话语与听起来最像话语的单词进行匹配。然而,由于许多因素,这可能是困难的。例如,由于单词的上下文、地区方言、声音质量和其他因素,这些差异可能表现为单词发音的差异。
匹配过程相当复杂,并且经常使用多个模型。模型可以用于将声学特征与声音相匹配。可以使用语音模型来匹配音素和单词。另一个模型用于将单词搜索限制到给定的语言。这些模型从来都不是完全准确的,并且会导致识别过程中的不准确性。
我们将使用 CMUSphinx 4 来说明这个过程。
使用 CMUPhinx 将语音转换为文本
CMUSphinx 处理的音频必须是脉码调制 ( PCM )格式。PCM 是一种对模拟数据(如代表语音的模拟波)进行采样并产生数字信号的技术。FFmpeg(【ffmpeg.org/】)是一个免费的工具,…
您需要使用 PCM 格式创建样本音频文件。这些文件应该相当短,可以包含数字或单词。建议您使用不同的文件运行示例,看看语音识别的效果如何。
首先,我们通过创建一个处理异常的 try-catch 块来设置转换的基本框架。首先,创建一个Configuration类的实例。它用于配置识别器以识别标准英语。需要更改配置模型和字典来处理其他语言:
try {
Configuration configuration = new Configuration();
String prefix = "resource:/edu/cmu/sphinx/models/en-us/";
configuration
.setAcousticModelPath(prefix + "en-us");
configuration
.setDictionaryPath(prefix + "cmudict-en-us.dict");
configuration
.setLanguageModelPath(prefix + "en-us.lm.bin");
...
} catch (IOException ex) {
// Handle exceptions
}
然后使用configuration创建StreamSpeechRecognizer类。这个类基于输入流处理语音。在下面的代码中,我们从语音文件中创建了一个StreamSpeechRecognizer类的实例和一个InputStream:
StreamSpeechRecognizer recognizer = new StreamSpeechRecognizer(
configuration);
InputStream stream = new FileInputStream(new File("filename"));
为了开始语音处理,调用了startRecognition方法。getResult方法返回一个保存处理结果的SpeechResult实例。然后,我们使用SpeechResult方法来获得最佳结果。我们使用stopRecognition方法停止处理:
recognizer.startRecognition(stream);
SpeechResult result;
while ((result = recognizer.getResult()) != null) {
out.println("Hypothesis: " + result.getHypothesis());
}
recognizer.stopRecognition();
当这个语句被执行时,我们得到如下结果,假设语音文件包含这个句子:
Hypothesis: mary had a little lamb
当语音被解释时,可能有不止一个可能的单词序列。我们可以使用getNbest方法获得最佳结果,该方法的参数指定了应该返回多少种可能性。下面演示了这种方法:
Collection<String> results = result.getNbest(3);
for (String sentence : results) {
out.println(sentence);
}
一个可能的输出如下:
<s> mary had a little lamb </s>
<s> marry had a little lamb </s>
<s> mary had a a little lamb </s>
这给了我们基本的结果。然而,我们可能想用实际的语言做些什么。接下来解释获取单词的技术。
获得关于单词的更多细节
可以使用getWords方法提取结果中的单个单词,如下所示。该方法返回一列WordResult实例,每个实例代表一个单词:
List<WordResult> words = result.getWords();
for (WordResult wordResult : words) {
out.print(wordResult.getWord() + " ");
}
跟随<sil>的这个代码序列的输出反映了在讲话开始时发现的沉默:
<sil> mary had a little lamb
我们可以使用WordResult类的各种方法提取关于单词的更多信息。在下面的序列中,我们将返回与每个单词相关的置信度和时间范围。
getConfidence方法返回以对数表示的置信度。我们使用SpeechResult类的getResult方法来获得Result类的一个实例。然后使用它的getLogMath方法获得一个LogMath实例。向logToLinear方法传递置信度值,返回值是 0 到 1.0 之间的实数。更大的值反映了更多的信心。
getTimeFrame方法返回一个TimeFrame实例。它的toString方法返回两个整数值,用冒号分隔,反映单词的开始和结束时间:
for (WordResult wordResult : words) {
out.printf("%s\n\tConfidence: %.3f\n\tTime Frame: %s\n",
wordResult.getWord(), result
.getResult()
.getLogMath()
.logToLinear((float)wordResult
.getConfidence()),
wordResult.getTimeFrame());
}
一个可能的输出如下:
<sil>
Confidence: 0.998
Time Frame: 0:430
mary
Confidence: 0.998
Time Frame: 440:900
had
Confidence: 0.998
Time Frame: 910:1200
a
Confidence: 0.998
Time Frame: 1210:1340
little
Confidence: 0.998
Time Frame: 1350:1680
lamb
Confidence: 0.997
Time Frame: 1690:2170
既然我们已经研究了声音是如何被处理的,我们将把注意力转向图像处理。
从图像中提取文本
从图像中提取文字的过程称为OT2【光学字符识别 ( OCR )。当需要处理的文本数据嵌入到图像中时,这非常有用。例如,包含在牌照、路标和方向中的信息有时会非常有用。
我们可以使用 Tess4j(tess4j.sourceforge.net/)来执行 OCR,这是一个用于 Tesseract OCR API 的 Java JNA 包装器。我们将使用从维基百科关于 OCR 的文章中捕获的图像来演示如何使用 API(https://en . Wikipedia . org/wiki/Optical _ character _ recognition # Applications)。API 的 Javadoc 可以在 tess4j.sourceforge.net/docs/docs-3…:](tess4j.sourceforge.net/docs/docs-3…)
使用 Tess4j 提取文本
ITesseract接口包含许多 OCR 方法。doOCR方法获取一个文件并返回一个包含在文件中找到的单词的字符串,如下所示:
ITesseract instance = new Tesseract();
try {
String result = instance.doOCR(new File("OCRExample.png"));
out.println(result);
} catch (TesseractException e) {
// Handle exceptions
}
部分输出如下所示:
OCR engines nave been developed into many lunds oiobiectorlented OCR applicatlons, sucn as reoeipt OCR, involoe OCR, check OCR, legal billing document OCR
They can be used ior
- Data entry ior business documents, e g check, passport, involoe, bank statement and receipt
- Automatic number plate recognnlon
如你所见,这个例子中有许多错误。通常,在正确处理图像之前,需要提高图像的质量。提高输出质量的技术可以在https://github . com/tessera CT-ocr/tessera CT/wiki/improve quality找到。例如,我们可以使用setLanguage方法来指定处理的语言。此外,该方法通常在 TIFF 图像上效果更好。
在下一个示例中,我们使用了上一幅图像的放大部分,如下所示:
输出要好得多,如下所示:
OCR engines have been developed into many kinds of object-oriented OCR applications, such as receipt OCR,
invoice OCR, check OCR, legal billing document OCR.
They can be used for:
. Data entry for business documents, e.g. check, passport, invoice, bank statement and receipt
. Automatic number plate recognition
这些例子强调了仔细清理数据的必要性。
识别面孔
在许多情况下,识别图像中的人脸是有用的。它可以潜在地将图像分类为包含人的图像,或者在图像中找到人以供进一步处理。我们将使用 OpenCV 3.1(opencv.org/opencv-3-1.…)作为例子。
OpenCV(opencv.org/)是一个开源的计算机视觉库,支持几种编程语言,包括 Java。它支持许多技术,包括机器学习算法,来执行计算机视觉任务。该库支持诸如人脸检测、跟踪相机运动、提取 3D 模型以及从图像中去除红眼之类的操作。在本节中,我们将演示人脸检测。
使用 OpenCV 检测人脸
下面的例子改编自http://docs . opencv . org/trunk/d9/d52/tutorial _ Java _ dev _ intro . html。首先加载 OpenCV 安装时添加到系统中的本地库。在 Windows 上,这要求有适当的 DLL 文件可用:
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
我们使用一个基本字符串来指定所需 OpenCV 文件的位置。使用绝对路径可以更好地配合许多方法:
String base = "PathToResources";
CascadeClassifier类用于对象分类。在这种情况下,我们将使用它进行人脸检测。XML 文件用于初始化该类。在下面的代码中,我们使用了lbpcascade_frontalface.xml文件,它提供了帮助识别对象的信息。OpenCV 下载中有几个文件,如下所示,可用于特定的人脸识别场景:
lbpcascade_frontalcatface.xmllbpcascade_frontalface.xmllbpcascade_frontalprofileface.xmllbpcascade_silverware.xml
下面的语句初始化类以检测人脸:
CascadeClassifier faceDetector =
new CascadeClassifier(base +
"/lbpcascade_frontalface.xml");
加载要处理的图像,如下所示:
Mat image = Imgcodecs.imread(base + "/images.jpg");
对于此示例,我们使用了以下图像:
要找到这张图片,使用术语 people 进行谷歌搜索。选择图像类别,然后过滤掉标记为重复使用的**。图片有标签:LyndaSanchez 拍摄的一群正在笑的商务人士的特写肖像。**
当检测到人脸时,图像中的位置被存储在一个MatOfRect实例中。这个类用于保存找到的任何面的向量和矩阵:
MatOfRect faceVectors = new MatOfRect();
此时,我们已经准备好检测人脸。detectMultiScale方法执行这个任务。图像和保存任何图像位置的MatOfRect实例被传递给方法:
faceDetector.detectMultiScale(image, faceVectors);
下一条语句显示了检测到的人脸数量:
out.println(faceVectors.toArray().length + " faces found");
我们需要用这些信息来增强图像。这个过程将在每个找到的面周围画出方框,如下所示。为此,使用了Imgproc class' rectangle方法。对每个检测到的人脸调用一次该方法。向其传递要修改的图像和表示面部边界的点:
for (Rect rect : faceVectors.toArray()) {
Imgproc.rectangle(image, new Point(rect.x, rect.y),
new Point(rect.x + rect.width, rect.y + rect.height),
new Scalar(0, 255, 0));
}
最后一步使用Imgcodecs class' imwrite方法将该图像写入文件:
Imgcodecs.imwrite("faceDetection.png", image);
如下图所示,它能够识别四幅图像:
使用不同的配置文件将更好地适用于其他面部轮廓。
**# 分类可视数据
在本节中,我们将演示一种对可视数据进行分类的技术。我们将使用欧米诺来完成这一任务。Neuroph 是一个基于 Java 的神经网络框架,支持多种神经网络架构。它的开源库为其他应用程序提供支持和插件。在本例中,我们将使用其神经网络编辑器 Neuroph Studio 来创建一个网络。该网络可以被保存并在其他应用中使用。欧米诺工作室可以在这里下载:neuroph.sourceforge.net/download.ht…。我们正在构建这里显示的流程:【neuroph.sourceforge.net/image_recog…](neuroph.sourceforge.net/image_recog…)
对于我们的例子,我们将创建一个多层感知器 ( MLP )网络。然后我们将训练我们的网络来识别图像。我们可以使用 Neuroph Studio 来训练和测试我们的网络。了解 MLP 网络如何识别和解释图像数据非常重要。每个图像基本上都由三个二维数组表示。每个数组都包含颜色分量的信息:一个数组包含红色的信息,一个包含绿色的信息,一个包含蓝色的信息。数组的每个元素都保存了图像中某个特定像素的信息。然后将这些数组展平为一维数组,用作神经网络的输入。
创建一个用于分类视觉图像的 Neuroph Studio 项目
首先,创建一个新的 Neuroph Studio 项目:
我们将把我们的项目命名为RecognizeFaces,因为我们将训练神经网络来识别人脸图像:
接下来,我们在项目中创建新文件。有许多类型的项目可供选择,但我们将选择一个图像识别类型:
点击下一个然后点击添加目录。我们在本地机器上创建了一个目录,并添加了一些不同的黑白人脸图像用于训练。这些可以通过搜索谷歌图片或其他搜索引擎找到。从理论上讲,您用来训练的高质量图像越多,您的网络就越好:
点击下一个的后,您将被引导选择一个不可识别的图像。您可能需要根据您想要识别的图像尝试不同的图像。您在此选择的图像将防止错误识别。我们从本地机器上的另一个目录中选择了一个简单的蓝色方块,但是如果您使用其他类型的图像进行训练,其他色块可能会更好:
接下来,我们需要提供网络训练参数。我们还需要标记我们的训练数据集并设置我们的分辨率。高度和宽度20是一个很好的起点,但是您可能想要更改这些值来改善您的结果。可能会涉及一些尝试和错误。提供该信息的目的是允许图像缩放。当我们将图像缩小时,我们的网络可以更快地处理和学习它们:
最后,我们可以创建我们的网络。我们给我们的网络分配一个标签,定义我们的传递函数。默认功能, Sigmoid ,将适用于大多数网络,但如果你的结果不是最佳的,你可能想尝试 Tanh 。隐层神经元计数的默认数量是12,这是一个很好的起点。请注意,增加神经元的数量会增加训练网络所需的时间,并降低您将网络推广到其他图像的能力。与我们以前的一些值一样,为了找到给定网络的最佳设置,可能需要进行一些反复试验。完成后选择完成:
训练模型
一旦我们创建了我们的网络,我们需要训练它。首先双击左窗格中的神经网络。这是扩展名为.nnet的文件。执行此操作时,您将在主窗口中打开网络的可视化表示。然后将文件扩展名为.tsest的数据集从左侧窗格拖到神经网络的顶部节点。您会注意到节点上的描述更改为数据集的名称。接下来,点击位于窗口左上方的列车按钮:
这将打开一个带有培训设置的对话框。您可以保留最大误差、学习率和动量的默认值。确保勾选了显示误差图框。随着训练过程的继续,您可以看到错误率的提高:
点击 Train 按钮后,您应该会看到类似下图的错误图:
选择标题为图像识别测试的选项卡。然后点击选择测试图像按钮。我们已经加载了一个简单的人脸图像,它不包含在我们的原始数据集中:
找到输出标签。它将位于底部或左侧窗格中,并将显示我们的测试图像与训练集中的每个图像进行比较的结果。数字越大,我们的测试图像与来自我们的训练集的图像越接近。最后一幅图像产生了比前几次比较更大的输出数。如果我们比较这些图像,它们比数据集中的其他图像更相似,因此网络能够对我们的测试图像创建更积极的识别:
我们现在可以保存我们的网络供以后使用。从文件菜单中选择保存,然后您可以在外部应用程序中使用.nnet文件。下面的代码示例显示了一种通过您预先构建的神经网络运行测试数据的简单技术。NeuralNetwork类是 Neuroph 核心包的一部分,而load方法允许你将训练好的网络加载到你的项目中。注意,我们使用了我们的神经网络名称faces_net。然后,我们检索图像识别文件的插件。接下来,我们用一个新图像调用recognizeImage方法,它必须处理一个IOException。我们的结果存储在HashMap中,并打印到控制台:
NeuralNetwork iRNet = NeuralNetwork.load("faces_net.nnet");
ImageRecognitionPlugin iRFile
= (ImageRecognitionPlugin)iRNet.getPlugin(
ImageRecognitionPlugin.class);
try {
HashMap<String, Double> newFaceMap
= imageRecognition.recognizeImage(
new File("testFace.jpg"));
out.println(newFaceMap.toString());
} catch(IOException e) {
// Handle exceptions
}
这个过程允许我们使用 GUI 编辑器应用程序在更直观的环境中创建我们的网络,然后将训练好的网络嵌入到我们自己的应用程序中。
总结
在这一章中,我们演示了许多处理语音和图像的技术。随着电子设备越来越多地采用这些通信媒介,这种能力变得越来越重要。
使用 FreeTSS 演示了 TTS。这种技术允许计算机以语音而不是文本的形式呈现结果。我们学会了如何控制声音的属性,比如性别和年龄。
识别语音是有用的,有助于弥合人机界面的差距。我们演示了如何使用 CMUSphinx 来识别人类语音。由于语音通常有多种解释方式,我们学习了 API 如何返回各种选项。我们还演示了如何提取单个单词,以及识别正确单词的相对置信度。
图像处理是许多应用的重要方面。我们通过使用 Tess4J 从图像中提取文本开始了对图像处理讨论。这个过程有时被称为 OCR。我们了解到,与许多视频和音频数据文件一样,结果的质量与图像的质量有关。
我们还学习了如何使用 OpenCV 来识别图像中的人脸。关于面的特定视图(如正视图或侧视图)的信息包含在 XML 文件中。这些文件用于在图像中勾勒人脸。一次可以检测多张脸。
对图像进行分类会很有帮助,有时外部工具对这一目的很有用。我们检查了 Neuroph Studio,并创建了一个旨在识别和分类图像的神经网络。然后我们用人脸图像测试我们的网络。
在下一章中,我们将学习如何使用多个处理器来加速常见的数据科学应用。**
十一、数据分析的数学和并行技术
程序的并发执行可以显著提高性能。在这一章中,我们将讨论可用于数据科学应用的各种技术。这些范围从低级的数学计算到高级的特定于 API 的选项。
请始终记住,性能增强始于确保实现正确的应用程序功能集。如果应用程序不能满足用户的期望,那么这些改进就是徒劳的。应用程序的架构和使用的算法也比代码增强更重要。总是使用最有效的算法。然后应该考虑代码增强。我们无法在本章中解决更高层次的优化问题;相反,我们将关注代码增强。
许多数据科学应用程序和支持 API 使用矩阵运算来完成任务。通常这些操作隐藏在 API 中,但是有时候我们可能需要直接使用它们。无论如何,理解这些操作是如何被支持的是有益的。为此,我们将解释如何使用几种不同的方法来处理矩阵乘法。
可以使用 Java 线程实现并发处理。开发人员可以使用线程和线程池来改善应用程序的响应时间。当多个 CPU 或 GPU 不可用时,许多 api 会使用线程,就像 Aparapi 的情况一样。这里我们不举例说明线程的使用。但是,我们假设读者对线程和线程池有基本的了解。
map-reduce 算法广泛用于数据科学应用。我们将介绍一种使用 Apache 的 Hadoop 实现这种并行处理的技术。Hadoop 是一个支持大型数据集操作的框架,可以大大减少大型数据科学项目所需的处理时间。我们将演示一种计算样本数据集平均值的技术。
有几个著名的 API 支持多处理器,包括 CUDA 和 OpenCL。使用用于 CUDA(JCuda)(【jcuda.org/】T4)的 Java 绑定来支持 CUDA。我们不会在这里直接演示这种技术。然而,我们将使用的许多 API 确实支持 CUDA,如果它可用的话,比如 DL4J。我们将简要讨论 OpenCL 以及 Java 是如何支持它的。Aparapi API 提供了更高级别的支持,可能使用多个 CPU 或 GPU,这是没有价值的。我们将演示一个支持矩阵乘法的 Aparapi。
在本章中,我们将研究如何利用多个 CPU 和 GPU 来加速数据挖掘任务。我们使用的许多 API 已经利用了多处理器的优势,或者至少提供了一种支持 GPU 使用的方法。我们将在本章中介绍其中的一些选项。
云中也广泛支持并发处理。这里讨论的许多技术都在云中使用。因此,我们不会明确说明如何在云中进行并行处理。
实现基本矩阵运算
有几种不同类型的矩阵运算,包括简单的加法、减法、标量乘法和各种形式的乘法。为了说明矩阵运算,我们将关注所谓的矩阵乘积。这是一种常见的方法,涉及两个矩阵相乘以产生第三个矩阵。
考虑两个矩阵, A 和 B ,其中矩阵 A 有 n 行和 m 列。矩阵 B 将有 m 行和 p 列。 A 和 B 的乘积,写成 AB ,是一个 n 行和 p 列的矩阵。将矩阵 B 列的 m 个条目乘以 A 行的 m 个条目。这一点在此处有更明确的显示,其中:
其中产品定义如下:
我们从矩阵的声明和初始化开始。变量n、m、p代表矩阵的维数。A矩阵由m表示n,B矩阵由p表示m,代表产品的C矩阵由p表示n:
int n = 4;
int m = 2;
int p = 3;
double A[][] = {
{0.1950, 0.0311},
{0.3588, 0.2203},
{0.1716, 0.5931},
{0.2105, 0.3242}};
double B[][] = {
{0.0502, 0.9823, 0.9472},
{0.5732, 0.2694, 0.916}};
double C[][] = new double[n][p];
以下代码序列说明了使用嵌套for循环的乘法运算:
for (int i = 0; i < n; i++) {
for (int k = 0; k < m; k++) {
for (int j = 0; j < p; j++) {
C[i][j] += A[i][k] * B[k][j];
}
}
}
以下代码序列格式化输出以显示我们的矩阵:
out.println("\nResult");
for (int i = 0; i < n; i++) {
for (int j = 0; j < p; j++) {
out.printf("%.4f ", C[i][j]);
}
out.println();
}
结果如下所示:
Result
0.0276 0.1999 0.2132
0.1443 0.4118 0.5417
0.3486 0.3283 0.7058
0.1964 0.2941 0.4964
稍后,我们将演示执行相同操作的几种替代技术。接下来,我们将讨论如何使用 DL4J 定制对多处理器的支持。
使用 GPU 和 DeepLearning4j
DeepLearning4j 可以与 NVIDIA 提供的 GPU 一起工作。有一些选项可以启用 GPU 的使用,指定应该使用多少 GPU,以及控制 GPU 内存的使用。在本节中,我们将展示如何使用这些选项。这种类型的控件通常可用于其他高级 API。
DL4J 使用 n 维数组进行 Java(ND4J)(【nd4j.org/】T4)进行数值计算。… n 维数组对象和其他数值计算的库,例如线性代数和信号处理。它包括对 GPU 的支持,还集成了 Hadoop 和 Spark。
一个向量是一个一维数组,广泛用于神经网络。向量是一种叫做张量的数学结构。张量本质上是一个多维数组。我们可以把一个张量想象成一个三维或者多维的数组,每个维度称为一个秩。
通常需要将一组多维数字映射到一维数组。这是通过使用定义的顺序展平阵列来实现的。例如,对于二维数组,许多系统将按行列顺序分配数组成员。这意味着第一行被添加到向量中,接着是第二个向量,然后是第三个,依此类推。我们将在使用 ND4J API 一节中使用这种方法。
要启用 GPU,需要修改项目的 POM 文件。在 POM 文件的 properties 部分,需要添加或修改nd4j.backend标记,如下所示:
<nd4j.backend>nd4j-cuda-7.5-platform</<nd4j.backend>
可以使用ParallelWrapper类并行训练模型。训练任务在可用的 CPU/GPU 之间自动分配。该模型被用作ParallelWrapper类的Builder构造函数的参数,如下所示:
ParallelWrapper parallelWrapper =
new ParallelWrapper.Builder(aModel)
// Builder methods...
.build();
当执行时,在每个 GPU 上使用模型的副本。在通过averagingFrequency方法指定迭代次数之后,模型被平均,然后训练过程继续。
有多种方法可用于配置该类,如下表所示:
| 方法 | 目的 |
| prefetchBuffer | 指定用于预取数据的缓冲区的大小 |
| workers | 指定要使用的工人数量 |
| averageUpdaters``averagingFrequency``reportScoreAfterAveraging``useLegacyAveraging | 控制如何实现平均的各种方法 |
工作线程的数量应该大于可用 GPU 的数量。
与大多数计算一样,使用较低的精度值将加快处理速度。这可以通过setDTypeForContext方法来控制,如下图所示。在这种情况下,指定了半精度:
DataTypeUtil.setDTypeForContext(DataBuffer.Type.HALF);
这种支持和更多关于优化技术的细节可以在deeplearning4j.org/gpu找到。
使用地图缩小
Map-reduce 是一种以并行、分布式方式处理大型数据集的模型。这个模型由一个用于过滤和排序数据的map方法和一个用于汇总数据的reduce方法组成。map-reduce 框架非常有效,因为它将数据集的处理分布在多个服务器上,同时对较小的数据块执行映射和缩减。当以多线程方式实现时,Map-reduce 提供了显著的性能改进。在这一节中,我们将使用 Apache 的 Hadoop 实现来演示一项技术。在【使用 Java 8 执行 map-reduce 的 一节中,我们将讨论使用 Java 8 流执行 map-reduce 的技术。
Hadoop 是一个为并行计算提供支持的软件生态系统。Map-reduce 作业可以在 Hadoop 服务器上运行,通常设置为集群,以显著提高处理速度。Hadoop 具有在 Hadoop 集群中的节点上运行 map-reduce 操作的跟踪器。每个节点独立运行,跟踪器监控进程并整合每个节点的输出以生成最终输出。下图位于http://www . developer . com/Java/data/big-data-tool-map-reduce . html,展示了带有跟踪器的基本 map-reduce 模型。
使用 Apache 的 Hadoop 执行 map-reduce
我们将向您展示一个非常简单的地图缩小应用程序示例。在使用 Hadoop 之前,我们需要下载并提取 Hadoop 应用程序文件。最新版本可以在hadoop.apache.org/releases.ht…找到。在这个演示中,我们使用的是版本 2.7.3。
您需要设置您的JAVA_HOME环境变量。此外,Hadoop 不能容忍长文件路径和路径中的空格,因此请确保将 Hadoop 提取到尽可能简单的目录结构中。
我们将使用一个包含书籍信息的样本文本文件。制表符分隔的文件的每一行都有书名、作者和页数:
Moby Dick Herman Melville 822
Charlotte's Web E.B. White 189
The Grapes of Wrath John Steinbeck 212
Jane Eyre Charlotte Bronte 299
A Tale of Two Cities Charles Dickens 673
War and Peace Leo Tolstoy 1032
The Great Gatsby F. Scott Fitzgerald 275
我们将使用一个map函数来提取标题和页数信息,然后使用一个reduce函数来计算数据集中书籍的平均页数。首先,创建一个新的类,AveragePageCount。我们将在AveragePageCount中创建两个静态类,一个处理 map 过程,一个处理 reduction。
绘制地图的方法
首先,我们将创建TextMapper类,它将实现map方法。这个类继承自Mapper类,有两个私有实例变量,pages和bookTitle。pages是一个IntWritable对象,bookTitle是一个Text对象。使用IntWritable和Text是因为这些对象在被传输到服务器进行处理之前需要被序列化为字节流。这些物体比类似的int或String 物体占用更少的空间,传输速度更快:
public static class TextMapper
extends Mapper<Object, Text, Text, IntWritable> {
private final IntWritable pages = new IntWritable();
private final Text bookTitle = new Text();
}
在我们的TextMapper类中,我们创建了map方法。这个方法有三个参数:key对象、Text对象、bookInfo和Context。该键允许跟踪器将每个特定的对象映射回正确的作业。对象包含每本书的文本或字符串数据。Context保存关于整个系统的信息,并允许该方法报告系统内的进度和更新值。
在map方法中,我们使用split方法将每条图书信息分解成一个String对象数组。我们将变量bookTitle设置为数组的位置0,并将pages设置为存储在位置2中的值,然后将其解析为一个整数。然后,我们可以通过上下文写出书名和页数信息,并更新我们的整个系统:
public void map(Object key, Text bookInfo, Context context)
throws IOException, InterruptedException {
String[] book = bookInfo.toString().split("\t");
bookTitle.set(book[0]);
pages.set(Integer.parseInt(book[2]));
context.write(bookTitle, pages);
}
编写 reduce 方法
接下来,我们将编写我们的AverageReduce类。这个类扩展了Reducer类,并将执行归约过程来计算我们的平均页数。我们为这个类创建了四个变量:一个FloatWritable对象存储我们的平均页数,一个浮点average保存我们的临时平均值,一个浮点count计算我们的数据集中有多少本书,一个整数sum计算总页数:
public static class AverageReduce
extends Reducer<Text, IntWritable, Text, FloatWritable> {
private final FloatWritable finalAvg = new FloatWritable();
Float average = 0f;
Float count = 0f;
int sum = 0;
}
在我们的AverageReduce类中,我们将创建reduce方法。这个方法将一个Text键、一个保存代表页面计数的可写整数的Iterable对象和Context作为输入。我们使用迭代器来处理页面计数,并将每个页面计数添加到我们的总和中。然后我们计算平均值并设置finalAvg的值。该信息与一个Text对象标签配对,并被写入Context:
public void reduce(Text key, Iterable<IntWritable> pageCnts,
Context context)
throws IOException, InterruptedException {
for (IntWritable cnt : pageCnts) {
sum += cnt.get();
}
count += 1;
average = sum / count;
finalAvg.set(average);
context.write(new Text("Average Page Count = "), finalAvg);
}
创建并执行新的 Hadoop 作业
我们现在准备在同一个类中创建我们的main方法,并执行我们的 map-reduce 过程。为此,我们需要创建一个新的Configuration对象和一个新的Job。然后,我们设置要在应用程序中使用的重要类。
public static void main(String[] args) throws Exception {
Configuration con = new Configuration();
Job bookJob = Job.getInstance(con, "Average Page Count");
...
}
我们在setJarByClass方法中设置我们的主类AveragePageCount。我们分别使用setMapperClass和setReducerClass方法指定我们的TextMapper和AverageReduce类。我们还使用setOutputKeyClass和setOutputValueClass方法指定我们的输出将有一个基于文本的键和一个可写整数:
bookJob.setJarByClass(AveragePageCount.class);
bookJob.setMapperClass(TextMapper.class);
bookJob.setReducerClass(AverageReduce.class);
bookJob.setOutputKeyClass(Text.class);
bookJob.setOutputValueClass(IntWritable.class);
最后,我们使用addInputPath和setOutputPath方法创建新的输入和输出路径。这些方法都将我们的Job对象作为第一个参数,将代表我们的输入和输出文件位置的Path对象作为第二个参数。我们于是称之为waitForCompletion。一旦这个调用返回 true,我们的应用程序就退出:
FileInputFormat.addInputPath(bookJob, new Path("C:/Hadoop/books.txt"));
FileOutputFormat.setOutputPath(bookJob, new
Path("C:/Hadoop/BookOutput"));
if (bookJob.waitForCompletion(true)) {
System.exit(0);
}
要执行应用程序,打开命令提示符并导航到包含我们的AveragePageCount.class文件的目录。然后,我们使用以下命令来执行我们的示例应用程序:
hadoop AveragePageCount
当我们的任务运行时,我们会在屏幕上看到关于流程输出的更新信息。我们的输出示例如下所示:
...
File System Counters
FILE: Number of bytes read=1132
FILE: Number of bytes written=569686
FILE: Number of read operations=0
FILE: Number of large read operations=0
FILE: Number of write operations=0
Map-Reduce Framework
Map input records=7
Map output records=7
Map output bytes=136
Map output materialized bytes=156
Input split bytes=90
Combine input records=0
Combine output records=0
Reduce input groups=7
Reduce shuffle bytes=156
Reduce input records=7
Reduce output records=7
Spilled Records=14
Shuffled Maps =1
Failed Shuffles=0
Merged Map outputs=1
GC time elapsed (ms)=11
Total committed heap usage (bytes)=536870912
Shuffle Errors
BAD_ID=0
CONNECTION=0
IO_ERROR=0
WRONG_LENGTH=0
WRONG_MAP=0
WRONG_REDUCE=0
File Input Format Counters
Bytes Read=249
File Output Format Counters
Bytes Written=216
如果我们打开在本地机器上创建的BookOutput目录,我们会发现四个新文件。使用文本编辑器打开part-r-00000。该文件包含使用并行进程计算的平均页数信息。该输出的示例如下:
Average Page Count = 673.0
Average Page Count = 431.0
Average Page Count = 387.0
Average Page Count = 495.75
Average Page Count = 439.0
Average Page Count = 411.66666
Average Page Count = 500.2857
请注意,当每个单独的过程与其他减少过程相结合时,平均值是如何变化的。这与首先计算前两本书的平均值,然后加入第三本书,然后第四本书,以此类推的效果相同。这里的优点当然是平均是以并行方式完成的。如果我们有一个巨大的数据集,我们应该会看到在执行时间上的明显优势。BookOutput的最后一行反映了所有七个页面计数的正确和最终平均值。
各种数学库
有许多数学库可供 Java 使用。在这一节中,我们将提供几个库的快速和高层次的概述。这些库不一定自动支持多处理器。此外,本节的目的是提供一些关于如何使用这些库的见解。在大多数情况下,它们相对容易使用。
Java 数学库的列表可以在 https://en . Wikipedia . org/wiki/List _ of _ numerical _ libraries # Java 和 java-matrix.org/的[找到。我们将演示 jblas、Apache Commons Math 和 ND4J 库的使用。](java-matrix.org/)
使用 jblas API
jblas API(jblas.org/)是一个支持 Java 的数学库。它基于基础线性代数子程序 ( 布拉斯)(www.netlib.org/blas/)和线性代数包(LAPACK)(www.netlib.org/lapack/),是快速算术计算的标准库。jblas API 提供了这些库的包装器。
下面是如何执行矩阵乘法的演示。我们从矩阵定义开始:
DoubleMatrix A = new DoubleMatrix(new double[][]{
{0.1950, 0.0311},
{0.3588, 0.2203},
{0.1716, 0.5931},
{0.2105, 0.3242}});
DoubleMatrix B = new DoubleMatrix(new double[][]{
{0.0502, 0.9823, 0.9472},
{0.5732, 0.2694, 0.916}});
DoubleMatrix C;
执行乘法的实际语句非常短,如下所示。对A矩阵执行mmul方法,其中B数组作为参数传递:
C = A.mmul(B);
然后显示生成的C矩阵:
for(int i=0; i<C.getRows(); i++) {
out.println(C.getRow(i));
}
输出应该如下所示:
[0.027616, 0.199927, 0.213192]
[0.144288, 0.411798, 0.541650]
[0.348579, 0.328344, 0.705819]
[0.196399, 0.294114, 0.496353]
这个库非常容易使用,并且支持大量的算术运算。
使用 Apache Commons 数学应用编程接口
Apache Commons math API(commons.apache.org/proper/comm…)支持大量的数学和统计运算。以下示例说明了如何执行矩阵乘法。
我们从声明和初始化A和B矩阵开始:
double[][] A = {
{0.1950, 0.0311},
{0.3588, 0.2203},
{0.1716, 0.5931},
{0.2105, 0.3242}};
double[][] B = {
{0.0502, 0.9823, 0.9472},
{0.5732, 0.2694, 0.916}};
Apache Commons 使用RealMatrix类来保存一个矩阵。在下面的代码序列中,使用Array2DRowRealMatrix构造函数创建了A和B矩阵的对应矩阵:
RealMatrix aRealMatrix = new Array2DRowRealMatrix(A);
RealMatrix bRealMatrix = new Array2DRowRealMatrix(B);
使用 multiply 方法进行乘法运算非常简单,如下所示:
RealMatrix cRealMatrix = aRealMatrix.multiply(bRealMatrix);
下一个for循环将显示以下结果:
for (int i = 0; i < cRealMatrix.getRowDimension(); i++) {
out.println(cRealMatrix.getRowVector(i));
}
输出应该如下所示:
{0.02761552; 0.19992684; 0.2131916}
{0.14428772; 0.41179806; 0.54165016}
{0.34857924; 0.32834382; 0.70581912}
{0.19639854; 0.29411363; 0.4963528}
使用 ND4J API
ND4J(【nd4j.org/】)是 DL4J 用来进行算术运算的库。该库也可以直接使用。在本节中,我们将演示如何使用A和B矩阵执行矩阵乘法。
在执行乘法之前,我们需要将矩阵展平为向量。下面声明并初始化这些向量:
double[] A = {
0.1950, 0.0311,
0.3588, 0.2203,
0.1716, 0.5931,
0.2105, 0.3242};
double[] B = {
0.0502, 0.9823, 0.9472,
0.5732, 0.2694, 0.916};
给定一个向量和维度信息,Nd4j class' create方法创建一个INDArray实例。该方法的第一个参数是向量。第二个参数指定矩阵的维数。最后一个参数指定行和列的布局顺序。这种顺序或者是如c所示的行列优先顺序,或者是 FORTRAN 使用的行列优先顺序。行列顺序意味着第一行被分配给向量,接着是第二行,依此类推。
在下面的代码序列中,使用A和B向量创建了2INDArray实例。第一个是由第三个参数c指定的使用行优先顺序的4行、2列矩阵。第二个INDArray实例表示B矩阵。如果我们想使用行列排序,我们将使用一个f来代替。
INDArray aINDArray = Nd4j.create(A,new int[]{4,2},'c');
INDArray bINDArray = Nd4j.create(B,new int[]{2,3},'c');
由cINDArray表示的C数组随后被声明,并被赋予乘法的结果。mmul执行操作:
INDArray cINDArray;
cINDArray = aINDArray.mmul(bINDArray);
以下序列使用getRow方法显示结果:
for(int i=0; i<cINDArray.rows(); i++) {
out.println(cINDArray.getRow(i));
}
输出应该如下所示:
[0.03, 0.20, 0.21]
[0.14, 0.41, 0.54]
[0.35, 0.33, 0.71]
[0.20, 0.29, 0.50]
接下来,我们将提供对 OpenCL API 的概述,该 API 为许多平台上的并发操作提供支持。
使用 OpenCL
开放计算语言(OpenCL)(【www.khronos.org/opencl/】T4)…** ( FPGA ,以及其他类型的处理器。
OpenCL 使用基于 C99 的语言对设备进行编程,为编程并发行为提供了标准接口。OpenCL 支持允许用不同语言编写代码的 API。对于 Java,有几个 API 支持开发基于 OpenCL 的语言:
- OpenCL 的 Java 绑定(JOCL)(【www.jocl.org/】T4)——这是一个到… OpenCL C 实现的绑定,可能会很冗长。
- JavaCl(【code.google.com/archive/p/j… JOCL 提供一个面向对象的接口。
- Java OpenCL(【jogamp.org/jocl/www/】T… JOCL 抽象。它不是供客户端使用的。
- 轻量级 Java 游戏库(LWJGL)(【www.lwjgl.org/】T4)——也为 OpenCL 提供支持,面向 GUI 应用。
此外,Aparapi 提供了对 OpenCL 的高级访问,从而避免了创建 OpenCL 应用程序所涉及的一些复杂性。
运行在处理器上的代码被封装在内核中。多个内核将在不同的计算设备上并行执行。OpenCL 支持不同级别的内存。特定设备可能不支持每个级别。这些级别包括:
- 全局内存 -由所有计算单元共享
- 只读存储器 -一般不可写
- 本地存储器 -由一组计算单元共享
- 每个元素的私有内存 -通常是一个寄存器
OpenCL 应用程序需要大量的初始代码才能发挥作用。这种复杂性不允许我们提供其使用的详细示例。然而,Aparapi 部分确实提供了一些 OpenCL 应用程序是如何构造的感觉。
使用 Aparapi
APAR API(github.com/aparapi/apa…)是一个支持并发操作的 Java 库。API 支持在 GPU 或 CPU 上运行的代码。GPU 操作使用 OpenCL 执行,CPU 操作使用 Java 线程。用户可以指定使用哪个计算资源。但是,如果 GPU 支持不可用,Aparapi 将恢复到 Java 线程。
API 将在运行时将 Java 字节代码转换成 OpenCL。这使得 API 很大程度上独立于所使用的显卡。该 API 最初是由 AMD 开发的,但已经作为开源软件发布。这反映在基本包名com.amd.aparari中。Aparapi 提供了比 OpenCL 更高层次的抽象。
Aparapi 代码位于从Kernel类派生的类中。它的execute方法将开始操作。这将导致对一个run方法的内部调用,该方法需要被覆盖。并发代码放在run方法中。run方法在不同的处理器上执行多次。
由于 OpenCL 的限制,我们不能使用继承或方法重载。此外,它不喜欢run方法中的println,因为代码可能运行在 GPI 上。Aparapi 只支持一维数组。使用二维或更多维的数组需要展平为一维数组。对双精度值的支持取决于 OpenCL 版本和 GPU 配置。
当使用 Java 线程池时,它为每个 CPU 内核分配一个线程。包含 Java 代码的内核被克隆,每个线程一个副本。这避免了跨线程访问数据的需要。每个线程都可以访问信息,如全局 ID,以帮助代码执行。内核将等待所有线程完成。
Aparapi 下载可以在 github.com/aparapi/apa… 找到。
创建 Aparapi 应用程序
Aparapi 应用程序的基本框架如下所示。它由一个Kernel派生类组成,其中run方法被重写。在这个例子中,run方法将执行标量乘法。这种运算包括将向量的每个元素乘以某个值。
ScalarMultiplicationKernel扩展了Kernel类。它拥有两个用于保存输入和输出矩阵的实例变量。构造函数将初始化矩阵。run方法将执行实际的计算,displayResult方法将显示乘法的结果:
public class ScalarMultiplicationKernel extends Kernel {
float[] inputMatrix;
float outputMatrix [];
public ScalarMultiplicationKernel(float inputMatrix[]) {
...
}
@Override
public void run() {
...
}
public void displayResult() {
...
}
}
此处显示了构造函数:
public ScalarMultiplicationKernel(float inputMatrix[]) {
this.inputMatrix = inputMatrix;
outputMatrix = new float[this.inputMatrix.length];
}
在run方法中,我们使用一个全局 ID 来索引矩阵。该代码在每个计算单元上执行,例如 GPU 或线程。为每个计算单元提供唯一的全局 ID,允许代码访问矩阵的特定元素。在这个例子中,输入矩阵的每个元素乘以2,然后分配给输出矩阵的相应元素:
public void run() {
int globalID = this.getGlobalId();
outputMatrix[globalID] = 2.0f * inputMatrix[globalID];
}
displayResult方法只是显示outputMatrix数组的内容:
public void displayResult() {
out.println("Result");
for (float element : outputMatrix) {
out.printf("%.4f ", element);
}
out.println();
}
为了使用这个内核,我们需要为inputMatrix和它的size声明变量。size将用于控制执行多少内核:
float inputMatrix[] = {3, 4, 5, 6, 7, 8, 9};
int size = inputMatrix.length;
然后使用输入矩阵创建内核,接着调用execute方法。该方法启动流程,并最终基于execute方法的参数调用Kernel类的run方法。该参数被称为通行证 ID。虽然在本例中没有使用,但我们将在下一节中使用它。当流程完成时,显示结果输出矩阵,并调用dispose方法停止流程:
ScalarMultiplicationKernel kernel =
new ScalarMultiplicationKernel(inputMatrix);
kernel.execute(size);
kernel.displayResult();
kernel.dispose();
当执行该应用程序时,我们将得到以下输出:
6.0000 8.0000 10.0000 12.0000 14.0000 16.0000 18.000
我们可以使用内核类'setExecutionMode方法指定执行模式,如下所示:
kernel.setExecutionMode(Kernel.EXECUTION_MODE.GPU);
但是,最好让 Aparapi 来决定执行模式。下表总结了可用的执行模式:
| 执行模式 | 意为 |
| Kernel.EXECUTION_MODE.NONE | 不指定模式 |
| Kernel.EXECUTION_MODE.CPU | 使用 CPU |
| Kernel.EXECUTION_MODE.GPU | 使用 GPU |
| Kernel.EXECUTION_MODE.JTP | 使用 Java 线程 |
| Kernel.EXECUTION_MODE.SEQ | 使用单循环(用于调试目的) |
接下来,我们将演示如何使用 Aparapi 来执行点积矩阵乘法。
使用 Aparapi 进行矩阵乘法
我们将使用在实现基本矩阵运算部分中使用的矩阵。我们从MatrixMultiplicationKernel类的声明开始,它包含向量声明、一个构造函数、run方法和一个displayResults方法。通过按行列顺序分配矩阵,矩阵A和B的向量已被展平为一维数组:
class MatrixMultiplicationKernel extends Kernel {
float[] vectorA = {
0.1950f, 0.0311f, 0.3588f,
0.2203f, 0.1716f, 0.5931f,
0.2105f, 0.3242f};
float[] vectorB = {
0.0502f, 0.9823f, 0.9472f,
0.5732f, 0.2694f, 0.916f};
float[] vectorC;
int n;
int m;
int p;
@Override
public void run() {
...
}
public MatrixMultiplicationKernel(int n, int m, int p) {
...
}
public void displayResults () {
...
}
}
MatrixMultiplicationKernel构造函数为矩阵的维数赋值,并为存储在vectorC,中的结果分配内存,如下所示:
public MatrixMultiplicationKernel(int n, int m, int p) {
this.n = n;
this.p = p;
this.m = m;
vectorC = new float[n * p];
}
run 方法使用全局 ID 和通道 ID 来执行矩阵乘法。pass ID 被指定为Kernel class' execute方法的第二个参数,我们很快就会看到。这个值允许我们提升vectorC的列索引。向量索引映射到原始矩阵的相应行和列位置:
public void run() {
int i = getGlobalId();
int j = this.getPassId();
float value = 0;
for (int k = 0; k < p; k++) {
value += vectorA[k + i * m] * vectorB[k * p + j];
}
vectorC[i * p + j] = value;
}
displayResults方法如下所示:
public void displayResults() {
out.println("Result");
for (int i = 0; i < n; i++) {
for (int j = 0; j < p; j++) {
out.printf("%.4f ", vectorC[i * p + j]);
}
out.println();
}
}
内核的启动方式与上一节相同。向execute方法传递应该创建的内核数和一个整数,该整数表示要传递的次数。通过次数用于控制进入vectorA和vectorB阵列的索引:
MatrixMultiplicationKernel kernel = new MatrixMultiplicationKernel(n, m,
p);kernel.execute(6, 3);kernel.displayResults();
kernel.dispose();
当执行此示例时,您将获得以下输出:
Result
0.0276 0.1999 0.2132
0.1443 0.4118 0.5417
0.3486 0.3283 0.7058
0.1964 0.2941 0.4964
接下来,我们将看到 Java 8 新增功能如何以并行方式帮助解决数学密集型问题。
使用 Java 8 流
Java 8 的发布对该语言进行了大量重要的增强。我们感兴趣的两个增强包括 lambda 表达式和流。lambda 表达式本质上是一个匿名函数,它为 Java 增加了一个函数式编程维度。Java 8 中引入的流的概念不是指 IO 流。相反,您可以将其视为一系列对象,可以使用流畅的编程风格来生成和操作这些对象。这种风格将很快演示。
与大多数 API 一样,程序员必须使用真实的测试用例和环境来仔细考虑他们代码的实际执行性能。如果使用不当,流实际上可能不会提供性能改进。特别是并行流,如果不仔细处理,可能会产生不正确的结果。
我们将从快速介绍 lambda 表达式和流开始。如果你熟悉这些概念,你可以跳过下一节。
理解 Java 8 lambda 表达式和流
lambda 表达式可以用几种不同的形式表示。下面是一个简单的 lambda 表达式,其中符号->是 lambda 运算符。这将采用某个值e,并返回乘以 2 的值。e这个名字没什么特别的。可以使用任何有效的 Java 变量名:
e -> 2 * e
它也可以用其他形式表示,例如:
(int e) -> 2 * e
(double e) -> 2 * e
(int e) -> {return 2 * e;
使用的形式取决于e的预期值。Lambda 表达式经常被用作方法的参数,我们很快就会看到。
可以使用多种技术创建流。在下面的示例中,流是从数组创建的。IntStream接口是一种使用整数的流。方法将一个数组转换成一个流:
IntStream stream = Arrays.stream(numbers);
然后,我们可以应用各种stream方法来执行操作。在下面的语句中,forEach方法将简单地显示流中的每个整数:
stream.forEach(e -> out.printf("%d ", e));
有各种各样的stream方法可以应用于一个流。在下面的例子中,mapToDouble方法将取一个整数,乘以2,然后返回一个double。forEach方法将显示这些值:
stream
.mapToDouble(e-> 2 * e)
.forEach(e -> out.printf("%.4f ", e));
方法调用的级联被称为流畅编程。
使用 Java 8 执行矩阵乘法
这里,我们将说明如何使用流来执行矩阵乘法。矩阵A、B和C的定义与实现基本矩阵运算一节中的定义相同。为了方便起见,这里复制了它们:
double A[][] = {
{0.1950, 0.0311},
{0.3588, 0.2203},
{0.1716, 0.5931},
{0.2105, 0.3242}};
double B[][] = {
{0.0502, 0.9823, 0.9472},
{0.5732, 0.2694, 0.916}};
double C[][] = new double[n][p];
以下序列是矩阵乘法的流实现。代码的详细解释如下:
C = Arrays.stream(A)
.parallel()
.map(AMatrixRow -> IntStream.range(0, B[0].length)
.mapToDouble(i -> IntStream.range(0, B.length)
.mapToDouble(j -> AMatrixRow[j] * B[j][i])
.sum()
).toArray()).toArray(double[][]::new);
第一个map方法,如下所示,创建了一个表示A矩阵的4行的双向量流。range方法将返回从第一个参数到第二个参数的流元素列表。
.map(AMatrixRow -> IntStream.range(0, B[0].length)
变量i对应第二个range方法生成的数字,对应B矩阵(2)中的行数。变量j对应第三个range方法生成的数字,代表B矩阵的列数(3)。
该语句的核心是矩阵乘法,其中的sum方法计算总和:
.mapToDouble(j -> AMatrixRow[j] * B[j][i])
.sum()
表达式的最后一部分为 C 矩阵创建二维数组。操作符::new称为方法引用,是调用 new 操作符创建新对象的一种更简短的方式:
).toArray()).toArray(double[][]::new);
displayResult方法如下:
public void displayResult() {
out.println("Result");
for (int i = 0; i < n; i++) {
for (int j = 0; j < p; j++) {
out.printf("%.4f ", C[i][j]);
}
out.println();
}
}
该序列的输出如下:
Result
0.0276 0.1999 0.2132
0.1443 0.4118 0.5417
0.3486 0.3283 0.7058
0.1964 0.2941 0.4964
使用 Java 8 执行 map-reduce
在下一节中,我们将使用 Java 8 streams 执行一个 map-reduce 操作,类似于在使用 map-reduce 一节中使用 Hadoop 演示的操作。在这个例子中,我们将使用Book个对象中的Stream。然后,我们将演示如何使用 Java 8 reduce和average方法来获得总页数和平均页数。
我们没有像在 Hadoop 示例中那样从文本文件开始,而是创建了一个带有标题、作者和页数字段的Book类。在driver类的main方法中,我们创建了Book的新实例,并将它们添加到名为books的ArrayList中。我们还创建了一个double值average来保存我们的平均值,并将变量totalPg初始化为零:
ArrayList<Book> books = new ArrayList<>();
double average;
int totalPg = 0;
books.add(new Book("Moby Dick", "Herman Melville", 822));
books.add(new Book("Charlotte's Web", "E.B. White", 189));
books.add(new Book("The Grapes of Wrath", "John Steinbeck", 212));
books.add(new Book("Jane Eyre", "Charlotte Bronte", 299));
books.add(new Book("A Tale of Two Cities", "Charles Dickens", 673));
books.add(new Book("War and Peace", "Leo Tolstoy", 1032));
books.add(new Book("The Great Gatsby", "F. Scott Fitzgerald", 275));
接下来,我们执行映射和归约操作来计算帐套中的总页数。为了以并行的方式实现这一点,我们使用了stream和parallel方法。然后我们使用带有 lambda 表达式的map方法来累计每个Book对象的所有页面计数。最后,我们使用reduce方法将我们的页面计数合并成一个最终值,该值将被分配给totalPg:
totalPg = books
.stream()
.parallel()
.map((b) -> b.pgCnt)
.reduce(totalPg, (accumulator, _item) -> {
out.println(accumulator + " " +_item);
return accumulator + _item;
});
注意,在前面的reduce方法中,我们选择了打印出关于归约操作的累积值和单个项目的信息。accumulator代表我们页面计数的集合。_item表示 map-reduce 流程中在任何给定时刻都在进行缩减的单个任务。
在接下来的输出中,我们将首先看到在处理每个图书项目时,accumulator值保持为零。逐渐地,accumulator值增加。最后的操作是减少1223和2279的值。这两个数字的总和就是3502,或者说我们所有书籍的总页数:
0 822
0 189
0 299
0 673
0 212
299 673
0 1032
0 275
1032 275
972 1307
189 212
822 401
1223 2279
接下来,我们将添加代码来计算帐套的平均页数。当我们除以由size方法返回的整数时,我们将使用 map-reduce 确定的totalPg值乘以1.0以防止截断。然后我们打印出average。
average = 1.0 * totalPg / books.size();
out.printf("Average Page Count: %.4f\n", average);
我们的输出如下:
Average Page Count: 500.2857
我们可以使用 Java 8 streams 直接使用map方法来计算平均值。将以下代码添加到main方法中。我们使用parallelStream和map方法来同时获得我们每本书的页数。然后,我们使用mapToDouble来确保我们的数据是正确的类型,以计算我们的平均值。最后,我们使用average和getAsDouble方法来计算我们的平均页数:
average = books
.parallelStream()
.map(b -> b.pgCnt)
.mapToDouble(s -> s)
.average()
.getAsDouble();
out.printf("Average Page Count: %.4f\n", average);
然后我们打印出我们的平均值。我们的输出与前面的示例相同,如下所示:
Average Page Count: 500.2857
这些技术利用与 map-reduce 框架相关的 Java 8 功能来解决数值问题。这种类型的过程也可以应用于其他类型的数据,包括基于文本的数据。当这些流程在大大缩短的时间框架内处理极大的数据集时,真正的好处就显现出来了。
总结
数据科学广泛使用数学来分析问题。有许多可用的 Java 数学库,其中许多都支持并发操作。在这一章中,我们介绍了一些库和技术,让我们深入了解如何使用它们来支持和提高应用程序的性能。
我们从讨论如何执行简单的矩阵乘法开始。给出了一个基本的 Java 实现。在后面的小节中,我们使用其他 API 和技术复制了实现。
许多高级 API,如 DL4J,支持许多有用的数据分析技术。在这些 API 之下,通常是对多个 CPU 和 GPU 的并发支持。有时这种支持是可配置的,例如 DL4J。我们简要讨论了如何配置 ND4J 来支持多处理器。
map-reduce 算法在数据科学领域得到了广泛的应用。我们利用这个框架的并行处理能力来计算一组给定值的平均值,即一组书的页数。这种技术使用 Apache 的 Hadoop 来执行 map 和 reduce 函数。
大量的库支持数学技术。许多这些库不直接支持并行操作。然而,了解什么是可用的以及如何使用它们是很重要的。为此,我们演示了如何使用三种不同的 Java API:jblas、Apache Commons Math 和 ND4J。
OpenCL 是一种 API,支持在各种硬件平台、处理器类型和语言上的并行操作。这种支持是相当低的水平。OpenCL 有许多 Java 绑定,我们已经讨论过了。
Aparapi 是对 Java 的高级支持,可以使用 CPU、CUDA 或 OpenCL 来实现并行操作。我们用矩阵乘法的例子演示了这种支持。
我们以对 Java 8 流和 lambda 表达式的介绍结束了我们的讨论。这些语言元素可以支持并行操作,从而提高应用程序的性能。此外,一旦程序员熟悉了这些技术,这通常可以提供更优雅、更易维护的实现。我们还演示了使用 Java 8 执行 map-reduce 的技术。
在下一章,我们将通过举例说明有多少介绍的技术可以用来构建一个完整的应用程序来结束这本书。
十二、将这一切结合在一起
虽然我们已经展示了使用 Java 支持数据科学任务的许多方面,但是仍然需要以集成的方式组合和使用这些技术。孤立地使用这些技术是一回事,以连贯的方式使用它们是另一回事。在本章中,我们将为您提供这些技术的额外经验,以及如何将它们结合使用的见解。
具体来说,我们将创建一个基于控制台的应用程序,分析与用户定义的主题相关的 tweets。使用基于控制台的应用程序使我们能够专注于特定于数据科学的技术,并避免选择可能与我们无关的特定 GUI 技术。它提供了一个公共基础,如果需要,可以从这个基础上创建 GUI 实现。
该应用程序执行并演示了以下高级任务:
-
数据采集
-
Data cleaning, including:
-
删除停用词
-
Cleaning the text
- 情感分析
- 基础数据统计收集
- 结果显示
-
这些步骤中的许多步骤可以使用多种类型的分析。我们将展示更相关的方法,并适当提及其他可能性。我们将尽可能使用 Java 8 的特性。
定义我们应用的目的和范围
该应用程序将提示用户输入一组选择标准,包括主题和子主题区域,以及要处理的 tweets 数量。执行的分析将简单地计算和显示一个主题和子主题的正面和负面推文的数量。我们使用了通用的情感分析模型,这将影响情感分析的质量。但是,可以添加其他模型和更多分析。
我们将使用 Java 8 流来构建 tweet 数据的处理。它是一串TweetHandler对象,我们将很快描述。
我们在这个应用程序中使用了几个类。它们总结如下:
- 这个类保存原始的 tweet 文本和处理所需的特定字段,包括实际的 tweet、用户名和类似的属性。
TwitterStream:用于获取应用程序的数据。使用特定的类将数据的获取与处理分开。该类拥有几个控制如何获取数据的字段。ApplicationDriver:这包含了main方法、用户提示和控制分析的TweetHandler流。
这些类中的每一个都将在后面的章节中详细介绍。然而,我们将在接下来的ApplicationDriver中概述分析过程以及用户如何与应用程序交互。
了解应用程序的架构
每个应用程序都有自己独特的结构或架构。这种架构为应用程序提供了总体的组织或框架。对于这个应用程序,我们在ApplicationDriver类中使用 Java 8 流来组合这三个类。这个类由三个方法组成:
ApplicationDriver:包含应用程序的用户输入performAnalysis:执行分析main:创建ApplicationDriver实例
接下来显示了类结构。三个实例变量用于控制处理:
public class ApplicationDriver {
private String topic;
private String subTopic;
private int numberOfTweets;
public ApplicationDriver() { ... }
public void performAnalysis() { ... }
public static void main(String[] args) {
new ApplicationDriver();
}
}
接下来是ApplicationDriver构造函数。创建一个Scanner实例并构建情感分析模型:
public ApplicationDriver() {
Scanner scanner = new Scanner(System.in);
TweetHandler swt = new TweetHandler();
swt.buildSentimentAnalysisModel();
...
}
该方法的其余部分提示用户输入,然后调用performAnalysis方法:
out.println("Welcome to the Tweet Analysis Application");
out.print("Enter a topic: ");
this.topic = scanner.nextLine();
out.print("Enter a sub-topic: ");
this.subTopic = scanner.nextLine().toLowerCase();
out.print("Enter number of tweets: ");
this.numberOfTweets = scanner.nextInt();
performAnalysis();
performAnalysis方法使用从TwitterStream实例获得的 Java 8 Stream实例。TwitterStream类构造函数使用 tweets 的数量和topic作为输入。该类在使用 Twitter 的数据采集部分讨论:
public void performAnalysis() {
Stream<TweetHandler> stream = new TwitterStream(
this.numberOfTweets, this.topic).stream();
...
}
该流使用一系列的map、filter和一个forEach方法来执行处理。map方法修改流的元素。filter方法从流中移除元素。forEach方法将终止流并生成输出。
流的各个方法按顺序执行。当从公共 Twitter 流获取 Twitter 信息时,Twitter 信息以 JSON 文档的形式到达,我们首先对其进行处理。这允许我们提取相关的 tweet 信息,并将数据设置到TweetHandler实例的字段中。接下来,推文的文本被转换成小写。只处理英语推文,并且只处理包含子主题的推文。然后处理推文。最后一步计算统计数据:
stream
.map(s -> s.processJSON())
.map(s -> s.toLowerCase())
.filter(s -> s.isEnglish())
.map(s -> s.removeStopWords())
.filter(s -> s.containsCharacter(this.subTopic))
.map(s -> s.performSentimentAnalysis())
.forEach((TweetHandler s) -> {
s.computeStats();
out.println(s);
});
然后显示处理结果:
out.println();
out.println("Positive Reviews: "
+ TweetHandler.getNumberOfPositiveReviews());
out.println("Negative Reviews: "
+ TweetHandler.getNumberOfNegativeReviews());
我们在周一晚上的一场足球比赛中测试了我们的应用程序,使用的主题是#MNF。 # 符号被称为标签,用于对推文进行分类。通过选择一个流行的推文类别,我们确保了我们将有大量的推文数据来处理。为了简单起见,我们选择了足球副题。对于这个例子,我们还选择只分析 50 条推文。以下是我们的提示、输入和输出的简短示例:
Building Sentiment Model
Welcome to the Tweet Analysis Application
Enter a topic: #MNF
Enter a sub-topic: football
Enter number of tweets: 50
Creating Twitter Stream
51 messages processed!
Text: rt @ bleacherreport : touchdown , broncos ! c . j . anderson punches ! lead , 7 - 6 # mnf # denvshou
Date: Mon Oct 24 20:28:20 CDT 2016
Category: neg
...
Text: i cannot emphasize enough how big td drive . @ broncos offense . needed confidence booster & amp ; just got . # mnf # denvshou
Date: Mon Oct 24 20:28:52 CDT 2016
Category: pos
Text: least touchdown game . # mnf
Date: Mon Oct 24 20:28:52 CDT 2016
Category: neg
Positive Reviews: 13
Negative Reviews: 27
我们打印出每条推文的文本,以及时间戳和类别。请注意,推文的文本并不总是有意义的。这可能是因为 Twitter 数据的缩写性质,但部分原因是因为该文本已被清理,停用词已被删除。我们仍然应该看到我们的主题,#MNF,尽管由于我们的文本清理,它将是小写的。最后,我们打印出分为正面和负面的推文总数。
推文的分类是通过performSentimentAnalysis方法完成的。注意,使用情感分析的分类过程并不总是精确的。下面的推文提到了一名丹佛野马队球员触地得分。根据个人对该团队的个人感受,这条推文可以被解释为积极或消极的,但我们的模型将其归类为积极的:
Text: cj anderson td run @ broncos . broncos now lead 7 - 6 . # mnf
Date: Mon Oct 24 20:28:42 CDT 2016
Category: pos
此外,一些推文可能有中性语气,如下图所示,但仍可分为正面或负面。下面这条推文是一个热门体育新闻推特账号的转发,@bleacherreport:
Text: rt @ bleacherreport : touchdown , broncos ! c . j . anderson punches ! lead , 7 - 6 # mnf # denvshou
Date: Mon Oct 24 20:28:37 CDT 2016
Category: neg
这条推文被归类为负面,但也许可以被认为是中立的。推文的内容只是提供了一场足球比赛的比分信息。这是积极的还是消极的事件将取决于一个人可能支持哪个队。当我们检查分析的整套 tweet 数据时,我们注意到同一条@bleacherreport tweet 被转发了很多次,每次都被归类为负面。当我们考虑到我们可能有大量分类不当的推文时,这可能会扭曲我们的分析。使用不正确的数据会降低结果的准确性。
根据分析的目的,一种选择可能是排除新闻媒体或其他受欢迎的 Twitter 用户的推文。此外,我们可以排除带有 RT 的推文,这是一个缩写,表示该推文是另一个用户的转发。
在执行这种类型的分析时,还需要考虑其他问题,包括所使用的子主题。如果我们要分析一个星球大战角色的受欢迎程度,那么我们需要小心我们使用的名字。例如,当选择诸如韩 Solo 的角色名称时,推文可能会使用别名。汉·索洛的别名包括维克·德雷戈、里斯多、杰诺斯·伊达尼安、索洛·贾萨尔、神枪手和乔贝克·琼恩,仅举几个例子(T1)。可能会用演员的名字来代替实际的角色,在韩索罗的情况下是哈里森·福特。我们也可以考虑演员的昵称,比如 Harry 代表 Harrison。
使用 Twitter 获取数据
Twitter API 与 HBC 的 HTTP 客户端结合使用来获取推文,如前面第二章、数据采集的处理 Twitter 部分所述。这个过程包括使用默认访问级别的公共流 API 来提取当前在 Twitter 上流动的公共 tweets 的样本。我们将根据用户选择的关键字来提炼数据。
首先,我们声明了TwitterStream类。它由两个实例变量(numberOfTweets和topic)、两个构造函数和一个stream方法组成。numberOfTweets变量包含要选择和处理的推文数量,而topic允许用户搜索与特定主题相关的推文。我们已经设置了默认的构造函数来提取与Star Wars相关的100 tweets:
public class TwitterStream {
private int numberOfTweets;
private String topic;
public TwitterStream() {
this(100, "Stars Wars");
}
public TwitterStream(int numberOfTweets, String topic) { ... }
}
我们的TwitterStream类的核心是stream方法。我们首先使用创建 Twitter 应用程序时 Twitter 提供的信息执行身份验证。然后我们创建一个BlockingQueue对象来保存我们的流数据。在本例中,我们将设置默认容量1000。我们在trackTerms方法中使用topic变量来指定我们要搜索的推文类型。最后,我们指定我们的endpoint并关闭失速警告:
String myKey = "mySecretKey";
String mySecret = "mySecret";
String myToken = "myToKen";
String myAccess = "myAccess";
out.println("Creating Twitter Stream");
BlockingQueue<String> statusQueue = new
LinkedBlockingQueue<>(1000);
StatusesFilterEndpoint endpoint = new StatusesFilterEndpoint();
endpoint.trackTerms(Lists.newArrayList("twitterapi", this.topic));
endpoint.stallWarnings(false);
现在我们可以使用OAuth1创建一个Authentication对象,这是OAuth类的一个变种。这允许我们构建我们的连接客户端并完成 HTTP 连接:
Authentication twitterAuth = new OAuth1(myKey, mySecret, myToken,
myAccess);
BasicClient twitterClient = new ClientBuilder()
.name("Twitter client")
.hosts(Constants.STREAM_HOST)
.endpoint(endpoint)
.authentication(twitterAuth)
.processor(new StringDelimitedProcessor(statusQueue))
.build();
twitterClient.connect();
接下来,我们创建两个数组列表,list保存我们的TweetHandler对象,twitterList保存从 Twitter 流出的 JSON 数据。我们将在下一节讨论TweetHandler对象。我们使用drainTo方法代替第 2 章、数据采集中演示的poll方法,因为它对于大量数据更有效:
List<TweetHandler> list = new ArrayList();
List<String> twitterList = new ArrayList();
接下来,我们遍历检索到的消息。我们调用take方法从BlockingQueue实例中移除每个字符串消息。然后,我们使用该消息创建一个新的TweetHandler对象,并将其放入我们的list中。在我们处理完所有消息并且 for 循环完成之后,我们停止 HTTP 客户端,显示消息的数量,并返回我们的TweetHandler对象流:
statusQueue.drainTo(twitterList);
for(int i=0; i<numberOfTweets; i++) {
String message;
try {
message = statusQueue.take();
list.add(new TweetHandler(message));
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
twitterClient.stop();
out.printf("%d messages processed!\n",
twitterClient.getStatsTracker().getNumMessages());
return list.stream();
}
我们现在准备清理和分析我们的数据。
了解 TweetHandler 类
TweetHandler类保存关于特定 tweet 的信息。它获取原始的 JSON tweet,并提取与应用程序需求相关的部分。它还拥有处理 tweet 文本的方法,比如将文本转换成小写,删除不相关的 tweet。该类的第一部分如下所示:
public class TweetHandler {
private String jsonText;
private String text;
private Date date;
private String language;
private String category;
private String userName;
...
public TweetHandler processJSON() { ... }
public TweetHandler toLowerCase(){ ... }
public TweetHandler removeStopWords(){ ... }
public boolean isEnglish(){ ... }
public boolean containsCharacter(String character) { ... }
public void computeStats(){ ... }
public void buildSentimentAnalysisModel{ ... }
public TweetHandler performSentimentAnalysis(){ ... }
}
实例变量显示了从 tweet 中检索并处理的数据类型,如下所示:
jsonText:原始 JSON 文本text:处理后的 tweet 的文本date:推文的日期- 推文的语言
category:推文分类,正面还是负面userName:Twitter 用户的名字
该类还使用了其他几个实例变量。以下内容用于创建和使用情感分析模型。分类器静态变量指的是模型:
private static String[] labels = {"neg", "pos"};
private static int nGramSize = 8;
private static DynamicLMClassifier<NGramProcessLM>
classifier = DynamicLMClassifier.createNGramProcess(
labels, nGramSize);
默认构造函数用于提供一个实例来构建情感模型。单参数构造函数使用原始 JSON 文本创建一个TweetHandler对象:
public TweetHandler() {
this.jsonText = "";
}
public TweetHandler(String jsonText) {
this.jsonText = jsonText;
}
其余的方法将在下面的章节中讨论。
为情感分析模型提取数据
在第九章、文本分析中,我们使用 DL4J 进行了情感分析。在这个例子中,我们将使用 LingPipe 作为前面方法的替代方法。因为我们想要对 Twitter 数据进行分类,所以我们选择了一个包含预先分类的推文的数据集,可以在http://thinknook . com/WP-content/uploads/2012/09/perspective-Analysis-dataset . zip获得。在继续我们的应用程序开发之前,我们必须完成一个一次性的过程,将这些数据提取为我们的模型可以使用的格式。
这个数据集存在于一个大的.csv文件中,每行有一条 tweet 和分类。推文被分为0(负面)或1(正面)。以下是该数据文件中一行的示例:
95,0,Sentiment140, - Longest night ever.. ugh! http://tumblr.com/xwp1yxhi6
第一个元素代表一个惟一的 ID 号,它是原始数据集的一部分,我们将用它作为文件名。第二个元素是分类,第三个是数据集标签(在本项目中被忽略),最后一个元素是实际的 tweet 文本。在将这些数据用于我们的 LingPipe 模型之前,我们必须将每条 tweet 写入一个单独的文件。为此,我们创建了三个字符串变量。根据每条推文的分类,filename变量将被赋予pos或neg,并将用于写操作。我们还使用file变量保存单个 tweet 文件的名称,使用text变量保存单个 tweet 文本。接下来,我们使用readAllLines方法和Paths类的get方法将数据存储在List对象中。我们还需要指定字符集StandardCharsets.ISO_8859_1:
try {
String filename;
String file;
String text;
List<String> lines = Files.readAllLines(
Paths.get("\\path-to-file\\SentimentAnalysisDataset.csv"),
StandardCharsets.ISO_8859_1);
...
} catch (IOException ex) {
// Handle exceptions
}
现在我们可以循环遍历我们的列表,并使用split方法将我们的.csv数据存储在一个字符串数组中。我们将位置1的元素转换为整数,并确定它是否是一个1。用1分类的推文被认为是正面推文,我们将filename设置为pos。所有其他推文将filename设置为neg。我们从位置0的元素中提取输出文件名,从元素3中提取文本。出于本项目的目的,我们忽略位置2的标签。最后,我们写出我们的数据:
for(String s : lines) {
String[] oneLine = s.split(",");
if(Integer.parseInt(oneLine[1])==1) {
filename = "pos";
} else {
filename = "neg";
}
file = oneLine[0]+".txt";
text = oneLine[3];
Files.write(Paths.get(
path-to-file\\txt_sentoken"+filename+""+file),
text.getBytes());
}
注意,我们在txt_sentoken目录中创建了neg和pos目录。当我们读取文件来构建模型时,这个位置很重要。
建立情感模型
现在我们已经准备好构建我们的模型了。我们遍历包含pos和neg的labels数组,并为每个标签创建一个新的Classification对象。然后我们使用这个标签创建一个新文件,并使用listFiles方法创建一个文件名数组。接下来,我们将使用一个for循环遍历这些文件名:
public void buildSentimentAnalysisModel() {
out.println("Building Sentiment Model");
File trainingDir = new File("\\path to file\\txt_sentoken");
for (int i = 0; i < labels.length; i++) {
Classification classification =
new Classification(labels[i]);
File file = new File(trainingDir, labels[i]);
File[] trainingFiles = file.listFiles();
...
}
}
在for循环中,我们提取 tweet 数据并将其存储在我们的字符串review中。然后我们使用review和classification创建一个新的Classified对象。最后我们可以调用handle方法来分类这个特殊的文本:
for (int j = 0; j < trainingFiles.length; j++) {
try {
String review = Files.readFromFile(trainingFiles[j],
"ISO-8859-1");
Classified<CharSequence> classified = new
Classified<>(review, classification);
classifier.handle(classified);
} catch (IOException ex) {
// Handle exceptions
}
}
对于上一节中讨论的数据集,此过程可能需要相当长的时间。然而,我们认为这种时间权衡是值得的,这种训练数据使分析质量成为可能。
处理 JSON 输入
Twitter 数据是使用 JSON 格式检索的。我们将使用 Twitter 4j(twitter4j.org)提取 tweet 的相关部分,并存储在TweetHandler类的相应字段中。
TweetHandler类的processJSON方法执行实际的数据提取。基于 JSON 文本创建了一个JSONObject的实例。该类拥有几种从对象中提取特定类型数据的方法。我们使用getString方法来获得我们需要的字段。
接下来显示了processJSON方法的开始,我们从获取JSONObject实例开始,我们将使用它来提取 tweet 的相关部分:
public TweetHandler processJSON() {
try {
JSONObject jsonObject = new JSONObject(this.jsonText);
...
} catch (JSONException ex) {
// Handle exceptions
}
return this;
}
首先,我们提取推文的文本,如下所示:
this.text = jsonObject.getString("text");
接下来,我们提取推文的日期。我们使用SimpleDateFormat类将日期字符串转换成一个Date对象。它的构造函数被传递一个指定日期字符串格式的字符串。我们使用了字符串"EEE MMM d HH:mm:ss Z yyyy",其组成部分将在下面详述。字符串元素的顺序对应于 JSON 实体中的顺序:
EEE:用三个字符指定的星期几MMM:月份,三位字符d:一个月中的某一天HH:mm:ss:时、分、秒Z:时区yyyy:年份
代码如下:
SimpleDateFormat sdf = new SimpleDateFormat(
"EEE MMM d HH:mm:ss Z yyyy");
try {
this.date = sdf.parse(jsonObject.getString("created_at"));
} catch (ParseException ex) {
// Handle exceptions
}
剩余的字段将被提取,如下所示。我们必须提取一个中间 JSON 对象来提取name字段:
this.language = jsonObject.getString("lang");
JSONObject user = jsonObject.getJSONObject("user");
this.userName = user.getString("name");
获取并提取了文本后,我们现在准备执行清理数据的重要任务。
清理数据以改善我们的结果
数据清理是大多数数据科学问题的关键步骤。没有正确清理的数据可能会有错误,如拼写错误、日期等元素的不一致表示以及无关的单词。
我们可以对 Twitter 数据应用许多数据清理选项。对于这个应用程序,我们执行简单的清理。另外,我们会过滤掉某些推文。
文本到小写字母的转换很容易实现,如下所示:
public TweetHandler toLowerCase() {
this.text = this.text.toLowerCase().trim();
return this;
}
这个过程的一部分是删除某些不需要的推文。例如,下面的代码说明了如何检测推文是否是英文的,以及它是否包含用户感兴趣的子主题。Java 8 流中的filter方法使用了boolean返回值,它执行实际的删除:
public boolean isEnglish() {
return this.language.equalsIgnoreCase("en");
}
public boolean containsCharacter(String character) {
return this.text.contains(character);
}
许多其他清理操作可以很容易地添加到该过程中,如删除前导和尾随空白,替换制表符,以及验证日期和电子邮件地址。
删除停用词
停用词是那些无助于理解或处理数据的词。典型的停用词包括 0、and、a 和 or。当它们对数据处理没有贡献时,它们可以被移除以简化处理并使其更有效。
有几种去除停用词的技巧,在第 9 章、文本分析中讨论。对于这个应用程序,我们将使用 LingPipe(alias-i.com/lingpipe/)来删除停止字。我们使用EnglishStopTokenizerFactory 类来获得基于IndoEuropeanTokenizerFactory实例的停用词模型:
public TweetHandler removeStopWords() {
TokenizerFactory tokenizerFactory
= IndoEuropeanTokenizerFactory.INSTANCE;
tokenizerFactory =
new EnglishStopTokenizerFactory(tokenizerFactory);
...
return this;
}
提取一系列不包含停用词的标记,并使用一个StringBuilder实例创建一个字符串来替换原始文本:
Tokenizer tokens = tokenizerFactory.tokenizer(
this.text.toCharArray(), 0, this.text.length());
StringBuilder buffer = new StringBuilder();
for (String word : tokens) {
buffer.append(word + " ");
}
this.text = buffer.toString();
我们使用的 LingPipe 模型可能不是最适合所有推文的。此外,有人提出,从推文中删除停用词可能不会有成效(oro.open.ac.uk/40666/)。可以将选择各种停用词以及是否应该删除停用词的选项添加到流过程中。
执行情感分析
现在,我们可以使用本章“构建情感模型”一节中构建的模型来执行情感分析。我们通过将清理后的文本传递给classify方法来创建一个新的Classification对象。然后我们使用bestCategory方法将我们的文本分为正面或负面。最后,我们将category设置为结果,并返回TweetHandler对象:
public TweetHandler performSentimentAnalysis() {
Classification classification =
classifier.classify(this.text);
String bestCategory = classification.bestCategory();
this.category = bestCategory;
return this;
}
我们现在准备分析我们的应用程序的结果。
分析结果
这个应用程序中执行的分析相当简单。一旦推文被分类为正面或负面,总数就会被计算出来。为此,我们使用了两个静态变量:
private static int numberOfPositiveReviews = 0;
private static int numberOfNegativeReviews = 0;
从 Java 8 流中调用computeStats方法,并增加适当的变量:
public void computeStats() {
if(this.category.equalsIgnoreCase("pos")) {
numberOfPositiveReviews++;
} else {
numberOfNegativeReviews++;
}
}
两种static方法提供对评论数量的访问:
public static int getNumberOfPositiveReviews() {
return numberOfPositiveReviews;
}
public static int getNumberOfNegativeReviews() {
return numberOfNegativeReviews;
}
此外,还提供了一个简单的toString方法来显示基本的 tweet 信息:
public String toString() {
return "\nText: " + this.text
+ "\nDate: " + this.date
+ "\nCategory: " + this.category;
}
可以根据需要添加更复杂的分析。这个应用程序的目的是演示一种组合各种数据处理任务的技术。
其他可选配件
可以对应用程序进行许多改进。其中许多是用户偏好,其他的与改进应用程序的结果有关。GUI 界面在许多情况下都很有用。在用户选项中,我们可能希望添加对以下内容的支持:
- 显示个人推文
- 允许空的子主题
- 处理其他 tweet 字段
- 提供主题或子主题列表,供用户选择
- 生成附加统计数据和支持图表
关于过程结果的改进,应考虑以下几点:
- 纠正拼写错误的用户条目
- 删除标点符号周围的空格
- 使用替代的停用字词删除技术
- 使用替代的情感分析技术
这些增强的细节取决于所使用的 GUI 界面以及应用程序的目的和范围。
总结
本章旨在说明如何将各种数据科学任务集成到应用程序中。我们选择了一个处理推文的应用程序,因为它是一个流行的社交媒体,允许我们应用前面章节中讨论的许多技术。
使用了一个简单的基于控制台的界面,以避免特定但可能不相关的 GUI 细节扰乱讨论。应用程序提示用户输入 Twitter 主题、子主题和要处理的 tweets 数量。分析包括确定推文的情绪,以及关于推文积极或消极性质的简单统计。
这个过程的第一步是建立一个情感模型。我们使用 LingPipe 类来构建模型并执行分析。使用了 Java 8 流,它支持流畅的编程风格,可以轻松地添加和删除各个处理步骤。
一旦流被创建,JSON 原始文本被处理并用于初始化一个TweetHandler类。这个类的实例随后被修改,包括将文本转换成小写,删除非英语 tweet,删除停用词,以及只选择包含子主题的 tweet。然后进行情感分析,接着进行统计数据的计算。
数据科学是一个广泛的主题,利用了大量的统计和计算机科学主题。在本书中,我们简要介绍了这些主题,以及 Java 是如何支持它们的。