新版群组/聊天室服务 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下载:点击下载 收起阅读 »
Ping++ 「变现时代」大会
「变」意味着「变化」,「现」代表着「当下」。所谓「与时迁移,应物变化,设策之机也」,企业们不仅要活在当下,更要拥抱变化。
值此年关,Ping++ 携手各界合作伙伴,邀请到那些勇于自我颠覆、创新商业模式的企业家们,分享他们这一年来对于商业模式、企业转型、业务增长上的见解与心得。
为了简单温暖的支付体验,Ping++ 惟愿伴你一路同行。
大会议程
时间:2016 年 12 月 17 日 13:00 - 17:00
地址:北京市朝阳区太阳宫北街 2 号院 1 号楼, LAVIN 玫瑰里·太阳公元·心之芳庭
(地铁乘坐城铁10号线、13号线到芍药居站下车即到)
13:00 - 13:30 入场签到
13:30 - 13:40 开场致辞
13:40 - 14:10 分主题 · 流量 | 在行 & 分答联合创始人 杨璐
14:15 - 14:45 分主题 · 内容 | 什么值得买 CEO 那昕
14:50 - 15:20 分主题 · 平台 | 掌合天下市场 VP 张龙飞
15:25 - 15:55 分主题 · 共享 | ofo 共享单车联合创始人 张巳丁
16:00 - 16:30 分主题 · 开放 | Ping++ 聚合支付 CEO 金亦冶
16:30 - 17:00 茶歇 & 交流
注:以上议程有可能变动,以现场实际议程为准
嘉宾介绍
杨璐
在行&分答联合创始人,市场 VP
嘉宾简介:
在进入在行&分答前,杨璐拥有 12 年的广告从业经验。八年的奥美工作经验,每年服务过亿投放量的客户,练就了扎实的专业基础和团队管理能力,成为奥美亚太地区最年轻的客户群总监。加入在行后,与团队上线了 2016 年互联网界最吸引人瞩目的付费语音问答产品——分答。分答上线后 42 天成功融资 2500 万美金,估值上亿美金。
那昕
什么值得买 CEO
嘉宾简介:
曾任京东智能集团副总裁并推出京东硬件平台“JD+”计划,于 2015 年 6 月受创始人隋国栋邀请加入什么值得买。什么值得买(SMZDM.COM)成立于 2010 年 6 月 30 日,是一家集导购、媒体、工具、社区属性为一体的消费领域门户型网站,因其中立、专业而在众多网友中树立了良好口碑,为消费者提供决策建议。
张龙飞
掌合天下市场 VP
嘉宾简介:
资深互联网/电子商务从业者,曾先后服务过 263 网络集团、中国数码集团、学大教育集团以及国内最大的电子商务新闻门户亿邦动力网,熟悉电子商务行业、精通互联网市场营销。拥有 10 年的互联网行业工作经验,现任掌合天下市场营销中心负责人。掌合天下成立于 2013 年,是一家专注于快消品供应链的电子商务平台,依托独创的“城市合伙人”模式和专业的快消互联网运营经验,为供货商和超市提供便捷,透明,安全的一站式 B2B 综合服务,现业务已覆盖全国的 27 省及自治区的 600 多个城市,并于今年 10 月完成 7 亿元的 B 轮融资。
张巳丁
ofo 共享单车联合创始人
嘉宾简介:
重度骑行爱好者和极限运动爱好者。北大考古 2013 届本科,2015 届文化遗产硕士。ofo 是全球首个无桩共享单车出行解决方案,首创“单车共享”模式。上线一年来,ofo 已成为国内规模最大的共享单车平台,拥有 10 万辆共享单车,提供超过 2000 万次共享单车服务,为 21 座城市 200 多万用户提供便捷的出行服务。2016 年 10 月,ofo 完成 1.3 亿美元 C 轮融资。
张一甲
Xtecher 联合创始人
嘉宾简介:
科技媒体、科技企业服务平台 Xtecher 联合创始人。毕业于北京大学数学科学学院,获经济学双学位。曾获中国数学奥林匹克金牌,入选国家集训队。毕业后先后就职于奥美中国、百度,为奔驰、伊利、奥迪、雀巢、康师傅、361度等国际一流客户提供品牌策划、创意管理和长期服务。Xtecher 成立于 2015 年 4 月,致力于发现最有潜力的早期科技项目,并为它们进行全面的价值加速。
金亦冶
Ping++ 创始人&CEO
嘉宾简介:
连续创业者,斯坦福大学电子工程硕士学位,2015 年入选福布斯中国 30 位 30 岁以下创业者榜单。2014 年创立面向开发者提供移动支付 SDK 服务的公司 --「Ping++」,让「7 行代码接入支付」成为了业内新标准,2015 年 12 月获得宽带资本领投的千万美元 B 轮融资。
大会媒体/合作联系:马锐 18608948398(同微信)、安贤龙 17710330365(同微信)
合作伙伴
主办方:Ping++ 聚合支付系统
联合主办:Xtecher
协办方:什么值得买、分答 & 在行、掌合天下、ofo 共享单车
特别合作:微软加速器、环信、北京天津企业商会、天天投、互动吧
合作伙伴:灵伴科技、纳什空间、鸟哥笔记、SDK.cn
媒体合作:Donews、创业邦、人人都是产品经理
速记支持:有道云笔记
ping++微信公众号,
特别提醒:为保证活动质量,本次大会为收费制,50 元/人。环信作为本次大会的合作伙伴,其用户/粉丝可直接使用 VIP 邀请码进行报名,无需付费。请通过关注 Ping++ 公众号:Pingplusplus,后台回复”环信+公司+职位“领取 VIP 邀请码。
报名方式:扫描以下二维码进入报名页面,
输入邀请码,报名费会自动变为 0.01 元,支付 0.01 元后即可完成报名。 收起阅读 »
iOS 环信昵称、头像、群头像、群昵称处理
废话不多说,直接进入主题。因为这次不涉及sdk内的什么,所以不用管SDK版本。
重中之重:先和你的好基佬,安卓哥们定好走什么流程
环信官方不会跟你存储好友关系什么的,昵称头像什么的就不要想着偷懒了,所以自力更生吧!!
单人昵称 + 头像
方法一:自家服务器建张表,把所有的好友关系存储起来,这也是最好的,能够做到昵称头像实时更新等:
1、后台建表存储好友关系后,我们在每次登陆后,开一个线程把表数据请求下来存起来。方法二:把昵称头像放到消息拓展中。
2、写几个方法,根据环信ID查找表中的昵称和头像。
3、更新昵称和头像,可以像微信一样,点击头像查看好友详情时进行更新;还有自家好友列表应该会给最新的,这个时候同步更新一份。(写方法,进行更新)
easeui中发送方法中把昵称头像传到ext中去
在push到聊天界面时,把昵称头像放到拓展中互传
原谅这是以前的代码,没有做到简化,原理一样,就是在聊天时,把自己的昵称头像和对方的昵称头像放到一个字典里,自己定Key,把它加到消息体中的拓展里面,每次拿到消息时,直接取消息体中的拓展,把相对应的拿出来展示就可以。
两者比较:
前者:最理想的方法之一,就是要多写点代码,建张表和刷选而已,具体缺点,我还没发现重点:后者其实也可以跟新的啦!!!!
后者:简单容易处理,新生版本可以考虑,减少开发时间,但是测试肯定给你找问题,我先聊几句,然后改个昵称和头像,尼玛,改完之前还是老昵称和头像,这是因为这些消息体也是存到本地的,没有给你更新,因为拓展就是为了便于开发者开发各种消息的,所以做不到给你更新。
这只是一个例子,我用来更新我的业务逻辑的
根据环信ID,拿到消息体的拓展,把以前的昵称和头像都更新下就可以啦!
具体的方法可以参考环信官方:http://docs.easemob.com/im/490integrationcases/10nickname
群聊名称 + 头像
方法一:把所有的好友信息包括昵称头像等放到群组的群名称中去。
群名称
拿到群名称后,自己写方法,把里面的的各种数据拿出来
各种方法
群名称的方法
其他的我就不做过多展示,给一个效果图:
群头像的九宫格展示:直接去gitub上面搜索就可以找到,各种各样的,只需把头像传进去,返回一个imageView(大部分是本地图片效果,所以需要自己改成网络的)。方法二:拓展消息
同样可以借鉴单聊中使用到的消息体中的拓展。然后自己写方法把里面的数据拿出来,具体方法我没有操作过,有兴趣的朋友可以尝试下。
有建议和好的想法的小伙伴可以提出来,大家互相讨论下,增长知识。有不懂的可以找我:qq:1804094055
收起阅读 »
iOS 新手集成单聊、群聊、语音和视频通话的简述
(1)集成环信SDK:
pod:pod 'HyphenateFullSDK'
手动:因为我们要使用到语音和视频的功能,所以我们需要导入(环信 iOS HyphenateFullSDK 开发使用(包含实时通话功能))HyphenateFullSDK这个包。手动导入包文件后,我们需要手动加上一些库:
第 1 步:SDK 包含实时语音依赖库有:
CoreMedia.framework
AudioToolbox.framework
AVFoundation.framework
MobileCoreServices.framework
ImageIO.framework
libc++.dylib
libz.dylib
libstdc++.6.0.9.dylib
libsqlite3.dylib
libiconv.dylib
(如果使用的是 xcode7,后缀为 tbd。)
第 2 步:SDK 不支持 bitcode,向 Build Settings → Linking → Enable Bitcode 中设置 NO。
(2)添加EaseUI:
必须和SDK相对应的版本,不然会出现各种报错(方法找不到或者不对)
注意:如果编译报错,1、先检查EaseUI中的第三方是否和本地工程中的重复。 2、在PCH文件中引入头文件时:
#ifdef __OBJC__ #import "easeUI.h" #import "EMSDKFull.h" 等等 #endif
正式开始代码的编写:
初始化SDK:
//AppKey:注册的AppKey,详细见下面注释。//apnsCertName:推送证书名(不需要加后缀),详细见下面注释。
EMOptions*options=[EMOptions optionsWithAppkey:@"douser#istore"]; options.apnsCertName=@"istore_dev"; [[EMClient sharedClient]initializeSDKWithOptions:options];
注册:
EMError*error=[[EMClient sharedClient]registerWithUsername:@"8001"password:@"111111"];if(error==nil){NSLog(@"注册成功");}
登陆:
EMError*error=[[EMClient sharedClient]loginWithUsername:@"8001"password:@"111111"];if(!error){NSLog(@"登录成功");}
注意:具体的自动登录(免登陆)、重连、退出登陆、异地登录等请移驾至环信官方:http://docs.easemob.com/im/300iosclientintegration/30iossdkbasic
聊天会话列表界面:建议自己搭建,便于管理和拓展
1:直接用环信的列表界面:ConversationListController 直接初始化加到自己的tabar上去
2:自己搭建列表界面:
获取与自己相关的所有的会话
cell上的角标、时间、信息等
tabar上的角标
- (void)didReceiveMessages:(NSArray *)aMessages;是刷新该列表的重点(记住注册及代理)。对方发来的消息都会在这个方法中进行刷新列表及跟新角标数等。
注意:其他代码自行到demo中去寻找,环信demo中都有参考;
单聊+群聊:因为自己写的效果远没有环信的好,加上需求不要对聊天界面进行自定义,所以最后我还是直接用的demo中的界面(把demo中的ChatViewController拖到自己工程中,编译,注释掉报错的代码),初始化,push到聊天界面即可:
ChatViewController *chatController = [[ChatViewController alloc] initWithConversationChatter:conversation.conversationId conversationType:conversation.type];
群列表:获取与之相关所有的群
NSArray *groups = [[EMClient sharedClient].groupManager getMyGroupsFromServerWithError:nil];
语音和视频及时通话:因为都是环信写的 所以把有关及时通话的文件拖进来;
ChatDemoHelper CallViewController
1:在pct中加上 #define DEMO_CALL 1
2:全局搜索#if DEMO_CALL == 1 删掉,编译如有报错请仔细看是不是哪句代码重复了或者哪里没有注释完全。
3.ChatDemoHelper.m中 注册代码到——#pragma mark - EMCallManagerDelegate宏中间的代码都可以注释掉
重中之重:在根控制器中一定要把控制器赋给ChatDemoHelper:[ChatDemoHelper shareHelper].mainVC = self;
语音和视频通话必走的代码
如果集成语音和视频过程中有问题的,可以参考官方给出的视频,不过只能借鉴:http://www.imgeek.org/video/24
上面所述就是我使用环信的心得,不是很全面。大神手下留情,新手一枚。后续我会把单人昵称、头像;群组名称、群组头像等等的一些方法写出来,如果发现我所提出的有问题和有好的建议,请直接提出来,也可以一起讨论。qq:1804094055 有问题可以找我,环信讨论群:340452063。
收起阅读 »
呼叫中心还是客户中心?
近年来,呼叫中心常常被称为这个时代的“白领血汗工厂”。作为一名资深的客户服务代表、主管、经理、总监,当我第一次听到这种说法时,被深深地触动了。我熟悉的很多呼叫中心都把办公场所设在比较偏远的地方,在这些地方一般人很难把工作有效地完成做好,但客服工作人员却可以在电话里处理好各种任务。
想一想,传统的呼叫中心是什么样子呢?它们通常是由一些普通格子间组成,几乎没有什么可以发挥创造力的空间。通话时长和通话处理时长都是很重要的指标,这说明每一通电话都是在处理业务而不是单纯地交流互动。另外对于呼叫中心的工作人员来说,即使公司朝着蓬勃的方向发展,客服工作人员的发展空间也是极其有限的。
将呼叫中心转型为客户中心。有人会认为,以这样一种方式来加深企业与客户之间的关系,无异于痴人说梦。但事实真的是这样吗?其实只要做出一些调整,呼叫中心就可以变为客户中心。
第一步也是最简单的一步是将部门名称改为客户中心。如果我们希望员工成为客户关系的建立者,那么我们应该从部门的定位开始。在这个以交易时间、任务量和呼入量来微观管理的时代,我们应该时常这样反问自己:如果我们不以最好的方式来对待我们的员工,又怎能期望他们以最好的方式来对待我们最尊贵的客人?如果做不到最好,起码得一视同仁,只可惜我们连这样也做不到,我们只是一味地强调员工是企业与客户关系的建立者以及公司产品的形象代表,仅此而已。通常情况下,呼叫中心是企业与客户仅有的互动方式。客户愿意花时间打电话给我们,是因为遇到了各种类型的问题,他们需要我们提供帮助。难道我们不想让员工在成为公司政策执行者的同时还是客户问题的解决者吗?这样不仅不会让客户的投诉不断升级,也能更好地帮旅客解决问题。
如何管理比较合适呢?怎样能在让公司在付出最小代价的同时能够给员工最大的权限去帮客户解决问题?这就需要我们花时间培养员工提前预判的能力并在此基础上出色地完成任务,这样也能在机会来临时给客户带去惊喜。这其中的关键是需要准备,我们不能单纯地期望我们的员工认识到这些东西,也不能在一个问题上只告诉他们一次就期待他们能坚持,更不能在没有被加强训练的情况下期待他们保持正确的工作习惯。
解决方案:创建一个世界一流的服务体系。在这样一个体系中,所有员工都能发现并及时指出不足,坚持同样的标准,在把握机会的同时不断变得更好。为了能让员工更好地处理每天发生的不同情况,我们需要花更多的时间在幕后管理他们。这允许我们监督客服代表的活动以确保团队中的每个成员不在微观管理下都能恪尽职守。这让我们的客服代表们不会压力过大、负担过重进而保持员工数量的稳定性,因为我们没有足够的人员来雇佣和培训,这对呼叫中心来说是一大难题。这听起来很棒,但我们如何把它完成好呢?我建议从以下两个步骤开始,但两者都将需要投入一定的时间和人力成本。
第一步是让你的团队一起参与创造你的客户体验周期,这包括与团队一起研究确定客户的接触点。一旦确定了这些接触点,你就可以借此去剖析每一个人,找到可能出错的地方(服务缺陷),接下来就知道每个人和每通电话里需要做什么(操作和体验标准),同时我们能通过哪些方式来取悦客户(利用机会超越机会)。
通过这个项目,你和你的一线团队将会大开眼界,并产生新的使命感。你的团队会重现活力并能很好地完成工作。虽然这是一个很棒的开始,你不能指望这样一个开始在没有加强巩固的基础上就能保持不断发展的势头。接下来我们该做第二步了。
第二步是解决日常会议。在你说“这在这里永远都行不通,因为……”之前(我知道你会这样说,因为我听过所有的借口,而且我自己曾经也这样说过),可以思考下金牌服务的典范:丽思卡尔顿酒店。他们每天都会举行一个他们称之为“站起来”的会议。福来鸡也一样。这些公司已经忽略每个员工不能每天都出席会议的事实,因为他们一天有多种轮班制。他们能做的是利用好平台专注他们的服务价值,讨论并解决问题,庆祝每天成功的案例。
结论:这个过程在成就一个伟大的团建活动的同时也会不断提升团队成员的自主性和增强他们的归属感。创建属于自己客户体验周期,并在日常活动中不断加强,这有助于给团队带去新的使命感,并让员工变成真正的客户关系的建设者。随着日常会议深入,活动并不会因时间推移而逐渐消失,而是会变成一种超越规范并深入人心的文化。
本文原载于《客户世界》2016年11月刊;作者Dave Murray,DiJulius集团高级客户体验顾问;译者皮晶晶,深圳航空营销委电子商务电话营销中心运营人员。 收起阅读 »
终于等到你|“好客之道-客户管理轻学院”领跑互联网轻学时代
老师,我想学习,没有时间怎么办?”2016年11月30日,广州。借举办“2016全媒体客户中心管理论坛”的契机,客户世界机构今天正式宣布推出“好客之道”客户管理轻学院。
“我入行客户中心行业10年了,想分享一些自己的管理经验,没有渠道什么办?”
“现在有好多微课,微信公众平台视频学习,直播学习,不知道如何规划,选择什么渠道学习什么办?”
“这是一个最好的时代,也是一个最坏的时代”。我们对学习的需求一直存在,但面对茫茫书海以及网上众多课程,如何筛选,如何迅速抓住重点,如何快速吸收成了最关键的问题。
“轻学院”以促进客户管理从业人员的岗位培训与职业发展、提升全社会客户管理意识及客户服务水平为宗旨;系统梳理客户中心和电子商务行业的专业知识并深入研讨相关领域的管理热点话题。帮助本领域的从业者大幅削减高昂的培训费用及大量的时间成本,在互联网+的热潮中真正享受低廉和便捷的专业培训,与行业大咖和知名讲师零距离交流,全面接收行业前端资讯,深度探讨客户管理精髓。
客户世界机构从2003年1月开始编辑出版《客户世界》月刊,至今已14年,累计发行超过160期;同时累计组织编撰出版行业图书超过100本,常年为业内超过千家企业提供咨询和培训服务。作为客户中心行业的旗帜媒体和管理智库,在特定领域沉淀了丰富的资源。
客户世界机构同步于2004年创立了国内最大的客户管理人俱乐部“好客会”,以加强行业同仁之间的互动;通过举办年会、行业/主题论坛及读书沙龙、编辑聚餐、心理学及禅修活动、户外健身运动等方式推动会员交流;已举办各类线下会员活动超过两百场,其中包括了中国客户中心行业历史最久、水准最高、规模最大的行业会议——客户世界年度大会,年度行业评选——“金耳唛杯”中国最佳客户中心,好客会读书沙龙,中国客户中心马拉松赛等业界耳熟能详的品牌活动。
近年来,通讯技术和社交媒体的发展促进了交互的便捷、多元和高效;纸媒和咨询等知识积累方式以及会议和培训等知识传播手段亟待突破和创新。消费者连接技术、接触技术和体验技术的不断创新也正深刻改变着以客户中心为代表的客户交互服务领域,大数据、万物互联、认知计算的深入发展大大扩展了本领域的产业边界, 数字化、体验化、共享经济化的时代特征使得整个服务产业的融合发展成为趋势。
基于互动技术的发展以及受众行为偏好的改变,客户世界机构适时推出“好客会” 知识平台及o2o行业社区(www.iCustomer.com.cn)。旨在搭建一个服务业内互助互帮、优质资源整合的平台,让更多从业者健康快速成长、获益、并享受于服务工作之中。“好客会”社区将采用APP、视频直播、微信互动、社区交流等整合式的渠道开展有计划的内容整合传播以及持续的深度互动。透过新渠道的知识平台,传播知识、培养人才、推动行业健康发展。本次推出的“好客之道”客户管理轻学院是“好客会”知识平台的核心内容,将深度梳理客户中心与电子商务行业的资源,采用视频直播、图文直播、内容点播相整合的传播方式,以管理讲堂、主题论坛、访谈对话、特别策划等栏目定期播出的形式与行业受众见面。区隔于传统呼叫中心的封闭式培训,也区别于大型电商服务在平台内部的客服公开课,是针对客户中心和电子商务行业的管理者和一线从业者搭建的与各行业客户管理工作环节相关的深度学习平台,并为客户中心和电子商务从业者提供行业交流社群和活动组织平台。课程的组织将更加社群化和互联网化,以更加轻松的形式呈现。实名制会员将可以通过视频课程、影音课程、笔记分享等较轻的形式,随时随地进行学习和分享。课程内容围绕着帮助管理者明确管理战略、清晰管理战术、完善高效有序地运作团队,以及帮助一线从业者理解工作的意义、提高工作技能、打通职业晋升通道等。
就在“好客会”平台正式上线发布前的一周(11月24日,感恩节),“好客之道”工作室在位于北京朝阳区兴隆公园的“好客咖啡馆”启动了节目录制工作。自2016年12月起,相关内容将陆续每周推出。 收起阅读 »
安卓修改头像样式
集成环信个人感觉还很容易的,当然,坑也很多,环信很多东西都是可以自己定义的,直接进入正题吧,在demo的EaseUi里面utils包下面有个EaseUserUtils类里面有如下代码:
然后只要在setUserAvatar这个方法里面稍作修改
这个GlideCircleTransform类 可以写成内部类,也可在是外部的,建议内部,GlideCircleTransform这种可以自己写,不会的去百度,实在不会的加下面的群,这个方法我把代码给你们贴上:
public static void setUserAvatar(Context context, String username, ImageView imageView){
EaseUser user = getUserInfo(username);
if(user != null && user.getAvatar() != null){
try {
int avatarResId = Integer.parseInt(user.getAvatar()); //Glide.with(context).load(avatarResId).into(imageView); Glide.with(context).load(avatarResId).transform(new GlideCircleTransform(context)).into(imageView);
} catch (Exception e) {
//use default avatar //Glide.with(context).load(user.getAvatar()).diskCacheStrategy(DiskCacheStrategy.ALL).placeholder(R.drawable.ease_default_avatar).into(imageView);
Glide.with(context).load(user.getAvatar()).diskCacheStrategy(DiskCacheStrategy.ALL). placeholder(R.drawable.ease_default_avatar).transform(new GlideCircleTransform(context)).into(imageView);
}
}else{
//Glide.with(context).load(R.drawable.ease_default_avatar).into(imageView); Glide.with(context).load(R.drawable.ease_default_avatar).transform(new GlideCircleTransform(context)).into(imageView);
}
}
自己去根据图片排版去吧,到这里已经完成一半了,前面说了环信的好,该吐槽一下坑了,我改到这里后,只有列表变成想要的样子,会话页面还是不行,后来和帝都的一个妹子交流了下,她给我说要把EaseUi里面关于聊天界面,会话列表,联系人列表,个人资料布局里面的图片资源全部删除就行了,具体几个我忘记了,大概有七个左右,是左右,我忘了,也没几个布局,你们可以一个一个找找,这样就可以修改成自己喜欢的头像样式了
好了,到这里就差不多了,如果实在还是不行,或者是有其他问题的,请加环信互帮互助群 340452063 到群里找我 杭州-andorid-中草 龙虾头像 最后附上二表哥送的四句话
做一番一生引以为豪的事业
找一个一生荣辱与共的妻子
在有生之年报答帮过我的人
并且帮助那些需要帮助的人 收起阅读 »
环信助力爱宠医生为50万宠物主人在线解决疑难杂症
1.遇到的挑战:
爱宠医生技术总监张振亚表示:“由于业务主要为宠物主人的在线问诊平台,对于初创公司来讲,IM这一块由自己来开发不仅时间不允许,技术沉淀也不够,即便开发出来稳定性也很难保证。所以当时在相对于比较成熟的几款IM服务中,通过服务质量,稳定性,开发团队以及和业务需求进行匹配时,最终认为环信最适合我们的业务。”
2.环信解决方案:
因为在业务场景上宠物主人与宠物医生的对话是以问题的形式出现的,解决问题后问题需要关闭。所以单聊的方式不是特别符合爱宠医生的需求,爱宠医生对聊天室与群组进行对比,最后决定使用群组的方式来实现,宠物主人提出问题创建群组将宠物医生加入群组(群组上线2人)进行对话,问题结束后删除群组,抢单模式为将第一位宠物医生移除群组后再有抢单医生抢单成功后在移入群组进行对话。
3.价值体现:
为超过50万的宠物主人在线解决平常遇到的疑难杂症,环信即时通讯云在当中起到了至关重要的管道作用,保证了爱宠医生IM服务7×24的正常稳定运行。
爱宠医生技术总监张振亚表示:整体来讲,对环信的服务很满意。在创业初期就帮助我们解决了技术集成等问题,并且通过不断的版本迭代一直稳定运行到现在。技术支持服务也相对的很及时也很到位,绝大多数的问题可以在第一时间帮助我们解答。版本迭代更新也是很稳定的,也比较开放,可以让我们在使用过程中自定义与业务相关的功能。正是因为环信对于我们业务上的帮助,可以让我们在越来越繁华的宠物行业以及宠物医疗行业占得先机。
关于爱宠医生
宠物生病了怎么办?不用马上去医院,使用爱宠医生APP随时随地向周边的宠物医生和全国知名的宠物专家提问,给宠物问诊治疗,你还可以查到小区附近的宠物医院,查看宠物医院的评论,帮你找到最好、最靠谱的宠物医院和宠物医生。
爱宠医生是上海宠零宠网络科技有限公司旗下互联网宠物医疗平台,公司成立于2015年6月,坐落于上海浦东张江高科技园区,是浦东软件园孵化器孵化企业。公司于2015年和2016年分别获得中金资本和普华资本的两轮融资。公司核心管理团队均为原互联网上市公司核心人员和宠物行业资深人士。 公司旗下产品包括宠物医生免费在线问诊app、宠物医院智能SaaS管理系统以及宠物医院的供应链采销平台。拥有注册宠物主50多万人,业务覆盖北上广等100多个城市,3000多家宠物医院,5000多位宠物医生,已成为当前互联网宠物医疗第一品牌。公司在 北京举办的“2016创新中国春季峰会”,获得优秀企业奖,同时,在2016年香港举办的“亚洲智能手机应用程式大赛”中获得优秀项目奖。 收起阅读 »
【环信3.x Android加表情】加表情三部曲,你值得拥有!!!
首先我实现方式就是仿照官方demo里兔斯基的实现方式,在自带的表情基础上增加一组新表情。所以就很简单了!!!首先就不说如何集成环信demo了,相信有这个需求的人都是已经集成好了的。
- 第一部
首先找到EmojiconExampleGroupData这个类,复制一份照着修改下,我的代码如下:
package com.liancheng.tiantianzhengchong.HuanXin.domain;这里说明下,
import com.hyphenate.easeui.domain.EaseEmojicon;
import com.hyphenate.easeui.domain.EaseEmojicon.Type;
import com.hyphenate.easeui.domain.EaseEmojiconGroupEntity;
import com.liancheng.tiantianzhengchong.R;
import java.util.Arrays;
public class EmojiconSinaGroupData {
private static int[] icons = new int[]{
R.drawable.d_aini,
R.drawable.d_aoteman,
R.drawable.d_baibai,
R.drawable.d_baobao,
R.drawable.d_beishang,
R.drawable.d_bishi,
R.drawable.d_bizui,
R.drawable.d_chanzui,
R.drawable.d_chijing,
R.drawable.d_dahaqi,
R.drawable.d_dalian,
R.drawable.d_ding,
R.drawable.d_doge,
R.drawable.d_erha,
R.drawable.d_feizao,
R.drawable.d_ganmao,
R.drawable.d_guzhang,
R.drawable.d_haha,
R.drawable.d_haixiu,
R.drawable.d_han,
R.drawable.d_hehe,
R.drawable.d_heixian,
R.drawable.d_heng,
R.drawable.d_huaixiao,
R.drawable.d_huaxin,
R.drawable.d_jiyan,
R.drawable.d_keai,
R.drawable.d_kelian,
R.drawable.d_ku,
R.drawable.d_kulou,
R.drawable.d_kun,
R.drawable.d_landelini,
R.drawable.d_lang,
R.drawable.d_lei,
R.drawable.d_miao,
R.drawable.d_nanhaier,
R.drawable.d_nu,
R.drawable.d_numa,
R.drawable.d_nvhaier,
R.drawable.d_qian,
R.drawable.d_qinqin,
R.drawable.d_shayan,
R.drawable.d_shengbing,
R.drawable.d_shenshou,
R.drawable.d_shiwang,
R.drawable.d_shuai,
R.drawable.d_shuijiao,
R.drawable.d_sikao,
R.drawable.d_taikaixin,
R.drawable.d_tanshou,
R.drawable.d_tian,
R.drawable.d_touxiao,
R.drawable.d_tu,
R.drawable.d_tuzi,
R.drawable.d_wabishi,
R.drawable.d_weiqu,
R.drawable.d_wu,
R.drawable.d_xiaoku,
R.drawable.d_xiongmao,
R.drawable.d_xixi,
R.drawable.d_xu,
R.drawable.d_yinxian,
R.drawable.d_yiwen,
R.drawable.d_youhengheng,
R.drawable.d_yun,
R.drawable.d_zhuakuang,
R.drawable.d_zhutou,
R.drawable.d_zuiyou,
R.drawable.d_zuohengheng,
R.drawable.f_geili,
R.drawable.f_hufen,
R.drawable.f_jiong,
R.drawable.f_meng,
R.drawable.f_shenma,
R.drawable.f_v5,
R.drawable.f_xi,
R.drawable.f_zhi,
R.drawable.h_buyao,
R.drawable.h_good,
R.drawable.h_haha,
R.drawable.h_jiayou,
R.drawable.h_lai,
R.drawable.h_ok,
R.drawable.h_quantou,
R.drawable.h_ruo,
R.drawable.h_woshou,
R.drawable.h_ye,
R.drawable.h_zan,
R.drawable.h_zuoyi,
R.drawable.l_shangxin,
R.drawable.l_xin,
R.drawable.lxh_haoaio,
R.drawable.lxh_haoxihuan,
R.drawable.lxh_oye,
R.drawable.lxh_qiuguanzhu,
R.drawable.lxh_toule,
R.drawable.lxh_xiaohaha,
R.drawable.lxh_xiudada,
R.drawable.lxh_zana,
R.drawable.o_dangao,
R.drawable.o_feiji,
R.drawable.o_ganbei,
R.drawable.o_huatong,
R.drawable.o_lazhu,
R.drawable.o_liwu,
R.drawable.o_lvsidai,
R.drawable.o_weibo,
R.drawable.o_weiguan,
R.drawable.o_yinyue,
R.drawable.o_zhaoxiangji,
R.drawable.o_zhong,
R.drawable.w_fuyun,
R.drawable.w_shachenbao,
R.drawable.w_taiyang,
R.drawable.w_weifeng,
R.drawable.w_xianhua,
R.drawable.w_xiayu,
R.drawable.w_yueliang
};
private static String[] icons_name = new String[]{
"[爱你]",
"[奥特曼]",
"[拜拜]",
"[抱抱]",
"[悲伤]",
"[鄙视]",
"[闭嘴]",
"[馋嘴]",
"[吃惊]",
"[打哈气]",
"[打脸]",
"[顶]",
"[doge]",
"[二哈]",
"[肥皂]",
"[感冒]",
"[鼓掌]",
"[哈哈]",
"[害羞]",
"[汗]",
"[呵呵]",
"[黑线]",
"[哼]",
"[坏笑]",
"[花心]",
"[挤眼]",
"[可爱]",
"[可怜]",
"[酷]",
"[骷髅]",
"[困]",
"[懒得理你]",
"[浪]",
"[泪]",
"[喵喵]",
"[男孩儿]",
"[怒]",
"[怒骂]",
"[女孩儿]",
"[钱]",
"[亲亲]",
"[傻眼]",
"[生病]",
"[草泥马]",
"[失望]",
"[衰]",
"[睡觉]",
"[思考]",
"[太开心]",
"[摊手]",
"[舔屏]",
"[偷笑]",
"[吐]",
"[兔子]",
"[挖鼻屎]",
"[委屈]",
"[污]",
"[笑cry]",
"[熊猫]",
"[嘻嘻]",
"[嘘]",
"[阴险]",
"[疑问]",
"[右哼哼]",
"[晕]",
"[抓狂]",
"[猪头]",
"[最右]",
"[左哼哼]",
"[给力]",
"[互粉]",
"[囧]",
"[萌]",
"[神马]",
"[威武]",
"[喜]",
"[织毛线]",
"[NO]",
"[good]",
"[haha]",
"[加油]",
"[来]",
"[ok]",
"[拳头]",
"[弱]",
"[握手]",
"[耶]",
"[赞]",
"[作揖]",
"[伤心]",
"[心]",
"[好爱哦]",
"[好喜欢]",
"[噢耶]",
"[求关注]",
"[偷乐]",
"[笑哈哈]",
"[羞嗒嗒]",
"[赞啊]",
"[蛋糕]",
"[飞机]",
"[干杯]",
"[话筒]",
"[蜡烛]",
"[礼物]",
"[绿丝带]",
"[围脖]",
"[围观]",
"[音乐]",
"[照相机]",
"[钟]",
"[浮云]",
"[沙尘暴]",
"[太阳]",
"[微风]",
"[鲜花]",
"[下雨]",
"[月亮]"
};
private static final EaseEmojiconGroupEntity DATA = createData();
private static EaseEmojiconGroupEntity createData() {
EaseEmojiconGroupEntity emojiconGroupEntity = new EaseEmojiconGroupEntity();
EaseEmojicon[] datas = new EaseEmojicon[icons.length];
for (int i = 0; i < icons.length; i++) {
datas[i] = new EaseEmojicon(icons[i], icons_name[i], Type.BIG_EXPRESSION);
datas[i].setBigIcon(icons[i]);
//you can replace this to any you want
datas[i].setName(icons_name[i]);
datas[i].setIdentityCode("sina" + (1000 + i + 1));
}
emojiconGroupEntity.setEmojiconList(Arrays.asList(datas));
emojiconGroupEntity.setIcon(R.drawable.ee_3);
emojiconGroupEntity.setType(Type.BIG_EXPRESSION);//设置类型,如果是nomal就可以输入输入框
return emojiconGroupEntity;
}
public static EaseEmojiconGroupEntity getData() {
return DATA;
}
}
datas[i] = new EaseEmojicon(icons[i], icons_name[i], Type.BIG_EXPRESSION);
emojiconGroupEntity.setType(Type.BIG_EXPRESSION);
主要是这里要设置下type类型,这里以BIG_EXPRESSION形式,如何设置成nomal的话,发出来的是纯文字的,不能显示表情的,需要用的话,得修改easeui里的东西,不推荐。一般需要加新表情都是已大图形式的,如果需要纯文字,可以加在默认的(第一组)表情里!
2.第二部
找到ChatFragment,找到((EaseEmojiconMenu)inputMenu.getEmojiconMenu()).addEmojiconGroup(EmojiconExampleGroupData.getData());
复制一份,修改成
((EaseEmojiconMenu) inputMenu.getEmojiconMenu()).addEmojiconGroup(EmojiconSinaGroupData.getData());
加在这句后面。
3.第三部
找到DemoHelper,找到easeUI.setEmojiconInfoProvider,写成如下:
//set emoji icon provider最后写完了,希望能帮到和我这样的小白!!!
easeUI.setEmojiconInfoProvider(new EaseEmojiconInfoProvider() {
@Override
public EaseEmojicon getEmojiconInfo(String emojiconIdentityCode) {
//第一种表情
EaseEmojiconGroupEntity data = EmojiconExampleGroupData.getData();
for (EaseEmojicon emojicon : data.getEmojiconList()) {
if (emojicon.getIdentityCode().equals(emojiconIdentityCode)) {
return emojicon;
}
}
//新增的第二种表情
EaseEmojiconGroupEntity data_sina = EmojiconSinaGroupData.getData();
for (EaseEmojicon emojicon : data_sina.getEmojiconList()) {
if (emojicon.getIdentityCode().equals(emojiconIdentityCode)) {
return emojicon;
}
}
return null;
}
@Override
public Map<String, Object> getTextEmojiconMapping() {
return null;
}
});
收起阅读 »