深度学习

深度学习

7
评论

【源码下载】一款使用环信实现的开源灵魂社交APP(含服务器) 猿匹配 开源

beyond 发表了文章 • 3851 次浏览 • 2019-07-01 10:48 • 来自相关话题

#前言
近期,环信热心开发者-穿裤衩闯天下使用环信IM开发了一款实时聊天应用,包含简单的服务器端,现在正式开源给小伙伴们。感兴趣的同学可以一起搞一下哦,详细介绍请往下看。






  上代码
服务器:VMServer
客户端:VMMatch
 
 #VMMatch
猿匹配 —— 国内首个程序猿非严肃婚恋交友应用,让我们一言不合就来场匹配吧
 
#介绍#
首先说下中文名:为什么叫这个名字呢,因为这是一个程序猿(媛)之间匹配交流的应用啊其实这是一个使用环信 IM 开发的一款开源聊天项目,涵盖了时下流行的一些聊天元素,同时已将 IM 功能封装为单独库,可以直接引用,方便使用
项目还处在初期阶段,还有许多功能需要实现,有兴趣的可以一起来
项目资源均来自于互联网,如果有侵权请联系我
 
 #下载体验
猿匹配 小米商店 审核中
猿匹配 Google Play
 
  #项目截图

























  
 #开发环境
项目基本属于在最新的Android开发环境下开发,使用Java8的一些新特性,比如Lambda表达式,
然后项目已经适配Android6.x以上的动态权限适配,以及7.x的文件选择,和8.x的通知提醒等;
· Mac OS 10.14.4
· Android Studio 3.3.2
  #项目模块儿
本项目包含两部分:
一部分是项目主模块app,这部分主要包含了项目的业务逻辑,比如匹配、信息修改、设置等
另一部分是封装成library的vmim,这是为了方便大家引用到自己的项目中做的一步封装,不用再去复杂的复制代码和资源等,
只需要将vmim以module导入到自己的项目中就行了,具体使用方式参见项目app模块儿;
 
  #功能与 TODO
IM部分功能
· [x] 链接监听
· [x] 登录注册
· [x] 会话功能
      。[x] 置顶
      。[x] 标为未读
      。[x] 删除与清空
      。[x] 草稿功能
· [x] 消息功能
      。[x] 下拉加载更多
      。[x] 消息复制(仅文字类消息)
      。[x] 消息删除
      。[x] 文本+Emoji消息收发
      。[x] 大表情消息收发
      。[x] 图片消息
        ~[x] 查看大图
        ~[ ] 保存图片
      。[x] 语音消息
        ~[x] 语音录制
        ~[x] 语音播放(可暂停,波形待优化)
        ~[x] 听筒和扬声器播放切换
      。[x] 语音实时通话功能
      。[x] 视频实时通话功能
      。[x] 通话过程中的娱乐消息收发
        ~[x] 骰子
        ~[x] 石头剪刀布
        ~[x] 大表情
      。[x] 昵称头像处理(通过回调实现)
App部分功能
· [x] 登录注册(包括业务逻辑和 IM 逻辑)
· [x] 匹配
      。[x] 提交匹配信息
      。[x] 拉取匹配信息
· [x] 聊天(这里直接加载 IM 模块儿)
· [x] 我的
      。[x] 个人信息展示
      。[x] 上传头像
      。[x] 设置昵称
      。[x] 设置签名
· [x] 设置
      。[x] 个人信息设置
      。[x] 通知提醒
      。[x] 聊天
      。[ ] 隐私(随业务部分一起完善)
      。[ ] 通用(随业务部分一起完善)
      。[ ] 帮助反馈(随业务部分一起完善)
      。[x] 关于
      。[x] 退出
· [ ] 社区
      。[ ] 发布
      。[ ] 评论
      。[ ] 收藏
      。[ ] 关注
发布功能
· [x] 多渠道打包
· [x] 签名配置
· [x] 开发与线上环境配置
· [x] 敏感信息保护
 
  #配置运行
1.首先复制config.default.gradle到config.gradle
2.配置下config.gradle环信appkey以及bugly统计Id
3.正式打包需要配置下签名信息,同时将签名文件放置在项目根目录
 
  #参与贡献
如果你有什么好的想法,或者好的实现,可以通过下边的步骤参与进来,让我们一起把这个项目做得更好,欢迎参与
1.Fork本仓库
2.新建feature_xxx分支 (单独创建一个实现你自己想法的分支)
3.提交代码
4.新建Pull Request
5.等待我们的Review & Merge
 
 #关联项目
服务器端由nodejs实现,地址见这里 VMServer
 
  #VMServer
是为Android开源项目VMMatch项目(中文名猿匹配)实现的服务端
 
  #简介
这个项目包含两部分
· 根目录:服务逻辑及API接口实现
· client目录:前端界面,和服务器端代码端放置在同一仓库下(暂未实现)
 
 #使用
简单介绍下运行环境及部署方法
1.安装nodejs开发时使用的是v10.16.0版本
2.需要安装mongodb并启动,开发使用版本4.0.10
3.下载项目到服务器,可以下载压缩包,或者用git clone命令
4.复制config_default.js到config.js,可根据自己需要修改配置文件
5.安装依赖npm install
6.全局安装pm2npm install pm2 -g 
7.运行 vmshell.sh
 




扫码备注【开源项目】邀你加入环信开源社群
 
转载自https://blog.melove.net/develop-open-source-im-match-and-server/ 
  查看全部
#前言
近期,环信热心开发者-穿裤衩闯天下使用环信IM开发了一款实时聊天应用,包含简单的服务器端,现在正式开源给小伙伴们。感兴趣的同学可以一起搞一下哦,详细介绍请往下看。

猿匹配_logo_副本.png


  上代码
服务器:VMServer
客户端:VMMatch
 
 #VMMatch
猿匹配 —— 国内首个程序猿非严肃婚恋交友应用,让我们一言不合就来场匹配吧
 
#介绍#
首先说下中文名:为什么叫这个名字呢,因为这是一个程序猿(媛)之间匹配交流的应用啊其实这是一个使用环信 IM 开发的一款开源聊天项目,涵盖了时下流行的一些聊天元素,同时已将 IM 功能封装为单独库,可以直接引用,方便使用
项目还处在初期阶段,还有许多功能需要实现,有兴趣的可以一起来
项目资源均来自于互联网,如果有侵权请联系我
 
 #下载体验
猿匹配 小米商店 审核中
猿匹配 Google Play
 
  #项目截图

1.png

2.png

3.png

4.png

5.png

6.png

  
 #开发环境
项目基本属于在最新的Android开发环境下开发,使用Java8的一些新特性,比如Lambda表达式,
然后项目已经适配Android6.x以上的动态权限适配,以及7.x的文件选择,和8.x的通知提醒等;
· Mac OS 10.14.4
· Android Studio 3.3.2
  #项目模块儿
本项目包含两部分:
一部分是项目主模块app,这部分主要包含了项目的业务逻辑,比如匹配、信息修改、设置等
另一部分是封装成library的vmim,这是为了方便大家引用到自己的项目中做的一步封装,不用再去复杂的复制代码和资源等,
只需要将vmim以module导入到自己的项目中就行了,具体使用方式参见项目app模块儿;
 
  #功能与 TODO
IM部分功能
· [x] 链接监听
· [x] 登录注册
· [x] 会话功能
      。[x] 置顶
      。[x] 标为未读
      。[x] 删除与清空
      。[x] 草稿功能
· [x] 消息功能
      。[x] 下拉加载更多
      。[x] 消息复制(仅文字类消息)
      。[x] 消息删除
      。[x] 文本+Emoji消息收发
      。[x] 大表情消息收发
      。[x] 图片消息
        ~[x] 查看大图
        ~[ ] 保存图片
      。[x] 语音消息
        ~[x] 语音录制
        ~[x] 语音播放(可暂停,波形待优化)
        ~[x] 听筒和扬声器播放切换
      。[x] 语音实时通话功能
      。[x] 视频实时通话功能
      。[x] 通话过程中的娱乐消息收发
        ~[x] 骰子
        ~[x] 石头剪刀布
        ~[x] 大表情
      。[x] 昵称头像处理(通过回调实现)
App部分功能
· [x] 登录注册(包括业务逻辑和 IM 逻辑)
· [x] 匹配
      。[x] 提交匹配信息
      。[x] 拉取匹配信息
· [x] 聊天(这里直接加载 IM 模块儿)
· [x] 我的
      。[x] 个人信息展示
      。[x] 上传头像
      。[x] 设置昵称
      。[x] 设置签名
· [x] 设置
      。[x] 个人信息设置
      。[x] 通知提醒
      。[x] 聊天
      。[ ] 隐私(随业务部分一起完善)
      。[ ] 通用(随业务部分一起完善)
      。[ ] 帮助反馈(随业务部分一起完善)
      。[x] 关于
      。[x] 退出
· [ ] 社区
      。[ ] 发布
      。[ ] 评论
      。[ ] 收藏
      。[ ] 关注
发布功能
· [x] 多渠道打包
· [x] 签名配置
· [x] 开发与线上环境配置
· [x] 敏感信息保护
 
  #配置运行
1.首先复制config.default.gradle到config.gradle
2.配置下config.gradle环信appkey以及bugly统计Id
3.正式打包需要配置下签名信息,同时将签名文件放置在项目根目录
 
  #参与贡献
如果你有什么好的想法,或者好的实现,可以通过下边的步骤参与进来,让我们一起把这个项目做得更好,欢迎参与
1.Fork本仓库
2.新建feature_xxx分支 (单独创建一个实现你自己想法的分支)
3.提交代码
4.新建Pull Request
5.等待我们的Review & Merge
 
 #关联项目
服务器端由nodejs实现,地址见这里 VMServer
 
  #VMServer
是为Android开源项目VMMatch项目(中文名猿匹配)实现的服务端
 
  #简介
这个项目包含两部分
· 根目录:服务逻辑及API接口实现
· client目录:前端界面,和服务器端代码端放置在同一仓库下(暂未实现)
 
 #使用
简单介绍下运行环境及部署方法
1.安装nodejs开发时使用的是v10.16.0版本
2.需要安装mongodb并启动,开发使用版本4.0.10
3.下载项目到服务器,可以下载压缩包,或者用git clone命令
4.复制config_default.js到config.js,可根据自己需要修改配置文件
5.安装依赖
npm install

6.全局安装pm2
npm install pm2 -g
 
7.运行 vmshell.sh
 
环信冬冬_副本.jpg

扫码备注【开源项目】邀你加入环信开源社群
 
转载自https://blog.melove.net/develop-open-source-im-match-and-server/ 
 
4
评论

在微信小程序里实现聊天室 聊天室 小程序

Tolazy 发表了文章 • 5940 次浏览 • 2019-04-19 17:49 • 来自相关话题

第一次搞小程序,老板让我实现一个聊天室功能,压力山大啊。
花了几天时间研究比较了一下方案,最后基于环信的小程序SDK 开发了一个聊天室。
 
准备工作
下载环信 小程序demo+sdkgit clone https://github.com/easemob/webim-weixin-xcx创建一个文件夹,将 demo 中的文件 comps、images、sdk、utils 拷贝到新的文件,文件目录说明



集成
登录环信没什么可说的,这里选择的是使用 username/password 登录,和demo中的一样,文件没有进行任何更改


在app.js 中注册的 WebIM.conn.listen, 然后在 登陆成功的回调 onOpened 设置的跳转页面,并将登陆的 username 赋给 myName,传到新的页面中使用


修改 roomlist.js 获取聊天室列表,是分页获取的,这里先偷个懒,获取了第一页 20 个聊天室


然后将listChatrooms() 分别在onLoad、onShow 内,更改下,将原有的 listGroups() 替换掉然后在roomlist.wxml 修改对应的 变量绑定名称





demo中的group.js 中,获取到的是当前登陆账号已加入的群组,咱们做的是聊天室功能,所以需要有一个加入的操作,找roomlist.js 中找到 into_room: function (event),然后填写加入聊天室的方法, 我是直接在当前这个里面加的跳转到聊天页面,并将当前登陆的IDmyName,聊天室IDgroupID,聊天室名称your 传给新页面


Ex:监听是否加入聊天室成功的回调是在 onPresence 中,type:memberJoinChatRoomSuccess,正常是监听这个回调跳转页面,有点麻烦就直接这样吧到会话页面后,需要修改一下对应的消息格式,在comps/chat/suit 目录下,将里面的文件对应的 js 文件根据文档给聊天室发送消息 格式进行修改,聊天室消息和群组消息不同,所以我目前是直接将getSendToParam()、isGroupChat() 注释,改成下面这样,demo 中下面还有代码的,这里就用 …… 代替了





就这样了,简单集成聊天室功能,demo中的UI 是开源的,可以根据自己的需求更改~下面是具体实现过程。代码也放在github 上了,有需要的兄弟自取。demo下载地址:https://github.com/lizgDonkey/room-xcx 查看全部
第一次搞小程序,老板让我实现一个聊天室功能,压力山大啊。
花了几天时间研究比较了一下方案,最后基于环信的小程序SDK 开发了一个聊天室。
 
准备工作
  1. 下载环信 小程序demo+sdk
    git clone https://github.com/easemob/webim-weixin-xcx
  2. 创建一个文件夹,将 demo 中的文件 comps、images、sdk、utils 拷贝到新的文件,文件目录说明
    ml.png

集成
  1. 登录环信没什么可说的,这里选择的是使用 username/password 登录,和demo中的一样,文件没有进行任何更改
    login.png
  2. 在app.js 中注册的 WebIM.conn.listen, 然后在 登陆成功的回调 onOpened 设置的跳转页面,并将登陆的 username 赋给 myName,传到新的页面中使用
    tz.png
  3. 修改 roomlist.js 获取聊天室列表,是分页获取的,这里先偷个懒,获取了第一页 20 个聊天室
    getroom.png
    然后将listChatrooms() 分别在onLoad、onShow 内,更改下,将原有的 listGroups() 替换掉
  4. 然后在roomlist.wxml 修改对应的 变量绑定名称
    listui.png
    list.png
  5. demo中的group.js 中,获取到的是当前登陆账号已加入的群组,咱们做的是聊天室功能,所以需要有一个加入的操作,找roomlist.js 中找到 into_room: function (event),然后填写加入聊天室的方法, 我是直接在当前这个里面加的跳转到聊天页面,并将当前登陆的IDmyName,聊天室IDgroupID,聊天室名称your 传给新页面
    joinrom.png
    Ex:监听是否加入聊天室成功的回调是在 onPresence 中,type:memberJoinChatRoomSuccess,正常是监听这个回调跳转页面,有点麻烦就直接这样吧
  6. 到会话页面后,需要修改一下对应的消息格式,在comps/chat/suit 目录下,将里面的文件对应的 js 文件根据文档给聊天室发送消息 格式进行修改,聊天室消息和群组消息不同,所以我目前是直接将getSendToParam()、isGroupChat() 注释,改成下面这样,demo 中下面还有代码的,这里就用 …… 代替了
    send.png
    chat.png
    就这样了,简单集成聊天室功能,demo中的UI 是开源的,可以根据自己的需求更改~下面是具体实现过程。代码也放在github 上了,有需要的兄弟自取。demo下载地址:https://github.com/lizgDonkey/room-xcx

13
回复

收集基于环信SDK开发的开源项目 开源项目

xiaoyan2015 回复了问题 • 13 人关注 • 18746 次浏览 • 2019-03-14 13:52 • 来自相关话题

4
评论

【开源项目】全国首个开源直播小程序源码

beyond 发表了文章 • 126896 次浏览 • 2018-07-20 17:30 • 来自相关话题

今天你看直播了吗?拥有10亿微信生态用户的小程序已经成为了继移动互联后的又一个现象级风口,随着微信小程序对外开放实时音视频录制及播放等更多连接能力,小程序与直播强强联合,在各行各业找到了非常多的玩法,小程序直播相比微信直播和APP直播更加简洁、流畅、低延时、多入口等众多优势迅速向商业直播领域及泛娱乐直播领域蔓延。从小游戏、内容付费、工具、大数据、社交电商创业者到传统品牌商们,都在努力搭上小程序直播这辆快车,以免错过微信生态里新的流量洼地。
 





作为一名环信生态圈资深开发者,本着对技术的热衷,对环信的眷恋和对党的忠诚,基于环信即时通讯云写了“直播购物小程序”,目前项目源码已全部免费开放,希望对有需求的企业和开发者提供一个思路和参考。
直播购物小程序源码github地址:https://github.com/YuTongNetworkTechnology/wechat_live/tree/master 
git打不开可直接点下面链接下载


小程序直播demo_2018-06-21.zip







直播购物小程序运行预览图 
 
小程序体验指南(仅需两步):
 
1、下载微信小程序开发工具,下载地址:https://developers.weixin.qq.c ... .html 
 




2、导入源码:将附件的源码解压直接导入 







环信小程序直播技术文档
一、 使用的技术
1、 环信IM直播室。
2、 微信小程序实时音视频播放组件live-player。
3、 推流软件(obs、易推流)等推流。
4、 视频流服务器(UCLOUD、七牛、腾讯)等视频流服务器。
二、 系统使用流程。
1、 视频推流软件将视频流推到流服务器。
2、 打开视频直播demo小程序注册环信账号。
3、 进入软件直播室进行测试。
三、 技术流程及使用的SDk
1、 注册环信账号
打开https://www.easemob.com/ 环信官网,点击右上角注册按钮,选择[注册即时通讯云]




填写对相关信息进行注册





注册成功后进行登录




注:新注册用户需进行账号的认证。
2、 直播应用创建
登录成功点击应用列表选择创建应用




输入应用名称等信息
 





创建成功后点击应用进入





需要注意的是应用的OrgName 和AppName这两个是以后都需要用到的两个参数变量




3、 直播创建
1)在创建直播之前需要对应用进行设置首先需要设置应用的直播流地址
第一步获取应用管理员的Tokencurl -X POST "https://a1.easemob.com/[应用OrgName]/[应用AppName]/token" -d '{"grant_type":"client_credentials","client_id":"[应用client_id]","client_secret":"[应用] client_secret"}'返回格式{
"access_token":"YWMtWY779DgJEeS2h9OR7fw4QgAAAUmO4Qukwd9cfJSpkWHiOa7MCSk0MrkVIco",
"expires_in":5184000,
"application":"c03b3e30-046a-11e4-8ed1-5701cdaaa0e4"












第二步设置直播流地址curl -X POST -H "Authorization: Bearer [管理员Token]" " https://a1.easemob.com/[应用OrgName]/[应用AppName]/liverooms/stream_url -d '{"pc_pull":"[pc拉流地址]","pc_push":"[pc推流地址]","mobile_pull":"[手机拉流地址]","mobile_push":"[手机推流地址]"}'"成功返回格式:{
"action": "post",
"application": "e1a09de0-0e03-11e7-ad8e-a1d913615409",
"uri": "http://127.0.0.1:8080/easemob- ... ot%3B,
"entities": [ ],
"data": {
"pc_pull": true,
"mobile_push": true,
"mobile_pull": true,
"pc_push": true
},
"timestamp": 1494084474885,
"duration": 1,
"organization": "easemob-demo",
"applicationName": "chatdemoui"
}












2)创建主播
点击IM用户





点击注册IM用户





填写用户信息





创建用户的过程同样也可以通过REST API形式进行curl -X POST -i " https://a1.easemob.com/[应用OrgName]/[应用AppName]/users" -d '{"username":"[用户名]","password":"[密码]"}'
注:应用必须为开放注册





将注册的用户添加为主播curl -X POST -H "Authorization: [管理员Token]" https://a1.easemob.com/[应用OrgName]/[应用AppName]/super_admin -d'{"superadmin":"[IM用户名]"}'返回结果示例:{
"action": "post",
"application": "4d7e4ba0-dc4a-11e3-90d5-e1ffbaacdaf5",
"uri": "http://127.0.0.1:8080/easemob- ... ot%3B,
"entities": [ ],
"data": {
"result": "success"
},
"timestamp": 1496236798886,
"duration": 0,
"organization": "easemob-demo",
"applicationName": "chatdemoui"
}












3)创建直播
点击直播





点击新建房间





填写房间信息




创建房间同时也可以使用REST API形式进行详情可以查看http://docs.easemob.com/im/live/server-integration环信官方文档。
4、 小程序demo集成使用
小程序直播购物demo集成官方WebIM SDK详情请查看https://github.com/easemob/webim-weixin-xcx
Demo具体配置如下
打开demo 下sdk配置文件





修改appkey为自己应用的appkey





打开pages/live/index.js修改房间默认拉流地址及直播间房间号





四、 扩展说明
Demo中房间为固定测试房间,实际使用中应获取环信直播的房间信息及房间列表。具体如下:
获取直播间列表:curl -X GET -H "Authorization: Bearer [用户Token]" https://a1.easemob.com/[应用OrgName]/[应用AppName]/liverooms?ongoing=true&limit=[获取数量]&cursor=[游标地址(不填写为充开始查询)]
响应:{
"action": "get",
"application": "4d7e4ba0-dc4a-11e3-90d5-e1ffbaacdaf5",
"params": {
"cursor": [
"ZGNiMjRmNGY1YjczYjlhYTNkYjk1MDY2YmEyNzFmODQ6aW06Y2hhdHJvb206ZWFzZW1vYi1kZW1vI2NoYXRkZW1vdWk6MzE"
],
"ongoing": [
"true"
],
"limit": [
"2"
]
},
"uri": "http://127.0.0.1:8080/easemob- ... ot%3B,
"entities": [ ],
"data": [
{
"id": "1924",
"chatroom_id": "17177265635330",
"title": "具体了",
"desc": "就咯",
"startTime": 1495779917352,
"endTime": 1495779917352,
"anchor": "wuls",
"gift_count": 0,
"praise_count": 0,
"current_user_count": 8,
"max_user_count": 9,
"status": "ongoing",
"cover_picture_url": "",
"pc_pull_url": "rtmp://vlive3.rtmp.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1924_1",
"pc_push_url": "rtmp://publish3.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1924_1",
"mobile_pull_url": "rtmp://vlive3.rtmp.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1924_1",
"mobile_push_url": "rtmp://publish3.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1924_1"
},
{
"id": "1922",
"chatroom_id": "17175003856897",
"title": "香山",
"desc": "随便",
"startTime": 1495777760957,
"endTime": 1495777760957,
"anchor": "sx001",
"gift_count": 0,
"praise_count": 8,
"current_user_count": 1,
"max_user_count": 3,
"status": "ongoing",
"cover_picture_url": "http://127.0.0.1:8080/easemob- ... ot%3B,
"pc_pull_url": "rtmp://vlive3.rtmp.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1922_1",
"pc_push_url": "rtmp://publish3.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1922_1",
"mobile_pull_url": "rtmp://vlive3.rtmp.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1922_1",
"mobile_push_url": "rtmp://publish3.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1922_1"
}
],
"timestamp": 1496303336669,
"duration": 0,
"organization": "easemob-demo",
"applicationName": "chatdemoui",
"cursor": "ZGNiMjRmNGY1YjczYjlhYTNkYjk1MDY2YmEyNzFmODQ6aW06Y2hhdHJvb206ZWFzZW1vYi1kZW1vI2NoYXRkZW1vdWk6NDk",
"count": 2
}












获取直播间详情:curl -X GET -H "Authorization: Bearer [用户Token]" " https://a1.easemob.com/[应用OrgName]/[应用AppName]/[房间id]/status"响应:{
"action": "get",
"application": "4d7e4ba0-dc4a-11e3-90d5-e1ffbaacdaf5",
"uri": "http://127.0.0.1:8080/easemob- ... ot%3B,
"entities": [ ],
"data": {
"liveRoomID": "1946",
"status": "ongoing"
},
"timestamp": 1496234759930,
"duration": 0,
"organization": "easemob-demo",
"applicationName": "chatdemoui",
"count": 0
}














 
使用环信直播购物小程序遇到任何问题欢迎跟帖讨论。 查看全部
今天你看直播了吗?
拥有10亿微信生态用户的小程序已经成为了继移动互联后的又一个现象级风口,随着微信小程序对外开放实时音视频录制及播放等更多连接能力,小程序与直播强强联合,在各行各业找到了非常多的玩法,小程序直播相比微信直播和APP直播更加简洁、流畅、低延时、多入口等众多优势迅速向商业直播领域及泛娱乐直播领域蔓延。从小游戏、内容付费、工具、大数据、社交电商创业者到传统品牌商们,都在努力搭上小程序直播这辆快车,以免错过微信生态里新的流量洼地。
 
微信图片_20180725162426.jpg


作为一名环信生态圈资深开发者,本着对技术的热衷,对环信的眷恋和对党的忠诚,基于环信即时通讯云写了“直播购物小程序”,目前项目源码已全部免费开放,希望对有需求的企业和开发者提供一个思路和参考。
直播购物小程序源码github地址:https://github.com/YuTongNetworkTechnology/wechat_live/tree/master 
git打不开可直接点下面链接下载



预览图.jpg

直播购物小程序运行预览图 
 
小程序体验指南(仅需两步):
 
1、下载微信小程序开发工具,下载地址:https://developers.weixin.qq.c ... .html 
 
Catch9A07(07-20-17-38-30).jpg

2、导入源码:将附件的源码解压直接导入 


Catch1C69(07-20-17-38-30).jpg


环信小程序直播技术文档
一、 使用的技术
1、 环信IM直播室。
2、 微信小程序实时音视频播放组件live-player。
3、 推流软件(obs、易推流)等推流。
4、 视频流服务器(UCLOUD、七牛、腾讯)等视频流服务器。
二、 系统使用流程。
1、 视频推流软件将视频流推到流服务器。
2、 打开视频直播demo小程序注册环信账号。
3、 进入软件直播室进行测试。
三、 技术流程及使用的SDk
1、 注册环信账号
打开https://www.easemob.com/ 环信官网,点击右上角注册按钮,选择[注册即时通讯云]
1.png

填写对相关信息进行注册

2.png

注册成功后进行登录
3.png

注:新注册用户需进行账号的认证。
2、 直播应用创建
登录成功点击应用列表选择创建应用
4.png

输入应用名称等信息
 

5.png

创建成功后点击应用进入

6.png

需要注意的是应用的OrgName 和AppName这两个是以后都需要用到的两个参数变量
7.png

3、 直播创建
1)在创建直播之前需要对应用进行设置首先需要设置应用的直播流地址
第一步获取应用管理员的Token
curl -X POST "https://a1.easemob.com/[应用OrgName]/[应用AppName]/token" -d '{"grant_type":"client_credentials","client_id":"[应用client_id]","client_secret":"[应用] client_secret"}'
返回格式
{
"access_token":"YWMtWY779DgJEeS2h9OR7fw4QgAAAUmO4Qukwd9cfJSpkWHiOa7MCSk0MrkVIco",
"expires_in":5184000,
"application":"c03b3e30-046a-11e4-8ed1-5701cdaaa0e4"












第二步设置直播流地址
curl -X POST -H "Authorization: Bearer [管理员Token]"  " https://a1.easemob.com/[应用OrgName]/[应用AppName]/liverooms/stream_url -d '{"pc_pull":"[pc拉流地址]","pc_push":"[pc推流地址]","mobile_pull":"[手机拉流地址]","mobile_push":"[手机推流地址]"}'"
成功返回格式:
{
"action": "post",
"application": "e1a09de0-0e03-11e7-ad8e-a1d913615409",
"uri": "http://127.0.0.1:8080/easemob- ... ot%3B,
"entities": [ ],
"data": {
"pc_pull": true,
"mobile_push": true,
"mobile_pull": true,
"pc_push": true
},
"timestamp": 1494084474885,
"duration": 1,
"organization": "easemob-demo",
"applicationName": "chatdemoui"
}












2)创建主播
点击IM用户

8.png

点击注册IM用户

9.png

填写用户信息

10.png

创建用户的过程同样也可以通过REST API形式进行
curl -X POST -i " https://a1.easemob.com/[应用OrgName]/[应用AppName]/users" -d '{"username":"[用户名]","password":"[密码]"}'

注:应用必须为开放注册

11.png

将注册的用户添加为主播
curl -X POST -H "Authorization: [管理员Token]"  https://a1.easemob.com/[应用OrgName]/[应用AppName]/super_admin -d'{"superadmin":"[IM用户名]"}'
返回结果示例:
{
"action": "post",
"application": "4d7e4ba0-dc4a-11e3-90d5-e1ffbaacdaf5",
"uri": "http://127.0.0.1:8080/easemob- ... ot%3B,
"entities": [ ],
"data": {
"result": "success"
},
"timestamp": 1496236798886,
"duration": 0,
"organization": "easemob-demo",
"applicationName": "chatdemoui"
}












3)创建直播
点击直播

12.png

点击新建房间

13.png

填写房间信息
14.png

创建房间同时也可以使用REST API形式进行详情可以查看http://docs.easemob.com/im/live/server-integration环信官方文档。
4、 小程序demo集成使用
小程序直播购物demo集成官方WebIM SDK详情请查看https://github.com/easemob/webim-weixin-xcx
Demo具体配置如下
打开demo 下sdk配置文件

15.png

修改appkey为自己应用的appkey

16.png

打开pages/live/index.js修改房间默认拉流地址及直播间房间号

17.png

四、 扩展说明
Demo中房间为固定测试房间,实际使用中应获取环信直播的房间信息及房间列表。具体如下:
获取直播间列表:
curl -X GET -H "Authorization: Bearer  [用户Token]"  https://a1.easemob.com/[应用OrgName]/[应用AppName]/liverooms?ongoing=true&limit=[获取数量]&cursor=[游标地址(不填写为充开始查询)]

响应:
{
"action": "get",
"application": "4d7e4ba0-dc4a-11e3-90d5-e1ffbaacdaf5",
"params": {
"cursor": [
"ZGNiMjRmNGY1YjczYjlhYTNkYjk1MDY2YmEyNzFmODQ6aW06Y2hhdHJvb206ZWFzZW1vYi1kZW1vI2NoYXRkZW1vdWk6MzE"
],
"ongoing": [
"true"
],
"limit": [
"2"
]
},
"uri": "http://127.0.0.1:8080/easemob- ... ot%3B,
"entities": [ ],
"data": [
{
"id": "1924",
"chatroom_id": "17177265635330",
"title": "具体了",
"desc": "就咯",
"startTime": 1495779917352,
"endTime": 1495779917352,
"anchor": "wuls",
"gift_count": 0,
"praise_count": 0,
"current_user_count": 8,
"max_user_count": 9,
"status": "ongoing",
"cover_picture_url": "",
"pc_pull_url": "rtmp://vlive3.rtmp.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1924_1",
"pc_push_url": "rtmp://publish3.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1924_1",
"mobile_pull_url": "rtmp://vlive3.rtmp.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1924_1",
"mobile_push_url": "rtmp://publish3.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1924_1"
},
{
"id": "1922",
"chatroom_id": "17175003856897",
"title": "香山",
"desc": "随便",
"startTime": 1495777760957,
"endTime": 1495777760957,
"anchor": "sx001",
"gift_count": 0,
"praise_count": 8,
"current_user_count": 1,
"max_user_count": 3,
"status": "ongoing",
"cover_picture_url": "http://127.0.0.1:8080/easemob- ... ot%3B,
"pc_pull_url": "rtmp://vlive3.rtmp.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1922_1",
"pc_push_url": "rtmp://publish3.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1922_1",
"mobile_pull_url": "rtmp://vlive3.rtmp.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1922_1",
"mobile_push_url": "rtmp://publish3.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1922_1"
}
],
"timestamp": 1496303336669,
"duration": 0,
"organization": "easemob-demo",
"applicationName": "chatdemoui",
"cursor": "ZGNiMjRmNGY1YjczYjlhYTNkYjk1MDY2YmEyNzFmODQ6aW06Y2hhdHJvb206ZWFzZW1vYi1kZW1vI2NoYXRkZW1vdWk6NDk",
"count": 2
}












获取直播间详情:
curl -X GET -H "Authorization: Bearer [用户Token]" " https://a1.easemob.com/[应用OrgName]/[应用AppName]/[房间id]/status"
响应:
{
"action": "get",
"application": "4d7e4ba0-dc4a-11e3-90d5-e1ffbaacdaf5",
"uri": "http://127.0.0.1:8080/easemob- ... ot%3B,
"entities": [ ],
"data": {
"liveRoomID": "1946",
"status": "ongoing"
},
"timestamp": 1496234759930,
"duration": 0,
"organization": "easemob-demo",
"applicationName": "chatdemoui",
"count": 0
}














 
使用环信直播购物小程序遇到任何问题欢迎跟帖讨论。
19
评论

【新手快速入门】集成环信常见问题+解决方案汇总

dujiepeng 发表了文章 • 24396 次浏览 • 2017-05-22 15:51 • 来自相关话题

   这里整理了集成环信的常见问题和一些功能的实现思路,希望能帮助到大家。感谢热心的开发者贡献,大家在观看过程中有不明白的地方欢迎直接跟帖咨询。
 
ios篇
APNs证书创建和上传到环信后台头像昵称的简述和处理方案音视频离线推送Demo实现环信服务器聊天记录保存多久?离线收不到好友请求IOS中环信聊天窗口如何实现文件发送和预览的功能ios集成常见问题环信推送的一些常见问题实现名片|红包|话题聊天室等自定义cell
 
Android篇
Android sdk 的两种导入方式环信3.0SDK集成小米推送教程EaseUI库中V4、v7包冲突解决方案Android EaseUI里的百度地图替换为高德地图android扩展消息(名片集成)关于会话列表的置顶聊天java.lang.UnsatisfiedLinkError: 的问题android 端 app 后台被杀死收不到消息的解决方案
昵称头像篇
android中如何显示开发者服务器上的昵称和头像 Android中显示头像(接上一篇文章看)环信(Android)设置头像和昵称的方法(最简单暴力的基于环信demo的集成)IOS中如何显示开发者服务器上的昵称和头像【环信公开课第12期视频回放】-所有关于环信IM昵称头像的问题听这课就够了
 
直播篇
一言不合你就搞个直播APP
 
客服集成
IM-SDK和客服SDK并存开发指南—Android篇IM-SDK和客服SDK并存开发指南—iOS篇
 
开源项目
Android简版demoios简版demo凡信2.0:超仿微信的开源项目 凡信3.0:携直播和红包而来高仿微信:Github 3,515 Star方圆十里:环信编程大赛冠军项目泛聊:定一个小目标写一个QQSlack聊天机器人:一天时间做一个聊天机器人TV视频通话:在电视上视频通话视频通话:Android手机视频通话酷信:ios高仿微信公众号助手:与订阅用户聊天沟通
 
持续更新ing...小伙伴们还有什么想知道欢迎跟帖提出。
  查看全部
   这里整理了集成环信的常见问题和一些功能的实现思路,希望能帮助到大家。感谢热心的开发者贡献,大家在观看过程中有不明白的地方欢迎直接跟帖咨询。
 
ios篇

 
Android篇

昵称头像篇

 
直播篇
  1. 一言不合你就搞个直播APP

 
客服集成
  1. IM-SDK和客服SDK并存开发指南—Android篇
  2. IM-SDK和客服SDK并存开发指南—iOS篇

 
开源项目

 
持续更新ing...小伙伴们还有什么想知道欢迎跟帖提出。
 
0
评论

如何用 30 天入门年薪 30 万的技术领域? 技术 深度学习

sunshine123 发表了文章 • 91 次浏览 • 2019-10-08 17:18 • 来自相关话题

2016年3月15日,人机大战第五场在韩国首尔进行,经过长达5个小时搏杀,李世石认输,最终李世石与AlphaGo总比分定格在1比4。
值得注意的是:李世石仅在3月15日结束的第四场对战中取得了唯一的一场胜利。而支撑机器取胜的核心技术,就是深度学习。
自此之后,深度学习开始逐渐进入大家的视野。

01
什么是深度学习?

深度学习其实是机器学习(Machine Learning)的一个分支学科,而机器学习主要是研究数据之言的关系的,比如它可以用来分析性别、年龄、学历、职业等因素之间的数学关系。
显而易见,这些因果关系并不是一个简单的线性关系就能解决的。但是深度学习通过多非线性模型表示数据之间的关系,从而确定数据之间的相互关系是什么。

02
深度学习已经融入到了人们的日常生活中

现在汽车的自动驾驶、短信邮件的自动回复、扫地机器人,以及在围棋中击败最优秀人类选手的软件,都应用到了深度学习。甚至骚扰电话,也有很多是AI机器人打过来的,可能电话那头和你聊得正酣、声音甜美的客服妹子只是一串代码。
因此,阿里达摩院的工程师们,受不了骚扰骚扰电话的骚扰,发起了“二哈”AI,让机器人与骚扰电话自动聊天。
大家感受一下这对话画风





 
总之,AI机器人的骚扰电话,正在用AI进行解决。

03

可能是编程领域薪资最高的岗位

而与此同时学习深度学习的人也开始变得多了起来,但由于深度学习涉及面比较广,很多学者会对此望而却步。然而如果学好深度学习,不仅仅可以给你带来高薪资,也有可能会给你带来很多的荣誉!因为在此领域还是非常缺乏人才的!
大家可以看一份数据





 
平均月薪已达到3w,年薪36w。





 
就应届生深度学习岗的薪资,都已经达到了很多其他互联网职业3年工作经验都难以企及的水平。
现在各个大厂,也都有深度学习相关的项目,而且待遇方面也是极其的诱人。
百度推出“少帅计划”,针对30岁以下的深度学习科学家,开出100万以上年薪
阿里巴巴对外宣布将通过校园招聘组建一支规模达数百人的 NASA“青年军”
华为更是开出200万年薪招聘应届毕业生,其专业均为最前沿的人工智能领域
作为一个有追求的程序员,相信你在脑海中闪过无数次这样的疑问:如何掌握深度学习?

04

如何掌握深度学习?

目前市场对于深度学习的需求,早已出现供不应求的现象,但太多工程师想入门或者转行却不知从何开始,很多初学者都会有这样的困惑:
一边要熟练掌握线性代数、矩阵计算,一边还要搞概率论
一边要去研究各种库与框架,一边学习如何在用编程语言实现算法
一边学习如何制作数据集、特征提取,一边微调参数,选择合适的算法
这样一轮下来,深度学习还没有开始就已经走上了放弃之路。
放眼网上现有的一些深度学习课程,经常会发现有些知识点覆盖不全,或者学习门槛较高,研究性的问题不多,或者说只关注面试,而忽略了底层的逻辑以及真实案例的实操。
因此,我们通上百名深度学习用户的调研,与老师花费上千小时的时间,提炼了102节精华课程。
我们希望通过“从零开始深度学习”这门课程,让你0基础入门深度学习,建立起完整的学习路径,同时通过“智能问答模型”的实战案例,将所学知识学以致用。
#购课即送王海良老师在京东原价69的《智能问答与深度学习》实体书一本#

讲师介绍
王海良:Chatopera联合创始人&CEO,微软人工智能最有价值专家,先后工作于IBM软件开发实验室和创新中心。
李卓桓:25年编程经验,曾任优酷网首席科学家、叽歪网创始人,水木清华BBS站长,紫霞BBS站长。
林旭鸣:北京邮电大学模式识别实验室研究生,任职阿里巴巴的阿里小蜜团队。
陈可心:香港大学硕士,任职经历包括:微软中国、今日头条研发中心,联想香港人工智能中心以及联合国亚太分部。
李思珍:现任职今日头条,主要工作实现人机交互系统的意图识别和关键词优化。

#课程介绍#
相信很多小伙伴,在入门深度学习的路上,会被一些数据基础搞得犯怵,搭建环境的时候也许会遇到很多坑。
因此,我们把课程分为五个模块:数学基础、Python编程语言基础、深度学习初步、深度学习深化、智能问答模型实践,带你一探深度学习的究竟,希望你通过此次学习,不仅掌握原理更能动手实操。
除了上述102节视频课程,我们还为这次课程搭配了书籍《深度学习与智能问答》,一边跟着视频实操,一边阅读书籍巩固,学习效果double~





 
如何保证你的学习效果?
由基础的数学学习开始,逐渐建立完整的深度学习知识体系
五大模块,由知识学习到真实案例的实践
购课送原价69元深度学习实体书,边学边读,加深记忆
建立一个互助、监督的高效学习社群,随时交流问题

购课须知
本课程包含哪些内容?
包含102节视频课(20+小时)+实体书(京东原价69)

上课形式是怎样的?
课程授课形式为:视频+群答疑

#如何报名#
限时特惠:99元(原价199元)
拼团特惠69元~
图书(含邮寄) + 100+节视频课程,每课仅需6毛钱

扫码立即抢购!
平均一天2元,就是一瓶水钱
坚持30天,换你一次进入高薪技术领域的机会
如果对课程有疑问
欢迎扫码回复“1”进课程咨询群





 
 
  查看全部
2016年3月15日,人机大战第五场在韩国首尔进行,经过长达5个小时搏杀,李世石认输,最终李世石与AlphaGo总比分定格在1比4。
值得注意的是:李世石仅在3月15日结束的第四场对战中取得了唯一的一场胜利。而支撑机器取胜的核心技术,就是深度学习。
自此之后,深度学习开始逐渐进入大家的视野。

01
什么是深度学习?


深度学习其实是机器学习(Machine Learning)的一个分支学科,而机器学习主要是研究数据之言的关系的,比如它可以用来分析性别、年龄、学历、职业等因素之间的数学关系。
显而易见,这些因果关系并不是一个简单的线性关系就能解决的。但是深度学习通过多非线性模型表示数据之间的关系,从而确定数据之间的相互关系是什么。

02
深度学习已经融入到了人们的日常生活中


现在汽车的自动驾驶、短信邮件的自动回复、扫地机器人,以及在围棋中击败最优秀人类选手的软件,都应用到了深度学习。甚至骚扰电话,也有很多是AI机器人打过来的,可能电话那头和你聊得正酣、声音甜美的客服妹子只是一串代码。
因此,阿里达摩院的工程师们,受不了骚扰骚扰电话的骚扰,发起了“二哈”AI,让机器人与骚扰电话自动聊天。
大家感受一下这对话画风

1.gif

 
总之,AI机器人的骚扰电话,正在用AI进行解决。

03

可能是编程领域薪资最高的岗位


而与此同时学习深度学习的人也开始变得多了起来,但由于深度学习涉及面比较广,很多学者会对此望而却步。然而如果学好深度学习,不仅仅可以给你带来高薪资,也有可能会给你带来很多的荣誉!因为在此领域还是非常缺乏人才的!
大家可以看一份数据

2.png

 
平均月薪已达到3w,年薪36w。

4.png

 
就应届生深度学习岗的薪资,都已经达到了很多其他互联网职业3年工作经验都难以企及的水平。
现在各个大厂,也都有深度学习相关的项目,而且待遇方面也是极其的诱人。
百度推出“少帅计划”,针对30岁以下的深度学习科学家,开出100万以上年薪
阿里巴巴对外宣布将通过校园招聘组建一支规模达数百人的 NASA“青年军”
华为更是开出200万年薪招聘应届毕业生,其专业均为最前沿的人工智能领域
作为一个有追求的程序员,相信你在脑海中闪过无数次这样的疑问:如何掌握深度学习?

04

如何掌握深度学习?


目前市场对于深度学习的需求,早已出现供不应求的现象,但太多工程师想入门或者转行却不知从何开始,很多初学者都会有这样的困惑:
一边要熟练掌握线性代数、矩阵计算,一边还要搞概率论
一边要去研究各种库与框架,一边学习如何在用编程语言实现算法
一边学习如何制作数据集、特征提取,一边微调参数,选择合适的算法
这样一轮下来,深度学习还没有开始就已经走上了放弃之路。
放眼网上现有的一些深度学习课程,经常会发现有些知识点覆盖不全,或者学习门槛较高,研究性的问题不多,或者说只关注面试,而忽略了底层的逻辑以及真实案例的实操。
因此,我们通上百名深度学习用户的调研,与老师花费上千小时的时间,提炼了102节精华课程。
我们希望通过“从零开始深度学习”这门课程,让你0基础入门深度学习,建立起完整的学习路径,同时通过“智能问答模型”的实战案例,将所学知识学以致用。
#购课即送王海良老师在京东原价69的《智能问答与深度学习》实体书一本#

讲师介绍
王海良:Chatopera联合创始人&CEO,微软人工智能最有价值专家,先后工作于IBM软件开发实验室和创新中心。
李卓桓:25年编程经验,曾任优酷网首席科学家、叽歪网创始人,水木清华BBS站长,紫霞BBS站长。
林旭鸣:北京邮电大学模式识别实验室研究生,任职阿里巴巴的阿里小蜜团队。
陈可心:香港大学硕士,任职经历包括:微软中国、今日头条研发中心,联想香港人工智能中心以及联合国亚太分部。
李思珍:现任职今日头条,主要工作实现人机交互系统的意图识别和关键词优化。

#课程介绍#
相信很多小伙伴,在入门深度学习的路上,会被一些数据基础搞得犯怵,搭建环境的时候也许会遇到很多坑。
因此,我们把课程分为五个模块:数学基础、Python编程语言基础、深度学习初步、深度学习深化、智能问答模型实践,带你一探深度学习的究竟,希望你通过此次学习,不仅掌握原理更能动手实操。
除了上述102节视频课程,我们还为这次课程搭配了书籍《深度学习与智能问答》,一边跟着视频实操,一边阅读书籍巩固,学习效果double~

5.jpg

 
如何保证你的学习效果?
由基础的数学学习开始,逐渐建立完整的深度学习知识体系
五大模块,由知识学习到真实案例的实践
购课送原价69元深度学习实体书,边学边读,加深记忆
建立一个互助、监督的高效学习社群,随时交流问题

购课须知
本课程包含哪些内容?
包含102节视频课(20+小时)+实体书(京东原价69)

上课形式是怎样的?
课程授课形式为:视频+群答疑

#如何报名#
限时特惠:99元(原价199元)
拼团特惠69元~
图书(含邮寄) + 100+节视频课程,每课仅需6毛钱

扫码立即抢购!
平均一天2元,就是一瓶水钱
坚持30天,换你一次进入高薪技术领域的机会
如果对课程有疑问
欢迎扫码回复“1”进课程咨询群

6.jpg

 
 
 
0
评论

李理:详解卷积神经网络 李理 深度学习

beyond 发表了文章 • 1905 次浏览 • 2017-03-06 16:46 • 来自相关话题

本系列文章面向深度学习研发者,希望通过Image Caption Generation,一个有意思的具体任务,深入浅出地介绍深度学习的知识。本系列文章涉及到很多深度学习流行的模型,如CNN,RNN/LSTM,Attention等。本文为第7篇。

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

相关文章: 
李理:从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
$ipython
>>> import network3
>>> from network3 import Network
>>> from network3 import ConvPoolLayer, FullyConnectedLayer, SoftmaxLayer
>>> training_data, validation_data, test_data = network3.load_data_shared()
>>> mini_batch_size = 10
>>> net = Network([
FullyConnectedLayer(n_in=784, n_out=100),
SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
>>> net.SGD(training_data, 60, mini_batch_size, 0.1,
validation_data, test_data)
得到的分类准确率是97.8%。这是在test_data上的准确率,这个模型使用训练数据训练,并根据validation_data来选择当前最好的模型。使用validation数据来可以避免过拟合。读者运行时可能结果会有一些差异,因为模型的参数是随机初始化的。
 
改进版本1
 
我们首先在输入的后面增加一个卷积层。我们使用5 5的局部感知域,stride等于1,20个特征映射。然后接一个2 2的max-pooling层。之后接一个全连接的层,最后是softmax(仿射变换加softmax):




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

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

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

如果要用gpu,可以把上面的命令保存到一个文件test.py,然后:$THEANO_FLAGS=mode=FAST_RUN,device=gpu,floatX=float32 python test.py 在这个网络结构中,我们吧卷积和池化层看出一个整体。这只是一种习惯。network3.py会把它们当成一个整体,每个卷积层后面都会跟一个池化层。但实际的一些卷积神经网络并不都要接池化层。
 
改进版本2
 
我们再加入第二个卷积-池化层。这个卷积层插入在第一个卷积层和全连接层中间。我们使用同样的5×5的局部感知域和2×2的max-pooling。代码如下:>>> net = Network([
ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28),
filter_shape=(20, 1, 5, 5),
poolsize=(2, 2)),
ConvPoolLayer(image_shape=(mini_batch_size, 20, 12, 12),
filter_shape=(40, 20, 5, 5),
poolsize=(2, 2)),
FullyConnectedLayer(n_in=40*4*4, n_out=100),
SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
>>> net.SGD(training_data, 60, mini_batch_size, 0.1,
validation_data, test_data) 【注意图片的大小,开始是(mini_batch_size, 1, 28 ,28),经过一个20个5 5的卷积池层后变成了(mini_batch_size, 20, 24,24),然后在经过2 2的max-pooling后变成了(mini_batch_size, 20, 12, 12)。然后是40个5*5的卷积层,变成了(mini_batch_size, 40, 8, 8),然后是max-pooling得到(mini_batch_size, 40, 4, 4)。然后是全连接的层】 
这个模型得到99.6%的准确率!

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

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

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

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

扩展数据。

深度学习非常依赖于数据。我们可以根据任务的特点”构造“新的数据。一种简单的方法是把训练数据里的数字进行一下平移,旋转等变换。虽然理论上卷积神经网络能学到与位置无关的特征,但如果训练数据里数字总是出现在固定的位置,实际的模型也不一定能学到。所以我们构造一些这样的数据效果会更好。$ python expand_mnist.pyexpand_mnist.py这个脚本就会扩展数据。它只是简单的把图片向上下左右各移动了一个像素。扩展后训练数据从50000个变成了250000个。
 
接下来我们用扩展后的数据来训练模型:>>> expanded_training_data, _, _ = network3.load_data_shared(
"../data/mnist_expanded.pkl.gz")
>>> net = Network([
ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28),
filter_shape=(20, 1, 5, 5),
poolsize=(2, 2),
activation_fn=ReLU),
ConvPoolLayer(image_shape=(mini_batch_size, 20, 12, 12),
filter_shape=(40, 20, 5, 5),
poolsize=(2, 2),
activation_fn=ReLU),
FullyConnectedLayer(n_in=40*4*4, n_out=100, activation_fn=ReLU),
SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
>>> net.SGD(expanded_training_data, 60, mini_batch_size, 0.03,
validation_data, test_data, lmbda=0.1)这个模型的准确率是99.37%。扩展数据看起来非常trival,但是却极大的提高了识别准确率。

改进版本5

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

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

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

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

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

改进版本6

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

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




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

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

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

更进一步

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

What’s Next?

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

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

相关文章: 
李理:从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条边(连接)。连接方式如下图所示:
001.jpg

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

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

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

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

局部感知域

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

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

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

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

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

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

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

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

共享权值

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

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

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

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

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

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

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

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

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

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

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

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

池化(Pooling)

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

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

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

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

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

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

放到一起

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

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

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

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

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

CNN实战

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

代码

首先得到代码:  git clone

安装theano

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

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

baseline

首先我们实现一个baseline的系统,我们构建一个只有一个隐藏层的3层全连接网络,隐藏层100个神经元。我们训练时60个epoch,使用learning rate $\eta = 0.1$,batch大小是10,没有正则化:
$cd src
$ipython
>>> import network3
>>> from network3 import Network
>>> from network3 import ConvPoolLayer, FullyConnectedLayer, SoftmaxLayer
>>> training_data, validation_data, test_data = network3.load_data_shared()
>>> mini_batch_size = 10
>>> net = Network([
FullyConnectedLayer(n_in=784, n_out=100),
SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
>>> net.SGD(training_data, 60, mini_batch_size, 0.1,
validation_data, test_data)

得到的分类准确率是97.8%。这是在test_data上的准确率,这个模型使用训练数据训练,并根据validation_data来选择当前最好的模型。使用validation数据来可以避免过拟合。读者运行时可能结果会有一些差异,因为模型的参数是随机初始化的。
 
改进版本1
 
我们首先在输入的后面增加一个卷积层。我们使用5 5的局部感知域,stride等于1,20个特征映射。然后接一个2 2的max-pooling层。之后接一个全连接的层,最后是softmax(仿射变换加softmax):
011.jpg

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

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

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

如果要用gpu,可以把上面的命令保存到一个文件test.py,然后:
$THEANO_FLAGS=mode=FAST_RUN,device=gpu,floatX=float32 python test.py 
在这个网络结构中,我们吧卷积和池化层看出一个整体。这只是一种习惯。network3.py会把它们当成一个整体,每个卷积层后面都会跟一个池化层。但实际的一些卷积神经网络并不都要接池化层。
 
改进版本2
 
我们再加入第二个卷积-池化层。这个卷积层插入在第一个卷积层和全连接层中间。我们使用同样的5×5的局部感知域和2×2的max-pooling。代码如下:
>>> net = Network([
ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28),
filter_shape=(20, 1, 5, 5),
poolsize=(2, 2)),
ConvPoolLayer(image_shape=(mini_batch_size, 20, 12, 12),
filter_shape=(40, 20, 5, 5),
poolsize=(2, 2)),
FullyConnectedLayer(n_in=40*4*4, n_out=100),
SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
>>> net.SGD(training_data, 60, mini_batch_size, 0.1,
validation_data, test_data)
【注意图片的大小,开始是(mini_batch_size, 1, 28 ,28),经过一个20个5 5的卷积池层后变成了(mini_batch_size, 20, 24,24),然后在经过2 2的max-pooling后变成了(mini_batch_size, 20, 12, 12)。然后是40个5*5的卷积层,变成了(mini_batch_size, 40, 8, 8),然后是max-pooling得到(mini_batch_size, 40, 4, 4)。然后是全连接的层】 
这个模型得到99.6%的准确率!

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

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

使用ReLU激活函数。ReLU的定义是:
ReLU(x)=max(0,x)
>>> from network3 import ReLU
>>> net = Network([
ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28),
filter_shape=(20, 1, 5, 5),
poolsize=(2, 2),
activation_fn=ReLU),
ConvPoolLayer(image_shape=(mini_batch_size, 20, 12, 12),
filter_shape=(40, 20, 5, 5),
poolsize=(2, 2),
activation_fn=ReLU),
FullyConnectedLayer(n_in=40*4*4, n_out=100, activation_fn=ReLU),
SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
>>> net.SGD(training_data, 60, mini_batch_size, 0.03,
validation_data, test_data, lmbda=0.1)

使用ReLU后准确率从99.06%提高到99.23%。从作者的经验来看,ReLU总是要比sigmoid激活函数要好。

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

扩展数据。

深度学习非常依赖于数据。我们可以根据任务的特点”构造“新的数据。一种简单的方法是把训练数据里的数字进行一下平移,旋转等变换。虽然理论上卷积神经网络能学到与位置无关的特征,但如果训练数据里数字总是出现在固定的位置,实际的模型也不一定能学到。所以我们构造一些这样的数据效果会更好。
$ python expand_mnist.py
expand_mnist.py这个脚本就会扩展数据。它只是简单的把图片向上下左右各移动了一个像素。扩展后训练数据从50000个变成了250000个。
 
接下来我们用扩展后的数据来训练模型:
>>> expanded_training_data, _, _ = network3.load_data_shared(
"../data/mnist_expanded.pkl.gz")
>>> net = Network([
ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28),
filter_shape=(20, 1, 5, 5),
poolsize=(2, 2),
activation_fn=ReLU),
ConvPoolLayer(image_shape=(mini_batch_size, 20, 12, 12),
filter_shape=(40, 20, 5, 5),
poolsize=(2, 2),
activation_fn=ReLU),
FullyConnectedLayer(n_in=40*4*4, n_out=100, activation_fn=ReLU),
SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
>>> net.SGD(expanded_training_data, 60, mini_batch_size, 0.03,
validation_data, test_data, lmbda=0.1)
这个模型的准确率是99.37%。扩展数据看起来非常trival,但是却极大的提高了识别准确率。

改进版本5

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

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

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

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

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


改进版本6

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

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

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

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

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

更进一步

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

What’s Next?

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

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

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

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

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

简介

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




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

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

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




首先来看导数的定义:




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




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




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




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




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

复杂表达式的链式法则

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




对y的求导也是类似的。

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

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

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




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

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

反向传播算法的直觉解释

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

Sigmoid模块的例子

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




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




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




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

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




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




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




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




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




上面我们看到把




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




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




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




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

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




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

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

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

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

Staged computation练习




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

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

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

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




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

(7) invden = 1.0 / den

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

(6) den = sigx + xpysqr

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

(5) xpysqr = xpy**2

local gradient是2*xpy,再乘以dxpysqr

(4) xpy = x + y

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

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

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

(2) num = x + sigy

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

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

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

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

梯度的矩阵运算

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

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

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

首先介绍graident of matrix

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




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

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




根据矩阵乘法的定义




我们可以计算:




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




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




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




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




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

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

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

简介

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

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

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

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

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

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

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

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

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

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

复杂表达式的链式法则

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

对y的求导也是类似的。

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

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

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

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

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

反向传播算法的直觉解释

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

Sigmoid模块的例子

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

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

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

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

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

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

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

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

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

上面我们看到把
018.jpg

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

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

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

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

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

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

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

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

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

Staged computation练习
023.jpg

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

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

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

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

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

(7) invden = 1.0 / den

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

(6) den = sigx + xpysqr

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

(5) xpysqr = xpy**2

local gradient是2*xpy,再乘以dxpysqr

(4) xpy = x + y

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

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

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

(2) num = x + sigy

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

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

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

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

梯度的矩阵运算

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

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

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

首先介绍graident of matrix

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

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

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

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

我们可以计算:
028.jpg

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2.2 多层神经网络

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

2.2.1 手写数字识别问题

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

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




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




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

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

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

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

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




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

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

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

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




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









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




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




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




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

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

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

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

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

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

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

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




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

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

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

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

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

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

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

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

求梯度有如下4种方法:

1.手工求解析解

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

2.数值解

使用极限的定义:




3.机器符号计算

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

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

4.自动梯度

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

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

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

2.2.4 编程实战

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

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

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

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

2.运行

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

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

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

3.1 mnist_loader.load_data_wrapper函数

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

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

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

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

   test_data的格式和validation_data一样。

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

3.2 Network类的构造函数

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

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

3.3 feedforward函数

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

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

3.4 SGD函数

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

2.2 多层神经网络

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

2.2.1 手写数字识别问题

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

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

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

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

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

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

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

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

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

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

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

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

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


006.png

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

求梯度有如下4种方法:

1.手工求解析解

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

2.数值解

使用极限的定义:
011.png

3.机器符号计算

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

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

4.自动梯度

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

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


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

2.2.4 编程实战

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

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

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

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

2.运行

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

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

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

3.1 mnist_loader.load_data_wrapper函数

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

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

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

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

   test_data的格式和validation_data一样。

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

3.2 Network类的构造函数

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

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

3.3 feedforward函数

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

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

3.4 SGD函数

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


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

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

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

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

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

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

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

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

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

【源码下载】一款使用环信实现的开源灵魂社交APP(含服务器) 猿匹配 开源

beyond 发表了文章 • 3851 次浏览 • 2019-07-01 10:48 • 来自相关话题

#前言
近期,环信热心开发者-穿裤衩闯天下使用环信IM开发了一款实时聊天应用,包含简单的服务器端,现在正式开源给小伙伴们。感兴趣的同学可以一起搞一下哦,详细介绍请往下看。






  上代码
服务器:VMServer
客户端:VMMatch
 
 #VMMatch
猿匹配 —— 国内首个程序猿非严肃婚恋交友应用,让我们一言不合就来场匹配吧
 
#介绍#
首先说下中文名:为什么叫这个名字呢,因为这是一个程序猿(媛)之间匹配交流的应用啊其实这是一个使用环信 IM 开发的一款开源聊天项目,涵盖了时下流行的一些聊天元素,同时已将 IM 功能封装为单独库,可以直接引用,方便使用
项目还处在初期阶段,还有许多功能需要实现,有兴趣的可以一起来
项目资源均来自于互联网,如果有侵权请联系我
 
 #下载体验
猿匹配 小米商店 审核中
猿匹配 Google Play
 
  #项目截图

























  
 #开发环境
项目基本属于在最新的Android开发环境下开发,使用Java8的一些新特性,比如Lambda表达式,
然后项目已经适配Android6.x以上的动态权限适配,以及7.x的文件选择,和8.x的通知提醒等;
· Mac OS 10.14.4
· Android Studio 3.3.2
  #项目模块儿
本项目包含两部分:
一部分是项目主模块app,这部分主要包含了项目的业务逻辑,比如匹配、信息修改、设置等
另一部分是封装成library的vmim,这是为了方便大家引用到自己的项目中做的一步封装,不用再去复杂的复制代码和资源等,
只需要将vmim以module导入到自己的项目中就行了,具体使用方式参见项目app模块儿;
 
  #功能与 TODO
IM部分功能
· [x] 链接监听
· [x] 登录注册
· [x] 会话功能
      。[x] 置顶
      。[x] 标为未读
      。[x] 删除与清空
      。[x] 草稿功能
· [x] 消息功能
      。[x] 下拉加载更多
      。[x] 消息复制(仅文字类消息)
      。[x] 消息删除
      。[x] 文本+Emoji消息收发
      。[x] 大表情消息收发
      。[x] 图片消息
        ~[x] 查看大图
        ~[ ] 保存图片
      。[x] 语音消息
        ~[x] 语音录制
        ~[x] 语音播放(可暂停,波形待优化)
        ~[x] 听筒和扬声器播放切换
      。[x] 语音实时通话功能
      。[x] 视频实时通话功能
      。[x] 通话过程中的娱乐消息收发
        ~[x] 骰子
        ~[x] 石头剪刀布
        ~[x] 大表情
      。[x] 昵称头像处理(通过回调实现)
App部分功能
· [x] 登录注册(包括业务逻辑和 IM 逻辑)
· [x] 匹配
      。[x] 提交匹配信息
      。[x] 拉取匹配信息
· [x] 聊天(这里直接加载 IM 模块儿)
· [x] 我的
      。[x] 个人信息展示
      。[x] 上传头像
      。[x] 设置昵称
      。[x] 设置签名
· [x] 设置
      。[x] 个人信息设置
      。[x] 通知提醒
      。[x] 聊天
      。[ ] 隐私(随业务部分一起完善)
      。[ ] 通用(随业务部分一起完善)
      。[ ] 帮助反馈(随业务部分一起完善)
      。[x] 关于
      。[x] 退出
· [ ] 社区
      。[ ] 发布
      。[ ] 评论
      。[ ] 收藏
      。[ ] 关注
发布功能
· [x] 多渠道打包
· [x] 签名配置
· [x] 开发与线上环境配置
· [x] 敏感信息保护
 
  #配置运行
1.首先复制config.default.gradle到config.gradle
2.配置下config.gradle环信appkey以及bugly统计Id
3.正式打包需要配置下签名信息,同时将签名文件放置在项目根目录
 
  #参与贡献
如果你有什么好的想法,或者好的实现,可以通过下边的步骤参与进来,让我们一起把这个项目做得更好,欢迎参与
1.Fork本仓库
2.新建feature_xxx分支 (单独创建一个实现你自己想法的分支)
3.提交代码
4.新建Pull Request
5.等待我们的Review & Merge
 
 #关联项目
服务器端由nodejs实现,地址见这里 VMServer
 
  #VMServer
是为Android开源项目VMMatch项目(中文名猿匹配)实现的服务端
 
  #简介
这个项目包含两部分
· 根目录:服务逻辑及API接口实现
· client目录:前端界面,和服务器端代码端放置在同一仓库下(暂未实现)
 
 #使用
简单介绍下运行环境及部署方法
1.安装nodejs开发时使用的是v10.16.0版本
2.需要安装mongodb并启动,开发使用版本4.0.10
3.下载项目到服务器,可以下载压缩包,或者用git clone命令
4.复制config_default.js到config.js,可根据自己需要修改配置文件
5.安装依赖npm install
6.全局安装pm2npm install pm2 -g 
7.运行 vmshell.sh
 




扫码备注【开源项目】邀你加入环信开源社群
 
转载自https://blog.melove.net/develop-open-source-im-match-and-server/ 
  查看全部
#前言
近期,环信热心开发者-穿裤衩闯天下使用环信IM开发了一款实时聊天应用,包含简单的服务器端,现在正式开源给小伙伴们。感兴趣的同学可以一起搞一下哦,详细介绍请往下看。

猿匹配_logo_副本.png


  上代码
服务器:VMServer
客户端:VMMatch
 
 #VMMatch
猿匹配 —— 国内首个程序猿非严肃婚恋交友应用,让我们一言不合就来场匹配吧
 
#介绍#
首先说下中文名:为什么叫这个名字呢,因为这是一个程序猿(媛)之间匹配交流的应用啊其实这是一个使用环信 IM 开发的一款开源聊天项目,涵盖了时下流行的一些聊天元素,同时已将 IM 功能封装为单独库,可以直接引用,方便使用
项目还处在初期阶段,还有许多功能需要实现,有兴趣的可以一起来
项目资源均来自于互联网,如果有侵权请联系我
 
 #下载体验
猿匹配 小米商店 审核中
猿匹配 Google Play
 
  #项目截图

1.png

2.png

3.png

4.png

5.png

6.png

  
 #开发环境
项目基本属于在最新的Android开发环境下开发,使用Java8的一些新特性,比如Lambda表达式,
然后项目已经适配Android6.x以上的动态权限适配,以及7.x的文件选择,和8.x的通知提醒等;
· Mac OS 10.14.4
· Android Studio 3.3.2
  #项目模块儿
本项目包含两部分:
一部分是项目主模块app,这部分主要包含了项目的业务逻辑,比如匹配、信息修改、设置等
另一部分是封装成library的vmim,这是为了方便大家引用到自己的项目中做的一步封装,不用再去复杂的复制代码和资源等,
只需要将vmim以module导入到自己的项目中就行了,具体使用方式参见项目app模块儿;
 
  #功能与 TODO
IM部分功能
· [x] 链接监听
· [x] 登录注册
· [x] 会话功能
      。[x] 置顶
      。[x] 标为未读
      。[x] 删除与清空
      。[x] 草稿功能
· [x] 消息功能
      。[x] 下拉加载更多
      。[x] 消息复制(仅文字类消息)
      。[x] 消息删除
      。[x] 文本+Emoji消息收发
      。[x] 大表情消息收发
      。[x] 图片消息
        ~[x] 查看大图
        ~[ ] 保存图片
      。[x] 语音消息
        ~[x] 语音录制
        ~[x] 语音播放(可暂停,波形待优化)
        ~[x] 听筒和扬声器播放切换
      。[x] 语音实时通话功能
      。[x] 视频实时通话功能
      。[x] 通话过程中的娱乐消息收发
        ~[x] 骰子
        ~[x] 石头剪刀布
        ~[x] 大表情
      。[x] 昵称头像处理(通过回调实现)
App部分功能
· [x] 登录注册(包括业务逻辑和 IM 逻辑)
· [x] 匹配
      。[x] 提交匹配信息
      。[x] 拉取匹配信息
· [x] 聊天(这里直接加载 IM 模块儿)
· [x] 我的
      。[x] 个人信息展示
      。[x] 上传头像
      。[x] 设置昵称
      。[x] 设置签名
· [x] 设置
      。[x] 个人信息设置
      。[x] 通知提醒
      。[x] 聊天
      。[ ] 隐私(随业务部分一起完善)
      。[ ] 通用(随业务部分一起完善)
      。[ ] 帮助反馈(随业务部分一起完善)
      。[x] 关于
      。[x] 退出
· [ ] 社区
      。[ ] 发布
      。[ ] 评论
      。[ ] 收藏
      。[ ] 关注
发布功能
· [x] 多渠道打包
· [x] 签名配置
· [x] 开发与线上环境配置
· [x] 敏感信息保护
 
  #配置运行
1.首先复制config.default.gradle到config.gradle
2.配置下config.gradle环信appkey以及bugly统计Id
3.正式打包需要配置下签名信息,同时将签名文件放置在项目根目录
 
  #参与贡献
如果你有什么好的想法,或者好的实现,可以通过下边的步骤参与进来,让我们一起把这个项目做得更好,欢迎参与
1.Fork本仓库
2.新建feature_xxx分支 (单独创建一个实现你自己想法的分支)
3.提交代码
4.新建Pull Request
5.等待我们的Review & Merge
 
 #关联项目
服务器端由nodejs实现,地址见这里 VMServer
 
  #VMServer
是为Android开源项目VMMatch项目(中文名猿匹配)实现的服务端
 
  #简介
这个项目包含两部分
· 根目录:服务逻辑及API接口实现
· client目录:前端界面,和服务器端代码端放置在同一仓库下(暂未实现)
 
 #使用
简单介绍下运行环境及部署方法
1.安装nodejs开发时使用的是v10.16.0版本
2.需要安装mongodb并启动,开发使用版本4.0.10
3.下载项目到服务器,可以下载压缩包,或者用git clone命令
4.复制config_default.js到config.js,可根据自己需要修改配置文件
5.安装依赖
npm install

6.全局安装pm2
npm install pm2 -g
 
7.运行 vmshell.sh
 
环信冬冬_副本.jpg

扫码备注【开源项目】邀你加入环信开源社群
 
转载自https://blog.melove.net/develop-open-source-im-match-and-server/ 
 
4
评论

在微信小程序里实现聊天室 聊天室 小程序

Tolazy 发表了文章 • 5940 次浏览 • 2019-04-19 17:49 • 来自相关话题

第一次搞小程序,老板让我实现一个聊天室功能,压力山大啊。
花了几天时间研究比较了一下方案,最后基于环信的小程序SDK 开发了一个聊天室。
 
准备工作
下载环信 小程序demo+sdkgit clone https://github.com/easemob/webim-weixin-xcx创建一个文件夹,将 demo 中的文件 comps、images、sdk、utils 拷贝到新的文件,文件目录说明



集成
登录环信没什么可说的,这里选择的是使用 username/password 登录,和demo中的一样,文件没有进行任何更改


在app.js 中注册的 WebIM.conn.listen, 然后在 登陆成功的回调 onOpened 设置的跳转页面,并将登陆的 username 赋给 myName,传到新的页面中使用


修改 roomlist.js 获取聊天室列表,是分页获取的,这里先偷个懒,获取了第一页 20 个聊天室


然后将listChatrooms() 分别在onLoad、onShow 内,更改下,将原有的 listGroups() 替换掉然后在roomlist.wxml 修改对应的 变量绑定名称





demo中的group.js 中,获取到的是当前登陆账号已加入的群组,咱们做的是聊天室功能,所以需要有一个加入的操作,找roomlist.js 中找到 into_room: function (event),然后填写加入聊天室的方法, 我是直接在当前这个里面加的跳转到聊天页面,并将当前登陆的IDmyName,聊天室IDgroupID,聊天室名称your 传给新页面


Ex:监听是否加入聊天室成功的回调是在 onPresence 中,type:memberJoinChatRoomSuccess,正常是监听这个回调跳转页面,有点麻烦就直接这样吧到会话页面后,需要修改一下对应的消息格式,在comps/chat/suit 目录下,将里面的文件对应的 js 文件根据文档给聊天室发送消息 格式进行修改,聊天室消息和群组消息不同,所以我目前是直接将getSendToParam()、isGroupChat() 注释,改成下面这样,demo 中下面还有代码的,这里就用 …… 代替了





就这样了,简单集成聊天室功能,demo中的UI 是开源的,可以根据自己的需求更改~下面是具体实现过程。代码也放在github 上了,有需要的兄弟自取。demo下载地址:https://github.com/lizgDonkey/room-xcx 查看全部
第一次搞小程序,老板让我实现一个聊天室功能,压力山大啊。
花了几天时间研究比较了一下方案,最后基于环信的小程序SDK 开发了一个聊天室。
 
准备工作
  1. 下载环信 小程序demo+sdk
    git clone https://github.com/easemob/webim-weixin-xcx
  2. 创建一个文件夹,将 demo 中的文件 comps、images、sdk、utils 拷贝到新的文件,文件目录说明
    ml.png

集成
  1. 登录环信没什么可说的,这里选择的是使用 username/password 登录,和demo中的一样,文件没有进行任何更改
    login.png
  2. 在app.js 中注册的 WebIM.conn.listen, 然后在 登陆成功的回调 onOpened 设置的跳转页面,并将登陆的 username 赋给 myName,传到新的页面中使用
    tz.png
  3. 修改 roomlist.js 获取聊天室列表,是分页获取的,这里先偷个懒,获取了第一页 20 个聊天室
    getroom.png
    然后将listChatrooms() 分别在onLoad、onShow 内,更改下,将原有的 listGroups() 替换掉
  4. 然后在roomlist.wxml 修改对应的 变量绑定名称
    listui.png
    list.png
  5. demo中的group.js 中,获取到的是当前登陆账号已加入的群组,咱们做的是聊天室功能,所以需要有一个加入的操作,找roomlist.js 中找到 into_room: function (event),然后填写加入聊天室的方法, 我是直接在当前这个里面加的跳转到聊天页面,并将当前登陆的IDmyName,聊天室IDgroupID,聊天室名称your 传给新页面
    joinrom.png
    Ex:监听是否加入聊天室成功的回调是在 onPresence 中,type:memberJoinChatRoomSuccess,正常是监听这个回调跳转页面,有点麻烦就直接这样吧
  6. 到会话页面后,需要修改一下对应的消息格式,在comps/chat/suit 目录下,将里面的文件对应的 js 文件根据文档给聊天室发送消息 格式进行修改,聊天室消息和群组消息不同,所以我目前是直接将getSendToParam()、isGroupChat() 注释,改成下面这样,demo 中下面还有代码的,这里就用 …… 代替了
    send.png
    chat.png
    就这样了,简单集成聊天室功能,demo中的UI 是开源的,可以根据自己的需求更改~下面是具体实现过程。代码也放在github 上了,有需要的兄弟自取。demo下载地址:https://github.com/lizgDonkey/room-xcx

4
评论

【开源项目】全国首个开源直播小程序源码

beyond 发表了文章 • 126896 次浏览 • 2018-07-20 17:30 • 来自相关话题

今天你看直播了吗?拥有10亿微信生态用户的小程序已经成为了继移动互联后的又一个现象级风口,随着微信小程序对外开放实时音视频录制及播放等更多连接能力,小程序与直播强强联合,在各行各业找到了非常多的玩法,小程序直播相比微信直播和APP直播更加简洁、流畅、低延时、多入口等众多优势迅速向商业直播领域及泛娱乐直播领域蔓延。从小游戏、内容付费、工具、大数据、社交电商创业者到传统品牌商们,都在努力搭上小程序直播这辆快车,以免错过微信生态里新的流量洼地。
 





作为一名环信生态圈资深开发者,本着对技术的热衷,对环信的眷恋和对党的忠诚,基于环信即时通讯云写了“直播购物小程序”,目前项目源码已全部免费开放,希望对有需求的企业和开发者提供一个思路和参考。
直播购物小程序源码github地址:https://github.com/YuTongNetworkTechnology/wechat_live/tree/master 
git打不开可直接点下面链接下载


小程序直播demo_2018-06-21.zip







直播购物小程序运行预览图 
 
小程序体验指南(仅需两步):
 
1、下载微信小程序开发工具,下载地址:https://developers.weixin.qq.c ... .html 
 




2、导入源码:将附件的源码解压直接导入 







环信小程序直播技术文档
一、 使用的技术
1、 环信IM直播室。
2、 微信小程序实时音视频播放组件live-player。
3、 推流软件(obs、易推流)等推流。
4、 视频流服务器(UCLOUD、七牛、腾讯)等视频流服务器。
二、 系统使用流程。
1、 视频推流软件将视频流推到流服务器。
2、 打开视频直播demo小程序注册环信账号。
3、 进入软件直播室进行测试。
三、 技术流程及使用的SDk
1、 注册环信账号
打开https://www.easemob.com/ 环信官网,点击右上角注册按钮,选择[注册即时通讯云]




填写对相关信息进行注册





注册成功后进行登录




注:新注册用户需进行账号的认证。
2、 直播应用创建
登录成功点击应用列表选择创建应用




输入应用名称等信息
 





创建成功后点击应用进入





需要注意的是应用的OrgName 和AppName这两个是以后都需要用到的两个参数变量




3、 直播创建
1)在创建直播之前需要对应用进行设置首先需要设置应用的直播流地址
第一步获取应用管理员的Tokencurl -X POST "https://a1.easemob.com/[应用OrgName]/[应用AppName]/token" -d '{"grant_type":"client_credentials","client_id":"[应用client_id]","client_secret":"[应用] client_secret"}'返回格式{
"access_token":"YWMtWY779DgJEeS2h9OR7fw4QgAAAUmO4Qukwd9cfJSpkWHiOa7MCSk0MrkVIco",
"expires_in":5184000,
"application":"c03b3e30-046a-11e4-8ed1-5701cdaaa0e4"












第二步设置直播流地址curl -X POST -H "Authorization: Bearer [管理员Token]" " https://a1.easemob.com/[应用OrgName]/[应用AppName]/liverooms/stream_url -d '{"pc_pull":"[pc拉流地址]","pc_push":"[pc推流地址]","mobile_pull":"[手机拉流地址]","mobile_push":"[手机推流地址]"}'"成功返回格式:{
"action": "post",
"application": "e1a09de0-0e03-11e7-ad8e-a1d913615409",
"uri": "http://127.0.0.1:8080/easemob- ... ot%3B,
"entities": [ ],
"data": {
"pc_pull": true,
"mobile_push": true,
"mobile_pull": true,
"pc_push": true
},
"timestamp": 1494084474885,
"duration": 1,
"organization": "easemob-demo",
"applicationName": "chatdemoui"
}












2)创建主播
点击IM用户





点击注册IM用户





填写用户信息





创建用户的过程同样也可以通过REST API形式进行curl -X POST -i " https://a1.easemob.com/[应用OrgName]/[应用AppName]/users" -d '{"username":"[用户名]","password":"[密码]"}'
注:应用必须为开放注册





将注册的用户添加为主播curl -X POST -H "Authorization: [管理员Token]" https://a1.easemob.com/[应用OrgName]/[应用AppName]/super_admin -d'{"superadmin":"[IM用户名]"}'返回结果示例:{
"action": "post",
"application": "4d7e4ba0-dc4a-11e3-90d5-e1ffbaacdaf5",
"uri": "http://127.0.0.1:8080/easemob- ... ot%3B,
"entities": [ ],
"data": {
"result": "success"
},
"timestamp": 1496236798886,
"duration": 0,
"organization": "easemob-demo",
"applicationName": "chatdemoui"
}












3)创建直播
点击直播





点击新建房间





填写房间信息




创建房间同时也可以使用REST API形式进行详情可以查看http://docs.easemob.com/im/live/server-integration环信官方文档。
4、 小程序demo集成使用
小程序直播购物demo集成官方WebIM SDK详情请查看https://github.com/easemob/webim-weixin-xcx
Demo具体配置如下
打开demo 下sdk配置文件





修改appkey为自己应用的appkey





打开pages/live/index.js修改房间默认拉流地址及直播间房间号





四、 扩展说明
Demo中房间为固定测试房间,实际使用中应获取环信直播的房间信息及房间列表。具体如下:
获取直播间列表:curl -X GET -H "Authorization: Bearer [用户Token]" https://a1.easemob.com/[应用OrgName]/[应用AppName]/liverooms?ongoing=true&limit=[获取数量]&cursor=[游标地址(不填写为充开始查询)]
响应:{
"action": "get",
"application": "4d7e4ba0-dc4a-11e3-90d5-e1ffbaacdaf5",
"params": {
"cursor": [
"ZGNiMjRmNGY1YjczYjlhYTNkYjk1MDY2YmEyNzFmODQ6aW06Y2hhdHJvb206ZWFzZW1vYi1kZW1vI2NoYXRkZW1vdWk6MzE"
],
"ongoing": [
"true"
],
"limit": [
"2"
]
},
"uri": "http://127.0.0.1:8080/easemob- ... ot%3B,
"entities": [ ],
"data": [
{
"id": "1924",
"chatroom_id": "17177265635330",
"title": "具体了",
"desc": "就咯",
"startTime": 1495779917352,
"endTime": 1495779917352,
"anchor": "wuls",
"gift_count": 0,
"praise_count": 0,
"current_user_count": 8,
"max_user_count": 9,
"status": "ongoing",
"cover_picture_url": "",
"pc_pull_url": "rtmp://vlive3.rtmp.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1924_1",
"pc_push_url": "rtmp://publish3.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1924_1",
"mobile_pull_url": "rtmp://vlive3.rtmp.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1924_1",
"mobile_push_url": "rtmp://publish3.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1924_1"
},
{
"id": "1922",
"chatroom_id": "17175003856897",
"title": "香山",
"desc": "随便",
"startTime": 1495777760957,
"endTime": 1495777760957,
"anchor": "sx001",
"gift_count": 0,
"praise_count": 8,
"current_user_count": 1,
"max_user_count": 3,
"status": "ongoing",
"cover_picture_url": "http://127.0.0.1:8080/easemob- ... ot%3B,
"pc_pull_url": "rtmp://vlive3.rtmp.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1922_1",
"pc_push_url": "rtmp://publish3.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1922_1",
"mobile_pull_url": "rtmp://vlive3.rtmp.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1922_1",
"mobile_push_url": "rtmp://publish3.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1922_1"
}
],
"timestamp": 1496303336669,
"duration": 0,
"organization": "easemob-demo",
"applicationName": "chatdemoui",
"cursor": "ZGNiMjRmNGY1YjczYjlhYTNkYjk1MDY2YmEyNzFmODQ6aW06Y2hhdHJvb206ZWFzZW1vYi1kZW1vI2NoYXRkZW1vdWk6NDk",
"count": 2
}












获取直播间详情:curl -X GET -H "Authorization: Bearer [用户Token]" " https://a1.easemob.com/[应用OrgName]/[应用AppName]/[房间id]/status"响应:{
"action": "get",
"application": "4d7e4ba0-dc4a-11e3-90d5-e1ffbaacdaf5",
"uri": "http://127.0.0.1:8080/easemob- ... ot%3B,
"entities": [ ],
"data": {
"liveRoomID": "1946",
"status": "ongoing"
},
"timestamp": 1496234759930,
"duration": 0,
"organization": "easemob-demo",
"applicationName": "chatdemoui",
"count": 0
}














 
使用环信直播购物小程序遇到任何问题欢迎跟帖讨论。 查看全部
今天你看直播了吗?
拥有10亿微信生态用户的小程序已经成为了继移动互联后的又一个现象级风口,随着微信小程序对外开放实时音视频录制及播放等更多连接能力,小程序与直播强强联合,在各行各业找到了非常多的玩法,小程序直播相比微信直播和APP直播更加简洁、流畅、低延时、多入口等众多优势迅速向商业直播领域及泛娱乐直播领域蔓延。从小游戏、内容付费、工具、大数据、社交电商创业者到传统品牌商们,都在努力搭上小程序直播这辆快车,以免错过微信生态里新的流量洼地。
 
微信图片_20180725162426.jpg


作为一名环信生态圈资深开发者,本着对技术的热衷,对环信的眷恋和对党的忠诚,基于环信即时通讯云写了“直播购物小程序”,目前项目源码已全部免费开放,希望对有需求的企业和开发者提供一个思路和参考。
直播购物小程序源码github地址:https://github.com/YuTongNetworkTechnology/wechat_live/tree/master 
git打不开可直接点下面链接下载



预览图.jpg

直播购物小程序运行预览图 
 
小程序体验指南(仅需两步):
 
1、下载微信小程序开发工具,下载地址:https://developers.weixin.qq.c ... .html 
 
Catch9A07(07-20-17-38-30).jpg

2、导入源码:将附件的源码解压直接导入 


Catch1C69(07-20-17-38-30).jpg


环信小程序直播技术文档
一、 使用的技术
1、 环信IM直播室。
2、 微信小程序实时音视频播放组件live-player。
3、 推流软件(obs、易推流)等推流。
4、 视频流服务器(UCLOUD、七牛、腾讯)等视频流服务器。
二、 系统使用流程。
1、 视频推流软件将视频流推到流服务器。
2、 打开视频直播demo小程序注册环信账号。
3、 进入软件直播室进行测试。
三、 技术流程及使用的SDk
1、 注册环信账号
打开https://www.easemob.com/ 环信官网,点击右上角注册按钮,选择[注册即时通讯云]
1.png

填写对相关信息进行注册

2.png

注册成功后进行登录
3.png

注:新注册用户需进行账号的认证。
2、 直播应用创建
登录成功点击应用列表选择创建应用
4.png

输入应用名称等信息
 

5.png

创建成功后点击应用进入

6.png

需要注意的是应用的OrgName 和AppName这两个是以后都需要用到的两个参数变量
7.png

3、 直播创建
1)在创建直播之前需要对应用进行设置首先需要设置应用的直播流地址
第一步获取应用管理员的Token
curl -X POST "https://a1.easemob.com/[应用OrgName]/[应用AppName]/token" -d '{"grant_type":"client_credentials","client_id":"[应用client_id]","client_secret":"[应用] client_secret"}'
返回格式
{
"access_token":"YWMtWY779DgJEeS2h9OR7fw4QgAAAUmO4Qukwd9cfJSpkWHiOa7MCSk0MrkVIco",
"expires_in":5184000,
"application":"c03b3e30-046a-11e4-8ed1-5701cdaaa0e4"












第二步设置直播流地址
curl -X POST -H "Authorization: Bearer [管理员Token]"  " https://a1.easemob.com/[应用OrgName]/[应用AppName]/liverooms/stream_url -d '{"pc_pull":"[pc拉流地址]","pc_push":"[pc推流地址]","mobile_pull":"[手机拉流地址]","mobile_push":"[手机推流地址]"}'"
成功返回格式:
{
"action": "post",
"application": "e1a09de0-0e03-11e7-ad8e-a1d913615409",
"uri": "http://127.0.0.1:8080/easemob- ... ot%3B,
"entities": [ ],
"data": {
"pc_pull": true,
"mobile_push": true,
"mobile_pull": true,
"pc_push": true
},
"timestamp": 1494084474885,
"duration": 1,
"organization": "easemob-demo",
"applicationName": "chatdemoui"
}












2)创建主播
点击IM用户

8.png

点击注册IM用户

9.png

填写用户信息

10.png

创建用户的过程同样也可以通过REST API形式进行
curl -X POST -i " https://a1.easemob.com/[应用OrgName]/[应用AppName]/users" -d '{"username":"[用户名]","password":"[密码]"}'

注:应用必须为开放注册

11.png

将注册的用户添加为主播
curl -X POST -H "Authorization: [管理员Token]"  https://a1.easemob.com/[应用OrgName]/[应用AppName]/super_admin -d'{"superadmin":"[IM用户名]"}'
返回结果示例:
{
"action": "post",
"application": "4d7e4ba0-dc4a-11e3-90d5-e1ffbaacdaf5",
"uri": "http://127.0.0.1:8080/easemob- ... ot%3B,
"entities": [ ],
"data": {
"result": "success"
},
"timestamp": 1496236798886,
"duration": 0,
"organization": "easemob-demo",
"applicationName": "chatdemoui"
}












3)创建直播
点击直播

12.png

点击新建房间

13.png

填写房间信息
14.png

创建房间同时也可以使用REST API形式进行详情可以查看http://docs.easemob.com/im/live/server-integration环信官方文档。
4、 小程序demo集成使用
小程序直播购物demo集成官方WebIM SDK详情请查看https://github.com/easemob/webim-weixin-xcx
Demo具体配置如下
打开demo 下sdk配置文件

15.png

修改appkey为自己应用的appkey

16.png

打开pages/live/index.js修改房间默认拉流地址及直播间房间号

17.png

四、 扩展说明
Demo中房间为固定测试房间,实际使用中应获取环信直播的房间信息及房间列表。具体如下:
获取直播间列表:
curl -X GET -H "Authorization: Bearer  [用户Token]"  https://a1.easemob.com/[应用OrgName]/[应用AppName]/liverooms?ongoing=true&limit=[获取数量]&cursor=[游标地址(不填写为充开始查询)]

响应:
{
"action": "get",
"application": "4d7e4ba0-dc4a-11e3-90d5-e1ffbaacdaf5",
"params": {
"cursor": [
"ZGNiMjRmNGY1YjczYjlhYTNkYjk1MDY2YmEyNzFmODQ6aW06Y2hhdHJvb206ZWFzZW1vYi1kZW1vI2NoYXRkZW1vdWk6MzE"
],
"ongoing": [
"true"
],
"limit": [
"2"
]
},
"uri": "http://127.0.0.1:8080/easemob- ... ot%3B,
"entities": [ ],
"data": [
{
"id": "1924",
"chatroom_id": "17177265635330",
"title": "具体了",
"desc": "就咯",
"startTime": 1495779917352,
"endTime": 1495779917352,
"anchor": "wuls",
"gift_count": 0,
"praise_count": 0,
"current_user_count": 8,
"max_user_count": 9,
"status": "ongoing",
"cover_picture_url": "",
"pc_pull_url": "rtmp://vlive3.rtmp.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1924_1",
"pc_push_url": "rtmp://publish3.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1924_1",
"mobile_pull_url": "rtmp://vlive3.rtmp.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1924_1",
"mobile_push_url": "rtmp://publish3.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1924_1"
},
{
"id": "1922",
"chatroom_id": "17175003856897",
"title": "香山",
"desc": "随便",
"startTime": 1495777760957,
"endTime": 1495777760957,
"anchor": "sx001",
"gift_count": 0,
"praise_count": 8,
"current_user_count": 1,
"max_user_count": 3,
"status": "ongoing",
"cover_picture_url": "http://127.0.0.1:8080/easemob- ... ot%3B,
"pc_pull_url": "rtmp://vlive3.rtmp.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1922_1",
"pc_push_url": "rtmp://publish3.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1922_1",
"mobile_pull_url": "rtmp://vlive3.rtmp.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1922_1",
"mobile_push_url": "rtmp://publish3.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1922_1"
}
],
"timestamp": 1496303336669,
"duration": 0,
"organization": "easemob-demo",
"applicationName": "chatdemoui",
"cursor": "ZGNiMjRmNGY1YjczYjlhYTNkYjk1MDY2YmEyNzFmODQ6aW06Y2hhdHJvb206ZWFzZW1vYi1kZW1vI2NoYXRkZW1vdWk6NDk",
"count": 2
}












获取直播间详情:
curl -X GET -H "Authorization: Bearer [用户Token]" " https://a1.easemob.com/[应用OrgName]/[应用AppName]/[房间id]/status"
响应:
{
"action": "get",
"application": "4d7e4ba0-dc4a-11e3-90d5-e1ffbaacdaf5",
"uri": "http://127.0.0.1:8080/easemob- ... ot%3B,
"entities": [ ],
"data": {
"liveRoomID": "1946",
"status": "ongoing"
},
"timestamp": 1496234759930,
"duration": 0,
"organization": "easemob-demo",
"applicationName": "chatdemoui",
"count": 0
}














 
使用环信直播购物小程序遇到任何问题欢迎跟帖讨论。
19
评论

【新手快速入门】集成环信常见问题+解决方案汇总

dujiepeng 发表了文章 • 24396 次浏览 • 2017-05-22 15:51 • 来自相关话题

   这里整理了集成环信的常见问题和一些功能的实现思路,希望能帮助到大家。感谢热心的开发者贡献,大家在观看过程中有不明白的地方欢迎直接跟帖咨询。
 
ios篇
APNs证书创建和上传到环信后台头像昵称的简述和处理方案音视频离线推送Demo实现环信服务器聊天记录保存多久?离线收不到好友请求IOS中环信聊天窗口如何实现文件发送和预览的功能ios集成常见问题环信推送的一些常见问题实现名片|红包|话题聊天室等自定义cell
 
Android篇
Android sdk 的两种导入方式环信3.0SDK集成小米推送教程EaseUI库中V4、v7包冲突解决方案Android EaseUI里的百度地图替换为高德地图android扩展消息(名片集成)关于会话列表的置顶聊天java.lang.UnsatisfiedLinkError: 的问题android 端 app 后台被杀死收不到消息的解决方案
昵称头像篇
android中如何显示开发者服务器上的昵称和头像 Android中显示头像(接上一篇文章看)环信(Android)设置头像和昵称的方法(最简单暴力的基于环信demo的集成)IOS中如何显示开发者服务器上的昵称和头像【环信公开课第12期视频回放】-所有关于环信IM昵称头像的问题听这课就够了
 
直播篇
一言不合你就搞个直播APP
 
客服集成
IM-SDK和客服SDK并存开发指南—Android篇IM-SDK和客服SDK并存开发指南—iOS篇
 
开源项目
Android简版demoios简版demo凡信2.0:超仿微信的开源项目 凡信3.0:携直播和红包而来高仿微信:Github 3,515 Star方圆十里:环信编程大赛冠军项目泛聊:定一个小目标写一个QQSlack聊天机器人:一天时间做一个聊天机器人TV视频通话:在电视上视频通话视频通话:Android手机视频通话酷信:ios高仿微信公众号助手:与订阅用户聊天沟通
 
持续更新ing...小伙伴们还有什么想知道欢迎跟帖提出。
  查看全部
   这里整理了集成环信的常见问题和一些功能的实现思路,希望能帮助到大家。感谢热心的开发者贡献,大家在观看过程中有不明白的地方欢迎直接跟帖咨询。
 
ios篇

 
Android篇

昵称头像篇

 
直播篇
  1. 一言不合你就搞个直播APP

 
客服集成
  1. IM-SDK和客服SDK并存开发指南—Android篇
  2. IM-SDK和客服SDK并存开发指南—iOS篇

 
开源项目

 
持续更新ing...小伙伴们还有什么想知道欢迎跟帖提出。
 
13
回复

收集基于环信SDK开发的开源项目 开源项目

xiaoyan2015 回复了问题 • 13 人关注 • 18746 次浏览 • 2019-03-14 13:52 • 来自相关话题

7
评论

【源码下载】一款使用环信实现的开源灵魂社交APP(含服务器) 猿匹配 开源

beyond 发表了文章 • 3851 次浏览 • 2019-07-01 10:48 • 来自相关话题

#前言
近期,环信热心开发者-穿裤衩闯天下使用环信IM开发了一款实时聊天应用,包含简单的服务器端,现在正式开源给小伙伴们。感兴趣的同学可以一起搞一下哦,详细介绍请往下看。






  上代码
服务器:VMServer
客户端:VMMatch
 
 #VMMatch
猿匹配 —— 国内首个程序猿非严肃婚恋交友应用,让我们一言不合就来场匹配吧
 
#介绍#
首先说下中文名:为什么叫这个名字呢,因为这是一个程序猿(媛)之间匹配交流的应用啊其实这是一个使用环信 IM 开发的一款开源聊天项目,涵盖了时下流行的一些聊天元素,同时已将 IM 功能封装为单独库,可以直接引用,方便使用
项目还处在初期阶段,还有许多功能需要实现,有兴趣的可以一起来
项目资源均来自于互联网,如果有侵权请联系我
 
 #下载体验
猿匹配 小米商店 审核中
猿匹配 Google Play
 
  #项目截图

























  
 #开发环境
项目基本属于在最新的Android开发环境下开发,使用Java8的一些新特性,比如Lambda表达式,
然后项目已经适配Android6.x以上的动态权限适配,以及7.x的文件选择,和8.x的通知提醒等;
· Mac OS 10.14.4
· Android Studio 3.3.2
  #项目模块儿
本项目包含两部分:
一部分是项目主模块app,这部分主要包含了项目的业务逻辑,比如匹配、信息修改、设置等
另一部分是封装成library的vmim,这是为了方便大家引用到自己的项目中做的一步封装,不用再去复杂的复制代码和资源等,
只需要将vmim以module导入到自己的项目中就行了,具体使用方式参见项目app模块儿;
 
  #功能与 TODO
IM部分功能
· [x] 链接监听
· [x] 登录注册
· [x] 会话功能
      。[x] 置顶
      。[x] 标为未读
      。[x] 删除与清空
      。[x] 草稿功能
· [x] 消息功能
      。[x] 下拉加载更多
      。[x] 消息复制(仅文字类消息)
      。[x] 消息删除
      。[x] 文本+Emoji消息收发
      。[x] 大表情消息收发
      。[x] 图片消息
        ~[x] 查看大图
        ~[ ] 保存图片
      。[x] 语音消息
        ~[x] 语音录制
        ~[x] 语音播放(可暂停,波形待优化)
        ~[x] 听筒和扬声器播放切换
      。[x] 语音实时通话功能
      。[x] 视频实时通话功能
      。[x] 通话过程中的娱乐消息收发
        ~[x] 骰子
        ~[x] 石头剪刀布
        ~[x] 大表情
      。[x] 昵称头像处理(通过回调实现)
App部分功能
· [x] 登录注册(包括业务逻辑和 IM 逻辑)
· [x] 匹配
      。[x] 提交匹配信息
      。[x] 拉取匹配信息
· [x] 聊天(这里直接加载 IM 模块儿)
· [x] 我的
      。[x] 个人信息展示
      。[x] 上传头像
      。[x] 设置昵称
      。[x] 设置签名
· [x] 设置
      。[x] 个人信息设置
      。[x] 通知提醒
      。[x] 聊天
      。[ ] 隐私(随业务部分一起完善)
      。[ ] 通用(随业务部分一起完善)
      。[ ] 帮助反馈(随业务部分一起完善)
      。[x] 关于
      。[x] 退出
· [ ] 社区
      。[ ] 发布
      。[ ] 评论
      。[ ] 收藏
      。[ ] 关注
发布功能
· [x] 多渠道打包
· [x] 签名配置
· [x] 开发与线上环境配置
· [x] 敏感信息保护
 
  #配置运行
1.首先复制config.default.gradle到config.gradle
2.配置下config.gradle环信appkey以及bugly统计Id
3.正式打包需要配置下签名信息,同时将签名文件放置在项目根目录
 
  #参与贡献
如果你有什么好的想法,或者好的实现,可以通过下边的步骤参与进来,让我们一起把这个项目做得更好,欢迎参与
1.Fork本仓库
2.新建feature_xxx分支 (单独创建一个实现你自己想法的分支)
3.提交代码
4.新建Pull Request
5.等待我们的Review & Merge
 
 #关联项目
服务器端由nodejs实现,地址见这里 VMServer
 
  #VMServer
是为Android开源项目VMMatch项目(中文名猿匹配)实现的服务端
 
  #简介
这个项目包含两部分
· 根目录:服务逻辑及API接口实现
· client目录:前端界面,和服务器端代码端放置在同一仓库下(暂未实现)
 
 #使用
简单介绍下运行环境及部署方法
1.安装nodejs开发时使用的是v10.16.0版本
2.需要安装mongodb并启动,开发使用版本4.0.10
3.下载项目到服务器,可以下载压缩包,或者用git clone命令
4.复制config_default.js到config.js,可根据自己需要修改配置文件
5.安装依赖npm install
6.全局安装pm2npm install pm2 -g 
7.运行 vmshell.sh
 




扫码备注【开源项目】邀你加入环信开源社群
 
转载自https://blog.melove.net/develop-open-source-im-match-and-server/ 
  查看全部
#前言
近期,环信热心开发者-穿裤衩闯天下使用环信IM开发了一款实时聊天应用,包含简单的服务器端,现在正式开源给小伙伴们。感兴趣的同学可以一起搞一下哦,详细介绍请往下看。

猿匹配_logo_副本.png


  上代码
服务器:VMServer
客户端:VMMatch
 
 #VMMatch
猿匹配 —— 国内首个程序猿非严肃婚恋交友应用,让我们一言不合就来场匹配吧
 
#介绍#
首先说下中文名:为什么叫这个名字呢,因为这是一个程序猿(媛)之间匹配交流的应用啊其实这是一个使用环信 IM 开发的一款开源聊天项目,涵盖了时下流行的一些聊天元素,同时已将 IM 功能封装为单独库,可以直接引用,方便使用
项目还处在初期阶段,还有许多功能需要实现,有兴趣的可以一起来
项目资源均来自于互联网,如果有侵权请联系我
 
 #下载体验
猿匹配 小米商店 审核中
猿匹配 Google Play
 
  #项目截图

1.png

2.png

3.png

4.png

5.png

6.png

  
 #开发环境
项目基本属于在最新的Android开发环境下开发,使用Java8的一些新特性,比如Lambda表达式,
然后项目已经适配Android6.x以上的动态权限适配,以及7.x的文件选择,和8.x的通知提醒等;
· Mac OS 10.14.4
· Android Studio 3.3.2
  #项目模块儿
本项目包含两部分:
一部分是项目主模块app,这部分主要包含了项目的业务逻辑,比如匹配、信息修改、设置等
另一部分是封装成library的vmim,这是为了方便大家引用到自己的项目中做的一步封装,不用再去复杂的复制代码和资源等,
只需要将vmim以module导入到自己的项目中就行了,具体使用方式参见项目app模块儿;
 
  #功能与 TODO
IM部分功能
· [x] 链接监听
· [x] 登录注册
· [x] 会话功能
      。[x] 置顶
      。[x] 标为未读
      。[x] 删除与清空
      。[x] 草稿功能
· [x] 消息功能
      。[x] 下拉加载更多
      。[x] 消息复制(仅文字类消息)
      。[x] 消息删除
      。[x] 文本+Emoji消息收发
      。[x] 大表情消息收发
      。[x] 图片消息
        ~[x] 查看大图
        ~[ ] 保存图片
      。[x] 语音消息
        ~[x] 语音录制
        ~[x] 语音播放(可暂停,波形待优化)
        ~[x] 听筒和扬声器播放切换
      。[x] 语音实时通话功能
      。[x] 视频实时通话功能
      。[x] 通话过程中的娱乐消息收发
        ~[x] 骰子
        ~[x] 石头剪刀布
        ~[x] 大表情
      。[x] 昵称头像处理(通过回调实现)
App部分功能
· [x] 登录注册(包括业务逻辑和 IM 逻辑)
· [x] 匹配
      。[x] 提交匹配信息
      。[x] 拉取匹配信息
· [x] 聊天(这里直接加载 IM 模块儿)
· [x] 我的
      。[x] 个人信息展示
      。[x] 上传头像
      。[x] 设置昵称
      。[x] 设置签名
· [x] 设置
      。[x] 个人信息设置
      。[x] 通知提醒
      。[x] 聊天
      。[ ] 隐私(随业务部分一起完善)
      。[ ] 通用(随业务部分一起完善)
      。[ ] 帮助反馈(随业务部分一起完善)
      。[x] 关于
      。[x] 退出
· [ ] 社区
      。[ ] 发布
      。[ ] 评论
      。[ ] 收藏
      。[ ] 关注
发布功能
· [x] 多渠道打包
· [x] 签名配置
· [x] 开发与线上环境配置
· [x] 敏感信息保护
 
  #配置运行
1.首先复制config.default.gradle到config.gradle
2.配置下config.gradle环信appkey以及bugly统计Id
3.正式打包需要配置下签名信息,同时将签名文件放置在项目根目录
 
  #参与贡献
如果你有什么好的想法,或者好的实现,可以通过下边的步骤参与进来,让我们一起把这个项目做得更好,欢迎参与
1.Fork本仓库
2.新建feature_xxx分支 (单独创建一个实现你自己想法的分支)
3.提交代码
4.新建Pull Request
5.等待我们的Review & Merge
 
 #关联项目
服务器端由nodejs实现,地址见这里 VMServer
 
  #VMServer
是为Android开源项目VMMatch项目(中文名猿匹配)实现的服务端
 
  #简介
这个项目包含两部分
· 根目录:服务逻辑及API接口实现
· client目录:前端界面,和服务器端代码端放置在同一仓库下(暂未实现)
 
 #使用
简单介绍下运行环境及部署方法
1.安装nodejs开发时使用的是v10.16.0版本
2.需要安装mongodb并启动,开发使用版本4.0.10
3.下载项目到服务器,可以下载压缩包,或者用git clone命令
4.复制config_default.js到config.js,可根据自己需要修改配置文件
5.安装依赖
npm install

6.全局安装pm2
npm install pm2 -g
 
7.运行 vmshell.sh
 
环信冬冬_副本.jpg

扫码备注【开源项目】邀你加入环信开源社群
 
转载自https://blog.melove.net/develop-open-source-im-match-and-server/ 
 
4
评论

在微信小程序里实现聊天室 聊天室 小程序

Tolazy 发表了文章 • 5940 次浏览 • 2019-04-19 17:49 • 来自相关话题

第一次搞小程序,老板让我实现一个聊天室功能,压力山大啊。
花了几天时间研究比较了一下方案,最后基于环信的小程序SDK 开发了一个聊天室。
 
准备工作
下载环信 小程序demo+sdkgit clone https://github.com/easemob/webim-weixin-xcx创建一个文件夹,将 demo 中的文件 comps、images、sdk、utils 拷贝到新的文件,文件目录说明



集成
登录环信没什么可说的,这里选择的是使用 username/password 登录,和demo中的一样,文件没有进行任何更改


在app.js 中注册的 WebIM.conn.listen, 然后在 登陆成功的回调 onOpened 设置的跳转页面,并将登陆的 username 赋给 myName,传到新的页面中使用


修改 roomlist.js 获取聊天室列表,是分页获取的,这里先偷个懒,获取了第一页 20 个聊天室


然后将listChatrooms() 分别在onLoad、onShow 内,更改下,将原有的 listGroups() 替换掉然后在roomlist.wxml 修改对应的 变量绑定名称





demo中的group.js 中,获取到的是当前登陆账号已加入的群组,咱们做的是聊天室功能,所以需要有一个加入的操作,找roomlist.js 中找到 into_room: function (event),然后填写加入聊天室的方法, 我是直接在当前这个里面加的跳转到聊天页面,并将当前登陆的IDmyName,聊天室IDgroupID,聊天室名称your 传给新页面


Ex:监听是否加入聊天室成功的回调是在 onPresence 中,type:memberJoinChatRoomSuccess,正常是监听这个回调跳转页面,有点麻烦就直接这样吧到会话页面后,需要修改一下对应的消息格式,在comps/chat/suit 目录下,将里面的文件对应的 js 文件根据文档给聊天室发送消息 格式进行修改,聊天室消息和群组消息不同,所以我目前是直接将getSendToParam()、isGroupChat() 注释,改成下面这样,demo 中下面还有代码的,这里就用 …… 代替了





就这样了,简单集成聊天室功能,demo中的UI 是开源的,可以根据自己的需求更改~下面是具体实现过程。代码也放在github 上了,有需要的兄弟自取。demo下载地址:https://github.com/lizgDonkey/room-xcx 查看全部
第一次搞小程序,老板让我实现一个聊天室功能,压力山大啊。
花了几天时间研究比较了一下方案,最后基于环信的小程序SDK 开发了一个聊天室。
 
准备工作
  1. 下载环信 小程序demo+sdk
    git clone https://github.com/easemob/webim-weixin-xcx
  2. 创建一个文件夹,将 demo 中的文件 comps、images、sdk、utils 拷贝到新的文件,文件目录说明
    ml.png

集成
  1. 登录环信没什么可说的,这里选择的是使用 username/password 登录,和demo中的一样,文件没有进行任何更改
    login.png
  2. 在app.js 中注册的 WebIM.conn.listen, 然后在 登陆成功的回调 onOpened 设置的跳转页面,并将登陆的 username 赋给 myName,传到新的页面中使用
    tz.png
  3. 修改 roomlist.js 获取聊天室列表,是分页获取的,这里先偷个懒,获取了第一页 20 个聊天室
    getroom.png
    然后将listChatrooms() 分别在onLoad、onShow 内,更改下,将原有的 listGroups() 替换掉
  4. 然后在roomlist.wxml 修改对应的 变量绑定名称
    listui.png
    list.png
  5. demo中的group.js 中,获取到的是当前登陆账号已加入的群组,咱们做的是聊天室功能,所以需要有一个加入的操作,找roomlist.js 中找到 into_room: function (event),然后填写加入聊天室的方法, 我是直接在当前这个里面加的跳转到聊天页面,并将当前登陆的IDmyName,聊天室IDgroupID,聊天室名称your 传给新页面
    joinrom.png
    Ex:监听是否加入聊天室成功的回调是在 onPresence 中,type:memberJoinChatRoomSuccess,正常是监听这个回调跳转页面,有点麻烦就直接这样吧
  6. 到会话页面后,需要修改一下对应的消息格式,在comps/chat/suit 目录下,将里面的文件对应的 js 文件根据文档给聊天室发送消息 格式进行修改,聊天室消息和群组消息不同,所以我目前是直接将getSendToParam()、isGroupChat() 注释,改成下面这样,demo 中下面还有代码的,这里就用 …… 代替了
    send.png
    chat.png
    就这样了,简单集成聊天室功能,demo中的UI 是开源的,可以根据自己的需求更改~下面是具体实现过程。代码也放在github 上了,有需要的兄弟自取。demo下载地址:https://github.com/lizgDonkey/room-xcx

13
回复

收集基于环信SDK开发的开源项目 开源项目

回复

xiaoyan2015 回复了问题 • 13 人关注 • 18746 次浏览 • 2019-03-14 13:52 • 来自相关话题

4
评论

【开源项目】全国首个开源直播小程序源码

beyond 发表了文章 • 126896 次浏览 • 2018-07-20 17:30 • 来自相关话题

今天你看直播了吗?拥有10亿微信生态用户的小程序已经成为了继移动互联后的又一个现象级风口,随着微信小程序对外开放实时音视频录制及播放等更多连接能力,小程序与直播强强联合,在各行各业找到了非常多的玩法,小程序直播相比微信直播和APP直播更加简洁、流畅、低延时、多入口等众多优势迅速向商业直播领域及泛娱乐直播领域蔓延。从小游戏、内容付费、工具、大数据、社交电商创业者到传统品牌商们,都在努力搭上小程序直播这辆快车,以免错过微信生态里新的流量洼地。
 





作为一名环信生态圈资深开发者,本着对技术的热衷,对环信的眷恋和对党的忠诚,基于环信即时通讯云写了“直播购物小程序”,目前项目源码已全部免费开放,希望对有需求的企业和开发者提供一个思路和参考。
直播购物小程序源码github地址:https://github.com/YuTongNetworkTechnology/wechat_live/tree/master 
git打不开可直接点下面链接下载


小程序直播demo_2018-06-21.zip







直播购物小程序运行预览图 
 
小程序体验指南(仅需两步):
 
1、下载微信小程序开发工具,下载地址:https://developers.weixin.qq.c ... .html 
 




2、导入源码:将附件的源码解压直接导入 







环信小程序直播技术文档
一、 使用的技术
1、 环信IM直播室。
2、 微信小程序实时音视频播放组件live-player。
3、 推流软件(obs、易推流)等推流。
4、 视频流服务器(UCLOUD、七牛、腾讯)等视频流服务器。
二、 系统使用流程。
1、 视频推流软件将视频流推到流服务器。
2、 打开视频直播demo小程序注册环信账号。
3、 进入软件直播室进行测试。
三、 技术流程及使用的SDk
1、 注册环信账号
打开https://www.easemob.com/ 环信官网,点击右上角注册按钮,选择[注册即时通讯云]




填写对相关信息进行注册





注册成功后进行登录




注:新注册用户需进行账号的认证。
2、 直播应用创建
登录成功点击应用列表选择创建应用




输入应用名称等信息
 





创建成功后点击应用进入





需要注意的是应用的OrgName 和AppName这两个是以后都需要用到的两个参数变量




3、 直播创建
1)在创建直播之前需要对应用进行设置首先需要设置应用的直播流地址
第一步获取应用管理员的Tokencurl -X POST "https://a1.easemob.com/[应用OrgName]/[应用AppName]/token" -d '{"grant_type":"client_credentials","client_id":"[应用client_id]","client_secret":"[应用] client_secret"}'返回格式{
"access_token":"YWMtWY779DgJEeS2h9OR7fw4QgAAAUmO4Qukwd9cfJSpkWHiOa7MCSk0MrkVIco",
"expires_in":5184000,
"application":"c03b3e30-046a-11e4-8ed1-5701cdaaa0e4"












第二步设置直播流地址curl -X POST -H "Authorization: Bearer [管理员Token]" " https://a1.easemob.com/[应用OrgName]/[应用AppName]/liverooms/stream_url -d '{"pc_pull":"[pc拉流地址]","pc_push":"[pc推流地址]","mobile_pull":"[手机拉流地址]","mobile_push":"[手机推流地址]"}'"成功返回格式:{
"action": "post",
"application": "e1a09de0-0e03-11e7-ad8e-a1d913615409",
"uri": "http://127.0.0.1:8080/easemob- ... ot%3B,
"entities": [ ],
"data": {
"pc_pull": true,
"mobile_push": true,
"mobile_pull": true,
"pc_push": true
},
"timestamp": 1494084474885,
"duration": 1,
"organization": "easemob-demo",
"applicationName": "chatdemoui"
}












2)创建主播
点击IM用户





点击注册IM用户





填写用户信息





创建用户的过程同样也可以通过REST API形式进行curl -X POST -i " https://a1.easemob.com/[应用OrgName]/[应用AppName]/users" -d '{"username":"[用户名]","password":"[密码]"}'
注:应用必须为开放注册





将注册的用户添加为主播curl -X POST -H "Authorization: [管理员Token]" https://a1.easemob.com/[应用OrgName]/[应用AppName]/super_admin -d'{"superadmin":"[IM用户名]"}'返回结果示例:{
"action": "post",
"application": "4d7e4ba0-dc4a-11e3-90d5-e1ffbaacdaf5",
"uri": "http://127.0.0.1:8080/easemob- ... ot%3B,
"entities": [ ],
"data": {
"result": "success"
},
"timestamp": 1496236798886,
"duration": 0,
"organization": "easemob-demo",
"applicationName": "chatdemoui"
}












3)创建直播
点击直播





点击新建房间





填写房间信息




创建房间同时也可以使用REST API形式进行详情可以查看http://docs.easemob.com/im/live/server-integration环信官方文档。
4、 小程序demo集成使用
小程序直播购物demo集成官方WebIM SDK详情请查看https://github.com/easemob/webim-weixin-xcx
Demo具体配置如下
打开demo 下sdk配置文件





修改appkey为自己应用的appkey





打开pages/live/index.js修改房间默认拉流地址及直播间房间号





四、 扩展说明
Demo中房间为固定测试房间,实际使用中应获取环信直播的房间信息及房间列表。具体如下:
获取直播间列表:curl -X GET -H "Authorization: Bearer [用户Token]" https://a1.easemob.com/[应用OrgName]/[应用AppName]/liverooms?ongoing=true&limit=[获取数量]&cursor=[游标地址(不填写为充开始查询)]
响应:{
"action": "get",
"application": "4d7e4ba0-dc4a-11e3-90d5-e1ffbaacdaf5",
"params": {
"cursor": [
"ZGNiMjRmNGY1YjczYjlhYTNkYjk1MDY2YmEyNzFmODQ6aW06Y2hhdHJvb206ZWFzZW1vYi1kZW1vI2NoYXRkZW1vdWk6MzE"
],
"ongoing": [
"true"
],
"limit": [
"2"
]
},
"uri": "http://127.0.0.1:8080/easemob- ... ot%3B,
"entities": [ ],
"data": [
{
"id": "1924",
"chatroom_id": "17177265635330",
"title": "具体了",
"desc": "就咯",
"startTime": 1495779917352,
"endTime": 1495779917352,
"anchor": "wuls",
"gift_count": 0,
"praise_count": 0,
"current_user_count": 8,
"max_user_count": 9,
"status": "ongoing",
"cover_picture_url": "",
"pc_pull_url": "rtmp://vlive3.rtmp.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1924_1",
"pc_push_url": "rtmp://publish3.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1924_1",
"mobile_pull_url": "rtmp://vlive3.rtmp.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1924_1",
"mobile_push_url": "rtmp://publish3.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1924_1"
},
{
"id": "1922",
"chatroom_id": "17175003856897",
"title": "香山",
"desc": "随便",
"startTime": 1495777760957,
"endTime": 1495777760957,
"anchor": "sx001",
"gift_count": 0,
"praise_count": 8,
"current_user_count": 1,
"max_user_count": 3,
"status": "ongoing",
"cover_picture_url": "http://127.0.0.1:8080/easemob- ... ot%3B,
"pc_pull_url": "rtmp://vlive3.rtmp.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1922_1",
"pc_push_url": "rtmp://publish3.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1922_1",
"mobile_pull_url": "rtmp://vlive3.rtmp.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1922_1",
"mobile_push_url": "rtmp://publish3.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1922_1"
}
],
"timestamp": 1496303336669,
"duration": 0,
"organization": "easemob-demo",
"applicationName": "chatdemoui",
"cursor": "ZGNiMjRmNGY1YjczYjlhYTNkYjk1MDY2YmEyNzFmODQ6aW06Y2hhdHJvb206ZWFzZW1vYi1kZW1vI2NoYXRkZW1vdWk6NDk",
"count": 2
}












获取直播间详情:curl -X GET -H "Authorization: Bearer [用户Token]" " https://a1.easemob.com/[应用OrgName]/[应用AppName]/[房间id]/status"响应:{
"action": "get",
"application": "4d7e4ba0-dc4a-11e3-90d5-e1ffbaacdaf5",
"uri": "http://127.0.0.1:8080/easemob- ... ot%3B,
"entities": [ ],
"data": {
"liveRoomID": "1946",
"status": "ongoing"
},
"timestamp": 1496234759930,
"duration": 0,
"organization": "easemob-demo",
"applicationName": "chatdemoui",
"count": 0
}














 
使用环信直播购物小程序遇到任何问题欢迎跟帖讨论。 查看全部
今天你看直播了吗?
拥有10亿微信生态用户的小程序已经成为了继移动互联后的又一个现象级风口,随着微信小程序对外开放实时音视频录制及播放等更多连接能力,小程序与直播强强联合,在各行各业找到了非常多的玩法,小程序直播相比微信直播和APP直播更加简洁、流畅、低延时、多入口等众多优势迅速向商业直播领域及泛娱乐直播领域蔓延。从小游戏、内容付费、工具、大数据、社交电商创业者到传统品牌商们,都在努力搭上小程序直播这辆快车,以免错过微信生态里新的流量洼地。
 
微信图片_20180725162426.jpg


作为一名环信生态圈资深开发者,本着对技术的热衷,对环信的眷恋和对党的忠诚,基于环信即时通讯云写了“直播购物小程序”,目前项目源码已全部免费开放,希望对有需求的企业和开发者提供一个思路和参考。
直播购物小程序源码github地址:https://github.com/YuTongNetworkTechnology/wechat_live/tree/master 
git打不开可直接点下面链接下载



预览图.jpg

直播购物小程序运行预览图 
 
小程序体验指南(仅需两步):
 
1、下载微信小程序开发工具,下载地址:https://developers.weixin.qq.c ... .html 
 
Catch9A07(07-20-17-38-30).jpg

2、导入源码:将附件的源码解压直接导入 


Catch1C69(07-20-17-38-30).jpg


环信小程序直播技术文档
一、 使用的技术
1、 环信IM直播室。
2、 微信小程序实时音视频播放组件live-player。
3、 推流软件(obs、易推流)等推流。
4、 视频流服务器(UCLOUD、七牛、腾讯)等视频流服务器。
二、 系统使用流程。
1、 视频推流软件将视频流推到流服务器。
2、 打开视频直播demo小程序注册环信账号。
3、 进入软件直播室进行测试。
三、 技术流程及使用的SDk
1、 注册环信账号
打开https://www.easemob.com/ 环信官网,点击右上角注册按钮,选择[注册即时通讯云]
1.png

填写对相关信息进行注册

2.png

注册成功后进行登录
3.png

注:新注册用户需进行账号的认证。
2、 直播应用创建
登录成功点击应用列表选择创建应用
4.png

输入应用名称等信息
 

5.png

创建成功后点击应用进入

6.png

需要注意的是应用的OrgName 和AppName这两个是以后都需要用到的两个参数变量
7.png

3、 直播创建
1)在创建直播之前需要对应用进行设置首先需要设置应用的直播流地址
第一步获取应用管理员的Token
curl -X POST "https://a1.easemob.com/[应用OrgName]/[应用AppName]/token" -d '{"grant_type":"client_credentials","client_id":"[应用client_id]","client_secret":"[应用] client_secret"}'
返回格式
{
"access_token":"YWMtWY779DgJEeS2h9OR7fw4QgAAAUmO4Qukwd9cfJSpkWHiOa7MCSk0MrkVIco",
"expires_in":5184000,
"application":"c03b3e30-046a-11e4-8ed1-5701cdaaa0e4"












第二步设置直播流地址
curl -X POST -H "Authorization: Bearer [管理员Token]"  " https://a1.easemob.com/[应用OrgName]/[应用AppName]/liverooms/stream_url -d '{"pc_pull":"[pc拉流地址]","pc_push":"[pc推流地址]","mobile_pull":"[手机拉流地址]","mobile_push":"[手机推流地址]"}'"
成功返回格式:
{
"action": "post",
"application": "e1a09de0-0e03-11e7-ad8e-a1d913615409",
"uri": "http://127.0.0.1:8080/easemob- ... ot%3B,
"entities": [ ],
"data": {
"pc_pull": true,
"mobile_push": true,
"mobile_pull": true,
"pc_push": true
},
"timestamp": 1494084474885,
"duration": 1,
"organization": "easemob-demo",
"applicationName": "chatdemoui"
}












2)创建主播
点击IM用户

8.png

点击注册IM用户

9.png

填写用户信息

10.png

创建用户的过程同样也可以通过REST API形式进行
curl -X POST -i " https://a1.easemob.com/[应用OrgName]/[应用AppName]/users" -d '{"username":"[用户名]","password":"[密码]"}'

注:应用必须为开放注册

11.png

将注册的用户添加为主播
curl -X POST -H "Authorization: [管理员Token]"  https://a1.easemob.com/[应用OrgName]/[应用AppName]/super_admin -d'{"superadmin":"[IM用户名]"}'
返回结果示例:
{
"action": "post",
"application": "4d7e4ba0-dc4a-11e3-90d5-e1ffbaacdaf5",
"uri": "http://127.0.0.1:8080/easemob- ... ot%3B,
"entities": [ ],
"data": {
"result": "success"
},
"timestamp": 1496236798886,
"duration": 0,
"organization": "easemob-demo",
"applicationName": "chatdemoui"
}












3)创建直播
点击直播

12.png

点击新建房间

13.png

填写房间信息
14.png

创建房间同时也可以使用REST API形式进行详情可以查看http://docs.easemob.com/im/live/server-integration环信官方文档。
4、 小程序demo集成使用
小程序直播购物demo集成官方WebIM SDK详情请查看https://github.com/easemob/webim-weixin-xcx
Demo具体配置如下
打开demo 下sdk配置文件

15.png

修改appkey为自己应用的appkey

16.png

打开pages/live/index.js修改房间默认拉流地址及直播间房间号

17.png

四、 扩展说明
Demo中房间为固定测试房间,实际使用中应获取环信直播的房间信息及房间列表。具体如下:
获取直播间列表:
curl -X GET -H "Authorization: Bearer  [用户Token]"  https://a1.easemob.com/[应用OrgName]/[应用AppName]/liverooms?ongoing=true&limit=[获取数量]&cursor=[游标地址(不填写为充开始查询)]

响应:
{
"action": "get",
"application": "4d7e4ba0-dc4a-11e3-90d5-e1ffbaacdaf5",
"params": {
"cursor": [
"ZGNiMjRmNGY1YjczYjlhYTNkYjk1MDY2YmEyNzFmODQ6aW06Y2hhdHJvb206ZWFzZW1vYi1kZW1vI2NoYXRkZW1vdWk6MzE"
],
"ongoing": [
"true"
],
"limit": [
"2"
]
},
"uri": "http://127.0.0.1:8080/easemob- ... ot%3B,
"entities": [ ],
"data": [
{
"id": "1924",
"chatroom_id": "17177265635330",
"title": "具体了",
"desc": "就咯",
"startTime": 1495779917352,
"endTime": 1495779917352,
"anchor": "wuls",
"gift_count": 0,
"praise_count": 0,
"current_user_count": 8,
"max_user_count": 9,
"status": "ongoing",
"cover_picture_url": "",
"pc_pull_url": "rtmp://vlive3.rtmp.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1924_1",
"pc_push_url": "rtmp://publish3.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1924_1",
"mobile_pull_url": "rtmp://vlive3.rtmp.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1924_1",
"mobile_push_url": "rtmp://publish3.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1924_1"
},
{
"id": "1922",
"chatroom_id": "17175003856897",
"title": "香山",
"desc": "随便",
"startTime": 1495777760957,
"endTime": 1495777760957,
"anchor": "sx001",
"gift_count": 0,
"praise_count": 8,
"current_user_count": 1,
"max_user_count": 3,
"status": "ongoing",
"cover_picture_url": "http://127.0.0.1:8080/easemob- ... ot%3B,
"pc_pull_url": "rtmp://vlive3.rtmp.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1922_1",
"pc_push_url": "rtmp://publish3.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1922_1",
"mobile_pull_url": "rtmp://vlive3.rtmp.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1922_1",
"mobile_push_url": "rtmp://publish3.cdn.ucloud.com.cn/ucloud/easemob-demo_chatdemoui_1922_1"
}
],
"timestamp": 1496303336669,
"duration": 0,
"organization": "easemob-demo",
"applicationName": "chatdemoui",
"cursor": "ZGNiMjRmNGY1YjczYjlhYTNkYjk1MDY2YmEyNzFmODQ6aW06Y2hhdHJvb206ZWFzZW1vYi1kZW1vI2NoYXRkZW1vdWk6NDk",
"count": 2
}












获取直播间详情:
curl -X GET -H "Authorization: Bearer [用户Token]" " https://a1.easemob.com/[应用OrgName]/[应用AppName]/[房间id]/status"
响应:
{
"action": "get",
"application": "4d7e4ba0-dc4a-11e3-90d5-e1ffbaacdaf5",
"uri": "http://127.0.0.1:8080/easemob- ... ot%3B,
"entities": [ ],
"data": {
"liveRoomID": "1946",
"status": "ongoing"
},
"timestamp": 1496234759930,
"duration": 0,
"organization": "easemob-demo",
"applicationName": "chatdemoui",
"count": 0
}














 
使用环信直播购物小程序遇到任何问题欢迎跟帖讨论。
19
评论

【新手快速入门】集成环信常见问题+解决方案汇总

dujiepeng 发表了文章 • 24396 次浏览 • 2017-05-22 15:51 • 来自相关话题

   这里整理了集成环信的常见问题和一些功能的实现思路,希望能帮助到大家。感谢热心的开发者贡献,大家在观看过程中有不明白的地方欢迎直接跟帖咨询。
 
ios篇
APNs证书创建和上传到环信后台头像昵称的简述和处理方案音视频离线推送Demo实现环信服务器聊天记录保存多久?离线收不到好友请求IOS中环信聊天窗口如何实现文件发送和预览的功能ios集成常见问题环信推送的一些常见问题实现名片|红包|话题聊天室等自定义cell
 
Android篇
Android sdk 的两种导入方式环信3.0SDK集成小米推送教程EaseUI库中V4、v7包冲突解决方案Android EaseUI里的百度地图替换为高德地图android扩展消息(名片集成)关于会话列表的置顶聊天java.lang.UnsatisfiedLinkError: 的问题android 端 app 后台被杀死收不到消息的解决方案
昵称头像篇
android中如何显示开发者服务器上的昵称和头像 Android中显示头像(接上一篇文章看)环信(Android)设置头像和昵称的方法(最简单暴力的基于环信demo的集成)IOS中如何显示开发者服务器上的昵称和头像【环信公开课第12期视频回放】-所有关于环信IM昵称头像的问题听这课就够了
 
直播篇
一言不合你就搞个直播APP
 
客服集成
IM-SDK和客服SDK并存开发指南—Android篇IM-SDK和客服SDK并存开发指南—iOS篇
 
开源项目
Android简版demoios简版demo凡信2.0:超仿微信的开源项目 凡信3.0:携直播和红包而来高仿微信:Github 3,515 Star方圆十里:环信编程大赛冠军项目泛聊:定一个小目标写一个QQSlack聊天机器人:一天时间做一个聊天机器人TV视频通话:在电视上视频通话视频通话:Android手机视频通话酷信:ios高仿微信公众号助手:与订阅用户聊天沟通
 
持续更新ing...小伙伴们还有什么想知道欢迎跟帖提出。
  查看全部
   这里整理了集成环信的常见问题和一些功能的实现思路,希望能帮助到大家。感谢热心的开发者贡献,大家在观看过程中有不明白的地方欢迎直接跟帖咨询。
 
ios篇

 
Android篇

昵称头像篇

 
直播篇
  1. 一言不合你就搞个直播APP

 
客服集成
  1. IM-SDK和客服SDK并存开发指南—Android篇
  2. IM-SDK和客服SDK并存开发指南—iOS篇

 
开源项目

 
持续更新ing...小伙伴们还有什么想知道欢迎跟帖提出。
 
0
评论

如何用 30 天入门年薪 30 万的技术领域? 技术 深度学习

sunshine123 发表了文章 • 91 次浏览 • 2019-10-08 17:18 • 来自相关话题

2016年3月15日,人机大战第五场在韩国首尔进行,经过长达5个小时搏杀,李世石认输,最终李世石与AlphaGo总比分定格在1比4。
值得注意的是:李世石仅在3月15日结束的第四场对战中取得了唯一的一场胜利。而支撑机器取胜的核心技术,就是深度学习。
自此之后,深度学习开始逐渐进入大家的视野。

01
什么是深度学习?

深度学习其实是机器学习(Machine Learning)的一个分支学科,而机器学习主要是研究数据之言的关系的,比如它可以用来分析性别、年龄、学历、职业等因素之间的数学关系。
显而易见,这些因果关系并不是一个简单的线性关系就能解决的。但是深度学习通过多非线性模型表示数据之间的关系,从而确定数据之间的相互关系是什么。

02
深度学习已经融入到了人们的日常生活中

现在汽车的自动驾驶、短信邮件的自动回复、扫地机器人,以及在围棋中击败最优秀人类选手的软件,都应用到了深度学习。甚至骚扰电话,也有很多是AI机器人打过来的,可能电话那头和你聊得正酣、声音甜美的客服妹子只是一串代码。
因此,阿里达摩院的工程师们,受不了骚扰骚扰电话的骚扰,发起了“二哈”AI,让机器人与骚扰电话自动聊天。
大家感受一下这对话画风





 
总之,AI机器人的骚扰电话,正在用AI进行解决。

03

可能是编程领域薪资最高的岗位

而与此同时学习深度学习的人也开始变得多了起来,但由于深度学习涉及面比较广,很多学者会对此望而却步。然而如果学好深度学习,不仅仅可以给你带来高薪资,也有可能会给你带来很多的荣誉!因为在此领域还是非常缺乏人才的!
大家可以看一份数据





 
平均月薪已达到3w,年薪36w。





 
就应届生深度学习岗的薪资,都已经达到了很多其他互联网职业3年工作经验都难以企及的水平。
现在各个大厂,也都有深度学习相关的项目,而且待遇方面也是极其的诱人。
百度推出“少帅计划”,针对30岁以下的深度学习科学家,开出100万以上年薪
阿里巴巴对外宣布将通过校园招聘组建一支规模达数百人的 NASA“青年军”
华为更是开出200万年薪招聘应届毕业生,其专业均为最前沿的人工智能领域
作为一个有追求的程序员,相信你在脑海中闪过无数次这样的疑问:如何掌握深度学习?

04

如何掌握深度学习?

目前市场对于深度学习的需求,早已出现供不应求的现象,但太多工程师想入门或者转行却不知从何开始,很多初学者都会有这样的困惑:
一边要熟练掌握线性代数、矩阵计算,一边还要搞概率论
一边要去研究各种库与框架,一边学习如何在用编程语言实现算法
一边学习如何制作数据集、特征提取,一边微调参数,选择合适的算法
这样一轮下来,深度学习还没有开始就已经走上了放弃之路。
放眼网上现有的一些深度学习课程,经常会发现有些知识点覆盖不全,或者学习门槛较高,研究性的问题不多,或者说只关注面试,而忽略了底层的逻辑以及真实案例的实操。
因此,我们通上百名深度学习用户的调研,与老师花费上千小时的时间,提炼了102节精华课程。
我们希望通过“从零开始深度学习”这门课程,让你0基础入门深度学习,建立起完整的学习路径,同时通过“智能问答模型”的实战案例,将所学知识学以致用。
#购课即送王海良老师在京东原价69的《智能问答与深度学习》实体书一本#

讲师介绍
王海良:Chatopera联合创始人&CEO,微软人工智能最有价值专家,先后工作于IBM软件开发实验室和创新中心。
李卓桓:25年编程经验,曾任优酷网首席科学家、叽歪网创始人,水木清华BBS站长,紫霞BBS站长。
林旭鸣:北京邮电大学模式识别实验室研究生,任职阿里巴巴的阿里小蜜团队。
陈可心:香港大学硕士,任职经历包括:微软中国、今日头条研发中心,联想香港人工智能中心以及联合国亚太分部。
李思珍:现任职今日头条,主要工作实现人机交互系统的意图识别和关键词优化。

#课程介绍#
相信很多小伙伴,在入门深度学习的路上,会被一些数据基础搞得犯怵,搭建环境的时候也许会遇到很多坑。
因此,我们把课程分为五个模块:数学基础、Python编程语言基础、深度学习初步、深度学习深化、智能问答模型实践,带你一探深度学习的究竟,希望你通过此次学习,不仅掌握原理更能动手实操。
除了上述102节视频课程,我们还为这次课程搭配了书籍《深度学习与智能问答》,一边跟着视频实操,一边阅读书籍巩固,学习效果double~





 
如何保证你的学习效果?
由基础的数学学习开始,逐渐建立完整的深度学习知识体系
五大模块,由知识学习到真实案例的实践
购课送原价69元深度学习实体书,边学边读,加深记忆
建立一个互助、监督的高效学习社群,随时交流问题

购课须知
本课程包含哪些内容?
包含102节视频课(20+小时)+实体书(京东原价69)

上课形式是怎样的?
课程授课形式为:视频+群答疑

#如何报名#
限时特惠:99元(原价199元)
拼团特惠69元~
图书(含邮寄) + 100+节视频课程,每课仅需6毛钱

扫码立即抢购!
平均一天2元,就是一瓶水钱
坚持30天,换你一次进入高薪技术领域的机会
如果对课程有疑问
欢迎扫码回复“1”进课程咨询群





 
 
  查看全部
2016年3月15日,人机大战第五场在韩国首尔进行,经过长达5个小时搏杀,李世石认输,最终李世石与AlphaGo总比分定格在1比4。
值得注意的是:李世石仅在3月15日结束的第四场对战中取得了唯一的一场胜利。而支撑机器取胜的核心技术,就是深度学习。
自此之后,深度学习开始逐渐进入大家的视野。

01
什么是深度学习?


深度学习其实是机器学习(Machine Learning)的一个分支学科,而机器学习主要是研究数据之言的关系的,比如它可以用来分析性别、年龄、学历、职业等因素之间的数学关系。
显而易见,这些因果关系并不是一个简单的线性关系就能解决的。但是深度学习通过多非线性模型表示数据之间的关系,从而确定数据之间的相互关系是什么。

02
深度学习已经融入到了人们的日常生活中


现在汽车的自动驾驶、短信邮件的自动回复、扫地机器人,以及在围棋中击败最优秀人类选手的软件,都应用到了深度学习。甚至骚扰电话,也有很多是AI机器人打过来的,可能电话那头和你聊得正酣、声音甜美的客服妹子只是一串代码。
因此,阿里达摩院的工程师们,受不了骚扰骚扰电话的骚扰,发起了“二哈”AI,让机器人与骚扰电话自动聊天。
大家感受一下这对话画风

1.gif

 
总之,AI机器人的骚扰电话,正在用AI进行解决。

03

可能是编程领域薪资最高的岗位


而与此同时学习深度学习的人也开始变得多了起来,但由于深度学习涉及面比较广,很多学者会对此望而却步。然而如果学好深度学习,不仅仅可以给你带来高薪资,也有可能会给你带来很多的荣誉!因为在此领域还是非常缺乏人才的!
大家可以看一份数据

2.png

 
平均月薪已达到3w,年薪36w。

4.png

 
就应届生深度学习岗的薪资,都已经达到了很多其他互联网职业3年工作经验都难以企及的水平。
现在各个大厂,也都有深度学习相关的项目,而且待遇方面也是极其的诱人。
百度推出“少帅计划”,针对30岁以下的深度学习科学家,开出100万以上年薪
阿里巴巴对外宣布将通过校园招聘组建一支规模达数百人的 NASA“青年军”
华为更是开出200万年薪招聘应届毕业生,其专业均为最前沿的人工智能领域
作为一个有追求的程序员,相信你在脑海中闪过无数次这样的疑问:如何掌握深度学习?

04

如何掌握深度学习?


目前市场对于深度学习的需求,早已出现供不应求的现象,但太多工程师想入门或者转行却不知从何开始,很多初学者都会有这样的困惑:
一边要熟练掌握线性代数、矩阵计算,一边还要搞概率论
一边要去研究各种库与框架,一边学习如何在用编程语言实现算法
一边学习如何制作数据集、特征提取,一边微调参数,选择合适的算法
这样一轮下来,深度学习还没有开始就已经走上了放弃之路。
放眼网上现有的一些深度学习课程,经常会发现有些知识点覆盖不全,或者学习门槛较高,研究性的问题不多,或者说只关注面试,而忽略了底层的逻辑以及真实案例的实操。
因此,我们通上百名深度学习用户的调研,与老师花费上千小时的时间,提炼了102节精华课程。
我们希望通过“从零开始深度学习”这门课程,让你0基础入门深度学习,建立起完整的学习路径,同时通过“智能问答模型”的实战案例,将所学知识学以致用。
#购课即送王海良老师在京东原价69的《智能问答与深度学习》实体书一本#

讲师介绍
王海良:Chatopera联合创始人&CEO,微软人工智能最有价值专家,先后工作于IBM软件开发实验室和创新中心。
李卓桓:25年编程经验,曾任优酷网首席科学家、叽歪网创始人,水木清华BBS站长,紫霞BBS站长。
林旭鸣:北京邮电大学模式识别实验室研究生,任职阿里巴巴的阿里小蜜团队。
陈可心:香港大学硕士,任职经历包括:微软中国、今日头条研发中心,联想香港人工智能中心以及联合国亚太分部。
李思珍:现任职今日头条,主要工作实现人机交互系统的意图识别和关键词优化。

#课程介绍#
相信很多小伙伴,在入门深度学习的路上,会被一些数据基础搞得犯怵,搭建环境的时候也许会遇到很多坑。
因此,我们把课程分为五个模块:数学基础、Python编程语言基础、深度学习初步、深度学习深化、智能问答模型实践,带你一探深度学习的究竟,希望你通过此次学习,不仅掌握原理更能动手实操。
除了上述102节视频课程,我们还为这次课程搭配了书籍《深度学习与智能问答》,一边跟着视频实操,一边阅读书籍巩固,学习效果double~

5.jpg

 
如何保证你的学习效果?
由基础的数学学习开始,逐渐建立完整的深度学习知识体系
五大模块,由知识学习到真实案例的实践
购课送原价69元深度学习实体书,边学边读,加深记忆
建立一个互助、监督的高效学习社群,随时交流问题

购课须知
本课程包含哪些内容?
包含102节视频课(20+小时)+实体书(京东原价69)

上课形式是怎样的?
课程授课形式为:视频+群答疑

#如何报名#
限时特惠:99元(原价199元)
拼团特惠69元~
图书(含邮寄) + 100+节视频课程,每课仅需6毛钱

扫码立即抢购!
平均一天2元,就是一瓶水钱
坚持30天,换你一次进入高薪技术领域的机会
如果对课程有疑问
欢迎扫码回复“1”进课程咨询群

6.jpg

 
 
 
0
评论

李理:详解卷积神经网络 李理 深度学习

beyond 发表了文章 • 1905 次浏览 • 2017-03-06 16:46 • 来自相关话题

本系列文章面向深度学习研发者,希望通过Image Caption Generation,一个有意思的具体任务,深入浅出地介绍深度学习的知识。本系列文章涉及到很多深度学习流行的模型,如CNN,RNN/LSTM,Attention等。本文为第7篇。

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

相关文章: 
李理:从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
$ipython
>>> import network3
>>> from network3 import Network
>>> from network3 import ConvPoolLayer, FullyConnectedLayer, SoftmaxLayer
>>> training_data, validation_data, test_data = network3.load_data_shared()
>>> mini_batch_size = 10
>>> net = Network([
FullyConnectedLayer(n_in=784, n_out=100),
SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
>>> net.SGD(training_data, 60, mini_batch_size, 0.1,
validation_data, test_data)
得到的分类准确率是97.8%。这是在test_data上的准确率,这个模型使用训练数据训练,并根据validation_data来选择当前最好的模型。使用validation数据来可以避免过拟合。读者运行时可能结果会有一些差异,因为模型的参数是随机初始化的。
 
改进版本1
 
我们首先在输入的后面增加一个卷积层。我们使用5 5的局部感知域,stride等于1,20个特征映射。然后接一个2 2的max-pooling层。之后接一个全连接的层,最后是softmax(仿射变换加softmax):




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

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

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

如果要用gpu,可以把上面的命令保存到一个文件test.py,然后:$THEANO_FLAGS=mode=FAST_RUN,device=gpu,floatX=float32 python test.py 在这个网络结构中,我们吧卷积和池化层看出一个整体。这只是一种习惯。network3.py会把它们当成一个整体,每个卷积层后面都会跟一个池化层。但实际的一些卷积神经网络并不都要接池化层。
 
改进版本2
 
我们再加入第二个卷积-池化层。这个卷积层插入在第一个卷积层和全连接层中间。我们使用同样的5×5的局部感知域和2×2的max-pooling。代码如下:>>> net = Network([
ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28),
filter_shape=(20, 1, 5, 5),
poolsize=(2, 2)),
ConvPoolLayer(image_shape=(mini_batch_size, 20, 12, 12),
filter_shape=(40, 20, 5, 5),
poolsize=(2, 2)),
FullyConnectedLayer(n_in=40*4*4, n_out=100),
SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
>>> net.SGD(training_data, 60, mini_batch_size, 0.1,
validation_data, test_data) 【注意图片的大小,开始是(mini_batch_size, 1, 28 ,28),经过一个20个5 5的卷积池层后变成了(mini_batch_size, 20, 24,24),然后在经过2 2的max-pooling后变成了(mini_batch_size, 20, 12, 12)。然后是40个5*5的卷积层,变成了(mini_batch_size, 40, 8, 8),然后是max-pooling得到(mini_batch_size, 40, 4, 4)。然后是全连接的层】 
这个模型得到99.6%的准确率!

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

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

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

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

扩展数据。

深度学习非常依赖于数据。我们可以根据任务的特点”构造“新的数据。一种简单的方法是把训练数据里的数字进行一下平移,旋转等变换。虽然理论上卷积神经网络能学到与位置无关的特征,但如果训练数据里数字总是出现在固定的位置,实际的模型也不一定能学到。所以我们构造一些这样的数据效果会更好。$ python expand_mnist.pyexpand_mnist.py这个脚本就会扩展数据。它只是简单的把图片向上下左右各移动了一个像素。扩展后训练数据从50000个变成了250000个。
 
接下来我们用扩展后的数据来训练模型:>>> expanded_training_data, _, _ = network3.load_data_shared(
"../data/mnist_expanded.pkl.gz")
>>> net = Network([
ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28),
filter_shape=(20, 1, 5, 5),
poolsize=(2, 2),
activation_fn=ReLU),
ConvPoolLayer(image_shape=(mini_batch_size, 20, 12, 12),
filter_shape=(40, 20, 5, 5),
poolsize=(2, 2),
activation_fn=ReLU),
FullyConnectedLayer(n_in=40*4*4, n_out=100, activation_fn=ReLU),
SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
>>> net.SGD(expanded_training_data, 60, mini_batch_size, 0.03,
validation_data, test_data, lmbda=0.1)这个模型的准确率是99.37%。扩展数据看起来非常trival,但是却极大的提高了识别准确率。

改进版本5

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

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

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

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

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

改进版本6

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

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




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

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

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

更进一步

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

What’s Next?

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

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

相关文章: 
李理:从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条边(连接)。连接方式如下图所示:
001.jpg

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

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

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

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

局部感知域

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

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

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

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

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

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

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

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

共享权值

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

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

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

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

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

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

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

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

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

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

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

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

池化(Pooling)

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

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

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

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

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

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

放到一起

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

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

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

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

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

CNN实战

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

代码

首先得到代码:  git clone

安装theano

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

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

baseline

首先我们实现一个baseline的系统,我们构建一个只有一个隐藏层的3层全连接网络,隐藏层100个神经元。我们训练时60个epoch,使用learning rate $\eta = 0.1$,batch大小是10,没有正则化:
$cd src
$ipython
>>> import network3
>>> from network3 import Network
>>> from network3 import ConvPoolLayer, FullyConnectedLayer, SoftmaxLayer
>>> training_data, validation_data, test_data = network3.load_data_shared()
>>> mini_batch_size = 10
>>> net = Network([
FullyConnectedLayer(n_in=784, n_out=100),
SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
>>> net.SGD(training_data, 60, mini_batch_size, 0.1,
validation_data, test_data)

得到的分类准确率是97.8%。这是在test_data上的准确率,这个模型使用训练数据训练,并根据validation_data来选择当前最好的模型。使用validation数据来可以避免过拟合。读者运行时可能结果会有一些差异,因为模型的参数是随机初始化的。
 
改进版本1
 
我们首先在输入的后面增加一个卷积层。我们使用5 5的局部感知域,stride等于1,20个特征映射。然后接一个2 2的max-pooling层。之后接一个全连接的层,最后是softmax(仿射变换加softmax):
011.jpg

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

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

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

如果要用gpu,可以把上面的命令保存到一个文件test.py,然后:
$THEANO_FLAGS=mode=FAST_RUN,device=gpu,floatX=float32 python test.py 
在这个网络结构中,我们吧卷积和池化层看出一个整体。这只是一种习惯。network3.py会把它们当成一个整体,每个卷积层后面都会跟一个池化层。但实际的一些卷积神经网络并不都要接池化层。
 
改进版本2
 
我们再加入第二个卷积-池化层。这个卷积层插入在第一个卷积层和全连接层中间。我们使用同样的5×5的局部感知域和2×2的max-pooling。代码如下:
>>> net = Network([
ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28),
filter_shape=(20, 1, 5, 5),
poolsize=(2, 2)),
ConvPoolLayer(image_shape=(mini_batch_size, 20, 12, 12),
filter_shape=(40, 20, 5, 5),
poolsize=(2, 2)),
FullyConnectedLayer(n_in=40*4*4, n_out=100),
SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
>>> net.SGD(training_data, 60, mini_batch_size, 0.1,
validation_data, test_data)
【注意图片的大小,开始是(mini_batch_size, 1, 28 ,28),经过一个20个5 5的卷积池层后变成了(mini_batch_size, 20, 24,24),然后在经过2 2的max-pooling后变成了(mini_batch_size, 20, 12, 12)。然后是40个5*5的卷积层,变成了(mini_batch_size, 40, 8, 8),然后是max-pooling得到(mini_batch_size, 40, 4, 4)。然后是全连接的层】 
这个模型得到99.6%的准确率!

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

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

使用ReLU激活函数。ReLU的定义是:
ReLU(x)=max(0,x)
>>> from network3 import ReLU
>>> net = Network([
ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28),
filter_shape=(20, 1, 5, 5),
poolsize=(2, 2),
activation_fn=ReLU),
ConvPoolLayer(image_shape=(mini_batch_size, 20, 12, 12),
filter_shape=(40, 20, 5, 5),
poolsize=(2, 2),
activation_fn=ReLU),
FullyConnectedLayer(n_in=40*4*4, n_out=100, activation_fn=ReLU),
SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
>>> net.SGD(training_data, 60, mini_batch_size, 0.03,
validation_data, test_data, lmbda=0.1)

使用ReLU后准确率从99.06%提高到99.23%。从作者的经验来看,ReLU总是要比sigmoid激活函数要好。

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

扩展数据。

深度学习非常依赖于数据。我们可以根据任务的特点”构造“新的数据。一种简单的方法是把训练数据里的数字进行一下平移,旋转等变换。虽然理论上卷积神经网络能学到与位置无关的特征,但如果训练数据里数字总是出现在固定的位置,实际的模型也不一定能学到。所以我们构造一些这样的数据效果会更好。
$ python expand_mnist.py
expand_mnist.py这个脚本就会扩展数据。它只是简单的把图片向上下左右各移动了一个像素。扩展后训练数据从50000个变成了250000个。
 
接下来我们用扩展后的数据来训练模型:
>>> expanded_training_data, _, _ = network3.load_data_shared(
"../data/mnist_expanded.pkl.gz")
>>> net = Network([
ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28),
filter_shape=(20, 1, 5, 5),
poolsize=(2, 2),
activation_fn=ReLU),
ConvPoolLayer(image_shape=(mini_batch_size, 20, 12, 12),
filter_shape=(40, 20, 5, 5),
poolsize=(2, 2),
activation_fn=ReLU),
FullyConnectedLayer(n_in=40*4*4, n_out=100, activation_fn=ReLU),
SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
>>> net.SGD(expanded_training_data, 60, mini_batch_size, 0.03,
validation_data, test_data, lmbda=0.1)
这个模型的准确率是99.37%。扩展数据看起来非常trival,但是却极大的提高了识别准确率。

改进版本5

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

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

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

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

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


改进版本6

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

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

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

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

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

更进一步

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

What’s Next?

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

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

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

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

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

简介

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




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

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

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




首先来看导数的定义:




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




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




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




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




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

复杂表达式的链式法则

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




对y的求导也是类似的。

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

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

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




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

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

反向传播算法的直觉解释

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

Sigmoid模块的例子

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




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




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




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

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




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




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




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




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




上面我们看到把




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




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




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




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

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




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

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

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

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

Staged computation练习




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

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

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

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




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

(7) invden = 1.0 / den

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

(6) den = sigx + xpysqr

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

(5) xpysqr = xpy**2

local gradient是2*xpy,再乘以dxpysqr

(4) xpy = x + y

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

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

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

(2) num = x + sigy

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

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

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

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

梯度的矩阵运算

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

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

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

首先介绍graident of matrix

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




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

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




根据矩阵乘法的定义




我们可以计算:




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




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




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




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




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

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

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

简介

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

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

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

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

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

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

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

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

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

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

复杂表达式的链式法则

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

对y的求导也是类似的。

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

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

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

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

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

反向传播算法的直觉解释

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

Sigmoid模块的例子

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

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

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

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

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

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

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

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

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

上面我们看到把
018.jpg

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

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

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

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

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

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

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

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

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

Staged computation练习
023.jpg

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

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

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

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

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

(7) invden = 1.0 / den

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

(6) den = sigx + xpysqr

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

(5) xpysqr = xpy**2

local gradient是2*xpy,再乘以dxpysqr

(4) xpy = x + y

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

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

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

(2) num = x + sigy

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

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

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

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

梯度的矩阵运算

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

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

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

首先介绍graident of matrix

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

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

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

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

我们可以计算:
028.jpg

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2.2 多层神经网络

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

2.2.1 手写数字识别问题

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

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




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




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

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

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

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

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




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

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

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

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




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









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




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




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




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

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

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

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

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

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

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

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




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

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

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

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

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

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

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

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

求梯度有如下4种方法:

1.手工求解析解

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

2.数值解

使用极限的定义:




3.机器符号计算

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

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

4.自动梯度

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

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

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

2.2.4 编程实战

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

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

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

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

2.运行

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

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

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

3.1 mnist_loader.load_data_wrapper函数

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

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

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

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

   test_data的格式和validation_data一样。

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

3.2 Network类的构造函数

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

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

3.3 feedforward函数

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

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

3.4 SGD函数

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

2.2 多层神经网络

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

2.2.1 手写数字识别问题

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

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

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

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

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

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

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

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

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

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

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

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

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


006.png

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

求梯度有如下4种方法:

1.手工求解析解

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

2.数值解

使用极限的定义:
011.png

3.机器符号计算

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

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

4.自动梯度

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

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


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

2.2.4 编程实战

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

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

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

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

2.运行

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

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

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

3.1 mnist_loader.load_data_wrapper函数

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

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

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

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

   test_data的格式和validation_data一样。

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

3.2 Network类的构造函数

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

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

3.3 feedforward函数

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

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

3.4 SGD函数

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


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

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

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

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

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

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

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

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

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