如何用C++編寫不到200行的神經網絡


目前已經有許多現成的深度學習框架,為什么我們還要用C++來編寫一個神經網絡?一個理由是我們需要了解學習框架內部的運行原理,當分析問題的時候能夠很快的定位原因;另一個理由是,我們需要為專有設備編寫一個推理引擎,它可能運行在手機端,或者移動設備上。這篇文章實現了一個最簡單的神經網絡框架,適合大家入門學習。

參考代碼鏈接:https://github.com/webbery/MiniEngine/tree/demo

一個最簡單的神經網絡包含輸入節點、一個線性層、一個激活函數和一個損失函數。為了減少編碼,我們采用Eigen代替numpy,來實現相關的矩陣操作。

首先定義一個Node節點類作為基類,它包含一個輸入節點的隊列和一個輸出節點的隊列;以及當前節點的值,與它連接的反向傳播的梯度值,前向傳播接口和反向傳播接口。另外為了調試方便,我們給Node增加了一個name成員變量,來標識數據對應到哪個節點。

 1 class Node {
 2 public:
 3   virtual void forward() = 0;
 4   virtual void backward() = 0;
 5 protected:
 6   Eigen::MatrixXf _value;
 7   std::vector<Node*> _inputs;
 8   std::vector<Node*> _outputs;
 9   std::map<Node*, Eigen::MatrixXf> _gradients;
10   std::string _name;
11 };

  輸入節點Input繼承Node類,代表輸入變量,這些變量將被數據賦值。對應於tensorflowVariable類。這里我們在構造函數里初始化了它的大小rowcol

1 class Input : public Node {
2 public:
3     Input(const char* name,size_t rows=0,size_t cols=0);
4 };

  Linear節點,代表全連接層,前向傳播接口的實現方式為WX+bias,其中biasWX計算出來的矩陣形式是不相同的,需要對bias做一個廣播操作;反向傳播需要計算輸出節點對應的WXbias梯度值

 1 class Linear : public Node {
 2 public:
 3     Linear(Node* nodes, Node* weights, Node* bias);
 4 
 5     virtual void forward(){_value = (_nodes->getValue() * _weights->getValue()).rowwise() + Eigen::VectorXf(_bias->getValue()).transpose();}
 6 
 7     virtual void backward(){
 8     for (auto node : _outputs){
 9       auto grad = node->getGradient(this);
10       _gradients[_weights] = _nodes->getValue().transpose() * grad;
11       _gradients[_bias] = grad.colwise().sum().transpose();
12       _gradients[_nodes] = grad * _weights->getValue().transpose();
13     }
14   }
15 
16 private:
17     Node* _nodes = nullptr;
18     Node* _weights = nullptr;
19     Node* _bias = nullptr;
20 };

  Sigmoid節點,代表激活函數,前向傳播計算sigmoid函數結果,反向傳播計算sigmoid導函數

 1 class Sigmoid : public Node {
 2 public:
 3   Sigmoid(Node* node);
 4   virtual void forward(){_value = _impl(_node->getValue());}
 5   virtual void backward(){
 6     auto y = _value;
 7     auto y2 = y.cwiseProduct(y);
 8     _partial = y-y2;
 9 
10     for (auto node : _outputs) {
11       auto grad = node->getGradient(this);
12       _gradients[_node] = grad.cwiseProduct(_partial);
13     }
14   }
15 private:
16   Eigen::MatrixXf _impl(const Eigen::MatrixXf& x){return (-x.array().exp() + 1).inverse();}
17 private:
18   Node* _node = nullptr;
19   //sigmoid的偏導
20   Eigen::MatrixXf _partial;
21 };

 MSE節點,代表Loss function,前向傳播計算估計值與真實值的方差,反向傳播需要計算方差的一階導數結果

 1 class MSE : public Node {
 2 public:
 3   MSE(Node* y, Node* y_hat);
 4   virtual void forward(){
 5     _diff = _y->getValue() - _y_hat->getValue();
 6     auto diff2= _diff.cwiseProduct(_diff);
 7     auto v = Eigen::MatrixXf(1, 1);
 8     v << (diff2).mean();
 9     _value = v;
10   }
11   virtual void backward(){
12     auto r = _y_hat->getValue().rows();
13     _gradients[_y] = _diff * (2.f / r);
14     _gradients[_y_hat] = _diff * (-2.f / r);
15   }
16 private:
17   Node* _y;
18   Node* _y_hat;
19   Eigen::MatrixXf _diff;
20 };

 這幾個節點是我們將要構建的框架的基本元素。然后我們還需要實現一個圖的拓撲排序,對排序后的節點進行前向迭代,計算預測結果;然后再反向迭代,計算每個連接的梯度。

 1 std::vector<Node*> topological_sort(Node* input_nodes){
 2   //根據傳入的數據初始化圖結構
 3   Node* pNode = nullptr;
 4   //pair第一個為輸入,第二個為輸出
 5   std::map < Node*, std::pair<std::set<Node*>, std::set<Node*> > > g;
 6   //待遍歷的周圍節點
 7   std::list<Node*> vNodes;
 8   vNodes.emplace_back(input_nodes);
 9   //廣度遍歷,先遍歷輸出節點,再遍歷輸入節點
10   //已經遍歷過的節點
11   std::set<Node*> sVisited;
12   while (vNodes.size() && (pNode = vNodes.front())) {
13     if (sVisited.find(pNode) != sVisited.end()) vNodes.pop_front();
14     const auto& outputs = pNode->getOutputs();
15     for (auto item: outputs){
16       g[pNode].second.insert(item);    //添加item為pnode的輸出節點
17       g[item].first.insert(pNode);    //添加pnode為item的輸入節點
18       if(sVisited.find(item)==sVisited.end()) vNodes.emplace_back(item);    //把沒有訪問過的節點添加到待訪問隊列中
19     }
20     const auto& inputs = pNode->getInputs();
21     for (auto item: inputs){
22       g[pNode].first.insert(item);    //添加item為pnode的輸入節點
23       g[item].second.insert(pNode);    //添加pnode為item的輸出節點
24       if (sVisited.find(item) == sVisited.end()) vNodes.emplace_back(item);
25     }
26     sVisited.emplace(pNode);
27     vNodes.pop_front();
28   }
29 
30   //根據圖結構進行拓撲排序
31   std::vector<Node*> vSorted;
32   while (g.size()) {
33     for (auto itr=g.begin();itr!=g.end();++itr)
34     {
35       //沒有輸入節點
36       auto& f = g[itr->first];
37       if (f.first.size() == 0) {
38         vSorted.push_back(itr->first);
39         //找到圖中這個節點的輸出節點,然后將輸出節點對應的這個父節點移除
40         auto outputs = f.second;//f['out']
41         for (auto& output: outputs) g[output].first.erase(itr->first);
42         //然后將這個節點從圖中移除
43         g.erase(itr->first);
44         break;
45       }
46     }
47   }
48   return vSorted;
49 }

 測試程序中,我們定義了每個節點,並構造了節點之間的連接關系;之后把輸入節點傳給了topological_sort。該函數從輸入節點開始,進行廣度優先遍歷,構建一個圖結構;然后根據拓撲排序算法,將這個圖結構排序成一個隊列返回;這個隊列在tensorflow里被稱為“圖”。

然后,測試程序運行train_one_batch前向遍歷一次得到預測值,然后再反向遍歷一次,得到每個節點連接的梯度變化;

1 void train_one_batch(std::vector<Node*>& graph){
2   for (auto node:graph){
3     node->forward();
4   }
5   for (int idx = graph.size() - 1; idx >= 0;--idx) {
6     graph[idx]->backward();
7   }
8 }

 接着根據計算出來的梯度值,更新權重節點Wb,完成一次訓練

1 void sgd_update(std::vector<Node*> update_nodes, float learning_rate){
2   for (auto node: update_nodes){
3     Eigen::MatrixXf delta = -1 * learning_rate * node->getGradient(node);
4     node->setValue(node->getValue() + delta);
5   }
6 }


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM