1. 程式人生 > >基於C#的機器學習--c# .NET中直觀的深度學習

基於C#的機器學習--c# .NET中直觀的深度學習

在本章中,將會學到:

l  如何使用Kelp.Net來執行自己的測試

l  如何編寫測試

l  如何對函式進行基準測試

Kelp.Net是一個用c#編寫的深度學習庫。由於能夠將函式鏈到函式堆疊中,它在一個非常靈活和直觀的平臺中提供了驚人的功能。它還充分利用OpenCL語言平臺,在支援cpu和gpu的裝置上實現無縫操作。深度學習是一個非常強大的工具,對Caffe和Chainer模型載入的本機支援使這個平臺更加強大。您將看到,只需幾行程式碼就可以建立一個100萬個隱藏層的深度學習網路。

Kelp.Net還使得從磁碟儲存中儲存和載入模型變得非常容易。這是一個非常強大的特性,允許您執行訓練、儲存模型,然後根據需要載入和測試。它還使程式碼的產品化變得更加容易,並且真正地將訓練和測試階段分離開來。

其中,Kelp.Net是一個非常強大的工具,可以幫助你更好地學習和理解各種型別的函式、它們的互動和效能。例如,你可以使用不同的優化器在相同的網路上執行測試,並通過更改一行程式碼來檢視結果。此外,可以輕鬆地設計你的測試,以檢視使用不同批處理大小、隱藏層數、紀元、和更多內容。

什麼是深度學習?

深度學習是機器學習和人工智慧的一個分支,它使用許多層次的神經網路層(如果你願意,可以稱之為層次結構)來完成它的工作。在很多情況下,這些網路的建立是為了反映我們對人類大腦的認知,神經元像錯綜複雜的網狀結構一樣將不同的層連線在一起。這允許以非線性的方式進行資料處理。每一層都處理來自上一層的資料(當然,第一層除外),並將其資訊傳遞到下一層。幸運的話,每一層都改進了模型,最終,我們實現了目標並解決了問題。

OpenCL

Kelp.Net 大量使用了開源計算語言(OpenCL).

OpenCL認為計算系統是由許多計算裝置組成的,這些計算裝置可以是中央處理器(CPU),也可以是附加在主機處理器(CPU)上的圖形處理單元(GPU)等加速器。在OpenCL裝置上執行的函式稱為核心。單個計算裝置通常由幾個計算單元組成,這些計算單元又由多個處理元素(PS)組成。一個核心執行可以在所有或多個PEs上並行執行。

在OpenCL中,任務是在命令佇列中排程的。每個裝置至少有一個命令佇列。OpenCL執行時將排程資料的並行任務分成幾部分,並將這些任務傳送給裝置處理元素。

OpenCL定義了一個記憶體層次結構:

       Global:由所有處理元素共享,並且具有高延遲。

Read-only:更小,更低的延遲,可由主機CPU寫入,但不包括計算裝置。

Local:由流程元素組共享。

Per-elemen:私有記憶體。

OpenCL還提供了一個更接近數學的API。這可以在固定長度向量型別的公開中看到,比如float4(單精度浮點數的四個向量),它的長度為2、3、4、8和16。如果你接觸了更多的Kelp.Net並開始建立自己的函式,你將會遇到OpenCL程式設計。現在,只要知道它的存在就足夠了,而且它正在被廣泛地使用。

     OpenCL 層次結構

       在Kelp.Net各種OpenCL資源的層次結構如下圖所示:

 

 

 

 

讓我們更詳細地描述這些。

       Compute kernel

            核心物件封裝在程式中宣告的特定核心函式,以及執行此核心函式時使用的引數值。

       Compute program

由一組核心組成的OpenCL程式。程式還可以包含核心函式和常量資料呼叫的輔助函式。

       Compute sampler

描述如何在核心中讀取影象時對影象進行取樣的物件。影象讀取函式以取樣器作為引數。取樣器指定影象定址模式(表示如何處理範圍外的座標)、過濾模式以及輸入影象座標是規範化還是非規範化值。

    Compute device

計算裝置是計算單元的集合。命令佇列用於將命令排隊到裝置。命令示例包括執行核心或讀寫記憶體物件。OpenCL裝置通常對應於GPU、多核CPU和其他處理器,如數字訊號處理器(DSP)和cell/B.E.處理器。

    Compute resource

可以由應用程式建立和刪除的OpenCL資源。

Compute object

    在OpenCL環境中由控制代碼標識的物件。

Compute context

計算上下文是核心執行的實際環境和定義同步和記憶體管理的域。

Compute command queue

命令佇列是一個物件,它包含將在特定裝置上執行的命令。命令佇列是在上下文中的特定裝置上建立的。對佇列的命令按順序排隊,但可以按順序執行,也可以不按順序執行。

Compute buffer

儲存線性位元組集合的記憶體物件。可以使用在裝置上執行的核心中的指標來訪問緩衝區物件。

Compute event

    事件封裝了操作(如命令)的狀態。它可用於同步上下文中的操作。

Compute image

儲存2D或3D結構陣列的記憶體物件。影象資料只能通過讀寫函式訪問。讀取函式使用取樣器。

Compute platform

主機加上OpenCL框架管理的裝置集合,允許應用程式共享資源並在平臺上的裝置上執行核心。

Compute user event

    這表示使用者建立的事件。

Kelp.Net Framework

函式

       函式是Kelp.Net神經網路的基本組成部分。單個函式在函式堆疊中連結在一起,以建立功能強大且可能複雜的網路鏈。

我們需要了解四種主要的函式型別:

Single-input functions 單輸入函式
Dual-input functions 雙輸入函式
Multi-input functions 多輸入函式
Multi-output functions 多輸出函式

       當從磁碟載入網路時,函式也被連結在一起。

       每個函式都有一個向前和向後的方法。           

  public abstract NdArray[] Forward(params NdArray[] xs);

  public virtual void Backward([CanBeNull] params NdArray[] ys){}    

     函式棧

       函式堆疊是在向前、向後或更新傳遞中同時執行的函式層。當我們建立一個測試或從磁碟載入一個模型時,將建立函式堆疊。下面是一些函式堆疊的例子。

       它們可以小而簡單:             

 FunctionStack nn = new FunctionStack(

                 new Linear(2, 2, name: "l1 Linear"),

                 new Sigmoid(name: "l1 Sigmoid"),

                 new Linear(2, 2, name: "l2 Linear"));

它們也可以在大一點:       

FunctionStack nn = new FunctionStack(

                  // Do not forget the GPU flag if necessary

                 new Convolution2D(1, 2, 3, name: "conv1", gpuEnable: true),

                 new ReLU(),

                 new MaxPooling(2, 2),

                 new Convolution2D(2, 2, 2, name: "conv2", gpuEnable: true),

                 new ReLU(),

                 new MaxPooling(2, 2),

                 new Linear(8, 2, name: "fl3"),

                 new ReLU(),

                 new Linear(2, 2, name: "fl4")

             );

       它們也可以非常大:

             

 FunctionStack nn = new FunctionStack(

                 new Linear(neuronCount * neuronCount, N, name: "l1 Linear"),//L1

                 new BatchNormalization(N, name: "l1 BatchNorm"),

                 new LeakyReLU(slope: 0.000001, name: "l1 LeakyReLU"),

                 new Linear(N, N, name: "l2 Linear"), // L2

                 new BatchNormalization(N, name: "l2 BatchNorm"),

                 new LeakyReLU(slope: 0.000001, name: "l2 LeakyReLU"),

                 new Linear(N, N, name: "l3 Linear"), // L3

                 new BatchNormalization(N, name: "l3 BatchNorm"),

                 new LeakyReLU(slope: 0.000001, name: "l3 LeakyReLU"),

                 new Linear(N, N, name: "l4 Linear"), // L4

                 new BatchNormalization(N, name: "l4 BatchNorm"),

                 new LeakyReLU(slope: 0.000001, name: "l4 LeakyReLU"),

                 new Linear(N, N, name: "l5 Linear"), // L5

                 new BatchNormalization(N, name: "l5 BatchNorm"),

                 new LeakyReLU(slope: 0.000001, name: "l5 LeakyReLU"),

                 new Linear(N, N, name: "l6 Linear"), // L6

                 new BatchNormalization(N, name: "l6 BatchNorm"),

                 new LeakyReLU(slope: 0.000001, name: "l6 LeakyReLU"),

                 new Linear(N, N, name: "l7 Linear"), // L7

                 new BatchNormalization(N, name: "l7 BatchNorm"),

                 new LeakyReLU(slope: 0.000001, name: "l7 ReLU"),

                 new Linear(N, N, name: "l8 Linear"), // L8

                 new BatchNormalization(N, name: "l8 BatchNorm"),

                 new LeakyReLU(slope: 0.000001, name: "l8 LeakyReLU"),

                 new Linear(N, N, name: "l9 Linear"), // L9

                 new BatchNormalization(N, name: "l9 BatchNorm"),

                 new PolynomialApproximantSteep(slope: 0.000001, name: "l9 PolynomialApproximantSteep"),

                 new Linear(N, N, name: "l10 Linear"), // L10

                 new BatchNormalization(N, name: "l10 BatchNorm"),

                 new PolynomialApproximantSteep(slope: 0.000001, name: "l10 PolynomialApproximantSteep"),

                 new Linear(N, N, name: "l11 Linear"), // L11

                 new BatchNormalization(N, name: "l11 BatchNorm"),

                 new PolynomialApproximantSteep(slope: 0.000001, name: "l11 PolynomialApproximantSteep"),

                 new Linear(N, N, name: "l12 Linear"), // L12

                 new BatchNormalization(N, name: "l12 BatchNorm"),

                 new PolynomialApproximantSteep(slope: 0.000001, name: "l12 PolynomialApproximantSteep"),

                 new Linear(N, N, name: "l13 Linear"), // L13

                 new BatchNormalization(N, name: "l13 BatchNorm"),

                 new PolynomialApproximantSteep(slope: 0.000001, name: "l13 PolynomialApproximantSteep"),

                 new Linear(N, N, name: "l14 Linear"), // L14

                 new BatchNormalization(N, name: "l14 BatchNorm"),

                 new PolynomialApproximantSteep(slope: 0.000001, name: "l14 PolynomialApproximantSteep"),

                 new Linear(N, 10, name: "l15 Linear") // L15

             );

函式字典

     函式字典是一個可序列化的函式字典(如前所述)。當從磁碟載入網路模型時,將返回一個函式字典,並且可以像在程式碼中建立函式堆疊一樣對其進行操作。函式字典主要用於Caffe資料模型載入器。

Caffe1

Kelp.Net是圍繞Caffe風格開發的,它支援許多特性。

Caffe為多媒體科學家和實踐者提供了一個簡潔和可修改的框架,用於最先進的深度學習演算法和一組參考模型。該框架是一個bsd許可的c++庫,帶有Python和MATLAB繫結,用於在普通架構上高效地培訓和部署通用卷積神經網路和其他深度模型。Caffe通過CUDA GPU計算滿足了行業和網際網路規模的媒體需求,在一個K40或Titan GPU上每天處理超過4000萬張影象(大約每張影象2毫秒)。通過分離模型表示和實際實現,Caffe允許在平臺之間進行試驗和無縫切換,以簡化開發和部署,從原型機到雲環境。

“Chainer是一個靈活的神經網路框架。一個主要的目標是靈活性,因此它必須使我們能夠簡單而直觀地編寫複雜的體系結構。”

Chainer採用了按執行定義的方案,即通過實際的正向計算動態地定義網路。更準確地說,Chainer儲存的是計算曆史,而不是程式設計邏輯。例如,Chainer不需要任何東西就可以將條件和迴圈引入到網路定義中。按執行定義方案是Chainer的核心概念。這種策略也使得編寫多gpu並行化變得容易,因為邏輯更接近於網路操作。

Kelp.Net可以直接從磁碟載入Chainer模型。

Loss

       Kelp.Net由一個抽象的LossFunction類組成,設計用於確定如何評估損失的特定例項。

       在機器學習中,損失函式或成本函式是將一個事件或一個或多個變數的值直觀地對映到一個實數上的函式,表示與該事件相關的一些成本。Kelp.Net提供了兩個開箱即用的損失函式:均方誤差和軟最大交叉熵。我們可以很容易地擴充套件它們以滿足我們的需求。

模型儲存和載入

Kelp.Net使得通過呼叫一個簡單的類來儲存和載入模型變得非常容易。ModelIO類同時提供了儲存和載入方法,以便輕鬆地儲存和載入到磁碟。下面是一個非常簡單的例子,在訓練、重新載入並對模型執行測試之後儲存模型:

 

 

 

 

優化程式

       優化演算法根據模型的引數最小化或最大化誤差函式。引數的例子有權重和偏差。它們通過最小化損失來幫助計算輸出值並將模型更新到最優解的位置。擴充套件Kelp.Net以新增我們自己的優化演算法是一個簡單的過程,儘管新增OpenCL和資源方面的東西是一個協調的工作。

       Kelp.Net提供了許多預定義的優化器,比如:

              AdaDelta

    AdaGrad

    Adam

    GradientClipping

    MomentumSGD

    RMSprop

    SGD

這些都是基於抽象的優化器類。

資料集

       Kelp.Net本身支援以下資料集:

              CIFAR

    MNIST

CIFAR

CIFAR資料集有兩種形式,CIFAR-10和CIFAR 100,它們之間的區別是類的數量。讓我們簡要地討論一下兩者。

CIFAR-10

CIFAR-10資料集包含10個類中的60000張32×32張彩色影象,每個類包含6000張影象。有50,000張訓練影象和10,000張測試影象。資料集分為五個訓練批次和一個測試批次,每個測試批次有10,000張影象。測試批次包含從每個類中隨機選擇的1000個影象。訓練批次包含隨機順序的剩餘影象,但是一些訓練批次可能包含一個類的影象多於另一個類的影象。在他們之間,每批訓練包含了5000張圖片。

              CIFAR-100

CIFAR-100資料集與CIFAR-10一樣,只是它有100個類,每個類包含600個影象。每班有500張訓練圖片和100張測試圖片。CIFAR-100中的100個類被分為20個超類。每個影象都有一個細標籤(它所屬的類)和一個粗標籤(它所屬的超類)。以下是CIFAR-100的型別列表:

Superclass

Classes

水生哺乳動物

海狸、海豚、水獺、海豹和鯨魚

水族魚,比目魚,鰩魚,鯊魚和魚

蘭花、罌粟、玫瑰、向日葵和鬱金香

食品容器

瓶子、碗、罐子、杯子和盤子

水果和蔬菜

蘋果、蘑菇、桔子、梨和甜椒

家用電器裝置

時鐘、電腦鍵盤、燈、電話和電視

家用傢俱

床、椅子、沙發、桌子和衣櫃

昆蟲

蜜蜂、甲蟲、蝴蝶、毛蟲和蟑螂

大型食肉動物

熊、豹、獅子、老虎和狼

大型人造戶外用品

橋、城堡、房子、道路和摩天大樓

大型自然戶外景觀

雲、林、山、平原、海

大型雜食動物和食草動物

駱駝、牛、黑猩猩、大象和袋鼠

中等大小的哺乳動物

狐狸,豪豬,負鼠,浣熊和臭鼬

無脊椎動物

螃蟹、龍蝦、蝸牛、蜘蛛和蠕蟲

寶貝,男孩,女孩,男人,女人

爬行動物

鱷魚、恐龍、蜥蜴、蛇和烏龜

小型哺乳動物

倉鼠,老鼠,兔子,鼩鼱和松鼠

楓樹、橡樹、棕櫚樹、松樹和柳樹

車輛1

自行車、公共汽車、摩托車、小貨車和火車

車輛2

割草機、火箭、有軌電車、坦克和拖拉機

 

MNIST

MNIST資料庫是一個手寫數字的大型資料庫,通常用於訓練各種影象處理系統。該資料庫還廣泛用於機器學習領域的培訓和測試。它有一個包含6萬個例子的訓練集和一個包含1萬個例子的測試集。數字的大小已經標準化,並集中在一個固定大小的影象中,這使它成為人們想要嘗試各種學習技術而不需要進行預處理和格式化的標準選擇:

 

 

 

 

測試

       測試是實際的執行事件,也可以說是小程式。由於OpenCL的使用,這些程式是在執行時編譯的。要建立一個測試,您只需要提供一個封裝程式碼的靜態執行函式。Kelp.Net提供了一個預配置的測試器,這使得新增我們自己的測試變得非常簡單。

現在,這裡有一個簡單的XOR測試程式的例子:

     

  public static void Run()

    {

         const int learningCount = 10000;

      Real[][] trainData =
         {    

           new Real[] { 0, 0 },     
           new Real[] { 1, 0 },
           new Real[] { 0, 1 },     
           new Real[] { 1, 1 }   
        };

      Real[][] trainLabel =
         {   
             new Real[] { 0 },   
             new Real[] { 1 },   
             new Real[] { 1 },   
             new Real[] { 0 }   
         };

    FunctionStack nn = new FunctionStack(   

             new Linear(2, 2, name: "l1 Linear"),   

             new ReLU(name: "l1 ReLU"),   

             new Linear(2, 1, name: "l2 Linear"));

     nn.SetOptimizer(new AdaGrad());

   RILogManager.Default?.SendDebug("Training...");   

   for (int i = 0; i < learningCount; i++)   
   {   

        //use MeanSquaredError for loss function   

        Trainer.Train(nn,trainData[0],trainLabel[0],newMeanSquaredError(), false);   

        Trainer.Train(nn, trainData[1], trainLabel[1], new MeanSquaredError(), false);   

        Trainer.Train(nn, trainData[2], trainLabel[2], new MeanSquaredError(), false);                

      Trainer.Train(nn, trainData[3], trainLabel[3], new MeanSquaredError(), false);

      //If you do not update every time after training, you can update it as a mini batch

          nn.Update();          
  }

  RILogManager.Default?.SendDebug("Test Start...");

  foreach (Real[] val in trainData)
  {

    NdArray result = nn.Predict(val)[0];   

    RILogManager.Default?.SendDebug($"{val[0]} xor {val[1]} = {(result.Data[0] > 0.5 ? 1 : 0)} {result}");   

  }    
}    

Weaver

       Weaver是Kelp.Net的重要組成部分。是執行測試時要執行的第一個物件呼叫。這個物件包含各種OpenCL物件,比如:

l  計算上下文

l  一組計算裝置

l  計算命令佇列

l  一個布林標誌,表明GPU是否為啟用狀態

l  可核心計算資源的字典

       Weaver是用來告訴我們的程式我們將使用CPU還是GPU,以及我們將使用哪個裝置(如果我們的系統能夠支援多個裝置)的地方。我們只需要在我們的程式開始時對Weaver做一個簡單的呼叫,就像在這裡看到的:              

Weaver.Initialize(ComputeDeviceTypes.Gpu);

       我們還可以避免使用weaver的初始化呼叫,並允許它確定需要自動發生什麼。

       以下是Weaver的基本內容。它的目的是構建(在執行時動態編譯)將執行的程式:

          

      ///<summary>上下文</summary>
         internal static ComputeContext Context;

         ///<summary>裝置</summary>
         private static ComputeDevice[] Devices;

         ///<summary>命令佇列</summary>
         internal static ComputeCommandQueue CommandQueue;

         ///<summary>裝置的從零開始索引</summary>
         private static int DeviceIndex;

         ///<summary>True啟用,false禁用</summary>
         internal static bool Enable;

         ///<summary>平臺</summary>
         private static ComputePlatform Platform;

         ///<summary>核心資源</summary>
         private static readonly Dictionary<string, string> KernelSources = new Dictionary<string, string>();

 

編寫測試

       為Kelp.Net建立測試非常簡單。我們編寫的每個測試只需要公開一個執行函式。剩下的就是我們希望網路如何運作的邏輯了。

執行函式的一般準則是:

  1. 負載資料(真實或模擬):
  Real[][] trainData = new Real[N][];

  Real[][] trainLabel = new Real[N][];

  for (int i = 0; i < N; i++)

  {

      //Prepare Sin wave for one cycle

      Real radian = -Math.PI + Math.PI * 2.0 * i / (N - 1);

      trainData[i] = new[] { radian };

      trainLabel[i] = new Real[] { Math.Sin(radian)

  };

  2.建立函式堆疊:

  FunctionStack nn = new FunctionStack(

                 new Linear(1, 4, name: "l1 Linear"),

                 new Tanh(name: "l1 Tanh"),

                 new Linear(4, 1, name: "l2 Linear")

             );

  3.選擇優化器:

  nn.SetOptimizer(new SGD());

  4.訓練資料:

for (int i = 0; i < EPOCH; i++)
{

      Real loss = 0;

      for (int j = 0; j < N; j++)
      {
           //When training is executed in the network, an error is returned to the return value
           loss += Trainer.Train(nn, trainData[j], trainLabel[j], new MeanSquaredError());
      }

      if (i % (EPOCH / 10) == 0)
      {
          RILogManager.Default?.SendDebug("loss:" + loss / N);
          RILogManager.Default?.SendDebug("");
      }
 }

  5.測試資料:

RILogManager.Default?.SendDebug("Test Start...");

foreach (Real[] val in trainData)
{
    RILogManager.Default?.SendDebug(val[0]+":"+nn.Predict(val)[0].Data[0]);
} 

總結

       在這一章中,我們進入了深度學習的世界。我們學習瞭如何使用Kelp.Net作為我們的研究平臺,它幾乎可以測試任何假設。我們還看到了Kelp.Net的強大功能和靈活