[已结束]  《全媒体客户中心管理》读书沙龙——武汉站
时间:2016-09-01 14:00
地点:武汉武昌区中北路汉街总部国际B座1504室
活动介绍:
8月3日,国内第一本“全媒体”客服学术著作《全媒体客户中心管理》读书沙龙将在包括北上广深等全国十大城市陆续举办。环信作为国内全媒体智能SaaS客服的倡领者一直致力于推动整个中国SaaS客服行业的发展,给企业提供最具效率和最佳用户体验的客服产品及服务。此次和客服行业权威媒体《客户世界》以及行业专家一起联合推出《全媒体客户中心管理》一书,环信提供了在全媒体客服领域包括产品技术和客户运营方面大量的最佳实践及成功案例。同时,环信参与了从前期图书主题的设定、相关最佳实践和成功案例的准备、中期文档图表的梳理、后期出版环节的编审和呈现,一直到各地读书沙龙的组织工作,目的也是推动整个SaaS客服行业和企业用户少走弯路,加速企业用户的信息化和用户体验进程,让呼叫中心(客户中心)提速迈向智能化和数据化时代。
活动时间:

演讲征集时间

2016-08-01 11:14 - 2016-08-23 11:10

活动举办时间

2016-09-01 14:00 - 2016-09-01 17:00

100% Complete

部分演讲主题展示:
  • 全媒体客户中心的规划、建设、运营与管理

    演讲嘉宾:张艳
    蜜芽服务副总裁  近20年客服中心管理经验,专注于企业自建呼叫中心运营管理、服务管理、会员精准营销。为多家企业搭建、重建呼叫中心,并成功推动呼叫中心从分布到集中、从服务到营销、从单渠道到全媒体转型。先后就职于携程旅行网、中青旅、中移动12580、红孩子、苏宁云商等企业, 现任蜜芽服务副总裁。

    从呼叫中心基础管理工作开始,多年来在电子商务类企业组织实战。张老师将她多年来的理论总结和实战经验加以提炼和归纳,汇集成为新书《全媒体客户中心管理》,期待对国内本行业管理者们的运营工作提供积极的帮助。
    投票
    1
参加活动人员展示:
主办单位:
合作伙伴:
志愿者:

Lory

在校学生
武汉大学

问演讲人:

问题标题: 环信

你的问题可能已经有答案

    问题补充 (选填):

     
    3
    回复

    WebIM加入聊天室代码要怎么写? WebIM聊天室问题 环信

    zhangyb 回复了问题 • 2 人关注 • 74 次浏览 • 2017-01-21 17:32 • 来自相关话题

    1
    回复

    安卓和iOS实时视频的问题,求教 环信

    donghai 回复了问题 • 2 人关注 • 111 次浏览 • 2017-01-14 00:00 • 来自相关话题

    2
    回复

    iOS,环信最新SDK与百度最新SDK冲突 环信 百度地图

    江南孤鹜 回复了问题 • 3 人关注 • 174 次浏览 • 2017-01-12 17:25 • 来自相关话题

    6
    最佳

    iOS环信SDK 3.2版本和百度地图SDK3.2.1冲突闪退 环信 环信 iOS 3.2 环信 iOS集成 iOS 环信

    江南孤鹜 回复了问题 • 3 人关注 • 226 次浏览 • 2017-01-12 17:26 • 来自相关话题

    2
    评论

    环信小程序 Demo源码发布,集成视频手把手教你玩转小程序! 小程序 环信

    beyond 发表了文章 • 3969 次浏览 • 2017-01-11 10:33 • 来自相关话题

       1月9日,向乔布斯致敬的张小龙如约发布了微信小程序,首批上线的小程序就有300多家,一时刷爆朋友圈,如果不转发一两条有关小程序的内容,你都不好意思自称挨踢人。从效率看,小程序成功给APP瘦身,节省了系统资源。以往用户经常面临APP安装繁琐、加载缓慢、吃运存、占空间等痛点,有了小程序,这些问题可以迎刃而解,同时,几乎所有的API可以移植在小程序上,这在很大程度上节约了开发成本和时间,所以小程序的前期投入并不高。

       为了让大家的小程序都能顺利“聊”起来,环信小程序Demo源码也于今日正式发布,也许在“用完即走”基础上“聊两句再走”也是极好的哦!同时,环信工程师们还贴心的为大家准备好了集成使用教程视频,手把手教你玩转小程序。
     
      环信小程序运行视频在线观看

     微信小程序 Demo

    环信准备了微信小程序 Demo,该 Demo 基于 Web IM SDK,并在其基础之上进行了修改。如果您想在您的微信小程序中添加即时通讯的功能,可以参考以下方式集成。 
     
    小程序运行效果







    Demo源码下载

    GitHub下载地址:https://github.com/easemob/webim-weixin-xcx.git 

    或者,执行如下命令: git clone https://github.com/easemob/webim-weixin-xcx.git安装IDE

    进入微信公众平台的官网下载“开发者工具”,并安装。目前支持 windows 64、windows 32、mac 版本。
    微信开发者文档:https://mp.weixin.qq.com/debug ... .html“开发者工具”下载地址:https://mp.weixin.qq.com/debug ... 01715
     配置服务器地址

    登录微信小程序账户,配置以下服务器地址:
    request合法域名 https://a1.easemob.comsocket合法域名 wss://im-api.easemob.comuploadFile合法域名 https://a1.easemob.com
    使用微信小程序 Demo

    使用“微信web开发者工具”打开微信小程序 Demo,步骤如下:
    打开“微信web开发者工具”,使用微信扫一扫授权登录。选择“本地小程序项目 → 添加项目”。填写AppID、项目名称(自定义)、项目目录(本地代码路径),并点击“添加项目”。

    进入“微信小程序Demo”项目后,可以对项目进行编辑、调试。 
     

      查看全部
       1月9日,向乔布斯致敬的张小龙如约发布了微信小程序,首批上线的小程序就有300多家,一时刷爆朋友圈,如果不转发一两条有关小程序的内容,你都不好意思自称挨踢人。从效率看,小程序成功给APP瘦身,节省了系统资源。以往用户经常面临APP安装繁琐、加载缓慢、吃运存、占空间等痛点,有了小程序,这些问题可以迎刃而解,同时,几乎所有的API可以移植在小程序上,这在很大程度上节约了开发成本和时间,所以小程序的前期投入并不高。

       为了让大家的小程序都能顺利“聊”起来,环信小程序Demo源码也于今日正式发布,也许在“用完即走”基础上“聊两句再走”也是极好的哦!同时,环信工程师们还贴心的为大家准备好了集成使用教程视频,手把手教你玩转小程序。
     
      环信小程序运行视频在线观看

     微信小程序 Demo

    环信准备了微信小程序 Demo,该 Demo 基于 Web IM SDK,并在其基础之上进行了修改。如果您想在您的微信小程序中添加即时通讯的功能,可以参考以下方式集成。 
     
    小程序运行效果

    未命名_meitu_0.jpg



    Demo源码下载

    GitHub下载地址:https://github.com/easemob/webim-weixin-xcx.git 

    或者,执行如下命令: 
    git clone https://github.com/easemob/webim-weixin-xcx.git
    安装IDE

    进入微信公众平台的官网下载“开发者工具”,并安装。目前支持 windows 64、windows 32、mac 版本。

     配置服务器地址

    登录微信小程序账户,配置以下服务器地址:

    使用微信小程序 Demo

    使用“微信web开发者工具”打开微信小程序 Demo,步骤如下:
    1. 打开“微信web开发者工具”,使用微信扫一扫授权登录。
    2. 选择“本地小程序项目 → 添加项目”。
    3. 填写AppID、项目名称(自定义)、项目目录(本地代码路径),并点击“添加项目”。


    进入“微信小程序Demo”项目后,可以对项目进行编辑、调试。 
     

     
    0
    评论

    美团热更方案ASM实践 Android 环信 热更新

    beyond 发表了文章 • 363 次浏览 • 2017-01-06 16:57 • 来自相关话题

        美团热更新的文章已经讲了,他们用的是Instant Run的方案。
    这篇文章主要讲美团热更方案中没讲到的部分,包含几个方面:
    作为云服务提供厂商,需要提供给客户SDK,SDK发布后同样要考虑bug修复问题。这里讲一下作为sdk发布者的热更新方案选型,也就是为什么用美团方案&Instant Run方案。美团方案实现的大致结构最后讲一下asm插桩的过程,字节码导读,以及遇到的各种坑。
     方案选择:

      我们公司提供及时通讯服务,同时需要提供给用户方便集成的及时通讯的SDK,每次SDK发布的同时也面临SDK发布后紧急bug的修复问题。 现在市面上的热更新方案通常不适用SDK提供方使用。 以阿里的andFix和微信的tinker为例,都是直接修改并合成新的apk。这样做对于普通app没有问题,但是对于sdk提供方是不可以的,SDK发布者不能够直接修改apk,这个事情只能由app开发者来做。

    tinker方案如图:




    女娲方案,由大众点评Jason Ross实现并开源,他们是在classLoader过程中,將自己的修改后的patch类所在的dex, 插入到dex队列前面,这样在classloader按照类名字加载的时候会优先加载patch类。

    女娲方案如图:




       女娲方案有一个条件约束,就是每个类都要插桩,插入一个类的引用,并且这个被引用类需要打包到单独的dex文件中,这样保证每个类都没有被打上CLASS_ISPREVERIFIED标志。 具体详细描述在早期的hotpatch方案 安卓App热补丁动态修复技术介绍

      作为SDK提供者,只能提供jar包给用户,无法约束用户的dex生成过程,所以女娲方案无法直接应用。 女娲方案是开源的,而且其中提供了asm插桩示例,对于后面应用美团方案有很好参考意义。

    美团&&Instant Run方案

       美团方案 也就是Instant Run的方案基本思路就是在每个函数都插桩,如果一个类存在bug,需要修复,就将插桩点类的changeRedirect字段从null值变成patch类。 基本原理在美团方案中有讲述,但是美团文中没有讲最重要的一个问题,就是如何在每一个函数前面插桩,下面会详细讲一下。 Patch应用部分,这里忽略,因为是java代码,大家可以反编译Instant Run.jar,看一下大致思路,基本都能写出来。

    插桩

       插桩的动作就是在每一个函数前面都插入PatchProxy.isSupport...PatchProxy.accessDisPatch这一系列代码(参看美团方案)。插桩工作直接修改class文件,因为这样不影响正常代码逻辑,只有最后打包发布的时候才进行插桩。
       插桩最常用的是asm.jar。接下来的部分需要用户先了解asm.jar的大致使用流程。了解这个过程最好是找个实例实践一下,光看介绍文章是看不懂的。

       asm有两种方式解析class文件,一种是core API, provides an event based representation of classes,类似解析xml的SAX的事件触发方式,遍历类,以及类的字段,类中的方法,在遍历的过程中会依次触发相应的函数,比如遍历类函数时,触发visitMethod(name, signature...),用户可以在这个方法中修改函数实现。 另外一种 tree API, provides an object based representation,类似解析xml中的DOM树方式。本文中,这里使用了core API方式。asm.jar有对应的manual asm4-guide.pdf,需要仔细研读,了解其用法。

    使用asm.jar把java class反编译为字节码

    反编译为字节码对应的命令是java -classpath "asm-all.jar" org.jetbrains.org.objectweb.asm.util.ASMifier State.class    这个地方有一个坑,官方版本asm.jar 在执行ASMifier命令的时候总是报错,后来在Android Stuidio的目录下面找一个asm-all.jar替换再执行就不出问题了。但是用asm.jar插桩的过程,仍然使用官方提供的asm.jar。
     
    插入前代码:class State {
    long getIndex(int val) {
    return 100;
    }
    }ASMifier反编译后字节码如下mv = cw.visitMethod(0, "getIndex", "(I)J", null, null);
    mv.visitCode();
    mv.visitLdcInsn(new Long(100L));
    mv.visitInsn(LRETURN);
    mv.visitMaxs(2, 2);
    mv.visitEnd();插桩后代码:long getIndex(int a) {
    if ($patch != null) {
    if (PatchProxy.isSupport(new Object[0], this, $patch, false)) {
    return ((Long) com.hyphenate.patch.PatchProxy.accessDispatch(new Object[] {a}, this, $patch, false)).longValue();
    }
    }
    return 100;
    }ASMifier反编译后代码如下:mv = cw.visitMethod(ACC_PUBLIC, "getIndex", "(I)J", null, null);
    mv.visitCode();
    mv.visitFieldInsn(GETSTATIC, "com/hyphenate/State", "$patch", "Lcom/hyphenate/patch/PatchReDirection;");
    Label l0 = new Label();
    mv.visitJumpInsn(IFNULL, l0);
    mv.visitInsn(ICONST_0);
    mv.visitTypeInsn(ANEWARRAY, "java/lang/Object");
    mv.visitVarInsn(ALOAD, 0);
    mv.visitFieldInsn(GETSTATIC, "com/hyphenate/State", "$patch", "Lcom/hyphenate/patch/PatchReDirection;");
    mv.visitInsn(ICONST_0);
    mv.visitMethodInsn(INVOKESTATIC, "com/hyphenate/patch/PatchProxy", "isSupport", "([Ljava/lang/Object;Ljava/lang/Object;Lcom/hyphenate/patch/PatchReDirection;Z)Z", false);
    mv.visitJumpInsn(IFEQ, l0);
    mv.visitIntInsn(BIPUSH, 1);
    mv.visitTypeInsn(ANEWARRAY, "java/lang/Object");
    mv.visitInsn(DUP);
    mv.visitIntInsn(BIPUSH, 0);
    mv.visitVarInsn(ILOAD, 1);
    mv.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", false);
    mv.visitInsn(AASTORE);
    mv.visitVarInsn(ALOAD, 0);
    mv.visitFieldInsn(GETSTATIC, "com/hyphenate/State", "$patch", "Lcom/hyphenate/patch/PatchReDirection;");
    mv.visitInsn(ICONST_0);
    mv.visitMethodInsn(INVOKESTATIC, "com/hyphenate/patch/PatchProxy", "accessDispatch", "([Ljava/lang/Object;Ljava/lang/Object;Lcom/hyphenate/patch/PatchReDirection;Z)Ljava/lang/Object;", false);
    mv.visitTypeInsn(CHECKCAST, "java/lang/Long");
    mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Long", "longValue", "()J", false);
    mv.visitInsn(LRETURN);
    mv.visitLabel(l0);
    mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
    mv.visitLdcInsn(new Long(100L));
    mv.visitInsn(LRETURN);
    mv.visitMaxs(4, 2);
    mv.visitEnd();对于插桩程序来说,需要做的就是把差异部分插桩到代码中​
     
       需要将全部入參传递给patch方法,插入的代码因此会根据入參进行调整,同时也要处理返回值.

       可以观察上面代码,上面的例子显示了一个int型入參a,装箱变成Integer,放在一个Object[]数组中,先后调用isSupport和accessDispatch,传递给patch类的对应方法,patch返回类型是Long,然后调用longValue,拆箱变成long类型。

       对于普通的java对象,因为均派生自Object,所以对象的引用直接放在数组中;对于primitive类型(包括int, long, float....)的处理,需要先调用Integer, Boolean, Float等java对象的构造函数,将primitive类型装箱后作为object对象放在数组中。

       如果原来函数返回结果的是primitive类型,需要插桩代码将其转化为primitive类型。还要处理数组类型,和void类型。 java的primitive类型在 java virtual machine specification中有定义。
     
       这个插入过程有两个关键问题,一个是函数signature的解析,另外一个是适配这个参数变化插入代码。下面详细解释下:@Override
    public MethodVisitor visitMethod(int access, String name, String desc,
    String signature, String[] exceptions) {   这个函数是asm.jar访问类函数时触发的事件,desc变量对应java jni中的signature,比如这里是'(I)J', 需要解析并转换成primitive类型,类,数组,void。这部分代码参考了android底层的源码libcore/luni/src/main/java/libcore/reflect,和sun java的SignatureParser.java,都有反映了这个遍历过程。

       关于java字节码的理解,汇编指令主要是看 Java bytecode instruction listings

       理解java字节码,需要理解jvm中的栈的结构。JVM是一个基于栈的架构。方法执行的时候(包括main方法),在栈上会分配一个新的帧,这个栈帧包含一组局部变量。这组局部变量包含了方法运行过程中用到的所有变量,包括this引用,所有的方法参数,以及其它局部定义的变量。对于类方法(也就是static方法)来说,方法参数是从第0个位置开始的,而对于实例方法来说,第0个位置上的变量是this指针。引自: Java字节码浅析

    分析中间部分字节码实现,com.hyphenate.patch.PatchProxy.accessDispatch(new Object[] {a}, this, $patch, false))对应字节码如下,请对照Java bytecode instruction listings中每条指令观察对应栈帧的变化,下面注释中'[]'中表示栈帧中的内容。
    mv.visitIntInsn(BIPUSH, 1); # 数字1入栈,对应new Object[1]数组长度1。 栈:[1]
    mv.visitTypeInsn(ANEWARRAY, "java/lang/Object"); # ANEWARRY:count(1) → arrayref, 栈:[arr_ref]
    mv.visitInsn(DUP); # 栈:[arr_ref, arr_ref]
    mv.visitIntInsn(BIPUSH, 0); # 栈:[arr_ref, arr_ref, 0]
    mv.visitVarInsn(ILOAD, 1); # 局部变量位置1的内容入栈, 栈:[arr_ref, arr_ref, 0, a]
    mv.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", false); # 调用Integer.valueOf, INVOKESTATIC: [arg1, arg2, ...] → result, 栈:[arr_ref, arr_ref, 0, integerObjectOf_a]
    mv.visitInsn(AASTORE); # store a reference into array: arrayref, index, value →, 栈:[arr_ref]
    mv.visitVarInsn(ALOAD, 0); # this入栈,栈:[arr_ref, this]
    mv.visitFieldInsn(GETSTATIC, "com/hyphenate/State", "$patch", "Lcom/hyphenate/patch/PatchReDirection;"); #$patch入栈,栈:[arr_ref, this, $patch]
    mv.visitInsn(ICONST_0); #false入栈, # 栈:[arr_ref, this, $patch, false]
    mv.visitMethodInsn(INVOKESTATIC, "com/hyphenate/patch/PatchProxy", "accessDispatch", "([Ljava/lang/Object;Ljava/lang/Object;Lcom/hyphenate/patch/PatchReDirection;Z)Ljava/lang/Object;", false); # 调用accessDispatch, 栈包含返回结果,栈:[longObject]熟悉上面的字节码以及对应的栈帧变化,也就掌握了插桩过程。
     
    坑:

       ClassVisitor.visitMethod()中access如果是ACC_SYNTHETIC或者ACC_BRIDGE,插桩后无法正常运行。ACC_SYNTHETIC表示函数由javac自动生成的,enum类型就会产生这种类型的方法,不需要插桩,直接略过。因为观察到模版类也会产生ACC_SYNTHETIC,所以插桩过程跳过了模版类。

    ClassVisitor.visit()函数对应遍历到类触发的事件,access如果是ACC_INTERFACE或者ACC_ENUM,无需插桩。简单说就是接口和enum不涉及方法修改,无需插桩。

    静态方法的实现和普通类成员函数略有出入,对于汇编程序来说,本地栈的第一个位置,如果是普通方法,会存储this引用,static方法没有this,这里稍微调整一下就可以实现的。

    不定参数由于要求连续输入的参数类型相同,被编译器直接变成了数组,所以对本程序没有造成影响。

    大小:

       插桩因为对每个函数都插桩,反编译后看实际上增加了大量代码,甚至可以说插入的代码比原有的代码还要多。但是实际上最终生成的jar包增长了大概20%多一点,并没有想的那么多,在可接受范围内。因为class所占的空间不止是代码部分,还包括类描述,字段描述,方法描述,const-pool等,代码段只占其中的不到一半。可以参考[The class File Format](link @http://docs.oracle.com/javase/ ... 4.html)

    讨论

       前面代码插桩的部分和美团热更文章中保持一致,实际上还有些细节还可以调整。isSupport这个函数的参数可以调整如下if (PatchProxy.isSupport(“getIndex”, "(I)J", false)) {这样能减小插桩部分代码,而且可以区分名字相同的函数。

    PatchProxy.isSupport最后一个参数表示是普通类函数还是static函数,这个是方便java应用patch的时候处理。

    源码地址
    https://github.com/easemob/empatch
     作者:李楠
    公司:环信
    关注领域:Android开发
    文章署名: greenmemo 查看全部
        美团热更新的文章已经讲了,他们用的是Instant Run的方案。
    这篇文章主要讲美团热更方案中没讲到的部分,包含几个方面:
    1. 作为云服务提供厂商,需要提供给客户SDK,SDK发布后同样要考虑bug修复问题。这里讲一下作为sdk发布者的热更新方案选型,也就是为什么用美团方案&Instant Run方案。
    2. 美团方案实现的大致结构
    3. 最后讲一下asm插桩的过程,字节码导读,以及遇到的各种坑。

     方案选择:

      我们公司提供及时通讯服务,同时需要提供给用户方便集成的及时通讯的SDK,每次SDK发布的同时也面临SDK发布后紧急bug的修复问题。 现在市面上的热更新方案通常不适用SDK提供方使用。 以阿里的andFix和微信的tinker为例,都是直接修改并合成新的apk。这样做对于普通app没有问题,但是对于sdk提供方是不可以的,SDK发布者不能够直接修改apk,这个事情只能由app开发者来做。

    tinker方案如图:
    图片1.png

    女娲方案,由大众点评Jason Ross实现并开源,他们是在classLoader过程中,將自己的修改后的patch类所在的dex, 插入到dex队列前面,这样在classloader按照类名字加载的时候会优先加载patch类。

    女娲方案如图:
    图片2.png

       女娲方案有一个条件约束,就是每个类都要插桩,插入一个类的引用,并且这个被引用类需要打包到单独的dex文件中,这样保证每个类都没有被打上CLASS_ISPREVERIFIED标志。 具体详细描述在早期的hotpatch方案 安卓App热补丁动态修复技术介绍

      作为SDK提供者,只能提供jar包给用户,无法约束用户的dex生成过程,所以女娲方案无法直接应用。 女娲方案是开源的,而且其中提供了asm插桩示例,对于后面应用美团方案有很好参考意义。

    美团&&Instant Run方案

       美团方案 也就是Instant Run的方案基本思路就是在每个函数都插桩,如果一个类存在bug,需要修复,就将插桩点类的changeRedirect字段从null值变成patch类。 基本原理在美团方案中有讲述,但是美团文中没有讲最重要的一个问题,就是如何在每一个函数前面插桩,下面会详细讲一下。 Patch应用部分,这里忽略,因为是java代码,大家可以反编译Instant Run.jar,看一下大致思路,基本都能写出来。

    插桩

       插桩的动作就是在每一个函数前面都插入PatchProxy.isSupport...PatchProxy.accessDisPatch这一系列代码(参看美团方案)。插桩工作直接修改class文件,因为这样不影响正常代码逻辑,只有最后打包发布的时候才进行插桩。
       插桩最常用的是asm.jar。接下来的部分需要用户先了解asm.jar的大致使用流程。了解这个过程最好是找个实例实践一下,光看介绍文章是看不懂的。

       asm有两种方式解析class文件,一种是core API, provides an event based representation of classes,类似解析xml的SAX的事件触发方式,遍历类,以及类的字段,类中的方法,在遍历的过程中会依次触发相应的函数,比如遍历类函数时,触发visitMethod(name, signature...),用户可以在这个方法中修改函数实现。 另外一种 tree API, provides an object based representation,类似解析xml中的DOM树方式。本文中,这里使用了core API方式。asm.jar有对应的manual asm4-guide.pdf,需要仔细研读,了解其用法。

    使用asm.jar把java class反编译为字节码

    反编译为字节码对应的命令是
    java -classpath "asm-all.jar"   org.jetbrains.org.objectweb.asm.util.ASMifier State.class 
       这个地方有一个坑,官方版本asm.jar 在执行ASMifier命令的时候总是报错,后来在Android Stuidio的目录下面找一个asm-all.jar替换再执行就不出问题了。但是用asm.jar插桩的过程,仍然使用官方提供的asm.jar。
     
    插入前代码:
    class State {
    long getIndex(int val) {
    return 100;
    }
    }
    ASMifier反编译后字节码如下
    mv = cw.visitMethod(0, "getIndex", "(I)J", null, null);
    mv.visitCode();
    mv.visitLdcInsn(new Long(100L));
    mv.visitInsn(LRETURN);
    mv.visitMaxs(2, 2);
    mv.visitEnd();
    插桩后代码:
    long getIndex(int a) {
    if ($patch != null) {
    if (PatchProxy.isSupport(new Object[0], this, $patch, false)) {
    return ((Long) com.hyphenate.patch.PatchProxy.accessDispatch(new Object[] {a}, this, $patch, false)).longValue();
    }
    }
    return 100;
    }
    ASMifier反编译后代码如下:
    mv = cw.visitMethod(ACC_PUBLIC, "getIndex", "(I)J", null, null);
    mv.visitCode();
    mv.visitFieldInsn(GETSTATIC, "com/hyphenate/State", "$patch", "Lcom/hyphenate/patch/PatchReDirection;");
    Label l0 = new Label();
    mv.visitJumpInsn(IFNULL, l0);
    mv.visitInsn(ICONST_0);
    mv.visitTypeInsn(ANEWARRAY, "java/lang/Object");
    mv.visitVarInsn(ALOAD, 0);
    mv.visitFieldInsn(GETSTATIC, "com/hyphenate/State", "$patch", "Lcom/hyphenate/patch/PatchReDirection;");
    mv.visitInsn(ICONST_0);
    mv.visitMethodInsn(INVOKESTATIC, "com/hyphenate/patch/PatchProxy", "isSupport", "([Ljava/lang/Object;Ljava/lang/Object;Lcom/hyphenate/patch/PatchReDirection;Z)Z", false);
    mv.visitJumpInsn(IFEQ, l0);
    mv.visitIntInsn(BIPUSH, 1);
    mv.visitTypeInsn(ANEWARRAY, "java/lang/Object");
    mv.visitInsn(DUP);
    mv.visitIntInsn(BIPUSH, 0);
    mv.visitVarInsn(ILOAD, 1);
    mv.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", false);
    mv.visitInsn(AASTORE);
    mv.visitVarInsn(ALOAD, 0);
    mv.visitFieldInsn(GETSTATIC, "com/hyphenate/State", "$patch", "Lcom/hyphenate/patch/PatchReDirection;");
    mv.visitInsn(ICONST_0);
    mv.visitMethodInsn(INVOKESTATIC, "com/hyphenate/patch/PatchProxy", "accessDispatch", "([Ljava/lang/Object;Ljava/lang/Object;Lcom/hyphenate/patch/PatchReDirection;Z)Ljava/lang/Object;", false);
    mv.visitTypeInsn(CHECKCAST, "java/lang/Long");
    mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Long", "longValue", "()J", false);
    mv.visitInsn(LRETURN);
    mv.visitLabel(l0);
    mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
    mv.visitLdcInsn(new Long(100L));
    mv.visitInsn(LRETURN);
    mv.visitMaxs(4, 2);
    mv.visitEnd();
    对于插桩程序来说,需要做的就是把差异部分插桩到代码中​
     
       需要将全部入參传递给patch方法,插入的代码因此会根据入參进行调整,同时也要处理返回值.

       可以观察上面代码,上面的例子显示了一个int型入參a,装箱变成Integer,放在一个Object[]数组中,先后调用isSupport和accessDispatch,传递给patch类的对应方法,patch返回类型是Long,然后调用longValue,拆箱变成long类型。

       对于普通的java对象,因为均派生自Object,所以对象的引用直接放在数组中;对于primitive类型(包括int, long, float....)的处理,需要先调用Integer, Boolean, Float等java对象的构造函数,将primitive类型装箱后作为object对象放在数组中。

       如果原来函数返回结果的是primitive类型,需要插桩代码将其转化为primitive类型。还要处理数组类型,和void类型。 java的primitive类型在 java virtual machine specification中有定义。
     
       这个插入过程有两个关键问题,一个是函数signature的解析,另外一个是适配这个参数变化插入代码。下面详细解释下:
    @Override
    public MethodVisitor visitMethod(int access, String name, String desc,
    String signature, String[] exceptions) {
       这个函数是asm.jar访问类函数时触发的事件,desc变量对应java jni中的signature,比如这里是'(I)J', 需要解析并转换成primitive类型,类,数组,void。这部分代码参考了android底层的源码libcore/luni/src/main/java/libcore/reflect,和sun java的SignatureParser.java,都有反映了这个遍历过程。

       关于java字节码的理解,汇编指令主要是看 Java bytecode instruction listings

       理解java字节码,需要理解jvm中的栈的结构。JVM是一个基于栈的架构。方法执行的时候(包括main方法),在栈上会分配一个新的帧,这个栈帧包含一组局部变量。这组局部变量包含了方法运行过程中用到的所有变量,包括this引用,所有的方法参数,以及其它局部定义的变量。对于类方法(也就是static方法)来说,方法参数是从第0个位置开始的,而对于实例方法来说,第0个位置上的变量是this指针。引自: Java字节码浅析

    分析中间部分字节码实现,
    com.hyphenate.patch.PatchProxy.accessDispatch(new Object[] {a}, this, $patch, false))
    对应字节码如下,请对照Java bytecode instruction listings中每条指令观察对应栈帧的变化,下面注释中'[]'中表示栈帧中的内容。
    mv.visitIntInsn(BIPUSH, 1);  # 数字1入栈,对应new Object[1]数组长度1。 栈:[1]
    mv.visitTypeInsn(ANEWARRAY, "java/lang/Object"); # ANEWARRY:count(1) → arrayref, 栈:[arr_ref]
    mv.visitInsn(DUP); # 栈:[arr_ref, arr_ref]
    mv.visitIntInsn(BIPUSH, 0); # 栈:[arr_ref, arr_ref, 0]
    mv.visitVarInsn(ILOAD, 1); # 局部变量位置1的内容入栈, 栈:[arr_ref, arr_ref, 0, a]
    mv.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", false); # 调用Integer.valueOf, INVOKESTATIC: [arg1, arg2, ...] → result, 栈:[arr_ref, arr_ref, 0, integerObjectOf_a]
    mv.visitInsn(AASTORE); # store a reference into array: arrayref, index, value →, 栈:[arr_ref]
    mv.visitVarInsn(ALOAD, 0); # this入栈,栈:[arr_ref, this]
    mv.visitFieldInsn(GETSTATIC, "com/hyphenate/State", "$patch", "Lcom/hyphenate/patch/PatchReDirection;"); #$patch入栈,栈:[arr_ref, this, $patch]
    mv.visitInsn(ICONST_0); #false入栈, # 栈:[arr_ref, this, $patch, false]
    mv.visitMethodInsn(INVOKESTATIC, "com/hyphenate/patch/PatchProxy", "accessDispatch", "([Ljava/lang/Object;Ljava/lang/Object;Lcom/hyphenate/patch/PatchReDirection;Z)Ljava/lang/Object;", false); # 调用accessDispatch, 栈包含返回结果,栈:[longObject]
    熟悉上面的字节码以及对应的栈帧变化,也就掌握了插桩过程。
     
    坑:

       ClassVisitor.visitMethod()中access如果是ACC_SYNTHETIC或者ACC_BRIDGE,插桩后无法正常运行。ACC_SYNTHETIC表示函数由javac自动生成的,enum类型就会产生这种类型的方法,不需要插桩,直接略过。因为观察到模版类也会产生ACC_SYNTHETIC,所以插桩过程跳过了模版类。

    ClassVisitor.visit()函数对应遍历到类触发的事件,access如果是ACC_INTERFACE或者ACC_ENUM,无需插桩。简单说就是接口和enum不涉及方法修改,无需插桩。

    静态方法的实现和普通类成员函数略有出入,对于汇编程序来说,本地栈的第一个位置,如果是普通方法,会存储this引用,static方法没有this,这里稍微调整一下就可以实现的。

    不定参数由于要求连续输入的参数类型相同,被编译器直接变成了数组,所以对本程序没有造成影响。

    大小:

       插桩因为对每个函数都插桩,反编译后看实际上增加了大量代码,甚至可以说插入的代码比原有的代码还要多。但是实际上最终生成的jar包增长了大概20%多一点,并没有想的那么多,在可接受范围内。因为class所占的空间不止是代码部分,还包括类描述,字段描述,方法描述,const-pool等,代码段只占其中的不到一半。可以参考[The class File Format](link @http://docs.oracle.com/javase/ ... 4.html)

    讨论

       前面代码插桩的部分和美团热更文章中保持一致,实际上还有些细节还可以调整。isSupport这个函数的参数可以调整如下if (PatchProxy.isSupport(“getIndex”, "(I)J", false)) {这样能减小插桩部分代码,而且可以区分名字相同的函数。

    PatchProxy.isSupport最后一个参数表示是普通类函数还是static函数,这个是方便java应用patch的时候处理。

    源码地址
    https://github.com/easemob/empatch
     作者:李楠
    公司:环信
    关注领域:Android开发
    文章署名: greenmemo
    1
    回复

    环信创建聊天室一直返回500 环信

    回复

    15015070064@163.com 回复了问题 • 1 人关注 • 95 次浏览 • 2017-01-05 22:14 • 来自相关话题

    0
    评论

    2016年度盘点:一家典型互联网公司的必备工具宝箱 相关推荐 行业活动 环信

    beyond 发表了文章 • 306 次浏览 • 2017-01-04 14:25 • 来自相关话题

    2016年刚刚过去,2017年已经来临。在过去的一年,作为互联网从业人员的你肯定和我有同样的感受:无论是产品研发、市场还是运营人员,大家在工作中使用的工具产品越来越多,对工具的依赖程度也越来越重。值此岁末年初之际,现在就盘点一下2016年互联网公司最常用的20款工具产品及服务。正是在这些工具的助力下,众多互联网公司才能以如此矫健的步伐跨入这充满希望的2017年,而没有倒在2016年的资本寒冬里。
     
    ● 即时通讯云服务商:环信






    微信、陌陌引领了社交媒体的爆发后,移动社交已成使用最多的产品功能之一,社交不仅可以让用户更具黏性, 越来越多的企业倾向在App上添加社交功能。但IM底层开发繁重的问题,也是让很多企业望而却步。
    环信作为集成通讯云服务商,通过云端开放的 Rest API 和客户端 SDK 包的方式让App内置聊天功能和以前网页中嵌入分享功能一样简单。不必为基础功能组件耗费自己太多精力,为开发者省钱省力,更是加速了APP的上线速度。

    产品官网:http://www.easemob.com/product/im 
    ●产品原型设计工具:墨刀




    身为一名PM,画原型写文档是我们的看家必备技能了,在寻觅尝试了众多国内外原型工具后,我们锁定了墨刀这款利器,一用上便爱不释手。

    墨刀采用拖拽式的原型制作和交互方式,十分钟即可上手,学习成本非常低。墨刀的功能非常强大:用墨刀可以完成从原型制作-交互动效-团队协同作业-实时分析查看效果-跟踪团队及用户反馈整个产品开发流程中大部分让人挠墙的问题。实时扫一扫预览、打点评论,不仅方便真机演示,还大大降低了沟通协作成本。

    产品官网:https://modao.cc/

    ●企业的云端文档协作平台:WPS云文档​




    文案校稿时,需要通过QQ/邮件在多人间反复往返发送稿件,异常繁琐低效;团队资料到处都是,需要时常常找不到;团队资料无法全平台同步,无法移动办公。如果你也在被这些问题困扰,不妨试试WPS云文档。

    WPS云文档是一个企业文档的存储、共享与协作平台,支持多人同时编辑一个文档、文档内评论和历史版本还原等功能。团队可将文档资料全部存储在云端,方便查找与管理。云文档能满足不同人群的文档协作需求,产品研发用它协作撰写需求文档、管理项目进度和搜集bug;运营团队可以多人在同一个文档里协作撰稿与校稿。WPS云文档既有免费版也有付费版。


    产品官网:https://drive.wps.cn/landing.html

    ●思维导图工具:MindManager​




    MindManager是一款专业思维导图工具,它可以将你的思想、策略及商务信息转换为行为导图,让你直观感受整个进度。它可以帮助你进行项目管理、头脑风暴、记录笔记、演示演讲,无论你是商务人员、老师还是学生,Mindmanager图文并茂的展示形式都可以为你提供高效清晰的思维方式。MindManager有免费试用版和付费版,试用30日后可付费购买使用。

    产品官网:http://www.mindmanager.cc

    ●表单与联系人管理工具:麦客CRM​




    作为现代企业营销工作者,我们需要更加有理有据地管理市场活动、品牌建设和线上线下的营销行为,了解这些工作能为企业带来的潜在客户,他们都是谁,他们在哪里,他们的质量高低。

    麦客CRM能满足你在营销、获客和客户管理方面的需求。麦客是一款在线表单制作工具,同时也是强大的客户信息处理和关系管理系统,可以帮助你轻松完成信息收集与整理,实现客户挖掘与消息推送,并开展持续营销。

    产品官网:www.mikecrm.com

    ●在线图形设计工具:创客贴​




    身为一个新媒体运营,老板指望我三头六臂,八项全能:开会要做ppt,得简洁美观; 
    文案不能干巴巴,要丰富有趣;热点追图、双微首图,必须好看刺激…还好有创客贴。

    创客贴是一款极简的网站式平面设计工具,解决了大多数人的设计痛点,让不会使用专业制图软件的运营人员也能快速制作出自己想要的图片:可使用平台提供的大量图片、字体和模板等素材,通过简单的拖拉拽操作,就可以轻松设计出精美的海报、PPT、公号文章首图等图片。另有团队协作版和ipad版会让你的做图工作更加直接高效。

    产品官网:https://www.chuangkit.com


    ●用户行为分析工具:神策分析​




    近期哪个渠道用户注册量最高?变化趋势如何?新增的注册引导流程是否提升用户粘性……作为产品运营的你如果想知道这些问题的答案,那么可以使用神策分析。

    神策分析是一款可以私有化部署的用户行为分析产品,致力帮助用户实现数据驱动。在保障数据安全的前提下,产品实现秒级数据导入与查询,带来灵活的PaaS平台,并针对多业务场景提供专业的数据分析服务,为业务决策提供数据支撑。

    产品官网:https://www.sensorsdata.cn/

    ●商业管理云:数据观​





    作为企业运营管理部门,需要每天关注各渠道的运营数据(用户、推广、客服、营销)等,整合这些数据孤岛很耗费人力和时间成本,我们需要一个实时的可视化看板以了解企业的运营动态,然后才能针对企业运营过程中的问题及时做出决策。
    数据观可以把我们的注册用户、百度推广、在线客服、微信、微博等数据全部关联起来,并基于全量数据进行可视化分析,以便我们实时、准确的了解企业的运营状况。现在公司的销售、市场、运营都在用数据观做日常的业务分析。

    产品官网:www.shujuguan.cn

    ●图文排版工具:135编辑器​




    内容运营每天都有大量的工作要做,要追热点,写原创,上午想着如何涨粉,下午想着如何促活。在做好内容的同时,如何快速做出既漂亮又有自己独特风格的排版,是很多内容运营的目标。

    135编辑器是一款在线图文排版工具,功能比较全面,操作也非常简单。比如一键导入,多平台发布,一键排版和定时群发等大大节约了时间。样式库丰富,自己排列组合自定义为模板,形成自己的风格。企业用户也可以将其嵌入企业内部系统,提升企业排版能力。

    产品官网:www.135editor.com

    ●深度链接企业服务商:LinkedME​





    当今,每个移动APP都是独立的,内容和服务之间的链接消失,应用搜索断裂,APP的内容被局限在每个APP内。我们能在电脑网页所监测的用户意图、广告曝光和其它盈利指标都不能在移动端很好进行。

    LinkedME打破了束缚我们的APP孤岛,它是一个企业级深度链接服务平台,可以帮忙APP解决用户增长和流量变现问题。它旗下的Linkpage提供APP一键直达和推广渠道监测服务,帮助APP企业获取社交媒体和广告曝光用户,提高运营转化,优化渠道投放策略。

    产品官网:https://www.linkedme.cc/ 

    ●SEM优化工具:九枝兰​




    搜索竞价推广真不是人干的活儿。单是最基础的调价,一个关键词优化师就需要管理1440种价格!如果以5万关键词的账户为例,每天每个关键词调价1440次,每天竟然需要进行7200万次调价!一个优化师即使有洪荒之力,也极难完成。

    于是九枝兰SEM优化工具应运而生,解决SEM优化师力所不能及的事,在需要发挥人的聪明才智的方向投入精力,如:拓词、创意、着陆页优化。

    产品官网:http://www.jiuzhilan.com/sem-tool-highlights/ 

    ●广告效果监测与分析平台:【友盟+】U-ADplus​




    U-ADplus广效监测是【友盟+】旗下聚焦营销全链路的第三方数据服务。其中AppTrack是面向App广告主推出的监测与分析平台。它能满足不同类型使用者的推广监测需求。提供基础、电商、游戏应用场景,使用者可根据想监控的指标选择适合的应用场景。支持多样的推广形式,不管是广告平台、信息流广告还是广告联盟 ,它都可以帮使用者监控推广效果。除了能监控推广点击和激活,还能监测用户注册、登陆、行为和付费,让使用者真正了解推广带来用户的转化情况。

    产品官网:https://at.umeng.com/fuia0z

    ●企业消费与报销管理平台:易快报​





    起初臃肿冗长的报销流程令我们非常头疼,很难进行费用管控。使用易快报后才深切感受到了原来报销流程也可以如此便捷。易快报打通了从提交申请到支付的全部环节,使员工报销的时间大为缩短,还能对企业进行实时全程费用控制,效率提高了不少。

    易快报是一个敏捷的企业消费和报销管理平台,面向企业提供专业的订购、费控和报销管理服务。据官网公布的数据,公司目前已经为超过5万家企业客户和220万个人用户提供了这种全新的报销服务。

    产品官网:https://www.ekuaibao.com/

    ●快速建站服务:友好速搭​




    要做电商,渠道是个问题,入驻各大平台还不够,要有自己的独立的门户网站,但创业初期,资金有限,技术太贵,维护太难,加上服务器配置、域名备案一堆事,一想头就大,有了友好速搭,60 秒轻松搭建门户网站,就算是技术小白,也能做出稳定可靠的官网。

    友好速搭可以为企业提供了一站式建站服务,集成域名、DNS、安全证书等基础设施,提供SaaS 建站系统,开放全部API并提供互联网、营销、传媒等资源服务,目前已服务超过2.3万个品牌。

    产品官网:youhaosuda.com

    ●移动应用分发与监控平台:酷传​




    酷传是一个一站式APP发布及监控平台。开发者不需要添加任何SDK,即可通过酷传把自己的APP同时上架到30家主流安卓应用商店,后续还可以实时跟进各个商店的审核进度,不再需要运营人员一一去各家应用商店进行操作;通过酷传的监控产品,还可以查询一百多万款APP的各项数据表现,目前已经支持安卓和IOS两个平台十多家应用商店。

    产品官网:www.coolchuan.com

    ●企业级云服务商​​​​​​​:七牛云​





    对于有大量数据存储需求的互联网公司,现在不仅仅是把数据托管到云存储供应商就够了,围绕数据展开的一站式的服务,成为当下互联网迫切所需。
    七牛云作为企业级云服务商,除了存储、CDN加速服务,和完整的直播云解决方案,围绕数据还有许多玩法。存储在七牛的数据,不需要下载下来,就可以进行批量加水印、裁剪、反垃圾等处理。通过提供稳定、高效、可信赖的底层服务,让客户能集中精力在自身业务的实现和创新上。
     
    产品官网:www.qiniu.com
     
    ● 应用性能监测平台:OneAPM
     





    网络访问缓慢?用户无法登录?接口突然失效?你是否也经常由于很多IT系统故障的原因,眼睁睁地看着用户流失?作为一个应用性能监测平台,OneAPM可以帮助你预先发现性能问题。OneAPM目前支持Java、PHP、Ruby、.Net、Python等多种编程语言,同时也支持iOS 和Android操作系统。它可以帮你实时抓取缓慢的程序代码和SQL语句,让你的应用运行更加流畅、稳定。

    产品官网:http://www.oneapm.com/


    ●APP测试服务商:Testin云测​




    移动互联网的竞争越来越激烈,迫使互联网公司必须要根据用户需求快速对APP进行迭代。每次迭代过程中最担心的是出现Bug,伤害到用户体验可能会是导致用户流失的重要原因。

    Testin专注于面向全球范围内的移动互联网应用开发者,如移动APP开发者、移动手机游戏开发商及互联网+相关移动应用企业提供“一站式测试服务”。包括从移动应用内测到功能测试、性能测试、兼容测试及移动应用发布后持续质量监控,解决APP终端在功能、性能、碎片化、兼容性、稳定性等广大移动互联网企业及开发者不易克服的难题。Testin一站式测试服务覆盖开发者从开发完成到版本迭代的全过程。

    产品官网:www.testin.cn

    ●推送技术和大数据服务商:个推​




    “这么大个红包,再不使用就过期啦!”、“美妆热搜销量王,全场五折起”,收到这样的消息,你肯定也会忍不住打开一探究竟。通过小小的消息,传递用户所需的信息,提高用户活跃度和留存率,这就是推送技术的力量。

    个推所做的就是搭建APP与用户沟通的桥梁,确保消息的毫秒级到达,用户即便足不出户,也能第一时间尽知天下事。个推给用户分群组、打标签,通过大数据分析送让合适的消息找到合适的用户。个推提供免费推送和VIP增值服务,并为各垂直领域提供专业大数据解决方案,目前已服务于50万APP,SDK累计接入用户数超过130亿。

    产品官网:http://www.getui.com/

    希望大家所在的公司都能在上面这20款工具服务的助力下在2017年继续高歌猛进,越跑越快。
      查看全部
    图片1.png

    2016年刚刚过去,2017年已经来临。在过去的一年,作为互联网从业人员的你肯定和我有同样的感受:无论是产品研发、市场还是运营人员,大家在工作中使用的工具产品越来越多,对工具的依赖程度也越来越重。值此岁末年初之际,现在就盘点一下2016年互联网公司最常用的20款工具产品及服务。正是在这些工具的助力下,众多互联网公司才能以如此矫健的步伐跨入这充满希望的2017年,而没有倒在2016年的资本寒冬里。
     
    ● 即时通讯云服务商:环信

    3DSB`HKV@VPSB6Y7LN~KH@B.jpg


    微信、陌陌引领了社交媒体的爆发后,移动社交已成使用最多的产品功能之一,社交不仅可以让用户更具黏性, 越来越多的企业倾向在App上添加社交功能。但IM底层开发繁重的问题,也是让很多企业望而却步。
    环信作为集成通讯云服务商,通过云端开放的 Rest API 和客户端 SDK 包的方式让App内置聊天功能和以前网页中嵌入分享功能一样简单。不必为基础功能组件耗费自己太多精力,为开发者省钱省力,更是加速了APP的上线速度。

    产品官网:http://www.easemob.com/product/im 
    ●产品原型设计工具:墨刀
    图片2.png

    身为一名PM,画原型写文档是我们的看家必备技能了,在寻觅尝试了众多国内外原型工具后,我们锁定了墨刀这款利器,一用上便爱不释手。

    墨刀采用拖拽式的原型制作和交互方式,十分钟即可上手,学习成本非常低。墨刀的功能非常强大:用墨刀可以完成从原型制作-交互动效-团队协同作业-实时分析查看效果-跟踪团队及用户反馈整个产品开发流程中大部分让人挠墙的问题。实时扫一扫预览、打点评论,不仅方便真机演示,还大大降低了沟通协作成本。

    产品官网:https://modao.cc/

    ●企业的云端文档协作平台:WPS云文档​
    图片3.png

    文案校稿时,需要通过QQ/邮件在多人间反复往返发送稿件,异常繁琐低效;团队资料到处都是,需要时常常找不到;团队资料无法全平台同步,无法移动办公。如果你也在被这些问题困扰,不妨试试WPS云文档。

    WPS云文档是一个企业文档的存储、共享与协作平台,支持多人同时编辑一个文档、文档内评论和历史版本还原等功能。团队可将文档资料全部存储在云端,方便查找与管理。云文档能满足不同人群的文档协作需求,产品研发用它协作撰写需求文档、管理项目进度和搜集bug;运营团队可以多人在同一个文档里协作撰稿与校稿。WPS云文档既有免费版也有付费版。


    产品官网:https://drive.wps.cn/landing.html

    ●思维导图工具:MindManager​
    图片4.png

    MindManager是一款专业思维导图工具,它可以将你的思想、策略及商务信息转换为行为导图,让你直观感受整个进度。它可以帮助你进行项目管理、头脑风暴、记录笔记、演示演讲,无论你是商务人员、老师还是学生,Mindmanager图文并茂的展示形式都可以为你提供高效清晰的思维方式。MindManager有免费试用版和付费版,试用30日后可付费购买使用。

    产品官网:http://www.mindmanager.cc

    ●表单与联系人管理工具:麦客CRM​
    图片5.png

    作为现代企业营销工作者,我们需要更加有理有据地管理市场活动、品牌建设和线上线下的营销行为,了解这些工作能为企业带来的潜在客户,他们都是谁,他们在哪里,他们的质量高低。

    麦客CRM能满足你在营销、获客和客户管理方面的需求。麦客是一款在线表单制作工具,同时也是强大的客户信息处理和关系管理系统,可以帮助你轻松完成信息收集与整理,实现客户挖掘与消息推送,并开展持续营销。

    产品官网:www.mikecrm.com

    ●在线图形设计工具:创客贴​
    图片6.png

    身为一个新媒体运营,老板指望我三头六臂,八项全能:开会要做ppt,得简洁美观; 
    文案不能干巴巴,要丰富有趣;热点追图、双微首图,必须好看刺激…还好有创客贴。

    创客贴是一款极简的网站式平面设计工具,解决了大多数人的设计痛点,让不会使用专业制图软件的运营人员也能快速制作出自己想要的图片:可使用平台提供的大量图片、字体和模板等素材,通过简单的拖拉拽操作,就可以轻松设计出精美的海报、PPT、公号文章首图等图片。另有团队协作版和ipad版会让你的做图工作更加直接高效。

    产品官网:https://www.chuangkit.com


    ●用户行为分析工具:神策分析​
    图片7.png

    近期哪个渠道用户注册量最高?变化趋势如何?新增的注册引导流程是否提升用户粘性……作为产品运营的你如果想知道这些问题的答案,那么可以使用神策分析。

    神策分析是一款可以私有化部署的用户行为分析产品,致力帮助用户实现数据驱动。在保障数据安全的前提下,产品实现秒级数据导入与查询,带来灵活的PaaS平台,并针对多业务场景提供专业的数据分析服务,为业务决策提供数据支撑。

    产品官网:https://www.sensorsdata.cn/

    ●商业管理云:数据观​

    ~XI`[SYBY3CG1@UTQHVAZ@1.png

    作为企业运营管理部门,需要每天关注各渠道的运营数据(用户、推广、客服、营销)等,整合这些数据孤岛很耗费人力和时间成本,我们需要一个实时的可视化看板以了解企业的运营动态,然后才能针对企业运营过程中的问题及时做出决策。
    数据观可以把我们的注册用户、百度推广、在线客服、微信、微博等数据全部关联起来,并基于全量数据进行可视化分析,以便我们实时、准确的了解企业的运营状况。现在公司的销售、市场、运营都在用数据观做日常的业务分析。

    产品官网:www.shujuguan.cn

    ●图文排版工具:135编辑器​
    图片9.png

    内容运营每天都有大量的工作要做,要追热点,写原创,上午想着如何涨粉,下午想着如何促活。在做好内容的同时,如何快速做出既漂亮又有自己独特风格的排版,是很多内容运营的目标。

    135编辑器是一款在线图文排版工具,功能比较全面,操作也非常简单。比如一键导入,多平台发布,一键排版和定时群发等大大节约了时间。样式库丰富,自己排列组合自定义为模板,形成自己的风格。企业用户也可以将其嵌入企业内部系统,提升企业排版能力。

    产品官网:www.135editor.com

    ●深度链接企业服务商:LinkedME​

    图片10.png

    当今,每个移动APP都是独立的,内容和服务之间的链接消失,应用搜索断裂,APP的内容被局限在每个APP内。我们能在电脑网页所监测的用户意图、广告曝光和其它盈利指标都不能在移动端很好进行。

    LinkedME打破了束缚我们的APP孤岛,它是一个企业级深度链接服务平台,可以帮忙APP解决用户增长和流量变现问题。它旗下的Linkpage提供APP一键直达和推广渠道监测服务,帮助APP企业获取社交媒体和广告曝光用户,提高运营转化,优化渠道投放策略。

    产品官网:https://www.linkedme.cc/ 

    ●SEM优化工具:九枝兰​
    图片11.png

    搜索竞价推广真不是人干的活儿。单是最基础的调价,一个关键词优化师就需要管理1440种价格!如果以5万关键词的账户为例,每天每个关键词调价1440次,每天竟然需要进行7200万次调价!一个优化师即使有洪荒之力,也极难完成。

    于是九枝兰SEM优化工具应运而生,解决SEM优化师力所不能及的事,在需要发挥人的聪明才智的方向投入精力,如:拓词、创意、着陆页优化。

    产品官网:http://www.jiuzhilan.com/sem-tool-highlights/ 

    ●广告效果监测与分析平台:【友盟+】U-ADplus​
    图片12.png

    U-ADplus广效监测是【友盟+】旗下聚焦营销全链路的第三方数据服务。其中AppTrack是面向App广告主推出的监测与分析平台。它能满足不同类型使用者的推广监测需求。提供基础、电商、游戏应用场景,使用者可根据想监控的指标选择适合的应用场景。支持多样的推广形式,不管是广告平台、信息流广告还是广告联盟 ,它都可以帮使用者监控推广效果。除了能监控推广点击和激活,还能监测用户注册、登陆、行为和付费,让使用者真正了解推广带来用户的转化情况。

    产品官网:https://at.umeng.com/fuia0z

    ●企业消费与报销管理平台:易快报​

    图片13.png

    起初臃肿冗长的报销流程令我们非常头疼,很难进行费用管控。使用易快报后才深切感受到了原来报销流程也可以如此便捷。易快报打通了从提交申请到支付的全部环节,使员工报销的时间大为缩短,还能对企业进行实时全程费用控制,效率提高了不少。

    易快报是一个敏捷的企业消费和报销管理平台,面向企业提供专业的订购、费控和报销管理服务。据官网公布的数据,公司目前已经为超过5万家企业客户和220万个人用户提供了这种全新的报销服务。

    产品官网:https://www.ekuaibao.com/

    ●快速建站服务:友好速搭​
    图片15.png

    要做电商,渠道是个问题,入驻各大平台还不够,要有自己的独立的门户网站,但创业初期,资金有限,技术太贵,维护太难,加上服务器配置、域名备案一堆事,一想头就大,有了友好速搭,60 秒轻松搭建门户网站,就算是技术小白,也能做出稳定可靠的官网。

    友好速搭可以为企业提供了一站式建站服务,集成域名、DNS、安全证书等基础设施,提供SaaS 建站系统,开放全部API并提供互联网、营销、传媒等资源服务,目前已服务超过2.3万个品牌。

    产品官网:youhaosuda.com

    ●移动应用分发与监控平台:酷传​
    图片16.png

    酷传是一个一站式APP发布及监控平台。开发者不需要添加任何SDK,即可通过酷传把自己的APP同时上架到30家主流安卓应用商店,后续还可以实时跟进各个商店的审核进度,不再需要运营人员一一去各家应用商店进行操作;通过酷传的监控产品,还可以查询一百多万款APP的各项数据表现,目前已经支持安卓和IOS两个平台十多家应用商店。

    产品官网:www.coolchuan.com

    ●企业级云服务商​​​​​​​:七牛云​

    图片17.png

    对于有大量数据存储需求的互联网公司,现在不仅仅是把数据托管到云存储供应商就够了,围绕数据展开的一站式的服务,成为当下互联网迫切所需。
    七牛云作为企业级云服务商,除了存储、CDN加速服务,和完整的直播云解决方案,围绕数据还有许多玩法。存储在七牛的数据,不需要下载下来,就可以进行批量加水印、裁剪、反垃圾等处理。通过提供稳定、高效、可信赖的底层服务,让客户能集中精力在自身业务的实现和创新上。
     
    产品官网:www.qiniu.com
     
    ● 应用性能监测平台:OneAPM
     

    图片18.png

    网络访问缓慢?用户无法登录?接口突然失效?你是否也经常由于很多IT系统故障的原因,眼睁睁地看着用户流失?作为一个应用性能监测平台,OneAPM可以帮助你预先发现性能问题。OneAPM目前支持Java、PHP、Ruby、.Net、Python等多种编程语言,同时也支持iOS 和Android操作系统。它可以帮你实时抓取缓慢的程序代码和SQL语句,让你的应用运行更加流畅、稳定。

    产品官网:http://www.oneapm.com/


    ●APP测试服务商:Testin云测​
    图片19.png

    移动互联网的竞争越来越激烈,迫使互联网公司必须要根据用户需求快速对APP进行迭代。每次迭代过程中最担心的是出现Bug,伤害到用户体验可能会是导致用户流失的重要原因。

    Testin专注于面向全球范围内的移动互联网应用开发者,如移动APP开发者、移动手机游戏开发商及互联网+相关移动应用企业提供“一站式测试服务”。包括从移动应用内测到功能测试、性能测试、兼容测试及移动应用发布后持续质量监控,解决APP终端在功能、性能、碎片化、兼容性、稳定性等广大移动互联网企业及开发者不易克服的难题。Testin一站式测试服务覆盖开发者从开发完成到版本迭代的全过程。

    产品官网:www.testin.cn

    ●推送技术和大数据服务商:个推​
    图片20.png

    “这么大个红包,再不使用就过期啦!”、“美妆热搜销量王,全场五折起”,收到这样的消息,你肯定也会忍不住打开一探究竟。通过小小的消息,传递用户所需的信息,提高用户活跃度和留存率,这就是推送技术的力量。

    个推所做的就是搭建APP与用户沟通的桥梁,确保消息的毫秒级到达,用户即便足不出户,也能第一时间尽知天下事。个推给用户分群组、打标签,通过大数据分析送让合适的消息找到合适的用户。个推提供免费推送和VIP增值服务,并为各垂直领域提供专业大数据解决方案,目前已服务于50万APP,SDK累计接入用户数超过130亿。

    产品官网:http://www.getui.com/

    希望大家所在的公司都能在上面这20款工具服务的助力下在2017年继续高歌猛进,越跑越快。
     
    2
    回复

    "本期环信直播课堂将由环信IOS工程师fudh给大家详细讲解集成3.0 SDK实时音视频"demo你放在那里? iOS 环信 音视频

    yu 回复了问题 • 2 人关注 • 185 次浏览 • 2016-12-27 13:31 • 来自相关话题

    6
    评论

    环信React Native Demo发布,支持 JavaScript 和 React Native 开发 react native 环信

    产品更新 发表了文章 • 1866 次浏览 • 2016-12-26 11:31 • 来自相关话题

    React Native Demo 介绍React Native 可以让开发者使用 JavaScript 和 React Native 开发原生 iOS 和 Android 应用,提高开发效率(Learn once, write anywhere)。

    React Native Demo 已集成环信 Web IM SDK,并提供即时通讯基本功能,开发者可以直接将该 Demo 集成到您的应用中,立即获得即时通讯的能力。后期将引入热加载功能,无需审核,直接发布。

    GitHub 下载地址:https://github.com/easemob/webim-react-native 
     
    版本支持React Native Demo 支持 iOS 9.0 以上版本,以及 Android 4.1 (API 16)。

    注:所有开发调试环境均基于Mac。


    功能

    React Native Demo 分为 iOS Demo 和 Android Demo 两部分,已完成的功能如下。 
     
    iOSiOS Demo 已完成功能:

    • 登录
    • 注册
    • 好友
    ◦ 列表及筛选
    ◦ 好友信息展示
    ◦ 黑名单
    ◦ 删除好友
    ◦ 好友通知
    ◾ 添加好友通知展示
    ◾ 接受好友请求
    ◾ 拒绝好友请求
    ◾ 添加好友
    • 群组
    ◦ 群组列表
    ◦ 群组成员列表
    • 聊天
    ◦ 相机图片消息
    ◦ 本地图片消息
    ◦ emoji消息
    ◦ 普通消息
    • 异常状态处理
    ◦ 断线退出到登录页
    ◦ 重复登录退出到登录页AndroidAndroid Demo 已完成功能:

    • 登录
    • 注册
    目录结构App 的目录结构如下:

    • Containers: 容器 | 页面 | 路由
    ◦ App.js 总入口
    ◾ Redux/ 初始化
    ◾ I18n/ 初始化
    ◾ Config/index.js 系统初始配置
    ◦ RootContainer.js 根容器
    ◾ Navigation/NavigationRouter.js 初始化路由
    ◾ /Config/ReduxPersist 持久化初始化
    • Components 常用组件
    • I18n 多语言支持
    • Images 图片资源
    • Lib WebIM初始化
    • Navigation: 路由相关
    • Redux: actions / reducers
    • Sdk: webim-easemobo SDKRedux State{
    // ui相关
    ui: [
    // ui通用:比如loading
    common: {
    fetching:false
    },
    login: {
    username: '',
    password: '',
    isSigned: false,
    },
    register: { },
    contactInfo: { },
    ],
    im: ,
    // 数据实体
    entities: {
    roster: {
    byName: {
    {
    jid, name, subscription, groups?
    }
    },
    names: ['lwz2'...],
    // 好友列表在此,因为好友列表来源于roster,息息相关
    friends: ,
    },
    // 订阅通知
    subscribe: {
    byFrom: {}
    },
    room: {},
    group: {
    byId: {},
    names:
    },
    members: {
    byName: ,
    byGroupId:
    }
    blacklist: {},
    message: {
    byId: {}
    chat: {
    [chatId]: [messageId1, messageId2]
    },
    groupChat: {
    [chatId]: {}
    },
    }
    }
    }
    版本历史 :更新日志
     
    SDK下载:点击下载 查看全部
    54471962_1.jpg

    React Native Demo 介绍
    React Native 可以让开发者使用 JavaScript 和 React Native 开发原生 iOS 和 Android 应用,提高开发效率(Learn once, write anywhere)。

    React Native Demo 已集成环信 Web IM SDK,并提供即时通讯基本功能,开发者可以直接将该 Demo 集成到您的应用中,立即获得即时通讯的能力。后期将引入热加载功能,无需审核,直接发布。

    GitHub 下载地址:https://github.com/easemob/webim-react-native 
     
    版本支持
    React Native Demo 支持 iOS 9.0 以上版本,以及 Android 4.1 (API 16)。

    注:所有开发调试环境均基于Mac。


    功能

    React Native Demo 分为 iOS Demo 和 Android Demo 两部分,已完成的功能如下。 
     
    iOS
    iOS Demo 已完成功能: 

    • 登录
    • 注册
    • 好友
    ◦ 列表及筛选
    ◦ 好友信息展示
    ◦ 黑名单
    ◦ 删除好友
    ◦ 好友通知
    ◾ 添加好友通知展示
    ◾ 接受好友请求
    ◾ 拒绝好友请求
    ◾ 添加好友
    • 群组
    ◦ 群组列表
    ◦ 群组成员列表
    • 聊天
    ◦ 相机图片消息
    ◦ 本地图片消息
    ◦ emoji消息
    ◦ 普通消息
    • 异常状态处理
    ◦ 断线退出到登录页
    ◦ 重复登录退出到登录页
    Android
    Android Demo 已完成功能: 

    • 登录
    • 注册
    目录结构
    App 的目录结构如下: 

    • Containers: 容器 | 页面 | 路由
    ◦ App.js 总入口
    ◾ Redux/ 初始化
    ◾ I18n/ 初始化
    ◾ Config/index.js 系统初始配置
    ◦ RootContainer.js 根容器
    ◾ Navigation/NavigationRouter.js 初始化路由
    ◾ /Config/ReduxPersist 持久化初始化
    • Components 常用组件
    • I18n 多语言支持
    • Images 图片资源
    • Lib WebIM初始化
    • Navigation: 路由相关
    • Redux: actions / reducers
    • Sdk: webim-easemobo SDK
    Redux State
    {
    // ui相关
    ui: [
    // ui通用:比如loading
    common: {
    fetching:false
    },
    login: {
    username: '',
    password: '',
    isSigned: false,
    },
    register: { },
    contactInfo: { },
    ],
    im: ,
    // 数据实体
    entities: {
    roster: {
    byName: {
    {
    jid, name, subscription, groups?
    }
    },
    names: ['lwz2'...],
    // 好友列表在此,因为好友列表来源于roster,息息相关
    friends: ,
    },
    // 订阅通知
    subscribe: {
    byFrom: {}
    },
    room: {},
    group: {
    byId: {},
    names:
    },
    members: {
    byName: ,
    byGroupId:
    }
    blacklist: {},
    message: {
    byId: {}
    chat: {
    [chatId]: [messageId1, messageId2]
    },
    groupChat: {
    [chatId]: {}
    },
    }
    }
    }
    版本历史 :更新日志
     
    SDK下载:点击下载
    0
    评论

    老哥,稳!圣诞节集齐4颗就能召唤神龙... 环信 环信移动客服

    beyond 发表了文章 • 239 次浏览 • 2016-12-23 19:24 • 来自相关话题

    有一种拿奖叫做拿到手抽筋,有一种环信叫做别人家的公司!

    447697058399273438.jpg


    ZAW)THCFYU_FXKU9BHDV5EO.png

    有一种拿奖叫做拿到手抽筋,有一种环信叫做别人家的公司!
    1
    回复

    集成3.2.2编译Success,但却有40+的警告 iOS 环信

    donghai 回复了问题 • 2 人关注 • 362 次浏览 • 2016-12-21 17:45 • 来自相关话题

    0
    评论

    李理:自动梯度求解——cs231n的notes 环信 深度学习 李理

    beyond 发表了文章 • 211 次浏览 • 2016-12-21 11:05 • 来自相关话题

    本系列文章面向深度学习研发者,希望通过Image Caption Generation,一个有意思的具体任务,深入浅出地介绍深度学习的知识。本系列文章涉及到很多深度学习流行的模型,如CNN,RNN/LSTM,Attention等。本文为第5篇。
     
    作者:李理 
    目前就职于环信,即时通讯云平台和全媒体智能客服平台,在环信从事智能客服和智能机器人相关工作,致力于用深度学习来提高智能机器人的性能。
    相关文章: 
    环信李理:从Image Caption Generation了解深度学习
    李理:从Image Caption Generation理解深度学习(part II)
    李理:从Image Caption Generation理解深度学习(part III)
    李理:自动梯度求解 反向传播算法的另外一种视角
     
    Optimization

    这一部分内容来自:CS231n Convolutional Neural Networks for Visual Recognition

    简介

    我们的目标:x是一个向量,f(x)是一个函数,它的输入是一个向量(或者认为是多变量的函数,这个输入向量就是自变量),输出是一个实数值。我们需要计算的是f对每一个自变量的导数,然后把它们排成一个向量,也就是梯度。




    为什么要求这个呢?前面我们也讲了,我们的神经网络的损失函数最终可以看成是权重weights和bias的函数,我们的目标就是调整这些参数,使得损失函数最小。

    简单的表达式和梯度的解释

    首先我们看一个很简单的函数 f(x,y)=xy,求f对x和y的偏导数很简单:




    首先来看导数的定义:




    函数在某个点的导数就是函数曲线在这个点的斜率,也就是f(x)随x的变化率。 
    比如上面的例子,当x=4,y=−3时 f(x,y)=−12,f对x的偏导数




    也就是说,如果我们固定y=4,然后给x一个很小的变化h,那么f(x,y)的变化大约是-3*h。 
    因此乘法的梯度就是




    同样,加法的梯度更简单:




    最后一个简单函数是max函数:




    这个导数是ReLU(x)=max(x,0)的导数,其实也简单,如果 x>=y,那么 max(x,y)=x,则导数是1,否则 max(x,y)=0,那么对x求导就是0。

    复杂表达式的链式法则

    接下来看一个稍微复杂一点的函数 f(x,y,z)=(x+y)z。我们引入一个中间变量q,f=qz,q=x+y,我们可以使用链式法则求f对x和y的导数。




    对y的求导也是类似的。

    下面是用python代码来求f对x和y的导数在某一个点的值。# 设置自变量的值
    x = -2; y = 5; z = -4

    # “前向”计算f
    q = x + y # q becomes 3
    f = q * z # f becomes -12

    # 从“后”往前“反向”计算
    # 首先是 f = q * z
    dfdz = q # 因为df/dz = q, 所以f对z的梯度是 3
    dfdq = z # 因为df/dq = z, 所以f对q的梯度是 -4
    # 然后 q = x + y
    dfdx = 1.0 * dfdq # 因为dq/dx = 1,所以使用链式法则计算dfdx=-4
    dfdy = 1.0 * dfdq # 因为dq/dy = 1,所以使用链式法则计算dfdy=-4我们也可以用计算图来表示和计算:




    绿色的值是feed forward的结果,而红色的值是backprop的结果。

    不过我觉得cs231n课程的这个图没有上面blog的清晰,原因是虽然它标示出来了最终的梯度,但是没有标示出local gradient,我在下面会画出完整的计算过程。

    反向传播算法的直觉解释

    我们如果把计算图的每一个点看成一个“门”(或者一个模块),或者说一个函数。它有一个输入(向量),也有一个输出(标量)。对于一个门来说有两个计算,首先是根据输入,计算输出,这个一般很容易。还有一种计算就是求输出对每一个输入的偏导数,或者说输出对输入向量的”局部“梯度(local gradient)。一个复杂计算图(神经网络)的计算首先就是前向计算,然后反向计算,反向计算公式可能看起来很复杂,但是如果在计算图上其实就是简单的用local gradient乘以从后面传过来的gradient,然后加起来。

    Sigmoid模块的例子

    接下来我们看一个更复杂的例子:




    这个函数是一个比较复杂的复合函数,但是构成它的基本函数是如下4个简单函数:




    下面是用计算图画出这个计算过程:




    这个图有4种gate,加法,乘法,指数和倒数。加法有加一个常数和两个变量相加,乘法也是一样。

    上图绿色的值是前向计算的结果,而红色的值是反向计算的结果,local graident并没有标示出来,所以看起来可能有些跳跃,下面我在纸上详细的分解了其中的步骤,请读者跟着下图自己动手计算一遍。




    上图就是前向计算的过程,比较简单。




    第二个图是计算local gradient,对于两个输入的乘法和加法,local gradient也是两个值,local gradient的值我是放到图的节点上了。




    第三个图是具体计算一个乘法的local gradient的过程,因为上图可能看不清,所以单独放大了这一步。




    最后计算真正的梯度,是把local gradient乘以来自上一步的gradient。不过这个例子一个节点只有一个输出,如果有多个的话,梯度是加起来的,可以参考1.4的




    上面我们看到把




    分解成最基本的加法,乘法,导数和指数函数,但是我们也可以不分解这么细。之前我们也学习过了sigmoid函数,那么我们可以这样分解:




    σ(x)σ(x) 的导数我们之前已经推导过一次了,这里再列一下:




    因此我们可以把后面一长串的gate”压缩“成一个gate:




    我们来比较一下,之前前向计算 σ(x)σ(x) 需要一次乘法,一次exp,一次加法导数;而反向计算需要分别计算这4个gate的导数。

    而压缩后前向计算是一样的,但是反向计算可以”利用“前向计算的结果




    这只需要一次减法和一次乘法!当然如果不能利用前向的结果,我们如果需要重新计算 σ(x)σ(x) ,那么压缩其实没有什么用处。能压缩的原因在于σ函数导数的特殊形式。而神经网络的关键问题是在训练,训练性能就取决于这些细节。如果是我们自己来实现反向传播算法,我们就需要利用这样的特性。而如果是使用工具,那么就依赖于工具的优化水平了。

    下面我们用代码来实现一下:w = [2,-3,-3] # assume some random weights and data
    x = [-1, -2]

    # forward pass
    dot = w[0]*x[0] + w[1]*x[1] + w[2]
    f = 1.0 / (1 + math.exp(-dot)) # sigmoid function

    # backward pass through the neuron (backpropagation)
    ddot = (1 - f) * f # gradient on dot variable, using the sigmoid gradient derivation
    dx = [w[0] * ddot, w[1] * ddot] # backprop into x
    dw = [x[0] * ddot, x[1] * ddot, 1.0 * ddot] # backprop into w
    # we're done! we have the gradients on the inputs to the circuit上面的例子用了一个小技巧,就是所谓的staged backpropagation,说白了就是给中间的计算节点起一个名字。比如dot。为了让大家熟悉这种技巧,下面有一个例子。

    Staged computation练习




    我们用代码来计算这个函数对x和y的梯度在某一点的值

    前向计算x = 3 # example values
    y = -4

    # forward pass
    sigy = 1.0 / (1 + math.exp(-y)) # 分子上的sigmoid #(1)
    num = x + sigy # 分子 #(2)
    sigx = 1.0 / (1 + math.exp(-x)) # 分母上的sigmoid #(3)
    xpy = x + y #(4)
    xpysqr = xpy**2 #(5)
    den = sigx + xpysqr # 分母 #(6)
    invden = 1.0 / den #(7)
    f = num * invden # done! #(8)反向计算# backprop f = num * invden
    dnum = invden # gradient on numerator #(8)
    dinvden = num #(8)
    # backprop invden = 1.0 / den
    dden = (-1.0 / (den**2)) * dinvden #(7)
    # backprop den = sigx + xpysqr
    dsigx = (1) * dden #(6)
    dxpysqr = (1) * dden #(6)
    # backprop xpysqr = xpy**2
    dxpy = (2 * xpy) * dxpysqr #(5)
    # backprop xpy = x + y
    dx = (1) * dxpy #(4)
    dy = (1) * dxpy #(4)
    # backprop sigx = 1.0 / (1 + math.exp(-x))
    dx += ((1 - sigx) * sigx) * dsigx # Notice += !! See notes below #(3)
    # backprop num = x + sigy
    dx += (1) * dnum #(2)
    dsigy = (1) * dnum #(2)
    # backprop sigy = 1.0 / (1 + math.exp(-y))
    dy += ((1 - sigy) * sigy) * dsigy #(1)
    # done! phew需要注意的两点:1. 前向的结果都要保存下来,反向的时候要用的。2. 如果某个变量有多个出去的边,第一次是等于,第二次就是+=,因为我们要把不同出去点的梯度加起来。

    下面我们来逐行分析反向计算: 
    (8) f = num * invden 
    local gradient




    而上面传过来的梯度是1,所以 dnum=1∗invden。注意变量的命名规则, df/dnum就命名为dnum【省略了df,因为默认我们是求f对所有变量的偏导数】 
    同理: dinvden=num

    (7) invden = 1.0 / den

    local gradient是 (−1.0/(den∗∗2)) ,然后乘以上面来的dinvden

    (6) den = sigx + xpysqr

    这个函数有两个变量sigx和xpysqr,所以需要计算两个local梯度,然后乘以dden 
    加法的local梯度是1,所以就是(1)*dden

    (5) xpysqr = xpy**2

    local gradient是2*xpy,再乘以dxpysqr

    (4) xpy = x + y

    还是一个加法,local gradient是1,所以dx和dy都是dxpy乘1

    (3) sigx = 1.0 / (1 + math.exp(-x))

    这是sigmoid函数,local gradient是 (1-sigx)*sigx,再乘以dsigx。 
    不过需要注意的是这是dx的第二次出现,所以是+=,表示来自不同路径反向传播过来给x的梯度值

    (2) num = x + sigy

    还是个很简单的加法,local gradient是1。需要注意的是dx是+=,理由同上。

    (1) sigy = 1.0 / (1 + math.exp(-y))

    最后是sigmoid(y)和前面(3)一样的。

    请仔细阅读上面反向计算的每一步代码,确保自己理解了之后再往下阅读。

    梯度的矩阵运算

    前面都是对一个标量的计算,在实际实现时用矩阵运算一次计算一层的所有梯度会更加高效。因为矩阵乘以向量和向量乘以向量都可以看出矩阵乘以矩阵的特殊形式,所以下面我们介绍矩阵乘法怎么求梯度。

    首先我们得定义什么叫矩阵对矩阵的梯度!

    我查阅了很多资料,也没找到哪里有矩阵对矩阵的梯度的定义,如果哪位读者知道,请告诉我,谢谢!唯一比较接近的是Andrew Ng的课程cs294的背景知识介绍的slides linalg的4.1节定义了gradient of Matrix,关于矩阵对矩阵的梯度我会有一个猜测性的解释,可能会有问题。

    首先介绍graident of matrix

    假设 f:Rm×n→R是一个函数,输入是一个m×n的实数值矩阵,输出是一个实数。那么f对A的梯度是如下定义的:




    看起来定义很复杂?其实很简单,我们把f看成一个mn个自变量的函数,因此我们可以求f对这mn个自变量的偏导数,然后把它们排列成m*n的矩阵就行了。为什么要多此一举把变量拍成矩阵把他们的偏导数也排成矩阵?想想我们之前的神经网络的weights矩阵,这是很自然的定义,同时我们需要计算loss对weights矩阵的每一个变量的偏导数,写出这样的形式计算起来比较方便。

    那么什么是矩阵对矩阵的梯度呢?我们先看实际神经网络的一个计算情况。对于全连接的神经网络,我们有一个矩阵乘以向量 D=WxD=Wx 【我们这里把向量x看成矩阵】。现在我们需要计算loss对某一个 WijWij 的偏导数,根据我们之前的计算图, WijWij 有多少条出边,那么就有多少个要累加的梯度乘以local梯度。 
    假设W是m×n的矩阵,x是n×p的矩阵,则D是m×p的矩阵




    根据矩阵乘法的定义




    我们可以计算:




    请仔细理解上面这一步,如果 k≠i,则不论s是什么,Wks跟Wij不是同一个变量,所以导数就是0;如果k=i,∑sWisxsl=xjl,也就求和的下标s取j的时候有WijWij。 
    因此




    上面计算了loss对一个Wij的偏导数,如果把它写成矩阵形式就是:




    前面我们推导出了对Wij的偏导数的计算公式,下面我们把它写成矩阵乘法的形式并验证【证明】它。




    为什么可以写成这样的形式呢?




    上面的推导似乎很复杂,但是我们只要能记住就行,记法也很简单——把矩阵都变成最特殊的1 1的矩阵(也就是标量,一个实数)。D=w x,这个导数很容易吧,对w求导就是local gradient x,然后乘以得到dW=dD x;同理dx=dD W。 
    但是等等,刚才那个公式里还有矩阵的转置,这个怎么记?这里有一个小技巧,就是矩阵乘法的条件,两个矩阵能相乘他们的大小必须匹配,比如D=Wx,W是m n,x是n p,也就是第二个矩阵的行数等于第一个的列数。 
    现在我们已经知道dW是dD”乘以“x了,dW的大小和W一样是m n,而dD和D一样是m p,而x是n p,那么为了得到一个m n的矩阵,唯一的办法就是 dD∗xT 
    同理dx是n p,dD是m p,W是m*n,唯一的乘法就是 WT∗dD 
    下面是用python代码来演示,numpy的dot就是矩阵乘法,可以用numpy.dot(A,B),也可以直接调用ndarray的dot函数——A.dot(B):# forward pass
    W = np.random.randn(5, 10)
    X = np.random.randn(10, 3)
    D = W.dot(X)

    # now suppose we had the gradient on D from above in the circuit
    dD = np.random.randn(*D.shape) # same shape as D
    dW = dD.dot(X.T) #.T gives the transpose of the matrix
    dX = W.T.dot(dD)至此,本系列文章的第5部分告一段落。在接下来的文章中,作者将为大家详细讲述关于常见的深度学习框架/工具的使用方法、使用自动求导来实现多层神经网络等内容,敬请期待。 查看全部
    本系列文章面向深度学习研发者,希望通过Image Caption Generation,一个有意思的具体任务,深入浅出地介绍深度学习的知识。本系列文章涉及到很多深度学习流行的模型,如CNN,RNN/LSTM,Attention等。本文为第5篇。
     
    作者:李理 
    目前就职于环信,即时通讯云平台和全媒体智能客服平台,在环信从事智能客服和智能机器人相关工作,致力于用深度学习来提高智能机器人的性能。
    相关文章: 
    环信李理:从Image Caption Generation了解深度学习
    李理:从Image Caption Generation理解深度学习(part II)
    李理:从Image Caption Generation理解深度学习(part III)
    李理:自动梯度求解 反向传播算法的另外一种视角
     
    Optimization

    这一部分内容来自:CS231n Convolutional Neural Networks for Visual Recognition

    简介

    我们的目标:x是一个向量,f(x)是一个函数,它的输入是一个向量(或者认为是多变量的函数,这个输入向量就是自变量),输出是一个实数值。我们需要计算的是f对每一个自变量的导数,然后把它们排成一个向量,也就是梯度。
    001.jpg

    为什么要求这个呢?前面我们也讲了,我们的神经网络的损失函数最终可以看成是权重weights和bias的函数,我们的目标就是调整这些参数,使得损失函数最小。

    简单的表达式和梯度的解释

    首先我们看一个很简单的函数 f(x,y)=xy,求f对x和y的偏导数很简单:
    002.jpg

    首先来看导数的定义:
    003.jpg

    函数在某个点的导数就是函数曲线在这个点的斜率,也就是f(x)随x的变化率。 
    比如上面的例子,当x=4,y=−3时 f(x,y)=−12,f对x的偏导数
    004.jpg

    也就是说,如果我们固定y=4,然后给x一个很小的变化h,那么f(x,y)的变化大约是-3*h。 
    因此乘法的梯度就是
    005.jpg

    同样,加法的梯度更简单:
    006.jpg

    最后一个简单函数是max函数:
    007.jpg

    这个导数是ReLU(x)=max(x,0)的导数,其实也简单,如果 x>=y,那么 max(x,y)=x,则导数是1,否则 max(x,y)=0,那么对x求导就是0。

    复杂表达式的链式法则

    接下来看一个稍微复杂一点的函数 f(x,y,z)=(x+y)z。我们引入一个中间变量q,f=qz,q=x+y,我们可以使用链式法则求f对x和y的导数。
    008.jpg

    对y的求导也是类似的。

    下面是用python代码来求f对x和y的导数在某一个点的值。
    # 设置自变量的值
    x = -2; y = 5; z = -4

    # “前向”计算f
    q = x + y # q becomes 3
    f = q * z # f becomes -12

    # 从“后”往前“反向”计算
    # 首先是 f = q * z
    dfdz = q # 因为df/dz = q, 所以f对z的梯度是 3
    dfdq = z # 因为df/dq = z, 所以f对q的梯度是 -4
    # 然后 q = x + y
    dfdx = 1.0 * dfdq # 因为dq/dx = 1,所以使用链式法则计算dfdx=-4
    dfdy = 1.0 * dfdq # 因为dq/dy = 1,所以使用链式法则计算dfdy=-4
    我们也可以用计算图来表示和计算:
    009.jpg

    绿色的值是feed forward的结果,而红色的值是backprop的结果。

    不过我觉得cs231n课程的这个图没有上面blog的清晰,原因是虽然它标示出来了最终的梯度,但是没有标示出local gradient,我在下面会画出完整的计算过程。

    反向传播算法的直觉解释

    我们如果把计算图的每一个点看成一个“门”(或者一个模块),或者说一个函数。它有一个输入(向量),也有一个输出(标量)。对于一个门来说有两个计算,首先是根据输入,计算输出,这个一般很容易。还有一种计算就是求输出对每一个输入的偏导数,或者说输出对输入向量的”局部“梯度(local gradient)。一个复杂计算图(神经网络)的计算首先就是前向计算,然后反向计算,反向计算公式可能看起来很复杂,但是如果在计算图上其实就是简单的用local gradient乘以从后面传过来的gradient,然后加起来。

    Sigmoid模块的例子

    接下来我们看一个更复杂的例子:
    010.jpg

    这个函数是一个比较复杂的复合函数,但是构成它的基本函数是如下4个简单函数:
    011.jpg

    下面是用计算图画出这个计算过程:
    012.jpg

    这个图有4种gate,加法,乘法,指数和倒数。加法有加一个常数和两个变量相加,乘法也是一样。

    上图绿色的值是前向计算的结果,而红色的值是反向计算的结果,local graident并没有标示出来,所以看起来可能有些跳跃,下面我在纸上详细的分解了其中的步骤,请读者跟着下图自己动手计算一遍。
    013.jpg

    上图就是前向计算的过程,比较简单。
    014.jpg

    第二个图是计算local gradient,对于两个输入的乘法和加法,local gradient也是两个值,local gradient的值我是放到图的节点上了。
    015.jpg

    第三个图是具体计算一个乘法的local gradient的过程,因为上图可能看不清,所以单独放大了这一步。
    016.jpg

    最后计算真正的梯度,是把local gradient乘以来自上一步的gradient。不过这个例子一个节点只有一个输出,如果有多个的话,梯度是加起来的,可以参考1.4的
    017.jpg

    上面我们看到把
    018.jpg

    分解成最基本的加法,乘法,导数和指数函数,但是我们也可以不分解这么细。之前我们也学习过了sigmoid函数,那么我们可以这样分解:
    019.jpg

    σ(x)σ(x) 的导数我们之前已经推导过一次了,这里再列一下:
    020.jpg

    因此我们可以把后面一长串的gate”压缩“成一个gate:
    021.jpg

    我们来比较一下,之前前向计算 σ(x)σ(x) 需要一次乘法,一次exp,一次加法导数;而反向计算需要分别计算这4个gate的导数。

    而压缩后前向计算是一样的,但是反向计算可以”利用“前向计算的结果
    022.jpg

    这只需要一次减法和一次乘法!当然如果不能利用前向的结果,我们如果需要重新计算 σ(x)σ(x) ,那么压缩其实没有什么用处。能压缩的原因在于σ函数导数的特殊形式。而神经网络的关键问题是在训练,训练性能就取决于这些细节。如果是我们自己来实现反向传播算法,我们就需要利用这样的特性。而如果是使用工具,那么就依赖于工具的优化水平了。

    下面我们用代码来实现一下:
    w = [2,-3,-3] # assume some random weights and data
    x = [-1, -2]

    # forward pass
    dot = w[0]*x[0] + w[1]*x[1] + w[2]
    f = 1.0 / (1 + math.exp(-dot)) # sigmoid function

    # backward pass through the neuron (backpropagation)
    ddot = (1 - f) * f # gradient on dot variable, using the sigmoid gradient derivation
    dx = [w[0] * ddot, w[1] * ddot] # backprop into x
    dw = [x[0] * ddot, x[1] * ddot, 1.0 * ddot] # backprop into w
    # we're done! we have the gradients on the inputs to the circuit
    上面的例子用了一个小技巧,就是所谓的staged backpropagation,说白了就是给中间的计算节点起一个名字。比如dot。为了让大家熟悉这种技巧,下面有一个例子。

    Staged computation练习
    023.jpg

    我们用代码来计算这个函数对x和y的梯度在某一点的值

    前向计算
    x = 3 # example values
    y = -4

    # forward pass
    sigy = 1.0 / (1 + math.exp(-y)) # 分子上的sigmoid #(1)
    num = x + sigy # 分子 #(2)
    sigx = 1.0 / (1 + math.exp(-x)) # 分母上的sigmoid #(3)
    xpy = x + y #(4)
    xpysqr = xpy**2 #(5)
    den = sigx + xpysqr # 分母 #(6)
    invden = 1.0 / den #(7)
    f = num * invden # done! #(8)
    反向计算
    # backprop f = num * invden
    dnum = invden # gradient on numerator #(8)
    dinvden = num #(8)
    # backprop invden = 1.0 / den
    dden = (-1.0 / (den**2)) * dinvden #(7)
    # backprop den = sigx + xpysqr
    dsigx = (1) * dden #(6)
    dxpysqr = (1) * dden #(6)
    # backprop xpysqr = xpy**2
    dxpy = (2 * xpy) * dxpysqr #(5)
    # backprop xpy = x + y
    dx = (1) * dxpy #(4)
    dy = (1) * dxpy #(4)
    # backprop sigx = 1.0 / (1 + math.exp(-x))
    dx += ((1 - sigx) * sigx) * dsigx # Notice += !! See notes below #(3)
    # backprop num = x + sigy
    dx += (1) * dnum #(2)
    dsigy = (1) * dnum #(2)
    # backprop sigy = 1.0 / (1 + math.exp(-y))
    dy += ((1 - sigy) * sigy) * dsigy #(1)
    # done! phew
    需要注意的两点:1. 前向的结果都要保存下来,反向的时候要用的。2. 如果某个变量有多个出去的边,第一次是等于,第二次就是+=,因为我们要把不同出去点的梯度加起来。

    下面我们来逐行分析反向计算: 
    (8) f = num * invden 
    local gradient
    024.jpg

    而上面传过来的梯度是1,所以 dnum=1∗invden。注意变量的命名规则, df/dnum就命名为dnum【省略了df,因为默认我们是求f对所有变量的偏导数】 
    同理: dinvden=num

    (7) invden = 1.0 / den

    local gradient是 (−1.0/(den∗∗2)) ,然后乘以上面来的dinvden

    (6) den = sigx + xpysqr

    这个函数有两个变量sigx和xpysqr,所以需要计算两个local梯度,然后乘以dden 
    加法的local梯度是1,所以就是(1)*dden

    (5) xpysqr = xpy**2

    local gradient是2*xpy,再乘以dxpysqr

    (4) xpy = x + y

    还是一个加法,local gradient是1,所以dx和dy都是dxpy乘1

    (3) sigx = 1.0 / (1 + math.exp(-x))

    这是sigmoid函数,local gradient是 (1-sigx)*sigx,再乘以dsigx。 
    不过需要注意的是这是dx的第二次出现,所以是+=,表示来自不同路径反向传播过来给x的梯度值

    (2) num = x + sigy

    还是个很简单的加法,local gradient是1。需要注意的是dx是+=,理由同上。

    (1) sigy = 1.0 / (1 + math.exp(-y))

    最后是sigmoid(y)和前面(3)一样的。

    请仔细阅读上面反向计算的每一步代码,确保自己理解了之后再往下阅读。

    梯度的矩阵运算

    前面都是对一个标量的计算,在实际实现时用矩阵运算一次计算一层的所有梯度会更加高效。因为矩阵乘以向量和向量乘以向量都可以看出矩阵乘以矩阵的特殊形式,所以下面我们介绍矩阵乘法怎么求梯度。

    首先我们得定义什么叫矩阵对矩阵的梯度!

    我查阅了很多资料,也没找到哪里有矩阵对矩阵的梯度的定义,如果哪位读者知道,请告诉我,谢谢!唯一比较接近的是Andrew Ng的课程cs294的背景知识介绍的slides linalg的4.1节定义了gradient of Matrix,关于矩阵对矩阵的梯度我会有一个猜测性的解释,可能会有问题。

    首先介绍graident of matrix

    假设 f:Rm×n→R是一个函数,输入是一个m×n的实数值矩阵,输出是一个实数。那么f对A的梯度是如下定义的:
    025.jpg

    看起来定义很复杂?其实很简单,我们把f看成一个mn个自变量的函数,因此我们可以求f对这mn个自变量的偏导数,然后把它们排列成m*n的矩阵就行了。为什么要多此一举把变量拍成矩阵把他们的偏导数也排成矩阵?想想我们之前的神经网络的weights矩阵,这是很自然的定义,同时我们需要计算loss对weights矩阵的每一个变量的偏导数,写出这样的形式计算起来比较方便。

    那么什么是矩阵对矩阵的梯度呢?我们先看实际神经网络的一个计算情况。对于全连接的神经网络,我们有一个矩阵乘以向量 D=WxD=Wx 【我们这里把向量x看成矩阵】。现在我们需要计算loss对某一个 WijWij 的偏导数,根据我们之前的计算图, WijWij 有多少条出边,那么就有多少个要累加的梯度乘以local梯度。 
    假设W是m×n的矩阵,x是n×p的矩阵,则D是m×p的矩阵
    026.jpg

    根据矩阵乘法的定义
    027.jpg

    我们可以计算:
    028.jpg

    请仔细理解上面这一步,如果 k≠i,则不论s是什么,Wks跟Wij不是同一个变量,所以导数就是0;如果k=i,∑sWisxsl=xjl,也就求和的下标s取j的时候有WijWij。 
    因此
    029.jpg

    上面计算了loss对一个Wij的偏导数,如果把它写成矩阵形式就是:
    031.jpg

    前面我们推导出了对Wij的偏导数的计算公式,下面我们把它写成矩阵乘法的形式并验证【证明】它。
    032.jpg

    为什么可以写成这样的形式呢?
    033.jpg

    上面的推导似乎很复杂,但是我们只要能记住就行,记法也很简单——把矩阵都变成最特殊的1 1的矩阵(也就是标量,一个实数)。D=w x,这个导数很容易吧,对w求导就是local gradient x,然后乘以得到dW=dD x;同理dx=dD W。 
    但是等等,刚才那个公式里还有矩阵的转置,这个怎么记?这里有一个小技巧,就是矩阵乘法的条件,两个矩阵能相乘他们的大小必须匹配,比如D=Wx,W是m n,x是n p,也就是第二个矩阵的行数等于第一个的列数。 
    现在我们已经知道dW是dD”乘以“x了,dW的大小和W一样是m n,而dD和D一样是m p,而x是n p,那么为了得到一个m n的矩阵,唯一的办法就是 dD∗xT 
    同理dx是n p,dD是m p,W是m*n,唯一的乘法就是 WT∗dD 
    下面是用python代码来演示,numpy的dot就是矩阵乘法,可以用numpy.dot(A,B),也可以直接调用ndarray的dot函数——A.dot(B):
    # forward pass
    W = np.random.randn(5, 10)
    X = np.random.randn(10, 3)
    D = W.dot(X)

    # now suppose we had the gradient on D from above in the circuit
    dD = np.random.randn(*D.shape) # same shape as D
    dW = dD.dot(X.T) #.T gives the transpose of the matrix
    dX = W.T.dot(dD)
    至此,本系列文章的第5部分告一段落。在接下来的文章中,作者将为大家详细讲述关于常见的深度学习框架/工具的使用方法、使用自动求导来实现多层神经网络等内容,敬请期待。
    0
    评论

    环信VP程旭文:从被动客服到主动营销 程开源 环信移动客服 环信 亿邦动力

    新闻资讯 发表了文章 • 198 次浏览 • 2016-12-20 11:07 • 来自相关话题

     [思路网注]程旭文分享了包括国美在线、楚楚街、金融界等很多环信客户在营销领域的应用。怎样做营销投放,再回到针对用户环节的营销、客服怎样使得这个环节可以做到闭环。   【亿邦动力网讯】12月19日消息,在2016亿邦未来零售大会新服务·思路服务分论坛上环信VP程旭文发表了《从被动客服到主动营销》演讲,他表示:“其实环信在很长时间内我们并没有做营销类的领域,更多的在做SaaS软件,主要做两个软件: 第一,即时通讯云。 第二,全媒体智能客服。”





    图为环信VP副总裁程旭文

    2016亿邦未来零售大会由亿邦动力网主办,思路网协办,于12月19日-21日在广州白云万达希尔顿酒店举行。国内外电商领域知名企业高管、专家学者、媒体代表共计2000余人出席。

    本届大会以“新物种、新规则、新电商”为主题,包括两天的主论坛、五场分论坛、电商经理人之夜以及马蹄社和亿邦疯人会等系列活动。值得关注的是,在本届大会上,电商产业所熟知的如阿里巴巴、京东、唯品会、当当、亚马逊等面孔都没有出现,取而代之的全部是新生代的零售平台和品牌商阵营,反映了电商领域正寻求破局、寻找新增长的行业心态。

    (温馨提示:本文为速记初审稿,保证现场嘉宾原意,未经删节,或存纰漏,敬请谅解。)

    以下是演讲实录:

    程旭文:我也是接到一个命题作文,其实环信在很长时间内我们并没有单独去做营销领域,更多的在做SaaS软件,我们主要做两个软件:

    第一,即时通讯云。

    第二,全媒体智能客服。

    以上两个都是IT领域做在线软件交付。

    我们一不小心进入了营销行业,我们可以基于环信的基础,包括服务10万多家APP企业,服务了超过5万家的有客服场景的APP和企业。

    我们怎么进入到营销服务领域?在这个过程当中做了什么样的实践?





    这张图是我们每一个企业要面对的,你覆盖的客户群体人数、客单价多少,可以算出整个生意有多大。这个沉淀用户是指已经触达到用户,或者注册了网站,或者订阅了你的微信公众号等等这些用户。还有一部分是你的付费用户,对于电商企业来讲很重要的两个环节是怎样降低你的获取成本,怎样降低用户转化成本,我们做了很多的实践,我们接触了很多客户,我们发现在这个环节可以给用户带来很多价值。





    如果拿一张图把整个营销获取用户到转化客户变成付费用户的过程当中,上面部分是获取成本,下面两个图是关乎转化率。用户的角度看上去接触的点是营销环节和客服环节,进而往后走就是一些落地点,现在大部分电商说落地点放在淘系,很多电商说有自己的网站、APP,这些都是企业商品的落地点,再往后走就是企业客户关系系统和物流、企业资源管理、ERP等等。

    今天王詠谈了关于企业大数据、关于电商大数据方面的话题,其实我感触特别深刻,用户很多行为,包括购买前的数据、购买后的数据,或多或少在企业内都有管理系统。通过这一套系统怎样做营销投放,再回到针对用户环节的营销、客服怎样使得这个环节可以做到闭环。

    环信在过去的实践当中我们会基于一个核心的点,就是讲“长连接”技术,指你时时刻刻和你的潜在客户、和你的目标客户、付费客户保持一条长连接通道,使得你随时可以找到你的客户,可以随时触达他们。

    从三个维度讲:

    第一,长连接相对传统客服可以随时触达你的客户,改变被动的接受用户询问的现状。

    第二,相对于传统的消息推送,长连接使得你与客户的互动是双向的、及时的。

    第三,结合自己的数据管理平台、环信的管理平台,可以使得你精准的挑选客户。

    举几个例子:

    第一,国美,国美有上亿的客户,他们客服系统用的是我们的,营销环节有什么机制可以随时找到他们、随时联系他们,随时和他们互动,他们的反馈可以让我们知道。

    比如说用户,国美APP放在后台或者锁屏状态,我们可以做到哪怕这个后台或者锁屏状态客服的坐席依然根据一定的规则发放消息给国美所有客户和部分客户,使得我们消息可以及时的触达,用户一点击基本上上做到客服状态了,后台可以监测哪些用户点了这个消息,有多少点了消息,有多少回复消息,可以做精准统计,用户行为洞察可以比单向消息推送更加细致。

    第二,楚楚街,他们号称“小淘宝”定向推动2000套儿童座椅给3-4岁儿童的妈妈,我们给楚楚街做了客户管理系统,通过这些找到目标群体,通过基于长连接可以时时刻刻和用户保持长连接的客服系统,将一些非常具有价值的营销消息给APP用户,只要APP没有被卸载,用户就可以看到我们消息,及时产生互动。

    第三,在金融界,互联网金融在大的范畴类也属于在线卖东西,只不过交付形式略有不同。

    一是为电话销售配置一个APP的主动营销的平台,当用户上线及时的推送,用户一上APP及时的推送用户行为,之前的历史行为、购买习惯给客户经理。

    二是结合用户分析推荐相关产品和用户密切互动。一些新的客户可以进入销售公海池,让新的销售抢单,立刻和客户产生互动。

    再举一个例子,楚楚街利用“回呼”功能降低退换货率,我们有的时候发现这个客户下单了,但是有点问题,比如说地址不对等等,我们审核的时候立刻发现这个单子有问题,通过“回呼”找到客户,减少因为小事情碰到一些退货情况。

    综合来讲,我们之前并没有主动做这方面,但是我们发现通过两个产品结合已经做了很多精准化营销方面的东西,我们与客户保持了长连接,这个长连接是即时通讯云的产品。在淘系、在腾讯,我们都离不开他们,因为他们与客户保持了密切联系,你们依然可以构建自己的APP,通过环信的即时通讯与用户保持长连接,业界当中除了旺旺、除了微信、除了陌陌第四大和用户保持长连接的平台就是环信。

    我的演讲到此结束,如果大家需要交流可以来我们展台继续沟通。谢谢大家! 查看全部
     
    [思路网注]程旭文分享了包括国美在线、楚楚街、金融界等很多环信客户在营销领域的应用。怎样做营销投放,再回到针对用户环节的营销、客服怎样使得这个环节可以做到闭环。
       【亿邦动力网讯】12月19日消息,在2016亿邦未来零售大会新服务·思路服务分论坛上环信VP程旭文发表了《从被动客服到主动营销》演讲,他表示:“其实环信在很长时间内我们并没有做营销类的领域,更多的在做SaaS软件,主要做两个软件: 第一,即时通讯云。 第二,全媒体智能客服。”


    001.jpg

    图为环信VP副总裁程旭文


    2016亿邦未来零售大会由亿邦动力网主办,思路网协办,于12月19日-21日在广州白云万达希尔顿酒店举行。国内外电商领域知名企业高管、专家学者、媒体代表共计2000余人出席。

    本届大会以“新物种、新规则、新电商”为主题,包括两天的主论坛、五场分论坛、电商经理人之夜以及马蹄社和亿邦疯人会等系列活动。值得关注的是,在本届大会上,电商产业所熟知的如阿里巴巴、京东、唯品会、当当、亚马逊等面孔都没有出现,取而代之的全部是新生代的零售平台和品牌商阵营,反映了电商领域正寻求破局、寻找新增长的行业心态。

    (温馨提示:本文为速记初审稿,保证现场嘉宾原意,未经删节,或存纰漏,敬请谅解。)

    以下是演讲实录:

    程旭文:我也是接到一个命题作文,其实环信在很长时间内我们并没有单独去做营销领域,更多的在做SaaS软件,我们主要做两个软件:

    第一,即时通讯云。

    第二,全媒体智能客服。

    以上两个都是IT领域做在线软件交付。

    我们一不小心进入了营销行业,我们可以基于环信的基础,包括服务10万多家APP企业,服务了超过5万家的有客服场景的APP和企业。

    我们怎么进入到营销服务领域?在这个过程当中做了什么样的实践?

    001.jpg

    这张图是我们每一个企业要面对的,你覆盖的客户群体人数、客单价多少,可以算出整个生意有多大。这个沉淀用户是指已经触达到用户,或者注册了网站,或者订阅了你的微信公众号等等这些用户。还有一部分是你的付费用户,对于电商企业来讲很重要的两个环节是怎样降低你的获取成本,怎样降低用户转化成本,我们做了很多的实践,我们接触了很多客户,我们发现在这个环节可以给用户带来很多价值。

    002.jpg

    如果拿一张图把整个营销获取用户到转化客户变成付费用户的过程当中,上面部分是获取成本,下面两个图是关乎转化率。用户的角度看上去接触的点是营销环节和客服环节,进而往后走就是一些落地点,现在大部分电商说落地点放在淘系,很多电商说有自己的网站、APP,这些都是企业商品的落地点,再往后走就是企业客户关系系统和物流、企业资源管理、ERP等等。

    今天王詠谈了关于企业大数据、关于电商大数据方面的话题,其实我感触特别深刻,用户很多行为,包括购买前的数据、购买后的数据,或多或少在企业内都有管理系统。通过这一套系统怎样做营销投放,再回到针对用户环节的营销、客服怎样使得这个环节可以做到闭环。

    环信在过去的实践当中我们会基于一个核心的点,就是讲“长连接”技术,指你时时刻刻和你的潜在客户、和你的目标客户、付费客户保持一条长连接通道,使得你随时可以找到你的客户,可以随时触达他们。

    从三个维度讲:

    第一,长连接相对传统客服可以随时触达你的客户,改变被动的接受用户询问的现状。

    第二,相对于传统的消息推送,长连接使得你与客户的互动是双向的、及时的。

    第三,结合自己的数据管理平台、环信的管理平台,可以使得你精准的挑选客户。

    举几个例子:

    第一,国美,国美有上亿的客户,他们客服系统用的是我们的,营销环节有什么机制可以随时找到他们、随时联系他们,随时和他们互动,他们的反馈可以让我们知道。

    比如说用户,国美APP放在后台或者锁屏状态,我们可以做到哪怕这个后台或者锁屏状态客服的坐席依然根据一定的规则发放消息给国美所有客户和部分客户,使得我们消息可以及时的触达,用户一点击基本上上做到客服状态了,后台可以监测哪些用户点了这个消息,有多少点了消息,有多少回复消息,可以做精准统计,用户行为洞察可以比单向消息推送更加细致。

    第二,楚楚街,他们号称“小淘宝”定向推动2000套儿童座椅给3-4岁儿童的妈妈,我们给楚楚街做了客户管理系统,通过这些找到目标群体,通过基于长连接可以时时刻刻和用户保持长连接的客服系统,将一些非常具有价值的营销消息给APP用户,只要APP没有被卸载,用户就可以看到我们消息,及时产生互动。

    第三,在金融界,互联网金融在大的范畴类也属于在线卖东西,只不过交付形式略有不同。

    一是为电话销售配置一个APP的主动营销的平台,当用户上线及时的推送,用户一上APP及时的推送用户行为,之前的历史行为、购买习惯给客户经理。

    二是结合用户分析推荐相关产品和用户密切互动。一些新的客户可以进入销售公海池,让新的销售抢单,立刻和客户产生互动。

    再举一个例子,楚楚街利用“回呼”功能降低退换货率,我们有的时候发现这个客户下单了,但是有点问题,比如说地址不对等等,我们审核的时候立刻发现这个单子有问题,通过“回呼”找到客户,减少因为小事情碰到一些退货情况。

    综合来讲,我们之前并没有主动做这方面,但是我们发现通过两个产品结合已经做了很多精准化营销方面的东西,我们与客户保持了长连接,这个长连接是即时通讯云的产品。在淘系、在腾讯,我们都离不开他们,因为他们与客户保持了密切联系,你们依然可以构建自己的APP,通过环信的即时通讯与用户保持长连接,业界当中除了旺旺、除了微信、除了陌陌第四大和用户保持长连接的平台就是环信。

    我的演讲到此结束,如果大家需要交流可以来我们展台继续沟通。谢谢大家!
    0
    评论

    李理:自动梯度求解 反向传播算法的另外一种视角 深度学习 环信 人工智能 李理

    beyond 发表了文章 • 337 次浏览 • 2016-12-12 14:59 • 来自相关话题

    本系列文章面向深度学习研发者,希望通过Image Caption Generation,一个有意思的具体任务,深入浅出地介绍深度学习的知识。本系列文章涉及到很多深度学习流行的模型,如CNN,RNN/LSTM,Attention等。本文为第四篇。
    作者:李理 目前就职于环信,即时通讯云平台和全媒体智能客服平台,在环信从事智能客服和智能机器人相关工作,致力于用深度学习来提高智能机器人的性能。
    相关文章: 
    环信李理:从Image Caption Generation了解深度学习
    李理:从Image Caption Generation理解深度学习(part II)
    李理:从Image Caption Generation理解深度学习(part III)
    接下来介绍一种非常重要的神经网络——卷积神经网络。这种神经网络在计算机视觉领域取得了重大的成功,而且在自然语言处理等其它领域也有很好的应用。深度学习受到大家的关注很大一个原因就是Alex等人实现的AlexNet(一种深度卷积神经网络)在LSVRC-2010 ImageNet这个比赛中取得了非常好的成绩。此后,卷积神经网络及其变种被广泛应用于各种图像相关任务。

    这里主要参考了Neural Networks and Deep Learning和cs231n的课程来介绍CNN,两部分都会有理论和代码。前者会用theano来实现,而后者会使用我们前一部分介绍的自动梯度来实现。下面首先介绍Michael Nielsen的部分(其实主要是翻译,然后加一些我自己的理解)。

    前面的话

    如果读者自己尝试了上一部分的代码,调过3层和5层全连接的神经网络的参数,我们会发现神经网络的层数越多,参数(超参数)就越难调。但是如果参数调得好,深的网络的效果确实比较浅的好(这也是为什么我们要搞深度学习的原因)。所以深度学习有这样的说法:“三个 bound 不如一个 heuristic,三个 heuristic 不如一个trick”。以前搞机器学习就是feature engineering加调参,现在就剩下调参了。网络的结构,参数的初始化,learning_rate,迭代次数等等都会影响最终的结果。有兴趣的同学可以看看Michael Nielsen这个电子书的相应章节,cs231n的Github资源也有介绍,另外《Neural Networks: Tricks of the Trade》这本书,看名字就知道讲啥的了吧。

    不过我们还是回到正题“卷积神经网络”吧。

    CNN简介

    在之前的章节我们使用了神经网络来解决手写数字识别(MNIST)的问题。我们使用了全连接的神经网络,也就是前一层的每一个神经元都会连接到后一层的每一个神经元,如果前一层有m个节点,后一层有n个,那么总共有m*n条边(连接)。连接方式如下图所示:




    具体来讲,对于输入图片的每一个像素,我们把它的灰度值作为对应神经元的输入。对于28×28的图像来说,我们的网络有784个输入神经元。然后我们训练这个网络的weights和biases来使得它可以正确的预测对应的数字。

    我们之前设计的神经网络工作的很好:在MNIST手写识别数据集上我们得到了超过98%的准确率。但是仔细想一想的话,使用全连接的网络来识别图像有一些奇怪。因为这样的网络结构没有考虑图像的空间结构。比如,它对于空间上很近或者很远的像素一样的对待。这些空间的概念【比如7字会出现某些像素在某个水平方向同时灰度值差不多,也就是上面的那一横】必须靠网络从训练数据中推测出来【但是如果训练数据不够而且图像没有做居中等归一化的话,如果训练数据的7的一横都出现在图像靠左的地方,而测试数据把7写到右下角,那么网络很可能学不到这样的特征】。那为什么我们不能设计一直网络结构考虑这些空间结构呢?这样的想法就是下面我们要讨论的CNN的思想。

    这种神经网络利用了空间结构,因此非常适合用来做图片分类。这种结构训练也非常的快,因此也可以训练更“深”的网络。目前,图像识别大都使用深层的卷积神经网络及其变种。

    卷积神经网络有3个基本的idea:局部感知域(Local Recpetive Field),权值共享和池化(Pooling)。下面我们来一个一个的介绍它们。

    局部感知域

    在前面图示的全连接的层里,输入是被描述成一列神经元。而在卷积网络里,我们把输入看成28×28方格的二维神经元,它的每一个神经元对应于图片在这个像素点的强度(灰度值),如下图所示:




    和往常一样,我们把输入像素连接到隐藏层的神经元。但是我们这里不再把输入的每一个像素都连接到隐藏层的每一个神经元。与之不同,我们把很小的相临近的区域内的输入连接在一起。

    更加具体的来讲,隐藏层的每一个神经元都会与输入层一个很小的区域(比如一个5×5的区域,也就是25个像素点)相连接。隐藏对于隐藏层的某一个神经元,连接如下图所示:




    输入图像的这个区域叫做那个隐藏层神经元的局部感知域。这是输入像素的一个小窗口。每个连接都有一个可以学习的权重,此外还有一个bias。你可以把那个神经元想象成用来分析这个局部感知域的。

    我们然后在整个输入图像上滑动这个局部感知域。对于每一个局部感知域,都有一个隐藏层的神经元与之对应。为了具体一点的展示,我们首先从最左上角的局部感知域开始:




    然后我们向右滑动这个局部感知域:




    以此类推,我们可以构建出第一个隐藏层。注意,如果我们的输入是28×28,并且使用5×5的局部关注域,那么隐藏层是24×24。因为我们只能向右和向下移动23个像素,再往下移动就会移出图像的边界了。【说明,后面我们会介绍padding和striding,从而让图像在经过这样一次卷积处理后尺寸可以不变小】

    这里我们展示了一次向右/下移动一个像素。事实上,我们也可以使用一次移动不止一个像素【这个移动的值叫stride】。比如,我们可以一次向右/下移动两个像素。在这篇文章里,我们只使用stride为1来实验,但是请读者知道其他人可能会用不同的stride值。

    共享权值

    之前提到过每一个隐藏层的神经元有一个5×5的权值。这24×24个隐藏层对应的权值是相同的。也就是说,对于隐藏层的第j,k个神经元,输出如下:
    σ(b+∑l=04∑m=04wl,maj+l,k+m)
    这里,σ是激活函数,可以是我们之前提到的sigmoid函数。b是共享的bias,Wl,m 是5×5的共享权值。ax,y 是输入在x,y的激活。

    【从这个公式可以看出,权值是5×5的矩阵,不同的局部感知域使用这一个参数矩阵和bias】

    这意味着这一个隐藏层的所有神经元都是检测同一个特征,只不过它们位于图片的不同位置而已。比如这组weights和bias是某个局部感知域学到的用来识别一个垂直的边。那么预测的时候不管这条边在哪个位置,它都会被某个对于的局部感知域检测到。更抽象一点,卷积网络能很好的适应图片的位置变化:把图片中的猫稍微移动一下位置,它仍然知道这是一只猫。

    因为这个原因,我们有时把输入层到隐藏层的映射叫做特征映射(feature map)。我们把定义特征映射的权重叫做共享的权重(shared weights),bias叫做共享的bias(shared bais)。这组weights和bias定义了一个kernel或者filter。

    上面描述的网络结构只能检测一种局部的特征。为了识别图片,我们需要更多的特征映射。隐藏一个完整的卷积神经网络会有很多不同的特征映射:




    在上面的例子里,我们有3个特征映射。每个映射由一个5×5的weights和一个biase确定。因此这个网络能检测3种特征,不管这3个特征出现在图像的那个局部感知域里。

    为了简化,上面之展示了3个特征映射。在实际使用的卷积神经网络中我们会使用非常多的特征映射。早期的一个卷积神经网络——LeNet-5,使用了6个特征映射,每一个都是5×5的局部感知域,来识别MNIST数字。因此上面的例子和LeNet-5很接近。后面我们开发的卷积层将使用20和40个特征映射。下面我们先看看模型学习到的一些特征:




    这20个图片对应了20个不同的特征映射。每个映射是一个5×5的图像,对应于局部感知域的5×5个权重。颜色越白(浅)说明权值越小(一般都是负的),因此对应像素对于识别这个特征越不重要。颜色越深(黑)说明权值越大,对应的像素越重要。

    那么我们可以从这些特征映射里得出什么结论呢?很显然这里包含了非随机的空间结构。这说明我们的网络学到了一些空间结构。但是,也很难说它具体学到了哪些特征。我们学到的不是一个 Gabor滤波器 的。事实上有很多研究工作尝试理解机器到底学到了什么样的特征。如果你感兴趣,可以参考Matthew Zeiler 和 Rob Fergus在2013年的论文 Visualizing and Understanding Convolutional Networks。

    共享权重和bias的一大好处是它极大的减少了网络的参数数量。对于每一个特征映射,我们只需要 25=5×5 个权重,再加一个bias。因此一个特征映射只有26个参数。如果我们有20个特征映射,那么只有20×26=520个参数。如果我们使用全连接的神经网络结构,假设隐藏层有30个神经元(这并不算很多),那么就有784*30个权重参数,再加上30个bias,总共有23,550个参数。换句话说,全连接的网络比卷积网络的参数多了40倍。

    当然,我们不能直接比较两种网络的参数,因为这两种模型有本质的区别。但是,凭直觉,由于卷积网络有平移不变的特性,为了达到相同的效果,它也可能使用更少的参数。由于参数变少,卷积网络的训练速度也更快,从而相同的计算资源我们可以训练更深的网络。

    “卷积”神经网络是因为公式(1)里的运算叫做“卷积运算”。更加具体一点,我们可以把公式(1)里的求和写成卷积:$a^1 = \sigma(b + w * a^0)$。*在这里不是乘法,而是卷积运算。这里不会讨论卷积的细节,所以读者如果不懂也不要担心,这里只不过是为了解释卷积神经网络这个名字的由来。【建议感兴趣的读者参考colah的博客文章 《Understanding Convolutions》】

    池化(Pooling)

    除了上面的卷积层,卷积神经网络也包括池化层(pooling layers)。池化层一般都直接放在卷积层后面池化层的目的是简化从卷积层输出的信息。

    更具体一点,一个池化层把卷积层的输出作为其输入并且输出一个更紧凑(condensed)的特征映射。比如,池化层的每一个神经元都提取了之前那个卷积层的一个2×2区域的信息。更为具体的一个例子,一种非常常见的池化操作叫做Max-pooling。在Max-Pooling中,这个神经元选择2×2区域里激活值最大的值,如下图所示:




    注意卷积层的输出是24×24的,而池化后是12×12的。

    就像上面提到的,卷积层通常会有多个特征映射。我们会对每一个特征映射进行max-pooling操作。因此,如果一个卷积层有3个特征映射,那么卷积加max-pooling后就如下图所示:




    我们可以把max-pooling看成神经网络关心某个特征在这个区域里是否出现。它忽略了这个特征出现的具体位置。直觉上看,如果某个特征出现了,那么这个特征相对于其它特征的精确位置是不重要的【精确位置不重要,但是大致的位置是重要的,比如识别一个猫,两只眼睛和鼻子有一个大致的相对位置关系,但是在一个2×2的小区域里稍微移动一下眼睛,应该不太影响我们识别一只猫,而且它还能解决图像拍摄角度变化,扭曲等问题】。而且一个很大的好处是池化可以减少特征的个数【2×2的max-pooling让特征的大小变为原来的1/4】,因此减少了之后层的参数个数。

    Max-pooling不是唯一的池化方法。另外一种常见的是L2 Pooling。这种方法不是取2×2区域的最大值,而是2×2区域的每个值平方然后求和然后取平方根。虽然细节有所不同,但思路和max-pooling是类似的:L2 Pooling也是从卷积层压缩信息的一种方法。在实践中,两种方法都被广泛使用。有时人们也使用其它的池化方法。如果你真的想尝试不同的方法来提供性能,那么你可以使用validation数据来尝试不同池化方法然后选择最合适的方法。但是这里我们不在讨论这些细节。【Max-Pooling是用的最多的,甚至也有人认为Pooling并没有什么卵用。深度学习一个问题就是很多经验的tricks由于没有太多理论依据,只是因为最早的人用了,而且看起来效果不错(但可能换一个数据集就不一定了),所以后面的人也跟着用。但是过了没多久又被认为这个trick其实没啥用】

    放到一起

    现在我们可以把这3个idea放到一起来构建一个完整的卷积神经网络了。它和之前我们看到的结构类似,不过增加了一个有10个神经元的输出层,这个层的每个神经元对应于0-9直接的一个数字:





    这个网络的输入的大小是28×28,每一个输入对于MNIST图像的一个像素。然后使用了3个特征映射,局部感知域的大小是5×5。这样得到3×24×24的输出。然后使用对每一个特征映射的输出应用2×2的max-pooling,得到3×12×12的输出。

    最后一层是全连接的网络,3×12×12个神经元会连接到输出10个神经元中的每一个。这和之前介绍的全连接神经网络是一样的。

    卷积结构和之前的全连接结构有很大的差别。但是整体的图景是类似的:一个神经网络有很多神经元,它们的行为有weights和biase确定。并且整体的目标也是类似的:使用训练数据来训练网络的weights和biases使得网络能够尽量好的识别图片。

    和之前介绍的一样,这里我们仍然使用随机梯度下降来训练。不过反向传播算法有所不同。原因是之前bp算法的推导是基于全连接的神经网络。不过幸运的是求卷积和max-pooling的导数是非常简单的。如果你想了解细节,请自己推导。【这篇文章不会介绍CNN的梯度求解,后面实现使用的是theano,后面介绍CS231N的CNN是会介绍怎么自己来基于自动求导来求这个梯度,而且还会介绍高效的算法,感兴趣的读者请持续关注】

    CNN实战

    前面我们介绍了CNN的基本理论,但是没有讲怎么求梯度。这里的代码是用theano来自动求梯度的。我们可以暂时把cnn看出一个黑盒,试试用它来识别MNIST的数字。后面的文章会介绍theano以及怎么用theano实现CNN。

    代码

    首先得到代码: git clone

    安装theano

    参考这里 ;如果是ubuntu的系统,可以参考这里 ;如果您的机器有gpu,请安装好cuda以及让theano支持gpu。

    默认的network3.py的第52行是 GPU = True,如果您的机器没有gpu,请把这一行改成GPU = False

    baseline

    首先我们实现一个baseline的系统,我们构建一个只有一个隐藏层的3层全连接网络,隐藏层100个神经元。我们训练时60个epoch,使用learning rate $\eta = 0.1$,batch大小是10,没有正则化:$cd src
    $ipython
    >>> import network3
    >>> from network3 import Network
    >>> from network3 import ConvPoolLayer, FullyConnectedLayer, SoftmaxLayer
    >>> training_data, validation_data, test_data = network3.load_data_shared()
    >>> mini_batch_size = 10
    >>> net = Network([
    FullyConnectedLayer(n_in=784, n_out=100),
    SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
    >>> net.SGD(training_data, 60, mini_batch_size, 0.1,
    validation_data, test_data)得到的分类准确率是97.8%。这是在test_data上的准确率,这个模型使用训练数据训练,并根据validation_data来选择当前最好的模型。使用validation数据来可以避免过拟合。读者运行时可能结果会有一些差异,因为模型的参数是随机初始化的。

    改进版本1

    我们首先在输入的后面增加一个卷积层。我们使用5 5的局部感知域,stride等于1,20个特征映射。然后接一个2 2的max-pooling层。之后接一个全连接的层,最后是softmax(仿射变换加softmax):




    在这种网络结构中,我们可以认为卷积和池化层可以学会输入图片的局部的空间特征,而全连接的层整合全局的信息,学习出更抽象的特征。这是卷积神经网络的常见结构。

    下面是代码:>>> net = Network([
    ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28),
    filter_shape=(20, 1, 5, 5),
    poolsize=(2, 2)),
    FullyConnectedLayer(n_in=20*12*12, n_out=100),
    SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
    >>> net.SGD(training_data, 60, mini_batch_size, 0.1,
    validation_data, test_data) 【注意图片的大小,开始是(mini_batch_size, 1, 28 ,28),经过一个20个5 5的卷积池层后变成了(mini_batch_size, 20, 24,24),然后在经过2 2的max-pooling后变成了(mini_batch_size, 20, 12, 12),然后接全连接层的时候可以理解成把所以的特征映射展开,也就是20 12 12,所以FullyConnectedLayer的n_in是20 12 12】

    这个模型得到98.78%的准确率,这相对之前的97.8%是一个很大的提高。事实上我们的错误率减少了1/3,这是一个很大的提高。【准确率很高的时候就看错误率的减少,这样比较有成就感,哈哈】

    如果要用gpu,可以把上面的命令保存到一个文件test.py,然后:$THEANO_FLAGS=mode=FAST_RUN,device=gpu,floatX=float32 python test.py 在这个网络结构中,我们吧卷积和池化层看出一个整体。这只是一种习惯。network3.py会把它们当成一个整体,每个卷积层后面都会跟一个池化层。但实际的一些卷积神经网络并不都要接池化层。

    改进版本2

    我们再加入第二个卷积-池化层。这个卷积层插入在第一个卷积层和全连接层中间。我们使用同样的5×5的局部感知域和2×2的max-pooling。代码如下:>>> net = Network([
    ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28),
    filter_shape=(20, 1, 5, 5),
    poolsize=(2, 2)),
    ConvPoolLayer(image_shape=(mini_batch_size, 20, 12, 12),
    filter_shape=(40, 20, 5, 5),
    poolsize=(2, 2)),
    FullyConnectedLayer(n_in=40*4*4, n_out=100),
    SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
    >>> net.SGD(training_data, 60, mini_batch_size, 0.1,
    validation_data, test_data) 【注意图片的大小,开始是(mini_batch_size, 1, 28 ,28),经过一个20个5 5的卷积池层后变成了(mini_batch_size, 20, 24,24),然后在经过2 2的max-pooling后变成了(mini_batch_size, 20, 12, 12)。然后是40个5*5的卷积层,变成了(mini_batch_size, 40, 8, 8),然后是max-pooling得到(mini_batch_size, 40, 4, 4)。然后是全连接的层】 
    这个模型得到99.6%的准确率!

    这里有两个很自然的问题。第一个是:加第二个卷积-池化层有什么意义呢?事实上,你可以认为第二个卷积层的输入是12*12的”图片“,它的”像素“代表某个局部特征。【比如你可以认为第一个卷积层识别眼睛鼻子,而第二个卷积层识别脸,不同生物的脸上面鼻子和眼睛的相对位置是有意义的】

    这是个看起来不错的解释,那么第二个问题来了:第一个卷积层的输出是不同的20个不同的局部特征,因此第二个卷积层的输入是20 12 12。这就像我们输入了20个不同的”图片“,而不是一个”图片“。那第二个卷积层的神经元学到的是什么呢?【如果第一层的卷积网络能识别”眼睛“,”鼻子“,”耳朵“。那么第二层的”脸“就是2个眼睛,2个耳朵,1个鼻子,并且它们满足一定的空间约束。所以第二层的每一个神经元需要连接第一层的每一个输出,如果第二层只连接”眼睛“这个特征映射,那么只能学习出2个眼睛,3个眼睛这样的特征,那就没有什么用处了】

    改进版本3

    使用ReLU激活函数。ReLU的定义是:
    ReLU(x)=max(0,x)
    >>> from network3 import ReLU
    >>> net = Network([
    ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28),
    filter_shape=(20, 1, 5, 5),
    poolsize=(2, 2),
    activation_fn=ReLU),
    ConvPoolLayer(image_shape=(mini_batch_size, 20, 12, 12),
    filter_shape=(40, 20, 5, 5),
    poolsize=(2, 2),
    activation_fn=ReLU),
    FullyConnectedLayer(n_in=40*4*4, n_out=100, activation_fn=ReLU),
    SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
    >>> net.SGD(training_data, 60, mini_batch_size, 0.03,
    validation_data, test_data, lmbda=0.1)使用ReLU后准确率从99.06%提高到99.23%。从作者的经验来看,ReLU总是要比sigmoid激活函数要好。

    但为什么ReLU就比sigmoid或者tanh要好呢?目前并没有很好的理论介绍。ReLU只是在最近几年开始流行起来的。为什么流行的原因是经验:有一些人尝试了ReLU,然后在他们的任务里取得了比sigmoid好的结果,然后其他人也就跟风。理论上没有人证明ReLU是更好的激活函数。【所以说深度学习有很多tricks,可能某几年就流行起来了,但过几年又有人认为这些tricks没有意义。比如最早的pretraining,现在几乎没人用了。】

    改进版本4

    扩展数据。

    深度学习非常依赖于数据。我们可以根据任务的特点”构造“新的数据。一种简单的方法是把训练数据里的数字进行一下平移,旋转等变换。虽然理论上卷积神经网络能学到与位置无关的特征,但如果训练数据里数字总是出现在固定的位置,实际的模型也不一定能学到。所以我们构造一些这样的数据效果会更好。$ python expand_mnist.pyexpand_mnist.py这个脚本就会扩展数据。它只是简单的把图片向上下左右各移动了一个像素。扩展后训练数据从50000个变成了250000个。

    接下来我们用扩展后的数据来训练模型:>>> expanded_training_data, _, _ = network3.load_data_shared(
    "../data/mnist_expanded.pkl.gz")
    >>> net = Network([
    ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28),
    filter_shape=(20, 1, 5, 5),
    poolsize=(2, 2),
    activation_fn=ReLU),
    ConvPoolLayer(image_shape=(mini_batch_size, 20, 12, 12),
    filter_shape=(40, 20, 5, 5),
    poolsize=(2, 2),
    activation_fn=ReLU),
    FullyConnectedLayer(n_in=40*4*4, n_out=100, activation_fn=ReLU),
    SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
    >>> net.SGD(expanded_training_data, 60, mini_batch_size, 0.03,
    validation_data, test_data, lmbda=0.1)这个模型的准确率是99.37%。扩展数据看起来非常trival,但是却极大的提高了识别准确率。

    改进版本5

    接下来还有改进的办法吗?我们的全连接层只有100个神经元,增加神经元有帮助吗? 作者尝试了300和1000个神经元的全连接层,得到了99.46%和99.43%的准确率。相对于99.37%并没有本质的提高。

    那再加一个全连接的层有帮助吗?我们了尝试一下:>>> net = Network([
    ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28),
    filter_shape=(20, 1, 5, 5),
    poolsize=(2, 2),
    activation_fn=ReLU),
    ConvPoolLayer(image_shape=(mini_batch_size, 20, 12, 12),
    filter_shape=(40, 20, 5, 5),
    poolsize=(2, 2),
    activation_fn=ReLU),
    FullyConnectedLayer(n_in=40*4*4, n_out=100, activation_fn=ReLU),
    FullyConnectedLayer(n_in=100, n_out=100, activation_fn=ReLU),
    SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
    >>> net.SGD(expanded_training_data, 60, mini_batch_size, 0.03,
    validation_data, test_data, lmbda=0.1)在第一个全连接的层之后有加了一个100个神经元的全连接层。得到的准确率是99.43%,把这一层的神经元个数从100增加到300个和1000个得到的准确率是99.48 %和99.47%。有一些提高但是也不明显。

    为什么增加更多层提高不多呢,按说它的表达能力变强了,可能的原因是过拟合。那怎么解决过拟合呢?一种方法就是dropout。drop的详细解释请参考这里。简单来说,dropout就是在训练的时候随机的让一些神经元的激活“丢失”,这样网络就能学到更加鲁棒的特征,因为它要求某些神经元”失效“的情况下网络仍然能工作,因此就不会那么依赖某一些神经元,而是每个神经元都有贡献。

    下面是在两个全连接层都加入50%的dropout:>>> net = Network([
    ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28),
    filter_shape=(20, 1, 5, 5),
    poolsize=(2, 2),
    activation_fn=ReLU),
    ConvPoolLayer(image_shape=(mini_batch_size, 20, 12, 12),
    filter_shape=(40, 20, 5, 5),
    poolsize=(2, 2),
    activation_fn=ReLU),
    FullyConnectedLayer(
    n_in=40*4*4, n_out=1000, activation_fn=ReLU, p_dropout=0.5),
    FullyConnectedLayer(
    n_in=1000, n_out=1000, activation_fn=ReLU, p_dropout=0.5),
    SoftmaxLayer(n_in=1000, n_out=10, p_dropout=0.5)],
    mini_batch_size)
    >>> net.SGD(expanded_training_data, 40, mini_batch_size, 0.03,
    validation_data, test_data)使用dropout后,我们得到了99.60%的一个模型。

    这里有两点值得注意:
    训练的epoch变成了40.因为dropout减少了过拟合,所以我们不需要60个epoch。全连接层使用了1000个神经元。因为dropout会丢弃50%的神经元,所以从直觉来看1000个神经元也相当于只有500个。如果过用100个神经元感觉太少了点。作者经过验证发现有了dropout用1000个比300个的效果好。

    改进版本6

    ensemble多个神经网络。作者分别训练了5个神经网络,每一个都达到了99.6%的准确率,然后用它们来投票,得到了99.67%准确率的模型。

    这是一个非常不错的模型了,10000个测试数据只有33个是错误的,我们把错误的图片都列举了出来:




    图片的右上角是正确的分类,右下角是模型的分类。可以发现有些错误可能人也会犯,因为有些数字人也很难分清楚。

    【为什么只对全连接的层使用dropout?】

    如果读者仔细的阅读代码,你会发现我们只对全连接层进行了dropout,而卷积层没有。当然我们也可以对卷积层进行dropout。但是没有必要。因为卷积层本身就有防止过拟合的能力。原因是权值共享强制网络学到的特征是能够应用到任何位置的特征。这让它不太容易学习到特别局部的特征。因此也就没有必要对它进行的dropout了。

    更进一步

    感兴趣的读者可以参考这里,列举了MNIST数据集的最好结果以及对应的论文。目前最好的结果是99.79%

    What’s Next?

    接下来的文章会介绍theano,一个非常流行的深度学习框架,然后会讲解network3.py,也就是怎么用theano实现CNN。敬请关注。 查看全部
    本系列文章面向深度学习研发者,希望通过Image Caption Generation,一个有意思的具体任务,深入浅出地介绍深度学习的知识。本系列文章涉及到很多深度学习流行的模型,如CNN,RNN/LSTM,Attention等。本文为第四篇。

    作者:李理 
    目前就职于环信,即时通讯云平台和全媒体智能客服平台,在环信从事智能客服和智能机器人相关工作,致力于用深度学习来提高智能机器人的性能。

    相关文章: 
    环信李理:从Image Caption Generation了解深度学习
    李理:从Image Caption Generation理解深度学习(part II)
    李理:从Image Caption Generation理解深度学习(part III)
    接下来介绍一种非常重要的神经网络——卷积神经网络。这种神经网络在计算机视觉领域取得了重大的成功,而且在自然语言处理等其它领域也有很好的应用。深度学习受到大家的关注很大一个原因就是Alex等人实现的AlexNet(一种深度卷积神经网络)在LSVRC-2010 ImageNet这个比赛中取得了非常好的成绩。此后,卷积神经网络及其变种被广泛应用于各种图像相关任务。

    这里主要参考了Neural Networks and Deep Learning和cs231n的课程来介绍CNN,两部分都会有理论和代码。前者会用theano来实现,而后者会使用我们前一部分介绍的自动梯度来实现。下面首先介绍Michael Nielsen的部分(其实主要是翻译,然后加一些我自己的理解)。

    前面的话

    如果读者自己尝试了上一部分的代码,调过3层和5层全连接的神经网络的参数,我们会发现神经网络的层数越多,参数(超参数)就越难调。但是如果参数调得好,深的网络的效果确实比较浅的好(这也是为什么我们要搞深度学习的原因)。所以深度学习有这样的说法:“三个 bound 不如一个 heuristic,三个 heuristic 不如一个trick”。以前搞机器学习就是feature engineering加调参,现在就剩下调参了。网络的结构,参数的初始化,learning_rate,迭代次数等等都会影响最终的结果。有兴趣的同学可以看看Michael Nielsen这个电子书的相应章节,cs231n的Github资源也有介绍,另外《Neural Networks: Tricks of the Trade》这本书,看名字就知道讲啥的了吧。

    不过我们还是回到正题“卷积神经网络”吧。

    CNN简介

    在之前的章节我们使用了神经网络来解决手写数字识别(MNIST)的问题。我们使用了全连接的神经网络,也就是前一层的每一个神经元都会连接到后一层的每一个神经元,如果前一层有m个节点,后一层有n个,那么总共有m*n条边(连接)。连接方式如下图所示:
    001.jpg

    具体来讲,对于输入图片的每一个像素,我们把它的灰度值作为对应神经元的输入。对于28×28的图像来说,我们的网络有784个输入神经元。然后我们训练这个网络的weights和biases来使得它可以正确的预测对应的数字。

    我们之前设计的神经网络工作的很好:在MNIST手写识别数据集上我们得到了超过98%的准确率。但是仔细想一想的话,使用全连接的网络来识别图像有一些奇怪。因为这样的网络结构没有考虑图像的空间结构。比如,它对于空间上很近或者很远的像素一样的对待。这些空间的概念【比如7字会出现某些像素在某个水平方向同时灰度值差不多,也就是上面的那一横】必须靠网络从训练数据中推测出来【但是如果训练数据不够而且图像没有做居中等归一化的话,如果训练数据的7的一横都出现在图像靠左的地方,而测试数据把7写到右下角,那么网络很可能学不到这样的特征】。那为什么我们不能设计一直网络结构考虑这些空间结构呢?这样的想法就是下面我们要讨论的CNN的思想。

    这种神经网络利用了空间结构,因此非常适合用来做图片分类。这种结构训练也非常的快,因此也可以训练更“深”的网络。目前,图像识别大都使用深层的卷积神经网络及其变种。

    卷积神经网络有3个基本的idea:局部感知域(Local Recpetive Field),权值共享和池化(Pooling)。下面我们来一个一个的介绍它们。

    局部感知域

    在前面图示的全连接的层里,输入是被描述成一列神经元。而在卷积网络里,我们把输入看成28×28方格的二维神经元,它的每一个神经元对应于图片在这个像素点的强度(灰度值),如下图所示:
    002.jpg

    和往常一样,我们把输入像素连接到隐藏层的神经元。但是我们这里不再把输入的每一个像素都连接到隐藏层的每一个神经元。与之不同,我们把很小的相临近的区域内的输入连接在一起。

    更加具体的来讲,隐藏层的每一个神经元都会与输入层一个很小的区域(比如一个5×5的区域,也就是25个像素点)相连接。隐藏对于隐藏层的某一个神经元,连接如下图所示:
    003.jpg

    输入图像的这个区域叫做那个隐藏层神经元的局部感知域。这是输入像素的一个小窗口。每个连接都有一个可以学习的权重,此外还有一个bias。你可以把那个神经元想象成用来分析这个局部感知域的。

    我们然后在整个输入图像上滑动这个局部感知域。对于每一个局部感知域,都有一个隐藏层的神经元与之对应。为了具体一点的展示,我们首先从最左上角的局部感知域开始:
    004.jpg

    然后我们向右滑动这个局部感知域:
    005.jpg

    以此类推,我们可以构建出第一个隐藏层。注意,如果我们的输入是28×28,并且使用5×5的局部关注域,那么隐藏层是24×24。因为我们只能向右和向下移动23个像素,再往下移动就会移出图像的边界了。【说明,后面我们会介绍padding和striding,从而让图像在经过这样一次卷积处理后尺寸可以不变小】

    这里我们展示了一次向右/下移动一个像素。事实上,我们也可以使用一次移动不止一个像素【这个移动的值叫stride】。比如,我们可以一次向右/下移动两个像素。在这篇文章里,我们只使用stride为1来实验,但是请读者知道其他人可能会用不同的stride值。

    共享权值

    之前提到过每一个隐藏层的神经元有一个5×5的权值。这24×24个隐藏层对应的权值是相同的。也就是说,对于隐藏层的第j,k个神经元,输出如下:

    σ(b+∑l=04∑m=04wl,maj+l,k+m)


    这里,σ是激活函数,可以是我们之前提到的sigmoid函数。b是共享的bias,Wl,m 是5×5的共享权值。ax,y 是输入在x,y的激活。

    【从这个公式可以看出,权值是5×5的矩阵,不同的局部感知域使用这一个参数矩阵和bias】

    这意味着这一个隐藏层的所有神经元都是检测同一个特征,只不过它们位于图片的不同位置而已。比如这组weights和bias是某个局部感知域学到的用来识别一个垂直的边。那么预测的时候不管这条边在哪个位置,它都会被某个对于的局部感知域检测到。更抽象一点,卷积网络能很好的适应图片的位置变化:把图片中的猫稍微移动一下位置,它仍然知道这是一只猫。

    因为这个原因,我们有时把输入层到隐藏层的映射叫做特征映射(feature map)。我们把定义特征映射的权重叫做共享的权重(shared weights),bias叫做共享的bias(shared bais)。这组weights和bias定义了一个kernel或者filter。

    上面描述的网络结构只能检测一种局部的特征。为了识别图片,我们需要更多的特征映射。隐藏一个完整的卷积神经网络会有很多不同的特征映射:
    006.jpg

    在上面的例子里,我们有3个特征映射。每个映射由一个5×5的weights和一个biase确定。因此这个网络能检测3种特征,不管这3个特征出现在图像的那个局部感知域里。

    为了简化,上面之展示了3个特征映射。在实际使用的卷积神经网络中我们会使用非常多的特征映射。早期的一个卷积神经网络——LeNet-5,使用了6个特征映射,每一个都是5×5的局部感知域,来识别MNIST数字。因此上面的例子和LeNet-5很接近。后面我们开发的卷积层将使用20和40个特征映射。下面我们先看看模型学习到的一些特征:
    007.jpg

    这20个图片对应了20个不同的特征映射。每个映射是一个5×5的图像,对应于局部感知域的5×5个权重。颜色越白(浅)说明权值越小(一般都是负的),因此对应像素对于识别这个特征越不重要。颜色越深(黑)说明权值越大,对应的像素越重要。

    那么我们可以从这些特征映射里得出什么结论呢?很显然这里包含了非随机的空间结构。这说明我们的网络学到了一些空间结构。但是,也很难说它具体学到了哪些特征。我们学到的不是一个 Gabor滤波器 的。事实上有很多研究工作尝试理解机器到底学到了什么样的特征。如果你感兴趣,可以参考Matthew Zeiler 和 Rob Fergus在2013年的论文 Visualizing and Understanding Convolutional Networks。

    共享权重和bias的一大好处是它极大的减少了网络的参数数量。对于每一个特征映射,我们只需要 25=5×5 个权重,再加一个bias。因此一个特征映射只有26个参数。如果我们有20个特征映射,那么只有20×26=520个参数。如果我们使用全连接的神经网络结构,假设隐藏层有30个神经元(这并不算很多),那么就有784*30个权重参数,再加上30个bias,总共有23,550个参数。换句话说,全连接的网络比卷积网络的参数多了40倍。

    当然,我们不能直接比较两种网络的参数,因为这两种模型有本质的区别。但是,凭直觉,由于卷积网络有平移不变的特性,为了达到相同的效果,它也可能使用更少的参数。由于参数变少,卷积网络的训练速度也更快,从而相同的计算资源我们可以训练更深的网络。

    “卷积”神经网络是因为公式(1)里的运算叫做“卷积运算”。更加具体一点,我们可以把公式(1)里的求和写成卷积:$a^1 = \sigma(b + w * a^0)$。*在这里不是乘法,而是卷积运算。这里不会讨论卷积的细节,所以读者如果不懂也不要担心,这里只不过是为了解释卷积神经网络这个名字的由来。【建议感兴趣的读者参考colah的博客文章 《Understanding Convolutions》】

    池化(Pooling)

    除了上面的卷积层,卷积神经网络也包括池化层(pooling layers)。池化层一般都直接放在卷积层后面池化层的目的是简化从卷积层输出的信息。

    更具体一点,一个池化层把卷积层的输出作为其输入并且输出一个更紧凑(condensed)的特征映射。比如,池化层的每一个神经元都提取了之前那个卷积层的一个2×2区域的信息。更为具体的一个例子,一种非常常见的池化操作叫做Max-pooling。在Max-Pooling中,这个神经元选择2×2区域里激活值最大的值,如下图所示:
    008.jpg

    注意卷积层的输出是24×24的,而池化后是12×12的。

    就像上面提到的,卷积层通常会有多个特征映射。我们会对每一个特征映射进行max-pooling操作。因此,如果一个卷积层有3个特征映射,那么卷积加max-pooling后就如下图所示:
    009.jpg

    我们可以把max-pooling看成神经网络关心某个特征在这个区域里是否出现。它忽略了这个特征出现的具体位置。直觉上看,如果某个特征出现了,那么这个特征相对于其它特征的精确位置是不重要的【精确位置不重要,但是大致的位置是重要的,比如识别一个猫,两只眼睛和鼻子有一个大致的相对位置关系,但是在一个2×2的小区域里稍微移动一下眼睛,应该不太影响我们识别一只猫,而且它还能解决图像拍摄角度变化,扭曲等问题】。而且一个很大的好处是池化可以减少特征的个数【2×2的max-pooling让特征的大小变为原来的1/4】,因此减少了之后层的参数个数。

    Max-pooling不是唯一的池化方法。另外一种常见的是L2 Pooling。这种方法不是取2×2区域的最大值,而是2×2区域的每个值平方然后求和然后取平方根。虽然细节有所不同,但思路和max-pooling是类似的:L2 Pooling也是从卷积层压缩信息的一种方法。在实践中,两种方法都被广泛使用。有时人们也使用其它的池化方法。如果你真的想尝试不同的方法来提供性能,那么你可以使用validation数据来尝试不同池化方法然后选择最合适的方法。但是这里我们不在讨论这些细节。【Max-Pooling是用的最多的,甚至也有人认为Pooling并没有什么卵用。深度学习一个问题就是很多经验的tricks由于没有太多理论依据,只是因为最早的人用了,而且看起来效果不错(但可能换一个数据集就不一定了),所以后面的人也跟着用。但是过了没多久又被认为这个trick其实没啥用】

    放到一起

    现在我们可以把这3个idea放到一起来构建一个完整的卷积神经网络了。它和之前我们看到的结构类似,不过增加了一个有10个神经元的输出层,这个层的每个神经元对应于0-9直接的一个数字:

    010.jpg

    这个网络的输入的大小是28×28,每一个输入对于MNIST图像的一个像素。然后使用了3个特征映射,局部感知域的大小是5×5。这样得到3×24×24的输出。然后使用对每一个特征映射的输出应用2×2的max-pooling,得到3×12×12的输出。

    最后一层是全连接的网络,3×12×12个神经元会连接到输出10个神经元中的每一个。这和之前介绍的全连接神经网络是一样的。

    卷积结构和之前的全连接结构有很大的差别。但是整体的图景是类似的:一个神经网络有很多神经元,它们的行为有weights和biase确定。并且整体的目标也是类似的:使用训练数据来训练网络的weights和biases使得网络能够尽量好的识别图片。

    和之前介绍的一样,这里我们仍然使用随机梯度下降来训练。不过反向传播算法有所不同。原因是之前bp算法的推导是基于全连接的神经网络。不过幸运的是求卷积和max-pooling的导数是非常简单的。如果你想了解细节,请自己推导。【这篇文章不会介绍CNN的梯度求解,后面实现使用的是theano,后面介绍CS231N的CNN是会介绍怎么自己来基于自动求导来求这个梯度,而且还会介绍高效的算法,感兴趣的读者请持续关注】

    CNN实战

    前面我们介绍了CNN的基本理论,但是没有讲怎么求梯度。这里的代码是用theano来自动求梯度的。我们可以暂时把cnn看出一个黑盒,试试用它来识别MNIST的数字。后面的文章会介绍theano以及怎么用theano实现CNN。

    代码

    首先得到代码: git clone

    安装theano

    参考这里 ;如果是ubuntu的系统,可以参考这里 ;如果您的机器有gpu,请安装好cuda以及让theano支持gpu。

    默认的network3.py的第52行是 GPU = True,如果您的机器没有gpu,请把这一行改成GPU = False

    baseline

    首先我们实现一个baseline的系统,我们构建一个只有一个隐藏层的3层全连接网络,隐藏层100个神经元。我们训练时60个epoch,使用learning rate $\eta = 0.1$,batch大小是10,没有正则化:
    $cd src
    $ipython
    >>> import network3
    >>> from network3 import Network
    >>> from network3 import ConvPoolLayer, FullyConnectedLayer, SoftmaxLayer
    >>> training_data, validation_data, test_data = network3.load_data_shared()
    >>> mini_batch_size = 10
    >>> net = Network([
    FullyConnectedLayer(n_in=784, n_out=100),
    SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
    >>> net.SGD(training_data, 60, mini_batch_size, 0.1,
    validation_data, test_data)
    得到的分类准确率是97.8%。这是在test_data上的准确率,这个模型使用训练数据训练,并根据validation_data来选择当前最好的模型。使用validation数据来可以避免过拟合。读者运行时可能结果会有一些差异,因为模型的参数是随机初始化的。

    改进版本1

    我们首先在输入的后面增加一个卷积层。我们使用5 5的局部感知域,stride等于1,20个特征映射。然后接一个2 2的max-pooling层。之后接一个全连接的层,最后是softmax(仿射变换加softmax):
    011.jpg

    在这种网络结构中,我们可以认为卷积和池化层可以学会输入图片的局部的空间特征,而全连接的层整合全局的信息,学习出更抽象的特征。这是卷积神经网络的常见结构。

    下面是代码:
    >>> net = Network([
    ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28),
    filter_shape=(20, 1, 5, 5),
    poolsize=(2, 2)),
    FullyConnectedLayer(n_in=20*12*12, n_out=100),
    SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
    >>> net.SGD(training_data, 60, mini_batch_size, 0.1,
    validation_data, test_data)
    【注意图片的大小,开始是(mini_batch_size, 1, 28 ,28),经过一个20个5 5的卷积池层后变成了(mini_batch_size, 20, 24,24),然后在经过2 2的max-pooling后变成了(mini_batch_size, 20, 12, 12),然后接全连接层的时候可以理解成把所以的特征映射展开,也就是20 12 12,所以FullyConnectedLayer的n_in是20 12 12】

    这个模型得到98.78%的准确率,这相对之前的97.8%是一个很大的提高。事实上我们的错误率减少了1/3,这是一个很大的提高。【准确率很高的时候就看错误率的减少,这样比较有成就感,哈哈】

    如果要用gpu,可以把上面的命令保存到一个文件test.py,然后:
    $THEANO_FLAGS=mode=FAST_RUN,device=gpu,floatX=float32 python test.py 
    在这个网络结构中,我们吧卷积和池化层看出一个整体。这只是一种习惯。network3.py会把它们当成一个整体,每个卷积层后面都会跟一个池化层。但实际的一些卷积神经网络并不都要接池化层。

    改进版本2

    我们再加入第二个卷积-池化层。这个卷积层插入在第一个卷积层和全连接层中间。我们使用同样的5×5的局部感知域和2×2的max-pooling。代码如下:
    >>> net = Network([
    ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28),
    filter_shape=(20, 1, 5, 5),
    poolsize=(2, 2)),
    ConvPoolLayer(image_shape=(mini_batch_size, 20, 12, 12),
    filter_shape=(40, 20, 5, 5),
    poolsize=(2, 2)),
    FullyConnectedLayer(n_in=40*4*4, n_out=100),
    SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
    >>> net.SGD(training_data, 60, mini_batch_size, 0.1,
    validation_data, test_data)
    【注意图片的大小,开始是(mini_batch_size, 1, 28 ,28),经过一个20个5 5的卷积池层后变成了(mini_batch_size, 20, 24,24),然后在经过2 2的max-pooling后变成了(mini_batch_size, 20, 12, 12)。然后是40个5*5的卷积层,变成了(mini_batch_size, 40, 8, 8),然后是max-pooling得到(mini_batch_size, 40, 4, 4)。然后是全连接的层】 
    这个模型得到99.6%的准确率!

    这里有两个很自然的问题。第一个是:加第二个卷积-池化层有什么意义呢?事实上,你可以认为第二个卷积层的输入是12*12的”图片“,它的”像素“代表某个局部特征。【比如你可以认为第一个卷积层识别眼睛鼻子,而第二个卷积层识别脸,不同生物的脸上面鼻子和眼睛的相对位置是有意义的】

    这是个看起来不错的解释,那么第二个问题来了:第一个卷积层的输出是不同的20个不同的局部特征,因此第二个卷积层的输入是20 12 12。这就像我们输入了20个不同的”图片“,而不是一个”图片“。那第二个卷积层的神经元学到的是什么呢?【如果第一层的卷积网络能识别”眼睛“,”鼻子“,”耳朵“。那么第二层的”脸“就是2个眼睛,2个耳朵,1个鼻子,并且它们满足一定的空间约束。所以第二层的每一个神经元需要连接第一层的每一个输出,如果第二层只连接”眼睛“这个特征映射,那么只能学习出2个眼睛,3个眼睛这样的特征,那就没有什么用处了】

    改进版本3

    使用ReLU激活函数。ReLU的定义是:

    ReLU(x)=max(0,x)


    >>> from network3 import ReLU
    >>> net = Network([
    ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28),
    filter_shape=(20, 1, 5, 5),
    poolsize=(2, 2),
    activation_fn=ReLU),
    ConvPoolLayer(image_shape=(mini_batch_size, 20, 12, 12),
    filter_shape=(40, 20, 5, 5),
    poolsize=(2, 2),
    activation_fn=ReLU),
    FullyConnectedLayer(n_in=40*4*4, n_out=100, activation_fn=ReLU),
    SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
    >>> net.SGD(training_data, 60, mini_batch_size, 0.03,
    validation_data, test_data, lmbda=0.1)
    使用ReLU后准确率从99.06%提高到99.23%。从作者的经验来看,ReLU总是要比sigmoid激活函数要好。

    但为什么ReLU就比sigmoid或者tanh要好呢?目前并没有很好的理论介绍。ReLU只是在最近几年开始流行起来的。为什么流行的原因是经验:有一些人尝试了ReLU,然后在他们的任务里取得了比sigmoid好的结果,然后其他人也就跟风。理论上没有人证明ReLU是更好的激活函数。【所以说深度学习有很多tricks,可能某几年就流行起来了,但过几年又有人认为这些tricks没有意义。比如最早的pretraining,现在几乎没人用了。】

    改进版本4

    扩展数据。

    深度学习非常依赖于数据。我们可以根据任务的特点”构造“新的数据。一种简单的方法是把训练数据里的数字进行一下平移,旋转等变换。虽然理论上卷积神经网络能学到与位置无关的特征,但如果训练数据里数字总是出现在固定的位置,实际的模型也不一定能学到。所以我们构造一些这样的数据效果会更好。
    $ python expand_mnist.py
    expand_mnist.py这个脚本就会扩展数据。它只是简单的把图片向上下左右各移动了一个像素。扩展后训练数据从50000个变成了250000个。

    接下来我们用扩展后的数据来训练模型:
    >>> expanded_training_data, _, _ = network3.load_data_shared(
    "../data/mnist_expanded.pkl.gz")
    >>> net = Network([
    ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28),
    filter_shape=(20, 1, 5, 5),
    poolsize=(2, 2),
    activation_fn=ReLU),
    ConvPoolLayer(image_shape=(mini_batch_size, 20, 12, 12),
    filter_shape=(40, 20, 5, 5),
    poolsize=(2, 2),
    activation_fn=ReLU),
    FullyConnectedLayer(n_in=40*4*4, n_out=100, activation_fn=ReLU),
    SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
    >>> net.SGD(expanded_training_data, 60, mini_batch_size, 0.03,
    validation_data, test_data, lmbda=0.1)
    这个模型的准确率是99.37%。扩展数据看起来非常trival,但是却极大的提高了识别准确率。

    改进版本5

    接下来还有改进的办法吗?我们的全连接层只有100个神经元,增加神经元有帮助吗? 作者尝试了300和1000个神经元的全连接层,得到了99.46%和99.43%的准确率。相对于99.37%并没有本质的提高。

    那再加一个全连接的层有帮助吗?我们了尝试一下:
    >>> net = Network([
    ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28),
    filter_shape=(20, 1, 5, 5),
    poolsize=(2, 2),
    activation_fn=ReLU),
    ConvPoolLayer(image_shape=(mini_batch_size, 20, 12, 12),
    filter_shape=(40, 20, 5, 5),
    poolsize=(2, 2),
    activation_fn=ReLU),
    FullyConnectedLayer(n_in=40*4*4, n_out=100, activation_fn=ReLU),
    FullyConnectedLayer(n_in=100, n_out=100, activation_fn=ReLU),
    SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
    >>> net.SGD(expanded_training_data, 60, mini_batch_size, 0.03,
    validation_data, test_data, lmbda=0.1)
    在第一个全连接的层之后有加了一个100个神经元的全连接层。得到的准确率是99.43%,把这一层的神经元个数从100增加到300个和1000个得到的准确率是99.48 %和99.47%。有一些提高但是也不明显。

    为什么增加更多层提高不多呢,按说它的表达能力变强了,可能的原因是过拟合。那怎么解决过拟合呢?一种方法就是dropout。drop的详细解释请参考这里。简单来说,dropout就是在训练的时候随机的让一些神经元的激活“丢失”,这样网络就能学到更加鲁棒的特征,因为它要求某些神经元”失效“的情况下网络仍然能工作,因此就不会那么依赖某一些神经元,而是每个神经元都有贡献。

    下面是在两个全连接层都加入50%的dropout:
    >>> net = Network([
    ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28),
    filter_shape=(20, 1, 5, 5),
    poolsize=(2, 2),
    activation_fn=ReLU),
    ConvPoolLayer(image_shape=(mini_batch_size, 20, 12, 12),
    filter_shape=(40, 20, 5, 5),
    poolsize=(2, 2),
    activation_fn=ReLU),
    FullyConnectedLayer(
    n_in=40*4*4, n_out=1000, activation_fn=ReLU, p_dropout=0.5),
    FullyConnectedLayer(
    n_in=1000, n_out=1000, activation_fn=ReLU, p_dropout=0.5),
    SoftmaxLayer(n_in=1000, n_out=10, p_dropout=0.5)],
    mini_batch_size)
    >>> net.SGD(expanded_training_data, 40, mini_batch_size, 0.03,
    validation_data, test_data)
    使用dropout后,我们得到了99.60%的一个模型。

    这里有两点值得注意:
    • 训练的epoch变成了40.因为dropout减少了过拟合,所以我们不需要60个epoch。
    • 全连接层使用了1000个神经元。因为dropout会丢弃50%的神经元,所以从直觉来看1000个神经元也相当于只有500个。如果过用100个神经元感觉太少了点。作者经过验证发现有了dropout用1000个比300个的效果好。


    改进版本6

    ensemble多个神经网络。作者分别训练了5个神经网络,每一个都达到了99.6%的准确率,然后用它们来投票,得到了99.67%准确率的模型。

    这是一个非常不错的模型了,10000个测试数据只有33个是错误的,我们把错误的图片都列举了出来:
    012.jpg

    图片的右上角是正确的分类,右下角是模型的分类。可以发现有些错误可能人也会犯,因为有些数字人也很难分清楚。

    【为什么只对全连接的层使用dropout?】

    如果读者仔细的阅读代码,你会发现我们只对全连接层进行了dropout,而卷积层没有。当然我们也可以对卷积层进行dropout。但是没有必要。因为卷积层本身就有防止过拟合的能力。原因是权值共享强制网络学到的特征是能够应用到任何位置的特征。这让它不太容易学习到特别局部的特征。因此也就没有必要对它进行的dropout了。

    更进一步

    感兴趣的读者可以参考这里,列举了MNIST数据集的最好结果以及对应的论文。目前最好的结果是99.79%

    What’s Next?

    接下来的文章会介绍theano,一个非常流行的深度学习框架,然后会讲解network3.py,也就是怎么用theano实现CNN。敬请关注。
    3
    回复

    在集成easeUI后,发送消息老显示失败是怎么回事? 环信 Android

    Wxin 回复了问题 • 2 人关注 • 190 次浏览 • 2016-12-09 22:32 • 来自相关话题

    0
    评论

    2017创业路上,我们陪你 环信 创业

    beyond 发表了文章 • 262 次浏览 • 2016-12-07 17:29 • 来自相关话题

    2015年,市场上的热词是:融资、D轮、A轮、万亿市场……
    2016年,市场的热词成为:裁员、资本寒冬、万亿市场……
    为争夺互联网新格局制高点、行业No1以及中国企业服务这万亿市场,Saas、Paas、Caas服务的创业者们不断涌现。但是他们的创业路却不尽相同。
       我现任COO是个创业狂,2012年至今,创业三次。2012年创立打车品牌,与滴滴、快滴以及当时的众多打车app抢占中国市场,但始终因为资金不敌滴滴背后的大腾讯,以滴滴成功占领全国打车市场成功,他的品牌失败告终。
    后来问及他,这么多创业经历,最大的感触是什么?困难是什么?他说创业要考虑很多事情:创业项目是什么?配置怎样的一个团队?时间节点怎么安排?找投资人、员工激励政策、员工效率、创业失败怎么个退路……

       整个创业过程中,融资固然重要,但是组建原始团队是迫切大事。但组团难就难在好的技术人员,比较难挖。招人的同时,还要兼顾融资、立项、开发、担当半个产品……招了人还要搞一堆社保福利……搞得自己精力被分去了一半。
    “那为什么当时不找个靠谱的招聘公司,不找个办事效率高的代理注册公司?”“12年的时候招聘公司就那么几家,注册公司有那沟通的时间,都不如自己跑!”

    评:多么诚实坦荡、干脆利落的一个创业老板……

       除了COO的惨痛创业经历,创业者的你们普遍存在一个错觉(不好意思泼了冷水),感觉通过互联网的渠道,做好推广、做好产品,用户就会“忽如一夜春分来,千树万树梨花开”的样子。然而除了这个叫微信的软件,几乎没有哪一家的成功这么地一马平川。

       真正能获得最终成功的,不论是不是2B,或2C,又或是B2B2C,还是看是否在整个环境中产生了足够的价值,比如O2O一定是未来,但你的产品并非在这个大帽子底下就一定会成功。
    不过,还好……你的创业是在当下移动互联网发达的2016年,不是互联网“欠发达”的2012年……所以,2017年创业,不要让自己被不必要的事情分心啦!

    2017年创业,有人陪你了!
    创业,有我们没我们,不一样!不一样!不一样!

    点击“立即领取”,领取属于你的创业福利礼包(20份大礼包哦,一般人我不告诉ta)!助力你的企业发展!

    PS:手机端可直接扫码 查看全部
    通用banner.jpg
    2015年,市场上的热词是:融资、D轮、A轮、万亿市场……
    2016年,市场的热词成为:裁员、资本寒冬、万亿市场……
    为争夺互联网新格局制高点、行业No1以及中国企业服务这万亿市场,Saas、Paas、Caas服务的创业者们不断涌现。但是他们的创业路却不尽相同。

       我现任COO是个创业狂,2012年至今,创业三次。2012年创立打车品牌,与滴滴、快滴以及当时的众多打车app抢占中国市场,但始终因为资金不敌滴滴背后的大腾讯,以滴滴成功占领全国打车市场成功,他的品牌失败告终。
    后来问及他,这么多创业经历,最大的感触是什么?困难是什么?他说创业要考虑很多事情:创业项目是什么?配置怎样的一个团队?时间节点怎么安排?找投资人、员工激励政策、员工效率、创业失败怎么个退路……

       整个创业过程中,融资固然重要,但是组建原始团队是迫切大事。但组团难就难在好的技术人员,比较难挖。招人的同时,还要兼顾融资、立项、开发、担当半个产品……招了人还要搞一堆社保福利……搞得自己精力被分去了一半。
    • “那为什么当时不找个靠谱的招聘公司,不找个办事效率高的代理注册公司?”
    • “12年的时候招聘公司就那么几家,注册公司有那沟通的时间,都不如自己跑!”


    评:多么诚实坦荡、干脆利落的一个创业老板……

       除了COO的惨痛创业经历,创业者的你们普遍存在一个错觉(不好意思泼了冷水),感觉通过互联网的渠道,做好推广、做好产品,用户就会“忽如一夜春分来,千树万树梨花开”的样子。然而除了这个叫微信的软件,几乎没有哪一家的成功这么地一马平川。

       真正能获得最终成功的,不论是不是2B,或2C,又或是B2B2C,还是看是否在整个环境中产生了足够的价值,比如O2O一定是未来,但你的产品并非在这个大帽子底下就一定会成功。
    不过,还好……你的创业是在当下移动互联网发达的2016年,不是互联网“欠发达”的2012年……所以,2017年创业,不要让自己被不必要的事情分心啦!

    2017年创业,有人陪你了!
    创业,有我们没我们,不一样!不一样!不一样!

    点击“立即领取”,领取属于你的创业福利礼包(20份大礼包哦,一般人我不告诉ta)!助力你的企业发展!

    PS:手机端可直接扫码
    1481102926.png
    1
    最佳

    集成easeui时一直报错 环信 Android

    ChrisWu 回复了问题 • 2 人关注 • 261 次浏览 • 2016-12-06 17:06 • 来自相关话题

    1
    回复

    注册一直失败,错误码208 环信 Android

    Wxin 回复了问题 • 2 人关注 • 261 次浏览 • 2016-12-04 13:25 • 来自相关话题

    3
    回复

    视频怎么看不了 环信 Android

    mazhihua 回复了问题 • 2 人关注 • 196 次浏览 • 2016-12-03 16:18 • 来自相关话题

    1
    回复

    开启聊天界面时 对方名称总是han 而且发不出去消息 Android 环信

    Wxin 回复了问题 • 2 人关注 • 185 次浏览 • 2016-11-30 21:06 • 来自相关话题

    1
    最佳

    updateMessage这个方法是更新db中已有的信息么?比如说文字消息的内容变了,可以通过这个方法更新? 环信

    zhuhy 回复了问题 • 3 人关注 • 198 次浏览 • 2016-11-29 13:05 • 来自相关话题

    7
    回复

    fudh环信ios工程师 能不能把你集成音视频的demo发给我1217212724 或者给一个连接 iOS 环信 音视频

    马二 回复了问题 • 8 人关注 • 1040 次浏览 • 2016-11-26 18:36 • 来自相关话题

    0
    评论

    李理:从Image Caption Generation理解深度学习(part II) 环信 深度学习 李理

    beyond 发表了文章 • 448 次浏览 • 2016-11-24 14:45 • 来自相关话题

    书接上文:环信李理:从Image Caption Generation了解深度学习(part I
     
    2. 机器学习基本概念和前馈神经网络
    2.1 机器学习基本概念

       大家可能平时都写过很多程序,写程序和机器学习的思路可能有一些不同。写程序时,我们是“上帝”,我们规定计算机的每一个步骤,第一步做什么第二步做什么,我们称之为算法。我们能够控制所有的情况,如果出了任何问题,肯定都是程序员的责任。而在机器学习的时候,我们只是“老师”。我们告诉学生(计算机)输入是什么,输出是什么,然后期望它能够学到和我们类似的知识。比如我们跟小孩说这是狗,那是猫,我们没有办法像上帝那样拿着“纳米手术刀”去操作人脑神 经元的连接方式。我们只能不断的给小孩“训练数据”,然后期望他能够学会什么是猫,即使我们觉得他“学会”了识别猫,我们也没有办法知道他是“怎么”学会 的,而且同样的训练过程可能换一个人就不好使。

       机器学习和人类的学习是类似的——我们也是给它训练数据,然后期望它能学会。我们会给机器建一个模型,从数学的角度来说一个模型就是一个函数,它的输入一般是一个向量【当然可以是二维的矩阵如图片或者三维的张量比如视频】,输出可以是有限的离散的标签如“猫”,“狗”,这类问题我们称之为分类;而如果输出 是连续的值比如用这个模型来预测气温,那么我们就称之为回归。其实人类的很多科学活动和日常生活,都是在“学习”模型和“应用”模型。比如开普勒通过观测 大量天文数据“归纳”出行星的运动规律。从本质上讲,智能就是从“过去”学习,然后根据“现在”来预测可能的将来并根据自己的目标选择有利于自己行为。只不过之前,似乎只有人类能够从数据中“学习”出规律,而人工智能的目标就是让机器也有类似的学习能力。

       模型用数学来说就是一个函数,我们人脑的函数由神经元的连接构成,它可能是一个很复杂的函数,我们现在还很难彻底研究清楚。神经网络就是试图通过计算机来 模拟和借鉴人脑这个模型,除了我们这里要讲的神经网络之外,机器学习领域还有各种各样的模型,它们各有特点。但不管形式怎么变化,本质都是一个函数。一个(或者更准确的是一种)模型一般都是一种函数形式,它有一些“参数”可以改变。而学习的过程就是不断调整这些参数,使得输出(尽量)接近“正确”的答案。 但是一般情况下很难所有的数据我们都能预测正确,所以一般我们会定义一个loss function,可以理解为“错误”的程度,错的越“离谱”,loss就越大。而我们的目标就是调整参数使得loss最小。

       但是我们是在“训练”数据上调整的参数,那么它能在“测试”数据上也表现的好吗?这个就是模型的“泛化”能力了。就和人在学校学习一样,有的同学做过的一 模一样的题就会,但是考试时稍微改变一下就不会了,这就是“泛化”能力太差,学到的不是最本质的东西。所以平时会定期有一些“模拟考试”,来检验学生是不 是真的学会了,如果考得不好,那就打回去重新训练模型调整参数。这在机器学习里对应的就是validation的阶段。最后到最终的考试了,就是最终检验 的时候了,这个试卷里的题目是不能提前让人看到的,只能拿出来用一次,否则就是作弊了。对应到机器学习里就是test阶段。

       当然这里用通俗的话描述了机器学习,主要是有监督的学习。其实机器学习还有无监督的学习和强化学习。前者就是不给答案,只给数据,让人总结规律;而后者会有答案,但是答案不是现在就告诉你。我个人觉得人类社会里更多的是监督学习和强化学习。从人类社会总体来说,强化学习是获取新知识的唯一途径,也就是向自 然学习,我们做了一个决策,其好坏可能要很长一段时间才能显现出来。而学习出来的这些知识通过监督的方式,通过家庭和学校的教育教给下一代。

       另外输出除了简单的分为离散和连续,还可以是序列(时序)的,比如自然语言(文本)是一个字符串的序列 ,对于我们的Image Caption Generation就是生成一个单词序列。另外还有更复杂的输出,比如parsing,输出是一棵语法树。

    2.2 多层神经网络

       前面介绍了机器学习的基本概念,接下来我们就来学习一下神经网络。现在流行的说法“深度学习”,其实大多指的就是“深度神经网络”,那么首先我们先了解一下“浅度神经网络”,也就是传统的神经网络。这里的内容主要来自http://neuralnetworksanddeeplearning.com的前两章。

    2.2.1 手写数字识别问题

       我们在学习一门新的语言时会写一个hello world程序,而mnist数据的手写数字识别就是一个很好的学习机器学习(包括深度学习)的一个hello world任务。

       计算机和人类大脑似乎有很大的不同,很多人类认为复杂的工作计算机可能认为很简单,而人类认为很简单的事情计算机可能非常难处理。比如数字的计算,记忆,人类的准确度和速度都远远不如计算机。但是识别0-9的手写数字,我们觉得很轻而易举的事情,让计算机程序来处理却异常困难。经过数百万年进化的人类视觉系统在我们大脑没有意识到的时候就已经帮我们完成了数字的识别,把那些复杂的视觉处理过程深深的掩藏了起来。但当我们想自己写一个程序来识别数字的时候,这些困难才能体现出来。首先,对于计算机来说,它“看到”的不是数字,甚至不是笔画。它“看到”的只是一个二位的矩阵(数组),每个点都是一个数字。比如下图,我们“看到”的是左边的“猫”,其实计算机“看到”的是右边的像素灰度值。当然我们视觉系统的视网膜看到的也是类似的一些“数值”,只不过我们的视觉系统已经处理了这些信息并且把它识别成了“猫”(甚至和语言还做了映射)。 




       MNIST数据介绍:MNIST的每个图片经过缩放和居中等预处理之后,大小是28*28,每个点都是0-255的灰度值,下图是一些样例。总共有60,000个训练数据(0-9共10个类别,每个类别6,000个)和10,000个测试数据。一般会拿60000个中的50000个来做训练集,而剩下的10000个用来做验证集(用来选择一些超参数)。




       如果我们自己来写一个“算法”识别数字“9”,我们可能会这么定义:9在上面有个圆圈,在这个圆圈的右下部分有一个竖直的笔画。说起来很简单,如果用算法 来实现就很麻烦了:什么是圆圈?每个人画的圆圈都不同,同样竖直的笔画怎么识别,圆圈和竖直笔画连接处怎么寻找,右下是哪?大家如果有兴趣可以尝试一下用 上面的方法,其实最早做数字识别就是这样的思路。

       机器学习的思路则不同,它不需要这么细节的“指示”计算机应该怎么做。而是给计算机足够的“训练”样本,让它“看”不同的10个数字,然后让它“学”出 来。前面我们也讲了,现在的机器学习一般是一个参数化的模型。比如最简单的一个线性模型:f(w;x)=w0+ w1*x1+w2*x2。如果我们的输入有两个“特征”x1和x2,那么这个模型有3个参数w0,w1和w2,机器学习的过程就是选择“最优”的参数。对 于上面的mnist数据,输入就是28*28=784维的向量。

       如果用“原始”的输入作为“特征”,线性的模型很可能学到一些简单的特征,比如它看到1一般是分布在从上到下居中的一些位置,那么对于这些位置一旦发现有比较大的灰度值,那么就倾向于判断成1。如果一个像素点2也经常出现,但3不出现,那么它就能学到如果这个像素出现,那么这个数字是2和3的可能性就大一些。

       但是这样的“特征”可能不是“本质”的,因为我写字的时候笔稍微平移一点,那么你之前“学到”的参数就可能有问题。而更“本质”的特征是什么呢?可能还是像之前我们总结的——9在上面有个圆圈,在这个圆圈的右下部分有一个竖直的笔画。我们把识别一个数字的问题转化成圆圈和竖直笔画的问题。传统的机器学习需要方法来提取“类似”(但不完全是)基本笔画这样的“特征”,这些特征相对于像素的特征会更加“本质”。但是要“提取”这些特征需要很多的“领域”知识,比如图像处理的技术。所以使用传统的机器学习方法来解决问题,我们不但需要很多机器学习的知识,而且也需要很多“领域”的知识,同时拥有这两方面的知识是比较难的。

       而“深度学习”最近之所以火热,其中很重要的一个原因就是对于很多问题,我们只需要输入最原始的信号,比如图片的像素值,通过“多层”的网络,让底层的网络学习出“底层”的特征,比如基本的形状,而中间的层学习出抽象一点的特征,比如眼睛鼻子耳朵。而更上的层次识别出这是一个猫还是一个狗。所有这些都是机器学习出来的,所以基本不需要领域的知识。




       上面的图就说明了这一点,而且我们发现越是底层的特征就越“通用”,不管是猫鼻子还是狗眼睛,可能用到的都是一些基本的形状,因此我们可以把这些知识(特征)transfer到别的任务,也就是transfer learning,后面我们讲到CNN的时候还会提及。

    2.2.2 单个神经元和多层神经网络(MLP)

       神经网络从名字来看是和人类的大脑有些关系的,而且即使到现在,很多有用的东西如CNN和Attention,都有很多借鉴神经科学研究人脑的结果的。不过这里我就不介绍这些东西了,有兴趣的读者可以找一些资料来了解。

    一个神经元如下图的结构:




       它的输入是一个向量,(x1,x2,x3),输出是一个标量,一个实数。z=w0+ w1*x1 + w2*x2 + w3*x3。z是输入的加权累加,权值是w1,w2,w3,w0是bias,输出 output = f(z)。函数f一般叫做激活函数。最早流行的激活函数是Sigmoid函数,当然现在更流行Relu和它的改进版本。Sigmoid函数的公式和图形如下:









       当z=0时,sigmoid(z)=0.5 z趋于无穷大时,sigmoid(z)趋近于1,z趋于负无穷,值趋于0。为什么选择这样的激活函数呢?因为是模拟人脑的神经元。人脑的神经元也是把输入的信号做加权累加,然后看累加和是否超过一个“阈值”。如果超过,继续向下一个神经元发送信号,否则就不发送。因此人脑的神经元更像是一个阶跃函数:




       最早的感知机(Perception)其实用的就是这个激活函数。但是它有一个缺点就是0之外的所有点的导数都是0,在0点的导数是无穷大,所以很难用梯度的方法优化。而Sigmoid函数是处处可导。下面我手工推导了一下,如果大家不熟悉可以试着推导一下Sigmoid函数的导数,我们后面也会用到。




    我们把许多的单个神经元按照层次组织起来就是多层的神经网络。




       比如我们的手写数字识别,输入层是784维,就是神经网络的地一层,然后中间有15个hidden(因为我们不知道它的值)神经元,然后输出层是10个神经元。中间隐层的每个神经元的输入都是784个原始像素通过上面的公式加权累加然后用sigmoid激活。而输出层的每一个神经元也是中间15个神经元的累加然后激活。上面的图就是一个3层的神经网络。

       输入一个28*28的图像,我们得到一个10维的输出,那么怎么分类呢?最直接的想法就是把认为最大的那个输出,比如输出是(10,11,12,13,14,15,16,17,18,19),那么我们认为输出是9。

       当然,更常见的做法是最后一次经过线性累加之后并不用Sigmoid函数激活,而是加一个softmax的函数,让10个输出加起来等于1,这样更像一个 概率。而我们上面的情况,虽然训练数据的输出加起来是1,但是实际给一个其它输入,输出加起来很可能不是1。不过为了与Nielsen的文章一致,我们还 是先用这种方法。

       因此,假设我们有了这些参数【总共是784*15 + 15(w0或者叫bias) + 15*10 + 10】,我们很容易通过上面的公式一个一个的计算出10维的输出。然后选择最大的那个作为我们识别的结果。问题的难点就在怎么 选择这么多参数,然后使得我们分类的错误最少。

       而我们怎么训练呢?对于一张图片,假设它是数字“1”,那么我们期望它的输出是(0,1,0,0,0,0,0,0,0,0),所以我们可以简单的用最小平方错误作为损失函数。不过你可能会有些疑问,我们关注的指标应该是分类的“正确率”(或者错误率),那么我们为什么不直接把分类的错误率作为损失函数呢?这样神经网络学习出来的参数就是最小化错误率。

       主要的原因就是错误率不是参数的连续函数。因为一个训练数据如果分类正确那么就是1,否则就是0,这样就不是一个连续的函数。比如最简单的两类线性分类器,f(x)=w0+w1*x1+w2*x2。如果f(x)>0我们分类成类别1;否则我们分类成类别2。如果当前的w0+w1*x1+w2*x2<0,我们很小的调整w0(或者w1,w2),w0+w1*x1+w2*x2仍然小于0,【事实上对于这个例子,只要是w0变小,他们的累加都是小于0的】所以f(x)的值不会变化,而w0一直增大到使累加和等于0之前都不会变化,只有大于0时突然变成1了,然后一直就是1。因此之前的错误率都是1,然后就突然是0。所以它不是个连续的函数。

       因为我们使用的优化算法一般是(随机)梯度下降的算法,在每次迭代的时候都是试图做一个微小的参数调整使得损失变小,但是不连续的函数显然也不可导,也就没法用这个算法来优化参数。

       因此我们使用了最小平方误差(MSE)损失函数。




       y(x)就是神经网络的输出,可能写成f(x)大家会习惯一点。a是目标的输出,比如当前分类是数字1,那么我们期望的输出就是(0,1,0,0,0,0,0,0,0,0)。

       首先这个损失函数是参数w的连续函数,因为y(x)就是神经网络的输出,每个神经元都是它的输入的线性加权累加,然后使用sigmoid激活函数【如果使用最早的阶跃函数就不连续了,所以后来使用了Sigmoid函数】,然后每一层的神经元都是用上一层的神经元通过这样的方式计算的(只不过每个神经元的参数也就是权重是不同的数值而已),所以这些连续函数的复合函数也是连续的。

       其次这个损失函数和我们的最终优化目标是“大致”一致的。比如C(w,b)趋于0时,它就要求y(x)趋于a,那么我们的分类也就趋于正确。当然可能存在一种极端的情况,比如有3个训练数据,第一组参数,它分类正确了2个训练数据,但是错的那1个错的很“离谱”,也就是y(x)和a差距极大;而第二组参数,他正确分类了1个训练数据,但是错的那两个都还不算太差。那么这种情况下MSE和正确率并不一致。

    2.2.3 随机梯度下降(Stochastic Gradient Descent)和自动求梯度(Automatic Derivatives)

       上面说了,我们有了一个参数化的模型,训练的过程就是根据训练数据和loss function,选择“最优”的参数,使得loss“最小”,这从数学上来讲就是一个优化问题。这看起来似乎不是什么值得一提的问题,也许你还记得微积 分里的知识,极值点的各种充分必要条件,比如必要条件是导数是0,然后直接把参数解出来。但在现实生活中的函数远比教科书里学到的复杂,很多模型都无法用 解析的方式求出最优解。所以现实的方法就是求“数值”解,一般最常见的方法就是迭代的方法,根据现在的参数,我们很小幅度的调整参数,使得loss变小一 点点。然后一步一步的最终能够达到一个最优解(一般是局部最优解)。那怎么小幅调整呢?像闷头苍蝇那样随机乱试显然效率极低。因此我们要朝着一个能使函数 值变小的方向前进。而在一个点能使函数值变小的方向有无穷多个,但有一个方向是下降速度最快的,那就是梯度。因此更常见的方法就是在当前点求函数的梯度, 然后朝着梯度的方向下降。朝梯度的方向走多远呢?一般走一个比较小的值是比较安全的,这个值就是“步长”。一般刚开始随机的初始化参数,loss比较大, 所以多走一些也没关系,但是到了后面,就不能走太快,否则很容易错过最优的点。

       因为loss是所有训练数据的函数,所以求loss的梯度需要计算所有的训练数据,对于很多task来说,训练数据可能上百万,计算一次代价太大,所以一 般会“随机”的采样少部分数据,比如128个数据,求它的梯度。虽然128个点的梯度和一百万个的是不一样的,但是从概率来讲至少是一致的方向而不会是相 反的方向,所以也能使loss变小。当然这个128是可以调整的,它一般被叫做batch size,最极端的就是batch是1和一百万,那么分别就是online learning和退化到梯度下降。batch size越大,计算一次梯度的时间就越久【当然由于GPU和各种类似SSE的指令,一次计算128个可能并不比计算1个慢多少】,随机梯度和真正梯度一致 的概率就越大,走的方向就更“正确”;batch size越小,计算一次的时间就越短,但可能方向偏离最优的方向就更远,会在不是“冤枉路”。但实际的情况也很难说哪个值是最优的,一般的经验取值都是几 十到一两百的范围,另外因为计算机都是字节对齐,32,64,128这样的值也许能稍微加快矩阵运算的速度。但是实际也很多人选择10,50,100这样 的值。

       除了常见的随机梯度下降,还有不少改进的方法,如Momentum,Adagrad等等,有兴趣的可以看看 http://cs231n.github.io/neural-networks-3/#update ,里面还有个动画,比较了不同方法的收敛速度的比较。

    通过上面的分析,我们把问题变成了怎么求loss对参数W的梯度。

    求梯度有如下4种方法:

    1.手工求解析解

    比如 f(x)=x^2, df/dx=2*x。然后我们要求f(x)在x=1.5的值,代进去就2*1.5=3

    2.数值解

    使用极限的定义:




    3.机器符号计算

    让机器做符号运算,实现1的方法,但是机器如果优化的不好的话可能会有一些不必要的运算。

    比如 x^2 + 2*x*y + y^2,直接对x求导数变成了 2*x + 2*y,两次乘法一次加分,但是我们可以合并一下变成2*(x+y),一次乘法一次加分。

    4.自动梯度

    下面我会在稍微细讲一下,所以这里暂时跳过。

    这些方法的优缺点:
     
    手工求解“数学”要求高,有可能水平不够求不对,但效率应该是能最优的。没任何函数,甚至没有解析导数的情况下都能使用,缺点是计算量太大,而且只是近似解【因为极限的定义】,在某些特别不“连续”的地方可能误差较大。所以实际使用是很少,只是用它来验证其它方法是否正确。机器符号计算,前面说的,依赖于这个库的好坏。

    实际的框架,如TensorFlow就是自动梯度,而Theano就是符号梯度。

    2.2.4 编程实战

       通过上面的介绍,我们其实就可以实现一个经典的前馈(feed forward)神经网络了,这种网络结构很简单,每一层的输入是前一层的输出。输入层没有输入,它就是原始的信号输入。而且上一层的所有神经元都会连接到下一层的所有神经元,就像我们刚才的例子,输入是784,中间层是15,那么就有785*15个连接【再加上每个中间节点有一个bias】。所以这种网络有时候也加做全连接的网络(full connected),用来和CNN这种不是全连接的网络有所区别,另外就是信号是从前往后传递,没有反馈,所以也叫前溃神经网络,这是为了和RNN这种有反馈的区别。

       当然,我们还没有讲怎么计算梯度,也就是损失函数相对于每一个参数的偏导数。在下一部分我们会详细讨论介绍,这里我们先把它当成一个黑盒的函数就好了。

    1.代码
    我们这里学习一下Nielsen提供的代码。代码非常简洁,只有不到100行代码。
    https://github.com/mnielsen/ne ... rning

    git clone https://github.com/mnielsen/ne ... g.git

    2.运行

    创建一个 test_network1.py,输入如下代码:import mnist_loader
    import network

    training_data, validation_data, test_data = mnist_loader.load_data_wrapper()
    net = network.Network([784, 30, 10])
    net.SGD(training_data, 30, 10, 3.0, test_data=test_data)保存后直接运行 Python test_network1.py。这里我们让他进行了30次迭代,最终在测试数据上的准确率大概在95%左右(当然因为随机初始化参数不同,最终的结果可能有所不同)Epoch 0: 8250 / 10000
    Epoch 1: 8371 / 10000
    Epoch 2: 9300 / 10000
    ......
    Epoch 28: 9552 / 10000
    Epoch 29: 9555 / 100003. 代码阅读

       Python代码很容易阅读,即使之前没有用过,稍微学习两天也就可以上手,而且大部分机器学习相关的代码不会用到太复杂的语言特性,基本就是一些数学的线性代数的运算。而Python的numpy这个库是用的最多的,后面阅读代码的时候我会把用到的函数做一些介绍,继续下面的阅读之前建议花十分钟阅读一下 http://cs231n.github.io/python-numpy-tutorial/。

    3.1 mnist_loader.load_data_wrapper函数

       这个函数用来读取mnist数据,数据是放在data/mnist.pkl.gz。首先这是个gzip的压缩文件,是Pickle工具序列化到磁盘的格式。不熟悉也没有关系,反正我们知道这个函数的返回值就行了。

       这个函数返回三个对象,分别代表training_data,validation_data和test_data。

       training_data是一个50,000的list,然后其中的每一个元素是一个tuple。tuple的第一个元素是一个784维的numpy一维数组。第二个元素是10维的数组,也就是one-hot的表示方法——如果正确的答案是数字0,那么这个10维数组就是(1, 0, 0, …)。

       而validation_data是一个10,000的list,每个元素也是一个tuple。tuple的第一个元素也是784维的numpy一维数组。第二个元素是一个0-9的数字,代表正确答案是那个数字。

       test_data的格式和validation_data一样。

       为什么training_data要是这样的格式呢?因为这样的格式计算loss更方便一些。

    3.2 Network类的构造函数

       我们在调用net = network.Network([784, 30, 10])时就到了init函数。为了减少篇幅,代码里的注释我都去掉了,重要的地方我会根据自己的理解说明,但是有空还是值得阅读代码里的注释。class Network(object):
    def __init__(self, sizes):self.num_layers = len(sizes)
    self.sizes = sizes
    self.biases = [np.random.randn(y, 1) for y in sizes[1:]]
    self.weights = [np.random.randn(y, x)
    for x, y in zip(sizes[:-1], sizes[1:])]   比如上面的参数,我们保存下来的self.num_layers=3,也就是3层的网络。每一层的神经元的个数保存到self.sizes里。接下来就是构造biases数组并随机初始化。因为输入层是没有参数的,所以是for y in sizes[1:],我们使用了numpy的random.randn生成正态分布的随机数用来作为参数的初始值。注意这里生成了2维的随机变量。回忆一下,如果我们有30个hidden unit,那么bias的个数也是30,那就生成一个30维的1维数组就行了,为什么要是30*1的二维数组呢?其实用1维也可以,不过为了和weights一致,后面代码方便,就用二维数组了。另外weights也是一样的初始化方法,不过注意randn(y,x)而不是randn(x,y)。比如对于我们输入的[784,30,10],weights分别是30*784和10*30的。当然其实weights矩阵转置一下也可以,就是计算矩阵乘法的时候也需要有一个转置。不同的文献可能有不同的记法,但是我们在实现代码的时候只需要随时注意矩阵的大小,检查矩阵乘法满足乘法的约束就行了,矩阵AB能相乘,必须满足的条件是B的列数等于A的函数就行。

       对于Nielsen的记法,矩阵的每一行就是一个神经元的784个参数,那么weights(30*784) * input(784*1)就得到30个hidden unit的加权累加。

    3.3 feedforward函数

       给点输入a(784维),计算最终神经网络的输出(10维)。def feedforward(self, a):
    """Return the output of the network if ``a`` is input."""for b, w in zip(self.biases, self.weights):
    a = sigmoid(np.dot(w, a)+b)
    return a   代码非常简单,这里用到了np.dot,也就是矩阵向量的乘法,此外这里有一个Sigmoid函数,这个函数的输入是numpy的ndarray,输出也是同样大小的数组,不过对于每个元素都进行了sigmoid的计算。用numpy的术语就是universal function,很多文献里一般都叫elementwise的function。我觉得后面这个名字更直接。#### Miscellaneous functionsdef sigmoid(z):
    """The sigmoid function."""return 1.0/(1.0+np.exp(-z))

    def sigmoid_prime(z):
    """Derivative of the sigmoid function."""return sigmoid(z)*(1-sigmoid(z))上面就是Sigmoid函数,另外也把sigmoid_prime,也就是Sigmoid的导数放在了一起【不记得的话看前面Sigmoid的导数的推导】。

    3.4 SGD函数

    这个函数是训练的入口,比如我们之前的训练代码:net.SGD(training_data, 30, 10, 3.0, test_data=test_data)


    def SGD(self, training_data, epochs, mini_batch_size, eta,
    test_data=None):
    if test_data: n_test = len(test_data)
    n = len(training_data)
    for j in xrange(epochs):
    random.shuffle(training_data)
    mini_batches = [
    training_data[k:k+mini_batch_size]
    for k in xrange(0, n, mini_batch_size)]
    for mini_batch in mini_batches:
    self.update_mini_batch(mini_batch, eta)
    if test_data:
    print "Epoch {0}: {1} / {2}".format(
    j, self.evaluate(test_data), n_test)
    else:
    print "Epoch {0} complete".format(j)第一个参数就是training_data。

    第二个参数就是epochs,也就是总共对训练数据迭代多少次,我们这里是30次迭代。

    第三个参数是batch大小,我们这里是10,最后一个参数是eta,也就是步长,这里是3.0。除了网络结构(比如总共多少个hidden layer,每个hidder layer多少个hidden unit),另外一个非常重要的参数就是步长。前面我们也讨论过了,步长太小,收敛速度过慢,步长太大,可能不收敛。实际的情况是没有一个万能的准则,更多的是根据数据,不停的尝试合适的步长。如果发现收敛太慢,就适当调大,反之则调小。所以要训练好一个神经网络,还是有很多tricky的技巧,包括参数怎么初始化,激活函数怎么选择,比SGD更好的优化算法等等。

    第四个参数test_data是可选的,如果有(我们的例子是穿了进来的),则每次epoch之后都测试一下。

    代码的大致解释我用注释的形式嵌在代码里了: for j in xrange(epochs): ## 一共进行 epochs=30 轮迭代
    random.shuffle(training_data) ## 训练数据随机打散
    mini_batches = [
    training_data[k:k+mini_batch_size]
    for k in xrange(0, n, mini_batch_size)] ## 把50,000个训练数据分成5,000个batch,每个batch包含10个训练数据。
    for mini_batch in mini_batches: ## 对于每个batch
    self.update_mini_batch(mini_batch, eta) ## 使用梯度下降更新参数
    if test_data: ## 如果提供了测试数据
    print "Epoch {0}: {1} / {2}".format(
    j, self.evaluate(test_data), n_test) ## 评价在测试数据上的准确率
    else:
    print "Epoch {0} complete".format(j)下面是evaluate函数:def evaluate(self, test_data):
    test_results = [(np.argmax(self.feedforward(x)), y)
    for (x, y) in test_data]
    return sum(int(x == y) for (x, y) in test_results)   对于test_data里的每一组(x,y),y是0-9之间的正确答案。而self.feedforward(x)返回的是10维的数组,我们选择得分最高的那个值作为模型的预测结果np.argmax就是返回最大值的下标。比如x=[0.3, 0.6, 0.1, 0, ….],那么argmax(x) = 1。

       因此test_results这个列表的每一个元素是一个tuple,tuple的第一个是模型预测的数字,而第二个是正确答案。

    所以最后一行返回的是模型预测正确的个数。
     
    3.5 update_mini_batch函数def update_mini_batch(self, mini_batch, eta):
    nabla_b = [np.zeros(b.shape) for b in self.biases]
    nabla_w = [np.zeros(w.shape) for w in self.weights]
    for x, y in mini_batch:
    delta_nabla_b, delta_nabla_w = self.backprop(x, y)
    nabla_b = [nb+dnb for nb, dnb in zip(nabla_b, delta_nabla_b)]
    nabla_w = [nw+dnw for nw, dnw in zip(nabla_w, delta_nabla_w)]
    self.weights = [w-(eta/len(mini_batch))*nw
    for w, nw in zip(self.weights, nabla_w)]
    self.biases = [b-(eta/len(mini_batch))*nb
    for b, nb in zip(self.biases, nabla_b)]它的输入参数是mini_batch【size=10的tuple(x,y)】和eta【3.0】。def update_mini_batch(self, mini_batch, eta):
    nabla_b = [np.zeros(b.shape) for b in self.biases]
    ## 回忆一下__init__,biases是一个列表,包含两个矩阵,分别是30*1和10*1
    ## 我们先构造一个和self.biases一样大小的列表,用来存放累加的梯度(偏导数)
    nabla_w = [np.zeros(w.shape) for w in self.weights]
    ## 同上, weights包含两个矩阵,大小分别是30*784和10*30
    for x, y in mini_batch:
    delta_nabla_b, delta_nabla_w = self.backprop(x, y)
    ## 对于一个训练数据(x,y)计算loss相对于所有参数的偏导数
    ## 因此delta_nabla_b和self.biases, nabla_b是一样大小(shape)
    ## 同样delta_nabla_w和self.weights,nabla_w一样大小
    nabla_b = [nb+dnb for nb, dnb in zip(nabla_b, delta_nabla_b)]
    ## 把bias的梯度累加到nabla_b里
    nabla_w = [nw+dnw for nw, dnw in zip(nabla_w, delta_nabla_w)]
    ## 把weight的梯度累加到nable_w里
    self.weights = [w-(eta/len(mini_batch))*nw
    for w, nw in zip(self.weights, nabla_w)]
    ## 使用这个batch的梯度和eta(步长)更新参数weights
    self.biases = [b-(eta/len(mini_batch))*nb
    for b, nb in zip(self.biases, nabla_b)]
    ## 更新biases
    ## 这里更新参数是除了batch的大小(10),有的人实现时不除,其实没有什么区别,因为超参数eta会有所不同,如果不除,那么eta相当于是0.3(在eta那里就除了batch的大小了)。3.6 backprop函数

    这个函数就是求loss相对于所有参数的偏导数,这里先不仔细讲解,等下次我们学习梯度的求解方法我们再回来讨论,这里可以先了解一下这个函数的输入和输出,把它当成一个黑盒就行,其实它的代码也很少,但是如果不知道梯度的公式,也很难明白。def backprop(self, x, y):
    nabla_b = [np.zeros(b.shape) for b in self.biases]
    nabla_w = [np.zeros(w.shape) for w in self.weights]
    # feedforwardactivation = x
    activations = [x] # list to store all the activations, layer by layerzs = [] # list to store all the z vectors, layer by layerfor b, w in zip(self.biases, self.weights):
    z = np.dot(w, activation)+b
    zs.append(z)
    activation = sigmoid(z)
    activations.append(activation)
    # backward passdelta = self.cost_derivative(activations[-1], y) * \
    sigmoid_prime(zs[-1])
    nabla_b[-1] = delta
    nabla_w[-1] = np.dot(delta, activations[-2].transpose())
    for l in xrange(2, self.num_layers):
    z = zs[-l]
    sp = sigmoid_prime(z)
    delta = np.dot(self.weights[-l+1].transpose(), delta) * sp
    nabla_b[-l] = delta
    nabla_w[-l] = np.dot(delta, activations[-l-1].transpose())
    return (nabla_b, nabla_w)   它的输入就是一个训练样本(x,y)分别是784*1和10*1。输出就是和self.biases,self.weights一样大小的列表,然后列表中的每一个数组的大小也是一样。具体到上面的例子,输出nabla_b包含两个矩阵,大小分别是30*1和10*1;nabla_w也包含两个矩阵,大小分别是30*784和10*30。
     
    未完待续
    作者简介:李理,目前就职于环信,即时通讯云平台和全媒体智能客服平台,在环信从事智能客服和智能机器人相关工作,致力于用深度学习来提高智能机器人的性能。
    责编:周建丁(zhoujd@csdn.net) 查看全部
    书接上文:环信李理:从Image Caption Generation了解深度学习(part I
     
    2. 机器学习基本概念和前馈神经网络
    2.1 机器学习基本概念

       大家可能平时都写过很多程序,写程序和机器学习的思路可能有一些不同。写程序时,我们是“上帝”,我们规定计算机的每一个步骤,第一步做什么第二步做什么,我们称之为算法。我们能够控制所有的情况,如果出了任何问题,肯定都是程序员的责任。而在机器学习的时候,我们只是“老师”。我们告诉学生(计算机)输入是什么,输出是什么,然后期望它能够学到和我们类似的知识。比如我们跟小孩说这是狗,那是猫,我们没有办法像上帝那样拿着“纳米手术刀”去操作人脑神 经元的连接方式。我们只能不断的给小孩“训练数据”,然后期望他能够学会什么是猫,即使我们觉得他“学会”了识别猫,我们也没有办法知道他是“怎么”学会 的,而且同样的训练过程可能换一个人就不好使。

       机器学习和人类的学习是类似的——我们也是给它训练数据,然后期望它能学会。我们会给机器建一个模型,从数学的角度来说一个模型就是一个函数,它的输入一般是一个向量【当然可以是二维的矩阵如图片或者三维的张量比如视频】,输出可以是有限的离散的标签如“猫”,“狗”,这类问题我们称之为分类;而如果输出 是连续的值比如用这个模型来预测气温,那么我们就称之为回归。其实人类的很多科学活动和日常生活,都是在“学习”模型和“应用”模型。比如开普勒通过观测 大量天文数据“归纳”出行星的运动规律。从本质上讲,智能就是从“过去”学习,然后根据“现在”来预测可能的将来并根据自己的目标选择有利于自己行为。只不过之前,似乎只有人类能够从数据中“学习”出规律,而人工智能的目标就是让机器也有类似的学习能力。

       模型用数学来说就是一个函数,我们人脑的函数由神经元的连接构成,它可能是一个很复杂的函数,我们现在还很难彻底研究清楚。神经网络就是试图通过计算机来 模拟和借鉴人脑这个模型,除了我们这里要讲的神经网络之外,机器学习领域还有各种各样的模型,它们各有特点。但不管形式怎么变化,本质都是一个函数。一个(或者更准确的是一种)模型一般都是一种函数形式,它有一些“参数”可以改变。而学习的过程就是不断调整这些参数,使得输出(尽量)接近“正确”的答案。 但是一般情况下很难所有的数据我们都能预测正确,所以一般我们会定义一个loss function,可以理解为“错误”的程度,错的越“离谱”,loss就越大。而我们的目标就是调整参数使得loss最小。

       但是我们是在“训练”数据上调整的参数,那么它能在“测试”数据上也表现的好吗?这个就是模型的“泛化”能力了。就和人在学校学习一样,有的同学做过的一 模一样的题就会,但是考试时稍微改变一下就不会了,这就是“泛化”能力太差,学到的不是最本质的东西。所以平时会定期有一些“模拟考试”,来检验学生是不 是真的学会了,如果考得不好,那就打回去重新训练模型调整参数。这在机器学习里对应的就是validation的阶段。最后到最终的考试了,就是最终检验 的时候了,这个试卷里的题目是不能提前让人看到的,只能拿出来用一次,否则就是作弊了。对应到机器学习里就是test阶段。

       当然这里用通俗的话描述了机器学习,主要是有监督的学习。其实机器学习还有无监督的学习和强化学习。前者就是不给答案,只给数据,让人总结规律;而后者会有答案,但是答案不是现在就告诉你。我个人觉得人类社会里更多的是监督学习和强化学习。从人类社会总体来说,强化学习是获取新知识的唯一途径,也就是向自 然学习,我们做了一个决策,其好坏可能要很长一段时间才能显现出来。而学习出来的这些知识通过监督的方式,通过家庭和学校的教育教给下一代。

       另外输出除了简单的分为离散和连续,还可以是序列(时序)的,比如自然语言(文本)是一个字符串的序列 ,对于我们的Image Caption Generation就是生成一个单词序列。另外还有更复杂的输出,比如parsing,输出是一棵语法树。

    2.2 多层神经网络

       前面介绍了机器学习的基本概念,接下来我们就来学习一下神经网络。现在流行的说法“深度学习”,其实大多指的就是“深度神经网络”,那么首先我们先了解一下“浅度神经网络”,也就是传统的神经网络。这里的内容主要来自http://neuralnetworksanddeeplearning.com的前两章。

    2.2.1 手写数字识别问题

       我们在学习一门新的语言时会写一个hello world程序,而mnist数据的手写数字识别就是一个很好的学习机器学习(包括深度学习)的一个hello world任务。

       计算机和人类大脑似乎有很大的不同,很多人类认为复杂的工作计算机可能认为很简单,而人类认为很简单的事情计算机可能非常难处理。比如数字的计算,记忆,人类的准确度和速度都远远不如计算机。但是识别0-9的手写数字,我们觉得很轻而易举的事情,让计算机程序来处理却异常困难。经过数百万年进化的人类视觉系统在我们大脑没有意识到的时候就已经帮我们完成了数字的识别,把那些复杂的视觉处理过程深深的掩藏了起来。但当我们想自己写一个程序来识别数字的时候,这些困难才能体现出来。首先,对于计算机来说,它“看到”的不是数字,甚至不是笔画。它“看到”的只是一个二位的矩阵(数组),每个点都是一个数字。比如下图,我们“看到”的是左边的“猫”,其实计算机“看到”的是右边的像素灰度值。当然我们视觉系统的视网膜看到的也是类似的一些“数值”,只不过我们的视觉系统已经处理了这些信息并且把它识别成了“猫”(甚至和语言还做了映射)。 
    001.png

       MNIST数据介绍:MNIST的每个图片经过缩放和居中等预处理之后,大小是28*28,每个点都是0-255的灰度值,下图是一些样例。总共有60,000个训练数据(0-9共10个类别,每个类别6,000个)和10,000个测试数据。一般会拿60000个中的50000个来做训练集,而剩下的10000个用来做验证集(用来选择一些超参数)。
    002.png

       如果我们自己来写一个“算法”识别数字“9”,我们可能会这么定义:9在上面有个圆圈,在这个圆圈的右下部分有一个竖直的笔画。说起来很简单,如果用算法 来实现就很麻烦了:什么是圆圈?每个人画的圆圈都不同,同样竖直的笔画怎么识别,圆圈和竖直笔画连接处怎么寻找,右下是哪?大家如果有兴趣可以尝试一下用 上面的方法,其实最早做数字识别就是这样的思路。

       机器学习的思路则不同,它不需要这么细节的“指示”计算机应该怎么做。而是给计算机足够的“训练”样本,让它“看”不同的10个数字,然后让它“学”出 来。前面我们也讲了,现在的机器学习一般是一个参数化的模型。比如最简单的一个线性模型:f(w;x)=w0+ w1*x1+w2*x2。如果我们的输入有两个“特征”x1和x2,那么这个模型有3个参数w0,w1和w2,机器学习的过程就是选择“最优”的参数。对 于上面的mnist数据,输入就是28*28=784维的向量。

       如果用“原始”的输入作为“特征”,线性的模型很可能学到一些简单的特征,比如它看到1一般是分布在从上到下居中的一些位置,那么对于这些位置一旦发现有比较大的灰度值,那么就倾向于判断成1。如果一个像素点2也经常出现,但3不出现,那么它就能学到如果这个像素出现,那么这个数字是2和3的可能性就大一些。

       但是这样的“特征”可能不是“本质”的,因为我写字的时候笔稍微平移一点,那么你之前“学到”的参数就可能有问题。而更“本质”的特征是什么呢?可能还是像之前我们总结的——9在上面有个圆圈,在这个圆圈的右下部分有一个竖直的笔画。我们把识别一个数字的问题转化成圆圈和竖直笔画的问题。传统的机器学习需要方法来提取“类似”(但不完全是)基本笔画这样的“特征”,这些特征相对于像素的特征会更加“本质”。但是要“提取”这些特征需要很多的“领域”知识,比如图像处理的技术。所以使用传统的机器学习方法来解决问题,我们不但需要很多机器学习的知识,而且也需要很多“领域”的知识,同时拥有这两方面的知识是比较难的。

       而“深度学习”最近之所以火热,其中很重要的一个原因就是对于很多问题,我们只需要输入最原始的信号,比如图片的像素值,通过“多层”的网络,让底层的网络学习出“底层”的特征,比如基本的形状,而中间的层学习出抽象一点的特征,比如眼睛鼻子耳朵。而更上的层次识别出这是一个猫还是一个狗。所有这些都是机器学习出来的,所以基本不需要领域的知识。
    003.png

       上面的图就说明了这一点,而且我们发现越是底层的特征就越“通用”,不管是猫鼻子还是狗眼睛,可能用到的都是一些基本的形状,因此我们可以把这些知识(特征)transfer到别的任务,也就是transfer learning,后面我们讲到CNN的时候还会提及。

    2.2.2 单个神经元和多层神经网络(MLP)

       神经网络从名字来看是和人类的大脑有些关系的,而且即使到现在,很多有用的东西如CNN和Attention,都有很多借鉴神经科学研究人脑的结果的。不过这里我就不介绍这些东西了,有兴趣的读者可以找一些资料来了解。

    一个神经元如下图的结构:
    004.png

       它的输入是一个向量,(x1,x2,x3),输出是一个标量,一个实数。z=w0+ w1*x1 + w2*x2 + w3*x3。z是输入的加权累加,权值是w1,w2,w3,w0是bias,输出 output = f(z)。函数f一般叫做激活函数。最早流行的激活函数是Sigmoid函数,当然现在更流行Relu和它的改进版本。Sigmoid函数的公式和图形如下:
    005.png


    006.png

       当z=0时,sigmoid(z)=0.5 z趋于无穷大时,sigmoid(z)趋近于1,z趋于负无穷,值趋于0。为什么选择这样的激活函数呢?因为是模拟人脑的神经元。人脑的神经元也是把输入的信号做加权累加,然后看累加和是否超过一个“阈值”。如果超过,继续向下一个神经元发送信号,否则就不发送。因此人脑的神经元更像是一个阶跃函数:
    007.png

       最早的感知机(Perception)其实用的就是这个激活函数。但是它有一个缺点就是0之外的所有点的导数都是0,在0点的导数是无穷大,所以很难用梯度的方法优化。而Sigmoid函数是处处可导。下面我手工推导了一下,如果大家不熟悉可以试着推导一下Sigmoid函数的导数,我们后面也会用到。
    008.jpg

    我们把许多的单个神经元按照层次组织起来就是多层的神经网络。
    009.png

       比如我们的手写数字识别,输入层是784维,就是神经网络的地一层,然后中间有15个hidden(因为我们不知道它的值)神经元,然后输出层是10个神经元。中间隐层的每个神经元的输入都是784个原始像素通过上面的公式加权累加然后用sigmoid激活。而输出层的每一个神经元也是中间15个神经元的累加然后激活。上面的图就是一个3层的神经网络。

       输入一个28*28的图像,我们得到一个10维的输出,那么怎么分类呢?最直接的想法就是把认为最大的那个输出,比如输出是(10,11,12,13,14,15,16,17,18,19),那么我们认为输出是9。

       当然,更常见的做法是最后一次经过线性累加之后并不用Sigmoid函数激活,而是加一个softmax的函数,让10个输出加起来等于1,这样更像一个 概率。而我们上面的情况,虽然训练数据的输出加起来是1,但是实际给一个其它输入,输出加起来很可能不是1。不过为了与Nielsen的文章一致,我们还 是先用这种方法。

       因此,假设我们有了这些参数【总共是784*15 + 15(w0或者叫bias) + 15*10 + 10】,我们很容易通过上面的公式一个一个的计算出10维的输出。然后选择最大的那个作为我们识别的结果。问题的难点就在怎么 选择这么多参数,然后使得我们分类的错误最少。

       而我们怎么训练呢?对于一张图片,假设它是数字“1”,那么我们期望它的输出是(0,1,0,0,0,0,0,0,0,0),所以我们可以简单的用最小平方错误作为损失函数。不过你可能会有些疑问,我们关注的指标应该是分类的“正确率”(或者错误率),那么我们为什么不直接把分类的错误率作为损失函数呢?这样神经网络学习出来的参数就是最小化错误率。

       主要的原因就是错误率不是参数的连续函数。因为一个训练数据如果分类正确那么就是1,否则就是0,这样就不是一个连续的函数。比如最简单的两类线性分类器,f(x)=w0+w1*x1+w2*x2。如果f(x)>0我们分类成类别1;否则我们分类成类别2。如果当前的w0+w1*x1+w2*x2<0,我们很小的调整w0(或者w1,w2),w0+w1*x1+w2*x2仍然小于0,【事实上对于这个例子,只要是w0变小,他们的累加都是小于0的】所以f(x)的值不会变化,而w0一直增大到使累加和等于0之前都不会变化,只有大于0时突然变成1了,然后一直就是1。因此之前的错误率都是1,然后就突然是0。所以它不是个连续的函数。

       因为我们使用的优化算法一般是(随机)梯度下降的算法,在每次迭代的时候都是试图做一个微小的参数调整使得损失变小,但是不连续的函数显然也不可导,也就没法用这个算法来优化参数。

       因此我们使用了最小平方误差(MSE)损失函数。
    010.png

       y(x)就是神经网络的输出,可能写成f(x)大家会习惯一点。a是目标的输出,比如当前分类是数字1,那么我们期望的输出就是(0,1,0,0,0,0,0,0,0,0)。

       首先这个损失函数是参数w的连续函数,因为y(x)就是神经网络的输出,每个神经元都是它的输入的线性加权累加,然后使用sigmoid激活函数【如果使用最早的阶跃函数就不连续了,所以后来使用了Sigmoid函数】,然后每一层的神经元都是用上一层的神经元通过这样的方式计算的(只不过每个神经元的参数也就是权重是不同的数值而已),所以这些连续函数的复合函数也是连续的。

       其次这个损失函数和我们的最终优化目标是“大致”一致的。比如C(w,b)趋于0时,它就要求y(x)趋于a,那么我们的分类也就趋于正确。当然可能存在一种极端的情况,比如有3个训练数据,第一组参数,它分类正确了2个训练数据,但是错的那1个错的很“离谱”,也就是y(x)和a差距极大;而第二组参数,他正确分类了1个训练数据,但是错的那两个都还不算太差。那么这种情况下MSE和正确率并不一致。

    2.2.3 随机梯度下降(Stochastic Gradient Descent)和自动求梯度(Automatic Derivatives)

       上面说了,我们有了一个参数化的模型,训练的过程就是根据训练数据和loss function,选择“最优”的参数,使得loss“最小”,这从数学上来讲就是一个优化问题。这看起来似乎不是什么值得一提的问题,也许你还记得微积 分里的知识,极值点的各种充分必要条件,比如必要条件是导数是0,然后直接把参数解出来。但在现实生活中的函数远比教科书里学到的复杂,很多模型都无法用 解析的方式求出最优解。所以现实的方法就是求“数值”解,一般最常见的方法就是迭代的方法,根据现在的参数,我们很小幅度的调整参数,使得loss变小一 点点。然后一步一步的最终能够达到一个最优解(一般是局部最优解)。那怎么小幅调整呢?像闷头苍蝇那样随机乱试显然效率极低。因此我们要朝着一个能使函数 值变小的方向前进。而在一个点能使函数值变小的方向有无穷多个,但有一个方向是下降速度最快的,那就是梯度。因此更常见的方法就是在当前点求函数的梯度, 然后朝着梯度的方向下降。朝梯度的方向走多远呢?一般走一个比较小的值是比较安全的,这个值就是“步长”。一般刚开始随机的初始化参数,loss比较大, 所以多走一些也没关系,但是到了后面,就不能走太快,否则很容易错过最优的点。

       因为loss是所有训练数据的函数,所以求loss的梯度需要计算所有的训练数据,对于很多task来说,训练数据可能上百万,计算一次代价太大,所以一 般会“随机”的采样少部分数据,比如128个数据,求它的梯度。虽然128个点的梯度和一百万个的是不一样的,但是从概率来讲至少是一致的方向而不会是相 反的方向,所以也能使loss变小。当然这个128是可以调整的,它一般被叫做batch size,最极端的就是batch是1和一百万,那么分别就是online learning和退化到梯度下降。batch size越大,计算一次梯度的时间就越久【当然由于GPU和各种类似SSE的指令,一次计算128个可能并不比计算1个慢多少】,随机梯度和真正梯度一致 的概率就越大,走的方向就更“正确”;batch size越小,计算一次的时间就越短,但可能方向偏离最优的方向就更远,会在不是“冤枉路”。但实际的情况也很难说哪个值是最优的,一般的经验取值都是几 十到一两百的范围,另外因为计算机都是字节对齐,32,64,128这样的值也许能稍微加快矩阵运算的速度。但是实际也很多人选择10,50,100这样 的值。

       除了常见的随机梯度下降,还有不少改进的方法,如Momentum,Adagrad等等,有兴趣的可以看看 http://cs231n.github.io/neural-networks-3/#update ,里面还有个动画,比较了不同方法的收敛速度的比较。

    通过上面的分析,我们把问题变成了怎么求loss对参数W的梯度。

    求梯度有如下4种方法:

    1.手工求解析解

    比如 f(x)=x^2, df/dx=2*x。然后我们要求f(x)在x=1.5的值,代进去就2*1.5=3

    2.数值解

    使用极限的定义:
    011.png

    3.机器符号计算

    让机器做符号运算,实现1的方法,但是机器如果优化的不好的话可能会有一些不必要的运算。

    比如 x^2 + 2*x*y + y^2,直接对x求导数变成了 2*x + 2*y,两次乘法一次加分,但是我们可以合并一下变成2*(x+y),一次乘法一次加分。

    4.自动梯度

    下面我会在稍微细讲一下,所以这里暂时跳过。

    这些方法的优缺点:
     
    • 手工求解“数学”要求高,有可能水平不够求不对,但效率应该是能最优的。
    • 没任何函数,甚至没有解析导数的情况下都能使用,缺点是计算量太大,而且只是近似解【因为极限的定义】,在某些特别不“连续”的地方可能误差较大。所以实际使用是很少,只是用它来验证其它方法是否正确。
    • 机器符号计算,前面说的,依赖于这个库的好坏。


    实际的框架,如TensorFlow就是自动梯度,而Theano就是符号梯度。

    2.2.4 编程实战

       通过上面的介绍,我们其实就可以实现一个经典的前馈(feed forward)神经网络了,这种网络结构很简单,每一层的输入是前一层的输出。输入层没有输入,它就是原始的信号输入。而且上一层的所有神经元都会连接到下一层的所有神经元,就像我们刚才的例子,输入是784,中间层是15,那么就有785*15个连接【再加上每个中间节点有一个bias】。所以这种网络有时候也加做全连接的网络(full connected),用来和CNN这种不是全连接的网络有所区别,另外就是信号是从前往后传递,没有反馈,所以也叫前溃神经网络,这是为了和RNN这种有反馈的区别。

       当然,我们还没有讲怎么计算梯度,也就是损失函数相对于每一个参数的偏导数。在下一部分我们会详细讨论介绍,这里我们先把它当成一个黑盒的函数就好了。

    1.代码
    我们这里学习一下Nielsen提供的代码。代码非常简洁,只有不到100行代码。
    https://github.com/mnielsen/ne ... rning

    git clone https://github.com/mnielsen/ne ... g.git

    2.运行

    创建一个 test_network1.py,输入如下代码:
    import mnist_loader
    import network

    training_data, validation_data, test_data = mnist_loader.load_data_wrapper()
    net = network.Network([784, 30, 10])
    net.SGD(training_data, 30, 10, 3.0, test_data=test_data)
    保存后直接运行 Python test_network1.py。这里我们让他进行了30次迭代,最终在测试数据上的准确率大概在95%左右(当然因为随机初始化参数不同,最终的结果可能有所不同)
    Epoch 0: 8250 / 10000
    Epoch 1: 8371 / 10000
    Epoch 2: 9300 / 10000
    ......
    Epoch 28: 9552 / 10000
    Epoch 29: 9555 / 10000
    3. 代码阅读

       Python代码很容易阅读,即使之前没有用过,稍微学习两天也就可以上手,而且大部分机器学习相关的代码不会用到太复杂的语言特性,基本就是一些数学的线性代数的运算。而Python的numpy这个库是用的最多的,后面阅读代码的时候我会把用到的函数做一些介绍,继续下面的阅读之前建议花十分钟阅读一下 http://cs231n.github.io/python-numpy-tutorial/

    3.1 mnist_loader.load_data_wrapper函数

       这个函数用来读取mnist数据,数据是放在data/mnist.pkl.gz。首先这是个gzip的压缩文件,是Pickle工具序列化到磁盘的格式。不熟悉也没有关系,反正我们知道这个函数的返回值就行了。

       这个函数返回三个对象,分别代表training_data,validation_data和test_data。

       training_data是一个50,000的list,然后其中的每一个元素是一个tuple。tuple的第一个元素是一个784维的numpy一维数组。第二个元素是10维的数组,也就是one-hot的表示方法——如果正确的答案是数字0,那么这个10维数组就是(1, 0, 0, …)。

       而validation_data是一个10,000的list,每个元素也是一个tuple。tuple的第一个元素也是784维的numpy一维数组。第二个元素是一个0-9的数字,代表正确答案是那个数字。

       test_data的格式和validation_data一样。

       为什么training_data要是这样的格式呢?因为这样的格式计算loss更方便一些。

    3.2 Network类的构造函数

       我们在调用net = network.Network([784, 30, 10])时就到了init函数。为了减少篇幅,代码里的注释我都去掉了,重要的地方我会根据自己的理解说明,但是有空还是值得阅读代码里的注释。
    class Network(object):
    def __init__(self, sizes):self.num_layers = len(sizes)
    self.sizes = sizes
    self.biases = [np.random.randn(y, 1) for y in sizes[1:]]
    self.weights = [np.random.randn(y, x)
    for x, y in zip(sizes[:-1], sizes[1:])]
       比如上面的参数,我们保存下来的self.num_layers=3,也就是3层的网络。每一层的神经元的个数保存到self.sizes里。接下来就是构造biases数组并随机初始化。因为输入层是没有参数的,所以是for y in sizes[1:],我们使用了numpy的random.randn生成正态分布的随机数用来作为参数的初始值。注意这里生成了2维的随机变量。回忆一下,如果我们有30个hidden unit,那么bias的个数也是30,那就生成一个30维的1维数组就行了,为什么要是30*1的二维数组呢?其实用1维也可以,不过为了和weights一致,后面代码方便,就用二维数组了。另外weights也是一样的初始化方法,不过注意randn(y,x)而不是randn(x,y)。比如对于我们输入的[784,30,10],weights分别是30*784和10*30的。当然其实weights矩阵转置一下也可以,就是计算矩阵乘法的时候也需要有一个转置。不同的文献可能有不同的记法,但是我们在实现代码的时候只需要随时注意矩阵的大小,检查矩阵乘法满足乘法的约束就行了,矩阵AB能相乘,必须满足的条件是B的列数等于A的函数就行。

       对于Nielsen的记法,矩阵的每一行就是一个神经元的784个参数,那么weights(30*784) * input(784*1)就得到30个hidden unit的加权累加。

    3.3 feedforward函数

       给点输入a(784维),计算最终神经网络的输出(10维)。
    def feedforward(self, a):
    """Return the output of the network if ``a`` is input."""for b, w in zip(self.biases, self.weights):
    a = sigmoid(np.dot(w, a)+b)
    return a
       代码非常简单,这里用到了np.dot,也就是矩阵向量的乘法,此外这里有一个Sigmoid函数,这个函数的输入是numpy的ndarray,输出也是同样大小的数组,不过对于每个元素都进行了sigmoid的计算。用numpy的术语就是universal function,很多文献里一般都叫elementwise的function。我觉得后面这个名字更直接。
    #### Miscellaneous functionsdef sigmoid(z):
    """The sigmoid function."""return 1.0/(1.0+np.exp(-z))

    def sigmoid_prime(z):
    """Derivative of the sigmoid function."""return sigmoid(z)*(1-sigmoid(z))
    上面就是Sigmoid函数,另外也把sigmoid_prime,也就是Sigmoid的导数放在了一起【不记得的话看前面Sigmoid的导数的推导】。

    3.4 SGD函数

    这个函数是训练的入口,比如我们之前的训练代码:
    net.SGD(training_data, 30, 10, 3.0, test_data=test_data)


    def SGD(self, training_data, epochs, mini_batch_size, eta,
    test_data=None):
    if test_data: n_test = len(test_data)
    n = len(training_data)
    for j in xrange(epochs):
    random.shuffle(training_data)
    mini_batches = [
    training_data[k:k+mini_batch_size]
    for k in xrange(0, n, mini_batch_size)]
    for mini_batch in mini_batches:
    self.update_mini_batch(mini_batch, eta)
    if test_data:
    print "Epoch {0}: {1} / {2}".format(
    j, self.evaluate(test_data), n_test)
    else:
    print "Epoch {0} complete".format(j)
    第一个参数就是training_data。

    第二个参数就是epochs,也就是总共对训练数据迭代多少次,我们这里是30次迭代。

    第三个参数是batch大小,我们这里是10,最后一个参数是eta,也就是步长,这里是3.0。除了网络结构(比如总共多少个hidden layer,每个hidder layer多少个hidden unit),另外一个非常重要的参数就是步长。前面我们也讨论过了,步长太小,收敛速度过慢,步长太大,可能不收敛。实际的情况是没有一个万能的准则,更多的是根据数据,不停的尝试合适的步长。如果发现收敛太慢,就适当调大,反之则调小。所以要训练好一个神经网络,还是有很多tricky的技巧,包括参数怎么初始化,激活函数怎么选择,比SGD更好的优化算法等等。

    第四个参数test_data是可选的,如果有(我们的例子是穿了进来的),则每次epoch之后都测试一下。

    代码的大致解释我用注释的形式嵌在代码里了:
      for j in xrange(epochs): ## 一共进行 epochs=30 轮迭代
    random.shuffle(training_data) ## 训练数据随机打散
    mini_batches = [
    training_data[k:k+mini_batch_size]
    for k in xrange(0, n, mini_batch_size)] ## 把50,000个训练数据分成5,000个batch,每个batch包含10个训练数据。
    for mini_batch in mini_batches: ## 对于每个batch
    self.update_mini_batch(mini_batch, eta) ## 使用梯度下降更新参数
    if test_data: ## 如果提供了测试数据
    print "Epoch {0}: {1} / {2}".format(
    j, self.evaluate(test_data), n_test) ## 评价在测试数据上的准确率
    else:
    print "Epoch {0} complete".format(j)
    下面是evaluate函数:
    def evaluate(self, test_data):
    test_results = [(np.argmax(self.feedforward(x)), y)
    for (x, y) in test_data]
    return sum(int(x == y) for (x, y) in test_results)
       对于test_data里的每一组(x,y),y是0-9之间的正确答案。而self.feedforward(x)返回的是10维的数组,我们选择得分最高的那个值作为模型的预测结果np.argmax就是返回最大值的下标。比如x=[0.3, 0.6, 0.1, 0, ….],那么argmax(x) = 1。

       因此test_results这个列表的每一个元素是一个tuple,tuple的第一个是模型预测的数字,而第二个是正确答案。

    所以最后一行返回的是模型预测正确的个数。
     
    3.5 update_mini_batch函数
    def update_mini_batch(self, mini_batch, eta):
    nabla_b = [np.zeros(b.shape) for b in self.biases]
    nabla_w = [np.zeros(w.shape) for w in self.weights]
    for x, y in mini_batch:
    delta_nabla_b, delta_nabla_w = self.backprop(x, y)
    nabla_b = [nb+dnb for nb, dnb in zip(nabla_b, delta_nabla_b)]
    nabla_w = [nw+dnw for nw, dnw in zip(nabla_w, delta_nabla_w)]
    self.weights = [w-(eta/len(mini_batch))*nw
    for w, nw in zip(self.weights, nabla_w)]
    self.biases = [b-(eta/len(mini_batch))*nb
    for b, nb in zip(self.biases, nabla_b)]
    它的输入参数是mini_batch【size=10的tuple(x,y)】和eta【3.0】。
    def update_mini_batch(self, mini_batch, eta):
    nabla_b = [np.zeros(b.shape) for b in self.biases]
    ## 回忆一下__init__,biases是一个列表,包含两个矩阵,分别是30*1和10*1
    ## 我们先构造一个和self.biases一样大小的列表,用来存放累加的梯度(偏导数)
    nabla_w = [np.zeros(w.shape) for w in self.weights]
    ## 同上, weights包含两个矩阵,大小分别是30*784和10*30
    for x, y in mini_batch:
    delta_nabla_b, delta_nabla_w = self.backprop(x, y)
    ## 对于一个训练数据(x,y)计算loss相对于所有参数的偏导数
    ## 因此delta_nabla_b和self.biases, nabla_b是一样大小(shape)
    ## 同样delta_nabla_w和self.weights,nabla_w一样大小
    nabla_b = [nb+dnb for nb, dnb in zip(nabla_b, delta_nabla_b)]
    ## 把bias的梯度累加到nabla_b里
    nabla_w = [nw+dnw for nw, dnw in zip(nabla_w, delta_nabla_w)]
    ## 把weight的梯度累加到nable_w里
    self.weights = [w-(eta/len(mini_batch))*nw
    for w, nw in zip(self.weights, nabla_w)]
    ## 使用这个batch的梯度和eta(步长)更新参数weights
    self.biases = [b-(eta/len(mini_batch))*nb
    for b, nb in zip(self.biases, nabla_b)]
    ## 更新biases
    ## 这里更新参数是除了batch的大小(10),有的人实现时不除,其实没有什么区别,因为超参数eta会有所不同,如果不除,那么eta相当于是0.3(在eta那里就除了batch的大小了)。
    3.6 backprop函数

    这个函数就是求loss相对于所有参数的偏导数,这里先不仔细讲解,等下次我们学习梯度的求解方法我们再回来讨论,这里可以先了解一下这个函数的输入和输出,把它当成一个黑盒就行,其实它的代码也很少,但是如果不知道梯度的公式,也很难明白。
    def backprop(self, x, y):
    nabla_b = [np.zeros(b.shape) for b in self.biases]
    nabla_w = [np.zeros(w.shape) for w in self.weights]
    # feedforwardactivation = x
    activations = [x] # list to store all the activations, layer by layerzs = [] # list to store all the z vectors, layer by layerfor b, w in zip(self.biases, self.weights):
    z = np.dot(w, activation)+b
    zs.append(z)
    activation = sigmoid(z)
    activations.append(activation)
    # backward passdelta = self.cost_derivative(activations[-1], y) * \
    sigmoid_prime(zs[-1])
    nabla_b[-1] = delta
    nabla_w[-1] = np.dot(delta, activations[-2].transpose())
    for l in xrange(2, self.num_layers):
    z = zs[-l]
    sp = sigmoid_prime(z)
    delta = np.dot(self.weights[-l+1].transpose(), delta) * sp
    nabla_b[-l] = delta
    nabla_w[-l] = np.dot(delta, activations[-l-1].transpose())
    return (nabla_b, nabla_w)
       它的输入就是一个训练样本(x,y)分别是784*1和10*1。输出就是和self.biases,self.weights一样大小的列表,然后列表中的每一个数组的大小也是一样。具体到上面的例子,输出nabla_b包含两个矩阵,大小分别是30*1和10*1;nabla_w也包含两个矩阵,大小分别是30*784和10*30。
     
    未完待续

    作者简介:李理,目前就职于环信,即时通讯云平台和全媒体智能客服平台,在环信从事智能客服和智能机器人相关工作,致力于用深度学习来提高智能机器人的性能。
    责编:周建丁(zhoujd@csdn.net
    1
    评论

    环信感恩一路有你 环信 感恩节

    beyond 发表了文章 • 168 次浏览 • 2016-11-24 13:34 • 来自相关话题

    感恩是一种陪伴;感恩是一种亲情;感恩是一起栉风沐雨;感恩是一起砥砺前行;感恩一路有你!
    感恩是一种陪伴;感恩是一种亲情;感恩是一起栉风沐雨;感恩是一起砥砺前行;感恩一路有你!

    262563490636374582.jpg