機器學習是時下很是流行的話題,而Tensorflow是機器學習中最有名的工具包。TensorflowSharp是Tensorflow的C#語言表述。本文會對TensorflowSharp的使用進行一個簡單的介紹。git
本文會先介紹Tensorflow的一些基本概念,而後實現一些基本操做例如數字相加等運算。而後,實現求兩個點(x1,y1)和(x2,y2)的距離。最後,經過這些前置基礎和一些C#代碼,實現使用KNN方法識別MNIST手寫數字集合(前半部分)。閱讀本文絕對不須要任何機器學習基礎,由於我如今也纔剛剛入門,行文不許確之處不免,敬請見諒。github
本文的後半部分還在整理之中。算法
用最最簡單的話來講,機器學習就是不斷改進一個模型的過程,使之能夠更好的描述一組數據的內在規律。假設,咱們拿到若干人的年齡(a1,a2,a3…)和他們的工資(b1,b2,b3…),此時,咱們就能夠將這些點畫在一個二維直角座標系中,包括(a1,b1),(a2,b2)等等。這些就稱爲輸入或訓練數據。windows
咱們能夠用數學的最小二乘法擬合一條直線,這樣就能夠獲得最好的能夠描述這些數據的規律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就會十分接近咱們使用最小二乘法作出來的值,這時,就能夠認爲模型訓練完成了。機器學習
固然,這只是機器學習最簡單的一個例子,使用的模型也只是線性的直線方程。若是使用更加複雜的模型,機器學習能夠作出十分強大的事情。工具
我使用VS2017建立一個新的控制檯應用,而後,使用下面的命令安裝TensorflowSharp:學習
nuget install TensorFlowSharp測試
TensorflowSharp的源碼地址:https://github.com/migueldeicaza/TensorFlowSharp
若是在運行時發現問題「找不到libtensorflow.dll」,則須要訪問
下載這個壓縮包。而後,在下載的壓縮包中的\lib中找到tensorflow.dll,將它更名爲libtensorflow.dll,並在你的工程中引用它。
這樣一來,環境初始化就完成了。
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來運行圖,才能獲得結果。在運行圖時,須要爲全部的變量和佔位符賦值,不然就會報錯。
Const:常量,這很好理解。它們在定義時就必須被賦值,並且值永遠沒法被改變。
Placeholder:佔位符。這是一個在定義時不須要賦值,但在使用以前必須賦值(feed)的變量,一般用做訓練數據。
Variable:變量,它和佔位符的不一樣是它在定義時須要賦值,並且它的數值是能夠在圖的計算過程當中隨時改變的。所以,佔位符一般用做圖的輸入(即訓練數據),而變量用做圖中能夠被「訓練」或「學習」的那些tensor,例如y=ax+b中的a和b。
下面的代碼演示了常量的使用:
//基礎常量運算,演示了常量的使用 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]); }; }
求兩點距離實際上就是若干操做的結合而已。咱們知道,(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。
K最近鄰(k-Nearest Neighbor,KNN)分類算法,是一個理論上比較成熟的方法,也是最簡單的機器學習算法之一。該方法的思路是:若是一個樣本在特徵空間中的k個最類似(即特徵空間中最鄰近)的樣本中的大多數屬於某一個類別,則認爲該樣本也屬於這個類別。
圖中,綠色圓要被決定賦予哪一個類,是紅色三角形仍是藍色四方形?若是K=3,因爲紅色三角形所佔比例爲2/3,綠色圓將被賦予紅色三角形那個類,若是K=5,因爲藍色四方形比例爲3/5,所以綠色圓被賦予藍色四方形類。
在進行計算時,KNN就表現爲:
對MNIST數據的KNN識別,在讀入若干個輸入數據(和表明的數字)以後,逐個讀入測試數據。對每一個測試數據,找到離他最近的K個輸入數據(和表明的數字),找出最多的表明數字A。此時,測試數據就被認爲表明數字A。所以,使用KNN識別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以後,咱們的任務就很清楚了:
經過屢次拿圖片,咱們就能夠得到一個準確率(獲勝的次數/拿圖片的總次數)。最後程序的輸出以下:
在下一篇文章中會詳細分析如何實現整個流程。