AiTechYun
编辑:yuxiangyu
你可能知道TensorFlow的核心是用C++构建的,然而只有python的API才能获得多种便利。
当我写上一篇文章时,目标是仅使用TensorFlow的C ++ API实现相同的DNN(深度神经网络),然后仅使用CuDNN。从我入手TensorFlow的C ++版本开始,我意识到即使对于简单DNN来说,也有很多东西被忽略了。
文章地址:https://matrices.io/deep-neural-network-from-scratch/
请记住,使用外部运算训练网络肯定是不可能的。你最可能面临的错误是缺少梯度运算。我目前正在将梯度运算从Python迁移到C ++。
在这个博客文章中,我们将建立一个深度神经网络,使用宝马车的车龄、公里数和发动机使用的燃料类型预测车的价格。我们将只在C ++中使用TensorFlow。目前在C ++中没有优化器,所以你会看到训练代码不那么好看,但是未来会添加优化器。
阅读本文需对谷歌的指南(https://www.tensorflow.org/api_guides/cc/guide)有所了解。
GitHub:https://github.com/theflofly/dnn_tensorflow_cpp
安装
我们将在TensorFlow C++ code中运行我们的C ++代码,我们可以尝试使用已编译的库,但是相信有些人会由于其环境的特殊性而遇到麻烦。从头开始构建TensorFlow会避免出现这些问题,并确保我们正在使用最新版本的API。
你需要安装bazel构建工具。
安装:https://docs.bazel.build/versions/master/install.html
在OSX上使用brew就可以了:
我们将从TensorFlow源文件开始构建:
然后你必须对安装进行配置,如选择是否启用GPU,你要运行以下配置脚本:
现在我们创建接收我们模型的代码并首次构建TensorFlow的文件。请注意,第一次构建需要相当长的时间(10 – 15分钟)。
非核心的C ++ TensorFlow代码位于/tensorflow/cc中,这是我们创建模型文件的地方,我们还需要一个BUILD文件,以便bazel可以建立model.cc。
我们将bazel指令添加到BUILD文件中:
基本上它会使用model.cc建立一个模型二进制文件。我们现在准备编写我们的模型。
读取数据
这些数据是从法国网站leboncoin.fr中截取,然后清理和归一化并保存到CSV文件中。我们的目标是读取这些数据。用于归一化数据的元数据被保存到CSV文件的第一行,我们需要他们重新构建网络输出的价格。我创建了一个data_set.h和data_set.cc文件以保持代码清洁。他们从CSV文件中产生一个浮点型二维数组,馈送给我们的网络。我把代码粘贴在这里,但这无关紧要,你不需要花时间阅读。
data_set.h
data_set.cc
我们还必须在我们的bazel BUILD文件中添加这两个文件。
建立模型
第一步是读取CSV文件加入两个张量:x表示输入,y表示预期的结果。我们使用之前定义的DataSet类。访问下方链接下载CSV数据集。
链接:https://github.com/theflofly/dnn_tensorflow_cpp/blob/master/normalized_car_features.csv
要定义一个张量,我们需要它的类型和形状。在data_set对象中,x数据以平坦(flat)的方式保存,所以我们要将尺寸缩减成3(每辆车有3个特征)。然后,我们正在使用std::copy_n将数据从data_set对象复制到张量(Eigen::TensorMap)的底层数据结构。我们现在将数据作为TensorFlow数据结构,开始构建模型。
你可以使用以下方法调试张量:
C ++ API的独特之处在于,你将需要一个Scope对象来保存图形构造的状态,并将该对象传递给每个操作。
我们将有两个占位符,x包含汽车的特征和y表示每辆车相应的价格。
我们的网络有两个隐藏层,因此我们将有三个权重矩阵和三个偏置矩阵。而在Python中,它是在底层完成的,在C++中你必须定义一个变量,然后定义一个Assign节点,以便为该变量分配一个默认值。我们使用RandomNormal来初始化我们的变量,这将给我们一个正态分布的随机值。
然后我们使用Tanh作为激活函数来构建我们的三个层。
添加L2正则化。
最后,我们计算损失,我们的预测和实际价格之间y的差异,并且将正则化加入损失。
至此,我们完成了前向传播,并准备做反向传播部分。第一步是使用一个函数调用将前向操作的梯度添加到图中。
所有操作必须计算关于每个变量被添加到图中的损失的梯度,关于,我们初始化一个空的grad_outputs向量,它会TensorFlow会话使用时填充了为变量提供梯度的节点,grad_outputs[0]会给我们关于w1, grad_outputs[1]损失的梯度和关于w2的损失梯度,它顺序为,变量的顺序传递给AddSymbolicGradients。
现在我们在grad_outputs中有一个节点列表。当在TensorFlow会话中使用时,每个节点计算一个变量的损失梯度。我们用它来更新变量。我们将为每个变量设置一行,在这里我们使用最简单的梯度下降进行更新。
Cast操作实际上是学习速率参数,在我们的例子中为0.01。
我们的网络已准备好在会话中启动,Python中的Optimizers API的最小化函数基本上封装了在函数调用中计算和应用梯度。这就是我在PR#11377中所做的。
PR#11377:https://github.com/tensorflow/tensorflow/pull/11377
我们初始化一个ClientSession和一个名为outputs的张量向量,它将接收我们网络的输出。
然后我们初始化我们的变量,在python中调用tf.global_variables_initializer()就可以了,因为在构建图的过程中我们保留了所有变量的列表。在C ++中,我们必须列出变量。每个RandomNormal输出将被分配给Assign节点中定义的变量。
在这一点上,我们可以按训练步骤的数量循环。在本例中,我们做5000步。首先使用loss节点运行前向传播部分,输出网络的损失。每隔100步记录一次损失值,减少损失是活动网络的强制性属性。然后我们必须计算我们的梯度节点并更新变量。我们的梯度节点被用作ApplyGradientDescent节点的输入,所以运行我们的apply_节点将首先计算梯度,然后将其应用于正确的变量。
到这里,我们的网络训练完成,可以试着预测(或者说推理)一辆车的价格。我们尝试预测一辆使用7年的宝马1系车的价格,这辆车是柴油发动机里程为11万公里。我们运行我们的layer_3节点吧汽车数据输入x,它本质上是一个前向传播步骤。因为我们已经训练过网络5000步,所以权重有一个学习值,所产生的结果不会是随机的。
我们不能直接使用汽车属性,因为我们的网络从归一化的属性中学习的,它们必须经过相同的归一化化过程。DataSet类有一个input方法,使用CSV读取期间加载的数据集的元数据来处理该步骤。
我们的网络产生一个介于0和1之间的值,data_set的output方法还会使用数据集元数据将该值转换为可读的价格。该模型可以使用命令bazel run -c opt //tensorflow/cc/models:model运行,如果最近编译了TensorFlow,你会很快看到如下输出:
它展示了汽车预计价格13377.7欧元。每次运行模型都会得到不同的结果,有时差异很大(8000—17000)。这是由于我们只用三个属性来描述汽车,而我们的网络架构也相对简单。
领取专属 10元无门槛券
私享最新 技术干货