• 机器学习(1)


    机器学习是时下非常流行的话题,而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”,则需要访问

    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,那么就增加一次获胜次数 

    通过多次拿图片,我们就可以获得一个准确率(获胜的次数/拿图片的总次数)。最后程序的输出如下:

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

  • 相关阅读:
    Android入门第六篇之ListView
    谷歌Volley网络框架讲解——第一篇
    Android网络通信库Volley简介
    Android网络通信框架Volley的学习笔记
    SharedPreferences介绍
    Android之Adapter用法总结
    ANDROID SQLITEOPENHELPER详解
    RxJava、RxBus学习
    【原创】【Andriod】自定义多行多列视图
    [转]android笔记--Intent和IntentFilter详解
  • 原文地址:https://www.cnblogs.com/haoyifei/p/8654743.html
Copyright © 2020-2023  润新知