在C ++中使用TensorFlow训练深度神经网络

2018年01月05日 由 yuxiangyu 发表 755552 0
你可能知道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就可以了:
brew install bazel

我们将从TensorFlow源文件开始构建:
mkdir /path/tensorflow
cd /path/tensorflow
git clone https://github.com/tensorflow/tensorflow.git

然后你必须对安装进行配置,如选择是否启用GPU,你要运行以下配置脚本:
cd /path/tensorflow
./configure

现在我们创建接收我们模型的代码并首次构建TensorFlow的文件。请注意,第一次构建需要相当长的时间(10 - 15分钟)。

非核心的C ++ TensorFlow代码位于/tensorflow/cc中,这是我们创建模型文件的地方,我们还需要一个BUILD文件,以便bazel可以建立model.cc。
mkdir /path/tensorflow/model
cd /path/tensorflow/model
touch model.cc
touch BUILD

我们将bazel指令添加到BUILD文件中:
load("//tensorflow:tensorflow.bzl", "tf_cc_binary")

tf_cc_binary(
name = "model",
srcs = [
"model.cc",
],
deps = [
"//tensorflow/cc:gradients",
"//tensorflow/cc:grad_ops",
"//tensorflow/cc:cc_ops",
"//tensorflow/cc:client_session",
"//tensorflow/core:tensorflow"
],
)

基本上它会使用model.cc建立一个模型二进制文件。我们现在准备编写我们的模型。

读取数据


这些数据是从法国网站leboncoin.fr中截取,然后清理和归一化并保存到CSV文件中。我们的目标是读取这些数据。用于归一化数据的元数据被保存到CSV文件的第一行,我们需要他们重新构建网络输出的价格。我创建了一个data_set.h和data_set.cc文件以保持代码清洁。他们从CSV文件中产生一个浮点型二维数组,馈送给我们的网络。我把代码粘贴在这里,但这无关紧要,你不需要花时间阅读。

data_set.h
using namespace std;

// Meta data used to normalize the data set. Useful to
// go back and forth between normalized data.
class DataSetMetaData {
friend class DataSet;
private:
float mean_km;
float std_km;
float mean_age;
float std_age;
float min_price;
float max_price;
};

enum class Fuel {
DIESEL,
GAZOLINE
};

class DataSet {
public:
// Construct a data set from the given csv file path.
DataSet(string path) {
ReadCSVFile(path);
}

// getters
vector& x() { return x_; }
vector& y() { return y_; }

// read the given csv file and complete x_ and y_
void ReadCSVFile(string path);

// convert one csv line to a vector of float
vector ReadCSVLine(string line);

// normalize a human input using the data set metadata
initializer_list input(float km, Fuel fuel, float age);

// convert a price outputted by the DNN to a human price
float output(float price);
private:
DataSetMetaData data_set_metadata;
vector x_;
vector y_;
};

data_set.cc
#include 
#include
#include
#include
#include "data_set.h"

using namespace std;

void DataSet::ReadCSVFile(string path) {
ifstream file(path);
stringstream buffer;
buffer << file.rdbuf();
string line;
vector lines;
while(getline(buffer, line, '\n')) {
lines.push_back(line);
}

// the first line contains the metadata
vector metadata = ReadCSVLine(lines[0]);

data_set_metadata.mean_km = metadata[0];
data_set_metadata.std_km = metadata[1];
data_set_metadata.mean_age = metadata[2];
data_set_metadata.std_age = metadata[3];
data_set_metadata.min_price = metadata[4];
data_set_metadata.max_price = metadata[5];

// the other lines contain the features for each car
for (int i = 2; i < lines.size(); ++i) {
vector features = ReadCSVLine(lines[i]);
x_.insert(x_.end(), features.begin(), features.begin() + 3);
y_.push_back(features[3]);
}
}

vector DataSet::ReadCSVLine(string line) {
vector line_data;
std::stringstream lineStream(line);
std::string cell;
while(std::getline(lineStream, cell, ','))
{
line_data.push_back(stod(cell));
}
return line_data;
}

initializer_list DataSet::input(float km, Fuel fuel, float age) {
km = (km - data_set_metadata.mean_km) / data_set_metadata.std_km;
age = (age - data_set_metadata.mean_age) / data_set_metadata.std_age;
float f = fuel == Fuel::DIESEL ? -1.f : 1.f;
return {km, f, age};
}

float DataSet::output(float price) {
return price * (data_set_metadata.max_price - data_set_metadata.min_price) + data_set_metadata.min_price;
}

我们还必须在我们的bazel BUILD文件中添加这两个文件。
load("//tensorflow:tensorflow.bzl", "tf_cc_binary")

tf_cc_binary(
name = "model",
srcs = [
"model.cc",
"data_set.h",
"data_set.cc"
],
deps = [
"//tensorflow/cc:gradients",
"//tensorflow/cc:grad_ops",
"//tensorflow/cc:cc_ops",
"//tensorflow/cc:client_session",
"//tensorflow/core:tensorflow"
],
)

建立模型


第一步是读取CSV文件加入两个张量:x表示输入,y表示预期的结果。我们使用之前定义的DataSet类。访问下方链接下载CSV数据集。

链接:https://github.com/theflofly/dnn_tensorflow_cpp/blob/master/normalized_car_features.csv
DataSet data_set("/path/normalized_car_features.csv");
Tensor x_data(DataTypeToEnum::v(),
TensorShape{static_cast(data_set.x().size())/3, 3});
copy_n(data_set.x().begin(), data_set.x().size(),
x_data.flat().data());

Tensor y_data(DataTypeToEnum::v(),
TensorShape{static_cast(data_set.y().size()), 1});
copy_n(data_set.y().begin(), data_set.y().size(),
y_data.flat().data());

要定义一个张量,我们需要它的类型和形状。在data_set对象中,x数据以平坦(flat)的方式保存,所以我们要将尺寸缩减成3(每辆车有3个特征)。然后,我们正在使用std::copy_n将数据从data_set对象复制到张量(Eigen::TensorMap)的底层数据结构。我们现在将数据作为TensorFlow数据结构,开始构建模型。

你可以使用以下方法调试张量:
LOG(INFO) << x_data.DebugString();

C ++ API的独特之处在于,你将需要一个Scope对象来保存图形构造的状态,并将该对象传递给每个操作。
Scope scope = Scope::NewRootScope();

我们将有两个占位符,x包含汽车的特征和y表示每辆车相应的价格。
auto x = Placeholder(scope, DT_FLOAT);
auto y = Placeholder(scope, DT_FLOAT);

我们的网络有两个隐藏层,因此我们将有三个权重矩阵和三个偏置矩阵。而在Python中,它是在底层完成的,在C++中你必须定义一个变量,然后定义一个Assign节点,以便为该变量分配一个默认值。我们使用RandomNormal来初始化我们的变量,这将给我们一个正态分布的随机值。
// weights init
auto w1 = Variable(scope, {3, 3}, DT_FLOAT);
auto assign_w1 = Assign(scope, w1, RandomNormal(scope, {3, 3}, DT_FLOAT));

auto w2 = Variable(scope, {3, 2}, DT_FLOAT);
auto assign_w2 = Assign(scope, w2, RandomNormal(scope, {3, 2}, DT_FLOAT));

auto w3 = Variable(scope, {2, 1}, DT_FLOAT);
auto assign_w3 = Assign(scope, w3, RandomNormal(scope, {2, 1}, DT_FLOAT));

// bias init
auto b1 = Variable(scope, {1, 3}, DT_FLOAT);
auto assign_b1 = Assign(scope, b1, RandomNormal(scope, {1, 3}, DT_FLOAT));

auto b2 = Variable(scope, {1, 2}, DT_FLOAT);
auto assign_b2 = Assign(scope, b2, RandomNormal(scope, {1, 2}, DT_FLOAT));

auto b3 = Variable(scope, {1, 1}, DT_FLOAT);
auto assign_b3 = Assign(scope, b3, RandomNormal(scope, {1, 1}, DT_FLOAT));

然后我们使用Tanh作为激活函数来构建我们的三个层。
// layers
auto layer_1 = Tanh(scope, Add(scope, MatMul(scope, x, w1), b1));
auto layer_2 = Tanh(scope, Add(scope, MatMul(scope, layer_1, w2), b2));
auto layer_3 = Tanh(scope, Add(scope, MatMul(scope, layer_2, w3), b3));

添加L2正则化。
// regularization
auto regularization = AddN(scope,
initializer_list{L2Loss(scope, w1),
L2Loss(scope, w2),
L2Loss(scope, w3)});

最后,我们计算损失,我们的预测和实际价格之间y的差异,并且将正则化加入损失。
// loss calculation
auto loss = Add(scope,
ReduceMean(scope, Square(scope, Sub(scope, layer_3, y)), {0, 1}),
Mul(scope, Cast(scope, 0.01, DT_FLOAT), regularization));

至此,我们完成了前向传播,并准备做反向传播部分。第一步是使用一个函数调用将前向操作的梯度添加到图中。
// add the gradients operations to the graph
std::vector grad_outputs;
TF_CHECK_OK(AddSymbolicGradients(scope, {loss}, {w1, w2, w3, b1, b2, b3}, &grad_outputs));

所有操作必须计算关于每个变量被添加到图中的损失的梯度,关于,我们初始化一个空的grad_outputs向量,它会TensorFlow会话使用时填充了为变量提供梯度的节点,grad_outputs[0]会给我们关于w1, grad_outputs[1]损失的梯度和关于w2的损失梯度,它顺序为{w1, w2, w3, b1, b2, b3},变量的顺序传递给AddSymbolicGradients。

现在我们在grad_outputs中有一个节点列表。当在TensorFlow会话中使用时,每个节点计算一个变量的损失梯度。我们用它来更新变量。我们将为每个变量设置一行,在这里我们使用最简单的梯度下降进行更新。
// update the weights and bias using gradient descent
auto apply_w1 = ApplyGradientDescent(scope, w1, Cast(scope, 0.01, DT_FLOAT), {grad_outputs[0]});
auto apply_w2 = ApplyGradientDescent(scope, w2, Cast(scope, 0.01, DT_FLOAT), {grad_outputs[1]});
auto apply_w3 = ApplyGradientDescent(scope, w3, Cast(scope, 0.01, DT_FLOAT), {grad_outputs[2]});
auto apply_b1 = ApplyGradientDescent(scope, b1, Cast(scope, 0.01, DT_FLOAT), {grad_outputs[3]});
auto apply_b2 = ApplyGradientDescent(scope, b2, Cast(scope, 0.01, DT_FLOAT), {grad_outputs[4]});
auto apply_b3 = ApplyGradientDescent(scope, b3, Cast(scope, 0.01, DT_FLOAT), {grad_outputs[5]});

Cast操作实际上是学习速率参数,在我们的例子中为0.01。

我们的网络已准备好在会话中启动,Python中的Optimizers API的最小化函数基本上封装了在函数调用中计算和应用梯度。这就是我在PR#11377中所做的。

PR#11377:https://github.com/tensorflow/tensorflow/pull/11377

我们初始化一个ClientSession和一个名为outputs的张量向量,它将接收我们网络的输出。
ClientSession session(scope);
std::vector outputs;

然后我们初始化我们的变量,在python中调用tf.global_variables_initializer()就可以了,因为在构建图的过程中我们保留了所有变量的列表。在C ++中,我们必须列出变量。每个RandomNormal输出将被分配给Assign节点中定义的变量。
// init the weights and biases by running the assigns nodes once
TF_CHECK_OK(session.Run({assign_w1, assign_w2, assign_w3, assign_b1, assign_b2, assign_b3}, nullptr));

在这一点上,我们可以按训练步骤的数量循环。在本例中,我们做5000步。首先使用loss节点运行前向传播部分,输出网络的损失。每隔100步记录一次损失值,减少损失是活动网络的强制性属性。然后我们必须计算我们的梯度节点并更新变量。我们的梯度节点被用作ApplyGradientDescent节点的输入,所以运行我们的apply_节点将首先计算梯度,然后将其应用于正确的变量。
// training steps
for (int i = 0; i < 5000; ++i) {
TF_CHECK_OK(session.Run({{x, x_data}, {y, y_data}}, {loss}, &outputs));
if (i % 100 == 0) {
std::cout << "Loss after " << i << " steps " << outputs[0].scalar() << std::endl;
}
// nullptr because the output from the run is useless
TF_CHECK_OK(session.Run({{x, x_data}, {y, y_data}}, {apply_w1, apply_w2, apply_w3, apply_b1, apply_b2, apply_b3, layer_3}, nullptr));
}

到这里,我们的网络训练完成,可以试着预测(或者说推理)一辆车的价格。我们尝试预测一辆使用7年的宝马1系车的价格,这辆车是柴油发动机里程为11万公里。我们运行我们的layer_3节点吧汽车数据输入x,它本质上是一个前向传播步骤。因为我们已经训练过网络5000步,所以权重有一个学习值,所产生的结果不会是随机的。

我们不能直接使用汽车属性,因为我们的网络从归一化的属性中学习的,它们必须经过相同的归一化化过程。DataSet类有一个input方法,使用CSV读取期间加载的数据集的元数据来处理该步骤。
// prediction using the trained neural net
TF_CHECK_OK(session.Run({{x, {data_set.input(110000.f, Fuel::DIESEL, 7.f)}}}, {layer_3}, &outputs));
cout << "DNN output: " << *outputs[0].scalar().data() << endl;
std::cout << "Price predicted " << data_set.output(*outputs[0].scalar().data()) << " euros" << std::endl;

我们的网络产生一个介于0和1之间的值,data_set的output方法还会使用数据集元数据将该值转换为可读的价格。该模型可以使用命令bazel run -c opt //tensorflow/cc/models:model运行,如果最近编译了TensorFlow,你会很快看到如下输出:
Loss after 0 steps 0.317394
Loss after 100 steps 0.0503757
Loss after 200 steps 0.0487724
Loss after 300 steps 0.047366
Loss after 400 steps 0.0460944
Loss after 500 steps 0.0449263
Loss after 600 steps 0.0438395
Loss after 700 steps 0.0428183
Loss after 800 steps 0.041851
Loss after 900 steps 0.040929
Loss after 1000 steps 0.0400459
Loss after 1100 steps 0.0391964
Loss after 1200 steps 0.0383768
Loss after 1300 steps 0.0375839
Loss after 1400 steps 0.0368152
Loss after 1500 steps 0.0360687
Loss after 1600 steps 0.0353427
Loss after 1700 steps 0.0346358
Loss after 1800 steps 0.0339468
Loss after 1900 steps 0.0332748
Loss after 2000 steps 0.0326189
Loss after 2100 steps 0.0319783
Loss after 2200 steps 0.0313524
Loss after 2300 steps 0.0307407
Loss after 2400 steps 0.0301426
Loss after 2500 steps 0.0295577
Loss after 2600 steps 0.0289855
Loss after 2700 steps 0.0284258
Loss after 2800 steps 0.0278781
Loss after 2900 steps 0.0273422
Loss after 3000 steps 0.0268178
Loss after 3100 steps 0.0263046
Loss after 3200 steps 0.0258023
Loss after 3300 steps 0.0253108
Loss after 3400 steps 0.0248298
Loss after 3500 steps 0.0243591
Loss after 3600 steps 0.0238985
Loss after 3700 steps 0.0234478
Loss after 3800 steps 0.0230068
Loss after 3900 steps 0.0225755
Loss after 4000 steps 0.0221534
Loss after 4100 steps 0.0217407
Loss after 4200 steps 0.0213369
Loss after 4300 steps 0.0209421
Loss after 4400 steps 0.020556
Loss after 4500 steps 0.0201784
Loss after 4600 steps 0.0198093
Loss after 4700 steps 0.0194484
Loss after 4800 steps 0.0190956
Loss after 4900 steps 0.0187508
DNN output: 0.0969611
Price predicted 13377.7 euros

它展示了汽车预计价格13377.7欧元。每次运行模型都会得到不同的结果,有时差异很大(8000—17000)。这是由于我们只用三个属性来描述汽车,而我们的网络架构也相对简单。

 
欢迎关注ATYUN官方公众号
商务合作及内容投稿请联系邮箱:bd@atyun.com
评论 登录
热门职位
Maluuba
20000~40000/月
Cisco
25000~30000/月 深圳市
PilotAILabs
30000~60000/年 深圳市
写评论取消
回复取消