在利用深度学习网络进行预测性分析之前,我们首先需要对其加以训练。目前市面上存在着大量能够用于神经网络训练的工具,但TensorFlow无疑是其中极为重要的首选方案之一。
大家可以利用TensorFlow训练自己的机器学习模型,并利用这些模型完成预测性分析。训练通常由一台极为强大的设备或者云端资源完成,但您可能想象不到的是,TensorFlow亦可以在iOS之上顺利起效——只是存在一定局限性。
在今天的博文中,我们将共同了解TensorFlow背后的设计思路、如何利用其训练一套简单的分类器,以及如何将上述成果引入您的iOS应用。
在本示例中,我们将使用 数据集以了解如何根据音频记录判断语音为男声抑或女声。
获取相关代码: 大家可以通过GitHub上的 获取本示例的源代码。
TensorFlow是什么,我们为何需要加以使用?
TensorFlow是一套用于构建计算性图形,从而实现机器学习的软件资源库。
其它一些工具往往作用于更高级别的抽象层级。以Caffe为例,大家需要将不同类型的“层”进行彼此互连,从而设计出一套神经网络。而iOS平台上的BNNS与MPSCNN亦可实现类似的功能。
在TensorFlow当中,大家亦可处理这些层,但具体处理深度将更为深入——甚至直达您算法中的各项计算流程。
大家可以将TensorFlow视为一套用于实现新型机器学习算法的工具集,而其它深度学习工具则用于帮助用户使用这些算法。
当然,这并不是说用户需要在TensorFlow当中从零开始构建一切。TensorFlow拥有一整套可复用的构建组件,同时囊括了Keras等负责为TensorFlow用户提供大量便捷模块的资源库。
因此TensorFlow在使用当中并不强制要求大家精通相关数学专业知识,当然如果各位愿意自行构建,TensorFlow也能够提供相应的工具。
利用逻辑回归实现二元分类
在今天的博文当中,我们将利用逻辑回归( logistic regression )算法创建一套分类器。没错,我们将从零开始进行构建,因此请大家做好准备——这可是项有点复杂的任务。所谓分类器,其基本工作原理是获取输入数据,而后告知用户该数据所归属的类别——或者种类。在本项目当中,我们只设定两个种类:男声与女声——也就是说,我们需要构建的是一套二元分类器( binary classifier )。
备注:二元分类器属于最简单的一种分类器,但其基本概念与设计思路同用于区分成百上千种不同类别的分类器完全一致。因此,尽管我们在本份教程中不会太过深入,但相信大家仍然能够从中一窥分类器设计的门径。
在输入数据方面,我们将使用包含20个数字朗读语音、囊括多种声学特性的给定录音。我将在后文中对此进行详尽解释,包括音频频率及其它相关信息。
在以下示意图当中,大家可以看到这20个数字全部接入一个名为sum的小框。这些连接拥有不同的weights(权重),对于分类器而言代表着这20个数字各自不同的重要程度。
以下框图展示了这套逻辑分类器的起效原理:
深度学习指南:在iOS平台上使用TensorFlow
在sum框当中,输入数据区间为x0到x19,且其对应连接的权重w0到w19进行直接相加。以下为一项常见的点积:
sum = x[0]w[0] + x[1]w[1] + x[2]w[2] + ... + x[19]w[19] + b
我们还在所谓bias(偏离)项的末尾加上了b。其仅仅代表另一个数字。
数组w中的权重与值b代表着此分类器所学习到的经验。对该分类器进行训练的过程,实际上是为了帮助其找到与w及b正确匹配的数字。最初,我们将首先将全部w与b设置为0。在数轮训练之后,w与b则将包含一组数字,分类器将利用这些数字将输入语音中的男声与女声区分开来。为了能够将sum转化为一条概率值——其取值在0与1之间——我们在这里使用logistic sigmoid函数:
y_pred = 1 / (1 + exp(-sum))
这条方程式看起来很可怕,但做法却非常简单:如果sum是一个较大正数,则sigmoid函数返回1或者概率为100%; 如果sum是一个较大负数,则sigmoid函数返回0。因此对于较大的正或者负数,我们即可得出较为肯定的“是”或者“否”预测结论。
然而,如果sum趋近于0,则sigmoid函数会给出一个接近于50%的概率,因为其无法确定预测结果。当我们最初对分类器进行训练时,其初始预期结果会因分类器本身训练尚不充分而显示为50%,即对判断结果并无信心。但随着训练工作的深入,其给出的概率开始更趋近于1及0,即分类器对于结果更为肯定。
现在y_pred中包含的预测结果显示,该语音为男声的可能性更高。如果其概率高于0.5(或者50%),则我们认为语音为男声; 相反则为女声。
这即是我们这套利用逻辑回归实现的二元分类器的基本设计原理。输入至该分类器的数据为一段对20个数字进行朗读的音频记录,我们会计算出一条权重sum并应用sigmoid函数,而我们获得的输出概率指示朗读者应为男性。
然而,我们仍然需要建立用于训练该分类器的机制,而这时就需要请出今天的主角——TensorFlow了。
在TensorFlow中实现此分类器
要在TensorFlow当中使用此分类器,我们需要首先将其设计转化为一套计算图( computational graph) 。一项计算图由多个负责执行计算的节点组成,且输入数据会在各节点之间往来流通。
我们这套逻辑回归算法的计算图如下所示:
深度学习指南:在iOS平台上使用TensorFlow
看起来与之前给出的示意图存在一定区别,但这主要是由于此处的输入内容x不再是20个独立的数字,而是一个包含有20个元素的向量。在这里,权重由矩阵W表示。因此,之前得出的点积也在这里被替换成了一项矩阵乘法。
另外,本示意图中还包含一项输入内容y。其用于对分类器进行训练并验证其运行效果。我们在这里使用的数据集为一套包含3168条example语音记录的集合,其中每条示例记录皆被明确标记为男声或女声。这些已知男声或女声结果亦被称为该数据集的label(标签),并作为我们交付至y的输入内容。
为了训练我们的分类器,这里需要将一条示例加载至x当中并允许该计算图进行预测:即语音到底为男声抑或是女声?由于初始权重值全部为0,因此该分类器很可能给出错误的预测。我们需要一种方法以计算其错误的“具体程度”,而这一目标需要通过loss函数实现。Loss函数会将预测结果y_pred与正确输出结果y进行比较。
在将loss函数提供给训练示例后,我们利用一项被称为backpropagation(反向传播)的技术通过该计算图进行回溯,旨在根据正确方向对W与b的权重进行小幅调整。如果预测结果为男声但实际结果为女声,则权重值即会稍微进行上调或者下调,从而在下一次面对同样的输入内容时增加将其判断为“女声”的概率。
这一训练规程会利用该数据集中的全部示例进行不断重复再重复,直到计算图本身已经获得了最优权重集合。而负责衡量预测结果错误程度的loss函数则因此随时间推移而变低。
反向传播在计算图的训练当中扮演着极为重要的角色,但我们还需要加入一点数学手段让结果更为准确。而这也正是TensorFlow的专长所在:我们只要将全部“前进”操作表达为计算图当中的节点,其即可自动意识到“后退”操作代表的是反向传播——我们完全无需亲自进行任何数学运算。太棒了!
Tensorflow到底是什么?
在以上计算图当中,数据流向为从左至右,即代表由输入到输出。而这正是TensorFlow中“流(flow)”的由来。不过Tensor又是什么?
Tensor一词本义为张量,而此计算图中全部数据流皆以张量形式存在。所谓张量,其实际代表的就是一个n维数组。我曾经提到W是一项权重矩阵,但从TensorFlow的角度来看,其实际上属于一项二阶张量——换言之,一个二组数组。
一个标量代表一个零阶张量。
一个向量代表一个一阶张量。
一个矩阵代表一个二阶张量。
一个三维数组代表一个三阶张量。
之后以此类推…… 深度学习指南:在iOS平台上使用TensorFlow
我们首先需要强调这一点,这里列出的并非实际音频数据!相反,这些数字代表着语音记录当中的不同声学特征。这些属性或者特征由一套脚本自音频记录中提取得出,并被转化为这个CSV文件。具体提取方式并不属于本篇文章希望讨论的范畴,但如果大家感兴趣,则可 查阅其原始R源代码。
这套数据集中包含3168项示例(每项示例在以上表格中作为一行),且基本半数为男声录制、半数为女声录制。每一项示例中存在20项声学特征,例如:
以kHz为单位的平均频率
频率的标准差
频谱平坦度
频谱熵
峰度
声学信号中测得的最大基频
调制指数
等等……
别担心,虽然我们并不了解其中大多数条目的实际意义,但这不会影响到本次教程。我们真正需要 关心的是如何利用这些数据训练自己的分类器,从而立足于上述特征确保其有能力区分男性与女性的语音。
如果大家希望在一款应用程序当中使用此分类器,从而通过录音或者来自麦克风的音频信息检测语音性别,则首先需要从此类音频数据中提取声学特征。在拥有了这20个数字之后,大家即可对其分类器加以训练,并利用其判断语音内容为男声还是女声。
因此,我们的分类器并不会直接处理音频记录,而是处理从记录中提取到的声学特征。
备注:我们可以以此为起点了解深度学习与逻辑回归等传统算法之间的差异。我们所训练的分类器无法学习非常复杂的内容,大家需要在预处理阶段提取更多数据特征对其进行帮助。在本示例的特定数据集当中,我们只需要考虑提取音频记录中的音频数据。
深度学习最酷的能力在于,大家完全可以训练一套神经网络来学习如何自行提取这些声学特征。如此一来,大家不必进行任何预处理即可利用深度学习系统采取原始音频作为输入内容,并从中提取任何其认为重要的声学特征,而后加以分类。
这当然也是一种有趣的深度学习探索方向,但并不属于我们今天讨论的范畴,因此也许日后我们将另开一篇文章单独介绍。
建立一套训练集与测试集
在前文当中,我提到过我们需要以如下步骤对分类器进行训练:
向其交付来自数据集的全部示例。
衡量预测结果的错误程度。
根据loss调整权重。
事实证明,我们不应利用全部数据进行训练。我们只需要其中的特定一部分数据——即测试集——从而评估分类器的实际工作效果。因此,我们将把整体数据集拆分为两大部分:训练集,用于对分类器进行训练; 测试集,用于了解该分类器的预测准确度。
为了将数据拆分为训练集与测试集,我创建了一套名为split_data.py的Python脚本,其内容如下所示:
import numpy as np # 1 import pandas as pd df = pd.read_csv("voice.csv", header=0) # 2 labels = (df["label"] == "male").values * 1 # 3 labels = labels.reshape(-1, 1) # 4 del df["label"] # 5 data = df.values # 6 from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split(data, labels, test_size=0.3, random_state=123456) np.save("X_train.npy", X_train) # 7 np.save("X_test.npy", X_test) np.save("y_train.npy", y_train) np.save("y_test.npy", y_test)
下面我们将分步骤了解这套脚本的工作方式:
首先导入NumPy与pandas软件包。Pandas能够轻松实现CSV文件的加载,并对数据进行预处理。
利用pandas从voice.csv加载数据集并将其作为dataframe。此对象在很大程度上类似于电子表格或者SQL表。
这里的label列包含有该数据集的各项标签:即该示例为男声或者女声。在这里,我们将这些标签提取进一个新的NumPy数组当中。各原始标签为文本形式,但我们将其转化为数字形式,其中1=男声,0=女声。(这里的数字赋值方式可任意选择,在二元分类器中,我们通常使用1表示‘正’类,或者说我们试图检测的类。)
这里创建的新labels数组是一套一维数组,但我们的TensorFlow脚本则需要一套二维张量,其中3168行中每一行皆对应一列。因此我们需要在这里对数组进行“重塑”,旨在将其转化为二维形式。这不会对内存中的数据产生影响,而仅变化NumPy对数据的解释方式。
在完成label列之后,我们将其从dataframe当中移除,这样我们就只剩下20项用于描述输入内容的特征。我们还将把该dataframe转换为一套常规NumPy数组。
这里,我们利用来自scikit-learn的一项helper函数将data与labels数组拆分为两个部分。这种对数据集内各示例进行随机洗牌的操作基于random_state,即一类随机生成器。无论具体内容为何,但只要青筋相同内容,我们即创造出了一项可重复进行的实验。
最后,将四项新的数组保存为NumPy的二进制文件格式。现在我们已经拥有了一套训练集与一套测试集!
前往Project Settings(项目设置)屏幕并切换至Build Settings(构建设置)标签。在Other Linker Flags(其它链接标记)下,大家会看到以下内容:
/Users/matthijs/tensorflow/tensorflow/contrib/makefile/gen/protobuf_ios/lib/ libprotobuf-lite.a /Users/matthijs/tensorflow/tensorflow/contrib/makefile/gen/protobuf_ios/lib/ libprotobuf.a -force_load /Users/matthijs/tensorflow/tensorflow/contrib/makefile/gen/lib/ libtensorflow-core.a
除非您的名称同样为“matthijs”,否则大家需要将其替换为您TensorFlow库的实际克隆路径。(请注意,这里tensorflow出现了两次,所以文件夹名称应为tensorflow/tensorflow/...)
备注:大家也可以将这三个.a文件复制到项目文件夹之内,如此即不必担心路径可能出现问题。我个人并不打算在这一示例项目中采取这种方式,因为libtensorflow-core.a文件是一套体积达440 MB的库。
另外请注意检查Header Search Paths(标题搜索路径)。以下为目前的设置:
~/tensorflow ~/tensorflow/tensorflow/contrib/makefile/downloads ~/tensorflow/tensorflow/contrib/makefile/downloads/eigen ~/tensorflow/tensorflow/contrib/makefile/downloads/protobuf/src ~/tensorflow/tensorflow/contrib/makefile/gen/proto
另外,大家还需要将其更新至您的克隆目录当中。
以下为我在构建设置当中进行了修改的其它条目:
Enable Bitcode: NoWarnings / Documentation Comments: NoWarnings / Deprecated Functions: No
Bitcode目前尚不受TensorFLow的支持,所以我决定将其禁用。我还关闭了警告选项,否则在编译应用时会出现一大票问题提示。(禁用之后,大家仍然会遇到几项关于值转换问题的警告。大家当然也可以将其一并禁用,但我个人还是希望多少了解一点其中的错误。)
在完成了对Other Linker Flags与Header Search Paths的变更之后,大家即可构建并运行我们的iOS应用。
很好,现在大家已经拥有了一款能够使用TensorFlow的iOS应用了!下面让我们看看它的实际运行效果。
使用 TensorFlow C++ API
TensorFlow for iOS由C++编写而成,但其中需要编写的C++代码量其实——幸运的是——并不多。一般来讲,大家只需要完成以下工作:
从.pb文件中加载计算图与权重值。
利用此计算图创建一项会话。
将您的数据放置在一个输入张量内。
在一个或者多个节点上运行计算图。
从输出结果张量中获取结果。
在本示例应用当中,这一切皆发生在ViewController.mm之内。首先,我们加载计算图:
- (BOOL)loadGraphFromPath:(NSString *)path { auto status = ReadBinaryProto(tensorflow::Env::Default(), path.fileSystemRepresentation, &graph); if (!status.ok()) { NSLog(@"Error reading graph: %s", status.error_message().c_str()); return NO; } return YES; }
此Xcode项目当中已经包含我们通过在graph.pb上运行freeze_graph与optimize_for_inference工具所构建的inference.pb计算图。如果大家希望直接加载graph.pb,则会得到以下错误信息:
Error adding graph to session: No OpKernel was registered to support Op 'L2Loss' with these attrs. Registered devices: [CPU], Registered kernels: <no registered kernels> [[Node: loss-function/L2Loss = L2Loss]]
这是因为C++ API所能支持的操作要远少于Python API。这里提到我们在loss函数节点中所使用的L2Loss操作在iOS上并不适用。正因为如此,我们才需要利用freeze_graph以简化自己的计算图。
在计算图加载完成之后,我们使用以下命令创建一项会话: - (BOOL)createSession{ tensorflow::SessionOptions options; auto status = tensorflow::NewSession(options, &session); if (!status.ok()) { NSLog(@"Error creating session: %s", status.error_message().c_str()); return NO; } status = session->Create(graph); if (!status.ok()) { NSLog(@"Error adding graph to session: %s", status.error_message().c_str()); return NO; } return YES;}
会话创建完成后,我们可以利用其执行预测操作。其中的predict:method会提供一个包含20项浮点数值的数组——即声学特征——并将这些数字馈送至计算得意洋洋发中。
下面我们一起来看此方法的工作方式: - (void)predict:(float *)example { tensorflow::Tensor x(tensorflow::DT_FLOAT, tensorflow::TensorShape({ 1, 20 })); auto input = x.tensor<float, 2>(); for (int i = 0; i < 20; ++i) { input(0, i) = example[i]; }
其首先将张量x定义为我们需要使用的输入数据。此张量为{1,20},因为其一次提取一项示例且该示例中包含20项特征。在此之后,我们将数据由float *数组复制至该张量当中。
接下来,我们运行该项会话:
std::vector<std::pair<std::string, tensorflow::Tensor>> inputs = { {"inputs/x-input", x} }; std::vector<std::string> nodes = { {"model/y_pred"}, {"inference/inference"} }; std::vector<tensorflow::Tensor> outputs; auto status = session->Run(inputs, nodes, {}, &outputs); if (!status.ok()) { NSLog(@"Error running model: %s", status.error_message().c_str()); return; }
这里得出了类似于Python代码的内容:
pred, inf = sess.run([y_pred, inference], feed_dict={x: example})
只是不那么简洁。我们需要创建馈送词典、用于列出需要运行的全部节点的向量,外加一个负责容纳对应结果的向量。最后,我们告知该会话完成上述任务。
在会话运行了全部必要节点后,我们即可输出以下结果:
auto y_pred = outputs[0].tensor<float, 2>(); NSLog(@"Probability spoken by a male: %f%%", y_pred(0, 0)); auto isMale = outputs[1].tensor<float, 2>(); if (isMale(0, 0)) { NSLog(@"Prediction: male"); } else { NSLog(@"Prediction: female"); }}
出于演示需求,只需要运行inference节点即可完成对音频数据的男声/女声判断。不过我还希望查看计算得出的概率,因此这里我也运行了y_pred节点。
运行iOS应用
大家可以在iPhone模拟器或者实机之上运行这款应用。在模拟器上,大家仍然会看到“此TensorFlow库无法利用SSE4.1指令进行编译”的提示,但在实机上则不会出现这样的问题。
出于测试的目的,这款应用只会进行两项预测:一次为男声示例预测,一次为女声示例预测。(我直接从测试集中提取了对应示例。大家也可以配合其它示例并修改maleExample或者emaleExample数组当中的数字。)
运行这款应用,大家应该会看到以下输出结果。该应用首先给出了计算图当中的各节点:
Node count: 9Node 0: Placeholder 'inputs/x-input'Node 1: Const 'model/W'Node 2: Const 'model/b'Node 3: MatMul 'model/MatMul'Node 4: Add 'model/add'Node 5: Sigmoid 'model/y_pred'Node 6: Const 'inference/Greater/y'Node 7: Greater 'inference/Greater'Node 8: Cast 'inference/inference'
需要注意的是,此计算图中仅包含实施预测所必需的操作,而不包括任何与训练相关的内容。
此后,其会输出预测结果:
Probability spoken by a male: 0.970405% Prediction: male Probability spoken by a male: 0.005632% Prediction: female
如果大家利用Python脚本尝试使用同样的示例,那么结果也将完全一致。任务完成!
备注:这里要提醒大家,此项演示项目中我们对数据进行了“伪造”(即使用了提取自测试集中的示例)。如果大家希望利用这套模型处理真正的音频,则首先需要将对应音频转化为20项声学特征。
iOS平台上TensorFlow的优势与缺点
TensorFlow是一款出色的机器学习模型训练工具,特别是对于那些不畏数学计算并乐于创建新型算法的朋友。要对规模更大的模型进行训练,大家甚至可以在云环境下使用TensorFLow。
除了训练之外,本篇博文还介绍了如何将TensorFLow添加至您的iOS应用当中。对于这一部分,我希望概括这种作法的优势与缺点。
在iOS之上使用TensorFlow的优势:
使用一款工具即可实现全部预期。大家可以同时利用TensorFlow训练模型并将其引用于设备之上。我们不再需要将自己的计算图移植至BNNS或者Metal等其它API处。在另一方面,大家则必须至少将部分Python代码“移植”为C++形式。
TensorFlow拥有众多超越BNNS或Metal的出色功能特性。
大家可以在模拟器上对其进行测试。(Metal要求用户始终利用实机进行测试。)
在iOS上使用TensorFLow的缺点:
目前其尚不支持GPU。TensorFlow确实能够利用Acclerate框架以发挥CPU的向量指令优势,但在原始处理速度上仍无法与Metal相提并论。
TensorFLow API为C++,因此大家需要使用Objective-C++自行编写代码。
大家无法直接利用Swift使用TensorFLow。C++ API相较于Python API存在更多局限性。这意味着大家无法在设备之上进行数据训练,因为反向传播所需要的自动梯度计算尚不受设备支持。但这并不是什么大问题,毕竟移动设备的硬件本身就不适合进行大规模数据集训练。
TensorFlow静态库的加入会令应用体积增加约40 MB。大家可以通过减少受支持操作的数量对其进行瘦身,但具体过程相当麻烦。另外这还不包含您模型本体的体积,这可能会让应用尺寸进一步膨胀。