您的位置:首页 > 理论基础 > 计算机网络

神经网络进阶(连载4)鼠标手势的识别

2017-01-24 09:41 429 查看
游戏编程中的人工智能技术

  


   

 (连载之四)

鼠标手势的识别


9.2 鼠标手势的识别(RecognizeIt-MouseGesture Recognition )

     
 设想你正在玩一种实时战争游戏,你必须把有关队伍攻击和防守的各种队列形式记忆下来。你不希望从键盘上敲击大量的代码来命令你的士兵排列成各种各样的队列形式,而是利用鼠标做的各种手势来进行指挥。你做一“V”形手势,你的士兵就立刻排成“V”形队伍;几分种后他们可能会受到威胁,这时你做一个方框形的手势,他们的剑与盾都向外,排成了一个方形队列。当你再用鼠标连扫2次,士兵们马上拆开成2个队列,…。

 
 注:

    如果你心急,想在进一步阅读更多内容之以前,去试一下演示程序,则你可以在光盘的Chapter9/Executable/RecognizeIt 1.0文件夹找到可执行文件。

    程序启动后就自动开始进入训练。你必须等待,直到网络训练结束再开始输入手势。为了输入一个手势,你必须按下鼠标右键,并在压下右键的状态下移动鼠标,然后释放鼠标按钮。

    图9.7是所有预定义的手势。如果网络能识别你的手势,则手势的名称将会在窗口左上角的蓝色横条中显示出来。如果网络不能确定,则它将会有一个猜测。

    在本书所附光盘上你还能找到RecognizeIt的其它的改进版本,在本章的后面我们也要介绍一些其它的方法。

 

     
这样的工作可以通过神经网络识别用户利用鼠标所作的各种手势的训练来达到,从而不必像普通游戏那样,需要有一个记录一切操作的“点击集合”。另外,也很容易让用户自己来定义自己的手势,而不需要束缚在利用内建的手势。嘿嘿,酷吧?让我告诉你这是怎么做的...

为了解决这个问题,我们必须:

    l. 寻找一种手势的表示方法,使他们能成为神经网络的一种输入数据。

    2. 利用上一步表示的方法,对某些预定义的手势来训练神经的网络。

    3. 指出一种了解用户何时在做手势以及怎样记录手势的方法。

    4. 指出一种将原始记录的鼠标数据转换成神经网络能识别的数据的方法。

    5. 使用户能够加进自己的手势。

 


9.2.1 用向量来表示一个手势(Representing a Gesture with Vectors)

   
   第一个工作是寻找一种能把鼠标手势数据输入神经网络的方法。为此可以有几种不同的方法来实现。但我所选择的方法是把鼠标路径表示成一系列(共12个)的向量。图9.5说明了如何将一个“向右”的鼠标手势划分为一系列的向量。



图9.5  把手势看作一系列的向量

   
  为了帮助训练,这些向量在转成训练集的一部分之前,需要规格化。所有进入网络的数据,要和上例那样,预先被标准化。这也给出了附加的优点,当我们处理用户所做的手势时,匀称散布的向量来表示手势模式,有助于人工神经网络的识别过程。

    神经网络的输出个数,是和要识别模式数目相同的。例如,如果我们预先定义的手势只有向左、向右、向上、向下四种,如图9.3那样,那么神经网络将有24个输入(代表12向量) 和4个输出。

    为这些模式设置的训练集如表9.3所示。

    数字9.6  向右、向左、向下、向上四个手势


 

表9.3  学习向右、向左、向下、向上手势的训练集
手 势
输 入 数 据
输出数据
向 右
(1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0)
(1,0,0,0)
向 左
(-1,0,-1,0,-1,0,-1,0,-1,0,-1,0,-1,0,-1,0,-1,0,-1,0,-1,0,-1,0)

(0,1,0,0)
向 下
( (0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1)
(0,0,1,0)
向 上
(0,-1,0,-1,0,-1,0,-1,0,-1,0,-1,0,-1,0,-1,0,-1,0,-1,0,-1,0,-1)

(0,0,0,1)
由表可以看出,如果用户做一个向右的手势,神经网络应该从其第一个输出神经细胞输出一个1,其余神经细胞均输出0。如果用户做一个向下的手势,则神经网络应从其第三个输出神经细胞输出一个1,其余神经细胞均输出0。但实际上,这样“干净”的输出类型是很罕见的,因为由用户得到得数据总是每次都有些不同的。即使是重复做一个简单的向右手势,用手工来画也几乎每次都不可能拉成一根完美的直线!因此,网络为了思索送给它的是什么模式,它将扫查所有的输出,被把具有最高分数的输出当作最有可能的候选者。如果某个神经细胞分数最高,但只有0.8这样的分,则很可能实际的手势并不是网络认为的那个手势。但如果输出分数在0.96以上(这是在代码工程作为缺省值MATCH_TOLERANCE定义的)
,则很有可能就是网络认为的那个手势了。

程序所有的训练数据封装在一个被称CData的类中。这个类根据预定义模式(作为常数,定义在CData.cpp的开始处)创建一个训练集,如果用户要添加自定义手势,则也由它来处理训练集的改变。我不准备在这里列出Gdata的源码,如果读者希望进一步了解该类是怎样来创建训练集的,可以自己去查看光盘上的源代码。你可以在光盘上的Chapter9/RecognizeIt1.0目录下找到为手势识别的第一个尝试所提供全部源码。

 


9.2.2 训练网络(Training the Network)

你现在已经知道了怎么用一系列向量来代表一个手势,并已经创建了一训练集,但这仅仅是训练网络是一块蛋糕。训练集将要传递给CNeuralNet::train方法,而后者则要反复调用backprop算法训练数据,直到SSE(误差平方的总和)小于用#define语句定义的误差阀值ERROR_THRESHOLD(缺省值是0.003)时为止。其代码形式为:

 

bool CNeuralNet::Train(CData* data, HWND hwnd)

{

    vector<vector<double> > SetIn  = data->GetInputSet();

    vector<vector<double> > SetOut = data->GetOutputSet();

 

//首先保证训练集为有效

     If((SetIn.size()     != SetOut.size())||

        (SetIn[0].size() != m_iNuminputs)||

        (SetOut[0].size() != m_iNumOutputs))

     {

       MessageBox(NULL, "Inputs !=Outputs", "Error", NULL);

 

       return false;

     }

 

//将所有权重初始化到随机的小的数值

InitializeNetwork();

 

     //利用backprop进行训练,直到SSE小于用户定义的阀值

    while( m_dErrorSum > ERROR_THRESHOLD )

{

 

//如果有任何问题,返回false

        If ( !NetworkTrainingEpoch(SetIn, SetOut))

        {

          return false;

}

 

       //调用显示程序来显示误差总和

        InvalidateRect(hwnd, NULL, TRUE);

        UpdateWindow(hwnd);

     }

    m_bTrained = true;

    return true;

}

 

当你把源程序装进你自己的编辑器后,你必须设置好学习率才能开始游玩。学习率的缺省值是0.5。你会发现,较低的学习率会使学习过程变慢,但几乎总能保证收敛。较大数值的学习率虽能加快学习进程,但有可能使网络跌进一个局部最小。或者甚至更坏,使网络根本不能收敛。因此,如同你在本书遇到过的许多其它参数一样,我们宁可花些时间以求获得正确的平衡是值得的。

 

图9.7显示了当你运行本程序时网络所学习的各种预定义手势。



图9.7 预定义的各种手势

 


9.2.3记录并变换鼠标数据

      (Recording and Transforming theMouse Data)

为了做一个手势,用户按住鼠标右键,再移动鼠标来画出一个模式。当用户放开右鼠标按钮时,手势就完成。手势是用一系列点的方式直接记录在std::vector中。点的结构POINTS是在windef.h中定义的,其形式为:

 

typedef struct tagPOINTS {

    SHORT x;

    SHORT y;

 }POINTS;

 

但不幸的是,这一个向量可以有大大小小各种非常不同的尺寸,它完全取决于用户按住鼠标按钮的时间长短。这是一个问题,因为送入神经网络的输入个数必须固定。所以,我们需要寻找一种方法来减少原始输入的点数,使它们达到预先规定的数目。当我们完成这一工作时,我们同时还要对鼠标路径数据进行某种意义的“光滑”,以消除用户在做手势时由于各种不规则动作而引起的数据毛病。这样做能帮助用户制作出更为一致的手势。

我们前面已谈到了,例子程序使用的网络共有代表12向量的24个输入。而为了制作12向量,你必须要有13个点(见图9.5)。因此,原始的鼠标数据必须用某种方法进行压缩,使它减少成为13个点。我在程序中采用的方法,是通过对所有点的循环,找出具有最小跨度(span,也就是2点间的距离,译注)的那一对邻点,然后在这最短跨度中间插入一个新点,再删去跨度的2个端点。显然,这一过程一次只能减少1个点。所以过程必须重复进行,直到剩下的点数达到需要的个数时为止。

能完成这一工作的代码可以在CController类中找到,其形式为:

 

boolCController::Smooth()

{

    //保证我们工作的点达到足够数目:

    If(m_vecPath.size() < m_iNumSmoothPoints)

     {

     //返回

    return false;

     }

    

    //复制原始未加工的鼠标数据

m_vecSmoothPath = m_vecPath;

 

//当点数过多时,通过对所有点的循环,找出最小跨度,在它原有位置中间

//创建一个新点,删除原有的2个点。

   while (m_vecSmoothPath.size() > m_iNumSmoothPoints)

    {

      double ShortestSofar = 99999999;

      int PointMarker = 0;

 

//计算最短跨度(即相邻2点间的距离)

      for(intSpanFront = 2; SpanFront< m_vecSmoothPath.size()-1;

++SpanFront)

      {

//在这些点之间计算距离

       doublelength = sqrt(( m_vecSmoothPath[SpanFront-1].x -

  m_vecSmoothPath[SpanFront].x)*

                          ( m_vecSmoothPath[SpanFront-1].x–

m_vecSmoothPath[SpanFront].x)+

( m_vecSmoothPath[SpanFront-1].y–

m_vecSmoothPath[SpanFront].y)*

                                ( m_vecSmoothPath[SpanFront-1].y–

m_vecSmoothPath[SpanFront].y));

 

       If (length < ShortestSoFar)

       {

          ShortestSoFar= length;

          PointMarker= SpanFront;

       }

      }

//最短跨度现已找到。下面是计算跨度的中点,作为新点的插入位置,并删除跨度

//原来的2个端点

      POINTS newPoint;

      newPoint.x =(m_vecSmoothPath[PointMarker-1].x +

                      m_vecSmoothPath[PointMarker].x)/2;

      newPolnt.y =(m_vecSmoothPath[PolntMarker-l].y +

                      m_vecSmoothPath[PointMarker].y)/2;

      m_vecSmoothPath[PointNarker-1] =newPoint;

     m_vecSmoothPath.erase(m_vecSmoothPath.begin() + PointMarker);

   }

  return true;

}

 

 这种点数削减方法并不完美,因为它没有把手势的形状特征考虑进去,例如,没有考虑路径是直的还是带有转弯角度的。因此,当你画一个带有角度的手势时,在经过光滑处理后的鼠标路径中,角度就变成圆滑了。但这种算法的优点是速度快,而为了成功地识别模式,利用光滑处理后所保留的信息作为输入数据来训练神经网络是足够的。


9.2.4 增加新手势(Adding New Gestures)

本程序也允许用户定义自己的手势。只要定义的手势唯一,这一步并不难,但因涉及一个如何在训练集中加入数据的重要问题,所以我要在这里多写一两段。如果你已经训练了一个神经网络,而要求这个网络去学习一个其它的模式,如果你仅仅为新加的一个模式再次去试用backprop算法,通常不是一个好的想法。当你需要增加新模式时,你首先把数据加入到原先已经存在的训练集中,然后删去已训练的网络权重数据,再重新开始对整个训练集进行训练就行了。

当用户需要增加一个新的手势时,首先需要在键盘上按一个L键,然后就和通常一样用鼠标去做一个手势。这时程序就会向用户提问他(或她)对输入的手势是否满意。如果用户满意,程序就开始对手势数据进行光滑处理,再把结果加到当前的训练集,并从头开始对网络进行训练。

 


9.2.5 控制器类 

你还可以对程序进行一些改进,但在我继续论述这一工作之前,让我先来为你列出CController类的头文件。与往常一样,CController类是一个和所有其它类有联系的类。所有的处理方法、变换方法、测试鼠标数据的方法等,都能在这里找到。

 

class CController

{

 

private:

 

//该神经网络

CNeuralNet*  m_pNet;

 

    //保持所有训练数据的类

CData*            m_pData;

 

//用户鼠标手势路径,包括原始的和光滑处理后的

vector<POINTS>    m_vecPath;

 

vector<POINTS>    m_vecSmoothPath;

 

//把光滑处理后的路径转变为向量

vector<double>   m_vecVectors;

 

//如果用户正在作手势时为真

bool              m_bDrawing;

 

    //网络产生的最大输出。这是一个最有可能的匹配手势候选者。

double            m_dHighestOutput;

 

//根据最大输出m_dHighestOutput确定手势的最好匹配

int               m_iBestMatch;

 

    //如果网络找到了一个pattern(模式),那么这就是匹配者

int               m_iMatch;

 

//原始鼠标数据光滑处理时需要达到这个点数

int               m_iNumSmoothPoints;

 

//数据库中pattern(模式)的数目

int               m_iNumValidPatterns;

 

    //程序的当前状态

   mode              m_Mode;

    

程序可以处在下面的四种状态之一: 

 o TRAINING状态:网络正处在训练时代;

 o ACTIVE状态:网络已训练好,程序准备去识别手势;

 o UNREADY状态:网络未受训练的状态;

 o LEARNING状态:用户正在输入一个自定义手势时的状态。

 

     //应用程序的本地复制版

      HNND         m_hwnd;

 

     //清除鼠标数据向量

      void         Clear();

 

     //给定一系列点之后,这种方法创建一条规范化的向量的路径

       void         CreateVectors();

     //把鼠标数据预处理为固定数目的点数

       bool         Smooth();

 

     //通过对神经网络的查询为一预学习手势检测一个匹配者

    bool         TestForMatch();

    

     //对话框过程。当用户打入一个新手势时弹出一个对话框。

      static BOOL CALLBACK DialogProc(HUND           hwnd,

                                                                   UINT             msg,

                                                                  WPARAM     wParam,

                                                                  LPARAMl      Param );

 

     //这一临时变量用来保存任何新创建模式的名称

      static stringm_sPatternName;

 

       public:

 

       CController(HNND hwnd);

    ~CController();

    

     //调用此函数,用backprop算法、为当前数据集训练网络

       bool TrainNetwork();

 

     //画出鼠标手势的图形并显示相关数据,包括训练达到的代数和现有的误差

      void Render(HDC &surface, intcxClient, int cyClient);

 

     //返回鼠标当前是否处于drawing状态

    bool Drawing()const(return m_bDrawing;}

    

      //下面这个Drawing函数是当鼠标右键按下或释放时都要调用的一个函数。

      //如果其中的第一个参数val为true,则标明鼠标右键已按下。鼠标原有数据

      //均要被清除,为接受下一个手势做好了准备;

 

      //如果val为false,则手势已完成。这时手势或者就被添加到当前数据集,

      //或者测试它是否与已经存在的某个模式匹配。

 

      //第2个参数hInstance用来创建一个对话框,它是主应用程序main 的一个

      //子窗体。对话框用于获得用户打入的手势名称

        bool Drawing(bool val, HINSTANCEhInstance);

     //清除屏幕,把应用程序设置为learning 模式,准备接受用户定义的手势

       void LearningMode();

 

     //调用本函数把一个点加入鼠标路径

     void AddPoint(POINTS p)

     {

        m_vecPath.push_back(p);

     }

};

 



内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: