好未来技术团队

好未来技术团队 查看完整档案

北京编辑  |  填写毕业院校好未来  |  技术 编辑填写个人主网站
编辑

好未来作为一家科技驱动的教育企业,始终坚持“爱和科技让教育更美好”的使命。
一直以来,好未来技术团队致力于教育科技技术的研究与创新。这里是好未来技术团队的对外窗口,每周推送精选技术文章,欢迎大家关注。

个人动态

好未来技术团队 发布了文章 · 1月15日

Android解耦式so库加载方案

背景说明

在业务开发过程中经常会进入一些三方sdk,这些三方的sdk引入so库,有些so库文件还比较大,这时候我们就需要考虑so库从网络获取异步加载,减少发布包的体积

image.png

传统方案

关于so异步加载方案,网上的资料随便搜下大把,核心思想
so库文件放到网络->下载到本地沙盒->通过System.load载入
看起来挺简单的 然而挺多资料没提到的是

so库存在依赖关系

比如当你把libunity.so下载下来通过
System.load("/data/data/com.example.soload/files/libunity.so")
加载的时候会得到如下异常

java.lang.UnsatisfiedLinkError: dlopen failed: library “libmain.so” not found
at java.lang.Runtime.loadLibrary0(Runtime.java:1071)
at java.lang.Runtime.loadLibrary0(Runtime.java:1007)
at java.lang.System.loadLibrary(System.java:1667)
原因分析

image.png

解决方案

载入libunity.so之前需要先载入libmain.so

扩展思考

我们写代码的时候怎么控制加载顺序呢
https://github.com/facebook/S...
https://github.com/KeepSafe/R...
以上2个开源库 都有处理相关逻辑
核心思想就是解析so库ELF格式,分析依赖并递归,直到依赖库都加载完成再载入自身
这里不扩展开来 有兴趣的伙伴可以看源码研究下

text relocations问题

当你把liblbs.so下载下来通过

System.load("/data/data/com.example.soload/files/liblbs.so")的时候又出现意外了
java.lang.UnsatisfiedLinkError: dlopen failed: “/data/data/com.example.soload/files/liblbs.so” has text relocations (https://android.googlesource....
at java.lang.Runtime.load0(Runtime.java:938)
at java.lang.System.load(System.java:1631)==
原因分析:

https://blog.csdn.net/chjqxxx...

扩展思考:

我们可以通过命令进行自查

image.png

有源码的可以重新编译,没有源码的只能通过降低targetSdkVersion处理(然而也是治标不治本)

进阶方案

除了System.load方案 还有没有其他方案呢
答案当然是肯定的
https://github.com/Tencent/ti...
核心思路:通过反射把自定义的native库path插入nativeLibraryDirect ories最前面,即使安装包libs目录里面有同名的so,也优先加载指定路径的外部so
这里截取tinker部分代码
com.tencent.tinker.lib.library.TinkerLoadLibrary.java

image.png

image.png

这个方案是不是看起来更简单了,由于这个方案是非常规手段,存在兼容风险, 我们需要验证该方案的兼容性,写个helloword的so,通过该方案加载,提前找个版本线上带上去,进行埋点统计
截至目前最新版上线2天 统计到数据如下

image.png

其中失败的1例是上线前自测故意让校验失败产生的
也就是说截至目前可以认为改方案是靠谱的
当然了如果后续有新的Android系统版本更新
我们还需要关注新版本的兼容情况

项目应用

上面铺垫了那么多,现在进入正题
技术方案是一回事,落实到实际项目是另一回事
现在开始我们思考下如下几个问题

  1. 项目中引用的so库都是那些功能再用何时载入
  2. 三方jar(aar)引入的so库加载时机不受我们代码逻辑控制
  3. 使用的so库那些适合做异步加载
  4. so库文件没有下载完成之前 用户用相关功能如何处理
  5. 版本迭代so文件升级如何处理
  6. 打包阶段如何把so文件进行剔除
解决问题1、2、3

我们可以通过切面编程的思路解决,这里用到了开源库https://github.com/HujiangTec...
由于so库的加载都是通过调用系统函数System.loadLibrary进行,
那么我们全局拦截该函数的调用并打印调用链,
运行app,配合日志,就能分析so库的具体使用情况

image.png
image.png

这里我们需要根据自己业务场景分析,比如app进入到首页依赖的库不建议进行异步加载,需要异步加载的库最好是完成一个功能(我们需要按照经验进行逻辑分组 参考文章开头的图)

解决问题4、5

Android是依据Activity作为活动单位,启动Activity是通过startActivity函数调用进行的,那么我们就可以拦截织入自己的逻辑
大致思路如下
image.png

对应实现部分代码截图
image.png

image.png

image.png
image.png

通过规则配置化+字节码拦截的逻辑 对原有业务无侵入即可实现动态加载
研发关注正常的业务逻辑 不需要针对性编写代码
后续引入新的so库,修改配置文件即可
在实现的过程中有些细节是需要考虑的
比如校验下载文件的完整性,下载要不要支持断点续传,so文件需要更新如何处理等

解决问题6

通过上面的介绍,我们实现了so文件的异步加载
但是打包的时候 我们如何剔除so 减少最终发布包的大小
可以在build.gradle进行配置
image.png

但是上面提到了我们是通过json配置的规则
这样再重复配置一遍
而且还有可能2处配置不一致
而且我们的大原则是解耦
那么我们就继续hook打包流程
image.png

image.png

汇总小结

通过上面的介绍大致说明了原理

有兴趣的伙伴可以评论区留言,提出自己的想法

目前还在灰度验证阶段,等线上验证稳定后在评论区放出相关代码

so异步加载方案大同小异,该方案跟其他方案相比

个人感觉特色就是侵入性低

接入只需要3个步骤
image.png
image.png
image.png

未完待续

目前该方案还存在几个点有待优化

====================

问题1

目前用的是进阶方案实现的so库加载 比较理想的方式是传统方案和进阶方案都应该支持 一开始其实我想用传统方案去实现 但是该方案在unity那块有问题(unity相关so的加载逻辑有点特殊 存在通过jni直接加载so的情况 由于没有源码 内部逻辑不太清楚 ) 后续还需要研究完善 毕竟官方System.load是官方提供的api 更可靠

问题2

进度加载的dialog是依附于调用startActivity的窗口
如果startActivuty之后里面 立马调用finish会导致dialog泄露
这个是需要注意的一个点 也就意味着对原有逻辑存在一定依赖
这个可以通过启动一个FLAG_ACTIVITY_NEW_TASK类型的activity
代理解决 但是感觉这样有点重 目前还有点纠结

问题3

目前文件只是放到了阿里云,用户的网络环境复杂多变,需要考虑支持腾讯云,七牛云等更多的服务商。

问题4

上传到oss的动作目前是动手完成的,需要做成脚本。

查看原文

赞 0 收藏 0 评论 0

好未来技术团队 发布了文章 · 2020-12-25

AI工程提效平台研发实践

TiD2020质量竞争力大会邀请了好未来AI中台质量与工程效率部负责人赵明为参会者带来《AI工程提效平台研发实践》精彩演讲。

赵明从准、快、稳三个关键词分享了AI算法指标与评测平台架构与实践、AI微服务性能测试平台架构与实践、数据集管理平台架构与实践以及相应的案例演示。

准 :AI的算法需要满足实际业务场景需求,达到很高的准确度。快:一张图片经过算法的处理,返回结果到用户的app/web端上去,需要去评价它的端到端指标,比如说200~300毫秒,是否能有一个快速的响应和返回。基于这种需求,我们开发了一个微服务性能测试平台。稳:比如语音、图像或者文本发送到模型里面,不会出现卡顿甚至模型崩溃,这跟模型的健壮性是有很大关联的。

image.png

好未来AI中台致力于将人工智能技术结合教育场景,打造各项AI能力,持续提升学习体验和效率。围绕AI技术主要有三种类型的模型研发。

image.png

第一语音,包括语音识别、语音评测,情感识别和情感分析。比如ASR把语音转成文本的形式,评测英文朗读的流利度、准确性。这都是它的教育场景。

第二图像,比如印刷体的OCR识别,拍照搜题,以及笔记工整度的评级,教育场景内容审核等,都会用图像的算法模型辅助提升效率。

第三数据挖掘,这里面有一部分跟NLP自然语言处理相关,比如文本关键词搜索,课堂的特征统计,精彩瞬间捕获,中文口语表达能力的评测。这一系列模型的能力,我们会把它作为一个AI的微服务部署在PaaS平台上面,供各个端调用。

image.png

(一)算法提测频繁,无自动化

算法层面,模型发生变化,如准召率提升、鲁棒性增强、新数据增加;服务化层面,如性能优化,接口变更等都要经过完整的测试,所以提测是非常频繁的。这个过程原来没有进行自动化,效率比较低。

(二)行业领先度无感知

算法工程师会将行业的领先度作为制定 KPI的非常客观的依据。比如50%的 AI能力能够做到行业第一,这个维度实际上是对于算法KPI的目标。如何去评估这个目标?我们需要把它量化,通过一个平台把指标展示出来。如果没有这个平台,对行业领先度是无法感知的。

(三)性能调优后性能评估成本高

无论是算法模型,还是服务化做了调优扩容,我们需要去进行一个端到端的性能评估。伴随评估频率的提高,成本也会相应增加。

(四)数据碎片化

数据是AI的血液。在AI生产研发过程中,每一个环节都离不开数据。数据需要在各个角色里面流转。针对产品,算法,工程研发,测试,都需要对数据进行统一管理。如果各个业务方的数据,包括各个版本的数据,没有一个统一的平台,就会非常碎片化,难于管理和维护。

image.png

基于痛点我们进行了改进,也就是平台化建设。我们希望通过算法评测平台、性能测试平台、数据集的管理平台建设可以解决上述四类业务痛点。

(一)工具平台体系建设

image.png

工具的建设需要对各个角色赋能。比如产品需求,包括产品上线后的效果,其实有很多的工具做链路跟踪。对于算法,例如核心的AI能力模型,也需要一些工具链辅助提升算法的产出。模型通过服务化的方式把工具暴露成接口,供外部调用。我们在整个过程中要保证它的质量,通过运维体系Devops做全链路监控。

从质量上说,产品负责需求的质量,算法负责AI模型的质量,开发负责代码的质量,测试负责端到端产品的质量,运维负责产品上线后线上的质量,所有的环节都离不开数据和质量相关工具的支撑。

(二)Al算法指标与评测平台架构与实践

1.场景需求及用户

第一个场景,无标注数据:AI算法badcase筛选。不需要提前做数据标注,可以通过竞品分析、竞品比对的过程,自动化筛选badcase。第二个场景,无标注数据:新数据的算法准确性摸底。我们会通过使用新的数据,基于这个平台做算法准确性摸底。第三个场景,有标注数据:AI算法指标竞品评测与领先性分析。会通过标注好的数据做算法指标,客观量化的进行评估,包括市场领先性分析。第四个场景,有标注数据:AI算法指标评测与分析。针对有标注数据,算法的指标可以做进一步的分析和评测,包括找到badcase,把这些数据提供给算法做模型的进一步优化。针对这四个场景,我们主要的用户是算法,算法测试,产品经理。

2.技术架构

Al算法指标与评测平台技术架构分为基础功能与接口调用层、逻辑抽象层、UI层。
image.png

基础功能与接口调用层,包括权限管理、数据源管理、存储管理、分析实例管理模块、报告管理。上面会通过接口调用,调用自研的接口和竞品接口,通过脚本库进行统一的管理,比如入参,出参的返回,格式的标准化。

逻辑抽象层,就是把基础的功能做一个工作流整合,要计算算法的指标,或者端到端地处理数据。数据需要进行预处理——脱敏、降噪、清洗、旋转图片、或过滤空音频。不同的模型的处理结果,算法指标的评价规则不同。比如语音的指标是字错率。图像的指标是准召率。那么,如何能够把它们归一化统一管理?AI能力注册,就是把新的开发能力统一。在底层逻辑和上层UI不变的情况下,如何去把它注册到这个平台上?比如说现在有10项能力到100项能力,需要尽可能地减少由于新能力注册导致前后端的逻辑或者代码发生重构的变化和影响。简单讲,它把底层基础构架和上层AI微服务的业务分离开。结果的导出,我们会根据不同的模板,不同的算法模型,会有相应的处理结果报告。可以把它导成表格,去做后续的报告整理和分析。

在逻辑抽象层的上面还会有一个多竞品处理引擎,用来做无标注数据,badcase自动分析,包括对其他竞品做数据比对,去发现自身的不足。

UI层。有评测实例,可视化的编排,也会通过一些可视化界面做badcase筛选和报告的展示。

3.实际效果

image.png

Al算法指标与评测平台可以提供算法badcase,为算法工程师做模型优化提供参考。比如:OCR识别错误,在图片识别过程中,因印刷问题造成的噪音数据会影响识别。上图在识别过程中,把“____in”识别成了“-tin”。所以在做算法模型处理的时候,就需要去提升模型本身的鲁棒性,需要针对于这种场景去做模型的专项调优。ASR转录错误就是把语音数据转成文本过程中出现与标注数据不一致的情况。例如标注数据是今年我们部门有多少offer。但模型AB的准确度都不是太好。在识别过程中出现了错误,模型A识别成了”一年我今年我们今年我们不没有多少分“,模型B识别成了“今年我们部门有多少分?”offer和分不能很好的区分。

我们会把错误罗列出来,针对 bad case做统一的报告整理和收集,提供给算法工程师去做模型的优化。

4.算法模型评估的指标

模型的好坏,评价标准需要一套客观的度量标准。
image.png

针对图像的统计指标有精确度、准确度、召回率,F1等。可以通过 TP、FN、FP、TN维度用公式计算出Precision、Recall、Accuracy,最后评估出相应的召回率和准确度,看看跟上一版本相比,哪些需要提升。针对语音,可以用WER/CER(词/子错率)做统计指标。这个指标越低越好。以插入错误、删除错误、替换错误除以总共单词数作为计算指标。计算指标会通过脚本的形式嵌入到平台里。

针对性能指标,我们将CPU,MEM,GPU等作为统计指标。针对稳定性指标,主要使用崩溃率和内存泄漏作为统计指标。通过这样的一些维度,我们可以客观地评价模型是不是准、快、稳。

5.Demo1-算法Badcase自动化筛选

针对于无标注数据场景

首先我们会有一个可视化编排的界面,先拖取数据源。这个数据源其实就是 FTP上的一组数据,实际是一个保存了被测图片的文件夹。我们会对图片进行预处理,比如说数据脱敏、自动正位等。因为图片有可能并不是正位的,会通过一些模型来进行正位。这样便于进行模型分析。我们会将脱敏后的数据同时与三个外部竞品模型做比对。

Badcase类似于投票法,比如有ABC三个模型,通过语音分析后如果得到的结果都是“今天是星期五”,那么它的置信度就非常高。如果自研结果和这三个模型的分析结果不一样,那么大概率就是Badcase。它的优势就在于不需要做标注,可以从线上抓取数据,自动化地进行筛选。模型处理完毕后,会做一些去除标点符号、空格、回车等非关键因素的后处理。处理完毕后,Badcase会显示在页面上。在这个页面我们可以调整阈值。阈值越高,说明Badcase的可能性越大。例如:Badcase准确度为0.9,说明它是badcase的可能性非常高。如果是0.2,说明有可能是误报。

针对印刷体的表格,我们会对它进行图片转成文本信息抓取的OCR评测,包括Goodcase和Badcase两种形式。我们可以通过平台,针对无标注数据进行自动化分析。分析出来的Badcase,就可以提供给算法工程师做核心迭代和优化。

6.Demo2-算法指标竞品评测

针对有标注数据

首先通过可视化编排界面,创建一个实例并命名版本。

我们从 FTP上拖取数据然后进行模型分析,把数据同时输入三个模型,其中一个是自研的模型。我们要看自研模型与竞品模型的差距,最后会导入标注数据,然后进行分析。

标注数据相当于一个标准答案。所以最后会得出三类指标。每一个模型都有一套指标。这样我们能够分析到底我们的模型行业的领先性是怎么样的。这有助于算法制定KPI。
image.png

针对 OCR类的量化评估指标有两个维度——序列准确度和字符准确度。这两个准确度可以评价模型的效果。在不同的时间迭代,每一个评价模型的标准会有差异。比如说每一个维度,有些可能是我们领先,有些可能是我们落后。这个指标可以看到我们在行业所处的地位。

当然我们也可以做筛选。这个筛选可以针对于不同的维度,或者说针对于不同的竞品,大家可以看到下面是个Badcase,有三个模型。最左侧是自研的版本。

自研版本的效果好于另外两个竞品。所以它的Badcase会比较少。这个页面是模型ABC跟标准数据的比对。如果它跟标准数据完全是一致的,那就是准确的。如果不一致,就说明识别是有误的。
image.png

对于印刷体公式的识别算法的模型,输入一张图片,最后会把图片里面的内容以文本的形式输出。一些比较复杂的公式识别,就需要增强模型鲁棒性。这是有一定难度的。正如刚才的演示,我们可以评测出来这个指标。它会通过计算逻辑公式,得到出一个结果。然后可以看到行业领先度。

这个平台算法、测试的伙伴都可以用。产品可以利用这个平台做新数据的摸底,无需算法或测试的参与。他不需要理解非常复杂的计算逻辑,可以通过拖拽等可视化编排的形式帮助工程师快速得出结论。

(三) AI微服务性能测试平台架构与实践

1.场景需求及用户

AI微服务性能测试平台是解决“快”的问题。

第一个需求是算法和服务化测试环境管理与共享。算法工程师在本地有一套研发的环境,服务化的开发有一套环境。这里面涉及到很多模型的版本,包括基础的依赖库的版本Python 等库。这些库的标准要尽量做到单一化,最简单的方式是容器化管理和环境共享。

第二个需求是实现自动化部署。提测后,性能测试的所有环境需要自动地准备好,如准备好脚本。

第三个需求是自动化探压,包括设置最大 TPS、并发数,去实现自动化的探压。

第四个需求是性能瓶颈分析。瓶颈的分析是很关键的部分。涉及处理速度的问题。针对模型本身,进行模型压缩,剪枝,提高处理速度,减少硬件资源的浪费。

AI微服务性能测试平台针对的用户就是服务化测试、算法测试、研发、产品经理。研发和产品经理可以拿到性能指标跟客户沟通是否满足业务的需求。

2.技术架构

image.png

AI微服务性能测试平台架构包括数据源、接口层、UI层。

最底层是数据源,我们会用Prometheus做远程数据监控。针对数据源,会有数据库做持久化的存储。

中间层是接口层。这里面会有自动化探压的逻辑,包括远程的执行、部署,指标落地的计算,远程终端的控制。还可以一键登录。

上层是UI层,能够通过平台的界面登录,包括资源申请和释放,自动化探压,瓶颈的分析等。

3.实际效果

平台可实现无人值守的自动化探压,依据二分法算法在最短的时间内探查系统的最大TPS或并发请求数量。
image.png

二分法就是并发从100开始,100是初始的压力,步长也是100。我们进行100 200 300 400递增压测,当它达到最大的一个TPS拐点,如上图300这个极大值出现后,会把压力降下来到200-300之间,降到250。如果没有这个平台,我们每次都需要手动改并发。因为服务种类会比较多,有基于HTTP协议,也有基于Websocket协议。为了简化和通用,底层还是基于Jmeter去实现,然后通过一个模版替换的逻辑,替换JMX文件中的配置。Jmeter开源工具的功能是比较强大的。
image.png

手动压测平均每一个AI微服务,需要运行10轮左右不同并发数量下的压测。一轮60分钟,10轮就是600分钟,十分耗时。而每次都需要改脚本,启动压测,观察各项指标输出。服务端报错,需要降压力。指标正常,则继续增压。这十分耗费人力,而且这个工作是重复性的。但是有了自动化后效果就非常明显,从600分钟可以降低到10分钟,最后复核数据,报告没有问题,就可以直接得出结论,非常快。

4.Demo3-自动化探压与瓶颈分析

首先申请一个压测机。压测机申请完后,我们可以通过终端连接实现一键登录。这个里面可以操作各种命令。随后就可以根据刚部署的压测机做脚本部署,对Jmeter原生格式文件,做持续30秒恒压的部署,部署完后就可以执行监控。在这个平台上可以完成所有工作,包括资源申请,脚本远程推送部署执行,结果输出,监控数据的返回和展示。这个平台可以作为工作台,可以按照实际需求做各种各样不同的脚本的配置,包括相关的结果报告的产出。这个报告展示出来是一个Jmeter原生的报告。在这个报告里面可以展示TPS等各项指标。
image.png

同时我们还可以进行自动化探压场景。压力是100,步长也是100。每一轮大概60秒钟,实际测试时比这个要长。然后颗粒度是20,这里有个场景是退出的阈值,也就是说颗粒度设置。颗粒度就是说最大成功压力和最小失败压力的之间的差要小于一个值。颗粒度越细,跑的轮数就会越多。
image.png

从监控图可以看到,通过一段时间的运行,可以看到这个图到达300的时候,TPS达到了一个拐点,也就是说 TPS出现了瓶颈。瓶颈出现,我们就需要把压力降到250。

image.png

当我们运行完后,可以看得一个报告的展示,100的时候TPS是551,200的时候TPS是733,300的时候就TPS降到722。所以我们就需要把压力降回来,降到250~225。这样我们就可以知道在什么样的并发下面,它的TPS是最大的,吞吐量最大。这是一个TPS的场景,还有一个并发数量的场景,不出错的情况下,最多能承受多少个并发。这可以连接到Jmeter原生的报告里。这是端到端的,里面包括了网络传输,服务化的处理,模型调用的时长。
image.png

我们可以选择一个时间窗口做监控结构和瓶颈的分析。我们会监测主要的指标,比如内存泄漏。内存泄漏的阈值可以控制,设置默认值为5%。如果结束点比起始点增加了5%以上,我们就判断疑似有内存泄露。这个过程阈值可以调整。我们后面还会再改进得更加智能。
image.png

CPU使用率检测阈值,如果阈值允许80%,而它的平均使用率是90%,这样就会报错说已经超过了设置的上限,包括网络流量。这是总体监控的看板。通过开发后台逻辑的服务,从普罗米修斯里面把数据拿回来做一些加工和处理,然后通过接口返回,从前端做渲染。这样一种形式就可以实现:在无人值守的情况下自动探测压力,比如周五晚上下班时,运行自动化脚本,它就一直在探查最大的TPS或者并发数量。然后周一早晨就可以拿到所有的报告,甚至可以多组运行。这就节省了时间。

这可以看作是一个敏捷测试,通过我们的工具和平台,不断地为算法或者研发缩短反馈周期。

(四)数据集管理平台架构与实践

1.场景需求及用户

针对于不同的需求场景,主要用于训练和测试。数据的分类需要打不同的标签,比如哪类的数据是属于哪个业务的,或者属于不同的版本,是正向数据还是负向数据,来应用到不同的算法模型的类别,其实就是在做评测的类别。

算法工程师可能会用到一部分数据。我们在做客观评价时,使用黑箱数据。这对算法工程师是不可见的。但是有一些数据是针对算法自测的,可以与一些冒烟测试的数据一起用于算法测试。

查询标签化数据,标注数据的情况,看看过去搜集到哪些数据,有哪些是已经标注好的,有哪些是没有标注的,或者说它的标注质量比较差,需要重新标注的这部分数据。

我们需要构建一个混沌测试体系提升稳定性,提高模型的鲁棒性。

数据集管理平台的用户也比较多,包括算法、算法测试、产品经理、数据运营。

2.流程

首先选定数据集,其次实现自动化的下载,然后进行模型相关的工作处理,最后进行结果检查。模型处理就是在数据位置模型里面,监控各类指标是否运行正常。在中间这个过程中,监控准确率,监控准不准、快不快、稳不稳,通过这样的维度,监控实际效果是不是符合用户和业务的预期。

针对噪音数据、空数据、模糊图片、非法格式、缩放图片、低质数据、超限数据、碎片数据、光源点图、对抗数据等不同类型的数据,我们会通过不同的维度关注算法模型的表现。

3.实际效果

image.png

监控同时段内存使用量和CPU使用率可以发现模型是否稳定。如上图,同时段内存使用量是在不断地变化波动,波动范围在6G-8G。

同时段CPU使用率的波动也比较正常。最大值的波动在50%上下,也是在可以接受的范围。所以这批数据的处理针对资源使用来讲没有太大风险,这说明模型的稳定性达到了交付的标准。

image.png

(一)目标

考虑不同维度的评估标准,未来,我们将以高质、提效、降本为核心,助力产研算测团队,通过工具链或者平台研发,更敏捷地交付 AI产品和微服务。这是我们的目标。

(二)方向

针对“准”:我们将继续让平台具备支持算法指标优化方向的智能推荐能力。采用了什么样的模型,基于Badcase分析,使用了哪些参数等,可以针对这些维度的数据进行智能推荐,告诉算法工程师下一步优化的方向。

针对“快”:找到算法系统的瓶颈从而实现智能定位与分析。比如说模型处理效率是不是可以提升,在工程层面是不是可以做一些数据压缩,在网络链路传输中,可不可以做一些性能的提效,服务化申请资源时是否要进行横向扩容。

针对“稳”:进行算法与服务化内存泄漏扫描与定位。内存泄漏,不仅影响服务化,而且影响工程质量。它是影响系统稳定性的第一大杀手。如何快速定位内存泄漏的问题,这很重要,也很难。通常一个内存泄露问题,研发和算法部门一起分析,可能都需要几天才能够定位出来。这与敏捷研发迭代是相悖的。所以我们持续地研究如何通过一些工具获得静态加动态扫描的形式,进行进一步精准的定位。

查看原文

赞 0 收藏 0 评论 0

好未来技术团队 发布了文章 · 2020-12-17

辣眼睛,前端已经这么逆天啦?web前端智能化在线推理的应用场景和实现原理

作者:王群 赵辉 刘东东

随着AI能力从实验室逐渐走向市场,需要在特定应用场景下将神经网络模型执行预测到相应的结果。针对不同业务背景,云端智能化方案和用户端智能化法案也都处于快速发展的阶段。想要在网页上实现智能化的能力,达到特别效果,你会发现浏览器等载体下实现本地智能化实时预测是非常必要的。很多业界一线公司也都在布局web智能化,比如Google的TensorFlow.js,百度的paddle.js以及阿里淘系技术团队正在酝酿MNN.js,W3C也开始联合各大公司深入讨论WebNN规范的设计,未来AI能力将会渗透在更多的应用领域。作为国内第一个开源的web前端智能化开源方案paddle.js的发起人和设计者,王老师非常荣幸与大家一起分享web前端机器学习引擎的应用场景和实现原理。

对于实现人工智能有很多方式实现,如机器学习就是一种特别接近于人的认知方式,就像田老师所描述的,“神经网络Neural Network,是最接近于人脑工作机制的算法,我们的人脑里有上百亿个神经元,单个神经元的表达能力很有限,但大量的神经元组合到一起,就产生了丰富的表达和和推理能力。我们看到什么,听到什么,都是输入,然后经由这么多神经元层层运算,输出我们的决策结果。神经网络,其实就是在模拟人脑的这套机制,从输入到最终的输出,中间还有很多层隐层,每层都有多个计算单元(神经元),这些计算单元层层紧密连接在一起就构成了网络。你可以认为一个计算单元就是一个简单的小函数,那这么多计算单元组合到一起之后,就形成了一个复杂的不可解释的函数。所谓深度神经网络Deep Neural Network,就是有很多层隐层的神经网络,隐层越多,就能表达越复杂的函数,也就越逼近于人脑。”

'图:CNN神经网络'
图:CNN神经网络

从人脑的识别过程,我们很容易发现三个重要的环节,即输入,输出,映射过程。抽象来看,整个过程就是给一个输入X,经过映射过程F,输出Y。

图:抽象表示过程
图:抽象表示过程

知道其中两个变量从理论上是可以推导出来另外一个。即给出X和Y,推出F。可以重复很多样本进行计算,得到F’(n)。标注的越准确、样本越多,机器大脑F’(n)就越接近人脑F,我们把这个寻找F‘的过程神经网络训练。

图:神经网络训练和预测推理
图:神经网络训练和预测推理

那么,形成神经网络的过程就是训练,使用神经网络的过程就是预测推理。

Web端智能是什么?

通过了解了机器学习的基础知识,端智能也就不再难以理解了。如果在云端进行训练和推理将结果呈现出来的就是云智能或者服务端智能,在客户端(Native / web)进行训练或者在线推理预测将结果呈现出来的过程就是端智能。

图:智能化的服务端实现与客户端实现
图:智能化的服务端实现与客户端实现

这个时候大家可能觉得在前端使用神经网络进行计算需要占用大量的计算资源,前端是否能够运行起来如此的庞然大物。其实,随着PC和移动设备上算力的稳步增强、算法逐步成熟以及各种创新需求的不断催生,在浏览器中实现端智能已经具备了良好的基础条件。

AI4QQ202012151427242x.png

Web端智能可以做什么?

为什么会出现端智能,以及他底层演化逻辑是什么?这两年有一个明显趋势就是机器学习从实验室往产业落地方向演进,海量终端设备成为落地最佳载体。对于web前端使用智能化的场景一般具有实时性强、数据隐私性、创新交互和有效降低server压力等特点,比如人脸实时美颜、目标实时跟随与识别、AR、数据实时检测、media pipe、实时人像分割等场景都有着大量的应用。web方案能够面向全网用户使用,浏览器内实现是不错的一种方案,保证分享出去后其他用户也能够无差别的直接使用,不需要安装指定的应用APP,而且实现热更新,更新迭代不需要发版。

人脸关键点
人脸关键点

手势识别
手势识别

实时识别
实时识别

实时Hair Segmentation
实时Hair Segmentation

肢体识别
肢体识别

物体实时跟随识别场景图片
物体实时跟随识别场景

AR场景图片
AR场景

很多业界一线公司也都在布局web智能化,比如Google的TensorFlow.js,百度的paddle.js以及阿里淘系技术团队正在酝酿的MNN.js,W3C也开始联合各大公司深入讨论WebNN规范的设计,Intel在API规范化上也做出了大量贡献。

实现web端智能落地需要做什么?

一般情况下,从实验室模型训练到实际应用落地有非常悠长的链路要完成,应用智能全链路核心流程包括数据采集、算法设计、模型训练、模型优化量化、模型部署、输入层处理、推理预测执行、输出层结果处理以及业务化等环节。无论是云端进行还是客户端(Native / web)端进行业务落地,这些环节都是不能缺少的。

图:智能化全链路
图:智能化全链路

因为存在技术边界,算法工程师和工程研发工程师的协同就变得异常的复杂,所以出现了各种自动化的平台在淡化边界、将人工操作的距离缩短,减少全链路中的人为问题出现。例如,AI中台就建设了专业化的平台集合,例如专业化的标注平台vegas、计算资源调度与模型训练平台Axer、数据审核管理平台Guardian、AI能力接入平台Paas等。

图:在线推理部分
图:在线推理部分

不同于服务端智能化应用,web端侧环境复杂性与用户设备息息相关,用户机器浏览器对于WebGL版本支持不同、操作系统的不同、webAssembly支持情况不同、webRTC支持情况不同、浏览器的不同都有可能造成web端智能化兼容实现的问题,而且工程师的技术能力也对业务落地效果有着一定的影响。因此,要实现web端侧在线推理不仅需要高性能、易用的端侧在线推理库,也需要有业务框架加持。比如对于实时视频流场景的实时在线推理预测就需要有相应的输入处理、分帧优化以、并行化计算和输出处理的通用业务封装,其实media pipe也就是针对这中场景下产生的web端智能业务框架。

Web端智能的实现原理是什么?

Web端侧智能化的实现方案主要包括离线工具部分、在线推理部分以及模型集。那么,为什么会有这些部分呢?我来一一跟大家分享一下这其中的“奥妙”。

图:web智能化解决方案设计
图:web智能化解决方案设计

离线部分主要工作就是神经网络模型的处理,这部分放在离线处理具有更多的优势,比如模型的优化、剪枝、精度量化以及模型格式的转换在离线时处理能够保证运行时不去做额外的资源开销。Web运行的主要载体是浏览器,在浏览器中能够解码的数据有限,所以需要把模型转换成web友好的格式是web端智能化的需要特意强调的,而且模型要尽可能的小,减少计算成本和网络加载负载成本。

在线部分主要包括在线推理库和业务框架两大部分。在线推理库主要作用是进行神经网络模型加载(网络加载 / 本地加载)、神经网络在运行时重建以及神经网络计算等,通过将输入数据(图像、语音、本文等)进行输入层计算,再到神经网络各个隐藏层的计算,直到输出结果的输出层的计算最终给业务呈现效果。这其中每层都需要有相应的算子来完成相应的计算,以CNN卷积神经网络为例,需要卷积、池化、全连接以及激活函数等算子的实现,而这些算子的实现又要考虑性能的极致性。

图:在线部分结构设计
图:在线部分结构设计

实现方式不同性能效果也有显著的差异,比如使用webGL利用GPU进行计算可以利用材质单一像素进行4通道的并行计算,这种方案速度能够满足视频实时场景需求;不同于webGL的方式,WASM是一种快速移植的方式,是能够将其他平台的代码编译到浏览器可用方案,但是计算性能相比webGL以Face Detector模型为例要有将近2倍左右的性能劣势,纯JavaScript利用CPU进行计算的性能就更加相差悬殊,所以在兼容性等基础上一般优选webGL方案。不过W3C也正在积极商讨webGPU的draft,更加快速的计算方案正在路上。

图:backends性能对比情况
图:backends性能对比情况

在线部分的另一个重要部分就是业务框架,业务方往往更加专注于业务,很难有精力去处理性能问题和使用调试,对于视频流场景、语音场景、文本场景更加希望“拿来即用,用之有效”,所以对于相应的场景在业内目前较为优秀的方案就是封装成不同场景的业务抽象框架,让业务研发更加专注于业务实现。

小结

本期的Web前端智能化在线推理的应用场景和实现原理就先介绍到这里吧,也欢迎关注我的后续更新。帮助web智能化不断成长的,除了在性能上的不断压榨,还有众多场景的哺育。众多的人工智能应用必将在web端落地,通过人工智能来提升工作能效、优化用户体验,在你我不经意之间,改变了我们的生活。

如果你对AI及AI工程化有着浓厚的兴趣,想要和我们一起探索,欢迎加入AI中台-产研中心,让我们一起通过AI改变世界~!

【简历投递邮箱:jiangyajie@tal.com】

查看原文

赞 1 收藏 1 评论 0

好未来技术团队 发布了文章 · 2020-12-11

好未来举办首届PHP开源技术大会

12月5日,好未来第一届PHP开源技术大会在北京举行。大会以“开源·分享·共建”为主题,由智慧教育国家新一代人工智能开放创新平台和好未来联合主办,开源中国技术社区协办。

本次大会邀请了多位PHP领域的技术大咖,为千余名开发者和技术爱好者进行全天的前沿技术干货分享,详尽展示了当下炙手可热的PHP技术全貌和技术开源发展新态势,为推动科技教育长线发展注入新动能。

(好未来第一届PHP开源技术大会在京举行)

作为智慧教育国家新一代人工智能开放创新平台建设单位,好未来倡导开源、分享、共建的理念,致力于用技术推动行业发展。从2020年8月,好未来正式上线首个开源项目并公布开源路线图开始,截至目前,已累计上线了30多个开源组件,在全球某主流开源社区获得的Star数量已超3000个,并面向行业开放了超过10项智慧教育解决方案,超过5类教学数据集。

作为大会主办方代表,好未来集团CTO田密表示,好未来重视技术的长期价值,希望通过开源放大技术的能量,实现人才聚合与协同创新,将经过教育行业复杂业务考验的技术和经验向开发者赋能,帮助更多技术人进入行业,收获成长,共同构建一个“共生”“互生”“创生”的智慧教育新生态,这也是好未来举办开源技术大会的初心。

(好未来集团CTO田密发表开场致辞)

会上,好未来旗下学而思网校首席架构师、Swoole开源项目创始人韩天峰对改变PHP未来的新技术做了展望,涉及PHP 8、JIT、Docker、Composer2、Swoole、Golang、K8s、Service Mesh、QUIC、云原生、物联网等12项技术。他表示,学而思网校也将第一时间应用这些新技术,实现响应性能、稳定性和处理效率的提升,为学生和老师带来更“丝滑流畅”的在线教育体验。

因为疫情无法来到现场的德国PHP内核核心开发者Nikita,借助视频分享了值得开发者重点关注的PHP 8——PHP全新一代大版本的新特性,包括JIT,注解、联合类型,以及更严格的错误处理和增强的类型安全。长期活跃于各大开源社区的PHP和Swoole内核开发者陈曹奇昊也从实操的角度“现场教学”,详细介绍了PHP 8与下一代协程技术的演进。KK集团技术负责人、Hyperf开源项目负责人黄朝晖分享了启动Hyperf项目的初心及核心逻辑,揭秘一个成功的开源项目是如何灵活满足不同生命周期的企业需求的实战经验。

开发者每天都在和数据库打交道,“玩转”数据库是技术人从入门到精通的必经之路。阿里云数据库产品事业部高级技术专家陈宗志详细介绍了PolarDB云原生数据库的核心技术和内核优化。知数堂联合创始人吴炳锡将15年MySQL数据库从业经验娓娓道来,用技术分析和业务实例阐释了如何利用MySQL 8.0帮业务增效的秘诀。PingCAP联合创始人兼CTO黄东旭则带开发者“遍历”当下主流分布式数据库的设计模式,并大开“脑洞”畅想了下一代数据库的新形态。

运维是产品能够稳定、安全、高效的基础保障,以教育行业为例,疫情期间面对在线教育流量的暴增,运维的技术响应能力是保障学而思网校面对百万并发、千万日均流量,实现“停课不停学”的关键因素之一。大会现场,云智慧技术副总裁高驰涛带领参会人员深入浅出地速览了AIOPS实现智能运维的关键技术、应用场景和未来展望。

PHP如何与流行的技术趋势相结合也是开发者关注的热门话题,其中“容器化”更是技术趋势中的“顶流”。好未来后端资深专家陈朝飞以学而思网校后端“网校云”的容器化落地实践为案例,分享如何从传统PHP应用向容器化迁移的实战经验,以及用软件工程的方法论解决生产系统稳定问题。

腾讯高级工程师,《PHP 7内核剖析》一书的作者秦朋以自身的成长为例,分享了如何从只会PHP的技术小白成长为合格工程师,从技术人才发展的角度帮助更多开发者。

(大会现场技术人热情高涨)

活动最后,好未来开源项目负责人谢华亮于大会闭幕发言中总结,以本次开源技术大会为起点,好未来希望为技术人搭建专业、纯粹、高水准的技术分享与成长平台,共同用技术推动教育进步。

补充资料:关于PHP开源技术大会的全部PPT资料及直播回放视频已上传至“好未来技术”公众号。
各位有兴趣的老师可以扫描下方二维码关注“好未来技术”公众号,回复"PHP"获取大会第一手资料!
 

查看原文

赞 2 收藏 0 评论 0

好未来技术团队 发布了文章 · 2020-11-29

好未来斩获NeurIPS2020国际竞赛冠军

近日,国际人工智能顶级会议NeurIPS2020(2020 Conference on Neural Information Processing Systems,即神经信息处理系统大会,以下简称“NeurIPS 2020”)公布竞赛成绩。

好未来AI中台机器学习团队在NeurIPS 2020“教育挑战”竞赛四项单项任务中,荣获一个单项冠军、一个单项亚军及综合评比亚军,向世界展现了中国“AI+教育”的前沿探索。

作为全球人工智能和机器学习领域顶级会议之一,NeurIPS旨在促进神经信息处理系统在生物学、技术、数学和理论方面的研究交流,备受行业内外瞩目。本届NeurIPS首次举办与教育相关的挑战赛,吸引了来自北京大学、中国科技大学、宾夕法尼亚州立大学等80多支专业团队参加。
image

(NeurIPS 2020是国际人工智能顶级会议之一)

荣获国际顶级竞赛冠军,彰显好未来“AI+教育”实力

NeurIPS2020“教育挑战”竞赛紧密围绕教育中的“诊断性问题”,要求参赛队伍分析学生在过去两年中的近千道数学选择题作答记录,从而准确预测学生的答案以及哪些问题的作答质量较高,并为每个学生确定一个最能预测其答案的个性化问题序列。

针对赛题,好未来AI中台机器学习团队采用自然语言处理(Natural Language Processing, 以下简称“NLP”)中最前沿的预训练语言模型,将赛题转化成自适应学习问题进行预测,有效地利用了学生练习的序列信息,最终在“评价题目质量”任务、“通过极少量题目对新学生进行能力诊断”任务以及综合排名上,分别取得冠军、亚军、亚军的成绩。

image

(好未来AI中台机器学习团队获奖邮件)

随着数字技术在教育中的深层次应用,全球学生对个性化、高质量教育资源的需求日益迫切。作为国际人工智能顶级会议,NeurIPS 2020“教育挑战”竞赛在AI领域的成果研究,将有可能对全球数百万学生的个性化教育产生持续、真实的影响。好未来AI中台机器学习团队通过长期在NLP和数据挖掘领域的研究积累,能更准确地了解学生的学习状况,并给出个性化学习建议,为数据分析、内容识别、自动批改等技术在教育场景中的应用打下了良好基础。

加大科研及人才投入,推动“AI+教育”落地应用

凭借扎实的技术积累和对智慧教育的深入理解,2019年8月,好未来获科技部批准,承建智慧教育国家新一代人工智能开放创新平台,以智慧教育重大需求为牵引,促进行业开放共享,助力中小微企业成长。与之同时,好未来不断加强底层科研能力的构建,与清华大学、中科院计算所等6所高校院所建立联合实验室或开展联合科研项目,不断加大科技研发和科研学术合作的力度。

同时,好未来不断加大对教育人才的培养,汇聚了大批心怀教育理想、致力于追求爱和科技让教育更美好的高精尖科技人才。好未来AI团队参与的数十项学术成果入选NeurIPS、AAAI、WWW、EMNLP、AIED、NCME等国际顶级学术会议,并接连斩获世界计算机视觉领域顶级会议CVPR-EmotioNet竞赛冠军、世界人机交互与普适计算领域顶级会议UbiComp竞赛冠军、中国计算语言学大会CCL2020竞赛冠军等荣誉。在纽约国际人工智能顶级会议AAAI上,好未来AI团队成功组织了首届AI for Education学术研讨会,推动“AI+教育”领域的国际学术交流。

在加大科研和人才投入的同时,好未来也在不断将AI技术大量应用到真实的教育场景中。近年来,好未来不断取得前沿核心技术突破,并在语音技术、视觉理解、知识图谱等AI能力持续积累的基础上,实现多项技术的产品化应用,打造了包括AI课堂、教学过程评估、口语表达能力评测、作业拍搜批改等创新产品解决方案。目前,好未来已探索形成了覆盖“教、学、测、练、评”各教学环节的100余项AI能力、10余项教育场景应用AI解决方案。

例如,好未来自主研发的“中英文口语表达能力评测解决方案”,基于业内领先的幼儿语音识别技术和多维度口语表达评测算法,从流利度、情感、内容相关度、语意逻辑、语言运用等维度输出评测报告,不仅广泛应用于好未来旗下学而思培优、学而思网校等品牌,也开放给行业其他的教育从业者。好未来旗下拍照搜题软件题拍拍,以“题目搜不到,老师免费答”为核心功能,拥有强大的AI技术及海量优质题库,一键拍照即可快速获得解题思路和详细的步骤分析,并持续推出更智能的AI功能。

接连荣获国际权威竞赛大奖,标志着国际学术界对好未来科研实力的认可,也凸显出好未来AI技术在教育场景地中的实用价值。未来,好未来将继续推进教育科技领域的深入研究,并基于智慧教育国家新一代人工智能开放创新平台,为教育从业者提供全场景、全过程、全周期的技术及服务支持,为孩子们提供更加公平、更有质量的教育体验。

查看原文

赞 0 收藏 0 评论 0

好未来技术团队 发布了文章 · 2020-11-22

十位业内顶尖大牛告诉你,PHP技术及未来

一、为什么我们要办一个技术大会?

每位程序员在职业规划的起点都曾问过自己一个问题:到底该专精于哪类计算机语言?是应用广泛的C语言,底层开发的C++,适于平台移植的Java还是效率出色的PHP? 

而在走出新手村后,疲于应对新需求、忙忙碌碌改Bug的PHPer,又该如何摆脱“工具人”阶段、去哪儿看清技术最佳实践和发展动向呢?

“听君一席话,胜读十年书。”12月5日,智慧教育国家新一代人工智能开放创新平台联合教育科技公司好未来、开源中国技术社区,特邀PHP领域顶尖大咖,将为全国PHP开发者和技术爱好者打造一场开源届的饕餮盛宴。 

PHP的第一行代码在1994年由Rasmus Lerdorf敲下,并于1997年正式由开发团队推出。历经20多年的发展,如今已站在了8.0版本的前沿。在2001年,PHP的官方文档写下了“PHP是世界上最好的语言“,这句话也成为了响彻开发者群体的一个梗。

基于以上原因,我们想举办第一届PHP开源技术大会,让我们的PHPer从前台到后台整个技术栈未来哪些新的东西,值得我们去学习。

二、这个技术大会你能够收获什么?

这次大会邀请了很多PHP和数据库领域的重磅大咖。被无数爱好者开发出诸如Yahoo、Facebook、新浪微博、贴吧等应用于PC端和手机端的优秀产品和App的后端语言PHP,仍在不断地超越着自己。在12月5日的大会上,Swoole创始人韩天峰阿里云MVP陈宗志,与来自德国的PHP内核核心开发者Nikita将详细讲解包括PHP8.0中有什么新特性PolarDB云原生数据库内核揭秘、Swoole开源项目未来的迭代方向等行业重磅议题。

面对着群雄并起的其它语言,PHP同样也在提升着自己的竞争优势,获取面向未来的更多可能性。云智慧技术副总裁高驰涛老师将带来《智能运维AIOPS关键技术速览》,帮助程序员摆脱埋头写Bug的苦恼。

基于PHP的性能问题,KK集团技术负责人黄朝晖老师开发了Hyperf框架。基于此框架,对于职场程序员而言,不必多次切换语言,就能保证运行速度。

这场峰会还将汇聚来自教育、电商、视频等行业龙头大厂的PHP技术实战干货。例如,PHP容器化便是领域一大发展方向。好未来成功用容器化解决了网课的峰值数据差异问题。在观看人数进入黄金时段,由流量低谷向流量高峰的转化中,容器化可智能扩容,对保证网课的稳定性至为关键。好未来后端资深专家陈朝飞将带来《PHP项目容器化线上实践》的精彩讲解。

当然,这场大会介绍的不仅是技术的突破,也是个人路线的发展启示。腾讯高级工程师、《PHP7内核剖析》作者秦朋老师将带来《一个PHP工程师的进阶之路》,面向PHPer答疑解惑。

这是一场大咖云集、头脑风暴的大会,更多大咖的演讲内容详见海报;这也是一场从早到晚、持续输出技术干货的PHP峰会,你将与行业大咖面对面交流关于PHP的方方面面。总之,对于使用PHP语言的工程师而言万万不可错过。

三、参加这个大会值不值?

本次大会将以OMO的形式,在线上、线下同步进行,线下参会99元线上直播9.9元。在行业内,这是“交个朋友”的超低良心票价,但你将收获技术峰会的全面体验。而对于主办方和分享嘉宾来说,则是以更好地推进PHP行业在国内的蓬勃发展为初衷。目前报名通道已正式开启,通过大会官方网站、开源中国等平台均可报名参会。

你不仅可以在活动现场与行业大咖面对面交流,开启更多合作机会。我们还将为每位参会观众准备午餐,更有精美大会主题伴手礼与现场福利抽奖等环节为活动增彩。

这是后疫情时代首个PHP技术人的线下聚会。欢迎各界技术伙伴踊跃参与,共绘技术开源开放生态图景。 

四、大会具体安排

会议时间:2020年12月5日(周六)

会议地点:北京市-黄河京都会议中心-3号楼

官方及报名入口:https://oscon.edu.com

大会嘉宾阵容

查看原文

赞 0 收藏 0 评论 0

好未来技术团队 发布了文章 · 2020-11-13

PHP大咖齐聚,首届好未来开源技术大会报名启动

在新一代信息技术快速发展和开源生态体系不断完善的当下,开源在技术创新、生态构建、数字变革等方面,都发挥着重要的引领作用。根据中国信息通信研究院发布的《开源生态白皮书(2020年)》显示,我国87.4%的企业正在使用开源技术。受益于开源技术的个人开发者更不计其数。

科技部于2019年发布的《国家新一代人工智能开放创新平台建设工作指引》中也强调,着力提升技术创新研发实力和基础软硬件开放共享服务能力,鼓励各类通用软件和技术的开源开放,支撑全社会创新创业人员、团队和中小微企业投身前沿技术研发。

 为更好的推动教育行业开源文化的形成,提升底层技术应用开源水准,智慧教育国家新一代人工智能开放创新平台、好未来教育集团、开源中国技术社区将于12月5日在北京联合举办首届PHP开源技术大会。本届技术大会以“开源·分享·共建”为主题,聚焦2020各领域最炙手可热的PHP技术方案,为全国PHP开发者和技术爱好者打造开源届的饕餮盛宴。

预邀嘉宾

大会遍邀PHP开源社区最具影响力的技术大咖与开源领域学术专家,带来一整天的前沿技术实战干货分享。来自教育、电商、出行、直播等各行业头部企业嘉宾同台论道,呈现各行业PHP技术热门实践。本次大会特邀远在德国的PHP内核贡献者, 目前就职于JetBrains的Nikita Popov,带来PHP8.0的重要变化及产生的影响;还邀请到PingCAP联合创始人兼CTO黄东旭,为大家分享系统化分析云带来的改变,以及云基础设施和软件的融合方向;学而思网校首席架构师、Swoole开源项目创始人韩天峰也将在会上为大家带来精彩分享,可谓PHP届半壁江山齐聚,亮点多多,诚意满满。

参会报名

本次大会将以OMO的形式,于线上、线下同步进行。目前报名通道已正式开启,在大会官方网站(好未来PHP开源技术大会)、开源中国等平台均可报名参会。虽然受疫情影响,大会组委会还是坚持在线下保留了一部分名额限量发售,力争给技术伙伴创造一个零距离热烈交流学习的技术氛围。无法线下赴会的技术伙伴也欢迎登记报名,通过线上直播参与到这场技术盛宴中来。

参会指南

会议地点:北京·黄河京都会议中心

会议时间:2020年12月5日

报名入口:oscon.edu.com

交通方式:地铁五号线天通苑南站下车,步行1.2公里即可。

作为大会主办方之一,科技教育公司好未来一直致力于用科技成就教育美好,依托智慧教育国家新一代人工智能开放创新平台,加速技术与产业的融合创新,助推行业高质量发展。从今年8月正式公布首个开源项目以来,好未来始终秉持着“开源·分享·共建”的开源理念,密集上线近二十个开源组件,积极拥抱开源社区。

好未来也期待以首届PHP开源技术大会为契机,加强业界交流,提升国内教育科技的整体技术实力。欢迎各界技术伙伴踊跃参与,共绘教育行业生机勃勃的技术开源开放生态图景。

查看原文

赞 0 收藏 0 评论 0

好未来技术团队 发布了文章 · 2020-11-08

程序员都应该知道的URI,一文帮你全面了解

URI 是每个程序员都应该了解的概念,同时相关联的还有 URL, URN 等概念簇。了解这些概念,可以帮助我们更好地窥探万维网(WWW)的设计,同时也能帮我们在工作中有效解决跟 URI 相关概念的问题,更加理解 encode,decode 工作原理,更好地助力网络编程!

1.URI

URI(Uniform Resource Identifier) ,意为统一资源标识符,提供了一套简单可扩展的方式对资源进行标识。

1.1 URI 的前世今生

为什么会有 URI?
随着万维网的发展,需要有各种不同类型的资源被在网络上查找以及传输。因此,也就需要一种唯一的可在万维网上传播的标识,这样的统一资源标识就称为 URI。当然,资源在这里是一种笼统概念,或者抽象概念,可以泛指可以被标识的实体,就像一个网页,一本e-book, 一份 pdf 等等,只要有需要被呈现或者传输,都可以称为一种资源。

万维网奠基人Tim Berners-Lee关于超文本(hypertext)的提案中间接提出了用来标识超链接的想法–URL(Uniform Resource Locator)。因此,URL 也就最早被用来进行网络上可以提供访问的地址表示。随着HTTP, HTML 以及浏览器的逐步发展,越来越需要把标识资源可访问地址以及单出命名表示资源这两种方式分开,因此也就提出了 URN(Uniform Resource Name),并用来表示后者。
1601481963133af1d4cf9300e41299cdd8e76d4ce49a3.png

IETF(网络工程任务小组)主要负责 URI 相关标准制订。
160151617244204c7ab6d7b584c66aaa5fb4ee94d6e20.png

  • 1994年发布RFC1630, 指出了 URL 和 URN 的存在,同时定义了 URI 的正式语法。
  • 94年12月,RFC1738正式提出了 absolute 和relative URL, RFC2141则补充了URN 相关的文法和语法定义。
  • 1999年的 RFC2732允许 URI 使用 IPv6地址
  • 在2005年发布的RFC3986标准,解决了上述标准提出的一些短板,同时标志着URI 通用语法正式称为官方互联网协议
  • RFC3305标准指出,虽然 URL 名词被广泛使用,但是其本身可能被逐渐废弃,并且只用来做为一些 URI 作为间接提供该资源访问地址的提示。并且指出资源标识符不需要表示该资源通过网络的访问地址,或者根本不需要隐含该资源是基于网络提供的。(这里相当矛盾,其实 URL 已经作为民间事实标准并被广泛使用,也不是标准想推翻就能立刻推翻的 - -)

1.2 URI 和 URL,URN 比较

了解到 URI 和 URL,URN 整体的历史,可以看出来最早 URI和 URL 其实是一脉相源的。后来为了兼容单纯通过命名或者名称来标识某个资源(并不是可被网络直接访达或者包含包含网络访问地址)的情况,提出了 URN标准。由此可见,这三个名称都可以表示对一项资源的定位标识。比较有意思的问题是,在平常的工作沟通中,如何区分,并且在什么样的场景下该使用哪个名称?
160281482081581bfcde4ae77465ca7966aa28b6e005c.png

1.2.1 基本概念

先具体了解每个名称的基本概念:
1.URI
统一资源标识符。
用来表示某个特定资源。设计出来可以进行任何实体或者非实体的标识,但是目前被经常用于在网络上可传输内容的标识。URI 是由一串特定字符集的字符组成,并且由 IETF 制订的标准定义了一组语法规则,用来保证某个资源的统一和唯一标识。

2.URL
统一资源定位符。
也可以被称为网络地址。在万维网上,每个资源都有可以有唯一地址指向该资源,同时,通过该地址可以进行资源的读写,这样的地址标识就称为 URL。URL 包含了目前网络上常见的格式,包括 web 站点地址 http, 文件传输协议ftp, emal 地址协议 mailto以及数据库访问地址 JDBC 等。

3.URN
统一资源名称。
URN用来通过名称标识在特定命名空间的某个资源,同时希望为资源可以提供一种较持久的,与位置和存取方式无关的表示方式。URN 并不关注这个表示名称里是否隐含了该资源的位置,或者如何获取它,也不一定代表该资源一定可用。
举个例子,在ISBN(Internal Standard Book Number)系统中,一个编号(类似9971-5-0210-0)代表了一个书本资源,该编号在 URN 中可以表示为 urn:isbn:9971-5-0210-0, 但是这个编号并没有给出在哪里或者如何找到这本书的信息,它只能唯一标识了这本书。

1.2.1 三者之间的关系

先上图来说明 URI,URL 和 URN 之间的关系。
1601520706993142c193f0da14e8bb6acaf3b173cabf2.png
URI 可以认为是一个抽象的概念,所有的 URL 以及 URN 都是 URI。RFC3986标准中有这样一段:

A URI can be further classified as a locator, a name, or both. The term “Uniform Resource Locator” (URL) refers to the subset of URIs that, in addition to identifying a resource, provide a means of locating the resource by describing its primary access mechanism (e.g., its network “location”).
rfc 3986, section 1.1.3

URI 可以被分类成 locator 或者对应的名称表示,也就是包含了 URL 和 URN 的概念。因此,平常我们在说 URL 的时候,它其实也可以被称为 URI。

同样,这里有个非常有意思的问题,URN 其实比较好区分开,在使用唯一标识资源名称时可以使用,但是 URI 和 URL 如何区分在哪个场景进行使用?
这个问题其实和 RFC3986标准定义的不够清楚有关,请再看下面这一段:

The URI itself only provides identification; access to the resource is neither guaranteed nor implied by the presence of a URI.

rfc 3986, section 1.2.2

URI 不保证提供该资源的访问方式,或者隐含保证该资源是否存在(其实语义就是该 URI 就是一个名称表示),但是在上一段中又声明了URI 会被分类成name 或者 locator,表示 URI 应该包含locator 这种访问方式。再看下面这一段:

Each URI begins with a scheme name, as defined in Section 3.1, that refers to a specification for assigning identifiers within that scheme.

rfc 3986, section 1.1.1

每个 URI 都需要包含有起始 scheme 名称。比如:https://www.example.com,这样的一串字符串就可以称为 URI,但是明确标识了应该如何去访问这个资源,同时它也是 URL,因为 URL 是用来告知接收方获取该资源的方式。

IETF在RFC3986中也有一段关于 URI 和 URL 使用方式的说明:

Future specifications and related documentation should use the general term “URI” rather than the more restrictive terms “URL” and “URN”

rfc 3986, section 1.1.3

这样看来,好像IETF 更支持使用 URI 来代替 URL 这个称呼。但是考虑到 URL 目前已经成为用来描述网络上资源定位的事实名称,而且 RFC3986已经诞生超过15年了(有些条目确实跟不上时代发展速度),所以在针对互联网资源定位(即网络地址)的时候,URL 可以算是更贴切的名称。当然,如果对方跟你谈 URI等等,这也没问题,因为 URI 算是超类,并且也可以代表该资源。

下面是这个问题结论:

  • URI 是一种标记符
  • URL 是可以告诉你如何去访问或者获取该资源的一种标记符
  • 在描述网络资源地址的时候,用哪种都没问题,需要明确的原则就是最好和你的信息接收方用同样的称呼,方便理解
  • 如果觉得不好拿捏属于 URL 或者 URN,那就可以直接使用 URI 描述

2.URI 字符集

2.1 URI的设计点

URI 需要提供一种简单,可扩展的方式来唯一标识资源。同时,又需要考虑到在不同媒介上进行传播的表示形式。因此,URI 在设计时需要考虑到以下几点:

  • URI 需要是可移植的。

不同的系统,或者不同的接收方之间都可以使用 URI 协议来标识资源。URI 可以被表示成多种形式,比如说在纸上书写的字符串,或者屏幕上的像素,或者一系列通过编码的二进制流等。URI 的解析只跟这些呈现方式所关联的字符串有关,而跟具体表现方式,载体无关。
考虑到 URI 更多需要在网络场景传输,因此:

  • URI 是由一串字符序列组成
  • URI 可能会从非网络环境中移植到网络环境下,但是网络环境的输入一般受制于键盘,鼠标等输入载体,因此最好由可以被这些物理载体方便输入的字符呈现
  • URI 一般需要被人们记住并使用,所以这些字符最好是人们经常使用并且熟悉的内容

基于上述考虑,URI 为一串受限的字符所组成的字符串,并选择 US-ASCII 作为字符集。US-ASCII 字符集基本上被所有系统支持,而且兼容性良好,能够支持 URI 所需要的移植性。

  • URI 需要将标识和动作分开

这一层思想其实是需要将表示和表现分开。URI 只关注某个资源的标识,如果进行这个资源的存取或者访问不做任何方式的保证。同资源相关的动作,引用等,在设计时被交给具体实现 URI 下 scheme 的协议来制订,例如,http 协议会具体关心一个用’http’ scheme 表示的资源如何进行’get’, ‘update’,'delete’等一系列操作等。
这样可以保证 URI 协议的相对稳定,以及比较好的扩展性

  • 层级标识

由于资源经常具有层级关系,比如在一个 example.com 站点下可能会挂有多个资源,或者下面会有一个目录’dir’, 该目录下会包含多个资源,这就意味着URI 需要有一种层级的组织方式。
在设计中也考虑到了这样类型的资源组织方式,允许 URI 按照层级组织,并且在字符串上按照从左到右的顺序拆分组件。
类似于常用操作系统的文件系统一样,URI 可以用来还原具有层级关系的资源系统的组织结构。

2.2 URI 所选择的字符集

如上所属,URI 选择 通过US-ASCII 字符集来进行表示,并限制使用从其中所挑选的一部分字符,数字以及符号。而且,由于需要支持层级结构,以及 URI 自身包含了不同的部分,因此也需要保留一些字符用来做这些有语义的部分的分隔。

Note: 由于需要对字符集或者语法进行描述,下文都是用 IETF使用的通用描述系统ABNF(Augmented Backus-Naur form), 即增强巴科斯范式。
增强巴科斯范式所定义的语法结构一般如下:

rule = definition / definition; comment CR LF
rule = *element

表示一组规则由一系列字符串组成的定义来描述,第一组 rule通过’/‘来表示定义中’或者’的关系。如果该条规则需要增加注释,那么需要通过’;'来标识注释的开始
第二组 rule 表示重复规则,其中 a标识最少重复次数,b 标识最多重复次数。例如,2*3element标识 element 最少出现两次,最多出现三次

关于增强巴科斯范式的具体内容请参照:
https://en.wikipedia.org/wiki/Augmented_Backus%E2%80%93Naur_form

2.2.1 Percent-Encoding

由于 URI 在协议中只挑选了部分ASCII 字符,数字以及符号,那么当需要表示不在这个范围之内的符号,字符,或者该字符在 URI 中被用来分隔符等特殊用途时,就需要对这个字符进行%编码。百分号编码也可以叫做URLEncode,其一般格式为:

pct-encoded = "%" HEXDIG HEXDIG 

将不能直接使用的字符先转为字节流表示(一般为 utf-8编码,需要具体看上下文和 URI scheme 协议制订),然后每个字节转换为%加两个十六进制字符来表示。例如:
“00101011” 该字节需要编码为 “%2B” ,在 ASCII 码表中表示为 "+"号

Note: 百分号编码不关心大小写,但是为了统一和一致,最好应该使用大写字符

2.2.2 Reserved Characters

URI 保留字符集。
URI 自身定义时包含了 components以及 subcomponents,那么这些不同的 components 就需要通过分隔符来进行标识。这些被用来进行表示分隔的字符就成为保留字符集,这些字符集可能会被用作(或者将来会被用作)URI 不同部分的分隔符。
以下为 reserved character 所涉及的字符集表示:

reserved    = gen-delims / sub-delims

gen-delims  = ":" / "/" / "?" / "#" / "[" / "]" / "@"

sub-delims  = "!" / "$" / "&" / "'" / "(" / ")"
                  / "*" / "+" / "," / ";" / "=" 

gen-delims 字符集用来表示 URI component 之间的分隔符,考虑到 component 内会由不同的 subcomponents 组成,因此需要 sub-delims 字符集来定义 subcomponents之间的分隔符。

Note:这些字符在 URI 中一般具有特殊语义,因此不能被编码。同时,如果在进行两个 URI 相等性比较时,如果其中一个对协议中component 部分不能编码的保留字符进行编码,即使解码后两个 URI 字符相同,也会被认为是两个不同的 URI

2.2.3 Unreserved Characters

允许出现在URI 中,并且不会被拿来用作保留字符集的字符集合成为 Unreserved Characters。所涉及到字符ABNF 表示为:

unreserved  = ALPHA / DIGIT / "-" / "." / "_" / "~"

ALPHA = a-z / A-Z

DIGIT = 0-9 

这些字符为非保留字符,在 URI 使用过程中是不需要进行编码的。

Note: 如果在 URI 比较中包含这些字符,那么该字符本身或者其编码格式都应该认为是相等的,即这些字符编码不编码不会影响相等性。另外,这些字符在使用时最好不要编码,即使已经被编码,那么在使用时也应该先对这些字符进行解码。

2.2.4 总结

一图来表示在 URI 中所涉及到的保留和非保留字符,需要注意的是保留字符在不做分隔符或者具有特殊含义的时候是需要编码的。
alphadigit.png

3.URI Component

URI 语法规则由一系列 component 组成,并且在设计时需要考虑到扩展性以及对各个资源定位类型的兼容,因此在其起始都会有一个 scheme 头来特定标识这个 URI 所定义的资源类型标识符。另外,URI 由于是所有资源类型的超集(会细分为 URL 和 URN),所以 URI 所涉及的定义都是需要被遵守的基本定义。
URI component 一般由以下 component 组成(使用 ABNF 描述):

URI         = scheme ":" [ //authority ] path [ "?" query ] [ "#" fragment ]

authority   = [ userinfo@ ] host [ :port ] 

Note:

schme 和 path 为 required

有了上述语法规则的定义,举个例子来说明 URI 下两种不同的标识符所定义的各个 component 部分
image.png

下文将详细介绍各个组件部分,以及相应的语法规则。

3.1 URI component

3.1.1 Scheme

component

scheme

允许字符集

a-z  A-Z  0-9 + . -

是否 case-sensitive

component 结束标识符

:

Note:

  • 表中字符集为了呈现清晰,因此正则中通过非必要空格进行分隔,并且表或者关系
  • 结束标识符表示语法解析时该 component 解析结束符

scheme用来标识URI 所对应的具体协议。每个 URI 都必须以 scheme 开头。URI 的语法规则如下(使用 ABNF 描述):

scheme      = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) 

如上文所说,URI 定义通用的语法规则,scheme 所标识的具体协议会定义通用规则外的具体语法规则。例如,以 geo 为scheme 的协议 URI,表示特定地理位置标识,其语法规则如下:

geo:<lat>,<lon>[<alt>][u=<uncertainty>] 
参考自 RFC 5870

URI scheme 的官方注册信息目前由 IANA(Internet Assigned Numbers Authority) 组织进行添加和维护,目前约包含了335种不同协议 scheme,具体可参考https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml

3.1.2 Authority

component

Authority

component 开始标识符

//

component 结束标识符

/  ?  #

authority component 设计的目的为设定一个命名空间,并且标识这个命名空间被哪个机构所管理,例如 baidu.com, google.com 等等。authority 一般由三部分组成,包含了可选的 userinfo, port 以及必选的 host 部分。
关于为什么 Authority 部分会选择 // 作为起始符号的原因,Tim Berners-Lee 曾回答过:

  1. 需要选择一个命名系统来进行资源的层级化命名,/ 作为 unix 系统通用的分隔符可以在 URI 的设计中得到复用,因此使用 / 来作为 relative URI 的分隔符
  2. 需要有符号将 host 部分(类似 www.example.com)同URI 的其他部分进行区分,这部分设计参考了当时 Apollo domain system (其使用//computername/file/path进行命名)的设计方式
  3. 现在来看,他认为这个语法是比较冗余的,更喜欢直接通过:来进行域名分隔,例如 http://www.example.com/foo/bar 转写为 http:www.example.com/foo/bar, 这样写同样可以识别到server 并且更为简化

由此可见,标准的设计也是需要再不断地迭代和试验中前进 :)

3.1.2.1 Userinfo

component

Userinfo

允许字符集

pct-encode字符集 unreserved字符集 sub-delims字符集  :

是否 case-sensitive

component 结束标识符

@

userinfo 包含了用户相关信息(一般为名称,旧式格式 user:password 由于涉及安全风险已被弃用),同时需要通过@符合和 host 进行分隔。Userinfo 部分的语法规则如下(使用 ABNF 描述):

userinfo    = *( unreserved / pct-encoded / sub-delims / ":" ) 

3.1.2.2 Host

component

Host

允许字符集

pct-encode字符集 unreserved字符集 sub-delims字符集

是否 case-sensitive

component 结束标识符

/  :

服务提供商通过 host来提供服务,同时基于 dns 域名解析, server 和 host 之间可以做到非一一对应。host 部分可以有三种表示方式,IPv6, IPv4或者 registered name。registered name host的语法规则如下(通过 ABNF 描述):

host        = IPv6address / IPv4address / reg-name

IPv6address = [ HEXDIG *( :: HEXDIG ) ]

IPv4address = DIGIT "." DIGIT "." DIGIT "." DIGIT

reg-name    = *( unreserved / pct-encoded / sub-delims ) 

3.1.2.2 Port

component

Port

允许字符集

0-9

component 结束标识符

/

port 为可选项,同时通过十进制进行表示。在URI语法中,port 需要跟在 : 后。port 的语法规则如下(使用 ABNF 描述):

port        = *DIGIT 

每种 scheme 一般会定义一个默认端口。例如, http 定义80默认端口,https 定义443默认端口等。

3.1.3 Path

component

Path

允许字符集

pct-encode字符集 unreserved字符集 sub-delims字符集  @  :

component 结束标识符

? #  EOF

path标识了 host 下特定的资源路径,包含了一系列通过 / 分隔的 segments。需要注意的是,如果URI已经包含了 authority 部分,那么 path部分或者为空,或者需要以 / 来开头。另外,URI还允许 relative-path 的使用方式,这样的方式第一段 path segment 不能包含 :(如果包含,会被 parser 认为是 authority 部分)。以下是简化的 path 语法规则(使用 ABNF 描述):

path          = path-abempty  /  path-relative

path-abempty  = *( "/" segment )

path-relative = segment-nocolon *( "/" segment )

segment       = *pchar

pchar         = unreserved / pct-encoded / sub-delims / ":" / "@"

segment-nocolon = unreserved / pct-encoded / sub-delims / "@" 

3.1.4 Query

component

Query

允许字符集

pct-encode字符集 unreserved字符集 sub-delims字符集  @  :

component 开始标识符

component 结束标识符

  EOF

query 部分提供了定位资源的辅助信息,query其内部语法并没有明确定义,但是一般由name-value 键值对组成的字符串组成,中间通过分隔符 & 进行分隔。例如:name1=value1&name2=value2。query 的语法规则如下(使用 ABNF 描述):

query       = *( pchar / "/" / "?" )

pchar       = unreserved / pct-encoded / sub-delims / ":" / "@" 

3.1.5 Fragment

component

Query

允许字符集

pct-encode字符集 unreserved字符集 sub-delims字符集  @  :  /  ?

component 开始标识符

component 结束标识符

EOF

fragment 为段落标识符,一般用来标识一个 resource 的特定部分(一个资源子集或者一部分,或者通过这个资源来描述的一些其他资源)。 fragment 以 # 作为起始标识符,其语法规则如下(通过 ABNF 描述):

fragment    = *( pchar / "/" / "?" )

pchar       = unreserved / pct-encoded / sub-delims / ":" / "@" 

3.1.6 小结

各个component 允许的字符集部分是我们需要特别关注的,需要注意在五个 component 之间允许使用 gen-delims 字符集,在每个 component 内(即小组件间)允许使用 sub-delims 字符集。

3.2 解析 URI

如何通过程序来解析 URI, 并得到 URI 各个 component?
如上一节 ABNF 语法规则描述,URI 满足上下文无关文法。因此,我们可以通过语法图来呈现整体 URI 的解析规则,如下:
16025072234541dc24351cc2b4627a4d6fac6257dbdda.png

有了上图,使用递归下降,解析的伪代码就非常好写了:

`/**

  • 读取下一个字符

**/
function next() {
skip space;
read next char and return;
}

/**

  • 预扫描,查看对应的 input 字符串是否包含有 special_char,
  • 以及其位置

**/
function contains(input, special_char) {
start = input.start, end = input.end;
while (start < end) then

 if special_char equals start then return;

end
return start
}

/**

  • 对 uri 的解析函数
  • 具体的解析 component 方法为 parse_*, 需要匹配的字符集以及语法规则可参照上文中各个 ABNF

**/
function parse(string uri) {
parse_scheme;
skip next ';' ;
if next() == "//" then

   if contains(substring_uri(// until path), '@') then
      parse_userinfo;
   end
   parse_host;
   if next() == ':' then
      parse_port;
   end

end
parse_path;
if next() == '?' then

   parse_query;

end
if next() == '#' then

   parse_fragment;

end
}`

5.再论Encode 和 Decode

什么时候该 encode 或者 decode?
先说 URI 的设计目的,URI 被设计出并可在万维网上进行广泛传播,因此对各个子系统,浏览器等媒介的兼容性是最重要的,因此被设计使用被广泛使用的 ASCII 码进行承载。
因此,在生成 URI 过程中,应该先完成各个 componet 部分的编码,然后在联合 gen-delimiter 拼接成 URI。由于各个 scheme 的具体协议不同,因此只有在生成 URI 的过程中,才可以知道具体哪些 delimiter 会需要被编码,或者会被使用作为真正的 delimiter。一旦 URI 被生成,该 URI 在传播时就应该保持其 百分号 encode 的格式。
当百分号编码的 URI 在解码时,应该先通过 gen-delimiter 以及 sub-delimiter 将各个 component 进行分离,然后再对各个 component 进行分别解码。这样可以保证按照生成的 URI 被完整解码。
另外,需要注意的是,2.2.3中提到的 unreserved 字符集可以在任意时刻被编码和解码,但是推荐在生成 URI 时不对这些字符集进行编码,同时在解码时应该优先对这些字符集的百分号编码格式进行解码。

Note: 不应该对同一个 URI 重复次编码或者解码,这样会导致 URI所代表的语义失效。例如,对已经进行百分号编码的 URI 再进行编码时,又会再次对其中的百分号进行二次编码,从而导致 URI 在进行解码时含义错误。

5.1 实现 encode 和 decode

按照上文的说法,encode 需要先根据对应的 component 部分来组成不需要进行 escape(即不需要编码) 字符的规则,然后再进行逐一的判断和编码,之后再将编码过后的 component 拼接称为 URI(当然,如果所有的 delimiter 都不需要进行编码,那可以直接对整个 URI 进行编码,不需要 escape 的字符集直接包含这些 delimiter 字符)。 decode 则需要先将各个 component 按照 delimiter 进行拆分,然后分别对各个 component 在需要解码的字符规则下进行解码。

Note: 在标识 ASCII 以外的字符集时,一般是用 Unicode 字符集,编码方式为 UTF-8。
因此,在编码和解码过程中,如果编程语言层面使用 UTF-16进行字符编码(类似于 Java 和 JavaScript),那么需要将其转为 UTF-8编码,同时需要针对 UTF-16带来的 surrogate pair 进行额外处理。
关于surrogate pair 描述,可以参考
https://stackoverflow.com/questions/5903008/what-is-a-surrogate-pair-in-java#:~:text=The%20term%20%22surrogate%20pair%22%20refers,values%20between%200x0%20and%200x10FFFF.&text=This%20is%20done%20using%20pairs%20of%20code%20units%20known%20as%20surrogates.

5.1.1 encode

encode 的实现中需要注意的就是对需要编码的字节进行%编码,伪代码如下:

`/**

  • 对某一段 string s 进行 URI encode 编码
  • 传入 s 以及不需要编码的字符集 dontNeedEncodingSet, 返回 URI encode后的string

*

  • dontNeedEncodingSet 字符集需要根据3.1中的 component描述来定,例如 Path 中的不需要编码字符集
  • 一般为 unreserved字符集 sub-delims字符集 @ :(sub-delims 字符集以及@ : 如果其本身需要出现在
  • component 中而不是用来做分隔语义,那么同样需要进行 encode),另外不同的语言实现在不需要编码字符集
  • 上可能会有不同的选择

**/
function encode(s, dontNeedEncodingSet) {
// 声明 R 为结果字符串
def R, index = 0, strLen = s.length();
while index < strLen then

  def c 为 s 在 index 下的字符表示;
  if c 包含在 dontNeedEncodingSet 里 then
    R += c;
  else
    def 临时结果 out;
    /**
    * 这里需要考虑如果是 utf-16字符编码,那么需要判断 surrogate pair
    **/
    if c 在 surrogate pair中的第一个字符所表示的范围内 then
      def c2 为 ++index 位置字符;
      将 c c2两个字符组成 utf-16并进行 utf-8编码;
      将上述结果赋值给 out;
    else 
      如果 c 为 utf-16编码,需要转为 utf-8编码;
      out = c;
    end
    // 核心百分号 encode
    取 out 中每一个字节 out_byte;
    R += '%' + ((out_byte >> 4) & 0xF)转为16进制大写表示 + 
      ((out_byte) & 0xF)转为16进制大写表示;
  end
  ++index;

end
return R;
}`

5.1.2 decode

decode 的实现中需要注意在遇到%号时读取后续字符进行解码,同时如果语言实现使用 utf-16编码那么需要对 surrogate pair 进行还原(这部分语言本身一般都提供方法来对 utf-8进行转换),伪代码如下:

`/**

  • 对 s 进行解码,返回解码后的 string

**/
function decode(s) {

// 声明 R 为结果 string
def R, index = 0, lenStr = s.length();
while index < lenStr then
    def c 为 s 在 index 下的字符表示;
    if c == '%' then
        def 中间临时结果 out;
        while c == '%' && index + 2 < lenStr then
            读取index+1, index+2 字符 c1, c2;
            // 核心 decode
            out += (字符转为 hex 表示(c1)) << 4 | (字符转为 hex 表示(c2));
            index += 3;
        end
        // 异常情况报错
        if c == '%' && index < lenStr then 抛出错误;
        // 注意:如果语言实现需要 utf-16编码,那么需要先行将 out 转为 utf-16编码
        R += out;
    else
        R += c;
        ++index;
    end
end
return R;

}`

5.1.3 小结

相信各位已经对 URI 有了一个相对全面的了解,在实际工作的使用中,还需要根据语言所提供的对应 encode,decode 方法文档来进一步了解其编解码所定义的 component 部分特殊保留字符,这样会对所使用语言提供的 encode/decode 有更深入的了解 :)
**
Enjoy your coding trip~

作者:王阳(好未来Java开发专家)

查看原文

赞 16 收藏 11 评论 1

好未来技术团队 发布了文章 · 2020-10-30

好未来拥抱技术开源 共建智慧教育开放生态

2020年8月,好未来正式公布首个开源项目。随后,在短短的一个月内,好未来密集上线近二十个开源组件,积极拥抱开源技术社区。

作为教育行业代表性企业,好未来为何选择开源?开源之路将如何走?现阶段进展怎样?

从内生到开源

好未来的前身学而思成立于2003年,经过17年的高速发展,已成为国内领先的科技教育公司。好未来始终秉持「爱和科技让教育更美好」的使命,不断加大研发投入,设立了人工智能、大数据、脑认知等多个科技研发部门,并获国家人社部批准设立博士后科研工作站。

好未来先后与清华大学、中科院计算所等高校院所建立了联合实验室,与北京师范大学等签署了教育科学、脑与认知领域的战略合作协议,并与斯坦福大学、华东师范大学等开展联合科研项目。

好未来沉淀了大量优质教育资源和海量教学数据,还拥有丰富的教学场景和应用需求,并利用技术推动了多项智慧教育解决方案的落地。为实现技术资源共享与技术落地经验复用,好未来内部建立起跨业务的技术互通机制,积极推进技术中台建设,推动内部开源。而这也为好未来后续的对外开源打下了坚实的基础。

对此,好未来集团CTO田密表示,「代码开源和技术协同可以大幅减少企业和开发者的重复投入,提升研发效率,降低运营成本。好未来希望拥抱开源,赋能更多开发者协同创新,让更多技术人能够站在前人经验的基础上更进一步,帮助更多开发者进入行业生态,共创价值。这也是好未来对外开源的初衷。」

当技术情怀遇到国家战略

2019年8月,科技部发布《国家新一代人工智能开放创新平台建设工作指引》,着力提升技术创新研发实力和基础软硬件开放共享服务能力,鼓励各类通用软件和技术的开源开放,支撑全社会创新创业人员、团队和中小微企业投身前沿技术研发。在此背景下,好未来成为了智慧教育国家新一代人工智能开放创新平台承建单位。这也是好未来积极拥抱开源的重要原因。

田密解释,「开源将推动教育行业的技术落地和升级,为教育发展带来更多新动能。作为智慧教育国家新一代人工智能开放创新平台建设单位,好未来会将更多技术能力通过开源开放的形式与行业共享,加速技术与产业的融合创新,成就智慧教育的美好未来。」

此外,好未来也期待通过智慧教育国家开放创新平台的建设运行,协同各界伙伴,夯实教育领域的基础技术,全力为智慧教育关键技术创新提供高质量的服务和支撑,助力构建从开发者到业务场景的生态体系,加码行业创新的推动力。

开源路线图全面公开

好未来开源项目负责人谢华亮表示,「教育行业受众广泛、需求多样、场景复杂,相比互联网通用技术,教育科技有其自身的特点与要求。目前在各大技术领域都缺少适配教育行业的高水准开源项目与资源,好未来计划逐步开源内部积累的一系列实用技术组件、教育AI能力和教育场景数据集,填补这一行业空白。」

从AI能力开源的角度来看,好未来将推动在语音、图像等多个方向AI能力开源,为更多教育领域技术创新开发者提供数据基础。智慧教育国家开放创新平台也将提供数据标注、模型训练等AI能力的开源服务接口,并与国家开源平台联动,推动教育AI能力开源项目建设。

据了解,好未来积累的大量教育场景数据集,也会随着智慧教育国家开放创新平台的建设,逐步开源开放,并通过建立数据标准和数据服务机制,吸引更多的教育机构和科技企业,通过平台开放共享更多类型的教育数据。

(好未来AI能力开放架构图)

同时,好未来希望通过推动网络服务、数据存储、直播和框架服务等6大方向的组件开源,在教育各场景通用技术能力方面进行开源,促进平台生态的参与者共享平台成果。

(好未来通用技术能力未来开源架构图)

用开源成就教育美好未来

自启动首个开源项目以来,好未来已对外开源了接近二十个项目和技术组件,覆盖多个领域,包括通用编程语言框架、通用微服务治理、通用前端框架、教育行业通用技术组件等。

好未来积极拥抱开源,也赢得了开源社区的认可。Star是在开源社区上收藏开源项目的功能,Star数量的多少体现了项目的受欢迎程度。开源首月,好未来在全球某主流开源社区获得的Star数量已超两千多个。其中,关键开源组件获得的Star数量约为1500个。同时,专注服务开发者的好未来开源社群,在短短一个月内便吸引了近两千名开源技术爱好者的加入,且有着很高的活跃度和参与度。这也体现了好未来开源项目的优质性和高潜性。

可以说,作为教育行业代表性企业,好未来首次技术开源的阶段性进展和未来规划,是教育行业进一步信息化、数字化的标志。对于致力于投身教育行业的开发者而言,这也是一个新的契机。

查看原文

赞 2 收藏 0 评论 1

好未来技术团队 发布了文章 · 2020-09-20

一文读懂特征工程

特征工程(feature engineering):利用领域知识和现有数据,创造出新的特征,用于机器学习算法;可以手动(manual)或自动(automated)。神经网络的自动特征工程,常常不适用于现实中其他的复杂任务。因此,本文主要针对数据挖掘以及传统的机器学习,不会涉及图像识别、自然语言处理等深度学习领域。
俗话说:数据与特征工程决定了模型的上限,改进算法只不过是逼近这个上限而已
在豆瓣图书频道,搜索‘特征工程’关键词,搜索结果是仅有两本评分在7分以上数据,分别是精通特征工程特征工程入门与实践
img
当数据维度有限,那‘特征工程’是非常重要的,但发表这方面文章较少,更没全局化、系统化讲解特征工程理论和实战等,所以我想结合工作经验、相关书籍、优秀文章等总结的一套通用的数据科学-特征工程方法论。
从这 newcommandcolorful{color{red}} colorful{特征理解、特征清洗、特征构造、特征变换}newcommandcolorful{color{red}} colorful{特征理解、特征清洗、特征构造、特征变换} 等维度展开,逐步讲解理论代码实现等,针对代码实现部分 不能公开公司相关数据,所以选择用泰坦尼克号公开数据。

import pandas as pd
import numpy as np
import seaborn as sns
df_titanic = sns.load_dataset('titanic')

数据字段描述如下:
img

一、特征理解

1.1 区分结构化数据与非结构化数据
如一些以表格形式进行存储的数据,都是结构化数据;而非结构化数据就是一堆数据,类似于文本、报文、日志之类的。
1.2 区分定量和定性数据

  • 定量数据:指的是一些数值,用于衡量某件东西的数量;
  • 定性数据:指的是一些类别,用于描述某件东西的性质。

img

二、特征清洗

目标是提高数据质量,降低算法错误建模的风险。
现实的业务建模过程中,数据常常存在各种问题,数据存在不完全的、有噪声的、不一致的等各种情况。而这些带有错误信息的数据会对模型造成不利的影响。
数据清洗过程包括数据对齐、缺失值处理、异常值处理、数据转化等数据处理。
img
2.1 数据对齐
主要有时间、字段以及相关量纲的对齐。
1) 时间:

  • 日期格式不一致【’2019-07-20’、’20190720’、’2019/07/20’、’20/07/2019’】
  • 时间戳单位不一致,有的用秒表示,有的用毫秒表示;
  • 使用无效时间表示,时间戳使用 0 表示,结束时间戳使用 FFFF 表示。

2) 字段:

  • 姓名写了性别,身份证号写了手机号等

3) 量纲:

  • 数值类型统一 【如 1、2.0、3.21E3、四】
  • 单位统一【如 180cm、1.80m】

2.2 缺失处理

主要包括少量缺失的情况下,考虑不处理删除缺失数据或者采用均值、中位数、众数、同类均值填充。
当缺失值对模型影响比较大,存在比较多的不缺失数据的情况下,可以采用模型预测或者插值的方式。当缺失值过多时,可以对缺失值进行编码操作。
 对每个字段都计算其缺失值比例,然后按照缺失比例和字段重要性,分别制定策略,可用下图表示:
img
空值汇总分布

df_titanic.isnull().sum()
survived         0
pclass           0
sex              0
age            177
sibsp            0
parch            0
fare             0
embarked         2
class            0
who              0
adult_male       0
deck           688
embark_town      2
alive            0
alone            0

1) 删除元组
将存在遗漏信息属性值的对象(元组,记录)删除,从而得到一个完备的信息表。
优点:
简单易行,在对象有多个属性缺失值、被删除的含缺失值的对象与初始数据集的数据量相比非常小的情况下有效;
不足:
当缺失数据所占比例较大,特别当遗漏数据非随机分布时,这种方法可能导致数据发生偏离,从而引出错误的结论。
代码实现
embark_town 字段 有 2 个空值,可以考虑删除缺失处理下

df_titanic[df_titanic["embark_town"].isnull()]
df_titanic.dropna(axis=0,how='any',subset=['embark_town'],inplace=True)

img
2) 数据填充
用一定的值去填充空值,从而使信息表完备化。通常基于统计学原理,根据初始数据集中其余对象取值的分布情况来对一个缺失值进行填充。
(a) 人工填充(filling manually
根据业务知识来进行人工填充。
(b) 特殊值填充(Treating Missing Attribute values as Special values)
将空值作为一种特殊的属性值来处理,它不同于其他的任何属性值。如所有的空值都用“unknown”填充。一般作为临时填充或中间过程。
代码实现

df_titanic['embark_town'].fillna('unknown', inplace=True)

(c) 统计量填充
若缺失率较低(小于 95%)且重要性较低,则根据数据分布的情况进行填充。
常用填充统计量:
平均值:对于数据符合均匀分布,用该变量的均值填补缺失值。
中位数:对于数据存在倾斜分布的情况,采用中位数填补缺失值。
众数:离散特征可使用众数进行填充缺失值。

  • 中位数填充

fare:缺失值较多,使用中位数填充。
img

df_titanic['fare'].fillna(df_titanic['fare'].median(), inplace=True) 
  • 众数填充

embarked:只有两个缺失值,使用众数填充

df_titanic['embarked'].isnull().sum()
执行结果:2
df_titanic['embarked'].fillna(df_titanic['embarked'].mode(), inplace=True)
df_titanic['embarked'].value_counts()
执行结果:
S    64
  • 用 imputer 填充缺失值

imputer 类提供了缺失数值处理的基本策略,比如使用缺失数值所在行或列的均值、中位数、众数来替代缺失值。该类也兼容不同的缺失值编码。
填补缺失值:sklearn.preprocessing.Imputer(missing_values=’NaN’, strategy=’mean’, axis=0, verbose=0, copy=True)
主要参数说明:

missing_values:缺失值,可以为整数或 NaN(缺失值 numpy.nan 用字符串‘NaN’表示),默认为 NaN strategy:替换策略,字符串,默认用均值‘mean’替换 ① 若为mean时,用特征列的均值替换 ② 若为median时,用特征列的中位数替换 ③ 若为most_frequent时,用特征列的众数替换  axis:指定轴数,默认 axis=0 代表列,axis=1 代表行 copy:设置为 True 代表不在原数据集上修改,设置为 False 时,就地修改,存在如下情况时,即使设置为 False 时,也不会就地修改 ① X不是浮点值数组 ② X是稀疏且missing_values=0 ③ axis=0且X为CRS矩阵 ④ axis=1且X为CSC矩阵 statistics_属性:axis 设置为 0 时,每个特征的填充值数组,axis=1 时,报没有该属性错误
  • 同类均值填充

age:根据 sex、pclass 和 who 分组,如果落在相同的组别里,就用这个组别的均值或中位数填充。

df_titanic.groupby(['sex', 'pclass', 'who'])['age'].mean()
执行结果:
 sex     pclass  who 
female  1       child    10.333333
 woman    35.500000
 2       child     6.600000
 woman    32.179688
 3       child     7.100000
 woman    27.854167
male    1       child     5.306667
 man      42.382653
 2       child     2.258889
 man      33.588889
 3       child     6.515000
 man      28.995556
Name: age, dtype: float64
age_group_mean = df_titanic.groupby(['sex', 'pclass', 'who'])['age'].mean().reset_index()
age_group_mean
执行结果:
 sex     pclass  who    age
0    female    1    child    10.333333
1    female    1    woman    35.500000
2    female    2    child    6.600000
3    female    2    woman    32.179688
4    female    3    child    7.100000
5    female    3    woman    27.854167
6    male    1    child    5.306667
7    male    1    man    42.382653
8    male    2    child    2.258889
9    male    2    man    33.588889
10    male    3    child    6.515000
11    male    3    man    28.995556
def select_group_age_median(row):
 condition = ((row['sex'] == age_group_mean['sex']) &
 (row['pclass'] == age_group_mean['pclass']) &
 (row['who'] == age_group_mean['who']))
 return age_group_mean[condition]['age'].values[0]
df_titanic['age'] =df_titanic.apply(
 lambda x: select_group_age_median(x) if np.isnan(x['age']) else x['age'],axis=1)
执行结果:
0      22.000000
1      38.000000
2      26.000000
3      35.000000
4      35.000000
 ... 
886    27.000000
887    19.000000
888    27.854167
889    26.000000
890    32.000000
sns.distplot(df_titani

img
(d) 模型预测填充
使用待填充字段作为 Label,没有缺失的数据作为训练数据,建立分类/回归模型,对待填充的缺失字段进行预测并进行填充。
最近距离邻法(KNN)
先根据欧式距离或相关分析来确定距离具有缺失数据样本最近的 K 个样本,将这 K 个值加权平均/投票来估计该样本的缺失数据。
回归(Regression)
基于完整的数据集,建立回归方程。对于包含空值的对象,将已知属性值代入方程来估计未知属性值,以此估计值来进行填充。当变量不是线性相关时会导致有偏差的估计,常用线性回归。 
代码实现
age:缺失量较大,用 sex、pclass、who、fare、parch、sibsp 六个特征构建随机森林模型,填充年龄缺失值。

df_titanic_age = df_titanic[['age', 'pclass', 'sex', 'who','fare', 'parch', 'sibsp']]
df_titanic_age = pd.get_dummies(df_titanic_age)
df_titanic_age.head()
执行结果为
age    pclass    fare    parch    sibsp    sex_female    sex_male    who_child    who_man    who_woman
0    22.0    3    7.2500    0    1    0    1    0    1    0
1    38.0    1    71.2833    0    1    1    0    0    0    1
2    26.0    3    7.9250    0    0    1    0    0    0    1
3    35.0    1    53.1000    0    1    1    0    0    0    1
4    35.0    3    8.0500    0    0    0    1    0    1    0
# 乘客分成已知年龄和未知年龄两部分
known_age = df_titanic_age[df_titanic_age.age.notnull()]
unknown_age = df_titanic_age[df_titanic_age.age.isnull()]
# y 即目标年龄
y_for_age = known_age['age']
# X 即特征属性值
X_train_for_age = known_age.drop(['age'], axis=1)
X_test_for_age = unknown_age.drop(['age'], axis=1)
from sklearn.ensemble import RandomForestRegressor
rfr = RandomForestRegressor(random_state=0, n_estimators=2000, n_jobs=-1)
rfr.fit(X_train_for_age, y_for_age)
# 用得到的模型进行未知年龄结果预测
y_pred_age = rfr.predict(X_test_for_age)
# 用得到的预测结果填补原缺失数据
df_titanic.loc[df_titanic.age.isnull(), 'age'] = y_pred_age
sns.distplot(df_titanic.age)

img
(e) 插值法填充
包括随机插值,多重插补法,热平台插补,拉格朗日插值,牛顿插值等。

  • 线性插值法

使用插值法可以计算缺失值的估计值,所谓的插值法就是通过两点(x0,y0),(x1,y1)估计中间点的值,假设 y=f(x)是一条直线,通过已知的两点来计算函数 f(x),然后只要知道 x 就能求出 y,以此方法来估计缺失值。
.interpolate(method = 'linear', axis) 方法将通过 linear 插值使用沿着给定 axis 的值替换 NaN 值, 这个差值也就是前后或者上下的中间值

df_titanic['fare'].interpolate(method = 'linear', axis = 0)

同时,也可用行值插入

df_titanic['fare'].interpolate(method = 'linear', axis = 1)

代码实现

df_titanic['fare'].interpolate()
  • 多重插补(Multiple Imputation)

多值插补的思想来源于贝叶斯估计,认为待插补的值是随机的,它的值来自于已观测到的值。具体实践上通常是估计出待插补的值,然后再加上不同的噪声,形成多组可选插补值。根据某种选择依据,选取最合适的插补值。
多重插补方法分为三个步骤:
Step1:为每个空值产生一套可能的插补值,这些值反映了无响应模型的不确定性;
每个值都可以被用来插补数据集中的缺失值,产生若干个完整数据集合;
Step2:每个插补数据集合都用针对完整数据集的统计方法进行统计分析;
Step3:对来自各个插补数据集的结果,根据评分函数进行选择,产生最终的插补值。
(f) 哑变量填充
若变量是离散型,且不同值较少,可转换成哑变量,例如性别 SEX 变量,存在 male,fameal,NA 三个不同的值,可将该列转换成 IS_SEX_MALE、IS_SEX_FEMALE、IS_SEX_NA。若某个变量存在十几个不同的值,可根据每个值的频数,将频数较小的值归为一类’other’,降低维度。此做法可最大化保留变量的信息。
代码实现

sex_list = ['MALE', 'FEMALE', np.NaN, 'FEMALE', 'FEMALE', np.NaN, 'MALE']
df = pd.DataFrame({'SEX': sex_list})
display(df)
df.fillna('NA', inplace=True)
df = pd.get_dummies(df['SEX'],prefix='IS_SEX')
display(df)
# 原始数据
 SEX
0    MALE
1    FEMALE
2    NaN
3    FEMALE
4    FEMALE
5    NaN
6    MALE
# 填充后
 IS_SEX_FEMALE     IS_SEX_MALE    IS_SEX_NA
0    0                 1                0
1    1                 0                0
2    0                 0                1
3    1                 0                0
4    1                 0                0
5    0                 0                1
6    0                 1 

(g) 当特征值缺失超过 80%以上,建议删除【或变成是否变量】,容易影响模型效果

df_titanic.drop(["deck"],axis=1)

2.3 异常处理:
1) 异常值识别

  • 箱线法
sns.catplot(y="fare",x="survived", kind="box", data=df_titanic,palette="Set2");

img

  • 正态分布
sns.distplot(df_titanic.age)

img

  • 异常值检测方法

(a) 基于统计分析
通常用户用某个统计分布对数据点进行建模,再以假定的模型,根据点的分布来确定是否异常。
如通过分析统计数据的散度情况,即数据变异指标,对数据的分布情况有所了解,进而通过数据变异指标来发现数据中的异常点数据。
常用的数据变异指标有极差、四分位数间距、均差、标准差、变异系数等等,如变异指标的值大表示变异大、散布广;值小表示离差小,较密集。
譬如最大最小值可以用来判断这个变量的取值是否超过了合理的范围,如客户的年龄为-20 岁或 200 岁,为异常值。
(b) 3σ原则
若数据存在正态分布,在 3σ原则下,异常值为一组测定值中与平均值的偏差超过3倍标准差的值。如果数据服从正态分布,距离平均值3σ之外的值出现的概率为P(|x - μ| > 3σ) <= 0.003,属于极个别的小概率事件。如果数据不服从正态分布,也可以用远离平均值的多少倍标准差来描述。
img
(c) 箱线图分析
箱线图提供了识别异常值的一个标准:如果一个值小于 Q1-1.5IQR 或大于 Q3+1.5IQR 的值,则被称为异常值。
Q1 为下四分位数,表示全部观察值中有四分之一的数据取值比它小;
Q4 为上四分位数,表示全部观察值中有四分之一的数据取值比它大;
IQR 为四分位数间距,是上四分位数 Q1 与下四分位数 Q3 的差值,包含了全部观察值的一半。
箱型图判断异常值的方法以四分位数和四分位距为基础,四分位数具有鲁棒性:25%的数据可以变得任意远并且不会干扰四分位数,所以异常值不能对这个标准施加影响。因此箱型图识别异常值比较客观,在识别异常值时有一定的优越性。 
img
(d) 基于模型检测
首先建立一个数据模型,异常是那些同模型不能完美拟合的对象;如果模型是簇的集合,则异常是不显著属于任何簇的对象;在使用回归模型时,异常是相对远离预测值的对象。
优点:
有坚实的统计学理论基础,当存在充分的数据和所用的检验类型的知识时,这些检验可能非常有效。
缺点:
对于多元数据,可用的选择少一些,并且对于高维数据,这些检测可能性很差。
(e) 基于距离
基于距离的方法是基于下面这个假设:即若一个数据对象和大多数点距离都很远,那这个对象就是异常。通过定义对象之间的临近性度量,根据距离判断异常对象是否远离其他对象,主要使用的距离度量方法有绝对距离(曼哈顿距离)、欧氏距离和马氏距离等方法。
优点
基于距离的方法比基于统计类方法要简单得多;
因为为一个数据集合定义一个距离的度量要比确定数据集合的分布容易的多。
缺点:
基于邻近度的方法需要 O(m2)时间,大数据集不适用;
该方法对参数的选择也是敏感的;
不能处理具有不同密度区域的数据集,因为它使用全局阈值,不能考虑这种密度的变化。
(f) 基于密度
考察当前点周围密度,可以发现局部异常点,离群点的局部密度显著低于大部分近邻点,适用于非均匀的数据集。
优点:
给出了对象是离群点的定量度量,并且即使数据具有不同的区域也能够很好的处理。
缺点:
与基于距离的方法一样,这些方法必然具有 O(m2)的时间复杂度。
对于低维数据使用特定的数据结构可以达到 O(mlogm);
参数选择困难。
虽然算法通过观察不同的 k 值,取得最大离群点得分来处理该问题,但是,仍然需要选择这些值的上下界。
(g) 基于聚类
对象是否被认为是异常点可能依赖于簇的个数(如 k 很大时的噪声簇)。该问题也没有简单的答案。一种策略是对于不同的簇个数重复该分析。另一种方法是找出大量小簇,其想法是:
较小的簇倾向于更加凝聚;
如果存在大量小簇时一个对象是异常点,则它多半是一个真正的异常点。
不利的一面是一组异常点可能形成小簇而逃避检测。
优点
基于线性和接近线性复杂度(k 均值)的聚类技术来发现离群点可能是高度有效的;
簇的定义通常是离群点的补,因此可能同时发现簇和离群点。
缺点:
产生的离群点集和它们的得分可能非常依赖所用的簇的个数和数据中离群点的存在性;
聚类算法产生的簇的质量对该算法产生的离群点的质量影响非常大。
(h) 基于邻近度的异常点检测
一个对象是异常的,如果它远离大部分点。这种方法比统计学方法更一般、更容易使用,因为确定数据集的有意义的邻近性度量比确定它的统计分布更容易。一个对象的异常点得分由到它的 k-最近邻的距离给定。异常点得分对 k 的取值高度敏感。如果 k 太小(例如 1),则少量的邻近异常异常点可能导致较异常低的异常点得分;如果 K 太大,则点数少于 k 的簇中所有的对象可能都成了异常异常点。为了使该方案对于 k 的选取更具有鲁棒性,可以使用 k 个最近邻的平均距离。
优点
简单
缺点:
基于邻近度的方法需要 O(m2)时间,大数据集不适用;
该方法对参数的选择也是敏感的;
不能处理具有不同密度区域的数据集,因为它使用全局阈值,不能考虑这种密度的变化。
总结:
在数据处理阶段将离群点作为影响数据质量的异常点考虑,而不是作为通常所说的异常检测目标点,一般采用较为简单直观的方法,结合箱线图和 MAD 的统计方法判断变量的离群点。

 sns.scatterplot(x="fare", y="age", hue="survived",data=df_titanic,palette="Set1")

img
2) 处理方法
对异常值处理,需要具体情况具体分析,异常值处理的方法常用有四种:

  • 删除含有异常值的记录;
  • 某些筛选出来的异常样本是否真的是不需要的异常特征样本,最好找懂业务的再确认一下,防止我们将正常的样本过滤掉了。
  • 将异常值视为缺失值,交给缺失值处理方法来处理;
  • 使用均值/中位数/众数来修正;
  • 不处理。

三、特征构造

3.1 特征构造

目标是增强数据表达,添加先验知识。
如果我们对变量进行处理之后,效果仍不是非常理想,就需要进行特征构建了,也就是衍生新变量。 

3.3.1 统计量构造:

1) 基于业务规则、先验知识等构建新特征
2) 四分位数、中位数、平均值、标准差、偏差、偏度、偏锋、离散系统
3) 构造长、短期统计量(如 周、月)
4) 时间衰减(越靠近观测权重值高)

  • 年龄分段:child、young、midlife、old
def age_bin(x):
 if x <= 18:
 return 'child'
 elif x <= 30:
 return 'young'
 elif x <= 55:
 return 'midlife'
 else:
 return 'old'
df_titanic['age_bin'] = df_titanic['age'].map(age_bin)
df_titanic['age_bin'].unique()
执行结果:
array(['young', 'midlife', 'child', 'old'], dtype=object)
  • 抽取 title 特征
df_titanic['title'] = df_titanic['name'].map(
 lambda x: x.split(',')[1].split('.')[0].strip())
 
df_titanic['title'].value_counts()
执行结果:
Mr              757
Miss            260
Mrs             197
Master           61
Rev               8
Dr                8
Col               4
Ms                2
Major             2
Mlle              2
Dona              1
Sir               1
Capt              1
Don               1
Lady              1
Mme               1
the Countess      1
Jonkheer          1
# 再根据这些 title 细分,是官员,还是皇室,还是女士、先生、小姐
df_titanic['title'].unique()
执行结果:
array(['Mr', 'Mrs', 'Miss', 'Master', 'Don', 'Rev', 'Dr', 'Mme', 'Ms',
 'Major', 'Lady', 'Sir', 'Mlle', 'Col', 'Capt', 'the Countess',
 'Jonkheer', 'Dona'], dtype=object)
 
title_dictionary = {
 "Mr": "Mr",
 "Mrs": "Mrs",
 "Miss": "Miss",
 "Master": "Master",
 "Don": "Royalty",
 "Rev": "Officer",
 "Dr": "Officer",
 "Mme": "Mrs",
 "Ms": "Mrs",
 "Major": "Officer",
 "Lady": "Royalty",
 "Sir": "Royalty",
 "Mlle": "Miss",
 "Col": "Officer",
 "Capt": "Officer",
 "the Countess": "Royalty",
 "Jonkheer": "Royalty",
 "Dona": 'Mrs'
}
df_titanic['title'] = df_titanic['title'].map(title_dictionary)
df_titanic['title'].value_counts()
执行结果:
Mr         757
Miss       262
Mrs        201
Master      61
Officer     23
Royalty      5
  • 抽取家庭规模
df_titanic['family_size'] = df_titanic['sibsp'] + df_titanic['parch'] + 1
df_titanic['family_size'].head()
执行结果:
0    2
1    2
2    1
3    2
4    1

3.3.2 周期值

1) 前n个周期/天/月/年的周期值,如过去5天分位数、平均值等
2) 同比/环比 

3.3.3 数据分桶

1) 等频、等距分桶

(a) 自定义分箱
指根据业务经验或者常识等自行设定划分的区间,然后将原始数据归类到各个区间中。
(b) 等距分箱
按照相同宽度将数据分成几等份。
从最小值到最大值之间,均分为 N 等份, 这样, 如果 A,B 为最小最大值, 则每个区间的长度为 W=(B−A)/N , 则区间边界值为A+W,A+2W,….A+(N−1)W 。这里只考虑边界,每个等份里面的实例数量可能不等。
缺点是受到异常值的影响比较大
img
(c) 等频分箱
将数据分成几等份,每等份数据里面的个数是一样的。
区间的边界值要经过选择,使得每个区间包含大致相等的实例数量。比如说 N=10 ,每个区间应该包含大约10%的实例。
img

  • 数值变量分箱
# qcut 等频率分箱
df_titanic['fare_bin'], bins = pd.qcut(df_titanic['fare'], 5, retbins=True)
df_titanic['fare_bin'].value_counts()
(7.854, 10.5]        184
(21.679, 39.688]     180
(-0.001, 7.854]      179
(39.688, 512.329]    176
(10.5, 21.679]       172
bins #array([  0.    ,   7.8542,  10.5   ,  21.6792,  39.6875, 512.3292])
def fare_cut(age):
 if age <=  7.8958:
 return 0
 if age <= 10.5:
 return 1
 if age <= 21.6792:
 return 2
 if age <=  39.6875:
 return 3
 return 4
 
df_titanic['fare_bin'] = df_titanic['fare'].map(fare_cut)
# cut 等距离分箱
bins = [0, 12, 18, 65, 100]
pd.cut(df_titanic['age'], bins).value_counts

2) Best-KS分桶

1.将特征值值进行从小到大的排序。
2.计算出KS最大的那个值,即为切点,记为D。然后把数据切分成两部分。
3.重复步骤2,进行递归,D左右的数据进一步切割。直到KS的箱体数达到我们的预设阈值即可。
4.连续型变量:分箱后的KS值<=分箱前的KS值
5.分箱过程中,决定分箱后的KS值是某一个切点,而不是多个切点的共同作用。这个切点的位置是原始KS值最大的位置。
 注:代码实现请从网上查阅
3) 卡方分桶
自底向上的(即基于合并的)数据离散化方法。它依赖于卡方检验:具有最小卡方值的相邻区间合并在一起,直到满足确定的停止准则。
基本思想
对于精确的离散化,相对类频率在一个区间内应当完全一致。因此,如果两个相邻的区间具有非常类似的类分布,则这两个区间可以合并;否则,它们应当保持分开。而低卡方值表明它们具有相似的类分布。
实现步骤
Step 1:预先定义一个卡方的阈值;
Step 2:初始化;根据要离散的属性对实例进行排序,每个实例属于一个区间;
Step 3:合并区间;
计算每一对相邻区间的卡方值;
将卡方值最小的一对区间合并;
Aij:第i区间第j类的实例的数量;Eij:Aij的期望频率(=(Ni*Cj)/N),N是总样本数,Ni是第i组的样本数,Cj是第j类样本在全体中的比例;
阈值的意义
类别和属性独立时,有90%的可能性,计算得到的卡方值会小于4.6。大于阈值4.6的卡方值就说明属性和类不是相互独立的,不能合并。如果阈值选的大,区间合并就会进行很多次,离散后的区间数量少、区间大。
注意

ChiMerge算法推荐使用0.90、0.95、0.99置信度,最大区间数取10到15之间; 也可以不考虑卡方阈值,此时可以考虑最小区间数或者最大区间数。 指定区间数量的上限和下限,最多几个区间,最少几个区间; 对于类别型变量,需要分箱时需要按照某种方式进行排序。
代码实现
https://github.com/tatsumiw/C...

3) 最小熵法分箱

需要使总熵值达到最小,也就是使分箱能够最大限度地区分因变量的各类别。
熵是信息论中数据无序程度的度量标准,提出信息熵的基本目的是找出某种符号系统的信息量和冗余度之间的关系,以便能用最小的成本和消耗来实现最高效率的数据存储、管理和传递。
数据集的熵越低,说明数据之间的差异越小,最小熵划分就是为了使每箱中的数据具有最好的相似性。给定箱的个数,如果考虑所有可能的分箱情况,最小熵方法得到的箱应该是具有最小熵的分箱。

3.3.4 特征组合

:有限考虑强特征维度
1) 离散+离散:笛卡尔积
2) 离散+连续:连续特征分桶后进行笛卡尔积或基于类别特征 group  by,类似于聚类特征构造
3) 连续+连续:加减乘除,二阶差分等

  • 多项式生成新特征【针对连续值】
df_titanic_numerical = df_titanic[['age','sibsp','parch','fare','family_size']]
df_titanic_numerical.head()
执行结果:
age    sibsp    parch    fare    family_size
0    22.0    1    0    7.2500    2
1    38.0    1    0    71.2833    2
2    26.0    0    0    7.9250    1
3    35.0    1    0    53.1000    2
4    35.0    0    0    8.0500    1
# 扩展数值特征
from sklearn.preprocessing import PolynomialFeatures
poly = PolynomialFeatures(degree=2, include_bias=False, interaction_only=False)
df_titanic_numerical_poly = poly.fit_transform(df_titanic_numerical)
pd.DataFrame(df_titanic_numerical_poly, columns=poly.get_feature_names()).hea

img
查看下衍生新变量后的相关性情况,颜色越深相关性越大:

sns.heatmap(pd.DataFrame(df_titanic_numerical_poly, columns=poly.get_feature_names()).corr())

img

3.4 特征选择

目标是降低噪声,平滑预测能力和计算复杂度,增强模型预测性能
当数据预处理完成后,我们需要选择有意义的特征输入机器学习的算法和模型进行训练。
通常来说,从两个方面考虑来选择特征:

  • 特征是否发散:如果一个特征不发散,例如方差接近于 0,也就是说样本在这个特征上基本上没有差异,这个特征对于样本的区分并没有什么用。
  • 特征与目标的相关性:这点比较显见,与目标相关性高的特征,应当优选选择。除方差法外,本文介绍的其他方法均从相关性考虑。

根据特征选择的形式又可以将特征选择方法分为 3 种:

  • Filter:过滤法,按照发散性或者相关性对各个特征进行评分,设定阈值或者待选择阈值的个数来选择特征。
  • Wrapper:包装法,根据目标函数(通常是预测效果评分),每次选择若干特征或者排除若干特征。
  • Embedded:嵌入法,先使用某些机器学习的算法和模型进行训练,得到各个特征的权值系数,根据系数从大到小选择特征。类似于 Filter 方法,但是是通过训练来确定特征的优劣。  我们使用 sklearn 中的 feature_selection 库来进行特征选择。

3.4.1 过滤式

1) 方差过滤
这是通过特征本身的方差来筛选特征的类。比如一个特征本身的方差很小,就表示样本在这个特征上基本没有差异,可能特征中的大多数值都一样,甚至整个特征的取值都相同,那这个特征对于样本区分没有什么作用。所以无论接下来的特征工程要做什么,都要优先消除方差为 0 的特征。VarianceThreshold 有重要参数 threshold,表示方差的阈值,表示舍弃所有方差小于 threshold 的特征,不填默认为 0,即删除所有的记录都相同的特征。

from sklearn.feature_selection import VarianceThreshold
variancethreshold = VarianceThreshold() #实例化,默认方差为 0.方差<=0 的过滤掉
df_titanic_numerical = df_titanic[['age','sibsp','parch','fare','family_size']]
X_var = variancethreshold.fit_transform(df_titanic_numerical)    #获取删除不合格特征后的新特征矩阵
variancethreshold.variances_
array([ 79.58,1.21467827,0.64899903,512.3292,2.60032675])
del_list = df_titanic_numerical.columns[variancethreshold.get_support()==0].to_list()  #获得删除

然 而,如果我们知道我们需要多少个特征,方差也可以帮助我们将特征选择一步到位。
比如说,我们希望留下一半的特征,那可以设定一个让特征总数减半的方差阈值,只要找到特征方差的中位数,再将这个中位数作为参数 threshold 的值输入就好了:

df_titanic_numerical_fsvar = VarianceThreshold(np.median(df_titanic_numerical.var().values)).fit_transform(df_titanic_numerical)

当特征是二分类时,特征的取值就是伯努利随机变,假设 p=0.8,即二分类特征中某种分类占到 80%以上的时候删除特征

X_bvar = VarianceThreshold(.8 * (1 - .8)).fit_transform(df_titanic_numerical) 
X_bvar.shape
执行结果:
(891, 5)

2) 卡方过滤
卡方检验,专用于分类算法,捕捉相关性 追求 p 小于显著性水平的特征
卡方过滤是专门针对离散型标签(即分类问题)的相关性过滤。
img
卡方检验类 feature_selection.chi2 计算每个非负特征和标签之间的卡方统计量,并依照卡方统计量由高到低为特征排名

df_titanic_categorical = df_titanic[['sex', 'class', 'embarked', 'who',  'age_bin','adult_male','alone','fare_bin']]
df_titanic_numerical = df_titanic[['age','sibsp','parch','fare','family_size','pclass']]
df_titanic_categorical_one_hot = pd.get_dummies(
 df_titanic_categorical,
 columns=['sex', 'class', 'embarked', 'who',  'age_bin','adult_male','alone','fare_bin'],
 drop_first=True)
df_titanic_combined = pd.concat([df_titanic_numerical,df_titanic_categorical_one_hot],axis=1)
y = df_titanic['survived']
X = df_titanic_combined.iloc[:,1:]
from sklearn.feature_selection import chi2
from sklearn.feature_selection import SelectKBest
chi_value, p_value = chi2(X,y)
#根据 p 值,得出 k 值
k = chi_value.shape[0] - (p_value > 0.05).sum()  #要保留的特征的数量 14
#根据卡方值,选择前几特征,筛选后特征
X_chi = SelectKBest(chi2, k=14).fit_transform(X, y)
X_chi.shape
(89

3)  F 检验
只能捕捉线性相关性 要求数据服从正态分布,追求 P 值小于显著性水平特征。
F 检验,又称 ANOVA,方差齐性检验,是用来捕捉每个特征与标签之间的线性关系的过滤方法。它即可以做回归也可以做分类,因此包含 feature_selection.f_classif(F 检验分类)和 feature_selection.f_regression(F 检验回归)两个类。其中 F 检验分类用于标签是离散型变量的数据,而 F 检验回归用于标签是连续型变量的数据。
F 检验的本质是寻找两组数据之间的线性关系,其原假设是”数据不存在显著的线性关系“。

from sklearn.feature_selection import f_classif
f_value, p_value = f_classif(X,y)
#根据 p 值,得出 k 值
k = f_value.shape[0] - (p_value > 0.05).sum()
#筛选后特征
X_classif = SelectKBest(f_classif, k=14).fit_transform(X, y)

4)  互信息法
可以捕捉任何相关性 不能用于稀疏矩阵,追求互信息大于 0 的特征
互信息法是用来捕捉每个特征与标签之间的任意关系(包括线性和非线性关系)的过滤方法。和 F 检验相似,它既可以做回归也可以做分类,并且包含两个类:
feature_selection.mutual_info_classif(互信息分类)feature_selection.mutual_info_regression(互信息回归)
这两个类的用法和参数都和 F 检验一模一样,不过 互信息法比 F 检验更加强大,F 检验只能够找出线性关系,而互信息法可以找出任意关系。 互信息法不返回 p 值或 F 值类似的统计量,它返回“每个特征与目标之间的互信息量的估计”,这个估计量在[0,1]之间取值,为 0 则表示两个变量独立,为 1 则表示两个变量完全相关。

from sklearn.feature_selection import mutual_info_classif as MIC
#互信息法
mic_result = MIC(X,y)   #互信息量估计
k = mic_result.shape[0] - sum(mic_result <= 0)    #16
X_mic = SelectKBest(MIC, k=16).fit_transform(X, y)
X_mic.shape
(891, 16)

3.4.2 包裹式

1)  递归特征消除法
递归消除特征法使用一个基模型来进行多轮训练,每轮训练后,消除若干权值系数的特征,再基于新的特征集进行下一轮训练。使用 feature_selection 库的 RFE 类来选择特征的代码如下:

from sklearn.feature_selection import RFE
from sklearn.linear_model import LogisticRegression
#递归特征消除法,返回特征选择后的数据
#参数 estimator 为基模型
#参数 n_features_to_select 为选择的特征个数
X_ref = RFE(estimator=LogisticRegression(), n_features_to_select=10).fit_transform(X, y)

2) 重要性评估

from sklearn.ensemble import ExtraTreesClassifier
# feature extraction
model = ExtraTreesClassifier()
model.fit(X, y)
print(model.feature_importances_)
feature=list(zip(X.columns,model.feature_importances_))
feature=pd.DataFrame(feature,columns=['feature','importances'])
feature.sort_values(by='importances',ascending=False).head(20)
feature    importances
2    fare    0.227659
15    adult_male_True    0.130000
10    who_man    0.108939
5    sex_male    0.078065
11    who_woman    0.059090
7    class_Third    0.055755
4    pclass    0.048733
3    family_size    0.038347
0    sibsp    0.035489
9    embarked_S    0.029512
1    parch    0.023778
20    fare_bin_(39.688, 512.329]    0.022985
14    age_bin_young    0.021404
12    age_bin_midlife    0.019379
6    class_Second    0.019301
17    fare_bin_(7.854, 10.5]    0.016448
19    fare_bin_(21.679, 39.688]    0.016006
18    fare_bin_(10.5, 21.679]    0.014871
16    alone_True    0.013093
13    age_bin_old    0.0112

3) 排列重要性评估
优点:快速计算;易于使用和理解;特征重要性度量的属性;追求特征稳定性
原理:在训练机器学习模型之后计算置换重要性。这种方法在向模型提出假设,如果在保留目标和所有其他列的同时随机打乱一列验证集特征数据,对预测机器学习模型的准确性的影响程度。对于一个具有高度重要性的特征,random-reshuffle会对机器学习模型预测的准确性造成更大的损害。
结果解读:每一行的第一个数字表示模型性能(例子中用的是准确率)衰减了多少,±后面的数字表示多次打乱的标准差。

import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
import eli5
from eli5.sklearn import PermutationImportance
my_model = RandomForestClassifier(random_state=0).fit(train_X, train_y)
perm = PermutationImportance(my_model, random_state=1).fit(val_X, val_y)
eli5.show_weights(perm, feature_names = val_X.columns.tolist())

img

3.4.3 嵌入式

1)  基于惩罚项的特征选择法
使用带惩罚项的基模型,除了筛选出特征外,同时也进行了降维。
使用 feature_selection 库的 SelectFromModel 类结合带 L1 惩罚项的逻辑回归模型,来选择特征的代码如下:

from sklearn.feature_selection import SelectFromModel
from sklearn.linear_model import LogisticRegression
#带 L1 和 L2 惩罚项的逻辑回归作为基模型的特征选择,这个设置带 L1 惩罚项的逻辑回归作为基模型的特征选择
lr = LogisticRegression(solver='liblinear',penalty="l1", C=0.1)
X_sfm = SelectFromModel(lr).fit_transform(X, y)
X_sfm.shape
(891, 7

使用 feature_selection 库的 SelectFromModel 类结合 SVM 模型,来选择特征的代码如下:

from sklearn.feature_selection import SelectFromModel
from sklearn.svm import LinearSVC
lsvc = LinearSVC(C=0.01,penalty='l1',dual=False).fit(X, y)
model = SelectFromModel(lsvc,prefit=True)
X_sfm_svm = model.transform(X)
X_sfm_svm.shape
(891, 7

2) 基于树模型
​    树模型中 GBDT 也可用来作为基模型进行特征选择,使用 feature_selection 库的 SelectFromModel 类结合 GBDT 模型,来选择特征的代码如下:

from sklearn.feature_selection import SelectFromModel
from sklearn.ensemble import GradientBoostingClassifier
#GBDT 作为基模型的特征选择
gbdt = GradientBoostingClassifier()
X_sfm_gbdt = SelectFromModel(gbdt).fit_transform(X, y)
X_sfm_gbdt.shape
(891, 5)

总结一下,有几点做特征选择的方法经验:
(1)如果特征是分类变量,那么可以从 SelectKBest 开始,用卡方或者基于树的选择器来选择变量;
(2)如果特征是定量变量,可以直接用线性模型和基于相关性的选择器来选择变量;
(3)如果是二分类问题,可以考虑使用 SelectFromModel 和 SVC;
(4)在进行特征选择前,还是需要做一下 EDA。

四、特征变换

img

1) 标准化(Standardization)

转换为 Z-score,使数值特征列的算数平均为 0,方差(以及标准差)为 1。不免疫 outlier。 
:如果数值特征列中存在数值极大或极小的 outlier(通过 EDA 发现),应该使用更稳健(robust)的统计数据:用中位数而不是算术平均数,用分位数(quantile)而不是方差。这种标准化方法有一个重要的参数:(分位数下限,分位数上限),最好通过 EDA 的数据可视化确定。免疫 outlier。
img

from sklearn.preprocessing import StandardScale
#标准化模型训练
Stan_scaler = StandardScaler()
Stan_scaler.fit(x)
x_zscore = Stan_scaler.transform(x)
x_test_zscore = Stan_scaler.transform(x_test)
joblib.dump(Stan_scaler,'zscore.m')  #写入文件

2)  归一化(Normalization)

把每一行数据归一化,使之有 unit norm,norm 的种类可以选 l1、l2 或 max。不免疫 outlier。 
img
,其中 iotaiota 表示 norm 函数。 

3)  区间缩放(scaling)

将一列的数值,除以这一列的最大绝对值。
MinMaxScaler:线性映射到 [ 0,1 ] ,不免疫 outlier。 
img
MaxAbsScaler:线性映射到 [ -1,1 ] ,不免疫 outlier。 
img

from sklearn import preprocessing
min_max_scaler = preprocessing.MinMaxScaler()
min_max_scaler.fit_transform(x)
x_minmax = min_max_scaler.transform(x)
x_test_minmax = min_max_scaler.transform(x_test)
joblib.dump(min_max_scaler,'min_max_scaler.m')  #写入文件

:如果数值特征列中存在数值极大或极小的 outlier(通过 EDA 发现),应该使用更稳健(robust)的统计数据:用中位数而不是算术平均数,用分位数(quantile)而不是方差。这种标准化方法有一个重要的参数:(分位数下限,分位数上限),最好通过 EDA 的数据可视化确定。免疫 outlier。
归一化与标准化区别

(a) 目的不同,归一化是为了消除纲量压缩到[0,1]区间;标准化只是调整特征整体的分布。  (b) 归一化与最大,最小值有关;标准化与均值,标准差有关。 (c) 归一化输出在[0,1]之间;标准化无限制。
归一化与标准化应用场景
(a) 在分类、聚类算法中,需要使用距离来度量相似性的时候(如 SVM、KNN)或者使用 PCA 技术进行降维的时候,标准化(Z-score standardization)表现更好。  (b) 在不涉及距离度量、协方差计算、数据不符合正太分布的时候,可以使用第一种方法或其他归一化方法。比如图像处理中,将 RGB 图像转换为灰度图像后将其值限定在[0 255]的范围。  (c) 基于树的方法不需要进行特征的归一化。例如随机森林,bagging 与 boosting 等方法。如果是基于参数的模型或者基于距离的模型,因为需要对参数或者距离进行计算,都需要进行归一化。

3.5.2 非线性变换【统计变换】

利用统计或数学变换来减轻数据分布倾斜的影响。使原本密集的区间的值尽可能的分散,原本分散的区间的值尽量的聚合。
这些变换函数都属于幂变换函数簇,通常用来创建单调的数据变换。它们的主要作用在于它能帮助稳定方差,始终保持分布接近于正态分布并使得数据与分布的平均值无关。

1) log 变换

log 变换通常用来创建单调的数据变换。它的主要作用在于帮助稳定方差,始终保持分布接近于正态分布并使得数据与分布的平均值无关。因为 log 变换倾向于拉伸那些落在较低的幅度范围内自变量值的范围,倾向于压缩或减少更高幅度范围内的自变量值的范围。从而使得倾斜分布尽可能的接近正态分布。  所以针对一些数值连续特征的方差不稳定,特征值重尾分布我们需要采用 log 化来调整整个数据分布的方差,属于方差稳定型数据转换。
log 变换属于幂变换函数簇。该函数用数学表达式表示为  
img
自然对数使用 b=e,e=2.71828,通常叫作欧拉常数。你可以使用通常在十进制系统中使用的 b=10 作为底数。
代码实现

sns.distplot(df_titanic.fare,kde=False)

img

df_titanic['fare_log'] = np.log((1+df_titanic['fare']))
sns.distplot(df_titanic.fare_log,kde=False)

img

2)  box-cox 变换

box-cox 变换是另一个流行的幂变换函数簇中的一个函数。该函数有一个前提条件,即数值型值必须先变换为正数(与 log 变换所要求的一样)。万一出现数值是负的,使用一个常数对数值进行偏移是有帮助的。
box-cox 变换是 box 和 cox 在 1964 年提出的一种广义幂变换方法,是统计建模中常用的一种数据变换,用于连续的响应变量不满足正态分布的情况。box-cox 变换之后,可以一定程度上减小不可观测的误差和预测变量的相关性。box-cox 变换的主要特点是引入一个参数,通过数据本身估计该参数进而确定应采取的数据变换形式,box-cox 变换可以明显地改善数据的正态性、对称性和方差相等性,对许多实际数据都是行之有效的。
box-cox 变换函数:
img
生成的变换后的输出 y 是输入 x 和变换参数的函数;当 λ=0 时,该变换就是自然对数 log 变换,前面我们已经提到过了。λ 的最佳取值通常由最大似然或最大对数似然确定。
代码实现

# 从数据分布中移除非零值
fare_positive_value = df_titanic[(~df_titanic['fare'].isnull()) & (df_titanic['fare']>0)]['fare']
import scipy.stats as spstats
# 计算最佳λ值
l, opt_lambda = spstats.boxcox(fare_positive_value)
print('Optimal lambda value:', opt_lambda) # -0.5239075895755266
# 进行 Box-Cox 变换
fare_boxcox_lambda_opt = spstats.boxcox(df_titanic[df_titanic['fare']>0]['fare'],lmbda=opt_lambda)
sns.distplot(fare_boxcox_lambda_opt,kde=Fal

img

3.5.3 离散变量处理

1)  标签编码(label encoder)

LabelEncoder 是对不连续的数字或者文本进行编号,编码值介于 0 和 n_classes-1 之间的标签。
例如:比如有[dog,cat,dog,mouse,cat],我们把其转换为[1,2,1,3,2]。这里就产生了一个奇怪的现象:dog 和 mouse 的平均值是 cat。
优点:相对于 OneHot 编码,LabelEncoder 编码占用内存空间小,并且支持文本特征编码。
缺点:它隐含了一个假设:不同的类别之间,存在一种顺序关系。在具体的代码实现里,LabelEncoder 会对定性特征列中的所有独特数据进行一次排序,从而得出从原始输入到整数的映射。所以目前还没有发现标签编码的广泛使用,一般在树模型中可以使用。
代码实现

from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()
le.fit(["超一线", "一线", "二线", "三线"])
print('特征:{}'.format(list(le.classes_)))
# 输出 特征:['一线', '三线', '二线', '超一线']
print('转换标签值:{}'.format(le.transform(["超一线", "一线", "二线"])))
# 输出 转换标签值:array([3 0 2]...)
print('特征标签值反转:{}'.format(list(le.inverse_transform([2, 2, 1]))))
# 输出 特征标签值反转:['二线', '二线', '三线

2) 独热编码(one hot encoder )

OneHotEncoder 用于将表示分类的数据扩维。最简单的理解用 N 位状态寄存器编码 N 个状态,每个状态都有独立的寄存器位,且这些寄存器位中只有一位有效,只能有一个状态。
为什么要使用独热编码?
独热编码是因为大部分算法是基于向量空间中的度量来进行计算的,为了使非偏序关系的变量取值不具有偏序性,并且到圆点是等距的。使用 one-hot 编码,将离散特征的取值扩展到了欧式空间,离散特征的某个取值就对应欧式空间的某个点。将离散型特征使用 one-hot 编码,会让特征之间的距离计算更加合理。  
为什么特征向量要映射到欧式空间?
将离散特征通过 one-hot 编码映射到欧式空间,是因为在回归、分类、聚类等机器学习算法中,特征之间距离或相似度的计算是非常重要的,而我们常用的距离或相似度的计算都是在欧式空间的相似度计算。
举个例子-假如有三种颜色特征:红、黄、蓝。
在利用机器学习的算法时一般需要进行向量化或者数字化。那么你可能想 假设 红=1,黄=2,蓝=3,那么这样实现了标签编码,即给不同类别以标签。然而这意味着机器可能会学习到“红<黄<蓝”,但这并不是我们的让机器学习的本意,只是想让机器区分它们,并无大小比较之意。
所以这时标签编码是不够的,需要进一步转换。因为有三种颜色状态,所以就有 3 个比特。即红色:1 0 0,黄色: 0 1 0,蓝色:0 0 1。如此一来每两个向量之间的距离都是根号 2,在向量空间距离都相等,所以这样不会出现偏序性,基本不会影响基于向量空间度量算法的效果。
优点:独热编码解决了分类器不好处理属性数据的问题,在一定程度上也起到了扩充特征的作用。它的值只有 0 和 1,不同的类型存储在垂直的空间。
缺点:只能对数值型变量二值化,无法直接对字符串型的类别变量编码。当类别的数量很多时,特征空间会变得非常大。在这种情况下,一般可以用 PCA 来减少维度。而且 one hot encoding+PCA 这种组合在实际中也非常有用。
代码实现

  • 使用 pandas 实现:
sex_list = ['MALE', 'FEMALE', np.NaN, 'FEMALE', 'FEMALE', np.NaN, 'MALE']
df = pd.DataFrame({'SEX': sex_list})
display(df)
df.fillna('NA', inplace=True)
df = pd.get_dummies(df['SEX'],prefix='IS_SEX')
display(df)
# 原始数据
 SEX
0   MALE
1   FEMALE
2   NaN
3   FEMALE
4   FEMALE
5   NaN
6   MALE
# 填充后
 IS_SEX_FEMALE     IS_SEX_MALE    IS_SEX_NA
0    0                 1                0
1    1                 0                0
2    0                 0                1
3    1                 0                0
4    1                 0                0
5    0                 0                1 
pd.get_dummies(
 df_titanic,
 columns=[
 'sex', 'class', 'pclass', 'embarked', 'who', 'family_size', 'age_bin'
 ],drop_first=True)

img

  • 使用 sklearn 实现:
注:当特征是字符串类型时,需要先用 LabelEncoder() 转换成连续的数值型变量,再用 OneHotEncoder() 二值化 
sklearn.preprocessing 中的 OneHotEncoder 将 shape=(None,1)的列向量中每个分量表示的下标(index)编码成 one hot 行向量。
import numpy as np
from sklearn.preprocessing import OneHotEncoder
行向量转列向量:
# 非负整数表示的标签列表
labels = [0,1,0,2]
# 行向量转列向量
labels = np.array(labels).reshape(len(labels), -1)
one hot 编码:
enc = OneHotEncoder()
enc.fit(labels)
targets = enc.transform(labels).toarray()
# 如果不加 toarray() 的话,输出的是稀疏的存储格式,即索引加值的形式,也可以通过参数指定 sparse = False 来达到同样的效果
编码结果:
array([[ 1.,  0.,  0.],
 [ 0.,  1.,  0.],
 [ 1.,  0.,  0.],
 [ 0.,  0.,  1.]])

3)  标签二值化(LabelBinarizer)

 功能与 OneHotEncoder 一样,但是 OneHotEncoder 只能对数值型变量二值化,无法直接对字符串型的类别变量编码,而 LabelBinarizer 可以直接对字符型变量二值化。

3.5.4 降维

读取数据&数据展示

from sklearn import datasets
iris_data = datasets.load_iris()
X = iris_data.data
y = iris_data.target
def draw_result(X, y):
 """
 X:     降维后的数据
 iris:  原数据
 """
 plt.figure()
 # 提取 Iris-setosa
 setosa = X[y == 0]
 # 绘制点:参数 1 x 向量,y 向量
 plt.scatter(setosa[:, 0], setosa[:, 1], color="red", label="Iris-setosa")
 # Iris-versicolor
 versicolor = X[y == 1]
 plt.scatter(versicolor[:, 0], versicolor[:, 1], color="orange", label="Iris-versicolor")
 # Iris-virginica
 virginica = X[y == 2]
 plt.scatter(virginica[:, 0], virginica[:, 1], color="blue", label="Iris-virginica")
 plt.legend()
 plt.show()
draw_result(X, y

img

1) PCA(Principal Component Analysis)

作用:降维、压缩
步骤:

  • XX 均值     
  • XX 减去均值计算协方差矩阵 C = frac{1}{m}XX^TC = frac{1}{m}XX^T 
  • 对协方差矩阵  CC 特征值分解    
  • 从大到小排列 CC 的特征值取前 kk 个特征值对应的特征向量按行组成矩阵即为变换矩阵 P_{ktimes n}P_{ktimes n} 

(a) 手动实现 PCA

class PCA:
 def __init__(self, dimension, train_x):
 # 降维后的维度
 self.dimension = dimension
 # 原始数据集
 self.train_x = train_x
 @property
 def result(self):
 '返回降维后的矩阵'
 # 1. 数据中心化
 data_centering = self.train_x - np.mean(self.train_x, axis=0)
 # 2. 计算协方差矩阵
 cov_matrix = np.cov(data_centering, rowvar=False)
 # 3. 特征值分解
 eigen_val, eigen_vec = np.linalg.eig(cov_matrix)
 # 4. 生成降维后的数据
 p = eigen_vec[:, 0:self.dimension]  # 取特征向量矩阵的前 k 维
 return np.dot(data_centering,p) 
调用方法:
pca = PCA(2,X)
iris_2d = pca.result
draw_result(iris_2d, y

img
(b) sklearn 的 PCA

import numpy as np
from sklearn.decomposition import PCA
pca = PCA(n_components=2)
newX = pca.fit_transform(X)
draw_result(newX, y)

img

2) SVD(Singular Value Decomposition)

作用:特征分解、降维
步骤
img
(a)手动实现 SVD

class SVD:
 def __init__(self, dimension, train_x):
 self.dimension = dimension
 self.train_x = train_x
 @property
 def result(self):
 '返回降维后的矩阵'
 data_centering = self.train_x - np.mean(self.train_x, axis=0)
 # SVD
 U, Sigma, VT = np.linalg.svd(data_centering)
 return np.dot(data_centering, np.transpose(VT)[:, :self.dimension])
调用方法:
svd = SVD(2,X)
iris_svd = svd.result
draw_result(iris_svd,y)

img
(b) sklearn 的 SVD
TruncatedSVD,截断奇异值分解(当数据量非常大,svd 跑不出来时使用此方法)。

from sklearn.decomposition import TruncatedSVD
iris_2d = TruncatedSVD(2).fit_transform(X)
draw_result(iris_2d, y)

img

3)  PCA 和 SVD 的关系

img

4)  Fisher线性判别分析(Linear Discriminant Analysis,LDA)

是有监督的降维,通过最小化类内离散度与最大化类间离散度来获得最优特征子集。 
img

LD1 通过线性判定,可以很好的将呈正态分布的两个类分开。 LD2 的线性判定保持了数据集的较大方差,但 LD2 无法提供关于类别的信息,因此 LD2 不是一个好的线性判定。
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis as LDA
lda = LDA(n_components=2)
iris_2d = lda.fit_transform(X, y)
draw_result(iris_2d, y)

img
LDA 与 PCA 相似:

PCA 试图寻找到方差最大的正交的主成分分量轴 LDA 发现可以最优化分类的特征子空间 LDA 和 PCA 都是可用于降低数据集维度的线性转换技巧 PCA 是无监督算法 LDA 是监督算法 LDA 是一种更优越的用于分类的特征提取技术

5) T-SNE

from sklearn.manifold import TSNE
tsne = TSNE(n_components=2)
iris_2d = tsne.fit_transform(X)
draw_result(iris_2d, y)

img

五 总结

切忌:一开始就把所有的特征一股脑地扔进模型,容易被一些没用的特征误导。
1) EDA
plot,plot,plot,重要的事情说三遍
2) 特征预处理
时间序列:把昨天的特征加入今天的特征,或者把和昨天相比,特征数值的改变量加入今天的特征。
连续特征离散化(决策树类型的模型没意义):一种挺有趣的变种,就是限制浮点数特征的精度,异常数据有很强的鲁棒性,模型也会更稳定。
clipping:可以用 pandas dataframe 的.clip(low, upper)方法,把特征值的取值限制在一定范围内
3) 数据清洗
要合情合理,不可盲目填充缺失值、删除异常值,要建立在统计科学基础上。
4) 特征变换
除非万不得已,不要用 PCA 或者 LDA 降维,建议直接减原始特征。

  • 特征变换要切记

任何针对单独特征列的单调变换(如对数):不适用于决策树类算法。
对于决策树而言,   X、X^3、X^5X、X^3、X^5  之间没有差异 , |X|、X^2、X^4|X|、X^2、X^4 之间没有差异,除非发生了舍入误差。
线性组合(linear combination):仅适用于决策树以及基于决策树的 ensemble(如 gradient boosting, random forest),因为常见的 axis-aligned split function 不擅长捕获不同特征之间的相关性;不适用于 SVM、线性回归、神经网络等。

  • 类别特征与数值特征的组合

用 N1 和 N2 表示数值特征,用 C1 和 C2 表示类别特征,利用 pandas 的 groupby 操作,可以创造出以下几种有意义的新特征:(其中,C2 还可以是离散化了的 N1)

median(N1)_by(C1)   中位数
mean(N1)_by(C1)   算术平均数
mode(N1)_by(C1)   众数
min(N1)_by(C1)   最小值
max(N1)_by(C1)   最大值
std(N1)_by(C1)   标准差
var(N1)_by(C1)   方差
freq(C2)_by(C1)   频数

仅仅将已有的类别和数值特征进行以上的有效组合,就能够大量增加优秀的可用特征。
将这种方法和线性组合等基础特征工程方法结合(仅用于决策树),可以得到更多有意义的特征,如:

N1 - median(N1)_by(C1)
N1 - mean(N1)_by(C1)
  • 用基因编程创造新特征

基于 genetic programming 的 symbolic regression【python 环境下首推基因编程库为 gplearn】。
基因编程的两大用法:
转换(transformation):把已有的特征进行组合转换,组合的方式(一元、二元、多元算子)可以由用户自行定义,也可以使用库中自带的函数(如加减乘除、min、max、三角函数、指数、对数)。组合的目的,是创造出和目标 y 值最“相关”的新特征。
spearman 多用于决策树(免疫单特征单调变换),pearson 多用于线性回归等其他算法。
回归(regression):原理同上,只不过直接用于回归而已。

  • 用决策树创造新特征:

在决策树系列的算法中(单棵决策树、gbdt、随机森林),每一个样本都会被映射到决策树的一片叶子上。因此,我们可以把样本经过每一棵决策树映射后的 index(自然数)或 one-hot-vector(哑编码得到的稀疏矢量)作为一项新的特征,加入到模型中。
具体实现:apply()以及 decision_path()方法,在 scikit-learn 和 xgboost 里都可以用。
5) 模型

  • 树模型:
对特征数值幅度不敏感,可以不进行无量纲化和统计变换处理; 由于数模型依赖于样本距离来进行学习,可以不进行类别特征编码(但字符型特征不能直接作为输入,所以需要至少要进行标签编码)。 LightGBM 和 XGBoost 都能将 NaN 作为数据的一部分进行学习,所以不需要处理缺失值。其他情况下,我们需要使用。
  • 依赖样本距离来学习的模型(如线性回归、SVM、深度学习等):
对于数值型特征需要进行无量纲化处理; 对于一些长尾分布的数据特征,可以做统计变换,使得模型能更好优化; 对于线性模型,特征分箱可以提升模型表达能力;
注:结合工作内容、学习总结以上内容,如有错误,请指出,诚心请教

参考资料

光喻:【持续更新】机器学习特征工程实用技巧大全zhuanlan.zhihu.com图标
特征工程系列:数据清洗 - 大咖驾到 - 博客园www.cnblogs.com图标

作者:王岳 好未来机器学习算法专家

查看原文

赞 2 收藏 2 评论 0

认证与成就

  • 获得 82 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2020-03-05
个人主页被 4.2k 人浏览