首先要跟各位催更的朋友道个歉。由于年初需要全力准备晋级答辩,所以一切其他不紧急的事务都尽量被我延后了,包括这篇年度总结。今年的客户端组答辩是定在了春节后,所以整个春节我几乎也没有休息,一直在优化和排练我的 slides 。一直拖到答辩结束,这才开始能腾出时间来完成这篇总结。
总的而言,2020 年是非常特殊的一年,对我而言更是如此。这一年有很多的酸楚和困惑,但也迎来了很多新的转机和希望。
特殊的年份要有特殊的总结方式。按照惯例,这次的总结依然会分几个主题来回顾:正经事(工作、开源、博客),不正经事(居家、旅行、游戏、健康、看书、感情)。但因为我再过几天就要去做个近视激光手术,预计术后会有一段时间不能盯着屏幕,而直到现在我的总结都还没赶完。所以为了避免这篇总结被拖到 2022 年,我把这次的总结拆成两篇🐶。
本篇是上部分:正经事部分。
2020 年最让我感慨万千的就是工作部分。因为各方面的原因,在这一年我的职场生活经历了一场大变动,我也因为这场大变动成长了很多。
先说下疫情期间做的事情吧。 因为春节期间疫情加剧,所以课堂在春节期间就打响了 “停课不停学” 的战斗:紧急开发课堂极速版和课堂 iPad 版。作为兄弟团队,我和组里的另外两名高工也参与了这一场战役,现学现卖 Flutter ,协同完成了课堂 iPad 版第一个版本的开发上线。
那段期间,虽然我们是在家办公,省去了很多通勤时间,但是因为时间很紧(iPad 版半个月内上线,开发时间只有不到一周),所以我们每天几乎都过着接近通宵的爆肝生活。
近半个月的奋战虽然艰苦,但现在回想起来,却又多了一种革命情感:每个人都觉得自己正在做的事情充满了意义。所以虽然很累,但是大家一起咬着牙坚持到了最后。
也因为那段期间的付出,我们一起拿下了公司级的 “卓越研发奖”、“腾讯战役特别贡献奖”、“腾讯‘同心战役’特别贡献奖” 等多个奖项。附图 1 “同心战役”纪念企鹅
疫情固然严峻,但在那段期间,组里的成员的流失给我带来了更大的打击。 在 3 月份,我们组立马有 4 个成员因为各种原因提出了离职或者转岗。还有一个同事因为家里发生了一些事情,所以休了两个月。在最困难的时候,我们组从年前的 10 个人缩减到了只剩 5 个人,其中 iOS 开发更是一个都不剩。
坦白来说,自从工作以来,这是最让我感到沮丧难过的一段时间,不仅惊慌失措,还深深陷入了自我的怀疑和否定中。这段期间我甚至因为焦虑而开始失眠。为了对抗焦虑,我脑海里不断回想起卡耐基的《人性的优点》里的办法:
也在 jolt 的建议下,我诚恳地跟 4 个成员进行了交谈,了解了他们想离开的真正原因。通过与他们的交谈,我也意识到了自己在管理上依然存在一些不足:
事情既然已经无可避免地发生,我就做好了心理上的准备。当务之急就是做好交接工作,并且在这些人正式离开之前招到人。于是我为每个人安排好具体的交接事务和时间点,并且把我几乎所有其他空余时间都用在了捞简历和面试上。让人崩溃的是,那段期间正好碰上我们正在从 Cocos Creator 1.10 升级到 Cocos Creator 2.2,升级后遇到了非常多的兼容性 bug ,而且在系统测试期间各种新发现的 bug 层出不穷。在一个个成员相继离开的情况下,剩下的人要顶着越来越大的压力去跟进修复新增问题,整个项目是否能如期上线充满了不确定性;另外,因为疫情的原因,招聘在这个时候也充满了各种变数:有因为疫情家人不让来深圳发展所以放弃 offer 的,有因为疫情上家公司一直拖着没法办理离职导致无法尽快入职的,有技术面表现优秀但卡在背调没过的,有拿了 offer 后被其他公司截胡的 …… 回想起来,那段期间我的生活几乎像是笼罩着一层雾霾。
不管怎么样日子还是得过下去。因为思维的项目马上要开展了,所以为了解决 iOS 人员紧缺的问题,我从企鹅辅导团队里借了 jaelin 过来支援,拉上 galio 一起研究起了 Cocos + Unity 混合开发的可行性。在最困难的时候,我也承担起了修复 iOS 端 crash 的任务。直到 ez 的加入,慢慢接手了项目里的 iOS 模块,我们才总算度过了没有 iOS 开发的难关 11后来在年底企鹅辅导也在上线使用 Cocos Creator 开发的 6 人小班课功能的时候遇到了一些技术问题,我们反过来也帮助了辅导解决了这些问题 ,颇有一种报恩的感觉 😄 。
当然,这样的惨痛经历从长远来看未必是一件坏事,它让我深刻意识到了我在管理方面还存在哪些阿克琉斯之踵,然后痛定思痛去改善这些缺陷。之后我在管理方面做了很大调整:
OKR 制定和绩效结果沟通是每一个考核周期的开头和结尾的两件大事。只有 OKR 制定和绩效结果沟通两个都做好了,组员才能感觉到被重视。这在很大幅度上能减少人员流失的情况。
在 OKR 制定上,除了要能为组员定 “任务” 之外,要充分考虑 ta 的职级发展,当前的工作是否有意义,是否有足够挑战。因此从这一年开始,我会先制定整个团队的 OKR 初稿并公开给全组的人,然后让每个组员在这份初稿的基础上,自己去思考能参与其中哪些事务,自己还想做哪些事情。然后我再组织 1v1 面谈。在面谈的时候,我会重点关注 “产出” 和 “挑战” 两大方面,一方面希望组员能保证有足够的产出,另一方面也希望每个组员的工作也有一些挑战性,这有助于他们的职业发展。
在绩效考核上,这一年我也下了很大一番心思去调整。以往的绩效考核比较依赖主观评定,虽然 leader 对孰优孰劣已经有一把秤,但这个感受对于组员而言是信息不对称的。所以我开始尝试用更加量化的方式来评定每个人的绩效表现。具体而言,我将每个组员的绩效按照需求分(分数占比80%)、全面反馈分以及 leader 考虑因素(是否做出技术突破,是否要申请晋升,人员梯队,当期是否有工作上的重大失误酌情给分)来考量。其中,需求分又围绕需求价值、贡献度、完成质量,并结合工作量、难度、效率来综合计分。虽然这样打分非常地累,但是这样的量化调整,可以在团队里释放一个信号:大家是公开平等地接受同一个标准进行考核;同时在分数评定完后,还可以清晰地看出每个人在这次考核周期里的亮点和不足,并以此作为绩效沟通的内容。目前来看,组员们对这样的考评方式接受度挺高。
在这一年我所做的另一大改变,就是在处理一些任务的时候,也多向组员请教,或者将权利下放给更适合的人。
在具体落实某个工作的时候,我会更加积极地问自己两个问题:“这件事情怎么才能做得更好?有没有能把这件事做得更好的人?”。
比如,在推动团队申请专利和双周技术分享制度建设方面,我授权给了 driver 来组织,而我需要做的事情是在经费等资源方面尽力给到支持。再比如,团建则交给了 yujie 和 cool 来组织,我则尽力避免做出任何决策。具体执行下来,发现效果比我预想的还要更好,大家也都非常满意。
与组员的 1v1 沟通也成为我非常重视的环节。尤其是新人刚来的前三个月,一旦新人收到要跟上级沟通的邮件,我都会非常乐意约他去喝杯咖啡聊聊他的工作感受甚至生活近况。
说到倾听,去年参与了公司组织的一个培训《教练式领导》,这个课程提倡使用 GROW 模型来展开辅导:
这种理念给我带来了比较大的启发:一直以来我比较喜欢深入到方案细节,对组员提出的方案指点较多。但实际上这并不是好的授权:责任又落回到了我身上。不仅限制了他们的发挥,我也会很累。所以在这一年我在许多方案的决策上,我也尽力使用教练的方式来辅导,让组员能够更大地发挥他的才能。
技术氛围建设也是去年我在团队管理中重点投入的部分。
前面提到我把专利事务和双周技术分享制度授权给了 driver 来组织。以双周分享制度为例,在征求大家的同意的基础上,driver 制定了一个半强制性的分享制度:要提晋升,需要有 1 到 2 次技术分享。同时,每次分享都会提供零食和饮料,以确保大家有充足的能量可以聆听。
从去年下半年开始施行至今,我们已经进行了 12 次分享。分享的主题涵盖了工作中用到的 Cocos Creator 开发、Native 端通用技术、Git、GPU 编程等,也有个人创业、游学经历、专利撰写、脑机开发等等方向的心得体会,题材五花八门。
除了内部分享,去年我也在思考怎么加强和外部团队的联系和交流。因为公司里也有其他不少使用 Cocos Creator 开发的团队,所以我建了个内部企微群,把这些团队里的人拉到了大群里互相交流。后来我转念一想也许可以把大家拉到一起做点事情,因为既然大家都用 Cocos Creator 进行开发,难免会遇到很多共性的问题。所以,与其烟囱式踩坑,还不如中台化,我们做一套可以给多个科目、业务复用的架构,这样就能提高每个科目或者业务的质量和效率。于是,我拉上群里的大多数人搞了个线下面基,号召大家一起成立了一个 oteam ,一起做一套 Cocos Creator 的通用组件库 ABCKit 。现在这个 oteam 已经进入孵化阶段,而且第一个版本已经在公司内部开源。
在这一年我们跟 Cocos 社区也有更加密切的互动。先是 BigBear 过来我们这做了一次技术交流;之后我也以 Cocos 社区 KOL 的身份参与了 Cocos 十周年的庆祝活动,并在 Cocos 开发者沙龙上分享了我们所做的基于 Cocos Creator 和 ABCKit 的高性能、高效能的在线教育应用解决方案。
跟 Cocos 社区的互动带来的一大好处是拓宽了团队的技术视野,能够更加清楚整个业界大家都在做什么,都有哪些大佬,最近又有哪些值得关注的技术。
以上就是去年一年下来我在工作上比较大的一些感悟。现在我的团队已经从最困难的时候的 5 人小组发展成了一支拥有 16 个正式员工的大军。在这 16 个人中,高工占了将近一半;另外还有另外三位 Cocos 社区 KOL 加入了我们团队,战斗力爆表。我也终于在工作生涯的这第 6 年获得了正式任命。现在回想起来,颇有一种 “拨云见日” 的感觉。
Bruce Tuckman 在 1965 年将团队发展模式归为 4 个阶段:
目前来看,我的团队在去年已经经历了风暴期和规范期两个阶段。今年预计将会进入表现期,值得期待。
今年主要参与了几个项目:
项目名 | 简介 | 去年的工作 |
---|---|---|
rhubarb-lip-sync-ccc | 一款专用于 Cocos Creator 的嘴型动画生成插件,它可以根据一段语音生成嘴型动画的 Animation Clip 。适合用于制作游戏角色的说话动画。 | 新开源 |
FedML | 主要用于学术研究的联邦学习(Federated Learning)库 | 新开源 |
Lip Sync 技术是指根据语音生成嘴型动画的一种技术,通常用在游戏或者动漫的角色制作上。与 Unity 上有很多 Lip Sync 的插件的情况相比,Cocos Creator 生态中似乎还缺少 Lip Sync 的方案。所以我在 DanielSWolf /rhubarb-lip-sync 的基础上添加了对自动生成 Cocos Creator 的 Animation Clip 的支持,为 Cocos Creator 社区贡献了第一个 Lip Sync 插件 rhubarb-lip-sync-ccc 。
去年 7 月份的时候 Chaoyang 找上我,希望我能够参与到他的 FedML 项目中,帮助他一起推广和维护这套联邦学习库。在了解了联邦学习的特点后,我觉得它将会是未来一个非常重要的机器学习技术方向,所以接受了他的邀请,在业余时间参与了 FedML 的一些维护工作。
因为那段期间也是我非常忙碌的时候,所以实际上我的贡献并不多,只是帮忙从规范化运营开源项目上给了一些建议和支持。例如帮忙搭建了文档站点、选用 License、设置代码质量 CI 以及设置捐赠信息(我也给这个项目捐赠了 25 刀 🎅 )。
整个 2020 年基本没发过博客,我已经可以预计这个博客站点将会彻底沦落为我的年度总结站点……至于去年提到的写书计划,也因为去年的人力变动以及 Cocos Creator 版本升级被暂时搁置了。我已经开始跟出版社取得了联系,等 ABCKit 对外开源后,我们就会重启这个写书计划。希望最迟在明年我们就能够完成这个小梦想。
关于博客还有一个非常让我不满意的事情,就是 hexo server 在文章数量多了之后渲染速度非常的慢,这还是在我已经把 cache
模式打开的情况下。每次改一点东西,等站点重新 render 要接近 20 秒的时间。这大幅影响了我写作时候的流畅感(嗯,这也是我一直不发总结的借口🐶)。我需要在这篇总结完成后去优化一下这糟糕的体验。
(下篇待续)
]]>rhubarb-lip-sync-ccc 基于 DanielSWolf /rhubarb-lip-sync,在他的基础上添加了对自动生成 Cocos Creator 的 Animation Clip 的支持。
https://github.com/wzpan/rhubarb-lip-sync-ccc
完成以上四步之后,点击 【生成】 按钮,即开始生成动画。可以打开 Cocos Creator 的控制台面板,查看控制台日志:
1 | isEnglish: true |
如果出现 “生成成功!” 的消息,说明生成已经成功。此时可以将该 Animation Clip 拖动到嘴巴节点的 Animation 组件上,看看效果。
温馨提示:如果发现虽然动画生成成功,但是该动画没有任何帧,可以重启 Cocos Creator 看看问题是否解决。
如果使用上遇到问题,请在 项目 仓库上提 issue 反馈。建议带上你的测试工程,方便我定位问题。
]]>2019 年对我而言,最大的意义是实现了几个重要的小目标。在这一年,我搬进了自己住的房子,紧接着又幸运地摇到号订了车。在工作中,我渐渐地胜任了管理角色。另外,我还达成了一个公益小目标:利用开源项目筹集并捐出一万元善款。当然还有一些不尽如人意的部分,比如这一年过得更宅了。
按照惯例,今年的总结依然会分几个主题来回顾:正经事(工作、开源、博客、授课),不正经事(居家、旅行、游戏、健康、看书、感情)。
如我在去年的总结中所述,从去年下半年开始,我开始挑战团队管理的工作。这里用了 “挑战” 这个词,是因为我一直对自己的管理能力并没有非常大的信心——我很怕自己没有尽到带好团队的责任,倘若真不幸如此,“一将无能,累死三军”,团队里每个成员的发展都将受我影响。这是我最不愿看到的事情,甚至比自己背个差的绩效还可怕。
幸运的是,公司有非常完善的基干培养方案以及足够长时间的过渡期,在这段足够长的时间里我可以慢慢学会从一个普通员工转变成一个相对合格的基干。在今年年中我就参加了包括公司的潜龙计划在内的几个关于管理的培训;此外,今年开始我们用上了 OKR 目标管理,也使得整个团队的进度把控和目标跟踪清晰了很多;再加上 jolt 的密切关注和 star、jack 两位同我一样同为团队里的 “实习” leader 的相互支持,一年半载下来自己也切实感受到自己在这方面开始有了一些质的变化。
先说说管理培训方面的收获。今年的培训给我印象最深刻的内容是 PDP 行为风格(Professional Dyna-Metric Programs)的概念。PDP 把人的性格分成了老虎型、孔雀型、无尾熊型、猫头鹰型、变色龙型等五种类型[1]。附图 1 陈振平老师的《登上成功管理的舞台》课后留念。
相比我在去年所介绍的四象限领导模型,PDP 行为风格的优点在于为与不同行为风格的人相处合作方式提供了简单有效的参考。
行为类型 | 向上沟通要点 | 向下沟通要点 |
---|---|---|
老虎型 | 1. 迅速反应 2. 表现能力 3. 独立作业 4. 直截了当 | 1. 表现自信 2. 提供自主机会 3. 奖励成果 4. 指出确定范围 5. 倾听,但有决断力 6. 在平等基础上争论 |
孔雀型 | 1. 善于交际 2. 圆熟事故 3. 处事有机巧 4. 有感染力 | 1. 友善亲切 2. 信息丰富,见闻广博 3. 给予有助益的响应 4. 表示了解 5. 加以鼓励 6. 具有弹性 7. 展现幽默感 |
考拉型 | 1. 表达忠诚 2. 态度真诚 3. 以团队为重 | 1. 给予肯定、信任与赞赏 2. 互相设定目标 3. 容易接近 4. 试着分享 5. 可以被依赖 6. 行事公平 7. 展现价值 |
猫头鹰型 | 1. 态度尊敬 2. 遵循规律 3. 讲求逻辑 4. 专心一致 | 1. 条理分明 2. 目的清楚 3. 重视细节 4. 具有系统 5. 客观合理 6. 受前后一致 |
作为一只 “老虎”,沟通方面一直不是我的强项。PDP 确实帮助我在处理日常沟通的时候至少变得有章可循,懂得了与不同风格的人日常交流要注意哪些要点。除了学习了沟通方面的技巧,我也借着这几次培训机会省视了一下自己在日常工作中的一些毛病。
当然管理对我而言始终是永远学不完的学问,有很多技巧我在实际工作中尚未能应用自如,只能在不断摸索和反省中要求自己改进。总的来说,在团队管理方面,希望明年自己能够在以下方面有更好的表现:
除了管理方面的挑战,这一年中,我还深刻感受到了其他方面的一些压力和挑战。其中最大的一项挑战来自于协调目标的困难。当一个任务,一方坚持要做,一方坚持不做的时候,这时候项目的执行者就会置身于冲突之中。在这一年中,我也有过一段痛苦的迷茫期,不知道自己该做些什么。我开始阅读起一本关于如何做决策的书,来自桥水基金公司创始人 Ray Dalio 的《原则》。
在这本书里,Ray Dalio 提出了一种称为创意择优的决策模型。这个模型包含三个部分:
其中,1 和 2 是进行可信度加权投票的前提。而 3 则要求对每个参与投票的人进行预先的可信度评估,但这个可信度评估制度需要自上而下推动落实,而且这种评估制度如果没有坚持效果跟进,很容易演变成形式化的流程。也正因为如此,要把它在团队中照搬落实几乎不可能。不过,极度开放、极度求真和透明在任何时刻都是应该鼓励的。后来我们就极度求真而透明地开展了一次关于如何处理分歧的讨论会,决定了后面执行需求预审制,有冲突再上周会讨论征求各方意见的方案。
另外,从下半年开始,我们用上了一个目标管理利器:OKR。OKR(Objectives and Key Results)即目标与关键成果法,是一套明确和跟踪目标及其完成情况的管理工具和方法,由英特尔公司创始人安迪·葛洛夫(Andy Grove)发明。有了 OKR 后,产品、开发、设计、测试等各个组的整体目标就能保持一致,大家能够保证在同样的目标前提下进行任务的决策,一个任务如果对实现重点目标有利,顺理成章也就能得到更高的优先级。附图 3 OKR = 目标+关键结果
OKR 也使得我们日常的工作开展更为清晰。比如,我们会在每周定期维护一个 OKR 的脑图,详细说明每项 KR 相关的工作进展,这样就能非常全面地了解和预估每一个目标的达成情况。今年在 OKR 方面的尝试还只是停留在团队的粒度,明年我想再进一步尝试往下细化:具体跟踪到团队里每个人每周的 OKR 情况。
今年主要开源了几个新项目:
项目名 | 简介 | 今年的工作 |
---|---|---|
wukong-robot | 一个简单、灵活、优雅的中文语音对话机器人/智能音箱项目 | 新开源 |
wukong-contrib | wukong-robot 的插件集 | 新开源 |
wukong-itchat | wukong-robot 的微信客户端 | 新开源 |
wukong-starter | wukong-robot 的课程版基本环境安装脚本 | 新开源 |
python-muse | Muse 头环的 Python 开发脚手架项目 | 新开源 |
MuseFlappyBird | 使用 Muse 头环玩 FlappyBird | 新开源 |
hexo-tag-bootstrap | hexo 的 twitter bootstrap tag 插件 | label 插件支持 markdown 渲染 |
在今年春节期间,我开始对 dingdang-robot 项目进行重写,做出一个更加 优雅、灵活、鲁棒 的版本。为了区别于以前的版本,我决定起给这个新版本起一个新的名字 “孙悟空” 。于是,正月初五那天,wukong-robot 1.0 正式发布了。
相比它的老大哥 dingdang-robot ,wukong-robot 有着更加迷人的特性:
以下是一段 wukong-robot 的定制版本 ycy-robot 的演示视频: 附图 4 ycy-robot ,是我一时技痒参加的杨超越编程大赛的参赛作品。基于 wukong-robot 定制了一些月芽专属技能,以及配套了佩奇粉后台管理端皮肤和“超超越越”唤醒词。
除了 wukong-robot 本体之外,我还给它开发了其他几个配套项目。包括第三方技能插件仓库 wukong-contrib,微信客户端项目 wukong-itchat ,wukong-robot 的树莓派 docker 安装工具 wukong-robot-pi-installer,配套 Python 开发教程基本环境安装脚本 wukong-starter 等。
wukong-robot 发布至今,已经在超过 4,100 台设备上安装运行,总唤醒次数超过了 320,000 次。只用了不到一年的时间,wukong-robot 项目的 star 数就已经后来居上,反超了它的大哥 dingdang-robot 。
关于 wukong-robot ,还有一个值得分享的事情。在去年 6 月份的时候立过一个 Flag :当我的机器人项目群的入群费用达到一万元时我将全部捐出给深圳壹基金等公益机构。到了今年的圣诞节期间,我的小目标终于达成了。
通过 QQ 上的腾讯公益平台,我一共给 16 个公益项目提供了捐赠,其中包括了 11 个壹基金相关项目,两个重症儿童项目,一对一帮扶了两名藏区儿童,等等。
附图 5 《未来简史》里认为未来人来三大命题是“长生不死”、“幸福快乐”、“化身为神”。
业余时间除了开发维护 wukong-robot 之外,我对脑机技术也有了非常浓厚的兴趣——一方面我认为这项技术会是未来最重要的一种人机交互技术,甚至还可能和实现长生不死相关;另一方面是因为今年在工作中开始遇到了一些小我十岁的实习生,让我不禁感叹“逝者如斯夫,不舍昼夜”。唯一能和时间赛跑的只有技术了。于是我买了一个 Muse 头环,开始折腾起了脑机实验。
用了两天我就写出了几个 IoT 应用。分别使用眨眼、咬牙、关注度来打开家里的灯。
之后我对我的智能音箱项目 wukong-robot 进行了改造,支持了 Muse 脑机唤醒:
脑控游戏也是一个有趣的应用方向。例如我魔改的一个眨眼控制的 FlappyBird 游戏 MuseFlappyBird 。
如下是一个稍微粗糙一点的早期版本演示视频:
在折腾 Muse 的过程中,我顺便开源了一个 Muse 的 Python 开发脚手架 python-muse 。详细开发流程及使用方法可以阅读我的上一篇博文 脑机接口概述及Muse头环Python开发基础。
玩了几天 Muse 后,我就已经把目前 Muse 所提供的现成能力都“榨干”了。后面就需要开始往更加困难的目标前进:真正识别意图。但如 Muse Direct 所述:
It is important to note that this raw data can be difficult to interpret and you may want to consider getting in touch with an experienced EEG Researcher for your project as we are unable to provide further support with the analysis and processing of the EEG data you collect.
仅靠 Muse 所提供的几个通道的 EEG 要准确识别出意图是非常困难的。不过,当我凝视深渊的时候,深渊不也正在凝视着我吗?
我与脑机的故事,才刚刚开始。
说来惭愧,博客在今年依然处于荒废状态:只写了两篇博客。后面该不出现一年只剩一篇总结吧?
其实今年之所以发这么少,有一个原因是我一直用的 Hexo 2.8.3 突然在某一天出现了兼容问题。
1 | (base) ➜ hexo-blog git:(master) ✗ npm start |
网上搜了很多方法,包括安装 natives,降级 node 版本,都不能解决问题。就这样放了好多时日,一直到了年底,我终于意识到再不折腾下自己的博客,连年度总结也没法写了,于是决定大动刀斧,升级 Hexo 到最新版本。
其实早在 15 年,我就尝试过将博客升级到 Hexo 3.x 。但如我在 Dockerize Your Hexo 文章中所述,我因为遇到了很多解决不了的问题而搁置了这个计划,遂将我的博客所使用的环境 “冻结” 在 2.8.3 。但 4 年过去,我的 hacking 能力又有了一些长进,再加上这次已经没有退路了,所以我终于耐着性子把原来遇到的问题都解决了(deadline 果然才是第一生产力啊)。另外还增加了 pjax 优化,还给博客加了个切换主题的功能,整体效果甚至比原来的版本更好了。
另一个我所做的关于博客的事情,是在今年 5 月份搭建了团队博客 。让其成为我们团队对外技术输出的标签。半年以来,我们一共在上面发了 12 篇文章,大多是跟 Cocos Creator 开发相关的技术。其中,xepher 的 《拒绝 evalString 进行回调,使用 JSB 进行手动绑定(流程篇)》以及 kevin 的 《Cocos Creater 中如何实现 JSB 的自动绑定》还被收录进了 Cocos Creator 官方文档中。
虽然这个博客站点说不上高产,但从 5 月份以来,总 PV 已经超过了 1w 。到了下半年,我们决定开始起草一本关于 Cocos Creator 开发实践的书籍,将我们在 Cocos Creator 开发过程中的一些经验总结成文。目前我们已经完成了约 30% 的内容。希望明年上半年能够完成并与读者们见面。
今年年初,jeep 找上我,希望我能在腾讯课堂开一门 Python 开发的课程。恰好那个时候我正有开发 wukong-robot 的打算,于是一拍即合,决定顺便开发一套《Python从入门到实战》的 Python 课程,并将 wukong-robot 的开发过程作为其中的一个项目案例。
附图 6 为了更好的录音质量,下血本买了套罗技G433降噪耳麦+海盗船声卡耳机架。这门课的准备和录制几乎占据了我上半年全部的业余时间,我每天的常态就是一回到家就录制到深夜。整个过程是非常痛苦和煎熬的。比如,为了讲好 subprocess ,我把 subprocess 的老版本高级 API 、新的高级 API,再到底层的 Popen 以及涉及到的 Linux 的标准输入输出和管道的概念都讲了一遍。对于讲授的方式,我比较提倡授人以鱼不如授人以渔的主张,所以我并不是直接贴 API ,而是带着读者一起看 Python 的官方文档,着重培养阅读文档的能力。这种讲法非常的累,但却是我认为每个工程师应该掌握的学习方式。
附图 7 年底的时候腾讯课堂送来的纪念礼物
参与这门课的制作也是为了完成我在去年的个人总结中立下的 flag 。Python 一直是我业余时间最常用的玩具语言,它非常适合用于原型开发。我有不少开源项目,比如 wukong-robot、dingdang-robot、LiveCV 都是用 Python 写的。而在我的工作中,它也帮助我完成了大量的工具和项目,这些工具和项目对个人或团队起到了非常大的作用(例如加班统计平台、已经在上百家中小银行中使用的fmanager),因此 Python 也无疑给我的职业发展起到了很大的推动作用。把我所掌握的 Python 知识分享给更多人,让更多人能够自如的使用这门语言来满足他们的需求,那也算是我对 Python 这门语言的回馈。
除了腾讯课堂的课程外,今年10月份我还接受了大学同学东跃(现在该叫方老师了)的邀请在他的课程中客串了一把,为东莞中学的学生们科普了一下模式识别。
说到今年最幸福的时刻之一,就是从租的房子搬进了自己的房子,生活质量一下子提高了不少。
因为是自己的房子,就可以开始折腾起智能家居。在这之前,我经常在 robot 用户群里看到很多用户晒他们用 dingdang-robot / wukong-robot 联动智能家居的视频,心里着实非常羡慕。现在终于我也可以跟他们一样玩起装备了。
于是我先买了一堆米家装置:
米家智能家电的不足是无法灵活地获取设备的当前状态作为环境条件。举个例子,我希望浴室的排气扇在关浴室门超过两分钟后自动打开,而米家的智能选项里是没有米家门窗传感器关闭一段时间的条件的。于是我又趁着双十一组了套群晖 DS218+ ,在上面装了个 HomeAssistant ,把大部分能接入的设备都接了进去。附图 8 群晖DS218+
不得不赞一下群晖的可玩性真的很高,除了提供非常丰富的套件,还支持跑 docker ,在上面搭 HomeAssistant 、Gitlab 都非常方便。
接入了 HomeAssistant 后,好多原来米家满足不了的需求就可以实现了。例如上面提到的自动开关排气扇的需求就可以这么实现:
1 | - id: 'extractor_fan_auto_on' |
还有一个很好玩的家电:米家扫地机器人。配合 Flovac 可以将划区的位置信息导出出来:
这样就可以很方便地将屋里的不同分区保存下来,写成 HomeAssistant 脚本。例如:
1 | vaccum_living_room: |
再搭配 wukong-robot 的 HASS 插件,就可以实现让 wukong-robot 来指挥机器人打扫屋里的指定区域。如下是一个示例视频(使用的是 Raspberry Pi Zero 主板,所以响应速度比较慢):
有一天我还突发奇想,把一块吃灰的 Kindle 给 hack 成了智能家居控制入口:
还有一个好笑的事情,圣诞节那天凌晨 6 点 Neal 从 LA 给我发来节日问候,没想到我的秒回把他吓到了。其实那个时候我正在拆家里的墙壁开关,看看里头有没有零线。大概我在朋友们眼中已经成为了下面这样的外星人😹:
因为要买车,今年在旅游上的预算比较有限。一年下来全是省内游:
说走就走的云顶山温泉。实际证明不自驾的话又贵又 adventurous 。去的路线没有确定好,到了南昆山发现原来云顶山在另一个镇。而山里头没有滴滴,打车只能靠刷脸(这个黑历史已经成了 Neal 的话柄😂)虽然行程很曲折,不过却意外的吃到了一顿巨好吃的农家菜,以至于到了永汉反而找不到能比得过的餐馆。也许我会在将来不久自驾回南昆山,开展一次回味之旅。
顺德和外伶仃岛是今年团队的团建地点。在顺德没什么好说的,就是一个字:吃。
顺德菜的确非常符合我这个广东人的口味。特别推荐顺德特色的陈村粉,有点像肠粉,但更入味一些。
为了怕在顺德行程太过无聊,我们还借了两台 switch 。玩聚会游戏就成了我们的室内活动。
外伶仃岛则是我们行程的第二站。在岛上就是玩一些比较常规的海岛活动:看日落、骑自行车、吃海鲜。附图 8 恰逢软件自由日,在岛上穿件节日T恤做个纪念。
看日落算是零成本活动,没什么可说的,挑对时机就好。
说说骑自行车,租自行车的时候店家可能会告诉你多交一点钱可以加到两小时。这个真的没必要多交。因为外伶仃岛的面积不大,不到半小时就没地方可骑了。
再谈谈吃的部分。岛上有海鲜市场,可以挑选海鲜然后选一家餐厅加工。但实际上这里头也有不少猫腻:称海鲜用的秤盘往往非常的沉,如果你不注意就会被诓。餐厅的加工质量也参差不齐,吃得让我直皱眉头。相比之下,反而是使用自家食材的食德乐餐厅是我在岛上吃的唯一比较合口味的一家餐厅。
今年终于入了索尼大法的坑,搬入新房子后我就立马买了套种草已久的 PlayStation 4 和 VR 头盔。这样我的 Macbook 就可以完全用于工作,不再用于游戏。
因为曾经在 VR 体验馆玩过一点 VR 游戏,所以 VR 游戏对我有很强的吸引力。于是到手后就立马买了《噢!我好神 VR(O! My Genesis VR)》和《上古卷轴 5 VR》(又称为老滚 5 VR)。
先说说 《噢!我好神》,这是台湾同胞开发的一款 VR 游戏,玩家可以在虚拟的宇宙世界中,扮演拥有多种神力的宇宙创世神,用自己的双手,陪同星球上的小生物一同发展星球。虽说这个作品的内容丰富度比不上老滚这样的 3A 大作,但是 VR 效果非常出色。
这个游戏的 VR 效果之所以出色,是因为游戏很好地解决了视角问题。当你戴上头盔后,整个星球是以立体的形式出现在你的眼前。你可以通过拖拽来旋转星球,而不需要考虑人物走动的情况,也就减轻了眩晕感(不过戴久了还是会晕)。
相比之下,老滚 5 VR 版在处理人物移动和视角旋转的时候,是需要通过按左右两个 PS Move 手柄的 X
键和 O
键来控制的。这并不符合人的直觉,所以玩没多久就会很难受。
不过我买老滚 5 VR 更多是出于一种情怀,毕竟它是我去年玩过的最印象深刻的游戏。况且老滚 5 的 VR 版本也有过人之处:最好玩的地方莫过于躲在一个角落,然后偷偷探出头来给敌人放暗箭。这是普通版本所无法感受到的乐趣。
除了这两款 VR 游戏之外,我还买了《PlayStation VR 世界》和《厨房》。《PlayStation VR 世界》是索尼出品的范例级别的 VR 游戏,里头包含了五个不同类型的 VR 游戏。既然是范例级别的游戏,VR 效果自然非常出色。例如在 《The London Heist(伦敦劫案)》这个游戏中,玩家需要扮演一个钻石大盗。其中一个抽雪茄的场景里,用户吐气的时候,在游戏场景中真的可以看到冒出来的烟雾。
再比如《Into The Deep(深海惊魂)》中,当戴着头盔在海底看各种奇特的景观的时候,真的会有戴着潜水镜的错觉。
至于《厨房》……一开始听说是一个恐怖游戏,而且才卖 8 块钱果断入了。后来我才发现这游戏只有五分钟的时长,而且只是作为生化危机 7 的一个预告性质的游戏。玩家在里头扮演一个被绑着的可怜人,除了坐以待毙,其他什么都做不了。游戏的恐怖指数也是令人发指,我一个人玩的时候不到一分钟就被吓出一身汗,卸甲投降。这游戏只能在人多的时候拿来给不怕死的朋友尝试。而最大的乐趣应该也是看着这位朋友被吓到尖叫吧?😝
以上介绍的都是 VR 类的游戏。总的而言 PlayStation VR 游戏更适合用于体验,而不适合长久地玩,因为用不了多久就晕的不行了。所以这些游戏的篇幅基本都非常的短,一会儿就可以通关(当然沙盒游戏代表老滚 5 VR 是个例外,但我也只玩到拿回黄金龙爪就放弃了)。今年真正说得上 “细品” 的游戏,是 《底特律:成为人类》和《巫师3》。
《底特律:成为人类》是一个多分支互动电影游戏,故事围绕着 3 个人形机器人,分别是为了探寻自己新产生的人工意识而逃离雇主的卡拉(Kara),追捕异常仿生人的康纳(Connor)和致力解救被奴役的仿生人的马库斯(Markus)。玩家所做的决择会决定 3 位角色的生死存亡。
游戏的剧情大致是人工智能觉醒后,机器人如何争取自己的权利和自由。随着剧情的发展,这三个不同的主角会最后走到一起,甚至还可能并肩作战,为机器人争取人权。这样的故事其实挺老套了,但作为一个技术nerd,这样的题材总是能够引起我的共鸣,更不用说是置身在这样的游戏世界中了。最出乎我意料的是突然有一天游戏首页的 Chole 妹子突然问我是否在关注脑机技术,着实让我惊艳了一把。
说回游戏玩法,《底特律:成为人类》是个多分支互动游戏,玩家会面临非常多的选择;而每个选择,包括主动探索获取的情报多少,都可能会影响到剧情的走向。
不过目前我只玩出了一个 happy ending ,就没有再去尝试其他的选择和结局。我向来不追求玩遍所有分支,即使是我最喜欢的游戏《武林群侠传》,我也没有试图刷遍全部线路和结局的欲望。一方面是因为一周目的剧情已经先入为主了,再去玩其他分支的时候总会有 “毁三观” 的感受;另一方面是时间太宝贵了。游戏对我而言,更多的作用只是利用很小一部分业余时间,去体验一下不一样的虚拟人生而已。
最后再说一下《巫师 3》,其实在去年我就已经买了《巫师 3》的 steam 版,但一直存在 Library 里没有安装,因为 Macbook 并不是一个合适的游戏电脑。到了今年,趁着 PS4 游戏大促,又花了 99 块钱入了《巫师 3》的 PS4 版本。
仔细品玩下来,《巫师 3》的确名不虚传是一个非常精彩的 3A 大作。相比去年花了我 60 个小时的老滚 5,《巫师 3》在剧情和打斗场景方面显得更加精致,人物形象也更加鲜明。巧舌如簧的丹德里恩、铁石心肠的拉多维德五世、铁汉柔情的血腥男爵、……即使是配角都给人留下非常深刻的印象。完全不像老滚 5 那样,清一色全是一群没有个性的脸谱化NPC。
“两害相权取其轻” 这句话在游戏中频繁出现,作为同样具有多分支元素的游戏,玩家经常需要在游戏中面临选择。而每种选择都有如蝴蝶效应一般,总需要为自己的选择付出或轻或重的代价。
有些选择甚至让人过后细思极恐,例如在洋特拉村的村口会遇到一群村民求助,希望能帮忙抵御以小红帽为首的强盗的攻击。如果答应帮忙村民,就会发现小红帽变成了狼人(这是什么毁童年的暗黑剧情👿),将其击杀后,从她身上的信件中又会发现她其实拥有一个非常可怜的身世,而她过来报复村民也只是因为这些村民曾经背叛过她。
和老滚 5 类似的是,这同样是一个需要花费非常多时间的游戏。我同样已经在上面花了 62 个小时,但是才玩到了史凯利杰群岛,远远看不到边。感觉明年还可以玩一整年😄。
要说游戏中让人跳戏的元素,莫过于昆特牌的元素了。 感受一下下面这段对话:附图 9 在《古剑奇谭三》也有类似的牌类游戏元素,应该就是借鉴了《巫师 3》中的设计。
血腥男爵:我的老婆和女儿一个被怪物抓走,一个加入邪教,你快点把她们找回来啊!
杰洛特:来盘昆特牌吧!
血腥男爵:好!
这样的带有反差萌属性的对话在游戏中多次出现,以至于 “来盘昆特牌吧!” 变成了巫师 3 玩家群体中盛行的一个梗。
话说回来,这个棋牌游戏确实非常好玩,以至于很多人反而把收集牌当成了主线剧情😂。
去年我定了个目标,要把平均睡眠提高到日均 6 小时以上。为此我又做了一些努力:
有了三个调整后,我的年度平均睡眠时长果然从去年的 5.4 小时增加到了 6.4 小时。
不过我的入睡时间还是太晚,这带来的问题是如果第二天中午没有午睡,下午就非常崩溃。希望明年能够提前到一点前入睡。
除了睡眠问题之外,其他一些健康问题也需要引起重视。今年我在体检中还查出了窦性心动过缓和血压偏低的小毛病,这和我平时较少运动和不怎么吃早餐有关。希望明年能恢复每日基本的锻炼,并保持吃早餐。
今年读了九本书。除了前面提到的《原则》之外,其他的都是网综《一本好书》第一季中所推荐的书单。
同样按照喜欢程度从高到低的顺序往下评论:
万历十五年:很喜欢看这种以历史人物为视角的书。这本书在明万历十五年这个时间点上进行着墨,万历十五年,即公元1587年,在中国历史上原本是极其普通的年份。作者以该年前后的历史事件及生活在那个时代的人物,包括万历皇帝、首辅申时行、张居正、海瑞、戚继光、李贽等人为视角,抽丝剥茧,梳理了中国传统社会管理层面存在的种种问题,并在此基础上探索现代中国应当涉取的经验和教训。
书的末尾给了一句点睛之笔:
“1587年,是为万历十五年,丁亥次岁,表面是似乎是四海升平,无事可记,实际上我们的大明帝国却已经走到了它发展的尽头。在这个时候,皇帝的励精图治或者宴安耽乐,首辅的独裁或者调和,高级将领的富于创造或者习于苟安,文官的廉洁奉公或者贪污舞弊,思想家的极端进步或者绝对保守,最后的结果,都是无分善恶,统统不能在事业上取得有意义的发展,有的身败,有的名裂,还有的人则身败而兼名裂。
因此我们的故事只好在这里作悲剧性的结束。万历丁亥年的年鉴,是为历史上一部失败的总记录。”
PS:这本书是我年初在滨海大厦排队领开工利是的时候顺便刷完的。看书排队真的是消遣时间的利器,今年也会继续带本书过去😁。
查令十字街84号:这其实是一本真实书信集,里头收录了纽约曼哈顿的穷作家海莲与远在英国伦敦查令十字街 84 号旧书店的长达二十年的通信邮件。故事的起因也非常简单:海莲在美国苦于找不到好书读,从一份报纸上看到了这个旧书店的广告,于是凭着一股莽撞劲,她开始给这个伦敦地址写信,希望能够买到自己想要的旧书。没想到书店老板弗兰克很快就回复了邮件,并且寄来了她要的书籍。于是,双方就开始了长达二十年的书信往来。在这二十年里,海莲与旧书店的人成了非常要好的 “笔友” 。当时英国战后物资短缺,海莲虽然生活也过得很穷困潦倒,却经常在重要节日给这些素未谋面的英国朋友们送去很多食物。整个书店也因此非常感激这位美国小姐姐,并真诚的邀请她一定要去英国找他们玩。而海莲也将之视为心愿,盼望着有一天自己的收入能够付得起这一趟伦敦之旅。然而本书的最大遗憾也在于此:当二十年后,海莲终于成为一个小有名气的剧作家时,弗兰克却早已不在人世。很多年后,她和这家书店的通信集,被称为“爱书人的圣经”,不断演绎。而那家书店的地址——查令十字街84号,已经成为全球爱书人之间的一个暗号。有无数人前往朝圣。
其实这本书的故事非常简单,但正因为如此简单,这本书读完有一种至纯至美的隽永。这是一本爱书人的圣经,在阅读的时候,总是在字里行间让人感受到文字的力量。比如下面这句话:
我寄给你们的东西,你们顶多一个星期就吃光抹净,根本休想指望还能留着过年;而你们送给我的礼物,却能和我朝夕相处,至死方休;我甚至还能将它遗爱人间含笑以终。
霍乱时期的爱情:如果说《查令十字街84号》是爱书人的圣经,那么《霍乱时期的爱情》就是一本爱情的圣经。书中的主角弗洛伦蒂诺·阿里萨爱上了一个富商的女儿费尔明娜·达萨,两人私定终身,却遭到了她父亲的从中阻拦,最终费尔明娜嫁给了一个名声颇高的乌尔比诺医生。弗洛伦蒂诺为了费尔明娜终身不娶,但爱情的痛苦就像霍乱一样折磨着他,于是他只好不断通过和其他女性交往来缓解这种病痛。他一生一共交往了 662 个情人,而唯一一个真正在他心里挥之不去的却永远只有费尔明娜。而另一方面,费尔明娜嫁给了这位医生后,两人一直过着模范夫妻般的生活,甚至早已把弗洛伦蒂诺这个“影子”忘记。然而实际上费尔明娜和医生之间却谈不上爱情,更多的只是亲情关系。直到等了五十一年九个月零四天后,在医生去世,费尔明娜成为寡妇的第一个晚上,弗洛伦蒂诺才再一次向她重申了自己对她永恒的忠诚和不渝的爱情。一直到五十三年七个月零十一天后,已是古稀之年的两个人才真正走到了一起。
有部分人看这本书的时候可能会骂弗洛伦蒂诺只是个渣男。诚然 662 个情人的确是渣的前无古人后无来者,但马尔克斯(本书作者)的用意其实是借这 622 个情人来将肉体之爱与灵魂之爱进行鲜明的对比。
凡赤身裸体干的事都是爱,灵魂之爱在腰部以上,肉体之爱在腰部以下。
弗洛伦蒂诺前脚刚和共享肉体之爱的 14 岁的小女友分手,后脚就跑去向自己灵魂深处爱了五十一年的费尔明娜表白。甚至还在两人重新再一起时,欺骗对方自己依然为她保留了童贞。其实,一个人愿意为另一个人保持五十一年的灵魂之爱,为其终身不娶,当终于等到爱情的希望时,愿意放弃一切重新来到对方身边。那么有没有为其拒绝肉体之爱,是否为其保持童贞又算得了什么呢?相比之下,医生和费尔明娜两人虽然拥有五十一年的肉体之爱,过着让人艳羡的模范夫妻生活,但两人却从未有过灵魂之爱,这样的爱情就真的幸福吗?
人类简史:尤瓦尔·赫拉利的成名作。这是一部宏大的、突破性的人类简史,讲清了 “我是谁,我从哪里来,又要到哪里去” 三大基本问题,理清了影响人类发展的重大脉络。在看这本书之前,我还停留在人类是从类人猿进化得来的这种原始的认知上。而当看完这本书后,我被里头很多颠覆性质的思想深深地震撼住了。比如,我们的智人祖先是如何打败身高上比我们更有优势的尼安德特人呢?答案是靠信仰。因为虚构出了“神”,智人们就在“神”的名义下团结一致,发动起了战争,才将尼安德特人赶尽杀绝。除了宗教信仰,我们直到现在也还活在各种各样虚构的信仰当中。例如国家、货币、人权、婚姻、有限公司……这些都并不是客观存在的具体事物,但因为人类共同相信这些信仰,于是就出现了各种各样的相应的规则和教条,这些规则和教条反过来为人类社会带来了深刻的改变。人类的将来一定也会随着发展诞生出各种各样新的虚构的信仰,这些虚构的信仰是否会带来反面的效果,就需要我们保持警惕了。
暗算:听风者和看风者的故事尤为精彩,结局也都让人无限唏嘘。看风者的解密过程扎实地提及了古典加密和现代密码学的置换和代换的基本门道,即使是学过安全学的人也挑不出毛病(看了书末花絮才知道麦家是真的为了写好这一部分专门查阅了大量资料。充实,有趣。)人物刻画形象鲜明,阿炳的不谙世事然则知恩图报爱憎分明,黄依依的亦正亦邪然则骨子里只是一个为爱不顾一切的人。关于陈二湖的解密信件更是把这个人物提拔到一个让人心生崇敬的高度:整一部分的标题名为“陈二湖的影子”,而他本人也成了整个701的缩影。将一生全奉献给了解密事业,以至于离开红墙后因为无密可解变得像江南一样疯癫,甚至让人想起了《肖申克里的救赎》里出狱后因适应不了外面的世界而轻生的Brooks。而最后回到红墙内解密,将最后一场战争最后的一颗子弹留给了自己,引人泪目。
无人生还:阿加莎的经典之作。一个无法藏人的孤岛,总共只有十个人在里头,但十个人却都被杀死了。凶手会是谁?其实在看这本书之前早就被同名电影、电视剧以及其他衍生的剧集透光了,但是依然忍不住想看看书中的剧情如何发展,凶手又是如何设计这场惊天连环谋杀。书中对细节的刻画,即使对于看过电影的人相信也依然会拍案称绝。唯一的问题是书里的人名太多,看的时候需要做下笔记。
原则:大概是我微信读书和Kindle上看得最久的一本书😂。Ray Dalio 在这本书洋洋洒洒得分享了他在生活和工作上的一些原则(或者说经验)。尤其是在个人生活中的决策、团队决策、招人用人、团队管理上需要避免踩的坑和加强的点。不过因为很多都是教条式的论述,在没有具体的案例的时候读起来就会很吃力,需要动脑思考在实际工作和生活中该如何运用。即便如此,有很多内容依然还没完全看明白。初次阅读建议可以直接先把第三部分开头的大纲看个几遍(我是直接打印了下来放在工位上,时常翻一翻看看有没有什么新的体会),赞同且有共鸣的,在生活中结合正文详细论述,多加运用。不赞同的,批判性接受即可。
尘埃落定:讲述了末代土司从兴盛走向衰败的故事。一个声势显赫的康巴藏族土司,在酒后和汉族太太生了一个傻瓜儿子。这个人人都认定的傻子与现实生活格格不入,却有着超时代的予感和举止,成为土司制度兴衰的见证人。
这本书有点像是魔幻版的《射雕英雄传》。主角很像《射雕英雄传》里的郭靖,虽然平日里很傻,但其实却往往在关键问题上显出大智慧。与之相比,他的哥哥是个聪明人,却经常在关键问题上聪明反被聪明误。恰恰是主角的傻,才让他得以在这个充满权斗的世界中安全生存;而恰恰又是主角的大智,他明白历史的巨轮无法改变,所以当他已经预见了土司王朝即将谢幕时,他唯一做的就是让这一切自然发生,包括自己的死亡。就像《守望者》里的曼哈顿博士一样,平静地看着一切向已预见的未来发展。最终尘归尘,土归土,一切尘埃落定。
相比《无人生还》,我比较喜欢这本书交代人物的方式:并不强调每个重要角色叫什么名字,而是都安插了一个好记的外号(或者说身份),土司、大少爷、二太太、三太太、活佛、管家、书记官……这使得我们不用费心去记每个人的名字,反而更容易帮助我们融入这个神秘的世界。
未来简史:进入21世纪后,曾经长期威胁人类生存、发展的瘟疫、饥荒和战争已经被攻克,智人面临着新的待办议题:长生不死、幸福快乐和化身为神。在解决这些新问题的过程中,科学技术的发展将颠覆我们很多当下认为无需佐证的“常识”,比如人文主义所推崇的自由意志将面临严峻挑战,机器将会代替人类做出更明智的选择。
这本书要给一个评价有点叫我纠结。开头部分的人类新的三大命题真的是精华中的精华,而且极具启发性。末尾对人文派和算法派的阐述也非常有意思。算法(或者说科学)是不是一种宗教?我认为是的。未来究竟是不是拉普拉斯妖,能否完全预测?甚至人的思想是不是也是算法?也可以模拟?细思恐极。除去头和尾,其他的部分就有点让我失望了。“博古”方能“通今”,讲未来之前先讲历史没毛病。所以作者在摆出书末的讨论前试图引入很多“简史”作为佐证,但是太多“简史”就不“简”了,反而一度让我出戏,像看回了《人类简史》。于是一度怀疑是不是赫拉利写《人类简史》堆叠材料堆得太爽刹不住车又把很多内容拖进了《未来简史》。难怪经济学人评价这本书是“一部肤浅、华而不实的作品”。如果把头尾保留,中间简化下篇幅。我觉得会是一部更好的作品。
最后是感情生活,这依然也是我今年最有挫败感的部分。
坦白说,我在今年其实曾经遇到过一位非常合适的结婚对象。对方无论是职业、年龄、性格、相貌都与我非常适合,双方也都非常积极地往男女朋友关系发展。然而最大的问题也在于此:如果感情是在没有心动的情况下,单纯冲着结婚而发展的话,那很可能并不是爱情。如果这段关系发展下去,我和对方在这一两年内走向婚姻的可能性非常大,而且我们也会有非常美满的婚姻生活。但我们之间就很可能像费尔明娜和乌尔比诺医生,永无法共享灵魂之爱。而以对方的条件,完全应当找到一个能共同达到灵魂之爱的人。我又怎么忍心去践踏她的幸福?所以,我终于在某一天跟对方袒露了自己的心声,结束了这段还处于萌芽阶段的感情。
今年还出现了一个让我无比心动的女孩,然而从客观条件上说,又存在着较大的差距,只好将这份欣赏存于灵魂深处了。弗洛伦蒂诺虽然痛苦地等了对方五十三年七个月零十一天,但这段漫长岁月里,他也因此一直拥有着灵魂之爱,这又何尝不是一种幸福呢?也许将来还会出现其他能取而代之的人,但在这之前我总得找点东西填补一下,以免内心的花园长出荒草。
幸好今年春节来得比较早,趁着节前的这几天休假抓紧完成了 2019 年拖到现在的最后一个任务。有趣的是,今年的总结和去年的总结发布时间相差刚好 365 天。Anyway,2019 基本就是按照计划按部就班的进行着。搬入新房之后的一个月我还幸运地摇到号,找了陪驾学开了几次车,总算是摆脱了驾驶恐惧感。
review 下去年的目标吧:
目标 | 进展 |
---|---|
继续改善睡眠 | 完成 |
把《一本好书》里的推荐书单读完。 | 完成 |
买车,并成为一个合格的司机 | 已订车,也算是会开车了吧(明年再练练) |
完成至少一次说走就走的旅行。 | 完成 |
20 年有几个目标:
BCI 技术(Brain-Computer Interface Science, 脑机接口)是一种用于在人脑和外部设备间发送和接受信号的技术。BCI 的基本原理是捕获并解释人脑的信号,然后传输到与人脑相连接的机器上,这个机器可以进一步对人脑的信号进行处理,输出相应指令。
BCI 技术有着非常广泛的应用前景,例如帮助残疾、老年或者行动不便的人士控制轮椅、智能家居以及机器人。BCI 的通信还可以是双工的:除了能将脑部信号传输到外部设备外,还能反向将外部设备的信号传输回脑部。这种能主动传回脑部的 BCI 称为主动 BCI(Active BCI),而只支持脑部到外部设备单向信号传输的 BCI 称为被动 BCI (Passive BCI)。
BCI 的主要挑战在于从信噪比匮乏的脑部信号中精确识别出人类的意图。较低的分类准确度以及较差的泛化能力都制约着 BCI 技术的应用和普及。为了解决以上的问题,最近几年深度学习开始被用在大脑信息处理上。
一个常见的 BCI 流程如图 1 所示 [1],该流程包含几个重要部分:脑部信号采集、信号预处理、特征工程、分类,以及智能设备。具体步骤如下:
在步骤1,针对不同的信号,采集方法则分为侵入式和非侵入式两大类。其中,侵入式的方法是在大脑皮层或皮层表面下方采集;而非侵入式信号则是借助外部传感器来采集。图 2 概括了两大类方法下各自包括的常见脑部信号 [1:1] 。
图 3 概括了几种代表信号所采集的信号源位置。
这些信号的特点和差异见表 1 [1:2]。
从表 1 可见,侵入式方法由于更靠近脑神经,所以具备较高的脑部信号时间、空间分辨率和信噪比。然而,侵入式的方法有两个挑战:1. 需要一个外科手术过程,价格昂贵且有很高风险。2. 植入的探测器是固定的,因此只能够识别固定位置的信号。因此,侵入式方法目前只用于动物研究(例如猴子、老鼠)以及特定疾病患者(例如 ALS 病患)上。附图 1 最近 Elon Musk 所带领提出的 Neuralink 技术也是一种侵入式的方法,其亮点在于通过一套称为“neural dust”的微型机器人来实现植入柔性电极。这套方法解决了以上两个挑战,值得后续关注。不过该技术目前最大的争议在于电极的植入位置无法同时覆盖全脑,且非消费级。
相比之下,非侵入式的方法可以在无需植入探测器的情况下采集人类的脑部信息。这种方案更加安全、快捷。然而,非侵入式的方法能读取到的信号类型和信噪比都很有限。
其中,EEG (Electroencephalogram,脑电波)信号是 BCI 系统中最常用的一种非侵入式信号。EEG 记录大脑活动时的电波变化,是脑神经细胞的电生理活动在大脑皮层或头皮表面的总体反映。如图 4 所示,EEG 的探测位置通常遵循国际的 10-20 系统或中间 10% 探测位置(the intermediate 10% electrode positions)。其中,国际 10-20 系统将头皮按照 10% 和 20% 的间隔精度进行划分,总共包含 21 个探测点;而中间 10% 则是统一使用 10% 的间隔精度进行划分,因此包含 75 个探测点。目前工业采用的 EEG 采集系统通常不会达到 75 个探测点。例如,BCI 2000 拥有 64 个探测点,openBCI 头盔拥有 32 个探测点,Emotiv EpPOC+ 拥有 14 个探测点,Emotiv insight 头盔拥有 5 个探测点。
EEG 信号包含了多种信息不重复的频率基带,这些频率基带各自的特点和与人脑活动的对应关系如表 2 [2] 所示:
实际上,如前面的图 2 所示,EEG 信号还可以细分为很多种子分类:
与其他几种非侵入式信号相比,EEG 有几个非常显著的优势:
EEG 主要有 3 个问题:
由于传感器和脑部之间的障碍物(例如头骨)会对检测到的 EEG 信号质量造成很大影响(信噪比约 5%),所以,在进行特征工程之前,通常需要对脑部信号进行诸如信号降噪、信号正规化、信号增强、信号简化/降维等预处理。
在步骤2,特征工程指的是对识别到的特征通过领域知识进行处理。传统的特征包括了时域特征(例如方差、均值、峰态),频域特征(例如快速傅里叶变换)以及时频特征(例如离散小波变换)。传统的特征工程非常依赖领域知识。例如,要研究癫痫病发作时的脑部信号就非常依赖医学知识。此外,人工提取这些特征也非常耗时和困难。而深度学习则提供了一种能自动识别特征的更好选择。
文献 [1:3] 针对 BCI 领域的深度学习模型做了一个总结如图 4 所示。
可以将深度学习模型分为如下四大类:
目前用于基于 EEG 信号的 BCI 技术 state-of-art 的深度学习主要用在两类 EEG 信号上:EEG 振荡(EEG oscillations,实际上就是指 Spontaneous EEG)以及 ERP。
EEG 振荡其实就是前面提的 Spontaneous EEG 。实际上,Spontaneous EEG 包括了睡眠 EEG、运动想象 EEG(MI EEG) 、情绪 EEG、心理疾病 EEG 等。以睡眠 EEG 为例,典型的应用场景就是根据 EEG 来识别睡眠状态。美国睡眠医学学会(the American Academy of Sleep Medicine,AASM)推荐将睡眠分为五个等级:清醒、非快速眼动1、非快速眼动2、慢波睡眠(Slow Wave Sleep,SWS)、快速眼动。
ERP 信号最重要的成分叫做 P300 (又称为 P3),与其他成分(P1,P2,N1,N2)相比,P3 用于最强的振幅,更容易被检测。因此在 ERP 中应用最普遍。ERP 又可以进一步细分为视觉诱发电位(Visual Evoked Potentials,VEP)、听觉诱发电位(Auditory Evoked Potentials,AEP)、体觉诱发电位(Somatosensory Evoked Potentials,SEP)等。
例如,VEP 是其中一种最受关注的子分类。Ma et al. [7] 基于运动 VEP (mVEP)使用深度学习提取出了运动时的表达特征。在初始阶段使用遗传算法来压缩原始的 mVEP EEG 信号,然后将该信号交给 DBN-RBM 算法来捕捉更抽象的高阶特征。另外,大量的 P300 相关研究是基于文献 [3] 提出的 P300 speller,该技术可以帮助用户拼写字母。目前该类技术的最高精确度达到了 95.5% ,则是结合了 CNN 的变种 [4]。
目前市面上所能购买的消费级脑机越来越多。基于 EEG 的非侵入式脑机最出名的有如下三种:
关于三款产品的详细比较,可以观看 Cody Rall MD 的分析(需自备梯子)。
出于信噪比和性价比(贫穷)的考虑,我选择先购入了 Muse 。下面将介绍如何使用 Python 语言开发 Muse 应用,以及我所开发的一个 python-muse 脚手架项目。
坑爹的 Muse 官方在 2019 年 7 月份就停止了对开发者的支持。现在要基于 Muse 进行开发,有两种方式:
不管使用哪个工具,流程都大同小异:
下面主要介绍基于 Muse Monitor 的 Python 开发方案。需要准备的东西包括:
Muse 开发的关键在于识别 Muse Monitor 通过蓝牙传输过来的 OSC 数据。为了便于开发,我开源了一个项目 python-muse 。
在使用之前,先将其克隆到本地:
1 | git clone https://github.com/wzpan/python-muse.git |
然后安装依赖:
1 | pip install -r requirements.txt |
OSC Stream Target IP
和 OSC Stream Port
两个输入区域,分别填入你的电脑的局域网 IP 地址及一个用于 OSC 通信的端口号(选择一个未被占用的端口号即可),然后回到主界面,点击一个有点像 AirDrop 的图标 即可开启 OSC 传输。1 | python demo.py --ip 电脑局域网IP --port 端口号 |
此时可以试试眨一下眼睛,看看屏幕中是否打印 blink
信息。如果有打印,说明 python-muse 就执行成功了!
python-muse 的实现非常简单,就是基于 python-osc 实现了对 Muse Monitor 传过来的 OSC 数据的解析和处理。
1 | if __name__ == "__main__": |
从上面的代码可见,在 python-osc 的 dispatcher 设置了对 alpha_absolute
、belta_absolute
、delta_absolute
、theta_absolute
、gamma_absolute
五类脑波以及眨眼动作 blink
、咬牙动作 jaw_clench
还有关注度算法 concentration
等类型数据的分发处理。每种数据类型各指定了一个 handler
方法来进行处理,例如 blink_handler
是在屏幕中打印 blink
字样:
1 | def blink_handler(unused_addr, args, blink): |
这样,当用户眨眼时,屏幕中就会打印出 blink
信息。
这些数据类型的分发操作可以根据需要开启或禁用(注释掉即可)。Muse Monitor 总共支持提取和传输的数据类型可以在其官网的 FAQ 页面 中查看。这里解释其中几个名词,帮助读者理解:
Horseshoe:Muse 的几个感应器分布在头环的四周,形成一个独特的马蹄状。这个马蹄形状的图可以表示每个感应器是否正确连接。如果展示的是实心的色块,表示连接良好;如果展示的是空心的形状,表示连接很弱;而如果没有颜色,则表示没有连接。
Blink:表示眨眼动作。
Jaw Clench:表示咬牙动作。
Markers:Muse Monitor 的 App 里额外设计的几个 UI 按钮事件,实际上和 Muse 头环本身无关,用处不大。
另外,如果希望能够识别当前大脑关注程度(concentration),则需要在 Muse 的设置页面里滑到最底部,在版本号上点击 10 次,启用算法功能,然后在 ALGORITHMS
区域里的 Concentration
选项。不过,如 Muse Monitor 的作者所述,作者对 Concentration 的识别算法效果并不是 100% 满意,仅仅只是提供一个实验性质的实现。我在实际使用过程中,也发现这个算法需要使用者保持相对静止的情况下训练十几秒时间,成本的确比较高。
如果电脑端无法正确获取 EEG 数据,或者希望可视化 EEG 数据,可以借助几种手段来进行:
首先检查局域网 IP 地址是否填写正确,对于 Linux/Mac 等 *nix 系统,可以使用 ifconfig
命令来打印出 Wi-Fi 网络获取到的 ip 地址;对于 Windows 系统,则可以使用 ipconfig
命令。
而如果要检查端口号是否被占用,Linux/Mac 等 *nix 系统可以使用 lsof -i:端口号
命令来打印出 Wi-Fi 网络获取到的 ip 地址;对于 Windows 系统,则可以使用 netstat -aon|findstr "端口号"
命令。
如果端口号被占用,改成其他端口即可。
MuseLab 提供了方便的工具,可以可视化 Muse 的 EEG 数据。
在 Muse Monitor 开启 OSC 数据流传输后,启动 MuseLab ,在左侧面板中,输入端口号,传输方式改为 UDP
,然后点击【Open Port】按钮,即可开始抓取 Muse Monitor 传输过来的 OSC 流数据。此时,左侧面板将会出现非常丰富的 EEG 数据类型:
这些数据类型与 Muse Monitor 的 FAQ 页面列举的 OSC Specification 一致。
之后,点击上方的下拉框,切换到 Visualizers 面板:
点击【New…】按钮,在弹出来的新建窗口中选择 “Scrolling Line Graph” ,创建一个时域的可视化器。此时左侧的面板将会出现所有可供可视化的数据类型,将感兴趣的数据类型勾上即可对该类型进行可视化:
频域信息的可视化也大同小异,区别是新建可视化器时选择 “Stationary Line Graph” 即可。
拿到脑机的前几天非常兴奋,很快我就写出了几个 IoT 应用。分别使用眨眼、咬牙、关注度来打开家里的灯。
之后我对我的智能音箱项目 wukong-robot 进行了改造,支持了 Muse 脑机唤醒:
脑控游戏也是一个有趣的应用方向。例如我魔改的一个眨眼控制的 FlappyBird 游戏 MuseFlappyBird 。
如下是一个稍微粗糙一点的早期版本演示视频:
玩了几天 Muse 后,我就已经把目前 Muse 所提供的现成能力都“榨干”了。后面就需要开始往更加困难的目标前进:真正识别意图。但如 Muse Direct 所述:
It is important to note that this raw data can be difficult to interpret and you may want to consider getting in touch with an experienced EEG Researcher for your project as we are unable to provide further support with the analysis and processing of the EEG data you collect.
仅靠 Muse 所提供的几个通道的 EEG 要准确识别出意图是非常困难的。不过,当我凝视深渊的时候,深渊不也正在凝视着我吗?
我与 BCI 的故事,才刚刚开始。
A Survey on Deep Learning based Brain Computer Interface: Recent Advances and New Frontiers ↩︎ ↩︎ ↩︎ ↩︎
脑电图-维基百科:https://zh.wikipedia.org/wiki/腦電圖 ↩︎
Ajay Shanbhag, Aman Prabhu Kholkar, Saish Sawant, Allister Vicente, Sparsh Martires, and Supriya Patil. 2017. P300analysis using deep neural network. In 2017 International Conference on Energy, Communication, Data Analytics andSoComputing (ICECDS). IEEE, 3142–3147. ↩︎
Mingfei Liu, Wei Wu, Zhenghui Gu, Zhuliang Yu, FeiFei Qi, and Yuanqing Li. 2018. Deep learning based on BatchNormalization for P300 signal detection. Neurocomputing 275 (2018), 288–297 ↩︎
这个目标缘起于 18 年中旬,当时我的 dingdang-robot 项目已经积累了一定的用户数,QQ 用户群(580447290)也已经接近 500 名,即将超出普通会员所能创建的群人员限制。
为了控制群人数,我开始实行了入群收费制。一开始,入群费设定为五元,但这个收费过于便宜,并没能刹住车——每天入群的人依然很多,很快就把群人数撑破了 500 名。于是我只好将我的 QQ 账户升级为 QQ 超级会员,将用户群人数上限升级为 2000 名。将群改为付费入群后,带来了两个好处:
但随之而来的问题是:这些入群费要怎么使用?
其实,除了 QQ 群收入外,wukong-robot 项目还接受支付宝/微信的捐赠,以及 OpenCollective 的定期捐赠(这里特别感谢DFRobot的定期支持)。这些捐赠费会被用来续费 wukong-robot 的更新服务器以及一些硬件设备,我还会将 OpenCollective 上筹到的捐赠费用拿来购买或者捐赠其他软件,以支持其他开发者。
所以,额外增加的 QQ 入群费用,我将会考虑用于其他方面的用途。
有些朋友建议我直接拿来当零花钱算了。但早在 wukong-robot(以及dingdang-robot)开发之初,我就没有拿它们来“赚外快”的打算。因为它们的诞生离不开其他开源力量的帮助。饮水思源,我也一直希望自己的项目能够对社会有所回馈。
我想起了 VIM 编辑器上经典的帮助乌干达儿童的启动页面:
利用开源项目来宣传公益。取之于民,用之于民,这是一件多么有意义的事情!所以,在 2018 年六月份,我给 dingdang-robot 项目定了一个小目标:当筹到一万元入群费后,我将全部捐出给壹基金等社会公益项目。这个目标随着 wukong-robot 的发布也延续到了这个新项目上。
经过了一年半陆陆续续的入群费筹集,到了 2019 年 12 月 26 号,也即是圣诞节的第二天,QQ 群的入群费用终于突破了一万元。这意味着,我可以在圣诞节期间当一把圣诞老人了!
QQ 群收入会自动划入 QQ 钱包里,如果要将其取出,会扣取 0.1% 的手续费。所以,我优先考虑了 QQ 钱包内的腾讯公益,好处在于支持 QQ 钱包直接支付,无需扣取手续费。
腾讯公益上的捐赠项目非常多,因为对壹基金的好感,所以我在选择项目时优先考虑了壹基金相关的项目。我一共给 16 个项目提供了捐赠,其中包括了 11 个壹基金相关项目,两个重症儿童项目,一对一帮扶了两名藏区儿童,等等。
在这里也顺便提一下使用 QQ 完成捐赠的过程中遇到的一些体验问题吧。
如前面所述,腾讯公益支持直接使用 QQ 钱包余额来支付,所以非常适合用于将群收入用于捐款。不过,在 26 号当天,我经历了多次无法使用余额完成付款的情况(提示暂不支持本交易)。后来,我才猜到 QQ 钱包的余额支付限额是 5000 元一天。然而我却找不到任何入口可以修改限额,只好等到次日再支付剩余的 5000 元。建议 QQ 钱包的限额提示可以做得更加友好。
一对一帮扶支付完成后,返回结果出现了超时,手动退出后发现钱已经扣了,但是捐赠记录里却不存在,吓得我差点要找客服投诉。后来等了几分钟后,我的捐赠记录里才出现了帮扶项目。猜测是一对一帮扶的数据同步需要一定时间,导致了回调超时。
捐款的去向目前都只能追溯到项目的粒度,无法具体追溯到具体的用处。这是大多数公益项目的通病:缺乏透明的开支查询方式。也希望后面腾讯公益能够尝试使用区块链技术,来支持跟踪每一笔善款的用处。相信这也会促进更多人参与公益项目。
一万元善款只是个开始,希望以后我能继续有更多的开源项目通过这样的形式来参与公益,也鼓励其他的开源项目作者能一同跟进。这里晒出我的腾讯公益等级,欢迎其他开源项目作者来 PK 一下 😃 。
]]>我们来了解一下 Cocos Creator 在各个端所使用的 JavaScript VM :
从上可见,由于 JS VM 不同,同一份代码在不同的平台上运行可能会有很大差异。为了让我们的产品能够给尽可能多的用户使用,我们在开发阶段就需要时刻注意 JavaScript 的 API 兼容性。
举个例子:fetch()
方法是一个用来取代 XMLHTTPRequest
的 API。相比后者,它的优点在于可读性更高,且可以很方便地使用 Promise 写出更优雅的代码。
1 | fetch( |
然而,fetch()
方法不支持所有的 IE 浏览器,也无法在 2017 年以前的 Chrome、Firefox 和 Safari 版本上运行。当你的用户有很大一部分是上述的用户时,你就需要考虑禁止使用 fetch()
API ,而重新回到 XMLHTTPRequest
的怀抱。
在开发阶段,人工保证 API 的兼容性是不可靠的。更可靠的方式是借助工具来自动化扫描。例如下面要介绍的 eslint-plugin-compat 。
eslint-plugin-compat 是 ESLint 的一个插件,由前 uber 工程师 Amila Welihinda 开发。它可以帮助发现代码中的不兼容 API 。
下面介绍如何在工程中接入 eslint-plugin-compat 。
安装 eslint-plugin-compat 和安装其他 ESLint 插件类似:
1 | $ npm install eslint-plugin-compat --save-dev |
还可以顺便把依赖的 browserslist 和 caniuse-lite 一起安装了:
1 | $ npm install browserslist caniuse-lite --save-dev |
之后,我们需要修改 ESLint 的配置,加上该插件的使用:
1 | // .eslintrc.json |
通过在 package.json 中增加 browserslist
字段来配置目标运行环境。示例:
1 | { |
上面的值表示 Chrome 版本 70 以上,或每种浏览器的最近一个版本,或者非 ie 8 及以下。这里的填写格式是遵循 browserslist (https://github.com/browserslist/browserslist )所定义的一套描述规范。browserslist 是一套描述产品目标运行环境的工具,它被广泛用在各种涉及浏览器/移动端的兼容性支持工具中,例如 eslint-plugin-compat 、babel、Autoprefixer 等。下面我们来详细了解一下 browserslist 的描述规范。
browserslist 支持指定目标浏览器类型,并且能够灵活组合多种指定条件。
browserslist 收录了如下一些浏览器,可以在条件中使用(注意大小写敏感):
Android
:用于 Android WebView。Baidu
:用于百度浏览器。BlackBerry
或 bb
:用于黑莓浏览器。Chrome
:用于 Google Chrome。ChromeAndroid
或 and_chr
:用于 Android Chrome。Edge
:用于 Microsoft Edge。Electron
:用于 Electron framework。 将会被转换成 Chrome 版本。Explorer
或 ie
:用于 Internet Explorer。ExplorerMobile
或 ie_mob
:用于 Internet Explorer Mobile。Firefox
或 ff
:用于 Mozilla Firefox。FirefoxAndroid
或 and_ff
:用于 Android Firefox。iOS
或 ios_saf
:用于 iOS Safari。Node
:用于 Node.js。Opera
:用于 Opera。OperaMini
或 op_mini
:用于 Opera Mini。OperaMobile
或 op_mob
:用于 Opera Mobile。QQAndroid
或 and_qq
:用于 Android QQ 浏览器。Safari
:用于 desktop Safari。Samsung
:用于 Samsung Internet。UCAndroid
或 and_uc
:用于 Android 端的 UC 浏览器。kaios
:用于 KaiOS 浏览器。browserslist 支持非常灵活的条件语法,下面给出一些例子作为参考(注意大小写敏感),供读者们举一反三。
> 5%
:表示要兼容全球用户统计比例 > 5% 的浏览器版本。>=
、<
及 <=
也都是可用的。> 5% in US
:表示要兼容美国用户统计比例 > 5% 的浏览器版本。这里的 US
是美国的 Alpha-2 编码 [1]。也可以换成其他国家/地区的 Alpha-2 编码。例如,中国就是 CN
。> 5% in alt-AS
:表示要兼容亚洲用户统计比例 > 5% 的浏览器版本。这里的 alt-AS
表示亚洲地区 [1:1] 。> 5% in my stats
:表示要兼容自定义的用户统计比例 > 5% 的浏览器版本。cover 99.5%
:表示要兼容用户份额累计前 99.5% 的浏览器版本。cover 99.5% in US
:同上,但通过 Alpha-2 编码来加上国家/地区的限定。cover 99.5% in my stats
:使用用户的数据。maintained node versions
:所有官方还在维护的 Node.js 版本。current node
:Browserslist 现在正在使用的 Node.js 版本。extends browserslist-config-mycompany
:表示要兼容 browserslist-config-mycompany 这个 npm 包的查询结果。ie 6-8
:表示要兼容 IE 6 ~ IE 8 的版本(即 IE 6、IE 7 和 IE 8)。Firefox > 20
:表示要兼容 > 20 的 Firefox 版本。>=
、<
及 <=
也都是可用的。iOS 7
:表示要兼容 iOS 7 。Firefox ESR
:表示要兼容最新的 Firefox ESR 版本。PhantomJS 2.1 and PhantomJS 1.9
:表示要兼容 PhantomJS 2.1 和 1.9 版本。unreleased versions
或 unreleased Chrome versions
:表示要兼容未发布的开发版本。后者则具体指明是要兼容未发布的 Chrome 版本。last 2 major versions
或 last 2 iOS major versions
:表示要兼容最近两个主要版本所包含的所有小版本。后者则具体指明是要兼容 iOS 的最近两个主要版本所包含的所有小版本。since 2015
或 last 2 years
:自 2015 年或最近两年到现在所发布的所有版本。dead
:官方不再维护或者超过两年没有更新的浏览器版本。last 2 versions
:每种浏览器的最近两个版本。last 2 Chrome versions
:Chrome 浏览器的最近两个版本。defaults
:Browserslist 的默认规则(> 0.5%, last 2 versions, Firefox ESR, not dead
)。not ie <= 8
:从前面的条件中再排除掉低于或者等于 IE 8 的浏览器。在阅读这些规则的时候,推荐访问 http://browsersl.ist 输入相同的命令进行测试,可以直接得出符合条件的浏览器版本。
细心的读者可能会发现最后一条的查询结果会报错,这是因为
not
操作需要放在一个查询条件之后(下文会介绍)。你可以从其他规则中随意挑一条规则来组合,例如ie 6-10, not ie <= 8
将会筛出 IE 9 和 IE 10 。
browserslist 支持多种条件的组合,下面我们来了解 browseslist 的条件组合方法。
,
和 or
都可以用来表示逻辑 “或”。例如,last 1 version or > 1%
与 last 1 version, > 1%
等价,都表示每种浏览器的最近 1 个版本,或者 > 1% 的市场份额。“或” 操作相当于集合论中的并集。and
用来表示逻辑 “与”。例如,last 1 version and > 1%
表示每种浏览器的最近一个版本,且 > 1% 的市场份额。“与” 操作相当于集合论中的交集。not
用来表示逻辑 “非”。例如 > .5% and not ie <= 8
表示 > 1% 的市场份额且排除 ie 8 及以下的版本。“非” 操作相当于集合论里头的补集,所以 not
不能作为第一个条件,因为你总需要知道“补”的是什么的“集”。三种条件组合类型可以用下面的表格来示意:
条件组合类型 | 示意图 | 示例 |
---|---|---|
or /, 组合 (并集) | > .5% or last 2 versions > .5%, last 2 versions | |
and 组合 (交集) | > .5% and last 2 versions | |
not 组合 (补集) | > .5% and not last 2 versions > .5% or not last 2 versions > .5%, not last 2 versions |
了解了以上规则后,我们可以来配置适用于我们的工程的 browserslist 。
举个例子:假如我们的项目希望在 iOS 8 及以上,或者版本号 49 及以上且市场份额大于 0.2% 的 Chrome 桌面浏览器运行,那么可以使用如下的规则:
1 | // ... |
完成后,可以使用 npx browserslist
来测试你配置的 browserslist 。
1 | $ npx browserslist |
也可以访问 https://browsersl.ist/ 上输入条件测试结果。
完成了 browserslist 规则的配置后,我们就可以结合 ESLint 扫描工程中的 API 兼容问题。同时 VS Code 插件也可以即时提示不兼容的 API 调用。
eslint-plugin-compat 的原理是针对确认的类型和属性,使用 caniuse (http://caniuse.com) 的数据集 caniuse-db 以及 MDN(https://developer.mozilla.org/en-US/ )的数据集 mdn-browser-compat-data 里的数据来确认 API 的兼容性。但对于不确定的实例对象,由于难以判断该实例的方法的兼容性,为了避免误报,eslint-plugin-compat 选择了跳过这类 API 的检查。
例如,foo.includes
在不确定 foo
是否为数组类型的时候,就无法判断 includes
方法的兼容性。在下图中,我们在使用上面的 browserslint 配置的情况下,includes
方法的兼容问题并没有被扫描出来:
然而,从 caniuse 上可以查知,Array.prototype.includes()
方法不能被 iOS 8 兼容:
实际上,Cocos Creator 的 engine 项目自 2.1.3 版本开始,就已经针对
Array.prototype.includes()
方法加入了 Polyfill ,从而彻底规避了该 API 的兼容问题。在本节后面介绍 Polyfill 的时候我们将介绍如何避免该 API 的误报。
为了避免漏报这种问题,我们可以结合另一个兼容检查插件 eslint-plugin-builtin-compat 。该插件同样借助 mdn-browser-compat-data 来进行兼容扫描,与 eslint-plugin-compat 不同的是,该插件不会放过实例对象,因此它会把所有 foo.includes
的 includes
方法当成是 Array.prototype.includes()
方法来扫描。可想而知,这个插件可能会导致误报。因此建议将其告警级别改为 warning 级别。
1 | $ npm install eslint-plugin-builtin-compat --save-dev |
与 eslint-plugin-compat 类似,我们可以修改 ESLint 的配置,加上该插件的使用。但由于该插件容易误报,因此只建议将其告警级别改为 warning 级别:
1 | // .eslintrc.json |
加入该插件后,可以发现 Array.prototype.includes()
方法将会被该插件告警:
靠 ESLint 在开发阶段扫描出 API 兼容问题固然是一种防治兼容性问题的手段,但如果团队里的同事并不认真注意 ESLint 的扫描结果,甚至没有将 ESLint 作为代码合入扫描的一环的话,就有可能会有漏网之鱼继续肆虐。
因此,一种更为一劳永逸的方法是为一些常用的 API 补上相应 Polyfill 。这样一方面可以为不兼容的浏览器版本添加上支持,另一方面又可以使得团队成员安心地使用新的 API ,提高开发效率。
实际上,Cocos Creator 的 engine 项目也内置了很多常见 API 的 Polyfill :
其中就包括了 Array.prototype.includes()
:
因此,如果使用 2.1.3 以上版本的 Cocos Creator 构建带有 Array.prototype.includes()
方法的工程,编译出来的应用将可以顺利在 iOS 8 机器上运行。这是因为 Array.prototype.includes()
在构建时被统一被 “翻译” 成了 engine 项目里提供的方法。
相应地,为了避免 Polyfill 里的 isArray
、find
、includes
等 API 被 eslint-plugin-builtin-compat 误报,可以在 .eslintrc 中将这些 API 加入该插件的排除列表中:
1 | // .eslintrc.json |
engine 项目里的 Polyfill 并不能覆盖所有的 API 。如果你希望使用的某个不兼容 API 并没有包含在 engine 项目中,那么就得考虑给你自己的项目补上该 API 的 Polyfill 。
例如,string.prototype.padStart()
和 string.prototype.padEnd()
两个 API 分别提供了用于字符串的头部和尾部补全的便利方法:
1 | 'x'.padStart(5, 'ab') // 'ababx' |
而这两个方法只在 iOS 10 及以上版本才被支持:
如何寻找这两个方法的 Polyfill 呢?一个最权威的来源就是 MDN 站点(https://developer.mozilla.org/en-US/ )。以 string.prototype.padStart()
为例,我们可以在站点右上角的搜索框中输入 padStart
:
之后敲回车进入搜索,在搜索结果中点击最匹配的结果:
就进入了 string-prototype-padStart
的文档页,在左侧的导航栏中可以看到有 Polyfill
的栏目:
点击它即可跳转到对应的 Polyfill 实现:
找到了 string.prototype.padStart()
和 string.prototype.padEnd()
两个 API 的 Polyfill 后,我们在自己的工程中编写一个自定义的 Polyfill 脚本。例如叫做 ABCPolyfill.js :
1 | /** |
接下来,我们要在应用启动后加载执行这个 Polyfill 脚本里的 ABCPolyfill()
方法,自动打上这两个 API 的 Polyfill 。我们可以再编写一个应用初始化脚本,例如叫做 ABCInit.js ,该脚本用于在应用初始化时执行一些指定工作。
1 | /** |
之后可以在你的工程的初始场景里脚本组件中引用该脚本即可生效:
1 | /** |
为了避免 eslint-plugin-builtin-compat 误报,可以将 padStart
和 padEnd
也追加进排除名单中:
1 | // .eslintrc.json |
所有国家/地区的 Alpha-2 编码可以在这里查询:https://www.iban.com/country-codes。所有的国家/地区/洲的编码也可以在 node_modules/caniuse-lite/data/regions 里找到。 ↩︎ ↩︎
在两年前,我做了第一个智能音箱项目 dingdang-robot 。在去年 7 月加入上报统计后,在不到一年的时间里,这个项目已经运行在 1000+ 台设备中,被唤醒了 128,000+ 次。截至今天,这个项目的个人版和社区版在 Github 上总共获得了 2,600+ 个 stars ,820+ 次 forks。
在我去年的一篇年度总结中,我提到因为 dingdang-robot 本身维护上的困难,我将项目迁移到了 dingdang-robot 组织交由社区进行维护。很遗憾的是,即使迁到了 dingdang-robot 组织,由于组织维护者们都并不是全职维护这个项目,而且硬件和操作系统上的差异始终给 dingdang-robot 的维护带来了很大的问题,所以取得的效果并不理想。而且随着自己能力的不断提升,我对 dingdang-robot 里头的代码也越发不满意:
try...catch
,虽然没人发现这一点,但我自己是过不了自己那一关的,每每想到自己在用一套有问题的代码作为别人的入口就像是留一个坑叫别人跳进来,内心觉得很有罪恶感;另外 PocketSphinx 的安装非常复杂,虽然我提供了树莓派的镜像,但是很多人还是希望手动安装,而 PocketSphinx 对环境要求也很苛刻,所以总会遇到各种奇怪的问题,而我又不能复现;:
)打成全角(:
),或者冒号后没有跟着空格再写键值;再比如当初我处理 log 的打印也设计得比较傻逼,为了写到文件里头,直接用的是重定向,完全没有考虑用 FileHandler 这种东西。到了今年,我决定对 dingdang-robot 进行完全重写,做出一个更加 优雅、灵活、鲁棒 的版本。
为了区别于以前的版本,我决定起给这个新版本起一个新的名字。我觉得三个字的唤醒词误唤醒率和长度都是比较理想的,所以我想取一个三个字的名字,另外还要能提现新版本的强大之处。于是我想到了“孙悟空”(后来才发现又一次跟优必选和腾讯叮当的合作项目重名了,real尴尬 😹 )。
于是,利用整个春节的假期(你没看错,我整个春节都用来写代码去了)。正月初五那天,wukong-robot 1.0 正式发布了。
以下是一段 wukong-robot 的定制版本 ycy-robot 的演示视频(如果访问不了,可以前往观看):
按照惯例,下面总结一下这个项目的一些开发心得。
project boards 是 Github 近期推出的一个新功能,它最大的用处是提供了类似 trello 的看板。我在开发维护 wukong-robot 的时候,也使用 project boards 管理这个项目。于是建了一个 wukong-project 。
我把项目分成了 To do
、In Progress
、Done
、Pending
几个状态。在规划第一个版本的时候,我就在 To do
栏中提了 10 个左右的需求。这使得我的项目可以朝着明确的目标演进。不过,在开发的时候,时常还会有一些新的想法冒出来,这时候我也会尽快写入需求池中。到真正发布 1.0 的时候,我已经完成了 21 个需求。
project boards 的另一个作用在于充当了项目的 roadmap 。你可以看到这个项目有哪些计划要做的需求,有哪些则是我正在开发中的需求。有兴趣的朋友还能参与进来帮忙完成其中的部分需求任务。
project boards 还有一个很有意思的特性:可以和 Github 的 issues 和 pull requests 等板块打通。当有人给你提 issue 或 pull request 的时候,可以设置自动追加到 To do
栏里。而当 issue 被 close 或者 pull request 被 accept 后,相应的条目可以自动挪入 Done
一栏。
不过,project boards 实际用起来还是有一些问题:因为整个 wukong-robot 项目不仅包括本体,还包含了第三方插件库 wukong-contrib ,以及将来可能有的其他一些衍生客户端。所以我希望用一个 wukong-project 来同时管理几个仓库。所以 wukong-project 并不是挂靠在 wukong-robot 仓库下的,而是直接挂在我的账户下。但不知道是不是 Github 设计上的疏忽:即使我在 wukong-project 里 link 了多个仓库,那些仓库下的 project 页面并没有展示 wukong-project 😵 。这种情况下,project boards 的 automation 也玩不起来 —— wukong-robot 的新 issue 也不会自动往 wukong-project 里新增 To do
条目。等我发现这个问题的时候,我早已创建了几十个条目,而 Github 又不支持将 transfer project boards ,所以只能将就着这么用下去了。
如前面所述,dingdang-robot 早期沿用了 jasper-client 的那套热词唤醒和静音检测的逻辑。虽然后来我也尝试给 dingdang-robot 加入了 snowboy 的支持,但让我很失望的是它在树莓派上使用效果很糟糕,所以我一直没有把 snowboy 作为默认的热词唤醒引擎。后来我发现其实我错怪了 snowboy :官方文档已经清楚地提到了问题的原因:而树莓派上或者其他板子上接的麦克风可能和 PC 上的麦克风的声音畸变差异非常大,所以现有的模型更加不能直接在树莓派上工作,否则效果会非常糟糕。
This is due to the acoustic distortion that results from the different microphones. If you record your voice with two different microphones (one on your laptop and the other on your Pi) and then play them (play t.wav), you will hear that they sound very differently (even though it is the same voice)!
了解到原因后,我在这个版本中去除了安装繁琐且中文识别较差的 PocketSphinx ,将 snowboy 作为主要的热词唤醒引擎。因为 snowboy 还提供了静音检测(VAD)的功能,所以我把原来 VAD 的代码全部去除,改为了直接使用 snowboy 的 VAD 。经过改写后,整个系统的稳定性和响应速度都有了质的提升。
不过,接入了 snowboy 后,整个交互模式就是先热词唤醒触发一个 detected_callback
的响应,说完指令后通过 audio_callback
将语音指令返回。有些时候我们并不想完全遵循这个形式:例如当我们希望 wukong-robot 能主动询问并澄清话术的时候,总是要求用户唤醒再说指令就显得整个交互很不智能了。于是我对 snowboydecorder 做了一点 hack :仿照 HotwordDetector 写了一个 ActiveListener 用来实现主动询问用户的功能。有了这个 ActiveListener 之后,当插件需要主动询问用户问题时,可以在 self.say()
的 onCompleted
回调方法中直接执行 self.activeListen()
方法得到即拿到用户的指令内容。例如:
1 | def onAsk(input): |
利用这个方法可以很方便地实现多轮对话以及极客模式。
原来的 dingdang-robot 在处理插件接口的时候,并没有考虑到多轮对话的情况。每一次 query 都会轮询一遍所有插件。如果要让某个插件在用户指示退出前持续响应用户的 query ,那么就得为这个插件实现一个内部循环。而在这个内部循环里头,用户只能响应有限的指令。
例如,NetEaseMusic 插件在一个 handleForever
方法中进入了一个循环,在这个循环里头,只能响应“上一首”、“下一首”等音乐播放相关的指令。而有时候,我们在播放音乐的时候,也会突然间想问一下天气再回来继续播放。对于这种情况,dingdang-robot 的插件交互模式就只能先退出音乐播放,再问天气,再重新要求播放音乐。这样的设计并不够人性化。
wukong-robot 重新考虑了插件的设计。你可以为 wukong-robot 开发两类技能插件:
self.activeListen()
方法进入主动聆听,从而实现多轮对话。不论是哪种类型的插件,都只需继承同一个基类 robot.sdk.AbstractPlugin
,并实现相应相关接口即可。其中:
isValid()
和 handle()
两个接口,分别用来判断用户指令是否适合交给该技能插件处理,以及如何处理;IS_IMMERSIVE
成员属性为 True
,此外还可以根据需求实现 isValidImmersive()
和 restore()
两个方法,分别用来支持沉浸模式下更多指令的响应以及恢复技能。1 | class AbstractPlugin(metaclass=ABCMeta): |
经过这次重构,所有的插件都继承自同一个基类。即使是需要多轮交互的沉浸式插件,用户不再需要为其编写类似 handleForever()
的循环,只需要关注核心的 query 处理即可。在沉浸式插件工作期间,wukong-robot 也支持响应其他技能的 query ,交给其他适合处理的技能插件处理,并在处理完成后根据情况恢复当前沉浸式插件的处理。作为对比,你可以看看 LocalPlayer 插件,它的可读性要比 NetEaseMusic 插件强很多。
大脑模块和技能系统实现一章中将更加深入地介绍 wukong-robot 插件机制的实现原理。
早在 dingdang-robot 发布初期,我就有为它配套开发一个后台管理端的想法。但因为种种原因(主要是因为懒),这个想法一直拖着没有去做。于是借着这次项目重写,趁热打铁就把后台管理端也完成了。
因为对 Jinja 比较有好感,所以我起初是打算用 Flask 来写后台管理端。但后面发现 Flask 的信号机制不能直接在非主线程里工作,而直接放主线程又会跟另一个必须工作在主线程的 snowboy 有冲突。折腾了半天后我决定改为直接支持在非主线程工作的 tornado 。后台管理端的技术栈主要包括:
比较费脑的是鉴权部分。除了后台管理端需要设计登录界面以避免非法访问之外,我希望后台的接口能够开放 API 以支持其他配套客户端的接入,所以后端代码需要考虑两种访问来源的鉴权。
最初我使用 cookie 来鉴权,管理端登录成功后,就把用户设置的鉴权密钥 validation
字段存到 cookie 里头。前端在 Ajax 调用后端 API 时,可以直接从 cookie
里取出 validation
然后作为鉴权字段发给后台。然而 cookie 本身是明文保存的,这种做法会直接暴露用户的密钥,因此是一种很不安全的做法。
然后我尝试了使用 secure_cookie
来保存鉴权信息,然而因为 secure_cookie
是加了密的字段,前端没办法直接解析并传回给后端,所以又暂时放弃了这个做法。
再后来我发现还有一个 csrf_cookies
,可以用来防止跨站请求的问题。于是我很兴奋地加入了这个校验。但后面我发现这个跨站请求保护也只适用于站点本身的保护,因为 xsrf_cookies
的校验会在调用我们的接口实现方法前就完成,一旦加了这个校验后,其他客户端在调用 API 时也必须带上 csrf_cookies
,否则会直接抛出 '_xsrf' argument missing from POST
的错误。因此这个校验更适合用于纯 Web 站点,而不适合用于开放 API 的应用。
最后我转念一想:虽然前端没办法直接解析 secure_cookie
得到 validation ,但是 secure_cookie
也只是一个加了密的 cookie ,我依然可以取出 secure_cookie
里这个加了密的 validation
的值然后传给后台,而后台则可以使用 get_cookie
(而不是 get_secure_cookie
)取出期望的加了密后的 validation
的值并与前端传过来的值进行比对,这样就实现了前端页面的鉴权;对于 API 的鉴权,则可以直接使用明文的 validation
并将其作为第三方客户端的一个配置。后端在鉴权时直接判断这个 validation
与后端的配置里的 validation
值是否相等即可。所以最终我完成了如下的一个带鉴权的基类:
1 | class BaseHandler(tornado.web.RequestHandler): |
在配置页面,我在保存配置的时候加了 yaml.load()
检查,如果用户修改 YAML 有格式问题,将会被拒绝写入配置。另外,我还基于 watchdog 加入了对配置文件的监听:一旦配置文件发生修改,就触发配置的重新读取,从而实现无需重启更新大部分的配置。
1 | # -*- coding: utf-8-*- |
要说不太满意的地方,主要是首页的聊天消息更新机制。目前我是直接使用轮询的方式实现的 —— 前端会每隔 5 秒调用一次 /gethistory
接口,从而更新聊天记录。这种方式无疑是低效且浪费资源的做法。我曾经尝试将更新机制改成用 websocket 来实现,但后来发现手机端的浏览器几乎都不支持 websocket ,考虑到便携性的重要程度,我就放弃了这种实现。
后面我将尝试使用 tordano 的 coroutine 来实现长连接通信以及后端的主动更新,这会是一种更好的实现方案。
在即将发布 wukong-robot 的时候,我突然想到应该给 wukong-robot 一个提示升级的功能。当检测到版本更新时,提示用户进行升级。
于是我给 wukong-robot 的主仓库和插件仓库设计了一套基于 git 的更新机制:
VERSION
文件用于记录当前的版本号,版本号使用 Semantic Versioning 标准;VERSION
的版本号,并为其打一个新的 tag ;git checkout master && git pull && git checkout TAG名
。剩下的主要问题是检查更新的服务应该部署到哪里。当然,简单的搭一个更新检查服务器并不复杂,但服务器的维护成本比较高。如果后面我换了服务器,又得重新到另一个服务器搭一遍更新服务。另外,我并不太希望每次要发布新版本都得打开终端登录到我的服务器进行修改。最理想的应该是有个可以随时修改的 云 json 串
。于是我选择使用了腾讯云的无服务器函数(SCF):把最新版本信息写成一个SCF,通过向SCF发请求完成版本更新检查。这样的好处是无需购买和维护服务器,无需到服务器发布代码,而且SCF提供了方便的在线编辑、版本管理和测试验证的能力,这比自己发版本还要靠谱的多。
wukong-robot 的改动如下:
wukong-robot 后续的重要计划是训练本地的 ASR 、TTS 、NLU 及对话系统,并引入 RNN 降噪来改善环境较嘈杂的情况下难以唤醒的问题。关于项目的计划,可以关注 wukong project board 。
而在近期,我正在腾讯课堂上推出一套 Python 开发教程,其中会用到 wukong-robot 作为一个开发案例。
这套视频课程将从零开始,一步步教你如何使用 Python 开发出 wukong-robot 。涉及 Python 的基础语法,以及离线唤醒、静音检测、语音识别、语音合成、对话机器人等知识背景的介绍及相关sdk和服务的接入,并在这个基础上如何通过一步步的重构优化,开发出一个灵活可配置的 wukong-robot 。另外,还介绍了如何使用 tornado + twitter bootstrap + jQuery + Ajax 开发后台管理端及前端页面。进阶版中还包括了爬虫技术及 Flask 等技术的相关实战。
这门课的准备和录制几乎占据了我全部的业余时间,录制的过程是非常痛苦和煎熬的。比如,为了讲好 subprocess ,我把 subprocess 的老版本高级 API 、新的高级 API,再到底层的 Popen 以及涉及到的 Linux 的标准输入输出和管道的概念都讲了一遍。对于讲授的方式,我比较提倡授人以鱼不如授人以渔的主张,所以我并不是直接贴 API ,而是带着读者一起看 Python 的官方文档,着重培养阅读文档的能力。这种讲法非常的累,但却是我认为每个工程师应该掌握的学习方式。
参与这门课的制作也是为了完成我在去年的个人总结中立下的 flag 。Python 一直是我业余时间最常用的玩具语言,它非常适合用于原型开发。我有不少开源项目,比如 wukong-robot、dingdang-robot、LiveCV 都是用 Python 写的。而在我的工作中,它也帮助我完成了大量的工具和项目,这些工具和项目对个人或团队起到了非常大的作用(例如加班统计平台、已经在上百家中小银行中使用的fmanager),因此 Python 也无疑给我的职业发展起到了很大的推动作用。把我所掌握的 Python 知识分享给更多人,让更多人能够自如的使用这门语言来满足他们的需求,那也算是我对 Python 这门语言的回馈。
]]>2018 年依旧是比较充(mang)实(lu)的一年,这一年我完成了买房大计,开始在 GMTC 这类的技术会议上做分享交流,开始带团队,首次挑战并完成了一次鹅厂的技术通道答辩,开始尝试担任腾讯课堂的讲师对外授课……收获颇丰。与之相对应的也有一些失去,包括一段还没来得及开始就匆匆结束的感情。
为了避免把总结写得太过于流水账,我想把总结拆成几个主题来回顾:正经事(工作、开源、博客、授课),不正经事(乐器、旅行、游戏、锻炼、看书、感情)。
先说说正经事吧。
在去年我写了一篇在平安两年的总结后,很多朋友都知道我跑来腾讯开发智能硬件了。我们做的这款产品叫做小Q机器人二代。我刚来的时候,这个音箱其实已经开发得差不多了。恰好管理端没人维护,所以我就充当救火队员的角色接手了后台管理端的维护,并且为它开发了一套热更新系统,用于给智能硬件升级技能。另外为了提升闲聊对话的质量,我又写了一个闲聊优化平台,用于评估闲聊的对话质量。作为一个安卓开发工程师,我也负责做了一个系统应用的UI重构工作。
到了去年 10 月份,我们又多了一个新项目的机会:ABCmouse 腾讯版。ABCmouse 是一个在美国家喻户晓的儿童英语教育软件。我们与 ABCmouse 的研发公司 Age of Learning 公司开展了战略合作,以期开发出一款针对中国儿童的英语学习应用。我们希望它能提供更符合中国儿童使用习惯的学习路径和交互方式,并在里头融入腾讯的社交元素,从而带动儿童外语学习的积极性。
当时在团队里头可以选择是否继续维护小Q机器人或者选择开发新项目,出于对教育这个方向的看好以及对从零开始开展一个新项目的兴奋感,我选择了后者。在那个时候,我们的开发人力算上 Jolt (我的 leader) 总共只有 4 个人,大家聚在一个小黑屋里就动手干了起来。在刚开始的阶段,因为没有后台,我也负责做了一些后台的工作。不过在大厂工作有个很重要的点:很多时候你要做的事情别人已经做过,这时候发现并且充分利用好已有轮子的能力就显得非常重要。作为一个客户端开发,要立马做到充分用好公司里的后台轮子,并且把美国团队已有的服务迁过来并和公司的整套框架轮子顺利对接,对我的挑战非常大。幸运的是 Jolt 也知道我一个人搞非常吃力,过了不久就安排了后台组的人力支持,紧接着前端也有同事加入到这个项目中。队伍越来越壮大,我就终于可以回归老本行搞起客户端的开发了。
附图 1 小黑屋时期的工位。虽然通风不好,但窗外的风景很好。虽然说是回归老本行,但正好遇到小Q机器人即将发布,所以我依然要经常抽身去支持小Q机器人的后台管理端维护和开发工作。一边搞A项目的客户端,一边搞B项目的后台,工作内容也变得很分裂。最坑的是有台小Q的 COS 服务器因为长期欠费,在某一天突然被回收,所以我只能到合作账户下购买新机器然后把 COS 服务迁移了一遍。但马上我又发现后台管理端所有 COS 的 SDK 都需要升级到新版本,相应的所有接口都得重写。恰好那时开发 ABCmouse 也陷入了跨平台的坑:开发一个音乐播放器的时候,Web 端和 Android 端调试都没问题,结果到了转体验的时候发现原来 iOS 端播放不了。于是那天晚上为了保证顺利转体验拉着 Nasky 调到凌晨五点才算解决。而为了避免影响小 Q 发布,第二天又自带鸡血地跑来搞 SDK 接口升级。大概是因为长时间处于比较高负荷而分裂的状态,有段期间脾气变得暴躁了起来,加上感情失意和我妈生病住院的双重打击,于是有一次因为控制不住脾气还向 Jolt 发了火。而 Jolt 也发现了我那段期间状态不太对,心平气和地把我拉去聊了下情绪控制的问题。现在想起来我真要非常谢谢他那次的警醒,我也因为那一次教训而更加意识到职场上的合作问题。
当然,那段期间不止我一个人很忙碌,大家就像一个创业团队一样在拼搏。最有意思的是,小黑屋里的空气比较差,分坐我两边的 Jolt 和 Xepher 都患上了鼻炎,于是一个打完喷嚏另一个接着打,而且似乎可以无限循环下去……In one word,在小黑屋里头发生了不少故事。
说说在腾讯的感受吧,刚从平安跳去腾讯的时候就和刚从百度跳去平安一样有着很大的落差。从百度到平安,工作节奏一下子放慢了下来,我花了挺长时间才适应没有高负荷工作带来的不踏实感(适应办法就是自己给自己找活干)。从平安来到腾讯,又自愿加入了一个初创团队,工作节奏一下子又上去了,Jolt 一开始还怕我适应不来。所幸自己在平安期间其实也没有闲过,过来这边依然能打。接手的活都能够如期交付,没有出现过 delay 的情况,并且还主动扛下了一些边界模糊的工作:比如通用组件的梳理和完善,或是一些有助于提高日常开发效率的工具开发以及流程优化措施。手上的包袱多了,自然而然就从一个打杂维护后台的客户端慢慢成为团队里的骨干了。
总的而言腾讯的工作氛围的确是非常好的。刚入职的时候,负责培训的小姐姐问我知道腾讯里面什么最多,我故作肤浅地回答了一个字“钱”。她笑着说其实是公仔和文化T-shirt。在腾讯待这一年半下来,果然是通过各种渠道领了非常多的公仔和 T-shirt 。特别是到了司庆或者项目庆功宴的时候,穿上统一的 T-shirt 马上就成为朋友圈爆款。初此之外,腾讯里头还有许多贴心的福利,人文关怀做得相当不错。我更喜欢的是公司的内网建设,员工对公司内的大小事务有任何不满,真的可以直接在内网发声,而不用担心被打击报复。有这样的平台让大家 speak out ,员工也会真正得到一种被 respect 的感觉,从而也会更有主人翁意识。相比之下,在平安大家就显得比较拘谨,即使我给团队内部搭了一个壹瓴阁
论坛让大家自由发言,还找上老大拨巨款鼓励多发帖,最后这个站点依然只停留在发技术文和征婚贴的地方(我甚至还给发征婚贴的同事颁发过奖金 😹)。
到了今年三月份,ABCmouse 终于发布了第一版。当晚我们很兴奋的聚在一起留了一张照。ABCmouse 也是我亲身参与并从零到一完成的第一个初创项目。
因为在整个过程中承担了比较多的工作,到了 6 月份,在 Jolt 和 Hebert 的推荐下,我代表团队去参加了今年的 GMTC 全球移动技术大会,分享了团队在整个项目开发过程中积累的一些经验。在 GMTC 上遇到了很多前同事和老朋友,交谈之下发现大家过的都挺不错。当然还认识了不少志同道合的新朋友。
我也因为这段期间的工作表现拿到了一面小锦旗以及两块砖头。说到奖励,相比平安随便一个小奖就有奖杯拿,腾讯的各种奖项的纪念物显得格外朴素。季度奖发小旗子,四五星则是发一块砖。目前我只见到 Jolt 升 T4 的时候获得了 Ross 亲授的一个奖杯。这带来的好处是奖杯的含金量也因此显得很高的样子(至少短期来看我没机会拿到啦)。
从 GMTC 回来后,团队发生了剧变。在一次团队聚餐上,Jolt 突然宣布即将升迁去另外一个部门当任总监,并安排我和 Xiangwen 代为管理好这支团队。虽然在这之前 Jolt 已经拉过我和 Xiangwen 大致说过后续的规划,但这个决定依然来得非常突然。于是从 7 月份开始,我的身份又一下子从一个普通骨干变成了团队的负责人。
对我而言,带团队从来就不是一件轻松的事情,我自己仅有的一点管理经验只是在大学期间担任过班长和校学生会网络部的部长。那个时候为了管好团队,看了不少管理相关的书。我比较欣赏的是一本叫做《领导沟通力》上提倡的观点:它简单粗暴地把领导力分成四个象限:
Q1 毫无疑问就是最理想的领导风格。因为这个观点简单有效,所以我在日常管理团队的时候总是暗自告诫自己既要能保持随和也要能维持足够的控制。但这只是一个基本的要求。具体到要做好一个开发岗的 leader ,控制力又体现到了技术驱动能力、决策能力、对风险的嗅觉、对下级成长的关注、上下级沟通能力等方方面面。所以说做好 leader 也是一个永远都学不完的学问。前阵子看到内网上有人在问 “什么样的leader才是称职的leader” ,有一个观点我非常赞同,也希望自己能往这方面努力:
A boss uses people but leader develops people. A boss issues ultimatums but leader generates enthusiasm. A boss says “I” but leader says “we”. A boss takes credit but leader gives credit. A boss says “go” but leader says “Let’s go”. A boss make followers but leader makes leaders.
开始带团队后还有一个变化就是感觉自己的时间更不够用了。以前虽然手里的需求更多,但毕竟有足够的 buffer 可以专心去完成。现在虽然花主要时间要用来带团队,但是依然希望自己能保持在开发前线,带着团队往前冲,所以每个版本都会给自己留一些需求去做。但实际操作下来,每周正常工作时间基本都被各种大大小小的会议给占满了,真正留给自己做需求的时间少得可怜,并且非常零散,恢复断点的成本随之变得很高。Seek 也看出了我的捉襟见肘,提醒我应该把握好自己的工作量,不要只顾着往前冲结果带头战死沙场。我现在也尽量避免给自己分配太多简单而琐碎的(trivial)的需求。一些一目了然知道怎么做的需求,我一般会交代给其他人去执行,而我所需要做的是跟他说清楚方案。而对于足够复杂的需求,我会思考这个活是不是最适合给我做。如果有更适合的人选,我会优先选择他去完成,但他需要跟我说清楚方案。
到了年底的时候,回顾了一下这半年期间和 Xiangwen 一起带着团队做过的一些技术优化工作,以及在流程措施、代码质量、效率提升上做的改进,对整个团队的战斗力还是有点小自豪的(部分细节不便公开,所以做了删减):
不过仅靠技术优化并不能保证产品能持久的做下去,技术也并非产品的第一推动力。Jolt 也私下批评过我太容易陷入对当前状态的满足。细细回想了一下我确实有这样的问题。明年除了做好技术优化,我还需要思考怎么跳出目前的边界,去推动产品往一个更好的方向发展,而不要轻易陷入满足。
因为工作比较忙碌的关系,整个 2018 年的格子数比起 2017 年少了很多,不过我也没有对涂满格子数有太深的执念。
今年主要开源/维护了几个项目:
项目名 | 简介 | 今年的工作 |
---|---|---|
dingdang-robot | 开源的中文智能音箱项目 | 社区化建设 |
BeamerStyleSlides | Beamer风格的幻灯片模板集,尤其适用于晋升述职、技术分享和学术汇报。 | 新开源 |
LiveCV | 可视化简历生成器,能抓取你的 Github 数据并生成精美的 PDF 格式简历 | 业余开发中,未完整开源 |
hexo-generator-search | 用于为 Hexo 生成本地搜索索引 | 若干 bugfix,并支持忽略索引指定文章。 |
comment.js | 一个纯JS实现的静态站点评论系统 | 若干 bugfix,特别是解决了 recent comment list 在不同浏览器展示效果不一致的问题。 |
cocos-jsc-endecryptor | 指导我的徒弟 khan 做的一个 Cocos Creator jsc 加解密工具。 | 新开源 |
dingdang-robot 在今年的一月份 star 数就已经超过了 1k,第三方插件数量超过了我自己提供的插件的两倍。过了不久 QQ 群用户也超过了 500 人,于是被逼着办了个 QQ 超级 VIP 升级成千人大群。
在 QQ 群里经常能看到有一些用户拿 dingdang-robot 做出了很多有意思的应用。在淘宝上还看到有人在卖 dingdang-robot 和魔镜的结合品。六月份的时候了个 flag:当 dingdang-robot 的入群收入达到一万时将全部捐出给深圳壹基金等公益机构。半年过去了,现在看这个小目标已经达成了一半。
不过,因为自己的业余时间比较少,而调试和维护 dingdang-robot 又很依赖硬件,有时候为了复现用户的问题,折腾起来非常耗时。看到项目上积压了一堆的 issues 没有去解,心里就觉得实在过意不去。所以在今年 3 月份我就将 wzpan/dingdang-robot 的代码迁移到了 dingdang-robot/dingdang-robot ,由更多的人一起维护。到了年底,dingdang-robot/dingdang-robot 和 wzpan/dingdang-robot 两个仓库的 star 数加起来已经超过了 2k 。
在春节期间,闲来无事又写了一个面向工程师的可视化简历生成器 LiveCV ,底层渲染器基于LaTeX,用YAML简化了LaTeX的语法,并且提供了一个可视化的编辑界面。后台用web.py,前端现学现卖用vue.js,用Docker包装镜像。亮点在于:
$stars
宏和一个 $forks
宏。
目前这个项目进度大概是 80% ,还剩多用户、权限管理以及多语言支持没有完成。体验地址:http://livecv.hahack.com:8021 。
这是一份渲染出来的简历效果:
7 月初参与了一次鹅厂的技术通道晋升答辩,在准备 slides 的时候苦于没有找到一个简洁实用的 PPT 模板,于是仿照着 Beamer 做了一份 PPT 模板,后来干脆搞成了个开源项目 BeamerStyleSlides ,对大部分 Beamer 的模板主题进行复刻。这套模板的介绍文章上了公司内网的头条,获得了两百多个同事的收藏。
自从开始带团队后,我更加珍惜业余时间动手做开源项目的机会。工作上处理技术需求的时候,如果脑海里已经有了个基本方案,我就更倾向于交给其他人去替我执行这个方案。但如果长期不注重细节实现,那么自己的动手能力就会下降。所以业余时间自己动手做点开源项目就是一个很好的避免眼高手低的方式,在做 prototype 实现自己的想法的时候,尽量给自己树立一个高标准,push 自己去达成自己的要求,并且在尽可能短的时间里实现出来 80% 的功能(以免自己三分钟热度过去就不想做了)。偶尔这样折磨一下自己,也会有很不错的收获。
博客基本是处于荒废状态:今年一共就发了三篇博客。
我比较反感把网上随便一搜就能找到的知识点加工后写进自己的博客中,如果这么做了,我充其量只是沦为了一个知识的搬运工。所以对于自己的博客,我向来是宁缺毋滥,只写原创的内容。
虽然自己的博客是荒废了,但我在公司内网上反而变得很高产。因为工作中就能产出非常多的原创内容,所以大概就是把写博客的习惯迁移到了内网里头。
在这一年半里我一共在内网写了 27 篇文章。所以遇到部门文章数排名的时候,我往往都是处于遥遥领先的位置:
因为分享比较勤快,也偶尔能有几篇文章登上内网的头条,所以在鹅厂一年半的时间里,我的内网影响力已经超过了 96% 的同事。
严格意义上对外授课并不能算工作本分内的事情。不过既然选择了教育这个行业,所以除了开发教育App之外,制作课程对外分享也就变成了对自己所处的这个大行业的一个积极探索和实践。Jerry 说 Age of Learning 公司在招聘的时候,会首先考察的就是这个人对教育这个行业有没有热情。我也非常认同将行业方向当作择业的第一标准。多参与行业内的不同形式的实践,反过来也能够提升我对这个行业的投入积极性和洞察能力。所以我也把授课列入“正经事”这个大分类里头。
在公司内,有一个做对外课程的平台,叫做腾讯课堂。如果收到腾讯课堂的邀请,只要我有信心能讲好,我都会尽量给予支持。
因为我们是最早一批使用 Cocos Creator 开发微信小游戏的团队,所以 6 月份我第一次收到了腾讯课堂的邀请,让我作为主要负责人来制作一套关于小游戏开发的课程,叫做《微信小游戏入门与实战》。
收到这个任务的时候,心情大概是兴奋和焦虑并存:兴奋是因为看了多年的 Udacity 等线上课程,没想到自己也有机会成为一个公开课程的讲师(而且课酬也挺给力 😄 );焦虑是因为这相当于又欠了一笔技术债,而我平时的工作已经很忙了,担心因为业余时间不够而影响课程的交付时间和质量。
为了提高交付效率,也为了把锻炼的机会多交给一些人,我把组里的大部分同事都拉来当讲师,根据各自所长来分配教学内容。为了保证整体的教学质量,我为这门课设计了一份教学大纲,并确定好每个讲师需要讲好的知识点,以及课后准备的练习项目,其他的交给各个讲师自由发挥。
一直准备到 11 月,我们的课程的前四章终于制作出来并上架了腾讯课堂。这段期间我们经历了无数次的修改重录,甚至还临时更换了部分讲师。虽然离原来预期交付时间慢了很多,但我还是宁愿慢工出细活,把整套课程做出精品,这样才对得起购买课程的人。
在 11 月底我又做了两场直播形式的公开课,在线直播分享如何使用 Cocos Creator 开发微信小游戏《2048》。这个公开课可以看做是对小游戏开发系列课程的预热。
让我比较欣慰的是,这两门课程目前都维持了 100% 的好评率,这说明我们的课程质量得到了学生们的认可。
除了这两门课程之外,我还当任了另一门 Python 课程的出品老师,负责评估这门课程的教学内容和质量。不过我这个出品老师一直没有起到任何实质性作用,因为一直到现在这门课的讲师都没有找过我。直到最近 Jeep 告诉其实这门课进度一直卡着,因为找不到合适的老师。他希望我能够转成主讲老师亲自去讲这门课。本来我已经打算不再讲课了,但在他的盛情邀请之下,我又有些被说动了。Python 一直是我业余时间最常用的玩具语言,它非常适合用做原型开发。我有不少开源项目,比如 dingdang-robot、LiveCV 都是用 Python 写的。如果把我所掌握的 Python 知识分享给更多人,让更多人能够自如的使用这门语言来满足他们的需求,那也算是我对 Python 这门语言的回馈。
今年并没有去学习什么新乐器,主要是因为回家基本都比较晚,夜间玩乐器太扰民,而只用周末是不太可能掌握什么新乐器的。
电子琴接近处于吃灰的状态。今年主要的收获是学会了一些常用的和弦,能够弹唱简单的歌曲。复杂点的琴谱还是弹不起来。要继续进阶还是得报班才行,自学永远都是半吊子,毕竟不是谁都能像 1900 那样无师即通。
为了能够在周末时间在公司练会儿琴,我又买了一把折叠电子琴,但发现这玩意儿只能算玩具,触感非常差,没有弹几次也被我收起来吃灰了。
唯一坚持下来的只有 Ukulele 了,它也是我平时最喜爱的玩具之一。今年主要的进步是开始玩指弹,慢慢地一些歌曲的前奏和间奏都能够很快地学会并流畅地弹出来(《一小段青花瓷》)。
另外,和弦摸熟了之后,也可以自己扒谱了。今年一共在有谱么(yoopu.me)上写了五首谱子:
扒谱是一件蛮有乐趣和成就感的事情。比如王力宏的《南京,南京》刚出来的时候,只有演唱会现场版,录音室版本都还没发布,更别提伴奏了。而我却可以很快扒出和弦来自弹自唱。
扒谱的时候曾经萌生出给 Hexo 做一个绘制谱的插件的想法,最理想的就是在博客里可以用 tag-plugin 的形式直接绘制成 svg 格式的和弦,这样我就可以把我的博客变成歌谱本。经过搜索发现了 haixiang 的几个有意思的 react.js 插件 react-chord-generator 和 Guitar-Editor,他基本上就是用自己的技术栈复刻了一下有谱么。我挺欣赏他的动手能力和自驱力,于是加了他微信好友。然而我自己还没有开始动手去写这个 Hexo 的绘制谱插件,主要是它的实现成本有点高,而回报有点低(毕竟太小众,而且已经有“有谱么”了,我没有在博客上写谱的刚需),所以暂时还没有提到很高优先级的程度(如果你有兴趣,欢迎留言申请加入)。
Ukulele 也让我对吉他重拾了兴趣。我曾在大学期间买了一把吉他,学了一段时间爬格子后就已经兴致索然。之后又在一次调琴弦的时候因为方法不当把弦弄断了,于是这把琴就被我带回家扔起来了。国庆的时候回了一趟家,找出了这把尘封多年的吉他,大概摸了半个小时很快就可以上手弹唱了。唯一遇到的困难是 F 和弦依然是按不下去,但我认为并不是自己的方法问题,而是这把琴质量太差,品距太高导致加大了 F 和弦的难度。我已经开始种草 Taylor 114ce ,说不定等搬到新房子后就拔草。不过在剁手前,我应该会先去逛下琴行看看有没有更适合自己的定制琴。附图 3 像这种路边摊ukulele千万别买
说到买乐器这个话题,真的要相信“一分钱一分货”硬道理。不同价格的乐器,演奏效果的确有云泥之别。我的 Ukulele 是三年前 Allen 送我的 Rainie Poet S-30L,这把琴用桃花芯实木作为面板,音色饱满,琴头弦枕也很稳定,可以放好几个星期都不用调弦。弹久了之后,再去碰其他人的那种一两百块的 Ukulele ,就能明显感觉到劣质琴的廉价感——劣质琴因为用的是很差的面板,音色很单薄,拿在手上感觉很轻,而且面板和背侧板的衔接处往往都不会打磨,手臂放在上面会感觉很硌手。除此之外,劣质琴的和弦很不稳,放个一两天就又得重新调弦。用差的乐器绝对也会影响学习质量。所以我认为学习乐器并不存在所谓的“入门琴”的概念,要买就得买好一点的,这样才能真正的入门。当然,买什么价位的乐器需要量力而行,否则如果买来只是放着吃灰的话就浪费了。
今年一共去了几个城市:
把 Galio 拉来腾讯后,战斗力爆表的他一下子就揽下了很多优化和重写组件和活动的活。对自己要求很高,又承担了很多的工作,所以他来团队不到半年就开始出现了失眠和头痛的症状。于是趁着五一,拉着 Galio 夫妇一起去了趟马来西亚。
为了避免写流水账,直接说说这次自由行的感受吧。
不过,到了美人鱼岛和仙本那之后,先前看的海景就完全不值一提了。作为沿海地区长大的孩子,见到这么清澈的大海依然深深地被震撼到了。
如果说海上世界已经足够壮观,那么海底世界就更是绚烂神奇了。这是一个完全不同的世界。海龟踱步,鱼群翱翔;珊瑚林立,海星蹁跹。第一次浮潜的时候,看到一大批鱼从脚下游过,我惊呆到下巴都掉了,以至于忘了咬住吸气管,猛喝了口海水。幸好第一次潜水选择在较浅的地方进行,不至于需要呼救。到了后面掌握了技巧了,就敢独自游到离船比较远的地方观赏海底世界。不过后面往船方向游回去时才发现自己和船之间已经隔了一片较深的海域,游的过程中只能看到光束从背后射向一片深渊,这时候才感受到一种无比渗人的深海恐惧感。
总体来说,如果你很喜欢大海,也很爱吃海鲜,那么来亚庇和仙本那是很不错的选择。
6 月份因为参加 GMTC 的缘故去了一趟北京。每次去北京都是去参加比赛或者演讲,所以北京给我的印象并不是很好,大概还跟唯一一段比较正式的感情就是在北京这个伤心地结束有关吧。GMTC 在国际会议中心举办,和国家体育馆非常近。所以 GMTC 结束后,和 GMTC 上认识的一帮朋友去逛了下鸟巢和水立方,顺便吃了顿小吊梨汤。之后在北京又逗留了两天,和平安的几个老同事决定在京城再转悠一下。正值酷暑(summer),所以我们决定去下颐和园(the summer palace)感受下两百多年的避暑胜地。比较有趣的是还在那里巧遇了即将赴美留学的 Chaoyang 。向来对帝王的豪宅没有太大的兴趣,所以关于颐和园的景色没什么好说的,看图就好。
比较让人印象深刻的是古人讲求的对称美。不仅建筑本身讲求对称,连布局也是严格对称的。这点在登上万寿山顶后以上帝视角往下看时能够明显的感受出来:
算下来我来北京已经逛过故宫、天坛、颐和园、雁西湖、王府井几个地方,下次再来估计只有长城能勾起我游玩的兴致了吧?
今年十月份组织了一场泰国清迈团建。我在两年前已经去过一次清迈,所以这次自然而然成了团队的导游。
对我而言,清迈是一个非常值得多去几次的城市。签证方便,东西好吃,重点是消费很便宜。如果你没有很强的购买欲的话,在国内定好酒店和机票后,再兑个一两千人民币就足够在清迈待上五天了。如果发现钱不够用,也能够找到很多ATM可以取款。我把这次的清迈之旅定义为我自己的佛系回忆之旅:基本就是重复一遍两年前的路线。本来还计划花两天跑去没去过的拜县,但大家担心行程太辛苦所以也取消了。
虽然是回忆之旅,这一路却发现了很大的区别。大概是受到了登革热的影响,清迈的夜市冷清了很多。原先在夜市有各种小吃和街头表演,这次去就只剩下一些衣服饰品的常驻摊位了,以至于我不断怀疑自己是不是去错了地方。还有,上回在清迈吃了一家泳池边的高逼格餐馆 Ruen Tamarind ,这次心心念念也跑去吃,结果发现泳池都被封掉了,只能在厅内就餐,完全没有第一次来的惊艳感。
说到美食,这回在泰国吃得最满意的是 Wangsingkham 路的 Wishing Tree 餐厅,因为餐厅在河边,风景不错,而且东西是真的好吃。同样因为登革热的原因,前来就餐的人只有我们这群不怕死的旅客。
当然还有必吃的网红甜品店 Mango Tango ,第二次来发现清迈周边多开了好多家,蹿红程度大概相当于国内的喜茶了吧。
永远维持不变的大概就只有清迈里的各种寺庙了。在泰国里有将近 95% 的当地人是上座部佛教徒,所以在泰国随处都可以看到各种壮观的佛教寺庙。所以每天我们几乎不是在找吃的就是在逛寺庙,中途累了就去做个 massage 。
在柴迪隆寺和两年前的自己来了张“超时空合照”:
洛杉矶之行严格上说其实是出差,主要的目的是跟 ABCmouse 的母公司 Age of Learning 公司商讨明年的运营计划以及技术交流。所以白天的时间我基本都是在 Age of Learning 的公司里头待着,给 Age of Learning 的 China team 讲关于 code standard、spine animation dynamically loading、input event system 等技术相关的东西。附图 4 在 Age of Learning 的临时工位,桌子是可以上下移动的。China team 非常体贴地专门为我申请了一台外接显示器。
第一次来美国,不可避免会遇到一些不适应的地方:
除去这些问题,加州的环境其实是挺舒适的。气候和深圳类似,不会特别冷,阳光充足,空气清新。
Age of Learning 的办公大楼在 Glendale 市,离 the Americana at Brand 很近,所以有一天晚上我们去那里溜达了一圈并和 Age of Learning 的老板们聚了个餐。虽然距离圣诞还有两周的时间,但商城已经被打扮得非常有圣诞气氛,在路上 Jerry 告诉我们 ABCmouse 里的商城的设计灵感就取自 Americana ,顿时有种走进了 ABCmouse 世界里的奇妙感。
Glendale 也离 Chaoyang 正在就读的 USC 不远,所以有天晚上我打了个 Uber 去跟他续了叙旧。Chaoyang 是我在百度认识的同事何朝阳(人称何老师),后面我们都相继来了腾讯,之后他又放弃了腾讯的 leader 岗位举家来 USC 读博。我对他的求学精神深感佩服。巧的是,刚好碰上他当晚要赶飞机回国实习,所以我们只能找个就近的餐馆吃个饭。我开玩笑说花了六十刀的打车费去 “University Surrounded by Chinese” 就为了找他蹭了顿十刀的饭。
完成了我们在 Age of Learning 里的 agenda 后,Neal 带着我和 Yul 去他在 Irvine 的房子玩,我也总算见识到了在美国住 house 有多爽:Neal 的房子总面积有两千尺,两层楼,总价格却只有我的房子的价格的一倍。相比之下,我的房子只是个六十平米的两房 department 。不过,在美国有钱的人喜欢住郊区,开车到上班的地方普遍要一两个小时。如果按这个距离算的话,到惠州、东莞买个 house 好像也可以达到类似的目的,但缺点就是学区不好。
在 Neal 家住了一晚后,和 Age of Learning 的小伙伴们一起去了趟 Disneyland 。沾了 Age of Learning 的光,除了过了机动游戏的瘾之外,我们在 Disneyland club 33 吃了顿大餐,这是一个隐藏在 Disneyland 里的西餐馆,只有会员才能进入。据说,想成为 club 33 的成员,不仅得交得起昂贵的会员费(坊间传言入会费为 2.5 万美元,每年年费为 1.2 万美元),而且等候入会的名单已经安排到十多年后了。说白了这就是一个饥饿营销的典型成功案例。
入会条件既然如此苛刻,里头的环境和服务就必须能足够与之相衬才行。实际体验下来,确实很有特色。比如会有个专业的合唱团在你周围唱歌给你听;另外,对私密的要求很高,不允许拿起手机拍摄周围的环境,只能拍自己或者菜品。
至于味道……毕竟美国的西餐比较符合西方人的口味,所以中国人吃起来并不会觉得多么可口。加上我今年五月份曾经在 FutureOne 吃过了一顿又贵又尴尬的西餐,所以我对西餐没有多少好感。
当然在 Disneyland 的重头戏还是那些机动游戏。但因为时差的原因,我到了下午依然是困得睁不开眼。所以我就一边排队一边站着打瞌睡,然后再利用各种 roller coaster 或者其他刺激项目提神,效果不错 😹 。附图 5 Golden State Free Way,这里是电影《爱乐之城》第一段公路舞蹈《Another Day of Sun》的取景地。
回国的最后一天,Jerry 开车带我们在 LA 兜了一圈。我们沿着 Golden State Free Way 开去了一个叫做 Sunset Ranch 的农场,近距离打卡拍下了 HOLLYWOOD 的标志。
总的来说这次的 LA 之行最大的收获在于拓宽了我的眼界,了解了美国人的生活和工作方式,也让我设身处地的思考如果我在这边生活能不能适应下来。从目前来看,美国的生活方式其实挺适合我这种晚上不爱出门闲逛的人。而交流方面,虽然我的口语并没有特别厉害,但理解和用简单的英文回答是没有问题的。比如,在打 Uber 的时候我能够一路跟一个埃及的司机 Hishan 交谈彼此国家的文化、交通、政治,还跟他分享了来中国旅游的建议。当然我目前还没有感受到移民工作的必要性,一方面我还没有离开目前团队的打算;另一方面 H1B 的限制太大,在拿绿卡前一直不能回家见父母其实是一件很难熬的事情。
我喜欢的游戏类型比较窄,主要爱玩 PC 上的角色扮演类游戏,而且我并不会把玩游戏作为平时的固定娱乐项目,只是突然心血来潮了就玩一下游戏,等玩上一段时间后,感到满足了就可以很长时间不玩。
今年的总结之所以想分享有关游戏的话题,是因为我终于在今年入了 WeGame 和 Steam 的坑。在这两个平台上一共买了 5 款游戏。
我是河洛工作室的粉丝,武林群侠传一直是我最喜欢的游戏。8 月份的他们的金庸群侠传续作《河洛群侠传》在 WeGame 上开始预售,于是我就二话不说就下了个 WeGame 购买。但距离游戏的真正发布要等两个月之久。心痒难耐之际,我看到河洛推荐了一款《天命奇御》,风格和武林群侠传类似,但战斗系统却是即时战斗的,这比武林群侠传的棋盘战斗要有趣很多(有意思的是这个游戏的开发商甲山林娱乐本身是一家台湾的知名房地产开发商)。于是我又在 WeGame 上买了这款游戏,作为《河洛群侠传》的前菜。
实际玩起来,《天命奇御》的确是一款很有意思的游戏。这个游戏提供了类似《侠客风云传前传》的传闻系统,有着丰富的支线剧情可供挖掘。最有趣的就是物品出示系统——通过收集某个物品并出示给特性NPC可以触发特定剧情。
反而让我感到失望的是《河洛群侠传》,贴图粗糙,建模丑陋,而且战斗系统也设计得很糟糕。游戏刚发布的时候,优化做得非常差,以至于经常出现内存泄漏和卡顿的问题。后面通过升级有了很大改善,但各方面细节的不足已经是一个无法挽回的问题。比如画面总是给人一种糊糊的感觉:
还有战斗系统的设计也很一言难尽:使用手柄操作移动位置非常繁琐,误操作了又不能撤销。
游戏的最大亮点在于它是一个非常开放的沙盒游戏,整个地图所有场景都是无需读条和场景切换就可抵达,这大幅提高了游戏的代入感。在玩的过程中不难让人回想起沙盒游戏的其他代表作品。我曾经在四年前尝试了一次《上古卷轴5》(当年下的D版),但直接被河木镇的鸡神劝退了。
但因为有了《河洛群侠传》的基础,也因为对《河洛群侠传》的不满,我决定弃坑,重新玩回《上古卷轴5》。于是,趁着黑色星期五 Steam 促销,我又注册了个 Steam 账号,买入了《上古卷轴5》。
与《河洛群侠传》相比,《上古卷轴5》就是一个非常成熟的沙盒游戏。贴图细致,建模精致,而且有着更高的自由度。比如,在《河洛群侠传》里,战斗只在特定的条件下触发,你是不能随便去攻击 NPC 的。再比如,盗窃只会降低道德从而影响后续的剧情发展。而上古卷轴里,你能够随意攻击任何一个 NPC甚至屠城,而随意攻击NPC也会迁怒整个城镇的人从而攻击你。另外盗窃或者深夜潜入别人的房子如果被发现也会被满城通缉。
除此之外,《上古卷轴5》还提供了非常多的有趣设计,比如当你在某个城市完成了当地领主委托的一些任务后,你就可以在当地购买或者建造房子。
有了房子之后,你可以带着祖拉项链去跟喜欢的 NPC 求爱并结婚,甚至还可以领养小孩。上古卷轴5把求爱过程设计得非常简单粗暴:碰到对你感兴趣的女孩,她会直接问你是否对她有意思,如果你回答“是”的话。你们就可以去登记结婚了😹……相比之下现实生活中的恋爱就复杂多了,直接告白往往难以奏效,多数人反而更喜欢玩欲擒故纵的套路,而且双方最终能否在一起要考虑非常多的因素,家庭背景、教育水平、兴趣爱好、沟通方式……所以爱情对我而言是一种接近玄学的存在。
当然,游戏更重要的还是剧情本身,《上古卷轴5》本身是架构在中世纪背景的,所以有着诸多和《指环王》、《权力的游戏》相似的魔幻元素:龙、精灵、巫师、骷髅、幽灵、雪怪、吸血鬼、狼人、巨型蜘蛛等。此外游戏的主线和支线也是多到令人发指,每条主线细挖下去都可能会碰到一个强大的魔神要应付。在英灵殿迎战超强BOSS——巨龙奥杜因是游戏里头最振奋人心的时刻之一,在龙裔DLC中学会了意志屈服
后御龙飞行又是一个成就感爆棚的时刻。
如果说这个游戏的缺点,除了上手难度大、任务引导缺乏之外,大概就是太花时间了……我在这个游戏里已经花了 60 多个小时的时间,而且好像还有很多主线没有去完成。而我在游戏方面的兴致持续得比较短,也不太愿意投入更多时间,所以目前已经停玩了。除了上面提到的三款游戏,我其实还买了《巫师3》和《古剑奇谭3》,但也因为兴致和时间原因没有玩。毕竟“我都花钱买游戏了,为什么还要再花时间去玩?” 😄
大概因为睡眠不足的原因,来腾讯半年不到,我就虚胖了十斤。有段时间我几乎不敢发朋友圈照片,因为得到的评论清一色都是这样的:
痛定思痛之下,我决定减肥。一开始是采用晚上不吃饭改吃水果的策略,但发现收效甚微。于是开始跑步。一开始我的跑步方式不正确,节奏过快,跑不到一公里就已经累到不行了。直到有一次被 Jolt 叫出去一起跑,才打通了任督二脉:他的理念是要控制好节奏。好的节奏就是在跑的过程中能自如谈话。经他这么一调教,我周末回家一次就跑了六公里,成就感爆棚。
为了控制自己的体重,促进新成代谢,我开始尽量避免喝冰饮,改喝黄芪水。同时为了改善自己的睡眠,我把原来的弹簧床垫扔了,换成了乳胶床垫。如果晚上睡得比较晚,我还会吃一到两片褪黑素。这些或多或少都改善了一些睡眠质量。
不过从全年来看,我的日均睡眠质量依然很少。只有在年初春节期间睡眠达到了8个小时以上,这是因为我是一个对睡眠极度缺乏自律的人,总是拖延睡眠时间去做各种事情,例如一写起博客或者业余项目的时候就容易停不下来,搞到大半夜,而后又很容易因此兴奋得睡不着觉。在家的时候因为有老妈监督,所以就不会轻易熬夜。所以将来结婚对我而言其中一大收益应该是能多一个在身边督促我去睡觉的人吧。Anyway,希望明年我的日均睡眠时长能够至少提高到6个小时以上。
除了跑步之外,在夏天的时候我会尽量腾出时间保证每周去游一次泳,冬天则是每天饭后和同事绕着荔香公园或者深大走一圈步。为了督促大家运动,我们团队定了个规矩:每周步数少于四万步的罚红包。于是饭后刷步数就成了避免挨罚的“例行公事”。
经过这些改善后,我的体重下降了 5 斤,目前维持在 120 斤左右。我的朋友圈也总算摆脱了“发福”、“双下巴”的标签。
今年开始用上了微信读书,所以大部分的书都是通过手机来阅读。不过,这依然没有显著提高我阅读的数量:今年看的书依然少得可怜。如果把三体的三集算成三本的话,今年一共也就看了十本书。我只能用“看书在精不在多”来安慰自己了。按照喜欢程度由高到低的顺序往下评论吧:
三体:终于把《三体》看了一遍。第一部大致讲述三体文明发现了地球文明,于是派出舰队意图侵略地球,并在舰队到达地球前通过智子技术限制地球科学的发展;然而三体人存在不善计谋的致命缺陷,于是在第二部中人类派出了四个面壁者来意图瞒过三体人的监视并阻止三体人的侵略,而三体人也在地球人中找到破壁者来识破面壁者的计划。最终其中一个面壁者罗辑成功地造出了能够在宇宙中通过引力波曝光地球和三体文明的开关,并利用黑暗森林法则来威慑三体人停止侵略;在第三部中,人类面对三体人的卷土重来终于启动了引力波向宇宙广播了三体文明和地球的位置,最终一起走向了被降维打击的命运。当然三言两语只能把基本的故事剧情讲得索然无味。要真正了解故事细节,依然推荐亲自阅读本书。全书充满了各种geek们一看就懂的梗,比如巴赫的音乐,牛顿-莱布尼茨公式的梗,对冯·诺依曼的人肉计算机模型工作原理的介绍堪称精彩绝伦。可见作者是有比较好的物理和计算机功底的。另外书里提出了黑暗森林法则、宇宙社会学等宇宙间生存的要领。相比《银河漫游指南》的 don’t panic 而言,大刘的这些法则显得更冰冷、残酷而现实,但却更有说服力。
小狗钱钱:今年开始下意识地读一些关于理财相关的书籍,《小狗钱钱》就是一本非常值得推荐的科普读物。和其他的说教式书籍不同,这本书把理财知识写成一个童话故事,故事中的主人公吉娅跟她捡到的一只受伤的猎狗money学会了如何理财。通过这本书我了解了基金定投的基本操作概念,也开始定投起了基金。此外我非常喜欢书里的一段剧情:吉娅在学会了一些理财知识后,收到了银行职员海内女士的邀请,去给其他小朋友分享她的理财知识。起初她因为担心怯场而想拒绝。这时一个老者说了一句话改变了她的决定:
我生命中出现了最美好的东西,是因为我做了原本不想做的事情。
回顾这一年,大会分享、课程讲师、技术负责人……我在工作中也出现了各种各样的支线任务可以选择去做。而我除非不乐意,否则通常都会选择去做,因为我不知道会有什么美好的收获。
富爸爸穷爸爸:Jolt 吐血推荐的书《富爸爸穷爸爸》。早在以前也经常有人推荐这本书,但一直以为又是一本成功学的书,再加上听说写这书的作者也破产了,所以一直没看。其实这是一本讲如何培养财商的书。从小到大我们受到的教育都是穷爸爸的教育“好好读书,将来找份好工作赚钱”,所以我们都被培养成为钱工作。这本书讲的是富爸爸的教育:如何让钱为你工作,成为钱的主人。虽然读完未必会暴富,但多一些富爸爸的思维,少一些韭菜思维和小鸡思想是很有益的。内容不厚,四个小时左右就可以读完。
月亮与六便士:讲述了查尔斯·斯朱兰人到中年放弃了自己让人羡慕的银行家的事业以及美满的家庭选择去当画家的离奇一生。从世俗的眼光看来,查尔斯是一个极其让人讨厌的奇葩。但他丝毫没有受到外界的指责的影响,依然执着地追求自己的理想,甚至不惜为此忍受饥寒交迫的生活。从这个角度来看,他又是伟大的。想起了漫威之父 Stan Lee 对年轻人的告诫:
I think whatever you do, you should do what you most want to do.
看完这本书我也不禁在反思自己想要的究竟是什么,目前而言,通过写一些方便自己和他人的技术工具并开源分享也许是最让我感受到乐趣的事情。从这个角度上来看,我是幸运的,因为我的最主要兴趣和我的本职工作相关,我还不至于为了追求自己的“月亮”而放弃现在能赚到“六便士”的生活。
年糕妈妈轻松育儿百科 :今年我哥生了个女儿,我们的大家庭迎来了一个新的生命。抱着小侄女的时候,我感受到了一种生命奇迹带给我的震撼和感动。儿童在初生阶段的养育方式正确与否可能会对将来她的一生产生着微妙的影响。所以为了帮助哥嫂分担,我阅读了一些育儿书籍。《年糕妈妈的育儿百科》就是其中一本我认真看完的工具书,通过这本书了解到了不少育儿的宝贵经验,并在适当时候分享给哥嫂,纸上谈兵地提供了一些参考意见,也顺便为我将来带娃做一个预热吧。当然,我需要先解决脱单问题了再说。
半小时漫画中国史:以漫画的形式和现代化的语言重新解说历史,形式新颖,内容也很风趣幽默。缺点就是为了用半小时讲完中国史,内容作了极大地压缩,读起来感觉始终有种快餐感,就像是在突击期末考点大纲。另外,为了尽可能有趣,人物的刻画也做了脸谱化处理,阅读过程中就像是在用上帝视角看每个人物角色依次登台唱戏然后领个便当回家——你并不能指望通过本书了解这些历史人物做某件事的背后有什么复杂的心理动机。相比之下,一些历史题材的小说则用的是VR视角——例如当年明月的《明朝那些事儿》就把明朝的人物刻画得栩栩如生,阅读过程中置身在明朝的世界里,近距离观摩每个人的行为,解读他们的思想。所以,如果要更细致的了解历史,精读某个具体朝代的相关书籍依然是必不可少的。当然,《半小时漫画中国史》的目的也不在此,相反它只是希望用极简的篇幅带读者了解一些历史重点事件。从这点来看,它是成功的。
非暴力沟通:看到标题就被吸引了。中国人比较注重婉转,过分担忧对方的感受,其实反而给沟通带来了障碍。这本书重点讲述了一种更加有效的沟通方式:观察->感受->需要->请求。并且提倡倾听的重要性(而这是我的弱项)。不过,要在日常中合理应用存在一定的难度,需要多多练习。
韭菜的自我修养:书很薄,一个小时就能看完的篇幅,但定价19.9有点小贵。前面关于投资相关的内容也比较老生常谈。不过,既然是写给韭菜看的,确实也针对性了提出了不少韭菜的思维误区。有则改进,无则加勉。还是有所收获。附图 6 网播节目《一本好书》
顺便推荐《一本好书》这档网播综艺节目。这个节目的亮点在于通过舞台戏剧、片段朗读、影像图文插播等手段,还原了《三体》、《月亮与六便士》等11本经典作品的经典场景。我是先看了《三体》的书再看的综艺改编,一边看一边惊叹导演组的改编再创作的能力。《三体》的故事非常庞大,仅一集网播节目的篇幅肯定讲不完。所以导演组选取了第二部面壁者和破壁者之间对弈的桥段进行改编。有意思的是,为了突出重点,三个破壁者被合成了一个人,另外还通过插播了早前网友自制的水滴概念视频,很好地表现出了三体人的科技水平。而饰演罗辑的赵立新的表演就像在TED演讲一样,把整个浓缩版的故事串得非常清晰,也把宇宙社会学讲述得非常透彻。
看完《一本好书》后我也对节目中介绍的其他书来了兴趣,《月亮与六便士》就是我在这档节目播出后开始阅读的。明年希望能把这季的《一本好书》中的书单都看完。
最后是感情生活。今年三月份我其实有很认真地谈了一段恋爱,然而从开始到结束只有短短两个月,对我打击非常地大。虽然事后还是做了不少挽回的努力,到了后面对方似乎也没有明确的拒绝了,但自己反而突然有了一种莫大的压力,怕再次搞砸,所以再也没敢约出口。有时一个人的时候会责备自己为什么不再约,为什么不厚着脸皮磨下去,但被拒绝的滋味又实在太难受。何况当周围的人都一致建议放弃的时候,放弃就成了一个非做不可的选择。
事后我一直在反思自己究竟为什么没有成功。其实周边的人一旦知道我单身,多数都会表现得非常诧异,因为我并不像是一个会注孤生的人。实际上一个人感情是否顺遂和他的条件并没有必然的联系,否则就不会有很多钻石王老五的存在了。我想我就是属于那种特别不适合谈恋爱的人。我的业余兴趣不管是乐器、看书、写博客、业余小项目都比较宅,平时也没有一个人到处去玩的爱好和动力,所以一旦要约女孩子出来,就会为了去哪吃和去哪玩发愁。另外我追女孩子的手段简单而愚笨,居然会想到用情书来表白。以至于后面的约会对方变得不自在了起来,而我又没有能力去化解这种不自在。我就像《月亮与六便士》里的德克,即使付出了全部的感情,也始终不能赢得布兰奇的芳心。
有些人的给建议是先随便找个人发展一下感情,锻炼下这方面的技能。这样遇到真正喜欢的人就不会表现得太怂。但我觉得这样自欺欺人的方式太流氓了,对待感情我还是坚持宁缺毋滥的态度。我也挺反感用一些欲擒故纵的套路来“泡妞”。喜欢就是喜欢,为什么非要假装不在乎?相比之下,我似乎更适合活在《上古卷轴5》的世界里,至少那个世界的人们从相识到结婚没有太多的套路。然而真实情况是,人们对越容易得到的东西越不会去珍惜。就像陈奕迅《红玫瑰》里的歌词:
得不到的永远在骚动,被偏爱的都有恃无恐。
所以我有时候会冒出用技术手段来帮助恋爱的念头,比如智能 match 合适人选,用 NLP 分析 IM 里对方的应答,提供化解尬聊的话题建议,或是自动根据双方喜好预定餐馆和菜式。无奈当前的 NLP 技术还不够成熟,而且这样的话似乎双方并不是真正地在交往,而是变成由机器人来确定恋爱关系——人在这场恋爱中反而成了傀儡了。就像《黑镜》第四季第 5 集《Hang the DJ》中的桥段,过度依赖技术来培养爱情其实是很容易弄巧成拙的。
不过这段感情对我而言还是带来了一定的收获的。除了反思自己在处理感情方面的天真和稚嫩之外,更重要的是更确定了自己喜欢什么类型的女孩子。亲戚朋友后面又陆陆续续介绍了不少女孩子,条件都很不错,但都因为没有那个人所带来的难以言喻的美好感觉而没有选择进行下去。大概我是很执着于第一感觉的人,感情如此,处理其他事情也这样。失恋后的那一周,心情糟糕得很,于是决定去看看房子散心,没想到一天不到就看上了一个房子,当天就谈好价钱并签了合同。我大概是周围所有人中看房最快的人了。
不论如何,没有伴侣其实我一个人也能生活得很充实,所以恋爱和婚姻对我而言并不是特别迫切的东西。如我所说,我还是抱有宁缺毋滥的态度。只是一个人难免会有孤独的时候,而且会开始怀疑自己究竟在为了什么而忙碌。关于恋爱观,我比较赞同 pluskid 的观点,在这里直接摘抄一段出来:
关于恋爱观,我也有考虑过一点。也许将来的想法会改变,不过现在我的态度是恋爱婚姻这些东西并不是人生目标甚至不是人生必须要经历的的一个 milestone,所以我大概不太会刻意去追求,当然也并不持反对态度。对我来说一个人和两个人大概就是不同的生活方式吧,一个选择问题,孰优孰劣无法定论。客观来讲我一个人就能开开心心地过,事实上我的生活看起来好像是非常充实的,甚至有时候非常亲密的朋友也会觉得我好像就不可能有空虚无聊的时候,然而事实并非如此,我想这大概就好比你再有钱也会有买不起东西的时候吧?——不同的人会有不同的烦恼。同理,两个人的生活也是有取有舍吧,我一点也不否认爱情的强大魔力显然也是这宇宙中独一无二的一种存在。
年底的忙碌程度超出了我的预期。本来希望赶在 2018 年的尾巴写完这篇总结,结果到接近写完的时候,2019 年的 1 月都已经快结束了。Anyway,2018 年依旧是非常充实的一年。有很多新的尝试和挑战。我还在年初组了一个合唱团,并当了一回合唱团的领唱,还进了次录音棚录唱。还有六月份终于跑去听了场喜欢了十三年的偶像王力宏的演唱会,唯一的遗憾是同行的人由喜欢的人换成了自己的哥哥。
19 年有几个目标:
至于爱情,随缘吧。
]]>年末,又到了很多朋友准备答辩晋升 slides 的时候。PPT工程
应该是很多技术人的天敌之一,不少人为了在工作之余折腾出一份既有内容又美观的 slides 弄得焦头烂额。
其实,大家对 PPT工程
之所以这么惶恐,主要是因为技术人更习惯跟代码打交道,更习惯用技术思维去解决问题。而做材料考察的是信息的提取整合能力、审美品味以及口头表达能力,而这些都不是做技术的必要条件。
然而,也不必对使用 slides 答辩过于深恶痛绝,它只是一个用来说你的故事的工具:我们不应该被工具所束缚,而应该把它用好,将它变成你的优势。我私下里也看过不少同事的答辩材料,发现了一个很有趣的现象:往往内容非常好的材料,美观度也很高,看起来赏心悦目。而内容一般的材料,排版、美观度也比较糟糕,缺乏美感。工程师的素养,在做 slides 花的心思上面也能够体现出来。
其实,做一个大方美观的 slides 并不简单,我在准备今年年中的答辩的时候,花费最多的时间居然是工具和模板主题的选择上。
做 slides 有非常多种选择,PowerPoint、Keynote、LaTeX+Beamer、reveal.js、Emacs org-mode、Jupyter Notebook……抛开过于花哨的 H5 系和不顺手的 Keynote,我的选择就在 PowerPoint 和 LaTeX Beamer 上。
LaTeX + Beamer 一开始是我的首选,因为 Beamer 的主题大方简洁,非常适合用作学术汇报和晋升述职。这是 beamer 的官方主题:http://www.hartwork.org/beamer-theme-matrix/ 可以看到这些主题全都非常精美大方。
我也用 Beamer 做过非常多的 slides ,对它的排版效果可以说是爱不释手:https://github.com/wzpan/wzpan.github.io/wiki/slides
正当我兴高采烈地写了很多页之后,我发现我忘了一个很严重的问题:Beamer 不支持直接插入视频。要播放视频,你需要先将视频转成帧图像,然后使用 animation 包来实现播放,而且只支持 Adobe Reader。体验非常糟糕。
于是我放弃了 LaTeX ,转回使用 PowerPoint 。但是我又花了非常多时间在寻找一个好看简洁的模板。我发现一个看上眼的都没有。
网上找了一些模板,都太过华丽,不符合工程师的气质:
最终我决定复刻一个 Beamer 的主题作为模板,有了一份称手的模板,写 slides 真的是效率大增,最终的效果个人是挺满意的。这里挑其中几页做个示例:
为了让像我一样审美较差的码农也能用 powerpoint/keynote 做出媲美 LaTeX Beamer 的 slides,成为一名合格的PPT工程师,轻松应对答辩晋升、技术分享和学术交流的任务,我从答辩结束后就开始了一个名为 BeamerStyleSlides 项目:对 Beamer 的更多官方主题进行复刻,并且在 Github 上开源:https://github.com/wzpan/BeamerStyleSlides 。这就是 BeamerStyleSlides 的由来。
另外,选择在 Github 上开源还有一个目的:就是让更多的人参与进来,贡献他们的模板。有很多人私下里收藏了一些非常美观实用的模板,与其独享,不如共享。
BeamerStyleSlides 是 LaTeX Beamer 风格的幻灯片模板集。包含了PowerPoint和Keynote两套格式。尤其适用于晋升述职、技术分享和学术汇报。
*.key
和 *.pptx
文件进行版本控制。所以,在克隆前,要求先 安装好 git-lfs 。1 | git clone https://github.com/wzpan/BeamerStyleSlides.git |
所有主题的风格尽力与 Beamer 所提供的风格保持一致。可以在 beamer-theme-matrix 中查看对应的主题样式。
不过,我做了一些取舍:
要注意的是模板并没有做到 100% 遵循原始的效果,毕竟 PowerPoint、Keynote 和 Beamer 各自的支持的能力有差异。我更希望做到的是在保留 Beamer 原有的风格的基础上因地制宜地做些调整。
BeamerStylesSlides 的目标是复刻 Beamer 上的 252 套主题,而因为个人精力有限,目前大概以每周两套主题的龟速,暂时先完成了 51 套主题的复刻。如果你对这个项目感兴趣,欢迎 fork 这个项目并参与复刻。
参与方式:
1 | brew install imagemagick |
theme
和 colortheme
分别修改为你要参与复刻的主题 collection 名和 colortheme 名:1 | \usetheme{Antibes} |
然后使用 xelatex slides.tex
编译得到 slides.pdf 文件。3. 对照这个 slides.pdf,编辑一个 pptx 母版,使之在排版、配色、列表项风格上尽量接近于这个 benchmark 。完成后保存,并导出一份 pdf 文件,以 colortheme + .pdf
的命名方式放进 preview 目录中,例如 lily.pdf
。4. 使用 keynote 导入这个 PPT,调整下个人经历这一节中的层级即可另存为 .key
模板。
5. cd
进入 preview 目录,执行如下命令生成封面图和预览图:
1 | sh montage.sh theme名 colortheme名 |
例如:
1 | sh montage.sh Antibes lily |
1 | [-] ~/Documents/projects/BeamerStyleSlides/ |
最后,祝愿参与晋升答辩的朋友都能取得最终的胜利!
]]>如果你对微信小游戏开发感兴趣,欢迎前往观看。
(PS: 请忽略散发着寒光的头像……)
学习这门课之前需要先了解 Cocos Creator 的基本使用,以及具备一定的 JS 语法基础。如果你对这些没有足够的了解,那么看这门公开课会比较吃力。
如果你是这种情况,或者学完觉得意犹未尽,希望学习更多内容,那么我建议你可以去学习一下我们的另一门付费课程《微信小游戏入门与实战》,这门课汇集了我们团队在使用 Cocos Creator 开发微信小游戏上的经验,由我们多名成员在业余时间整理录制而成(我已经数不清为了达到质量要求而修改重录了多少遍 T_T
)。课程将会提供更全面而系统的教学内容。
有多全面而系统
呢?可以感受下我在项目初期为这个课程系列设计的教学大纲。严谨起见,这里加个免责说明:大纲只起到初步划定教学内容的作用,最终呈现的课程内容请以实际视频为准,因为各章讲师在讲解时会根据个人喜好做些调整。
]]>
我作为讲师嘉宾也参与了这次大会,分享了《基于 Cocos 的高性能跨平台开发方案》。
如下是整理后的演讲正文。
大概从去年九月份开始,我们选择使用Cocos来作为我们一款产品 ABCmouse 的跨平台应用开发方案,在这个过程中,我们做了一系列的优化,也踩了一些坑,本文将对这个过程做一个回顾和总结。
本文的内容主要分三块来讲。首先简单介绍一下项目背景,接下来具体介绍下我们的实践过程,分享一些经验。最后给出新的开发方案和以前的效果对比。
附图 1 现场观众首先介绍一下我们的产品,ABCmouse 是美国知名的儿童英语在线学习领导品牌,在美国有超过百万家庭在使用,也获得了7万多个教师的推荐。
这个应用采用的是典型的 Hybrid App 跨平台开发方案,里头基本全是 H5 的页面。附图 2 Apollo GraphQL的开发者 Sashko Stuballo 也来了
Hybrid App 最大的问题就是性能问题,用户经常会在页面加载上等待非常多时间。
我们统计了 ABCmouse 各个场景的平均加载耗时,发现平均都要花费大约三到四秒的时间。漫长的等待时间也对用户的学习积极性带来影响。
从去年九月份开始,团队与 ABCmouse 的研发公司 Age of Learning 公司开展了战略合作,我们希望能够开发出一款针对中国儿童的英语学习应用——我们称之为 ABCmouse 腾讯版。我们希望它能提供更符合中国儿童使用习惯的学习路径,并在里头融入腾讯的社交元素,从而带动儿童外语学习的积极性。附图 3 在GMTC上遇到很多老朋友
从技术上,我们希望新版的 ABCmouse 能够在表现力、性能、效率和社交四大方面都能有更好的表现(这里的表现力指的是产品的界面和交互,能够做到更吸引中国的小朋友)。
通过初期技术预研后,我们决定使用 Cocos 来改造这个项目:
在具体实践这一块,我准备分成架构篇、甜头篇、踩坑篇、优化篇四个部分来介绍。附图 4 新认识的一帮来自腾讯、Facebook、Twitter、UC、搜狗的小伙伴。我们开玩笑说互联网社交圈快凑齐了。学会一个新词儿,叫做“局气”。
一图胜千言。我们整个系统架构可以用这张图来概括。
我们自底向上看,最底层是 native 层,Cocos2d-x 开发框架,在这一层提供了对 JavaScriptCore、SpiderMonkey、V8、ChakraCore 等多种可选的 JS 执行引擎的封装。在这基础上又架设了一层 JSB ,主要起到桥接作用。我们的应用也在底层封装了多种基础能力,包括支持直出的webview、自定义的视频播放器、音频播放器、支付、推送等。
再往上是 JS 层,在这一层 Cocos 提供了丰富的开发组件和 API,我们也扩展了多种组件,包括一些通用的UI组件、一个多端通用的音频播放器、一个带缓存和内存回收功能的图片加载器、常驻节点、上报、日志等组件。有些组件是依赖 native 层的。
Cocos 层和 Native 层就通过 callStaticMethod 和 evalString 来完成互相调用。
有了这些基础后,再往上则可以开展具体的场景开发了。
为了帮助大家更好地理解 Cocos 的跨平台原理,我们可以拿 Cocos 的渲染原理和 React Native 做一个对比。
Cocos 的渲染原理是在 UI 线程将场景文件理解成场景树,然后交给 GL 线程渲染。也就是说,用户看到的大部分场景都是使用 OpenGL 或者 WebGL 绘制的,即使在不同的平台,也能够有完全相同的表现。
而 React Native 的渲染原理是将 JS/JSX 理解成 Virtual DOM,然后调用各自平台的 Widget 。由于不同的平台,底层的 Widget 表现是不同的,因此使用上可能会存在差异。这也是 React Native 为人诟病的一点。
采用 Cocos 作为我们的跨平台开发框架后,我们尝到了不少甜头。
首先是跨平台带来的便利。我们使用一套代码可以生成到安卓、iOS、Web、微信小游戏等多种平台,并且在多个端达到了高度一致的体验。在 React Native 上经常遇到的 UI 体验不一致的问题,在 Cocos 开发中基本没有遇到过。
由于Cocos支持构建小游戏版本的应用,所以我们的项目也提供了小游戏版本。上周末已经有很多爸爸在微信小游戏里收到了他们的孩子使用 ABCmouse 制作的贺卡。值得一提的是,小游戏版本是我们两个开发在花了一周左右的时间内移植完成的。这里头主要的移植工作在于接入微信小游戏的登录授权,接入 VideoPlayer 和 InnerAudioContext 以分别支持视频播放和音频播放。
第二个甜头是开发效率的提升。
首先,Cocos 提供了可视化的 Cocos Creator ,使用它来管理和构建工程非常轻松。
其次,设计萌妹子也能直接使用 Cocos Creator 编辑动效,输出动效资源给开发,提高协作效率。
另外,Cocos Creator 支持直接在浏览器中预览调试场景,节省了大把构建编译的耗时。
第三个甜头是热更新带来的便利。
附图 6 颐和园摆渡Cocos 同时支持脚本和资源的热更新,这给我们修复线上问题、发布运营活动带来了很大便利。
此外,Cocos 的热更新可以做到 hot reload,无需冷重启,很好的保证了用户的体验。并且,Cocos 的热更新支持高度可定制,可以很方便的定制满足业务需要的热更新流程。
第四个甜头是 Cocos 提供的强大的社区支持。Cocos 的开发团队来自中国,有着非常活跃的中文社区。
另外,使用 Cocos 开发小游戏也成了最主要的方式,可见 Cocos 的受欢迎程度,也侧面证明了这套开发框架的生命力。
跨平台开发虽然方便,但是在一些具体的实践中难免也会踩到坑。
首先,Cocos 主要是面向游戏开发的,要使用它来开发应用,少不了需要开发一些 UI 组件。因此,我们在 Cocos 层开发了一系列的通用 UI 组件,包括对话框、选择器、表单、按钮、toast、loading 等组件,这些组件遵循一套规范化的接口标准,使用起来非常便捷灵活。
开发完 UI 组件后,我们发现这些组件的加载也存在问题。和原生应用开发不同,这些UI组件本质上都是挂载在场景里头的节点,如果没有调度的话,可能存在同时弹出多种弹窗和对话框的情况,整个场景就会变得很混乱。
为了解决这种问题,我们写了一个针对 Cocos 的弹窗调度器,统一由它来调度弹窗,避免了弹窗的混乱。
我们接下来遇到的另一个坑是 VideoPlayer 的置顶问题。
前面提到,Cocos 的场景是在 GL 上绘制的。例如,对于 Android 平台,Cocos 开启了一个 OpenGL 的 SurfaceView 来进行场景绘制。而这个 GLSurfaceView 不能直接支持渲染视频,所以,Cocos 提供了一个 VideoPlayer 组件用于播放视频。这个 VideoPlayer 是独立且置顶的一层。
这带来的一个问题是:无法在视频上绘制 UI 。
比如我们希望视频播放器里头能加上我们自定义的按钮、进度条,如果是直接在 Cocos 层对 VideoPlayer 进行封装的话,会发现这些 UI 元素会被视频本身遮盖,达不到定制界面的目的。
最终我们放弃了直接使用 Cocos 提供的 VideoPlayer 组件,而是在底层为各个端开发视频播放器,并各自实现界面的定制。
视频播放问题解决了,我们又遇到了音频播放的问题。
由于应用中有非常多的音乐、音效、语音,为了减小包大小,大部分的语音素材放在 CDN 上,需要的时候才从 CDN 上拉取播放。少部分常见的音效会直接打进应用包中。而 Cocos 自带的 AudioEngine 组件在 Native 端只支持本地资源的播放。因此,我们又封装了一个跨平台的音频播放器,可以自动根据指定的音频路径决定使用播放方式:
由于对外的接口只有一套,开发者无需考虑具体的平台和底层播放器的选择。并且可以使用同样的接口来统一管理不同的音频。
最后我们遇到的一个比较严重的问题是 local reference table overflow error 问题。
为了复用 Native 端的能力,我们在 Cocos 层大量地使用反射机制来调用 Native 端提供的方法。然而,我们经常会遇到 local reference table overflow error 错误导致的界面卡死问题。
1 | A/art: art/runtime/indirect_reference_table.cc:138] JNI ERROR (app bug): local reference table overflow (max=512) |
最初,我们怀疑是反射调用使用得太频繁导致。因此,我们对诸如打 log、事件上报等 Native 方法进行了频率限制,例如使用缓冲的方法将多个 log 合并后再打印。
然而,虽然这个做法减少了界面卡死的发生,但依然没有彻底杜绝问题的再次出现,就像是一个定时炸弹一样,威胁着我们应用的稳定性。
通过阅读引擎的代码,我们发现 Cocos 的引擎在反射阶段处理字符串参数时,使用了 NewStringUTF()
方法将其转换为 JNI 层的字符串,然而在调用执行完成后并没有相应地使用 DeleteLocalRef()
释放该字符串的引用,从而导致了引用表的溢出。
1 | static bool JavaScriptJavaBridge_callStaticMethod(se::State& s) |
了解到这个原因后,我们给 Cocos 的引擎提交了一个 pull request,修复了这个问题。
虽然 Cocos 比起纯 Hybrid 的方案在性能上已经占据了优势,但是比起 native 还是有一些差距的。下面就说说我们在开发过程中尝试过的一些优化,让我们的应用做到接近原生的体验。
官方 ScrollView 组件需要配合 layout 组件,当一次加载大量的子节点组件,或者分帧加载单个子节点组件时,初始化 ScrollView 节点视图会比较慢,在加载完成前存在拖动掉帧的问题。另外,一次性加载所有节点,也会导致内存资源的浪费。
下图这个场景是 ABCmouse 里的二级资源页,由于一次性加载了太多子节点,当屏幕滚动时,帧率降到了 8 fps 左右,给人的感受是非常卡顿。
我们对 ScrollView 进行了重写,基本的优化思路是:一次仅加载页面可容纳的少量数目子节点。并在滚动过程中,回收不可视的子节点组件并重用。
具体来说,ScrollView 大多数情况下表现为列表组件和宫格组件,以列表组件为例,可以根据子节点数目和子节点大小,计算出整个 ScrollView 内容的宽高,同时计算出屏幕可视区域最多可以容纳的子节点行数 rows,加载时仅加载 rows + 2 个子节点组件,其中添加的 2 个字节点组件作为滚动回收缓冲。
下图是对上述思路的图例。当手势向上,内容往下滚动时,一旦最上排的子节点组件不可视,就立马将它们回收掉并将其重用于将要渲染的子节点组件中。
这么做的优点在于:一次仅加载页面可容纳的少量数目子节点,并且逐帧加载,能极大提升展示和滚动性能,另外大大减少了内存占用。
经过优化后,不管二级资源页场景里有多少元素需要展示,整体的帧率都维持在 60 fps 左右,非常流畅。
内存占用过高也是 Cocos 开发过程中很容易遇到的问题。如果没有优化好内存占用,很可能就会引发黑屏或者 OOM。
要优化内存占用,有几个思路。第一个思路是把内存消耗大以及没有回收的元凶先找出来对症下药。
于是,我们仿照 Cocos 的监视器也写了一个内存监视器,利用它来找出疑似存在内存泄漏的场景。
对于每一个场景,我们也对每个节点的内存占用做了一个排名,找出靠前的,分析是否合理,并进行针对性的优化。比如把原图缩小,把无需透明像素的png图转换成JPG图,等等。
第二个思路是为图片渲染开启纹理压缩,从而大幅度降低图片渲染的内存占用。Cocos 提供了 ETC1、PVR 等几种纹理压缩方案,其中,PVR 兼容性最好,内存消耗也最低,但是质量较差;ETC1 不支持 iOS 的低端机型,质量也较差。我们又对 Cocos2d-x 进行扩展,增加了 ETC2 纹理压缩,这种方案的优势比起 ETC1 而言,压缩质量更好。
下图可以看到 ETC2 和 PVR 压缩质量和内存占用的直观对比。对比原图,我们可以看出 ETC2 的压缩结果与原图相差不大,但内存减少了 75% 。而 PVR 的压缩结果相比 ETC2 言在细节方面少了很多,内存则减少了 87.5% 。
针对兼容性问题,我们设计了一种混合纹理压缩方案:对于高质量要求的纹理,如果该机型能支持ETC2,就使用ETC2纹理压缩;如果不支持,就将该纹理进行大小减半压缩;对于低质量要求的纹理,使用兼容性好的PVR纹理压缩。单图渲染的内存消耗可以降低接近 75%~87.5%。
纹理压缩是一项耗时的任务,所以我们把这项任务放在项目构建完后进行,而不是在客户端运行的时候才动态压缩。
我们编写了一个扩展工具,在构建完成后自动进行纹理压缩任务。后面我们发现这个工具压缩完一遍纹理要花费大概3分钟的时间,我们又改进成了增量压缩的方式,一次压缩任务缩短到10秒左右。
每一帧的渲染耗时直接影响到整个应用的性能,而和渲染耗时相关的操作是 drawcall 。
什么是 drawcall 呢?我们可以看这张图来了解一下。在一帧的渲染过程中,场景会先被解析成场景树。场景树的每一个节点依次加入渲染队列中等待交付 GPU 渲染。GPU 接收渲染指令并执行的操作就叫做一次 drawcall。在一帧里头,drawcall 越少,性能当然就越好。
Cocos 针对 drawcall 优化已经提供了一种自动合并技术:比如,上图中的渲染指令 1、2 来自贴图 A,3、4 来自贴图 B ,5、6、7 来自贴图 C,这些指令会被分别合并优化,最终只产生 3 次 drawcall。我们要做的就是利用好这个自动合并技术。
首先可以找出浪费 drawcall 的节点对症下药。一般可以通过把节点的 active 属性设为 false 看看 drawcall 有没有大量减少来判断。
接下来我们可以利用好 Cocos 的合并技术。
目前这套优化方案还不能满足动态资源和动画的优化,我们也期待 Cocos 能够把 batching 技术做得更完善。
另外,还有另外一个需要注意的地方:小心避免跨层切换合图。Cocos 是按照节点层级顺序依次提交渲染指令的,如果不注重层级顺序,可能会导致贴图的切换从而浪费不必要的 drawcall 。
例如,下图中的渲染指令 4 使用的是贴图 C,直接卡在了渲染指令 3 和 5 之间,导致贴图 B 的渲染指令没法合并,从而浪费了多余的 drawcall。通过调整节点层级可以避免这个问题。
我们的应用里头目前依然存在一些原来的版本遗留下来的 H5 页面构成的场景,对于这些 H5 页面,我们也使用了一些比较常规的 Hybrid 优化技术,来达到首屏直出的要求。
因为已经有很多现有的优化方案了,所以这一块我并不打算细讲。简单为大家罗列几个技术点吧:
通过这一系列的优化,我们的应用里头的 H5 页面的加载耗时也能够控制在 1 秒以内。
最后我们来看一下整体的改造效果。
项目整体的 Cocos 化率目前占到了 56%,剩下的还有 40% 的 H5 的页面(主要是一些小游戏),还有像视频这种 native 场景。
对比原来的场景启动耗时,经过一系列改造和优化后的场景都能控制在 1 秒内启动。
直接看数据不够直观,我们可以看一下原来加载耗时最长的一个场景,经过改造后做到了秒开。
而腾讯版本的包大小也比原来的版本小了 64% 。
欢迎扫码体验新版本的 ABCmouse :
]]>IFTTT 是一个被称为 “网络自动化神器” 的创新型互联网服务,它非常实用而且完全免费。它的全称是 If this then that,意思是“如果这样,那么就那样”。简单地讲,IFTTT 的作用就是,如果「这个」网络服务满足条件,那么就自动触发「那个」网络服务去执行一个动作。而条件和动作都是可以由用户自己去根据自身需求设置的。IFTTT 能将前后这两个不同的网络服务神奇般地连通来实现各种各样的功能,并且为你不间断地工作。
在阅读下面的文章前,建议先去注册一个 IFTTT 账号,为了方便关联叮当,建议使用叮当的收件邮箱注册。
不想被晚睡拖延症影响你第二天的工作?让叮当在每晚喊你去睡觉吧!
首先进入 IFTTT 的 创建页,点击 [+This] 按钮:
在里头可以找到一个 Date & Time 插件,这个插件用来在特定时间触发事件。
我们点击它,进入插件的二级选择页面:
在这里我们选择 [Every day at] ,
在接下来的界面中设定好触发时间。这就完成了触发事件的设置。
接下来我们完成响应。怎么让叮当去响应这个事件呢?答案就是利用叮当的邮箱检查机制。了解了 [echo]
和 [control]
两个关键词的用途后,接下来我们就可以利用 IFTTT 发送标题为 [echo] 主人,您得睡觉了
的邮件到叮当的收件邮箱中,让叮当提醒您睡觉。
点击 [+that] 按钮,
然后从插件列表中选择 Email 插件,选择 Send me an Email :
Subject 就是邮件标题,填写您希望叮当通知你的内容,并带上 [echo]
前缀即可:
由于邮件只用作提醒,所以 Body 里头的正文没有什么用途,不用改。完成后点击 [Create action] 按钮即可。
简单几步,就实现了 0 行代码让叮当通知你睡觉了,是不是很方便?
当你的 Github 项目收到别人的 star 时,是不是很有成就感?可以让叮当监控 Github 上的 star 事件,然后通知你,给你打打鸡血。
首先先确定 star 事件的捕获方法。Github 为 star 等与你相关的事件提供了 RSS 订阅地址。地址就放在 Github 首页的 Activities 页面的底部:
点击 [Subscribe to your news feed] 链接,我们可以看到所有首页的事件。通过阅读该 RSS 源,可以发现与用户的 star 有关的事件都带有 starred 账户名/
的关键词。
针对 RSS 订阅, IFTTT 提供了 RSS 插件:
其中的 [New feed item matches] 项目可用于监控特定形式的 RSS 条目,并触发事件。
因此我们可以添加如下的 Applet,详细流程参考上面的例子,不再赘述:
this 的设置:
that 的设置:
希望叮当在即将下雨的时候主动提醒你外出带伞?可以使用 IFTTT 的 Weather Underground 服务插件:
该插件提供了十几种天气事件监控服务:
在这里可以选择 [Current condition changes to] 服务,设定当下雨时触发邮件事件:
this 的设置:
that 的设置:
]]>
说是“对的事情”是因为平安正处于技术转型期,其间会有大量的技术需求可以放手去做。而来平安这个团队的首要任务是开发完善底层的技术框架,有机会接触比较核心的技术,这对我的技术成长很有好处。
说是“对的团队”是因为我对平安这个团队的了解。平安那边的老大锋哥也是原先我们百度 LBS 团队的研发 leader,而 leader 定一也是之前在百度的时候合作非常愉快的一个同事。再加上鑫哥 @ASCE1885 每周一更的技术周报,让我觉得这个团队是非常尊重技术的团队,而我过去应该能有更多发挥的机会。
于是,在锋哥的几次劝说下,我来到了平安。时间过得很快,一转眼的功夫,我也已经在平安待了两年的时间。从团队初建,到发展壮大,我和这个团队携手共同成长。对平安,对金融壹账通,对这个团队,我有说不尽的感激。如今我即将告别平安,走向一段新的旅程。在启程之前,我想对这两年走过的路做一个回顾。
我这两年的工作基本是两条线的思路:主线任务保证做得漂亮,然后主动从日常工作中找问题和需求,做点支线任务。
我刚去平安的时候就被安排去做应用端的离线缓存。那时候我们的手机应用里有大量的 H5 页面,在线加载非常耗时。而我在这之前其实并没有移动开发的经验,这么重要的一个框架功能全权交给我去设计和实现,心里还是没底的。所幸平安的入职流程很长,十天半个月都拿不到账号上不到网,所以 leader 也没指望我很快能完成这个项目。于是我刚去的头两个月自己一个人保持了 997 的全勤,拼命恶补移动端的开发知识。当时腾讯的 AlloyKit 提出了一种比较理想的 H5 离线缓存的方案,页面静态资源通过打包 APK 的方式解决了首次加载问题,另外还允许更新这些缓存资源。我就在这个想法的基础上结合团队的实际需要进行设计和完善。在这段期间,我和定一以及后台的同事马文经常会在细节上展开讨论,从基本的流程、包管理、插件ID分配、自动打包、资源加密等方面都进行了认真的思考和设计。大概两个月后,我们的离线缓存就正式上线了,插件的加载速度和以前相比有了质的提升,得到了团队的认可。我拿着这个第一次参加了公司的 3A 论坛技术峰会,认识了公司里头来自不同团队的大佬。
在百度的期间我最大的收获在于对潜在的需求有着非常灵敏的嗅觉。当时的平安才刚面临技术转型,一些内部系统难免有不尽完善的地方,这对于来自 BAT 这类有着完善的内部系统的公司的同事而言很难适应。和大多数花时间吐槽环境的同事不同,我觉得这是一个好机会,这代表内部就有很多需求。所以,利用空余时间,我也做了一些“支线任务”,提高团队的效率。
第一个让我们觉得麻烦的是每天的日报,科技要求我们每天写一篇日报,汇报当天的工作,并以此来作为确认考勤的依据。但实际的情况是,我们平常已经通过站会来确认沟通每天的工作了,日报便沦为了一种形式:填了没人看,但不填又影响考勤。所以我写了一个工具,在月底的时候跑一遍,就能一次性帮我们到日报平台上填写完整个月的日报,这样就节省了很多同事的时间。这个工具在内部一直工作良好,直到我们从科技独立出去,无需填写日报了才停止使用。
团队里头也没有像样的 Wiki 平台,所以我用 Gollum 搭了个 Wiki 。为了方便在文档中插图,我又搭了一个相册平台,可以在上面上传照片并插到 Wiki 中。这两个平台一直使用到我们把代码由 SVN 切换到 Gitlab 时改用 Gitlab 的 Wiki 才结束服务。
当时我们有一个叫做“任意门”的项目,它的线上问题一直是使用 Excel 这种原始方式来跟进处理的,缺乏一个统一的问题跟进平台。而因为任意门是一个 SDK ,问题可能来自各种宿主应用,这些 Excel 报表的格式也有所不同,很影响问题的处理和定位。于是我又用了一个星期为任意门团队写了一个线上问题处理系统,可以导入 Excel 存到数据库中,然后提供一个报表页面,用于展示问题的描述、堆栈信息、出现次数、处理情况、宿主ID等数据。这个平台给开发维护任意门的同事带来了很大的便利。附图 1 这两年拿过的各种奖杯奖牌
本份工作顺利完成,再加上一些工具和平台加成,我在刚去的第一个季度就拿了团队的杰出贡献奖,奖品是一部 iPhone 6S 。
之后我们团队进行了业务的调整,大部分人(包括我)转去做银行一账通的项目,代号 F 项目。这个项目的目标就是低成本地生成中小银行的直销银行 APP ,可想而知这对核心框架的稳定性、可定制性和交付能力带来非常大的挑战。为了节省人力成本以及提高业务交付能力,我们经过评估后,决定采用 React Native 的开发模式,核心功能都尽量封装成 RN 层可调用的模块,再通过 RN 热更新来提升业务交付能力。RN 热更新的设计和实现就成了我在平安的第二个季度的主线任务。
当时 Android 版本的 RN 才刚刚发布,RN 热更新还处于尝试阶段,一开始的思路是使用反射修改 PathClassLoader
来修改 bundle 包的路径。v0.15.0 开始 ReactInstantManager
新增了 setJSBundleFile
方法,可以在程序运行时执行 Bundle 包的位置,这就给实现热更新提供了更大的便利。RN 热更新本身技术倒没有什么特别的难度,后面出现的 Microsoft code-push 也给我们提供了有价值的参考。不过,技术实现往往不是最困难的地方,最困难的地方是落地。我们花费了较多时间在热更新平台的增量更新、图片热更新、图片精度选择性下发、包版本管理、跨版本升级支持等问题上。一直做到年前,这个项目正式上线,为应用的运营能力提供了强有力的支持。凭借着 RN 热更新我也拿了一个公司的创新奖。
在第二个季度我也照旧从日常工作中找了一些问题,做了一些支线任务。
第一个问题就是团队的文件共享问题。在 F 项目中,开发、测试、运营、产品需要经常共享 UED 、设计稿等文档。这些文件共享都是通过邮件的形式,而公司的邮件有附件大小限制,并且邮件只保留一个月的时间,超过这个时间的会被归档,只能从公司的邮件找回平台中去找。所以经常出现诸如附件找回困难、附件上传失败的问题。所以我在内网搭了一个网盘“百宝箱”,可以很方便的让各个成员在内网共享文档。这个网盘很快得到成为了团队文档交流的主要方式,直到公司推广了 Jira 才退出了历史。
第二个问题是我们内网上的平台越来越多,缺乏一个统一的门户站点。所以我和一个实习生写了一个内部门户“星黎殿”,这个门户收录了我们日常使用的各种站点平台,并提供检索功能。这样,只需要将这个门户保存到书签就相当于保存了所有常用的站点的书签了。
平常大家的线上技术交流较少,所以我又用 discourse 搭建了一个内部技术交流论坛,为了鼓励同事们多发点文章,我搞了几个月的有奖征文活动,并且给每个小组都设立了文章数量指标,于是这个论坛同样受到了同事们的欢迎(甚至有同事在上面发布了征婚广告)。
另外我发现的一个需求是图书馆的管理。我们团队自己搞了个小型图书馆,采用的是人工管理图书借阅的方式。由于缺乏自动化,借出去的书除非定期轮询一下,否则很难知道哪些书已经到期该归还。所以我带着几个实习生写了个基于公司内部聊天工具“天下通”的聊天机器人“金科大白”,可以与它对话查询和借阅书籍,当书籍借阅到期时,将自动邮件通知还书。这个机器人也给后面我开发内部群管家机器人以及叮当机器人带来了灵感。
年前我还带了两个实习生做了一个我们团队的内部网站,主要用来展示团队在做的东西,以及一些可以对外开放的文档。后来我们从科技独立了出去,原来的团队随即更名,这个项目就胎死腹中了。
不过在这个过程中,我们却做了一个更有价值的东西。由于网站本身使用 Hexo 来编写,为了方便检索文档,我带着其中一个实习生写了个生成检索源的库 hexo-generator-search 并开源在 Github 上。这个库后来成了一个非常流行的 Hexo 插件,NPM 月均下载量一度达到 2000 次/月。
主线 + 支线双开,所以年底我被公司授予“一狼当先”创新员工奖。年底绩效沟通,leader 给我定级 A 档。
平时的工作虽然比较忙,公司的文体活动还是挺丰富的。在团队里头可以申请俱乐部,于是我常年混迹在桌球俱乐部、游泳俱乐部、户外俱乐部中打酱油。附图 2 我和我的Ukulele
年底的时候公司的行政美女黎老板给我安排了一个的政治任务——年会表演。但纯粹上台唱歌实在无趣,所以我在年会前的一个月开始学习 Ukulele 。就跟学习一门新的语言范式一样,一开始面对陌生的东西依然会有一种恐惧感,但坚持下来很快就会发现它的确非常简单,很容易做到拿谱即弹,这也增强了我挑战新鲜事物的信心。放一段为年会准备的弹唱曲目吧: 秒拍:一小段I’m Yours 。
到了 2016 年,我们正式从平安科技独立了出来,成立了平安金科。后来公司名字又做了几次调整,到我离职时,公司的名字改成了“上海壹账通金融科技有限公司”。
平安的内部版本控制一直使用的是 SVN ,但 SVN 对分支和子模块的支持不给力,所以年前我们做了个决定,搭了内部的 Gitlab ,然后把整个工程代码拆分成主工程和多个子模块,然后全部迁移到 Gitlab 中托管。为了简化代码迁移的复杂度,我写了个内部工具 svn-git-transfer ,只需要配置好源 SVN 地址和目标 Gitlab 仓库地址,即可一键完成所有模块到 Gitlab 的同步,并保留了原先的所有提交记录。有了这个工具,我们只用了一天就完成了代码的迁移,然后大家高高兴兴的回家过年了。
年后回公司,我们遇到了更加头疼的一个问题:Git 的子模块极容易出错。具体可以参见我的一篇博文《化繁为简的企业级 Git 管理实践(一):多分支子模块依赖管理》。于是,在和同事们充分讨论之后,我们决定放弃子模块的 commit id ,采用我们自定义的一个 modules.json 的文件来维护子模块的版本。于是我用了几天时间完成了内部代码管理工具 fmanager 的第一个版本。这个工具将繁琐的子模块管理化繁为简,得到了同事们的一致好评。在之后我对这个工具不断的修改完善,加入了诸如分支切换、多分支 cherry-pick、Code Review、代码风格检查、Git-LFS 支持、钩子自动安装等,并且支持通过改配置文件动态增删子模块。
1 | fmanager - F 项目专用 Git 管理工具 |
这些功能使它成为了团队日常开发中必备的工具,也因此获得了一个季度优秀工具奖以及公司创新奖。
除了技术上的问题,迁移 Git 也给团队的学习接受能力提出了挑战。由于我对 Git 比较熟悉,所以我自然承担了在团队中推广 Git 的角色。一开始的时候,找我解决代码拉取问题的人需要排一条很长的队伍。我也忙得几乎顾不上干其他事情。为了减轻负担,我先是进行了几场 Git 的使用技术分享。之后我在 Gitlab 上写了份 FAQ ,然后把团队遇到的各种常见问题都放到 FAQ 上。一旦帮同事解决了新的问题,我都会要求他去负责把解决办法添加到 FAQ 上,久而久之 FAQ 上的问题集就非常完善了。除此之外,先前为团队搭建的技术论坛也成了我用来推广 Git 技术的平台,我在上面写了几篇 Git 的教程,也对团队成员的学习起到了一定的帮助作用。我甚至把一些常用的 Git 技巧和 fmanager 做了整合,于是每次使用 fmanager 后,都会随机出现类似下面这样的卖萌 ASCII 图和贴士:
1 | _____________________________________________________________________________________ |
这些技术推广策略对团队普及 Git 起到了非常有效的推动作用。过了一个月后,找我的人就变得很少了。多数人也从 Git 的新手慢慢变成了老手,也能够帮忙解决新人们的 Git 相关的问题了。
还有一个避不开的问题就是分支管理方案。因为我们的业务模型比较复杂,Git-Flow 和 Github-Flow 都无法满足我们的需求。而没有一个统一的分支方案,就好比没有一个成文的交通规则,老司机也开不了车。于是有很长一段时间,我们都聚在会议室里头激烈地讨论分支管理方案,见我的另一篇博文 《化繁为简的企业级 Git 管理实战(三):分支管理策略》。这个过程虽然很痛苦,但是却很能够锻炼你的思维,以及考虑问题的完整程度。而且,你会感觉到你真正成为了团队里头一个领域的游戏规则的制定者,更加能感受到你对于这个团队的价值。
到了年中,我们使用 Git 来管理代码已经非常稳定可靠,Git 的迁移可以说非常成功。我们也因此获得了团队的优秀项目奖。
在下半年,我终于有时间回归到客户端开发了。我们遇到一个问题是几种更新的弹窗抢占问题。我们的 APP 已经包含了 Native 热修复、RN 热更新和 H5 离线缓存三个层级的更新能力。但这三种更新各自为政,可能发生弹窗抢占。此外,几种依赖可能存在依赖关系:比如 RN 层的某个接口可能依赖 Native 层相应的改动,这时候如果 RN 先更新成功了,而 Native 端又修复失败,就会导致应用崩溃。所以我承担设计了三合一更新器,将三层的更新交由一个更新器来统一调度,三层更新都允许设置静默、建议、强制三种更新优先级,并在必要时刻允许对优先级进行提升。这个三合一更新器优化了用户的更新体验,也消除了三层更新没有同步完成导致应用崩溃的潜在隐患。
除了三合一更新之外,下半年我们开始思考如何更好地引入 Code Review 来规范化我们的工作。
在这之前,我已经通过 pre-commit 客户端钩子和 pre-receive 远程钩子来自动禁止了不规范的代码提交进我们的仓库。但代码风格只是最低层次的要求。有些代码本身的风格并没有问题,但是实现的思路与需求存在偏差,或者实现时对一些特殊情况欠考虑,这些 bad case 都无法通过静态分析的手段禁止提交,只能靠人工审核。所以,一个 Code Review 平台是很有必要的。
我先尝试了 ReviewBoard ,这个平台的优点是易于搭建,而且也有相应的 CLI 工具配套。但我发现这个平台似乎只支持 post review ,即 push 到仓库之后才发起 review 。而我们更希望做到 pre review,在 push 前就必须通过 code review ,否则禁止 push 。所以我决定弃用 ReviewBoard 。
之后我尝试了 Phabricator ,据说是 Facebook 内部使用的 Code Review 平台。我在搭建试用了几分钟后就觉得这个就是我们要的平台:
还是那句话,技术实现不是难点,难的是推广。为了在团队中推广 Code Review,我做了几个工作:
为了方便查询和自己相关的 Code Review 的进展,我还写了个 Alfred 的 Workflow,输入自己的账号即可查询所有相关的 Review 任务:
这个 Code Review 平台内测不到两周,我们的 Code Review 数量就破百了,两个月后,团队的 Code Review 数量就突破了一千。这个平台也拿到了该季度的优秀项目奖。
年初我发现团队里的一个痛点:每个月月初我们都需要到内网导出打卡记录,然后从中手动筛选出加班的那些天,再整理成一份报销表格给行政的同事申请加班补贴。手动筛选加班的打卡记录不仅耗时,而且很容易出错,尤其是当天是特殊节假日的时候。每次整理加班记录,可能要花费二十分钟的时间。而这些工作完全可以交给机器自动完成。
所以我写了一个内部使用的加班统计平台,从内网导出的打卡记录只需要交给平台进行一下分析,马上就可以生成一份符合要求的加班统计表格,并且还考虑了特殊节假日以及漏打卡的情况。原先要花费二十分钟完成的事情,只需要交给这个平台,一分钟左右即可导出需要的表格。
为了吸引同事们使用,我又加了加班数据排名以及可视化的功能,可以很直观的看出近期加班的情况。
这个平台也是我首次尝试收费的一个服务,可选择一年授权和长期授权两种激活码,并提供首次免费试用。有 100 多位同事在试用成功后就购买了我的服务。
由于我们同时开发维护多个 APP ,数量一多,要查出每个 APP 的某个模块在使用什么版本就变得很繁琐了。
所以我写了一个模块配置可视化平台,支持多条件的检索模块的配置信息。
为了保证数据是最新的,我给主工程的 Git 仓库加了 post-receive 远程钩子:每次检测到主工程的 modules.json 发生修改,在服务端的 post-receive 钩子就会把新的 modules.json 配置情况同步到可视化平台中。
在我们成功迁移到 Git 管理代码后,公司也开始普及 Git,搭了一个公司的 Gitlab 。在正式发布前,科技的工程管理团队甚至邮件向我咨询了 Git 管理的一些经验心得。
尽管公司的 Gitlab 有更大的团队来维护和支持,我们并不愿意把我们的代码迁移到公司的服务上。最主要的原因是我们自己的 Gitlab 可以更方便的定制,迁移到公司的 Gitlab 就没有这么高的权限了。附图 4 年会电子琴弹唱
虽然自己的 Gitlab 定制起来很爽,但稳定性和单点问题也成了隐患。我们甚至遇到过 Gitlab 所在的服务器发生维护而导致我们一整天无法正常拉取和推送代码的情况,简直是一场噩梦。为了避免单点问题带来的灾难,我写了一个全自动代码同步工具,能够在每天凌晨把我们的 Gitlab 的组织、代码、成员、分支保护等东西都同步到公司的 Gitlab 中,而且无需管理员权限。见 《化繁为简的企业级 Git 管理实战(四):多 Gitlab 数据同步》。这个工具给我们的代码安全提供了一个强有力的保障,也因此获得了该季度的优秀工具奖。
年中和年底的两次绩效沟通,leader 又继续给我评级了 A 档。同时我被评为了公司的年度优秀员工。
有了去年的 Ukulele 弹奏的基础,今年的年会我决定玩点其他的乐器。于是我在九月份买了一台卡西欧入门电子琴,开始学习电子琴弹唱。
电子琴弹唱和 Ukulele 弹唱完全是不同级别的难度。Ukulele 的弹唱基本就是弹和弦搞定,而电子琴弹唱可以说是吉他指弹 + 演唱的难度了,要在几个月内达到拿得出手的水平非常困难。好在现在的电子琴的教学软件都做的非常人性化,我只需学好指法,然后照着软件给出的演示进行肌肉记忆,整首歌硬记下来也是可能的。年会上,我弹唱了偶像王力宏的经典歌曲《你不知道的事》( 练习视频:你不知道的事),算是把大伙糊弄过去了。
到了 2017 年,我们的框架变得稳定,不像上一年有诸多需求。于是我们进入了舒适区,过上了朝九晚六的生活。但这种情况对于我而言并不见得是好事:需求变少了,人也会跟着变懒,久而久之就没有成长了。既然业余时间变多了,我决定把时间用来学点 AI 的相关的知识。附图 5 顺手给课程提了几个建议和bug,收到了 Udacity 寄来的小礼物。
机器学习对数学的要求比较高,所以我先花了半个月的时间看了可汗学院的《线性代数》课程,写了几篇学习笔记。然后又看完了 Udacity 上的《机器学习入门》,并做完了全部练习。这门课讲的非常生动,极大地帮助我加深了对机器学习如何解决实际问题的理解。
在锋哥的鼓励下,我带头成立了一个人工智能兴趣小组。我们一共开展了 6 次分享,涵盖了 TensorFlow、贝叶斯分类、线性回归、K-means 聚类、自然语言处理、对话机器人实现等多个 topic 。另外我申请购买了一台高性能的深度学习机,自己搭建了 Jupyter Notebook 和支持 GPU 加速的 TensorFlow 环境,可以供小组成员在 Notebook 上做感兴趣的实验。
之后我想尝试将分类的技术用在我们内部的 issue tracker 来判断是否重复提 bug 。由于我们的框架同时对接了多款 App ,一旦框架出现了一个 bug,多个 App 上都会报相同的问题。如果能通过机器学习的技术来分辨出是否重复提 bug ,那么就能减少开发在 follow issue 上面花费的时间。判断是否重复提 bug 和判断文本内容相似度类似,可以对文本统计 TFIDF 词频信息,然后利用余弦相似度来判断相似程度。所以,我写了个爬虫,把我们的 Redmine 平台上的数据按照格式爬了下来,总共爬了接近两万条 bug 数据。但过了几天公司开始推 Jira 作为新的 issue tracker,我们的 Redmine 就停止使用了。这个项目就只能搁浅了。
虽然我们并没有真正用机器学习算法做出什么成果,但这个过程中积累的一些机器学习的知识也将为我日后的工作方向带来收益。
我为团队做的一个比较好玩的项目是微信聊天机器人“图灵”。这是基于 wxbot 和 图灵机器人 API 做的一个微信群聊天机器人。
起初它是为了取代金科大白而做的一个新的管家项目,因此它可以实现诸如询问内网站点地址、恶劣天气通知、群成员生日提醒、新人入群欢迎、活动报名征集、考勤异常提醒等功能。
大伙对图灵非常稀奇,刚推出的短短两天,它就被群里的同事 @ 了 800 多次。
后面我发现把图灵和项目管理结合可以产生更多意想不到的效果。例如,我们的框架的源码和 SDK 是分开分支进行版本控制的。以往要构建 SDK ,需要先切到源码分支,执行构建脚本,然后再切到 SDK 分支上更新 SDK 。过程比较繁琐。我给图灵加上了构建框架 SDK 的功能。要构建某个分支的 SDK,只需要跟图灵说一声即可:
交给机器人构建 SDK 是如此方便,于是 @图灵 构建 SDK 成了我们日常构建框架 SDK 的主要方式。
李开复说,“5秒以下的工作将会被人工智能替代”。我也在工作中尽量找出 5 秒内可以用微信机器人取代的活,并交由它去完成。于是我给图灵实现了诸如苹果APP审核状态跟踪、项目日报查询、Jenkins 构建站任务跟踪、个人bug通知查询、线上 App 下载地址查询等功能,这些功能为项目的日常管理带来了很大便利。
由于今年加班时间变少了,我也有了更多时间投入在开源项目中。我的问题开始由“这个团队需要什么”转变为“外面的世界需要什么”。
在 5 月份的时候我决定自己动手做一个智能音箱,一方面来自于自己生活中的需求,另一方面也是因为找不到一个比较成熟可用的中文的开源智能音箱项目。于是我买来了硬件设备,花了三个星期的时间完成了叮当的第一个版本。由于树莓派+智能音箱毕竟是小众项目,所以我一开始并不期望这个项目能得到多少关注。然而,随着 QQ 用户群人数的不断壮大,越来越多的朋友安装了叮当,并且真正将它投入在了日常的使用上。发布不到 10 天,star 数量就突破了 100 个,有两天登上了当天 Python 的 Github Trending。截至本文发布,叮当的 QQ 用户群已经达到了 117 人, Github Star 数量为 211 个。
叮当也给我的职业生涯带来了新的变化。在发布叮当后的第三天,腾讯团队找上了我问我是否考虑他们的工作机会。考虑到在这边已经处于舒适区,我决定去了解一下情况。
经过了三轮技术面试和一轮通道面试,我拿到了 offer 。拿到 offer 的时候,我非常纠结。最大的原因来自对现在团队的不舍。但考虑到工作方向和个人的职业规划,我最终还是提了离职。
在项目交接期间,我又写了一个基于 Github issue 的评论脚本 comment.js ,用来取代 Disqus、网易云跟贴这类社会化评论系统。有趣的是,在我发布 comment.js 一周后,网易云跟贴宣布停止服务。有种自己在船沉了之前造出了飞机的感觉 :-D
。
白驹过隙,在平安的两年时光已走到了尾声。除了不舍,更多的是对团队的感激。谢谢锋哥的知遇之恩,谢谢定一的悉心栽培,谢谢技术大神鑫哥的指点,谢谢所有合作过的同事,谢谢团队对我的信任。
在平安的两年时间里,我一共获得了一次年度优秀员工奖项,一次年度创新员工奖项,一个杰出贡献奖,两个工具奖,两个项目奖,四个创新奖,参与申请了八项专利,三次绩效考核都保持了 A 档。如果要我总结点经验:
雄关漫道真如铁,而今迈步从头越。在即将奔赴新的单位前,我也以此文告诫自己:
dingdang-robot (以下简称“叮当”),是我在今年5月20号开源的一个中文智能音箱项目。
起初,我只是抱着一个好玩的心态做这个项目,并不期望这个项目能得到多少关注。然而,随着 QQ 用户群人数的不断壮大,越来越多的朋友安装了叮当,并且真正将它投入在了日常的使用上。很多朋友也提出了各种好玩的建议、想法,甚至为叮当贡献了插件或者捐赠了这个项目,让我觉得这个项目是很有价值的,也让我知道,我能做的还有很多东西。因此,我并不是开源了叮当之后就撒手不管了,而是利用业余时间不断完善叮当,又陆续发布了 8 个小版本,使得叮当在响应速度和功能丰富程度上都有了很大提升:
[echo]
前缀的邮件给叮当接入的邮箱,从而实现让叮当朗读标题的消息,而不是提醒有新的邮件。利用这个功能,可以很方便地和 ifttt 结合,当满足某个条件时让图灵通知这个事件。例如,我将 ifttt 的 Date & Time 触发器和 Email 做了联动,实现定时在每天晚上 11 点发送邮件通知我睡觉;[control]
前缀支持,用户可以发送带 [control]
前缀标题的邮件,该标题里的内容会被当成一句指令交给叮当执行,从而实现类似微信的远程控制叮当的功能;enable
选项开关;除了完善已有的功能,我也正试图让叮当往智能家电控制中心的方向上发展。
如今很多智能音箱除了用来听歌对话之外,还扮演了家庭中的一个控制中心的角色。不过,由于各家采用的接入协议有所区别,A 家的智能家电并不见得能得到 B 家的智能音箱的支持。而由于叮当是开源的项目,对其进行定制,接入控制家电所需的协议,从而实现声控大部分的智能家电是完全可能的。今天我就分享一下如何用叮当控制智米电风扇。
先上视频。这个视频演示了使用叮当实现对智米风扇的电源开关、摇头开关、自然风开关、风量调节、预约关机的声控:
插件的主页:https://github.com/wzpan/dingdang-smart-mi-fan 。
先安装 python2-miio :
1 | pip install python2-miio |
之后,克隆本项目到任意目录:
1 | git clone https://github.com/wzpan/dingdang-smart-mi-fan.git |
再将里头的 SmartMiFan.py 拷贝至 /home/pi/.dingdang/custom 目录。
1 | cp dingdang-smart-mi-fan/SmartMiFan.py /home/pi/.dingdang/custom/ |
如果没有 custom 目录,就先创建它然后再执行上面的拷贝命令:
1 | mkdir /home/pi/.dingdang/custom |
然后,确保你的智米电风扇已开机并和叮当所在的机器处于同一个局域网下。然后执行以下命令获取风扇的 host
和 token
:
1 | miio2 discover |
最后在 /home/pi/.dingdang/profile.yml 中添加如下配置:
1 | # 智米风扇 |
完成后重启叮当即可使用本插件。
指令 | 相同指令 | 用途 |
---|---|---|
打开风扇 | 启动风扇 | 打开风扇 |
关闭风扇 | - | 关闭风扇 |
开启自然风 | 启动自然风 | 切换到自然风模式 |
关闭自然风 | 关闭自然风 | 切换到普通模式 |
开始摇头 | 开启摇头 | 开始摇头 |
停止摇头 | 结束摇头,关闭摇头 | 结束摇头 |
加大风速 | 加快风速,加大风量,加大风力 | 加大风扇转速 |
减少风速 | 减慢风速,减少风量,减小风力 | 降低风扇转速 |
$num $unit 后关闭风扇 | $num 是数字,$unit 可以是秒/分钟/小时 | 预约关机 |
下面说说开发心得。小米的智能家电使用的是 miio 协议。在编写这个插件之前,我先试用了 @homeassistantchina 提供的 HomeAssistant 的智米风扇插件 smart_mi_fan.py,这个插件在 HomeAssistant 里头的体验非常不错:
通过阅读这个插件,我发现这个插件依赖了一个封装了 miio 协议的 Python 3 的库 python-miio 。再阅读 python-miio 的源码,可以发现这个库就是通过 socket 来实现和家电的通讯的。整个发送消息的逻辑写成了一个 send
函数:
1 | def send(self, command, parameters=None) |
使用这个函数非常简单,只需要传入要发送的指令即可。
通过阅读 smart_mi_fan.py
的源码,不难发现 miio 的指令主要是两个类型:
获取属性。获取风扇的温度、角度、电源、风速等属性。通过发送 get_prop
指令并带上需要获取的属性名即可得到这些属性的值。具体有如下这些:
temp_dec
:温度;humidity
:湿度;angle
:角度;speed
:风速;poweroff_time
:预约关机的时间(秒);power
:是否开机;ac_power
:是否使用交流电供电;battery
:电池剩余电量;angle_enable
:是否摇头;speed_level
:正常风风速等级;natural_level
:自然风风速等级;child_lock
:儿童锁;buzzer
:是否有声音反馈;led_b
:LED指示灯的亮度。设置属性。设置某个属性的值。通过发送 set_属性名
指令并带上值即可对该属性进行控制。
了解了这个套路后,我只需要照着写一个叮当的插件,即可实现让叮当声控智米风扇的目标。
不过,在完成这个目标前,我还遇到了一个问题:python-miio 只能在 Python 3 环境中使用,而叮当是使用 Python 2 编写的。于是我对 python-miio 进行了 Python 2 版本的移植,发布为 python2-miio 。为了避免两个版本的 miio
命令行工具冲突,我把 python 2 版本的 miio
更名为 miio2
。
插件的编写过程也很轻松,和写其他的叮当插件一样的套路,所以整个插件我只用了一个晚上的时间就写好了。如果你看过我之前写的一篇编写叮当插件的教程《手把手教你编写叮当机器人插件》,那么阅读风扇声控插件的源码也不会很困难,这里就只放上源码链接:https://github.com/wzpan/dingdang-smart-mi-fan/blob/master/SmartMiFan.py。
最后就是插件的发布问题。考虑到这个插件比较私人——大部分用户并没有智米风扇,因此我并没有把它丢进 dingdang-contrib 中,而是单独创建了一个仓库来托管。对于需要使用这个插件的人,只需要将它放到个人的插件目录 /home/pi/.dingdang/custom 中,即可让该插件生效[1]。这样的好处是无需改动 contrib 目录,也不会影响 contrib 目录的更新。
custom
目录的支持在叮当 v0.1.9 开始引进。 ↩︎
我的博客最早是使用 Disqus 来实现评论功能的。Disqus 被墙了之后,改成了多说。今年年初,多说也正式关闭了,于是我被逼着又开始寻找其他的替代评论系统。
我先是试用了网易云跟贴、畅言等几种类似的社会化评论系统。畅言要求站点必须备案,而我实在没有为了评论去申请备案的动力。网易云跟贴的管理后台上有很多不明觉厉的功能,但好像都没多大用处。最致命的问题是我不小心把我的站点绑定到了另一个网易账户,而不是我常用的微博账户。这样的话,我每次回贴就得退登到微博账户,要管理贴子的时候又得切回管理员账户,非常不方便。然而网易云跟贴并没有提供解绑的功能。于是我给他们提了需求,然而一直到现在都没有回复。再加上有了多说作为前车之鉴,我对国内的免费评论服务已经失去了信心。今天把A换成B,难以保证日后B也关闭了,被逼着又换到C,实在是懒得折腾下去啊。于是,我放弃了换用类似的评论系统的念头。
之后我找到了 isso 项目,它是一个 Python 实现的开源评论服务。这个服务需要搭建在自己的服务器上。官方的简介简明扼要:“a Disqus alternative”。出于对 Python 的好感,我把站点的评论功能迁移到了 isso 。然而,我对 isso 也并不是很满意。首先它的功能其实也非常弱,不支持 Markdown 语法,不支持 Gravatar 头像,也没有一个像样的管理后台,搭建和配置的过程也比较费时,远达不到开箱即用的程度。再加上 isso 需要服务器运营,为了一个评论系统而去购买服务器确实太奢侈了。用了几个月后,我又萌生了换掉它的念头。
我的想法来源于一些基于 Github issue 的博客。其实 Github 的 issue 本身就是一个非常完善的评论系统,有完善的管理后台,灵活的通知设置,而且 Github 是开放 API 的。只要我能把 Github 的 issue 与博客的页面打通,把 issue 上的内容显示在我的博客上,然后在需要评论的时候点击跳转到 Github 的 issue 页,就实现了一个基本可用的评论系统了。
comment.js 就是基于这个想法实现的一个评论系统,它的核心代码只有 400 行左右,却能够用来实现评论会话和最新评论列表的两个功能。比起已有的社会化评论系统,它有如下几个优点:
comment.js 依赖几个 JS 前端库:
在页面中添加这些资源:
1 | <!-- stylesheet --> |
为了避免 API 被恶意滥用,Github API (以及 OSChina API)设定了一个API调用频率限制。为了提高频率限额,建议 [注册一个 Oauth App](Register a OAuth application](https://github.com/settings/applications/new)。
完成注册后,你将得到一个 client id
以及一个 client_secret
,先将这两个值记下来,后面我们会用到。
(提示:注册 App 的时候你可能会对 Authorization callback URL
这一项目感到困惑,一般填写你的站点地址即可。例如 http://hahack.com )
第一步,在页面中添加一个 DIV ,用于展示评论会话内容。
1 | <div id="comment-thread"></div> |
第二步(可选),如果希望在加载完数据前先展示一个loading动画,还可以添加一个用于动画的 DIV :
1 | <div id="loading-spin"></div> |
最后,调用 getComments()
方法,获取该页面对应的 issue 包含的所有评论,然后展示到我们指定的 DIV 中:
1 | <script type="text/javascript"> |
参数说明:
type
: 要作为后端的站点。目前支持 Github
和 OSChina
。user
: 您的 Github 用户名。repo
: 您用作评论后端的仓库名。no_comment
: 当没有评论时,展示的提示消息。go_to_comment
: “去留言” 按钮的按钮文本。issue_title
: 您当前页面对应的 issue 标题。也可以使用 issue_id
,二者只选其一。issue_id
: 您当前页面对应的 issue id。也可以使用 issue_title
,二者只选其一。btn_class
: “去留言”按钮的 CSS 样式名。comments_target
: 用于展示评论内容的容器。例如我们上面所写的 comment-thread
DIV 。loading_target
(可选):用于展示 loading 动画的容器。例如我们上面所写的 loading-spin
DIV 。client_id
(可选但建议):您注册的 OAuth App 的 client id。client_secret
(可选但建议):您注册的 OAuth App 的 client secret。效果参见本页面下方的留言区。
评论列表用于获取你最近的若干条评论,效果可以参见 站点首页 右侧的最新留言区。
要获取最新评论列表的方法也大同小异。首先写一个 DIV 用于加载获取得到的评论列表数据:
1 | <div id="recent-comments"></div> |
之后可以调用 getRecentCommentsList()
方法,获取最近评论列表并展示到指定的 DIV 中。
1 | <script type="text/javascript"> |
参数说明:
type
: 要作为后端的站点。目前支持 Github
和 OSChina
。user
: 您的 Github 用户名。repo
: 您用作评论后端的仓库名。recent_comments_target
: 用于展示最新评论列表的容器。例如我们上面所写的 recent-comments
DIV 。count
: 列表的最大长度。client_id
(可选但建议):您注册的 OAuth App 的 client id。client_secret
(可选但建议):您注册的 OAuth App 的 client secret。下面照例总结下项目的开发心得。虽然整个项目只有几百行的代码,但这个过程中还是不可避免的遇到一些困难。
一开始的想法只是给 Hexo 写一个插件,让其能够实现评论功能。最理想的情况是类似 hexo-generator-search 那样,npm install 一下,然后 _config.yml 里添加下配置就完事。通过阅读 Hexo 的文档后我发现 helper 似乎比较适合用作这个目的:把核心功能写成一个 helper ,然后在模板文件里直接执行这个 helper ,得到的数据还能进一步再模板中调诸如 markdown 等其他现成的 helper, 这样还能实现 Markdown 支持。于是我最初的项目仓库名叫做 hexo-helper-github-comment 。
等我实现了 getComments()
方法后,我发现我的想法是错误的:helper 只适用于同步执行的操作,不适合网络请求这种异步操作。这带来的问题就是模板文件里已经成功执行了 helper 了,也返回了数据,但此时 renderer 早已经完成了模板的渲染了,而异步返回的评论数据却不再能够被渲染。
之后我想在 NodeJS 中加入 jQuery,用 jQuery 来操纵 DOM ,而不再依赖 renderer 。但这个方案似乎也不可行。因为在模板文件中,DOM 还没有创建,jQuery 拿不到实际的 DOM 。
所以最终我改成了纯 JS 的方案,把请求的方式也从 request-promise 改成了 AJAX ,然后在模板文件中直接跑 JS ,让 JS 完成请求,此时的 DOM 是已创建的,可以使用 jQuery 来操纵页面。虽然这样做就不能直接用 Hexo 现成的 markdown helper 了,但由于是纯 JS 实现,这个库也就可以在任何静态站点中使用,变得更加通用了。于是我把仓库名改成了 github-comment 。
又后来,我准备开源的前一天,在微博上先公开了关于这个项目的信息。有些人也表示了 Github 将来也可能被墙的质疑。于是我花了几分钟时间,也加入了对 OSChina 的支持。这个仓库名似乎也不只是基于 Github 了,于是我又把仓库名改成了 comment.js 。
我最纠结的部分,在于要不要把评论框也写进来。
直接在页面中写评论,减少了页面的跳数,当然是一大收益。但这样做也有几个问题:
有意思的是,当我刚发布 comment.js 的时候,我才发现几个月前已经有人做了一个类似的项目:gitment,真是心有灵犀啊。这个项目与我的项目的最大区别就在于它实现了内置的编辑框,并且目前只支持 Github 。如果你认为评论框必不可少,那么建议使用 gitment;反之如果你觉得点击按钮跳到 Github 页面似乎也还能接受,担心 Github 单点问题,而且觉得保证代码的简单和通用性更重要的话,那么不妨使用 comment.js 。
由于叮当是完全开源的,编写插件来满足自己的个性化需求就成了使用叮当的最大乐趣。您可以自由地接入自己需要的服务,让叮当更好的为您服务。
这篇文章将一步步教你如何编写一个天气插件,并最终发布到 dingdang-contrib 中。
交互示例:
首先需要确保您已安装了叮当的第三方插件库 dingdang-contrib :
1 | cd /home/pi/.dingdang |
接下来可以在这个插件库中开发您的插件。
要实现天气预报功能,少不了要了解一下天气 API 。通过搜索,我找到了一款免费的天气 API —— 心知天气 。心知天气提供了天气、空气质量、生活指数等多种数据信息。其中逐日天气预报是免费的,可以利用来实现天气预报查询插件。
选择心知天气的另一个理由是他们的 API 文档非常详细,还提供了多种语言的 demo (连 common-lisp 都有,点个赞! )。下面是官方提供的一个 Python 版的示例:
1 | import requests |
其中,API
是 API 的地址,逐日天气预报的 API 地址是 https://api.seniverse.com/v3/weather/daily.json ;KEY
则是心知天气的 API 密钥,每个注册账户都可以得到一个密钥;location
是城市名,例如深圳就是 深圳
或者 shenzhen
;而 language
和 unit
分别表示语言和单位,由于是可选参数,这里不做详细介绍。有兴趣的朋友请阅读官方文档。
整段代码也没有什么特别好说的:先是定义了一个 fetchWeather
函数,该函数使用 requests 模块发起 API 请求,请求超时设置为 1 秒。之后调用这个函数并打印返回的结果。
了解了心知天气的 API 后,编写插件就是一件水到渠成的事情了。
编写插件主要要考虑以下几个问题:
下面我们将在编写这个插件的过程中解决这些问题。
首先在 ~/.dingdang/contrib
中创建一个新的文件 Weather.py :
1 | # -*- coding: utf-8-*- # 天气插件 |
这个是插件的模板文件。一个标准的插件至少包含 WORDS
,SLUG
,isValid
函数和 handle
函数四个部分。下面将一一介绍。
WORDS
是一个关键词列表,用于存储这个插件的指令关键词(的拼音)。当 Active Listening 的 SST 引擎设置成离线引擎时,这里设置的关键词会被自动添加到 PocketSphinx 的语音指令集中,从而使得 PocketSphinx 能识别出这个指令。
WORDS
的另一个作用是作为是否插件的判断标准。如果 contrib 目录中的某个 .py
文件没有 WORDS
变量,那么这个文件就会被当成无效插件,而不会响应它。
与关键词有关的还有 isValid
函数,该函数用于判断用户输入的指令是否要用这个插件来处理。如果 isValid
返回结果为 true
,handle
函数就会被调用,以处理指令。对于天气插件,关键词可以设置为天气,即只要包含“天气”的输入都使用本插件做处理。因此,可以将 WORDS
和 isValid
函数改写成:
1 | ... |
SLUG
是该插件的标识符,它主要用作在 profile.yml
中标识该插件的配置头。例如,对于天气插件,可以设置 SLUG 为 “weather”,
1 | SLUG = 'weather' |
那么在 profile.xml 中如果要添加天气插件的配置,就应该以 weather
字段开头添加配置信息:
1 | weather: |
每一个插件都有一个默认的可选配置项 enable
,用来决定是否开启或关闭这个插件。默认值为 true ,即开启该插件。
接下来需要实现 handle()
函数。
1 | def handle(text, mic, profile, wxbot=None): |
这个函数接收四个参数:
text
是STT识别到的用户指令;mic
是麦克风和喇叭模块,最常用的是通过调用 mic.say()
函数来让喇叭说话;profile
是用户配置信息,它是一个字典,记录了 ~/.dingdang/profile.yml
的全部内容;wxbot
是一个微信机器人实例,可以利用它给用户发送微信消息。Camera 插件提供了通过微信发送照片给用户的例子。我们可以把心知的 Demo 给出的 fetchWeather
函数做一点调整,放进代码中方便复用:
1 | def fetch_weather(api, key, location): |
显然,KEY
和 location
应该作为用户的配置项,允许用户在配置文件中进行设置。因此我们可以在 profile.yml 配置文件中添加如下配置:
1 | # 天气 |
接下来在 handle
函数中调用 fetch_weather
函数,得到天气信息:
1 | def handle(text, mic, profile, wxbot=None): |
完成后,可以重启下叮当,看看插件是否能正常工作。
插件正常工作后,可以将该插件发布到 dingdang-contrib ,让更多人用上您的插件。
首先先访问 dingdang-contrib 的 Github 主页 ,点击右上角的 【fork】 按钮,将仓库 fork 到自己的账户。如果之前已经 fork 过,这一步可以跳过。
fork 完仓库后,在您的账户下也会有一个 dingdang-contrib 项目,点击绿色的 【Clone or download】 按钮,记下新的仓库的地址。
之后在树莓派中执行如下命令,添加新的仓库地址:
1 | cd ~/.dingdang/contrib |
将新建的插件提交推送到您的 dingdang-contrib 仓库中:
1 | git add Weather.py |
完成后访问您的 dingdang-contrib 仓库主页,可以看到一个创建 pull request 的提示:
点击 【compare and pull request】 按钮,进入 pull request 创建页面,申请将您的改动合并到 dingdang-contrib 项目中:
在里头认真填写插件的用途、使用示例和配置项。完成后点击 【Create pull requset】 ,完成创建,等待 dingdang-robot 组织的审核。
一旦审核通过,您的插件就发布成功了。
为了让更多人了解您的插件的用途,还应该到 dingdang-contrib 的 Wiki 中添加该插件的用途。先在首页中增加一条插件记录:
完成后首页就增加了您创建的插件的记录:
点击该插件的链接,进入插件详情页创建页面,填入和刚刚创建 pull request 时相同的内容,即插件的用途、使用示例和配置项即可(也可以根据需求增加更多信息)。
]]>然而,不论是 Amazon Echo 、Google Home 还是微软 Cortana 音箱,在国内的使用都是个问题。虽然国内也有类似的智能音箱产品,但我没有用过这些产品,不知道可定制性如何。比如,如果我需要开发个功能让它告诉我某种面包的配方是什么,这些产品就不一定能做到了。考虑再三,我决定自己动手写一个。整个项目用了差不多三个星期的业余零碎时间。
先放上项目主页:http://dingdang.hahack.com
下面分享一下我在开发这个项目过程中的心得。
首先要解决的是硬件问题。我选择在 Raspberry Pi 上开发。于是我买了块 Raspberry Pi 三代主板。麦克风和音响方面,出于美观的目的,买了个自带音响的 USB 全向会议麦克风。整套设备看起来就像这样:
后面觉得这个麦克风自带的音响音质太一般了,所以我又外接了一个小音箱。然后再插了一个摄像头,用来实现拍照功能。最终的完全体进化成了这样:
硬件有了,接下来就得开始写软件了。主要的框架借鉴了 Jasper 项目,并加入了我自己的定制和想法。这里说说一些有意思的部分。
智能音箱要解决的一个最重要的问题就是如何接收指令。这里头主要涉及两个问题:
被动唤醒阶段的基本策略是:每次以 16000 的采样率录制 1024 个采样作为一个采样集,然后对采样集进行信号强度估计,当某个采样集信号强度大于一个阈值时,就认为可能接受到了指令。然后持续录制多 1 秒时间,再转交给语音识别模块。当语音识别模块认为是唤醒词时,进入主动聆听阶段。
主动聆听的策略与被动唤醒基本相似,每次以 16000 的采样率录制 1024 个采样作为一个采样集,然后对采样集进行信号强度估计,当某个采样集信号强度低于一个阈值约 1 秒的时间时,就认为用户已说完了指令。当然还要考虑环境吵杂,一直处于聆听的可能。因此可以再加一个超时保护,超过 12 秒就结束聆听。
说说STT(语音识别)引擎和TTS(文本转文本)引擎的选择。由于被动唤醒会试图识别所有听到的内容,出于隐私保护的目的,应该使用离线的语音识别引擎,因此我选择的是 PocketSphinx 。而对于主动聆听,由于是在唤醒阶段才会进行转换,进入主动聆听前会有蜂鸣提示,用户也会清楚此时叮当正在听他们说话,相对来说隐私泄露的可能性就比较低,因此我选择的是在线的百度 STT 语音识别服务,也省下了扩展语音识别模型的工夫,有利于更好地实现插件可扩展。TTS 引擎方面同样也先支持了百度的语音合成。
在实际测试中,PocketSphinx 的识别出乎意料的好。由于我的离线指令集只有几个候选唤醒词,PocketSphinx 对这些唤醒词的识别非常灵敏,甚至有时候其他声音也可能被误当成唤醒词而唤醒叮当。但即使被意外唤醒了,不去理会叮当就可以了。
相比之下,百度的语音识别就比较迟钝了。有时候明明我发音很清晰了,还是会识别成另外的含义。通过在百度的语音识别平台上传自定义的语音识别词库 可以提高识别的准确率。另外,由于我用的是 Restful API,网速比较差的时候响应也比较慢。我在家用的是 10M 带宽的网络,反应速度还算可以接受。我准备后面尝试接入更多的语音识别平台,看看识别速度和准确度方面能否有所提升。
下面这个视频是我与叮当对话的演示。我把唤醒词设置成了“小梅”:
一个问题是当回答内容比较长(比如问叮当当天的新闻)时,合成语音的耗时会变得很长,给人的感受是叮当的响应很慢。所以我加了个 read_long_content
的选项。当内容过长时,改成发送到用户的邮箱或者微信。下面这段音频是一个例子:
叮当最好玩的部分当然就是玩插件了,通过写插件可以让叮当接入各种各样的服务,完成各种各样的事情。我在叮当里也内置了几个插件[1]。为了方便用户扩展,我把 ~/.dingdang/contrib
设定为第三方插件目录,允许让用户在里头编写插件并提交到 dingdang-contrib 项目共享。
下面这个视频是 Camera 插件的演示[2]:
另外,如果接入了微信,还可以让叮当安静地拍一张家里的照片,而不发出任何声音。下面这个视频演示了如何使用微信与家里的机器人交互,包括远程控制拍照。
这对于需要远程监控家里的情况的用户而言就非常方便了,比如家里有小孩的情况。
既然是智能音箱,当然少不了播放音乐的功能。所以我额外写了个播放网易云音乐的插件 NetEaseMusic 。出于版权考虑,并不集成进官方插件中,而是放进 dingdang-contrib 里头。
这个插件的实现比较复杂。普通的插件接受到指令,响应完就退出了。而为了能支持各种指令控制音乐播放,这个插件在接收到播放控制指令后并不退出插件,而是进入一个播放器模式,这个模式主动聆听得到的指令只会在播放控制指令集中匹配,其他的插件指令都不起作用。只有当用户要求退出播放时才回到普通模式。NetEaseMusic 的播放控制指令如下:
指令 | 相同指令 | 用途 |
---|---|---|
播放音乐 | - | 进入音乐播放模式。在音乐播放模式下,其他的插件功能将不可用。 |
下一首 | 切歌, 下一首歌, 下首歌 | 切换到下一首歌。如果没有下一首歌,就回到列表中第一首歌 |
上一首 | 上一首歌,上首歌 | 切换到上一首歌。如果没有上一首歌,就跳到列表中最后一首歌 |
大声点 | 大点声,大声 | 调高播放音量 |
小声点 | 小点声,小声 | 降低播放音量 |
随机播放 | - | 随机播放列表中的音乐 |
顺序播放 | - | 顺序播放列表中的音乐 |
暂停播放 | - | 暂停音乐的播放 |
播放 | 继续 | 继续音乐的播放 |
榜单 | - | 播放推荐榜单 |
歌单 | - | 播放用户的歌单(如果有多张,将只播放第一张) |
结束播放 | 退出播放,停止播放 | 退出音乐播放模式。 |
搜索 | 查找 | 搜索歌曲/歌手。将自动播放搜索结果。 |
什么歌 | - | 正在播放的是什么歌 |
实现这个插件的过程中还参考了 Vellow 的 MusicBox 项目[3]以及 yaphone 的 RasWxNeteaseMusic 。为了方便重用,我把 MusicBox 的核心 API 抽离了出来封成了一个 MusicBoxApi 库 。比较坑爹的是就在我准备发布叮当的前几天,老的获取音乐地址的方式彻底不能用了,而新的接口批量获取的地址不知道为什么是乱序的,于是我只能在播放每首歌前都调用一下新版的获取地址的 POST 接口,又增加了一点响应时间[4]。
下面这段音频是使用叮当控制音乐播放的演示:
完成了音乐播放功能后,叮当的好玩程度提高了很多。以前要听歌,至少得把电脑或者手机打开。现在只需要喊一声叫叮当播放歌曲就可以了。想换歌、搜索歌曲、调节音量都是说句话就搞定的事情,生活幸福指数大幅提升 ^_^
。
对于有 Coding 能力的 Hacker 而言,自己动手做一个智能音箱,不仅可以当做业余练手项目,还可以自由地定制硬件模块,并实现自己需要的各种功能,这远比直接购买一个 Amazon Echo 有趣得多。
后面我计划做的事情有:
更重要的,我更希望能有其他有兴趣的朋友参与进来,一同开发完善这个智能音箱项目。我相信,这种个性化服务的产品本身就应该是完全可定制的。而您的加入可以使叮当变得更智能!
而对于存在二进制文件的仓库,情况就变了:Git 并不能很好地支持二进制文件的增量提交,每次更新一个二进制文件,就相当于把这份文件的完整内容再往仓库里扔。久而久之,这个仓库就会变得非常大,影响代码拉取速度。
举一个实际的例子,为了加快应用的构建速度,我们团队的框架先会编译成 SDK ,再交由上层构建应用。框架 SDK 也是一个独立的 Git 仓库,里头包含了大量的二进制包:
由于框架也有多个分支,每个分支的迭代速度比较快,SDK 仓库的体积在三个月的时间内就膨胀到了 1G 。
如此庞大的仓库体积让第一次拉代码的同事叫苦不迭。一次全新的 clone ,即使拉取速度达到了 5.01 MB/s,在 framework 这个模块上就需要花上大约 7 分钟的时间:
当很多人同时拉代码时,还有很大概率因为 HTTP 超时而拉取失败:
为了解决这个问题,我先后尝试了几种方案。
第一个思路非常 intuitive :既然 HTTP 的拉取不稳定,那改成 SSH 如何呢?SSH 的长连接总比 HTTP 稳定吧?
1 | git remote remove origin # 删除原来的http仓库地址 |
这个思路被证实是有效的。通过修改这几个模块的仓库地址为 SSH ,仓库的拉取成功率提升了很多,出现 RPC Failed 的情况也变少了。
然而,这种方案依然无法解决拉取速度慢的问题,完整的拉取该模块的耗时并不比 HTTP 方式快(甚至可能更慢):
另外,这种方式要求每个人都配好 SSH Keys ,否则拉取仓库时也会直接报错。这对于刚接触 Git 的同事而言又增加了一点 cognitive load 。
第二个思路是在初次拉取的时候不完整克隆整个工程,而是只克隆一个分支,这样也能减少 N 倍的时间。Git 允许带上 --single-branch -b <分支名>
选项,指定只拉取某一分支:
1 | git clone --single-branch -b <分支名> http://your-site.com/your-group/your-repo.git |
用这种方法确实减少了一定的时间,但耗时依然可能很长。以我们的框架 SDK 仓库为例,单纯拉一个 master_dev 分支也要 3 分钟左右的时间。
没有数量级别的减少,也就意味着不久之后单个分支的拉取时间也会超过现在整个仓库的完整克隆时间。
大部分人使用 SDK 时并不需要检出历史版本,对这些人而言,只需要拿到需要的一个快照就可以满足构建需求了。因此方案三就是限定克隆时的深度来加快拉取速度。Git 允许带上 --depth <深度>
来指定拉取深度。例如只拉取分支最新的快照:
1 | git clone --single-branch -b <分支名> --depth 1 http://your-site.com/your-group/your-repo.git |
由于只拉取最新快照,用这种方式的拉取速度就快了很多。以我们的框架 SDK 仓库为例,拉 master_dev 最新的快照只需要不到 6 秒的时间。
浅克隆虽然能够解决代码拉取的问题,但可想而知这样拉取下来的仓库是不完整的,它缺失了所有历史记录,也不能在这个仓库上提交新的内容。对于框架的开发人员,为了能够提交新内容,依然需要花长时间去克隆完整的仓库。因此浅克隆依然不是一个完美的方案。
虽然 Git 本身并不能很好地支持二进制大文件的版本控制,但幸运的是已经出现了一些扩展能够帮助 Git 胜任这些工作。我所选择的扩展就是由 Github 团队开发的 Git-LFS 。
Git-LFS 的原理并不复杂:大文件不再支持添加到仓库中,而是存储到另外的 LFS 服务器上。仓库中只保留这些文件的文本链接。当拉取仓库时,Git-LFS 的钩子将自动把这些文本链接恢复成 LFS 中的实际内容。一图胜千言:
选择 Git-LFS 的一个首要原因是 Gitlab 原生提供了对 Git-LFS 的支持(有趣的是,就在我写这篇文章的时候,Coding 也宣布了对 Git LFS 的支持)。要在 Gitlab 中开启 Git-LFS 非常简单:
Git LFS
项目;gitlab_rails['lfs_enabled']
项目设置成 true
;gitlab_rails['lfs_storage_path']
项目设置为本地的一个已存在目录。这个目录就是实际的 LFS 存储目录。gitlab-ctl reconfigure
重新配置 Gitlab;gitlab-ctl restart
重启 Gitlab ,使配置生效。至此服务端就配置完成了。
下载 Git LFS 。解压完后执行:
1 | git lfs install |
完成工具的安装。这步骤只需要做一次。这个步骤实际做的事情是给 git 加上 lfs 命令,另外还创建了 post-checkout、post-commit、post-merge、pre-push 几个全局钩子。当我们在一个使用 LFS 的仓库执行诸如 checkout
、commit
、merge
、push
的 Git 操作时,将触发这些钩子自动地维护用 LFS 管理的文件。
接下来就可以开始改造仓库,把大文件都改用 LFS 来管理。
1 | git lfs track "*.jar" |
这几步执行完会在仓库中创建一个 .gitattribute 文件:
1 | cat .gitattributes |
Git 的钩子就是根据这个文件来确定当前仓库是否有使用 LFS 管理的文件的。所以这个文件一定要确保添加进仓库中:
1 | git add .gitattributes |
完成后像往常一样暂存和提交文件即可:
1 | git add foo.jar |
要注意的是,这个改造过程只会把当前这次 commit 的指定类型文件改成用 LFS 才存储,而不会影响所有历史记录。对于我们的 SDK 仓库,仓库本身已经非常庞大,直接这么改造是没有任何瘦身效果的。所以最好的做法就是重新创建一个仓库,把各个分支最新的快照同步过来。
由图可以看出,重新创建的这个仓库,把大部分的二进制大文件都改用了 LFS 来存储,整个仓库的大小从 1G 减小到 3M 不到!
测试对这个新的仓库进行克隆,由于本身仓库很小,一下子就克隆下来了。之后 Git LFS 的全局钩子将自动将当前仓库里的 LFS 链接文件恢复成真正的文件:
由于这个仓库的二进制包多达64个,整个克隆过程的时间主要花在下载这些二进制包,总耗时约为 43 秒。虽然没有浅克隆快,但这样的方式拉下来的仓库是完整的仓库,而且对普通开发者而言是完全透明的操作(他们甚至不需要知道 LFS 是什么),因此是更加理想的方案。
虽然 Git-LFS 很好地解决了大文件的版本控制问题,但实际应用到实际团队中时也不见得能顺风顺水。在我将它推广到团队的项目中时,就遇到了几个水土不服的问题。下面整理一下,方便后来人。
第一个遇到的问题就是钩子的覆盖问题。前面我们提到 Git-LFS 其实是利用全局钩子来关联 Git 与 LFS 的。当你的工程中也加了钩子时,这时候就要格外小心了。
以我们的工程为例,我给每个子模块都加了个 pre-push 钩子用来做 push 前检查:
问题来了,这个 pre-push 钩子的优先级会高于全局的那个 Git LFS 钩子,因此使得 Git LFS 的 pre-push 失去作用。而这个钩子非常重要:它的作用是在 push 的时候把用 Git LFS 跟踪管理的文件上传到 LFS 服务器上。如果这些文件没有上传成功,别人拉取仓库就会报如下错误:
1 | Downloading hotfix/plugin/commons-io-2.4.jar (180.80 KB) |
解决办法就是将 Git LFS 钩子的内容与自定义钩子相结合。这是我对 Git LFS 的 pre-push 钩子的改写:
1 | !/bin/sh |
最后一行的作用就是先执行 git lfs pre-push 确保正确上传 LFS tracking 的文件,然后再执行 hooks 中的 pre-push-custom 钩子进行其他自定义的检查。
Gitlab 对 Git-LFS 也存在着不足。当我完成了几个大仓库的改造之后,我发现新的仓库在本地可以顺利编译,但在构建站却死活编译不了,报了类找不到的错误:
本地构建和构建站构建在代码拉取上面有一个区别:为了加快代码拉取速度,我们在构建站并不使用克隆仓库的方式来拉取代码,而是采用下载 Zip 包的方式。所以我把这个仓库的 Zip 包下载了下来:
这个类是在其中一个 jar 包里定义的,而解压发现 jar 包明明已经下载下来了:
尝试使用 JD-Gui 打开这个 jar 包,发现这个包打不开。
那这个文件究竟是什么东西?打印它的内容,真相浮出水面:
这是个链接文件!说明 Gitlab 并没有将它恢复成实际的文件内容!仔细观察这些二进制文件,我发现它们的大小全部都在 130 字节左右,这意味着这些文件全都没有被恢复。
不幸的是,由于下载下来的内容不再是个 Git 仓库,这些链接文件已无法恢复成实际的文件内容。
我认为这个是 Gitlab 的问题,于是给 Gitlab 提了一个 bug ,而一个开发人员告诉我类似的问题在去年 3 月份已经有人提过,而目前还未修复 --bb
(Coding 也有相同的问题,哈哈)。
找到这个原因后,对症下药就简单了:既然下载 Zip 包的方式没法恢复大文件的内容,那就改成用浅克隆。于是我改写了下构建站的代码拉取脚本,将使用 Git LFS 管理大文件的几个模块由下载 zip 的方式改成浅克隆,终于解决了编译问题!
本文列举了几种二进制大文件导致仓库过大的解决方案。其中,使用 Git-LFS 的方案是一种比较理想的选择。但在实际使用中,一定要小心处理 Git-LFS 可能带来的问题,希望本文的若干踩坑总结也能对读者有所帮助。
]]>在继续写数学系列前,我想切回去之前的 Git 系列写点东西。我想写系列文章也可以像操作系统的进程调度一样,一个系列暂时写不动了,先 保存现场
跳去另一个 topic 写点东西,同时也给自己留点 buffer 再酝酿一下这个暂时 中断
的系列。等这个系列酝酿够了,再 恢复现场
,继续还这个系列的技术债。
对于一个规模较大的企业,存在多个 Gitlab 站点是很常见的事情。
比如,我们团队在公司发布统一的 Gitlab 之前早已经搭了一个团队用的 Gitlab ,当公司开始推 Git 时,由于我们已经对自己团队的 Gitlab 做了大量的定制,因此并不打算迁移到公司的 Gitlab 。
自己搭建 Gitlab 的好处是可以随心所欲的进行定制,像加远程钩子之类的东西想加就加。但缺点就是平台的维护成本也落到了自己身上。相比之下,公司 Gitlab 则没有什么维护成本,服务的稳定性由更专业的运维人员保证,也不用考虑扩容的问题,但灵活定制就别想了。如果能够实现 Gitlab 间的数据自动同步,我们可以没有顾忌的使用自己的 Gitlab 平台,一旦出现问题,再无痛迁移到公司的 Gitlab 。这样一方面避免了单点问题,节省了维护成本;另一方面也能尽可能保证灵活可定制。本文想讨论的就是多个 Gitlab 站点间的数据同步问题。
要实现数据同步,Gitlab 官方提供了一套 备份恢复机制 。但这套机制并不能很好地满足我们的需求:
出于以上的考虑,我们自己设计了一套同步工具。与 Gitlab 官方的备份恢复机制相比,它具有以下一些优点:
下面将逐步说明整套同步的方案。为了方便描述,我把同步原 Gitlab 站点称为 A Gitlab,把同步目标站点称为 B Gitlab 。
数据的自动同步主要经历如下几步:
利用 Gitlab API 列举出 A Gitlab 中的所有 groups,然后在 B Gitlab 中自动新建不存在的组织。
列举 Gitlab 的所有组织:
1 | GET /groups |
返回示例:
1 | [ |
根据这个可以获取组织名(name)、组织路径(path)和组织描述(description)。
同样使用类似接口获取 B Gitlab 的所有组织。如果发现 A Gitlab 的某个组织在 B Gitlab 里不存在,可以在 B Gitlab 新增一个组织:
1 | POST /groups |
参数:
同步所有组织的所有仓库的代码和 wiki 文档到 B Gitlab 。
获取一个组织的所有仓库信息接口:
1 | GET /groups/:id/projects |
参数:
id
, name
, path
, created_at
, updated_at
或 last_activity_at
字段来排序。默认使用 created_at
。ci_enabled
字段来排序。返回示例:
1 | [ |
之后利用同个接口结合 search
参数判断 B Gitlab 上的该组织是否存在同名项目。
如果不存在该项目,可以导入该项目:
1 | POST /projects |
参数:
true
,相当于设置 visibility_level
为 20完成后 B Gitlab 即会导入 A Gitlab 中的对应仓库。
如果该项目已存在,可以利用我开源的一个 代码同步工具 来实现两个仓库之间所有分支的同步。
根据 A Gitlab ,将 B Gitlab 的已激活用户添加到组织中。并从 B Gitlab 删除 A Gitlab 中已 block 或者已移除的用户。
这里要注意的是两个站点间的用户的关联问题。我们的 Gitlab 在一开始就要求使用公司邮箱注册,而公司的 Gitlab 同样也是使用邮箱的 LDAP 账户体系,因此可以利用邮箱来关联两个站点间的账户。
获取 Gitlab 某个组织的所有用户:
1 | GET /groups/:id/members |
返回结果示例:
1 | [ |
找出两个 Gitlab 上用户的差异,并执行如下操作:
state
字段为 blocked
,则该成员可能已离职或 transfer,将该成员从 B Gitlab 中删除;添加组织成员的 API :
1 | POST /groups/:id/members |
参数:
删除组织成员的 API :
1 | DELETE /groups/:id/members/:user_id |
编辑组织成员的 API :
1 | PUT /groups/:id/members/:user_id |
另外还需要考虑 B Gitlab 不存在该用户的情况,需做容错处理。
项目的权限控制信息主要包括项目成员设定及分支保护设定。
项目成员的同步与组织成员的同步大同小异。
获取项目成员的 API :
1 | GET /projects/:id/members |
添加项目成员的 API :
1 | POST /projects/:id/members |
删除项目成员的 API :
1 | DELETE /projects/:id/members/:user_id |
编辑项目成员的 API :
1 | PUT /projects/:id/members/:user_id |
首先获取 A Gitlab 中一个仓库的所有分支:
1 | GET /projects/:id/repository/branches |
返回示例:
1 | [ |
其中 protected
表示该分支是否受到保护。
根据分支的保护情况修改 B Gitlab 上的分支。
保护某个分支:
1 | PUT /projects/:id/repository/branches/:branch/protect |
取消某个分支的保护:
1 | PUT /projects/:id/repository/branches/:branch/unprotect |
对于同时搭建了多个 Gitlab 的团队,多个 Gitlab 间的数据同步是值得去实现的事情。它一方面能避免单点问题,降低小团队的维护成本,一方面也能尽量保证小团队的定制灵活性。因此,文本列举了组织同步、仓库代码和wiki同步、组织关系同步、权限控制信息同步等四大方面的同步的方案。我将这四个类型的同步可以写成了三个工具:
group_sync
- 处理组织同步project_sync
- 处理项目代码、wiki同步member_sync
- 处理组织关系、权限控制信息的同步设定每天自动按顺序执行这几个工具的同步,完成后邮件汇报同步结果。作为实例,这是我们每天都会收到的同步结果邮件(出于保护隐私的考虑,我修改了部分隐私信息):
由于项目变动、成员变动比较频繁,当希望在计划任务之前进行某方面同步,仍然可以单独手动运行以上工具完成所需方面的同步。对于一些同步及时性要求更高的仓库,则可以通过加 post-receive 钩子调用 代码同步工具 来实现 push 后即时同步。
要注意的是,这个同步方案并没有保证 A Gitlab 的所有数据都能被完整地同步。在设计同步策略的时候,我跳过了下述类型的同步:
在前面两篇文章中,我简要概括了线性代数中两个最基本的数据表达方式:矩阵 和 向量。有了这两个数学工具作为基础,我们可以再进一步,讨论下面一些内容:
本篇文章将作为线性代数子系列的最终篇。
阶梯形矩阵是一类非常实用的工具,可以帮助我们求解出线性空间的基,这就能用在诸如计算解不唯一的方程组之类的问题上。
若矩阵 \(\mathbf{A}\) 满足两条件:
则称此矩阵 \(\mathbf{A}\) 为阶梯形矩阵。
示例:
\[\begin{bmatrix}2 & 0 & 2 & 1 \\0 & 5 & 2 & -2 \\0 & 0 & 3 & 2 \\0 & 0 & 0 & 0\end{bmatrix}\]
若矩阵 \(\mathbf{A}\) 满足两条件:
则称此矩阵 \(\mathbf{A}\) 为行简化阶梯形矩阵。
示例:
\[\begin{bmatrix}2 & 0 & 2 & 1 \\0 & 5 & 2 & -2 \\0 & 0 & 3 & 2 \\0 & 0 & 0 & 0\end{bmatrix}\]
若矩阵 \(\mathbf{A}\) 满足两条件:
则称此矩阵 \(\mathbf{A}\) 为行最简形矩阵。
对如下矩阵
\[\begin{bmatrix}1 & 2 & 1 & 1 & 7\\ 1 & 2 & 2 & -1 & 12\\ 2 & 4 & 0 & 6 & 4\end{bmatrix}\]
,使用初等变换可以将这个矩阵转换成如下的形式:
\[\begin{bmatrix}1 & 2 & 1 & 1 & 7\\ 1 & 2 & 2 & -1 & 12\\ 2 & 4 & 0 & 6 & 4\end{bmatrix}\rightarrow\begin{bmatrix}1 & 2 & 1 & 1 & 7\\ 0 & 0 & 1 & -2 & 5\\ 2 & 4 & 0 & 6 & 4\end{bmatrix}\rightarrow\begin{bmatrix}1 & 2 & 1 & 1 & 7\\ 0 & 0 & 1 & -2 & 5\\ 0 & 0 & -2 & 4 & -10\end{bmatrix}\rightarrow\begin{bmatrix}1 & 2 & 1 & 1 & 7\\ 0 & 0 & 1 & -2 & 5\\ 0 & 0 & 0 & 0 & 0\end{bmatrix}\rightarrow\begin{bmatrix}1 & 2 & 0 & 3 & 2\\ 0 & 0 & 1 & -2 & 5\\ 0 & 0 & 0 & 0 & 0\end{bmatrix}\]
行最简形非常实用。例如,对于下面的方程组:
\[\left\{ \begin{eqnarray} x_1 + 2x_2 + x_3 + x_4 &=& 7 \\\x_1 + 2x_2 + 2x_3 - x_4 &=& 12 \\\2x_1 + 4x_2 + 6x_4 &=& 4\end{eqnarray}\right.\]
只有三个方程,肯定无法求解出四个未知数(此时如果在用 numpy.linalg.solve
求解这个矩阵会引发 LinAlgError
),但是通过化成行最简形,我们可以进一步找出变量的限制关系。先将方程组表达成增广矩阵形式:
\[\begin{bmatrix}1 & 2 & 1 & 1 & 7\\ 1 & 2 & 2 & -1 & 12\\ 2 & 4 & 0 & 6 & 4\end{bmatrix}\]
这个矩阵和完全和我们上一步给出的矩阵相同,因此其行简化阶梯性就是
\[\begin{bmatrix}1 & 2 & 0 & 3 & 2\\ 0 & 0 & 1 & -2 & 5\\ 0 & 0 & 0 & 0 & 0\end{bmatrix}\]
对于方程组,非0首元位置对应的变量就叫做主元变量,其他的变量就叫做自由变量。例如上面的行最简形,\(x_1\) 和 \(x_3\) 是首元变量,\(x_2\) 和 \(x_4\) 就是自由变量。我们可以将方程改写成下面的形式:
\[\left\{ \begin{eqnarray} x_1 &=& 2 - 2x_2 - 3x_4 \\\x_3 &=& 5 + 2x_4\end{eqnarray}\right.\]
然后可以得到:
\[\begin{bmatrix} x_{ 1 } \\ x_{ 2 } \\ x_{ 3 } \\ x_{ 4 } \end{bmatrix}=\begin{bmatrix} 2 \\ 0 \\ 5 \\ 0 \end{bmatrix}+x_{ 2 }\underbrace { \begin{bmatrix} -2 \\ 1 \\ 0 \\ 0 \end{bmatrix} }_{ \vec{\mathbf{a}} } +x_{ 4 }\underbrace{\begin{bmatrix} -3 \\ 0 \\ 2 \\ 1 \end{bmatrix}}_{\vec{\mathbf{b}}}\]
观察这个结果,方程组的解集就是向量 \(\vec{\mathbf{a}}\) 和向量 \(\vec{\mathbf{b}}\) 的线性组合。这两个向量张成了 \(\mathbb{R}^1\) 中的一个平面。
在前面的内容中我们已经多少涉及到了一些关于空间、张成空间的知识了。有时候我们需要从一个空间 \(K\) 里头挑出一些向量张成一个新的空间 \(\mathbf{W}\) ,这个空间 \(\mathbf{W}\) 就是原来的向量 \(\mathbf{K}\) 的子空间。
子空间的引入有助于我们更专注于某类线性组合,从中找出这些子空间的特点,以及与原来的空间的关系。下面将列举几种典型的子空间。
矩阵 \(\mathbf{A}\) 的零空间 \(N(\mathbf{A})\) 就是由满足 \(\mathbf{A}\vec{\mathbf{x}}=0\) 的所有向量 \(\vec{\mathbf{x}}\) 的集合。
要求解一个矩阵的零空间,可以先将其化简成行最简形。例如矩阵 $\mathbf{A} = \begin{bmatrix} 1 & 1 & 1 & 1 \\ 1 & 2 & 3 & 4 \\ 4 & 3 & 2 & 1 \end{bmatrix} $,为了计算零空间,可以写出如下的等式:
\[\begin{bmatrix} 1 & 1 & 1 & 1 \\ 1 & 2 & 3 & 4 \\ 4 & 3 & 2 & 1 \end{bmatrix} \begin{bmatrix} x_1 \\ x_2 \\ x_3 \\ x_4 \end{bmatrix} = \begin{bmatrix} 0 \\ 0 \\ 0 \end{bmatrix}\]
展开得到如下的方程组:
\[\left\{ \begin{eqnarray} x_1 + x_2 + x_3 + x_4 &=& 0 \\\x_1 + 2x_2 + 3x_3 + 4x_4 &=& 0 \\\4x_1 + 3x_2 + 2x_4 + x_4 &=& 0\end{eqnarray}\right.\]
参考 化简成行最简阶梯形 一节里介绍的方法,先把上面的方程组表示成增广矩阵:
\[\begin{bmatrix} 1 & 1 & 1 & 1 & 0 \\ 1 & 2 & 3 & 4 & 0 \\ 4 & 3 & 2 & 1 & 0 \end{bmatrix}\]
然后将其转换成行最简形:
\[\begin{bmatrix} 1 & 0 & -1 & -2 & 0 \\ 0 & 1 & 2 & 3 & 0 \\ 0 & 0 & 0 & 0 & 0 \end{bmatrix}\]
最终求解得到:
\[\begin{bmatrix} x_{ 1 } \\ x_{ 2 } \\ x_{ 3 } \\ x_{ 4 } \end{bmatrix}=x_{ 3 }\underbrace { \begin{bmatrix} 1 \\ -2 \\ 1 \\ 0 \end{bmatrix} }_{ \vec{\mathbf{a}} } +x_{ 4 }\underbrace{\begin{bmatrix} 2 \\ -3 \\ 0 \\ 1 \end{bmatrix}}_{\vec{\mathbf{b}}}\]
因此矩阵 \(\mathbf{A}\) 的零空间就是由上式中的 \(\vec{\mathbf{a}}\) 向量和 \(\vec{\mathbf{b}}\) 向量张成的空间。即
\[N(\mathbf{A}) = span\left(\begin{bmatrix} 1 \\ -2 \\ 1 \\ 0 \end{bmatrix} \begin{bmatrix} 2 \\ -3 \\ 0 \\ 1 \end{bmatrix}\right)\]
另外,上面得到的这个行最简形有两个自由变量,就称矩阵 \(\mathbf{A}\) 的 零度 为 2。零度等于 \(\mathbf{A}\vec{\mathbf{x}} = 0\) 化成行最简形后自由变量的个数。
矩阵的列空间就是由每一列的向量张成的空间。对于矩阵 \(\underset { m\times n }{ \mathbf{A} } =\begin{bmatrix} \underbrace { \begin{bmatrix} a_{ 11 } \\ a_{ 21 } \\ \ldots \\ a_{ m1 } \end{bmatrix} }_{ \vec { \mathbf{ V }_{ 1 } } } & \underbrace { \begin{bmatrix} a_{ 12 } \\ a_{ 22 } \\\ldots \\ a_{ m2 } \end{bmatrix} }_{ \vec { \mathbf{ V_{ 2 } } } } & \ldots & \underbrace { \begin{bmatrix} a_{ 1n } \\ a_{ 2n } \\ \ldots \\ a_{ mn } \end{bmatrix} }_{ \vec { \mathbf{ V_{ n } } } } \end{bmatrix}\),那么矩阵 \(\mathbf{A}\) 的列空间就是
\[C(\mathbf{A}) = span(\vec{v_1}, \vec{v_2}, \ldots, \vec{v_n})\]
例如,矩阵 \(\mathbf{A} = \begin{bmatrix}1 & 1 & 1 & 1 \\ 1 & 2 & 3 & 4 \\4 & 3 & 2 & 1\end{bmatrix}\) 的列空间是 \(C(\mathbf{A}) = span\left(\begin{bmatrix}1 \\ 1 \\ 4\end{bmatrix}\begin{bmatrix}1 \\ 2 \\ 3\end{bmatrix}\begin{bmatrix}1 \\ 3 \\ 2\end{bmatrix}\begin{bmatrix}1 \\ 4 \\ 1\end{bmatrix}\right)\)
把一个矩阵化成行最简形后,这个矩阵的不相关主列(基底)的个数就称为矩阵的秩(Rank),或者叫维数。
例如,上面的矩阵 \(\mathbf{A}\) 化成最简形矩阵是(参考上节的化简结果):
\[\begin{bmatrix} 1 & 0 & -1 & -2 \\ 0 & 1 & 2 & 3 \\ 0 & 0 & 0 & 0 \end{bmatrix}\]
从结果可以看出这个矩阵的主列有 2 个,而且是线性无关的。所以矩阵 \(\mathbf{A}\) 的秩为 2 ,即 \(rank(\mathbf{A}) = 2\)。
在 Python 中,可以使用 Numpy 包中的 linalg.matrix_rank
方法计算矩阵的秩:
1 | a = np.matrix('1 1 1 1;1 2 3 4;4 3 2 1') |
1 | import numpy as np |
有了列空间的定义,行空间顾名思义就是矩阵的每一行转置得到的向量张成的子空间,也就是矩阵的转置的列空间,记为 \(R(\mathbf{A}) = C(\mathbf{A}^T)\)。
例如,矩阵 \(\mathbf{A} = \begin{bmatrix}1 & 1 & 1 & 1 \\ 1 & 2 & 3 & 4 \\4 & 3 & 2 & 1\end{bmatrix}\) 的行空间是 \(R(\mathbf{A}) = C(\mathbf{A}^T) = span\left(\begin{bmatrix}1 \\ 1 \\ 1 \\ 1\end{bmatrix}\begin{bmatrix}1 \\ 2 \\ 3 \\ 4\end{bmatrix}\begin{bmatrix}4 \\ 3 \\ 2 \\ 1\end{bmatrix}\right)\)。
矩阵 \(\mathbf{A}\) 的左零空间是 \(\mathbf{A}\) 的转置的零空间。即:
\[N(\mathbf{A}^T) = \left\{ \vec{\mathbf{x}} | \mathbf{A}^{T} \vec{\mathbf{x}} = \vec{\mathbf{0}} \right\} = \left\{ \vec{\mathbf{x}} | \vec{\mathbf{x}}^{T} \mathbf{A} = \vec{\mathbf{0}}^{T} \right\}\]
例如,矩阵 \(\mathbf{B} = \begin{bmatrix}1 & 1 & 4 \\ 1 & 2 & 3 \\1 & 4 & 2\\ 1 & 3 & 1\end{bmatrix}\) 的转置是矩阵 \(\mathbf{A} = \mathbf{A} = \begin{bmatrix}1 & 1 & 1 & 1 \\ 1 & 2 & 3 & 4 \\4 & 3 & 2 & 1\end{bmatrix}\) ,因此左零空间是 \(N(\mathbf{B^T}) = N(\mathbf{A}) = span\left(\begin{bmatrix} 1 \\ -2 \\ 1 \\ 0 \end{bmatrix} \begin{bmatrix} 2 \\ -3 \\ 0 \\ 1 \end{bmatrix}\right)\)
由于转置是对称的,所以矩阵 \(\mathbf{A}\) 的转置的左零空间也是矩阵 \(\mathbf{A}\) 的零空间。
假设 \(\mathbf{V}\) 是 \(\mathbb{R}^n\) 的一个子空间,那么 \(\mathbf{V}\) 的正交补 \(\mathbf{V}^{\bot}\) 也是一个子空间,定义为 \(\left\{\vec{\mathbf{x}} | \vec{\mathbf{x}} \vec{\mathbf{v}}=0\right\}\),也即是 \(\mathbb{R}^{n}\) 中所有正交于 \(\mathbf{V}\) 的向量所组成的子空间。
由于正交是对称的,所以正交补也是对称的。一个子空间的正交补的正交补依然等于这个子空间。
矩阵的零空间是行空间的正交补[1],即 \(N(\mathbf{A}) = R(\mathbf{A})^{\bot}\)。反过来,矩阵的左零空间是列空间的正交补,即 \(N(\mathbf{B}^T) = C(\mathbf{B})^{\bot}\)。
最小二乘法是一个实用的数学工具,利用它可以在方程无解的情况下给出近似解。在机器学习中,最小二乘逼近也是一个重要的拟合方法。
假设有一个方程
\[\underset{n\times k}{\mathbf{A}}\vec{\mathbf{x}} = \vec{\mathbf{b}}\]
无解。把上式写成:
\[\vec{a_1}\vec{\mathbf{x}} + \vec{a_2}\vec{\mathbf{x}} + \ldots + \vec{a_k}\vec{\mathbf{x}} = \vec{\mathbf{b}}\]
无解就意味着 \(\mathbf{A}\) 的所有列向量的张成空间不包括向量 \(\vec{\mathbf{b}}\) 。即 \(\vec{\mathbf{b}} \notin span(C(\mathbf{A}))\)。
我们可以通过最小二乘法求解出近似解。即是要让找出一些向量 \(\vec{\mathbf{x}^*}\) 使得 \(\left\|\vec{\mathbf{b}}-\mathbf{A}\vec{\mathbf{x}^*}\right\|\) 最小。用向量 \(\vec{\mathbf{V}}\) 代表 \(\mathbf{A}\vec{\mathbf{x}^*}\) ,有:
\[\left\|\begin{bmatrix}\vec{b_1}-\vec{v_1}\\\vec{b_2}-\vec{v_2}\\\ldots\\\vec{b_n}-\vec{v_n}\\\end{bmatrix}\right\|^2= (b_1-v_1)^2 + (b_2-v_2)^2 + \ldots + (b_n-v_n)^2\]
把这个值最小化的过程就叫做最小二乘逼近。
如何求出 \(\mathbf{A}\vec{\mathbf{x}^*}\) 这个近似值呢?从几何上考虑,列空间可以看成空间中张成的一个平面,而向量 \(\vec{\mathbf{b}}\) 并不落在这个平面上。但我们知道,在这个平面上与向量 \(\vec{\mathbf{b}}\) 最接近的向量就是它的投影!所以,
\[\mathbf{A}\vec{\mathbf{x}^*} = Proj_{C(\mathbf{A})}\vec{\mathbf{b}}\]
直接计算 \(Proj_{C(\mathbf{A})}\vec{\mathbf{b}}\) 并不简单。不过,\(\vec{\mathbf{b}}-\mathbf{A}\vec{\mathbf{x}}\) 其实就是 \(\mathbf{A}\vec{\mathbf{x}}\) 的正交补,所以一个简单的求解方法是将原来无解的方程左乘一个 \(\mathbf{A}\) 的转置再求解:
\[\mathbf{A}^T\mathbf{A}\vec{\mathbf{x}^*} = \mathbf{A}^T\vec{\mathbf{b}}\]
得出的解就是原方程的近似解。
问题:求解如下方程组
\[\left\{ \begin{eqnarray} x + y &=& 3 \\\x - y &=& -2 \\\y &=& 1\end{eqnarray}\right.\]
将三个方程表示的直线画出来,可以看出这三条直线并没有交点:
如何找出一个与三条直线距离最近的一个点呢?这时候我们的最小二乘逼近就派上用场了。
先将方程写成矩阵和向量的形式:
\[\underbrace{\begin{bmatrix}1 & 1 \\1 & -1 \\0 & 1\end{bmatrix}}_{\mathbf{A}}\underbrace{\begin{bmatrix}x \\y\end{bmatrix}}_{\vec{\mathbf{x}}}=\underbrace{\begin{bmatrix}3 \\-2 \\1\end{bmatrix}}_{\vec{\mathbf{b}}}\]
这个等式的最小二乘逼近就是:
\[\begin{align}\begin{bmatrix}1 & 1 & 0 \\1 & -1 & 1\\\end{bmatrix}\begin{bmatrix}1 & 1 \\1 & -1 \\0 & 1\end{bmatrix}\begin{bmatrix}x^* \\y^*\end{bmatrix}& = \begin{bmatrix}1 & 1 & 0 \\1 & -1 & 1\\\end{bmatrix}\begin{bmatrix}3 \\-2 \\1\end{bmatrix}\\\\begin{bmatrix}2 & 0 \\0 & 3\end{bmatrix}\begin{bmatrix}x^* \\y^*\end{bmatrix}& =\begin{bmatrix}1 \\6\end{bmatrix}\end{align}\]
由于是二阶方程,可以很容易求出矩阵 \(\begin{bmatrix}2 & 0 \\ 0 & 3\end{bmatrix}\) 的逆是 \(\begin{bmatrix}\frac{1}{2} & 0 \\ 0 & \frac{1}{3}\end{bmatrix}\),所以:
\[\begin{bmatrix}x^* \\y^*\end{bmatrix}=\begin{bmatrix}\frac{1}{2} & 0 \\ 0 & \frac{1}{3}\end{bmatrix}\begin{bmatrix}1 \\6\end{bmatrix}=\begin{bmatrix}\frac{1}{2} \\2\end{bmatrix}\]
因此 \(\begin{bmatrix}\frac{1}{2} \\ 2 \end{bmatrix}\) 就是方程组的近似解。
在 Python 中,可以使用 numpy.linalg.lstsq
方法来求解最小二乘逼近。
1 | 1, 1], [1, -1], [0, 1]]) a = np.array([[ |
numpy.linalg.lstsq
的返回包括四个部分:
b
是二维的,那么这个逼近的结果有多个列,每一列是一个逼近解。对于上例,逼近解就是 \(\begin{bmatrix}0.5 \\ 2 \end{bmatrix}\) 。b - a*x
的长度的和。对于上例,残差是 1.5 。a
的秩。对于上例,矩阵 a
的秩为 2 。a
的奇异值。对于上例,矩阵 a
的奇异值为 \(\begin{bmatrix}1.73205081 \\ 1.41421356\end{bmatrix}\)问题:给定4个坐标点 \((-1, 0)\), \((0, 1)\), \((1, 2)\), \((2, 1)\) ,求一条经过这些点的直线 \(y=mx+b\)。
将四个点画图如下:
显然这样的直线并不存在。然而我们能够使用最小二乘逼近,找到一条尽可能接近这些点的直线。将四个点表示成方程组的形式:
\[\left\{\begin{eqnarray}f(-1) &= -m + b = 0\\\f(0) &= 0 + b = 1\\\f(1) &= m + b = 2\\\f(2) &= 2m + b = 1\end{eqnarray}\right.\]
还是那个套路,将方程组表示成矩阵和向量的形式:
\[\underbrace{\begin{bmatrix}-1 & 1 \\0 & 1 \\1 & 1 \\2 & 1\end{bmatrix}}_{\mathbf{A}}\underbrace{\begin{bmatrix}m\\b\end{bmatrix}}_{\vec{\mathbf{x}}}=\underbrace{\begin{bmatrix}0\\1\\2\\1\end{bmatrix}}_{\vec{\mathbf{b}}}\]
这个等式的最小二乘逼近就是:
\[\begin{align}\begin{bmatrix}-1 & 0 & 1 & 2 \\1 & 1 & 1 & 1\end{bmatrix}\begin{bmatrix}-1 & 1 \\0 & 1 \\1 & 1 \\2 & 1\end{bmatrix}\begin{bmatrix}m^*\\b^*\end{bmatrix}&=\begin{bmatrix}-1 & 0 & 1 & 2 \\1 & 1 & 1 & 1\end{bmatrix}\begin{bmatrix}0\\1\\2\\1\end{bmatrix}\\\\begin{bmatrix}6 & 2 \\2 & 4\end{bmatrix}\begin{bmatrix}m^*\\b^*\end{bmatrix}&=\begin{bmatrix}4\\4\end{bmatrix}\end{align}\]
容易求得 \(\begin{bmatrix}6 & 2\\2 & 4\end{bmatrix}\) 的逆为 \(\frac{1}{20}\begin{bmatrix}4 & -2\\-2 & 6\end{bmatrix}\),因此
\[\begin{bmatrix}m^*\\b^*\end{bmatrix} = \frac{1}{20}\begin{bmatrix}4 & -2\\-2 & 6\end{bmatrix}\begin{bmatrix}4 \\ 4\end{bmatrix} = \frac{1}{20}\begin{bmatrix}8 \\ 16\end{bmatrix} = \begin{bmatrix}\frac{2}{5} \\ \frac{4}{5}\end{bmatrix}\]
将直线 \(y = \frac{2}{5}x + \frac{4}{5}\) 绘图如下所示:
这就是所求的直线的近似解。
Python 示例如下:
1 | '-1 1;0 1;1 1;2 1') a = np.matrix( |
“特征”在模式识别和图像处理中是非常常见的一个词汇。我们要认识和描绘一件事物,首先得找出这个事物的特征。同样的道理,要让计算机识别一件事物,首先就要让计算机学会理解或者抽象出事物的特征。什么样的东西能当成特征呢?那必须是能“放之四海皆准”的依据,不论个体如何变换,都能从中找到这类群体共有的特点。例如,计算机视觉中常用的 SIFT 特征点 是一种很经典的用于视觉跟踪的特征点,即使被跟踪的物体的尺度、角度发生了变化,这种特征点依然能够找到关联。在机器学习中, 特征向量选取也是整个机器学习系统中非常重要的一步。
在线性代数中,“特征” 就是一种更抽象的描述。我们知道,矩阵乘法对应了一个变换,是把任意一个向量变成另一个方向或长度都大多不同的新向量。在这个变换的过程中,原向量主要发生旋转、伸缩的变化。如果矩阵对某一个向量或某些向量只发生伸缩(尺度)变换,而没有产生旋转的效果(也就意味着张成的子空间没有发生改变),这样的向量就认为是特征向量。
\[\mathbf{T}(\vec{\mathbf{v}}) = \underbrace{\mathbf{A}}_{n\times n}\vec{\mathbf{v}} = \underbrace{\lambda}_{特征值} \overbrace{\vec{\mathbf{v}}}^{特征向量}\]
其中, \(T\) 是一种线性变换,我们知道线性变换可以用矩阵向量积来表示,因此可以表示成 \(\mathbf{A}\vec{\mathbf{v}}\) 。\(\mathbf{A}\) 是一个 \(n\times n\) 的方阵。\(\vec{\mathbf{v}}\) 就是特征向量(Eigen Vector),也就是能被伸缩的向量
(要求是非 \(\mathbf{0}\) 向量),而 \(\lambda\) 是特征向量 \(\vec{\mathbf{v}}\) 所对应的特征值,也就是伸缩了多少
。如果特征值是负数,那说明了矩阵不但把向量拉长(缩短)了,而且让向量指向了相反的方向。
听起来很抽象,放个例子就清楚了。下图出自 wikipedia的《特征向量》一文:
在这个仿射变换中,蒙娜丽莎的图像被变形,但是中心的纵轴在变换下保持不变。(注意:角落在右边的图像中被裁掉了。)蓝色的向量,从胸部到肩膀,其方向改变了,但是红色的向量,从胸部到下巴,其方向不变。因此红色向量是该变换的一个特征向量,而蓝色的不是。因为红色向量既没有被拉伸又没有被压缩,其特征值为1。所有沿着垂直线的向量也都是特征向量,它们的特征值相等。它们构成这个特征值的特征空间。
非 \(\mathbf{0}\) 向量 \(\vec{\mathbf{v}}\) 是线性变化矩阵 \(\mathbf{A}\) 的特征向量,需要满足如下条件
\[det(\lambda \mathbf{I}_n - \underbrace{\mathbf{A}}_{n\times n}) = 0\]
其中,\(det\) 表示矩阵行列式,\(\lambda\) 是特征值,\(\mathbf{I}\) 是单位矩阵。
例如矩阵 \(\mathbf{A} = \begin{bmatrix}1 & 2 \\ 4 & 3\end{bmatrix}\) ,代入公式 2 得:
\[\begin{align} det\left( \lambda \begin{bmatrix} 1 & 0 \\ 0 & 1 \end{bmatrix}-\begin{bmatrix} 1 & 2 \\ 4 & 3 \end{bmatrix} \right) &=0 \\ det\left( \begin{bmatrix} \lambda & 0 \\ 0 & \lambda \end{bmatrix}-\begin{bmatrix} 1 & 2 \\ 4 & 3 \end{bmatrix} \right) &=0 \\ det\left( \begin{bmatrix} \lambda -1 & -2 \\ -4 & \lambda -3 \end{bmatrix} \right) &=0 \end{align}\]
所以有:
\[\begin{align} (\lambda -1)(\lambda -3)-8 & =0 \\ \lambda ^{ 2 }-4\lambda -5 &=0 \\ (\lambda - 5)(\lambda +1) &= 0\end{align}\]
因此 \(\lambda\) 的值为 5 或者 -1 。
在 Python 中,可以使用 numpy.linalg.eigvals
方法求解一个方阵的特征值:
1 | '1 2;4 3') a = np.matrix( |
前面说了变换矩阵必须是方阵,所以如果用在其他形状的矩阵上就会抛出 LinAlgError
错误:
1 | '1 2 3;4 3 1') b = np.matrix( |
变换矩阵 \(\mathbf{A}\) 的特征空间(特征向量张成的空间)可以用下面的等式来求解:
\[\mathbf{E}_{\lambda}=N(\lambda I_n - \mathbf{A})\]
例如上面的变换矩阵 \(\mathbf{A} = \begin{bmatrix}1 & 2 \\ 4 & 3\end{bmatrix}\) ,代入公式 3 得:
\[{ E }_{ \lambda }=N\left( \lambda I_{ n }-\begin{bmatrix} 1 & 2 \\ 4 & 3 \end{bmatrix} \right) =N\left( \lambda \begin{bmatrix} 1 & 0 \\ 0 & 1 \end{bmatrix}-\begin{bmatrix} 1 & 2 \\ 4 & 3 \end{bmatrix} \right) =N\left( \begin{bmatrix} \lambda -1 & -2 \\ -4 & \lambda -3 \end{bmatrix} \right) \]
当 \(\lambda = 5\) 时,
\[{ E }_{ 5 }=N\left( \begin{bmatrix} 4 & -2 \\ -4 & 2 \end{bmatrix} \right) \]
利用前面所学的 零空间的求解方法 ,得
\[{ E }_{ 5 }= span\left(\begin{bmatrix}\frac{1}{2} \\ 1 \end{bmatrix}\right) \]
同样地,当 \(\lambda = -1\) 时,
\[{ E }_{ -1 }= span\left(\begin{bmatrix}1 \\ -1 \end{bmatrix}\right) \]
在 Python 中,可以使用 numpy.linalg.eig
方法来求解方阵的特征值和特征向量:
1 | '1 2;4 3') a = np.matrix( |
得到的元组中,第一部分是特征值,和前面使用 numpy.linalg.eigvals
得到的结果完全一样;第二部分是特征向量,乍一看好像和我们上面求解的结果不一样,但如果我们这么写就完全一样了:\(\begin{bmatrix}-0.70710678\begin{bmatrix}1 \\ -1\end{bmatrix} & -0.89442719\begin{bmatrix}\frac{1}{2} \\ 1\end{bmatrix} \end{bmatrix}\)
终于完成了线性代数的系列。作为保研党,真正系统学习线性代数也就是在大一的时期,然后大学四年也没怎么用到数学,渐渐地就忘得差不多了。后来读研的时候虽然也用到些线性代数,但都是用到啥补啥,跟其他考研上来的同学比起来,心里面总是缺少一点底气。在中科院实习结束的时候,陈宝权老师和 Andrei 一直劝我读博,最后我婉拒了,其中一个原因也和这个“没底气”有关吧。而今我也工作快两年了,虽然还是没有读博的念头,但还是希望把数学捡起来,让自己也有底气一些。
读者也许会发现最近我很喜欢写系列文章。我倒不是为了要出书啦。只是我觉得既然博客的文章不止是写给我一个人看的,那么也得考虑读者的感受。如果把一篇文章写得太长,那就很难让人坚持读完,更别说这种公式很多的文章了;而如果写得太短,又不够完整,读得不够尽兴,于我也没有多少益处。
写系列文章其实最大的难点在于把握好 tradeoff 。像线性代数的知识点,两三篇文章的篇幅肯定是讲不完的,有些知识点如果再深入一下,就又拔出萝卜带出泥。比如最后一节提到特征向量,其实我还可以继续讨论特征值分解,然后又可以扯到奇异值分解。这样就很容易把整个系列写成像裹脚布一样了。所以,我只讲最基础的知识点,而且是可能对机器学习有帮助的,目的是让自己今后读相关的文章时有底气一些,至少不会在“秩”、“转置矩阵”这种最基础的知识点上犯晕。有了这个基础后,再去学习像奇异值分解之类的其他知识也会轻松很多。如果这个系列也能对读者们有所帮助,那就再好不过了。
如果您希望将这个线性代数子系列保存为书签,作为后面的工具文来查阅,我建议您保存我在wiki上的线性代数笔记(跳转入口)。因为我的 wiki 的更新频率会更频繁一些。且日后随着我的学习还可能继续添加一些新的内容。
下篇文章我将继续从一个机器学习工程师的角度,开始回顾微积分的基础知识点。