1. 程式人生 > 其它 >【學術】在C ++中使用TensorFlow訓練深度神經網路

【學術】在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)。這是由於我們只用三個屬性來描述汽車,而我們的網路架構也相對簡單。