TensorFlow入门(2):使用DNN分类器对数据进行分类

背景

上一篇《TensorFlow入门:求N元一次方程》根据官网的入门教程,使用基础的API稍作修改解决了N个数据的权重问题,再继续看官网后面的教程,有一篇高级API入门教程教我们如何使用DNN(深度神经网络)分类器实现对鸢尾花的分类。刚看到这篇文章的时候,中间出现了几种鸢尾花的图案,我还以为输入是图片,API会进行图片识别,后来发现输入的训练集只是一组组特征数据(包含花萼的长度宽度和花瓣的长度宽度)对应分类,可以看做能够解决这样的一个问题:给定一组特征数据,求这组数据的分类。

和之前一样,先分析一下原文中的示例,很多文章对原文中的示例进行翻译,但是并没有举一反三,这样其实学习效果并不好,本文会在学习后使用原文的方法,解决一个新的问题。

由于作者能力有限,目前仅停留在使用阶段,先培养机器学习思维方式,对于原理部分,可以参考其他的资料。能保证的是,阅读本文不会让你过于枯燥,也不会很难,我的宗旨是用简单的语言将复杂的问题说清楚。

原文示例

原文链接在这里,我们先逐行分析一下,首先进行必要的包含工作,我对python不是特别熟悉,前面这3行我还专门去查了一下是什么含义,具体可以参考这篇文章

然后定义训练集和测试集的路径,这次的数据是以csv的格式加载进来:

然后进入到主函数,主函数首先是将训练集和测试集的csv文件下载下来:

下载下来的文件可以打开看看,我们打开训练集:

可以发现首行的格式看起来并不是一个表头,这个格式是有规范的,但是原文没有讲,我们继续往后看它是怎么读取的:

TensorFlow使用tf.contrib.learn.datasets.base.load_csv_with_header对CSV文件进行读取,它有3个参数:

  • filename:CSV文件名
  • target_dtype:目标数据的类型,本例中为分类ID,使用整形表示
  • features_dtype:特征值的类型,本例中是花萼花瓣的长宽度,使用浮点数表示

打开load_csv_with_header的源代码,可以看到它的实现方式:

这样就很清楚了,使用load_csv_with_header函数读取的CSV文件首行前两列分别表示数据组的个数和每个数据组的特征数,训练集中一共有120组数据,每组数据包含4个特征。首行的另外3个数据,实际上并不会读取到。具体的特征数据从第二行开始,最后一列为目标值(即训练完毕后期望的输出值),前面的4列为特征数据(即训练完毕后的输入值),这个4必须和第一行第二列相等,否则就会读取失败了。

数据读取完毕后,可以把结果打印出来看看:

因为篇幅问题,上面省略了很多数据,可以看到和load_csv_with_header代码中一致,结果为一个Dataset结构,其中data为120组数据,每组数据包含4个特征值,而target为一个长度为120的数组,表示这120组数据的分类。

这样就完成了训练集和测试集的数据加载工作,之后创建一个DNN分类器:

这段代码,我目前的知识还无法理解全部参数的含义,先看看第一行real_valued_column的参数:

  • column_name填的是””,这个我还不明白有什么作用
  • dimension填4,对应每组数据有4个特征值。

DNNClassifier的参数:

  • feature_columns:把之前创建的特征列传入,具体有什么含义还没深入理解。
  • hidden_units:每层神经元数量,跟DNN原理有关。
  • n_classes:目标的类型的个数,目前是3个。
  • model_dir:训练模型保存的路径,这个很重要。

然后要构造一个输入函数,用于将训练数据输入到TensorFlow中用来训练,这个函数返回2个Tensor数据,一个是大小为[120,4]的输入数据,表示120组数据,每组数据包含4个特征值,还有就是120个输出数据,这120组数据用于训练模型。因为返回的数据是Tensor常量,直接打印会显示出他们的属性:

如果想看看他们的值,可以创建一个Session执行一下:

接下来就开始训练,使用classifier的fit函数进行训练,次数为2000次:

训练的结果会保存在之前创建classifier传入的model_dir中,本例中是”/tmp/iris_model”,这是一个目录,训练结束后,可以看到该目录保存了一些数据:

如果再执行2000次训练,会发现目录中数据量增加:

可见,训练的结果在执行完训练后,就已经保留下来了,后续对于数据的分类,可以直接使用当前的训练数据而不用重新训练:

训练结束后,通过30组测试集来对训练效果进行测试,与训练时一样,同样构建一个数据输入函数get_test_inputs,将数据和结果传入,使用classifier.evaluate对数据进行测试:

注意到例子中把classifier.evaluate返回的结果的”accuracy”字段打印出来,其实返回的结果是一个字典,可以打印出来看看是什么:

可以看到打印结果中有损失函数、训练次数、准确率和AUC信息,auc信息我还不太能理解它的具体含义,但是可以看做是评价模型效果的一个指标,有兴趣的同学可以顺手Google一下。

可以看到测试集的准确率是96.67%,总共30个测试数据,错了1个。

那么未来对于单个输入数据,我们怎么使用训练好的模型对其进行分类呢?继续看代码:

还是创建一个输入函数,把数据传入,使用classifier.predict对数据进行分类,返回值是一个生成器generator,所以用list包一下,结果为:

表示这2组数据分别被分类为1和2。

这就是我对于官方的DNN分类器示例的一些理解,希望能帮助读者学习,完整代码:

举一反三

学习了DNN分类器的用法之后,我们可以用它来做什么呢?先随便举个例子吧,给出一个坐标,输出它所在的象限,比如(1,1)的象限为1,(1,-1)的象限为4,其中比较特殊的,令在坐标轴上的数据点的象限为0,比如(0,1)和(0,0)的象限输出为0。

要完成这个测试,首先要生成训练集和测试集csv文件,使用一个gen_data函数生成数据,首行为数据组数和特征的数量,在本例中,特征数量为2。我们使用随机数生成一个坐标(x,y),它们的值限制在[-10,10)的范围内,x和y低于0.2的部分,将其置为0,用来表示坐标轴上的点:

在main函数中,判断数据文件是否存在,不存在则生成数据,其中训练集包含2000个数据,测试集包含5000个数据:

训练的内容和前面的例子几乎完全没有变化,这里我认为可以调整的参数有神经网络的层数以及每层的神经元数,这个目前我还没有经验对其进行调整:

最后传入几个测试数据,由模型对数据进行分类,这样可以直观的看到训练的效果,其中包含了几个在训练集中没有的数据,训练集中的坐标点绝对值都限制在10以内,测试中传入了坐标值为100的点,看是否能够得到正确的结果:

完整代码如下:

执行上述代码,每执行一次,程序会训练2000次,多次执行,可以逐步提高训练准确度,首次执行的结果如下:

可以看到达到了99.88%的准确度,手工传入的测试数据全部正确,可见效果确实很不错。

再多执行几次程序,提高训练的次数,loss函数值会越来越小,分类准确率越来越高。本例中,在进行14000次训练后,准确度达到了100%:

在我的机器上,执行2000次训练耗时将近8s,14000次差不多耗时1分钟,在训练完毕后,如果只是需要对数据进行分类,则耗时可以降低到0.5s左右,其中加载训练数据耗时0.22s,对数据进行分类耗时0.2s,其他则是脚本本身的开销。从这里也可以看到,DNN分类器的训练过程是比较耗时的,具体执行的过程并不算特别耗时。

学会使用DNN分类器之后,如果有一些数据,有几个输入特征值,需要将其分类,就可以采用DNN分类器很方便地对其进行处理,前提是训练的数据集数量足够,这样才能达到比较好的训练效果。
比如我能想到的一个例子是文字识别,或者验证码识别,通过对图像的特征描述,达到识别文字或者验证码的目的。特征可以是简单的文字二维点阵描述,或者复杂点,描述为文字中封闭区域,转折的数量、方向等。其他还有很多问题可以通过DNN分类器解决,了解这个工具后,遇到问题时可以想想能否用这些机器学习的工具帮忙解决问题,在使用过程中,逐步理解各种神经网络的知识,如果直接看理论,难度很大也很枯燥,在实践中学习会更加容易,记忆也更加深刻,这也是我学习TensorFlow的一个目的。

参考资料

TensorFlow入门(1):求N元一次方程

背景

今年以来,人工智能成为一个时代热点,同时TensorFlow 1.0的发布后,我也想蹭蹭时代的热点,初步学习一下神经网络和机器学习,在这里把成果以初学者的方式记录下来。

学习一个新东西,不可避免会遇到很多坑,很多教程都是一个有经验,熟悉的人写的,那样其实并不是特别接地气,因为很多坑在作者写文章的时候都忘了,这篇文章也是记录一下我掉的坑,做一个备忘的作用。

阅读这一系列文章,你需要做好以下准备,或者有以下技能:

  • 能够在你的环境中安装好Python、TensorFlow
  • 了解基本的Python语法和数据结构
  • 有基础的线性代数知识
  • 英语水平能够大概看懂TensorFlow官方入门教程

我也只是一个刚接触机器学习的初学者,所以你不用太过于担心这篇文章难度问题,我会尽量用高中生能够看懂的方式来叙述问题,希望能够帮助到大部分初学者。TensorFlow介绍性的话就不多说了,直奔主题。

解决什么问题?

这篇文章是我正式使用TensorFlow第一天写的,在这之前,我阅读了一些关于机器学习的理论知识,在阅读完官网的入门教程后,我发现TensorFlow最基础的应用可以用来拟合方程,即给出N个点(x,y),这些点符合一定规律,我们希望推导出其他符合这个规律的x对应的y值。

最简单的情况是线性的,我们希望能够使用一条直线拟合这几个点,得到方程式完整的内容,即假设y = a * x + b,我们只需要求得a和b的值就好了,在初中数学中,只需要提供2组(x,y),即可通过消元法求得a和b,这是一个很简单的数学问题。但是如果想用TensorFlow的比较通用的方式解决这个问题,就不能教机器这样解了,我们必须让机器通过不断尝试的方式,来获得a和b的值。

我们来实际操作一下,假设我们现在有一个方程y = 5 * x + 13,我们需要让机器通过一些(x,y)来推导出a = 5,b = 13。

首先要使用TensorFlow,需要import tensorflow和数学库numpy,TensorFlow的数学计算是以numpy为基础的(这点我还不是特别确认,可能也可以有其他的数学库,不过他们之间关系紧密):

我们要提供一系列已经存在的(x,y)组合,这个叫做训练集,我们先用代码生成5组训练集,先随机生成5组x的值,命名为t_x(t为训练train的意思),其中使用np.random.random([5])来生成0到1之间的随机数,将其乘以10可以获得0到10之间的随机数,最后使用np.floor函数对其进行取整,并且令数据类型为浮点数np.float32以便于计算:

结果如下,其中随机数每次执行会不一样:

然后根据公式求得t_y的值:

结果为:

这样我们就能够得到5组训练集了,可以开始使用TensorFlow求解了,关于TensorFlow的基本用法我现在也没办法讲的太清楚,可以简单参考下官网基础教程,我以我的理解,重新简单讲解一下,TensorFlow所有的执行流程会在一个Session中执行,可以把它暂时看做执行计算的一个载体。我们要在执行前,构造计算的规则,对于计算量的表示,目前只需要知道2种,一种是输入量,在TensorFlow中以占位符placeholder表示,另一种是变量,以Variable表示。我们的训练集是在计算过程中以输入表示,因此将其定义为占位符,它的类型为浮点类型tf.float32:

而需要用于输出的a和b的值,我们将其定义为变量,初始化为浮点数0.0,这个初始值并不是特别重要,因为TensorFlow在训练的过程中,会不断调整这两个值,这个后面会详细说明:

而在TensorFlow的Session内部,我们需要根据内部x,a,b的值,求得当前的y的值curr_y,因此按照线性公式使用x,a,b定义curr_y,这行语句执行时并不会进行真正的计算,只是仅仅描述他们的关系:

得到了当前的y值curr_y,我们就要和我们提供的训练集中对应的y值进行比较,使得差异最小,这个差异在机器学习中称为损失函数(Loss function),当损失函数值最低时,就可以认为找到了一个比较好的值,当然实际应用中会有一些局部最小值,这个本例不涉及到,就不讨论了。一般来说,可以使用他们的方差来描述损失函数,因为TensorFlow能够很好的支持矩阵运算,而curr_y和y都可以看做是1行5列的一个矩阵,因此损失函数可以定义为矩阵各元素之差的平方之和:

重温一下我们训练的目的,是通过不断调整变量a和b的值,来达到损失函数值最小的目的。而调整a和b的值的方法,我们采用一个叫做梯度下降(Gradient descent)的方法,简单来说,可以看做做loss = fun(a,b)形成了一个三维曲面,大概如下图所示(图来源文末参考资料):

梯度下降可以看做是一个小球,沿着曲面滚动,它距离地面的面积,就是loss函数的值,当它滚动到最低点时,也就找到了损失函数最小的位置(关于局部最小值和梯度下降更深入的内容可以参考原文)。

TensorFlow中能够很方便地定义梯度下降的训练方法以及描述求损失函数最小值的目的:

其中梯度下降的参数0.001是我调出来的,我目前并不清楚这个值如何更好的调整,太大可能找不到局部最小点,太小会导致训练过慢,也许这就是机器学习工程师有时候被戏称为“调参工程师”的原因?

至此,我们的TensorFlow描述部分已经完成了,可以开始进入执行流程了,首先,我们要创建一个Session用于执行对于变量,我们需要进行初始化操作,内部会对变量进行内存的分配操作,这个内存会在Session关闭时被释放:

然后我们可以对数据开始训练,第一个参数是训练的内容train,第二个参数是指定变量x和y对应的实际值:

一般,训练次数和准确度是有关系的,我通过“调参”,确定训练10000次,在每次训练后,把当前的a,b和损失函数loss的值打印出来,需要注意的是,TensorFlow中的值需要在sess.run中执行才能看到结果,如果需要得到多个值,可以将其放到一个数组[]中,因此打印a,b和loss的值需要放到sess.run中执行,同时也要将t_x和t_y传入:

完整的Python代码如下:

好了,现在可以执行一下,如果环境没有问题的话,你应该能看到屏幕上不断出现的值,我稍微截取头尾一部分看看:

在刚开始执行的时候,代码打印出训练集t_x和t_y的值,然后开始进行训练,a和b的值快速增长,损失函数也在不断减少,最后a的值停留在3.0000157,b的值停留在7.9999132,损失函数则为1.0950316e-08,可见与结果a=3,b=8已经非常接近了,如果要更加接近结果,可以尝试降低梯度下降学习速率参数。这样就达到了求a和b的值的目的。

再深入一点:多元一次方程

上面的例子如果能完成,结合官网的资料和其他博主的资料,我相信你已经算入了个门了,后面能不能通过修改上面的例子进行解决更加复杂的问题呢?再看看下一个问题,如果有一个值,它受到N个参数的影响,但是每个参数的权重我们并不清楚,我们希望能用刚刚学到的TensorFlow来解决这个问题。

首先建立一个模型,表示N组数据,具体点,先实现5个变量的求解,生成10个数据集,我们可以很容易联想到使用大小为[10,5]的矩阵表示t_x,使用大小为[5,1]的矩阵表示参数权重t_w,使用大小为[10,1]的矩阵表示结果t_y,即t_y = t_x * t_w。

当然,为了更加通用,变量的数量和数据集的数量可以使用常量来表示,矩阵的向量乘法在numpy库中使用dot函数实现:

与上面的例子一样,我们以TensorFlow占位符形式定义输入训练集x和y,矩阵大小可以使用shape参数来定义:

以TensorFlow变量形式定义结果w:

定义TensorFlow计算结果y、损失函数loss和训练方法:

针对训练次数的问题,我们可以优化一下之前的方式,设定当loss函数值低于一定值或者不再变化的时候停止,因为loss函数需要在Session中使用,它需要使用TensorFlow的常量表示:

好了,模型已经建立完毕,开始训练,我们使用变量run_count来记录训练的次数,以last_loss记录上一次训练的损失函数的值,初始值为0。

训练主循环,将当前的loss函数值保存在curr_loss中,与上一次相比,如果相同,则退出训练,另外如果loss函数低于设定的精度LOSS_MIN_VALUE,也会退出训练:

最后打印结果,由于我们知道t_w的值是整数,因此将得到的结果四舍五入的值fix_w也打印出来,再看看fix_w与t_w的差距fix_w_loss是多少:

完整代码如下:

运行一下,仍然把头尾的部分记录下来,中间部分太多就省略掉:

可见,这次在执行了2828次之后,loss函数从3.30516e+13降低到6637.81后不再变动,看起来有点大,但是实际上我们的y值也是非常大的,最后求得的结果与实际值有大约不到千分之一的差距,要缩小这个差距,可以通过减少梯度下降学习速率,同时增加训练次数来解决,而fix_w的值已经等于t_w的值了。

目前这个代码也可以修改一下训练集的数量以及变量的数量,然后通过调梯度下降学习速率参数来进行训练,如果学习速率过大,可能就会得到loss函数为inf值,这样就无法得到结果了,具体原因我还得继续研究一下。

还能做什么呢?

能够解决这样的问题,基本上就能够初步使用机器学习的思维来解决问题了,比如预测股票下个交易日涨跌幅,参数可以是昨日开盘价,昨日收盘价,昨日涨跌幅,昨日成交量等。不过先别激动,股票的模型也不是简单的线性模型,如果想建立股票预测模型,还需要使用更加复杂的方法才行,有兴趣的读者可以继续深入研究,比如使用多元多次方程来进行数据的拟合,只要建立起这个思想,这篇文章的目的就达到了。

参考资料

Go语言学习笔记(3):函数

 

Go语言学习笔记(2):流程控制

 

Go语言学习笔记(1):变量