機器學習是時下非常流行的話題,而Tensorflow是機器學習中最有名的工具包。TensorflowSharp是Tensorflow的C#語言表述。本文會對TensorflowSharp的使用進行一個簡單的介紹。
本文會先介紹Tensorflow的一些基本概念,然后實現一些基本操作例如數字相加等運算。然后,實現求兩個點(x1,y1)和(x2,y2)的距離。最后,通過這些前置基礎和一些C#代碼,實現使用KNN方法識別MNIST手寫數字集合(前半部分)。閱讀本文絕對不需要任何機器學習基礎,因為我現在也才剛剛入門,行文不准確之處難免,敬請見諒。
本文的后半部分還在整理之中。
1. 什么是機器學習
用最最簡單的話來說,機器學習就是不斷改進一個模型的過程,使之可以更好的描述一組數據的內在規律。假設,我們拿到若干人的年齡(a1,a2,a3…)和他們的工資(b1,b2,b3…),此時,我們就可以將這些點畫在一個二維直角坐標系中,包括(a1,b1),(a2,b2)等等。這些就稱為輸入或訓練數據。
我們可以用數學的最小二乘法擬合一條直線,這樣就可以得到最好的可以描述這些數據的規律y=ax+b了。當然,因為我們有很多個點,所以它們可能不在一條直線上,因此任何的直線都不會過它們所有的點,即一定會有誤差。
但對於電腦來說,它可以使用一種截然不同的方式來得到y=ax+b中a,b的值。首先,它從一個隨便指定的a和b出發(例如a=100,b=1),然后它算出y=100(a1)+1的值和b1的區別,y=100(a2)+1和b2的區別,等等。它發現誤差非常大,此時,它就會調整a和b的值(通過某種算法),使得下一次的誤差會變小。如果下次的誤差反而變得更大了,那就說明,要么是初始值a,b給的不好,要么是y=ax+b可能不是一個好的模型,可能一個二次方程y=a^2+bx+c更好,等等。
經過N輪調整(這稱為模型的訓練),誤差的總和可能已經到了一個穩定的,較小的值。誤差小時,a和b的調整相對當然也會較小。此時的a和b就會十分接近我們使用最小二乘法做出來的值,這時,就可以認為模型訓練完成了。
當然,這只是機器學習最簡單的一個例子,使用的模型也只是線性的直線方程。如果使用更加復雜的模型,機器學習可以做出十分強大的事情。
2. 環境初始化
我使用VS2017創建一個新的控制台應用,然后,使用下面的命令安裝TensorflowSharp:
nuget install TensorFlowSharp
TensorflowSharp的源碼地址:https://github.com/migueldeicaza/TensorFlowSharp
如果在運行時發現問題“找不到libtensorflow.dll”,則需要訪問
下載這個壓縮包。然后,在下載的壓縮包中的\lib中找到tensorflow.dll,將它改名為libtensorflow.dll,並在你的工程中引用它。
這樣一來,環境初始化就完成了。
3. TensorflowSharp中的概念
TensorflowSharp / Tensorflow中最重要的幾個概念:
圖(Graph):它包含了一個計算任務中的所有變量和計算方式。可以將它和C#中的表達式樹進行類比。例如,一個1+2可以被看作為兩個常量表達式,以一個二元運算表達式連接起來。在Tensorflow的世界中,則可以看成是兩個tensor和一個op(operation的縮寫,即操作)。簡單來說,做一個機器學習的任務就是計算一張圖。
在計算圖之前,當然要把圖建立好。例如,計算(1+2)*3再開根號,是一個包括了3個tensor和3個Op的圖。
不過,Tensorflow的圖和常規的表達式還有所不同,Tensorflow中的節點變量是可以被遞歸的更新的。我們所說的“訓練”,也就是不停的計算一個圖,獲得圖的計算結果,再根據結果的值調整節點變量的值,然后根據新的變量的值再重新計算圖,如此重復,直到結果令人滿意(小於某個閾值),或跑到了一個無窮大/小(這說明圖的變量初始值設置的有問題),或者結果基本不變了為止。
會話(Session):為了獲得圖的計算結果,圖必須在會話中被啟動。圖是會話類型的一個成員,會話類型還包括一個runner,負責執行這張圖。會話的主要任務是在圖運算時分配CPU或GPU。
張量(tensor): Tensorflow中所有的輸入輸出變量都是張量,而不是基本的int,double這樣的類型,即使是一個整數1,也必須被包裝成一個0維的,長度為1的張量【1】。一個張量和一個矩陣差不多,可以被看成是一個多維的數組,從最基本的一維到N維都可以。張量擁有階(rank),形狀(shape),和數據類型。其中,形狀可以被理解為長度,例如,一個形狀為2的張量就是一個長度為2的一維數組。而階可以被理解為維數。
階 |
數學實例 |
Python 例子 |
0 |
純量 (只有大小) |
s = 483 |
1 |
向量(大小和方向) |
v = [1.1, 2.2, 3.3] |
2 |
矩陣(數據表) |
m = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] |
3 |
3階張量 (數據立體) |
t = [[[2], [4], [6]], [[8], [10], [12]], [[14], [16], [18]]] |
Tensorflow中的運算(op)有很多很多種,最簡單的當然就是加減乘除,它們的輸入和輸出都是tensor。
Runner:在建立圖之后,必須使用會話中的Runner來運行圖,才能得到結果。在運行圖時,需要為所有的變量和占位符賦值,否則就會報錯。
4. TensorflowSharp中的幾類主要變量
Const:常量,這很好理解。它們在定義時就必須被賦值,而且值永遠無法被改變。
Placeholder:占位符。這是一個在定義時不需要賦值,但在使用之前必須賦值(feed)的變量,通常用作訓練數據。
Variable:變量,它和占位符的不同是它在定義時需要賦值,而且它的數值是可以在圖的計算過程中隨時改變的。因此,占位符通常用作圖的輸入(即訓練數據),而變量用作圖中可以被“訓練”或“學習”的那些tensor,例如y=ax+b中的a和b。
5. 基本運算
下面的代碼演示了常量的使用:
//基礎常量運算,演示了常量的使用 static void BasicOperation() { using (var s = new TFSession()) { var g = s.Graph; //建立兩個TFOutput,都是常數 var v1 = g.Const(1.5); var v2 = g.Const(0.5); //建立一個相加的運算 var add = g.Add(v1, v2); //獲得runner var runner = s.GetRunner(); //相加 var result = runner.Run(add); //獲得result的值2 Console.WriteLine($"相加的結果:{result.GetValue()}"); } }
使用占位符:
//基礎占位符運算 static void BasicPlaceholderOperation() { using (var s = new TFSession()) { var g = s.Graph; //占位符 - 一種不需要初始化,在運算時再提供值的對象 //1*2的占位符 var v1 = g.Placeholder(TFDataType.Double, new TFShape(2)); var v2 = g.Placeholder(TFDataType.Double, new TFShape(2)); //建立一個相乘的運算 var add = g.Mul(v1, v2); //獲得runner var runner = s.GetRunner(); //相加 //在這里給占位符提供值 var data1 = new double[] { 0.3, 0.5 }; var data2 = new double[] { 0.4, 0.8 }; var result = runner .Fetch(add) .AddInput(v1, new TFTensor(data1)) .AddInput(v2, new TFTensor(data2)) .Run(); var dataResult = (double[])result[0].GetValue(); //獲得result的值 Console.WriteLine($"相乘的結果: [{dataResult[0]}, {dataResult[1]}]"); } }
在上面的代碼中,我們使用了fetch方法來獲得數據。Fetch方法用來幫助取回操作的結果,上面的例子中操作就是add。我們看到,整個圖的計算是一個類似管道的流程。在fetch之后,為占位符輸入數據,最后進行運算。
使用常量表示矩陣:
//基礎矩陣運算 static void BasicMatrixOperation() { using (var s = new TFSession()) { var g = s.Graph; //1x2矩陣 var matrix1 = g.Const(new double[,] { { 1, 2 } }); //2x1矩陣 var matrix2 = g.Const(new double[,] { { 3 }, { 4 } }); var product = g.MatMul(matrix1, matrix2); var result = s.GetRunner().Run(product); Console.WriteLine("矩陣相乘的值:" + ((double[,])result.GetValue())[0, 0]); }; }
6. 求兩個點的距離(L1,L2)
求兩點距離實際上就是若干操作的結合而已。我們知道,(x1,x2), (y1,y2)的距離為:
Sqrt((x1-x2)^2 + (y1-y2)^2)
因此,我們通過張量的運算,獲得
[x1-x2, y1-y2] (通過Sub)
[(x1-x2)^2, (y1-y2)^2] (通過Pow)
然后,把這兩個數加起來,這需要ReduceSum運算符。最后開根就可以了。我們把整個運算賦給變量distance,然后fetch distance:
//求兩個點的L2距離 static void DistanceL2(TFSession s, TFOutput v1, TFOutput v2) { var graph = s.Graph; //定義求距離的運算 //這里要特別注意,如果第一個系數為double,第二個也需要是double,所以傳入2d而不是2 var pow = graph.Pow(graph.Sub(v1, v2), graph.Const(2d)); //ReduceSum運算將輸入的一串數字相加並得出一個值(而不是保留輸入參數的size) var distance = graph.Sqrt(graph.ReduceSum(pow)); //獲得runner var runner = s.GetRunner(); //求距離 //在這里給占位符提供值 var data1 = new double[] { 6, 4 }; var data2 = new double[] { 9, 8 }; var result = runner .Fetch(distance) .AddInput(v1, new TFTensor(data1)) .AddInput(v2, new TFTensor(data2)) .Run(); Console.WriteLine($"點v1和v2的距離為{result[0].GetValue()}"); }
最后,我們根據目前所學,實現KNN識別MNIST。
7. 實現KNN識別MNIST(1)
什么是KNN
K最近鄰(k-Nearest Neighbor,KNN)分類算法,是一個理論上比較成熟的方法,也是最簡單的機器學習算法之一。該方法的思路是:如果一個樣本在特征空間中的k個最相似(即特征空間中最鄰近)的樣本中的大多數屬於某一個類別,則認為該樣本也屬於這個類別。
圖中,綠色圓要被決定賦予哪個類,是紅色三角形還是藍色四方形?如果K=3,由於紅色三角形所占比例為2/3,綠色圓將被賦予紅色三角形那個類,如果K=5,由於藍色四方形比例為3/5,因此綠色圓被賦予藍色四方形類。
在進行計算時,KNN就表現為:
- 首先獲得所有的數據
- 然后對一個輸入的點,找到離它最近的K個點(通過L1或L2距離)
- 然后,對這K個點所代表的值,找出最多的那個類,那么,這個輸入的數據就被認為屬於那個類
對MNIST數據的KNN識別,在讀入若干個輸入數據(和代表的數字)之后,逐個讀入測試數據。對每個測試數據,找到離他最近的K個輸入數據(和代表的數字),找出最多的代表數字A。此時,測試數據就被認為代表數字A。因此,使用KNN識別MNIST數據就可以化為求兩個點(群)的距離的問題。
MNIST數據集
MNIST是一個非常有名的手寫數字識別的數據集。它包含了6萬張手寫數字圖片,例如:
當然,對於我們人類而言,識別上面四幅圖是什么數字是十分容易的,理由很簡單,就是“看着像”。比如,第一張圖看着就像5。但如果是讓計算機來識別,它可無法理解什么叫看着像,就顯得非常困難。實際上,解決這個問題有很多種方法,KNN是其中最簡單的一種。除了KNN之外,還可以使用各種類型的神經網絡。
我們可以將每個圖片看成一個點的集合。實際上,在MNIST輸入中,圖片被表示為28乘28的一個矩陣。例如,當我們成功讀取了一張圖之后,將它打印出來會發現結果是這樣的(做了一些處理):
其中,數字均為byte類型(0-255),數字越大,代表灰度越深。當然,0就代表白色了。因此,你可以想象上面的那張圖就是一個手寫的2。如果把上圖的000換成3個空格可以看的更清楚:
對於每張這樣的圖,MNIST提供了它的正確答案(即它應該是代表哪個數字),被稱為label。上圖的label顯然就是2了。因此,每張輸入的小圖片都是一個28乘28的矩陣(含有784個數字),那么,我們當然也可以計算任意兩個小圖片的距離,它就是784個點和另外784個點的距離之和。因此,如果兩張圖的距離很小,那么它們就“看着像”。在這里,我們可以有很多定義距離的方式,簡單起見,我們就將兩點的距離定義為L1距離,即直接相減之后取絕對值。例如,如果兩個圖片完全相同(784個數字位置和值都一樣),那么它們的距離為0。如果它們僅有一個數字不同,一個是6,一個是8,那么它們的距離就是2。
那么,在簡單了解了什么是KNN之后,我們的任務就很清楚了:
- 獲得數據
- 把數據處理為一種標准形式
- 拿出數據中的一部分(例如,5000張圖片)作為KNN的訓練數據,然后,再從數據中的另一部分拿一張圖片A
- 對這張圖片A,求它和5000張訓練圖片的距離,並找出一張訓練圖片B,它是所有訓練圖片中,和A距離最小的那張(這意味着K=1)
- 此時,就認為A所代表的數字等同於B所代表的數字b
- 如果A的label真的是b,那么就增加一次獲勝次數
通過多次拿圖片,我們就可以獲得一個准確率(獲勝的次數/拿圖片的總次數)。最后程序的輸出如下:
在下一篇文章中會詳細分析如何實現整個流程。