机器学习(1) - TensorflowSharp 简单使用与KNN识别MNIST流程

机器学习是时下很是流行的话题,而Tensorflow是机器学习中最有名的工具包。TensorflowSharp是Tensorflow的C#语言表述。本文会对TensorflowSharp的使用进行一个简单的介绍。git

本文会先介绍Tensorflow的一些基本概念,而后实现一些基本操做例如数字相加等运算。而后,实现求两个点(x1,y1)和(x2,y2)的距离。最后,经过这些前置基础和一些C#代码,实现使用KNN方法识别MNIST手写数字集合(前半部分)。阅读本文绝对不须要任何机器学习基础,由于我如今也才刚刚入门,行文不许确之处不免,敬请见谅。github

本文的后半部分还在整理之中。算法

1. 什么是机器学习

用最最简单的话来讲,机器学习就是不断改进一个模型的过程,使之能够更好的描述一组数据的内在规律。假设,咱们拿到若干人的年龄(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就会十分接近咱们使用最小二乘法作出来的值,这时,就能够认为模型训练完成了。机器学习

固然,这只是机器学习最简单的一个例子,使用的模型也只是线性的直线方程。若是使用更加复杂的模型,机器学习能够作出十分强大的事情。工具

2. 环境初始化

我使用VS2017建立一个新的控制台应用,而后,使用下面的命令安装TensorflowSharp:学习

nuget install TensorFlowSharp测试

TensorflowSharp的源码地址:https://github.com/migueldeicaza/TensorFlowSharp

若是在运行时发现问题“找不到libtensorflow.dll”,则须要访问

http://ci.tensorflow.org/view/Nightly/job/nightly-libtensorflow-windows/lastSuccessfulBuild/artifact/lib_package/libtensorflow-cpu-windows-x86_64.zip

下载这个压缩包。而后,在下载的压缩包中的\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就表现为:

  1. 首先得到全部的数据
  2. 而后对一个输入的点,找到离它最近的K个点(经过L1或L2距离)
  3. 而后,对这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以后,咱们的任务就很清楚了:

  1. 得到数据
  2. 把数据处理为一种标准形式
  3. 拿出数据中的一部分(例如,5000张图片)做为KNN的训练数据,而后,再从数据中的另外一部分拿一张图片A
  4. 对这张图片A,求它和5000张训练图片的距离,并找出一张训练图片B,它是全部训练图片中,和A距离最小的那张(这意味着K=1)
  5. 此时,就认为A所表明的数字等同于B所表明的数字b
  6. 若是A的label真的是b,那么就增长一次获胜次数 

经过屡次拿图片,咱们就能够得到一个准确率(获胜的次数/拿图片的总次数)。最后程序的输出以下:

在下一篇文章中会详细分析如何实现整个流程。

相关文章
相关标签/搜索