前年夏天,谷歌云负责维护开发者关系的Kaz Sato带着他的儿子,用一些传感器和一个简单的机器学习线性模型,开发了一个“猜拳机器”,能检测石头剪刀布的手势。
他还根据这个过程写了一份教程,详细介绍了怎样构建这个机器,以及怎样用机器学习算法解决日常问题。
下面是教程译文,适合有一定编程基础的同学,需要大约200美元的硬件设备。
我们先来看一下这个机器:
上面视频中,我们搭建的系统正在通过手套上的传感器,借助一个用Tensorflow编写的简单机器学习算法来检测我儿子的手势,然后选择相应的选项:石头、剪刀、布。
项目源代码在此:
https://github.com/kazunori279/ml-misc/tree/master/glove-sensor
具体是怎样实现的呢?接下来我会一步一步地讲。
第1步:
制作手套传感器
我们使用littleBits来构建硬件系统。这套设备对儿童很友好,包含各种各样的组件,如LED灯、电机、开关、传感器和控制器等,这些组件可以靠磁性链接,无需焊接。在这个实验中,我们使用了三个 弯曲传感器,将它们附在塑料手套上。
△littleBits弯曲传感器
当你戴着手套、弯曲手指时,传感器会输出一个从0V到5V变化的电压信号。为传感器输出加一个指示器,比如LED光柱,就能实时看到每个传感器受到的压力。
△弯曲传感器输出0V-5V信号
第2步:
安装Arduino和伺服模块
要读取弯曲传感器的输出信号并控制机器的转动幅度,我们使用了Arduino模块和伺服模块。Arduino模块内部有一个微控制器芯片,且具有多个输入和输出端口。你可以在笔记本电脑上用Processing语言(和C语言比较像)编写一个程序并编译,然后通过USB线传输到该模块中。
△littleBits Arduino模块
△伺服模块
△我儿子在画转盘指示图
现在,用于构建猜拳机的所有硬件已经准确齐全,接下来,就该写代码了。
△猜拳机硬件部分 第3步:
写程序从弯曲传感器读取数据
在配置好硬件后,我们开始在Arduino模块上编写代码,实现从弯曲传感器读取数据的功能。在Arduino的IDE中,设定为每隔0.1秒读取传感器数据,然后将其记录在串行控制台上,代码如下。
△在Arduino IDE中编写程序
运行这段代码时,你会在控制台上看到这样的数字:
其中,每行的三个数字表示弯曲传感器输出的三个数据。Arduino模块将输入信号电压(0V - 5V)转换成从0到1023变化的数字。
上图是“石头”手势的数据,所有传感器都是弯曲的。如果换成“布”的收拾,所有传感器都不弯曲,则上图的数据都会趋近于0。
第4步:
使用Cloud Datalab可视化数据
该如何确定这三个数字的组合是代表着“石头”、“布”还是“剪刀”?
最简单的方法是编写能判断阈值和条件的IF语句。比如:
当三个输出数值都低于100时,则输出“布”; 当三个输出数值都高于400时,则输出“石头”; 若不满足以上两个条件,则输出“剪刀”。这个程序可能满足当前任务的要求,但是很不灵活也不稳定。
当然,主要是因为我比较懒,想编写出更强大和更灵活的代码,能在不改变基本设计的前提下,灵活处理善变的甲方(我儿子)可能提出的各种请求。
为了找到更好的数据处理方法,我对手套传感器数据做了一些快速的分析。我使用的工具是Cloud Datalab,这是一个很受欢迎的Jupyter Notebook版本,并已集成到Google Cloud平台,可提供基于云数据分析的一站式服务。你可以在Web UI中编写Python代码,使用如NumPy、Scikit-learning和TensorFlow等函数库,并将其与Google Cloud服务(如BigQuery、Cloud Dataflow和Cloud ML Engine)相结合。
根据不同手势,我把手套传感器数据分开保存成三个CSV文件,每个文件包含800行数据。你可以在Cloud Datalab上编写Python代码,将它们读取并转换为NumPy数组,示例代码如下:
△使用Cloud Datalab读取CSV文件转为NumPy数组
完整代码:https://github.com/kazunori279/ml-misc/blob/master/glove-sensor/Rock-paper-scissors.ipynb
你也可以使用Matplotlib库来可视化NumPy数组。下面代码画出了一个3D图,其中每个轴对应着一个不同传感器。
△用3D图绘制传感器数据,已缩放原始多维数据
通过观察上面的3D图,你可以更清楚地看到数据的空间分布。
第5步:
创建一个线性模型
接下来,我们要将这些原始传感器数据划分到三种不同手势类别中。这里会用到线性代数,你们在高中或者大学应该都学过了。
线性代数是一种可以将某个空间映射到另一空间的数学方法。例如,下面公式表示了一种从某个一维空间到另一个一维空间的线性映射。
△一元公式
其中,x和y分别为两个一维空间中的变量,w为权重,b为偏差。利用这个公式,可以将一维空间“纽约市出租车的行驶距离”映射到另一个一维空间“出租车费用”,其中把2.5美元(每英里费用)作为权重,把3.3美元(初始费用)设置为偏差。
△“行驶距离”和“出租车费用”的映射函数
从图中看出,权重和偏差(也叫参数)分别定义该直线的斜率和初始位置。你可以通过调整这些参数,来创建从一个一维空间到另一个一维空间的任何线性映射。
线性代数的优点在于,在从任意m维空间到任意n维空间进行线性映射时,可使用相同公式。例如,在将三维空间(x1,x2,x3)中的某个点映射到另一个三维空间(y1,y2,y3)中,均可使用以下公式。
△三维空间之间的映射函数
数学家认为上面公式写得太冗长了,所以设计了一种更容易的表示方法:矩阵乘法。
三维映射关系也可以这么表示:
或者,更简单写成:
其中,x和y为3维列向量,W为3×3的权重矩阵,b是3维偏置列向量。是的,它与一维空间的映射函数完全一样。而且,该公式可应用于m维空间和n维空间之间的任何线性映射,这就是所谓的“线性模型”。
那么,线性模型在本项目能起到什么作用呢?我们可以利用它,将“手套传感器数据”的3维空间转换为“石头剪刀布”的3维空间,如下所示:
△3维空间的动态转换
在完成手套传感器数据与“石头剪刀布”3维空间的配对后,很容易写出用于分类的IF语句,如下:
当石头方向值高于其他方向,则输出“石头”; 当布方向值高于其他方向,则输出“布”; 当剪刀方向值高于其他方向,则输出“剪刀”。线性模型可以将原始输入数据转换到特征空间中,在该空间中可为要捕获的每个特征设定不同方向,这样更容易处理转换后的数据。这就是为什么我认为线性代数不仅是数据科学家的奇妙数学工具,对懒惰的程序员来说也是如此。
在输入数据有多个维度或是有多个不同属性时,线性模型尤为重要。
比如,当你将几十个弯曲传感器连接到紧身衣后,则可使用线性模型将来自传感器的原始数据映射到用多个方向来表示不同身体姿势的特征空间(如站立、坐着或蹲下等),无须基于原始数据来编写很多不稳定的IF语句。
当然,线性模型还可处理非结构化或稠密数据,以提取所需的特定特征。这类数据的维度一般为数百个,甚至数千个,如图像、音频、自然语言和时间序列数据等。
但请注意,线性模型并不是万能灵药。
要在复杂的非结构化或稠密数据的分类任务中达到更高的正确率,可能要使用非线性模型,如神经网络或支持向量机。这样,你可以通过非线性变换来提取有用的特征,这种非线性变换能以一种更灵活的方式来调整原始数据。
在刚开始处理复杂数据时,你可以先尝试使用线性模型,如果不能提取满足要求的所需特征,可进一步尝试非线性模型来获得更好效果。
第6步:
让TensorFlow寻找参数
既然我们已经了解线性模型十分有用和强大,你可能想知道:
该如何确定最佳映射的参数(即权重和偏差)?
答案是:机器学习。
你可以利用机器学习,让计算机根据测得的输入数据来计算线性模型的最佳参数组合。利用TensorFlow能很容易地实现这些想法。在TensorFlow中,只需将线性模型的公式“y = Wx + b”公式定义为计算图,如下:
在上面代码中,tf.Variable创建两个初始化为0的变量,分别保存了3 x 3的权重矩阵和3维偏置列向量。此外,tf.placeholder创建一个占位符,可接收任何数目的手套传感器数据作为输入;tf.matmul是手套传感器数据和权重进行矩阵乘法的函数,并根据tf.matmul用法,将手套数据置于前者。
请注意,当你调用这些函数(TensorFlow中的低级API)时,不执行任何计算,只是建立一个计算图,如下所示:
△计算图
机器学习和TensorFlow的强大在于,可利用计算机寻找最佳参数(包括权重和偏差)。在上面例子中,我们输入了手套的三个传感器数据及其期望输出(有石头、剪刀或布)。TensorFlow可利用该数据,在图中进行反向计算,寻找最佳的权重和偏差以得到期望的线性变换。这个过程叫做“训练机器学习模型”。
利用机器学习,你只需设定输入和输出,即可利用计算机训练得到最佳的映射函数,这就跟自动编程一样。在21世纪,机器学习从某种程度上可看作是一种工程师的计算器。任何人都可以使用它来执行简单任务,以减少编码工作。
第7步:
定义一个训练“教练”
训练线性模型时,需要一个监督“教练”。我们通过以下两行代码来引导模型训练,以达到期望效果。
rps_labels是用来接收每行手套传感器数据标签的占位符,为每个手套传感器数据按照一定格式来定义标签,如下所示:
其中,[1 0 0]表示石头,[0 1 0]为布,[0 0 1]为剪刀,这叫做one-hot编码,是在训练分类模型中表示标签的一种通俗方法。
在第二行,我们调用了tf.losses.softmax_cross_entropy来定义损失函数。关于softmax、交叉熵以及损失函数的详细介绍,可参考维基百科。对于这三者,你只要了解以下内容:
Softmax能将rps_data中的数值对应压缩到区间[0, 1],这样可将其输出作为石头、布和剪刀的估计概率。 交叉熵返回两个概率分布间的差异程度:rps_labels中的one-hot标签(真实值)和softmax函数输出的估计概率。 损失函数是一个衡量模型实际准确程度的函数。因此,我们使用交叉熵作为损失函数。△“交叉熵指出了实际标签与计算概率间的差异度”
——Martin Gorner, TensorFlow and deep learning, without a PhD
在这种情况下,损失函数看作是softmax函数和交叉熵的组合体,指出当前参数在线性模型中对应的误差值。该函数是TensorFlow中的“教练”,引导模型沿着正确方向寻找最佳参数。
顺便提一下,线性模型和softmax函数的组合被叫做多元逻辑回归(multinomial logistic regression),或是softmax回归,这是在统计学和机器学习中一种常用的分类算法。
第8步:
训练线性模型
接下来,我们准备在TensorFlow中加入优化器来训练模型。
tf.train.GradientDescentOptimizer是TensorFlow中一种常用的优化器,通过梯度下降算法调整参数,来最小化损失函数返回的误差。
在实际训练中,你需要创建一个会话(Session)并将手套传感器数据和标签传给优化器。由于优化器会以指定的学习速率逐渐地更改参数值,因此可能要运行多达数千次。观察训练过程中的损失值,你可以发现它在逐渐减小,这意味着模型的错误率越来越低。
在训练结束后,你将获取一系列训练好的权重和偏差,可利用softmax概率将手套传感器数据映射到相应决策空间。在原始手套传感器空间中绘制softmax概率分布,如下所示。
△石头、布和剪刀的估计概率分布 第9步:
在Arduino上运用线性模型
我们已经得到了一种能分类手套传感器数据的实用方法,接下来完成对Arduino的编码。
在Datalab上运行sess.run(weights),可输出训练好的权重值。复制这些权重值并写入Arduino代码中,对偏置也进行以下操作。
最后,利用Arduino中的线性模型,可将手套传感器数据映射到决策空间。你可以用下面的Arduino代码来实现数据、权重和偏差间的矩阵乘法计算。
然后,比较这些值并找到最大值。一旦确定了手套表示的手势,Servo就可以正确控制机器手并赢得比赛。在这个例子中,你不需要计算出softmax值,只需比较下线性变换的三个输出值,其中这三个值分别对应着石头、布和剪刀。
到这里已经完成了,你可以使用机器学习来创建专属于你的石头布剪刀机器。
下一步计划
正如这篇文章中提到的,线性模型是一个强大的工具,可以通过线性变换将任意的m维空间映射到n维空间。如果你觉得编写多个if语句来校验复杂条件下的原始输入数据过于乏味,可以考虑这种工具。与直接处理原始数据不同,在处理能映射到特征空间中的数据时,这种方法更为简单。在这篇文章中,特征空间指的是石头、布和剪刀的决策空间。
这里用到的关键技术是机器学习和TensorFlow,在构建线性模型可帮助你找到最佳参数。这些技术不仅只是针对深度学习和人工智能的工具,也可用于为各种编程任务构建出一个强大且灵活的代码。
原文地址:https://cloud.google.com/blog/big-data/2017/10/my-summer-project-a-rock-paper-scissors-machine-built-on-tensorflow