學(xué)習深度學(xué)習首先得知道反向傳播,這是神經(jīng)網(wǎng)絡(luò )能夠學(xué)習得重要原因,也是深度學(xué)習得基石。所以,本系列以此為開(kāi)篇,著(zhù)重介紹神經(jīng)網(wǎng)絡(luò )得正向/反向傳播得流程。哈哈,肯定有人會(huì )問(wèn)為什么用C++實(shí)現,python不是更好嗎?哈哈,本人嚴重C++控,好吧后續得一些文章大多是基于c++實(shí)現的,所以,最好有一定的c++基礎。本文代碼得配置要求:
C++
OpenCV3.4
完整的代碼工程可以訪(fǎng)問(wèn)我的github:https://github.com/kingqiuol/ann
本文代碼主要通過(guò)一個(gè)NN類(lèi)來(lái)實(shí)現,支持設定插入多個(gè)隱藏層及每個(gè)隱藏層的神經(jīng)元個(gè)數,當然這部分可以跳過(guò),主要讓大家有個(gè)整體的概念,我一般在實(shí)現某一個(gè)功能或框架時(shí)都會(huì )提前做好準備,需要那幾個(gè)功能/那些模塊,并一一列舉出來(lái),先寫(xiě)好,具體細節不用管。對于一個(gè)神經(jīng)網(wǎng)絡(luò )來(lái)說(shuō),這里包括大多數卷積神經(jīng)網(wǎng)絡(luò ),一般包括:模型的訓練和模型的預測(使用),來(lái)說(shuō)一說(shuō)搭建該網(wǎng)絡(luò )具體的流程:
模型訓練:加載數據 → 創(chuàng )建模型 → 參數設置/優(yōu)化器設置 → 模型訓練 →保存權重
模型預測:創(chuàng )建模型 → 加載權重 → 傳入數據 → 預測結果
- #ifndef ANN_H_
- #define ANN_H_
- #include <iostream>
- #include <memory>
- #include <fstream>
- #include <sstream>
- #include <string>
- #include <time.h>
- #include <stdlib.h>
- #include <opencv2/opencv.hpp>
- using namespace std;
- using namespace cv;
- class NN{
- public:
- NN(size_t classes, size_t input = 0,
- float reg = 0.005, float learning_rate = 0.0001,
- size_t max_epochs = 5000,size_t batch_size = 500) :
- input_(input), classes_(classes),
- reg_(reg), learning_rate_(learning_rate),
- max_epochs_(max_epochs), batch_size_(batch_size),
- data_ptr(nullptr), label_ptr(nullptr){}
- ~NN(){}
- //加載數據
- void data_loader(const string &path);
- //添加隱藏層
- void add_hidden_layer(const vector<size_t> &num_hiddens = {});
- //初始化網(wǎng)絡(luò )
- void initial_networks();
- //前向傳播
- void forward(Mat &X);
- //反向傳播
- void backward();
- //計算損失函數
- float loss(Mat &y);
- //更新權重
- void update_weight();
- //訓練網(wǎng)絡(luò )
- void train(const string &file_path, const vector<size_t> &num_hiddens);
- //保存權重
- void save_weights(const string &save_path);
- //加載權重
- void load_weights(const string &load_path);
- //預測
- Mat predict(Mat &data);
- //其他方法
- inline float get_learning_rate()const{ return this->learning_rate_; }
- inline void set_learning_rate(float learning_rate){ this->learning_rate_ = learning_rate; }
- inline float get_reg() const{ return this->reg_; }
- inline void set_reg(float reg){ this->reg_ = reg; }
- inline size_t get_epoch()const{ return this->max_epochs_; }
- private:
- /***神經(jīng)網(wǎng)絡(luò )相關(guān)的函數***/
- void get_batch(Mat &batch_X,Mat &batch_y);
- void initial_layer(const size_t &input, const size_t &output);//單個(gè)層的初始化
- void relu(Mat &X);//激勵函數
- void softmax(Mat &out);//softmax分類(lèi)器
- float L2_regular();//L2正則化
- /***其他與矩陣操作相關(guān)的方法***/
- //計算矩陣行/列方向的和,并進(jìn)行矩陣增廣
- Mat mat_sum(const Mat &X, const int &axis, const int &dtype);
- //計算矩陣行/列方向的最大值,并進(jìn)行矩陣增廣
- Mat mat_max(const Mat &X, const int &axis, const int &dtype);
- //求矩陣行對對應的最大值的下標所在的列
- int argmax(Mat &row, float &max);//單行對應的下標
- Mat argmaxes(Mat &out);
- /***常見(jiàn)神經(jīng)網(wǎng)絡(luò )參數設置***/
- float reg_; //正則化系數
- float learning_rate_; //學(xué)習率
- size_t max_epochs_; //最大訓練次數
- size_t batch_size_; //批量處理大小
- private:
- //輸入數據、數據標簽
- shared_ptr<Mat> data_ptr, label_ptr;
- size_t input_; //輸入個(gè)數
- size_t classes_; //分類(lèi)個(gè)數
- vector<size_t> hiddens_; //各個(gè)隱藏層中神經(jīng)元個(gè)數
- vector<Mat> W_; //保存權重
- vector<Mat> b_; //保存偏置項
- vector<Mat> out_; //存儲各個(gè)層的輸出
- vector<Mat> dW_; //保存各個(gè)層的計算權重梯度
- vector<Mat> db_; //保存各個(gè)層的偏置項梯度
- };
- #endif // !ANN_H_
正如頭文件所見(jiàn),對于整個(gè)網(wǎng)絡(luò )我們需要保存一些中間變量為后續反向傳播做準備。我們需要保存的節點(diǎn)有:每一層的輸入/輸出、當前層的權重、以及反向傳播時(shí)的梯度,然后還需要定義傳播過(guò)程中的操作,主要操作有:全連接、激活函數(relu)和分類(lèi)器(softmax)。對于當前大多數深度學(xué)習框架(tensorflow/pytorch/caffe)來(lái)說(shuō),基本都采用運算圖模型,說(shuō)白了就是一系列的節點(diǎn)+邊(操作),節點(diǎn)用于存儲中間結果,邊用于計算。這里的代碼不是很明顯,后續講到深度學(xué)習時(shí)你就會(huì )由此體會(huì )。接下來(lái)我將對這些流程進(jìn)行講解并一一細說(shuō)其中的一些基礎知識。
- void NN::data_loader(const string &path)
- {
- ifstream file(path);
- //將數據存儲到vector中
- vector<vector<float>> dataset;
- string ss;
- while (getline(file, ss)){
- vector<float> data;
- stringstream w(ss);
- float temp;
- while (w >> temp){
- data.push_back(temp);
- }
- if (data.size() == 0){
- continue;
- }
- dataset.push_back(data);
- }
- //隨機打亂數據
- srand(static_cast<unsigned int>(time(0)));
- random_shuffle(dataset.begin(), dataset.end());
- //將vector轉化為Mat并分別存儲到訓練集和label中
- int rows = static_cast<int>(dataset.size());
- int cols = static_cast<int>(dataset[0].size() - 1);
- //創(chuàng )建數據集和label矩陣
- Mat train_data(rows, cols, CV_32FC1);
- Mat labels(rows, 1, CV_32FC1);
- //加載數據
- auto it = dataset.begin();
- for (int i = 0; i < rows; ++i){
- float *data = train_data.ptr<float>(i);
- float *label = labels.ptr<float>(i);
- for (int j = 0; j < cols + 1; ++j){
- data[j] = (*it)[j];
- if (cols == j){
- label[0] = (*it)[j];
- }
- }
- ++it;
- }
- //將共享指針指向數據
- this->data_ptr = make_shared<Mat>(train_data);
- this->label_ptr = make_shared<Mat>(labels);
- }
這一部分沒(méi)啥好說(shuō)的,每個(gè)人可以根據自己的數據形式進(jìn)行改變,最好在訓練前將數據打亂(shuffle)和進(jìn)行歸一化(normal)等預處理,這里我沒(méi)有進(jìn)行歸一化是因為我的數據在存儲時(shí)已經(jīng)進(jìn)行歸一化了。最主要的是最終訓練數據將轉化為矩陣形式,這也是為什么使用OenCV,有Mat的話(huà)更方便進(jìn)行相關(guān)矩陣操作,嘿嘿。注意,假設我們的數據的特征有F維,那么其矩陣形式維1XF,所以對于N個(gè)數據集,其矩陣形式如下:
接下來(lái),我將介紹在加載數據時(shí)進(jìn)行相關(guān)的預處理更多詳細的信息。對于常見(jiàn)的數據預處理有以下幾種情況:
(1)數據清洗
去除缺失大量特征的數據
去掉樣本數據的異常數據。(比如連續型數據中的離群點(diǎn))
對于數據缺失的特征,可以使用該特征的均值來(lái)代替缺失的部分
對于一些類(lèi)別類(lèi)型,如:L,R,可以one-hot形式進(jìn)行編碼
(2)數據采樣
在我們采集數據時(shí)經(jīng)常會(huì )遇到樣本不均衡的問(wèn)題,我們可以采用上/下采樣的方法進(jìn)行樣本補充,其具體為假設正負樣本比例1:100,把正樣本的數量重復100次,這就叫上采樣,也就是把比例小的樣本放大。下采樣同理,把比例大的數據抽取一部分,從而使比例變得接近于1;1。通過(guò)這種方式可以避免樣本的不均衡問(wèn)題,同時(shí)我們也要注意在實(shí)際特征工程中,均衡問(wèn)題會(huì )極大影響訓練的結果。
(3)預處理
數據歸一化,數據標準化的方法有很多,如對于所有的數值特征,我們都會(huì )減去均值,除以方差。博主主要工作方向是圖像處理領(lǐng)域,在圖像處理方面,主要的預處理方法為去均值,這里的均值為訓練集的均值,之后再驗證/測試集中都是減去這個(gè)均值。對于為什么要去均值,有很多的解釋?zhuān)缦拢?/p>
對于我們的自然圖像其實(shí)是一種平穩的數據分布【即圖像的每一維都服從相同的分布】。所以通過(guò)減去數據對應維度的統計平均值,來(lái)消除公共的部分,以凸顯個(gè)體之間的特征和差異。
根據求導的鏈式法則,w的局部梯度是X,當X全為正時(shí),由反向傳播傳下來(lái)的梯度乘以X后不會(huì )改變方向,要么為正數要么為負數,也就是說(shuō)w權重的更新在一次更新迭代計算中要么同時(shí)減小,要么同時(shí)增大。
其中,w的更新方向向量只能在第一和第三象限。假設最佳的w向量如藍色線(xiàn)所示,由于輸入全為正,現在迭代更新只能沿著(zhù)紅色路徑做zig-zag運動(dòng),更新的效率很慢?;诖?,當輸入數據減去均值后,就會(huì )有負有正,會(huì )消除這種影響。
當然,目前講的一些預處理方法只是我在機器學(xué)習方面的常用的方法,對于數據預處理還有很多方法還沒(méi)試過(guò),有興趣的小伙伴可以試試。常見(jiàn)的特征工程的處理方法如下圖:
聯(lián)系客服