深度學習:RBM理論與C++實戰

    時間過得真快啊,深度學習已經火的快十年了,不過目前,仍有人繼續觀望,也有些觀望者“忍不住”陸續加入了這個逐漸龐大的研究團體,開始相信“深度”的power了。這不,前些日子Duke大學的副校長Lawrence Carin就過來介紹他們團隊的研究成果,談了談他對深度學習的理解,並且開始相信這種深層結構的有效性。

    我也談談對深度學習的理解吧,才疏學淺,不正確的地方請多多指教。深度學習表達的是一種概念,而非某一種具體的算法。它在某個方面是受到了神經生物學研究成果的啓發,主要是HubelWiesel1959-1962)發現視覺皮質是分層次的,換句話說,視覺系統是分層處理其接收到的信息的。自此人們開始模擬這種分層的信息處理系統,如風靡一時的BPAutoencoderCNNsSparseCoding等。直到2006Hinton提出DBNsDeep Belief Networks)和DBMsDeep Boltzmann Machines,這裏指AutoEncoder Stacks這種框架)後,深度學習這個名詞才被提出。DBNsDBMs的訓練主要分爲兩部分,pretrainingfinetuningpretraing中採用分層訓練的方法,這樣不僅降低了訓練時間也防止出現過擬合。每層網絡訓練中,有一個輸入層(也叫可見層),也就是系統接收到的信息,有一個特徵層(也叫隱含層),也就是系統對接收到的信息的處理結果(特徵表達)。每層的網絡結構實際上是一個生成模型RBMRestricted Boltzmann Machine),爲了防止深層模型過擬合,要保證每層網絡結構中的輸入層到特徵層的響應再次生成到輸入層後要儘可能的與原輸入層相似,並以此準則爲優化目標去調整層網絡結構的參數。訓練完一層後,再將該層的特徵層作爲下一層網絡結構的輸入層繼續以同樣的準則訓練下一層網絡。在pretraining階段訓練(unsupervised)得到的這個深層網絡的參數實際上是比較接近模型的最優參數,此時再通過BP進行深層模型的參數調優是比較合理的,並有效的防止了過擬合。在這裏值得一提的是,正是因爲在finetuning階段DBNs需要樣本的類別,這就決定了DBNsDBMs是不同的模型,DBNs是生成模型(generative model),而DBMs是判別模型(discriminative model)。生成模型認爲觀察值(類比於DBNs中的輸入層)是由狀態(類比於樣本觀察值的類別)生成的,它不像判別模型直接對P(狀態|觀察值)建模,而是考慮聯合概率P(狀態,觀察值),也就是還考慮了P(觀察值|狀態);而判別模型直接對P(狀態|觀察值)建模。在finetuning階段,對於DBNs模型參數的調優就是讓調整後的參數使得P(觀察值|狀態)最大,因此其是一個生成模型;而對於DBMs模型,由於其是一個對稱模型,對於模型參數的調優仍是在讓P(狀態|觀察值)最大,因此其是判別模型。

    鑑於DBNsDBMs模型是深度學習中的兩種主要算法框架,本文旨在介紹構成這兩種模型的基本結構RBM模型的理論,並給出其程序實現。這裏推薦一篇論文,張春霞的《受限制玻爾茲曼機簡介》,對於想自己實現RBM的小夥伴來說是非常實用的資料,內容不僅豐富而且簡單易懂,當然Hinton的論文也是要看的哈,畢竟人家是鼻祖。接下來的理論部分就來自該論文再加上自己的一些理解。後續看情況也會對DBNsDBMs框架的理論及實現進行分析,源代碼也不難找到的,Github上,Hinton的主頁上都可以下載的哈。


1.    RBM理論


1)RBM的基本模型

RBM的基本模型可用下圖描述(爲了討論方便,定義模型中所有結點的取值只能是01,當然其也可以是01的實數):

wKioL1UOblKSNGnPAACwbZ3_VqU956.jpg

對於給定的一組狀態(v,h)是RBM系統的能量定義爲:

wKiom1UObWHTPo8rAAA1U6vp6GI928.jpg

其中Wij表示可見單元i與隱單元j之間的連接權重,ai表示可見單元i的偏置,bj表示隱單元j的偏置。當參數確定時,基於該能量函數我們可以得到(vh)的聯合概率分佈:

wKiom1UObYLA7PU1AAAqqHQ1tus095.jpg

對於一個實際問題,我們最關心的是由RBM所定義的關於觀測數據v的分佈Pv|θ),即聯合概率分佈Pv,h|θ)的邊際分佈:

wKiom1UObbWxeOZZAAAc7bIufds112.jpg

爲了計算該分佈,我們需要計算Z(θ)(歸一化因子,也叫配分函數, partition function)。但對於一個可見層有n個結點,隱含層有m個結點的RBM來說,計算Z(θ)需要2^n * 2^m次,這使得不能有效的計算Z(θ)。

但是,由於RBM是層間有連接,層內無連接的,於是當給定可見單元的狀態時,各隱單元之間是條件獨立的。此時,可以使用sigmoid函數來作爲隱單元的概率響應,於是有:

wKioL1UObw7SpgTDAAAdNQ3VI5w469.jpg

其中,σ(x)爲sigmoid激活函數。

         由於RBM的結構是對稱的,當給定隱單元的狀態時,各可見單元的激活狀態之間也是條件獨立的,即第i個可見單元的激活概率爲:

wKioL1UObyqhU14YAAAeb7tDEH8599.jpg


2)RBM模型參數的學習

RBM模型參數θ的學習的準則如下:

wKioL1UOb4STyNqkAAAqsE7I3Ds114.jpg

這個準則通俗一點描述就是,輸入層到特徵層的響應再次生成到輸入層後要儘可能的與原輸入層相似。

    爲了獲得最優的θ,可以使用隨機梯度上升法(stochastic gradient ascent)求L(θ)的最大值。其中,求L(θ)的梯度的推導如下:

wKiom1UOboPzjrsOAAIDx7pokrM052.jpg

其中《。》P表示求關於分佈P的數學期望。上式中的第一項容易求出,而第二項由於Z(θ)的存在,一般採用近似計算的方法,如Gibbs採樣。

    分別用datamodel表示P(h|v(t), θ)P(v,h|θ),於是L(θ)的梯度可由如下式子來表示:

wKioL1UOb_jjGbcmAABvgiDpcUY929.jpg


3)Gibbs採樣

wKioL1UOcEnzLcmTAAOoY2Urn7A188.jpg

    通俗一點說,要求一個可見層n個結點及隱含層m個結點的RBM模型的Z(θ)需要2^n * 2^m次,如果nm很大的話,那麼Z的計算就變得不可行,於是我們把該RBM模型所有可能出現的狀態(2^n * 2^m種)看做是一個總體,對它進行採樣,用採樣得到的樣本去估計總體。上面的k步採樣中的k實際上是遠比2^n * 2^m小的,這樣對Z進行估計計算的話就變得可行了。


4)基於對比散度的RBM快速學習算法

2002Hinton提出了一個RBM的快速學習算法,對比散度(contrastive divergenceCD)。與吉布斯採樣不同Hinton指出當使用訓練數據初始化v0時,我們僅需要使用k(通常k=1)步吉布斯採樣便可以得到足夠好的近似。在CD算法一開始,可見單元的狀態被設置成一個訓練樣本,並利用式

wKioL1UOcKvQwtB4AAAdNQ3VI5w895.jpg

計算所有隱層單元的二值狀態。在所有隱層單元的狀態確定之後,根據式

wKiom1UOb8SjlmaCAAAeb7tDEH8879.jpg

來確定第i個可見單元vi取值爲1的概率,進而產生可見層的一個重構(reconstruction)。這樣,在使用隨機梯度上升法最大化L(θ)在訓練數據上的值時,各參數的更新準則爲:

wKiom1UOcAqQTyXRAAA-Wxq2cXE909.jpg

這裏,e是學習率,也就是按梯度方向更新參數的速度,《。》recon表示一步重構後模型定義的分佈。CD算法描述如下:

wKiom1UOcDGj8HjRAAGp8PPPff8146.jpg


2.    RBMC++實現

由於個人比較偏向於使用C++語言,所以一般對於感興趣的算法都會選擇C++語言去實現。這裏分享一個算法實現過程的經驗,一般在算法實現過程中,首先我會考慮找其源代碼,如果別人已經實現了,我就會對他的代碼進行評價,比如說最重要的代碼層次是否清晰,層次清晰是很重要的,要知道整個計算機系統在搭建的過程中我們考慮最多的問題就是分層,哪些由硬件做,哪些由軟件做。算法實現也一樣,對於算法的核心部分最好不要依賴於特定的庫,不是說不可以,這樣做是考慮到可移植性。而在基於核心算法的應用中需要與用戶進行交互,這裏就可以依賴於特定的庫了,方便我們快速的完成應用系統的實現。如果對現有的代碼比較滿意的話,直接拿來用就好了,如果不滿意,可以自己寫(有些代碼看着看着就想自己寫了,哈哈),或者改現有的代碼。

下面列出來的這個代碼是Github上的一個開源的對HintonMatlab版本的一個C++實現,個人比較喜歡C++的這個版本,層次清晰,稍微的改動了幾個個人覺得不好的部分和給出較爲詳細的註釋。Matlab版本可以到Hinton的主頁下載:

http://www.cs.toronto.edu/~hinton/MatlabForSciencePaper.html

C++版本地址:https://github.com/jdeng/rbm-mnist

下面給出代碼:

#include <iostream>
#include <numeric>
#include <vector>
#include <random>
#include <memory>
#include <fstream>
#include <chrono>
#include <functional>

/*
  Note:: it is better not to extend the standard vectors, because they
  do not have virtual functions.
*/

/**
 * @brief definitions about some basic operations that could
 *  be used for training a RBM
 */
typedef std::vector<float> ivector;
namespace v {

	struct LightVector
	{
	  std::pair<float *, float *> pair_;

	  LightVector() {}
	  LightVector(float* begin, float* end) {
		pair_ = std::pair<float*, float*>(begin, end);
	  }
	  LightVector(const LightVector& bh) {
		pair_ = bh.pair_;
	  }
	  
	  float *data() const { return pair_.first; }
	  size_t size() const { return std::distance(pair_.first, pair_.second); }
	  bool empty() const  { return pair_.first == pair_.second; }

	  float& operator[](size_t i) { return *(pair_.first + i); }
	  float operator[](size_t i) const { return *(pair_.first + i); }
	};

	template <class Vector1, class Vector2> inline float dot(const Vector1&x, const Vector2& y) { 
		int m = x.size(); const float *xd = x.data(), *yd = y.data();
		float sum = 0.0;
		while (--m >= 0) sum += (*xd++) * (*yd++);
		return sum;
	}

	// saxpy: x = x + g * y; x = a * x + g * y
	inline void saxpy(ivector& x, float g, const ivector& y) {
		int m = x.size(); float *xd = x.data(); const float *yd = y.data();
		while (--m >= 0) (*xd++) += g * (*yd++);
	}

	inline void saxpy(float a, ivector& x, float g, const ivector& y) {
		int m = x.size(); float *xd = x.data(); const float *yd = y.data();
		while (--m >= 0) { (*xd) = a * (*xd) + g * (*yd); ++xd; ++yd; }
	}

	inline void saxpy2(ivector& x, float g, const ivector& y, float h, const ivector& z) {
		int m = x.size(); float *xd = x.data(); const float *yd = y.data(); const float *zd = z.data();
		while (--m >= 0) { (*xd++) +=  (g * (*yd++)  + h * (*zd++)); }
	}

	inline void scale(ivector& x, float g) {
		int m = x.size(); float *xd = x.data();
		while (--m >= 0) (*xd++) *= g;
	}

#if 0
	inline void addsub(ivector& x, const ivector& y, const ivector& z) {
		int m = x.size(); float *xd = x.data(); const float *yd = y.data(); const float *zd = z.data();
		while (--m >= 0) (*xd++) += ((*yd++) - (*zd++));
	}
#endif

	inline void unit(ivector& x) {
		float len = ::sqrt(dot(x, x));
		if (len == 0) return;

		int m = x.size(); float *xd = x.data();
		while (--m >= 0) (*xd++) /= len;
	}

	inline bool isfinite(const ivector& x) { 
		for(auto const& i: x) { if (! _finite(i)) return false; } //std::isfinite(i)
		return true;
	}

}


/**
 * @brief Batch is a struct used for training, during which all samples
 *  are deviede into several batchs and each batch keeps its begin and 
 *  end
 *
 * @param pair_ keep each batch's begin and end iterator
 *
 */
struct Batch
{
  typedef std::vector<ivector>::iterator Iterator;
  std::pair<Iterator, Iterator> pair_;

  Batch() {
  }
  Batch(Iterator begin, Iterator end) {
	pair_ = std::pair<Iterator, Iterator>(begin, end);
  }
  Batch(const Batch& bh) {
	  pair_ = bh.pair_;
  }

  Iterator begin() const { return pair_.first; }
  Iterator end() const { return pair_.second; }
  size_t size() const { return std::distance(pair_.first, pair_.second); }
  bool empty() const  { return pair_.first == pair_.second; }

  ivector& operator[](size_t i) { return *(pair_.first + i); }
  const ivector& operator[](size_t i) const { return *(pair_.first + i); }

};

/**
 * @brief RBMType an enum for hidden layer's active function,
 *  struct RBMConf keeps configuration for each RBM's training,
 *  struct RBM represents a RBM
 */
enum class RBMType 
{
  SIGMOID,
  LINEAR,
  EXP
};
struct RBMConf 
 {
	 float momentum_, weight_cost_, learning_rate_;
	 RBMConf(float momentum=0.5, float weight_cost=0.0002, float learning_rate=0.1):
		 momentum_(momentum), weight_cost_(weight_cost), learning_rate_(learning_rate) {

	 }
	 RBMConf(const RBMConf& conf) {
		 momentum_=conf.momentum_;
		 weight_cost_=conf.weight_cost_;
		 learning_rate_=conf.learning_rate_;
	 }
  };
struct RBM
{
  /**
   * @brief what is inside RBM
   * 
   */
  ivector bias_visible_, bias_hidden_, bias_visible_inc_, bias_hidden_inc_;
  ivector weight_, weight_inc_;
  RBMType type_;
  RBMConf conf_;

  static float sigmoid(float x) { return (1.0 / (1.0 + exp(-x))); }

  static const ivector& bernoulli(const ivector& input, ivector& output)
  { 
    static std::default_random_engine eng(::time(NULL));
    static std::uniform_real_distribution<float> rng(0.0, 1.0);

    for (size_t i=0; i<input.size(); ++i) { output[i] = (rng(eng) < input[i])? 1.0 : 0.0; } 
    return output;
  }

  static const ivector& add_noise(const ivector& input, ivector& output)
  { 
    static std::default_random_engine eng(::time(NULL));
    static std::normal_distribution<float> rng(0.0, 1.0);

    for (size_t i=0; i<input.size(); ++i) { output[i] = input[i] + rng(eng); }
    return output;
  }

  RBM() {}

  /**
   * @brief RBM constructor
   *
   * @param visible number of nodes for visible layer
   * @param hidden number of nodes for hidden layer
   * @param type of active function for hidden nodes
   * @param conf configuration for traing a RBM
   */
  RBM(size_t visible, size_t hidden, RBMType type = RBMType::SIGMOID, RBMConf& conf = RBMConf())
    : bias_visible_(visible), bias_hidden_(hidden), weight_(visible * hidden)
    , bias_visible_inc_(visible), bias_hidden_inc_(hidden), weight_inc_(visible * hidden)
	, type_(type), conf_(conf)
  {
    static std::default_random_engine eng(::time(NULL));
    static std::normal_distribution<float> rng(0.0, 1.0);
    for (auto& x: weight_) x = rng(eng) * .1;
  }

  /**
   * @brief get size of RBM
   */
  size_t num_hidden() const { return bias_hidden_.size(); }
  size_t num_visible() const { return bias_visible_.size(); }
  size_t num_weight() const { return weight_.size(); }

  /**
   * @brief copy a RBM to another
   */
  int mirror(const RBM& rbm)
  {
    size_t n_visible = bias_visible_.size(), n_hidden = bias_hidden_.size();
    if (n_hidden != rbm.num_visible() || n_visible != rbm.num_hidden()) { 
      std::cout << "not mirrorable" << std::endl;
      return -1;
    }
    
    bias_visible_ = rbm.bias_hidden_;
    bias_hidden_ = rbm.bias_visible_;
    for (size_t i = 0; i < n_visible; ++i) {
       for (size_t j = 0; j < n_hidden; ++j) {
        weight_[j * n_visible + i] = rbm.weight_[i * n_hidden + j];
      }
    }
    return 0;  
  }

  /**
   * @brief activate the hidden layer's nodes according to RBM's Type
   *
   * @param visibel for visible layer's nodes
   * @param hidden for hidden layer's nodes
   * @param bias_hidden for hidden nodes' bias
   * @param weight for corresponding weights between visible nodes and hidden nodes
   *
   * @return hidden layer's nodes
   */
  const ivector& activate_hidden(const ivector& visible, ivector& hidden) const {

    size_t n_visible = visible.size(), n_hidden = hidden.size();

    std::fill(hidden.begin(), hidden.end(), 0);
    for (size_t i = 0; i < n_hidden; ++i) {
      float *xd = const_cast<float *>(weight_.data() + i * n_visible);
      float s = v::dot(visible, v::LightVector(xd, xd + n_visible));
      s += bias_hidden_[i];

      if (type_ == RBMType::SIGMOID) s = sigmoid(s);
      else if (type_ == RBMType::EXP) s = exp(s);

      hidden[i] = s;
    }

    return hidden;
  }
  template <class Vector1, class Vector2, class Vector3>
  static const Vector2& activate_hidden(const Vector1& visible, Vector2& hidden, const Vector3& bias_hidden, const Vector3& weight, RBMType type)
  {
    size_t n_visible = visible.size(), n_hidden = hidden.size();

    std::fill(hidden.begin(), hidden.end(), 0);
    for (size_t i = 0; i < n_hidden; ++i) {
      float *xd = const_cast<float *>(weight.data() + i * n_visible);
      float s = v::dot(visible, v::LightVector(xd, xd + n_visible));
      s += bias_hidden[i];

      if (type == RBMType::SIGMOID) s = sigmoid(s);
      else if (type == RBMType::EXP) s = exp(s);

      hidden[i] = s;
    }

    return hidden;
  }

  /**
   * @brief activate the hidden layer's nodes using sigmoid
   *
   * @param hidden for hidden layer's nodes
   * @param visible for visible layer's nodes
   *
   * @return visible nodes
   */
  const ivector& activate_visible(const ivector& hidden, ivector& visible) const
  {
    size_t n_visible = bias_visible_.size(), n_hidden = bias_hidden_.size();

    std::fill(visible.begin(), visible.end(), 0);
    for (size_t i = 0; i < n_visible; ++i) {
      float s = 0;
      for (size_t j = 0; j < n_hidden; ++j) s += hidden[j] * weight_[j * n_visible+ i];
      s += bias_visible_[i];

      s = sigmoid(s);
      visible[i] = s;
    }

    return visible;
  }

  /**
   * @brief train the RBM
   *
   * @param inputs for training samples
   *
   * @return training error
   */
  float train(Batch& inputs)
  {
    size_t n_visible = bias_visible_.size(), n_hidden = bias_hidden_.size();
    float momentum = conf_.momentum_, learning_rate = conf_.learning_rate_, weight_cost = conf_.weight_cost_;

    // temporary results
    ivector v1(n_visible), h1(n_hidden), v2(n_visible), h2(n_hidden), hs(n_hidden);

    //delta
    ivector gw(n_visible * n_hidden), gv(n_visible), gh(n_hidden);
    for (auto const& input: inputs) {//C++11 serial iterateration, 
									 //inputs could be a vector or an object of a class
									 //in which functions begin() and end() are defined 
									 //with a iterator type return.
      v1 = input;
      this->activate_hidden(v1, h1);
      this->activate_visible((type_ == RBMType::LINEAR? add_noise(h1, hs): bernoulli(h1, hs)), v2);
      this->activate_hidden(v2, h2);

      for (size_t i = 0; i < n_visible; ++i) {
        for (size_t j = 0; j < n_hidden; ++j) gw[j * n_visible + i] += h1[j] * v1[i] - h2[j] * v2[i];
      }

//      gh += (h1 - h2);
//      gv += (v1 - v2);
      v::saxpy2(gh, 1.0, h1, -1.0, h2);
      v::saxpy2(gv, 1.0, v1, -1.0, v2);
    }

    //update
    size_t n_samples = inputs.size();
//    gw /= float(n_samples);
//    gw -= weight_ * weight_cost;
    v::saxpy(1.0/n_samples, gw, -weight_cost, weight_);
//    weight_inc_ = weight_inc_ * momentum + gw * learning_rate;
    v::saxpy(momentum, weight_inc_, learning_rate, gw);

//    weight_ += weight_inc_;
    v::saxpy(weight_, 1.0, weight_inc_);

//    gh /= float(n_samples); 
//    bias_hidden_inc_ = bias_hidden_inc_ * momentum + gh * learning_rate;
    v::saxpy(momentum, bias_hidden_inc_, learning_rate / n_samples, gh);
//    bias_hidden_ += bias_hidden_inc_;
    v::saxpy(bias_hidden_, 1.0, bias_hidden_inc_);

//    gv /= float(n_samples); 
//    bias_visible_inc_ = bias_visible_inc_ * momentum + gv * learning_rate;
    v::saxpy(momentum, bias_visible_inc_, learning_rate / n_samples, gv);
//    bias_visible_ += bias_visible_inc_;
    v::saxpy(bias_visible_, 1.0, bias_visible_inc_);

//    float error = sqrt(gv.dot(gv) / n_visible);
    v::scale(gv, 1.0/n_samples);
    float error = sqrt(v::dot(gv, gv) / n_visible);

    return error;
  }

  virtual float free_energy() const
  {
    size_t n_visible = bias_visible_.size(), n_hidden = bias_hidden_.size();
    float s = 0;
    for (size_t i = 0; i < n_visible; ++i) {
      for (size_t j = 0; j < n_hidden; ++j) 
        s += weight_[j * n_visible+ i] * bias_hidden_[j] * bias_visible_[i];
    }
    return -s;
  }

  template <typename T> static void _write(std::ostream& os, const T& v) { os.write(reinterpret_cast<const char *>(&v), sizeof(v)); }
  void store(std::ostream& os) const
  {
    int type = (int)type_;
    size_t n_visible = bias_visible_.size();
    size_t n_hidden = bias_hidden_.size();

    _write(os, type); _write(os, n_visible); _write(os, n_hidden);
    for (float v: bias_visible_) _write(os, v);
    for (float v: bias_hidden_) _write(os, v);
    for (float v: weight_) _write(os, v);
  }

  template <typename T> static void _read(std::istream& is, T& v) { is.read(reinterpret_cast<char *>(&v), sizeof(v)); }
  void load(std::istream& is)
  {
    int type = 0;
    size_t n_visible = 0, n_hidden = 0;
    _read(is, type); _read(is, n_visible); _read(is, n_hidden);

    type_ = (RBMType)type;
    bias_visible_.resize(n_visible);
    bias_hidden_.resize(n_hidden);
    weight_.resize(n_visible * n_hidden);

    for (float& v: bias_visible_) _read(is, v);
    for (float& v: bias_hidden_) _read(is, v);
    for (float& v: weight_) _read(is, v);
  }

};

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章