精华内容
下载资源
问答
  • 容易写错的笔顺

    万次阅读 2018-05-14 20:06:23
    个笔画笔顺特别容易写错的汉字听王美金老师上《可爱的草塘》(人教社五年级上册),示范书写“舀”时,特别强调“舀”下面的“臼”的笔画笔顺,第一笔是短撇,第二笔是竖,第三笔是短横,第四笔是横折,第五笔是...
    个笔画笔顺特别容易写错的汉字
    听王美金老师上《可爱的草塘》(人教社五年级上册),
    示范书写“舀”字时,特别强调“舀”字下面的“臼”的笔画笔顺,
    第一笔是短撇,第二笔是竖,第三笔是短横,第四笔是横折,第五笔是短横,第六笔是长横(即从左到右)。
    我很惊讶(其他老师也很惊讶),以为王老师讲错了。
    这个字,我历来都是从外到内,第一笔写短撇,第二笔写竖,第三笔写横折,第四笔写短横,第五笔写短横,第六笔写长横。
    课间,我悄悄问王老师“是不是讲错了?”
    王老师说:“没有,这个字就是这样写的。”
    我将信将疑,回家后立即查阅工具书(《小学生常用字规范笔顺》语文出版社1999年5月版),果不其然!
    而且由“臼”构成的常用字都按这样的笔顺,如:舀、稻、滔、蹈、掐、焰、陷、舅、鼠、搜、插、嫂、艘,等等,
    我竟然差一点儿错了一辈子!
    顺势,我又查了几个笔画笔顺似是而非的字,如“敝”“兆”“臣”等。
    现在我录下来,供同伴们参考。
    “敝”:共11笔写成,先写左上边的“丷”,再写下面的“冂”,第五笔写中间的长竖,最后写“攵”。
    由“敝”构成的常用字有:蔽、撇、弊,等等。
    “兆”:共6笔写成,第一笔写长撇,第二笔写左边的点,第三笔写左边的提,第四笔写竖弯钩,最后两笔是右边的两点。
    由“兆”构成的常用字有:桃、逃、挑,等等。
    “臣”:共6笔写成,第一笔横,第二笔短竖,第三笔横折,第四笔横,第五笔短竖,第六笔竖弯。
    特别注意凡含“匚”的汉字,都是先写上面的横,再写中间的部分,最后写竖弯。如:巨、卧、颐、藏、臧,等等。
    “匕”,共2笔,第一笔是撇,第二是竖弯钩。特别注意含“匕”部首的常用字,只有“北”的短撇不出头,其他如“化”“华”“花”等的短撇均出头。“兜”,共11画,先写上面中间的“白”,第六笔写左边的短撇,第七笔写左边的竖提,第八笔写右边的横折,第九笔写短横,最后写下面的“儿”字。
    还有如“凹”“凸”两个字也很容易写错,两个字都是5笔写成,“凹”的第二笔是横折弯;
    “凸”的第四笔是横折折折。有些字的笔顺很容易写错。根据《现代汉语通用字笔顺规范》,你写下面这些字的笔顺对吗?
    1、“匕”先写撇,后写竖弯钩。
    2、“万”先写横,再写横折钩,后写撇。这样容易把整个字写端正。
    3、“义”先写点,再写撇和捺。点在上边或左上边的要先写,如“门、斗”等;点在右边或字里面的要后写,如“玉、瓦”等。“母”字的最后三笔是点、横、点。
    4、“及”先写撇,再写横折折撇,后写捺。这个字和“乃”字形相近,但笔顺完全不同。
    5、“火”先写上面两笔,即点和撇,再写人字。
    6、“讯”右半部分的笔顺是:横斜钩(不是横折弯钩)、横、竖(不是撇)。
    7、“凸”第一笔先写左上的竖,接着短横和竖,然后写横折折折,最后写下边的长横。“凹”“凸”两个字都是5笔写成,“凹”的第二笔是横折弯。
    8、“出”先写竖折,然后写短竖,再写中间从上到下的长竖,最后是竖折和短竖。
    9、“贯”上边是先写竖折,再写横折,第三笔写里面的竖,最后写长横。“重”上面的撇和横写后,紧接着写日,再写竖,最后写下面两横(上短下长)。
    11、“脊”字上边的笔顺是先写左边的点和提,再写右边的撇和点,最后写中间的人。
    12、“敝”的左边先写上部的点、撇,接着写左下角的竖、横折钩,然后写中间的长竖,最后写里面的撇、点。1
    3、“爽”先写横,再从左到右写四个“×”,最后写“人”。
    14、“登”的右上角先写两撇,再写捺。
    15、“噩”字的横、竖写后,接着写上边的左右两个“口”,再写中间的横和横下的两个“口”,最后写一长横。这样写符合从上到下、先中间后两边的规则,与“王”字的笔顺不同。
    16、“车”、“牛” 两字在作左偏旁时,笔顺有变化。“车”字由横、折、横、竖改为横、折、竖、提;“牛”字由撇、横、横、竖改为撇、横、竖、提。
    17、“臼”的笔画笔顺,第一笔是短撇,第二笔是竖,第三笔是短横,第四笔是横折,第五笔是短横,第六笔是长横(即从左到右),而且由“臼”构成的常用字都按这样的笔顺,如:舀、稻、滔、蹈、掐、焰、陷、舅、鼠、搜、插、嫂、艘??
    18、“敝”:共11笔写成,先写左上边的“丷”,再写下面的“冂”,第五笔写中间的长竖,最后写“攵”。由“敝”构成的常用字有:蔽、撇、弊,等等。
    19、“兆”:共6笔写成,第一笔写长撇,第二笔写左边的点,第三笔写左边的提,第四笔写竖弯钩,最后两笔是右边的两点。由“兆”构成的常用字有:桃、逃、挑,等等。
    20、“臣”:共6笔写成,第一笔横,第二笔短竖,第三笔横折,第四笔横,第五笔短竖,第六笔竖弯。特别注意凡含“匚”的汉字,都是先写上面的横,再写中间的部分,最后写竖弯。如:巨、卧、颐、藏、臧,等等。
    21、“匕”,共2笔,第一笔是撇,第二是竖弯钩。特别注意含“匕”部首的常用字,只有“北”的短撇不出头,其他如“化”“华”“花”等的短撇均出头。
    22、“兜”,共11画,先写上面中间的“白”,第六笔写左边的短撇,第七笔写左边的竖提,第八笔写右边的横折,第九笔写短横,最后写下面的“儿”字。
                                                                                                                                                                             ---- 摘自百度百科
    收起
    展开全文
  • 如何优化MySQL千万级大表,我了6000的解读

    万次阅读 多人点赞 2019-10-21 20:03:03
    千万级大表如何优化,这是个很有技术含量的问题,通常我们的直觉思维都会跳转到拆分或者数据分区,在此我想做一些补充和梳理,想和大家做一些这方面的经验总结,也欢迎大家提出建议。 从开始脑海里开始也是...
        

    这是学习笔记的第 2138 篇文章

      640?wx_fmt=gif

    千万级大表如何优化,这是一个很有技术含量的问题,通常我们的直觉思维都会跳转到拆分或者数据分区,在此我想做一些补充和梳理,想和大家做一些这方面的经验总结,也欢迎大家提出建议。 

    从一开始脑海里开始也是火光四现,到不断的自我批评,后来也参考了一些团队的经验,我整理了下面的大纲内容。

    640?wx_fmt=png

    既然要吃透这个问题,我们势必要回到本源,我把这个问题分为三部分:

    “千万级”,“大表”,“优化”,

    也分别对应我们在图中标识的

    “数据量”,“对象”和“目标”。

    我来逐步展开说明一下,从而给出一系列的解决方案。 

    1.数据量:千万级

    千万级其实只是一个感官的数字,就是我们印象中的数据量大。 这里我们需要把这个概念细化,因为随着业务和时间的变化,数据量也会有变化,我们应该是带着一种动态思维来审视这个指标,从而对于不同的场景我们应该有不同的处理策略。

     

    1) 数据量为千万级,可能达到亿级或者更高

    通常是一些数据流水,日志记录的业务,里面的数据随着时间的增长会逐步增多,超过千万门槛是很容易的一件事情。

    2) 数据量为千万级,是一个相对稳定的数据量

    如果数据量相对稳定,通常是在一些偏向于状态的数据,比如有1000万用户,那么这些用户的信息在表中都有相应的一行数据记录,随着业务的增长,这个量级相对是比较稳定的。

    3) 数据量为千万级,不应该有这么多的数据

    这种情况是我们被动发现的居多,通常发现的时候已经晚了,比如你看到一个配置表,数据量上千万;或者说一些表里的数据已经存储了很久,99%的数据都属于过期数据或者垃圾数据。

    数据量是一个整体的认识,我们需要对数据做更近一层的理解,这就可以引出第二个部分的内容。 

    2.对象:数据表

    数据操作的过程就好比数据库中存在着多条管道,这些管道中都流淌着要处理的数据,这些数据的用处和归属是不一样的。

    一般根据业务类型把数据分为三种:

    (1)流水型数据

    流水型数据是无状态的,多笔业务之间没有关联,每次业务过来的时候都会产生新的单据,比如交易流水、支付流水,只要能插入新单据就能完成业务,特点是后面的数据不依赖前面的数据,所有的数据按时间流水进入数据库。

    (2)状态型数据

    状态型数据是有状态的,多笔业务之间依赖于有状态的数据,而且要保证该数据的准确性,比如充值时必须要拿到原来的余额,才能支付成功。

    (3)配置型数据

    此类型数据数据量较小,而且结构简单,一般为静态数据,变化频率很低。

    至此,我们可以对整体的背景有一个认识了,如果要做优化,其实要面对的是这样的3*3的矩阵,如果要考虑表的读写比例(读多写少,读少写多...),那么就会是3*3*4=24种,显然做穷举是不显示的,而且也完全没有必要,可以针对不同的数据存储特性和业务特点来指定不同的业务策略。 

    对此我们采取抓住重点的方式,把常见的一些优化思路梳理出来,尤其是里面的核心思想,也是我们整个优化设计的一把尺子,而难度决定了我们做这件事情的动力和风险。

    数据量增长情况

    数据表类型

    业务特点

    优化核心思想

    优化难度

    数据量为千万级,是一个相对稳定的数据量

    状态表

    OLTP业务方向

    能不拆就不拆读需求水平扩展

    ****

    数据量为千万级,可能达到亿级或者更高

    流水表

    OLTP业务的历史记录

    业务拆分,面向分布式存储设计

    ****

    OLAP业务统计数据源

    设计数据统计需求存储的分布式扩展

    ***

    数据量为千万级,不应该有这么多的数据

    配置表

    通用业务

    小而简,避免大一统

    *

    而对于优化方案,我想采用面向业务的维度来进行阐述。 

    3.目标:优化

    在这个阶段,我们要说优化的方案了,总结的有点多,相对来说是比较全了。

    整体分为五个部分:

    640?wx_fmt=png

    其实我们通常所说的分库分表等方案只是其中的一小部分,如果展开之后就比较丰富了。

    640?wx_fmt=png

    其实不难理解,我们要支撑的表数据量是千万级别,相对来说是比较大了,DBA要维护的表肯定不止一张,如何能够更好的管理,同时在业务发展中能够支撑扩展,同时保证性能,这是摆在我们面前的几座大山。

    我们分别来说一下这五类改进方案:

    优化设计方案1.规范设计

    在此我们先提到的是规范设计,而不是其他高大上的设计方案。

    黑格尔说:秩序是自由的第一条件。在分工协作的工作场景中尤其重要,否则团队之间互相牵制太多,问题多多。

    规范设计我想提到如下的几个规范,其实只是属于开发规范的一部分内容,可以作为参考。

    640?wx_fmt=png

    规范的本质不是解决问题,而是有效杜绝一些潜在问题,对于千万级大表要遵守的规范,我梳理了如下的一些细则,基本可以涵盖我们常见的一些设计和使用问题,比如表的字段设计不管三七二十一,都是varchar(500),其实是很不规范的一种实现方式,我们来展开说一下这几个规范。

    1)配置规范

    (1)MySQL数据库默认使用InnoDB存储引擎。

    (2)保证字符集设置统一,MySQL数据库相关系统、数据库、表的字符集使都用UTF8,应用程序连接、展示等可以设置字符集的地方也都统一设置为UTF8字符集。

    注:UTF8格式是存储不了表情类数据,需要使用UTF8MB4,可在MySQL字符集里面设置。在8.0中已经默认为UTF8MB4,可以根据公司的业务情况进行统一或者定制化设置。

    (3)MySQL数据库的事务隔离级别默认为RR(Repeatable-Read),建议初始化时统一设置为RC(Read-Committed),对于OLTP业务更适合。

    (4)数据库中的表要合理规划,控制单表数据量,对于MySQL数据库来说,建议单表记录数控制在2000W以内。

    (5)MySQL实例下,数据库、表数量尽可能少;数据库一般不超过50个,每个数据库下,数据表数量一般不超过500个(包括分区表)。

    2)建表规范

    (1)InnoDB禁止使用外键约束,可以通过程序层面保证。

    (2)存储精确浮点数必须使用DECIMAL替代FLOAT和DOUBLE。

    (3)整型定义中无需定义显示宽度,比如:使用INT,而不是INT(4)。

    (4)不建议使用ENUM类型,可使用TINYINT来代替。

    (5)尽可能不使用TEXT、BLOB类型,如果必须使用,建议将过大字段或是不常用的描述型较大字段拆分到其他表中;另外,禁止用数据库存储图片或文件。

    (6)存储年时使用YEAR(4),不使用YEAR(2)。

    (7)建议字段定义为NOT NULL。

    (8)建议DBA提供SQL审核工具,建表规范性需要通过审核工具审核后

    3)命名规范

    (1)库、表、字段全部采用小写。

    (2)库名、表名、字段名、索引名称均使用小写字母,并以“_”分割。

    (3)库名、表名、字段名建议不超过12个字符。(库名、表名、字段名支持最多64个字符,但为了统一规范、易于辨识以及减少传输量,统一不超过12字符)

    (4)库名、表名、字段名见名知意,不需要添加注释。

    对于对象命名规范的一个简要总结如下表4-1所示,供参考。

    命名列表

    对象中文名称

    对象英文全称

    MySQL对象简写

    视图

    view

    view_

    函数

    function

    func_

    存储过程

    procedure

    proc_

    触发器

    trigger

    trig_

    普通索引

    index

    idx_

    唯一索引

    unique index

    uniq_

    主键索引

    primary key

    pk_

    4)索引规范

    (1)索引建议命名规则:idx_col1_col2[_colN]、uniq_col1_col2[_colN](如果字段过长建议采用缩写)。

    (2)索引中的字段数建议不超过5个。

    (3)单张表的索引个数控制在5个以内。

    (4)InnoDB表一般都建议有主键列,尤其在高可用集群方案中是作为必须项的。

    (5)建立复合索引时,优先将选择性高的字段放在前面。

    (6)UPDATE、DELETE语句需要根据WHERE条件添加索引。

    (7)不建议使用%前缀模糊查询,例如LIKE “%weibo”,无法用到索引,会导致全表扫描。

    (8)合理利用覆盖索引,例如:

    (9)SELECT email,uid FROM user_email WHERE uid=xx,如果uid不是主键,可以创建覆盖索引idx_uid_email(uid,email)来提高查询效率。

    (10)避免在索引字段上使用函数,否则会导致查询时索引失效。

    (11)确认索引是否需要变更时要联系DBA。

    5)应用规范

    (1)避免使用存储过程、触发器、自定义函数等,容易将业务逻辑和DB耦合在一起,后期做分布式方案时会成为瓶颈。

    (2)考虑使用UNION ALL,减少使用UNION,因为UNION ALL不去重,而少了排序操作,速度相对比UNION要快,如果没有去重的需求,优先使用UNION ALL。

    (3)考虑使用limit N,少用limit M,N,特别是大表或M比较大的时候。

    (4)减少或避免排序,如:group by语句中如果不需要排序,可以增加order by null。

    (5)统计表中记录数时使用COUNT(*),而不是COUNT(primary_key)和COUNT(1);InnoDB表避免使用COUNT(*)操作,计数统计实时要求较强可以使用Memcache或者Redis,非实时统计可以使用单独统计表,定时更新。

    (6)做字段变更操作(modify column/change column)的时候必须加上原有的注释属性,否则修改后,注释会丢失。

    (7)使用prepared statement可以提高性能并且避免SQL注入。

    (8)SQL语句中IN包含的值不应过多。

    (9)UPDATE、DELETE语句一定要有明确的WHERE条件。

    (10)WHERE条件中的字段值需要符合该字段的数据类型,避免MySQL进行隐式类型转化。

    (11)SELECT、INSERT语句必须显式的指明字段名称,禁止使用SELECT * 或是INSERT INTO table_name values()。

    (12)INSERT语句使用batch提交(INSERT INTO table_name VALUES(),(),()……),values的个数不应过多。

    优化设计方案2:业务层优化

    业务层优化应该是收益最高的优化方式了,而且对于业务层完全可见,主要有业务拆分,数据拆分和两类常见的优化场景(读多写少,读少写多)

    640?wx_fmt=png

    1)业务拆分

    ü 将混合业务拆分为独立业务

    ü 将状态和历史数据分离

    业务拆分其实是把一个混合的业务剥离成为更加清晰的独立业务,这样业务1,业务2。。。独立的业务使得业务总量依旧很大,但是每个部分都是相对独立的,可靠性依然有保证。

    对于状态和历史数据分离,我可以举一个例子来说明。

    例如:我们有一张表Account,假设用户余额为100。

    640?wx_fmt=png

    我们需要在发生数据变更后,能够追溯数据变更的历史信息,如果对账户更新状态数据,增加100的余额,这样余额为200。

    这个过程可能对应一条update语句,一条insert语句。

    对此我们可以改造为两个不同的数据源,account和account_hist

    在account_hist中就会是两条insert记录,如下:

    640?wx_fmt=png

    而在account中则是一条update语句,如下:

    640?wx_fmt=png

    这也是一种很基础的冷热分离,可以大大减少维护的复杂度,提高业务响应效率。

    2)数据拆分

    2.1 按照日期拆分,这种使用方式比较普遍,尤其是按照日期维度的拆分,其实在程序层面的改动很小,但是扩展性方面的收益很大。

    • 数据按照日期维度拆分,如test_20191021

    • 数据按照周月为维度拆分,test_201910

    • 数据按照季度,年维度拆分,test_2019

    2.2 采用分区模式,分区模式也是常见的使用方式,采用hash,range等方式会多一些,在MySQL中我是不大建议使用分区表的使用方式,因为随着存储容量的增长,数据虽然做了垂直拆分,但是归根结底,数据其实难以实现水平扩展,在MySQL中是有更好的扩展方式。

    2.3 读多写少优化场景

    采用缓存,采用Redis技术,将读请求打在缓存层面,这样可以大大降低MySQL层面的热点数据查询压力。

    2.4 读少写多优化场景,可以采用三步走:

    1) 采用异步提交模式,异步对于应用层来说最直观的就是性能的提升,产生最少的同步等待。

    2) 使用队列技术,大量的写请求可以通过队列的方式来进行扩展,实现批量的数据写入。

    3) 降低写入频率,这个比较难理解,我举个例子

    对于业务数据,比如积分类,相比于金额来说业务优先级略低的场景,如果数据的更新过于频繁,可以适度调整数据更新的范围(比如从原来的每分钟调整为10分钟)来减少更新的频率。

    例如:更新状态数据,积分为200,如下图所示

    640?wx_fmt=png

    可以改造为,如下图所示。

    640?wx_fmt=png

    如果业务数据在短时间内更新过于频繁,比如1分钟更新100次,积分从100到10000,则可以根据时间频率批量提交。

    例如:更新状态数据,积分为100,如下图所示。

    640?wx_fmt=png

    无需生成100个事务(200SQL语句)可以改造为2SQL语句,如下图所示。

    640?wx_fmt=png

    对于业务指标,比如更新频率细节信息,可以根据具体业务场景来讨论决定。

    优化设计方案3:架构层优化

    架构层优化其实就是我们认为的那种技术含量很高的工作,我们需要根据业务场景在架构层面引入一些新的花样来。

    640?wx_fmt=png

    3.1.系统水平扩展场景

    3.1.1采用中间件技术,可以实现数据路由,水平扩展,常见的中间件有MyCATShardingSphere,ProxySQL等

    640?wx_fmt=jpeg

    3.1.2 采用读写分离技术,这是针对读需求的扩展,更侧重于状态表,在允许一定延迟的情况下,可以采用多副本的模式实现读需求的水平扩展,也可以采用中间件来实现,如MyCAT,ProxySQL,MaxScale,MySQL Router

    640?wx_fmt=png

    3.1.3 采用负载均衡技术,常见的有LVS技术或者基于域名服务的Consul技术

    3.2.兼顾OLTP+OLAP的业务场景,可以采用NewSQL,优先兼容MySQL协议的HTAP技术栈,如TiDB

    3.3.离线统计的业务场景,有几类方案可供选择。

    3.3.1 采用NoSQL体系,主要有两类,一类是适合兼容MySQL协议的数据仓库体系,常见的有Infobright或者ColumnStore,另外一类是基于列式存储,属于异构方向,如HBase技术

    3.3.2 采用数仓体系,基于MPP架构,如使用Greenplum统计,如T+1统计

    优化设计方案4:数据库优化

    数据库优化,其实可打的牌也不少,但是相对来说空间没有那么大了,我们来逐个说一下。

    640?wx_fmt=png

    4.1 事务优化

    根据业务场景选择事务模型,是否是强事务依赖

    对于事务降维策略,我们来举出几个小例子来。

    4.1.1 降维策略1:存储过程调用转换为透明的SQL调用

    对于新业务而言,使用存储过程显然不是一个好主意,MySQL的存储过程和其他商业数据库相比,功能和性能都有待验证,而且在目前轻量化的业务处理中,存储过程的处理方式太“重”了。

    有些应用架构看起来是按照分布式部署的,但在数据库层的调用方式是基于存储过程,因为存储过程封装了大量的逻辑,难以调试,而且移植性不高,这样业务逻辑和性能压力都在数据库层面了,使得数据库层很容易成为瓶颈,而且难以实现真正的分布式。

    所以有一个明确的改进方向就是对于存储过程的改造,把它改造为SQL调用的方式,可以极大地提高业务的处理效率,在数据库的接口调用上足够简单而且清晰可控。

    4.1.2 降维策略2DDL操作转换为DML操作

    有些业务经常会有一种紧急需求,总是需要给一个表添加字段,搞得DBA和业务同学都挺累,可以想象一个表有上百个字段,而且基本都是name1,name2……name100,这种设计本身就是有问题的,更不用考虑性能了。究其原因,是因为业务的需求动态变化,比如一个游戏装备有20个属性,可能过了一个月之后就增加到了40个属性,这样一来,所有的装备都有40个属性,不管用没用到,而且这种方式也存在诸多的冗余。

    我们在设计规范里面也提到了一些设计的基本要素,在这些基础上需要补充的是,保持有限的字段,如果要实现这些功能的扩展,其实完全可以通过配置化的方式来实现,比如把一些动态添加的字段转换为一些配置信息。配置信息可以通过DML的方式进行修改和补充,对于数据入口也可以更加动态、易扩展。

    4.1.3 降维策略3:Delete操作转换为高效操作

    有些业务需要定期来清理一些周期性数据,比如表里的数据只保留一个月,那么超出时间范围的数据就要清理掉了,而如果表的量级比较大的情况下,这种Delete操作的代价实在太高,我们可以有两类解决方案来把Delete操作转换为更为高效的方式。 

    第一种是根据业务建立周期表,比如按照月表、周表、日表等维度来设计,这样数据的清理就是一个相对可控而且高效的方式了。 

    第二种方案是使用MySQL rename的操作方式,比如一张2千万的大表要清理99%的数据,那么需要保留的1%的数据我们可以很快根据条件过滤补录,实现“移形换位”。

    4.2 SQL优化

    其实相对来说需要的极简的设计,很多点都在规范设计里面了,如果遵守规范,八九不离十的问题都会杜绝掉,在此补充几点:

    4.2.1 SQL语句简化,简化是SQL优化的一大利器,因为简单,所以优越。

    4.2.2 尽可能避免或者杜绝多表复杂关联,大表关联是大表处理的噩梦,一旦打开了这个口子,越来越多的需求需要关联,性能优化就没有回头路了,更何况大表关联是MySQL的弱项,尽管Hash Join才推出,不要像掌握了绝对大杀器一样,在商业数据库中早就存在,问题照样层出不穷。

    4.2.3 SQL中尽可能避免反连接,避免半连接,这是优化器做得薄弱的一方面,什么是反连接,半连接?其实比较好理解,举个例子,not in ,not exists就是反连接,in,exists就是半连接,在千万级大表中出现这种问题,性能是几个数量级的差异。 

    4.3 索引优化

    应该是大表优化中需要把握的一个度。

    4.3.1 首先必须有主键,规范设计中第一条就是,此处不接收反驳。

    4.3.2 其次,SQL查询基于索引或者唯一性索引,使得查询模型尽可能简单。

    4.3.3 最后,尽可能杜绝范围数据的查询,范围扫描在千万级大表情况下还是尽可能减少。

    优化设计方案4:管理优化

    这部分应该是在所有的解决方案中最容易被忽视的部分了,我放在最后,在此也向运维同事致敬,总是为很多认为本应该正常的问题尽职尽责(背锅)。

    640?wx_fmt=png

    千万级大表的数据清理一般来说是比较耗时的,在此建议在设计中需要完善冷热数据分离的策略,可能听起来比较拗口,我来举一个例子,把大表的Drop 操作转换为可逆的DDL操作。

    Drop操作是默认提交的,而且是不可逆的,在数据库操作中都是跑路的代名词,MySQL层面目前没有相应的Drop操作恢复功能,除非通过备份来恢复,但是我们可以考虑将Drop操作转换为一种可逆的DDL操作。

    MySQL中默认每个表有一个对应的ibd文件,其实可以把Drop操作转换为一个rename操作,即把文件从testdb迁移到testdb_arch下面;从权限上来说,testdb_arch是业务不可见的,rename操作可以平滑的实现这个删除功能,如果在一定时间后确认可以清理,则数据清理对于已有的业务流程是不可见的,如下图所示。

    640?wx_fmt=png

    此外,还有两个额外建议,一个是对于大表变更,尽可能考虑低峰时段的在线变更,比如使用pt-osc工具或者是维护时段的变更,就不再赘述了。

    最后总结一下,其实就是一句话:

    千万级大表的优化是根据业务场景,以成本为代价进行优化的,绝对不是孤立的一个层面的优化

    近期热文:

    个人新书 《MySQL DBA工作笔记》

    个人公众号:jianrong-notes

    QQ群号:763628645

    QQ群二维码如下,个人微信号:jeanron100, 添加请注明:姓名+地区+职位,否则不予通过

    640?wx_fmt=png640?wx_fmt=png

    在看,让更多人看到

    展开全文
  • 章: 利用神经网络识别手写数字

    万次阅读 2017-03-28 11:35:51
    人类视觉系统是大自然的大奇迹。 考虑下面的手写数字序列: 大部分人能够毫不费力的识别出这些数字是 504192。这种简单性只是个幻觉。在我们大脑各半球,有个主要的视觉皮层,即V1,它包含1.4...

    人类视觉系统是大自然的一大奇迹。 考虑下面的手写数字序列:

    大部分人能够毫不费力的识别出这些数字是 504192。这种简单性只是一个幻觉。在我们大脑各半球,有一个主要的视觉皮层,即V1,它包含1.4亿个神经元以及数以百亿的神经元连接。而且人类不只是有V1,还有一系列的视觉皮层——V2,V3,V4和V5,它们能够执行更加复杂的图像处理。我们可以将大脑想象成一台超级计算机,在几亿年的进化中不断改进,最终非常适合理解这个视觉世界。要识别手写数字不是一件非常容易的事。然而,我们人类却能非常惊人的通过我们的眼睛理解所看到的一切,但是几乎所有的工作都是在不知不觉中完成的,以至于我们不会赞叹我们的视觉系统解决的问题有多么艰难。

    视觉模式识别困难在于如何让计算机程序识别上面数字变得显而易见。看上去非常简单的事操作起来却变得非常困难。我们识别这个形状的简单直觉是——“一个9头上有一个圆圈,右下角有一笔竖线”——但是对于识别算法却不是那么简单。当试图让这些规则更加精确,你将迅速迷失在大量的例外、警告和特殊案例,而且似乎看不到解决的希望。

    神经网络用不同的方法来处理这个问题。它的思想就是利用大量的手写数字(训练样本),

    然后开发出一套从训练样本中进行学习的系统。换句话说,神经网络使用样本来自动推理出识别手写数字的规则。此外,通过增加训练样本规模,神经网络能学到手写体的更多规则从而提升它的识别精度。因此在我们像上面一样只有100张训练数字同时,有可能我们能通过成千上万更多的训练样本来构建更好的手写识别算法。

    本章我们将编写一段计算机程序来实现一个能识别手写数字的神经网络。这个程序大概有74行,而且没有使用其他特别的神经网络库,但是这段程序能够具有96%的数字识别精度,而且没有人工干预。此外,在后面的章节中我们将改进算法达到99%以上的精度。实际上,最好的商业神经网络已经很好的应用在银行支票处理以及邮局识别地址等。

    我们专注在手写体识别是因为它是一个很好的学习神经网络的原型问题。做为一个原型,它刚好合适:识别手写数字是一个挑战,不是那么容易,而它也不需要一个极其复杂的方案或者巨大的计算能力。此外,它也是开发更多高级技术的好方法,比如深度学习。因此整本书我们将不断重复回到手写识别这个问题。这本书的后面,我们将讨论这些算法思想如何应用到计算机视觉的其他问题,还包括语言识别、自然语言处理和其他领域。

    当然,如果本章只是写一个识别手写数字的计算机程序,那么将非常短小!在编写过程中,我们将介绍很多神经网络的关键思想,包括人工神经网络的两大类别(感知器和sigmoid神经元)以及神经网络标准学习算法,即随机梯度下降。整个过程我们将关注在阐述为什么这样处理是有效的,从而让你构建起神经网络直觉。这比只陈述基础的机制更加冗长,但这对更深入理解所学到的内容是值得的。在这些收获上,本章结束你将明白深度学习是什么,且它为什么这么重要。

    感知器(Perceptrons)

    什么是神经网络?在开始之前,我将介绍一种人工的神经元,即感知器(perceptron)。感知器是由Frank Rosenblatt 在上世纪50到60年代发明的 , 灵感来源于 Warren McCulloch 和 Walter Pitts 的早期工作。今天人工神经网络使用更加通用的其他模型,本书以及许多现代的神经网络工作中,主要的神经网络模型是sigmoid神经元。 我们将很快讨论sigmoid神经元,但是为了更好了解它的原理,我们首先要理解感知器。

    那么感知器如何工作呢?一个感知器获取几个二进制输入$x_1, x_2, \ldots$,并且产生一个二进制数出:

    在这个例子中,感知器具有三个输入$x_1, x_2, x_3$。通常它会具有更多或更少的输入。Rosenblatt 提出了一个简单规则来计算最后数出。他引入了权重(weights) $w_1,w_2,\ldots$,这些实数表示各个输入对输出的重要性。这个神经元输出(output) $0$ 或者 $1$ 是由这些输入的加权求和 $\sum_j w_j x_j$ 是否大于或者小于某一个阈值(threshold)。不像这些权重,阈值是这个神经元的实数参数。将它们放入更加精确的代数术语中: \begin{eqnarray} \mbox{output} & = & \left\{ \begin{array}{ll} 0 & \mbox{if } \sum_j w_j x_j \leq \mbox{ threshold} \\ 1 & \mbox{if } \sum_j w_j x_j > \mbox{ threshold} \end{array} \right. \tag{1}\end{eqnarray} 这就是一个感知器如何工作的全部内容!

    这是一个基础的数学模型。你可以这么理解感知器,它是一个通过加权凭据来进行决策的设备。让我们来看这个例子,可能它不是一个真实的例子,但是非常好理解,后面我们将很快进入真实的例子。假设周末到了,你听说在你所在的城市将有一个奶酪节,你很喜欢吃奶酪,并且正决定是否要去参加这个节日,你可能会通过以下三个方面来权衡你的决定:

    1. 天气好吗?
    2. 你的男(女)朋友是否愿意陪你去?
    3. 是否这个活动距离公共交通很近?(你自己没车)
    我们将这三个因素用对应的二进制变量$x_1, x_2$和$x_3$表示。比如,当天气还不错时,我们有$x_1 = 1$ ,天气不好时$x_1 = 0$;相似的,如果男或女朋友愿意去,$x_2 = 1$,否则$x_2 = 0$;对于公共交通$x_3$同理赋值。

    现在假设奶酪是你的最爱,以致于即便你的男或女朋友不感兴趣而且去那里也不太方便,你仍然非常想去参加这个节日活动。但是也许你真的讨厌坏天气,而且如果天气很糟,你也没办法去。你能使用感知器来模拟这类决策。一种决策方式是,让天气权重 $w_1 = 6$,其他条件权重分别为$w_2 = 2$,$w_3 = 2$。权重$w_1$值越大表示天气影响最大,比起男或女朋友加入或者交通距离的影响都大。最后,假设你选择5做为感知器阈值,按照这种选择,这个感知器就能实现这个决策模型:当天气好时候输出$1$,天气不好时候输出$0$,无论你男或女朋友是否愿意去,或者交通是否比较近。

    通过更改权重和阈值,我们能得到不同的决策模型。例如,我们将阈值设为$3$,那么感知器会在以下条件满足时决定去参加活动:如果天气很好、或者男(女)朋友愿意去并且交通很近。换句话说,它将是决策的不同模型,阈值越低,表明你越想去参加这个节日活动。

    显然,这个感知器不是人类决策的完整模型!但是这个例子说明了一个感知器如何能将各种凭据进行加权和来制定决策,而且一个复杂的感知器网络能做出非常微妙的决策:

    在这个网络中,第一感知器(我们称其为第一层感知器)通过加权输入凭据来做出三个非常简单的决策。那第二列感知器是什么呢?其中每一个感知器都是通过将第一列的决策结果进行加权和来做出自己的决策。通过这种方式,第二层感知器能够比第一层感知器做出更加复杂和抽象层的决策。第三层感知器能做出更加复杂的决策,以此类推,更多层感知器能够进行更加复杂的决策。

    顺便说一句,当我们定义感知器时,它们都只有一个输出。但上面的网络中,这些感知器看上去有多个输出。实际上,它们也仍然只有一个输出,只不过为了更好的表明这些感知器输出被其他感知器所使用,因此采用了多个输出的箭头线表示,这比起绘制一条输出线然后分裂开更好一些。

    让我们简化描述感知器的方式。加权求和大于阈值的条件 $\sum_j w_j x_j > \mbox{threshold}$比较麻烦,我们能用两个符号变化来简化它。第一个改变是将 $\sum_j w_j x_j$ 改写成点乘方式,$w \cdot x \equiv \sum_j w_j x_j$,其中$w$ 和 $x$ 分别是权重和输入的向量。第二个改变是将阈值放在不等式另一边,并用偏移 $b \equiv -\mbox{threshold}$ 表示。通过使用偏移代替阈值,感知器规则能够重写为: \begin{eqnarray} \mbox{output} = \left\{ \begin{array}{ll} 0 & \mbox{if } w\cdot x + b \leq 0 \\ 1 & \mbox{if } w\cdot x + b > 0 \end{array} \right. \tag{2}\end{eqnarray} 你可以将偏移想象成使感知器如何更容易输出 $1$,或者用更加生物学术语,偏移是指衡量感知器触发的难易程度。对于一个大的偏移,感知器更容易输出 $1$。如果偏移负值很大,那么感知器将很难输出 $1$。显然,引入偏移对于描述感知器改动不大,但是我们后面将看到这将简化符号描述。正因如此,本书剩下部分都将使用偏移,而不是用阈值。

    我们已经介绍了用感知器,基于凭据(evidence)的加权求和来进行决策。感知器也能够被用来计算基本逻辑函数,像 ANDOR, and NAND这些通常被看做是实现计算的基础。比如说, 假设我们有一个两个输入的感知器,每个输入具有权重$-2$,整个偏移为$3$,如下所示感知器:

    那么我们将看到输入$00$会产生输出$1$,因为$(-2)*0+(-2)*0+3=3$是正数。这里,我们引入$*$符号来进行显示乘法。与此类似,输入$01$和$10$都将产生输出$1$。但是输入$11$将产生输出$0$,因为$(-2)*1+(-2)*1+3=-1$是负数。因此这个感知器实现了一个NAND门!

    这个NAND 门例子表明我们能够使用感知器来计算简单的逻辑函数。实际上,我们能够使用感知器网络来计算任意的逻辑函数。原因是NAND 门在计算中是通用的,也就是,我们能够用一些NAND 门来构造任意计算。比如,我们能够用NAND门来构建两个二进制变量$x_1$和$x_2$的位加法电路。这需要对它们按位求和$x_1+x_2$,同时当$x_1$和$x_2$都是$1$时候进位$1$。进位就是$x_1$和$x_2$的按位乘:

    为了得到相同的感知器网络,我们将NAND门用具有两个输入的感知器代替,每一个输入具有权重$-2$,整体具有偏移$3$。以下是感知器网络结果,注意我们把右下角的NAND门对应的感知器向左移动了一点,以便绘制图表中的箭头:
    值得注意的是,这个感知器网络中最左边的感知器输出被右下角的感知器使用了两次。当我们定义感知器模型时,我们说这种双倍输出到同一个感知器是不允许的。实际上,这没什么关系,如果我们不允许这种情况,只需要把这两条线合并成一条线,并且将权重设置为-4即可。(如果不这么认为,那么你需要停下来并证明一下它们等同性)在这点小改变后,感知器网络如下所示,没有标签的权重为-2,所有偏移为3,唯一权重为-4的边进行了标记: 直到现在,我们将变量输入$x_1$和$x_2$绘制到感知器网络的左边。实际上,更变的应该绘制一层额外的感知器——输入层(input layer)来编码输入:
    这个只有一个输出,没有输入的输入感知器,
    是一个简写。这么看,假设我们有一个没有输入的感知器,那么权重和 $\sum_j w_j x_j$ 将始终为$0$,那么如果偏差$b > 0$,感知器将输出$1$,否则输出$0$。也就是,这个感知器将简单的输出一个固定的值,而不是希望的值(如上面的例子中$x_1$)。更好的理解输入感知器可以将其不当作感知器,而是一个简单定义期望值$x_1, x_2,\ldots$输出的特殊单元。

    这个加法器例子演示了感知器网络能够模拟具有许多与非门的电路。并且由于与非门对于计算是通用的,因此感知器也是对计算通用的。

    感知器的计算通用性同时让人满意,也让人失望。它让人满意是因为感知器网络能够像其他计算装置那么强大,但是也是让人失望是因为它看起来只是一种新的与非门,不是什么大新闻!

    不过,情况比这个观点更好一些。它能够让我们设计学习算法,这种算法能够自动调整人工神经网络的权重和偏移。这会在响应外部刺激时候发生,而且没有程序员的直接干预。这些学习算法能让我们用一种新的方式使用人工神经网络,它将与传统的逻辑门方法完全不同。替代显示的摆放一个具有与非门和其他门的电路,我们的神经网络能够学会简单地解决这些问题,有些问题对于直接设计 传统电路来说非常困难。

    Sigmoid神经元

    学习算法听起来好极了。但是我们如何对一个神经网络设计算法。假设我们有一个想通过学习来解决问题的感知器网络。比如,网络的输入可能是一些扫描来的手写数字图像原始像素数据。且我们希望这个网络能够学会调权和偏移以便正确的对它们进行数字分类。为了看到学习如何进行,假设我们在网络中改变一点权重(或者偏移)。我们希望很小的权重改变能够引起网络输出的细微改变。后面我们将看到,这个特性将使学习成为可能。以下示意图就是我们想要的(显然这个网络对于手写识别太简单!):

    如果权重或者偏移的细小改变能够轻微影响到网络输出,那么我们会逐步更改权重和偏移来让网络按照我们想要的方式发展。比如,假设网络错误的将数字“9”的图像识别为“8”,我们将指出如何在权重和偏移上进行细小的改动来使其更加接近于“9”。(tensorfly.cn社区原创)并且这个过程将不断重复,不断地改变权重和偏移来产生更好的输出。于是这个网络将具有学习特性。

    问题是在我们的感知器网络中并没有这样发生。实际上,任何一个感知器的权重或者偏移细小的改变有时都能使得感知器输出彻底翻转,从$0$到$1$。这种翻转可能导致网络剩余部分的行为以某种复杂的方式完全改变。因此当“9”可能被正确分类后,对于其他图片,神经网络行为结果将以一种很难控制的方式被完全改变。这使得很难看出如何逐渐的调整权重和偏移以至于神经网络能够逐渐按照预期行为接近。也许对于这个问题有一些聪明的方式解决,但是目前如何让感知器网络学习还不够清楚。

    我们能通过引入一种新的人工神经元(sigmoid神经元)来克服这个问题。它和感知器类似,但是细微调整它的权重和偏移只会很细小地影响到输出结果。这就是让sigmoid神经元网络学习的关键原因。

    好的,让我们来描述一下这个sigmoid神经元。我们将用描绘感知器一样来描绘它:

    就像感知器,sigmoid神经元有输入$x_1, x_2, \ldots$。但是输入值不仅是$0$或者$1$,还可以是$0$$1$的任意值。因此,比如$0.638\ldots$对于sigmoid神经元是一个合理的输入。也像感知器一样,sigmoid神经元对每一个输入都有对应的权重$w_1, w_2, \ldots$,还有一个全局的偏移$b$。但是输出不是$0$或者$1$,而是$\sigma(w \cdot x+b)$,$\sigma$是sigmoid函数**顺便说一下,$\sigma$有时叫做逻辑函数,并且这种新的神经元叫做逻辑神经元。记住这个术语非常重要,因为许多研究中的神经网络都在使用它。,定义如下:\begin{eqnarray} \sigma(z) \equiv \frac{1}{1+e^{-z}}. \tag{3}\end{eqnarray}把这些更直接的放在一起,具有输入$x_1,x_2,\ldots$,权重$w_1,w_2,\ldots$和偏移$b$的sigmoid神经元输出为: \begin{eqnarray} \frac{1}{1+\exp(-\sum_j w_j x_j-b)}. \tag{4}\end{eqnarray}

    眨眼一看,sigmoid神经元跟感知器非常不同。对于不熟悉的人来说,sigmoid函数的代数形式看起来很晦涩和令人生畏。实际上,感知器和sigmoid神经元有很多相似之处,并且sigmoid函数的代数形式展现了更多的技术细节。

    为了理解和感知器模型的相似点,假设$z \equiv w \cdot x + b$是一个大的正数,那么$e^{-z} \approx 0$且$\sigma(z) \approx 1$。换句话说,当$z = w \cdot x+b$是一个很大的正数,sigmoid神经元的输出接近于$1$,这和感知器结果类似。另外假设$z = w \cdot x+b$是一个很小的负数,那么$e^{-z} \rightarrow \infty$,并且$\sigma(z) \approx 0$。因此当$z = w \cdot x +b$是一个很小负数时候,sigmoid神经元和感知器的输出也是非常接近的。只有当$w \cdot x+b$在一个适度的值,sigmoid神经元和感知器偏差才较大。

    $\sigma$函数的代数形式是什么?我们如何理解它呢?实际上,$\sigma$函数的准确形式不太重要——真正有用的是绘制的函数形状,看看下面这个形状:

    -4-3-2-1012340.00.20.40.60.81.0zsigmoid function

    这是一个阶跃函数的平滑版本:

    -4-3-2-1012340.00.20.40.60.81.0zstep function

    如果$\sigma$函数实际上是一个阶跃函数,那么sigmoid神经元就一个感知器,因为输出是$1$或者$0$,取决于$w\cdot x+b$是正还是负。**实际上,当$w \cdot x +b = 0$时,感知器趋于$0$,而阶跃函数输出$1$。所以,严格的说,我们只需要在一个点上修改阶跃函数。使用上面我们说明的实际$\sigma$函数,将得到一个平滑的感知器。实际上,$\sigma$函数的平滑程度是至关重要的,而不是它的具体形式。$\sigma$函数越平滑意味着权重微调$\Delta w_j$和偏移微调$\Delta b$将对神经元产生一个细小的输出改变$\Delta \mbox{output}$。实际上,微积分告诉我们细小的输出$\Delta \mbox{output}$近似等于: \begin{eqnarray} \Delta \mbox{output} \approx \sum_j \frac{\partial \, \mbox{output}}{\partial w_j} \Delta w_j + \frac{\partial \, \mbox{output}}{\partial b} \Delta b, \tag{5}\end{eqnarray}其中总和基于所有的权重$w_j$,且$\partial \, \mbox{output} / \partial w_j$和$\partial \, \mbox{output} /\partial b$分别表示$\mbox{output}$基于$w_j$和$b$的偏导数。不用见到偏导就惊慌!虽然上面具有偏导运算的表达式看起来很复杂,但实际上很简单(这是一个好消息):输出改变$\Delta \mbox{output}$是权重和偏移改变$\Delta w_j$和$\Delta b$的线性函数。这种线性使得权重和偏移的细微改变就能很容易使得输出按期望方式微小改变。因此sigmoid神经元具有和感知器同样的定性行为,同时还能方便的找出权重与偏移如何对输出的产生影响。

    如果$\sigma$函数的形状的确起作用,而不是其精确的形式,那么在表达式(3) 中为什么使用这种形式呢?实际上在本书后面,我们将偶尔考虑神经元的输出函数为$f(w \cdot x + b)$来做为激活函数$f(\cdot)$。主要的改变是当我们使用不同的激活函数之后,等式(5)中的偏导值将跟随改变。结果就是当我们后面计算偏导时,使用$\sigma$函数将简化代数计算,因为指数形式区别于其他具有更好的偏导特性。在任何情况下,$\sigma$函数在神经网络中被广泛使用,在本书中也是我们经常使用的激励函数。

    我们如何解读sigmoid神经元输出?显然,感知器和sigmoid神经元最大的区别是sigmoid神经元不是只输出$0$或者$1$。它能够输出$0$到$1$之间任意实数,如$0.173\ldots$和$0.689\ldots$都是合理的输出。这将非常有用,比如,如果你想使用输出值来表示神经网络中一张输入图像像素的平均强度。但是有时候这又非常讨厌。假设我们想从神经网络中指明要么“输入图像是9”或者“输入图像不是9”。显然像感知器一样只输出$0$或$1$更容易处理。但实际上,我们将建立一个约定来处理这个问题,比如,将至少是$0.5$的输出当做为“9”,其他比$0.5$小的输出当做为“非9”。我们使用这样约定总是明确的,因此不会产生如何混淆。

    练习

    • Sigmoid神经元模拟感知器,第一部分 $\mbox{}$ 
      假设我们将感知器网络中的权重和偏移乘以一个正常数$c > 0$。证明该网络的输出行为不会改变。

    • Sigmoid神经元模拟感知器,第二部分 $\mbox{}$ 
      假设和感知器网络最后一个问题一样,整个输入都设定好,不需要真实的输入值,只需要固定输入值。假设对于网络中某些特殊感知器输入$x$,使得$w \cdot x + b \neq 0$。将这些感知器用神经元替换,那么再将权重和偏移乘以一个正常数$c > 0$,可以证明当$c \rightarrow \infty$ 时,神经元网络和感知器网络的行为将是一样的。那么对于感知器中$w \cdot x + b = 0$的那些呢,是否不成立?

    神经网络体系结构

    在下一区段,我将介绍用神经网络能够很好的对手写数字进行区分。先做点准备,让我们命名一下网络中不同部分的术语。假如我们有如下网络:

    就像先前说的,网络的最左边一层被称为输入层,其中的神经元被称为输入神经元。最右边及输出层包含输出神经元,在这个例子中,只有一个单一的输出神经元。中间层被称为隐含层,因为里面的神经元既不是输入也不是输出。“隐含”这个术语可能听起来很神秘——当我第一次听到时候觉得一定有深层的哲学或者数学意义——但实际上它只表示“不是输入和输出”而已。上面的网络只包含了唯一个隐含层,但是一些网络可能有多层。比如,下面的4层网络具有2个隐含层:
    令人困扰的是,由于一些历史原因,这样的多层网络有时被叫做多层感知器或者MLPs尽管它是由sigmoid神经元构成的,而不是感知器。在这本书中,我将不会使用MLP,因为我觉得它太混淆了,但是提醒你它可能在其它地方出现。

    网络中的输入和输出层设计通常很简单,比如,假设我们试着确定手写图片是否代表“9”。设计网络更自然的方式是将图像像素强度编码进输入神经元。如果图像是一幅$64$ by $64$的灰度图,那么我们将有$4,096 = 64 \times 64$个输入神经元,每一个是介于$0$和$1$的强度值。输出层将只包含一个神经元,输出值小于$0.5$表示“输入图像不是9”,大于$0.5$表示“输入图像是9”。

    虽然神经元网络的输入输出层很简单,设计好隐含层却是一门艺术。特别是很难将隐含层的设计过程总结出简单的经验规则。相反,神经元研究者已经为隐含层开发出许多启发式设计,它们能帮助大家获取所期望行为的网络。例如,一些启发式算法能帮助确定如何平衡隐含层数量与网络训练花费时间。我们将在本书后面见到一些这样的启发式设计。

    直到现在,我们已经讨论了某一层的输出当作下一层的输入的神经网络。这样的网络被称为前向反馈神经网络。这意味着在网络中没有循环——信息总是向前反馈,决不向后。如果真的有循环,我们将终止这种输入依赖于输出的$\sigma$函数。这会很难理解,因此我们不允许这样的循环。

    但是,人工神经网络也有一些具有循环反馈。这样的模型被称为递归神经网络。这样的模型思想是让神经元在不活跃之前激励一段有限的时间。这种激励能刺激其它神经元,使其也能之后激励一小会。这就导致了更多神经元产生激励,等过了一段时间,我们将得到神经元的级联反应。在这样的模型中循环也不会有太大问题,因为一个神经元的输出过一会才影响它的输入,而不是瞬间马上影响到。

    递归神经网络没有前馈神经网络具有影响力,因为递归网络的学习算法威力不大(至少到目前)。但是递归网络仍然很有趣,它们比起前馈网络更加接近于我们人脑的工作方式。并且递归神经网络有可能解决那些前馈网络很难解决的重要问题。但是为了限制本书的范围,我们将集中在已经被大量使用的前馈神经网络上。

    一个分类手写数字的简单神经网络

    定义了神经网络后,让我们回到手写识别。我们能将识别手写数字分成两个子问题。首先,我们想办法将一个包含很多数字的图像分成一系列独立的图像,每张包含唯一的数字。比如我们将把下面图像

    分成6个分离的小图像,

    我们人类能够很容易解决这个分段问题,但是对于计算机程序如何正确的分离图像却是一个挑战。然后,一旦这幅图像被分离,程序需要将各个数字进行分类。因此,比如,我们希望程序能够将上面的第一个数字

    识别为5。

    我们主要关注在第二个问题,也就是,分类这些独立的数字图像。我们这么做是因为一旦你能够找到一个好方式来分类独立的图像,分离问题就不是那么难解决了。有许多途径能够解决分离问题,一种途径是试验很多不同的分离图像方法,并采用独立图像分类器给每个分离试验打分。如果独立数字分类器坚信某种分离方式,那么打分较高。如果分类器在某些片断上问题很多,那么得分就低。这个思想就是如果分类器分类效果很有问题,那么很可能是分离方式不对造成。这种想法和一些变型能够很好解决分离问题。因此无须担心分离,我们将集中精力开发一个神经网络,它能解决更有趣和复杂的问题,即识别独立的手写数字。

    为了识别这些数字,我们将采用三层神经网络:

    网络的输入层含有输入像素编码的神经元。跟下节讨论的一样,我们用于网络训练的数据将包含许多$28$ by $28$像素手写数字扫描图像,因此输入层包含$784 = 28 \times 28$个神经元。为了简化,我们在上图中忽略了许多输入神经元。输入像素是灰度值,白色值是$0.0$,黑色值是$1.0$,它们之间的值表明灰度逐渐变暗的程度。

    网络的第二层是隐含层。我们将其神经元的数量表示为$n$,后面还对其采用不同的$n$值进行实验。这个例子中使用了一个小的隐含层,只包含$n = 15$个神经元。

    网络的输出层包含10个神经元。如果第一个神经元被触发,例如它有一个$\approx 1$的输出,那么这就表明这个网络认为识别的数字是$0$。更精确一点,我们将神经元输出标记为$0$到$9$,然后找出具有最大激励值的神经元。如果这个神经元是$6$,那么这个网络将认为输入数字是$6$。对于其他神经元也有类似结果。

    你可能想知道为什么用$10$个输出神经元。别忘了,网络的目标是识别出输入图像对应的数字($0, 1, 2, \ldots, 9$)。一种看起来更自然的方法是只用$4$个输出神经元,每个神经元都当做一个二进制值,取值方式取决于神经元输出接近$0$,还是$1$。4个神经元已经足够用来编码输出,因为$2^4 = 16$大于输入数字的10种可能值。(tensorfly社区原创,qq群: 472113439)为什么我们的网络使用$10$个神经元呢?是否这样效率太低?最终的理由是以观察和实验为依据的:我们能尝试两种网络设计,最后结果表明对于这个特殊问题,具有$10$个输出神经元的网络能比只有$4$个输出神经元的网络更好的学习识别手写数字。这使得我们想知道为什么具有$10$个输出神经元的网络效果更好。这里是否有一些启发,事先告诉我们应该用$10$个输出神经元,而不是$4$个输出神经元?

    为了弄懂我们为什么这么做,从原理上想清楚神经网络的工作方式很重要。首先考虑我们使用$10$个输出神经元的情况。让我们集中在第一个输出神经元上,它能试图确定这个手写数字是否为$0$。它通过对隐含层的凭据进行加权和求出。隐含层的神经元做了什么呢?假设隐含层中第一个神经元目标是检测到是否存在像下面一样的图像:

    它能够对输入图像与上面图像中像素重叠部分进行重加权,其他像素轻加权来实现。相似的方式,对隐含层的第二、三和四个神经元同样能检测到如下所示的图像:

    你可能已经猜到,这四幅图像一起构成了我们之前看到的$0$的图像:

    因此如果这四个神经元一起被触发,那么我们能够推断出手写数字是$0$。当然,这些不是我们能推断出$0$的凭据类别——我们也可以合理的用其他方式得到$0$(通过对上述图像的平移或轻微扭曲)。但是至少这种方式推断出输入是$0$看起来是安全的。

    假设神经网络按这种方式工作,我们能对为什么$10$个输出神经元更好,而不是$4$个,给出合理的解释。如果我们有$4$个输出神经元,那么第一个输出神经元将试着确定什么是数字图像中最重要的位。这里没有很容易的方式来得到如上图所示简单的形状的重要位。很难想象,这里存在任何一个很好的根据来说明数字的形状组件与输出重要位的相关性。

    现在,就像所说的那样,这就是一个启发式。没有什么能说明三层神经网络会按照我描述的那样运作,这些隐含层能够确定简单的形状组件。可能一个更聪明的学习算法将找到权重赋值以便我们使用$4$个输出神经元。但是我描述的这个启发式方法已经运作很好,能够节约很多时间来设计更好的神经元网络架构。

    练习

    • 有一个方法能够对上面的三层网络增加一个额外层来确定数值位表示。这个额外层将先前输出层转换为二进制表示,就像下图说明一样。找到这层新的输出神经元权重和偏移。假定第$3$层神经元(例如,原来的输出层)正确的输出有至少$0.99$的激励值,不正确的输出有小于$0.01$的激励值。

    梯度下降学习算法

    现在我们已经有一个设计好的神经网络,它怎样能够学习识别数字呢?首要的事情是我们需要一个用于学习的数据集——也叫做训练数据集。我们将使用MNIST数据集,它包含成千上万的手写数字图像,同时还有它们对应的数字分类。MNIST的名字来源于NIST(美国国家标准与技术研究所)收集的两个修改后的数据集。这里是一些来自于MNIST的图像:

    就像你看到的,事实上这些数字和本章开始展示的用于识别的图像一样。当然,测试我们的网络时候,我们会让它识别不在训练集中的图像!

    MNIST数据来自两部分。第一部分包含60,000幅用于训练的图像。这些图像是扫描250位人员的手写样本得到的,他们一半是美国人口普查局雇员,一半是高中学生。这些图像都是28乘28像素的灰度图。MNIST数据的第二部分包含10,000幅用于测试的图像。它们也是28乘28的灰度图像。我们将使用这些测试数据来评估我们用于学习识别数字的神经网络。为了得到很好的测试性能,测试数据来自和训练数据不同的250位人员(仍然是来自人口普查局雇员和高中生)。 这帮助我们相信这个神经网络能够识别在训练期间没有看到的其他人写的数字。

    我们将使用符号$x$来表示训练输入。可以方便的把训练输入$x$当作一个$28 \times 28 = 784$维的向量。每一个向量单元代表图像中一个像素的灰度值。我们将表示对应的输出为$y = y(x)$,其中$y$是一个$10$维向量。比如,如果一个特殊的训练图像$x$,表明是$6$,那么$y(x) = (0, 0, 0, 0, 0, 0, 1, 0, 0, 0)^T$就是网络期望的输出。注意$T$是转置运算,将一个行向量转换为列向量。

    我们想要的是一个能让我们找到合适的权重和偏移的算法,以便网络输出$y(x)$能够几乎满足所有训练输入$x$。为了量化这个匹配度目标,我们定义了一个代价函数**有时被称为损失目标函数。我们在本书中都使用代价函数,但是你需要注意其他术语,因为它通常在一些研究文章和神经网络讨论中被使用到。\begin{eqnarray} C(w,b) \equiv \frac{1}{2n} \sum_x \| y(x) - a\|^2. \tag{6}\end{eqnarray} 这里,$w$表示网络中的所有权重,$b$是所有偏移,$n$训练输入的总数,$a$是网络输入为$x$时的输出向量,总和是对所有输入$x$进行的累加。当然输出$a$取决于$x$,$w$和$b$,但是为了符号简化,我没有指明这种依赖关系。符号$\| v \|$只是表示向量$v$的长度。我们称$C$为二次型代价函数;它有时候也叫做均方误差MSE。检验二次型代价函数,我们能看到$C(w,b)$是一个非负值,因为求和中的每一项都是非负的。此外,对于所有训练数据$x$,$y(x)$和输出$a$近似相等时候,$C(w,b)$会变得很小,例如,$C(w,b) \approx 0$。因此如果我们的训练算法能找到合理的权重和偏移使得$C(w,b) \approx 0$,这将是一个很好的算法。相反,如果$C(w,b)$很大——这意味着$y(x)$和大量输出$a$相差较大。所以训练算法的目标就是找到合适的权重和偏移来最小化$C(w,b)$。换句话说,我们想找到一组权重和偏移集合,使得代价函数值尽可能小。我们将使用梯度下降算法来实现。

    为什么引入二次型代价函数?毕竟,我们不是主要关注在网络对多少图像进行了正确分类?为什么不直接最大化这个数值,而不是最小化一个像二次型代价函数一样的间接测量值?问题就在于正确识别的图像数量不是权重和偏移的平滑的函数。对于大多数,权重和偏移的很小改变不会让正确识别的图像数量值有任何改变。这就使得很难指出如何改变权重和偏移来提高性能。如果我们使用一个像二次型的平滑代价函数,它将很容易指出如何细微的改变权重和偏移来改进代价函数。这就是为什么我们首先关注在最小化二次型代价函数,而且只有那样我们才能检测分类的准确性。

    即使我们想使用一个平滑的代价函数,你可能仍然想知道为什么在等式(6) 中选择了二次型代价函数。它难道不是一个临时的选择?也许如果我们选择一个不同的代价函数,我们将获取到完全不同的最小化权重和偏移?这是一个很好的考虑,后面我们将回顾这个代价函数,并且进行一些改变。但是,这个等式(6) 中的二次型代价函数对理解神经网络学习基础非常好,因此目前我们将坚持使用它。

    简要回顾一下,我们训练神经网络的目标是找出能最小化二次型代价函数$C(w, b)$的权重和偏移。这是一个适定问题,但是它会产生许多当前提出的干扰结构——权重$w$和偏移$b$的解释、背后隐藏的$\sigma$函数、网络架构的选择、MNIST等等。结果说明我们能通过忽略许多这类结构来理解大量内容,且只需要集中在最小化方面。因此现在我们将忘掉所有关于代价函数的特殊形式,神经网络的连接关系等等。相反,我们将想象成只简单的给出一个具有许多变量的函数,且我们想要最小化它的值。我们将开发一个新技术叫梯度下降,它能被用来解决这类最小化问题。然后我们将回到神经网络中这个想最小化的特殊函数。

    好的,假设我们将最小化一些函数$C(v)$。它可能是具有许多变量$v = v_1, v_2, \ldots$的任意真实值函数。请注意,我们将用$v$替换符号$w$和$b$来强调它可以适合任意一个函数——我们并不是一定在神经网络的背景下考虑。为了最小化$C(v)$,可以把$C$想象成只具有两个变量,即$v_1$和$v_2$:

    我们想要找到怎样才能使$C$达到全局最小化。现在,当然,对上面绘制的函数,我们能看出该图中最小化位置。从这种意义上讲,我可能展示了一个简单的函数!一个通常的函数$C$可能由许多变量构成,并且很难看出该函数最小化位置在哪里。

    一种解决办法就是使用微积分来解析地找到这个最小值。我们能计算导数,然后使用它们来找到$C$函数最小极值的位置。当$C$只有一个或少量变量时,这个办法可能行得通。但是当我们有许多变量时,这将变成一场噩梦。且对于神经网络,我们通常需要更多的变量——最大的神经网络在极端情况下,具有依赖数十亿权重和偏移的代价函数。使用微积分来求最小值显然行不通!

    (在断言我们通过把$C$当作只具有两个变量的函数来获得领悟后,我将用两段第二次转回来,并且说:“嘿,一个具有超过两个变量的函数是什么呢?”,对此很抱歉。请相信我,把$C$想象成只具有两个变量的函数对我们理解很有帮助。有时候想象会中断,且最后两段将处理这种中断。好的数学思维通常会涉及应付多个直观想象,学会什么时候合适使用每一个想象,什么时候不合适。)

    Ok,所以用微分解析的方法行不通。幸运的是,我们可以通过一种非常直观的类比来找到一种“行得通”的算法。首先将我们的函数看作是一个凹形的山谷。(瞄一眼上面的插图,这种想法应该是很直观的。)好,现在让我们想象有一个小球沿着山谷的坡面向低处滚。生活经验告诉我们这个小球最终会到达谷底。或许我们可以采用类似的思路找到函数的最小值?好,首先我们将随机选取一个点作为这个(想象中的)球的起点,然后再模拟这个小球沿着坡面向谷底运动的轨迹。我们可以简单地通过计算 $C$ 的偏导数(可能包括某些二阶偏导数)来进行轨迹的模拟——这些偏导数蕴涵了山谷坡面局部“形状”的所有信息,因此也决定了小球应该怎样在坡面上滚动。

    根据我刚才所写的内容,你可能会以为我们将从小球在坡面上滚动的牛顿运动方程出发,然后考虑重力和坡面阻力的影响,如此等等……实际上,我们不会把“小球在坡面滚动”的这一类比太过当真——毕竟,我们我们的初衷是要设计一种最小化 $C$ 的算法,而不是真地想精确模拟现实世界的物理定律!“小球在坡面滚动”的直观图像是为了促进我们的理解和想象,而不应束缚我们具体的思考。所以,与其陷入繁琐的物理细节,我们不妨这样问自己:如果让我们来当一天上帝,那么我们会怎样设计我们自己的物理定律来引导小球的运动?我们该怎样选择运动定律来确保小球一定会最终滚到谷底?

    为了将这个问题描述得更确切,现在让我们来想想,当我们将小球沿着 $v_1$ 方向移动一个小量$\Delta v_1$,并沿着 $v_2$ 方向移动一个小量 $\Delta v_2$ 之后会发生什么。微分法则告诉我们,$C$ 将作如下改变: \begin{eqnarray} \Delta C \approx \frac{\partial C}{\partial v_1} \Delta v_1 + \frac{\partial C}{\partial v_2} \Delta v_2. \tag{7}\end{eqnarray} 我们将设法选择 $\Delta v_1$ 和 $\Delta v_2$ 以确保 $\Delta C$ 是负数,换句话说,我们将通过选择 $\Delta v_1$ 和 $\Delta v_2$ 以确保小球是向着谷底的方向滚动的。为了弄清楚该怎样选择 $\Delta v_1$ 和 $\Delta v_2$,我们定义一个描述 $v$ 改变的矢量 $\Delta v \equiv (\Delta v_1, \Delta v_2)^T$,这里的 $T$ 同样是 转置(transpose)算符,用来将一个行矢量转化为列矢量。我们还将定义 $C$ 的“梯度”,它是由 $C$ 的偏导数构成的矢量,$\left(\frac{\partial C}{\partial v_1}, \frac{\partial C}{\partial v_2}\right)^T$ 。我们将“梯度”矢量记为 $\nabla C$,即:\begin{eqnarray} \nabla C \equiv \left( \frac{\partial C}{\partial v_1}, \frac{\partial C}{\partial v_2} \right)^T. \tag{8}\end{eqnarray} 很快我们将会用 $\Delta v$ 和梯度 $\nabla C$ 来重写 $\Delta C$ 的表达式。在这之前,为了避免读者可能对“梯度”产生的困惑,我想再多做一点解释。当人们首次碰到$\nabla C$这个记号的时候,可能会想究竟该怎样看待$\nabla$这个符号。$\nabla$的含义到底是什么?其实,我们完全可以把$\nabla C$整体看作是单一的数学对象——按照上述公式定义的矢量,仅仅是偶然写成了用两个符号标记的形式。根据这个观点,$\nabla$像是一种数学形式的旗语,来提醒我们“嘿,$\nabla C$是一个梯度矢量”,仅此而已。当然也有更抽象的观点,在这种观点下,∇被看作是一个独立的数学实体(例如,被看作一个微分算符),不过我们这里没有必要采用这种观点。

    使用上述定义,$\Delta C$ 的表达式 (7) 可以被重写为:\begin{eqnarray} \Delta C \approx \nabla C \cdot \Delta v. \tag{9}\end{eqnarray} 这个方程有助于我们理解为什么 $\nabla C$ 被称为“梯度”矢量:$\nabla C$ 将 $v$ 的改变和 $C$ 的改变联系在了一起,而这正是我们对于“梯度”这个词所期望的含义。然而,真正令我们兴奋的是这个方程让我们找到一种 $\Delta v$ 的选择方法可以确保 $\Delta C$ 是负的。具体来说,如果我们选择 \begin{eqnarray} \Delta v = -\eta \nabla C, \tag{10}\end{eqnarray} 这里 $\eta$ 是一个正的小参数,被称为“学习率”(learning rate)。这样方程span id="margin_859162866010_reveal" class="equation_link">(9) 化为 $\Delta C \approx -\eta \nabla C \cdot \nabla C = -\eta \|\nabla C\|^2$。如果我们根据公式 (10) 指定 $v$ 的改变,由于 $\| \nabla C \|^2 \geq 0$ ,它确保了 $\Delta C \leq 0$,即 $C$ 的值将一直减小,不会反弹。(当然要在近似关系 (9) ) 成立的范围内)。这正是我们期望的性质!因此,我们将在梯度下降算法(gradient descent algorithm)中用公式 (10) 来定义小球的“运动定律”。也就是说,我们将用公式(10) 来计算 $\Delta v$ 的值,然后将小球的位置 $v$ 移动一小步:\begin{eqnarray} v \rightarrow v' = v -\eta \nabla C. \tag{11}\end{eqnarray} 然后,我们使用这一规则再将小球移动一小步。如果我们不断这样做,$C$ 将不断减小,符合期望的话,$C$ 将一直减小到全局最小值。

    总之,梯度下降算法(gradient descent algorithm)是通过不断计算梯度$\nabla C$,然后向着梯度相反的方向小步移动的方式让小球不断顺着坡面滑向谷底。这个过程可以形象地用下图表示:

    注意:采用这一规则,梯度下降算法并没有模拟真实的物理运动。在现实世界,小球具有动量,而动量可能让小球偏离最陡的下降走向,甚至可以让其暂时向着山顶的方向逆向运动。只有考虑了摩擦力的作用,才能确保小球是最终落在谷底的。但这里,我们选择Δv的规则却是说“立马给我往下滚”,而这个规则仍然是寻找最小值的一个好方法!

    为了让“梯度下降法”能够正确发挥作用,我们必须选择一个足够小的“学习率” $\eta$ 以确保方程 (9) 是一个好的近似。如果我们不这样做,最终可能会导致 $\Delta C > 0$,这当然不是我们想要的。(机器学习社区tensorfly.cn原创)与此同时,我们也不希望 $η$ 太小,因为η太小的话每一步的 $\Delta v$ 就会非常小, 从而导致梯度下降算法的效率非常低。在实际运用过程中,η 经常是变化的从而确保一方面方程 (9) 是好的近似,另一方面算法也不会太慢。后面我们将会明白具体该怎么做。

    前面我们已经解释了当函数 $C$ 仅仅依赖两个变量时的梯度下降算法。但其实,当 $C$ 依赖更多变量的时候,前面所有的论述依然成立。具体来说,假设函数 $C$ 依赖$m$个变量, $v_1,\ldots,v_m$,那么由微小改变 $\Delta v = (\Delta v_1, \ldots, \Delta v_m)^T$ 引起的 $C$ 的变化$\Delta C$是: \begin{eqnarray} \Delta C \approx \nabla C \cdot \Delta v, \tag{12}\end{eqnarray} 这里梯度矢量 $\nabla C$ 定义为 \begin{eqnarray} \nabla C \equiv \left(\frac{\partial C}{\partial v_1}, \ldots, \frac{\partial C}{\partial v_m}\right)^T. \tag{13}\end{eqnarray} 类似于两个自变量的情形,我们可以选择 \begin{eqnarray} \Delta v = -\eta \nabla C, \tag{14}\end{eqnarray} 那么就可以确保由公式 (12) 得到的 $\Delta C$ 是负的。这给了我们一种沿着梯度找到最小值的方法,即使 $C$ 依赖于很多变量。即通过不断重复地使用以下更新规则,\begin{eqnarray} v \rightarrow v' = v-\eta \nabla C. \tag{15}\end{eqnarray} 你可以把这种更新规则看作是“梯度下降算法”的定义。它给了我们一种不断改变 $v$ 的位置从而找到函数 $C$ 最小值的方法。这一规则并不总是有效——有些情况可能会导致出错,阻止梯度下降算法找到 $C$ 的全局最小值。在后面的章节我们会回过头来讨论这种可能性。不过,在实际应用中,梯度下降算法常常非常有效,并且在神经网络中,我们发现它也是一种最小化代价函数(cost function)的强有力方法,因此也有助于神经网络的学习。

    的确,从某种意义上说梯度下降其实是寻找最小值的最佳策略。假设我们正试图通过移动一小步 $\Delta v$ 来让函数 $C$ 尽可能地减小。这等价于要要最小化 $\Delta C \approx \nabla C \cdot \Delta v$。我们将限定移动步幅的大小,即令 $\| \Delta v \| = \epsilon$ 其中 $\epsilon > 0$ 是一个固定的小量. 换句话说,我们想移动步长限定的一小步,并试图找到一个移动方向能够让C尽可能减小。可以证明这里最小化$\nabla C \cdot \Delta v$的$\Delta v$选择是 $\Delta v = - \eta \nabla C$, 这里 $\eta = \epsilon / \|\nabla C\|$ 是由步长约束 $\|\Delta v\| = \epsilon$ 所要求的。因此梯度下降可以被认为是一种沿着 $C$ 局域下降最快的方向一步一步向前走,从而找到最小值的方法。

    练习

    • 证明上面最后一段的断言。提示:如果你还不熟悉柯西-施瓦茨不等式,你最好学习一下,这会对你很有帮助。

    • 我们阐述当$C$有两个变量时的梯度下降,也阐述了超过两个变量的梯度下降。当$C$只有一个变量时候,会发生什么呢?你能提供在一个维度上梯度下降的几何解释吗?

    人们调研过很多从梯度下降变种而来的算法,包括精确模仿小球在真实物理世界运动的方法。这些模仿小球真实物理运动的方法有一些优点,但是也有一个显著的缺点:需要计算 $C$ 的二阶偏导数,而那样的话,计算成本会很高。为了看清楚为什么计算成本会很高,假设我们要计算所有的二阶偏导数 $\partial^2 C/ \partial v_j \partial v_k$。 如果这里需要计算的 $v_j$ 变量有一百万个,我们就要计算大概万亿(百万的平方)量级的二阶偏导数。* *实际上,稍微估准一点的话,需要计算的偏导数大概有万亿的一半个,因为∂2C/∂vj∂vk=∂2C/∂vk∂vj。不管怎样,你依然很容易看得出计算成本会非常高。! 这个计算成本非常高。 正因为上面所说的,人们搞出不少小技巧来避免计算成本过高的问题,同时,寻找梯度下降的替代算法也是非常活跃的研究领域。不过,本书将始终把梯度下降(及其变种)算法作为我们训练神经网络的主要方法。

    我们怎样把梯度下降应用于神经网络的学习过程呢?具体思路是用梯度下降来寻找权重(weights)$w_k$ 和 偏移(bias) $b_l$ ,从而最小化代价函数(6) 。为了看得更清楚,我们将梯度下降更新规则中的变量$v_j$用权重和偏移替换,进行重新表述。换句话说,我们现在的“位置”有 $w_k$ and $b_l$ 这些分量, 而梯度矢量 $\nabla C$ 也具有相应的分量 $\partial C / \partial w_k$ 和 $\partial C / \partial b_l$。将梯度下降规则用这些分量的形式写出来,我们有\begin{eqnarray} w_k & \rightarrow & w_k' = w_k-\eta \frac{\partial C}{\partial w_k} \tag{16}\\ b_l & \rightarrow & b_l' = b_l-\eta \frac{\partial C}{\partial b_l}. \tag{17}\end{eqnarray} 通过不断重复应用这一更新规则,我们可以“从山上滚下来”,符合预期的话,我们将找到代价函数的最小值。换句话说,这个规则可以用于神经网络的学习过程。

    在运用梯度下降规则的时候会碰到许多挑战难题。在后面的章节我们会仔细研究这些困难。但这里我只想提及其中的一个难题。为了更好地理解是什么难题,让我们回过头来看一下公式 (6) 定义的二次型代价函数。注意到这个代价函数具有形式$C = \frac{1}{n} \sum_x C_x$, 即,每一个训练样例误差 $C_x \equiv \frac{\|y(x)-a\|^2}{2}$ 的平均。实际上,为了计算梯度 $\nabla C$ ,我们需要对每个训练输入(trainning input)分别计算其梯度 $\nabla C_x$,然后再对它们求平均。不幸的是,如果训练输入的样本数量非常大,那么这样会消耗很多时间,因此导致神经网络的学习过程非常慢。

    有一个可用于加速学习过程的办法被称作 “随机梯度下降”(stochastic gradient descent)。 这个办法是通过随机选择训练输入的少量样本,并只计算这些样本的$\nabla C_x$的平均值来估算梯度 nablaC。通过计算这些少量样本的平均值,我们可以很快估算出梯度$\nabla C$的真实值,从而加速梯度下降过程,即加速神经网络的学习过程。

    具体来说, 首先“随机梯度下降” 要随机挑出 $m$(一个较小的数目)个训练输入。我们将这些随机的训练输入标记为 $X_1,X_2...X_m$, 并将它们称为 一个 “小组”(mini-batch)。假设样本数$m$足够大,我们预计这个随机小组 $\nabla C_{X_j}$ 的平均值应该和所有 $\nabla C_x$ 的平均值大致相等,即 \begin{eqnarray} \frac{\sum_{j=1}^m \nabla C_{X_{j}}}{m} \approx \frac{\sum_x \nabla C_x}{n} = \nabla C, \tag{18}\end{eqnarray} 这里第二个求和符号表示对整个训练数据集的求和。交换等号两边的顺序,我们有 \begin{eqnarray} \nabla C \approx \frac{1}{m} \sum_{j=1}^m \nabla C_{X_{j}}, \tag{19}\end{eqnarray} 这个公式明确了我们可以通过仅仅计算随机选择的“小组”梯度来估算整体的梯度值。

    为了将其和神经网络的学习过程直接联系起来,假设我们用 $w_k$ 和 $b_l$ 标记神经网络中的权重(weights)和 偏移(biases),那么随机梯度下降的工作方式为:随机挑出一“小组”训练输入,并用如下方式来训练\begin{eqnarray} w_k & \rightarrow & w_k' = w_k-\frac{\eta}{m} \sum_j \frac{\partial C_{X_j}}{\partial w_k} \tag{20}\\ b_l & \rightarrow & b_l' = b_l-\frac{\eta}{m} \sum_j \frac{\partial C_{X_j}}{\partial b_l}, \tag{21}\end{eqnarray} 这里的求和是对当前小组里的所有训练样本 $X_j$ 进行的。然后我们再随机挑出另一小组进行训练。如此反复下去,直到我们用尽了所有的训练输入。这个过程被称为完成了一“”(epoch)训练。到那个时候,我们就可以重新开始另一代训练。

    有的时候,我们需要注意:人们在标定代价函数(cost function)和对权重、偏移进行“组”(mini-batch)更新时所采用的惯例可能有所差异。公式 (6) 中,我们用了一个 $\frac{1}{n}$ 的因子对整体代价函数进行了标定。有时候,人们会忽略这个 $\frac{1}{n}$ 因子,即对所有训练样例“求和”而不是“求平均”。这一点在训练样例总数不能提前确定的时候非常有用,比如当更多的训练数据在不断实时产生的时候就会发生这种情况。同样类似,公式(20) 和(21) 中的“组”更新规则,有时候也会忽略求和前面的$\frac{1}{m}$因子。从概念上说,两者几乎没什么不同,因为这等价于重新标定了学习率 $\eta$ 而已。不过当我们在仔细对比不同的研究工作时,需要对此留心一点。

    我们可以将随机梯度下降看做是某种类似于民意调查的过程:将梯度下降应用于一“小组”样例要比应用于全部样例要容易得多,就如同采样民意调查要比全民公投要容易得多。例如,在MNIST样例中,整个训练集的样本数 $n=60000$ 个,而比如说我们选定的“小组”样本数为$m=10$个。这意味着我们在估算梯度的时候会快$6000$倍!当然,估算结果不一定精确——因为有统计涨落的误差——不过,我们不需要它精确:因为我们关心的仅仅是这个过程是不是整体向着$C$减小的方向在走,而不是要严格计算出梯度值。实际上,随机梯度下降在训练神经网络的过程中是非常常见和有效的手段,也是本书将要演绎的许多训练技巧的基础。

    练习

    • 梯度下降的一种极端方式是使用大小为1的“小组”。也就是,给定一个训练输入$x$,我们按照规则$w_k \rightarrow w_k' = w_k - \eta \partial C_x / \partial w_k$和$b_l \rightarrow b_l' = b_l - \eta \partial C_x / \partial b_l$来更新权重和偏差。然后选择另外一个训练输入,再来更新权重和偏差。反复这样,这个过程被称为在线在线增量学习。在线学习中,神经网络每次从一个训练输入中进行学习(就像人来开始那样)。请指出在线学习的优点和缺点,相对于具有“小组”大小为$20$的随机梯度下降算法。

    好,这一节的最后,我们来讨论一个对于刚刚接触梯度下降算法的人常有的困扰。在神经网络中,代价函数 $C$ 当然依赖于很多变量,即,所有的权重和偏移。因此,从某种意义上说,$C$ 定义了一个非常非常高维空间中的曲面。有些人可能会觉得;“嗯,我需要想象这些额外的维度”,然后开始担心:“我甚至连四维空间都没法想象,更别说五维或者五百万维了”。是不是这些人缺少了某种“真正的”超级数学家所具有的超能力?当然不是。即便是最牛哄哄的数学家也很难想象四维空间,如果可以的话。技巧在于这些数学家发展了一套描述问题的替代方法,而这正是我们刚才所做的:我们用一套代数形式的(而非直观的)算法来表征 $\Delta C$,从而进一步思考怎样让$C$不断减小。那些擅长思考高维空间的人,其实头脑中内建了一个“库”,这个“库”包含了许多类似于刚才描述的这些抽象方法。这些方法也许不像我们所熟知的三维空间那样简单直观,不过一旦你在头脑中也建立了这个“库”,你就可以驾轻就熟地“想象”高维空间了。这里我就不再深入讨论下去了,如果你对于数学家们怎样想象高维空间感兴趣的话,你可能会喜欢读 这篇讨论。虽然其中讨论到的一些方法非常复杂,但主要的方法还是比较直观,也可以被大家掌握的。

    使用神经网络识别手写数字

    好了,让我们使用随机梯度下降算法和MNIST训练数据来写一个能够学习识别手写数字的程序。首先我们需要获得MNIST数据。如果你是git的使用者,你可以通过克隆这本书的源代码库来获得这些数据,

    git clone https://github.com/mnielsen/neural-networks-and-deep-learning.git
    

    如果你不使用git,你可以通过这里下载所需的数据和源代码。

    顺便说一下,早些时候我在描述MNIST数据时,我说过它被分成了两份,其中60000幅图片用于训练,另外10000幅用于测试。那是MNIST官方的陈述。事实上,我们将要使用的分法会有一点区别。用于测试的数据我们不会改动,但是60000幅用于训练的图片将被分成两部分:其中一份包括50000幅图片,将用于训练我们的神经网络,剩下的10000幅图片将作为验证集(validation set)。虽然我们在这一章不会用这个验证集,但是在这本书之后的部分我们会发现验证集对确定怎么设置神经网络的一些超参数(hyper-parameters)很有用,比如学习速率等,这些参数没有在我们的学习算法中直接选取。尽管验证集不是MNIST数据的原始规范,但是很多人都是用这种方式使用MNIST,验证集的使用在神经网络中非常普遍。从现在开始,当我说到“MNIST训练数据集”时,我指的是我们分出来的50000幅图片,而不是原来的60000幅图片集。**像之前提到的,MNIST数据集是基于NIST(美国国家标准与技术研究所)收集的两种数据。为了构建MNIST数据集,原始数据被Yann LeCun,Corinna Cortes和Christopher J. C. Burges转换为更加方便使用的格式。请参考这个连接查看细节。我的git仓库中存放的数据集是用一种更容易在Python中使用的格式。我也是从蒙特利尔大学(链接)LISA机器学习实验室获得的这个特殊格式。

    除去MNIST数据,我们还需要一个叫Numpy的Python函数库用于做快速线性代数运算。如果你还没有安装Numpy,你可以从这里获得它。

    在给出完整的列表之前,下面让我先解释一下神经网络程序的核心特征。最重要的部分就是一个叫Network的类(class),它被用来表示一个神经网络。下面是用于初始化Network对象的代码:

    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:])]
    

    在这段代码中,列表sizes包含在对应层的神经元的个数。因此,比如说,如果我们想要构造一个Network对象,其中第一层有2个神经元,第二层有3个神经元,最后一层只有1个神经元,我们可以通过下面的代码做到些:

    net = Network([2, 3, 1])
    
    Network对象中所有的偏移(biases)和权重(weights)都通过Numpy中的np.random.randn函数产生标准正态分布的随机数进行初始化。这个随机的初始化给了随机梯度下降算法一个起始点。在随后的章节中我们会找到更好的初始化的方法,但是现在会用这个方法。注意到Network初始化的代码假设了第一层神经元为输入层,并且没有对那些神经元设置偏移量,因为只有在计算之后层的输出时才用到偏移量。

    同时也要注意到,偏移量和权重系数被储存为Numpy矩阵的列表。因此,比如说net.weights[1] 就是一个Numpy矩阵,它储存着连接第2层和第3层神经元的权重系数。(注意不是第1层和第2层,因为python列表的索引指标是从0开始的。)因为net.weights[1]比较冗长,让我们用w表示这个矩阵。矩阵元$w_{jk}$就是连接第2层中第$k$个神经元和第3层中第$j$个神经元的权重系数。索引指标$j$和$k$这样的排序是不是看上去有点奇怪,交换$j$和$k$的顺序是不是看上去更合理?这样排序的最大的好处就是第三层的神经元的激活向量(vector of activations)可以直接写成: \begin{eqnarray} a' = \sigma(w a + b). \tag{22}\end{eqnarray} 这个公式描述的过程有点多,因此让我们一点一点地来解开。$a$是第2层神经元的激活向量(vector of activations)。为了得到$a'$,我们先将$a$乘以权重矩阵$w$,然后加上偏移向量$b$,最后我们对向量$w a +b$中的每一个元使用函数$\sigma$。(这叫函数$\sigma$的向量化(vectorizing)。)很容验证,在计算sigmoid神经元的输出结果时,公式(22) 给出的结果会和我们之前用公式(4) 所说的方式给出的结果是一样的。

    练习

    • 将公式(22) 写成向量元素的形式,验证在算sigmoid神经元的输出时,它给出的结果和公式(4) 的结果是一样的。

    有了这些在心中,很容易写一段程序用于计算一个Network 对象的输出结果。我们从定义sigmoid函数开始:

    def sigmoid(z):
        return 1.0/(1.0+np.exp(-z))
    
    注意到,当输入量z是一个向量或者Numpy数组,Numpy自动地对向量的每一个元运用sigmoid函数。

    然后我们对Network 类加一个feedforward方法。当一个输入量a 给了network,这个方法就给出对应的输出**假定输入a是一个(n, 1)Numpy的n维数组,不是一个(n,)向量。这里n是网络的输入数量。如果你试着用一个(n,)的向量作为输入,你将得到一个奇怪的结果。虽然用一个(n,)的向量看起来是一种更自然的选择,但是使用一个(n, 1)的n维数组使得更容易修改代码来一次性前向反馈多个输入,这时常更加方便。. 这个方法所做的就是将公式(22) 应用到每一层:

        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
    

    当然,我们想让Network 对象做的最主要的事情是学习。为了达到这个目的,我们要给它们一个SGD方法,用这个方法实现随机梯度下降算法。下面是代码。有几处地方有一点难以理解,但是我们会在给出代码后加以讲解。

        def SGD(self, training_data, epochs, mini_batch_size, eta,
                test_data=None):
            """Train the neural network using mini-batch stochastic
            gradient descent.  The "training_data" is a list of tuples
            "(x, y)" representing the training inputs and the desired
            outputs.  http://tensorfly.cn/home The other non-optional parameters are
            self-explanatory.  If "test_data" is provided then the
            network will be evaluated against the test data after each
            epoch, and partial progress printed out.  This is useful for
            tracking progress, but slows things down substantially."""
            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是一个由(x, y)类型元组组成的列表,其中x表示训练数据的输入,y为对应的输出。变量epochsmini_batch_size正如你所预期的一样,分别是训练的epochs数和取样时mini_batch_size的大小。eta是学习的速率,$\eta$。如果可选参量test_data被提供了,程序在每一个训练“代”(epoch)结束之后都会评估神经网络的表现,然后输出部分进展。这对跟踪进展有用,但是会大大减慢速度。

    这段代码按照下面的方式运行。在每一代(epoch),程序首先随机地打乱训练数据的顺序,然后将数据分成合适大小的mini_batch。这个很容,就是随机从训练数据中抽样。接下来,对每一个epochs,我们使用一次梯度下降算法。这个通过self.update_mini_batch(mini_batch, eta)所对应的程序代码实现,这段代码会利用mini_batch中的训练数据,通过一个梯度下降的循环来更新神经网络的权重系数和偏移量。下面是update_mini_batch方法的源代码:

        def update_mini_batch(self, mini_batch, eta):
            """Update the network's weights and biases by applying
            gradient descent using backpropagation to a single mini batch.
            The "mini_batch" is a list of tuples "(x, y)", and "eta"
            is the learning rate."""
            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)]
    
    大部分工作都是下面这一行做的:
                delta_nabla_b, delta_nabla_w = self.backprop(x, y)
    
    这个牵涉到一个叫逆传播(backpropagation)的算法,这个算法可以快速计算代价函数(cost function)的梯度。因此update_mini_batch 所作的仅仅就是对 mini_batch中每一个训练数据样本计算这些梯度,然后更新self.weightsself.biases

    现在我不会展示出self.backprop的源代码。我们会在下一章学习逆传播是怎么工作的,以及self.backprop的源代码。现在,就假设它如上面所说的方式工作,返回每一个训练数据样本x所对应成本的正确梯度。 让我们看一下整个程序,包括之前忽略了的说明文档。除了self.backprop,整个程序是无需解释的--所有重要的事情是在self.SGDself.update_mini_batch中做的,我们之前已经讨论过了。self.backprop方法在计算梯度的时候还要用到一些额外的函数,包括sigmoid_prime,用来计算$\sigma$函数的导数,和self.cost_derivative。后面这个我暂时不会在这里描述。只要看一看这些方法的代码或者说明文档,你就可以了解它们的主要意思(甚至细节)。我们将在下一章详细研究它们。注意到这个程序看上去很长,但是大部分都是说明文档,让这个程序更容易理解。实际上,整个程序只有74行非空白、非注释代码。所有的代码可以在GitHub上找到。

    """
    network.py
    ~~~~~~~~~~
    
    A module to implement the stochastic gradient descent learning
    algorithm for a feedforward neural network.  Gradients are calculated
    using backpropagation.  Note that I have focused on making the code
    simple, easily readable, and easily modifiable.  It is not optimized,
    and omits many desirable features.
    """
    
    #### Libraries
    # Standard library
    import random
    
    # Third-party libraries
    import numpy as np
    
    class Network(object):
    
        def __init__(self, sizes):
            """The list ``sizes`` contains the number of neurons in the
            respective layers of the network.  For example, if the list
            was [2, 3, 1] then it would be a three-layer network, with the
            first layer containing 2 neurons, the second layer 3 neurons,
            and the third layer 1 neuron.  The biases and weights for the
            network are initialized randomly, using a Gaussian
            distribution with mean 0, and variance 1.  Note that the first
            layer is assumed to be an input layer, and by convention we
            won't set any biases for those neurons, since biases are only
            ever used in computing the outputs from later layers."""
            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:])]
    
        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
    
        def SGD(self, training_data, epochs, mini_batch_size, eta,
                test_data=None):
            """Train the neural network using mini-batch stochastic
            gradient descent.  The ``training_data`` is a list of tuples
            ``(x, y)`` representing the training inputs and the desired
            outputs.  The other non-optional parameters are
            self-explanatory.  If ``test_data`` is provided then the
            network will be evaluated against the test data after each
            epoch, and partial progress printed out.  This is useful for
            tracking progress, but slows things down substantially."""
            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)
    
        def update_mini_batch(self, mini_batch, eta):
            """Update the network's weights and biases by applying
            gradient descent using backpropagation to a single mini batch.
            The ``mini_batch`` is a list of tuples ``(x, y)``, and ``eta``
            is the learning rate."""
            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)]
    
        def backprop(self, x, y):
            """Return a tuple ``(nabla_b, nabla_w)`` representing the
            gradient for the cost function C_x.  ``nabla_b`` and
            ``nabla_w`` are layer-by-layer lists of numpy arrays, similar
            to ``self.biases`` and ``self.weights``."""
            nabla_b = [np.zeros(b.shape) for b in self.biases]
            nabla_w = [np.zeros(w.shape) for w in self.weights]
            # feedforward
            activation = x
            activations = [x] # list to store all the activations, layer by layer
            zs = [] # list to store all the z vectors, layer by layer
            for b, w in zip(self.biases, self.weights):
                z = np.dot(w, activation)+b
                zs.append(z)
                activation = sigmoid(z)
                activations.append(activation)
            # backward pass
            delta = self.cost_derivative(activations[-1], y) * \
                sigmoid_prime(zs[-1])
            nabla_b[-1] = delta
            nabla_w[-1] = np.dot(delta, activations[-2].transpose())
            # Note that the variable l in the loop below is used a little
            # differently to the notation in Chapter 2 of the book.  Here,
            # l = 1 means the last layer of neurons, l = 2 is the
            # second-last layer, and so on.  It's a renumbering of the
            # scheme in the book, used here to take advantage of the fact
            # that Python can use negative indices in lists.
            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)
    
        def evaluate(self, test_data):
            """Return the number of test inputs for which the neural
            network outputs the correct result. Note that the neural
            network's output is assumed to be the index of whichever
            neuron in the final layer has the highest activation."""
            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)
    
        def cost_derivative(self, output_activations, y):
            """Return the vector of partial derivatives \partial C_x /
            \partial a for the output activations."""
            return (output_activations-y)
    
    #### Miscellaneous functions
    def 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))
    

    这个程序识别手写数字的效果怎么样呢? 好,让我们从加载MNIST数据开始。我将用下面这个小脚本mnist_loader.py来做这件事, 我们将在python shell中运行下面的指令,

    >>> import mnist_loader
    >>> training_data, validation_data, test_data = \
    ... mnist_loader.load_data_wrapper()
    

    当然,这也可以通过一个独立的python程序来完成,不过如果你一直在跟着这个教程走,在python shell中完成应该是最简单的。

    加载完MNIST数据后,我们将要建立一个有$30$个隐藏神经元的Network。这件事是在我们导入上面的Python程序之后去做的,对应的变量我们称其为network,

    >>> import network
    >>> net = network.Network([784, 30, 10])
    

    最终,我们将用随机梯度下降法,由MNIST training_data完成学习过程。该过程将历经30代(epoch),其每“组”(mini-batch)的样本数为10,学习率(learning rate)为 $\eta = 3.0$,

    >>> net.SGD(training_data, 30, 10, 3.0, test_data=test_data)
    

    注意:如果你在一边在读教程一边在跑程序,你会发现这些程序的执行需要花点时间 —— 对于一般的主机(2015年左右),大概需要几分钟。我建议你一边运行,一边继续阅读,并且周期性地查看代码的输出结果。如果你时间比较赶,你可以通过减少学习的epoch数目,减少隐藏神经元的数目,或者只用部分训练数据等方法来加速程序的运行过程。注意用于生产环境的代码(production code)会远远快于现在的速度:这里的python脚本是用来帮助你理解神经网络是怎样工作的,而并非以高效运行为目标的代码!当然,一旦我们将神经网络训练好,我们就可以在几乎任何计算平台上运行得飞快。例如,一旦我们通过神经网络的学习过程获得了一组很好的权重和偏移,我们就可以将其非常容易地移植为网页浏览器的javascript版本,或者是移动设备的原生应用版本。不管怎样,下面是训练神经网络过程的一组结果。这组结果显示了经历每一代训练后,可以正确辨认出来的测试图片的个数。正如你所看到的,经历过第一代训练后,其准确率已经达到10,000张图片中识别出9,129张图片,并且这个数字在不断增加,

    Epoch 0: 9129 / 10000
    Epoch 1: 9295 / 10000
    Epoch 2: 9348 / 10000
    ...
    Epoch 27: 9528 / 10000
    Epoch 28: 9542 / 10000
    Epoch 29: 9534 / 10000
    

    就是说,训练后的网络给出了一个分类准确率峰值大约在 $95\%$ 到 $95.42\%$ 之间的结果 ("第28代")! 这个首次试验的结果非常鼓舞人心。不过我需要提醒你的是,如果你来运行代码的话,可能结果和我的不完全一样,因为我们初始化权重和偏移参数的时候是随机选择的。为了获得这里的结果,我实际上挑了三次运行中最好的那次结果。

    让我们将隐藏神经元的数目改为$100$个,重新跑一遍上面的程序试验。跟前面的情况一样,如果你在一边读本文一边运行代码,需要提醒你的是,这个运行时间可能会有点长(在我的主机上,这个试验每一代大约需要几十秒的时间),因此在代码执行的时候,你最好并行地接着往下读。

    >>> net = network.Network([784, 100, 10])
    >>> net.SGD(training_data, 30, 10, 3.0, test_data=test_data)
    

    我们能非常确切地看到,这个试验将结果的准确率提高到 $96.59\%$。至少,对于当前情形,增加隐藏神经元的个数让结果变得更好。**读者反馈指出这个试验的许多变化,有一些训练结果更加糟糕。使用在第三章将引入的技术可以大幅度减少不同网络训练的性能变化。

    当然,为了或者这些准确率,我们还要选择一个具体的训练“代”数(number of epochs),mini-batch 的大小(即每个batch中的样本个数),以及学习率 $\eta$。正如上面提到的,这些都是我们神经网络的超参数(hyper-parameters)。所谓“超参数”是要将它们和学习算法的训练“参数”(权重和偏移)相区分。如果我们选定的“超参数”比较差,我们的结果可能也比较糟糕。例如,如果我们选择学习率为 $\eta = 0.001$ 的话,

    >>> net = network.Network([784, 100, 10])
    >>> net.SGD(training_data, 30, 10, 0.001, test_data=test_data)
    

    这些结果就显得不那么鼓舞人心了,

    Epoch 0: 1139 / 10000
    Epoch 1: 1136 / 10000
    Epoch 2: 1135 / 10000
    ...
    Epoch 27: 2101 / 10000
    Epoch 28: 2123 / 10000
    Epoch 29: 2142 / 10000
    
    不管怎样,你还是看到神经网络的表现还是在随着学习时间的推移不断慢慢变好。这个结果暗示我们需要增加学习率,比如说 $\eta = 0.01$。如果我们这么做之后,结果更好了,这就意味着我们应该进一步增加学习率。(如果某种改变让结果更好,那就试着再多改变一点!)如果我们这么重复若干次后,我们的学习率可能就比较大了,比如说 $\eta = 1.0$ (有可能甚至会调到$\eta = 3.0$),这个值就和我们之前的试验值相近了。所以,就算我们一开始“超参数”选得不好,我们至少有足够的信息量可以知道该怎样改进我们“超参数”的选择。

    一般来说,分析神经网络的错误是很有挑战性的。尤其是,当初始选择的“超参数”,其得到的结果和随机噪声没什么差别的话,这句话就更对了。假设我们选择神经网络的隐藏神经元数目还是30个,不过将学习率变为 $\eta = 100.0$:

    >>> net = network.Network([784, 30, 10])
    >>> net.SGD(training_data, 30, 10, 100.0, test_data=test_data)
    
    这种选择就有点过了,即,学习率的值有太高了:
    Epoch 0: 1009 / 10000
    Epoch 1: 1009 / 10000
    Epoch 2: 1009 / 10000
    Epoch 3: 1009 / 10000
    ...
    Epoch 27: 982 / 10000
    Epoch 28: 982 / 10000
    Epoch 29: 982 / 10000
    
    现在设想一下我们第一次遇到这个问题。当然,我们从之前的试验知道正确的减少学习率的方式。但是如果是我们第一次遇到这个问题,那么在结果中没有什么能指导我们怎么去做。我们可能不只担心学习率,而且还担心神经网络的其他每个方面。我们可能想知道是否采用了一种让网络很难学习的初始化权重和偏移?或者我们没有足够的训练数据来正确学习?可能我们没有进行足够的“代”来学习?或者用这种架构的神经网络不可能学会去识别手写数字?可能学习率太?或者可能学习率太高?当你第一次遇到这个问题,你不可能总是很确定它们。

    我们应该从中吸取的经验是,调试分析神经网络的工作不是那么简单的事。就像一般的编程调试一样,这其实是个手艺活儿。为了能够从神经网络中获得漂亮的结果,你需要学习调试这门手艺。更普遍地来说,我们需要发展一套启发性的思路来选择好的“超参数”和网络层次结构。本书我们将对此展开详细论述,包括上面的程序我是怎样选择“超参数”的。

    练习

    • 试着构造一个只有两层的神经网络——一个输入层和一个输出层,没有隐含层——即分别784和10个神经元。使用随机梯度下降法训练这个网络,你能得到什么样的分类精确度?

    之前,我跳过了关于怎样加载MNIST数据的细节。这个其实非常直截了当。出于完整性,这里是代码。用于存储 MNIST数据的数据结构在代码说明中作了解释—— 都是很直接的东东, tuples 和 Numpyndarray 对象 的lists (如果你对ndarray不熟,可以将其看作是矢量):

    """
    mnist_loader
    ~~~~~~~~~~~~
    
    A library to load the MNIST image data.  For details of the data
    structures that are returned, see the doc strings for ``load_data``
    and ``load_data_wrapper``.  In practice, ``load_data_wrapper`` is the
    function usually called by our neural network code.
    """
    
    #### Libraries
    # Standard library
    import cPickle
    import gzip
    
    # Third-party libraries
    import numpy as np
    
    def load_data():
        """Return the MNIST data as a tuple containing the training data,
        the validation data, and the test data.
    
        The ``training_data`` is returned as a tuple with two entries.
        The first entry contains the actual training images.  This is a
        numpy ndarray with 50,000 entries.  Each entry is, in turn, a
        numpy ndarray with 784 values, representing the 28 * 28 = 784
        pixels in a single MNIST image.
    
        The second entry in the ``training_data`` tuple is a numpy ndarray
        containing 50,000 entries.  Those entries are just the digit
        values (0...9) for the corresponding images contained in the first
        entry of the tuple.
    
        The ``validation_data`` and ``test_data`` are similar, except
        each contains only 10,000 images.
    
        This is a nice data format, but for use in neural networks it's
        helpful to modify the format of the ``training_data`` a little.
        That's done in the wrapper function ``load_data_wrapper()``, see
        below.
        """
        f = gzip.open('../data/mnist.pkl.gz', 'rb')
        training_data, validation_data, test_data = cPickle.load(f)
        f.close()
        return (training_data, validation_data, test_data)
    
    def load_data_wrapper():
        """Return a tuple containing ``(training_data, validation_data,
        test_data)``. Based on ``load_data``, but the format is more
        convenient for use in our implementation of neural networks.
    
        In particular, ``training_data`` is a list containing 50,000
        2-tuples ``(x, y)``.  ``x`` is a 784-dimensional numpy.ndarray
        containing the input image.  ``y`` is a 10-dimensional
        numpy.ndarray representing the unit vector corresponding to the
        correct digit for ``x``.
    
        ``validation_data`` and ``test_data`` are lists containing 10,000
        2-tuples ``(x, y)``.  In each case, ``x`` is a 784-dimensional
        numpy.ndarry containing the input image, and ``y`` is the
        corresponding classification, i.e., the digit values (integers)
        corresponding to ``x``.
    
        Obviously, this means we're using slightly different formats for
        the training data and the validation / test data.  These formats
        turn out to be the most convenient for use in our neural network
        code."""
        tr_d, va_d, te_d = load_data()
        training_inputs = [np.reshape(x, (784, 1)) for x in tr_d[0]]
        training_results = [vectorized_result(y) for y in tr_d[1]]
        training_data = zip(training_inputs, training_results)
        validation_inputs = [np.reshape(x, (784, 1)) for x in va_d[0]]
        validation_data = zip(validation_inputs, va_d[1])
        test_inputs = [np.reshape(x, (784, 1)) for x in te_d[0]]
        test_data = zip(test_inputs, te_d[1])
        return (training_data, validation_data, test_data)
    
    def vectorized_result(j):
        """Return a 10-dimensional unit vector with a 1.0 in the jth
        position and zeroes elsewhere.  This is used to convert a digit
        (0...9) into a corresponding desired output from the neural
        network."""
        e = np.zeros((10, 1))
        e[j] = 1.0
        return e
    

    上面我说我们程序的结果相当好。这具体是啥意思呢?究竟是相对于啥东东好? 如果我们有一些可以对比的测试基准,所谓的“好”才更有信息量一些。最简单的基准,当然是纯随机地去猜数字。这个准确率大概是 $10\%$。我们的结果当然远比这要好!

    对于一个不那么普通的比较基准呢?让我们先试试非常简单的办法:去看图片的暗度值。比如说,一张$2$的图片,一般来说比$1$的图片要暗一些,仅仅是因为$2$相对于$1$有更多像素是黑的,比如下面的例子:

    这暗示我们可以用训练数据来计算每个数字,$0, 1, 2,\ldots, 9$,的平均暗度值。每当提供我们一张新图,我们就计算这张图有多暗,然后根据那个数字的平均暗度值和这张图的暗度值最接近,来猜测这张图是什么数字。这个方法很简单,也很容易编程实现,所以这里我就不把代码写出来了,如果你对此感兴趣,可以看一看这个Github库。即便如此,这相对于随机猜测已经有了很大进度,对于$10,000$张测试图片,已经可以猜对大概 $2,225$ 张,即准确率大约是$22.25\%$。

    要求准确率在$20\%$ 和 $50\%$ 之间,找到其他办法来实现并不困难。如果你再努力一点,超过$50\%$ 的准确率也不难。但是,如果想要准确率远远高于这个区间,建立机器学习的算法就很有帮助了。让我们来使用知名度最高的算法之一,向量机(support vector machine,简记 SVM)。如果你对向量机不太熟悉,不用担心,我们不需要关心向量机的具体工作细节。我们将使用一个叫 scikit-learn的Python库。它提供了SVM著名的C语言库LIBSVM的一个简单的Python界面。

    如果我们使用默认的设置来跑scikit-learn的SVN分类器,对于10,000张测试图片,大概能猜对9,435张。(代码可以在 这里找到。)这相对于根据图片暗度值来猜,显然已经有很大的进步。确实,这说明SVM的表现相对于神经网络大致相当,仅仅稍差一点点。在后面的章节,我们将引入新的手段让神经网络的表现可以远远超过SVM。

    然而这并不是故事的全部。对于scikit-learn向量机,10,000张图片中猜对9,435的识别率是对于默认设置而言的。SVM中有不少可以调节的参数,我们可以寻找那些可以让SVM表现更好的参数值。这里我就不具体去找了,有兴趣的可以参考 Andreas Mueller 的 这篇博客。Mueller 说明了通过优化SVM的参数值,有可能让其准确率超过$98.5\%$。话句话说,参数合适的SVM在70个数字中只会认错1个。这个表现已经非常出色了!我们的神经网络可以做得更好吗?

    事实上,确实可以更好。现在,对于解决MNIST问题,一个良好设计的神经网络,其表现超过所有其他方法,包括向量机。(机器学习社区tensorfly.cn原创, qq群号472113439) 目前(2013)的记录是 10,000张图片中认对9,979张。这个工作是由 Li WanMatthew Zeiler, Sixin Zhang, Yann LeCun, 和 Rob Fergus 实现的。后面我们会见到他们用过的大部分技术手段。对于那种程度的准确率,其表现已经和人眼相当了,某种意义上甚至比人眼更高了,因为 MNIST 中的不少图片对于人眼来说也很难认对,例如:

    我相信你会同意这些图确实很难认!注意到MNIST数据集中还有这些难认的图片存在,神经网络居然可以精准识别10,000张除去21张以外的其他所有图片,这确实是非常出色的。编程的时候,我们通常会认为要解决类似于识别MNIST数字这样的复杂问题,需要一个复杂的算法。但即便是刚提到的Wan et al 文献,他们所使用的神经网络算法也是相当简单的。所有的复杂性都从训练数据中自动习得。某种意义上说,从我们的结果和其他更复杂文献中的结果中,对于许多问题,我们有这样的结论:

    复杂的算法 $\leq$ 简单的学习算法 + 好的训练数据

    通往深度学习(deep learning)

    尽管神经网络的优异表现让人印象深刻,但这种优异变现却似乎是神秘难解的。神经网络中的权重和偏移是自动发现的。而这意味着我们没法对网络怎样工作给出一个直截了当的解释。对于分类手写文字的网络,我们可以找到某种方法去理解其工作原理吗?如果知道了这些原理,我们可以让神经网络表现得更好吗?

    为了让问题表现得更尖锐,假设再过几十年之后,神经网络最终实现了真正意义的人工智能(AI),我们就最终理解那样的智能神经网络了吗?也许这个神经网络依然对于我们来说是个“黑匣子”,其中的权重和偏移我们都没法理解,因为这些参数都是通过学习自动获得的。早年研究AI的人员希望通过努力创建某种人工智能来帮助人们理解人类“智能”的原理或者人脑的工作方式。然而,最终结果可能是,我们最终对人脑和人工智能的工作方式都理解不了!

    为了强调这些问题,让我们回到本章一开始对人工神经元所做的解释,即,它是一种衡量凭据重要性(weighing evidence)的手段。假设我们利用它来决定一张图是否是人脸:

    Credits: 1. Ester Inbar. 2. Unknown. 3. NASA, ESA, G. Illingworth, D. Magee, and P. Oesch (University of California, Santa Cruz), R. Bouwens (Leiden University), and the HUDF09 Team. Click on the images for more details.

      

    我们可以采用识别手写数字一样的方法来处理这个问题,即,将图片的像素值作为神经网络的输入,而将某个神经元的最终输出作为结果来表示“是的,这是一张脸” 或者 “不是,这不是人脸”。

    假设让我们来做这件事,但是不采用学习算法。取而代之,我们将试图选择恰当的权重和偏移参数,手工设计一个网络。我们具体将怎么办呢?让我们暂时将整个神经网络都忘掉,一种启发性的办法是我们可以将这个问题细化为以下“子问题”:是不是有个眼睛在图片的左上方?是不是有另一个眼睛在图片的右上方?是不是有个鼻子在中间?是不是有张嘴在中下方?是不是有头发在顶上?如此等等。

    如果这些问题的许多答案都是 “是的”,或者仅仅是 “很可能是”,那么我们就可以得出结论:这张图片很可能是人脸。与此相反,如果这些问题的绝大多数答案都是“不是”,那么这张图片就很可能不是人脸。

    当然,这仅仅是种大体上的启发性方法,并且也有很多缺陷。比如说,这个人也许是个秃子,因此没有头发。也许我们只能看到脸的一部分,或者是从某个特殊视角看到的脸,因此脸部的某些特征可能是模糊的。不过,这种启发性的思路依然暗示着:如果我们可以用神经网络来解决这些“子问题”,那么我们也许可以通过将解决这些“子问题”的网络合并起来,来建立一个可以实现人脸识别的神经网络。这里有一种可能的网络结构,其中方框标记的是解决“子问题”的网络。注意:我们并不是真打算用这个方法来解决人脸识别问题,而是将其作为一种对理解神经网络工作方式建立某种直观印象和感觉的手段。下面是网络结构:

    这些子网络似乎也应该可以进一步细化。假设我们来考虑这个问题:“是不是有个眼睛在图片的左上方?” 这个问题可以被进一步细化为如下“子问题”:“是不是有眉毛?”;“是不是有睫毛?”;“是不是有瞳孔?”;如此等等。当然,这些问题其实应该也包含相关的位置信息,例如:“眉毛是在左上方,并且在瞳孔上方吗?” 。不过,为了简化表述,我们认为对于“是不是有个眼睛在图片的左上方?”这个问题,我们可以进一步细化为:

    这些问题当然可以通过多层结构(multiple layers)被一步一步更加细化。直到最终,我们要通过子网络来解决的问题如此简单,以至于可以在像素级别上很容易地回答它们。比如说,这些问题可能是:某种很简单的形状是否出现于图片某个特定的位置上。这些问题可以由直接与图片像素相连接的神经元来回答。

    最终的结果是,神经网络通过一系列的“多层结构”将一个复杂的问题:“图片上有没有人脸?”逐步简化为在像素级别就可以轻易回答的简单问题。其中靠前的结构层用来回答关于输入图片的非常简单而具体的问题,靠后的结构层用来回答较复杂而抽象的问题。这种多层结构,即有两个或更多“隐藏层”(hidden lyaers)的网络,被称为深度神经网络

    当然,我前面并没有说该怎样将一个问题逐渐一步一步细化为“子网络”。并且,通过手工设计来决定网络中的权重和偏移显然行不通的。因此,我们将通过训练数据,采用学习算法来自动决定网络中权重和偏移参数,当然也决定了问题的概念层次结构。在上世纪八九十年代,研究者们试图用“随机梯度下降法” 和 “逆传播”(backpropagation)算法来训练深度神经网络。不幸的是,除了某些特殊的结构,他们没有成功。这些网络可以学习,但是非常之慢,以至于没有实用性。

    2006年以来,一系列的新技术被发展出来,让深度神经网络的学习成为可能。这些深度学习的技术同样是基于“随机梯度下降法” 和 “逆传播”,但是引入了一些新的办法。这些新技术让训练更深(也更大)的神经网络成为现实——人们现在一般可以训练有5到10个“隐藏层”的神经网络。并且对于许多问题,这些深度网络的表现要远远好于浅的网络,即,只有1个“隐藏层”的网络。其中的原因,当然是因为深度网络可以建立起一个复杂的概念层次结构。这有点像编程语言通过模块化设计和抽象出类来实现一个复杂的计算机程序。深度网络和潜网络的区别有点像一种可以调用函数的编程语言和不能调用函数的编程语言之间的区别。在神经网络中,抽象方式可能和编程语言有所不同,但其重要性确是一样的。

    展开全文
  • 用哪种字体博客比较好尼?

    千次阅读 2015-02-24 14:10:05
    用哪种字体博客比较好尼?

    字体写博客?!
    一、宋体:为适应印刷术而生,因互联网半死。

    宋体的特点是:“横平竖直,横细竖粗,起落笔有棱有角。”

    “横平竖直”是为美观;

    “横细竖粗”是为坚固,为何坚固?

    度娘说:木板具有木纹,一般都是横向,刻制字的横向线条和木纹一致,比较结实;但刻制字的竖向线条时和木纹交叉,容易断裂。因此字体的竖向线条较粗,横向较细。

    说实话,我没看懂。

    “起落笔有棱有角”为防磨损。

    宋体一旦放大至19px及以上或屏幕亮度比较大的时候,就会出现屏幕光侵蚀字体的现象,容易出现视觉上笔画不连贯等问题,影响文字的阅读效率,夜间尤其如此。

    当宋体显示为17px、19px-24px时,易出现锯齿、笔画不均的现象。

    如果网页没有指定字体,就会默认为宋体。

    像XP系统,即便你指定了微软雅黑的字体,可能显示的仍为宋体,因为XP系统并没有自带微软雅黑字体。

    如果你需要用特别大的字体,建议放弃宋体。

    二、仿宋:看仿宋,不轻松

    模仿宋体字的结构、笔意,改成笔画粗细一致、秀丽狭长的印刷字体,这就是仿宋体。

    我建议最好不要用仿宋,当设置为24px的时候,并且让浏览器显示到200%时,字已失真,笔画上好多刺,看着不爽,

    三、楷体:尽量不用。

    楷体的特点是:“形体方正,笔画平直,可作楷模。”

    楷书是我国封建社会南北魏到晋唐最为流行的一种书体。

    我测试了楷体,和仿宋的效果差不多,很容易出现锯齿,建议不用。

    四、黑体:太压抑了。

    黑体是以几何学的方式确立汉字的基本结构。

    由于汉字笔划多,小字黑体清晰度较差,建议小字体不要用黑体。

    当我把字体设置为24像素时,黑体给人一种浓密、稠黑、黑云压城城欲摧的感觉。

    建议不用黑体。

    五、微软雅黑:压轴,推荐。

    刚开始,我完全没有想到微软雅黑。

    查资料的过程中,认识到了这种字体,并且测试了一下,效果确实很好。

    下面的内容摘自百科。

    在微软Windows Vista发布之前,中文操作系统的默认字体是宋体或者细明体,他们都属于衬线字体,无论是从审美角度还是从眼睛的承受程度都不及作为无衬线字体的微软雅黑体或者微软正黑体,因为笔划上过多的点缀(笔划末端的小三角)很容易造成视觉疲劳(尤其是显示在屏幕上)!而很早以前,苹果公司就很有远见的使用华文细黑作为其系统的默认字体!

    微软雅黑的特点有:

    1. 单独设计的粗体。一个字100美元,成本翻倍。

    2. 显示品质优异的斜体。

    在 windows 上第一次看到如此清晰的中文斜体,真的有点令人感动。虽然是 14px ,但宋体的斜体已经显得支离破碎了。

    1. 更清晰的小号字显示。

    2. 对于最常用的字号14px ,微软雅黑的显示非常的清晰和优美,中英文的搭配非常的和谐。

    和宋体相比,雅黑的字形不是正方形的,而是稍微的扁宽,字间距很小,这样的处理使得默认的行间距更为明晰;同时雅黑的字心显得更为饱满,在同样的字号下,雅黑的单字面积就显得更大,更容易识别,阅读起来也更舒服。

    5 更优美而现代的字形设计。

    也许有人说,XP系统用户还有很多,不过你可以设置两个字体啊,让微软雅黑在前,宋体在后,当没有微软雅黑的时候,自然显示的就是宋体了。

    总结:建议大家使用微软雅黑。

    本文转自
    [http://www.ebeta.org/post/641.html]

    展开全文
  • 写一手好:硬笔书法轻松自学指南(知乎周刊 Plus) 知乎编辑团队 楷书,认知好的范本 2017-03-16 《黄自元间架结构九十二法》 选本好字帖 2017-03-16 先谈书体。 前人对练习书法的程序,各有主张。有的认为...
  • 毛笔前要了解的

    千次阅读 2013-07-14 14:26:11
    毛笔之前,首先得准备必备工具,那就是古人称之为“文房四宝”的、墨、纸、砚。 第节 毛笔 、 毛笔的种类 在挑选毛笔之前,我们先要了解一下毛笔的种类和性能。 毛笔的种类很,笔头都是用...
  • 手写数字识别系统之图像分割

    千次阅读 2016-11-17 16:08:00
    背景本文,主要介绍我之前在学校时候,研究的...并且为了这个项目,我当时还特地整套神经网络库,从图像处理开始到最后的识别过程,没有使用任何第三方库,都是从0还是起 也没有用到opencv啊什么的。上层u
  • 我相信大家都不陌生,不论是联系人列表还是城市列表,基本上都是需要字母导航,那我们就有必要来研究一下这个思路的探索了,毕竟这是个非常常用的功能,如果现在把轮子造好,那以后也可以节省很的时间,同时,...
  • (1)先搞清楚我们的对象!才能针对性的测试客户最在意的地方,设置测试的优先级。 (2)按测试类型逐一测试。测试主要就分为界面测试、功能测试、性能测试(压力)、安全测试、兼容...23333倒着、墙上)  
  • 文字识别()--传统方案综述

    万次阅读 多人点赞 2019-02-17 12:48:15
    文字识别是计算机视觉研究领域的分支之,归属于...计算机文字识别,俗称光学字符识别,英文全称是Optical Character Recognition(简称OCR),它是利用光学技术和计算机技术把印在或在纸上的文字读取出来,并转...
  • 情况二:button频繁点击(特别是触发网络请求),本人公司是金融公司很容易产生两交易的情况。 情况三:button点击时不断轮流切换文字 看效果: 情况:button触发倒计时请移步我的:频繁点击 安卓
  • 个简单有效的手写识别引擎

    千次阅读 2014-07-06 21:48:58
    实现个手写识别引擎有复杂困难?那就要
  • 废话不说,先上效果图 整体来看,效果是非常不错的,模型的训练,参照官方代码mnist_deep.py,准确率是高达99.2% 那么,我是怎么实现的呢? .读懂卷积神经网络代码(至少得把程序跑通) ...
  • 进行测试?

    千次阅读 2017-05-05 21:04:12
    前几天过了两个电话面试,其中有个问题:给你支签字,你要如何测试它。  大白如我,后来才知道,这是个软测的面试老题目了,当时稀里糊涂答了一通,后来才回味过来,其实HR是想看我的测试思想之类的的,...
  • 如何测试支钢笔、电梯、纸杯等

    千次阅读 2013-03-25 16:07:34
    要用大的力气才能来? 长期放着不用,墨水会不会堵住? 加一次墨水能用长时间? 上的标签有没有错别字?是否考虑了globalization,不同国家、不同文化?logo会不会让某种人反感?
  • 毛笔运

    千次阅读 2011-07-04 14:03:46
    毛笔的要求是中锋铺毫,既要让毫在点画中间运行,又要灵活自如。指导学生运时,要一边讲一边示范,注意以下四点。 1.逆锋起笔。指起笔时笔锋要“欲左先右,欲右先左;欲上先下,欲下先上”,做到起笔...
  • 据说美国宇航员在太空很郁闷,失重条件下钢笔和圆珠总是不了。美国科学家花费了X年时间X经费(最近个在农大演讲的我国某专家已经加码到100亿美元)终于研制出能在失重条件下使用的钢笔。而与此同时,苏联...
  •  笔画延长之后还可以做很效果,比如扭转,如上图所示,很外国拉丁字母花体设计,喜欢将最后一笔延长,然后转成很漂亮的花纹,汉字中也有很女性意义的字体设计,也模仿这种手法,塑造女性的感觉,有的可能要...
  • 日常生活或学习中必须手写汉字的机会越来越少,而且,有兴趣学习书法和硬笔书法的人也是在日渐萎缩,因此,现在能够写一手漂亮汉字的90后真的很稀少,就算80后甚至是70后中能够写得一手漂亮汉字的人也不。...
  • 2014年 底,他篇《中国移动应用设计趋势》,引起了大家的广泛关注,时隔一年,他根据中国当今的移动 App UI 趋势,加上自己的新想法,总结出这篇新文章,希望能给移动世界带来一些新进步。▌本文作者:Dan ...
  • OCR 文字特征提取

    千次阅读 2017-03-08 17:58:55
    作为OCR系统的第步,特征提取是希望找出图像中候选的文字区域特征,以便我们在第二步进行文字定位和第三步进行识别. 在这部分内容中,我们集中精力模仿肉眼对图像与汉字的处理过程,在图像的处理和汉字的定位方面...
  • Android自定义View——实现联系人列表字母索引

    万次阅读 多人点赞 2016-11-18 13:53:32
    相信大家对这个列表字母索引已经不陌生了,在很app中也随处可见,像没团的城市地址选择,微信联系人列表,手机通讯录…等等。既然是个这么nb这么实用的功能我们怎么能不Get到来呢,下面就让我们一起造个出来吧...
  • 数字IC小白起步()

    千次阅读 多人点赞 2018-09-27 10:49:16
    然而在后来的工作过程中,我认识了很牛人,也从他们身上学到了很,从中总结了个IC设计工程师需要具备的知识架构,想跟大家分享一下。 I. 技能清单 作为个真正合格的数字IC设计工程师,你永远都需要去不断...
  • 测试用例---铅笔

    千次阅读 2019-06-14 17:08:00
    功能测试 1.铅笔的长度和生产要求是否一致 2.铅笔的宽度是否和生产要求一致 3.铅笔在不同的纸上显色度是否符合要求 4.铅笔的笔芯粗细是否符合...11.在高温的情况下是否可以 12.在低温的情况下是否可以 ...
  • 网页中使用的字体介绍

    万次阅读 2013-08-22 09:25:15
    serif是有衬线字体,意思是在的笔画开始、结束的地方有额外的装饰,而且笔画的粗细会有所不同。相反的,sans serif就没有这些额外的装饰,而且笔画的粗细差不多。 serif字体容易识别,它强调了每个字母笔画的开始...
  •  作为个真正合格的数字IC设计工程师,你永远都需要去不断学习更加先进的知识和技术。因此,这里列出来的技能永远都不会是完整的。我尽量每年都对这个列表进行次更新。如果你觉得这个清单不全面,可以在本
  • 容易进的大厂工作,百度经典百题

    千次阅读 多人点赞 2021-05-24 23:27:44
    容易进大厂的机会就是百度的测试,不服来辩。
  • 推荐给IT项目经理的好书

    万次阅读 2018-03-21 11:46:34
    http://www.cnblogs.com/cbook/archive/2011/01/19/1939060.html(防止原文作者删除、只能拷贝份了)推荐给IT项目经理的好书清理电脑,十数年来,无数资料,近来每天抽空好好整理整理, 做IT的特别是整ERP的,四...
  • 程序员书到底赚钱吗

    千次阅读 2020-03-24 09:22:37
    时隔半年,昨天又收到了出版社一笔稿费,时间很突然,金额也很突然。 年前的时候松哥发了一篇文章,说新书交稿后入手了一台 MacBook Pro(MacBook Pro 入手一年了,到底香不香?),于是有小伙伴问松哥,出书是不是...
  • 实际上字母索引表的效果,可以说在现在的众多APP中使用的非常流行,比如支付宝,微信中的联系人,还有购物,买票的APP中选择全国城市,切换城市的时候,这时候的城市也就是按照个字母索引的顺序来显示,看起来是很...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 45,956
精华内容 18,382
关键字:

容易多写一笔的字