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