美团热更方案ASM实践
这篇文章主要讲美团热更方案中没讲到的部分,包含几个方面:
- 作为云服务提供厂商,需要提供给客户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 {ASMifier反编译后字节码如下
long getIndex(int val) {
return 100;
}
}
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) {ASMifier反编译后代码如下:
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;
}
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这个函数是asm.jar访问类函数时触发的事件,desc变量对应java jni中的signature,比如这里是'(I)J', 需要解析并转换成primitive类型,类,数组,void。这部分代码参考了android底层的源码libcore/luni/src/main/java/libcore/reflect,和sun java的SignatureParser.java,都有反映了这个遍历过程。
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
关于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/specs/jvms/se7/html/jvms-4.html)
讨论
前面代码插桩的部分和美团热更文章中保持一致,实际上还有些细节还可以调整。isSupport这个函数的参数可以调整如下if (PatchProxy.isSupport(“getIndex”, "(I)J", false)) {这样能减小插桩部分代码,而且可以区分名字相同的函数。
PatchProxy.isSupport最后一个参数表示是普通类函数还是static函数,这个是方便java应用patch的时候处理。
源码地址
https://github.com/easemob/empatch
作者:李楠
公司:环信
关注领域:Android开发
文章署名: greenmemo 收起阅读 »
17年第一班车,老司机带你撸一个超级表单
麦客表单(http://www.mikecrm.com/)是一款在线表单制作工具,可以帮助我们轻松完成信息收集与整理。
环信移动客服是环信(http://www.easemob.com/)基于环信即时通讯云打造的全媒体智能云客服平台。
先看最终效果http://form.mikecrm.com/xnnc3o
细心的老司机们估计已经发现
在表单的下方多了一个在线咨询按钮,用户填写表单的过程中遇到了问题,可以点击“在线咨询”按钮获得帮助,极大的提升了用户体验与我们的工作效率。
这得益于麦客表单的组件自定义功能与环信移动客服返璞归真的集成方式,可以将两者结合就能让我们在表单中快搭建与用户无障碍交流的桥梁。
原理为:麦客表单组件的超链接里填入环信移动客服访客端入口地址
赶紧分享,仅需3步!
1:注册环信移动客服
http://kefu.easemob.com/
2:获取环信移动客服租户ID
点击右上角切换到管理员模式-->设置-->企业信息
3:将环信移动客服访客端入口地址填入麦客表单的自定义连接。
(PS:环信移动客服访客端入口地址为http://kefu.easemob.com/webim/im.html?tenantId=租户ID)
然后就大功告成!环信移动客服的访客入口地址你能将他放在任何地方,微信公众号、网站,APP,怎么舒服怎么来。 当然,环信移动客服和麦客表单的神奇之处不竟如此,更多功能等待你挖掘。
最后附上客服端的截图,也就是解答你问题的MM工作台
作者:捞鱼-一个0岁的产品经理
收起阅读 »
React Native V0.2.0已发布,android 支持调用相机/本地图片
React Native :v0.2.0 @ 2017-01-03
Feature 新功能:
Android:
Android:BugFix and Update 修复和更新:
• 登录
• 注册
• 好友
◦ 列表及筛选
◦ 好友信息展示
◦ 黑名单
◦ 删除好友
• 好友通知
◦ 添加好友通知展示
◦ 接受好友请求
◦ 拒绝好友请求
◦ 添加好友
• 群组
◦ 群组列表
◦ 群组成员列表
• 聊天
◦ 相机图片消息
◦ 本地图片消息
◦ emoji消息
◦ 普通消息
• 异常状态处理
◦ 断线退出到登录页
◦ 重复登录退出到登录页
• 群成员刷新crash的问题Extra 其他:
• 添加logout图标
• 统一iOS和Android UI
• emoji颜色在android颜色太浅
• 登出的时候清空用户状态
• 完善联系人页面的搜索在切换场景及刷新时的显示逻辑
•[react-native] 升级到最新0.39.2版本历史 :更新日志
• [component/input] 功能完善
• [component/button] 功能完善
• [addContactModal] 统一通过react-native-router-flux管理modal
• [tabBar] 统一通过react-native-router-flux管理tabBar
• [camera] android 支持调用相机/本地图片
• [yarn] 添加yarn lock file,可以通过yarn做包管理
SDK下载:点击下载 收起阅读 »
关于Android Studio如何正确集成环信3.1.5 SDK
2 将build.gradle删除(这一点非常重要,因为没有删除这个文件,后面出现的冲突浪费了我三四天时间)
3 在examples-ChatDemoUI3.0找到libs
4 在你的 (注意是主modules) 主modules-main下创建jniLibs文件夹,把demo中的jar包全部父之过去
5 之后就是导入easeui了(注意:确认build.gradle已经删除)
接下来是添加依赖,选择菜单栏上Project Structure,找到自己的工程,在Dependencies找到绿色的“+”,选择第三个“Module dependency”
6 导入easeui后发现他自动生成的build.gradle,这时才是我们需要的,将easeui的v4版本提升到23
compile'com.android.support:support-v4:23.+'
7 这时应该编译通过了,有些机子运行发现以下问题
java.lang.UnsatisfiedLinkError: com.android.tools.fd.runtime.IncrementalClassLoader$DelegateClassLoader[DexPathList[[dex file "/data/data/com.xxx.xxx/files/instant-run/dex/slice-support-annotations-24.1.0_ce9c5697cabea4565e89d9bb7a81deef74f26296-classes.dex", dex file "/data/data/com.xxx
如果遇到UnsatisfiedLinkError的问题,则在easeUI依赖库-jniLibs创建armeabi-v7a,然后将armeabi的.so文件拷贝一份进去即可。
之后你就可以根据你的需要进行自定义了,可以参考ChatDemoUI3.0,里面的逻辑很齐全了
这是导入easeui的正确方式,是我用几天换来的宝贵经验,希望能帮到大家,多多打赏哈 收起阅读 »
聊天机器人成网络话题焦点,移动客服才是幕后推手
Kik Services的总裁Josh Jacobs在最近的Forrester CXSF大会上尝试对这一问题做出回答。
“我认为答案的关键在于移动端的崛起,首先以及最重要的是,我们正处于一个临界点,此时绝大多数的计算都发生在移动设备上。”
Jacobs说,“聊天成为了杀手级应用,在中国,微信所成就的一切为世界其他国家树立了榜样。我们采取了这样的模式,Facebook也已采取了这种模式。我们认为在世界的其他地方也可以建立这样的模式。”
Jacobs说,在过去的25年里,“我们已经开发出了涵盖各种需求的网络服务,这些服务延展到了我们房间灯泡下面的一切东西;然而没有一种用户界面,能够让所有这些服务以一种简单的方式整合起来。在物联网时代,对话成了一种简便易行的方式;在网络服务驱动的世界里,对话也是一个更好的方案。如今,人们已经习惯了用移动设备来进行对话,越来越多的人也都加入到这个行列之中,且认为体验‘很赞’。”
Jacobs还说,他的公司和其他公司现在所做的工作并不是前无古人的。
“鉴于我的年龄,我的经验告诉我,我们现在所做的一切都并不是前人没有做过的。”他说,“这就是CRM,CRM已经在我们身边出现了很多年。另一个我们看到的趋势是,大公司,尤其是客服领域的大公司都会说‘我们必须得把这个新渠道加上。’”
首先,Jacobs说,聊天“要么扩展了漏斗的顶端,并把很多人吸引进去;要么,就是被成为移动端客户的不适应吓跑。”
其次,Jacobs建议各大品牌关注他们能够提供的休闲体验,以在吸引客户方面“放长线钓大鱼”。
品牌公司们应该随时了解自家客户最舒适的交互方式,并相应地参与到这些方式中去。
“下一代人希望用聊天或者语音,所以你就需要进入到这些环境中去,这些渠道是你接触到这些用户的必由之路。”Jacobs说,“这就是面向Z世代和千禧一代的CRM、再参与、再营销和客户终身价值。”
有的品牌还在对此保持怀疑态度,在往聊天或任何与之相关项目的迈进上踌躇不前。
“我认为我们讨论的一切都是关于如何让品牌发出自己的声音,让你的品牌参与到对话之中。”Jacobs说,“你可以更多的围绕着市场营销的环节去做这件事。”(环信编译自www.loyalty360.org)
关于环信移动客服
全媒体智能云客服倡领者,于2016年荣膺“Gartner 2016 Cool Vendor”。环信支持全媒体接入,包括网页在线客服、社交媒体客服(微博、微信)、APP内置客服和呼叫中心等多种渠道均可一键接入。基于环信业界领先的IM长连接技术保证消息必达,并通过智能客服机器人技术降低人工客服工作量。同时,基于人工智能和大数据挖掘的客户旅程透析产品"环信客户声音"能够帮助企业优化运营,提高跨渠道客服体验。收起阅读 »
截至2016年上半年,环信移动客服共服务了29437家企业用户,现已覆盖包括电商、O2O、互联网金融、在线教育、在线旅游、移动医疗、智能硬件、游戏等领域的Top10客户,典型用户包括国美在线、58到家、楚楚街、海尔、神州专车、新东方、链家、泰康在线、中信证券等众多标杆企业。根据易观发布的《中国SaaS客服市场专题研究报告》显示,环信移动客服在SaaS移动端客服用户覆盖占比高达77.4%,稳居行业第一。
浅论呼入型电话银行中心的风险管理
商业银行是运营风险的企业,与商业银行的其他产品、渠道一样,电话银行的每一运营环节都存在风险。研究电话银行的风险管理。一是更好地服务客户、保障客户权益的需要;二是商业银行风险管理的重要组成部分;三是提高和完善电话银行运营管理水平的需要。
一、电话银行风险管理理念现状
目前利用电话银行的自助交易服务进行犯罪的案件越来越多,尤其在2004年之后,犯罪分子的作案手段不断翻新,甚至出现银行网点员工内部作案的情况。更令人担心的是,随着电话银行开通人工辅助交易,对电话银行中心的风险管理也提出了新的要求。
由于我国银行业对客户服务中心的风险管理并没有一个统一的规范或行业标准,所以各家银行的风险防范理念各不相同,对电话银行的风险认识差异比较大,这就直接导致了在电话银行开通的业务呈现多样化发展趋势,对不同业务防范风险的手段也是多样化的。我国银行对电话银行的风险管理理念主要有以下三种。
(一)业务规避型
由于担心开通的业务可能带来风险,这类银行一般宁可舍弃一些可能给电话银行带来利润的业务,通过减少向客户提供的服务以降低电话银行可能面临的风险。基于这种理念设立的客户服务中心将成为银行的成本中心。
(二)风险规避型
基于“没有风险,就没有利润”的认识,这类银行会选择性地开办一些能够给客户服务中心带来利润且风险可控的业务。
(三)混合型
我国较大规模的银行在各个区域开通的服务不尽相同,同一家银行不同区域的客户服务中心采用的风险管理理念有的是业务规避型、有的是风险规避型,这样的银行由于目前还没有形成统一的理念,所以可以称为“混合型”,但从长远来讲,这种兼而有之的模式并不稳定,最终是会“二者择其一”。
风险防范手段的不足已经极大地影响商业银行电话银行在我国的发展。随着居民对银行推出的金融产品需求日益迫切,银行推出新业务的周期不断缩短,人们更加渴望通过便利的渠道获得这些新业务,但风险防范手段的匮乏已经阻碍了银行在电话银行开拓业务的进程。监管部门多次强调防范风险是商业银行发展的前提,电话银行面临着防范风险与拓展业务双重压力。
二、呼入型电话银行中心业务情况风险分析
(一)一般性呼入业务常见风险
呼入型电话银行中心一般业务包括业务咨询、建议、投诉、交易等,尽管各大电话银行中心出台了许多合规管理条例,也配套客户满意度、抽听录音质检等手段,但特定风险依然存在。主要包括:
1、业务咨询差错风险
业务咨询差错风险的产生因素众多,主要有几个方面。一是知识库更新维护有一定滞后性,甚至可能同一工作日的不同时间段,知识库的更新将导致部分的客户得到的信息不对称,甚至出现矛盾。二是工作人员自身综合能力的差异,在对知识进行解密再加密再解密的过程中对知识理解的偏差、表述的偏差以及客户理解的偏差都可能导致业务咨询出错。三是绩效考核的要求,通话时长、接通率、小休率等指标都会给客服代表造成一定影响,而客户在线等待长也会给客服代表带来无形压力,容易使工作人员在回答过程中凭借主观的知识记忆作答。不仅限于此,业务咨询差错极易导致客户时间和费用的消耗,易引起客户不满,进而引发深层次矛盾,造成声誉风险等。
2、口头挂失处理风险
作为经营风险的企业,商业银行在处理特殊业务时稍有不慎都会导致实实在在的资金风险,而对于电话银行而言最常见的操作风险便是挂失风险,挂错账户、挂失不及时、未提醒客户挂失等都可能给客户带来损失。近年来由于新型电信诈骗层出不穷,客户来电核实账户交易信息,很多情况下应协助其尽快办理挂失,而由于客服代表未能有效识别风险、遗忘提醒客户挂失而导致客户发生的损失的情况也时有发生。另外为最大限度的保障客户资金安全,各大银行在办理电话口头挂失时的核实条件往往会放宽,这种“宽进严出”的做法也时常被不法分子利用,导致蓄意挂失他人账户的情况发生。诸如此类事件,都给我们的日常工作敲响警钟。
3、交易处理风险
随着商业银行电话银行业务受理范围的拓展,目前可通过电话银行办理的交易不断增多,包括转账、缴费、信用卡还款、基金理财、账户金买卖等,该类交易操作一般都有严格的操作流程及风险控制机制,如身份核实要素、验证密码,设置约定账户、主叫号码限制等,但由于人为操作以及新型诈骗手段等因素的影响,仍然存在巨大风险,这要求工作人员在处理交易时既要严格遵照规程办理,也要灵活机警地识别风险,务必集中精力,谨慎操作。
4、身份校验风险
目前电话银行的身份验证既包括IVR密码验证、CSR人工辅助电话银行密码验证、客户账户信息核对等传统手段,也包括声纹识别等新型技术手段。验证身份的过程中若不提高警惕、对储户保密原则保持高度的敏感,将极容易泄露客户个人信息,这将导致隐私风险。在实际操作过程中,限于电话银行的虚拟性及远程性,只能通过客户提供的信息为依据判断是否为户主本人,这种核实的准确度明显不及网点柜台的当面核实。虽然客户有保护个人信息的责任,但银行对进一步加强储户信息安全同样义不容辞。
5、客户问题升级处理风险
电话银行中心由于知识储量、信息传递、处理权限等原因限制,相当一部分问题需升级处理,这包括电话银行中心内部的升级处理,比如投诉受理等,也包括登记工单转交其他业务部门处理的情况。由于工单流转涉及多个部门且不同业务信息处理所需时间不同,甚至由于座席员的疏忽导致工单无法流转,各种不可控因素都会影响回复客户的时间。客户将质疑通话过程中所提供的回复时间,小则为下一手座席及后续处理造成沟通困难,大则影响信誉及客户情感的维系。
6、设施环境风险
电话银行中心主要依赖电信网络等设施提供服务,路由切换、IP地址管理等技术方面的工作显得尤为重要,而这也是电话银行中心不可忽视的风险源。这类风险既包括技术设施老旧等引发故障的风险,也包括新旧设备切换过程中引发的风险,以及新设备投入使用后造成的稳定性风险。该类风险给电话银行中心带来的影响往往是全面性、毁灭性的,具有影响范围广、影响程度深等特点,是电话银行中心的面临的基础性风险之一。
(二)承接海外机构电话银行业务的风险
随着银行国际化经营的发展,目前国内各大银行的电话银行中心承接海外分行电话银行业务的情况越来越多,海外电话银行业务风险管理方面除了面临上述一般性呼入业务的风险外还面临着以下突出的风险源。
1、不符合当地监管规定引发的合规风险
目前我国商业银行的海外拓展一般以子公司、子机构等方式进行,这就意味着在银行的监管体系中如果该银行的国内电话银行中心需要承接海外电话银行业务,需要以第三方公司外包的形式进行承接,这就往往会涉及第三方公司的资质审核等问题,同时在承接的业务类型方面也有更多限制,面临更多的合规性审查等问题。此外,在实际的业务操作中也要以当地的监管规定为准,因此由于国内国外的监管政策不一,极容易出现不符合当地监管规定而引发的合规风险,给海外机构带来损失。
2、服务需求差异引发的声誉风险
随着各大银行海外版图的不断扩大,其电话银行中心的服务范围也不断增大,各国各地区间的文化差异也相对较大,同时各国客户对电话银行的服务需求存在巨大差异,例如亚洲客户和欧美客户,其语言表达、心理诉求等各方面差异都会给海外电话银行服务带来巨大挑战,同时也会产生一定风险。这种风险表面上导致电话银行中心在服务过程中无法达到客户的期望值,而实质上将对海外客户对银行的印象的大打折扣,甚至影响中国银行业在海外的拓展及发展。
(三)运营管理及业务管理风险
1、人员排班风险
受季节、气候、突发事件等外部因素影响,电话银行中心的来电量经常呈不规律变化,来电量预测及排班管理难度较大,人员需求与业务量匹配是否合理是电话银行运营管理的重大风险点之一。排班风险对电话银行的接通率及人员利用率具有重大影响,如果排班人数不足,则可能导致一定时段的接通率低下,严重影响客户体验,甚至引发重大投诉的声誉风险;如某一时段的排班人数过多,又容易造成人力资源浪费,为日后的人员调配埋下隐患,因此人员排班风险也是影响电话银行运营稳定的基础性风险之一。
2、应急处理风险
成熟的电话银行中心均有完善的应急管理体系,尽管如此,应急风险只能有效减小而无法消除,以笔者所在的电话银行中心为例,2013年4月20日四川雅安发生7.0级地震,对当地造成重大影响,当地电话银行中心工作人员紧急撤离,暂停对外服务,电话直接溢入其他电话银行中心。除以上重大突发自然灾害引发来电变化外,在日常运营中还可能遇到诸如电子银行系统、自助设备等系统突发故障引发的短期来电剧增的情况。短期内来电剧增首先会影响接通率,如果处理不当则容易引发重大投诉,因此突发事件处理是影响中心运营的重大风险点之一。
3、集约化处理风险
为提高处理效率,体现集约化经营的优势,目前不少电话银行中心都采取集约化处理的方式,例如针对疑难问题、交易等业务集中处理,或者在话务高峰期采取高峰话务分流等措施,这些集约化处理手段一方面有利于提高处理效率,保障中心整体接通率;另一方面,由于涉及问题登记、流转交接、无法即时处理等,也是引发客户投诉的风险点之一。
4、知识及信息传递风险
知识库是电话银行中心客服代表工作的信息库,是做好电话银行工作的重要基础。一是银行金融服务具有种类齐全、产品丰富、业务规定变化快、产品系统不断推陈出新的特征,且客户数量庞大,若知识库维护不及时,业务资料缺失、出错或者过时等都会在短时间内引发巨大的客户投诉风险,严重的还可能给客户带来损失或者引发声誉风险。二是信息传递容易出现不对称的情况,由于业务规则制定者、知识维护人员、培训人员、前线座席员等岗位的工作分工不同、沟通问题,都可能导致知识信息在传递的过程中出现偏差,从而引发服务风险。
(四)电话银行信息管理风险
1、录像录音凭证管理风险
电话银行中心通过电话语音为客户提供金融服务,无法像柜面渠道一样提供纸质凭证作为交易或服务凭证,因此录音凭证是客户与座席员产生业务往来的最重要凭证。2005年4月1日起施行的《中华人民共和国电子签名法》规定:“数据电文不得仅因为其是以电子、光学、磁或者类似手段生成、发送、接收或者储存的而被拒绝作为证据使用。”客户录音可以作为电子(数码录音)、光学(光盘)、磁(磁带)等形式保存,从法律上讲,可以作为客户与银行要约关系的凭证,录音同签字一样具有不可抵赖性。目前,客户拨打服务95533入线接通人工服务之前,自助语音会提示对服务过程进行录音,客户确认之后才能接通人工座席,此环节可避免因客户不知情而产生侵权风险。
正是由于录音凭证的重要性,所以录音凭证的管理也极为关键。目前录音管理的风险环节包括:一是内部听取录音或使用录音所产生的风险。电话银行中心内部使用录音包括质检抽听录音、培训分享等,这其中涉及人员较多,容易引发泄露风险。二是外部调取录音所产生的风险。包括协助客户、公安机关调查取证等调取录音,由于涉及客户、司法机关、建设银行等多个主体,在录音调取及使用过程容易形成风险。三是由于录音保管期限较短所产生的风险。因前期形成的风险可能在未来的某一个时间段爆发,所以延长录音的保管期限有一定的必要性。另外,一些电话银行中心还对座席员的CSR操作进行录像监屏并在所有的办公区域设置监控探头,系统录像监屏和办公场所实时监控都是为了防范风险,若相关的监测系统失效或者数据丢失,都存在潜在风险。
2、信息保密
电话银行中心涉及到众多数据信息,包括业务知识库、客户数据信息、录音录像监屏等。业务知识库是建行业务的大辞典,所有的信息都只供业务查询使用,需防止员工或外部人员侵入系统或者通过邮件、U盘拷贝等方式外传。另外,电话银行业务需要记录、传输、存储和处理客户手机号、账号、余额、姓名等客户重要信息,在为客户提供人工服务时,客服代表会查询客户的账户信息或按照客户要求协助客户办理相关交易。在操作过程中电话被窃听或客服代表使用的电脑终端存储的客户信息能够通过U 盘或其他手段拷贝等,都会形成客户信息泄露风险。
三、风险防范和对策
(一)业务流程持续改善
业务流程是员工赖以处理问题的行为准则,它是动态的优化过程。流程的优化不仅会带来效率的提高,更加能够防范风险,最大程度上减少漏洞。反之,有漏洞的流程本身便暗藏风险。电话银行中心可通过不断优化的流程来规范员工的日常操作行为,而日常操作行为又推动着流程的优化,二者相辅相成。做好流程优化,一是要从制度上规范流程管理,明确流程制定、试运行、实施、效果评估及再优化等环节的标准;二是调动员工的积极性,全员参与,有利于发现流程漏洞,持续做好各项优化措施的落实。
(二) 优化知识工具和应用短信工具
知识库是座席员的大脑,也是客户了解银行产品和服务的基础。由于知识库有一定的滞后性且涉及面广,在和柜台实际操作中也存在差异,因此及时和相关部门沟通获取第一手资料尤为重要。再者,在进行知识文档优化的过程中,语言表达和排版也要尽量通俗易懂,便于查找,以此提高效率。除此之外还可以开发利用短信工具,避免员工和客户之间口头沟通所产生的误解或遗漏,便于客户直观地获取业务资料和解决方案。
(三) 做好信息传递及培训辅导
信息的传递分由上至下、由下至上。一方面,从银行的产品开发到电话银行中心的服务开展,须保证原则统一、内容统一、落实统一,因此文件的拟定以及解读既要通俗易懂,也要保证传达到位,否则进一步传递到客户的耳朵中可能有大偏差。另一方面,员工所获取到的一手信息不管是高频率咨询业务亦或密集发生型事件都要求能及时上报,进而层层传递再进行确认。培训辅导是信息传递的重要方式之一,通过定期定量的培训不仅有利于业务知识学习、提高服务质量,培训师搜集到的信息反馈,也促进下一次培训辅导的优化,更具针对性。形成回流式的信息传递模式,最终有利于实现大方向的战略部署。
(四) 排班管理科学化
一是要加强来电预测,通过经验数据总结,收集影响因素等建立来电预测模型;二是要加快实现电子化排班,通过引入排班系统来减少人为估算的偏差,实现快速高效排班;三是要做好员工思想动态管理,24小时客服工作性质的特殊性要求必须轮班制,这是对生物钟的挑战,更是对排班管理的挑战,排班人员要深入研究员工的行为状态,结合不同员工的特点进行科学排班。总之,根据经验和科学的预测,不仅能横向覆盖客户的问题,也能纵向提高银行的接通率,而员工也因此得到合理的工作时间安排,与此同时也要做好紧急预案、做好防范工作、安排后备人员,以免来电量异常激增而措手不及。
(五)优化问题处理机制
百密难防一疏,更重要的是问题发生后如何解决,这是客户关注的焦点,也是电话银行时时刻刻面临的挑战。一是要积极地促进多部门合作,同时按照岗位设置的逻辑流转,明确职责,形成多方而高效的处理难题局面;二是要明确原则,分级处理,通过设定不同层级问题的处理时限,及时向客户反馈信息;三是归口统一,在问题解决过程中必须统一由一个落实部门去和客户交流、解决问题;四是要注重问题解决案例的经验总结,通过形成问题解决案例库,引导和提升问题解决人员的处理能力。
(六)规范录音凭证管理
录音是保护客户权益、员工权益和银行权益的手段之一,是具有法律效应的重要凭证。现如今,录音凭证管理更加依赖于技术范畴,因此对设备的维护以及储存空间的容量有了更高要求,定期检查技术设施,及时清理不必要的内存,都能为保存创造更加良好的客观条件。与此同时,凭证管理的人员安排也格外重要,既要与一线工作人员独立开来,保持高度独立性,又要与前台座席员保持紧密联系,且必须做好保密措施。
(七)做好信息保密的核查审计工作
商业银行本身是经营风险的组织,涉及到无数客户个人隐私,因此客户信息的保管异常重要。电话银行中心不仅要严禁工作人员携带纸张、通讯设备等便于记录的工具进入工作区域,也要每天仔细清算工作人员领取及上交的工作稿纸,同时由主管部门牵头定时安排人员清理电脑中含有敏感信息的文件并进行核查。另外还要加大力度培育员工的保密习惯,不携带任何有关客户信息的资料走出工作区域,形成定期检查、互相监督的安全审计局面,做好信息保密的核查审计工作。
(八)加大对服务对象的研究分析
服务对象既包括宏观的服务区域及社会群体,也包括微观的客户组成。随着国内银行的海外拓展,目前不少国内的电话银行中心均有承接海外电话银行服务的职能,而国内的跨区域服务就更为常见,因此研究服务对象尤其重要。一方面要聆听客户声音,发掘客户需求有利于更高效地解决客户问题;另一方面,要进行社会心理分析,针对性给予个性化服务。只有全方位剖析才更有利于信息的反馈,从外部的思考及到内部产品的优化,从源头上防范风险,做好电话银行服务。
(九)建设合规文化以提高风险意识
企业文化是日常工作中形成后被成员广泛认可,认为有效而共享,并共同遵循的基本信念和认知。建设合规文化,提高风险意识,有利于员工在日常工作中下意识地利用流程、知识库等合理地来保护客户、公司、自己的权益。
(十)利用新技术手段提高风险防控能力
一方面,技术的日新月异让不法分子有机可趁,利用改号软件和伪基站冒充各商业银行的客服热线发短信或致电客户,实施诈骗;另一方面,技术的发展也给我们商业银行提供了提高风险防控能力的机会。充分利用新技术手段,加强防盗防骗的防火墙建设,做好预防工作,不让不法分子趁虚而入。同时提高应对能力,面对紧急事件能充分利用新技术手段破解不法行为。这不仅要求有风险防控的成本资金投入,完善设备,也要求有高科技人才的储备。目前可用于电话银行中心的技术手段主要包括更严密的密码校验方式、智能质检系统、智能排班系统、话务监控系统等,通过各种技术系统组合可实现对运营的前瞻预测、事中控制以及事后分析,同时利用大数据分析的方法可充分较少风险发生的可能性,提升风险防控能力。
综上所述,电话银行中心的发展面临着诸多机遇,同时也是巨大挑战,一方面是集约化经营、转型发展的升级良机;另一方面也面临着更加多变的风险挑战及监管要求。只有处理好合规管理与经营发展的关系,才能在不断推进电话银行中心发展的过程中切实抓好风险管理,最大程度维护客户、员工以及银行自身的权益。
本文刊载于《客户世界》2016年11月刊文章;原文作者刘添权,梁雅静,本文作者单位为中国建设银行广州电话银行中心。 收起阅读 »
环信联合阿里云新年送福利
2017“环信”有“你”更精彩!环信祝福小伙伴们,新春快乐,鸡年吉祥!
新的一年,环信联合阿里云给各位环信小伙伴送福利
参与形式:
步骤一:在IMGeek社区发布一篇环信集成笔记
可以是集成教程:遇到的问题,如何解决的,对环信的的建议。
步骤二:在本贴留言区回复文章地址
活动奖品:
奖品一:参与发布文章的童鞋,将获得环信与阿里云共同推出的【阿里云免费套餐半年使用权】,将通过社区私信方式送出激活码,直接激活即可使用。
阿里云免费套餐简介:
• 包含 30 余款云计算产品最长 6 个月免费使用资格
• 产品总价值超 20000 元
套餐里包括云服务器、数据库、存储、网络产品等基础云计算产品,搞个电商、玩个网站等想法都可以轻松实现
奖品二:【.xyz域名免费注册】,在下方回复留言新年祝福,然后自己领取xyz域名带走吧http://click.aliyun.com/m/8900/ ,总量2000个名额,先到先得哦~~
活动时间:1月4日开始,下手要快,先到先得哦~
附:【阿里云免费套餐半年使用权】是以邀请码形式开通
领到码之后,到活动官网(http://click.aliyun.com/m/8901/ ),用阿里云新手帐户登录,通过验证后即可开通。
收起阅读 »
新版群组/聊天室服务 REST API差异说明v2
(最新更新时间: 2017-01-06)
第一版差异说明发出后,我们收到了一些开发者的反馈。根据这些反馈,我们对新版群组/聊天室的异常、状态码进行了调整和优化。相比2017-01-03第一个版本,主要更新内容:
- com.easemob.group.exception.ForbiddenOpException 异常类型的状态码 由400变成403,该异常表示本次调用不符合群组/聊天室操作的正确逻辑,例如调用添加成员接口,添加已经在群组里的用户,或者移除聊天室中不存在的成员等操作。
- 部分操作抛出的com.easemob.group.exception.InvalidParameterException (状态码400) 异常类型替换为 com.easemob.group.exception.ForbiddenOpException (状态码403)
- 如果用户加入群组/聊天室的个数超限,或者appkey下群组/聊天室的个数超限,将抛出异常com.easemob.group.exception.ExceedLimitException, 状态码 403
- 群加人(单加/批量),用户已经在群里
- 群减人(单减/批量减)被减用户(存在但)都不在群
- 转让群主,自己转自己
- 转让群主,转给一个不在群里的用户
- 批量添加黑名单,用户都已经在黑名单
- 单个加入黑名单,用户不是群组成员
- 把群主加入黑名单
- 修改聊天室信息,修改的字段含有不允许修改的字段,例如id
- 聊天室减人,被删除成员不在聊天室
- 限制检查
- 旧群组抛出的异常类型:java.lang.IllegalArgumentException 状态码为400
- 新群组抛出的异常类型: com.easemob.group.exception.ResourceNotFoundException 状态码为404,异常描述为"username %s doesn't exist!"
- 旧群组服务返回200,
- 新群组服务抛出的异常类型:com.easemob.group.exception.ForbiddenOpException 状态码为403 异常描述为 "can not join this group, reason:%s"(注:批量加人时,如果部分用户不在群里,则该部分用户可以添加成功,返回状态码为200)
- 旧群组服务返回200
- 新群组服务抛出的异常类型: com.easemob.group.exception.ForbiddenOpException 状态码为403 异常描述为 "users %s are not members of this group!"(注:批量减人时,如果部分用户在群里,则该部分用户可以移除成功,返回状态码为200)
- 旧版群组服务返回200
- 新版群组服务抛出异常:com.easemob.group.exception.ForbiddenOpException 状态码403 异常描述:"new owner and old owner are the same "
- 旧群组服务可以转成功,原来的群主被从群里移除
- 新群组服务要求新群主必须首先是群成员,因此会抛出异常 com.easemob.group.exception.ForbiddenOpException 状态码403 异常描述为:"user: %s doesn't exist in group: %s"
- 旧群组服务旧群主从群成员中移除
- 新版群组服务,旧群主还留在群里。
- 旧群服务返回200
- 新群服务抛出异常: com.easemob.group.exception.ForbiddenOpException 状态码 403 异常描述为:"users %s are not members of this group!"
- 旧群服务返回200
- 新群服务抛出异常: com.easemob.group.exception.ForbiddenOpException 状态码 403 异常描述为:"forbidden operation on group owner!"
- 旧群组抛出的异常类型:java.lang.IllegalArgumentException 状态码为400
- 新群组抛出的异常类型: com.easemob.group.exception.ResourceNotFoundException 状态码为404 异常描述为"username %s doesn't exist!"
- 旧群组服务返回200
- 新群组服务抛出的异常类型: com.easemob.group.exception.ResourceNotFoundException 状态码为404 异常描述为:"grpID %s does not exist!"
- 旧群组服务返回200
- 新群组服务抛出的异常类型: com.easemob.group.exception.ResourceNotFoundException 状态码为404 异常描述为:"grpID %s does not exist!"
- 旧群组服务返回200,修改了允许修改的内容
- 新群组服务,如果传入的修改字段中,包含不可更改,或者是无效的字段,会抛出异常: com.easemob.group.exception.InvalidParameterException 状态码为400 异常描述为 "some of %s could not be modified" 或者 "some of %s are not valid fields"
- 新群组服务,禁止通过修改群组/聊天室接口来修改owner, 否则会抛出异常:com.easemob.group.exception.ForbiddenOpException,状态码403 异常描述为: "owner cannot be updated through this method!")
- 旧群组服务返回200
- 新群组服务抛出的异常类型: com.easemob.group.exception.ResourceNotFoundException 状态码为404 异常描述为:"grpID %s does not exist!"
- 旧群组服务返回200
- 新群组服务抛出的异常类型: com.easemob.group.exception.ResourceNotFoundException 状态码为404 异常描述:"grpID %s does not exist!"
- 旧群组抛出的异常类型:java.lang.IllegalArgumentException 状态码为400
- 新群组抛出的异常类型: com.easemob.group.exception.ResourceNotFoundException 状态码为404 异常描述: "username %s doesn't exist!"
- 旧群组服务返回200
- 新群组服务抛出的异常类型: com.easemob.group.exception.ForbiddenOpException 状态码为403 异常描述: "users %s are not members of this group!"
- 旧群组服务返回200
- 新群组服务抛出的异常类型: com.easemob.group.exception.ResourceNotFoundException 状态码为404 异常描述:"grpID %s does not exist!"
- 用户加入的群组/聊天室超过上限:"can not join this group, reason:user %s has joined too many groups/chatroom!"
- appkey下创建群组/聊天室超过上限:"this appKey has create too many groups/chatrooms!"
权限检查
所有群组/聊天室的接口,如果调用者不具备相应接口的调用权限, 抛出的异常类型:com.easemob.group.exception.GroupAuthorizationException,状态码为 401 收起阅读 »
2016年度盘点:一家典型互联网公司的必备工具宝箱
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年继续高歌猛进,越跑越快。
收起阅读 »
聊聊即时通讯(IM),基于环信 web im SDK
感觉自从qq、微信这种APP用多了,现在都没啥人发短信了,现在什么APP都想加入IM的功能,曾经有段时间在折腾自己撸一个聊天的东西,也尝试过很多平台,今天这里给大家介绍一下从零开始自己做一个聊天的app功能。因为之前帮朋友做过一个基于环信的聊天功能,这里就以环信的平台为例举个例子说明。这篇文章注意想讲解一下集成这种第三方的一般实现方法。
准备工作
1.注册账号
我们要先去环信官网注册一个账号,然后在后台创建一个应用,因为我们后面在做功能的时候可以用后面发送消息及图片来测试收消息,用户管理在后台也可以看得一清二楚。
创建成功后找到应用标识(AppKey),这个在后期配置中会用到。
2.下载SDK
http://www.easemob.com/download/im
这里我们使用的是Web IM,所以下载的SDK是Web IM版本,下载之后我们会看到一个演示demo,由于这个是pc版本,和我们需求不一致,所以我们只需要关心sdk目录下的文件和sdk集成需要修改的配置文件easemob.im.config.js。
|---README.MD:3.开发文档
|---index.html:demo首页,包含sdk基础功能和浏览器兼容性的解决方案
|---static/:
js/:
easemob.im.config.js:sdk集成需要修改的配置文件
css/:
img/:
sdk/:/*sdk相关文件*/
release.txt:各版本更新细节
quickstart.md:环信WebIM快速入门文档
easemob.im-1.1.js:js sdk
easemob.im-1.1.shim.js:支持老版本sdk api
strophe.js:sdk依赖脚本
Web IM 介绍 http://docs.easemob.com/im/400webimintegration/10webimintro
项目实战
由于这篇重在在于如何使用第三方开发IM,感觉说再多也诶有意义,直接上代码说明。不讲解过多的原理、细节,只讲究开发流程。
1.用户注册功能
首先我们在hbuilder中先新建一个项目easemobIM,然后把环信sdk文件夹和配置文件拷贝到我们的工程中。为了节约时间,下面的功能演示我是根据官方登录模板改的。
html/reg.html
<!DOCTYPE HTML>这是注册页面的代码,我们首先要引入环信的sdk和easemob.im.config.js,并且将easemob.im.config.js中的appkey换成自己的,然后根据用户名/密码/昵称注册环信 Web IM,提交注册的代码为:
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
<title></title>
<link href="../css/mui.min.css" rel="stylesheet" />
<link href="../css/style.css" rel="stylesheet" />
<style>
.mui-input-group:first-child {
margin-top: 20px;
}
.mui-input-group label {
width: 22%;
}
.mui-input-row label~input,
.mui-input-row label~select,
.mui-input-row label~textarea {
width: 78%;
}
.mui-checkbox input[type=checkbox],
.mui-radio input[type=radio] {
top: 6px;
}
.mui-content-padded {
margin-top: 25px;
}
.mui-btn {
padding: 10px;
}
</style>
</head>
<body>
<header class="mui-bar mui-bar-nav">
<a class="mui-action-back mui-icon mui-icon-left-nav mui-pull-left"></a>
<h1 class="mui-title">注册</h1>
</header>
<div class="mui-content">
<form class="mui-input-group">
<div class="mui-input-row">
<label>手机</label>
<input id='username' type="text" class="mui-input-clear mui-input" placeholder="请输入手机号码">
</div>
<div class="mui-input-row">
<label>昵称</label>
<input id='nickname' type="text" class="mui-input-clear mui-input" placeholder="请输入昵称">
</div>
<div class="mui-input-row">
<label>密码</label>
<input id='password' type="password" class="mui-input-clear mui-input" placeholder="请输入密码">
</div>
<div class="mui-input-row">
<label>确认</label>
<input id='password_confirm' type="password" class="mui-input-clear mui-input" placeholder="请确认密码">
</div>
</form>
<div class="mui-content-padded">
<button id='reg' class="mui-btn mui-btn-block mui-btn-primary">注册</button>
</div>
</div>
<script src="../js/mui.min.js"></script>
<!--sdk-->
<script src="../sdk/strophe.js"></script>
<script src="../sdk/easemob.im-1.1.js"></script>
<script src="../sdk/easemob.im-1.1.shim.js"></script><!--兼容老版本sdk需引入此文件-->
<!--config-->
<script src="../js/easemob.im.config.js"></script>
<script>
mui.init();
// 输入参数
var regConfig = {
username: mui("#username")[0],
nickname: mui("#nickname")[0],
password: mui("#password")[0],
passwordConfirm: mui("#password_confirm")[0]
};
// 注册事件监听
mui("#reg")[0].addEventListener('tap',function(){
var username = regConfig.username.value;
var nickname = regConfig.nickname.value;
var password = regConfig.password.value;
var passwordConfirm = regConfig.passwordConfirm.value;
// 电话号码校验
if (!isMobile(username)){
mui.toast("电话号码格式不正确");
return;
}
// 昵称非空校验
if (!isEmpty(nickname)){
mui.toast('昵称不能为空');
return;
}
// 密码非空校验
if (!isEmpty(password)){
mui.toast('密码不能为空');
return;
}
// 密码重复校验
if (passwordConfirm != password) {
mui.toast('密码两次输入不一致');
return;
}
// 环信SDK注册
var options = {
username : username,
password : password,
nickname : nickname,
appKey : Easemob.im.config.appkey,
success : function(result) {
//注册成功;
console.log(JSON.stringify(result))
mui.toast('注册成功');
},
error : function(e) {
//注册失败;
console.log(JSON.stringify(e));
mui.toast('注册失败:'+e.error);
}
};
Easemob.im.Helper.registerUser(options);
});
// 是否为电话号码
function isMobile(value) {
var validateReg = /0?(13|14|15|18)[0-9]{9}/;
return validateReg.test(value);
}
// 是否为空
function isEmpty(value){
var validateReg = /^\S+$/;
return validateReg.test(value);
}
</script>
</body>
</html>
var options = {我们注册完了后可以在环信后台【IM用户】查看用户注册信息,我们我们用其他平台,只需要把这块的内容改成相应的内容就OK。
username : username,
password : password,
nickname : nickname,
appKey : Easemob.im.config.appkey,
success : function(result) {
//注册成功;
console.log(JSON.stringify(result))
mui.toast('注册成功');
},
error : function(e) {
//注册失败;
console.log(JSON.stringify(e));
mui.toast('注册失败:'+e.error);
}
};
Easemob.im.Helper.registerUser(options);
2.用户登录功能
有了注册页面的经验,我们写登录页面也很简单,页面布局脚本和其他与登录逻辑无关的代码我这里不贴了,大家在我最后给的地址上下载完整代码,这里只讲解基本基本思路。环信登录优两种方法,一种是通过实例化new Easemob.im.Connection()建立连接,一种是使用工具类Easemob.im.Helper.login2UserGrid(options),我们刚刚注册就是使用了工具类,为了便于大家后面的学习,我们在这里把两种方法都说一下:
实例化new Easemob.im.Connection()建立连接
1.创建连接
var conn = new Easemob.im.Connection();2.初始化连接
conn.init({3.初始化连接
onOpened : function() {
alert("成功登录");
conn.setPresence();
}
});
// 打开连接
conn.open({
user : username,
pwd : password,
appKey : Easemob.im.config.appkey
});
这里我们需要注意的是open()方法中需要配置的属性是user和pwd,这和我们注册时的有区别,要注意哦!
这里需要说明的是init()是环信提供的一个通用的方法,比如后面我们要用到的接收文本消息、图片消息等一系列的回调方法都写在这个里面,onOpened()方法主要是用于当执行conn.open()方法时需要执行的方法,我们一般会把页面需要初始化的逻辑写在onOpened()中,比如查询好友。
完整代码:
// 输入参数工具类Easemob.im.Helper.login2UserGrid(options)建立连接
var loginConfig = {
username: mui("#username")[0],
password: mui("#password")[0]
};
// 创建一个新的连接
var conn = new Easemob.im.Connection();
// 初始化连接
conn.init({
onOpened : function() {
mui.toast("成功登录");
conn.setPresence();
mui.openWindow({
url: 'html/tab-webview-main.html',
extras:{
username:loginConfig.username.value,
password:loginConfig.password.value
}
})
}
});
// 登录事件监听
mui("#login")[0].addEventListener('tap',function(){
var username = loginConfig.username.value;
var password = loginConfig.password.value;
// 电话号码校验
if (!isMobile(username)){
mui.toast("电话号码格式不正确");
return;
}
// 密码非空校验
if (!isEmpty(password)){
mui.toast('密码不能为空');
return;
}
// 打开连接
conn.open({
user : username,
pwd : password,
appKey : Easemob.im.config.appkey
});
});
// 登录
var options = {
user : username,
pwd : password,
appKey : Easemob.im.config.appkey,
success:function(data){
console.log(JSON.stringify(data))
mui.toast("成功登录");
mui.openWindow({
url: 'html/tab-webview-main.html',
extras:{
username:loginConfig.username.value,
password:loginConfig.password.value
}
})
},
error: function(e){
console.log(JSON.stringify(e))
mui.toast("成功失败:"+e);
}
};
Easemob.im.Helper.login2UserGrid(options);
上面我们用了两种方法讲解了登录的方法,各有优劣,第二种只做登录的工作,代码也比较简洁,但是当我们的页面是多个页面时我们的登录状态是不能检测到的,这个时候我们还是需要在每个页面通过创建连接初始化,所以我们在页面跳转过程加入了拓展参数extras传递参数,然后在登陆后的页面接收就可以。
3.页面传参深入探究
为了尽可能简单的演示我们的功能,我这里不使用个性化的设计,就用官方模板组中的【mui底部选项卡(webview模式)】进行展示。新建模板文件如下:
我们去掉第一个选项卡,只保留消息tab-webview-subpage-chat.html、通讯录tab-webview-subpage-contact.html、设置tab-webview-subpage-setting.html三个选项卡。
拓展参数extras传值
上一小节中,我们在登陆页面通过拓展参数extras传值,在主页面接收数据的方法为:
mui.plusReady(function(){在主界面mui.plusReady方法里面拿到值,然后可以在创建子webview时候用拓展参数传值,然后在子页用下面的方法用同样的方法可以拿到值。但是其实我们不需要父页面向子页面发消息,直接在子页面通过这个找到父页面对象就OK了,如下:
var self = plus.webview.currentWebview();
var username = self.username;
var password = self.password;
mui.toast("username:"+username+"<br />"+"password:"+password);
});
子页面代码:
mui.plusReady(function(){预加载时使用mui.fire()传值
var self = plus.webview.currentWebview().parent();
var username = self.username;
var password = self.password;
console.log("username:"+username+"password:"+password);
});
这里需要特别说明一下的是我们有时候想要预加载我们的主页面,这里我们有个地方我需要特别注意的是,我们需要用mui.fire()传递参数:
mui.fire(target,event,data)
特别提醒一下:target是需要接受参数的webview对象,而不是id,在这个地方我出过错误,当时一直没有察觉,如果是id,需要使用plus.webview.getWebviewById(id)进行转换。
比如我们在登陆页面使用preload预加载,代码如下:
...登陆按钮监听事件中的success方法:
var mainPage = null;
mui.plusReady(function(){
mainPage = mui.preload({
"url": 'html/tab-webview-main.html',
"id": 'main'
});
})
...
mui.fire(mainPage,'show',{在主页面中通过自定义show事件获得参数:
username:loginConfig.username.value,
password:loginConfig.password.value
});
setTimeout(function() {
mui.openWindow({
id: 'main',
show: {
aniShow: 'pop-in'
},
waiting: {
autoShow: false
}
});
}, 0);
var username=null,password=null;
// 页面传参数事件监听
window.addEventListener('show',function(event){
// 获得事件参数
username = event.detail.username;
password = event.detail.password;
console.log("username:"+username+"password:"+password);
});
我们需要注意的是我们刚刚在登录页面的账号密码传递到了tab-webview-main.html主页面,但是我们的每个子页面没有拿到账号密码。这里就有个容易犯错的地方,我们可能会直接在创建子webview时候通过拓展参数extras传值。
经过试验发现经过预加载的主界面tab-webview-main.html的mui.plusReady方法比页面的自定义事件监听先执行,这是因为我们通过预加载的时候其实已经就执行了mui.plusReady方法,而自定义事件是在webview打开的时候执行。当主界面被预加载时,子页面的loaded事件也随着完成,创建子页面的时候我们根本就没有拿到数据怎么传,自然在子页得到的是undefined。我们这个时候如果想在主界面生成子页面的时候通过拓展参数extras传递给子页面根本行不通!
当需要接受参数的webview已经完成loaded事件,我们就不能使用拓展参数extras传参数,这个时候我们可以使用webview.evalJS()或者mui.fire();另外我们使用webview.evalJS()或者mui.fire()时,接收参数的页面的loaded事件也必须发生才能使用。
mui传参数只能相互关联的两个webview之间传,比如A页面打开B页面,B页面打开C页面,A页面可以传值给B页面,但是A页面不能传值给C页面,我们可以通过B页面传给C页面。
验证一个webview的loaded事件是否完成的方法:
var ws = plus.webview.getWebviewById(id)验证一个webview的show事件是否完成的方法:
ws.addEventListener( "loaded", function(e){
console.log( "Loaded: "+e.target.getURL() );
}, false );
var ws=plus.webview.currentWebview();说这两个监听事件有啥用处呢,我们在预加载webview的时候,预加载完成的过程,loaded事件也随之完成,但是只有页面被打开时,show事件才完成,我们可以选择合适的时机发送或者接受参数。
ws.addEventListener("show", function(e){
console.log( "Webview Showed" );
}, false );
这里需要说明的是如果你想localstorage、Storage等本地存储传值,完全可以不用extras或者mui.fire(),当然还可以用url传参数。
因为当初就是为了一个想法,预加载试试,然后试着试着各种问题,不过也因此明白了很多规则和调试方法,在这里提出来顺便总结一下页面传参需要注意的问题,免得新手在此花了很多冤枉时间,搞得现在都快忘了前面写了啥。其实这一部分可以独立出来,但是总感觉这种东西不是啥难事,脱离实际去讲总觉得不合适。
4.获取好友列表及添加好友
获取好友列表
我们在登陆页面与环信的服务器建立了联系,但是由于我们执行跳转了,我们依然还需要在需要请求数据时候在当前页面再次建立连接,前面我们讲到可以通过实例化new Easemob.im.Connection()建立连接,我们这里可以在当前页面实例化建立连接,而不是使用登录时的登陆工具类。实例化new Easemob.im.Connection()的三个步骤大家可以查看前面的内容,这里需要说明的是我们获取好友列表是在conn.init方法的onOpened : function(){}; 中添加 getRoster 回调方法,从而获取好友列表。
// 创建连接很显然我们在执行后是空的,因为从开始到现在我们都是自己和自己玩,都没有找朋友,那下面我们就去找朋友,之所以先要把这个先写出来,因为这个我觉得是基本逻辑,你待会儿加了好友,怎么看,就通过这里查询,然后才能说后面的聊天。
var conn = new Easemob.im.Connection();
// 初始化连接
conn.init({
onOpened : function(){
// mui.toast("成功登录");
conn.setPresence(); //设置在线状态
conn.getRoster({
success : function(roster) {
console.log(JSON.stringify(roster))
// 获取当前登录人的好友列表
for ( var i in roster) {
var ros = roster[i]; //好友的对象
//ros.name为好友名称
}
}
});
}
});
mui.plusReady(function(){
var self = plus.webview.currentWebview().parent();
var username = self.username;
var password = self.password;
console.log("username:"+username+"password:"+password);
// 打开连接
conn.open({
user : username,
pwd : password,
appKey : Easemob.im.config.appkey
});
});[/i]
添加好友
首先我们得去邀请对方吧,那么我们得知道对方的号码吧,上面我们用的是手机号码作为用户名,为的就是保证用户ID唯一性。
邀请发起方:
我们通过执行conn.subscribe可以发起邀请,添加发起方,获取要添加好友名称,参数为:
{这里我们在头部右上角叫一个添加好友按钮:
to: user, //对方用户名
message:"加个好友呗" //对方收到的消息
}
<button id="addfriend" class="mui-btn mui-btn-blue mui-btn-link mui-pull-right">添加</button>为了简单演示,我们直接弹出一个输入对话框:
// 添加好友
mui("#addfriend")[0].addEventListener('tap',function(e){
e.detail.gesture.preventDefault();
var btnArray = ['确定','取消'];
mui.prompt('请输入你要添加的好友的用户名:', '手机号', '邀请好友', btnArray, function(e) {
if (e.index == 0) {
var user = e.value;
conn.subscribe({
to : user,
message : "加个好友呗"
});
mui.toast('邀请发送成功!');
} else {
mui.toast('你取消了发送!');
}
});
})
需要说明的是如果添加好友是一个单独的页面,或者说所在页面没有和环信建立连接,依然还有进行前面说的三步连接。
邀请接受方:
被添加方,在 con.init 方法中调用 handlePresence 回调方法。
conn.init({前面登陆注册一直很顺利,没啥问题,但是做这个请求好友的时候就出问题了,我们在发送好友请求的时候,然后切换账号登陆的时候接受不到消息。调了好久才发现一些问题:
//收到联系人订阅请求的回调方法
onPresence : function(message) {
handlePresence(message);
}
});
//easemobwebim-sdk中收到联系人订阅请求的处理方法,具体的type值所对应的值请参考xmpp协议规范
var handlePresence = function(e) {
mui.toast(JSON.stringify(e));
var user = e.from;
//(发送者希望订阅接收者的出席信息)
if (e.type == 'subscribe') {
mui.confirm('有人要添加你为好友', '添加好友', ['确定','取消'], function(e){
if (e.index == 0) {
//同意添加好友操作的实现方法
conn.subscribed({
to : user,
message : "[resp:true]"
});
mui.toast('你同意添加好友请求');
} else {
//拒绝添加好友的方法处理
conn.unsubscribed({
to : user,
message : "rejectAddFriend"
});
mui.toast('你拒绝了添加好友');
}
})
}
};
我们发送好友的消息在主界面,所以我初始化了连接,接受消息的在子页面也初始化了连接,居然有时候会有提示onflict,有两种方法:第一,主界面不做任何请求的事,点击添加好友时候,父页面给子页面发消息,然后子页面执行请求添加好友;第二,所有的初始化请求放在主界面,然后收到消息给对应的子页面发消息,为了减少请求,个人采用第二种方法。
当解决上面的冲突问题,为什么登录后收不到消息?这里有个略坑的是环信文档中查询好友时候把onOpened中的这句conn.setPresence();屏蔽了,然后就收不到消息。查文档 常见问题 中说:
登录之后需要设置在线状态,才能收到消息。请检查登录成功后是否调用过 conn.setPresence();。
加上果然没问题了。。。
剩下的功能我们主要看这个文档 初始化连接,主要是说明了初始化时候的一些回调函数的基本用法,我们这里先来看看onPresence,这个是收到联系人订阅请求的回调方法,基本数据类型如下:
{
"from":"xxxxxxxxxxx",
"to":"yyyyyyyyyyy",
"fromJid":"jszblog#musicbox_xxxxxxxxxxx@easemob.com",
"toJid":"jszblog#musicbox_yyyyyyyyyyy@easemob.com",
"type":"subscribe",
"chatroom":false,
"destroy":false,
"status":"加个好友呗"
}
这里的xxxxxxxxxxx和yyyyyyyyyyy是电话号码,以为我是用电话作为用户名的,出于隐私保护用字母代替。
当我们切换账号会发现查询好友的地方可以查到好友,下面我们就进行好友列表展示,然后就是和好友聊天咯。
5.数据绑定和本地缓存处理机制
当我们重新登录的时候打印roster时会得到下面的json对象:
[{为了考虑如果用户没有联网或者数据不能及时更新也能够正常看到历史记录,这里我们考虑做缓存,由于环信web im不具备缓存功能,所以我们这里采用本地存储作为缓存的方案,本地存储可以使用5+中的storage模块,也可以使用localStorage、sessionStorage,由于storage模块中的数据有效域不同,可在应用内跨域操作,数据存储期是持久化的,并且没有容量限制,这里我们采用这个方案,至于如果想把本案例中的例子用于浏览器端的同志,可以采用localStorage作缓存功能。
"subscription":"from",
"jid":"jszblog#musicbox_xxxxxxxxxxx@easemob.com",
"name":"xxxxxxxxxxx",
"groups":[]
}]
html5+中的storage模块比较简单,文档中介绍了几个基本方法,具体看看文档就可以学会使用,文档见 【storage】。
plus.storage.setItem(key, value);plus.storage.setItem在存储时是以key-value的形式存储,我们可以在查询到好友信息时候,将对象转换成字符串存储在本地,JSON.stringify()将json对象转换成json字符串。
plus.storage.setItem("roster",JSON.stringify(roster));
plus.storage.getItem(key);我们在子页面通过plus.storage.getItem获取存储的字符串,然后通过JSON.parse()将字符串转化成对象获取相关信息。
var roster = plus.storage.getItem("roster");我们现在要做的无非是将信息展示出来,但是这里有用的信息目前只有name,毕竟没有上传文件,所以也不存在头像、昵称、签名这种个性化信息。如何把json信息展示出来前面的文章中我们是使用直接生成dom节点或者拼接html字符串,但是这种过于繁琐,当然也有人使用【js模板引擎】,本来准备早点在文章中给一些新手介绍一下vue.js这种MV-*框架,但是考虑本文中实例的性能,暂且还是用之前用过的一个js模板引擎artTemplate,文档戳这里:https://github.com/aui/artTemplate。
var obj = JSON.parse(roster);
for(var i in obj){
console.log(obj.name);
}
artTemplate有简洁语法版和原生语法版,就是使用语法不一样而已,这里我使用简洁语法版,戳这里下载—— 下载地址
为了简单,我们采用模板中通讯录的html结构,文档中有这样的一个例子:
编写模板:
使用一个type=”text/html”的script标签存放模板:
<script id="test" type="text/html">渲染模板:
<h1>{{title}}</h1>
<ul>
{{each list as value i}}
<li>索引 {{i + 1}} :{{value}}</li>
{{/each}}
</ul>
</script>
var data = {具体语法参考这里:artTemplate 简洁版语法
title: '标签',
list: ['文艺', '博客', '摄影', '电影', '民谣', '旅行', '吉他']
};
var html = template('test', data);
document.getElementById('content').innerHTML = html;
我们可以这样写:
...我们其实可以直接先遍历找到name然后填充就ok,这为了后续
<div class="mui-content">
<!--内容-->
<ul id="roster-cnt" class="mui-table-view mui-table-view-striped mui-table-view-condensed"></ul>
</div>
<!--模板-->
<script id="roster-tpl" type="text/html">
{{each roster as value index}}
<li class="mui-table-view-cell" data-chatname="{{value.name}}">
<div class="mui-slider-cell">
<div class="oa-contact-cell mui-table">
<div class="oa-contact-avatar mui-table-cell">
<img src="http://placehold.it/60x60" />
</div>
<div class="oa-contact-content mui-table-cell">
<div class="mui-clearfix">
<h4 class="oa-contact-name">小青年</h4>
<span class="oa-contact-position mui-h6">湖北</span>
</div>
<p class="oa-contact-email mui-h6">
{{value.name}}
</p>
</div>
</div>
</div>
</li>
{{/each}}
</script>
...
mui.plusReady(function(){
var roster = plus.storage.getItem("roster");
// console.log(roster);
var data = {
roster: JSON.parse(roster)
}
var html = template('roster-tpl', data);
document.getElementById('roster-cnt').innerHTML = html;
})
方便添加昵称、地址、头像等个性化地址,直接使用artTemplate的each方法。
6.聊天消息封装
当我们完成了前面登陆、注册、添加好友等功能,我们就进行最重要的内容了,既然是聊天功能,当然要聊起来,不然就不叫IM,但是很多人一开始就太过于关注聊天这个功能,而忽略了前面的基础过程,导致对api不熟悉,自然些聊天过程也是漏洞百出,代码逻辑混乱,所以也就放弃了。本文为即时通讯第一篇,没有介绍过多原理,也没有介绍聊天过程的高级功能,仅作为新手入门的基础篇介绍,后面会再深入探究更多内容。废话不多说,我们继续看文档写下面的内容。
我们先新建一个single-chat.html,本文不打算基于html mui中的页面去构建聊天页面,打算从零开始写。
首先我们需要在刚刚那个通讯录页面里面点击进入聊天页面,将用户名的值传到聊天页面,我们可以直接在创建的时候用拓展参数传,或者预加载打开时用mui.fire(),不多说,自己参考第三小节。
我们先说说布局的问题,先上图
对应的布局详细代码如下:
<style>我们的消息分为发送和收到两种情况,上面是静态效果,我们下面需要做的事获取数据然后动态展示,现在我们先封装一下页面展示效果的代码。这里我们使用两种方法,一种是直接用js生成dom节点,这种使用于结构固定后面不需要改动的,直接用一个js function封装,每次调用一行代码就可以直接显示内容,这样想想都觉得很棒。
.chat-history-date{
display: block;
padding-top: 5px;
text-align: center;
font-size: 12px;
}
.chat-receiver,.chat-sender{
margin: 5px;
clear:both;
}
.chat-avatar img{
width: 40px;
height: 40px;
border-radius: 50%;
}
.chat-receiver .chat-avatar{
float: left;
}
.chat-sender .chat-avatar{
float: right;
}
.chat-content{
position: relative;
max-width: 60%;
min-height: 20px;
margin: 0 10px 10px 10px;
padding: 10px;
font-size:15px;
border-radius:7px;
}
.chat-content img{
width: 100%;
}
.chat-receiver .chat-content{
float: left;
color: #383838;
background-color: #f5f5f5;
}
.chat-sender .chat-content{
float:right;
color: #ffffff;
background-color: #15b5e9;
}
.chat-triangle{
position: absolute;
top:6px;
width:0px;
height:0px;
border-width:8px;
border-style:solid;
}
.chat-receiver .chat-triangle{
left:-16px;
border-color:transparent #f5f5f5 transparent transparent;
}
.chat-sender .chat-triangle{
right:-16px;
border-color:transparent transparent transparent #15b5e9;
}
</style>
<!--消息最后历史时间-->
<p class="chat-history-date">01:59</p>
<!--接收文本消息-->
<div class="chat-receiver">
<div class="chat-avatar">
<img src="../img/chat-1.png">
</div>
<div class="chat-content">
<div class="chat-triangle"></div>
<span>如果是接受消息,请使用.chat-receiver类,如果是发送消息,请使用.chat-sender,头像是.chat-avatar类,内容是.chat-content类。.chat-content下如果是span标签则为文本消息,若为img标签则为图片消息。</span>
</div>
</div>
<!--发送文本消息-->
<div class="chat-sender">
<div class="chat-avatar">
<img src="../img/chat-2.png">
</div>
<div class="chat-content">
<div class="chat-triangle"></div>
<span>如果你要修改聊天气泡的背景颜色,请修改.chat-content的background-color和.chat-triangle的border-color</span>
</div>
</div>
<!--发送图片消息-->
<div class="chat-sender">
<div class="chat-avatar">
<img src="../img/chat-2.png">
</div>
<div class="chat-content">
<div class="chat-triangle"></div>
<img src="../img/test.jpg"/>
</div>
</div>
老司机,别说话,快看代码!
/**其实后面我们拓展也很容易的,只需要不断加type类型就ok,这些都是dom操作的基本方法,如果对一些方法不熟悉,建议看看相关的内容。这里遵照JSDoc+规范还加上了使用参数提示,在hbuilder使用可以查看参数含义,再也不用担心写代码时忘记了参数含义。
* @description 显示消息
* @param {String} who 消息来源,可选参数: {params} 'sender','receiver'
* @param {Object} type 消息类型,可选参数: {params} 'text','url','img'
* @param {JSON} data 消息数据,可选参数: {params} {{el:'消息容器选择器'},{senderAvatar:'发送者头像地址'},{receiverAvatar:'接收者头像地址'},{msg:'消息内容'}}
* ('text'和'url'类型的msg是文字,img类型的msg是img地址)
*/
var appendMsg = function(who,type,data) {
// 生成节点
var domCreat = function(node){
return document.createElement(node)
};
// 基本节点
var msgItem = domCreat("div"),
avatarBox = domCreat("div"),
contentBox = domCreat("div"),
avatar = domCreat("img"),
triangle = domCreat("div");
// 头像节点
avatarBox.className="chat-avatar";
avatar.src = (who=="sender")?data.senderAvatar:data.receiverAvatar;
avatarBox.appendChild(avatar);
// 内容节点
contentBox.className="chat-content";
triangle.className="chat-triangle";
contentBox.appendChild(triangle);
// 消息类型
switch (type){
case "text":
var msgTextNode = domCreat("span");
var textnode=document.createTextNode(data.msg);
msgTextNode.appendChild(textnode);
contentBox.appendChild(msgTextNode);
break;
case "url":
var msgUrlNode = domCreat("a");
var textnode=document.createTextNode(data.msg);
if(data.indexOf('http://') < 0){
data.msg = "http://" + data.msg;
}
msgUrlNode.setAttribute("href",data.msg);
msgUrlNode.appendChild(textnode);
contentBox.appendChild(msgUrlNode);
break;
case "img":
var msgImgNode = domCreat("img");
msgImgNode.src = data.msg;
contentBox.appendChild(msgImgNode);
break;
default:
break;
}
// 节点连接
msgItem.className="chat-"+who;
msgItem.appendChild(avatarBox);
msgItem.appendChild(contentBox);
document.querySelector(data.el).appendChild(msgItem);
}
这里我们也可以用模板引擎的办法去封装,代码如下:
模板内容:
<script id="msg-tpl" type="text/html">模板渲染:
<div class="chat-{{who}}">
<div class="chat-avatar">
<img src="{{avatar}}">
</div>
<div class="chat-content">
<div class="chat-triangle"></div>
{{if type=="text"}}
<span>{{msg}}</span>
{{else if type=="url"}}
<a href="{{msg}}">{{msg}}</a>
{{else if type=="img"}}
<img src="{{msg}}"/>
{{/if}}
</div>
</div>
</script>
/**大家使用也很简单,调用方法如下:
* @description 显示消息
* @param {String} who 消息来源,可选参数: {params} 'sender','receiver'
* @param {Object} type 消息类型,可选参数: {params} 'text','url','img'
* @param {JSON} data 消息数据,可选参数: {params} {{el:'消息容器选择器'},{senderAvatar:'发送者头像地址'},{receiverAvatar:'接收者头像地址'},{msg:'消息内容'}}
* ('text'和'url'类型的msg是文字,img类型的msg是img地址)
*/
var appendMsg = function(who,type,data){
var html = template('msg-tpl', {
who: who,
type: type,
avatar: who=='sender'?data.senderAvatar:data.receiverAvatar,
msg: data.msg
});
document.querySelector(data.el).innerHTML += html;
}
appendMsg('sender','text',{如果大家觉得每次调用还要填写容器id,头像地址这种基本固定的内容很麻烦,大家也可以继续封装:
el: '#msg-list', //消息容器
senderAvatar: '../img/chat-1.png', //发送者头像
receiverAvatar: '../img/chat-2.png', //接收者头像
msg: '你好' //消息内容
})
/**调用方法很简单:
* 消息初始化
*/
var msgInit = {
el: '#msg-list', //消息容器
senderAvatar: '../img/chat-1.png', //发送者头像
receiverAvatar: '../img/chat-2.png', //接收者头像
}
/**
* @description 展示消息精简版
* @param {String} who 消息来源,可选参数: {params} 'sender','receiver'
* @param {Object} type 消息类型,可选参数: {params} 'text','url','img'
* @param {Object} msg ('text'和'url'类型的msg是文字,img类型的msg是img地址)
*/
var msgShow = function(who,type,msg){
appendMsg(who,type,{
el: msgInit.el,
senderAvatar: msgInit.senderAvatar,
receiverAvatar: msgInit.receiverAvatar,
msg: msg
});
}
msgShow('sender','text','你好');两种方法实现封装的函数一样,这里只是给大家演示一下对于这种动态结构的html的一些方法,当然只要你愿意,你可以直接用字符串拼接,或者用<template></template>标签自己做一个这样的模板引擎,或者使用使用更加方便的mvc或mvvm框架。
之所以要花大篇幅内容将这些基础内容,是因为看到很多人代码写得那叫一个混乱,如果接口啥的一改,我相信这些人会疯掉,因为代码缺乏一定的通用性,没有把变与不变的内容分别拿出来。当然我们上面其实有些东西没有封装进去,比如用户名或者昵称,这在群聊中是有必要的,这里只是以最简单的例子来说明,大家可以根据自己的业务需求自由发挥。
7.单聊之文本消息
基本思路
其实写到这里本篇基本也算告一段落,但是考虑到很多新手对于收发消息很多还是有一些问题,我们这里就还是把文本消息发送接收写完了再收篇。
上面我们我们讲了怎么把消息展示出来,但是毕竟聊起来数据是动态的,那么发送接收数据是很重要的一步,先来写发送消息。我们先定义一个底部的输入框加按钮,代码如下:
<style type="text/css">为了代码整洁规范,方便后期封装,参考hello mui中im-chat.html的写法,我们先定义一下ui控件对象:
footer {
position: fixed;
width: 100%;
height: 50px;
min-height: 50px;
border-top: solid 1px #bbb;
left: 0px;
bottom: 0px;
overflow: hidden;
padding: 0px 50px;
background-color: #fafafa;
}
.footer-left {
position: absolute;
width: 50px;
height: 50px;
left: 0px;
bottom: 0px;
text-align: center;
vertical-align: middle;
line-height: 100%;
padding: 12px 4px;
}
.footer-right {
position: absolute;
width: 50px;
height: 50px;
right: 0px;
bottom: 0px;
text-align: center;
vertical-align: middle;
line-height: 100%;
padding: 12px 5px;
display: inline-block;
}
.footer-center {
height: 100%;
padding: 5px 0px;
}
.footer-center [class*=input] {
width: 100%;
height: 100%;
border-radius: 5px;
}
.footer-center .input-text {
background: #fff;
border: solid 1px #ddd;
padding: 10px !important;
font-size: 16px !important;
line-height: 18px !important;
font-family: verdana !important;
overflow: hidden;
}
footer .mui-icon {
color: #000;
}
footer .mui-icon:active {
color: #007AFF !important;
}
.footer-right span{
color: #0062CC;
line-height: 30px;
}
</style>
<div class="mui-content">
<div id="msg-list"></div>
</div>
<footer>
<div class="footer-left">
<i id='msg-choose-img' class="mui-icon mui-icon-camera" style="font-size: 28px;"></i>
</div>
<div class="footer-center">
<textarea id='msg-text' type="text" class='input-text'></textarea>
</div>
<div class="footer-right">
<span id='msg-send-text'>发送</span>
</div>
</footer>
// UI控件对象发送文本消息很简单:
var ui = {
content: mui('.mui-content'[0]),
msgList: mui('#msg-list')[0],
footer: mui('footer')[0],
msgChooseImg: mui("#msg-choose-img")[0],
msgText: mui('#msg-text')[0],
msgSendText: mui('#msg-send-text')[0]
}
// 发送文本消息这里的msgTextFocus();和msgScrollTop();是封装的两个方法,具体的且看下文。
ui.msgSendText.addEventListener('tap',function(){
sendText();
})
// 发送文本
var sendText = function(){
var msg = ui.msgText.value.replace(new RegExp('\n', 'gm'), '<br/>');
var validateReg = /^\S+$/;
// 获得键盘焦点
msgTextFocus();
if(validateReg.test(msg)){
// 消息展示出来
msgShow('sender','text',msg);
// 发送文本消息到环信服务器
conn.sendTextMessage({
to: chatName, //用户登录名,SDK根据AppKey和domain组织jid,如easemob-demo#chatdemoui_**TEST**@easemob.com,中"to:TEST",下同
msg: msg, //文本消息
type: "chat"
//ext :{"extmsg":"extends messages"}//用户自扩展的消息内容(群聊用法相同)
});
// 清空文本框
ui.msgText.value = '';
// 恢复输入框高度(因为我们这里是50px,你可以写一个全局变量)
ui.footer.style.height = '50px';
// 保持输入状态
mui.trigger(ui.msgText, 'input', null);
// 这一句让内容滚动起来
msgScrollTop();
}else{
mui.toast("文本消息不能为空");
}
}
再来说说收消息,我们需要在conn.init()配置设置收到消息的回调函数onTextMessage:
// 初始化连接至此我们完成了基本的文本消息收发功能,但是有几个细节是需要处理的,比如我们上面说的两个函数啥意思,我们没有解释。
conn.init({
onOpened : function(){
//mui.toast("成功登录");
conn.setPresence();
},
// 收到文本消息时的回调函数
onTextMessage : function(message) {
// console.log(JSON.stringify(message));
var from = message.from;//消息的发送者
var msg = message.data;//文本消息体
//mui.toast(msg);
// 收到文本消息在页面展示
msgShow('receiver','text',msg);
msgScrollTop();
},
// 收到图片消息时的回调函数
onPictureMessage : function(message) {
handlePictureMessage(message);
}
});
获得输入框焦点事件和强制弹出软键盘
我们如果不做处理,在输入框失去焦点时软键盘会自动收回软键盘,这样很影响聊天时候的用户体验。这个时候我们可以在输入完内容,准备发送时,保持输入状态mui.trigger(ui.msgText, 'input', null);。
让输入框获得焦点的方法:
// 获得输入框键盘焦点强制弹出软键盘的方法:
var msgTextFocus = function(){
ui.msgText.focus();
setTimeout(function() {
ui.msgText.focus();
}, 150);
}
// 强制弹出软键盘聊天消息高度调整
var showKeyboard = function() {
if (mui.os.ios) {
var webView = plus.webview.currentWebview().nativeInstanceObject();
webView.plusCallMethod({
"setKeyboardDisplayRequiresUserAction": false
});
} else if(mui.os.android) {
var Context = plus.android.importClass("android.content.Context");
var InputMethodManager = plus.android.importClass("android.view.inputmethod.InputMethodManager");
var main = plus.android.runtimeMainActivity();
var imm = main.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.toggleSoftInput(0,InputMethodManager.SHOW_FORCED);
}
};
聊天消息如何发送或者收到一条自己往上滚动呢?我们看qq消息就是最后一条消息就会自动出现在输入框之上,调整方法是使用scrollTop方法,通过计算scrollHeight和`offsetHeight的高度,实现调整。对这些高度不理解?看这里:
HTML 获取屏幕、浏览器、页面的高度宽度
深入理解高度。获取屏幕、webview、软键盘高度
其实这个地方有很多技术细节,比如消息高度虽然可以获取,但是要实现局部滚动,那么必须禁止浏览器默认的滚动模式,具体可以看看这篇文章的实现原理浅议内滚动布局
具体css样式设置方法:
html,调用的函数封装如下:
body {
height: 100%;
margin: 0px;
padding: 0px;
overflow: hidden;
-webkit-touch-callout: none;
-webkit-user-select: none;
}
.mui-content{
height: 100%;
padding: 44px 0px 50px 0px;
overflow: auto;
background-color: #eaeaea;
}
#msg-list {
height: 100%;
overflow: auto;
-webkit-overflow-scrolling: touch;
}
// 消息滚动输入框高度如何自适应
var msgScrollTop = function(){
ui.msgList.scrollTop = ui.msgList.scrollHeight + ui.msgList.offsetHeight;
}
不多说直接上代码:
// 输入框监听事件解决长按导致致键盘关闭的问题
ui.msgText.addEventListener('input', function(event) {
msgTextFocus();
ui.footer.style.height = this.scrollHeight + 'px';
});
// 解决长按“发送”按钮,导致键盘关闭的问题;
ui.msgSendText.addEventListener('touchstart', function(event) {
msgTextFocus();
event.preventDefault();
});
ui.msgSendText.addEventListener('touchmove', function(event) {
msgTextFocus();
event.preventDefault();
});
当做到这里我们基本要讲解的够新手去理解了,但是对于项目功能实现来说,远远不够,毕竟只是文字发送接收,那么图片、语音、地址等等高级功能呢,我们这篇文章限于篇幅不可能一一道来,只能后面再做补充。这里希望更多人参与到其中进行贡献。这里可以放出地址了,详情代码请关注这里:https://github.com/zhaomenghuan/mui-demo/tree/master/easemobIM。后期功能拓展和bug修复都贵提交到这里,欢迎大家贡献。
写在后面
由于这段时间确实有点忙,这篇文章也花了很多时间去码字,去修改,改了很多次,才有这篇文章,希望能够给新手一些启示和帮助吧!本文不是着重讲环信sdk怎么用,而是讲解这个过程中可能会遇到的一些问题和实现思路,所以不建议新手直接拿最后的代码改之类的,还是看懂了思路再说,所以至于这个IM更多的功能后期会不会继续开发,暂时是未知数,所以大家不要等待,欢迎大神多多贡献分享相关代码,这样方便更多人学习使用。
这里想和大家简单说下,不要所有的问题直接私聊我,我时间精力有限,个人觉得不回复不好,所以我不会看到消息装着没看到,但是也不可能一一去回复,毕竟时间精力上也有限,我也需要不断学习,所以不希望过多的被打扰。大家有问题建议去论坛先搜搜答案,看看官方文档自己解决,大家确实有解决不了的问题,可以在群里寻求帮助或者给我发消息。提问前建议把问题描述清楚,想要实现什么,然后现在的实现思路,最好附上代码说明或者发测试文件,这样也方便解决问题。很多人说直接是直接是根据官方demo改的,说啥bug
,很多时候聊到最后发现是他自己的原因,这种情况真的很浪费彼此时间。在此声明以后一上来直接要帮忙写代码的,或者让我发一遍文档地址的等等这种可以自己可以解决的问题,请原谅我直接果断拒绝,我理解新手理解小白初入门的盲目,但是建议不要去依赖,要自己去尝试,不懂再问,不要还没有去了解去查资料,直接一上来求带这种要求。
如果有项目需求,欢迎私聊。承接各种前端项目,同时如果有功能定制,代码优化等需求也可以商量,算发个小广告吧,毕竟我也要生活,要挣钱娶媳妇养家糊口。
写文章不容易,也许写这些代码就几十分钟的事,写一篇大家好接受的文章或许需要几天的酝酿,然后加上几天的码字,累并快乐着。如果文章对您有帮助请我喝杯咖啡吧!进行赞助的同学私信留下你们的联系方式,后期发文章会单独邮件通知,有开发的问题也可以私聊,有相关功能需求,可以考虑优先写文分享。在此特别感谢之前给予赞助的同学,名单有保存,后期在博客会有公示。
如果觉得觉得本文不错在github上给我点个赞呗!
https://github.com/zhaomenghuan/mui-demo/tree/master/example/easemobIM
作者:环信开发者小青年 收起阅读 »
pika新特性支持codis slot迁移
已经有越来越多的公司在线上使用pika. pika兼容redis绝大数接口,所以之前版本已经支持作为codis的server,替换redis,但是由于不支持codis的slot迁移命令,所以不能在线动态扩容。
现在pika的codis分支已经支持codis的slot迁移,目前slot 迁移已经在环信线上稳定运行, 每天承载数亿的访问量, 这个大feature 由环信云乾同学开发测试完成.
环信即时通讯云使用案例
背景介绍
pika在环信的即时通讯云平台系统中使用,用来存储消息内容和日志,目前在部分集群中数据量已经达到数TB,QPS在数十万级别。这些数据最早存储在mysql里面,但mysql性能有限,而且扩容不方便。我们评估了下codis,能解决我们在mysql里面面临的性能及扩容问题,在一段时间用codis存储这些数据。但随着数据量级增长,TB级别的数据存储在内存里面,成本过于昂贵。后来了解到pika数据存储在磁盘里面,而且兼容redis协议及命令,可以挂载在codis proxy后面做server,这样既保留codis proxy的高性能,又能解决TB级数据量的存储成本问题。但pika还不支持codis的slot迁移,当压力上涨时,扩容是个问题。通过研究codis及pika的实现,在pika上开发实现了codis的slot迁移。目前已在生产环境中使用,运行良好,考虑到该功能能帮助更多的人解决一些实际的问题,我们经过再三测试验证后向官方提交了PR
线上数据
支持slot迁移版本的pika已经在环信的生产环境中使用,获得了一些线上环境的数据:
- 当pika容量在500G时,全量同步一次的时间大概在7个小时左右(千M网卡)
- 运行slotsreload命令每秒钟处理key的数据在万级,所以上面针对过期数据使用支持slot迁移的方式是可行的,已经过线上环境验证。
- 在进行slot迁移时,每秒钟迁移的key数量在200~300之间,当数据量大时,迁移需要花费很长时间,但迁移期间不影响线上业务正常使用。
- codis多连接版本可以提升性能2~3倍,完全释放pika的性能
# slotmigrate [yes | no]slotmigrate : yes
部署成功后即可进行使用,和官方的codis没有任何不同,一个大的可以扩容的redis。当codis-server(pika)压力过大需要扩容的使用,按照官方的迁移方法进行slot迁移即可。带来好处 - codis-server集群可以在不停机的情况下进行水平扩容
- dbsize命令可以实时获取当前数据库(pika)中key的数量
$ redis-cli -h 127.0.0.1 -p 9221 config set slotmigrate yes$ redis-cli -h 127.0.0.1 -p 9221 config set slotmigrate no当关闭开关时,pika不支持slot迁移,和之前版本的pika没有区别(dbsize也不能实时获取);当打开开关时,支持slot迁移(dbsize能实时获取),但需要有以下几点注意: [list=1]
127.0.0.1:9221> slotsreload
$ 127.0.0.1:9221> slotsdel 1013 990额外开销如上面所说,pika支持slot迁移会使用更多一些的磁盘,性能上会有一些下降。如果对磁盘使用或者性能有很高的要求,则可以按照上面3针对过期数据的使用方式进行使用。同名不同类型的keypika支持同名的key有kv,hash,list,set, zset等5种类型,但如果要使用pika支持slot迁移,不要使用同名但不同类型的key,如不要:set test1 100后再lpush test1 a b c,产生同名不同类型的key。在支持slot迁移的pika中使用同名但不同类型的key,迁移的时候会丢失那些同名的key。codis多连接codis 2.0及之前版本proxy和后端server是使用的单连接,当后端server是redis时,这个性能还是很不错的,但当后面是pika、ssdb等磁盘数据库时,单连接严重限制了后端server的性能,需要让codis支持多连接,针对多连接这个问题的讨论见:https://github.com/CodisLabs/codis/pull/1007该讨论提供了两种解决方法: [list=1]
# Proxy connections number model with backend server: server/slot, server means only one connection between proxy and backend server, # slot means every slot has one connection between proxy and backend server, default is serverbackend_connection_model=server
总结
pika支持codis slot迁移版本带来了一些好处,如动态水平扩容,dbsize实时获取,也带来了一些开销,如磁盘和性能。但它提供了一个开关,一个供你在这之间可以进行权衡的开关;它同时为pika的水平扩展提供了一个选择,一个当你的数据量快速增长带来问题时无痛解决问题的选择。
有任何问题可以在pika讨论群(294254078)交流,也可以在官方github提issue,欢迎试用
本文作者:环信性能工程师张云乾 收起阅读 »
Android ios V3.2.3 SDK 已发布,SDK十余项更新,更加简洁易用,新增广告红包
Android V3.2.3 2016-12-29
新功能/优化:
- sdk提供aar及gradle方式集成,具体方法查看gradle方式导入aar
- 增加离线推送设置的相关接口,具体方法可查看EMPushManager API文档
- 为了使sdk更简洁易用,修改以及过时了一些api,具体修改查看3.2.3api修改,另外过时的api后续3-5个版本会进行删除
- 优化loadAllConversationsFromDB()方法,从联表查询改为从两个表分别查询,解决在个别乐视手机上执行很慢的问题
- 优化登录模块,减少登录失败的概率
- 鉴于市面上的手机基本都是armeabi-v7a及以上的架构,从这版本开始不再提供普通的armeabi架构的so,减少打包时app的体积
- 小额随机红包
- 增加广告红包(需要使用请单独联系商务)
- 商户后台增加广告红包配置、统计功能
- 商户后台增加修改密码功能
- 绑卡后的用户验证四要素改为验证二要素
- 发红包等页面增加点击空白区域收回键盘的功能
- 群成员列表索引增加常用姓氏以及汉字的支持
- 红包详情页领取人列表展示不全
- 华为P8手机密码框无法获取焦点
- 部分银行卡号输入正确,提示银行卡号不正确
- 红包祝福语有换行符显示不正确
- 修复Emoji表情显示乱码
- 修复商户自主配置红包最低限额错误
- 修复零钱明细显示顺序错误问题
- 新增:实时1v1音视频,设置了对方不在线发送离线推送的前提下,当对方不在线时返回回调,以便于用户自定义离线消息推送
- 更新:SDK支持bitcode
- 更新:SDK使用动态库
- 为了使SDK更简洁易用,过时的API会在后续3~5个版本进行删除
- 小额随机红包
- 商户后台增加修改密码功能
- 绑卡后的用户验证四要素改为验证二要素
- iOS和Android两端UI展示一致性
- 支付流程的优化
- SDK注册流程
- 去掉XIB
- 集成过程的参数检查
- 风险策略
- SDKToken注册失败的问题
- 发红包缺少参数的问题
- 修复Emoji表情显示乱码
- 修复支付密码可能误报出错
- 修复商户自主配置红包最低限额错误
- 修复零钱明细显示顺序错误问题
- 修改抢红包流程为依赖后端数据
- 修复支行信息返回为空时的文案
版本历史:Android SDK更新日志 ios SDK更新日志
下载地址:SDK下载 收起阅读 »
个推开发者说第一期:论前端技术实践之道
个推技术开放日第一期,以「前端」为主题,围绕前端开发实践,力邀3位技术大牛就前端的设计、开发、测试,以及跨平台等进行多维度分享和探讨。我们拒绝华而不实的背景和光环,让技术分享回归实践,只为开发者们带来纯粹的技术分享会。
「开发者说」是由个推主办,联合业内开发大牛致力于为开发者打造高品质分享会的系列技术沙龙。
活动议程
13:30-14:00 签到
14:00-14:30 主题分享《前端开发背后的力量—前端构建之路》
14:30-14:40 Q&A
14:40-15:10 主题分享《移动互联网产品中如何用好HTML5 》
15:10-15:20 Q&A
15:20-15:50 主题分享《JavaScript之面向对象那些事儿》
15:50-16:00 Q&A
16:00-16:30 自由交流
活动群二维码
合作和报名,随时联系:美玉,18600532085;Jack,17710330365
嘉宾介绍
姜季廷 个推前端首席架构师
资深前端工程师,开源项目活跃贡献者。曾做过全端开发,发起并完成“人才储备池”O2O平台,“同心网”等项目。拥有多年ASP,PHP,Angular JS,Node.js开发经验。2013年开始全面投入到 Angular JS 的研究中,目前全面负责个推前端云组件设计及Node JS中间衔接模式的研究与实践以及前端技术团队管理。
李德兴 APICloud技术负责人
APICloud终端引擎及相关核心模块的技术实现者。一直从事浏览器、JavaScript引擎及相关中间件技术,参与过多个基于浏览器的移动中间件引擎的开发工作。有丰富的App架构及开发经验。热爱新技术,熟悉HTML5及W3C规范,对HTML5及浏览器技术的实际应用有深入研究并付诸大量实践。
刘溟川码易CBO
北京航空航天大学硕士,精通java、c、c#、c++、html语言及平台开发语言,曾牵头完成奔驰、戴尔、搜狐等软件开发项目,后因业务需求转型商务合作,主要负责为企业提供权威、优质、高效的售前、项目管理及售后服务。
主办方
个推
个推是基于大数据的推送技术服务商,为App开发者提供推送技术服务,帮助有效提升产品活跃度,增加用户留存率。截止2016年11月,个推SDK累计接入安装量达128亿,覆盖独立终端16亿。以推送技术为基础,个推同时发展了大数据和移动营销业务,目前已成为基于大数据的移动互联网综合服务提供商。
联合主办
太库
太库科技创业发展有限公司作为一家专注于孵化器运营管理和科技创业企业培育的专业机构,致力于成为全球领先的创业生态系统的生力军。目前,太库在全球主要创新城市北京、上海、深圳、河北、硅谷、首尔、特拉维夫、德国等地建立孵化培育体系,帮助全球太库会员快速成长。太库将与全球的产、学、政、金、研 等领域的创新要素紧密合作,全程陪伴企业从创业第一步到每一步,真正帮助企业在创业者国度快速成长。
合作伙伴
APICloud
APICloud现有APP开发平台、APP定制平台两大业务满足企业的APP开发需求。APP开发平台简化移动应用开发技术,大幅降低APP开发周期和成本,帮助开发者快速实现APP的开发、测试、发布、管理和运营,目前开发平台上已累计创建应用50多万个。APP定制平台旨在为企业提供快速高效、高性价比的App定制服务。官方签约、保上线、便宜、快,四大亮点给企业客户带来可靠的承诺。
码易
码易是为开发者提供个人任务众包、微猎头、技术分享交流的平台。同时码易也为需求客户提供高质量的软件产品研发、软件技术孵化服务。码易现已为世界500强、政府、企事业机构和优秀创业公司在内的数百家客户,完成了千余个项目的高质量交付。
环信
环信成立于2013年4月,是一家企业级服务软件提供商,并于2016年荣膺“Gartner 2016 Cool Vendor”。产品包括国内上线最早规模最大的即时通讯云平台——环信即时通讯云,以及移动端最佳实践的全媒体智能云客服平台——环信移动客服。
特别支持
报名链接:http://www.huodongxing.com/event/7367567171200 收起阅读 »
环信获“2016最佳企业服务商”大奖,SaaS客服将迎来春天
环信荣获猎云网“2016年度最佳企业服务商”大奖
2015年作为中国企业级服务的元年,受益于互联网+国家战略的推动,人口红利消失以及消费升级的大市场背景下,包括CRM、SaaS客服、HR、协同办公等企业级服务赛道百花齐放百家争鸣,其中诞生了包括阿里钉钉、纷享销客、销售易、北森、环信等一批明星公司,融资金额屡创新高。2016年企业级服务在所谓“资本寒冬”的情况下,仍凸显为创投热点,资本的青睐从侧面印证了行业高速发展机会的到来。2016年全球范围内,根据网络公开数据显示,共计619起企业级服务获得融资,其中402起总融资额为1469亿元。数据显示,美国投资机构有40%的钱投在ToB的公司上,中国目前只有10%左右,因此国内企业级服务领域的冬天远未到来。
在企业级服务市场,SaaS是目前最火的一个领域,显然,资本对于这个市场的预估远远超过现在的空间,随着企业级SaaS服务市场的火爆,包括腾讯、阿里、网易等互联网巨头都已经进入,但相对于细分垂直领域BAT的流量和技术优势并不明显,对于创业公司而言,机会空间还将非常大,SaaS客服领域亟待爆发!
从行业细分来看:“客服”是CRM四大细分市场之一。而且,从2015年的数据显示,客服是CRM细分领域中最大的一个,占市场总额的37%。在北美,2015年客服软件市场采购总额高达96亿美元。这个市场中已经出现了数家“独角兽”SaaS公司包括Salesforce、Zendesk和Freshdesk。
从政策来看:SaaS业务需要IDC托管服务,对这一块国家有明确的政策监管,国外巨头企业进入中国市场有高政策门槛。同时,他们很难支撑在中国的落地,实事上Zendesk的中国客户整体使用体验因为访问缓慢问题而变得糟糕。所以中国企业有足够的时间和空间野蛮生长。
从用户体验来看:随着移动互联时代带来,社交媒体的渗透率不断上升,用户随时、随地、跨平台接触商家咨询获取服务不再有软硬件障碍,客服咨询也不再局限于售后支持,售前售中的咨询比例在稳步上升。随着消费升级,用户从价格敏感上升到品牌服务和用户体验敏感,有数据显示:1、71%的顾客因糟糕的客户服务而停止合作。2、坏的服务体验后,48%的客户会劝说他人不要购买。3、高收入人士和85-90后更容易对坏的体验耿耿于怀。4、吸引新顾客的成本是维系老客户的5倍。良好的客户体验越来越重要,“客户是上帝”正在被重新定义和加强。
从技术趋势来看:1、移动端的特性支持任何人、任何时间、任何地点都可以瞬间连接在线客服。环信移动客服在移动端领域的大中型客户以及超高的市场占有率就完美证明了市场的刚需。2、客户咨询在包括企业官网、APP、社交媒体、400电话等渠道蜂拥而至,环信全媒体客服接入技术可以支持商家一个工作台一键回复来自所有渠道咨询。3、由新技术、新行业和新消费习惯引爆的海量客服咨询需要新的技术手段解决,环信智能客服机器人将能够帮助解答80%的常见问题,极大节省成本提高效率。4、环信大数据技术全景用户画像深度挖掘用户需求,轻松玩转反向营销。5,国内首款基于人工智能和大数据挖掘的客户旅程透析产品"环信客户声音"能够帮助企业优化运营,提高跨渠道客服体验。
综上所述,SaaS客服的春天即将到来,你已经准备好了么? 收起阅读 »
软件测试员工作经验分享
1、手机app的测试:当前主要为android和ios两大阵营了。针对不同平台的功能、自动化框架、兼容性等都有很大的差别。而当今移动互联网的快速发展让app的测试人员需求大大的增加,同时应运而生的是类似于TestBird这样的第三方测试机构。
2、web测试:不仅仅是网站,包括web服务器的测试都属于web的测试;在移动互联网兴起前,这块是主流,不过现在该领域的需求还是很大的。
3、游戏测试:这里包括手游和网游。只要有游戏的地方就有游戏测试,这块的需求就不用怀疑了,对于喜欢游戏的朋友们来说,是一个不错的选择。
4、传统软件测试:这块在在移动互联网兴起前也属于主流测试,现在当然也占据这不小的需求,毕竟还是有很大一部分人是用电脑的。
5、网络设备测试:这块的测试应该主要是面向企业客户的,如路由器,防火墙等等。以华为、中兴为代表的企业部分业务都是在这块。
6、云产品的测试:这块主要说指云存储和云计算,典型的如现在的阿里云。应该来说,这方面的测试大部分还属于初级阶段,对于测试的需求还是比较大的。
小编再强调下,选择一个领域还是很重要的,建议自己能够选择一个打算长期发展的领域,这个对自己的业务积累以及职业发展都是很有帮助的(当然,这里并没有说哪个行业不好,还是看个人的兴趣)。
确定行业后,大家会看到涉及到的测试类型都差不多(比如:功能测试、性能测试、自动化测试等等)。一般情况下会先安排功能测试相关的任务,这个对熟悉业务很有帮助的(而且大部分的人员还一直在做这方面的测试),接着会再根据个人的擅长和意愿去安排性能测试或自动化测试。然后自己在某个领域去持续发展,成为该行业的测试专家或者走向管理岗位。
根据上图我们将测试领域的技术岗位归纳下吧!
app测试专家,自动化测试专家,测试开发工程师,性能测试专家,web测试专家,web性能测试专家,安全测试专家,游戏测试专家,网络软件业务测试专家,网络设备业务测试专家,云产品的测试专家(云计算、大数据分析、网络存储、服务器等等)。 收起阅读 »
IOS 2.2.9设置https only遇到无法登录的解决方案
SDK的老版本(版本范围为SDK2.1.5-2.2.3)中存在默认不使用https的设置。部分使用了范围内SDK版本的用户在升级到最新的SDK2.2.9时,设置https only的选项后会出现用户无法正常登录的问题。
这个问题可以升级到SDK2.2.9及以上版本,在SDK初始化时添加otherConfig:@{kSDKConfigUseHttps:@YES}的设置,具体的代码实现如下:
[[EaseMob sharedInstance] registerSDKWithAppKey:@"easemob-demo#chatdemoui" apnsCertName:@"chatdemoui" otherConfig:@{kSDKConfigUseHttps:@YES}];
上记代码仅未示例,具体在使用时需要将appkey等信息替换为自己的对应信息就可以了。
集成过程中遇到问题欢迎在IMGeek社区发帖咨询。 收起阅读 »
SaaS颠覆软件世界已成全球化现象
文章摘要:SaaS 初创公司已经遍及越来越多的国家,其全球化达到了怎样的水平呢?
澳大利亚、加拿大、以色列、中国、印度,SaaS 初创公司已经遍及越来越多的国家。下一代软件公司将来自于世界不同地区,其中一些或许价值数十亿美元,比如加拿大的 Shopify 和 Hootsuite、澳大利亚的 Atlassian、新西兰的 Xer。随着这些成功的初创公司的迅速发展,其早期筹资市场又是如何演变的呢?
根据 Crunchbase 数据,2010 年以来,美国的 SaaS 投资从每年 15 亿美元迅速增长到 70 亿美元,2016 年略有下降,环境或许是一个原因,此外这个结果也并非涵盖了所有数据。同一时期,非美国 SaaS 投资则增长了 6.3 倍,这对于 SaaS 创始人来说是一个非常振奋人心的数据。
分别来看不同国家的数据,可以发现在美国以外存在一定不稳定性,但是长期趋势是,在以上八个国家:巴西、加拿大、德国、西班牙、法国、应该、印度、以色列,SAAS 投资都呈增长态势。
A 轮投资数额中值随地域变化。在巴西,这个数值是 100 万美元,同时,以色列超过六美元,法国和德国情况也都类似。加拿大的 SaaS 环境从稳定性和投资周期上来看与美国最相似。产生波动的原因有很多,不同地区筹资环境不同,术语也有天壤之别。但是一定程度上,我们可以预期,数值会聚集。无论这家 SaaS 初创公司的名字是什么,在什么地区。
通过投资数额来观察这些数据可以发现,几乎每个地区的长期增长都有着更加振奋人心的趋势。再次声明,2016 年的数据并未全部包含。毫无疑问,SaaS 是一个全球趋势。
关于国际 SaaS 初创公司,令人激动的部分应该是它们能够识别到对自己地区来说最独特的机会,而目前还没有软件公司抓住了这些机会。有时这些独特的方法和解决方案可以发展到全球规模。
对于软件公司来说,这只是一个起步,2016 对于创始人来说是伟大的一年,融资环境越来越能满足他们的需求。美国依然是软件公司最大的筹资市场。
但是,我们可以看到越来越多的国际 SaaS 公司在自己国家内部进行首轮融资,之后在到美国进行融资。建立一个以美国为目标的进入市场团队,就能接触到更广阔的融资生态系统。
所以,SaaS 的确是一个全球现象,在很长一段时间里会继续颠覆软件世界。
原文作者:Tomasz Tunguz
翻译:徐婧欣 收起阅读 »
数人云1月Meetup上海|容器之Mesos/K8S/Swarm三国演义
Mesos/K8S/Swarm集群管理工具在容器生态圈里帮助企业客户排兵布阵,驰骋疆场,
呈现出三国鼎立的局面;
16年10月份北京场活动中IBM、新浪微博、Acttao、数人云的大牛们各抒己见(文末推荐4篇活动后期整理的文章),
在即将到来的17年,数人云Meetup重新起航啦,
1月7日上海、深圳"三国演义" 活动两城联动,
相信不一样的技术实践,
有着同样的精彩分享~
—— 文臣武将 ——
罗勇,携程云平台开发经理
主要负责携程云平台建设和维护,熟悉OpenStack,Docker ,Linux/Windows Container 等技术领域。
《Windows Container在携程的应用》
主要分享Linux/Windows Container在携程的应用场景,
Windows Container技术介绍包括:
传统应用的容器化迁移,容器网络模型选择、
与OpenStack/Mesos集成的取舍、监控方案等,
Windows Container在落地过程中我们遇到和解决的问题,
对未来的展望等。
王成昌,唯品会PaaS平台高级开发工程师
主要工作内容包括:平台DevOps方案流程优化,持续部署,平台日志收集,Docker以及Kubernetes研究。
《唯品会PaaS基于kubernetes的实践》
PaaS构建部署流程、架构;
基于kubernetes的网络方案定制;
PaaS日志收集及监控方案。
梁晓聪,哔哩哔哩弹幕网运维开发经理
曾就职猎豹移动任运维开发负责人。目前主要负责B站运维与运维技术栈建设,对于服务端微服务docker化,持续交付,集中式配置管理领域具有丰富经验。
《B站基于Mesos的弹性计算资源探索》
主要分享B站在2016年基于Mesos和Docker实现弹性计算资源的探索之路
我们的场景
解决什么问题
弹性计算资源探索的6个细节
未来的展望
谢乐冰,数人云COO
在德国工作十年,回国后加盟惠普电信运营商部门。 拥有多年项目经验和创业公司工作经验。在数人科技负责互联网数据搜集和处理,擅长技术应用领域,为电商、招聘、电信、互联网金融等行业提供服务。
《一款基于Mesos的分布式系统应用开发手记》
gPRC使用手记;
将Raft集成到Mesos调度器Swan中;
Actor模式和事件驱动。
—— 排兵布阵 ——
13:30 - 14:00 签到/微软嘉宾发言
14:00 - 14:40 《Windows Container在携程的应用》@携程
14:40 - 15:20 《一款基于Mesos的分布式系统应用开发手记》谢乐冰@数人云
15:20 - 16:00 《B站基于Mesos的弹性计算资源探索》梁晓聪@哔哩哔哩
16:00 - 16:40 《唯品会PaaS基于kubernetes的实践》王成昌@唯品会
16:40 - 17:00 自由交流
—— 安营扎寨 ——
时间:1月7日 14:00 - 17:00
地点:上海市虹桥路3号港汇中心2座10层
微软(中国)上海分公司
主办方/数人云
联合主办/微软
报名链接:http://www.huodongxing.com/event/8365715940700 收起阅读 »
数人云1月深圳Meetup|容器之Mesos/K8S/Swarm三国演义
容器正在成为企业级应用的新一代交付标准,
Mesos/K8S/Swarm集群管理工具在容器生态圈里帮助企业客户排兵布阵,驰骋疆场,
呈现出三国鼎立的局面;
16年10月份北京场活动中IBM、新浪微博、Acttao、数人云的大牛们各抒己见(文末推荐4篇活动后期整理的文章),
在即将到来的17年,数人云Meetup重新起航,
1月7日上海、深圳"三国演义" 活动两城联动,
相信不一样的技术实践,
有着同样的精彩分享
数人云1月Meetup上海站|容器之Mesos/K8S/Swarm三国演义
—— 文臣武将 ——
黄惠波,腾讯互娱高级工程师
目前主要负责游戏计算资源容器化平台的研发工作,包括kubernetes/docker研究以及定制化开发,主导腾讯游戏万级容器资源调度平台的建设工作。
《kubernetes在腾讯游戏的应用实践》
主要分享海量在线游戏场景下,基于kubernetes的容器资源调度的探索和应用实践。包括:
腾讯在线游戏的容器化应用场景;
基于kubernetes的调度方案和网络方案定制优化;
海量应用过程中遇到的问题与解决方案。
黄浩松,Apache Mesos PMC
目前就职于Shopee(东南亚社交电商平台),负责公司自动化运维平台建设,业余时间长期在Apache Hadoop / Apache HBase / Tensorflow 打酱油。
《Mesos Unified Containerizer及对Pod的支持》
Mesos Unified Containerizer的最新特性解读
王璞,数人云创始人&CEO
美国 George Mason 大学计算机博士。曾先后供职于 Google、Groupon 和 StumbleUpon 等硅谷互联网公司。擅长分布式计算、大规模机器学习、海量数据处理。曾担任 Google 广告部门数据平台构架师,负责管理每秒访问量全球最高的架构平台。
《SRE-分布式系统运维的DevOps实践》
来自Google的SRE理念
SRE与DevOps
SRE落地实践-开源Mesos调度器
神秘嘉宾,敬请期待~~
——排兵布阵——
13:30 - 14:00 签到
14:00 - 14:40 《SRE-分布式系统运维的DevOps实践》@王璞
14:40 - 15:20 《Mesos Unified Containerizer及对Pod的支持》@黄浩松
15:20 - 16:00 《kubernetes在腾讯游戏的应用实践》@黄惠波
16:00 - 16:40 神秘嘉宾
16:40 - 17:00 自由交流
——安营扎寨——
时间:1月7日 14:00 - 17:00
地点:深圳市南山区科园路18号北科大厦4002室
报名链接:http://www.huodongxing.com/event/4366729880600 收起阅读 »
IMGeek社区赞赏功能临时下线通知,请于12月31日前完成提现
根据中国人民银行制定的《非金融机构支付服务管理办法》,规定未经中国人民银行批准,任何非金融机构和个人不得从事或变相从事支付业务。
IMGeek作为移动开发者技术社区,接到了上级有关部门整改,根据第三方支付政策法规,不具备支付牌照,IMGeek配合停掉赞助功能使用,下次上线时间待通知。请各位小伙伴们于本周内(2016年12月31号前)将收到的赞赏提现到自己账户,逾期赞赏功能关闭,做清零处理。
社区赞赏提现教程提现教程
Thx! 收起阅读 »
环信React Native Demo发布,支持 JavaScript 和 React Native 开发
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 已完成功能:Android
• 登录
• 注册
• 好友
◦ 列表及筛选
◦ 好友信息展示
◦ 黑名单
◦ 删除好友
◦ 好友通知
◾ 添加好友通知展示
◾ 接受好友请求
◾ 拒绝好友请求
◾ 添加好友
• 群组
◦ 群组列表
◦ 群组成员列表
• 聊天
◦ 相机图片消息
◦ 本地图片消息
◦ emoji消息
◦ 普通消息
• 异常状态处理
◦ 断线退出到登录页
◦ 重复登录退出到登录页
Android Demo 已完成功能:目录结构
• 登录
• 注册
App 的目录结构如下:Redux State
• 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
{版本历史 :更新日志
// 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下载:点击下载 收起阅读 »
Web IM V1.4.7已发布,优化手机浏览器后台重连
新功能:
[demo] 在demo.html中新增视频聊天及发送视频文件的功能
Bug修复:
[sdk] 解决在手机浏览器在后台运行时无法断线重连的问题
[demo] WebIM建群,等待后台建群成功后再拉取群信息并更新UI中的群列表
[demo] WebIM群加人,群主和被添加的群成员均可以收到通知
[demo] WebIM群主将群成员从黑名单移除后,不再回到群成员列表中,而直接被删除
webim体验:https://webim.easemob.com/
版本历史:更新日志
SDK下载:点击下载 收起阅读 »
李理:自动梯度求解——使用自动求导实现多层神经网络
作者:李理
目前就职于环信,即时通讯云平台和全媒体智能客服平台,在环信从事智能客服和智能机器人相关工作,致力于用深度学习来提高智能机器人的性能。
相关文章:
李理:从Image Caption Generation理解深度学习(part I)
李理:从Image Caption Generation理解深度学习(part II)
李理:从Image Caption Generation理解深度学习(part III)
李理:自动梯度求解 反向传播算法的另外一种视角
李理:自动梯度求解——cs231n的notes
接下来介绍一种非常重要的神经网络——卷积神经网络。这种神经网络在计算机视觉领域取得了重大的成功,而且在自然语言处理等其它领域也有很好的应用。深度学习受到大家的关注很大一个原因就是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得到的分类准确率是97.8%。这是在test_data上的准确率,这个模型使用训练数据训练,并根据validation_data来选择当前最好的模型。使用validation数据来可以避免过拟合。读者运行时可能结果会有一些差异,因为模型的参数是随机初始化的。
$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)
改进版本1
我们首先在输入的后面增加一个卷积层。我们使用5 5的局部感知域,stride等于1,20个特征映射。然后接一个2 2的max-pooling层。之后接一个全连接的层,最后是softmax(仿射变换加softmax):
在这种网络结构中,我们可以认为卷积和池化层可以学会输入图片的局部的空间特征,而全连接的层整合全局的信息,学习出更抽象的特征。这是卷积神经网络的常见结构。
下面是代码:
>>> net = Network([【注意图片的大小,开始是(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】
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)
这个模型得到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([【注意图片的大小,开始是(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)。然后是全连接的层】
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)
这个模型得到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使用ReLU后准确率从99.06%提高到99.23%。从作者的经验来看,ReLU总是要比sigmoid激活函数要好。
>>> 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就比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(这个模型的准确率是99.37%。扩展数据看起来非常trival,但是却极大的提高了识别准确率。
"../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)
改进版本5
接下来还有改进的办法吗?我们的全连接层只有100个神经元,增加神经元有帮助吗? 作者尝试了300和1000个神经元的全连接层,得到了99.46%和99.43%的准确率。相对于99.37%并没有本质的提高。
那再加一个全连接的层有帮助吗?我们了尝试一下:
>>> net = Network([在第一个全连接的层之后有加了一个100个神经元的全连接层。得到的准确率是99.43%,把这一层的神经元个数从100增加到300个和1000个得到的准确率是99.48 %和99.47%。有一些提高但是也不明显。
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)
为什么增加更多层提高不多呢,按说它的表达能力变强了,可能的原因是过拟合。那怎么解决过拟合呢?一种方法就是dropout。drop的详细解释请参考这里。简单来说,dropout就是在训练的时候随机的让一些神经元的激活“丢失”,这样网络就能学到更加鲁棒的特征,因为它要求某些神经元”失效“的情况下网络仍然能工作,因此就不会那么依赖某一些神经元,而是每个神经元都有贡献。
下面是在两个全连接层都加入50%的dropout:
>>> net = Network([使用dropout后,我们得到了99.60%的一个模型。
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)
这里有两点值得注意:
- 训练的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。敬请关注。 收起阅读 »
从被动客服到主动营销 环信荣获“杰出新零售客服服务商”大奖
环信荣获“杰出新零售客服服务商”大奖
过去,以阿里、京东等巨头为代表的电商企业在包括消费习惯的形成、商业配套(支付、物流等)的完善、零售平台的规范等一系列做出了巨大贡献。然而巨大的光环下,也隐藏了危机:流量越来越贵、营销成本越来越高,小企业的生存空间被打压,整个电商好像只是巨头的游戏。中国零售行业亟需出现新物种、新规则、新电商,而环信移动客服作为SaaS客服领域的破局者,凭借其在大中小型电商领域的深耕细作以及对新一代SaaS客服技术的推动,一举夺得“杰出新零售客服服务商”大奖。
同时,在新服务·思路服务分论坛上环信VP程旭文发表了《从被动客服到主动营销》的主题演讲,他分享了包括国美在线、楚楚街、金融界等很多环信客户在营销领域的应用。如何根据用户的购买前后的数据做营销投放,客服产品怎样帮助企业使得这个营销环节做到闭环。环信在过去的实践当中会基于一个核心的点,就是讲“长连接”技术,指你时时刻刻和你的潜在客户、和你的目标客户、付费客户保持一条长连接通道,使得你随时可以找到你的客户,可以随时触达他们。
环信VP程旭文主题演讲《从被动客服到主动营销》
以下是演讲实录:
程旭文:我也是接到一个命题作文,其实环信在很长时间内我们并没有单独去做营销领域,更多的在做SaaS软件,我们主要做两个产品:
第一,即时通讯云。第二,全媒体智能客服。
以上两个都是IT领域做在线软件交付。
我们一不小心进入了营销行业,我们可以基于环信的基础,包括服务10万多家APP企业,服务了超过5万家的有客服场景的APP和企业。
我们怎么进入到营销服务领域?在这个过程当中做了什么样的实践?
(PPT图示)这张图是我们每一个企业要面对的,你覆盖的客户群体人数、客单价多少,可以算出整个生意有多大。这个沉淀用户是指已经触达到用户,或者注册了网站,或者订阅了你的微信公众号等等这些用户。还有一部分是你的付费用户,对于电商企业来讲很重要的两个环节是怎样降低你的获取成本,怎样降低用户转化成本,我们做了很多的实践,我们接触了很多客户,我们发现在这个环节可以给用户带来很多价值。
(PPT图示)如果拿一张图把整个营销获取用户到转化客户变成付费用户的过程当中,上面部分是获取成本,下面两个图是关乎转化率。用户的角度看上去接触的点是营销环节和客服环节,进而往后走就是一些落地点,现在大部分电商说落地点放在淘系,很多电商说有自己的网站、APP,这些都是企业商品的落地点,再往后走就是企业客户关系系统和物流、企业资源管理、ERP等等。
今天王詠谈了关于企业大数据、关于电商大数据方面的话题,其实我感触特别深刻,用户很多行为,包括购买前的数据、购买后的数据,或多或少在企业内都有管理系统。通过这一套系统怎样做营销投放,再回到针对用户环节的营销、客服怎样使得这个环节可以做到闭环。
环信在过去的实践当中我们会基于一个核心的点,就是讲“长连接”技术,指你时时刻刻和你的潜在客户、和你的目标客户、付费客户保持一条长连接通道,使得你随时可以找到你的客户,可以随时触达他们。
从三个维度讲:
第一,长连接相对传统客服可以随时触达你的客户,改变被动的接受用户询问的现状。第二,相对于传统的消息推送,长连接使得你与客户的互动是双向的、及时的。第三,结合自己的数据管理平台、环信的管理平台,可以使得你精准的挑选客户。
举几个例子:
第一,国美在线,国美在线有上亿的客户,他们客服系统用的是环信的,营销环节有什么机制可以随时找到他们、随时联系他们,随时和他们互动,他们的反馈可以让我们知道。
比如说用户,国美APP放在后台或者锁屏状态,我们可以做到哪怕这个后台或者锁屏状态客服的坐席依然根据一定的规则发放消息给国美所有客户和部分客户,使得我们消息可以及时的触达,用户一点击基本上上做到客服状态了,后台可以监测哪些用户点了这个消息,有多少点了消息,有多少回复消息,可以做精准统计,用户行为洞察可以比单向消息推送更加细致。
第二,楚楚街,他们号称“小淘宝”定向推动2000套儿童座椅给3-4岁儿童的妈妈,我们给楚楚街做了客户管理系统,通过这些找到目标群体,通过基于长连接可以时时刻刻和用户保持长连接的客服系统,将一些非常具有价值的营销消息给APP用户,只要APP没有被卸载,用户就可以看到我们消息,及时产生互动。
第三,在金融界,互联网金融在大的范畴类也属于在线卖东西,只不过交付形式略有不同。
一是为电话销售配置一个APP的主动营销的平台,当用户上线及时的推送,用户一上APP及时的推送用户行为,之前的历史行为、购买习惯给客户经理。
二是结合用户分析推荐相关产品和用户密切互动。一些新的客户可以进入销售公海池,让新的销售抢单,立刻和客户产生互动。
再举一个例子,楚楚街利用“回呼”功能降低退换货率,我们有的时候发现这个客户下单了,但是有点问题,比如说地址不对等等,我们审核的时候立刻发现这个单子有问题,通过“回呼”找到客户,减少因为小事情碰到一些退货情况。
综合来讲,我们之前并没有主动做这方面,但是我们发现通过两个产品结合已经做了很多精准化营销方面的东西,我们与客户保持了长连接,这个长连接是即时通讯云的产品。在淘系、在腾讯,我们都离不开他们,因为他们与客户保持了密切联系,你们依然可以构建自己的APP,通过环信的即时通讯与用户保持长连接,业界当中除了旺旺、除了微信、除了陌陌第四大和用户保持长连接的平台就是环信。
我的演讲到此结束,如果大家需要交流可以来我们展台继续沟通。谢谢大家! 收起阅读 »
李理:自动梯度求解——cs231n的notes
作者:李理
目前就职于环信,即时通讯云平台和全媒体智能客服平台,在环信从事智能客服和智能机器人相关工作,致力于用深度学习来提高智能机器人的性能。
相关文章:
环信李理:从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上面的例子用了一个小技巧,就是所谓的staged backpropagation,说白了就是给中间的计算节点起一个名字。比如dot。为了让大家熟悉这种技巧,下面有一个例子。
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 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需要注意的两点:1. 前向的结果都要保存下来,反向的时候要用的。2. 如果某个变量有多个出去的边,第一次是等于,第二次就是+=,因为我们要把不同出去点的梯度加起来。
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
下面我们来逐行分析反向计算:
(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至此,本系列文章的第5部分告一段落。在接下来的文章中,作者将为大家详细讲述关于常见的深度学习框架/工具的使用方法、使用自动求导来实现多层神经网络等内容,敬请期待。 收起阅读 »
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)
环信VP程旭文:从被动客服到主动营销
[思路网注]程旭文分享了包括国美在线、楚楚街、金融界等很多环信客户在营销领域的应用。怎样做营销投放,再回到针对用户环节的营销、客服怎样使得这个环节可以做到闭环。
【亿邦动力网讯】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,通过环信的即时通讯与用户保持长连接,业界当中除了旺旺、除了微信、除了陌陌第四大和用户保持长连接的平台就是环信。
我的演讲到此结束,如果大家需要交流可以来我们展台继续沟通。谢谢大家! 收起阅读 »
游戏测试与软件测试的区别!
游戏本质也是软件的一种,所以从测试工程的角度来讲,游戏测试与软件测试的本质是完全相同的。2者的不同更多的是在表象层面或者流程方面,我们可以把游戏测试看作软件测试的子类,它继承了软件测试这个父类的特性,又有自己的一些新特性。
笔者通过归纳总结,把游戏测试相对软件测试的不同归纳为以下几点:
1. UI&&UE
2. 数值
3. 活动
4. 进度
5. 工具
6. 性能
7. 安全
8. 合服(针对网游)
9. 交互
10. 网络
下面我们就每一点来详细探讨下。
1、UI&&UE。相对来讲UI&&UE在游戏和软件测试中,重要性并非很高,但它们确是用户和测试人员最直观感受的部分,也最受“非专业人士”的关注,游戏行业尤甚。对大部分软件来说,UI&&UE的重要性没有游戏那么高,毕竟软件使用过程愉悦感和趣味性并非是重要的事情,我们日常使用各种各样的软件时肯定深有体会,大部分情况是用软件来完成一项任务,能完成就好了,在使用过程中很难体会到上面说的愉悦感和趣味性。而游戏则不然,在玩游戏的过程中,愉悦感和趣味性是至关重要的,如果缺失了这些要素,用户可能瞬间就流失了,也就意味着这款游戏失败了。这好比高层小户型和海景别墅,虽然都能满足居住需求,但给人的感觉是完全不同的。
2、数值。数值对游戏而言是至关重要的,无论是单机游戏还是网络游戏,玩家非常重视自己角色的数值增长,任何差错都可能导致用户的抱怨甚至流失。另一个层面是游戏的功能之间的耦合度非常高,数值之间有着千丝万缕的关联。所以测试的过程中需要关注每个数值变化带来的各种影响。而软件功能之间的耦合度则没有这么高,很多情况下功能之间的数值是相对独立的。而且软件的用户很多时候并不关注内部的数值,能完成所需即可,细微的差错甚至都没人关心。举个例子,比如很多显示开机速度的软件,在用户打开电脑时会提示用户开机速度击败了百分之多少的其它用户,至于是20%还是25%,可能对用户而言没什么太大的差别。而游戏则不然,比如一个角色的战斗力是1000,下次登陆变成999,仅仅是1的差距,玩家可能就会愤怒的打客服电话质问了。
3、活动。很多软件也经常搞活动,笔者经常遇到某邮箱或某论坛搞活动送积分之类的,但是在游戏中,活动则是频度更高的一种玩法。所以测试过程中可能受到的关注度更高一些,尤其是网络游戏。游戏活动的测试更关注时间与资源产出,如开启时间,关闭时间,资源产出概率等。因为一个活动的开启和关闭及产出都已经提前公告给玩家,如果出了任何差错,都会导致玩家不满。而且一个活动完毕后可能紧接另一个活动,任何差错都可能导致更大的损失。而软件上的活动则没这么严格的概念。
4、进度。在软件开发和测试过程中,延期是非常普遍的情况。很多软件测试人员的时间观念也没那么强。游戏则是非常不同的,由于游戏的**倾向,所以其产业链涉及很多前期的市场推广,各种广告和推广活动都是真金白银砸下去的,任何延期可能都会导致前期的推广功亏一篑及商业上的信誉,这些损失都是不可接受的。所以游戏测试作为产品发布前的最后一环,必须严格控制版本进度,确保能够按期交付。
5、工具。游戏测试依赖更多的测试工具,因为用户的数值和角色状态千差万别,为了尽量模拟用户状态,测试过程中总需要造出各色各样的测试数据,而制造这些数据,则需要测试工具的帮助。另一个层面是游戏测试还需要对测试工具本身的正确性进行测试,确保工具本身是正确的。这点在传统软件测试行业则是不多见的。
6、性能。性能测试对游戏而言也是至关重要的一点,无论在台式机还是移动设备上,任何游戏的卡顿都会让玩家产生厌恶感。游戏测试过程中比较重视的是客户端的内存和cpu的使用率,确保游戏能够流畅的运行。对网络游戏而言,服务端的性能也十分重要,一款良好的网游,需要服务器能够稳定持久的运行。而且我们也希望大部分用户都能玩我们的游戏,而用户的设备则差异性很大,尤其是移动设备。所以我们必须确保客户端的性能符合我们的预期标准,以使更多的玩家能够玩我们的游戏。软件则没太多这方面的需求。
7、安全。安全对软件和游戏而言都十分重要。但是对游戏而言,则是关乎身家性命的事情,很多游戏都死于外挂横行。而且游戏的客户端与服务端的交互非常频繁,数据安全更加凸显。所以测试的时候更加关注安全方面的测试。有资源产出的地方则有安全测试的地方。防刷防外挂,是游戏测试人员始终要保持谨慎认真的对待的地方。
8、合服。这个可能是游戏的独有特色。有时候服务器中用户便少,为了带给玩家更好的游戏体验,需要合并几组服务器为1组。在合服的过程中需要保证原有服务器和目标服务器中所有用户的数据信息不发生错乱。涉及到用户方方面面的数据信息,复杂度也比较高,所以也许要测试人员认真的测试。确保测试无误后,才能正式开始合服操作。
9、交互。更多的时候是相对网络游戏而言,网游中很大程度的乐趣都来源于玩家与玩家之间的交互。这一特性在传统软件(此处请忽略各种社交软件)中并不多见。玩家交互的越频繁,则意味着数据之间交互的程度越高,数据之间的复杂变换及相互影响需要我们时刻关注。
10、网络。网络对于网络游戏是必不可少的,游戏的实时交互性比较高,游戏过程中突然断网的痛苦是难以忍受的。所以对网络的测试要求也比较高,因为不同用户用的网络运营商可能不同,不同地区的网络信号也不同,甚至移动过程中会出现不同网络之间的切换,这些都是需要我们去认真测试的。这样才能尽量保证不同网络条件下用户的体验达到最佳。
想要高效的完成app功能测试,就需要选择一款合适的功能测试工具。尽管现阶段存在少数不采用任何功能测试工具,从事功能测试外包项目的软件服务企业。短期来看,这类企业盈利状况尚可,但长久来看,它们极有可能被自动化程度较高的软件服务企业取代。
TestBird - 手游和App自动化测试平台 收起阅读 »
环信移动客服v5.5.1更新:新增客户资料自定义
支持查看客服同事的真实姓名
客服与同事在移动客服系统交流时,可以查看对方的真实姓名,更利于同事间沟通。支持以下两种场景:
- 与同事聊天时,将鼠标放在“客服同事”列表中同事的昵称上,可以查看该同事的真实姓名;
- 转接会话时,将鼠标放在“转接会话”对话框中同事的昵称上,可以查看该同事的真实姓名。
注意:客服可以在客服模式下“客服信息”页面设置自己的名字(真实姓名);管理员可以在管理员模式下“成员管理 > 客服”页面设置其他客服的真实姓名。
支持查看待接入会话详情
在待接入页面,点击任意一条会话,可以查看该会话的消息详情。
前提条件:管理员进入“管理员模式 > 设置 > 系统开关”页面,打开“客服查看待接入详情”开关。
租户下待接入会话数上限
新增待接入会话数上限,每个租户允许的最大待接入会话数为1000,如果某个租户下坐席数超过5个,则该租户的最大待接入会话数为坐席数x200。
待接入会话数超过上限后,不允许访客创建新的会话,当访客试图接入时提示,系统繁忙无法接入。
为避免访客无法接入的情况,当租户的待接入会话数即将达到上限时,系统向消息中心发送通知提醒管理员;当租户的待接入会话数已达到上限时,系统会再次向消息中心发送通知提醒管理员及时处理。
支持客服主动发起会话
在待接入页面,客服可以查看正在访问网站的访客列表,并主动发起会话。发起会话后,会话进入客服的进行中会话列表,客服可以主动与访客聊天。
该功能为增值服务,如需开通,请联系环信商务经理。开通后,在网页访客端进行配置eventCollector为true即可使用。关于详细配置方法,请参考网页渠道集成。
呼叫中心支持电话转接和呼叫保持
呼叫中心支持电话转接和呼叫等待功能。在通话过程中,如果电话需要转接,可以点击转接按钮 [转接] ,将电话转接给呼叫中心客服同事;如果有其他操作处理,需要暂停通话,可以点击保持按钮 [保持] ,将通话置为“保持中”状态,完成操作后,可以手动恢复通话。
呼叫中心功能为增值服务,如需开通,请联系环信商务经理。
管理员模式
优化机器人开关设置
优化机器人开关的“工作时间设置”,支持为机器人设置不同的工作场景:
- 全天接会话:访客发起会话时,由机器人接待。
- 上班时间客服全忙以及下班时间接会话:在上班时间,访客发起会话时,如果客服全忙,会话由机器人接待;在下班时间,访客发起会话时,由机器人接待。
- 仅下班时间接会话:在下班时间,访客发起会话时,由机器人接待。
机器人回答不了时,访客可以选择转人工客服。转人工后,如果有空闲客服则自动调度,如果没有空闲客服,则会话进入待接入,客服可以手动接入会话。
注意:该版本更新前的“仅下班时间接会话”与更新后的“上班时间客服全忙以及下班时间接会话”功能一致。如果您之前选择了“仅下班时间接会话”,更新后默认选择的是“上班时间客服全忙以及下班时间接会话”,您可以根据您的需要调整机器人开关。
优化上下班时间设置
“设置 > 系统开关”页面的“上下班时间”设置更名为“工作时间设置”,“会话结束语”和“下班提示语”上移至“工作时间设置”之前,原有数据保持不变。“工作时间设置”支持为周一至周日设置单独的上下班时间,以适应不同的工作时间场景:
- 为工作日(周一至周五)和周末(周六、周日)设置不同的上下班时间段(如下图)
- 为工作日的上午、下午设置不同的上下班时间段
- 以星期为周期,自定义每天的上下班时间
进入“设置 > 系统开关”页面,在“工作时间设置”区域,点击“添加新的工作时间”,设置新的工作时间段。
历史会话支持分配功能
管理员可以将历史会话重新分配给客服或技能组,分配后,生成一个新的会话。
- 分配给客服时,新会话直接进入客服的进行中会话列表。
- 分配给技能组时,如果技能组内有空闲客服,新会话进入空闲客服的进行中会话列表;如果技能组内客服全忙,新会话进入该技能组的待接入会话列表。
进入管理员模式,选择“历史会话”,点击会话右侧的转接按钮可以对该会话进行分配。
问候语中增加访客昵称
新增在问候语中增加访客昵称的功能,提升亲密度。问候语包含企业问候语、客服问候语、技能组问候语,在这三种问候语中均可以设置。设置方式为,在问候语中添加特殊字符和默认称呼(##亲##)。当访客昵称有效时,显示访客昵称;当访客昵称无效时,显示默认称呼(亲)。默认称呼可以自定义。
例如,设置企业问候语为“##亲##,您好,很高兴为您服务!”
- 当访客昵称有效时(访客昵称和ID不一致),假设访客昵称为Jon,该访客收到的问候语为“Jon,您好,很高兴为您服务!”
- 当访客昵称无效时(访客昵称和ID一致),访客收到的问候语为“亲,您好,很高兴为您服务!”
客户资料自定义
新增“客户资料自定义”功能,允许管理员设置在系统中显示哪些客户资料,包括系统字段和自定义字段,并对这些字段进行排序。设置后,新的字段列表和顺序将显示在客服模式下“会话”、“历史会话”和“客户中心”等页面的“资料”页签,以及管理员模式下“客户中心”、“历史会话”、“当前会话”等页面的“资料”页签。
进入“设置 > 客户资料自定义”页面对客户资料进行自定义,步骤如下:
1. 添加自定义字段。点击“添加自定义字段”按钮,在对话框中输入字段名称,选择字段格式,并进行相应设置,点击“保存”按钮。重复该步骤,可添加多个自定义字段。
自定义字段默认对坐席可见,当关闭“坐席可见”开关时,在客服模式下不显示该字段。
2. 设置字段是否显示,以及在“资料”页签的排列顺序。在“字段开关”一列,勾选 [勾选] 需要显示的字段,取消勾选 [取消勾选] 不需要显示的字段。点击字段后面的排序按钮 [排序] ,可将该字段的顺序上移一位。
示例,根据下图的设置,“资料”页签将只显示:昵称、名字、ID、微信号、微博账号、描述。
允许客服查看待接入会话详情
新增“客服查看待接入详情”功能,在客服模式的待接入页面,点击任意一条会话,可以查看该会话的历史消息。进入“设置 > 系统开关”页面,打开“客服查看待接入详情”开关。该开关默认关闭。
待接入超时提醒
新增“待接入超时提醒”功能,当访客进入待接入排队超过一定时间后,系统将自动发送消息提示访客。进入“设置 > 系统开关”页面,打开“待接入超时提醒”开关,并设置超时提示语、排队超时提醒时间及提醒次数。该开关默认关闭。
- 当提醒次数设置为1次时,访客在待接入排队时长达到“排队超时提醒时间”时,系统发送“超时提示语”给访客;
- 当提醒次数设置为多次时(例如3次),访客在待接入排队时长达到“排队超时提醒时间”时,系统发送“超时提示语”给访客,之后,当每次达到“排队提醒间隔”设定的时长,系统再次发送“超时提示语”给访客,直到会话被客服接起,或达到“提醒次数”。
待接入超时结束会话
新增“待接入超时结束会话”功能,当访客排队时长达到设定数值时,仍然没有被客服接入,会话将被自动结束。进入“设置 > 系统开关”页面,打开“待接入超时结束会话”开关,并设置超时提示语、超时时间及会话标签和备注。该开关默认关闭。
支持转人工指定技能组
支持为机器人设置转人工指定技能组。为了不影响现有会话路由规则,默认情况下不指定。
如果没有开通多机器人功能,分两种场景:
- 场景一:使用默认配置(不指定)。会话经过默认机器人转接人工客服时,按照原有会话路由规则分配给对应的技能组或客服。
- 场景二:设置转人工指定某个技能组。会话经过默认机器人转接人工客服时,都转给指定的技能组。
如果开通了多机器人功能,建议配置如下:
- 为每个新创建的机器人指定不同的技能组。这样,会话经过机器人转接人工客服时,将分配给指定的技能组,从而实现机器人绑定技能组功能。
进入“智能机器人 > 机器人设置”页面,选择一个机器人,再次选择“自动回复 > 转人工设置”页签,为该机器人选择“转人工指定技能组”。
机器人问答优化功能
新增机器人问答优化功能,系统自动收集机器人未能匹配的重复出现的访客消息,并以列表的形式显示。您可以将这些访客消息添加到知识规则中,并设置对应的答案,提高机器人回答的匹配率和准确性。
进入“智能机器人 > 机器人设置 > 问答优化”页面,点击任意一条未匹配问句后的加号(+),可以将该问句添加到知识规则,支持三种方式:
- 添加到推荐知识规则:如果存在相似度高的知识规则,系统会将其展示为“推荐知识规则”,选择该知识规则,并点击“添加”按钮即可;
- 将问句添加到现有知识规则:选择“将问句添加到现有知识规则”,系统展示现有知识规则列表,您可以选择并添加该问句到任一知识规则;
- 创建新知识规则并添加此问句:选择“创建新知识规则并添加此问句”,系统自动为该问句创建一条知识规则,请您手动为该问句添加对应答案(如果知识规则中没有答案,机器人会回复空消息)。
机器人问答优化功能为增值服务,如需开通,请联系环信商务经理。
新增“删除坐席”事件
自定义事件推送功能新增“删除坐席”事件,当坐席被删除时,可将相关信息以回调方式自动推送到其他系统。进入“设置 > 自定义事件推送”页面,点击“创建事件推送”,填写自定义事件名称、接收事件的服务器地址,勾选需要推送的事件,并保存。
自定义事件推送功能为增值服务,如需开通,请联系环信商务经理。
客户之声支持手动添加关注的关键词
优化客户之声的“热门关键词”,支持查看一段时间内的热门关键词,并且支持手动添加您关注的业务相关的关键词。
进入客户之声页面,在“热门关键词”区域的右上角可以选择时间范围;点击“关注词设置”,可以添加您关注的关键词,更新的关注词将于第二天生效,并在词云中高亮显示。
客户之声功能为增值服务,如需开通,请联系环信商务经理。
环信移动客服更新日志http://docs.easemob.com/cs/releasenote/5.6
环信移动客服登陆地址http://kefu.easemob.com/ 收起阅读 »
没想到你是这样的环信!!!
一封来自环信小伙伴的来信,没想到你是这样任劳任怨,兢兢业业,恪尽职守的环信!
致环信:
在自如客服项目的支持过程中,贵公司的孔令莹兢兢业业、不辞劳苦的工作态度令人由衷赞佩,必须提笔赞扬一下,为工作上能有这样值得信赖的合作伙伴感到欣慰和放心。
我们客服系统是早9:00-22:00在线提供支持,自如客是O2O的业务应用模式,我们的客服需要及时解决来自租客和业主的各种问题,涉及多个渠道接入:4个微信号,1个APP,1个门户。每个渠道的接入情况都不一样,关联系统复杂,在这样的业务背景下,很考验环信客服系统无停歇稳定运行能力,以及项目支持人员的应急处理能力。我们要特别感谢孔令莹,从支持自如客服系统以来,不论是工作日还是节假日,我们任何时候提出的问题,她都会快速响应帮我们排查协调。令人印象深刻的是,有很多次都是由于我们内部系统原因造成,但孔令莹依旧会和我们一起帮助客服同学定位原因,直到客服系统正常恢复使用。还记得在国庆节和双十一期间,我问到环信是否会安排轮班值守人员时,孔令莹的答复是遇到问题随时联系她,会及时为我们解决问题。这样的态度让我们倍感欣慰,她用行动诠释了她的承诺,这样的工作精神令我们感动。
为她以客户为先的工作精神而点赞,希望贵司能对这样优秀的员工给予表扬,特发此邮件表达诚挚的谢意。
自如服务产品部
2016-11-30
收起阅读 »
比较简单的解决android 3.2.2 昵称和头像的问题
- 第一步:本地新建一张环信用户表,字段有id(app本地的userId)、昵称、头像url,至于用什么数据库自己决定;
- 第二步:写上环信用户表的query、insert和update操作;
- 第三步:在EMClient.getInstance().login()的onSuccess()方法内使用insert插入自己的userId、昵称和头像url数据到数据表去,当然,如果已经插入过就update,要始终保持数据是最新的,因为你自己的可能会改变昵称和头像;
- 第四步:在进入环信的 ChatActivity.class 之前跟第三步一样保存你要聊天的对象的userId、昵称和头像url,同样的也是每次都要保存一下,防止聊天对象的昵称和头像不是最新的;
- 第五步:如果基于官方demo的 在DemoHeler.class的getUseriInfo()方法里改为如下:
private EaseUser getUserInfo(String username) {
EaseUser user = null;
UserInfoForEM userInfoForEM = DBUtil.findUserInfoForEM(username);
if (null != userInfoForEM) {
user = new EaseUser(username);
user.setAvatar(userInfoForEM.getUserHeadPhoto());
user.setNick(userInfoForEM.getNickName());
}
return user;
}
如果没有DemoHeler类,那么就在下面这个方法处理:
EaseUI.getInstance().setUserProfileProvider(new EaseUI.EaseUserProfileProvider() {
@Override
public EaseUser getUser(String username) {
EaseUser user = null;
UserInfoForEM userInfoForEM = DBUtil.findUserInfoForEM(username);
if (null != userInfoForEM) {
user = new EaseUser(username);
user.setAvatar(userInfoForEM.getUserHeadPhoto());
user.setNick(userInfoForEM.getNickName());
}
return user;
}
});
解释一下,UserInfoForEM 是我自己的环信用户表实体类,如果userInfoForEM=null,聊天昵称就会显示注册环信的id。
好了,完了,是不是很简单,根本不要考虑什么服务器获取,也不用考虑什么附加字段,也不会去考虑非得收到一条信息才能显示。要说明一下我这是基于官方SDK3.2.2版本以及官方demo基础上弄的, 希望对大家有帮助! 收起阅读 »
APP自动化测试框架
1. Instrumentation
Instrumentation,早期Google提供的Android自动化测试工具类。它和Activity有点类似,但其没有界面,通过将主程序和测试程序运行在同一个进程中,在程序运行期间,模拟按键按下、抬起、屏幕点击、滚动、屏幕长按等事件,监控主程序的工具类。缺点是受到Android进程安全限制不可跨App,对测试人员的编程能力要求较高,需要对AndroidManifest.xml文件进行配置。
2. Uiautomator
Uiautomator,也是Android提供的自动化测试框架,基本上支持所有的Android事件操作。与Instrumentation不同的是,测试代码和被测应用程序分别运行在不同的进程内,相互独立,可以跨多个App。缺点是不支持WebView,不支持获取toast文本,只适用于SDK level 16(Android 4.1)及以上。
3. Robotium
基于Instrumentation实现,提供的接口可以满足大部分自动化需求,使用方法简单,支持Activities、Dialogs、Toasts、Menus、Context Menus和其他Android SDK控件。缺点是测试人员需了解Android组件相关知识,同样不可跨App。
4. Monkey
Monkey是Android中的一个命令行工具,可以运行在模拟器里或实际设备中。它向系统发送伪随机的用户事件流(如按键输入、触摸屏输入、手势输入等),实现对正在开发的应用程序进行压力测试。Monkey测试是一种为了测试软件的稳定性、健壮性的快速有效的方法。
5. MonkeyRunner
Monkeyrunner工具提供了一个API,使用此API写出的程序可以在Android代码之外控制Android设备和模拟器。Monkeyrunner工具的主要设计目的是用于测试功能/框架水平上的应用程序和设备,或用于运行单元测试套件。
6. MonkeyTalk
MonkeyTalk是Gorilla Logic的一款开源的支持录制回放并跨平台的自动化工具。支持iOS 和 Android,它可以为应用进行真实的,功能性交互测试。它提供简单的 “smoke tests”,复杂数据驱动的测试套件。MonkeyTalk 支持原生,移动和混合应用,真实设备或者模拟器。MonkeyTalk 使得场景捕获非常容易,可以记录高级别,可读的测试脚本。可以真实测试用户行为,用户交互如触摸、手指滚动、长按等,还支持HTML5的一些特性,比如本地存储、session存储、应用缓存等。缺点是需要应用源码。
7. Appium
Appium是最近比较热门的框架,支持IOS、Android和FirefoxOS平台的UI测试,支持WebDriver兼容的任何语言编写测试脚本,Android SDK Level在16及以上时,底层使用的UIAutomator,低于16使用Selendroid。
自动化框架种类多多,在选取框架时,除了需要适合做UI测试外,还需要具备以下几点特性:工具开源,易于扩展; 脚本编写简洁,维护成本低;满足Android客户端的自动化需求;便与校验结果的正确性;可用于持续集成。
想要高效的完成app功能测试,就需要选择一款合适的功能测试工具。尽管现阶段存在少数不采用任何功能测试工具,从事功能测试外包项目的软件服务企业。短期来看,这类企业盈利状况尚可,但长久来看,它们极有可能被自动化程度较高的软件服务企业取代。
TestBird - 手游和App自动化测试平台 收起阅读 »
李理:自动梯度求解 反向传播算法的另外一种视角
本系列文章面向深度学习研发者,希望通过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得到的分类准确率是97.8%。这是在test_data上的准确率,这个模型使用训练数据训练,并根据validation_data来选择当前最好的模型。使用validation数据来可以避免过拟合。读者运行时可能结果会有一些差异,因为模型的参数是随机初始化的。
$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)
改进版本1
我们首先在输入的后面增加一个卷积层。我们使用5 5的局部感知域,stride等于1,20个特征映射。然后接一个2 2的max-pooling层。之后接一个全连接的层,最后是softmax(仿射变换加softmax):
在这种网络结构中,我们可以认为卷积和池化层可以学会输入图片的局部的空间特征,而全连接的层整合全局的信息,学习出更抽象的特征。这是卷积神经网络的常见结构。
下面是代码:
>>> net = Network([【注意图片的大小,开始是(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】
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)
这个模型得到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([【注意图片的大小,开始是(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)。然后是全连接的层】
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)
这个模型得到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使用ReLU后准确率从99.06%提高到99.23%。从作者的经验来看,ReLU总是要比sigmoid激活函数要好。
>>> 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就比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(这个模型的准确率是99.37%。扩展数据看起来非常trival,但是却极大的提高了识别准确率。
"../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)
改进版本5
接下来还有改进的办法吗?我们的全连接层只有100个神经元,增加神经元有帮助吗? 作者尝试了300和1000个神经元的全连接层,得到了99.46%和99.43%的准确率。相对于99.37%并没有本质的提高。
那再加一个全连接的层有帮助吗?我们了尝试一下:
>>> net = Network([在第一个全连接的层之后有加了一个100个神经元的全连接层。得到的准确率是99.43%,把这一层的神经元个数从100增加到300个和1000个得到的准确率是99.48 %和99.47%。有一些提高但是也不明显。
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)
为什么增加更多层提高不多呢,按说它的表达能力变强了,可能的原因是过拟合。那怎么解决过拟合呢?一种方法就是dropout。drop的详细解释请参考这里。简单来说,dropout就是在训练的时候随机的让一些神经元的激活“丢失”,这样网络就能学到更加鲁棒的特征,因为它要求某些神经元”失效“的情况下网络仍然能工作,因此就不会那么依赖某一些神经元,而是每个神经元都有贡献。
下面是在两个全连接层都加入50%的dropout:
>>> net = Network([使用dropout后,我们得到了99.60%的一个模型。
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)
这里有两点值得注意:
- 训练的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理解深度学习(part III)
作者:李理,目前就职于环信,即时通讯云平台和全媒体智能客服平台,在环信从事智能客服和智能机器人相关工作,致力于用深度学习来提高智能机器人的性能。
相关文章:
从Image Caption Generation理解深度学习(part I)
从Image Caption Generation理解深度学习(part II)
2.2.5 反向传播算法的推导
前面我们用很简单的几十行python代码基本上完成了一个多层神经网络。但是还差最重要的部分,那就是计算loss function对参数的偏导数,也就是反向传播算法。下面我们来仔细的完成公式的推导,以及接下来会讲怎么用代码来实现。这一部分数学公式多一些,可能很多读者会希望跳过去,不过我还是建议大家仔细的阅读,其实神经网络用到的数学相比svm,bayes network等机器学习算法,已经非常简单了。请读者阅读的时候最好准备一支笔和几张白纸,每一个公式都能推导一下。如果坚持下来,你会觉得其实挺简单的。
(1) feedforward阶段的矩阵参数表示和计算
之前我们讨论的是一个神经元的计算,而在代码里用到的却是矩阵向量乘法。而且细心的读者会发现我们在构造参数矩阵weights的时候,行数和列数分别是后一层的节点数和前一层的节点数。这似乎有点不自然,为什么不反过来呢?看过下面这一部分就会明白了。
首先我们熟悉一下第L(因为小写的L和1太像,所以我用大写的L)层的参数w_jk。它表示第L-1层的第k个神经元到第L层的第j个神经元的权重。比如第3层的w_24,参考上面的图,它表示的是第2层的第4个神经元到第3层的第二个神经元。
对bias和激活函数后的结果a也采用类似的记号,如下图所示。
b_32表示第2层的第3个神经元的bias,而a_13第3层的第1个神经元的激活。
使用上面的记号,我们就可以计算第L层的第j个神经元的输出a_jl:
第L层的第j个神经元的输入是L-1层的a_1,a_2,...;对应的权值是w_j1,w_j2,...;bias是b_jL。所以a_jL就是上面的公式,k的范围是从1到第L-1层的神经元的个数。
为了用矩阵向量乘法来一次计算第L层的所有神经元的输出,我们需要定义第L层的参数矩阵w_l,它的大小是m*n,其中m是第L层的神经元个数;而n则是第L-1层的个数。它的第i行第j列就是我们上面定义的w_jk。此外我们还要定义向量b_l,它的大小是m(也就是第L层神经元的个数),它的第j个元素就是我们上面定义的b_j。
最后,我们定义element-wise的函数,比如f(x) = x^2,如果输入是一个向量,那么结果是和输入一样大小的向量,它的每个元素是对输入向量的每一个元素应用这个函数的结果。
有了上面的定义,我们就可以一次计算出第L层的输出(一个长度为m的向量)
下面是对上面这个公式的详细证明(说明):
我们需要证明的是向量aL的第j个元素就是前面的a_jL
此外,为了方便后面的求解,我们把加权累加和也用一个符号z_l来表示。
其中,它的第j个元素就是第L层的第j个神经元的加权累加和:
这样a_l就可以简单的对z_l的每个元素计算激活函数
现在我们再回顾一下feedforward的代码就非常直观了:
def feedforward(self, a):传给函数feedforward的参数a就是输入向量x,第一层就是x,第二层就是第一个隐层,每一层的计算就是非常简单的参数矩阵w_l乘以上一层的激活a_l-1在加上b_l,然后用激活函数计算。
"""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
初始化的时候w的大小是 (后一层的神经元个数) * (前一层的神经元个数),再回顾一下初始化参数的代码:
# sizes = [784, 30, 10]x, y in zip(sizes[:-1], sizes[1:]) x是第一层到最后倒数第二层,y是第二层到最后一层,比如上面的sizes=[784, 30, 10]
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:])]
x是[784, 30], y是[30, 10],注意随机的矩阵是(y,x),所以self.weights是两个矩阵,大小分别是30*784和10*30
(2) 关于损失函数C的两个假设
1. 损失函数是每个训练数据的损失的平均
也就是C是这样的形式:
对于之前我们使用的MSE损失函数,这是满足的。我们使用batch的梯度下降的时候需要求C对参数w的偏导数,因为损失函数是每个训练数据的损失的平均,所以我们只需要求每个数据的偏导数,然后加起来平均就行。这个假设几乎所有的损失函数都是满足的【我是没见过损失函数不满足这个条件】
损失函数是最后一层输出的函数
这个条件几乎常见的损失函数都是这样的,我们之前时候的MSE就是计算最后一层的输出aL和正确的y(one-hot)的均方误差,显然是满足的。
(3) Hadamard product
这个名字看起来很复杂,其实很简单,就是两个向量elementwise的乘法。看一个例子就清楚了:
(4) 反向传播算法(back propagation)的4个公式
回顾一下,我们之前说了,梯度下降其实最核心的问题就是求损失函数对每一个参数的偏导数。那我们就直接一个一个求好了,为什么又要搞出一个反向传播算法呢?其实这个算法在不同的领域被不同的人重复“发现”过很多次,有过很多不同的名字,最本质的应该就是逆向求导(reverse-mode differentiation)或者叫做自动求导(automatic differentiation)。自动求导(AD)是非常通用的一种求偏导数的方法,很早就在流体力学和大气物理等领域使用,反向传播算法可以认为是AD在神经网络中的应用。不过最早发现这个算法的人(是谁最早好像还有点争议)并不是先知道AD可以直接用于神经网络,他发现这个算法是基于错误的反向传播而得到的,所有命名为(错误的)反向传播算法。后面我们会讲到AD,这是一个强大的算法,任何一个函数,你能把它分解成有向无环图的计算图【函数一般都能分解成一些无依赖的最基础的变量的复合函数,因此肯定可以表示成这样一个有向无环图】,然后每个节点都表示一个函数。只要你能求出这个函数在特定点的梯度【也就是这个函数对所以自变量的偏导数】(不需要求解析的偏导数,当然很多情况,这些函数都是能直接求出解析解,然后代入这个特定点就行,但理论上我们是可以用其他方法,比如数值梯度近似来求的),就能自动的计算损失函数对每一个参数的偏导数(也是在这个点的),而且只要反向根据拓扑排序遍历这个图一次就行,非常高效和简单。后面我们会详细的介绍AD。这个方法非常通用,TensorFlow的核心就是AD。使用AD的框架就比较灵活,我想“创造”一种新的网络结构,我又不想【其实更可能是不会】推导出梯度的公式,那么我只需要把我的网络能用这样一个有向无环图表示就行。当然节点必须要能够求出梯度来,一般我们的函数比如矩阵的运算,卷积等等TensorFlow都封装好了——它把它叫做一个op。我们只需要搭积木一样把这个计算图定义出来,TensorFlow就自动的能根据AD计算出损失函数对所有参数的梯度来了。当然如果你要用到一个TensorFlow没有的op,那你就需要根据它的规范实现这个op,一个op最核心的接口就是两个,一个是输入x,求f(x);另一个就是求f在某个x0点的梯度。
不过这里,我们还是沿着神经网络的发展历史,从错误的反向传播角度来理解和推导这个算法。
首先,我们会对每一个神经元比如第L层的第j个,都定义一个错误δ_jL
也就是损失函数对z也就是线性累加和的偏导数。为什么定义这样一个东西呢?我们假设在第L层的第j个神经元上有一个精灵(Daemon)
当这个神经元得到来自上一次的输入累加计算出z_jL的时候,它会恶作剧的给一点很小的干扰Δz_jL。原来它应该输出的是σ(z_jL),现在变成了σ(z_jL +Δz_jL)。这个微小的变化逐层传播,最终导致损失函数C也发生如下的变化:
这个其实就是导数的直觉定义:微小的Δx引起微小的Δy,Δy/Δx约等于导数。
不过这个精灵是个好精灵,它想帮助我们减少损失。 当
大于0的时候,它让Δz_jL小于0,反之当它小于0的时候它让Δz_jL大于0。这样
总是小于0
因此我们的loss就会变小。而其绝对值越大,我们的损失减少的越多。
当然你会说为什么不能让Δz_jL非常大,这样我们的损失总是减少很多?可惜这个精灵是个数学家,它说如果Δx太大,那么Δy=df/dx *Δx就不准确了。
所以我们可以这样认为:它就是第L层的第j个神经元“引起”的“错误”。如果绝对值大,则它的“责任”也大,它就得多做出一些调整;反之如果它趋近于0,说明它没有什么“责任”,也就不需要做出什么改变。
因此通过上面的启发,我们定义出δ_jL来。
接下来我们逐个介绍反向传播算法的4个公式。
公式1. 第L层(最后一层) 的错误
这个公式的第一项,就是损失C对a_jL的导数,它越大,说明C受a_jL的影响也就越大,如果有了错误,第a_jL的“责任”也就越大,错误也就越大。第二项是a_jL受z_jL的影响。两者乘起来就是z_jL对最终损失的影响,也就是它的“责任”的大小。
这个公式很好计算,首先第二项就是把z_jL的值(这个在feedforward节点就算出来并存储下来了)代入σ'(x)。如果σ是sigmoid函数,我们前面也推导过它的导数:σ’(x)=σ(x)*(1-σ(x))。第一项当然依赖于损失函数的定义,一般也很好求。比如我们的MSE损失:
具体的推导我在纸上写了一下,虽然很简单,我们也可以练练手,尤其是对于求和公式的展开,希望大家能熟悉它,以后的推导我可能就不展开求和公式了,你需要知道求和公式里哪些项是和外面的自变量无关的。
公式BP1是elementwise的,我们需要变量j来计算每一个δ_jL。我们也可以把它写成向量的形式,以方便利用线性代数库,它们可以一次计算向量或者矩阵,可以用很多技术利用硬件特性来优化(包括GPU,SSE等)速度。
右边δ'(z_L)很容易理解,左边的记号可能有些费解,其实我们把∇aC当成一个整体就好了,它是一个向量,第一个元素是∂C/∂a_1L,第二个就是∂C/∂a_2L,…
如果算上函数C是MSE的话,上面的公式就可以简化成:
公式2. 第l层(非最后一层) 的错误
等下我们会证明这个公式,不过首先我们来熟悉一下公式。如果我们想“背”下这个公式的话,似乎看起来比第一个BP1要复杂很多 。我们先检查一下矩阵和向量的维度,假设l+1层有m个元素,l层n个。则w_l+1的大小是m*n,转置之后是n*m,δ_l+1的大小是n*1,所以矩阵相乘后是m*1,这和δ_l是一样的,没有问题。
接下来我们仔细观察一下BP2这个公式,首先第二项σ'(z_l)和前面的含义一样,代表a_l对于z_l的变化率。
而第一项复杂一点,我们知道第l层的第j个神经元会影响第l+1层的所有神经元,从而也影响最终的损失C。这个公式直接给了一个矩阵向量的形式,看起来不清楚,所以我在草稿纸上展开了:
最终第L层的第j个神经元的损失就是如下公式:
这下应该就比较清楚了,第l层的第j个神经元的损失,就是把l+1层的损失“反向传播”回来,当然要带上权重,权重越大,“责任”也就越大。
如果要“背”出这个公式也没有那么复杂了,先不看σ'(z_l),第一项应该是矩阵w_l+1乘以δ_l+1。由于矩阵是m*n,而
向量δ_l+1是m*1,为了能让矩阵乘法成立,那么就只能把w转置一下,变成n*m,然后就很容易记住这个公式了。
注意,BP2的计算是从后往前的,首先根据BP1,最后一层的δ_L我们已经算出来了,因此可以向前计算L-1层的δ_L-1,
有了δ_L-1就能计算δ_L-2,…,最终能算出第一个隐层(也就是第2层)δ_1来。
公式3. 损失函数对偏置b的梯度
这前面费了大力气求δ_l,不要忘了我们的最终目标是求损失函数对参数w和b的偏导数,而不是求对中间变量z的偏导数。
因此这个公式就是对b的偏导数。
或者写成向量的形式:
∂C/∂b就是δ!
公式4. 损失函数对w的梯度
或者参考下图写成好记的形式:
也就是说对于一条边w_jkL,∂C/∂w_ij就是这条边射出的点的错误δ乘以进入点的激活。非常好记。
我们把这四个公式再总结一下:
(5) 这四个公式的证明
首先是BP1,请参考下图:
剩下的BP3和BP4也非常类似,我就不证明了。
反向传播算法
1. a_1 = 输入向量x
2. Feedforward 根据公式
和
计算z_l和a_l并存储下来(反向传播时要用的)
3. 计算最后一层的错误
计算损失对所有参数的偏导数
2.2.6 代码实现反向传播算法
我们已经把公式推导出来了,那怎么用代码实现呢?我们先把代码复制一下,然后说明部分都是作为代码的注释了,
请仔细阅读。
class Network(object):2.2.7 为什么反向传播算法是一个高效的算法?
def update_mini_batch(self, mini_batch, eta):
# mini_batch是batch大小,eta是learning rate
nabla_b = [np.zeros(b.shape) for b in self.biases]
# 构造和self.biases一样大小的向量,比如前面的例子 sizes=[784,30,10],则
# nabla_b是两个向量,大小分别是30和10
nabla_w = [np.zeros(w.shape) for w in self.weights]
# 构造和self.weights一样大小的矩阵,比如前面的例子 sizes=[784,30,10],则
# nabla_w是两个矩阵,大小分别是30*784和10*30
for x, y in mini_batch: #对于每个训练样本x和y
delta_nabla_b, delta_nabla_w = self.backprop(x, y)
# 用backprop函数计算损失函数对每一个参数的偏导数。
# backprop函数下面会详细讲解
nabla_b = [nb+dnb for nb, dnb in zip(nabla_b, delta_nabla_b)]
# 把返回的对b偏导数累加到nabla_b中
nabla_w = [nw+dnw for nw, dnw in zip(nabla_w, delta_nabla_w)]
# 把返回的对w的偏导数累加到nabla_w中
self.weights = [w-(eta/len(mini_batch))*nw
for w, nw in zip(self.weights, nabla_w)]
# 计算完一个batch后更新参数w
self.biases = [b-(eta/len(mini_batch))*nb
for b, nb in zip(self.biases, nabla_b)]
# 更新b
...
def backprop(self, x, y):
# 输入是x和y,返回损失函数C对每个参数w和b的偏导数
# 返回的格式是两个元组,第一个是b的偏导数,第二个是w的。
nabla_b = [np.zeros(b.shape) for b in self.biases]
# 构造和self.biases一样大小的向量,比如前面的例子 sizes=[784,30,10],则
# nabla_b是两个向量,大小分别是30和10
nabla_w = [np.zeros(w.shape) for w in self.weights]
# 构造和self.weights一样大小的矩阵,比如前面的例子 sizes=[784,30,10],则
# nabla_w是两个矩阵,大小分别是30*784和10*30
# feedforward
activation = x
activations = [x] # 用一个list保存所有层的激活,下面backward会有用的
zs = # 同样的用一个list保存所有层的加权累加和z,下面也会用到。
#下面这段代码在feedward也有,不过那里是用来predict用的不需要保存zs和activations
for b, w in zip(self.biases, self.weights):
z = np.dot(w, activation)+b
zs.append(z)
activation = sigmoid(z)
activations.append(activation)
# backward pass
#1. 首先计算最后一层的错误delta,根据公式BP1,它是损失函数对a_L的梯度乘以σ'(z_L)
# sigmoid_prime就是σ'(z_L),而∂C/∂a_L就是函数cost_derivative,对于MSE的损失函数,
# 它就是最后一层的激活activations[-1] - y
delta = self.cost_derivative(activations[-1], y) * \
sigmoid_prime(zs[-1])
# 2. 根据公式BP3,损失对b的偏导数就是delta
nabla_b[-1] = delta
# 3. 根据公式BP4,损失对w的偏导数时delta_out * activation_in
# 注意,我们的公式BP4是elementwise的,我们需要写成矩阵向量的形式
# 那怎么写呢?我们只需要关心矩阵的大小就行了。
# 假设最后一层有m(10)个神经元,前一层有n(30)个,
# 则delta是10*1, 倒数第二层的激活activations[-2]是30*1
# 我们想求的最后一层的参数nabla_w[-1]是10*30,那么为了能够正确的矩阵乘法,
# 只要一种可能就是 delta 乘以 activations[-2]的转置,其实也就是向量delta和activations[-2]的外积
nabla_w[-1] = np.dot(delta, activations[-2].transpose())
# 接下来从倒数第二层一直往前计算delta,同时也把对w和b的偏导数求出来。
# 这里用到一个比较小的trick就是python的下标是支持负数的,-1表示最后一个元素,-2是倒数第二个
# l表示倒数第l层,2就表示倒数第2层,num_layers - 1就表示顺数第2层(也就是第1个隐层)
# 比如我们的例子:sizes=[784, 30, 10],那么l就是从2到3(不包含3),l就只能是2,页就是第1个(也是唯一的一
# 个)隐层
for l in xrange(2, self.num_layers):
# 倒数第l层的z
z = zs[-l]
# 计算σ'(z_l)
sp = sigmoid_prime(z)
# 根据BP2,计算delta_l,注意weights[-l+1]表示倒数第l层的下一层
delta = np.dot(self.weights[-l+1].transpose(), delta) * sp
# 同上,根据BP3
nabla_b[-l] = delta
# BP4,矩阵乘法参考前面的说明
nabla_w[-l] = np.dot(delta, activations[-l-1].transpose())
return (nabla_b, nabla_w)
分析完代码,我们发现一次backprop函数调用需要feedforward一次,网络有多少边,就有多少次乘法,有多少个点就有多少次加分和激活函数计算(不算第一层输入层)。反向计算也是一样,不过是从后往前。也就是说这是时间复杂度为O(n)的算法。
如果我们不用反向传播算法,假设我们用梯度的定义计算数值梯度。对于每一个参数wj,
我们都用公式 limit (f(w1, w2, …, wj+Δ wj, …) - f(w1, w2, …, wj, …)/Δwj
f(w1, w2, wj, …)只需要feedforward一次,但是对于每个参数wj,都需要feedforward一层来计算f(w1, w2, …, wj+Δ wj, …),它的时间复杂度是O(n),那么对所有的参数的计算需要O(n^2)的时间复杂度。
假设神经网络有1百万个参数,那么每次需要10^12这个数量级的运算,而反向传播算法只需要10^6,因此这个方法比反向传播算法要慢1百万倍。 收起阅读 »
【公告】环信IOS支持https的版本正式发布
因苹果公司从2017.1.1日起强制要求所有上线APPStore的APP都需要支持ATS标准。为了适应这一政策不影响用户APP的正常发布,环信分别于2016.12.6日和8日正式发布IOS V2.2.9和V3.2.2版本,请尽快更新SDK的版本以免影响APP的正常发布计划。
ios V3.2.2 SDK 已发布,增加是否删除会话选项
新功能/优化:
SDK满足apple ATS的要求
删除好友逻辑的修改(增加是否删除会话选项)
修复呼叫时对方不在线,不能正确显示通话结束原因的问题
ios V2.2.9 SDK 已发布,SDK满足apple ATS的要求
新功能/优化:
SDK满足apple ATS的要求
删除好友逻辑的修改(增加是否删除会话选项)
修复呼叫时对方不在线,不能正确显示通话结束原因的问题
新版SDK下载:SDK下载
更新过程中遇到问题欢迎社区发帖咨询或者联系环信技术支持 收起阅读 »
客户中心的“手艺人”
属于21世界的新工匠,应该是懂得关心他人、知道感恩、能为别人着想的人,是能够说“好的,明白了,请交给我来做”的人,也就是拥有一流人品、“会好好做事”的匠人。
一流的匠人,人品比技术更重要。
《匠人精神》秋山利辉
“劳动密集”,几乎是对客户中心一种“约定俗成”的说法,以至于许多业内的文章都以“这是一个劳动密集型的行业……”开篇,以至于每每聊到一个不熟悉的客户中心,人们必谈其规模,“嗨,你们那里有多少人……?”
曾经,我也这样认为,也不假思索地一次次这样说着,“嗯,这是一个劳动密集型行业……”。
蓬勃发展的CTI技术,早已让铃声不似家中电话那样响声外泄;专业的客服职场空间设计,让越来越多的中心逼格大增,各种消音、吸音、采光、通风以及人性化的设置,也让如今的客户中心不再像走进蜂窝那样,“人声鼎沸”、“沉闷无趣”;而全媒体客户中心的发展演进,又出现了许多不戴耳麦,静静坐在那里狂敲键盘的员工。
客户中心,这个客户心中略显神秘的世界,一直在改变着。
一次次,被耳边此起彼伏但无比一致的亲切声音所打动。客户中心的这帮孩子们,面带微笑、身姿端正、热情地重复着重复了不知多少遍的对话,那专注讲话的神态,挥在空中边说边比划的双手,正像是一个个“手艺人”,反复雕琢着自己的“作品”,或是声音、或是文字、或是一丝淡淡的微笑,在他与客户的世界里。
他们,是以沟通为一技之长的“手艺人”!
为了让客户听得清楚明白,客户中心的“手艺人”们,一次次逐字逐句修改话术、一遍遍聆听录音,反复琢磨怎样说以及说什么;为了让客户在有需要的时候更容易找到他们,客户中心的“手艺人”们打通所有的联络渠道,提供7*24小时的不间断服务,甚至包括各种节假日;为了让客户的体验更好,“手艺人”们梳理每一个服务流程,不放过每一个细节,通话时长精确到秒、接通率指标关注到小时、甚至连上厕所都要计算着时间……他们,这些客户中心的坐席代表与管理者们,正是企业全面客户管理系统里的“手艺人”。
一致性,是客户中心管理的至高追求。哪怕是一个简单业务通知的上传下达,放在几百及至上千人的客户中心,就是一件非常考究的“管理艺术品”:从经理到主管,从主管到组长,从组长到员工,从员工到客户,一传十十传百,百传千千万,最终清晰准确传到成千上万个客户那里,这本就是一项客户中心的手艺人必须修炼的基本功。
很多人认为客户中心的工作是机械重复的,而客户中心的这些手艺人,对这种重复则有着更深远的理解:手艺人独具的工匠精神,代表着这个以客户为本的时代气质,满满的爱、无微不至、精益求精。他们身上传承的工匠精神,是有信仰的踏实和认真!这个信仰,叫“以客为本”。
“以客为本”的手艺人精神,其价值在于精益求精,对匠心、精品的坚持和追求。对每一个客户而言,或许微不足道,却改变着整个行业的生态,激励着世界文明的进步。
如今,客户中心的“手艺人”们,有了自己的节日:中国客户管理人节(China Customer Care & Service Professionals Day),定于毎年10月第三周的周二,这一天惯例举办客户世界年度大会,颁发“金耳唛杯”中国最佳客户中心奖项!我们为自己代言,我们向社会发声,我们为中国服务的进步而努力!
致敬,向客户心中所有客户中心的手艺人们。
本文刊载于《客户世界》2016年10月刊文章;原文作者郭勇强 收起阅读 »
2017创业路上,我们陪你
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:手机端可直接扫码
收起阅读 »
猿生态十城巡回沙龙丨深圳
时间:2016.12.10 13:30-17:00
地点:深圳市南山区高新科技产业园南区科园路北科大厦4002室 开源中国
活动议程
13: 00 – 13: 30 活动签到
13: 30 – 16: 30 技术分享 + 实战演示
16: 30 – 17: 00 抽奖 + 自由交流
讲师介绍
报名链接
http://devhub.cc/api/h5/activity/c368dbb3c1b52a37344985116d641e2f
收起阅读 »
Android V3.2.2 SDK 已发布,新增音视频离线通知
Android V3.2.2 2016-12-2
新功能/优化:
- 新增设置音视频参数及呼叫时对方离线是否发推送的接口
- 新增修改群描述的接口;
- 删除好友时的逻辑修改: 删除好友增加接口,根据参数是否删除消息; 被动被删除时不再删除会话消息, 用户需要删除会话及消息时可以在onContactDeleted()中调用EMClient.getInstance().chatManager().deleteConversation(username, true)。
- 修复3.2.1版本中某些情况下心跳比较频繁的问题,节约流量电量,建议升级到最新版本;
- 修复呼叫时对方不在线,不能正确显示通话结束原因的问题;
- 修复某些特殊情况下获取群成员列表时crash的问题;
- 修复某些特殊情况下退出时crash的问题;
- demo中增加音视频参数设置页;
版本历史:更新日志
下载地址:SDK下载 收起阅读 »
【产品快递】Web IM V1.4.5已发布,支持实时视频聊天
新功能:
1.GNU风格的版本号命名格式: 主版本号.子版本号.修正版本号 (新版本规则的1.4.5 = 旧版本规则的1.1.4.5)
2.【DEMO】好友之间可以通过webrtc进行视频聊(仅支持 https + Webkit浏览器)
3.【DEMO】支持同一账号最多8个标签页登录 `isMultiLoginSessions:true`
4.【DEMO】http访问加入ip策略功能,防止DNS劫持 `isHttpDNS:true`
5.【DEMO】新增两种安装引用方式(具体引用方式,请参考集成方式)
- 添加 `<script>` 标签,并通过WebIM命名空间访问websdk
- NPM(websdk 已经发布到NPM),先require,再访问WebIM
Bug修复:
1.【SDK】 解散群组不更新UI
2.【SDK】 修复了发送cmd消息成功后无法调用回调函数的bug
webim体验:https://webim.easemob.com/
版本历史:更新日志
SDK下载:点击下载 收起阅读 »