精华内容
下载资源
问答
  • 一文读懂麦克纳姆全向移动原理及剖析

    万次阅读 多人点赞 2020-07-09 14:57:32
    参考文章,计算过程小白可能看不懂,于是做进一步补充,写出该文 ...为了实现全向移动,一般机器人会使用「全向」(Omni Wheel)或「麦克纳姆」(Mecanum Wheel)这两种特殊轮子。 全向: .

    扣扣技术交流群:460189483

    参考文章如下,计算过程小白可能看不懂,于是做进一步补充,写出该文

    https://zhuanlan.zhihu.com/p/20282234?utm_source=qq&utm_medium=social


    什么是麦克纳姆轮
    在竞赛机器人和特殊工种机器人中,全向移动经常是一个必需的功能。「全向移动」意味着可以在平面内做出任意方向平移同时自转的动作。为了实现全向移动,一般机器人会使用「全向轮」(Omni Wheel)或「麦克纳姆轮」(Mecanum Wheel)这两种特殊轮子。

    全向轮:

     

    麦克纳姆轮

     

    全向轮与麦克纳姆轮的共同点在于他们都由两大部分组成:轮毂和辊子(roller)。轮毂是整个轮子的主体支架,辊子则是安装在轮毂上的鼓状物。全向轮的轮毂轴与辊子转轴相互垂直,而麦克纳姆轮的轮毂轴与辊子转轴呈 45° 角。理论上,这个夹角可以是任意值,根据不同的夹角可以制作出不同的轮子,但最常用的还是这两种。

    全向轮与麦克纳姆轮(以下简称「麦轮」)在结构、力学特性、运动学特性上都有差异,其本质原因是轮毂轴与辊子转轴的角度不同。经过分析,二者的运动学和力学特性区别可以通过以下表格来体现。

     

    计算过程如下,供参考,学霸可点开大图验算:

     

    近年来,麦轮的应用逐渐增多,特别是在 Robocon、FRC 等机器人赛事上。这是因为麦克纳姆轮可以像传统轮子一样,安装在相互平行的轴上。而若想使用全向轮完成类似的功能,几个轮毂轴之间的角度就必须是 60°,90° 或 120° 等角度,这样的角度生产和制造起来比较麻烦。所以许多工业全向移动平台都是使用麦克纳姆轮而不是全向轮,比如这个国产的叉车: 全向移动平台 麦克纳姆轮叉车 美科斯叉车

    另外一个原因,可能是麦轮的造型比全向轮要酷炫得多,看起来有一种不明觉厉的感觉……

    的确,第一次看到麦轮运转起来,不少人都会惊叹。以下视频直观地说明了麦轮底盘在平移和旋转时的轮子旋转方向。

    麦克纳姆轮工作原理

    【物理篇-力学专题】E03 S1小车为什么横着走?~滚动摩擦与麦克纳姆轮

     

    【初中-物理】E08 速度解算?绕圆运动?麦克纳姆轮-进阶

    麦轮的安装方法

    麦轮一般是四个一组使用,两个左旋轮,两个右旋轮。左旋轮和右旋轮呈手性对称,区别如下图。

     

    安装方式有多种,主要分为:X-正方形(X-square)、X-长方形(X-rectangle)、O-正方形(O-square)、O-长方形(O-rectangle)。其中 X 和 O 表示的是与四个轮子地面接触的辊子所形成的图形;正方形与长方形指的是四个轮子与地面接触点所围成的形状。

    • X-正方形:轮子转动产生的力矩会经过同一个点,所以 yaw 轴无法主动旋转,也无法主动保持 yaw 轴的角度。一般几乎不会使用这种安装方式。
    • X-长方形:轮子转动可以产生 yaw 轴转动力矩,但转动力矩的力臂一般会比较短。这种安装方式也不多见。
    • O-正方形:四个轮子位于正方形的四个顶点,平移和旋转都没有任何问题。受限于机器人底盘的形状、尺寸等因素,这种安装方式虽然理想,但可遇而不可求。
    • O-长方形:轮子转动可以产生 yaw 轴转动力矩,而且转动力矩的力臂也比较长。是最常见的安装方式。

      麦轮底盘的正逆运动学模型

      以O-长方形的安装方式为例,四个轮子的着地点形成一个矩形。正运动学模型(forward kinematic model)将得到一系列公式,让我们可以通过四个轮子的速度,计算出底盘的运动状态;而逆运动学模型(inverse kinematic model)得到的公式则是可以根据底盘的运动状态解算出四个轮子的速度。需要注意的是,底盘的运动可以用三个独立变量来描述:X轴平动、Y轴平动、yaw 轴自转;而四个麦轮的速度也是由四个独立的电机提供的。所以四个麦轮的合理速度是存在某种约束关系的,逆运动学可以得到唯一解,而正运动学中不符合这个约束关系的方程将无解。

      先试图构建逆运动学模型,由于麦轮底盘的数学模型比较复杂,我们在此分四步进行:

      ①将底盘的运动分解为三个独立变量来描述;

      ②根据第一步的结果,计算出每个轮子轴心位置的速度;

      ③根据第二步的结果,计算出每个轮子与地面接触的辊子的速度;

      ④根据第三部的结果,计算出轮子的真实转速。

       

      一、底盘运动的分解

      我们知道,刚体在平面内的运动可以分解为三个独立分量:X轴平动、Y轴平动、yaw 轴自转。如下图所示,底盘的运动也可以分解为三个量:

           V_{tx} 表示 X 轴运动的速度,即左右方向,定义向右为正,这是人为设置的底盘水平方向的速度,是已知量;

           V_{ty} 表示 Y 轴运动的速度,即前后方向,定义向前为正,这是人为设置的底盘垂直方向的速度,是已知量;

           \underset{W}{\rightarrow} 表示 Z轴自转的角速度,定义逆时针为正,这是人为设置的底盘原地旋转的速度,是已知量;

           以上三个量一般都视为四个轮子的几何中心(矩形的对角线交点)的速度,也是底盘控制时设置的已知量

     

    •                                                          

      二、计算出轮子轴心位置的速度(以右上角轮子为例)

    • 定义:

          \underset{V}{\rightarrow}_{t} 表示几何中心的合速度(不包括W)向量,由已知量 V_{tx}和 V_{ty}组成的向量合速度,不一定与\underset{r}{\rightarrow}在一条线上;

          \underset{r}{\rightarrow} 为从几何中心指向轮子轴心的矢量;

          \underset{V}{\rightarrow} 为轮子轴心的运动速度矢量;

          \underset{V}{\rightarrow}_{r}为轮子轴心沿垂直于  \underset{r}{\rightarrow} 的方向(即切线方向)的速度分量;

        如下图所示: (手动绘制部分不喜勿喷,怕个别小白不理解,专门增加该部分讲解)

                       

    那么可以计算出轮子中心速度: \underset{V}{\rightarrow} =  \underset{V}{\rightarrow}_{t} + \underset{V}{\rightarrow}_{r} =  \underset{V}{\rightarrow}_{t} +  \underset{W}{\rightarrow} x \underset{r}{\rightarrow}

    分别计算 几何中心X、Y 轴的分量为:

    V_{x}  =  V_{tx} - V_{rx}      

    V_{y} =  V_{ty} + V_{ry}

    其中V_{rx} 、V_{ry}表示为\underset{V}{\rightarrow}_{r}在X轴、Y轴的速度数值分量,V_{r}表示\underset{V}{\rightarrow}_{r}的值大小,不是向量

    r_{x} 与 r_{y}表示 r在 X轴、Y轴的投影长度,也就是两个轮子之间的水平距离(宽度)一半、垂直距离(长度)的一半

    V_{rx} = V_{r} X cosθ  = V_{r} X \frac{r_{y}}{r} = W x r X \frac{r_{y}}{r}W*r_{y}

    V_{ry} = V_{r} x sinθ = V_{r} x \frac{r_{x}}{r}w x r x\frac{r_{x}}{r}W*r_{x}

    所以可求得几何中心X、Y轴速度数值(非向量)分别为:

    V_{x}  =  V_{tx} - V_{rx}     =  V_{tx} - W*r_{y}

    V_{y} =  V_{ty} + V_{ry} = V_{ty} + W*r_{x}

    同理可以算出其他三个轮子轴心的速度。

                                   

    三、计算辊子的速度

    根据轮子轴心的速度,可以分解出沿辊子方向的速度 \underset{V_{||}}{\rightarrow}和垂直于辊子方向的速度\underset{V_{\perp }}{\rightarrow}。其中 \underset{V_{\perp }}{\rightarrow}是可以无视的(思考题:为什么垂直方向的速度可以无视?),而

    V_{||} = \underset{V}{\rightarrow} * \hat{u}   此处表示向量\underset{V}{\rightarrow}点乘向量\hat{u},其中 \hat{u} 是沿辊子方向的单位向量。这一步表示向量\underset{V}{\rightarrow}在向量\hat{u}上的投影,是数值大小,不是向量噢,这样就可以求得沿辊子方向的速度数值大小V_{||}。例如向量a*b表示向量b在向量a上的投影a_{0},是一个长度单位,不是向量

    向量几何知识了解参考 https://www.jianshu.com/p/6b37baa326ec

    \underset{V}{\rightarrow} = \underset{V_{x}}{\rightarrow}  +  \underset{V_{y}}{\rightarrow}  ,其中\underset{V_{x}}{\rightarrow}\underset{V_{y}}{\rightarrow}表示V_{x}V_{y}的向量,上面我们求出来的是V_{x}V_{y}的数值,那向量如何表示呢?

    可以通过膜*单位方向向量实现,如下图所示,设X轴单位方向向量\underset{i}{\rightarrow},坐标就是(1,0),Y轴单位方向向量\underset{j}{\rightarrow},坐标就是(0,1),那么V_{x}的方向向量就可以表示为V_{x}*\underset{i}{\rightarrow}V_{y}的方向向量就可以表示为V_{y}*\underset{j}{\rightarrow},且\underset{i}{\rightarrow}*\underset{i}{\rightarrow}=1,\underset{j}{\rightarrow}*\underset{j}{\rightarrow}=1,原因是两个向量点乘结果是数值,不是向量噢

    \hat{u}是单位向量,可以表示为(-\frac{1}{\sqrt{2}}\frac{1}{\sqrt{2}}),或者\hat{u} = -\frac{1}{\sqrt{2}}*\underset{i}{\rightarrow} + \frac{1}{\sqrt{2}}*\underset{j}{\rightarrow}

    V_{||} = \underset{V}{\rightarrow} * \hat{u} =(V_{x}*\underset{i}{\rightarrow} + V_{y}*\underset{j}{\rightarrow})*(-\frac{1}{\sqrt{2}}*\underset{i}{\rightarrow} + \frac{1}{\sqrt{2}}*\underset{j}{\rightarrow}

    V_{||} = -\frac{1}{\sqrt{2}}*V_{x}*\underset{i}{\rightarrow}*\underset{i}{\rightarrow} +  \frac{1}{\sqrt{2}}V_{y}*\underset{j}{\rightarrow}*\underset{j}{\rightarrow} =  -\frac{1}{\sqrt{2}}*V_{x}  +  \frac{1}{\sqrt{2}}V_{y}

    所以最终求得沿辊子方向的速度数值大小V_{||} =  -\frac{1}{\sqrt{2}}*V_{x}  +  \frac{1}{\sqrt{2}}V_{y}

                                

    四、计算轮子的速度

    从与地面接触的辊子速度到轮子线转速的计算比较简单:原因是辊子与轮子夹角是45°,

    V_{v} =  \frac{V_{||}}{cos(45°)} =( -\frac{1}{\sqrt{2}}*V_{x}  +  \frac{1}{\sqrt{2}}V_{y})*\sqrt{2} = -V_{x} + V_{y}

    根据上面求出的       V_{x}    =  V_{tx} - W*r_{y},       V_{y} =  V_{ty} + W*r_{x}由此可得

      V_{v} =-( V_{tx} - W*r_{y})+ (V_{ty} + W*r_{x}) = V_{ty}  - V_{tx} +  W*(  r_{x}  + r_{y})

    这样求出来的是轮子与地面接触的一点的线速度,如果要求轮子转速V_{w}(单位rpm),需要再除以轮子半径r_{r}

      V_{w}  =   \frac{V_{v}}{r_{r}} ,单位是弧度/秒,转化到rpm(转/分钟),需要30*V_{w}/PI  ,到此,轮子速度已经求出来啦

              

    根据上图定义,可知r_{x} = a , r_{y} = b,由此简化可得轮子线速度为:

      V_{v}  = V_{ty}  - V_{tx} +  W*\left ( a+b \right )

    由此计算获得四个轮子线速度如下

      V_{v1}  = V_{ty}  - V_{tx} +  W*\left ( a+b \right )

      V_{v2}  = V_{ty}  + V_{tx} -  W*\left ( a+b \right )

      V_{v3}  = V_{ty}  - V_{tx} -  W*\left ( a+b \right )

      V_{v4}  = V_{ty}  + V_{tx} +  W*\left ( a+b \right )

    以上方程组就是O-长方形麦轮底盘的逆运动学模型,而正运动学模型可以直接根据逆运动学模型中的三个方程解出来,此处不再赘述。

    另一种计算方式

    「传统」的推导过程虽然严谨,但还是比较繁琐的。这里介绍一种简单的逆运动学计算方式。

    我们知道,全向移动底盘是一个纯线性系统,而刚体运动又可以线性分解为三个分量。那么只需要计算出麦轮底盘在「沿X轴平移」、「沿Y轴平移」、「绕几何中心自转」时,四个轮子的速度,就可以通过简单的加法,计算出这三种简单运动所合成的「平动+旋转」运动时所需要的四个轮子的转速。而这三种简单运动时,四个轮子的速度可以通过简单的测试,或是推动底盘观察现象得出。

    当底盘沿着 X 轴平移时:

    V_{v1} =  - V_{tx} 

    V_{v2} =  + V_{tx} 

    V_{v3} =  - V_{tx} 

    V_{v4} =  + V_{tx} 

    当底盘沿着 Y 轴平移时:

    V_{v1} =  + V_{ty} 

    V_{v2} =  + V_{ty} 

    V_{v3} =  + V_{ty} 

    V_{v4} =  + V_{ty} 

    当底盘绕几何中心自转时:

      V_{v1}  = +  W*\left ( a+b \right )

      V_{v2}  =  -  W*\left ( a+b \right )

      V_{v3}  =  -  W*\left ( a+b \right )

      V_{v4}  =  +  W*\left ( a+b \right )

    将以上三个方程组相加,得到的恰好是根据「传统」方法计算出的结果。这种计算方式不仅适用于O-长方形的麦轮底盘,也适用于任何一种全向移动的机器人底盘。

    Makeblock 麦轮底盘的组装

    理论分析完成,可以开始尝试将其付诸实践了。

    第一步,组装矩形框架。

    第二步,组装电机模块。

    由于麦轮底盘的四个轮子速度有约束关系,必须精确地控制每个轮子的速度,否则将会导致辊子与地面发生滑动摩擦,不仅会让底盘运动异常,还会让麦轮的寿命减少。所以必须使用编码电机。

     

    第三步,将电机模块安装到框架上。

     

     

    第四步,将麦轮安装到框架上。

     

    第五步,安装电路板并接线。

    编码电机必须配上相应的驱动板才能正常工作。这里使用的 Makeblock 编码电机驱动板,每一块板可以驱动两个电机。接线顺序在下文中会提及,也可以随意接上,在代码中定义好对应的顺序即可。

     

    第六步,装上电池。

     

    至此,一个能独立运行的麦轮底盘就完成了。

     

    控制程序

    根据麦轮的底盘的运动学模型,要完全控制它的运动,需要有三个控制量:X轴速度、Y轴速度、自转角速度。要产生这三个控制量,有很多种方法,本文将使用一个 USB 游戏手柄,左边的摇杆产生平移速度,右边的摇杆产生角速度。

    首先将一个 USB Host 模块连接到 Orion 主板的 3 口。

     

    然后插上一个无线 USB 游戏手柄。

     

     

    然后再添加其他细节,就大功告成啦!

     

     

    其他细节:

     

    #include <Wire.h>
    #include <SoftwareSerial.h>
    #include "MeOrion.h"
    
    MeUSBHost joypad(PORT_3);
    //	手柄代码(红灯亮模式)
    //	默认:128-127-128-127-15-0-0-128
    //	左一:128-127-128-127-15-1-0-128
    //	右一:128-127-128-127-15-2-0-128
    //	左二:128-127-128-127-15-4-0-128
    //	右二:128-127-128-127-15-8-0-128
    //	三角:128-127-128-127-31-0-0-128 (0001 1111)
    //	方形:128-127-128-127-143-0-0-128 (1000 1111)
    //	叉号:128-127-128-127-79-0-0-128 (0100 1111)
    //	圆圈:128-127-128-127-47-0-0-128 (0010 1111)
    //	向上:128-127-128-127-0-0-0-128 (0000 0000)
    //	向下:128-127-128-127-4-0-0-128 (0000 0100)
    //	向左:128-127-128-127-6-0-0-128 (0000 0110)
    //	向右:128-127-128-127-2-0-0-128 (0000 0010)
    //	左上:128-127-128-127-7-0-0-128 (0000 0111)
    //	左下:128-127-128-127-5-0-0-128 (0000 0101)
    //	右上:128-127-128-127-1-0-0-128 (0000 0001)
    //	右下:128-127-128-127-3-0-0-128 (0000 0011)
    //	选择:128-127-128-127-15-16-0-128
    //	开始:128-127-128-127-15-32-0-128
    //	摇杆:右X-右Y-左X-左Y-15-0-0-128
    
    
    MeEncoderMotor motor1(0x02, SLOT2);
    MeEncoderMotor motor2(0x02, SLOT1);
    MeEncoderMotor motor3(0x0A, SLOT2);
    MeEncoderMotor motor4(0x0A, SLOT1);
    
    //  底盘:a = 130mm, b = 120mm
    
    float linearSpeed = 100;
    float angularSpeed = 100;
    float maxLinearSpeed = 200;
    float maxAngularSpeed = 200;
    float minLinearSpeed = 30;
    float minAngularSpeed = 30;
    
    void setup()
    {
        //  要上电才能工作,不能只是插上 USB 线来调试。
    	motor1.begin();
    	motor2.begin();
    	motor3.begin();
    	motor4.begin();
    	
    	Serial.begin(57600);
    	joypad.init(USB1_0);
    }
    
    void loop()
    {
        Serial.println("loop:");
    	//setEachMotorSpeed(100, 50, 50, 100);
    	if(!joypad.device_online)
        {
            //  若一直输出离线状态,重新拔插 USB Host 的 RJ25 线试一下。
            Serial.println("Device offline.");
            joypad.probeDevice();
            delay(1000);
        }
        else
        {
            int len = joypad.host_recv();
            parseJoystick(joypad.RECV_BUFFER);
            delay(5);
        }
    	//delay(500);
    }
    
    
    void setEachMotorSpeed(float speed1, float speed2, float speed3, float speed4)
    {
    	motor1.runSpeed(speed1);
    	motor2.runSpeed(-speed2);
    	motor3.runSpeed(-speed3);
    	motor4.runSpeed(-speed4);
    }
    
    void parseJoystick(unsigned char *buf)   //Analytic function, print 8 bytes from USB Host
    {
        //  输出手柄的数据,调试用
        // int i = 0;
        // for(i = 0; i < 7; i++)
        // {
        //     Serial.print(buf[i]);  //It won't work if you connect to the Makeblock Orion.
        //     Serial.print('-');
        // }
        // Serial.println(buf[7]);
        // delay(10);
    
        //  速度增减
        switch (buf[5])
        {
            case 1:
                linearSpeed += 5;
                if (linearSpeed > maxLinearSpeed)
                {
                    linearSpeed = maxLinearSpeed;
                }
                break;
            case 2:
                angularSpeed += 5;
                if (angularSpeed > maxAngularSpeed)
                {
                    angularSpeed = maxAngularSpeed;
                }
                break;
            case 4:
                linearSpeed -= 5;
                if (linearSpeed < minLinearSpeed)
                {
                    linearSpeed = minLinearSpeed;
                }
                break;
            case 8:
                angularSpeed -= 5;
                if (angularSpeed < minAngularSpeed)
                {
                    angularSpeed = minAngularSpeed;
                }
                break;
            default:
                break;
        }
    
    
        
        if ((128 != buf[0]) || (127 != buf[1]) || (128 != buf[2]) || (127 != buf[3]))
        {
            //  处理摇杆
            float x = ((float)(buf[2]) - 127) / 128;
            float y = (127 - (float)(buf[3])) / 128;
            float a = (127 - (float)(buf[0])) / 128;
            mecanumRun(x * linearSpeed, y * linearSpeed, a * angularSpeed);
        }
        else
        {
            switch (buf[4])
            {
                case 0:
                    mecanumRun(0, linearSpeed, 0);
                    break;
                case 4:
                    mecanumRun(0, -linearSpeed, 0);
                    break;
                case 6:
                    mecanumRun(-linearSpeed, 0, 0);
                    break;
                case 2:
                    mecanumRun(linearSpeed, 0, 0);
                    break;
                case 7:
                    mecanumRun(-linearSpeed/2, linearSpeed/2, 0);
                    break;
                case 5:
                    mecanumRun(-linearSpeed/2, -linearSpeed/2, 0);
                    break;
                case 1:
                    mecanumRun(linearSpeed/2, linearSpeed/2, 0);
                    break;
                case 3:
                    mecanumRun(linearSpeed/2,  -linearSpeed/2, 0);
                    break;
                default:
                    mecanumRun(0, 0, 0);
                    break;
            }
        }
    }
    
    void mecanumRun(float xSpeed, float ySpeed, float aSpeed)
    {
        float speed1 = ySpeed - xSpeed + aSpeed; 
        float speed2 = ySpeed + xSpeed - aSpeed;
        float speed3 = ySpeed - xSpeed - aSpeed;
        float speed4 = ySpeed + xSpeed + aSpeed;
        
        float max = speed1;
        if (max < speed2)   max = speed2;
        if (max < speed3)   max = speed3;
        if (max < speed4)   max = speed4;
        
        if (max > maxLinearSpeed)
        {
            speed1 = speed1 / max * maxLinearSpeed;
            speed2 = speed2 / max * maxLinearSpeed;
            speed3 = speed3 / max * maxLinearSpeed;
            speed4 = speed4 / max * maxLinearSpeed;
        }
        
        setEachMotorSpeed(speed1, speed2, speed3, speed4);
    }

    Makeblock 麦克纳姆轮 全向移动机器人

     PS:答疑

    1. 这轮子这么灵活,为啥不都装成麦克纳姆轮呢,多方便啊

    答:1. 如果上面的算法你掌握了,那么就明白所有的理论计算都是基于小车轮子在一个平面的,那么如果有悬挂的,或者地面不平坦的时候,很明显计算过程中的速度会有偏差,就会导致你的小车底盘跑的不准、不直,如果你因此计算里程计就更差了。2. 效率上不行,为啥呢?记得上面那个问题吗?棍子垂直方向的速度不考虑,为啥?因为这个速度主要用于棍子自转,对小车运行没有影响,那这就势必造成了能量浪费(相比普通轮子),所以效率就降低了 。3 .因为结构特性原因,不利于爬坡、越障,所以比较适合于平地使用,大大限制了应用场景

    2. 程序中为啥有速度反向

    因为电机运行方向的原因

    展开全文
  • 目录1 反向传播算法数学基础1.1 前向传播1.2 反向传播2 矩阵运算 1 反向传播算法数学基础 反向传播算法是用于对神经网络中的各个网络参数计算偏导值的一种算法,其核心是链式求导法则。 注:本节涉及到较复杂的数学...

    *注:本博客参考李宏毅老师2020年机器学习课程. 视频链接


    1 反向传播算法数学基础

    反向传播算法是用于对神经网络中的各个网络参数计算偏导值的一种算法,其核心是链式求导法则。
    注:本节涉及到较复杂的数学计算,了解思想即可。

    在这里插入图片描述
    (图一)

    在图一中,画出了一个神经网络的前两层结构的部分神经元。图中, x i x_i xi表示网络的输入。 w i j w_{ij} wij表示第i层神经元的参数,j仅用作区分不同的参数。 z i j z_{ij} zij表示第i层神经元经过激活函数之前的值, a i j a_{ij} aij表示第i层第j个神经元的输出。 C C C表示网络计算得到的损失函数值(Cost).由此可知:
    z 11 = ∑ j = 1 n w 1 j x j (1) z_{11}=\sum_{j=1}^{n}{w_{1j}x_j}\tag{1} z11=j=1nw1jxj(1)
    a = σ ( z ) (2) a=\sigma{\left(z\right)}\tag{2} a=σ(z)(2)
    其中 σ \sigma σ表示激活函数。


    如果要计算 ∂ C ∂ w 11 \frac{\partial{C}}{\partial{w_{11}}} w11C,根据链式求导法则,有:
    ∂ C ∂ w 11 = ∂ z 11 ∂ w 11 ∂ C ∂ z 11 (3) \frac{\partial{C}}{\partial{w_{11}}}=\frac{\partial{z_{11}}}{\partial{w_{11}}}\frac{\partial{C}}{\partial{z_{11}}}\tag{3} w11C=w11z11z11C(3)
    在等式3的右边,计算前一项的过程称为前向传播,计算后一项的过程称为反向传播。

    1.1 前向传播

    前向传播即计算 ∂ z 11 ∂ w 11 \frac{\partial{z_{11}}}{\partial{w_{11}}} w11z11,根据式1可知, ∂ z 11 ∂ w 11 = x 1 \frac{\partial{z_{11}}}{\partial{w_{11}}}=x_{1} w11z11=x1,实际上:
    ∂ z 2 i ∂ w 2 i = a 11 , i = 1 , 2 , . . . m (4) \frac{\partial{z_{2i}}}{\partial{w_{2i}}}=a_{11},i=1,2,...m\tag{4} w2iz2i=a11,i=1,2,...m(4)
    照此推理,只要知道上一层神经元的输出,就可以求得这一层神经元的输入z对这一层神经元的参数w的偏导数值。在顺序计算损失函数值得过程中,所有的 ∂ z ∂ w \frac{\partial{z}}{\partial{w}} wz都可以计算完毕,因此称为前向传播。

    1.2 反向传播

    ∂ C ∂ z 11 \frac{\partial{C}}{\partial{z_{11}}} z11C的计算要稍微复杂一些,首先可以根据式2,得:
    ∂ C ∂ z 11 = ∂ C ∂ a 11 σ ′ ( z 11 ) (5) \frac{\partial{C}}{\partial{z_{11}}}=\frac{\partial{C}}{\partial{a_{11}}}\sigma'(z_{11})\tag{5} z11C=a11Cσ(z11)(5)
    由于 z 11 z_{11} z11是一个已知数值,因此 σ ′ ( z 11 ) \sigma'(z_{11}) σ(z11)是一个常数。对于上式等号右侧得第一项,从图1中可以看出, a 11 a_{11} a11得变化首先会改变 z 2 j , j = 1 , 2 , . . . , m z_{2j},j=1,2,...,m z2j,j=1,2,...,m的值变化,最终导致C的值变化,那么:
    ∂ C ∂ a 11 = ∑ i = 1 m ∂ C ∂ z 2 i w 2 i (6) \frac{\partial{C}}{\partial{a_{11}}}=\sum_{i=1}^{m}{\frac{\partial{C}}{\partial{z_{2i}}}w_{2i}}\tag{6} a11C=i=1mz2iCw2i(6)
    将式5和式6联立化简得:
    ∂ C ∂ z 11 = σ ′ ( z 11 ) ∑ i = 1 m ∂ C ∂ z 2 i w 2 i (7) \frac{\partial{C}}{\partial{z_{11}}}=\sigma'(z_{11})\sum_{i=1}^{m}{\frac{\partial{C}}{\partial{z_{2i}}}w_{2i}}\tag{7} z11C=σ(z11)i=1mz2iCw2i(7)
    上式中的 σ ′ ( z 11 ) \sigma'(z_{11}) σ(z11) w 2 i w_{2i} w2i的值均为已知,于是不难发现,式7的定义是递归的,也就是说,要计算损失函数值C对某一层神经元的输入 z i z_i zi的偏导值,必须先知道C对下一层神经元的输入 z i + 1 z_{i+1} zi+1的偏导值。那么自然会想到从最后一层开始,向前计算。
    在这里插入图片描述
    (图2)

    在图2中给出了神经网络的最后一层的结构。
    y i = 1 1 + e − z i , i = 1 , 2 , . . . , t (8) y_i=\frac{1}{1+e^{-z_i}},i=1,2,...,t\tag{8} yi=1+ezi1,i=1,2,...,t(8)
    当使用交叉熵作为损失函数时:
    C = − ∑ i = 1 t ( y i ^ ln ⁡ y i + ( 1 − y i ^ ) ln ⁡ ( 1 − y i ) ) (9) C=-\sum_{i=1}^{t}{(\hat{y_i}\ln{y_i}+(1-\hat{y_i})\ln{(1-y_i)})} \tag{9} C=i=1t(yi^lnyi+(1yi^)ln(1yi))(9)

    ∂ C ∂ z 1 = ∂ C ∂ y 1 ∂ y 1 ∂ z 1 = − ( y 1 ^ y 1 − 1 − y 1 ^ 1 − y 1 ) σ ′ ( y 1 ) = y 1 − y 1 ^ (10) \frac{\partial{C}}{\partial{z_1}}=\frac{\partial{C}}{\partial{y_1}}\frac{\partial{y_1}}{\partial{z_1}}=-(\frac{\hat{y_1}}{y_1}-\frac{1-\hat{y_1}}{1-y_1})\sigma'({y_1})=y_1-\hat{y_1} \tag{10} z1C=y1Cz1y1=(y1y1^1y11y1^)σ(y1)=y1y1^(10)
    综上,结合式3、4、7、10,便可以求得网络中任何一个神经元对于其参数w的偏导值。对于式3,我们可以写出它的表达式:
    ∂ C ∂ w 11 = x 1 σ ′ ( z 11 ) ∑ i = 1 m ∂ C ∂ z 2 i w 2 i (11) \frac{\partial{C}}{\partial{w_{11}}}=x_1\sigma'(z_{11})\sum_{i=1}^{m}{\frac{\partial{C}}{\partial{z_{2i}}}w_{2i}}\tag{11} w11C=x1σ(z11)i=1mz2iCw2i(11)

    2 矩阵运算

    下面我们将上述计算过程写成矩阵运算的形式,这种形式更适合在实际的编码过程中使用各种矩阵运算库进行加速运算(如numpy、tensorflow、PyTorch等)。

    • 首先回到图1,我们将神经网络的输入写成 X 1 ∈ R n × 1 X_1\in{\mathbb{R}^{n\times1}} X1Rn×1,表示它是一个n行1列的矩阵,对应输入具有n个维度。
    • 对于神经网络的第一层,假设第一层有k个神经元,则每一个神经元都应该具有n个参数w。因此将第一层神经元的所有参数w写成矩阵 W 1 ∈ R k × n W_1\in{\mathbb{R}^{k\times n}} W1Rk×n,表示它是一个k行n列的矩阵,其中每一行表示同一个神经元对输入 X X X不同维度的对应参数,每一列表示不同神经元对输入 X X X相同维度的对应参数,下标1代表它是第一个隐藏层的参数。
    • 同理,第一层的每一个神经元还有一个参数b,将该曾所有的b写成矩阵为 B 1 ∈ R k × 1 B_1\in{\mathbb{R}^{k\times 1}} B1Rk×1
    • 第一个隐藏层的激活函数前的输出 Z Z Z也是一个矩阵,表示为 Z 1 ∈ R k × 1 Z_1\in{\mathbb{R}^{k\times 1}} Z1Rk×1,且 Z 1 = W 1 X 1 + B 1 Z_1=W_1X_1+B_1 Z1=W1X1+B1。该层的输出表示为 A 1 ∈ R k × 1 A_1\in{\mathbb{R}^{k\times 1}} A1Rk×1,且 A 1 = σ ( Z 1 ) A_1=\sigma(Z_1) A1=σ(Z1),同时该层的输出也是第二层的输入,因此 A 1 = X 2 A_1=X_2 A1=X2
    • 其余各层的运算与上述过程类似,不再赘述。
    • 对于神经网络的最后一层,如图2,将得到整个网络的输出 Y Y Y,与之前不同的是,将激活函数 σ \sigma σ换成了Softmax函数,假设网络输出包含t维,那么 Y ∈ R t × 1 Y\in{\mathbb{R}^{t\times1}} YRt×1 Y Y Y的各个维度的值的计算遵循公式8。

    因此,矩阵运算计算第二层输入的过程可以表示如下:

    在这里插入图片描述
    (图3)

    其中,dot表示矩阵乘法。在前向传播的过程中,不断重复以上过程,就可以得到所有的层的输入 X X X。该过程如下图4,前向传播的任务就是完成矩阵 X X X的计算:

    在这里插入图片描述
    (图4)

    将损失函数 C C C对第i层(非最后一层)的参数 W i W_i Wi求导的计算式11加以扩展,改写为矩阵运算形式如下:
    ∂ C ∂ W i = [ σ ′ ( Z i ) ∗ ( W i + 1 T ∂ C ∂ Z i + 1 ) ] X i T (12) \frac{\partial{C}}{\partial{W_i}}=[\sigma'({Z_i})*(W_{i+1}^T\frac{\partial{C}}{\partial{Z_{i+1}}})]X_i^T\tag{12} WiC=[σ(Zi)(Wi+1TZi+1C)]XiT(12)
    上式中, W i W_i Wi为第i层的网络权重, X i X_i Xi为第i层神经元的输入, Z i = W i X i + B i Z_i=W_iX_i+B_i Zi=WiXi+Bi为第i层神经元激活前的输出值, ∗ * 表示元素乘法(即对应位置元素相乘), T T T表示矩阵转置。
    假设该网络的第i层有m个神经元,第i+1层有n个神经元,第i-1层有p个神经元,那么式11中各变量的维度分别如下表:

    变量维度变量维度
    X i X_i Xi(p,1) Z i Z_i Zi(m,1)
    Z i + 1 Z_{i+1} Zi+1(n,1) W i W_i Wi(m,p)
    ∂ C ∂ W i \frac{\partial{C}}{\partial{W_i}} WiC(m,p) ∂ C ∂ Z i + 1 \frac{\partial{C}}{\partial{Z_{i+1}}} Zi+1C(n,1)
    W i + 1 T ∂ C ∂ Z i + 1 W_{i+1}^T\frac{\partial{C}}{\partial{Z_{i+1}}} Wi+1TZi+1C(m,1) W i + 1 T W_{i+1}^T Wi+1T(m,n)

    上述讨论中我们没有考虑对B的偏导数,实际上,如果将式3中等号右边第一项替换为 ∂ z 11 ∂ b 11 \frac{\partial{z_{11}}}{\partial{b_{11}}} b11z11即可得出对B的偏导,并且这一项始终为1,因此:
    ∂ C ∂ B i = σ ′ ( Z i ) ∗ ( W i + 1 T ∂ C ∂ Z i + 1 ) (13) \frac{\partial{C}}{\partial{B_i}}=\sigma'({Z_i})*(W_{i+1}^T\frac{\partial{C}}{\partial{Z_{i+1}}})\tag{13} BiC=σ(Zi)(Wi+1TZi+1C)(13)

    3 模型实现

    根据上述数学推理,设计并实现一个神经网络已经变得可能。下面我们开始着手设计实现这个数据结构。

    3.1 模型结构设计

    首先明确我们要设计的神经网络的结构,假设该模型有m层,m最小为3,因为除了输入层与输出层之外,网络应该包含至少一个隐藏层。
    每个隐藏层都有自己的W和B。若第 i ∈ [ 1 , m − 1 ] i\in[1,m-1] i[1,m1]层有 n i n_i ni个神经元,那么其参数满足: W i ∈ R n i × n i − 1 , B i ∈ R n i × 1 W_i\in{\mathbb{R}^{n_i\times n_{i-1}}},B_i\in{\mathbb{R}^{n_i\times1}} WiRni×ni1,BiRni×1,整个网络的参数一共2(m-2)个矩阵(其中m-2个矩阵为权重W的矩阵,m-2个矩阵为偏置B的矩阵)。网络结构定义如下:

    import numpy as np
    class Network():
        def __init__(self, *layers):
            # 至少包含输入层,输出层,和一个隐藏层,所以至少有三层
            assert(len(layers) > 2)
            # W的维度为(当前层神经元数,上一层神经元数)
            self.W = [np.random.randn(y, x)
                      for x, y in zip(layers[:-1], layers[1:])]
            # B的维度为(当前层神经元数,1)
            self.B = [np.random.randn(y, 1)
                      for x, y in zip(layers[:-1], layers[1:])]
    

    根据式12可知,在前向传播的过程中,需要记录 X i X_i Xi Z i Z_i Zi的值,以方便计算偏导值。

    def forward(network, X):
        Zs = []
        Xs = []
        for W, B in zip(network.W, network.B):
            Xs.append(X)
            Z = W.dot(X)+B
            X = sigmoid(Z)
            Zs.append(Z)
        Y = sigmoid(Z)
        return Xs, Zs, Y
    # sigmoid函数
    def sigmoid(Z):
        A = 1/(1+np.exp(-Z))
        return A
    

    反向传播的计算 ∂ C ∂ Z i \frac{\partial{C}}{\partial{Z_{i}}} ZiC的表达式就是式12等式右边第一项,计算过程中需要保存所有的 ∂ C ∂ Z i \frac{\partial{C}}{\partial{Z_{i}}} ZiC

    def backward(network, pcpz, Zs):
        P_z = [pcpz]
        for W, Z in zip(network.W[::-1][:-1], Zs[::-1][1:]):
            pcpz = sigma_(Z)*(W.T.dot(pcpz))
            P_z.append(pcpz)
        P_z.reverse()
        return P_z
    # sigmoid函数的导数
    def sigma_(Z):
        return sigmoid(Z)*(1-sigmoid(Z))
    

    在开始反向传播之前,首先需要按照式10计算 ∂ C ∂ Z i \frac{\partial{C}}{\partial{Z_{i}}} ZiC的最后一项:

    def get_last_pcpz(Y0, Y1):
        return Y1-Y0
    

    下面给出训练过程的代码:

    def start_train(train_x, train_y,epoch,lr,*hidden_layers):
        nn = Network(train_x[0].shape[0], *hidden_layers, train_y[0].shape[0])
        loss = [] #记录损失函数值的变化
        for i in range(epoch):
            # 初始化W和B在当轮迭代中的变化量
            delta_W = [np.zeros_like(w) for w in nn.W]
            delta_B = [np.zeros_like(b) for b in nn.B]
            l = 0
            # 对训练数据中的每一个样本进行计算
            for X, Y in zip(train_x, train_y):
                Xs, Zs, Y1 = forward(nn, X) #前向传播
                pcpz = get_last_pcpz(Y, Y1) #计算最后一项
                P_z = backward(nn, pcpz, Zs) #反向传播
                l += np.abs(Y-Y1).sum() #计算当前样本的损失函数值
                # 更新当前轮次的W和B由于该样本所带来的变化量
                delta_B = [db+pz for db, pz in zip(delta_B, P_z)] 
                delta_W = [dw+pz.dot(x.T) for dw, pz, x in zip(delta_B, P_z, Xs)]
            loss.append(l)
            # 更新模型参数
            nn.W = [w-dw*lr/len(train_x) for w, dw in zip(nn.W, delta_W)]
            nn.B = [b-db*lr/len(train_x) for b, db in zip(nn.B, delta_B)]
        return nn, loss
    

    3.2 模型测试

    使用一组简单的数据对所实现的模型进行测试。该组数据由2个样本构成,其值分别为(1,1)和(0,0),我们将前者归为类别1,后者归为类别2,对于类别1,我们用(1,0)表示,类别2,我们用(0,1)表示,下面对着一组简单的数据进行训练,并给出训练过程中loss的变化情况。

    import matplotlib.pyplot as plt
    train_x = [np.ones((2, 1)), np.zeros((2, 1))]
    train_y = [np.asarray([[1], [0]]), np.asarray([[0], [1]])]
    nn, loss = start_train(train_x, train_y,3000,1,3,4)
    for X, Y in zip(train_x, train_y):
        Xs, Zs, Y1 = forward(nn, X)
        print("input:{} output: {} class:{}".format(X.tolist(),Y1.tolist(),np.argmax(Y1)+1))
    plt.plot(loss)
    plt.show()
    
    input:[[1.0], [1.0]] output: [[0.9989013915763175], [0.00127671782739844]] class:1
    input:[[0.0], [0.0]] output: [[0.0008225495145913559], [0.9990508703257248]] class:2
    

    在这里插入图片描述

    展开全文
  • 梯度下降和反向传播算法讲解

    千次阅读 2019-10-22 16:19:07
    1. 原理讲解 在讲梯度下降之前,先向大家推荐一门课程《数值分析》,有的书也将它称为《计算方法》。 数值分析(numerical analysis),为数学的一个分支,是研究分析用计算机求解数学计算问题的数值计算方法及其...

    一、梯度下降

    1. 原理讲解

            在讲梯度下降之前,先向大家推荐一门课程《数值分析》,有的书也将它称为《计算方法》。

    数值分析(numerical analysis),为数学的一个分支,是研究分析用计算机求解数学计算问题的数值计算方法及其理论的学科。它以数字计算机求解数学问题的理论和方法为研究对象,为计算数学的主体部分。

            这门课程详细得介绍了如何用计算机求解积分、微分、导数、方程根、微分方程、一元线性方程组、函数极值。那么我们今天要讲解的梯度下降方法就是《数值分析》里面的一个模块。梯度下降算法是用来求一个函数的极小值点。举个例子:

    求   y = x^{3} - 2x^{2} - 19x + 20 的函数极小值。函数图像如图:

    y的图像

    在数学上,求y的极小值点可以通过对y求一次导数,根据导数的零点和函数的单调性能够精确地算出极小值点地值。我们在这里手算以一下:

    y^{'} = 3x^{2}-4x-19 ,   解出x1 = 3.270083,    x2 = -1.936750。结合函数图像可以知道极小值点是x1。

    但是,计算机解决的问题是普遍性的,通用性的。这道题的y毕竟是一个普通的多项式函数,如果现在一个函数带上积分、带上三角函数,带上e,带上指数对数函数,此时人工手算的方法就远远不够了。

    梯度下降方法就是通过迭代的方式,每次进一小步寻找最小值。

    注意到x1的左侧A点导数值小于0,x1的右侧B点导数值大于0,x1处导数值等于0. 随机初始化一个点p,当p在点A时,p = p + δ,当p在B点时,p = p - δ,其中δ = 前进步长,也就是可以写成 p = p - 学习率 * p点导数值。当p点充分接近于x1时,可以停止迭代,那么经过迭代,极小值点就可以找出来。

    公式化为:p = p - r * \frac{dy}{dx}  (r是很小的一个数,表示学习率)。当然最后迭代的结果取决于选择的点的位置,当初始点p在A, B之间,则能够迭代出x1,当初始点在c附近,则无法迭代出结果,也就是说算法不会收敛,只会向左一泻千里。当然,数值分析里还有很多算法都取决于初始值的位置。

    2.代码实现

    说这么多,我们用代码实现一下这个算法,选用cpp语言。

    #include<cstdio>
    #include<cmath>
    
    using namespace std;
    
    long long N;        //迭代次数
    double p;           //初始位置
    double r;           //学习率
    
    
    double ff(double x){        // y的导数
        return 3.0 * pow(x, 2) - 4.0 * x - 19;
    }
    
    
    int main(){
        scanf("%lld%lf%lf",&N, &p, &r);
        for(int i = 0;i<N;i++){
            p = p - r * ff(p);      //p处的导数
        }
        printf("%lf", p);
        return 0;
    }

    运行一下程序,显示执行结果:

    程序运行结果
    Npr极小值点
    10000.013.270083
    10010.053.270083
    1000.2-4989812654385.430664
    10-50.01-1447.283969

     

    从上面的表格可以看到,当我们的参数选择的合适的时候,是可以计算出极小值点x1的,但是如果学习率过大,会出现点p在极值点处左右摇摆的情况,而不能收敛到x1。而当p被“甩”到左侧时,就会出现最后一种情况,函数永远无法收敛。当然,现在有很多方法能够有效避免这种情况,比如学习率随着迭代逐渐改变,而不使用一个固定的学习率,或者随机初始化几个点,然后找到能够收敛的点等等措施。

    3. 用梯度下降求解线性回归

            线性是一个好的特性,线性回归同样也是一个最简单的回归,我们可以用手算或者某种公式就能够算出需要评估的斜率和截距。回顾用手算线性回归方法:已知<x1, y1>, ...... , <xn, yn>,拟合出直线 y = ax + b。

    <1,6> , <2, 8> , <3, 9.5>, <4, 11.5>, <5, 14>

    首先确定距离函数L(也称为损失函数), L =\frac{1}{2} \sum_{i = 1}^{n}(y_{i} - (ax_{i}+b))^{2},我们要寻找使L取极小值的a和b。把xi和yi看成常数,那么L可以看成关于a和b的函数,也即求L的极小值点。

    我们有两种方法,一种是手算求导找到函数零点后判断极小值点,另一种方法就是使用我们的梯度下降方法。两种方法都算一遍。计算结果如图:

    \frac{\partial L}{\partial a} = \sum_{i=1}^{n}x_{i}(ax_{i}+b-y_{i})    

    \frac{\partial L}{\partial b} = \sum_{i=1}^{n}(ax_{i}+b-y_{i})

    \frac{\partial L}{\partial a} = 0 , \frac{\partial L}{\partial b} = 0,解出a和b即可。结果为:

      (抄百度百科的)

    计算出a = 1.95, b = 3.95

    用梯度下降方法计算:

    回顾梯度下降公式:

    a = a - \gamma * \partial L / \partial a

    b = b - \gamma * \partial L / \partial b

    那么每次迭代都小部分更新a和b,当迭代次数充分后,可以计算出a和b的数值解。

    下面上代码:

    #include<iostream>
    #include<unordered_map>
    
    using namespace std;
    
    /**
        用梯度下降方法求线性回归(不是最小二乘)
    
        输入:N L, 表示数据结点数和训练轮次
        输出:a, b, 表示y = ax + b
    
    **/
    
    double a, b;          // y = ax + b
    double r1, r2;      //a, b 学习率
    long long N;        //顶点个数
    long long L;        //迭代次数
    unordered_map<double, double> data;
    
    void init(){        //初始化
        a = 0.5;        
        b = 0.5;
        r1 = r2 = 0.005;    
    }
    
    void train(){
        double na = 0, nb = 0;
        for(pair<double, double> p : data){
            double xi = p.first, y_bar = p.second;
            na += (xi * (a * xi + b - y_bar));
            nb += (a * xi + b - y_bar);
        }
        a = a - (r1 * na);
        b = b - (r2 * nb);
        return;
    }
    
    int main(){
        init();
        cin>>N>>L;     
    
        double x, y;
        for(long long i = 0;i<N;i++){
            cin>>x>>y;
            data[x] = y;
        }
    
        for(long long i = 0;i<L;i++){
            train();
        }
    
        cout<<"a = "<<a<<endl<<"b = "<<b<<endl;
        return 0;
    }
    

    执行结果如图:

    可以看到a = 1.95, b = 3.95,计算结果和解析解一致。 

    所以我们也可以用梯度下降的方法来求解线性回归(不过都能通过手算出解析解了,为啥还有梯度下降多此一举呢,手动斜眼)。

    练习1:用梯度下降求解y = e^{x} - x^3的极值点

    练习2:用梯度下降求解点集{<0.5,1.455>, <1, 9.777>, <1.5, 29.224>, <2,  403.429>, <2.2, 1422.257>} 拟合y = e^{ax^{2}}中a的结果。

    二、反向传播

    反向传播是在神经网络中,求解参数的一个方法,其中用到了梯度下降求解极小值和函数求导的链式法则两个工具。

    我们先考虑一个极简单的情况,其中神经元内不含激活函数,(也就是激活函数是y = x):

     

    已知输出结果有下面公式:

    \begin{bmatrix} \hat{y_{1}} \\ \hat{y_{2}} \end{bmatrix} = \begin{bmatrix} w_{1,1} & w_{1,2} & w_{1,3}\\ w_{2,1}&w_{2,2} &w_{2,3} \end{bmatrix} * \begin{bmatrix} x_{1}\\ x_{2} \\ x_{3} \end{bmatrix}

    损失函数为:L = \frac{1}{2}\sum_{i = 1}^{N}\sum _{j=1}^{2}(\hat{y_{i,j}} - y_{i,j})^{2}   

    其中 

    \hat{y_{i,1}} = w_{1,1} * x_{i,1} + w_{1,2}*x_{i,2} + w_{1,3}*x_{i,3}   (1)

    \hat{y_{i,2}} = w_{2,1}*x_{i,1}+w_{2,2}*x_{i,2}+w_{2,3}*x_{i,3}    (2)

    我们需要做的是,寻找当L最小的时候, wi,j的值。换句话说,对于函数L,其自变量为wi,j。当然采用梯度下降法。

    回忆梯度下降法公式(用w1,1举例子):

    w_{1,1} = w_{1,1}-\gamma * \frac{\partial {L}}{\partial{w_{1,1}}}

    每轮迭代时,都需要计算    \frac{\partial {L}}{\partial{w_{1,1}}}    ,如何计算呢?这明显是一个多元函数的嵌套,求导符合链式法则。

    则   \frac{\partial {L}}{\partial{w_{1,1}}} = \sum _{i = 1}^{N}\frac{\partial {L}}{\partial{\hat{y_{i,1}}}} * \frac{\partial {\hat{y_{i,1}}}}{\partial{w_{1,1}}} ,  等式右边第一项求导后是 \hat{y_{i,1}} - y_{i,1} , 等式第二项求导可根据(1)式进行,求导结果是xi,1。

    那么二者相乘即可算出 \frac{\partial {L}}{\partial{w_{1,1}}} =\sum _{i = 1}^{N} x_{i,1} * (\hat{y_{i,1}}- y_{i,1}) =\sum _{i=1}^{N} x_{i,1} * (w_{1,1} * x_{i,1} + w_{1,2}*x_{i,2} + w_{1,3}*x_{i,3} - y_{i1})

    从上面的公式中可以看出,每轮迭代时,w1,1的更新公式是w_{1,1} = w_{1,1} - \gamma * \sum _{i=1}^{N} x_{i,1} * (w_{1,1} * x_{i,1} + w_{1,2}*x_{i,2} + w_{1,3}*x_{i,3} - y_{i1})

    同理,w_{1,2}, w_{1,3}, w_{2,1}, w_{2,2}, w_{2,3} 也可以按这种方法,一步一步迭代出来。

     

    考虑激活函数

    令 \begin{bmatrix} \hat{z_{i,1}} \\ \hat{z_{i,2}} \end{bmatrix} = \begin{bmatrix} w_{1,1} & w_{1,2} & w_{1,3}\\ w_{2,1}&w_{2,2} &w_{2,3} \end{bmatrix} * \begin{bmatrix} x_{i,1}\\ x_{i,2} \\ x_{i,3} \end{bmatrix}

    激活函数采用sigmoid函数:

    \LARGE \hat{y_{i,1}} = \frac{1}{1+e^{-z_{i,1}}}

    还是观察w1,1的更新公式: \frac{\partial {L}}{\partial{w_{1,1}}} = \sum _{i = 1}^{N}\frac{\partial {L}}{\partial{\hat{y_{i,1}}}} * \frac{\partial {\hat{y_{i,1}}}}{\partial{w_{1,1}}} = \sum _{i = 1}^{N}\frac{\partial {L}}{\partial{\hat{y_{i,1}}}} * \frac{\partial {\hat{y_{i,1}}}}{\partial{z_{i,1}}} * \frac{\partial {\hat{z_{i,1}}}}{\partial{w_{1,1}}}

    对比原来的更新公式,其实就多了一个中间变量zi,1。

    上式第一项不变,第二项是对sigmoid求导,第三项不变,最后可以整理成一个求解参数式子,即为反向传播。

    为什么叫反向传播呢?

    这里只是包含一层的神经网络,没法体现出反向传播的直观,但是如果是深层结构,我们更新完最内层的w后,其求导结果还可以继续使用,然后再通过内层对外层的结果,算出L对外层w的导数,实现了误差函数的结果反向传播。

     

    用C++实现简单的神经网络

    为了方便,采用随机梯度下降方法,同时隐藏层的激活函数采用sigmoid函数,学习率固定为0.2.

    下图是首先需要的公式推导,相应的偏导数已经计算出来。

     

    代码如下:

    #include<iostream>
    #include<bits/stdc++.h>
    
    /**
        神经网络(2,3,2),2输入,2输出,最后一维是偏置,恒为1,激活函数采用sigmoid,输出不包含softmax
    
        输入:
        N    L  , N点的个数,L迭代轮次
        然后输入N个(x1,x2,y1,y2)
    
        然后输入测试样例
    
        输入如:
    
    20 10000
    0 1 1 0
    1 2 1 0
    1 3 1 0
    -1 2 1 0
    -1 3 1 0
    2 10 1 0
    -3 6 1 0
    3 7 1 0
    0 5 1 0
    -1 8 1 0
    0 -1 0 1
    1 -2 0 1
    1 -3 0 1
    -1 -2 0 1
    -1 -3 0 1
    2 -10 0 1
    3 -6 0 1
    -3 -2 0 1
    0 -5 0 1
    2 -3 0 1
    
    //测试样例
    2 10
    2 -5
    0 -9
    0 4
    
    
    **/
    using namespace std;
    
    
    int N;          //顶点数
    int L;          //学习轮次
    
    double W[3][3];
    double U[2][3];
    double rate = 0.02;
    
    
    struct elem{
        double x[3];            //
        double y[2];
    };
    
    elem data[10005];
    
    void init(){
        for(int i = 0;i<3;i++){
            for(int j = 0;j<3;j++){
                W[i][j] = 0.5;
            }
        }
    
        for(int i = 0;i<3;i++){
            U[1][i] = 0.5;
            U[2][i] = 0.5;
        }
    }
    
    double h[3];        //记录前向传播的值
    double z[3];
    double o[3];
    
    void train(int n){          //
        //前向传播
        for(int i = 0;i<3;i++){
            double d = 0;
            for(int j = 0;j<3;j++){
                d += W[i][j] * data[n].x[j];
            }
            h[i] = d;
        }
        for(int i = 0;i<3;i++){
            z[i] = 1.0/(1.0 +pow(exp(1.0),-h[i]));
        }
    
        for(int i = 0;i<2;i++){
            double d = 0;
            for(int j = 0;j<3;j++){
                d += U[i][j] * z[j];
            }
            o[i] = d;
        }
    
        //梯度下降更新
        double uu[3][2];        //  dL/du
        fill(uu[0], uu[0]+3 * 2, 0);
    
        //更新uij
        for(int i = 0;i<3;i++){
            for(int j = 0;j<2;j++){
                uu[i][j] = (o[i]-data[n].y[i]) * z[j];
                U[i][j] = U[i][j] - rate * uu[i][j];
            }
        }
    
        //更新wij
        double ww[3][3];    //    dL/wij
        fill(ww[0], ww[0]+3*3, 0);
    
    
        for(int i = 0;i<3;i++){
            for(int j = 0;j<3;j++){
                for(int k = 0;k<2;k++){
                    double tmp = (1.0/(1.0 +pow(exp(1.0),-h[i]))) * (1-((1.0/(1.0 +pow(exp(1.0),-h[i])))));
                    ww[i][j] += (o[k]-data[n].y[k])*U[k][i]*tmp*data[n].x[j];
                }
                W[i][j] = W[i][j] - rate * ww[i][j];
            }
        }
        return;
    }
    
    void printParam(){
        cout<<"W:"<<endl;
        for(int i = 0;i<3;i++){
            for(int j = 0;j<3;j++){
                cout<<W[i][j]<<" ";
            }
            cout<<endl;
        }
        cout<<endl<<"U:"<<endl;
        for(int i = 0;i<2;i++){
            for(int j = 0;j<3;j++){
                cout<<U[i][j]<<endl;
            }
            cout<<endl;
        }
    }
    
    void res(elem e){
        for(int i = 0;i<3;i++){
            double d = 0;
            for(int j = 0;j<3;j++){
                d += W[i][j] * e.x[j];
            }
            h[i] = d;
        }
    
        for(int i = 0;i<3;i++){
            z[i] = 1.0/(1.0 +pow(exp(1.0),-h[i]));
        }
    
        for(int i = 0;i<2;i++){
            double d = 0;
            for(int j = 0;j<3;j++){
                d += U[i][j] * z[j];
            }
            o[i] = d;
        }
    
    }
    
    int main(){
        cin>>N>>L;
        for(int i = 0;i<N;i++){
            cin>>data[i].x[0]>>data[i].x[1]>>data[i].y[0]>>data[i].y[1];
            data[i].x[2] = 1;
        }
    
        init();
    
        for(int i = 0;i<L;i++){
            for(int j = 0;j<N;j++){
                train(j);
            }
        }
    
        printParam();
        for(int i = 0 ;i<100;i++){
            elem e;
            cin>>e.x[0]>>e.x[1];
            e.x[2] = 1;
            res(e);
            cout<<o[0]<<"   "<<o[1]<<endl;
        }
    
        return 0;
    }
    
    

     

     

     

     

     

     

    展开全文
  • 本文会以最容易理解的方式来介绍反向传播,避免大量公式。 1. 前向传播和反向传播 前向还是反向区别在于计算顺序。我们通常把inference称为前向,因为计算顺序是从浅层到深层,一层一层地传递。而反向传播就是从...

    BP算法是深度学习中最重要也是最基础的算法之一,也是Hinton本人的图灵奖工作之一。更重要的是,BP是深度学习岗位面试必问题之一。本文会以最容易理解的方式来介绍反向传播,避免大量公式


    1. 前向传播和反向传播

    前向还是反向区别在于计算顺序。我们通常把inference称为前向,因为计算顺序是从浅层到深层,一层一层地传递。而反向传播就是从深层到浅层。

    先看一个二层感知机网络,如上图。每条虚线代表一个权重w,每个节点会带有一个b。在一开始会给所有权重初始化赋值,当模型没有被训练的时候也能有前向传播的输出。

    每一次模型前向传播的输出y,我们会拿y和标准答案\bar{y}进行比较,就会有一个量化差异f_{_loss}

    我们把这个量化差异称为损失函数,用公式f_{loss}(y, \bar{y})就可以表示。训练模型就是为了最下化输出值和标准答案的差异,即 最小化损失函数minf_{loss}(y,\bar{y})   (公式 1)

    损失函数可以根据具体任务选取,比如分类任务可以使用交叉熵,超分辨率任务可以使用MSE等等。在本文中,我们不讨论损失函数的选取。咱们只需要明白,损失函数就是这一轮模型输出和标准答案(即标签)差异的量化结果


    2. 梯度

    事实上,方向传播中梯度的定义和咱们以前在高数里学习的梯度是有区别的。不过思想是互通的。

    上节提到的“最小化损失函数”,咱在这节要将具体方法。只需要把每层参数加以“斧正”,则最后的输出就会越接近标准答案(标注值),从而损失函数值会越低。

    那么参数该如何“斧正”呢?每次斧正多少呢?答:使用梯度来斧正,每次斧正的量就是一个负梯度

    用公式来表示就是(w表示网络参数):{w}' \leftarrow w+\Delta w

    在本来的参数值上再加一个增量值(该增量可以为负),就变为更新后的参数。所以,我们只用确定了\Delta w就直到该怎么优化网络了。当然,这也是反向传播算法的精髓。公式如下:

    \Delta w_{i}=-\eta \frac{\partial f_{loss}}{\partial w_i}(公式 2)

    用一句话来解释:直接通过偏导寻找参数对于损失函数的负梯度。

    怎么计算偏导?

    链式法则走一波,放心,本文是不会介绍链式法则的。

    下图截自西瓜书:误差反向传播算法


    3. 异形层反向传播

    掌握第2节就可以明白Hinton老爷子最初的反向传播算法了。可是,随着神经网络的发展,咱们的网络层可不止多层感知机呀。当遇到卷积层,最大池化,平均池化,空洞卷积,反卷积,子像素卷积等等异形网络层的时候,怎么传播呢?

    尤其是池化层和pixel shuffle这种并不可导的网络层。在神经网络里,我们通常可以用一个函数来建模一个网络层,如果所有的网络层都是可导函数,则我们可以通过链式法则计算任何一层的梯度完成训练。事实上,并不是所有网络层都是可导的。

    有一篇关于卷积层和池化层的反向传播文章《卷积池化反向传播》,大家可以先看着。

    我先留个坑,以后时间充足再来详细解析异形层的反向传播。

     

     

     

     

     

     

    展开全文
  • PyTorch中在反向传播前为什么要手动将梯度清零? 这种模式可以让梯度玩出更多花样,比如说梯度累加(gradient accumulation) 传统的训练函数,一个batch是这么训练的: for i,(images,target) in enumerate...
  • 我用的是9g舵机 N20电机 1 选扭力大一点,这样转速会比较稳定 皮带和皮带 如上面的图片,我是用皮带来传输动力的 电路 名称 数量 备注 电池、电池盒 洞洞板 lm1117-3.3 降压芯片给控制系统供电 stm32f103c8t6核心...
  • 反向传播(英语:Backpropagation,缩写为BP)是“误差反向传播”的简称,是一种与最优化方法(如梯度下降法)结合使用的,用来训练人工神经网络的常见方法。该方法对网络中所有权重计算损失函数的梯度。这个...
  • Nginx反向代理实现负载均衡总结

    万次阅读 2016-03-23 22:57:12
    Nginx反向代理实现负载均衡总结: 从Nginx的诞生来源就可以知道,它是为了解决大数据量高并发访问而产生的,这也要感谢Nginx的开发者,使Nginx现今成为了LNMP重要的成员。好了,废话不多说了,下面就来总结介绍下...
  • 前向传播算法和反向传播算法

    千次阅读 2017-12-28 22:22:55
    最近在看神经网络中的前向传播算法(FP)和反向传播算法(BP),特地进行总结一下,方便以后理解。1.基本概念  上图是一张自己画的神经网络的图。假设每一层都添加了偏度单元(即值为1的神经元),用来表示阀值(因为...
  • 神经网络的前向传播和反向传播

    千次阅读 2020-05-25 00:50:23
    在讨论反向传播之前,我们讨论一下前向传播,即根据输入X来计算输出Y。输入X用矩阵表示,我们看一下如何基于矩阵X来计算网络的输出Y。 我们使用 表示从层的第 个神经元到层的个神经元的链接上的权重。例如,下图给...
  • 文章目录 2.1 引言 2.2 磁悬浮惯性动量整体结构 2.2.1 磁悬浮惯性动量转子结构 2.2.2 磁悬浮惯性动量定子结构 2.3 磁悬浮惯性动量磁轴承结构及构型 2.3.1 磁轴承工作原理与分类 2.3.1.1 磁阻力主动磁轴承 ...
  • ROS小车三轴全向底座运动学分析

    千次阅读 2019-10-28 12:36:04
    最近几天时间里,实验室的小伙伴研究了ROS小车的三轴全向底座的运动学原理,并稍有收获,现加以整理记录。 一、全向基础知识 全向由于能够向着与轴承垂直的方向运动,同时能依靠从动向着与轴承的平行方向...
  • 编码器的工作原理

    万次阅读 多人点赞 2019-11-12 09:03:37
    首先简述一下编码器的工作原理 编码器可按以下方式来分类。 1、按码盘的刻孔方式不同分类 (1)增量型:就是每转过单位的角度就发出一个脉冲信号(也有发正余弦信号, 编码器(图1) 然后对其进行细分,斩波出...
  • 文中介绍了AD22157的内部结构及其工作原理,并对其误差源作了简要的说明。 关键词:霍尔效应;差模信号;误差源;调制;AD221571 概述AD22157是一种混合信号磁场转换器,它具有很大的测速范围(0~...
  • 最近在学习深度学习中的梯度下降与反向传播方面的知识。偶有心得,特此记录。若有不足之处,烦请指正赐教。  在训练阶段,深度神经网络经过前向传播之后,得到的预测值与先前给出真实值之间存在差距。我们可以使用...
  • 误差反向传播算法(BP网络)

    千次阅读 2020-04-20 09:50:30
    误差反向传播网络(Error Back Propagtion,),简称为BP神经网络,是一种多层神经网络,与之相关的另一个概念是多层感知器(Multi-Layer Perceptron,MLP)。多层感知器除了输入层和输出层以外,还具有若干个隐含层...
  • 移动机器人式里程计

    千次阅读 多人点赞 2020-06-13 18:57:46
    移动机器人灵魂三问:我在哪? 我要去哪里?...与其它定位方案一样,式里程计也需要传感器感知外部信息,只不过,式里程计采用的电机转速测量模块是一种成本非常低廉的传感器。本文对搭建智能小车系统过程.
  • 反向传播算法(过程及公式推导)

    万次阅读 多人点赞 2018-01-29 11:15:27
    一、反向传播的由来 在我们开始DL的研究之前,需要把ANN—人工神经元网络以及bp算法做一个简单解释。 关于ANN的结构,我不再多说,网上有大量的学习资料,主要就是搞清一些名词: 输入层/输入神经元,输出层/输出...
  • 今天介绍随机梯度下降法和反向传播,首先介绍梯度下降法。 1.梯度下降法 梯度下降法是从初始值开始,向函数上当前点对应梯度的反方向的规定步长的距离点进行迭代搜索,最终得到最小值的过程。公式简易推导如下: ...
  • TRIZ 40创新原理

    千次阅读 2018-11-29 10:35:22
    原理1.&nbsp;分割&nbsp;  A、把一个物体分成相互独立的部分为不同材料(如玻璃、纸、铁罐等)的再回收设置不同的回收箱&nbsp;  B、将物体分成容易组装和拆卸的部分组合家具&nbsp;  C提高物体的可...
  • 卷积神经网络的反向传播算法(笔记)

    千次阅读 2017-06-21 11:28:42
    全连接神经网络的反向传播算法前向传播参考连接 列举论文中的公式,并与上图所示流程一一对应: 代价函数: EN=12∑n=1N∑k=1c(tnk−ynk)2 E^N = \frac{1}{2}\sum_{n=1}^{N}\sum_{k=1}^{c} (t_k^n-y_k^n)^2为...
  • 电动车结构及其工作原理

    万次阅读 多人点赞 2018-12-06 18:03:10
    电动车结构及其工作原理电动车定义电动车结构电源系统电力驱动系统整车控制器辅助系统 转自:汽车维修技术网 电动车定义 纯电动汽车是完全由可充电电池(如铅酸电池、镍镉电池、镍氢电池或锂电子电池)提供动力源...
  • 在这期间还有一种便是绑架,呈现ASP,PHP,HTML脚本绑架,反向代理绑架等,反向代理绑架案例典型的便是当年的李毅吧,原理拜见反向代理绑架材料拜见ZAC大牛的博客,脚本绑架原理便是调用写好的脚本,使用政府站或高...
  • SpringBoot 面试专题答案: [图片上传失败…(image-9a13b8-1606481756979)] (2)Spring Boot 核心技术-笔记 Spring Boot入门 配置文件 日志 Web开发 Docker Spring Boot与数据访问 启动配置原理 自定义Starter ...
  • 自己动手写一个反向代理

    千次阅读 2019-06-11 11:16:55
    也可以阅读原文:https://jiajunhuang.com/articles/2019_06_09-write_you_a_simple_reverse_proxy.md.html ...明白了底层原理,才心安。 先来看看frp的架构图,基本上反向代理都是这样的架构。 图片见:h...
  • 在前面我们讲到了DNN,以及DNN的特例CNN的模型和前向反向传播算法。链接如下: 深度学习(一):DNN前向传播算法和反向传播算法 深度学习(二):DNN损失函数和激活函数的选择 深度学习(四):卷积神经网络(CNN)...
  • 今天介绍一下倒排索引,倒排索引又叫反向索引(inverted index),既然有反向索引那就有正向索引(forward index)了。一些相关概念可以看前文信息检索(Information Retrieval)相关概念 1 正向索引和反向索引 先介绍一下...
  • 鼠标滚轮机械编码器原理

    千次阅读 2021-03-16 14:16:44
    鼠标滚轮一旦出现滚动跳动,不连贯,基本都要换,修鼠标会经常遇到,好奇之下想了解一下这个小东西的原理。 滚轮一端插在这个转盘里面,我们滚动滚轮时候,转盘被带动旋转,产生脉冲信号,电脑依靠这个信号判断滚轮...
  • 反向代理服务 概念简介 什么是高可用 高可用HA(High Availability)是分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计减少系统不能提供服务的时间。 假设系统一直能够提供服务,我们说系统的可用性...
  • 你真的理解反向传播吗?面试必备

    千次阅读 2018-04-26 00:00:00
    王小新 编译自 Medium量子位 出品 | 公众号 QbitAI当前,训练机器学习模型的唯一方式是反向传播算法。深度学习框架越来越容易上手,训练一个模型也只需简单几行代...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 13,212
精华内容 5,284
关键字:

反向轮的原理