【学术】在C ++中使用TensorFlow训练深度神经网络

你可能知道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<float>& x() {return x_; }
  vector<float>& 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<float> ReadCSVLine(string line);

  // normalize a human input using the data set metadata
  initializer_list<float> 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<float> x_;
  vector<float> y_;
};
data_set.cc
#include <vector>
#include <fstream>
#include <sstream>
#include <iostream>
#include "data_set.h"

using namespace std;

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

  // the first line contains the metadata
  vector<float> 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<float> features = ReadCSVLine(lines[i]);
    x_.insert(x_.end(), features.begin(), features.begin() + 3);
    y_.push_back(features[3]);
  }
}

vector<float> DataSet::ReadCSVLine(string line) {
  vector<float> 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<float> 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<float>::v(),
              TensorShape{static_cast<int>(data_set.x().size())/3, 3});
copy_n(data_set.x().begin(), data_set.x().size(),
       x_data.flat<float>().data());

Tensor y_data(DataTypeToEnum<float>::v(),
              TensorShape{static_cast<int>(data_set.y().size()), 1});
copy_n(data_set.y().begin(), data_set.y().size(),
       y_data.flat<float>().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<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的最小化函数基本上封装了在函数调用中计算和应用梯度。这就是我在PR#11377中所做的。

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

我们初始化一个ClientSession和一个名为outputs的张量向量,它将接收我们网络的输出。

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_节点将首先计算梯度,然后将其应用于正确的变量。

// 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<float>() << 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<float>().data() << endl;
std::cout <<"Price predicted " << data_set.output(*outputs[0].scalar<float>().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订阅号(atyun_com)

原文发表时间:2018-01-05

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Jack-Cui

Caffe学习笔记(一):CIFRA-10在Caffe上进行训练学习

运行平台:Ubuntu14.04     安装完Caffe后,如何开始学习Caffe呢?一个不错的方法就是从Caffe自带的examples开始学起。在caff...

2197
来自专栏机器学习算法全栈工程师

手把手教你搭建目标检测器-附代码

翻译:刘威威 编辑:祝鑫泉 前 言 本文译自:[http://www.hackevolve.com/create-your...

3244
来自专栏腾讯Bugly的专栏

机器学习入门之HelloWorld(Tensorflow)

1 环境搭建 (Windows) 安装虚拟环境 Anaconda,方便python包管理和环境隔离。 Anaconda3 4.2 http://mirrors...

4048
来自专栏量化投资与机器学习

使用CNN(LSTM架构)进行序列预测基于TensorFlow

作者 / mouradmourafiq 翻译 / 编辑部翻译组 来源 / https://github.com/mouradmourafiq 前言 这篇推文抛...

3317
来自专栏小小挖掘机

推荐系统实战-基于用户的协同过滤

1、数据集简介 MovieLens数据集包含多个用户对多部电影的评级数据,也包括电影元数据信息和用户属性信息。 这个数据集经常用来做推荐系统,机器学习算法的测试...

4326
来自专栏目标检测和深度学习

手把手教你搭建目标检测器-附代码

902
来自专栏CNN

MobileNet原理+手写python代码实现MobileNet

MobileNet是针对移动端优化的卷积,所以当需要压缩模型时,可以考虑使用MobileNet替换卷积。下面我们开始学习MobileNet原理,并且先通过Ten...

552
来自专栏编程

Python那些事——15分钟用Python破解验证码系统!

让我们一起攻破世界上最流行的WordPress的验证码插件 每个人都讨厌验证码——在你被允许访问一个网站之前,你总被要求输入那些烦人的图像中所包含的文本。 验证...

2379
来自专栏专知

Tensorflow实战系列:手把手教你使用CNN进行图像分类(附完整代码)

【导读】专知小组计划近期推出Tensorflow实战系列,计划教大家手把手实战各项子任务。本教程旨在手把手教大家使用Tensorflow构建卷积神经网络(CNN...

6344
来自专栏和蔼的张星的图像处理专栏

5. 小项目之Face_detection

想做的是这么一个东西:识别视频(或者摄像头获得的实时视频)中的人脸,并判断是谁(因为数据采集的原因,找了身边的5个朋友采集了一些数据),如果不是这几个人,标记为...

572

扫描关注云+社区