资讯专栏INFORMATION COLUMN

作为TensorFlow的底层语言,你会用C++构建深度神经网络吗?

flyer_dev / 2062人阅读

摘要:它们从文件中生成一个浮点型的二维数组,并用于馈送到神经网络。最后计算损失函数,即计算预测价格和实际价格之间的差异,并添加正则化到损失函数中。现在我们在有一系列节点,当在会话中使用时,每个节点计算损失函数对一个变量的梯度。

目前流行的深度学习框架 TensorFlow(TensorFlow 中文官方公众号已于月初发布) 是以 C++为底层构建的,但绝大多数人都在 Python 上使用 TensorFlow 来开发自己的模型。随着 C++ API 的完善,直接使用 C++来搭建神经网络已经成为可能,本文将向你介绍一种简单的实现方法。

很多人都知道 TensorFlow 的核心是构建在 C++之上的,但是这种深度学习框架的大多数功能只在 Python API 上才方便使用。

当我写上一篇文章的时候,我的目标是仅使用 TensorFlow 中的 C++ API 和 CuDNN 来实现基本的深度神经网络(DNN)。在实践中,我意识到在这个过程中我们忽略了很多东西。

注意,使用外部操作(exotic operations)训练神经网络是不可能的,你面临的错误最有可能就是缺少梯度运算。目前我正在试图将 Python 上的梯度运算迁移到 C++上。

在本文中,我将展示如何使用 TensorFlow 在 C++ 上构建深度神经网络,并通过车龄、公里数和使用油品等条件为宝马 1 系汽车进行估价。目前,我们还没有可用的 C++ 优化器,所以你会看到训练代码看起来不那么吸引人,但是我们会在未来加入的。

本文章遵从 TensorFlow 1.4 C++ API 官方指南:https://www.tensorflow.org/api_guides/cc/guide

代码 GitHub:https://github.com/theflofly/dnn_tensorflow_cpp

安装

我们会在 C++ 中运行 TensorFlow 框架,我们需要尝试使用已编译的库,但肯定有些人会因为环境的特殊性而遇到麻烦。从头开始构建 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++ TF 代码在 /tensorflow/cc 中,这是我们创建模型文件的位置,我们也需要 BUILD 文件让 bazel 可以构建模型。

mkdir /path/tensorflow/model

cd /path/tensorflow/model

touch model.cc

touch BUILD

我们在 BUILD 文件中加入 bazel 指令:

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, 

" "

)) {

    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 数据以向量的方式保存,所以我们将尺寸缩减为 3(每个保存三个特征)。随后我们使用 std::copy_n 来从 data_set 对象中复制数据到 Tensor(一个 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<

Input

>{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<

Output

> 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 基本封装了计算和应用过程中的损失函数最小化方法。当 Optimizer API 可以接入 C++ 时我们就可以在这里使用它了。

我们初始化一个以 ClientSession 和一个以 Tensor 命名的输出向量,用来接收网络的输出。

ClientSession

 session(scope);

std::vector<

Tensor

> 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_nodes 会首先计算梯度,随后将其应用到正确的变量上。

// 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 年,里程 11 万公里,柴油发动机的宝马 1 系轿车。为了这样做我们需要运行 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 之间。这是因为我们只使用了三个属性来描述汽车,而我们的的模型架构也相对比较简单。

正如之前所说的,C++ API 的开发仍在进行中,我们希望在不久的将来,更多的功能可以加入进来。 

原文链接:https://matrices.io/training-a-deep-neural-network-using-only-tensorflow-c/

欢迎加入本站公开兴趣群

商业智能与数据分析群

兴趣范围包括各种让数据产生价值的办法,实际应用案例分享与讨论,分析工具,ETL工具,数据仓库,数据挖掘工具,报表系统等全方位知识

QQ群:81035754

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/4696.html

相关文章

  • 最新Github上各DL框架Star数量大PK

    摘要:下图总结了绝大多数上的开源深度学习框架项目,根据项目在的数量来评级,数据采集于年月初。然而,近期宣布将转向作为其推荐深度学习框架因为它支持移动设备开发。该框架可以出色完成图像识别,欺诈检测和自然语言处理任务。 很多神经网络框架已开源多年,支持机器学习和人工智能的专有解决方案也有很多。多年以来,开发人员在Github上发布了一系列的可以支持图像、手写字、视频、语音识别、自然语言处理、物体检测的...

    oogh 评论0 收藏0
  • 深度学习:你该知道八大开源框架

    摘要:作为当下最热门的话题,等巨头都围绕深度学习重点投资了一系列新兴项目,他们也一直在支持一些开源深度学习框架。八来自一个日本的深度学习创业公司,今年月发布的一个框架。 深度学习(Deep Learning)是机器学习中一种基于对数据进行表征学习的方法,深度学习的好处是用 非 监督式或半监督式 的特征学习、分层特征提取高效算法来替代手工获取特征(feature)。作为当下最热门的话题,Google...

    Rindia 评论0 收藏0
  • Deep Learning 相关库简介

    摘要:首先是最顶层的抽象,这个里面最基础的就是和,记忆中和的抽象是类似的,将计算结果和偏导结果用一个抽象类来表示了。不过,本身并没有像其它两个库一样提供,等模型的抽象类,因此往往不会直接使用去写模型。 本文将从deep learning 相关工具库的使用者角度来介绍下github上stars数排在前面的几个库(tensorflow, keras, torch, theano, skflow, la...

    ThinkSNS 评论0 收藏0
  • 以静制动TensorFlow Fold动态计算图介绍

    摘要:近日它们交锋的战场就是动态计算图,谁能在这场战争中取得优势,谁就把握住了未来用户的流向。所以动态框架对虚拟计算图的构建速度有较高的要求。动态计算图问题之一的多结构输入问题的高效计 随着深度学习的发展,深度学习框架之间竞争也日益激烈,新老框架纷纷各显神通,想要在广大DeepLearner的服务器上占据一席之地。近日它们交锋的战场就是动态计算图,谁能在这场战争中取得优势,谁就把握住了未来用户的流...

    waltr 评论0 收藏0
  • 当AlphaGo火了以后,我们来聊聊深度学习

    摘要:大家好,我是黄文坚,今天给大家讲讲深度学习。我们再来看看这两个深度学习的网络,左边是策略网络,我走到一步的时候,分析棋盘上每个位置有多大价值,给每个位置打一个分数。可以说深度学习让机器人拥有几岁小孩拾起物体的能力。 大家好,我是黄文坚,今天给大家讲讲深度学习。我不讲技术原理,讲讲技术应用。深度学习是我们明略重要的研究方向,是未来实现很多令人惊叹的功能的工具,也可以说是通向人工智能的必经之路。...

    silvertheo 评论0 收藏0

发表评论

0条评论

flyer_dev

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<