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

  • 2019 年 10 月 3 日
  • 筆記

在本章中,將會學到:

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的強大功能和靈活性。